Compare commits
71 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 1a8d640db1 | |||
| dc6b2deddd | |||
| a9fcf2fe08 | |||
| f195b9e6df | |||
| d07fd26cb3 | |||
| 5aee0d1b04 | |||
| c4457d74c4 | |||
| ba90a6fb84 | |||
| e7424cea06 | |||
| e274feda6e | |||
| 8ec2378191 | |||
| ef57a98681 | |||
| bdda959f36 | |||
| 04f3ba2b45 | |||
| f1a261ab28 | |||
| da2ee3b883 | |||
| b98d3861b4 | |||
| ff59330d20 | |||
| b0639e79a8 | |||
| 5931a0dc07 | |||
| cf40acb725 | |||
| a2974ccb44 | |||
| 5eda558f09 | |||
| cf7f1402d3 | |||
| 834a8d9e2e | |||
| fe5b3f4a5b | |||
| ec4eaf4d49 | |||
| 0c0b599452 | |||
| ac26c67361 | |||
| d205572a5e | |||
| e5d52fcd9d | |||
| 61424e533a | |||
| c3ef170385 | |||
| 430c7a7341 | |||
| e4b934061b | |||
| 3579671ade | |||
| 893d229c25 | |||
| ea3f5ac7ee | |||
| e4027efbb1 | |||
| f5dd7153cc | |||
| 38b0097261 | |||
| 2f7215512f | |||
| 9a96a34cda | |||
| a7f6d2cd3c | |||
| 5dfe6a7ab8 | |||
| 3fb35e2d32 | |||
| 35d5cf987a | |||
| 7e28d12a8a | |||
| ff575de2e8 | |||
| 962fcc534d | |||
| acd8843b0e | |||
| c956d764c5 | |||
| ca0f62f118 | |||
| 4d766555b7 | |||
| edcc4c0bcd | |||
| 8e1a9274e5 | |||
| d0b11c9b47 | |||
| 3a9784a7a0 | |||
| 66e3945427 | |||
| 7773d50607 | |||
| 721d3842fb | |||
| 773c33f3bf | |||
| 1addb54690 | |||
| ce29a5456d | |||
| 4eda2d4624 | |||
| bae2fa40f3 | |||
| cf5d9e18bb | |||
| e87ecb48b4 | |||
| d4140fcc52 | |||
| 4858f22a88 | |||
| 6aa977a562 |
@@ -1,4 +0,0 @@
|
||||
{
|
||||
"12bb71342c6255bbf50437ec8f4441c083f47cdb74bd89160c15e4f43e52a1cb": true,
|
||||
"40b842e832070c58deac6aa9e08fa459302ee3f9da492c7e77d93d2fbf4a56fd": true
|
||||
}
|
||||
@@ -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
|
||||
@@ -7,10 +7,10 @@ import { Provider } from "react-redux";
|
||||
import { persistor, store } from "./src/redux/store";
|
||||
import { PersistGate } from "redux-persist/lib/integration/react";
|
||||
|
||||
import Main from "./src/Navigators/Main";
|
||||
import Main from "./src/navigators/Main";
|
||||
|
||||
import { createDB, deleteDb } from "./src/Database/DBHelper";
|
||||
import { fonts } from "./src/Theme/fonts";
|
||||
import { createDB } from "./src/database/DBHelper";
|
||||
import { fonts } from "./src/theme/fonts";
|
||||
|
||||
const App = () => {
|
||||
const [loaded] = useFonts(fonts);
|
||||
|
||||
@@ -136,7 +136,7 @@ android {
|
||||
minSdkVersion rootProject.ext.minSdkVersion
|
||||
targetSdkVersion rootProject.ext.targetSdkVersion
|
||||
versionCode 1
|
||||
versionName "1.0.21"
|
||||
versionName "1.1.1"
|
||||
}
|
||||
splits {
|
||||
abi {
|
||||
|
||||
@@ -7,10 +7,11 @@
|
||||
<uses-permission android:name="android.permission.READ_EXTERNAL_STORAGE"/>
|
||||
<uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE"/>
|
||||
<uses-permission android:name="android.permission.RECEIVE_BOOT_COMPLETED"/>
|
||||
<uses-permission android:name="android.permission.FOREGROUND_SERVICE" />
|
||||
<uses-permission android:name="android.permission.WAKE_LOCK"/>
|
||||
<uses-permission android:name="android.permission.ACCESS_NETWORK_STATE"/>
|
||||
<uses-permission android:name="com.google.android.c2dm.permission.RECEIVE"/>
|
||||
<application android:name=".MainApplication" android:label="@string/app_name" android:icon="@mipmap/ic_launcher" android:roundIcon="@mipmap/ic_launcher_round" android:allowBackup="true" android:theme="@style/AppTheme">
|
||||
<application android:name=".MainApplication" android:label="@string/app_name" android:icon="@mipmap/ic_launcher" android:roundIcon="@mipmap/ic_launcher_round" android:allowBackup="true" android:theme="@style/AppTheme" android:usesCleartextTraffic="true">
|
||||
<meta-data android:name="expo.modules.updates.EXPO_UPDATE_URL" android:value="https://exp.host/@rajarsheechatterjee/LNReader"/>
|
||||
<meta-data android:name="expo.modules.updates.EXPO_SDK_VERSION" android:value="41.0.0"/>
|
||||
<meta-data android:name="expo.modules.updates.ENABLED" android:value="false"/>
|
||||
@@ -32,5 +33,6 @@
|
||||
</intent-filter>
|
||||
</activity>
|
||||
<activity android:name="com.facebook.react.devsupport.DevSettingsActivity"/>
|
||||
<service android:name="com.asterinet.react.bgactions.RNBackgroundActionsTask" />
|
||||
</application>
|
||||
</manifest>
|
||||
File diff suppressed because one or more lines are too long
@@ -1,62 +0,0 @@
|
||||
// FullScreenModule.java
|
||||
|
||||
package com.rajarsheechatterjee.LNReader;
|
||||
|
||||
import android.os.Build;
|
||||
import android.view.View;
|
||||
import android.view.WindowManager;
|
||||
import android.app.Activity;
|
||||
import com.facebook.react.bridge.NativeModule;
|
||||
import com.facebook.react.bridge.ReactApplicationContext;
|
||||
import com.facebook.react.bridge.ReactContext;
|
||||
import com.facebook.react.bridge.UiThreadUtil;
|
||||
import com.facebook.react.bridge.ReactContextBaseJavaModule;
|
||||
import com.facebook.react.bridge.ReactMethod;
|
||||
|
||||
|
||||
public class FullScreenModule extends ReactContextBaseJavaModule {
|
||||
@Override
|
||||
public String getName() {
|
||||
return "FullScreen";
|
||||
}
|
||||
|
||||
@ReactMethod
|
||||
public void enable() {
|
||||
UiThreadUtil.runOnUiThread(
|
||||
new Runnable() {
|
||||
@Override
|
||||
public void run() {
|
||||
getCurrentActivity().getWindow().getDecorView().setSystemUiVisibility(
|
||||
View.SYSTEM_UI_FLAG_LAYOUT_FULLSCREEN |
|
||||
View.SYSTEM_UI_FLAG_LAYOUT_HIDE_NAVIGATION |
|
||||
View.SYSTEM_UI_FLAG_FULLSCREEN |
|
||||
View.SYSTEM_UI_FLAG_HIDE_NAVIGATION |
|
||||
View.SYSTEM_UI_FLAG_IMMERSIVE_STICKY
|
||||
);
|
||||
}
|
||||
}
|
||||
);
|
||||
|
||||
}
|
||||
|
||||
@ReactMethod
|
||||
public void disable() {
|
||||
UiThreadUtil.runOnUiThread(
|
||||
new Runnable() {
|
||||
@Override
|
||||
public void run() {
|
||||
getCurrentActivity().getWindow().getDecorView().setSystemUiVisibility(
|
||||
View.SYSTEM_UI_FLAG_LAYOUT_FULLSCREEN |
|
||||
View.SYSTEM_UI_FLAG_LAYOUT_HIDE_NAVIGATION |
|
||||
View.SYSTEM_UI_FLAG_LAYOUT_STABLE
|
||||
);
|
||||
}
|
||||
}
|
||||
);
|
||||
|
||||
}
|
||||
|
||||
FullScreenModule(ReactApplicationContext reactContext) {
|
||||
super(reactContext);
|
||||
}
|
||||
}
|
||||
@@ -1,29 +0,0 @@
|
||||
package com.rajarsheechatterjee.LNReader;
|
||||
|
||||
import com.facebook.react.ReactPackage;
|
||||
import com.facebook.react.bridge.NativeModule;
|
||||
import com.facebook.react.bridge.ReactApplicationContext;
|
||||
import com.facebook.react.uimanager.ViewManager;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.Collections;
|
||||
import java.util.List;
|
||||
|
||||
public class FullScreenPackage implements ReactPackage {
|
||||
|
||||
@Override
|
||||
public List<ViewManager> createViewManagers(ReactApplicationContext reactContext) {
|
||||
return Collections.emptyList();
|
||||
}
|
||||
|
||||
@Override
|
||||
public List<NativeModule> createNativeModules(
|
||||
ReactApplicationContext reactContext) {
|
||||
List<NativeModule> modules = new ArrayList<>();
|
||||
|
||||
modules.add(new FullScreenModule(reactContext));
|
||||
|
||||
return modules;
|
||||
}
|
||||
|
||||
}
|
||||
@@ -31,9 +31,6 @@ import java.util.Arrays;
|
||||
import java.util.List;
|
||||
import javax.annotation.Nullable;
|
||||
|
||||
import com.rajarsheechatterjee.LNReader.FullScreenPackage;
|
||||
|
||||
|
||||
public class MainApplication extends Application implements ReactApplication {
|
||||
private final ReactModuleRegistryProvider mModuleRegistryProvider = new ReactModuleRegistryProvider(
|
||||
new BasePackageList().getPackageList()
|
||||
@@ -49,7 +46,6 @@ public class MainApplication extends Application implements ReactApplication {
|
||||
protected List<ReactPackage> getPackages() {
|
||||
List<ReactPackage> packages = new PackageList(this).getPackages();
|
||||
packages.add(new ModuleRegistryAdapter(mModuleRegistryProvider));
|
||||
packages.add(new FullScreenPackage());
|
||||
return packages;
|
||||
}
|
||||
|
||||
|
||||
-3
@@ -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 |
@@ -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.
Generated
+19712
-8682
File diff suppressed because it is too large
Load Diff
+71
-69
@@ -1,71 +1,73 @@
|
||||
{
|
||||
"scripts": {
|
||||
"start": "react-native start",
|
||||
"android": "react-native run-android",
|
||||
"ios": "react-native run-ios",
|
||||
"web": "expo start --web",
|
||||
"eject": "expo eject"
|
||||
},
|
||||
"dependencies": {
|
||||
"@react-native-async-storage/async-storage": "^1.13.0",
|
||||
"@react-native-community/masked-view": "0.1.10",
|
||||
"@react-native-community/netinfo": "6.0.0",
|
||||
"@react-native-community/slider": "3.0.3",
|
||||
"@react-navigation/material-bottom-tabs": "^5.3.10",
|
||||
"@react-navigation/native": "^5.8.10",
|
||||
"@react-navigation/stack": "^5.12.8",
|
||||
"expo": "^41.0.1",
|
||||
"expo-background-fetch": "~9.1.0",
|
||||
"expo-document-picker": "^9.1.2",
|
||||
"expo-file-system": "~11.0.2",
|
||||
"expo-font": "~9.1.0",
|
||||
"expo-linear-gradient": "~9.1.0",
|
||||
"expo-linking": "~2.2.3",
|
||||
"expo-notifications": "~0.11.6",
|
||||
"expo-permissions": "^12.0.1",
|
||||
"expo-sharing": "~9.1.2",
|
||||
"expo-splash-screen": "~0.10.2",
|
||||
"expo-sqlite": "~9.1.0",
|
||||
"expo-status-bar": "~1.0.4",
|
||||
"expo-task-manager": "~9.1.0",
|
||||
"expo-updates": "~0.5.4",
|
||||
"expo-web-browser": "~9.1.0",
|
||||
"moment": "^2.29.1",
|
||||
"qs": "^6.10.1",
|
||||
"react": "16.13.1",
|
||||
"react-dom": "16.13.1",
|
||||
"react-native": "^0.64.2",
|
||||
"react-native-cheerio": "^1.0.0-rc.4",
|
||||
"react-native-collapsible-header-views": "^1.1.2",
|
||||
"react-native-device-info": "^8.1.3",
|
||||
"react-native-easing-gradient": "^0.1.2",
|
||||
"react-native-fast-image": "^8.3.4",
|
||||
"react-native-fit-image": "^1.5.5",
|
||||
"react-native-gesture-handler": "~1.10.2",
|
||||
"react-native-immersive-mode": "^2.0.0",
|
||||
"react-native-navigation-bar-color": "^2.0.1",
|
||||
"react-native-offline": "^5.7.0",
|
||||
"react-native-pager-view": "^5.1.10",
|
||||
"react-native-paper": "^4.5.0",
|
||||
"react-native-reanimated": "~2.1.0",
|
||||
"react-native-safe-area-context": "3.2.0",
|
||||
"react-native-screens": "~3.0.0",
|
||||
"react-native-tab-view": "^3.0.1",
|
||||
"react-native-unimodules": "~0.13.3",
|
||||
"react-native-vector-icons": "^7.1.0",
|
||||
"react-native-web": "^0.16.5",
|
||||
"react-native-webview": "^11.6.2",
|
||||
"react-redux": "^7.2.2",
|
||||
"redux": "^4.0.5",
|
||||
"redux-devtools-extension": "^2.13.9",
|
||||
"redux-persist": "^6.0.0",
|
||||
"redux-thunk": "^2.3.0",
|
||||
"rn-sliding-up-panel": "^2.4.4"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@babel/core": "~7.9.0"
|
||||
},
|
||||
"private": true,
|
||||
"name": "lnreader-eject",
|
||||
"version": "1.0.0"
|
||||
"scripts": {
|
||||
"start": "react-native start",
|
||||
"android": "react-native run-android",
|
||||
"ios": "react-native run-ios",
|
||||
"web": "expo start --web",
|
||||
"eject": "expo eject"
|
||||
},
|
||||
"dependencies": {
|
||||
"@react-native-async-storage/async-storage": "^1.13.0",
|
||||
"@react-native-community/masked-view": "0.1.10",
|
||||
"@react-native-community/netinfo": "6.0.0",
|
||||
"@react-native-community/slider": "3.0.3",
|
||||
"@react-navigation/material-bottom-tabs": "^5.3.10",
|
||||
"@react-navigation/native": "^5.8.10",
|
||||
"@react-navigation/stack": "^5.12.8",
|
||||
"expo": "^41.0.1",
|
||||
"expo-document-picker": "^9.1.2",
|
||||
"expo-file-system": "~11.0.2",
|
||||
"expo-font": "~9.1.0",
|
||||
"expo-linear-gradient": "~9.1.0",
|
||||
"expo-linking": "~2.2.3",
|
||||
"expo-notifications": "~0.11.6",
|
||||
"expo-splash-screen": "^0.11.2",
|
||||
"expo-sqlite": "~9.1.0",
|
||||
"expo-updates": "~0.5.4",
|
||||
"expo-web-browser": "~9.1.0",
|
||||
"moment": "^2.29.1",
|
||||
"qs": "^6.10.1",
|
||||
"react": "16.13.1",
|
||||
"react-dom": "16.13.1",
|
||||
"react-native": "^0.64.2",
|
||||
"react-native-background-actions": "^2.6.1",
|
||||
"react-native-cheerio": "^1.0.0-rc.4",
|
||||
"react-native-device-info": "^8.1.3",
|
||||
"react-native-easing-gradient": "^0.1.2",
|
||||
"react-native-fast-image": "^8.3.4",
|
||||
"react-native-fit-image": "^1.5.5",
|
||||
"react-native-gesture-handler": "~1.10.2",
|
||||
"react-native-navigation-bar-color": "^2.0.1",
|
||||
"react-native-pager-view": "^5.1.10",
|
||||
"react-native-paper": "^4.5.0",
|
||||
"react-native-reanimated": "^2.1.0",
|
||||
"react-native-safe-area-context": "3.2.0",
|
||||
"react-native-screens": "~3.0.0",
|
||||
"react-native-swipe-gestures": "^1.0.5",
|
||||
"react-native-tab-view": "^3.0.1",
|
||||
"react-native-unimodules": "~0.13.3",
|
||||
"react-native-vector-icons": "^7.1.0",
|
||||
"react-native-web": "^0.16.5",
|
||||
"react-native-webview": "^11.6.2",
|
||||
"react-redux": "^7.2.2",
|
||||
"redux": "^4.0.5",
|
||||
"redux-persist": "^6.0.0",
|
||||
"redux-thunk": "^2.3.0",
|
||||
"rn-sliding-up-panel": "^2.4.4"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@babel/core": "~7.9.0"
|
||||
},
|
||||
"prettier": {
|
||||
"tabWidth": 4,
|
||||
"semi": true,
|
||||
"singleQuote": false,
|
||||
"quoteProps": "as-needed",
|
||||
"trailingComma": "es5",
|
||||
"bracketSpacing": true,
|
||||
"arrowParens": "always"
|
||||
},
|
||||
"private": true,
|
||||
"name": "lnreader-eject",
|
||||
"version": "1.0.0"
|
||||
}
|
||||
|
||||
@@ -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,
|
||||
},
|
||||
});
|
||||
@@ -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);
|
||||
};
|
||||
@@ -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;
|
||||
@@ -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;
|
||||
@@ -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,
|
||||
},
|
||||
});
|
||||
@@ -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 };
|
||||
@@ -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,66 +0,0 @@
|
||||
import React from "react";
|
||||
import { StyleSheet, View, Image } from "react-native";
|
||||
|
||||
import { Appbar } from "../../Components/Appbar";
|
||||
import { ListItem, ListSection } from "../../Components/List";
|
||||
|
||||
import { useTheme } from "../../Hooks/reduxHooks";
|
||||
|
||||
const MoreScreen = ({ navigation }) => {
|
||||
const theme = useTheme();
|
||||
|
||||
return (
|
||||
<>
|
||||
<Appbar title="More" style={{ elevation: 0 }} />
|
||||
<View style={{ flex: 1, backgroundColor: theme.colorPrimaryDark }}>
|
||||
<View style={{ overflow: "hidden", paddingBottom: 4 }}>
|
||||
<View
|
||||
style={{
|
||||
paddingTop: 10,
|
||||
paddingBottom: 25,
|
||||
alignItems: "center",
|
||||
backgroundColor: theme.colorPrimary,
|
||||
elevation: 4,
|
||||
}}
|
||||
>
|
||||
<Image
|
||||
source={require("../../../assets/logo.png")}
|
||||
style={{
|
||||
height: 80,
|
||||
width: 80,
|
||||
tintColor: theme.textColorPrimary,
|
||||
}}
|
||||
/>
|
||||
</View>
|
||||
</View>
|
||||
<ListSection>
|
||||
<ListItem
|
||||
title="Settings"
|
||||
icon="cog-outline"
|
||||
onPress={() =>
|
||||
navigation.navigate("MoreStack", {
|
||||
screen: "SettingsStack",
|
||||
})
|
||||
}
|
||||
theme={theme}
|
||||
/>
|
||||
|
||||
<ListItem
|
||||
title="About"
|
||||
icon="information-outline"
|
||||
onPress={() =>
|
||||
navigation.navigate("MoreStack", {
|
||||
screen: "About",
|
||||
})
|
||||
}
|
||||
theme={theme}
|
||||
/>
|
||||
</ListSection>
|
||||
</View>
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
export default MoreScreen;
|
||||
|
||||
const styles = StyleSheet.create({});
|
||||
@@ -1,80 +0,0 @@
|
||||
import React, { useState } from "react";
|
||||
import { StyleSheet, Text } from "react-native";
|
||||
import { Modal, Portal, TextInput } from "react-native-paper";
|
||||
|
||||
const AccentColorModal = ({
|
||||
accentColorModal,
|
||||
hideAccentColorModal,
|
||||
theme,
|
||||
onPress,
|
||||
color,
|
||||
}) => {
|
||||
const [text, setText] = useState(color);
|
||||
const [error, setError] = useState();
|
||||
|
||||
return (
|
||||
<Portal>
|
||||
<Modal
|
||||
visible={accentColorModal}
|
||||
onDismiss={() => {
|
||||
hideAccentColorModal();
|
||||
setText();
|
||||
setError();
|
||||
}}
|
||||
contentContainerStyle={[
|
||||
styles.containerStyle,
|
||||
{ backgroundColor: theme.colorPrimary },
|
||||
]}
|
||||
>
|
||||
<Text
|
||||
style={[
|
||||
styles.dialogTitle,
|
||||
{ color: theme.textColorPrimary },
|
||||
]}
|
||||
>
|
||||
Hex Color Code
|
||||
</Text>
|
||||
<TextInput
|
||||
value={text}
|
||||
defaultValue={color}
|
||||
onChangeText={(text) => setText(text)}
|
||||
onSubmitEditing={() => {
|
||||
if (text.match(/^#([0-9a-f]{6}|[0-9a-f]{3})$/i)) {
|
||||
onPress(text);
|
||||
hideAccentColorModal();
|
||||
} else {
|
||||
setError("Enter a valid hex color code");
|
||||
}
|
||||
}}
|
||||
mode="outlined"
|
||||
theme={{
|
||||
colors: {
|
||||
primary: color,
|
||||
placeholder: theme.textColorHint,
|
||||
text: theme.textColorPrimary,
|
||||
background: "transparent",
|
||||
},
|
||||
}}
|
||||
underlineColor={theme.textColorHint}
|
||||
dense
|
||||
error={error}
|
||||
/>
|
||||
<Text style={{ color: "#FF0033", paddingTop: 8 }}>{error}</Text>
|
||||
</Modal>
|
||||
</Portal>
|
||||
);
|
||||
};
|
||||
|
||||
export default AccentColorModal;
|
||||
|
||||
const styles = StyleSheet.create({
|
||||
containerStyle: {
|
||||
margin: 30,
|
||||
padding: 20,
|
||||
borderRadius: 8,
|
||||
},
|
||||
dialogTitle: {
|
||||
fontSize: 18,
|
||||
marginBottom: 16,
|
||||
},
|
||||
});
|
||||
@@ -1,60 +0,0 @@
|
||||
import React from "react";
|
||||
import { StyleSheet } from "react-native";
|
||||
import { Portal, Modal, Checkbox } from "react-native-paper";
|
||||
|
||||
import { setStatusBarStyle } from "expo-status-bar";
|
||||
import { setAppTheme } from "../../../redux/settings/settings.actions";
|
||||
import ImmersiveMode from "react-native-immersive-mode";
|
||||
|
||||
const ThemeModal = ({ themeModalVisible, hidethemeModal, dispatch, theme }) => {
|
||||
const themes = [
|
||||
{ id: 0, name: "AMOLED Dark", statusBar: "light" },
|
||||
{ id: 1, name: "Light", statusBar: "dark" },
|
||||
{ id: 2, name: "Dark", statusBar: "light" },
|
||||
{ id: 3, name: "Midnight Dusk", statusBar: "light" },
|
||||
{ id: 4, name: "Lime", statusBar: "light" },
|
||||
{ id: 5, name: "Iris Blue", statusBar: "light" },
|
||||
];
|
||||
|
||||
const renderThemeCheckboxes = () =>
|
||||
themes.map((item) => (
|
||||
<Checkbox.Item
|
||||
key={item.id}
|
||||
label={item.name}
|
||||
labelStyle={{ color: theme.textColorPrimary }}
|
||||
status={theme.id === item.id ? "checked" : "unchecked"}
|
||||
mode="ios"
|
||||
color={theme.colorAccent}
|
||||
onPress={() => {
|
||||
dispatch(setAppTheme(item.id));
|
||||
// setStatusBarStyle(item.statusBar);
|
||||
ImmersiveMode.setBarColor(item.colorPrimary);
|
||||
}}
|
||||
/>
|
||||
));
|
||||
|
||||
return (
|
||||
<Portal>
|
||||
<Modal
|
||||
visible={themeModalVisible}
|
||||
onDismiss={hidethemeModal}
|
||||
contentContainerStyle={[
|
||||
styles.containerStyle,
|
||||
{ backgroundColor: theme.colorPrimaryDark },
|
||||
]}
|
||||
>
|
||||
{renderThemeCheckboxes()}
|
||||
</Modal>
|
||||
</Portal>
|
||||
);
|
||||
};
|
||||
|
||||
export default ThemeModal;
|
||||
|
||||
const styles = StyleSheet.create({
|
||||
containerStyle: {
|
||||
padding: 20,
|
||||
margin: 20,
|
||||
borderRadius: 6,
|
||||
},
|
||||
});
|
||||
@@ -1,176 +0,0 @@
|
||||
import React, { useState } from "react";
|
||||
import { StyleSheet, View, Text, Animated } from "react-native";
|
||||
import { List } from "react-native-paper";
|
||||
|
||||
import Bottomsheet from "rn-sliding-up-panel";
|
||||
import BottomSheetHandle from "../../../Components/BottomSheetHandle";
|
||||
import { ListItem } from "../../../Components/List";
|
||||
import {
|
||||
markPreviousChaptersReadAction,
|
||||
markPreviousChaptersUnreadAction,
|
||||
} from "../../../redux/novel/novel.actions";
|
||||
|
||||
const ChapterActionsSheet = ({
|
||||
bottomSheetRef,
|
||||
selected,
|
||||
theme,
|
||||
markChapterReadAction,
|
||||
markChapterUnreadAction,
|
||||
dispatch,
|
||||
bookmarkChapterAction,
|
||||
setSelected,
|
||||
}) => {
|
||||
const [animatedValue] = useState(new Animated.Value(0));
|
||||
|
||||
return (
|
||||
<Bottomsheet
|
||||
animatedValue={animatedValue}
|
||||
ref={bottomSheetRef}
|
||||
draggableRange={{ top: 270, bottom: 0 }}
|
||||
snappingPoints={[0, 270]}
|
||||
>
|
||||
<View
|
||||
style={[
|
||||
styles.contentContainer,
|
||||
{ backgroundColor: theme.colorPrimary },
|
||||
]}
|
||||
>
|
||||
<Text
|
||||
style={{
|
||||
color: theme.textColorPrimary,
|
||||
textAlign: "center",
|
||||
paddingTop: 16,
|
||||
paddingBottom: 8,
|
||||
fontSize: 16,
|
||||
}}
|
||||
numberOfLines={1}
|
||||
>
|
||||
{selected?.chapterName}
|
||||
</Text>
|
||||
<List.Item
|
||||
title="Mark previous chapters as read"
|
||||
titleStyle={{ color: theme.textColorPrimary, fontSize: 15 }}
|
||||
left={() => (
|
||||
<List.Icon
|
||||
color={theme.textColorPrimary}
|
||||
icon="eye-check"
|
||||
style={{ marginVertical: 0 }}
|
||||
/>
|
||||
)}
|
||||
onPress={() => {
|
||||
dispatch(
|
||||
markPreviousChaptersReadAction(
|
||||
selected?.chapterId,
|
||||
selected?.novelId
|
||||
)
|
||||
);
|
||||
// bottomSheetRef.current.hide();
|
||||
}}
|
||||
rippleColor={theme.rippleColor}
|
||||
/>
|
||||
<List.Item
|
||||
title="Mark previous chapters as unread"
|
||||
titleStyle={{ color: theme.textColorPrimary, fontSize: 15 }}
|
||||
left={() => (
|
||||
<List.Icon
|
||||
color={theme.textColorPrimary}
|
||||
icon="eye-off"
|
||||
style={{ marginVertical: 0 }}
|
||||
/>
|
||||
)}
|
||||
onPress={() => {
|
||||
dispatch(
|
||||
markPreviousChaptersUnreadAction(
|
||||
selected?.chapterId,
|
||||
selected?.novelId
|
||||
)
|
||||
);
|
||||
// bottomSheetRef.current.hide();
|
||||
}}
|
||||
rippleColor={theme.rippleColor}
|
||||
/>
|
||||
<List.Item
|
||||
title={
|
||||
selected?.read
|
||||
? "Mark chapter as unread"
|
||||
: "Mark chapter as read"
|
||||
}
|
||||
titleStyle={{ color: theme.textColorPrimary, fontSize: 15 }}
|
||||
left={() => (
|
||||
<List.Icon
|
||||
color={theme.textColorPrimary}
|
||||
icon={selected?.read ? "eye-off" : "eye"}
|
||||
style={{ marginVertical: 0 }}
|
||||
/>
|
||||
)}
|
||||
onPress={() => {
|
||||
if (selected?.read) {
|
||||
dispatch(
|
||||
markChapterUnreadAction(selected?.chapterId)
|
||||
);
|
||||
setSelected((selected) => ({
|
||||
...selected,
|
||||
read: 0,
|
||||
}));
|
||||
} else {
|
||||
dispatch(
|
||||
markChapterReadAction(
|
||||
selected?.chapterId,
|
||||
selected?.novelId
|
||||
)
|
||||
);
|
||||
setSelected((selected) => ({
|
||||
...selected,
|
||||
read: 1,
|
||||
}));
|
||||
}
|
||||
// bottomSheetRef.current.hide();
|
||||
}}
|
||||
rippleColor={theme.rippleColor}
|
||||
/>
|
||||
<List.Item
|
||||
title={
|
||||
selected?.bookmark
|
||||
? "Remove bookmark"
|
||||
: "Bookmark chapter"
|
||||
}
|
||||
titleStyle={{ color: theme.textColorPrimary, fontSize: 15 }}
|
||||
left={() => (
|
||||
<List.Icon
|
||||
color={theme.textColorPrimary}
|
||||
icon={
|
||||
selected?.bookmark ? "bookmark-off" : "bookmark"
|
||||
}
|
||||
style={{ marginVertical: 0 }}
|
||||
/>
|
||||
)}
|
||||
onPress={() => {
|
||||
dispatch(
|
||||
bookmarkChapterAction(
|
||||
selected?.bookmark,
|
||||
selected?.chapterId
|
||||
)
|
||||
);
|
||||
setSelected((selected) => ({
|
||||
...selected,
|
||||
bookmark: !selected.bookmark,
|
||||
}));
|
||||
|
||||
// bottomSheetRef.current.hide();
|
||||
}}
|
||||
rippleColor={theme.rippleColor}
|
||||
/>
|
||||
</View>
|
||||
</Bottomsheet>
|
||||
);
|
||||
};
|
||||
|
||||
export default ChapterActionsSheet;
|
||||
|
||||
const styles = StyleSheet.create({
|
||||
contentContainer: {
|
||||
flex: 1,
|
||||
borderTopRightRadius: 8,
|
||||
borderTopLeftRadius: 8,
|
||||
},
|
||||
});
|
||||
@@ -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,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,
|
||||
},
|
||||
});
|
||||
@@ -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;
|
||||
};
|
||||
@@ -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;
|
||||
}
|
||||
};
|
||||
@@ -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",
|
||||
};
|
||||
@@ -0,0 +1,42 @@
|
||||
import React from "react";
|
||||
import { Dimensions, StyleSheet, View } from "react-native";
|
||||
|
||||
import { IconButton } from "react-native-paper";
|
||||
|
||||
export const Actionbar = ({ actions, theme }) => {
|
||||
return (
|
||||
<View
|
||||
style={[
|
||||
styles.actionbarContainer,
|
||||
{ backgroundColor: theme.colorPrimary },
|
||||
]}
|
||||
>
|
||||
{actions.map(
|
||||
(action, index) =>
|
||||
action.icon && (
|
||||
<IconButton
|
||||
key={index}
|
||||
icon={action.icon}
|
||||
color={theme.textColorPrimary}
|
||||
onPress={action.onPress}
|
||||
/>
|
||||
)
|
||||
)}
|
||||
</View>
|
||||
);
|
||||
};
|
||||
|
||||
const styles = StyleSheet.create({
|
||||
actionbarContainer: {
|
||||
position: "absolute",
|
||||
width: Dimensions.get("window").width - 32,
|
||||
bottom: 0,
|
||||
margin: 16,
|
||||
borderRadius: 6,
|
||||
flexDirection: "row",
|
||||
justifyContent: "space-between",
|
||||
paddingHorizontal: 8,
|
||||
paddingVertical: 4,
|
||||
elevation: 2,
|
||||
},
|
||||
});
|
||||
@@ -10,6 +10,7 @@ const ColorPickerModal = ({
|
||||
onSubmit,
|
||||
hideModal,
|
||||
modalVisible,
|
||||
showAccentColors,
|
||||
}) => {
|
||||
const [text, setText] = useState(color);
|
||||
const [error, setError] = useState();
|
||||
@@ -10,7 +10,7 @@ const EmptyView = ({ icon, description, style, children, iconStyle }) => {
|
||||
<Text
|
||||
style={[
|
||||
styles.emptyViewIcon,
|
||||
{ color: theme.textColorSecondary },
|
||||
{ color: theme.textColorHint },
|
||||
style,
|
||||
iconStyle,
|
||||
]}
|
||||
@@ -20,7 +20,7 @@ const EmptyView = ({ icon, description, style, children, iconStyle }) => {
|
||||
<Text
|
||||
style={[
|
||||
styles.emptyViewText,
|
||||
{ color: theme.textColorSecondary },
|
||||
{ color: theme.textColorHint },
|
||||
style,
|
||||
]}
|
||||
>
|
||||
@@ -17,7 +17,7 @@ const ErrorView = ({ theme, onRetry, error, openWebView }) => (
|
||||
icon="reload"
|
||||
size={25}
|
||||
style={{ margin: 0, marginTop: 24 }}
|
||||
color={theme.textColorSecondary}
|
||||
color={theme.text}
|
||||
onPress={onRetry}
|
||||
/>
|
||||
<Text style={{ color: theme.textColorPrimary }}>Retry</Text>
|
||||
@@ -1,5 +1,5 @@
|
||||
import React from "react";
|
||||
import { StyleSheet } from "react-native";
|
||||
import { StyleSheet, View } from "react-native";
|
||||
|
||||
import { List, Divider as PaperDivider } from "react-native-paper";
|
||||
import MaterialCommunityIcons from "react-native-vector-icons/MaterialCommunityIcons";
|
||||
@@ -34,15 +34,19 @@ const ListItem = ({
|
||||
description={description}
|
||||
descriptionStyle={{ color: theme.textColorSecondary }}
|
||||
descriptionNumberOfLines={1}
|
||||
left={() =>
|
||||
icon && (
|
||||
<List.Icon
|
||||
color={theme.colorAccent}
|
||||
icon={icon}
|
||||
style={{ marginVertical: 0 }}
|
||||
/>
|
||||
)
|
||||
}
|
||||
left={() => (
|
||||
<View style={{ justifyContent: "center" }}>
|
||||
{icon && (
|
||||
<List.Icon
|
||||
color={theme.colorAccent}
|
||||
icon={icon}
|
||||
style={{
|
||||
marginVertical: 0,
|
||||
}}
|
||||
/>
|
||||
)}
|
||||
</View>
|
||||
)}
|
||||
right={() =>
|
||||
right && (
|
||||
<MaterialCommunityIcons
|
||||
@@ -3,7 +3,14 @@ import { StyleSheet, View, Text, Image } from "react-native";
|
||||
import FastImage from "react-native-fast-image";
|
||||
import { TouchableRipple } from "react-native-paper";
|
||||
|
||||
const ListView = ({ item, downloadBadge, unreadBadge, theme, onPress }) => {
|
||||
const ListView = ({
|
||||
item,
|
||||
downloadBadge,
|
||||
unreadBadge,
|
||||
inLibraryBadge,
|
||||
theme,
|
||||
onPress,
|
||||
}) => {
|
||||
return (
|
||||
<TouchableRipple
|
||||
borderless
|
||||
@@ -15,7 +22,10 @@ const ListView = ({ item, downloadBadge, unreadBadge, theme, onPress }) => {
|
||||
<>
|
||||
<FastImage
|
||||
source={{ uri: item.novelCover }}
|
||||
style={styles.extensionIcon}
|
||||
style={[
|
||||
styles.extensionIcon,
|
||||
inLibraryBadge && { opacity: 0.5 },
|
||||
]}
|
||||
/>
|
||||
<Text
|
||||
style={[
|
||||
@@ -29,6 +39,7 @@ const ListView = ({ item, downloadBadge, unreadBadge, theme, onPress }) => {
|
||||
<View style={styles.badgeContainer}>
|
||||
{downloadBadge}
|
||||
{unreadBadge}
|
||||
{inLibraryBadge}
|
||||
</View>
|
||||
</>
|
||||
</TouchableRipple>
|
||||
@@ -0,0 +1,8 @@
|
||||
import React from "react";
|
||||
import { ActivityIndicator, View } from "react-native";
|
||||
|
||||
export const LoadingScreen = ({ theme }) => (
|
||||
<View style={{ flex: 1, justifyContent: "center" }}>
|
||||
<ActivityIndicator size={50} color={theme.colorAccent} />
|
||||
</View>
|
||||
);
|
||||
@@ -2,7 +2,7 @@ import React, { useState } from "react";
|
||||
import { StyleSheet, View, Text } from "react-native";
|
||||
import { Portal, Modal, Button } from "react-native-paper";
|
||||
import * as Linking from "expo-linking";
|
||||
import { useTheme } from "../Hooks/reduxHooks";
|
||||
import { useTheme } from "../hooks/reduxHooks";
|
||||
|
||||
const NewUpdateDialog = ({ newVersion }) => {
|
||||
const [newUpdateDialog, showNewUpdateDialog] = useState(true);
|
||||
@@ -0,0 +1,282 @@
|
||||
import React, { useCallback } from "react";
|
||||
import {
|
||||
StyleSheet,
|
||||
View,
|
||||
Text,
|
||||
useWindowDimensions,
|
||||
Pressable,
|
||||
} from "react-native";
|
||||
|
||||
import { LinearGradient } from "expo-linear-gradient";
|
||||
import FastImage from "react-native-fast-image";
|
||||
|
||||
import ListView from "./ListView";
|
||||
|
||||
import { getDeviceOrientation } from "../services/utils/helpers";
|
||||
import { useSettings } from "../hooks/reduxHooks";
|
||||
|
||||
const NovelCover = ({ item, onPress, libraryStatus, theme }) => {
|
||||
const { displayMode, novelsPerRow, showDownloadBadges, showUnreadBadges } =
|
||||
useSettings();
|
||||
|
||||
const window = useWindowDimensions();
|
||||
|
||||
const orientation = getDeviceOrientation();
|
||||
|
||||
const getNovelsPerRow = () =>
|
||||
orientation === "landscape" ? 6 : novelsPerRow;
|
||||
|
||||
const getHeight = useCallback(
|
||||
() => (window.width / getNovelsPerRow()) * (4 / 3),
|
||||
[]
|
||||
);
|
||||
|
||||
return displayMode !== 2 ? (
|
||||
<View
|
||||
style={{
|
||||
flex: 1 / getNovelsPerRow(),
|
||||
borderRadius: 4,
|
||||
overflow: "hidden",
|
||||
}}
|
||||
>
|
||||
<Pressable
|
||||
android_ripple={{ color: theme.colorAccent }}
|
||||
style={styles.opac}
|
||||
onPress={onPress}
|
||||
>
|
||||
<>
|
||||
<View style={styles.badgeContainer}>
|
||||
{libraryStatus && <InLibraryBadge theme={theme} />}
|
||||
{showDownloadBadges && item.chaptersDownloaded && (
|
||||
<DownloadBadge
|
||||
showUnreadBadges={showUnreadBadges}
|
||||
chaptersDownloaded={item.chaptersDownloaded}
|
||||
chaptersUnread={item.chaptersUnread}
|
||||
/>
|
||||
)}
|
||||
{showUnreadBadges && item.chaptersUnread && (
|
||||
<UnreadBadge
|
||||
theme={theme}
|
||||
chaptersDownloaded={item.chaptersDownloaded}
|
||||
chaptersUnread={item.chaptersUnread}
|
||||
showDownloadBadges={showDownloadBadges}
|
||||
/>
|
||||
)}
|
||||
</View>
|
||||
<FastImage
|
||||
source={{ uri: item.novelCover }}
|
||||
style={[
|
||||
{ height: getHeight(), borderRadius: 4 },
|
||||
libraryStatus && { opacity: 0.5 },
|
||||
]}
|
||||
/>
|
||||
<View style={styles.compactTitleContainer}>
|
||||
{displayMode === 0 && (
|
||||
<CompactTitle novelName={item.novelName} />
|
||||
)}
|
||||
</View>
|
||||
{displayMode === 1 && (
|
||||
<ComfortableTitle
|
||||
novelName={item.novelName}
|
||||
theme={theme}
|
||||
/>
|
||||
)}
|
||||
</>
|
||||
</Pressable>
|
||||
</View>
|
||||
) : (
|
||||
<ListView
|
||||
item={item}
|
||||
downloadBadge={
|
||||
showDownloadBadges &&
|
||||
item.chaptersDownloaded && (
|
||||
<DownloadBadge
|
||||
showUnreadBadges={showUnreadBadges}
|
||||
chaptersDownloaded={item.chaptersDownloaded}
|
||||
chaptersUnread={item.chaptersUnread}
|
||||
/>
|
||||
)
|
||||
}
|
||||
unreadBadge={
|
||||
showUnreadBadges &&
|
||||
item.chaptersUnread && (
|
||||
<UnreadBadge
|
||||
theme={theme}
|
||||
chaptersDownloaded={item.chaptersDownloaded}
|
||||
chaptersUnread={item.chaptersUnread}
|
||||
showDownloadBadges={showDownloadBadges}
|
||||
/>
|
||||
)
|
||||
}
|
||||
inLibraryBadge={libraryStatus && <InLibraryBadge theme={theme} />}
|
||||
theme={theme}
|
||||
onPress={onPress}
|
||||
/>
|
||||
);
|
||||
};
|
||||
|
||||
export default NovelCover;
|
||||
|
||||
const ComfortableTitle = ({ theme, novelName }) => (
|
||||
<Text
|
||||
numberOfLines={2}
|
||||
style={[
|
||||
styles.title,
|
||||
{
|
||||
color: theme.textColorPrimary,
|
||||
padding: 4,
|
||||
},
|
||||
]}
|
||||
>
|
||||
{novelName}
|
||||
</Text>
|
||||
);
|
||||
|
||||
const CompactTitle = ({ novelName }) => (
|
||||
<View style={styles.titleContainer}>
|
||||
<LinearGradient
|
||||
colors={["transparent", "rgba(0,0,0,0.7)"]}
|
||||
style={styles.linearGradient}
|
||||
>
|
||||
<Text
|
||||
numberOfLines={2}
|
||||
style={[styles.title, { color: "rgba(255,255,255,1)" }]}
|
||||
>
|
||||
{novelName}
|
||||
</Text>
|
||||
</LinearGradient>
|
||||
</View>
|
||||
);
|
||||
|
||||
const InLibraryBadge = ({ theme }) => (
|
||||
<Text
|
||||
style={[
|
||||
styles.inLibraryBadge,
|
||||
{
|
||||
backgroundColor: theme.colorAccent,
|
||||
color: theme.colorButtonText,
|
||||
borderRadius: 4,
|
||||
},
|
||||
]}
|
||||
>
|
||||
In library
|
||||
</Text>
|
||||
);
|
||||
|
||||
const UnreadBadge = ({
|
||||
chaptersDownloaded,
|
||||
chaptersUnread,
|
||||
showDownloadBadges,
|
||||
theme,
|
||||
}) => (
|
||||
<Text
|
||||
style={[
|
||||
styles.unreadBadge,
|
||||
!chaptersDownloaded && {
|
||||
borderTopLeftRadius: 4,
|
||||
borderBottomLeftRadius: 4,
|
||||
},
|
||||
!showDownloadBadges && {
|
||||
borderRadius: 4,
|
||||
},
|
||||
{
|
||||
backgroundColor: theme.colorAccent,
|
||||
color: theme.colorButtonText,
|
||||
},
|
||||
]}
|
||||
>
|
||||
{chaptersUnread}
|
||||
</Text>
|
||||
);
|
||||
|
||||
const DownloadBadge = ({
|
||||
chaptersDownloaded,
|
||||
showUnreadBadges,
|
||||
chaptersUnread,
|
||||
}) => (
|
||||
<Text
|
||||
style={[
|
||||
styles.downloadBadge,
|
||||
!chaptersUnread && {
|
||||
borderTopRightRadius: 4,
|
||||
borderBottomRightRadius: 4,
|
||||
},
|
||||
!showUnreadBadges && {
|
||||
borderRadius: 4,
|
||||
},
|
||||
]}
|
||||
>
|
||||
{chaptersDownloaded}
|
||||
</Text>
|
||||
);
|
||||
|
||||
const styles = StyleSheet.create({
|
||||
titleContainer: {
|
||||
flex: 1,
|
||||
borderRadius: 4,
|
||||
},
|
||||
title: {
|
||||
fontFamily: "pt-sans-bold",
|
||||
fontSize: 14,
|
||||
padding: 8,
|
||||
},
|
||||
linearGradient: {
|
||||
bottom: -1,
|
||||
borderRadius: 4,
|
||||
},
|
||||
opac: {
|
||||
paddingHorizontal: 4,
|
||||
paddingVertical: 4,
|
||||
borderRadius: 4,
|
||||
flex: 1,
|
||||
},
|
||||
extensionIcon: {
|
||||
width: 42,
|
||||
height: 42,
|
||||
borderRadius: 4,
|
||||
},
|
||||
listView: {
|
||||
flex: 1,
|
||||
paddingVertical: 8,
|
||||
paddingHorizontal: 12,
|
||||
flexDirection: "row",
|
||||
alignItems: "center",
|
||||
borderRadius: 4,
|
||||
},
|
||||
|
||||
downloadBadge: {
|
||||
backgroundColor: "#47a84a",
|
||||
color: "#FFFFFF",
|
||||
borderTopLeftRadius: 4,
|
||||
borderBottomLeftRadius: 4,
|
||||
paddingTop: 2,
|
||||
paddingHorizontal: 4,
|
||||
fontSize: 12,
|
||||
},
|
||||
unreadBadge: {
|
||||
color: "#FFFFFF",
|
||||
borderTopRightRadius: 4,
|
||||
borderBottomRightRadius: 4,
|
||||
paddingTop: 2,
|
||||
paddingHorizontal: 4,
|
||||
fontSize: 12,
|
||||
},
|
||||
inLibraryBadge: {
|
||||
paddingVertical: 2,
|
||||
paddingHorizontal: 4,
|
||||
fontSize: 12,
|
||||
},
|
||||
compactTitleContainer: {
|
||||
position: "absolute",
|
||||
bottom: 4,
|
||||
left: 4,
|
||||
right: 4,
|
||||
},
|
||||
badgeContainer: {
|
||||
position: "absolute",
|
||||
zIndex: 1,
|
||||
top: 8,
|
||||
left: 8,
|
||||
flexDirection: "row",
|
||||
},
|
||||
});
|
||||
@@ -1,14 +1,17 @@
|
||||
import React from "react";
|
||||
import React, { useCallback } from "react";
|
||||
import { StyleSheet, FlatList } from "react-native";
|
||||
|
||||
import { useSettings } from "../Hooks/reduxHooks";
|
||||
import { getDeviceOrientation } from "../Services/utils/helpers";
|
||||
import { useSettings } from "../hooks/reduxHooks";
|
||||
import { getDeviceOrientation } from "../services/utils/helpers";
|
||||
|
||||
const NovelList = ({
|
||||
data,
|
||||
onScroll,
|
||||
onEndReached,
|
||||
renderItem,
|
||||
refreshControl,
|
||||
ListEmptyComponent,
|
||||
ListFooterComponent,
|
||||
}) => {
|
||||
const { displayMode, novelsPerRow } = useSettings();
|
||||
|
||||
@@ -26,16 +29,21 @@ const NovelList = ({
|
||||
}
|
||||
};
|
||||
|
||||
const keyExtractor = useCallback((item) => item.novelUrl, []);
|
||||
|
||||
return (
|
||||
<FlatList
|
||||
contentContainerStyle={styles.flatListCont}
|
||||
numColumns={getNovelsPerRow()}
|
||||
key={[orientation, getNovelsPerRow()]}
|
||||
data={data}
|
||||
keyExtractor={(item) => item.novelUrl}
|
||||
keyExtractor={keyExtractor}
|
||||
renderItem={renderItem}
|
||||
refreshControl={refreshControl}
|
||||
ListEmptyComponent={ListEmptyComponent}
|
||||
ListFooterComponent={ListFooterComponent}
|
||||
onScroll={onScroll}
|
||||
onEndReached={onEndReached}
|
||||
/>
|
||||
);
|
||||
};
|
||||
@@ -2,7 +2,7 @@ import React, { useRef } from "react";
|
||||
import { StyleSheet, View, TextInput, Text, StatusBar } from "react-native";
|
||||
|
||||
import { TouchableRipple, IconButton } from "react-native-paper";
|
||||
import { Row } from "./Common";
|
||||
import { Row } from "../Common";
|
||||
|
||||
export const Searchbar = ({
|
||||
theme,
|
||||
@@ -0,0 +1,56 @@
|
||||
import React from "react";
|
||||
import { Pressable, StyleSheet, View, Text } from "react-native";
|
||||
|
||||
import { Switch, List } from "react-native-paper";
|
||||
|
||||
const SwitchSetting = ({ label, description, onPress, theme, icon, value }) => (
|
||||
<Pressable
|
||||
android_ripple={{ color: theme.rippleColor }}
|
||||
style={styles.container}
|
||||
onPress={onPress}
|
||||
>
|
||||
<View style={{ flexDirection: "row", flex: 1 }}>
|
||||
{icon && (
|
||||
<List.Icon
|
||||
color={theme.colorAccent}
|
||||
icon={icon}
|
||||
style={{ margin: 0 }}
|
||||
/>
|
||||
)}
|
||||
<View style={[icon && { marginLeft: 16 }]}>
|
||||
<Text
|
||||
style={{
|
||||
color: theme.textColorPrimary,
|
||||
fontSize: 16,
|
||||
flex: 1,
|
||||
textAlignVertical: "center",
|
||||
}}
|
||||
>
|
||||
{label}
|
||||
</Text>
|
||||
{description && (
|
||||
<Text style={{ color: theme.textColorSecondary }}>
|
||||
{description}
|
||||
</Text>
|
||||
)}
|
||||
</View>
|
||||
</View>
|
||||
<Switch
|
||||
value={value}
|
||||
onValueChange={onPress}
|
||||
color={theme.colorAccent}
|
||||
style={{ marginRight: 8 }}
|
||||
/>
|
||||
</Pressable>
|
||||
);
|
||||
|
||||
export default SwitchSetting;
|
||||
|
||||
const styles = StyleSheet.create({
|
||||
container: {
|
||||
paddingHorizontal: 16,
|
||||
paddingVertical: 12,
|
||||
flexDirection: "row",
|
||||
justifyContent: "space-between",
|
||||
},
|
||||
});
|
||||
@@ -1,4 +1,5 @@
|
||||
import * as SQLite from "expo-sqlite";
|
||||
import { getSource } from "../../sources/sources";
|
||||
const db = SQLite.openDatabase("lnreader.db");
|
||||
|
||||
const insertChaptersQuery = `INSERT INTO chapters (chapterUrl, chapterName, releaseDate, novelId) values (?, ?, ?, ?)`;
|
||||
@@ -16,15 +17,15 @@ export const insertChapters = async (novelId, chapters) => {
|
||||
novelId,
|
||||
],
|
||||
(txObj, res) => {},
|
||||
(txObj, error) =>
|
||||
console.log(
|
||||
"Error ",
|
||||
error,
|
||||
" Chapter URL: ",
|
||||
chapter.chapterUrl,
|
||||
" Novel ID ",
|
||||
novelId
|
||||
)
|
||||
(txObj, error) => {}
|
||||
// console.log(
|
||||
// "Error ",
|
||||
// error,
|
||||
// " Chapter URL: ",
|
||||
// chapter.chapterUrl,
|
||||
// " Novel ID ",
|
||||
// novelId
|
||||
// )
|
||||
),
|
||||
(txObj, res) => {
|
||||
console.log("Success");
|
||||
@@ -34,13 +35,10 @@ export const insertChapters = async (novelId, chapters) => {
|
||||
});
|
||||
};
|
||||
|
||||
const getChaptersQuery = (sort, filter) =>
|
||||
`SELECT * FROM chapters WHERE novelId = ? ${filter || ""} ${
|
||||
sort || "ORDER BY chapterId ASC"
|
||||
}`;
|
||||
const getChaptersQuery = (sort = "ORDER BY chapterId ASC", filter = "") =>
|
||||
`SELECT * FROM chapters WHERE novelId = ? ${filter} ${sort}`;
|
||||
|
||||
export const getChapters = (novelId, sort, filter) => {
|
||||
// console.log(getChaptersQuery(sort, filter));
|
||||
return new Promise((resolve, reject) =>
|
||||
db.transaction((tx) => {
|
||||
tx.executeSql(
|
||||
@@ -157,10 +155,9 @@ export const downloadChapter = async (
|
||||
chapterUrl,
|
||||
chapterId
|
||||
) => {
|
||||
const downloadUrl = `https://lnreader-extensions.vercel.app/api/${sourceId}/novel/${novelUrl}${chapterUrl}`;
|
||||
const source = getSource(sourceId);
|
||||
|
||||
const response = await fetch(downloadUrl);
|
||||
const chapter = await response.json();
|
||||
const chapter = await source.parseChapter(novelUrl, chapterUrl);
|
||||
|
||||
db.transaction((tx) => {
|
||||
tx.executeSql(
|
||||
@@ -262,3 +259,32 @@ export const markPreviousChaptersUnread = async (chapterId, novelId) => {
|
||||
);
|
||||
});
|
||||
};
|
||||
|
||||
const getDownloadedChaptersQuery = `
|
||||
SELECT chapters.*, novels.sourceId, novels.novelName, novels.novelCover
|
||||
FROM chapters
|
||||
JOIN novels
|
||||
ON chapters.novelId = novels.novelId
|
||||
WHERE chapters.downloaded = 1`;
|
||||
|
||||
export const getDownloadedChapters = () => {
|
||||
return new Promise((resolve, reject) =>
|
||||
db.transaction((tx) => {
|
||||
tx.executeSql(
|
||||
getDownloadedChaptersQuery,
|
||||
null,
|
||||
(txObj, { rows: { _array } }) => {
|
||||
resolve(_array);
|
||||
},
|
||||
(txObj, error) => console.log("Error ", error)
|
||||
);
|
||||
})
|
||||
);
|
||||
};
|
||||
|
||||
export const deleteDownloads = async () => {
|
||||
db.transaction((tx) => {
|
||||
tx.executeSql(`UPDATE chapters SET downloaded = 0`);
|
||||
tx.executeSql("DELETE FROM downloads; VACCUM;");
|
||||
});
|
||||
};
|
||||
@@ -1,9 +1,13 @@
|
||||
import * as SQLite from "expo-sqlite";
|
||||
const db = SQLite.openDatabase("lnreader.db");
|
||||
|
||||
import { fetchChapters, fetchNovel } from "../../Services/Source/source";
|
||||
import BackgroundService from "react-native-background-actions";
|
||||
|
||||
import { fetchChapters, fetchNovel } from "../../services/Source/source";
|
||||
import { insertChapters } from "./ChapterQueries";
|
||||
|
||||
import { showToast } from "../../hooks/showToast";
|
||||
|
||||
const insertNovelQuery = `INSERT INTO novels (novelUrl, sourceUrl, sourceId, source, novelName, novelCover, novelSummary, author, artist, status, genre) VALUES(?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`;
|
||||
|
||||
export const insertNovel = async (novel) => {
|
||||
@@ -81,9 +85,7 @@ export const deleteNovelCache = () => {
|
||||
tx.executeSql(
|
||||
"DELETE FROM novels WHERE followed = 0",
|
||||
null,
|
||||
(txObj, res) => {
|
||||
console.log("Cleared Novel Cache");
|
||||
},
|
||||
(txObj, res) => showToast("Entries deleted"),
|
||||
(txObj, error) => console.log("Error ", error)
|
||||
);
|
||||
});
|
||||
@@ -127,33 +129,67 @@ export const restoreLibrary = async (novel) => {
|
||||
const migrateNovelQuery = `INSERT INTO novels (novelUrl, sourceUrl, sourceId, source, novelName, novelCover, novelSummary, author, artist, status, genre, followed) VALUES(?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`;
|
||||
|
||||
export const migrateNovel = async (sourceId, novelUrl) => {
|
||||
const novel = await fetchNovel(sourceId, novelUrl);
|
||||
try {
|
||||
const novel = await fetchNovel(sourceId, novelUrl);
|
||||
|
||||
db.transaction((tx) =>
|
||||
tx.executeSql(
|
||||
migrateNovelQuery,
|
||||
[
|
||||
novel.novelUrl,
|
||||
novel.sourceUrl,
|
||||
novel.sourceId,
|
||||
novel.source,
|
||||
novel.novelName,
|
||||
novel.novelCover,
|
||||
novel.novelSummary,
|
||||
novel.author,
|
||||
novel.artist,
|
||||
novel.status,
|
||||
novel.genre,
|
||||
1,
|
||||
],
|
||||
async (txObj, { insertId }) => {
|
||||
const chapters = await fetchChapters(
|
||||
novel.sourceId,
|
||||
novel.novelUrl
|
||||
);
|
||||
await insertChapters(insertId, chapters);
|
||||
const options = {
|
||||
taskName: "Migration",
|
||||
taskTitle: `Migrating ${novel.novelName} to new source`,
|
||||
taskDesc: novel.source,
|
||||
taskIcon: {
|
||||
name: "notification_icon",
|
||||
type: "drawable",
|
||||
},
|
||||
(txObj, error) => console.log("Error ", error)
|
||||
)
|
||||
);
|
||||
color: "#00adb5",
|
||||
parameters: {
|
||||
delay: 1000,
|
||||
},
|
||||
progressBar: {
|
||||
max: 1,
|
||||
value: 0,
|
||||
indeterminate: true,
|
||||
},
|
||||
};
|
||||
|
||||
const veryIntensiveTask = async () => {
|
||||
await new Promise(async (resolve) => {
|
||||
db.transaction((tx) =>
|
||||
tx.executeSql(
|
||||
migrateNovelQuery,
|
||||
[
|
||||
novel.novelUrl,
|
||||
novel.sourceUrl,
|
||||
novel.sourceId,
|
||||
novel.source,
|
||||
novel.novelName,
|
||||
novel.novelCover,
|
||||
novel.novelSummary,
|
||||
novel.author,
|
||||
novel.artist,
|
||||
novel.status,
|
||||
novel.genre,
|
||||
1,
|
||||
],
|
||||
async (txObj, { insertId }) => {
|
||||
const chapters = await fetchChapters(
|
||||
novel.sourceId,
|
||||
novel.novelUrl
|
||||
);
|
||||
await insertChapters(insertId, chapters);
|
||||
resolve();
|
||||
},
|
||||
(txObj, error) => console.log("Error ", error)
|
||||
)
|
||||
);
|
||||
});
|
||||
};
|
||||
|
||||
await BackgroundService.start(veryIntensiveTask, options);
|
||||
await BackgroundService.updateNotification({
|
||||
progressBar: { value: 1, indeterminate: false },
|
||||
});
|
||||
await BackgroundService.stop();
|
||||
} catch (error) {
|
||||
showToast(error.message);
|
||||
}
|
||||
};
|
||||
@@ -15,4 +15,4 @@ export const createChapterTableQuery = `
|
||||
|
||||
export const createNovelIdIndexQuery = `CREATE INDEX IF NOT EXISTS chapterNovelIdIndex ON chapters(novelId)`;
|
||||
|
||||
export const createUnreadChaptersIndexQuery = `CREATE INDEX IF NOT EXISTS chapterUnreadByNovelIndex ON chapters(novelId, \`read\`)`;
|
||||
export const createUnreadChaptersIndexQuery = `CREATE INDEX IF NOT EXISTS chapterUnreadByNovelIndex ON chapters(novelId, \`read\`) WHERE \`read\`=0`;
|
||||
@@ -22,7 +22,7 @@ export const githubUpdateChecker = () => {
|
||||
};
|
||||
|
||||
const isNewVersion = (versionTag) => {
|
||||
let currentVersion = "v1.0.21";
|
||||
let currentVersion = "v1.1.1";
|
||||
const regex = /[^\\d.]/;
|
||||
|
||||
let newVersion = versionTag.replace(regex, "");
|
||||
@@ -0,0 +1,93 @@
|
||||
import React from "react";
|
||||
|
||||
import { createMaterialBottomTabNavigator } from "@react-navigation/material-bottom-tabs";
|
||||
import MaterialCommunityIcons from "react-native-vector-icons/MaterialCommunityIcons";
|
||||
|
||||
import Library from "../screens/Library/Library";
|
||||
import Updates from "../screens/Updates/Updates";
|
||||
import History from "../screens/History/History";
|
||||
import Browse from "../screens/Browse/Browse";
|
||||
import More from "../screens/More/More";
|
||||
|
||||
import { useTheme } from "../hooks/reduxHooks";
|
||||
|
||||
const Tab = createMaterialBottomTabNavigator();
|
||||
|
||||
const BottomNavigator = () => {
|
||||
const theme = useTheme();
|
||||
|
||||
return (
|
||||
<Tab.Navigator
|
||||
barStyle={{ backgroundColor: theme.colorPrimary }}
|
||||
activeColor={theme.colorAccent}
|
||||
>
|
||||
<Tab.Screen
|
||||
name="Library"
|
||||
component={Library}
|
||||
options={{
|
||||
tabBarIcon: ({ color }) => (
|
||||
<MaterialCommunityIcons
|
||||
name="book-variant-multiple"
|
||||
color={color}
|
||||
size={24}
|
||||
/>
|
||||
),
|
||||
}}
|
||||
/>
|
||||
<Tab.Screen
|
||||
name="Updates"
|
||||
component={Updates}
|
||||
options={{
|
||||
tabBarIcon: ({ color }) => (
|
||||
<MaterialCommunityIcons
|
||||
name="alert-decagram-outline"
|
||||
color={color}
|
||||
size={24}
|
||||
/>
|
||||
),
|
||||
}}
|
||||
/>
|
||||
<Tab.Screen
|
||||
name="History"
|
||||
component={History}
|
||||
options={{
|
||||
tabBarIcon: ({ color }) => (
|
||||
<MaterialCommunityIcons
|
||||
name="history"
|
||||
color={color}
|
||||
size={24}
|
||||
/>
|
||||
),
|
||||
}}
|
||||
/>
|
||||
<Tab.Screen
|
||||
name="Browse"
|
||||
component={Browse}
|
||||
options={{
|
||||
tabBarIcon: ({ color }) => (
|
||||
<MaterialCommunityIcons
|
||||
name="compass-outline"
|
||||
color={color}
|
||||
size={24}
|
||||
/>
|
||||
),
|
||||
}}
|
||||
/>
|
||||
<Tab.Screen
|
||||
name="More"
|
||||
component={More}
|
||||
options={{
|
||||
tabBarIcon: ({ color }) => (
|
||||
<MaterialCommunityIcons
|
||||
name="dots-horizontal"
|
||||
color={color}
|
||||
size={24}
|
||||
/>
|
||||
),
|
||||
}}
|
||||
/>
|
||||
</Tab.Navigator>
|
||||
);
|
||||
};
|
||||
|
||||
export default BottomNavigator;
|
||||
@@ -1,36 +1,38 @@
|
||||
import React from "react";
|
||||
import {
|
||||
createStackNavigator,
|
||||
TransitionPresets,
|
||||
} from "@react-navigation/stack";
|
||||
|
||||
import BottomNavigator from "./BottomNavigator";
|
||||
import Novel from "../Screens/Novel/Novel";
|
||||
import Chapter from "../Screens/Chapter/Chapter";
|
||||
import Extension from "../Screens/Extension/Extension";
|
||||
import GlobalSearch from "../Screens/Browse/globalsearch/GlobalSearch";
|
||||
import Migration from "../Screens/Browse/migration/Migration";
|
||||
import SourceNovels from "../Screens/Browse/SourceNovels";
|
||||
import MigrateNovel from "../Screens/Browse/migration/MigrationNovels";
|
||||
import MoreStack from "./More";
|
||||
import MalTopNovels from "../Screens/Browse/discover/MalTopNovels";
|
||||
import { setStatusBarStyle } from "../Hooks/setStatusBarStyle";
|
||||
import React, { useEffect } from "react";
|
||||
import { View } from "react-native";
|
||||
import { useTheme } from "../Hooks/reduxHooks";
|
||||
import { githubUpdateChecker } from "../Hooks/githubUpdateChecker";
|
||||
import NewUpdateDialog from "../Components/NewUpdateDialog";
|
||||
|
||||
import { NavigationContainer } from "@react-navigation/native";
|
||||
import BrowseNovelUpdates from "../Screens/Browse/discover/BrowseNovelUpdates";
|
||||
import { createStackNavigator } from "@react-navigation/stack";
|
||||
|
||||
import { setBarColor } from "../theme/utils/setBarColor";
|
||||
import { useTheme } from "../hooks/reduxHooks";
|
||||
import { githubUpdateChecker } from "../hooks/githubUpdateChecker";
|
||||
|
||||
// Navigators
|
||||
import BottomNavigator from "./BottomNavigator";
|
||||
|
||||
// Screens
|
||||
import Novel from "../screens/Novel/Novel";
|
||||
import Chapter from "../screens/Chapter/Chapter";
|
||||
import Extension from "../screens/Extension/Extension";
|
||||
import GlobalSearch from "../screens/Browse/globalsearch/GlobalSearch";
|
||||
import Migration from "../screens/Browse/migration/Migration";
|
||||
import SourceNovels from "../screens/Browse/SourceNovels";
|
||||
import MigrateNovel from "../screens/Browse/migration/MigrationNovels";
|
||||
import MoreStack from "./More";
|
||||
import MalTopNovels from "../screens/Browse/discover/MalTopNovels";
|
||||
import NewUpdateDialog from "../components/NewUpdateDialog";
|
||||
import BrowseSettings from "../screens/Browse/BrowseSettings";
|
||||
|
||||
const Stack = createStackNavigator();
|
||||
|
||||
const stackNavigatorConfig = {
|
||||
headerShown: false,
|
||||
};
|
||||
|
||||
const MainNavigator = () => {
|
||||
const theme = useTheme();
|
||||
setStatusBarStyle();
|
||||
|
||||
useEffect(() => {
|
||||
setBarColor(theme);
|
||||
}, [theme]);
|
||||
|
||||
const { isNewVersion, latestRelease } = githubUpdateChecker() || {};
|
||||
|
||||
return (
|
||||
@@ -39,29 +41,19 @@ const MainNavigator = () => {
|
||||
>
|
||||
<View style={{ flex: 1, backgroundColor: theme.colorPrimaryDark }}>
|
||||
{isNewVersion && <NewUpdateDialog newVersion={latestRelease} />}
|
||||
<Stack.Navigator screenOptions={stackNavigatorConfig}>
|
||||
<Stack.Navigator screenOptions={{ headerShown: false }}>
|
||||
<Stack.Screen
|
||||
name="BottomNavigator"
|
||||
component={BottomNavigator}
|
||||
/>
|
||||
<Stack.Screen
|
||||
name="Novel"
|
||||
component={Novel}
|
||||
options={{
|
||||
...TransitionPresets.RevealFromBottomAndroid,
|
||||
}}
|
||||
/>
|
||||
<Stack.Screen
|
||||
name="Chapter"
|
||||
component={Chapter}
|
||||
options={{ ...TransitionPresets.SlideFromRightIOS }}
|
||||
/>
|
||||
<Stack.Screen name="Novel" component={Novel} />
|
||||
<Stack.Screen name="Chapter" component={Chapter} />
|
||||
<Stack.Screen name="MoreStack" component={MoreStack} />
|
||||
<Stack.Screen name="Extension" component={Extension} />
|
||||
<Stack.Screen name="BrowseMal" component={MalTopNovels} />
|
||||
<Stack.Screen
|
||||
name="BrowseNovelUpdates"
|
||||
component={BrowseNovelUpdates}
|
||||
name="BrowseSettings"
|
||||
component={BrowseSettings}
|
||||
/>
|
||||
<Stack.Screen
|
||||
name="GlobalSearch"
|
||||
@@ -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;
|
||||
@@ -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";
|
||||
@@ -0,0 +1,101 @@
|
||||
import {
|
||||
SET_DOWNLOAD_QUEUE,
|
||||
CANCEL_DOWNLOAD,
|
||||
PAUSE_DOWNLOADS,
|
||||
} from "./donwloads.types";
|
||||
import BackgroundService from "react-native-background-actions";
|
||||
import { downloadChapter } from "../../database/queries/ChapterQueries";
|
||||
import { CHAPTER_DOWNLOADED } from "../novel/novel.types";
|
||||
import { showToast } from "../../hooks/showToast";
|
||||
|
||||
export const pauseDownloads = () => async (dispatch) => {
|
||||
showToast("Download paused");
|
||||
|
||||
await BackgroundService.stop();
|
||||
|
||||
dispatch({ type: PAUSE_DOWNLOADS });
|
||||
};
|
||||
|
||||
export const resumeDownloads = (chapters) => async (dispatch) => {
|
||||
showToast("Download resumed");
|
||||
|
||||
const options = {
|
||||
taskName: "Library Update",
|
||||
taskTitle: chapters[0].chapterName,
|
||||
taskDesc: "0/" + chapters.length,
|
||||
taskIcon: {
|
||||
name: "notification_icon",
|
||||
type: "drawable",
|
||||
},
|
||||
color: "#00adb5",
|
||||
parameters: {
|
||||
delay: 1000,
|
||||
},
|
||||
progressBar: {
|
||||
max: chapters.length,
|
||||
value: 0,
|
||||
},
|
||||
};
|
||||
|
||||
const sleep = (time) =>
|
||||
new Promise((resolve) => setTimeout(() => resolve(), time));
|
||||
|
||||
const veryIntensiveTask = async (taskData) => {
|
||||
await new Promise(async (resolve) => {
|
||||
for (
|
||||
let i = 0;
|
||||
BackgroundService.isRunning() && i < chapters.length;
|
||||
i++
|
||||
) {
|
||||
if (BackgroundService.isRunning()) {
|
||||
if (!chapters[i].downloaded) {
|
||||
await downloadChapter(
|
||||
chapters[i].sourceId,
|
||||
chapters[i].novelUrl,
|
||||
chapters[i].chapterUrl,
|
||||
chapters[i].chapterId
|
||||
);
|
||||
}
|
||||
|
||||
dispatch({
|
||||
type: CHAPTER_DOWNLOADED,
|
||||
payload: chapters[i].chapterId,
|
||||
});
|
||||
|
||||
await BackgroundService.updateNotification({
|
||||
taskTitle: chapters[i].chapterName,
|
||||
taskDesc: i + 1 + "/" + chapters.length,
|
||||
progressBar: {
|
||||
max: chapters.length,
|
||||
value: i + 1,
|
||||
},
|
||||
});
|
||||
|
||||
if (i + 1 === chapters.length) {
|
||||
resolve();
|
||||
await BackgroundService.stop();
|
||||
|
||||
Notifications.scheduleNotificationAsync({
|
||||
content: {
|
||||
title: "Downloader",
|
||||
body: "Download completed",
|
||||
},
|
||||
trigger: null,
|
||||
});
|
||||
}
|
||||
await sleep(taskData.delay);
|
||||
}
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
await BackgroundService.start(veryIntensiveTask, options);
|
||||
|
||||
dispatch({ type: PAUSE_DOWNLOADS });
|
||||
};
|
||||
|
||||
export const cancelDownload = () => async (dispatch) => {
|
||||
await BackgroundService.stop();
|
||||
|
||||
dispatch({ type: CANCEL_DOWNLOAD });
|
||||
};
|
||||
@@ -0,0 +1,35 @@
|
||||
import { CHAPTER_DOWNLOADED } from "../novel/novel.types";
|
||||
import { CANCEL_DOWNLOAD, SET_DOWNLOAD_QUEUE } from "./donwloads.types";
|
||||
|
||||
const initialState = {
|
||||
downloadQueue: [],
|
||||
};
|
||||
|
||||
const downloadsReducer = (state = initialState, action) => {
|
||||
const { type, payload } = action;
|
||||
|
||||
switch (type) {
|
||||
case SET_DOWNLOAD_QUEUE:
|
||||
return {
|
||||
...state,
|
||||
downloadQueue: [...state.downloadQueue, ...payload],
|
||||
};
|
||||
|
||||
case CHAPTER_DOWNLOADED:
|
||||
return {
|
||||
...state,
|
||||
downloadQueue: state.downloadQueue.filter(
|
||||
(chapter) => chapter.chapterId !== payload
|
||||
),
|
||||
};
|
||||
case CANCEL_DOWNLOAD:
|
||||
return {
|
||||
...state,
|
||||
downloadQueue: [],
|
||||
};
|
||||
default:
|
||||
return state;
|
||||
}
|
||||
};
|
||||
|
||||
export default downloadsReducer;
|
||||
@@ -9,9 +9,9 @@ import {
|
||||
insertHistory,
|
||||
deleteHistory,
|
||||
deleteAllHistory,
|
||||
} from "../../Database/queries/HistoryQueries";
|
||||
} from "../../database/queries/HistoryQueries";
|
||||
import { SET_LAST_READ } from "../preferences/preference.types";
|
||||
import { showToast } from "../../Hooks/showToast";
|
||||
import { showToast } from "../../hooks/showToast";
|
||||
|
||||
export const getHistoryAction = () => async (dispatch) => {
|
||||
dispatch({ type: LOAD_HISTORY });
|
||||
|
||||
@@ -26,7 +26,7 @@ const historyReducer = (state = initialState, action) => {
|
||||
// ),
|
||||
// };
|
||||
case CLEAR_HISTORY:
|
||||
return { ...state, history: [] };
|
||||
return { ...state, loading: false, history: [] };
|
||||
default:
|
||||
return state;
|
||||
}
|
||||
|
||||
@@ -7,8 +7,8 @@ import {
|
||||
import {
|
||||
getLibrary,
|
||||
searchLibrary,
|
||||
} from "../../Database/queries/LibraryQueries";
|
||||
import { restoreBackup } from "../../Services/backup";
|
||||
} from "../../database/queries/LibraryQueries";
|
||||
import { restoreBackup } from "../../services/backup";
|
||||
|
||||
export const getLibraryAction = (sort, filter) => async (dispatch) => {
|
||||
// console.log("1.", sort, filter);
|
||||
|
||||
@@ -20,13 +20,13 @@ import {
|
||||
MARK_PREVIOUS_CHAPTERS_UNREAD,
|
||||
} from "./novel.types";
|
||||
|
||||
import { updateNovel } from "../../Services/updates";
|
||||
import { fetchNovel } from "../../Services/Source/source";
|
||||
import { updateNovel } from "../../services/updates";
|
||||
import { fetchNovel } from "../../services/Source/source";
|
||||
import {
|
||||
followNovel,
|
||||
insertNovel,
|
||||
getNovel,
|
||||
} from "../../Database/queries/NovelQueries";
|
||||
} from "../../database/queries/NovelQueries";
|
||||
import {
|
||||
getChapters,
|
||||
insertChapters,
|
||||
@@ -38,18 +38,34 @@ import {
|
||||
markPreviuschaptersRead,
|
||||
markPreviousChaptersUnread,
|
||||
getNextChapterFromDB,
|
||||
} from "../../Database/queries/ChapterQueries";
|
||||
import { deleteNovelUpdates } from "../../Database/queries/UpdateQueries";
|
||||
} from "../../database/queries/ChapterQueries";
|
||||
import { deleteNovelUpdates } from "../../database/queries/UpdateQueries";
|
||||
import {
|
||||
SAVE_SCROLL_POSITION,
|
||||
SET_CHAPTER_LIST_PREF,
|
||||
SET_LAST_READ,
|
||||
} from "../preferences/preference.types";
|
||||
import { GET_LIBRARY_NOVELS } from "../library/library.types";
|
||||
import { getLibrary } from "../../Database/queries/LibraryQueries";
|
||||
import { showToast } from "../../Hooks/showToast";
|
||||
import { getLibrary } from "../../database/queries/LibraryQueries";
|
||||
import { showToast } from "../../hooks/showToast";
|
||||
import { store } from "../store";
|
||||
|
||||
import * as Notifications from "expo-notifications";
|
||||
|
||||
Notifications.setNotificationHandler({
|
||||
handleNotification: async () => {
|
||||
return {
|
||||
shouldShowAlert: true,
|
||||
shouldPlaySound: true,
|
||||
shouldSetBadge: true,
|
||||
};
|
||||
},
|
||||
});
|
||||
|
||||
import BackgroundService from "react-native-background-actions";
|
||||
import { setDownloadQueue } from "../downloads/downloads.actions";
|
||||
import { SET_DOWNLOAD_QUEUE } from "../downloads/donwloads.types";
|
||||
|
||||
export const setNovel = (novel) => async (dispatch) => {
|
||||
dispatch({ type: SET_NOVEL, payload: novel });
|
||||
};
|
||||
@@ -265,14 +281,18 @@ export const markChapterUnreadAction =
|
||||
};
|
||||
|
||||
export const downloadChapterAction =
|
||||
(extensionId, novelUrl, chapterUrl, chapterName, chapterId) =>
|
||||
(sourceId, novelUrl, chapterUrl, chapterName, chapterId) =>
|
||||
async (dispatch) => {
|
||||
// dispatch({
|
||||
// type: CHAPTER_DOWNLOADING,
|
||||
// payload: chapterId,
|
||||
// });
|
||||
dispatch({
|
||||
type: CHAPTER_DOWNLOADING,
|
||||
payload: chapterId,
|
||||
type: SET_DOWNLOAD_QUEUE,
|
||||
payload: [{ chapterId, chapterName }],
|
||||
});
|
||||
|
||||
await downloadChapter(extensionId, novelUrl, chapterUrl, chapterId);
|
||||
await downloadChapter(sourceId, novelUrl, chapterUrl, chapterId);
|
||||
|
||||
dispatch({
|
||||
type: CHAPTER_DOWNLOADED,
|
||||
@@ -283,34 +303,93 @@ export const downloadChapterAction =
|
||||
};
|
||||
|
||||
export const downloadAllChaptersAction =
|
||||
(extensionId, novelUrl, chapters) => async (dispatch) => {
|
||||
(sourceId, novelUrl, chaps) => async (dispatch) => {
|
||||
try {
|
||||
await chapters.map((chapter, index) => {
|
||||
setTimeout(async () => {
|
||||
dispatch({
|
||||
type: CHAPTER_DOWNLOADING,
|
||||
payload: chapter.chapterId,
|
||||
});
|
||||
let chapters = chaps.filter((chapter) => chapter.downloaded === 0);
|
||||
|
||||
if (!chapter.downloaded) {
|
||||
await downloadChapter(
|
||||
extensionId,
|
||||
novelUrl,
|
||||
chapter.chapterUrl,
|
||||
chapter.chapterId
|
||||
);
|
||||
chapters = chapters.map((chapter) => ({
|
||||
...chapter,
|
||||
sourceId,
|
||||
novelUrl,
|
||||
}));
|
||||
|
||||
dispatch({ type: SET_DOWNLOAD_QUEUE, payload: chapters });
|
||||
const options = {
|
||||
taskName: "Library Update",
|
||||
taskTitle: chapters[0].chapterName,
|
||||
taskDesc: "0/" + chapters.length,
|
||||
taskIcon: {
|
||||
name: "notification_icon",
|
||||
type: "drawable",
|
||||
},
|
||||
color: "#00adb5",
|
||||
parameters: {
|
||||
delay: 1000,
|
||||
},
|
||||
progressBar: {
|
||||
max: chapters.length,
|
||||
value: 0,
|
||||
},
|
||||
};
|
||||
|
||||
const sleep = (time) =>
|
||||
new Promise((resolve) => setTimeout(() => resolve(), time));
|
||||
|
||||
const veryIntensiveTask = async (taskData) => {
|
||||
await new Promise(async (resolve) => {
|
||||
for (
|
||||
let i = 0;
|
||||
BackgroundService.isRunning() && i < chapters.length;
|
||||
i++
|
||||
) {
|
||||
if (BackgroundService.isRunning()) {
|
||||
// dispatch({
|
||||
// type: CHAPTER_DOWNLOADING,
|
||||
// payload: chapters[i].chapterId,
|
||||
// });
|
||||
|
||||
if (!chapters[i].downloaded) {
|
||||
await downloadChapter(
|
||||
sourceId,
|
||||
novelUrl,
|
||||
chapters[i].chapterUrl,
|
||||
chapters[i].chapterId
|
||||
);
|
||||
}
|
||||
|
||||
dispatch({
|
||||
type: CHAPTER_DOWNLOADED,
|
||||
payload: chapters[i].chapterId,
|
||||
});
|
||||
|
||||
await BackgroundService.updateNotification({
|
||||
taskTitle: chapters[i].chapterName,
|
||||
taskDesc: i + 1 + "/" + chapters.length,
|
||||
progressBar: {
|
||||
max: chapters.length,
|
||||
value: i + 1,
|
||||
},
|
||||
});
|
||||
|
||||
if (i + 1 === chapters.length) {
|
||||
resolve();
|
||||
await BackgroundService.stop();
|
||||
|
||||
Notifications.scheduleNotificationAsync({
|
||||
content: {
|
||||
title: "Downloader",
|
||||
body: "Download completed",
|
||||
},
|
||||
trigger: null,
|
||||
});
|
||||
}
|
||||
await sleep(taskData.delay);
|
||||
}
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
dispatch({
|
||||
type: CHAPTER_DOWNLOADED,
|
||||
payload: chapter.chapterId,
|
||||
});
|
||||
|
||||
if (index + 1 === chapters.length) {
|
||||
showToast(`Download completed `);
|
||||
}
|
||||
}, 1000 * index);
|
||||
});
|
||||
await BackgroundService.start(veryIntensiveTask, options);
|
||||
} catch (error) {
|
||||
showToast(error.message);
|
||||
}
|
||||
|
||||
@@ -4,6 +4,8 @@ import {
|
||||
SET_READER_SETTINGS,
|
||||
SET_APP_SETTINGS,
|
||||
SET_ACCENT_COLOR,
|
||||
SET_RIPPLE_COLOR,
|
||||
SET_AMOLED_MODE,
|
||||
} from "./settings.types";
|
||||
|
||||
export const setAppTheme = (val) => (dispatch) => {
|
||||
@@ -40,3 +42,17 @@ export const setAccentColor = (val) => (dispatch) => {
|
||||
payload: val,
|
||||
});
|
||||
};
|
||||
|
||||
export const setRippleColor = (val) => (dispatch) => {
|
||||
dispatch({
|
||||
type: SET_RIPPLE_COLOR,
|
||||
payload: val,
|
||||
});
|
||||
};
|
||||
|
||||
export const setAmoledMode = (id, val) => (dispatch) => {
|
||||
dispatch({
|
||||
type: SET_AMOLED_MODE,
|
||||
payload: { id, val },
|
||||
});
|
||||
};
|
||||
|
||||
@@ -4,23 +4,29 @@ import {
|
||||
SET_READER_SETTINGS,
|
||||
SET_APP_SETTINGS,
|
||||
SET_ACCENT_COLOR,
|
||||
SET_RIPPLE_COLOR,
|
||||
SET_AMOLED_MODE,
|
||||
} from "./settings.types";
|
||||
import {
|
||||
amoledDarkTheme,
|
||||
lightTheme,
|
||||
darkTheme,
|
||||
midnightDuskTheme,
|
||||
limeTheme,
|
||||
greenAppleTheme,
|
||||
irisBlueTheme,
|
||||
} from "../../Theme/theme";
|
||||
hotPinkTheme,
|
||||
strawberryDaiquiri,
|
||||
} from "../../theme/theme";
|
||||
|
||||
const themes = {
|
||||
0: amoledDarkTheme,
|
||||
1: lightTheme,
|
||||
2: darkTheme,
|
||||
3: midnightDuskTheme,
|
||||
4: limeTheme,
|
||||
4: greenAppleTheme,
|
||||
5: irisBlueTheme,
|
||||
6: hotPinkTheme,
|
||||
7: strawberryDaiquiri,
|
||||
};
|
||||
|
||||
/**
|
||||
@@ -56,6 +62,12 @@ const initialState = {
|
||||
showUnreadBadges: true,
|
||||
showNumberOfNovels: false,
|
||||
showScrollPercentage: true,
|
||||
swipeGestures: true,
|
||||
incognitoMode: false,
|
||||
searchAllSources: false,
|
||||
textSelectable: false,
|
||||
updateLibraryOnLaunch: false,
|
||||
downloadNewChapters: false,
|
||||
};
|
||||
|
||||
const settingsReducer = (state = initialState, action) => {
|
||||
@@ -77,6 +89,28 @@ const settingsReducer = (state = initialState, action) => {
|
||||
colorAccent: payload,
|
||||
},
|
||||
};
|
||||
case SET_RIPPLE_COLOR:
|
||||
return {
|
||||
...state,
|
||||
theme: {
|
||||
...state.theme,
|
||||
rippleColor: payload,
|
||||
},
|
||||
};
|
||||
case SET_AMOLED_MODE:
|
||||
return {
|
||||
...state,
|
||||
theme: payload.val
|
||||
? {
|
||||
...themes[0],
|
||||
colorAccent: state.theme.colorAccent,
|
||||
rippleColor: state.theme.rippleColor,
|
||||
id: state.theme.id,
|
||||
name: state.theme.name,
|
||||
}
|
||||
: themes[payload.id],
|
||||
};
|
||||
|
||||
case SET_APP_SETTINGS:
|
||||
return {
|
||||
...state,
|
||||
|
||||
@@ -3,3 +3,5 @@ export const SET_APP_THEME = "SET_APP_THEME";
|
||||
export const SET_NOVELS_PER_ROW = "SET_NOVELS_PER_ROW";
|
||||
export const SET_READER_SETTINGS = "SET_READER_SETTINGS";
|
||||
export const SET_ACCENT_COLOR = "SET_ACCENT_COLOR";
|
||||
export const SET_RIPPLE_COLOR = "SET_RIPPLE_COLOR";
|
||||
export const SET_AMOLED_MODE = "SET_AMOLED_MODE";
|
||||
|
||||
@@ -1,14 +1,25 @@
|
||||
import { GET_SOURCES, PIN_SOURCES, SEARCH_SOURCES } from "./source.types";
|
||||
import { fetchSources } from "../../Services/Source/source";
|
||||
import { showToast } from "../../Hooks/showToast";
|
||||
import {
|
||||
ENABLE_DISCOVER,
|
||||
FILTER_LANGUAGE,
|
||||
GET_SOURCES,
|
||||
PIN_SOURCES,
|
||||
SEARCH_SOURCES,
|
||||
} from "./source.types";
|
||||
import { showToast } from "../../hooks/showToast";
|
||||
import sources from "../../sources/sources.json";
|
||||
|
||||
export const getSourcesAction = () => async (dispatch) => {
|
||||
try {
|
||||
const res = await fetchSources();
|
||||
/**
|
||||
* Sort sources aplhabetically
|
||||
*/
|
||||
let sortedSources = sources.sort((a, b) =>
|
||||
a.sourceName.localeCompare(b.sourceName)
|
||||
);
|
||||
|
||||
dispatch({
|
||||
type: GET_SOURCES,
|
||||
payload: res,
|
||||
payload: sortedSources,
|
||||
});
|
||||
} catch (error) {
|
||||
showToast(error.message);
|
||||
@@ -30,3 +41,17 @@ export const pinSourceAction = (sourceId) => async (dispatch) => {
|
||||
payload: sourceId,
|
||||
});
|
||||
};
|
||||
|
||||
export const filterLanguage = (language) => async (dispatch) => {
|
||||
dispatch({
|
||||
type: FILTER_LANGUAGE,
|
||||
payload: language,
|
||||
});
|
||||
};
|
||||
|
||||
export const enableDiscover = (key) => async (dispatch) => {
|
||||
dispatch({
|
||||
type: ENABLE_DISCOVER,
|
||||
payload: key,
|
||||
});
|
||||
};
|
||||
|
||||
@@ -1,10 +1,18 @@
|
||||
import { GET_SOURCES, SEARCH_SOURCES, PIN_SOURCES } from "./source.types";
|
||||
import {
|
||||
GET_SOURCES,
|
||||
SEARCH_SOURCES,
|
||||
PIN_SOURCES,
|
||||
FILTER_LANGUAGE,
|
||||
ENABLE_DISCOVER,
|
||||
} from "./source.types";
|
||||
|
||||
const initialState = {
|
||||
sources: [],
|
||||
pinnedSources: [],
|
||||
search: [],
|
||||
pinned: [],
|
||||
filters: [],
|
||||
showMyAnimeList: true,
|
||||
loading: true,
|
||||
};
|
||||
|
||||
@@ -30,6 +38,25 @@ const sourceReducer = (state = initialState, action) => {
|
||||
source.sourceName.toLowerCase().includes(payload)
|
||||
),
|
||||
};
|
||||
|
||||
case FILTER_LANGUAGE:
|
||||
if (!state.filters) {
|
||||
state.filters = [];
|
||||
}
|
||||
return {
|
||||
...state,
|
||||
filters:
|
||||
state.filters.indexOf(payload) === -1
|
||||
? [...state.filters, payload]
|
||||
: state.filters.filter(
|
||||
(language) => language !== payload
|
||||
),
|
||||
};
|
||||
case ENABLE_DISCOVER:
|
||||
return {
|
||||
...state,
|
||||
[payload]: !state[payload],
|
||||
};
|
||||
default:
|
||||
return state;
|
||||
}
|
||||
|
||||
@@ -1,3 +1,5 @@
|
||||
export const GET_SOURCES = "GET_SOURCES";
|
||||
export const SEARCH_SOURCES = "SEARCH_SOURCES";
|
||||
export const PIN_SOURCES = "PIN_SOURCES";
|
||||
export const FILTER_LANGUAGE = "FILTER_LANGUAGE";
|
||||
export const ENABLE_DISCOVER = "ENABLE_DISCOVER";
|
||||
|
||||
@@ -11,6 +11,7 @@ import historyReducer from "./history/history.reducer";
|
||||
import updatesReducer from "./updates/updates.reducer";
|
||||
import trackerReducer from "./tracker/tracker.reducer";
|
||||
import preferenceReducer from "./preferences/preference.reducer";
|
||||
import downloadsReducer from "./downloads/downloads.reducer";
|
||||
|
||||
const persistConfig = {
|
||||
key: "root",
|
||||
@@ -29,6 +30,7 @@ const persistedReducer = persistReducer(
|
||||
updatesReducer,
|
||||
trackerReducer,
|
||||
preferenceReducer,
|
||||
downloadsReducer,
|
||||
})
|
||||
);
|
||||
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { findListItem, updateItem } from "../../Services/Trackers/myAnimeList";
|
||||
import { findListItem, updateItem } from "../../services/Trackers/myAnimeList";
|
||||
import {
|
||||
REMOVE_TRACKER,
|
||||
SET_TRACKER,
|
||||
@@ -8,7 +8,7 @@ import {
|
||||
UPDATE_CHAPTERS_READ,
|
||||
} from "./tracker.types";
|
||||
|
||||
import { updateMalChaptersRead } from "../../Services/Trackers/myAnimeList";
|
||||
import { updateMalChaptersRead } from "../../services/Trackers/myAnimeList";
|
||||
|
||||
export const setTracker = (res) => async (dispatch) => {
|
||||
dispatch({
|
||||
@@ -43,15 +43,14 @@ export const untrackNovel = (id) => async (dispatch) => {
|
||||
dispatch({ type: UNTRACK_NOVEL, payload: id });
|
||||
};
|
||||
|
||||
export const updateChaptersRead = (malId, accessToken, chaptersRead) => async (
|
||||
dispatch
|
||||
) => {
|
||||
let res = await updateMalChaptersRead(malId, accessToken, {
|
||||
num_chapters_read: chaptersRead,
|
||||
});
|
||||
export const updateChaptersRead =
|
||||
(malId, accessToken, chaptersRead) => async (dispatch) => {
|
||||
let res = await updateMalChaptersRead(malId, accessToken, {
|
||||
num_chapters_read: chaptersRead,
|
||||
});
|
||||
|
||||
dispatch({
|
||||
type: UPDATE_CHAPTERS_READ,
|
||||
payload: { malId, chaptersRead: res },
|
||||
});
|
||||
};
|
||||
dispatch({
|
||||
type: UPDATE_CHAPTERS_READ,
|
||||
payload: { malId, chaptersRead: res },
|
||||
});
|
||||
};
|
||||
|
||||
@@ -1,9 +1,11 @@
|
||||
import { GET_UPDATES } from "./updates.types";
|
||||
|
||||
import { getUpdates } from "../../Database/queries/UpdateQueries";
|
||||
import { updateAllNovels } from "../../Services/updates";
|
||||
import store from "../store";
|
||||
|
||||
import { showToast } from "../../Hooks/showToast";
|
||||
import { getUpdates } from "../../database/queries/UpdateQueries";
|
||||
import { updateAllNovels } from "../../services/updates";
|
||||
|
||||
import { showToast } from "../../hooks/showToast";
|
||||
|
||||
export const getUpdatesAction = () => async (dispatch) => {
|
||||
const updates = await getUpdates();
|
||||
@@ -33,10 +35,12 @@ export const getUpdatesAction = () => async (dispatch) => {
|
||||
dispatch({ type: GET_UPDATES, payload: groupedUpdates });
|
||||
};
|
||||
|
||||
export const updateLibraryAction = () => async (dispatch) => {
|
||||
export const updateLibraryAction = () => async (dispatch, getState) => {
|
||||
showToast("Updating library");
|
||||
|
||||
await updateAllNovels();
|
||||
const { downloadNewChapters = false } = getState().settingsReducer;
|
||||
|
||||
await updateAllNovels({ downloadNewChapters });
|
||||
|
||||
dispatch(getUpdatesAction());
|
||||
};
|
||||
|
||||
@@ -7,34 +7,43 @@ import {
|
||||
ActivityIndicator,
|
||||
RefreshControl,
|
||||
} from "react-native";
|
||||
import { useTheme } from "../../Hooks/reduxHooks";
|
||||
|
||||
import { useDispatch, useSelector } from "react-redux";
|
||||
|
||||
import {
|
||||
getSourcesAction,
|
||||
searchSourcesAction,
|
||||
} from "../../redux/source/source.actions";
|
||||
import { useTheme } from "../../hooks/reduxHooks";
|
||||
import { showToast } from "../../hooks/showToast";
|
||||
|
||||
import { Searchbar } from "../../components/Searchbar/Searchbar";
|
||||
import SourceCard from "./components/SourceCard";
|
||||
import { showToast } from "../../Hooks/showToast";
|
||||
import { Searchbar } from "../../Components/Searchbar";
|
||||
import MalCard from "./components/MalCard";
|
||||
import NovelUpdatesCard from "./components/NovelUpdatesCard";
|
||||
import EmptyView from "../../components/EmptyView";
|
||||
import DiscoverCard from "./discover/DiscoverCard";
|
||||
|
||||
const Browse = ({ navigation }) => {
|
||||
const [refreshing, setRefreshing] = useState(false);
|
||||
const [searchText, setSearchText] = useState("");
|
||||
// const [globalSearch, setGlobalSearch] = useState(false);
|
||||
// const [searchResult, setSearchResults] = useState();
|
||||
const { sources, search, loading, pinned } = useSelector(
|
||||
(state) => state.sourceReducer
|
||||
);
|
||||
|
||||
let {
|
||||
sources,
|
||||
search,
|
||||
loading,
|
||||
pinned,
|
||||
filters = [],
|
||||
showMyAnimeList = true,
|
||||
} = useSelector((state) => state.sourceReducer);
|
||||
const theme = useTheme();
|
||||
const dispatch = useDispatch();
|
||||
|
||||
const isPinned = (sourceId) =>
|
||||
pinned.indexOf(sourceId) === -1 ? false : true;
|
||||
|
||||
sources = sources.filter((source) => filters.indexOf(source.lang) === -1);
|
||||
|
||||
let pinnedSources = sources.filter((source) => isPinned(source.sourceId));
|
||||
|
||||
useEffect(() => {
|
||||
dispatch(getSourcesAction());
|
||||
}, [getSourcesAction]);
|
||||
@@ -55,16 +64,24 @@ const Browse = ({ navigation }) => {
|
||||
dispatch(searchSourcesAction(text));
|
||||
};
|
||||
|
||||
// const onSubmitEditing = () => {};
|
||||
|
||||
const renderItem = ({ item }) => (
|
||||
<SourceCard
|
||||
item={item}
|
||||
isPinned={isPinned(item.sourceId)}
|
||||
dispatch={dispatch}
|
||||
navigation={navigation}
|
||||
theme={theme}
|
||||
/>
|
||||
);
|
||||
|
||||
const SourceHeader = ({ title }) => (
|
||||
<Text
|
||||
style={[styles.sourcesHeader, { color: theme.textColorSecondary }]}
|
||||
>
|
||||
{title}
|
||||
</Text>
|
||||
);
|
||||
|
||||
return (
|
||||
<View
|
||||
style={[
|
||||
@@ -88,6 +105,10 @@ const Browse = ({ navigation }) => {
|
||||
icon: "swap-vertical-variant",
|
||||
onPress: () => navigation.navigate("Migration"),
|
||||
},
|
||||
{
|
||||
icon: "cog-outline",
|
||||
onPress: () => navigation.navigate("BrowseSettings"),
|
||||
},
|
||||
]}
|
||||
/>
|
||||
|
||||
@@ -96,62 +117,55 @@ const Browse = ({ navigation }) => {
|
||||
data={!searchText ? sources : search}
|
||||
keyExtractor={(item) => item.sourceId.toString()}
|
||||
renderItem={renderItem}
|
||||
extraData={pinned}
|
||||
extraData={pinnedSources}
|
||||
ListHeaderComponent={
|
||||
<View>
|
||||
<Text
|
||||
style={{
|
||||
color: theme.textColorSecondary,
|
||||
paddingHorizontal: 16,
|
||||
paddingVertical: 8,
|
||||
}}
|
||||
>
|
||||
Discover
|
||||
</Text>
|
||||
<NovelUpdatesCard theme={theme} />
|
||||
<MalCard theme={theme} />
|
||||
{pinned.length > 0 && (
|
||||
{showMyAnimeList && (
|
||||
<>
|
||||
<SourceHeader title="Discover" />
|
||||
{showMyAnimeList && (
|
||||
<DiscoverCard
|
||||
label="MyAnimeList"
|
||||
onPress={() =>
|
||||
navigation.navigate("BrowseMal")
|
||||
}
|
||||
icon={require("../../../assets/mal.png")}
|
||||
theme={theme}
|
||||
/>
|
||||
)}
|
||||
</>
|
||||
)}
|
||||
{pinnedSources.length > 0 && (
|
||||
<FlatList
|
||||
contentContainerStyle={{ paddingBottom: 16 }}
|
||||
data={sources.filter((source) =>
|
||||
isPinned(source.sourceId)
|
||||
)}
|
||||
data={pinnedSources}
|
||||
keyExtractor={(item) =>
|
||||
item.sourceId.toString()
|
||||
}
|
||||
renderItem={renderItem}
|
||||
extraData={pinned}
|
||||
extraData={pinnedSources}
|
||||
ListHeaderComponent={
|
||||
<Text
|
||||
style={{
|
||||
color: theme.textColorSecondary,
|
||||
paddingHorizontal: 16,
|
||||
paddingVertical: 8,
|
||||
}}
|
||||
>
|
||||
Pinned
|
||||
</Text>
|
||||
<SourceHeader title="Pinned" />
|
||||
}
|
||||
/>
|
||||
)}
|
||||
<Text
|
||||
style={{
|
||||
color: theme.textColorSecondary,
|
||||
paddingHorizontal: 16,
|
||||
paddingVertical: 8,
|
||||
}}
|
||||
>
|
||||
Sources
|
||||
</Text>
|
||||
{sources.length > 0 && <SourceHeader title="Sources" />}
|
||||
</View>
|
||||
}
|
||||
ListEmptyComponent={
|
||||
loading && (
|
||||
loading ? (
|
||||
<ActivityIndicator
|
||||
size="small"
|
||||
color={theme.colorAccent}
|
||||
style={{ marginTop: 16 }}
|
||||
/>
|
||||
) : (
|
||||
sources.length === 0 && (
|
||||
<EmptyView
|
||||
icon="(・Д・。"
|
||||
description="Enable languages from settings"
|
||||
/>
|
||||
)
|
||||
)
|
||||
}
|
||||
refreshControl={
|
||||
@@ -170,5 +184,11 @@ const Browse = ({ navigation }) => {
|
||||
export default Browse;
|
||||
|
||||
const styles = StyleSheet.create({
|
||||
container: { flex: 1 },
|
||||
container: {
|
||||
flex: 1,
|
||||
},
|
||||
sourcesHeader: {
|
||||
paddingHorizontal: 16,
|
||||
paddingVertical: 8,
|
||||
},
|
||||
});
|
||||
@@ -3,17 +3,18 @@ import { StyleSheet, View, ActivityIndicator, FlatList } from "react-native";
|
||||
|
||||
import * as WebBrowser from "expo-web-browser";
|
||||
|
||||
import NovelCover from "../../Components/NovelCover";
|
||||
import { Searchbar } from "../../Components/Searchbar";
|
||||
import ErrorView from "../../Components/ErrorView";
|
||||
import NovelCover from "../../components/NovelCover";
|
||||
import { Searchbar } from "../../components/Searchbar/Searchbar";
|
||||
import ErrorView from "../../components/ErrorView";
|
||||
|
||||
import { useTheme, useSettings } from "../../Hooks/reduxHooks";
|
||||
import { showToast } from "../../Hooks/showToast";
|
||||
import { getDeviceOrientation } from "../../Services/utils/helpers";
|
||||
import { useTheme, useSettings } from "../../hooks/reduxHooks";
|
||||
import { showToast } from "../../hooks/showToast";
|
||||
import { getDeviceOrientation } from "../../services/utils/helpers";
|
||||
import { scrapeSearchResults } from "./discover/MyAnimeListScraper";
|
||||
|
||||
const BrowseMalScreen = ({ navigation, route }) => {
|
||||
const theme = useTheme();
|
||||
const settings = useSettings();
|
||||
|
||||
const [loading, setLoading] = useState(true);
|
||||
const [novels, setNovels] = useState();
|
||||
@@ -75,10 +76,11 @@ const BrowseMalScreen = ({ navigation, route }) => {
|
||||
novelName: item.title,
|
||||
})
|
||||
}
|
||||
theme={theme}
|
||||
/>
|
||||
);
|
||||
|
||||
const { displayMode, novelsPerRow } = useSettings();
|
||||
const { displayMode, novelsPerRow } = settings;
|
||||
|
||||
const orientation = getDeviceOrientation();
|
||||
|
||||
@@ -0,0 +1,121 @@
|
||||
import React, { useState } from "react";
|
||||
import { FlatList, Pressable, Text, View } from "react-native";
|
||||
|
||||
import { Switch } from "react-native-paper";
|
||||
import { useDispatch, useSelector } from "react-redux";
|
||||
|
||||
import { Appbar } from "../../components/Appbar";
|
||||
import { ScreenContainer } from "../../components/Common";
|
||||
import { ListSubHeader } from "../../components/List";
|
||||
|
||||
import { useSettings, useTheme } from "../../hooks/reduxHooks";
|
||||
import { setAppSettings } from "../../redux/settings/settings.actions";
|
||||
import {
|
||||
enableDiscover,
|
||||
filterLanguage,
|
||||
} from "../../redux/source/source.actions";
|
||||
|
||||
const BrowseSettings = ({ navigation }) => {
|
||||
const theme = useTheme();
|
||||
const dispatch = useDispatch();
|
||||
const { filters = [], showMyAnimeList = true } = useSelector(
|
||||
(state) => state.sourceReducer
|
||||
);
|
||||
const languages = ["English", "Spanish", "Japanese", "Chinese", "Arabic"];
|
||||
const { searchAllSources = false } = useSettings();
|
||||
|
||||
const renderItem = ({ item }) => {
|
||||
return (
|
||||
<Pressable
|
||||
android_ripple={{ color: theme.rippleColor }}
|
||||
style={{
|
||||
padding: 16,
|
||||
flexDirection: "row",
|
||||
alignItems: "center",
|
||||
justifyContent: "space-between",
|
||||
}}
|
||||
onPress={() => dispatch(filterLanguage(item))}
|
||||
>
|
||||
<Text style={{ color: theme.textColorPrimary }}>{item}</Text>
|
||||
<Switch
|
||||
color={theme.colorAccent}
|
||||
value={filters.indexOf(item) === -1 ? true : false}
|
||||
onValueChange={() => dispatch(filterLanguage(item))}
|
||||
/>
|
||||
</Pressable>
|
||||
);
|
||||
};
|
||||
|
||||
return (
|
||||
<ScreenContainer theme={theme}>
|
||||
<Appbar onBackAction={navigation.goBack} title="Sources" />
|
||||
<View>
|
||||
<ListSubHeader theme={theme}>Global Search</ListSubHeader>
|
||||
<Pressable
|
||||
android_ripple={{ color: theme.rippleColor }}
|
||||
style={{
|
||||
padding: 16,
|
||||
flexDirection: "row",
|
||||
alignItems: "center",
|
||||
justifyContent: "space-between",
|
||||
}}
|
||||
onPress={() =>
|
||||
dispatch(
|
||||
setAppSettings(
|
||||
"searchAllSources",
|
||||
!searchAllSources
|
||||
)
|
||||
)
|
||||
}
|
||||
>
|
||||
<Text style={{ color: theme.textColorPrimary }}>
|
||||
Search all sources
|
||||
</Text>
|
||||
<Switch
|
||||
color={theme.colorAccent}
|
||||
value={searchAllSources}
|
||||
onValueChange={() =>
|
||||
dispatch(
|
||||
setAppSettings(
|
||||
"searchAllSources",
|
||||
!searchAllSources
|
||||
)
|
||||
)
|
||||
}
|
||||
/>
|
||||
</Pressable>
|
||||
<ListSubHeader theme={theme}>Discover</ListSubHeader>
|
||||
|
||||
<Pressable
|
||||
android_ripple={{ color: theme.rippleColor }}
|
||||
style={{
|
||||
padding: 16,
|
||||
flexDirection: "row",
|
||||
alignItems: "center",
|
||||
justifyContent: "space-between",
|
||||
}}
|
||||
onPress={() => dispatch(enableDiscover("showMyAnimeList"))}
|
||||
>
|
||||
<Text style={{ color: theme.textColorPrimary }}>
|
||||
MyAnimeList
|
||||
</Text>
|
||||
<Switch
|
||||
color={theme.colorAccent}
|
||||
value={showMyAnimeList}
|
||||
onValueChange={() =>
|
||||
dispatch(enableDiscover("showMyAnimeList"))
|
||||
}
|
||||
/>
|
||||
</Pressable>
|
||||
<ListSubHeader theme={theme}>Languages</ListSubHeader>
|
||||
<FlatList
|
||||
data={languages}
|
||||
keyExtractor={(item) => item}
|
||||
renderItem={renderItem}
|
||||
/>
|
||||
</View>
|
||||
</ScreenContainer>
|
||||
);
|
||||
};
|
||||
|
||||
export default BrowseSettings;
|
||||
@@ -1,10 +1,10 @@
|
||||
import React from "react";
|
||||
import { StyleSheet, View, FlatList, Text } from "react-native";
|
||||
import { useLibrary, useTheme } from "../../Hooks/reduxHooks";
|
||||
import { useLibrary, useTheme } from "../../hooks/reduxHooks";
|
||||
|
||||
import EmptyView from "../../Components/EmptyView";
|
||||
import { Appbar } from "../../Components/Appbar";
|
||||
import ListView from "../../Components/ListView";
|
||||
import EmptyView from "../../components/EmptyView";
|
||||
import { Appbar } from "../../components/Appbar";
|
||||
import ListView from "../../components/ListView";
|
||||
|
||||
const SourceNovels = ({ navigation, route }) => {
|
||||
const sourceId = route.params;
|
||||
@@ -53,25 +53,6 @@ const SourceNovels = ({ navigation, route }) => {
|
||||
</Text>
|
||||
}
|
||||
/>
|
||||
{/* <Portal>
|
||||
<MigrationDialog
|
||||
migrationDialog={migrationDialog}
|
||||
hideMigrationDialog={hideMigrationDIalog}
|
||||
theme={theme}
|
||||
selectedNovel={selectedNovel}
|
||||
loading={loading}
|
||||
migrationNovel={migrationNovel}
|
||||
/>
|
||||
<SourceToMigrateDialog
|
||||
selectedNovel={selectedNovel}
|
||||
sourceToMigrateDialog={sourceToMigrateDialog}
|
||||
hideSourceToMigrateDialog={hideSourceToMigrateDialog}
|
||||
sources={sources}
|
||||
theme={theme}
|
||||
showMigrationDialog={showMigrationDialog}
|
||||
selectSourceToMigrate={selectSourceToMigrate}
|
||||
/>
|
||||
</Portal> */}
|
||||
</View>
|
||||
);
|
||||
};
|
||||
@@ -0,0 +1,98 @@
|
||||
import React from "react";
|
||||
import { View, Text, Image, StyleSheet, Pressable } from "react-native";
|
||||
|
||||
import { Button, IconButton } from "react-native-paper";
|
||||
|
||||
import { pinSourceAction } from "../../../redux/source/source.actions";
|
||||
|
||||
const ExtensionCard = ({ item, theme, isPinned, dispatch, navigation }) => {
|
||||
const { sourceId, sourceName, icon, lang } = item;
|
||||
|
||||
const navigateToExtension = () =>
|
||||
navigation.navigate("Extension", {
|
||||
sourceId: item.sourceId,
|
||||
sourceName: item.sourceName,
|
||||
url: item.url,
|
||||
});
|
||||
|
||||
return (
|
||||
<Pressable
|
||||
style={styles.extensionCard}
|
||||
onPress={navigateToExtension}
|
||||
android_ripple={{ color: theme.rippleColor }}
|
||||
>
|
||||
<Image source={{ uri: icon }} style={styles.extensionIcon} />
|
||||
<View style={styles.extensionDetails}>
|
||||
<View>
|
||||
<Text
|
||||
style={{
|
||||
color: theme.textColorPrimary,
|
||||
fontSize: 14,
|
||||
}}
|
||||
>
|
||||
{sourceName}
|
||||
</Text>
|
||||
<Text
|
||||
style={{
|
||||
color: theme.textColorSecondary,
|
||||
fontSize: 12,
|
||||
}}
|
||||
>
|
||||
{lang}
|
||||
</Text>
|
||||
</View>
|
||||
<View style={{ flexDirection: "row", alignItems: "center" }}>
|
||||
<Button
|
||||
labelStyle={{ letterSpacing: 0 }}
|
||||
uppercase={false}
|
||||
color={theme.colorAccent}
|
||||
onPress={navigateToExtension}
|
||||
>
|
||||
Browse
|
||||
</Button>
|
||||
<IconButton
|
||||
icon={isPinned ? "pin" : "pin-outline"}
|
||||
animated
|
||||
size={21}
|
||||
onPress={() => dispatch(pinSourceAction(sourceId))}
|
||||
color={
|
||||
isPinned
|
||||
? theme.colorAccent
|
||||
: theme.textColorSecondary
|
||||
}
|
||||
style={{ margin: 2 }}
|
||||
/>
|
||||
</View>
|
||||
</View>
|
||||
</Pressable>
|
||||
);
|
||||
};
|
||||
|
||||
export default ExtensionCard;
|
||||
|
||||
const styles = StyleSheet.create({
|
||||
extensionCard: {
|
||||
flexDirection: "row",
|
||||
alignItems: "center",
|
||||
marginVertical: 4,
|
||||
paddingVertical: 8,
|
||||
paddingHorizontal: 20,
|
||||
},
|
||||
extensionIcon: {
|
||||
width: 40,
|
||||
height: 40,
|
||||
borderRadius: 4,
|
||||
},
|
||||
extensionDetails: {
|
||||
flex: 1,
|
||||
flexDirection: "row",
|
||||
justifyContent: "space-between",
|
||||
alignItems: "center",
|
||||
marginLeft: 16,
|
||||
},
|
||||
sourceStatus: {
|
||||
color: "#C14033",
|
||||
fontSize: 12,
|
||||
marginLeft: 5,
|
||||
},
|
||||
});
|
||||
@@ -0,0 +1,142 @@
|
||||
import React from "react";
|
||||
import { StyleSheet, Pressable, Text, View, FlatList } from "react-native";
|
||||
import FastImage from "react-native-fast-image";
|
||||
import { Row } from "../../../components/Common";
|
||||
|
||||
const GenreChip = ({ children, theme }) => (
|
||||
<View style={{ flexDirection: "row" }}>
|
||||
<Text
|
||||
style={[styles.genreChip, { color: theme.textColorSecondary }]}
|
||||
numberOfLines={2}
|
||||
>
|
||||
<Text
|
||||
style={[
|
||||
styles.genreChip,
|
||||
{
|
||||
color: theme.textColorPrimary,
|
||||
fontSize: 14,
|
||||
},
|
||||
]}
|
||||
>
|
||||
{`${"Score "}`}
|
||||
</Text>
|
||||
{children}
|
||||
</Text>
|
||||
</View>
|
||||
);
|
||||
|
||||
const BrowseMalCard = ({ novel, theme, onPress }) => {
|
||||
return (
|
||||
<View
|
||||
style={{
|
||||
flex: 1,
|
||||
backgroundColor: theme.colorPrimary,
|
||||
borderRadius: 8,
|
||||
elevation: 1,
|
||||
marginVertical: 4,
|
||||
marginHorizontal: 8,
|
||||
}}
|
||||
>
|
||||
<Pressable
|
||||
style={styles.novelUpdatesCard}
|
||||
onPress={onPress}
|
||||
android_ripple={{
|
||||
color: theme.rippleColor,
|
||||
borderless: true,
|
||||
}}
|
||||
>
|
||||
<FastImage
|
||||
source={{ uri: novel.novelCover }}
|
||||
style={{
|
||||
width: 130,
|
||||
height: 180,
|
||||
borderTopLeftRadius: 8,
|
||||
borderBottomLeftRadius: 8,
|
||||
}}
|
||||
/>
|
||||
<View
|
||||
style={{
|
||||
flex: 1,
|
||||
paddingHorizontal: 8,
|
||||
paddingVertical: 8,
|
||||
}}
|
||||
>
|
||||
<Text
|
||||
style={{ color: theme.textColorPrimary, fontSize: 17 }}
|
||||
>
|
||||
{novel.novelName}
|
||||
</Text>
|
||||
<GenreChip theme={theme}>{novel.score}</GenreChip>
|
||||
{novel.info && (
|
||||
<>
|
||||
<View style={{ flexDirection: "row" }}>
|
||||
<Text
|
||||
style={[
|
||||
styles.genreChip,
|
||||
{ color: theme.textColorSecondary },
|
||||
]}
|
||||
>
|
||||
<Text
|
||||
style={[
|
||||
styles.genreChip,
|
||||
{
|
||||
color: theme.textColorPrimary,
|
||||
fontSize: 14,
|
||||
},
|
||||
]}
|
||||
>
|
||||
{`${"Type "}`}
|
||||
</Text>
|
||||
{novel.info[1]}
|
||||
</Text>
|
||||
</View>
|
||||
<View
|
||||
style={{
|
||||
flexDirection: "row",
|
||||
marginBottom: 4,
|
||||
}}
|
||||
>
|
||||
<Text
|
||||
style={[
|
||||
styles.genreChip,
|
||||
{ color: theme.textColorSecondary },
|
||||
]}
|
||||
>
|
||||
<Text
|
||||
style={[
|
||||
styles.genreChip,
|
||||
{
|
||||
color: theme.textColorPrimary,
|
||||
fontSize: 14,
|
||||
},
|
||||
]}
|
||||
>
|
||||
{`${"Published "}`}
|
||||
</Text>
|
||||
{novel.info[2]}
|
||||
</Text>
|
||||
</View>
|
||||
</>
|
||||
)}
|
||||
</View>
|
||||
</Pressable>
|
||||
</View>
|
||||
);
|
||||
};
|
||||
|
||||
export default BrowseMalCard;
|
||||
|
||||
const styles = StyleSheet.create({
|
||||
novelUpdatesCard: {
|
||||
flex: 1,
|
||||
flexDirection: "row",
|
||||
},
|
||||
contentContainer: {
|
||||
flex: 1,
|
||||
},
|
||||
genreChip: {
|
||||
marginTop: 4,
|
||||
fontSize: 13,
|
||||
// fontStyle: "italic",
|
||||
},
|
||||
});
|
||||
@@ -0,0 +1,58 @@
|
||||
import React from "react";
|
||||
import { View, Text, Image, StyleSheet, Pressable } from "react-native";
|
||||
|
||||
import { Button } from "react-native-paper";
|
||||
|
||||
const DiscoverCard = ({ icon, label, onPress, theme }) => {
|
||||
return (
|
||||
<Pressable
|
||||
style={styles.discoverCard}
|
||||
onPress={onPress}
|
||||
android_ripple={{ color: theme.rippleColor }}
|
||||
>
|
||||
<Image source={icon} style={styles.discoverIcon} />
|
||||
<View style={styles.label}>
|
||||
<Text
|
||||
style={{
|
||||
color: theme.textColorPrimary,
|
||||
fontSize: 14,
|
||||
}}
|
||||
>
|
||||
{label}
|
||||
</Text>
|
||||
<Button
|
||||
labelStyle={{ letterSpacing: 0 }}
|
||||
uppercase={false}
|
||||
color={theme.colorAccent}
|
||||
onPress={onPress}
|
||||
>
|
||||
Browse
|
||||
</Button>
|
||||
</View>
|
||||
</Pressable>
|
||||
);
|
||||
};
|
||||
|
||||
export default DiscoverCard;
|
||||
|
||||
const styles = StyleSheet.create({
|
||||
discoverCard: {
|
||||
flexDirection: "row",
|
||||
alignItems: "center",
|
||||
marginVertical: 4,
|
||||
paddingVertical: 8,
|
||||
paddingHorizontal: 20,
|
||||
},
|
||||
discoverIcon: {
|
||||
width: 40,
|
||||
height: 40,
|
||||
borderRadius: 4,
|
||||
},
|
||||
label: {
|
||||
flex: 1,
|
||||
flexDirection: "row",
|
||||
justifyContent: "space-between",
|
||||
alignItems: "center",
|
||||
marginLeft: 16,
|
||||
},
|
||||
});
|
||||
+11
-19
@@ -1,22 +1,16 @@
|
||||
import React, { useEffect, useState } from "react";
|
||||
import {
|
||||
StyleSheet,
|
||||
View,
|
||||
ActivityIndicator,
|
||||
FlatList,
|
||||
Button,
|
||||
} from "react-native";
|
||||
import { StyleSheet, View, ActivityIndicator, FlatList } from "react-native";
|
||||
|
||||
import * as WebBrowser from "expo-web-browser";
|
||||
|
||||
import NovelCover from "../../../Components/NovelCover";
|
||||
import { Searchbar } from "../../../Components/Searchbar";
|
||||
import ErrorView from "../../../Components/ErrorView";
|
||||
import { Searchbar } from "../../../components/Searchbar/Searchbar";
|
||||
import ErrorView from "../../../components/ErrorView";
|
||||
|
||||
import { useTheme, useSettings } from "../../../Hooks/reduxHooks";
|
||||
import { showToast } from "../../../Hooks/showToast";
|
||||
import { getDeviceOrientation } from "../../../Services/utils/helpers";
|
||||
import { useTheme, useSettings } from "../../../hooks/reduxHooks";
|
||||
import { showToast } from "../../../hooks/showToast";
|
||||
import { getDeviceOrientation } from "../../../services/utils/helpers";
|
||||
import { scrapeSearchResults, scrapeTopNovels } from "./MyAnimeListScraper";
|
||||
import BrowseMalCard from "./BrowseMalCard";
|
||||
|
||||
const BrowseMalScreen = ({ navigation, route }) => {
|
||||
const theme = useTheme();
|
||||
@@ -28,7 +22,6 @@ const BrowseMalScreen = ({ navigation, route }) => {
|
||||
|
||||
const [searchText, setSearchText] = useState("");
|
||||
|
||||
const baseUrl = "https://api.jikan.moe/v3/";
|
||||
const malUrl = "https://myanimelist.net/topmanga.php?type=lightnovels";
|
||||
|
||||
const getNovels = async (limit) => {
|
||||
@@ -72,8 +65,9 @@ const BrowseMalScreen = ({ navigation, route }) => {
|
||||
}, []);
|
||||
|
||||
const renderItem = ({ item }) => (
|
||||
<NovelCover
|
||||
item={{ novelName: item.novelName, novelCover: item.novelCover }}
|
||||
<BrowseMalCard
|
||||
novel={item}
|
||||
theme={theme}
|
||||
onPress={() =>
|
||||
navigation.navigate("GlobalSearch", {
|
||||
novelName: item.novelName,
|
||||
@@ -153,8 +147,6 @@ const BrowseMalScreen = ({ navigation, route }) => {
|
||||
) : (
|
||||
<FlatList
|
||||
contentContainerStyle={styles.novelsContainer}
|
||||
numColumns={getNovelsPerRow()}
|
||||
key={getNovelsPerRow()}
|
||||
data={novels}
|
||||
keyExtractor={(item) => item.novelName}
|
||||
renderItem={renderItem}
|
||||
@@ -189,7 +181,7 @@ const styles = StyleSheet.create({
|
||||
},
|
||||
novelsContainer: {
|
||||
flexGrow: 1,
|
||||
paddingVertical: 8,
|
||||
paddingBottom: 8,
|
||||
paddingHorizontal: 4,
|
||||
},
|
||||
});
|
||||
+9
-2
@@ -22,7 +22,12 @@ const scrapeTopNovels = async (pageNo) => {
|
||||
".jpg";
|
||||
const novelName = $(this).find("h3").text();
|
||||
|
||||
const novel = { novelName, novelCover };
|
||||
const score = $(this)
|
||||
.find(".js-top-ranking-score-col > span.score-label")
|
||||
.text();
|
||||
|
||||
const info = $(this).find("div.information").text().split(/\s\s+/);
|
||||
const novel = { novelName, novelCover, score, info };
|
||||
|
||||
novels.push(novel);
|
||||
});
|
||||
@@ -58,7 +63,9 @@ const scrapeSearchResults = async (searchTerm) => {
|
||||
novelId[8].split(".")[0] +
|
||||
".jpg";
|
||||
|
||||
const novel = { novelName, novelCover };
|
||||
const score = $(this).find("td:nth-child(5)").text().trim();
|
||||
|
||||
const novel = { novelName, novelCover, score };
|
||||
|
||||
novels.push(novel);
|
||||
}
|
||||
+63
-63
@@ -4,14 +4,15 @@ import { StyleSheet, View, FlatList, ActivityIndicator } from "react-native";
|
||||
import { ProgressBar } from "react-native-paper";
|
||||
import { useSelector } from "react-redux";
|
||||
|
||||
import { Searchbar } from "../../../Components/Searchbar";
|
||||
import EmptyView from "../../../Components/EmptyView";
|
||||
import { Searchbar } from "../../../components/Searchbar/Searchbar";
|
||||
import EmptyView from "../../../components/EmptyView";
|
||||
|
||||
import { ScreenContainer } from "../../../Components/Common";
|
||||
import { ScreenContainer } from "../../../components/Common";
|
||||
import GlobalSearchSourceItem from "./GlobalSearchSourceItem";
|
||||
|
||||
import { showToast } from "../../../Hooks/showToast";
|
||||
import { useLibrary, useTheme } from "../../../Hooks/reduxHooks";
|
||||
import { showToast } from "../../../hooks/showToast";
|
||||
import { useLibrary, useSettings, useTheme } from "../../../hooks/reduxHooks";
|
||||
import { getSource } from "../../../sources/sources";
|
||||
|
||||
const GlobalSearch = ({ route, navigation }) => {
|
||||
const theme = useTheme();
|
||||
@@ -21,10 +22,17 @@ const GlobalSearch = ({ route, navigation }) => {
|
||||
novelName = route.params.novelName;
|
||||
}
|
||||
|
||||
const { sources, pinned } = useSelector((state) => state.sourceReducer);
|
||||
let {
|
||||
sources,
|
||||
pinned,
|
||||
filters = [],
|
||||
} = useSelector((state) => state.sourceReducer);
|
||||
sources = sources.filter((source) => filters.indexOf(source.lang) === -1);
|
||||
|
||||
const pinnedSources = sources.filter(
|
||||
(source) => pinned.indexOf(source.sourceId) !== -1
|
||||
);
|
||||
const { searchAllSources = false } = useSettings();
|
||||
|
||||
const [loading, setLoading] = useState(false);
|
||||
const [searchText, setSearchText] = useState(novelName);
|
||||
@@ -34,60 +42,57 @@ const GlobalSearch = ({ route, navigation }) => {
|
||||
const library = useLibrary();
|
||||
|
||||
useEffect(() => {
|
||||
let mounted = true;
|
||||
novelName && onSubmitEditing();
|
||||
|
||||
return () => (mounted = false);
|
||||
}, []);
|
||||
|
||||
const clearSearchbar = () => setSearchText("");
|
||||
const onChangeText = (text) => setSearchText(text);
|
||||
|
||||
const onSubmitEditing = () => {
|
||||
const onSubmitEditing = async () => {
|
||||
setSearchResults([]);
|
||||
setProgress(0);
|
||||
|
||||
const getSearchUrl = (sourceId) => {
|
||||
if (sourceId === 1) {
|
||||
return `https://lnreader-extensions.vercel.app/api/1/search/?s=${searchText}&?o=rating`;
|
||||
} else {
|
||||
return `https://lnreader-extensions.vercel.app/api/${sourceId}/search/?s=${searchText}`;
|
||||
}
|
||||
};
|
||||
let globalSearchSources = searchAllSources ? sources : pinnedSources;
|
||||
|
||||
pinnedSources.map((source, index) =>
|
||||
setTimeout(() => {
|
||||
for (let i = 0; i < globalSearchSources.length; i++) {
|
||||
try {
|
||||
setLoading(true);
|
||||
fetch(getSearchUrl(source.sourceId))
|
||||
.then((response) => response.json())
|
||||
.then((json) => {
|
||||
setSearchResults((searchResults) => [
|
||||
...searchResults,
|
||||
{
|
||||
sourceId: source.sourceId,
|
||||
sourceName: source.sourceName,
|
||||
sourceLanguage: source.sourceLanguage,
|
||||
novels: json,
|
||||
},
|
||||
]);
|
||||
setLoading(false);
|
||||
})
|
||||
.catch((error) => {
|
||||
showToast(error.message);
|
||||
setSearchResults((searchResults) => [
|
||||
...searchResults,
|
||||
{
|
||||
sourceId: source.sourceId,
|
||||
sourceName: source.sourceName,
|
||||
sourceLanguage: source.sourceLanguage,
|
||||
novels: [],
|
||||
},
|
||||
]);
|
||||
setLoading(false);
|
||||
})
|
||||
.finally(() =>
|
||||
setProgress(
|
||||
(progress) => progress + 1 / pinnedSources.length
|
||||
)
|
||||
);
|
||||
}, 1000 * index)
|
||||
);
|
||||
|
||||
const source = getSource(globalSearchSources[i].sourceId);
|
||||
const data = await source.searchNovels(encodeURI(searchText));
|
||||
|
||||
setSearchResults((searchResults) => [
|
||||
...searchResults,
|
||||
{
|
||||
sourceId: globalSearchSources[i].sourceId,
|
||||
sourceName: globalSearchSources[i].sourceName,
|
||||
lang: globalSearchSources[i].lang,
|
||||
novels: data,
|
||||
},
|
||||
]);
|
||||
setLoading(false);
|
||||
setProgress(
|
||||
(progress) => progress + 1 / globalSearchSources.length
|
||||
);
|
||||
} catch (error) {
|
||||
showToast(
|
||||
globalSearchSources[i].sourceName + ": " + error.message
|
||||
);
|
||||
setSearchResults((searchResults) => [
|
||||
...searchResults,
|
||||
{
|
||||
sourceId: globalSearchSources[i].sourceId,
|
||||
sourceName: globalSearchSources[i].sourceName,
|
||||
lang: globalSearchSources[i].lang,
|
||||
novels: [],
|
||||
},
|
||||
]);
|
||||
setLoading(false);
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
const renderItem = ({ item }) => (
|
||||
@@ -111,15 +116,8 @@ const GlobalSearch = ({ route, navigation }) => {
|
||||
onSubmitEditing={onSubmitEditing}
|
||||
clearSearchbar={clearSearchbar}
|
||||
/>
|
||||
{progress < 1 && progress > 0 && pinned && (
|
||||
<ProgressBar
|
||||
color={
|
||||
progress > 0.9
|
||||
? theme.colorPrimaryDark
|
||||
: theme.colorAccent
|
||||
}
|
||||
progress={progress}
|
||||
/>
|
||||
{progress > 0 && (
|
||||
<ProgressBar color={theme.colorAccent} progress={progress} />
|
||||
)}
|
||||
<FlatList
|
||||
contentContainerStyle={{
|
||||
@@ -135,10 +133,12 @@ const GlobalSearch = ({ route, navigation }) => {
|
||||
{!loading && (
|
||||
<EmptyView
|
||||
icon="__φ(..)"
|
||||
description={`Search a novel in your pinned sources ${
|
||||
pinned.length === 0
|
||||
? "(No sources pinned)"
|
||||
: ""
|
||||
description={`Search a novel in ${
|
||||
searchAllSources
|
||||
? "all sources"
|
||||
: pinned.length === 0
|
||||
? "pinned sources\n(No sources pinned)"
|
||||
: "pinned sources"
|
||||
}`}
|
||||
/>
|
||||
)}
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user