Compare commits
289 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| d749225820 | |||
| d6d487101b | |||
| ba62c1ba0a | |||
| 75f4f18448 | |||
| f9ebec4a30 | |||
| de18e3d9d8 | |||
| acaac74db4 | |||
| d8845661a7 | |||
| b66a6130c5 | |||
| 220f25bf72 | |||
| 934edbb84c | |||
| 0d8a5d6172 | |||
| e706745f30 | |||
| dce3652ae8 | |||
| 0ca3b8ce15 | |||
| c62db5cb23 | |||
| e33b433e84 | |||
| c95f376e20 | |||
| 9b45375638 | |||
| 7fbdc6732c | |||
| 3f9549d320 | |||
| 079010b6ca | |||
| 107e534565 | |||
| f0665866ae | |||
| 706b4581b1 | |||
| a8d9c3abfa | |||
| 66798c46db | |||
| 87fa081cfd | |||
| ea26cd67e4 | |||
| 9400fcd252 | |||
| 30ca05df06 | |||
| eac3ee32c8 | |||
| 0cf249eec5 | |||
| 27e5300f4f | |||
| 97e02177a4 | |||
| 42b37f87fe | |||
| 4ad1c0253b | |||
| a57ed7a7a2 | |||
| 9fa00d9d9b | |||
| 18f63a217f | |||
| 05401ada6b | |||
| e256537079 | |||
| 0eec38f26f | |||
| fa90d57eff | |||
| fd6a907c5f | |||
| c7561fc3dd | |||
| e1401ba39f | |||
| cbb3c7cfa2 | |||
| 4b515502bf | |||
| baba8814f0 | |||
| 5b01f61c9f | |||
| c02a72018d | |||
| 4d446d7c0b | |||
| e580f8f151 | |||
| 92f164fc23 | |||
| 44c805b6f2 | |||
| d3f5619276 | |||
| 0261c6373d | |||
| fd5b9a7c17 | |||
| b884600dfb | |||
| 8355907a76 | |||
| b391e09e53 | |||
| a4c27924f8 | |||
| 221c77d2cb | |||
| 3a0047ee25 | |||
| 3afbb5c45d | |||
| 47102ff65e | |||
| c10eac6cd6 | |||
| b4c234ef93 | |||
| 3487bbd4cf | |||
| bece88663f | |||
| 054124a004 | |||
| d2b62c0e8b | |||
| e98980bdb9 | |||
| 62a847acb2 | |||
| 13f394c76d | |||
| 71f6d863c0 | |||
| d5a8ca2b47 | |||
| c969916cd5 | |||
| e68c72ce02 | |||
| 5f83104171 | |||
| 7837d7cdaf | |||
| b243d764ce | |||
| 56c7716595 | |||
| d44abc9039 | |||
| 6f30bfeeab | |||
| 3d90e04415 | |||
| 23e27e8d40 | |||
| d0f7e25956 | |||
| 9b297620fe | |||
| df851bf9d1 | |||
| 5fc411e14c | |||
| 0f221b2823 | |||
| 9e50159589 | |||
| 58752a5589 | |||
| c1a76458a8 | |||
| 5b3a6ceff4 | |||
| c4a14784f0 | |||
| d64e2e7fdb | |||
| 5d8463509c | |||
| 5b4e32e184 | |||
| a7fb3962f1 | |||
| d333381959 | |||
| d599e6669f | |||
| 5436bf8aaf | |||
| 9f6f9e71ca | |||
| 6caba807df | |||
| 9dabf1a60e | |||
| 7716ab75de | |||
| 94b2dd51cb | |||
| 010d49daea | |||
| 6eb733eb66 | |||
| 33a33842c6 | |||
| 1bfd24a1b4 | |||
| 7e1bf4dd3f | |||
| 955e552597 | |||
| 8b586754d2 | |||
| b921f9b764 | |||
| 4d30546b9a | |||
| 58eac42b14 | |||
| 872ec15ca5 | |||
| e013b3cc55 | |||
| 8e58ce2eb1 | |||
| a9e2d872f2 | |||
| ccdb150d6a | |||
| 4e1d8533e3 | |||
| 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 | |||
| f732162688 | |||
| 81a5ea0aa7 | |||
| 8be78f7e92 | |||
| bb144936b5 | |||
| 724089d940 | |||
| c22440195d | |||
| dbb20e666f | |||
| 0a7f2e8948 | |||
| a83cdcc66e | |||
| ceca91fcab | |||
| ecc8c35bfb | |||
| 3522b7e06d | |||
| ab4fc42f69 | |||
| 47676a0de5 | |||
| 7854b20b1f | |||
| a825db2965 | |||
| 57cc5a7f42 | |||
| 15cd0b7b6b | |||
| ca1303f422 | |||
| e79563f2d0 | |||
| ffe5b04360 | |||
| 1c59964a67 | |||
| 5cd08a0d29 | |||
| 4bc61f425b | |||
| 6d631b8921 | |||
| 70833ef37f | |||
| cc4c242224 | |||
| 35731ec38f | |||
| 3735e8ca1b | |||
| e0746ff092 | |||
| d0c478b89f | |||
| 3f29aee4e6 | |||
| 76df79d9ca | |||
| 3f874e6550 | |||
| 84ec20ffb3 | |||
| 65027ee41e | |||
| 3425892a16 | |||
| 97890c1596 | |||
| c97e1b8e23 | |||
| c44537a1c0 | |||
| fd8fb367bd | |||
| a3d8aa5bcd | |||
| e7a1f8210f | |||
| 9423054b10 | |||
| d11348d9a3 | |||
| 8ec90ae8f7 | |||
| 5fde669106 | |||
| c6cdf1da0e | |||
| 8cb0a5eb73 | |||
| 09e15607d5 | |||
| a9d399ea6d | |||
| b9eddc2cc2 | |||
| bb68b2f407 | |||
| 5e0d19a9c9 | |||
| 0aa78e56c8 | |||
| 078e311995 | |||
| 9a22eba72c | |||
| 2b0032a4e4 | |||
| 708225c855 |
+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',
|
||||
},
|
||||
},
|
||||
],
|
||||
};
|
||||
|
||||
@@ -0,0 +1,94 @@
|
||||
# Cloned from tachiyomi
|
||||
|
||||
name: Issue report
|
||||
description: Report an issue in LNReader
|
||||
labels: [Bug]
|
||||
body:
|
||||
- type: textarea
|
||||
id: reproduce-steps
|
||||
attributes:
|
||||
label: Steps to reproduce
|
||||
description: Provide an example of the issue.
|
||||
placeholder: |
|
||||
Example:
|
||||
1. First step
|
||||
2. Second step
|
||||
3. Issue here
|
||||
validations:
|
||||
required: true
|
||||
|
||||
- type: textarea
|
||||
id: expected-behavior
|
||||
attributes:
|
||||
label: Expected behavior
|
||||
description: Explain what you should expect to happen.
|
||||
placeholder: |
|
||||
Example:
|
||||
"This should happen..."
|
||||
validations:
|
||||
required: true
|
||||
|
||||
- type: textarea
|
||||
id: actual-behavior
|
||||
attributes:
|
||||
label: Actual behavior
|
||||
description: Explain what actually happens.
|
||||
placeholder: |
|
||||
Example:
|
||||
"This happened instead..."
|
||||
validations:
|
||||
required: true
|
||||
|
||||
- type: input
|
||||
id: lnreader-version
|
||||
attributes:
|
||||
label: LNReader version
|
||||
description: You can find your LNReader version in **More → About**.
|
||||
placeholder: |
|
||||
Example: "1.1.12"
|
||||
validations:
|
||||
required: true
|
||||
|
||||
- type: input
|
||||
id: android-version
|
||||
attributes:
|
||||
label: Android version
|
||||
description: You can find this somewhere in your Android settings.
|
||||
placeholder: |
|
||||
Example: "Android 11"
|
||||
validations:
|
||||
required: true
|
||||
|
||||
- type: input
|
||||
id: device
|
||||
attributes:
|
||||
label: Device
|
||||
description: List your device and model.
|
||||
placeholder: |
|
||||
Example: "Google Pixel 5"
|
||||
validations:
|
||||
required: true
|
||||
|
||||
- type: textarea
|
||||
id: other-details
|
||||
attributes:
|
||||
label: Other details
|
||||
placeholder: |
|
||||
Additional details and attachments.
|
||||
|
||||
- type: checkboxes
|
||||
id: acknowledgements
|
||||
attributes:
|
||||
label: Acknowledgements
|
||||
description: Read this carefully, we will close and ignore your issue if you skimmed through this.
|
||||
options:
|
||||
- label: I have searched the existing issues and this is a new ticket, **NOT** a duplicate or related to another open or closed issue.
|
||||
required: true
|
||||
- label: I have written a short but informative title.
|
||||
required: true
|
||||
- label: If this is an issue with an source, I should be opening an issue in the [sources repository](https://github.com/lnreader/lnreader-sources/issues/new/choose).
|
||||
required: true
|
||||
- label: I have updated the app to version **[1.1.12](https://github.com/lnreader/lnreader/releases/latest)**.
|
||||
required: true
|
||||
- label: I will fill out all of the requested information in this form.
|
||||
required: true
|
||||
@@ -0,0 +1,40 @@
|
||||
# Cloned from tachiyomi
|
||||
|
||||
name: Feature request
|
||||
description: Suggest a feature to improve LNReader
|
||||
labels: [Feature Request]
|
||||
body:
|
||||
- type: textarea
|
||||
id: feature-description
|
||||
attributes:
|
||||
label: Describe your suggested feature
|
||||
description: How can LNReader be improved?
|
||||
placeholder: |
|
||||
Example:
|
||||
"It should work like this..."
|
||||
validations:
|
||||
required: true
|
||||
|
||||
- type: textarea
|
||||
id: other-details
|
||||
attributes:
|
||||
label: Other details
|
||||
placeholder: |
|
||||
Additional details and attachments.
|
||||
|
||||
- type: checkboxes
|
||||
id: acknowledgements
|
||||
attributes:
|
||||
label: Acknowledgements
|
||||
description: Read this carefully, we will close and ignore your issue if you skimmed through this.
|
||||
options:
|
||||
- label: I have searched the existing issues and this is a new ticket, **NOT** a duplicate or related to another open or closed issue.
|
||||
required: true
|
||||
- label: I have written a short but informative title.
|
||||
required: true
|
||||
- label: If this is an issue with a source, I should be opening an issue in the [sources repository](https://github.com/LNReader/lnreader-sources/issues/new/choose).
|
||||
required: true
|
||||
- label: I have updated the app to version **[1.1.12](https://github.com/LNReader/lnreader/releases/latest)**.
|
||||
required: true
|
||||
- label: I will fill out all of the requested information in this form.
|
||||
required: true
|
||||
+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 --legacy-peer-deps
|
||||
|
||||
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
|
||||
|
||||
@@ -0,0 +1,31 @@
|
||||
# Cloned from tachiyomi
|
||||
|
||||
name: Issue moderator
|
||||
|
||||
on:
|
||||
issues:
|
||||
types: [opened, edited, reopened]
|
||||
issue_comment:
|
||||
types: [created]
|
||||
|
||||
jobs:
|
||||
moderate:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Moderate issues
|
||||
uses: tachiyomiorg/issue-moderator-action@v1
|
||||
with:
|
||||
repo-token: ${{ secrets.GITHUB_TOKEN }}
|
||||
auto-close-rules: |
|
||||
[
|
||||
{
|
||||
"type": "body",
|
||||
"regex": ".*DELETE THIS SECTION IF YOU HAVE READ AND ACKNOWLEDGED IT.*",
|
||||
"message": "The acknowledgment section was not removed."
|
||||
},
|
||||
{
|
||||
"type": "body",
|
||||
"regex": ".*\\* (LNReader version|Android version|Device): \\?.*",
|
||||
"message": "Requested information in the template was not filled out."
|
||||
}
|
||||
]
|
||||
@@ -1,12 +1,19 @@
|
||||
name: Lint
|
||||
on: [push, pull_request]
|
||||
name: CI
|
||||
on:
|
||||
push:
|
||||
branches:
|
||||
- main
|
||||
pull_request:
|
||||
branches:
|
||||
- main
|
||||
|
||||
jobs:
|
||||
build:
|
||||
name: ESLint
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v2
|
||||
- name: Install modules
|
||||
run: npm i
|
||||
- name: Run ESLint
|
||||
run: npm run lint
|
||||
- uses: actions/checkout@v2
|
||||
- name: Install modules
|
||||
run: npm i --legacy-peer-deps
|
||||
- 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,31 +1,27 @@
|
||||
import React, {useEffect} from 'react';
|
||||
import {StatusBar} from 'react-native';
|
||||
import { enableFreeze } from 'react-native-screens';
|
||||
|
||||
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';
|
||||
enableFreeze(true);
|
||||
|
||||
import React, { useEffect } from 'react';
|
||||
import { StatusBar } from 'react-native';
|
||||
|
||||
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 { createDatabase } from './src/database/db';
|
||||
import { SafeAreaProvider } from 'react-native-safe-area-context';
|
||||
|
||||
const App = () => {
|
||||
const [loaded] = useFonts(fonts);
|
||||
|
||||
useEffect(() => {
|
||||
SplashScreen.preventAutoHideAsync();
|
||||
createDB();
|
||||
createDatabase();
|
||||
}, []);
|
||||
|
||||
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)
|
||||
|
||||
@@ -84,7 +84,6 @@ project.ext.react = [
|
||||
apply from: '../../node_modules/react-native-unimodules/gradle.groovy'
|
||||
apply from: "../../node_modules/react-native/react.gradle"
|
||||
apply from: "../../node_modules/expo-constants/scripts/get-app-config-android.gradle"
|
||||
apply from: "../../node_modules/expo-updates/scripts/create-manifest-android.gradle"
|
||||
|
||||
/**
|
||||
* Set this to true to create two separate APKs instead of one:
|
||||
@@ -136,7 +135,7 @@ android {
|
||||
minSdkVersion rootProject.ext.minSdkVersion
|
||||
targetSdkVersion rootProject.ext.targetSdkVersion
|
||||
versionCode 1
|
||||
versionName "1.1.6"
|
||||
versionName "1.1.12"
|
||||
}
|
||||
splits {
|
||||
abi {
|
||||
@@ -188,6 +187,10 @@ dependencies {
|
||||
//noinspection GradleDynamicVersion
|
||||
implementation "com.facebook.react:react-native:+" // From node_modules
|
||||
implementation "androidx.swiperefreshlayout:swiperefreshlayout:1.0.0"
|
||||
|
||||
implementation 'com.github.bumptech.glide:glide:4.12.0'
|
||||
annotationProcessor 'com.github.bumptech.glide:compiler:4.12.0'
|
||||
|
||||
debugImplementation("com.facebook.flipper:flipper:${FLIPPER_VERSION}") {
|
||||
exclude group:'com.facebook.fbjni'
|
||||
}
|
||||
|
||||
Vendored
+10
@@ -8,3 +8,13 @@
|
||||
# http://developer.android.com/guide/developing/tools/proguard.html
|
||||
|
||||
# Add any project specific keep options here:
|
||||
-keep class com.facebook.hermes.unicode.** { *; }
|
||||
-keep class com.facebook.jni.** { *; }
|
||||
-keep public class com.dylanvann.fastimage.* {*;}
|
||||
-keep public class com.dylanvann.fastimage.** {*;}
|
||||
-keep public class * implements com.bumptech.glide.module.GlideModule
|
||||
-keep public class * extends com.bumptech.glide.module.AppGlideModule
|
||||
-keep public enum com.bumptech.glide.load.ImageHeaderParser$** {
|
||||
**[] $VALUES;
|
||||
public *;
|
||||
}
|
||||
@@ -4,6 +4,7 @@
|
||||
<!-- Internet -->
|
||||
<uses-permission android:name="android.permission.INTERNET"/>
|
||||
<uses-permission android:name="android.permission.ACCESS_NETWORK_STATE"/>
|
||||
<uses-permission android:name="android.permission.DOWNLOAD_WITHOUT_NOTIFICATION" />
|
||||
|
||||
<!-- Storage -->
|
||||
<uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE"/>
|
||||
@@ -21,34 +22,21 @@
|
||||
<uses-permission android:name="android.permission.RECEIVE_BOOT_COMPLETED"/>
|
||||
<uses-permission android:name="com.google.android.c2dm.permission.RECEIVE"/>
|
||||
|
||||
<queries>
|
||||
<intent>
|
||||
<action android:name="android.intent.action.TTS_SERVICE" />
|
||||
</intent>
|
||||
</queries>
|
||||
|
||||
<application
|
||||
android:name=".MainApplication"
|
||||
android:name=".MainApplication"
|
||||
android:largeHeap="true"
|
||||
android:label="@string/app_name"
|
||||
android:icon="@mipmap/ic_launcher"
|
||||
android:roundIcon="@mipmap/ic_launcher_round"
|
||||
android:allowBackup="true"
|
||||
android:theme="@style/AppTheme"
|
||||
android:usesCleartextTraffic="true">
|
||||
|
||||
<meta-data
|
||||
android:name="expo.modules.updates.EXPO_UPDATE_URL"
|
||||
android:value="https://exp.host/@rajarsheechatterjee/LNReader"/>
|
||||
|
||||
<meta-data
|
||||
android:name="expo.modules.updates.EXPO_SDK_VERSION"
|
||||
android:value="41.0.0"/>
|
||||
|
||||
<meta-data
|
||||
android:name="expo.modules.updates.ENABLED"
|
||||
android:value="false"/>
|
||||
|
||||
<meta-data
|
||||
android:name="expo.modules.updates.EXPO_UPDATES_CHECK_ON_LAUNCH"
|
||||
android:value="ALWAYS"/>
|
||||
|
||||
<meta-data
|
||||
android:name="expo.modules.updates.EXPO_UPDATES_LAUNCH_WAIT_MS"
|
||||
android:value="0"/>
|
||||
|
||||
<meta-data
|
||||
android:name="expo.modules.notifications.default_notification_icon"
|
||||
@@ -86,4 +74,4 @@
|
||||
<service android:name="com.asterinet.react.bgactions.RNBackgroundActionsTask" />
|
||||
|
||||
</application>
|
||||
</manifest>
|
||||
</manifest>
|
||||
|
||||
Binary file not shown.
@@ -1,9 +1,11 @@
|
||||
package com.rajarsheechatterjee.LNReader;
|
||||
|
||||
import android.content.res.Configuration;
|
||||
import android.content.Intent;
|
||||
|
||||
import android.os.Bundle;
|
||||
import android.graphics.Color;
|
||||
import android.view.KeyEvent;
|
||||
import android.view.WindowManager;
|
||||
import android.os.Build;
|
||||
import android.view.View;
|
||||
@@ -12,6 +14,7 @@ import android.view.Window;
|
||||
import com.facebook.react.ReactActivity;
|
||||
import com.facebook.react.ReactActivityDelegate;
|
||||
import com.facebook.react.ReactRootView;
|
||||
import com.rajarsheechatterjee.VolumeButtonListener.VolumeButtonListener;
|
||||
import com.swmansion.gesturehandler.react.RNGestureHandlerEnabledRootView;
|
||||
|
||||
import expo.modules.splashscreen.singletons.SplashScreen;
|
||||
@@ -31,24 +34,53 @@ public class MainActivity extends ReactActivity {
|
||||
|
||||
@Override
|
||||
protected void onCreate(Bundle savedInstanceState) {
|
||||
// SplashScreen.show(...) has to be called after super.onCreate(...)
|
||||
// Below line is handled by '@expo/configure-splash-screen' command and it's discouraged to modify it manually
|
||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.P) {
|
||||
WindowManager.LayoutParams layoutParams = new WindowManager.LayoutParams();
|
||||
layoutParams.layoutInDisplayCutoutMode = WindowManager.LayoutParams.LAYOUT_IN_DISPLAY_CUTOUT_MODE_SHORT_EDGES;
|
||||
getWindow().setAttributes(layoutParams);
|
||||
}
|
||||
// SplashScreen.show(...) has to be called after super.onCreate(...)
|
||||
// Below line is handled by '@expo/configure-splash-screen' command and it's discouraged to modify it manually
|
||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.P) {
|
||||
WindowManager.LayoutParams layoutParams = new WindowManager.LayoutParams();
|
||||
layoutParams.layoutInDisplayCutoutMode = WindowManager.LayoutParams.LAYOUT_IN_DISPLAY_CUTOUT_MODE_SHORT_EDGES;
|
||||
getWindow().setAttributes(layoutParams);
|
||||
}
|
||||
|
||||
Window w = getWindow();
|
||||
w.setStatusBarColor(Color.TRANSPARENT);
|
||||
w.setNavigationBarColor(Color.TRANSPARENT);
|
||||
w.getDecorView().setSystemUiVisibility(View.SYSTEM_UI_FLAG_LAYOUT_HIDE_NAVIGATION | View.SYSTEM_UI_FLAG_LAYOUT_STABLE);
|
||||
Window w = getWindow();
|
||||
w.setStatusBarColor(Color.TRANSPARENT);
|
||||
w.setNavigationBarColor(Color.TRANSPARENT);
|
||||
w.getDecorView().setSystemUiVisibility(View.SYSTEM_UI_FLAG_LAYOUT_HIDE_NAVIGATION | View.SYSTEM_UI_FLAG_LAYOUT_STABLE);
|
||||
|
||||
super.onCreate(null);
|
||||
SplashScreen.show(this, SplashScreenImageResizeMode.COVER, ReactRootView.class, false);
|
||||
super.onCreate(null);
|
||||
SplashScreen.show(this, SplashScreenImageResizeMode.COVER, ReactRootView.class, false);
|
||||
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean dispatchKeyEvent(KeyEvent event) {
|
||||
if (VolumeButtonListener.isActive()) {
|
||||
|
||||
int action = event.getAction();
|
||||
int keyCode = event.getKeyCode();
|
||||
switch (keyCode) {
|
||||
case KeyEvent.KEYCODE_VOLUME_UP:
|
||||
if (action == KeyEvent.ACTION_DOWN) {
|
||||
// volup
|
||||
VolumeButtonListener.up();
|
||||
if (!VolumeButtonListener.prevent)
|
||||
return super.dispatchKeyEvent(event);
|
||||
}
|
||||
return true;
|
||||
case KeyEvent.KEYCODE_VOLUME_DOWN:
|
||||
if (action == KeyEvent.ACTION_DOWN) {
|
||||
//voldown
|
||||
VolumeButtonListener.down();
|
||||
if (!VolumeButtonListener.prevent)
|
||||
return super.dispatchKeyEvent(event);
|
||||
}
|
||||
return true;
|
||||
default:
|
||||
return super.dispatchKeyEvent(event);
|
||||
}
|
||||
}
|
||||
return super.dispatchKeyEvent(event);
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the name of the main component registered from JavaScript.
|
||||
@@ -68,4 +100,5 @@ public class MainActivity extends ReactActivity {
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -2,34 +2,24 @@ package com.rajarsheechatterjee.LNReader;
|
||||
|
||||
import android.app.Application;
|
||||
import android.content.Context;
|
||||
import android.net.Uri;
|
||||
|
||||
import com.facebook.react.PackageList;
|
||||
import com.facebook.react.ReactApplication;
|
||||
import com.facebook.react.ReactInstanceManager;
|
||||
import com.facebook.react.ReactNativeHost;
|
||||
import com.facebook.react.ReactPackage;
|
||||
import com.facebook.react.shell.MainReactPackage;
|
||||
import com.facebook.soloader.SoLoader;
|
||||
import com.rajarsheechatterjee.LNReader.generated.BasePackageList;
|
||||
import com.rajarsheechatterjee.VolumeButtonListener.VolumeButtonListenerPackage;
|
||||
|
||||
import org.unimodules.adapters.react.ReactAdapterPackage;
|
||||
import org.unimodules.adapters.react.ModuleRegistryAdapter;
|
||||
import org.unimodules.adapters.react.ReactModuleRegistryProvider;
|
||||
import org.unimodules.core.interfaces.Package;
|
||||
import org.unimodules.core.interfaces.SingletonModule;
|
||||
import expo.modules.constants.ConstantsPackage;
|
||||
import expo.modules.permissions.PermissionsPackage;
|
||||
import expo.modules.filesystem.FileSystemPackage;
|
||||
import expo.modules.updates.UpdatesController;
|
||||
|
||||
import com.facebook.react.bridge.JSIModulePackage;
|
||||
import com.swmansion.reanimated.ReanimatedJSIModulePackage;
|
||||
|
||||
import java.lang.reflect.InvocationTargetException;
|
||||
import java.util.Arrays;
|
||||
import java.util.List;
|
||||
import javax.annotation.Nullable;
|
||||
|
||||
public class MainApplication extends Application implements ReactApplication {
|
||||
private final ReactModuleRegistryProvider mModuleRegistryProvider = new ReactModuleRegistryProvider(
|
||||
@@ -46,7 +36,8 @@ public class MainApplication extends Application implements ReactApplication {
|
||||
protected List<ReactPackage> getPackages() {
|
||||
List<ReactPackage> packages = new PackageList(this).getPackages();
|
||||
packages.add(new ModuleRegistryAdapter(mModuleRegistryProvider));
|
||||
packages.add(new NavigationBarColorPackage());
|
||||
packages.add(new com.rajarsheechatterjee.LNReader.NavigationBarColorPackage());
|
||||
packages.add(new VolumeButtonListenerPackage());
|
||||
return packages;
|
||||
}
|
||||
|
||||
@@ -59,24 +50,6 @@ public class MainApplication extends Application implements ReactApplication {
|
||||
protected JSIModulePackage getJSIModulePackage() {
|
||||
return new ReanimatedJSIModulePackage();
|
||||
}
|
||||
|
||||
@Override
|
||||
protected @Nullable String getJSBundleFile() {
|
||||
if (BuildConfig.DEBUG) {
|
||||
return super.getJSBundleFile();
|
||||
} else {
|
||||
return UpdatesController.getInstance().getLaunchAssetFile();
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
protected @Nullable String getBundleAssetName() {
|
||||
if (BuildConfig.DEBUG) {
|
||||
return super.getBundleAssetName();
|
||||
} else {
|
||||
return UpdatesController.getInstance().getBundleAssetName();
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
@Override
|
||||
@@ -89,10 +62,6 @@ public class MainApplication extends Application implements ReactApplication {
|
||||
super.onCreate();
|
||||
SoLoader.init(this, /* native exopackage */ false);
|
||||
|
||||
if (!BuildConfig.DEBUG) {
|
||||
UpdatesController.initialize(this);
|
||||
}
|
||||
|
||||
initializeFlipper(this, getReactNativeHost().getReactInstanceManager());
|
||||
}
|
||||
|
||||
|
||||
+1
-1
@@ -17,11 +17,11 @@ 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(),
|
||||
new expo.modules.sqlite.SQLitePackage(),
|
||||
new expo.modules.updates.UpdatesPackage(),
|
||||
new expo.modules.webbrowser.WebBrowserPackage()
|
||||
);
|
||||
}
|
||||
|
||||
+93
@@ -0,0 +1,93 @@
|
||||
package com.rajarsheechatterjee.VolumeButtonListener;
|
||||
|
||||
import androidx.annotation.NonNull;
|
||||
|
||||
import com.facebook.react.bridge.Arguments;
|
||||
import com.facebook.react.bridge.ReactApplicationContext;
|
||||
import com.facebook.react.bridge.ReactContextBaseJavaModule;
|
||||
import com.facebook.react.bridge.ReactMethod;
|
||||
import com.facebook.react.bridge.WritableMap;
|
||||
import com.facebook.react.modules.core.DeviceEventManagerModule;
|
||||
|
||||
|
||||
public class VolumeButtonListener extends ReactContextBaseJavaModule {
|
||||
|
||||
static ReactApplicationContext appContext;
|
||||
|
||||
public VolumeButtonListener(ReactApplicationContext context) {
|
||||
super(context);
|
||||
appContext = context;
|
||||
}
|
||||
|
||||
private static boolean active = false;
|
||||
|
||||
public static boolean isActive() {
|
||||
return active;
|
||||
}
|
||||
|
||||
public static boolean prevent = false;
|
||||
|
||||
public static void sendEvent(boolean up) {
|
||||
if (active) {
|
||||
WritableMap args = Arguments.createMap();
|
||||
if (up)
|
||||
appContext.getJSModule(DeviceEventManagerModule.RCTDeviceEventEmitter.class).emit("VolumeUp", args);
|
||||
else
|
||||
appContext.getJSModule(DeviceEventManagerModule.RCTDeviceEventEmitter.class).emit("VolumeDown", args);
|
||||
}
|
||||
}
|
||||
|
||||
public static void down() {
|
||||
sendEvent(false);
|
||||
}
|
||||
|
||||
public static void up() {
|
||||
sendEvent(true);
|
||||
}
|
||||
|
||||
@ReactMethod
|
||||
public void connect() {
|
||||
active = true;
|
||||
}
|
||||
|
||||
@ReactMethod
|
||||
void disconnect() {
|
||||
active = false;
|
||||
}
|
||||
|
||||
@ReactMethod
|
||||
public void pause() {
|
||||
active = false;
|
||||
}
|
||||
|
||||
@ReactMethod
|
||||
public void unpause() {
|
||||
active = true;
|
||||
}
|
||||
|
||||
@ReactMethod
|
||||
public void preventDefault() {
|
||||
prevent = true;
|
||||
}
|
||||
|
||||
@ReactMethod
|
||||
public void noPreventDefault() {
|
||||
prevent = false;
|
||||
}
|
||||
|
||||
@NonNull
|
||||
@Override
|
||||
public String getName() {
|
||||
return "VolumeButtonListener";
|
||||
}
|
||||
|
||||
@ReactMethod
|
||||
public void addListener(String eventName) {
|
||||
// Keep: Required for RN built in Event Emitter Calls.
|
||||
}
|
||||
|
||||
@ReactMethod
|
||||
public void removeListeners(Integer count) {
|
||||
// Keep: Required for RN built in Event Emitter Calls.
|
||||
}
|
||||
}
|
||||
+30
@@ -0,0 +1,30 @@
|
||||
package com.rajarsheechatterjee.VolumeButtonListener;
|
||||
|
||||
import androidx.annotation.NonNull;
|
||||
|
||||
import com.facebook.react.ReactPackage;
|
||||
import com.facebook.react.bridge.NativeModule;
|
||||
import com.facebook.react.bridge.ReactApplicationContext;
|
||||
import com.facebook.react.uimanager.ViewManager;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.Collections;
|
||||
import java.util.List;
|
||||
|
||||
public class VolumeButtonListenerPackage implements ReactPackage {
|
||||
@NonNull
|
||||
@Override
|
||||
public List<NativeModule> createNativeModules(@NonNull ReactApplicationContext reactContext) {
|
||||
List<NativeModule> modules = new ArrayList<NativeModule>();
|
||||
|
||||
modules.add(new VolumeButtonListener(reactContext));
|
||||
|
||||
return modules;
|
||||
}
|
||||
|
||||
@NonNull
|
||||
@Override
|
||||
public List<ViewManager> createViewManagers(@NonNull ReactApplicationContext reactContext) {
|
||||
return Collections.emptyList();
|
||||
}
|
||||
}
|
||||
@@ -6,6 +6,7 @@ buildscript {
|
||||
minSdkVersion = 21
|
||||
compileSdkVersion = 30
|
||||
targetSdkVersion = 30
|
||||
ndkVersion = "21.1.6352462"
|
||||
}
|
||||
repositories {
|
||||
google()
|
||||
|
||||
@@ -2,5 +2,24 @@ module.exports = function (api) {
|
||||
api.cache(true);
|
||||
return {
|
||||
presets: ['babel-preset-expo'],
|
||||
plugins: [
|
||||
[
|
||||
'module-resolver',
|
||||
{
|
||||
alias: {
|
||||
'@components': './src/components',
|
||||
'@database': './src/database',
|
||||
'@hooks': './src/hooks',
|
||||
'@screens': './src/screens',
|
||||
'@strings': './strings',
|
||||
'@redux': './src/redux',
|
||||
'@services': './src/services',
|
||||
'@sources': './src/sources',
|
||||
'@utils': './src/utils',
|
||||
'@theme': './src/theme',
|
||||
},
|
||||
},
|
||||
],
|
||||
],
|
||||
};
|
||||
};
|
||||
|
||||
@@ -0,0 +1,5 @@
|
||||
pull_request_title: 'chore: Update translations'
|
||||
commit_message: '[ci skip]'
|
||||
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
+7757
-1971
File diff suppressed because it is too large
Load Diff
+78
-39
@@ -1,67 +1,106 @@
|
||||
{
|
||||
"name": "lnreader",
|
||||
"version": "1.1.12",
|
||||
"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",
|
||||
"source:generator": "node scripts/generateSource.cjs",
|
||||
"strings": "node scripts/stringTypes.cjs",
|
||||
"prepare": "husky install"
|
||||
},
|
||||
"dependencies": {
|
||||
"@react-native-async-storage/async-storage": "^1.13.0",
|
||||
"@react-native-community/masked-view": "0.1.10",
|
||||
"@react-native-community/netinfo": "6.0.0",
|
||||
"@react-native-community/slider": "3.0.3",
|
||||
"@react-navigation/bottom-tabs": "^5.11.15",
|
||||
"@react-navigation/material-bottom-tabs": "^5.3.10",
|
||||
"@react-navigation/native": "^5.8.10",
|
||||
"@react-navigation/stack": "^5.12.8",
|
||||
"@react-native-async-storage/async-storage": "^1.15.11",
|
||||
"@react-native-community/clipboard": "^1.5.1",
|
||||
"@react-native-community/masked-view": "^0.1.11",
|
||||
"@react-native-community/slider": "^4.1.12",
|
||||
"@react-native-picker/picker": "^2.4.1",
|
||||
"@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",
|
||||
"@shopify/flash-list": "^1.2.1",
|
||||
"buffer": "^6.0.3",
|
||||
"cheerio": "^1.0.0-rc.10",
|
||||
"color": "^4.2.3",
|
||||
"dayjs": "^1.11.4",
|
||||
"expo": "^41.0.1",
|
||||
"expo-document-picker": "^9.1.2",
|
||||
"expo-document-picker": "^9.2.4",
|
||||
"expo-file-system": "~11.0.2",
|
||||
"expo-font": "~9.1.0",
|
||||
"expo-haptics": "~10.0.0",
|
||||
"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.2",
|
||||
"expo-splash-screen": "^0.11.4",
|
||||
"expo-sqlite": "~9.1.0",
|
||||
"expo-updates": "~0.5.4",
|
||||
"expo-web-browser": "~9.1.0",
|
||||
"moment": "^2.29.1",
|
||||
"i18n-js": "^3.8.0",
|
||||
"lodash": "^4.17.21",
|
||||
"prop-types": "^15.7.2",
|
||||
"qs": "^6.10.1",
|
||||
"react": "16.13.1",
|
||||
"react-dom": "16.13.1",
|
||||
"react-native": "^0.64.2",
|
||||
"react-native-background-actions": "^2.6.1",
|
||||
"react-native-cheerio": "^1.0.0-rc.4",
|
||||
"react-native-device-info": "^8.1.3",
|
||||
"react-native-easing-gradient": "^0.1.2",
|
||||
"react-native-fast-image": "^8.3.4",
|
||||
"react-native": "^0.64.3",
|
||||
"react-native-background-actions": "^2.6.5",
|
||||
"react-native-device-info": "^8.4.7",
|
||||
"react-native-easing-gradient": "^1.0.0",
|
||||
"react-native-fast-image": "^8.5.11",
|
||||
"react-native-gesture-handler": "~1.10.2",
|
||||
"react-native-pager-view": "^5.1.10",
|
||||
"react-native-paper": "^4.5.0",
|
||||
"react-native-reanimated": "^2.1.0",
|
||||
"react-native-safe-area-context": "3.2.0",
|
||||
"react-native-screens": "~3.0.0",
|
||||
"react-native-markdown-display": "^7.0.0-alpha.2",
|
||||
"react-native-mmkv": "^2.4.3",
|
||||
"react-native-pager-view": "^5.4.9",
|
||||
"react-native-paper": "^5.0.0-rc.4",
|
||||
"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-swipe-gestures": "^1.0.5",
|
||||
"react-native-tab-view": "^3.0.1",
|
||||
"react-native-tab-view": "^3.1.1",
|
||||
"react-native-tts": "^4.1.0",
|
||||
"react-native-unimodules": "~0.13.3",
|
||||
"react-native-vector-icons": "^7.1.0",
|
||||
"react-native-web": "^0.16.5",
|
||||
"react-native-webview": "^11.6.2",
|
||||
"react-redux": "^7.2.2",
|
||||
"redux": "^4.0.5",
|
||||
"react-native-unimodules": "^0.13.3",
|
||||
"react-native-vector-icons": "^9.0.0",
|
||||
"react-native-webview": "^11.14.3",
|
||||
"react-redux": "^7.2.6",
|
||||
"redux": "^4.1.2",
|
||||
"redux-persist": "^6.0.0",
|
||||
"redux-thunk": "^2.3.0",
|
||||
"rn-sliding-up-panel": "^2.4.4"
|
||||
"rn-fetch-blob": "^0.12.0",
|
||||
"rn-sliding-up-panel": "^2.4.4",
|
||||
"sanitize-html": "^2.7.0"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@babel/core": "~7.9.0",
|
||||
"@babel/core": "^7.16.0",
|
||||
"@react-native-community/eslint-config": "^3.0.1",
|
||||
"eslint": "^7.32.0"
|
||||
"@types/color": "^3.0.3",
|
||||
"@types/i18n-js": "^3.8.2",
|
||||
"@types/jest": "^27.4.0",
|
||||
"@types/lodash": "^4.14.179",
|
||||
"@types/react-native": "^0.66.15",
|
||||
"@types/react-native-vector-icons": "^6.4.10",
|
||||
"@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",
|
||||
"babel-plugin-module-resolver": "^4.1.0",
|
||||
"eslint": "^7.32.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,79 @@
|
||||
const path = require('path');
|
||||
const fs = require('fs');
|
||||
const prettier = require('prettier');
|
||||
const prettierConfig = require('../.prettierrc');
|
||||
|
||||
const MadaraSources = require('../src/sources/multisrc/madara/MadaraSources.json');
|
||||
const AllSources = require('../src/sources/sources.json');
|
||||
|
||||
const getScraperName = scraperName => {
|
||||
let name = scraperName
|
||||
.replace(/\s+/g, '')
|
||||
.replace(/-/g, 'Dash')
|
||||
.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`;
|
||||
});
|
||||
|
||||
content = prettier.format(content, {
|
||||
parser: 'typescript',
|
||||
...prettierConfig,
|
||||
});
|
||||
|
||||
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
|
||||
.replace(/\s+/g, '')
|
||||
.toLowerCase()}.png?raw=true`,
|
||||
url: madaraSource.baseUrl,
|
||||
lang: madaraSource.options.lang || 'English',
|
||||
});
|
||||
|
||||
allSourcesContent = JSON.stringify(allSourcesContent);
|
||||
|
||||
allSourcesContent = prettier.format(allSourcesContent, {
|
||||
parser: 'json5',
|
||||
quoteProps: 'preserve',
|
||||
trailingComma: 'none',
|
||||
});
|
||||
|
||||
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('${source.sourceName}: 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,47 +0,0 @@
|
||||
import React from 'react';
|
||||
import {Dimensions, StyleSheet, View} from 'react-native';
|
||||
|
||||
import {IconButton} from 'react-native-paper';
|
||||
|
||||
export const Actionbar = ({active, actions, theme, style}) => {
|
||||
if (active) {
|
||||
return (
|
||||
<View
|
||||
style={[
|
||||
styles.actionbarContainer,
|
||||
{backgroundColor: theme.colorPrimary},
|
||||
style,
|
||||
]}
|
||||
>
|
||||
{actions.map(
|
||||
(action, index) =>
|
||||
action.icon && (
|
||||
<IconButton
|
||||
key={index}
|
||||
icon={action.icon}
|
||||
color={theme.textColorPrimary}
|
||||
onPress={action.onPress}
|
||||
/>
|
||||
),
|
||||
)}
|
||||
</View>
|
||||
);
|
||||
} else {
|
||||
return null;
|
||||
}
|
||||
};
|
||||
|
||||
const styles = StyleSheet.create({
|
||||
actionbarContainer: {
|
||||
position: 'absolute',
|
||||
width: Dimensions.get('window').width - 32,
|
||||
bottom: 0,
|
||||
margin: 16,
|
||||
borderRadius: 6,
|
||||
flexDirection: 'row',
|
||||
justifyContent: 'space-between',
|
||||
paddingHorizontal: 8,
|
||||
paddingVertical: 4,
|
||||
elevation: 2,
|
||||
},
|
||||
});
|
||||
@@ -0,0 +1,88 @@
|
||||
import { useTheme } from '@hooks/useTheme';
|
||||
import React from 'react';
|
||||
import {
|
||||
Dimensions,
|
||||
Pressable,
|
||||
StyleProp,
|
||||
StyleSheet,
|
||||
View,
|
||||
ViewProps,
|
||||
} from 'react-native';
|
||||
import MaterialCommunityIcons from 'react-native-vector-icons/MaterialCommunityIcons';
|
||||
import color from 'color';
|
||||
|
||||
import { useSafeAreaInsets } from 'react-native-safe-area-context';
|
||||
import { overlay } from 'react-native-paper';
|
||||
|
||||
type Action = {
|
||||
icon: string;
|
||||
onPress: () => void;
|
||||
};
|
||||
|
||||
interface ActionbarProps {
|
||||
active: boolean;
|
||||
actions: Action[];
|
||||
viewStyle?: StyleProp<ViewProps>;
|
||||
}
|
||||
|
||||
export const Actionbar: React.FC<ActionbarProps> = ({
|
||||
active,
|
||||
actions,
|
||||
viewStyle,
|
||||
}) => {
|
||||
const theme = useTheme();
|
||||
|
||||
const { bottom } = useSafeAreaInsets();
|
||||
|
||||
if (active) {
|
||||
return (
|
||||
<View
|
||||
style={[
|
||||
styles.actionbarContainer,
|
||||
{
|
||||
backgroundColor: theme.isDark
|
||||
? overlay(2, theme.surface)
|
||||
: theme.secondaryContainer,
|
||||
minHeight: 80 + bottom,
|
||||
paddingBottom: bottom,
|
||||
},
|
||||
viewStyle,
|
||||
]}
|
||||
>
|
||||
{actions.map(({ icon, onPress }, id) => (
|
||||
<Pressable
|
||||
key={id}
|
||||
android_ripple={{
|
||||
radius: 50,
|
||||
color: color(theme.primary).alpha(0.12).string(),
|
||||
borderless: true,
|
||||
}}
|
||||
onPress={onPress}
|
||||
>
|
||||
<MaterialCommunityIcons
|
||||
name={icon}
|
||||
color={theme.onSurface}
|
||||
size={24}
|
||||
/>
|
||||
</Pressable>
|
||||
))}
|
||||
</View>
|
||||
);
|
||||
} else {
|
||||
return null;
|
||||
}
|
||||
};
|
||||
|
||||
const styles = StyleSheet.create({
|
||||
actionbarContainer: {
|
||||
position: 'absolute',
|
||||
width: Dimensions.get('window').width,
|
||||
bottom: 0,
|
||||
borderTopLeftRadius: 16,
|
||||
borderTopRightRadius: 16,
|
||||
elevation: 1,
|
||||
flexDirection: 'row',
|
||||
justifyContent: 'space-around',
|
||||
alignItems: 'center',
|
||||
},
|
||||
});
|
||||
@@ -1,30 +0,0 @@
|
||||
import React from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
|
||||
import {Appbar as MaterialAppbar} from 'react-native-paper';
|
||||
|
||||
import {useTheme} from '../hooks/reduxHooks';
|
||||
import {ViewPropTypes} from 'react-native';
|
||||
|
||||
export const Appbar = ({title, onBackAction, style, children}) => {
|
||||
const theme = useTheme();
|
||||
|
||||
return (
|
||||
<MaterialAppbar.Header
|
||||
style={[{backgroundColor: theme.colorPrimary}, style]}
|
||||
>
|
||||
{onBackAction && <MaterialAppbar.BackAction onPress={onBackAction} />}
|
||||
<MaterialAppbar.Content
|
||||
title={title}
|
||||
titleStyle={{color: theme.textColorPrimary}}
|
||||
/>
|
||||
{children}
|
||||
</MaterialAppbar.Header>
|
||||
);
|
||||
};
|
||||
|
||||
Appbar.propTypes = {
|
||||
title: PropTypes.string,
|
||||
onBackAction: PropTypes.func,
|
||||
style: ViewPropTypes.style,
|
||||
};
|
||||
@@ -0,0 +1,40 @@
|
||||
import React from 'react';
|
||||
import { StatusBar } from 'react-native';
|
||||
|
||||
import { Appbar as PaperAppbar } from 'react-native-paper';
|
||||
import { MD3ThemeType } from '../../theme/types';
|
||||
|
||||
interface AppbarProps {
|
||||
title: string;
|
||||
handleGoBack: () => void;
|
||||
theme: MD3ThemeType;
|
||||
mode?: 'small' | 'medium' | 'large' | 'center-aligned';
|
||||
}
|
||||
|
||||
const Appbar: React.FC<AppbarProps> = ({
|
||||
title,
|
||||
handleGoBack,
|
||||
theme,
|
||||
mode = 'large',
|
||||
children,
|
||||
}) => (
|
||||
<PaperAppbar.Header
|
||||
style={{ backgroundColor: theme.surface }}
|
||||
statusBarHeight={StatusBar.currentHeight}
|
||||
mode={mode}
|
||||
>
|
||||
{handleGoBack && (
|
||||
<PaperAppbar.BackAction
|
||||
onPress={handleGoBack}
|
||||
iconColor={theme.textColorPrimary}
|
||||
/>
|
||||
)}
|
||||
<PaperAppbar.Content
|
||||
title={title}
|
||||
titleStyle={{ color: theme.textColorPrimary }}
|
||||
/>
|
||||
{children}
|
||||
</PaperAppbar.Header>
|
||||
);
|
||||
|
||||
export default Appbar;
|
||||
@@ -1,17 +0,0 @@
|
||||
import React from 'react';
|
||||
import {View} from 'react-native';
|
||||
|
||||
const BottomSheetHandle = ({theme}) => (
|
||||
<View
|
||||
style={{
|
||||
backgroundColor: `${theme.textColorPrimary}66`,
|
||||
height: 4,
|
||||
width: 40,
|
||||
borderRadius: 50,
|
||||
top: 8,
|
||||
alignSelf: 'center',
|
||||
}}
|
||||
/>
|
||||
);
|
||||
|
||||
export default BottomSheetHandle;
|
||||
@@ -0,0 +1,105 @@
|
||||
import {
|
||||
Pressable,
|
||||
StyleProp,
|
||||
StyleSheet,
|
||||
Text,
|
||||
TextStyle,
|
||||
View,
|
||||
ViewStyle,
|
||||
} from 'react-native';
|
||||
import React, { useMemo } from 'react';
|
||||
import color from 'color';
|
||||
|
||||
import { MD3ThemeType } from '../../theme/types';
|
||||
|
||||
export enum ButtonVariation {
|
||||
DEFAULT = 'default',
|
||||
OUTLINED = 'outlined',
|
||||
CLEAR = 'clear',
|
||||
}
|
||||
|
||||
interface Props {
|
||||
variation?: ButtonVariation;
|
||||
title: string;
|
||||
textColor?: string;
|
||||
onPress?: () => void;
|
||||
theme: MD3ThemeType;
|
||||
style?: ViewStyle;
|
||||
labelStyle?: StyleProp<TextStyle>;
|
||||
disabled?: boolean;
|
||||
}
|
||||
|
||||
const Button: React.FC<Props> = ({
|
||||
variation = ButtonVariation.DEFAULT,
|
||||
title,
|
||||
textColor,
|
||||
onPress,
|
||||
theme,
|
||||
style,
|
||||
disabled,
|
||||
labelStyle,
|
||||
}) => {
|
||||
const textStyles: StyleProp<TextStyle> = useMemo(
|
||||
() => ({
|
||||
color: disabled
|
||||
? theme.textColorHint
|
||||
: textColor
|
||||
? textColor
|
||||
: variation === ButtonVariation.OUTLINED
|
||||
? theme.primary
|
||||
: variation === ButtonVariation.CLEAR
|
||||
? theme.primary
|
||||
: theme.onSecondary,
|
||||
}),
|
||||
[],
|
||||
);
|
||||
|
||||
return (
|
||||
<View
|
||||
style={[
|
||||
styles.button,
|
||||
variation === ButtonVariation.OUTLINED
|
||||
? {
|
||||
...styles.outlined,
|
||||
borderColor: theme.textColorHint,
|
||||
}
|
||||
: variation !== ButtonVariation.CLEAR && {
|
||||
backgroundColor: theme.secondary,
|
||||
},
|
||||
style,
|
||||
]}
|
||||
>
|
||||
<Pressable
|
||||
style={styles.pressable}
|
||||
onPress={onPress}
|
||||
android_ripple={{ color: color(theme.primary).alpha(0.12).string() }}
|
||||
disabled={disabled}
|
||||
>
|
||||
<Text style={[textStyles, labelStyle]} 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,61 +0,0 @@
|
||||
import React from 'react';
|
||||
import {Pressable, StyleSheet, Text} from 'react-native';
|
||||
import {Checkbox as PaperCheckbox} from 'react-native-paper';
|
||||
|
||||
import MaterialCommunityIcons from 'react-native-vector-icons/MaterialCommunityIcons';
|
||||
|
||||
export const Checkbox = ({label, status, onPress, disabled, theme}) => (
|
||||
<Pressable
|
||||
android_ripple={{color: theme.rippleColor}}
|
||||
style={styles.pressable}
|
||||
onPress={onPress}
|
||||
disabled={disabled}
|
||||
>
|
||||
<PaperCheckbox
|
||||
status={
|
||||
status === 'indeterminate'
|
||||
? 'indeterminate'
|
||||
: status
|
||||
? 'checked'
|
||||
: 'unchecked'
|
||||
}
|
||||
onPress={onPress}
|
||||
color={theme.colorAccent}
|
||||
uncheckedColor={theme.textColorSecondary}
|
||||
disabled={disabled}
|
||||
/>
|
||||
<Text style={{color: theme.textColorPrimary, marginLeft: 12}}>{label}</Text>
|
||||
</Pressable>
|
||||
);
|
||||
|
||||
export const SortItem = ({label, status, onPress, theme}) => (
|
||||
<Pressable
|
||||
android_ripple={{color: theme.rippleColor}}
|
||||
style={[styles.pressable, {paddingVertical: 16, paddingLeft: 64}]}
|
||||
onPress={onPress}
|
||||
>
|
||||
{status && (
|
||||
<MaterialCommunityIcons
|
||||
name={status === 'asc' ? 'arrow-up' : 'arrow-down'}
|
||||
color={theme.colorAccent}
|
||||
size={21}
|
||||
style={styles.icon}
|
||||
/>
|
||||
)}
|
||||
<Text style={{color: theme.textColorPrimary}}>{label}</Text>
|
||||
</Pressable>
|
||||
);
|
||||
|
||||
const styles = StyleSheet.create({
|
||||
pressable: {
|
||||
paddingHorizontal: 16,
|
||||
paddingVertical: 6,
|
||||
flexDirection: 'row',
|
||||
alignItems: 'center',
|
||||
},
|
||||
icon: {
|
||||
position: 'absolute',
|
||||
left: 24,
|
||||
alignSelf: 'center',
|
||||
},
|
||||
});
|
||||
@@ -0,0 +1,96 @@
|
||||
import React from 'react';
|
||||
import {
|
||||
Pressable,
|
||||
StyleProp,
|
||||
StyleSheet,
|
||||
Text,
|
||||
TextStyle,
|
||||
ViewStyle,
|
||||
} from 'react-native';
|
||||
import { Checkbox as PaperCheckbox } from 'react-native-paper';
|
||||
import MaterialCommunityIcons from 'react-native-vector-icons/MaterialCommunityIcons';
|
||||
import color from 'color';
|
||||
|
||||
import { MD3ThemeType } from '../../theme/types';
|
||||
|
||||
interface CheckboxProps {
|
||||
label: string;
|
||||
status: boolean | 'indeterminate';
|
||||
onPress?: () => void;
|
||||
disabled?: boolean;
|
||||
theme: MD3ThemeType;
|
||||
viewStyle?: StyleProp<ViewStyle>;
|
||||
labelStyle?: StyleProp<TextStyle>;
|
||||
}
|
||||
|
||||
export const Checkbox: React.FC<CheckboxProps> = ({
|
||||
label,
|
||||
status,
|
||||
theme,
|
||||
disabled,
|
||||
onPress,
|
||||
viewStyle,
|
||||
labelStyle,
|
||||
}) => (
|
||||
<Pressable
|
||||
android_ripple={{ color: color(theme.primary).alpha(0.12).string() }}
|
||||
style={[styles.pressable, viewStyle]}
|
||||
onPress={onPress}
|
||||
disabled={disabled}
|
||||
>
|
||||
<PaperCheckbox
|
||||
status={
|
||||
status === 'indeterminate'
|
||||
? 'indeterminate'
|
||||
: status
|
||||
? 'checked'
|
||||
: 'unchecked'
|
||||
}
|
||||
onPress={onPress}
|
||||
color={theme.primary}
|
||||
theme={{
|
||||
colors: { disabled: theme.textColorSecondary },
|
||||
}}
|
||||
uncheckedColor={theme.textColorSecondary}
|
||||
disabled={disabled}
|
||||
/>
|
||||
<Text style={[styles.defaultLabel, { color: theme.onSurface }, labelStyle]}>
|
||||
{label}
|
||||
</Text>
|
||||
</Pressable>
|
||||
);
|
||||
|
||||
export const SortItem = ({ label, status, onPress, theme }) => (
|
||||
<Pressable
|
||||
android_ripple={{ color: color(theme.primary).alpha(0.12).string() }}
|
||||
style={[styles.pressable, { paddingVertical: 16, paddingLeft: 64 }]}
|
||||
onPress={onPress}
|
||||
>
|
||||
{status && (
|
||||
<MaterialCommunityIcons
|
||||
name={status === 'asc' ? 'arrow-up' : 'arrow-down'}
|
||||
color={theme.primary}
|
||||
size={21}
|
||||
style={styles.icon}
|
||||
/>
|
||||
)}
|
||||
<Text style={{ color: theme.onSurface }}>{label}</Text>
|
||||
</Pressable>
|
||||
);
|
||||
|
||||
const styles = StyleSheet.create({
|
||||
pressable: {
|
||||
paddingHorizontal: 16,
|
||||
paddingVertical: 6,
|
||||
flexDirection: 'row',
|
||||
alignItems: 'center',
|
||||
},
|
||||
icon: {
|
||||
position: 'absolute',
|
||||
left: 24,
|
||||
alignSelf: 'center',
|
||||
},
|
||||
defaultLabel: {
|
||||
marginLeft: 12,
|
||||
},
|
||||
});
|
||||
@@ -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,60 @@
|
||||
import React from 'react';
|
||||
import { Pressable, StyleSheet, Text, View } from 'react-native';
|
||||
import color from 'color';
|
||||
|
||||
import { MD3ThemeType } from '../../theme/types';
|
||||
import { overlay } from 'react-native-paper';
|
||||
|
||||
interface ChipProps {
|
||||
label: string;
|
||||
theme: MD3ThemeType;
|
||||
}
|
||||
|
||||
const Chip: React.FC<ChipProps> = ({ label, theme }) => (
|
||||
<View
|
||||
style={[
|
||||
styles.chipContainer,
|
||||
{
|
||||
backgroundColor: theme.isDark
|
||||
? overlay(1, theme.surface)
|
||||
: theme.secondaryContainer,
|
||||
},
|
||||
]}
|
||||
>
|
||||
<Pressable
|
||||
android_ripple={{ color: color(theme.primary).alpha(0.12).string() }}
|
||||
style={styles.pressable}
|
||||
>
|
||||
<Text
|
||||
style={[
|
||||
styles.label,
|
||||
{
|
||||
color: theme.isDark ? theme.onSurface : theme.onSecondaryContainer,
|
||||
},
|
||||
]}
|
||||
>
|
||||
{label}
|
||||
</Text>
|
||||
</Pressable>
|
||||
</View>
|
||||
);
|
||||
|
||||
export default Chip;
|
||||
|
||||
const styles = StyleSheet.create({
|
||||
chipContainer: {
|
||||
overflow: 'hidden',
|
||||
borderRadius: 8,
|
||||
height: 32,
|
||||
marginRight: 8,
|
||||
},
|
||||
pressable: {
|
||||
flex: 1,
|
||||
justifyContent: 'center',
|
||||
alignItems: 'center',
|
||||
paddingHorizontal: 16,
|
||||
},
|
||||
label: {
|
||||
fontSize: 12,
|
||||
},
|
||||
});
|
||||
+42
-38
@@ -1,48 +1,52 @@
|
||||
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, overlay, Portal, TextInput } from 'react-native-paper';
|
||||
import { MD3ThemeType } from '../../theme/types';
|
||||
|
||||
const ColorPickerModal = ({
|
||||
interface ColorPickerModalProps {
|
||||
visible: boolean;
|
||||
title: string;
|
||||
color: string;
|
||||
onSubmit: (val: string) => void;
|
||||
closeModal: () => void;
|
||||
theme: MD3ThemeType;
|
||||
showAccentColors?: boolean;
|
||||
}
|
||||
|
||||
const ColorPickerModal: React.FC<ColorPickerModalProps> = ({
|
||||
theme,
|
||||
color,
|
||||
title,
|
||||
onSubmit,
|
||||
hideModal,
|
||||
modalVisible,
|
||||
closeModal,
|
||||
visible,
|
||||
showAccentColors,
|
||||
}) => {
|
||||
const [text, setText] = useState(color);
|
||||
const [error, setError] = useState();
|
||||
const [text, setText] = useState<string>(color);
|
||||
const [error, setError] = useState<string | null>();
|
||||
|
||||
const onDismiss = () => {
|
||||
hideModal();
|
||||
setText();
|
||||
setError();
|
||||
closeModal();
|
||||
if (error) {
|
||||
setText(color);
|
||||
}
|
||||
setError(null);
|
||||
};
|
||||
|
||||
const onChangeText = txt => setText(txt);
|
||||
const onChangeText = (txt: string) => setText(txt);
|
||||
|
||||
const onSubmitEditing = () => {
|
||||
const re = /^#([0-9a-f]{6}|[0-9a-f]{3})$/i;
|
||||
const re = /^#([0-9a-f]{8}|[0-9a-f]{6}|[0-9a-f]{3})$/i;
|
||||
|
||||
if (text.match(re)) {
|
||||
onSubmit(text);
|
||||
hideModal();
|
||||
closeModal();
|
||||
} else {
|
||||
setError('Enter a valid hex color code');
|
||||
}
|
||||
};
|
||||
|
||||
const textInputTheme = {
|
||||
colors: {
|
||||
primary: color,
|
||||
placeholder: theme.textColorHint,
|
||||
text: theme.textColorPrimary,
|
||||
background: 'transparent',
|
||||
},
|
||||
};
|
||||
|
||||
const accentColors = [
|
||||
'#EF5350',
|
||||
'#EC407A',
|
||||
@@ -69,23 +73,23 @@ const ColorPickerModal = ({
|
||||
return (
|
||||
<Portal>
|
||||
<Modal
|
||||
visible={modalVisible}
|
||||
visible={visible}
|
||||
onDismiss={onDismiss}
|
||||
contentContainerStyle={[
|
||||
styles.modalContainer,
|
||||
{backgroundColor: theme.colorPrimary},
|
||||
{ backgroundColor: overlay(2, theme.surface) },
|
||||
]}
|
||||
>
|
||||
<Text style={[styles.modalTitle, {color: theme.textColorPrimary}]}>
|
||||
<Text style={[styles.modalTitle, { color: theme.textColorPrimary }]}>
|
||||
{title}
|
||||
</Text>
|
||||
{showAccentColors && (
|
||||
{showAccentColors ? (
|
||||
<FlatList
|
||||
contentContainerStyle={{marginBottom: 8}}
|
||||
contentContainerStyle={{ marginBottom: 8 }}
|
||||
data={accentColors}
|
||||
numColumns={4}
|
||||
keyExtractor={item => item}
|
||||
renderItem={({item}) => (
|
||||
renderItem={({ item }) => (
|
||||
<View
|
||||
style={{
|
||||
borderRadius: 4,
|
||||
@@ -98,30 +102,30 @@ const ColorPickerModal = ({
|
||||
}}
|
||||
>
|
||||
<Pressable
|
||||
style={{flex: 1}}
|
||||
style={{ flex: 1 }}
|
||||
android_ripple={{
|
||||
color: 'rgba(0,0,0,0.12)',
|
||||
}}
|
||||
onPress={() => {
|
||||
onSubmit(item);
|
||||
hideModal();
|
||||
closeModal();
|
||||
}}
|
||||
/>
|
||||
</View>
|
||||
)}
|
||||
/>
|
||||
)}
|
||||
) : null}
|
||||
<TextInput
|
||||
value={text}
|
||||
defaultValue={color}
|
||||
defaultValue={typeof color === 'string' ? color : ''}
|
||||
placeholder="Hex Color Code (E.g. #3399FF)"
|
||||
onChangeText={onChangeText}
|
||||
onSubmitEditing={onSubmitEditing}
|
||||
mode="outlined"
|
||||
theme={textInputTheme}
|
||||
theme={{ colors: { ...theme } }}
|
||||
underlineColor={theme.textColorHint}
|
||||
dense
|
||||
error={error}
|
||||
error={Boolean(error)}
|
||||
/>
|
||||
<Text style={styles.errorText}>{error}</Text>
|
||||
</Modal>
|
||||
@@ -134,11 +138,11 @@ export default ColorPickerModal;
|
||||
const styles = StyleSheet.create({
|
||||
modalContainer: {
|
||||
margin: 30,
|
||||
padding: 20,
|
||||
borderRadius: 8,
|
||||
padding: 24,
|
||||
borderRadius: 28,
|
||||
},
|
||||
modalTitle: {
|
||||
fontSize: 18,
|
||||
fontSize: 24,
|
||||
marginBottom: 16,
|
||||
},
|
||||
errorText: {
|
||||
@@ -0,0 +1,54 @@
|
||||
import React from 'react';
|
||||
import { Pressable, StyleSheet, Text, View } from 'react-native';
|
||||
|
||||
import { ThemeType } from '../../theme/types';
|
||||
|
||||
interface ColorPreferenceItemProps {
|
||||
label: string;
|
||||
description?: string;
|
||||
onPress: () => void;
|
||||
theme: ThemeType;
|
||||
}
|
||||
|
||||
const ColorPreferenceItem: React.FC<ColorPreferenceItemProps> = ({
|
||||
label,
|
||||
description,
|
||||
theme,
|
||||
onPress,
|
||||
}) => (
|
||||
<Pressable
|
||||
style={styles.container}
|
||||
android_ripple={{ color: theme.rippleColor }}
|
||||
onPress={onPress}
|
||||
>
|
||||
<View>
|
||||
<Text style={[styles.label, { color: theme.textColorPrimary }]}>
|
||||
{label}
|
||||
</Text>
|
||||
<Text style={{ color: theme.textColorSecondary }}>
|
||||
{description?.toUpperCase?.()}
|
||||
</Text>
|
||||
</View>
|
||||
<View style={[{ backgroundColor: description }, styles.colorPreview]} />
|
||||
</Pressable>
|
||||
);
|
||||
|
||||
export default ColorPreferenceItem;
|
||||
|
||||
const styles = StyleSheet.create({
|
||||
container: {
|
||||
padding: 16,
|
||||
flexDirection: 'row',
|
||||
justifyContent: 'space-between',
|
||||
alignItems: 'center',
|
||||
},
|
||||
label: {
|
||||
fontSize: 16,
|
||||
},
|
||||
colorPreview: {
|
||||
height: 24,
|
||||
width: 24,
|
||||
borderRadius: 50,
|
||||
marginRight: 16,
|
||||
},
|
||||
});
|
||||
@@ -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,64 +0,0 @@
|
||||
import React from 'react';
|
||||
import {Pressable, View} from 'react-native';
|
||||
import MaterialCommunityIcons from 'react-native-vector-icons/MaterialCommunityIcons';
|
||||
|
||||
export const ToggleButton = ({icon, selected, theme, color, onPress}) => (
|
||||
<View
|
||||
style={{
|
||||
borderRadius: 8,
|
||||
overflow: 'hidden',
|
||||
marginHorizontal: 4,
|
||||
}}
|
||||
>
|
||||
<Pressable
|
||||
android_ripple={{color: theme.rippleColor}}
|
||||
style={{
|
||||
padding: 10,
|
||||
alignItems: 'center',
|
||||
justifyContent: 'center',
|
||||
}}
|
||||
onPress={onPress}
|
||||
>
|
||||
<MaterialCommunityIcons
|
||||
name={icon}
|
||||
color={
|
||||
selected ? theme.colorAccent : color ? color : theme.textColorPrimary
|
||||
}
|
||||
size={24}
|
||||
/>
|
||||
</Pressable>
|
||||
</View>
|
||||
);
|
||||
|
||||
export const ToggleColorButton = ({
|
||||
selected,
|
||||
backgroundColor,
|
||||
textColor,
|
||||
onPress,
|
||||
}) => (
|
||||
<View
|
||||
style={{
|
||||
borderRadius: 50,
|
||||
overflow: 'hidden',
|
||||
marginHorizontal: 6,
|
||||
height: 44,
|
||||
width: 44,
|
||||
}}
|
||||
>
|
||||
<Pressable
|
||||
android_ripple={{color: textColor}}
|
||||
style={{
|
||||
flex: 1,
|
||||
padding: 10,
|
||||
backgroundColor: backgroundColor,
|
||||
alignItems: 'center',
|
||||
justifyContent: 'center',
|
||||
}}
|
||||
onPress={onPress}
|
||||
>
|
||||
{selected && (
|
||||
<MaterialCommunityIcons name="check" color={textColor} size={24} />
|
||||
)}
|
||||
</Pressable>
|
||||
</View>
|
||||
);
|
||||
@@ -0,0 +1,92 @@
|
||||
import React from 'react';
|
||||
import { Pressable, View } from 'react-native';
|
||||
import MaterialCommunityIcons from 'react-native-vector-icons/MaterialCommunityIcons';
|
||||
import { MD3ThemeType } from '../../theme/types';
|
||||
import Color from 'color';
|
||||
|
||||
interface ToggleButtonProps {
|
||||
icon: string;
|
||||
selected: boolean;
|
||||
theme: MD3ThemeType;
|
||||
color?: string;
|
||||
onPress: () => void;
|
||||
}
|
||||
|
||||
export const ToggleButton: React.FC<ToggleButtonProps> = ({
|
||||
icon,
|
||||
selected,
|
||||
theme,
|
||||
color,
|
||||
onPress,
|
||||
}) => (
|
||||
<View
|
||||
style={{
|
||||
borderRadius: 6,
|
||||
overflow: 'hidden',
|
||||
marginHorizontal: 6,
|
||||
}}
|
||||
>
|
||||
<Pressable
|
||||
android_ripple={{ color: Color(theme.primary).alpha(0.12).string() }}
|
||||
style={{
|
||||
padding: 8,
|
||||
alignItems: 'center',
|
||||
justifyContent: 'center',
|
||||
backgroundColor: selected
|
||||
? Color(theme.primary).alpha(0.12).string()
|
||||
: 'transparent',
|
||||
}}
|
||||
onPress={onPress}
|
||||
>
|
||||
<MaterialCommunityIcons
|
||||
name={icon}
|
||||
color={
|
||||
selected ? theme.primary : color ? color : theme.textColorPrimary
|
||||
}
|
||||
size={24}
|
||||
/>
|
||||
</Pressable>
|
||||
</View>
|
||||
);
|
||||
|
||||
interface ToggleColorButtonProps {
|
||||
selected: boolean;
|
||||
backgroundColor: string;
|
||||
textColor: string;
|
||||
onPress: () => void;
|
||||
}
|
||||
|
||||
export const ToggleColorButton: React.FC<ToggleColorButtonProps> = ({
|
||||
selected,
|
||||
backgroundColor,
|
||||
textColor,
|
||||
onPress,
|
||||
}) => (
|
||||
<View
|
||||
style={{
|
||||
borderRadius: 50,
|
||||
overflow: 'hidden',
|
||||
marginHorizontal: 6,
|
||||
height: 44,
|
||||
width: 44,
|
||||
}}
|
||||
>
|
||||
<Pressable
|
||||
android_ripple={{ color: textColor }}
|
||||
style={{
|
||||
flex: 1,
|
||||
padding: 10,
|
||||
backgroundColor: backgroundColor,
|
||||
alignItems: 'center',
|
||||
justifyContent: 'center',
|
||||
}}
|
||||
onPress={onPress}
|
||||
>
|
||||
<MaterialCommunityIcons
|
||||
name={selected ? 'check' : 'format-color-text'}
|
||||
color={textColor}
|
||||
size={24}
|
||||
/>
|
||||
</Pressable>
|
||||
</View>
|
||||
);
|
||||
@@ -0,0 +1,78 @@
|
||||
import React from 'react';
|
||||
import { StyleSheet, View } from 'react-native';
|
||||
|
||||
import { getString } from '@strings/translations';
|
||||
|
||||
import { Dialog, Portal } from 'react-native-paper';
|
||||
import { MD3ThemeType } from '../../theme/types';
|
||||
import { Button } from '@components/index';
|
||||
import { ButtonVariation } from '@components/Button/Button';
|
||||
import { getDialogBackground } from '@theme/colors';
|
||||
|
||||
interface ConfirmationDialogProps {
|
||||
title: string;
|
||||
visible: boolean;
|
||||
theme: MD3ThemeType;
|
||||
onSubmit: () => void;
|
||||
onDismiss: () => void;
|
||||
}
|
||||
|
||||
const ConfirmationDialog: React.FC<ConfirmationDialogProps> = ({
|
||||
title,
|
||||
visible,
|
||||
onDismiss,
|
||||
theme,
|
||||
onSubmit,
|
||||
}) => {
|
||||
const handleOnSubmit = () => {
|
||||
onSubmit();
|
||||
onDismiss();
|
||||
};
|
||||
|
||||
return (
|
||||
<Portal>
|
||||
<Dialog
|
||||
visible={visible}
|
||||
onDismiss={onDismiss}
|
||||
style={[
|
||||
styles.container,
|
||||
{ backgroundColor: getDialogBackground(theme) },
|
||||
]}
|
||||
>
|
||||
<Dialog.Title style={[styles.title, { color: theme.textColorPrimary }]}>
|
||||
{title}
|
||||
</Dialog.Title>
|
||||
<View style={styles.buttonCtn}>
|
||||
<Button
|
||||
theme={theme}
|
||||
onPress={handleOnSubmit}
|
||||
title={getString('common.ok')}
|
||||
variation={ButtonVariation.CLEAR}
|
||||
/>
|
||||
<Button
|
||||
theme={theme}
|
||||
onPress={onDismiss}
|
||||
title={getString('common.cancel')}
|
||||
variation={ButtonVariation.CLEAR}
|
||||
/>
|
||||
</View>
|
||||
</Dialog>
|
||||
</Portal>
|
||||
);
|
||||
};
|
||||
|
||||
export default ConfirmationDialog;
|
||||
|
||||
const styles = StyleSheet.create({
|
||||
container: {
|
||||
borderRadius: 28,
|
||||
},
|
||||
title: {
|
||||
letterSpacing: 0,
|
||||
fontSize: 16,
|
||||
},
|
||||
buttonCtn: {
|
||||
flexDirection: 'row-reverse',
|
||||
padding: 16,
|
||||
},
|
||||
});
|
||||
@@ -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 +0,0 @@
|
||||
/* eslint-disable no-unused-vars */
|
||||
|
||||
import React from 'react';
|
||||
import {StyleSheet} from 'react-native';
|
||||
|
||||
export const Dialog = ({visible, theme}) => {};
|
||||
|
||||
const styles = StyleSheet.create({});
|
||||
@@ -1,23 +1,25 @@
|
||||
import { useTheme } from '@hooks/useTheme';
|
||||
import React from 'react';
|
||||
import {StyleSheet, Text, View} from 'react-native';
|
||||
import {useSelector} from 'react-redux';
|
||||
import { StyleSheet, Text, View } from 'react-native';
|
||||
|
||||
const EmptyView = ({icon, description, style, children, iconStyle}) => {
|
||||
const theme = useSelector(state => state.settingsReducer.theme);
|
||||
const EmptyView = ({ icon, description, style, children, iconStyle }) => {
|
||||
const theme = useTheme();
|
||||
|
||||
return (
|
||||
<View style={styles.emptyViewContainer}>
|
||||
<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,39 @@
|
||||
import React from 'react';
|
||||
import { StyleSheet, View, Text } from 'react-native';
|
||||
|
||||
import { MD3ThemeType } from '../../theme/types';
|
||||
|
||||
interface EmptyViewProps {
|
||||
icon?: string;
|
||||
description: string;
|
||||
theme: MD3ThemeType;
|
||||
}
|
||||
|
||||
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,
|
||||
padding: 16,
|
||||
alignItems: 'center',
|
||||
justifyContent: 'center',
|
||||
},
|
||||
icon: {
|
||||
fontSize: 40,
|
||||
fontWeight: 'bold',
|
||||
},
|
||||
text: {
|
||||
marginTop: 16,
|
||||
},
|
||||
});
|
||||
@@ -0,0 +1,38 @@
|
||||
import React from 'react';
|
||||
import { StyleSheet, Text, View } from 'react-native';
|
||||
|
||||
import { MD3ThemeType } from '@theme/types';
|
||||
|
||||
interface ErrorScreenProps {
|
||||
error?: string;
|
||||
theme: MD3ThemeType;
|
||||
}
|
||||
|
||||
const ErrorScreen: React.FC<ErrorScreenProps> = ({ error, theme }) => {
|
||||
return (
|
||||
<View style={styles.container}>
|
||||
<Text style={[styles.icon, { color: theme.textColorHint }]}>ಥ_ಥ</Text>
|
||||
<Text style={[styles.error, { color: theme.textColorHint }]}>
|
||||
{error}
|
||||
</Text>
|
||||
</View>
|
||||
);
|
||||
};
|
||||
|
||||
export default ErrorScreen;
|
||||
|
||||
const styles = StyleSheet.create({
|
||||
container: {
|
||||
flex: 1,
|
||||
justifyContent: 'center',
|
||||
alignItems: 'center',
|
||||
},
|
||||
icon: {
|
||||
fontSize: 44,
|
||||
},
|
||||
error: {
|
||||
marginTop: 16,
|
||||
paddingHorizontal: 16,
|
||||
textAlign: 'center',
|
||||
},
|
||||
});
|
||||
@@ -1,28 +0,0 @@
|
||||
import React from 'react';
|
||||
import {Text, View} from 'react-native';
|
||||
|
||||
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'}}>
|
||||
<EmptyView
|
||||
icon="(; ̄Д ̄)"
|
||||
description={error}
|
||||
style={{color: theme.textColorSecondary}}
|
||||
>
|
||||
<IconButton
|
||||
icon="reload"
|
||||
size={25}
|
||||
style={{margin: 0, marginTop: 24}}
|
||||
color={theme.text}
|
||||
onPress={onRetry}
|
||||
/>
|
||||
<Text style={{color: theme.textColorPrimary}}>Retry</Text>
|
||||
</EmptyView>
|
||||
</View>
|
||||
);
|
||||
|
||||
export default ErrorView;
|
||||
@@ -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,59 @@
|
||||
import React from 'react';
|
||||
import { Pressable, StyleSheet, View, ViewStyle } from 'react-native';
|
||||
import MaterialCommunityIcons from 'react-native-vector-icons/MaterialCommunityIcons';
|
||||
import Color from 'color';
|
||||
|
||||
import { MD3ThemeType } from '../../theme/types';
|
||||
|
||||
type Props = {
|
||||
name: string;
|
||||
color?: string;
|
||||
size?: number;
|
||||
disabled?: boolean;
|
||||
padding?: number;
|
||||
onPress?: () => void;
|
||||
theme: MD3ThemeType;
|
||||
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={
|
||||
onPress
|
||||
? { color: Color(theme.primary).alpha(0.12).string() }
|
||||
: undefined
|
||||
}
|
||||
>
|
||||
<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,
|
||||
},
|
||||
});
|
||||
+34
-30
@@ -1,17 +1,16 @@
|
||||
import React from 'react';
|
||||
import {Pressable, StyleSheet, Text, View} from 'react-native';
|
||||
import { Pressable, StyleSheet, Text, View } from 'react-native';
|
||||
import color from 'color';
|
||||
|
||||
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}) => (
|
||||
<PaperList.Subheader
|
||||
style={[styles.listSubHeader, {color: theme.colorAccent}]}
|
||||
>
|
||||
const ListSubHeader = ({ children, theme }) => (
|
||||
<PaperList.Subheader style={[styles.listSubHeader, { color: theme.primary }]}>
|
||||
{children}
|
||||
</PaperList.Subheader>
|
||||
);
|
||||
@@ -30,15 +29,15 @@ 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}
|
||||
color={theme.primary}
|
||||
icon={icon}
|
||||
style={{
|
||||
marginVertical: 0,
|
||||
@@ -53,37 +52,39 @@ const ListItem = ({
|
||||
name={right}
|
||||
color={iconColor || '#47a84a'}
|
||||
size={23}
|
||||
style={{marginRight: 16, textAlignVertical: 'center'}}
|
||||
style={{ marginRight: 16, textAlignVertical: 'center' }}
|
||||
/>
|
||||
)
|
||||
}
|
||||
onPress={onPress}
|
||||
rippleColor={theme.rippleColor}
|
||||
rippleColor={color(theme.primary).alpha(0.12).string()}
|
||||
/>
|
||||
);
|
||||
|
||||
const Divider = ({theme}) => (
|
||||
<PaperDivider style={{backgroundColor: theme.dividerColor, height: 1}} />
|
||||
const Divider = ({ theme }) => (
|
||||
<PaperDivider
|
||||
style={{
|
||||
backgroundColor: color(theme.isDark ? '#FFFFFF' : '#000000')
|
||||
.alpha(0.12)
|
||||
.rgb()
|
||||
.string(),
|
||||
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}
|
||||
style={{marginVertical: 0}}
|
||||
/>
|
||||
)
|
||||
icon && <PaperList.Icon color={theme.textColorSecondary} icon={icon} />
|
||||
}
|
||||
/>
|
||||
);
|
||||
|
||||
const ColorItem = ({title, description, theme, onPress}) => (
|
||||
const ColorItem = ({ title, description, theme, onPress }) => (
|
||||
<Pressable
|
||||
style={{
|
||||
padding: 16,
|
||||
@@ -91,12 +92,14 @@ const ColorItem = ({title, description, theme, onPress}) => (
|
||||
justifyContent: 'space-between',
|
||||
alignItems: 'center',
|
||||
}}
|
||||
android_ripple={{color: theme.rippleColor}}
|
||||
android_ripple={{ color: color(theme.primary).alpha(0.12).string() }}
|
||||
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={{
|
||||
@@ -110,8 +113,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.primary} icon={icon} style={{ margin: 0 }} />
|
||||
);
|
||||
|
||||
export const List = {
|
||||
@@ -130,6 +133,7 @@ const styles = StyleSheet.create({
|
||||
marginVertical: 0,
|
||||
},
|
||||
listSubHeader: {
|
||||
fontWeight: 'bold',
|
||||
paddingBottom: 5,
|
||||
},
|
||||
});
|
||||
|
||||
@@ -0,0 +1,111 @@
|
||||
import React, { ReactNode } from 'react';
|
||||
import { StyleSheet, View } from 'react-native';
|
||||
import color from 'color';
|
||||
|
||||
import { List as PaperList, Divider as PaperDivider } from 'react-native-paper';
|
||||
import { MD3ThemeType } from '../../theme/types';
|
||||
import { dividerColor } from '../../theme/colors';
|
||||
|
||||
interface ListItemProps {
|
||||
title: string;
|
||||
description?: string | null;
|
||||
icon?: string;
|
||||
onPress?: () => void;
|
||||
theme: MD3ThemeType;
|
||||
}
|
||||
|
||||
const Section = ({ children }: { children: ReactNode }) => (
|
||||
<PaperList.Section style={styles.listSection}>{children}</PaperList.Section>
|
||||
);
|
||||
|
||||
const SubHeader = ({
|
||||
children,
|
||||
theme,
|
||||
}: {
|
||||
children: ReactNode;
|
||||
theme: MD3ThemeType;
|
||||
}) => (
|
||||
<PaperList.Subheader style={{ color: theme.primary }}>
|
||||
{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.primary}
|
||||
icon={icon}
|
||||
style={styles.icon}
|
||||
/>
|
||||
)}
|
||||
</View>
|
||||
)}
|
||||
onPress={onPress}
|
||||
rippleColor={color(theme.primary).alpha(0.12).string()}
|
||||
/>
|
||||
);
|
||||
|
||||
const Divider = ({ theme }: { theme: MD3ThemeType }) => (
|
||||
<PaperDivider
|
||||
style={[styles.divider, { backgroundColor: dividerColor(theme.isDark) }]}
|
||||
/>
|
||||
);
|
||||
|
||||
const InfoItem = ({
|
||||
title,
|
||||
icon,
|
||||
theme,
|
||||
}: {
|
||||
title: string;
|
||||
icon: string;
|
||||
theme: MD3ThemeType;
|
||||
}) => (
|
||||
<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,
|
||||
},
|
||||
});
|
||||
+13
-12
@@ -1,8 +1,11 @@
|
||||
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';
|
||||
|
||||
import color from 'color';
|
||||
|
||||
const ListView = ({
|
||||
item,
|
||||
@@ -14,29 +17,26 @@ const ListView = ({
|
||||
isSelected,
|
||||
onLongPress,
|
||||
}) => {
|
||||
const uri =
|
||||
item.novelCover && !item.novelCover.startsWith('/')
|
||||
? item.novelCover
|
||||
: defaultCoverUri;
|
||||
|
||||
return (
|
||||
<Pressable
|
||||
android_ripple={{color: theme.rippleColor}}
|
||||
android_ripple={{ color: color(theme.primary).alpha(0.12).string() }}
|
||||
style={[
|
||||
styles.listView,
|
||||
isSelected && {backgroundColor: theme.rippleColor},
|
||||
isSelected && {
|
||||
backgroundColor: color(theme.primary).alpha(0.12).string(),
|
||||
},
|
||||
]}
|
||||
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 +64,7 @@ const styles = StyleSheet.create({
|
||||
width: 40,
|
||||
height: 40,
|
||||
borderRadius: 4,
|
||||
backgroundColor: coverPlaceholderColor,
|
||||
},
|
||||
novelName: {
|
||||
flex: 1,
|
||||
|
||||
@@ -0,0 +1,22 @@
|
||||
import React from 'react';
|
||||
import { ActivityIndicator, StyleSheet } from 'react-native';
|
||||
import { MD3ThemeType } from '../../theme/types';
|
||||
|
||||
interface Props {
|
||||
theme: MD3ThemeType;
|
||||
}
|
||||
|
||||
const LoadingMoreIndicator: React.FC<Props> = ({ theme }) => (
|
||||
<ActivityIndicator color={theme.primary} style={styles.indicator} />
|
||||
);
|
||||
|
||||
export default LoadingMoreIndicator;
|
||||
|
||||
const styles = StyleSheet.create({
|
||||
indicator: {
|
||||
padding: 32,
|
||||
flex: 1,
|
||||
justifyContent: 'center',
|
||||
alignItems: 'center',
|
||||
},
|
||||
});
|
||||
@@ -1,8 +0,0 @@
|
||||
import React from 'react';
|
||||
import {ActivityIndicator, View} from 'react-native';
|
||||
|
||||
export const LoadingScreen = ({theme}) => (
|
||||
<View style={{flex: 1, justifyContent: 'center'}}>
|
||||
<ActivityIndicator size={50} color={theme.colorAccent} />
|
||||
</View>
|
||||
);
|
||||
@@ -0,0 +1,18 @@
|
||||
import React from 'react';
|
||||
import { StyleSheet, ActivityIndicator } from 'react-native';
|
||||
|
||||
import { MD3ThemeType } from '../../theme/types';
|
||||
|
||||
const LoadingScreen: React.FC<{ theme: MD3ThemeType }> = ({ theme }) => (
|
||||
<ActivityIndicator size={50} color={theme.primary} style={styles.indicator} />
|
||||
);
|
||||
|
||||
export default LoadingScreen;
|
||||
|
||||
const styles = StyleSheet.create({
|
||||
indicator: {
|
||||
flex: 1,
|
||||
justifyContent: 'center',
|
||||
alignItems: 'center',
|
||||
},
|
||||
});
|
||||
@@ -1,75 +0,0 @@
|
||||
import React, {useState} from 'react';
|
||||
import {StyleSheet, View, Text} from 'react-native';
|
||||
import {Portal, Modal, Button} from 'react-native-paper';
|
||||
import * as Linking from 'expo-linking';
|
||||
import {useTheme} from '../hooks/reduxHooks';
|
||||
|
||||
const NewUpdateDialog = ({newVersion}) => {
|
||||
const [newUpdateDialog, showNewUpdateDialog] = useState(true);
|
||||
|
||||
const theme = useTheme();
|
||||
|
||||
return (
|
||||
<Portal>
|
||||
<Modal
|
||||
visible={newUpdateDialog}
|
||||
onDismiss={() => showNewUpdateDialog(false)}
|
||||
contentContainerStyle={[
|
||||
styles.containerStyle,
|
||||
{backgroundColor: theme.colorPrimaryDark},
|
||||
]}
|
||||
>
|
||||
<Text style={[styles.modalHeader, {color: theme.textColorPrimary}]}>
|
||||
{`New update available ${newVersion.tag_name}`}
|
||||
</Text>
|
||||
<Text style={{color: theme.textColorSecondary, fontSize: 16}}>
|
||||
{newVersion.body}
|
||||
</Text>
|
||||
<View
|
||||
style={{
|
||||
flexDirection: 'row',
|
||||
marginTop: 30,
|
||||
justifyContent: 'flex-end',
|
||||
}}
|
||||
>
|
||||
<Button
|
||||
labelStyle={{
|
||||
color: theme.colorAccent,
|
||||
letterSpacing: 0,
|
||||
textTransform: 'none',
|
||||
}}
|
||||
theme={{colors: {primary: theme.colorAccent}}}
|
||||
onPress={() => showNewUpdateDialog(false)}
|
||||
>
|
||||
Cancel
|
||||
</Button>
|
||||
<Button
|
||||
labelStyle={{
|
||||
color: theme.colorAccent,
|
||||
letterSpacing: 0,
|
||||
textTransform: 'none',
|
||||
}}
|
||||
theme={{colors: {primary: theme.colorAccent}}}
|
||||
onPress={() => Linking.openURL(newVersion.downloadUrl)}
|
||||
>
|
||||
Install
|
||||
</Button>
|
||||
</View>
|
||||
</Modal>
|
||||
</Portal>
|
||||
);
|
||||
};
|
||||
|
||||
export default NewUpdateDialog;
|
||||
|
||||
const styles = StyleSheet.create({
|
||||
containerStyle: {
|
||||
padding: 20,
|
||||
margin: 20,
|
||||
borderRadius: 6,
|
||||
},
|
||||
modalHeader: {
|
||||
fontSize: 20,
|
||||
marginBottom: 10,
|
||||
},
|
||||
});
|
||||
@@ -0,0 +1,98 @@
|
||||
import React, { useState } from 'react';
|
||||
import { StyleSheet, View, Text, Dimensions } from 'react-native';
|
||||
import { Portal, Modal } from 'react-native-paper';
|
||||
import * as Linking from 'expo-linking';
|
||||
import { ScrollView } from 'react-native-gesture-handler';
|
||||
import Button, { ButtonVariation } from './Button/Button';
|
||||
import { getString } from '@strings/translations';
|
||||
import Markdown from 'react-native-markdown-display';
|
||||
import { dividerColor, getDialogBackground } from '@theme/colors';
|
||||
import { useTheme } from '@hooks/useTheme';
|
||||
|
||||
interface NewUpdateDialogProps {
|
||||
newVersion: {
|
||||
tag_name: string;
|
||||
body: string;
|
||||
downloadUrl: string;
|
||||
};
|
||||
}
|
||||
|
||||
const NewUpdateDialog: React.FC<NewUpdateDialogProps> = ({ newVersion }) => {
|
||||
const [newUpdateDialog, showNewUpdateDialog] = useState(true);
|
||||
|
||||
const theme = useTheme();
|
||||
|
||||
const modalHeight = Dimensions.get('window').height / 2;
|
||||
|
||||
return (
|
||||
<Portal>
|
||||
<Modal
|
||||
visible={newUpdateDialog}
|
||||
onDismiss={() => showNewUpdateDialog(false)}
|
||||
contentContainerStyle={[
|
||||
styles.containerStyle,
|
||||
{ backgroundColor: getDialogBackground(theme) },
|
||||
]}
|
||||
>
|
||||
<Text style={[styles.modalHeader, { color: theme.textColorPrimary }]}>
|
||||
{`${getString('common.newUpdateAvailable')} ${newVersion.tag_name}`}
|
||||
</Text>
|
||||
<ScrollView style={{ height: modalHeight }}>
|
||||
<Markdown
|
||||
style={{
|
||||
body: {
|
||||
color: theme.textColorSecondary,
|
||||
lineHeight: 20,
|
||||
paddingVertical: 8,
|
||||
},
|
||||
hr: {
|
||||
backgroundColor: dividerColor(theme.isDark),
|
||||
marginVertical: 16,
|
||||
},
|
||||
code_inline: {
|
||||
backgroundColor: getDialogBackground(theme),
|
||||
},
|
||||
}}
|
||||
>
|
||||
{newVersion.body}
|
||||
</Markdown>
|
||||
</ScrollView>
|
||||
<View style={styles.buttonCtn}>
|
||||
<Button
|
||||
title={getString('common.cancel')}
|
||||
theme={theme}
|
||||
variation={ButtonVariation.CLEAR}
|
||||
onPress={() => showNewUpdateDialog(false)}
|
||||
/>
|
||||
<Button
|
||||
title={getString('common.install')}
|
||||
theme={theme}
|
||||
variation={ButtonVariation.CLEAR}
|
||||
onPress={() => Linking.openURL(newVersion.downloadUrl)}
|
||||
/>
|
||||
</View>
|
||||
</Modal>
|
||||
</Portal>
|
||||
);
|
||||
};
|
||||
|
||||
export default NewUpdateDialog;
|
||||
|
||||
const styles = StyleSheet.create({
|
||||
containerStyle: {
|
||||
padding: 20,
|
||||
margin: 20,
|
||||
borderRadius: 6,
|
||||
},
|
||||
modalHeader: {
|
||||
fontWeight: 'bold',
|
||||
fontSize: 20,
|
||||
marginBottom: 16,
|
||||
},
|
||||
body: {},
|
||||
buttonCtn: {
|
||||
flexDirection: 'row',
|
||||
marginTop: 16,
|
||||
justifyContent: 'flex-end',
|
||||
},
|
||||
});
|
||||
@@ -1,4 +1,4 @@
|
||||
import React, {memo, useCallback} from 'react';
|
||||
import React, { memo, useMemo } from 'react';
|
||||
import {
|
||||
StyleSheet,
|
||||
View,
|
||||
@@ -6,15 +6,16 @@ import {
|
||||
useWindowDimensions,
|
||||
Pressable,
|
||||
} from 'react-native';
|
||||
import color from 'color';
|
||||
|
||||
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 { coverPlaceholderColor } from '../theme/colors';
|
||||
import { useLibrarySettings } from '@hooks/useSettings';
|
||||
import { DisplayModes } from '@screens/library/constants/constants';
|
||||
|
||||
const NovelCover = ({
|
||||
item,
|
||||
@@ -25,46 +26,48 @@ const NovelCover = ({
|
||||
onLongPress,
|
||||
selectedNovels,
|
||||
}) => {
|
||||
const {displayMode, novelsPerRow, showDownloadBadges, showUnreadBadges} =
|
||||
useSettings();
|
||||
const {
|
||||
displayMode = DisplayModes.Comfortable,
|
||||
showDownloadBadges = true,
|
||||
showUnreadBadges = true,
|
||||
novelsPerRow = 3,
|
||||
} = useLibrarySettings();
|
||||
|
||||
const window = useWindowDimensions();
|
||||
|
||||
const orientation = useDeviceOrientation();
|
||||
|
||||
const getNovelsPerRow = () =>
|
||||
orientation === 'landscape' ? 6 : novelsPerRow;
|
||||
const numColumns = useMemo(
|
||||
() => (orientation === 'landscape' ? 6 : novelsPerRow),
|
||||
[orientation, novelsPerRow],
|
||||
);
|
||||
|
||||
const getHeight = useCallback(
|
||||
() => (window.width / getNovelsPerRow()) * (4 / 3),
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
[],
|
||||
const coverHeight = useMemo(
|
||||
() => (window.width / numColumns) * (4 / 3),
|
||||
[numColumns],
|
||||
);
|
||||
|
||||
const selectNovel = () => onLongPress && onLongPress(item.novelId);
|
||||
|
||||
const uri =
|
||||
item.novelCover && !item.novelCover.startsWith('/')
|
||||
? item.novelCover
|
||||
: defaultCoverUri;
|
||||
const uri = item.novelCover;
|
||||
|
||||
return displayMode !== 2 ? (
|
||||
return displayMode !== DisplayModes.List ? (
|
||||
<View
|
||||
style={[
|
||||
{
|
||||
flex: 1 / getNovelsPerRow(),
|
||||
flex: 1 / numColumns,
|
||||
borderRadius: 6,
|
||||
overflow: 'hidden',
|
||||
margin: 2,
|
||||
},
|
||||
isSelected && {
|
||||
backgroundColor: theme.colorAccent,
|
||||
backgroundColor: theme.primary,
|
||||
opacity: 0.8,
|
||||
},
|
||||
]}
|
||||
>
|
||||
<Pressable
|
||||
android_ripple={{color: theme.colorAccent}}
|
||||
android_ripple={{ color: color(theme.primary).alpha(0.12).string() }}
|
||||
style={styles.opac}
|
||||
onPress={
|
||||
selectedNovels && selectedNovels.length > 0 ? selectNovel : onPress
|
||||
@@ -78,6 +81,7 @@ const NovelCover = ({
|
||||
showUnreadBadges={showUnreadBadges}
|
||||
chaptersDownloaded={item.chaptersDownloaded}
|
||||
chaptersUnread={item.chaptersUnread}
|
||||
theme={theme}
|
||||
/>
|
||||
)}
|
||||
{showUnreadBadges && item.chaptersUnread && (
|
||||
@@ -90,16 +94,22 @@ const NovelCover = ({
|
||||
)}
|
||||
</View>
|
||||
<FastImage
|
||||
source={{uri}}
|
||||
source={{ uri }}
|
||||
style={[
|
||||
{height: getHeight(), borderRadius: 4},
|
||||
libraryStatus && {opacity: 0.5},
|
||||
{
|
||||
height: coverHeight,
|
||||
borderRadius: 4,
|
||||
backgroundColor: coverPlaceholderColor,
|
||||
},
|
||||
libraryStatus && { opacity: 0.5 },
|
||||
]}
|
||||
/>
|
||||
<View style={styles.compactTitleContainer}>
|
||||
{displayMode === 0 && <CompactTitle novelName={item.novelName} />}
|
||||
{displayMode === DisplayModes.Compact && (
|
||||
<CompactTitle novelName={item.novelName} />
|
||||
)}
|
||||
</View>
|
||||
{displayMode === 1 && (
|
||||
{displayMode === DisplayModes.Comfortable && (
|
||||
<ComfortableTitle novelName={item.novelName} theme={theme} />
|
||||
)}
|
||||
</Pressable>
|
||||
@@ -111,6 +121,7 @@ const NovelCover = ({
|
||||
showDownloadBadges &&
|
||||
item.chaptersDownloaded && (
|
||||
<DownloadBadge
|
||||
theme={theme}
|
||||
showUnreadBadges={showUnreadBadges}
|
||||
chaptersDownloaded={item.chaptersDownloaded}
|
||||
chaptersUnread={item.chaptersUnread}
|
||||
@@ -141,7 +152,7 @@ const NovelCover = ({
|
||||
|
||||
export default memo(NovelCover);
|
||||
|
||||
const ComfortableTitle = ({theme, novelName}) => (
|
||||
const ComfortableTitle = ({ theme, novelName }) => (
|
||||
<Text
|
||||
numberOfLines={2}
|
||||
style={[
|
||||
@@ -156,7 +167,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 +180,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,13 +191,13 @@ const CompactTitle = ({novelName}) => (
|
||||
</View>
|
||||
);
|
||||
|
||||
const InLibraryBadge = ({theme}) => (
|
||||
const InLibraryBadge = ({ theme }) => (
|
||||
<Text
|
||||
style={[
|
||||
styles.inLibraryBadge,
|
||||
{
|
||||
backgroundColor: theme.colorAccent,
|
||||
color: theme.colorButtonText,
|
||||
backgroundColor: theme.primary,
|
||||
color: theme.onPrimary,
|
||||
borderRadius: 4,
|
||||
},
|
||||
]}
|
||||
@@ -212,8 +223,8 @@ const UnreadBadge = ({
|
||||
borderRadius: 4,
|
||||
},
|
||||
{
|
||||
backgroundColor: theme.colorAccent,
|
||||
color: theme.colorButtonText,
|
||||
backgroundColor: theme.primary,
|
||||
color: theme.onPrimary,
|
||||
},
|
||||
]}
|
||||
>
|
||||
@@ -225,6 +236,7 @@ const DownloadBadge = ({
|
||||
chaptersDownloaded,
|
||||
showUnreadBadges,
|
||||
chaptersUnread,
|
||||
theme,
|
||||
}) => (
|
||||
<Text
|
||||
style={[
|
||||
@@ -236,6 +248,10 @@ const DownloadBadge = ({
|
||||
!showUnreadBadges && {
|
||||
borderRadius: 4,
|
||||
},
|
||||
{
|
||||
backgroundColor: theme.tertiary,
|
||||
color: theme.onTertiary,
|
||||
},
|
||||
]}
|
||||
>
|
||||
{chaptersDownloaded}
|
||||
@@ -275,16 +291,13 @@ const styles = StyleSheet.create({
|
||||
},
|
||||
|
||||
downloadBadge: {
|
||||
backgroundColor: '#2979FF',
|
||||
color: '#FFFFFF',
|
||||
borderTopLeftRadius: 4,
|
||||
borderBottomLeftRadius: 4,
|
||||
paddingTop: 2,
|
||||
paddingHorizontal: 4,
|
||||
paddingHorizontal: 5,
|
||||
fontSize: 12,
|
||||
},
|
||||
unreadBadge: {
|
||||
color: '#FFFFFF',
|
||||
borderTopRightRadius: 4,
|
||||
borderBottomRightRadius: 4,
|
||||
paddingTop: 2,
|
||||
@@ -305,8 +318,8 @@ const styles = StyleSheet.create({
|
||||
badgeContainer: {
|
||||
position: 'absolute',
|
||||
zIndex: 1,
|
||||
top: 8,
|
||||
left: 8,
|
||||
top: 10,
|
||||
left: 10,
|
||||
flexDirection: 'row',
|
||||
},
|
||||
});
|
||||
|
||||
@@ -1,61 +0,0 @@
|
||||
import React, {useCallback} from 'react';
|
||||
import {StyleSheet, FlatList} from 'react-native';
|
||||
|
||||
import {useSettings} from '../hooks/reduxHooks';
|
||||
import {useDeviceOrientation} from '../services/utils/helpers';
|
||||
|
||||
const NovelList = ({
|
||||
data,
|
||||
onScroll,
|
||||
onEndReached,
|
||||
renderItem,
|
||||
refreshControl,
|
||||
ListEmptyComponent,
|
||||
ListFooterComponent,
|
||||
}) => {
|
||||
const {displayMode, novelsPerRow} = useSettings();
|
||||
|
||||
const orientation = useDeviceOrientation();
|
||||
|
||||
const getNovelsPerRow = () => {
|
||||
if (displayMode === 2) {
|
||||
return 1;
|
||||
}
|
||||
|
||||
if (orientation === 'landscape') {
|
||||
return 6;
|
||||
} else {
|
||||
return novelsPerRow;
|
||||
}
|
||||
};
|
||||
|
||||
const keyExtractor = useCallback(item => item.novelUrl, []);
|
||||
|
||||
return (
|
||||
<FlatList
|
||||
contentContainerStyle={[
|
||||
styles.flatListCont,
|
||||
displayMode !== 2 && {paddingHorizontal: 4},
|
||||
]}
|
||||
numColumns={getNovelsPerRow()}
|
||||
key={[orientation, getNovelsPerRow()]}
|
||||
data={data}
|
||||
keyExtractor={keyExtractor}
|
||||
renderItem={renderItem}
|
||||
refreshControl={refreshControl}
|
||||
ListEmptyComponent={ListEmptyComponent}
|
||||
ListFooterComponent={ListFooterComponent}
|
||||
onScroll={onScroll}
|
||||
onEndReached={onEndReached}
|
||||
/>
|
||||
);
|
||||
};
|
||||
|
||||
export default NovelList;
|
||||
|
||||
const styles = StyleSheet.create({
|
||||
flatListCont: {
|
||||
flexGrow: 1,
|
||||
paddingBottom: 56,
|
||||
},
|
||||
});
|
||||
@@ -0,0 +1,67 @@
|
||||
import { useLibrarySettings } from '@hooks/useSettings';
|
||||
import { DisplayModes } from '@screens/library/constants/constants';
|
||||
import React, { useCallback, useMemo } from 'react';
|
||||
import {
|
||||
StyleSheet,
|
||||
FlatList,
|
||||
FlatListProps,
|
||||
ListRenderItem,
|
||||
} from 'react-native';
|
||||
import { SourceNovelItem } from '../sources/types';
|
||||
import { LibraryNovelInfo, NovelInfo } from '../database/types';
|
||||
|
||||
import { useDeviceOrientation } from '../services/utils/helpers';
|
||||
|
||||
export type NovelListRenderItem = ListRenderItem<
|
||||
LibraryNovelInfo | NovelInfo | SourceNovelItem
|
||||
>;
|
||||
|
||||
const NovelList: React.FC<
|
||||
FlatListProps<LibraryNovelInfo | NovelInfo | SourceNovelItem>
|
||||
> = props => {
|
||||
const { displayMode = DisplayModes.Comfortable, novelsPerRow = 3 } =
|
||||
useLibrarySettings();
|
||||
|
||||
const orientation = useDeviceOrientation();
|
||||
|
||||
const isListView = displayMode === DisplayModes.List;
|
||||
|
||||
const numColumns = useMemo(() => {
|
||||
if (isListView) {
|
||||
return 1;
|
||||
}
|
||||
|
||||
if (orientation === 'landscape') {
|
||||
return 6;
|
||||
} else {
|
||||
return novelsPerRow;
|
||||
}
|
||||
}, [isListView, orientation, novelsPerRow]);
|
||||
|
||||
const keyExtractor = useCallback(item => item.sourceId + item.novelUrl, []);
|
||||
|
||||
return (
|
||||
<FlatList
|
||||
contentContainerStyle={[
|
||||
!isListView && styles.listView,
|
||||
styles.flatListCont,
|
||||
]}
|
||||
numColumns={numColumns}
|
||||
key={numColumns}
|
||||
keyExtractor={keyExtractor}
|
||||
{...props}
|
||||
/>
|
||||
);
|
||||
};
|
||||
|
||||
export default NovelList;
|
||||
|
||||
const styles = StyleSheet.create({
|
||||
flatListCont: {
|
||||
flexGrow: 1,
|
||||
paddingBottom: 56,
|
||||
},
|
||||
listView: {
|
||||
paddingHorizontal: 4,
|
||||
},
|
||||
});
|
||||
@@ -1,24 +1,24 @@
|
||||
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}
|
||||
color={theme.colorAccent}
|
||||
color={theme.primary}
|
||||
uncheckedColor={theme.textColorSecondary}
|
||||
/>
|
||||
<Text
|
||||
style={[
|
||||
styles.radioButtonLabel,
|
||||
{color: theme.textColorPrimary},
|
||||
{ color: theme.textColorPrimary },
|
||||
labelStyle,
|
||||
]}
|
||||
>
|
||||
|
||||
@@ -1,33 +0,0 @@
|
||||
import React from 'react';
|
||||
import {Pressable, StyleSheet, Text} from 'react-native';
|
||||
import {RadioButton as PaperRadioButton} from 'react-native-paper';
|
||||
|
||||
export const RadioButton = ({label, status, onPress, style, theme}) => (
|
||||
<Pressable
|
||||
android_ripple={{color: theme.rippleColor}}
|
||||
style={[styles.pressable, style]}
|
||||
onPress={onPress}
|
||||
>
|
||||
<PaperRadioButton
|
||||
status={status ? 'checked' : 'unchecked'}
|
||||
onPress={onPress}
|
||||
color={theme.colorAccent}
|
||||
uncheckedColor={theme.textColorSecondary}
|
||||
/>
|
||||
<Text style={{color: theme.textColorPrimary, marginLeft: 12}}>{label}</Text>
|
||||
</Pressable>
|
||||
);
|
||||
|
||||
const styles = StyleSheet.create({
|
||||
pressable: {
|
||||
paddingHorizontal: 16,
|
||||
paddingVertical: 6,
|
||||
flexDirection: 'row',
|
||||
alignItems: 'center',
|
||||
},
|
||||
icon: {
|
||||
position: 'absolute',
|
||||
left: 24,
|
||||
alignSelf: 'center',
|
||||
},
|
||||
});
|
||||
@@ -0,0 +1,64 @@
|
||||
import React from 'react';
|
||||
import {
|
||||
Pressable,
|
||||
StyleProp,
|
||||
StyleSheet,
|
||||
Text,
|
||||
TextStyle,
|
||||
ViewStyle,
|
||||
} from 'react-native';
|
||||
import { RadioButton as PaperRadioButton } from 'react-native-paper';
|
||||
import { MD3ThemeType } from '../../theme/types';
|
||||
import color from 'color';
|
||||
|
||||
interface Props {
|
||||
label: string;
|
||||
status: boolean;
|
||||
onPress?: () => void;
|
||||
style?: StyleProp<ViewStyle>;
|
||||
labelStyle?: StyleProp<TextStyle>;
|
||||
theme: MD3ThemeType;
|
||||
}
|
||||
|
||||
export const RadioButton: React.FC<Props> = ({
|
||||
label,
|
||||
status,
|
||||
onPress,
|
||||
style,
|
||||
labelStyle,
|
||||
theme,
|
||||
}) => (
|
||||
<Pressable
|
||||
android_ripple={{ color: color(theme.primary).alpha(0.12).string() }}
|
||||
style={[styles.pressable, style]}
|
||||
onPress={onPress}
|
||||
>
|
||||
<PaperRadioButton
|
||||
status={status ? 'checked' : 'unchecked'}
|
||||
value={label}
|
||||
onPress={onPress}
|
||||
color={theme.primary}
|
||||
uncheckedColor={theme.textColorSecondary}
|
||||
/>
|
||||
<Text style={[styles.label, labelStyle, { color: theme.onSurface }]}>
|
||||
{label}
|
||||
</Text>
|
||||
</Pressable>
|
||||
);
|
||||
|
||||
const styles = StyleSheet.create({
|
||||
pressable: {
|
||||
paddingHorizontal: 16,
|
||||
paddingVertical: 6,
|
||||
flexDirection: 'row',
|
||||
alignItems: 'center',
|
||||
},
|
||||
icon: {
|
||||
position: 'absolute',
|
||||
left: 24,
|
||||
alignSelf: 'center',
|
||||
},
|
||||
label: {
|
||||
marginLeft: 12,
|
||||
},
|
||||
});
|
||||
@@ -1,86 +0,0 @@
|
||||
import React, {useRef} from 'react';
|
||||
import {StyleSheet, View, TextInput, StatusBar, Pressable} from 'react-native';
|
||||
|
||||
import {Row} from '../Common';
|
||||
import {IconButton} from '../IconButton/IconButton';
|
||||
|
||||
export const Searchbar = ({
|
||||
theme,
|
||||
placeholder,
|
||||
searchText,
|
||||
backAction,
|
||||
onBackAction,
|
||||
clearSearchbar,
|
||||
onChangeText,
|
||||
onSubmitEditing,
|
||||
actions,
|
||||
}) => {
|
||||
const searchRef = useRef(null);
|
||||
|
||||
return (
|
||||
<View style={styles.searchBar}>
|
||||
<Pressable
|
||||
onPress={() => searchRef.current.focus()}
|
||||
style={[styles.pressable, {backgroundColor: theme.searchBarColor}]}
|
||||
android_ripple={{color: theme.rippleColor}}
|
||||
>
|
||||
<Row style={{flex: 1}}>
|
||||
{backAction && (
|
||||
<IconButton
|
||||
icon={backAction}
|
||||
color={theme.textColorPrimary}
|
||||
onPress={onBackAction}
|
||||
theme={theme}
|
||||
containerStyle={{marginLeft: 0}}
|
||||
/>
|
||||
)}
|
||||
<TextInput
|
||||
ref={searchRef}
|
||||
style={{fontSize: 16, color: theme.textColorPrimary, marginLeft: 4}}
|
||||
placeholder={placeholder}
|
||||
placeholderTextColor={theme.textColorSecondary}
|
||||
onChangeText={onChangeText}
|
||||
onSubmitEditing={onSubmitEditing}
|
||||
defaultValue={searchText}
|
||||
/>
|
||||
</Row>
|
||||
{searchText !== '' && (
|
||||
<IconButton
|
||||
icon="close"
|
||||
color={theme.textColorPrimary}
|
||||
onPress={clearSearchbar}
|
||||
theme={theme}
|
||||
/>
|
||||
)}
|
||||
{actions &&
|
||||
actions.map((action, index) => (
|
||||
<IconButton
|
||||
key={index}
|
||||
icon={action.icon}
|
||||
color={action.color ?? theme.textColorPrimary}
|
||||
onPress={action.onPress}
|
||||
theme={theme}
|
||||
containerStyle={{marginRight: 0, marginLeft: 16}}
|
||||
/>
|
||||
))}
|
||||
</Pressable>
|
||||
</View>
|
||||
);
|
||||
};
|
||||
|
||||
const styles = StyleSheet.create({
|
||||
searchBar: {
|
||||
marginTop: StatusBar.currentHeight + 8,
|
||||
marginBottom: 8,
|
||||
marginHorizontal: 16,
|
||||
height: 48,
|
||||
borderRadius: 50,
|
||||
overflow: 'hidden',
|
||||
},
|
||||
pressable: {
|
||||
flexDirection: 'row',
|
||||
justifyContent: 'space-between',
|
||||
alignItems: 'center',
|
||||
paddingHorizontal: 16,
|
||||
},
|
||||
});
|
||||
@@ -0,0 +1,134 @@
|
||||
import React, { useRef } from 'react';
|
||||
import { Pressable, StyleSheet, View, TextInput } from 'react-native';
|
||||
import { useSafeAreaInsets } from 'react-native-safe-area-context';
|
||||
import color from 'color';
|
||||
|
||||
import IconButtonV2 from '../IconButtonV2/IconButtonV2';
|
||||
import { MD3ThemeType } from '../../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: MD3ThemeType;
|
||||
}
|
||||
|
||||
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: color(theme.primary).alpha(0.08).string(),
|
||||
},
|
||||
]}
|
||||
>
|
||||
<Pressable
|
||||
onPress={focusSearchbar}
|
||||
android_ripple={{ color: color(theme.primary).alpha(12).toString() }}
|
||||
style={[styles.searchbar]}
|
||||
>
|
||||
<IconButtonV2
|
||||
name={handleBackAction ? 'arrow-left' : leftIcon}
|
||||
color={theme.onSurface}
|
||||
onPress={() => {
|
||||
if (handleBackAction) {
|
||||
handleBackAction();
|
||||
} else if (onLeftIconPress) {
|
||||
onLeftIconPress();
|
||||
}
|
||||
}}
|
||||
theme={theme}
|
||||
/>
|
||||
|
||||
<TextInput
|
||||
ref={searchbarRef}
|
||||
style={[styles.textInput, { color: theme.onSurface }]}
|
||||
placeholder={placeholder}
|
||||
placeholderTextColor={theme.onSurface}
|
||||
onChangeText={onChangeText}
|
||||
onSubmitEditing={onSubmitEditing}
|
||||
defaultValue={searchText}
|
||||
/>
|
||||
{searchText !== '' ? (
|
||||
<IconButtonV2
|
||||
name="close"
|
||||
color={theme.onSurface}
|
||||
onPress={clearSearchbar}
|
||||
theme={theme}
|
||||
/>
|
||||
) : null}
|
||||
{rightIcons?.map((icon, index) => (
|
||||
<IconButtonV2
|
||||
key={index}
|
||||
name={icon.iconName}
|
||||
color={icon.color || theme.onSurface}
|
||||
onPress={icon.onPress}
|
||||
theme={theme}
|
||||
/>
|
||||
))}
|
||||
</Pressable>
|
||||
</View>
|
||||
);
|
||||
};
|
||||
|
||||
export default Searchbar;
|
||||
|
||||
const styles = StyleSheet.create({
|
||||
searchbarContainer: {
|
||||
marginHorizontal: 16,
|
||||
marginBottom: 12,
|
||||
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,19 @@
|
||||
import React from 'react';
|
||||
import {Pressable, StyleSheet, View, Text} from 'react-native';
|
||||
import { Pressable, StyleSheet, View, Text } from 'react-native';
|
||||
import color from 'color';
|
||||
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}
|
||||
android_ripple={{ color: color(theme.primary).alpha(0.12).rgb().string() }}
|
||||
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.primary} icon={icon} style={{ margin: 0 }} />
|
||||
)}
|
||||
<View style={[icon && {marginLeft: 16}]}>
|
||||
<View style={[icon && { marginLeft: 16 }]}>
|
||||
<Text
|
||||
style={{
|
||||
color: theme.textColorPrimary,
|
||||
@@ -25,15 +25,15 @@ 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>
|
||||
<Switch
|
||||
value={value}
|
||||
onValueChange={onPress}
|
||||
color={theme.colorAccent}
|
||||
style={{marginRight: 8}}
|
||||
color={theme.primary}
|
||||
style={{ marginLeft: 8 }}
|
||||
/>
|
||||
</Pressable>
|
||||
);
|
||||
@@ -43,7 +43,6 @@ export default SwitchSetting;
|
||||
const styles = StyleSheet.create({
|
||||
container: {
|
||||
paddingHorizontal: 16,
|
||||
paddingVertical: 12,
|
||||
flexDirection: 'row',
|
||||
justifyContent: 'space-between',
|
||||
},
|
||||
|
||||
@@ -0,0 +1,64 @@
|
||||
import React from 'react';
|
||||
import { Pressable, StyleSheet, View, Text } from 'react-native';
|
||||
import color from 'color';
|
||||
|
||||
import { Switch } from 'react-native-paper';
|
||||
import { MD3ThemeType } from '../../theme/types';
|
||||
|
||||
interface SwitchItemProps {
|
||||
value: boolean;
|
||||
label: string;
|
||||
description?: string;
|
||||
onPress: () => void;
|
||||
theme: MD3ThemeType;
|
||||
}
|
||||
|
||||
const SwitchItem: React.FC<SwitchItemProps> = ({
|
||||
label,
|
||||
description,
|
||||
onPress,
|
||||
theme,
|
||||
value,
|
||||
}) => (
|
||||
<Pressable
|
||||
android_ripple={{ color: color(theme.primary).alpha(0.12).string() }}
|
||||
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.primary}
|
||||
style={styles.switch}
|
||||
/>
|
||||
</Pressable>
|
||||
);
|
||||
|
||||
export default SwitchItem;
|
||||
|
||||
const styles = StyleSheet.create({
|
||||
container: {
|
||||
flexDirection: 'row',
|
||||
justifyContent: 'space-between',
|
||||
paddingHorizontal: 16,
|
||||
paddingVertical: 12,
|
||||
},
|
||||
labelContainer: {
|
||||
flex: 1,
|
||||
justifyContent: 'center',
|
||||
},
|
||||
label: {
|
||||
fontSize: 16,
|
||||
},
|
||||
switch: {
|
||||
marginLeft: 8,
|
||||
},
|
||||
});
|
||||
@@ -1,8 +1,10 @@
|
||||
import React from 'react';
|
||||
import {View, Text, /* StyleSheet, */ Pressable} from 'react-native';
|
||||
import { View, Text, /* StyleSheet, */ Pressable } from 'react-native';
|
||||
import { overlay } from 'react-native-paper';
|
||||
import color from 'color';
|
||||
import MaterialCommunityIcons from 'react-native-vector-icons/MaterialCommunityIcons';
|
||||
|
||||
export const ThemePicker = ({theme, currentTheme, onPress}) => (
|
||||
export const ThemePicker = ({ theme, currentTheme, onPress }) => (
|
||||
<View
|
||||
style={{
|
||||
justifyContent: 'center',
|
||||
@@ -12,12 +14,12 @@ export const ThemePicker = ({theme, currentTheme, onPress}) => (
|
||||
>
|
||||
<View
|
||||
style={{
|
||||
backgroundColor: theme.colorPrimaryDark,
|
||||
backgroundColor: theme.background,
|
||||
borderWidth: 3.6,
|
||||
borderColor:
|
||||
currentTheme.id === theme.id
|
||||
? theme.colorAccent
|
||||
: currentTheme.colorPrimaryDark,
|
||||
? theme.primary
|
||||
: currentTheme.background,
|
||||
width: 95,
|
||||
height: 140,
|
||||
borderRadius: 16,
|
||||
@@ -25,14 +27,14 @@ 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"
|
||||
color={theme.colorButtonText}
|
||||
color={theme.onPrimary}
|
||||
size={15}
|
||||
style={{
|
||||
backgroundColor: theme.colorAccent,
|
||||
backgroundColor: theme.primary,
|
||||
position: 'absolute',
|
||||
top: 5,
|
||||
right: 5,
|
||||
@@ -45,7 +47,7 @@ export const ThemePicker = ({theme, currentTheme, onPress}) => (
|
||||
<View
|
||||
style={{
|
||||
height: 20,
|
||||
backgroundColor: theme.colorPrimary,
|
||||
backgroundColor: overlay(2, theme.surface),
|
||||
elevation: 1,
|
||||
justifyContent: 'center',
|
||||
}}
|
||||
@@ -60,7 +62,7 @@ export const ThemePicker = ({theme, currentTheme, onPress}) => (
|
||||
}}
|
||||
/>
|
||||
</View>
|
||||
<View style={{padding: 8}}>
|
||||
<View style={{ padding: 8 }}>
|
||||
<View
|
||||
style={{
|
||||
height: 18,
|
||||
@@ -68,7 +70,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,
|
||||
@@ -82,12 +84,12 @@ export const ThemePicker = ({theme, currentTheme, onPress}) => (
|
||||
height: 10,
|
||||
width: 16,
|
||||
marginLeft: 4,
|
||||
backgroundColor: theme.colorAccent,
|
||||
backgroundColor: theme.primary,
|
||||
borderRadius: 50,
|
||||
}}
|
||||
/>
|
||||
</View>
|
||||
<View style={{flexDirection: 'row'}}>
|
||||
<View style={{ flexDirection: 'row' }}>
|
||||
<View
|
||||
style={{
|
||||
height: 10,
|
||||
@@ -114,7 +116,7 @@ export const ThemePicker = ({theme, currentTheme, onPress}) => (
|
||||
left: 0,
|
||||
right: 0,
|
||||
height: 24,
|
||||
backgroundColor: theme.colorPrimary,
|
||||
backgroundColor: color(theme.primary).alpha(0.08).string(),
|
||||
justifyContent: 'center',
|
||||
}}
|
||||
>
|
||||
@@ -141,7 +143,7 @@ export const ThemePicker = ({theme, currentTheme, onPress}) => (
|
||||
height: 12,
|
||||
width: 12,
|
||||
borderRadius: 50,
|
||||
backgroundColor: theme.colorAccent,
|
||||
backgroundColor: theme.primary,
|
||||
}}
|
||||
/>
|
||||
<View
|
||||
|
||||
@@ -1,20 +0,0 @@
|
||||
import React from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
import {Text as NativeText, TextPropTypes} from 'react-native';
|
||||
|
||||
const Text = ({type, children, style, theme}) => {
|
||||
const color =
|
||||
type === 'primary'
|
||||
? theme.textColorPrimary
|
||||
: type === 'secondary'
|
||||
? theme.textColorSecondary
|
||||
: theme.textColorHint;
|
||||
|
||||
return <NativeText style={[style, {color}]}>{children}</NativeText>;
|
||||
};
|
||||
|
||||
Text.propType = {
|
||||
style: TextPropTypes.style,
|
||||
type: PropTypes.oneOf(['primary', 'secondary', 'hint']),
|
||||
theme: PropTypes.object.isRequired,
|
||||
};
|
||||
@@ -0,0 +1,15 @@
|
||||
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 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';
|
||||
export { default as ColorPreferenceItem } from './ColorPreferenceItem/ColorPreferenceItem';
|
||||
export { default as LoadingMoreIndicator } from './LoadingMoreIndicator/LoadingMoreIndicator';
|
||||
export { Checkbox } from './Checkbox/Checkbox';
|
||||
export { RadioButton } from './RadioButton/RadioButton';
|
||||
@@ -1,57 +0,0 @@
|
||||
import * as SQLite from 'expo-sqlite';
|
||||
import {
|
||||
createNovelTableQuery,
|
||||
createUrlIndexQuery,
|
||||
createLibraryIndexQuery,
|
||||
} from './tables/NovelTable';
|
||||
import {
|
||||
createChapterTableQuery,
|
||||
createNovelIdIndexQuery,
|
||||
createUnreadChaptersIndexQuery,
|
||||
} from './tables/ChapterTable';
|
||||
import {
|
||||
createHistoryTableQuery,
|
||||
createChapterIdIndexQuery,
|
||||
} from './tables/HistoryTable';
|
||||
import {createDownloadTableQuery} from './tables/DownloadTable';
|
||||
import {
|
||||
createUpdatesTableQuery,
|
||||
// updatesSeedDataQuery,
|
||||
} from './tables/UpdateTable';
|
||||
|
||||
/**
|
||||
* Database Version = 2
|
||||
*/
|
||||
|
||||
const dbName = 'lnreader.db';
|
||||
|
||||
const db = SQLite.openDatabase(dbName);
|
||||
|
||||
export const createDB = () => {
|
||||
db.transaction(tx => {
|
||||
tx.executeSql(createNovelTableQuery);
|
||||
tx.executeSql(createChapterTableQuery);
|
||||
tx.executeSql(createHistoryTableQuery);
|
||||
tx.executeSql(createDownloadTableQuery);
|
||||
tx.executeSql(createUpdatesTableQuery);
|
||||
|
||||
/**
|
||||
* Indexes
|
||||
*/
|
||||
tx.executeSql(createUrlIndexQuery);
|
||||
tx.executeSql(createLibraryIndexQuery);
|
||||
tx.executeSql(createNovelIdIndexQuery);
|
||||
tx.executeSql(createUnreadChaptersIndexQuery);
|
||||
tx.executeSql(createChapterIdIndexQuery);
|
||||
});
|
||||
};
|
||||
|
||||
export const deleteDb = () => {
|
||||
db.transaction(tx => {
|
||||
tx.executeSql('DROP TABLE novels');
|
||||
tx.executeSql('DROP TABLE chapters');
|
||||
tx.executeSql('DROP TABLE history');
|
||||
tx.executeSql('DROP TABLE downloads');
|
||||
tx.executeSql('DROP TABLE updates');
|
||||
});
|
||||
};
|
||||
@@ -0,0 +1,98 @@
|
||||
import * as SQLite from 'expo-sqlite';
|
||||
import {
|
||||
createNovelTableQuery,
|
||||
createUrlIndexQuery,
|
||||
createLibraryIndexQuery,
|
||||
addCategoryColumnQuery,
|
||||
} from './tables/NovelTable';
|
||||
import {
|
||||
createChapterTableQuery,
|
||||
createNovelIdIndexQuery,
|
||||
createUnreadChaptersIndexQuery,
|
||||
} from './tables/ChapterTable';
|
||||
import {
|
||||
createHistoryTableQuery,
|
||||
createChapterIdIndexQuery,
|
||||
} from './tables/HistoryTable';
|
||||
import { createDownloadTableQuery } from './tables/DownloadTable';
|
||||
import { createUpdatesTableQuery } from './tables/UpdateTable';
|
||||
import {
|
||||
addCategorySortColumnQuery,
|
||||
createCategoriesTableQuery,
|
||||
createCategorydIndexQuery,
|
||||
createDefaultCategoryQuery,
|
||||
} from './tables/CategoryTable';
|
||||
import {
|
||||
dbTxnErrorCallback,
|
||||
txnErrorCallbackWithoutToast,
|
||||
} from './utils/helpers';
|
||||
import { noop } from 'lodash';
|
||||
|
||||
const dbName = 'lnreader.db';
|
||||
|
||||
const db = SQLite.openDatabase(dbName);
|
||||
|
||||
const createTables = () => {
|
||||
db.transaction(tx => {
|
||||
tx.executeSql(createCategoriesTableQuery, [], () => {
|
||||
tx.executeSql(
|
||||
addCategorySortColumnQuery,
|
||||
undefined,
|
||||
noop,
|
||||
txnErrorCallbackWithoutToast,
|
||||
);
|
||||
tx.executeSql(
|
||||
createDefaultCategoryQuery,
|
||||
undefined,
|
||||
noop,
|
||||
txnErrorCallbackWithoutToast,
|
||||
);
|
||||
});
|
||||
tx.executeSql(createNovelTableQuery, [], () => {
|
||||
tx.executeSql(
|
||||
addCategoryColumnQuery,
|
||||
undefined,
|
||||
noop,
|
||||
txnErrorCallbackWithoutToast,
|
||||
);
|
||||
});
|
||||
tx.executeSql(createChapterTableQuery);
|
||||
tx.executeSql(createHistoryTableQuery);
|
||||
tx.executeSql(createDownloadTableQuery);
|
||||
tx.executeSql(createUpdatesTableQuery);
|
||||
});
|
||||
};
|
||||
|
||||
const createIndexes = () => {
|
||||
db.transaction(tx => {
|
||||
tx.executeSql(createUrlIndexQuery);
|
||||
tx.executeSql(createLibraryIndexQuery);
|
||||
tx.executeSql(createNovelIdIndexQuery);
|
||||
tx.executeSql(createUnreadChaptersIndexQuery);
|
||||
tx.executeSql(createChapterIdIndexQuery);
|
||||
tx.executeSql(createCategorydIndexQuery);
|
||||
});
|
||||
};
|
||||
|
||||
export const createDatabase = async () => {
|
||||
createTables();
|
||||
createIndexes();
|
||||
};
|
||||
|
||||
/**
|
||||
* For Testing
|
||||
*/
|
||||
export const deleteDatabase = () => {
|
||||
db.transaction(
|
||||
tx => {
|
||||
tx.executeSql('DROP TABLE novels');
|
||||
tx.executeSql('DROP TABLE chapters');
|
||||
tx.executeSql('DROP TABLE history');
|
||||
tx.executeSql('DROP TABLE downloads');
|
||||
tx.executeSql('DROP TABLE updates');
|
||||
tx.executeSql('DROP TABLE categories');
|
||||
},
|
||||
dbTxnErrorCallback,
|
||||
noop,
|
||||
);
|
||||
};
|
||||
@@ -0,0 +1,97 @@
|
||||
import * as SQLite from 'expo-sqlite';
|
||||
import { noop } from 'lodash';
|
||||
import { Category } from '../types';
|
||||
import { txnErrorCallback } from '../utils/helpers';
|
||||
const db = SQLite.openDatabase('lnreader.db');
|
||||
|
||||
const getCategoriesQuery = `
|
||||
SELECT * FROM categories ORDER BY CASE WHEN id > 1 THEN 1 ELSE 0 END, IFNULL(sort, 9999)
|
||||
`;
|
||||
|
||||
export const getCategoriesFromDb = (): Promise<Category[]> => {
|
||||
return new Promise(resolve =>
|
||||
db.transaction(tx => {
|
||||
tx.executeSql(
|
||||
getCategoriesQuery,
|
||||
[],
|
||||
(txObj, { rows }) => resolve((rows as any)._array),
|
||||
txnErrorCallback,
|
||||
);
|
||||
}),
|
||||
);
|
||||
};
|
||||
|
||||
const createCategoryQuery = `
|
||||
INSERT INTO categories (name)
|
||||
VALUES
|
||||
(?)
|
||||
`;
|
||||
|
||||
export const createCategory = (categoryName: string): void =>
|
||||
db.transaction(tx =>
|
||||
tx.executeSql(createCategoryQuery, [categoryName], noop, txnErrorCallback),
|
||||
);
|
||||
|
||||
const deleteCategoryQuery = `
|
||||
DELETE FROM categories
|
||||
WHERE
|
||||
id = ?
|
||||
`;
|
||||
|
||||
export const deleteCategoryById = (categoryId: number): void =>
|
||||
db.transaction(tx =>
|
||||
tx.executeSql(deleteCategoryQuery, [categoryId], noop, txnErrorCallback),
|
||||
);
|
||||
|
||||
const updateCategoryQuery = `
|
||||
UPDATE categories SET name = ? WHERE id = ?
|
||||
`;
|
||||
|
||||
export const updateCategory = (
|
||||
categoryId: number,
|
||||
categoryName: string,
|
||||
): void =>
|
||||
db.transaction(tx =>
|
||||
tx.executeSql(
|
||||
updateCategoryQuery,
|
||||
[categoryName, categoryId],
|
||||
noop,
|
||||
txnErrorCallback,
|
||||
),
|
||||
);
|
||||
|
||||
const isCategoryNameDuplicateQuery = `
|
||||
SELECT COUNT(*) as isDuplicate FROM categories WHERE name = ?
|
||||
`;
|
||||
|
||||
export const isCategoryNameDuplicate = (
|
||||
categoryName: string,
|
||||
): Promise<boolean> => {
|
||||
return new Promise(resolve =>
|
||||
db.transaction(tx => {
|
||||
tx.executeSql(
|
||||
isCategoryNameDuplicateQuery,
|
||||
[categoryName],
|
||||
(txObj, { rows: { _array } }) =>
|
||||
resolve(Boolean(_array[0]?.isDuplicate)),
|
||||
txnErrorCallback,
|
||||
);
|
||||
}),
|
||||
);
|
||||
};
|
||||
|
||||
const updateCategoryOrderQuery = `
|
||||
UPDATE categories SET sort = ? WHERE id = ?
|
||||
`;
|
||||
|
||||
export const updateCategoryOrderInDb = (categories: Category[]): void =>
|
||||
db.transaction(tx => {
|
||||
categories.map(category => {
|
||||
tx.executeSql(
|
||||
updateCategoryOrderQuery,
|
||||
[category.sort, category.id],
|
||||
noop,
|
||||
txnErrorCallback,
|
||||
);
|
||||
});
|
||||
});
|
||||
@@ -1,359 +0,0 @@
|
||||
import * as SQLite from 'expo-sqlite';
|
||||
import {getSource} from '../../sources/sources';
|
||||
const db = SQLite.openDatabase('lnreader.db');
|
||||
|
||||
const insertChaptersQuery =
|
||||
'INSERT INTO chapters (chapterUrl, chapterName, releaseDate, novelId) values (?, ?, ?, ?)';
|
||||
|
||||
export const insertChapters = async (novelId, chapters) => {
|
||||
db.transaction(tx => {
|
||||
chapters.map(
|
||||
chapter =>
|
||||
tx.executeSql(
|
||||
insertChaptersQuery,
|
||||
[
|
||||
chapter.chapterUrl,
|
||||
chapter.chapterName,
|
||||
chapter.releaseDate,
|
||||
novelId,
|
||||
],
|
||||
(txObj, res) => {},
|
||||
(txObj, error) => {},
|
||||
// console.log(
|
||||
// "Error ",
|
||||
// error,
|
||||
// " Chapter URL: ",
|
||||
// chapter.chapterUrl,
|
||||
// " Novel ID ",
|
||||
// novelId
|
||||
// )
|
||||
),
|
||||
(txObj, res) => {
|
||||
// console.log('Success');
|
||||
},
|
||||
(txObj, error) => {
|
||||
// console.log('Error ', error)
|
||||
},
|
||||
);
|
||||
});
|
||||
};
|
||||
|
||||
const getChaptersQuery = (sort = 'ORDER BY chapterId ASC', filter = '') =>
|
||||
`SELECT * FROM chapters WHERE novelId = ? ${filter} ${sort}`;
|
||||
|
||||
export const getChapters = (novelId, sort, filter) => {
|
||||
return new Promise((resolve, reject) =>
|
||||
db.transaction(tx => {
|
||||
tx.executeSql(
|
||||
getChaptersQuery(sort, filter),
|
||||
[novelId],
|
||||
(txObj, {rows: {_array}}) => {
|
||||
resolve(_array);
|
||||
},
|
||||
(txObj, error) => {
|
||||
// console.log('Error ', error)
|
||||
},
|
||||
);
|
||||
}),
|
||||
);
|
||||
};
|
||||
|
||||
const getChapterQuery = 'SELECT * FROM downloads WHERE downloadChapterId = ?';
|
||||
|
||||
export const getChapterFromDB = async chapterId => {
|
||||
return new Promise((resolve, reject) =>
|
||||
db.transaction(tx => {
|
||||
tx.executeSql(
|
||||
getChapterQuery,
|
||||
[chapterId],
|
||||
(txObj, results) => {
|
||||
resolve(results.rows.item(0));
|
||||
},
|
||||
(txObj, error) => {
|
||||
// console.log('Error ', error)
|
||||
},
|
||||
);
|
||||
}),
|
||||
);
|
||||
};
|
||||
|
||||
export const getNextChapterFromDB = async (novelId, chapterId) => {
|
||||
return new Promise((resolve, reject) =>
|
||||
db.transaction(tx => {
|
||||
tx.executeSql(
|
||||
'SELECT * FROM chapters WHERE novelId = ? AND chapterId > ?',
|
||||
[novelId, chapterId],
|
||||
(txObj, results) => {
|
||||
resolve(results.rows.item(0));
|
||||
},
|
||||
(txObj, error) => {
|
||||
// console.log('Error ', error)
|
||||
},
|
||||
);
|
||||
}),
|
||||
);
|
||||
};
|
||||
|
||||
export const getPrevChapterFromDB = async (novelId, chapterId) => {
|
||||
return new Promise((resolve, reject) =>
|
||||
db.transaction(tx => {
|
||||
tx.executeSql(
|
||||
'SELECT * FROM chapters WHERE novelId = ? AND chapterId < ?',
|
||||
[novelId, chapterId],
|
||||
(txObj, results) => {
|
||||
resolve(results.rows.item(results.rows.length - 1));
|
||||
},
|
||||
(txObj, error) => {
|
||||
// console.log('Error ', error)
|
||||
},
|
||||
);
|
||||
}),
|
||||
);
|
||||
};
|
||||
|
||||
const markChapterReadQuery =
|
||||
'UPDATE chapters SET `read` = 1 WHERE chapterId = ?';
|
||||
|
||||
export const markChapterRead = async chapterId => {
|
||||
db.transaction(tx => {
|
||||
tx.executeSql(
|
||||
markChapterReadQuery,
|
||||
[chapterId],
|
||||
(txObj, res) => {},
|
||||
(txObj, error) => {
|
||||
// console.log(error)
|
||||
},
|
||||
);
|
||||
});
|
||||
};
|
||||
|
||||
const markChapterUnreadQuery =
|
||||
'UPDATE chapters SET `read` = 0 WHERE chapterId = ?';
|
||||
|
||||
export const markChapterUnread = async chapterId => {
|
||||
db.transaction(tx => {
|
||||
tx.executeSql(
|
||||
markChapterUnreadQuery,
|
||||
[chapterId],
|
||||
(txObj, res) => {},
|
||||
(txObj, error) => {
|
||||
// console.log(error)
|
||||
},
|
||||
);
|
||||
});
|
||||
};
|
||||
|
||||
const markAllChaptersReadQuery =
|
||||
'UPDATE chapters SET `read` = 1 WHERE novelId = ?';
|
||||
|
||||
export const markAllChaptersRead = async novelId => {
|
||||
db.transaction(tx => {
|
||||
tx.executeSql(
|
||||
markAllChaptersReadQuery,
|
||||
[novelId],
|
||||
(txObj, res) => {},
|
||||
(txObj, error) => {
|
||||
// console.log(error)
|
||||
},
|
||||
);
|
||||
});
|
||||
};
|
||||
|
||||
const markAllChaptersUnreadQuery =
|
||||
'UPDATE chapters SET `read` = 0 WHERE novelId = ?';
|
||||
|
||||
export const markAllChaptersUnread = async novelId => {
|
||||
db.transaction(tx => {
|
||||
tx.executeSql(
|
||||
markAllChaptersUnreadQuery,
|
||||
[novelId],
|
||||
(txObj, res) => {},
|
||||
(txObj, error) => {
|
||||
// console.log(error)
|
||||
},
|
||||
);
|
||||
});
|
||||
};
|
||||
|
||||
const isChapterDownloadedQuery =
|
||||
'SELECT * FROM downloads WHERE downloadChapterId=?';
|
||||
|
||||
export const isChapterDownloaded = async chapterId => {
|
||||
return new Promise((resolve, reject) =>
|
||||
db.transaction(tx => {
|
||||
tx.executeSql(
|
||||
isChapterDownloadedQuery,
|
||||
[chapterId],
|
||||
(txObj, res) => {
|
||||
if (res.rows.length !== 0) {
|
||||
resolve(true);
|
||||
} else {
|
||||
resolve(false);
|
||||
}
|
||||
},
|
||||
(txObj, error) => {
|
||||
// console.log('Error ', error)
|
||||
},
|
||||
);
|
||||
}),
|
||||
);
|
||||
};
|
||||
|
||||
const downloadChapterQuery =
|
||||
'INSERT INTO downloads (downloadChapterId, chapterName, chapterText) VALUES (?, ?, ?)';
|
||||
|
||||
export const downloadChapter = async (
|
||||
sourceId,
|
||||
novelUrl,
|
||||
chapterUrl,
|
||||
chapterId,
|
||||
) => {
|
||||
const source = getSource(sourceId);
|
||||
|
||||
const chapter = await source.parseChapter(novelUrl, chapterUrl);
|
||||
|
||||
db.transaction(tx => {
|
||||
tx.executeSql('UPDATE chapters SET downloaded = 1 WHERE chapterId = ?', [
|
||||
chapterId,
|
||||
]);
|
||||
tx.executeSql(
|
||||
downloadChapterQuery,
|
||||
[chapterId, chapter.chapterName, chapter.chapterText],
|
||||
(txObj, res) => {
|
||||
// console.log(`Downloaded Chapter ${chapter.chapterUrl}`);
|
||||
},
|
||||
(txObj, error) => {
|
||||
// console.log('Error ', error)
|
||||
},
|
||||
);
|
||||
});
|
||||
};
|
||||
|
||||
export const deleteChapter = async chapterId => {
|
||||
const updateIsDownloadedQuery =
|
||||
'UPDATE chapters SET downloaded = 0 WHERE chapterId=?';
|
||||
const deleteChapterQuery = 'DELETE FROM downloads WHERE downloadChapterId=?';
|
||||
|
||||
db.transaction(tx => {
|
||||
tx.executeSql(
|
||||
updateIsDownloadedQuery,
|
||||
[chapterId],
|
||||
(txObj, res) => {},
|
||||
(txObj, error) => {
|
||||
// console.log('Error ', error)
|
||||
},
|
||||
);
|
||||
tx.executeSql(
|
||||
deleteChapterQuery,
|
||||
[chapterId],
|
||||
(txObj, res) => {
|
||||
// console.log('Chapter deleted');
|
||||
},
|
||||
(txObj, error) => {
|
||||
// console.log('Error ', error)
|
||||
},
|
||||
);
|
||||
});
|
||||
};
|
||||
|
||||
const getLastReadChapterQuery = `
|
||||
SELECT chapters.*
|
||||
FROM history
|
||||
JOIN chapters
|
||||
ON history.historyChapterId = chapters.chapterId
|
||||
WHERE history.historyNovelId = ?
|
||||
`;
|
||||
|
||||
export const getLastReadChapter = async novelId => {
|
||||
return new Promise((resolve, reject) => {
|
||||
db.transaction(tx => {
|
||||
tx.executeSql(
|
||||
getLastReadChapterQuery,
|
||||
[novelId],
|
||||
(txObj, {rows}) => resolve(rows.item(0)),
|
||||
(txObj, error) => {
|
||||
// console.log('Error ', error)
|
||||
},
|
||||
);
|
||||
});
|
||||
});
|
||||
};
|
||||
|
||||
const bookmarkChapterQuery =
|
||||
'UPDATE chapters SET bookmark = ? WHERE chapterId = ?';
|
||||
|
||||
export const bookmarkChapter = async (bookmark, chapterId) => {
|
||||
db.transaction(tx => {
|
||||
tx.executeSql(
|
||||
bookmarkChapterQuery,
|
||||
[!bookmark, chapterId],
|
||||
(txObj, res) => {},
|
||||
(txObj, error) => {
|
||||
// console.log('Error ', error)
|
||||
},
|
||||
);
|
||||
});
|
||||
};
|
||||
|
||||
const markPreviuschaptersReadQuery =
|
||||
'UPDATE chapters SET `read` = 1 WHERE chapterId < ? AND novelId = ?';
|
||||
|
||||
export const markPreviuschaptersRead = async (chapterId, novelId) => {
|
||||
db.transaction(tx => {
|
||||
tx.executeSql(
|
||||
markPreviuschaptersReadQuery,
|
||||
[chapterId, novelId],
|
||||
(txObj, res) => {},
|
||||
(txObj, error) => {
|
||||
// console.log(error)
|
||||
},
|
||||
);
|
||||
});
|
||||
};
|
||||
|
||||
const markPreviousChaptersUnreadQuery =
|
||||
'UPDATE chapters SET `read` = 0 WHERE chapterId < ? AND novelId = ?';
|
||||
|
||||
export const markPreviousChaptersUnread = async (chapterId, novelId) => {
|
||||
db.transaction(tx => {
|
||||
tx.executeSql(
|
||||
markPreviousChaptersUnreadQuery,
|
||||
[chapterId, novelId],
|
||||
(txObj, res) => {},
|
||||
(txObj, error) => {
|
||||
// console.log(error)
|
||||
},
|
||||
);
|
||||
});
|
||||
};
|
||||
|
||||
const getDownloadedChaptersQuery = `
|
||||
SELECT chapters.*, novels.sourceId, novels.novelName, novels.novelCover, novels.novelUrl
|
||||
FROM chapters
|
||||
JOIN novels
|
||||
ON chapters.novelId = novels.novelId
|
||||
WHERE chapters.downloaded = 1`;
|
||||
|
||||
export const getDownloadedChapters = () => {
|
||||
return new Promise((resolve, reject) =>
|
||||
db.transaction(tx => {
|
||||
tx.executeSql(
|
||||
getDownloadedChaptersQuery,
|
||||
null,
|
||||
(txObj, {rows: {_array}}) => {
|
||||
resolve(_array);
|
||||
},
|
||||
(txObj, error) => {
|
||||
// console.log('Error ', error)
|
||||
},
|
||||
);
|
||||
}),
|
||||
);
|
||||
};
|
||||
|
||||
export const deleteDownloads = async () => {
|
||||
db.transaction(tx => {
|
||||
tx.executeSql('UPDATE chapters SET downloaded = 0');
|
||||
tx.executeSql('DELETE FROM downloads; VACCUM;');
|
||||
});
|
||||
};
|
||||
@@ -0,0 +1,549 @@
|
||||
import * as SQLite from 'expo-sqlite';
|
||||
import { showToast } from '../../hooks/showToast';
|
||||
import { sourceManager } from '../../sources/sourceManager';
|
||||
import { ChapterItem } from '../types';
|
||||
|
||||
import * as cheerio from 'cheerio';
|
||||
import RNFetchBlob from 'rn-fetch-blob';
|
||||
import { txnErrorCallback } from '@database/utils/helpers';
|
||||
|
||||
const db = SQLite.openDatabase('lnreader.db');
|
||||
|
||||
const insertChaptersQuery =
|
||||
'INSERT INTO chapters (chapterUrl, chapterName, releaseDate, novelId) values (?, ?, ?, ?)';
|
||||
|
||||
export const insertChapters = async (
|
||||
novelId: number,
|
||||
chapters: ChapterItem[],
|
||||
) => {
|
||||
db.transaction(
|
||||
tx => {
|
||||
chapters.map(chapter =>
|
||||
tx.executeSql(
|
||||
insertChaptersQuery,
|
||||
[
|
||||
chapter.chapterUrl,
|
||||
chapter.chapterName,
|
||||
chapter.releaseDate,
|
||||
novelId,
|
||||
],
|
||||
(_txObj, _res) => {},
|
||||
(_txObj, _error) => false,
|
||||
// console.log(
|
||||
// "Error ",
|
||||
// error,
|
||||
// " Chapter URL: ",
|
||||
// chapter.chapterUrl,
|
||||
// " Novel ID ",
|
||||
// novelId
|
||||
// )
|
||||
),
|
||||
);
|
||||
},
|
||||
_err => {
|
||||
// console.log('Error', err);
|
||||
},
|
||||
() => {
|
||||
// console.log('Success ')
|
||||
},
|
||||
);
|
||||
};
|
||||
|
||||
const getChaptersQuery = (sort = 'ORDER BY chapterId ASC', filter = '') =>
|
||||
`SELECT * FROM chapters WHERE novelId = ? ${filter} ${sort}`;
|
||||
|
||||
export const getChapters = (novelId: number, sort: string, filter: string) => {
|
||||
return new Promise(resolve =>
|
||||
db.transaction(tx => {
|
||||
tx.executeSql(
|
||||
getChaptersQuery(sort, filter),
|
||||
[novelId],
|
||||
(txObj, { rows }) => {
|
||||
const _array = (rows as any)._array;
|
||||
resolve(_array);
|
||||
},
|
||||
(_txObj, _error) => {
|
||||
// console.log('Error ', error)
|
||||
return false;
|
||||
},
|
||||
);
|
||||
}),
|
||||
);
|
||||
};
|
||||
|
||||
const getChapterQuery = 'SELECT * FROM downloads WHERE downloadChapterId = ?';
|
||||
|
||||
export const getChapterFromDB = async (chapterId: number) => {
|
||||
return new Promise(resolve =>
|
||||
db.transaction(tx => {
|
||||
tx.executeSql(
|
||||
getChapterQuery,
|
||||
[chapterId],
|
||||
(txObj, results) => {
|
||||
resolve(results.rows.item(0));
|
||||
},
|
||||
(_txObj, _error) => {
|
||||
// console.log('Error ', error)
|
||||
return false;
|
||||
},
|
||||
);
|
||||
}),
|
||||
);
|
||||
};
|
||||
|
||||
const getPrevChapterQuery = `
|
||||
SELECT
|
||||
*
|
||||
FROM
|
||||
chapters
|
||||
WHERE
|
||||
novelId = ?
|
||||
AND
|
||||
chapterId < ?
|
||||
`;
|
||||
|
||||
export const getPrevChapter = (
|
||||
novelId: number,
|
||||
chapterId: number,
|
||||
): Promise<ChapterItem> => {
|
||||
return new Promise(resolve =>
|
||||
db.transaction(tx => {
|
||||
tx.executeSql(
|
||||
getPrevChapterQuery,
|
||||
[novelId, chapterId],
|
||||
(_txObj, results) =>
|
||||
resolve(results.rows.item(results.rows.length - 1)),
|
||||
(_txObj, error) => {
|
||||
showToast(error.message);
|
||||
return false;
|
||||
},
|
||||
);
|
||||
}),
|
||||
);
|
||||
};
|
||||
|
||||
const getNextChapterQuery = `
|
||||
SELECT
|
||||
*
|
||||
FROM
|
||||
chapters
|
||||
WHERE
|
||||
novelId = ?
|
||||
AND
|
||||
chapterId > ?
|
||||
`;
|
||||
|
||||
export const getNextChapter = (
|
||||
novelId: number,
|
||||
chapterId: number,
|
||||
): Promise<ChapterItem> => {
|
||||
return new Promise(resolve =>
|
||||
db.transaction(tx => {
|
||||
tx.executeSql(
|
||||
getNextChapterQuery,
|
||||
[novelId, chapterId],
|
||||
(_txObj, results) => resolve(results.rows.item(0)),
|
||||
(_txObj, error) => {
|
||||
showToast(error.message);
|
||||
return false;
|
||||
},
|
||||
);
|
||||
}),
|
||||
);
|
||||
};
|
||||
|
||||
const markChapterReadQuery =
|
||||
'UPDATE chapters SET `read` = 1 WHERE chapterId = ?';
|
||||
|
||||
export const markChapterRead = async (chapterId: number) => {
|
||||
db.transaction(tx => {
|
||||
tx.executeSql(
|
||||
markChapterReadQuery,
|
||||
[chapterId],
|
||||
(_txObj, _res) => {},
|
||||
(_txObj, _error) => {
|
||||
// console.log(error)
|
||||
return false;
|
||||
},
|
||||
);
|
||||
});
|
||||
};
|
||||
|
||||
const markChapterUnreadQuery =
|
||||
'UPDATE chapters SET `read` = 0 WHERE chapterId = ?';
|
||||
|
||||
export const markChapterUnread = async (chapterId: number) => {
|
||||
db.transaction(tx => {
|
||||
tx.executeSql(
|
||||
markChapterUnreadQuery,
|
||||
[chapterId],
|
||||
(_txObj, _res) => {},
|
||||
(_txObj, _error) => {
|
||||
// console.log(error)
|
||||
return false;
|
||||
},
|
||||
);
|
||||
});
|
||||
};
|
||||
|
||||
const markAllChaptersReadQuery =
|
||||
'UPDATE chapters SET `read` = 1 WHERE novelId = ?';
|
||||
|
||||
export const markAllChaptersRead = async (novelId: number) => {
|
||||
db.transaction(tx => {
|
||||
tx.executeSql(
|
||||
markAllChaptersReadQuery,
|
||||
[novelId],
|
||||
(_txObj, _res) => {},
|
||||
(_txObj, _error) => {
|
||||
// console.log(error)
|
||||
return false;
|
||||
},
|
||||
);
|
||||
});
|
||||
};
|
||||
|
||||
const markAllChaptersUnreadQuery =
|
||||
'UPDATE chapters SET `read` = 0 WHERE novelId = ?';
|
||||
|
||||
export const markAllChaptersUnread = async (novelId: number) => {
|
||||
db.transaction(tx => {
|
||||
tx.executeSql(
|
||||
markAllChaptersUnreadQuery,
|
||||
[novelId],
|
||||
(_txObj, _res) => {},
|
||||
(_txObj, _error) => {
|
||||
// console.log(error)
|
||||
return false;
|
||||
},
|
||||
);
|
||||
});
|
||||
};
|
||||
|
||||
const isChapterDownloadedQuery =
|
||||
'SELECT * FROM downloads WHERE downloadChapterId=?';
|
||||
|
||||
export const isChapterDownloaded = async (chapterId: number) => {
|
||||
return new Promise(resolve =>
|
||||
db.transaction(tx => {
|
||||
tx.executeSql(
|
||||
isChapterDownloadedQuery,
|
||||
[chapterId],
|
||||
(txObj, res) => {
|
||||
if (res.rows.length !== 0) {
|
||||
resolve(true);
|
||||
} else {
|
||||
resolve(false);
|
||||
}
|
||||
},
|
||||
(_txObj, _error) => {
|
||||
// console.log('Error ', error)
|
||||
return false;
|
||||
},
|
||||
);
|
||||
}),
|
||||
);
|
||||
};
|
||||
|
||||
const downloadChapterQuery =
|
||||
'INSERT INTO downloads (downloadChapterId, chapterName, chapterText) VALUES (?, ?, ?)';
|
||||
|
||||
const createImageFolder = async (
|
||||
path: string,
|
||||
data?: {
|
||||
sourceId: number;
|
||||
novelId: number;
|
||||
chapterId: number;
|
||||
},
|
||||
): Promise<string> => {
|
||||
const mkdirIfNot = async (p: string) => {
|
||||
if (!(await RNFetchBlob.fs.exists(p))) {
|
||||
await RNFetchBlob.fs.mkdir(p);
|
||||
}
|
||||
};
|
||||
await mkdirIfNot(path);
|
||||
if (data) {
|
||||
const { sourceId, novelId, chapterId } = data;
|
||||
await mkdirIfNot(`${path}/${sourceId}/`);
|
||||
await mkdirIfNot(`${path}/${sourceId}/${novelId}/`);
|
||||
await mkdirIfNot(`${path}/${sourceId}/${novelId}/${chapterId}/`);
|
||||
return `${path}/${sourceId}/${novelId}/${chapterId}/`;
|
||||
} else {
|
||||
return path;
|
||||
}
|
||||
};
|
||||
|
||||
const downloadImages = async (
|
||||
html: string,
|
||||
sourceId: number,
|
||||
novelId: number,
|
||||
chapterId: number,
|
||||
): Promise<string> => {
|
||||
try {
|
||||
const loadedCheerio = cheerio.load(html);
|
||||
const imgs = loadedCheerio('img').toArray();
|
||||
for (let i = 0; i < imgs.length; i++) {
|
||||
const elem = loadedCheerio(imgs[i]);
|
||||
const url = elem.attr('src');
|
||||
if (url) {
|
||||
const imageb64 = (await RNFetchBlob.fetch('GET', url)).base64();
|
||||
const fileurl =
|
||||
(await createImageFolder(
|
||||
`${RNFetchBlob.fs.dirs.DownloadDir}/LNReader`,
|
||||
{ sourceId, novelId, chapterId },
|
||||
)) +
|
||||
i +
|
||||
'.b64.png';
|
||||
elem.replaceWith(
|
||||
`<img type="file" src="${url}" file-src="${fileurl}" file-id="${i}">`,
|
||||
);
|
||||
const exists = await RNFetchBlob.fs.exists(fileurl);
|
||||
if (!exists) {
|
||||
RNFetchBlob.fs.createFile(fileurl, imageb64, 'utf8');
|
||||
} else {
|
||||
RNFetchBlob.fs.writeFile(fileurl, imageb64, 'utf8');
|
||||
}
|
||||
}
|
||||
}
|
||||
return loadedCheerio.html();
|
||||
} catch (e) {
|
||||
return html;
|
||||
}
|
||||
};
|
||||
|
||||
export const downloadChapter = async (
|
||||
sourceId: number,
|
||||
novelUrl: string,
|
||||
novelId: number,
|
||||
chapterUrl: string,
|
||||
chapterId: number,
|
||||
) => {
|
||||
try {
|
||||
const source = sourceManager(sourceId);
|
||||
|
||||
const chapter = await source.parseChapter(novelUrl, chapterUrl);
|
||||
|
||||
if (chapter.chapterText?.length) {
|
||||
const imagedChapterText =
|
||||
chapter.chapterText &&
|
||||
(await downloadImages(
|
||||
chapter.chapterText,
|
||||
sourceId,
|
||||
novelId,
|
||||
chapterId,
|
||||
));
|
||||
|
||||
db.transaction(tx => {
|
||||
tx.executeSql(
|
||||
'UPDATE chapters SET downloaded = 1 WHERE chapterId = ?',
|
||||
[chapterId],
|
||||
);
|
||||
tx.executeSql(
|
||||
downloadChapterQuery,
|
||||
[chapterId, chapter.chapterName, imagedChapterText],
|
||||
(_txObj, _res) => {
|
||||
return true;
|
||||
},
|
||||
txnErrorCallback,
|
||||
);
|
||||
});
|
||||
} else {
|
||||
throw new Error("Either chapter is empty or the app couldn't scrape it");
|
||||
}
|
||||
} catch (error) {
|
||||
throw error;
|
||||
}
|
||||
};
|
||||
|
||||
const deleteDownloadedImages = async (
|
||||
sourceId: number,
|
||||
novelId: number,
|
||||
chapterId: number,
|
||||
) => {
|
||||
try {
|
||||
const path = `${RNFetchBlob.fs.dirs.DownloadDir}/LNReader/`;
|
||||
const files = await RNFetchBlob.fs.ls(
|
||||
await createImageFolder(path, { sourceId, novelId, chapterId }),
|
||||
);
|
||||
for (let i = 0; i < files.length; i++) {
|
||||
const ex = /(.*?)_(.*?)#(.*?)/.exec(files[i]);
|
||||
if (ex) {
|
||||
if (
|
||||
parseInt(ex[1], 10) === sourceId &&
|
||||
parseInt(ex[2], 10) === chapterId
|
||||
) {
|
||||
if (await RNFetchBlob.fs.exists(`${path}${files[i]}`)) {
|
||||
RNFetchBlob.fs.unlink(`${path}${files[i]}`);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
} catch (error) {
|
||||
showToast(error.message);
|
||||
}
|
||||
};
|
||||
|
||||
export const deleteChapter = async (
|
||||
sourceId: number,
|
||||
novelId: number,
|
||||
chapterId: number,
|
||||
) => {
|
||||
const updateIsDownloadedQuery =
|
||||
'UPDATE chapters SET downloaded = 0 WHERE chapterId=?';
|
||||
const deleteChapterQuery = 'DELETE FROM downloads WHERE downloadChapterId=?';
|
||||
|
||||
await deleteDownloadedImages(sourceId, novelId, chapterId);
|
||||
|
||||
db.transaction(tx => {
|
||||
tx.executeSql(
|
||||
updateIsDownloadedQuery,
|
||||
[chapterId],
|
||||
(_txObj, _res) => {},
|
||||
(_txObj, _error) => {
|
||||
// console.log('Error ', error)
|
||||
return false;
|
||||
},
|
||||
);
|
||||
tx.executeSql(
|
||||
deleteChapterQuery,
|
||||
[chapterId],
|
||||
(_txObj, _res) => {
|
||||
// console.log('Chapter deleted');
|
||||
},
|
||||
(_txObj, _error) => {
|
||||
// console.log('Error ', error)
|
||||
return false;
|
||||
},
|
||||
);
|
||||
});
|
||||
};
|
||||
|
||||
const getLastReadChapterQuery = `
|
||||
SELECT chapters.*
|
||||
FROM history
|
||||
JOIN chapters
|
||||
ON history.historyChapterId = chapters.chapterId
|
||||
WHERE history.historyNovelId = ?
|
||||
`;
|
||||
|
||||
export const getLastReadChapter = async (novelId: number) => {
|
||||
return new Promise(resolve => {
|
||||
db.transaction(tx => {
|
||||
tx.executeSql(
|
||||
getLastReadChapterQuery,
|
||||
[novelId],
|
||||
(txObj, { rows }) => resolve(rows.item(0)),
|
||||
(_txObj, _error) => {
|
||||
// console.log('Error ', error)
|
||||
return false;
|
||||
},
|
||||
);
|
||||
});
|
||||
});
|
||||
};
|
||||
|
||||
const bookmarkChapterQuery =
|
||||
'UPDATE chapters SET bookmark = ? WHERE chapterId = ?';
|
||||
|
||||
export const bookmarkChapter = async (bookmark: boolean, chapterId: number) => {
|
||||
db.transaction(tx => {
|
||||
tx.executeSql(
|
||||
bookmarkChapterQuery,
|
||||
[!bookmark, chapterId],
|
||||
(_txObj, _res) => {},
|
||||
(_txObj, _error) => {
|
||||
// console.log('Error ', error)
|
||||
return false;
|
||||
},
|
||||
);
|
||||
});
|
||||
};
|
||||
|
||||
const markPreviuschaptersReadQuery =
|
||||
'UPDATE chapters SET `read` = 1 WHERE chapterId < ? AND novelId = ?';
|
||||
|
||||
export const markPreviuschaptersRead = async (
|
||||
chapterId: number,
|
||||
novelId: number,
|
||||
) => {
|
||||
db.transaction(tx => {
|
||||
tx.executeSql(
|
||||
markPreviuschaptersReadQuery,
|
||||
[chapterId, novelId],
|
||||
(_txObj, _res) => {},
|
||||
(_txObj, _error) => {
|
||||
// console.log(error)
|
||||
return false;
|
||||
},
|
||||
);
|
||||
});
|
||||
};
|
||||
|
||||
const markPreviousChaptersUnreadQuery =
|
||||
'UPDATE chapters SET `read` = 0 WHERE chapterId < ? AND novelId = ?';
|
||||
|
||||
export const markPreviousChaptersUnread = async (
|
||||
chapterId: number,
|
||||
novelId: number,
|
||||
) => {
|
||||
db.transaction(tx => {
|
||||
tx.executeSql(
|
||||
markPreviousChaptersUnreadQuery,
|
||||
[chapterId, novelId],
|
||||
(_txObj, _res) => {},
|
||||
(_txObj, _error) => {
|
||||
// console.log(error)
|
||||
return false;
|
||||
},
|
||||
);
|
||||
});
|
||||
};
|
||||
|
||||
const getDownloadedChaptersQuery = `
|
||||
SELECT chapters.*, novels.sourceId, novels.novelName, novels.novelCover, novels.novelUrl
|
||||
FROM chapters
|
||||
JOIN novels
|
||||
ON chapters.novelId = novels.novelId
|
||||
WHERE chapters.downloaded = 1`;
|
||||
|
||||
export const getDownloadedChapters = () => {
|
||||
return new Promise(resolve =>
|
||||
db.transaction(tx => {
|
||||
tx.executeSql(
|
||||
getDownloadedChaptersQuery,
|
||||
undefined,
|
||||
(txObj, { rows }) => {
|
||||
const _array = (rows as any)._array;
|
||||
resolve(_array);
|
||||
},
|
||||
(_txObj, _error) => {
|
||||
// console.log('Error ', error)
|
||||
return false;
|
||||
},
|
||||
);
|
||||
}),
|
||||
);
|
||||
};
|
||||
|
||||
export const deleteDownloads = async () => {
|
||||
db.transaction(tx => {
|
||||
tx.executeSql('UPDATE chapters SET downloaded = 0');
|
||||
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,134 @@
|
||||
/* 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),
|
||||
);
|
||||
}),
|
||||
);
|
||||
};
|
||||
|
||||
const deleteReadChaptersFromDbQuery = `
|
||||
DELETE FROM
|
||||
downloads
|
||||
WHERE
|
||||
downloads.downloadChapterId IN (
|
||||
SELECT
|
||||
chapters.chapterId
|
||||
FROM
|
||||
downloads
|
||||
INNER JOIN chapters ON chapters.chapterId = downloads.downloadChapterId
|
||||
WHERE
|
||||
chapters.read = 1
|
||||
);
|
||||
`;
|
||||
|
||||
let updateChaptersDeletedQuery = `
|
||||
UPDATE
|
||||
chapters
|
||||
SET
|
||||
downloaded = 0
|
||||
WHERE
|
||||
chapters.chapterId IN (
|
||||
SELECT
|
||||
downloads.downloadChapterId
|
||||
FROM
|
||||
downloads
|
||||
INNER JOIN chapters ON chapters.chapterId = downloads.downloadChapterId
|
||||
WHERE
|
||||
chapters.read = 1
|
||||
);
|
||||
`;
|
||||
|
||||
export const deleteReadChaptersFromDb = (chapterId: number) => {
|
||||
db.transaction(tx => {
|
||||
tx.executeSql(
|
||||
updateChaptersDeletedQuery,
|
||||
[],
|
||||
(txObj, res) => {},
|
||||
(txObj, error) => showToast(error.message),
|
||||
);
|
||||
tx.executeSql(
|
||||
deleteReadChaptersFromDbQuery,
|
||||
[],
|
||||
(txObj, res) => showToast('Deleted read chapters.'),
|
||||
(txObj, error) => showToast(error.message),
|
||||
);
|
||||
});
|
||||
};
|
||||
@@ -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 = ?',
|
||||
|
||||
@@ -1,79 +0,0 @@
|
||||
import * as SQLite from 'expo-sqlite';
|
||||
const db = SQLite.openDatabase('lnreader.db');
|
||||
|
||||
const getLibraryQuery = (sort, filter) => `
|
||||
SELECT novels.*, C.chaptersUnread, D.chaptersDownloaded
|
||||
FROM novels
|
||||
LEFT JOIN (
|
||||
SELECT chapters.novelId, COUNT(*) AS chaptersUnread
|
||||
FROM chapters
|
||||
WHERE chapters.read = 0
|
||||
GROUP BY chapters.novelId
|
||||
) AS C
|
||||
ON novels.novelId = C.novelId
|
||||
LEFT JOIN (
|
||||
SELECT chapters.novelId, COUNT(*) AS chaptersDownloaded
|
||||
FROM chapters
|
||||
WHERE chapters.downloaded = 1
|
||||
GROUP BY chapters.novelId
|
||||
) AS D
|
||||
ON novels.novelId = D.novelId
|
||||
WHERE novels.followed = 1 ${filter ? 'AND ' + filter : ''}
|
||||
${sort ? 'ORDER BY ' + sort : ''}
|
||||
`;
|
||||
|
||||
export const getLibrary = (sort, filter) => {
|
||||
return new Promise((resolve, reject) =>
|
||||
db.transaction(tx => {
|
||||
tx.executeSql(
|
||||
getLibraryQuery(sort, filter),
|
||||
null,
|
||||
(txObj, {rows: {_array}}) => {
|
||||
// console.log(_array);
|
||||
resolve(_array);
|
||||
},
|
||||
(txObj, error) => {
|
||||
// console.log('Error ', error);
|
||||
},
|
||||
);
|
||||
}),
|
||||
);
|
||||
};
|
||||
|
||||
const searchLibraryQuery = (searchText, sort, filter) =>
|
||||
`
|
||||
SELECT novels.*, C.chaptersUnread, D.chaptersDownloaded
|
||||
FROM novels
|
||||
LEFT JOIN (
|
||||
SELECT chapters.novelId, COUNT(*) AS chaptersUnread
|
||||
FROM chapters
|
||||
WHERE chapters.read = 0
|
||||
GROUP BY chapters.novelId
|
||||
) AS C
|
||||
ON novels.novelId = C.novelId
|
||||
LEFT JOIN (
|
||||
SELECT chapters.novelId, COUNT(*) AS chaptersDownloaded
|
||||
FROM chapters
|
||||
WHERE chapters.downloaded = 1
|
||||
GROUP BY chapters.novelId
|
||||
) AS D
|
||||
ON novels.novelId = D.novelId
|
||||
WHERE novels.followed = 1 AND novelName LIKE '%${searchText}%' ${
|
||||
filter ? 'AND ' + filter : ''
|
||||
}
|
||||
${sort ? 'ORDER BY ' + sort : ''} `;
|
||||
|
||||
export const searchLibrary = (searchText, sort, filter) => {
|
||||
return new Promise((resolve, reject) => {
|
||||
db.transaction(tx => {
|
||||
tx.executeSql(
|
||||
searchLibraryQuery(searchText, sort, filter),
|
||||
null,
|
||||
(txObj, {rows: {_array}}) => resolve(_array),
|
||||
(txObj, error) => {
|
||||
// console.log('Error ', error);
|
||||
},
|
||||
);
|
||||
});
|
||||
});
|
||||
};
|
||||
@@ -0,0 +1,96 @@
|
||||
import { LibraryFilter } from '@screens/library/constants/constants';
|
||||
import * as SQLite from 'expo-sqlite';
|
||||
import { LibraryNovelInfo, NovelInfo } from '../types';
|
||||
import { txnErrorCallback } from '../utils/helpers';
|
||||
|
||||
const db = SQLite.openDatabase('lnreader.db');
|
||||
|
||||
const getLibraryQuery = `
|
||||
SELECT novels.*, C.chaptersUnread, D.chaptersDownloaded, H.lastReadAt
|
||||
FROM novels
|
||||
LEFT JOIN (
|
||||
SELECT chapters.novelId, COUNT(*) AS chaptersUnread
|
||||
FROM chapters
|
||||
WHERE chapters.read = 0
|
||||
GROUP BY chapters.novelId
|
||||
) AS C
|
||||
ON novels.novelId = C.novelId
|
||||
LEFT JOIN (
|
||||
SELECT chapters.novelId, COUNT(*) AS chaptersDownloaded
|
||||
FROM chapters
|
||||
WHERE chapters.downloaded = 1
|
||||
GROUP BY chapters.novelId
|
||||
) AS D
|
||||
ON novels.novelId = D.novelId
|
||||
LEFT JOIN (
|
||||
SELECT history.historyNovelId as novelId, historyTimeRead AS lastReadAt
|
||||
FROM history
|
||||
GROUP BY history.historyNovelId
|
||||
HAVING history.historyTimeRead = MAX(history.historyTimeRead)
|
||||
ORDER BY history.historyTimeRead DESC
|
||||
) AS H
|
||||
ON novels.novelId = H.novelId
|
||||
WHERE novels.followed = 1
|
||||
`;
|
||||
|
||||
export const getLibrary = ({
|
||||
filter,
|
||||
searchText,
|
||||
sortOrder,
|
||||
downloadedOnlyMode,
|
||||
}: {
|
||||
sortOrder?: string;
|
||||
filter?: string;
|
||||
searchText?: string;
|
||||
downloadedOnlyMode?: boolean;
|
||||
}): Promise<LibraryNovelInfo[]> => {
|
||||
let query = getLibraryQuery;
|
||||
|
||||
if (filter) {
|
||||
query += ` AND ${filter}`;
|
||||
}
|
||||
|
||||
if (downloadedOnlyMode) {
|
||||
query += ' ' + LibraryFilter.DownloadedOnly;
|
||||
}
|
||||
|
||||
if (searchText) {
|
||||
query += ` AND novelName LIKE '%${searchText}%'`;
|
||||
}
|
||||
|
||||
if (sortOrder) {
|
||||
query += ` ORDER BY ${sortOrder}`;
|
||||
}
|
||||
|
||||
return new Promise(resolve =>
|
||||
db.transaction(tx => {
|
||||
tx.executeSql(
|
||||
query,
|
||||
undefined,
|
||||
(txObj, { rows: { _array } }) => resolve(_array),
|
||||
txnErrorCallback,
|
||||
);
|
||||
}),
|
||||
);
|
||||
};
|
||||
|
||||
export const getLibraryNovelsFromDb = (
|
||||
onlyOngoingNovels?: boolean,
|
||||
): Promise<NovelInfo[]> => {
|
||||
let getLibraryNovelsQuery = 'SELECT * FROM novels WHERE followed = 1';
|
||||
|
||||
if (onlyOngoingNovels) {
|
||||
getLibraryNovelsQuery += " AND status NOT LIKE 'Completed'";
|
||||
}
|
||||
|
||||
return new Promise(resolve =>
|
||||
db.transaction(tx => {
|
||||
tx.executeSql(
|
||||
getLibraryNovelsQuery,
|
||||
undefined,
|
||||
(txObj, { rows: { _array } }) => resolve(_array),
|
||||
txnErrorCallback,
|
||||
);
|
||||
}),
|
||||
);
|
||||
};
|
||||
@@ -4,13 +4,15 @@ 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';
|
||||
import { txnErrorCallback } from '../utils/helpers';
|
||||
import { noop } from 'lodash';
|
||||
|
||||
const insertNovelQuery =
|
||||
'INSERT INTO novels (novelUrl, sourceUrl, sourceId, source, novelName, novelCover, novelSummary, author, artist, status, genre) VALUES(?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)';
|
||||
'INSERT INTO novels (novelUrl, sourceUrl, sourceId, source, novelName, novelCover, novelSummary, author, artist, status, genre, categoryIds) VALUES(?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)';
|
||||
|
||||
export const insertNovel = async novel => {
|
||||
return new Promise((resolve, reject) =>
|
||||
@@ -23,17 +25,16 @@ 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,
|
||||
JSON.stringify(novel.categoryIds),
|
||||
],
|
||||
(txObj, {insertId}) => resolve(insertId),
|
||||
(txObj, error) => {
|
||||
// console.log('Error ', error)
|
||||
},
|
||||
(txObj, { insertId }) => resolve(insertId),
|
||||
txnErrorCallback,
|
||||
),
|
||||
),
|
||||
);
|
||||
@@ -94,7 +95,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 +121,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 +140,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 +202,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 +220,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,17 +248,17 @@ 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;
|
||||
|
||||
db.transaction(tx => {
|
||||
tx.executeSql(
|
||||
'UPDATE novels SET novelCover = ? WHERE novelId = ?',
|
||||
[image.uri, novelId],
|
||||
[uri, novelId],
|
||||
(txObj, res) => {},
|
||||
(txObj, error) => {
|
||||
// console.log('Error ', error)
|
||||
},
|
||||
(txObj, error) => showToast(`Error: ${error}`),
|
||||
);
|
||||
});
|
||||
|
||||
|
||||
@@ -0,0 +1,205 @@
|
||||
import { noop, xor } from 'lodash';
|
||||
|
||||
import * as SQLite from 'expo-sqlite';
|
||||
const db = SQLite.openDatabase('lnreader.db');
|
||||
|
||||
import { txnErrorCallback } from '../utils/helpers';
|
||||
import { LibraryNovelInfo } from '../types';
|
||||
import { fetchNovel } from '../../services/Source/source';
|
||||
import { showToast } from '@hooks/showToast';
|
||||
import { getString } from '@strings/translations';
|
||||
import { checkNovelInCache } from './NovelQueries';
|
||||
|
||||
export const getCategoryNovelsFromDb = async (
|
||||
categoryId: number,
|
||||
onlyOngoingNovels?: boolean,
|
||||
): Promise<LibraryNovelInfo[]> => {
|
||||
let query = `
|
||||
SELECT
|
||||
*
|
||||
FROM
|
||||
novels
|
||||
WHERE (
|
||||
categoryIds LIKE '[${categoryId}]'
|
||||
OR
|
||||
categoryIds LIKE '[${categoryId},%'
|
||||
OR
|
||||
categoryIds LIKE '%,${categoryId}]'
|
||||
OR
|
||||
categoryIds LIKE '%,${categoryId},%'
|
||||
)
|
||||
AND
|
||||
followed = 1
|
||||
`;
|
||||
|
||||
if (onlyOngoingNovels) {
|
||||
query += ' AND status NOT LIKE "Completed"';
|
||||
}
|
||||
|
||||
return new Promise(resolve => {
|
||||
db.transaction(tx => {
|
||||
tx.executeSql(
|
||||
query,
|
||||
undefined,
|
||||
(txObj, { rows }) => resolve((rows as any)._array),
|
||||
txnErrorCallback,
|
||||
);
|
||||
});
|
||||
});
|
||||
};
|
||||
|
||||
export const resetCategoryIdsToDefault = async (deletedCategoryId: number) => {
|
||||
const categoryNovels = await getCategoryNovelsFromDb(deletedCategoryId);
|
||||
|
||||
db.transaction(tx => {
|
||||
categoryNovels.forEach(novel => {
|
||||
let categoryIds = xor(JSON.parse(novel.categoryIds), [deletedCategoryId]);
|
||||
|
||||
categoryIds = categoryIds.length ? categoryIds : [1];
|
||||
|
||||
tx.executeSql('UPDATE novels SET categoryIds = ? WHERE novelId = ?', [
|
||||
JSON.stringify(categoryIds),
|
||||
novel.novelId,
|
||||
]);
|
||||
});
|
||||
});
|
||||
};
|
||||
|
||||
export const insertNovelInLibrary = async (
|
||||
sourceId: number,
|
||||
novelUrl: string,
|
||||
inLibrary: boolean,
|
||||
defaultCategoryId: number,
|
||||
) => {
|
||||
if (inLibrary) {
|
||||
showToast(getString('browseScreen.removeFromLibrary'));
|
||||
|
||||
db.transaction(tx => {
|
||||
tx.executeSql(
|
||||
`
|
||||
UPDATE novels SET
|
||||
followed = 0
|
||||
WHERE
|
||||
sourceId = ?
|
||||
AND
|
||||
novelUrl = ?
|
||||
`,
|
||||
[sourceId, novelUrl],
|
||||
noop,
|
||||
txnErrorCallback,
|
||||
);
|
||||
});
|
||||
} else {
|
||||
showToast(getString('browseScreen.addedToLibrary'));
|
||||
|
||||
const novelSavedInDb = await checkNovelInCache(novelUrl);
|
||||
|
||||
if (novelSavedInDb) {
|
||||
db.transaction(tx => {
|
||||
tx.executeSql(
|
||||
`
|
||||
UPDATE novels SET
|
||||
followed = 1
|
||||
WHERE
|
||||
sourceId = ?
|
||||
AND
|
||||
novelUrl = ?
|
||||
`,
|
||||
[sourceId, novelUrl],
|
||||
noop,
|
||||
txnErrorCallback,
|
||||
);
|
||||
});
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
const novel = await fetchNovel(sourceId, novelUrl);
|
||||
|
||||
db.transaction(tx => {
|
||||
tx.executeSql(
|
||||
`
|
||||
INSERT INTO novels
|
||||
(
|
||||
novelUrl,
|
||||
sourceUrl,
|
||||
sourceId,
|
||||
source,
|
||||
novelName,
|
||||
novelCover,
|
||||
novelSummary,
|
||||
author,
|
||||
artist,
|
||||
status,
|
||||
genre,
|
||||
followed,
|
||||
categoryIds
|
||||
)
|
||||
VALUES
|
||||
(?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, 1, ?)`,
|
||||
[
|
||||
novel.novelUrl,
|
||||
novel.sourceUrl,
|
||||
novel.sourceId,
|
||||
novel.source,
|
||||
novel.novelName,
|
||||
novel.novelCover,
|
||||
novel.novelSummary,
|
||||
novel.author,
|
||||
novel.artist,
|
||||
novel.status,
|
||||
novel.genre,
|
||||
JSON.stringify([defaultCategoryId]),
|
||||
],
|
||||
(txObj, { insertId }) => {
|
||||
novel.chapters?.map(chapter => {
|
||||
tx.executeSql(
|
||||
`
|
||||
INSERT INTO chapters
|
||||
(chapterUrl, chapterName, releaseDate, novelId)
|
||||
VALUES
|
||||
(?, ?, ?, ?)`,
|
||||
[
|
||||
chapter.chapterUrl,
|
||||
chapter.chapterName,
|
||||
chapter.releaseDate,
|
||||
insertId,
|
||||
],
|
||||
);
|
||||
});
|
||||
},
|
||||
txnErrorCallback,
|
||||
);
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
export const updateNovelCategoryById = async (
|
||||
novelId: number,
|
||||
categoryIds: number[],
|
||||
) => {
|
||||
db.transaction(tx => {
|
||||
tx.executeSql(
|
||||
'UPDATE novels SET categoryIds = ? WHERE novelId = ?',
|
||||
[JSON.stringify(categoryIds.length ? categoryIds : [1]), novelId],
|
||||
noop,
|
||||
txnErrorCallback,
|
||||
);
|
||||
});
|
||||
};
|
||||
|
||||
export const updateNovelCategoryByIds = async (
|
||||
novelIds: number[],
|
||||
categoryIds: number[],
|
||||
) => {
|
||||
db.transaction(tx => {
|
||||
novelIds.map(novelId =>
|
||||
tx.executeSql(
|
||||
'UPDATE novels SET categoryIds = ? WHERE novelId = ?',
|
||||
[JSON.stringify(categoryIds.length ? categoryIds : [1]), novelId],
|
||||
noop,
|
||||
txnErrorCallback,
|
||||
),
|
||||
);
|
||||
});
|
||||
};
|
||||
@@ -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, chapters.releaseDate 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),
|
||||
),
|
||||
);
|
||||
@@ -0,0 +1,17 @@
|
||||
export const createCategoriesTableQuery = `
|
||||
CREATE TABLE IF NOT EXISTS categories(
|
||||
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||||
name TEXT NOT NULL,
|
||||
lastUpdatedAt DATETIME DEFAULT CURRENT_TIMESTAMP,
|
||||
sort INTEGER DEFAULT NULL
|
||||
)
|
||||
`;
|
||||
|
||||
export const createDefaultCategoryQuery =
|
||||
'INSERT INTO categories (id, name) VALUES (1, "Default");';
|
||||
|
||||
export const createCategorydIndexQuery =
|
||||
'CREATE INDEX IF NOT EXISTS categoryIdIndex ON categories(id)';
|
||||
|
||||
export const addCategorySortColumnQuery =
|
||||
'ALTER TABLE categories ADD COLUMN sort INTEGER DEFAULT NULL';
|
||||
@@ -1,9 +0,0 @@
|
||||
export const createDownloadTableQuery = `
|
||||
CREATE TABLE IF NOT EXISTS downloads(
|
||||
downloadId INTEGER PRIMARY KEY AUTOINCREMENT,
|
||||
downloadChapterId INTEGER NOT NULL,
|
||||
chapterName TEXT,
|
||||
chapterText TEXT,
|
||||
FOREIGN KEY (downloadChapterId) REFERENCES chapters(chapterId)
|
||||
ON DELETE CASCADE
|
||||
)`;
|
||||
@@ -0,0 +1,9 @@
|
||||
export const createDownloadTableQuery = `
|
||||
CREATE TABLE IF NOT EXISTS downloads(
|
||||
downloadId INTEGER PRIMARY KEY AUTOINCREMENT,
|
||||
downloadChapterId INTEGER NOT NULL,
|
||||
chapterName TEXT,
|
||||
chapterText TEXT,
|
||||
FOREIGN KEY (downloadChapterId) REFERENCES chapters(chapterId) ON DELETE CASCADE
|
||||
)
|
||||
`;
|
||||
@@ -6,14 +6,15 @@ 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,
|
||||
status TEXT,
|
||||
genre TEXT,
|
||||
followed INTEGER DEFAULT 0,
|
||||
unread INTEGER DEFAULT 1
|
||||
unread INTEGER DEFAULT 1,
|
||||
categoryIds TEXT DEFAULT "[1]"
|
||||
)`;
|
||||
|
||||
export const createUrlIndexQuery =
|
||||
@@ -21,3 +22,6 @@ export const createUrlIndexQuery =
|
||||
|
||||
export const createLibraryIndexQuery =
|
||||
'CREATE INDEX IF NOT EXISTS novelFollowedIndex ON novels(followed)';
|
||||
|
||||
export const addCategoryColumnQuery =
|
||||
'ALTER TABLE novels ADD COLUMN categoryIds TEXT DEFAULT "[1]"';
|
||||
|
||||
@@ -0,0 +1,76 @@
|
||||
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;
|
||||
categoryIds: string;
|
||||
}
|
||||
|
||||
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;
|
||||
}
|
||||
|
||||
export interface Category {
|
||||
id: number;
|
||||
name: string;
|
||||
lastUpdatedAt: Date;
|
||||
sort: number | null;
|
||||
}
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user