Compare commits

...

118 Commits

Author SHA1 Message Date
Rajarshee Chatterjee 9b3d65dded refactor: Reorganize reader sheet 2021-08-11 09:53:18 +05:30
Rajarshee Chatterjee aa3a5c2d0c Minor fixes 2021-08-10 14:32:25 +05:30
Rajarshee Chatterjee 2aab5621a6 feat: Select novels in library 2021-08-10 13:34:19 +05:30
Rajarshee Chatterjee b4b7e6770d feat(source): Add Divine Dao Library
Closes LNReader/lnreader-sources#54
2021-08-09 12:44:32 +05:30
Rajarshee Chatterjee 0ed65d64e4 fix: novelCover crash in list view 2021-08-09 07:26:04 +05:30
Rajarshee Chatterjee a7aaa7d230 feat: Custom CSS 2021-08-07 12:08:31 +05:30
Rajarshee Chatterjee e358b3753f feat(source): Add ranobehub 2021-08-07 08:34:08 +05:30
Rajarshee Chatterjee 780a67e1a6 Update README.md 2021-08-06 10:50:00 +05:30
Rajarshee Chatterjee 5b57e78486 Update README.md 2021-08-06 10:37:22 +05:30
Rajarshee Chatterjee 6430c11cae Update README.md 2021-08-06 10:31:09 +05:30
Rajarshee Chatterjee 322bfc224d Full backups (Closes #34) 2021-08-05 12:24:54 +05:30
Rajarshee Chatterjee a14d35f031 Minor fixes 2021-08-05 12:23:59 +05:30
Rajarshee Chatterjee f03b1703e2 fix(source): ArNovel (Closes #61)
Closes LNReader/lnreader-sources#52
2021-08-04 09:20:35 +05:30
Rajarshee Chatterjee 40d2052f16 Custom covers and open links in reader 2021-08-03 14:38:54 +05:30
Rajarshee Chatterjee 01814e92d0 Fix empty novelCover crash 2021-08-03 10:52:13 +05:30
Rajarshee Chatterjee dffc9c3b99 fix(source): LightNovelPub 2021-08-03 10:48:12 +05:30
Rajarshee Chatterjee 8bb3ffe82c fix(source): ComradeMao 2021-08-03 10:40:34 +05:30
Rajarshee Chatterjee c746081dee Minor fixes 2021-08-03 09:44:14 +05:30
Rajarshee Chatterjee d25fe85a64 Fix discord link 2021-08-02 16:52:32 +05:30
Rajarshee Chatterjee 92d9d53da7 Fix no novelCover crash 2021-07-31 13:32:00 +05:30
Rajarshee Chatterjee 17fcd96f16 Source fixes 2021-07-30 09:15:15 +05:30
Rajarshee Chatterjee 31bada1c1a Refactor 2021-07-29 20:49:21 +05:30
Rajarshee Chatterjee 605bd6a514 Add edit info modal 2021-07-29 11:17:13 +05:30
Rajarshee Chatterjee 01bac76bed Minor fixes 2021-07-29 09:35:55 +05:30
Rajarshee Chatterjee b5ed62d9f8 Fix global search and splashscreen 2021-07-28 17:47:23 +05:30
Rajarshee Chatterjee fd21cc6e85 Minor fixes 2021-07-28 12:14:38 +05:30
Rajarshee Chatterjee 40f1495cf1 Add webview option to support tables and original text style 2021-07-28 11:46:57 +05:30
Rajarshee Chatterjee 983b069dba Add Readwn.com source 2021-07-28 09:47:12 +05:30
Rajarshee Chatterjee e21101ca28 Fix build 2021-07-26 17:01:18 +05:30
Bread Genie d381e878bc docs(readme): Add badges (#60) 2021-07-26 16:59:43 +05:30
Rajarshee Chatterjee bcf5632668 Remove history from redux state and minor fixes 2021-07-26 08:24:59 +05:30
Bread Genie ac8bca7f03 feat(sources): Add ReaperScans Novels (#59) 2021-07-25 21:38:33 +05:30
Bread Genie 62a5e27b1e fix: Indonesia -> Indonesian (#58) 2021-07-25 19:19:08 +05:30
Rajarshee Chatterjee 7c5a9a407f Add new sources 2021-07-23 09:04:47 +05:30
Rajarshee Chatterjee d8d231cb90 Add source WLNUpdates 2021-07-22 09:08:40 +05:30
Rajarshee Chatterjee a5f90eaf8b Add new sources 2021-07-21 20:32:30 +05:30
Rajarshee Chatterjee ec50189ea4 Minor fixes 2021-07-21 08:34:45 +05:30
Rajarshee Chatterjee ccdccdb6bb Remove prevChapter and nextChapter 2021-07-20 08:38:55 +05:30
Bread Genie 059f440713 fix(source): nextChapter link undefined (#55) 2021-07-20 07:55:48 +05:30
Rajarshee Chatterjee b695f57ef4 Refactor 2021-07-19 09:32:48 +05:30
Rajarshee Chatterjee db86b707e7 Fix bookmark icon 2021-07-19 06:00:51 +05:30
Rajarshee Chatterjee 7db4e4b872 Minor fixes 2021-07-18 18:21:25 +05:30
Rajarshee Chatterjee a2c6e9fe58 Custom checkbox 2021-07-18 10:31:07 +05:30
Rajarshee Chatterjee 6c27993454 Remove [hr]() chapter content 2021-07-18 10:30:21 +05:30
Rajarshee Chatterjee 63de0a3794 Fix long chapter names (Closes #53) 2021-07-17 20:24:32 +05:30
Marcin Majewski 93fbcf1246 fix(syosetu): change oneshot chapter name (#52)
Changed oneshot chapter names to "Oneshot" to avoid too long chapters names destroying UI.
2021-07-17 19:55:24 +05:30
Rajarshee Chatterjee 50357f7d6f Release v1.1.2 2021-07-17 08:12:04 +05:30
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
248 changed files with 39782 additions and 15613 deletions
-4
View File
@@ -1,4 +0,0 @@
{
"12bb71342c6255bbf50437ec8f4441c083f47cdb74bd89160c15e4f43e52a1cb": true,
"40b842e832070c58deac6aa9e08fa459302ee3f9da492c7e77d93d2fbf4a56fd": true
}
Binary file not shown.

After

Width:  |  Height:  |  Size: 9.0 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.3 MiB

After

Width:  |  Height:  |  Size: 1.1 MiB

+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
+5 -3
View File
@@ -6,16 +6,18 @@ import { Provider as PaperProvider } from "react-native-paper";
import { Provider } from "react-redux";
import { persistor, store } from "./src/redux/store";
import { PersistGate } from "redux-persist/lib/integration/react";
import * as SplashScreen from "expo-splash-screen";
import Main from "./src/Navigators/Main";
import 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);
useEffect(() => {
SplashScreen.preventAutoHideAsync();
createDB();
}, []);
+33 -12
View File
@@ -1,20 +1,41 @@
<h1 align="center">
<br>
LNReader
<br>
</h1>
<p align="center">LNReader is a Tachiyomi-like, free and open source light novel reader for Android.</p>
---
## Screenshots:
<p align="center">
<img src="./.github/readme-images/screenshots.png" align="center" height="350" />
<img src="./.github/readme-images/icon_alt.png" align="center" width="128" />
<p>
<h1 align="center">
LNReader
</h1>
<p align="center">
LNReader is a Tachiyomi-like, free and open source light novel reader for Android.
</p>
<div align="center">
<a href="https://github.com/LNReader/lnreader/actions/workflows/build.yml">
<img alt="GitHub Workflow Status" src="https://img.shields.io/github/workflow/status/LNReader/lnreader/CI">
</a>
<a href="https://discord.gg/QdcWN4MD63">
<img alt="Discord Chat" src="https://img.shields.io/discord/835746409357246465.svg?logo=discord&logoColor=white&logoWidth=20&labelColor=7289DA&label=Discord&color=17cf48">
</a>
<a href="https://github.com/LNReader/lnreader/releases/latest">
<img alt="GitHub release (latest by date)" src="https://img.shields.io/github/v/release/LNReader/lnreader">
</a>
<a href="https://github.com/LNReader/lnreader/blob/main/LICENSE">
<img alt="GitHub" src="https://img.shields.io/github/license/LNReader/lnreader">
</a>
</div>
<br>
<br>
<p align="center">
<img src="./.github/readme-images/screenshots.png" align="center" />
</p>
## Download
Get the app from our [releases page](https://github.com/rajarsheechatterjee/LNReader/releases).
## Sources
LNReader does not have any affiliation with the content providers available.
Source requests should be created at [LNReader/lnreader-sources](https://github.com/LNReader/lnreader-sources).
## License
[MIT ](https://github.com/rajarsheechatterjee/lnreader/blob/main/LICENSE)
+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.2"
}
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>
Binary file not shown.
Binary file not shown.
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",
"version": "1.0.0"
}
-142
View File
@@ -1,142 +0,0 @@
import React, { useRef } from "react";
import { View, TextInput, StyleSheet } from "react-native";
import {
TouchableRipple,
IconButton,
Appbar as MaterialAppbar,
} from "react-native-paper";
import Constants from "expo-constants";
import { useNavigation } from "@react-navigation/native";
import { useSelector } from "react-redux";
export const Appbar = ({ title, onBackAction, style, children }) => {
const theme = useSelector((state) => state.settingsReducer.theme);
return (
<MaterialAppbar.Header
style={[{ backgroundColor: theme.colorPrimary }, style]}
>
{onBackAction && (
<MaterialAppbar.BackAction onPress={onBackAction} />
)}
<MaterialAppbar.Content
title={title}
titleStyle={{ color: theme.textColorPrimary }}
/>
{children}
</MaterialAppbar.Header>
);
};
export const SearchAppbar = ({
placeholder,
getSearchResults,
setSearchText,
searchText,
getNovels,
setLoading,
screen,
openWebView,
}) => {
const searchRef = useRef(null);
const navigation = useNavigation();
const theme = useSelector((state) => state.settingsReducer.theme);
return (
<TouchableRipple
borderless
onPress={() => searchRef.current.focus()}
style={[
styles.searchAppbarContainer,
{ backgroundColor: theme.searchBarColor },
]}
>
<View
style={{
flex: 1,
flexDirection: "row",
justifyContent: "space-between",
}}
>
<View style={{ flex: 1, flexDirection: "row" }}>
{screen === "Library" ? (
<IconButton
icon="magnify"
color={theme.textColorSecondary}
style={{ marginLeft: 0 }}
size={23}
/>
) : (
<IconButton
icon="arrow-left"
color={theme.textColorSecondary}
style={{ marginLeft: 0 }}
size={23}
onPress={() => navigation.goBack()}
/>
)}
<TextInput
ref={searchRef}
style={{
fontSize: 16,
color: theme.textColorSecondary,
}}
placeholder={placeholder}
placeholderTextColor={theme.textColorSecondary}
onChangeText={(text) => {
setSearchText(text);
if (screen === "Library") {
getSearchResults(text);
}
}}
onSubmitEditing={() => {
if (screen === "Extension") {
getSearchResults(searchText);
}
}}
defaultValue={searchText}
/>
</View>
{searchText !== "" && (
<IconButton
icon="close"
color={theme.textColorSecondary}
style={{ marginRight: 0 }}
size={23}
onPress={() => {
setLoading?.(true);
getNovels();
setSearchText("");
}}
/>
)}
{openWebView && (
<IconButton
icon="earth"
color={theme.textColorSecondary}
style={{ marginRight: 0 }}
size={23}
onPress={openWebView}
/>
)}
</View>
</TouchableRipple>
);
};
const styles = StyleSheet.create({
searchAppbarContainer: {
marginTop: Constants.statusBarHeight + 8,
height: 48,
flexDirection: "row",
alignItems: "center",
paddingHorizontal: 16,
marginBottom: 12,
borderRadius: 8,
marginHorizontal: 12,
elevation: 2,
},
});
-92
View File
@@ -1,92 +0,0 @@
import React from "react";
import { StyleSheet } from "react-native";
import { List, Divider as PaperDivider } from "react-native-paper";
import MaterialCommunityIcons from "react-native-vector-icons/MaterialCommunityIcons";
const ListSection = ({ children }) => (
<List.Section style={styles.listSection}>{children}</List.Section>
);
const ListSubHeader = ({ children, theme }) => (
<List.Subheader
style={[styles.listSubHeader, { color: theme.colorAccent }]}
>
{children}
</List.Subheader>
);
const ListItem = ({
title,
description,
icon,
onPress,
theme,
right,
iconColor,
titleStyle,
style,
}) => (
<List.Item
title={title}
style={style}
titleStyle={[{ color: theme.textColorPrimary }, titleStyle]}
description={description}
descriptionStyle={{ color: theme.textColorSecondary }}
descriptionNumberOfLines={1}
left={() =>
icon && (
<List.Icon
color={theme.colorAccent}
icon={icon}
style={{ marginVertical: 0 }}
/>
)
}
right={() =>
right && (
<MaterialCommunityIcons
name={right}
color={iconColor || "#47a84a"}
size={23}
style={{ marginRight: 16, textAlignVertical: "center" }}
/>
)
}
onPress={onPress}
rippleColor={theme.rippleColor}
/>
);
const Divider = ({ theme }) => (
<PaperDivider style={{ backgroundColor: theme.dividerColor, height: 1 }} />
);
const InfoItem = ({ title, icon, theme }) => (
<List.Item
title={title}
titleStyle={{ color: theme.textColorSecondary, fontSize: 14 }}
titleNumberOfLines={5}
left={() =>
icon && (
<List.Icon
color={theme.textColorSecondary}
icon={icon}
style={{ marginVertical: 0 }}
/>
)
}
/>
);
export { ListSection, ListSubHeader, ListItem, Divider, InfoItem };
const styles = StyleSheet.create({
listSection: {
flex: 1,
marginVertical: 0,
},
listSubHeader: {
paddingBottom: 5,
},
});
-65
View File
@@ -1,65 +0,0 @@
import React from "react";
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 }) => {
return (
<TouchableRipple
borderless
centered
rippleColor={theme.rippleColor}
style={styles.listView}
onPress={onPress}
>
<>
<FastImage
source={{ uri: item.novelCover }}
style={styles.extensionIcon}
/>
<Text
style={[
{ color: theme.textColorPrimary },
styles.novelName,
]}
numberOfLines={1}
>
{item.novelName}
</Text>
<View style={styles.badgeContainer}>
{downloadBadge}
{unreadBadge}
</View>
</>
</TouchableRipple>
);
};
export default ListView;
const styles = StyleSheet.create({
listView: {
flex: 1,
flexDirection: "row",
alignItems: "center",
paddingVertical: 8,
paddingHorizontal: 12,
},
extensionIcon: {
width: 40,
height: 40,
borderRadius: 4,
},
novelName: {
flex: 1,
marginLeft: 16,
fontSize: 15,
paddingRight: 8,
flexWrap: "wrap",
},
badgeContainer: {
flexDirection: "row",
justifyContent: "flex-end",
alignItems: "flex-end",
},
});
-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;
-166
View File
@@ -1,166 +0,0 @@
import React, { useEffect, useState } from "react";
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 { 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 [loading, setLoading] = useState(true);
const [novels, setNovels] = useState();
const [error, setError] = useState();
const [searchText, setSearchText] = useState("");
const baseUrl = "https://api.jikan.moe/v3/";
const malUrl = "https://myanimelist.net/topmanga.php?type=lightnovels";
const getNovels = async () => {
try {
const url = `${baseUrl}top/manga/1/novels`;
const res = await fetch(url);
const data = await res.json();
setNovels(data.top);
setLoading(false);
} catch (error) {
setError(error.message);
setNovels([]);
setLoading(false);
showToast(error.message);
}
};
const clearSearchbar = () => {
getNovels();
setLoading(true);
setSearchText("");
};
const getSearchResults = async () => {
try {
setLoading(true);
const data = await scrapeSearchResults(searchText);
setNovels(data);
setLoading(false);
} catch (error) {
setError(error.message);
setNovels([]);
setLoading(false);
showToast(error.message);
}
};
useEffect(() => {
getNovels();
}, []);
const renderItem = ({ item }) => (
<NovelCover
item={{ novelName: item.title, novelCover: item.image_url }}
onPress={() =>
navigation.navigate("GlobalSearch", {
novelName: item.title,
})
}
/>
);
const { displayMode, novelsPerRow } = useSettings();
const orientation = getDeviceOrientation();
const getNovelsPerRow = () => {
if (displayMode === 2) {
return 1;
}
if (orientation === "landscape") {
return 6;
} else {
return novelsPerRow;
}
};
const ListEmptyComponent = () => (
<ErrorView
error={error || "No results found"}
onRetry={() => {
getNovels();
setLoading(true);
setError();
}}
openWebView={() => WebBrowser.openBrowserAsync(topNovelsUrl)}
theme={theme}
/>
);
return (
<View
style={[
styles.container,
{ backgroundColor: theme.colorPrimaryDark },
]}
>
<Searchbar
theme={theme}
placeholder="Search MyAnimeList"
backAction="arrow-left"
onBackAction={() => navigation.goBack()}
searchText={searchText}
onChangeText={(text) => setSearchText(text)}
onSubmitEditing={getSearchResults}
clearSearchbar={clearSearchbar}
actions={[
{
icon: "earth",
onPress: () => WebBrowser.openBrowserAsync(malUrl),
},
]}
/>
{loading ? (
<View style={{ flex: 1, justifyContent: "center" }}>
<ActivityIndicator size={60} color={theme.colorAccent} />
</View>
) : (
<FlatList
contentContainerStyle={styles.novelsContainer}
numColumns={getNovelsPerRow()}
key={getNovelsPerRow()}
data={novels}
keyExtractor={(item) => item.mal_id.toString()}
renderItem={renderItem}
ListEmptyComponent={ListEmptyComponent}
/>
)}
</View>
);
};
export default BrowseMalScreen;
const styles = StyleSheet.create({
container: {
flex: 1,
},
contentContainer: {
flex: 1,
},
novelsContainer: {
flexGrow: 1,
paddingVertical: 8,
paddingHorizontal: 4,
},
});
-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;
@@ -1,442 +0,0 @@
import React, { useState } from "react";
import { StyleSheet, View, Text, Dimensions } from "react-native";
import { ToggleButton, IconButton, Menu, Switch } from "react-native-paper";
import Slider from "@react-native-community/slider";
import Bottomsheet from "rn-sliding-up-panel";
import { fonts } from "../../../Services/utils/constants";
import {
setAppSettings,
setReaderSettings,
} from "../../../redux/settings/settings.actions";
import BottomSheetHandle from "../../../Components/BottomSheetHandle";
import { Row } from "../../../Components/Common";
const ReaderSheet = ({
theme,
reader,
dispatch,
bottomSheetRef,
selectText,
setSelectText,
showScrollPercentage,
}) => {
const [fontMenu, setFontMenu] = useState(false);
const openFontMenu = () => setFontMenu(true);
const closeFontMenu = () => setFontMenu(false);
const ReaderSettingTitle = ({ title }) => (
<Text style={styles.title}>{title}</Text>
);
const textColor = {
1: "rgba(255,255,255,0.7)",
2: "#000000",
3: "#000000",
4: "#FFFFFF",
};
return (
<Bottomsheet
ref={bottomSheetRef}
draggableRange={{ top: 450, bottom: 0 }}
snappingPoints={[0, 450]}
showBackdrop={true}
backdropOpacity={0}
>
<View style={styles.contentContainer}>
<BottomSheetHandle theme={theme} />
<View style={styles.readerSettingsContainer}>
<ReaderSettingTitle title="Text Size" />
<Slider
style={{
width: Dimensions.get("window").width,
height: 40,
}}
value={reader.textSize}
minimumValue={12}
maximumValue={20}
step={1}
minimumTrackTintColor={theme.colorAccent}
maximumTrackTintColor="#000000"
thumbTintColor={theme.colorAccent}
onValueChange={(value) =>
dispatch(setReaderSettings("textSize", value))
}
/>
<Row
style={{
justifyContent: "space-between",
width: "100%",
paddingHorizontal: 16,
}}
>
<ReaderSettingTitle title="Reader Theme" />
<Row>
<ToggleButton.Row
onValueChange={(value) => {
dispatch(
setReaderSettings(
"theme",
value ?? reader.theme
)
);
dispatch(
setReaderSettings(
"textColor",
textColor[value] ?? reader.textColor
)
);
}}
value={reader.theme}
>
<ToggleButton
icon={reader.theme === 1 && "check"}
color="#FFFFFF"
value={1}
style={{
backgroundColor: "#000000",
marginHorizontal: 10,
borderRadius: 50,
borderTopRightRadius: 50,
borderBottomRightRadius: 50,
}}
/>
<ToggleButton
icon={reader.theme === 4 && "check"}
color="#FFFFFF"
value={4}
style={{
backgroundColor: "#444444",
marginHorizontal: 10,
borderWidth: 0,
borderRadius: 50,
borderTopRightRadius: 50,
borderBottomRightRadius: 50,
}}
/>
<ToggleButton
icon={reader.theme === 2 && "check"}
color="#000000"
value={2}
style={{
backgroundColor: "#FFFFFF",
marginHorizontal: 10,
borderRadius: 50,
}}
/>
<ToggleButton
icon={reader.theme === 3 && "check"}
color="#000000"
value={3}
style={{
backgroundColor: "#F4ECD8",
marginHorizontal: 10,
borderRadius: 50,
borderTopLeftRadius: 50,
borderBottomLeftRadius: 50,
}}
/>
</ToggleButton.Row>
</Row>
</Row>
<Row
style={{
justifyContent: "space-between",
width: "100%",
paddingHorizontal: 16,
marginTop: 16,
}}
>
<ReaderSettingTitle title="Text Align" />
<Row>
<ToggleButton.Row
onValueChange={(value) =>
dispatch(
setReaderSettings(
"textAlign",
value ?? reader.textAlign
)
)
}
value={reader.textAlign}
>
<ToggleButton
icon="format-align-left"
color={
reader.textAlign === "left"
? theme.colorAccent
: "#FFFFFF"
}
value="left"
style={{
backgroundColor: "transparent",
marginHorizontal: 10,
borderWidth: 0,
borderTopRightRadius: 4,
borderBottomRightRadius: 4,
}}
/>
<ToggleButton
icon="format-align-justify"
color={
reader.textAlign === "justify"
? theme.colorAccent
: "#FFFFFF"
}
value="justify"
style={{
backgroundColor: "transparent",
marginHorizontal: 10,
borderWidth: 0,
borderRadius: 4,
}}
/>
<ToggleButton
icon="format-align-right"
color={
reader.textAlign === "right"
? theme.colorAccent
: "#FFFFFF"
}
value="right"
style={{
backgroundColor: "transparent",
marginHorizontal: 10,
borderWidth: 0,
borderTopLeftRadius: 4,
borderBottomLeftRadius: 4,
}}
/>
</ToggleButton.Row>
</Row>
</Row>
<Row
style={{
justifyContent: "space-between",
width: "100%",
paddingHorizontal: 16,
marginTop: 16,
}}
>
<ReaderSettingTitle title="Padding" />
<Row>
<IconButton
icon="minus"
color={theme.colorAccent}
size={26}
disabled={reader.padding <= 0 ? true : false}
onPress={() =>
dispatch(
setReaderSettings(
"padding",
reader.padding - 1
)
)
}
style={{ marginVertical: 0 }}
/>
<Text
style={{
color: "#FFFFFF",
paddingHorizontal: 24,
}}
>
{`${reader.padding}%`}
</Text>
<IconButton
icon="plus"
color={theme.colorAccent}
size={26}
disabled={reader.padding >= 10 ? true : false}
onPress={() =>
dispatch(
setReaderSettings(
"padding",
reader.padding + 1
)
)
}
style={{ marginVertical: 0 }}
/>
</Row>
</Row>
<Row
style={{
justifyContent: "space-between",
width: "100%",
paddingHorizontal: 16,
marginTop: 16,
}}
>
<ReaderSettingTitle title="Line Height" />
<Row>
<IconButton
icon="minus"
color={theme.colorAccent}
size={26}
disabled={
reader.lineHeight <= 1.3 ? true : false
}
onPress={() =>
dispatch(
setReaderSettings(
"lineHeight",
reader.lineHeight - 0.1
)
)
}
style={{ marginVertical: 0 }}
/>
<Text
style={{
color: "#FFFFFF",
paddingHorizontal: 24,
}}
>
{`${Math.round(reader.lineHeight * 10) / 10}%`}
</Text>
<IconButton
icon="plus"
color={theme.colorAccent}
size={26}
disabled={reader.lineHeight >= 2 ? true : false}
onPress={() =>
dispatch(
setReaderSettings(
"lineHeight",
reader.lineHeight + 0.1
)
)
}
style={{ marginVertical: 0 }}
/>
</Row>
</Row>
<Row
style={{
justifyContent: "space-between",
width: "100%",
paddingHorizontal: 16,
marginTop: 16,
}}
>
<ReaderSettingTitle title="Font Style" />
<Menu
visible={fontMenu}
onDismiss={closeFontMenu}
contentStyle={{ backgroundColor: theme.menuColor }}
anchor={
<Text
onPress={openFontMenu}
style={{
color: "white",
paddingHorizontal: 16,
fontSize: 16,
fontFamily: reader.fontFamily,
}}
>
{fonts.find(
(font) =>
font.fontFamily ===
reader.fontFamily
).name || "Default"}
</Text>
}
>
{fonts.map((font) => (
<Menu.Item
key={font.fontFamily}
onPress={() =>
dispatch(
setReaderSettings(
"fontFamily",
font.fontFamily
)
)
}
icon={
font.fontFamily === reader.fontFamily &&
"check"
}
title={font.name}
style={{ backgroundColor: theme.menuColor }}
titleStyle={{
color: theme.textColorPrimary,
}}
/>
))}
</Menu>
</Row>
<Row
style={{
justifyContent: "space-between",
width: "100%",
paddingHorizontal: 16,
marginTop: 16,
}}
>
<ReaderSettingTitle title="Show Progress Percentage" />
<Switch
value={showScrollPercentage}
onValueChange={() =>
dispatch(
setAppSettings(
"showScrollPercentage",
!showScrollPercentage
)
)
}
color={theme.colorAccent}
/>
</Row>
<Row
style={{
justifyContent: "space-between",
width: "100%",
paddingHorizontal: 16,
marginTop: 16,
}}
>
<View>
<ReaderSettingTitle title="Select Text " />
<Text
style={{
fontSize: 14,
color: "white",
}}
>
(Justify does not work with selectable text)
</Text>
</View>
<Switch
value={selectText}
onValueChange={setSelectText}
color={theme.colorAccent}
/>
</Row>
</View>
</View>
</Bottomsheet>
);
};
export default ReaderSheet;
const styles = StyleSheet.create({
contentContainer: {
flex: 1,
borderTopLeftRadius: 8,
borderTopRightRadius: 8,
backgroundColor: "rgba(0,0,0,0.7)",
},
readerSettingsContainer: {
flex: 1,
alignItems: "center",
paddingTop: 30,
},
title: {
color: "#FFFFFF",
fontWeight: "bold",
},
});
-178
View File
@@ -1,178 +0,0 @@
import React, { useState, useCallback, useRef } from "react";
import {
StyleSheet,
Text,
View,
RefreshControl,
ActivityIndicator,
TouchableOpacity,
} from "react-native";
import { useFocusEffect } from "@react-navigation/native";
import { useSelector, useDispatch } from "react-redux";
import { Searchbar } from "../../Components/Searchbar";
import NovelList from "../../Components/NovelList";
import NovelCover from "../../Components/NovelCover";
import EmptyView from "../../Components/EmptyView";
import {
getLibraryAction,
searchLibraryAction,
} from "../../redux/library/library.actions";
import { updateLibraryAction } from "../../redux/updates/updates.actions";
import { useSettings, useTheme } from "../../Hooks/reduxHooks";
import { setNovel } from "../../redux/novel/novel.actions";
import { Portal, TouchableRipple } from "react-native-paper";
import LibraryFilterSheet from "./components/LibraryFilterSheet";
const LibraryScreen = ({ navigation }) => {
const theme = useTheme();
const dispatch = useDispatch();
const { showNumberOfNovels } = useSettings();
const libraryFilterSheetRef = useRef(null);
const {
loading,
novels,
filters: { sort, filter },
} = useSelector((state) => state.libraryReducer);
const [refreshing, setRefreshing] = useState(false);
const [searchText, setSearchText] = useState("");
useFocusEffect(
useCallback(() => {
setSearchText("");
dispatch(getLibraryAction(sort, filter));
}, [getLibraryAction, sort, filter])
);
const onRefresh = () => {
setRefreshing(true);
dispatch(updateLibraryAction());
setRefreshing(false);
};
const clearSearchbar = () => {
dispatch(getLibraryAction(sort, filter));
setSearchText("");
};
const onChangeText = (text) => {
setSearchText(text);
dispatch(searchLibraryAction(text, sort, filter));
};
const renderItem = ({ item }) => (
<NovelCover
item={item}
onPress={() => {
navigation.navigate("Novel", item);
dispatch(setNovel(item));
}}
/>
);
const refreshControl = () => (
<RefreshControl
refreshing={refreshing}
onRefresh={onRefresh}
colors={["white"]}
progressBackgroundColor={theme.colorAccent}
/>
);
// if (!loading) {
// console.log(novels);
// }
const listEmptyComponent = () =>
searchText === "" && (
<EmptyView
icon="Σ(ಠ_ಠ)"
description="Your library is empty. Add series to your library from Browse."
/>
);
return (
<>
<View
style={[
styles.container,
{ backgroundColor: theme.colorPrimaryDark },
]}
>
<Searchbar
placeholder={`Search Library ${
showNumberOfNovels ? "(" + novels.length + ")" : ""
}`}
searchText={searchText}
clearSearchbar={clearSearchbar}
onChangeText={onChangeText}
backAction="magnify"
actions={[
{
icon: "filter-variant",
onPress: () => libraryFilterSheetRef.current.show(),
color: filter
? theme.filterColor
: theme.textColorSecondary,
},
]}
theme={theme}
/>
{loading ? (
<ActivityIndicator size="small" color={theme.colorAccent} />
) : (
<>
{searchText !== "" && (
<TouchableRipple
onPress={() =>
navigation.navigate("GlobalSearch", {
novelName: searchText,
})
}
borderless
rippleColor={theme.colorAccent}
style={styles.emptySearch}
>
<Text style={{ color: theme.colorAccent }}>
{`Search for "${searchText}" globally`}
</Text>
</TouchableRipple>
)}
<NovelList
data={novels}
renderItem={renderItem}
refreshControl={refreshControl()}
ListEmptyComponent={listEmptyComponent}
/>
<Portal>
<LibraryFilterSheet
bottomSheetRef={libraryFilterSheetRef}
theme={theme}
dispatch={dispatch}
sort={sort}
filter={filter}
/>
</Portal>
</>
)}
</View>
</>
);
};
export default LibraryScreen;
const styles = StyleSheet.create({
container: {
flex: 1,
},
emptySearch: {
alignItems: "center",
paddingVertical: 8,
borderRadius: 4,
},
});
@@ -1,362 +0,0 @@
import React, { useState } from "react";
import {
StyleSheet,
View,
Text,
Animated,
useWindowDimensions,
} from "react-native";
import { TabView, SceneMap, TabBar } from "react-native-tab-view";
import { Checkbox } from "react-native-paper";
import Bottomsheet from "rn-sliding-up-panel";
import { ListItem } from "../../../Components/List";
import { filterLibrary } from "../../../redux/library/library.actions";
import { useSettings } from "../../../Hooks/reduxHooks";
import { setAppSettings } from "../../../redux/settings/settings.actions";
const LibraryFilterSheet = ({
bottomSheetRef,
dispatch,
sort,
filter,
theme,
}) => {
const [animatedValue] = useState(new Animated.Value(0));
const FirstRoute = () => (
<View style={{ flex: 1 }}>
<Checkbox.Item
label="Downloaded"
labelStyle={{
fontSize: 14,
color: theme.textColorPrimary,
}}
color={theme.colorAccent}
uncheckedColor={theme.textColorHint}
status={
filter === "chaptersDownloaded > 0"
? "checked"
: "unchecked"
}
onPress={() =>
filter === "chaptersDownloaded > 0"
? dispatch(filterLibrary(sort, ""))
: dispatch(
filterLibrary(sort, "chaptersDownloaded > 0")
)
}
/>
<Checkbox.Item
label="Unread"
labelStyle={{
fontSize: 14,
color: theme.textColorPrimary,
}}
color={theme.colorAccent}
uncheckedColor={theme.textColorHint}
status={filter === "unread = 1" ? "checked" : "unchecked"}
onPress={() =>
filter === "unread = 1"
? dispatch(filterLibrary(sort, ""))
: dispatch(filterLibrary(sort, "unread = 1"))
}
/>
<Checkbox.Item
label="Completed"
labelStyle={{
fontSize: 14,
color: theme.textColorPrimary,
}}
color={theme.colorAccent}
uncheckedColor={theme.textColorHint}
status={
filter === "chaptersUnread IS NULL"
? "checked"
: "unchecked"
}
onPress={() =>
filter === "chaptersUnread IS NULL"
? dispatch(filterLibrary(sort, ""))
: dispatch(
filterLibrary(sort, "chaptersUnread IS NULL")
)
}
/>
</View>
);
const SecondRoute = () => (
<View style={{ flex: 1 }}>
<ListItem
style={{ paddingVertical: 10 }}
title="Date Added"
titleStyle={{ fontSize: 14 }}
theme={theme}
right={
sort === "novels.novelId ASC"
? "arrow-up"
: sort === "novels.novelId DESC"
? "arrow-down"
: null
}
iconColor={theme.colorAccent}
onPress={() =>
sort === "novels.novelId ASC"
? dispatch(filterLibrary("novels.novelId DESC", filter))
: dispatch(filterLibrary("novels.novelId ASC", filter))
}
/>
<ListItem
style={{ paddingVertical: 10 }}
title="Alphabetically"
titleStyle={{ fontSize: 14 }}
theme={theme}
right={
sort === "novels.novelName ASC"
? "arrow-up"
: sort === "novels.novelName DESC"
? "arrow-down"
: null
}
iconColor={theme.colorAccent}
onPress={() =>
sort === "novels.novelName ASC"
? dispatch(
filterLibrary("novels.novelName DESC", filter)
)
: dispatch(
filterLibrary("novels.novelName ASC", filter)
)
}
/>
<ListItem
style={{ paddingVertical: 10 }}
title="Unread"
titleStyle={{ fontSize: 14 }}
theme={theme}
right={
sort === "novels.unread ASC"
? "arrow-up"
: sort === "novels.unread DESC"
? "arrow-down"
: null
}
iconColor={theme.colorAccent}
onPress={() =>
sort === "novels.unread ASC"
? dispatch(filterLibrary("novels.unread DESC", filter))
: dispatch(filterLibrary("novels.unread ASC", filter))
}
/>
<ListItem
style={{ paddingVertical: 10 }}
title="Downloaded"
titleStyle={{ fontSize: 14 }}
theme={theme}
right={
sort === "chaptersDownloaded ASC"
? "arrow-up"
: sort === "chaptersDownloaded DESC"
? "arrow-down"
: null
}
iconColor={theme.colorAccent}
onPress={() =>
sort === "chaptersDownloaded ASC"
? dispatch(
filterLibrary("chaptersDownloaded DESC", filter)
)
: dispatch(
filterLibrary("chaptersDownloaded ASC", filter)
)
}
/>
<ListItem
style={{ paddingVertical: 10 }}
title="Total Chapters"
titleStyle={{ fontSize: 14 }}
theme={theme}
right={
sort === "chaptersUnread ASC"
? "arrow-up"
: sort === "chaptersUnread DESC"
? "arrow-down"
: null
}
iconColor={theme.colorAccent}
onPress={() =>
sort === "chaptersUnread ASC"
? dispatch(filterLibrary("chaptersUnread DESC", filter))
: dispatch(filterLibrary("chaptersUnread ASC", filter))
}
/>
</View>
);
const displayModes = [
{ displayMode: 0, label: "Compact Grid" },
{ displayMode: 1, label: "Comfortable Grid" },
{ displayMode: 2, label: "List" },
];
const {
displayMode,
showDownloadBadges,
showUnreadBadges,
showNumberOfNovels,
} = useSettings();
const renderCheckboxes = () => {
return displayModes.map((mode) => (
<Checkbox.Item
key={mode.displayMode}
label={mode.label}
labelStyle={{ color: theme.textColorPrimary, fontSize: 14 }}
status={
displayMode === mode.displayMode ? "checked" : "unchecked"
}
// mode="ios"
uncheckedColor={theme.textColorSecondary}
color={theme.colorAccent}
onPress={() =>
dispatch(setAppSettings("displayMode", mode.displayMode))
}
/>
));
};
const ThirdRoute = () => (
<View style={{ flex: 1 }}>
<Text
style={{
color: theme.textColorSecondary,
paddingHorizontal: 16,
paddingVertical: 16,
textTransform: "uppercase",
}}
>
Display Mode
</Text>
{renderCheckboxes()}
<Text
style={{
color: theme.textColorSecondary,
paddingHorizontal: 16,
paddingVertical: 16,
textTransform: "uppercase",
}}
>
Badges
</Text>
<Checkbox.Item
label="Download Badges"
labelStyle={{ color: theme.textColorPrimary, fontSize: 14 }}
status={showDownloadBadges ? "checked" : "unchecked"}
uncheckedColor={theme.textColorSecondary}
color={theme.colorAccent}
onPress={() =>
dispatch(
setAppSettings(
"showDownloadBadges",
!showDownloadBadges
)
)
}
/>
<Checkbox.Item
label="Unread Badges"
labelStyle={{ color: theme.textColorPrimary, fontSize: 14 }}
status={showUnreadBadges ? "checked" : "unchecked"}
uncheckedColor={theme.textColorSecondary}
color={theme.colorAccent}
onPress={() =>
dispatch(
setAppSettings("showUnreadBadges", !showUnreadBadges)
)
}
/>
<Checkbox.Item
label="Show number of items"
labelStyle={{ color: theme.textColorPrimary, fontSize: 14 }}
status={showNumberOfNovels ? "checked" : "unchecked"}
uncheckedColor={theme.textColorSecondary}
color={theme.colorAccent}
onPress={() =>
dispatch(
setAppSettings(
"showNumberOfNovels",
!showNumberOfNovels
)
)
}
/>
</View>
);
const renderScene = SceneMap({
first: FirstRoute,
second: SecondRoute,
third: ThirdRoute,
});
const layout = useWindowDimensions();
const [index, setIndex] = useState(0);
const [routes] = useState([
{ key: "first", title: "Filter" },
{ key: "second", title: "Sort" },
{ key: "third", title: "Display" },
]);
const renderTabBar = (props) => (
<TabBar
{...props}
indicatorStyle={{ backgroundColor: theme.colorAccent }}
style={{ backgroundColor: theme.colorPrimary }}
renderLabel={({ route, focused, color }) => (
<Text style={{ color, margin: 8 }}>{route.title}</Text>
)}
inactiveColor={theme.textColorSecondary}
activeColor={theme.colorAccent}
pressColor={theme.rippleColor}
/>
);
return (
<Bottomsheet
animatedValue={animatedValue}
ref={bottomSheetRef}
draggableRange={{ top: 470, bottom: 0 }}
snappingPoints={[0, 470]}
>
<View
style={[
styles.contentContainer,
{ backgroundColor: theme.colorPrimaryDark },
]}
>
<TabView
navigationState={{ index, routes }}
renderTabBar={renderTabBar}
renderScene={renderScene}
onIndexChange={setIndex}
initialLayout={{ width: layout.width }}
style={{ borderTopRightRadius: 8, borderTopLeftRadius: 8 }}
/>
</View>
</Bottomsheet>
);
};
export default LibraryFilterSheet;
const styles = StyleSheet.create({
contentContainer: {
flex: 1,
borderTopRightRadius: 8,
borderTopLeftRadius: 8,
},
});
-72
View File
@@ -1,72 +0,0 @@
import React from "react";
import { View } from "react-native";
import * as Linking from "expo-linking";
import { Appbar } from "../../Components/Appbar";
import { useSelector } from "react-redux";
import { Divider, ListItem, ListSection } from "../../Components/List";
const AboutScreen = ({ navigation }) => {
const theme = useSelector((state) => state.settingsReducer.theme);
return (
<>
<Appbar title="About" onBackAction={() => navigation.goBack()} />
<View style={{ flex: 1, backgroundColor: theme.colorPrimaryDark }}>
<ListSection>
<ListItem
title="Version"
description="Stable 1.0.21"
theme={theme}
/>
<ListItem
title="Build Time"
description="09-06-21 14:43 PM"
theme={theme}
/>
<ListItem
title="What's new"
onPress={() =>
Linking.openURL(
"https://github.com/rajarsheechatterjee/lnreader/commits/main"
)
}
theme={theme}
/>
<Divider theme={theme} />
<ListItem
title="Discord"
description="https://discord.gg/QdcWN4MD63"
onPress={() =>
Linking.openURL("https://discord.gg/QdcWN4MD63")
}
theme={theme}
/>
<ListItem
title="Github"
description="https://github.com/rajarsheechatterjee/lnreader"
onPress={() =>
Linking.openURL(
"https://github.com/rajarsheechatterjee/lnreader"
)
}
theme={theme}
/>
<ListItem
title="Sources"
description="https://github.com/rajarsheechatterjee/lnreader-sources"
onPress={() =>
Linking.openURL(
"https://github.com/rajarsheechatterjee/lnreader-sources"
)
}
theme={theme}
/>
</ListSection>
</View>
</>
);
};
export default AboutScreen;
-133
View File
@@ -1,133 +0,0 @@
import React, { useState } from "react";
import { useDispatch, useSelector } from "react-redux";
import { Appbar } from "../../Components/Appbar";
import {
Divider,
ListItem,
ListSection,
ListSubHeader,
} from "../../Components/List";
import { ScreenContainer } from "../../Components/Common";
import { useTheme } from "../../Hooks/reduxHooks";
import DisplayModeModal from "./components/DisplayModeModal";
import GridSizeModal from "./components/GridSizeModal";
import ThemeModal from "./components/ThemeModal";
import AccentColorModal from "./components/AccentColorModal";
import { setAccentColor } from "../../redux/settings/settings.actions";
import ColorPickerModal from "../../Components/ColorPickerModal";
const GenralSettings = ({ navigation }) => {
const theme = useTheme();
const dispatch = useDispatch();
const { displayMode, novelsPerRow } = useSelector(
(state) => state.settingsReducer
);
const displayModeLabel = (displayMode) => {
const label = {
0: "Compact Grid",
1: "Comfortable Grid",
2: "List",
};
return label[displayMode] ?? "Compact Grid";
};
/**
* Display Mode Modal
*/
const [displayModalVisible, setDisplayModalVisible] = useState(false);
const showDisplayModal = () => setDisplayModalVisible(true);
const hideDisplayModal = () => setDisplayModalVisible(false);
/**
* Theme Modal
*/
const [themeModalVisible, setthemeModalVisible] = useState(false);
const showthemeModal = () => setthemeModalVisible(true);
const hidethemeModal = () => setthemeModalVisible(false);
/**
* Grid Size Modal
*/
const [gridSizeModalVisible, setGridSizeModalVisible] = useState(false);
const showGridSizeModal = () => setGridSizeModalVisible(true);
const hideGridSizeModal = () => setGridSizeModalVisible(false);
/**
* Accent Color Modal
*/
const [accentColorModal, setAccentColorModal] = useState(false);
const showAccentColorModal = () => setAccentColorModal(true);
const hideAccentColorModal = () => setAccentColorModal(false);
return (
<ScreenContainer theme={theme}>
<Appbar title="General" onBackAction={() => navigation.goBack()} />
<ListSection>
<ListSubHeader theme={theme}>Display</ListSubHeader>
<ListItem
title="Display Mode"
description={displayModeLabel(displayMode)}
onPress={showDisplayModal}
theme={theme}
/>
<ListItem
title="Items per row in library"
description={`${novelsPerRow} items per row`}
onPress={showGridSizeModal}
theme={theme}
/>
<Divider theme={theme} />
<ListSubHeader theme={theme}>Theme</ListSubHeader>
<ListItem
title="Theme"
description={theme.name}
onPress={showthemeModal}
theme={theme}
/>
<ListItem
title="Accent Color"
description={theme.colorAccent.toUpperCase()}
onPress={showAccentColorModal}
theme={theme}
iconColor={theme.colorAccent}
right="circle"
/>
</ListSection>
<DisplayModeModal
displayMode={displayMode}
displayModalVisible={displayModalVisible}
hideDisplayModal={hideDisplayModal}
dispatch={dispatch}
theme={theme}
/>
<GridSizeModal
dispatch={dispatch}
novelsPerRow={novelsPerRow}
gridSizeModalVisible={gridSizeModalVisible}
hideGridSizeModal={hideGridSizeModal}
theme={theme}
/>
<ThemeModal
dispatch={dispatch}
themeModalVisible={themeModalVisible}
hidethemeModal={hidethemeModal}
theme={theme}
/>
<ColorPickerModal
title="Accent color"
modalVisible={accentColorModal}
hideModal={hideAccentColorModal}
color={theme.colorAccent}
onSubmit={(val) => dispatch(setAccentColor(val))}
theme={theme}
/>
</ScreenContainer>
);
};
export default GenralSettings;
-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({});
-278
View File
@@ -1,278 +0,0 @@
import React, { useState } from "react";
import { Pressable, ScrollView, StyleSheet, Text, View } from "react-native";
import { ToggleButton } from "react-native-paper";
import { useDispatch } from "react-redux";
import { Appbar } from "../../Components/Appbar";
import ColorPickerModal from "../../Components/ColorPickerModal";
import { Row, ScreenContainer } from "../../Components/Common";
import { ListItem, ListSection, ListSubHeader } from "../../Components/List";
import { useReaderSettings, useTheme } from "../../Hooks/reduxHooks";
import { setReaderSettings } from "../../redux/settings/settings.actions";
import {
readerBackground,
readerLineHeight,
readerTextColor,
} from "../Chapter/readerStyleController";
const ReaderSettings = ({ navigation }) => {
const theme = useTheme();
const dispatch = useDispatch();
const reader = useReaderSettings();
const textColor = {
1: "rgba(255,255,255,0.7)",
2: "#000000",
3: "#000000",
4: "#FFFFFF",
};
/**
* Reader Background Color Modal
*/
const [readerBackgroundColorModal, setReaderBackgroundtColorModal] =
useState(false);
const showReaderBackgroundtColorModal = () =>
setReaderBackgroundtColorModal(true);
const hideReaderBackgroundtColorModal = () =>
setReaderBackgroundtColorModal(false);
/**
* Reader Text Color Modal
*/
const [readerTextColorModal, setReaderTextColorModal] = useState(false);
const showReaderTextColorModal = () => setReaderTextColorModal(true);
const hideReaderTextColorModal = () => setReaderTextColorModal(false);
const setReaderBackground = (val) => {
dispatch(setReaderSettings("theme", val));
};
const setReaderTextColor = (val) => {
dispatch(setReaderSettings("textColor", val ?? reader.textColor));
};
const readerStyles = [
{
paddingVertical: 16,
paddingBottom: 32,
fontSize: reader.textSize,
color: reader.textColor,
lineHeight: readerLineHeight(reader.textSize, reader.lineHeight),
textAlign: reader.textAlign,
paddingHorizontal: `${reader.padding}%`,
},
reader.fontFamily && {
fontFamily: reader.fontFamily,
},
];
const presetThemes = [
{
value: 1,
backgroundColor: "#000000",
textColor: "rgba(255,255,255,0.7)",
},
{ value: 2, backgroundColor: "#FFFFFF", textColor: "#000000" },
{ value: 3, backgroundColor: "#F4ECD8", textColor: "#000000" },
{ value: 4, backgroundColor: "#444444", textColor: "#FFFFFF" },
];
const onValueChange = (value) => {
dispatch(setReaderSettings("theme", value ?? reader.theme));
dispatch(
setReaderSettings("textColor", textColor[value] ?? reader.textColor)
);
};
const textAlignments = [
{ value: "left", icon: "format-align-left" },
{ value: "justify", icon: "format-align-justify" },
{ value: "right", icon: "format-align-right" },
];
const onValueChangeTextAlign = (value) => {
dispatch(setReaderSettings("textAlign", value ?? reader.textAlign));
};
return (
<ScreenContainer theme={theme}>
<Appbar title="Reader" onBackAction={navigation.goBack} />
<ScrollView>
<View
style={{ backgroundColor: readerBackground(reader.theme) }}
>
<Text style={readerStyles}>
Lorem ipsum dolor sit amet, consectetur adipiscing elit.
Ut lobortis nunc id nisl tempor, sed tristique sem
sagittis. Mauris consequat orci egestas volutpat
vestibulum. Nullam convallis, leo in porttitor
fringilla, ex mi finibus felis, non molestie ligula
ligula facilisis ligula. Nam eu est a turpis porttitor
aliquet sed vel risus. Ut ac scelerisque diam. Nulla
fringilla enim non ex rhoncus faucibus. Integer eget
lacus justo. Lorem ipsum dolor sit amet, consectetur
adipiscing elit. Ut vitae tempor metus. Morbi nec nisl
cursus, gravida ex vitae, laoreet leo. Pellentesque in
lectus ante. Aliquam porta suscipit pretium. Sed ut
ligula at dolor pretium egestas quis vitae urna. In
ullamcorper venenatis orci, eu pulvinar nibh venenatis
sit amet.
</Text>
</View>
<ListSection>
<ListSubHeader theme={theme}>Preset</ListSubHeader>
<Pressable
android_ripple={{ color: theme.rippleColor }}
style={styles.pressableListItem}
>
<Text
style={{
color: theme.textColorPrimary,
fontSize: 16,
}}
>
Preset Themes
</Text>
<Row>
<ToggleButton.Row
onValueChange={onValueChange}
value={reader.theme}
>
{presetThemes.map((theme) => (
<ToggleButton
key={theme.value}
icon={
reader.theme === theme.value &&
"check"
}
color={theme.textColor}
value={theme.value}
style={[
{
backgroundColor:
theme.backgroundColor,
},
styles.presetToggleButton,
]}
/>
))}
</ToggleButton.Row>
</Row>
</Pressable>
<ListSubHeader theme={theme}>Reader</ListSubHeader>
<ListItem
title="Background Color"
description={readerBackground(
reader.theme
).toUpperCase()}
onPress={showReaderBackgroundtColorModal}
theme={theme}
iconColor={readerBackground(reader.theme)}
right="circle"
/>
<ListItem
title="Text Color"
description={
reader.textColor.toUpperCase() ||
readerTextColor(reader.theme)
}
onPress={showReaderTextColorModal}
theme={theme}
iconColor={
reader.textColor || readerTextColor(reader.theme)
}
right="circle"
/>
<ListSubHeader theme={theme}>Text</ListSubHeader>
<Pressable
android_ripple={{ color: theme.rippleColor }}
style={styles.pressableListItem}
>
<View>
<Text
style={{
color: theme.textColorPrimary,
fontSize: 16,
}}
>
Text Align
</Text>
<Text
style={{
color: theme.textColorSecondary,
textTransform: "capitalize",
}}
>
{reader.textAlign}
</Text>
</View>
<Row>
<ToggleButton.Row
onValueChange={onValueChangeTextAlign}
value={reader.textAlign}
>
{textAlignments.map((item) => (
<ToggleButton
key={item.value}
icon={item.icon}
color={
reader.textAlign === item.value
? theme.colorAccent
: "#FFFFFF"
}
value={item.value}
style={{
backgroundColor: "transparent",
marginHorizontal: 10,
borderWidth: 0,
borderRadius: 4,
}}
/>
))}
</ToggleButton.Row>
</Row>
</Pressable>
</ListSection>
</ScrollView>
<ColorPickerModal
title="Reader background color"
modalVisible={readerBackgroundColorModal}
color={readerBackground(reader.theme)}
hideModal={hideReaderBackgroundtColorModal}
theme={theme}
onSubmit={setReaderBackground}
/>
<ColorPickerModal
title="Reader text color"
modalVisible={readerTextColorModal}
color={reader.textColor}
hideModal={hideReaderTextColorModal}
theme={theme}
onSubmit={setReaderTextColor}
/>
</ScreenContainer>
);
};
export default ReaderSettings;
const styles = StyleSheet.create({
pressableListItem: {
padding: 16,
flexDirection: "row",
justifyContent: "space-between",
alignItems: "center",
},
presetToggleButton: {
marginLeft: 12,
borderRadius: 50,
borderTopRightRadius: 50,
borderBottomRightRadius: 50,
borderTopLeftRadius: 50,
borderBottomLeftRadius: 50,
borderWidth: 0,
},
});
@@ -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,
},
});
@@ -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",
},
});
@@ -1,30 +0,0 @@
import React from "react";
import { Text, StyleSheet } from "react-native";
export const GenreChip = ({ children, theme }) => (
<Text
style={[
styles.genreChip,
{
color: theme.colorAccent,
borderColor: theme.colorAccent,
backgroundColor: theme.colorPrimary,
},
]}
>
{children}
</Text>
);
const styles = StyleSheet.create({
genreChip: {
flex: 1,
justifyContent: "center",
paddingVertical: 2,
paddingHorizontal: 10,
marginHorizontal: 2,
fontSize: 13,
borderWidth: 1,
borderRadius: 50,
},
});
-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;
}
};
-14
View File
@@ -1,14 +0,0 @@
/**
* Reader fonts
*/
export const fonts = {
"pt-sans-bold": require("../../assets/fonts/PTSansNarrow-Bold.ttf"),
"arbutus-slab": require("../../assets/fonts/ArbutusSlab-Regular.ttf"),
domine: require("../../assets/fonts/Domine-VariableFont_wght.ttf"),
lato: require("../../assets/fonts/Lato-Regular.ttf"),
"noto-sans": require("../../assets/fonts/NotoSans-Regular.ttf"),
"noto-sans-bold": require("../../assets/fonts/NotoSans-Bold.ttf"),
"open-sans": require("../../assets/fonts/OpenSans-Regular.ttf"),
"pt-serif": require("../../assets/fonts/PTSerif-Regular.ttf"),
};
-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,
},
});
+24
View File
@@ -0,0 +1,24 @@
import React from "react";
import { Appbar as MaterialAppbar } from "react-native-paper";
import { useTheme } from "../hooks/reduxHooks";
export const Appbar = ({ title, onBackAction, style, children }) => {
const theme = useTheme();
return (
<MaterialAppbar.Header
style={[{ backgroundColor: theme.colorPrimary }, style]}
>
{onBackAction && (
<MaterialAppbar.BackAction onPress={onBackAction} />
)}
<MaterialAppbar.Content
title={title}
titleStyle={{ color: theme.textColorPrimary }}
/>
{children}
</MaterialAppbar.Header>
);
};
@@ -4,11 +4,11 @@ import { View } from "react-native";
const BottomSheetHandle = ({ theme }) => (
<View
style={{
backgroundColor: theme.textColorHint,
height: 5,
width: 30,
backgroundColor: "rgba(255,255,255,0.4)",
height: 4,
width: 40,
borderRadius: 50,
top: 10,
top: 8,
alignSelf: "center",
}}
/>
+73
View File
@@ -0,0 +1,73 @@
import React from "react";
import { Pressable, StyleSheet, Text, View } from "react-native";
import MaterialCommunityIcons from "react-native-vector-icons/MaterialCommunityIcons";
export const Checkbox = ({ label, status, onPress, theme }) => (
<View
style={[
styles.checkboxContainer,
status && { backgroundColor: theme.rippleColor },
]}
>
<Pressable
android_ripple={{ color: theme.rippleColor }}
style={styles.pressable}
onPress={onPress}
>
<Text style={{ color: theme.textColorPrimary }}>{label}</Text>
{status && (
<MaterialCommunityIcons
name="check"
color={theme.colorAccent}
size={21}
style={styles.icon}
/>
)}
</Pressable>
</View>
);
export const SortItem = ({ label, status, onPress, theme }) => (
<View
style={[
styles.checkboxContainer,
status && { backgroundColor: theme.rippleColor },
]}
>
<Pressable
android_ripple={{ color: theme.rippleColor }}
style={styles.pressable}
onPress={onPress}
>
<Text style={{ color: theme.textColorPrimary }}>{label}</Text>
{status && (
<MaterialCommunityIcons
name={status === "asc" ? "arrow-up" : "arrow-down"}
color={theme.colorAccent}
size={21}
style={styles.icon}
/>
)}
</Pressable>
</View>
);
const styles = StyleSheet.create({
checkboxContainer: {
borderRadius: 8,
overflow: "hidden",
marginVertical: 4,
},
pressable: {
paddingHorizontal: 16,
paddingVertical: 12,
flexDirection: "row",
justifyContent: "space-between",
},
icon: {
position: "absolute",
right: 16,
alignSelf: "center",
},
});
@@ -10,6 +10,7 @@ const ColorPickerModal = ({
onSubmit,
hideModal,
modalVisible,
showAccentColors,
}) => {
const [text, setText] = useState(color);
const [error, setError] = useState();
+72
View File
@@ -0,0 +1,72 @@
import React from "react";
import { Pressable, View } from "react-native";
import MaterialCommunityIcons from "react-native-vector-icons/MaterialCommunityIcons";
export const ToggleButton = ({ icon, selected, theme, color, onPress }) => (
<View
style={{
borderRadius: 8,
overflow: "hidden",
marginHorizontal: 4,
}}
>
<Pressable
android_ripple={{ color: theme.rippleColor }}
style={{
padding: 10,
alignItems: "center",
justifyContent: "center",
}}
onPress={onPress}
>
<MaterialCommunityIcons
name={icon}
color={
selected
? theme.colorAccent
: color
? color
: theme.textColorPrimary
}
size={24}
/>
</Pressable>
</View>
);
export const ToggleColorButton = ({
selected,
backgroundColor,
textColor,
onPress,
}) => (
<View
style={{
borderRadius: 50,
overflow: "hidden",
marginHorizontal: 6,
height: 44,
width: 44,
}}
>
<Pressable
android_ripple={{ color: textColor }}
style={{
flex: 1,
padding: 10,
backgroundColor: backgroundColor,
alignItems: "center",
justifyContent: "center",
}}
onPress={onPress}
>
{selected && (
<MaterialCommunityIcons
name="check"
color={textColor}
size={24}
/>
)}
</Pressable>
</View>
);
@@ -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>
+75
View File
@@ -0,0 +1,75 @@
import React from "react";
import { Pressable, StyleSheet, Text, View } from "react-native";
import { IconButton } from "react-native-paper";
const icons = ["(・o・;)", "Σ(ಠ_ಠ)", "ಥ_ಥ", "(˘・_・˘)", "(; ̄Д ̄)", "(・Д・。"];
export const ErrorView = ({ errorName, actions, theme }) => (
<View style={{ flex: 1, justifyContent: "center", alignItems: "center" }}>
<Text style={[styles.emptyViewIcon, { color: theme.textColorHint }]}>
{/* {icons[Math.floor(Math.random() * 5)]} */}
ಥ_ಥ
</Text>
<Text style={[styles.emptyViewText, { color: theme.textColorHint }]}>
{errorName}
</Text>
<View style={{ flexDirection: "row" }}>
{actions.map((action) => (
<View
style={{
borderRadius: 4,
overflow: "hidden",
margin: 16,
}}
>
<Pressable
android_ripple={{
color: theme.rippleColor,
borderless: false,
}}
onPress={action.onPress}
style={{
justifyContent: "center",
alignItems: "center",
paddingBottom: 8,
paddingHorizontal: 20,
minWidth: 100,
}}
>
<IconButton
icon={action.icon}
color={theme.textColorHint}
size={24}
style={{ margin: 0 }}
/>
<Text
style={{
fontSize: 12,
color: theme.textColorHint,
}}
>
{action.name}
</Text>
</Pressable>
</View>
))}
</View>
</View>
);
const styles = StyleSheet.create({
emptyViewContainer: {
flex: 1,
justifyContent: "center",
alignItems: "center",
},
emptyViewIcon: {
fontSize: 45,
},
emptyViewText: {
fontWeight: "bold",
marginTop: 10,
textAlign: "center",
paddingHorizontal: 30,
},
});
+143
View File
@@ -0,0 +1,143 @@
import React from "react";
import { Pressable, StyleSheet, Text, View } from "react-native";
import { List as PaperList, Divider as PaperDivider } from "react-native-paper";
import MaterialCommunityIcons from "react-native-vector-icons/MaterialCommunityIcons";
const ListSection = ({ children }) => (
<PaperList.Section style={styles.listSection}>{children}</PaperList.Section>
);
const ListSubHeader = ({ children, theme }) => (
<PaperList.Subheader
style={[styles.listSubHeader, { color: theme.colorAccent }]}
>
{children}
</PaperList.Subheader>
);
const ListItem = ({
title,
description,
icon,
onPress,
theme,
right,
iconColor,
titleStyle,
style,
}) => (
<PaperList.Item
title={title}
style={style}
titleStyle={[{ color: theme.textColorPrimary }, titleStyle]}
description={description}
descriptionStyle={{ color: theme.textColorSecondary }}
descriptionNumberOfLines={1}
left={() => (
<View style={{ justifyContent: "center" }}>
{icon && (
<PaperList.Icon
color={theme.colorAccent}
icon={icon}
style={{
marginVertical: 0,
}}
/>
)}
</View>
)}
right={() =>
right && (
<MaterialCommunityIcons
name={right}
color={iconColor || "#47a84a"}
size={23}
style={{ marginRight: 16, textAlignVertical: "center" }}
/>
)
}
onPress={onPress}
rippleColor={theme.rippleColor}
/>
);
const Divider = ({ theme }) => (
<PaperDivider style={{ backgroundColor: theme.dividerColor, height: 1 }} />
);
const InfoItem = ({ title, icon, theme }) => (
<PaperList.Item
title={title}
titleStyle={{ color: theme.textColorSecondary, fontSize: 14 }}
titleNumberOfLines={5}
left={() =>
icon && (
<PaperList.Icon
color={theme.textColorSecondary}
icon={icon}
style={{ marginVertical: 0 }}
/>
)
}
/>
);
const ColorItem = ({ title, description, theme, onPress }) => (
<Pressable
style={{
padding: 16,
flexDirection: "row",
justifyContent: "space-between",
alignItems: "center",
}}
android_ripple={{ color: theme.rippleColor }}
onPress={onPress}
>
<View>
<Text style={{ color: theme.textColorPrimary, fontSize: 16 }}>
{title}
</Text>
<Text style={{ color: theme.textColorSecondary }}>
{description}
</Text>
</View>
<View
style={{
backgroundColor: description,
height: 24,
width: 24,
borderRadius: 50,
marginRight: 16,
}}
/>
</Pressable>
);
const Icon = ({ icon, theme }) => (
<PaperList.Icon
color={theme.colorAccent}
icon={icon}
style={{ margin: 0 }}
/>
);
export const List = {
Section: ListSection,
SubHeader: ListSubHeader,
Item: ListItem,
Divider,
InfoItem,
ColorItem,
Icon,
};
const styles = StyleSheet.create({
listSection: {
flex: 1,
marginVertical: 0,
},
listSubHeader: {
paddingBottom: 5,
},
});
+80
View File
@@ -0,0 +1,80 @@
import React from "react";
import { StyleSheet, View, Text, Pressable } from "react-native";
import FastImage from "react-native-fast-image";
const ListView = ({
item,
downloadBadge,
unreadBadge,
inLibraryBadge,
theme,
onPress,
isSelected,
onLongPress,
}) => {
return (
<Pressable
android_ripple={{ color: theme.rippleColor }}
style={[
styles.listView,
isSelected && { backgroundColor: theme.rippleColor },
]}
onPress={onPress}
onLongPress={onLongPress}
>
<FastImage
source={{
uri:
item.novelCover && !item.novelCover.startsWith("/")
? item.novelCover
: "https://github.com/LNReader/lnreader-sources/blob/main/src/coverNotAvailable.jpg?raw=true",
}}
style={[
styles.extensionIcon,
inLibraryBadge && { opacity: 0.5 },
]}
/>
<Text
style={[{ color: theme.textColorPrimary }, styles.novelName]}
numberOfLines={1}
>
{item.novelName}
</Text>
<View style={styles.badgeContainer}>
{downloadBadge}
{unreadBadge}
{inLibraryBadge}
</View>
</Pressable>
);
};
export default ListView;
const styles = StyleSheet.create({
listView: {
flex: 1,
flexDirection: "row",
alignItems: "center",
paddingVertical: 8,
paddingHorizontal: 12,
},
extensionIcon: {
width: 40,
height: 40,
borderRadius: 4,
},
novelName: {
flex: 1,
marginLeft: 16,
fontSize: 15,
paddingRight: 8,
flexWrap: "wrap",
},
badgeContainer: {
flexDirection: "row",
justifyContent: "flex-end",
alignItems: "flex-end",
},
});
@@ -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);
+317
View File
@@ -0,0 +1,317 @@
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,
isSelected,
onLongPress,
selectedNovels,
}) => {
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),
[]
);
const selectNovel = () => onLongPress(item.novelId);
return displayMode !== 2 ? (
<View
style={[
{
flex: 1 / getNovelsPerRow(),
borderRadius: 6,
overflow: "hidden",
margin: 2,
},
isSelected && {
backgroundColor: theme.colorAccent,
opacity: 0.8,
},
]}
>
<Pressable
android_ripple={{ color: theme.colorAccent }}
style={styles.opac}
onPress={
selectedNovels && selectedNovels.length > 0
? selectNovel
: onPress
}
onLongPress={selectNovel}
>
<>
<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 &&
!item.novelCover.startsWith("/")
? item.novelCover
: "https://github.com/LNReader/lnreader-sources/blob/main/src/coverNotAvailable.jpg?raw=true",
}}
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={
selectedNovels && selectedNovels.length > 0
? selectNovel
: onPress
}
onLongPress={selectNovel}
isSelected={isSelected}
/>
);
};
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,
textShadowColor: "rgba(0, 0, 0, 0.75)",
textShadowOffset: { width: -1, height: 1 },
textShadowRadius: 10,
},
linearGradient: {
borderRadius: 4,
},
opac: {
padding: 4.8,
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: "#2979FF",
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,24 @@ const NovelList = ({
}
};
const keyExtractor = useCallback((item) => item.novelUrl, []);
return (
<FlatList
contentContainerStyle={styles.flatListCont}
contentContainerStyle={[
styles.flatListCont,
displayMode !== 2 && { paddingHorizontal: 4 },
]}
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}
/>
);
};
@@ -46,6 +57,5 @@ const styles = StyleSheet.create({
flatListCont: {
flexGrow: 1,
paddingVertical: 8,
paddingHorizontal: 4,
},
});
@@ -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,
@@ -78,20 +78,20 @@ export const Searchbar = ({
const styles = StyleSheet.create({
searchbarContainer: {
marginTop: StatusBar.currentHeight + 4,
height: 48,
marginTop: StatusBar.currentHeight + 8,
height: 46,
flexDirection: "row",
alignItems: "center",
paddingHorizontal: 16,
marginBottom: 12,
paddingHorizontal: 12,
marginBottom: 6,
borderRadius: 8,
marginHorizontal: 12,
elevation: 2,
backgroundColor: "white",
},
container: {
flex: 1,
flexDirection: "row",
justifyContent: "space-between",
alignItems: "center",
},
});
+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(
@@ -115,13 +113,26 @@ export const markChapterRead = async (chapterId) => {
});
};
const markChapterUnreadQuery = `UPDATE chapters SET \`read\` = 0 WHERE chapterId = ?`;
const markAllChaptersReadQuery = `UPDATE chapters SET \`read\` = 1 WHERE novelId = ?`;
export const markChapterUnread = async (chapterId) => {
export const markAllChaptersRead = async (novelId) => {
db.transaction((tx) => {
tx.executeSql(
markChapterUnreadQuery,
[chapterId],
markAllChaptersReadQuery,
[novelId],
(tx, res) => {},
(tx, error) => console.log(error)
);
});
};
const markAllChaptersUnreadQuery = `UPDATE chapters SET \`read\` = 0 WHERE novelId = ?`;
export const markAllChaptersUnread = async (novelId) => {
db.transaction((tx) => {
tx.executeSql(
markAllChaptersUnreadQuery,
[novelId],
(tx, res) => {},
(tx, error) => console.log(error)
);
@@ -149,7 +160,7 @@ export const isChapterDownloaded = async (chapterId) => {
);
};
const downloadChapterQuery = `INSERT INTO downloads (downloadChapterId, chapterName, chapterText, prevChapter, nextChapter) VALUES (?, ?, ?, ?, ?)`;
const downloadChapterQuery = `INSERT INTO downloads (downloadChapterId, chapterName, chapterText) VALUES (?, ?, ?)`;
export const downloadChapter = async (
sourceId,
@@ -157,10 +168,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(
@@ -169,15 +179,10 @@ export const downloadChapter = async (
);
tx.executeSql(
downloadChapterQuery,
[
chapterId,
chapter.chapterName,
chapter.chapterText,
chapter.prevChapter,
chapter.nextChapter,
],
(tx, res) =>
console.log(`Downloaded Chapter ${chapter.chapterUrl}`),
[chapterId, chapter.chapterName, chapter.chapterText],
(tx, res) => {
// console.log(`Downloaded Chapter ${chapter.chapterUrl}`);
},
(txObj, error) => console.log("Error ", error)
);
});
@@ -262,3 +267,32 @@ export const markPreviousChaptersUnread = async (chapterId, novelId) => {
);
});
};
const getDownloadedChaptersQuery = `
SELECT chapters.*, novels.sourceId, novels.novelName, novels.novelCover, novels.novelUrl
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;");
});
};
@@ -13,7 +13,7 @@ const getHistoryQuery = `
ORDER BY history.historyTimeRead DESC
`;
export const getHistory = async () => {
export const getHistoryFromDb = async () => {
return new Promise((resolve, reject) => {
db.transaction((tx) => {
tx.executeSql(
@@ -1,9 +1,14 @@
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 * as DocumentPicker from "expo-document-picker";
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) => {
@@ -42,6 +47,17 @@ export const followNovel = async (followed, novelId) => {
});
};
export const unfollowNovel = async (novelId) => {
db.transaction((tx) => {
tx.executeSql(
"UPDATE novels SET followed = 0 WHERE novelId = ?",
[novelId],
(tx, res) => {},
(txObj, error) => console.log("Error ", error)
);
});
};
const checkNovelInCacheQuery = `SELECT * FROM novels WHERE novelUrl=? LIMIT 1`;
export const checkNovelInCache = (novelUrl) => {
@@ -81,9 +97,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 +141,102 @@ 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);
}
};
export const updateNovelInfo = async (info, novelId) => {
db.transaction((tx) => {
tx.executeSql(
"UPDATE novels SET novelName = ?, novelSummary = ?, author = ?, genre = ?, status = ? WHERE novelId = ?",
[
info.novelName,
info.novelSummary,
info.author,
info.genre,
info.status,
novelId,
],
(txObj, res) => {},
(txObj, error) => console.log("Error ", error)
);
});
};
export const setCustomNovelCover = async (novelId) => {
const image = await DocumentPicker.getDocumentAsync({ type: "image/*" });
if (image.uri) {
db.transaction((tx) => {
tx.executeSql(
"UPDATE novels SET novelCover = ? WHERE novelId = ?",
[image.uri, novelId],
(txObj, res) => {},
(txObj, error) => console.log("Error ", error)
);
});
return image.uri;
}
};
@@ -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`;
@@ -4,8 +4,6 @@ export const createDownloadTableQuery = `
downloadChapterId INTEGER NOT NULL,
chapterName TEXT,
chapterText TEXT,
nextChapter TEXT,
prevChapter TEXT,
FOREIGN KEY (downloadChapterId) REFERENCES chapters(chapterId)
ON DELETE CASCADE
)`;
@@ -22,7 +22,7 @@ export const githubUpdateChecker = () => {
};
const isNewVersion = (versionTag) => {
let currentVersion = "v1.0.21";
let currentVersion = "v1.1.2";
const regex = /[^\\d.]/;
let newVersion = versionTag.replace(regex, "");
@@ -1,10 +1,10 @@
import { shallowEqual, useSelector } from "react-redux";
import { showChapterTitlesAction } from "../redux/novel/novel.actions";
import { useSelector } from "react-redux";
import { darkTheme } from "../theme/theme";
const useTheme = () => {
const theme = useSelector((state) => state.settingsReducer.theme);
return theme;
return theme ?? darkTheme;
};
const useReaderSettings = () => {
+104
View File
@@ -0,0 +1,104 @@
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 { useSettings, useTheme } from "../hooks/reduxHooks";
const Tab = createMaterialBottomTabNavigator();
const BottomNavigator = () => {
const theme = useTheme();
const {
showHistoryTab = true,
showUpdatesTab = true,
showLabelsInNav = false,
} = useSettings();
return (
<Tab.Navigator
barStyle={{ backgroundColor: theme.colorPrimary }}
activeColor={theme.colorAccent}
shifting={!showLabelsInNav}
>
<Tab.Screen
name="Library"
component={Library}
options={{
tabBarIcon: ({ color }) => (
<MaterialCommunityIcons
name="book-variant-multiple"
color={color}
size={24}
/>
),
}}
/>
{showUpdatesTab && (
<Tab.Screen
name="Updates"
component={Updates}
options={{
tabBarIcon: ({ color }) => (
<MaterialCommunityIcons
name="alert-decagram-outline"
color={color}
size={24}
/>
),
}}
/>
)}
{showHistoryTab && (
<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,42 @@
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";
import * as SplashScreen from "expo-splash-screen";
// 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(() => {
setTimeout(async () => {
await SplashScreen.hideAsync();
setBarColor(theme);
}, 500);
}, [theme]);
const { isNewVersion, latestRelease } = githubUpdateChecker() || {};
return (
@@ -39,29 +45,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";
+102
View File
@@ -0,0 +1,102 @@
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,
},
linkingURI: "lnreader://downloads",
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 });
};

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