Add ESLint and Prettier

This commit is contained in:
Rajarshee Chatterjee
2021-09-30 08:06:43 +05:30
parent e40af59230
commit 49160b7d98
218 changed files with 45076 additions and 42796 deletions
+3
View File
@@ -0,0 +1,3 @@
# Windows files
[*.bat]
end_of_line = crlf
+7
View File
@@ -0,0 +1,7 @@
module.exports = {
root: true,
extends: '@react-native-community',
rules: {
'react-native/no-inline-styles': 'off',
},
};
+3
View File
@@ -0,0 +1,3 @@
# Windows files should use crlf line endings
# https://help.github.com/articles/dealing-with-line-endings/
*.bat text eol=crlf
+45 -45
View File
@@ -1,45 +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
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
+1
View File
@@ -0,0 +1 @@
**/node_modules
+6
View File
@@ -0,0 +1,6 @@
module.exports = {
bracketSpacing: false,
singleQuote: true,
trailingComma: 'all',
arrowParens: 'avoid',
};
+29 -32
View File
@@ -1,43 +1,40 @@
import React, { useEffect } from "react";
import { StatusBar } from "react-native";
import React, {useEffect} from 'react';
import {StatusBar} from 'react-native';
import { useFonts } from "expo-font";
import { Provider as PaperProvider } from "react-native-paper";
import { Provider } from "react-redux";
import { persistor, store } from "./src/redux/store";
import { PersistGate } from "redux-persist/lib/integration/react";
import * as SplashScreen from "expo-splash-screen";
import {useFonts} from 'expo-font';
import {Provider as PaperProvider} from 'react-native-paper';
import {Provider} from 'react-redux';
import {persistor, store} from './src/redux/store';
import {PersistGate} from 'redux-persist/lib/integration/react';
import * as SplashScreen from 'expo-splash-screen';
import Main from "./src/navigators/Main";
import Main from './src/navigators/Main';
import { createDB } 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);
const [loaded] = useFonts(fonts);
useEffect(() => {
SplashScreen.preventAutoHideAsync();
createDB();
}, []);
useEffect(() => {
SplashScreen.preventAutoHideAsync();
createDB();
}, []);
if (!loaded) {
return null;
}
if (!loaded) {
return null;
}
return (
<Provider store={store}>
<PersistGate loading={null} persistor={persistor}>
<PaperProvider>
<StatusBar
translucent={true}
backgroundColor="transparent"
/>
<Main />
</PaperProvider>
</PersistGate>
</Provider>
);
return (
<Provider store={store}>
<PersistGate loading={null} persistor={persistor}>
<PaperProvider>
<StatusBar translucent={true} backgroundColor="transparent" />
<Main />
</PaperProvider>
</PersistGate>
</Provider>
);
};
export default App;
+41 -41
View File
@@ -1,41 +1,41 @@
<p align="center">
<img src="./.github/readme-images/icon_alt.png" align="center" width="128" />
<p>
<h1 align="center">
LNReader
</h1>
<p align="center">
LNReader is a Tachiyomi-like, free and open source light novel reader for Android.
</p>
<div align="center">
<a href="https://github.com/LNReader/lnreader/actions/workflows/build.yml">
<img alt="GitHub Workflow Status" src="https://img.shields.io/github/workflow/status/LNReader/lnreader/CI">
</a>
<a href="https://discord.gg/QdcWN4MD63">
<img alt="Discord Chat" src="https://img.shields.io/discord/835746409357246465.svg?logo=discord&logoColor=white&logoWidth=20&labelColor=7289DA&label=Discord&color=17cf48">
</a>
<a href="https://github.com/LNReader/lnreader/releases/latest">
<img alt="GitHub release (latest by date)" src="https://img.shields.io/github/v/release/LNReader/lnreader">
</a>
<a href="https://github.com/LNReader/lnreader/blob/main/LICENSE">
<img alt="GitHub" src="https://img.shields.io/github/license/LNReader/lnreader">
</a>
</div>
<br>
<br>
<p align="center">
<img src="./.github/readme-images/screenshots.png" align="center" />
</p>
## Download
Get the app from our [releases page](https://github.com/rajarsheechatterjee/LNReader/releases).
## Sources
LNReader does not have any affiliation with the content providers available.
Source requests should be created at [LNReader/lnreader-sources](https://github.com/LNReader/lnreader-sources).
## License
[MIT ](https://github.com/rajarsheechatterjee/lnreader/blob/main/LICENSE)
<p align="center">
<img src="./.github/readme-images/icon_alt.png" align="center" width="128" />
<p>
<h1 align="center">
LNReader
</h1>
<p align="center">
LNReader is a Tachiyomi-like, free and open source light novel reader for Android.
</p>
<div align="center">
<a href="https://github.com/LNReader/lnreader/actions/workflows/build.yml">
<img alt="GitHub Workflow Status" src="https://img.shields.io/github/workflow/status/LNReader/lnreader/CI">
</a>
<a href="https://discord.gg/QdcWN4MD63">
<img alt="Discord Chat" src="https://img.shields.io/discord/835746409357246465.svg?logo=discord&logoColor=white&logoWidth=20&labelColor=7289DA&label=Discord&color=17cf48">
</a>
<a href="https://github.com/LNReader/lnreader/releases/latest">
<img alt="GitHub release (latest by date)" src="https://img.shields.io/github/v/release/LNReader/lnreader">
</a>
<a href="https://github.com/LNReader/lnreader/blob/main/LICENSE">
<img alt="GitHub" src="https://img.shields.io/github/license/LNReader/lnreader">
</a>
</div>
<br>
<br>
<p align="center">
<img src="./.github/readme-images/screenshots.png" align="center" />
</p>
## Download
Get the app from our [releases page](https://github.com/rajarsheechatterjee/LNReader/releases).
## Sources
LNReader does not have any affiliation with the content providers available.
Source requests should be created at [LNReader/lnreader-sources](https://github.com/LNReader/lnreader-sources).
## License
[MIT ](https://github.com/rajarsheechatterjee/lnreader/blob/main/LICENSE)
+219 -219
View File
@@ -1,219 +1,219 @@
apply plugin: "com.android.application"
import com.android.build.OutputFile
/**
* The react.gradle file registers a task for each build variant (e.g. bundleDebugJsAndAssets
* and bundleReleaseJsAndAssets).
* These basically call `react-native bundle` with the correct arguments during the Android build
* cycle. By default, bundleDebugJsAndAssets is skipped, as in debug/dev mode we prefer to load the
* bundle directly from the development server. Below you can see all the possible configurations
* and their defaults. If you decide to add a configuration block, make sure to add it before the
* `apply from: "../../node_modules/react-native/react.gradle"` line.
*
* project.ext.react = [
* // the name of the generated asset file containing your JS bundle
* bundleAssetName: "index.android.bundle",
*
* // the entry file for bundle generation. If none specified and
* // "index.android.js" exists, it will be used. Otherwise "index.js" is
* // default. Can be overridden with ENTRY_FILE environment variable.
* entryFile: "index.android.js",
*
* // https://reactnative.dev/docs/performance#enable-the-ram-format
* bundleCommand: "ram-bundle",
*
* // whether to bundle JS and assets in debug mode
* bundleInDebug: false,
*
* // whether to bundle JS and assets in release mode
* bundleInRelease: true,
*
* // whether to bundle JS and assets in another build variant (if configured).
* // See http://tools.android.com/tech-docs/new-build-system/user-guide#TOC-Build-Variants
* // The configuration property can be in the following formats
* // 'bundleIn${productFlavor}${buildType}'
* // 'bundleIn${buildType}'
* // bundleInFreeDebug: true,
* // bundleInPaidRelease: true,
* // bundleInBeta: true,
*
* // whether to disable dev mode in custom build variants (by default only disabled in release)
* // for example: to disable dev mode in the staging build type (if configured)
* devDisabledInStaging: true,
* // The configuration property can be in the following formats
* // 'devDisabledIn${productFlavor}${buildType}'
* // 'devDisabledIn${buildType}'
*
* // the root of your project, i.e. where "package.json" lives
* root: "../../",
*
* // where to put the JS bundle asset in debug mode
* jsBundleDirDebug: "$buildDir/intermediates/assets/debug",
*
* // where to put the JS bundle asset in release mode
* jsBundleDirRelease: "$buildDir/intermediates/assets/release",
*
* // where to put drawable resources / React Native assets, e.g. the ones you use via
* // require('./image.png')), in debug mode
* resourcesDirDebug: "$buildDir/intermediates/res/merged/debug",
*
* // where to put drawable resources / React Native assets, e.g. the ones you use via
* // require('./image.png')), in release mode
* resourcesDirRelease: "$buildDir/intermediates/res/merged/release",
*
* // by default the gradle tasks are skipped if none of the JS files or assets change; this means
* // that we don't look at files in android/ or ios/ to determine whether the tasks are up to
* // date; if you have any other folders that you want to ignore for performance reasons (gradle
* // indexes the entire tree), add them here. Alternatively, if you have JS files in android/
* // for example, you might want to remove it from here.
* inputExcludes: ["android/**", "ios/**"],
*
* // override which node gets called and with what additional arguments
* nodeExecutableAndArgs: ["node"],
*
* // supply additional arguments to the packager
* extraPackagerArgs: []
* ]
*/
project.ext.react = [
enableHermes: true
]
apply from: '../../node_modules/react-native-unimodules/gradle.groovy'
apply from: "../../node_modules/react-native/react.gradle"
apply from: "../../node_modules/expo-constants/scripts/get-app-config-android.gradle"
apply from: "../../node_modules/expo-updates/scripts/create-manifest-android.gradle"
/**
* Set this to true to create two separate APKs instead of one:
* - An APK that only works on ARM devices
* - An APK that only works on x86 devices
* The advantage is the size of the APK is reduced by about 4MB.
* Upload all the APKs to the Play Store and people will download
* the correct one based on the CPU architecture of their device.
*/
def enableSeparateBuildPerCPUArchitecture = false
/**
* Run Proguard to shrink the Java bytecode in release builds.
*/
def enableProguardInReleaseBuilds = false
/**
* The preferred build flavor of JavaScriptCore.
*
* For example, to use the international variant, you can use:
* `def jscFlavor = 'org.webkit:android-jsc-intl:+'`
*
* The international variant includes ICU i18n library and necessary data
* allowing to use e.g. `Date.toLocaleString` and `String.localeCompare` that
* give correct results when using with locales other than en-US. Note that
* this variant is about 6MiB larger per architecture than default.
*/
def jscFlavor = 'org.webkit:android-jsc:+'
/**
* Whether to enable the Hermes VM.
*
* This should be set on project.ext.react and mirrored here. If it is not set
* on project.ext.react, JavaScript will not be compiled to Hermes Bytecode
* and the benefits of using Hermes will therefore be sharply reduced.
*/
def enableHermes = project.ext.react.get("enableHermes", false);
android {
compileSdkVersion rootProject.ext.compileSdkVersion
compileOptions {
sourceCompatibility JavaVersion.VERSION_1_8
targetCompatibility JavaVersion.VERSION_1_8
}
defaultConfig {
applicationId 'com.rajarsheechatterjee.LNReader'
minSdkVersion rootProject.ext.minSdkVersion
targetSdkVersion rootProject.ext.targetSdkVersion
versionCode 1
versionName "1.1.5"
}
splits {
abi {
reset()
enable enableSeparateBuildPerCPUArchitecture
universalApk false // If true, also generate a universal APK
include "armeabi-v7a", "x86", "arm64-v8a", "x86_64"
}
}
signingConfigs {
debug {
storeFile file('debug.keystore')
storePassword 'android'
keyAlias 'androiddebugkey'
keyPassword 'android'
}
}
buildTypes {
debug {
signingConfig signingConfigs.debug
}
release {
// Caution! In production, you need to generate your own keystore file.
// see https://reactnative.dev/docs/signed-apk-android.
signingConfig signingConfigs.debug
minifyEnabled enableProguardInReleaseBuilds
proguardFiles getDefaultProguardFile("proguard-android.txt"), "proguard-rules.pro"
}
}
// applicationVariants are e.g. debug, release
applicationVariants.all { variant ->
variant.outputs.each { output ->
// For each separate APK per architecture, set a unique version code as described here:
// https://developer.android.com/studio/build/configure-apk-splits.html
def versionCodes = ["armeabi-v7a": 1, "x86": 2, "arm64-v8a": 3, "x86_64": 4]
def abi = output.getFilter(OutputFile.ABI)
if (abi != null) { // null for the universal-debug, universal-release variants
output.versionCodeOverride =
versionCodes.get(abi) * 1048576 + defaultConfig.versionCode
}
}
}
}
dependencies {
implementation fileTree(dir: "libs", include: ["*.jar"])
//noinspection GradleDynamicVersion
implementation "com.facebook.react:react-native:+" // From node_modules
implementation "androidx.swiperefreshlayout:swiperefreshlayout:1.0.0"
debugImplementation("com.facebook.flipper:flipper:${FLIPPER_VERSION}") {
exclude group:'com.facebook.fbjni'
}
debugImplementation("com.facebook.flipper:flipper-network-plugin:${FLIPPER_VERSION}") {
exclude group:'com.facebook.flipper'
exclude group:'com.squareup.okhttp3', module:'okhttp'
}
debugImplementation("com.facebook.flipper:flipper-fresco-plugin:${FLIPPER_VERSION}") {
exclude group:'com.facebook.flipper'
}
addUnimodulesDependencies()
if (enableHermes) {
def hermesPath = "../../node_modules/hermes-engine/android/";
debugImplementation files(hermesPath + "hermes-debug.aar")
releaseImplementation files(hermesPath + "hermes-release.aar")
} else {
implementation jscFlavor
}
}
// Run this once to be able to run the application with BUCK
// puts all compile dependencies into folder libs for BUCK to use
task copyDownloadableDepsToLibs(type: Copy) {
from configurations.compile
into 'libs'
}
apply from: file("../../node_modules/@react-native-community/cli-platform-android/native_modules.gradle"); applyNativeModulesAppBuildGradle(project)
apply plugin: "com.android.application"
import com.android.build.OutputFile
/**
* The react.gradle file registers a task for each build variant (e.g. bundleDebugJsAndAssets
* and bundleReleaseJsAndAssets).
* These basically call `react-native bundle` with the correct arguments during the Android build
* cycle. By default, bundleDebugJsAndAssets is skipped, as in debug/dev mode we prefer to load the
* bundle directly from the development server. Below you can see all the possible configurations
* and their defaults. If you decide to add a configuration block, make sure to add it before the
* `apply from: "../../node_modules/react-native/react.gradle"` line.
*
* project.ext.react = [
* // the name of the generated asset file containing your JS bundle
* bundleAssetName: "index.android.bundle",
*
* // the entry file for bundle generation. If none specified and
* // "index.android.js" exists, it will be used. Otherwise "index.js" is
* // default. Can be overridden with ENTRY_FILE environment variable.
* entryFile: "index.android.js",
*
* // https://reactnative.dev/docs/performance#enable-the-ram-format
* bundleCommand: "ram-bundle",
*
* // whether to bundle JS and assets in debug mode
* bundleInDebug: false,
*
* // whether to bundle JS and assets in release mode
* bundleInRelease: true,
*
* // whether to bundle JS and assets in another build variant (if configured).
* // See http://tools.android.com/tech-docs/new-build-system/user-guide#TOC-Build-Variants
* // The configuration property can be in the following formats
* // 'bundleIn${productFlavor}${buildType}'
* // 'bundleIn${buildType}'
* // bundleInFreeDebug: true,
* // bundleInPaidRelease: true,
* // bundleInBeta: true,
*
* // whether to disable dev mode in custom build variants (by default only disabled in release)
* // for example: to disable dev mode in the staging build type (if configured)
* devDisabledInStaging: true,
* // The configuration property can be in the following formats
* // 'devDisabledIn${productFlavor}${buildType}'
* // 'devDisabledIn${buildType}'
*
* // the root of your project, i.e. where "package.json" lives
* root: "../../",
*
* // where to put the JS bundle asset in debug mode
* jsBundleDirDebug: "$buildDir/intermediates/assets/debug",
*
* // where to put the JS bundle asset in release mode
* jsBundleDirRelease: "$buildDir/intermediates/assets/release",
*
* // where to put drawable resources / React Native assets, e.g. the ones you use via
* // require('./image.png')), in debug mode
* resourcesDirDebug: "$buildDir/intermediates/res/merged/debug",
*
* // where to put drawable resources / React Native assets, e.g. the ones you use via
* // require('./image.png')), in release mode
* resourcesDirRelease: "$buildDir/intermediates/res/merged/release",
*
* // by default the gradle tasks are skipped if none of the JS files or assets change; this means
* // that we don't look at files in android/ or ios/ to determine whether the tasks are up to
* // date; if you have any other folders that you want to ignore for performance reasons (gradle
* // indexes the entire tree), add them here. Alternatively, if you have JS files in android/
* // for example, you might want to remove it from here.
* inputExcludes: ["android/**", "ios/**"],
*
* // override which node gets called and with what additional arguments
* nodeExecutableAndArgs: ["node"],
*
* // supply additional arguments to the packager
* extraPackagerArgs: []
* ]
*/
project.ext.react = [
enableHermes: true
]
apply from: '../../node_modules/react-native-unimodules/gradle.groovy'
apply from: "../../node_modules/react-native/react.gradle"
apply from: "../../node_modules/expo-constants/scripts/get-app-config-android.gradle"
apply from: "../../node_modules/expo-updates/scripts/create-manifest-android.gradle"
/**
* Set this to true to create two separate APKs instead of one:
* - An APK that only works on ARM devices
* - An APK that only works on x86 devices
* The advantage is the size of the APK is reduced by about 4MB.
* Upload all the APKs to the Play Store and people will download
* the correct one based on the CPU architecture of their device.
*/
def enableSeparateBuildPerCPUArchitecture = false
/**
* Run Proguard to shrink the Java bytecode in release builds.
*/
def enableProguardInReleaseBuilds = false
/**
* The preferred build flavor of JavaScriptCore.
*
* For example, to use the international variant, you can use:
* `def jscFlavor = 'org.webkit:android-jsc-intl:+'`
*
* The international variant includes ICU i18n library and necessary data
* allowing to use e.g. `Date.toLocaleString` and `String.localeCompare` that
* give correct results when using with locales other than en-US. Note that
* this variant is about 6MiB larger per architecture than default.
*/
def jscFlavor = 'org.webkit:android-jsc:+'
/**
* Whether to enable the Hermes VM.
*
* This should be set on project.ext.react and mirrored here. If it is not set
* on project.ext.react, JavaScript will not be compiled to Hermes Bytecode
* and the benefits of using Hermes will therefore be sharply reduced.
*/
def enableHermes = project.ext.react.get("enableHermes", false);
android {
compileSdkVersion rootProject.ext.compileSdkVersion
compileOptions {
sourceCompatibility JavaVersion.VERSION_1_8
targetCompatibility JavaVersion.VERSION_1_8
}
defaultConfig {
applicationId 'com.rajarsheechatterjee.LNReader'
minSdkVersion rootProject.ext.minSdkVersion
targetSdkVersion rootProject.ext.targetSdkVersion
versionCode 1
versionName "1.1.5"
}
splits {
abi {
reset()
enable enableSeparateBuildPerCPUArchitecture
universalApk false // If true, also generate a universal APK
include "armeabi-v7a", "x86", "arm64-v8a", "x86_64"
}
}
signingConfigs {
debug {
storeFile file('debug.keystore')
storePassword 'android'
keyAlias 'androiddebugkey'
keyPassword 'android'
}
}
buildTypes {
debug {
signingConfig signingConfigs.debug
}
release {
// Caution! In production, you need to generate your own keystore file.
// see https://reactnative.dev/docs/signed-apk-android.
signingConfig signingConfigs.debug
minifyEnabled enableProguardInReleaseBuilds
proguardFiles getDefaultProguardFile("proguard-android.txt"), "proguard-rules.pro"
}
}
// applicationVariants are e.g. debug, release
applicationVariants.all { variant ->
variant.outputs.each { output ->
// For each separate APK per architecture, set a unique version code as described here:
// https://developer.android.com/studio/build/configure-apk-splits.html
def versionCodes = ["armeabi-v7a": 1, "x86": 2, "arm64-v8a": 3, "x86_64": 4]
def abi = output.getFilter(OutputFile.ABI)
if (abi != null) { // null for the universal-debug, universal-release variants
output.versionCodeOverride =
versionCodes.get(abi) * 1048576 + defaultConfig.versionCode
}
}
}
}
dependencies {
implementation fileTree(dir: "libs", include: ["*.jar"])
//noinspection GradleDynamicVersion
implementation "com.facebook.react:react-native:+" // From node_modules
implementation "androidx.swiperefreshlayout:swiperefreshlayout:1.0.0"
debugImplementation("com.facebook.flipper:flipper:${FLIPPER_VERSION}") {
exclude group:'com.facebook.fbjni'
}
debugImplementation("com.facebook.flipper:flipper-network-plugin:${FLIPPER_VERSION}") {
exclude group:'com.facebook.flipper'
exclude group:'com.squareup.okhttp3', module:'okhttp'
}
debugImplementation("com.facebook.flipper:flipper-fresco-plugin:${FLIPPER_VERSION}") {
exclude group:'com.facebook.flipper'
}
addUnimodulesDependencies()
if (enableHermes) {
def hermesPath = "../../node_modules/hermes-engine/android/";
debugImplementation files(hermesPath + "hermes-debug.aar")
releaseImplementation files(hermesPath + "hermes-release.aar")
} else {
implementation jscFlavor
}
}
// Run this once to be able to run the application with BUCK
// puts all compile dependencies into folder libs for BUCK to use
task copyDownloadableDepsToLibs(type: Copy) {
from configurations.compile
into 'libs'
}
apply from: file("../../node_modules/@react-native-community/cli-platform-android/native_modules.gradle"); applyNativeModulesAppBuildGradle(project)
+88 -88
View File
@@ -1,89 +1,89 @@
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
package="com.rajarsheechatterjee.LNReader">
<!-- Internet -->
<uses-permission android:name="android.permission.INTERNET"/>
<uses-permission android:name="android.permission.ACCESS_NETWORK_STATE"/>
<!-- Storage -->
<uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE"/>
<uses-permission android:name="android.permission.READ_EXTERNAL_STORAGE"/>
<!-- For background jobs -->
<uses-permission android:name="android.permission.FOREGROUND_SERVICE" />
<uses-permission android:name="android.permission.WAKE_LOCK"/>
<uses-permission android:name="android.permission.SYSTEM_ALERT_WINDOW"/>
<uses-permission android:name="android.permission.READ_PHONE_STATE"/>
<uses-permission android:name="android.permission.VIBRATE"/>
<uses-permission android:name="android.permission.MODIFY_AUDIO_SETTINGS"/>
<uses-permission android:name="android.permission.RECEIVE_BOOT_COMPLETED"/>
<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"
android:usesCleartextTraffic="true">
<meta-data
android:name="expo.modules.updates.EXPO_UPDATE_URL"
android:value="https://exp.host/@rajarsheechatterjee/LNReader"/>
<meta-data
android:name="expo.modules.updates.EXPO_SDK_VERSION"
android:value="41.0.0"/>
<meta-data
android:name="expo.modules.updates.ENABLED"
android:value="false"/>
<meta-data
android:name="expo.modules.updates.EXPO_UPDATES_CHECK_ON_LAUNCH"
android:value="ALWAYS"/>
<meta-data
android:name="expo.modules.updates.EXPO_UPDATES_LAUNCH_WAIT_MS"
android:value="0"/>
<meta-data
android:name="expo.modules.notifications.default_notification_icon"
android:resource="@drawable/notification_icon"/>
<meta-data
android:name="expo.modules.notifications.default_notification_color"
android:resource="@color/notification_icon_color"/>
<activity
android:name=".MainActivity"
android:label="@string/app_name"
android:configChanges="keyboard|keyboardHidden|orientation|screenSize|uiMode"
android:launchMode="singleTask"
android:windowSoftInputMode="adjustResize"
android:theme="@style/Theme.App.SplashScreen"
android:screenOrientation="unspecified">
<intent-filter>
<action android:name="android.intent.action.MAIN"/>
<category android:name="android.intent.category.LAUNCHER"/>
</intent-filter>
<intent-filter>
<action android:name="android.intent.action.VIEW"/>
<category android:name="android.intent.category.DEFAULT"/>
<category android:name="android.intent.category.BROWSABLE"/>
<data android:scheme="lnreader"/>
<data android:scheme="com.rajarsheechatterjee.LNReader"/>
</intent-filter>
</activity>
<activity android:name="com.facebook.react.devsupport.DevSettingsActivity" />
<service android:name="com.asterinet.react.bgactions.RNBackgroundActionsTask" />
</application>
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
package="com.rajarsheechatterjee.LNReader">
<!-- Internet -->
<uses-permission android:name="android.permission.INTERNET"/>
<uses-permission android:name="android.permission.ACCESS_NETWORK_STATE"/>
<!-- Storage -->
<uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE"/>
<uses-permission android:name="android.permission.READ_EXTERNAL_STORAGE"/>
<!-- For background jobs -->
<uses-permission android:name="android.permission.FOREGROUND_SERVICE" />
<uses-permission android:name="android.permission.WAKE_LOCK"/>
<uses-permission android:name="android.permission.SYSTEM_ALERT_WINDOW"/>
<uses-permission android:name="android.permission.READ_PHONE_STATE"/>
<uses-permission android:name="android.permission.VIBRATE"/>
<uses-permission android:name="android.permission.MODIFY_AUDIO_SETTINGS"/>
<uses-permission android:name="android.permission.RECEIVE_BOOT_COMPLETED"/>
<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"
android:usesCleartextTraffic="true">
<meta-data
android:name="expo.modules.updates.EXPO_UPDATE_URL"
android:value="https://exp.host/@rajarsheechatterjee/LNReader"/>
<meta-data
android:name="expo.modules.updates.EXPO_SDK_VERSION"
android:value="41.0.0"/>
<meta-data
android:name="expo.modules.updates.ENABLED"
android:value="false"/>
<meta-data
android:name="expo.modules.updates.EXPO_UPDATES_CHECK_ON_LAUNCH"
android:value="ALWAYS"/>
<meta-data
android:name="expo.modules.updates.EXPO_UPDATES_LAUNCH_WAIT_MS"
android:value="0"/>
<meta-data
android:name="expo.modules.notifications.default_notification_icon"
android:resource="@drawable/notification_icon"/>
<meta-data
android:name="expo.modules.notifications.default_notification_color"
android:resource="@color/notification_icon_color"/>
<activity
android:name=".MainActivity"
android:label="@string/app_name"
android:configChanges="keyboard|keyboardHidden|orientation|screenSize|uiMode"
android:launchMode="singleTask"
android:windowSoftInputMode="adjustResize"
android:theme="@style/Theme.App.SplashScreen"
android:screenOrientation="unspecified">
<intent-filter>
<action android:name="android.intent.action.MAIN"/>
<category android:name="android.intent.category.LAUNCHER"/>
</intent-filter>
<intent-filter>
<action android:name="android.intent.action.VIEW"/>
<category android:name="android.intent.category.DEFAULT"/>
<category android:name="android.intent.category.BROWSABLE"/>
<data android:scheme="lnreader"/>
<data android:scheme="com.rajarsheechatterjee.LNReader"/>
</intent-filter>
</activity>
<activity android:name="com.facebook.react.devsupport.DevSettingsActivity" />
<service android:name="com.asterinet.react.bgactions.RNBackgroundActionsTask" />
</application>
</manifest>
@@ -1,71 +1,71 @@
package com.rajarsheechatterjee.LNReader;
import android.content.res.Configuration;
import android.content.Intent;
import android.os.Bundle;
import android.graphics.Color;
import android.view.WindowManager;
import android.os.Build;
import android.view.View;
import android.view.Window;
import com.facebook.react.ReactActivity;
import com.facebook.react.ReactActivityDelegate;
import com.facebook.react.ReactRootView;
import com.swmansion.gesturehandler.react.RNGestureHandlerEnabledRootView;
import expo.modules.splashscreen.singletons.SplashScreen;
import expo.modules.splashscreen.SplashScreenImageResizeMode;
public class MainActivity extends ReactActivity {
// Added automatically by Expo Config
@Override
public void onConfigurationChanged(Configuration newConfig) {
super.onConfigurationChanged(newConfig);
Intent intent = new Intent("onConfigurationChanged");
intent.putExtra("newConfig", newConfig);
sendBroadcast(intent);
}
@Override
protected void onCreate(Bundle savedInstanceState) {
// SplashScreen.show(...) has to be called after super.onCreate(...)
// Below line is handled by '@expo/configure-splash-screen' command and it's discouraged to modify it manually
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.P) {
WindowManager.LayoutParams layoutParams = new WindowManager.LayoutParams();
layoutParams.layoutInDisplayCutoutMode = WindowManager.LayoutParams.LAYOUT_IN_DISPLAY_CUTOUT_MODE_SHORT_EDGES;
getWindow().setAttributes(layoutParams);
}
Window w = getWindow();
w.setStatusBarColor(Color.TRANSPARENT);
w.setNavigationBarColor(Color.TRANSPARENT);
w.getDecorView().setSystemUiVisibility(View.SYSTEM_UI_FLAG_LAYOUT_HIDE_NAVIGATION | View.SYSTEM_UI_FLAG_LAYOUT_STABLE);
super.onCreate(savedInstanceState);
SplashScreen.show(this, SplashScreenImageResizeMode.COVER, ReactRootView.class, false);
}
/**
* Returns the name of the main component registered from JavaScript.
* This is used to schedule rendering of the component.
*/
@Override
protected String getMainComponentName() {
return "main";
}
@Override
protected ReactActivityDelegate createReactActivityDelegate() {
return new ReactActivityDelegate(this, getMainComponentName()) {
@Override
protected ReactRootView createRootView() {
return new RNGestureHandlerEnabledRootView(MainActivity.this);
}
};
}
}
package com.rajarsheechatterjee.LNReader;
import android.content.res.Configuration;
import android.content.Intent;
import android.os.Bundle;
import android.graphics.Color;
import android.view.WindowManager;
import android.os.Build;
import android.view.View;
import android.view.Window;
import com.facebook.react.ReactActivity;
import com.facebook.react.ReactActivityDelegate;
import com.facebook.react.ReactRootView;
import com.swmansion.gesturehandler.react.RNGestureHandlerEnabledRootView;
import expo.modules.splashscreen.singletons.SplashScreen;
import expo.modules.splashscreen.SplashScreenImageResizeMode;
public class MainActivity extends ReactActivity {
// Added automatically by Expo Config
@Override
public void onConfigurationChanged(Configuration newConfig) {
super.onConfigurationChanged(newConfig);
Intent intent = new Intent("onConfigurationChanged");
intent.putExtra("newConfig", newConfig);
sendBroadcast(intent);
}
@Override
protected void onCreate(Bundle savedInstanceState) {
// SplashScreen.show(...) has to be called after super.onCreate(...)
// Below line is handled by '@expo/configure-splash-screen' command and it's discouraged to modify it manually
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.P) {
WindowManager.LayoutParams layoutParams = new WindowManager.LayoutParams();
layoutParams.layoutInDisplayCutoutMode = WindowManager.LayoutParams.LAYOUT_IN_DISPLAY_CUTOUT_MODE_SHORT_EDGES;
getWindow().setAttributes(layoutParams);
}
Window w = getWindow();
w.setStatusBarColor(Color.TRANSPARENT);
w.setNavigationBarColor(Color.TRANSPARENT);
w.getDecorView().setSystemUiVisibility(View.SYSTEM_UI_FLAG_LAYOUT_HIDE_NAVIGATION | View.SYSTEM_UI_FLAG_LAYOUT_STABLE);
super.onCreate(savedInstanceState);
SplashScreen.show(this, SplashScreenImageResizeMode.COVER, ReactRootView.class, false);
}
/**
* Returns the name of the main component registered from JavaScript.
* This is used to schedule rendering of the component.
*/
@Override
protected String getMainComponentName() {
return "main";
}
@Override
protected ReactActivityDelegate createReactActivityDelegate() {
return new ReactActivityDelegate(this, getMainComponentName()) {
@Override
protected ReactRootView createRootView() {
return new RNGestureHandlerEnabledRootView(MainActivity.this);
}
};
}
}
Vendored Executable → Regular
+183 -183
View File
@@ -1,183 +1,183 @@
#!/usr/bin/env sh
#
# Copyright 2015 the original author or authors.
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# https://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
#
##############################################################################
##
## Gradle start up script for UN*X
##
##############################################################################
# Attempt to set APP_HOME
# Resolve links: $0 may be a link
PRG="$0"
# Need this for relative symlinks.
while [ -h "$PRG" ] ; do
ls=`ls -ld "$PRG"`
link=`expr "$ls" : '.*-> \(.*\)$'`
if expr "$link" : '/.*' > /dev/null; then
PRG="$link"
else
PRG=`dirname "$PRG"`"/$link"
fi
done
SAVED="`pwd`"
cd "`dirname \"$PRG\"`/" >/dev/null
APP_HOME="`pwd -P`"
cd "$SAVED" >/dev/null
APP_NAME="Gradle"
APP_BASE_NAME=`basename "$0"`
# Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script.
DEFAULT_JVM_OPTS='"-Xmx64m" "-Xms64m"'
# Use the maximum available, or set MAX_FD != -1 to use that value.
MAX_FD="maximum"
warn () {
echo "$*"
}
die () {
echo
echo "$*"
echo
exit 1
}
# OS specific support (must be 'true' or 'false').
cygwin=false
msys=false
darwin=false
nonstop=false
case "`uname`" in
CYGWIN* )
cygwin=true
;;
Darwin* )
darwin=true
;;
MINGW* )
msys=true
;;
NONSTOP* )
nonstop=true
;;
esac
CLASSPATH=$APP_HOME/gradle/wrapper/gradle-wrapper.jar
# Determine the Java command to use to start the JVM.
if [ -n "$JAVA_HOME" ] ; then
if [ -x "$JAVA_HOME/jre/sh/java" ] ; then
# IBM's JDK on AIX uses strange locations for the executables
JAVACMD="$JAVA_HOME/jre/sh/java"
else
JAVACMD="$JAVA_HOME/bin/java"
fi
if [ ! -x "$JAVACMD" ] ; then
die "ERROR: JAVA_HOME is set to an invalid directory: $JAVA_HOME
Please set the JAVA_HOME variable in your environment to match the
location of your Java installation."
fi
else
JAVACMD="java"
which java >/dev/null 2>&1 || die "ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH.
Please set the JAVA_HOME variable in your environment to match the
location of your Java installation."
fi
# Increase the maximum file descriptors if we can.
if [ "$cygwin" = "false" -a "$darwin" = "false" -a "$nonstop" = "false" ] ; then
MAX_FD_LIMIT=`ulimit -H -n`
if [ $? -eq 0 ] ; then
if [ "$MAX_FD" = "maximum" -o "$MAX_FD" = "max" ] ; then
MAX_FD="$MAX_FD_LIMIT"
fi
ulimit -n $MAX_FD
if [ $? -ne 0 ] ; then
warn "Could not set maximum file descriptor limit: $MAX_FD"
fi
else
warn "Could not query maximum file descriptor limit: $MAX_FD_LIMIT"
fi
fi
# For Darwin, add options to specify how the application appears in the dock
if $darwin; then
GRADLE_OPTS="$GRADLE_OPTS \"-Xdock:name=$APP_NAME\" \"-Xdock:icon=$APP_HOME/media/gradle.icns\""
fi
# For Cygwin or MSYS, switch paths to Windows format before running java
if [ "$cygwin" = "true" -o "$msys" = "true" ] ; then
APP_HOME=`cygpath --path --mixed "$APP_HOME"`
CLASSPATH=`cygpath --path --mixed "$CLASSPATH"`
JAVACMD=`cygpath --unix "$JAVACMD"`
# We build the pattern for arguments to be converted via cygpath
ROOTDIRSRAW=`find -L / -maxdepth 1 -mindepth 1 -type d 2>/dev/null`
SEP=""
for dir in $ROOTDIRSRAW ; do
ROOTDIRS="$ROOTDIRS$SEP$dir"
SEP="|"
done
OURCYGPATTERN="(^($ROOTDIRS))"
# Add a user-defined pattern to the cygpath arguments
if [ "$GRADLE_CYGPATTERN" != "" ] ; then
OURCYGPATTERN="$OURCYGPATTERN|($GRADLE_CYGPATTERN)"
fi
# Now convert the arguments - kludge to limit ourselves to /bin/sh
i=0
for arg in "$@" ; do
CHECK=`echo "$arg"|egrep -c "$OURCYGPATTERN" -`
CHECK2=`echo "$arg"|egrep -c "^-"` ### Determine if an option
if [ $CHECK -ne 0 ] && [ $CHECK2 -eq 0 ] ; then ### Added a condition
eval `echo args$i`=`cygpath --path --ignore --mixed "$arg"`
else
eval `echo args$i`="\"$arg\""
fi
i=`expr $i + 1`
done
case $i in
0) set -- ;;
1) set -- "$args0" ;;
2) set -- "$args0" "$args1" ;;
3) set -- "$args0" "$args1" "$args2" ;;
4) set -- "$args0" "$args1" "$args2" "$args3" ;;
5) set -- "$args0" "$args1" "$args2" "$args3" "$args4" ;;
6) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" ;;
7) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" ;;
8) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" ;;
9) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" "$args8" ;;
esac
fi
# Escape application args
save () {
for i do printf %s\\n "$i" | sed "s/'/'\\\\''/g;1s/^/'/;\$s/\$/' \\\\/" ; done
echo " "
}
APP_ARGS=`save "$@"`
# Collect all arguments for the java command, following the shell quoting and substitution rules
eval set -- $DEFAULT_JVM_OPTS $JAVA_OPTS $GRADLE_OPTS "\"-Dorg.gradle.appname=$APP_BASE_NAME\"" -classpath "\"$CLASSPATH\"" org.gradle.wrapper.GradleWrapperMain "$APP_ARGS"
exec "$JAVACMD" "$@"
#!/usr/bin/env sh
#
# Copyright 2015 the original author or authors.
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# https://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
#
##############################################################################
##
## Gradle start up script for UN*X
##
##############################################################################
# Attempt to set APP_HOME
# Resolve links: $0 may be a link
PRG="$0"
# Need this for relative symlinks.
while [ -h "$PRG" ] ; do
ls=`ls -ld "$PRG"`
link=`expr "$ls" : '.*-> \(.*\)$'`
if expr "$link" : '/.*' > /dev/null; then
PRG="$link"
else
PRG=`dirname "$PRG"`"/$link"
fi
done
SAVED="`pwd`"
cd "`dirname \"$PRG\"`/" >/dev/null
APP_HOME="`pwd -P`"
cd "$SAVED" >/dev/null
APP_NAME="Gradle"
APP_BASE_NAME=`basename "$0"`
# Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script.
DEFAULT_JVM_OPTS='"-Xmx64m" "-Xms64m"'
# Use the maximum available, or set MAX_FD != -1 to use that value.
MAX_FD="maximum"
warn () {
echo "$*"
}
die () {
echo
echo "$*"
echo
exit 1
}
# OS specific support (must be 'true' or 'false').
cygwin=false
msys=false
darwin=false
nonstop=false
case "`uname`" in
CYGWIN* )
cygwin=true
;;
Darwin* )
darwin=true
;;
MINGW* )
msys=true
;;
NONSTOP* )
nonstop=true
;;
esac
CLASSPATH=$APP_HOME/gradle/wrapper/gradle-wrapper.jar
# Determine the Java command to use to start the JVM.
if [ -n "$JAVA_HOME" ] ; then
if [ -x "$JAVA_HOME/jre/sh/java" ] ; then
# IBM's JDK on AIX uses strange locations for the executables
JAVACMD="$JAVA_HOME/jre/sh/java"
else
JAVACMD="$JAVA_HOME/bin/java"
fi
if [ ! -x "$JAVACMD" ] ; then
die "ERROR: JAVA_HOME is set to an invalid directory: $JAVA_HOME
Please set the JAVA_HOME variable in your environment to match the
location of your Java installation."
fi
else
JAVACMD="java"
which java >/dev/null 2>&1 || die "ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH.
Please set the JAVA_HOME variable in your environment to match the
location of your Java installation."
fi
# Increase the maximum file descriptors if we can.
if [ "$cygwin" = "false" -a "$darwin" = "false" -a "$nonstop" = "false" ] ; then
MAX_FD_LIMIT=`ulimit -H -n`
if [ $? -eq 0 ] ; then
if [ "$MAX_FD" = "maximum" -o "$MAX_FD" = "max" ] ; then
MAX_FD="$MAX_FD_LIMIT"
fi
ulimit -n $MAX_FD
if [ $? -ne 0 ] ; then
warn "Could not set maximum file descriptor limit: $MAX_FD"
fi
else
warn "Could not query maximum file descriptor limit: $MAX_FD_LIMIT"
fi
fi
# For Darwin, add options to specify how the application appears in the dock
if $darwin; then
GRADLE_OPTS="$GRADLE_OPTS \"-Xdock:name=$APP_NAME\" \"-Xdock:icon=$APP_HOME/media/gradle.icns\""
fi
# For Cygwin or MSYS, switch paths to Windows format before running java
if [ "$cygwin" = "true" -o "$msys" = "true" ] ; then
APP_HOME=`cygpath --path --mixed "$APP_HOME"`
CLASSPATH=`cygpath --path --mixed "$CLASSPATH"`
JAVACMD=`cygpath --unix "$JAVACMD"`
# We build the pattern for arguments to be converted via cygpath
ROOTDIRSRAW=`find -L / -maxdepth 1 -mindepth 1 -type d 2>/dev/null`
SEP=""
for dir in $ROOTDIRSRAW ; do
ROOTDIRS="$ROOTDIRS$SEP$dir"
SEP="|"
done
OURCYGPATTERN="(^($ROOTDIRS))"
# Add a user-defined pattern to the cygpath arguments
if [ "$GRADLE_CYGPATTERN" != "" ] ; then
OURCYGPATTERN="$OURCYGPATTERN|($GRADLE_CYGPATTERN)"
fi
# Now convert the arguments - kludge to limit ourselves to /bin/sh
i=0
for arg in "$@" ; do
CHECK=`echo "$arg"|egrep -c "$OURCYGPATTERN" -`
CHECK2=`echo "$arg"|egrep -c "^-"` ### Determine if an option
if [ $CHECK -ne 0 ] && [ $CHECK2 -eq 0 ] ; then ### Added a condition
eval `echo args$i`=`cygpath --path --ignore --mixed "$arg"`
else
eval `echo args$i`="\"$arg\""
fi
i=`expr $i + 1`
done
case $i in
0) set -- ;;
1) set -- "$args0" ;;
2) set -- "$args0" "$args1" ;;
3) set -- "$args0" "$args1" "$args2" ;;
4) set -- "$args0" "$args1" "$args2" "$args3" ;;
5) set -- "$args0" "$args1" "$args2" "$args3" "$args4" ;;
6) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" ;;
7) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" ;;
8) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" ;;
9) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" "$args8" ;;
esac
fi
# Escape application args
save () {
for i do printf %s\\n "$i" | sed "s/'/'\\\\''/g;1s/^/'/;\$s/\$/' \\\\/" ; done
echo " "
}
APP_ARGS=`save "$@"`
# Collect all arguments for the java command, following the shell quoting and substitution rules
eval set -- $DEFAULT_JVM_OPTS $JAVA_OPTS $GRADLE_OPTS "\"-Dorg.gradle.appname=$APP_BASE_NAME\"" -classpath "\"$CLASSPATH\"" org.gradle.wrapper.GradleWrapperMain "$APP_ARGS"
exec "$JAVACMD" "$@"
+11 -11
View File
@@ -1,11 +1,11 @@
{
"expo": {
"name": "LNReader",
"slug": "LNReader",
"scheme": "lnreader",
"notification": {
"icon": "./assets/notification.png",
"color": "#00adb5"
}
}
}
{
"expo": {
"name": "LNReader",
"slug": "LNReader",
"scheme": "lnreader",
"notification": {
"icon": "./assets/notification.png",
"color": "#00adb5"
}
}
}
+1 -1
View File
@@ -1,4 +1,4 @@
module.exports = function(api) {
module.exports = function (api) {
api.cache(true);
return {
presets: ['babel-preset-expo'],
+1 -1
View File
@@ -1,5 +1,5 @@
import 'react-native-gesture-handler';
import { registerRootComponent } from 'expo';
import {registerRootComponent} from 'expo';
import App from './App';
+1 -1
View File
@@ -1,4 +1,4 @@
// Learn more https://docs.expo.io/guides/customizing-metro
const { getDefaultConfig } = require('expo/metro-config');
const {getDefaultConfig} = require('expo/metro-config');
module.exports = getDefaultConfig(__dirname);
+22879 -19810
View File
File diff suppressed because it is too large Load Diff
+66 -74
View File
@@ -1,74 +1,66 @@
{
"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-haptics": "~10.0.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-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-tts": "^4.1.0",
"react-native-unimodules": "~0.13.3",
"react-native-vector-icons": "^7.1.0",
"react-native-web": "^0.16.5",
"react-native-webview": "^11.6.2",
"react-redux": "^7.2.2",
"redux": "^4.0.5",
"redux-persist": "^6.0.0",
"redux-thunk": "^2.3.0",
"rn-sliding-up-panel": "^2.4.4"
},
"devDependencies": {
"@babel/core": "~7.9.0"
},
"prettier": {
"tabWidth": 4,
"semi": true,
"singleQuote": false,
"quoteProps": "as-needed",
"trailingComma": "es5",
"bracketSpacing": true,
"arrowParens": "always"
},
"private": true,
"name": "lnreader",
"version": "1.0.0"
}
{
"scripts": {
"android": "react-native run-android",
"ios": "react-native run-ios",
"start": "react-native start",
"lint": "eslint ."
},
"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-haptics": "~10.0.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-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-tts": "^4.1.0",
"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",
"@react-native-community/eslint-config": "^3.0.1",
"eslint": "^7.32.0"
},
"private": true,
"name": "lnreader",
"version": "1.1.5"
}
+41 -41
View File
@@ -1,47 +1,47 @@
import React from "react";
import { Dimensions, StyleSheet, View } from "react-native";
import React from 'react';
import {Dimensions, StyleSheet, View} from 'react-native';
import { IconButton } from "react-native-paper";
import {IconButton} from 'react-native-paper';
export const Actionbar = ({ active, actions, theme, style }) => {
if (active) {
return (
<View
style={[
styles.actionbarContainer,
{ backgroundColor: theme.colorPrimary },
style,
]}
>
{actions.map(
(action, index) =>
action.icon && (
<IconButton
key={index}
icon={action.icon}
color={theme.textColorPrimary}
onPress={action.onPress}
/>
)
)}
</View>
);
} else {
return null;
}
export const Actionbar = ({active, actions, theme, style}) => {
if (active) {
return (
<View
style={[
styles.actionbarContainer,
{backgroundColor: theme.colorPrimary},
style,
]}
>
{actions.map(
(action, index) =>
action.icon && (
<IconButton
key={index}
icon={action.icon}
color={theme.textColorPrimary}
onPress={action.onPress}
/>
),
)}
</View>
);
} else {
return null;
}
};
const styles = StyleSheet.create({
actionbarContainer: {
position: "absolute",
width: Dimensions.get("window").width - 32,
bottom: 0,
margin: 16,
borderRadius: 6,
flexDirection: "row",
justifyContent: "space-between",
paddingHorizontal: 8,
paddingVertical: 4,
elevation: 2,
},
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,
},
});
+17 -19
View File
@@ -1,24 +1,22 @@
import React from "react";
import React from 'react';
import { Appbar as MaterialAppbar } from "react-native-paper";
import {Appbar as MaterialAppbar} from 'react-native-paper';
import { useTheme } from "../hooks/reduxHooks";
import {useTheme} from '../hooks/reduxHooks';
export const Appbar = ({ title, onBackAction, style, children }) => {
const theme = useTheme();
export const Appbar = ({title, onBackAction, style, children}) => {
const theme = useTheme();
return (
<MaterialAppbar.Header
style={[{ backgroundColor: theme.colorPrimary }, style]}
>
{onBackAction && (
<MaterialAppbar.BackAction onPress={onBackAction} />
)}
<MaterialAppbar.Content
title={title}
titleStyle={{ color: theme.textColorPrimary }}
/>
{children}
</MaterialAppbar.Header>
);
return (
<MaterialAppbar.Header
style={[{backgroundColor: theme.colorPrimary}, style]}
>
{onBackAction && <MaterialAppbar.BackAction onPress={onBackAction} />}
<MaterialAppbar.Content
title={title}
titleStyle={{color: theme.textColorPrimary}}
/>
{children}
</MaterialAppbar.Header>
);
};
+13 -13
View File
@@ -1,17 +1,17 @@
import React from "react";
import { View } from "react-native";
import React from 'react';
import {View} from 'react-native';
const BottomSheetHandle = ({ theme }) => (
<View
style={{
backgroundColor: `${theme.textColorPrimary}66`,
height: 4,
width: 40,
borderRadius: 50,
top: 8,
alignSelf: "center",
}}
/>
const BottomSheetHandle = ({theme}) => (
<View
style={{
backgroundColor: `${theme.textColorPrimary}66`,
height: 4,
width: 40,
borderRadius: 50,
top: 8,
alignSelf: 'center',
}}
/>
);
export default BottomSheetHandle;
+75 -75
View File
@@ -1,85 +1,85 @@
import React from "react";
import { Pressable, StyleSheet, Text, View } from "react-native";
import React from 'react';
import {Pressable, StyleSheet, Text, View} from 'react-native';
import MaterialCommunityIcons from "react-native-vector-icons/MaterialCommunityIcons";
import MaterialCommunityIcons from 'react-native-vector-icons/MaterialCommunityIcons';
export const Checkbox = ({ label, status, onPress, theme }) => (
<View
style={[
styles.checkboxContainer,
status && { backgroundColor: theme.rippleColor },
]}
export const Checkbox = ({label, status, onPress, theme}) => (
<View
style={[
styles.checkboxContainer,
status && {backgroundColor: theme.rippleColor},
]}
>
<Pressable
android_ripple={{color: theme.rippleColor}}
style={styles.pressable}
onPress={onPress}
>
<Pressable
android_ripple={{ color: theme.rippleColor }}
style={styles.pressable}
onPress={onPress}
>
<Text
style={{
color: status ? theme.colorAccent : theme.textColorPrimary,
}}
>
{label}
</Text>
{status && (
<MaterialCommunityIcons
name="check"
color={theme.colorAccent}
size={21}
style={styles.icon}
/>
)}
</Pressable>
</View>
<Text
style={{
color: status ? theme.colorAccent : theme.textColorPrimary,
}}
>
{label}
</Text>
{status && (
<MaterialCommunityIcons
name="check"
color={theme.colorAccent}
size={21}
style={styles.icon}
/>
)}
</Pressable>
</View>
);
export const SortItem = ({ label, status, onPress, theme }) => (
<View
style={[
styles.checkboxContainer,
status && { backgroundColor: theme.rippleColor },
]}
export const SortItem = ({label, status, onPress, theme}) => (
<View
style={[
styles.checkboxContainer,
status && {backgroundColor: theme.rippleColor},
]}
>
<Pressable
android_ripple={{color: theme.rippleColor}}
style={styles.pressable}
onPress={onPress}
>
<Pressable
android_ripple={{ color: theme.rippleColor }}
style={styles.pressable}
onPress={onPress}
>
<Text
style={{
color: status ? theme.colorAccent : theme.textColorPrimary,
}}
>
{label}
</Text>
{status && (
<MaterialCommunityIcons
name={status === "asc" ? "arrow-up" : "arrow-down"}
color={theme.colorAccent}
size={21}
style={styles.icon}
/>
)}
</Pressable>
</View>
<Text
style={{
color: status ? theme.colorAccent : theme.textColorPrimary,
}}
>
{label}
</Text>
{status && (
<MaterialCommunityIcons
name={status === 'asc' ? 'arrow-up' : 'arrow-down'}
color={theme.colorAccent}
size={21}
style={styles.icon}
/>
)}
</Pressable>
</View>
);
const styles = StyleSheet.create({
checkboxContainer: {
borderRadius: 8,
overflow: "hidden",
marginVertical: 4,
},
pressable: {
paddingHorizontal: 16,
paddingVertical: 12,
flexDirection: "row",
justifyContent: "space-between",
},
icon: {
position: "absolute",
right: 16,
alignSelf: "center",
},
checkboxContainer: {
borderRadius: 8,
overflow: 'hidden',
marginVertical: 4,
},
pressable: {
paddingHorizontal: 16,
paddingVertical: 12,
flexDirection: 'row',
justifyContent: 'space-between',
},
icon: {
position: 'absolute',
right: 16,
alignSelf: 'center',
},
});
+130 -135
View File
@@ -1,153 +1,148 @@
import React, { useState } from "react";
import { FlatList, Pressable, StyleSheet, Text, View } from "react-native";
import React, {useState} from 'react';
import {FlatList, Pressable, StyleSheet, Text, View} from 'react-native';
import { Modal, Portal, TextInput } from "react-native-paper";
import {Modal, Portal, TextInput} from 'react-native-paper';
const ColorPickerModal = ({
theme,
color,
title,
onSubmit,
hideModal,
modalVisible,
showAccentColors,
theme,
color,
title,
onSubmit,
hideModal,
modalVisible,
showAccentColors,
}) => {
const [text, setText] = useState(color);
const [error, setError] = useState();
const [text, setText] = useState(color);
const [error, setError] = useState();
const onDismiss = () => {
hideModal();
setText();
setError();
};
const onDismiss = () => {
hideModal();
setText();
setError();
};
const onChangeText = (text) => setText(text);
const onChangeText = text => setText(text);
const onSubmitEditing = () => {
const re = /^#([0-9a-f]{6}|[0-9a-f]{3})$/i;
const onSubmitEditing = () => {
const re = /^#([0-9a-f]{6}|[0-9a-f]{3})$/i;
if (text.match(re)) {
onSubmit(text);
hideModal();
} else {
setError("Enter a valid hex color code");
}
};
if (text.match(re)) {
onSubmit(text);
hideModal();
} else {
setError('Enter a valid hex color code');
}
};
const textInputTheme = {
colors: {
primary: color,
placeholder: theme.textColorHint,
text: theme.textColorPrimary,
background: "transparent",
},
};
const textInputTheme = {
colors: {
primary: color,
placeholder: theme.textColorHint,
text: theme.textColorPrimary,
background: 'transparent',
},
};
const accentColors = [
"#EF5350",
"#EC407A",
"#AB47BC",
"#7E57C2",
"#5C6BC0",
"#42A5F5",
"#29B6FC",
"#26C6DA",
"#26A69A",
"#66BB6A",
"#9CCC65",
"#D4E157",
"#FFEE58",
"#FFCA28",
"#FFA726",
"#FF7043",
"#8D6E63",
"#BDBDBD",
"#78909C",
"#000000",
];
const accentColors = [
'#EF5350',
'#EC407A',
'#AB47BC',
'#7E57C2',
'#5C6BC0',
'#42A5F5',
'#29B6FC',
'#26C6DA',
'#26A69A',
'#66BB6A',
'#9CCC65',
'#D4E157',
'#FFEE58',
'#FFCA28',
'#FFA726',
'#FF7043',
'#8D6E63',
'#BDBDBD',
'#78909C',
'#000000',
];
return (
<Portal>
<Modal
visible={modalVisible}
onDismiss={onDismiss}
contentContainerStyle={[
styles.modalContainer,
{ backgroundColor: theme.colorPrimary },
]}
>
<Text
style={[
styles.modalTitle,
{ color: theme.textColorPrimary },
]}
>
{title}
</Text>
{showAccentColors && (
<FlatList
contentContainerStyle={{ marginBottom: 8 }}
data={accentColors}
numColumns={4}
keyExtractor={(item) => item}
renderItem={({ item }) => (
<View
style={{
borderRadius: 4,
overflow: "hidden",
backgroundColor: item,
flex: 1 / 4,
height: 40,
marginHorizontal: 4,
marginVertical: 4,
}}
>
<Pressable
style={{ flex: 1 }}
android_ripple={{
color: "rgba(0,0,0,0.12)",
}}
onPress={() => {
onSubmit(item);
hideModal();
}}
/>
</View>
)}
/>
)}
<TextInput
value={text}
defaultValue={color}
placeholder="Hex Color Code (E.g. #3399FF)"
onChangeText={onChangeText}
onSubmitEditing={onSubmitEditing}
mode="outlined"
theme={textInputTheme}
underlineColor={theme.textColorHint}
dense
error={error}
return (
<Portal>
<Modal
visible={modalVisible}
onDismiss={onDismiss}
contentContainerStyle={[
styles.modalContainer,
{backgroundColor: theme.colorPrimary},
]}
>
<Text style={[styles.modalTitle, {color: theme.textColorPrimary}]}>
{title}
</Text>
{showAccentColors && (
<FlatList
contentContainerStyle={{marginBottom: 8}}
data={accentColors}
numColumns={4}
keyExtractor={item => item}
renderItem={({item}) => (
<View
style={{
borderRadius: 4,
overflow: 'hidden',
backgroundColor: item,
flex: 1 / 4,
height: 40,
marginHorizontal: 4,
marginVertical: 4,
}}
>
<Pressable
style={{flex: 1}}
android_ripple={{
color: 'rgba(0,0,0,0.12)',
}}
onPress={() => {
onSubmit(item);
hideModal();
}}
/>
<Text style={styles.errorText}>{error}</Text>
</Modal>
</Portal>
);
</View>
)}
/>
)}
<TextInput
value={text}
defaultValue={color}
placeholder="Hex Color Code (E.g. #3399FF)"
onChangeText={onChangeText}
onSubmitEditing={onSubmitEditing}
mode="outlined"
theme={textInputTheme}
underlineColor={theme.textColorHint}
dense
error={error}
/>
<Text style={styles.errorText}>{error}</Text>
</Modal>
</Portal>
);
};
export default ColorPickerModal;
const styles = StyleSheet.create({
modalContainer: {
margin: 30,
padding: 20,
borderRadius: 8,
},
modalTitle: {
fontSize: 18,
marginBottom: 16,
},
errorText: {
color: "#FF0033",
paddingTop: 8,
},
modalContainer: {
margin: 30,
padding: 20,
borderRadius: 8,
},
modalTitle: {
fontSize: 18,
marginBottom: 16,
},
errorText: {
color: '#FF0033',
paddingTop: 8,
},
});
+19 -19
View File
@@ -1,27 +1,27 @@
import React from "react";
import { View, StyleSheet } from "react-native";
import React from 'react';
import {View, StyleSheet} from 'react-native';
const ScreenContainer = ({ children, theme }) => (
<View
style={{
flex: 1,
backgroundColor: theme.colorPrimaryDark,
paddingBottom: 16,
}}
>
{children}
</View>
const ScreenContainer = ({children, theme}) => (
<View
style={{
flex: 1,
backgroundColor: theme.colorPrimaryDark,
paddingBottom: 16,
}}
>
{children}
</View>
);
const Row = ({ children, style }) => (
<View style={[styles.row, style]}>{children}</View>
const Row = ({children, style}) => (
<View style={[styles.row, style]}>{children}</View>
);
export { ScreenContainer, Row };
export {ScreenContainer, Row};
const styles = StyleSheet.create({
row: {
flexDirection: "row",
alignItems: "center",
},
row: {
flexDirection: 'row',
alignItems: 'center',
},
});
+89 -91
View File
@@ -1,110 +1,108 @@
import PropTypes from "prop-types";
import React, { PureComponent, Children } from "react";
import { View, Animated, StyleSheet } from "react-native";
import PropTypes from 'prop-types';
import React, {PureComponent, Children} from 'react';
import {View, Animated, StyleSheet} from 'react-native';
export default class FadeView extends PureComponent {
static defaultProps = {
animationDuration: 225,
static defaultProps = {
animationDuration: 225,
active: false,
removeHiddenSubviews: true,
active: false,
removeHiddenSubviews: true,
};
static propTypes = {
animationDuration: PropTypes.number,
active: PropTypes.bool,
removeHiddenSubviews: PropTypes.bool,
children: PropTypes.oneOfType([
PropTypes.arrayOf(PropTypes.node),
PropTypes.node,
]),
};
constructor(props) {
super(props);
this.renderChild = this.renderChild.bind(this);
this.mounted = false;
let {active} = this.props;
this.state = {
progress: new Animated.Value(Number(active)),
animating: false,
};
}
static propTypes = {
animationDuration: PropTypes.number,
componentDidMount() {
this.mounted = true;
}
active: PropTypes.bool,
removeHiddenSubviews: PropTypes.bool,
componentWillUnmount() {
this.mounted = false;
}
children: PropTypes.oneOfType([
PropTypes.arrayOf(PropTypes.node),
PropTypes.node,
]),
};
UNSAFE_componentWillReceiveProps({active, animationDuration}) {
let {progress} = this.state;
constructor(props) {
super(props);
if (active ^ this.props.active) {
this.setState({animating: true});
this.renderChild = this.renderChild.bind(this);
this.mounted = false;
let { active } = this.props;
this.state = {
progress: new Animated.Value(Number(active)),
animating: false,
};
}
componentDidMount() {
this.mounted = true;
}
componentWillUnmount() {
this.mounted = false;
}
UNSAFE_componentWillReceiveProps({ active, animationDuration }) {
let { progress } = this.state;
if (active ^ this.props.active) {
this.setState({ animating: true });
Animated.timing(progress, {
toValue: Number(active),
duration: animationDuration,
useNativeDriver: true,
}).start(() => {
if (this.mounted) {
this.setState({ animating: false });
}
});
Animated.timing(progress, {
toValue: Number(active),
duration: animationDuration,
useNativeDriver: true,
}).start(() => {
if (this.mounted) {
this.setState({animating: false});
}
});
}
}
renderChild(child, index) {
let {active, removeHiddenSubviews} = this.props;
let {animating, progress} = this.state;
let opacity = progress.interpolate({
inputRange: [0, 1],
outputRange: index ? [0, 1] : [1, 0],
});
let hidden = active ^ !!index;
if (removeHiddenSubviews) {
if (!animating && hidden) {
return null;
}
}
renderChild(child, index) {
let { active, removeHiddenSubviews } = this.props;
let { animating, progress } = this.state;
let pointerEvents = hidden ? 'none' : 'box-none';
let opacity = progress.interpolate({
inputRange: [0, 1],
outputRange: index ? [0, 1] : [1, 0],
});
return (
<Animated.View
style={[styles.container, {opacity}]}
pointerEvents={pointerEvents}
>
{child}
</Animated.View>
);
}
let hidden = active ^ !!index;
render() {
let {children, ...props} = this.props;
if (removeHiddenSubviews) {
if (!animating && hidden) {
return null;
}
}
let pointerEvents = hidden ? "none" : "box-none";
return (
<Animated.View
style={[styles.container, { opacity }]}
pointerEvents={pointerEvents}
>
{child}
</Animated.View>
);
}
render() {
let { children, ...props } = this.props;
return (
<View {...props}>{Children.map(children, this.renderChild)}</View>
);
}
return <View {...props}>{Children.map(children, this.renderChild)}</View>;
}
}
const styles = StyleSheet.create({
container: {
// ...StyleSheet.absoluteFillObject,
flexDirection: "row",
justifyContent: "center",
alignItems: "center",
},
container: {
// ...StyleSheet.absoluteFillObject,
flexDirection: 'row',
justifyContent: 'center',
alignItems: 'center',
},
});
+56 -64
View File
@@ -1,72 +1,64 @@
import React from "react";
import { Pressable, View } from "react-native";
import MaterialCommunityIcons from "react-native-vector-icons/MaterialCommunityIcons";
import React from 'react';
import {Pressable, View} from 'react-native';
import MaterialCommunityIcons from 'react-native-vector-icons/MaterialCommunityIcons';
export const ToggleButton = ({ icon, selected, theme, color, onPress }) => (
<View
style={{
borderRadius: 8,
overflow: "hidden",
marginHorizontal: 4,
}}
export const ToggleButton = ({icon, selected, theme, color, onPress}) => (
<View
style={{
borderRadius: 8,
overflow: 'hidden',
marginHorizontal: 4,
}}
>
<Pressable
android_ripple={{color: theme.rippleColor}}
style={{
padding: 10,
alignItems: 'center',
justifyContent: 'center',
}}
onPress={onPress}
>
<Pressable
android_ripple={{ color: theme.rippleColor }}
style={{
padding: 10,
alignItems: "center",
justifyContent: "center",
}}
onPress={onPress}
>
<MaterialCommunityIcons
name={icon}
color={
selected
? theme.colorAccent
: color
? color
: theme.textColorPrimary
}
size={24}
/>
</Pressable>
</View>
<MaterialCommunityIcons
name={icon}
color={
selected ? theme.colorAccent : color ? color : theme.textColorPrimary
}
size={24}
/>
</Pressable>
</View>
);
export const ToggleColorButton = ({
selected,
backgroundColor,
textColor,
onPress,
selected,
backgroundColor,
textColor,
onPress,
}) => (
<View
style={{
borderRadius: 50,
overflow: "hidden",
marginHorizontal: 6,
height: 44,
width: 44,
}}
<View
style={{
borderRadius: 50,
overflow: 'hidden',
marginHorizontal: 6,
height: 44,
width: 44,
}}
>
<Pressable
android_ripple={{color: textColor}}
style={{
flex: 1,
padding: 10,
backgroundColor: backgroundColor,
alignItems: 'center',
justifyContent: 'center',
}}
onPress={onPress}
>
<Pressable
android_ripple={{ color: textColor }}
style={{
flex: 1,
padding: 10,
backgroundColor: backgroundColor,
alignItems: "center",
justifyContent: "center",
}}
onPress={onPress}
>
{selected && (
<MaterialCommunityIcons
name="check"
color={textColor}
size={24}
/>
)}
</Pressable>
</View>
{selected && (
<MaterialCommunityIcons name="check" color={textColor} size={24} />
)}
</Pressable>
</View>
);
+37 -43
View File
@@ -1,51 +1,45 @@
import React from "react";
import { StyleSheet, Text, View } from "react-native";
import { useSelector } from "react-redux";
import React from 'react';
import {StyleSheet, Text, View} from 'react-native';
import {useSelector} from 'react-redux';
const EmptyView = ({ icon, description, style, children, iconStyle }) => {
const theme = useSelector((state) => state.settingsReducer.theme);
const EmptyView = ({icon, description, style, children, iconStyle}) => {
const theme = useSelector(state => state.settingsReducer.theme);
return (
<View style={styles.emptyViewContainer}>
<Text
style={[
styles.emptyViewIcon,
{ color: theme.textColorHint },
style,
iconStyle,
]}
>
{icon}
</Text>
<Text
style={[
styles.emptyViewText,
{ color: theme.textColorHint },
style,
]}
>
{description}
</Text>
{children}
</View>
);
return (
<View style={styles.emptyViewContainer}>
<Text
style={[
styles.emptyViewIcon,
{color: theme.textColorHint},
style,
iconStyle,
]}
>
{icon}
</Text>
<Text style={[styles.emptyViewText, {color: theme.textColorHint}, style]}>
{description}
</Text>
{children}
</View>
);
};
export default EmptyView;
const styles = StyleSheet.create({
emptyViewContainer: {
flex: 1,
justifyContent: "center",
alignItems: "center",
},
emptyViewIcon: {
fontSize: 45,
},
emptyViewText: {
fontWeight: "bold",
marginTop: 10,
textAlign: "center",
paddingHorizontal: 30,
},
emptyViewContainer: {
flex: 1,
justifyContent: 'center',
alignItems: 'center',
},
emptyViewIcon: {
fontSize: 45,
},
emptyViewText: {
fontWeight: 'bold',
marginTop: 10,
textAlign: 'center',
paddingHorizontal: 30,
},
});
+22 -22
View File
@@ -1,28 +1,28 @@
import React from "react";
import { Text, View } from "react-native";
import React from 'react';
import {Text, View} from 'react-native';
import { IconButton } from "react-native-paper";
import { Row } from "./Common";
import {IconButton} from 'react-native-paper';
import {Row} from './Common';
import EmptyView from "./EmptyView";
import EmptyView from './EmptyView';
const ErrorView = ({ theme, onRetry, error, openWebView }) => (
<View style={{ flex: 1, justifyContent: "center" }}>
<EmptyView
icon="(; ̄Д ̄)"
description={error}
style={{ color: theme.textColorSecondary }}
>
<IconButton
icon="reload"
size={25}
style={{ margin: 0, marginTop: 24 }}
color={theme.text}
onPress={onRetry}
/>
<Text style={{ color: theme.textColorPrimary }}>Retry</Text>
</EmptyView>
</View>
const ErrorView = ({theme, onRetry, error, openWebView}) => (
<View style={{flex: 1, justifyContent: 'center'}}>
<EmptyView
icon="(; ̄Д ̄)"
description={error}
style={{color: theme.textColorSecondary}}
>
<IconButton
icon="reload"
size={25}
style={{margin: 0, marginTop: 24}}
color={theme.text}
onPress={onRetry}
/>
<Text style={{color: theme.textColorPrimary}}>Retry</Text>
</EmptyView>
</View>
);
export default ErrorView;
+67 -67
View File
@@ -1,75 +1,75 @@
import React from "react";
import { Pressable, StyleSheet, Text, View } from "react-native";
import { IconButton } from "react-native-paper";
import React from 'react';
import {Pressable, StyleSheet, Text, View} from 'react-native';
import {IconButton} from 'react-native-paper';
const icons = ["(・o・;)", "Σ(ಠ_ಠ)", "ಥ_ಥ", "(˘・_・˘)", "(; ̄Д ̄)", "(・Д・。"];
const icons = ['(・o・;)', 'Σ(ಠ_ಠ)', 'ಥ_ಥ', '(˘・_・˘)', '(; ̄Д ̄)', '(・Д・。'];
export const ErrorView = ({ errorName, actions, theme }) => (
<View style={{ flex: 1, justifyContent: "center", alignItems: "center" }}>
<Text style={[styles.emptyViewIcon, { color: theme.textColorHint }]}>
{/* {icons[Math.floor(Math.random() * 5)]} */}
ಥ_ಥ
</Text>
<Text style={[styles.emptyViewText, { color: theme.textColorHint }]}>
{errorName}
</Text>
<View style={{ flexDirection: "row" }}>
{actions.map((action) => (
<View
style={{
borderRadius: 4,
overflow: "hidden",
margin: 16,
}}
>
<Pressable
android_ripple={{
color: theme.rippleColor,
borderless: false,
}}
onPress={action.onPress}
style={{
justifyContent: "center",
alignItems: "center",
paddingBottom: 8,
paddingHorizontal: 20,
minWidth: 100,
}}
>
<IconButton
icon={action.icon}
color={theme.textColorHint}
size={24}
style={{ margin: 0 }}
/>
<Text
style={{
fontSize: 12,
color: theme.textColorHint,
}}
>
{action.name}
</Text>
</Pressable>
</View>
))}
export const ErrorView = ({errorName, actions, theme}) => (
<View style={{flex: 1, justifyContent: 'center', alignItems: 'center'}}>
<Text style={[styles.emptyViewIcon, {color: theme.textColorHint}]}>
{/* {icons[Math.floor(Math.random() * 5)]} */}
ಥ_ಥ
</Text>
<Text style={[styles.emptyViewText, {color: theme.textColorHint}]}>
{errorName}
</Text>
<View style={{flexDirection: 'row'}}>
{actions.map(action => (
<View
style={{
borderRadius: 4,
overflow: 'hidden',
margin: 16,
}}
>
<Pressable
android_ripple={{
color: theme.rippleColor,
borderless: false,
}}
onPress={action.onPress}
style={{
justifyContent: 'center',
alignItems: 'center',
paddingBottom: 8,
paddingHorizontal: 20,
minWidth: 100,
}}
>
<IconButton
icon={action.icon}
color={theme.textColorHint}
size={24}
style={{margin: 0}}
/>
<Text
style={{
fontSize: 12,
color: theme.textColorHint,
}}
>
{action.name}
</Text>
</Pressable>
</View>
))}
</View>
</View>
);
const styles = StyleSheet.create({
emptyViewContainer: {
flex: 1,
justifyContent: "center",
alignItems: "center",
},
emptyViewIcon: {
fontSize: 45,
},
emptyViewText: {
fontWeight: "bold",
marginTop: 10,
textAlign: "center",
paddingHorizontal: 30,
},
emptyViewContainer: {
flex: 1,
justifyContent: 'center',
alignItems: 'center',
},
emptyViewIcon: {
fontSize: 45,
},
emptyViewText: {
fontWeight: 'bold',
marginTop: 10,
textAlign: 'center',
paddingHorizontal: 30,
},
});
+1 -1
View File
@@ -1,2 +1,2 @@
import { NativeModules } from "react-native";
import {NativeModules} from 'react-native';
module.exports = NativeModules.FullScreen;
+114 -122
View File
@@ -1,143 +1,135 @@
import React from "react";
import { Pressable, StyleSheet, Text, View } from "react-native";
import React from 'react';
import {Pressable, StyleSheet, Text, View} from 'react-native';
import { List as PaperList, Divider as PaperDivider } from "react-native-paper";
import MaterialCommunityIcons from "react-native-vector-icons/MaterialCommunityIcons";
import {List as PaperList, Divider as PaperDivider} from 'react-native-paper';
import MaterialCommunityIcons from 'react-native-vector-icons/MaterialCommunityIcons';
const ListSection = ({ children }) => (
<PaperList.Section style={styles.listSection}>{children}</PaperList.Section>
const ListSection = ({children}) => (
<PaperList.Section style={styles.listSection}>{children}</PaperList.Section>
);
const ListSubHeader = ({ children, theme }) => (
<PaperList.Subheader
style={[styles.listSubHeader, { color: theme.colorAccent }]}
>
{children}
</PaperList.Subheader>
const ListSubHeader = ({children, theme}) => (
<PaperList.Subheader
style={[styles.listSubHeader, {color: theme.colorAccent}]}
>
{children}
</PaperList.Subheader>
);
const ListItem = ({
title,
description,
icon,
onPress,
theme,
right,
iconColor,
titleStyle,
style,
title,
description,
icon,
onPress,
theme,
right,
iconColor,
titleStyle,
style,
}) => (
<PaperList.Item
title={title}
style={style}
titleStyle={[{ color: theme.textColorPrimary }, titleStyle]}
description={description}
descriptionStyle={{ color: theme.textColorSecondary }}
descriptionNumberOfLines={1}
left={() => (
<View style={{ justifyContent: "center" }}>
{icon && (
<PaperList.Icon
color={theme.colorAccent}
icon={icon}
style={{
marginVertical: 0,
}}
/>
)}
</View>
)}
right={() =>
right && (
<MaterialCommunityIcons
name={right}
color={iconColor || "#47a84a"}
size={23}
style={{ marginRight: 16, textAlignVertical: "center" }}
/>
)
}
onPress={onPress}
rippleColor={theme.rippleColor}
/>
);
const Divider = ({ theme }) => (
<PaperDivider style={{ backgroundColor: theme.dividerColor, height: 1 }} />
);
const InfoItem = ({ title, icon, theme }) => (
<PaperList.Item
title={title}
titleStyle={{ color: theme.textColorSecondary, fontSize: 14 }}
titleNumberOfLines={5}
left={() =>
icon && (
<PaperList.Icon
color={theme.textColorSecondary}
icon={icon}
style={{ marginVertical: 0 }}
/>
)
}
/>
);
const ColorItem = ({ title, description, theme, onPress }) => (
<Pressable
style={{
padding: 16,
flexDirection: "row",
justifyContent: "space-between",
alignItems: "center",
}}
android_ripple={{ color: theme.rippleColor }}
onPress={onPress}
>
<View>
<Text style={{ color: theme.textColorPrimary, fontSize: 16 }}>
{title}
</Text>
<Text style={{ color: theme.textColorSecondary }}>
{description}
</Text>
</View>
<View
<PaperList.Item
title={title}
style={style}
titleStyle={[{color: theme.textColorPrimary}, titleStyle]}
description={description}
descriptionStyle={{color: theme.textColorSecondary}}
descriptionNumberOfLines={1}
left={() => (
<View style={{justifyContent: 'center'}}>
{icon && (
<PaperList.Icon
color={theme.colorAccent}
icon={icon}
style={{
backgroundColor: description,
height: 24,
width: 24,
borderRadius: 50,
marginRight: 16,
marginVertical: 0,
}}
/>
)}
</View>
)}
right={() =>
right && (
<MaterialCommunityIcons
name={right}
color={iconColor || '#47a84a'}
size={23}
style={{marginRight: 16, textAlignVertical: 'center'}}
/>
</Pressable>
)
}
onPress={onPress}
rippleColor={theme.rippleColor}
/>
);
const Icon = ({ icon, theme }) => (
<PaperList.Icon
color={theme.colorAccent}
icon={icon}
style={{ margin: 0 }}
const Divider = ({theme}) => (
<PaperDivider style={{backgroundColor: theme.dividerColor, height: 1}} />
);
const InfoItem = ({title, icon, theme}) => (
<PaperList.Item
title={title}
titleStyle={{color: theme.textColorSecondary, fontSize: 14}}
titleNumberOfLines={5}
left={() =>
icon && (
<PaperList.Icon
color={theme.textColorSecondary}
icon={icon}
style={{marginVertical: 0}}
/>
)
}
/>
);
const ColorItem = ({title, description, theme, onPress}) => (
<Pressable
style={{
padding: 16,
flexDirection: 'row',
justifyContent: 'space-between',
alignItems: 'center',
}}
android_ripple={{color: theme.rippleColor}}
onPress={onPress}
>
<View>
<Text style={{color: theme.textColorPrimary, fontSize: 16}}>{title}</Text>
<Text style={{color: theme.textColorSecondary}}>{description}</Text>
</View>
<View
style={{
backgroundColor: description,
height: 24,
width: 24,
borderRadius: 50,
marginRight: 16,
}}
/>
</Pressable>
);
const Icon = ({icon, theme}) => (
<PaperList.Icon color={theme.colorAccent} icon={icon} style={{margin: 0}} />
);
export const List = {
Section: ListSection,
SubHeader: ListSubHeader,
Item: ListItem,
Divider,
InfoItem,
ColorItem,
Icon,
Section: ListSection,
SubHeader: ListSubHeader,
Item: ListItem,
Divider,
InfoItem,
ColorItem,
Icon,
};
const styles = StyleSheet.create({
listSection: {
flex: 1,
marginVertical: 0,
},
listSubHeader: {
paddingBottom: 5,
},
listSection: {
flex: 1,
marginVertical: 0,
},
listSubHeader: {
paddingBottom: 5,
},
});
+67 -70
View File
@@ -1,80 +1,77 @@
import React from "react";
import { StyleSheet, View, Text, Pressable } from "react-native";
import React from 'react';
import {StyleSheet, View, Text, Pressable} from 'react-native';
import FastImage from "react-native-fast-image";
import FastImage from 'react-native-fast-image';
const ListView = ({
item,
downloadBadge,
unreadBadge,
inLibraryBadge,
theme,
onPress,
isSelected,
onLongPress,
item,
downloadBadge,
unreadBadge,
inLibraryBadge,
theme,
onPress,
isSelected,
onLongPress,
}) => {
return (
<Pressable
android_ripple={{ color: theme.rippleColor }}
style={[
styles.listView,
isSelected && { backgroundColor: theme.rippleColor },
]}
onPress={onPress}
onLongPress={onLongPress}
>
<FastImage
source={{
uri:
item.novelCover && !item.novelCover.startsWith("/")
? item.novelCover
: "https://github.com/LNReader/lnreader-sources/blob/main/src/coverNotAvailable.jpg?raw=true",
}}
style={[
styles.extensionIcon,
inLibraryBadge && { opacity: 0.5 },
]}
/>
<Text
style={[{ color: theme.textColorPrimary }, styles.novelName]}
numberOfLines={1}
>
{item.novelName}
</Text>
<View style={styles.badgeContainer}>
{downloadBadge}
{unreadBadge}
{inLibraryBadge}
</View>
</Pressable>
);
return (
<Pressable
android_ripple={{color: theme.rippleColor}}
style={[
styles.listView,
isSelected && {backgroundColor: theme.rippleColor},
]}
onPress={onPress}
onLongPress={onLongPress}
>
<FastImage
source={{
uri:
item.novelCover && !item.novelCover.startsWith('/')
? item.novelCover
: 'https://github.com/LNReader/lnreader-sources/blob/main/src/coverNotAvailable.jpg?raw=true',
}}
style={[styles.extensionIcon, inLibraryBadge && {opacity: 0.5}]}
/>
<Text
style={[{color: theme.textColorPrimary}, styles.novelName]}
numberOfLines={1}
>
{item.novelName}
</Text>
<View style={styles.badgeContainer}>
{downloadBadge}
{unreadBadge}
{inLibraryBadge}
</View>
</Pressable>
);
};
export default ListView;
const styles = StyleSheet.create({
listView: {
flex: 1,
flexDirection: "row",
alignItems: "center",
paddingVertical: 8,
paddingHorizontal: 12,
},
extensionIcon: {
width: 40,
height: 40,
borderRadius: 4,
},
novelName: {
flex: 1,
marginLeft: 16,
fontSize: 15,
paddingRight: 8,
flexWrap: "wrap",
},
badgeContainer: {
flexDirection: "row",
justifyContent: "flex-end",
alignItems: "flex-end",
},
listView: {
flex: 1,
flexDirection: 'row',
alignItems: 'center',
paddingVertical: 8,
paddingHorizontal: 12,
},
extensionIcon: {
width: 40,
height: 40,
borderRadius: 4,
},
novelName: {
flex: 1,
marginLeft: 16,
fontSize: 15,
paddingRight: 8,
flexWrap: 'wrap',
},
badgeContainer: {
flexDirection: 'row',
justifyContent: 'flex-end',
alignItems: 'flex-end',
},
});
@@ -1,8 +1,8 @@
import React from "react";
import { ActivityIndicator, View } from "react-native";
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>
export const LoadingScreen = ({theme}) => (
<View style={{flex: 1, justifyContent: 'center'}}>
<ActivityIndicator size={50} color={theme.colorAccent} />
</View>
);
+66 -71
View File
@@ -1,80 +1,75 @@
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 React, {useState} from 'react';
import {StyleSheet, View, Text} from 'react-native';
import {Portal, Modal, Button} from 'react-native-paper';
import * as Linking from 'expo-linking';
import {useTheme} from '../hooks/reduxHooks';
const NewUpdateDialog = ({ newVersion }) => {
const [newUpdateDialog, showNewUpdateDialog] = useState(true);
const NewUpdateDialog = ({newVersion}) => {
const [newUpdateDialog, showNewUpdateDialog] = useState(true);
const theme = useTheme();
const theme = useTheme();
return (
<Portal>
<Modal
visible={newUpdateDialog}
onDismiss={() => showNewUpdateDialog(false)}
contentContainerStyle={[
styles.containerStyle,
{ backgroundColor: theme.colorPrimaryDark },
]}
>
<Text
style={[
styles.modalHeader,
{ color: theme.textColorPrimary },
]}
>
{`New update available ${newVersion.tag_name}`}
</Text>
<Text style={{ color: theme.textColorSecondary, fontSize: 16 }}>
{newVersion.body}
</Text>
<View
style={{
flexDirection: "row",
marginTop: 30,
justifyContent: "flex-end",
}}
>
<Button
labelStyle={{
color: theme.colorAccent,
letterSpacing: 0,
textTransform: "none",
}}
theme={{ colors: { primary: theme.colorAccent } }}
onPress={() => showNewUpdateDialog(false)}
>
Cancel
</Button>
<Button
labelStyle={{
color: theme.colorAccent,
letterSpacing: 0,
textTransform: "none",
}}
theme={{ colors: { primary: theme.colorAccent } }}
onPress={() => Linking.openURL(newVersion.downloadUrl)}
>
Install
</Button>
</View>
</Modal>
</Portal>
);
return (
<Portal>
<Modal
visible={newUpdateDialog}
onDismiss={() => showNewUpdateDialog(false)}
contentContainerStyle={[
styles.containerStyle,
{backgroundColor: theme.colorPrimaryDark},
]}
>
<Text style={[styles.modalHeader, {color: theme.textColorPrimary}]}>
{`New update available ${newVersion.tag_name}`}
</Text>
<Text style={{color: theme.textColorSecondary, fontSize: 16}}>
{newVersion.body}
</Text>
<View
style={{
flexDirection: 'row',
marginTop: 30,
justifyContent: 'flex-end',
}}
>
<Button
labelStyle={{
color: theme.colorAccent,
letterSpacing: 0,
textTransform: 'none',
}}
theme={{colors: {primary: theme.colorAccent}}}
onPress={() => showNewUpdateDialog(false)}
>
Cancel
</Button>
<Button
labelStyle={{
color: theme.colorAccent,
letterSpacing: 0,
textTransform: 'none',
}}
theme={{colors: {primary: theme.colorAccent}}}
onPress={() => Linking.openURL(newVersion.downloadUrl)}
>
Install
</Button>
</View>
</Modal>
</Portal>
);
};
export default NewUpdateDialog;
const styles = StyleSheet.create({
containerStyle: {
padding: 20,
margin: 20,
borderRadius: 6,
},
modalHeader: {
fontSize: 20,
marginBottom: 10,
},
containerStyle: {
padding: 20,
margin: 20,
borderRadius: 6,
},
modalHeader: {
fontSize: 20,
marginBottom: 10,
},
});
+273 -283
View File
@@ -1,322 +1,312 @@
import React, { useCallback } from "react";
import React, {useCallback} from 'react';
import {
StyleSheet,
View,
Text,
useWindowDimensions,
Pressable,
} from "react-native";
StyleSheet,
View,
Text,
useWindowDimensions,
Pressable,
} from 'react-native';
import { LinearGradient } from "expo-linear-gradient";
import FastImage from "react-native-fast-image";
import {LinearGradient} from 'expo-linear-gradient';
import FastImage from 'react-native-fast-image';
import ListView from "./ListView";
import ListView from './ListView';
import { getDeviceOrientation } from "../services/utils/helpers";
import { useSettings } from "../hooks/reduxHooks";
import {getDeviceOrientation} from '../services/utils/helpers';
import {useSettings} from '../hooks/reduxHooks';
const NovelCover = ({
item,
onPress,
libraryStatus,
theme,
isSelected,
onLongPress,
selectedNovels,
item,
onPress,
libraryStatus,
theme,
isSelected,
onLongPress,
selectedNovels,
}) => {
const { displayMode, novelsPerRow, showDownloadBadges, showUnreadBadges } =
useSettings();
const {displayMode, novelsPerRow, showDownloadBadges, showUnreadBadges} =
useSettings();
const window = useWindowDimensions();
const window = useWindowDimensions();
const orientation = getDeviceOrientation();
const orientation = getDeviceOrientation();
const getNovelsPerRow = () =>
orientation === "landscape" ? 6 : novelsPerRow;
const getNovelsPerRow = () =>
orientation === 'landscape' ? 6 : novelsPerRow;
const getHeight = useCallback(
() => (window.width / getNovelsPerRow()) * (4 / 3),
[]
);
const getHeight = useCallback(
() => (window.width / getNovelsPerRow()) * (4 / 3),
[],
);
const selectNovel = () => onLongPress && onLongPress(item.novelId);
const selectNovel = () => onLongPress && onLongPress(item.novelId);
return displayMode !== 2 ? (
<View
return displayMode !== 2 ? (
<View
style={[
{
flex: 1 / getNovelsPerRow(),
borderRadius: 6,
overflow: 'hidden',
margin: 2,
},
isSelected && {
backgroundColor: theme.colorAccent,
opacity: 0.8,
},
]}
>
<Pressable
android_ripple={{color: theme.colorAccent}}
style={styles.opac}
onPress={
selectedNovels && selectedNovels.length > 0 ? selectNovel : onPress
}
onLongPress={selectNovel}
>
<>
<View style={styles.badgeContainer}>
{libraryStatus && <InLibraryBadge theme={theme} />}
{showDownloadBadges && item.chaptersDownloaded && (
<DownloadBadge
showUnreadBadges={showUnreadBadges}
chaptersDownloaded={item.chaptersDownloaded}
chaptersUnread={item.chaptersUnread}
/>
)}
{showUnreadBadges && item.chaptersUnread && (
<UnreadBadge
theme={theme}
chaptersDownloaded={item.chaptersDownloaded}
chaptersUnread={item.chaptersUnread}
showDownloadBadges={showDownloadBadges}
/>
)}
</View>
<FastImage
source={{
uri:
item.novelCover && !item.novelCover.startsWith('/')
? item.novelCover
: 'https://github.com/LNReader/lnreader-sources/blob/main/src/coverNotAvailable.jpg?raw=true',
}}
style={[
{
flex: 1 / getNovelsPerRow(),
borderRadius: 6,
overflow: "hidden",
margin: 2,
},
isSelected && {
backgroundColor: theme.colorAccent,
opacity: 0.8,
},
{height: getHeight(), borderRadius: 4},
libraryStatus && {opacity: 0.5},
]}
>
<Pressable
android_ripple={{ color: theme.colorAccent }}
style={styles.opac}
onPress={
selectedNovels && selectedNovels.length > 0
? selectNovel
: onPress
}
onLongPress={selectNovel}
>
<>
<View style={styles.badgeContainer}>
{libraryStatus && <InLibraryBadge theme={theme} />}
{showDownloadBadges && item.chaptersDownloaded && (
<DownloadBadge
showUnreadBadges={showUnreadBadges}
chaptersDownloaded={item.chaptersDownloaded}
chaptersUnread={item.chaptersUnread}
/>
)}
{showUnreadBadges && item.chaptersUnread && (
<UnreadBadge
theme={theme}
chaptersDownloaded={item.chaptersDownloaded}
chaptersUnread={item.chaptersUnread}
showDownloadBadges={showDownloadBadges}
/>
)}
</View>
<FastImage
source={{
uri:
item.novelCover &&
!item.novelCover.startsWith("/")
? item.novelCover
: "https://github.com/LNReader/lnreader-sources/blob/main/src/coverNotAvailable.jpg?raw=true",
}}
style={[
{ height: getHeight(), borderRadius: 4 },
libraryStatus && { opacity: 0.5 },
]}
/>
<View style={styles.compactTitleContainer}>
{displayMode === 0 && (
<CompactTitle novelName={item.novelName} />
)}
</View>
{displayMode === 1 && (
<ComfortableTitle
novelName={item.novelName}
theme={theme}
/>
)}
</>
</Pressable>
</View>
) : (
<ListView
item={item}
downloadBadge={
showDownloadBadges &&
item.chaptersDownloaded && (
<DownloadBadge
showUnreadBadges={showUnreadBadges}
chaptersDownloaded={item.chaptersDownloaded}
chaptersUnread={item.chaptersUnread}
/>
)
}
unreadBadge={
showUnreadBadges &&
item.chaptersUnread && (
<UnreadBadge
theme={theme}
chaptersDownloaded={item.chaptersDownloaded}
chaptersUnread={item.chaptersUnread}
showDownloadBadges={showDownloadBadges}
/>
)
}
inLibraryBadge={libraryStatus && <InLibraryBadge theme={theme} />}
/>
<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}
onPress={
selectedNovels && selectedNovels.length > 0
? selectNovel
: onPress
}
onLongPress={selectNovel}
isSelected={isSelected}
/>
);
chaptersDownloaded={item.chaptersDownloaded}
chaptersUnread={item.chaptersUnread}
showDownloadBadges={showDownloadBadges}
/>
)
}
inLibraryBadge={libraryStatus && <InLibraryBadge theme={theme} />}
theme={theme}
onPress={
selectedNovels && selectedNovels.length > 0 ? selectNovel : onPress
}
onLongPress={selectNovel}
isSelected={isSelected}
/>
);
};
export default NovelCover;
const ComfortableTitle = ({ theme, novelName }) => (
<Text
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: theme.textColorPrimary,
padding: 4,
},
styles.title,
{
color: 'rgba(255,255,255,1)',
textShadowColor: 'rgba(0, 0, 0, 0.75)',
textShadowOffset: {width: -1, height: 1},
textShadowRadius: 10,
},
]}
>
>
{novelName}
</Text>
</Text>
</LinearGradient>
</View>
);
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)",
textShadowColor: "rgba(0, 0, 0, 0.75)",
textShadowOffset: { width: -1, height: 1 },
textShadowRadius: 10,
},
]}
>
{novelName}
</Text>
</LinearGradient>
</View>
);
const InLibraryBadge = ({ theme }) => (
<Text
style={[
styles.inLibraryBadge,
{
backgroundColor: theme.colorAccent,
color: theme.colorButtonText,
borderRadius: 4,
},
]}
>
In library
</Text>
const InLibraryBadge = ({theme}) => (
<Text
style={[
styles.inLibraryBadge,
{
backgroundColor: theme.colorAccent,
color: theme.colorButtonText,
borderRadius: 4,
},
]}
>
In library
</Text>
);
const UnreadBadge = ({
chaptersDownloaded,
chaptersUnread,
showDownloadBadges,
theme,
chaptersDownloaded,
chaptersUnread,
showDownloadBadges,
theme,
}) => (
<Text
style={[
styles.unreadBadge,
!chaptersDownloaded && {
borderTopLeftRadius: 4,
borderBottomLeftRadius: 4,
},
!showDownloadBadges && {
borderRadius: 4,
},
{
backgroundColor: theme.colorAccent,
color: theme.colorButtonText,
},
]}
>
{chaptersUnread}
</Text>
<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,
chaptersDownloaded,
showUnreadBadges,
chaptersUnread,
}) => (
<Text
style={[
styles.downloadBadge,
!chaptersUnread && {
borderTopRightRadius: 4,
borderBottomRightRadius: 4,
},
!showUnreadBadges && {
borderRadius: 4,
},
]}
>
{chaptersDownloaded}
</Text>
<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: {
borderRadius: 4,
},
opac: {
padding: 4.8,
borderRadius: 4,
flex: 1,
},
extensionIcon: {
width: 42,
height: 42,
borderRadius: 4,
},
listView: {
flex: 1,
paddingVertical: 8,
paddingHorizontal: 12,
flexDirection: "row",
alignItems: "center",
borderRadius: 4,
},
titleContainer: {
flex: 1,
borderRadius: 4,
},
title: {
fontFamily: 'pt-sans-bold',
fontSize: 14,
padding: 8,
},
linearGradient: {
borderRadius: 4,
},
opac: {
padding: 4.8,
borderRadius: 4,
flex: 1,
},
extensionIcon: {
width: 42,
height: 42,
borderRadius: 4,
},
listView: {
flex: 1,
paddingVertical: 8,
paddingHorizontal: 12,
flexDirection: 'row',
alignItems: 'center',
borderRadius: 4,
},
downloadBadge: {
backgroundColor: "#2979FF",
color: "#FFFFFF",
borderTopLeftRadius: 4,
borderBottomLeftRadius: 4,
paddingTop: 2,
paddingHorizontal: 4,
fontSize: 12,
},
unreadBadge: {
color: "#FFFFFF",
borderTopRightRadius: 4,
borderBottomRightRadius: 4,
paddingTop: 2,
paddingHorizontal: 4,
fontSize: 12,
},
inLibraryBadge: {
paddingVertical: 2,
paddingHorizontal: 4,
fontSize: 12,
},
compactTitleContainer: {
position: "absolute",
bottom: 4,
left: 4,
right: 4,
},
badgeContainer: {
position: "absolute",
zIndex: 1,
top: 8,
left: 8,
flexDirection: "row",
},
downloadBadge: {
backgroundColor: '#2979FF',
color: '#FFFFFF',
borderTopLeftRadius: 4,
borderBottomLeftRadius: 4,
paddingTop: 2,
paddingHorizontal: 4,
fontSize: 12,
},
unreadBadge: {
color: '#FFFFFF',
borderTopRightRadius: 4,
borderBottomRightRadius: 4,
paddingTop: 2,
paddingHorizontal: 4,
fontSize: 12,
},
inLibraryBadge: {
paddingVertical: 2,
paddingHorizontal: 4,
fontSize: 12,
},
compactTitleContainer: {
position: 'absolute',
bottom: 4,
left: 4,
right: 4,
},
badgeContainer: {
position: 'absolute',
zIndex: 1,
top: 8,
left: 8,
flexDirection: 'row',
},
});
+46 -46
View File
@@ -1,61 +1,61 @@
import React, { useCallback } from "react";
import { StyleSheet, FlatList } from "react-native";
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,
data,
onScroll,
onEndReached,
renderItem,
refreshControl,
ListEmptyComponent,
ListFooterComponent,
}) => {
const { displayMode, novelsPerRow } = useSettings();
const {displayMode, novelsPerRow} = useSettings();
const orientation = getDeviceOrientation();
const orientation = getDeviceOrientation();
const getNovelsPerRow = () => {
if (displayMode === 2) {
return 1;
}
const getNovelsPerRow = () => {
if (displayMode === 2) {
return 1;
}
if (orientation === "landscape") {
return 6;
} else {
return novelsPerRow;
}
};
if (orientation === 'landscape') {
return 6;
} else {
return novelsPerRow;
}
};
const keyExtractor = useCallback((item) => item.novelUrl, []);
const keyExtractor = useCallback(item => item.novelUrl, []);
return (
<FlatList
contentContainerStyle={[
styles.flatListCont,
displayMode !== 2 && { paddingHorizontal: 4 },
]}
numColumns={getNovelsPerRow()}
key={[orientation, getNovelsPerRow()]}
data={data}
keyExtractor={keyExtractor}
renderItem={renderItem}
refreshControl={refreshControl}
ListEmptyComponent={ListEmptyComponent}
ListFooterComponent={ListFooterComponent}
onScroll={onScroll}
onEndReached={onEndReached}
/>
);
return (
<FlatList
contentContainerStyle={[
styles.flatListCont,
displayMode !== 2 && {paddingHorizontal: 4},
]}
numColumns={getNovelsPerRow()}
key={[orientation, getNovelsPerRow()]}
data={data}
keyExtractor={keyExtractor}
renderItem={renderItem}
refreshControl={refreshControl}
ListEmptyComponent={ListEmptyComponent}
ListFooterComponent={ListFooterComponent}
onScroll={onScroll}
onEndReached={onEndReached}
/>
);
};
export default NovelList;
const styles = StyleSheet.create({
flatListCont: {
flexGrow: 1,
paddingVertical: 8,
},
flatListCont: {
flexGrow: 1,
paddingBottom: 56,
},
});
+33 -33
View File
@@ -1,40 +1,40 @@
import React from "react";
import { StyleSheet, View, Text } from "react-native";
import { RadioButton as MaterialRadioButton } from "react-native-paper";
import React from 'react';
import {StyleSheet, View, Text} from 'react-native';
import {RadioButton as MaterialRadioButton} from 'react-native-paper';
export const RadioButtonGroup = ({ children, value, onValueChange }) => (
<MaterialRadioButton.Group onValueChange={onValueChange} value={value}>
{children}
</MaterialRadioButton.Group>
export const RadioButtonGroup = ({children, value, onValueChange}) => (
<MaterialRadioButton.Group onValueChange={onValueChange} value={value}>
{children}
</MaterialRadioButton.Group>
);
export const RadioButton = ({ value, label, theme, labelStyle }) => (
<View style={styles.radioButtonContainer}>
<MaterialRadioButton
value={value}
color={theme.colorAccent}
uncheckedColor={theme.textColorSecondary}
/>
<Text
style={[
styles.radioButtonLabel,
{ color: theme.textColorPrimary },
labelStyle,
]}
>
{label}
</Text>
</View>
export const RadioButton = ({value, label, theme, labelStyle}) => (
<View style={styles.radioButtonContainer}>
<MaterialRadioButton
value={value}
color={theme.colorAccent}
uncheckedColor={theme.textColorSecondary}
/>
<Text
style={[
styles.radioButtonLabel,
{color: theme.textColorPrimary},
labelStyle,
]}
>
{label}
</Text>
</View>
);
const styles = StyleSheet.create({
radioButtonContainer: {
flexDirection: "row",
alignItems: "center",
paddingVertical: 8,
},
radioButtonLabel: {
marginLeft: 16,
fontSize: 16,
},
radioButtonContainer: {
flexDirection: 'row',
alignItems: 'center',
paddingVertical: 8,
},
radioButtonLabel: {
marginLeft: 16,
fontSize: 16,
},
});
+87 -88
View File
@@ -1,97 +1,96 @@
import React, { useRef } from "react";
import { StyleSheet, View, TextInput, Text, StatusBar } from "react-native";
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 {TouchableRipple, IconButton} from 'react-native-paper';
import {Row} from '../Common';
export const Searchbar = ({
theme,
placeholder,
searchText,
backAction,
onBackAction,
clearSearchbar,
onChangeText,
onSubmitEditing,
actions,
theme,
placeholder,
searchText,
backAction,
onBackAction,
clearSearchbar,
onChangeText,
onSubmitEditing,
actions,
}) => {
const searchRef = useRef(null);
const searchRef = useRef(null);
return (
<TouchableRipple
borderless
onPress={() => searchRef.current.focus()}
style={[
styles.searchbarContainer,
{ backgroundColor: theme.searchBarColor },
]}
rippleColor={theme.rippleColor}
>
<View style={styles.container}>
<Row style={{ flex: 1 }}>
{backAction && (
<IconButton
icon={backAction}
color={theme.textColorPrimary}
style={{ marginLeft: 0 }}
size={23}
onPress={onBackAction}
/>
)}
<TextInput
ref={searchRef}
style={{
fontSize: 16,
color: theme.textColorPrimary,
}}
placeholder={placeholder}
placeholderTextColor={theme.textColorSecondary}
onChangeText={onChangeText}
onSubmitEditing={onSubmitEditing}
defaultValue={searchText}
/>
</Row>
{searchText !== "" && (
<IconButton
icon="close"
color={theme.textColorPrimary}
style={{ marginRight: 0 }}
size={23}
onPress={clearSearchbar}
/>
)}
{actions &&
actions.map((action, index) => (
<IconButton
key={index}
icon={action.icon}
color={action.color ?? theme.textColorPrimary}
style={{ marginRight: 0 }}
size={23}
onPress={action.onPress}
/>
))}
</View>
</TouchableRipple>
);
return (
<TouchableRipple
borderless
onPress={() => searchRef.current.focus()}
style={[
styles.searchbarContainer,
{backgroundColor: theme.searchBarColor},
]}
rippleColor={theme.rippleColor}
>
<View style={styles.container}>
<Row style={{flex: 1}}>
{backAction && (
<IconButton
icon={backAction}
color={theme.textColorPrimary}
style={{marginLeft: 0}}
size={23}
onPress={onBackAction}
/>
)}
<TextInput
ref={searchRef}
style={{
fontSize: 16,
color: theme.textColorPrimary,
}}
placeholder={placeholder}
placeholderTextColor={theme.textColorSecondary}
onChangeText={onChangeText}
onSubmitEditing={onSubmitEditing}
defaultValue={searchText}
/>
</Row>
{searchText !== '' && (
<IconButton
icon="close"
color={theme.textColorPrimary}
style={{marginRight: 0}}
size={23}
onPress={clearSearchbar}
/>
)}
{actions &&
actions.map((action, index) => (
<IconButton
key={index}
icon={action.icon}
color={action.color ?? theme.textColorPrimary}
style={{marginRight: 0}}
size={23}
onPress={action.onPress}
/>
))}
</View>
</TouchableRipple>
);
};
const styles = StyleSheet.create({
searchbarContainer: {
marginTop: StatusBar.currentHeight + 8,
height: 46,
flexDirection: "row",
alignItems: "center",
paddingHorizontal: 12,
marginBottom: 6,
borderRadius: 8,
marginHorizontal: 12,
elevation: 2,
},
container: {
flex: 1,
flexDirection: "row",
justifyContent: "space-between",
alignItems: "center",
},
searchbarContainer: {
marginTop: StatusBar.currentHeight + 8,
flexDirection: 'row',
alignItems: 'center',
paddingHorizontal: 12,
marginBottom: 8,
borderRadius: 50,
marginHorizontal: 16,
elevation: 1,
},
container: {
flex: 1,
flexDirection: 'row',
justifyContent: 'space-between',
alignItems: 'center',
},
});
+42 -48
View File
@@ -1,56 +1,50 @@
import React from "react";
import { Pressable, StyleSheet, View, Text } from "react-native";
import React from 'react';
import {Pressable, StyleSheet, View, Text} from 'react-native';
import { Switch, List } from "react-native-paper";
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>
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",
},
container: {
paddingHorizontal: 16,
paddingVertical: 12,
flexDirection: 'row',
justifyContent: 'space-between',
},
});
+159 -159
View File
@@ -1,172 +1,172 @@
import React from "react";
import { View, Text, StyleSheet, Pressable } from "react-native";
import MaterialCommunityIcons from "react-native-vector-icons/MaterialCommunityIcons";
import React from 'react';
import {View, Text, StyleSheet, Pressable} from 'react-native';
import MaterialCommunityIcons from 'react-native-vector-icons/MaterialCommunityIcons';
export const ThemePicker = ({ theme, currentTheme, onPress }) => (
export const ThemePicker = ({theme, currentTheme, onPress}) => (
<View
style={{
justifyContent: 'center',
alignItems: 'center',
marginRight: 16,
}}
>
<View
style={{
justifyContent: "center",
alignItems: "center",
marginRight: 16,
}}
style={{
backgroundColor: theme.colorPrimaryDark,
borderWidth: 3.6,
borderColor:
currentTheme.id == theme.id
? theme.colorAccent
: currentTheme.colorPrimaryDark,
width: 95,
height: 140,
borderRadius: 16,
overflow: 'hidden',
elevation: 1,
}}
>
<Pressable style={{flex: 1}} onPress={onPress}>
{currentTheme.id == theme.id && (
<MaterialCommunityIcons
name="check"
color={theme.colorButtonText}
size={15}
style={{
backgroundColor: theme.colorAccent,
position: 'absolute',
top: 5,
right: 5,
elevation: 2,
borderRadius: 50,
padding: 1.6,
}}
/>
)}
<View
style={{
backgroundColor: theme.colorPrimaryDark,
borderWidth: 3.6,
borderColor:
currentTheme.id == theme.id
? theme.colorAccent
: currentTheme.colorPrimaryDark,
width: 95,
height: 140,
borderRadius: 16,
overflow: "hidden",
elevation: 1,
}}
style={{
height: 20,
backgroundColor: theme.colorPrimary,
elevation: 1,
justifyContent: 'center',
}}
>
<Pressable style={{ flex: 1 }} onPress={onPress}>
{currentTheme.id == theme.id && (
<MaterialCommunityIcons
name="check"
color={theme.colorButtonText}
size={15}
style={{
backgroundColor: theme.colorAccent,
position: "absolute",
top: 5,
right: 5,
elevation: 2,
borderRadius: 50,
padding: 1.6,
}}
/>
)}
<View
style={{
height: 20,
backgroundColor: theme.colorPrimary,
elevation: 1,
justifyContent: "center",
}}
>
<View
style={{
backgroundColor: theme.textColorPrimary,
width: 44,
height: 10,
marginLeft: 8,
borderRadius: 50,
}}
/>
</View>
<View style={{ padding: 8 }}>
<View
style={{
height: 18,
backgroundColor: theme.textColorSecondary,
borderRadius: 4,
}}
/>
<View style={{ paddingVertical: 4, flexDirection: "row" }}>
<View
style={{
height: 10,
width: 44,
backgroundColor: theme.textColorPrimary,
borderRadius: 50,
}}
/>
<View
style={{
height: 10,
width: 16,
marginLeft: 4,
backgroundColor: theme.colorAccent,
borderRadius: 50,
}}
/>
</View>
<View style={{ flexDirection: "row" }}>
<View
style={{
height: 10,
width: 24,
backgroundColor: theme.textColorSecondary,
borderRadius: 50,
}}
/>
<View
style={{
height: 10,
width: 24,
backgroundColor: theme.textColorSecondary,
borderRadius: 50,
marginLeft: 4,
}}
/>
</View>
</View>
<View
style={{
position: "absolute",
bottom: 0,
left: 0,
right: 0,
height: 24,
backgroundColor: theme.colorPrimary,
justifyContent: "center",
}}
>
<View
style={{
flex: 1,
justifyContent: "space-between",
alignItems: "center",
flexDirection: "row",
paddingHorizontal: 16,
}}
>
<View
style={{
height: 12,
width: 12,
borderRadius: 50,
backgroundColor: theme.textColorPrimary,
opacity: 0.54,
}}
/>
<View
style={{
height: 12,
width: 12,
borderRadius: 50,
backgroundColor: theme.colorAccent,
}}
/>
<View
style={{
height: 12,
width: 12,
borderRadius: 50,
backgroundColor: theme.textColorPrimary,
opacity: 0.54,
}}
/>
</View>
</View>
</Pressable>
<View
style={{
backgroundColor: theme.textColorPrimary,
width: 44,
height: 10,
marginLeft: 8,
borderRadius: 50,
}}
/>
</View>
<Text
<View style={{padding: 8}}>
<View
style={{
color: currentTheme.textColorPrimary,
fontSize: 12,
paddingVertical: 4,
height: 18,
backgroundColor: theme.textColorSecondary,
borderRadius: 4,
}}
/>
<View style={{paddingVertical: 4, flexDirection: 'row'}}>
<View
style={{
height: 10,
width: 44,
backgroundColor: theme.textColorPrimary,
borderRadius: 50,
}}
/>
<View
style={{
height: 10,
width: 16,
marginLeft: 4,
backgroundColor: theme.colorAccent,
borderRadius: 50,
}}
/>
</View>
<View style={{flexDirection: 'row'}}>
<View
style={{
height: 10,
width: 24,
backgroundColor: theme.textColorSecondary,
borderRadius: 50,
}}
/>
<View
style={{
height: 10,
width: 24,
backgroundColor: theme.textColorSecondary,
borderRadius: 50,
marginLeft: 4,
}}
/>
</View>
</View>
<View
style={{
position: 'absolute',
bottom: 0,
left: 0,
right: 0,
height: 24,
backgroundColor: theme.colorPrimary,
justifyContent: 'center',
}}
>
{theme.name}
</Text>
<View
style={{
flex: 1,
justifyContent: 'space-between',
alignItems: 'center',
flexDirection: 'row',
paddingHorizontal: 16,
}}
>
<View
style={{
height: 12,
width: 12,
borderRadius: 50,
backgroundColor: theme.textColorPrimary,
opacity: 0.54,
}}
/>
<View
style={{
height: 12,
width: 12,
borderRadius: 50,
backgroundColor: theme.colorAccent,
}}
/>
<View
style={{
height: 12,
width: 12,
borderRadius: 50,
backgroundColor: theme.textColorPrimary,
opacity: 0.54,
}}
/>
</View>
</View>
</Pressable>
</View>
<Text
style={{
color: currentTheme.textColorPrimary,
fontSize: 12,
paddingVertical: 4,
}}
>
{theme.name}
</Text>
</View>
);
const styles = StyleSheet.create({});
+37 -37
View File
@@ -1,55 +1,55 @@
import * as SQLite from "expo-sqlite";
import * as SQLite from 'expo-sqlite';
import {
createNovelTableQuery,
createUrlIndexQuery,
createLibraryIndexQuery,
} from "./tables/NovelTable";
createNovelTableQuery,
createUrlIndexQuery,
createLibraryIndexQuery,
} from './tables/NovelTable';
import {
createChapterTableQuery,
createNovelIdIndexQuery,
createUnreadChaptersIndexQuery,
} from "./tables/ChapterTable";
createChapterTableQuery,
createNovelIdIndexQuery,
createUnreadChaptersIndexQuery,
} from './tables/ChapterTable';
import {
createHistoryTableQuery,
createChapterIdIndexQuery,
} from "./tables/HistoryTable";
import { createDownloadTableQuery } from "./tables/DownloadTable";
createHistoryTableQuery,
createChapterIdIndexQuery,
} from './tables/HistoryTable';
import {createDownloadTableQuery} from './tables/DownloadTable';
import {
createUpdatesTableQuery,
updatesSeedDataQuery,
} from "./tables/UpdateTable";
createUpdatesTableQuery,
updatesSeedDataQuery,
} from './tables/UpdateTable';
/**
* Database Version = 2
*/
const dbName = "lnreader.db";
const dbName = 'lnreader.db';
const db = SQLite.openDatabase(dbName);
export const createDB = () => {
db.transaction((tx) => {
tx.executeSql(createNovelTableQuery);
tx.executeSql(createChapterTableQuery);
tx.executeSql(createHistoryTableQuery);
tx.executeSql(createDownloadTableQuery);
tx.executeSql(createUpdatesTableQuery);
db.transaction(tx => {
tx.executeSql(createNovelTableQuery);
tx.executeSql(createChapterTableQuery);
tx.executeSql(createHistoryTableQuery);
tx.executeSql(createDownloadTableQuery);
tx.executeSql(createUpdatesTableQuery);
//Db indexes
tx.executeSql(createUrlIndexQuery);
tx.executeSql(createLibraryIndexQuery);
tx.executeSql(createNovelIdIndexQuery);
tx.executeSql(createUnreadChaptersIndexQuery);
tx.executeSql(createChapterIdIndexQuery);
});
//Db indexes
tx.executeSql(createUrlIndexQuery);
tx.executeSql(createLibraryIndexQuery);
tx.executeSql(createNovelIdIndexQuery);
tx.executeSql(createUnreadChaptersIndexQuery);
tx.executeSql(createChapterIdIndexQuery);
});
};
export const deleteDb = () => {
db.transaction((tx) => {
tx.executeSql("DROP TABLE novels");
tx.executeSql("DROP TABLE chapters");
tx.executeSql("DROP TABLE history");
tx.executeSql("DROP TABLE downloads");
tx.executeSql("DROP TABLE updates");
});
db.transaction(tx => {
tx.executeSql('DROP TABLE novels');
tx.executeSql('DROP TABLE chapters');
tx.executeSql('DROP TABLE history');
tx.executeSql('DROP TABLE downloads');
tx.executeSql('DROP TABLE updates');
});
};
+221 -222
View File
@@ -1,224 +1,223 @@
import * as SQLite from "expo-sqlite";
import { getSource } from "../../sources/sources";
const db = SQLite.openDatabase("lnreader.db");
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 (?, ?, ?, ?)`;
export const insertChapters = async (novelId, chapters) => {
db.transaction((tx) => {
chapters.map(
(chapter) =>
tx.executeSql(
insertChaptersQuery,
[
chapter.chapterUrl,
chapter.chapterName,
chapter.releaseDate,
novelId,
],
(txObj, res) => {},
(txObj, error) => {}
// console.log(
// "Error ",
// error,
// " Chapter URL: ",
// chapter.chapterUrl,
// " Novel ID ",
// novelId
// )
),
(txObj, res) => {
console.log("Success");
},
(txObj, error) => console.log("Error ", error)
);
});
db.transaction(tx => {
chapters.map(
chapter =>
tx.executeSql(
insertChaptersQuery,
[
chapter.chapterUrl,
chapter.chapterName,
chapter.releaseDate,
novelId,
],
(txObj, res) => {},
(txObj, error) => {},
// console.log(
// "Error ",
// error,
// " Chapter URL: ",
// chapter.chapterUrl,
// " Novel ID ",
// novelId
// )
),
(txObj, res) => {
console.log('Success');
},
(txObj, error) => console.log('Error ', error),
);
});
};
const getChaptersQuery = (sort = "ORDER BY chapterId ASC", filter = "") =>
`SELECT * FROM chapters WHERE novelId = ? ${filter} ${sort}`;
const getChaptersQuery = (sort = 'ORDER BY chapterId ASC', filter = '') =>
`SELECT * FROM chapters WHERE novelId = ? ${filter} ${sort}`;
export const getChapters = (novelId, sort, filter) => {
return new Promise((resolve, reject) =>
db.transaction((tx) => {
tx.executeSql(
getChaptersQuery(sort, filter),
[novelId],
(txObj, { rows: { _array } }) => {
resolve(_array);
},
(txObj, error) => console.log("Error ", error)
);
})
);
return new Promise((resolve, reject) =>
db.transaction(tx => {
tx.executeSql(
getChaptersQuery(sort, filter),
[novelId],
(txObj, {rows: {_array}}) => {
resolve(_array);
},
(txObj, error) => console.log('Error ', error),
);
}),
);
};
const getChapterQuery = `SELECT * FROM downloads WHERE downloadChapterId = ?`;
export const getChapterFromDB = async (chapterId) => {
return new Promise((resolve, reject) =>
db.transaction((tx) => {
tx.executeSql(
getChapterQuery,
[chapterId],
(tx, results) => {
resolve(results.rows.item(0));
},
(txObj, error) => console.log("Error ", error)
);
})
);
export const getChapterFromDB = async chapterId => {
return new Promise((resolve, reject) =>
db.transaction(tx => {
tx.executeSql(
getChapterQuery,
[chapterId],
(tx, results) => {
resolve(results.rows.item(0));
},
(txObj, error) => console.log('Error ', error),
);
}),
);
};
export const getNextChapterFromDB = async (novelId, chapterId) => {
return new Promise((resolve, reject) =>
db.transaction((tx) => {
tx.executeSql(
`SELECT * FROM chapters WHERE novelId = ? AND chapterId > ?`,
[novelId, chapterId],
(tx, results) => {
resolve(results.rows.item(0));
},
(txObj, error) => console.log("Error ", error)
);
})
);
return new Promise((resolve, reject) =>
db.transaction(tx => {
tx.executeSql(
`SELECT * FROM chapters WHERE novelId = ? AND chapterId > ?`,
[novelId, chapterId],
(tx, results) => {
resolve(results.rows.item(0));
},
(txObj, error) => console.log('Error ', error),
);
}),
);
};
export const getPrevChapterFromDB = async (novelId, chapterId) => {
return new Promise((resolve, reject) =>
db.transaction((tx) => {
tx.executeSql(
`SELECT * FROM chapters WHERE novelId = ? AND chapterId < ?`,
[novelId, chapterId],
(tx, results) => {
resolve(results.rows.item(results.rows.length - 1));
},
(txObj, error) => console.log("Error ", error)
);
})
);
return new Promise((resolve, reject) =>
db.transaction(tx => {
tx.executeSql(
`SELECT * FROM chapters WHERE novelId = ? AND chapterId < ?`,
[novelId, chapterId],
(tx, results) => {
resolve(results.rows.item(results.rows.length - 1));
},
(txObj, error) => console.log('Error ', error),
);
}),
);
};
const markChapterReadQuery = `UPDATE chapters SET \`read\` = 1 WHERE chapterId = ?`;
export const markChapterRead = async (chapterId) => {
db.transaction((tx) => {
tx.executeSql(
markChapterReadQuery,
[chapterId],
(tx, res) => {},
(tx, error) => console.log(error)
);
});
export const markChapterRead = async chapterId => {
db.transaction(tx => {
tx.executeSql(
markChapterReadQuery,
[chapterId],
(tx, res) => {},
(tx, error) => console.log(error),
);
});
};
const markChapterUnreadQuery = `UPDATE chapters SET \`read\` = 0 WHERE chapterId = ?`;
export const markChapterUnread = async (chapterId) => {
db.transaction((tx) => {
tx.executeSql(
markChapterUnreadQuery,
[chapterId],
(tx, res) => {},
(tx, error) => console.log(error)
);
});
export const markChapterUnread = async chapterId => {
db.transaction(tx => {
tx.executeSql(
markChapterUnreadQuery,
[chapterId],
(tx, res) => {},
(tx, error) => console.log(error),
);
});
};
const markAllChaptersReadQuery = `UPDATE chapters SET \`read\` = 1 WHERE novelId = ?`;
export const markAllChaptersRead = async (novelId) => {
db.transaction((tx) => {
tx.executeSql(
markAllChaptersReadQuery,
[novelId],
(tx, res) => {},
(tx, error) => console.log(error)
);
});
export const markAllChaptersRead = async novelId => {
db.transaction(tx => {
tx.executeSql(
markAllChaptersReadQuery,
[novelId],
(tx, res) => {},
(tx, error) => console.log(error),
);
});
};
const markAllChaptersUnreadQuery = `UPDATE chapters SET \`read\` = 0 WHERE novelId = ?`;
export const markAllChaptersUnread = async (novelId) => {
db.transaction((tx) => {
tx.executeSql(
markAllChaptersUnreadQuery,
[novelId],
(tx, res) => {},
(tx, error) => console.log(error)
);
});
export const markAllChaptersUnread = async novelId => {
db.transaction(tx => {
tx.executeSql(
markAllChaptersUnreadQuery,
[novelId],
(tx, res) => {},
(tx, error) => console.log(error),
);
});
};
const isChapterDownloadedQuery = `SELECT * FROM downloads WHERE downloadChapterId=?`;
export const isChapterDownloaded = async (chapterId) => {
return new Promise((resolve, reject) =>
db.transaction((tx) => {
tx.executeSql(
isChapterDownloadedQuery,
[chapterId],
(txObj, res) => {
if (res.rows.length !== 0) {
resolve(true);
} else {
resolve(false);
}
},
(txObj, error) => console.log("Error ", error)
);
})
);
export const isChapterDownloaded = async chapterId => {
return new Promise((resolve, reject) =>
db.transaction(tx => {
tx.executeSql(
isChapterDownloadedQuery,
[chapterId],
(txObj, res) => {
if (res.rows.length !== 0) {
resolve(true);
} else {
resolve(false);
}
},
(txObj, error) => console.log('Error ', error),
);
}),
);
};
const downloadChapterQuery = `INSERT INTO downloads (downloadChapterId, chapterName, chapterText) VALUES (?, ?, ?)`;
export const downloadChapter = async (
sourceId,
novelUrl,
chapterUrl,
chapterId
sourceId,
novelUrl,
chapterUrl,
chapterId,
) => {
const source = getSource(sourceId);
const source = getSource(sourceId);
const chapter = await source.parseChapter(novelUrl, chapterUrl);
const chapter = await source.parseChapter(novelUrl, chapterUrl);
db.transaction((tx) => {
tx.executeSql(
`UPDATE chapters SET downloaded = 1 WHERE chapterId = ?`,
[chapterId]
);
tx.executeSql(
downloadChapterQuery,
[chapterId, chapter.chapterName, chapter.chapterText],
(tx, res) => {
// console.log(`Downloaded Chapter ${chapter.chapterUrl}`);
},
(txObj, error) => console.log("Error ", error)
);
});
db.transaction(tx => {
tx.executeSql(`UPDATE chapters SET downloaded = 1 WHERE chapterId = ?`, [
chapterId,
]);
tx.executeSql(
downloadChapterQuery,
[chapterId, chapter.chapterName, chapter.chapterText],
(tx, res) => {
// console.log(`Downloaded Chapter ${chapter.chapterUrl}`);
},
(txObj, error) => console.log('Error ', error),
);
});
};
export const deleteChapter = async (chapterId) => {
const updateIsDownloadedQuery = `UPDATE chapters SET downloaded = 0 WHERE chapterId=?`;
const deleteChapterQuery = `DELETE FROM downloads WHERE downloadChapterId=?`;
export const deleteChapter = async chapterId => {
const updateIsDownloadedQuery = `UPDATE chapters SET downloaded = 0 WHERE chapterId=?`;
const deleteChapterQuery = `DELETE FROM downloads WHERE downloadChapterId=?`;
db.transaction((tx) => {
tx.executeSql(
updateIsDownloadedQuery,
[chapterId],
(tx, res) => {},
(txObj, error) => console.log("Error ", error)
);
tx.executeSql(
deleteChapterQuery,
[chapterId],
(tx, res) => console.log(`Chapter deleted`),
(txObj, error) => console.log("Error ", error)
);
});
db.transaction(tx => {
tx.executeSql(
updateIsDownloadedQuery,
[chapterId],
(tx, res) => {},
(txObj, error) => console.log('Error ', error),
);
tx.executeSql(
deleteChapterQuery,
[chapterId],
(tx, res) => console.log(`Chapter deleted`),
(txObj, error) => console.log('Error ', error),
);
});
};
const getLastReadChapterQuery = `
@@ -229,56 +228,56 @@ const getLastReadChapterQuery = `
WHERE history.historyNovelId = ?
`;
export const getLastReadChapter = async (novelId) => {
return new Promise((resolve, reject) => {
db.transaction((tx) => {
tx.executeSql(
getLastReadChapterQuery,
[novelId],
(tx, { rows }) => resolve(rows.item(0)),
(txObj, error) => console.log("Error ", error)
);
});
export const getLastReadChapter = async novelId => {
return new Promise((resolve, reject) => {
db.transaction(tx => {
tx.executeSql(
getLastReadChapterQuery,
[novelId],
(tx, {rows}) => resolve(rows.item(0)),
(txObj, error) => console.log('Error ', error),
);
});
});
};
const bookmarkChapterQuery = `UPDATE chapters SET bookmark = ? WHERE chapterId = ?`;
export const bookmarkChapter = async (bookmark, chapterId) => {
db.transaction((tx) => {
tx.executeSql(
bookmarkChapterQuery,
[!bookmark, chapterId],
(tx, res) => {},
(txObj, error) => console.log("Error ", error)
);
});
db.transaction(tx => {
tx.executeSql(
bookmarkChapterQuery,
[!bookmark, chapterId],
(tx, res) => {},
(txObj, error) => console.log('Error ', error),
);
});
};
const markPreviuschaptersReadQuery = `UPDATE chapters SET \`read\` = 1 WHERE chapterId < ? AND novelId = ?`;
export const markPreviuschaptersRead = async (chapterId, novelId) => {
db.transaction((tx) => {
tx.executeSql(
markPreviuschaptersReadQuery,
[chapterId, novelId],
(tx, res) => {},
(tx, error) => console.log(error)
);
});
db.transaction(tx => {
tx.executeSql(
markPreviuschaptersReadQuery,
[chapterId, novelId],
(tx, res) => {},
(tx, error) => console.log(error),
);
});
};
const markPreviousChaptersUnreadQuery = `UPDATE chapters SET \`read\` = 0 WHERE chapterId < ? AND novelId = ?`;
export const markPreviousChaptersUnread = async (chapterId, novelId) => {
db.transaction((tx) => {
tx.executeSql(
markPreviousChaptersUnreadQuery,
[chapterId, novelId],
(tx, res) => {},
(tx, error) => console.log(error)
);
});
db.transaction(tx => {
tx.executeSql(
markPreviousChaptersUnreadQuery,
[chapterId, novelId],
(tx, res) => {},
(tx, error) => console.log(error),
);
});
};
const getDownloadedChaptersQuery = `
@@ -289,23 +288,23 @@ const getDownloadedChaptersQuery = `
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)
);
})
);
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;");
});
db.transaction(tx => {
tx.executeSql(`UPDATE chapters SET downloaded = 0`);
tx.executeSql('DELETE FROM downloads; VACCUM;');
});
};
+34 -36
View File
@@ -1,5 +1,5 @@
import * as SQLite from "expo-sqlite";
const db = SQLite.openDatabase("lnreader.db");
import * as SQLite from 'expo-sqlite';
const db = SQLite.openDatabase('lnreader.db');
const getHistoryQuery = `
SELECT history.*, chapters.*, novels.*
@@ -14,49 +14,47 @@ const getHistoryQuery = `
`;
export const getHistoryFromDb = async () => {
return new Promise((resolve, reject) => {
db.transaction((tx) => {
tx.executeSql(
getHistoryQuery,
null,
(txObj, { rows: { _array } }) => {
resolve(_array);
},
(txObj, error) => console.log("Error ", error)
);
});
return new Promise((resolve, reject) => {
db.transaction(tx => {
tx.executeSql(
getHistoryQuery,
null,
(txObj, {rows: {_array}}) => {
resolve(_array);
},
(txObj, error) => console.log('Error ', error),
);
});
});
};
// const insertHistoryQuery =
export const insertHistory = async (novelId, chapterId) => {
db.transaction((tx) => {
tx.executeSql(
"INSERT OR REPLACE INTO history (historyNovelId, historyChapterId, historyTimeRead) VALUES (?, ?, (datetime('now','localtime')))",
[novelId, chapterId],
(txObj, res) => {},
(txObj, error) => console.log("Error ", error)
);
tx.executeSql("UPDATE novels SET unread = 0 WHERE novelId = ?", [
novelId,
]);
});
db.transaction(tx => {
tx.executeSql(
"INSERT OR REPLACE INTO history (historyNovelId, historyChapterId, historyTimeRead) VALUES (?, ?, (datetime('now','localtime')))",
[novelId, chapterId],
(txObj, res) => {},
(txObj, error) => console.log('Error ', error),
);
tx.executeSql('UPDATE novels SET unread = 0 WHERE novelId = ?', [novelId]);
});
};
export const deleteHistory = async (historyId) => {
db.transaction((tx) => {
tx.executeSql(
"DELETE FROM history WHERE historyId = ?",
[historyId],
(txObj, res) => {},
(txObj, error) => console.log("Error ", error)
);
});
export const deleteHistory = async historyId => {
db.transaction(tx => {
tx.executeSql(
'DELETE FROM history WHERE historyId = ?',
[historyId],
(txObj, res) => {},
(txObj, error) => console.log('Error ', error),
);
});
};
export const deleteAllHistory = async () => {
db.transaction((tx) => {
tx.executeSql("DELETE FROM history; VACCUM;");
});
db.transaction(tx => {
tx.executeSql('DELETE FROM history; VACCUM;');
});
};
+30 -30
View File
@@ -1,5 +1,5 @@
import * as SQLite from "expo-sqlite";
const db = SQLite.openDatabase("lnreader.db");
import * as SQLite from 'expo-sqlite';
const db = SQLite.openDatabase('lnreader.db');
const getLibraryQuery = (sort, filter) => `
SELECT novels.*, C.chaptersUnread, D.chaptersDownloaded
@@ -18,28 +18,28 @@ const getLibraryQuery = (sort, filter) => `
GROUP BY chapters.novelId
) AS D
ON novels.novelId = D.novelId
WHERE novels.followed = 1 ${filter ? "AND " + filter : ""}
${sort ? "ORDER BY " + sort : ""}
WHERE novels.followed = 1 ${filter ? 'AND ' + filter : ''}
${sort ? 'ORDER BY ' + sort : ''}
`;
export const getLibrary = (sort, filter) => {
return new Promise((resolve, reject) =>
db.transaction((tx) => {
tx.executeSql(
getLibraryQuery(sort, filter),
null,
(txObj, { rows: { _array } }) => {
// console.log(_array);
resolve(_array);
},
(txObj, error) => console.log("Error ", error)
);
})
);
return new Promise((resolve, reject) =>
db.transaction(tx => {
tx.executeSql(
getLibraryQuery(sort, filter),
null,
(txObj, {rows: {_array}}) => {
// console.log(_array);
resolve(_array);
},
(txObj, error) => console.log('Error ', error),
);
}),
);
};
const searchLibraryQuery = (searchText, sort, filter) =>
`
`
SELECT novels.*, C.chaptersUnread, D.chaptersDownloaded
FROM novels
LEFT JOIN (
@@ -57,19 +57,19 @@ const searchLibraryQuery = (searchText, sort, filter) =>
) AS D
ON novels.novelId = D.novelId
WHERE novels.followed = 1 AND novelName LIKE '%${searchText}%' ${
filter ? "AND " + filter : ""
}
${sort ? "ORDER BY " + sort : ""} `;
filter ? 'AND ' + filter : ''
}
${sort ? 'ORDER BY ' + sort : ''} `;
export const searchLibrary = (searchText, sort, filter) => {
return new Promise((resolve, reject) => {
db.transaction((tx) => {
tx.executeSql(
searchLibraryQuery(searchText, sort, filter),
null,
(txObj, { rows: { _array } }) => resolve(_array),
(txObj, error) => console.log("Error ", error)
);
});
return new Promise((resolve, reject) => {
db.transaction(tx => {
tx.executeSql(
searchLibraryQuery(searchText, sort, filter),
null,
(txObj, {rows: {_array}}) => resolve(_array),
(txObj, error) => console.log('Error ', error),
);
});
});
};
+200 -203
View File
@@ -1,241 +1,238 @@
import * as SQLite from "expo-sqlite";
const db = SQLite.openDatabase("lnreader.db");
import * as SQLite from 'expo-sqlite';
const db = SQLite.openDatabase('lnreader.db');
import BackgroundService from "react-native-background-actions";
import * as DocumentPicker from "expo-document-picker";
import BackgroundService from 'react-native-background-actions';
import * as DocumentPicker from 'expo-document-picker';
import { fetchChapters, fetchNovel } from "../../services/Source/source";
import { insertChapters } from "./ChapterQueries";
import {fetchChapters, fetchNovel} from '../../services/Source/source';
import {insertChapters} from './ChapterQueries';
import { showToast } from "../../hooks/showToast";
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) => {
return new Promise((resolve, reject) =>
db.transaction((tx) =>
tx.executeSql(
insertNovelQuery,
[
novel.novelUrl,
novel.sourceUrl,
novel.sourceId,
novel.source,
novel.novelName,
novel.novelCover,
novel.novelSummary,
novel.author,
novel.artist,
novel.status,
novel.genre,
],
(txObj, { insertId }) => resolve(insertId),
(txObj, error) => console.log("Error ", error)
)
)
);
export const insertNovel = async novel => {
return new Promise((resolve, reject) =>
db.transaction(tx =>
tx.executeSql(
insertNovelQuery,
[
novel.novelUrl,
novel.sourceUrl,
novel.sourceId,
novel.source,
novel.novelName,
novel.novelCover,
novel.novelSummary,
novel.author,
novel.artist,
novel.status,
novel.genre,
],
(txObj, {insertId}) => resolve(insertId),
(txObj, error) => console.log('Error ', error),
),
),
);
};
export const followNovel = async (followed, novelId) => {
db.transaction((tx) => {
tx.executeSql(
"UPDATE novels SET followed = ? WHERE novelId = ?",
[!followed, novelId],
(tx, res) => {},
(txObj, error) => console.log("Error ", error)
);
});
db.transaction(tx => {
tx.executeSql(
'UPDATE novels SET followed = ? WHERE novelId = ?',
[!followed, novelId],
(tx, res) => {},
(txObj, error) => console.log('Error ', error),
);
});
};
export const unfollowNovel = async (novelId) => {
db.transaction((tx) => {
tx.executeSql(
"UPDATE novels SET followed = 0 WHERE novelId = ?",
[novelId],
(tx, res) => {},
(txObj, error) => console.log("Error ", error)
);
});
export const unfollowNovel = async novelId => {
db.transaction(tx => {
tx.executeSql(
'UPDATE novels SET followed = 0 WHERE novelId = ?',
[novelId],
(tx, res) => {},
(txObj, error) => console.log('Error ', error),
);
});
};
const checkNovelInCacheQuery = `SELECT * FROM novels WHERE novelUrl=? LIMIT 1`;
export const checkNovelInCache = (novelUrl) => {
return new Promise((resolve, reject) =>
db.transaction((tx) => {
tx.executeSql(
checkNovelInCacheQuery,
[novelUrl],
(txObj, res) => {
if (res.rows.length !== 0) {
resolve(true);
} else {
resolve(false);
}
},
(txObj, error) => console.log("Error ", error)
);
})
);
export const checkNovelInCache = novelUrl => {
return new Promise((resolve, reject) =>
db.transaction(tx => {
tx.executeSql(
checkNovelInCacheQuery,
[novelUrl],
(txObj, res) => {
if (res.rows.length !== 0) {
resolve(true);
} else {
resolve(false);
}
},
(txObj, error) => console.log('Error ', error),
);
}),
);
};
export const getNovel = async (sourceId, novelUrl) => {
return new Promise((resolve, reject) =>
db.transaction((tx) => {
tx.executeSql(
`SELECT * FROM novels WHERE novelUrl = ? AND sourceId = ?`,
[novelUrl, sourceId],
(txObj, { rows }) => resolve(rows.item(0)),
(txObj, error) => console.log("Error ", error)
);
})
);
return new Promise((resolve, reject) =>
db.transaction(tx => {
tx.executeSql(
`SELECT * FROM novels WHERE novelUrl = ? AND sourceId = ?`,
[novelUrl, sourceId],
(txObj, {rows}) => resolve(rows.item(0)),
(txObj, error) => console.log('Error ', error),
);
}),
);
};
export const deleteNovelCache = () => {
db.transaction((tx) => {
tx.executeSql(
"DELETE FROM novels WHERE followed = 0",
null,
(txObj, res) => showToast("Entries deleted"),
(txObj, error) => console.log("Error ", error)
);
});
db.transaction(tx => {
tx.executeSql(
'DELETE FROM novels WHERE followed = 0',
null,
(txObj, res) => showToast('Entries deleted'),
(txObj, error) => console.log('Error ', error),
);
});
};
const restoreFromBackupQuery = `INSERT INTO novels (novelUrl, sourceUrl, sourceId, source, novelName, novelCover, novelSummary, author, artist, status, genre, followed, unread) VALUES(?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`;
export const restoreLibrary = async (novel) => {
return new Promise((resolve, reject) =>
db.transaction((tx) =>
tx.executeSql(
restoreFromBackupQuery,
[
novel.novelUrl,
novel.sourceUrl,
novel.sourceId,
novel.source,
novel.novelName,
novel.novelCover,
novel.novelSummary,
novel.author,
novel.artist,
novel.status,
novel.genre,
novel.followed,
novel.unread,
],
async (txObj, { insertId }) => {
const chapters = await fetchChapters(
novel.sourceId,
novel.novelUrl
);
await insertChapters(insertId, chapters);
},
(txObj, error) => console.log("Error ", error)
)
)
);
export const restoreLibrary = async novel => {
return new Promise((resolve, reject) =>
db.transaction(tx =>
tx.executeSql(
restoreFromBackupQuery,
[
novel.novelUrl,
novel.sourceUrl,
novel.sourceId,
novel.source,
novel.novelName,
novel.novelCover,
novel.novelSummary,
novel.author,
novel.artist,
novel.status,
novel.genre,
novel.followed,
novel.unread,
],
async (txObj, {insertId}) => {
const chapters = await fetchChapters(novel.sourceId, novel.novelUrl);
await insertChapters(insertId, chapters);
},
(txObj, error) => console.log('Error ', error),
),
),
);
};
const migrateNovelQuery = `INSERT INTO novels (novelUrl, sourceUrl, sourceId, source, novelName, novelCover, novelSummary, author, artist, status, genre, followed) VALUES(?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`;
export const migrateNovel = async (sourceId, novelUrl) => {
try {
const novel = await fetchNovel(sourceId, novelUrl);
try {
const novel = await fetchNovel(sourceId, novelUrl);
const options = {
taskName: "Migration",
taskTitle: `Migrating ${novel.novelName} to new source`,
taskDesc: novel.source,
taskIcon: {
name: "notification_icon",
type: "drawable",
},
color: "#00adb5",
parameters: {
delay: 1000,
},
progressBar: {
max: 1,
value: 0,
indeterminate: true,
},
};
const options = {
taskName: 'Migration',
taskTitle: `Migrating ${novel.novelName} to new source`,
taskDesc: novel.source,
taskIcon: {
name: 'notification_icon',
type: 'drawable',
},
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)
)
);
});
};
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 },
});
} catch (error) {
showToast(error.message);
}
await BackgroundService.start(veryIntensiveTask, options);
await BackgroundService.updateNotification({
progressBar: {value: 1, indeterminate: false},
});
} catch (error) {
showToast(error.message);
}
};
export const updateNovelInfo = async (info, novelId) => {
db.transaction((tx) => {
tx.executeSql(
"UPDATE novels SET novelName = ?, novelSummary = ?, author = ?, genre = ?, status = ? WHERE novelId = ?",
[
info.novelName,
info.novelSummary,
info.author,
info.genre,
info.status,
novelId,
],
(txObj, res) => {},
(txObj, error) => console.log("Error ", error)
);
db.transaction(tx => {
tx.executeSql(
'UPDATE novels SET novelName = ?, novelSummary = ?, author = ?, genre = ?, status = ? WHERE novelId = ?',
[
info.novelName,
info.novelSummary,
info.author,
info.genre,
info.status,
novelId,
],
(txObj, res) => {},
(txObj, error) => console.log('Error ', error),
);
});
};
export const pickCustomNovelCover = async novelId => {
const image = await DocumentPicker.getDocumentAsync({type: 'image/*'});
if (image.type === 'success' && image.uri) {
db.transaction(tx => {
tx.executeSql(
'UPDATE novels SET novelCover = ? WHERE novelId = ?',
[image.uri, novelId],
(txObj, res) => {},
(txObj, error) => console.log('Error ', error),
);
});
};
export const pickCustomNovelCover = async (novelId) => {
const image = await DocumentPicker.getDocumentAsync({ type: "image/*" });
if (image.type === "success" && image.uri) {
db.transaction((tx) => {
tx.executeSql(
"UPDATE novels SET novelCover = ? WHERE novelId = ?",
[image.uri, novelId],
(txObj, res) => {},
(txObj, error) => console.log("Error ", error)
);
});
return image.uri;
}
return image.uri;
}
};
+23 -23
View File
@@ -1,5 +1,5 @@
import * as SQLite from "expo-sqlite";
const db = SQLite.openDatabase("lnreader.db");
import * as SQLite from 'expo-sqlite';
const db = SQLite.openDatabase('lnreader.db');
const getUpdatesQuery = `
SELECT chapters.chapterId, chapters.read, chapters.downloaded, chapters.chapterUrl, chapters.chapterName, novels.novelUrl, novels.novelId, novels.novelCover, novels.novelName, novels.sourceId, updates.updateTime, updates.updateId
@@ -13,30 +13,30 @@ const getUpdatesQuery = `
`;
export const getUpdates = async () => {
return new Promise((resolve, reject) => {
db.transaction((tx) => {
tx.executeSql(
getUpdatesQuery,
null,
(txObj, { rows: { _array } }) => {
resolve(_array);
},
(txObj, error) => console.log("Error ", error)
);
});
return new Promise((resolve, reject) => {
db.transaction(tx => {
tx.executeSql(
getUpdatesQuery,
null,
(txObj, {rows: {_array}}) => {
resolve(_array);
},
(txObj, error) => console.log('Error ', error),
);
});
});
};
export const deleteNovelUpdates = (novelId) => {
db.transaction((tx) => {
tx.executeSql(
"DELETE FROM updates WHERE novelId = ?",
[novelId],
(txObj, res) => {},
(txObj, error) => console.log("Error ", error)
);
});
export const deleteNovelUpdates = novelId => {
db.transaction(tx => {
tx.executeSql(
'DELETE FROM updates WHERE novelId = ?',
[novelId],
(txObj, res) => {},
(txObj, error) => console.log('Error ', error),
);
});
};
export const clearUpdates = () =>
db.transaction((tx) => tx.executeSql("DELETE FROM updates; VACCUM;"));
db.transaction(tx => tx.executeSql('DELETE FROM updates; VACCUM;'));
+34 -34
View File
@@ -1,46 +1,46 @@
import { useState, useEffect } from "react";
import {useState, useEffect} from 'react';
export const githubUpdateChecker = () => {
const latestReleaseUrl =
"https://api.github.com/repos/rajarsheechatterjee/lnreader/releases/latest";
const latestReleaseUrl =
'https://api.github.com/repos/rajarsheechatterjee/lnreader/releases/latest';
const [checking, setChecking] = useState(true);
const [latestRelease, setLatestRelease] = useState();
const [checking, setChecking] = useState(true);
const [latestRelease, setLatestRelease] = useState();
const checkForRelease = async () => {
let res = await fetch(latestReleaseUrl);
res = await res.json();
const checkForRelease = async () => {
let res = await fetch(latestReleaseUrl);
res = await res.json();
const release = {
tag_name: res.tag_name,
body: res.body,
downloadUrl: res.assets[0].browser_download_url,
};
setLatestRelease(release);
setChecking(false);
const release = {
tag_name: res.tag_name,
body: res.body,
downloadUrl: res.assets[0].browser_download_url,
};
const isNewVersion = (versionTag) => {
let currentVersion = "v1.1.5";
const regex = /[^\\d.]/;
setLatestRelease(release);
setChecking(false);
};
let newVersion = versionTag.replace(regex, "");
currentVersion = currentVersion.replace(regex, "");
const isNewVersion = versionTag => {
let currentVersion = 'v1.1.5';
const regex = /[^\\d.]/;
return newVersion !== currentVersion;
let newVersion = versionTag.replace(regex, '');
currentVersion = currentVersion.replace(regex, '');
return newVersion !== currentVersion;
};
useEffect(() => {
checkForRelease();
}, []);
if (!checking) {
const data = {
latestRelease,
isNewVersion: isNewVersion(latestRelease.tag_name),
};
useEffect(() => {
checkForRelease();
}, []);
if (!checking) {
const data = {
latestRelease,
isNewVersion: isNewVersion(latestRelease.tag_name),
};
return data;
}
return data;
}
};
+68 -70
View File
@@ -1,131 +1,129 @@
import { useSelector } from "react-redux";
import { darkTheme } from "../theme/theme";
import {useSelector} from 'react-redux';
import {darkTheme} from '../theme/theme';
const useTheme = () => {
const theme = useSelector((state) => state.settingsReducer.theme);
const theme = useSelector(state => state.settingsReducer.theme);
return theme || darkTheme;
return theme || darkTheme;
};
const useReaderSettings = () => {
const readerSettings = useSelector((state) => state.settingsReducer.reader);
const readerSettings = useSelector(state => state.settingsReducer.reader);
return readerSettings;
return readerSettings;
};
const useSettings = () => {
const settings = useSelector((state) => state.settingsReducer);
const settings = useSelector(state => state.settingsReducer);
return settings;
return settings;
};
const useLibrary = () => {
const library = useSelector((state) => state.libraryReducer.novels);
const library = useSelector(state => state.libraryReducer.novels);
return library;
return library;
};
const useNovel = () => {
const novel = useSelector((state) => state.novelReducer);
const novel = useSelector(state => state.novelReducer);
return novel;
return novel;
};
const useChapter = () => {
const chapter = useSelector((state) => state.chapterReducer);
const chapter = useSelector(state => state.chapterReducer);
return chapter;
return chapter;
};
const findNovel = (novelId) => {
let novel = useSelector((state) => state.preferenceReducer.novelSettings);
novel = novel[novelId];
const findNovel = novelId => {
let novel = useSelector(state => state.preferenceReducer.novelSettings);
novel = novel[novelId];
return novel;
return novel;
};
const usePreferences = (novelId) => {
let sort, filter, position, showChapterTitles;
const usePreferences = novelId => {
let sort, filter, position, showChapterTitles;
const novel = findNovel(novelId);
const novel = findNovel(novelId);
if (novel) {
sort = novel.sort;
filter = novel.filter;
position = novel.position;
showChapterTitles = novel.showChapterTitles;
}
if (novel) {
sort = novel.sort;
filter = novel.filter;
position = novel.position;
showChapterTitles = novel.showChapterTitles;
}
return { sort, filter, position, showChapterTitles };
return {sort, filter, position, showChapterTitles};
};
const useSavedSettings = () => {
const settings = useSelector(
(state) => state.preferenceReducer.novelSettings
);
const settings = useSelector(state => state.preferenceReducer.novelSettings);
return settings;
return settings;
};
const useContinueReading = (chapters, novelId) => {
let lastReadChapter, chapterId, novel, position;
let lastReadChapter, chapterId, novel, position;
novel = findNovel(novelId);
if (novel) {
chapterId = novel.lastRead;
position = novel.position;
}
novel = findNovel(novelId);
if (novel) {
chapterId = novel.lastRead;
position = novel.position;
}
if (chapterId) {
lastReadChapter = chapters.find(
(obj) => obj.chapterId === chapterId && obj.read === 0
);
}
if (chapterId) {
lastReadChapter = chapters.find(
obj => obj.chapterId === chapterId && obj.read === 0,
);
}
if (!lastReadChapter) {
lastReadChapter = chapters.find((obj) => obj.read === 0);
}
if (!lastReadChapter) {
lastReadChapter = chapters.find(obj => obj.read === 0);
}
return { lastReadChapter, position };
return {lastReadChapter, position};
};
const usePosition = (novelId, chapterId) => {
let novel, position;
let novel, position;
novel = findNovel(novelId);
novel = findNovel(novelId);
if (novel) {
if (novel.position) {
position = novel.position[chapterId] || null;
}
if (novel) {
if (novel.position) {
position = novel.position[chapterId] || null;
}
}
return position;
return position;
};
const useTrackingStatus = () => {
const tracker = useSelector((state) => state.trackerReducer);
const tracker = useSelector(state => state.trackerReducer);
return tracker;
return tracker;
};
const useLibraryFilters = () => {
const filters = useSelector((state) => state.libraryReducer.filters);
const filters = useSelector(state => state.libraryReducer.filters);
return filters;
return filters;
};
export {
useTheme,
useReaderSettings,
useLibrary,
useSettings,
usePreferences,
useContinueReading,
useTrackingStatus,
useNovel,
useChapter,
useSavedSettings,
useLibraryFilters,
usePosition,
useTheme,
useReaderSettings,
useLibrary,
useSettings,
usePreferences,
useContinueReading,
useTrackingStatus,
useNovel,
useChapter,
useSavedSettings,
useLibraryFilters,
usePosition,
};
+3 -3
View File
@@ -1,5 +1,5 @@
import { ToastAndroid } from "react-native";
import {ToastAndroid} from 'react-native';
export const showToast = (message) => {
ToastAndroid.show(message, ToastAndroid.SHORT);
export const showToast = message => {
ToastAndroid.show(message, ToastAndroid.SHORT);
};
+90 -94
View File
@@ -1,109 +1,105 @@
import React from "react";
import React from 'react';
import { createMaterialBottomTabNavigator } from "@react-navigation/material-bottom-tabs";
import MaterialCommunityIcons from "react-native-vector-icons/MaterialCommunityIcons";
import { useSafeAreaInsets } from "react-native-safe-area-context";
import {createMaterialBottomTabNavigator} from '@react-navigation/material-bottom-tabs';
import MaterialCommunityIcons from 'react-native-vector-icons/MaterialCommunityIcons';
import {useSafeAreaInsets} from 'react-native-safe-area-context';
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 Library from '../screens/Library/Library';
import Updates from '../screens/Updates/Updates';
import History from '../screens/History/History';
import Browse from '../screens/Browse/Browse';
import More from '../screens/More/More';
import { useSettings, useTheme } from "../hooks/reduxHooks";
import {useSettings, useTheme} from '../hooks/reduxHooks';
const Tab = createMaterialBottomTabNavigator();
const BottomNavigator = () => {
const theme = useTheme();
const insets = useSafeAreaInsets();
const theme = useTheme();
const insets = useSafeAreaInsets();
const {
showHistoryTab = true,
showUpdatesTab = true,
showLabelsInNav = false,
} = useSettings();
const {
showHistoryTab = true,
showUpdatesTab = true,
showLabelsInNav = false,
} = useSettings();
return (
<Tab.Navigator
barStyle={{
backgroundColor: theme.colorPrimary,
paddingBottom: insets.bottom,
}}
activeColor={theme.colorAccent}
shifting={!showLabelsInNav}
>
<Tab.Screen
name="Library"
component={Library}
options={{
tabBarIcon: ({ color }) => (
<MaterialCommunityIcons
name="book-variant-multiple"
color={color}
size={24}
/>
),
}}
return (
<Tab.Navigator
barStyle={{
backgroundColor: theme.colorPrimary,
paddingBottom: insets.bottom,
}}
activeColor={theme.colorAccent}
shifting={!showLabelsInNav}
>
<Tab.Screen
name="Library"
component={Library}
options={{
tabBarIcon: ({color}) => (
<MaterialCommunityIcons
name="book-variant-multiple"
color={color}
size={24}
/>
{showUpdatesTab && (
<Tab.Screen
name="Updates"
component={Updates}
options={{
tabBarIcon: ({ color }) => (
<MaterialCommunityIcons
name="alert-decagram-outline"
color={color}
size={24}
/>
),
}}
/>
)}
{showHistoryTab && (
<Tab.Screen
name="History"
component={History}
options={{
tabBarIcon: ({ color }) => (
<MaterialCommunityIcons
name="history"
color={color}
size={24}
/>
),
}}
/>
)}
<Tab.Screen
name="Browse"
component={Browse}
options={{
tabBarIcon: ({ color }) => (
<MaterialCommunityIcons
name="compass-outline"
color={color}
size={24}
/>
),
}}
),
}}
/>
{showUpdatesTab && (
<Tab.Screen
name="Updates"
component={Updates}
options={{
tabBarIcon: ({color}) => (
<MaterialCommunityIcons
name="alert-decagram-outline"
color={color}
size={24}
/>
),
}}
/>
)}
{showHistoryTab && (
<Tab.Screen
name="History"
component={History}
options={{
tabBarIcon: ({color}) => (
<MaterialCommunityIcons name="history" color={color} size={24} />
),
}}
/>
)}
<Tab.Screen
name="Browse"
component={Browse}
options={{
tabBarIcon: ({color}) => (
<MaterialCommunityIcons
name="compass-outline"
color={color}
size={24}
/>
<Tab.Screen
name="More"
component={More}
options={{
tabBarIcon: ({ color }) => (
<MaterialCommunityIcons
name="dots-horizontal"
color={color}
size={24}
/>
),
}}
),
}}
/>
<Tab.Screen
name="More"
component={More}
options={{
tabBarIcon: ({color}) => (
<MaterialCommunityIcons
name="dots-horizontal"
color={color}
size={24}
/>
</Tab.Navigator>
);
),
}}
/>
</Tab.Navigator>
);
};
export default BottomNavigator;
+48 -65
View File
@@ -1,81 +1,64 @@
import React, { useEffect } from "react";
import { View } from "react-native";
import React, {useEffect} from 'react';
import {View} from 'react-native';
import { NavigationContainer } from "@react-navigation/native";
import { createStackNavigator } from "@react-navigation/stack";
import {NavigationContainer} from '@react-navigation/native';
import {createStackNavigator} from '@react-navigation/stack';
import { setBarColor } from "../theme/utils/setBarColor";
import { useTheme } from "../hooks/reduxHooks";
import { githubUpdateChecker } from "../hooks/githubUpdateChecker";
import * as SplashScreen from "expo-splash-screen";
import {setBarColor} from '../theme/utils/setBarColor';
import {useTheme} from '../hooks/reduxHooks';
import {githubUpdateChecker} from '../hooks/githubUpdateChecker';
import * as SplashScreen from 'expo-splash-screen';
// Navigators
import BottomNavigator from "./BottomNavigator";
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";
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 MainNavigator = () => {
const theme = useTheme();
const theme = useTheme();
useEffect(() => {
setTimeout(async () => {
await SplashScreen.hideAsync();
setBarColor(theme);
}, 500);
}, [theme]);
useEffect(() => {
setTimeout(async () => {
await SplashScreen.hideAsync();
setBarColor(theme);
}, 500);
}, [theme]);
const { isNewVersion, latestRelease } = githubUpdateChecker() || {};
const {isNewVersion, latestRelease} = githubUpdateChecker() || {};
return (
<NavigationContainer
theme={{ colors: { background: theme.colorPrimaryDark } }}
>
<View style={{ flex: 1, backgroundColor: theme.colorPrimaryDark }}>
{isNewVersion && <NewUpdateDialog newVersion={latestRelease} />}
<Stack.Navigator screenOptions={{ headerShown: false }}>
<Stack.Screen
name="BottomNavigator"
component={BottomNavigator}
/>
<Stack.Screen name="Novel" component={Novel} />
<Stack.Screen name="Chapter" component={Chapter} />
<Stack.Screen name="MoreStack" component={MoreStack} />
<Stack.Screen name="Extension" component={Extension} />
<Stack.Screen name="BrowseMal" component={MalTopNovels} />
<Stack.Screen
name="BrowseSettings"
component={BrowseSettings}
/>
<Stack.Screen
name="GlobalSearch"
component={GlobalSearch}
/>
<Stack.Screen name="Migration" component={Migration} />
<Stack.Screen
name="SourceNovels"
component={SourceNovels}
/>
<Stack.Screen
name="MigrateNovel"
component={MigrateNovel}
/>
</Stack.Navigator>
</View>
</NavigationContainer>
);
return (
<NavigationContainer theme={{colors: {background: theme.colorPrimaryDark}}}>
<View style={{flex: 1, backgroundColor: theme.colorPrimaryDark}}>
{isNewVersion && <NewUpdateDialog newVersion={latestRelease} />}
<Stack.Navigator screenOptions={{headerShown: false}}>
<Stack.Screen name="BottomNavigator" component={BottomNavigator} />
<Stack.Screen name="Novel" component={Novel} />
<Stack.Screen name="Chapter" component={Chapter} />
<Stack.Screen name="MoreStack" component={MoreStack} />
<Stack.Screen name="Extension" component={Extension} />
<Stack.Screen name="BrowseMal" component={MalTopNovels} />
<Stack.Screen name="BrowseSettings" component={BrowseSettings} />
<Stack.Screen name="GlobalSearch" component={GlobalSearch} />
<Stack.Screen name="Migration" component={Migration} />
<Stack.Screen name="SourceNovels" component={SourceNovels} />
<Stack.Screen name="MigrateNovel" component={MigrateNovel} />
</Stack.Navigator>
</View>
</NavigationContainer>
);
};
export default MainNavigator;
+28 -31
View File
@@ -1,45 +1,42 @@
import React from "react";
import React from 'react';
import { createStackNavigator } from "@react-navigation/stack";
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";
import AppearanceSettings from "../screens/More/AppearanceSettings";
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';
import AppearanceSettings from '../screens/More/AppearanceSettings';
const Stack = createStackNavigator();
const stackNavigatorConfig = { headerShown: false };
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="AppearanceSettings"
component={AppearanceSettings}
/>
<Stack.Screen name="AdvancedSettings" component={AdvancedSettings} />
</Stack.Navigator>
<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="AppearanceSettings" component={AppearanceSettings} />
<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>
<Stack.Navigator screenOptions={stackNavigatorConfig}>
<Stack.Screen name="SettingsStack" component={SettingsStack} />
<Stack.Screen name="About" component={About} />
<Stack.Screen name="DownloadQueue" component={DownloadQueue} />
<Stack.Screen name="Downloads" component={Downloads} />
</Stack.Navigator>
);
export default MoreStack;
+4 -4
View File
@@ -1,4 +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";
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';
+83 -83
View File
@@ -1,102 +1,102 @@
import {
SET_DOWNLOAD_QUEUE,
CANCEL_DOWNLOAD,
PAUSE_DOWNLOADS,
} from "./donwloads.types";
import BackgroundService from "react-native-background-actions";
import { downloadChapter } from "../../database/queries/ChapterQueries";
import { CHAPTER_DOWNLOADED } from "../novel/novel.types";
import { showToast } from "../../hooks/showToast";
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");
export const pauseDownloads = () => async dispatch => {
showToast('Download paused');
await BackgroundService.stop();
await BackgroundService.stop();
dispatch({ type: PAUSE_DOWNLOADS });
dispatch({type: PAUSE_DOWNLOADS});
};
export const resumeDownloads = (chapters) => async (dispatch) => {
showToast("Download resumed");
export const resumeDownloads = chapters => async dispatch => {
showToast('Download resumed');
const options = {
taskName: "Library Update",
taskTitle: chapters[0].chapterName,
taskDesc: "0/" + chapters.length,
taskIcon: {
name: "notification_icon",
type: "drawable",
},
color: "#00adb5",
parameters: {
delay: 1000,
},
linkingURI: "lnreader://downloads",
progressBar: {
max: chapters.length,
value: 0,
},
};
const options = {
taskName: 'Library Update',
taskTitle: chapters[0].chapterName,
taskDesc: '0/' + chapters.length,
taskIcon: {
name: 'notification_icon',
type: 'drawable',
},
color: '#00adb5',
parameters: {
delay: 1000,
},
linkingURI: 'lnreader://downloads',
progressBar: {
max: chapters.length,
value: 0,
},
};
const sleep = (time) =>
new Promise((resolve) => setTimeout(() => resolve(), time));
const 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
);
}
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,
});
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,
},
});
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();
if (i + 1 === chapters.length) {
resolve();
await BackgroundService.stop();
Notifications.scheduleNotificationAsync({
content: {
title: "Downloader",
body: "Download completed",
},
trigger: null,
});
}
await sleep(taskData.delay);
}
}
});
};
Notifications.scheduleNotificationAsync({
content: {
title: 'Downloader',
body: 'Download completed',
},
trigger: null,
});
}
await sleep(taskData.delay);
}
}
});
};
await BackgroundService.start(veryIntensiveTask, options);
await BackgroundService.start(veryIntensiveTask, options);
dispatch({ type: PAUSE_DOWNLOADS });
dispatch({type: PAUSE_DOWNLOADS});
};
export const cancelDownload = () => async (dispatch) => {
await BackgroundService.stop();
export const cancelDownload = () => async dispatch => {
await BackgroundService.stop();
dispatch({ type: CANCEL_DOWNLOAD });
dispatch({type: CANCEL_DOWNLOAD});
};
+25 -25
View File
@@ -1,35 +1,35 @@
import { CHAPTER_DOWNLOADED } from "../novel/novel.types";
import { CANCEL_DOWNLOAD, SET_DOWNLOAD_QUEUE } from "./donwloads.types";
import {CHAPTER_DOWNLOADED} from '../novel/novel.types';
import {CANCEL_DOWNLOAD, SET_DOWNLOAD_QUEUE} from './donwloads.types';
const initialState = {
downloadQueue: [],
downloadQueue: [],
};
const downloadsReducer = (state = initialState, action) => {
const { type, payload } = action;
const {type, payload} = action;
switch (type) {
case SET_DOWNLOAD_QUEUE:
return {
...state,
downloadQueue: [...state.downloadQueue, ...payload],
};
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;
}
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;
+46 -49
View File
@@ -1,62 +1,59 @@
import {
GET_LIBRARY_NOVELS,
GET_LIBRARY_SEARCH_RESULTS,
SET_LIBRARY_LOADING,
SORT_FILTER_LIBRARY,
} from "./library.types";
import {
getLibrary,
searchLibrary,
} from "../../database/queries/LibraryQueries";
import { restoreBackup } from "../../services/backup";
GET_LIBRARY_NOVELS,
GET_LIBRARY_SEARCH_RESULTS,
SET_LIBRARY_LOADING,
SORT_FILTER_LIBRARY,
} from './library.types';
import {getLibrary, searchLibrary} from '../../database/queries/LibraryQueries';
import {restoreBackup} from '../../services/backup';
export const getLibraryAction =
(sort, filter) => async (dispatch, getState) => {
const { downloadedOnlyMode = false } = getState().settingsReducer;
(sort, filter) => async (dispatch, getState) => {
const {downloadedOnlyMode = false} = getState().settingsReducer;
const res = await getLibrary(
sort,
downloadedOnlyMode ? "chaptersDownloaded > 0" : filter
);
dispatch({
type: GET_LIBRARY_NOVELS,
payload: res,
});
};
export const searchLibraryAction =
(searchText, sort, filter) => async (dispatch) => {
const res = await searchLibrary(searchText, sort, filter);
dispatch({
type: GET_LIBRARY_SEARCH_RESULTS,
payload: res,
});
};
export const filterLibrary = (sort, filter) => async (dispatch) => {
dispatch({ type: SORT_FILTER_LIBRARY, payload: { sort, filter } });
const res = await getLibrary(sort, filter);
const res = await getLibrary(
sort,
downloadedOnlyMode ? 'chaptersDownloaded > 0' : filter,
);
dispatch({
type: GET_LIBRARY_NOVELS,
payload: res,
type: GET_LIBRARY_NOVELS,
payload: res,
});
};
export const searchLibraryAction =
(searchText, sort, filter) => async dispatch => {
const res = await searchLibrary(searchText, sort, filter);
dispatch({
type: GET_LIBRARY_SEARCH_RESULTS,
payload: res,
});
};
export const filterLibrary = (sort, filter) => async dispatch => {
dispatch({type: SORT_FILTER_LIBRARY, payload: {sort, filter}});
const res = await getLibrary(sort, filter);
dispatch({
type: GET_LIBRARY_NOVELS,
payload: res,
});
};
export const restoreLibraryAction = () => async (dispatch) => {
dispatch({ type: SET_LIBRARY_LOADING });
export const restoreLibraryAction = () => async dispatch => {
dispatch({type: SET_LIBRARY_LOADING});
const res = await restoreBackup();
const res = await restoreBackup();
if (res) {
const novels = await getLibrary();
if (res) {
const novels = await getLibrary();
dispatch({
type: GET_LIBRARY_NOVELS,
payload: novels,
});
}
dispatch({
type: GET_LIBRARY_NOVELS,
payload: novels,
});
}
};
+26 -26
View File
@@ -1,36 +1,36 @@
import {
GET_LIBRARY_NOVELS,
GET_LIBRARY_SEARCH_RESULTS,
SET_LIBRARY_LOADING,
SORT_FILTER_LIBRARY,
} from "./library.types";
GET_LIBRARY_NOVELS,
GET_LIBRARY_SEARCH_RESULTS,
SET_LIBRARY_LOADING,
SORT_FILTER_LIBRARY,
} from './library.types';
const initialState = {
novels: [],
searchResults: [],
filters: { sort: "novels.novelId ASC", filter: "" },
loading: true,
novels: [],
searchResults: [],
filters: {sort: 'novels.novelId ASC', filter: ''},
loading: true,
};
const libraryReducer = (state = initialState, action) => {
const { type, payload } = action;
const {type, payload} = action;
switch (type) {
case SET_LIBRARY_LOADING:
return { ...state, loading: true };
case GET_LIBRARY_NOVELS:
return { ...state, novels: payload, loading: false };
case GET_LIBRARY_SEARCH_RESULTS:
return { ...state, novels: payload, loading: false };
case SORT_FILTER_LIBRARY:
// console.log("2.", payload.sort, payload.filter);
return {
...state,
filters: { sort: payload.sort, filter: payload.filter },
};
default:
return state;
}
switch (type) {
case SET_LIBRARY_LOADING:
return {...state, loading: true};
case GET_LIBRARY_NOVELS:
return {...state, novels: payload, loading: false};
case GET_LIBRARY_SEARCH_RESULTS:
return {...state, novels: payload, loading: false};
case SORT_FILTER_LIBRARY:
// console.log("2.", payload.sort, payload.filter);
return {
...state,
filters: {sort: payload.sort, filter: payload.filter},
};
default:
return state;
}
};
export default libraryReducer;
+4 -4
View File
@@ -1,5 +1,5 @@
export const GET_LIBRARY_NOVELS = "GET_LIBRARY_NOVELS";
export const GET_LIBRARY_SEARCH_RESULTS = "GET_LIBRARY_SEARCH_RESULTS";
export const SET_LIBRARY_LOADING = "SET_LIBRARY_LOADING";
export const GET_LIBRARY_NOVELS = 'GET_LIBRARY_NOVELS';
export const GET_LIBRARY_SEARCH_RESULTS = 'GET_LIBRARY_SEARCH_RESULTS';
export const SET_LIBRARY_LOADING = 'SET_LIBRARY_LOADING';
export const SORT_FILTER_LIBRARY = "SORT_FILTER_LIBRARY";
export const SORT_FILTER_LIBRARY = 'SORT_FILTER_LIBRARY';
+396 -403
View File
@@ -1,442 +1,435 @@
import { ToastAndroid } from "react-native";
import {ToastAndroid} from 'react-native';
import {
LOADING_NOVEL,
GET_NOVEL,
GET_CHAPTERS,
NOVEL_ERROR,
FETCHING_NOVEL,
SET_NOVEL,
UPDATE_IN_LIBRARY,
CHAPTER_READ,
CHAPTER_DOWNLOADING,
CHAPTER_DOWNLOADED,
CHAPTER_DELETED,
UPDATE_NOVEL,
SET_NOVEL_SETTINGS,
CHAPTER_UNREAD,
BOOKMARK_CHAPTER,
MARK_PREVIOUS_CHAPTERS_READ,
MARK_PREVIOUS_CHAPTERS_UNREAD,
} from "./novel.types";
LOADING_NOVEL,
GET_NOVEL,
GET_CHAPTERS,
NOVEL_ERROR,
FETCHING_NOVEL,
SET_NOVEL,
UPDATE_IN_LIBRARY,
CHAPTER_READ,
CHAPTER_DOWNLOADING,
CHAPTER_DOWNLOADED,
CHAPTER_DELETED,
UPDATE_NOVEL,
SET_NOVEL_SETTINGS,
CHAPTER_UNREAD,
BOOKMARK_CHAPTER,
MARK_PREVIOUS_CHAPTERS_READ,
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";
followNovel,
insertNovel,
getNovel,
} from '../../database/queries/NovelQueries';
import {
getChapters,
insertChapters,
markChapterRead,
downloadChapter,
deleteChapter,
markChapterUnread,
bookmarkChapter,
markPreviuschaptersRead,
markPreviousChaptersUnread,
getNextChapterFromDB,
} from "../../database/queries/ChapterQueries";
import { deleteNovelUpdates } from "../../database/queries/UpdateQueries";
getChapters,
insertChapters,
markChapterRead,
downloadChapter,
deleteChapter,
markChapterUnread,
bookmarkChapter,
markPreviuschaptersRead,
markPreviousChaptersUnread,
getNextChapterFromDB,
} 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 { store } from "../store";
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 {store} from '../store';
import * as Notifications from "expo-notifications";
import * as Notifications from 'expo-notifications';
Notifications.setNotificationHandler({
handleNotification: async () => {
return {
shouldShowAlert: true,
shouldPlaySound: true,
shouldSetBadge: true,
};
},
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";
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 });
export const setNovel = novel => async dispatch => {
dispatch({type: SET_NOVEL, payload: novel});
};
export const getNovelAction =
(followed, sourceId, novelUrl, novelId, sort, filter) =>
async (dispatch) => {
try {
dispatch({ type: LOADING_NOVEL });
if (followed === 1) {
/**
* If novel is followed directly get the chapters from db.
*/
const chapters = await getChapters(novelId, sort, filter);
dispatch({
type: GET_CHAPTERS,
payload: chapters,
});
} else {
dispatch({ type: FETCHING_NOVEL });
/**
* Check if novel is cached.
*/
const novel = await getNovel(sourceId, novelUrl);
if (novel) {
/**
* Get chapters from db.
*/
novel.chapters = await getChapters(
novel.novelId,
sort,
filter
);
dispatch({
type: GET_NOVEL,
payload: novel,
});
} else {
/**
* Fetch novel from source.
*/
const fetchedNovel = await fetchNovel(sourceId, novelUrl);
/**
* Insert novel in db.
*/
const fetchedNovelId = await insertNovel(fetchedNovel);
await insertChapters(fetchedNovelId, fetchedNovel.chapters);
/**
* Get novel from db.
*/
const novel = await getNovel(sourceId, novelUrl);
novel.chapters = await getChapters(novel.novelId);
dispatch({
type: GET_NOVEL,
payload: novel,
});
}
}
} catch (error) {
showToast(error.message);
// dispatch({ type: NOVEL_ERROR });
}
};
export const sortAndFilterChapters =
(novelId, sort, filter) => async (dispatch) => {
dispatch({ type: FETCHING_NOVEL });
(followed, sourceId, novelUrl, novelId, sort, filter) => async dispatch => {
try {
dispatch({type: LOADING_NOVEL});
if (followed === 1) {
/**
* If novel is followed directly get the chapters from db.
*/
const chapters = await getChapters(novelId, sort, filter);
dispatch({
type: GET_CHAPTERS,
payload: chapters,
});
dispatch({
type: SET_CHAPTER_LIST_PREF,
payload: { novelId, sort, filter },
});
};
export const showChapterTitlesAction = (novelId, value) => async (dispatch) => {
dispatch({
type: SET_NOVEL_SETTINGS,
payload: { novelId, value: value },
});
};
export const followNovelAction = (novel) => async (dispatch) => {
await followNovel(novel.followed, novel.novelId);
if (!novel.followNovel) {
deleteNovelUpdates(novel.novelId);
}
dispatch({
type: UPDATE_IN_LIBRARY,
payload: { novelUrl: novel.novelUrl, followed: !novel.followed },
});
const res = await getLibrary();
dispatch({
type: GET_LIBRARY_NOVELS,
payload: res,
});
showToast(!novel.followed ? "Added to library" : "Removed from library");
};
export const bookmarkChapterAction = (chapters) => async (dispatch) => {
chapters.map((chapter) => {
bookmarkChapter(chapter.bookmark, chapter.chapterId);
dispatch({
type: BOOKMARK_CHAPTER,
payload: {
bookmark: chapter.bookmark,
chapterId: chapter.chapterId,
},
});
});
};
export const markChapterReadAction =
(chapterId, novelId) => async (dispatch) => {
await markChapterRead(chapterId);
dispatch({
type: CHAPTER_READ,
payload: { chapterId },
});
const nextChapter = await getNextChapterFromDB(novelId, chapterId);
dispatch({
type: SET_LAST_READ,
payload: { novelId, chapterId: nextChapter.chapterId },
type: GET_CHAPTERS,
payload: chapters,
});
} else {
dispatch({type: FETCHING_NOVEL});
/**
* Reset progress on marked read
* Check if novel is cached.
*/
dispatch({
type: SAVE_SCROLL_POSITION,
payload: {
position: 0,
percentage: 0,
chapterId,
novelId,
},
});
};
const novel = await getNovel(sourceId, novelUrl);
export const markPreviousChaptersReadAction =
(chapterId, novelId) => async (dispatch) => {
await markPreviuschaptersRead(chapterId, novelId);
if (novel) {
/**
* Get chapters from db.
*/
novel.chapters = await getChapters(novel.novelId, sort, filter);
dispatch({
type: GET_NOVEL,
payload: novel,
});
} else {
/**
* Fetch novel from source.
*/
const fetchedNovel = await fetchNovel(sourceId, novelUrl);
dispatch({
type: MARK_PREVIOUS_CHAPTERS_READ,
payload: chapterId,
});
/**
* Insert novel in db.
*/
const fetchedNovelId = await insertNovel(fetchedNovel);
await insertChapters(fetchedNovelId, fetchedNovel.chapters);
showToast("Marked previous chapters read");
};
/**
* Get novel from db.
*/
const novel = await getNovel(sourceId, novelUrl);
novel.chapters = await getChapters(novel.novelId);
export const markChaptersRead =
(chapters, novelId, sort, filter) => async (dispatch) => {
try {
chapters.map((chapter) => markChapterRead(chapter.chapterId));
const chaps = await getChapters(novelId, sort, filter);
dispatch({
type: GET_CHAPTERS,
payload: chaps,
});
} catch (error) {
showToast(error.message);
dispatch({
type: GET_NOVEL,
payload: novel,
});
}
};
}
} catch (error) {
showToast(error.message);
// dispatch({ type: NOVEL_ERROR });
}
};
export const markPreviousChaptersUnreadAction =
(chapterId, novelId) => async (dispatch) => {
await markPreviousChaptersUnread(chapterId, novelId);
export const sortAndFilterChapters =
(novelId, sort, filter) => async dispatch => {
dispatch({type: FETCHING_NOVEL});
dispatch({
type: MARK_PREVIOUS_CHAPTERS_UNREAD,
payload: chapterId,
});
};
const chapters = await getChapters(novelId, sort, filter);
export const markChapterUnreadAction =
(chapters, novelId) => async (dispatch, getState) => {
const { sort = "ORDER BY chapterId ASC", filter = "" } =
getState().preferenceReducer.novelSettings[novelId];
chapters.map((chapter) => markChapterUnread(chapter.chapterId));
const chaps = await getChapters(novelId, sort, filter);
dispatch({
type: GET_CHAPTERS,
payload: chaps,
});
};
export const downloadChapterAction =
(sourceId, novelUrl, chapterUrl, chapterName, chapterId) =>
async (dispatch) => {
// dispatch({
// type: CHAPTER_DOWNLOADING,
// payload: chapterId,
// });
dispatch({
type: SET_DOWNLOAD_QUEUE,
payload: [{ chapterId, chapterName }],
});
await downloadChapter(sourceId, novelUrl, chapterUrl, chapterId);
dispatch({
type: CHAPTER_DOWNLOADED,
payload: chapterId,
});
ToastAndroid.show(`Downloaded ${chapterName}`, ToastAndroid.SHORT);
};
export const downloadAllChaptersAction =
(sourceId, novelUrl, chaps) => async (dispatch) => {
try {
/**
* Filter downloaded chapters
*/
let chapters = chaps.filter((chapter) => chapter.downloaded === 0);
chapters = chapters.map((chapter) => ({
...chapter,
sourceId,
novelUrl,
}));
if (chapters.length > 0) {
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",
linkingURI: "lnreader://downloads/redirect",
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();
Notifications.scheduleNotificationAsync({
content: {
title: "Downloader",
body: "Download completed",
},
trigger: null,
});
}
await sleep(taskData.delay);
}
}
});
};
await BackgroundService.start(veryIntensiveTask, options);
}
} catch (error) {
showToast(error.message);
}
};
export const deleteChapterAction =
(chapterId, chapterName) => async (dispatch) => {
await deleteChapter(chapterId);
dispatch({
type: CHAPTER_DELETED,
payload: chapterId,
});
showToast(`Deleted ${chapterName}`);
};
export const deleteAllChaptersAction = (chapters) => async (dispatch) => {
await chapters.map((chapter) => {
deleteChapter(chapter.chapterId);
dispatch({
type: CHAPTER_DELETED,
payload: chapter.chapterId,
});
dispatch({
type: GET_CHAPTERS,
payload: chapters,
});
showToast("Chapters deleted");
dispatch({
type: SET_CHAPTER_LIST_PREF,
payload: {novelId, sort, filter},
});
};
export const showChapterTitlesAction = (novelId, value) => async dispatch => {
dispatch({
type: SET_NOVEL_SETTINGS,
payload: {novelId, value: value},
});
};
export const followNovelAction = novel => async dispatch => {
await followNovel(novel.followed, novel.novelId);
if (!novel.followNovel) {
deleteNovelUpdates(novel.novelId);
}
dispatch({
type: UPDATE_IN_LIBRARY,
payload: {novelUrl: novel.novelUrl, followed: !novel.followed},
});
const res = await getLibrary();
dispatch({
type: GET_LIBRARY_NOVELS,
payload: res,
});
showToast(!novel.followed ? 'Added to library' : 'Removed from library');
};
export const bookmarkChapterAction = chapters => async dispatch => {
chapters.map(chapter => {
bookmarkChapter(chapter.bookmark, chapter.chapterId);
dispatch({
type: BOOKMARK_CHAPTER,
payload: {
bookmark: chapter.bookmark,
chapterId: chapter.chapterId,
},
});
});
};
export const markChapterReadAction = (chapterId, novelId) => async dispatch => {
await markChapterRead(chapterId);
dispatch({
type: CHAPTER_READ,
payload: {chapterId},
});
const nextChapter = await getNextChapterFromDB(novelId, chapterId);
dispatch({
type: SET_LAST_READ,
payload: {novelId, chapterId: nextChapter.chapterId},
});
/**
* Reset progress on marked read
*/
dispatch({
type: SAVE_SCROLL_POSITION,
payload: {
position: 0,
percentage: 0,
chapterId,
novelId,
},
});
};
export const markPreviousChaptersReadAction =
(chapterId, novelId) => async dispatch => {
await markPreviuschaptersRead(chapterId, novelId);
dispatch({
type: MARK_PREVIOUS_CHAPTERS_READ,
payload: chapterId,
});
showToast('Marked previous chapters read');
};
export const markChaptersRead =
(chapters, novelId, sort, filter) => async dispatch => {
try {
chapters.map(chapter => markChapterRead(chapter.chapterId));
const chaps = await getChapters(novelId, sort, filter);
dispatch({
type: GET_CHAPTERS,
payload: chaps,
});
} catch (error) {
showToast(error.message);
}
};
export const markPreviousChaptersUnreadAction =
(chapterId, novelId) => async dispatch => {
await markPreviousChaptersUnread(chapterId, novelId);
dispatch({
type: MARK_PREVIOUS_CHAPTERS_UNREAD,
payload: chapterId,
});
};
export const markChapterUnreadAction =
(chapters, novelId) => async (dispatch, getState) => {
const {sort = 'ORDER BY chapterId ASC', filter = ''} =
getState().preferenceReducer.novelSettings[novelId];
chapters.map(chapter => markChapterUnread(chapter.chapterId));
const chaps = await getChapters(novelId, sort, filter);
dispatch({
type: GET_CHAPTERS,
payload: chaps,
});
};
export const downloadChapterAction =
(sourceId, novelUrl, chapterUrl, chapterName, chapterId) =>
async dispatch => {
// dispatch({
// type: CHAPTER_DOWNLOADING,
// payload: chapterId,
// });
dispatch({
type: SET_DOWNLOAD_QUEUE,
payload: [{chapterId, chapterName}],
});
await downloadChapter(sourceId, novelUrl, chapterUrl, chapterId);
dispatch({
type: CHAPTER_DOWNLOADED,
payload: chapterId,
});
ToastAndroid.show(`Downloaded ${chapterName}`, ToastAndroid.SHORT);
};
export const downloadAllChaptersAction =
(sourceId, novelUrl, chaps) => async dispatch => {
try {
/**
* Filter downloaded chapters
*/
let chapters = chaps.filter(chapter => chapter.downloaded === 0);
chapters = chapters.map(chapter => ({
...chapter,
sourceId,
novelUrl,
}));
if (chapters.length > 0) {
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',
linkingURI: 'lnreader://downloads/redirect',
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();
Notifications.scheduleNotificationAsync({
content: {
title: 'Downloader',
body: 'Download completed',
},
trigger: null,
});
}
await sleep(taskData.delay);
}
}
});
};
await BackgroundService.start(veryIntensiveTask, options);
}
} catch (error) {
showToast(error.message);
}
};
export const deleteChapterAction =
(chapterId, chapterName) => async dispatch => {
await deleteChapter(chapterId);
dispatch({
type: CHAPTER_DELETED,
payload: chapterId,
});
showToast(`Deleted ${chapterName}`);
};
export const deleteAllChaptersAction = chapters => async dispatch => {
await chapters.map(chapter => {
deleteChapter(chapter.chapterId);
dispatch({
type: CHAPTER_DELETED,
payload: chapter.chapterId,
});
});
showToast('Chapters deleted');
};
export const updateNovelAction =
(sourceId, novelUrl, novelId, sort, filter) => async (dispatch) => {
dispatch({ type: FETCHING_NOVEL });
(sourceId, novelUrl, novelId, sort, filter) => async dispatch => {
dispatch({type: FETCHING_NOVEL});
await updateNovel(sourceId, novelUrl, novelId);
await updateNovel(sourceId, novelUrl, novelId);
let novel = await getNovel(sourceId, novelUrl);
let chapters = await getChapters(novel.novelId, sort, filter);
let novel = await getNovel(sourceId, novelUrl);
let chapters = await getChapters(novel.novelId, sort, filter);
dispatch({
type: UPDATE_NOVEL,
payload: { novel, chapters },
});
};
dispatch({
type: UPDATE_NOVEL,
payload: {novel, chapters},
});
};
+138 -146
View File
@@ -1,157 +1,149 @@
import {
CHAPTER_DOWNLOADING,
CHAPTER_DOWNLOADED,
CHAPTER_DELETED,
GET_CHAPTERS,
LOADING_NOVEL,
GET_NOVEL,
FETCHING_NOVEL,
SET_NOVEL,
UPDATE_IN_LIBRARY,
NOVEL_ERROR,
CHAPTER_READ,
UPDATE_NOVEL,
UPDATE_LAST_READ,
CHAPTER_UNREAD,
BOOKMARK_CHAPTER,
MARK_PREVIOUS_CHAPTERS_READ,
MARK_PREVIOUS_CHAPTERS_UNREAD,
} from "./novel.types";
CHAPTER_DOWNLOADING,
CHAPTER_DOWNLOADED,
CHAPTER_DELETED,
GET_CHAPTERS,
LOADING_NOVEL,
GET_NOVEL,
FETCHING_NOVEL,
SET_NOVEL,
UPDATE_IN_LIBRARY,
NOVEL_ERROR,
CHAPTER_READ,
UPDATE_NOVEL,
UPDATE_LAST_READ,
CHAPTER_UNREAD,
BOOKMARK_CHAPTER,
MARK_PREVIOUS_CHAPTERS_READ,
MARK_PREVIOUS_CHAPTERS_UNREAD,
} from './novel.types';
const initialState = {
novel: {},
chapters: [],
loading: true,
updating: false,
downloading: [],
novel: {},
chapters: [],
loading: true,
updating: false,
downloading: [],
};
const novelReducer = (state = initialState, action) => {
const { type, payload } = action;
const {type, payload} = action;
switch (type) {
case LOADING_NOVEL:
return { ...state, loading: true };
case FETCHING_NOVEL:
return { ...state, updating: true };
case SET_NOVEL:
return { ...state, novel: payload };
case GET_NOVEL:
return {
...state,
novel: payload,
chapters: payload.chapters,
loading: false,
updating: false,
};
case GET_CHAPTERS:
return {
...state,
chapters: payload,
loading: false,
updating: false,
};
case UPDATE_IN_LIBRARY:
return {
...state,
novel: { ...state.novel, followed: payload.followed },
};
case UPDATE_NOVEL:
return {
...state,
novel: payload.novel,
chapters: payload.chapters,
loading: false,
updating: false,
};
case CHAPTER_READ:
return {
...state,
chapters: state.chapters.map((chapter) =>
chapter.chapterId === payload.chapterId
? { ...chapter, read: 1 }
: chapter
),
};
case CHAPTER_UNREAD:
return {
...state,
chapters: state.chapters.map((chapter) =>
chapter.chapterId === payload.chapterId
? { ...chapter, read: 0 }
: chapter
),
};
case BOOKMARK_CHAPTER:
return {
...state,
chapters: state.chapters.map((chapter) =>
chapter.chapterId === payload.chapterId
? { ...chapter, bookmark: !payload.bookmark }
: chapter
),
};
case CHAPTER_DOWNLOADING:
return {
...state,
downloading: [...state.downloading, payload],
};
case CHAPTER_DOWNLOADED:
return {
...state,
chapters: state.chapters.map((chapter) =>
chapter.chapterId === payload
? { ...chapter, downloaded: 1 }
: chapter
),
downloading: state.downloading.filter(
(chapterId) => chapterId !== payload
),
};
case CHAPTER_DELETED:
return {
...state,
chapters: state.chapters.map((chapter) =>
chapter.chapterId === payload
? { ...chapter, downloaded: 0 }
: chapter
),
};
case UPDATE_LAST_READ:
return {
...state,
novel: { ...state.novel, lastRead: payload },
};
switch (type) {
case LOADING_NOVEL:
return {...state, loading: true};
case FETCHING_NOVEL:
return {...state, updating: true};
case SET_NOVEL:
return {...state, novel: payload};
case GET_NOVEL:
return {
...state,
novel: payload,
chapters: payload.chapters,
loading: false,
updating: false,
};
case GET_CHAPTERS:
return {
...state,
chapters: payload,
loading: false,
updating: false,
};
case UPDATE_IN_LIBRARY:
return {
...state,
novel: {...state.novel, followed: payload.followed},
};
case UPDATE_NOVEL:
return {
...state,
novel: payload.novel,
chapters: payload.chapters,
loading: false,
updating: false,
};
case CHAPTER_READ:
return {
...state,
chapters: state.chapters.map(chapter =>
chapter.chapterId === payload.chapterId
? {...chapter, read: 1}
: chapter,
),
};
case CHAPTER_UNREAD:
return {
...state,
chapters: state.chapters.map(chapter =>
chapter.chapterId === payload.chapterId
? {...chapter, read: 0}
: chapter,
),
};
case BOOKMARK_CHAPTER:
return {
...state,
chapters: state.chapters.map(chapter =>
chapter.chapterId === payload.chapterId
? {...chapter, bookmark: !payload.bookmark}
: chapter,
),
};
case CHAPTER_DOWNLOADING:
return {
...state,
downloading: [...state.downloading, payload],
};
case CHAPTER_DOWNLOADED:
return {
...state,
chapters: state.chapters.map(chapter =>
chapter.chapterId === payload ? {...chapter, downloaded: 1} : chapter,
),
downloading: state.downloading.filter(
chapterId => chapterId !== payload,
),
};
case CHAPTER_DELETED:
return {
...state,
chapters: state.chapters.map(chapter =>
chapter.chapterId === payload ? {...chapter, downloaded: 0} : chapter,
),
};
case UPDATE_LAST_READ:
return {
...state,
novel: {...state.novel, lastRead: payload},
};
case MARK_PREVIOUS_CHAPTERS_READ:
return {
...state,
chapters: state.chapters.map((chapter) =>
chapter.chapterId < payload
? { ...chapter, read: 1 }
: chapter
),
};
case MARK_PREVIOUS_CHAPTERS_UNREAD:
return {
...state,
chapters: state.chapters.map((chapter) =>
chapter.chapterId < payload
? { ...chapter, read: 0 }
: chapter
),
};
case NOVEL_ERROR:
return {
...state,
novel: {},
chapters: [],
loading: false,
updating: false,
};
default:
return state;
}
case MARK_PREVIOUS_CHAPTERS_READ:
return {
...state,
chapters: state.chapters.map(chapter =>
chapter.chapterId < payload ? {...chapter, read: 1} : chapter,
),
};
case MARK_PREVIOUS_CHAPTERS_UNREAD:
return {
...state,
chapters: state.chapters.map(chapter =>
chapter.chapterId < payload ? {...chapter, read: 0} : chapter,
),
};
case NOVEL_ERROR:
return {
...state,
novel: {},
chapters: [],
loading: false,
updating: false,
};
default:
return state;
}
};
export default novelReducer;
+20 -20
View File
@@ -1,23 +1,23 @@
export const LOADING_NOVEL = "LOADING_NOVEL";
export const FETCHING_NOVEL = "FETCHING_NOVEL";
export const LOADING_CHAPTERS = "LOADING_CHAPTERS";
export const GET_NOVEL = "GET_NOVEL";
export const SET_NOVEL = "SET_NOVEL";
export const UPDATE_NOVEL = "UPDATE_NOVEL";
export const UPDATE_IN_LIBRARY = "UPDATE_IN_LIBRARY";
export const GET_CHAPTERS = "GET_CHAPTERS";
export const UPDATE_LAST_READ = "UPDATE_LAST_READ";
export const LOADING_NOVEL = 'LOADING_NOVEL';
export const FETCHING_NOVEL = 'FETCHING_NOVEL';
export const LOADING_CHAPTERS = 'LOADING_CHAPTERS';
export const GET_NOVEL = 'GET_NOVEL';
export const SET_NOVEL = 'SET_NOVEL';
export const UPDATE_NOVEL = 'UPDATE_NOVEL';
export const UPDATE_IN_LIBRARY = 'UPDATE_IN_LIBRARY';
export const GET_CHAPTERS = 'GET_CHAPTERS';
export const UPDATE_LAST_READ = 'UPDATE_LAST_READ';
export const CHAPTER_DOWNLOADED = "CHAPTER_DOWNLOADED";
export const CHAPTER_DELETED = "CHAPTER_DELETED";
export const CHAPTER_DOWNLOADING = "CHAPTER_DOWNLOADING";
export const CHAPTER_READ = "CHAPTER_READ";
export const CHAPTER_UNREAD = "CHAPTER_UNREAD";
export const GET_CHAPTER = "GET_CHAPTER";
export const NOVEL_ERROR = "NOVEL_ERROR";
export const BOOKMARK_CHAPTER = "BOOKMARK_CHAPTER";
export const CHAPTER_DOWNLOADED = 'CHAPTER_DOWNLOADED';
export const CHAPTER_DELETED = 'CHAPTER_DELETED';
export const CHAPTER_DOWNLOADING = 'CHAPTER_DOWNLOADING';
export const CHAPTER_READ = 'CHAPTER_READ';
export const CHAPTER_UNREAD = 'CHAPTER_UNREAD';
export const GET_CHAPTER = 'GET_CHAPTER';
export const NOVEL_ERROR = 'NOVEL_ERROR';
export const BOOKMARK_CHAPTER = 'BOOKMARK_CHAPTER';
export const SET_NOVEL_SETTINGS = "SET_NOVEL_SETTINGS";
export const SET_NOVEL_SETTINGS = 'SET_NOVEL_SETTINGS';
export const MARK_PREVIOUS_CHAPTERS_READ = "MARK_PREVIOUS_CHAPTERS_READ";
export const MARK_PREVIOUS_CHAPTERS_UNREAD = "MARK_PREVIOUS_CHAPTERS_UNREAD";
export const MARK_PREVIOUS_CHAPTERS_READ = 'MARK_PREVIOUS_CHAPTERS_READ';
export const MARK_PREVIOUS_CHAPTERS_UNREAD = 'MARK_PREVIOUS_CHAPTERS_UNREAD';
+12 -12
View File
@@ -1,14 +1,14 @@
import { SAVE_SCROLL_POSITION } from "../preferences/preference.types";
import {SAVE_SCROLL_POSITION} from '../preferences/preference.types';
export const saveScrollPosition =
(position, percentage, chapterId, novelId) => async (dispatch) => {
dispatch({
type: SAVE_SCROLL_POSITION,
payload: {
position,
percentage,
chapterId,
novelId,
},
});
};
(position, percentage, chapterId, novelId) => async dispatch => {
dispatch({
type: SAVE_SCROLL_POSITION,
payload: {
position,
percentage,
chapterId,
novelId,
},
});
};
+62 -62
View File
@@ -1,72 +1,72 @@
import { SET_NOVEL_SETTINGS } from "../novel/novel.types";
import {SET_NOVEL_SETTINGS} from '../novel/novel.types';
import {
SAVE_SCROLL_POSITION,
SET_CHAPTER_LIST_PREF,
SET_LAST_READ,
} from "./preference.types";
SAVE_SCROLL_POSITION,
SET_CHAPTER_LIST_PREF,
SET_LAST_READ,
} from './preference.types';
const initialState = {
novelSettings: {},
novelSettings: {},
};
const novelReducer = (state = initialState, action) => {
const { type, payload } = action;
const {type, payload} = action;
switch (type) {
case SET_LAST_READ:
return {
...state,
novelSettings: {
...state.novelSettings,
[payload.novelId]: {
...state.novelSettings[payload.novelId],
lastRead: payload.chapterId,
},
},
};
case SET_CHAPTER_LIST_PREF:
return {
...state,
novelSettings: {
...state.novelSettings,
[payload.novelId]: {
...state.novelSettings[payload.novelId],
sort: payload.sort,
filter: payload.filter,
},
},
};
case SET_NOVEL_SETTINGS:
return {
...state,
novelSettings: {
...state.novelSettings,
[payload.novelId]: {
...state.novelSettings[payload.novelId],
showChapterTitles: payload.value,
},
},
};
case SAVE_SCROLL_POSITION:
return {
...state,
novelSettings: {
...state.novelSettings,
[payload.novelId]: {
...state.novelSettings[payload.novelId],
position: {
...state.novelSettings[payload.novelId].position,
[payload.chapterId]: {
position: payload.position,
percentage: payload.percentage,
},
},
},
},
};
default:
return state;
}
switch (type) {
case SET_LAST_READ:
return {
...state,
novelSettings: {
...state.novelSettings,
[payload.novelId]: {
...state.novelSettings[payload.novelId],
lastRead: payload.chapterId,
},
},
};
case SET_CHAPTER_LIST_PREF:
return {
...state,
novelSettings: {
...state.novelSettings,
[payload.novelId]: {
...state.novelSettings[payload.novelId],
sort: payload.sort,
filter: payload.filter,
},
},
};
case SET_NOVEL_SETTINGS:
return {
...state,
novelSettings: {
...state.novelSettings,
[payload.novelId]: {
...state.novelSettings[payload.novelId],
showChapterTitles: payload.value,
},
},
};
case SAVE_SCROLL_POSITION:
return {
...state,
novelSettings: {
...state.novelSettings,
[payload.novelId]: {
...state.novelSettings[payload.novelId],
position: {
...state.novelSettings[payload.novelId].position,
[payload.chapterId]: {
position: payload.position,
percentage: payload.percentage,
},
},
},
},
};
default:
return state;
}
};
export default novelReducer;
+3 -3
View File
@@ -1,3 +1,3 @@
export const SET_CHAPTER_LIST_PREF = "SET_CHAPTER_LIST_PREF";
export const SET_LAST_READ = "SET_LAST_READ";
export const SAVE_SCROLL_POSITION = "SAVE_SCROLL_POSITION";
export const SET_CHAPTER_LIST_PREF = 'SET_CHAPTER_LIST_PREF';
export const SET_LAST_READ = 'SET_LAST_READ';
export const SAVE_SCROLL_POSITION = 'SAVE_SCROLL_POSITION';
+43 -43
View File
@@ -1,58 +1,58 @@
import {
SET_APP_THEME,
SET_NOVELS_PER_ROW,
SET_READER_SETTINGS,
SET_APP_SETTINGS,
SET_ACCENT_COLOR,
SET_RIPPLE_COLOR,
SET_AMOLED_MODE,
} from "./settings.types";
SET_APP_THEME,
SET_NOVELS_PER_ROW,
SET_READER_SETTINGS,
SET_APP_SETTINGS,
SET_ACCENT_COLOR,
SET_RIPPLE_COLOR,
SET_AMOLED_MODE,
} from './settings.types';
export const setAppTheme = (val) => (dispatch) => {
dispatch({
type: SET_APP_THEME,
payload: val,
});
export const setAppTheme = val => dispatch => {
dispatch({
type: SET_APP_THEME,
payload: val,
});
};
export const setNovelsPerRow = (val) => (dispatch) => {
dispatch({
type: SET_NOVELS_PER_ROW,
payload: val,
});
export const setNovelsPerRow = val => dispatch => {
dispatch({
type: SET_NOVELS_PER_ROW,
payload: val,
});
};
export const setAppSettings = (key, val) => (dispatch) => {
dispatch({
type: SET_APP_SETTINGS,
payload: { key, val },
});
export const setAppSettings = (key, val) => dispatch => {
dispatch({
type: SET_APP_SETTINGS,
payload: {key, val},
});
};
export const setReaderSettings = (key, val) => (dispatch) => {
dispatch({
type: SET_READER_SETTINGS,
payload: { key, val },
});
export const setReaderSettings = (key, val) => dispatch => {
dispatch({
type: SET_READER_SETTINGS,
payload: {key, val},
});
};
export const setAccentColor = (val) => (dispatch) => {
dispatch({
type: SET_ACCENT_COLOR,
payload: val,
});
export const setAccentColor = val => dispatch => {
dispatch({
type: SET_ACCENT_COLOR,
payload: val,
});
};
export const setRippleColor = (val) => (dispatch) => {
dispatch({
type: SET_RIPPLE_COLOR,
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 },
});
export const setAmoledMode = (id, val) => dispatch => {
dispatch({
type: SET_AMOLED_MODE,
payload: {id, val},
});
};
+147 -147
View File
@@ -1,43 +1,43 @@
import {
SET_APP_THEME,
SET_NOVELS_PER_ROW,
SET_READER_SETTINGS,
SET_APP_SETTINGS,
SET_ACCENT_COLOR,
SET_RIPPLE_COLOR,
SET_AMOLED_MODE,
} from "./settings.types";
SET_APP_THEME,
SET_NOVELS_PER_ROW,
SET_READER_SETTINGS,
SET_APP_SETTINGS,
SET_ACCENT_COLOR,
SET_RIPPLE_COLOR,
SET_AMOLED_MODE,
} from './settings.types';
import {
lightTheme,
darkTheme,
midnightDuskTheme,
greenAppleTheme,
irisBlueTheme,
strawberryDaiquiri,
takoTheme,
yinYangTheme,
springBlossomTheme,
colorsAmoled,
oceanicTheme,
yangTheme,
yotsubaTheme,
takoLightTheme,
} from "../../theme/theme";
lightTheme,
darkTheme,
midnightDuskTheme,
greenAppleTheme,
irisBlueTheme,
strawberryDaiquiri,
takoTheme,
yinYangTheme,
springBlossomTheme,
colorsAmoled,
oceanicTheme,
yangTheme,
yotsubaTheme,
takoLightTheme,
} from '../../theme/theme';
const themes = {
1: lightTheme,
2: darkTheme,
3: midnightDuskTheme,
4: greenAppleTheme,
5: irisBlueTheme,
6: oceanicTheme,
7: strawberryDaiquiri,
8: takoTheme,
9: yinYangTheme,
10: springBlossomTheme,
11: yangTheme,
12: yotsubaTheme,
13: takoLightTheme,
1: lightTheme,
2: darkTheme,
3: midnightDuskTheme,
4: greenAppleTheme,
5: irisBlueTheme,
6: oceanicTheme,
7: strawberryDaiquiri,
8: takoTheme,
9: yinYangTheme,
10: springBlossomTheme,
11: yangTheme,
12: yotsubaTheme,
13: takoLightTheme,
};
/**
@@ -50,136 +50,136 @@ const themes = {
*/
const initialState = {
/**
* General settings
*/
/**
* General settings
*/
incognitoMode: false,
incognitoMode: false,
/**
* Appearence settings
*/
/**
* Appearence settings
*/
theme: darkTheme,
displayMode: 0,
novelsPerRow: 3,
showHistoryTab: true,
showUpdatesTab: true,
showLabelsInNav: false,
useFabForContinueReading: false,
theme: darkTheme,
displayMode: 0,
novelsPerRow: 3,
showHistoryTab: true,
showUpdatesTab: true,
showLabelsInNav: false,
useFabForContinueReading: false,
/**
* Library settings
*/
/**
* Library settings
*/
showDownloadBadges: true,
showUnreadBadges: true,
showNumberOfNovels: false,
downloadedOnlyMode: false,
showDownloadBadges: true,
showUnreadBadges: true,
showNumberOfNovels: false,
downloadedOnlyMode: false,
/**
* Browse settings
*/
/**
* Browse settings
*/
searchAllSources: false,
searchAllSources: false,
/**
* Update settings
*/
/**
* Update settings
*/
onlyUpdateOngoingNovels: false,
updateLibraryOnLaunch: false,
downloadNewChapters: false,
onlyUpdateOngoingNovels: false,
updateLibraryOnLaunch: false,
downloadNewChapters: false,
/**
* Novel settings
*/
/**
* Novel settings
*/
hideBackdrop: false,
hideBackdrop: false,
/**
* Reader settings
*/
/**
* Reader settings
*/
fullScreenMode: true,
swipeGestures: true,
showScrollPercentage: true,
useWebViewForChapter: false,
showBatteryAndTime: false,
autoScroll: false,
autoScrollInterval: 10,
textSelectable: false,
fullScreenMode: true,
swipeGestures: true,
showScrollPercentage: true,
useWebViewForChapter: false,
showBatteryAndTime: false,
autoScroll: false,
autoScrollInterval: 10,
textSelectable: false,
reader: {
theme: 1,
textColor: "rgba(255,255,255,0.7)",
textSize: 16,
textAlign: "left",
padding: 5,
fontFamily: "",
lineHeight: 1.5,
customCSS: null,
},
reader: {
theme: 1,
textColor: 'rgba(255,255,255,0.7)',
textSize: 16,
textAlign: 'left',
padding: 5,
fontFamily: '',
lineHeight: 1.5,
customCSS: null,
},
};
const settingsReducer = (state = initialState, action) => {
const { type, payload } = action;
const {type, payload} = action;
switch (type) {
case SET_APP_THEME:
return { ...state, theme: themes[payload] };
case SET_NOVELS_PER_ROW:
return {
...state,
novelsPerRow: state.displayMode !== 2 ? payload : 1,
};
case SET_ACCENT_COLOR:
return {
...state,
theme: {
...state.theme,
colorAccent: payload,
},
};
case SET_RIPPLE_COLOR:
return {
...state,
theme: {
...state.theme,
rippleColor: payload,
},
};
case SET_AMOLED_MODE:
return {
...state,
theme: payload.val
? {
...colorsAmoled,
colorAccent: state.theme.colorAccent,
rippleColor: state.theme.rippleColor,
colorButtonText: state.theme.colorButtonText,
id: state.theme.id,
name: state.theme.name,
}
: themes[payload.id],
};
switch (type) {
case SET_APP_THEME:
return {...state, theme: themes[payload]};
case SET_NOVELS_PER_ROW:
return {
...state,
novelsPerRow: state.displayMode !== 2 ? payload : 1,
};
case SET_ACCENT_COLOR:
return {
...state,
theme: {
...state.theme,
colorAccent: payload,
},
};
case SET_RIPPLE_COLOR:
return {
...state,
theme: {
...state.theme,
rippleColor: payload,
},
};
case SET_AMOLED_MODE:
return {
...state,
theme: payload.val
? {
...colorsAmoled,
colorAccent: state.theme.colorAccent,
rippleColor: state.theme.rippleColor,
colorButtonText: state.theme.colorButtonText,
id: state.theme.id,
name: state.theme.name,
}
: themes[payload.id],
};
case SET_APP_SETTINGS:
return {
...state,
[payload.key]: payload.val,
};
case SET_READER_SETTINGS:
return {
...state,
reader: {
...state.reader,
[payload.key]: payload.val,
},
};
default:
return state;
}
case SET_APP_SETTINGS:
return {
...state,
[payload.key]: payload.val,
};
case SET_READER_SETTINGS:
return {
...state,
reader: {
...state.reader,
[payload.key]: payload.val,
},
};
default:
return state;
}
};
export default settingsReducer;
+7 -7
View File
@@ -1,7 +1,7 @@
export const SET_APP_SETTINGS = "SET_APP_SETTINGS";
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";
export const SET_APP_SETTINGS = 'SET_APP_SETTINGS';
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';
+45 -45
View File
@@ -1,57 +1,57 @@
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";
ENABLE_DISCOVER,
FILTER_LANGUAGE,
GET_SOURCES,
PIN_SOURCES,
SEARCH_SOURCES,
} from './source.types';
import {showToast} from '../../hooks/showToast';
import sources from '../../sources/sources.json';
export const getSourcesAction = () => async (dispatch) => {
try {
/**
* Sort sources aplhabetically
*/
let sortedSources = sources.sort((a, b) =>
a.sourceName.localeCompare(b.sourceName)
);
dispatch({
type: GET_SOURCES,
payload: sortedSources,
});
} catch (error) {
showToast(error.message);
}
};
export const searchSourcesAction = (searchText) => async (dispatch) => {
const res = searchText.toLowerCase();
export const getSourcesAction = () => async dispatch => {
try {
/**
* Sort sources aplhabetically
*/
let sortedSources = sources.sort((a, b) =>
a.sourceName.localeCompare(b.sourceName),
);
dispatch({
type: SEARCH_SOURCES,
payload: res,
type: GET_SOURCES,
payload: sortedSources,
});
} catch (error) {
showToast(error.message);
}
};
export const pinSourceAction = (sourceId) => async (dispatch) => {
dispatch({
type: PIN_SOURCES,
payload: sourceId,
});
export const searchSourcesAction = searchText => async dispatch => {
const res = searchText.toLowerCase();
dispatch({
type: SEARCH_SOURCES,
payload: res,
});
};
export const filterLanguage = (language) => async (dispatch) => {
dispatch({
type: FILTER_LANGUAGE,
payload: language,
});
export const pinSourceAction = sourceId => async dispatch => {
dispatch({
type: PIN_SOURCES,
payload: sourceId,
});
};
export const enableDiscover = (key) => async (dispatch) => {
dispatch({
type: ENABLE_DISCOVER,
payload: key,
});
export const filterLanguage = language => async dispatch => {
dispatch({
type: FILTER_LANGUAGE,
payload: language,
});
};
export const enableDiscover = key => async dispatch => {
dispatch({
type: ENABLE_DISCOVER,
payload: key,
});
};
+52 -54
View File
@@ -1,65 +1,63 @@
import {
GET_SOURCES,
SEARCH_SOURCES,
PIN_SOURCES,
FILTER_LANGUAGE,
ENABLE_DISCOVER,
} from "./source.types";
GET_SOURCES,
SEARCH_SOURCES,
PIN_SOURCES,
FILTER_LANGUAGE,
ENABLE_DISCOVER,
} from './source.types';
const initialState = {
sources: [],
pinnedSources: [],
search: [],
pinned: [],
filters: [],
showMyAnimeList: true,
loading: true,
sources: [],
pinnedSources: [],
search: [],
pinned: [],
filters: [],
showMyAnimeList: true,
loading: true,
};
const sourceReducer = (state = initialState, action) => {
const { type, payload } = action;
const {type, payload} = action;
switch (type) {
case GET_SOURCES:
return { ...state, sources: payload, loading: false };
case PIN_SOURCES:
return {
...state,
pinned:
state.pinned.indexOf(payload) === -1
? [...state.pinned, payload]
: state.pinned.filter((source) => source !== payload),
loading: false,
};
case SEARCH_SOURCES:
return {
...state,
search: state.sources.filter((source) =>
source.sourceName.toLowerCase().includes(payload)
),
};
switch (type) {
case GET_SOURCES:
return {...state, sources: payload, loading: false};
case PIN_SOURCES:
return {
...state,
pinned:
state.pinned.indexOf(payload) === -1
? [...state.pinned, payload]
: state.pinned.filter(source => source !== payload),
loading: false,
};
case SEARCH_SOURCES:
return {
...state,
search: state.sources.filter(source =>
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;
}
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;
}
};
export default sourceReducer;
+5 -5
View File
@@ -1,5 +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";
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';
+26 -26
View File
@@ -1,35 +1,35 @@
import { createStore, applyMiddleware, combineReducers } from "redux";
import { persistStore, persistReducer } from "redux-persist";
import AsyncStorage from "@react-native-async-storage/async-storage";
import thunk from "redux-thunk";
import {createStore, applyMiddleware, combineReducers} from 'redux';
import {persistStore, persistReducer} from 'redux-persist';
import AsyncStorage from '@react-native-async-storage/async-storage';
import thunk from 'redux-thunk';
import settingsReducer from "./settings/settings.reducer";
import libraryReducer from "./library/library.reducer";
import sourceReducer from "./source/source.reducers";
import novelReducer from "./novel/novel.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";
import settingsReducer from './settings/settings.reducer';
import libraryReducer from './library/library.reducer';
import sourceReducer from './source/source.reducers';
import novelReducer from './novel/novel.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",
storage: AsyncStorage,
blacklist: ["novelReducer"],
key: 'root',
storage: AsyncStorage,
blacklist: ['novelReducer'],
};
const persistedReducer = persistReducer(
persistConfig,
combineReducers({
settingsReducer,
libraryReducer,
sourceReducer,
novelReducer,
updatesReducer,
trackerReducer,
preferenceReducer,
downloadsReducer,
})
persistConfig,
combineReducers({
settingsReducer,
libraryReducer,
sourceReducer,
novelReducer,
updatesReducer,
trackerReducer,
preferenceReducer,
downloadsReducer,
}),
);
export const store = createStore(persistedReducer, applyMiddleware(thunk));
+39 -39
View File
@@ -1,56 +1,56 @@
import { findListItem, updateItem } from "../../services/Trackers/myAnimeList";
import {findListItem, updateItem} from '../../services/Trackers/myAnimeList';
import {
REMOVE_TRACKER,
SET_TRACKER,
TRACK_NOVEL,
UNTRACK_NOVEL,
UPDATE_TRACKER,
UPDATE_CHAPTERS_READ,
} from "./tracker.types";
REMOVE_TRACKER,
SET_TRACKER,
TRACK_NOVEL,
UNTRACK_NOVEL,
UPDATE_TRACKER,
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({
type: SET_TRACKER,
payload: res,
});
export const setTracker = res => async dispatch => {
dispatch({
type: SET_TRACKER,
payload: res,
});
};
export const removeTracker = () => async (dispatch) => {
dispatch({ type: REMOVE_TRACKER });
export const removeTracker = () => async dispatch => {
dispatch({type: REMOVE_TRACKER});
};
export const trackNovel = (trackObj, accessToken) => async (dispatch) => {
let res = await findListItem(trackObj.id, accessToken);
trackObj.num_chapters = res.num_chapters;
trackObj.my_list_status = {
num_chapters_read: res.my_list_status.num_chapters_read,
score: res.my_list_status.score,
status: res.my_list_status.status,
};
export const trackNovel = (trackObj, accessToken) => async dispatch => {
let res = await findListItem(trackObj.id, accessToken);
trackObj.num_chapters = res.num_chapters;
trackObj.my_list_status = {
num_chapters_read: res.my_list_status.num_chapters_read,
score: res.my_list_status.score,
status: res.my_list_status.status,
};
dispatch({ type: TRACK_NOVEL, payload: trackObj });
dispatch({type: TRACK_NOVEL, payload: trackObj});
};
export const updateTracker = (malId, accessToken, body) => async (dispatch) => {
let res = await updateItem(malId, accessToken, body);
export const updateTracker = (malId, accessToken, body) => async dispatch => {
let res = await updateItem(malId, accessToken, body);
dispatch({ type: UPDATE_TRACKER, payload: { malId, ...res } });
dispatch({type: UPDATE_TRACKER, payload: {malId, ...res}});
};
export const untrackNovel = (id) => async (dispatch) => {
dispatch({ type: UNTRACK_NOVEL, payload: id });
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,
});
(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},
});
};
+69 -71
View File
@@ -1,81 +1,79 @@
import {
REMOVE_TRACKER,
SET_TRACKER,
TRACK_NOVEL,
UNTRACK_NOVEL,
UPDATE_CHAPTERS_READ,
UPDATE_TRACKER,
} from "./tracker.types";
REMOVE_TRACKER,
SET_TRACKER,
TRACK_NOVEL,
UNTRACK_NOVEL,
UPDATE_CHAPTERS_READ,
UPDATE_TRACKER,
} from './tracker.types';
const initialState = {
tracker: null,
trackedNovels: [],
tracker: null,
trackedNovels: [],
};
const trackerReducer = (state = initialState, action) => {
const { type, payload } = action;
const {type, payload} = action;
switch (type) {
case SET_TRACKER:
return {
...state,
tracker: { ...payload, name: "MyAnimeList" },
};
case REMOVE_TRACKER:
return {
...state,
tracker: null,
};
case TRACK_NOVEL:
return {
...state,
trackedNovels: state.trackedNovels.some(
(obj) => obj.novelId === payload.novelId
)
? state.trackedNovels
: [...state.trackedNovels, payload],
};
case UPDATE_TRACKER:
return {
...state,
trackedNovels: state.trackedNovels.map((novel) =>
novel.id === payload.malId
? {
...novel,
my_list_status: {
num_chapters_read: payload.num_chapters_read,
status: payload.status,
score: payload.score,
},
}
: novel
),
};
case UNTRACK_NOVEL:
return {
...state,
trackedNovels: state.trackedNovels.filter(
(id) => id === payload
),
};
case UPDATE_CHAPTERS_READ:
return {
...state,
trackedNovels: state.trackedNovels.map((novel) =>
novel.id === payload.malId
? {
...novel,
my_list_status: {
...novel.my_list_status,
num_chapters_read: payload.chaptersRead,
},
}
: novel
),
};
default:
return state;
}
switch (type) {
case SET_TRACKER:
return {
...state,
tracker: {...payload, name: 'MyAnimeList'},
};
case REMOVE_TRACKER:
return {
...state,
tracker: null,
};
case TRACK_NOVEL:
return {
...state,
trackedNovels: state.trackedNovels.some(
obj => obj.novelId === payload.novelId,
)
? state.trackedNovels
: [...state.trackedNovels, payload],
};
case UPDATE_TRACKER:
return {
...state,
trackedNovels: state.trackedNovels.map(novel =>
novel.id === payload.malId
? {
...novel,
my_list_status: {
num_chapters_read: payload.num_chapters_read,
status: payload.status,
score: payload.score,
},
}
: novel,
),
};
case UNTRACK_NOVEL:
return {
...state,
trackedNovels: state.trackedNovels.filter(id => id === payload),
};
case UPDATE_CHAPTERS_READ:
return {
...state,
trackedNovels: state.trackedNovels.map(novel =>
novel.id === payload.malId
? {
...novel,
my_list_status: {
...novel.my_list_status,
num_chapters_read: payload.chaptersRead,
},
}
: novel,
),
};
default:
return state;
}
};
export default trackerReducer;
+6 -6
View File
@@ -1,9 +1,9 @@
export const SET_TRACKER = "SET_TRACKER";
export const REMOVE_TRACKER = "REMOVE_TRACKER";
export const SET_TRACKER = 'SET_TRACKER';
export const REMOVE_TRACKER = 'REMOVE_TRACKER';
export const TRACK_NOVEL = "TRACK_NOVEL";
export const UNTRACK_NOVEL = "UNTRACK_NOVEL";
export const TRACK_NOVEL = 'TRACK_NOVEL';
export const UNTRACK_NOVEL = 'UNTRACK_NOVEL';
export const UPDATE_TRACKER = "UPDATE_TRACKER";
export const UPDATE_TRACKER = 'UPDATE_TRACKER';
export const UPDATE_CHAPTERS_READ = "UPDATE_CHAPTERS_READ";
export const UPDATE_CHAPTERS_READ = 'UPDATE_CHAPTERS_READ';
+33 -33
View File
@@ -1,47 +1,47 @@
import { GET_UPDATES, SET_LAST_UPDATE_TIME } from "./updates.types";
import {GET_UPDATES, SET_LAST_UPDATE_TIME} from './updates.types';
import { getUpdates } from "../../database/queries/UpdateQueries";
import { updateAllNovels } from "../../services/updates";
import {getUpdates} from '../../database/queries/UpdateQueries';
import {updateAllNovels} from '../../services/updates';
import { showToast } from "../../hooks/showToast";
import {showToast} from '../../hooks/showToast';
export const getUpdatesAction = () => async (dispatch) => {
const updates = await getUpdates();
export const getUpdatesAction = () => async dispatch => {
const updates = await getUpdates();
const groups = updates.reduce((groups, update) => {
var dateParts = update.updateTime.split("-");
var jsDate = new Date(
dateParts[0],
dateParts[1] - 1,
dateParts[2].substr(0, 2)
);
const date = jsDate.toISOString();
if (!groups[date]) {
groups[date] = [];
}
groups[date].push(update);
return groups;
}, {});
const groups = updates.reduce((groups, update) => {
var dateParts = update.updateTime.split('-');
var jsDate = new Date(
dateParts[0],
dateParts[1] - 1,
dateParts[2].substr(0, 2),
);
const date = jsDate.toISOString();
if (!groups[date]) {
groups[date] = [];
}
groups[date].push(update);
return groups;
}, {});
const groupedUpdates = Object.keys(groups).map((date) => {
return {
date,
chapters: groups[date],
};
});
const groupedUpdates = Object.keys(groups).map(date => {
return {
date,
chapters: groups[date],
};
});
dispatch({ type: GET_UPDATES, payload: groupedUpdates });
dispatch({type: GET_UPDATES, payload: groupedUpdates});
};
export const updateLibraryAction = () => async (dispatch, getState) => {
showToast("Updating library");
showToast('Updating library');
dispatch({ type: SET_LAST_UPDATE_TIME, payload: Date.now() });
dispatch({type: SET_LAST_UPDATE_TIME, payload: Date.now()});
const { downloadNewChapters = false, onlyUpdateOngoingNovels = false } =
getState().settingsReducer;
const {downloadNewChapters = false, onlyUpdateOngoingNovels = false} =
getState().settingsReducer;
await updateAllNovels({ downloadNewChapters, onlyUpdateOngoingNovels });
await updateAllNovels({downloadNewChapters, onlyUpdateOngoingNovels});
dispatch(getUpdatesAction());
dispatch(getUpdatesAction());
};
+28 -28
View File
@@ -1,38 +1,38 @@
import {
LOAD_UPDATES,
GET_UPDATES,
SET_LAST_UPDATE_TIME,
SHOW_LAST_UPDATE_TIME,
} from "./updates.types";
LOAD_UPDATES,
GET_UPDATES,
SET_LAST_UPDATE_TIME,
SHOW_LAST_UPDATE_TIME,
} from './updates.types';
const initialState = {
updates: [],
lastUpdateTime: null,
showLastUpdateTime: true,
loading: true,
updates: [],
lastUpdateTime: null,
showLastUpdateTime: true,
loading: true,
};
const updateReducer = (state = initialState, action) => {
const { type, payload } = action;
const {type, payload} = action;
switch (type) {
case LOAD_UPDATES:
return { ...state, loading: true };
case GET_UPDATES:
return { ...state, updates: payload, loading: false };
case SET_LAST_UPDATE_TIME:
return {
...state,
lastUpdateTime: payload,
};
case SHOW_LAST_UPDATE_TIME:
return {
...state,
showLastUpdateTime: payload,
};
default:
return state;
}
switch (type) {
case LOAD_UPDATES:
return {...state, loading: true};
case GET_UPDATES:
return {...state, updates: payload, loading: false};
case SET_LAST_UPDATE_TIME:
return {
...state,
lastUpdateTime: payload,
};
case SHOW_LAST_UPDATE_TIME:
return {
...state,
showLastUpdateTime: payload,
};
default:
return state;
}
};
export default updateReducer;
+4 -4
View File
@@ -1,4 +1,4 @@
export const GET_UPDATES = "GET_UPDATES";
export const LOAD_UPDATES = "LOAD_UPDATES";
export const SET_LAST_UPDATE_TIME = "SET_LAST_UPDATE_TIME";
export const SHOW_LAST_UPDATE_TIME = "SHOW_LAST_UPDATE_TIME";
export const GET_UPDATES = 'GET_UPDATES';
export const LOAD_UPDATES = 'LOAD_UPDATES';
export const SET_LAST_UPDATE_TIME = 'SET_LAST_UPDATE_TIME';
export const SHOW_LAST_UPDATE_TIME = 'SHOW_LAST_UPDATE_TIME';
+154 -168
View File
@@ -1,195 +1,181 @@
import React, { useEffect, useState } from "react";
import React, {useEffect, useState} from 'react';
import {
StyleSheet,
View,
Text,
FlatList,
ActivityIndicator,
RefreshControl,
} from "react-native";
StyleSheet,
View,
Text,
FlatList,
ActivityIndicator,
RefreshControl,
} from 'react-native';
import { useDispatch, useSelector } from "react-redux";
import {useDispatch, useSelector} from 'react-redux';
import {
getSourcesAction,
searchSourcesAction,
} from "../../redux/source/source.actions";
import { useTheme } from "../../hooks/reduxHooks";
import { showToast } from "../../hooks/showToast";
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 EmptyView from "../../components/EmptyView";
import DiscoverCard from "./discover/DiscoverCard";
import {Searchbar} from '../../components/Searchbar/Searchbar';
import SourceCard from './components/SourceCard';
import EmptyView from '../../components/EmptyView';
import DiscoverCard from './discover/DiscoverCard';
const Browse = ({ navigation }) => {
const [refreshing, setRefreshing] = useState(false);
const [searchText, setSearchText] = useState("");
const Browse = ({navigation}) => {
const [refreshing, setRefreshing] = useState(false);
const [searchText, setSearchText] = useState('');
let {
sources,
search,
loading,
pinned,
filters = [],
showMyAnimeList = true,
} = useSelector((state) => state.sourceReducer);
const theme = useTheme();
const dispatch = useDispatch();
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;
const isPinned = sourceId => (pinned.indexOf(sourceId) === -1 ? false : true);
sources = sources.filter((source) => filters.indexOf(source.lang) === -1);
search = search.filter((source) => filters.indexOf(source.lang) === -1);
sources = sources.filter(source => filters.indexOf(source.lang) === -1);
search = search.filter(source => filters.indexOf(source.lang) === -1);
let pinnedSources = sources.filter((source) => isPinned(source.sourceId));
let pinnedSources = sources.filter(source => isPinned(source.sourceId));
useEffect(() => {
dispatch(getSourcesAction());
}, [getSourcesAction]);
useEffect(() => {
dispatch(getSourcesAction());
}, [getSourcesAction]);
const onRefresh = () => {
showToast("Updating extension list");
setRefreshing(true);
getSourcesAction();
setRefreshing(false);
};
const onRefresh = () => {
showToast('Updating extension list');
setRefreshing(true);
getSourcesAction();
setRefreshing(false);
};
const clearSearchbar = () => {
setSearchText("");
};
const clearSearchbar = () => {
setSearchText('');
};
const onChangeText = (text) => {
setSearchText(text);
dispatch(searchSourcesAction(text));
};
const onChangeText = text => {
setSearchText(text);
dispatch(searchSourcesAction(text));
};
const renderItem = ({ item }) => (
<SourceCard
item={item}
isPinned={isPinned(item.sourceId)}
dispatch={dispatch}
navigation={navigation}
theme={theme}
/>
);
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>
);
const SourceHeader = ({title}) => (
<Text style={[styles.sourcesHeader, {color: theme.textColorSecondary}]}>
{title}
</Text>
);
return (
<View
style={[
styles.container,
{ backgroundColor: theme.colorPrimaryDark },
]}
>
<Searchbar
theme={theme}
placeholder="Search Source"
backAction="magnify"
searchText={searchText}
onChangeText={onChangeText}
clearSearchbar={clearSearchbar}
actions={[
{
icon: "book-search",
onPress: () => navigation.navigate("GlobalSearch"),
},
{
icon: "swap-vertical-variant",
onPress: () => navigation.navigate("Migration"),
},
{
icon: "cog-outline",
onPress: () => navigation.navigate("BrowseSettings"),
},
]}
/>
return (
<View style={[styles.container, {backgroundColor: theme.colorPrimaryDark}]}>
<Searchbar
theme={theme}
placeholder="Search Source"
backAction="magnify"
searchText={searchText}
onChangeText={onChangeText}
clearSearchbar={clearSearchbar}
actions={[
{
icon: 'book-search',
onPress: () => navigation.navigate('GlobalSearch'),
},
{
icon: 'swap-vertical-variant',
onPress: () => navigation.navigate('Migration'),
},
{
icon: 'cog-outline',
onPress: () => navigation.navigate('BrowseSettings'),
},
]}
/>
<FlatList
contentContainerStyle={{ flexGrow: 1, paddingBottom: 32 }}
data={!searchText ? sources : search}
keyExtractor={(item) => item.sourceId.toString()}
<FlatList
contentContainerStyle={{flexGrow: 1, paddingBottom: 32}}
data={!searchText ? sources : search}
keyExtractor={item => item.sourceId.toString()}
renderItem={renderItem}
extraData={pinnedSources}
ListHeaderComponent={
<View>
{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={pinnedSources}
keyExtractor={item => item.sourceId.toString()}
renderItem={renderItem}
extraData={pinnedSources}
ListHeaderComponent={
<View>
{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={pinnedSources}
keyExtractor={(item) =>
item.sourceId.toString()
}
renderItem={renderItem}
extraData={pinnedSources}
ListHeaderComponent={
<SourceHeader title="Pinned" />
}
/>
)}
{sources.length > 0 && <SourceHeader title="Sources" />}
</View>
}
ListEmptyComponent={
loading ? (
<ActivityIndicator
size="small"
color={theme.colorAccent}
style={{ marginTop: 16 }}
/>
) : (
sources.length === 0 && (
<EmptyView
icon="(・Д・。"
description="Enable languages from settings"
/>
)
)
}
refreshControl={
<RefreshControl
refreshing={refreshing}
onRefresh={onRefresh}
colors={["white"]}
progressBackgroundColor={theme.colorAccent}
/>
}
ListHeaderComponent={<SourceHeader title="Pinned" />}
/>
)}
{sources.length > 0 && <SourceHeader title="Sources" />}
</View>
}
ListEmptyComponent={
loading ? (
<ActivityIndicator
size="small"
color={theme.colorAccent}
style={{marginTop: 16}}
/>
</View>
);
) : (
sources.length === 0 && (
<EmptyView
icon="(・Д・。"
description="Enable languages from settings"
/>
)
)
}
refreshControl={
<RefreshControl
refreshing={refreshing}
onRefresh={onRefresh}
colors={['white']}
progressBackgroundColor={theme.colorAccent}
/>
}
/>
</View>
);
};
export default Browse;
const styles = StyleSheet.create({
container: {
flex: 1,
},
sourcesHeader: {
paddingHorizontal: 16,
paddingVertical: 8,
},
container: {
flex: 1,
},
sourcesHeader: {
paddingHorizontal: 16,
paddingVertical: 8,
},
});
+115 -129
View File
@@ -1,142 +1,128 @@
import React from "react";
import { FlatList, Pressable, Text, View } from "react-native";
import React from 'react';
import {FlatList, Pressable, Text, View} from 'react-native';
import { Switch } from "react-native-paper";
import { useDispatch, useSelector } from "react-redux";
import {Switch} from 'react-native-paper';
import {useDispatch, useSelector} from 'react-redux';
import { Appbar } from "../../components/Appbar";
import { ScreenContainer } from "../../components/Common";
import { List } from "../../components/List";
import {Appbar} from '../../components/Appbar';
import {ScreenContainer} from '../../components/Common';
import {List} from '../../components/List';
import { useSettings, useTheme } from "../../hooks/reduxHooks";
import { setAppSettings } from "../../redux/settings/settings.actions";
import {useSettings, useTheme} from '../../hooks/reduxHooks';
import {setAppSettings} from '../../redux/settings/settings.actions';
import {
enableDiscover,
filterLanguage,
} from "../../redux/source/source.actions";
enableDiscover,
filterLanguage,
} from '../../redux/source/source.actions';
const BrowseSettings = ({ navigation }) => {
const theme = useTheme();
const dispatch = useDispatch();
const BrowseSettings = ({navigation}) => {
const theme = useTheme();
const dispatch = useDispatch();
const { filters = [], showMyAnimeList = true } = useSelector(
(state) => state.sourceReducer
);
const languages = [
"English",
"Spanish",
"Japanese",
"Chinese",
"Arabic",
"Indonesian",
"Russian",
"Turkish",
"French",
"Portuguese (Brazil)",
].sort();
const {filters = [], showMyAnimeList = true} = useSelector(
state => state.sourceReducer,
);
const languages = [
'English',
'Spanish',
'Japanese',
'Chinese',
'Arabic',
'Indonesian',
'Russian',
'Turkish',
'French',
'Portuguese (Brazil)',
].sort();
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>
);
};
const {searchAllSources = false} = useSettings();
const renderItem = ({item}) => {
return (
<View style={{ flex: 1 }}>
<Appbar onBackAction={navigation.goBack} title="Sources" />
<FlatList
contentContainerStyle={{ paddingBottom: 32 }}
data={languages}
keyExtractor={(item) => item}
renderItem={renderItem}
ListHeaderComponent={
<View>
<List.SubHeader theme={theme}>
Global Search
</List.SubHeader>
<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>
<List.Divider theme={theme} />
<List.SubHeader theme={theme}>Discover</List.SubHeader>
<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>
<List.Divider theme={theme} />
<List.SubHeader theme={theme}>Languages</List.SubHeader>
</View>
}
/>
</View>
<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 (
<View style={{flex: 1}}>
<Appbar onBackAction={navigation.goBack} title="Sources" />
<FlatList
contentContainerStyle={{paddingBottom: 32}}
data={languages}
keyExtractor={item => item}
renderItem={renderItem}
ListHeaderComponent={
<View>
<List.SubHeader theme={theme}>Global Search</List.SubHeader>
<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>
<List.Divider theme={theme} />
<List.SubHeader theme={theme}>Discover</List.SubHeader>
<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>
<List.Divider theme={theme} />
<List.SubHeader theme={theme}>Languages</List.SubHeader>
</View>
}
/>
</View>
);
};
export default BrowseSettings;
+45 -53
View File
@@ -1,64 +1,56 @@
import React from "react";
import { StyleSheet, View, FlatList, Text } from "react-native";
import { useLibrary, useTheme } from "../../hooks/reduxHooks";
import React from 'react';
import {StyleSheet, View, FlatList, Text} from 'react-native';
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;
const theme = useTheme();
const library = useLibrary();
const SourceNovels = ({navigation, route}) => {
const sourceId = route.params;
const theme = useTheme();
const library = useLibrary();
const sourceNovels = library.filter((novel) => novel.sourceId === sourceId);
const sourceNovels = library.filter(novel => novel.sourceId === sourceId);
const renderItem = ({ item }) => (
<ListView
item={item}
theme={theme}
onPress={() =>
navigation.navigate("MigrateNovel", {
sourceId: item.sourceId,
novelName: item.novelName,
})
}
/>
);
const renderItem = ({item}) => (
<ListView
item={item}
theme={theme}
onPress={() =>
navigation.navigate('MigrateNovel', {
sourceId: item.sourceId,
novelName: item.novelName,
})
}
/>
);
return (
<View
style={[
styles.container,
{ backgroundColor: theme.colorPrimaryDark },
]}
>
<Appbar
title="Select Novel"
onBackAction={() => navigation.goBack()}
/>
<FlatList
data={sourceNovels}
keyExtractor={(item) => item.novelId.toString()}
renderItem={renderItem}
ListEmptyComponent={
<Text
style={{
color: theme.textColorSecondary,
padding: 20,
textAlign: "center",
}}
>
Your library does not have any novels from this source
</Text>
}
/>
</View>
);
return (
<View style={[styles.container, {backgroundColor: theme.colorPrimaryDark}]}>
<Appbar title="Select Novel" onBackAction={() => navigation.goBack()} />
<FlatList
data={sourceNovels}
keyExtractor={item => item.novelId.toString()}
renderItem={renderItem}
ListEmptyComponent={
<Text
style={{
color: theme.textColorSecondary,
padding: 20,
textAlign: 'center',
}}
>
Your library does not have any novels from this source
</Text>
}
/>
</View>
);
};
export default SourceNovels;
const styles = StyleSheet.create({
container: { flex: 1 },
container: {flex: 1},
});
+82 -86
View File
@@ -1,98 +1,94 @@
import React from "react";
import { View, Text, Image, StyleSheet, Pressable } from "react-native";
import React from 'react';
import {View, Text, Image, StyleSheet, Pressable} from 'react-native';
import { Button, IconButton } from "react-native-paper";
import {Button, IconButton} from 'react-native-paper';
import { pinSourceAction } from "../../../redux/source/source.actions";
import {pinSourceAction} from '../../../redux/source/source.actions';
const ExtensionCard = ({ item, theme, isPinned, dispatch, navigation }) => {
const { sourceId, sourceName, icon, lang } = item;
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,
});
const navigateToExtension = () =>
navigation.navigate('Extension', {
sourceId: item.sourceId,
sourceName: item.sourceName,
url: item.url,
});
return (
<Pressable
style={styles.extensionCard}
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}
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>
);
>
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,
},
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,
},
});
+121 -129
View File
@@ -1,142 +1,134 @@
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";
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 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 (
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,
backgroundColor: theme.colorPrimary,
borderRadius: 8,
elevation: 1,
marginVertical: 4,
marginHorizontal: 8,
}}
style={{
flex: 1,
paddingHorizontal: 8,
paddingVertical: 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={{ 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>
<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",
},
novelUpdatesCard: {
flex: 1,
flexDirection: 'row',
},
contentContainer: {
flex: 1,
},
genreChip: {
marginTop: 4,
fontSize: 13,
// fontStyle: "italic",
},
});
+49 -49
View File
@@ -1,58 +1,58 @@
import React from "react";
import { View, Text, Image, StyleSheet, Pressable } from "react-native";
import React from 'react';
import {View, Text, Image, StyleSheet, Pressable} from 'react-native';
import { Button } from "react-native-paper";
import {Button} from 'react-native-paper';
const DiscoverCard = ({ icon, label, onPress, theme }) => {
return (
<Pressable
style={styles.discoverCard}
onPress={onPress}
android_ripple={{ color: theme.rippleColor }}
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,
}}
>
<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>
);
{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,
},
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,
},
});
+155 -164
View File
@@ -1,192 +1,183 @@
import React, { useEffect, useState } from "react";
import { StyleSheet, View, ActivityIndicator, FlatList } from "react-native";
import React, {useEffect, useState} from 'react';
import {StyleSheet, View, ActivityIndicator, FlatList} from 'react-native';
import * as WebBrowser from "expo-web-browser";
import * as WebBrowser from 'expo-web-browser';
import { Searchbar } from "../../../components/Searchbar/Searchbar";
import { ErrorView } from "../../../components/ErrorView/ErrorView";
import {Searchbar} from '../../../components/Searchbar/Searchbar';
import {ErrorView} from '../../../components/ErrorView/ErrorView';
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";
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();
const BrowseMalScreen = ({navigation, route}) => {
const theme = useTheme();
const [loading, setLoading] = useState(true);
const [novels, setNovels] = useState([]);
const [error, setError] = useState();
const [limit, setLimit] = useState(0);
const [loading, setLoading] = useState(true);
const [novels, setNovels] = useState([]);
const [error, setError] = useState();
const [limit, setLimit] = useState(0);
const [searchText, setSearchText] = useState("");
const [searchText, setSearchText] = useState('');
const malUrl = "https://myanimelist.net/topmanga.php?type=lightnovels";
const malUrl = 'https://myanimelist.net/topmanga.php?type=lightnovels';
const getNovels = async (limit) => {
try {
const data = await scrapeTopNovels(limit);
const getNovels = async limit => {
try {
const data = await scrapeTopNovels(limit);
setNovels((novels) => novels.concat(data));
setLoading(false);
} catch (error) {
setError(error.message);
setNovels([]);
setLoading(false);
showToast(error.message);
}
};
setNovels(novels => novels.concat(data));
setLoading(false);
} catch (error) {
setError(error.message);
setNovels([]);
setLoading(false);
showToast(error.message);
}
};
const clearSearchbar = () => {
getNovels();
setLoading(true);
setSearchText("");
};
const clearSearchbar = () => {
getNovels();
setLoading(true);
setSearchText('');
};
const getSearchResults = async () => {
try {
setLoading(true);
const getSearchResults = async () => {
try {
setLoading(true);
const data = await scrapeSearchResults(searchText);
const data = await scrapeSearchResults(searchText);
setNovels(data);
setLoading(false);
} catch (error) {
setError(error.message);
setNovels([]);
setLoading(false);
showToast(error.message);
}
};
setNovels(data);
setLoading(false);
} catch (error) {
setError(error.message);
setNovels([]);
setLoading(false);
showToast(error.message);
}
};
useEffect(() => {
getNovels();
}, []);
useEffect(() => {
getNovels();
}, []);
const renderItem = ({ item }) => (
<BrowseMalCard
novel={item}
theme={theme}
onPress={() =>
navigation.navigate("GlobalSearch", {
novelName: item.novelName,
})
}
/>
);
const renderItem = ({item}) => (
<BrowseMalCard
novel={item}
theme={theme}
onPress={() =>
navigation.navigate('GlobalSearch', {
novelName: item.novelName,
})
}
/>
);
const { displayMode, novelsPerRow } = useSettings();
const {displayMode, novelsPerRow} = useSettings();
const orientation = getDeviceOrientation();
const orientation = getDeviceOrientation();
const getNovelsPerRow = () => {
if (displayMode === 2) {
return 1;
}
const getNovelsPerRow = () => {
if (displayMode === 2) {
return 1;
}
if (orientation === "landscape") {
return 6;
} else {
return novelsPerRow;
}
};
const isCloseToBottom = ({
layoutMeasurement,
contentOffset,
contentSize,
}) => {
const paddingToBottom = 20;
return (
layoutMeasurement.height + contentOffset.y >=
contentSize.height - paddingToBottom
);
};
const ListEmptyComponent = () => (
<ErrorView
errorName={error || "No results found"}
actions={[
{
name: "Retry",
onPress: () => {
getNovels();
setLoading(true);
setError();
},
icon: "reload",
},
]}
theme={theme}
/>
);
if (orientation === 'landscape') {
return 6;
} else {
return novelsPerRow;
}
};
const isCloseToBottom = ({layoutMeasurement, contentOffset, contentSize}) => {
const paddingToBottom = 20;
return (
<View
style={[
styles.container,
{ backgroundColor: theme.colorPrimaryDark },
]}
>
<Searchbar
theme={theme}
placeholder="Search MyAnimeList"
backAction="arrow-left"
onBackAction={() => navigation.goBack()}
searchText={searchText}
onChangeText={(text) => setSearchText(text)}
onSubmitEditing={getSearchResults}
clearSearchbar={clearSearchbar}
actions={[
{
icon: "earth",
onPress: () => WebBrowser.openBrowserAsync(malUrl),
},
]}
/>
{loading ? (
<View style={{ flex: 1, justifyContent: "center" }}>
<ActivityIndicator size={60} color={theme.colorAccent} />
</View>
) : (
<FlatList
contentContainerStyle={styles.novelsContainer}
data={novels}
keyExtractor={(item) => item.novelName}
renderItem={renderItem}
ListEmptyComponent={ListEmptyComponent}
onScroll={({ nativeEvent }) => {
if (!searchText && isCloseToBottom(nativeEvent)) {
getNovels(limit + 50);
setLimit((limit) => limit + 50);
}
}}
ListFooterComponent={
!searchText && (
<View style={{ paddingVertical: 16 }}>
<ActivityIndicator color={theme.colorAccent} />
</View>
)
}
/>
)}
</View>
layoutMeasurement.height + contentOffset.y >=
contentSize.height - paddingToBottom
);
};
const ListEmptyComponent = () => (
<ErrorView
errorName={error || 'No results found'}
actions={[
{
name: 'Retry',
onPress: () => {
getNovels();
setLoading(true);
setError();
},
icon: 'reload',
},
]}
theme={theme}
/>
);
return (
<View style={[styles.container, {backgroundColor: theme.colorPrimaryDark}]}>
<Searchbar
theme={theme}
placeholder="Search MyAnimeList"
backAction="arrow-left"
onBackAction={() => navigation.goBack()}
searchText={searchText}
onChangeText={text => setSearchText(text)}
onSubmitEditing={getSearchResults}
clearSearchbar={clearSearchbar}
actions={[
{
icon: 'earth',
onPress: () => WebBrowser.openBrowserAsync(malUrl),
},
]}
/>
{loading ? (
<View style={{flex: 1, justifyContent: 'center'}}>
<ActivityIndicator size={60} color={theme.colorAccent} />
</View>
) : (
<FlatList
contentContainerStyle={styles.novelsContainer}
data={novels}
keyExtractor={item => item.novelName}
renderItem={renderItem}
ListEmptyComponent={ListEmptyComponent}
onScroll={({nativeEvent}) => {
if (!searchText && isCloseToBottom(nativeEvent)) {
getNovels(limit + 50);
setLimit(limit => limit + 50);
}
}}
ListFooterComponent={
!searchText && (
<View style={{paddingVertical: 16}}>
<ActivityIndicator color={theme.colorAccent} />
</View>
)
}
/>
)}
</View>
);
};
export default BrowseMalScreen;
const styles = StyleSheet.create({
container: {
flex: 1,
},
contentContainer: {
flex: 1,
},
novelsContainer: {
flexGrow: 1,
paddingBottom: 8,
paddingHorizontal: 4,
},
container: {
flex: 1,
},
contentContainer: {
flex: 1,
},
novelsContainer: {
flexGrow: 1,
paddingBottom: 8,
paddingHorizontal: 4,
},
});
@@ -1,77 +1,75 @@
import cheerio from "react-native-cheerio";
import cheerio from 'react-native-cheerio';
const scrapeTopNovels = async (pageNo) => {
const url =
"https://myanimelist.net/topmanga.php?type=lightnovels&limit=" + pageNo;
const scrapeTopNovels = async pageNo => {
const url =
'https://myanimelist.net/topmanga.php?type=lightnovels&limit=' + pageNo;
const res = await fetch(url);
const body = await res.text();
const res = await fetch(url);
const body = await res.text();
const $ = cheerio.load(body);
const $ = cheerio.load(body);
const novels = [];
const novels = [];
$("tr.ranking-list").each(function (res) {
const novelId = $(this).find("img").attr("data-src").split("/");
$('tr.ranking-list').each(function (res) {
const novelId = $(this).find('img').attr('data-src').split('/');
const novelCover =
'https://cdn.myanimelist.net/images/manga/' +
novelId[7] +
'/' +
novelId[8].split('.')[0] +
'.jpg';
const novelName = $(this).find('h3').text();
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);
});
return novels;
};
const scrapeSearchResults = async searchTerm => {
const url =
'https://myanimelist.net/manga.php?q=' + searchTerm + '&cat=manga&type=2';
const res = await fetch(url);
const body = await res.text();
const $ = cheerio.load(body);
const novels = [];
$('.list')
.find('tr')
.each(function (res) {
const novelName = $(this).find('a > strong').text();
if (novelName) {
const novelId = $(this).find('img').attr('data-src').split('/');
const novelCover =
"https://cdn.myanimelist.net/images/manga/" +
novelId[7] +
"/" +
novelId[8].split(".")[0] +
".jpg";
const novelName = $(this).find("h3").text();
'https://cdn.myanimelist.net/images/manga/' +
novelId[7] +
'/' +
novelId[8].split('.')[0] +
'.jpg';
const score = $(this)
.find(".js-top-ranking-score-col > span.score-label")
.text();
const score = $(this).find('td:nth-child(5)').text().trim();
const info = $(this).find("div.information").text().split(/\s\s+/);
const novel = { novelName, novelCover, score, info };
const novel = {novelName, novelCover, score};
novels.push(novel);
}
});
return novels;
return novels;
};
const scrapeSearchResults = async (searchTerm) => {
const url =
"https://myanimelist.net/manga.php?q=" +
searchTerm +
"&cat=manga&type=2";
const res = await fetch(url);
const body = await res.text();
const $ = cheerio.load(body);
const novels = [];
$(".list")
.find("tr")
.each(function (res) {
const novelName = $(this).find("a > strong").text();
if (novelName) {
const novelId = $(this).find("img").attr("data-src").split("/");
const novelCover =
"https://cdn.myanimelist.net/images/manga/" +
novelId[7] +
"/" +
novelId[8].split(".")[0] +
".jpg";
const score = $(this).find("td:nth-child(5)").text().trim();
const novel = { novelName, novelCover, score };
novels.push(novel);
}
});
return novels;
};
export { scrapeTopNovels, scrapeSearchResults };
export {scrapeTopNovels, scrapeSearchResults};
+148 -155
View File
@@ -1,168 +1,161 @@
import React, { useEffect, useState } from "react";
import { StyleSheet, View, FlatList, ActivityIndicator } from "react-native";
import React, {useEffect, useState} from 'react';
import {StyleSheet, View, FlatList, ActivityIndicator} from 'react-native';
import { ProgressBar } from "react-native-paper";
import { useSelector } from "react-redux";
import {ProgressBar} from 'react-native-paper';
import {useSelector} from 'react-redux';
import { Searchbar } from "../../../components/Searchbar/Searchbar";
import EmptyView from "../../../components/EmptyView";
import {Searchbar} from '../../../components/Searchbar/Searchbar';
import EmptyView from '../../../components/EmptyView';
import { ScreenContainer } from "../../../components/Common";
import GlobalSearchSourceItem from "./GlobalSearchSourceItem";
import {ScreenContainer} from '../../../components/Common';
import GlobalSearchSourceItem from './GlobalSearchSourceItem';
import { showToast } from "../../../hooks/showToast";
import { useLibrary, useSettings, useTheme } from "../../../hooks/reduxHooks";
import { getSource } from "../../../sources/sources";
import {showToast} from '../../../hooks/showToast';
import {useLibrary, useSettings, useTheme} from '../../../hooks/reduxHooks';
import {getSource} from '../../../sources/sources';
const GlobalSearch = ({ route, navigation }) => {
const theme = useTheme();
const GlobalSearch = ({route, navigation}) => {
const theme = useTheme();
let novelName = "";
if (route.params) {
novelName = route.params.novelName;
let novelName = '';
if (route.params) {
novelName = route.params.novelName;
}
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);
const [searchResults, setSearchResults] = useState([]);
const [progress, setProgress] = useState(0);
const library = useLibrary();
useEffect(() => {
let mounted = true;
novelName && onSubmitEditing();
return () => (mounted = false);
}, []);
const clearSearchbar = () => setSearchText('');
const onChangeText = text => setSearchText(text);
const onSubmitEditing = async () => {
setSearchResults([]);
setProgress(0);
let globalSearchSources = searchAllSources ? sources : pinnedSources;
for (let i = 0; i < globalSearchSources.length; i++) {
try {
setLoading(true);
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);
} 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);
}
setProgress(progress => progress + 1 / globalSearchSources.length);
}
};
let {
sources,
pinned,
filters = [],
} = useSelector((state) => state.sourceReducer);
sources = sources.filter((source) => filters.indexOf(source.lang) === -1);
const renderItem = ({item}) => (
<GlobalSearchSourceItem
source={item}
library={library}
theme={theme}
navigation={navigation}
/>
);
const pinnedSources = sources.filter(
(source) => pinned.indexOf(source.sourceId) !== -1
);
const { searchAllSources = false } = useSettings();
const [loading, setLoading] = useState(false);
const [searchText, setSearchText] = useState(novelName);
const [searchResults, setSearchResults] = useState([]);
const [progress, setProgress] = useState(0);
const library = useLibrary();
useEffect(() => {
let mounted = true;
novelName && onSubmitEditing();
return () => (mounted = false);
}, []);
const clearSearchbar = () => setSearchText("");
const onChangeText = (text) => setSearchText(text);
const onSubmitEditing = async () => {
setSearchResults([]);
setProgress(0);
let globalSearchSources = searchAllSources ? sources : pinnedSources;
for (let i = 0; i < globalSearchSources.length; i++) {
try {
setLoading(true);
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);
} 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);
}
setProgress(
(progress) => progress + 1 / globalSearchSources.length
);
}
};
const renderItem = ({ item }) => (
<GlobalSearchSourceItem
source={item}
library={library}
theme={theme}
navigation={navigation}
/>
);
return (
<ScreenContainer theme={theme}>
<Searchbar
theme={theme}
placeholder="Global Search"
backAction="arrow-left"
onBackAction={navigation.goBack}
searchText={searchText}
onChangeText={onChangeText}
onSubmitEditing={onSubmitEditing}
clearSearchbar={clearSearchbar}
/>
{progress > 0 && (
<ProgressBar color={theme.colorAccent} progress={progress} />
return (
<ScreenContainer theme={theme}>
<Searchbar
theme={theme}
placeholder="Global Search"
backAction="arrow-left"
onBackAction={navigation.goBack}
searchText={searchText}
onChangeText={onChangeText}
onSubmitEditing={onSubmitEditing}
clearSearchbar={clearSearchbar}
/>
{progress > 0 && (
<ProgressBar color={theme.colorAccent} progress={progress} />
)}
<FlatList
contentContainerStyle={{
flexGrow: 1,
padding: 4,
}}
data={searchResults}
keyExtractor={item => item.sourceId.toString()}
renderItem={renderItem}
extraData={pinned}
ListEmptyComponent={
<>
{!loading && (
<EmptyView
icon="__φ(..)"
description={`Search a novel in ${
searchAllSources
? 'all sources'
: pinned.length === 0
? 'pinned sources\n(No sources pinned)'
: 'pinned sources'
}`}
/>
)}
<FlatList
contentContainerStyle={{
flexGrow: 1,
padding: 4,
}}
data={searchResults}
keyExtractor={(item) => item.sourceId.toString()}
renderItem={renderItem}
extraData={pinned}
ListEmptyComponent={
<>
{!loading && (
<EmptyView
icon="__φ(..)"
description={`Search a novel in ${
searchAllSources
? "all sources"
: pinned.length === 0
? "pinned sources\n(No sources pinned)"
: "pinned sources"
}`}
/>
)}
</>
}
ListFooterComponent={
loading && (
<View
style={{
flex: 1,
justifyContent: "center",
padding: 16,
}}
>
<ActivityIndicator
size="large"
color={theme.colorAccent}
/>
</View>
)
}
/>
</ScreenContainer>
);
</>
}
ListFooterComponent={
loading && (
<View
style={{
flex: 1,
justifyContent: 'center',
padding: 16,
}}
>
<ActivityIndicator size="large" color={theme.colorAccent} />
</View>
)
}
/>
</ScreenContainer>
);
};
export default GlobalSearch;
@@ -1,70 +1,64 @@
import React from "react";
import { StyleSheet, View, Text, Image, Pressable } from "react-native";
import React from 'react';
import {StyleSheet, View, Text, Image, Pressable} from 'react-native';
const GlobalSearchNovelCover = ({
novel,
theme,
onPress,
inLibrary,
onLongPress,
novel,
theme,
onPress,
inLibrary,
onLongPress,
}) => {
const { novelName, novelCover } = novel;
const {novelName, novelCover} = novel;
return (
<View style={{ flex: 1, borderRadius: 6, overflow: "hidden" }}>
<Pressable
android_ripple={{ color: theme.rippleColor }}
style={styles.novelContainer}
onPress={onPress}
onLongPress={onLongPress}
>
<>
<Image
source={{
uri:
novelCover && !novelCover.startsWith("/")
? novelCover
: "https://github.com/LNReader/lnreader-sources/blob/main/src/coverNotAvailable.jpg?raw=true",
}}
style={[
styles.novelCover,
inLibrary && { opacity: 0.5 },
]}
progressiveRenderingEnabled={true}
/>
<Text
numberOfLines={2}
style={[
styles.title,
{ color: theme.textColorPrimary },
]}
>
{novelName}
</Text>
</>
</Pressable>
</View>
);
return (
<View style={{flex: 1, borderRadius: 6, overflow: 'hidden'}}>
<Pressable
android_ripple={{color: theme.rippleColor}}
style={styles.novelContainer}
onPress={onPress}
onLongPress={onLongPress}
>
<>
<Image
source={{
uri:
novelCover && !novelCover.startsWith('/')
? novelCover
: 'https://github.com/LNReader/lnreader-sources/blob/main/src/coverNotAvailable.jpg?raw=true',
}}
style={[styles.novelCover, inLibrary && {opacity: 0.5}]}
progressiveRenderingEnabled={true}
/>
<Text
numberOfLines={2}
style={[styles.title, {color: theme.textColorPrimary}]}
>
{novelName}
</Text>
</>
</Pressable>
</View>
);
};
export default GlobalSearchNovelCover;
const styles = StyleSheet.create({
novelContainer: {
paddingHorizontal: 5,
paddingVertical: 4,
borderRadius: 4,
},
novelCover: {
height: 150,
width: 115,
borderRadius: 4,
},
title: {
fontFamily: "pt-sans-bold",
fontSize: 14,
padding: 4,
flexWrap: "wrap",
width: 115,
},
novelContainer: {
paddingHorizontal: 5,
paddingVertical: 4,
borderRadius: 4,
},
novelCover: {
height: 150,
width: 115,
borderRadius: 4,
},
title: {
fontFamily: 'pt-sans-bold',
fontSize: 14,
padding: 4,
flexWrap: 'wrap',
width: 115,
},
});
@@ -1,53 +1,51 @@
import React from "react";
import { StyleSheet, FlatList, Text } from "react-native";
import React from 'react';
import {StyleSheet, FlatList, Text} from 'react-native';
import GlobalSearchNovelCover from "./GlobalSearchNovelCover";
import GlobalSearchNovelCover from './GlobalSearchNovelCover';
const GlobalSearchNovelList = ({ data, theme, library, navigation }) => {
const inLibrary = (sourceId, novelUrl) =>
library.some(
(obj) => obj.novelUrl === novelUrl && obj.sourceId === sourceId
);
const GlobalSearchNovelList = ({data, theme, library, navigation}) => {
const inLibrary = (sourceId, novelUrl) =>
library.some(obj => obj.novelUrl === novelUrl && obj.sourceId === sourceId);
const navigateToNovel = (item) => navigation.push("Novel", item);
const navigateToNovel = item => navigation.push('Novel', item);
const renderItem = ({ item }) => (
<GlobalSearchNovelCover
novel={item}
theme={theme}
onPress={() => navigateToNovel(item)}
inLibrary={inLibrary(item.sourceId, item.novelUrl)}
/>
);
const renderItem = ({item}) => (
<GlobalSearchNovelCover
novel={item}
theme={theme}
onPress={() => navigateToNovel(item)}
inLibrary={inLibrary(item.sourceId, item.novelUrl)}
/>
);
const ListEmptyComponent = (
<Text style={[{ color: theme.textColorSecondary }, styles.emptyList]}>
No results found
</Text>
);
const ListEmptyComponent = (
<Text style={[{color: theme.textColorSecondary}, styles.emptyList]}>
No results found
</Text>
);
return (
<FlatList
contentContainerStyle={styles.listContainer}
horizontal={true}
data={data}
keyExtractor={(item) => item.novelUrl}
renderItem={renderItem}
ListEmptyComponent={ListEmptyComponent}
/>
);
return (
<FlatList
contentContainerStyle={styles.listContainer}
horizontal={true}
data={data}
keyExtractor={item => item.novelUrl}
renderItem={renderItem}
ListEmptyComponent={ListEmptyComponent}
/>
);
};
export default GlobalSearchNovelList;
const styles = StyleSheet.create({
listContainer: {
flexGrow: 1,
paddingVertical: 8,
paddingHorizontal: 4,
},
emptyList: {
padding: 8,
paddingVertical: 4,
},
listContainer: {
flexGrow: 1,
paddingVertical: 8,
paddingHorizontal: 4,
},
emptyList: {
padding: 8,
paddingVertical: 4,
},
});
@@ -1,36 +1,34 @@
import React from "react";
import { StyleSheet, View, Text } from "react-native";
import React from 'react';
import {StyleSheet, View, Text} from 'react-native';
import GlobalSearchNovelList from "./GlobalSearchNovelList";
import GlobalSearchNovelList from './GlobalSearchNovelList';
const GlobalSearchSourceItem = ({ source, library, theme, navigation }) => {
const { sourceName, lang, novels } = source;
const GlobalSearchSourceItem = ({source, library, theme, navigation}) => {
const {sourceName, lang, novels} = source;
return (
<>
<View style={styles.sourceContainer}>
<Text style={{ color: theme.textColorPrimary }}>
{sourceName}
</Text>
<Text style={{ color: theme.textColorSecondary, fontSize: 12 }}>
{lang}
</Text>
</View>
<GlobalSearchNovelList
data={novels}
theme={theme}
library={library}
navigation={navigation}
/>
</>
);
return (
<>
<View style={styles.sourceContainer}>
<Text style={{color: theme.textColorPrimary}}>{sourceName}</Text>
<Text style={{color: theme.textColorSecondary, fontSize: 12}}>
{lang}
</Text>
</View>
<GlobalSearchNovelList
data={novels}
theme={theme}
library={library}
navigation={navigation}
/>
</>
);
};
export default GlobalSearchSourceItem;
const styles = StyleSheet.create({
sourceContainer: {
padding: 8,
paddingVertical: 16,
},
sourceContainer: {
padding: 8,
paddingVertical: 16,
},
});
+54 -59
View File
@@ -1,75 +1,70 @@
import React from "react";
import { StyleSheet, View, FlatList, Text } from "react-native";
import React from 'react';
import {StyleSheet, View, FlatList, Text} from 'react-native';
import { useSelector } from "react-redux";
import {useSelector} from 'react-redux';
import { Appbar } from "../../../components/Appbar";
import MigrationSourceCard from "./MigrationSourceCard";
import {Appbar} from '../../../components/Appbar';
import MigrationSourceCard from './MigrationSourceCard';
import { useLibrary, useTheme } from "../../../hooks/reduxHooks";
import {useLibrary, useTheme} from '../../../hooks/reduxHooks';
const GlobalSearch = ({ navigation }) => {
const theme = useTheme();
const GlobalSearch = ({navigation}) => {
const theme = useTheme();
const library = useLibrary();
let { sources } = useSelector((state) => state.sourceReducer);
const library = useLibrary();
let {sources} = useSelector(state => state.sourceReducer);
const novelsPerSource = (sourceId) =>
library.filter((novel) => novel.sourceId === sourceId).length;
const novelsPerSource = sourceId =>
library.filter(novel => novel.sourceId === sourceId).length;
sources = sources.filter(
(source) =>
library.filter((novel) => novel.sourceId === source.sourceId).length
);
sources = sources.filter(
source =>
library.filter(novel => novel.sourceId === source.sourceId).length,
);
const renderItem = ({ item }) => (
<MigrationSourceCard
item={item}
theme={theme}
noOfNovels={novelsPerSource(item.sourceId)}
buttonLabel="All"
onPress={() => navigation.navigate("SourceNovels", item.sourceId)}
/>
);
const renderItem = ({item}) => (
<MigrationSourceCard
item={item}
theme={theme}
noOfNovels={novelsPerSource(item.sourceId)}
buttonLabel="All"
onPress={() => navigation.navigate('SourceNovels', item.sourceId)}
/>
);
const ListHeaderComponent = (
<Text style={[{ color: theme.textColorSecondary }, styles.listHeader]}>
Select a Source To Migrate From
</Text>
);
const ListHeaderComponent = (
<Text style={[{color: theme.textColorSecondary}, styles.listHeader]}>
Select a Source To Migrate From
</Text>
);
return (
<View
style={[
styles.container,
{ backgroundColor: theme.colorPrimaryDark },
]}
>
<Appbar title="Select Source" onBackAction={navigation.goBack} />
<FlatList
data={sources}
keyExtractor={(item) => item.sourceId.toString()}
renderItem={renderItem}
ListHeaderComponent={ListHeaderComponent}
/>
</View>
);
return (
<View style={[styles.container, {backgroundColor: theme.colorPrimaryDark}]}>
<Appbar title="Select Source" onBackAction={navigation.goBack} />
<FlatList
data={sources}
keyExtractor={item => item.sourceId.toString()}
renderItem={renderItem}
ListHeaderComponent={ListHeaderComponent}
/>
</View>
);
};
export default GlobalSearch;
const styles = StyleSheet.create({
container: {
flex: 1,
},
containerStyle: {
padding: 20,
margin: 20,
borderRadius: 6,
},
listHeader: {
padding: 20,
paddingBottom: 10,
textTransform: "uppercase",
},
container: {
flex: 1,
},
containerStyle: {
padding: 20,
margin: 20,
borderRadius: 6,
},
listHeader: {
padding: 20,
paddingBottom: 10,
textTransform: 'uppercase',
},
});
+123 -127
View File
@@ -1,142 +1,138 @@
import React, { useState } from "react";
import { StyleSheet, FlatList, Text, View } from "react-native";
import React, {useState} from 'react';
import {StyleSheet, FlatList, Text, View} from 'react-native';
import { Portal, Modal, Button } from "react-native-paper";
import {Portal, Modal, Button} from 'react-native-paper';
import GlobalSearchNovelCover from "../globalsearch/GlobalSearchNovelCover";
import GlobalSearchNovelCover from '../globalsearch/GlobalSearchNovelCover';
import { migrateNovel } from "../../../database/queries/NovelQueries";
import { showToast } from "../../../hooks/showToast";
import {migrateNovel} from '../../../database/queries/NovelQueries';
import {showToast} from '../../../hooks/showToast';
const MigrationNovelList = ({ data, theme, library, navigation }) => {
const [selectedNovel, setSelectedNovel] = useState(false);
const MigrationNovelList = ({data, theme, library, navigation}) => {
const [selectedNovel, setSelectedNovel] = useState(false);
const [migrateNovelDialog, setMigrateNovelDialog] = useState(false);
const showMigrateNovelDialog = () => setMigrateNovelDialog(true);
const hideMigrateNovelDialog = () => setMigrateNovelDialog(false);
const [migrateNovelDialog, setMigrateNovelDialog] = useState(false);
const showMigrateNovelDialog = () => setMigrateNovelDialog(true);
const hideMigrateNovelDialog = () => setMigrateNovelDialog(false);
const inLibrary = (sourceId, novelUrl) => {
return library.some(
(obj) => obj.novelUrl === novelUrl && obj.sourceId === sourceId
);
};
const renderItem = ({ item }) => (
<GlobalSearchNovelCover
novel={item}
theme={theme}
onPress={() =>
showModal(item.sourceId, item.novelUrl, item.novelName)
}
onLongPress={() =>
navigation.navigate("Novel", {
...item,
sourceId: item.sourceId,
})
}
inLibrary={inLibrary(item.sourceId, item.novelUrl)}
/>
const inLibrary = (sourceId, novelUrl) => {
return library.some(
obj => obj.novelUrl === novelUrl && obj.sourceId === sourceId,
);
};
const showModal = (sourceId, novelUrl, novelName) => {
if (inLibrary(sourceId, novelUrl)) {
showToast("Novel already in library");
} else {
setSelectedNovel({ sourceId, novelUrl, novelName });
showMigrateNovelDialog();
const renderItem = ({item}) => (
<GlobalSearchNovelCover
novel={item}
theme={theme}
onPress={() => showModal(item.sourceId, item.novelUrl, item.novelName)}
onLongPress={() =>
navigation.navigate('Novel', {
...item,
sourceId: item.sourceId,
})
}
inLibrary={inLibrary(item.sourceId, item.novelUrl)}
/>
);
const showModal = (sourceId, novelUrl, novelName) => {
if (inLibrary(sourceId, novelUrl)) {
showToast('Novel already in library');
} else {
setSelectedNovel({sourceId, novelUrl, novelName});
showMigrateNovelDialog();
}
};
return (
<>
<FlatList
contentContainerStyle={styles.flatListCont}
horizontal={true}
data={data}
keyExtractor={item => item.novelUrl}
renderItem={renderItem}
ListEmptyComponent={
<Text
style={{
color: theme.textColorSecondary,
padding: 8,
paddingVertical: 4,
}}
>
No results found
</Text>
}
};
return (
<>
<FlatList
contentContainerStyle={styles.flatListCont}
horizontal={true}
data={data}
keyExtractor={(item) => item.novelUrl}
renderItem={renderItem}
ListEmptyComponent={
<Text
style={{
color: theme.textColorSecondary,
padding: 8,
paddingVertical: 4,
}}
>
No results found
</Text>
}
/>
<Portal>
<Modal
visible={migrateNovelDialog}
onDismiss={hideMigrateNovelDialog}
contentContainerStyle={{
padding: 20,
margin: 20,
borderRadius: 6,
backgroundColor: theme.colorPrimaryDark,
}}
>
<Text
style={{
color: theme.textColorPrimary,
fontSize: 18,
}}
>
{`Migrate ${selectedNovel.novelName}?`}
</Text>
<View
style={{
flexDirection: "row",
justifyContent: "flex-end",
}}
>
<Button
style={{ marginTop: 30 }}
labelStyle={{
color: theme.colorAccent,
letterSpacing: 0,
textTransform: "none",
}}
onPress={hideMigrateNovelDialog}
>
Cancel
</Button>
<Button
style={{ marginTop: 30 }}
labelStyle={{
color: theme.colorAccent,
letterSpacing: 0,
textTransform: "none",
}}
theme={{ colors: { primary: theme.colorAccent } }}
onPress={async () => {
hideMigrateNovelDialog();
await migrateNovel(
selectedNovel.sourceId,
selectedNovel.novelUrl
);
showToast(
`${selectedNovel.novelName} migrated to new source.`
);
}}
>
Migrate
</Button>
</View>
</Modal>
</Portal>
</>
);
/>
<Portal>
<Modal
visible={migrateNovelDialog}
onDismiss={hideMigrateNovelDialog}
contentContainerStyle={{
padding: 20,
margin: 20,
borderRadius: 6,
backgroundColor: theme.colorPrimaryDark,
}}
>
<Text
style={{
color: theme.textColorPrimary,
fontSize: 18,
}}
>
{`Migrate ${selectedNovel.novelName}?`}
</Text>
<View
style={{
flexDirection: 'row',
justifyContent: 'flex-end',
}}
>
<Button
style={{marginTop: 30}}
labelStyle={{
color: theme.colorAccent,
letterSpacing: 0,
textTransform: 'none',
}}
onPress={hideMigrateNovelDialog}
>
Cancel
</Button>
<Button
style={{marginTop: 30}}
labelStyle={{
color: theme.colorAccent,
letterSpacing: 0,
textTransform: 'none',
}}
theme={{colors: {primary: theme.colorAccent}}}
onPress={async () => {
hideMigrateNovelDialog();
await migrateNovel(
selectedNovel.sourceId,
selectedNovel.novelUrl,
);
showToast(`${selectedNovel.novelName} migrated to new source.`);
}}
>
Migrate
</Button>
</View>
</Modal>
</Portal>
</>
);
};
export default MigrationNovelList;
const styles = StyleSheet.create({
flatListCont: {
flexGrow: 1,
paddingVertical: 8,
paddingHorizontal: 4,
},
flatListCont: {
flexGrow: 1,
paddingVertical: 8,
paddingHorizontal: 4,
},
});
+129 -140
View File
@@ -1,158 +1,147 @@
import React, { useEffect, useState } from "react";
import React, {useEffect, useState} from 'react';
import {
StyleSheet,
View,
Text,
FlatList,
ActivityIndicator,
} from "react-native";
import { ProgressBar } from "react-native-paper";
import { useLibrary, useSettings, useTheme } from "../../../hooks/reduxHooks";
import { useSelector } from "react-redux";
StyleSheet,
View,
Text,
FlatList,
ActivityIndicator,
} from 'react-native';
import {ProgressBar} from 'react-native-paper';
import {useLibrary, useSettings, useTheme} from '../../../hooks/reduxHooks';
import {useSelector} from 'react-redux';
import EmptyView from "../../../components/EmptyView";
import MigrationNovelList from "./MigrationNovelList";
import { Appbar } from "../../../components/Appbar";
import EmptyView from '../../../components/EmptyView';
import MigrationNovelList from './MigrationNovelList';
import {Appbar} from '../../../components/Appbar';
import { showToast } from "../../../hooks/showToast";
import { migrateNovel } from "../../../database/queries/NovelQueries";
import { ScreenContainer } from "../../../components/Common";
import { getSource } from "../../../sources/sources";
import {showToast} from '../../../hooks/showToast';
import {migrateNovel} from '../../../database/queries/NovelQueries';
import {ScreenContainer} from '../../../components/Common';
import {getSource} from '../../../sources/sources';
const MigrationNovels = ({ navigation, route }) => {
const { sourceId, novelName } = route.params;
const theme = useTheme();
const MigrationNovels = ({navigation, route}) => {
const {sourceId, novelName} = route.params;
const theme = useTheme();
const [progress, setProgress] = useState(0);
const [loading, setLoading] = useState(false);
const [searchResults, setSearchResults] = useState("");
const { searchAllSources = false } = useSettings();
let {
sources,
pinned,
filters = [],
} = useSelector((state) => state.sourceReducer);
sources = sources.filter((source) => filters.indexOf(source.lang) === -1);
const [progress, setProgress] = useState(0);
const [loading, setLoading] = useState(false);
const [searchResults, setSearchResults] = useState('');
const {searchAllSources = false} = useSettings();
let {
sources,
pinned,
filters = [],
} = useSelector(state => state.sourceReducer);
sources = sources.filter(source => filters.indexOf(source.lang) === -1);
const library = useLibrary();
const library = useLibrary();
const pinnedSources = sources.filter(
(source) =>
pinned.indexOf(source.sourceId) !== -1 &&
source.sourceId !== sourceId
);
const pinnedSources = sources.filter(
source =>
pinned.indexOf(source.sourceId) !== -1 && source.sourceId !== sourceId,
);
const getSearchResults = async () => {
let migrationSources = searchAllSources ? sources : pinnedSources;
for (let i = 0; i < migrationSources.length; i++) {
try {
setLoading(true);
const getSearchResults = async () => {
let migrationSources = searchAllSources ? sources : pinnedSources;
for (let i = 0; i < migrationSources.length; i++) {
try {
setLoading(true);
const source = getSource(migrationSources[i].sourceId);
const data = await source.searchNovels(novelName);
const source = getSource(migrationSources[i].sourceId);
const data = await source.searchNovels(novelName);
setSearchResults((searchResults) => [
...searchResults,
{
sourceId: migrationSources[i].sourceId,
sourceName: migrationSources[i].sourceName,
sourceLanguage: migrationSources[i].lang,
novels: data,
},
]);
setLoading(false);
} catch (error) {
showToast(error.message);
setSearchResults(searchResults => [
...searchResults,
{
sourceId: migrationSources[i].sourceId,
sourceName: migrationSources[i].sourceName,
sourceLanguage: migrationSources[i].lang,
novels: data,
},
]);
setLoading(false);
} catch (error) {
showToast(error.message);
setSearchResults((searchResults) => [
...searchResults,
{
sourceId: migrationSources[i].sourceId,
sourceName: migrationSources[i].sourceName,
sourceLanguage: migrationSources[i].lang,
novels: [],
},
]);
setLoading(false);
}
setProgress((progress) => progress + 1 / migrationSources.length);
}
};
setSearchResults(searchResults => [
...searchResults,
{
sourceId: migrationSources[i].sourceId,
sourceName: migrationSources[i].sourceName,
sourceLanguage: migrationSources[i].lang,
novels: [],
},
]);
setLoading(false);
}
setProgress(progress => progress + 1 / migrationSources.length);
}
};
useEffect(() => {
getSearchResults();
}, []);
useEffect(() => {
getSearchResults();
}, []);
const renderItem = ({ item }) => (
<>
<View style={{ padding: 8, paddingVertical: 16 }}>
<Text style={{ color: theme.textColorPrimary }}>
{item.sourceName}
</Text>
<Text style={{ color: theme.textColorSecondary, fontSize: 12 }}>
{item.sourceLanguage}
</Text>
</View>
<MigrationNovelList
data={item.novels}
theme={theme}
library={library}
navigation={navigation}
/>
</>
);
const renderItem = ({item}) => (
<>
<View style={{padding: 8, paddingVertical: 16}}>
<Text style={{color: theme.textColorPrimary}}>{item.sourceName}</Text>
<Text style={{color: theme.textColorSecondary, fontSize: 12}}>
{item.sourceLanguage}
</Text>
</View>
<MigrationNovelList
data={item.novels}
theme={theme}
library={library}
navigation={navigation}
/>
</>
);
return (
<ScreenContainer theme={theme}>
<Appbar
title={novelName}
onBackAction={() => navigation.goBack()}
/>
{progress > 0 && (
<ProgressBar color={theme.colorAccent} progress={progress} />
return (
<ScreenContainer theme={theme}>
<Appbar title={novelName} onBackAction={() => navigation.goBack()} />
{progress > 0 && (
<ProgressBar color={theme.colorAccent} progress={progress} />
)}
<FlatList
contentContainerStyle={{
flexGrow: 1,
padding: 4,
}}
data={searchResults}
keyExtractor={item => item.sourceId.toString()}
renderItem={renderItem}
extraData={pinned}
ListEmptyComponent={
<>
{!loading && (
<EmptyView
icon="__φ(..)"
description={`Search a novel in your pinned sources ${
pinned.length === 0 ? '(No sources pinned)' : ''
}`}
/>
)}
<FlatList
contentContainerStyle={{
flexGrow: 1,
padding: 4,
}}
data={searchResults}
keyExtractor={(item) => item.sourceId.toString()}
renderItem={renderItem}
extraData={pinned}
ListEmptyComponent={
<>
{!loading && (
<EmptyView
icon="__φ(..)"
description={`Search a novel in your pinned sources ${
pinned.length === 0
? "(No sources pinned)"
: ""
}`}
/>
)}
</>
}
ListFooterComponent={
loading && (
<View
style={{
flex: 1,
justifyContent: "center",
padding: 16,
}}
>
<ActivityIndicator
size="large"
color={theme.colorAccent}
/>
</View>
)
}
/>
</ScreenContainer>
);
</>
}
ListFooterComponent={
loading && (
<View
style={{
flex: 1,
justifyContent: 'center',
padding: 16,
}}
>
<ActivityIndicator size="large" color={theme.colorAccent} />
</View>
)
}
/>
</ScreenContainer>
);
};
export default MigrationNovels;
@@ -1,66 +1,66 @@
import React from "react";
import { View, Text, Image, StyleSheet } from "react-native";
import { TouchableRipple } from "react-native-paper";
import React from 'react';
import {View, Text, Image, StyleSheet} from 'react-native';
import {TouchableRipple} from 'react-native-paper';
const MigrationSourceCard = ({ item, theme, noOfNovels, onPress }) => {
const { sourceName, icon, sourceLanguage } = item;
const MigrationSourceCard = ({item, theme, noOfNovels, onPress}) => {
const {sourceName, icon, sourceLanguage} = item;
return (
<TouchableRipple
style={styles.cardContainer}
onPress={onPress}
rippleColor={theme.rippleColor}
>
<>
<Image source={{ uri: icon }} style={styles.sourceIcon} />
<View style={styles.sourceDetailsContainer}>
<Text
style={{
color: theme.textColorPrimary,
fontSize: 14,
}}
>
{sourceName} {` (${noOfNovels || 0})`}
</Text>
<Text
style={{
color: theme.textColorSecondary,
fontSize: 12,
}}
>
{sourceLanguage}
</Text>
</View>
</>
</TouchableRipple>
);
return (
<TouchableRipple
style={styles.cardContainer}
onPress={onPress}
rippleColor={theme.rippleColor}
>
<>
<Image source={{uri: icon}} style={styles.sourceIcon} />
<View style={styles.sourceDetailsContainer}>
<Text
style={{
color: theme.textColorPrimary,
fontSize: 14,
}}
>
{sourceName} {` (${noOfNovels || 0})`}
</Text>
<Text
style={{
color: theme.textColorSecondary,
fontSize: 12,
}}
>
{sourceLanguage}
</Text>
</View>
</>
</TouchableRipple>
);
};
export default MigrationSourceCard;
const styles = StyleSheet.create({
cardContainer: {
flexDirection: "row",
alignItems: "center",
marginVertical: 4,
paddingVertical: 8,
paddingHorizontal: 20,
},
sourceIcon: {
width: 40,
height: 40,
borderRadius: 4,
},
sourceDetailsContainer: {
flex: 1,
flexDirection: "row",
justifyContent: "space-between",
alignItems: "center",
marginLeft: 16,
},
sourceStatus: {
color: "#C14033",
fontSize: 12,
marginLeft: 5,
},
cardContainer: {
flexDirection: 'row',
alignItems: 'center',
marginVertical: 4,
paddingVertical: 8,
paddingHorizontal: 20,
},
sourceIcon: {
width: 40,
height: 40,
borderRadius: 4,
},
sourceDetailsContainer: {
flex: 1,
flexDirection: 'row',
justifyContent: 'space-between',
alignItems: 'center',
marginLeft: 16,
},
sourceStatus: {
color: '#C14033',
fontSize: 12,
marginLeft: 5,
},
});
File diff suppressed because it is too large Load Diff
+118 -118
View File
@@ -1,129 +1,129 @@
import React from "react";
import { Dimensions, View, Text, StyleSheet } from "react-native";
import React from 'react';
import {Dimensions, View, Text, StyleSheet} from 'react-native';
import Slider from "@react-native-community/slider";
import { getDeviceOrientation } from "../../../services/utils/helpers";
import Slider from '@react-native-community/slider';
import {getDeviceOrientation} from '../../../services/utils/helpers';
const ReaderSeekbar = ({
theme,
hide,
contentSize,
scrollPercentage,
setLoading,
scrollViewRef,
theme,
hide,
contentSize,
scrollPercentage,
setLoading,
scrollViewRef,
}) => {
const onSlidingComplete = (value) => {
setLoading(true);
scrollViewRef.current.scrollTo({
x: 0,
y: Math.round((value * contentSize) / 100),
animated: false,
});
setLoading(false);
};
const onSlidingComplete = value => {
setLoading(true);
scrollViewRef.current.scrollTo({
x: 0,
y: Math.round((value * contentSize) / 100),
animated: false,
});
setLoading(false);
};
if (hide) {
return null;
} else if (getDeviceOrientation() === "potrait") {
return (
<View style={styles.verticalSliderContainer}>
<Text
style={{
color: "#FFFFFF",
marginLeft: 16,
transform: [{ rotate: "-90deg" }],
}}
>
{scrollPercentage}
</Text>
<Slider
style={{
flex: 1,
height: 40,
}}
minimumValue={0}
maximumValue={100}
step={1}
value={scrollPercentage}
onSlidingComplete={onSlidingComplete}
thumbTintColor={theme.colorAccent}
minimumTrackTintColor={theme.colorAccent}
maximumTrackTintColor={theme.textColorPrimary}
/>
<Text
style={{
color: "#FFFFFF",
marginRight: 16,
transform: [{ rotate: "-90deg" }],
}}
>
100
</Text>
</View>
);
} else {
return (
<View style={styles.horizontalSliderContainer}>
<Text
style={{
color: "#FFFFFF",
marginLeft: 16,
}}
>
{scrollPercentage}
</Text>
<Slider
style={{
flex: 1,
height: 40,
}}
minimumValue={0}
maximumValue={100}
step={1}
value={scrollPercentage}
onSlidingComplete={onSlidingComplete}
thumbTintColor={theme.colorAccent}
minimumTrackTintColor={theme.colorAccent}
maximumTrackTintColor={theme.textColorPrimary}
/>
<Text
style={{
color: "#FFFFFF",
marginRight: 16,
}}
>
100
</Text>
</View>
);
}
if (hide) {
return null;
} else if (getDeviceOrientation() === 'potrait') {
return (
<View style={styles.verticalSliderContainer}>
<Text
style={{
color: '#FFFFFF',
marginLeft: 16,
transform: [{rotate: '-90deg'}],
}}
>
{scrollPercentage}
</Text>
<Slider
style={{
flex: 1,
height: 40,
}}
minimumValue={0}
maximumValue={100}
step={1}
value={scrollPercentage}
onSlidingComplete={onSlidingComplete}
thumbTintColor={theme.colorAccent}
minimumTrackTintColor={theme.colorAccent}
maximumTrackTintColor={theme.textColorPrimary}
/>
<Text
style={{
color: '#FFFFFF',
marginRight: 16,
transform: [{rotate: '-90deg'}],
}}
>
100
</Text>
</View>
);
} else {
return (
<View style={styles.horizontalSliderContainer}>
<Text
style={{
color: '#FFFFFF',
marginLeft: 16,
}}
>
{scrollPercentage}
</Text>
<Slider
style={{
flex: 1,
height: 40,
}}
minimumValue={0}
maximumValue={100}
step={1}
value={scrollPercentage}
onSlidingComplete={onSlidingComplete}
thumbTintColor={theme.colorAccent}
minimumTrackTintColor={theme.colorAccent}
maximumTrackTintColor={theme.textColorPrimary}
/>
<Text
style={{
color: '#FFFFFF',
marginRight: 16,
}}
>
100
</Text>
</View>
);
}
};
export default ReaderSeekbar;
const styles = StyleSheet.create({
verticalSliderContainer: {
position: "absolute",
zIndex: 2,
right: -(Dimensions.get("window").width / 2) + 40,
bottom: 300,
backgroundColor: "rgba(0,0,0,0.7)",
flexDirection: "row",
justifyContent: "flex-end",
alignItems: "center",
borderRadius: 50,
marginHorizontal: 8,
transform: [{ rotate: "90deg" }],
},
horizontalSliderContainer: {
position: "absolute",
zIndex: 2,
bottom: 100,
backgroundColor: "rgba(0,0,0,0.7)",
flexDirection: "row",
justifyContent: "flex-end",
alignItems: "center",
borderRadius: 50,
marginHorizontal: 8,
},
verticalSliderContainer: {
position: 'absolute',
zIndex: 2,
right: -(Dimensions.get('window').width / 2) + 40,
bottom: 300,
backgroundColor: 'rgba(0,0,0,0.7)',
flexDirection: 'row',
justifyContent: 'flex-end',
alignItems: 'center',
borderRadius: 50,
marginHorizontal: 8,
transform: [{rotate: '90deg'}],
},
horizontalSliderContainer: {
position: 'absolute',
zIndex: 2,
bottom: 100,
backgroundColor: 'rgba(0,0,0,0.7)',
flexDirection: 'row',
justifyContent: 'flex-end',
alignItems: 'center',
borderRadius: 50,
marginHorizontal: 8,
},
});
+76 -82
View File
@@ -1,91 +1,85 @@
import React, { useState } from "react";
import { View } from "react-native";
import React, {useState} from 'react';
import {View} from 'react-native';
import { Appbar } from "react-native-paper";
import FadeView from "../../../components/Common/CrossFadeView";
import { bookmarkChapterAction } from "../../../redux/novel/novel.actions";
import {Appbar} from 'react-native-paper';
import FadeView from '../../../components/Common/CrossFadeView';
import {bookmarkChapterAction} from '../../../redux/novel/novel.actions';
const ChapterAppbar = ({
navigation,
bookmark,
novelName,
chapterId,
chapterName,
hide,
dispatch,
tts,
textToSpeech,
theme,
textToSpeechPosition,
pauseTts,
navigation,
bookmark,
novelName,
chapterId,
chapterName,
hide,
dispatch,
tts,
textToSpeech,
theme,
textToSpeechPosition,
pauseTts,
}) => {
const [bookmarked, setBookmarked] = useState(bookmark);
const [bookmarked, setBookmarked] = useState(bookmark);
return (
<FadeView
style={{
flex: 1,
position: "absolute",
width: "100%",
top: 0,
zIndex: 1,
return (
<FadeView
style={{
flex: 1,
position: 'absolute',
width: '100%',
top: 0,
zIndex: 1,
}}
active={hide}
animationDuration={150}
>
<View style={{flex: 1, backgroundColor: `${theme.colorPrimary}E6`}}>
<Appbar.Header style={{backgroundColor: 'transparent', elevation: 0}}>
<Appbar.BackAction
onPress={navigation.goBack}
color={theme.textColorPrimary}
size={26}
style={{marginRight: 0}}
/>
<Appbar.Content
title={novelName}
titleStyle={{color: theme.textColorPrimary}}
subtitle={chapterName}
subtitleStyle={{color: theme.textColorSecondary}}
/>
<Appbar.Action
icon="volume-high"
size={24}
onPress={tts}
color={
textToSpeech === 'progress'
? theme.colorAccent
: theme.textColorPrimary
}
/>
{textToSpeechPosition.end > 0 && (
<Appbar.Action
icon={textToSpeech === 'paused' ? 'play' : 'pause'}
size={24}
onPress={pauseTts}
color={theme.textColorPrimary}
/>
)}
<Appbar.Action
icon={bookmarked ? 'bookmark' : 'bookmark-outline'}
size={24}
onPress={() => {
dispatch(bookmarkChapterAction([{bookmark, chapterId}]));
setBookmarked(!bookmarked);
}}
active={hide}
animationDuration={150}
>
<View
style={{ flex: 1, backgroundColor: `${theme.colorPrimary}E6` }}
>
<Appbar.Header
style={{ backgroundColor: "transparent", elevation: 0 }}
>
<Appbar.BackAction
onPress={navigation.goBack}
color={theme.textColorPrimary}
size={26}
style={{ marginRight: 0 }}
/>
<Appbar.Content
title={novelName}
titleStyle={{ color: theme.textColorPrimary }}
subtitle={chapterName}
subtitleStyle={{ color: theme.textColorSecondary }}
/>
<Appbar.Action
icon="volume-high"
size={24}
onPress={tts}
color={
textToSpeech === "progress"
? theme.colorAccent
: theme.textColorPrimary
}
/>
{textToSpeechPosition.end > 0 && (
<Appbar.Action
icon={textToSpeech === "paused" ? "play" : "pause"}
size={24}
onPress={pauseTts}
color={theme.textColorPrimary}
/>
)}
<Appbar.Action
icon={bookmarked ? "bookmark" : "bookmark-outline"}
size={24}
onPress={() => {
dispatch(
bookmarkChapterAction([{ bookmark, chapterId }])
);
setBookmarked(!bookmarked);
}}
color={theme.textColorPrimary}
/>
</Appbar.Header>
</View>
</FadeView>
);
color={theme.textColorPrimary}
/>
</Appbar.Header>
</View>
</FadeView>
);
};
export default ChapterAppbar;
+126 -126
View File
@@ -1,103 +1,103 @@
import React from "react";
import { Pressable, StyleSheet, View } from "react-native";
import { IconButton } from "react-native-paper";
import { useSafeAreaInsets } from "react-native-safe-area-context";
import FadeView from "../../../components/Common/CrossFadeView";
import React from 'react';
import {Pressable, StyleSheet, View} from 'react-native';
import {IconButton} from 'react-native-paper';
import {useSafeAreaInsets} from 'react-native-safe-area-context';
import FadeView from '../../../components/Common/CrossFadeView';
const ChapterFooter = ({
hide,
theme,
swipeGestures,
readerSheetRef,
scrollViewRef,
navigateToNextChapter,
navigateToPrevChapter,
nextChapter,
prevChapter,
useWebViewForChapter,
enableSwipeGestures,
autoScroll,
enableWebView,
enableAutoScroll,
hide,
theme,
swipeGestures,
readerSheetRef,
scrollViewRef,
navigateToNextChapter,
navigateToPrevChapter,
nextChapter,
prevChapter,
useWebViewForChapter,
enableSwipeGestures,
autoScroll,
enableWebView,
enableAutoScroll,
}) => {
const rippleConfig = {
color: theme.rippleColor,
borderless: true,
radius: 50,
};
const rippleConfig = {
color: theme.rippleColor,
borderless: true,
radius: 50,
};
const insets = useSafeAreaInsets();
const insets = useSafeAreaInsets();
return (
<FadeView
style={{
position: "absolute",
zIndex: 2,
bottom: 0,
width: "100%",
}}
active={hide}
animationDuration={150}
return (
<FadeView
style={{
position: 'absolute',
zIndex: 2,
bottom: 0,
width: '100%',
}}
active={hide}
animationDuration={150}
>
<View
style={{
backgroundColor: `${theme.colorPrimary}E6`,
flexDirection: 'row',
paddingBottom: insets.bottom,
}}
>
<Pressable
android_ripple={rippleConfig}
style={styles.buttonStyles}
onPress={navigateToPrevChapter}
>
<View
style={{
backgroundColor: `${theme.colorPrimary}E6`,
flexDirection: "row",
paddingBottom: insets.bottom,
}}
<IconButton
icon="chevron-left"
size={26}
disabled={!prevChapter}
color={theme.textColorPrimary}
/>
</Pressable>
<Pressable
android_ripple={rippleConfig}
style={styles.buttonStyles}
onPress={enableSwipeGestures}
>
<IconButton
icon="gesture-swipe"
disabled={!swipeGestures}
size={26}
color={theme.textColorPrimary}
/>
</Pressable>
{!useWebViewForChapter && (
<>
<Pressable
android_ripple={rippleConfig}
style={styles.buttonStyles}
onPress={enableAutoScroll}
>
<Pressable
android_ripple={rippleConfig}
style={styles.buttonStyles}
onPress={navigateToPrevChapter}
>
<IconButton
icon="chevron-left"
size={26}
disabled={!prevChapter}
color={theme.textColorPrimary}
/>
</Pressable>
<Pressable
android_ripple={rippleConfig}
style={styles.buttonStyles}
onPress={enableSwipeGestures}
>
<IconButton
icon="gesture-swipe"
disabled={!swipeGestures}
size={26}
color={theme.textColorPrimary}
/>
</Pressable>
{!useWebViewForChapter && (
<>
<Pressable
android_ripple={rippleConfig}
style={styles.buttonStyles}
onPress={enableAutoScroll}
>
<IconButton
icon="chevron-triple-down"
disabled={!autoScroll}
size={26}
color={theme.textColorPrimary}
/>
</Pressable>
<Pressable
android_ripple={rippleConfig}
style={styles.buttonStyles}
onPress={() => scrollViewRef.current.scrollTo({})}
>
<IconButton
icon="format-vertical-align-top"
size={26}
color={theme.textColorPrimary}
/>
</Pressable>
</>
)}
{/* <Pressable
<IconButton
icon="chevron-triple-down"
disabled={!autoScroll}
size={26}
color={theme.textColorPrimary}
/>
</Pressable>
<Pressable
android_ripple={rippleConfig}
style={styles.buttonStyles}
onPress={() => scrollViewRef.current.scrollTo({})}
>
<IconButton
icon="format-vertical-align-top"
size={26}
color={theme.textColorPrimary}
/>
</Pressable>
</>
)}
{/* <Pressable
android_ripple={rippleConfig}
style={styles.buttonStyles}
onPress={enableWebView}
@@ -109,42 +109,42 @@ const ChapterFooter = ({
color={theme.textColorPrimary}
/>
</Pressable> */}
<Pressable
android_ripple={rippleConfig}
style={styles.buttonStyles}
onPress={() => readerSheetRef.current.show()}
>
<IconButton
icon="cog-outline"
size={26}
color={theme.textColorPrimary}
/>
</Pressable>
<Pressable
android_ripple={rippleConfig}
style={styles.buttonStyles}
onPress={navigateToNextChapter}
>
<IconButton
icon="chevron-right"
size={26}
disabled={!nextChapter}
color={theme.textColorPrimary}
/>
</Pressable>
</View>
</FadeView>
);
<Pressable
android_ripple={rippleConfig}
style={styles.buttonStyles}
onPress={() => readerSheetRef.current.show()}
>
<IconButton
icon="cog-outline"
size={26}
color={theme.textColorPrimary}
/>
</Pressable>
<Pressable
android_ripple={rippleConfig}
style={styles.buttonStyles}
onPress={navigateToNextChapter}
>
<IconButton
icon="chevron-right"
size={26}
disabled={!nextChapter}
color={theme.textColorPrimary}
/>
</Pressable>
</View>
</FadeView>
);
};
export default ChapterFooter;
const styles = StyleSheet.create({
buttonStyles: {
flex: 1,
justifyContent: "center",
alignItems: "center",
paddingVertical: 8,
paddingBottom: 4,
},
buttonStyles: {
flex: 1,
justifyContent: 'center',
alignItems: 'center',
paddingVertical: 8,
paddingBottom: 4,
},
});
File diff suppressed because it is too large Load Diff
@@ -1,135 +1,135 @@
import React from "react";
import { Dimensions, View, Text, StyleSheet } from "react-native";
import React from 'react';
import {Dimensions, View, Text, StyleSheet} from 'react-native';
import Slider from "@react-native-community/slider";
import { getDeviceOrientation } from "../../../services/utils/helpers";
import FadeView from "../../../components/Common/CrossFadeView";
import Slider from '@react-native-community/slider';
import {getDeviceOrientation} from '../../../services/utils/helpers';
import FadeView from '../../../components/Common/CrossFadeView';
const VerticalScrollbar = ({
theme,
hide,
contentSize,
scrollPercentage,
setLoading,
scrollViewRef,
theme,
hide,
contentSize,
scrollPercentage,
setLoading,
scrollViewRef,
}) => {
const onSlidingComplete = (value) => {
setLoading(true);
scrollViewRef.current.scrollTo({
x: 0,
y: Math.round((value * contentSize) / 100),
animated: false,
});
setLoading(false);
};
const onSlidingComplete = value => {
setLoading(true);
scrollViewRef.current.scrollTo({
x: 0,
y: Math.round((value * contentSize) / 100),
animated: false,
});
setLoading(false);
};
if (hide) {
return null;
if (hide) {
return null;
} else {
if (getDeviceOrientation() === 'potrait') {
return (
<View
style={[
styles.verticalSliderContainer,
{backgroundColor: `${theme.colorPrimary}E6`},
]}
>
<Text
style={{
color: theme.textColorPrimary,
marginLeft: 16,
transform: [{rotate: '-90deg'}],
}}
>
{scrollPercentage}
</Text>
<Slider
style={{
flex: 1,
height: 40,
}}
minimumValue={0}
maximumValue={100}
step={1}
value={scrollPercentage}
onSlidingComplete={onSlidingComplete}
thumbTintColor={theme.colorAccent}
minimumTrackTintColor={theme.colorAccent}
maximumTrackTintColor={theme.textColorPrimary}
/>
<Text
style={{
color: theme.textColorPrimary,
marginRight: 16,
transform: [{rotate: '-90deg'}],
}}
>
100
</Text>
</View>
);
} else {
if (getDeviceOrientation() === "potrait") {
return (
<View
style={[
styles.verticalSliderContainer,
{ backgroundColor: `${theme.colorPrimary}E6` },
]}
>
<Text
style={{
color: theme.textColorPrimary,
marginLeft: 16,
transform: [{ rotate: "-90deg" }],
}}
>
{scrollPercentage}
</Text>
<Slider
style={{
flex: 1,
height: 40,
}}
minimumValue={0}
maximumValue={100}
step={1}
value={scrollPercentage}
onSlidingComplete={onSlidingComplete}
thumbTintColor={theme.colorAccent}
minimumTrackTintColor={theme.colorAccent}
maximumTrackTintColor={theme.textColorPrimary}
/>
<Text
style={{
color: theme.textColorPrimary,
marginRight: 16,
transform: [{ rotate: "-90deg" }],
}}
>
100
</Text>
</View>
);
} else {
return (
<View style={styles.horizontalSliderContainer}>
<Text
style={{
color: "#FFFFFF",
marginLeft: 16,
}}
>
{scrollPercentage}
</Text>
<Slider
style={{
flex: 1,
height: 40,
}}
minimumValue={0}
maximumValue={100}
step={1}
value={scrollPercentage}
onSlidingComplete={onSlidingComplete}
thumbTintColor={theme.colorAccent}
minimumTrackTintColor={theme.colorAccent}
maximumTrackTintColor={theme.textColorPrimary}
/>
<Text
style={{
color: "#FFFFFF",
marginRight: 16,
}}
>
100
</Text>
</View>
);
}
return (
<View style={styles.horizontalSliderContainer}>
<Text
style={{
color: '#FFFFFF',
marginLeft: 16,
}}
>
{scrollPercentage}
</Text>
<Slider
style={{
flex: 1,
height: 40,
}}
minimumValue={0}
maximumValue={100}
step={1}
value={scrollPercentage}
onSlidingComplete={onSlidingComplete}
thumbTintColor={theme.colorAccent}
minimumTrackTintColor={theme.colorAccent}
maximumTrackTintColor={theme.textColorPrimary}
/>
<Text
style={{
color: '#FFFFFF',
marginRight: 16,
}}
>
100
</Text>
</View>
);
}
}
};
export default VerticalScrollbar;
const styles = StyleSheet.create({
verticalSliderContainer: {
flexDirection: "row",
justifyContent: "flex-end",
alignItems: "center",
borderRadius: 50,
marginHorizontal: 8,
transform: [{ rotate: "90deg" }],
position: "absolute",
zIndex: 2,
right: -(Dimensions.get("window").width / 2) + 40,
bottom: 300,
},
horizontalSliderContainer: {
position: "absolute",
zIndex: 2,
bottom: 100,
flexDirection: "row",
justifyContent: "flex-end",
alignItems: "center",
borderRadius: 50,
marginHorizontal: 8,
},
verticalSliderContainer: {
flexDirection: 'row',
justifyContent: 'flex-end',
alignItems: 'center',
borderRadius: 50,
marginHorizontal: 8,
transform: [{rotate: '90deg'}],
position: 'absolute',
zIndex: 2,
right: -(Dimensions.get('window').width / 2) + 40,
bottom: 300,
},
horizontalSliderContainer: {
position: 'absolute',
zIndex: 2,
bottom: 100,
flexDirection: 'row',
justifyContent: 'flex-end',
alignItems: 'center',
borderRadius: 50,
marginHorizontal: 8,
},
});
+13 -13
View File
@@ -1,15 +1,15 @@
import React from "react";
import WebView from "react-native-webview";
import React from 'react';
import WebView from 'react-native-webview';
export const WebViewReader = ({ style, css, text, onScroll }) => (
<WebView
style={style}
originWhitelist={["*"]}
scalesPageToFit={true}
showsVerticalScrollIndicator={false}
onScroll={onScroll}
source={{
html: `
export const WebViewReader = ({style, css, text, onScroll}) => (
<WebView
style={style}
originWhitelist={['*']}
scalesPageToFit={true}
showsVerticalScrollIndicator={false}
onScroll={onScroll}
source={{
html: `
<html>
<head>
<meta name="viewport" content="width=device-width, initial-scale=1.0, maximum-scale=1.0">
@@ -22,6 +22,6 @@ export const WebViewReader = ({ style, css, text, onScroll }) => (
</body>
</html>
`,
}}
/>
}}
/>
);

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