Compare commits

..

71 Commits

Author SHA1 Message Date
Rajarshee Chatterjee 1a8d640db1 Add delete downloads dialog and refactor themes 2021-07-17 07:13:00 +05:30
Rajarshee Chatterjee dc6b2deddd Minor fixes 2021-07-16 22:05:49 +05:30
Rajarshee Chatterjee a9fcf2fe08 Fix NovelCover 2021-07-16 17:46:04 +05:30
Rajarshee Chatterjee f195b9e6df Minor fixes 2021-07-16 15:50:01 +05:30
Rajarshee Chatterjee d07fd26cb3 Refactor 2021-07-16 15:22:36 +05:30
Rajarshee Chatterjee 5aee0d1b04 Refactor 2021-07-15 22:51:50 +05:30
Rajarshee Chatterjee c4457d74c4 Parse links in chapter 2021-07-15 08:52:04 +05:30
Rajarshee Chatterjee ba90a6fb84 Refactor sources and show headings 2021-07-14 12:27:07 +05:30
Bread Genie e7424cea06 Fix: source images (#50)
One of the source image was linked to a fork and rest were linked to the old extension repo (this would still work, but for consistency's sake every image is linked to the repo under the org).
2021-07-14 10:19:16 +05:30
Rajarshee Chatterjee e274feda6e Minor fixes 2021-07-13 17:07:55 +05:30
Bread Genie 8ec2378191 feat(source): Add Yushubo (#48) 2021-07-13 17:05:42 +05:30
Rajarshee Chatterjee ef57a98681 Minor fixes 2021-07-12 09:49:05 +05:30
Rajarshee Chatterjee bdda959f36 Improve novel updates 2021-07-11 16:14:17 +05:30
Rajarshee Chatterjee 04f3ba2b45 Refactor themes 2021-07-11 09:43:12 +05:30
Rajarshee Chatterjee f1a261ab28 Add ranobes.net 2021-07-10 10:52:26 +05:30
Rajarshee Chatterjee da2ee3b883 Pause/Resume downloads in queue 2021-07-09 17:58:19 +05:30
Marcin Majewski b98d3861b4 format: Setting Prettier formating in package.json (#47)
Because of different formatting on some computers
Prettier settings were set in package.json file so that everyone
will format their code in the same way using Prettier
2021-07-09 15:13:56 +05:30
Rajarshee Chatterjee ff59330d20 Add Novel Updates 2021-07-09 14:24:26 +05:30
Rajarshee Chatterjee b0639e79a8 Add new sources 2021-07-09 08:51:08 +05:30
Rajarshee Chatterjee 5931a0dc07 Cleanup 2021-07-08 11:22:45 +05:30
Rajarshee Chatterjee cf40acb725 Create multisrc 2021-07-07 14:56:01 +05:30
Rajarshee Chatterjee a2974ccb44 Merge 2021-07-06 20:11:45 +05:30
Rajarshee Chatterjee 5eda558f09 Add LNMTL 2021-07-06 20:07:45 +05:30
Marcin Majewski cf7f1402d3 Fixed Syosetu name and Oneshot not working (#46)
* Fixed Syosetu name and Oneshot not working

Oneshots didn't work at all because of use of scaner from lnreader-sources
Oneshots now load properly desciptions (it didn't work before)
Syosetsu changed into Syosetu as it should be

* Remove unnecessary console.logs
2021-07-06 20:06:36 +05:30
Rajarshee Chatterjee 834a8d9e2e Minor fix 2021-07-06 06:16:28 +05:30
Rajarshee Chatterjee fe5b3f4a5b Add Syosetu 2021-07-06 06:11:30 +05:30
Rajarshee Chatterjee ec4eaf4d49 Remove mipmap-anydpi-v26 folder 2021-07-05 16:15:20 +05:30
Rajarshee Chatterjee 0c0b599452 v1.1.0 2021-07-04 21:04:18 +05:30
Rajarshee Chatterjee ac26c67361 Fix build workflow 2021-07-04 10:45:19 +05:30
Rajarshee Chatterjee d205572a5e Add Gradle cache 2021-07-04 10:36:53 +05:30
Rajarshee Chatterjee e5d52fcd9d Fix build workflow 2021-07-04 10:23:39 +05:30
Rajarshee Chatterjee 61424e533a Minor fixes 2021-07-04 10:16:29 +05:30
Rajarshee Chatterjee c3ef170385 Lightnovelpub: Fix chapterUrl 2021-07-03 16:31:55 +05:30
Rajarshee Chatterjee 430c7a7341 Minor fixes 2021-07-03 14:59:55 +05:30
Rajarshee Chatterjee e4b934061b History and UI fixes 2021-07-03 13:43:14 +05:30
Rajarshee Chatterjee 3579671ade Fix global search and migration 2021-07-03 13:42:48 +05:30
Rajarshee Chatterjee 893d229c25 Add themes 2021-07-03 11:52:31 +05:30
Rajarshee Chatterjee ea3f5ac7ee Add jump to chapter 2021-07-03 11:24:04 +05:30
Rajarshee Chatterjee e4027efbb1 Add update library on start option and save select text option 2021-07-03 10:06:07 +05:30
Rajarshee Chatterjee f5dd7153cc Add details in novel updates & mal 2021-06-30 10:23:26 +05:30
Rajarshee Chatterjee 38b0097261 Add Scribble Hub 2021-06-30 10:22:00 +05:30
Rajarshee Chatterjee 2f7215512f Fix continue reading button 2021-06-27 19:18:00 +05:30
Rajarshee Chatterjee 9a96a34cda Add NovelPassion & Royal Road 2021-06-27 19:17:43 +05:30
Rajarshee Chatterjee a7f6d2cd3c Fix sources 2021-06-27 10:51:27 +05:30
Rajarshee Chatterjee 5dfe6a7ab8 Minor fixes 2021-06-24 07:53:30 +05:30
Rajarshee Chatterjee 3fb35e2d32 v1.1.0 2021-06-20 23:06:06 +05:30
Rajarshee Chatterjee 35d5cf987a Fixes 2021-06-20 23:03:02 +05:30
Rajarshee Chatterjee 7e28d12a8a v1.1.0 2021-06-20 21:46:16 +05:30
Rajarshee Chatterjee ff575de2e8 Fixes 2021-06-20 21:44:58 +05:30
Rajarshee Chatterjee 962fcc534d Minor fixes 2021-06-20 18:19:50 +05:30
Rajarshee Chatterjee acd8843b0e Fix sources 2021-06-20 17:49:53 +05:30
Rajarshee Chatterjee c956d764c5 Add option to search all sources 2021-06-20 17:39:31 +05:30
Rajarshee Chatterjee ca0f62f118 Add pagination 2021-06-20 11:20:40 +05:30
Rajarshee Chatterjee 4d766555b7 Add incognito mode 2021-06-20 08:31:51 +05:30
Rajarshee Chatterjee edcc4c0bcd Use background service for downloads and updates (fixes #36) 2021-06-19 13:47:07 +05:30
Rajarshee Chatterjee 8e1a9274e5 Rename 2021-06-19 09:18:50 +05:30
Rajarshee Chatterjee d0b11c9b47 Rename folders 2021-06-19 09:17:52 +05:30
Rajarshee Chatterjee 3a9784a7a0 Fix launch crash on android 7 2021-06-15 09:46:50 +05:30
Rajarshee Chatterjee 66e3945427 Add option to disbale swipe gestures 2021-06-15 09:46:24 +05:30
Rajarshee Chatterjee 7773d50607 Filter sources by languages 2021-06-13 10:47:41 +05:30
Rajarshee Chatterjee 721d3842fb Fix swipe directions 2021-06-12 20:29:07 +05:30
Rajarshee Chatterjee 773c33f3bf Add swipe gestures 2021-06-12 15:52:42 +05:30
Rajarshee Chatterjee 1addb54690 Vertical Scrollbar 2021-06-12 14:32:34 +05:30
Rajarshee Chatterjee ce29a5456d Html to text 2021-06-11 17:54:07 +05:30
Rajarshee Chatterjee 4eda2d4624 Add action bar and use pressable 2021-06-11 17:53:33 +05:30
Rajarshee Chatterjee bae2fa40f3 Chapter footer 2021-06-11 17:52:34 +05:30
Rajarshee Chatterjee cf5d9e18bb Update all english sources 2021-06-10 21:53:59 +05:30
Rajarshee Chatterjee e87ecb48b4 Add more sources and fix sources 1-9 2021-06-10 18:29:13 +05:30
Rajarshee Chatterjee d4140fcc52 (~_~;) 2021-06-10 15:46:39 +05:30
Rajarshee Chatterjee 4858f22a88 Parse chapter 2021-06-10 11:51:22 +05:30
Rajarshee Chatterjee 6aa977a562 Add ReadLightNovel 2021-06-10 10:33:12 +05:30
211 changed files with 36306 additions and 13378 deletions
-4
View File
@@ -1,4 +0,0 @@
{
"12bb71342c6255bbf50437ec8f4441c083f47cdb74bd89160c15e4f43e52a1cb": true,
"40b842e832070c58deac6aa9e08fa459302ee3f9da492c7e77d93d2fbf4a56fd": true
}
+45
View File
@@ -0,0 +1,45 @@
name: CI
on:
push:
branches:
- main
jobs:
install-and-test:
name: Install dependencies
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v2
- name: Install npm dependencies
run: |
npm install
build-android:
name: Build app
needs: install-and-test
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v2
- name: Install Dependencies
run: |
npm install
- name: Cache Gradle Wrapper
uses: actions/cache@v2
with:
path: ~/.gradle/wrapper
key: ${{ runner.os }}-gradle-wrapper-${{ hashFiles('gradle/wrapper/gradle-wrapper.properties') }}
- name: Cache Gradle Dependencies
uses: actions/cache@v2
with:
path: ~/.gradle/caches
key: ${{ runner.os }}-gradle-caches-${{ hashFiles('gradle/wrapper/gradle-wrapper.properties') }}
restore-keys: |
${{ runner.os }}-gradle-caches-
- name: Make Gradlew Executable
run: cd android && chmod +x ./gradlew
- name: Build Android Release
run: |
cd android && ./gradlew assembleRelease --no-daemon
+3 -3
View File
@@ -7,10 +7,10 @@ import { Provider } from "react-redux";
import { persistor, store } from "./src/redux/store";
import { PersistGate } from "redux-persist/lib/integration/react";
import Main from "./src/Navigators/Main";
import Main from "./src/navigators/Main";
import { createDB, deleteDb } from "./src/Database/DBHelper";
import { fonts } from "./src/Theme/fonts";
import { createDB } from "./src/database/DBHelper";
import { fonts } from "./src/theme/fonts";
const App = () => {
const [loaded] = useFonts(fonts);
+1 -1
View File
@@ -136,7 +136,7 @@ android {
minSdkVersion rootProject.ext.minSdkVersion
targetSdkVersion rootProject.ext.targetSdkVersion
versionCode 1
versionName "1.0.21"
versionName "1.1.1"
}
splits {
abi {
+3 -1
View File
@@ -7,10 +7,11 @@
<uses-permission android:name="android.permission.READ_EXTERNAL_STORAGE"/>
<uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE"/>
<uses-permission android:name="android.permission.RECEIVE_BOOT_COMPLETED"/>
<uses-permission android:name="android.permission.FOREGROUND_SERVICE" />
<uses-permission android:name="android.permission.WAKE_LOCK"/>
<uses-permission android:name="android.permission.ACCESS_NETWORK_STATE"/>
<uses-permission android:name="com.google.android.c2dm.permission.RECEIVE"/>
<application android:name=".MainApplication" android:label="@string/app_name" android:icon="@mipmap/ic_launcher" android:roundIcon="@mipmap/ic_launcher_round" android:allowBackup="true" android:theme="@style/AppTheme">
<application android:name=".MainApplication" android:label="@string/app_name" android:icon="@mipmap/ic_launcher" android:roundIcon="@mipmap/ic_launcher_round" android:allowBackup="true" android:theme="@style/AppTheme" android:usesCleartextTraffic="true">
<meta-data android:name="expo.modules.updates.EXPO_UPDATE_URL" android:value="https://exp.host/@rajarsheechatterjee/LNReader"/>
<meta-data android:name="expo.modules.updates.EXPO_SDK_VERSION" android:value="41.0.0"/>
<meta-data android:name="expo.modules.updates.ENABLED" android:value="false"/>
@@ -32,5 +33,6 @@
</intent-filter>
</activity>
<activity android:name="com.facebook.react.devsupport.DevSettingsActivity"/>
<service android:name="com.asterinet.react.bgactions.RNBackgroundActionsTask" />
</application>
</manifest>
File diff suppressed because one or more lines are too long
@@ -1,62 +0,0 @@
// FullScreenModule.java
package com.rajarsheechatterjee.LNReader;
import android.os.Build;
import android.view.View;
import android.view.WindowManager;
import android.app.Activity;
import com.facebook.react.bridge.NativeModule;
import com.facebook.react.bridge.ReactApplicationContext;
import com.facebook.react.bridge.ReactContext;
import com.facebook.react.bridge.UiThreadUtil;
import com.facebook.react.bridge.ReactContextBaseJavaModule;
import com.facebook.react.bridge.ReactMethod;
public class FullScreenModule extends ReactContextBaseJavaModule {
@Override
public String getName() {
return "FullScreen";
}
@ReactMethod
public void enable() {
UiThreadUtil.runOnUiThread(
new Runnable() {
@Override
public void run() {
getCurrentActivity().getWindow().getDecorView().setSystemUiVisibility(
View.SYSTEM_UI_FLAG_LAYOUT_FULLSCREEN |
View.SYSTEM_UI_FLAG_LAYOUT_HIDE_NAVIGATION |
View.SYSTEM_UI_FLAG_FULLSCREEN |
View.SYSTEM_UI_FLAG_HIDE_NAVIGATION |
View.SYSTEM_UI_FLAG_IMMERSIVE_STICKY
);
}
}
);
}
@ReactMethod
public void disable() {
UiThreadUtil.runOnUiThread(
new Runnable() {
@Override
public void run() {
getCurrentActivity().getWindow().getDecorView().setSystemUiVisibility(
View.SYSTEM_UI_FLAG_LAYOUT_FULLSCREEN |
View.SYSTEM_UI_FLAG_LAYOUT_HIDE_NAVIGATION |
View.SYSTEM_UI_FLAG_LAYOUT_STABLE
);
}
}
);
}
FullScreenModule(ReactApplicationContext reactContext) {
super(reactContext);
}
}
@@ -1,29 +0,0 @@
package com.rajarsheechatterjee.LNReader;
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 FullScreenPackage implements ReactPackage {
@Override
public List<ViewManager> createViewManagers(ReactApplicationContext reactContext) {
return Collections.emptyList();
}
@Override
public List<NativeModule> createNativeModules(
ReactApplicationContext reactContext) {
List<NativeModule> modules = new ArrayList<>();
modules.add(new FullScreenModule(reactContext));
return modules;
}
}
@@ -31,9 +31,6 @@ import java.util.Arrays;
import java.util.List;
import javax.annotation.Nullable;
import com.rajarsheechatterjee.LNReader.FullScreenPackage;
public class MainApplication extends Application implements ReactApplication {
private final ReactModuleRegistryProvider mModuleRegistryProvider = new ReactModuleRegistryProvider(
new BasePackageList().getPackageList()
@@ -49,7 +46,6 @@ 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 FullScreenPackage());
return packages;
}
@@ -8,7 +8,6 @@ public class BasePackageList {
public List<Package> getPackageList() {
return Arrays.<Package>asList(
new expo.modules.application.ApplicationPackage(),
new expo.modules.backgroundfetch.BackgroundFetchPackage(),
new expo.modules.constants.ConstantsPackage(),
new expo.modules.documentpicker.DocumentPickerPackage(),
new expo.modules.errorrecovery.ErrorRecoveryPackage(),
@@ -19,10 +18,8 @@ public class BasePackageList {
new expo.modules.lineargradient.LinearGradientPackage(),
new expo.modules.notifications.NotificationsPackage(),
new expo.modules.permissions.PermissionsPackage(),
new expo.modules.sharing.SharingPackage(),
new expo.modules.splashscreen.SplashScreenPackage(),
new expo.modules.sqlite.SQLitePackage(),
new expo.modules.taskManager.TaskManagerPackage(),
new expo.modules.updates.UpdatesPackage(),
new expo.modules.webbrowser.WebBrowserPackage()
);

Before

Width:  |  Height:  |  Size: 28 KiB

After

Width:  |  Height:  |  Size: 28 KiB

Vendored Regular → Executable
View File
-35
View File
@@ -3,44 +3,9 @@
"name": "LNReader",
"slug": "LNReader",
"scheme": "lnreader",
"version": "1.0.21",
"orientation": "default",
"icon": "./assets/icon.png",
"splash": {
"image": "./assets/splash.png",
"resizeMode": "cover",
"backgroundColor": "#000000"
},
"updates": {
"fallbackToCacheTimeout": 0,
"enabled": false
},
"notification": {
"icon": "./assets/notification.png",
"color": "#00adb5"
},
"assetBundlePatterns": ["**/*"],
"ios": {
"supportsTablet": true
},
"android": {
"adaptiveIcon": {
"foregroundImage": "./assets/icon.png",
"backgroundColor": "#FFFFFF"
},
"package": "com.rajarsheechatterjee.LNReader"
},
"androidStatusBar": {
"barStyle": "light-content",
"backgroundColor": "#00000000",
"translucent": true
},
"androidNavigationBar": {
"barStyle": "light-content",
"backgroundColor": "#000000"
},
"web": {
"favicon": "./assets/favicon.png"
}
}
}
Binary file not shown.
+19712 -8682
View File
File diff suppressed because it is too large Load Diff
+71 -69
View File
@@ -1,71 +1,73 @@
{
"scripts": {
"start": "react-native start",
"android": "react-native run-android",
"ios": "react-native run-ios",
"web": "expo start --web",
"eject": "expo eject"
},
"dependencies": {
"@react-native-async-storage/async-storage": "^1.13.0",
"@react-native-community/masked-view": "0.1.10",
"@react-native-community/netinfo": "6.0.0",
"@react-native-community/slider": "3.0.3",
"@react-navigation/material-bottom-tabs": "^5.3.10",
"@react-navigation/native": "^5.8.10",
"@react-navigation/stack": "^5.12.8",
"expo": "^41.0.1",
"expo-background-fetch": "~9.1.0",
"expo-document-picker": "^9.1.2",
"expo-file-system": "~11.0.2",
"expo-font": "~9.1.0",
"expo-linear-gradient": "~9.1.0",
"expo-linking": "~2.2.3",
"expo-notifications": "~0.11.6",
"expo-permissions": "^12.0.1",
"expo-sharing": "~9.1.2",
"expo-splash-screen": "~0.10.2",
"expo-sqlite": "~9.1.0",
"expo-status-bar": "~1.0.4",
"expo-task-manager": "~9.1.0",
"expo-updates": "~0.5.4",
"expo-web-browser": "~9.1.0",
"moment": "^2.29.1",
"qs": "^6.10.1",
"react": "16.13.1",
"react-dom": "16.13.1",
"react-native": "^0.64.2",
"react-native-cheerio": "^1.0.0-rc.4",
"react-native-collapsible-header-views": "^1.1.2",
"react-native-device-info": "^8.1.3",
"react-native-easing-gradient": "^0.1.2",
"react-native-fast-image": "^8.3.4",
"react-native-fit-image": "^1.5.5",
"react-native-gesture-handler": "~1.10.2",
"react-native-immersive-mode": "^2.0.0",
"react-native-navigation-bar-color": "^2.0.1",
"react-native-offline": "^5.7.0",
"react-native-pager-view": "^5.1.10",
"react-native-paper": "^4.5.0",
"react-native-reanimated": "~2.1.0",
"react-native-safe-area-context": "3.2.0",
"react-native-screens": "~3.0.0",
"react-native-tab-view": "^3.0.1",
"react-native-unimodules": "~0.13.3",
"react-native-vector-icons": "^7.1.0",
"react-native-web": "^0.16.5",
"react-native-webview": "^11.6.2",
"react-redux": "^7.2.2",
"redux": "^4.0.5",
"redux-devtools-extension": "^2.13.9",
"redux-persist": "^6.0.0",
"redux-thunk": "^2.3.0",
"rn-sliding-up-panel": "^2.4.4"
},
"devDependencies": {
"@babel/core": "~7.9.0"
},
"private": true,
"name": "lnreader-eject",
"version": "1.0.0"
"scripts": {
"start": "react-native start",
"android": "react-native run-android",
"ios": "react-native run-ios",
"web": "expo start --web",
"eject": "expo eject"
},
"dependencies": {
"@react-native-async-storage/async-storage": "^1.13.0",
"@react-native-community/masked-view": "0.1.10",
"@react-native-community/netinfo": "6.0.0",
"@react-native-community/slider": "3.0.3",
"@react-navigation/material-bottom-tabs": "^5.3.10",
"@react-navigation/native": "^5.8.10",
"@react-navigation/stack": "^5.12.8",
"expo": "^41.0.1",
"expo-document-picker": "^9.1.2",
"expo-file-system": "~11.0.2",
"expo-font": "~9.1.0",
"expo-linear-gradient": "~9.1.0",
"expo-linking": "~2.2.3",
"expo-notifications": "~0.11.6",
"expo-splash-screen": "^0.11.2",
"expo-sqlite": "~9.1.0",
"expo-updates": "~0.5.4",
"expo-web-browser": "~9.1.0",
"moment": "^2.29.1",
"qs": "^6.10.1",
"react": "16.13.1",
"react-dom": "16.13.1",
"react-native": "^0.64.2",
"react-native-background-actions": "^2.6.1",
"react-native-cheerio": "^1.0.0-rc.4",
"react-native-device-info": "^8.1.3",
"react-native-easing-gradient": "^0.1.2",
"react-native-fast-image": "^8.3.4",
"react-native-fit-image": "^1.5.5",
"react-native-gesture-handler": "~1.10.2",
"react-native-navigation-bar-color": "^2.0.1",
"react-native-pager-view": "^5.1.10",
"react-native-paper": "^4.5.0",
"react-native-reanimated": "^2.1.0",
"react-native-safe-area-context": "3.2.0",
"react-native-screens": "~3.0.0",
"react-native-swipe-gestures": "^1.0.5",
"react-native-tab-view": "^3.0.1",
"react-native-unimodules": "~0.13.3",
"react-native-vector-icons": "^7.1.0",
"react-native-web": "^0.16.5",
"react-native-webview": "^11.6.2",
"react-redux": "^7.2.2",
"redux": "^4.0.5",
"redux-persist": "^6.0.0",
"redux-thunk": "^2.3.0",
"rn-sliding-up-panel": "^2.4.4"
},
"devDependencies": {
"@babel/core": "~7.9.0"
},
"prettier": {
"tabWidth": 4,
"semi": true,
"singleQuote": false,
"quoteProps": "as-needed",
"trailingComma": "es5",
"bracketSpacing": true,
"arrowParens": "always"
},
"private": true,
"name": "lnreader-eject",
"version": "1.0.0"
}
-189
View File
@@ -1,189 +0,0 @@
import React from "react";
import { StyleSheet, View, Text, useWindowDimensions } from "react-native";
import { LinearGradient } from "expo-linear-gradient";
import { TouchableRipple } from "react-native-paper";
import { useSelector } from "react-redux";
import { useSettings, useTheme } from "../Hooks/reduxHooks";
import ListView from "./ListView";
import FastImage from "react-native-fast-image";
import { getDeviceOrientation } from "../Services/utils/helpers";
const NovelCover = ({ item, onPress, libraryStatus }) => {
const theme = useTheme();
const { displayMode, novelsPerRow, showDownloadBadges, showUnreadBadges } =
useSettings();
const window = useWindowDimensions();
const orientation = getDeviceOrientation();
const getNovelsPerRow = () =>
orientation === "landscape" ? 6 : novelsPerRow;
const getHeight = () => (window.width / getNovelsPerRow()) * (4 / 3);
const comfortableTitle = () =>
displayMode === 1 && (
<Text
numberOfLines={2}
style={[
styles.title,
{
color: theme.textColorPrimary,
padding: 4,
},
]}
>
{item.novelName}
</Text>
);
const compactTitle = () =>
displayMode === 0 && (
<View style={styles.titleContainer}>
<LinearGradient
colors={["transparent", "rgba(0,0,0,0.6)"]}
style={styles.linearGradient}
>
<Text
numberOfLines={2}
style={[styles.title, { color: "rgba(255,255,255,1)" }]}
>
{item.novelName}
</Text>
</LinearGradient>
</View>
);
const unreadBadge = () =>
showUnreadBadges &&
item.chaptersUnread && (
<Text
style={[
styles.unreadBadge,
!item.chaptersDownloaded && {
borderTopLeftRadius: 4,
borderBottomLeftRadius: 4,
},
!showDownloadBadges && {
borderRadius: 4,
},
{ backgroundColor: theme.colorAccent },
]}
>
{item.chaptersUnread}
</Text>
);
const downloadBadge = () =>
showDownloadBadges &&
item.chaptersDownloaded && (
<Text
style={[
styles.downloadBadge,
!item.chaptersUnread && {
borderTopRightRadius: 4,
borderBottomRightRadius: 4,
},
!showUnreadBadges && {
borderRadius: 4,
},
]}
>
{item.chaptersDownloaded}
</Text>
);
return displayMode !== 2 ? (
<View style={{ flex: 1 / getNovelsPerRow() }}>
<TouchableRipple
borderless
centered
rippleColor={theme.rippleColor}
style={styles.opac}
onPress={onPress}
>
<>
<FastImage
source={{ uri: item.novelCover }}
style={[
{ height: getHeight(), borderRadius: 4 },
libraryStatus && { opacity: 0.5 },
]}
>
<View style={{ flexDirection: "row", margin: 4 }}>
{downloadBadge()}
{unreadBadge()}
</View>
{compactTitle()}
</FastImage>
{comfortableTitle()}
</>
</TouchableRipple>
</View>
) : (
<ListView
item={item}
downloadBadge={downloadBadge()}
unreadBadge={unreadBadge()}
theme={theme}
onPress={onPress}
/>
);
};
export default NovelCover;
const styles = StyleSheet.create({
titleContainer: {
flex: 1,
justifyContent: "flex-end",
borderRadius: 4,
},
title: {
fontFamily: "pt-sans-bold",
fontSize: 14,
padding: 8,
},
linearGradient: {
bottom: -1,
borderRadius: 4,
},
opac: {
paddingHorizontal: 4.5,
paddingVertical: 4,
borderRadius: 4,
},
extensionIcon: {
width: 42,
height: 42,
borderRadius: 4,
},
listView: {
flex: 1,
paddingVertical: 8,
paddingHorizontal: 12,
flexDirection: "row",
alignItems: "center",
borderRadius: 4,
},
downloadBadge: {
backgroundColor: "#47a84a",
color: "#FFFFFF",
borderTopLeftRadius: 4,
borderBottomLeftRadius: 4,
paddingTop: 2,
paddingHorizontal: 4,
fontSize: 12,
},
unreadBadge: {
color: "#FFFFFF",
borderTopRightRadius: 4,
borderBottomRightRadius: 4,
paddingTop: 2,
paddingHorizontal: 4,
fontSize: 12,
},
});
-10
View File
@@ -1,10 +0,0 @@
import { setStatusBarStyle as setExpoStatusBarStyle } from "expo-status-bar";
import { useTheme } from "../Hooks/reduxHooks";
import changeNavigationBarColor from "react-native-navigation-bar-color";
export const setStatusBarStyle = () => {
const theme = useTheme();
setExpoStatusBarStyle(theme.id === 1 ? "dark" : "light");
changeNavigationBarColor(theme.colorPrimary);
};
-96
View File
@@ -1,96 +0,0 @@
import React from "react";
import { View } from "react-native";
import { createMaterialBottomTabNavigator } from "@react-navigation/material-bottom-tabs";
import MaterialCommunityIcons from "react-native-vector-icons/MaterialCommunityIcons";
import Library from "../Screens/Library/Library";
import Updates from "../Screens/Updates/Updates";
import History from "../Screens/History/History";
import Browse from "../Screens/Browse/Browse";
import More from "../Screens/More/More";
import { useTheme } from "../Hooks/reduxHooks";
const Tab = createMaterialBottomTabNavigator();
const BottomNavigator = () => {
const theme = useTheme();
return (
<View style={{ flex: 1, backgroundColor: theme.colorPrimaryDark }}>
<Tab.Navigator
barStyle={{ backgroundColor: theme.colorPrimary }}
activeColor={theme.colorAccent}
>
<Tab.Screen
name="Library"
component={Library}
options={{
tabBarIcon: ({ color }) => (
<MaterialCommunityIcons
name="book-variant-multiple"
color={color}
size={24}
/>
),
}}
/>
<Tab.Screen
name="Updates"
component={Updates}
options={{
tabBarIcon: ({ color }) => (
<MaterialCommunityIcons
name="alert-decagram-outline"
color={color}
size={24}
/>
),
}}
/>
<Tab.Screen
name="History"
component={History}
options={{
tabBarIcon: ({ color }) => (
<MaterialCommunityIcons
name="history"
color={color}
size={24}
/>
),
}}
/>
<Tab.Screen
name="Browse"
component={Browse}
options={{
tabBarIcon: ({ color }) => (
<MaterialCommunityIcons
name="compass-outline"
color={color}
size={24}
/>
),
}}
/>
<Tab.Screen
name="More"
component={More}
options={{
tabBarIcon: ({ color }) => (
<MaterialCommunityIcons
name="dots-horizontal"
color={color}
size={24}
/>
),
}}
/>
</Tab.Navigator>
</View>
);
};
export default BottomNavigator;
-64
View File
@@ -1,64 +0,0 @@
import React from "react";
import { View } from "react-native";
import { useTheme } from "../Hooks/reduxHooks";
import { createStackNavigator } from "@react-navigation/stack";
import About from "../Screens/More/About";
import Settings from "../Screens/More/Settings";
import TrackerSettings from "../Screens/More/TrackerSettings";
import ReaderSettings from "../Screens/More/ReaderSettings";
import BackupSettings from "../Screens/More/BackupSettings";
import AdvancedSettings from "../Screens/More/AdvancedSettings";
import GeneralSettings from "../Screens/More/GeneralSettings";
const Stack = createStackNavigator();
const stackNavigatorConfig = { headerShown: false };
const SettingsStack = () => {
const theme = useTheme();
return (
<View style={{ flex: 1, backgroundColor: theme.colorPrimaryDark }}>
<Stack.Navigator screenOptions={stackNavigatorConfig}>
<Stack.Screen name="Settings" component={Settings} />
<Stack.Screen
name="GeneralSettings"
component={GeneralSettings}
/>
<Stack.Screen
name="ReaderSettings"
component={ReaderSettings}
/>
<Stack.Screen
name="TrackerSettings"
component={TrackerSettings}
/>
<Stack.Screen
name="BackupSettings"
component={BackupSettings}
/>
<Stack.Screen
name="AdvancedSettings"
component={AdvancedSettings}
/>
</Stack.Navigator>
</View>
);
};
const MoreStack = () => {
const theme = useTheme();
return (
<View style={{ flex: 1, backgroundColor: theme.colorPrimaryDark }}>
<Stack.Navigator screenOptions={stackNavigatorConfig}>
<Stack.Screen name="SettingsStack" component={SettingsStack} />
<Stack.Screen name="About" component={About} />
</Stack.Navigator>
</View>
);
};
export default MoreStack;
-80
View File
@@ -1,80 +0,0 @@
import React from "react";
import { View, Text, Image, StyleSheet } from "react-native";
import { TouchableRipple, Button } from "react-native-paper";
import { useNavigation } from "@react-navigation/native";
const MalCard = ({ theme }) => {
const navigation = useNavigation();
const navigateToExtension = () => navigation.navigate("BrowseMal");
return (
<TouchableRipple
style={styles.extensionCard}
onPress={navigateToExtension}
rippleColor={theme.rippleColor}
>
<>
<Image
source={require("../../../../assets/mal.png")}
style={styles.extensionIcon}
resizeMode="contain"
/>
<View style={styles.extensionDetails}>
<View>
<Text
style={{
color: theme.textColorPrimary,
fontSize: 14,
}}
>
MyAnimeList
</Text>
</View>
<View
style={{ flexDirection: "row", alignItems: "center" }}
>
<Button
labelStyle={{ letterSpacing: 0 }}
uppercase={false}
color={theme.colorAccent}
onPress={navigateToExtension}
>
Browse
</Button>
</View>
</View>
</>
</TouchableRipple>
);
};
export default MalCard;
const styles = StyleSheet.create({
extensionCard: {
flexDirection: "row",
alignItems: "center",
marginVertical: 4,
paddingVertical: 8,
paddingHorizontal: 20,
},
extensionIcon: {
width: 40,
height: 40,
borderRadius: 8,
},
extensionDetails: {
flex: 1,
flexDirection: "row",
justifyContent: "space-between",
alignItems: "center",
marginLeft: 16,
},
sourceStatus: {
color: "#C14033",
fontSize: 12,
marginLeft: 5,
},
});
@@ -1,80 +0,0 @@
import React from "react";
import { View, Text, Image, StyleSheet } from "react-native";
import { TouchableRipple, Button } from "react-native-paper";
import { useNavigation } from "@react-navigation/native";
const NovelUpdatesCard = ({ theme }) => {
const navigation = useNavigation();
const navigateToExtension = () => navigation.navigate("BrowseNovelUpdates");
return (
<TouchableRipple
style={styles.extensionCard}
onPress={navigateToExtension}
rippleColor={theme.rippleColor}
>
<>
<Image
source={require("../../../../assets/novelupdates.png")}
style={styles.extensionIcon}
resizeMode="contain"
/>
<View style={styles.extensionDetails}>
<View>
<Text
style={{
color: theme.textColorPrimary,
fontSize: 14,
}}
>
Novel Updates
</Text>
</View>
<View
style={{ flexDirection: "row", alignItems: "center" }}
>
<Button
labelStyle={{ letterSpacing: 0 }}
uppercase={false}
color={theme.colorAccent}
onPress={navigateToExtension}
>
Browse
</Button>
</View>
</View>
</>
</TouchableRipple>
);
};
export default NovelUpdatesCard;
const styles = StyleSheet.create({
extensionCard: {
flexDirection: "row",
alignItems: "center",
marginVertical: 4,
paddingVertical: 8,
paddingHorizontal: 20,
},
extensionIcon: {
width: 40,
height: 40,
borderRadius: 8,
},
extensionDetails: {
flex: 1,
flexDirection: "row",
justifyContent: "space-between",
alignItems: "center",
marginLeft: 16,
},
sourceStatus: {
color: "#C14033",
fontSize: 12,
marginLeft: 5,
},
});
-115
View File
@@ -1,115 +0,0 @@
import React from "react";
import { View, Text, Image, StyleSheet } from "react-native";
import { TouchableRipple, Button, IconButton } from "react-native-paper";
import { useNavigation } from "@react-navigation/native";
import { useDispatch } from "react-redux";
import { pinSourceAction } from "../../../redux/source/source.actions";
import FastImage from "react-native-fast-image";
const ExtensionCard = ({ item, theme, isPinned }) => {
const { sourceId, sourceName, sourceCover, sourceLanguage, status } = item;
const navigation = useNavigation();
const dispatch = useDispatch();
const navigateToExtension = () =>
navigation.navigate("Extension", {
sourceId: item.sourceId,
sourceName: item.sourceName,
sourceUrl: item.sourceUrl,
});
return (
<TouchableRipple
style={styles.extensionCard}
onPress={navigateToExtension}
rippleColor={theme.rippleColor}
>
<>
<Image
source={{ uri: sourceCover }}
style={styles.extensionIcon}
resizeMode="contain"
/>
<View style={styles.extensionDetails}>
<View>
<Text
style={{
color: theme.textColorPrimary,
fontSize: 14,
}}
>
{sourceName}
</Text>
<View style={{ flexDirection: "row" }}>
<Text
style={{
color: theme.textColorSecondary,
fontSize: 12,
}}
>
{sourceLanguage}
</Text>
<Text style={styles.sourceStatus}>
{status === 0 && "• Down"}
</Text>
</View>
</View>
<View
style={{ flexDirection: "row", alignItems: "center" }}
>
<Button
labelStyle={{ letterSpacing: 0 }}
uppercase={false}
color={theme.colorAccent}
onPress={navigateToExtension}
>
Browse
</Button>
<IconButton
icon={isPinned ? "pin" : "pin-outline"}
animated
size={21}
onPress={() => dispatch(pinSourceAction(sourceId))}
color={
isPinned
? theme.colorAccent
: theme.textColorSecondary
}
style={{ margin: 2 }}
/>
</View>
</View>
</>
</TouchableRipple>
);
};
export default ExtensionCard;
const styles = StyleSheet.create({
extensionCard: {
flexDirection: "row",
alignItems: "center",
marginVertical: 4,
paddingVertical: 8,
paddingHorizontal: 20,
},
extensionIcon: {
width: 40,
height: 40,
borderRadius: 8,
},
extensionDetails: {
flex: 1,
flexDirection: "row",
justifyContent: "space-between",
alignItems: "center",
marginLeft: 16,
},
sourceStatus: {
color: "#C14033",
fontSize: 12,
marginLeft: 5,
},
});
@@ -1,127 +0,0 @@
import React, { useState, useEffect } from "react";
import {
ActivityIndicator,
Button,
FlatList,
StyleSheet,
Text,
View,
} from "react-native";
import * as WebBrowser from "expo-web-browser";
import { ScreenContainer } from "../../../Components/Common";
import { Searchbar } from "../../../Components/Searchbar";
import { useTheme } from "../../../Hooks/reduxHooks";
import { scrapeSearchResults, scrapeTopNovels } from "./NovelUpdatesScraper";
import NovelUpdatesNovelCard from "./NovelUpdatesNovelCard";
const BrowseNovelUpdates = ({ navigation }) => {
const theme = useTheme();
const [novels, setNovels] = useState([]);
const [pageNo, setPageNo] = useState(1);
const [loading, setLoading] = useState(true);
const [searchText, setSearchText] = useState("");
const getNovels = async (pageNo) => {
const data = await scrapeTopNovels(pageNo);
setNovels((novels) => novels.concat(data));
setLoading(false);
};
useEffect(() => {
getNovels(1);
}, []);
const getSearchResults = async () => {
setLoading(true);
const data = await scrapeSearchResults(searchText);
setNovels(data);
setLoading(false);
};
const clearSearchbar = () => {
getNovels();
setLoading(true);
setSearchText("");
};
const isCloseToBottom = ({
layoutMeasurement,
contentOffset,
contentSize,
}) => {
const paddingToBottom = 20;
return (
layoutMeasurement.height + contentOffset.y >=
contentSize.height - paddingToBottom
);
};
return (
<ScreenContainer theme={theme}>
<Searchbar
theme={theme}
placeholder="Search Novel Updates"
backAction="arrow-left"
onBackAction={navigation.goBack}
searchText={searchText}
onChangeText={(text) => setSearchText(text)}
onSubmitEditing={getSearchResults}
clearSearchbar={clearSearchbar}
actions={[
{
icon: "earth",
onPress: () =>
WebBrowser.openBrowserAsync(
"https://www.novelupdates.com/"
),
},
]}
/>
{loading ? (
<View style={{ flex: 1, justifyContent: "center" }}>
<ActivityIndicator size={60} color={theme.colorAccent} />
</View>
) : (
<FlatList
data={novels}
keyExtractor={(item) => item.novelName.toString()}
renderItem={({ item }) => (
<NovelUpdatesNovelCard
novel={item}
theme={theme}
onPress={() =>
navigation.navigate("GlobalSearch", {
novelName: item.novelName,
})
}
/>
)}
onScroll={({ nativeEvent }) => {
if (!searchText && isCloseToBottom(nativeEvent)) {
getNovels(pageNo + 1);
setPageNo((pageNo) => pageNo + 1);
}
}}
ListFooterComponent={
!searchText && (
<View style={{ paddingVertical: 16 }}>
<ActivityIndicator color={theme.colorAccent} />
</View>
)
}
/>
)}
</ScreenContainer>
);
};
export default BrowseNovelUpdates;
const styles = StyleSheet.create({});
@@ -1,72 +0,0 @@
import React from "react";
import { StyleSheet, Pressable, Text, View, FlatList } from "react-native";
import FastImage from "react-native-fast-image";
import { Row } from "../../../Components/Common";
const GenreChip = ({ children, theme }) => (
<Text
style={[
styles.genreChip,
{
color: theme.colorAccent,
borderColor: theme.colorAccent,
},
]}
numberOfLines={1}
>
{children}
</Text>
);
const NovelUpdatesNovelCard = ({ novel, theme, onPress }) => {
return (
<Pressable
style={styles.novelUpdatesCard}
onPress={onPress}
android_ripple={{ color: theme.rippleColor }}
>
<View style={{ flex: 1, flexDirection: "row" }}>
<FastImage
source={{ uri: novel.novelCover }}
style={{ width: 85, height: 120, borderRadius: 4 }}
/>
<View style={{ flex: 1, paddingLeft: 16 }}>
<Text
style={{ color: theme.textColorPrimary, fontSize: 16 }}
>
{novel.novelName}
</Text>
<GenreChip theme={theme}>{novel.genres}</GenreChip>
<Text
style={{
fontSize: 13,
color: theme.textColorPrimary,
}}
numberOfLines={3}
>
{novel.novelSummary}
</Text>
</View>
</View>
</Pressable>
);
};
export default NovelUpdatesNovelCard;
const styles = StyleSheet.create({
novelUpdatesCard: {
flex: 1,
padding: 16,
},
contentContainer: {
flex: 1,
},
genreChip: {
marginVertical: 4,
fontSize: 12,
// fontStyle: "italic",
},
});
@@ -1,88 +0,0 @@
import cheerio from "react-native-cheerio";
const scrapeTopNovels = async (pageNo) => {
const url =
"https://www.novelupdates.com/series-ranking/?rank=sixmonths&pg=" +
pageNo;
let headers = new Headers({
Accept: "application/json",
"Content-Type": "application/json",
"User-Agent":
"'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/83.0.4103.116 Safari/537.36",
});
const res = await fetch(url, {
method: "GET",
headers: headers,
});
const body = await res.text();
const $ = cheerio.load(body);
const novels = [];
$("div.search_main_box_nu").each(function (res) {
const novelCover = $(this).find("img").attr("src");
const novelName = $(this).find(".search_title > a").text();
const genres = $(this)
.find(".search_genre")
.text()
.replace(/\s/g, ", ");
const novelSummary =
$(this).find(".search_genre")[0].nextSibling.nodeValue;
const novel = { novelName, novelCover, genres, novelSummary };
novels.push(novel);
});
return novels;
};
const scrapeSearchResults = async (searchTerm) => {
const url =
"https://www.novelupdates.com/?s=" +
searchTerm +
"&post_type=seriesplans";
let headers = new Headers({
Accept: "application/json",
"Content-Type": "application/json",
"User-Agent":
"'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/83.0.4103.116 Safari/537.36",
});
const res = await fetch(url, {
method: "GET",
headers: headers,
});
const body = await res.text();
const $ = cheerio.load(body);
const novels = [];
$("div.search_main_box_nu").each(function (res) {
const novelCover = $(this).find("img").attr("src");
const novelName = $(this).find(".search_title > a").text();
const genres = $(this)
.find(".search_genre")
.text()
.replace(/\s/g, ", ");
const novelSummary =
$(this).find(".search_genre")[0].nextSibling.nodeValue;
const novel = { novelName, novelCover, genres, novelSummary };
novels.push(novel);
});
return novels;
};
export { scrapeTopNovels, scrapeSearchResults };
-332
View File
@@ -1,332 +0,0 @@
import React, { useState, useEffect, useRef } from "react";
import {
StyleSheet,
View,
Text,
ActivityIndicator,
StatusBar,
} from "react-native";
import { useDispatch, useSelector } from "react-redux";
import Constants from "expo-constants";
import { IconButton, Portal } from "react-native-paper";
import { CollapsibleHeaderScrollView } from "react-native-collapsible-header-views";
import { getChapterFromDB } from "../../Database/queries/ChapterQueries";
import { fetchChapter } from "../../Services/Source/source";
import { showToast } from "../../Hooks/showToast";
import {
usePosition,
useReaderSettings,
useTheme,
useTrackingStatus,
} from "../../Hooks/reduxHooks";
import { updateChaptersRead } from "../../redux/tracker/tracker.actions";
import { insertHistoryAction } from "../../redux/history/history.actions";
import {
errorTextColor,
readerBackground,
readerLineHeight,
readerTextColor,
} from "./readerStyleController";
import { markChapterReadAction } from "../../redux/novel/novel.actions";
import { saveScrollPosition } from "../../redux/preferences/preference.actions";
import { parseChapterNumber } from "../../Services/updates";
import ChapterAppbar from "./components/ChapterAppbar";
import ReaderSheet from "./components/ReaderSheet";
import EmptyView from "../../Components/EmptyView";
import FitImage from "react-native-fit-image";
import {
hideNavigationBar,
showNavigationBar,
} from "react-native-navigation-bar-color";
const Chapter = ({ route, navigation }) => {
const {
sourceId,
chapterId,
chapterUrl,
novelId,
novelUrl,
novelName,
chapterName,
} = route.params;
let scrollViewRef;
let readerSheetRef = useRef(null);
const theme = useTheme();
const dispatch = useDispatch();
const reader = useReaderSettings();
const showScrollPercentage = useSelector(
(state) => state.settingsReducer.showScrollPercentage
);
const { tracker, trackedNovels } = useTrackingStatus();
const position = usePosition(novelId, chapterId);
const isTracked = trackedNovels.find((obj) => obj.novelId === novelId);
const [chapter, setChapter] = useState({});
const [loading, setLoading] = useState(true);
const [error, setError] = useState();
const [scrollPercentage, setScrollPercentage] = useState(0);
const [firstLayout, setFirstLayout] = useState(true);
const [selectText, setSelectText] = useState(false);
const [images, setImages] = useState([]);
const getChapter = async (chapterId) => {
try {
if (chapterId) {
const chapterDownloaded = await getChapterFromDB(chapterId);
if (chapterDownloaded) {
setChapter(chapterDownloaded);
parseImages(chapterDownloaded.chapterText);
} else {
const res = await fetchChapter(
sourceId,
novelUrl,
chapterUrl
);
parseImages(res.chapterText);
setChapter(res);
}
} else {
const res = await fetchChapter(sourceId, novelUrl, chapterUrl);
parseImages(res.chapterText);
setChapter(res);
}
setLoading(false);
} catch (error) {
setError(error.message);
setLoading(false);
showToast(error.message);
}
};
const parseImages = (chapterText) => {
let imageLinks = chapterText.match(/\[https.*?\]/g, "");
let chapterTextWithoutLinks;
if (imageLinks) {
imageLinks = imageLinks.map((link) => link.slice(1, -1));
setImages(imageLinks);
}
};
useEffect(() => {
getChapter(chapterId);
dispatch(insertHistoryAction(novelId, chapterId));
return () => {
StatusBar.setHidden(false);
showNavigationBar();
};
}, []);
const isCloseToBottom = ({
layoutMeasurement,
contentOffset,
contentSize,
}) => {
const paddingToBottom = 40;
return (
layoutMeasurement.height + contentOffset.y >=
contentSize.height - paddingToBottom
);
};
const updateTracker = () => {
const chapterNumber = parseChapterNumber(chapterName);
isTracked &&
chapterNumber &&
Number.isInteger(chapterNumber) &&
chapterNumber > isTracked.my_list_status.num_chapters_read &&
dispatch(
updateChaptersRead(
isTracked.id,
tracker.access_token,
chapterNumber
)
);
};
const onScroll = ({ nativeEvent }) => {
const offsetY = nativeEvent.contentOffset.y;
const position =
nativeEvent.contentOffset.y + nativeEvent.layoutMeasurement.height;
const percentage = Math.round(
(position / nativeEvent.contentSize.height) * 100
);
setScrollPercentage(percentage);
dispatch(saveScrollPosition(offsetY, percentage, chapterId, novelId));
if (isCloseToBottom(nativeEvent)) {
dispatch(markChapterReadAction(chapterId, novelId));
updateTracker();
}
};
const scrollToSavedProgress = () => {
StatusBar.setHidden(true);
hideNavigationBar();
if (position && firstLayout) {
position.percentage < 100 &&
scrollViewRef.getNode().scrollTo({
x: 0,
y: position.position,
animated: false,
});
setFirstLayout(false);
}
};
const readerStyles = [
{
paddingVertical: 16,
paddingBottom: 32,
fontSize: reader.textSize,
color: reader.textColor || readerTextColor(reader.theme),
lineHeight: readerLineHeight(reader.textSize, reader.lineHeight),
textAlign: reader.textAlign,
paddingHorizontal: `${reader.padding}%`,
},
reader.fontFamily && {
fontFamily: reader.fontFamily,
},
];
return (
<>
<CollapsibleHeaderScrollView
headerContainerBackgroundColor="rgba(0,0,0,0.5)"
ref={(ref) => (scrollViewRef = ref)}
CollapsibleHeaderComponent={
<ChapterAppbar
navigation={navigation}
sourceId={sourceId}
novelId={novelId}
novelUrl={novelUrl}
novelName={novelName}
chapterName={chapterName}
chapterId={chapterId}
chapter={chapter}
theme={theme}
readerSheetRef={readerSheetRef}
/>
}
headerHeight={Constants.statusBarHeight + 56}
contentContainerStyle={[
styles.screenContainer,
{ backgroundColor: readerBackground(reader.theme) },
]}
onScroll={onScroll}
showsVerticalScrollIndicator={false}
>
{error ? (
<View style={{ flex: 1, justifyContent: "center" }}>
<EmptyView
icon="(-_-;)・・・"
description={error}
style={{ color: errorTextColor(reader.theme) }}
>
<IconButton
icon="reload"
size={25}
style={{ margin: 0, marginTop: 16 }}
color={errorTextColor(reader.theme)}
onPress={() => {
getChapter(chapterId);
setLoading(true);
setError();
}}
/>
<Text
style={{ color: errorTextColor(reader.theme) }}
>
Retry
</Text>
</EmptyView>
</View>
) : loading ? (
<View style={{ flex: 1, justifyContent: "center" }}>
<ActivityIndicator
size={50}
color={theme.colorAccent}
/>
</View>
) : (
<View style={{ flex: 1 }}>
{images.length > 0 &&
images.map((image, index) => (
<FitImage source={{ uri: image }} />
))}
<Text
style={readerStyles}
onLayout={scrollToSavedProgress}
selectable={selectText}
>
{chapter.chapterText
.trim()
.replace(/\[https.*?\]/g, "")}
</Text>
</View>
)}
<Portal>
<ReaderSheet
theme={theme}
reader={reader}
dispatch={dispatch}
bottomSheetRef={readerSheetRef}
selectText={selectText}
setSelectText={setSelectText}
showScrollPercentage={showScrollPercentage}
/>
</Portal>
</CollapsibleHeaderScrollView>
{showScrollPercentage && (
<Text
style={[
styles.scrollPercentage,
{
color: reader.textColor,
backgroundColor: readerBackground(reader.theme),
},
]}
>
{scrollPercentage + "%"}
</Text>
)}
</>
);
};
export default Chapter;
const styles = StyleSheet.create({
screenContainer: {
flexGrow: 1,
paddingVertical: 10,
},
scrollPercentage: {
width: "100%",
position: "absolute",
bottom: 0,
height: 30,
textAlign: "center",
textAlignVertical: "center",
alignSelf: "center",
fontFamily: "noto-sans",
fontWeight: "bold",
zIndex: 1,
},
});
@@ -1,95 +0,0 @@
import React, { useEffect, useState } from "react";
import { Appbar } from "react-native-paper";
import {
getNextChapterFromDB,
getPrevChapterFromDB,
} from "../../../Database/queries/ChapterQueries";
const ChapterAppbar = ({
navigation,
sourceId,
novelId,
novelName,
novelUrl,
chapterId,
chapterName,
theme,
readerSheetRef,
}) => {
const [nextChapter, setNextChapter] = useState({});
const [prevChapter, setPrevChapter] = useState({});
const setPrevAndNextChap = async () => {
const nextChap = await getNextChapterFromDB(novelId, chapterId);
const prevChap = await getPrevChapterFromDB(novelId, chapterId);
setNextChapter(nextChap);
setPrevChapter(prevChap);
};
useEffect(() => {
setPrevAndNextChap();
}, []);
const navigateToPrevChapter = () =>
navigation.replace("Chapter", {
chapterUrl: prevChapter.chapterUrl,
chapterId: prevChapter.chapterId,
sourceId,
novelUrl,
novelId,
chapterName: prevChapter.chapterName,
novelName,
});
const navigateToNextChapter = () =>
navigation.replace("Chapter", {
chapterUrl: nextChapter.chapterUrl,
sourceId,
novelUrl,
novelId,
chapterId: nextChapter.chapterId,
chapterName: nextChapter.chapterName,
novelName,
});
return (
<Appbar.Header style={{ backgroundColor: "transparent", elevation: 0 }}>
<Appbar.BackAction
onPress={() => navigation.goBack()}
color="#FFFFFF"
size={26}
style={{ marginRight: 0 }}
/>
<Appbar.Content
title={novelName}
titleStyle={{ color: "#FFFFFF" }}
subtitle={chapterName}
subtitleStyle={{ color: "rgba(255, 255, 255, 0.7)" }}
/>
<Appbar.Action
icon="chevron-left"
size={26}
disabled={!prevChapter}
onPress={navigateToPrevChapter}
color="#FFFFFF"
/>
<Appbar.Action
icon="chevron-right"
size={26}
disabled={!nextChapter}
onPress={navigateToNextChapter}
color="#FFFFFF"
/>
<Appbar.Action
icon="dots-vertical"
size={26}
onPress={() => readerSheetRef.current.show()}
color="#FFFFFF"
/>
</Appbar.Header>
);
};
export default ChapterAppbar;
-66
View File
@@ -1,66 +0,0 @@
import React from "react";
import { StyleSheet, View, Image } from "react-native";
import { Appbar } from "../../Components/Appbar";
import { ListItem, ListSection } from "../../Components/List";
import { useTheme } from "../../Hooks/reduxHooks";
const MoreScreen = ({ navigation }) => {
const theme = useTheme();
return (
<>
<Appbar title="More" style={{ elevation: 0 }} />
<View style={{ flex: 1, backgroundColor: theme.colorPrimaryDark }}>
<View style={{ overflow: "hidden", paddingBottom: 4 }}>
<View
style={{
paddingTop: 10,
paddingBottom: 25,
alignItems: "center",
backgroundColor: theme.colorPrimary,
elevation: 4,
}}
>
<Image
source={require("../../../assets/logo.png")}
style={{
height: 80,
width: 80,
tintColor: theme.textColorPrimary,
}}
/>
</View>
</View>
<ListSection>
<ListItem
title="Settings"
icon="cog-outline"
onPress={() =>
navigation.navigate("MoreStack", {
screen: "SettingsStack",
})
}
theme={theme}
/>
<ListItem
title="About"
icon="information-outline"
onPress={() =>
navigation.navigate("MoreStack", {
screen: "About",
})
}
theme={theme}
/>
</ListSection>
</View>
</>
);
};
export default MoreScreen;
const styles = StyleSheet.create({});
@@ -1,80 +0,0 @@
import React, { useState } from "react";
import { StyleSheet, Text } from "react-native";
import { Modal, Portal, TextInput } from "react-native-paper";
const AccentColorModal = ({
accentColorModal,
hideAccentColorModal,
theme,
onPress,
color,
}) => {
const [text, setText] = useState(color);
const [error, setError] = useState();
return (
<Portal>
<Modal
visible={accentColorModal}
onDismiss={() => {
hideAccentColorModal();
setText();
setError();
}}
contentContainerStyle={[
styles.containerStyle,
{ backgroundColor: theme.colorPrimary },
]}
>
<Text
style={[
styles.dialogTitle,
{ color: theme.textColorPrimary },
]}
>
Hex Color Code
</Text>
<TextInput
value={text}
defaultValue={color}
onChangeText={(text) => setText(text)}
onSubmitEditing={() => {
if (text.match(/^#([0-9a-f]{6}|[0-9a-f]{3})$/i)) {
onPress(text);
hideAccentColorModal();
} else {
setError("Enter a valid hex color code");
}
}}
mode="outlined"
theme={{
colors: {
primary: color,
placeholder: theme.textColorHint,
text: theme.textColorPrimary,
background: "transparent",
},
}}
underlineColor={theme.textColorHint}
dense
error={error}
/>
<Text style={{ color: "#FF0033", paddingTop: 8 }}>{error}</Text>
</Modal>
</Portal>
);
};
export default AccentColorModal;
const styles = StyleSheet.create({
containerStyle: {
margin: 30,
padding: 20,
borderRadius: 8,
},
dialogTitle: {
fontSize: 18,
marginBottom: 16,
},
});
-60
View File
@@ -1,60 +0,0 @@
import React from "react";
import { StyleSheet } from "react-native";
import { Portal, Modal, Checkbox } from "react-native-paper";
import { setStatusBarStyle } from "expo-status-bar";
import { setAppTheme } from "../../../redux/settings/settings.actions";
import ImmersiveMode from "react-native-immersive-mode";
const ThemeModal = ({ themeModalVisible, hidethemeModal, dispatch, theme }) => {
const themes = [
{ id: 0, name: "AMOLED Dark", statusBar: "light" },
{ id: 1, name: "Light", statusBar: "dark" },
{ id: 2, name: "Dark", statusBar: "light" },
{ id: 3, name: "Midnight Dusk", statusBar: "light" },
{ id: 4, name: "Lime", statusBar: "light" },
{ id: 5, name: "Iris Blue", statusBar: "light" },
];
const renderThemeCheckboxes = () =>
themes.map((item) => (
<Checkbox.Item
key={item.id}
label={item.name}
labelStyle={{ color: theme.textColorPrimary }}
status={theme.id === item.id ? "checked" : "unchecked"}
mode="ios"
color={theme.colorAccent}
onPress={() => {
dispatch(setAppTheme(item.id));
// setStatusBarStyle(item.statusBar);
ImmersiveMode.setBarColor(item.colorPrimary);
}}
/>
));
return (
<Portal>
<Modal
visible={themeModalVisible}
onDismiss={hidethemeModal}
contentContainerStyle={[
styles.containerStyle,
{ backgroundColor: theme.colorPrimaryDark },
]}
>
{renderThemeCheckboxes()}
</Modal>
</Portal>
);
};
export default ThemeModal;
const styles = StyleSheet.create({
containerStyle: {
padding: 20,
margin: 20,
borderRadius: 6,
},
});
@@ -1,176 +0,0 @@
import React, { useState } from "react";
import { StyleSheet, View, Text, Animated } from "react-native";
import { List } from "react-native-paper";
import Bottomsheet from "rn-sliding-up-panel";
import BottomSheetHandle from "../../../Components/BottomSheetHandle";
import { ListItem } from "../../../Components/List";
import {
markPreviousChaptersReadAction,
markPreviousChaptersUnreadAction,
} from "../../../redux/novel/novel.actions";
const ChapterActionsSheet = ({
bottomSheetRef,
selected,
theme,
markChapterReadAction,
markChapterUnreadAction,
dispatch,
bookmarkChapterAction,
setSelected,
}) => {
const [animatedValue] = useState(new Animated.Value(0));
return (
<Bottomsheet
animatedValue={animatedValue}
ref={bottomSheetRef}
draggableRange={{ top: 270, bottom: 0 }}
snappingPoints={[0, 270]}
>
<View
style={[
styles.contentContainer,
{ backgroundColor: theme.colorPrimary },
]}
>
<Text
style={{
color: theme.textColorPrimary,
textAlign: "center",
paddingTop: 16,
paddingBottom: 8,
fontSize: 16,
}}
numberOfLines={1}
>
{selected?.chapterName}
</Text>
<List.Item
title="Mark previous chapters as read"
titleStyle={{ color: theme.textColorPrimary, fontSize: 15 }}
left={() => (
<List.Icon
color={theme.textColorPrimary}
icon="eye-check"
style={{ marginVertical: 0 }}
/>
)}
onPress={() => {
dispatch(
markPreviousChaptersReadAction(
selected?.chapterId,
selected?.novelId
)
);
// bottomSheetRef.current.hide();
}}
rippleColor={theme.rippleColor}
/>
<List.Item
title="Mark previous chapters as unread"
titleStyle={{ color: theme.textColorPrimary, fontSize: 15 }}
left={() => (
<List.Icon
color={theme.textColorPrimary}
icon="eye-off"
style={{ marginVertical: 0 }}
/>
)}
onPress={() => {
dispatch(
markPreviousChaptersUnreadAction(
selected?.chapterId,
selected?.novelId
)
);
// bottomSheetRef.current.hide();
}}
rippleColor={theme.rippleColor}
/>
<List.Item
title={
selected?.read
? "Mark chapter as unread"
: "Mark chapter as read"
}
titleStyle={{ color: theme.textColorPrimary, fontSize: 15 }}
left={() => (
<List.Icon
color={theme.textColorPrimary}
icon={selected?.read ? "eye-off" : "eye"}
style={{ marginVertical: 0 }}
/>
)}
onPress={() => {
if (selected?.read) {
dispatch(
markChapterUnreadAction(selected?.chapterId)
);
setSelected((selected) => ({
...selected,
read: 0,
}));
} else {
dispatch(
markChapterReadAction(
selected?.chapterId,
selected?.novelId
)
);
setSelected((selected) => ({
...selected,
read: 1,
}));
}
// bottomSheetRef.current.hide();
}}
rippleColor={theme.rippleColor}
/>
<List.Item
title={
selected?.bookmark
? "Remove bookmark"
: "Bookmark chapter"
}
titleStyle={{ color: theme.textColorPrimary, fontSize: 15 }}
left={() => (
<List.Icon
color={theme.textColorPrimary}
icon={
selected?.bookmark ? "bookmark-off" : "bookmark"
}
style={{ marginVertical: 0 }}
/>
)}
onPress={() => {
dispatch(
bookmarkChapterAction(
selected?.bookmark,
selected?.chapterId
)
);
setSelected((selected) => ({
...selected,
bookmark: !selected.bookmark,
}));
// bottomSheetRef.current.hide();
}}
rippleColor={theme.rippleColor}
/>
</View>
</Bottomsheet>
);
};
export default ChapterActionsSheet;
const styles = StyleSheet.create({
contentContainer: {
flex: 1,
borderTopRightRadius: 8,
borderTopLeftRadius: 8,
},
});
-252
View File
@@ -1,252 +0,0 @@
import React, { useState } from "react";
import {
View,
Text,
ActivityIndicator,
StyleSheet,
Pressable,
} from "react-native";
import { TouchableRipple, IconButton, Menu } from "react-native-paper";
import { Row } from "../../../Components/Common";
import {
deleteChapterAction,
downloadChapterAction,
} from "../../../redux/novel/novel.actions";
import { parseChapterNumber } from "../../../Services/utils/helpers";
const ChapterItem = ({
novelUrl,
novelName,
chapter,
sourceId,
dispatch,
theme,
navigation,
downloading,
index,
position,
selected,
setSelected,
showChapterTitles,
chapterActionsSheetRef,
}) => {
const [deleteChapterMenu, setDeleteChapterMenu] = useState(false);
const showDeleteChapterMenu = () => setDeleteChapterMenu(true);
const hideDeleteChapterMenu = () => setDeleteChapterMenu(false);
const navigateToChapter = () =>
navigation.navigate("Chapter", {
sourceId,
novelUrl,
novelId: chapter.novelId,
chapterId: chapter.chapterId,
chapterUrl: chapter.chapterUrl,
chapterName: chapter.chapterName,
novelName: novelName,
});
const displayDownloadButton = () => {
if (downloading.indexOf(chapter.chapterId) !== -1) {
return (
<ActivityIndicator
color={theme.textColorHint}
size={25}
style={{ margin: 3.5, padding: 5 }}
/>
);
} else if (chapter.downloaded === 1) {
return (
<Menu
visible={deleteChapterMenu}
onDismiss={hideDeleteChapterMenu}
anchor={
<IconButton
icon="check-circle"
animated
color={theme.textColorPrimary}
size={25}
onPress={showDeleteChapterMenu}
style={{ margin: 2 }}
/>
}
contentStyle={{ backgroundColor: theme.menuColor }}
>
<Menu.Item
onPress={() =>
dispatch(
deleteChapterAction(
chapter.chapterId,
chapter.chapterName
)
)
}
title="Delete"
titleStyle={{ color: theme.textColorPrimary }}
/>
</Menu>
);
} else {
return (
<IconButton
icon="arrow-down-circle-outline"
animated
color={theme.textColorHint}
size={25}
onPress={() =>
dispatch(
downloadChapterAction(
sourceId,
novelUrl,
chapter.chapterUrl,
chapter.chapterName,
chapter.chapterId
)
)
}
style={{ margin: 2 }}
/>
);
}
};
const bookmarkButton = () => {
if (chapter.bookmark) {
return (
<IconButton
icon="bookmark"
color={theme.colorAccent}
size={18}
style={{ marginLeft: 2 }}
/>
);
}
};
const isSelected = (chapterId) => {
return selected.some((obj) => obj.chapterId === chapterId);
};
const onPress = () => {
if (selected.length === 0) {
navigateToChapter();
} else {
if (isSelected(chapter.chapterId)) {
setSelected((selected) =>
selected.filter(
(item) => item.chapterId !== chapter.chapterId
)
);
} else {
setSelected((selected) => [...selected, chapter]);
}
}
};
const onLongPress = () => {
if (isSelected(chapter.chapterId)) {
setSelected((selected) =>
selected.filter((item) => item.chapterId !== chapter.chapterId)
);
} else {
setSelected((selected) => [...selected, chapter]);
}
};
return (
<Pressable
style={[
styles.chapterCardContainer,
isSelected(chapter.chapterId) && {
backgroundColor: theme.rippleColor,
},
]}
onPress={onPress}
onLongPress={onLongPress}
android_ripple={{ color: theme.rippleColor }}
>
<Row>
{bookmarkButton()}
<View>
<Text
style={[
{ color: theme.textColorPrimary },
chapter.read && {
color: theme.textColorHint,
},
]}
numberOfLines={1}
>
{showChapterTitles
? parseChapterNumber(chapter.chapterName)
? "Chapter " +
parseChapterNumber(chapter.chapterName)
: "Chapter " + index
: chapter.chapterName.substring(0, 50)}
</Text>
<View
style={{
flexDirection: "row",
marginTop: 5,
}}
>
{chapter.releaseDate && (
<Text
style={[
{
color: theme.textColorSecondary,
fontSize: 12,
},
chapter.read && {
color: theme.textColorHint,
},
]}
numberOfLines={1}
>
{chapter.releaseDate}
</Text>
)}
{position &&
!chapter.read &&
position[chapter.chapterId] &&
position[chapter.chapterId].percentage < 100 &&
position[chapter.chapterId].percentage > 0 && (
<Text
style={[
{
color: theme.textColorHint,
fontSize: 12,
marginLeft: 0,
},
chapter.releaseDate && {
marginLeft: 5,
},
]}
numberOfLines={1}
>
{chapter.releaseDate && "• "}
{"Progress " +
position[chapter.chapterId].percentage +
"%"}
</Text>
)}
</View>
</View>
</Row>
<View>{displayDownloadButton()}</View>
</Pressable>
);
};
export default ChapterItem;
const styles = StyleSheet.create({
chapterCardContainer: {
paddingHorizontal: 15,
paddingVertical: 7,
flexDirection: "row",
justifyContent: "space-between",
alignItems: "center",
},
});
-17
View File
@@ -1,17 +0,0 @@
import React from "react";
import { Appbar as MaterialAppbar } from "react-native-paper";
import { updateLibraryAction } from "../../../redux/updates/updates.actions";
export const Appbar = ({ theme, dispatch }) => (
<MaterialAppbar.Header style={{ backgroundColor: theme.colorPrimary }}>
<MaterialAppbar.Content
title="Updates"
titleStyle={{ color: theme.textColorPrimary }}
/>
<MaterialAppbar.Action
icon="reload"
onPress={() => dispatch(updateLibraryAction())}
/>
</MaterialAppbar.Header>
);
@@ -1,66 +0,0 @@
import React from "react";
import { View, Text, Image, StyleSheet } from "react-native";
import FastImage from "react-native-fast-image";
import { TouchableRipple } from "react-native-paper";
const UpdateCard = ({ item, theme, onPress }) => {
return (
<TouchableRipple
style={styles.updateCard}
onPress={onPress}
rippleColor={theme.rippleColor}
borderless
>
<>
<FastImage
source={{ uri: item.novelCover }}
style={styles.updateIcon}
/>
<View style={styles.chapterDetails}>
<Text
style={[
{ color: theme.textColorPrimary, fontSize: 14 },
item.read && {
color: theme.textColorHint,
},
]}
numberOfLines={1}
>
{item.novelName}
</Text>
<Text
style={[
{ color: theme.textColorPrimary, fontSize: 12 },
item.read && {
color: theme.textColorHint,
},
]}
numberOfLines={1}
>
{item.chapterName}
</Text>
</View>
</>
</TouchableRipple>
);
};
export default UpdateCard;
const styles = StyleSheet.create({
updateCard: {
flexDirection: "row",
alignItems: "center",
paddingVertical: 8,
paddingHorizontal: 16,
},
updateIcon: {
width: 42,
height: 42,
borderRadius: 4,
},
chapterDetails: {
flex: 1,
marginLeft: 16,
},
});
-55
View File
@@ -1,55 +0,0 @@
import { showToast } from "../../Hooks/showToast";
export const fetchSources = async () => {
const url = `https://lnreader-extensions.vercel.app/api/`;
let res = await fetch(url);
let sources = await res.json();
return sources;
};
export const fetchNovel = async (sourceId, novelUrl) => {
const url = `https://lnreader-extensions.vercel.app/api/${sourceId}/novel/${novelUrl}`;
let data = await fetch(url);
let res = await data.json();
const novel = {
novelUrl: res.novelUrl,
sourceUrl: res.sourceUrl,
source: res.sourceName,
sourceId: res.extensionId,
novelName: res.novelName,
novelCover: res.novelCover,
novelSummary: res.novelSummary,
author: res["Author(s)"],
artist: res["Artist(s)"],
status: res.Status,
genre: res["Genre(s)"],
followed: 0,
chapters: res.novelChapters,
};
return novel;
};
export const fetchChapter = async (sourceId, novelUrl, chapterUrl) => {
const url = `https://lnreader-extensions.vercel.app/api/${sourceId}/novel/${novelUrl}${chapterUrl}`;
let res = await fetch(url);
let chapter = await res.json();
return chapter;
};
export const fetchChapters = async (sourceId, novelUrl) => {
const url = `https://lnreader-extensions.vercel.app/api/${sourceId}/novel/${novelUrl}`;
let data = await fetch(url);
let res = await data.json();
const chapters = res.novelChapters;
return chapters;
};
-159
View File
@@ -1,159 +0,0 @@
import * as SQLite from "expo-sqlite";
const db = SQLite.openDatabase("lnreader.db");
import { getLibrary } from "../Database/queries/LibraryQueries";
import { fetchChapters, fetchNovel } from "../Services/Source/source";
import * as Notifications from "expo-notifications";
Notifications.setNotificationHandler({
handleNotification: async () => {
return {
shouldShowAlert: true,
shouldPlaySound: true,
shouldSetBadge: true,
};
},
});
const updateNovelDetails = async (novel, novelId) => {
db.transaction((tx) => {
tx.executeSql(
"UPDATE novels SET novelName = ?, novelCover = ?, novelSummary = ?, author = ?, artist = ?, genre = ?, status = ? WHERE novelId = ?",
[
novel.novelName,
novel.novelCover,
novel.novelSummary,
novel.author,
novel.artist,
novel.genre,
novel.status,
novelId,
],
(txObj, res) => {},
(txObj, error) => console.log("Error ", error)
);
});
};
export const updateNovel = async (extensionId, novelUrl, novelId) => {
let novel = await fetchNovel(extensionId, novelUrl);
await updateNovelDetails(novel, novelId);
db.transaction((tx) => {
novel.chapters.map((chapter) =>
tx.executeSql(
"INSERT OR IGNORE INTO chapters (chapterUrl, chapterName, releaseDate, novelId) values (?, ?, ?, ?)",
[
chapter.chapterUrl,
chapter.chapterName,
chapter.releaseDate,
novelId,
],
(txObj, { insertId }) => {
if (insertId !== -1) {
tx.executeSql(
"INSERT OR IGNORE INTO updates (chapterId, novelId, updateTime) values (?, ?, (datetime('now','localtime')))",
[insertId, novelId],
(txObj, res) => {
console.log(
"Inserted Chapter Id -> " +
insertId +
" Novel Id " +
novelId
);
},
(txObj, error) => console.log("Error ", error)
);
}
},
(txObj, error) => console.log("Error ", error)
)
);
});
};
export const updateNovelChapters = async (extensionId, novelUrl, novelId) => {
let chapters = await fetchChapters(extensionId, novelUrl);
db.transaction((tx) => {
chapters.map((chapter) =>
tx.executeSql(
"INSERT OR IGNORE INTO chapters (chapterUrl, chapterName, releaseDate, novelId) values (?, ?, ?, ?)",
[
chapter.chapterUrl,
chapter.chapterName,
chapter.releaseDate,
novelId,
],
(txObj, { insertId }) => {
if (insertId !== -1) {
console.log(
"Inserted Chapter " +
chapter.chapterUrl +
" Chapter Name " +
chapter.chapterName +
" Novel Id " +
novelId
);
tx.executeSql(
"INSERT OR IGNORE INTO updates (chapterId, novelId, updateTime) values (?, ?, (datetime('now','localtime')))",
[insertId, novelId],
(txObj, res) => {},
(txObj, error) => console.log("Error ", error)
);
}
},
(txObj, error) => console.log("Error ", error)
)
);
});
};
export const updateAllNovels = async () => {
const libraryNovels = await getLibrary();
Notifications.scheduleNotificationAsync({
identifier: "updatingNotif",
content: { title: "Updating library" },
trigger: null,
});
libraryNovels.map((novel, index) =>
setTimeout(async () => {
await updateNovelChapters(
novel.sourceId,
novel.novelUrl,
novel.novelId
);
console.log(novel.novelName + " Updated");
if (index + 1 === libraryNovels.length) {
Notifications.scheduleNotificationAsync({
content: {
title: "Library Updated",
body: libraryNovels.length + " novels updated",
},
trigger: null,
});
}
}, 3000 * index)
);
};
export const parseChapterNumber = (chapterName) => {
chapterName = chapterName.toLowerCase();
chapterName = chapterName.replace(/volume (\d+)/, "");
const basic = chapterName.match(/ch (\d+)/);
const occurrence = chapterName.match(/\d+/);
if (basic) {
return basic[0];
} else if (occurrence) {
return occurrence[0];
} else {
return 0;
}
};
-142
View File
@@ -1,142 +0,0 @@
/**
* Application Colors
*/
export const theme = {
colorPrimary: "#000000",
colorPrimaryDark: "#000000",
colorAccent: "#3399FF",
textColorPrimary: "#FFFFFF",
textColorSecondary: "rgba(255,255,255,0.7)",
textColorHint: "rgba(255,255,255,0.5)",
rippleColor: "rgba(255,255,255,0.06)",
};
/**
* Light
*/
export const lightTheme = {
id: 1,
name: "Light",
colorPrimary: "#FFFFFF",
colorPrimaryDark: "#FAFAFA",
colorAccent: "#2979FF",
textColorPrimary: "#000000",
textColorSecondary: "rgba(0,0,0,0.54)",
textColorHint: "rgba(0,0,0,0.38)",
rippleColor: "rgba(0,0,0,0.06)",
searchBarColor: "#FFFFFF",
menuColor: "#FFFFFF",
dividerColor: "rgba(0,0,0,0.1)",
filterColor: "#FFC107",
colorDisabled: "rgba(220,220,220,255)",
colorButtonText: "#FFFFFF",
statusBar: "dark",
};
/**
* Dark
*/
export const darkTheme = {
id: 2,
name: "Dark",
colorPrimary: "#242529",
colorPrimaryDark: "#202125",
colorAccent: "#3399FF",
textColorPrimary: "#FFFFFF",
textColorSecondary: "rgba(255,255,255,0.7)",
textColorHint: "rgba(255,255,255,0.5)",
rippleColor: "rgba(255,255,255,0.06)",
searchBarColor: "#303135",
menuColor: "#242529",
filterColor: "#FFEB3B",
dividerColor: "rgba(255,255,255,0.1)",
colorDisabled: "rgba(57,57,57,255)",
colorButtonText: "#FFFFFF",
statusBar: "light",
};
/**
* Amoled Dark
*/
export const amoledDarkTheme = {
id: 0,
name: "AMOLED Dark",
colorPrimary: "#000000",
colorPrimaryDark: "#000000",
colorAccent: "#3399FF",
textColorPrimary: "#FFFFFF",
textColorSecondary: "rgba(255,255,255,0.7)",
textColorHint: "rgba(255,255,255,0.5)",
rippleColor: "rgba(255,255,255,0.12)",
searchBarColor: "#1F1F1F",
menuColor: "#242529",
filterColor: "#FFEB3B",
dividerColor: "rgba(255,255,255,0.15)",
colorDisabled: "rgba(33,33,33,255)",
colorButtonText: "#FFFFFF",
statusBar: "light",
};
/**
* Midnight Dusk
*/
export const midnightDuskTheme = {
id: 3,
name: "Midnight Dusk",
colorPrimary: "#201F27",
colorPrimaryDark: "#16151D",
colorAccent: "#F02475",
textColorPrimary: "#FFFFFF",
textColorSecondary: "rgba(255,255,255,0.7)",
textColorHint: "rgba(255,255,255,0.5)",
rippleColor: "rgba(255,255,255,0.06)",
searchBarColor: "#201F27",
menuColor: "#201F27",
filterColor: "#FFEB3B",
dividerColor: "rgba(255,255,255,0.1)",
colorDisabled: "rgba(57,57,57,255)",
colorButtonText: "#FFFFFF",
statusBar: "light",
};
/**
* Lime
*/
export const limeTheme = {
id: 4,
name: "Lime",
colorPrimary: "#201F27",
colorPrimaryDark: "#16151D",
colorAccent: "#4AF88A",
textColorPrimary: "#FFFFFF",
textColorSecondary: "rgba(255,255,255,0.7)",
textColorHint: "rgba(255,255,255,0.5)",
rippleColor: "rgba(255,255,255,0.06)",
searchBarColor: "#201F27",
menuColor: "#201F27",
filterColor: "#FFEB3B",
dividerColor: "rgba(255,255,255,0.1)",
colorDisabled: "rgba(57,57,57,255)",
colorButtonText: "#16151D",
statusBar: "light",
};
export const irisBlueTheme = {
id: 5,
name: "Iris Blue",
colorPrimary: "#393e46",
colorPrimaryDark: "#222831",
colorAccent: "#00adb5",
textColorPrimary: "#FFFFFF",
textColorSecondary: "rgba(255,255,255,0.7)",
textColorHint: "rgba(255,255,255,0.5)",
rippleColor: "rgba(255,255,255,0.06)",
searchBarColor: "#393e46",
menuColor: "#393e46",
filterColor: "#FFEB3B",
dividerColor: "rgba(255,255,255,0.1)",
colorDisabled: "#393e46",
colorButtonText: "#16151D",
statusBar: "light",
};
+42
View File
@@ -0,0 +1,42 @@
import React from "react";
import { Dimensions, StyleSheet, View } from "react-native";
import { IconButton } from "react-native-paper";
export const Actionbar = ({ actions, theme }) => {
return (
<View
style={[
styles.actionbarContainer,
{ backgroundColor: theme.colorPrimary },
]}
>
{actions.map(
(action, index) =>
action.icon && (
<IconButton
key={index}
icon={action.icon}
color={theme.textColorPrimary}
onPress={action.onPress}
/>
)
)}
</View>
);
};
const styles = StyleSheet.create({
actionbarContainer: {
position: "absolute",
width: Dimensions.get("window").width - 32,
bottom: 0,
margin: 16,
borderRadius: 6,
flexDirection: "row",
justifyContent: "space-between",
paddingHorizontal: 8,
paddingVertical: 4,
elevation: 2,
},
});
@@ -10,6 +10,7 @@ const ColorPickerModal = ({
onSubmit,
hideModal,
modalVisible,
showAccentColors,
}) => {
const [text, setText] = useState(color);
const [error, setError] = useState();
@@ -10,7 +10,7 @@ const EmptyView = ({ icon, description, style, children, iconStyle }) => {
<Text
style={[
styles.emptyViewIcon,
{ color: theme.textColorSecondary },
{ color: theme.textColorHint },
style,
iconStyle,
]}
@@ -20,7 +20,7 @@ const EmptyView = ({ icon, description, style, children, iconStyle }) => {
<Text
style={[
styles.emptyViewText,
{ color: theme.textColorSecondary },
{ color: theme.textColorHint },
style,
]}
>
@@ -17,7 +17,7 @@ const ErrorView = ({ theme, onRetry, error, openWebView }) => (
icon="reload"
size={25}
style={{ margin: 0, marginTop: 24 }}
color={theme.textColorSecondary}
color={theme.text}
onPress={onRetry}
/>
<Text style={{ color: theme.textColorPrimary }}>Retry</Text>
@@ -1,5 +1,5 @@
import React from "react";
import { StyleSheet } from "react-native";
import { StyleSheet, View } from "react-native";
import { List, Divider as PaperDivider } from "react-native-paper";
import MaterialCommunityIcons from "react-native-vector-icons/MaterialCommunityIcons";
@@ -34,15 +34,19 @@ const ListItem = ({
description={description}
descriptionStyle={{ color: theme.textColorSecondary }}
descriptionNumberOfLines={1}
left={() =>
icon && (
<List.Icon
color={theme.colorAccent}
icon={icon}
style={{ marginVertical: 0 }}
/>
)
}
left={() => (
<View style={{ justifyContent: "center" }}>
{icon && (
<List.Icon
color={theme.colorAccent}
icon={icon}
style={{
marginVertical: 0,
}}
/>
)}
</View>
)}
right={() =>
right && (
<MaterialCommunityIcons
@@ -3,7 +3,14 @@ import { StyleSheet, View, Text, Image } from "react-native";
import FastImage from "react-native-fast-image";
import { TouchableRipple } from "react-native-paper";
const ListView = ({ item, downloadBadge, unreadBadge, theme, onPress }) => {
const ListView = ({
item,
downloadBadge,
unreadBadge,
inLibraryBadge,
theme,
onPress,
}) => {
return (
<TouchableRipple
borderless
@@ -15,7 +22,10 @@ const ListView = ({ item, downloadBadge, unreadBadge, theme, onPress }) => {
<>
<FastImage
source={{ uri: item.novelCover }}
style={styles.extensionIcon}
style={[
styles.extensionIcon,
inLibraryBadge && { opacity: 0.5 },
]}
/>
<Text
style={[
@@ -29,6 +39,7 @@ const ListView = ({ item, downloadBadge, unreadBadge, theme, onPress }) => {
<View style={styles.badgeContainer}>
{downloadBadge}
{unreadBadge}
{inLibraryBadge}
</View>
</>
</TouchableRipple>
@@ -0,0 +1,8 @@
import React from "react";
import { ActivityIndicator, View } from "react-native";
export const LoadingScreen = ({ theme }) => (
<View style={{ flex: 1, justifyContent: "center" }}>
<ActivityIndicator size={50} color={theme.colorAccent} />
</View>
);
@@ -2,7 +2,7 @@ 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";
import { useTheme } from "../hooks/reduxHooks";
const NewUpdateDialog = ({ newVersion }) => {
const [newUpdateDialog, showNewUpdateDialog] = useState(true);
+282
View File
@@ -0,0 +1,282 @@
import React, { useCallback } from "react";
import {
StyleSheet,
View,
Text,
useWindowDimensions,
Pressable,
} from "react-native";
import { LinearGradient } from "expo-linear-gradient";
import FastImage from "react-native-fast-image";
import ListView from "./ListView";
import { getDeviceOrientation } from "../services/utils/helpers";
import { useSettings } from "../hooks/reduxHooks";
const NovelCover = ({ item, onPress, libraryStatus, theme }) => {
const { displayMode, novelsPerRow, showDownloadBadges, showUnreadBadges } =
useSettings();
const window = useWindowDimensions();
const orientation = getDeviceOrientation();
const getNovelsPerRow = () =>
orientation === "landscape" ? 6 : novelsPerRow;
const getHeight = useCallback(
() => (window.width / getNovelsPerRow()) * (4 / 3),
[]
);
return displayMode !== 2 ? (
<View
style={{
flex: 1 / getNovelsPerRow(),
borderRadius: 4,
overflow: "hidden",
}}
>
<Pressable
android_ripple={{ color: theme.colorAccent }}
style={styles.opac}
onPress={onPress}
>
<>
<View style={styles.badgeContainer}>
{libraryStatus && <InLibraryBadge theme={theme} />}
{showDownloadBadges && item.chaptersDownloaded && (
<DownloadBadge
showUnreadBadges={showUnreadBadges}
chaptersDownloaded={item.chaptersDownloaded}
chaptersUnread={item.chaptersUnread}
/>
)}
{showUnreadBadges && item.chaptersUnread && (
<UnreadBadge
theme={theme}
chaptersDownloaded={item.chaptersDownloaded}
chaptersUnread={item.chaptersUnread}
showDownloadBadges={showDownloadBadges}
/>
)}
</View>
<FastImage
source={{ uri: item.novelCover }}
style={[
{ height: getHeight(), borderRadius: 4 },
libraryStatus && { opacity: 0.5 },
]}
/>
<View style={styles.compactTitleContainer}>
{displayMode === 0 && (
<CompactTitle novelName={item.novelName} />
)}
</View>
{displayMode === 1 && (
<ComfortableTitle
novelName={item.novelName}
theme={theme}
/>
)}
</>
</Pressable>
</View>
) : (
<ListView
item={item}
downloadBadge={
showDownloadBadges &&
item.chaptersDownloaded && (
<DownloadBadge
showUnreadBadges={showUnreadBadges}
chaptersDownloaded={item.chaptersDownloaded}
chaptersUnread={item.chaptersUnread}
/>
)
}
unreadBadge={
showUnreadBadges &&
item.chaptersUnread && (
<UnreadBadge
theme={theme}
chaptersDownloaded={item.chaptersDownloaded}
chaptersUnread={item.chaptersUnread}
showDownloadBadges={showDownloadBadges}
/>
)
}
inLibraryBadge={libraryStatus && <InLibraryBadge theme={theme} />}
theme={theme}
onPress={onPress}
/>
);
};
export default NovelCover;
const ComfortableTitle = ({ theme, novelName }) => (
<Text
numberOfLines={2}
style={[
styles.title,
{
color: theme.textColorPrimary,
padding: 4,
},
]}
>
{novelName}
</Text>
);
const CompactTitle = ({ novelName }) => (
<View style={styles.titleContainer}>
<LinearGradient
colors={["transparent", "rgba(0,0,0,0.7)"]}
style={styles.linearGradient}
>
<Text
numberOfLines={2}
style={[styles.title, { color: "rgba(255,255,255,1)" }]}
>
{novelName}
</Text>
</LinearGradient>
</View>
);
const InLibraryBadge = ({ theme }) => (
<Text
style={[
styles.inLibraryBadge,
{
backgroundColor: theme.colorAccent,
color: theme.colorButtonText,
borderRadius: 4,
},
]}
>
In library
</Text>
);
const UnreadBadge = ({
chaptersDownloaded,
chaptersUnread,
showDownloadBadges,
theme,
}) => (
<Text
style={[
styles.unreadBadge,
!chaptersDownloaded && {
borderTopLeftRadius: 4,
borderBottomLeftRadius: 4,
},
!showDownloadBadges && {
borderRadius: 4,
},
{
backgroundColor: theme.colorAccent,
color: theme.colorButtonText,
},
]}
>
{chaptersUnread}
</Text>
);
const DownloadBadge = ({
chaptersDownloaded,
showUnreadBadges,
chaptersUnread,
}) => (
<Text
style={[
styles.downloadBadge,
!chaptersUnread && {
borderTopRightRadius: 4,
borderBottomRightRadius: 4,
},
!showUnreadBadges && {
borderRadius: 4,
},
]}
>
{chaptersDownloaded}
</Text>
);
const styles = StyleSheet.create({
titleContainer: {
flex: 1,
borderRadius: 4,
},
title: {
fontFamily: "pt-sans-bold",
fontSize: 14,
padding: 8,
},
linearGradient: {
bottom: -1,
borderRadius: 4,
},
opac: {
paddingHorizontal: 4,
paddingVertical: 4,
borderRadius: 4,
flex: 1,
},
extensionIcon: {
width: 42,
height: 42,
borderRadius: 4,
},
listView: {
flex: 1,
paddingVertical: 8,
paddingHorizontal: 12,
flexDirection: "row",
alignItems: "center",
borderRadius: 4,
},
downloadBadge: {
backgroundColor: "#47a84a",
color: "#FFFFFF",
borderTopLeftRadius: 4,
borderBottomLeftRadius: 4,
paddingTop: 2,
paddingHorizontal: 4,
fontSize: 12,
},
unreadBadge: {
color: "#FFFFFF",
borderTopRightRadius: 4,
borderBottomRightRadius: 4,
paddingTop: 2,
paddingHorizontal: 4,
fontSize: 12,
},
inLibraryBadge: {
paddingVertical: 2,
paddingHorizontal: 4,
fontSize: 12,
},
compactTitleContainer: {
position: "absolute",
bottom: 4,
left: 4,
right: 4,
},
badgeContainer: {
position: "absolute",
zIndex: 1,
top: 8,
left: 8,
flexDirection: "row",
},
});
@@ -1,14 +1,17 @@
import React from "react";
import React, { useCallback } from "react";
import { StyleSheet, FlatList } from "react-native";
import { useSettings } from "../Hooks/reduxHooks";
import { getDeviceOrientation } from "../Services/utils/helpers";
import { useSettings } from "../hooks/reduxHooks";
import { getDeviceOrientation } from "../services/utils/helpers";
const NovelList = ({
data,
onScroll,
onEndReached,
renderItem,
refreshControl,
ListEmptyComponent,
ListFooterComponent,
}) => {
const { displayMode, novelsPerRow } = useSettings();
@@ -26,16 +29,21 @@ const NovelList = ({
}
};
const keyExtractor = useCallback((item) => item.novelUrl, []);
return (
<FlatList
contentContainerStyle={styles.flatListCont}
numColumns={getNovelsPerRow()}
key={[orientation, getNovelsPerRow()]}
data={data}
keyExtractor={(item) => item.novelUrl}
keyExtractor={keyExtractor}
renderItem={renderItem}
refreshControl={refreshControl}
ListEmptyComponent={ListEmptyComponent}
ListFooterComponent={ListFooterComponent}
onScroll={onScroll}
onEndReached={onEndReached}
/>
);
};
@@ -2,7 +2,7 @@ import React, { useRef } from "react";
import { StyleSheet, View, TextInput, Text, StatusBar } from "react-native";
import { TouchableRipple, IconButton } from "react-native-paper";
import { Row } from "./Common";
import { Row } from "../Common";
export const Searchbar = ({
theme,
+56
View File
@@ -0,0 +1,56 @@
import React from "react";
import { Pressable, StyleSheet, View, Text } from "react-native";
import { Switch, List } from "react-native-paper";
const SwitchSetting = ({ label, description, onPress, theme, icon, value }) => (
<Pressable
android_ripple={{ color: theme.rippleColor }}
style={styles.container}
onPress={onPress}
>
<View style={{ flexDirection: "row", flex: 1 }}>
{icon && (
<List.Icon
color={theme.colorAccent}
icon={icon}
style={{ margin: 0 }}
/>
)}
<View style={[icon && { marginLeft: 16 }]}>
<Text
style={{
color: theme.textColorPrimary,
fontSize: 16,
flex: 1,
textAlignVertical: "center",
}}
>
{label}
</Text>
{description && (
<Text style={{ color: theme.textColorSecondary }}>
{description}
</Text>
)}
</View>
</View>
<Switch
value={value}
onValueChange={onPress}
color={theme.colorAccent}
style={{ marginRight: 8 }}
/>
</Pressable>
);
export default SwitchSetting;
const styles = StyleSheet.create({
container: {
paddingHorizontal: 16,
paddingVertical: 12,
flexDirection: "row",
justifyContent: "space-between",
},
});
@@ -1,4 +1,5 @@
import * as SQLite from "expo-sqlite";
import { getSource } from "../../sources/sources";
const db = SQLite.openDatabase("lnreader.db");
const insertChaptersQuery = `INSERT INTO chapters (chapterUrl, chapterName, releaseDate, novelId) values (?, ?, ?, ?)`;
@@ -16,15 +17,15 @@ export const insertChapters = async (novelId, chapters) => {
novelId,
],
(txObj, res) => {},
(txObj, error) =>
console.log(
"Error ",
error,
" Chapter URL: ",
chapter.chapterUrl,
" Novel ID ",
novelId
)
(txObj, error) => {}
// console.log(
// "Error ",
// error,
// " Chapter URL: ",
// chapter.chapterUrl,
// " Novel ID ",
// novelId
// )
),
(txObj, res) => {
console.log("Success");
@@ -34,13 +35,10 @@ export const insertChapters = async (novelId, chapters) => {
});
};
const getChaptersQuery = (sort, filter) =>
`SELECT * FROM chapters WHERE novelId = ? ${filter || ""} ${
sort || "ORDER BY chapterId ASC"
}`;
const getChaptersQuery = (sort = "ORDER BY chapterId ASC", filter = "") =>
`SELECT * FROM chapters WHERE novelId = ? ${filter} ${sort}`;
export const getChapters = (novelId, sort, filter) => {
// console.log(getChaptersQuery(sort, filter));
return new Promise((resolve, reject) =>
db.transaction((tx) => {
tx.executeSql(
@@ -157,10 +155,9 @@ export const downloadChapter = async (
chapterUrl,
chapterId
) => {
const downloadUrl = `https://lnreader-extensions.vercel.app/api/${sourceId}/novel/${novelUrl}${chapterUrl}`;
const source = getSource(sourceId);
const response = await fetch(downloadUrl);
const chapter = await response.json();
const chapter = await source.parseChapter(novelUrl, chapterUrl);
db.transaction((tx) => {
tx.executeSql(
@@ -262,3 +259,32 @@ export const markPreviousChaptersUnread = async (chapterId, novelId) => {
);
});
};
const getDownloadedChaptersQuery = `
SELECT chapters.*, novels.sourceId, novels.novelName, novels.novelCover
FROM chapters
JOIN novels
ON chapters.novelId = novels.novelId
WHERE chapters.downloaded = 1`;
export const getDownloadedChapters = () => {
return new Promise((resolve, reject) =>
db.transaction((tx) => {
tx.executeSql(
getDownloadedChaptersQuery,
null,
(txObj, { rows: { _array } }) => {
resolve(_array);
},
(txObj, error) => console.log("Error ", error)
);
})
);
};
export const deleteDownloads = async () => {
db.transaction((tx) => {
tx.executeSql(`UPDATE chapters SET downloaded = 0`);
tx.executeSql("DELETE FROM downloads; VACCUM;");
});
};
@@ -1,9 +1,13 @@
import * as SQLite from "expo-sqlite";
const db = SQLite.openDatabase("lnreader.db");
import { fetchChapters, fetchNovel } from "../../Services/Source/source";
import BackgroundService from "react-native-background-actions";
import { fetchChapters, fetchNovel } from "../../services/Source/source";
import { insertChapters } from "./ChapterQueries";
import { showToast } from "../../hooks/showToast";
const insertNovelQuery = `INSERT INTO novels (novelUrl, sourceUrl, sourceId, source, novelName, novelCover, novelSummary, author, artist, status, genre) VALUES(?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`;
export const insertNovel = async (novel) => {
@@ -81,9 +85,7 @@ export const deleteNovelCache = () => {
tx.executeSql(
"DELETE FROM novels WHERE followed = 0",
null,
(txObj, res) => {
console.log("Cleared Novel Cache");
},
(txObj, res) => showToast("Entries deleted"),
(txObj, error) => console.log("Error ", error)
);
});
@@ -127,33 +129,67 @@ export const restoreLibrary = async (novel) => {
const migrateNovelQuery = `INSERT INTO novels (novelUrl, sourceUrl, sourceId, source, novelName, novelCover, novelSummary, author, artist, status, genre, followed) VALUES(?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`;
export const migrateNovel = async (sourceId, novelUrl) => {
const novel = await fetchNovel(sourceId, novelUrl);
try {
const novel = await fetchNovel(sourceId, novelUrl);
db.transaction((tx) =>
tx.executeSql(
migrateNovelQuery,
[
novel.novelUrl,
novel.sourceUrl,
novel.sourceId,
novel.source,
novel.novelName,
novel.novelCover,
novel.novelSummary,
novel.author,
novel.artist,
novel.status,
novel.genre,
1,
],
async (txObj, { insertId }) => {
const chapters = await fetchChapters(
novel.sourceId,
novel.novelUrl
);
await insertChapters(insertId, chapters);
const options = {
taskName: "Migration",
taskTitle: `Migrating ${novel.novelName} to new source`,
taskDesc: novel.source,
taskIcon: {
name: "notification_icon",
type: "drawable",
},
(txObj, error) => console.log("Error ", error)
)
);
color: "#00adb5",
parameters: {
delay: 1000,
},
progressBar: {
max: 1,
value: 0,
indeterminate: true,
},
};
const veryIntensiveTask = async () => {
await new Promise(async (resolve) => {
db.transaction((tx) =>
tx.executeSql(
migrateNovelQuery,
[
novel.novelUrl,
novel.sourceUrl,
novel.sourceId,
novel.source,
novel.novelName,
novel.novelCover,
novel.novelSummary,
novel.author,
novel.artist,
novel.status,
novel.genre,
1,
],
async (txObj, { insertId }) => {
const chapters = await fetchChapters(
novel.sourceId,
novel.novelUrl
);
await insertChapters(insertId, chapters);
resolve();
},
(txObj, error) => console.log("Error ", error)
)
);
});
};
await BackgroundService.start(veryIntensiveTask, options);
await BackgroundService.updateNotification({
progressBar: { value: 1, indeterminate: false },
});
await BackgroundService.stop();
} catch (error) {
showToast(error.message);
}
};
@@ -15,4 +15,4 @@ export const createChapterTableQuery = `
export const createNovelIdIndexQuery = `CREATE INDEX IF NOT EXISTS chapterNovelIdIndex ON chapters(novelId)`;
export const createUnreadChaptersIndexQuery = `CREATE INDEX IF NOT EXISTS chapterUnreadByNovelIndex ON chapters(novelId, \`read\`)`;
export const createUnreadChaptersIndexQuery = `CREATE INDEX IF NOT EXISTS chapterUnreadByNovelIndex ON chapters(novelId, \`read\`) WHERE \`read\`=0`;
@@ -22,7 +22,7 @@ export const githubUpdateChecker = () => {
};
const isNewVersion = (versionTag) => {
let currentVersion = "v1.0.21";
let currentVersion = "v1.1.1";
const regex = /[^\\d.]/;
let newVersion = versionTag.replace(regex, "");
+93
View File
@@ -0,0 +1,93 @@
import React from "react";
import { createMaterialBottomTabNavigator } from "@react-navigation/material-bottom-tabs";
import MaterialCommunityIcons from "react-native-vector-icons/MaterialCommunityIcons";
import Library from "../screens/Library/Library";
import Updates from "../screens/Updates/Updates";
import History from "../screens/History/History";
import Browse from "../screens/Browse/Browse";
import More from "../screens/More/More";
import { useTheme } from "../hooks/reduxHooks";
const Tab = createMaterialBottomTabNavigator();
const BottomNavigator = () => {
const theme = useTheme();
return (
<Tab.Navigator
barStyle={{ backgroundColor: theme.colorPrimary }}
activeColor={theme.colorAccent}
>
<Tab.Screen
name="Library"
component={Library}
options={{
tabBarIcon: ({ color }) => (
<MaterialCommunityIcons
name="book-variant-multiple"
color={color}
size={24}
/>
),
}}
/>
<Tab.Screen
name="Updates"
component={Updates}
options={{
tabBarIcon: ({ color }) => (
<MaterialCommunityIcons
name="alert-decagram-outline"
color={color}
size={24}
/>
),
}}
/>
<Tab.Screen
name="History"
component={History}
options={{
tabBarIcon: ({ color }) => (
<MaterialCommunityIcons
name="history"
color={color}
size={24}
/>
),
}}
/>
<Tab.Screen
name="Browse"
component={Browse}
options={{
tabBarIcon: ({ color }) => (
<MaterialCommunityIcons
name="compass-outline"
color={color}
size={24}
/>
),
}}
/>
<Tab.Screen
name="More"
component={More}
options={{
tabBarIcon: ({ color }) => (
<MaterialCommunityIcons
name="dots-horizontal"
color={color}
size={24}
/>
),
}}
/>
</Tab.Navigator>
);
};
export default BottomNavigator;
@@ -1,36 +1,38 @@
import React from "react";
import {
createStackNavigator,
TransitionPresets,
} from "@react-navigation/stack";
import BottomNavigator from "./BottomNavigator";
import Novel from "../Screens/Novel/Novel";
import Chapter from "../Screens/Chapter/Chapter";
import Extension from "../Screens/Extension/Extension";
import GlobalSearch from "../Screens/Browse/globalsearch/GlobalSearch";
import Migration from "../Screens/Browse/migration/Migration";
import SourceNovels from "../Screens/Browse/SourceNovels";
import MigrateNovel from "../Screens/Browse/migration/MigrationNovels";
import MoreStack from "./More";
import MalTopNovels from "../Screens/Browse/discover/MalTopNovels";
import { setStatusBarStyle } from "../Hooks/setStatusBarStyle";
import React, { useEffect } from "react";
import { View } from "react-native";
import { useTheme } from "../Hooks/reduxHooks";
import { githubUpdateChecker } from "../Hooks/githubUpdateChecker";
import NewUpdateDialog from "../Components/NewUpdateDialog";
import { NavigationContainer } from "@react-navigation/native";
import BrowseNovelUpdates from "../Screens/Browse/discover/BrowseNovelUpdates";
import { createStackNavigator } from "@react-navigation/stack";
import { setBarColor } from "../theme/utils/setBarColor";
import { useTheme } from "../hooks/reduxHooks";
import { githubUpdateChecker } from "../hooks/githubUpdateChecker";
// Navigators
import BottomNavigator from "./BottomNavigator";
// Screens
import Novel from "../screens/Novel/Novel";
import Chapter from "../screens/Chapter/Chapter";
import Extension from "../screens/Extension/Extension";
import GlobalSearch from "../screens/Browse/globalsearch/GlobalSearch";
import Migration from "../screens/Browse/migration/Migration";
import SourceNovels from "../screens/Browse/SourceNovels";
import MigrateNovel from "../screens/Browse/migration/MigrationNovels";
import MoreStack from "./More";
import MalTopNovels from "../screens/Browse/discover/MalTopNovels";
import NewUpdateDialog from "../components/NewUpdateDialog";
import BrowseSettings from "../screens/Browse/BrowseSettings";
const Stack = createStackNavigator();
const stackNavigatorConfig = {
headerShown: false,
};
const MainNavigator = () => {
const theme = useTheme();
setStatusBarStyle();
useEffect(() => {
setBarColor(theme);
}, [theme]);
const { isNewVersion, latestRelease } = githubUpdateChecker() || {};
return (
@@ -39,29 +41,19 @@ const MainNavigator = () => {
>
<View style={{ flex: 1, backgroundColor: theme.colorPrimaryDark }}>
{isNewVersion && <NewUpdateDialog newVersion={latestRelease} />}
<Stack.Navigator screenOptions={stackNavigatorConfig}>
<Stack.Navigator screenOptions={{ headerShown: false }}>
<Stack.Screen
name="BottomNavigator"
component={BottomNavigator}
/>
<Stack.Screen
name="Novel"
component={Novel}
options={{
...TransitionPresets.RevealFromBottomAndroid,
}}
/>
<Stack.Screen
name="Chapter"
component={Chapter}
options={{ ...TransitionPresets.SlideFromRightIOS }}
/>
<Stack.Screen name="Novel" component={Novel} />
<Stack.Screen name="Chapter" component={Chapter} />
<Stack.Screen name="MoreStack" component={MoreStack} />
<Stack.Screen name="Extension" component={Extension} />
<Stack.Screen name="BrowseMal" component={MalTopNovels} />
<Stack.Screen
name="BrowseNovelUpdates"
component={BrowseNovelUpdates}
name="BrowseSettings"
component={BrowseSettings}
/>
<Stack.Screen
name="GlobalSearch"
+40
View File
@@ -0,0 +1,40 @@
import React from "react";
import { createStackNavigator } from "@react-navigation/stack";
// Screens
import About from "../screens/More/About";
import Settings from "../screens/More/Settings";
import TrackerSettings from "../screens/More/TrackerSettings";
import ReaderSettings from "../screens/More/ReaderSettings";
import BackupSettings from "../screens/More/BackupSettings";
import AdvancedSettings from "../screens/More/AdvancedSettings";
import GeneralSettings from "../screens/More/GeneralSettings";
import DownloadQueue from "../screens/More/DownloadQueue";
import Downloads from "../screens/More/Downloads";
const Stack = createStackNavigator();
const stackNavigatorConfig = { headerShown: false };
const SettingsStack = () => (
<Stack.Navigator screenOptions={stackNavigatorConfig}>
<Stack.Screen name="Settings" component={Settings} />
<Stack.Screen name="GeneralSettings" component={GeneralSettings} />
<Stack.Screen name="ReaderSettings" component={ReaderSettings} />
<Stack.Screen name="TrackerSettings" component={TrackerSettings} />
<Stack.Screen name="BackupSettings" component={BackupSettings} />
<Stack.Screen name="AdvancedSettings" component={AdvancedSettings} />
</Stack.Navigator>
);
const MoreStack = () => (
<Stack.Navigator screenOptions={stackNavigatorConfig}>
<Stack.Screen name="SettingsStack" component={SettingsStack} />
<Stack.Screen name="About" component={About} />
<Stack.Screen name="DownloadQueue" component={DownloadQueue} />
<Stack.Screen name="Downloads" component={Downloads} />
</Stack.Navigator>
);
export default MoreStack;
+4
View File
@@ -0,0 +1,4 @@
export const SET_DOWNLOAD_QUEUE = "SET_DOWNLOAD_QUEUE";
export const CANCEL_DOWNLOAD = "CANCEL_DOWNLOAD";
export const PAUSE_DOWNLOADS = "PAUSE_DOWNLOADS";
export const RESUME_DOWNLOADS = "RESUME_DOWNLOADS";
+101
View File
@@ -0,0 +1,101 @@
import {
SET_DOWNLOAD_QUEUE,
CANCEL_DOWNLOAD,
PAUSE_DOWNLOADS,
} from "./donwloads.types";
import BackgroundService from "react-native-background-actions";
import { downloadChapter } from "../../database/queries/ChapterQueries";
import { CHAPTER_DOWNLOADED } from "../novel/novel.types";
import { showToast } from "../../hooks/showToast";
export const pauseDownloads = () => async (dispatch) => {
showToast("Download paused");
await BackgroundService.stop();
dispatch({ type: PAUSE_DOWNLOADS });
};
export const resumeDownloads = (chapters) => async (dispatch) => {
showToast("Download resumed");
const options = {
taskName: "Library Update",
taskTitle: chapters[0].chapterName,
taskDesc: "0/" + chapters.length,
taskIcon: {
name: "notification_icon",
type: "drawable",
},
color: "#00adb5",
parameters: {
delay: 1000,
},
progressBar: {
max: chapters.length,
value: 0,
},
};
const sleep = (time) =>
new Promise((resolve) => setTimeout(() => resolve(), time));
const veryIntensiveTask = async (taskData) => {
await new Promise(async (resolve) => {
for (
let i = 0;
BackgroundService.isRunning() && i < chapters.length;
i++
) {
if (BackgroundService.isRunning()) {
if (!chapters[i].downloaded) {
await downloadChapter(
chapters[i].sourceId,
chapters[i].novelUrl,
chapters[i].chapterUrl,
chapters[i].chapterId
);
}
dispatch({
type: CHAPTER_DOWNLOADED,
payload: chapters[i].chapterId,
});
await BackgroundService.updateNotification({
taskTitle: chapters[i].chapterName,
taskDesc: i + 1 + "/" + chapters.length,
progressBar: {
max: chapters.length,
value: i + 1,
},
});
if (i + 1 === chapters.length) {
resolve();
await BackgroundService.stop();
Notifications.scheduleNotificationAsync({
content: {
title: "Downloader",
body: "Download completed",
},
trigger: null,
});
}
await sleep(taskData.delay);
}
}
});
};
await BackgroundService.start(veryIntensiveTask, options);
dispatch({ type: PAUSE_DOWNLOADS });
};
export const cancelDownload = () => async (dispatch) => {
await BackgroundService.stop();
dispatch({ type: CANCEL_DOWNLOAD });
};
+35
View File
@@ -0,0 +1,35 @@
import { CHAPTER_DOWNLOADED } from "../novel/novel.types";
import { CANCEL_DOWNLOAD, SET_DOWNLOAD_QUEUE } from "./donwloads.types";
const initialState = {
downloadQueue: [],
};
const downloadsReducer = (state = initialState, action) => {
const { type, payload } = action;
switch (type) {
case SET_DOWNLOAD_QUEUE:
return {
...state,
downloadQueue: [...state.downloadQueue, ...payload],
};
case CHAPTER_DOWNLOADED:
return {
...state,
downloadQueue: state.downloadQueue.filter(
(chapter) => chapter.chapterId !== payload
),
};
case CANCEL_DOWNLOAD:
return {
...state,
downloadQueue: [],
};
default:
return state;
}
};
export default downloadsReducer;
+2 -2
View File
@@ -9,9 +9,9 @@ import {
insertHistory,
deleteHistory,
deleteAllHistory,
} from "../../Database/queries/HistoryQueries";
} from "../../database/queries/HistoryQueries";
import { SET_LAST_READ } from "../preferences/preference.types";
import { showToast } from "../../Hooks/showToast";
import { showToast } from "../../hooks/showToast";
export const getHistoryAction = () => async (dispatch) => {
dispatch({ type: LOAD_HISTORY });
+1 -1
View File
@@ -26,7 +26,7 @@ const historyReducer = (state = initialState, action) => {
// ),
// };
case CLEAR_HISTORY:
return { ...state, history: [] };
return { ...state, loading: false, history: [] };
default:
return state;
}
+2 -2
View File
@@ -7,8 +7,8 @@ import {
import {
getLibrary,
searchLibrary,
} from "../../Database/queries/LibraryQueries";
import { restoreBackup } from "../../Services/backup";
} from "../../database/queries/LibraryQueries";
import { restoreBackup } from "../../services/backup";
export const getLibraryAction = (sort, filter) => async (dispatch) => {
// console.log("1.", sort, filter);
+114 -35
View File
@@ -20,13 +20,13 @@ import {
MARK_PREVIOUS_CHAPTERS_UNREAD,
} from "./novel.types";
import { updateNovel } from "../../Services/updates";
import { fetchNovel } from "../../Services/Source/source";
import { updateNovel } from "../../services/updates";
import { fetchNovel } from "../../services/Source/source";
import {
followNovel,
insertNovel,
getNovel,
} from "../../Database/queries/NovelQueries";
} from "../../database/queries/NovelQueries";
import {
getChapters,
insertChapters,
@@ -38,18 +38,34 @@ import {
markPreviuschaptersRead,
markPreviousChaptersUnread,
getNextChapterFromDB,
} from "../../Database/queries/ChapterQueries";
import { deleteNovelUpdates } from "../../Database/queries/UpdateQueries";
} from "../../database/queries/ChapterQueries";
import { deleteNovelUpdates } from "../../database/queries/UpdateQueries";
import {
SAVE_SCROLL_POSITION,
SET_CHAPTER_LIST_PREF,
SET_LAST_READ,
} from "../preferences/preference.types";
import { GET_LIBRARY_NOVELS } from "../library/library.types";
import { getLibrary } from "../../Database/queries/LibraryQueries";
import { showToast } from "../../Hooks/showToast";
import { getLibrary } from "../../database/queries/LibraryQueries";
import { showToast } from "../../hooks/showToast";
import { store } from "../store";
import * as Notifications from "expo-notifications";
Notifications.setNotificationHandler({
handleNotification: async () => {
return {
shouldShowAlert: true,
shouldPlaySound: true,
shouldSetBadge: true,
};
},
});
import BackgroundService from "react-native-background-actions";
import { setDownloadQueue } from "../downloads/downloads.actions";
import { SET_DOWNLOAD_QUEUE } from "../downloads/donwloads.types";
export const setNovel = (novel) => async (dispatch) => {
dispatch({ type: SET_NOVEL, payload: novel });
};
@@ -265,14 +281,18 @@ export const markChapterUnreadAction =
};
export const downloadChapterAction =
(extensionId, novelUrl, chapterUrl, chapterName, chapterId) =>
(sourceId, novelUrl, chapterUrl, chapterName, chapterId) =>
async (dispatch) => {
// dispatch({
// type: CHAPTER_DOWNLOADING,
// payload: chapterId,
// });
dispatch({
type: CHAPTER_DOWNLOADING,
payload: chapterId,
type: SET_DOWNLOAD_QUEUE,
payload: [{ chapterId, chapterName }],
});
await downloadChapter(extensionId, novelUrl, chapterUrl, chapterId);
await downloadChapter(sourceId, novelUrl, chapterUrl, chapterId);
dispatch({
type: CHAPTER_DOWNLOADED,
@@ -283,34 +303,93 @@ export const downloadChapterAction =
};
export const downloadAllChaptersAction =
(extensionId, novelUrl, chapters) => async (dispatch) => {
(sourceId, novelUrl, chaps) => async (dispatch) => {
try {
await chapters.map((chapter, index) => {
setTimeout(async () => {
dispatch({
type: CHAPTER_DOWNLOADING,
payload: chapter.chapterId,
});
let chapters = chaps.filter((chapter) => chapter.downloaded === 0);
if (!chapter.downloaded) {
await downloadChapter(
extensionId,
novelUrl,
chapter.chapterUrl,
chapter.chapterId
);
chapters = chapters.map((chapter) => ({
...chapter,
sourceId,
novelUrl,
}));
dispatch({ type: SET_DOWNLOAD_QUEUE, payload: chapters });
const options = {
taskName: "Library Update",
taskTitle: chapters[0].chapterName,
taskDesc: "0/" + chapters.length,
taskIcon: {
name: "notification_icon",
type: "drawable",
},
color: "#00adb5",
parameters: {
delay: 1000,
},
progressBar: {
max: chapters.length,
value: 0,
},
};
const sleep = (time) =>
new Promise((resolve) => setTimeout(() => resolve(), time));
const veryIntensiveTask = async (taskData) => {
await new Promise(async (resolve) => {
for (
let i = 0;
BackgroundService.isRunning() && i < chapters.length;
i++
) {
if (BackgroundService.isRunning()) {
// dispatch({
// type: CHAPTER_DOWNLOADING,
// payload: chapters[i].chapterId,
// });
if (!chapters[i].downloaded) {
await downloadChapter(
sourceId,
novelUrl,
chapters[i].chapterUrl,
chapters[i].chapterId
);
}
dispatch({
type: CHAPTER_DOWNLOADED,
payload: chapters[i].chapterId,
});
await BackgroundService.updateNotification({
taskTitle: chapters[i].chapterName,
taskDesc: i + 1 + "/" + chapters.length,
progressBar: {
max: chapters.length,
value: i + 1,
},
});
if (i + 1 === chapters.length) {
resolve();
await BackgroundService.stop();
Notifications.scheduleNotificationAsync({
content: {
title: "Downloader",
body: "Download completed",
},
trigger: null,
});
}
await sleep(taskData.delay);
}
}
});
};
dispatch({
type: CHAPTER_DOWNLOADED,
payload: chapter.chapterId,
});
if (index + 1 === chapters.length) {
showToast(`Download completed `);
}
}, 1000 * index);
});
await BackgroundService.start(veryIntensiveTask, options);
} catch (error) {
showToast(error.message);
}
+16
View File
@@ -4,6 +4,8 @@ import {
SET_READER_SETTINGS,
SET_APP_SETTINGS,
SET_ACCENT_COLOR,
SET_RIPPLE_COLOR,
SET_AMOLED_MODE,
} from "./settings.types";
export const setAppTheme = (val) => (dispatch) => {
@@ -40,3 +42,17 @@ export const setAccentColor = (val) => (dispatch) => {
payload: val,
});
};
export const setRippleColor = (val) => (dispatch) => {
dispatch({
type: SET_RIPPLE_COLOR,
payload: val,
});
};
export const setAmoledMode = (id, val) => (dispatch) => {
dispatch({
type: SET_AMOLED_MODE,
payload: { id, val },
});
};
+37 -3
View File
@@ -4,23 +4,29 @@ import {
SET_READER_SETTINGS,
SET_APP_SETTINGS,
SET_ACCENT_COLOR,
SET_RIPPLE_COLOR,
SET_AMOLED_MODE,
} from "./settings.types";
import {
amoledDarkTheme,
lightTheme,
darkTheme,
midnightDuskTheme,
limeTheme,
greenAppleTheme,
irisBlueTheme,
} from "../../Theme/theme";
hotPinkTheme,
strawberryDaiquiri,
} from "../../theme/theme";
const themes = {
0: amoledDarkTheme,
1: lightTheme,
2: darkTheme,
3: midnightDuskTheme,
4: limeTheme,
4: greenAppleTheme,
5: irisBlueTheme,
6: hotPinkTheme,
7: strawberryDaiquiri,
};
/**
@@ -56,6 +62,12 @@ const initialState = {
showUnreadBadges: true,
showNumberOfNovels: false,
showScrollPercentage: true,
swipeGestures: true,
incognitoMode: false,
searchAllSources: false,
textSelectable: false,
updateLibraryOnLaunch: false,
downloadNewChapters: false,
};
const settingsReducer = (state = initialState, action) => {
@@ -77,6 +89,28 @@ const settingsReducer = (state = initialState, action) => {
colorAccent: payload,
},
};
case SET_RIPPLE_COLOR:
return {
...state,
theme: {
...state.theme,
rippleColor: payload,
},
};
case SET_AMOLED_MODE:
return {
...state,
theme: payload.val
? {
...themes[0],
colorAccent: state.theme.colorAccent,
rippleColor: state.theme.rippleColor,
id: state.theme.id,
name: state.theme.name,
}
: themes[payload.id],
};
case SET_APP_SETTINGS:
return {
...state,
+2
View File
@@ -3,3 +3,5 @@ export const SET_APP_THEME = "SET_APP_THEME";
export const SET_NOVELS_PER_ROW = "SET_NOVELS_PER_ROW";
export const SET_READER_SETTINGS = "SET_READER_SETTINGS";
export const SET_ACCENT_COLOR = "SET_ACCENT_COLOR";
export const SET_RIPPLE_COLOR = "SET_RIPPLE_COLOR";
export const SET_AMOLED_MODE = "SET_AMOLED_MODE";
+30 -5
View File
@@ -1,14 +1,25 @@
import { GET_SOURCES, PIN_SOURCES, SEARCH_SOURCES } from "./source.types";
import { fetchSources } from "../../Services/Source/source";
import { showToast } from "../../Hooks/showToast";
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 {
const res = await fetchSources();
/**
* Sort sources aplhabetically
*/
let sortedSources = sources.sort((a, b) =>
a.sourceName.localeCompare(b.sourceName)
);
dispatch({
type: GET_SOURCES,
payload: res,
payload: sortedSources,
});
} catch (error) {
showToast(error.message);
@@ -30,3 +41,17 @@ export const pinSourceAction = (sourceId) => async (dispatch) => {
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,
});
};
+28 -1
View File
@@ -1,10 +1,18 @@
import { GET_SOURCES, SEARCH_SOURCES, PIN_SOURCES } from "./source.types";
import {
GET_SOURCES,
SEARCH_SOURCES,
PIN_SOURCES,
FILTER_LANGUAGE,
ENABLE_DISCOVER,
} from "./source.types";
const initialState = {
sources: [],
pinnedSources: [],
search: [],
pinned: [],
filters: [],
showMyAnimeList: true,
loading: true,
};
@@ -30,6 +38,25 @@ const sourceReducer = (state = initialState, action) => {
source.sourceName.toLowerCase().includes(payload)
),
};
case FILTER_LANGUAGE:
if (!state.filters) {
state.filters = [];
}
return {
...state,
filters:
state.filters.indexOf(payload) === -1
? [...state.filters, payload]
: state.filters.filter(
(language) => language !== payload
),
};
case ENABLE_DISCOVER:
return {
...state,
[payload]: !state[payload],
};
default:
return state;
}
+2
View File
@@ -1,3 +1,5 @@
export const GET_SOURCES = "GET_SOURCES";
export const SEARCH_SOURCES = "SEARCH_SOURCES";
export const PIN_SOURCES = "PIN_SOURCES";
export const FILTER_LANGUAGE = "FILTER_LANGUAGE";
export const ENABLE_DISCOVER = "ENABLE_DISCOVER";
+2
View File
@@ -11,6 +11,7 @@ import historyReducer from "./history/history.reducer";
import updatesReducer from "./updates/updates.reducer";
import trackerReducer from "./tracker/tracker.reducer";
import preferenceReducer from "./preferences/preference.reducer";
import downloadsReducer from "./downloads/downloads.reducer";
const persistConfig = {
key: "root",
@@ -29,6 +30,7 @@ const persistedReducer = persistReducer(
updatesReducer,
trackerReducer,
preferenceReducer,
downloadsReducer,
})
);
+12 -13
View File
@@ -1,4 +1,4 @@
import { findListItem, updateItem } from "../../Services/Trackers/myAnimeList";
import { findListItem, updateItem } from "../../services/Trackers/myAnimeList";
import {
REMOVE_TRACKER,
SET_TRACKER,
@@ -8,7 +8,7 @@ import {
UPDATE_CHAPTERS_READ,
} from "./tracker.types";
import { updateMalChaptersRead } from "../../Services/Trackers/myAnimeList";
import { updateMalChaptersRead } from "../../services/Trackers/myAnimeList";
export const setTracker = (res) => async (dispatch) => {
dispatch({
@@ -43,15 +43,14 @@ export const untrackNovel = (id) => async (dispatch) => {
dispatch({ type: UNTRACK_NOVEL, payload: id });
};
export const updateChaptersRead = (malId, accessToken, chaptersRead) => async (
dispatch
) => {
let res = await updateMalChaptersRead(malId, accessToken, {
num_chapters_read: chaptersRead,
});
export const updateChaptersRead =
(malId, accessToken, chaptersRead) => async (dispatch) => {
let res = await updateMalChaptersRead(malId, accessToken, {
num_chapters_read: chaptersRead,
});
dispatch({
type: UPDATE_CHAPTERS_READ,
payload: { malId, chaptersRead: res },
});
};
dispatch({
type: UPDATE_CHAPTERS_READ,
payload: { malId, chaptersRead: res },
});
};
+9 -5
View File
@@ -1,9 +1,11 @@
import { GET_UPDATES } from "./updates.types";
import { getUpdates } from "../../Database/queries/UpdateQueries";
import { updateAllNovels } from "../../Services/updates";
import store from "../store";
import { showToast } from "../../Hooks/showToast";
import { getUpdates } from "../../database/queries/UpdateQueries";
import { updateAllNovels } from "../../services/updates";
import { showToast } from "../../hooks/showToast";
export const getUpdatesAction = () => async (dispatch) => {
const updates = await getUpdates();
@@ -33,10 +35,12 @@ export const getUpdatesAction = () => async (dispatch) => {
dispatch({ type: GET_UPDATES, payload: groupedUpdates });
};
export const updateLibraryAction = () => async (dispatch) => {
export const updateLibraryAction = () => async (dispatch, getState) => {
showToast("Updating library");
await updateAllNovels();
const { downloadNewChapters = false } = getState().settingsReducer;
await updateAllNovels({ downloadNewChapters });
dispatch(getUpdatesAction());
};
@@ -7,34 +7,43 @@ import {
ActivityIndicator,
RefreshControl,
} from "react-native";
import { useTheme } from "../../Hooks/reduxHooks";
import { useDispatch, useSelector } from "react-redux";
import {
getSourcesAction,
searchSourcesAction,
} from "../../redux/source/source.actions";
import { useTheme } from "../../hooks/reduxHooks";
import { showToast } from "../../hooks/showToast";
import { Searchbar } from "../../components/Searchbar/Searchbar";
import SourceCard from "./components/SourceCard";
import { showToast } from "../../Hooks/showToast";
import { Searchbar } from "../../Components/Searchbar";
import MalCard from "./components/MalCard";
import NovelUpdatesCard from "./components/NovelUpdatesCard";
import EmptyView from "../../components/EmptyView";
import DiscoverCard from "./discover/DiscoverCard";
const Browse = ({ navigation }) => {
const [refreshing, setRefreshing] = useState(false);
const [searchText, setSearchText] = useState("");
// const [globalSearch, setGlobalSearch] = useState(false);
// const [searchResult, setSearchResults] = useState();
const { sources, search, loading, pinned } = useSelector(
(state) => state.sourceReducer
);
let {
sources,
search,
loading,
pinned,
filters = [],
showMyAnimeList = true,
} = useSelector((state) => state.sourceReducer);
const theme = useTheme();
const dispatch = useDispatch();
const isPinned = (sourceId) =>
pinned.indexOf(sourceId) === -1 ? false : true;
sources = sources.filter((source) => filters.indexOf(source.lang) === -1);
let pinnedSources = sources.filter((source) => isPinned(source.sourceId));
useEffect(() => {
dispatch(getSourcesAction());
}, [getSourcesAction]);
@@ -55,16 +64,24 @@ const Browse = ({ navigation }) => {
dispatch(searchSourcesAction(text));
};
// const onSubmitEditing = () => {};
const renderItem = ({ item }) => (
<SourceCard
item={item}
isPinned={isPinned(item.sourceId)}
dispatch={dispatch}
navigation={navigation}
theme={theme}
/>
);
const SourceHeader = ({ title }) => (
<Text
style={[styles.sourcesHeader, { color: theme.textColorSecondary }]}
>
{title}
</Text>
);
return (
<View
style={[
@@ -88,6 +105,10 @@ const Browse = ({ navigation }) => {
icon: "swap-vertical-variant",
onPress: () => navigation.navigate("Migration"),
},
{
icon: "cog-outline",
onPress: () => navigation.navigate("BrowseSettings"),
},
]}
/>
@@ -96,62 +117,55 @@ const Browse = ({ navigation }) => {
data={!searchText ? sources : search}
keyExtractor={(item) => item.sourceId.toString()}
renderItem={renderItem}
extraData={pinned}
extraData={pinnedSources}
ListHeaderComponent={
<View>
<Text
style={{
color: theme.textColorSecondary,
paddingHorizontal: 16,
paddingVertical: 8,
}}
>
Discover
</Text>
<NovelUpdatesCard theme={theme} />
<MalCard theme={theme} />
{pinned.length > 0 && (
{showMyAnimeList && (
<>
<SourceHeader title="Discover" />
{showMyAnimeList && (
<DiscoverCard
label="MyAnimeList"
onPress={() =>
navigation.navigate("BrowseMal")
}
icon={require("../../../assets/mal.png")}
theme={theme}
/>
)}
</>
)}
{pinnedSources.length > 0 && (
<FlatList
contentContainerStyle={{ paddingBottom: 16 }}
data={sources.filter((source) =>
isPinned(source.sourceId)
)}
data={pinnedSources}
keyExtractor={(item) =>
item.sourceId.toString()
}
renderItem={renderItem}
extraData={pinned}
extraData={pinnedSources}
ListHeaderComponent={
<Text
style={{
color: theme.textColorSecondary,
paddingHorizontal: 16,
paddingVertical: 8,
}}
>
Pinned
</Text>
<SourceHeader title="Pinned" />
}
/>
)}
<Text
style={{
color: theme.textColorSecondary,
paddingHorizontal: 16,
paddingVertical: 8,
}}
>
Sources
</Text>
{sources.length > 0 && <SourceHeader title="Sources" />}
</View>
}
ListEmptyComponent={
loading && (
loading ? (
<ActivityIndicator
size="small"
color={theme.colorAccent}
style={{ marginTop: 16 }}
/>
) : (
sources.length === 0 && (
<EmptyView
icon="(・Д・。"
description="Enable languages from settings"
/>
)
)
}
refreshControl={
@@ -170,5 +184,11 @@ const Browse = ({ navigation }) => {
export default Browse;
const styles = StyleSheet.create({
container: { flex: 1 },
container: {
flex: 1,
},
sourcesHeader: {
paddingHorizontal: 16,
paddingVertical: 8,
},
});
@@ -3,17 +3,18 @@ import { StyleSheet, View, ActivityIndicator, FlatList } from "react-native";
import * as WebBrowser from "expo-web-browser";
import NovelCover from "../../Components/NovelCover";
import { Searchbar } from "../../Components/Searchbar";
import ErrorView from "../../Components/ErrorView";
import NovelCover from "../../components/NovelCover";
import { Searchbar } from "../../components/Searchbar/Searchbar";
import ErrorView from "../../components/ErrorView";
import { useTheme, useSettings } from "../../Hooks/reduxHooks";
import { showToast } from "../../Hooks/showToast";
import { getDeviceOrientation } from "../../Services/utils/helpers";
import { useTheme, useSettings } from "../../hooks/reduxHooks";
import { showToast } from "../../hooks/showToast";
import { getDeviceOrientation } from "../../services/utils/helpers";
import { scrapeSearchResults } from "./discover/MyAnimeListScraper";
const BrowseMalScreen = ({ navigation, route }) => {
const theme = useTheme();
const settings = useSettings();
const [loading, setLoading] = useState(true);
const [novels, setNovels] = useState();
@@ -75,10 +76,11 @@ const BrowseMalScreen = ({ navigation, route }) => {
novelName: item.title,
})
}
theme={theme}
/>
);
const { displayMode, novelsPerRow } = useSettings();
const { displayMode, novelsPerRow } = settings;
const orientation = getDeviceOrientation();
+121
View File
@@ -0,0 +1,121 @@
import React, { useState } from "react";
import { FlatList, Pressable, Text, View } from "react-native";
import { Switch } from "react-native-paper";
import { useDispatch, useSelector } from "react-redux";
import { Appbar } from "../../components/Appbar";
import { ScreenContainer } from "../../components/Common";
import { ListSubHeader } from "../../components/List";
import { useSettings, useTheme } from "../../hooks/reduxHooks";
import { setAppSettings } from "../../redux/settings/settings.actions";
import {
enableDiscover,
filterLanguage,
} from "../../redux/source/source.actions";
const BrowseSettings = ({ navigation }) => {
const theme = useTheme();
const dispatch = useDispatch();
const { filters = [], showMyAnimeList = true } = useSelector(
(state) => state.sourceReducer
);
const languages = ["English", "Spanish", "Japanese", "Chinese", "Arabic"];
const { searchAllSources = false } = useSettings();
const renderItem = ({ item }) => {
return (
<Pressable
android_ripple={{ color: theme.rippleColor }}
style={{
padding: 16,
flexDirection: "row",
alignItems: "center",
justifyContent: "space-between",
}}
onPress={() => dispatch(filterLanguage(item))}
>
<Text style={{ color: theme.textColorPrimary }}>{item}</Text>
<Switch
color={theme.colorAccent}
value={filters.indexOf(item) === -1 ? true : false}
onValueChange={() => dispatch(filterLanguage(item))}
/>
</Pressable>
);
};
return (
<ScreenContainer theme={theme}>
<Appbar onBackAction={navigation.goBack} title="Sources" />
<View>
<ListSubHeader theme={theme}>Global Search</ListSubHeader>
<Pressable
android_ripple={{ color: theme.rippleColor }}
style={{
padding: 16,
flexDirection: "row",
alignItems: "center",
justifyContent: "space-between",
}}
onPress={() =>
dispatch(
setAppSettings(
"searchAllSources",
!searchAllSources
)
)
}
>
<Text style={{ color: theme.textColorPrimary }}>
Search all sources
</Text>
<Switch
color={theme.colorAccent}
value={searchAllSources}
onValueChange={() =>
dispatch(
setAppSettings(
"searchAllSources",
!searchAllSources
)
)
}
/>
</Pressable>
<ListSubHeader theme={theme}>Discover</ListSubHeader>
<Pressable
android_ripple={{ color: theme.rippleColor }}
style={{
padding: 16,
flexDirection: "row",
alignItems: "center",
justifyContent: "space-between",
}}
onPress={() => dispatch(enableDiscover("showMyAnimeList"))}
>
<Text style={{ color: theme.textColorPrimary }}>
MyAnimeList
</Text>
<Switch
color={theme.colorAccent}
value={showMyAnimeList}
onValueChange={() =>
dispatch(enableDiscover("showMyAnimeList"))
}
/>
</Pressable>
<ListSubHeader theme={theme}>Languages</ListSubHeader>
<FlatList
data={languages}
keyExtractor={(item) => item}
renderItem={renderItem}
/>
</View>
</ScreenContainer>
);
};
export default BrowseSettings;
@@ -1,10 +1,10 @@
import React from "react";
import { StyleSheet, View, FlatList, Text } from "react-native";
import { useLibrary, useTheme } from "../../Hooks/reduxHooks";
import { useLibrary, useTheme } from "../../hooks/reduxHooks";
import EmptyView from "../../Components/EmptyView";
import { Appbar } from "../../Components/Appbar";
import ListView from "../../Components/ListView";
import EmptyView from "../../components/EmptyView";
import { Appbar } from "../../components/Appbar";
import ListView from "../../components/ListView";
const SourceNovels = ({ navigation, route }) => {
const sourceId = route.params;
@@ -53,25 +53,6 @@ const SourceNovels = ({ navigation, route }) => {
</Text>
}
/>
{/* <Portal>
<MigrationDialog
migrationDialog={migrationDialog}
hideMigrationDialog={hideMigrationDIalog}
theme={theme}
selectedNovel={selectedNovel}
loading={loading}
migrationNovel={migrationNovel}
/>
<SourceToMigrateDialog
selectedNovel={selectedNovel}
sourceToMigrateDialog={sourceToMigrateDialog}
hideSourceToMigrateDialog={hideSourceToMigrateDialog}
sources={sources}
theme={theme}
showMigrationDialog={showMigrationDialog}
selectSourceToMigrate={selectSourceToMigrate}
/>
</Portal> */}
</View>
);
};
@@ -0,0 +1,98 @@
import React from "react";
import { View, Text, Image, StyleSheet, Pressable } from "react-native";
import { Button, IconButton } from "react-native-paper";
import { pinSourceAction } from "../../../redux/source/source.actions";
const ExtensionCard = ({ item, theme, isPinned, dispatch, navigation }) => {
const { sourceId, sourceName, icon, lang } = item;
const navigateToExtension = () =>
navigation.navigate("Extension", {
sourceId: item.sourceId,
sourceName: item.sourceName,
url: item.url,
});
return (
<Pressable
style={styles.extensionCard}
onPress={navigateToExtension}
android_ripple={{ color: theme.rippleColor }}
>
<Image source={{ uri: icon }} style={styles.extensionIcon} />
<View style={styles.extensionDetails}>
<View>
<Text
style={{
color: theme.textColorPrimary,
fontSize: 14,
}}
>
{sourceName}
</Text>
<Text
style={{
color: theme.textColorSecondary,
fontSize: 12,
}}
>
{lang}
</Text>
</View>
<View style={{ flexDirection: "row", alignItems: "center" }}>
<Button
labelStyle={{ letterSpacing: 0 }}
uppercase={false}
color={theme.colorAccent}
onPress={navigateToExtension}
>
Browse
</Button>
<IconButton
icon={isPinned ? "pin" : "pin-outline"}
animated
size={21}
onPress={() => dispatch(pinSourceAction(sourceId))}
color={
isPinned
? theme.colorAccent
: theme.textColorSecondary
}
style={{ margin: 2 }}
/>
</View>
</View>
</Pressable>
);
};
export default ExtensionCard;
const styles = StyleSheet.create({
extensionCard: {
flexDirection: "row",
alignItems: "center",
marginVertical: 4,
paddingVertical: 8,
paddingHorizontal: 20,
},
extensionIcon: {
width: 40,
height: 40,
borderRadius: 4,
},
extensionDetails: {
flex: 1,
flexDirection: "row",
justifyContent: "space-between",
alignItems: "center",
marginLeft: 16,
},
sourceStatus: {
color: "#C14033",
fontSize: 12,
marginLeft: 5,
},
});
@@ -0,0 +1,142 @@
import React from "react";
import { StyleSheet, Pressable, Text, View, FlatList } from "react-native";
import FastImage from "react-native-fast-image";
import { Row } from "../../../components/Common";
const GenreChip = ({ children, theme }) => (
<View style={{ flexDirection: "row" }}>
<Text
style={[styles.genreChip, { color: theme.textColorSecondary }]}
numberOfLines={2}
>
<Text
style={[
styles.genreChip,
{
color: theme.textColorPrimary,
fontSize: 14,
},
]}
>
{`${"Score "}`}
</Text>
{children}
</Text>
</View>
);
const BrowseMalCard = ({ novel, theme, onPress }) => {
return (
<View
style={{
flex: 1,
backgroundColor: theme.colorPrimary,
borderRadius: 8,
elevation: 1,
marginVertical: 4,
marginHorizontal: 8,
}}
>
<Pressable
style={styles.novelUpdatesCard}
onPress={onPress}
android_ripple={{
color: theme.rippleColor,
borderless: true,
}}
>
<FastImage
source={{ uri: novel.novelCover }}
style={{
width: 130,
height: 180,
borderTopLeftRadius: 8,
borderBottomLeftRadius: 8,
}}
/>
<View
style={{
flex: 1,
paddingHorizontal: 8,
paddingVertical: 8,
}}
>
<Text
style={{ color: theme.textColorPrimary, fontSize: 17 }}
>
{novel.novelName}
</Text>
<GenreChip theme={theme}>{novel.score}</GenreChip>
{novel.info && (
<>
<View style={{ flexDirection: "row" }}>
<Text
style={[
styles.genreChip,
{ color: theme.textColorSecondary },
]}
>
<Text
style={[
styles.genreChip,
{
color: theme.textColorPrimary,
fontSize: 14,
},
]}
>
{`${"Type "}`}
</Text>
{novel.info[1]}
</Text>
</View>
<View
style={{
flexDirection: "row",
marginBottom: 4,
}}
>
<Text
style={[
styles.genreChip,
{ color: theme.textColorSecondary },
]}
>
<Text
style={[
styles.genreChip,
{
color: theme.textColorPrimary,
fontSize: 14,
},
]}
>
{`${"Published "}`}
</Text>
{novel.info[2]}
</Text>
</View>
</>
)}
</View>
</Pressable>
</View>
);
};
export default BrowseMalCard;
const styles = StyleSheet.create({
novelUpdatesCard: {
flex: 1,
flexDirection: "row",
},
contentContainer: {
flex: 1,
},
genreChip: {
marginTop: 4,
fontSize: 13,
// fontStyle: "italic",
},
});
@@ -0,0 +1,58 @@
import React from "react";
import { View, Text, Image, StyleSheet, Pressable } from "react-native";
import { Button } from "react-native-paper";
const DiscoverCard = ({ icon, label, onPress, theme }) => {
return (
<Pressable
style={styles.discoverCard}
onPress={onPress}
android_ripple={{ color: theme.rippleColor }}
>
<Image source={icon} style={styles.discoverIcon} />
<View style={styles.label}>
<Text
style={{
color: theme.textColorPrimary,
fontSize: 14,
}}
>
{label}
</Text>
<Button
labelStyle={{ letterSpacing: 0 }}
uppercase={false}
color={theme.colorAccent}
onPress={onPress}
>
Browse
</Button>
</View>
</Pressable>
);
};
export default DiscoverCard;
const styles = StyleSheet.create({
discoverCard: {
flexDirection: "row",
alignItems: "center",
marginVertical: 4,
paddingVertical: 8,
paddingHorizontal: 20,
},
discoverIcon: {
width: 40,
height: 40,
borderRadius: 4,
},
label: {
flex: 1,
flexDirection: "row",
justifyContent: "space-between",
alignItems: "center",
marginLeft: 16,
},
});
@@ -1,22 +1,16 @@
import React, { useEffect, useState } from "react";
import {
StyleSheet,
View,
ActivityIndicator,
FlatList,
Button,
} from "react-native";
import { StyleSheet, View, ActivityIndicator, FlatList } from "react-native";
import * as WebBrowser from "expo-web-browser";
import NovelCover from "../../../Components/NovelCover";
import { Searchbar } from "../../../Components/Searchbar";
import ErrorView from "../../../Components/ErrorView";
import { Searchbar } from "../../../components/Searchbar/Searchbar";
import ErrorView from "../../../components/ErrorView";
import { useTheme, useSettings } from "../../../Hooks/reduxHooks";
import { showToast } from "../../../Hooks/showToast";
import { getDeviceOrientation } from "../../../Services/utils/helpers";
import { useTheme, useSettings } from "../../../hooks/reduxHooks";
import { showToast } from "../../../hooks/showToast";
import { getDeviceOrientation } from "../../../services/utils/helpers";
import { scrapeSearchResults, scrapeTopNovels } from "./MyAnimeListScraper";
import BrowseMalCard from "./BrowseMalCard";
const BrowseMalScreen = ({ navigation, route }) => {
const theme = useTheme();
@@ -28,7 +22,6 @@ const BrowseMalScreen = ({ navigation, route }) => {
const [searchText, setSearchText] = useState("");
const baseUrl = "https://api.jikan.moe/v3/";
const malUrl = "https://myanimelist.net/topmanga.php?type=lightnovels";
const getNovels = async (limit) => {
@@ -72,8 +65,9 @@ const BrowseMalScreen = ({ navigation, route }) => {
}, []);
const renderItem = ({ item }) => (
<NovelCover
item={{ novelName: item.novelName, novelCover: item.novelCover }}
<BrowseMalCard
novel={item}
theme={theme}
onPress={() =>
navigation.navigate("GlobalSearch", {
novelName: item.novelName,
@@ -153,8 +147,6 @@ const BrowseMalScreen = ({ navigation, route }) => {
) : (
<FlatList
contentContainerStyle={styles.novelsContainer}
numColumns={getNovelsPerRow()}
key={getNovelsPerRow()}
data={novels}
keyExtractor={(item) => item.novelName}
renderItem={renderItem}
@@ -189,7 +181,7 @@ const styles = StyleSheet.create({
},
novelsContainer: {
flexGrow: 1,
paddingVertical: 8,
paddingBottom: 8,
paddingHorizontal: 4,
},
});
@@ -22,7 +22,12 @@ const scrapeTopNovels = async (pageNo) => {
".jpg";
const novelName = $(this).find("h3").text();
const novel = { novelName, novelCover };
const score = $(this)
.find(".js-top-ranking-score-col > span.score-label")
.text();
const info = $(this).find("div.information").text().split(/\s\s+/);
const novel = { novelName, novelCover, score, info };
novels.push(novel);
});
@@ -58,7 +63,9 @@ const scrapeSearchResults = async (searchTerm) => {
novelId[8].split(".")[0] +
".jpg";
const novel = { novelName, novelCover };
const score = $(this).find("td:nth-child(5)").text().trim();
const novel = { novelName, novelCover, score };
novels.push(novel);
}
@@ -4,14 +4,15 @@ import { StyleSheet, View, FlatList, ActivityIndicator } from "react-native";
import { ProgressBar } from "react-native-paper";
import { useSelector } from "react-redux";
import { Searchbar } from "../../../Components/Searchbar";
import EmptyView from "../../../Components/EmptyView";
import { Searchbar } from "../../../components/Searchbar/Searchbar";
import EmptyView from "../../../components/EmptyView";
import { ScreenContainer } from "../../../Components/Common";
import { ScreenContainer } from "../../../components/Common";
import GlobalSearchSourceItem from "./GlobalSearchSourceItem";
import { showToast } from "../../../Hooks/showToast";
import { useLibrary, useTheme } from "../../../Hooks/reduxHooks";
import { showToast } from "../../../hooks/showToast";
import { useLibrary, useSettings, useTheme } from "../../../hooks/reduxHooks";
import { getSource } from "../../../sources/sources";
const GlobalSearch = ({ route, navigation }) => {
const theme = useTheme();
@@ -21,10 +22,17 @@ const GlobalSearch = ({ route, navigation }) => {
novelName = route.params.novelName;
}
const { sources, pinned } = useSelector((state) => state.sourceReducer);
let {
sources,
pinned,
filters = [],
} = useSelector((state) => state.sourceReducer);
sources = sources.filter((source) => filters.indexOf(source.lang) === -1);
const pinnedSources = sources.filter(
(source) => pinned.indexOf(source.sourceId) !== -1
);
const { searchAllSources = false } = useSettings();
const [loading, setLoading] = useState(false);
const [searchText, setSearchText] = useState(novelName);
@@ -34,60 +42,57 @@ const GlobalSearch = ({ route, navigation }) => {
const library = useLibrary();
useEffect(() => {
let mounted = true;
novelName && onSubmitEditing();
return () => (mounted = false);
}, []);
const clearSearchbar = () => setSearchText("");
const onChangeText = (text) => setSearchText(text);
const onSubmitEditing = () => {
const onSubmitEditing = async () => {
setSearchResults([]);
setProgress(0);
const getSearchUrl = (sourceId) => {
if (sourceId === 1) {
return `https://lnreader-extensions.vercel.app/api/1/search/?s=${searchText}&?o=rating`;
} else {
return `https://lnreader-extensions.vercel.app/api/${sourceId}/search/?s=${searchText}`;
}
};
let globalSearchSources = searchAllSources ? sources : pinnedSources;
pinnedSources.map((source, index) =>
setTimeout(() => {
for (let i = 0; i < globalSearchSources.length; i++) {
try {
setLoading(true);
fetch(getSearchUrl(source.sourceId))
.then((response) => response.json())
.then((json) => {
setSearchResults((searchResults) => [
...searchResults,
{
sourceId: source.sourceId,
sourceName: source.sourceName,
sourceLanguage: source.sourceLanguage,
novels: json,
},
]);
setLoading(false);
})
.catch((error) => {
showToast(error.message);
setSearchResults((searchResults) => [
...searchResults,
{
sourceId: source.sourceId,
sourceName: source.sourceName,
sourceLanguage: source.sourceLanguage,
novels: [],
},
]);
setLoading(false);
})
.finally(() =>
setProgress(
(progress) => progress + 1 / pinnedSources.length
)
);
}, 1000 * index)
);
const source = getSource(globalSearchSources[i].sourceId);
const data = await source.searchNovels(encodeURI(searchText));
setSearchResults((searchResults) => [
...searchResults,
{
sourceId: globalSearchSources[i].sourceId,
sourceName: globalSearchSources[i].sourceName,
lang: globalSearchSources[i].lang,
novels: data,
},
]);
setLoading(false);
setProgress(
(progress) => progress + 1 / globalSearchSources.length
);
} catch (error) {
showToast(
globalSearchSources[i].sourceName + ": " + error.message
);
setSearchResults((searchResults) => [
...searchResults,
{
sourceId: globalSearchSources[i].sourceId,
sourceName: globalSearchSources[i].sourceName,
lang: globalSearchSources[i].lang,
novels: [],
},
]);
setLoading(false);
}
}
};
const renderItem = ({ item }) => (
@@ -111,15 +116,8 @@ const GlobalSearch = ({ route, navigation }) => {
onSubmitEditing={onSubmitEditing}
clearSearchbar={clearSearchbar}
/>
{progress < 1 && progress > 0 && pinned && (
<ProgressBar
color={
progress > 0.9
? theme.colorPrimaryDark
: theme.colorAccent
}
progress={progress}
/>
{progress > 0 && (
<ProgressBar color={theme.colorAccent} progress={progress} />
)}
<FlatList
contentContainerStyle={{
@@ -135,10 +133,12 @@ const GlobalSearch = ({ route, navigation }) => {
{!loading && (
<EmptyView
icon="__φ(..)"
description={`Search a novel in your pinned sources ${
pinned.length === 0
? "(No sources pinned)"
: ""
description={`Search a novel in ${
searchAllSources
? "all sources"
: pinned.length === 0
? "pinned sources\n(No sources pinned)"
: "pinned sources"
}`}
/>
)}

Some files were not shown because too many files have changed in this diff Show More