Compare commits

...

48 Commits

Author SHA1 Message Date
Rajarshee Chatterjee d749225820 chore: Update translations (#343)
* New translations strings.json (Russian)
[ci skip]

* New translations strings.json (Japanese)
[ci skip]

* New translations strings.json (Hindi)
[ci skip]

* New translations strings.json (Vietnamese)
[ci skip]

* New translations strings.json (Chinese Traditional)
[ci skip]

* New translations strings.json (Turkish)
[ci skip]

* New translations strings.json (Swedish)
[ci skip]

* New translations strings.json (Serbian (Cyrillic))
[ci skip]

* New translations strings.json (Albanian)
[ci skip]

* New translations strings.json (Portuguese)
[ci skip]

* New translations strings.json (Polish)
[ci skip]

* New translations strings.json (Norwegian)
[ci skip]

* New translations strings.json (Dutch)
[ci skip]

* New translations strings.json (Korean)
[ci skip]

* New translations strings.json (Italian)
[ci skip]

* New translations strings.json (Ukrainian)
[ci skip]

* New translations strings.json (Hungarian)
[ci skip]

* New translations strings.json (Hebrew)
[ci skip]

* New translations strings.json (Finnish)
[ci skip]

* New translations strings.json (Greek)
[ci skip]

* New translations strings.json (German)
[ci skip]

* New translations strings.json (Danish)
[ci skip]

* New translations strings.json (Czech)
[ci skip]

* New translations strings.json (Catalan)
[ci skip]

* New translations strings.json (Arabic)
[ci skip]

* New translations strings.json (Afrikaans)
[ci skip]

* New translations strings.json (Spanish)
[ci skip]

* New translations strings.json (French)
[ci skip]

* New translations strings.json (Romanian)
[ci skip]

* New translations strings.json (Portuguese, Brazilian)
[ci skip]

* New translations strings.json (Indonesian)
[ci skip]

* New translations strings.json (Russian)
[ci skip]

* New translations strings.json (Portuguese, Brazilian)
[ci skip]

* New translations strings.json (Turkish)
[ci skip]

* New translations strings.json (Portuguese)
[ci skip]

* New translations strings.json (Portuguese, Brazilian)
[ci skip]

* New translations strings.json (Russian)
[ci skip]

* New translations strings.json (Japanese)
[ci skip]

* New translations strings.json (Hindi)
[ci skip]

* New translations strings.json (Vietnamese)
[ci skip]

* New translations strings.json (Chinese Traditional)
[ci skip]

* New translations strings.json (Turkish)
[ci skip]

* New translations strings.json (Swedish)
[ci skip]

* New translations strings.json (Serbian (Cyrillic))
[ci skip]

* New translations strings.json (Albanian)
[ci skip]

* New translations strings.json (Portuguese)
[ci skip]

* New translations strings.json (Polish)
[ci skip]

* New translations strings.json (Norwegian)
[ci skip]

* New translations strings.json (Dutch)
[ci skip]

* New translations strings.json (Korean)
[ci skip]

* New translations strings.json (Italian)
[ci skip]

* New translations strings.json (Ukrainian)
[ci skip]

* New translations strings.json (Hungarian)
[ci skip]

* New translations strings.json (Hebrew)
[ci skip]

* New translations strings.json (Finnish)
[ci skip]

* New translations strings.json (Greek)
[ci skip]

* New translations strings.json (German)
[ci skip]

* New translations strings.json (Danish)
[ci skip]

* New translations strings.json (Czech)
[ci skip]

* New translations strings.json (Catalan)
[ci skip]

* New translations strings.json (Arabic)
[ci skip]

* New translations strings.json (Afrikaans)
[ci skip]

* New translations strings.json (Spanish)
[ci skip]

* New translations strings.json (French)
[ci skip]

* New translations strings.json (Romanian)
[ci skip]

* New translations strings.json (Portuguese, Brazilian)
[ci skip]

* New translations strings.json (Indonesian)
[ci skip]

* New translations strings.json (Russian)
[ci skip]

* New translations strings.json (Portuguese, Brazilian)
[ci skip]

* New translations strings.json (Turkish)
[ci skip]

* New translations strings.json (Russian)
[ci skip]

* New translations strings.json (Japanese)
[ci skip]

* New translations strings.json (Hindi)
[ci skip]

* New translations strings.json (Vietnamese)
[ci skip]

* New translations strings.json (Chinese Traditional)
[ci skip]

* New translations strings.json (Turkish)
[ci skip]

* New translations strings.json (Swedish)
[ci skip]

* New translations strings.json (Serbian (Cyrillic))
[ci skip]

* New translations strings.json (Albanian)
[ci skip]

* New translations strings.json (Portuguese)
[ci skip]

* New translations strings.json (Polish)
[ci skip]

* New translations strings.json (Norwegian)
[ci skip]

* New translations strings.json (Dutch)
[ci skip]

* New translations strings.json (Korean)
[ci skip]

* New translations strings.json (Italian)
[ci skip]

* New translations strings.json (Ukrainian)
[ci skip]

* New translations strings.json (Hungarian)
[ci skip]

* New translations strings.json (Hebrew)
[ci skip]

* New translations strings.json (Finnish)
[ci skip]

* New translations strings.json (Greek)
[ci skip]

* New translations strings.json (German)
[ci skip]

* New translations strings.json (Danish)
[ci skip]

* New translations strings.json (Czech)
[ci skip]

* New translations strings.json (Catalan)
[ci skip]

* New translations strings.json (Arabic)
[ci skip]

* New translations strings.json (Afrikaans)
[ci skip]

* New translations strings.json (Spanish)
[ci skip]

* New translations strings.json (French)
[ci skip]

* New translations strings.json (Romanian)
[ci skip]

* New translations strings.json (Portuguese, Brazilian)
[ci skip]

* New translations strings.json (Indonesian)
[ci skip]

* New translations strings.json (Portuguese, Brazilian)
[ci skip]

* New translations strings.json (Turkish)
[ci skip]

* New translations strings.json (Portuguese, Brazilian)
[ci skip]

* New translations strings.json (Russian)
[ci skip]

* New translations strings.json (Japanese)
[ci skip]

* New translations strings.json (Hindi)
[ci skip]

* New translations strings.json (Vietnamese)
[ci skip]

* New translations strings.json (Chinese Traditional)
[ci skip]

* New translations strings.json (Turkish)
[ci skip]

* New translations strings.json (Swedish)
[ci skip]

* New translations strings.json (Serbian (Cyrillic))
[ci skip]

* New translations strings.json (Albanian)
[ci skip]

* New translations strings.json (Portuguese)
[ci skip]

* New translations strings.json (Polish)
[ci skip]

* New translations strings.json (Norwegian)
[ci skip]

* New translations strings.json (Dutch)
[ci skip]

* New translations strings.json (Korean)
[ci skip]

* New translations strings.json (Italian)
[ci skip]

* New translations strings.json (Ukrainian)
[ci skip]

* New translations strings.json (Hungarian)
[ci skip]

* New translations strings.json (Hebrew)
[ci skip]

* New translations strings.json (Finnish)
[ci skip]

* New translations strings.json (Greek)
[ci skip]

* New translations strings.json (German)
[ci skip]

* New translations strings.json (Danish)
[ci skip]

* New translations strings.json (Czech)
[ci skip]

* New translations strings.json (Catalan)
[ci skip]

* New translations strings.json (Arabic)
[ci skip]

* New translations strings.json (Afrikaans)
[ci skip]

* New translations strings.json (Spanish)
[ci skip]

* New translations strings.json (French)
[ci skip]

* New translations strings.json (Romanian)
[ci skip]

* New translations strings.json (Portuguese, Brazilian)
[ci skip]

* New translations strings.json (Indonesian)
[ci skip]

* New translations strings.json (Portuguese, Brazilian)
[ci skip]

* New translations strings.json (Turkish)
[ci skip]

* New translations strings.json (Russian)
[ci skip]
2022-09-02 09:30:32 +05:30
Rajarshee Chatterjee d6d487101b feat(source): Add Madara Sources
Closes LNReader/lnreader-sources#365
Closes LNReader/lnreader-sources#363
2022-08-28 11:49:05 +05:30
Rajarshee Chatterjee ba62c1ba0a Minor Fixes 2022-08-28 11:37:04 +05:30
Rajarshee Chatterjee 75f4f18448 Minor Fixes 2022-08-28 11:05:25 +05:30
Rajarshee Chatterjee f9ebec4a30 fix: Global Search Navigation (Closes #406) 2022-08-28 10:56:05 +05:30
Rajarshee Chatterjee de18e3d9d8 fix: Category Fixes 2022-08-28 10:56:05 +05:30
Rajarshee Chatterjee acaac74db4 fix: Add Chapter Name Back in Reader (Closes #410) 2022-08-28 10:56:05 +05:30
Rajarshee Chatterjee d8845661a7 fix: Long Category Names (Closes #407) 2022-08-28 10:56:05 +05:30
Rider21 b66a6130c5 feat(source): Add Ficbook (#409)
* update

* fix

* oops
2022-08-25 07:15:34 +05:30
Rhino 220f25bf72 feat: Update chapter tab sort (#402)
* Added following sources Zetro Translation, Nocturne, Asada Translation and Sugar Babies

* added time stamp to backup file

* tweaks updated chapter sort order in Updates Tab
2022-08-25 07:12:45 +05:30
Rajarshee Chatterjee 934edbb84c feat: Add Change Default Category (Closes #383) 2022-08-18 21:16:35 +05:30
Rajarshee Chatterjee 0d8a5d6172 fix: Navbar Color (Closes #386) 2022-08-18 21:13:36 +05:30
Rajarshee Chatterjee e706745f30 feat: Select Current Category in Library (Closes #396) 2022-08-18 09:43:05 +05:30
Rajarshee Chatterjee dce3652ae8 feat: Add Select All in Library (Closes #380) 2022-08-18 09:07:12 +05:30
Rajarshee Chatterjee 0ca3b8ce15 fix: Cleanup
Closes #378
Closes #393
2022-08-18 08:40:49 +05:30
Rajarshee Chatterjee c62db5cb23 chore: Update Source Icon Links 2022-08-18 07:44:55 +05:30
Rajarshee Chatterjee e33b433e84 fix: Change status bar style on background color change (Closes #150) 2022-08-17 08:18:12 +05:30
Rhino c95f376e20 feat(source): Add following sources Zetro, Nocturne, Asada and SugarBB (#389)
* Added following sources Zetro Translation, Nocturne, Asada Translation and Sugar Babies

* added time stamp to backup file
2022-08-16 23:20:00 +05:30
Rhino 9b45375638 feat: Add time stamp to backup file (#390) 2022-08-16 23:17:03 +05:30
Rajarshee Chatterjee 7fbdc6732c fix: Reset Category to default on removing from current category (Closes #379) 2022-08-16 13:27:54 +05:30
Rajarshee Chatterjee 3f9549d320 fix: Path Aliases 2022-08-16 13:03:29 +05:30
Rajarshee Chatterjee 079010b6ca chore: Fix Issue Template Label 2022-08-16 13:00:34 +05:30
Rajarshee Chatterjee 107e534565 Cleanup 2022-08-16 12:38:13 +05:30
Rajarshee Chatterjee f0665866ae feat: Custom Order in Categories (Closes #376) 2022-08-16 10:56:08 +05:30
Rajarshee Chatterjee 706b4581b1 fix: Hide "Removed from library" toast if novel is in library 2022-08-16 07:26:06 +05:30
Rajarshee Chatterjee a8d9c3abfa chore: Add Issue Templates and Moderator 2022-08-15 20:27:19 +05:30
Rajarshee Chatterjee 66798c46db Minor fixes
Closes #373
Closes #374
2022-08-15 16:51:21 +05:30
Rajarshee Chatterjee 87fa081cfd feat: Material You (#377)
* feat: Material You

* feat: Material You
2022-08-15 15:36:06 +05:30
Rajarshee Chatterjee ea26cd67e4 fix(source): Add Status in Ranobes (Closes LNReader/lnreader-sources#359) 2022-08-14 15:44:09 +05:30
Rajarshee Chatterjee 9400fcd252 feat(source): Add NeoSekai Translations and MTL-Novel
Closes LNReader/lnreader-sources#306
Closes LNReader/lnreader-sources#328
2022-08-14 15:43:22 +05:30
Rajarshee Chatterjee 30ca05df06 feat: Add novel in library on long press in search results (Closes #151) 2022-08-14 13:00:21 +05:30
Rajarshee Chatterjee eac3ee32c8 fix: Add Empty Summary placeholder and show Full Novel Title (Closes #355) 2022-08-14 11:43:49 +05:30
Rajarshee Chatterjee 0cf249eec5 fix: Use Separate States for Search Results in Browse Screen (Closes #359) 2022-08-14 11:15:56 +05:30
Rajarshee Chatterjee 27e5300f4f fix: Clear Searchbar when Screen is Unfocused (Closes #358) 2022-08-14 11:13:10 +05:30
Rajarshee Chatterjee 97e02177a4 fix: Global Search from Novel and MAL Screen (Closes #356) 2022-08-14 10:23:04 +05:30
Rajarshee Chatterjee 42b37f87fe fix(source): Remove NovelSpace and Move ArNovel to MadaraGenerator
(Closes LNReader/lnreader-sources#353)
2022-08-14 10:06:03 +05:30
Rajarshee Chatterjee 4ad1c0253b fix: Reset Novel Categories on deleting a Category (Closes #370) 2022-08-14 09:52:00 +05:30
Rajarshee Chatterjee a57ed7a7a2 feat: Add category-wise updates 2022-08-14 09:30:16 +05:30
Rajarshee Chatterjee 9fa00d9d9b fix(source): Update WuxiaWorldCo baseUrl 2022-08-14 09:28:33 +05:30
Rajarshee Chatterjee 18f63a217f fix: Category/Library bugs 2022-08-14 07:44:53 +05:30
Rajarshee Chatterjee 05401ada6b fix: Fix deleting downloaded chapters (Closes #341) 2022-08-14 07:02:25 +05:30
Rajarshee Chatterjee e256537079 feat: Add Categories (#366)
* feat: Add Categories (Closes #45)

* fix: ESLint Errors

* feat: Add Validation for Duplicate Category Name

* feat: Multiple Categories Support & Hide Default Category

* fix: Creating Tables on Clean Installation

* feat: Add Started Filter
2022-08-13 23:07:58 +05:30
Rider21 0eec38f26f feat: Source Updates (#350)
- Fix ranobes.net (Closes LNReader/lnreader-sources#70)
- Add ranobes.com source
2022-08-05 21:22:24 +05:30
Rider21 fa90d57eff refactor: Replace moment with Dayjs (#346)
* dayjs

* locales added

* fix
2022-08-02 13:17:45 +05:30
Rajarshee Chatterjee fd6a907c5f Release v1.1.12 2022-07-30 21:36:45 +05:30
Rajarshee Chatterjee c7561fc3dd fix: New Update Dialog Enhancements 2022-07-30 21:33:26 +05:30
Rajarshee Chatterjee e1401ba39f fix(source): Fix Riwyat Chapter Text 2022-07-30 21:32:20 +05:30
Rajarshee Chatterjee cbb3c7cfa2 Release v1.1.11 2022-07-30 09:53:23 +05:30
252 changed files with 8922 additions and 4087 deletions
+94
View File
@@ -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
+31
View File
@@ -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."
}
]
+2 -2
View File
@@ -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 (
+1 -1
View File
@@ -135,7 +135,7 @@ android {
minSdkVersion rootProject.ext.minSdkVersion
targetSdkVersion rootProject.ext.targetSdkVersion
versionCode 1
versionName "1.1.10"
versionName "1.1.12"
}
splits {
abi {
+1
View File
@@ -6,6 +6,7 @@ buildscript {
minSdkVersion = 21
compileSdkVersion = 30
targetSdkVersion = 30
ndkVersion = "21.1.6352462"
}
repositories {
google()
+5 -2
View File
@@ -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'],
},
],
],
+398 -169
View File
File diff suppressed because it is too large Load Diff
+9 -4
View File
@@ -1,6 +1,6 @@
{
"name": "lnreader",
"version": "1.1.10",
"version": "1.1.12",
"private": true,
"scripts": {
"android": "react-native run-android",
@@ -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",
@@ -51,8 +53,10 @@
"react-native-easing-gradient": "^1.0.0",
"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",
@@ -73,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",
+14
View File
@@ -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,
-47
View File
@@ -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,
},
});
+88
View File
@@ -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',
},
});
-30
View File
@@ -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,
};
+19 -5
View File
@@ -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>
);
+36 -18
View File
@@ -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,
},
});
+24 -6
View File
@@ -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: {
+8 -5
View File
@@ -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,
},
});
+2 -2
View File
@@ -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}>
+3 -2
View File
@@ -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 }) => {
+8 -3
View File
@@ -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
View File
@@ -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,
},
});
+11 -9
View File
@@ -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}
+6 -2
View File
@@ -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;
-75
View File
@@ -1,75 +0,0 @@
import React, { useState } from 'react';
import { StyleSheet, View, Text } from 'react-native';
import { Portal, Modal, Button } from 'react-native-paper';
import * as Linking from 'expo-linking';
import { useTheme } from '../hooks/reduxHooks';
const NewUpdateDialog = ({ newVersion }) => {
const [newUpdateDialog, showNewUpdateDialog] = useState(true);
const theme = useTheme();
return (
<Portal>
<Modal
visible={newUpdateDialog}
onDismiss={() => showNewUpdateDialog(false)}
contentContainerStyle={[
styles.containerStyle,
{ backgroundColor: theme.colorPrimaryDark },
]}
>
<Text style={[styles.modalHeader, { color: theme.textColorPrimary }]}>
{`New update available ${newVersion.tag_name}`}
</Text>
<Text style={{ color: theme.textColorSecondary, fontSize: 16 }}>
{newVersion.body}
</Text>
<View
style={{
flexDirection: 'row',
marginTop: 30,
justifyContent: 'flex-end',
}}
>
<Button
labelStyle={{
color: theme.colorAccent,
letterSpacing: 0,
textTransform: 'none',
}}
theme={{ colors: { primary: theme.colorAccent } }}
onPress={() => showNewUpdateDialog(false)}
>
Cancel
</Button>
<Button
labelStyle={{
color: theme.colorAccent,
letterSpacing: 0,
textTransform: 'none',
}}
theme={{ colors: { primary: theme.colorAccent } }}
onPress={() => Linking.openURL(newVersion.downloadUrl)}
>
Install
</Button>
</View>
</Modal>
</Portal>
);
};
export default NewUpdateDialog;
const styles = StyleSheet.create({
containerStyle: {
padding: 20,
margin: 20,
borderRadius: 6,
},
modalHeader: {
fontSize: 20,
marginBottom: 10,
},
});
+98
View File
@@ -0,0 +1,98 @@
import React, { useState } from 'react';
import { StyleSheet, View, Text, Dimensions } from 'react-native';
import { Portal, Modal } from 'react-native-paper';
import * as Linking from 'expo-linking';
import { ScrollView } from 'react-native-gesture-handler';
import Button, { ButtonVariation } from './Button/Button';
import { getString } from '@strings/translations';
import Markdown from 'react-native-markdown-display';
import { dividerColor, getDialogBackground } from '@theme/colors';
import { useTheme } from '@hooks/useTheme';
interface NewUpdateDialogProps {
newVersion: {
tag_name: string;
body: string;
downloadUrl: string;
};
}
const NewUpdateDialog: React.FC<NewUpdateDialogProps> = ({ newVersion }) => {
const [newUpdateDialog, showNewUpdateDialog] = useState(true);
const theme = useTheme();
const modalHeight = Dimensions.get('window').height / 2;
return (
<Portal>
<Modal
visible={newUpdateDialog}
onDismiss={() => showNewUpdateDialog(false)}
contentContainerStyle={[
styles.containerStyle,
{ backgroundColor: getDialogBackground(theme) },
]}
>
<Text style={[styles.modalHeader, { color: theme.textColorPrimary }]}>
{`${getString('common.newUpdateAvailable')} ${newVersion.tag_name}`}
</Text>
<ScrollView style={{ height: modalHeight }}>
<Markdown
style={{
body: {
color: theme.textColorSecondary,
lineHeight: 20,
paddingVertical: 8,
},
hr: {
backgroundColor: dividerColor(theme.isDark),
marginVertical: 16,
},
code_inline: {
backgroundColor: getDialogBackground(theme),
},
}}
>
{newVersion.body}
</Markdown>
</ScrollView>
<View style={styles.buttonCtn}>
<Button
title={getString('common.cancel')}
theme={theme}
variation={ButtonVariation.CLEAR}
onPress={() => showNewUpdateDialog(false)}
/>
<Button
title={getString('common.install')}
theme={theme}
variation={ButtonVariation.CLEAR}
onPress={() => Linking.openURL(newVersion.downloadUrl)}
/>
</View>
</Modal>
</Portal>
);
};
export default NewUpdateDialog;
const styles = StyleSheet.create({
containerStyle: {
padding: 20,
margin: 20,
borderRadius: 6,
},
modalHeader: {
fontWeight: 'bold',
fontSize: 20,
marginBottom: 16,
},
body: {},
buttonCtn: {
flexDirection: 'row',
marginTop: 16,
justifyContent: 'flex-end',
},
});
+38 -26
View File
@@ -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,
-61
View File
@@ -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,
},
});
+67
View File
@@ -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,
},
});
-40
View File
@@ -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,
},
});
+1 -1
View File
@@ -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
@@ -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,
},
});
-97
View File
@@ -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,
},
});
+14 -10
View File
@@ -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',
+4 -8
View File
@@ -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>
+5 -4
View File
@@ -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>
+11 -9
View File
@@ -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
+2 -1
View File
@@ -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
View File
@@ -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,
);
};
+97
View File
@@ -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,
);
});
});
+61 -37
View File
@@ -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(
-95
View File
@@ -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);
},
);
});
});
};
+96
View File
@@ -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,
);
}),
);
};
+5 -4
View File
@@ -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,
),
),
);
+205
View File
@@ -0,0 +1,205 @@
import { noop, xor } from 'lodash';
import * as SQLite from 'expo-sqlite';
const db = SQLite.openDatabase('lnreader.db');
import { txnErrorCallback } from '../utils/helpers';
import { LibraryNovelInfo } from '../types';
import { fetchNovel } from '../../services/Source/source';
import { showToast } from '@hooks/showToast';
import { getString } from '@strings/translations';
import { checkNovelInCache } from './NovelQueries';
export const getCategoryNovelsFromDb = async (
categoryId: number,
onlyOngoingNovels?: boolean,
): Promise<LibraryNovelInfo[]> => {
let query = `
SELECT
*
FROM
novels
WHERE (
categoryIds LIKE '[${categoryId}]'
OR
categoryIds LIKE '[${categoryId},%'
OR
categoryIds LIKE '%,${categoryId}]'
OR
categoryIds LIKE '%,${categoryId},%'
)
AND
followed = 1
`;
if (onlyOngoingNovels) {
query += ' AND status NOT LIKE "Completed"';
}
return new Promise(resolve => {
db.transaction(tx => {
tx.executeSql(
query,
undefined,
(txObj, { rows }) => resolve((rows as any)._array),
txnErrorCallback,
);
});
});
};
export const resetCategoryIdsToDefault = async (deletedCategoryId: number) => {
const categoryNovels = await getCategoryNovelsFromDb(deletedCategoryId);
db.transaction(tx => {
categoryNovels.forEach(novel => {
let categoryIds = xor(JSON.parse(novel.categoryIds), [deletedCategoryId]);
categoryIds = categoryIds.length ? categoryIds : [1];
tx.executeSql('UPDATE novels SET categoryIds = ? WHERE novelId = ?', [
JSON.stringify(categoryIds),
novel.novelId,
]);
});
});
};
export const insertNovelInLibrary = async (
sourceId: number,
novelUrl: string,
inLibrary: boolean,
defaultCategoryId: number,
) => {
if (inLibrary) {
showToast(getString('browseScreen.removeFromLibrary'));
db.transaction(tx => {
tx.executeSql(
`
UPDATE novels SET
followed = 0
WHERE
sourceId = ?
AND
novelUrl = ?
`,
[sourceId, novelUrl],
noop,
txnErrorCallback,
);
});
} else {
showToast(getString('browseScreen.addedToLibrary'));
const novelSavedInDb = await checkNovelInCache(novelUrl);
if (novelSavedInDb) {
db.transaction(tx => {
tx.executeSql(
`
UPDATE novels SET
followed = 1
WHERE
sourceId = ?
AND
novelUrl = ?
`,
[sourceId, novelUrl],
noop,
txnErrorCallback,
);
});
return;
}
const novel = await fetchNovel(sourceId, novelUrl);
db.transaction(tx => {
tx.executeSql(
`
INSERT INTO novels
(
novelUrl,
sourceUrl,
sourceId,
source,
novelName,
novelCover,
novelSummary,
author,
artist,
status,
genre,
followed,
categoryIds
)
VALUES
(?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, 1, ?)`,
[
novel.novelUrl,
novel.sourceUrl,
novel.sourceId,
novel.source,
novel.novelName,
novel.novelCover,
novel.novelSummary,
novel.author,
novel.artist,
novel.status,
novel.genre,
JSON.stringify([defaultCategoryId]),
],
(txObj, { insertId }) => {
novel.chapters?.map(chapter => {
tx.executeSql(
`
INSERT INTO chapters
(chapterUrl, chapterName, releaseDate, novelId)
VALUES
(?, ?, ?, ?)`,
[
chapter.chapterUrl,
chapter.chapterName,
chapter.releaseDate,
insertId,
],
);
});
},
txnErrorCallback,
);
});
}
};
export const updateNovelCategoryById = async (
novelId: number,
categoryIds: number[],
) => {
db.transaction(tx => {
tx.executeSql(
'UPDATE novels SET categoryIds = ? WHERE novelId = ?',
[JSON.stringify(categoryIds.length ? categoryIds : [1]), novelId],
noop,
txnErrorCallback,
);
});
};
export const updateNovelCategoryByIds = async (
novelIds: number[],
categoryIds: number[],
) => {
db.transaction(tx => {
novelIds.map(novelId =>
tx.executeSql(
'UPDATE novels SET categoryIds = ? WHERE novelId = ?',
[JSON.stringify(categoryIds.length ? categoryIds : [1]), novelId],
noop,
txnErrorCallback,
),
);
});
};
+1 -1
View File
@@ -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[]> => {
+17
View File
@@ -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';
+5 -1
View File
@@ -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]"';
+8
View File
@@ -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;
}
+17
View File
@@ -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
View File
@@ -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';
-22
View File
@@ -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,
};
+10
View File
@@ -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]);
}
+35
View File
@@ -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;
-18
View File
@@ -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;
+7 -3
View File
@@ -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;
-10
View File
@@ -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 };
};
-34
View File
@@ -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
View File
@@ -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 };
};
+28
View File
@@ -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,
};
};
+35
View File
@@ -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);
+18 -35
View File
@@ -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
View File
@@ -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} />
+4
View File
@@ -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>
);
+23 -13
View File
@@ -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,
-37
View File
@@ -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 -12
View File
@@ -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);
-61
View File
@@ -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,
// });
// }
// };
-36
View File
@@ -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;
-5
View File
@@ -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';
-108
View File
@@ -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;
+33 -26
View File
@@ -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 -44
View File
@@ -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 -91
View File
@@ -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,
-5
View File
@@ -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';
+4
View File
@@ -1,3 +1,7 @@
/**
* @deprecated
*/
import { createSlice, PayloadAction } from '@reduxjs/toolkit';
import { BrowseSettingsMap } from './types';
-63
View File
@@ -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;
-5
View File
@@ -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';
+9 -11
View File
@@ -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;
-8
View File
@@ -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);
+5 -2
View File
@@ -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';
+145
View File
@@ -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>
+41 -12
View File
@@ -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 ? (
<>

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