Compare commits
44 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 |
@@ -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
|
||||
@@ -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."
|
||||
}
|
||||
]
|
||||
@@ -13,13 +13,13 @@ import * as SplashScreen from 'expo-splash-screen';
|
||||
|
||||
import Main from './src/navigators/Main';
|
||||
|
||||
import { createDB } from './src/database/db';
|
||||
import { createDatabase } from './src/database/db';
|
||||
import { SafeAreaProvider } from 'react-native-safe-area-context';
|
||||
|
||||
const App = () => {
|
||||
useEffect(() => {
|
||||
SplashScreen.preventAutoHideAsync();
|
||||
createDB();
|
||||
createDatabase();
|
||||
}, []);
|
||||
|
||||
return (
|
||||
|
||||
@@ -6,6 +6,7 @@ buildscript {
|
||||
minSdkVersion = 21
|
||||
compileSdkVersion = 30
|
||||
targetSdkVersion = 30
|
||||
ndkVersion = "21.1.6352462"
|
||||
}
|
||||
repositories {
|
||||
google()
|
||||
|
||||
+5
-2
@@ -6,15 +6,18 @@ module.exports = function (api) {
|
||||
[
|
||||
'module-resolver',
|
||||
{
|
||||
root: ['./src'],
|
||||
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',
|
||||
},
|
||||
extensions: ['.ios.js', '.android.js', '.js', '.ts', '.tsx', '.json'],
|
||||
},
|
||||
],
|
||||
],
|
||||
|
||||
Generated
+241
-57
@@ -17,8 +17,11 @@
|
||||
"@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.2.4",
|
||||
"expo-file-system": "~11.0.2",
|
||||
@@ -33,7 +36,6 @@
|
||||
"expo-web-browser": "~9.1.0",
|
||||
"i18n-js": "^3.8.0",
|
||||
"lodash": "^4.17.21",
|
||||
"moment": "^2.29.4",
|
||||
"prop-types": "^15.7.2",
|
||||
"qs": "^6.10.1",
|
||||
"react": "16.13.1",
|
||||
@@ -45,8 +47,9 @@
|
||||
"react-native-fast-image": "^8.5.11",
|
||||
"react-native-gesture-handler": "~1.10.2",
|
||||
"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": "^4.10.1",
|
||||
"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",
|
||||
@@ -67,6 +70,7 @@
|
||||
"devDependencies": {
|
||||
"@babel/core": "^7.16.0",
|
||||
"@react-native-community/eslint-config": "^3.0.1",
|
||||
"@types/color": "^3.0.3",
|
||||
"@types/i18n-js": "^3.8.2",
|
||||
"@types/jest": "^27.4.0",
|
||||
"@types/lodash": "^4.14.179",
|
||||
@@ -3911,6 +3915,15 @@
|
||||
"react-native-screens": ">= 3.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@react-navigation/stack/node_modules/color": {
|
||||
"version": "3.2.1",
|
||||
"resolved": "https://registry.npmjs.org/color/-/color-3.2.1.tgz",
|
||||
"integrity": "sha512-aBl7dZI9ENN6fUGC7mWpMTPNHmWUSNan9tuWN6ahh5ZLNk9baLJOnSMlrQkHcrfFgz2/RigjUVAjdx36VcemKA==",
|
||||
"dependencies": {
|
||||
"color-convert": "^1.9.3",
|
||||
"color-string": "^1.6.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@reduxjs/toolkit": {
|
||||
"version": "1.7.2",
|
||||
"resolved": "https://registry.npmjs.org/@reduxjs/toolkit/-/toolkit-1.7.2.tgz",
|
||||
@@ -3934,6 +3947,20 @@
|
||||
}
|
||||
}
|
||||
},
|
||||
"node_modules/@shopify/flash-list": {
|
||||
"version": "1.2.1",
|
||||
"resolved": "https://registry.npmjs.org/@shopify/flash-list/-/flash-list-1.2.1.tgz",
|
||||
"integrity": "sha512-32N3CD+eJErgdEtyM8YP4iocvMJoOMeFbLnFE65Hpc+/CFz6rKbdN1zB5u6DXrE30WYKfKdTwhw0iTJje98dPA==",
|
||||
"dependencies": {
|
||||
"recyclerlistview": "4.1.2",
|
||||
"tslib": "2.4.0"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"@babel/runtime": "*",
|
||||
"react": "*",
|
||||
"react-native": "*"
|
||||
}
|
||||
},
|
||||
"node_modules/@sideway/address": {
|
||||
"version": "4.1.2",
|
||||
"resolved": "https://registry.npmjs.org/@sideway/address/-/address-4.1.2.tgz",
|
||||
@@ -3952,6 +3979,30 @@
|
||||
"resolved": "https://registry.npmjs.org/@sideway/pinpoint/-/pinpoint-2.0.0.tgz",
|
||||
"integrity": "sha512-RNiOoTPkptFtSVzQevY/yWtZwf/RxyVnPy/OcA9HBM3MlGDnBEYL5B41H0MTn0Uec8Hi+2qUtTfG2WWZBmMejQ=="
|
||||
},
|
||||
"node_modules/@types/color": {
|
||||
"version": "3.0.3",
|
||||
"resolved": "https://registry.npmjs.org/@types/color/-/color-3.0.3.tgz",
|
||||
"integrity": "sha512-X//qzJ3d3Zj82J9sC/C18ZY5f43utPbAJ6PhYt/M7uG6etcF6MRpKdN880KBy43B0BMzSfeT96MzrsNjFI3GbA==",
|
||||
"dev": true,
|
||||
"dependencies": {
|
||||
"@types/color-convert": "*"
|
||||
}
|
||||
},
|
||||
"node_modules/@types/color-convert": {
|
||||
"version": "2.0.0",
|
||||
"resolved": "https://registry.npmjs.org/@types/color-convert/-/color-convert-2.0.0.tgz",
|
||||
"integrity": "sha512-m7GG7IKKGuJUXvkZ1qqG3ChccdIM/qBBo913z+Xft0nKCX4hAU/IxKwZBU4cpRZ7GS5kV4vOblUkILtSShCPXQ==",
|
||||
"dev": true,
|
||||
"dependencies": {
|
||||
"@types/color-name": "*"
|
||||
}
|
||||
},
|
||||
"node_modules/@types/color-name": {
|
||||
"version": "1.1.1",
|
||||
"resolved": "https://registry.npmjs.org/@types/color-name/-/color-name-1.1.1.tgz",
|
||||
"integrity": "sha512-rr+OQyAjxze7GgWrSaJwydHStIhHq2lvY3BOC2Mj7KnzI7XK0Uw1TOOdI9lDoajEbSWLiYgoo4f1R51erQfhPQ==",
|
||||
"dev": true
|
||||
},
|
||||
"node_modules/@types/graceful-fs": {
|
||||
"version": "4.1.5",
|
||||
"resolved": "https://registry.npmjs.org/@types/graceful-fs/-/graceful-fs-4.1.5.tgz",
|
||||
@@ -6054,12 +6105,15 @@
|
||||
}
|
||||
},
|
||||
"node_modules/color": {
|
||||
"version": "3.1.3",
|
||||
"resolved": "https://registry.npmjs.org/color/-/color-3.1.3.tgz",
|
||||
"integrity": "sha512-xgXAcTHa2HeFCGLE9Xs/R82hujGtu9Jd9x4NW3T34+OMs7VoPsjwzRczKHvTAHeJwWFwX5j15+MgAppE8ztObQ==",
|
||||
"version": "4.2.3",
|
||||
"resolved": "https://registry.npmjs.org/color/-/color-4.2.3.tgz",
|
||||
"integrity": "sha512-1rXeuUUiGGrykh+CeBdu5Ie7OJwinCgQY0bc7GCRxy5xVHy+moaqkpL/jqQq0MtQOeYcrqEz4abc5f0KtU7W4A==",
|
||||
"dependencies": {
|
||||
"color-convert": "^1.9.1",
|
||||
"color-string": "^1.5.4"
|
||||
"color-convert": "^2.0.1",
|
||||
"color-string": "^1.9.0"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=12.5.0"
|
||||
}
|
||||
},
|
||||
"node_modules/color-convert": {
|
||||
@@ -6076,14 +6130,30 @@
|
||||
"integrity": "sha1-p9BVi9icQveV3UIyj3QIMcpTvCU="
|
||||
},
|
||||
"node_modules/color-string": {
|
||||
"version": "1.5.5",
|
||||
"resolved": "https://registry.npmjs.org/color-string/-/color-string-1.5.5.tgz",
|
||||
"integrity": "sha512-jgIoum0OfQfq9Whcfc2z/VhCNcmQjWbey6qBX0vqt7YICflUmBCh9E9CiQD5GSJ+Uehixm3NUwHVhqUAWRivZg==",
|
||||
"version": "1.9.1",
|
||||
"resolved": "https://registry.npmjs.org/color-string/-/color-string-1.9.1.tgz",
|
||||
"integrity": "sha512-shrVawQFojnZv6xM40anx4CkoDP+fZsw/ZerEMsW/pyzsRbElpsL/DBVW7q3ExxwusdNXI3lXpuhEZkzs8p5Eg==",
|
||||
"dependencies": {
|
||||
"color-name": "^1.0.0",
|
||||
"simple-swizzle": "^0.2.2"
|
||||
}
|
||||
},
|
||||
"node_modules/color/node_modules/color-convert": {
|
||||
"version": "2.0.1",
|
||||
"resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz",
|
||||
"integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==",
|
||||
"dependencies": {
|
||||
"color-name": "~1.1.4"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=7.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/color/node_modules/color-name": {
|
||||
"version": "1.1.4",
|
||||
"resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz",
|
||||
"integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA=="
|
||||
},
|
||||
"node_modules/colorette": {
|
||||
"version": "1.2.2",
|
||||
"resolved": "https://registry.npmjs.org/colorette/-/colorette-1.2.2.tgz",
|
||||
@@ -6350,9 +6420,9 @@
|
||||
"integrity": "sha512-2u44ZG2OcNUO9HDp/Jl8C07x6pU/eTR3ncV91SiK3dhG9TWvRVsCoJw14Ckx5DgWkzGA3waZWO3d7pgqpUI/XA=="
|
||||
},
|
||||
"node_modules/dayjs": {
|
||||
"version": "1.10.5",
|
||||
"resolved": "https://registry.npmjs.org/dayjs/-/dayjs-1.10.5.tgz",
|
||||
"integrity": "sha512-BUFis41ikLz+65iH6LHQCDm4YPMj5r1YFLdupPIyM4SGcXMmtiLQ7U37i+hGS8urIuqe7I/ou3IS1jVc4nbN4g=="
|
||||
"version": "1.11.4",
|
||||
"resolved": "https://registry.npmjs.org/dayjs/-/dayjs-1.11.4.tgz",
|
||||
"integrity": "sha512-Zj/lPM5hOvQ1Bf7uAvewDaUcsJoI6JmNqmHhHl3nyumwe0XHwt8sWdOVAPACJzCebL8gQCi+K49w7iKWnGwX9g=="
|
||||
},
|
||||
"node_modules/debug": {
|
||||
"version": "4.3.4",
|
||||
@@ -11495,14 +11565,6 @@
|
||||
"resolved": "https://registry.npmjs.org/mockdate/-/mockdate-3.0.5.tgz",
|
||||
"integrity": "sha512-iniQP4rj1FhBdBYS/+eQv7j1tadJ9lJtdzgOpvsOHng/GbcDh2Fhdeq+ZRldrPYdXvCyfFUmFeEwEGXZB5I/AQ=="
|
||||
},
|
||||
"node_modules/moment": {
|
||||
"version": "2.29.4",
|
||||
"resolved": "https://registry.npmjs.org/moment/-/moment-2.29.4.tgz",
|
||||
"integrity": "sha512-5LC9SOxjSc2HF6vO2CyuTDNivEdoz2IvyJJGj6X8DJ0eFyfszE0QiEd+iXmBvUP3WHxSjFH/vIsA0EN00cgr8w==",
|
||||
"engines": {
|
||||
"node": "*"
|
||||
}
|
||||
},
|
||||
"node_modules/ms": {
|
||||
"version": "2.1.2",
|
||||
"resolved": "https://registry.npmjs.org/ms/-/ms-2.1.2.tgz",
|
||||
@@ -12641,13 +12703,13 @@
|
||||
}
|
||||
},
|
||||
"node_modules/prop-types": {
|
||||
"version": "15.7.2",
|
||||
"resolved": "https://registry.npmjs.org/prop-types/-/prop-types-15.7.2.tgz",
|
||||
"integrity": "sha512-8QQikdH7//R2vurIJSutZ1smHYTcLpRWEOlHnzcWHmBYrOGUysKwSsrC89BCiFj3CbrfJ/nXFdJepOVrY1GCHQ==",
|
||||
"version": "15.8.1",
|
||||
"resolved": "https://registry.npmjs.org/prop-types/-/prop-types-15.8.1.tgz",
|
||||
"integrity": "sha512-oj87CgZICdulUohogVAR7AjlC0327U4el4L6eAvOqCeudMDVU0NThNaV+b9Df4dXgSP1gXMTnPdhfe/2qDH5cg==",
|
||||
"dependencies": {
|
||||
"loose-envify": "^1.4.0",
|
||||
"object-assign": "^4.1.1",
|
||||
"react-is": "^16.8.1"
|
||||
"react-is": "^16.13.1"
|
||||
}
|
||||
},
|
||||
"node_modules/pump": {
|
||||
@@ -12954,6 +13016,15 @@
|
||||
"react-native": ">=0.50.4"
|
||||
}
|
||||
},
|
||||
"node_modules/react-native-mmkv": {
|
||||
"version": "2.4.3",
|
||||
"resolved": "https://registry.npmjs.org/react-native-mmkv/-/react-native-mmkv-2.4.3.tgz",
|
||||
"integrity": "sha512-0hYNOTbsjJ5j5cpX+qjwReGIxuYE9MWRsyPQ72fTOvzmEjYXCF4Wr2VQoqx8R6W1/Uinekvln7dGvJ4RExmHhQ==",
|
||||
"peerDependencies": {
|
||||
"react": "*",
|
||||
"react-native": "*"
|
||||
}
|
||||
},
|
||||
"node_modules/react-native-pager-view": {
|
||||
"version": "5.4.9",
|
||||
"resolved": "https://registry.npmjs.org/react-native-pager-view/-/react-native-pager-view-5.4.9.tgz",
|
||||
@@ -12964,9 +13035,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/react-native-paper": {
|
||||
"version": "4.10.1",
|
||||
"resolved": "https://registry.npmjs.org/react-native-paper/-/react-native-paper-4.10.1.tgz",
|
||||
"integrity": "sha512-FcwfYyIbhD7bqDy01d0kVL1+T+gjMA1OVMJw3rN7GbUCHRJWI2guEXptNF4QCYiuR80X3WowqxXl6CZPs1757Q==",
|
||||
"version": "5.0.0-rc.4",
|
||||
"resolved": "https://registry.npmjs.org/react-native-paper/-/react-native-paper-5.0.0-rc.4.tgz",
|
||||
"integrity": "sha512-BZHa9i/xGnxCuxve6LYtPGYM23R7oMOiMt4hy+Yb620aqbQHhVUnrQB67tIJn1vcwy7DleYWn4mCaJLa0agpLQ==",
|
||||
"dependencies": {
|
||||
"@callstack/react-theme-provider": "^3.0.7",
|
||||
"color": "^3.1.2",
|
||||
@@ -12978,6 +13049,15 @@
|
||||
"react-native-vector-icons": "*"
|
||||
}
|
||||
},
|
||||
"node_modules/react-native-paper/node_modules/color": {
|
||||
"version": "3.2.1",
|
||||
"resolved": "https://registry.npmjs.org/color/-/color-3.2.1.tgz",
|
||||
"integrity": "sha512-aBl7dZI9ENN6fUGC7mWpMTPNHmWUSNan9tuWN6ahh5ZLNk9baLJOnSMlrQkHcrfFgz2/RigjUVAjdx36VcemKA==",
|
||||
"dependencies": {
|
||||
"color-convert": "^1.9.3",
|
||||
"color-string": "^1.6.0"
|
||||
}
|
||||
},
|
||||
"node_modules/react-native-reanimated": {
|
||||
"version": "2.2.4",
|
||||
"resolved": "https://registry.npmjs.org/react-native-reanimated/-/react-native-reanimated-2.2.4.tgz",
|
||||
@@ -13702,6 +13782,20 @@
|
||||
"node": ">= 0.10"
|
||||
}
|
||||
},
|
||||
"node_modules/recyclerlistview": {
|
||||
"version": "4.1.2",
|
||||
"resolved": "https://registry.npmjs.org/recyclerlistview/-/recyclerlistview-4.1.2.tgz",
|
||||
"integrity": "sha512-fvopyPoXaDY/RJGJKzroGYHgBAZoXlEdLw1D4PSi3isTRhyfbh0WNNuTyLfJSiz6ctZeb0J2ErPCInYcahFTlw==",
|
||||
"dependencies": {
|
||||
"lodash.debounce": "4.0.8",
|
||||
"prop-types": "15.8.1",
|
||||
"ts-object-utils": "0.0.5"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"react": ">= 15.2.1",
|
||||
"react-native": ">= 0.30.0"
|
||||
}
|
||||
},
|
||||
"node_modules/redux": {
|
||||
"version": "4.1.2",
|
||||
"resolved": "https://registry.npmjs.org/redux/-/redux-4.1.2.tgz",
|
||||
@@ -15309,15 +15403,20 @@
|
||||
"resolved": "https://registry.npmjs.org/ts-interface-checker/-/ts-interface-checker-0.1.13.tgz",
|
||||
"integrity": "sha512-Y/arvbn+rrz3JCKl9C4kVNfTfSm2/mEp5FSz5EsZSANGPSlQrpRI5M4PKF+mJnE52jOO90PnPSc3Ur3bTQw0gA=="
|
||||
},
|
||||
"node_modules/ts-object-utils": {
|
||||
"version": "0.0.5",
|
||||
"resolved": "https://registry.npmjs.org/ts-object-utils/-/ts-object-utils-0.0.5.tgz",
|
||||
"integrity": "sha512-iV0GvHqOmilbIKJsfyfJY9/dNHCs969z3so90dQWsO1eMMozvTpnB1MEaUbb3FYtZTGjv5sIy/xmslEz0Rg2TA=="
|
||||
},
|
||||
"node_modules/ts-toolbelt": {
|
||||
"version": "6.15.5",
|
||||
"resolved": "https://registry.npmjs.org/ts-toolbelt/-/ts-toolbelt-6.15.5.tgz",
|
||||
"integrity": "sha512-FZIXf1ksVyLcfr7M317jbB67XFJhOO1YqdTcuGaq9q5jLUoTikukZ+98TPjKiP2jC5CgmYdWWYs0s2nLSU0/1A=="
|
||||
},
|
||||
"node_modules/tslib": {
|
||||
"version": "2.2.0",
|
||||
"resolved": "https://registry.npmjs.org/tslib/-/tslib-2.2.0.tgz",
|
||||
"integrity": "sha512-gS9GVHRU+RGn5KQM2rllAlR3dU6m7AcpJKdtH8gFvQiC4Otgk98XnmMU+nZenHt/+VhnBPWwgrJsyrdcw6i23w=="
|
||||
"version": "2.4.0",
|
||||
"resolved": "https://registry.npmjs.org/tslib/-/tslib-2.4.0.tgz",
|
||||
"integrity": "sha512-d6xOpEDfsi2CZVlPQzGeux8XMwLT9hssAsaPYExaQMuYskwb+x1x7J371tWlbBdWHroy99KnVB6qIkUbs5X3UQ=="
|
||||
},
|
||||
"node_modules/tsutils": {
|
||||
"version": "3.21.0",
|
||||
@@ -19101,6 +19200,17 @@
|
||||
"@react-navigation/elements": "^1.2.1",
|
||||
"color": "^3.1.3",
|
||||
"warn-once": "^0.1.0"
|
||||
},
|
||||
"dependencies": {
|
||||
"color": {
|
||||
"version": "3.2.1",
|
||||
"resolved": "https://registry.npmjs.org/color/-/color-3.2.1.tgz",
|
||||
"integrity": "sha512-aBl7dZI9ENN6fUGC7mWpMTPNHmWUSNan9tuWN6ahh5ZLNk9baLJOnSMlrQkHcrfFgz2/RigjUVAjdx36VcemKA==",
|
||||
"requires": {
|
||||
"color-convert": "^1.9.3",
|
||||
"color-string": "^1.6.0"
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"@reduxjs/toolkit": {
|
||||
@@ -19114,6 +19224,15 @@
|
||||
"reselect": "^4.1.5"
|
||||
}
|
||||
},
|
||||
"@shopify/flash-list": {
|
||||
"version": "1.2.1",
|
||||
"resolved": "https://registry.npmjs.org/@shopify/flash-list/-/flash-list-1.2.1.tgz",
|
||||
"integrity": "sha512-32N3CD+eJErgdEtyM8YP4iocvMJoOMeFbLnFE65Hpc+/CFz6rKbdN1zB5u6DXrE30WYKfKdTwhw0iTJje98dPA==",
|
||||
"requires": {
|
||||
"recyclerlistview": "4.1.2",
|
||||
"tslib": "2.4.0"
|
||||
}
|
||||
},
|
||||
"@sideway/address": {
|
||||
"version": "4.1.2",
|
||||
"resolved": "https://registry.npmjs.org/@sideway/address/-/address-4.1.2.tgz",
|
||||
@@ -19132,6 +19251,30 @@
|
||||
"resolved": "https://registry.npmjs.org/@sideway/pinpoint/-/pinpoint-2.0.0.tgz",
|
||||
"integrity": "sha512-RNiOoTPkptFtSVzQevY/yWtZwf/RxyVnPy/OcA9HBM3MlGDnBEYL5B41H0MTn0Uec8Hi+2qUtTfG2WWZBmMejQ=="
|
||||
},
|
||||
"@types/color": {
|
||||
"version": "3.0.3",
|
||||
"resolved": "https://registry.npmjs.org/@types/color/-/color-3.0.3.tgz",
|
||||
"integrity": "sha512-X//qzJ3d3Zj82J9sC/C18ZY5f43utPbAJ6PhYt/M7uG6etcF6MRpKdN880KBy43B0BMzSfeT96MzrsNjFI3GbA==",
|
||||
"dev": true,
|
||||
"requires": {
|
||||
"@types/color-convert": "*"
|
||||
}
|
||||
},
|
||||
"@types/color-convert": {
|
||||
"version": "2.0.0",
|
||||
"resolved": "https://registry.npmjs.org/@types/color-convert/-/color-convert-2.0.0.tgz",
|
||||
"integrity": "sha512-m7GG7IKKGuJUXvkZ1qqG3ChccdIM/qBBo913z+Xft0nKCX4hAU/IxKwZBU4cpRZ7GS5kV4vOblUkILtSShCPXQ==",
|
||||
"dev": true,
|
||||
"requires": {
|
||||
"@types/color-name": "*"
|
||||
}
|
||||
},
|
||||
"@types/color-name": {
|
||||
"version": "1.1.1",
|
||||
"resolved": "https://registry.npmjs.org/@types/color-name/-/color-name-1.1.1.tgz",
|
||||
"integrity": "sha512-rr+OQyAjxze7GgWrSaJwydHStIhHq2lvY3BOC2Mj7KnzI7XK0Uw1TOOdI9lDoajEbSWLiYgoo4f1R51erQfhPQ==",
|
||||
"dev": true
|
||||
},
|
||||
"@types/graceful-fs": {
|
||||
"version": "4.1.5",
|
||||
"resolved": "https://registry.npmjs.org/@types/graceful-fs/-/graceful-fs-4.1.5.tgz",
|
||||
@@ -20651,12 +20794,27 @@
|
||||
}
|
||||
},
|
||||
"color": {
|
||||
"version": "3.1.3",
|
||||
"resolved": "https://registry.npmjs.org/color/-/color-3.1.3.tgz",
|
||||
"integrity": "sha512-xgXAcTHa2HeFCGLE9Xs/R82hujGtu9Jd9x4NW3T34+OMs7VoPsjwzRczKHvTAHeJwWFwX5j15+MgAppE8ztObQ==",
|
||||
"version": "4.2.3",
|
||||
"resolved": "https://registry.npmjs.org/color/-/color-4.2.3.tgz",
|
||||
"integrity": "sha512-1rXeuUUiGGrykh+CeBdu5Ie7OJwinCgQY0bc7GCRxy5xVHy+moaqkpL/jqQq0MtQOeYcrqEz4abc5f0KtU7W4A==",
|
||||
"requires": {
|
||||
"color-convert": "^1.9.1",
|
||||
"color-string": "^1.5.4"
|
||||
"color-convert": "^2.0.1",
|
||||
"color-string": "^1.9.0"
|
||||
},
|
||||
"dependencies": {
|
||||
"color-convert": {
|
||||
"version": "2.0.1",
|
||||
"resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz",
|
||||
"integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==",
|
||||
"requires": {
|
||||
"color-name": "~1.1.4"
|
||||
}
|
||||
},
|
||||
"color-name": {
|
||||
"version": "1.1.4",
|
||||
"resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz",
|
||||
"integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA=="
|
||||
}
|
||||
}
|
||||
},
|
||||
"color-convert": {
|
||||
@@ -20673,9 +20831,9 @@
|
||||
"integrity": "sha1-p9BVi9icQveV3UIyj3QIMcpTvCU="
|
||||
},
|
||||
"color-string": {
|
||||
"version": "1.5.5",
|
||||
"resolved": "https://registry.npmjs.org/color-string/-/color-string-1.5.5.tgz",
|
||||
"integrity": "sha512-jgIoum0OfQfq9Whcfc2z/VhCNcmQjWbey6qBX0vqt7YICflUmBCh9E9CiQD5GSJ+Uehixm3NUwHVhqUAWRivZg==",
|
||||
"version": "1.9.1",
|
||||
"resolved": "https://registry.npmjs.org/color-string/-/color-string-1.9.1.tgz",
|
||||
"integrity": "sha512-shrVawQFojnZv6xM40anx4CkoDP+fZsw/ZerEMsW/pyzsRbElpsL/DBVW7q3ExxwusdNXI3lXpuhEZkzs8p5Eg==",
|
||||
"requires": {
|
||||
"color-name": "^1.0.0",
|
||||
"simple-swizzle": "^0.2.2"
|
||||
@@ -20906,9 +21064,9 @@
|
||||
"integrity": "sha512-2u44ZG2OcNUO9HDp/Jl8C07x6pU/eTR3ncV91SiK3dhG9TWvRVsCoJw14Ckx5DgWkzGA3waZWO3d7pgqpUI/XA=="
|
||||
},
|
||||
"dayjs": {
|
||||
"version": "1.10.5",
|
||||
"resolved": "https://registry.npmjs.org/dayjs/-/dayjs-1.10.5.tgz",
|
||||
"integrity": "sha512-BUFis41ikLz+65iH6LHQCDm4YPMj5r1YFLdupPIyM4SGcXMmtiLQ7U37i+hGS8urIuqe7I/ou3IS1jVc4nbN4g=="
|
||||
"version": "1.11.4",
|
||||
"resolved": "https://registry.npmjs.org/dayjs/-/dayjs-1.11.4.tgz",
|
||||
"integrity": "sha512-Zj/lPM5hOvQ1Bf7uAvewDaUcsJoI6JmNqmHhHl3nyumwe0XHwt8sWdOVAPACJzCebL8gQCi+K49w7iKWnGwX9g=="
|
||||
},
|
||||
"debug": {
|
||||
"version": "4.3.4",
|
||||
@@ -24891,11 +25049,6 @@
|
||||
"resolved": "https://registry.npmjs.org/mockdate/-/mockdate-3.0.5.tgz",
|
||||
"integrity": "sha512-iniQP4rj1FhBdBYS/+eQv7j1tadJ9lJtdzgOpvsOHng/GbcDh2Fhdeq+ZRldrPYdXvCyfFUmFeEwEGXZB5I/AQ=="
|
||||
},
|
||||
"moment": {
|
||||
"version": "2.29.4",
|
||||
"resolved": "https://registry.npmjs.org/moment/-/moment-2.29.4.tgz",
|
||||
"integrity": "sha512-5LC9SOxjSc2HF6vO2CyuTDNivEdoz2IvyJJGj6X8DJ0eFyfszE0QiEd+iXmBvUP3WHxSjFH/vIsA0EN00cgr8w=="
|
||||
},
|
||||
"ms": {
|
||||
"version": "2.1.2",
|
||||
"resolved": "https://registry.npmjs.org/ms/-/ms-2.1.2.tgz",
|
||||
@@ -25733,13 +25886,13 @@
|
||||
}
|
||||
},
|
||||
"prop-types": {
|
||||
"version": "15.7.2",
|
||||
"resolved": "https://registry.npmjs.org/prop-types/-/prop-types-15.7.2.tgz",
|
||||
"integrity": "sha512-8QQikdH7//R2vurIJSutZ1smHYTcLpRWEOlHnzcWHmBYrOGUysKwSsrC89BCiFj3CbrfJ/nXFdJepOVrY1GCHQ==",
|
||||
"version": "15.8.1",
|
||||
"resolved": "https://registry.npmjs.org/prop-types/-/prop-types-15.8.1.tgz",
|
||||
"integrity": "sha512-oj87CgZICdulUohogVAR7AjlC0327U4el4L6eAvOqCeudMDVU0NThNaV+b9Df4dXgSP1gXMTnPdhfe/2qDH5cg==",
|
||||
"requires": {
|
||||
"loose-envify": "^1.4.0",
|
||||
"object-assign": "^4.1.1",
|
||||
"react-is": "^16.8.1"
|
||||
"react-is": "^16.13.1"
|
||||
}
|
||||
},
|
||||
"pump": {
|
||||
@@ -26242,19 +26395,35 @@
|
||||
"react-native-fit-image": "^1.5.5"
|
||||
}
|
||||
},
|
||||
"react-native-mmkv": {
|
||||
"version": "2.4.3",
|
||||
"resolved": "https://registry.npmjs.org/react-native-mmkv/-/react-native-mmkv-2.4.3.tgz",
|
||||
"integrity": "sha512-0hYNOTbsjJ5j5cpX+qjwReGIxuYE9MWRsyPQ72fTOvzmEjYXCF4Wr2VQoqx8R6W1/Uinekvln7dGvJ4RExmHhQ=="
|
||||
},
|
||||
"react-native-pager-view": {
|
||||
"version": "5.4.9",
|
||||
"resolved": "https://registry.npmjs.org/react-native-pager-view/-/react-native-pager-view-5.4.9.tgz",
|
||||
"integrity": "sha512-D6tzxpwMGdl6CXgtskGWhKRc5cJakCazESRGt7PkqnpyiH3N35ft1KmR82pCSQetAFlytFiToeu3a/dG5CELvA=="
|
||||
},
|
||||
"react-native-paper": {
|
||||
"version": "4.10.1",
|
||||
"resolved": "https://registry.npmjs.org/react-native-paper/-/react-native-paper-4.10.1.tgz",
|
||||
"integrity": "sha512-FcwfYyIbhD7bqDy01d0kVL1+T+gjMA1OVMJw3rN7GbUCHRJWI2guEXptNF4QCYiuR80X3WowqxXl6CZPs1757Q==",
|
||||
"version": "5.0.0-rc.4",
|
||||
"resolved": "https://registry.npmjs.org/react-native-paper/-/react-native-paper-5.0.0-rc.4.tgz",
|
||||
"integrity": "sha512-BZHa9i/xGnxCuxve6LYtPGYM23R7oMOiMt4hy+Yb620aqbQHhVUnrQB67tIJn1vcwy7DleYWn4mCaJLa0agpLQ==",
|
||||
"requires": {
|
||||
"@callstack/react-theme-provider": "^3.0.7",
|
||||
"color": "^3.1.2",
|
||||
"react-native-iphone-x-helper": "^1.3.1"
|
||||
},
|
||||
"dependencies": {
|
||||
"color": {
|
||||
"version": "3.2.1",
|
||||
"resolved": "https://registry.npmjs.org/color/-/color-3.2.1.tgz",
|
||||
"integrity": "sha512-aBl7dZI9ENN6fUGC7mWpMTPNHmWUSNan9tuWN6ahh5ZLNk9baLJOnSMlrQkHcrfFgz2/RigjUVAjdx36VcemKA==",
|
||||
"requires": {
|
||||
"color-convert": "^1.9.3",
|
||||
"color-string": "^1.6.0"
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"react-native-reanimated": {
|
||||
@@ -26558,6 +26727,16 @@
|
||||
"resolve": "^1.1.6"
|
||||
}
|
||||
},
|
||||
"recyclerlistview": {
|
||||
"version": "4.1.2",
|
||||
"resolved": "https://registry.npmjs.org/recyclerlistview/-/recyclerlistview-4.1.2.tgz",
|
||||
"integrity": "sha512-fvopyPoXaDY/RJGJKzroGYHgBAZoXlEdLw1D4PSi3isTRhyfbh0WNNuTyLfJSiz6ctZeb0J2ErPCInYcahFTlw==",
|
||||
"requires": {
|
||||
"lodash.debounce": "4.0.8",
|
||||
"prop-types": "15.8.1",
|
||||
"ts-object-utils": "0.0.5"
|
||||
}
|
||||
},
|
||||
"redux": {
|
||||
"version": "4.1.2",
|
||||
"resolved": "https://registry.npmjs.org/redux/-/redux-4.1.2.tgz",
|
||||
@@ -27836,15 +28015,20 @@
|
||||
"resolved": "https://registry.npmjs.org/ts-interface-checker/-/ts-interface-checker-0.1.13.tgz",
|
||||
"integrity": "sha512-Y/arvbn+rrz3JCKl9C4kVNfTfSm2/mEp5FSz5EsZSANGPSlQrpRI5M4PKF+mJnE52jOO90PnPSc3Ur3bTQw0gA=="
|
||||
},
|
||||
"ts-object-utils": {
|
||||
"version": "0.0.5",
|
||||
"resolved": "https://registry.npmjs.org/ts-object-utils/-/ts-object-utils-0.0.5.tgz",
|
||||
"integrity": "sha512-iV0GvHqOmilbIKJsfyfJY9/dNHCs969z3so90dQWsO1eMMozvTpnB1MEaUbb3FYtZTGjv5sIy/xmslEz0Rg2TA=="
|
||||
},
|
||||
"ts-toolbelt": {
|
||||
"version": "6.15.5",
|
||||
"resolved": "https://registry.npmjs.org/ts-toolbelt/-/ts-toolbelt-6.15.5.tgz",
|
||||
"integrity": "sha512-FZIXf1ksVyLcfr7M317jbB67XFJhOO1YqdTcuGaq9q5jLUoTikukZ+98TPjKiP2jC5CgmYdWWYs0s2nLSU0/1A=="
|
||||
},
|
||||
"tslib": {
|
||||
"version": "2.2.0",
|
||||
"resolved": "https://registry.npmjs.org/tslib/-/tslib-2.2.0.tgz",
|
||||
"integrity": "sha512-gS9GVHRU+RGn5KQM2rllAlR3dU6m7AcpJKdtH8gFvQiC4Otgk98XnmMU+nZenHt/+VhnBPWwgrJsyrdcw6i23w=="
|
||||
"version": "2.4.0",
|
||||
"resolved": "https://registry.npmjs.org/tslib/-/tslib-2.4.0.tgz",
|
||||
"integrity": "sha512-d6xOpEDfsi2CZVlPQzGeux8XMwLT9hssAsaPYExaQMuYskwb+x1x7J371tWlbBdWHroy99KnVB6qIkUbs5X3UQ=="
|
||||
},
|
||||
"tsutils": {
|
||||
"version": "3.21.0",
|
||||
|
||||
+7
-3
@@ -9,7 +9,7 @@
|
||||
"lint": "eslint . --ext .js,.jsx,.ts,.tsx",
|
||||
"prettier": "prettier --write \"./src/**/*.{js,jsx,ts,tsx}\"",
|
||||
"buildRelease": "cd android && gradlew clean && gradlew assembleRelease",
|
||||
"sources:madara": "node scripts/generateMadaraSource.cjs && prettier --write {src/sources/multisrc/madara/MadaraGenerator.ts,src/sources/sources.json}",
|
||||
"sources:madara": "node scripts/generateMadaraSource.cjs",
|
||||
"source:generator": "node scripts/generateSource.cjs",
|
||||
"strings": "node scripts/stringTypes.cjs",
|
||||
"prepare": "husky install"
|
||||
@@ -24,8 +24,11 @@
|
||||
"@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.2.4",
|
||||
"expo-file-system": "~11.0.2",
|
||||
@@ -40,7 +43,6 @@
|
||||
"expo-web-browser": "~9.1.0",
|
||||
"i18n-js": "^3.8.0",
|
||||
"lodash": "^4.17.21",
|
||||
"moment": "^2.29.4",
|
||||
"prop-types": "^15.7.2",
|
||||
"qs": "^6.10.1",
|
||||
"react": "16.13.1",
|
||||
@@ -52,8 +54,9 @@
|
||||
"react-native-fast-image": "^8.5.11",
|
||||
"react-native-gesture-handler": "~1.10.2",
|
||||
"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": "^4.10.1",
|
||||
"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",
|
||||
@@ -74,6 +77,7 @@
|
||||
"devDependencies": {
|
||||
"@babel/core": "^7.16.0",
|
||||
"@react-native-community/eslint-config": "^3.0.1",
|
||||
"@types/color": "^3.0.3",
|
||||
"@types/i18n-js": "^3.8.2",
|
||||
"@types/jest": "^27.4.0",
|
||||
"@types/lodash": "^4.14.179",
|
||||
|
||||
@@ -1,5 +1,7 @@
|
||||
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');
|
||||
@@ -7,6 +9,7 @@ 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');
|
||||
|
||||
@@ -26,6 +29,11 @@ MadaraSources.forEach(source => {
|
||||
}", ${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,
|
||||
@@ -52,6 +60,12 @@ MadaraSources.forEach(madaraSource => {
|
||||
|
||||
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,
|
||||
|
||||
@@ -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,
|
||||
};
|
||||
@@ -2,24 +2,38 @@ import React from 'react';
|
||||
import { StatusBar } from 'react-native';
|
||||
|
||||
import { Appbar as PaperAppbar } from 'react-native-paper';
|
||||
import { ThemeType } from '../../theme/types';
|
||||
import { MD3ThemeType } from '../../theme/types';
|
||||
|
||||
interface AppbarProps {
|
||||
title: string;
|
||||
handleGoBack: () => void;
|
||||
theme: ThemeType;
|
||||
theme: MD3ThemeType;
|
||||
mode?: 'small' | 'medium' | 'large' | 'center-aligned';
|
||||
}
|
||||
|
||||
const Appbar: React.FC<AppbarProps> = ({ title, handleGoBack, theme }) => (
|
||||
const Appbar: React.FC<AppbarProps> = ({
|
||||
title,
|
||||
handleGoBack,
|
||||
theme,
|
||||
mode = 'large',
|
||||
children,
|
||||
}) => (
|
||||
<PaperAppbar.Header
|
||||
style={{ backgroundColor: theme.colorPrimary }}
|
||||
style={{ backgroundColor: theme.surface }}
|
||||
statusBarHeight={StatusBar.currentHeight}
|
||||
mode={mode}
|
||||
>
|
||||
{handleGoBack && <PaperAppbar.BackAction onPress={handleGoBack} />}
|
||||
{handleGoBack && (
|
||||
<PaperAppbar.BackAction
|
||||
onPress={handleGoBack}
|
||||
iconColor={theme.textColorPrimary}
|
||||
/>
|
||||
)}
|
||||
<PaperAppbar.Content
|
||||
title={title}
|
||||
titleStyle={{ color: theme.textColorPrimary }}
|
||||
/>
|
||||
{children}
|
||||
</PaperAppbar.Header>
|
||||
);
|
||||
|
||||
|
||||
@@ -1,7 +1,16 @@
|
||||
import { Pressable, StyleSheet, Text, View, ViewStyle } from 'react-native';
|
||||
import React from 'react';
|
||||
import {
|
||||
Pressable,
|
||||
StyleProp,
|
||||
StyleSheet,
|
||||
Text,
|
||||
TextStyle,
|
||||
View,
|
||||
ViewStyle,
|
||||
} from 'react-native';
|
||||
import React, { useMemo } from 'react';
|
||||
import color from 'color';
|
||||
|
||||
import { ThemeType } from '../../theme/types';
|
||||
import { MD3ThemeType } from '../../theme/types';
|
||||
|
||||
export enum ButtonVariation {
|
||||
DEFAULT = 'default',
|
||||
@@ -14,8 +23,10 @@ interface Props {
|
||||
title: string;
|
||||
textColor?: string;
|
||||
onPress?: () => void;
|
||||
theme: ThemeType;
|
||||
theme: MD3ThemeType;
|
||||
style?: ViewStyle;
|
||||
labelStyle?: StyleProp<TextStyle>;
|
||||
disabled?: boolean;
|
||||
}
|
||||
|
||||
const Button: React.FC<Props> = ({
|
||||
@@ -25,7 +36,24 @@ const Button: React.FC<Props> = ({
|
||||
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={[
|
||||
@@ -36,7 +64,7 @@ const Button: React.FC<Props> = ({
|
||||
borderColor: theme.textColorHint,
|
||||
}
|
||||
: variation !== ButtonVariation.CLEAR && {
|
||||
backgroundColor: theme.colorAccent,
|
||||
backgroundColor: theme.secondary,
|
||||
},
|
||||
style,
|
||||
]}
|
||||
@@ -44,20 +72,10 @@ const Button: React.FC<Props> = ({
|
||||
<Pressable
|
||||
style={styles.pressable}
|
||||
onPress={onPress}
|
||||
android_ripple={{ color: theme.rippleColor }}
|
||||
android_ripple={{ color: color(theme.primary).alpha(0.12).string() }}
|
||||
disabled={disabled}
|
||||
>
|
||||
<Text
|
||||
style={{
|
||||
color: textColor
|
||||
? textColor
|
||||
: variation === ButtonVariation.OUTLINED
|
||||
? theme.colorAccent
|
||||
: variation === ButtonVariation.CLEAR
|
||||
? theme.colorAccent
|
||||
: theme.textColorPrimary,
|
||||
}}
|
||||
numberOfLines={1}
|
||||
>
|
||||
<Text style={[textStyles, labelStyle]} numberOfLines={1}>
|
||||
{title}
|
||||
</Text>
|
||||
</Pressable>
|
||||
|
||||
@@ -1,19 +1,40 @@
|
||||
import React from 'react';
|
||||
import { Pressable, StyleSheet, Text } from 'react-native';
|
||||
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';
|
||||
|
||||
export const Checkbox = ({
|
||||
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,
|
||||
onPress,
|
||||
disabled = false,
|
||||
theme,
|
||||
disabled,
|
||||
onPress,
|
||||
viewStyle,
|
||||
labelStyle,
|
||||
}) => (
|
||||
<Pressable
|
||||
android_ripple={{ color: theme.rippleColor }}
|
||||
style={styles.pressable}
|
||||
android_ripple={{ color: color(theme.primary).alpha(0.12).string() }}
|
||||
style={[styles.pressable, viewStyle]}
|
||||
onPress={onPress}
|
||||
disabled={disabled}
|
||||
>
|
||||
@@ -26,11 +47,14 @@ export const Checkbox = ({
|
||||
: 'unchecked'
|
||||
}
|
||||
onPress={onPress}
|
||||
color={theme.colorAccent}
|
||||
color={theme.primary}
|
||||
theme={{
|
||||
colors: { disabled: theme.textColorSecondary },
|
||||
}}
|
||||
uncheckedColor={theme.textColorSecondary}
|
||||
disabled={disabled}
|
||||
/>
|
||||
<Text style={{ color: theme.textColorSecondary, marginLeft: 12 }}>
|
||||
<Text style={[styles.defaultLabel, { color: theme.onSurface }, labelStyle]}>
|
||||
{label}
|
||||
</Text>
|
||||
</Pressable>
|
||||
@@ -38,19 +62,19 @@ export const Checkbox = ({
|
||||
|
||||
export const SortItem = ({ label, status, onPress, theme }) => (
|
||||
<Pressable
|
||||
android_ripple={{ color: theme.rippleColor }}
|
||||
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.colorAccent}
|
||||
color={theme.primary}
|
||||
size={21}
|
||||
style={styles.icon}
|
||||
/>
|
||||
)}
|
||||
<Text style={{ color: theme.textColorPrimary }}>{label}</Text>
|
||||
<Text style={{ color: theme.onSurface }}>{label}</Text>
|
||||
</Pressable>
|
||||
);
|
||||
|
||||
@@ -66,4 +90,7 @@ const styles = StyleSheet.create({
|
||||
left: 24,
|
||||
alignSelf: 'center',
|
||||
},
|
||||
defaultLabel: {
|
||||
marginLeft: 12,
|
||||
},
|
||||
});
|
||||
@@ -1,20 +1,38 @@
|
||||
import React from 'react';
|
||||
import { Pressable, StyleSheet, Text, View } from 'react-native';
|
||||
import color from 'color';
|
||||
|
||||
import { ThemeType } from '../../theme/types';
|
||||
import { MD3ThemeType } from '../../theme/types';
|
||||
import { overlay } from 'react-native-paper';
|
||||
|
||||
interface ChipProps {
|
||||
label: string;
|
||||
theme: ThemeType;
|
||||
theme: MD3ThemeType;
|
||||
}
|
||||
|
||||
const Chip: React.FC<ChipProps> = ({ label, theme }) => (
|
||||
<View style={[styles.chipContainer, { backgroundColor: theme.colorPrimary }]}>
|
||||
<View
|
||||
style={[
|
||||
styles.chipContainer,
|
||||
{
|
||||
backgroundColor: theme.isDark
|
||||
? overlay(1, theme.surface)
|
||||
: theme.secondaryContainer,
|
||||
},
|
||||
]}
|
||||
>
|
||||
<Pressable
|
||||
android_ripple={{ color: theme.rippleColor }}
|
||||
android_ripple={{ color: color(theme.primary).alpha(0.12).string() }}
|
||||
style={styles.pressable}
|
||||
>
|
||||
<Text style={[styles.label, { color: theme.textColorSecondary }]}>
|
||||
<Text
|
||||
style={[
|
||||
styles.label,
|
||||
{
|
||||
color: theme.isDark ? theme.onSurface : theme.onSecondaryContainer,
|
||||
},
|
||||
]}
|
||||
>
|
||||
{label}
|
||||
</Text>
|
||||
</Pressable>
|
||||
@@ -27,7 +45,7 @@ const styles = StyleSheet.create({
|
||||
chipContainer: {
|
||||
overflow: 'hidden',
|
||||
borderRadius: 8,
|
||||
height: 28,
|
||||
height: 32,
|
||||
marginRight: 8,
|
||||
},
|
||||
pressable: {
|
||||
|
||||
@@ -1,8 +1,8 @@
|
||||
import React, { useState } from 'react';
|
||||
import { FlatList, Pressable, StyleSheet, Text, View } from 'react-native';
|
||||
|
||||
import { Modal, Portal, TextInput } from 'react-native-paper';
|
||||
import { ThemeType } from '../../theme/types';
|
||||
import { Modal, overlay, Portal, TextInput } from 'react-native-paper';
|
||||
import { MD3ThemeType } from '../../theme/types';
|
||||
|
||||
interface ColorPickerModalProps {
|
||||
visible: boolean;
|
||||
@@ -10,7 +10,7 @@ interface ColorPickerModalProps {
|
||||
color: string;
|
||||
onSubmit: (val: string) => void;
|
||||
closeModal: () => void;
|
||||
theme: ThemeType;
|
||||
theme: MD3ThemeType;
|
||||
showAccentColors?: boolean;
|
||||
}
|
||||
|
||||
@@ -47,15 +47,6 @@ const ColorPickerModal: React.FC<ColorPickerModalProps> = ({
|
||||
}
|
||||
};
|
||||
|
||||
const textInputTheme = {
|
||||
colors: {
|
||||
primary: color,
|
||||
placeholder: theme.textColorHint,
|
||||
text: theme.textColorPrimary,
|
||||
background: 'transparent',
|
||||
},
|
||||
};
|
||||
|
||||
const accentColors = [
|
||||
'#EF5350',
|
||||
'#EC407A',
|
||||
@@ -86,7 +77,7 @@ const ColorPickerModal: React.FC<ColorPickerModalProps> = ({
|
||||
onDismiss={onDismiss}
|
||||
contentContainerStyle={[
|
||||
styles.modalContainer,
|
||||
{ backgroundColor: theme.colorPrimary },
|
||||
{ backgroundColor: overlay(2, theme.surface) },
|
||||
]}
|
||||
>
|
||||
<Text style={[styles.modalTitle, { color: theme.textColorPrimary }]}>
|
||||
@@ -131,7 +122,7 @@ const ColorPickerModal: React.FC<ColorPickerModalProps> = ({
|
||||
onChangeText={onChangeText}
|
||||
onSubmitEditing={onSubmitEditing}
|
||||
mode="outlined"
|
||||
theme={textInputTheme}
|
||||
theme={{ colors: { ...theme } }}
|
||||
underlineColor={theme.textColorHint}
|
||||
dense
|
||||
error={Boolean(error)}
|
||||
@@ -147,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: {
|
||||
|
||||
@@ -1,12 +1,13 @@
|
||||
import React from 'react';
|
||||
import { Pressable, View } from 'react-native';
|
||||
import MaterialCommunityIcons from 'react-native-vector-icons/MaterialCommunityIcons';
|
||||
import { ThemeType } from '../../theme/types';
|
||||
import { MD3ThemeType } from '../../theme/types';
|
||||
import Color from 'color';
|
||||
|
||||
interface ToggleButtonProps {
|
||||
icon: string;
|
||||
selected: boolean;
|
||||
theme: ThemeType;
|
||||
theme: MD3ThemeType;
|
||||
color?: string;
|
||||
onPress: () => void;
|
||||
}
|
||||
@@ -26,19 +27,21 @@ export const ToggleButton: React.FC<ToggleButtonProps> = ({
|
||||
}}
|
||||
>
|
||||
<Pressable
|
||||
android_ripple={{ color: theme.rippleColor }}
|
||||
android_ripple={{ color: Color(theme.primary).alpha(0.12).string() }}
|
||||
style={{
|
||||
padding: 8,
|
||||
alignItems: 'center',
|
||||
justifyContent: 'center',
|
||||
backgroundColor: selected ? theme.rippleColor : 'transparent',
|
||||
backgroundColor: selected
|
||||
? Color(theme.primary).alpha(0.12).string()
|
||||
: 'transparent',
|
||||
}}
|
||||
onPress={onPress}
|
||||
>
|
||||
<MaterialCommunityIcons
|
||||
name={icon}
|
||||
color={
|
||||
selected ? theme.colorAccent : color ? color : theme.textColorPrimary
|
||||
selected ? theme.primary : color ? color : theme.textColorPrimary
|
||||
}
|
||||
size={24}
|
||||
/>
|
||||
|
||||
@@ -1,17 +1,18 @@
|
||||
import React from 'react';
|
||||
import { StyleSheet } from 'react-native';
|
||||
import { StyleSheet, View } from 'react-native';
|
||||
|
||||
import { getString } from '@strings/translations';
|
||||
|
||||
import { Dialog, Portal } from 'react-native-paper';
|
||||
import { ThemeType } from '../../theme/types';
|
||||
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: ThemeType;
|
||||
theme: MD3ThemeType;
|
||||
onSubmit: () => void;
|
||||
onDismiss: () => void;
|
||||
}
|
||||
@@ -33,26 +34,28 @@ const ConfirmationDialog: React.FC<ConfirmationDialogProps> = ({
|
||||
<Dialog
|
||||
visible={visible}
|
||||
onDismiss={onDismiss}
|
||||
style={[styles.container, { backgroundColor: theme.colorPrimary }]}
|
||||
style={[
|
||||
styles.container,
|
||||
{ backgroundColor: getDialogBackground(theme) },
|
||||
]}
|
||||
>
|
||||
<Dialog.Title style={[styles.title, { color: theme.textColorPrimary }]}>
|
||||
{title}
|
||||
</Dialog.Title>
|
||||
<Dialog.Actions>
|
||||
<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}
|
||||
/>
|
||||
<Button
|
||||
theme={theme}
|
||||
onPress={handleOnSubmit}
|
||||
title={getString('common.ok')}
|
||||
variation={ButtonVariation.CLEAR}
|
||||
style={styles.button}
|
||||
/>
|
||||
</Dialog.Actions>
|
||||
</View>
|
||||
</Dialog>
|
||||
</Portal>
|
||||
);
|
||||
@@ -62,13 +65,14 @@ export default ConfirmationDialog;
|
||||
|
||||
const styles = StyleSheet.create({
|
||||
container: {
|
||||
borderRadius: 8,
|
||||
borderRadius: 28,
|
||||
},
|
||||
title: {
|
||||
letterSpacing: 0,
|
||||
fontSize: 16,
|
||||
},
|
||||
button: {
|
||||
marginLeft: 4,
|
||||
buttonCtn: {
|
||||
flexDirection: 'row-reverse',
|
||||
padding: 16,
|
||||
},
|
||||
});
|
||||
|
||||
@@ -1,9 +1,9 @@
|
||||
import { useTheme } from '@hooks/useTheme';
|
||||
import React from 'react';
|
||||
import { StyleSheet, Text, View } from 'react-native';
|
||||
import { useSelector } from 'react-redux';
|
||||
|
||||
const EmptyView = ({ icon, description, style, children, iconStyle }) => {
|
||||
const theme = useSelector(state => state.settingsReducer.theme);
|
||||
const theme = useTheme();
|
||||
|
||||
return (
|
||||
<View style={styles.emptyViewContainer}>
|
||||
|
||||
@@ -1,12 +1,12 @@
|
||||
import React from 'react';
|
||||
import { StyleSheet, View, Text } from 'react-native';
|
||||
|
||||
import { ThemeType } from '../../theme/types';
|
||||
import { MD3ThemeType } from '../../theme/types';
|
||||
|
||||
interface EmptyViewProps {
|
||||
icon?: string;
|
||||
description: string;
|
||||
theme: ThemeType;
|
||||
theme: MD3ThemeType;
|
||||
}
|
||||
|
||||
const EmptyView: React.FC<EmptyViewProps> = ({ icon, description, theme }) => (
|
||||
@@ -25,6 +25,7 @@ export default EmptyView;
|
||||
const styles = StyleSheet.create({
|
||||
container: {
|
||||
flex: 1,
|
||||
padding: 16,
|
||||
alignItems: 'center',
|
||||
justifyContent: 'center',
|
||||
},
|
||||
|
||||
@@ -1,11 +1,11 @@
|
||||
import React from 'react';
|
||||
import { StyleSheet, Text, View } from 'react-native';
|
||||
|
||||
import { ThemeType } from '../../theme/types';
|
||||
import { MD3ThemeType } from '@theme/types';
|
||||
|
||||
interface ErrorScreenProps {
|
||||
error?: string;
|
||||
theme: ThemeType;
|
||||
theme: MD3ThemeType;
|
||||
}
|
||||
|
||||
const ErrorScreen: React.FC<ErrorScreenProps> = ({ error, theme }) => {
|
||||
|
||||
@@ -1,8 +1,9 @@
|
||||
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 { ThemeType } from '../../theme/types';
|
||||
import { MD3ThemeType } from '../../theme/types';
|
||||
|
||||
type Props = {
|
||||
name: string;
|
||||
@@ -11,7 +12,7 @@ type Props = {
|
||||
disabled?: boolean;
|
||||
padding?: number;
|
||||
onPress?: () => void;
|
||||
theme: ThemeType;
|
||||
theme: MD3ThemeType;
|
||||
style?: ViewStyle;
|
||||
};
|
||||
|
||||
@@ -30,7 +31,11 @@ const IconButton: React.FC<Props> = ({
|
||||
style={[styles.pressable, { padding }]}
|
||||
onPress={onPress}
|
||||
disabled={disabled}
|
||||
android_ripple={{ color: theme.rippleColor }}
|
||||
android_ripple={
|
||||
onPress
|
||||
? { color: Color(theme.primary).alpha(0.12).string() }
|
||||
: undefined
|
||||
}
|
||||
>
|
||||
<MaterialCommunityIcons
|
||||
name={name}
|
||||
|
||||
+16
-8
@@ -1,5 +1,6 @@
|
||||
import React from 'react';
|
||||
import { Pressable, StyleSheet, Text, View } from 'react-native';
|
||||
import color from 'color';
|
||||
|
||||
import { List as PaperList, Divider as PaperDivider } from 'react-native-paper';
|
||||
import MaterialCommunityIcons from 'react-native-vector-icons/MaterialCommunityIcons';
|
||||
@@ -9,9 +10,7 @@ const ListSection = ({ children }) => (
|
||||
);
|
||||
|
||||
const ListSubHeader = ({ children, theme }) => (
|
||||
<PaperList.Subheader
|
||||
style={[styles.listSubHeader, { color: theme.colorAccent }]}
|
||||
>
|
||||
<PaperList.Subheader style={[styles.listSubHeader, { color: theme.primary }]}>
|
||||
{children}
|
||||
</PaperList.Subheader>
|
||||
);
|
||||
@@ -38,7 +37,7 @@ const ListItem = ({
|
||||
<View style={{ justifyContent: 'center' }}>
|
||||
{icon && (
|
||||
<PaperList.Icon
|
||||
color={theme.colorAccent}
|
||||
color={theme.primary}
|
||||
icon={icon}
|
||||
style={{
|
||||
marginVertical: 0,
|
||||
@@ -58,12 +57,20 @@ const ListItem = ({
|
||||
)
|
||||
}
|
||||
onPress={onPress}
|
||||
rippleColor={theme.rippleColor}
|
||||
rippleColor={color(theme.primary).alpha(0.12).string()}
|
||||
/>
|
||||
);
|
||||
|
||||
const Divider = ({ theme }) => (
|
||||
<PaperDivider style={{ backgroundColor: theme.dividerColor, height: 1 }} />
|
||||
<PaperDivider
|
||||
style={{
|
||||
backgroundColor: color(theme.isDark ? '#FFFFFF' : '#000000')
|
||||
.alpha(0.12)
|
||||
.rgb()
|
||||
.string(),
|
||||
height: 1,
|
||||
}}
|
||||
/>
|
||||
);
|
||||
|
||||
const InfoItem = ({ title, icon, theme }) => (
|
||||
@@ -85,7 +92,7 @@ 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>
|
||||
@@ -107,7 +114,7 @@ const ColorItem = ({ title, description, theme, onPress }) => (
|
||||
);
|
||||
|
||||
const Icon = ({ icon, theme }) => (
|
||||
<PaperList.Icon color={theme.colorAccent} icon={icon} style={{ margin: 0 }} />
|
||||
<PaperList.Icon color={theme.primary} icon={icon} style={{ margin: 0 }} />
|
||||
);
|
||||
|
||||
export const List = {
|
||||
@@ -126,6 +133,7 @@ const styles = StyleSheet.create({
|
||||
marginVertical: 0,
|
||||
},
|
||||
listSubHeader: {
|
||||
fontWeight: 'bold',
|
||||
paddingBottom: 5,
|
||||
},
|
||||
});
|
||||
|
||||
@@ -1,15 +1,17 @@
|
||||
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 { ThemeType } from '../../theme/types';
|
||||
import { MD3ThemeType } from '../../theme/types';
|
||||
import { dividerColor } from '../../theme/colors';
|
||||
|
||||
interface ListItemProps {
|
||||
title: string;
|
||||
description?: string | null;
|
||||
icon?: string;
|
||||
onPress?: () => void;
|
||||
theme: ThemeType;
|
||||
theme: MD3ThemeType;
|
||||
}
|
||||
|
||||
const Section = ({ children }: { children: ReactNode }) => (
|
||||
@@ -21,9 +23,9 @@ const SubHeader = ({
|
||||
theme,
|
||||
}: {
|
||||
children: ReactNode;
|
||||
theme: ThemeType;
|
||||
theme: MD3ThemeType;
|
||||
}) => (
|
||||
<PaperList.Subheader style={{ color: theme.colorAccent }}>
|
||||
<PaperList.Subheader style={{ color: theme.primary }}>
|
||||
{children}
|
||||
</PaperList.Subheader>
|
||||
);
|
||||
@@ -44,7 +46,7 @@ const Item: React.FC<ListItemProps> = ({
|
||||
<View style={styles.iconContainer}>
|
||||
{icon && (
|
||||
<PaperList.Icon
|
||||
color={theme.colorAccent}
|
||||
color={theme.primary}
|
||||
icon={icon}
|
||||
style={styles.icon}
|
||||
/>
|
||||
@@ -52,13 +54,13 @@ const Item: React.FC<ListItemProps> = ({
|
||||
</View>
|
||||
)}
|
||||
onPress={onPress}
|
||||
rippleColor={theme.rippleColor}
|
||||
rippleColor={color(theme.primary).alpha(0.12).string()}
|
||||
/>
|
||||
);
|
||||
|
||||
const Divider = ({ theme }: { theme: ThemeType }) => (
|
||||
const Divider = ({ theme }: { theme: MD3ThemeType }) => (
|
||||
<PaperDivider
|
||||
style={[styles.divider, { backgroundColor: theme.dividerColor }]}
|
||||
style={[styles.divider, { backgroundColor: dividerColor(theme.isDark) }]}
|
||||
/>
|
||||
);
|
||||
|
||||
@@ -69,7 +71,7 @@ const InfoItem = ({
|
||||
}: {
|
||||
title: string;
|
||||
icon: string;
|
||||
theme: ThemeType;
|
||||
theme: MD3ThemeType;
|
||||
}) => (
|
||||
<PaperList.Item
|
||||
title={title}
|
||||
|
||||
@@ -5,6 +5,8 @@ import FastImage from 'react-native-fast-image';
|
||||
import { defaultCoverUri } from '../sources/helpers/constants';
|
||||
import { coverPlaceholderColor } from '../theme/colors';
|
||||
|
||||
import color from 'color';
|
||||
|
||||
const ListView = ({
|
||||
item,
|
||||
downloadBadge,
|
||||
@@ -17,10 +19,12 @@ const ListView = ({
|
||||
}) => {
|
||||
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}
|
||||
|
||||
@@ -1,13 +1,13 @@
|
||||
import React from 'react';
|
||||
import { ActivityIndicator, StyleSheet } from 'react-native';
|
||||
import { ThemeType } from '../../theme/types';
|
||||
import { MD3ThemeType } from '../../theme/types';
|
||||
|
||||
interface Props {
|
||||
theme: ThemeType;
|
||||
theme: MD3ThemeType;
|
||||
}
|
||||
|
||||
const LoadingMoreIndicator: React.FC<Props> = ({ theme }) => (
|
||||
<ActivityIndicator color={theme.colorAccent} style={styles.indicator} />
|
||||
<ActivityIndicator color={theme.primary} style={styles.indicator} />
|
||||
);
|
||||
|
||||
export default LoadingMoreIndicator;
|
||||
|
||||
@@ -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>
|
||||
);
|
||||
@@ -1,14 +1,10 @@
|
||||
import React from 'react';
|
||||
import { StyleSheet, ActivityIndicator } from 'react-native';
|
||||
|
||||
import { ThemeType } from '../../theme/types';
|
||||
import { MD3ThemeType } from '../../theme/types';
|
||||
|
||||
const LoadingScreen: React.FC<{ theme: ThemeType }> = ({ theme }) => (
|
||||
<ActivityIndicator
|
||||
size={50}
|
||||
color={theme.colorAccent}
|
||||
style={styles.indicator}
|
||||
/>
|
||||
const LoadingScreen: React.FC<{ theme: MD3ThemeType }> = ({ theme }) => (
|
||||
<ActivityIndicator size={50} color={theme.primary} style={styles.indicator} />
|
||||
);
|
||||
|
||||
export default LoadingScreen;
|
||||
|
||||
@@ -2,11 +2,12 @@ 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 { useTheme } from '../hooks/reduxHooks';
|
||||
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: {
|
||||
@@ -30,7 +31,7 @@ const NewUpdateDialog: React.FC<NewUpdateDialogProps> = ({ newVersion }) => {
|
||||
onDismiss={() => showNewUpdateDialog(false)}
|
||||
contentContainerStyle={[
|
||||
styles.containerStyle,
|
||||
{ backgroundColor: theme.colorPrimaryDark },
|
||||
{ backgroundColor: getDialogBackground(theme) },
|
||||
]}
|
||||
>
|
||||
<Text style={[styles.modalHeader, { color: theme.textColorPrimary }]}>
|
||||
@@ -45,11 +46,11 @@ const NewUpdateDialog: React.FC<NewUpdateDialogProps> = ({ newVersion }) => {
|
||||
paddingVertical: 8,
|
||||
},
|
||||
hr: {
|
||||
backgroundColor: theme.dividerColor,
|
||||
backgroundColor: dividerColor(theme.isDark),
|
||||
marginVertical: 16,
|
||||
},
|
||||
code_inline: {
|
||||
backgroundColor: theme.colorPrimaryDark,
|
||||
backgroundColor: getDialogBackground(theme),
|
||||
},
|
||||
}}
|
||||
>
|
||||
|
||||
@@ -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 FastImage from 'react-native-fast-image';
|
||||
|
||||
import ListView from './ListView';
|
||||
|
||||
import { useDeviceOrientation } from '../services/utils/helpers';
|
||||
import { useSettings } from '../hooks/reduxHooks';
|
||||
import { coverPlaceholderColor } from '../theme/colors';
|
||||
import { useLibrarySettings } from '@hooks/useSettings';
|
||||
import { DisplayModes } from '@screens/library/constants/constants';
|
||||
|
||||
const NovelCover = ({
|
||||
item,
|
||||
@@ -25,43 +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;
|
||||
|
||||
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
|
||||
@@ -75,6 +81,7 @@ const NovelCover = ({
|
||||
showUnreadBadges={showUnreadBadges}
|
||||
chaptersDownloaded={item.chaptersDownloaded}
|
||||
chaptersUnread={item.chaptersUnread}
|
||||
theme={theme}
|
||||
/>
|
||||
)}
|
||||
{showUnreadBadges && item.chaptersUnread && (
|
||||
@@ -90,7 +97,7 @@ const NovelCover = ({
|
||||
source={{ uri }}
|
||||
style={[
|
||||
{
|
||||
height: getHeight(),
|
||||
height: coverHeight,
|
||||
borderRadius: 4,
|
||||
backgroundColor: coverPlaceholderColor,
|
||||
},
|
||||
@@ -98,9 +105,11 @@ const NovelCover = ({
|
||||
]}
|
||||
/>
|
||||
<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>
|
||||
@@ -112,6 +121,7 @@ const NovelCover = ({
|
||||
showDownloadBadges &&
|
||||
item.chaptersDownloaded && (
|
||||
<DownloadBadge
|
||||
theme={theme}
|
||||
showUnreadBadges={showUnreadBadges}
|
||||
chaptersDownloaded={item.chaptersDownloaded}
|
||||
chaptersUnread={item.chaptersUnread}
|
||||
@@ -186,8 +196,8 @@ const InLibraryBadge = ({ theme }) => (
|
||||
style={[
|
||||
styles.inLibraryBadge,
|
||||
{
|
||||
backgroundColor: theme.colorAccent,
|
||||
color: theme.colorButtonText,
|
||||
backgroundColor: theme.primary,
|
||||
color: theme.onPrimary,
|
||||
borderRadius: 4,
|
||||
},
|
||||
]}
|
||||
@@ -213,8 +223,8 @@ const UnreadBadge = ({
|
||||
borderRadius: 4,
|
||||
},
|
||||
{
|
||||
backgroundColor: theme.colorAccent,
|
||||
color: theme.colorButtonText,
|
||||
backgroundColor: theme.primary,
|
||||
color: theme.onPrimary,
|
||||
},
|
||||
]}
|
||||
>
|
||||
@@ -226,6 +236,7 @@ const DownloadBadge = ({
|
||||
chaptersDownloaded,
|
||||
showUnreadBadges,
|
||||
chaptersUnread,
|
||||
theme,
|
||||
}) => (
|
||||
<Text
|
||||
style={[
|
||||
@@ -237,6 +248,10 @@ const DownloadBadge = ({
|
||||
!showUnreadBadges && {
|
||||
borderRadius: 4,
|
||||
},
|
||||
{
|
||||
backgroundColor: theme.tertiary,
|
||||
color: theme.onTertiary,
|
||||
},
|
||||
]}
|
||||
>
|
||||
{chaptersDownloaded}
|
||||
@@ -276,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,
|
||||
|
||||
@@ -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.sourceId + item.novelUrl, []);
|
||||
|
||||
return (
|
||||
<FlatList
|
||||
contentContainerStyle={[
|
||||
styles.flatListCont,
|
||||
displayMode !== 2 && { paddingHorizontal: 4 },
|
||||
]}
|
||||
numColumns={getNovelsPerRow()}
|
||||
key={[orientation, getNovelsPerRow()]}
|
||||
data={data}
|
||||
keyExtractor={keyExtractor}
|
||||
renderItem={renderItem}
|
||||
refreshControl={refreshControl}
|
||||
ListEmptyComponent={ListEmptyComponent}
|
||||
ListFooterComponent={ListFooterComponent}
|
||||
onScroll={onScroll}
|
||||
onEndReached={onEndReached}
|
||||
/>
|
||||
);
|
||||
};
|
||||
|
||||
export default NovelList;
|
||||
|
||||
const styles = StyleSheet.create({
|
||||
flatListCont: {
|
||||
flexGrow: 1,
|
||||
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,40 +0,0 @@
|
||||
import React from 'react';
|
||||
import { FlatList, FlatListProps, StyleSheet } from 'react-native';
|
||||
|
||||
import { useAppearanceSettings } from '../../redux/hooks';
|
||||
import { useDeviceOrientation } from '../../hooks';
|
||||
|
||||
import { LibraryNovelInfo, NovelInfo } from '../../database/types';
|
||||
import { SourceNovelItem } from '../../sources/types';
|
||||
import { DeviceOrientation } from '../../hooks/useDeviceOrientation';
|
||||
|
||||
const NovelList: React.FC<
|
||||
FlatListProps<SourceNovelItem | NovelInfo | LibraryNovelInfo>
|
||||
> = props => {
|
||||
const orientation = useDeviceOrientation();
|
||||
|
||||
const { novelsPerRowPotrait, novelsPerRowLandscape } =
|
||||
useAppearanceSettings();
|
||||
|
||||
return (
|
||||
<FlatList
|
||||
contentContainerStyle={styles.container}
|
||||
numColumns={
|
||||
orientation === DeviceOrientation.LANDSCAPE
|
||||
? novelsPerRowLandscape
|
||||
: novelsPerRowPotrait
|
||||
}
|
||||
key={[orientation, novelsPerRowPotrait] as any}
|
||||
{...props}
|
||||
/>
|
||||
);
|
||||
};
|
||||
|
||||
export default NovelList;
|
||||
|
||||
const styles = StyleSheet.create({
|
||||
container: {
|
||||
flexGrow: 1,
|
||||
padding: 4,
|
||||
},
|
||||
});
|
||||
@@ -12,7 +12,7 @@ export const RadioButton = ({ value, label, theme, labelStyle }) => (
|
||||
<View style={styles.radioButtonContainer}>
|
||||
<MaterialRadioButton
|
||||
value={value}
|
||||
color={theme.colorAccent}
|
||||
color={theme.primary}
|
||||
uncheckedColor={theme.textColorSecondary}
|
||||
/>
|
||||
<Text
|
||||
|
||||
+27
-7
@@ -1,8 +1,26 @@
|
||||
import React from 'react';
|
||||
import { Pressable, StyleSheet, Text } from 'react-native';
|
||||
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';
|
||||
|
||||
export const RadioButton = ({
|
||||
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,
|
||||
@@ -11,19 +29,18 @@ export const RadioButton = ({
|
||||
theme,
|
||||
}) => (
|
||||
<Pressable
|
||||
android_ripple={{ color: theme.rippleColor }}
|
||||
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.colorAccent}
|
||||
color={theme.primary}
|
||||
uncheckedColor={theme.textColorSecondary}
|
||||
/>
|
||||
<Text
|
||||
style={[labelStyle, { color: theme.textColorPrimary, marginLeft: 12 }]}
|
||||
>
|
||||
<Text style={[styles.label, labelStyle, { color: theme.onSurface }]}>
|
||||
{label}
|
||||
</Text>
|
||||
</Pressable>
|
||||
@@ -41,4 +58,7 @@ const styles = StyleSheet.create({
|
||||
left: 24,
|
||||
alignSelf: 'center',
|
||||
},
|
||||
label: {
|
||||
marginLeft: 12,
|
||||
},
|
||||
});
|
||||
@@ -1,97 +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={{
|
||||
flex: 1,
|
||||
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: 46,
|
||||
borderRadius: 50,
|
||||
overflow: 'hidden',
|
||||
},
|
||||
pressable: {
|
||||
flexDirection: 'row',
|
||||
justifyContent: 'space-between',
|
||||
alignItems: 'center',
|
||||
paddingHorizontal: 16,
|
||||
},
|
||||
});
|
||||
@@ -1,9 +1,10 @@
|
||||
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 { ThemeType } from '../../theme/types';
|
||||
import { MD3ThemeType } from '../../theme/types';
|
||||
|
||||
interface RightIcon {
|
||||
iconName: string;
|
||||
@@ -21,7 +22,7 @@ interface SearcbarProps {
|
||||
handleBackAction?: () => void;
|
||||
clearSearchbar: () => void;
|
||||
onLeftIconPress?: () => void;
|
||||
theme: ThemeType;
|
||||
theme: MD3ThemeType;
|
||||
}
|
||||
|
||||
const Searchbar: React.FC<SearcbarProps> = ({
|
||||
@@ -46,17 +47,20 @@ const Searchbar: React.FC<SearcbarProps> = ({
|
||||
<View
|
||||
style={[
|
||||
styles.searchbarContainer,
|
||||
{ marginTop, backgroundColor: theme.searchBarColor },
|
||||
{
|
||||
marginTop,
|
||||
backgroundColor: color(theme.primary).alpha(0.08).string(),
|
||||
},
|
||||
]}
|
||||
>
|
||||
<Pressable
|
||||
onPress={focusSearchbar}
|
||||
android_ripple={{ color: theme.rippleColor }}
|
||||
android_ripple={{ color: color(theme.primary).alpha(12).toString() }}
|
||||
style={[styles.searchbar]}
|
||||
>
|
||||
<IconButtonV2
|
||||
name={handleBackAction ? 'arrow-left' : leftIcon}
|
||||
color={theme.textColorPrimary}
|
||||
color={theme.onSurface}
|
||||
onPress={() => {
|
||||
if (handleBackAction) {
|
||||
handleBackAction();
|
||||
@@ -69,9 +73,9 @@ const Searchbar: React.FC<SearcbarProps> = ({
|
||||
|
||||
<TextInput
|
||||
ref={searchbarRef}
|
||||
style={[styles.textInput, { color: theme.textColorPrimary }]}
|
||||
style={[styles.textInput, { color: theme.onSurface }]}
|
||||
placeholder={placeholder}
|
||||
placeholderTextColor={theme.textColorSecondary}
|
||||
placeholderTextColor={theme.onSurface}
|
||||
onChangeText={onChangeText}
|
||||
onSubmitEditing={onSubmitEditing}
|
||||
defaultValue={searchText}
|
||||
@@ -79,7 +83,7 @@ const Searchbar: React.FC<SearcbarProps> = ({
|
||||
{searchText !== '' ? (
|
||||
<IconButtonV2
|
||||
name="close"
|
||||
color={theme.textColorPrimary}
|
||||
color={theme.onSurface}
|
||||
onPress={clearSearchbar}
|
||||
theme={theme}
|
||||
/>
|
||||
@@ -88,7 +92,7 @@ const Searchbar: React.FC<SearcbarProps> = ({
|
||||
<IconButtonV2
|
||||
key={index}
|
||||
name={icon.iconName}
|
||||
color={icon.color || theme.textColorPrimary}
|
||||
color={icon.color || theme.onSurface}
|
||||
onPress={icon.onPress}
|
||||
theme={theme}
|
||||
/>
|
||||
@@ -103,7 +107,7 @@ export default Searchbar;
|
||||
const styles = StyleSheet.create({
|
||||
searchbarContainer: {
|
||||
marginHorizontal: 16,
|
||||
marginBottom: 16,
|
||||
marginBottom: 12,
|
||||
minHeight: 46,
|
||||
borderRadius: 50,
|
||||
overflow: 'hidden',
|
||||
|
||||
@@ -1,21 +1,17 @@
|
||||
import React from 'react';
|
||||
import { Pressable, StyleSheet, View, Text } from 'react-native';
|
||||
|
||||
import color from 'color';
|
||||
import { Switch, List } from 'react-native-paper';
|
||||
|
||||
const SwitchSetting = ({ label, description, onPress, theme, icon, value }) => (
|
||||
<Pressable
|
||||
android_ripple={{ color: theme.rippleColor }}
|
||||
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 }}>
|
||||
{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 }]}>
|
||||
<Text
|
||||
@@ -36,7 +32,7 @@ const SwitchSetting = ({ label, description, onPress, theme, icon, value }) => (
|
||||
<Switch
|
||||
value={value}
|
||||
onValueChange={onPress}
|
||||
color={theme.colorAccent}
|
||||
color={theme.primary}
|
||||
style={{ marginLeft: 8 }}
|
||||
/>
|
||||
</Pressable>
|
||||
|
||||
@@ -1,15 +1,16 @@
|
||||
import React from 'react';
|
||||
import { Pressable, StyleSheet, View, Text } from 'react-native';
|
||||
import color from 'color';
|
||||
|
||||
import { Switch } from 'react-native-paper';
|
||||
import { ThemeType } from '../../theme/types';
|
||||
import { MD3ThemeType } from '../../theme/types';
|
||||
|
||||
interface SwitchItemProps {
|
||||
value: boolean;
|
||||
label: string;
|
||||
description?: string;
|
||||
onPress: () => void;
|
||||
theme: ThemeType;
|
||||
theme: MD3ThemeType;
|
||||
}
|
||||
|
||||
const SwitchItem: React.FC<SwitchItemProps> = ({
|
||||
@@ -20,7 +21,7 @@ const SwitchItem: React.FC<SwitchItemProps> = ({
|
||||
value,
|
||||
}) => (
|
||||
<Pressable
|
||||
android_ripple={{ color: theme.rippleColor }}
|
||||
android_ripple={{ color: color(theme.primary).alpha(0.12).string() }}
|
||||
style={[styles.container]}
|
||||
onPress={onPress}
|
||||
>
|
||||
@@ -35,7 +36,7 @@ const SwitchItem: React.FC<SwitchItemProps> = ({
|
||||
<Switch
|
||||
value={value}
|
||||
onValueChange={onPress}
|
||||
color={theme.colorAccent}
|
||||
color={theme.primary}
|
||||
style={styles.switch}
|
||||
/>
|
||||
</Pressable>
|
||||
|
||||
@@ -1,5 +1,7 @@
|
||||
import React from 'react';
|
||||
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 }) => (
|
||||
@@ -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,
|
||||
@@ -29,10 +31,10 @@ export const ThemePicker = ({ theme, currentTheme, 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',
|
||||
}}
|
||||
@@ -82,7 +84,7 @@ export const ThemePicker = ({ theme, currentTheme, onPress }) => (
|
||||
height: 10,
|
||||
width: 16,
|
||||
marginLeft: 4,
|
||||
backgroundColor: theme.colorAccent,
|
||||
backgroundColor: theme.primary,
|
||||
borderRadius: 50,
|
||||
}}
|
||||
/>
|
||||
@@ -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
|
||||
|
||||
@@ -4,7 +4,6 @@ export { default as LoadingScreenV2 } from './LoadingScreenV2/LoadingScreenV2';
|
||||
export { default as Container } from './Container/Container';
|
||||
export { default as ErrorScreenV2 } from './ErrorScreenV2/ErrorScreenV2';
|
||||
export { default as EmptyView } from './EmptyView/EmptyView';
|
||||
export { default as NovelList } from './NovelList/NovelList';
|
||||
export { default as Chip } from './Chip/Chip';
|
||||
export { default as Button } from './Button/Button';
|
||||
export { default as Appbar } from './Appbar/Appbar';
|
||||
@@ -12,3 +11,5 @@ 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';
|
||||
|
||||
+61
-13
@@ -3,6 +3,7 @@ import {
|
||||
createNovelTableQuery,
|
||||
createUrlIndexQuery,
|
||||
createLibraryIndexQuery,
|
||||
addCategoryColumnQuery,
|
||||
} from './tables/NovelTable';
|
||||
import {
|
||||
createChapterTableQuery,
|
||||
@@ -15,36 +16,83 @@ import {
|
||||
} 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);
|
||||
|
||||
export const createDB = () => {
|
||||
const createTables = () => {
|
||||
db.transaction(tx => {
|
||||
tx.executeSql(createNovelTableQuery);
|
||||
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);
|
||||
});
|
||||
};
|
||||
|
||||
/**
|
||||
* Indexes
|
||||
*/
|
||||
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 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');
|
||||
});
|
||||
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,
|
||||
);
|
||||
});
|
||||
});
|
||||
@@ -5,6 +5,7 @@ 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');
|
||||
|
||||
@@ -317,30 +318,69 @@ export const downloadChapter = async (
|
||||
chapterUrl: string,
|
||||
chapterId: number,
|
||||
) => {
|
||||
const source = sourceManager(sourceId);
|
||||
try {
|
||||
const source = sourceManager(sourceId);
|
||||
|
||||
const chapter = await source.parseChapter(novelUrl, chapterUrl);
|
||||
const chapter = await source.parseChapter(novelUrl, chapterUrl);
|
||||
|
||||
const imagedChapterText =
|
||||
chapter.chapterText &&
|
||||
(await downloadImages(chapter.chapterText, sourceId, novelId, chapterId));
|
||||
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) => {
|
||||
// console.log(`Downloaded Chapter ${chapter.chapterUrl}`);
|
||||
},
|
||||
(_txObj, _error) => {
|
||||
// console.log('Error ', error)
|
||||
return false;
|
||||
},
|
||||
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 (
|
||||
@@ -352,23 +392,7 @@ export const deleteChapter = async (
|
||||
'UPDATE chapters SET downloaded = 0 WHERE chapterId=?';
|
||||
const deleteChapterQuery = 'DELETE FROM downloads WHERE downloadChapterId=?';
|
||||
|
||||
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]}`);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
await deleteDownloadedImages(sourceId, novelId, chapterId);
|
||||
|
||||
db.transaction(tx => {
|
||||
tx.executeSql(
|
||||
|
||||
@@ -1,95 +0,0 @@
|
||||
import * as SQLite from 'expo-sqlite';
|
||||
const db = SQLite.openDatabase('lnreader.db');
|
||||
|
||||
const getLibraryQuery = (sort, filter) => `
|
||||
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 ${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(JSON.stringify(_array, null, 2));
|
||||
resolve(_array);
|
||||
},
|
||||
(txObj, error) => {
|
||||
// console.log('Error ', error);
|
||||
},
|
||||
);
|
||||
}),
|
||||
);
|
||||
};
|
||||
|
||||
const searchLibraryQuery = (searchText, sort, filter) =>
|
||||
`
|
||||
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 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,
|
||||
);
|
||||
}),
|
||||
);
|
||||
};
|
||||
@@ -8,9 +8,11 @@ import { fetchChapters, fetchNovel } from '../../services/Source/source';
|
||||
import { insertChapters } from './ChapterQueries';
|
||||
|
||||
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) =>
|
||||
@@ -29,11 +31,10 @@ export const insertNovel = async novel => {
|
||||
novel.artist,
|
||||
novel.status,
|
||||
novel.genre,
|
||||
JSON.stringify(novel.categoryIds),
|
||||
],
|
||||
(txObj, { insertId }) => resolve(insertId),
|
||||
(txObj, error) => {
|
||||
// console.log('Error ', error)
|
||||
},
|
||||
txnErrorCallback,
|
||||
),
|
||||
),
|
||||
);
|
||||
|
||||
@@ -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,
|
||||
),
|
||||
);
|
||||
});
|
||||
};
|
||||
@@ -35,7 +35,7 @@ const getUpdatesFromDbQuery = `
|
||||
WHERE
|
||||
date(updates.updateTime) > date('now','-3 months')
|
||||
ORDER BY
|
||||
updates.updateTime DESC
|
||||
updates.updateTime DESC, chapters.releaseDate DESC
|
||||
`;
|
||||
|
||||
export const getUpdatesFromDb = async (): Promise<Update[]> => {
|
||||
|
||||
@@ -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';
|
||||
@@ -13,7 +13,8 @@ export const createNovelTableQuery = `
|
||||
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]"';
|
||||
|
||||
@@ -16,6 +16,7 @@ export interface NovelInfo {
|
||||
export interface LibraryNovelInfo extends NovelInfo {
|
||||
chaptersUnread: number;
|
||||
chaptersDownloaded: number;
|
||||
categoryIds: string;
|
||||
}
|
||||
|
||||
export interface ChapterItem {
|
||||
@@ -66,3 +67,10 @@ export interface Update {
|
||||
bookmark: number;
|
||||
releaseDate: string;
|
||||
}
|
||||
|
||||
export interface Category {
|
||||
id: number;
|
||||
name: string;
|
||||
lastUpdatedAt: Date;
|
||||
sort: number | null;
|
||||
}
|
||||
|
||||
@@ -0,0 +1,17 @@
|
||||
import { showToast } from '@hooks/showToast';
|
||||
import { SQLError, SQLTransaction } from 'expo-sqlite';
|
||||
|
||||
export const txnErrorCallback = (
|
||||
txnObj: SQLTransaction,
|
||||
error: SQLError,
|
||||
): boolean => {
|
||||
showToast(error.message);
|
||||
return false;
|
||||
};
|
||||
|
||||
export const txnErrorCallbackWithoutToast = (): boolean => {
|
||||
return false;
|
||||
};
|
||||
|
||||
export const dbTxnErrorCallback = (error: SQLError): void =>
|
||||
showToast(error.message);
|
||||
+3
-2
@@ -4,7 +4,8 @@ export { default as useUpdates } from './useUpdates';
|
||||
export { default as useFullscreenMode } from './useFullscreenMode';
|
||||
export { default as useChapter } from './useChapter';
|
||||
export { default as useNextAndPrevChapter } from './useNextAndPrevChapter';
|
||||
export { default as useDeviceOrientation } from './useDeviceOrientation';
|
||||
export { default as useNovelCoverHeight } from './useNovelCoverHeight';
|
||||
export { default as useNovelTrackerInfo } from './useNovelTrackerInfo';
|
||||
export { default as useBoolean } from './useBoolean';
|
||||
export { useTheme } from './useTheme';
|
||||
export { default as useCategories } from './useCategories';
|
||||
export { useCategorySettings, useLibrarySettings } from './useSettings';
|
||||
|
||||
@@ -1,11 +1,4 @@
|
||||
import { useSelector } from 'react-redux';
|
||||
import { darkTheme } from '../theme/theme';
|
||||
|
||||
const useTheme = () => {
|
||||
const theme = useSelector(state => state.settingsReducer.theme);
|
||||
|
||||
return theme || darkTheme;
|
||||
};
|
||||
|
||||
const useSettings = () => {
|
||||
const settings = useSelector(state => state.settingsReducer);
|
||||
@@ -13,12 +6,6 @@ const useSettings = () => {
|
||||
return settings;
|
||||
};
|
||||
|
||||
const useLibrary = () => {
|
||||
const library = useSelector(state => state.libraryReducer.novels);
|
||||
|
||||
return library;
|
||||
};
|
||||
|
||||
const useNovel = () => {
|
||||
const novel = useSelector(state => state.novelReducer);
|
||||
|
||||
@@ -104,15 +91,7 @@ const useTrackingStatus = () => {
|
||||
return tracker;
|
||||
};
|
||||
|
||||
const useLibraryFilters = () => {
|
||||
const filters = useSelector(state => state.libraryReducer.filters);
|
||||
|
||||
return filters;
|
||||
};
|
||||
|
||||
export {
|
||||
useTheme,
|
||||
useLibrary,
|
||||
useSettings,
|
||||
usePreferences,
|
||||
useContinueReading,
|
||||
@@ -120,6 +99,5 @@ export {
|
||||
useNovel,
|
||||
useChapter,
|
||||
useSavedSettings,
|
||||
useLibraryFilters,
|
||||
usePosition,
|
||||
};
|
||||
|
||||
@@ -0,0 +1,10 @@
|
||||
import { useEffect } from 'react';
|
||||
import { BackHandler } from 'react-native';
|
||||
|
||||
export function useBackHandler(handler: () => boolean) {
|
||||
useEffect(() => {
|
||||
BackHandler.addEventListener('hardwareBackPress', handler);
|
||||
|
||||
return () => BackHandler.removeEventListener('hardwareBackPress', handler);
|
||||
}, [handler]);
|
||||
}
|
||||
@@ -0,0 +1,35 @@
|
||||
import { useEffect, useState } from 'react';
|
||||
|
||||
import { getCategoriesFromDb } from '@database/queries/CategoryQueries';
|
||||
import { Category } from '@database/types';
|
||||
|
||||
const useCategories = () => {
|
||||
const [isLoading, setIsLoading] = useState(true);
|
||||
const [categories, setCategories] = useState<Category[]>([]);
|
||||
const [error, setError] = useState<string>();
|
||||
|
||||
const getCategories = async () => {
|
||||
try {
|
||||
const res = await getCategoriesFromDb();
|
||||
setCategories(res);
|
||||
} catch (err) {
|
||||
if (err instanceof Error) {
|
||||
setError(err.message);
|
||||
}
|
||||
} finally {
|
||||
setIsLoading(false);
|
||||
}
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
getCategories();
|
||||
}, []);
|
||||
|
||||
return {
|
||||
isLoading,
|
||||
categories,
|
||||
error,
|
||||
};
|
||||
};
|
||||
|
||||
export default useCategories;
|
||||
@@ -1,18 +0,0 @@
|
||||
import { useWindowDimensions } from 'react-native';
|
||||
|
||||
export enum DeviceOrientation {
|
||||
POTRAIT = 'potrait',
|
||||
LANDSCAPE = 'landscape',
|
||||
}
|
||||
|
||||
const useDeviceOrientation = (): DeviceOrientation => {
|
||||
const window = useWindowDimensions();
|
||||
|
||||
if (window.width > window.height) {
|
||||
return DeviceOrientation.LANDSCAPE;
|
||||
} else {
|
||||
return DeviceOrientation.POTRAIT;
|
||||
}
|
||||
};
|
||||
|
||||
export default useDeviceOrientation;
|
||||
@@ -1,14 +1,15 @@
|
||||
import { useCallback, useEffect } from 'react';
|
||||
import { StatusBar } from 'react-native';
|
||||
import { useNavigation } from '@react-navigation/native';
|
||||
|
||||
import { useReaderSettings, useSettingsV1, useTheme } from '../redux/hooks';
|
||||
import color from 'color';
|
||||
import { useReaderSettings, useSettingsV1 } from '../redux/hooks';
|
||||
import {
|
||||
changeNavigationBarColor,
|
||||
hideNavigationBar,
|
||||
showNavigationBar,
|
||||
} from '../theme/NativeModules/NavigationBarColor';
|
||||
import { readerBackground } from '../screens/reader/utils/readerStyles';
|
||||
import { useTheme } from './useTheme';
|
||||
|
||||
const useFullscreenMode = () => {
|
||||
const { addListener } = useNavigation();
|
||||
@@ -24,6 +25,9 @@ const useFullscreenMode = () => {
|
||||
StatusBar.setHidden(true);
|
||||
hideNavigationBar();
|
||||
} else {
|
||||
StatusBar.setBarStyle(
|
||||
color(backgroundColor).isDark() ? 'light-content' : 'dark-content',
|
||||
);
|
||||
StatusBar.setBackgroundColor(backgroundColor);
|
||||
changeNavigationBarColor(backgroundColor as any);
|
||||
}
|
||||
@@ -41,7 +45,7 @@ const useFullscreenMode = () => {
|
||||
useEffect(() => {
|
||||
const unsubscribe = addListener('beforeRemove', () => {
|
||||
showStatusAndNavBar();
|
||||
StatusBar.setBarStyle(theme.statusBar);
|
||||
StatusBar.setBarStyle(theme.isDark ? 'light-content' : 'dark-content');
|
||||
});
|
||||
|
||||
return unsubscribe;
|
||||
|
||||
@@ -1,10 +0,0 @@
|
||||
import { useState } from 'react';
|
||||
|
||||
export const useModal = () => {
|
||||
const [visible, setVisible] = useState(false);
|
||||
|
||||
const showModal = () => setVisible(true);
|
||||
const hideModal = () => setVisible(false);
|
||||
|
||||
return { visible, showModal, hideModal };
|
||||
};
|
||||
@@ -1,34 +0,0 @@
|
||||
import { useCallback, useEffect, useState } from 'react';
|
||||
import { useWindowDimensions } from 'react-native';
|
||||
|
||||
import useDeviceOrientation from './useDeviceOrientation';
|
||||
import { DeviceOrientation } from '../hooks/useDeviceOrientation';
|
||||
import { useAppearanceSettings } from '../redux/hooks';
|
||||
|
||||
const useNovelCoverHeight = () => {
|
||||
const window = useWindowDimensions();
|
||||
const orientation = useDeviceOrientation();
|
||||
|
||||
const { novelsPerRowLandscape, novelsPerRowPotrait } =
|
||||
useAppearanceSettings();
|
||||
|
||||
const [coverHeight, setCoverHeight] = useState<number>();
|
||||
|
||||
const getCoverHeight = useCallback(
|
||||
() =>
|
||||
(window.width /
|
||||
(orientation === DeviceOrientation.POTRAIT
|
||||
? novelsPerRowPotrait
|
||||
: novelsPerRowLandscape)) *
|
||||
(4 / 3),
|
||||
[window.width, novelsPerRowPotrait, novelsPerRowLandscape, orientation],
|
||||
);
|
||||
|
||||
useEffect(() => {
|
||||
setCoverHeight(getCoverHeight());
|
||||
}, [getCoverHeight]);
|
||||
|
||||
return coverHeight;
|
||||
};
|
||||
|
||||
export default useNovelCoverHeight;
|
||||
+11
-1
@@ -1,9 +1,19 @@
|
||||
import { useCallback, useState } from 'react';
|
||||
import { useIsFocused } from '@react-navigation/native';
|
||||
import { useCallback, useEffect, useState } from 'react';
|
||||
|
||||
const useSearch = (defaultSearchText?: string) => {
|
||||
const isFocused = useIsFocused();
|
||||
|
||||
const [searchText, setSearchText] = useState<string>(defaultSearchText || '');
|
||||
|
||||
const clearSearchbar = useCallback(() => setSearchText(''), []);
|
||||
|
||||
useEffect(() => {
|
||||
if (!isFocused) {
|
||||
clearSearchbar();
|
||||
}
|
||||
}, [isFocused, clearSearchbar]);
|
||||
|
||||
return { searchText, setSearchText, clearSearchbar };
|
||||
};
|
||||
|
||||
|
||||
@@ -0,0 +1,28 @@
|
||||
import { useMMKVObject } from 'react-native-mmkv';
|
||||
import { CategorySettings, LibrarySettings } from '../utils/constants/settings';
|
||||
|
||||
export const useLibrarySettings = () => {
|
||||
const [librarySettings, setSettings] =
|
||||
useMMKVObject<LibrarySettings>('LibrarySettings');
|
||||
|
||||
const setLibrarySettings = (value: Partial<LibrarySettings>) =>
|
||||
setSettings({ ...librarySettings, ...value });
|
||||
|
||||
return {
|
||||
...librarySettings,
|
||||
setLibrarySettings,
|
||||
};
|
||||
};
|
||||
|
||||
export const useCategorySettings = () => {
|
||||
const [categorySettings, setSettings] =
|
||||
useMMKVObject<CategorySettings>('CategorySettings');
|
||||
|
||||
const setCategorySettings = (value: Partial<CategorySettings>) =>
|
||||
setSettings({ ...categorySettings, ...value });
|
||||
|
||||
return {
|
||||
...categorySettings,
|
||||
setCategorySettings,
|
||||
};
|
||||
};
|
||||
@@ -0,0 +1,35 @@
|
||||
import {
|
||||
useMMKVBoolean,
|
||||
useMMKVObject,
|
||||
useMMKVString,
|
||||
} from 'react-native-mmkv';
|
||||
import { Appearance } from 'react-native';
|
||||
import { defaultTheme } from '../theme/md3/defaultTheme';
|
||||
import { MD3ThemeType } from '../theme/types';
|
||||
import { useMemo } from 'react';
|
||||
|
||||
export const useTheme = (): MD3ThemeType => {
|
||||
const [appTheme] = useMMKVObject<MD3ThemeType>('APP_THEME');
|
||||
const [isAmoledBlack] = useMMKVBoolean('AMOLED_BLACK');
|
||||
const [customAccent] = useMMKVString('CUSTOM_ACCENT_COLOR');
|
||||
|
||||
const theme: MD3ThemeType = useMemo(() => {
|
||||
let colors =
|
||||
appTheme ||
|
||||
((Appearance.getColorScheme() === 'dark'
|
||||
? defaultTheme.dark
|
||||
: defaultTheme.light) as MD3ThemeType);
|
||||
|
||||
if (isAmoledBlack && colors.isDark) {
|
||||
colors = { ...colors, background: '#000000', surface: '#000000' };
|
||||
}
|
||||
|
||||
if (customAccent) {
|
||||
colors = { ...colors, primary: customAccent, secondary: customAccent };
|
||||
}
|
||||
|
||||
return colors;
|
||||
}, [appTheme?.id, isAmoledBlack, customAccent]);
|
||||
|
||||
return theme;
|
||||
};
|
||||
@@ -0,0 +1,211 @@
|
||||
import { SafeAreaProviderCompat } from '@react-navigation/elements';
|
||||
import {
|
||||
MaterialBottomTabDescriptorMap,
|
||||
MaterialBottomTabNavigationConfig,
|
||||
MaterialBottomTabNavigationHelpers,
|
||||
} from '@react-navigation/material-bottom-tabs/lib/typescript/src/types';
|
||||
import {
|
||||
CommonActions,
|
||||
Link,
|
||||
ParamListBase,
|
||||
Route,
|
||||
TabNavigationState,
|
||||
useLinkBuilder,
|
||||
} from '@react-navigation/native';
|
||||
import * as React from 'react';
|
||||
import { Platform, StyleSheet, Text } from 'react-native';
|
||||
import { BottomNavigation } from 'react-native-paper';
|
||||
import { useSafeAreaInsets } from 'react-native-safe-area-context';
|
||||
|
||||
type Props = MaterialBottomTabNavigationConfig & {
|
||||
state: TabNavigationState<ParamListBase>;
|
||||
navigation: MaterialBottomTabNavigationHelpers;
|
||||
descriptors: MaterialBottomTabDescriptorMap;
|
||||
};
|
||||
|
||||
type Scene = { route: { key: string } };
|
||||
|
||||
// Optionally require vector-icons referenced from react-native-paper:
|
||||
// https://github.com/callstack/react-native-paper/blob/4b26429c49053eaa4c3e0fae208639e01093fa87/src/components/MaterialCommunityIcon.tsx#L14
|
||||
let MaterialCommunityIcons: React.ComponentType<
|
||||
React.ComponentProps<
|
||||
typeof import('react-native-vector-icons/MaterialCommunityIcons').default
|
||||
>
|
||||
>;
|
||||
|
||||
try {
|
||||
// Optionally require vector-icons
|
||||
MaterialCommunityIcons =
|
||||
require('react-native-vector-icons/MaterialCommunityIcons').default;
|
||||
} catch (e: any) {
|
||||
let isErrorLogged = false;
|
||||
|
||||
// Fallback component for icons
|
||||
MaterialCommunityIcons = ({
|
||||
name,
|
||||
color,
|
||||
size,
|
||||
selectionColor: _0,
|
||||
onLayout: _1,
|
||||
...rest
|
||||
}) => {
|
||||
if (!isErrorLogged) {
|
||||
if (
|
||||
!/(Cannot find module|Module not found|Cannot resolve module)/.test(
|
||||
e.message,
|
||||
)
|
||||
) {
|
||||
console.error(e);
|
||||
}
|
||||
|
||||
console.warn(
|
||||
`Tried to use the icon '${name}' in a component from '@react-navigation/material-bottom-tabs', but 'react-native-vector-icons/MaterialCommunityIcons' could not be loaded.`,
|
||||
"To remove this warning, try installing 'react-native-vector-icons' or use another method to specify icon: https://reactnavigation.org/docs/material-bottom-tab-navigator/#tabbaricon.",
|
||||
);
|
||||
|
||||
isErrorLogged = true;
|
||||
}
|
||||
|
||||
return (
|
||||
// @ts-expect-error: we're passing icon props to text, but we don't care about it since it's just fallback
|
||||
<Text
|
||||
{...rest}
|
||||
style={[
|
||||
styles.icon,
|
||||
{
|
||||
color: typeof color !== 'number' ? color : undefined,
|
||||
fontSize: size,
|
||||
},
|
||||
]}
|
||||
>
|
||||
□
|
||||
</Text>
|
||||
);
|
||||
};
|
||||
}
|
||||
|
||||
function MaterialBottomTabViewInner({
|
||||
state,
|
||||
navigation,
|
||||
descriptors,
|
||||
...rest
|
||||
}: Props) {
|
||||
const insets = useSafeAreaInsets();
|
||||
|
||||
const buildLink = useLinkBuilder();
|
||||
|
||||
return (
|
||||
<BottomNavigation
|
||||
{...rest}
|
||||
navigationState={state}
|
||||
onIndexChange={(index: number) =>
|
||||
navigation.dispatch({
|
||||
...CommonActions.navigate({
|
||||
name: state.routes[index].name,
|
||||
merge: true,
|
||||
}),
|
||||
target: state.key,
|
||||
})
|
||||
}
|
||||
renderScene={({ route }) => descriptors[route.key].render()}
|
||||
renderTouchable={
|
||||
Platform.OS === 'web'
|
||||
? ({
|
||||
onPress,
|
||||
route,
|
||||
accessibilityRole: _0,
|
||||
borderless: _1,
|
||||
centered: _2,
|
||||
rippleColor: _3,
|
||||
style,
|
||||
...rest
|
||||
}) => {
|
||||
return (
|
||||
<Link
|
||||
{...rest}
|
||||
// @ts-expect-error: to could be undefined, but it doesn't affect functionality
|
||||
to={buildLink(route.name, route.params)}
|
||||
accessibilityRole="link"
|
||||
onPress={(e: any) => {
|
||||
if (
|
||||
!(e.metaKey || e.altKey || e.ctrlKey || e.shiftKey) && // ignore clicks with modifier keys
|
||||
(e.button == null || e.button === 0) // ignore everything but left clicks
|
||||
) {
|
||||
e.preventDefault();
|
||||
onPress?.(e);
|
||||
}
|
||||
}}
|
||||
style={[styles.touchable, style]}
|
||||
/>
|
||||
);
|
||||
}
|
||||
: undefined
|
||||
}
|
||||
renderIcon={({ route, focused, color }) => {
|
||||
const { options } = descriptors[route.key];
|
||||
|
||||
if (typeof options.tabBarIcon === 'string') {
|
||||
return (
|
||||
<MaterialCommunityIcons
|
||||
name={options.tabBarIcon}
|
||||
color={color}
|
||||
size={24}
|
||||
style={styles.icon}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
if (typeof options.tabBarIcon === 'function') {
|
||||
return options.tabBarIcon({ focused, color });
|
||||
}
|
||||
|
||||
return null;
|
||||
}}
|
||||
getLabelText={({ route }: Scene) => {
|
||||
const { options } = descriptors[route.key];
|
||||
|
||||
return options.tabBarLabel !== undefined
|
||||
? options.tabBarLabel
|
||||
: options.title !== undefined
|
||||
? options.title
|
||||
: (route as Route<string>).name;
|
||||
}}
|
||||
getColor={({ route }) => descriptors[route.key].options.tabBarColor}
|
||||
getBadge={({ route }) => descriptors[route.key].options.tabBarBadge}
|
||||
getAccessibilityLabel={({ route }) =>
|
||||
descriptors[route.key].options.tabBarAccessibilityLabel
|
||||
}
|
||||
getTestID={({ route }) => descriptors[route.key].options.tabBarTestID}
|
||||
onTabPress={({ route, preventDefault }) => {
|
||||
const event = navigation.emit({
|
||||
type: 'tabPress',
|
||||
target: route.key,
|
||||
canPreventDefault: true,
|
||||
});
|
||||
|
||||
if (event.defaultPrevented) {
|
||||
preventDefault();
|
||||
}
|
||||
}}
|
||||
safeAreaInsets={insets}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
export default function MaterialBottomTabView(props: Props) {
|
||||
return (
|
||||
<SafeAreaProviderCompat>
|
||||
<MaterialBottomTabViewInner {...props} />
|
||||
</SafeAreaProviderCompat>
|
||||
);
|
||||
}
|
||||
|
||||
const styles = StyleSheet.create({
|
||||
icon: {
|
||||
backgroundColor: 'transparent',
|
||||
},
|
||||
touchable: {
|
||||
display: 'flex',
|
||||
justifyContent: 'center',
|
||||
},
|
||||
});
|
||||
@@ -0,0 +1,71 @@
|
||||
import {
|
||||
createNavigatorFactory,
|
||||
DefaultNavigatorOptions,
|
||||
ParamListBase,
|
||||
TabActionHelpers,
|
||||
TabNavigationState,
|
||||
TabRouter,
|
||||
TabRouterOptions,
|
||||
useNavigationBuilder,
|
||||
} from '@react-navigation/native';
|
||||
import * as React from 'react';
|
||||
|
||||
import {
|
||||
MaterialBottomTabNavigationConfig,
|
||||
MaterialBottomTabNavigationEventMap,
|
||||
MaterialBottomTabNavigationOptions,
|
||||
} from '@react-navigation/material-bottom-tabs/lib/typescript/src/types';
|
||||
import MaterialBottomTabView from './MaterialBottomTabView';
|
||||
|
||||
type Props = DefaultNavigatorOptions<
|
||||
ParamListBase,
|
||||
TabNavigationState<ParamListBase>,
|
||||
MaterialBottomTabNavigationOptions,
|
||||
MaterialBottomTabNavigationEventMap
|
||||
> &
|
||||
TabRouterOptions &
|
||||
MaterialBottomTabNavigationConfig;
|
||||
|
||||
function MaterialBottomTabNavigator({
|
||||
id,
|
||||
initialRouteName,
|
||||
backBehavior,
|
||||
children,
|
||||
screenListeners,
|
||||
screenOptions,
|
||||
...rest
|
||||
}: Props) {
|
||||
const { state, descriptors, navigation, NavigationContent } =
|
||||
useNavigationBuilder<
|
||||
TabNavigationState<ParamListBase>,
|
||||
TabRouterOptions,
|
||||
TabActionHelpers<ParamListBase>,
|
||||
MaterialBottomTabNavigationOptions,
|
||||
MaterialBottomTabNavigationEventMap
|
||||
>(TabRouter, {
|
||||
id,
|
||||
initialRouteName,
|
||||
backBehavior,
|
||||
children,
|
||||
screenListeners,
|
||||
screenOptions,
|
||||
});
|
||||
|
||||
return (
|
||||
<NavigationContent>
|
||||
<MaterialBottomTabView
|
||||
{...rest}
|
||||
state={state}
|
||||
navigation={navigation}
|
||||
descriptors={descriptors}
|
||||
/>
|
||||
</NavigationContent>
|
||||
);
|
||||
}
|
||||
|
||||
export default createNavigatorFactory<
|
||||
TabNavigationState<ParamListBase>,
|
||||
MaterialBottomTabNavigationOptions,
|
||||
MaterialBottomTabNavigationEventMap,
|
||||
typeof MaterialBottomTabNavigator
|
||||
>(MaterialBottomTabNavigator);
|
||||
@@ -1,6 +1,5 @@
|
||||
import React from 'react';
|
||||
|
||||
import { createMaterialBottomTabNavigator } from '@react-navigation/material-bottom-tabs';
|
||||
import MaterialCommunityIcons from 'react-native-vector-icons/MaterialCommunityIcons';
|
||||
|
||||
import Library from '../screens/library/LibraryScreen';
|
||||
@@ -9,8 +8,11 @@ import History from '../screens/history/HistoryScreen';
|
||||
import Browse from '../screens/browse/BrowseScreen';
|
||||
import More from '../screens/more/MoreScreen';
|
||||
|
||||
import { useSettings, useTheme } from '../hooks/reduxHooks';
|
||||
import { useSettings } from '../hooks/reduxHooks';
|
||||
import { getString } from '../../strings/translations';
|
||||
import createMaterialBottomTabNavigator from './BottomNavigation/createMaterialBottomTabNavigator';
|
||||
import { useTheme } from '@hooks/useTheme';
|
||||
import Color from 'color';
|
||||
|
||||
const Tab = createMaterialBottomTabNavigator();
|
||||
|
||||
@@ -25,21 +27,22 @@ const BottomNavigator = () => {
|
||||
|
||||
return (
|
||||
<Tab.Navigator
|
||||
barStyle={{ backgroundColor: theme.colorPrimary }}
|
||||
activeColor={theme.colorAccent}
|
||||
theme={{
|
||||
colors: {
|
||||
...theme,
|
||||
background: theme.background,
|
||||
elevation: {
|
||||
level2: Color(theme.primary).alpha(0.08).string(),
|
||||
},
|
||||
},
|
||||
}}
|
||||
shifting={!showLabelsInNav}
|
||||
>
|
||||
<Tab.Screen
|
||||
name={getString('library')}
|
||||
component={Library}
|
||||
options={{
|
||||
tabBarIcon: ({ color }) => (
|
||||
<MaterialCommunityIcons
|
||||
name="book-variant-multiple"
|
||||
color={color}
|
||||
size={24}
|
||||
/>
|
||||
),
|
||||
tabBarIcon: 'book-variant-multiple',
|
||||
}}
|
||||
/>
|
||||
{showUpdatesTab && (
|
||||
@@ -47,13 +50,7 @@ const BottomNavigator = () => {
|
||||
name={getString('updates')}
|
||||
component={Updates}
|
||||
options={{
|
||||
tabBarIcon: ({ color }) => (
|
||||
<MaterialCommunityIcons
|
||||
name="alert-decagram-outline"
|
||||
color={color}
|
||||
size={24}
|
||||
/>
|
||||
),
|
||||
tabBarIcon: 'alert-decagram-outline',
|
||||
}}
|
||||
/>
|
||||
)}
|
||||
@@ -62,9 +59,7 @@ const BottomNavigator = () => {
|
||||
name={getString('history')}
|
||||
component={History}
|
||||
options={{
|
||||
tabBarIcon: ({ color }) => (
|
||||
<MaterialCommunityIcons name="history" color={color} size={24} />
|
||||
),
|
||||
tabBarIcon: 'history',
|
||||
}}
|
||||
/>
|
||||
)}
|
||||
@@ -72,26 +67,14 @@ const BottomNavigator = () => {
|
||||
name={getString('browse')}
|
||||
component={Browse}
|
||||
options={{
|
||||
tabBarIcon: ({ color }) => (
|
||||
<MaterialCommunityIcons
|
||||
name="compass-outline"
|
||||
color={color}
|
||||
size={24}
|
||||
/>
|
||||
),
|
||||
tabBarIcon: 'compass-outline',
|
||||
}}
|
||||
/>
|
||||
<Tab.Screen
|
||||
name={getString('more')}
|
||||
component={More}
|
||||
options={{
|
||||
tabBarIcon: ({ color }) => (
|
||||
<MaterialCommunityIcons
|
||||
name="dots-horizontal"
|
||||
color={color}
|
||||
size={24}
|
||||
/>
|
||||
),
|
||||
tabBarIcon: 'dots-horizontal',
|
||||
}}
|
||||
/>
|
||||
</Tab.Navigator>
|
||||
|
||||
+14
-5
@@ -4,7 +4,7 @@ import { NavigationContainer } from '@react-navigation/native';
|
||||
import { createStackNavigator } from '@react-navigation/stack';
|
||||
|
||||
import { setBarColor } from '../theme/utils/setBarColor';
|
||||
import { useTheme } from '../hooks/reduxHooks';
|
||||
import { useTheme } from '../hooks/useTheme';
|
||||
import { useGithubUpdateChecker } from '../hooks/githubUpdateChecker';
|
||||
import * as SplashScreen from 'expo-splash-screen';
|
||||
|
||||
@@ -28,11 +28,16 @@ import MigrateNovel from '../screens/browse/migration/MigrationNovels';
|
||||
import MalTopNovels from '../screens/browse/discover/MalTopNovels';
|
||||
import NewUpdateDialog from '../components/NewUpdateDialog';
|
||||
import BrowseSettings from '../screens/browse/BrowseSettings';
|
||||
import { useAppDispatch } from '@redux/hooks';
|
||||
import { updateLibraryAction } from '@redux/updates/updates.actions';
|
||||
import { useSettings } from '@hooks/reduxHooks';
|
||||
|
||||
const Stack = createStackNavigator();
|
||||
|
||||
const MainNavigator = () => {
|
||||
const theme = useTheme();
|
||||
const dispatch = useAppDispatch();
|
||||
const { updateLibraryOnLaunch = false } = useSettings();
|
||||
|
||||
useEffect(() => {
|
||||
const timer = setTimeout(async () => {
|
||||
@@ -43,14 +48,18 @@ const MainNavigator = () => {
|
||||
return () => {
|
||||
clearTimeout(timer);
|
||||
};
|
||||
}, [theme]);
|
||||
}, [theme.id]);
|
||||
|
||||
useEffect(() => {
|
||||
if (updateLibraryOnLaunch) {
|
||||
dispatch(updateLibraryAction());
|
||||
}
|
||||
}, []);
|
||||
|
||||
const { isNewVersion, latestRelease } = useGithubUpdateChecker() || {};
|
||||
|
||||
return (
|
||||
<NavigationContainer
|
||||
theme={{ colors: { background: theme.colorPrimaryDark } }}
|
||||
>
|
||||
<NavigationContainer theme={{ colors: { background: theme.background } }}>
|
||||
{isNewVersion && <NewUpdateDialog newVersion={latestRelease} />}
|
||||
<Stack.Navigator screenOptions={{ headerShown: false }}>
|
||||
<Stack.Screen name="BottomNavigator" component={BottomNavigator} />
|
||||
|
||||
@@ -13,6 +13,8 @@ import GeneralSettings from '../screens/settings/SettingsGeneralScreen';
|
||||
import DownloadQueue from '../screens/more/DownloadQueueScreen';
|
||||
import Downloads from '../screens/more/DownloadsScreen';
|
||||
import AppearanceSettings from '../screens/settings/SettingsAppearanceScreen';
|
||||
import CategoriesScreen from '@screens/Categories/CategoriesScreen';
|
||||
import LibrarySettings from '@screens/settings/SettingsLibraryScreen/SettingsLibraryScreen';
|
||||
|
||||
const Stack = createStackNavigator();
|
||||
|
||||
@@ -27,6 +29,7 @@ const SettingsStack = () => (
|
||||
<Stack.Screen name="BackupSettings" component={BackupSettings} />
|
||||
<Stack.Screen name="AppearanceSettings" component={AppearanceSettings} />
|
||||
<Stack.Screen name="AdvancedSettings" component={AdvancedSettings} />
|
||||
<Stack.Screen name="LibrarySettings" component={LibrarySettings} />
|
||||
</Stack.Navigator>
|
||||
);
|
||||
|
||||
@@ -36,6 +39,7 @@ const MoreStack = () => (
|
||||
<Stack.Screen name="About" component={About} />
|
||||
<Stack.Screen name="DownloadQueue" component={DownloadQueue} />
|
||||
<Stack.Screen name="Downloads" component={Downloads} />
|
||||
<Stack.Screen name="Categories" component={CategoriesScreen} />
|
||||
</Stack.Navigator>
|
||||
);
|
||||
|
||||
|
||||
@@ -47,20 +47,30 @@ export const resumeDownloads = chapters => async dispatch => {
|
||||
i++
|
||||
) {
|
||||
if (BackgroundService.isRunning()) {
|
||||
if (!chapters[i].downloaded) {
|
||||
await downloadChapter(
|
||||
chapters[i].sourceId,
|
||||
chapters[i].novelUrl,
|
||||
chapters[i].novelId,
|
||||
chapters[i].chapterUrl,
|
||||
chapters[i].chapterId,
|
||||
);
|
||||
}
|
||||
try {
|
||||
if (!chapters[i].downloaded) {
|
||||
await downloadChapter(
|
||||
chapters[i].sourceId,
|
||||
chapters[i].novelUrl,
|
||||
chapters[i].novelId,
|
||||
chapters[i].chapterUrl,
|
||||
chapters[i].chapterId,
|
||||
);
|
||||
}
|
||||
|
||||
dispatch({
|
||||
type: CHAPTER_DOWNLOADED,
|
||||
payload: chapters[i].chapterId,
|
||||
});
|
||||
dispatch({
|
||||
type: CHAPTER_DOWNLOADED,
|
||||
payload: chapters[i].chapterId,
|
||||
});
|
||||
} catch (error) {
|
||||
Notifications.scheduleNotificationAsync({
|
||||
content: {
|
||||
title: chapters[i].chapterName,
|
||||
body: `Download failed: ${error.message}`,
|
||||
},
|
||||
trigger: null,
|
||||
});
|
||||
}
|
||||
|
||||
await BackgroundService.updateNotification({
|
||||
taskTitle: chapters[i].chapterName,
|
||||
|
||||
@@ -1,37 +0,0 @@
|
||||
import { createSlice, PayloadAction } from '@reduxjs/toolkit';
|
||||
|
||||
import { ChapterItem } from '../../database/types';
|
||||
|
||||
interface SettingsState {
|
||||
downloadQueue: ChapterItem[];
|
||||
}
|
||||
|
||||
const initialState: SettingsState = {
|
||||
downloadQueue: [],
|
||||
};
|
||||
|
||||
export const settingsSlice = createSlice({
|
||||
name: 'downloadsReducer',
|
||||
initialState,
|
||||
reducers: {
|
||||
addToDownloadQueue: (state, action: PayloadAction<ChapterItem[]>) => {
|
||||
state.downloadQueue = state.downloadQueue.concat(action.payload);
|
||||
},
|
||||
removeFromDownloadQueue: (state, action: PayloadAction<number>) => {
|
||||
state.downloadQueue = state.downloadQueue.filter(
|
||||
(chapter: ChapterItem) => chapter.chapterId !== action.payload,
|
||||
);
|
||||
},
|
||||
clearDownloadQueue: state => {
|
||||
state.downloadQueue = [];
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
export const {
|
||||
addToDownloadQueue,
|
||||
removeFromDownloadQueue,
|
||||
clearDownloadQueue,
|
||||
} = settingsSlice.actions;
|
||||
|
||||
export default settingsSlice.reducer;
|
||||
@@ -1,15 +1,9 @@
|
||||
import { TypedUseSelectorHook, useDispatch, useSelector } from 'react-redux';
|
||||
import { ThemeType } from '../../theme/types';
|
||||
import type { RootState, AppDispatch } from '../store';
|
||||
|
||||
export const useAppDispatch = () => useDispatch<AppDispatch>();
|
||||
export const useAppSelector: TypedUseSelectorHook<RootState> = useSelector;
|
||||
|
||||
export const useTheme = () =>
|
||||
useAppSelector(
|
||||
(state: RootState) => state.settingsReducer.theme,
|
||||
) as ThemeType;
|
||||
|
||||
export const useSettingsV1 = () =>
|
||||
useAppSelector((state: RootState) => state.settingsReducer);
|
||||
|
||||
@@ -25,18 +19,13 @@ export const useAppearanceSettings = () =>
|
||||
export const useReaderSettings = () =>
|
||||
useAppSelector((state: RootState) => state.settingsReducer.reader);
|
||||
|
||||
/** @deprecated */
|
||||
export const useLibrarySettings = () =>
|
||||
useAppSelector((state: RootState) => state.settingsReducerV2.library);
|
||||
|
||||
export const useUpdateSettings = () =>
|
||||
useAppSelector((state: RootState) => state.settingsReducerV2.updates);
|
||||
|
||||
export const useSavedChapterData = (chapterId: number) =>
|
||||
useAppSelector(
|
||||
(state: RootState) =>
|
||||
state.localStorageReducer.chapterData[chapterId] || {},
|
||||
);
|
||||
|
||||
export const useDownloadQueue = () =>
|
||||
useAppSelector((state: RootState) => state.downloadsReducer.downloadQueue);
|
||||
|
||||
|
||||
@@ -1,61 +0,0 @@
|
||||
import {
|
||||
GET_LIBRARY_NOVELS,
|
||||
GET_LIBRARY_SEARCH_RESULTS,
|
||||
SET_LIBRARY_LOADING,
|
||||
SORT_FILTER_LIBRARY,
|
||||
} from './library.types';
|
||||
import {
|
||||
getLibrary,
|
||||
searchLibrary,
|
||||
} from '../../database/queries/LibraryQueries';
|
||||
|
||||
export const getLibraryAction =
|
||||
(sort, filter) => async (dispatch, getState) => {
|
||||
const { downloadedOnlyMode = false } = getState().settingsReducer;
|
||||
|
||||
const res = await getLibrary(
|
||||
sort,
|
||||
downloadedOnlyMode ? 'chaptersDownloaded > 0' : filter,
|
||||
);
|
||||
|
||||
dispatch({
|
||||
type: GET_LIBRARY_NOVELS,
|
||||
payload: res,
|
||||
});
|
||||
};
|
||||
|
||||
export const searchLibraryAction =
|
||||
(searchText, sort, filter) => async dispatch => {
|
||||
const res = await searchLibrary(searchText, sort, filter);
|
||||
|
||||
dispatch({
|
||||
type: GET_LIBRARY_SEARCH_RESULTS,
|
||||
payload: res,
|
||||
});
|
||||
};
|
||||
|
||||
export const filterLibrary = (sort, filter) => async dispatch => {
|
||||
dispatch({ type: SORT_FILTER_LIBRARY, payload: { sort, filter } });
|
||||
|
||||
const res = await getLibrary(sort, filter);
|
||||
|
||||
dispatch({
|
||||
type: GET_LIBRARY_NOVELS,
|
||||
payload: res,
|
||||
});
|
||||
};
|
||||
|
||||
// export const restoreLibraryAction = () => async dispatch => {
|
||||
// dispatch({type: SET_LIBRARY_LOADING});
|
||||
|
||||
// const res = await restoreBackup();
|
||||
|
||||
// if (res) {
|
||||
// const novels = await getLibrary();
|
||||
|
||||
// dispatch({
|
||||
// type: GET_LIBRARY_NOVELS,
|
||||
// payload: novels,
|
||||
// });
|
||||
// }
|
||||
// };
|
||||
@@ -1,36 +0,0 @@
|
||||
import {
|
||||
GET_LIBRARY_NOVELS,
|
||||
GET_LIBRARY_SEARCH_RESULTS,
|
||||
SET_LIBRARY_LOADING,
|
||||
SORT_FILTER_LIBRARY,
|
||||
} from './library.types';
|
||||
|
||||
const initialState = {
|
||||
novels: [],
|
||||
searchResults: [],
|
||||
filters: { sort: 'novels.novelId ASC', filter: '' },
|
||||
loading: true,
|
||||
};
|
||||
|
||||
const libraryReducer = (state = initialState, action) => {
|
||||
const { type, payload } = action;
|
||||
|
||||
switch (type) {
|
||||
case SET_LIBRARY_LOADING:
|
||||
return { ...state, loading: true };
|
||||
case GET_LIBRARY_NOVELS:
|
||||
return { ...state, novels: payload, loading: false };
|
||||
case GET_LIBRARY_SEARCH_RESULTS:
|
||||
return { ...state, novels: payload, loading: false };
|
||||
case SORT_FILTER_LIBRARY:
|
||||
// console.log("2.", payload.sort, payload.filter);
|
||||
return {
|
||||
...state,
|
||||
filters: { sort: payload.sort, filter: payload.filter },
|
||||
};
|
||||
default:
|
||||
return state;
|
||||
}
|
||||
};
|
||||
|
||||
export default libraryReducer;
|
||||
@@ -1,5 +0,0 @@
|
||||
export const GET_LIBRARY_NOVELS = 'GET_LIBRARY_NOVELS';
|
||||
export const GET_LIBRARY_SEARCH_RESULTS = 'GET_LIBRARY_SEARCH_RESULTS';
|
||||
export const SET_LIBRARY_LOADING = 'SET_LIBRARY_LOADING';
|
||||
|
||||
export const SORT_FILTER_LIBRARY = 'SORT_FILTER_LIBRARY';
|
||||
@@ -1,108 +0,0 @@
|
||||
import { createSlice, PayloadAction } from '@reduxjs/toolkit';
|
||||
|
||||
export enum ChapterTitleDisplayModes {
|
||||
SOURCE_TITLE = 'Source title',
|
||||
CHAPTER_NUMBER = 'Chapter number',
|
||||
}
|
||||
|
||||
interface ChapterData {
|
||||
progressPercentage: number;
|
||||
progressLocation: number;
|
||||
}
|
||||
|
||||
interface NovelData {
|
||||
filters?: string[];
|
||||
sort?: string;
|
||||
chapterTitleDisplayMode?: ChapterTitleDisplayModes;
|
||||
}
|
||||
|
||||
interface localStorageState {
|
||||
novelData: Record<number, NovelData>;
|
||||
chapterData: Record<number, ChapterData>;
|
||||
}
|
||||
|
||||
const initialState: localStorageState = {
|
||||
novelData: [],
|
||||
chapterData: [],
|
||||
};
|
||||
|
||||
export const localStorageSlice = createSlice({
|
||||
name: 'localStorageSlice',
|
||||
initialState,
|
||||
reducers: {
|
||||
saveChapterProgress: (
|
||||
state,
|
||||
action: PayloadAction<{
|
||||
chapterId: number;
|
||||
progressPercentage: number;
|
||||
progressLocation: number;
|
||||
}>,
|
||||
) => {
|
||||
state.chapterData[action.payload.chapterId] = {
|
||||
progressLocation: action.payload.progressLocation,
|
||||
progressPercentage: action.payload.progressPercentage,
|
||||
};
|
||||
},
|
||||
setNovelChapterFilter: (
|
||||
state,
|
||||
action: PayloadAction<{ novelId: number; filter: string }>,
|
||||
) => {
|
||||
const novelId = action.payload.novelId;
|
||||
if (state.novelData[novelId]) {
|
||||
state.novelData[novelId] = {};
|
||||
}
|
||||
if (!state.novelData[novelId].filters) {
|
||||
state.novelData[novelId].filters = [];
|
||||
}
|
||||
|
||||
if (
|
||||
state.novelData[action.payload.novelId].filters.includes(
|
||||
action.payload.filter,
|
||||
)
|
||||
) {
|
||||
state.novelData[action.payload.novelId].filters = state.novelData[
|
||||
action.payload.novelId
|
||||
].filters.filter(item => item !== action.payload.filter);
|
||||
} else {
|
||||
state.novelData[action.payload.novelId].filters = [
|
||||
...state.novelData[action.payload.novelId].filters,
|
||||
action.payload.filter,
|
||||
];
|
||||
}
|
||||
},
|
||||
setNovelChapterSortOrder: (
|
||||
state,
|
||||
action: PayloadAction<{ novelId: number; sort: string }>,
|
||||
) => {
|
||||
const novelId = action.payload.novelId;
|
||||
if (!state.novelData[novelId]) {
|
||||
state.novelData[novelId] = {};
|
||||
}
|
||||
|
||||
state.novelData[action.payload.novelId].sort = action.payload.sort;
|
||||
},
|
||||
setNovelChapterTitleDisplayMode: (
|
||||
state,
|
||||
action: PayloadAction<{
|
||||
novelId: number;
|
||||
chapterTitleDisplayMode: ChapterTitleDisplayModes;
|
||||
}>,
|
||||
) => {
|
||||
const novelId = action.payload.novelId;
|
||||
if (state.novelData[novelId] === undefined) {
|
||||
state.novelData[novelId] = {};
|
||||
}
|
||||
state.novelData[action.payload.novelId].chapterTitleDisplayMode =
|
||||
action.payload.chapterTitleDisplayMode;
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
export const {
|
||||
saveChapterProgress,
|
||||
setNovelChapterFilter,
|
||||
setNovelChapterSortOrder,
|
||||
setNovelChapterTitleDisplayMode,
|
||||
} = localStorageSlice.actions;
|
||||
|
||||
export default localStorageSlice.reducer;
|
||||
@@ -41,8 +41,6 @@ import {
|
||||
SET_CHAPTER_LIST_PREF,
|
||||
SET_LAST_READ,
|
||||
} from '../preferences/preference.types';
|
||||
import { GET_LIBRARY_NOVELS } from '../library/library.types';
|
||||
import { getLibrary } from '../../database/queries/LibraryQueries';
|
||||
import { showToast } from '../../hooks/showToast';
|
||||
|
||||
import * as Notifications from 'expo-notifications';
|
||||
@@ -66,7 +64,8 @@ export const setNovel = novel => async dispatch => {
|
||||
};
|
||||
|
||||
export const getNovelAction =
|
||||
(followed, sourceId, novelUrl, novelId, sort, filter) => async dispatch => {
|
||||
(followed, sourceId, novelUrl, novelId, sort, filter, defaultCategoryId) =>
|
||||
async dispatch => {
|
||||
try {
|
||||
dispatch({ type: LOADING_NOVEL });
|
||||
|
||||
@@ -106,7 +105,10 @@ export const getNovelAction =
|
||||
/**
|
||||
* Insert novel in db.
|
||||
*/
|
||||
const fetchedNovelId = await insertNovel(fetchedNovel);
|
||||
const fetchedNovelId = await insertNovel({
|
||||
...fetchedNovel,
|
||||
categoryIds: [defaultCategoryId],
|
||||
});
|
||||
await insertChapters(fetchedNovelId, fetchedNovel.chapters);
|
||||
|
||||
/**
|
||||
@@ -163,12 +165,12 @@ export const followNovelAction = novel => async dispatch => {
|
||||
payload: { novelUrl: novel.novelUrl, followed: !novel.followed },
|
||||
});
|
||||
|
||||
const res = await getLibrary();
|
||||
// const res = await getLibrary();
|
||||
|
||||
dispatch({
|
||||
type: GET_LIBRARY_NOVELS,
|
||||
payload: res,
|
||||
});
|
||||
// dispatch({
|
||||
// type: GET_LIBRARY_NOVELS,
|
||||
// payload: res,
|
||||
// });
|
||||
|
||||
showToast(!novel.followed ? 'Added to library' : 'Removed from library');
|
||||
};
|
||||
@@ -364,26 +366,31 @@ export const downloadAllChaptersAction =
|
||||
i++
|
||||
) {
|
||||
if (BackgroundService.isRunning()) {
|
||||
// dispatch({
|
||||
// type: CHAPTER_DOWNLOADING,
|
||||
// payload: chapters[i].chapterId,
|
||||
// });
|
||||
try {
|
||||
if (!chapters[i].downloaded) {
|
||||
await downloadChapter(
|
||||
sourceId,
|
||||
novelUrl,
|
||||
chapters[i].novelId,
|
||||
chapters[i].chapterUrl,
|
||||
chapters[i].chapterId,
|
||||
);
|
||||
}
|
||||
|
||||
if (!chapters[i].downloaded) {
|
||||
await downloadChapter(
|
||||
sourceId,
|
||||
novelUrl,
|
||||
chapters[i].novelId,
|
||||
chapters[i].chapterUrl,
|
||||
chapters[i].chapterId,
|
||||
);
|
||||
dispatch({
|
||||
type: CHAPTER_DOWNLOADED,
|
||||
payload: chapters[i].chapterId,
|
||||
});
|
||||
} catch (error) {
|
||||
Notifications.scheduleNotificationAsync({
|
||||
content: {
|
||||
title: chapters[i].chapterName,
|
||||
body: `Download failed: ${error.message}`,
|
||||
},
|
||||
trigger: null,
|
||||
});
|
||||
}
|
||||
|
||||
dispatch({
|
||||
type: CHAPTER_DOWNLOADED,
|
||||
payload: chapters[i].chapterId,
|
||||
});
|
||||
|
||||
await BackgroundService.updateNotification({
|
||||
taskTitle: chapters[i].chapterName,
|
||||
taskDesc: i + 1 + '/' + chapters.length,
|
||||
|
||||
@@ -1,26 +1,4 @@
|
||||
import {
|
||||
SET_APP_THEME,
|
||||
SET_NOVELS_PER_ROW,
|
||||
SET_READER_SETTINGS,
|
||||
SET_APP_SETTINGS,
|
||||
SET_ACCENT_COLOR,
|
||||
SET_RIPPLE_COLOR,
|
||||
SET_AMOLED_MODE,
|
||||
} from './settings.types';
|
||||
|
||||
export const setAppTheme = val => dispatch => {
|
||||
dispatch({
|
||||
type: SET_APP_THEME,
|
||||
payload: val,
|
||||
});
|
||||
};
|
||||
|
||||
export const setNovelsPerRow = val => dispatch => {
|
||||
dispatch({
|
||||
type: SET_NOVELS_PER_ROW,
|
||||
payload: val,
|
||||
});
|
||||
};
|
||||
import { SET_READER_SETTINGS, SET_APP_SETTINGS } from './settings.types';
|
||||
|
||||
export const setAppSettings = (key, val) => dispatch => {
|
||||
dispatch({
|
||||
@@ -35,24 +13,3 @@ export const setReaderSettings = (key, val) => dispatch => {
|
||||
payload: { key, val },
|
||||
});
|
||||
};
|
||||
|
||||
export const setAccentColor = val => dispatch => {
|
||||
dispatch({
|
||||
type: SET_ACCENT_COLOR,
|
||||
payload: val,
|
||||
});
|
||||
};
|
||||
|
||||
export const setRippleColor = val => dispatch => {
|
||||
dispatch({
|
||||
type: SET_RIPPLE_COLOR,
|
||||
payload: val,
|
||||
});
|
||||
};
|
||||
|
||||
export const setAmoledMode = (id, val) => dispatch => {
|
||||
dispatch({
|
||||
type: SET_AMOLED_MODE,
|
||||
payload: { id, val },
|
||||
});
|
||||
};
|
||||
|
||||
@@ -1,50 +1,4 @@
|
||||
import {
|
||||
SET_APP_THEME,
|
||||
SET_NOVELS_PER_ROW,
|
||||
SET_READER_SETTINGS,
|
||||
SET_APP_SETTINGS,
|
||||
SET_ACCENT_COLOR,
|
||||
SET_RIPPLE_COLOR,
|
||||
SET_AMOLED_MODE,
|
||||
} from './settings.types';
|
||||
import {
|
||||
lightTheme,
|
||||
darkTheme,
|
||||
midnightDuskTheme,
|
||||
greenAppleTheme,
|
||||
irisBlueTheme,
|
||||
strawberryDaiquiri,
|
||||
takoTheme,
|
||||
yinYangTheme,
|
||||
springBlossomTheme,
|
||||
colorsAmoled,
|
||||
oceanicTheme,
|
||||
yangTheme,
|
||||
yotsubaTheme,
|
||||
takoLightTheme,
|
||||
tealTheme,
|
||||
sapphireDuskTheme,
|
||||
chocolateStrawberriesTheme,
|
||||
} from '../../theme/theme.ts';
|
||||
|
||||
const themes = {
|
||||
1: lightTheme,
|
||||
2: darkTheme,
|
||||
3: midnightDuskTheme,
|
||||
4: greenAppleTheme,
|
||||
5: irisBlueTheme,
|
||||
6: oceanicTheme,
|
||||
7: strawberryDaiquiri,
|
||||
8: takoTheme,
|
||||
9: yinYangTheme,
|
||||
10: springBlossomTheme,
|
||||
11: yangTheme,
|
||||
12: yotsubaTheme,
|
||||
13: takoLightTheme,
|
||||
14: tealTheme,
|
||||
15: sapphireDuskTheme,
|
||||
16: chocolateStrawberriesTheme,
|
||||
};
|
||||
import { SET_READER_SETTINGS, SET_APP_SETTINGS } from './settings.types';
|
||||
|
||||
/**
|
||||
* Display Mode
|
||||
@@ -67,9 +21,6 @@ export const initialState = {
|
||||
* Appearence settings
|
||||
*/
|
||||
|
||||
theme: darkTheme,
|
||||
displayMode: 0,
|
||||
novelsPerRow: 3,
|
||||
showHistoryTab: true,
|
||||
showUpdatesTab: true,
|
||||
showLabelsInNav: false,
|
||||
@@ -79,9 +30,6 @@ export const initialState = {
|
||||
* Library settings
|
||||
*/
|
||||
|
||||
showDownloadBadges: true,
|
||||
showUnreadBadges: true,
|
||||
showNumberOfNovels: false,
|
||||
downloadedOnlyMode: false,
|
||||
|
||||
/**
|
||||
@@ -141,44 +89,6 @@ const settingsReducer = (state = initialState, action) => {
|
||||
const { type, payload } = action;
|
||||
|
||||
switch (type) {
|
||||
case SET_APP_THEME:
|
||||
return { ...state, theme: themes[payload] };
|
||||
case SET_NOVELS_PER_ROW:
|
||||
return {
|
||||
...state,
|
||||
novelsPerRow: state.displayMode !== 2 ? payload : 1,
|
||||
};
|
||||
case SET_ACCENT_COLOR:
|
||||
return {
|
||||
...state,
|
||||
theme: {
|
||||
...state.theme,
|
||||
colorAccent: payload,
|
||||
},
|
||||
};
|
||||
case SET_RIPPLE_COLOR:
|
||||
return {
|
||||
...state,
|
||||
theme: {
|
||||
...state.theme,
|
||||
rippleColor: payload,
|
||||
},
|
||||
};
|
||||
case SET_AMOLED_MODE:
|
||||
return {
|
||||
...state,
|
||||
theme: payload.val
|
||||
? {
|
||||
...colorsAmoled,
|
||||
colorAccent: state.theme.colorAccent,
|
||||
rippleColor: state.theme.rippleColor,
|
||||
colorButtonText: state.theme.colorButtonText,
|
||||
id: state.theme.id,
|
||||
name: state.theme.name,
|
||||
}
|
||||
: themes[payload.id],
|
||||
};
|
||||
|
||||
case SET_APP_SETTINGS:
|
||||
return {
|
||||
...state,
|
||||
|
||||
@@ -1,7 +1,2 @@
|
||||
export const SET_APP_SETTINGS = 'SET_APP_SETTINGS';
|
||||
export const SET_APP_THEME = 'SET_APP_THEME';
|
||||
export const SET_NOVELS_PER_ROW = 'SET_NOVELS_PER_ROW';
|
||||
export const SET_READER_SETTINGS = 'SET_READER_SETTINGS';
|
||||
export const SET_ACCENT_COLOR = 'SET_ACCENT_COLOR';
|
||||
export const SET_RIPPLE_COLOR = 'SET_RIPPLE_COLOR';
|
||||
export const SET_AMOLED_MODE = 'SET_AMOLED_MODE';
|
||||
|
||||
@@ -1,3 +1,7 @@
|
||||
/**
|
||||
* @deprecated
|
||||
*/
|
||||
|
||||
import { createSlice, PayloadAction } from '@reduxjs/toolkit';
|
||||
|
||||
import { BrowseSettingsMap } from './types';
|
||||
|
||||
@@ -1,63 +0,0 @@
|
||||
import {
|
||||
GET_SOURCES,
|
||||
SEARCH_SOURCES,
|
||||
PIN_SOURCES,
|
||||
FILTER_LANGUAGE,
|
||||
ENABLE_DISCOVER,
|
||||
} from './source.types';
|
||||
|
||||
const initialState = {
|
||||
sources: [],
|
||||
pinnedSources: [],
|
||||
search: [],
|
||||
pinned: [],
|
||||
filters: [],
|
||||
showMyAnimeList: true,
|
||||
loading: true,
|
||||
};
|
||||
|
||||
const sourceReducer = (state = initialState, action) => {
|
||||
const { type, payload } = action;
|
||||
|
||||
switch (type) {
|
||||
case GET_SOURCES:
|
||||
return { ...state, sources: payload, loading: false };
|
||||
case PIN_SOURCES:
|
||||
return {
|
||||
...state,
|
||||
pinned:
|
||||
state.pinned.indexOf(payload) === -1
|
||||
? [...state.pinned, payload]
|
||||
: state.pinned.filter(source => source !== payload),
|
||||
loading: false,
|
||||
};
|
||||
case SEARCH_SOURCES:
|
||||
return {
|
||||
...state,
|
||||
search: state.sources.filter(source =>
|
||||
source.sourceName.toLowerCase().includes(payload),
|
||||
),
|
||||
};
|
||||
|
||||
case FILTER_LANGUAGE:
|
||||
if (!state.filters) {
|
||||
state.filters = [];
|
||||
}
|
||||
return {
|
||||
...state,
|
||||
filters:
|
||||
state.filters.indexOf(payload) === -1
|
||||
? [...state.filters, payload]
|
||||
: state.filters.filter(language => language !== payload),
|
||||
};
|
||||
case ENABLE_DISCOVER:
|
||||
return {
|
||||
...state,
|
||||
[payload]: !state[payload],
|
||||
};
|
||||
default:
|
||||
return state;
|
||||
}
|
||||
};
|
||||
|
||||
export default sourceReducer;
|
||||
@@ -1,5 +0,0 @@
|
||||
export const GET_SOURCES = 'GET_SOURCES';
|
||||
export const SEARCH_SOURCES = 'SEARCH_SOURCES';
|
||||
export const PIN_SOURCES = 'PIN_SOURCES';
|
||||
export const FILTER_LANGUAGE = 'FILTER_LANGUAGE';
|
||||
export const ENABLE_DISCOVER = 'ENABLE_DISCOVER';
|
||||
@@ -5,6 +5,7 @@ import { Source } from '../../sources/types';
|
||||
|
||||
interface SourcesState {
|
||||
allSources: Source[];
|
||||
searchResults: Source[];
|
||||
pinnedSourceIds: number[];
|
||||
lastUsed: number | null;
|
||||
languageFilters: string[];
|
||||
@@ -12,6 +13,7 @@ interface SourcesState {
|
||||
|
||||
const initialState: SourcesState = {
|
||||
allSources: AllSources,
|
||||
searchResults: [],
|
||||
pinnedSourceIds: [],
|
||||
lastUsed: null,
|
||||
languageFilters: ['English'],
|
||||
@@ -29,17 +31,13 @@ export const sourcesSlice = createSlice({
|
||||
state.allSources = sources;
|
||||
},
|
||||
searchSourcesAction: (state, action: PayloadAction<string>) => {
|
||||
state.allSources = AllSources.filter(
|
||||
source => state.languageFilters.indexOf(source.lang) !== -1,
|
||||
)
|
||||
.sort((a, b) => a.sourceName.localeCompare(b.sourceName))
|
||||
.filter(
|
||||
(source: Source) =>
|
||||
source.sourceName
|
||||
.toLowerCase()
|
||||
.includes(action.payload.toLowerCase()) &&
|
||||
state.languageFilters.indexOf(source.lang) !== -1,
|
||||
);
|
||||
if (state?.pinnedSourceIds === undefined) {
|
||||
state.pinnedSourceIds = [];
|
||||
}
|
||||
|
||||
state.searchResults = state.allSources.filter((source: Source) =>
|
||||
source.sourceName.toLowerCase().includes(action.payload.toLowerCase()),
|
||||
);
|
||||
},
|
||||
setLastUsedSource: (state, action: PayloadAction<{ sourceId: number }>) => {
|
||||
state.lastUsed = action.payload.sourceId;
|
||||
|
||||
@@ -4,16 +4,12 @@ import { persistStore, persistReducer } from 'redux-persist';
|
||||
|
||||
import settingsReducer from './settings/settings.reducer';
|
||||
import settingsReducerV2 from './settings/settingsSlice';
|
||||
import localStorageReducer from './localStorage/localStorageSlice';
|
||||
import libraryReducer from './library/library.reducer';
|
||||
import sourceReducer from './source/source.reducers';
|
||||
import sourceReducerV2 from './source/sourcesSlice';
|
||||
import novelReducer from './novel/novel.reducer';
|
||||
import updatesReducer from './updates/updates.reducer';
|
||||
import trackerReducer from './tracker/tracker.reducer';
|
||||
import preferenceReducer from './preferences/preference.reducer';
|
||||
import downloadsReducer from './downloads/downloads.reducer';
|
||||
import downloadsReducerV2 from './downloads/downloadsSlice';
|
||||
|
||||
const persistConfig = {
|
||||
key: 'root',
|
||||
@@ -24,16 +20,12 @@ const persistConfig = {
|
||||
const reducers = combineReducers({
|
||||
settingsReducer,
|
||||
settingsReducerV2,
|
||||
localStorageReducer,
|
||||
libraryReducer,
|
||||
sourceReducer,
|
||||
sourceReducerV2,
|
||||
novelReducer,
|
||||
updatesReducer,
|
||||
trackerReducer,
|
||||
preferenceReducer,
|
||||
downloadsReducer,
|
||||
downloadsReducerV2,
|
||||
});
|
||||
|
||||
const persistedReducer = persistReducer(persistConfig, reducers);
|
||||
|
||||
@@ -1,9 +1,9 @@
|
||||
import { GET_UPDATES, SET_LAST_UPDATE_TIME } from './updates.types';
|
||||
import { SET_LAST_UPDATE_TIME } from './updates.types';
|
||||
|
||||
import { showToast } from '../../hooks/showToast';
|
||||
import { updateLibrary } from '../../services/updates/updates';
|
||||
|
||||
export const updateLibraryAction = () => async (dispatch, getState) => {
|
||||
export const updateLibraryAction = props => async (dispatch, getState) => {
|
||||
showToast('Updating library');
|
||||
|
||||
dispatch({ type: SET_LAST_UPDATE_TIME, payload: Date.now() });
|
||||
@@ -14,9 +14,12 @@ export const updateLibraryAction = () => async (dispatch, getState) => {
|
||||
refreshNovelMetadata = false,
|
||||
} = getState().settingsReducer;
|
||||
|
||||
const categoryId = props?.categoryId;
|
||||
|
||||
await updateLibrary({
|
||||
downloadNewChapters,
|
||||
onlyUpdateOngoingNovels,
|
||||
refreshNovelMetadata,
|
||||
categoryId,
|
||||
});
|
||||
};
|
||||
|
||||
@@ -15,15 +15,16 @@ import NovelList from '@components/NovelList';
|
||||
import NovelCover from '@components/NovelCover';
|
||||
import FilterBottomSheet from './components/BottomSheet';
|
||||
|
||||
import { useSearch } from '../../hooks';
|
||||
import { useTheme } from '../../redux/hooks';
|
||||
import { useCategorySettings, useSearch } from '../../hooks';
|
||||
import { useTheme } from '@hooks/useTheme';
|
||||
import { useBrowseSource, useSearchSource } from './useBrowseSource';
|
||||
import { useLibrary } from '@hooks/reduxHooks';
|
||||
|
||||
import { LibraryNovelInfo } from '../../database/types';
|
||||
import { SourceNovelItem } from '../../sources/types';
|
||||
import { getString } from '@strings/translations';
|
||||
import { StyleSheet } from 'react-native';
|
||||
import { useLibraryNovels } from '../../screens/library/hooks/useLibrary';
|
||||
import { insertNovelInLibrary } from '../../database/queries/NovelQueriesV2';
|
||||
import { LibraryNovelInfo } from '../../database/types';
|
||||
|
||||
interface BrowseSourceScreenProps {
|
||||
route: {
|
||||
@@ -58,6 +59,7 @@ const BrowseSourceScreen: React.FC<BrowseSourceScreenProps> = ({ route }) => {
|
||||
clearFilters,
|
||||
} = useBrowseSource(sourceId, showLatestNovels);
|
||||
|
||||
const { defaultCategoryId = 1 } = useCategorySettings();
|
||||
const {
|
||||
isSearching,
|
||||
searchResults,
|
||||
@@ -81,12 +83,11 @@ const BrowseSourceScreen: React.FC<BrowseSourceScreenProps> = ({ route }) => {
|
||||
WebBrowser.openBrowserAsync(sourceUrl);
|
||||
};
|
||||
|
||||
const library = useLibrary();
|
||||
const { library, setLibrary } = useLibraryNovels();
|
||||
|
||||
const novelInLibrary = (novelUrl: string) =>
|
||||
library?.some(
|
||||
(novel: LibraryNovelInfo) =>
|
||||
novel.novelUrl === novelUrl && novel.sourceId === sourceId,
|
||||
novel => novel.novelUrl === novelUrl && novel.sourceId === sourceId,
|
||||
);
|
||||
|
||||
const navigateToNovel = useCallback(
|
||||
@@ -128,21 +129,52 @@ const BrowseSourceScreen: React.FC<BrowseSourceScreenProps> = ({ route }) => {
|
||||
) : (
|
||||
<NovelList
|
||||
data={novelList}
|
||||
renderItem={({ item }) => (
|
||||
<NovelCover
|
||||
item={item}
|
||||
theme={theme}
|
||||
libraryStatus={novelInLibrary(item.novelUrl)}
|
||||
onPress={() => navigateToNovel(item)}
|
||||
/>
|
||||
)}
|
||||
renderItem={({ item }) => {
|
||||
const inLibrary = novelInLibrary(item.novelUrl);
|
||||
|
||||
return (
|
||||
<NovelCover
|
||||
item={item}
|
||||
theme={theme}
|
||||
libraryStatus={inLibrary}
|
||||
onPress={() => navigateToNovel(item)}
|
||||
onLongPress={() => {
|
||||
setLibrary(prevValues => {
|
||||
if (inLibrary) {
|
||||
return [
|
||||
...prevValues.filter(
|
||||
novel => novel.novelUrl !== item.novelUrl,
|
||||
),
|
||||
];
|
||||
} else {
|
||||
return [
|
||||
...prevValues,
|
||||
{
|
||||
novelUrl: item.novelUrl,
|
||||
sourceId,
|
||||
} as LibraryNovelInfo,
|
||||
];
|
||||
}
|
||||
});
|
||||
insertNovelInLibrary(
|
||||
sourceId,
|
||||
item.novelUrl,
|
||||
inLibrary,
|
||||
defaultCategoryId,
|
||||
);
|
||||
}}
|
||||
/>
|
||||
);
|
||||
}}
|
||||
onEndReached={() => {
|
||||
if (hasNextPage && !searchText) {
|
||||
fetchNextPage();
|
||||
}
|
||||
}}
|
||||
ListFooterComponent={
|
||||
hasNextPage && !searchText && <LoadingMoreIndicator theme={theme} />
|
||||
hasNextPage && !searchText ? (
|
||||
<LoadingMoreIndicator theme={theme} />
|
||||
) : undefined
|
||||
}
|
||||
/>
|
||||
)}
|
||||
@@ -150,10 +182,10 @@ const BrowseSourceScreen: React.FC<BrowseSourceScreenProps> = ({ route }) => {
|
||||
<>
|
||||
<FAB
|
||||
icon={'filter-variant'}
|
||||
style={[styles.filterFab, { backgroundColor: theme.colorAccent }]}
|
||||
style={[styles.filterFab, { backgroundColor: theme.primary }]}
|
||||
label={'Filter'}
|
||||
uppercase={false}
|
||||
color={theme.colorButtonText}
|
||||
color={theme.onPrimary}
|
||||
onPress={() => filterSheetRef?.current?.show()}
|
||||
/>
|
||||
<Portal>
|
||||
|
||||
@@ -1,9 +1,9 @@
|
||||
import React, { LegacyRef, useState } from 'react';
|
||||
import { Animated, Pressable, StyleSheet, Text, View } from 'react-native';
|
||||
|
||||
import color from 'color';
|
||||
import Bottomsheet from 'rn-sliding-up-panel';
|
||||
|
||||
import { useTheme } from '@redux/hooks';
|
||||
import { useTheme } from '@hooks/useTheme';
|
||||
import {
|
||||
FilterInputs,
|
||||
SelectedFilter,
|
||||
@@ -12,11 +12,12 @@ import {
|
||||
import { FlatList } from 'react-native-gesture-handler';
|
||||
import { Button, IconButtonV2 } from '@components/index';
|
||||
import { ButtonVariation } from '@components/Button/Button';
|
||||
import { coverPlaceholderColor } from '../../../theme/colors';
|
||||
import { dividerColor } from '../../../theme/colors';
|
||||
import { Checkbox } from '@components/Checkbox/Checkbox';
|
||||
import { defaultTo } from 'lodash';
|
||||
import { Picker } from '@react-native-picker/picker';
|
||||
import useBoolean from '@hooks/useBoolean';
|
||||
import { overlay } from 'react-native-paper';
|
||||
|
||||
const insertOrRemoveIntoArray = (array: string[], val: string): string[] =>
|
||||
array.indexOf(val) > -1 ? array.filter(ele => ele !== val) : [...array, val];
|
||||
@@ -48,7 +49,7 @@ const FilterItem: React.FC<FilterItemProps> = ({
|
||||
style={styles.picker}
|
||||
mode="dropdown"
|
||||
selectedValue={selectedFilters?.[filter.key] as string}
|
||||
dropdownIconRippleColor={theme.colorAccent}
|
||||
dropdownIconRippleColor={theme.primary}
|
||||
onValueChange={(itemValue: string) =>
|
||||
setSelectedFilters(prevFilters => ({
|
||||
...prevFilters,
|
||||
@@ -74,7 +75,7 @@ const FilterItem: React.FC<FilterItemProps> = ({
|
||||
<Pressable
|
||||
style={styles.checkboxHeader}
|
||||
onPress={toggleCard}
|
||||
android_ripple={{ color: theme.rippleColor }}
|
||||
android_ripple={{ color: color(theme.primary).alpha(0.12).string() }}
|
||||
>
|
||||
<Text
|
||||
style={[styles.filterLabel, { color: theme.textColorSecondary }]}
|
||||
@@ -117,7 +118,7 @@ const FilterItem: React.FC<FilterItemProps> = ({
|
||||
interface BottomSheetProps {
|
||||
filtersValues: SourceFilter[] | undefined;
|
||||
filterSheetRef: LegacyRef<Bottomsheet> | null;
|
||||
setFilters: React.Dispatch<React.SetStateAction<SelectedFilter | undefined>>;
|
||||
setFilters: (filters?: SelectedFilter) => void;
|
||||
clearFilters: () => void;
|
||||
}
|
||||
|
||||
@@ -143,8 +144,18 @@ const BottomSheet: React.FC<BottomSheetProps> = ({
|
||||
backdropOpacity={0.3}
|
||||
height={600}
|
||||
>
|
||||
<View style={[styles.container, { backgroundColor: theme.colorPrimary }]}>
|
||||
<View style={styles.buttonContainer}>
|
||||
<View
|
||||
style={[
|
||||
styles.container,
|
||||
{ backgroundColor: overlay(2, theme.surface) },
|
||||
]}
|
||||
>
|
||||
<View
|
||||
style={[
|
||||
styles.buttonContainer,
|
||||
{ borderBottomColor: dividerColor(theme.isDark) },
|
||||
]}
|
||||
>
|
||||
<Button
|
||||
title={'Reset'}
|
||||
variation={ButtonVariation.CLEAR}
|
||||
@@ -195,7 +206,6 @@ const styles = StyleSheet.create({
|
||||
alignItems: 'center',
|
||||
paddingVertical: 12,
|
||||
paddingHorizontal: 16,
|
||||
borderBottomColor: coverPlaceholderColor,
|
||||
borderBottomWidth: 1,
|
||||
},
|
||||
filterContainer: {
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import { useState, useEffect, useCallback, useRef } from 'react';
|
||||
import { SelectedFilter, SourceFilter } from 'src/sources/types/filterTypes';
|
||||
import { SelectedFilter, SourceFilter } from '../../sources/types/filterTypes';
|
||||
import { sourceManager } from '../../sources/sourceManager';
|
||||
import { SourceNovelItem } from '../../sources/types';
|
||||
|
||||
|
||||
@@ -0,0 +1,145 @@
|
||||
import { FlatList, StyleSheet } from 'react-native';
|
||||
import React, { useEffect, useState } from 'react';
|
||||
import { FAB, Portal } from 'react-native-paper';
|
||||
import { useNavigation } from '@react-navigation/native';
|
||||
|
||||
import { Appbar, EmptyView, LoadingScreenV2 } from '@components/index';
|
||||
import AddCategoryModal from './components/AddCategoryModal';
|
||||
|
||||
import {
|
||||
getCategoriesFromDb,
|
||||
updateCategoryOrderInDb,
|
||||
} from '../../database/queries/CategoryQueries';
|
||||
import useBoolean from '@hooks/useBoolean';
|
||||
import { useTheme } from '@hooks/useTheme';
|
||||
import { getString } from '@strings/translations';
|
||||
|
||||
import { Category } from '../../database/types';
|
||||
import CategoryCard from './components/CategoryCard';
|
||||
import { orderBy } from 'lodash';
|
||||
|
||||
const CategoriesScreen = () => {
|
||||
const theme = useTheme();
|
||||
const { goBack } = useNavigation();
|
||||
|
||||
const [isLoading, setIsLoading] = useState(true);
|
||||
const [categories, setCategories] = useState<Category[]>();
|
||||
const [error, setError] = useState();
|
||||
|
||||
const getCategories = async () => {
|
||||
try {
|
||||
let res = await getCategoriesFromDb();
|
||||
res.shift();
|
||||
|
||||
setCategories(res);
|
||||
} catch (err) {
|
||||
setError(error?.message);
|
||||
} finally {
|
||||
setIsLoading(false);
|
||||
}
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
getCategories();
|
||||
}, []);
|
||||
|
||||
const updateCategorySort = (
|
||||
categoryId: number,
|
||||
currentIndex: number,
|
||||
newIndex: number,
|
||||
) => {
|
||||
const updatedOrderCategories = orderBy(
|
||||
categories?.map((category, index) => {
|
||||
if (category.sort === newIndex || newIndex === index) {
|
||||
return {
|
||||
...category,
|
||||
sort: currentIndex,
|
||||
};
|
||||
}
|
||||
|
||||
if (category.id === categoryId) {
|
||||
return {
|
||||
...category,
|
||||
sort: newIndex,
|
||||
};
|
||||
}
|
||||
|
||||
return category;
|
||||
}),
|
||||
'sort',
|
||||
);
|
||||
|
||||
setCategories(updatedOrderCategories);
|
||||
updateCategoryOrderInDb(updatedOrderCategories || []);
|
||||
};
|
||||
|
||||
const {
|
||||
value: categoryModalVisible,
|
||||
setTrue: showCategoryModal,
|
||||
setFalse: closeCategoryModal,
|
||||
} = useBoolean();
|
||||
|
||||
return (
|
||||
<>
|
||||
<Appbar
|
||||
title={getString('categories.header')}
|
||||
handleGoBack={goBack}
|
||||
theme={theme}
|
||||
/>
|
||||
{isLoading ? (
|
||||
<LoadingScreenV2 theme={theme} />
|
||||
) : (
|
||||
<FlatList
|
||||
data={categories}
|
||||
contentContainerStyle={styles.contentCtn}
|
||||
renderItem={({ item, index }) => (
|
||||
<CategoryCard
|
||||
category={item}
|
||||
getCategories={getCategories}
|
||||
categoryIndex={index}
|
||||
updateCategorySort={updateCategorySort}
|
||||
totalCategories={categories?.length || 0}
|
||||
/>
|
||||
)}
|
||||
ListEmptyComponent={
|
||||
<EmptyView
|
||||
icon="Σ(ಠ_ಠ)"
|
||||
description={getString('categories.emptyMsg')}
|
||||
theme={theme}
|
||||
/>
|
||||
}
|
||||
/>
|
||||
)}
|
||||
<FAB
|
||||
style={[styles.fab, { backgroundColor: theme.primary }]}
|
||||
color={theme.onPrimary}
|
||||
label={getString('common.add')}
|
||||
uppercase={false}
|
||||
onPress={showCategoryModal}
|
||||
icon={'plus'}
|
||||
/>
|
||||
<Portal>
|
||||
<AddCategoryModal
|
||||
visible={categoryModalVisible}
|
||||
closeModal={closeCategoryModal}
|
||||
onSuccess={getCategories}
|
||||
/>
|
||||
</Portal>
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
export default CategoriesScreen;
|
||||
|
||||
const styles = StyleSheet.create({
|
||||
fab: {
|
||||
position: 'absolute',
|
||||
margin: 16,
|
||||
right: 0,
|
||||
bottom: 16,
|
||||
},
|
||||
contentCtn: {
|
||||
paddingVertical: 16,
|
||||
paddingBottom: 100,
|
||||
},
|
||||
});
|
||||
@@ -0,0 +1,112 @@
|
||||
import React, { useState } from 'react';
|
||||
import { StyleSheet, Text, View } from 'react-native';
|
||||
import { Modal, overlay, Portal, TextInput } from 'react-native-paper';
|
||||
|
||||
import { ButtonVariation } from '@components/Button/Button';
|
||||
import { Button } from '@components/index';
|
||||
|
||||
import { Category } from '../../../database/types';
|
||||
import {
|
||||
createCategory,
|
||||
isCategoryNameDuplicate,
|
||||
updateCategory,
|
||||
} from '../../../database/queries/CategoryQueries';
|
||||
import { useTheme } from '@hooks/useTheme';
|
||||
|
||||
import { getString } from '@strings/translations';
|
||||
import { showToast } from '@hooks/showToast';
|
||||
|
||||
interface AddCategoryModalProps {
|
||||
isEditMode?: boolean;
|
||||
category?: Category;
|
||||
visible: boolean;
|
||||
closeModal: () => void;
|
||||
onSuccess: () => Promise<void>;
|
||||
}
|
||||
|
||||
const AddCategoryModal: React.FC<AddCategoryModalProps> = ({
|
||||
isEditMode,
|
||||
category,
|
||||
closeModal,
|
||||
visible,
|
||||
onSuccess,
|
||||
}) => {
|
||||
const theme = useTheme();
|
||||
const [categoryName, setCategoryName] = useState(category?.name || '');
|
||||
|
||||
return (
|
||||
<Portal>
|
||||
<Modal
|
||||
visible={visible}
|
||||
onDismiss={closeModal}
|
||||
contentContainerStyle={[
|
||||
styles.modalContainer,
|
||||
{ backgroundColor: overlay(2, theme.surface) },
|
||||
]}
|
||||
>
|
||||
<Text style={[styles.modalTitle, { color: theme.textColorPrimary }]}>
|
||||
{getString(
|
||||
isEditMode
|
||||
? 'categories.editCategories'
|
||||
: 'categories.addCategories',
|
||||
)}
|
||||
</Text>
|
||||
<TextInput
|
||||
autoFocus
|
||||
defaultValue={categoryName}
|
||||
placeholder={getString('common.name')}
|
||||
onChangeText={setCategoryName}
|
||||
mode="outlined"
|
||||
underlineColor={theme.textColorHint}
|
||||
theme={{ colors: { ...theme } }}
|
||||
/>
|
||||
<View style={styles.btnContainer}>
|
||||
<Button
|
||||
title={getString(isEditMode ? 'common.ok' : 'common.add')}
|
||||
theme={theme}
|
||||
variation={ButtonVariation.CLEAR}
|
||||
onPress={async () => {
|
||||
if (await isCategoryNameDuplicate(categoryName)) {
|
||||
showToast(getString('categories.duplicateError'));
|
||||
} else {
|
||||
if (isEditMode && category) {
|
||||
updateCategory(category?.id, categoryName);
|
||||
} else {
|
||||
createCategory(categoryName);
|
||||
}
|
||||
onSuccess();
|
||||
}
|
||||
setCategoryName('');
|
||||
closeModal();
|
||||
}}
|
||||
/>
|
||||
<Button
|
||||
title={getString('common.cancel')}
|
||||
theme={theme}
|
||||
variation={ButtonVariation.CLEAR}
|
||||
onPress={closeModal}
|
||||
/>
|
||||
</View>
|
||||
</Modal>
|
||||
</Portal>
|
||||
);
|
||||
};
|
||||
|
||||
export default AddCategoryModal;
|
||||
|
||||
const styles = StyleSheet.create({
|
||||
modalContainer: {
|
||||
margin: 30,
|
||||
paddingHorizontal: 24,
|
||||
paddingVertical: 32,
|
||||
borderRadius: 32,
|
||||
},
|
||||
modalTitle: {
|
||||
fontSize: 24,
|
||||
marginBottom: 16,
|
||||
},
|
||||
btnContainer: {
|
||||
marginTop: 24,
|
||||
flexDirection: 'row-reverse',
|
||||
},
|
||||
});
|
||||
@@ -0,0 +1,162 @@
|
||||
import { StyleSheet, Text, View } from 'react-native';
|
||||
import React from 'react';
|
||||
|
||||
import { Category } from '../../../database/types';
|
||||
import { useTheme } from '@hooks/useTheme';
|
||||
import AddCategoryModal from './AddCategoryModal';
|
||||
import useBoolean from '@hooks/useBoolean';
|
||||
import { overlay, Portal } from 'react-native-paper';
|
||||
import IconButton from '@components/IconButtonV2/IconButtonV2';
|
||||
import DeleteCategoryModal from './DeleteCategoryModal';
|
||||
|
||||
interface CategoryCardProps {
|
||||
categoryIndex: number;
|
||||
category: Category;
|
||||
getCategories: () => Promise<void>;
|
||||
updateCategorySort: (
|
||||
categoryId: number,
|
||||
currentIndex: number,
|
||||
newIndex: number,
|
||||
) => void;
|
||||
totalCategories: number;
|
||||
}
|
||||
|
||||
const CategoryCard: React.FC<CategoryCardProps> = ({
|
||||
category,
|
||||
getCategories,
|
||||
categoryIndex,
|
||||
updateCategorySort,
|
||||
totalCategories,
|
||||
}) => {
|
||||
const theme = useTheme();
|
||||
|
||||
const {
|
||||
value: categoryModalVisible,
|
||||
setTrue: showCategoryModal,
|
||||
setFalse: closeCategoryModal,
|
||||
} = useBoolean();
|
||||
|
||||
const {
|
||||
value: deletecategoryModalVisible,
|
||||
setTrue: showDeleteCategoryModal,
|
||||
setFalse: closeDeleteCategoryModal,
|
||||
} = useBoolean();
|
||||
|
||||
return (
|
||||
<>
|
||||
<View
|
||||
style={[
|
||||
styles.cardCtn,
|
||||
{
|
||||
backgroundColor: theme.isDark
|
||||
? overlay(2, theme.surface)
|
||||
: theme.secondaryContainer,
|
||||
},
|
||||
]}
|
||||
>
|
||||
<View style={styles.nameCtn}>
|
||||
<IconButton
|
||||
name="label-outline"
|
||||
color={theme.onSurface}
|
||||
padding={0}
|
||||
theme={theme}
|
||||
/>
|
||||
<Text
|
||||
style={[styles.name, { color: theme.onSurface }]}
|
||||
onPress={showCategoryModal}
|
||||
>
|
||||
{category.name}
|
||||
</Text>
|
||||
</View>
|
||||
<View style={styles.buttonsCtn}>
|
||||
<IconButton
|
||||
name="menu-up"
|
||||
color={theme.onSurface}
|
||||
onPress={() =>
|
||||
updateCategorySort(category.id, categoryIndex, categoryIndex - 1)
|
||||
}
|
||||
theme={theme}
|
||||
disabled={categoryIndex <= 0}
|
||||
/>
|
||||
<IconButton
|
||||
name="menu-down"
|
||||
color={theme.onSurface}
|
||||
style={styles.orderDownBtn}
|
||||
onPress={() =>
|
||||
updateCategorySort(category.id, categoryIndex, categoryIndex + 1)
|
||||
}
|
||||
theme={theme}
|
||||
disabled={categoryIndex + 1 >= totalCategories}
|
||||
/>
|
||||
<View style={styles.flex} />
|
||||
<IconButton
|
||||
name="pencil-outline"
|
||||
color={theme.onSurface}
|
||||
style={styles.manageBtn}
|
||||
onPress={showCategoryModal}
|
||||
theme={theme}
|
||||
/>
|
||||
<IconButton
|
||||
name="delete-outline"
|
||||
color={theme.onSurface}
|
||||
style={styles.manageBtn}
|
||||
onPress={showDeleteCategoryModal}
|
||||
theme={theme}
|
||||
/>
|
||||
</View>
|
||||
</View>
|
||||
<Portal>
|
||||
<AddCategoryModal
|
||||
isEditMode
|
||||
category={category}
|
||||
visible={categoryModalVisible}
|
||||
closeModal={closeCategoryModal}
|
||||
onSuccess={getCategories}
|
||||
/>
|
||||
<DeleteCategoryModal
|
||||
category={category}
|
||||
visible={deletecategoryModalVisible}
|
||||
closeModal={closeDeleteCategoryModal}
|
||||
onSuccess={getCategories}
|
||||
/>
|
||||
</Portal>
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
export default CategoryCard;
|
||||
|
||||
const styles = StyleSheet.create({
|
||||
cardCtn: {
|
||||
paddingVertical: 8,
|
||||
paddingHorizontal: 8,
|
||||
marginHorizontal: 16,
|
||||
marginBottom: 8,
|
||||
borderRadius: 12,
|
||||
elevation: 1,
|
||||
},
|
||||
name: {
|
||||
marginHorizontal: 16,
|
||||
},
|
||||
flex: {
|
||||
flex: 1,
|
||||
},
|
||||
manageBtn: {
|
||||
marginLeft: 16,
|
||||
},
|
||||
orderDownBtn: {
|
||||
marginLeft: 8,
|
||||
},
|
||||
nameCtn: {
|
||||
flex: 1,
|
||||
marginLeft: 8,
|
||||
paddingRight: 16,
|
||||
flexDirection: 'row',
|
||||
alignItems: 'center',
|
||||
paddingVertical: 4,
|
||||
},
|
||||
buttonsCtn: {
|
||||
flexDirection: 'row',
|
||||
alignItems: 'center',
|
||||
},
|
||||
});
|
||||
@@ -0,0 +1,90 @@
|
||||
import React from 'react';
|
||||
import { StyleSheet, Text, View } from 'react-native';
|
||||
import { Modal, Portal } from 'react-native-paper';
|
||||
|
||||
import { ButtonVariation } from '@components/Button/Button';
|
||||
import { Button } from '@components/index';
|
||||
|
||||
import { Category } from '../../../database/types';
|
||||
import { deleteCategoryById } from '../../../database/queries/CategoryQueries';
|
||||
import { useTheme } from '@hooks/useTheme';
|
||||
|
||||
import { getString } from '@strings/translations';
|
||||
import { resetCategoryIdsToDefault } from '../../../database/queries/NovelQueriesV2';
|
||||
import { getDialogBackground } from '../../../theme/colors';
|
||||
|
||||
interface DeleteCategoryModalProps {
|
||||
category: Category;
|
||||
visible: boolean;
|
||||
closeModal: () => void;
|
||||
onSuccess: () => Promise<void>;
|
||||
}
|
||||
|
||||
const DeleteCategoryModal: React.FC<DeleteCategoryModalProps> = ({
|
||||
category,
|
||||
closeModal,
|
||||
visible,
|
||||
onSuccess,
|
||||
}) => {
|
||||
const theme = useTheme();
|
||||
|
||||
return (
|
||||
<Portal>
|
||||
<Modal
|
||||
visible={visible}
|
||||
onDismiss={closeModal}
|
||||
contentContainerStyle={[
|
||||
styles.modalContainer,
|
||||
{ backgroundColor: getDialogBackground(theme) },
|
||||
]}
|
||||
>
|
||||
<Text style={[styles.modalTitle, { color: theme.textColorPrimary }]}>
|
||||
{getString('categories.deleteModal.header')}
|
||||
</Text>
|
||||
<Text style={[styles.modalDesc, { color: theme.textColorSecondary }]}>
|
||||
{getString('categories.deleteModal.desc')}
|
||||
{` "${category.name}"?`}
|
||||
</Text>
|
||||
<View style={styles.btnContainer}>
|
||||
<Button
|
||||
title={getString('common.ok')}
|
||||
theme={theme}
|
||||
variation={ButtonVariation.CLEAR}
|
||||
onPress={() => {
|
||||
deleteCategoryById(category.id);
|
||||
resetCategoryIdsToDefault(category.id);
|
||||
closeModal();
|
||||
onSuccess();
|
||||
}}
|
||||
/>
|
||||
<Button
|
||||
title={getString('common.cancel')}
|
||||
theme={theme}
|
||||
variation={ButtonVariation.CLEAR}
|
||||
onPress={closeModal}
|
||||
/>
|
||||
</View>
|
||||
</Modal>
|
||||
</Portal>
|
||||
);
|
||||
};
|
||||
|
||||
export default DeleteCategoryModal;
|
||||
|
||||
const styles = StyleSheet.create({
|
||||
modalContainer: {
|
||||
margin: 30,
|
||||
paddingHorizontal: 24,
|
||||
paddingVertical: 32,
|
||||
borderRadius: 32,
|
||||
},
|
||||
modalTitle: {
|
||||
fontSize: 24,
|
||||
marginBottom: 16,
|
||||
},
|
||||
btnContainer: {
|
||||
marginTop: 24,
|
||||
flexDirection: 'row-reverse',
|
||||
},
|
||||
modalDesc: {},
|
||||
});
|
||||
@@ -6,7 +6,8 @@ import { EmptyView, SearchbarV2 } from '@components/index';
|
||||
import GlobalSearchResultsList from './components/GlobalSearchResultsList';
|
||||
|
||||
import useSearch from '@hooks/useSearch';
|
||||
import { useTheme } from '@redux/hooks';
|
||||
import { useTheme } from '@hooks/useTheme';
|
||||
|
||||
import { getString } from '@strings/translations';
|
||||
import { useGlobalSearch } from './hooks/useGlobalSearch';
|
||||
|
||||
@@ -43,7 +44,7 @@ const GlobalSearchScreen = (props: Props) => {
|
||||
theme={theme}
|
||||
/>
|
||||
{progress ? (
|
||||
<ProgressBar color={theme.colorAccent} progress={progress} />
|
||||
<ProgressBar color={theme.primary} progress={progress} />
|
||||
) : null}
|
||||
<GlobalSearchResultsList
|
||||
searchResults={searchResults}
|
||||
|
||||
@@ -1,43 +1,63 @@
|
||||
import { Dimensions, Pressable, StyleSheet, Text, View } from 'react-native';
|
||||
import React, { useMemo } from 'react';
|
||||
|
||||
import color from 'color';
|
||||
import FastImage from 'react-native-fast-image';
|
||||
|
||||
import { coverPlaceholderColor } from '../../../theme/colors';
|
||||
|
||||
import { SourceNovelItem } from '../../../sources/types';
|
||||
import { ThemeType } from '../../../theme/types';
|
||||
import { MD3ThemeType } from '../../../theme/types';
|
||||
import { getString } from '@strings/translations';
|
||||
|
||||
interface Props {
|
||||
novel: SourceNovelItem;
|
||||
navigateToNovel: (novel: SourceNovelItem) => void;
|
||||
theme: ThemeType;
|
||||
theme: MD3ThemeType;
|
||||
onLongPress?: () => void;
|
||||
inLibrary?: boolean;
|
||||
}
|
||||
|
||||
const GlobalSearchNovelItem: React.FC<Props> = ({
|
||||
novel,
|
||||
navigateToNovel,
|
||||
theme,
|
||||
onLongPress,
|
||||
inLibrary,
|
||||
}) => {
|
||||
const novelItemDimensions = useMemo(
|
||||
() => ({
|
||||
width: Dimensions.get('window').width / 3 - 16,
|
||||
height: ((Dimensions.get('window').width / 3 - 16) * 4) / 3,
|
||||
opacity: inLibrary ? 0.5 : 1,
|
||||
}),
|
||||
[],
|
||||
[inLibrary],
|
||||
);
|
||||
|
||||
return (
|
||||
<View style={styles.novelItem}>
|
||||
<Pressable
|
||||
style={styles.pressable}
|
||||
android_ripple={{ color: theme.rippleColor }}
|
||||
android_ripple={{ color: color(theme.primary).alpha(0.12).string() }}
|
||||
onPress={() => navigateToNovel(novel)}
|
||||
onLongPress={onLongPress}
|
||||
>
|
||||
<FastImage
|
||||
source={{ uri: novel.novelCover }}
|
||||
style={[styles.novelCover, { ...novelItemDimensions }]}
|
||||
/>
|
||||
{inLibrary ? (
|
||||
<Text
|
||||
style={[
|
||||
styles.inLibraryBadge,
|
||||
{
|
||||
backgroundColor: theme.primary,
|
||||
color: theme.onPrimary,
|
||||
},
|
||||
]}
|
||||
>
|
||||
{getString('novelScreen.inLibaray')}
|
||||
</Text>
|
||||
) : null}
|
||||
<Text
|
||||
style={[
|
||||
{ color: theme.textColorPrimary, width: novelItemDimensions.width },
|
||||
@@ -75,4 +95,13 @@ const styles = StyleSheet.create({
|
||||
borderRadius: 4,
|
||||
backgroundColor: coverPlaceholderColor,
|
||||
},
|
||||
inLibraryBadge: {
|
||||
position: 'absolute',
|
||||
top: 8,
|
||||
left: 8,
|
||||
fontSize: 12,
|
||||
paddingVertical: 2,
|
||||
paddingHorizontal: 4,
|
||||
borderRadius: 4,
|
||||
},
|
||||
});
|
||||
|
||||
@@ -7,15 +7,20 @@ import {
|
||||
View,
|
||||
} from 'react-native';
|
||||
import React, { useCallback, useMemo } from 'react';
|
||||
import color from 'color';
|
||||
|
||||
import { StackNavigationProp } from '@react-navigation/stack';
|
||||
import { useNavigation } from '@react-navigation/native';
|
||||
import MaterialCommunityIcons from 'react-native-vector-icons/MaterialCommunityIcons';
|
||||
|
||||
import { getString } from '@strings/translations';
|
||||
import { useTheme } from '@redux/hooks';
|
||||
import { useTheme } from '@hooks/useTheme';
|
||||
|
||||
import { GlobalSearchResult } from '../hooks/useGlobalSearch';
|
||||
import GlobalSearchNovelItem from './GlobalSearchNovelItem';
|
||||
import { useLibraryNovels } from '@screens/library/hooks/useLibrary';
|
||||
import { insertNovelInLibrary } from '../../../database/queries/NovelQueriesV2';
|
||||
import { LibraryNovelInfo } from '../../../database/types';
|
||||
|
||||
interface GlobalSearchResultsListProps {
|
||||
searchResults: GlobalSearchResult[];
|
||||
@@ -29,11 +34,14 @@ const GlobalSearchResultsList: React.FC<GlobalSearchResultsListProps> = ({
|
||||
const theme = useTheme();
|
||||
const navigation = useNavigation<StackNavigationProp<any>>();
|
||||
const keyExtractor = useCallback(item => item.source.sourceId.toString(), []);
|
||||
const { library, setLibrary } = useLibraryNovels();
|
||||
|
||||
const errorColor = useMemo(
|
||||
() => (theme.statusBar === 'dark-content' ? '#B3261E' : '#F2B8B5'),
|
||||
[],
|
||||
);
|
||||
const novelInLibrary = (sourceId: number, novelUrl: string) =>
|
||||
library?.some(
|
||||
novel => novel.novelUrl === novelUrl && novel.sourceId === sourceId,
|
||||
);
|
||||
|
||||
const errorColor = useMemo(() => (theme.isDark ? '#B3261E' : '#F2B8B5'), []);
|
||||
|
||||
const navigateToNovel = useCallback(
|
||||
item => navigation.push('Novel', item),
|
||||
@@ -49,7 +57,9 @@ const GlobalSearchResultsList: React.FC<GlobalSearchResultsListProps> = ({
|
||||
<>
|
||||
<View>
|
||||
<Pressable
|
||||
android_ripple={{ color: theme.rippleColor }}
|
||||
android_ripple={{
|
||||
color: color(theme.primary).alpha(0.12).string(),
|
||||
}}
|
||||
style={styles.sourceHeader}
|
||||
onPress={() =>
|
||||
navigation.navigate('SourceScreen', {
|
||||
@@ -78,10 +88,7 @@ const GlobalSearchResultsList: React.FC<GlobalSearchResultsListProps> = ({
|
||||
/>
|
||||
</Pressable>
|
||||
{item.isLoading ? (
|
||||
<ActivityIndicator
|
||||
color={theme.colorAccent}
|
||||
style={styles.loading}
|
||||
/>
|
||||
<ActivityIndicator color={theme.primary} style={styles.loading} />
|
||||
) : item.error ? (
|
||||
<Text style={[styles.error, { color: errorColor }]}>
|
||||
{item.error}
|
||||
@@ -104,13 +111,45 @@ const GlobalSearchResultsList: React.FC<GlobalSearchResultsListProps> = ({
|
||||
{getString('sourceScreen.noResultsFound')}
|
||||
</Text>
|
||||
}
|
||||
renderItem={({ item: novelItem }) => (
|
||||
<GlobalSearchNovelItem
|
||||
novel={novelItem}
|
||||
navigateToNovel={navigateToNovel}
|
||||
theme={theme}
|
||||
/>
|
||||
)}
|
||||
renderItem={({ item: novelItem }) => {
|
||||
const inLibrary = novelInLibrary(
|
||||
novelItem.sourceId,
|
||||
novelItem.novelUrl,
|
||||
);
|
||||
|
||||
return (
|
||||
<GlobalSearchNovelItem
|
||||
novel={novelItem}
|
||||
inLibrary={inLibrary}
|
||||
navigateToNovel={navigateToNovel}
|
||||
theme={theme}
|
||||
onLongPress={() => {
|
||||
setLibrary(prevValues => {
|
||||
if (inLibrary) {
|
||||
return [
|
||||
...prevValues.filter(
|
||||
novel => novel.novelUrl !== novelItem.novelUrl,
|
||||
),
|
||||
];
|
||||
} else {
|
||||
return [
|
||||
...prevValues,
|
||||
{
|
||||
novelUrl: novelItem.novelUrl,
|
||||
sourceId: novelItem.sourceId,
|
||||
} as LibraryNovelInfo,
|
||||
];
|
||||
}
|
||||
});
|
||||
insertNovelInLibrary(
|
||||
novelItem.sourceId,
|
||||
novelItem.novelUrl,
|
||||
inLibrary,
|
||||
);
|
||||
}}
|
||||
/>
|
||||
);
|
||||
}}
|
||||
/>
|
||||
)}
|
||||
</View>
|
||||
|
||||
@@ -12,8 +12,8 @@ import {
|
||||
useAppDispatch,
|
||||
useBrowseSettings,
|
||||
useSourcesReducer,
|
||||
useTheme,
|
||||
} from '../../redux/hooks';
|
||||
import { useTheme } from '@hooks/useTheme';
|
||||
import { useNavigation } from '@react-navigation/native';
|
||||
import { useSearch } from '../../hooks';
|
||||
import SourceCard from './components/SourceCard/SourceCard';
|
||||
@@ -40,6 +40,7 @@ const BrowseScreen = () => {
|
||||
|
||||
const {
|
||||
allSources,
|
||||
searchResults,
|
||||
pinnedSourceIds = [],
|
||||
languageFilters = ['English'],
|
||||
lastUsed,
|
||||
@@ -93,6 +94,44 @@ const BrowseScreen = () => {
|
||||
[],
|
||||
);
|
||||
|
||||
const sections = useMemo(() => {
|
||||
const list = [
|
||||
{
|
||||
header: getString('browseScreen.lastUsed'),
|
||||
data: lastUsedSource,
|
||||
},
|
||||
];
|
||||
|
||||
if (pinnedSourceIds) {
|
||||
list.push({
|
||||
header: getString('browseScreen.pinned'),
|
||||
data: pinnedSources,
|
||||
});
|
||||
}
|
||||
|
||||
if (!onlyShowPinnedSources) {
|
||||
if (searchText) {
|
||||
list.unshift({
|
||||
header: getString('common.searchResults'),
|
||||
data: searchResults,
|
||||
});
|
||||
} else {
|
||||
list.push({
|
||||
header: getString('browseScreen.all'),
|
||||
data: allSources,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
return list;
|
||||
}, [
|
||||
lastUsedSource,
|
||||
pinnedSourceIds,
|
||||
onlyShowPinnedSources,
|
||||
JSON.stringify(searchResults),
|
||||
searchText,
|
||||
]);
|
||||
|
||||
return (
|
||||
<>
|
||||
<SearchbarV2
|
||||
@@ -113,17 +152,7 @@ const BrowseScreen = () => {
|
||||
) : allSources.length === 0 ? null : (
|
||||
<>
|
||||
<SectionList
|
||||
sections={[
|
||||
{
|
||||
header: getString('browseScreen.lastUsed'),
|
||||
data: lastUsedSource,
|
||||
},
|
||||
{ header: getString('browseScreen.pinned'), data: pinnedSources },
|
||||
{
|
||||
header: getString('browseScreen.all'),
|
||||
data: onlyShowPinnedSources ? [] : allSources,
|
||||
},
|
||||
]}
|
||||
sections={sections}
|
||||
ListHeaderComponent={
|
||||
showMyAnimeList ? (
|
||||
<>
|
||||
|
||||
@@ -6,8 +6,8 @@ import {
|
||||
useAppDispatch,
|
||||
useBrowseSettings,
|
||||
useSourcesReducer,
|
||||
useTheme,
|
||||
} from '../../redux/hooks';
|
||||
import { useTheme } from '@hooks/useTheme';
|
||||
import { useNavigation } from '@react-navigation/native';
|
||||
import { getString } from '../../../strings/translations';
|
||||
import { languages } from '../../utils/constants/languages';
|
||||
|
||||
@@ -1,14 +1,15 @@
|
||||
import React from 'react';
|
||||
import { StyleSheet, View, FlatList, Text } from 'react-native';
|
||||
import { useLibrary, useTheme } from '../../hooks/reduxHooks';
|
||||
import { useTheme } from '@hooks/useTheme';
|
||||
|
||||
import { Appbar } from '../../components/Appbar';
|
||||
import ListView from '../../components/ListView';
|
||||
import { useLibraryNovels } from '@screens/library/hooks/useLibrary';
|
||||
import { Appbar } from '@components';
|
||||
|
||||
const SourceNovels = ({ navigation, route }) => {
|
||||
const sourceId = route.params;
|
||||
const theme = useTheme();
|
||||
const library = useLibrary();
|
||||
const { library } = useLibraryNovels();
|
||||
|
||||
const sourceNovels = library.filter(novel => novel.sourceId === sourceId);
|
||||
|
||||
@@ -29,7 +30,11 @@ const SourceNovels = ({ navigation, route }) => {
|
||||
<View
|
||||
style={[styles.container, { backgroundColor: theme.colorPrimaryDark }]}
|
||||
>
|
||||
<Appbar title="Select Novel" onBackAction={() => navigation.goBack()} />
|
||||
<Appbar
|
||||
title="Select Novel"
|
||||
handleGoBack={navigation.goBack}
|
||||
theme={theme}
|
||||
/>
|
||||
<FlatList
|
||||
data={sourceNovels}
|
||||
keyExtractor={item => item.novelId.toString()}
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user