Compare commits
104 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| da18cd9ade | |||
| 802aa6d71a | |||
| 3fbca6c14d | |||
| 5a07255b95 | |||
| 9ed1d5c8b6 | |||
| f07a4845b8 | |||
| 922ffbe7b0 | |||
| d9b40589b7 | |||
| aee3126e6c | |||
| ae16cc70f5 | |||
| bc3cbd7d7b | |||
| c463a99e9a | |||
| 1c2dce3a44 | |||
| d484380734 | |||
| 7f2d3ac9b7 | |||
| 410ff3d727 | |||
| b8824a9bd3 | |||
| 74be58ee81 | |||
| 6ef45e8b39 | |||
| f42f316748 | |||
| b9052e6d38 | |||
| acfb73c424 | |||
| 1c3ea20aaf | |||
| 815172b91d | |||
| 2d85fa0938 | |||
| 5e2152ad0d | |||
| 0388ec4f7e | |||
| 6f10966714 | |||
| d2c0a56c35 | |||
| 3dbb835045 | |||
| 38c062c195 | |||
| 969c17e68d | |||
| c3a6eb372f | |||
| 09bcbd58f5 | |||
| fd642c9035 | |||
| 642b8a855d | |||
| 54a6ec64bf | |||
| b97a3cf16d | |||
| ae8c858ae2 | |||
| 9f4b849d34 | |||
| be82f3d9ae | |||
| 027f4420f6 | |||
| e7b8edf8f1 | |||
| 9266b57560 | |||
| 4214caa87c | |||
| 766c7b565e | |||
| 06d66c15d4 | |||
| e2d58d1587 | |||
| 1f86979165 | |||
| 951690d49a | |||
| f4be4aa4e1 | |||
| 1adb5a9ce3 | |||
| fdef214dcf | |||
| e99bf2a430 | |||
| 471e7f56e7 | |||
| 1f527a653d | |||
| c4240e6c1a | |||
| 0bc936becf | |||
| f4643b0a4e | |||
| ac42c025c2 | |||
| 20cfc960f8 | |||
| de357c3822 | |||
| 5e73b32b97 | |||
| d289b0ea17 | |||
| 20d3a786b6 | |||
| ab2a17b41e | |||
| e76bc77128 | |||
| 783d1e1873 | |||
| c307781cf9 | |||
| 10fb3ec347 | |||
| 71eab75048 | |||
| d6b24a756d | |||
| a779cc12d0 | |||
| 5c64314d7d | |||
| 0b41728789 | |||
| 388052b59d | |||
| edcb661b86 | |||
| a248c9e961 | |||
| 0e49e873cd | |||
| 9137d09743 | |||
| 4d2b275a27 | |||
| c06c80c40f | |||
| e258d5c1a9 | |||
| de94340616 | |||
| 8e1820213f | |||
| 36b9e2cf69 | |||
| 04928e8cdc | |||
| 69e269216a | |||
| 5f3959d62d | |||
| 2c02437e62 | |||
| 4255e5d060 | |||
| a1d32a9e1d | |||
| 93a7a00735 | |||
| cb875b5d71 | |||
| ce0fa93959 | |||
| 1537b67f12 | |||
| 3a1921b7b1 | |||
| 87375bc005 | |||
| 13f4b08b7e | |||
| 4321b892ad | |||
| b258f9cf13 | |||
| 02eb632484 | |||
| f6faaea0db | |||
| 046b020f26 |
+15
-4
@@ -1,8 +1,19 @@
|
||||
module.exports = {
|
||||
root: true,
|
||||
extends: '@react-native-community',
|
||||
rules: {
|
||||
'react-native/no-inline-styles': 'off',
|
||||
'no-console': 'warn',
|
||||
},
|
||||
parser: '@typescript-eslint/parser',
|
||||
plugins: ['@typescript-eslint'],
|
||||
overrides: [
|
||||
{
|
||||
files: ['*.js', '*.jsx', '*.ts', '*.tsx'],
|
||||
rules: {
|
||||
'@typescript-eslint/no-shadow': ['warn'],
|
||||
'no-shadow': 'off',
|
||||
'no-undef': 'off',
|
||||
'react-native/no-inline-styles': 'warn',
|
||||
'no-console': 'warn',
|
||||
'react-hooks/exhaustive-deps': 'warn',
|
||||
},
|
||||
},
|
||||
],
|
||||
};
|
||||
|
||||
+28
-38
@@ -1,45 +1,35 @@
|
||||
name: CI
|
||||
on:
|
||||
push:
|
||||
branches:
|
||||
- main
|
||||
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
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v2
|
||||
- name: Install 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 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: 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: Make Gradlew Executable
|
||||
run: cd android && chmod +x ./gradlew
|
||||
|
||||
- name: Build Android Release
|
||||
run: |
|
||||
cd android && ./gradlew assembleRelease --no-daemon
|
||||
- name: Build Android Release
|
||||
run: |
|
||||
cd android && ./gradlew assembleRelease --no-daemon
|
||||
|
||||
@@ -5,8 +5,8 @@ jobs:
|
||||
name: ESLint
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v2
|
||||
- name: Install modules
|
||||
run: npm i
|
||||
- name: Run ESLint
|
||||
run: npm run lint
|
||||
- uses: actions/checkout@v2
|
||||
- name: Install modules
|
||||
run: npm i
|
||||
- name: Run ESLint
|
||||
run: npm run lint
|
||||
|
||||
+2
-1
@@ -72,4 +72,5 @@ web-build/
|
||||
|
||||
# @end expo-cli
|
||||
|
||||
.vscode/
|
||||
.vscode/
|
||||
src/sources/en/generators/
|
||||
@@ -0,0 +1 @@
|
||||
_
|
||||
@@ -0,0 +1,4 @@
|
||||
#!/bin/sh
|
||||
. "$(dirname "$0")/_/husky.sh"
|
||||
|
||||
npx lint-staged
|
||||
+5
-1
@@ -1,6 +1,10 @@
|
||||
module.exports = {
|
||||
bracketSpacing: false,
|
||||
tabWidth: 2,
|
||||
bracketSpacing: true,
|
||||
useTabs: false,
|
||||
bracketSameLine: false,
|
||||
singleQuote: true,
|
||||
trailingComma: 'all',
|
||||
arrowParens: 'avoid',
|
||||
quoteProps: 'preserve',
|
||||
};
|
||||
|
||||
@@ -1,35 +1,27 @@
|
||||
import {enableFreeze} from 'react-native-screens';
|
||||
import { enableFreeze } from 'react-native-screens';
|
||||
|
||||
enableFreeze(true);
|
||||
|
||||
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 { 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 {createDB} from './src/database/DBHelper';
|
||||
import {fonts} from './src/theme/fonts';
|
||||
import {SafeAreaProvider} from 'react-native-safe-area-context';
|
||||
import { createDB } from './src/database/db';
|
||||
import { SafeAreaProvider } from 'react-native-safe-area-context';
|
||||
|
||||
const App = () => {
|
||||
const [loaded] = useFonts(fonts);
|
||||
|
||||
useEffect(() => {
|
||||
SplashScreen.preventAutoHideAsync();
|
||||
createDB();
|
||||
}, []);
|
||||
|
||||
if (!loaded) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return (
|
||||
<Provider store={store}>
|
||||
<PersistGate loading={null} persistor={persistor}>
|
||||
|
||||
+11
-11
@@ -17,23 +17,23 @@ diverse, inclusive, and healthy community.
|
||||
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,
|
||||
- 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
|
||||
- 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
|
||||
- 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
|
||||
- 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
|
||||
- Other conduct which could reasonably be considered inappropriate in a
|
||||
professional setting
|
||||
|
||||
## Enforcement Responsibilities
|
||||
@@ -106,7 +106,7 @@ 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
|
||||
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
|
||||
|
||||
@@ -2,7 +2,6 @@
|
||||
|
||||
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:
|
||||
|
||||
@@ -17,6 +17,10 @@
|
||||
<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-preview/releases/latest">
|
||||
<img alt="GitHub release (latest SemVer)" src="https://img.shields.io/github/v/release/LNReader/lnreader-preview?color=red&label=preview">
|
||||
</a>
|
||||
<a title="Crowdin" target="_blank" href="https://crowdin.com/project/lnreader"><img src="https://badges.crowdin.net/lnreader/localized.svg"></a>
|
||||
<a href="https://github.com/LNReader/lnreader/blob/main/LICENSE">
|
||||
<img alt="GitHub" src="https://img.shields.io/github/license/LNReader/lnreader">
|
||||
</a>
|
||||
@@ -30,12 +34,15 @@
|
||||
</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.7"
|
||||
versionName "1.1.9"
|
||||
}
|
||||
splits {
|
||||
abi {
|
||||
|
||||
Binary file not shown.
+1
@@ -17,6 +17,7 @@ public class BasePackageList {
|
||||
new expo.modules.imageloader.ImageLoaderPackage(),
|
||||
new expo.modules.keepawake.KeepAwakePackage(),
|
||||
new expo.modules.lineargradient.LinearGradientPackage(),
|
||||
new expo.modules.localization.LocalizationPackage(),
|
||||
new expo.modules.notifications.NotificationsPackage(),
|
||||
new expo.modules.permissions.PermissionsPackage(),
|
||||
new expo.modules.splashscreen.SplashScreenPackage(),
|
||||
|
||||
@@ -0,0 +1,4 @@
|
||||
pull_request_title: 'chore: Update translations'
|
||||
files:
|
||||
- source: /strings/languages/en/strings.json
|
||||
translation: /strings/languages/%two_letters_code%/strings.json
|
||||
@@ -1,5 +1,5 @@
|
||||
import 'react-native-gesture-handler';
|
||||
import {registerRootComponent} from 'expo';
|
||||
import { registerRootComponent } from 'expo';
|
||||
|
||||
import App from './App';
|
||||
|
||||
|
||||
@@ -0,0 +1,4 @@
|
||||
module.exports = {
|
||||
preset: 'react-native',
|
||||
moduleFileExtensions: ['ts', 'tsx', 'js', 'jsx', 'json', 'node'],
|
||||
};
|
||||
+1
-1
@@ -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);
|
||||
|
||||
Generated
+5525
-745
File diff suppressed because it is too large
Load Diff
+41
-8
@@ -1,19 +1,30 @@
|
||||
{
|
||||
"name": "lnreader",
|
||||
"version": "1.1.9",
|
||||
"private": true,
|
||||
"scripts": {
|
||||
"android": "react-native run-android",
|
||||
"ios": "react-native run-ios",
|
||||
"start": "react-native start",
|
||||
"lint": "eslint ."
|
||||
"lint": "eslint . --ext .js,.jsx,.ts,.tsx",
|
||||
"prettier": "prettier --write \"./src/**/*.{js,jsx,ts,tsx}\"",
|
||||
"buildRelease": "cd android && gradlew clean && gradlew assembleRelease",
|
||||
"sources:madara": "node scripts/generateMadaraSource.cjs && prettier --write {src/sources/multisrc/madara/MadaraGenerator.ts,src/sources/sources.json}",
|
||||
"source:generator": "node scripts/generateSource.cjs",
|
||||
"strings": "node scripts/stringTypes.cjs",
|
||||
"prepare": "husky install"
|
||||
},
|
||||
"dependencies": {
|
||||
"@react-native-async-storage/async-storage": "^1.15.11",
|
||||
"@react-native-community/masked-view": "^0.1.11",
|
||||
"@react-native-community/netinfo": "^7.1.1",
|
||||
"@react-native-community/slider": "^4.1.12",
|
||||
"@react-navigation/bottom-tabs": "^6.0.9",
|
||||
"@react-navigation/material-bottom-tabs": "^6.0.9",
|
||||
"@react-navigation/native": "^6.0.6",
|
||||
"@react-navigation/stack": "^6.0.11",
|
||||
"@reduxjs/toolkit": "^1.7.2",
|
||||
"@types/react-native-vector-icons": "^6.4.10",
|
||||
"cheerio": "^1.0.0-rc.10",
|
||||
"expo": "^41.0.1",
|
||||
"expo-document-picker": "^9.2.4",
|
||||
"expo-file-system": "~11.0.2",
|
||||
@@ -22,11 +33,14 @@
|
||||
"expo-keep-awake": "~9.1.2",
|
||||
"expo-linear-gradient": "~9.1.0",
|
||||
"expo-linking": "~2.2.3",
|
||||
"expo-localization": "~10.1.0",
|
||||
"expo-notifications": "~0.11.6",
|
||||
"expo-splash-screen": "^0.11.4",
|
||||
"expo-sqlite": "~9.1.0",
|
||||
"expo-updates": "~0.5.4",
|
||||
"expo-web-browser": "~9.1.0",
|
||||
"i18n-js": "^3.8.0",
|
||||
"lodash": "^4.17.21",
|
||||
"moment": "^2.29.1",
|
||||
"prop-types": "^15.7.2",
|
||||
"qs": "^6.10.1",
|
||||
@@ -34,7 +48,6 @@
|
||||
"react-dom": "16.13.1",
|
||||
"react-native": "^0.64.3",
|
||||
"react-native-background-actions": "^2.6.5",
|
||||
"react-native-cheerio": "^1.0.0-rc.4",
|
||||
"react-native-device-info": "^8.4.7",
|
||||
"react-native-easing-gradient": "^1.0.0",
|
||||
"react-native-fast-image": "^8.5.11",
|
||||
@@ -42,8 +55,10 @@
|
||||
"react-native-pager-view": "^5.4.9",
|
||||
"react-native-paper": "^4.10.1",
|
||||
"react-native-reanimated": "^2.2.4",
|
||||
"react-native-render-html": "^6.3.4",
|
||||
"react-native-safe-area-context": "^3.2.0",
|
||||
"react-native-screens": "^3.9.0",
|
||||
"react-native-skeleton-placeholder": "^5.0.0",
|
||||
"react-native-swipe-gestures": "^1.0.5",
|
||||
"react-native-tab-view": "^3.1.1",
|
||||
"react-native-tts": "^4.1.0",
|
||||
@@ -55,15 +70,33 @@
|
||||
"redux": "^4.1.2",
|
||||
"redux-persist": "^6.0.0",
|
||||
"redux-thunk": "^2.4.0",
|
||||
"rn-sliding-up-panel": "^2.4.4"
|
||||
"rn-sliding-up-panel": "^2.4.4",
|
||||
"sanitize-html": "^2.7.0"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@babel/core": "^7.16.0",
|
||||
"@react-native-community/eslint-config": "^3.0.1",
|
||||
"@types/i18n-js": "^3.8.2",
|
||||
"@types/jest": "^27.4.0",
|
||||
"@types/lodash": "^4.14.179",
|
||||
"@types/react-native": "^0.66.15",
|
||||
"@types/react-test-renderer": "^17.0.1",
|
||||
"@types/sanitize-html": "^2.6.2",
|
||||
"@typescript-eslint/eslint-plugin": "^5.10.2",
|
||||
"@typescript-eslint/parser": "^5.10.2",
|
||||
"eslint": "^7.32.0",
|
||||
"eslint-plugin-react-native": "^3.11.0"
|
||||
"eslint-plugin-react-native": "^3.11.0",
|
||||
"husky": "^7.0.4",
|
||||
"lint-staged": "^12.3.7",
|
||||
"typescript": "^4.5.5"
|
||||
},
|
||||
"private": true,
|
||||
"name": "lnreader",
|
||||
"version": "1.1.6"
|
||||
"resolutions": {
|
||||
"@types/react": "^17"
|
||||
},
|
||||
"lint-staged": {
|
||||
"*.{js,jsx,ts,tsx}": [
|
||||
"eslint --fix",
|
||||
"prettier --check"
|
||||
]
|
||||
}
|
||||
}
|
||||
|
||||
Vendored
+7
@@ -0,0 +1,7 @@
|
||||
module.exports = {
|
||||
project: {
|
||||
ios: {},
|
||||
android: {},
|
||||
},
|
||||
assets: ['./assets/fonts/'],
|
||||
};
|
||||
@@ -0,0 +1,60 @@
|
||||
const path = require('path');
|
||||
const fs = require('fs');
|
||||
|
||||
const MadaraSources = require('../src/sources/multisrc/madara/MadaraSources.json');
|
||||
const AllSources = require('../src/sources/sources.json');
|
||||
|
||||
const getScraperName = scraperName => {
|
||||
let name = scraperName.replace(/\./g, 'Dot').replace('1st', 'First');
|
||||
|
||||
return name;
|
||||
};
|
||||
|
||||
let content =
|
||||
"/**\n * This file is auto-generated\n */\n\nimport MadaraScraper from './MadaraScraper';\n\n";
|
||||
|
||||
MadaraSources.forEach(source => {
|
||||
content =
|
||||
content +
|
||||
`export const ${getScraperName(
|
||||
source.sourceName,
|
||||
)}Scraper = new MadaraScraper(${source.sourceId}, "${source.baseUrl}", "${
|
||||
source.sourceName
|
||||
}", ${source.options ? JSON.stringify(source.options) : ''});\n\n`;
|
||||
});
|
||||
|
||||
fs.writeFile(
|
||||
path.resolve(process.cwd(), 'src/sources/multisrc/madara/MadaraGenerator.ts'),
|
||||
content,
|
||||
error => {
|
||||
if (error) {
|
||||
console.log(error);
|
||||
}
|
||||
},
|
||||
);
|
||||
|
||||
MadaraSources.forEach(madaraSource => {
|
||||
if (!AllSources.some(source => source.sourceId === madaraSource.sourceId)) {
|
||||
let allSourcesContent = AllSources;
|
||||
|
||||
allSourcesContent.push({
|
||||
sourceId: madaraSource.sourceId,
|
||||
sourceName: madaraSource.sourceName,
|
||||
icon: `https://github.com/LNReader/lnreader-sources/blob/main/multisrc/madara/icons/${madaraSource.sourceName.toLowerCase()}.png?raw=true`,
|
||||
url: madaraSource.baseUrl,
|
||||
lang: madaraSource.options.lang || 'English',
|
||||
});
|
||||
|
||||
allSourcesContent = JSON.stringify(allSourcesContent);
|
||||
|
||||
fs.writeFile(
|
||||
path.resolve(process.cwd(), 'src/sources/sources.json'),
|
||||
allSourcesContent,
|
||||
error => {
|
||||
if (error) {
|
||||
console.log(error);
|
||||
}
|
||||
},
|
||||
);
|
||||
}
|
||||
});
|
||||
@@ -0,0 +1,328 @@
|
||||
/**
|
||||
* This script can be used to generate sources
|
||||
*/
|
||||
|
||||
const fs = require('fs');
|
||||
const path = require('path');
|
||||
const prettier = require('prettier');
|
||||
const prettierConfig = require('../.prettierrc');
|
||||
|
||||
const AllSources = require('../src/sources/sources.json');
|
||||
|
||||
const getLanguageCode = language => {
|
||||
const codes = {
|
||||
English: 'en',
|
||||
Arabic: 'ar',
|
||||
French: 'fr',
|
||||
Turkish: 'tr',
|
||||
Chinese: 'cn',
|
||||
Japanese: 'jp',
|
||||
Vietnamese: 'vi',
|
||||
};
|
||||
|
||||
return codes[language];
|
||||
};
|
||||
|
||||
try {
|
||||
if (process.argv[2]) {
|
||||
const source = JSON.parse(
|
||||
fs.readFileSync(path.resolve(process.cwd(), process.argv[2]), 'utf8'),
|
||||
);
|
||||
|
||||
const isDuplicate = AllSources.some(
|
||||
item => item.sourceId === source.sourceId,
|
||||
);
|
||||
|
||||
const content = `
|
||||
import * as cheerio from 'cheerio';
|
||||
import { isUrlAbsolute } from '../../utils/isAbsoluteUrl';
|
||||
${
|
||||
source?.options?.imports?.toaster
|
||||
? "import { showToast } from '../../hooks/showToast'"
|
||||
: ''
|
||||
};
|
||||
import {
|
||||
SourceChapter,
|
||||
SourceChapterItem,
|
||||
SourceNovel,
|
||||
SourceNovelItem,
|
||||
} from '../types';
|
||||
|
||||
const sourceId = ${source.sourceId};
|
||||
const sourceName = "${source.sourceName}";
|
||||
const baseUrl = "${source.baseUrl}";
|
||||
|
||||
${
|
||||
!source.popularNovels.url.includes('${page}')
|
||||
? '// eslint-disable-next-line @typescript-eslint/no-unused-vars'
|
||||
: ''
|
||||
}
|
||||
const popularNovels = async (page: number) => {
|
||||
const totalPages = ${source.popularNovels.totalPages};
|
||||
const url = \`${source.popularNovels.url}\`;
|
||||
|
||||
const result = await fetch(url);
|
||||
const body = await result.text();
|
||||
|
||||
const loadedCheerio = cheerio.load(body);
|
||||
|
||||
const novels: SourceNovelItem[] = [];
|
||||
|
||||
loadedCheerio('${
|
||||
source.popularNovels.selectors.novelList
|
||||
}').each(function () {
|
||||
let novelUrl = loadedCheerio(this).find('${
|
||||
source.popularNovels.selectors.novelUrl
|
||||
}').attr('href');
|
||||
|
||||
if (novelUrl && !isUrlAbsolute(novelUrl)) {
|
||||
novelUrl = baseUrl + novelUrl;
|
||||
}
|
||||
|
||||
if (novelUrl) {
|
||||
const novelName = loadedCheerio(this).find('${
|
||||
source.popularNovels.selectors.novelName
|
||||
}').text().trim();
|
||||
let novelCover = loadedCheerio(this).find('${
|
||||
source.popularNovels.selectors.novelCover
|
||||
}').attr('src');
|
||||
|
||||
if (novelCover && !isUrlAbsolute(novelCover)) {
|
||||
novelCover = baseUrl + novelCover;
|
||||
}
|
||||
|
||||
const novel = {
|
||||
sourceId,
|
||||
novelUrl,
|
||||
novelName,
|
||||
novelCover,
|
||||
};
|
||||
|
||||
novels.push(novel);
|
||||
}
|
||||
});
|
||||
|
||||
return { totalPages, novels };
|
||||
};
|
||||
|
||||
const parseNovelAndChapters = async (novelUrl: string) => {
|
||||
const url = novelUrl;
|
||||
|
||||
const result = await fetch(url);
|
||||
const body = await result.text();
|
||||
|
||||
let loadedCheerio = cheerio.load(body);
|
||||
|
||||
const novel: SourceNovel = {
|
||||
sourceId,
|
||||
sourceName,
|
||||
url: novelUrl,
|
||||
novelUrl,
|
||||
chapters: [],
|
||||
};
|
||||
|
||||
novel.novelName = loadedCheerio('${
|
||||
source.novelAndChapters.selectors.novelName
|
||||
}').text();
|
||||
|
||||
let novelCover = loadedCheerio('${
|
||||
source.novelAndChapters.selectors.novelCover
|
||||
}').attr('src');
|
||||
|
||||
novel.novelCover = novelCover
|
||||
? isUrlAbsolute(novelCover)
|
||||
? novelCover
|
||||
: baseUrl + novelCover
|
||||
: undefined;
|
||||
|
||||
novel.summary = loadedCheerio('${
|
||||
source.novelAndChapters.selectors.summary
|
||||
}').text().trim();
|
||||
|
||||
novel.author = loadedCheerio('${
|
||||
source.novelAndChapters.selectors.author
|
||||
}').text().trim();
|
||||
|
||||
novel.genre = loadedCheerio('${
|
||||
source.novelAndChapters.selectors.genre
|
||||
}').text().trim()${source?.options?.status?.replace || ''};
|
||||
|
||||
|
||||
novel.status = loadedCheerio('${
|
||||
source.novelAndChapters.selectors.status
|
||||
}').text().trim();
|
||||
|
||||
loadedCheerio('${
|
||||
source.novelAndChapters.selectors.chapterList
|
||||
}').each(function () {
|
||||
let chapterUrl = loadedCheerio(this).find('a').attr('href');
|
||||
|
||||
if (chapterUrl && !isUrlAbsolute(chapterUrl)) {
|
||||
chapterUrl = baseUrl + chapterUrl;
|
||||
}
|
||||
|
||||
if (chapterUrl) {
|
||||
const chapterName = loadedCheerio(this).find('${
|
||||
source.novelAndChapters.selectors.chapterName
|
||||
}').text().trim();
|
||||
const releaseDate = ${
|
||||
source.novelAndChapters.selectors.releaseDate
|
||||
? `loadedCheerio(this).find('${source.novelAndChapters.selectors.releaseDate}').text();`
|
||||
: null
|
||||
}
|
||||
|
||||
const chapter: SourceChapterItem = {
|
||||
chapterName,
|
||||
releaseDate,
|
||||
chapterUrl,
|
||||
};
|
||||
|
||||
novel.chapters?.push(chapter);
|
||||
}
|
||||
});
|
||||
|
||||
return novel;
|
||||
};
|
||||
|
||||
const parseChapter = async (novelUrl: string, chapterUrl: string) => {
|
||||
const url = chapterUrl;
|
||||
|
||||
const result = await fetch(url);
|
||||
const body = await result.text();
|
||||
|
||||
const loadedCheerio = cheerio.load(body);
|
||||
|
||||
const chapterName = loadedCheerio('${
|
||||
source.chapter.selectors.chapterName
|
||||
}').text();
|
||||
const chapterText = loadedCheerio('${
|
||||
source.chapter.selectors.chapterText
|
||||
}').html() || '';
|
||||
|
||||
const chapter: SourceChapter = {
|
||||
sourceId,
|
||||
novelUrl,
|
||||
chapterUrl,
|
||||
chapterName,
|
||||
chapterText,
|
||||
};
|
||||
|
||||
return chapter;
|
||||
};
|
||||
|
||||
${
|
||||
!source.search.url.includes('${searchTerm}')
|
||||
? '// eslint-disable-next-line @typescript-eslint/no-unused-vars'
|
||||
: ''
|
||||
}
|
||||
const searchNovels = async (searchTerm: string) => {
|
||||
${
|
||||
source.search
|
||||
? `
|
||||
const url = \`${source.search.url}\`;
|
||||
|
||||
const result = await fetch(url);
|
||||
const body = await result.text();
|
||||
|
||||
const loadedCheerio = cheerio.load(body);
|
||||
|
||||
const novels: SourceNovelItem[] = [];
|
||||
|
||||
loadedCheerio('${source.search.selectors.novelList}').each(function () {
|
||||
let novelUrl = loadedCheerio(this).find('${source.search.selectors.novelUrl}').attr('href');
|
||||
|
||||
if (novelUrl && !isUrlAbsolute(novelUrl)) {
|
||||
novelUrl = baseUrl + novelUrl;
|
||||
}
|
||||
|
||||
if (novelUrl) {
|
||||
const novelName = loadedCheerio(this).find('${source.search.selectors.novelName}').text();
|
||||
let novelCover = loadedCheerio(this).find('${source.search.selectors.novelCover}').attr('src');
|
||||
|
||||
if (novelCover && !isUrlAbsolute(novelCover)) {
|
||||
novelCover = baseUrl + novelCover;
|
||||
}
|
||||
|
||||
novels.push({
|
||||
sourceId,
|
||||
novelUrl,
|
||||
novelName,
|
||||
novelCover,
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
return novels;`
|
||||
: `
|
||||
showToast('Search is not available in this source')
|
||||
|
||||
return [];
|
||||
`
|
||||
}
|
||||
};
|
||||
|
||||
const ${source.sourceName}Scraper = {
|
||||
popularNovels,
|
||||
parseNovelAndChapters,
|
||||
parseChapter,
|
||||
searchNovels,
|
||||
};
|
||||
|
||||
export default ${source.sourceName}Scraper;
|
||||
|
||||
`;
|
||||
|
||||
const formatContent = prettier.format(content, {
|
||||
parser: 'typescript',
|
||||
...prettierConfig,
|
||||
});
|
||||
|
||||
fs.writeFile(
|
||||
path.resolve(
|
||||
process.cwd(),
|
||||
`src/sources/${getLanguageCode(source.lang)}/${source.sourceName}.ts`,
|
||||
),
|
||||
formatContent,
|
||||
error => {
|
||||
if (error) {
|
||||
console.log(error);
|
||||
}
|
||||
},
|
||||
);
|
||||
|
||||
if (!isDuplicate) {
|
||||
let allSourcesContent = AllSources;
|
||||
|
||||
allSourcesContent.push({
|
||||
sourceId: source.sourceId,
|
||||
sourceName: source.sourceName,
|
||||
icon: `https://github.com/LNReader/lnreader-sources/blob/main/src/${getLanguageCode(
|
||||
source.lang,
|
||||
)}/${source.sourceName.toLowerCase()}/icon.png?raw=true`,
|
||||
url: source.baseUrl,
|
||||
lang: source.lang || 'English',
|
||||
});
|
||||
|
||||
allSourcesContent = prettier.format(JSON.stringify(allSourcesContent), {
|
||||
parser: 'json',
|
||||
...prettierConfig,
|
||||
});
|
||||
|
||||
fs.writeFile(
|
||||
path.resolve(process.cwd(), 'src/sources/sources.json'),
|
||||
allSourcesContent,
|
||||
error => {
|
||||
if (error) {
|
||||
console.log(error);
|
||||
}
|
||||
},
|
||||
);
|
||||
} else {
|
||||
console.log('Source with same id already exists');
|
||||
}
|
||||
} else {
|
||||
console.log('Template path not provided');
|
||||
}
|
||||
} catch (e) {
|
||||
console.log(e);
|
||||
}
|
||||
@@ -0,0 +1,44 @@
|
||||
const fs = require('fs');
|
||||
const path = require('path');
|
||||
const prettier = require('prettier');
|
||||
const prettierConfig = require('../.prettierrc');
|
||||
|
||||
const flatten = (obj, target, prefix) => {
|
||||
(target = target || {}), (prefix = prefix || '');
|
||||
|
||||
Object.keys(obj).forEach(function (key) {
|
||||
if (typeof obj[key] === 'object' && obj[key] !== null) {
|
||||
flatten(obj[key], target, prefix + key + '.');
|
||||
} else {
|
||||
return (target[prefix + key] = 'string');
|
||||
}
|
||||
});
|
||||
|
||||
return target;
|
||||
};
|
||||
|
||||
const strings = fs.readFileSync(
|
||||
path.resolve(process.cwd(), 'strings/languages/en/strings.json'),
|
||||
'utf8',
|
||||
);
|
||||
|
||||
const stringTypes = `/**\n * This file is auto-generated\n */\n\n
|
||||
|
||||
export interface StringMap ${JSON.stringify(flatten(JSON.parse(strings)))}
|
||||
|
||||
`;
|
||||
|
||||
const formatContent = prettier.format(stringTypes, {
|
||||
parser: 'typescript',
|
||||
...prettierConfig,
|
||||
});
|
||||
|
||||
fs.writeFile(
|
||||
path.resolve(process.cwd(), 'strings/types/index.ts'),
|
||||
formatContent,
|
||||
err => {
|
||||
if (err) {
|
||||
console.log(err);
|
||||
}
|
||||
},
|
||||
);
|
||||
@@ -1,15 +1,15 @@
|
||||
import React from 'react';
|
||||
import {Dimensions, StyleSheet, View} from 'react-native';
|
||||
import { Dimensions, StyleSheet, View } from 'react-native';
|
||||
|
||||
import {IconButton} from 'react-native-paper';
|
||||
import { IconButton } from 'react-native-paper';
|
||||
|
||||
export const Actionbar = ({active, actions, theme, style}) => {
|
||||
export const Actionbar = ({ active, actions, theme, style }) => {
|
||||
if (active) {
|
||||
return (
|
||||
<View
|
||||
style={[
|
||||
styles.actionbarContainer,
|
||||
{backgroundColor: theme.colorPrimary},
|
||||
{ backgroundColor: theme.colorPrimary },
|
||||
style,
|
||||
]}
|
||||
>
|
||||
|
||||
@@ -1,22 +1,22 @@
|
||||
import React from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
|
||||
import {Appbar as MaterialAppbar} from 'react-native-paper';
|
||||
import { Appbar as MaterialAppbar } from 'react-native-paper';
|
||||
|
||||
import {useTheme} from '../hooks/reduxHooks';
|
||||
import {ViewPropTypes} from 'react-native';
|
||||
import { useTheme } from '../hooks/reduxHooks';
|
||||
import { ViewPropTypes } from 'react-native';
|
||||
|
||||
export const Appbar = ({title, onBackAction, style, children}) => {
|
||||
export const Appbar = ({ title, onBackAction, style, children }) => {
|
||||
const theme = useTheme();
|
||||
|
||||
return (
|
||||
<MaterialAppbar.Header
|
||||
style={[{backgroundColor: theme.colorPrimary}, style]}
|
||||
style={[{ backgroundColor: theme.colorPrimary }, style]}
|
||||
>
|
||||
{onBackAction && <MaterialAppbar.BackAction onPress={onBackAction} />}
|
||||
<MaterialAppbar.Content
|
||||
title={title}
|
||||
titleStyle={{color: theme.textColorPrimary}}
|
||||
titleStyle={{ color: theme.textColorPrimary }}
|
||||
/>
|
||||
{children}
|
||||
</MaterialAppbar.Header>
|
||||
|
||||
@@ -0,0 +1,26 @@
|
||||
import React from 'react';
|
||||
import { StatusBar } from 'react-native';
|
||||
|
||||
import { Appbar as PaperAppbar } from 'react-native-paper';
|
||||
import { ThemeTypeV1 } from '../../theme/v1/theme/types';
|
||||
|
||||
interface AppbarProps {
|
||||
title: string;
|
||||
handleGoBack: () => void;
|
||||
theme: ThemeTypeV1;
|
||||
}
|
||||
|
||||
const Appbar: React.FC<AppbarProps> = ({ title, handleGoBack, theme }) => (
|
||||
<PaperAppbar.Header
|
||||
style={{ backgroundColor: theme.colorPrimary }}
|
||||
statusBarHeight={StatusBar.currentHeight}
|
||||
>
|
||||
{handleGoBack && <PaperAppbar.BackAction onPress={handleGoBack} />}
|
||||
<PaperAppbar.Content
|
||||
title={title}
|
||||
titleStyle={{ color: theme.textColorPrimary }}
|
||||
/>
|
||||
</PaperAppbar.Header>
|
||||
);
|
||||
|
||||
export default Appbar;
|
||||
@@ -1,7 +1,7 @@
|
||||
import React from 'react';
|
||||
import {View} from 'react-native';
|
||||
import { View } from 'react-native';
|
||||
|
||||
const BottomSheetHandle = ({theme}) => (
|
||||
const BottomSheetHandle = ({ theme }) => (
|
||||
<View
|
||||
style={{
|
||||
backgroundColor: `${theme.textColorPrimary}66`,
|
||||
|
||||
@@ -0,0 +1,87 @@
|
||||
import { Pressable, StyleSheet, Text, View } from 'react-native';
|
||||
import React from 'react';
|
||||
|
||||
import { ThemeTypeV1 } from '../../theme/v1/theme/types';
|
||||
|
||||
export enum ButtonVariation {
|
||||
DEFAULT = 'default',
|
||||
OUTLINED = 'outlined',
|
||||
CLEAR = 'clear',
|
||||
}
|
||||
|
||||
interface Props {
|
||||
variation?: ButtonVariation;
|
||||
title: string;
|
||||
textColor?: string;
|
||||
onPress?: () => void;
|
||||
margin?: number;
|
||||
theme: ThemeTypeV1;
|
||||
}
|
||||
|
||||
const Button: React.FC<Props> = ({
|
||||
variation = ButtonVariation.DEFAULT,
|
||||
title,
|
||||
textColor,
|
||||
margin = 0,
|
||||
onPress,
|
||||
theme,
|
||||
}) => {
|
||||
return (
|
||||
<View
|
||||
style={[
|
||||
styles.button,
|
||||
{ margin },
|
||||
variation === 'outlined'
|
||||
? {
|
||||
...styles.outlined,
|
||||
borderColor: theme.textColorHint,
|
||||
}
|
||||
: variation !== 'clear' && {
|
||||
backgroundColor: theme.colorAccent,
|
||||
},
|
||||
]}
|
||||
>
|
||||
<Pressable
|
||||
style={styles.pressable}
|
||||
onPress={onPress}
|
||||
android_ripple={{ color: theme.rippleColor }}
|
||||
>
|
||||
<Text
|
||||
style={{
|
||||
color: textColor
|
||||
? textColor
|
||||
: variation === 'outlined'
|
||||
? theme.colorAccent
|
||||
: variation === 'default'
|
||||
? theme.colorButtonText
|
||||
: theme.textColorPrimary,
|
||||
}}
|
||||
numberOfLines={1}
|
||||
>
|
||||
{title}
|
||||
</Text>
|
||||
</Pressable>
|
||||
</View>
|
||||
);
|
||||
};
|
||||
|
||||
export default Button;
|
||||
|
||||
const styles = StyleSheet.create({
|
||||
button: {
|
||||
minHeight: 40,
|
||||
justifyContent: 'center',
|
||||
borderRadius: 50,
|
||||
overflow: 'hidden',
|
||||
},
|
||||
pressable: {
|
||||
flex: 1,
|
||||
justifyContent: 'center',
|
||||
alignItems: 'center',
|
||||
paddingVertical: 8,
|
||||
paddingHorizontal: 24,
|
||||
},
|
||||
outlined: {
|
||||
borderWidth: 1,
|
||||
},
|
||||
});
|
||||
@@ -1,12 +1,12 @@
|
||||
import React from 'react';
|
||||
import {Pressable, StyleSheet, Text} from 'react-native';
|
||||
import {Checkbox as PaperCheckbox} from 'react-native-paper';
|
||||
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}) => (
|
||||
export const Checkbox = ({ label, status, onPress, disabled, theme }) => (
|
||||
<Pressable
|
||||
android_ripple={{color: theme.rippleColor}}
|
||||
android_ripple={{ color: theme.rippleColor }}
|
||||
style={styles.pressable}
|
||||
onPress={onPress}
|
||||
disabled={disabled}
|
||||
@@ -24,14 +24,16 @@ export const Checkbox = ({label, status, onPress, disabled, theme}) => (
|
||||
uncheckedColor={theme.textColorSecondary}
|
||||
disabled={disabled}
|
||||
/>
|
||||
<Text style={{color: theme.textColorPrimary, marginLeft: 12}}>{label}</Text>
|
||||
<Text style={{ color: theme.textColorPrimary, marginLeft: 12 }}>
|
||||
{label}
|
||||
</Text>
|
||||
</Pressable>
|
||||
);
|
||||
|
||||
export const SortItem = ({label, status, onPress, theme}) => (
|
||||
export const SortItem = ({ label, status, onPress, theme }) => (
|
||||
<Pressable
|
||||
android_ripple={{color: theme.rippleColor}}
|
||||
style={[styles.pressable, {paddingVertical: 16, paddingLeft: 64}]}
|
||||
android_ripple={{ color: theme.rippleColor }}
|
||||
style={[styles.pressable, { paddingVertical: 16, paddingLeft: 64 }]}
|
||||
onPress={onPress}
|
||||
>
|
||||
{status && (
|
||||
@@ -42,7 +44,7 @@ export const SortItem = ({label, status, onPress, theme}) => (
|
||||
style={styles.icon}
|
||||
/>
|
||||
)}
|
||||
<Text style={{color: theme.textColorPrimary}}>{label}</Text>
|
||||
<Text style={{ color: theme.textColorPrimary }}>{label}</Text>
|
||||
</Pressable>
|
||||
);
|
||||
|
||||
|
||||
@@ -1,49 +0,0 @@
|
||||
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,
|
||||
},
|
||||
});
|
||||
@@ -0,0 +1,42 @@
|
||||
import React from 'react';
|
||||
import { Pressable, StyleSheet, Text, View } from 'react-native';
|
||||
|
||||
import { ThemeTypeV1 } from '../../theme/v1/theme/types';
|
||||
|
||||
interface ChipProps {
|
||||
label: string;
|
||||
theme: ThemeTypeV1;
|
||||
}
|
||||
|
||||
const Chip: React.FC<ChipProps> = ({ label, theme }) => (
|
||||
<View style={[styles.chipContainer, { backgroundColor: theme.colorPrimary }]}>
|
||||
<Pressable
|
||||
android_ripple={{ color: theme.rippleColor }}
|
||||
style={styles.pressable}
|
||||
>
|
||||
<Text style={[styles.label, { color: theme.textColorSecondary }]}>
|
||||
{label}
|
||||
</Text>
|
||||
</Pressable>
|
||||
</View>
|
||||
);
|
||||
|
||||
export default Chip;
|
||||
|
||||
const styles = StyleSheet.create({
|
||||
chipContainer: {
|
||||
overflow: 'hidden',
|
||||
borderRadius: 8,
|
||||
height: 28,
|
||||
marginRight: 8,
|
||||
},
|
||||
pressable: {
|
||||
flex: 1,
|
||||
justifyContent: 'center',
|
||||
alignItems: 'center',
|
||||
paddingHorizontal: 16,
|
||||
},
|
||||
label: {
|
||||
fontSize: 12,
|
||||
},
|
||||
});
|
||||
@@ -1,7 +1,7 @@
|
||||
import React, {useState} from 'react';
|
||||
import {FlatList, Pressable, StyleSheet, Text, View} from 'react-native';
|
||||
import React, { useState } from 'react';
|
||||
import { FlatList, Pressable, StyleSheet, Text, View } from 'react-native';
|
||||
|
||||
import {Modal, Portal, TextInput} from 'react-native-paper';
|
||||
import { Modal, Portal, TextInput } from 'react-native-paper';
|
||||
|
||||
const ColorPickerModal = ({
|
||||
theme,
|
||||
@@ -73,19 +73,19 @@ const ColorPickerModal = ({
|
||||
onDismiss={onDismiss}
|
||||
contentContainerStyle={[
|
||||
styles.modalContainer,
|
||||
{backgroundColor: theme.colorPrimary},
|
||||
{ backgroundColor: theme.colorPrimary },
|
||||
]}
|
||||
>
|
||||
<Text style={[styles.modalTitle, {color: theme.textColorPrimary}]}>
|
||||
<Text style={[styles.modalTitle, { color: theme.textColorPrimary }]}>
|
||||
{title}
|
||||
</Text>
|
||||
{showAccentColors && (
|
||||
<FlatList
|
||||
contentContainerStyle={{marginBottom: 8}}
|
||||
contentContainerStyle={{ marginBottom: 8 }}
|
||||
data={accentColors}
|
||||
numColumns={4}
|
||||
keyExtractor={item => item}
|
||||
renderItem={({item}) => (
|
||||
renderItem={({ item }) => (
|
||||
<View
|
||||
style={{
|
||||
borderRadius: 4,
|
||||
@@ -98,7 +98,7 @@ const ColorPickerModal = ({
|
||||
}}
|
||||
>
|
||||
<Pressable
|
||||
style={{flex: 1}}
|
||||
style={{ flex: 1 }}
|
||||
android_ripple={{
|
||||
color: 'rgba(0,0,0,0.12)',
|
||||
}}
|
||||
|
||||
@@ -1,9 +1,9 @@
|
||||
import React from 'react';
|
||||
import {View, StyleSheet} from 'react-native';
|
||||
import {useSafeAreaInsets} from 'react-native-safe-area-context';
|
||||
import { View, StyleSheet } from 'react-native';
|
||||
import { useSafeAreaInsets } from 'react-native-safe-area-context';
|
||||
|
||||
const ScreenContainer = ({children, theme}) => {
|
||||
const {bottom} = useSafeAreaInsets();
|
||||
const ScreenContainer = ({ children, theme }) => {
|
||||
const { bottom } = useSafeAreaInsets();
|
||||
|
||||
return (
|
||||
<View
|
||||
@@ -18,11 +18,11 @@ const ScreenContainer = ({children, theme}) => {
|
||||
);
|
||||
};
|
||||
|
||||
const Row = ({children, style}) => (
|
||||
const Row = ({ children, style }) => (
|
||||
<View style={[styles.row, style]}>{children}</View>
|
||||
);
|
||||
|
||||
export {ScreenContainer, Row};
|
||||
export { ScreenContainer, Row };
|
||||
|
||||
const styles = StyleSheet.create({
|
||||
row: {
|
||||
|
||||
@@ -1,8 +1,8 @@
|
||||
/* eslint-disable no-bitwise */
|
||||
|
||||
import PropTypes from 'prop-types';
|
||||
import React, {PureComponent, Children} from 'react';
|
||||
import {View, Animated, StyleSheet} from 'react-native';
|
||||
import React, { PureComponent, Children } from 'react';
|
||||
import { View, Animated, StyleSheet } from 'react-native';
|
||||
|
||||
export default class FadeView extends PureComponent {
|
||||
static defaultProps = {
|
||||
@@ -30,7 +30,7 @@ export default class FadeView extends PureComponent {
|
||||
this.renderChild = this.renderChild.bind(this);
|
||||
this.mounted = false;
|
||||
|
||||
let {active} = this.props;
|
||||
let { active } = this.props;
|
||||
|
||||
this.state = {
|
||||
progress: new Animated.Value(Number(active)),
|
||||
@@ -46,11 +46,11 @@ export default class FadeView extends PureComponent {
|
||||
this.mounted = false;
|
||||
}
|
||||
|
||||
UNSAFE_componentWillReceiveProps({active, animationDuration}) {
|
||||
let {progress} = this.state;
|
||||
UNSAFE_componentWillReceiveProps({ active, animationDuration }) {
|
||||
let { progress } = this.state;
|
||||
|
||||
if (active ^ this.props.active) {
|
||||
this.setState({animating: true});
|
||||
this.setState({ animating: true });
|
||||
|
||||
Animated.timing(progress, {
|
||||
toValue: Number(active),
|
||||
@@ -58,15 +58,15 @@ export default class FadeView extends PureComponent {
|
||||
useNativeDriver: true,
|
||||
}).start(() => {
|
||||
if (this.mounted) {
|
||||
this.setState({animating: false});
|
||||
this.setState({ animating: false });
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
renderChild(child, index) {
|
||||
let {active, removeHiddenSubviews} = this.props;
|
||||
let {animating, progress} = this.state;
|
||||
let { active, removeHiddenSubviews } = this.props;
|
||||
let { animating, progress } = this.state;
|
||||
|
||||
let opacity = progress.interpolate({
|
||||
inputRange: [0, 1],
|
||||
@@ -85,7 +85,7 @@ export default class FadeView extends PureComponent {
|
||||
|
||||
return (
|
||||
<Animated.View
|
||||
style={[styles.container, {opacity}]}
|
||||
style={[styles.container, { opacity }]}
|
||||
pointerEvents={pointerEvents}
|
||||
>
|
||||
{child}
|
||||
@@ -94,7 +94,7 @@ export default class FadeView extends PureComponent {
|
||||
}
|
||||
|
||||
render() {
|
||||
let {children, ...props} = this.props;
|
||||
let { children, ...props } = this.props;
|
||||
|
||||
return <View {...props}>{Children.map(children, this.renderChild)}</View>;
|
||||
}
|
||||
|
||||
@@ -1,8 +1,8 @@
|
||||
import React from 'react';
|
||||
import {Pressable, View} from 'react-native';
|
||||
import { Pressable, View } from 'react-native';
|
||||
import MaterialCommunityIcons from 'react-native-vector-icons/MaterialCommunityIcons';
|
||||
|
||||
export const ToggleButton = ({icon, selected, theme, color, onPress}) => (
|
||||
export const ToggleButton = ({ icon, selected, theme, color, onPress }) => (
|
||||
<View
|
||||
style={{
|
||||
borderRadius: 8,
|
||||
@@ -11,7 +11,7 @@ export const ToggleButton = ({icon, selected, theme, color, onPress}) => (
|
||||
}}
|
||||
>
|
||||
<Pressable
|
||||
android_ripple={{color: theme.rippleColor}}
|
||||
android_ripple={{ color: theme.rippleColor }}
|
||||
style={{
|
||||
padding: 10,
|
||||
alignItems: 'center',
|
||||
@@ -46,7 +46,7 @@ export const ToggleColorButton = ({
|
||||
}}
|
||||
>
|
||||
<Pressable
|
||||
android_ripple={{color: textColor}}
|
||||
android_ripple={{ color: textColor }}
|
||||
style={{
|
||||
flex: 1,
|
||||
padding: 10,
|
||||
|
||||
@@ -0,0 +1,28 @@
|
||||
import React, { ReactNode } from 'react';
|
||||
import { StyleSheet, View } from 'react-native';
|
||||
import { useSafeAreaInsets } from 'react-native-safe-area-context';
|
||||
|
||||
interface Props {
|
||||
children: ReactNode;
|
||||
}
|
||||
|
||||
const Container: React.FC<Props> = ({ children }) => {
|
||||
const { bottom } = useSafeAreaInsets();
|
||||
|
||||
const containerStyle = [
|
||||
styles.container,
|
||||
{
|
||||
paddingBottom: bottom,
|
||||
},
|
||||
];
|
||||
|
||||
return <View style={containerStyle}>{children}</View>;
|
||||
};
|
||||
|
||||
export default Container;
|
||||
|
||||
const styles = StyleSheet.create({
|
||||
container: {
|
||||
flex: 1,
|
||||
},
|
||||
});
|
||||
@@ -1,8 +1,8 @@
|
||||
/* eslint-disable no-unused-vars */
|
||||
|
||||
import React from 'react';
|
||||
import {StyleSheet} from 'react-native';
|
||||
import { StyleSheet } from 'react-native';
|
||||
|
||||
export const Dialog = ({visible, theme}) => {};
|
||||
export const Dialog = ({ visible, theme }) => {};
|
||||
|
||||
const styles = StyleSheet.create({});
|
||||
|
||||
@@ -1,8 +1,8 @@
|
||||
import React from 'react';
|
||||
import {StyleSheet, Text, View} from 'react-native';
|
||||
import {useSelector} from 'react-redux';
|
||||
import { StyleSheet, Text, View } from 'react-native';
|
||||
import { useSelector } from 'react-redux';
|
||||
|
||||
const EmptyView = ({icon, description, style, children, iconStyle}) => {
|
||||
const EmptyView = ({ icon, description, style, children, iconStyle }) => {
|
||||
const theme = useSelector(state => state.settingsReducer.theme);
|
||||
|
||||
return (
|
||||
@@ -10,14 +10,16 @@ const EmptyView = ({icon, description, style, children, iconStyle}) => {
|
||||
<Text
|
||||
style={[
|
||||
styles.emptyViewIcon,
|
||||
{color: theme.textColorHint},
|
||||
{ color: theme.textColorHint },
|
||||
style,
|
||||
iconStyle,
|
||||
]}
|
||||
>
|
||||
{icon}
|
||||
</Text>
|
||||
<Text style={[styles.emptyViewText, {color: theme.textColorHint}, style]}>
|
||||
<Text
|
||||
style={[styles.emptyViewText, { color: theme.textColorHint }, style]}
|
||||
>
|
||||
{description}
|
||||
</Text>
|
||||
{children}
|
||||
|
||||
@@ -0,0 +1,38 @@
|
||||
import React from 'react';
|
||||
import { StyleSheet, View, Text } from 'react-native';
|
||||
|
||||
import { ThemeTypeV1 } from '../../theme/v1/theme/types';
|
||||
|
||||
interface EmptyViewProps {
|
||||
icon?: string;
|
||||
description: string;
|
||||
theme: ThemeTypeV1;
|
||||
}
|
||||
|
||||
const EmptyView: React.FC<EmptyViewProps> = ({ icon, description, theme }) => (
|
||||
<View style={styles.container}>
|
||||
{icon ? (
|
||||
<Text style={[styles.icon, { color: theme.textColorHint }]}>{icon}</Text>
|
||||
) : null}
|
||||
<Text style={[styles.text, { color: theme.textColorHint }]}>
|
||||
{description}
|
||||
</Text>
|
||||
</View>
|
||||
);
|
||||
|
||||
export default EmptyView;
|
||||
|
||||
const styles = StyleSheet.create({
|
||||
container: {
|
||||
flex: 1,
|
||||
alignItems: 'center',
|
||||
justifyContent: 'center',
|
||||
},
|
||||
icon: {
|
||||
fontSize: 40,
|
||||
fontWeight: 'bold',
|
||||
},
|
||||
text: {
|
||||
marginTop: 16,
|
||||
},
|
||||
});
|
||||
@@ -0,0 +1,39 @@
|
||||
import React from 'react';
|
||||
import { StyleSheet, Text, View } from 'react-native';
|
||||
import { Container } from '..';
|
||||
|
||||
import { ThemeTypeV1 } from '../../theme/v1/theme/types';
|
||||
|
||||
interface ErrorScreenProps {
|
||||
error?: string;
|
||||
theme: ThemeTypeV1;
|
||||
}
|
||||
|
||||
const ErrorScreen: React.FC<ErrorScreenProps> = ({ error, theme }) => {
|
||||
return (
|
||||
<Container>
|
||||
<View style={styles.container}>
|
||||
<Text style={[styles.icon, { color: theme.textColorHint }]}>ಥ_ಥ</Text>
|
||||
<Text style={[styles.error, { color: theme.textColorHint }]}>
|
||||
{error}
|
||||
</Text>
|
||||
</View>
|
||||
</Container>
|
||||
);
|
||||
};
|
||||
|
||||
export default ErrorScreen;
|
||||
|
||||
const styles = StyleSheet.create({
|
||||
container: {
|
||||
flex: 1,
|
||||
justifyContent: 'center',
|
||||
alignItems: 'center',
|
||||
},
|
||||
icon: {
|
||||
fontSize: 44,
|
||||
},
|
||||
error: {
|
||||
marginTop: 16,
|
||||
},
|
||||
});
|
||||
@@ -1,26 +1,26 @@
|
||||
import React from 'react';
|
||||
import {Text, View} from 'react-native';
|
||||
import { Text, View } from 'react-native';
|
||||
|
||||
import {IconButton} from 'react-native-paper';
|
||||
import { IconButton } from 'react-native-paper';
|
||||
// import {Row} from './Common';
|
||||
|
||||
import EmptyView from './EmptyView';
|
||||
|
||||
const ErrorView = ({theme, onRetry, error, openWebView}) => (
|
||||
<View style={{flex: 1, justifyContent: 'center'}}>
|
||||
const ErrorView = ({ theme, onRetry, error, openWebView }) => (
|
||||
<View style={{ flex: 1, justifyContent: 'center' }}>
|
||||
<EmptyView
|
||||
icon="(; ̄Д ̄)"
|
||||
description={error}
|
||||
style={{color: theme.textColorSecondary}}
|
||||
style={{ color: theme.textColorSecondary }}
|
||||
>
|
||||
<IconButton
|
||||
icon="reload"
|
||||
size={25}
|
||||
style={{margin: 0, marginTop: 24}}
|
||||
style={{ margin: 0, marginTop: 24 }}
|
||||
color={theme.text}
|
||||
onPress={onRetry}
|
||||
/>
|
||||
<Text style={{color: theme.textColorPrimary}}>Retry</Text>
|
||||
<Text style={{ color: theme.textColorPrimary }}>Retry</Text>
|
||||
</EmptyView>
|
||||
</View>
|
||||
);
|
||||
|
||||
@@ -1,19 +1,19 @@
|
||||
import React from 'react';
|
||||
import {Pressable, StyleSheet, Text, View} from 'react-native';
|
||||
import {IconButton} from 'react-native-paper';
|
||||
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}]}>
|
||||
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}]}>
|
||||
<Text style={[styles.emptyViewText, { color: theme.textColorHint }]}>
|
||||
{errorName}
|
||||
</Text>
|
||||
<View style={{flexDirection: 'row'}}>
|
||||
<View style={{ flexDirection: 'row' }}>
|
||||
{actions.map(action => (
|
||||
<View
|
||||
style={{
|
||||
@@ -40,7 +40,7 @@ export const ErrorView = ({errorName, actions, theme}) => (
|
||||
icon={action.icon}
|
||||
color={theme.textColorHint}
|
||||
size={24}
|
||||
style={{margin: 0}}
|
||||
style={{ margin: 0 }}
|
||||
/>
|
||||
<Text
|
||||
style={{
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import React from 'react';
|
||||
import {Pressable, StyleSheet, ViewPropTypes} from 'react-native';
|
||||
import { Pressable, StyleSheet, ViewPropTypes } from 'react-native';
|
||||
import PropTypes from 'prop-types';
|
||||
|
||||
import MaterialCommunityIcons from 'react-native-vector-icons/MaterialCommunityIcons';
|
||||
@@ -14,7 +14,7 @@ export const IconButton = ({
|
||||
}) => (
|
||||
<Pressable
|
||||
style={[styles.container, containerStyle]}
|
||||
android_ripple={{color: theme.rippleColor, borderless: true, radius: 20}}
|
||||
android_ripple={{ color: theme.rippleColor, borderless: true, radius: 20 }}
|
||||
onPress={onPress}
|
||||
>
|
||||
<MaterialCommunityIcons name={icon} size={size} color={color} />
|
||||
|
||||
@@ -0,0 +1,54 @@
|
||||
import React from 'react';
|
||||
import { Pressable, StyleSheet, View, ViewStyle } from 'react-native';
|
||||
import MaterialCommunityIcons from 'react-native-vector-icons/MaterialCommunityIcons';
|
||||
|
||||
import { ThemeTypeV1 } from '../../theme/v1/theme/types';
|
||||
|
||||
type Props = {
|
||||
name: string;
|
||||
color?: string;
|
||||
size?: number;
|
||||
disabled?: boolean;
|
||||
padding?: number;
|
||||
onPress?: () => void;
|
||||
theme: ThemeTypeV1;
|
||||
style?: ViewStyle;
|
||||
};
|
||||
|
||||
const IconButton: React.FC<Props> = ({
|
||||
name,
|
||||
color,
|
||||
size = 24,
|
||||
padding = 8,
|
||||
onPress,
|
||||
disabled,
|
||||
theme,
|
||||
style,
|
||||
}) => (
|
||||
<View style={[styles.container, style]}>
|
||||
<Pressable
|
||||
style={[styles.pressable, { padding }]}
|
||||
onPress={onPress}
|
||||
disabled={disabled}
|
||||
android_ripple={{ color: theme.rippleColor }}
|
||||
>
|
||||
<MaterialCommunityIcons
|
||||
name={name}
|
||||
size={size}
|
||||
color={disabled ? theme.textColorHint : color || theme.textColorPrimary}
|
||||
/>
|
||||
</Pressable>
|
||||
</View>
|
||||
);
|
||||
|
||||
export default IconButton;
|
||||
|
||||
const styles = StyleSheet.create({
|
||||
container: {
|
||||
borderRadius: 50,
|
||||
overflow: 'hidden',
|
||||
},
|
||||
pressable: {
|
||||
padding: 8,
|
||||
},
|
||||
});
|
||||
+21
-19
@@ -1,16 +1,16 @@
|
||||
import React from 'react';
|
||||
import {Pressable, StyleSheet, Text, View} from 'react-native';
|
||||
import { Pressable, StyleSheet, Text, View } from 'react-native';
|
||||
|
||||
import {List as PaperList, Divider as PaperDivider} from 'react-native-paper';
|
||||
import { List as PaperList, Divider as PaperDivider } from 'react-native-paper';
|
||||
import MaterialCommunityIcons from 'react-native-vector-icons/MaterialCommunityIcons';
|
||||
|
||||
const ListSection = ({children}) => (
|
||||
const ListSection = ({ children }) => (
|
||||
<PaperList.Section style={styles.listSection}>{children}</PaperList.Section>
|
||||
);
|
||||
|
||||
const ListSubHeader = ({children, theme}) => (
|
||||
const ListSubHeader = ({ children, theme }) => (
|
||||
<PaperList.Subheader
|
||||
style={[styles.listSubHeader, {color: theme.colorAccent}]}
|
||||
style={[styles.listSubHeader, { color: theme.colorAccent }]}
|
||||
>
|
||||
{children}
|
||||
</PaperList.Subheader>
|
||||
@@ -30,12 +30,12 @@ const ListItem = ({
|
||||
<PaperList.Item
|
||||
title={title}
|
||||
style={style}
|
||||
titleStyle={[{color: theme.textColorPrimary}, titleStyle]}
|
||||
titleStyle={[{ color: theme.textColorPrimary }, titleStyle]}
|
||||
description={description}
|
||||
descriptionStyle={{color: theme.textColorSecondary}}
|
||||
descriptionStyle={{ color: theme.textColorSecondary }}
|
||||
descriptionNumberOfLines={1}
|
||||
left={() => (
|
||||
<View style={{justifyContent: 'center'}}>
|
||||
<View style={{ justifyContent: 'center' }}>
|
||||
{icon && (
|
||||
<PaperList.Icon
|
||||
color={theme.colorAccent}
|
||||
@@ -53,7 +53,7 @@ const ListItem = ({
|
||||
name={right}
|
||||
color={iconColor || '#47a84a'}
|
||||
size={23}
|
||||
style={{marginRight: 16, textAlignVertical: 'center'}}
|
||||
style={{ marginRight: 16, textAlignVertical: 'center' }}
|
||||
/>
|
||||
)
|
||||
}
|
||||
@@ -62,14 +62,14 @@ const ListItem = ({
|
||||
/>
|
||||
);
|
||||
|
||||
const Divider = ({theme}) => (
|
||||
<PaperDivider style={{backgroundColor: theme.dividerColor, height: 1}} />
|
||||
const Divider = ({ theme }) => (
|
||||
<PaperDivider style={{ backgroundColor: theme.dividerColor, height: 1 }} />
|
||||
);
|
||||
|
||||
const InfoItem = ({title, icon, theme}) => (
|
||||
const InfoItem = ({ title, icon, theme }) => (
|
||||
<PaperList.Item
|
||||
title={title}
|
||||
titleStyle={{color: theme.textColorSecondary, fontSize: 14}}
|
||||
titleStyle={{ color: theme.textColorSecondary, fontSize: 14 }}
|
||||
titleNumberOfLines={5}
|
||||
left={() =>
|
||||
icon && <PaperList.Icon color={theme.textColorSecondary} icon={icon} />
|
||||
@@ -77,7 +77,7 @@ const InfoItem = ({title, icon, theme}) => (
|
||||
/>
|
||||
);
|
||||
|
||||
const ColorItem = ({title, description, theme, onPress}) => (
|
||||
const ColorItem = ({ title, description, theme, onPress }) => (
|
||||
<Pressable
|
||||
style={{
|
||||
padding: 16,
|
||||
@@ -85,12 +85,14 @@ const ColorItem = ({title, description, theme, onPress}) => (
|
||||
justifyContent: 'space-between',
|
||||
alignItems: 'center',
|
||||
}}
|
||||
android_ripple={{color: theme.rippleColor}}
|
||||
android_ripple={{ color: theme.rippleColor }}
|
||||
onPress={onPress}
|
||||
>
|
||||
<View>
|
||||
<Text style={{color: theme.textColorPrimary, fontSize: 16}}>{title}</Text>
|
||||
<Text style={{color: theme.textColorSecondary}}>{description}</Text>
|
||||
<Text style={{ color: theme.textColorPrimary, fontSize: 16 }}>
|
||||
{title}
|
||||
</Text>
|
||||
<Text style={{ color: theme.textColorSecondary }}>{description}</Text>
|
||||
</View>
|
||||
<View
|
||||
style={{
|
||||
@@ -104,8 +106,8 @@ const ColorItem = ({title, description, theme, onPress}) => (
|
||||
</Pressable>
|
||||
);
|
||||
|
||||
const Icon = ({icon, theme}) => (
|
||||
<PaperList.Icon color={theme.colorAccent} icon={icon} style={{margin: 0}} />
|
||||
const Icon = ({ icon, theme }) => (
|
||||
<PaperList.Icon color={theme.colorAccent} icon={icon} style={{ margin: 0 }} />
|
||||
);
|
||||
|
||||
export const List = {
|
||||
|
||||
@@ -0,0 +1,109 @@
|
||||
import React, { ReactNode } from 'react';
|
||||
import { StyleSheet, View } from 'react-native';
|
||||
|
||||
import { List as PaperList, Divider as PaperDivider } from 'react-native-paper';
|
||||
import { ThemeTypeV1 } from '../../theme/v1/theme/types';
|
||||
|
||||
interface ListItemProps {
|
||||
title: string;
|
||||
description?: string | null;
|
||||
icon?: string;
|
||||
onPress?: () => void;
|
||||
theme: ThemeTypeV1;
|
||||
}
|
||||
|
||||
const Section = ({ children }: { children: ReactNode }) => (
|
||||
<PaperList.Section style={styles.listSection}>{children}</PaperList.Section>
|
||||
);
|
||||
|
||||
const SubHeader = ({
|
||||
children,
|
||||
theme,
|
||||
}: {
|
||||
children: ReactNode;
|
||||
theme: ThemeTypeV1;
|
||||
}) => (
|
||||
<PaperList.Subheader style={{ color: theme.colorAccent }}>
|
||||
{children}
|
||||
</PaperList.Subheader>
|
||||
);
|
||||
|
||||
const Item: React.FC<ListItemProps> = ({
|
||||
title,
|
||||
description,
|
||||
icon,
|
||||
onPress,
|
||||
theme,
|
||||
}) => (
|
||||
<PaperList.Item
|
||||
title={title}
|
||||
titleStyle={{ color: theme.textColorPrimary }}
|
||||
description={description}
|
||||
descriptionStyle={{ color: theme.textColorSecondary }}
|
||||
left={() => (
|
||||
<View style={styles.iconContainer}>
|
||||
{icon && (
|
||||
<PaperList.Icon
|
||||
color={theme.colorAccent}
|
||||
icon={icon}
|
||||
style={styles.icon}
|
||||
/>
|
||||
)}
|
||||
</View>
|
||||
)}
|
||||
onPress={onPress}
|
||||
rippleColor={theme.rippleColor}
|
||||
/>
|
||||
);
|
||||
|
||||
const Divider = ({ theme }: { theme: ThemeTypeV1 }) => (
|
||||
<PaperDivider
|
||||
style={[styles.divider, { backgroundColor: theme.dividerColor }]}
|
||||
/>
|
||||
);
|
||||
|
||||
const InfoItem = ({
|
||||
title,
|
||||
icon,
|
||||
theme,
|
||||
}: {
|
||||
title: string;
|
||||
icon: string;
|
||||
theme: ThemeTypeV1;
|
||||
}) => (
|
||||
<PaperList.Item
|
||||
title={title}
|
||||
titleStyle={[{ color: theme.textColorSecondary }, styles.infoItem]}
|
||||
titleNumberOfLines={5}
|
||||
left={() =>
|
||||
icon && <PaperList.Icon color={theme.textColorSecondary} icon={icon} />
|
||||
}
|
||||
/>
|
||||
);
|
||||
|
||||
export default {
|
||||
Section,
|
||||
SubHeader,
|
||||
Item,
|
||||
Divider,
|
||||
InfoItem,
|
||||
};
|
||||
|
||||
const styles = StyleSheet.create({
|
||||
listSection: {
|
||||
flex: 1,
|
||||
marginVertical: 0,
|
||||
},
|
||||
divider: {
|
||||
height: 1,
|
||||
},
|
||||
iconContainer: {
|
||||
justifyContent: 'center',
|
||||
},
|
||||
icon: {
|
||||
marginVertical: 0,
|
||||
},
|
||||
infoItem: {
|
||||
fontSize: 14,
|
||||
},
|
||||
});
|
||||
@@ -1,8 +1,9 @@
|
||||
import React from 'react';
|
||||
import {StyleSheet, View, Text, Pressable} from 'react-native';
|
||||
import { StyleSheet, View, Text, Pressable } from 'react-native';
|
||||
|
||||
import FastImage from 'react-native-fast-image';
|
||||
import {defaultCoverUri} from '../sources/helpers/constants';
|
||||
import { defaultCoverUri } from '../sources/helpers/constants';
|
||||
import { coverPlaceholderColor } from '../theme/colors';
|
||||
|
||||
const ListView = ({
|
||||
item,
|
||||
@@ -14,29 +15,24 @@ const ListView = ({
|
||||
isSelected,
|
||||
onLongPress,
|
||||
}) => {
|
||||
const uri =
|
||||
item.novelCover && !item.novelCover.startsWith('/')
|
||||
? item.novelCover
|
||||
: defaultCoverUri;
|
||||
|
||||
return (
|
||||
<Pressable
|
||||
android_ripple={{color: theme.rippleColor}}
|
||||
android_ripple={{ color: theme.rippleColor }}
|
||||
style={[
|
||||
styles.listView,
|
||||
isSelected && {backgroundColor: theme.rippleColor},
|
||||
isSelected && { backgroundColor: theme.rippleColor },
|
||||
]}
|
||||
onPress={onPress}
|
||||
onLongPress={onLongPress}
|
||||
>
|
||||
<FastImage
|
||||
source={{
|
||||
uri,
|
||||
uri: item.novelCover,
|
||||
}}
|
||||
style={[styles.extensionIcon, inLibraryBadge && {opacity: 0.5}]}
|
||||
style={[styles.extensionIcon, inLibraryBadge && { opacity: 0.5 }]}
|
||||
/>
|
||||
<Text
|
||||
style={[{color: theme.textColorPrimary}, styles.novelName]}
|
||||
style={[{ color: theme.textColorPrimary }, styles.novelName]}
|
||||
numberOfLines={1}
|
||||
>
|
||||
{item.novelName}
|
||||
@@ -64,6 +60,7 @@ const styles = StyleSheet.create({
|
||||
width: 40,
|
||||
height: 40,
|
||||
borderRadius: 4,
|
||||
backgroundColor: coverPlaceholderColor,
|
||||
},
|
||||
novelName: {
|
||||
flex: 1,
|
||||
|
||||
@@ -1,8 +1,8 @@
|
||||
import React from 'react';
|
||||
import {ActivityIndicator, View} from 'react-native';
|
||||
import { ActivityIndicator, View } from 'react-native';
|
||||
|
||||
export const LoadingScreen = ({theme}) => (
|
||||
<View style={{flex: 1, justifyContent: 'center'}}>
|
||||
export const LoadingScreen = ({ theme }) => (
|
||||
<View style={{ flex: 1, justifyContent: 'center' }}>
|
||||
<ActivityIndicator size={50} color={theme.colorAccent} />
|
||||
</View>
|
||||
);
|
||||
|
||||
@@ -0,0 +1,25 @@
|
||||
import React from 'react';
|
||||
import { StyleSheet, ActivityIndicator } from 'react-native';
|
||||
|
||||
import { Container } from '..';
|
||||
import { ThemeTypeV1 } from '../../theme/v1/theme/types';
|
||||
|
||||
const LoadingScreen: React.FC<{ theme: ThemeTypeV1 }> = ({ theme }) => (
|
||||
<Container>
|
||||
<ActivityIndicator
|
||||
size={50}
|
||||
color={theme.colorAccent}
|
||||
style={styles.indicator}
|
||||
/>
|
||||
</Container>
|
||||
);
|
||||
|
||||
export default LoadingScreen;
|
||||
|
||||
const styles = StyleSheet.create({
|
||||
indicator: {
|
||||
flex: 1,
|
||||
justifyContent: 'center',
|
||||
alignItems: 'center',
|
||||
},
|
||||
});
|
||||
@@ -1,10 +1,10 @@
|
||||
import React, {useState} from 'react';
|
||||
import {StyleSheet, View, Text} from 'react-native';
|
||||
import {Portal, Modal, Button} from 'react-native-paper';
|
||||
import React, { useState } from 'react';
|
||||
import { StyleSheet, View, Text } from 'react-native';
|
||||
import { Portal, Modal, Button } from 'react-native-paper';
|
||||
import * as Linking from 'expo-linking';
|
||||
import {useTheme} from '../hooks/reduxHooks';
|
||||
import { useTheme } from '../hooks/reduxHooks';
|
||||
|
||||
const NewUpdateDialog = ({newVersion}) => {
|
||||
const NewUpdateDialog = ({ newVersion }) => {
|
||||
const [newUpdateDialog, showNewUpdateDialog] = useState(true);
|
||||
|
||||
const theme = useTheme();
|
||||
@@ -16,13 +16,13 @@ const NewUpdateDialog = ({newVersion}) => {
|
||||
onDismiss={() => showNewUpdateDialog(false)}
|
||||
contentContainerStyle={[
|
||||
styles.containerStyle,
|
||||
{backgroundColor: theme.colorPrimaryDark},
|
||||
{ backgroundColor: theme.colorPrimaryDark },
|
||||
]}
|
||||
>
|
||||
<Text style={[styles.modalHeader, {color: theme.textColorPrimary}]}>
|
||||
<Text style={[styles.modalHeader, { color: theme.textColorPrimary }]}>
|
||||
{`New update available ${newVersion.tag_name}`}
|
||||
</Text>
|
||||
<Text style={{color: theme.textColorSecondary, fontSize: 16}}>
|
||||
<Text style={{ color: theme.textColorSecondary, fontSize: 16 }}>
|
||||
{newVersion.body}
|
||||
</Text>
|
||||
<View
|
||||
@@ -38,7 +38,7 @@ const NewUpdateDialog = ({newVersion}) => {
|
||||
letterSpacing: 0,
|
||||
textTransform: 'none',
|
||||
}}
|
||||
theme={{colors: {primary: theme.colorAccent}}}
|
||||
theme={{ colors: { primary: theme.colorAccent } }}
|
||||
onPress={() => showNewUpdateDialog(false)}
|
||||
>
|
||||
Cancel
|
||||
@@ -49,7 +49,7 @@ const NewUpdateDialog = ({newVersion}) => {
|
||||
letterSpacing: 0,
|
||||
textTransform: 'none',
|
||||
}}
|
||||
theme={{colors: {primary: theme.colorAccent}}}
|
||||
theme={{ colors: { primary: theme.colorAccent } }}
|
||||
onPress={() => Linking.openURL(newVersion.downloadUrl)}
|
||||
>
|
||||
Install
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import React, {memo, useCallback} from 'react';
|
||||
import React, { memo, useCallback } from 'react';
|
||||
import {
|
||||
StyleSheet,
|
||||
View,
|
||||
@@ -7,14 +7,15 @@ import {
|
||||
Pressable,
|
||||
} from 'react-native';
|
||||
|
||||
import {LinearGradient} from 'expo-linear-gradient';
|
||||
import { LinearGradient } from 'expo-linear-gradient';
|
||||
import FastImage from 'react-native-fast-image';
|
||||
|
||||
import ListView from './ListView';
|
||||
|
||||
import {useDeviceOrientation} from '../services/utils/helpers';
|
||||
import {useSettings} from '../hooks/reduxHooks';
|
||||
import {defaultCoverUri} from '../sources/helpers/constants';
|
||||
import { useDeviceOrientation } from '../services/utils/helpers';
|
||||
import { useSettings } from '../hooks/reduxHooks';
|
||||
import { defaultCoverUri } from '../sources/helpers/constants';
|
||||
import { coverPlaceholderColor } from '../theme/colors';
|
||||
|
||||
const NovelCover = ({
|
||||
item,
|
||||
@@ -25,7 +26,7 @@ const NovelCover = ({
|
||||
onLongPress,
|
||||
selectedNovels,
|
||||
}) => {
|
||||
const {displayMode, novelsPerRow, showDownloadBadges, showUnreadBadges} =
|
||||
const { displayMode, novelsPerRow, showDownloadBadges, showUnreadBadges } =
|
||||
useSettings();
|
||||
|
||||
const window = useWindowDimensions();
|
||||
@@ -43,10 +44,7 @@ const NovelCover = ({
|
||||
|
||||
const selectNovel = () => onLongPress && onLongPress(item.novelId);
|
||||
|
||||
const uri =
|
||||
item.novelCover && !item.novelCover.startsWith('/')
|
||||
? item.novelCover
|
||||
: defaultCoverUri;
|
||||
const uri = item.novelCover;
|
||||
|
||||
return displayMode !== 2 ? (
|
||||
<View
|
||||
@@ -64,7 +62,7 @@ const NovelCover = ({
|
||||
]}
|
||||
>
|
||||
<Pressable
|
||||
android_ripple={{color: theme.colorAccent}}
|
||||
android_ripple={{ color: theme.colorAccent }}
|
||||
style={styles.opac}
|
||||
onPress={
|
||||
selectedNovels && selectedNovels.length > 0 ? selectNovel : onPress
|
||||
@@ -90,10 +88,14 @@ const NovelCover = ({
|
||||
)}
|
||||
</View>
|
||||
<FastImage
|
||||
source={{uri}}
|
||||
source={{ uri }}
|
||||
style={[
|
||||
{height: getHeight(), borderRadius: 4},
|
||||
libraryStatus && {opacity: 0.5},
|
||||
{
|
||||
height: getHeight(),
|
||||
borderRadius: 4,
|
||||
backgroundColor: coverPlaceholderColor,
|
||||
},
|
||||
libraryStatus && { opacity: 0.5 },
|
||||
]}
|
||||
/>
|
||||
<View style={styles.compactTitleContainer}>
|
||||
@@ -141,7 +143,7 @@ const NovelCover = ({
|
||||
|
||||
export default memo(NovelCover);
|
||||
|
||||
const ComfortableTitle = ({theme, novelName}) => (
|
||||
const ComfortableTitle = ({ theme, novelName }) => (
|
||||
<Text
|
||||
numberOfLines={2}
|
||||
style={[
|
||||
@@ -156,7 +158,7 @@ const ComfortableTitle = ({theme, novelName}) => (
|
||||
</Text>
|
||||
);
|
||||
|
||||
const CompactTitle = ({novelName}) => (
|
||||
const CompactTitle = ({ novelName }) => (
|
||||
<View style={styles.titleContainer}>
|
||||
<LinearGradient
|
||||
colors={['transparent', 'rgba(0,0,0,0.7)']}
|
||||
@@ -169,7 +171,7 @@ const CompactTitle = ({novelName}) => (
|
||||
{
|
||||
color: 'rgba(255,255,255,1)',
|
||||
textShadowColor: 'rgba(0, 0, 0, 0.75)',
|
||||
textShadowOffset: {width: -1, height: 1},
|
||||
textShadowOffset: { width: -1, height: 1 },
|
||||
textShadowRadius: 10,
|
||||
},
|
||||
]}
|
||||
@@ -180,7 +182,7 @@ const CompactTitle = ({novelName}) => (
|
||||
</View>
|
||||
);
|
||||
|
||||
const InLibraryBadge = ({theme}) => (
|
||||
const InLibraryBadge = ({ theme }) => (
|
||||
<Text
|
||||
style={[
|
||||
styles.inLibraryBadge,
|
||||
@@ -305,8 +307,8 @@ const styles = StyleSheet.create({
|
||||
badgeContainer: {
|
||||
position: 'absolute',
|
||||
zIndex: 1,
|
||||
top: 8,
|
||||
left: 8,
|
||||
top: 10,
|
||||
left: 10,
|
||||
flexDirection: 'row',
|
||||
},
|
||||
});
|
||||
|
||||
@@ -0,0 +1,180 @@
|
||||
import React from 'react';
|
||||
import {
|
||||
Pressable,
|
||||
PressableProps,
|
||||
StyleSheet,
|
||||
Text,
|
||||
View,
|
||||
} from 'react-native';
|
||||
import FastImage from 'react-native-fast-image';
|
||||
import { useAppearanceSettings, useLibrarySettings } from '../../redux/hooks';
|
||||
|
||||
import { LibraryNovelInfo, NovelInfo } from '../../database/types';
|
||||
import { SourceNovelItem } from '../../sources/types';
|
||||
import { ThemeType } from '../../theme/types';
|
||||
import {
|
||||
UnreadBadge,
|
||||
DownloadsBadge,
|
||||
InLibraryBadge,
|
||||
} from './NovelCoverBadges';
|
||||
import { DisplayModes } from '../../redux/settings/settingsSlice';
|
||||
import { LinearGradient } from 'expo-linear-gradient';
|
||||
import { useDeviceOrientation, useNovelCoverHeight } from '../../hooks';
|
||||
import { DeviceOrientation } from '../../hooks/useDeviceOrientation';
|
||||
|
||||
interface Props extends PressableProps {
|
||||
item: LibraryNovelInfo | SourceNovelItem | NovelInfo;
|
||||
isSelected: boolean;
|
||||
theme: ThemeType;
|
||||
showInLibraryBadge?: boolean;
|
||||
}
|
||||
|
||||
const NovelItem: React.FC<Props> = props => {
|
||||
const { item, showInLibraryBadge = false, theme, isSelected = false } = props;
|
||||
|
||||
const { displayMode } = useAppearanceSettings();
|
||||
const { novelsPerRowLandscape, novelsPerRowPotrait } =
|
||||
useAppearanceSettings();
|
||||
const { showUnreadBadge, showDownloadsBadge } = useLibrarySettings();
|
||||
|
||||
const orientation = useDeviceOrientation();
|
||||
|
||||
const coverHeight = useNovelCoverHeight();
|
||||
|
||||
const coverSelectedStyle = {
|
||||
backgroundColor: theme.primary,
|
||||
opacity: 0.8,
|
||||
};
|
||||
|
||||
return (
|
||||
<View
|
||||
style={[
|
||||
{
|
||||
flex:
|
||||
1 /
|
||||
(orientation === DeviceOrientation.POTRAIT
|
||||
? novelsPerRowPotrait
|
||||
: novelsPerRowLandscape),
|
||||
},
|
||||
styles.container,
|
||||
isSelected && coverSelectedStyle,
|
||||
]}
|
||||
>
|
||||
<Pressable
|
||||
style={styles.pressable}
|
||||
android_ripple={{ color: theme.rippleColor }}
|
||||
{...props}
|
||||
>
|
||||
<View style={styles.badgeContainer}>
|
||||
{'chaptersDownloaded' in item &&
|
||||
item.chaptersDownloaded &&
|
||||
showDownloadsBadge ? (
|
||||
<DownloadsBadge
|
||||
unreadCount={item.chaptersUnread}
|
||||
downloadCount={item.chaptersDownloaded}
|
||||
showUnreadBadge={showUnreadBadge}
|
||||
theme={theme}
|
||||
/>
|
||||
) : null}
|
||||
{'chaptersUnread' in item &&
|
||||
item.chaptersUnread &&
|
||||
showUnreadBadge ? (
|
||||
<UnreadBadge
|
||||
unreadCount={item.chaptersUnread}
|
||||
downloadCount={item.chaptersDownloaded}
|
||||
showDownloadsBadge={showDownloadsBadge}
|
||||
theme={theme}
|
||||
/>
|
||||
) : null}
|
||||
</View>
|
||||
{showInLibraryBadge ? <InLibraryBadge theme={theme} /> : null}
|
||||
<FastImage
|
||||
source={{ uri: item.novelCover }}
|
||||
style={[
|
||||
{ height: coverHeight },
|
||||
styles.image,
|
||||
showInLibraryBadge && styles.inLibrary,
|
||||
]}
|
||||
>
|
||||
{displayMode === DisplayModes.Compact ? (
|
||||
<CompactTitle name={item.novelName} />
|
||||
) : null}
|
||||
</FastImage>
|
||||
{displayMode === DisplayModes.Comfortable ? (
|
||||
<Text
|
||||
numberOfLines={2}
|
||||
style={[{ color: theme.textColorPrimary }, styles.title]}
|
||||
>
|
||||
{item.novelName}
|
||||
</Text>
|
||||
) : null}
|
||||
</Pressable>
|
||||
</View>
|
||||
);
|
||||
};
|
||||
|
||||
export default NovelItem;
|
||||
|
||||
const CompactTitle = ({ name }: { name: string }) => (
|
||||
<View style={styles.compactTitleContainer}>
|
||||
<LinearGradient
|
||||
colors={['transparent', 'rgba(0,0,0,0.7)']}
|
||||
style={styles.linearGradient}
|
||||
>
|
||||
<Text numberOfLines={2} style={[styles.title, styles.compactTitle]}>
|
||||
{name}
|
||||
</Text>
|
||||
</LinearGradient>
|
||||
</View>
|
||||
);
|
||||
|
||||
const styles = StyleSheet.create({
|
||||
container: {
|
||||
overflow: 'hidden',
|
||||
borderRadius: 6,
|
||||
margin: 2,
|
||||
},
|
||||
pressable: {
|
||||
flex: 1,
|
||||
padding: 4,
|
||||
},
|
||||
badgeContainer: {
|
||||
position: 'absolute',
|
||||
zIndex: 1,
|
||||
top: 12,
|
||||
left: 12,
|
||||
flexDirection: 'row',
|
||||
},
|
||||
image: {
|
||||
borderRadius: 4,
|
||||
},
|
||||
inLibrary: {
|
||||
opacity: 0.5,
|
||||
},
|
||||
title: {
|
||||
padding: 4,
|
||||
fontSize: 12,
|
||||
},
|
||||
compactTitleContainer: {
|
||||
position: 'absolute',
|
||||
bottom: 0,
|
||||
left: 0,
|
||||
right: 0,
|
||||
flex: 1,
|
||||
borderRadius: 4,
|
||||
},
|
||||
linearGradient: {
|
||||
borderRadius: 4,
|
||||
padding: 4,
|
||||
paddingTop: 16,
|
||||
},
|
||||
compactTitle: {
|
||||
color: 'rgba(255,255,255,1)',
|
||||
textShadowColor: 'rgba(0, 0, 0, 0.75)',
|
||||
textShadowOffset: {
|
||||
width: -1,
|
||||
height: 1,
|
||||
},
|
||||
textShadowRadius: 10,
|
||||
},
|
||||
});
|
||||
@@ -0,0 +1,104 @@
|
||||
import React from 'react';
|
||||
import { StyleSheet, Text } from 'react-native';
|
||||
|
||||
import { ThemeType } from '../../theme/types';
|
||||
|
||||
interface UnreadBadgeProps {
|
||||
unreadCount: number;
|
||||
downloadCount: number;
|
||||
showDownloadsBadge: boolean;
|
||||
theme: ThemeType;
|
||||
}
|
||||
|
||||
const UnreadBadge: React.FC<UnreadBadgeProps> = ({
|
||||
unreadCount,
|
||||
downloadCount,
|
||||
showDownloadsBadge,
|
||||
theme,
|
||||
}) => (
|
||||
<Text
|
||||
style={[
|
||||
styles.unreadBadge,
|
||||
(!downloadCount || !showDownloadsBadge) && styles.borderLeftRadius,
|
||||
{ backgroundColor: theme.primary, color: theme.onPrimary },
|
||||
]}
|
||||
>
|
||||
{unreadCount}
|
||||
</Text>
|
||||
);
|
||||
|
||||
interface DownloadsBadgeProps {
|
||||
unreadCount: number;
|
||||
downloadCount: number;
|
||||
showUnreadBadge: boolean;
|
||||
theme: ThemeType;
|
||||
}
|
||||
|
||||
const DownloadsBadge: React.FC<DownloadsBadgeProps> = ({
|
||||
unreadCount,
|
||||
downloadCount,
|
||||
showUnreadBadge,
|
||||
theme,
|
||||
}) => (
|
||||
<Text
|
||||
style={[
|
||||
styles.downloadBadge,
|
||||
(!unreadCount || !showUnreadBadge) && styles.borderRightRadius,
|
||||
{ backgroundColor: theme.tertiary, color: theme.onTertiary },
|
||||
]}
|
||||
>
|
||||
{downloadCount}
|
||||
</Text>
|
||||
);
|
||||
|
||||
interface InLibraryBadgeProps {
|
||||
theme: ThemeType;
|
||||
}
|
||||
|
||||
const InLibraryBadge: React.FC<InLibraryBadgeProps> = ({ theme }) => (
|
||||
<Text
|
||||
style={[
|
||||
styles.inLibraryBadge,
|
||||
{ backgroundColor: theme.primary, color: theme.onPrimary },
|
||||
]}
|
||||
>
|
||||
In library
|
||||
</Text>
|
||||
);
|
||||
|
||||
export { UnreadBadge, DownloadsBadge, InLibraryBadge };
|
||||
|
||||
const styles = StyleSheet.create({
|
||||
borderLeftRadius: {
|
||||
borderTopLeftRadius: 4,
|
||||
borderBottomLeftRadius: 4,
|
||||
},
|
||||
borderRightRadius: {
|
||||
borderTopRightRadius: 4,
|
||||
borderBottomRightRadius: 4,
|
||||
},
|
||||
unreadBadge: {
|
||||
borderTopRightRadius: 4,
|
||||
borderBottomRightRadius: 4,
|
||||
paddingVertical: 2,
|
||||
paddingHorizontal: 4,
|
||||
fontSize: 12,
|
||||
},
|
||||
downloadBadge: {
|
||||
borderTopLeftRadius: 4,
|
||||
borderBottomLeftRadius: 4,
|
||||
paddingVertical: 2,
|
||||
paddingHorizontal: 4,
|
||||
fontSize: 12,
|
||||
},
|
||||
inLibraryBadge: {
|
||||
position: 'absolute',
|
||||
top: 12,
|
||||
right: 12,
|
||||
borderRadius: 4,
|
||||
paddingVertical: 2,
|
||||
paddingHorizontal: 4,
|
||||
fontSize: 12,
|
||||
zIndex: 2,
|
||||
},
|
||||
});
|
||||
@@ -1,8 +1,8 @@
|
||||
import React, {useCallback} from 'react';
|
||||
import {StyleSheet, FlatList} from 'react-native';
|
||||
import React, { useCallback } from 'react';
|
||||
import { StyleSheet, FlatList } from 'react-native';
|
||||
|
||||
import {useSettings} from '../hooks/reduxHooks';
|
||||
import {useDeviceOrientation} from '../services/utils/helpers';
|
||||
import { useSettings } from '../hooks/reduxHooks';
|
||||
import { useDeviceOrientation } from '../services/utils/helpers';
|
||||
|
||||
const NovelList = ({
|
||||
data,
|
||||
@@ -13,7 +13,7 @@ const NovelList = ({
|
||||
ListEmptyComponent,
|
||||
ListFooterComponent,
|
||||
}) => {
|
||||
const {displayMode, novelsPerRow} = useSettings();
|
||||
const { displayMode, novelsPerRow } = useSettings();
|
||||
|
||||
const orientation = useDeviceOrientation();
|
||||
|
||||
@@ -35,7 +35,7 @@ const NovelList = ({
|
||||
<FlatList
|
||||
contentContainerStyle={[
|
||||
styles.flatListCont,
|
||||
displayMode !== 2 && {paddingHorizontal: 4},
|
||||
displayMode !== 2 && { paddingHorizontal: 4 },
|
||||
]}
|
||||
numColumns={getNovelsPerRow()}
|
||||
key={[orientation, getNovelsPerRow()]}
|
||||
|
||||
@@ -0,0 +1,40 @@
|
||||
import React from 'react';
|
||||
import { FlatList, FlatListProps, StyleSheet } from 'react-native';
|
||||
|
||||
import { useAppearanceSettings } from '../../redux/hooks';
|
||||
import { useDeviceOrientation } from '../../hooks';
|
||||
|
||||
import { LibraryNovelInfo, NovelInfo } from '../../database/types';
|
||||
import { SourceNovelItem } from '../../sources/types';
|
||||
import { DeviceOrientation } from '../../hooks/useDeviceOrientation';
|
||||
|
||||
const NovelList: React.FC<
|
||||
FlatListProps<SourceNovelItem | NovelInfo | LibraryNovelInfo>
|
||||
> = props => {
|
||||
const orientation = useDeviceOrientation();
|
||||
|
||||
const { novelsPerRowPotrait, novelsPerRowLandscape } =
|
||||
useAppearanceSettings();
|
||||
|
||||
return (
|
||||
<FlatList
|
||||
contentContainerStyle={styles.container}
|
||||
numColumns={
|
||||
orientation === DeviceOrientation.LANDSCAPE
|
||||
? novelsPerRowLandscape
|
||||
: novelsPerRowPotrait
|
||||
}
|
||||
key={[orientation, novelsPerRowPotrait] as any}
|
||||
{...props}
|
||||
/>
|
||||
);
|
||||
};
|
||||
|
||||
export default NovelList;
|
||||
|
||||
const styles = StyleSheet.create({
|
||||
container: {
|
||||
flexGrow: 1,
|
||||
padding: 4,
|
||||
},
|
||||
});
|
||||
@@ -1,14 +1,14 @@
|
||||
import React from 'react';
|
||||
import {StyleSheet, View, Text} from 'react-native';
|
||||
import {RadioButton as MaterialRadioButton} from 'react-native-paper';
|
||||
import { StyleSheet, View, Text } from 'react-native';
|
||||
import { RadioButton as MaterialRadioButton } from 'react-native-paper';
|
||||
|
||||
export const RadioButtonGroup = ({children, value, onValueChange}) => (
|
||||
export const RadioButtonGroup = ({ children, value, onValueChange }) => (
|
||||
<MaterialRadioButton.Group onValueChange={onValueChange} value={value}>
|
||||
{children}
|
||||
</MaterialRadioButton.Group>
|
||||
);
|
||||
|
||||
export const RadioButton = ({value, label, theme, labelStyle}) => (
|
||||
export const RadioButton = ({ value, label, theme, labelStyle }) => (
|
||||
<View style={styles.radioButtonContainer}>
|
||||
<MaterialRadioButton
|
||||
value={value}
|
||||
@@ -18,7 +18,7 @@ export const RadioButton = ({value, label, theme, labelStyle}) => (
|
||||
<Text
|
||||
style={[
|
||||
styles.radioButtonLabel,
|
||||
{color: theme.textColorPrimary},
|
||||
{ color: theme.textColorPrimary },
|
||||
labelStyle,
|
||||
]}
|
||||
>
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import React from 'react';
|
||||
import {Pressable, StyleSheet, Text} from 'react-native';
|
||||
import {RadioButton as PaperRadioButton} from 'react-native-paper';
|
||||
import { Pressable, StyleSheet, Text } from 'react-native';
|
||||
import { RadioButton as PaperRadioButton } from 'react-native-paper';
|
||||
|
||||
export const RadioButton = ({
|
||||
label,
|
||||
@@ -11,7 +11,7 @@ export const RadioButton = ({
|
||||
theme,
|
||||
}) => (
|
||||
<Pressable
|
||||
android_ripple={{color: theme.rippleColor}}
|
||||
android_ripple={{ color: theme.rippleColor }}
|
||||
style={[styles.pressable, style]}
|
||||
onPress={onPress}
|
||||
>
|
||||
@@ -21,7 +21,9 @@ export const RadioButton = ({
|
||||
color={theme.colorAccent}
|
||||
uncheckedColor={theme.textColorSecondary}
|
||||
/>
|
||||
<Text style={[labelStyle, {color: theme.textColorPrimary, marginLeft: 12}]}>
|
||||
<Text
|
||||
style={[labelStyle, { color: theme.textColorPrimary, marginLeft: 12 }]}
|
||||
>
|
||||
{label}
|
||||
</Text>
|
||||
</Pressable>
|
||||
|
||||
@@ -1,8 +1,14 @@
|
||||
import React, {useRef} from 'react';
|
||||
import {StyleSheet, View, TextInput, StatusBar, Pressable} from 'react-native';
|
||||
import React, { useRef } from 'react';
|
||||
import {
|
||||
StyleSheet,
|
||||
View,
|
||||
TextInput,
|
||||
StatusBar,
|
||||
Pressable,
|
||||
} from 'react-native';
|
||||
|
||||
import {Row} from '../Common';
|
||||
import {IconButton} from '../IconButton/IconButton';
|
||||
import { Row } from '../Common';
|
||||
import { IconButton } from '../IconButton/IconButton';
|
||||
|
||||
export const Searchbar = ({
|
||||
theme,
|
||||
@@ -21,22 +27,26 @@ export const Searchbar = ({
|
||||
<View style={styles.searchBar}>
|
||||
<Pressable
|
||||
onPress={() => searchRef.current.focus()}
|
||||
style={[styles.pressable, {backgroundColor: theme.searchBarColor}]}
|
||||
android_ripple={{color: theme.rippleColor}}
|
||||
style={[styles.pressable, { backgroundColor: theme.searchBarColor }]}
|
||||
android_ripple={{ color: theme.rippleColor }}
|
||||
>
|
||||
<Row style={{flex: 1}}>
|
||||
<Row style={{ flex: 1 }}>
|
||||
{backAction && (
|
||||
<IconButton
|
||||
icon={backAction}
|
||||
color={theme.textColorPrimary}
|
||||
onPress={onBackAction}
|
||||
theme={theme}
|
||||
containerStyle={{marginLeft: 0}}
|
||||
containerStyle={{ marginLeft: 0 }}
|
||||
/>
|
||||
)}
|
||||
<TextInput
|
||||
ref={searchRef}
|
||||
style={{fontSize: 16, color: theme.textColorPrimary, marginLeft: 4}}
|
||||
style={{
|
||||
fontSize: 16,
|
||||
color: theme.textColorPrimary,
|
||||
marginLeft: 4,
|
||||
}}
|
||||
placeholder={placeholder}
|
||||
placeholderTextColor={theme.textColorSecondary}
|
||||
onChangeText={onChangeText}
|
||||
@@ -60,7 +70,7 @@ export const Searchbar = ({
|
||||
color={action.color ?? theme.textColorPrimary}
|
||||
onPress={action.onPress}
|
||||
theme={theme}
|
||||
containerStyle={{marginRight: 0, marginLeft: 16}}
|
||||
containerStyle={{ marginRight: 0, marginLeft: 16 }}
|
||||
/>
|
||||
))}
|
||||
</Pressable>
|
||||
|
||||
@@ -0,0 +1,130 @@
|
||||
import React, { useRef } from 'react';
|
||||
import { Pressable, StyleSheet, View, TextInput } from 'react-native';
|
||||
import { useSafeAreaInsets } from 'react-native-safe-area-context';
|
||||
|
||||
import { IconButtonV2 } from '..';
|
||||
import { ThemeTypeV1 } from '../../theme/v1/theme/types';
|
||||
|
||||
interface RightIcon {
|
||||
iconName: string;
|
||||
color?: string;
|
||||
onPress: () => void;
|
||||
}
|
||||
|
||||
interface SearcbarProps {
|
||||
searchText: string;
|
||||
placeholder: string;
|
||||
onChangeText?: (text: string) => void;
|
||||
onSubmitEditing?: () => void;
|
||||
leftIcon: string;
|
||||
rightIcons?: RightIcon[];
|
||||
handleBackAction?: () => void;
|
||||
clearSearchbar: () => void;
|
||||
onLeftIconPress?: () => void;
|
||||
theme: ThemeTypeV1;
|
||||
}
|
||||
|
||||
const Searchbar: React.FC<SearcbarProps> = ({
|
||||
searchText,
|
||||
placeholder,
|
||||
onChangeText,
|
||||
onSubmitEditing,
|
||||
leftIcon,
|
||||
rightIcons,
|
||||
handleBackAction,
|
||||
clearSearchbar,
|
||||
onLeftIconPress,
|
||||
theme,
|
||||
}) => {
|
||||
const searchbarRef = useRef<any>(null);
|
||||
const focusSearchbar = () => searchbarRef.current.focus();
|
||||
|
||||
const { top } = useSafeAreaInsets();
|
||||
const marginTop = top + 8;
|
||||
|
||||
return (
|
||||
<View
|
||||
style={[
|
||||
styles.searchbarContainer,
|
||||
{ marginTop, backgroundColor: theme.searchBarColor },
|
||||
]}
|
||||
>
|
||||
<Pressable
|
||||
onPress={focusSearchbar}
|
||||
android_ripple={{ color: theme.rippleColor }}
|
||||
style={[styles.searchbar]}
|
||||
>
|
||||
<IconButtonV2
|
||||
name={handleBackAction ? 'arrow-left' : leftIcon}
|
||||
color={theme.textColorPrimary}
|
||||
onPress={() => {
|
||||
if (handleBackAction) {
|
||||
handleBackAction();
|
||||
} else if (onLeftIconPress) {
|
||||
onLeftIconPress();
|
||||
}
|
||||
}}
|
||||
theme={theme}
|
||||
/>
|
||||
|
||||
<TextInput
|
||||
ref={searchbarRef}
|
||||
style={[styles.textInput, { color: theme.textColorPrimary }]}
|
||||
placeholder={placeholder}
|
||||
placeholderTextColor={theme.textColorSecondary}
|
||||
onChangeText={onChangeText}
|
||||
onSubmitEditing={onSubmitEditing}
|
||||
defaultValue={searchText}
|
||||
/>
|
||||
{searchText !== '' ? (
|
||||
<IconButtonV2
|
||||
name="close"
|
||||
color={theme.textColorPrimary}
|
||||
onPress={clearSearchbar}
|
||||
theme={theme}
|
||||
/>
|
||||
) : null}
|
||||
{rightIcons?.map((icon, index) => (
|
||||
<IconButtonV2
|
||||
key={index}
|
||||
name={icon.iconName}
|
||||
color={icon.color || theme.textColorPrimary}
|
||||
onPress={icon.onPress}
|
||||
theme={theme}
|
||||
/>
|
||||
))}
|
||||
</Pressable>
|
||||
</View>
|
||||
);
|
||||
};
|
||||
|
||||
export default Searchbar;
|
||||
|
||||
const styles = StyleSheet.create({
|
||||
searchbarContainer: {
|
||||
marginHorizontal: 16,
|
||||
marginBottom: 16,
|
||||
minHeight: 46,
|
||||
borderRadius: 50,
|
||||
overflow: 'hidden',
|
||||
zIndex: 1,
|
||||
},
|
||||
searchbar: {
|
||||
flex: 1,
|
||||
flexDirection: 'row',
|
||||
alignItems: 'center',
|
||||
paddingHorizontal: 8,
|
||||
},
|
||||
textInput: {
|
||||
flex: 1,
|
||||
fontSize: 16,
|
||||
marginHorizontal: 8,
|
||||
},
|
||||
icon: {
|
||||
marginHorizontal: 8,
|
||||
},
|
||||
searchIconContainer: {
|
||||
borderRadius: 50,
|
||||
overflow: 'hidden',
|
||||
},
|
||||
});
|
||||
@@ -1,19 +1,23 @@
|
||||
import React from 'react';
|
||||
import {Pressable, StyleSheet, View, Text} from 'react-native';
|
||||
import { Pressable, StyleSheet, View, Text } from 'react-native';
|
||||
|
||||
import {Switch, List} from 'react-native-paper';
|
||||
import { Switch, List } from 'react-native-paper';
|
||||
|
||||
const SwitchSetting = ({label, description, onPress, theme, icon, value}) => (
|
||||
const SwitchSetting = ({ label, description, onPress, theme, icon, value }) => (
|
||||
<Pressable
|
||||
android_ripple={{color: theme.rippleColor}}
|
||||
style={[styles.container, {paddingVertical: description ? 16 : 12}]}
|
||||
android_ripple={{ color: theme.rippleColor }}
|
||||
style={[styles.container, { paddingVertical: description ? 16 : 12 }]}
|
||||
onPress={onPress}
|
||||
>
|
||||
<View style={{flexDirection: 'row', flex: 1}}>
|
||||
<View style={{ flexDirection: 'row', flex: 1 }}>
|
||||
{icon && (
|
||||
<List.Icon color={theme.colorAccent} icon={icon} style={{margin: 0}} />
|
||||
<List.Icon
|
||||
color={theme.colorAccent}
|
||||
icon={icon}
|
||||
style={{ margin: 0 }}
|
||||
/>
|
||||
)}
|
||||
<View style={[icon && {marginLeft: 16}]}>
|
||||
<View style={[icon && { marginLeft: 16 }]}>
|
||||
<Text
|
||||
style={{
|
||||
color: theme.textColorPrimary,
|
||||
@@ -25,7 +29,7 @@ const SwitchSetting = ({label, description, onPress, theme, icon, value}) => (
|
||||
{label}
|
||||
</Text>
|
||||
{description && (
|
||||
<Text style={{color: theme.textColorSecondary}}>{description}</Text>
|
||||
<Text style={{ color: theme.textColorSecondary }}>{description}</Text>
|
||||
)}
|
||||
</View>
|
||||
</View>
|
||||
@@ -33,7 +37,7 @@ const SwitchSetting = ({label, description, onPress, theme, icon, value}) => (
|
||||
value={value}
|
||||
onValueChange={onPress}
|
||||
color={theme.colorAccent}
|
||||
style={{marginLeft: 8}}
|
||||
style={{ marginLeft: 8 }}
|
||||
/>
|
||||
</Pressable>
|
||||
);
|
||||
|
||||
@@ -0,0 +1,63 @@
|
||||
import React from 'react';
|
||||
import { Pressable, StyleSheet, View, Text } from 'react-native';
|
||||
|
||||
import { Switch } from 'react-native-paper';
|
||||
import { ThemeTypeV1 } from '../../theme/v1/theme/types';
|
||||
|
||||
interface SwitchItemProps {
|
||||
value: boolean;
|
||||
label: string;
|
||||
description?: string;
|
||||
onPress: () => void;
|
||||
theme: ThemeTypeV1;
|
||||
}
|
||||
|
||||
const SwitchItem: React.FC<SwitchItemProps> = ({
|
||||
label,
|
||||
description,
|
||||
onPress,
|
||||
theme,
|
||||
value,
|
||||
}) => (
|
||||
<Pressable
|
||||
android_ripple={{ color: theme.rippleColor }}
|
||||
style={[styles.container]}
|
||||
onPress={onPress}
|
||||
>
|
||||
<View style={styles.labelContainer}>
|
||||
<Text style={[{ color: theme.textColorPrimary }, styles.label]}>
|
||||
{label}
|
||||
</Text>
|
||||
{description ? (
|
||||
<Text style={{ color: theme.textColorSecondary }}>{description}</Text>
|
||||
) : null}
|
||||
</View>
|
||||
<Switch
|
||||
value={value}
|
||||
onValueChange={onPress}
|
||||
color={theme.colorAccent}
|
||||
style={styles.switch}
|
||||
/>
|
||||
</Pressable>
|
||||
);
|
||||
|
||||
export default SwitchItem;
|
||||
|
||||
const styles = StyleSheet.create({
|
||||
container: {
|
||||
flexDirection: 'row',
|
||||
justifyContent: 'space-between',
|
||||
paddingHorizontal: 16,
|
||||
paddingVertical: 12,
|
||||
},
|
||||
labelContainer: {
|
||||
flex: 1,
|
||||
},
|
||||
label: {
|
||||
fontSize: 16,
|
||||
textAlignVertical: 'center',
|
||||
},
|
||||
switch: {
|
||||
marginLeft: 8,
|
||||
},
|
||||
});
|
||||
@@ -1,8 +1,8 @@
|
||||
import React from 'react';
|
||||
import {View, Text, /* StyleSheet, */ Pressable} from 'react-native';
|
||||
import { View, Text, /* StyleSheet, */ Pressable } from 'react-native';
|
||||
import MaterialCommunityIcons from 'react-native-vector-icons/MaterialCommunityIcons';
|
||||
|
||||
export const ThemePicker = ({theme, currentTheme, onPress}) => (
|
||||
export const ThemePicker = ({ theme, currentTheme, onPress }) => (
|
||||
<View
|
||||
style={{
|
||||
justifyContent: 'center',
|
||||
@@ -25,7 +25,7 @@ export const ThemePicker = ({theme, currentTheme, onPress}) => (
|
||||
elevation: 1,
|
||||
}}
|
||||
>
|
||||
<Pressable style={{flex: 1}} onPress={onPress}>
|
||||
<Pressable style={{ flex: 1 }} onPress={onPress}>
|
||||
{currentTheme.id === theme.id && (
|
||||
<MaterialCommunityIcons
|
||||
name="check"
|
||||
@@ -60,7 +60,7 @@ export const ThemePicker = ({theme, currentTheme, onPress}) => (
|
||||
}}
|
||||
/>
|
||||
</View>
|
||||
<View style={{padding: 8}}>
|
||||
<View style={{ padding: 8 }}>
|
||||
<View
|
||||
style={{
|
||||
height: 18,
|
||||
@@ -68,7 +68,7 @@ export const ThemePicker = ({theme, currentTheme, onPress}) => (
|
||||
borderRadius: 4,
|
||||
}}
|
||||
/>
|
||||
<View style={{paddingVertical: 4, flexDirection: 'row'}}>
|
||||
<View style={{ paddingVertical: 4, flexDirection: 'row' }}>
|
||||
<View
|
||||
style={{
|
||||
height: 10,
|
||||
@@ -87,7 +87,7 @@ export const ThemePicker = ({theme, currentTheme, onPress}) => (
|
||||
}}
|
||||
/>
|
||||
</View>
|
||||
<View style={{flexDirection: 'row'}}>
|
||||
<View style={{ flexDirection: 'row' }}>
|
||||
<View
|
||||
style={{
|
||||
height: 10,
|
||||
|
||||
@@ -1,8 +1,8 @@
|
||||
import React from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
import {Text as NativeText, TextPropTypes} from 'react-native';
|
||||
import { Text as NativeText, TextPropTypes } from 'react-native';
|
||||
|
||||
const Text = ({type, children, style, theme}) => {
|
||||
const Text = ({ type, children, style, theme }) => {
|
||||
const color =
|
||||
type === 'primary'
|
||||
? theme.textColorPrimary
|
||||
@@ -10,7 +10,7 @@ const Text = ({type, children, style, theme}) => {
|
||||
? theme.textColorSecondary
|
||||
: theme.textColorHint;
|
||||
|
||||
return <NativeText style={[style, {color}]}>{children}</NativeText>;
|
||||
return <NativeText style={[style, { color }]}>{children}</NativeText>;
|
||||
};
|
||||
|
||||
Text.propType = {
|
||||
|
||||
@@ -0,0 +1,12 @@
|
||||
export { default as IconButtonV2 } from './IconButtonV2/IconButtonV2';
|
||||
export { default as SearchbarV2 } from './SearchbarV2/SearchbarV2';
|
||||
export { default as LoadingScreenV2 } from './LoadingScreenV2/LoadingScreenV2';
|
||||
export { default as Container } from './Container/Container';
|
||||
export { default as ErrorScreenV2 } from './ErrorScreenV2/ErrorScreenV2';
|
||||
export { default as EmptyView } from './EmptyView/EmptyView';
|
||||
export { default as NovelList } from './NovelList/NovelList';
|
||||
export { default as Chip } from './Chip/Chip';
|
||||
export { default as Button } from './Button/Button';
|
||||
export { default as Appbar } from './Appbar/Appbar';
|
||||
export { default as SwitchItem } from './SwitchItem/SwitchItem';
|
||||
export { default as List } from './List/List';
|
||||
@@ -13,15 +13,8 @@ import {
|
||||
createHistoryTableQuery,
|
||||
createChapterIdIndexQuery,
|
||||
} from './tables/HistoryTable';
|
||||
import {createDownloadTableQuery} from './tables/DownloadTable';
|
||||
import {
|
||||
createUpdatesTableQuery,
|
||||
// updatesSeedDataQuery,
|
||||
} from './tables/UpdateTable';
|
||||
|
||||
/**
|
||||
* Database Version = 2
|
||||
*/
|
||||
import { createDownloadTableQuery } from './tables/DownloadTable';
|
||||
import { createUpdatesTableQuery } from './tables/UpdateTable';
|
||||
|
||||
const dbName = 'lnreader.db';
|
||||
|
||||
@@ -1,5 +1,9 @@
|
||||
/* eslint-disable @typescript-eslint/no-unused-vars */
|
||||
|
||||
import * as SQLite from 'expo-sqlite';
|
||||
import {getSource} from '../../sources/sources';
|
||||
import { showToast } from '../../hooks/showToast';
|
||||
import { sourceManager } from '../../sources/sourceManager';
|
||||
import { ChapterItem } from '../types';
|
||||
const db = SQLite.openDatabase('lnreader.db');
|
||||
|
||||
const insertChaptersQuery =
|
||||
@@ -47,7 +51,7 @@ export const getChapters = (novelId, sort, filter) => {
|
||||
tx.executeSql(
|
||||
getChaptersQuery(sort, filter),
|
||||
[novelId],
|
||||
(txObj, {rows: {_array}}) => {
|
||||
(txObj, { rows: { _array } }) => {
|
||||
resolve(_array);
|
||||
},
|
||||
(txObj, error) => {
|
||||
@@ -77,35 +81,55 @@ export const getChapterFromDB = async chapterId => {
|
||||
);
|
||||
};
|
||||
|
||||
export const getNextChapterFromDB = async (novelId, chapterId) => {
|
||||
const getPrevChapterQuery = `
|
||||
SELECT
|
||||
*
|
||||
FROM
|
||||
chapters
|
||||
WHERE
|
||||
novelId = ?
|
||||
AND
|
||||
chapterId < ?
|
||||
`;
|
||||
|
||||
export const getPrevChapter = (
|
||||
novelId: number,
|
||||
chapterId: number,
|
||||
): Promise<ChapterItem> => {
|
||||
return new Promise((resolve, reject) =>
|
||||
db.transaction(tx => {
|
||||
tx.executeSql(
|
||||
'SELECT * FROM chapters WHERE novelId = ? AND chapterId > ?',
|
||||
getPrevChapterQuery,
|
||||
[novelId, chapterId],
|
||||
(txObj, results) => {
|
||||
resolve(results.rows.item(0));
|
||||
},
|
||||
(txObj, error) => {
|
||||
// console.log('Error ', error)
|
||||
},
|
||||
(txObj, results) => resolve(results.rows.item(results.rows.length - 1)),
|
||||
(txObj, error) => showToast(error.message),
|
||||
);
|
||||
}),
|
||||
);
|
||||
};
|
||||
|
||||
export const getPrevChapterFromDB = async (novelId, chapterId) => {
|
||||
const getNextChapterQuery = `
|
||||
SELECT
|
||||
*
|
||||
FROM
|
||||
chapters
|
||||
WHERE
|
||||
novelId = ?
|
||||
AND
|
||||
chapterId > ?
|
||||
`;
|
||||
|
||||
export const getNextChapter = (
|
||||
novelId: number,
|
||||
chapterId: number,
|
||||
): Promise<ChapterItem> => {
|
||||
return new Promise((resolve, reject) =>
|
||||
db.transaction(tx => {
|
||||
tx.executeSql(
|
||||
'SELECT * FROM chapters WHERE novelId = ? AND chapterId < ?',
|
||||
getNextChapterQuery,
|
||||
[novelId, chapterId],
|
||||
(txObj, results) => {
|
||||
resolve(results.rows.item(results.rows.length - 1));
|
||||
},
|
||||
(txObj, error) => {
|
||||
// console.log('Error ', error)
|
||||
},
|
||||
(txObj, results) => resolve(results.rows.item(0)),
|
||||
(txObj, error) => showToast(error.message),
|
||||
);
|
||||
}),
|
||||
);
|
||||
@@ -208,7 +232,7 @@ export const downloadChapter = async (
|
||||
chapterUrl,
|
||||
chapterId,
|
||||
) => {
|
||||
const source = getSource(sourceId);
|
||||
const source = sourceManager(sourceId);
|
||||
|
||||
const chapter = await source.parseChapter(novelUrl, chapterUrl);
|
||||
|
||||
@@ -270,7 +294,7 @@ export const getLastReadChapter = async novelId => {
|
||||
tx.executeSql(
|
||||
getLastReadChapterQuery,
|
||||
[novelId],
|
||||
(txObj, {rows}) => resolve(rows.item(0)),
|
||||
(txObj, { rows }) => resolve(rows.item(0)),
|
||||
(txObj, error) => {
|
||||
// console.log('Error ', error)
|
||||
},
|
||||
@@ -340,7 +364,7 @@ export const getDownloadedChapters = () => {
|
||||
tx.executeSql(
|
||||
getDownloadedChaptersQuery,
|
||||
null,
|
||||
(txObj, {rows: {_array}}) => {
|
||||
(txObj, { rows: { _array } }) => {
|
||||
resolve(_array);
|
||||
},
|
||||
(txObj, error) => {
|
||||
@@ -357,3 +381,19 @@ export const deleteDownloads = async () => {
|
||||
tx.executeSql('DELETE FROM downloads; VACCUM;');
|
||||
});
|
||||
};
|
||||
|
||||
export const updateChapterDownloadedQuery = `
|
||||
UPDATE
|
||||
chapters
|
||||
SET
|
||||
downloaded = 1
|
||||
WHERE
|
||||
chapterId = ?`;
|
||||
|
||||
export const updateChapterDeletedQuery = `
|
||||
UPDATE
|
||||
chapters
|
||||
SET
|
||||
downloaded = 0
|
||||
WHERE
|
||||
chapterId = ?`;
|
||||
@@ -0,0 +1,85 @@
|
||||
/* eslint-disable @typescript-eslint/no-unused-vars */
|
||||
|
||||
import * as SQLite from 'expo-sqlite';
|
||||
import { showToast } from '../../hooks/showToast';
|
||||
import { sourceManager } from '../../sources/sourceManager';
|
||||
import { DownloadedChapter } from '../types';
|
||||
|
||||
import {
|
||||
updateChapterDeletedQuery,
|
||||
updateChapterDownloadedQuery,
|
||||
} from './ChapterQueries';
|
||||
|
||||
const db = SQLite.openDatabase('lnreader.db');
|
||||
|
||||
const downloadChapterQuery = `
|
||||
INSERT INTO
|
||||
downloads (downloadChapterId, chapterName, chapterText)
|
||||
VALUES
|
||||
(?, ?, ?)
|
||||
`;
|
||||
|
||||
export const fetchAndInsertChapterInDb = async (
|
||||
sourceId: number,
|
||||
novelUrl: string,
|
||||
chapterId: number,
|
||||
chapterUrl: string,
|
||||
) => {
|
||||
const chapter = await sourceManager(sourceId).parseChapter(
|
||||
novelUrl,
|
||||
chapterUrl,
|
||||
);
|
||||
|
||||
db.transaction(tx => {
|
||||
tx.executeSql(updateChapterDownloadedQuery, [chapterId]);
|
||||
tx.executeSql(
|
||||
downloadChapterQuery,
|
||||
[chapterId, chapter.chapterName, chapter.chapterText],
|
||||
(txObj, res) => {},
|
||||
(_txObj, error) => showToast(error),
|
||||
);
|
||||
});
|
||||
};
|
||||
|
||||
const deleteChapterFromDbQuery = `
|
||||
DELETE FROM
|
||||
downloads
|
||||
WHERE
|
||||
downloadChapterId = ?
|
||||
`;
|
||||
|
||||
export const deleteChapterFromDb = (chapterId: number) => {
|
||||
db.transaction(tx => {
|
||||
tx.executeSql(updateChapterDeletedQuery, [chapterId]);
|
||||
tx.executeSql(
|
||||
deleteChapterFromDbQuery,
|
||||
[chapterId],
|
||||
(txObj, res) => {},
|
||||
(_txObj, error) => showToast(error.message),
|
||||
);
|
||||
});
|
||||
};
|
||||
|
||||
const getChapterFromDbQuery = `
|
||||
SELECT
|
||||
*
|
||||
FROM
|
||||
downloads
|
||||
WHERE
|
||||
downloadChapterId = ?
|
||||
`;
|
||||
|
||||
export const getChapterFromDb = async (
|
||||
chapterId: number,
|
||||
): Promise<DownloadedChapter> => {
|
||||
return new Promise((resolve, reject) =>
|
||||
db.transaction(tx => {
|
||||
tx.executeSql(
|
||||
getChapterFromDbQuery,
|
||||
[chapterId],
|
||||
(txObj, results) => resolve(results.rows.item(0)),
|
||||
(txObj, error) => reject(error),
|
||||
);
|
||||
}),
|
||||
);
|
||||
};
|
||||
@@ -1,7 +1,7 @@
|
||||
import * as SQLite from 'expo-sqlite';
|
||||
const db = SQLite.openDatabase('lnreader.db');
|
||||
|
||||
import {showToast} from '../../hooks/showToast';
|
||||
import { showToast } from '../../hooks/showToast';
|
||||
|
||||
const getHistoryQuery = `
|
||||
SELECT history.*, chapters.*, novels.*
|
||||
@@ -21,7 +21,7 @@ export const getHistoryFromDb = async () => {
|
||||
tx.executeSql(
|
||||
getHistoryQuery,
|
||||
null,
|
||||
(txObj, {rows: {_array}}) => {
|
||||
(txObj, { rows: { _array } }) => {
|
||||
resolve(_array);
|
||||
},
|
||||
(txObj, error) => {
|
||||
@@ -48,7 +48,7 @@ export const insertHistory = async (novelId, chapterId) => {
|
||||
});
|
||||
};
|
||||
|
||||
export const deleteHistory = async historyId => {
|
||||
export const deleteChapterHistory = async historyId => {
|
||||
db.transaction(tx => {
|
||||
tx.executeSql(
|
||||
'DELETE FROM history WHERE historyId = ?',
|
||||
|
||||
@@ -28,7 +28,7 @@ export const getLibrary = (sort, filter) => {
|
||||
tx.executeSql(
|
||||
getLibraryQuery(sort, filter),
|
||||
null,
|
||||
(txObj, {rows: {_array}}) => {
|
||||
(txObj, { rows: { _array } }) => {
|
||||
// console.log(_array);
|
||||
resolve(_array);
|
||||
},
|
||||
@@ -69,7 +69,7 @@ export const searchLibrary = (searchText, sort, filter) => {
|
||||
tx.executeSql(
|
||||
searchLibraryQuery(searchText, sort, filter),
|
||||
null,
|
||||
(txObj, {rows: {_array}}) => resolve(_array),
|
||||
(txObj, { rows: { _array } }) => resolve(_array),
|
||||
(txObj, error) => {
|
||||
// console.log('Error ', error);
|
||||
},
|
||||
|
||||
@@ -4,10 +4,10 @@ const db = SQLite.openDatabase('lnreader.db');
|
||||
import BackgroundService from 'react-native-background-actions';
|
||||
import * as DocumentPicker from 'expo-document-picker';
|
||||
|
||||
import {fetchChapters, fetchNovel} from '../../services/Source/source';
|
||||
import {insertChapters} from './ChapterQueries';
|
||||
import { fetchChapters, fetchNovel } from '../../services/Source/source';
|
||||
import { insertChapters } from './ChapterQueries';
|
||||
|
||||
import {showToast} from '../../hooks/showToast';
|
||||
import { showToast } from '../../hooks/showToast';
|
||||
|
||||
const insertNovelQuery =
|
||||
'INSERT INTO novels (novelUrl, sourceUrl, sourceId, source, novelName, novelCover, novelSummary, author, artist, status, genre) VALUES(?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)';
|
||||
@@ -23,14 +23,14 @@ export const insertNovel = async novel => {
|
||||
novel.sourceId,
|
||||
novel.source,
|
||||
novel.novelName,
|
||||
novel.novelCover,
|
||||
novel.novelCover || '',
|
||||
novel.novelSummary,
|
||||
novel.author,
|
||||
novel.artist,
|
||||
novel.status,
|
||||
novel.genre,
|
||||
],
|
||||
(txObj, {insertId}) => resolve(insertId),
|
||||
(txObj, { insertId }) => resolve(insertId),
|
||||
(txObj, error) => {
|
||||
// console.log('Error ', error)
|
||||
},
|
||||
@@ -94,7 +94,7 @@ export const getNovel = async (sourceId, novelUrl) => {
|
||||
tx.executeSql(
|
||||
'SELECT * FROM novels WHERE novelUrl = ? AND sourceId = ?',
|
||||
[novelUrl, sourceId],
|
||||
(txObj, {rows}) => resolve(rows.item(0)),
|
||||
(txObj, { rows }) => resolve(rows.item(0)),
|
||||
(txObj, error) => {
|
||||
// console.log('Error ', error)
|
||||
},
|
||||
@@ -120,7 +120,7 @@ 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) =>
|
||||
return new Promise((resolve, reject) => {
|
||||
db.transaction(tx =>
|
||||
tx.executeSql(
|
||||
restoreFromBackupQuery,
|
||||
@@ -139,16 +139,21 @@ export const restoreLibrary = async novel => {
|
||||
novel.followed,
|
||||
novel.unread,
|
||||
],
|
||||
async (txObj, {insertId}) => {
|
||||
async (txObj, { insertId }) => {
|
||||
const chapters = await fetchChapters(novel.sourceId, novel.novelUrl);
|
||||
await insertChapters(insertId, chapters);
|
||||
|
||||
if (chapters) {
|
||||
await insertChapters(insertId, chapters);
|
||||
|
||||
resolve();
|
||||
}
|
||||
},
|
||||
(txObj, error) => {
|
||||
// console.log('Error ', error)
|
||||
resolve();
|
||||
},
|
||||
),
|
||||
),
|
||||
);
|
||||
);
|
||||
});
|
||||
};
|
||||
|
||||
const migrateNovelQuery =
|
||||
@@ -196,7 +201,7 @@ export const migrateNovel = async (sourceId, novelUrl) => {
|
||||
novel.genre,
|
||||
1,
|
||||
],
|
||||
async (txObj, {insertId}) => {
|
||||
async (txObj, { insertId }) => {
|
||||
const chapters = await fetchChapters(
|
||||
novel.sourceId,
|
||||
novel.novelUrl,
|
||||
@@ -214,7 +219,7 @@ export const migrateNovel = async (sourceId, novelUrl) => {
|
||||
|
||||
await BackgroundService.start(veryIntensiveTask, options);
|
||||
await BackgroundService.updateNotification({
|
||||
progressBar: {value: 1, indeterminate: false},
|
||||
progressBar: { value: 1, indeterminate: false },
|
||||
});
|
||||
} catch (error) {
|
||||
showToast(error.message);
|
||||
@@ -242,7 +247,7 @@ export const updateNovelInfo = async (info, novelId) => {
|
||||
};
|
||||
|
||||
export const pickCustomNovelCover = async novelId => {
|
||||
const image = await DocumentPicker.getDocumentAsync({type: 'image/*'});
|
||||
const image = await DocumentPicker.getDocumentAsync({ type: 'image/*' });
|
||||
|
||||
if (image.type === 'success' && image.uri) {
|
||||
const uri = 'file://' + image.uri;
|
||||
|
||||
@@ -1,46 +0,0 @@
|
||||
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
|
||||
FROM updates
|
||||
JOIN chapters
|
||||
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)
|
||||
},
|
||||
);
|
||||
});
|
||||
});
|
||||
};
|
||||
|
||||
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;'));
|
||||
@@ -0,0 +1,104 @@
|
||||
/* eslint-disable @typescript-eslint/no-unused-vars */
|
||||
|
||||
import * as SQLite from 'expo-sqlite';
|
||||
import { showToast } from '../../hooks/showToast';
|
||||
import { Update } from '../types';
|
||||
|
||||
const db = SQLite.openDatabase('lnreader.db');
|
||||
|
||||
const getUpdatesFromDbQuery = `
|
||||
SELECT
|
||||
chapters.chapterId,
|
||||
chapters.read,
|
||||
chapters.downloaded,
|
||||
chapters.chapterUrl,
|
||||
chapters.chapterName,
|
||||
chapters.bookmark,
|
||||
chapters.releaseDate,
|
||||
novels.novelUrl,
|
||||
novels.novelId,
|
||||
novels.novelCover,
|
||||
novels.novelName,
|
||||
novels.sourceId,
|
||||
updates.updateTime,
|
||||
updates.updateId
|
||||
FROM
|
||||
updates
|
||||
JOIN
|
||||
chapters
|
||||
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 getUpdatesFromDb = async (): Promise<Update[]> => {
|
||||
return new Promise((resolve, reject) => {
|
||||
db.transaction(tx => {
|
||||
tx.executeSql(
|
||||
getUpdatesFromDbQuery,
|
||||
[],
|
||||
(txObj, { rows: { _array } }) => resolve(_array),
|
||||
(txObj, error) => reject(error),
|
||||
);
|
||||
});
|
||||
});
|
||||
};
|
||||
|
||||
const insertChapterUpdateQuery = `
|
||||
INSERT OR IGNORE INTO
|
||||
updates
|
||||
(chapterId, novelId, updateTime)
|
||||
VALUES
|
||||
(?, ?, (datetime('now','localtime')))`;
|
||||
|
||||
export const insertChapterUpdate = async (
|
||||
chapterId: number,
|
||||
novelId: number,
|
||||
) => {
|
||||
db.transaction(tx => {
|
||||
tx.executeSql(
|
||||
insertChapterUpdateQuery,
|
||||
[chapterId, novelId],
|
||||
(txOBJ, res) => {},
|
||||
(txOBJ, error) => showToast(error.message),
|
||||
);
|
||||
});
|
||||
};
|
||||
|
||||
const deleteUpdateFromDbQuery = `
|
||||
DELETE FROM
|
||||
updates
|
||||
WHERE
|
||||
novelId = ?`;
|
||||
|
||||
export const deleteUpdateFromDb = (novelId: number) => {
|
||||
db.transaction(tx => {
|
||||
tx.executeSql(
|
||||
deleteUpdateFromDbQuery,
|
||||
[novelId],
|
||||
(txObj, res) => {},
|
||||
(txObj, error) => showToast(error.message),
|
||||
);
|
||||
});
|
||||
};
|
||||
const clearUpdatesQuery = `
|
||||
DELETE FROM updates;
|
||||
VACCUM;
|
||||
`;
|
||||
|
||||
export const clearUpdates = () =>
|
||||
db.transaction(tx =>
|
||||
tx.executeSql(
|
||||
clearUpdatesQuery,
|
||||
[],
|
||||
() => showToast('Updates cleared.'),
|
||||
(txObj, error) => showToast(error.message),
|
||||
),
|
||||
);
|
||||
@@ -6,7 +6,7 @@ export const createNovelTableQuery = `
|
||||
sourceId INTEGER NOT NULL,
|
||||
source TEXT NOT NULL,
|
||||
novelName TEXT NOT NULL,
|
||||
novelCover TEXT NOT NULL,
|
||||
novelCover TEXT,
|
||||
novelSummary TEXT,
|
||||
author TEXT,
|
||||
artist TEXT,
|
||||
|
||||
@@ -0,0 +1,68 @@
|
||||
export interface NovelInfo {
|
||||
novelId: number;
|
||||
sourceUrl: string;
|
||||
novelUrl: string;
|
||||
sourceId: number;
|
||||
source: string;
|
||||
novelName: string;
|
||||
novelCover?: string;
|
||||
novelSummary?: string;
|
||||
genre?: string;
|
||||
author?: string;
|
||||
status?: string;
|
||||
followed: number;
|
||||
}
|
||||
|
||||
export interface LibraryNovelInfo extends NovelInfo {
|
||||
chaptersUnread: number;
|
||||
chaptersDownloaded: number;
|
||||
}
|
||||
|
||||
export interface ChapterItem {
|
||||
chapterId: number;
|
||||
novelId: number;
|
||||
chapterUrl: string;
|
||||
chapterName: string;
|
||||
releaseDate?: string;
|
||||
read: number;
|
||||
bookmark: number;
|
||||
downloaded: number;
|
||||
}
|
||||
|
||||
export interface DownloadedChapter {
|
||||
downloadId: number;
|
||||
downloadChapterId: number;
|
||||
chapterName: string | null;
|
||||
chapterText: string;
|
||||
}
|
||||
|
||||
export interface History {
|
||||
historyId: number;
|
||||
sourceId: number;
|
||||
novelId: number;
|
||||
chapterId: number;
|
||||
novelName: string;
|
||||
novelUrl: string;
|
||||
novelCover: string;
|
||||
chapterName: string;
|
||||
chapterUrl: string;
|
||||
historyTimeRead: string;
|
||||
bookmark: number;
|
||||
}
|
||||
|
||||
export interface Update {
|
||||
updateId: number;
|
||||
sourceId: number;
|
||||
novelId: number;
|
||||
novelName: string;
|
||||
novelUrl: string;
|
||||
novelCover: string;
|
||||
chapterId: number;
|
||||
chapterUrl: string;
|
||||
chapterName: string;
|
||||
downloaded: number;
|
||||
read: number;
|
||||
updateTime: string;
|
||||
bookmark: number;
|
||||
releaseDate: string;
|
||||
}
|
||||
@@ -0,0 +1,13 @@
|
||||
export const convertDateToISOString = (SQLiteDate: string) => {
|
||||
const dateParts: string[] = SQLiteDate.split('-');
|
||||
|
||||
const JSDate = new Date(
|
||||
Number(dateParts[0]),
|
||||
Number(dateParts[1]) - 1,
|
||||
Number(dateParts[2].substring(0, 2)),
|
||||
);
|
||||
|
||||
const date = JSDate.toISOString();
|
||||
|
||||
return date;
|
||||
};
|
||||
@@ -1,5 +1,5 @@
|
||||
import {useState, useEffect} from 'react';
|
||||
import {appversion} from '../utils/utils';
|
||||
import { useState, useEffect } from 'react';
|
||||
import { appVersion } from '../utils/versionUtils';
|
||||
|
||||
export const useGithubUpdateChecker = () => {
|
||||
const latestReleaseUrl =
|
||||
@@ -23,7 +23,7 @@ export const useGithubUpdateChecker = () => {
|
||||
};
|
||||
|
||||
const isNewVersion = versionTag => {
|
||||
let currentVersion = `${appversion}`;
|
||||
let currentVersion = `${appVersion}`;
|
||||
const regex = /[^\\d.]/;
|
||||
|
||||
let newVersion = versionTag.replace(regex, '');
|
||||
|
||||
@@ -0,0 +1,9 @@
|
||||
export { default as useSearch } from './useSearch';
|
||||
export { default as useHistory } from './useHistory';
|
||||
export { default as useUpdates } from './useUpdates';
|
||||
export { default as useFullscreenMode } from './useFullscreenMode';
|
||||
export { default as useChapter } from './useChapter';
|
||||
export { default as useNextAndPrevChapter } from './useNextAndPrevChapter';
|
||||
export { default as useDeviceOrientation } from './useDeviceOrientation';
|
||||
export { default as useNovelCoverHeight } from './useNovelCoverHeight';
|
||||
export { default as useNovelTrackerInfo } from './useNovelTrackerInfo';
|
||||
@@ -1,5 +1,5 @@
|
||||
import {useSelector} from 'react-redux';
|
||||
import {darkTheme} from '../theme/theme';
|
||||
import { useSelector } from 'react-redux';
|
||||
import { darkTheme } from '../theme/v1/theme/theme';
|
||||
|
||||
const useTheme = () => {
|
||||
const theme = useSelector(state => state.settingsReducer.theme);
|
||||
@@ -56,7 +56,7 @@ const usePreferences = novelId => {
|
||||
showChapterTitles = novel.showChapterTitles;
|
||||
}
|
||||
|
||||
return {sort, filter, position, showChapterTitles};
|
||||
return { sort, filter, position, showChapterTitles };
|
||||
};
|
||||
|
||||
const useSavedSettings = () => {
|
||||
@@ -80,11 +80,14 @@ const useContinueReading = (chapters, novelId) => {
|
||||
);
|
||||
}
|
||||
|
||||
// If the last read chapter is 100% done, set the next chapter as the 'last read'.
|
||||
// If all chapters are read, then set the last chapter in the list as the last read (Fixed bug)
|
||||
if (!lastReadChapter) {
|
||||
lastReadChapter = chapters.find(obj => obj.read === 0);
|
||||
lastReadChapter =
|
||||
chapters.find(obj => obj.read === 0) || chapters[chapters.length - 1];
|
||||
}
|
||||
|
||||
return {lastReadChapter, position};
|
||||
return { lastReadChapter, position };
|
||||
};
|
||||
|
||||
const usePosition = (novelId, chapterId) => {
|
||||
|
||||
@@ -1,5 +0,0 @@
|
||||
import {ToastAndroid} from 'react-native';
|
||||
|
||||
export const showToast = message => {
|
||||
ToastAndroid.show(message, ToastAndroid.SHORT);
|
||||
};
|
||||
@@ -0,0 +1,4 @@
|
||||
import { ToastAndroid } from 'react-native';
|
||||
|
||||
export const showToast = (message: string) =>
|
||||
ToastAndroid.show(message, ToastAndroid.SHORT);
|
||||
@@ -0,0 +1,57 @@
|
||||
import { useCallback, useEffect, useState } from 'react';
|
||||
import sanitize from 'sanitize-html';
|
||||
import { defaultTo } from 'lodash';
|
||||
import { getChapterFromDb } from '../database/queries/DownloadQueries';
|
||||
|
||||
import { sourceManager } from '../sources/sourceManager';
|
||||
|
||||
import { DownloadedChapter } from '../database/types';
|
||||
import { SourceChapter } from '../sources/types';
|
||||
|
||||
type Chapter = SourceChapter | DownloadedChapter;
|
||||
|
||||
const useChapter = (
|
||||
sourceId: number,
|
||||
chapterId: number,
|
||||
chapterUrl: string,
|
||||
novelUrl: string,
|
||||
) => {
|
||||
const [isLoading, setIsLoading] = useState<boolean>(true);
|
||||
const [chapter, setChapter] = useState<Chapter>();
|
||||
const [error, setError] = useState<string>();
|
||||
|
||||
const getChapter = useCallback(async () => {
|
||||
try {
|
||||
let res: Chapter = await getChapterFromDb(chapterId);
|
||||
|
||||
if (!res) {
|
||||
res = await sourceManager(sourceId).parseChapter(novelUrl, chapterUrl);
|
||||
}
|
||||
|
||||
let chapterText = sanitize(
|
||||
defaultTo(
|
||||
res.chapterText,
|
||||
"Chapter is empty. Report if it's available in WebView.",
|
||||
),
|
||||
);
|
||||
|
||||
setChapter({ ...res, chapterText });
|
||||
} catch (err: unknown) {
|
||||
if (typeof err === 'string') {
|
||||
setError(err);
|
||||
} else if (err instanceof Error) {
|
||||
setError(err.message);
|
||||
}
|
||||
} finally {
|
||||
setIsLoading(false);
|
||||
}
|
||||
}, [sourceId, chapterId, chapterUrl, novelUrl]);
|
||||
|
||||
useEffect(() => {
|
||||
getChapter();
|
||||
}, [getChapter]);
|
||||
|
||||
return { isLoading, setIsLoading, chapter, error };
|
||||
};
|
||||
|
||||
export default useChapter;
|
||||
@@ -0,0 +1,65 @@
|
||||
import { useEffect, useRef, useState } from 'react';
|
||||
|
||||
import { useAppDispatch, useSavedChapterData } from '../redux/hooks';
|
||||
import { saveChapterProgress } from '../redux/localStorage/localStorageSlice';
|
||||
|
||||
export const useChapterProgress = (chapterId: number, readerRef: any) => {
|
||||
const dispatch = useAppDispatch();
|
||||
const { progressPercentage = 0, progressLocation = 0 } =
|
||||
useSavedChapterData(chapterId);
|
||||
|
||||
const [percentage, setPercentage] = useState(progressPercentage);
|
||||
const [location, setLocation] = useState(progressLocation);
|
||||
|
||||
const percentageRef = useRef(0);
|
||||
percentageRef.current = percentage;
|
||||
|
||||
const locationRef = useRef(0);
|
||||
locationRef.current = location;
|
||||
|
||||
const setProgress = ({ nativeEvent }: { nativeEvent: any }) => {
|
||||
const currentOffsetY = nativeEvent.contentOffset.y;
|
||||
const currentPosition =
|
||||
nativeEvent.contentOffset.y + nativeEvent.layoutMeasurement.height;
|
||||
|
||||
const currentProgressPercentage = Math.round(
|
||||
(currentPosition / nativeEvent.contentSize.height) * 100,
|
||||
);
|
||||
|
||||
setPercentage(currentProgressPercentage);
|
||||
setLocation(currentOffsetY);
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
return () => {
|
||||
dispatch(
|
||||
saveChapterProgress({
|
||||
chapterId,
|
||||
progressLocation: locationRef.current,
|
||||
progressPercentage: percentageRef.current,
|
||||
}),
|
||||
);
|
||||
};
|
||||
}, []);
|
||||
|
||||
const [firstLayout, setFirstLayout] = useState(true);
|
||||
|
||||
const scrollToSavedProgress = () => {
|
||||
if (firstLayout) {
|
||||
percentage < 100 &&
|
||||
readerRef.current.scrollTo({
|
||||
x: 0,
|
||||
y: location,
|
||||
animated: false,
|
||||
});
|
||||
setFirstLayout(false);
|
||||
}
|
||||
};
|
||||
|
||||
return {
|
||||
progressLocation,
|
||||
progressPercentage: percentage,
|
||||
setProgress,
|
||||
scrollToSavedProgress,
|
||||
};
|
||||
};
|
||||
@@ -0,0 +1,18 @@
|
||||
import { useWindowDimensions } from 'react-native';
|
||||
|
||||
export enum DeviceOrientation {
|
||||
POTRAIT = 'potrait',
|
||||
LANDSCAPE = 'landscape',
|
||||
}
|
||||
|
||||
const useDeviceOrientation = (): DeviceOrientation => {
|
||||
const window = useWindowDimensions();
|
||||
|
||||
if (window.width > window.height) {
|
||||
return DeviceOrientation.LANDSCAPE;
|
||||
} else {
|
||||
return DeviceOrientation.POTRAIT;
|
||||
}
|
||||
};
|
||||
|
||||
export default useDeviceOrientation;
|
||||
@@ -0,0 +1,53 @@
|
||||
import { useCallback, useEffect } from 'react';
|
||||
import { StatusBar } from 'react-native';
|
||||
import { useNavigation } from '@react-navigation/native';
|
||||
|
||||
import { useReaderSettingsV1, useSettingsV1, useThemeV1 } from '../redux/hooks';
|
||||
import {
|
||||
changeNavigationBarColor,
|
||||
hideNavigationBar,
|
||||
showNavigationBar,
|
||||
} from '../theme/NativeModules/NavigationBarColor';
|
||||
import { readerBackground } from '../screens/reader/utils/readerStyles';
|
||||
|
||||
const useFullscreenMode = () => {
|
||||
const { addListener } = useNavigation();
|
||||
const readerSettings = useReaderSettingsV1();
|
||||
const { fullScreenMode } = useSettingsV1();
|
||||
|
||||
const backgroundColor = readerBackground(readerSettings.theme);
|
||||
|
||||
const theme = useThemeV1();
|
||||
|
||||
const setImmersiveMode = useCallback(() => {
|
||||
if (fullScreenMode) {
|
||||
StatusBar.setHidden(true);
|
||||
hideNavigationBar();
|
||||
} else {
|
||||
StatusBar.setBackgroundColor(backgroundColor);
|
||||
changeNavigationBarColor(backgroundColor as any);
|
||||
}
|
||||
}, [backgroundColor, fullScreenMode]);
|
||||
|
||||
const showStatusAndNavBar = useCallback(() => {
|
||||
StatusBar.setHidden(false);
|
||||
showNavigationBar();
|
||||
}, []);
|
||||
|
||||
useEffect(() => {
|
||||
setImmersiveMode();
|
||||
}, [setImmersiveMode]);
|
||||
|
||||
useEffect(() => {
|
||||
const unsubscribe = addListener('beforeRemove', () => {
|
||||
showStatusAndNavBar();
|
||||
StatusBar.setBarStyle(theme.statusBar);
|
||||
});
|
||||
|
||||
return unsubscribe;
|
||||
}, []);
|
||||
|
||||
return { setImmersiveMode, showStatusAndNavBar };
|
||||
};
|
||||
|
||||
export default useFullscreenMode;
|
||||
@@ -0,0 +1,51 @@
|
||||
import { useCallback, useState } from 'react';
|
||||
import { useFocusEffect } from '@react-navigation/native';
|
||||
|
||||
import { History } from '../database/types';
|
||||
|
||||
import {
|
||||
deleteAllHistory,
|
||||
deleteChapterHistory,
|
||||
getHistoryFromDb,
|
||||
} from '../database/queries/HistoryQueries';
|
||||
|
||||
const useHistory = () => {
|
||||
const [isLoading, setIsLoading] = useState(true);
|
||||
const [history, setHistory] = useState<History[]>([]);
|
||||
const [error, setError] = useState<string>();
|
||||
|
||||
const getHistory = async () => {
|
||||
try {
|
||||
const res = await getHistoryFromDb();
|
||||
setHistory(res);
|
||||
} catch (err) {
|
||||
if (err instanceof Error) {
|
||||
setError(err.message);
|
||||
}
|
||||
} finally {
|
||||
setIsLoading(false);
|
||||
}
|
||||
};
|
||||
|
||||
const clearAllHistory = () => deleteAllHistory();
|
||||
|
||||
const removeChapterFromHistory = async (historyId: number) => {
|
||||
deleteChapterHistory(historyId);
|
||||
getHistory();
|
||||
};
|
||||
|
||||
useFocusEffect(
|
||||
useCallback(() => {
|
||||
getHistory();
|
||||
}, []),
|
||||
);
|
||||
return {
|
||||
isLoading,
|
||||
history,
|
||||
error,
|
||||
removeChapterFromHistory,
|
||||
clearAllHistory,
|
||||
};
|
||||
};
|
||||
|
||||
export default useHistory;
|
||||
@@ -0,0 +1,61 @@
|
||||
import { useCallback, useState } from 'react';
|
||||
import { useFocusEffect } from '@react-navigation/native';
|
||||
|
||||
import { LibraryNovelInfo } from '../database/types';
|
||||
import { getLibraryNovelsFromDb } from '../database/queries/LibraryQueries';
|
||||
import { useLibrarySettings } from '../redux/hooks';
|
||||
|
||||
const useLibrary = () => {
|
||||
const [isLoading, setIsLoading] = useState(true);
|
||||
const [novels, setNovels] = useState<LibraryNovelInfo[]>([]);
|
||||
const [error, setError] = useState<string>();
|
||||
|
||||
const { filters, sort } = useLibrarySettings();
|
||||
|
||||
const getLibraryNovels = useCallback(async () => {
|
||||
try {
|
||||
const res = await getLibraryNovelsFromDb(filters, sort);
|
||||
setNovels(res);
|
||||
} catch (err) {
|
||||
if (err instanceof Error) {
|
||||
setError(err.message);
|
||||
}
|
||||
} finally {
|
||||
setIsLoading(false);
|
||||
}
|
||||
}, [filters, sort]);
|
||||
|
||||
const getLibrarySearchResults = useCallback(
|
||||
async (searchText: string) => {
|
||||
try {
|
||||
const res = await getLibraryNovelsFromDb(filters, sort, searchText);
|
||||
setNovels(res);
|
||||
} catch (err) {
|
||||
if (err instanceof Error) {
|
||||
setError(err.message);
|
||||
}
|
||||
} finally {
|
||||
setIsLoading(false);
|
||||
}
|
||||
},
|
||||
[filters, sort],
|
||||
);
|
||||
|
||||
const clearSearchResults = () => getLibraryNovels();
|
||||
|
||||
useFocusEffect(
|
||||
useCallback(() => {
|
||||
getLibraryNovels();
|
||||
}, [getLibraryNovels]),
|
||||
);
|
||||
return {
|
||||
isLoading,
|
||||
novels,
|
||||
error,
|
||||
getLibrarySearchResults,
|
||||
clearSearchResults,
|
||||
getLibraryNovels,
|
||||
};
|
||||
};
|
||||
|
||||
export default useLibrary;
|
||||
@@ -1,4 +1,4 @@
|
||||
import {useState} from 'react';
|
||||
import { useState } from 'react';
|
||||
|
||||
export const useModal = () => {
|
||||
const [visible, setVisible] = useState(false);
|
||||
@@ -6,5 +6,5 @@ export const useModal = () => {
|
||||
const showModal = () => setVisible(true);
|
||||
const hideModal = () => setVisible(false);
|
||||
|
||||
return {visible, showModal, hideModal};
|
||||
return { visible, showModal, hideModal };
|
||||
};
|
||||
|
||||
@@ -0,0 +1,83 @@
|
||||
import { useCallback, useEffect, useState } from 'react';
|
||||
import { StackActions, useNavigation } from '@react-navigation/native';
|
||||
import {
|
||||
getNextChapter,
|
||||
getPrevChapter,
|
||||
} from '../database/queries/ChapterQueries';
|
||||
|
||||
import { ChapterItem } from '../database/types';
|
||||
import { showToast } from '../hooks/showToast';
|
||||
|
||||
const useNextAndPrevChapter = (
|
||||
sourceId: number,
|
||||
novelId: number,
|
||||
chapterId: number,
|
||||
) => {
|
||||
const navigation = useNavigation();
|
||||
const [isLoading, setIsLoading] = useState(true);
|
||||
|
||||
const [nextChapter, setNextChapter] = useState<ChapterItem>();
|
||||
const [previousChapter, setPreviousChapter] = useState<ChapterItem>();
|
||||
|
||||
const getNextAndPrevChapter = useCallback(async () => {
|
||||
try {
|
||||
const nextChap = await getNextChapter(novelId, chapterId);
|
||||
const prevChap = await getPrevChapter(novelId, chapterId);
|
||||
|
||||
setNextChapter(nextChap);
|
||||
setPreviousChapter(prevChap);
|
||||
} catch (err: unknown) {
|
||||
if (typeof err === 'string') {
|
||||
showToast(err);
|
||||
} else if (err instanceof Error) {
|
||||
showToast(err.message);
|
||||
}
|
||||
} finally {
|
||||
setIsLoading(false);
|
||||
}
|
||||
}, [novelId, chapterId]);
|
||||
|
||||
useEffect(() => {
|
||||
getNextAndPrevChapter();
|
||||
}, [getNextAndPrevChapter]);
|
||||
|
||||
const navigateToPrevChapter = () =>
|
||||
previousChapter
|
||||
? navigation.dispatch(
|
||||
StackActions.replace(
|
||||
'ReaderScreen' as never,
|
||||
{
|
||||
sourceId,
|
||||
novelId,
|
||||
chapterId: previousChapter?.chapterId,
|
||||
chapterUrl: previousChapter?.chapterUrl,
|
||||
} as never,
|
||||
),
|
||||
)
|
||||
: showToast("There's no previous chapter");
|
||||
|
||||
const navigateToNextChapter = () =>
|
||||
nextChapter
|
||||
? navigation.dispatch(
|
||||
StackActions.replace(
|
||||
'ReaderScreen' as never,
|
||||
{
|
||||
sourceId,
|
||||
novelId,
|
||||
chapterId: nextChapter?.chapterId,
|
||||
chapterUrl: nextChapter?.chapterUrl,
|
||||
} as never,
|
||||
),
|
||||
)
|
||||
: showToast("There's no next chapter");
|
||||
|
||||
return {
|
||||
isLoading,
|
||||
nextChapter,
|
||||
previousChapter,
|
||||
navigateToNextChapter,
|
||||
navigateToPrevChapter,
|
||||
};
|
||||
};
|
||||
|
||||
export default useNextAndPrevChapter;
|
||||
@@ -0,0 +1,34 @@
|
||||
import { useCallback, useEffect, useState } from 'react';
|
||||
import { useWindowDimensions } from 'react-native';
|
||||
|
||||
import { useDeviceOrientation } from '../hooks';
|
||||
import { DeviceOrientation } from '../hooks/useDeviceOrientation';
|
||||
import { useAppearanceSettings } from '../redux/hooks';
|
||||
|
||||
const useNovelCoverHeight = () => {
|
||||
const window = useWindowDimensions();
|
||||
const orientation = useDeviceOrientation();
|
||||
|
||||
const { novelsPerRowLandscape, novelsPerRowPotrait } =
|
||||
useAppearanceSettings();
|
||||
|
||||
const [coverHeight, setCoverHeight] = useState<number>();
|
||||
|
||||
const getCoverHeight = useCallback(
|
||||
() =>
|
||||
(window.width /
|
||||
(orientation === DeviceOrientation.POTRAIT
|
||||
? novelsPerRowPotrait
|
||||
: novelsPerRowLandscape)) *
|
||||
(4 / 3),
|
||||
[window.width, novelsPerRowPotrait, novelsPerRowLandscape, orientation],
|
||||
);
|
||||
|
||||
useEffect(() => {
|
||||
setCoverHeight(getCoverHeight());
|
||||
}, [getCoverHeight]);
|
||||
|
||||
return coverHeight;
|
||||
};
|
||||
|
||||
export default useNovelCoverHeight;
|
||||
@@ -0,0 +1,13 @@
|
||||
import { useAppSelector } from '../redux/hooks';
|
||||
|
||||
const useNovelTrackerInfo = (novelId: number) => {
|
||||
const tracker = useAppSelector(state => state.trackerReducer) as any;
|
||||
|
||||
const isTracked = tracker.trackedNovels.find(
|
||||
(novel: any) => novel.novelId === novelId,
|
||||
);
|
||||
|
||||
return { isTracked, isTrackerAvailable: tracker.tracker };
|
||||
};
|
||||
|
||||
export default useNovelTrackerInfo;
|
||||
@@ -0,0 +1,10 @@
|
||||
import { useCallback, useState } from 'react';
|
||||
|
||||
const useSearch = () => {
|
||||
const [searchText, setSearchText] = useState<string>('');
|
||||
const clearSearchbar = useCallback(() => setSearchText(''), []);
|
||||
|
||||
return { searchText, setSearchText, clearSearchbar };
|
||||
};
|
||||
|
||||
export default useSearch;
|
||||
@@ -0,0 +1,97 @@
|
||||
import { useState, useEffect } from 'react';
|
||||
import Tts from 'react-native-tts';
|
||||
import { htmlToText } from '../sources/helpers/htmlToText';
|
||||
|
||||
export const useTextToSpeech = html => {
|
||||
const [ttsStatus, setTtsStatus] = useState();
|
||||
const [ttsPosition, setTtsPosition] = useState({ end: 0 });
|
||||
|
||||
let text = htmlToText(html);
|
||||
|
||||
useEffect(() => {
|
||||
Tts.addEventListener('tts-start', () => setTtsStatus('start'));
|
||||
Tts.addEventListener('tts-progress', event => {
|
||||
setTtsStatus('progress');
|
||||
setTtsPosition(event);
|
||||
});
|
||||
Tts.addEventListener('tts-finish', () => setTtsStatus('finish'));
|
||||
Tts.addEventListener('tts-cancel', () => setTtsStatus('paused'));
|
||||
|
||||
return () => Tts.stop();
|
||||
}, []);
|
||||
|
||||
const startTts = () => {
|
||||
if (ttsStatus === 'progress') {
|
||||
setTtsPosition({ end: 0 });
|
||||
setTtsStatus('finish');
|
||||
Tts.stop();
|
||||
return;
|
||||
}
|
||||
|
||||
if (text.length >= 3999) {
|
||||
const splitNChars = (txt, num) => {
|
||||
let result = [];
|
||||
for (let i = 0; i < txt.length; i += num) {
|
||||
result.push(txt.substr(i, num));
|
||||
}
|
||||
return result;
|
||||
};
|
||||
|
||||
let splitMe = splitNChars(text, 3999);
|
||||
|
||||
splitMe.forEach(value => {
|
||||
Tts.speak(value, {
|
||||
androidParams: {
|
||||
KEY_PARAM_STREAM: 'STREAM_MUSIC',
|
||||
},
|
||||
});
|
||||
});
|
||||
} else {
|
||||
Tts.stop();
|
||||
Tts.speak(text, {
|
||||
androidParams: {
|
||||
KEY_PARAM_STREAM: 'STREAM_MUSIC',
|
||||
},
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
const pauseTts = () => {
|
||||
if (ttsStatus === 'progress') {
|
||||
setTtsStatus('paused');
|
||||
Tts.stop();
|
||||
return;
|
||||
} else {
|
||||
text = text.slice(ttsPosition.end);
|
||||
|
||||
if (text.length >= 3999) {
|
||||
const splitNChars = (txt, num) => {
|
||||
let result = [];
|
||||
for (let i = 0; i < txt.length; i += num) {
|
||||
result.push(txt.substr(i, num));
|
||||
}
|
||||
return result;
|
||||
};
|
||||
|
||||
let splitMe = splitNChars(text, 3999);
|
||||
|
||||
splitMe.forEach(value => {
|
||||
Tts.speak(value, {
|
||||
androidParams: {
|
||||
KEY_PARAM_STREAM: 'STREAM_MUSIC',
|
||||
},
|
||||
});
|
||||
});
|
||||
} else {
|
||||
Tts.stop();
|
||||
Tts.speak(text, {
|
||||
androidParams: {
|
||||
KEY_PARAM_STREAM: 'STREAM_MUSIC',
|
||||
},
|
||||
});
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
return [ttsStatus, ttsPosition, startTts, pauseTts];
|
||||
};
|
||||
@@ -0,0 +1,68 @@
|
||||
import { useCallback, useState } from 'react';
|
||||
|
||||
import { getUpdatesFromDb } from '../database/queries/UpdateQueries';
|
||||
|
||||
import { useFocusEffect } from '@react-navigation/native';
|
||||
import { Update } from '../database/types';
|
||||
import { useAppSelector, useDownloadQueue } from '../redux/hooks';
|
||||
|
||||
const useUpdates = () => {
|
||||
const [isLoading, setIsLoading] = useState(false);
|
||||
const [updates, setUpdates] = useState<Update[]>([]);
|
||||
const [searchResults, setSearchResults] = useState<Update[]>([]);
|
||||
|
||||
const { lastUpdateTime = null, showLastUpdateTime = false } = useAppSelector(
|
||||
state => state.updatesReducer,
|
||||
);
|
||||
const downloadQueue = useDownloadQueue();
|
||||
|
||||
const [error, setError] = useState('');
|
||||
|
||||
const getUpdates = async () => {
|
||||
try {
|
||||
const res = await getUpdatesFromDb();
|
||||
setUpdates(res);
|
||||
} catch (err) {
|
||||
if (err instanceof Error) {
|
||||
setError(err.message);
|
||||
}
|
||||
} finally {
|
||||
setIsLoading(false);
|
||||
}
|
||||
};
|
||||
|
||||
const searchUpdates = useCallback(
|
||||
async (searchText: string) => {
|
||||
setSearchResults(
|
||||
updates.filter(item =>
|
||||
item.novelName.toLowerCase().includes(searchText.toLowerCase()),
|
||||
),
|
||||
);
|
||||
},
|
||||
[updates],
|
||||
);
|
||||
|
||||
const clearSearchResults = useCallback(() => {
|
||||
setSearchResults([]);
|
||||
getUpdates();
|
||||
}, []);
|
||||
|
||||
useFocusEffect(
|
||||
useCallback(() => {
|
||||
getUpdates();
|
||||
}, [downloadQueue]),
|
||||
);
|
||||
|
||||
return {
|
||||
isLoading,
|
||||
updates,
|
||||
searchResults,
|
||||
clearSearchResults,
|
||||
searchUpdates,
|
||||
lastUpdateTime,
|
||||
showLastUpdateTime,
|
||||
error,
|
||||
};
|
||||
};
|
||||
|
||||
export default useUpdates;
|
||||
@@ -1,6 +1,6 @@
|
||||
import React from 'react';
|
||||
|
||||
import {createMaterialBottomTabNavigator} from '@react-navigation/material-bottom-tabs';
|
||||
import { createMaterialBottomTabNavigator } from '@react-navigation/material-bottom-tabs';
|
||||
import MaterialCommunityIcons from 'react-native-vector-icons/MaterialCommunityIcons';
|
||||
|
||||
import Library from '../screens/library/LibraryScreen';
|
||||
@@ -9,7 +9,8 @@ import History from '../screens/history/HistoryScreen';
|
||||
import Browse from '../screens/browse/BrowseScreen';
|
||||
import More from '../screens/more/MoreScreen';
|
||||
|
||||
import {useSettings, useTheme} from '../hooks/reduxHooks';
|
||||
import { useSettings, useTheme } from '../hooks/reduxHooks';
|
||||
import { getString } from '../../strings/translations';
|
||||
|
||||
const Tab = createMaterialBottomTabNavigator();
|
||||
|
||||
@@ -24,15 +25,15 @@ const BottomNavigator = () => {
|
||||
|
||||
return (
|
||||
<Tab.Navigator
|
||||
barStyle={{backgroundColor: theme.colorPrimary}}
|
||||
barStyle={{ backgroundColor: theme.colorPrimary }}
|
||||
activeColor={theme.colorAccent}
|
||||
shifting={!showLabelsInNav}
|
||||
>
|
||||
<Tab.Screen
|
||||
name="Library"
|
||||
name={getString('library')}
|
||||
component={Library}
|
||||
options={{
|
||||
tabBarIcon: ({color}) => (
|
||||
tabBarIcon: ({ color }) => (
|
||||
<MaterialCommunityIcons
|
||||
name="book-variant-multiple"
|
||||
color={color}
|
||||
@@ -43,10 +44,10 @@ const BottomNavigator = () => {
|
||||
/>
|
||||
{showUpdatesTab && (
|
||||
<Tab.Screen
|
||||
name="Updates"
|
||||
name={getString('updates')}
|
||||
component={Updates}
|
||||
options={{
|
||||
tabBarIcon: ({color}) => (
|
||||
tabBarIcon: ({ color }) => (
|
||||
<MaterialCommunityIcons
|
||||
name="alert-decagram-outline"
|
||||
color={color}
|
||||
@@ -58,20 +59,20 @@ const BottomNavigator = () => {
|
||||
)}
|
||||
{showHistoryTab && (
|
||||
<Tab.Screen
|
||||
name="History"
|
||||
name={getString('history')}
|
||||
component={History}
|
||||
options={{
|
||||
tabBarIcon: ({color}) => (
|
||||
tabBarIcon: ({ color }) => (
|
||||
<MaterialCommunityIcons name="history" color={color} size={24} />
|
||||
),
|
||||
}}
|
||||
/>
|
||||
)}
|
||||
<Tab.Screen
|
||||
name="Browse"
|
||||
name={getString('browse')}
|
||||
component={Browse}
|
||||
options={{
|
||||
tabBarIcon: ({color}) => (
|
||||
tabBarIcon: ({ color }) => (
|
||||
<MaterialCommunityIcons
|
||||
name="compass-outline"
|
||||
color={color}
|
||||
@@ -81,10 +82,10 @@ const BottomNavigator = () => {
|
||||
}}
|
||||
/>
|
||||
<Tab.Screen
|
||||
name="More"
|
||||
name={getString('more')}
|
||||
component={More}
|
||||
options={{
|
||||
tabBarIcon: ({color}) => (
|
||||
tabBarIcon: ({ color }) => (
|
||||
<MaterialCommunityIcons
|
||||
name="dots-horizontal"
|
||||
color={color}
|
||||
|
||||
+26
-27
@@ -1,12 +1,11 @@
|
||||
import React, {useEffect} from 'react';
|
||||
import {View} from 'react-native';
|
||||
import React, { useEffect } from 'react';
|
||||
|
||||
import {NavigationContainer} from '@react-navigation/native';
|
||||
import {createStackNavigator} from '@react-navigation/stack';
|
||||
import { NavigationContainer } from '@react-navigation/native';
|
||||
import { createStackNavigator } from '@react-navigation/stack';
|
||||
|
||||
import {setBarColor} from '../theme/utils/setBarColor';
|
||||
import {useTheme} from '../hooks/reduxHooks';
|
||||
import {useGithubUpdateChecker} from '../hooks/githubUpdateChecker';
|
||||
import { setBarColor } from '../theme/utils/setBarColor';
|
||||
import { useTheme } from '../hooks/reduxHooks';
|
||||
import { useGithubUpdateChecker } from '../hooks/githubUpdateChecker';
|
||||
import * as SplashScreen from 'expo-splash-screen';
|
||||
|
||||
/**
|
||||
@@ -20,7 +19,7 @@ import MoreStack from './MoreStack';
|
||||
*/
|
||||
import Novel from '../screens/novel/NovelScreen';
|
||||
import Reader from '../screens/reader/ReaderScreen';
|
||||
import Extension from '../screens/source/SourceScreen';
|
||||
import SourceScreen from '../screens/source/SourceScreen';
|
||||
import GlobalSearch from '../screens/browse/globalsearch/GlobalSearch';
|
||||
import Migration from '../screens/browse/migration/Migration';
|
||||
import SourceNovels from '../screens/browse/SourceNovels';
|
||||
@@ -28,7 +27,7 @@ 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';
|
||||
import BrowseSettings from '../screens/browse/BrowseSettings';
|
||||
|
||||
const Stack = createStackNavigator();
|
||||
|
||||
@@ -42,26 +41,26 @@ const MainNavigator = () => {
|
||||
}, 500);
|
||||
}, [theme]);
|
||||
|
||||
const {isNewVersion, latestRelease} = useGithubUpdateChecker() || {};
|
||||
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
|
||||
theme={{ colors: { background: 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="SourceScreen" component={SourceScreen} />
|
||||
<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>
|
||||
</NavigationContainer>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import React from 'react';
|
||||
|
||||
import {createStackNavigator} from '@react-navigation/stack';
|
||||
import { createStackNavigator } from '@react-navigation/stack';
|
||||
|
||||
// Screens
|
||||
import About from '../screens/more/About';
|
||||
@@ -16,7 +16,7 @@ import AppearanceSettings from '../screens/setting/SettingsAppearanceScreen';
|
||||
|
||||
const Stack = createStackNavigator();
|
||||
|
||||
const stackNavigatorConfig = {headerShown: false};
|
||||
const stackNavigatorConfig = { headerShown: false };
|
||||
|
||||
const SettingsStack = () => (
|
||||
<Stack.Navigator screenOptions={stackNavigatorConfig}>
|
||||
|
||||
@@ -1,8 +1,8 @@
|
||||
import {CANCEL_DOWNLOAD, PAUSE_DOWNLOADS} from './donwloads.types';
|
||||
import { CANCEL_DOWNLOAD, PAUSE_DOWNLOADS } from './donwloads.types';
|
||||
import BackgroundService from 'react-native-background-actions';
|
||||
import {downloadChapter} from '../../database/queries/ChapterQueries';
|
||||
import {CHAPTER_DOWNLOADED} from '../novel/novel.types';
|
||||
import {showToast} from '../../hooks/showToast';
|
||||
import { downloadChapter } from '../../database/queries/ChapterQueries';
|
||||
import { CHAPTER_DOWNLOADED } from '../novel/novel.types';
|
||||
import { showToast } from '../../hooks/showToast';
|
||||
|
||||
import * as Notifications from 'expo-notifications';
|
||||
|
||||
@@ -11,7 +11,7 @@ export const pauseDownloads = () => async dispatch => {
|
||||
|
||||
await BackgroundService.stop();
|
||||
|
||||
dispatch({type: PAUSE_DOWNLOADS});
|
||||
dispatch({ type: PAUSE_DOWNLOADS });
|
||||
};
|
||||
|
||||
export const resumeDownloads = chapters => async dispatch => {
|
||||
@@ -90,11 +90,11 @@ export const resumeDownloads = chapters => async dispatch => {
|
||||
|
||||
await BackgroundService.start(veryIntensiveTask, options);
|
||||
|
||||
dispatch({type: PAUSE_DOWNLOADS});
|
||||
dispatch({ type: PAUSE_DOWNLOADS });
|
||||
};
|
||||
|
||||
export const cancelDownload = () => async dispatch => {
|
||||
await BackgroundService.stop();
|
||||
|
||||
dispatch({type: CANCEL_DOWNLOAD});
|
||||
dispatch({ type: CANCEL_DOWNLOAD });
|
||||
};
|
||||
|
||||
@@ -1,12 +1,12 @@
|
||||
import {CHAPTER_DOWNLOADED} from '../novel/novel.types';
|
||||
import {CANCEL_DOWNLOAD, SET_DOWNLOAD_QUEUE} from './donwloads.types';
|
||||
import { CHAPTER_DOWNLOADED } from '../novel/novel.types';
|
||||
import { CANCEL_DOWNLOAD, SET_DOWNLOAD_QUEUE } from './donwloads.types';
|
||||
|
||||
const initialState = {
|
||||
downloadQueue: [],
|
||||
};
|
||||
|
||||
const downloadsReducer = (state = initialState, action) => {
|
||||
const {type, payload} = action;
|
||||
const { type, payload } = action;
|
||||
|
||||
switch (type) {
|
||||
case SET_DOWNLOAD_QUEUE:
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user