Compare commits
223 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 3425892a16 | |||
| 97890c1596 | |||
| c97e1b8e23 | |||
| c44537a1c0 | |||
| fd8fb367bd | |||
| a3d8aa5bcd | |||
| e7a1f8210f | |||
| 9423054b10 | |||
| d11348d9a3 | |||
| 8ec90ae8f7 | |||
| 5fde669106 | |||
| c6cdf1da0e | |||
| 8cb0a5eb73 | |||
| 09e15607d5 | |||
| a9d399ea6d | |||
| b9eddc2cc2 | |||
| bb68b2f407 | |||
| 5e0d19a9c9 | |||
| 0aa78e56c8 | |||
| 078e311995 | |||
| 9a22eba72c | |||
| 2b0032a4e4 | |||
| 708225c855 | |||
| e514876170 | |||
| 911d25bb84 | |||
| b6d2e43d3d | |||
| 7375eb7ad0 | |||
| 627592d477 | |||
| dbe2d13b80 | |||
| 97e172a2aa | |||
| ba083d1afe | |||
| 9e614c6753 | |||
| 5054e60195 | |||
| 2aa36393a6 | |||
| c77b166fec | |||
| 73276bd05f | |||
| 7f47661468 | |||
| ec63de8115 | |||
| ee277bfbf1 | |||
| e330eb122f | |||
| d84935a6a6 | |||
| 6d852dc138 | |||
| 4bd5e9b4f5 | |||
| 4fc480d2e9 | |||
| 3760b5ae4e | |||
| f286f9337e | |||
| bca620ea37 | |||
| eb9516adda | |||
| 8dbec40b9d | |||
| bdac5161d2 | |||
| 6eca36c295 | |||
| 5cccc7c556 | |||
| 5350bf92de | |||
| 662610b227 | |||
| 8d53125184 | |||
| a61d08f443 | |||
| d8cd2b184a | |||
| 33ceb60850 | |||
| ef1a9d1f35 | |||
| dabd0c5401 | |||
| 3ae029c77b | |||
| efe96d45ea | |||
| 06ca51a841 | |||
| b947ec2665 | |||
| c262c1e1de | |||
| 19d089fa7a | |||
| 9447d10c79 | |||
| d2e2314a3e | |||
| 9fe74ad137 | |||
| 5877448f9d | |||
| fb5d5cf186 | |||
| 8faac73558 | |||
| 3f9c662c68 | |||
| ecce374abe | |||
| 48a67402ff | |||
| 7c4da7f761 | |||
| 3daa11f642 | |||
| f26292f73a | |||
| 49160b7d98 | |||
| e40af59230 | |||
| 8084cdbffb | |||
| dc9529c322 | |||
| 91168a335b | |||
| a6b3eb9350 | |||
| 0ce4e6b985 | |||
| 6684848a42 | |||
| 87d12cabb3 | |||
| e01fc21a49 | |||
| f0cfd9066b | |||
| c2cc6ea96b | |||
| eeeea6b4e0 | |||
| ddce153b53 | |||
| 38e45ca097 | |||
| a6a60607a7 | |||
| d3e772f0dd | |||
| 095d46681f | |||
| 3e4e967197 | |||
| c5b4e7b902 | |||
| e9421a5fd0 | |||
| afef0d63ce | |||
| e68a5c5f82 | |||
| ff1635b817 | |||
| f1ca221b91 | |||
| 5a1106fc68 | |||
| 3b49a1e516 | |||
| 85acbc8c4a | |||
| 66e0be5e84 | |||
| 2234006272 | |||
| eca00ddcfa | |||
| 2328e55105 | |||
| b8a25b9172 | |||
| 9af8d55985 | |||
| 4777f453a4 | |||
| 4a0195762d | |||
| e8785c2905 | |||
| 1302cbde16 | |||
| 43d0adced8 | |||
| 0c8b30cd72 | |||
| cc86fc7da4 | |||
| 625acdb585 | |||
| 93617e60bd | |||
| d75c607f67 | |||
| dd087ff485 | |||
| 5da3e05bba | |||
| e628c3cfd6 | |||
| deba021384 | |||
| c6d2ba8442 | |||
| 6a3736dced | |||
| f63ae573ba | |||
| 8f3020a544 | |||
| 04e32a4234 | |||
| 9b3d65dded | |||
| aa3a5c2d0c | |||
| 2aab5621a6 | |||
| b4b7e6770d | |||
| 0ed65d64e4 | |||
| a7aaa7d230 | |||
| e358b3753f | |||
| 780a67e1a6 | |||
| 5b57e78486 | |||
| 6430c11cae | |||
| 322bfc224d | |||
| a14d35f031 | |||
| f03b1703e2 | |||
| 40d2052f16 | |||
| 01814e92d0 | |||
| dffc9c3b99 | |||
| 8bb3ffe82c | |||
| c746081dee | |||
| d25fe85a64 | |||
| 92d9d53da7 | |||
| 17fcd96f16 | |||
| 31bada1c1a | |||
| 605bd6a514 | |||
| 01bac76bed | |||
| b5ed62d9f8 | |||
| fd21cc6e85 | |||
| 40f1495cf1 | |||
| 983b069dba | |||
| e21101ca28 | |||
| d381e878bc | |||
| bcf5632668 | |||
| ac8bca7f03 | |||
| 62a5e27b1e | |||
| 7c5a9a407f | |||
| d8d231cb90 | |||
| a5f90eaf8b | |||
| ec50189ea4 | |||
| ccdccdb6bb | |||
| 059f440713 | |||
| b695f57ef4 | |||
| db86b707e7 | |||
| 7db4e4b872 | |||
| a2c6e9fe58 | |||
| 6c27993454 | |||
| 63de0a3794 | |||
| 93fbcf1246 | |||
| 50357f7d6f | |||
| 1a8d640db1 | |||
| dc6b2deddd | |||
| a9fcf2fe08 | |||
| f195b9e6df | |||
| d07fd26cb3 | |||
| 5aee0d1b04 | |||
| c4457d74c4 | |||
| ba90a6fb84 | |||
| e7424cea06 | |||
| e274feda6e | |||
| 8ec2378191 | |||
| ef57a98681 | |||
| bdda959f36 | |||
| 04f3ba2b45 | |||
| f1a261ab28 | |||
| da2ee3b883 | |||
| b98d3861b4 | |||
| ff59330d20 | |||
| b0639e79a8 | |||
| 5931a0dc07 | |||
| cf40acb725 | |||
| a2974ccb44 | |||
| 5eda558f09 | |||
| cf7f1402d3 | |||
| 834a8d9e2e | |||
| fe5b3f4a5b | |||
| ec4eaf4d49 | |||
| 0c0b599452 | |||
| ac26c67361 | |||
| d205572a5e | |||
| e5d52fcd9d | |||
| 61424e533a | |||
| c3ef170385 | |||
| 430c7a7341 | |||
| e4b934061b | |||
| 3579671ade | |||
| 893d229c25 | |||
| ea3f5ac7ee | |||
| e4027efbb1 | |||
| f5dd7153cc | |||
| 38b0097261 | |||
| 2f7215512f | |||
| 9a96a34cda | |||
| a7f6d2cd3c | |||
| 5dfe6a7ab8 |
@@ -0,0 +1,3 @@
|
||||
# Windows files
|
||||
[*.bat]
|
||||
end_of_line = crlf
|
||||
@@ -0,0 +1,8 @@
|
||||
module.exports = {
|
||||
root: true,
|
||||
extends: '@react-native-community',
|
||||
rules: {
|
||||
'react-native/no-inline-styles': 'off',
|
||||
'no-console': 'warn',
|
||||
},
|
||||
};
|
||||
@@ -1,4 +0,0 @@
|
||||
{
|
||||
"12bb71342c6255bbf50437ec8f4441c083f47cdb74bd89160c15e4f43e52a1cb": true,
|
||||
"40b842e832070c58deac6aa9e08fa459302ee3f9da492c7e77d93d2fbf4a56fd": true
|
||||
}
|
||||
@@ -0,0 +1,3 @@
|
||||
# Windows files should use crlf line endings
|
||||
# https://help.github.com/articles/dealing-with-line-endings/
|
||||
*.bat text eol=crlf
|
||||
|
After Width: | Height: | Size: 48 KiB |
|
Before Width: | Height: | Size: 1.3 MiB After Width: | Height: | Size: 1.1 MiB |
@@ -0,0 +1,45 @@
|
||||
name: CI
|
||||
on:
|
||||
push:
|
||||
branches:
|
||||
- main
|
||||
jobs:
|
||||
install-and-test:
|
||||
name: Install dependencies
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v2
|
||||
- name: Install npm dependencies
|
||||
run: |
|
||||
npm install
|
||||
|
||||
build-android:
|
||||
name: Build app
|
||||
needs: install-and-test
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v2
|
||||
- name: Install Dependencies
|
||||
run: |
|
||||
npm install
|
||||
|
||||
- name: Cache Gradle Wrapper
|
||||
uses: actions/cache@v2
|
||||
with:
|
||||
path: ~/.gradle/wrapper
|
||||
key: ${{ runner.os }}-gradle-wrapper-${{ hashFiles('gradle/wrapper/gradle-wrapper.properties') }}
|
||||
|
||||
- name: Cache Gradle Dependencies
|
||||
uses: actions/cache@v2
|
||||
with:
|
||||
path: ~/.gradle/caches
|
||||
key: ${{ runner.os }}-gradle-caches-${{ hashFiles('gradle/wrapper/gradle-wrapper.properties') }}
|
||||
restore-keys: |
|
||||
${{ runner.os }}-gradle-caches-
|
||||
|
||||
- name: Make Gradlew Executable
|
||||
run: cd android && chmod +x ./gradlew
|
||||
|
||||
- name: Build Android Release
|
||||
run: |
|
||||
cd android && ./gradlew assembleRelease --no-daemon
|
||||
@@ -0,0 +1,12 @@
|
||||
name: Lint
|
||||
on: [push, pull_request]
|
||||
jobs:
|
||||
build:
|
||||
name: ESLint
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v2
|
||||
- name: Install modules
|
||||
run: npm i
|
||||
- name: Run ESLint
|
||||
run: npm run lint
|
||||
@@ -70,4 +70,6 @@ buck-out/
|
||||
.expo/
|
||||
web-build/
|
||||
|
||||
# @end expo-cli
|
||||
# @end expo-cli
|
||||
|
||||
.vscode/
|
||||
@@ -0,0 +1 @@
|
||||
**/node_modules
|
||||
@@ -0,0 +1,6 @@
|
||||
module.exports = {
|
||||
bracketSpacing: false,
|
||||
singleQuote: true,
|
||||
trailingComma: 'all',
|
||||
arrowParens: 'avoid',
|
||||
};
|
||||
@@ -1,41 +1,43 @@
|
||||
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 {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, deleteDb } from "./src/database/DBHelper";
|
||||
import { fonts } from "./src/theme/fonts";
|
||||
import {createDB} from './src/database/DBHelper';
|
||||
import {fonts} from './src/theme/fonts';
|
||||
import {SafeAreaProvider} from 'react-native-safe-area-context';
|
||||
|
||||
const App = () => {
|
||||
const [loaded] = useFonts(fonts);
|
||||
const [loaded] = useFonts(fonts);
|
||||
|
||||
useEffect(() => {
|
||||
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}>
|
||||
<SafeAreaProvider>
|
||||
<PaperProvider>
|
||||
<StatusBar translucent={true} backgroundColor="transparent" />
|
||||
<Main />
|
||||
</PaperProvider>
|
||||
</SafeAreaProvider>
|
||||
</PersistGate>
|
||||
</Provider>
|
||||
);
|
||||
};
|
||||
|
||||
export default App;
|
||||
|
||||
@@ -0,0 +1,128 @@
|
||||
# Contributor Covenant Code of Conduct
|
||||
|
||||
## Our Pledge
|
||||
|
||||
We as members, contributors, and leaders pledge to make participation in our
|
||||
community a harassment-free experience for everyone, regardless of age, body
|
||||
size, visible or invisible disability, ethnicity, sex characteristics, gender
|
||||
identity and expression, level of experience, education, socio-economic status,
|
||||
nationality, personal appearance, race, religion, or sexual identity
|
||||
and orientation.
|
||||
|
||||
We pledge to act and interact in ways that contribute to an open, welcoming,
|
||||
diverse, inclusive, and healthy community.
|
||||
|
||||
## Our Standards
|
||||
|
||||
Examples of behavior that contributes to a positive environment for our
|
||||
community include:
|
||||
|
||||
* Demonstrating empathy and kindness toward other people
|
||||
* Being respectful of differing opinions, viewpoints, and experiences
|
||||
* Giving and gracefully accepting constructive feedback
|
||||
* Accepting responsibility and apologizing to those affected by our mistakes,
|
||||
and learning from the experience
|
||||
* Focusing on what is best not just for us as individuals, but for the
|
||||
overall community
|
||||
|
||||
Examples of unacceptable behavior include:
|
||||
|
||||
* The use of sexualized language or imagery, and sexual attention or
|
||||
advances of any kind
|
||||
* Trolling, insulting or derogatory comments, and personal or political attacks
|
||||
* Public or private harassment
|
||||
* Publishing others' private information, such as a physical or email
|
||||
address, without their explicit permission
|
||||
* Other conduct which could reasonably be considered inappropriate in a
|
||||
professional setting
|
||||
|
||||
## Enforcement Responsibilities
|
||||
|
||||
Community leaders are responsible for clarifying and enforcing our standards of
|
||||
acceptable behavior and will take appropriate and fair corrective action in
|
||||
response to any behavior that they deem inappropriate, threatening, offensive,
|
||||
or harmful.
|
||||
|
||||
Community leaders have the right and responsibility to remove, edit, or reject
|
||||
comments, commits, code, wiki edits, issues, and other contributions that are
|
||||
not aligned to this Code of Conduct, and will communicate reasons for moderation
|
||||
decisions when appropriate.
|
||||
|
||||
## Scope
|
||||
|
||||
This Code of Conduct applies within all community spaces, and also applies when
|
||||
an individual is officially representing the community in public spaces.
|
||||
Examples of representing our community include using an official e-mail address,
|
||||
posting via an official social media account, or acting as an appointed
|
||||
representative at an online or offline event.
|
||||
|
||||
## Enforcement
|
||||
|
||||
Instances of abusive, harassing, or otherwise unacceptable behavior may be
|
||||
reported to the community leaders responsible for enforcement at
|
||||
rajarshee.adm@gmail.com.
|
||||
All complaints will be reviewed and investigated promptly and fairly.
|
||||
|
||||
All community leaders are obligated to respect the privacy and security of the
|
||||
reporter of any incident.
|
||||
|
||||
## Enforcement Guidelines
|
||||
|
||||
Community leaders will follow these Community Impact Guidelines in determining
|
||||
the consequences for any action they deem in violation of this Code of Conduct:
|
||||
|
||||
### 1. Correction
|
||||
|
||||
**Community Impact**: Use of inappropriate language or other behavior deemed
|
||||
unprofessional or unwelcome in the community.
|
||||
|
||||
**Consequence**: A private, written warning from community leaders, providing
|
||||
clarity around the nature of the violation and an explanation of why the
|
||||
behavior was inappropriate. A public apology may be requested.
|
||||
|
||||
### 2. Warning
|
||||
|
||||
**Community Impact**: A violation through a single incident or series
|
||||
of actions.
|
||||
|
||||
**Consequence**: A warning with consequences for continued behavior. No
|
||||
interaction with the people involved, including unsolicited interaction with
|
||||
those enforcing the Code of Conduct, for a specified period of time. This
|
||||
includes avoiding interactions in community spaces as well as external channels
|
||||
like social media. Violating these terms may lead to a temporary or
|
||||
permanent ban.
|
||||
|
||||
### 3. Temporary Ban
|
||||
|
||||
**Community Impact**: A serious violation of community standards, including
|
||||
sustained inappropriate behavior.
|
||||
|
||||
**Consequence**: A temporary ban from any sort of interaction or public
|
||||
communication with the community for a specified period of time. No public or
|
||||
private interaction with the people involved, including unsolicited interaction
|
||||
with those enforcing the Code of Conduct, is allowed during this period.
|
||||
Violating these terms may lead to a permanent ban.
|
||||
|
||||
### 4. Permanent Ban
|
||||
|
||||
**Community Impact**: Demonstrating a pattern of violation of community
|
||||
standards, including sustained inappropriate behavior, harassment of an
|
||||
individual, or aggression toward or disparagement of classes of individuals.
|
||||
|
||||
**Consequence**: A permanent ban from any sort of public interaction within
|
||||
the community.
|
||||
|
||||
## Attribution
|
||||
|
||||
This Code of Conduct is adapted from the [Contributor Covenant][homepage],
|
||||
version 2.0, available at
|
||||
https://www.contributor-covenant.org/version/2/0/code_of_conduct.html.
|
||||
|
||||
Community Impact Guidelines were inspired by [Mozilla's code of conduct
|
||||
enforcement ladder](https://github.com/mozilla/diversity).
|
||||
|
||||
[homepage]: https://www.contributor-covenant.org
|
||||
|
||||
For answers to common questions about this code of conduct, see the FAQ at
|
||||
https://www.contributor-covenant.org/faq. Translations are available at
|
||||
https://www.contributor-covenant.org/translations.
|
||||
@@ -0,0 +1,41 @@
|
||||
# Contributing Guide
|
||||
|
||||
Contributions are welcome and are greatly appreciated!
|
||||
|
||||
|
||||
## Setting up your environment
|
||||
|
||||
After forking to your own github org or account, do the following steps to get started:
|
||||
|
||||
```bash
|
||||
# clone your fork to your local machine
|
||||
git clone https://github.com/<your-account-name>/lnreader.git
|
||||
|
||||
# step into local repo
|
||||
cd lnreader
|
||||
|
||||
# install dependencies
|
||||
npm install
|
||||
|
||||
# run metro for development
|
||||
npm start
|
||||
```
|
||||
|
||||
### Developing on Android
|
||||
|
||||
While the packager is running and you have an Android device or emulator connected to your computer, build and launch the Android app.
|
||||
|
||||
```
|
||||
npm run android
|
||||
```
|
||||
|
||||
### Style & Linting
|
||||
|
||||
This codebase's linting rules are enforced using [ESLint](http://eslint.org/).
|
||||
|
||||
It is recommended that you install an eslint plugin for your editor of choice when working on this
|
||||
codebase, however you can always check to see if the source code is compliant by running:
|
||||
|
||||
```bash
|
||||
npm run lint
|
||||
```
|
||||
@@ -1,20 +1,41 @@
|
||||
<h1 align="center">
|
||||
<br>
|
||||
LNReader
|
||||
<br>
|
||||
</h1>
|
||||
|
||||
<p align="center">LNReader is a Tachiyomi-like, free and open source light novel reader for Android.</p>
|
||||
|
||||
---
|
||||
|
||||
## Screenshots:
|
||||
<p align="center">
|
||||
<img src="./.github/readme-images/screenshots.png" align="center" height="350" />
|
||||
<img src="./.github/readme-images/icon_alt.png" align="center" width="128" />
|
||||
<p>
|
||||
<h1 align="center">
|
||||
LNReader
|
||||
</h1>
|
||||
<p align="center">
|
||||
LNReader is a Tachiyomi-like, free and open source light novel reader for Android.
|
||||
</p>
|
||||
<div align="center">
|
||||
<a href="https://github.com/LNReader/lnreader/actions/workflows/build.yml">
|
||||
<img alt="GitHub Workflow Status" src="https://img.shields.io/github/workflow/status/LNReader/lnreader/CI">
|
||||
</a>
|
||||
<a href="https://discord.gg/QdcWN4MD63">
|
||||
<img alt="Discord Chat" src="https://img.shields.io/discord/835746409357246465.svg?logo=discord&logoColor=white&logoWidth=20&labelColor=7289DA&label=Discord&color=17cf48">
|
||||
</a>
|
||||
<a href="https://github.com/LNReader/lnreader/releases/latest">
|
||||
<img alt="GitHub release (latest by date)" src="https://img.shields.io/github/v/release/LNReader/lnreader">
|
||||
</a>
|
||||
<a href="https://github.com/LNReader/lnreader/blob/main/LICENSE">
|
||||
<img alt="GitHub" src="https://img.shields.io/github/license/LNReader/lnreader">
|
||||
</a>
|
||||
</div>
|
||||
|
||||
<br>
|
||||
<br>
|
||||
|
||||
<p align="center">
|
||||
<img src="./.github/readme-images/screenshots.png" align="center" />
|
||||
</p>
|
||||
|
||||
## Download
|
||||
Get the app from our [releases page](https://github.com/rajarsheechatterjee/LNReader/releases).
|
||||
|
||||
## Sources
|
||||
LNReader does not have any affiliation with the content providers available.
|
||||
|
||||
Source requests should be created at [LNReader/lnreader-sources](https://github.com/LNReader/lnreader-sources).
|
||||
|
||||
## License
|
||||
[MIT ](https://github.com/rajarsheechatterjee/lnreader/blob/main/LICENSE)
|
||||
|
||||
@@ -136,7 +136,7 @@ android {
|
||||
minSdkVersion rootProject.ext.minSdkVersion
|
||||
targetSdkVersion rootProject.ext.targetSdkVersion
|
||||
versionCode 1
|
||||
versionName "1.1.0"
|
||||
versionName "1.1.7"
|
||||
}
|
||||
splits {
|
||||
abi {
|
||||
|
||||
@@ -1,38 +1,89 @@
|
||||
<manifest xmlns:android="http://schemas.android.com/apk/res/android" package="com.rajarsheechatterjee.LNReader">
|
||||
<uses-permission android:name="android.permission.INTERNET"/>
|
||||
<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.READ_EXTERNAL_STORAGE"/>
|
||||
<uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE"/>
|
||||
<uses-permission android:name="android.permission.RECEIVE_BOOT_COMPLETED"/>
|
||||
<uses-permission android:name="android.permission.FOREGROUND_SERVICE" />
|
||||
<uses-permission android:name="android.permission.WAKE_LOCK"/>
|
||||
<uses-permission android:name="android.permission.ACCESS_NETWORK_STATE"/>
|
||||
<uses-permission android:name="com.google.android.c2dm.permission.RECEIVE"/>
|
||||
<application android:name=".MainApplication" android:label="@string/app_name" android:icon="@mipmap/ic_launcher" android:roundIcon="@mipmap/ic_launcher_round" android:allowBackup="true" android:theme="@style/AppTheme">
|
||||
<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,62 +0,0 @@
|
||||
// FullScreenModule.java
|
||||
|
||||
package com.rajarsheechatterjee.LNReader;
|
||||
|
||||
import android.os.Build;
|
||||
import android.view.View;
|
||||
import android.view.WindowManager;
|
||||
import android.app.Activity;
|
||||
import com.facebook.react.bridge.NativeModule;
|
||||
import com.facebook.react.bridge.ReactApplicationContext;
|
||||
import com.facebook.react.bridge.ReactContext;
|
||||
import com.facebook.react.bridge.UiThreadUtil;
|
||||
import com.facebook.react.bridge.ReactContextBaseJavaModule;
|
||||
import com.facebook.react.bridge.ReactMethod;
|
||||
|
||||
|
||||
public class FullScreenModule extends ReactContextBaseJavaModule {
|
||||
@Override
|
||||
public String getName() {
|
||||
return "FullScreen";
|
||||
}
|
||||
|
||||
@ReactMethod
|
||||
public void enable() {
|
||||
UiThreadUtil.runOnUiThread(
|
||||
new Runnable() {
|
||||
@Override
|
||||
public void run() {
|
||||
getCurrentActivity().getWindow().getDecorView().setSystemUiVisibility(
|
||||
View.SYSTEM_UI_FLAG_LAYOUT_FULLSCREEN |
|
||||
View.SYSTEM_UI_FLAG_LAYOUT_HIDE_NAVIGATION |
|
||||
View.SYSTEM_UI_FLAG_FULLSCREEN |
|
||||
View.SYSTEM_UI_FLAG_HIDE_NAVIGATION |
|
||||
View.SYSTEM_UI_FLAG_IMMERSIVE_STICKY
|
||||
);
|
||||
}
|
||||
}
|
||||
);
|
||||
|
||||
}
|
||||
|
||||
@ReactMethod
|
||||
public void disable() {
|
||||
UiThreadUtil.runOnUiThread(
|
||||
new Runnable() {
|
||||
@Override
|
||||
public void run() {
|
||||
getCurrentActivity().getWindow().getDecorView().setSystemUiVisibility(
|
||||
View.SYSTEM_UI_FLAG_LAYOUT_FULLSCREEN |
|
||||
View.SYSTEM_UI_FLAG_LAYOUT_HIDE_NAVIGATION |
|
||||
View.SYSTEM_UI_FLAG_LAYOUT_STABLE
|
||||
);
|
||||
}
|
||||
}
|
||||
);
|
||||
|
||||
}
|
||||
|
||||
FullScreenModule(ReactApplicationContext reactContext) {
|
||||
super(reactContext);
|
||||
}
|
||||
}
|
||||
@@ -1,29 +0,0 @@
|
||||
package com.rajarsheechatterjee.LNReader;
|
||||
|
||||
import com.facebook.react.ReactPackage;
|
||||
import com.facebook.react.bridge.NativeModule;
|
||||
import com.facebook.react.bridge.ReactApplicationContext;
|
||||
import com.facebook.react.uimanager.ViewManager;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.Collections;
|
||||
import java.util.List;
|
||||
|
||||
public class FullScreenPackage implements ReactPackage {
|
||||
|
||||
@Override
|
||||
public List<ViewManager> createViewManagers(ReactApplicationContext reactContext) {
|
||||
return Collections.emptyList();
|
||||
}
|
||||
|
||||
@Override
|
||||
public List<NativeModule> createNativeModules(
|
||||
ReactApplicationContext reactContext) {
|
||||
List<NativeModule> modules = new ArrayList<>();
|
||||
|
||||
modules.add(new FullScreenModule(reactContext));
|
||||
|
||||
return modules;
|
||||
}
|
||||
|
||||
}
|
||||
@@ -3,8 +3,11 @@ 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;
|
||||
@@ -26,19 +29,25 @@ public class MainActivity extends ReactActivity {
|
||||
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);
|
||||
}
|
||||
super.onCreate(savedInstanceState);
|
||||
SplashScreen.show(this, SplashScreenImageResizeMode.COVER, ReactRootView.class, false);
|
||||
@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(null);
|
||||
SplashScreen.show(this, SplashScreenImageResizeMode.COVER, ReactRootView.class, false);
|
||||
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
|
||||
@@ -31,9 +31,6 @@ import java.util.Arrays;
|
||||
import java.util.List;
|
||||
import javax.annotation.Nullable;
|
||||
|
||||
import com.rajarsheechatterjee.LNReader.FullScreenPackage;
|
||||
|
||||
|
||||
public class MainApplication extends Application implements ReactApplication {
|
||||
private final ReactModuleRegistryProvider mModuleRegistryProvider = new ReactModuleRegistryProvider(
|
||||
new BasePackageList().getPackageList()
|
||||
@@ -49,7 +46,7 @@ public class MainApplication extends Application implements ReactApplication {
|
||||
protected List<ReactPackage> getPackages() {
|
||||
List<ReactPackage> packages = new PackageList(this).getPackages();
|
||||
packages.add(new ModuleRegistryAdapter(mModuleRegistryProvider));
|
||||
packages.add(new FullScreenPackage());
|
||||
packages.add(new NavigationBarColorPackage());
|
||||
return packages;
|
||||
}
|
||||
|
||||
|
||||
@@ -8,21 +8,19 @@ public class BasePackageList {
|
||||
public List<Package> getPackageList() {
|
||||
return Arrays.<Package>asList(
|
||||
new expo.modules.application.ApplicationPackage(),
|
||||
new expo.modules.backgroundfetch.BackgroundFetchPackage(),
|
||||
new expo.modules.constants.ConstantsPackage(),
|
||||
new expo.modules.documentpicker.DocumentPickerPackage(),
|
||||
new expo.modules.errorrecovery.ErrorRecoveryPackage(),
|
||||
new expo.modules.filesystem.FileSystemPackage(),
|
||||
new expo.modules.font.FontLoaderPackage(),
|
||||
new expo.modules.haptics.HapticsPackage(),
|
||||
new expo.modules.imageloader.ImageLoaderPackage(),
|
||||
new expo.modules.keepawake.KeepAwakePackage(),
|
||||
new expo.modules.lineargradient.LinearGradientPackage(),
|
||||
new expo.modules.notifications.NotificationsPackage(),
|
||||
new expo.modules.permissions.PermissionsPackage(),
|
||||
new expo.modules.sharing.SharingPackage(),
|
||||
new expo.modules.splashscreen.SplashScreenPackage(),
|
||||
new expo.modules.sqlite.SQLitePackage(),
|
||||
new expo.modules.taskManager.TaskManagerPackage(),
|
||||
new expo.modules.updates.UpdatesPackage(),
|
||||
new expo.modules.webbrowser.WebBrowserPackage()
|
||||
);
|
||||
|
||||
@@ -0,0 +1,170 @@
|
||||
package com.rajarsheechatterjee.LNReader;
|
||||
|
||||
import android.animation.ArgbEvaluator;
|
||||
import android.animation.ValueAnimator;
|
||||
import android.annotation.TargetApi;
|
||||
import android.graphics.Color;
|
||||
import android.os.Build;
|
||||
import android.app.Activity;
|
||||
import android.view.View;
|
||||
import android.view.Window;
|
||||
import android.view.WindowManager;
|
||||
import androidx.annotation.UiThread;
|
||||
import com.facebook.react.bridge.Arguments;
|
||||
import com.facebook.react.bridge.Promise;
|
||||
import com.facebook.react.bridge.ReactApplicationContext;
|
||||
import com.facebook.react.bridge.ReactContextBaseJavaModule;
|
||||
import com.facebook.react.bridge.ReactMethod;
|
||||
import com.facebook.react.bridge.UiThreadUtil;
|
||||
import com.facebook.react.bridge.WritableMap;
|
||||
import java.util.HashMap;
|
||||
import java.util.Map;
|
||||
import com.facebook.react.uimanager.IllegalViewOperationException;
|
||||
import static com.facebook.react.bridge.UiThreadUtil.runOnUiThread;
|
||||
|
||||
public class NavigationBarColorModule extends ReactContextBaseJavaModule {
|
||||
public static final String REACT_CLASS = "NavigationBarColor";
|
||||
private static final String ERROR_NO_ACTIVITY = "E_NO_ACTIVITY";
|
||||
private static final String ERROR_NO_ACTIVITY_MESSAGE = "Tried to change the navigation bar while not attached to an Activity";
|
||||
private static final String ERROR_API_LEVEL = "API_LEVEl";
|
||||
private static final String ERROR_API_LEVEL_MESSAGE = "Only Android Oreo and above is supported";
|
||||
private static ReactApplicationContext reactContext = null;
|
||||
private static final int UI_FLAG_HIDE_NAV_BAR = View.SYSTEM_UI_FLAG_LAYOUT_STABLE
|
||||
| View.SYSTEM_UI_FLAG_LAYOUT_HIDE_NAVIGATION
|
||||
| View.SYSTEM_UI_FLAG_HIDE_NAVIGATION // hide nav bar
|
||||
| View.SYSTEM_UI_FLAG_IMMERSIVE_STICKY;
|
||||
|
||||
public NavigationBarColorModule(ReactApplicationContext context) {
|
||||
super(context);
|
||||
reactContext = context;
|
||||
}
|
||||
|
||||
public void setNavigationBarTheme(Activity activity, Boolean light) {
|
||||
if (activity != null && Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
|
||||
Window window = activity.getWindow();
|
||||
int flags = window.getDecorView().getSystemUiVisibility();
|
||||
if (light) {
|
||||
flags |= View.SYSTEM_UI_FLAG_LIGHT_NAVIGATION_BAR;
|
||||
} else {
|
||||
flags &= ~View.SYSTEM_UI_FLAG_LIGHT_NAVIGATION_BAR;
|
||||
}
|
||||
window.getDecorView().setSystemUiVisibility(flags);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@Override
|
||||
public String getName() {
|
||||
return REACT_CLASS;
|
||||
}
|
||||
|
||||
@Override
|
||||
public Map<String, Object> getConstants() {
|
||||
final Map<String, Object> constants = new HashMap<>();
|
||||
constants.put("EXAMPLE_CONSTANT", "example");
|
||||
|
||||
return constants;
|
||||
}
|
||||
|
||||
@ReactMethod
|
||||
public void changeNavigationBarColor(final String color, final Boolean light, final Boolean animated, final Promise promise) {
|
||||
final WritableMap map = Arguments.createMap();
|
||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) {
|
||||
if (getCurrentActivity() != null) {
|
||||
try {
|
||||
final Window window = getCurrentActivity().getWindow();
|
||||
runOnUiThread(new Runnable() {
|
||||
@Override
|
||||
public void run() {
|
||||
if (color.equals("transparent") || color.equals("translucent")) {
|
||||
window.clearFlags(WindowManager.LayoutParams.FLAG_LAYOUT_NO_LIMITS);
|
||||
window.clearFlags(WindowManager.LayoutParams.FLAG_TRANSLUCENT_NAVIGATION);
|
||||
if (color.equals("transparent")) {
|
||||
window.setFlags(WindowManager.LayoutParams.FLAG_LAYOUT_NO_LIMITS, WindowManager.LayoutParams.FLAG_LAYOUT_NO_LIMITS);
|
||||
} else {
|
||||
window.setFlags(WindowManager.LayoutParams.FLAG_TRANSLUCENT_NAVIGATION, WindowManager.LayoutParams.FLAG_TRANSLUCENT_NAVIGATION);
|
||||
}
|
||||
setNavigationBarTheme(getCurrentActivity(), light);
|
||||
map.putBoolean("success", true);
|
||||
promise.resolve(map);
|
||||
return;
|
||||
} else {
|
||||
window.clearFlags(WindowManager.LayoutParams.FLAG_LAYOUT_NO_LIMITS);
|
||||
window.clearFlags(WindowManager.LayoutParams.FLAG_TRANSLUCENT_NAVIGATION);
|
||||
}
|
||||
if (animated) {
|
||||
Integer colorFrom = window.getNavigationBarColor();
|
||||
Integer colorTo = Color.parseColor(String.valueOf(color));
|
||||
//window.setNavigationBarColor(colorTo);
|
||||
ValueAnimator colorAnimation = ValueAnimator.ofObject(new ArgbEvaluator(), colorFrom, colorTo);
|
||||
colorAnimation.addUpdateListener(new ValueAnimator.AnimatorUpdateListener() {
|
||||
@Override
|
||||
public void onAnimationUpdate(ValueAnimator animator) {
|
||||
window.setNavigationBarColor((Integer) animator.getAnimatedValue());
|
||||
}
|
||||
});
|
||||
colorAnimation.start();
|
||||
} else {
|
||||
window.setNavigationBarColor(Color.parseColor(String.valueOf(color)));
|
||||
}
|
||||
setNavigationBarTheme(getCurrentActivity(), light);
|
||||
WritableMap map = Arguments.createMap();
|
||||
map.putBoolean("success", true);
|
||||
promise.resolve(map);
|
||||
}
|
||||
});
|
||||
} catch (IllegalViewOperationException e) {
|
||||
map.putBoolean("success", false);
|
||||
promise.reject("error", e);
|
||||
}
|
||||
|
||||
} else {
|
||||
promise.reject(ERROR_NO_ACTIVITY, new Throwable(ERROR_NO_ACTIVITY_MESSAGE));
|
||||
|
||||
}
|
||||
} else {
|
||||
promise.reject(ERROR_API_LEVEL, new Throwable(ERROR_API_LEVEL_MESSAGE));
|
||||
}
|
||||
}
|
||||
|
||||
@ReactMethod
|
||||
public void hideNavigationBar(Promise promise) {
|
||||
try {
|
||||
runOnUiThread(new Runnable() {
|
||||
@Override
|
||||
public void run() {
|
||||
if (getCurrentActivity() != null) {
|
||||
View decorView = getCurrentActivity().getWindow().getDecorView();
|
||||
decorView.setSystemUiVisibility(UI_FLAG_HIDE_NAV_BAR);
|
||||
}
|
||||
}
|
||||
});
|
||||
} catch (IllegalViewOperationException e) {
|
||||
WritableMap map = Arguments.createMap();
|
||||
map.putBoolean("success", false);
|
||||
promise.reject("error", e);
|
||||
}
|
||||
}
|
||||
|
||||
@ReactMethod
|
||||
public void showNavigationBar(Promise promise) {
|
||||
try {
|
||||
runOnUiThread(new Runnable() {
|
||||
@Override
|
||||
public void run() {
|
||||
if (getCurrentActivity() != null) {
|
||||
Window w = getCurrentActivity().getWindow();
|
||||
w.setStatusBarColor(Color.TRANSPARENT);
|
||||
w.setNavigationBarColor(Color.TRANSPARENT);
|
||||
View decorView = w.getDecorView();
|
||||
decorView.setSystemUiVisibility(View.SYSTEM_UI_FLAG_LAYOUT_HIDE_NAVIGATION | View.SYSTEM_UI_FLAG_LAYOUT_STABLE);
|
||||
}
|
||||
}
|
||||
});
|
||||
} catch (IllegalViewOperationException e) {
|
||||
WritableMap map = Arguments.createMap();
|
||||
map.putBoolean("success", false);
|
||||
promise.reject("error", e);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,25 @@
|
||||
package com.rajarsheechatterjee.LNReader;
|
||||
import com.facebook.react.ReactPackage;
|
||||
import com.facebook.react.bridge.NativeModule;
|
||||
import com.facebook.react.bridge.ReactApplicationContext;
|
||||
import com.facebook.react.uimanager.ViewManager;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.Collections;
|
||||
import java.util.List;
|
||||
|
||||
public class NavigationBarColorPackage implements ReactPackage {
|
||||
@Override
|
||||
public List<ViewManager> createViewManagers(ReactApplicationContext reactContext) {
|
||||
return Collections.emptyList();
|
||||
}
|
||||
|
||||
@Override
|
||||
public List<NativeModule> createNativeModules(ReactApplicationContext reactContext) {
|
||||
List<NativeModule> modules = new ArrayList<>();
|
||||
|
||||
modules.add(new NavigationBarColorModule(reactContext));
|
||||
|
||||
return modules;
|
||||
}
|
||||
}
|
||||
|
Before Width: | Height: | Size: 28 KiB After Width: | Height: | Size: 29 KiB |
@@ -1,9 +1,9 @@
|
||||
<?xml version="1.0" encoding="UTF-8" standalone="yes"?>
|
||||
<resources>
|
||||
<color name="iconBackground">#FFFFFF</color>
|
||||
<color name="splashscreen_background">#000000</color>
|
||||
<color name="splashscreen_background">#202125</color>
|
||||
<color name="colorPrimary">#023c69</color>
|
||||
<color name="colorPrimaryDark">#00000000</color>
|
||||
<color name="navigationBarColor">#000000</color>
|
||||
<color name="colorPrimaryDark">#202125</color>
|
||||
<color name="navigationBarColor">#202125</color>
|
||||
<color name="notification_icon_color">#00adb5</color>
|
||||
</resources>
|
||||
@@ -8,16 +8,16 @@
|
||||
<item name="android:textColor">#000000</item>
|
||||
<item name="colorPrimary">@color/colorPrimary</item>
|
||||
<item name="colorPrimaryDark">@color/colorPrimaryDark</item>
|
||||
<item name="android:navigationBarColor">#000000</item>
|
||||
<item name="android:statusBarColor">#000000</item>
|
||||
<item name="android:navigationBarColor">#202125</item>
|
||||
<item name="android:statusBarColor">#202125</item>
|
||||
|
||||
</style>
|
||||
<style name="Theme.App.SplashScreen" parent="Theme.AppCompat.Light.NoActionBar">
|
||||
<!-- Below line is handled by '@expo/configure-splash-screen' command and it's discouraged to modify it manually -->
|
||||
<!-- Customize your splash screen theme here -->
|
||||
<item name="android:windowBackground">@drawable/splashscreen</item>
|
||||
<item name="android:navigationBarColor">#000000</item>
|
||||
<item name="android:statusBarColor">#000000</item>
|
||||
<item name="android:navigationBarColor">#202125</item>
|
||||
<item name="android:statusBarColor">#202125</item>
|
||||
|
||||
</style>
|
||||
</resources>
|
||||
|
||||
@@ -1,46 +1,11 @@
|
||||
{
|
||||
"expo": {
|
||||
"name": "LNReader",
|
||||
"slug": "LNReader",
|
||||
"scheme": "lnreader",
|
||||
"version": "1.1.0",
|
||||
"orientation": "default",
|
||||
"icon": "./assets/icon.png",
|
||||
"splash": {
|
||||
"image": "./assets/splash.png",
|
||||
"resizeMode": "cover",
|
||||
"backgroundColor": "#000000"
|
||||
},
|
||||
"updates": {
|
||||
"fallbackToCacheTimeout": 0,
|
||||
"enabled": false
|
||||
},
|
||||
"notification": {
|
||||
"icon": "./assets/notification.png",
|
||||
"color": "#00adb5"
|
||||
},
|
||||
"assetBundlePatterns": ["**/*"],
|
||||
"ios": {
|
||||
"supportsTablet": true
|
||||
},
|
||||
"android": {
|
||||
"adaptiveIcon": {
|
||||
"foregroundImage": "./assets/icon.png",
|
||||
"backgroundColor": "#FFFFFF"
|
||||
},
|
||||
"package": "com.rajarsheechatterjee.LNReader"
|
||||
},
|
||||
"androidStatusBar": {
|
||||
"barStyle": "light-content",
|
||||
"backgroundColor": "#00000000",
|
||||
"translucent": true
|
||||
},
|
||||
"androidNavigationBar": {
|
||||
"barStyle": "light-content",
|
||||
"backgroundColor": "#000000"
|
||||
},
|
||||
"web": {
|
||||
"favicon": "./assets/favicon.png"
|
||||
}
|
||||
"expo": {
|
||||
"name": "LNReader",
|
||||
"slug": "LNReader",
|
||||
"scheme": "lnreader",
|
||||
"notification": {
|
||||
"icon": "./assets/notification.png",
|
||||
"color": "#00adb5"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
Before Width: | Height: | Size: 17 KiB |
|
Before Width: | Height: | Size: 1.4 KiB |
|
Before Width: | Height: | Size: 266 KiB |
|
Before Width: | Height: | Size: 14 KiB After Width: | Height: | Size: 6.2 KiB |
|
Before Width: | Height: | Size: 2.1 KiB |
|
Before Width: | Height: | Size: 56 KiB |
|
Before Width: | Height: | Size: 28 KiB |
@@ -1,4 +1,4 @@
|
||||
module.exports = function(api) {
|
||||
module.exports = function (api) {
|
||||
api.cache(true);
|
||||
return {
|
||||
presets: ['babel-preset-expo'],
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import 'react-native-gesture-handler';
|
||||
import { registerRootComponent } from 'expo';
|
||||
import {registerRootComponent} from 'expo';
|
||||
|
||||
import App from './App';
|
||||
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -1,73 +1,67 @@
|
||||
{
|
||||
"scripts": {
|
||||
"start": "react-native start",
|
||||
"android": "react-native run-android",
|
||||
"ios": "react-native run-ios",
|
||||
"web": "expo start --web",
|
||||
"eject": "expo eject"
|
||||
},
|
||||
"dependencies": {
|
||||
"@react-native-async-storage/async-storage": "^1.13.0",
|
||||
"@react-native-community/masked-view": "0.1.10",
|
||||
"@react-native-community/netinfo": "6.0.0",
|
||||
"@react-native-community/slider": "3.0.3",
|
||||
"@react-navigation/material-bottom-tabs": "^5.3.10",
|
||||
"@react-navigation/native": "^5.8.10",
|
||||
"@react-navigation/stack": "^5.12.8",
|
||||
"expo": "^41.0.1",
|
||||
"expo-background-fetch": "~9.1.0",
|
||||
"expo-document-picker": "^9.1.2",
|
||||
"expo-file-system": "~11.0.2",
|
||||
"expo-font": "~9.1.0",
|
||||
"expo-linear-gradient": "~9.1.0",
|
||||
"expo-linking": "~2.2.3",
|
||||
"expo-notifications": "~0.11.6",
|
||||
"expo-permissions": "^12.0.1",
|
||||
"expo-sharing": "~9.1.2",
|
||||
"expo-splash-screen": "~0.10.2",
|
||||
"expo-sqlite": "~9.1.0",
|
||||
"expo-status-bar": "~1.0.4",
|
||||
"expo-task-manager": "~9.1.0",
|
||||
"expo-updates": "~0.5.4",
|
||||
"expo-web-browser": "~9.1.0",
|
||||
"moment": "^2.29.1",
|
||||
"qs": "^6.10.1",
|
||||
"react": "16.13.1",
|
||||
"react-dom": "16.13.1",
|
||||
"react-native": "^0.64.2",
|
||||
"react-native-background-actions": "^2.6.1",
|
||||
"react-native-cheerio": "^1.0.0-rc.4",
|
||||
"react-native-collapsible-header-views": "^1.1.2",
|
||||
"react-native-device-info": "^8.1.3",
|
||||
"react-native-easing-gradient": "^0.1.2",
|
||||
"react-native-fast-image": "^8.3.4",
|
||||
"react-native-fit-image": "^1.5.5",
|
||||
"react-native-gesture-handler": "~1.10.2",
|
||||
"react-native-immersive-mode": "^2.0.0",
|
||||
"react-native-navigation-bar-color": "^2.0.1",
|
||||
"react-native-offline": "^5.7.0",
|
||||
"react-native-pager-view": "^5.1.10",
|
||||
"react-native-paper": "^4.5.0",
|
||||
"react-native-reanimated": "^2.1.0",
|
||||
"react-native-safe-area-context": "3.2.0",
|
||||
"react-native-screens": "~3.0.0",
|
||||
"react-native-swipe-gestures": "^1.0.5",
|
||||
"react-native-tab-view": "^3.0.1",
|
||||
"react-native-unimodules": "~0.13.3",
|
||||
"react-native-vector-icons": "^7.1.0",
|
||||
"react-native-web": "^0.16.5",
|
||||
"react-native-webview": "^11.6.2",
|
||||
"react-redux": "^7.2.2",
|
||||
"redux": "^4.0.5",
|
||||
"redux-devtools-extension": "^2.13.9",
|
||||
"redux-persist": "^6.0.0",
|
||||
"redux-thunk": "^2.3.0",
|
||||
"rn-sliding-up-panel": "^2.4.4"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@babel/core": "~7.9.0"
|
||||
},
|
||||
"private": true,
|
||||
"name": "lnreader-eject",
|
||||
"version": "1.0.0"
|
||||
"scripts": {
|
||||
"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/bottom-tabs": "^5.11.15",
|
||||
"@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",
|
||||
"prop-types": "^15.7.2",
|
||||
"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-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.6"
|
||||
}
|
||||
|
||||
@@ -0,0 +1,47 @@
|
||||
import React from 'react';
|
||||
import {Dimensions, StyleSheet, View} from 'react-native';
|
||||
|
||||
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;
|
||||
}
|
||||
};
|
||||
|
||||
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,
|
||||
},
|
||||
});
|
||||
@@ -1,142 +1,30 @@
|
||||
import React, { useRef } from "react";
|
||||
import { View, TextInput, StyleSheet } from "react-native";
|
||||
import {
|
||||
TouchableRipple,
|
||||
IconButton,
|
||||
Appbar as MaterialAppbar,
|
||||
} from "react-native-paper";
|
||||
import Constants from "expo-constants";
|
||||
import React from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
|
||||
import { useNavigation } from "@react-navigation/native";
|
||||
import { useSelector } from "react-redux";
|
||||
import {Appbar as MaterialAppbar} from 'react-native-paper';
|
||||
|
||||
export const Appbar = ({ title, onBackAction, style, children }) => {
|
||||
const theme = useSelector((state) => state.settingsReducer.theme);
|
||||
import {useTheme} from '../hooks/reduxHooks';
|
||||
import {ViewPropTypes} from 'react-native';
|
||||
|
||||
return (
|
||||
<MaterialAppbar.Header
|
||||
style={[{ backgroundColor: theme.colorPrimary }, style]}
|
||||
>
|
||||
{onBackAction && (
|
||||
<MaterialAppbar.BackAction onPress={onBackAction} />
|
||||
)}
|
||||
<MaterialAppbar.Content
|
||||
title={title}
|
||||
titleStyle={{ color: theme.textColorPrimary }}
|
||||
/>
|
||||
{children}
|
||||
</MaterialAppbar.Header>
|
||||
);
|
||||
export const 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>
|
||||
);
|
||||
};
|
||||
|
||||
export const SearchAppbar = ({
|
||||
placeholder,
|
||||
getSearchResults,
|
||||
setSearchText,
|
||||
searchText,
|
||||
getNovels,
|
||||
setLoading,
|
||||
screen,
|
||||
openWebView,
|
||||
}) => {
|
||||
const searchRef = useRef(null);
|
||||
|
||||
const navigation = useNavigation();
|
||||
|
||||
const theme = useSelector((state) => state.settingsReducer.theme);
|
||||
|
||||
return (
|
||||
<TouchableRipple
|
||||
borderless
|
||||
onPress={() => searchRef.current.focus()}
|
||||
style={[
|
||||
styles.searchAppbarContainer,
|
||||
{ backgroundColor: theme.searchBarColor },
|
||||
]}
|
||||
>
|
||||
<View
|
||||
style={{
|
||||
flex: 1,
|
||||
flexDirection: "row",
|
||||
justifyContent: "space-between",
|
||||
}}
|
||||
>
|
||||
<View style={{ flex: 1, flexDirection: "row" }}>
|
||||
{screen === "Library" ? (
|
||||
<IconButton
|
||||
icon="magnify"
|
||||
color={theme.textColorSecondary}
|
||||
style={{ marginLeft: 0 }}
|
||||
size={23}
|
||||
/>
|
||||
) : (
|
||||
<IconButton
|
||||
icon="arrow-left"
|
||||
color={theme.textColorSecondary}
|
||||
style={{ marginLeft: 0 }}
|
||||
size={23}
|
||||
onPress={() => navigation.goBack()}
|
||||
/>
|
||||
)}
|
||||
<TextInput
|
||||
ref={searchRef}
|
||||
style={{
|
||||
fontSize: 16,
|
||||
color: theme.textColorSecondary,
|
||||
}}
|
||||
placeholder={placeholder}
|
||||
placeholderTextColor={theme.textColorSecondary}
|
||||
onChangeText={(text) => {
|
||||
setSearchText(text);
|
||||
if (screen === "Library") {
|
||||
getSearchResults(text);
|
||||
}
|
||||
}}
|
||||
onSubmitEditing={() => {
|
||||
if (screen === "Extension") {
|
||||
getSearchResults(searchText);
|
||||
}
|
||||
}}
|
||||
defaultValue={searchText}
|
||||
/>
|
||||
</View>
|
||||
{searchText !== "" && (
|
||||
<IconButton
|
||||
icon="close"
|
||||
color={theme.textColorSecondary}
|
||||
style={{ marginRight: 0 }}
|
||||
size={23}
|
||||
onPress={() => {
|
||||
setLoading?.(true);
|
||||
getNovels();
|
||||
setSearchText("");
|
||||
}}
|
||||
/>
|
||||
)}
|
||||
{openWebView && (
|
||||
<IconButton
|
||||
icon="earth"
|
||||
color={theme.textColorSecondary}
|
||||
style={{ marginRight: 0 }}
|
||||
size={23}
|
||||
onPress={openWebView}
|
||||
/>
|
||||
)}
|
||||
</View>
|
||||
</TouchableRipple>
|
||||
);
|
||||
Appbar.propTypes = {
|
||||
title: PropTypes.string,
|
||||
onBackAction: PropTypes.func,
|
||||
style: ViewPropTypes.style,
|
||||
};
|
||||
|
||||
const styles = StyleSheet.create({
|
||||
searchAppbarContainer: {
|
||||
marginTop: Constants.statusBarHeight + 8,
|
||||
height: 48,
|
||||
flexDirection: "row",
|
||||
alignItems: "center",
|
||||
paddingHorizontal: 16,
|
||||
marginBottom: 12,
|
||||
borderRadius: 8,
|
||||
marginHorizontal: 12,
|
||||
elevation: 2,
|
||||
},
|
||||
});
|
||||
|
||||
@@ -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.textColorHint,
|
||||
height: 5,
|
||||
width: 30,
|
||||
borderRadius: 50,
|
||||
top: 10,
|
||||
alignSelf: "center",
|
||||
}}
|
||||
/>
|
||||
const BottomSheetHandle = ({theme}) => (
|
||||
<View
|
||||
style={{
|
||||
backgroundColor: `${theme.textColorPrimary}66`,
|
||||
height: 4,
|
||||
width: 40,
|
||||
borderRadius: 50,
|
||||
top: 8,
|
||||
alignSelf: 'center',
|
||||
}}
|
||||
/>
|
||||
);
|
||||
|
||||
export default BottomSheetHandle;
|
||||
|
||||
@@ -0,0 +1,61 @@
|
||||
import React from 'react';
|
||||
import {Pressable, StyleSheet, Text} from 'react-native';
|
||||
import {Checkbox as PaperCheckbox} from 'react-native-paper';
|
||||
|
||||
import MaterialCommunityIcons from 'react-native-vector-icons/MaterialCommunityIcons';
|
||||
|
||||
export const Checkbox = ({label, status, onPress, disabled, theme}) => (
|
||||
<Pressable
|
||||
android_ripple={{color: theme.rippleColor}}
|
||||
style={styles.pressable}
|
||||
onPress={onPress}
|
||||
disabled={disabled}
|
||||
>
|
||||
<PaperCheckbox
|
||||
status={
|
||||
status === 'indeterminate'
|
||||
? 'indeterminate'
|
||||
: status
|
||||
? 'checked'
|
||||
: 'unchecked'
|
||||
}
|
||||
onPress={onPress}
|
||||
color={theme.colorAccent}
|
||||
uncheckedColor={theme.textColorSecondary}
|
||||
disabled={disabled}
|
||||
/>
|
||||
<Text style={{color: theme.textColorPrimary, marginLeft: 12}}>{label}</Text>
|
||||
</Pressable>
|
||||
);
|
||||
|
||||
export const SortItem = ({label, status, onPress, theme}) => (
|
||||
<Pressable
|
||||
android_ripple={{color: theme.rippleColor}}
|
||||
style={[styles.pressable, {paddingVertical: 16, paddingLeft: 64}]}
|
||||
onPress={onPress}
|
||||
>
|
||||
{status && (
|
||||
<MaterialCommunityIcons
|
||||
name={status === 'asc' ? 'arrow-up' : 'arrow-down'}
|
||||
color={theme.colorAccent}
|
||||
size={21}
|
||||
style={styles.icon}
|
||||
/>
|
||||
)}
|
||||
<Text style={{color: theme.textColorPrimary}}>{label}</Text>
|
||||
</Pressable>
|
||||
);
|
||||
|
||||
const styles = StyleSheet.create({
|
||||
pressable: {
|
||||
paddingHorizontal: 16,
|
||||
paddingVertical: 6,
|
||||
flexDirection: 'row',
|
||||
alignItems: 'center',
|
||||
},
|
||||
icon: {
|
||||
position: 'absolute',
|
||||
left: 24,
|
||||
alignSelf: 'center',
|
||||
},
|
||||
});
|
||||
@@ -0,0 +1,49 @@
|
||||
import React from 'react';
|
||||
import {View, Text, Pressable, StyleSheet} from 'react-native';
|
||||
|
||||
import PropTypes from 'prop-types';
|
||||
|
||||
export const Chip = ({label, onPress, theme}) => {
|
||||
return (
|
||||
<View style={[styles.container, {backgroundColor: theme.colorPrimary}]}>
|
||||
<Pressable
|
||||
style={styles.content}
|
||||
onPress={onPress}
|
||||
android_ripple={{color: theme.rippleColor}}
|
||||
>
|
||||
<Text style={[styles.label, {color: theme.textColorSecondary}]}>
|
||||
{label}
|
||||
</Text>
|
||||
</Pressable>
|
||||
</View>
|
||||
);
|
||||
};
|
||||
|
||||
Chip.propTypes = {
|
||||
label: PropTypes.string,
|
||||
onPress: PropTypes.func,
|
||||
theme: PropTypes.object.isRequired,
|
||||
};
|
||||
|
||||
const styles = StyleSheet.create({
|
||||
container: {
|
||||
borderRadius: 8,
|
||||
overflow: 'hidden',
|
||||
alignSelf: 'flex-start',
|
||||
justifyContent: 'center',
|
||||
alignItems: 'center',
|
||||
height: 32,
|
||||
marginHorizontal: 4,
|
||||
elevation: 1,
|
||||
},
|
||||
content: {
|
||||
flex: 1,
|
||||
flexDirection: 'row',
|
||||
justifyContent: 'center',
|
||||
alignItems: 'center',
|
||||
paddingHorizontal: 16,
|
||||
},
|
||||
label: {
|
||||
fontSize: 14,
|
||||
},
|
||||
});
|
||||
@@ -1,97 +1,148 @@
|
||||
import React, { useState } from "react";
|
||||
import { StyleSheet, Text } 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,
|
||||
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 = txt => setText(txt);
|
||||
|
||||
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',
|
||||
},
|
||||
};
|
||||
|
||||
return (
|
||||
<Portal>
|
||||
<Modal
|
||||
visible={modalVisible}
|
||||
onDismiss={onDismiss}
|
||||
contentContainerStyle={[
|
||||
styles.modalContainer,
|
||||
{ backgroundColor: theme.colorPrimary },
|
||||
]}
|
||||
>
|
||||
<Text
|
||||
style={[
|
||||
styles.modalTitle,
|
||||
{ color: theme.textColorPrimary },
|
||||
]}
|
||||
>
|
||||
{title}
|
||||
</Text>
|
||||
<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}
|
||||
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();
|
||||
}}
|
||||
/>
|
||||
<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,
|
||||
},
|
||||
});
|
||||
|
||||
@@ -1,21 +1,32 @@
|
||||
import React from "react";
|
||||
import { View, Text, Image, StyleSheet } from "react-native";
|
||||
import React from 'react';
|
||||
import {View, StyleSheet} from 'react-native';
|
||||
import {useSafeAreaInsets} from 'react-native-safe-area-context';
|
||||
|
||||
const ScreenContainer = ({ children, theme }) => (
|
||||
<View style={{ flex: 1, backgroundColor: theme.colorPrimaryDark }}>
|
||||
{children}
|
||||
const ScreenContainer = ({children, theme}) => {
|
||||
const {bottom} = useSafeAreaInsets();
|
||||
|
||||
return (
|
||||
<View
|
||||
style={{
|
||||
flex: 1,
|
||||
backgroundColor: theme.colorPrimaryDark,
|
||||
paddingBottom: bottom,
|
||||
}}
|
||||
>
|
||||
{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',
|
||||
},
|
||||
});
|
||||
|
||||
@@ -0,0 +1,110 @@
|
||||
/* eslint-disable no-bitwise */
|
||||
|
||||
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,
|
||||
|
||||
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,
|
||||
};
|
||||
}
|
||||
|
||||
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});
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
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;
|
||||
}
|
||||
}
|
||||
|
||||
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>;
|
||||
}
|
||||
}
|
||||
|
||||
const styles = StyleSheet.create({
|
||||
container: {
|
||||
// ...StyleSheet.absoluteFillObject,
|
||||
flexDirection: 'row',
|
||||
justifyContent: 'center',
|
||||
alignItems: 'center',
|
||||
},
|
||||
});
|
||||
@@ -0,0 +1,64 @@
|
||||
import React from 'react';
|
||||
import {Pressable, View} from 'react-native';
|
||||
import MaterialCommunityIcons from 'react-native-vector-icons/MaterialCommunityIcons';
|
||||
|
||||
export const ToggleButton = ({icon, selected, theme, color, onPress}) => (
|
||||
<View
|
||||
style={{
|
||||
borderRadius: 8,
|
||||
overflow: 'hidden',
|
||||
marginHorizontal: 4,
|
||||
}}
|
||||
>
|
||||
<Pressable
|
||||
android_ripple={{color: theme.rippleColor}}
|
||||
style={{
|
||||
padding: 10,
|
||||
alignItems: 'center',
|
||||
justifyContent: 'center',
|
||||
}}
|
||||
onPress={onPress}
|
||||
>
|
||||
<MaterialCommunityIcons
|
||||
name={icon}
|
||||
color={
|
||||
selected ? theme.colorAccent : color ? color : theme.textColorPrimary
|
||||
}
|
||||
size={24}
|
||||
/>
|
||||
</Pressable>
|
||||
</View>
|
||||
);
|
||||
|
||||
export const ToggleColorButton = ({
|
||||
selected,
|
||||
backgroundColor,
|
||||
textColor,
|
||||
onPress,
|
||||
}) => (
|
||||
<View
|
||||
style={{
|
||||
borderRadius: 50,
|
||||
overflow: 'hidden',
|
||||
marginHorizontal: 6,
|
||||
height: 44,
|
||||
width: 44,
|
||||
}}
|
||||
>
|
||||
<Pressable
|
||||
android_ripple={{color: textColor}}
|
||||
style={{
|
||||
flex: 1,
|
||||
padding: 10,
|
||||
backgroundColor: backgroundColor,
|
||||
alignItems: 'center',
|
||||
justifyContent: 'center',
|
||||
}}
|
||||
onPress={onPress}
|
||||
>
|
||||
{selected && (
|
||||
<MaterialCommunityIcons name="check" color={textColor} size={24} />
|
||||
)}
|
||||
</Pressable>
|
||||
</View>
|
||||
);
|
||||
@@ -0,0 +1,8 @@
|
||||
/* eslint-disable no-unused-vars */
|
||||
|
||||
import React from 'react';
|
||||
import {StyleSheet} from 'react-native';
|
||||
|
||||
export const Dialog = ({visible, theme}) => {};
|
||||
|
||||
const styles = StyleSheet.create({});
|
||||
@@ -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.textColorSecondary },
|
||||
style,
|
||||
iconStyle,
|
||||
]}
|
||||
>
|
||||
{icon}
|
||||
</Text>
|
||||
<Text
|
||||
style={[
|
||||
styles.emptyViewText,
|
||||
{ color: theme.textColorSecondary },
|
||||
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,
|
||||
},
|
||||
});
|
||||
|
||||
@@ -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.textColorSecondary}
|
||||
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;
|
||||
|
||||
@@ -0,0 +1,75 @@
|
||||
import React from 'react';
|
||||
import {Pressable, StyleSheet, Text, View} from 'react-native';
|
||||
import {IconButton} from 'react-native-paper';
|
||||
|
||||
// const icons = ['(・o・;)', 'Σ(ಠ_ಠ)', 'ಥ_ಥ', '(˘・_・˘)', '(; ̄Д ̄)', '(・Д・。'];
|
||||
|
||||
export const ErrorView = ({errorName, actions, theme}) => (
|
||||
<View style={{flex: 1, justifyContent: 'center', alignItems: 'center'}}>
|
||||
<Text style={[styles.emptyViewIcon, {color: theme.textColorHint}]}>
|
||||
{/* {icons[Math.floor(Math.random() * 5)]} */}
|
||||
ಥ_ಥ
|
||||
</Text>
|
||||
<Text style={[styles.emptyViewText, {color: theme.textColorHint}]}>
|
||||
{errorName}
|
||||
</Text>
|
||||
<View style={{flexDirection: 'row'}}>
|
||||
{actions.map(action => (
|
||||
<View
|
||||
style={{
|
||||
borderRadius: 4,
|
||||
overflow: 'hidden',
|
||||
margin: 16,
|
||||
}}
|
||||
>
|
||||
<Pressable
|
||||
android_ripple={{
|
||||
color: theme.rippleColor,
|
||||
borderless: false,
|
||||
}}
|
||||
onPress={action.onPress}
|
||||
style={{
|
||||
justifyContent: 'center',
|
||||
alignItems: 'center',
|
||||
paddingBottom: 8,
|
||||
paddingHorizontal: 20,
|
||||
minWidth: 100,
|
||||
}}
|
||||
>
|
||||
<IconButton
|
||||
icon={action.icon}
|
||||
color={theme.textColorHint}
|
||||
size={24}
|
||||
style={{margin: 0}}
|
||||
/>
|
||||
<Text
|
||||
style={{
|
||||
fontSize: 12,
|
||||
color: theme.textColorHint,
|
||||
}}
|
||||
>
|
||||
{action.name}
|
||||
</Text>
|
||||
</Pressable>
|
||||
</View>
|
||||
))}
|
||||
</View>
|
||||
</View>
|
||||
);
|
||||
|
||||
const styles = StyleSheet.create({
|
||||
emptyViewContainer: {
|
||||
flex: 1,
|
||||
justifyContent: 'center',
|
||||
alignItems: 'center',
|
||||
},
|
||||
emptyViewIcon: {
|
||||
fontSize: 45,
|
||||
},
|
||||
emptyViewText: {
|
||||
fontWeight: 'bold',
|
||||
marginTop: 10,
|
||||
textAlign: 'center',
|
||||
paddingHorizontal: 30,
|
||||
},
|
||||
});
|
||||
@@ -1,2 +0,0 @@
|
||||
import { NativeModules } from "react-native";
|
||||
module.exports = NativeModules.FullScreen;
|
||||
@@ -0,0 +1,42 @@
|
||||
import React from 'react';
|
||||
import {Pressable, StyleSheet, ViewPropTypes} from 'react-native';
|
||||
import PropTypes from 'prop-types';
|
||||
|
||||
import MaterialCommunityIcons from 'react-native-vector-icons/MaterialCommunityIcons';
|
||||
|
||||
export const IconButton = ({
|
||||
icon,
|
||||
size,
|
||||
color,
|
||||
onPress,
|
||||
containerStyle,
|
||||
theme,
|
||||
}) => (
|
||||
<Pressable
|
||||
style={[styles.container, containerStyle]}
|
||||
android_ripple={{color: theme.rippleColor, borderless: true, radius: 20}}
|
||||
onPress={onPress}
|
||||
>
|
||||
<MaterialCommunityIcons name={icon} size={size} color={color} />
|
||||
</Pressable>
|
||||
);
|
||||
|
||||
IconButton.defaultProps = {
|
||||
size: 24,
|
||||
color: '#FFFFFF',
|
||||
};
|
||||
|
||||
IconButton.propTypes = {
|
||||
icon: PropTypes.string.isRequired,
|
||||
size: PropTypes.number.isRequired,
|
||||
color: PropTypes.string,
|
||||
onPress: PropTypes.func,
|
||||
containerStyle: ViewPropTypes.style,
|
||||
theme: PropTypes.object.isRequired,
|
||||
};
|
||||
|
||||
const styles = StyleSheet.create({
|
||||
container: {
|
||||
margin: 6,
|
||||
},
|
||||
});
|
||||
@@ -1,96 +1,129 @@
|
||||
import React from "react";
|
||||
import { StyleSheet, View } from "react-native";
|
||||
import React from 'react';
|
||||
import {Pressable, StyleSheet, Text, View} from 'react-native';
|
||||
|
||||
import { List, 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 }) => (
|
||||
<List.Section style={styles.listSection}>{children}</List.Section>
|
||||
const ListSection = ({children}) => (
|
||||
<PaperList.Section style={styles.listSection}>{children}</PaperList.Section>
|
||||
);
|
||||
|
||||
const ListSubHeader = ({ children, theme }) => (
|
||||
<List.Subheader
|
||||
style={[styles.listSubHeader, { color: theme.colorAccent }]}
|
||||
>
|
||||
{children}
|
||||
</List.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,
|
||||
}) => (
|
||||
<List.Item
|
||||
title={title}
|
||||
style={style}
|
||||
titleStyle={[{ color: theme.textColorPrimary }, titleStyle]}
|
||||
description={description}
|
||||
descriptionStyle={{ color: theme.textColorSecondary }}
|
||||
descriptionNumberOfLines={1}
|
||||
left={() => (
|
||||
<View style={{ justifyContent: "center" }}>
|
||||
{icon && (
|
||||
<List.Icon
|
||||
color={theme.colorAccent}
|
||||
icon={icon}
|
||||
style={{
|
||||
marginVertical: 0,
|
||||
}}
|
||||
/>
|
||||
)}
|
||||
</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={{
|
||||
marginVertical: 0,
|
||||
}}
|
||||
/>
|
||||
)}
|
||||
right={() =>
|
||||
right && (
|
||||
<MaterialCommunityIcons
|
||||
name={right}
|
||||
color={iconColor || "#47a84a"}
|
||||
size={23}
|
||||
style={{ marginRight: 16, textAlignVertical: "center" }}
|
||||
/>
|
||||
)
|
||||
}
|
||||
onPress={onPress}
|
||||
rippleColor={theme.rippleColor}
|
||||
</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} />
|
||||
}
|
||||
/>
|
||||
);
|
||||
|
||||
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 Divider = ({ theme }) => (
|
||||
<PaperDivider style={{ backgroundColor: theme.dividerColor, height: 1 }} />
|
||||
const Icon = ({icon, theme}) => (
|
||||
<PaperList.Icon color={theme.colorAccent} icon={icon} style={{margin: 0}} />
|
||||
);
|
||||
|
||||
const InfoItem = ({ title, icon, theme }) => (
|
||||
<List.Item
|
||||
title={title}
|
||||
titleStyle={{ color: theme.textColorSecondary, fontSize: 14 }}
|
||||
titleNumberOfLines={5}
|
||||
left={() =>
|
||||
icon && (
|
||||
<List.Icon
|
||||
color={theme.textColorSecondary}
|
||||
icon={icon}
|
||||
style={{ marginVertical: 0 }}
|
||||
/>
|
||||
)
|
||||
}
|
||||
/>
|
||||
);
|
||||
|
||||
export { ListSection, ListSubHeader, ListItem, Divider, InfoItem };
|
||||
export const List = {
|
||||
Section: ListSection,
|
||||
SubHeader: ListSubHeader,
|
||||
Item: ListItem,
|
||||
Divider,
|
||||
InfoItem,
|
||||
ColorItem,
|
||||
Icon,
|
||||
};
|
||||
|
||||
const styles = StyleSheet.create({
|
||||
listSection: {
|
||||
flex: 1,
|
||||
marginVertical: 0,
|
||||
},
|
||||
listSubHeader: {
|
||||
paddingBottom: 5,
|
||||
},
|
||||
listSection: {
|
||||
flex: 1,
|
||||
marginVertical: 0,
|
||||
},
|
||||
listSubHeader: {
|
||||
paddingBottom: 5,
|
||||
},
|
||||
});
|
||||
|
||||
@@ -1,65 +1,80 @@
|
||||
import React from "react";
|
||||
import { StyleSheet, View, Text, Image } from "react-native";
|
||||
import FastImage from "react-native-fast-image";
|
||||
import { TouchableRipple } from "react-native-paper";
|
||||
import React from 'react';
|
||||
import {StyleSheet, View, Text, Pressable} from 'react-native';
|
||||
|
||||
const ListView = ({ item, downloadBadge, unreadBadge, theme, onPress }) => {
|
||||
return (
|
||||
<TouchableRipple
|
||||
borderless
|
||||
centered
|
||||
rippleColor={theme.rippleColor}
|
||||
style={styles.listView}
|
||||
onPress={onPress}
|
||||
>
|
||||
<>
|
||||
<FastImage
|
||||
source={{ uri: item.novelCover }}
|
||||
style={styles.extensionIcon}
|
||||
/>
|
||||
<Text
|
||||
style={[
|
||||
{ color: theme.textColorPrimary },
|
||||
styles.novelName,
|
||||
]}
|
||||
numberOfLines={1}
|
||||
>
|
||||
{item.novelName}
|
||||
</Text>
|
||||
<View style={styles.badgeContainer}>
|
||||
{downloadBadge}
|
||||
{unreadBadge}
|
||||
</View>
|
||||
</>
|
||||
</TouchableRipple>
|
||||
);
|
||||
import FastImage from 'react-native-fast-image';
|
||||
import {defaultCoverUri} from '../sources/helpers/constants';
|
||||
|
||||
const ListView = ({
|
||||
item,
|
||||
downloadBadge,
|
||||
unreadBadge,
|
||||
inLibraryBadge,
|
||||
theme,
|
||||
onPress,
|
||||
isSelected,
|
||||
onLongPress,
|
||||
}) => {
|
||||
const uri =
|
||||
item.novelCover && !item.novelCover.startsWith('/')
|
||||
? item.novelCover
|
||||
: defaultCoverUri;
|
||||
|
||||
return (
|
||||
<Pressable
|
||||
android_ripple={{color: theme.rippleColor}}
|
||||
style={[
|
||||
styles.listView,
|
||||
isSelected && {backgroundColor: theme.rippleColor},
|
||||
]}
|
||||
onPress={onPress}
|
||||
onLongPress={onLongPress}
|
||||
>
|
||||
<FastImage
|
||||
source={{
|
||||
uri,
|
||||
}}
|
||||
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',
|
||||
},
|
||||
});
|
||||
|
||||
@@ -0,0 +1,8 @@
|
||||
import React from 'react';
|
||||
import {ActivityIndicator, View} from 'react-native';
|
||||
|
||||
export const LoadingScreen = ({theme}) => (
|
||||
<View style={{flex: 1, justifyContent: 'center'}}>
|
||||
<ActivityIndicator size={50} color={theme.colorAccent} />
|
||||
</View>
|
||||
);
|
||||
@@ -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,
|
||||
},
|
||||
});
|
||||
|
||||
@@ -1,189 +1,312 @@
|
||||
import React from "react";
|
||||
import { StyleSheet, View, Text, useWindowDimensions } from "react-native";
|
||||
import { LinearGradient } from "expo-linear-gradient";
|
||||
import { TouchableRipple } from "react-native-paper";
|
||||
import React, {memo, useCallback} from 'react';
|
||||
import {
|
||||
StyleSheet,
|
||||
View,
|
||||
Text,
|
||||
useWindowDimensions,
|
||||
Pressable,
|
||||
} from 'react-native';
|
||||
|
||||
import { useSelector } from "react-redux";
|
||||
import { useSettings, useTheme } from "../hooks/reduxHooks";
|
||||
import ListView from "./ListView";
|
||||
import FastImage from "react-native-fast-image";
|
||||
import { getDeviceOrientation } from "../services/utils/helpers";
|
||||
import {LinearGradient} from 'expo-linear-gradient';
|
||||
import FastImage from 'react-native-fast-image';
|
||||
|
||||
const NovelCover = ({ item, onPress, libraryStatus }) => {
|
||||
const theme = useTheme();
|
||||
const { displayMode, novelsPerRow, showDownloadBadges, showUnreadBadges } =
|
||||
useSettings();
|
||||
import ListView from './ListView';
|
||||
|
||||
const window = useWindowDimensions();
|
||||
import {useDeviceOrientation} from '../services/utils/helpers';
|
||||
import {useSettings} from '../hooks/reduxHooks';
|
||||
import {defaultCoverUri} from '../sources/helpers/constants';
|
||||
|
||||
const orientation = getDeviceOrientation();
|
||||
const NovelCover = ({
|
||||
item,
|
||||
onPress,
|
||||
libraryStatus,
|
||||
theme,
|
||||
isSelected,
|
||||
onLongPress,
|
||||
selectedNovels,
|
||||
}) => {
|
||||
const {displayMode, novelsPerRow, showDownloadBadges, showUnreadBadges} =
|
||||
useSettings();
|
||||
|
||||
const getNovelsPerRow = () =>
|
||||
orientation === "landscape" ? 6 : novelsPerRow;
|
||||
const window = useWindowDimensions();
|
||||
|
||||
const getHeight = () => (window.width / getNovelsPerRow()) * (4 / 3);
|
||||
const orientation = useDeviceOrientation();
|
||||
|
||||
const comfortableTitle = () =>
|
||||
displayMode === 1 && (
|
||||
<Text
|
||||
numberOfLines={2}
|
||||
style={[
|
||||
styles.title,
|
||||
{
|
||||
color: theme.textColorPrimary,
|
||||
padding: 4,
|
||||
},
|
||||
]}
|
||||
>
|
||||
{item.novelName}
|
||||
</Text>
|
||||
);
|
||||
const getNovelsPerRow = () =>
|
||||
orientation === 'landscape' ? 6 : novelsPerRow;
|
||||
|
||||
const compactTitle = () =>
|
||||
displayMode === 0 && (
|
||||
<View style={styles.titleContainer}>
|
||||
<LinearGradient
|
||||
colors={["transparent", "rgba(0,0,0,0.6)"]}
|
||||
style={styles.linearGradient}
|
||||
>
|
||||
<Text
|
||||
numberOfLines={2}
|
||||
style={[styles.title, { color: "rgba(255,255,255,1)" }]}
|
||||
>
|
||||
{item.novelName}
|
||||
</Text>
|
||||
</LinearGradient>
|
||||
</View>
|
||||
);
|
||||
const getHeight = useCallback(
|
||||
() => (window.width / getNovelsPerRow()) * (4 / 3),
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
[],
|
||||
);
|
||||
|
||||
const unreadBadge = () =>
|
||||
showUnreadBadges &&
|
||||
item.chaptersUnread && (
|
||||
<Text
|
||||
style={[
|
||||
styles.unreadBadge,
|
||||
!item.chaptersDownloaded && {
|
||||
borderTopLeftRadius: 4,
|
||||
borderBottomLeftRadius: 4,
|
||||
},
|
||||
!showDownloadBadges && {
|
||||
borderRadius: 4,
|
||||
},
|
||||
{ backgroundColor: theme.colorAccent },
|
||||
]}
|
||||
>
|
||||
{item.chaptersUnread}
|
||||
</Text>
|
||||
);
|
||||
const selectNovel = () => onLongPress && onLongPress(item.novelId);
|
||||
|
||||
const downloadBadge = () =>
|
||||
const uri =
|
||||
item.novelCover && !item.novelCover.startsWith('/')
|
||||
? item.novelCover
|
||||
: defaultCoverUri;
|
||||
|
||||
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}}
|
||||
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 && (
|
||||
<Text
|
||||
style={[
|
||||
styles.downloadBadge,
|
||||
!item.chaptersUnread && {
|
||||
borderTopRightRadius: 4,
|
||||
borderBottomRightRadius: 4,
|
||||
},
|
||||
!showUnreadBadges && {
|
||||
borderRadius: 4,
|
||||
},
|
||||
]}
|
||||
>
|
||||
{item.chaptersDownloaded}
|
||||
</Text>
|
||||
);
|
||||
|
||||
return displayMode !== 2 ? (
|
||||
<View style={{ flex: 1 / getNovelsPerRow() }}>
|
||||
<TouchableRipple
|
||||
borderless
|
||||
centered
|
||||
rippleColor={theme.rippleColor}
|
||||
style={styles.opac}
|
||||
onPress={onPress}
|
||||
>
|
||||
<>
|
||||
<FastImage
|
||||
source={{ uri: item.novelCover }}
|
||||
style={[
|
||||
{ height: getHeight(), borderRadius: 4 },
|
||||
libraryStatus && { opacity: 0.5 },
|
||||
]}
|
||||
>
|
||||
<View style={{ flexDirection: "row", margin: 4 }}>
|
||||
{downloadBadge()}
|
||||
{unreadBadge()}
|
||||
</View>
|
||||
{compactTitle()}
|
||||
</FastImage>
|
||||
{comfortableTitle()}
|
||||
</>
|
||||
</TouchableRipple>
|
||||
</View>
|
||||
) : (
|
||||
<ListView
|
||||
item={item}
|
||||
downloadBadge={downloadBadge()}
|
||||
unreadBadge={unreadBadge()}
|
||||
<DownloadBadge
|
||||
showUnreadBadges={showUnreadBadges}
|
||||
chaptersDownloaded={item.chaptersDownloaded}
|
||||
chaptersUnread={item.chaptersUnread}
|
||||
/>
|
||||
)
|
||||
}
|
||||
unreadBadge={
|
||||
showUnreadBadges &&
|
||||
item.chaptersUnread && (
|
||||
<UnreadBadge
|
||||
theme={theme}
|
||||
onPress={onPress}
|
||||
/>
|
||||
);
|
||||
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;
|
||||
export default memo(NovelCover);
|
||||
|
||||
const styles = StyleSheet.create({
|
||||
titleContainer: {
|
||||
flex: 1,
|
||||
justifyContent: "flex-end",
|
||||
borderRadius: 4,
|
||||
},
|
||||
title: {
|
||||
fontFamily: "pt-sans-bold",
|
||||
fontSize: 14,
|
||||
padding: 8,
|
||||
},
|
||||
linearGradient: {
|
||||
bottom: -1,
|
||||
borderRadius: 4,
|
||||
},
|
||||
opac: {
|
||||
paddingHorizontal: 4.5,
|
||||
paddingVertical: 4,
|
||||
borderRadius: 4,
|
||||
},
|
||||
extensionIcon: {
|
||||
width: 42,
|
||||
height: 42,
|
||||
borderRadius: 4,
|
||||
},
|
||||
listView: {
|
||||
flex: 1,
|
||||
paddingVertical: 8,
|
||||
paddingHorizontal: 12,
|
||||
flexDirection: "row",
|
||||
alignItems: "center",
|
||||
borderRadius: 4,
|
||||
},
|
||||
const ComfortableTitle = ({theme, novelName}) => (
|
||||
<Text
|
||||
numberOfLines={2}
|
||||
style={[
|
||||
styles.title,
|
||||
{
|
||||
color: theme.textColorPrimary,
|
||||
padding: 4,
|
||||
},
|
||||
]}
|
||||
>
|
||||
{novelName}
|
||||
</Text>
|
||||
);
|
||||
|
||||
downloadBadge: {
|
||||
backgroundColor: "#47a84a",
|
||||
color: "#FFFFFF",
|
||||
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 UnreadBadge = ({
|
||||
chaptersDownloaded,
|
||||
chaptersUnread,
|
||||
showDownloadBadges,
|
||||
theme,
|
||||
}) => (
|
||||
<Text
|
||||
style={[
|
||||
styles.unreadBadge,
|
||||
!chaptersDownloaded && {
|
||||
borderTopLeftRadius: 4,
|
||||
borderBottomLeftRadius: 4,
|
||||
paddingTop: 2,
|
||||
paddingHorizontal: 4,
|
||||
fontSize: 12,
|
||||
},
|
||||
unreadBadge: {
|
||||
color: "#FFFFFF",
|
||||
},
|
||||
!showDownloadBadges && {
|
||||
borderRadius: 4,
|
||||
},
|
||||
{
|
||||
backgroundColor: theme.colorAccent,
|
||||
color: theme.colorButtonText,
|
||||
},
|
||||
]}
|
||||
>
|
||||
{chaptersUnread}
|
||||
</Text>
|
||||
);
|
||||
|
||||
const DownloadBadge = ({
|
||||
chaptersDownloaded,
|
||||
showUnreadBadges,
|
||||
chaptersUnread,
|
||||
}) => (
|
||||
<Text
|
||||
style={[
|
||||
styles.downloadBadge,
|
||||
!chaptersUnread && {
|
||||
borderTopRightRadius: 4,
|
||||
borderBottomRightRadius: 4,
|
||||
paddingTop: 2,
|
||||
paddingHorizontal: 4,
|
||||
fontSize: 12,
|
||||
},
|
||||
},
|
||||
!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,
|
||||
},
|
||||
|
||||
downloadBadge: {
|
||||
backgroundColor: '#2979FF',
|
||||
color: '#FFFFFF',
|
||||
borderTopLeftRadius: 4,
|
||||
borderBottomLeftRadius: 4,
|
||||
paddingTop: 2,
|
||||
paddingHorizontal: 4,
|
||||
fontSize: 12,
|
||||
},
|
||||
unreadBadge: {
|
||||
color: '#FFFFFF',
|
||||
borderTopRightRadius: 4,
|
||||
borderBottomRightRadius: 4,
|
||||
paddingTop: 2,
|
||||
paddingHorizontal: 4,
|
||||
fontSize: 12,
|
||||
},
|
||||
inLibraryBadge: {
|
||||
paddingVertical: 2,
|
||||
paddingHorizontal: 4,
|
||||
fontSize: 12,
|
||||
},
|
||||
compactTitleContainer: {
|
||||
position: 'absolute',
|
||||
bottom: 4,
|
||||
left: 4,
|
||||
right: 4,
|
||||
},
|
||||
badgeContainer: {
|
||||
position: 'absolute',
|
||||
zIndex: 1,
|
||||
top: 8,
|
||||
left: 8,
|
||||
flexDirection: 'row',
|
||||
},
|
||||
});
|
||||
|
||||
@@ -1,57 +1,61 @@
|
||||
import React 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 {useDeviceOrientation} 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 = useDeviceOrientation();
|
||||
|
||||
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;
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<FlatList
|
||||
contentContainerStyle={styles.flatListCont}
|
||||
numColumns={getNovelsPerRow()}
|
||||
key={[orientation, getNovelsPerRow()]}
|
||||
data={data}
|
||||
keyExtractor={(item) => item.novelUrl}
|
||||
renderItem={renderItem}
|
||||
refreshControl={refreshControl}
|
||||
ListEmptyComponent={ListEmptyComponent}
|
||||
ListFooterComponent={ListFooterComponent}
|
||||
onScroll={onScroll}
|
||||
onEndReached={onEndReached}
|
||||
/>
|
||||
);
|
||||
const keyExtractor = useCallback(item => item.sourceId + 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}
|
||||
/>
|
||||
);
|
||||
};
|
||||
|
||||
export default NovelList;
|
||||
|
||||
const styles = StyleSheet.create({
|
||||
flatListCont: {
|
||||
flexGrow: 1,
|
||||
paddingVertical: 8,
|
||||
paddingHorizontal: 4,
|
||||
},
|
||||
flatListCont: {
|
||||
flexGrow: 1,
|
||||
paddingBottom: 56,
|
||||
},
|
||||
});
|
||||
|
||||
@@ -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,
|
||||
},
|
||||
});
|
||||
|
||||
@@ -0,0 +1,33 @@
|
||||
import React from 'react';
|
||||
import {Pressable, StyleSheet, Text} from 'react-native';
|
||||
import {RadioButton as PaperRadioButton} from 'react-native-paper';
|
||||
|
||||
export const RadioButton = ({label, status, onPress, style, theme}) => (
|
||||
<Pressable
|
||||
android_ripple={{color: theme.rippleColor}}
|
||||
style={[styles.pressable, style]}
|
||||
onPress={onPress}
|
||||
>
|
||||
<PaperRadioButton
|
||||
status={status ? 'checked' : 'unchecked'}
|
||||
onPress={onPress}
|
||||
color={theme.colorAccent}
|
||||
uncheckedColor={theme.textColorSecondary}
|
||||
/>
|
||||
<Text style={{color: theme.textColorPrimary, marginLeft: 12}}>{label}</Text>
|
||||
</Pressable>
|
||||
);
|
||||
|
||||
const styles = StyleSheet.create({
|
||||
pressable: {
|
||||
paddingHorizontal: 16,
|
||||
paddingVertical: 6,
|
||||
flexDirection: 'row',
|
||||
alignItems: 'center',
|
||||
},
|
||||
icon: {
|
||||
position: 'absolute',
|
||||
left: 24,
|
||||
alignSelf: 'center',
|
||||
},
|
||||
});
|
||||
@@ -1,97 +0,0 @@
|
||||
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";
|
||||
|
||||
export const Searchbar = ({
|
||||
theme,
|
||||
placeholder,
|
||||
searchText,
|
||||
backAction,
|
||||
onBackAction,
|
||||
clearSearchbar,
|
||||
onChangeText,
|
||||
onSubmitEditing,
|
||||
actions,
|
||||
}) => {
|
||||
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.textColorSecondary}
|
||||
style={{ marginLeft: 0 }}
|
||||
size={23}
|
||||
onPress={onBackAction}
|
||||
/>
|
||||
)}
|
||||
<TextInput
|
||||
ref={searchRef}
|
||||
style={{
|
||||
fontSize: 16,
|
||||
color: theme.textColorSecondary,
|
||||
}}
|
||||
placeholder={placeholder}
|
||||
placeholderTextColor={theme.textColorSecondary}
|
||||
onChangeText={onChangeText}
|
||||
onSubmitEditing={onSubmitEditing}
|
||||
defaultValue={searchText}
|
||||
/>
|
||||
</Row>
|
||||
{searchText !== "" && (
|
||||
<IconButton
|
||||
icon="close"
|
||||
color={theme.textColorSecondary}
|
||||
style={{ marginRight: 0 }}
|
||||
size={23}
|
||||
onPress={clearSearchbar}
|
||||
/>
|
||||
)}
|
||||
{actions &&
|
||||
actions.map((action, index) => (
|
||||
<IconButton
|
||||
key={index}
|
||||
icon={action.icon}
|
||||
color={action.color ?? theme.textColorSecondary}
|
||||
style={{ marginRight: 0 }}
|
||||
size={23}
|
||||
onPress={action.onPress}
|
||||
/>
|
||||
))}
|
||||
</View>
|
||||
</TouchableRipple>
|
||||
);
|
||||
};
|
||||
|
||||
const styles = StyleSheet.create({
|
||||
searchbarContainer: {
|
||||
marginTop: StatusBar.currentHeight + 4,
|
||||
height: 48,
|
||||
flexDirection: "row",
|
||||
alignItems: "center",
|
||||
paddingHorizontal: 16,
|
||||
marginBottom: 12,
|
||||
borderRadius: 8,
|
||||
marginHorizontal: 12,
|
||||
elevation: 2,
|
||||
backgroundColor: "white",
|
||||
},
|
||||
container: {
|
||||
flex: 1,
|
||||
flexDirection: "row",
|
||||
justifyContent: "space-between",
|
||||
},
|
||||
});
|
||||
@@ -0,0 +1,86 @@
|
||||
import React, {useRef} from 'react';
|
||||
import {StyleSheet, View, TextInput, StatusBar, Pressable} from 'react-native';
|
||||
|
||||
import {Row} from '../Common';
|
||||
import {IconButton} from '../IconButton/IconButton';
|
||||
|
||||
export const Searchbar = ({
|
||||
theme,
|
||||
placeholder,
|
||||
searchText,
|
||||
backAction,
|
||||
onBackAction,
|
||||
clearSearchbar,
|
||||
onChangeText,
|
||||
onSubmitEditing,
|
||||
actions,
|
||||
}) => {
|
||||
const searchRef = useRef(null);
|
||||
|
||||
return (
|
||||
<View style={styles.searchBar}>
|
||||
<Pressable
|
||||
onPress={() => searchRef.current.focus()}
|
||||
style={[styles.pressable, {backgroundColor: theme.searchBarColor}]}
|
||||
android_ripple={{color: theme.rippleColor}}
|
||||
>
|
||||
<Row style={{flex: 1}}>
|
||||
{backAction && (
|
||||
<IconButton
|
||||
icon={backAction}
|
||||
color={theme.textColorPrimary}
|
||||
onPress={onBackAction}
|
||||
theme={theme}
|
||||
containerStyle={{marginLeft: 0}}
|
||||
/>
|
||||
)}
|
||||
<TextInput
|
||||
ref={searchRef}
|
||||
style={{fontSize: 16, color: theme.textColorPrimary, marginLeft: 4}}
|
||||
placeholder={placeholder}
|
||||
placeholderTextColor={theme.textColorSecondary}
|
||||
onChangeText={onChangeText}
|
||||
onSubmitEditing={onSubmitEditing}
|
||||
defaultValue={searchText}
|
||||
/>
|
||||
</Row>
|
||||
{searchText !== '' && (
|
||||
<IconButton
|
||||
icon="close"
|
||||
color={theme.textColorPrimary}
|
||||
onPress={clearSearchbar}
|
||||
theme={theme}
|
||||
/>
|
||||
)}
|
||||
{actions &&
|
||||
actions.map((action, index) => (
|
||||
<IconButton
|
||||
key={index}
|
||||
icon={action.icon}
|
||||
color={action.color ?? theme.textColorPrimary}
|
||||
onPress={action.onPress}
|
||||
theme={theme}
|
||||
containerStyle={{marginRight: 0, marginLeft: 16}}
|
||||
/>
|
||||
))}
|
||||
</Pressable>
|
||||
</View>
|
||||
);
|
||||
};
|
||||
|
||||
const styles = StyleSheet.create({
|
||||
searchBar: {
|
||||
marginTop: StatusBar.currentHeight + 8,
|
||||
marginBottom: 8,
|
||||
marginHorizontal: 16,
|
||||
height: 48,
|
||||
borderRadius: 50,
|
||||
overflow: 'hidden',
|
||||
},
|
||||
pressable: {
|
||||
flexDirection: 'row',
|
||||
justifyContent: 'space-between',
|
||||
alignItems: 'center',
|
||||
paddingHorizontal: 16,
|
||||
},
|
||||
});
|
||||
@@ -0,0 +1,50 @@
|
||||
import React from 'react';
|
||||
import {Pressable, StyleSheet, View, Text} from 'react-native';
|
||||
|
||||
import {Switch, List} from 'react-native-paper';
|
||||
|
||||
const SwitchSetting = ({label, description, onPress, theme, icon, value}) => (
|
||||
<Pressable
|
||||
android_ripple={{color: theme.rippleColor}}
|
||||
style={styles.container}
|
||||
onPress={onPress}
|
||||
>
|
||||
<View style={{flexDirection: 'row', flex: 1}}>
|
||||
{icon && (
|
||||
<List.Icon color={theme.colorAccent} icon={icon} style={{margin: 0}} />
|
||||
)}
|
||||
<View style={[icon && {marginLeft: 16}]}>
|
||||
<Text
|
||||
style={{
|
||||
color: theme.textColorPrimary,
|
||||
fontSize: 16,
|
||||
flex: 1,
|
||||
textAlignVertical: 'center',
|
||||
}}
|
||||
>
|
||||
{label}
|
||||
</Text>
|
||||
{description && (
|
||||
<Text style={{color: theme.textColorSecondary}}>{description}</Text>
|
||||
)}
|
||||
</View>
|
||||
</View>
|
||||
<Switch
|
||||
value={value}
|
||||
onValueChange={onPress}
|
||||
color={theme.colorAccent}
|
||||
style={{marginRight: 8}}
|
||||
/>
|
||||
</Pressable>
|
||||
);
|
||||
|
||||
export default SwitchSetting;
|
||||
|
||||
const styles = StyleSheet.create({
|
||||
container: {
|
||||
paddingHorizontal: 16,
|
||||
paddingVertical: 12,
|
||||
flexDirection: 'row',
|
||||
justifyContent: 'space-between',
|
||||
},
|
||||
});
|
||||
@@ -0,0 +1,172 @@
|
||||
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}) => (
|
||||
<View
|
||||
style={{
|
||||
justifyContent: 'center',
|
||||
alignItems: 'center',
|
||||
marginRight: 16,
|
||||
}}
|
||||
>
|
||||
<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,
|
||||
}}
|
||||
>
|
||||
<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>
|
||||
<Text
|
||||
style={{
|
||||
color: currentTheme.textColorPrimary,
|
||||
fontSize: 12,
|
||||
paddingVertical: 4,
|
||||
}}
|
||||
>
|
||||
{theme.name}
|
||||
</Text>
|
||||
</View>
|
||||
);
|
||||
|
||||
// const styles = StyleSheet.create({});
|
||||
@@ -0,0 +1,20 @@
|
||||
import React from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
import {Text as NativeText, TextPropTypes} from 'react-native';
|
||||
|
||||
const Text = ({type, children, style, theme}) => {
|
||||
const color =
|
||||
type === 'primary'
|
||||
? theme.textColorPrimary
|
||||
: type === 'secondary'
|
||||
? theme.textColorSecondary
|
||||
: theme.textColorHint;
|
||||
|
||||
return <NativeText style={[style, {color}]}>{children}</NativeText>;
|
||||
};
|
||||
|
||||
Text.propType = {
|
||||
style: TextPropTypes.style,
|
||||
type: PropTypes.oneOf(['primary', 'secondary', 'hint']),
|
||||
theme: PropTypes.object.isRequired,
|
||||
};
|
||||
@@ -1,55 +1,57 @@
|
||||
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);
|
||||
});
|
||||
/**
|
||||
* 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');
|
||||
});
|
||||
};
|
||||
|
||||
@@ -1,206 +1,259 @@
|
||||
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 (?, ?, ?, ?)`;
|
||||
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, filter) =>
|
||||
`SELECT * FROM chapters WHERE novelId = ? ${filter || ""} ${
|
||||
sort || "ORDER BY chapterId ASC"
|
||||
}`;
|
||||
const getChaptersQuery = (sort = 'ORDER BY chapterId ASC', filter = '') =>
|
||||
`SELECT * FROM chapters WHERE novelId = ? ${filter} ${sort}`;
|
||||
|
||||
export const getChapters = (novelId, sort, filter) => {
|
||||
// console.log(getChaptersQuery(sort, filter));
|
||||
return new Promise((resolve, reject) =>
|
||||
db.transaction((tx) => {
|
||||
tx.executeSql(
|
||||
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 = ?`;
|
||||
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],
|
||||
(txObj, 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],
|
||||
(txObj, 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],
|
||||
(txObj, 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],
|
||||
(txObj, res) => {},
|
||||
(txObj, error) => {
|
||||
// console.log(error)
|
||||
},
|
||||
);
|
||||
});
|
||||
};
|
||||
|
||||
const markChapterReadQuery = `UPDATE chapters SET \`read\` = 1 WHERE chapterId = ?`;
|
||||
const markChapterUnreadQuery =
|
||||
'UPDATE chapters SET `read` = 0 WHERE chapterId = ?';
|
||||
|
||||
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)
|
||||
);
|
||||
});
|
||||
};
|
||||
|
||||
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 markChapterUnread = async chapterId => {
|
||||
db.transaction(tx => {
|
||||
tx.executeSql(
|
||||
markChapterUnreadQuery,
|
||||
[chapterId],
|
||||
(txObj, res) => {},
|
||||
(txObj, error) => {
|
||||
// console.log(error)
|
||||
},
|
||||
);
|
||||
});
|
||||
};
|
||||
|
||||
const downloadChapterQuery = `INSERT INTO downloads (downloadChapterId, chapterName, chapterText, prevChapter, nextChapter) VALUES (?, ?, ?, ?, ?)`;
|
||||
const markAllChaptersReadQuery =
|
||||
'UPDATE chapters SET `read` = 1 WHERE novelId = ?';
|
||||
|
||||
export const markAllChaptersRead = async novelId => {
|
||||
db.transaction(tx => {
|
||||
tx.executeSql(
|
||||
markAllChaptersReadQuery,
|
||||
[novelId],
|
||||
(txObj, res) => {},
|
||||
(txObj, 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],
|
||||
(txObj, res) => {},
|
||||
(txObj, 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)
|
||||
},
|
||||
);
|
||||
}),
|
||||
);
|
||||
};
|
||||
|
||||
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,
|
||||
chapter.prevChapter,
|
||||
chapter.nextChapter,
|
||||
],
|
||||
(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],
|
||||
(txObj, 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],
|
||||
(txObj, res) => {},
|
||||
(txObj, error) => {
|
||||
// console.log('Error ', error)
|
||||
},
|
||||
);
|
||||
tx.executeSql(
|
||||
deleteChapterQuery,
|
||||
[chapterId],
|
||||
(txObj, res) => {
|
||||
// console.log('Chapter deleted');
|
||||
},
|
||||
(txObj, error) => {
|
||||
// console.log('Error ', error)
|
||||
},
|
||||
);
|
||||
});
|
||||
};
|
||||
|
||||
const getLastReadChapterQuery = `
|
||||
@@ -211,54 +264,96 @@ 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],
|
||||
(txObj, {rows}) => resolve(rows.item(0)),
|
||||
(txObj, error) => {
|
||||
// console.log('Error ', error)
|
||||
},
|
||||
);
|
||||
});
|
||||
});
|
||||
};
|
||||
|
||||
const bookmarkChapterQuery = `UPDATE chapters SET bookmark = ? WHERE chapterId = ?`;
|
||||
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],
|
||||
(txObj, res) => {},
|
||||
(txObj, error) => {
|
||||
// console.log('Error ', error)
|
||||
},
|
||||
);
|
||||
});
|
||||
};
|
||||
|
||||
const markPreviuschaptersReadQuery = `UPDATE chapters SET \`read\` = 1 WHERE chapterId < ? AND novelId = ?`;
|
||||
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],
|
||||
(txObj, res) => {},
|
||||
(txObj, error) => {
|
||||
// console.log(error)
|
||||
},
|
||||
);
|
||||
});
|
||||
};
|
||||
|
||||
const markPreviousChaptersUnreadQuery = `UPDATE chapters SET \`read\` = 0 WHERE chapterId < ? AND novelId = ?`;
|
||||
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],
|
||||
(txObj, res) => {},
|
||||
(txObj, error) => {
|
||||
// console.log(error)
|
||||
},
|
||||
);
|
||||
});
|
||||
};
|
||||
|
||||
const getDownloadedChaptersQuery = `
|
||||
SELECT chapters.*, novels.sourceId, novels.novelName, novels.novelCover, novels.novelUrl
|
||||
FROM chapters
|
||||
JOIN novels
|
||||
ON chapters.novelId = novels.novelId
|
||||
WHERE chapters.downloaded = 1`;
|
||||
|
||||
export const getDownloadedChapters = () => {
|
||||
return new Promise((resolve, reject) =>
|
||||
db.transaction(tx => {
|
||||
tx.executeSql(
|
||||
getDownloadedChaptersQuery,
|
||||
null,
|
||||
(txObj, {rows: {_array}}) => {
|
||||
resolve(_array);
|
||||
},
|
||||
(txObj, error) => {
|
||||
// console.log('Error ', error)
|
||||
},
|
||||
);
|
||||
}),
|
||||
);
|
||||
};
|
||||
|
||||
export const deleteDownloads = async () => {
|
||||
db.transaction(tx => {
|
||||
tx.executeSql('UPDATE chapters SET downloaded = 0');
|
||||
tx.executeSql('DELETE FROM downloads; VACCUM;');
|
||||
});
|
||||
};
|
||||
|
||||
@@ -1,5 +1,7 @@
|
||||
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 {showToast} from '../../hooks/showToast';
|
||||
|
||||
const getHistoryQuery = `
|
||||
SELECT history.*, chapters.*, novels.*
|
||||
@@ -13,50 +15,55 @@ const getHistoryQuery = `
|
||||
ORDER BY history.historyTimeRead DESC
|
||||
`;
|
||||
|
||||
export const getHistory = async () => {
|
||||
return new Promise((resolve, reject) => {
|
||||
db.transaction((tx) => {
|
||||
tx.executeSql(
|
||||
getHistoryQuery,
|
||||
null,
|
||||
(txObj, { rows: { _array } }) => {
|
||||
resolve(_array);
|
||||
},
|
||||
(txObj, error) => console.log("Error ", error)
|
||||
);
|
||||
});
|
||||
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)
|
||||
},
|
||||
);
|
||||
});
|
||||
});
|
||||
};
|
||||
|
||||
// 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;');
|
||||
});
|
||||
showToast('History deleted.');
|
||||
};
|
||||
|
||||
@@ -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,30 @@ 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 +59,21 @@ 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);
|
||||
},
|
||||
);
|
||||
});
|
||||
});
|
||||
};
|
||||
|
||||
@@ -1,159 +1,261 @@
|
||||
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 { fetchChapters, fetchNovel } from "../../services/Source/source";
|
||||
import { insertChapters } from "./ChapterQueries";
|
||||
import BackgroundService from 'react-native-background-actions';
|
||||
import * as DocumentPicker from 'expo-document-picker';
|
||||
|
||||
const insertNovelQuery = `INSERT INTO novels (novelUrl, sourceUrl, sourceId, source, novelName, novelCover, novelSummary, author, artist, status, genre) VALUES(?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`;
|
||||
import {fetchChapters, fetchNovel} from '../../services/Source/source';
|
||||
import {insertChapters} from './ChapterQueries';
|
||||
|
||||
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)
|
||||
)
|
||||
)
|
||||
);
|
||||
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 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],
|
||||
(txo, 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 unfollowNovel = async novelId => {
|
||||
db.transaction(tx => {
|
||||
tx.executeSql(
|
||||
'UPDATE novels SET followed = 0 WHERE novelId = ?',
|
||||
[novelId],
|
||||
(txo, 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 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) => {
|
||||
console.log("Cleared Novel Cache");
|
||||
},
|
||||
(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)
|
||||
)
|
||||
)
|
||||
db.transaction(tx => {
|
||||
tx.executeSql(
|
||||
'DELETE FROM novels WHERE followed = 0',
|
||||
null,
|
||||
(txObj, res) => showToast('Entries deleted'),
|
||||
(txObj, error) => {
|
||||
// console.log('Error ', error)
|
||||
},
|
||||
);
|
||||
});
|
||||
};
|
||||
|
||||
const migrateNovelQuery = `INSERT INTO novels (novelUrl, sourceUrl, sourceId, source, novelName, novelCover, novelSummary, author, artist, status, genre, followed) VALUES(?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`;
|
||||
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)
|
||||
},
|
||||
),
|
||||
),
|
||||
);
|
||||
};
|
||||
|
||||
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);
|
||||
|
||||
db.transaction((tx) =>
|
||||
tx.executeSql(
|
||||
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,
|
||||
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);
|
||||
async (txObj, {insertId}) => {
|
||||
const chapters = await fetchChapters(
|
||||
novel.sourceId,
|
||||
novel.novelUrl,
|
||||
);
|
||||
await insertChapters(insertId, chapters);
|
||||
resolve();
|
||||
},
|
||||
(txObj, error) => console.log("Error ", error)
|
||||
)
|
||||
);
|
||||
(txObj, error) => {
|
||||
// console.log('Error ', error)
|
||||
},
|
||||
),
|
||||
);
|
||||
});
|
||||
};
|
||||
|
||||
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)
|
||||
},
|
||||
);
|
||||
});
|
||||
};
|
||||
|
||||
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;
|
||||
}
|
||||
};
|
||||
|
||||
@@ -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
|
||||
@@ -8,31 +8,39 @@ const getUpdatesQuery = `
|
||||
ON updates.chapterId = chapters.chapterId
|
||||
JOIN novels
|
||||
ON updates.novelId = novels.novelId
|
||||
WHERE date(updates.updateTime) > date('now','-3 months')
|
||||
ORDER BY updates.updateTime DESC
|
||||
`;
|
||||
|
||||
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;'));
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
export const createChapterTableQuery = `
|
||||
const createChapterTableQuery = `
|
||||
CREATE TABLE IF NOT EXISTS chapters(
|
||||
chapterId INTEGER PRIMARY KEY AUTOINCREMENT,
|
||||
chapterUrl TEXT,
|
||||
@@ -13,6 +13,18 @@ export const createChapterTableQuery = `
|
||||
ON DELETE CASCADE
|
||||
)`;
|
||||
|
||||
export const createNovelIdIndexQuery = `CREATE INDEX IF NOT EXISTS chapterNovelIdIndex ON chapters(novelId)`;
|
||||
const createNovelIdIndexQuery = `
|
||||
CREATE INDEX
|
||||
IF NOT EXISTS
|
||||
chapterNovelIdIndex ON chapters(novelId)`;
|
||||
|
||||
export const createUnreadChaptersIndexQuery = `CREATE INDEX IF NOT EXISTS chapterUnreadByNovelIndex ON chapters(novelId, \`read\`) WHERE \`read\`=0`;
|
||||
const createUnreadChaptersIndexQuery = `
|
||||
CREATE INDEX
|
||||
IF NOT EXISTS
|
||||
chapterUnreadByNovelIndex ON chapters(novelId, \`read\`) WHERE \`read\`=0`;
|
||||
|
||||
export {
|
||||
createChapterTableQuery,
|
||||
createNovelIdIndexQuery,
|
||||
createUnreadChaptersIndexQuery,
|
||||
};
|
||||
|
||||
@@ -4,8 +4,6 @@ export const createDownloadTableQuery = `
|
||||
downloadChapterId INTEGER NOT NULL,
|
||||
chapterName TEXT,
|
||||
chapterText TEXT,
|
||||
nextChapter TEXT,
|
||||
prevChapter TEXT,
|
||||
FOREIGN KEY (downloadChapterId) REFERENCES chapters(chapterId)
|
||||
ON DELETE CASCADE
|
||||
)`;
|
||||
|
||||
@@ -8,4 +8,5 @@ export const createHistoryTableQuery = `
|
||||
ON DELETE CASCADE
|
||||
)`;
|
||||
|
||||
export const createChapterIdIndexQuery = `CREATE INDEX IF NOT EXISTS historyChapterIdIndex ON history(historyChapterId)`;
|
||||
export const createChapterIdIndexQuery =
|
||||
'CREATE INDEX IF NOT EXISTS historyChapterIdIndex ON history(historyChapterId)';
|
||||
|
||||
@@ -16,6 +16,8 @@ export const createNovelTableQuery = `
|
||||
unread INTEGER DEFAULT 1
|
||||
)`;
|
||||
|
||||
export const createUrlIndexQuery = `CREATE INDEX IF NOT EXISTS novelUrlIndex ON novels(novelUrl)`;
|
||||
export const createUrlIndexQuery =
|
||||
'CREATE INDEX IF NOT EXISTS novelUrlIndex ON novels(novelUrl)';
|
||||
|
||||
export const createLibraryIndexQuery = `CREATE INDEX IF NOT EXISTS novelFollowedIndex ON novels(followed)`;
|
||||
export const createLibraryIndexQuery =
|
||||
'CREATE INDEX IF NOT EXISTS novelFollowedIndex ON novels(followed)';
|
||||
|
||||
@@ -1,46 +1,52 @@
|
||||
import { useState, useEffect } from "react";
|
||||
import {useState, useEffect} from 'react';
|
||||
|
||||
export const githubUpdateChecker = () => {
|
||||
const latestReleaseUrl =
|
||||
"https://api.github.com/repos/rajarsheechatterjee/lnreader/releases/latest";
|
||||
import {showToast} from './../hooks/showToast';
|
||||
|
||||
const [checking, setChecking] = useState(true);
|
||||
const [latestRelease, setLatestRelease] = useState();
|
||||
export const useGithubUpdateChecker = () => {
|
||||
const latestReleaseUrl =
|
||||
'https://api.github.com/repos/rajarsheechatterjee/lnreader/releases/latest';
|
||||
|
||||
const checkForRelease = async () => {
|
||||
let res = await fetch(latestReleaseUrl);
|
||||
res = await res.json();
|
||||
const [checking, setChecking] = useState(true);
|
||||
const [latestRelease, setLatestRelease] = useState();
|
||||
|
||||
const release = {
|
||||
tag_name: res.tag_name,
|
||||
body: res.body,
|
||||
downloadUrl: res.assets[0].browser_download_url,
|
||||
};
|
||||
const checkForRelease = async () => {
|
||||
let res = await fetch(latestReleaseUrl);
|
||||
res = await res.json();
|
||||
|
||||
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.0";
|
||||
const regex = /[^\\d.]/;
|
||||
setLatestRelease(release);
|
||||
setChecking(false);
|
||||
};
|
||||
|
||||
let newVersion = versionTag.replace(regex, "");
|
||||
currentVersion = currentVersion.replace(regex, "");
|
||||
const isNewVersion = versionTag => {
|
||||
let currentVersion = 'v1.1.7';
|
||||
const regex = /[^\\d.]/;
|
||||
|
||||
return newVersion !== currentVersion;
|
||||
let newVersion = versionTag.replace(regex, '');
|
||||
currentVersion = currentVersion.replace(regex, '');
|
||||
|
||||
return newVersion !== currentVersion;
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
checkForRelease();
|
||||
// .catch(e => {
|
||||
// showToast(`Could not connect to github:\n${`${e}`.split(':')[1].trim()}`);
|
||||
// // console.error(e);
|
||||
// });
|
||||
}, []);
|
||||
|
||||
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;
|
||||
}
|
||||
};
|
||||
|
||||
@@ -1,131 +1,129 @@
|
||||
import { shallowEqual, useSelector } from "react-redux";
|
||||
import { showChapterTitlesAction } from "../redux/novel/novel.actions";
|
||||
import {useSelector} from 'react-redux';
|
||||
import {darkTheme} from '../theme/theme';
|
||||
|
||||
const useTheme = () => {
|
||||
const theme = useSelector((state) => state.settingsReducer.theme);
|
||||
const theme = useSelector(state => state.settingsReducer.theme);
|
||||
|
||||
return theme;
|
||||
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 useFindNovel = 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 = useFindNovel(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 = useFindNovel(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 = useFindNovel(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,
|
||||
};
|
||||
|
||||
@@ -1,10 +0,0 @@
|
||||
import { setStatusBarStyle as setExpoStatusBarStyle } from "expo-status-bar";
|
||||
import { useTheme } from "../hooks/reduxHooks";
|
||||
import changeNavigationBarColor from "react-native-navigation-bar-color";
|
||||
|
||||
export const setStatusBarStyle = () => {
|
||||
const theme = useTheme();
|
||||
|
||||
setExpoStatusBarStyle(theme.id === 1 ? "dark" : "light");
|
||||
changeNavigationBarColor(theme.colorPrimary);
|
||||
};
|
||||
@@ -1,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);
|
||||
};
|
||||
|
||||
@@ -1,35 +0,0 @@
|
||||
import { useState, useEffect } from "react";
|
||||
import { getSource } from "../sources/sources";
|
||||
|
||||
const usePopularNovels = (sourceId) => {
|
||||
const [loading, setLoading] = useState(true);
|
||||
const [pageNumber, setPage] = useState(1);
|
||||
const [novels, setNovels] = useState([]);
|
||||
const [error, setError] = useState();
|
||||
|
||||
const source = getSource(sourceId);
|
||||
|
||||
useEffect(() => {
|
||||
const getNextPage = async (p) => {
|
||||
try {
|
||||
if (p === 1) setLoading(true);
|
||||
const res = await source.popularNovels(p);
|
||||
setHasMoreNovels(res.totalPages > pageNumber);
|
||||
setNovels((novels) => [...novels, ...data.novels]);
|
||||
setLoading(false);
|
||||
} catch (err) {
|
||||
setError(err.message);
|
||||
}
|
||||
};
|
||||
|
||||
getNextPage(pageNumber);
|
||||
}, [pageNumber, sourceId]);
|
||||
|
||||
const incrementPage = () => {
|
||||
setPage((pageNumber) => pageNumber + 1);
|
||||
};
|
||||
|
||||
return { novels, incrementPage, loading, error };
|
||||
};
|
||||
|
||||
export { usePopularNovels };
|
||||
@@ -0,0 +1,10 @@
|
||||
import {useState} from 'react';
|
||||
|
||||
export const useModal = () => {
|
||||
const [visible, setVisible] = useState(false);
|
||||
|
||||
const showModal = () => setVisible(true);
|
||||
const hideModal = () => setVisible(false);
|
||||
|
||||
return {visible, showModal, hideModal};
|
||||
};
|
||||
@@ -1,96 +1,102 @@
|
||||
import React from "react";
|
||||
import { View } from "react-native";
|
||||
import React from 'react';
|
||||
|
||||
import { createMaterialBottomTabNavigator } from "@react-navigation/material-bottom-tabs";
|
||||
import MaterialCommunityIcons from "react-native-vector-icons/MaterialCommunityIcons";
|
||||
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/LibraryScreen';
|
||||
import Updates from '../screens/updates/UpdatesScreen';
|
||||
import History from '../screens/history/HistoryScreen';
|
||||
import Browse from '../screens/browse/BrowseScreen';
|
||||
import More from '../screens/more/MoreScreen';
|
||||
|
||||
import { useTheme } from "../hooks/reduxHooks";
|
||||
import {useSettings, useTheme} from '../hooks/reduxHooks';
|
||||
|
||||
const Tab = createMaterialBottomTabNavigator();
|
||||
|
||||
const BottomNavigator = () => {
|
||||
const theme = useTheme();
|
||||
const theme = useTheme();
|
||||
const {bottom} = useSafeAreaInsets();
|
||||
|
||||
return (
|
||||
<View style={{ flex: 1, backgroundColor: theme.colorPrimaryDark }}>
|
||||
<Tab.Navigator
|
||||
barStyle={{ backgroundColor: theme.colorPrimary }}
|
||||
activeColor={theme.colorAccent}
|
||||
>
|
||||
<Tab.Screen
|
||||
name="Library"
|
||||
component={Library}
|
||||
options={{
|
||||
tabBarIcon: ({ color }) => (
|
||||
<MaterialCommunityIcons
|
||||
name="book-variant-multiple"
|
||||
color={color}
|
||||
size={24}
|
||||
/>
|
||||
),
|
||||
}}
|
||||
/>
|
||||
<Tab.Screen
|
||||
name="Updates"
|
||||
component={Updates}
|
||||
options={{
|
||||
tabBarIcon: ({ color }) => (
|
||||
<MaterialCommunityIcons
|
||||
name="alert-decagram-outline"
|
||||
color={color}
|
||||
size={24}
|
||||
/>
|
||||
),
|
||||
}}
|
||||
/>
|
||||
<Tab.Screen
|
||||
name="History"
|
||||
component={History}
|
||||
options={{
|
||||
tabBarIcon: ({ color }) => (
|
||||
<MaterialCommunityIcons
|
||||
name="history"
|
||||
color={color}
|
||||
size={24}
|
||||
/>
|
||||
),
|
||||
}}
|
||||
/>
|
||||
<Tab.Screen
|
||||
name="Browse"
|
||||
component={Browse}
|
||||
options={{
|
||||
tabBarIcon: ({ color }) => (
|
||||
<MaterialCommunityIcons
|
||||
name="compass-outline"
|
||||
color={color}
|
||||
size={24}
|
||||
/>
|
||||
),
|
||||
}}
|
||||
/>
|
||||
<Tab.Screen
|
||||
name="More"
|
||||
component={More}
|
||||
options={{
|
||||
tabBarIcon: ({ color }) => (
|
||||
<MaterialCommunityIcons
|
||||
name="dots-horizontal"
|
||||
color={color}
|
||||
size={24}
|
||||
/>
|
||||
),
|
||||
}}
|
||||
/>
|
||||
</Tab.Navigator>
|
||||
</View>
|
||||
);
|
||||
const {
|
||||
showHistoryTab = true,
|
||||
showUpdatesTab = true,
|
||||
showLabelsInNav = false,
|
||||
} = useSettings();
|
||||
|
||||
return (
|
||||
<Tab.Navigator
|
||||
barStyle={{backgroundColor: theme.colorPrimary, paddingBottom: 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}
|
||||
/>
|
||||
),
|
||||
}}
|
||||
/>
|
||||
<Tab.Screen
|
||||
name="More"
|
||||
component={More}
|
||||
options={{
|
||||
tabBarIcon: ({color}) => (
|
||||
<MaterialCommunityIcons
|
||||
name="dots-horizontal"
|
||||
color={color}
|
||||
size={24}
|
||||
/>
|
||||
),
|
||||
}}
|
||||
/>
|
||||
</Tab.Navigator>
|
||||
);
|
||||
};
|
||||
|
||||
export default BottomNavigator;
|
||||
|
||||
@@ -1,80 +1,69 @@
|
||||
import React from "react";
|
||||
import {
|
||||
createStackNavigator,
|
||||
TransitionPresets,
|
||||
} from "@react-navigation/stack";
|
||||
import React, {useEffect} from 'react';
|
||||
import {View} from 'react-native';
|
||||
|
||||
import BottomNavigator from "./BottomNavigator";
|
||||
import Novel from "../screens/Novel/Novel";
|
||||
import Chapter from "../screens/Chapter/Chapter";
|
||||
import Extension from "../screens/Extension/Extension";
|
||||
import GlobalSearch from "../screens/Browse/globalsearch/GlobalSearch";
|
||||
import Migration from "../screens/Browse/migration/Migration";
|
||||
import SourceNovels from "../screens/Browse/SourceNovels";
|
||||
import MigrateNovel from "../screens/Browse/migration/MigrationNovels";
|
||||
import MoreStack from "./More";
|
||||
import MalTopNovels from "../screens/Browse/discover/MalTopNovels";
|
||||
import { setStatusBarStyle } from "../hooks/setStatusBarStyle";
|
||||
import { View } from "react-native";
|
||||
import { useTheme } from "../hooks/reduxHooks";
|
||||
import { githubUpdateChecker } from "../hooks/githubUpdateChecker";
|
||||
import NewUpdateDialog from "../components/NewUpdateDialog";
|
||||
import { NavigationContainer } from "@react-navigation/native";
|
||||
import BrowseNovelUpdates from "../screens/Browse/discover/BrowseNovelUpdates";
|
||||
import BrowseSettings from "../screens/Browse/BrowseSettings";
|
||||
import {NavigationContainer} from '@react-navigation/native';
|
||||
import {createStackNavigator} from '@react-navigation/stack';
|
||||
|
||||
import {setBarColor} from '../theme/utils/setBarColor';
|
||||
import {useTheme} from '../hooks/reduxHooks';
|
||||
import {useGithubUpdateChecker} from '../hooks/githubUpdateChecker';
|
||||
import * as SplashScreen from 'expo-splash-screen';
|
||||
|
||||
/**
|
||||
* Navigators
|
||||
*/
|
||||
import BottomNavigator from './BottomNavigator';
|
||||
import MoreStack from './MoreStack';
|
||||
|
||||
/**
|
||||
* Screens
|
||||
*/
|
||||
import Novel from '../screens/novel/NovelScreen';
|
||||
import Reader from '../screens/reader/ReaderScreen';
|
||||
import Extension from '../screens/source/SourceScreen';
|
||||
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 MalTopNovels from '../screens/browse/discover/MalTopNovels';
|
||||
import NewUpdateDialog from '../components/NewUpdateDialog';
|
||||
import BrowseSettings from '../screens/browse/SettingsBrowseScreen';
|
||||
|
||||
const Stack = createStackNavigator();
|
||||
|
||||
const stackNavigatorConfig = {
|
||||
headerShown: false,
|
||||
};
|
||||
|
||||
const MainNavigator = () => {
|
||||
const theme = useTheme();
|
||||
setStatusBarStyle();
|
||||
const { isNewVersion, latestRelease } = githubUpdateChecker() || {};
|
||||
const theme = useTheme();
|
||||
|
||||
return (
|
||||
<NavigationContainer
|
||||
theme={{ colors: { background: theme.colorPrimaryDark } }}
|
||||
>
|
||||
<View style={{ flex: 1, backgroundColor: theme.colorPrimaryDark }}>
|
||||
{isNewVersion && <NewUpdateDialog newVersion={latestRelease} />}
|
||||
<Stack.Navigator screenOptions={stackNavigatorConfig}>
|
||||
<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="BrowseNovelUpdates"
|
||||
component={BrowseNovelUpdates}
|
||||
/>
|
||||
<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>
|
||||
);
|
||||
useEffect(() => {
|
||||
setTimeout(async () => {
|
||||
await SplashScreen.hideAsync();
|
||||
setBarColor(theme);
|
||||
}, 500);
|
||||
}, [theme]);
|
||||
|
||||
const {isNewVersion, latestRelease} = useGithubUpdateChecker() || {};
|
||||
|
||||
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={Reader} />
|
||||
<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;
|
||||
|
||||
@@ -1,66 +0,0 @@
|
||||
import React from "react";
|
||||
import { View } from "react-native";
|
||||
import { useTheme } from "../hooks/reduxHooks";
|
||||
|
||||
import { createStackNavigator } from "@react-navigation/stack";
|
||||
|
||||
import About from "../screens/More/About";
|
||||
import Settings from "../screens/More/Settings";
|
||||
import TrackerSettings from "../screens/More/TrackerSettings";
|
||||
import ReaderSettings from "../screens/More/ReaderSettings";
|
||||
import BackupSettings from "../screens/More/BackupSettings";
|
||||
import AdvancedSettings from "../screens/More/AdvancedSettings";
|
||||
import GeneralSettings from "../screens/More/GeneralSettings";
|
||||
import DownloadQueue from "../screens/More/DownloadQueue";
|
||||
|
||||
const Stack = createStackNavigator();
|
||||
|
||||
const stackNavigatorConfig = { headerShown: false };
|
||||
|
||||
const SettingsStack = () => {
|
||||
const theme = useTheme();
|
||||
|
||||
return (
|
||||
<View style={{ flex: 1, backgroundColor: theme.colorPrimaryDark }}>
|
||||
<Stack.Navigator screenOptions={stackNavigatorConfig}>
|
||||
<Stack.Screen name="Settings" component={Settings} />
|
||||
<Stack.Screen
|
||||
name="GeneralSettings"
|
||||
component={GeneralSettings}
|
||||
/>
|
||||
<Stack.Screen
|
||||
name="ReaderSettings"
|
||||
component={ReaderSettings}
|
||||
/>
|
||||
<Stack.Screen
|
||||
name="TrackerSettings"
|
||||
component={TrackerSettings}
|
||||
/>
|
||||
<Stack.Screen
|
||||
name="BackupSettings"
|
||||
component={BackupSettings}
|
||||
/>
|
||||
<Stack.Screen
|
||||
name="AdvancedSettings"
|
||||
component={AdvancedSettings}
|
||||
/>
|
||||
</Stack.Navigator>
|
||||
</View>
|
||||
);
|
||||
};
|
||||
|
||||
const MoreStack = () => {
|
||||
const theme = useTheme();
|
||||
|
||||
return (
|
||||
<View style={{ flex: 1, backgroundColor: theme.colorPrimaryDark }}>
|
||||
<Stack.Navigator screenOptions={stackNavigatorConfig}>
|
||||
<Stack.Screen name="SettingsStack" component={SettingsStack} />
|
||||
<Stack.Screen name="About" component={About} />
|
||||
<Stack.Screen name="DownloadQueue" component={DownloadQueue} />
|
||||
</Stack.Navigator>
|
||||
</View>
|
||||
);
|
||||
};
|
||||
|
||||
export default MoreStack;
|
||||
@@ -0,0 +1,42 @@
|
||||
import React from 'react';
|
||||
|
||||
import {createStackNavigator} from '@react-navigation/stack';
|
||||
|
||||
// Screens
|
||||
import About from '../screens/more/About';
|
||||
import Settings from '../screens/setting/SettingsScreen';
|
||||
import TrackerSettings from '../screens/setting/SettingsTrackerScreen';
|
||||
import ReaderSettings from '../screens/setting/SettingsReaderScreen';
|
||||
import BackupSettings from '../screens/setting/SettingsBackupScreen';
|
||||
import AdvancedSettings from '../screens/setting/SettingsAdvancedScreen';
|
||||
import GeneralSettings from '../screens/setting/SettingsGeneralScreen';
|
||||
import DownloadQueue from '../screens/more/DownloadQueueScreen';
|
||||
import Downloads from '../screens/more/DownloadsScreen';
|
||||
import AppearanceSettings from '../screens/setting/SettingsAppearanceScreen';
|
||||
|
||||
const Stack = createStackNavigator();
|
||||
|
||||
const stackNavigatorConfig = {headerShown: false};
|
||||
|
||||
const SettingsStack = () => (
|
||||
<Stack.Navigator screenOptions={stackNavigatorConfig}>
|
||||
<Stack.Screen name="Settings" component={Settings} />
|
||||
<Stack.Screen name="GeneralSettings" component={GeneralSettings} />
|
||||
<Stack.Screen name="ReaderSettings" component={ReaderSettings} />
|
||||
<Stack.Screen name="TrackerSettings" component={TrackerSettings} />
|
||||
<Stack.Screen name="BackupSettings" component={BackupSettings} />
|
||||
<Stack.Screen name="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>
|
||||
);
|
||||
|
||||
export default MoreStack;
|
||||