Compare commits

..

74 Commits

Author SHA1 Message Date
Rajarshee Chatterjee 9b3d65dded refactor: Reorganize reader sheet 2021-08-11 09:53:18 +05:30
Rajarshee Chatterjee aa3a5c2d0c Minor fixes 2021-08-10 14:32:25 +05:30
Rajarshee Chatterjee 2aab5621a6 feat: Select novels in library 2021-08-10 13:34:19 +05:30
Rajarshee Chatterjee b4b7e6770d feat(source): Add Divine Dao Library
Closes LNReader/lnreader-sources#54
2021-08-09 12:44:32 +05:30
Rajarshee Chatterjee 0ed65d64e4 fix: novelCover crash in list view 2021-08-09 07:26:04 +05:30
Rajarshee Chatterjee a7aaa7d230 feat: Custom CSS 2021-08-07 12:08:31 +05:30
Rajarshee Chatterjee e358b3753f feat(source): Add ranobehub 2021-08-07 08:34:08 +05:30
Rajarshee Chatterjee 780a67e1a6 Update README.md 2021-08-06 10:50:00 +05:30
Rajarshee Chatterjee 5b57e78486 Update README.md 2021-08-06 10:37:22 +05:30
Rajarshee Chatterjee 6430c11cae Update README.md 2021-08-06 10:31:09 +05:30
Rajarshee Chatterjee 322bfc224d Full backups (Closes #34) 2021-08-05 12:24:54 +05:30
Rajarshee Chatterjee a14d35f031 Minor fixes 2021-08-05 12:23:59 +05:30
Rajarshee Chatterjee f03b1703e2 fix(source): ArNovel (Closes #61)
Closes LNReader/lnreader-sources#52
2021-08-04 09:20:35 +05:30
Rajarshee Chatterjee 40d2052f16 Custom covers and open links in reader 2021-08-03 14:38:54 +05:30
Rajarshee Chatterjee 01814e92d0 Fix empty novelCover crash 2021-08-03 10:52:13 +05:30
Rajarshee Chatterjee dffc9c3b99 fix(source): LightNovelPub 2021-08-03 10:48:12 +05:30
Rajarshee Chatterjee 8bb3ffe82c fix(source): ComradeMao 2021-08-03 10:40:34 +05:30
Rajarshee Chatterjee c746081dee Minor fixes 2021-08-03 09:44:14 +05:30
Rajarshee Chatterjee d25fe85a64 Fix discord link 2021-08-02 16:52:32 +05:30
Rajarshee Chatterjee 92d9d53da7 Fix no novelCover crash 2021-07-31 13:32:00 +05:30
Rajarshee Chatterjee 17fcd96f16 Source fixes 2021-07-30 09:15:15 +05:30
Rajarshee Chatterjee 31bada1c1a Refactor 2021-07-29 20:49:21 +05:30
Rajarshee Chatterjee 605bd6a514 Add edit info modal 2021-07-29 11:17:13 +05:30
Rajarshee Chatterjee 01bac76bed Minor fixes 2021-07-29 09:35:55 +05:30
Rajarshee Chatterjee b5ed62d9f8 Fix global search and splashscreen 2021-07-28 17:47:23 +05:30
Rajarshee Chatterjee fd21cc6e85 Minor fixes 2021-07-28 12:14:38 +05:30
Rajarshee Chatterjee 40f1495cf1 Add webview option to support tables and original text style 2021-07-28 11:46:57 +05:30
Rajarshee Chatterjee 983b069dba Add Readwn.com source 2021-07-28 09:47:12 +05:30
Rajarshee Chatterjee e21101ca28 Fix build 2021-07-26 17:01:18 +05:30
Bread Genie d381e878bc docs(readme): Add badges (#60) 2021-07-26 16:59:43 +05:30
Rajarshee Chatterjee bcf5632668 Remove history from redux state and minor fixes 2021-07-26 08:24:59 +05:30
Bread Genie ac8bca7f03 feat(sources): Add ReaperScans Novels (#59) 2021-07-25 21:38:33 +05:30
Bread Genie 62a5e27b1e fix: Indonesia -> Indonesian (#58) 2021-07-25 19:19:08 +05:30
Rajarshee Chatterjee 7c5a9a407f Add new sources 2021-07-23 09:04:47 +05:30
Rajarshee Chatterjee d8d231cb90 Add source WLNUpdates 2021-07-22 09:08:40 +05:30
Rajarshee Chatterjee a5f90eaf8b Add new sources 2021-07-21 20:32:30 +05:30
Rajarshee Chatterjee ec50189ea4 Minor fixes 2021-07-21 08:34:45 +05:30
Rajarshee Chatterjee ccdccdb6bb Remove prevChapter and nextChapter 2021-07-20 08:38:55 +05:30
Bread Genie 059f440713 fix(source): nextChapter link undefined (#55) 2021-07-20 07:55:48 +05:30
Rajarshee Chatterjee b695f57ef4 Refactor 2021-07-19 09:32:48 +05:30
Rajarshee Chatterjee db86b707e7 Fix bookmark icon 2021-07-19 06:00:51 +05:30
Rajarshee Chatterjee 7db4e4b872 Minor fixes 2021-07-18 18:21:25 +05:30
Rajarshee Chatterjee a2c6e9fe58 Custom checkbox 2021-07-18 10:31:07 +05:30
Rajarshee Chatterjee 6c27993454 Remove [hr]() chapter content 2021-07-18 10:30:21 +05:30
Rajarshee Chatterjee 63de0a3794 Fix long chapter names (Closes #53) 2021-07-17 20:24:32 +05:30
Marcin Majewski 93fbcf1246 fix(syosetu): change oneshot chapter name (#52)
Changed oneshot chapter names to "Oneshot" to avoid too long chapters names destroying UI.
2021-07-17 19:55:24 +05:30
Rajarshee Chatterjee 50357f7d6f Release v1.1.2 2021-07-17 08:12:04 +05:30
Rajarshee Chatterjee 1a8d640db1 Add delete downloads dialog and refactor themes 2021-07-17 07:13:00 +05:30
Rajarshee Chatterjee dc6b2deddd Minor fixes 2021-07-16 22:05:49 +05:30
Rajarshee Chatterjee a9fcf2fe08 Fix NovelCover 2021-07-16 17:46:04 +05:30
Rajarshee Chatterjee f195b9e6df Minor fixes 2021-07-16 15:50:01 +05:30
Rajarshee Chatterjee d07fd26cb3 Refactor 2021-07-16 15:22:36 +05:30
Rajarshee Chatterjee 5aee0d1b04 Refactor 2021-07-15 22:51:50 +05:30
Rajarshee Chatterjee c4457d74c4 Parse links in chapter 2021-07-15 08:52:04 +05:30
Rajarshee Chatterjee ba90a6fb84 Refactor sources and show headings 2021-07-14 12:27:07 +05:30
Bread Genie e7424cea06 Fix: source images (#50)
One of the source image was linked to a fork and rest were linked to the old extension repo (this would still work, but for consistency's sake every image is linked to the repo under the org).
2021-07-14 10:19:16 +05:30
Rajarshee Chatterjee e274feda6e Minor fixes 2021-07-13 17:07:55 +05:30
Bread Genie 8ec2378191 feat(source): Add Yushubo (#48) 2021-07-13 17:05:42 +05:30
Rajarshee Chatterjee ef57a98681 Minor fixes 2021-07-12 09:49:05 +05:30
Rajarshee Chatterjee bdda959f36 Improve novel updates 2021-07-11 16:14:17 +05:30
Rajarshee Chatterjee 04f3ba2b45 Refactor themes 2021-07-11 09:43:12 +05:30
Rajarshee Chatterjee f1a261ab28 Add ranobes.net 2021-07-10 10:52:26 +05:30
Rajarshee Chatterjee da2ee3b883 Pause/Resume downloads in queue 2021-07-09 17:58:19 +05:30
Marcin Majewski b98d3861b4 format: Setting Prettier formating in package.json (#47)
Because of different formatting on some computers
Prettier settings were set in package.json file so that everyone
will format their code in the same way using Prettier
2021-07-09 15:13:56 +05:30
Rajarshee Chatterjee ff59330d20 Add Novel Updates 2021-07-09 14:24:26 +05:30
Rajarshee Chatterjee b0639e79a8 Add new sources 2021-07-09 08:51:08 +05:30
Rajarshee Chatterjee 5931a0dc07 Cleanup 2021-07-08 11:22:45 +05:30
Rajarshee Chatterjee cf40acb725 Create multisrc 2021-07-07 14:56:01 +05:30
Rajarshee Chatterjee a2974ccb44 Merge 2021-07-06 20:11:45 +05:30
Rajarshee Chatterjee 5eda558f09 Add LNMTL 2021-07-06 20:07:45 +05:30
Marcin Majewski cf7f1402d3 Fixed Syosetu name and Oneshot not working (#46)
* Fixed Syosetu name and Oneshot not working

Oneshots didn't work at all because of use of scaner from lnreader-sources
Oneshots now load properly desciptions (it didn't work before)
Syosetsu changed into Syosetu as it should be

* Remove unnecessary console.logs
2021-07-06 20:06:36 +05:30
Rajarshee Chatterjee 834a8d9e2e Minor fix 2021-07-06 06:16:28 +05:30
Rajarshee Chatterjee fe5b3f4a5b Add Syosetu 2021-07-06 06:11:30 +05:30
Rajarshee Chatterjee ec4eaf4d49 Remove mipmap-anydpi-v26 folder 2021-07-05 16:15:20 +05:30
190 changed files with 28331 additions and 13717 deletions
-4
View File
@@ -1,4 +0,0 @@
{
"12bb71342c6255bbf50437ec8f4441c083f47cdb74bd89160c15e4f43e52a1cb": true,
"40b842e832070c58deac6aa9e08fa459302ee3f9da492c7e77d93d2fbf4a56fd": true
}
Binary file not shown.

After

Width:  |  Height:  |  Size: 9.0 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.3 MiB

After

Width:  |  Height:  |  Size: 1.1 MiB

+3 -1
View File
@@ -6,16 +6,18 @@ import { Provider as PaperProvider } from "react-native-paper";
import { Provider } from "react-redux";
import { persistor, store } from "./src/redux/store";
import { PersistGate } from "redux-persist/lib/integration/react";
import * as SplashScreen from "expo-splash-screen";
import Main from "./src/navigators/Main";
import { createDB, deleteDb } from "./src/database/DBHelper";
import { createDB } from "./src/database/DBHelper";
import { fonts } from "./src/theme/fonts";
const App = () => {
const [loaded] = useFonts(fonts);
useEffect(() => {
SplashScreen.preventAutoHideAsync();
createDB();
}, []);
+33 -12
View File
@@ -1,20 +1,41 @@
<h1 align="center">
<br>
LNReader
<br>
</h1>
<p align="center">LNReader is a Tachiyomi-like, free and open source light novel reader for Android.</p>
---
## Screenshots:
<p align="center">
<img src="./.github/readme-images/screenshots.png" align="center" height="350" />
<img src="./.github/readme-images/icon_alt.png" align="center" width="128" />
<p>
<h1 align="center">
LNReader
</h1>
<p align="center">
LNReader is a Tachiyomi-like, free and open source light novel reader for Android.
</p>
<div align="center">
<a href="https://github.com/LNReader/lnreader/actions/workflows/build.yml">
<img alt="GitHub Workflow Status" src="https://img.shields.io/github/workflow/status/LNReader/lnreader/CI">
</a>
<a href="https://discord.gg/QdcWN4MD63">
<img alt="Discord Chat" src="https://img.shields.io/discord/835746409357246465.svg?logo=discord&logoColor=white&logoWidth=20&labelColor=7289DA&label=Discord&color=17cf48">
</a>
<a href="https://github.com/LNReader/lnreader/releases/latest">
<img alt="GitHub release (latest by date)" src="https://img.shields.io/github/v/release/LNReader/lnreader">
</a>
<a href="https://github.com/LNReader/lnreader/blob/main/LICENSE">
<img alt="GitHub" src="https://img.shields.io/github/license/LNReader/lnreader">
</a>
</div>
<br>
<br>
<p align="center">
<img src="./.github/readme-images/screenshots.png" align="center" />
</p>
## Download
Get the app from our [releases page](https://github.com/rajarsheechatterjee/LNReader/releases).
## Sources
LNReader does not have any affiliation with the content providers available.
Source requests should be created at [LNReader/lnreader-sources](https://github.com/LNReader/lnreader-sources).
## License
[MIT ](https://github.com/rajarsheechatterjee/lnreader/blob/main/LICENSE)
+1 -1
View File
@@ -136,7 +136,7 @@ android {
minSdkVersion rootProject.ext.minSdkVersion
targetSdkVersion rootProject.ext.targetSdkVersion
versionCode 1
versionName "1.1.1"
versionName "1.1.2"
}
splits {
abi {
+1 -1
View File
@@ -11,7 +11,7 @@
<uses-permission android:name="android.permission.WAKE_LOCK"/>
<uses-permission android:name="android.permission.ACCESS_NETWORK_STATE"/>
<uses-permission android:name="com.google.android.c2dm.permission.RECEIVE"/>
<application android:name=".MainApplication" android:label="@string/app_name" android:icon="@mipmap/ic_launcher" android:roundIcon="@mipmap/ic_launcher_round" android:allowBackup="true" android:theme="@style/AppTheme">
<application android:name=".MainApplication" android:label="@string/app_name" android:icon="@mipmap/ic_launcher" android:roundIcon="@mipmap/ic_launcher_round" android:allowBackup="true" android:theme="@style/AppTheme" android:usesCleartextTraffic="true">
<meta-data android:name="expo.modules.updates.EXPO_UPDATE_URL" android:value="https://exp.host/@rajarsheechatterjee/LNReader"/>
<meta-data android:name="expo.modules.updates.EXPO_SDK_VERSION" android:value="41.0.0"/>
<meta-data android:name="expo.modules.updates.ENABLED" android:value="false"/>
Binary file not shown.
Binary file not shown.
@@ -1,62 +0,0 @@
// FullScreenModule.java
package com.rajarsheechatterjee.LNReader;
import android.os.Build;
import android.view.View;
import android.view.WindowManager;
import android.app.Activity;
import com.facebook.react.bridge.NativeModule;
import com.facebook.react.bridge.ReactApplicationContext;
import com.facebook.react.bridge.ReactContext;
import com.facebook.react.bridge.UiThreadUtil;
import com.facebook.react.bridge.ReactContextBaseJavaModule;
import com.facebook.react.bridge.ReactMethod;
public class FullScreenModule extends ReactContextBaseJavaModule {
@Override
public String getName() {
return "FullScreen";
}
@ReactMethod
public void enable() {
UiThreadUtil.runOnUiThread(
new Runnable() {
@Override
public void run() {
getCurrentActivity().getWindow().getDecorView().setSystemUiVisibility(
View.SYSTEM_UI_FLAG_LAYOUT_FULLSCREEN |
View.SYSTEM_UI_FLAG_LAYOUT_HIDE_NAVIGATION |
View.SYSTEM_UI_FLAG_FULLSCREEN |
View.SYSTEM_UI_FLAG_HIDE_NAVIGATION |
View.SYSTEM_UI_FLAG_IMMERSIVE_STICKY
);
}
}
);
}
@ReactMethod
public void disable() {
UiThreadUtil.runOnUiThread(
new Runnable() {
@Override
public void run() {
getCurrentActivity().getWindow().getDecorView().setSystemUiVisibility(
View.SYSTEM_UI_FLAG_LAYOUT_FULLSCREEN |
View.SYSTEM_UI_FLAG_LAYOUT_HIDE_NAVIGATION |
View.SYSTEM_UI_FLAG_LAYOUT_STABLE
);
}
}
);
}
FullScreenModule(ReactApplicationContext reactContext) {
super(reactContext);
}
}
@@ -1,29 +0,0 @@
package com.rajarsheechatterjee.LNReader;
import com.facebook.react.ReactPackage;
import com.facebook.react.bridge.NativeModule;
import com.facebook.react.bridge.ReactApplicationContext;
import com.facebook.react.uimanager.ViewManager;
import java.util.ArrayList;
import java.util.Collections;
import java.util.List;
public class FullScreenPackage implements ReactPackage {
@Override
public List<ViewManager> createViewManagers(ReactApplicationContext reactContext) {
return Collections.emptyList();
}
@Override
public List<NativeModule> createNativeModules(
ReactApplicationContext reactContext) {
List<NativeModule> modules = new ArrayList<>();
modules.add(new FullScreenModule(reactContext));
return modules;
}
}
@@ -31,9 +31,6 @@ import java.util.Arrays;
import java.util.List;
import javax.annotation.Nullable;
import com.rajarsheechatterjee.LNReader.FullScreenPackage;
public class MainApplication extends Application implements ReactApplication {
private final ReactModuleRegistryProvider mModuleRegistryProvider = new ReactModuleRegistryProvider(
new BasePackageList().getPackageList()
@@ -49,7 +46,6 @@ public class MainApplication extends Application implements ReactApplication {
protected List<ReactPackage> getPackages() {
List<ReactPackage> packages = new PackageList(this).getPackages();
packages.add(new ModuleRegistryAdapter(mModuleRegistryProvider));
packages.add(new FullScreenPackage());
return packages;
}
@@ -8,7 +8,6 @@ public class BasePackageList {
public List<Package> getPackageList() {
return Arrays.<Package>asList(
new expo.modules.application.ApplicationPackage(),
new expo.modules.backgroundfetch.BackgroundFetchPackage(),
new expo.modules.constants.ConstantsPackage(),
new expo.modules.documentpicker.DocumentPickerPackage(),
new expo.modules.errorrecovery.ErrorRecoveryPackage(),
@@ -19,10 +18,8 @@ public class BasePackageList {
new expo.modules.lineargradient.LinearGradientPackage(),
new expo.modules.notifications.NotificationsPackage(),
new expo.modules.permissions.PermissionsPackage(),
new expo.modules.sharing.SharingPackage(),
new expo.modules.splashscreen.SplashScreenPackage(),
new expo.modules.sqlite.SQLitePackage(),
new expo.modules.taskManager.TaskManagerPackage(),
new expo.modules.updates.UpdatesPackage(),
new expo.modules.webbrowser.WebBrowserPackage()
);
Vendored Regular → Executable
View File
-35
View File
@@ -3,44 +3,9 @@
"name": "LNReader",
"slug": "LNReader",
"scheme": "lnreader",
"version": "1.1.1",
"orientation": "default",
"icon": "./assets/icon.png",
"splash": {
"image": "./assets/splash.png",
"resizeMode": "cover",
"backgroundColor": "#000000"
},
"updates": {
"fallbackToCacheTimeout": 0,
"enabled": false
},
"notification": {
"icon": "./assets/notification.png",
"color": "#00adb5"
},
"assetBundlePatterns": ["**/*"],
"ios": {
"supportsTablet": true
},
"android": {
"adaptiveIcon": {
"foregroundImage": "./assets/icon.png",
"backgroundColor": "#FFFFFF"
},
"package": "com.rajarsheechatterjee.LNReader"
},
"androidStatusBar": {
"barStyle": "light-content",
"backgroundColor": "#00000000",
"translucent": true
},
"androidNavigationBar": {
"barStyle": "light-content",
"backgroundColor": "#000000"
},
"web": {
"favicon": "./assets/favicon.png"
}
}
}
Binary file not shown.
Binary file not shown.
+19712 -8700
View File
File diff suppressed because it is too large Load Diff
+11 -11
View File
@@ -15,19 +15,14 @@
"@react-navigation/native": "^5.8.10",
"@react-navigation/stack": "^5.12.8",
"expo": "^41.0.1",
"expo-background-fetch": "~9.1.0",
"expo-document-picker": "^9.1.2",
"expo-file-system": "~11.0.2",
"expo-font": "~9.1.0",
"expo-linear-gradient": "~9.1.0",
"expo-linking": "~2.2.3",
"expo-notifications": "~0.11.6",
"expo-permissions": "^12.0.1",
"expo-sharing": "~9.1.2",
"expo-splash-screen": "~0.10.2",
"expo-splash-screen": "^0.11.2",
"expo-sqlite": "~9.1.0",
"expo-status-bar": "~1.0.4",
"expo-task-manager": "~9.1.0",
"expo-updates": "~0.5.4",
"expo-web-browser": "~9.1.0",
"moment": "^2.29.1",
@@ -37,15 +32,12 @@
"react-native": "^0.64.2",
"react-native-background-actions": "^2.6.1",
"react-native-cheerio": "^1.0.0-rc.4",
"react-native-collapsible-header-views": "^1.1.2",
"react-native-device-info": "^8.1.3",
"react-native-easing-gradient": "^0.1.2",
"react-native-fast-image": "^8.3.4",
"react-native-fit-image": "^1.5.5",
"react-native-gesture-handler": "~1.10.2",
"react-native-immersive-mode": "^2.0.0",
"react-native-navigation-bar-color": "^2.0.1",
"react-native-offline": "^5.7.0",
"react-native-pager-view": "^5.1.10",
"react-native-paper": "^4.5.0",
"react-native-reanimated": "^2.1.0",
@@ -59,7 +51,6 @@
"react-native-webview": "^11.6.2",
"react-redux": "^7.2.2",
"redux": "^4.0.5",
"redux-devtools-extension": "^2.13.9",
"redux-persist": "^6.0.0",
"redux-thunk": "^2.3.0",
"rn-sliding-up-panel": "^2.4.4"
@@ -67,7 +58,16 @@
"devDependencies": {
"@babel/core": "~7.9.0"
},
"prettier": {
"tabWidth": 4,
"semi": true,
"singleQuote": false,
"quoteProps": "as-needed",
"trailingComma": "es5",
"bracketSpacing": true,
"arrowParens": "always"
},
"private": true,
"name": "lnreader-eject",
"name": "lnreader",
"version": "1.0.0"
}
+12 -48
View File
@@ -1,52 +1,8 @@
import React from "react";
import { StyleSheet } from "react-native";
import { Dimensions, StyleSheet, View } from "react-native";
import { IconButton } from "react-native-paper";
const actions = [
{
icon: "download-outline",
onPress: () => {
dispatch(
downloadAllChaptersAction(
novel.sourceId,
novel.novelUrl,
selected
)
);
setSelected([]);
},
},
{
icon: "trash-can-outline",
onPress: () => {
dispatch(deleteAllChaptersAction(selected));
setSelected([]);
},
},
{
icon: "bookmark-outline",
onPress: () => {
dispatch(bookmarkChapterAction(selected));
setSelected([]);
},
},
{
icon: "check",
onPress: () => {
dispatch(markChaptersRead(selected, novel.novelId, sort, filter));
setSelected([]);
},
},
{
icon: "check-outline",
onPress: () => {
dispatch(markChapterUnreadAction(selected, novel.novelId));
setSelected([]);
},
},
];
export const Actionbar = ({ actions, theme }) => {
return (
<View
@@ -55,9 +11,17 @@ export const Actionbar = ({ actions, theme }) => {
{ backgroundColor: theme.colorPrimary },
]}
>
{actions.map((action) => (
<IconButton icon={action.icon} onPress={action.onPress} />
))}
{actions.map(
(action, index) =>
action.icon && (
<IconButton
key={index}
icon={action.icon}
color={theme.textColorPrimary}
onPress={action.onPress}
/>
)
)}
</View>
);
};
+5 -123
View File
@@ -1,17 +1,11 @@
import React, { useRef } from "react";
import { View, TextInput, StyleSheet } from "react-native";
import {
TouchableRipple,
IconButton,
Appbar as MaterialAppbar,
} from "react-native-paper";
import Constants from "expo-constants";
import React from "react";
import { useNavigation } from "@react-navigation/native";
import { useSelector } from "react-redux";
import { Appbar as MaterialAppbar } from "react-native-paper";
import { useTheme } from "../hooks/reduxHooks";
export const Appbar = ({ title, onBackAction, style, children }) => {
const theme = useSelector((state) => state.settingsReducer.theme);
const theme = useTheme();
return (
<MaterialAppbar.Header
@@ -28,115 +22,3 @@ export const Appbar = ({ title, onBackAction, style, children }) => {
</MaterialAppbar.Header>
);
};
export const SearchAppbar = ({
placeholder,
getSearchResults,
setSearchText,
searchText,
getNovels,
setLoading,
screen,
openWebView,
}) => {
const searchRef = useRef(null);
const navigation = useNavigation();
const theme = useSelector((state) => state.settingsReducer.theme);
return (
<TouchableRipple
borderless
onPress={() => searchRef.current.focus()}
style={[
styles.searchAppbarContainer,
{ backgroundColor: theme.searchBarColor },
]}
>
<View
style={{
flex: 1,
flexDirection: "row",
justifyContent: "space-between",
}}
>
<View style={{ flex: 1, flexDirection: "row" }}>
{screen === "Library" ? (
<IconButton
icon="magnify"
color={theme.textColorSecondary}
style={{ marginLeft: 0 }}
size={23}
/>
) : (
<IconButton
icon="arrow-left"
color={theme.textColorSecondary}
style={{ marginLeft: 0 }}
size={23}
onPress={() => navigation.goBack()}
/>
)}
<TextInput
ref={searchRef}
style={{
fontSize: 16,
color: theme.textColorSecondary,
}}
placeholder={placeholder}
placeholderTextColor={theme.textColorSecondary}
onChangeText={(text) => {
setSearchText(text);
if (screen === "Library") {
getSearchResults(text);
}
}}
onSubmitEditing={() => {
if (screen === "Extension") {
getSearchResults(searchText);
}
}}
defaultValue={searchText}
/>
</View>
{searchText !== "" && (
<IconButton
icon="close"
color={theme.textColorSecondary}
style={{ marginRight: 0 }}
size={23}
onPress={() => {
setLoading?.(true);
getNovels();
setSearchText("");
}}
/>
)}
{openWebView && (
<IconButton
icon="earth"
color={theme.textColorSecondary}
style={{ marginRight: 0 }}
size={23}
onPress={openWebView}
/>
)}
</View>
</TouchableRipple>
);
};
const styles = StyleSheet.create({
searchAppbarContainer: {
marginTop: Constants.statusBarHeight + 8,
height: 48,
flexDirection: "row",
alignItems: "center",
paddingHorizontal: 16,
marginBottom: 12,
borderRadius: 8,
marginHorizontal: 12,
elevation: 2,
},
});
+4 -4
View File
@@ -4,11 +4,11 @@ import { View } from "react-native";
const BottomSheetHandle = ({ theme }) => (
<View
style={{
backgroundColor: theme.textColorHint,
height: 5,
width: 30,
backgroundColor: "rgba(255,255,255,0.4)",
height: 4,
width: 40,
borderRadius: 50,
top: 10,
top: 8,
alignSelf: "center",
}}
/>
+73
View File
@@ -0,0 +1,73 @@
import React from "react";
import { Pressable, StyleSheet, Text, View } from "react-native";
import MaterialCommunityIcons from "react-native-vector-icons/MaterialCommunityIcons";
export const Checkbox = ({ label, status, onPress, theme }) => (
<View
style={[
styles.checkboxContainer,
status && { backgroundColor: theme.rippleColor },
]}
>
<Pressable
android_ripple={{ color: theme.rippleColor }}
style={styles.pressable}
onPress={onPress}
>
<Text style={{ color: theme.textColorPrimary }}>{label}</Text>
{status && (
<MaterialCommunityIcons
name="check"
color={theme.colorAccent}
size={21}
style={styles.icon}
/>
)}
</Pressable>
</View>
);
export const SortItem = ({ label, status, onPress, theme }) => (
<View
style={[
styles.checkboxContainer,
status && { backgroundColor: theme.rippleColor },
]}
>
<Pressable
android_ripple={{ color: theme.rippleColor }}
style={styles.pressable}
onPress={onPress}
>
<Text style={{ color: theme.textColorPrimary }}>{label}</Text>
{status && (
<MaterialCommunityIcons
name={status === "asc" ? "arrow-up" : "arrow-down"}
color={theme.colorAccent}
size={21}
style={styles.icon}
/>
)}
</Pressable>
</View>
);
const styles = StyleSheet.create({
checkboxContainer: {
borderRadius: 8,
overflow: "hidden",
marginVertical: 4,
},
pressable: {
paddingHorizontal: 16,
paddingVertical: 12,
flexDirection: "row",
justifyContent: "space-between",
},
icon: {
position: "absolute",
right: 16,
alignSelf: "center",
},
});
+1
View File
@@ -10,6 +10,7 @@ const ColorPickerModal = ({
onSubmit,
hideModal,
modalVisible,
showAccentColors,
}) => {
const [text, setText] = useState(color);
const [error, setError] = useState();
+72
View File
@@ -0,0 +1,72 @@
import React from "react";
import { Pressable, View } from "react-native";
import MaterialCommunityIcons from "react-native-vector-icons/MaterialCommunityIcons";
export const ToggleButton = ({ icon, selected, theme, color, onPress }) => (
<View
style={{
borderRadius: 8,
overflow: "hidden",
marginHorizontal: 4,
}}
>
<Pressable
android_ripple={{ color: theme.rippleColor }}
style={{
padding: 10,
alignItems: "center",
justifyContent: "center",
}}
onPress={onPress}
>
<MaterialCommunityIcons
name={icon}
color={
selected
? theme.colorAccent
: color
? color
: theme.textColorPrimary
}
size={24}
/>
</Pressable>
</View>
);
export const ToggleColorButton = ({
selected,
backgroundColor,
textColor,
onPress,
}) => (
<View
style={{
borderRadius: 50,
overflow: "hidden",
marginHorizontal: 6,
height: 44,
width: 44,
}}
>
<Pressable
android_ripple={{ color: textColor }}
style={{
flex: 1,
padding: 10,
backgroundColor: backgroundColor,
alignItems: "center",
justifyContent: "center",
}}
onPress={onPress}
>
{selected && (
<MaterialCommunityIcons
name="check"
color={textColor}
size={24}
/>
)}
</Pressable>
</View>
);
+2 -2
View File
@@ -10,7 +10,7 @@ const EmptyView = ({ icon, description, style, children, iconStyle }) => {
<Text
style={[
styles.emptyViewIcon,
{ color: theme.textColorSecondary },
{ color: theme.textColorHint },
style,
iconStyle,
]}
@@ -20,7 +20,7 @@ const EmptyView = ({ icon, description, style, children, iconStyle }) => {
<Text
style={[
styles.emptyViewText,
{ color: theme.textColorSecondary },
{ color: theme.textColorHint },
style,
]}
>
+1 -1
View File
@@ -17,7 +17,7 @@ const ErrorView = ({ theme, onRetry, error, openWebView }) => (
icon="reload"
size={25}
style={{ margin: 0, marginTop: 24 }}
color={theme.textColorSecondary}
color={theme.text}
onPress={onRetry}
/>
<Text style={{ color: theme.textColorPrimary }}>Retry</Text>
+75
View File
@@ -0,0 +1,75 @@
import React from "react";
import { Pressable, StyleSheet, Text, View } from "react-native";
import { IconButton } from "react-native-paper";
const icons = ["(・o・;)", "Σ(ಠ_ಠ)", "ಥ_ಥ", "(˘・_・˘)", "(; ̄Д ̄)", "(・Д・。"];
export const ErrorView = ({ errorName, actions, theme }) => (
<View style={{ flex: 1, justifyContent: "center", alignItems: "center" }}>
<Text style={[styles.emptyViewIcon, { color: theme.textColorHint }]}>
{/* {icons[Math.floor(Math.random() * 5)]} */}
ಥ_ಥ
</Text>
<Text style={[styles.emptyViewText, { color: theme.textColorHint }]}>
{errorName}
</Text>
<View style={{ flexDirection: "row" }}>
{actions.map((action) => (
<View
style={{
borderRadius: 4,
overflow: "hidden",
margin: 16,
}}
>
<Pressable
android_ripple={{
color: theme.rippleColor,
borderless: false,
}}
onPress={action.onPress}
style={{
justifyContent: "center",
alignItems: "center",
paddingBottom: 8,
paddingHorizontal: 20,
minWidth: 100,
}}
>
<IconButton
icon={action.icon}
color={theme.textColorHint}
size={24}
style={{ margin: 0 }}
/>
<Text
style={{
fontSize: 12,
color: theme.textColorHint,
}}
>
{action.name}
</Text>
</Pressable>
</View>
))}
</View>
</View>
);
const styles = StyleSheet.create({
emptyViewContainer: {
flex: 1,
justifyContent: "center",
alignItems: "center",
},
emptyViewIcon: {
fontSize: 45,
},
emptyViewText: {
fontWeight: "bold",
marginTop: 10,
textAlign: "center",
paddingHorizontal: 30,
},
});
+57 -10
View File
@@ -1,19 +1,19 @@
import React from "react";
import { StyleSheet, View } from "react-native";
import { Pressable, StyleSheet, Text, View } from "react-native";
import { List, Divider as PaperDivider } from "react-native-paper";
import { List as PaperList, Divider as PaperDivider } from "react-native-paper";
import MaterialCommunityIcons from "react-native-vector-icons/MaterialCommunityIcons";
const ListSection = ({ children }) => (
<List.Section style={styles.listSection}>{children}</List.Section>
<PaperList.Section style={styles.listSection}>{children}</PaperList.Section>
);
const ListSubHeader = ({ children, theme }) => (
<List.Subheader
<PaperList.Subheader
style={[styles.listSubHeader, { color: theme.colorAccent }]}
>
{children}
</List.Subheader>
</PaperList.Subheader>
);
const ListItem = ({
@@ -27,7 +27,7 @@ const ListItem = ({
titleStyle,
style,
}) => (
<List.Item
<PaperList.Item
title={title}
style={style}
titleStyle={[{ color: theme.textColorPrimary }, titleStyle]}
@@ -37,7 +37,7 @@ const ListItem = ({
left={() => (
<View style={{ justifyContent: "center" }}>
{icon && (
<List.Icon
<PaperList.Icon
color={theme.colorAccent}
icon={icon}
style={{
@@ -67,13 +67,13 @@ const Divider = ({ theme }) => (
);
const InfoItem = ({ title, icon, theme }) => (
<List.Item
<PaperList.Item
title={title}
titleStyle={{ color: theme.textColorSecondary, fontSize: 14 }}
titleNumberOfLines={5}
left={() =>
icon && (
<List.Icon
<PaperList.Icon
color={theme.textColorSecondary}
icon={icon}
style={{ marginVertical: 0 }}
@@ -83,7 +83,54 @@ const InfoItem = ({ title, icon, theme }) => (
/>
);
export { ListSection, ListSubHeader, ListItem, Divider, InfoItem };
const ColorItem = ({ title, description, theme, onPress }) => (
<Pressable
style={{
padding: 16,
flexDirection: "row",
justifyContent: "space-between",
alignItems: "center",
}}
android_ripple={{ color: theme.rippleColor }}
onPress={onPress}
>
<View>
<Text style={{ color: theme.textColorPrimary, fontSize: 16 }}>
{title}
</Text>
<Text style={{ color: theme.textColorSecondary }}>
{description}
</Text>
</View>
<View
style={{
backgroundColor: description,
height: 24,
width: 24,
borderRadius: 50,
marginRight: 16,
}}
/>
</Pressable>
);
const Icon = ({ icon, theme }) => (
<PaperList.Icon
color={theme.colorAccent}
icon={icon}
style={{ margin: 0 }}
/>
);
export const List = {
Section: ListSection,
SubHeader: ListSubHeader,
Item: ListItem,
Divider,
InfoItem,
ColorItem,
Icon,
};
const styles = StyleSheet.create({
listSection: {
+35 -28
View File
@@ -1,7 +1,7 @@
import React from "react";
import { StyleSheet, View, Text, Image } from "react-native";
import { StyleSheet, View, Text, Pressable } from "react-native";
import FastImage from "react-native-fast-image";
import { TouchableRipple } from "react-native-paper";
const ListView = ({
item,
@@ -10,36 +10,43 @@ const ListView = ({
inLibraryBadge,
theme,
onPress,
isSelected,
onLongPress,
}) => {
return (
<TouchableRipple
borderless
centered
rippleColor={theme.rippleColor}
style={styles.listView}
<Pressable
android_ripple={{ color: theme.rippleColor }}
style={[
styles.listView,
isSelected && { backgroundColor: theme.rippleColor },
]}
onPress={onPress}
onLongPress={onLongPress}
>
<>
<FastImage
source={{ uri: item.novelCover }}
style={styles.extensionIcon}
/>
<Text
style={[
{ color: theme.textColorPrimary },
styles.novelName,
]}
numberOfLines={1}
>
{item.novelName}
</Text>
<View style={styles.badgeContainer}>
{downloadBadge}
{unreadBadge}
{inLibraryBadge}
</View>
</>
</TouchableRipple>
<FastImage
source={{
uri:
item.novelCover && !item.novelCover.startsWith("/")
? item.novelCover
: "https://github.com/LNReader/lnreader-sources/blob/main/src/coverNotAvailable.jpg?raw=true",
}}
style={[
styles.extensionIcon,
inLibraryBadge && { opacity: 0.5 },
]}
/>
<Text
style={[{ color: theme.textColorPrimary }, styles.novelName]}
numberOfLines={1}
>
{item.novelName}
</Text>
<View style={styles.badgeContainer}>
{downloadBadge}
{unreadBadge}
{inLibraryBadge}
</View>
</Pressable>
);
};
@@ -0,0 +1,8 @@
import React from "react";
import { ActivityIndicator, View } from "react-native";
export const LoadingScreen = ({ theme }) => (
<View style={{ flex: 1, justifyContent: "center" }}>
<ActivityIndicator size={50} color={theme.colorAccent} />
</View>
);
+226 -139
View File
@@ -1,16 +1,29 @@
import React from "react";
import { StyleSheet, View, Text, useWindowDimensions } from "react-native";
import React, { useCallback } from "react";
import {
StyleSheet,
View,
Text,
useWindowDimensions,
Pressable,
} from "react-native";
import { LinearGradient } from "expo-linear-gradient";
import { TouchableRipple } from "react-native-paper";
import { useSelector } from "react-redux";
import { useSettings, useTheme } from "../hooks/reduxHooks";
import ListView from "./ListView";
import FastImage from "react-native-fast-image";
import { getDeviceOrientation } from "../services/utils/helpers";
const NovelCover = ({ item, onPress, libraryStatus }) => {
const theme = useTheme();
import ListView from "./ListView";
import { getDeviceOrientation } from "../services/utils/helpers";
import { useSettings } from "../hooks/reduxHooks";
const NovelCover = ({
item,
onPress,
libraryStatus,
theme,
isSelected,
onLongPress,
selectedNovels,
}) => {
const { displayMode, novelsPerRow, showDownloadBadges, showUnreadBadges } =
useSettings();
@@ -21,157 +34,216 @@ const NovelCover = ({ item, onPress, libraryStatus }) => {
const getNovelsPerRow = () =>
orientation === "landscape" ? 6 : novelsPerRow;
const getHeight = () => (window.width / getNovelsPerRow()) * (4 / 3);
const getHeight = useCallback(
() => (window.width / getNovelsPerRow()) * (4 / 3),
[]
);
const comfortableTitle = () =>
displayMode === 1 && (
<Text
numberOfLines={2}
style={[
styles.title,
{
color: theme.textColorPrimary,
padding: 4,
},
]}
>
{item.novelName}
</Text>
);
const compactTitle = () =>
displayMode === 0 && (
<View style={styles.titleContainer}>
<LinearGradient
colors={["transparent", "rgba(0,0,0,0.7)"]}
style={styles.linearGradient}
>
<Text
numberOfLines={2}
style={[styles.title, { color: "rgba(255,255,255,1)" }]}
>
{item.novelName}
</Text>
</LinearGradient>
</View>
);
const unreadBadge = () =>
showUnreadBadges &&
item.chaptersUnread && (
<Text
style={[
styles.unreadBadge,
!item.chaptersDownloaded && {
borderTopLeftRadius: 4,
borderBottomLeftRadius: 4,
},
!showDownloadBadges && {
borderRadius: 4,
},
{
backgroundColor: theme.colorAccent,
color: theme.colorButtonText,
},
]}
>
{item.chaptersUnread}
</Text>
);
const downloadBadge = () =>
showDownloadBadges &&
item.chaptersDownloaded && (
<Text
style={[
styles.downloadBadge,
!item.chaptersUnread && {
borderTopRightRadius: 4,
borderBottomRightRadius: 4,
},
!showUnreadBadges && {
borderRadius: 4,
},
]}
>
{item.chaptersDownloaded}
</Text>
);
const inLibraryBadge = () =>
libraryStatus && (
<Text
style={[
styles.inLibraryBadge,
{
backgroundColor: theme.colorAccent,
color: theme.colorButtonText,
borderRadius: 4,
},
]}
>
In library
</Text>
);
const selectNovel = () => onLongPress(item.novelId);
return displayMode !== 2 ? (
<View style={{ flex: 1 / getNovelsPerRow() }}>
<TouchableRipple
borderless
centered
rippleColor={theme.rippleColor}
<View
style={[
{
flex: 1 / getNovelsPerRow(),
borderRadius: 6,
overflow: "hidden",
margin: 2,
},
isSelected && {
backgroundColor: theme.colorAccent,
opacity: 0.8,
},
]}
>
<Pressable
android_ripple={{ color: theme.colorAccent }}
style={styles.opac}
onPress={onPress}
onPress={
selectedNovels && selectedNovels.length > 0
? selectNovel
: onPress
}
onLongPress={selectNovel}
>
<>
<View
style={{
position: "absolute",
zIndex: 1,
top: 8,
left: 8,
flexDirection: "row",
}}
>
{inLibraryBadge()}
{downloadBadge()}
{unreadBadge()}
<View style={styles.badgeContainer}>
{libraryStatus && <InLibraryBadge theme={theme} />}
{showDownloadBadges && item.chaptersDownloaded && (
<DownloadBadge
showUnreadBadges={showUnreadBadges}
chaptersDownloaded={item.chaptersDownloaded}
chaptersUnread={item.chaptersUnread}
/>
)}
{showUnreadBadges && item.chaptersUnread && (
<UnreadBadge
theme={theme}
chaptersDownloaded={item.chaptersDownloaded}
chaptersUnread={item.chaptersUnread}
showDownloadBadges={showDownloadBadges}
/>
)}
</View>
<FastImage
source={{ uri: item.novelCover }}
source={{
uri:
item.novelCover &&
!item.novelCover.startsWith("/")
? item.novelCover
: "https://github.com/LNReader/lnreader-sources/blob/main/src/coverNotAvailable.jpg?raw=true",
}}
style={[
{ height: getHeight(), borderRadius: 4 },
libraryStatus && { opacity: 0.5 },
]}
/>
<View
style={{
position: "absolute",
bottom: 4,
left: 4,
right: 4,
}}
>
{compactTitle()}
<View style={styles.compactTitleContainer}>
{displayMode === 0 && (
<CompactTitle novelName={item.novelName} />
)}
</View>
{comfortableTitle()}
{displayMode === 1 && (
<ComfortableTitle
novelName={item.novelName}
theme={theme}
/>
)}
</>
</TouchableRipple>
</Pressable>
</View>
) : (
<ListView
item={item}
downloadBadge={downloadBadge()}
unreadBadge={unreadBadge()}
inLibraryBadge={inLibraryBadge()}
downloadBadge={
showDownloadBadges &&
item.chaptersDownloaded && (
<DownloadBadge
showUnreadBadges={showUnreadBadges}
chaptersDownloaded={item.chaptersDownloaded}
chaptersUnread={item.chaptersUnread}
/>
)
}
unreadBadge={
showUnreadBadges &&
item.chaptersUnread && (
<UnreadBadge
theme={theme}
chaptersDownloaded={item.chaptersDownloaded}
chaptersUnread={item.chaptersUnread}
showDownloadBadges={showDownloadBadges}
/>
)
}
inLibraryBadge={libraryStatus && <InLibraryBadge theme={theme} />}
theme={theme}
onPress={onPress}
onPress={
selectedNovels && selectedNovels.length > 0
? selectNovel
: onPress
}
onLongPress={selectNovel}
isSelected={isSelected}
/>
);
};
export default NovelCover;
const ComfortableTitle = ({ theme, novelName }) => (
<Text
numberOfLines={2}
style={[
styles.title,
{
color: theme.textColorPrimary,
padding: 4,
},
]}
>
{novelName}
</Text>
);
const CompactTitle = ({ novelName }) => (
<View style={styles.titleContainer}>
<LinearGradient
colors={["transparent", "rgba(0,0,0,0.7)"]}
style={styles.linearGradient}
>
<Text
numberOfLines={2}
style={[styles.title, { color: "rgba(255,255,255,1)" }]}
>
{novelName}
</Text>
</LinearGradient>
</View>
);
const InLibraryBadge = ({ theme }) => (
<Text
style={[
styles.inLibraryBadge,
{
backgroundColor: theme.colorAccent,
color: theme.colorButtonText,
borderRadius: 4,
},
]}
>
In library
</Text>
);
const UnreadBadge = ({
chaptersDownloaded,
chaptersUnread,
showDownloadBadges,
theme,
}) => (
<Text
style={[
styles.unreadBadge,
!chaptersDownloaded && {
borderTopLeftRadius: 4,
borderBottomLeftRadius: 4,
},
!showDownloadBadges && {
borderRadius: 4,
},
{
backgroundColor: theme.colorAccent,
color: theme.colorButtonText,
},
]}
>
{chaptersUnread}
</Text>
);
const DownloadBadge = ({
chaptersDownloaded,
showUnreadBadges,
chaptersUnread,
}) => (
<Text
style={[
styles.downloadBadge,
!chaptersUnread && {
borderTopRightRadius: 4,
borderBottomRightRadius: 4,
},
!showUnreadBadges && {
borderRadius: 4,
},
]}
>
{chaptersDownloaded}
</Text>
);
const styles = StyleSheet.create({
titleContainer: {
flex: 1,
@@ -181,15 +253,17 @@ const styles = StyleSheet.create({
fontFamily: "pt-sans-bold",
fontSize: 14,
padding: 8,
textShadowColor: "rgba(0, 0, 0, 0.75)",
textShadowOffset: { width: -1, height: 1 },
textShadowRadius: 10,
},
linearGradient: {
bottom: -1,
borderRadius: 4,
},
opac: {
paddingHorizontal: 4.5,
paddingVertical: 4,
padding: 4.8,
borderRadius: 4,
flex: 1,
},
extensionIcon: {
width: 42,
@@ -206,7 +280,7 @@ const styles = StyleSheet.create({
},
downloadBadge: {
backgroundColor: "#47a84a",
backgroundColor: "#2979FF",
color: "#FFFFFF",
borderTopLeftRadius: 4,
borderBottomLeftRadius: 4,
@@ -227,4 +301,17 @@ const styles = StyleSheet.create({
paddingHorizontal: 4,
fontSize: 12,
},
compactTitleContainer: {
position: "absolute",
bottom: 4,
left: 4,
right: 4,
},
badgeContainer: {
position: "absolute",
zIndex: 1,
top: 8,
left: 8,
flexDirection: "row",
},
});
+8 -4
View File
@@ -1,4 +1,4 @@
import React from "react";
import React, { useCallback } from "react";
import { StyleSheet, FlatList } from "react-native";
import { useSettings } from "../hooks/reduxHooks";
@@ -29,13 +29,18 @@ const NovelList = ({
}
};
const keyExtractor = useCallback((item) => item.novelUrl, []);
return (
<FlatList
contentContainerStyle={styles.flatListCont}
contentContainerStyle={[
styles.flatListCont,
displayMode !== 2 && { paddingHorizontal: 4 },
]}
numColumns={getNovelsPerRow()}
key={[orientation, getNovelsPerRow()]}
data={data}
keyExtractor={(item) => item.novelUrl}
keyExtractor={keyExtractor}
renderItem={renderItem}
refreshControl={refreshControl}
ListEmptyComponent={ListEmptyComponent}
@@ -52,6 +57,5 @@ const styles = StyleSheet.create({
flatListCont: {
flexGrow: 1,
paddingVertical: 8,
paddingHorizontal: 4,
},
});
@@ -2,7 +2,7 @@ import React, { useRef } from "react";
import { StyleSheet, View, TextInput, Text, StatusBar } from "react-native";
import { TouchableRipple, IconButton } from "react-native-paper";
import { Row } from "./Common";
import { Row } from "../Common";
export const Searchbar = ({
theme,
@@ -78,20 +78,20 @@ export const Searchbar = ({
const styles = StyleSheet.create({
searchbarContainer: {
marginTop: StatusBar.currentHeight + 4,
height: 48,
marginTop: StatusBar.currentHeight + 8,
height: 46,
flexDirection: "row",
alignItems: "center",
paddingHorizontal: 16,
marginBottom: 12,
paddingHorizontal: 12,
marginBottom: 6,
borderRadius: 8,
marginHorizontal: 12,
elevation: 2,
backgroundColor: "white",
},
container: {
flex: 1,
flexDirection: "row",
justifyContent: "space-between",
alignItems: "center",
},
});
+62 -28
View File
@@ -17,15 +17,15 @@ export const insertChapters = async (novelId, chapters) => {
novelId,
],
(txObj, res) => {},
(txObj, error) =>
console.log(
"Error ",
error,
" Chapter URL: ",
chapter.chapterUrl,
" Novel ID ",
novelId
)
(txObj, error) => {}
// console.log(
// "Error ",
// error,
// " Chapter URL: ",
// chapter.chapterUrl,
// " Novel ID ",
// novelId
// )
),
(txObj, res) => {
console.log("Success");
@@ -35,13 +35,10 @@ export const insertChapters = async (novelId, chapters) => {
});
};
const getChaptersQuery = (sort, filter) =>
`SELECT * FROM chapters WHERE novelId = ? ${filter || ""} ${
sort || "ORDER BY chapterId ASC"
}`;
const getChaptersQuery = (sort = "ORDER BY chapterId ASC", filter = "") =>
`SELECT * FROM chapters WHERE novelId = ? ${filter} ${sort}`;
export const getChapters = (novelId, sort, filter) => {
// console.log(getChaptersQuery(sort, filter));
return new Promise((resolve, reject) =>
db.transaction((tx) => {
tx.executeSql(
@@ -116,13 +113,26 @@ export const markChapterRead = async (chapterId) => {
});
};
const markChapterUnreadQuery = `UPDATE chapters SET \`read\` = 0 WHERE chapterId = ?`;
const markAllChaptersReadQuery = `UPDATE chapters SET \`read\` = 1 WHERE novelId = ?`;
export const markChapterUnread = async (chapterId) => {
export const markAllChaptersRead = async (novelId) => {
db.transaction((tx) => {
tx.executeSql(
markChapterUnreadQuery,
[chapterId],
markAllChaptersReadQuery,
[novelId],
(tx, res) => {},
(tx, error) => console.log(error)
);
});
};
const markAllChaptersUnreadQuery = `UPDATE chapters SET \`read\` = 0 WHERE novelId = ?`;
export const markAllChaptersUnread = async (novelId) => {
db.transaction((tx) => {
tx.executeSql(
markAllChaptersUnreadQuery,
[novelId],
(tx, res) => {},
(tx, error) => console.log(error)
);
@@ -150,7 +160,7 @@ export const isChapterDownloaded = async (chapterId) => {
);
};
const downloadChapterQuery = `INSERT INTO downloads (downloadChapterId, chapterName, chapterText, prevChapter, nextChapter) VALUES (?, ?, ?, ?, ?)`;
const downloadChapterQuery = `INSERT INTO downloads (downloadChapterId, chapterName, chapterText) VALUES (?, ?, ?)`;
export const downloadChapter = async (
sourceId,
@@ -169,15 +179,10 @@ export const downloadChapter = async (
);
tx.executeSql(
downloadChapterQuery,
[
chapterId,
chapter.chapterName,
chapter.chapterText,
chapter.prevChapter,
chapter.nextChapter,
],
(tx, res) =>
console.log(`Downloaded Chapter ${chapter.chapterUrl}`),
[chapterId, chapter.chapterName, chapter.chapterText],
(tx, res) => {
// console.log(`Downloaded Chapter ${chapter.chapterUrl}`);
},
(txObj, error) => console.log("Error ", error)
);
});
@@ -262,3 +267,32 @@ export const markPreviousChaptersUnread = async (chapterId, novelId) => {
);
});
};
const getDownloadedChaptersQuery = `
SELECT chapters.*, novels.sourceId, novels.novelName, novels.novelCover, novels.novelUrl
FROM chapters
JOIN novels
ON chapters.novelId = novels.novelId
WHERE chapters.downloaded = 1`;
export const getDownloadedChapters = () => {
return new Promise((resolve, reject) =>
db.transaction((tx) => {
tx.executeSql(
getDownloadedChaptersQuery,
null,
(txObj, { rows: { _array } }) => {
resolve(_array);
},
(txObj, error) => console.log("Error ", error)
);
})
);
};
export const deleteDownloads = async () => {
db.transaction((tx) => {
tx.executeSql(`UPDATE chapters SET downloaded = 0`);
tx.executeSql("DELETE FROM downloads; VACCUM;");
});
};
+1 -1
View File
@@ -13,7 +13,7 @@ const getHistoryQuery = `
ORDER BY history.historyTimeRead DESC
`;
export const getHistory = async () => {
export const getHistoryFromDb = async () => {
return new Promise((resolve, reject) => {
db.transaction((tx) => {
tx.executeSql(
+48 -3
View File
@@ -2,6 +2,7 @@ import * as SQLite from "expo-sqlite";
const db = SQLite.openDatabase("lnreader.db");
import BackgroundService from "react-native-background-actions";
import * as DocumentPicker from "expo-document-picker";
import { fetchChapters, fetchNovel } from "../../services/Source/source";
import { insertChapters } from "./ChapterQueries";
@@ -46,6 +47,17 @@ export const followNovel = async (followed, novelId) => {
});
};
export const unfollowNovel = async (novelId) => {
db.transaction((tx) => {
tx.executeSql(
"UPDATE novels SET followed = 0 WHERE novelId = ?",
[novelId],
(tx, res) => {},
(txObj, error) => console.log("Error ", error)
);
});
};
const checkNovelInCacheQuery = `SELECT * FROM novels WHERE novelUrl=? LIMIT 1`;
export const checkNovelInCache = (novelUrl) => {
@@ -85,9 +97,7 @@ export const deleteNovelCache = () => {
tx.executeSql(
"DELETE FROM novels WHERE followed = 0",
null,
(txObj, res) => {
console.log("Cleared Novel Cache");
},
(txObj, res) => showToast("Entries deleted"),
(txObj, error) => console.log("Error ", error)
);
});
@@ -195,3 +205,38 @@ export const migrateNovel = async (sourceId, novelUrl) => {
showToast(error.message);
}
};
export const updateNovelInfo = async (info, novelId) => {
db.transaction((tx) => {
tx.executeSql(
"UPDATE novels SET novelName = ?, novelSummary = ?, author = ?, genre = ?, status = ? WHERE novelId = ?",
[
info.novelName,
info.novelSummary,
info.author,
info.genre,
info.status,
novelId,
],
(txObj, res) => {},
(txObj, error) => console.log("Error ", error)
);
});
};
export const setCustomNovelCover = async (novelId) => {
const image = await DocumentPicker.getDocumentAsync({ type: "image/*" });
if (image.uri) {
db.transaction((tx) => {
tx.executeSql(
"UPDATE novels SET novelCover = ? WHERE novelId = ?",
[image.uri, novelId],
(txObj, res) => {},
(txObj, error) => console.log("Error ", error)
);
});
return image.uri;
}
};
-2
View File
@@ -4,8 +4,6 @@ export const createDownloadTableQuery = `
downloadChapterId INTEGER NOT NULL,
chapterName TEXT,
chapterText TEXT,
nextChapter TEXT,
prevChapter TEXT,
FOREIGN KEY (downloadChapterId) REFERENCES chapters(chapterId)
ON DELETE CASCADE
)`;
+1 -1
View File
@@ -22,7 +22,7 @@ export const githubUpdateChecker = () => {
};
const isNewVersion = (versionTag) => {
let currentVersion = "v1.1.1";
let currentVersion = "v1.1.2";
const regex = /[^\\d.]/;
let newVersion = versionTag.replace(regex, "");
+3 -3
View File
@@ -1,10 +1,10 @@
import { shallowEqual, useSelector } from "react-redux";
import { showChapterTitlesAction } from "../redux/novel/novel.actions";
import { useSelector } from "react-redux";
import { darkTheme } from "../theme/theme";
const useTheme = () => {
const theme = useSelector((state) => state.settingsReducer.theme);
return theme;
return theme ?? darkTheme;
};
const useReaderSettings = () => {
-10
View File
@@ -1,10 +0,0 @@
import { setStatusBarStyle as setExpoStatusBarStyle } from "expo-status-bar";
import { useTheme } from "../hooks/reduxHooks";
import changeNavigationBarColor from "react-native-navigation-bar-color";
export const setStatusBarStyle = () => {
const theme = useTheme();
setExpoStatusBarStyle(theme.id === 1 ? "dark" : "light");
changeNavigationBarColor(theme.colorPrimary);
};
-35
View File
@@ -1,35 +0,0 @@
import { useState, useEffect } from "react";
import { getSource } from "../sources/sources";
const usePopularNovels = (sourceId) => {
const [loading, setLoading] = useState(true);
const [pageNumber, setPage] = useState(1);
const [novels, setNovels] = useState([]);
const [error, setError] = useState();
const source = getSource(sourceId);
useEffect(() => {
const getNextPage = async (p) => {
try {
if (p === 1) setLoading(true);
const res = await source.popularNovels(p);
setHasMoreNovels(res.totalPages > pageNumber);
setNovels((novels) => [...novels, ...data.novels]);
setLoading(false);
} catch (err) {
setError(err.message);
}
};
getNextPage(pageNumber);
}, [pageNumber, sourceId]);
const incrementPage = () => {
setPage((pageNumber) => pageNumber + 1);
};
return { novels, incrementPage, loading, error };
};
export { usePopularNovels };
+56 -48
View File
@@ -1,5 +1,4 @@
import React from "react";
import { View } from "react-native";
import { createMaterialBottomTabNavigator } from "@react-navigation/material-bottom-tabs";
import MaterialCommunityIcons from "react-native-vector-icons/MaterialCommunityIcons";
@@ -10,32 +9,39 @@ import History from "../screens/History/History";
import Browse from "../screens/Browse/Browse";
import More from "../screens/More/More";
import { useTheme } from "../hooks/reduxHooks";
import { useSettings, useTheme } from "../hooks/reduxHooks";
const Tab = createMaterialBottomTabNavigator();
const BottomNavigator = () => {
const theme = useTheme();
const {
showHistoryTab = true,
showUpdatesTab = true,
showLabelsInNav = false,
} = useSettings();
return (
<View style={{ flex: 1, backgroundColor: theme.colorPrimaryDark }}>
<Tab.Navigator
barStyle={{ backgroundColor: theme.colorPrimary }}
activeColor={theme.colorAccent}
>
<Tab.Screen
name="Library"
component={Library}
options={{
tabBarIcon: ({ color }) => (
<MaterialCommunityIcons
name="book-variant-multiple"
color={color}
size={24}
/>
),
}}
/>
<Tab.Navigator
barStyle={{ backgroundColor: theme.colorPrimary }}
activeColor={theme.colorAccent}
shifting={!showLabelsInNav}
>
<Tab.Screen
name="Library"
component={Library}
options={{
tabBarIcon: ({ color }) => (
<MaterialCommunityIcons
name="book-variant-multiple"
color={color}
size={24}
/>
),
}}
/>
{showUpdatesTab && (
<Tab.Screen
name="Updates"
component={Updates}
@@ -49,6 +55,8 @@ const BottomNavigator = () => {
),
}}
/>
)}
{showHistoryTab && (
<Tab.Screen
name="History"
component={History}
@@ -62,34 +70,34 @@ const BottomNavigator = () => {
),
}}
/>
<Tab.Screen
name="Browse"
component={Browse}
options={{
tabBarIcon: ({ color }) => (
<MaterialCommunityIcons
name="compass-outline"
color={color}
size={24}
/>
),
}}
/>
<Tab.Screen
name="More"
component={More}
options={{
tabBarIcon: ({ color }) => (
<MaterialCommunityIcons
name="dots-horizontal"
color={color}
size={24}
/>
),
}}
/>
</Tab.Navigator>
</View>
)}
<Tab.Screen
name="Browse"
component={Browse}
options={{
tabBarIcon: ({ color }) => (
<MaterialCommunityIcons
name="compass-outline"
color={color}
size={24}
/>
),
}}
/>
<Tab.Screen
name="More"
component={More}
options={{
tabBarIcon: ({ color }) => (
<MaterialCommunityIcons
name="dots-horizontal"
color={color}
size={24}
/>
),
}}
/>
</Tab.Navigator>
);
};
+22 -21
View File
@@ -1,10 +1,18 @@
import React from "react";
import {
createStackNavigator,
TransitionPresets,
} from "@react-navigation/stack";
import React, { useEffect } from "react";
import { View } from "react-native";
import { NavigationContainer } from "@react-navigation/native";
import { createStackNavigator } from "@react-navigation/stack";
import { setBarColor } from "../theme/utils/setBarColor";
import { useTheme } from "../hooks/reduxHooks";
import { githubUpdateChecker } from "../hooks/githubUpdateChecker";
import * as SplashScreen from "expo-splash-screen";
// Navigators
import BottomNavigator from "./BottomNavigator";
// Screens
import Novel from "../screens/Novel/Novel";
import Chapter from "../screens/Chapter/Chapter";
import Extension from "../screens/Extension/Extension";
@@ -14,24 +22,21 @@ import SourceNovels from "../screens/Browse/SourceNovels";
import MigrateNovel from "../screens/Browse/migration/MigrationNovels";
import MoreStack from "./More";
import MalTopNovels from "../screens/Browse/discover/MalTopNovels";
import { setStatusBarStyle } from "../hooks/setStatusBarStyle";
import { View } from "react-native";
import { useTheme } from "../hooks/reduxHooks";
import { githubUpdateChecker } from "../hooks/githubUpdateChecker";
import NewUpdateDialog from "../components/NewUpdateDialog";
import { NavigationContainer } from "@react-navigation/native";
import BrowseNovelUpdates from "../screens/Browse/discover/BrowseNovelUpdates";
import BrowseSettings from "../screens/Browse/BrowseSettings";
const Stack = createStackNavigator();
const stackNavigatorConfig = {
headerShown: false,
};
const MainNavigator = () => {
const theme = useTheme();
setStatusBarStyle();
useEffect(() => {
setTimeout(async () => {
await SplashScreen.hideAsync();
setBarColor(theme);
}, 500);
}, [theme]);
const { isNewVersion, latestRelease } = githubUpdateChecker() || {};
return (
@@ -40,7 +45,7 @@ const MainNavigator = () => {
>
<View style={{ flex: 1, backgroundColor: theme.colorPrimaryDark }}>
{isNewVersion && <NewUpdateDialog newVersion={latestRelease} />}
<Stack.Navigator screenOptions={stackNavigatorConfig}>
<Stack.Navigator screenOptions={{ headerShown: false }}>
<Stack.Screen
name="BottomNavigator"
component={BottomNavigator}
@@ -54,10 +59,6 @@ const MainNavigator = () => {
name="BrowseSettings"
component={BrowseSettings}
/>
<Stack.Screen
name="BrowseNovelUpdates"
component={BrowseNovelUpdates}
/>
<Stack.Screen
name="GlobalSearch"
component={GlobalSearch}
+20 -46
View File
@@ -1,9 +1,8 @@
import React from "react";
import { View } from "react-native";
import { useTheme } from "../hooks/reduxHooks";
import { createStackNavigator } from "@react-navigation/stack";
// Screens
import About from "../screens/More/About";
import Settings from "../screens/More/Settings";
import TrackerSettings from "../screens/More/TrackerSettings";
@@ -12,55 +11,30 @@ import BackupSettings from "../screens/More/BackupSettings";
import AdvancedSettings from "../screens/More/AdvancedSettings";
import GeneralSettings from "../screens/More/GeneralSettings";
import DownloadQueue from "../screens/More/DownloadQueue";
import Downloads from "../screens/More/Downloads";
const Stack = createStackNavigator();
const stackNavigatorConfig = { headerShown: false };
const SettingsStack = () => {
const theme = useTheme();
const SettingsStack = () => (
<Stack.Navigator screenOptions={stackNavigatorConfig}>
<Stack.Screen name="Settings" component={Settings} />
<Stack.Screen name="GeneralSettings" component={GeneralSettings} />
<Stack.Screen name="ReaderSettings" component={ReaderSettings} />
<Stack.Screen name="TrackerSettings" component={TrackerSettings} />
<Stack.Screen name="BackupSettings" component={BackupSettings} />
<Stack.Screen name="AdvancedSettings" component={AdvancedSettings} />
</Stack.Navigator>
);
return (
<View style={{ flex: 1, backgroundColor: theme.colorPrimaryDark }}>
<Stack.Navigator screenOptions={stackNavigatorConfig}>
<Stack.Screen name="Settings" component={Settings} />
<Stack.Screen
name="GeneralSettings"
component={GeneralSettings}
/>
<Stack.Screen
name="ReaderSettings"
component={ReaderSettings}
/>
<Stack.Screen
name="TrackerSettings"
component={TrackerSettings}
/>
<Stack.Screen
name="BackupSettings"
component={BackupSettings}
/>
<Stack.Screen
name="AdvancedSettings"
component={AdvancedSettings}
/>
</Stack.Navigator>
</View>
);
};
const MoreStack = () => {
const theme = useTheme();
return (
<View style={{ flex: 1, backgroundColor: theme.colorPrimaryDark }}>
<Stack.Navigator screenOptions={stackNavigatorConfig}>
<Stack.Screen name="SettingsStack" component={SettingsStack} />
<Stack.Screen name="About" component={About} />
<Stack.Screen name="DownloadQueue" component={DownloadQueue} />
</Stack.Navigator>
</View>
);
};
const MoreStack = () => (
<Stack.Navigator screenOptions={stackNavigatorConfig}>
<Stack.Screen name="SettingsStack" component={SettingsStack} />
<Stack.Screen name="About" component={About} />
<Stack.Screen name="DownloadQueue" component={DownloadQueue} />
<Stack.Screen name="Downloads" component={Downloads} />
</Stack.Navigator>
);
export default MoreStack;
+2
View File
@@ -1,2 +1,4 @@
export const SET_DOWNLOAD_QUEUE = "SET_DOWNLOAD_QUEUE";
export const CANCEL_DOWNLOAD = "CANCEL_DOWNLOAD";
export const PAUSE_DOWNLOADS = "PAUSE_DOWNLOADS";
export const RESUME_DOWNLOADS = "RESUME_DOWNLOADS";
+95 -1
View File
@@ -1,5 +1,99 @@
import { SET_DOWNLOAD_QUEUE, CANCEL_DOWNLOAD } from "./donwloads.types";
import {
SET_DOWNLOAD_QUEUE,
CANCEL_DOWNLOAD,
PAUSE_DOWNLOADS,
} from "./donwloads.types";
import BackgroundService from "react-native-background-actions";
import { downloadChapter } from "../../database/queries/ChapterQueries";
import { CHAPTER_DOWNLOADED } from "../novel/novel.types";
import { showToast } from "../../hooks/showToast";
export const pauseDownloads = () => async (dispatch) => {
showToast("Download paused");
await BackgroundService.stop();
dispatch({ type: PAUSE_DOWNLOADS });
};
export const resumeDownloads = (chapters) => async (dispatch) => {
showToast("Download resumed");
const options = {
taskName: "Library Update",
taskTitle: chapters[0].chapterName,
taskDesc: "0/" + chapters.length,
taskIcon: {
name: "notification_icon",
type: "drawable",
},
color: "#00adb5",
parameters: {
delay: 1000,
},
linkingURI: "lnreader://downloads",
progressBar: {
max: chapters.length,
value: 0,
},
};
const sleep = (time) =>
new Promise((resolve) => setTimeout(() => resolve(), time));
const veryIntensiveTask = async (taskData) => {
await new Promise(async (resolve) => {
for (
let i = 0;
BackgroundService.isRunning() && i < chapters.length;
i++
) {
if (BackgroundService.isRunning()) {
if (!chapters[i].downloaded) {
await downloadChapter(
chapters[i].sourceId,
chapters[i].novelUrl,
chapters[i].chapterUrl,
chapters[i].chapterId
);
}
dispatch({
type: CHAPTER_DOWNLOADED,
payload: chapters[i].chapterId,
});
await BackgroundService.updateNotification({
taskTitle: chapters[i].chapterName,
taskDesc: i + 1 + "/" + chapters.length,
progressBar: {
max: chapters.length,
value: i + 1,
},
});
if (i + 1 === chapters.length) {
resolve();
await BackgroundService.stop();
Notifications.scheduleNotificationAsync({
content: {
title: "Downloader",
body: "Download completed",
},
trigger: null,
});
}
await sleep(taskData.delay);
}
}
});
};
await BackgroundService.start(veryIntensiveTask, options);
dispatch({ type: PAUSE_DOWNLOADS });
};
export const cancelDownload = () => async (dispatch) => {
await BackgroundService.stop();
-75
View File
@@ -1,75 +0,0 @@
import {
CLEAR_NOVEL_HISTORY,
GET_HISTORY,
LOAD_HISTORY,
CLEAR_HISTORY,
} from "./history.types";
import {
getHistory,
insertHistory,
deleteHistory,
deleteAllHistory,
} from "../../database/queries/HistoryQueries";
import { SET_LAST_READ } from "../preferences/preference.types";
import { showToast } from "../../hooks/showToast";
export const getHistoryAction = () => async (dispatch) => {
dispatch({ type: LOAD_HISTORY });
const history = await getHistory();
const groups = history.reduce((groups, update) => {
var dateParts = update.historyTimeRead.split("-");
var jsDate = new Date(
dateParts[0],
dateParts[1] - 1,
dateParts[2].substr(0, 2)
);
const date = jsDate.toISOString();
if (!groups[date]) {
groups[date] = [];
}
groups[date].push(update);
return groups;
}, {});
const groupedHistory = Object.keys(groups).map((date) => {
return {
date,
novels: groups[date],
};
});
// console.log(groupedHistory);
dispatch({ type: GET_HISTORY, payload: groupedHistory });
};
export const insertHistoryAction = (novelId, chapterId) => async (dispatch) => {
await insertHistory(novelId, chapterId);
dispatch({
type: SET_LAST_READ,
payload: { novelId, chapterId },
});
dispatch(getHistoryAction());
};
export const deleteHistoryAction = (historyId) => async (dispatch) => {
await deleteHistory(historyId);
console.log("Delete History Action Called");
dispatch(getHistoryAction());
};
export const clearAllHistoryAction = () => async (dispatch) => {
try {
await deleteAllHistory();
showToast("History deleted");
dispatch({ type: CLEAR_HISTORY });
} catch (error) {
showToast(error.message);
}
};
-35
View File
@@ -1,35 +0,0 @@
import {
CLEAR_HISTORY,
CLEAR_NOVEL_HISTORY,
GET_HISTORY,
LOAD_HISTORY,
} from "./history.types";
const initialState = {
history: [],
loading: true,
};
const historyReducer = (state = initialState, action) => {
const { type, payload } = action;
switch (type) {
case LOAD_HISTORY:
return { ...state, loading: true };
case GET_HISTORY:
return { ...state, history: payload, loading: false };
// case CLEAR_NOVEL_HISTORY:
// return {
// ...state,
// history: state.history.filter(
// (item) => item.novelId !== payload.novelId
// ),
// };
case CLEAR_HISTORY:
return { ...state, loading: false, history: [] };
default:
return state;
}
};
export default historyReducer;
-5
View File
@@ -1,5 +0,0 @@
export const LOAD_HISTORY = "LOAD_HISTORY";
export const GET_HISTORY = "GET_HISTORY";
export const UPDATE_NOVEL_HISTORY = "UPDATE_NOVEL_HISTORY";
export const CLEAR_NOVEL_HISTORY = "CLEAR_NOVEL_HISTORY";
export const CLEAR_HISTORY = "CLEAR_HISTORY";
+7
View File
@@ -307,6 +307,12 @@ export const downloadAllChaptersAction =
try {
let chapters = chaps.filter((chapter) => chapter.downloaded === 0);
chapters = chapters.map((chapter) => ({
...chapter,
sourceId,
novelUrl,
}));
dispatch({ type: SET_DOWNLOAD_QUEUE, payload: chapters });
const options = {
taskName: "Library Update",
@@ -317,6 +323,7 @@ export const downloadAllChaptersAction =
type: "drawable",
},
color: "#00adb5",
linkingURI: "lnreader://downloads/redirect",
parameters: {
delay: 1000,
},
+16
View File
@@ -4,6 +4,8 @@ import {
SET_READER_SETTINGS,
SET_APP_SETTINGS,
SET_ACCENT_COLOR,
SET_RIPPLE_COLOR,
SET_AMOLED_MODE,
} from "./settings.types";
export const setAppTheme = (val) => (dispatch) => {
@@ -40,3 +42,17 @@ export const setAccentColor = (val) => (dispatch) => {
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 },
});
};
+42 -14
View File
@@ -4,27 +4,34 @@ import {
SET_READER_SETTINGS,
SET_APP_SETTINGS,
SET_ACCENT_COLOR,
SET_RIPPLE_COLOR,
SET_AMOLED_MODE,
} from "./settings.types";
import {
amoledDarkTheme,
lightTheme,
darkTheme,
midnightDuskTheme,
limeTheme,
greenAppleTheme,
irisBlueTheme,
hotPinkTheme,
strawberryDaiquiri,
takoTheme,
yinYangTheme,
springBlossomTheme,
colorsAmoled,
oceanicTheme,
} from "../../theme/theme";
const themes = {
0: amoledDarkTheme,
1: lightTheme,
2: darkTheme,
3: midnightDuskTheme,
4: limeTheme,
4: greenAppleTheme,
5: irisBlueTheme,
6: hotPinkTheme,
6: oceanicTheme,
7: strawberryDaiquiri,
8: takoTheme,
9: yinYangTheme,
10: springBlossomTheme,
};
/**
@@ -35,14 +42,6 @@ const themes = {
* 2 -> List
*/
/**
* Reader Theme
*
* 1 -> Dark
* 2 -> White
* 3 -> Sepia
*/
const initialState = {
theme: darkTheme,
displayMode: 0,
@@ -55,6 +54,7 @@ const initialState = {
padding: 5,
fontFamily: "",
lineHeight: 1.5,
customCSS: null,
},
showDownloadBadges: true,
showUnreadBadges: true,
@@ -65,6 +65,11 @@ const initialState = {
searchAllSources: false,
textSelectable: false,
updateLibraryOnLaunch: false,
downloadNewChapters: false,
showHistoryTab: true,
showUpdatesTab: true,
showLabelsInNav: false,
useWebViewForChapter: false,
};
const settingsReducer = (state = initialState, action) => {
@@ -86,6 +91,29 @@ const settingsReducer = (state = initialState, action) => {
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,
+2
View File
@@ -3,3 +3,5 @@ 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";
+12 -2
View File
@@ -6,11 +6,21 @@ import {
SEARCH_SOURCES,
} from "./source.types";
import { showToast } from "../../hooks/showToast";
import { sources } from "../../sources/sources";
import sources from "../../sources/sources.json";
export const getSourcesAction = () => async (dispatch) => {
try {
dispatch({ type: GET_SOURCES, payload: sources });
/**
* Sort sources aplhabetically
*/
let sortedSources = sources.sort((a, b) =>
a.sourceName.localeCompare(b.sourceName)
);
dispatch({
type: GET_SOURCES,
payload: sortedSources,
});
} catch (error) {
showToast(error.message);
}
-2
View File
@@ -12,7 +12,6 @@ const initialState = {
search: [],
pinned: [],
filters: [],
showNovelUpdates: true,
showMyAnimeList: true,
loading: true,
};
@@ -44,7 +43,6 @@ const sourceReducer = (state = initialState, action) => {
if (!state.filters) {
state.filters = [];
}
return {
...state,
filters:
-2
View File
@@ -7,7 +7,6 @@ import settingsReducer from "./settings/settings.reducer";
import libraryReducer from "./library/library.reducer";
import sourceReducer from "./source/source.reducers";
import novelReducer from "./novel/novel.reducer";
import historyReducer from "./history/history.reducer";
import updatesReducer from "./updates/updates.reducer";
import trackerReducer from "./tracker/tracker.reducer";
import preferenceReducer from "./preferences/preference.reducer";
@@ -26,7 +25,6 @@ const persistedReducer = persistReducer(
libraryReducer,
sourceReducer,
novelReducer,
historyReducer,
updatesReducer,
trackerReducer,
preferenceReducer,
+4 -2
View File
@@ -33,10 +33,12 @@ export const getUpdatesAction = () => async (dispatch) => {
dispatch({ type: GET_UPDATES, payload: groupedUpdates });
};
export const updateLibraryAction = () => async (dispatch) => {
export const updateLibraryAction = () => async (dispatch, getState) => {
showToast("Updating library");
await updateAllNovels();
const { downloadNewChapters = false } = getState().settingsReducer;
await updateAllNovels({ downloadNewChapters });
dispatch(getUpdatesAction());
};
+30 -54
View File
@@ -7,17 +7,18 @@ import {
ActivityIndicator,
RefreshControl,
} from "react-native";
import { useTheme } from "../../hooks/reduxHooks";
import { useDispatch, useSelector } from "react-redux";
import {
getSourcesAction,
searchSourcesAction,
} from "../../redux/source/source.actions";
import SourceCard from "./components/SourceCard";
import { useTheme } from "../../hooks/reduxHooks";
import { showToast } from "../../hooks/showToast";
import { Searchbar } from "../../components/Searchbar";
import { Searchbar } from "../../components/Searchbar/Searchbar";
import SourceCard from "./components/SourceCard";
import EmptyView from "../../components/EmptyView";
import DiscoverCard from "./discover/DiscoverCard";
@@ -31,7 +32,6 @@ const Browse = ({ navigation }) => {
loading,
pinned,
filters = [],
showNovelUpdates = true,
showMyAnimeList = true,
} = useSelector((state) => state.sourceReducer);
const theme = useTheme();
@@ -42,6 +42,8 @@ const Browse = ({ navigation }) => {
sources = sources.filter((source) => filters.indexOf(source.lang) === -1);
let pinnedSources = sources.filter((source) => isPinned(source.sourceId));
useEffect(() => {
dispatch(getSourcesAction());
}, [getSourcesAction]);
@@ -72,6 +74,14 @@ const Browse = ({ navigation }) => {
/>
);
const SourceHeader = ({ title }) => (
<Text
style={[styles.sourcesHeader, { color: theme.textColorSecondary }]}
>
{title}
</Text>
);
return (
<View
style={[
@@ -107,32 +117,12 @@ const Browse = ({ navigation }) => {
data={!searchText ? sources : search}
keyExtractor={(item) => item.sourceId.toString()}
renderItem={renderItem}
extraData={pinned}
extraData={pinnedSources}
ListHeaderComponent={
<View>
{(showNovelUpdates || showMyAnimeList) && (
{showMyAnimeList && (
<>
<Text
style={{
color: theme.textColorSecondary,
paddingHorizontal: 16,
paddingVertical: 8,
}}
>
Discover
</Text>
{showNovelUpdates && (
<DiscoverCard
label="Novel Updates"
onPress={() =>
navigation.navigate(
"BrowseNovelUpdates"
)
}
icon={require("../../../assets/novelupdates.png")}
theme={theme}
/>
)}
<SourceHeader title="Discover" />
{showMyAnimeList && (
<DiscoverCard
label="MyAnimeList"
@@ -145,41 +135,21 @@ const Browse = ({ navigation }) => {
)}
</>
)}
{pinned.length > 0 && (
{pinnedSources.length > 0 && (
<FlatList
contentContainerStyle={{ paddingBottom: 16 }}
data={sources.filter((source) =>
isPinned(source.sourceId)
)}
data={pinnedSources}
keyExtractor={(item) =>
item.sourceId.toString()
}
renderItem={renderItem}
extraData={pinned}
extraData={pinnedSources}
ListHeaderComponent={
<Text
style={{
color: theme.textColorSecondary,
paddingHorizontal: 16,
paddingVertical: 8,
}}
>
Pinned
</Text>
<SourceHeader title="Pinned" />
}
/>
)}
{sources.length > 0 && (
<Text
style={{
color: theme.textColorSecondary,
paddingHorizontal: 16,
paddingVertical: 8,
}}
>
Sources
</Text>
)}
{sources.length > 0 && <SourceHeader title="Sources" />}
</View>
}
ListEmptyComponent={
@@ -214,5 +184,11 @@ const Browse = ({ navigation }) => {
export default Browse;
const styles = StyleSheet.create({
container: { flex: 1 },
container: {
flex: 1,
},
sourcesHeader: {
paddingHorizontal: 16,
paddingVertical: 8,
},
});
-166
View File
@@ -1,166 +0,0 @@
import React, { useEffect, useState } from "react";
import { StyleSheet, View, ActivityIndicator, FlatList } from "react-native";
import * as WebBrowser from "expo-web-browser";
import NovelCover from "../../components/NovelCover";
import { Searchbar } from "../../components/Searchbar";
import ErrorView from "../../components/ErrorView";
import { useTheme, useSettings } from "../../hooks/reduxHooks";
import { showToast } from "../../hooks/showToast";
import { getDeviceOrientation } from "../../services/utils/helpers";
import { scrapeSearchResults } from "./discover/MyAnimeListScraper";
const BrowseMalScreen = ({ navigation, route }) => {
const theme = useTheme();
const [loading, setLoading] = useState(true);
const [novels, setNovels] = useState();
const [error, setError] = useState();
const [searchText, setSearchText] = useState("");
const baseUrl = "https://api.jikan.moe/v3/";
const malUrl = "https://myanimelist.net/topmanga.php?type=lightnovels";
const getNovels = async () => {
try {
const url = `${baseUrl}top/manga/1/novels`;
const res = await fetch(url);
const data = await res.json();
setNovels(data.top);
setLoading(false);
} catch (error) {
setError(error.message);
setNovels([]);
setLoading(false);
showToast(error.message);
}
};
const clearSearchbar = () => {
getNovels();
setLoading(true);
setSearchText("");
};
const getSearchResults = async () => {
try {
setLoading(true);
const data = await scrapeSearchResults(searchText);
setNovels(data);
setLoading(false);
} catch (error) {
setError(error.message);
setNovels([]);
setLoading(false);
showToast(error.message);
}
};
useEffect(() => {
getNovels();
}, []);
const renderItem = ({ item }) => (
<NovelCover
item={{ novelName: item.title, novelCover: item.image_url }}
onPress={() =>
navigation.navigate("GlobalSearch", {
novelName: item.title,
})
}
/>
);
const { displayMode, novelsPerRow } = useSettings();
const orientation = getDeviceOrientation();
const getNovelsPerRow = () => {
if (displayMode === 2) {
return 1;
}
if (orientation === "landscape") {
return 6;
} else {
return novelsPerRow;
}
};
const ListEmptyComponent = () => (
<ErrorView
error={error || "No results found"}
onRetry={() => {
getNovels();
setLoading(true);
setError();
}}
openWebView={() => WebBrowser.openBrowserAsync(topNovelsUrl)}
theme={theme}
/>
);
return (
<View
style={[
styles.container,
{ backgroundColor: theme.colorPrimaryDark },
]}
>
<Searchbar
theme={theme}
placeholder="Search MyAnimeList"
backAction="arrow-left"
onBackAction={() => navigation.goBack()}
searchText={searchText}
onChangeText={(text) => setSearchText(text)}
onSubmitEditing={getSearchResults}
clearSearchbar={clearSearchbar}
actions={[
{
icon: "earth",
onPress: () => WebBrowser.openBrowserAsync(malUrl),
},
]}
/>
{loading ? (
<View style={{ flex: 1, justifyContent: "center" }}>
<ActivityIndicator size={60} color={theme.colorAccent} />
</View>
) : (
<FlatList
contentContainerStyle={styles.novelsContainer}
numColumns={getNovelsPerRow()}
key={getNovelsPerRow()}
data={novels}
keyExtractor={(item) => item.mal_id.toString()}
renderItem={renderItem}
ListEmptyComponent={ListEmptyComponent}
/>
)}
</View>
);
};
export default BrowseMalScreen;
const styles = StyleSheet.create({
container: {
flex: 1,
},
contentContainer: {
flex: 1,
},
novelsContainer: {
flexGrow: 1,
paddingVertical: 8,
paddingHorizontal: 4,
},
});
+21 -32
View File
@@ -1,4 +1,4 @@
import React, { useState } from "react";
import React from "react";
import { FlatList, Pressable, Text, View } from "react-native";
import { Switch } from "react-native-paper";
@@ -6,7 +6,7 @@ import { useDispatch, useSelector } from "react-redux";
import { Appbar } from "../../components/Appbar";
import { ScreenContainer } from "../../components/Common";
import { ListSubHeader } from "../../components/List";
import { List } from "../../components/List";
import { useSettings, useTheme } from "../../hooks/reduxHooks";
import { setAppSettings } from "../../redux/settings/settings.actions";
@@ -18,12 +18,20 @@ import {
const BrowseSettings = ({ navigation }) => {
const theme = useTheme();
const dispatch = useDispatch();
const {
filters = [],
showNovelUpdates = true,
showMyAnimeList = true,
} = useSelector((state) => state.sourceReducer);
const languages = ["English", "Spanish"];
const { filters = [], showMyAnimeList = true } = useSelector(
(state) => state.sourceReducer
);
const languages = [
"English",
"Spanish",
"Japanese",
"Chinese",
"Arabic",
"Indonesian",
"Russian",
];
const { searchAllSources = false } = useSettings();
const renderItem = ({ item }) => {
@@ -52,7 +60,7 @@ const BrowseSettings = ({ navigation }) => {
<ScreenContainer theme={theme}>
<Appbar onBackAction={navigation.goBack} title="Sources" />
<View>
<ListSubHeader theme={theme}>Global Search</ListSubHeader>
<List.SubHeader theme={theme}>Global Search</List.SubHeader>
<Pressable
android_ripple={{ color: theme.rippleColor }}
style={{
@@ -86,28 +94,8 @@ const BrowseSettings = ({ navigation }) => {
}
/>
</Pressable>
<ListSubHeader theme={theme}>Discover</ListSubHeader>
<Pressable
android_ripple={{ color: theme.rippleColor }}
style={{
padding: 16,
flexDirection: "row",
alignItems: "center",
justifyContent: "space-between",
}}
onPress={() => dispatch(enableDiscover("showNovelUpdates"))}
>
<Text style={{ color: theme.textColorPrimary }}>
Novel Updates
</Text>
<Switch
color={theme.colorAccent}
value={showNovelUpdates}
onValueChange={() =>
dispatch(enableDiscover("showNovelUpdates"))
}
/>
</Pressable>
<List.Divider theme={theme} />
<List.SubHeader theme={theme}>Discover</List.SubHeader>
<Pressable
android_ripple={{ color: theme.rippleColor }}
style={{
@@ -129,7 +117,8 @@ const BrowseSettings = ({ navigation }) => {
}
/>
</Pressable>
<ListSubHeader theme={theme}>Languages</ListSubHeader>
<List.Divider theme={theme} />
<List.SubHeader theme={theme}>Languages</List.SubHeader>
<FlatList
data={languages}
keyExtractor={(item) => item}
+1 -5
View File
@@ -21,11 +21,7 @@ const ExtensionCard = ({ item, theme, isPinned, dispatch, navigation }) => {
onPress={navigateToExtension}
android_ripple={{ color: theme.rippleColor }}
>
<Image
source={{ uri: icon }}
style={styles.extensionIcon}
// resizeMode="contain"
/>
<Image source={{ uri: icon }} style={styles.extensionIcon} />
<View style={styles.extensionDetails}>
<View>
<Text
@@ -1,127 +0,0 @@
import React, { useState, useEffect } from "react";
import {
ActivityIndicator,
Button,
FlatList,
StyleSheet,
Text,
View,
} from "react-native";
import * as WebBrowser from "expo-web-browser";
import { ScreenContainer } from "../../../components/Common";
import { Searchbar } from "../../../components/Searchbar";
import { useTheme } from "../../../hooks/reduxHooks";
import { scrapeSearchResults, scrapeTopNovels } from "./NovelUpdatesScraper";
import NovelUpdatesNovelCard from "./NovelUpdatesNovelCard";
const BrowseNovelUpdates = ({ navigation }) => {
const theme = useTheme();
const [novels, setNovels] = useState([]);
const [pageNo, setPageNo] = useState(1);
const [loading, setLoading] = useState(true);
const [searchText, setSearchText] = useState("");
const getNovels = async (pageNo) => {
const data = await scrapeTopNovels(pageNo);
setNovels((novels) => novels.concat(data));
setLoading(false);
};
useEffect(() => {
getNovels(1);
}, []);
const getSearchResults = async () => {
setLoading(true);
const data = await scrapeSearchResults(searchText);
setNovels(data);
setLoading(false);
};
const clearSearchbar = () => {
getNovels();
setLoading(true);
setSearchText("");
};
const isCloseToBottom = ({
layoutMeasurement,
contentOffset,
contentSize,
}) => {
const paddingToBottom = 20;
return (
layoutMeasurement.height + contentOffset.y >=
contentSize.height - paddingToBottom
);
};
return (
<ScreenContainer theme={theme}>
<Searchbar
theme={theme}
placeholder="Search Novel Updates"
backAction="arrow-left"
onBackAction={navigation.goBack}
searchText={searchText}
onChangeText={(text) => setSearchText(text)}
onSubmitEditing={getSearchResults}
clearSearchbar={clearSearchbar}
actions={[
{
icon: "earth",
onPress: () =>
WebBrowser.openBrowserAsync(
"https://www.novelupdates.com/"
),
},
]}
/>
{loading ? (
<View style={{ flex: 1, justifyContent: "center" }}>
<ActivityIndicator size={60} color={theme.colorAccent} />
</View>
) : (
<FlatList
data={novels}
keyExtractor={(item) => item.novelName.toString()}
renderItem={({ item }) => (
<NovelUpdatesNovelCard
novel={item}
theme={theme}
onPress={() =>
navigation.navigate("GlobalSearch", {
novelName: item.novelName,
})
}
/>
)}
onScroll={({ nativeEvent }) => {
if (!searchText && isCloseToBottom(nativeEvent)) {
getNovels(pageNo + 1);
setPageNo((pageNo) => pageNo + 1);
}
}}
ListFooterComponent={
!searchText && (
<View style={{ paddingVertical: 16 }}>
<ActivityIndicator color={theme.colorAccent} />
</View>
)
}
/>
)}
</ScreenContainer>
);
};
export default BrowseNovelUpdates;
const styles = StyleSheet.create({});
+14 -9
View File
@@ -3,8 +3,8 @@ import { StyleSheet, View, ActivityIndicator, FlatList } from "react-native";
import * as WebBrowser from "expo-web-browser";
import { Searchbar } from "../../../components/Searchbar";
import ErrorView from "../../../components/ErrorView";
import { Searchbar } from "../../../components/Searchbar/Searchbar";
import { ErrorView } from "../../../components/ErrorView/ErrorView";
import { useTheme, useSettings } from "../../../hooks/reduxHooks";
import { showToast } from "../../../hooks/showToast";
@@ -106,13 +106,18 @@ const BrowseMalScreen = ({ navigation, route }) => {
const ListEmptyComponent = () => (
<ErrorView
error={error || "No results found"}
onRetry={() => {
getNovels();
setLoading(true);
setError();
}}
openWebView={() => WebBrowser.openBrowserAsync(topNovelsUrl)}
errorName={error || "No results found"}
actions={[
{
name: "Retry",
onPress: () => {
getNovels();
setLoading(true);
setError();
},
icon: "reload",
},
]}
theme={theme}
/>
);
@@ -1,5 +0,0 @@
import React from "react";
const NovelUpdatesBottomSheet = () => {};
export default NovelUpdatesBottomSheet;
@@ -1,122 +0,0 @@
import React from "react";
import { StyleSheet, Pressable, Text, View, FlatList } from "react-native";
import FastImage from "react-native-fast-image";
import { Row } from "../../../components/Common";
const GenreChip = ({ children, theme }) => (
<View style={{ flexDirection: "row" }}>
<Text
style={[styles.genreChip, { color: theme.textColorSecondary }]}
numberOfLines={2}
>
<Text
style={[
styles.genreChip,
{
color: theme.textColorPrimary,
fontSize: 14,
},
]}
>
{`${"Genre "}`}
</Text>
{children}
</Text>
</View>
);
const NovelUpdatesNovelCard = ({ novel, theme, onPress }) => {
return (
<View
style={{
flex: 1,
backgroundColor: theme.colorPrimary,
borderRadius: 8,
elevation: 1,
marginHorizontal: 12,
marginVertical: 4,
}}
>
<Pressable
style={styles.novelUpdatesCard}
onPress={onPress}
android_ripple={{ color: theme.rippleColor, borderless: true }}
>
<FastImage
source={{ uri: novel.novelCover }}
style={{
width: 130,
height: 180,
borderTopLeftRadius: 8,
borderBottomLeftRadius: 8,
}}
/>
<View
style={{
flex: 1,
paddingHorizontal: 8,
paddingVertical: 8,
}}
>
<Text
style={{ color: theme.textColorPrimary, fontSize: 17 }}
>
{novel.novelName}
</Text>
<GenreChip theme={theme}>{novel.genres}</GenreChip>
<View style={{ flexDirection: "row", marginBottom: 4 }}>
<Text
style={[
styles.genreChip,
{ color: theme.textColorSecondary },
]}
numberOfLines={2}
>
<Text
style={[
styles.genreChip,
{
color: theme.textColorPrimary,
fontSize: 14,
},
]}
>
{`${"Chapter Count "}`}
</Text>
{novel.chapterCount}
</Text>
</View>
<Text
style={{
fontSize: 12,
color: theme.textColorSecondary,
marginTop: 8,
}}
numberOfLines={4}
>
{novel.novelSummary}
</Text>
</View>
</Pressable>
</View>
);
};
export default NovelUpdatesNovelCard;
const styles = StyleSheet.create({
novelUpdatesCard: {
flex: 1,
flexDirection: "row",
},
contentContainer: {
flex: 1,
},
genreChip: {
marginTop: 4,
fontSize: 13,
// fontStyle: "italic",
},
});
@@ -1,130 +0,0 @@
import cheerio from "react-native-cheerio";
const scrapeTopNovels = async (pageNo) => {
const url =
"https://www.novelupdates.com/series-ranking/?rank=sixmonths&pg=" +
pageNo;
let headers = new Headers({
Accept: "application/json",
"Content-Type": "application/json",
"User-Agent":
"'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/83.0.4103.116 Safari/537.36",
});
const res = await fetch(url, {
method: "GET",
headers: headers,
});
const body = await res.text();
const $ = cheerio.load(body);
const novels = [];
$("div.search_main_box_nu").each(function (res) {
const novelCover = $(this).find("img").attr("src");
const novelName = $(this).find(".search_title > a").text();
const genres = $(this)
.find(".search_genre")
.text()
.replace(/\s/g, ", ");
const novelSummary =
$(this).find(".search_genre")[0].nextSibling.nodeValue;
let chapterCount = "";
$(this)
.find(".search_stats > span")
.each(function () {
let detail = $(this).text();
if (detail.includes("Chapters")) {
chapterCount = detail.replace(" Chapters", "");
}
});
// const chapterCount = $(this)
// .find('i[title="Chapter Count"]')
// const updateFrequency = $(this)
// .find('i[title="Updates Frequency"]')
const novel = {
novelName,
novelCover,
genres,
novelSummary,
chapterCount,
// updateFrequency,
};
novels.push(novel);
});
return novels;
};
const scrapeSearchResults = async (searchTerm) => {
const url =
"https://www.novelupdates.com/?s=" +
searchTerm +
"&post_type=seriesplans";
let headers = new Headers({
Accept: "application/json",
"Content-Type": "application/json",
"User-Agent":
"'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/83.0.4103.116 Safari/537.36",
});
const res = await fetch(url, {
method: "GET",
headers: headers,
});
const body = await res.text();
const $ = cheerio.load(body);
const novels = [];
$("div.search_main_box_nu").each(function (res) {
const novelCover = $(this).find("img").attr("src");
const novelName = $(this).find(".search_title > a").text();
const genres = $(this)
.find(".search_genre")
.text()
.replace(/\s/g, ", ");
const novelSummary =
$(this).find(".search_genre")[0].nextSibling.nodeValue;
let chapterCount = "";
$(this)
.find(".search_stats > span")
.each(function () {
let detail = $(this).text();
if (detail.includes("Chapters")) {
chapterCount = detail.replace(" Chapters", "");
}
});
const novel = {
novelName,
novelCover,
genres,
novelSummary,
chapterCount,
};
novels.push(novel);
});
return novels;
};
export { scrapeTopNovels, scrapeSearchResults };
@@ -4,7 +4,7 @@ import { StyleSheet, View, FlatList, ActivityIndicator } from "react-native";
import { ProgressBar } from "react-native-paper";
import { useSelector } from "react-redux";
import { Searchbar } from "../../../components/Searchbar";
import { Searchbar } from "../../../components/Searchbar/Searchbar";
import EmptyView from "../../../components/EmptyView";
import { ScreenContainer } from "../../../components/Common";
@@ -74,9 +74,6 @@ const GlobalSearch = ({ route, navigation }) => {
},
]);
setLoading(false);
setProgress(
(progress) => progress + 1 / globalSearchSources.length
);
} catch (error) {
showToast(
globalSearchSources[i].sourceName + ": " + error.message
@@ -92,6 +89,9 @@ const GlobalSearch = ({ route, navigation }) => {
]);
setLoading(false);
}
setProgress(
(progress) => progress + 1 / globalSearchSources.length
);
}
};
@@ -1,7 +1,5 @@
import React from "react";
import { StyleSheet, View, Text, Image } from "react-native";
import { TouchableRipple } from "react-native-paper";
import { StyleSheet, View, Text, Image, Pressable } from "react-native";
const GlobalSearchNovelCover = ({
novel,
@@ -13,18 +11,21 @@ const GlobalSearchNovelCover = ({
const { novelName, novelCover } = novel;
return (
<View style={{ flex: 1 }}>
<TouchableRipple
borderless
centered
rippleColor={theme.rippleColor}
<View style={{ flex: 1, borderRadius: 6, overflow: "hidden" }}>
<Pressable
android_ripple={{ color: theme.rippleColor }}
style={styles.novelContainer}
onPress={onPress}
onLongPress={onLongPress}
>
<>
<Image
source={{ uri: novelCover }}
source={{
uri:
novelCover && !novelCover.startsWith("/")
? novelCover
: "https://github.com/LNReader/lnreader-sources/blob/main/src/coverNotAvailable.jpg?raw=true",
}}
style={[
styles.novelCover,
inLibrary && { opacity: 0.5 },
@@ -41,7 +42,7 @@ const GlobalSearchNovelCover = ({
{novelName}
</Text>
</>
</TouchableRipple>
</Pressable>
</View>
);
};
@@ -9,11 +9,7 @@ const GlobalSearchNovelList = ({ data, theme, library, navigation }) => {
(obj) => obj.novelUrl === novelUrl && obj.sourceId === sourceId
);
const navigateToNovel = (item) =>
navigation.navigate("Novel", {
...item,
sourceId: item.sourceId,
});
const navigateToNovel = (item) => navigation.push("Novel", item);
const renderItem = ({ item }) => (
<GlobalSearchNovelCover
@@ -61,9 +61,6 @@ const MigrationNovels = ({ navigation, route }) => {
},
]);
setLoading(false);
setProgress(
(progress) => progress + 1 / migrationSources.length
);
} catch (error) {
showToast(error.message);
@@ -78,6 +75,7 @@ const MigrationNovels = ({ navigation, route }) => {
]);
setLoading(false);
}
setProgress((progress) => progress + 1 / migrationSources.length);
}
};
@@ -12,30 +12,24 @@ const MigrationSourceCard = ({ item, theme, noOfNovels, onPress }) => {
rippleColor={theme.rippleColor}
>
<>
<Image
source={{ uri: icon }}
style={styles.sourceIcon}
resizeMode="contain"
/>
<Image source={{ uri: icon }} style={styles.sourceIcon} />
<View style={styles.sourceDetailsContainer}>
<View>
<Text
style={{
color: theme.textColorPrimary,
fontSize: 14,
}}
>
{sourceName} {` (${noOfNovels || 0})`}
</Text>
<Text
style={{
color: theme.textColorSecondary,
fontSize: 12,
}}
>
{sourceLanguage}
</Text>
</View>
<Text
style={{
color: theme.textColorPrimary,
fontSize: 14,
}}
>
{sourceName} {` (${noOfNovels || 0})`}
</Text>
<Text
style={{
color: theme.textColorSecondary,
fontSize: 12,
}}
>
{sourceLanguage}
</Text>
</View>
</>
</TouchableRipple>
+261 -161
View File
@@ -1,19 +1,20 @@
import React, { useState, useEffect, useRef } from "react";
import React, { useState, useEffect, useRef, useCallback } from "react";
import {
StyleSheet,
View,
Text,
ActivityIndicator,
StatusBar,
ScrollView,
TouchableWithoutFeedback,
} from "react-native";
import { useDispatch, useSelector } from "react-redux";
import { useDispatch } from "react-redux";
import { IconButton, Portal } from "react-native-paper";
import {
hideNavigationBar,
showNavigationBar,
} from "react-native-navigation-bar-color";
import WebView from "react-native-webview";
import {
getChapterFromDB,
@@ -30,9 +31,7 @@ import {
useTrackingStatus,
} from "../../hooks/reduxHooks";
import { updateChaptersRead } from "../../redux/tracker/tracker.actions";
import { insertHistoryAction } from "../../redux/history/history.actions";
import {
errorTextColor,
readerBackground,
readerLineHeight,
readerTextColor,
@@ -44,11 +43,16 @@ import { parseChapterNumber } from "../../services/updates";
import ChapterAppbar from "./components/ChapterAppbar";
import ReaderSheet from "./components/ReaderSheet";
import EmptyView from "../../components/EmptyView";
import FitImage from "react-native-fit-image";
import ChapterFooter from "./components/ChapterFooter";
import VerticalScrollbar from "./components/VerticalScrollbar";
import GestureRecognizer from "react-native-swipe-gestures";
import { LoadingScreen } from "../../components/LoadingScreen/LoadingScreen";
import { insertHistory } from "../../database/queries/HistoryQueries";
import { SET_LAST_READ } from "../../redux/preferences/preference.types";
import { htmlToText } from "../../sources/helpers/htmlToText";
import { setAppSettings } from "../../redux/settings/settings.actions";
import { cleanHtml } from "../../sources/helpers/cleanHtml";
const Chapter = ({ route, navigation }) => {
const {
@@ -61,19 +65,20 @@ const Chapter = ({ route, navigation }) => {
chapterName,
bookmark,
} = route.params;
let scrollViewRef = useRef(null);
let readerSheetRef = useRef(null);
const theme = useTheme();
const dispatch = useDispatch();
const reader = useReaderSettings();
const showScrollPercentage = useSelector(
(state) => state.settingsReducer.showScrollPercentage
);
const {
showScrollPercentage = true,
swipeGestures = true,
incognitoMode = false,
textSelectable = false,
useWebViewForChapter = false,
} = useSettings();
const [hidden, setHidden] = useState(true);
@@ -92,8 +97,6 @@ const Chapter = ({ route, navigation }) => {
const [contentSize, setContentSize] = useState(0);
const [images, setImages] = useState([]);
const getChapter = async (chapterId) => {
try {
if (chapterId) {
@@ -101,19 +104,17 @@ const Chapter = ({ route, navigation }) => {
if (chapterDownloaded) {
setChapter(chapterDownloaded);
parseImages(chapterDownloaded.chapterText);
} else {
const res = await fetchChapter(
sourceId,
novelUrl,
chapterUrl
);
parseImages(res.chapterText);
setChapter(res);
}
} else {
const res = await fetchChapter(sourceId, novelUrl, chapterUrl);
parseImages(res.chapterText);
setChapter(res);
}
@@ -125,17 +126,6 @@ const Chapter = ({ route, navigation }) => {
}
};
const parseImages = (chapterText) => {
let imageLinks = chapterText.match(/\[https.*?\]/g, "");
let chapterTextWithoutLinks;
if (imageLinks) {
imageLinks = imageLinks.map((link) => link.slice(1, -1));
setImages(imageLinks);
}
};
const [nextChapter, setNextChapter] = useState({});
const [prevChapter, setPrevChapter] = useState({});
@@ -149,25 +139,33 @@ const Chapter = ({ route, navigation }) => {
useEffect(() => {
getChapter(chapterId);
setPrevAndNextChap();
!incognitoMode && dispatch(insertHistoryAction(novelId, chapterId));
if (!incognitoMode) {
insertHistory(novelId, chapterId);
dispatch({
type: SET_LAST_READ,
payload: { novelId, chapterId },
});
}
return () => {
StatusBar.setHidden(false);
showNavigationBar();
};
}, []);
const isCloseToBottom = ({
layoutMeasurement,
contentOffset,
contentSize,
}) => {
const paddingToBottom = 40;
return (
layoutMeasurement.height + contentOffset.y >=
contentSize.height - paddingToBottom
);
};
useEffect(() => {
setPrevAndNextChap();
}, [chapter]);
const isCloseToBottom = useCallback(
({ layoutMeasurement, contentOffset, contentSize }) => {
const paddingToBottom = 40;
return (
layoutMeasurement.height + contentOffset.y >=
contentSize.height - paddingToBottom
);
},
[]
);
const updateTracker = () => {
const chapterNumber = parseChapterNumber(chapterName);
@@ -185,7 +183,7 @@ const Chapter = ({ route, navigation }) => {
);
};
const onScroll = ({ nativeEvent }) => {
const onScroll = useCallback(({ nativeEvent }) => {
const offsetY = nativeEvent.contentOffset.y;
const position =
nativeEvent.contentOffset.y + nativeEvent.layoutMeasurement.height;
@@ -205,11 +203,15 @@ const Chapter = ({ route, navigation }) => {
dispatch(markChapterReadAction(chapterId, novelId));
updateTracker();
}
};
}, []);
const scrollToSavedProgress = (event) => {
const setImmersiveMode = () => {
StatusBar.setHidden(true);
hideNavigationBar();
};
const scrollToSavedProgress = useCallback((event) => {
setImmersiveMode();
if (position && firstLayout) {
position.percentage < 100 &&
scrollViewRef.current.scrollTo({
@@ -219,25 +221,19 @@ const Chapter = ({ route, navigation }) => {
});
setFirstLayout(false);
}
};
}, []);
const hideHeader = () => {
if (!hidden) {
StatusBar.setHidden(true);
hideNavigationBar();
}
!hidden && setImmersiveMode();
setHidden(!hidden);
};
const readerStyles = [
{
paddingVertical: 16,
paddingBottom: 32,
fontSize: reader.textSize,
color: reader.textColor || readerTextColor(reader.theme),
lineHeight: readerLineHeight(reader.textSize, reader.lineHeight),
textAlign: reader.textAlign,
paddingHorizontal: `${reader.padding}%`,
},
reader.fontFamily && {
fontFamily: reader.fontFamily,
@@ -261,7 +257,7 @@ const Chapter = ({ route, navigation }) => {
novelName,
bookmark: prevChapter.bookmark,
})
: showToast("'There's no previous chapter");
: showToast("There's no previous chapter");
const navigateToNextChapter = () =>
nextChapter
@@ -275,138 +271,246 @@ const Chapter = ({ route, navigation }) => {
novelName,
bookmark: nextChapter.bookmark,
})
: showToast("'There's no next chapter");
: showToast("There's no next chapter");
const enableSwipeGestures = () => {
dispatch(setAppSettings("swipeGestures", !swipeGestures));
showToast(
swipeGestures ? "Swipe gestures disabled" : "Swipe gestured enabled"
);
};
const enableWebView = () =>
dispatch(setAppSettings("useWebViewForChapter", !useWebViewForChapter));
return (
<>
<ChapterAppbar
novelName={novelName}
chapterName={chapterName}
chapterId={chapterId}
bookmark={bookmark}
readerSheetRef={readerSheetRef}
hide={hidden}
navigation={navigation}
dispatch={dispatch}
/>
<ScrollView
ref={(ref) => (scrollViewRef.current = ref)}
contentContainerStyle={[
styles.screenContainer,
{ backgroundColor: readerBackground(reader.theme) },
]}
onScroll={onScroll}
onContentSizeChange={(x, y) => setContentSize(y)}
showsVerticalScrollIndicator={false}
>
<>
<ChapterAppbar
novelName={novelName}
chapterName={chapterName}
chapterId={chapterId}
bookmark={bookmark}
readerSheetRef={readerSheetRef}
hide={hidden}
navigation={navigation}
dispatch={dispatch}
/>
<GestureRecognizer
onSwipeRight={swipeGestures && navigateToPrevChapter}
onSwipeLeft={swipeGestures && navigateToNextChapter}
config={config}
style={{ flex: 1 }}
>
{error ? (
<View style={{ flex: 1, justifyContent: "center" }}>
<EmptyView
icon="(-_-;)・・・"
description={error}
style={{ color: errorTextColor(reader.theme) }}
>
<IconButton
icon="reload"
size={25}
style={{ margin: 0, marginTop: 16 }}
color={errorTextColor(reader.theme)}
onPress={() => {
getChapter(chapterId);
setLoading(true);
setError();
}}
/>
<Text
<ScrollView
ref={(ref) => (scrollViewRef.current = ref)}
contentContainerStyle={[
styles.screenContainer,
{ backgroundColor: readerBackground(reader.theme) },
]}
onScroll={onScroll}
onContentSizeChange={(x, y) => setContentSize(y)}
showsVerticalScrollIndicator={false}
>
{error ? (
<View style={{ flex: 1, justifyContent: "center" }}>
<EmptyView
icon="Σ(ಠ_ಠ)"
description={error}
style={{
color: errorTextColor(reader.theme),
color:
reader.textColor ||
readerTextColor(reader.theme),
}}
>
Retry
</Text>
</EmptyView>
</View>
) : loading ? (
<View style={{ flex: 1, justifyContent: "center" }}>
<ActivityIndicator
size={50}
color={theme.colorAccent}
/>
</View>
) : (
<View style={{ flex: 1 }}>
{images.length > 0 &&
images.map((image, index) => (
<FitImage
source={{ uri: image }}
key={index}
<IconButton
icon="reload"
size={25}
style={{ margin: 0, marginTop: 16 }}
color={
reader.textColor ||
readerTextColor(reader.theme)
}
onPress={() => {
getChapter(chapterId);
setLoading(true);
setError();
}}
/>
))}
<Text
style={readerStyles}
onLayout={scrollToSavedProgress}
selectable={textSelectable}
<Text
style={{
color:
reader.textColor ||
readerTextColor(reader.theme),
}}
>
Retry
</Text>
</EmptyView>
</View>
) : loading ? (
<LoadingScreen theme={theme} />
) : (
<TouchableWithoutFeedback
style={{ flex: 1 }}
onPress={hideHeader}
onLayout={scrollToSavedProgress}
>
{chapter.chapterText
.trim()
.replace(/\[https.*?\]/g, "")}
</Text>
</View>
)}
{useWebViewForChapter ? (
<WebView
style={{
backgroundColor: readerBackground(
reader.theme
),
paddingBottom: 24,
}}
originWhitelist={["*"]}
scalesPageToFit={true}
showsVerticalScrollIndicator={false}
onScroll={onScroll}
onNavigationStateChange={async ({
url,
}) => {
if (
(sourceId === 50 ||
sourceId == 62) &&
url !== "about:blank"
) {
setLoading(true);
const res = await fetchChapter(
sourceId,
novelUrl,
url
);
setChapter(res);
setLoading(false);
}
}}
source={{
html: `
<html>
<head>
<meta name="viewport" content="width=device-width, initial-scale=1.0, maximum-scale=1.0">
<style>
html {
overflow-x: hidden;
padding-top: ${StatusBar.currentHeight};
}
body {
padding-left: ${reader.padding}%;
padding-right: ${reader.padding}%;
font-size: ${reader.textSize}px;
color: ${reader.textColor};
text-align: ${reader.textAlign};
line-height: ${reader.lineHeight};
font-family: ${reader.fontFamily};
}
hr {
margin-top: 20px;
margin-bottom: 20px;
}
a {
color: ${theme.colorAccent};
}
img {
display: block;
width: auto;
height: auto;
max-width: 100%;
}
@font-face {
font-family: ${reader.fontFamily};
src: url("file:///android_asset/fonts/${
reader.fontFamily
}.ttf");
}
${reader.customCSS}
</style>
</head>
<body>
${cleanHtml(chapter.chapterText)}
</body>
</html>
`,
}}
onScroll={onScroll}
/>
) : (
<View
style={{
flex: 1,
paddingVertical: 16,
paddingBottom: 32,
paddingHorizontal: `${reader.padding}%`,
paddingTop: StatusBar.currentHeight,
}}
>
<Text
style={readerStyles}
selectable={textSelectable}
>
{htmlToText(chapter.chapterText)}
</Text>
</View>
)}
</TouchableWithoutFeedback>
)}
</ScrollView>
</GestureRecognizer>
<Portal>
<ReaderSheet
theme={theme}
reader={reader}
dispatch={dispatch}
navigation={navigation}
bottomSheetRef={readerSheetRef}
selectText={textSelectable}
useWebViewForChapter={useWebViewForChapter}
showScrollPercentage={showScrollPercentage}
/>
</Portal>
</ScrollView>
<VerticalScrollbar
theme={theme}
hide={hidden}
setLoading={setLoading}
contentSize={contentSize}
scrollViewRef={scrollViewRef}
scrollPercentage={scrollPercentage}
setScrollPercentage={setScrollPercentage}
/>
<ChapterFooter
theme={theme}
swipeGestures={swipeGestures}
dispatch={dispatch}
nextChapter={nextChapter}
prevChapter={prevChapter}
hide={hidden}
navigateToNextChapter={navigateToNextChapter}
navigateToPrevChapter={navigateToPrevChapter}
readerSheetRef={readerSheetRef}
scrollViewRef={scrollViewRef}
/>
{showScrollPercentage && (
<Text
style={[
styles.scrollPercentage,
{
color: reader.textColor,
backgroundColor: readerBackground(reader.theme),
},
]}
>
{scrollPercentage + "%"}
</Text>
)}
{!useWebViewForChapter && (
<VerticalScrollbar
theme={theme}
hide={hidden}
setLoading={setLoading}
contentSize={contentSize}
scrollViewRef={scrollViewRef}
scrollPercentage={scrollPercentage}
setScrollPercentage={setScrollPercentage}
/>
)}
<ChapterFooter
theme={theme}
swipeGestures={swipeGestures}
enableSwipeGestures={enableSwipeGestures}
enableWebView={enableWebView}
dispatch={dispatch}
nextChapter={nextChapter}
prevChapter={prevChapter}
hide={hidden}
useWebViewForChapter={useWebViewForChapter}
navigateToNextChapter={navigateToNextChapter}
navigateToPrevChapter={navigateToPrevChapter}
readerSheetRef={readerSheetRef}
scrollViewRef={scrollViewRef}
/>
{showScrollPercentage && (
<Text
style={[
styles.scrollPercentage,
{
color: reader.textColor,
backgroundColor: readerBackground(reader.theme),
},
]}
>
{scrollPercentage + "%"}
</Text>
)}
</>
</>
);
};
@@ -414,10 +518,7 @@ const Chapter = ({ route, navigation }) => {
export default Chapter;
const styles = StyleSheet.create({
screenContainer: {
flexGrow: 1,
paddingTop: StatusBar.currentHeight,
},
screenContainer: { flexGrow: 1 },
scrollPercentage: {
width: "100%",
position: "absolute",
@@ -426,7 +527,6 @@ const styles = StyleSheet.create({
textAlign: "center",
textAlignVertical: "center",
alignSelf: "center",
fontFamily: "noto-sans",
fontWeight: "bold",
zIndex: 1,
},
+4 -4
View File
@@ -29,7 +29,7 @@ const ReaderSeekbar = ({
<View style={styles.verticalSliderContainer}>
<Text
style={{
color: theme.textColorPrimary,
color: "#FFFFFF",
marginLeft: 16,
transform: [{ rotate: "-90deg" }],
}}
@@ -52,7 +52,7 @@ const ReaderSeekbar = ({
/>
<Text
style={{
color: theme.textColorPrimary,
color: "#FFFFFF",
marginRight: 16,
transform: [{ rotate: "-90deg" }],
}}
@@ -66,7 +66,7 @@ const ReaderSeekbar = ({
<View style={styles.horizontalSliderContainer}>
<Text
style={{
color: theme.textColorPrimary,
color: "#FFFFFF",
marginLeft: 16,
}}
>
@@ -88,7 +88,7 @@ const ReaderSeekbar = ({
/>
<Text
style={{
color: theme.textColorPrimary,
color: "#FFFFFF",
marginRight: 16,
}}
>
@@ -1,11 +1,7 @@
import React, { useEffect, useState } from "react";
import { Dimensions, View } from "react-native";
import React, { useState } from "react";
import { View } from "react-native";
import { Appbar } from "react-native-paper";
import {
getNextChapterFromDB,
getPrevChapterFromDB,
} from "../../../database/queries/ChapterQueries";
import { bookmarkChapterAction } from "../../../redux/novel/novel.actions";
const ChapterAppbar = ({
@@ -27,7 +23,7 @@ const ChapterAppbar = ({
style={{
position: "absolute",
flex: 1,
backgroundColor: "rgba(0,0,0,0.6)",
backgroundColor: "rgba(0,0,0,0.7)",
width: "100%",
top: 0,
zIndex: 1,
+24 -22
View File
@@ -1,25 +1,20 @@
import React, { useState, useEffect } from "react";
import { Dimensions, Pressable, StyleSheet, Text, View } from "react-native";
import React from "react";
import { Pressable, StyleSheet, View } from "react-native";
import { IconButton } from "react-native-paper";
import {
getNextChapterFromDB,
getPrevChapterFromDB,
} from "../../../database/queries/ChapterQueries";
import { useSettings } from "../../../hooks/reduxHooks";
import { showToast } from "../../../hooks/showToast";
import { setAppSettings } from "../../../redux/settings/settings.actions";
const ChapterFooter = ({
hide,
theme,
swipeGestures,
dispatch,
readerSheetRef,
scrollViewRef,
navigateToNextChapter,
navigateToPrevChapter,
nextChapter,
prevChapter,
useWebViewForChapter,
enableSwipeGestures,
enableWebView,
}) => {
const rippleConfig = {
color: theme.rippleColor,
@@ -27,13 +22,6 @@ const ChapterFooter = ({
radius: 50,
};
const enableSwipeGestures = () => {
dispatch(setAppSettings("swipeGestures", !swipeGestures));
showToast(
swipeGestures ? "Swipe gestures disabled" : "Swipe gestured enabled"
);
};
if (hide) {
return null;
} else {
@@ -44,7 +32,7 @@ const ChapterFooter = ({
zIndex: 2,
bottom: 0,
width: "100%",
backgroundColor: "rgba(0,0,0,0.6)",
backgroundColor: "rgba(0,0,0,0.7)",
flexDirection: "row",
}}
>
@@ -72,17 +60,31 @@ const ChapterFooter = ({
color="#FFFFFF"
/>
</Pressable>
<Pressable
{!useWebViewForChapter && (
<Pressable
android_ripple={rippleConfig}
style={styles.buttonStyles}
onPress={() => scrollViewRef.current.scrollTo({})}
>
<IconButton
icon="format-vertical-align-top"
size={26}
color="#FFFFFF"
/>
</Pressable>
)}
{/* <Pressable
android_ripple={rippleConfig}
style={styles.buttonStyles}
onPress={() => scrollViewRef.current.scrollTo({})}
onPress={enableWebView}
>
<IconButton
icon="format-vertical-align-top"
icon="language-html5"
disabled={!useWebViewForChapter}
size={26}
color="#FFFFFF"
/>
</Pressable>
</Pressable> */}
<Pressable
android_ripple={rippleConfig}
style={styles.buttonStyles}
+440 -396
View File
@@ -1,356 +1,314 @@
import React, { useState } from "react";
import { StyleSheet, View, Text, Dimensions, Pressable } from "react-native";
import { ToggleButton, IconButton, Menu, Switch } from "react-native-paper";
import {
StyleSheet,
View,
Text,
Dimensions,
Pressable,
ScrollView,
useWindowDimensions,
Animated,
} from "react-native";
import { TabView, SceneMap, TabBar } from "react-native-tab-view";
import Slider from "@react-native-community/slider";
import { IconButton, Menu, Switch } from "react-native-paper";
import Bottomsheet from "rn-sliding-up-panel";
import BottomSheetHandle from "../../../components/BottomSheetHandle";
import { Row } from "../../../components/Common";
import { fonts } from "../../../services/utils/constants";
import {
setAppSettings,
setReaderSettings,
} from "../../../redux/settings/settings.actions";
import BottomSheetHandle from "../../../components/BottomSheetHandle";
import { Row } from "../../../components/Common";
import {
ToggleButton,
ToggleColorButton,
} from "../../../components/Common/ToggleButton";
const ReaderSettingTitle = ({ title }) => (
<Text style={styles.title}>{title}</Text>
);
const ReaderSheet = ({
theme,
reader,
dispatch,
navigation,
bottomSheetRef,
selectText,
useWebViewForChapter,
showScrollPercentage,
}) => {
const [animatedValue] = useState(new Animated.Value(0));
const [fontMenu, setFontMenu] = useState(false);
const openFontMenu = () => setFontMenu(true);
const closeFontMenu = () => setFontMenu(false);
const ReaderSettingTitle = ({ title }) => (
<Text style={styles.title}>{title}</Text>
);
const presetThemes = [
{ value: 2, backgroundColor: "#f5f5fa", textColor: "#111111" },
{ value: 3, backgroundColor: "#F7DFC6", textColor: "#593100" },
{ value: 6, backgroundColor: "#dce5e2", textColor: "#000000" },
{ value: 4, backgroundColor: "#292832", textColor: "#CCCCCC" },
{
value: 1,
backgroundColor: "#000000",
textColor: "rgba(255,255,255,0.7)",
},
];
const textColor = {
1: "rgba(255,255,255,0.7)",
2: "#000000",
3: "#000000",
4: "#FFFFFF",
};
const textAlignments = [
{ value: "left", icon: "format-align-left" },
{ value: "center", icon: "format-align-center" },
{ value: "justify", icon: "format-align-justify" },
{ value: "right", icon: "format-align-right" },
];
return (
<Bottomsheet
ref={bottomSheetRef}
draggableRange={{ top: 470, bottom: 0 }}
snappingPoints={[0, 470]}
showBackdrop={true}
backdropOpacity={0}
>
<View style={styles.contentContainer}>
<BottomSheetHandle theme={theme} />
<View style={styles.readerSettingsContainer}>
<ReaderSettingTitle title="Text Size" />
<Slider
style={{
width: Dimensions.get("window").width,
height: 40,
}}
value={reader.textSize}
minimumValue={12}
maximumValue={20}
step={1}
minimumTrackTintColor={theme.colorAccent}
maximumTrackTintColor="#000000"
thumbTintColor={theme.colorAccent}
onSlidingComplete={(value) =>
dispatch(setReaderSettings("textSize", value))
}
/>
<Row
style={{
justifyContent: "space-between",
width: "100%",
paddingHorizontal: 16,
paddingVertical: 8,
}}
>
<ReaderSettingTitle title="Reader Theme" />
<Row>
<ToggleButton.Row
onValueChange={(value) => {
dispatch(
setReaderSettings(
"theme",
value ?? reader.theme
)
);
dispatch(
setReaderSettings(
"textColor",
textColor[value] ?? reader.textColor
)
);
}}
value={reader.theme}
>
<ToggleButton
icon={reader.theme === 1 && "check"}
color="#FFFFFF"
value={1}
style={{
backgroundColor: "#000000",
marginHorizontal: 10,
borderWidth: 0,
borderRadius: 50,
borderTopRightRadius: 50,
borderBottomRightRadius: 50,
const FirstRoute = () => (
<View style={{ flex: 1, paddingVertical: 16 }}>
<View
style={{
flexDirection: "row",
justifyContent: "space-between",
alignItems: "center",
}}
>
<Text
style={{
color: theme.textColorSecondary,
paddingHorizontal: 16,
textAlign: "center",
}}
>
Text size
</Text>
<Slider
style={{
flex: 1,
height: 40,
}}
value={reader.textSize}
minimumValue={12}
maximumValue={20}
step={1}
minimumTrackTintColor={theme.colorAccent}
maximumTrackTintColor="#000000"
thumbTintColor={theme.colorAccent}
onSlidingComplete={(value) =>
dispatch(setReaderSettings("textSize", value))
}
/>
</View>
<View style={{ paddingLeft: 16, paddingRight: 8 }}>
<Row
style={{
justifyContent: "space-between",
marginVertical: 6,
}}
>
<Text style={{ color: theme.textColorSecondary }}>
Color
</Text>
<View style={{ marginLeft: 16 }}>
<ScrollView horizontal={true}>
{presetThemes.map((item, index) => (
<ToggleColorButton
key={index}
selected={reader.theme === item.value}
backgroundColor={item.backgroundColor}
textColor={item.textColor}
theme={theme}
onPress={() => {
dispatch(
setReaderSettings(
"theme",
item.value
)
);
dispatch(
setReaderSettings(
"textColor",
item.textColor
)
);
}}
/>
<ToggleButton
icon={reader.theme === 4 && "check"}
color="#FFFFFF"
value={4}
style={{
backgroundColor: "#444444",
marginHorizontal: 10,
borderWidth: 0,
borderRadius: 50,
borderTopRightRadius: 50,
borderBottomRightRadius: 50,
}}
/>
<ToggleButton
icon={reader.theme === 2 && "check"}
color="#000000"
value={2}
style={{
backgroundColor: "#FFFFFF",
marginHorizontal: 10,
borderRadius: 50,
}}
/>
<ToggleButton
icon={reader.theme === 3 && "check"}
color="#000000"
value={3}
style={{
backgroundColor: "#F4ECD8",
marginHorizontal: 10,
borderRadius: 50,
borderTopLeftRadius: 50,
borderBottomLeftRadius: 50,
}}
/>
</ToggleButton.Row>
</Row>
</Row>
<Row
style={{
justifyContent: "space-between",
width: "100%",
paddingHorizontal: 16,
paddingVertical: 8,
}}
>
<ReaderSettingTitle title="Text Align" />
<Row>
<ToggleButton.Row
onValueChange={(value) =>
))}
</ScrollView>
</View>
</Row>
<Row
style={{
justifyContent: "space-between",
marginVertical: 6,
}}
>
<Text style={{ color: theme.textColorSecondary }}>
Text align
</Text>
<Row>
{textAlignments.map((item) => (
<ToggleButton
key={item.value}
selected={item.value === reader.textAlign}
icon={item.icon}
theme={theme}
onPress={() =>
dispatch(
setReaderSettings(
"textAlign",
value ?? reader.textAlign
item.value
)
)
}
value={reader.textAlign}
>
<ToggleButton
icon="format-align-left"
color={
reader.textAlign === "left"
? theme.colorAccent
: "#FFFFFF"
}
value="left"
style={{
backgroundColor: "transparent",
marginHorizontal: 10,
borderWidth: 0,
borderTopRightRadius: 4,
borderBottomRightRadius: 4,
}}
/>
<ToggleButton
icon="format-align-justify"
color={
reader.textAlign === "justify"
? theme.colorAccent
: "#FFFFFF"
}
value="justify"
style={{
backgroundColor: "transparent",
marginHorizontal: 10,
borderWidth: 0,
borderRadius: 4,
}}
/>
<ToggleButton
icon="format-align-right"
color={
reader.textAlign === "right"
? theme.colorAccent
: "#FFFFFF"
}
value="right"
style={{
backgroundColor: "transparent",
marginHorizontal: 10,
borderWidth: 0,
borderTopLeftRadius: 4,
borderBottomLeftRadius: 4,
}}
/>
</ToggleButton.Row>
</Row>
/>
))}
</Row>
<Row
style={{
justifyContent: "space-between",
width: "100%",
paddingHorizontal: 16,
paddingVertical: 8,
}}
>
<ReaderSettingTitle title="Padding" />
<Row>
<IconButton
icon="minus"
color={theme.colorAccent}
size={26}
disabled={reader.padding <= 0 ? true : false}
onPress={() =>
dispatch(
setReaderSettings(
"padding",
reader.padding - 1
)
</Row>
<Row
style={{
justifyContent: "space-between",
marginVertical: 6,
}}
>
<Text style={{ color: theme.textColorSecondary }}>
Padding
</Text>
<Row>
<IconButton
icon="minus"
color={theme.colorAccent}
size={26}
disabled={reader.padding <= 0 ? true : false}
onPress={() =>
dispatch(
setReaderSettings(
"padding",
reader.padding - 1
)
}
style={{ marginVertical: 0 }}
/>
<Text
style={{
color: "#FFFFFF",
paddingHorizontal: 24,
}}
>
{`${reader.padding}%`}
</Text>
<IconButton
icon="plus"
color={theme.colorAccent}
size={26}
disabled={reader.padding >= 10 ? true : false}
onPress={() =>
dispatch(
setReaderSettings(
"padding",
reader.padding + 1
)
)
}
style={{ marginVertical: 0 }}
/>
</Row>
</Row>
<Row
style={{
justifyContent: "space-between",
width: "100%",
paddingHorizontal: 16,
paddingVertical: 8,
}}
>
<ReaderSettingTitle title="Line Height" />
<Row>
<IconButton
icon="minus"
color={theme.colorAccent}
size={26}
disabled={
reader.lineHeight <= 1.3 ? true : false
}
onPress={() =>
dispatch(
setReaderSettings(
"lineHeight",
reader.lineHeight - 0.1
)
)
}
style={{ marginVertical: 0 }}
/>
<Text
style={{
color: "#FFFFFF",
paddingHorizontal: 24,
}}
>
{`${Math.round(reader.lineHeight * 10) / 10}%`}
</Text>
<IconButton
icon="plus"
color={theme.colorAccent}
size={26}
disabled={reader.lineHeight >= 2 ? true : false}
onPress={() =>
dispatch(
setReaderSettings(
"lineHeight",
reader.lineHeight + 0.1
)
)
}
style={{ marginVertical: 0 }}
/>
</Row>
</Row>
<Pressable
style={{
flexDirection: "row",
alignItems: "center",
justifyContent: "space-between",
width: "100%",
paddingHorizontal: 16,
paddingVertical: 8,
}}
android_ripple={{ color: theme.colorAccent }}
onPress={openFontMenu}
>
<ReaderSettingTitle title="Font Style" />
<Menu
visible={fontMenu}
onDismiss={closeFontMenu}
contentStyle={{ backgroundColor: theme.menuColor }}
anchor={
<Text
style={{
color: "white",
fontSize: 16,
fontFamily: reader.fontFamily,
paddingHorizontal: 16,
}}
>
{fonts.find(
(font) =>
font.fontFamily ===
reader.fontFamily
).name || "Default"}
</Text>
)
}
style={{ marginVertical: 0 }}
/>
<Text
style={{
color: "#FFFFFF",
paddingHorizontal: 24,
}}
>
{fonts.map((font) => (
<Menu.Item
key={font.fontFamily}
{`${reader.padding}%`}
</Text>
<IconButton
icon="plus"
color={theme.colorAccent}
size={26}
disabled={reader.padding >= 10 ? true : false}
onPress={() =>
dispatch(
setReaderSettings(
"padding",
reader.padding + 1
)
)
}
style={{ marginVertical: 0 }}
/>
</Row>
</Row>
<Row
style={{
justifyContent: "space-between",
marginVertical: 6,
}}
>
<Text style={{ color: theme.textColorSecondary }}>
Line height
</Text>
<Row>
<IconButton
icon="minus"
color={theme.colorAccent}
size={26}
disabled={reader.lineHeight <= 1.3 ? true : false}
onPress={() =>
dispatch(
setReaderSettings(
"lineHeight",
reader.lineHeight - 0.1
)
)
}
style={{ marginVertical: 0 }}
/>
<Text
style={{
color: "#FFFFFF",
paddingHorizontal: 24,
}}
>
{`${Math.round(reader.lineHeight * 10) / 10}%`}
</Text>
<IconButton
icon="plus"
color={theme.colorAccent}
size={26}
disabled={reader.lineHeight >= 2 ? true : false}
onPress={() =>
dispatch(
setReaderSettings(
"lineHeight",
reader.lineHeight + 0.1
)
)
}
style={{ marginVertical: 0 }}
/>
</Row>
</Row>
<Row
style={{
justifyContent: "space-between",
marginVertical: 6,
}}
>
<Text
style={{
color: theme.textColorSecondary,
marginRight: 16,
}}
>
Font style
</Text>
<ScrollView
horizontal={true}
contentContainerStyle={{
alignItems: "center",
}}
>
{fonts.map((font) => (
<View
style={[
{
borderRadius: 8,
overflow: "hidden",
marginHorizontal: 6,
},
reader.fontFamily === font.fontFamily && {
backgroundColor: theme.rippleColor,
},
]}
>
<Pressable
style={{
borderRadius: 8,
paddingHorizontal: 10,
paddingVertical: 4,
}}
onPress={() =>
dispatch(
setReaderSettings(
@@ -359,93 +317,171 @@ const ReaderSheet = ({
)
)
}
icon={
font.fontFamily === reader.fontFamily &&
"check"
}
title={font.name}
style={{ backgroundColor: theme.menuColor }}
titleStyle={{
color: theme.textColorPrimary,
android_ripple={{
color: theme.rippleColor,
}}
/>
))}
</Menu>
</Pressable>
<Pressable
style={{
flexDirection: "row",
alignItems: "center",
justifyContent: "space-between",
width: "100%",
paddingHorizontal: 16,
paddingVertical: 8,
}}
android_ripple={{ color: theme.colorAccent }}
onPress={() =>
dispatch(
setAppSettings(
"showScrollPercentage",
!showScrollPercentage
)
>
<Text
style={{
fontSize: 16,
fontFamily: font.fontFamily,
color: theme.textColorPrimary,
}}
>
{font.name}
</Text>
</Pressable>
</View>
))}
</ScrollView>
</Row>
</View>
</View>
);
const SecondRoute = () => (
<View style={{ flex: 1 }}>
<Pressable
style={styles.switchStyle}
android_ripple={{ color: theme.rippleColor }}
onPress={() =>
dispatch(
setAppSettings(
"useWebViewForChapter",
!useWebViewForChapter
)
)
}
>
<Text
style={{
color: theme.textColorSecondary,
}}
>
Use WebView
</Text>
<Switch
value={useWebViewForChapter}
onValueChange={() =>
dispatch(
setAppSettings(
"useWebViewForChapter",
!useWebViewForChapter
)
}
>
<ReaderSettingTitle title="Show Progress Percentage" />
<Switch
value={showScrollPercentage}
onValueChange={() =>
dispatch(
setAppSettings(
"showScrollPercentage",
!showScrollPercentage
)
)
}
color={theme.colorAccent}
/>
</Pressable>
<Pressable
)
}
color={theme.colorAccent}
/>
</Pressable>
<Pressable
style={styles.switchStyle}
android_ripple={{ color: theme.rippleColor }}
onPress={() =>
dispatch(
setAppSettings(
"showScrollPercentage",
!showScrollPercentage
)
)
}
>
<Text
style={{
color: theme.textColorSecondary,
}}
>
Show progress percentage
</Text>
<Switch
value={showScrollPercentage}
onValueChange={() =>
dispatch(
setAppSettings(
"showScrollPercentage",
!showScrollPercentage
)
)
}
color={theme.colorAccent}
/>
</Pressable>
{!useWebViewForChapter && (
<Pressable
style={styles.switchStyle}
android_ripple={{ color: theme.rippleColor }}
onPress={() =>
dispatch(setAppSettings("textSelectable", !selectText))
}
>
<Text
style={{
flexDirection: "row",
alignItems: "center",
justifyContent: "space-between",
width: "100%",
paddingHorizontal: 16,
paddingVertical: 8,
color: theme.textColorSecondary,
}}
android_ripple={{ color: theme.colorAccent }}
onPress={() =>
>
Select text
</Text>
<Switch
value={selectText}
onValueChange={() =>
dispatch(
setAppSettings("textSelectable", !selectText)
)
}
>
<View>
<ReaderSettingTitle title="Select Text " />
<Text
style={{
fontSize: 14,
color: "white",
}}
>
(Justify does not work with selectable text)
</Text>
</View>
<Switch
value={selectText}
onValueChange={() =>
dispatch(
setAppSettings(
"textSelectable",
!selectText
)
)
}
color={theme.colorAccent}
/>
</Pressable>
</View>
color={theme.colorAccent}
/>
</Pressable>
)}
</View>
);
const renderScene = SceneMap({
first: FirstRoute,
second: SecondRoute,
});
const layout = useWindowDimensions();
const [index, setIndex] = useState(0);
const [routes] = useState([
{ key: "first", title: "Display" },
{ key: "second", title: "General" },
]);
const renderTabBar = (props) => (
<TabBar
{...props}
indicatorStyle={{ backgroundColor: theme.colorAccent }}
style={{ backgroundColor: theme.colorPrimary }}
renderLabel={({ route, focused, color }) => (
<Text style={{ color }}>{route.title}</Text>
)}
inactiveColor={theme.textColorSecondary}
activeColor={theme.colorAccent}
pressColor={theme.rippleColor}
/>
);
return (
<Bottomsheet
animatedValue={animatedValue}
ref={bottomSheetRef}
draggableRange={{ top: 400, bottom: 0 }}
snappingPoints={[0, 400]}
>
<View
style={[
styles.contentContainer,
{ backgroundColor: theme.colorPrimaryDark },
]}
>
<TabView
navigationState={{ index, routes }}
renderTabBar={renderTabBar}
renderScene={renderScene}
onIndexChange={setIndex}
initialLayout={{ width: layout.width }}
style={{ borderTopRightRadius: 8, borderTopLeftRadius: 8 }}
/>
</View>
</Bottomsheet>
);
@@ -469,4 +505,12 @@ const styles = StyleSheet.create({
color: "#FFFFFF",
fontWeight: "bold",
},
switchStyle: {
flexDirection: "row",
alignItems: "center",
justifyContent: "space-between",
width: "100%",
paddingHorizontal: 16,
paddingVertical: 12,
},
});
@@ -11,11 +11,9 @@ const VerticalScrollbar = ({
scrollPercentage,
setLoading,
scrollViewRef,
setScrollPercentage,
}) => {
const onSlidingComplete = (value) => {
setLoading(true);
// setScrollPercentage(value);
scrollViewRef.current.scrollTo({
x: 0,
y: Math.round((value * contentSize) / 100),
@@ -31,7 +29,7 @@ const VerticalScrollbar = ({
<View style={styles.verticalSliderContainer}>
<Text
style={{
color: theme.textColorPrimary,
color: "#FFFFFF",
marginLeft: 16,
transform: [{ rotate: "-90deg" }],
}}
@@ -50,11 +48,11 @@ const VerticalScrollbar = ({
onSlidingComplete={onSlidingComplete}
thumbTintColor={theme.colorAccent}
minimumTrackTintColor={theme.colorAccent}
maximumTrackTintColor={theme.textColorPrimary}
maximumTrackTintColor="#FFFFFF"
/>
<Text
style={{
color: theme.textColorPrimary,
color: "#FFFFFF",
marginRight: 16,
transform: [{ rotate: "-90deg" }],
}}
@@ -68,7 +66,7 @@ const VerticalScrollbar = ({
<View style={styles.horizontalSliderContainer}>
<Text
style={{
color: theme.textColorPrimary,
color: "#FFFFFF",
marginLeft: 16,
}}
>
@@ -90,7 +88,7 @@ const VerticalScrollbar = ({
/>
<Text
style={{
color: theme.textColorPrimary,
color: "#FFFFFF",
marginRight: 16,
}}
>
@@ -0,0 +1,27 @@
import React from "react";
import WebView from "react-native-webview";
export const WebViewReader = ({ style, css, text, onScroll }) => (
<WebView
style={style}
originWhitelist={["*"]}
scalesPageToFit={true}
showsVerticalScrollIndicator={false}
onScroll={onScroll}
source={{
html: `
<html>
<head>
<meta name="viewport" content="width=device-width, initial-scale=1.0, maximum-scale=1.0">
<style>
${css}
</style>
</head>
<body>
${text}
</body>
</html>
`,
}}
/>
);
+14 -12
View File
@@ -2,18 +2,26 @@ export const readerBackground = (val) => {
const backgroundColor = {
1: "#000000",
2: "#FFFFFF",
3: "#F4ECD8",
4: "#444444",
3: "#F7DFC6",
4: "#292832",
5: "#2B2C30",
6: "#dce5e2",
};
return backgroundColor[val] ?? val;
return backgroundColor[val] || val;
};
export const readerTextColor = (val) => {
const textColor =
val === 1 ? "rgba(255,255,255,0.7)" : val === 4 ? "#FFFFFF" : "#000000";
const textColor = {
1: "rgba(255,255,255,0.7)",
2: "#111111",
3: "#593100",
4: "#CCCCCC",
5: "#CCCCCC",
6: "#000000",
};
return textColor ?? val;
return textColor[val] || val;
};
export const readerLineHeight = (fontSize, lineHeightMultiplier) => {
@@ -23,9 +31,3 @@ export const readerLineHeight = (fontSize, lineHeightMultiplier) => {
return lineHeight;
};
export const errorTextColor = (val) => {
const textColor = val === 1 ? "rgba(255,255,255,0.7)" : "rgba(0,0,0,0.54)";
return textColor;
};
+37 -25
View File
@@ -1,27 +1,30 @@
import React, { useEffect, useState } from "react";
import { StyleSheet, View, ActivityIndicator, Text } from "react-native";
import { StyleSheet, View, ActivityIndicator } from "react-native";
import * as WebBrowser from "expo-web-browser";
import NovelCover from "../../components/NovelCover";
import EmptyView from "../../components/EmptyView";
import { Searchbar } from "../../components/Searchbar";
import { useTheme, useLibrary, useSettings } from "../../hooks/reduxHooks";
import NovelList from "../../components/NovelList";
import { showToast } from "../../hooks/showToast";
import ErrorView from "../../components/ErrorView";
import { useDispatch } from "react-redux";
import { setAppSettings } from "../../redux/settings/settings.actions";
import { useTheme, useLibrary, useSettings } from "../../hooks/reduxHooks";
import { setAppSettings } from "../../redux/settings/settings.actions";
import { getSource } from "../../sources/sources";
import { showToast } from "../../hooks/showToast";
import { Searchbar } from "../../components/Searchbar/Searchbar";
import { LoadingScreen } from "../../components/LoadingScreen/LoadingScreen";
import NovelCover from "../../components/NovelCover";
import NovelList from "../../components/NovelList";
import { ErrorView } from "../../components/ErrorView/ErrorView";
const Extension = ({ navigation, route }) => {
const { sourceId, sourceName, url } = route.params;
const theme = useTheme();
const library = useLibrary();
const dispatch = useDispatch();
const { displayMode } = useSettings();
const settings = useSettings();
const { displayMode } = settings;
const [loading, setLoading] = useState(true);
const [novels, setNovels] = useState([]);
@@ -82,7 +85,9 @@ const Extension = ({ navigation, route }) => {
let mounted = true;
getNovels();
return () => (mounted = false);
return () => {
mounted = false;
};
}, [page]);
const isCloseToBottom = ({
@@ -115,18 +120,24 @@ const Extension = ({ navigation, route }) => {
})
}
libraryStatus={checkIFInLibrary(item.sourceId, item.novelUrl)}
theme={theme}
/>
);
const listEmptyComponent = () => (
<ErrorView
error={error || "No results found"}
onRetry={() => {
getNovels();
setLoading(true);
setError();
}}
openWebView={() => WebBrowser.openBrowserAsync(url)}
errorName={error || "No results found"}
actions={[
{
name: "Retry",
onPress: () => {
getNovels();
setLoading(true);
setError();
},
icon: "reload",
},
]}
theme={theme}
/>
);
@@ -141,7 +152,7 @@ const Extension = ({ navigation, route }) => {
return icons[displayMode];
};
const getDisplayMode = () =>
const getNextDisplayMode = () =>
displayMode === 0 ? 1 : displayMode === 1 ? 2 : displayMode === 2 && 0;
return (
@@ -166,7 +177,10 @@ const Extension = ({ navigation, route }) => {
color: theme.textColorSecondary,
onPress: () =>
dispatch(
setAppSettings("displayMode", getDisplayMode())
setAppSettings(
"displayMode",
getNextDisplayMode()
)
),
},
{
@@ -178,9 +192,7 @@ const Extension = ({ navigation, route }) => {
/>
{loading ? (
<View style={{ flex: 1, justifyContent: "center" }}>
<ActivityIndicator size={60} color={theme.colorAccent} />
</View>
<LoadingScreen theme={theme} />
) : (
<NovelList
data={novels}
+62 -20
View File
@@ -1,27 +1,62 @@
import React, { useState, useEffect } from "react";
import React, { useState, useCallback } from "react";
import { StyleSheet, FlatList, ActivityIndicator, Text } from "react-native";
import moment from "moment";
import { useDispatch, useSelector } from "react-redux";
import { useFocusEffect } from "@react-navigation/native";
import { useDispatch } from "react-redux";
import EmptyView from "../../components/EmptyView";
import HistoryItem from "./HistoryItem";
import RemoveHistoryDialog from "./RemoveHistoryDialog";
import { ScreenContainer } from "../../components/Common";
import { Searchbar } from "../../components/Searchbar";
import { Searchbar } from "../../components/Searchbar/Searchbar";
import { getHistoryAction } from "../../redux/history/history.actions";
import { dateFormat } from "../../services/utils/constants";
import { useTheme } from "../../hooks/reduxHooks";
import {
deleteHistory,
getHistoryFromDb,
} from "../../database/queries/HistoryQueries";
const History = ({ navigation }) => {
const theme = useTheme();
const dispatch = useDispatch();
const { history, loading } = useSelector((state) => state.historyReducer);
const [searchText, setSearchText] = useState("");
const [searchResults, setSearchResults] = useState([]);
const [history, setHistory] = useState([]);
const [loading, setLoading] = useState(true);
const getHistory = async () => {
const history = await getHistoryFromDb();
const groups = history.reduce((groups, update) => {
var dateParts = update.historyTimeRead.split("-");
var jsDate = new Date(
dateParts[0],
dateParts[1] - 1,
dateParts[2].substr(0, 2)
);
const date = jsDate.toISOString();
if (!groups[date]) {
groups[date] = [];
}
groups[date].push(update);
return groups;
}, {});
const groupedHistory = Object.keys(groups).map((date) => {
return {
date,
novels: groups[date],
};
});
setHistory(groupedHistory);
setLoading(false);
};
/**
* Confirm Clear History Dialog
*/
@@ -29,9 +64,16 @@ const History = ({ navigation }) => {
const showDialog = () => setVisible(true);
const hideDialog = () => setVisible(false);
useEffect(() => {
dispatch(getHistoryAction());
}, [getHistoryAction]);
useFocusEffect(
useCallback(() => {
getHistory();
}, [visible])
);
const deleteChapterHistory = (historyId) => {
deleteHistory(historyId);
getHistory();
};
const renderHistoryCard = ({ item }) => (
<HistoryItem
@@ -39,6 +81,7 @@ const History = ({ navigation }) => {
history={item}
theme={theme}
navigation={navigation}
deleteHistory={deleteChapterHistory}
/>
);
@@ -87,16 +130,12 @@ const History = ({ navigation }) => {
]}
/>
<FlatList
contentContainerStyle={{
flexGrow: 1,
paddingVertical: 8,
paddingHorizontal: 16,
}}
contentContainerStyle={{ flexGrow: 1 }}
data={searchText ? searchResults : history}
keyExtractor={(item, index) => item.date}
keyExtractor={(item) => item.date}
renderItem={({ item }) => (
<FlatList
keyExtractor={(item, index) => item.novelId.toString()}
keyExtractor={(item) => item.novelId.toString()}
data={item.novels}
renderItem={renderHistoryCard}
ListHeaderComponent={
@@ -104,6 +143,7 @@ const History = ({ navigation }) => {
style={{
textTransform: "uppercase",
paddingVertical: 8,
paddingHorizontal: 16,
color: theme.textColorSecondary,
}}
>
@@ -117,20 +157,22 @@ const History = ({ navigation }) => {
<ActivityIndicator
size="small"
color={theme.colorAccent}
style={{ margin: 16 }}
/>
)
}
ListEmptyComponent={
<EmptyView
icon="(˘・_・˘)"
description="Nothing read recently"
/>
!loading && (
<EmptyView
icon="(˘・_・˘)"
description="Nothing read recently"
/>
)
}
/>
<RemoveHistoryDialog
dialogVisible={visible}
hideDialog={hideDialog}
dispatch={dispatch}
theme={theme}
/>
</ScreenContainer>
+26 -17
View File
@@ -5,10 +5,15 @@ import moment from "moment";
import { IconButton } from "react-native-paper";
import { parseChapterNumber } from "../../services/utils/helpers";
import { deleteHistoryAction } from "../../redux/history/history.actions";
import { setNovel } from "../../redux/novel/novel.actions";
const HistoryItem = ({ history, theme, dispatch, navigation }) => {
const HistoryItem = ({
history,
theme,
dispatch,
navigation,
deleteHistory,
}) => {
const {
historyId,
historyTimeRead,
@@ -46,12 +51,12 @@ const HistoryItem = ({ history, theme, dispatch, navigation }) => {
});
return (
<View style={styles.historyItemContainer}>
<Pressable
onPress={navigateToNovel}
android_ripple={{ color: theme.rippleColor }}
style={styles.pressable}
>
<Pressable
onPress={navigateToNovel}
android_ripple={{ color: theme.rippleColor }}
style={styles.pressable}
>
<View style={styles.historyItemContainer}>
<View
style={{
flex: 1,
@@ -61,7 +66,12 @@ const HistoryItem = ({ history, theme, dispatch, navigation }) => {
>
<View>
<Image
source={{ uri: novelCover }}
source={{
uri:
novelCover && !novelCover.startsWith("/")
? novelCover
: "https://github.com/LNReader/lnreader-sources/blob/main/src/coverNotAvailable.jpg?raw=true",
}}
style={styles.historyItemCover}
/>
</View>
@@ -97,7 +107,7 @@ const HistoryItem = ({ history, theme, dispatch, navigation }) => {
icon="delete-outline"
size={22}
color={theme.textColorPrimary}
onPress={() => dispatch(deleteHistoryAction(historyId))}
onPress={() => deleteHistory(historyId)}
/>
<IconButton
icon="play"
@@ -106,8 +116,8 @@ const HistoryItem = ({ history, theme, dispatch, navigation }) => {
onPress={navigateToChapter}
/>
</View>
</Pressable>
</View>
</View>
</Pressable>
);
};
@@ -115,14 +125,13 @@ export default HistoryItem;
const styles = StyleSheet.create({
historyItemContainer: {
marginVertical: 8,
borderRadius: 4,
overflow: "hidden",
paddingVertical: 8,
paddingHorizontal: 16,
flexDirection: "row",
justifyContent: "space-between",
},
pressable: {
flex: 1,
flexDirection: "row",
justifyContent: "space-between",
},
historyItemCover: {
height: 80,
+5 -8
View File
@@ -1,16 +1,13 @@
import React from "react";
import { Button, Dialog, Portal } from "react-native-paper";
import { clearAllHistoryAction } from "../../redux/history/history.actions";
import { deleteAllHistory } from "../../database/queries/HistoryQueries";
import { showToast } from "../../hooks/showToast";
const RemoveHistoryDialog = ({
dialogVisible,
hideDialog,
dispatch,
theme,
}) => {
const RemoveHistoryDialog = ({ dialogVisible, hideDialog, theme }) => {
const onPress = () => {
dispatch(clearAllHistoryAction());
deleteAllHistory();
showToast("History deleted.");
hideDialog();
};
+113 -18
View File
@@ -5,11 +5,14 @@ import {
View,
RefreshControl,
ActivityIndicator,
Pressable,
} from "react-native";
import MaterialCommunityIcons from "react-native-vector-icons/MaterialCommunityIcons";
import { useFocusEffect } from "@react-navigation/native";
import { useSelector, useDispatch } from "react-redux";
import { Searchbar } from "../../components/Searchbar";
import { Searchbar } from "../../components/Searchbar/Searchbar";
import NovelList from "../../components/NovelList";
import NovelCover from "../../components/NovelCover";
import EmptyView from "../../components/EmptyView";
@@ -21,17 +24,24 @@ import {
import { updateLibraryAction } from "../../redux/updates/updates.actions";
import { useSettings, useTheme } from "../../hooks/reduxHooks";
import { setNovel } from "../../redux/novel/novel.actions";
import { Portal, TouchableRipple } from "react-native-paper";
import { Portal } from "react-native-paper";
import LibraryFilterSheet from "./components/LibraryFilterSheet";
import { Actionbar } from "../../components/Actionbar/Actionbar";
import {
markAllChaptersRead,
markAllChaptersUnread,
} from "../../database/queries/ChapterQueries";
import { unfollowNovel } from "../../database/queries/NovelQueries";
const LibraryScreen = ({ navigation }) => {
const theme = useTheme();
const dispatch = useDispatch();
const settings = useSettings();
const {
showNumberOfNovels,
incognitoMode = false,
updateLibraryOnLaunch = false,
} = useSettings();
} = settings;
const libraryFilterSheetRef = useRef(null);
@@ -78,6 +88,12 @@ const LibraryScreen = ({ navigation }) => {
navigation.navigate("Novel", item);
dispatch(setNovel(item));
}}
theme={theme}
onLongPress={selectNovel}
isSelected={
selectedNovels.indexOf(item.novelId) === -1 ? false : true
}
selectedNovels={selectedNovels}
/>
);
@@ -90,10 +106,6 @@ const LibraryScreen = ({ navigation }) => {
/>
);
// if (!loading) {
// console.log(novels);
// }
const listEmptyComponent = () =>
searchText === "" && (
<EmptyView
@@ -102,6 +114,22 @@ const LibraryScreen = ({ navigation }) => {
/>
);
/**
* Selected Novels
*/
const [selectedNovels, setSelectedNovels] = useState([]);
const selectNovel = (novelId) => {
if (selectedNovels.indexOf(novelId) === -1) {
setSelectedNovels((prevState) => [...prevState, novelId]);
} else {
setSelectedNovels((prevState) =>
prevState.filter((id) => id !== novelId)
);
}
};
return (
<>
<View
@@ -111,13 +139,22 @@ const LibraryScreen = ({ navigation }) => {
]}
>
<Searchbar
placeholder={`Search Library ${
showNumberOfNovels ? "(" + novels.length + ")" : ""
}`}
placeholder={
selectedNovels.length === 0
? `Search Library ${
showNumberOfNovels
? "(" + novels.length + ")"
: ""
}`
: `${selectedNovels.length} selected`
}
searchText={searchText}
clearSearchbar={clearSearchbar}
onChangeText={onChangeText}
backAction="magnify"
backAction={selectedNovels.length > 0 ? "close" : "magnify"}
onBackAction={() =>
selectedNovels.length > 0 && setSelectedNovels([])
}
actions={[
{
icon: "filter-variant",
@@ -132,12 +169,28 @@ const LibraryScreen = ({ navigation }) => {
{incognitoMode && (
<View
style={{
backgroundColor: "#424242",
paddingVertical: 4,
backgroundColor: theme.searchBarColor,
paddingVertical: 6,
alignItems: "center",
marginVertical: 8,
elevation: 2,
flexDirection: "row",
justifyContent: "center",
}}
>
<Text style={{ color: "#FFFFFF" }}>Incognito Mode</Text>
<MaterialCommunityIcons
name="incognito"
color={theme.textColorSecondary}
size={18}
/>
<Text
style={{
color: theme.textColorSecondary,
marginLeft: 8,
}}
>
Incognito Mode
</Text>
</View>
)}
{loading ? (
@@ -145,20 +198,19 @@ const LibraryScreen = ({ navigation }) => {
) : (
<>
{searchText !== "" && (
<TouchableRipple
<Pressable
onPress={() =>
navigation.navigate("GlobalSearch", {
novelName: searchText,
})
}
borderless
rippleColor={theme.colorAccent}
android_ripple={{ color: theme.colorAccent }}
style={styles.emptySearch}
>
<Text style={{ color: theme.colorAccent }}>
{`Search for "${searchText}" globally`}
</Text>
</TouchableRipple>
</Pressable>
)}
<NovelList
data={novels}
@@ -166,6 +218,49 @@ const LibraryScreen = ({ navigation }) => {
refreshControl={refreshControl()}
ListEmptyComponent={listEmptyComponent}
/>
{selectedNovels.length > 0 && (
<Actionbar
theme={theme}
actions={[
{
icon: "check",
onPress: () => {
selectedNovels.map((id) =>
markAllChaptersRead(id)
),
setSelectedNovels([]);
dispatch(
getLibraryAction(sort, filter)
);
},
},
{
icon: "check-outline",
onPress: () => {
selectedNovels.map((id) =>
markAllChaptersUnread(id)
);
setSelectedNovels([]);
dispatch(
getLibraryAction(sort, filter)
);
},
},
{
icon: "delete-outline",
onPress: () => {
selectedNovels.map((id) =>
unfollowNovel(id)
);
setSelectedNovels([]);
dispatch(
getLibraryAction(sort, filter)
);
},
},
]}
/>
)}
<Portal>
<LibraryFilterSheet
bottomSheetRef={libraryFilterSheetRef}
@@ -6,15 +6,14 @@ import {
Animated,
useWindowDimensions,
} from "react-native";
import { TabView, SceneMap, TabBar } from "react-native-tab-view";
import { Checkbox } from "react-native-paper";
import Bottomsheet from "rn-sliding-up-panel";
import { ListItem } from "../../../components/List";
import { filterLibrary } from "../../../redux/library/library.actions";
import { useSettings } from "../../../hooks/reduxHooks";
import { setAppSettings } from "../../../redux/settings/settings.actions";
import { Checkbox, SortItem } from "../../../components/Checkbox/Checkbox";
const LibraryFilterSheet = ({
bottomSheetRef,
@@ -26,20 +25,11 @@ const LibraryFilterSheet = ({
const [animatedValue] = useState(new Animated.Value(0));
const FirstRoute = () => (
<View style={{ flex: 1 }}>
<Checkbox.Item
<View style={{ flex: 1, margin: 8 }}>
<Checkbox
label="Downloaded"
labelStyle={{
fontSize: 14,
color: theme.textColorPrimary,
}}
color={theme.colorAccent}
uncheckedColor={theme.textColorHint}
status={
filter === "chaptersDownloaded > 0"
? "checked"
: "unchecked"
}
theme={theme}
status={filter === "chaptersDownloaded > 0"}
onPress={() =>
filter === "chaptersDownloaded > 0"
? dispatch(filterLibrary(sort, ""))
@@ -48,34 +38,20 @@ const LibraryFilterSheet = ({
)
}
/>
<Checkbox.Item
<Checkbox
label="Unread"
labelStyle={{
fontSize: 14,
color: theme.textColorPrimary,
}}
color={theme.colorAccent}
uncheckedColor={theme.textColorHint}
status={filter === "unread = 1" ? "checked" : "unchecked"}
theme={theme}
status={filter === "unread = 1"}
onPress={() =>
filter === "unread = 1"
? dispatch(filterLibrary(sort, ""))
: dispatch(filterLibrary(sort, "unread = 1"))
}
/>
<Checkbox.Item
<Checkbox
label="Completed"
labelStyle={{
fontSize: 14,
color: theme.textColorPrimary,
}}
color={theme.colorAccent}
uncheckedColor={theme.textColorHint}
status={
filter === "chaptersUnread IS NULL"
? "checked"
: "unchecked"
}
theme={theme}
status={filter === "chaptersUnread IS NULL"}
onPress={() =>
filter === "chaptersUnread IS NULL"
? dispatch(filterLibrary(sort, ""))
@@ -87,117 +63,62 @@ const LibraryFilterSheet = ({
</View>
);
const sortOrders = [
{
label: "Date Added",
ASC: "novels.novelId ASC",
DESC: "novels.novelId DESC",
},
{
label: "Aplhabetically",
ASC: "novels.novelName ASC",
DESC: "novels.novelName DESC",
},
{
label: "Unread",
ASC: "novels.unread ASC",
DESC: "novels.unread DESC",
},
{
label: "Downloaded",
ASC: "chaptersDownloaded ASC",
DESC: "chaptersDownloaded DESC",
},
{
label: "Total Chapters",
ASC: "chaptersUnread ASC",
DESC: "chaptersUnread DESC",
},
];
const SecondRoute = () => (
<View style={{ flex: 1 }}>
<ListItem
style={{ paddingVertical: 10 }}
title="Date Added"
titleStyle={{ fontSize: 14 }}
theme={theme}
right={
sort === "novels.novelId ASC"
? "arrow-up"
: sort === "novels.novelId DESC"
? "arrow-down"
: null
}
iconColor={theme.colorAccent}
onPress={() =>
sort === "novels.novelId ASC"
? dispatch(filterLibrary("novels.novelId DESC", filter))
: dispatch(filterLibrary("novels.novelId ASC", filter))
}
/>
<ListItem
style={{ paddingVertical: 10 }}
title="Alphabetically"
titleStyle={{ fontSize: 14 }}
theme={theme}
right={
sort === "novels.novelName ASC"
? "arrow-up"
: sort === "novels.novelName DESC"
? "arrow-down"
: null
}
iconColor={theme.colorAccent}
onPress={() =>
sort === "novels.novelName ASC"
? dispatch(
filterLibrary("novels.novelName DESC", filter)
)
: dispatch(
filterLibrary("novels.novelName ASC", filter)
)
}
/>
<ListItem
style={{ paddingVertical: 10 }}
title="Unread"
titleStyle={{ fontSize: 14 }}
theme={theme}
right={
sort === "novels.unread ASC"
? "arrow-up"
: sort === "novels.unread DESC"
? "arrow-down"
: null
}
iconColor={theme.colorAccent}
onPress={() =>
sort === "novels.unread ASC"
? dispatch(filterLibrary("novels.unread DESC", filter))
: dispatch(filterLibrary("novels.unread ASC", filter))
}
/>
<ListItem
style={{ paddingVertical: 10 }}
title="Downloaded"
titleStyle={{ fontSize: 14 }}
theme={theme}
right={
sort === "chaptersDownloaded ASC"
? "arrow-up"
: sort === "chaptersDownloaded DESC"
? "arrow-down"
: null
}
iconColor={theme.colorAccent}
onPress={() =>
sort === "chaptersDownloaded ASC"
? dispatch(
filterLibrary("chaptersDownloaded DESC", filter)
)
: dispatch(
filterLibrary("chaptersDownloaded ASC", filter)
)
}
/>
<ListItem
style={{ paddingVertical: 10 }}
title="Total Chapters"
titleStyle={{ fontSize: 14 }}
theme={theme}
right={
sort === "chaptersUnread ASC"
? "arrow-up"
: sort === "chaptersUnread DESC"
? "arrow-down"
: null
}
iconColor={theme.colorAccent}
onPress={() =>
sort === "chaptersUnread ASC"
? dispatch(filterLibrary("chaptersUnread DESC", filter))
: dispatch(filterLibrary("chaptersUnread ASC", filter))
}
/>
<View style={{ flex: 1, margin: 8 }}>
{sortOrders.map((item, index) => (
<SortItem
key={index}
label={item.label}
theme={theme}
status={
sort === item.ASC
? "asc"
: sort === item.DESC
? "desc"
: null
}
onPress={() =>
sort === item.ASC
? dispatch(filterLibrary(item.DESC, filter))
: dispatch(filterLibrary(item.ASC, filter))
}
/>
))}
</View>
);
const displayModes = [
{ displayMode: 0, label: "Compact Grid" },
{ displayMode: 1, label: "Comfortable Grid" },
{ displayMode: 3, label: "No Title Grid" },
{ displayMode: 2, label: "List" },
];
@@ -210,30 +131,25 @@ const LibraryFilterSheet = ({
const renderCheckboxes = () => {
return displayModes.map((mode) => (
<Checkbox.Item
<Checkbox
key={mode.displayMode}
label={mode.label}
labelStyle={{ color: theme.textColorPrimary, fontSize: 14 }}
status={
displayMode === mode.displayMode ? "checked" : "unchecked"
}
// mode="ios"
uncheckedColor={theme.textColorSecondary}
color={theme.colorAccent}
status={displayMode === mode.displayMode}
onPress={() =>
dispatch(setAppSettings("displayMode", mode.displayMode))
}
theme={theme}
/>
));
};
const ThirdRoute = () => (
<View style={{ flex: 1 }}>
<View style={{ flex: 1, margin: 8 }}>
<Text
style={{
color: theme.textColorSecondary,
paddingHorizontal: 16,
paddingVertical: 16,
paddingVertical: 8,
textTransform: "uppercase",
}}
>
@@ -245,18 +161,15 @@ const LibraryFilterSheet = ({
style={{
color: theme.textColorSecondary,
paddingHorizontal: 16,
paddingVertical: 16,
paddingVertical: 8,
textTransform: "uppercase",
}}
>
Badges
</Text>
<Checkbox.Item
<Checkbox
label="Download Badges"
labelStyle={{ color: theme.textColorPrimary, fontSize: 14 }}
status={showDownloadBadges ? "checked" : "unchecked"}
uncheckedColor={theme.textColorSecondary}
color={theme.colorAccent}
status={showDownloadBadges}
onPress={() =>
dispatch(
setAppSettings(
@@ -265,25 +178,21 @@ const LibraryFilterSheet = ({
)
)
}
theme={theme}
/>
<Checkbox.Item
<Checkbox
label="Unread Badges"
labelStyle={{ color: theme.textColorPrimary, fontSize: 14 }}
status={showUnreadBadges ? "checked" : "unchecked"}
uncheckedColor={theme.textColorSecondary}
color={theme.colorAccent}
status={showUnreadBadges}
onPress={() =>
dispatch(
setAppSettings("showUnreadBadges", !showUnreadBadges)
)
}
theme={theme}
/>
<Checkbox.Item
<Checkbox
label="Show number of items"
labelStyle={{ color: theme.textColorPrimary, fontSize: 14 }}
status={showNumberOfNovels ? "checked" : "unchecked"}
uncheckedColor={theme.textColorSecondary}
color={theme.colorAccent}
status={showNumberOfNovels}
onPress={() =>
dispatch(
setAppSettings(
@@ -292,6 +201,7 @@ const LibraryFilterSheet = ({
)
)
}
theme={theme}
/>
</View>
);
@@ -317,7 +227,7 @@ const LibraryFilterSheet = ({
indicatorStyle={{ backgroundColor: theme.colorAccent }}
style={{ backgroundColor: theme.colorPrimary }}
renderLabel={({ route, focused, color }) => (
<Text style={{ color, margin: 8 }}>{route.title}</Text>
<Text style={{ color }}>{route.title}</Text>
)}
inactiveColor={theme.textColorSecondary}
activeColor={theme.colorAccent}
@@ -329,8 +239,8 @@ const LibraryFilterSheet = ({
<Bottomsheet
animatedValue={animatedValue}
ref={bottomSheetRef}
draggableRange={{ top: 470, bottom: 0 }}
snappingPoints={[0, 470]}
draggableRange={{ top: 480, bottom: 0 }}
snappingPoints={[0, 480]}
>
<View
style={[
+56 -60
View File
@@ -1,71 +1,67 @@
import React from "react";
import { View } from "react-native";
import * as Linking from "expo-linking";
import { Appbar } from "../../components/Appbar";
import { useSelector } from "react-redux";
import { Divider, ListItem, ListSection } from "../../components/List";
import { List } from "../../components/List";
import { ScreenContainer } from "../../components/Common";
import { MoreHeader } from "./components/MoreHeader";
const AboutScreen = ({ navigation }) => {
const theme = useSelector((state) => state.settingsReducer.theme);
return (
<>
<Appbar title="About" onBackAction={() => navigation.goBack()} />
<View style={{ flex: 1, backgroundColor: theme.colorPrimaryDark }}>
<ListSection>
<ListItem
title="Version"
description="Stable 1.1.1"
theme={theme}
/>
<ListItem
title="Build Time"
description="04-07-21 21:00 PM"
theme={theme}
/>
<ListItem
title="What's new"
onPress={() =>
Linking.openURL(
"https://github.com/rajarsheechatterjee/lnreader/commits/main"
)
}
theme={theme}
/>
<Divider theme={theme} />
<ListItem
title="Discord"
description="https://discord.gg/QdcWN4MD63"
onPress={() =>
Linking.openURL("https://discord.gg/QdcWN4MD63")
}
theme={theme}
/>
<ListItem
title="Github"
description="https://github.com/rajarsheechatterjee/lnreader"
onPress={() =>
Linking.openURL(
"https://github.com/rajarsheechatterjee/lnreader"
)
}
theme={theme}
/>
<ListItem
title="Sources"
description="https://github.com/rajarsheechatterjee/lnreader-sources"
onPress={() =>
Linking.openURL(
"https://github.com/rajarsheechatterjee/lnreader-sources"
)
}
theme={theme}
/>
</ListSection>
</View>
</>
<ScreenContainer theme={theme}>
<MoreHeader
title="About"
navigation={navigation}
theme={theme}
goBack={true}
/>
<List.Section>
<List.Item
title="Version"
description="Stable 1.1.2 (17/07/21 08:00 AM)"
theme={theme}
/>
<List.Item
title="What's new"
onPress={() =>
Linking.openURL(
"https://github.com/LNReader/lnreader/releases/tag/v1.1.2"
)
}
theme={theme}
/>
<List.Divider theme={theme} />
<List.Item
title="Discord"
description="https://discord.gg/QdcWN4MD63"
onPress={() =>
Linking.openURL("https://discord.gg/QdcWN4MD63")
}
theme={theme}
/>
<List.Item
title="Github"
description="https://github.com/LNReader/lnreader"
onPress={() =>
Linking.openURL("https://github.com/LNReader/lnreader")
}
theme={theme}
/>
<List.Item
title="Sources"
description="https://github.com/LNReader/lnreader-sources"
onPress={() =>
Linking.openURL(
"https://github.com/LNReader/lnreader-sources"
)
}
theme={theme}
/>
</List.Section>
</ScreenContainer>
);
};
+5 -55
View File
@@ -1,23 +1,16 @@
import React, { useState } from "react";
import { useDispatch } from "react-redux";
import { Appbar } from "../../components/Appbar";
import { ScreenContainer } from "../../components/Common";
import { ListItem, ListSection, ListSubHeader } from "../../components/List";
import { List } from "../../components/List";
import { useTheme } from "../../hooks/reduxHooks";
import { clearAllHistoryAction } from "../../redux/history/history.actions";
import { deleteNovelCache } from "../../database/queries/NovelQueries";
import { Button, Dialog, Portal } from "react-native-paper";
const AdvancedSettings = ({ navigation }) => {
const theme = useTheme();
const dispatch = useDispatch();
const [visible, setVisible] = useState(false);
const showDialog = () => setVisible(true);
const hideDialog = () => setVisible(false);
/**
* Confirm Clear Database Dialog
@@ -29,59 +22,16 @@ const AdvancedSettings = ({ navigation }) => {
return (
<ScreenContainer theme={theme}>
<Appbar title="Advanced" onBackAction={() => navigation.goBack()} />
<ListSection>
<ListSubHeader theme={theme}>Data</ListSubHeader>
<ListItem
<List.Section>
<List.SubHeader theme={theme}>Data</List.SubHeader>
<List.Item
title="Clear database"
description="Delete history for novels not in your library"
onPress={showClearDatabaseDialog}
theme={theme}
/>
<ListItem
title="Clear history"
description="Delete reading history for all novels"
onPress={showDialog}
theme={theme}
/>
</ListSection>
</List.Section>
<Portal>
<Dialog
visible={visible}
onDismiss={hideDialog}
style={{
borderRadius: 6,
backgroundColor: theme.colorPrimary,
}}
>
<Dialog.Title
style={{
letterSpacing: 0,
fontSize: 16,
color: theme.textColorPrimary,
}}
>
Are you sure? All history will be lost.
</Dialog.Title>
<Dialog.Actions>
<Button
uppercase={false}
theme={{ colors: { primary: theme.colorAccent } }}
onPress={hideDialog}
>
Cancel
</Button>
<Button
uppercase={false}
theme={{ colors: { primary: theme.colorAccent } }}
onPress={() => {
dispatch(clearAllHistoryAction());
hideDialog();
}}
>
Ok
</Button>
</Dialog.Actions>
</Dialog>
<Dialog
visible={clearDatabaseDialog}
onDismiss={hideClearDatabaseDialog}
+19 -14
View File
@@ -2,17 +2,15 @@ import React from "react";
import { useDispatch } from "react-redux";
import { Appbar } from "../../components/Appbar";
import {
InfoItem,
ListItem,
ListSection,
ListSubHeader,
} from "../../components/List";
import { List } from "../../components/List";
import { ScreenContainer } from "../../components/Common";
import { useTheme } from "../../hooks/reduxHooks";
import { restoreLibraryAction } from "../../redux/library/library.actions";
import { createBackup } from "../../services/backup";
import {
createFullBackup,
restoreFullBackup,
} from "../../services/backup/backup";
const BackupSettings = ({ navigation }) => {
const theme = useTheme();
@@ -25,27 +23,34 @@ const BackupSettings = ({ navigation }) => {
title="Backup"
onBackAction={() => navigation.goBack()}
/>
<ListSection>
<ListSubHeader theme={theme}>Backup</ListSubHeader>
<ListItem
<List.Section>
<List.SubHeader theme={theme}>Backup</List.SubHeader>
<List.Item
title="Create backup"
description="Can be used to restore current library"
onPress={createBackup}
onPress={createFullBackup}
theme={theme}
/>
<ListItem
<List.Item
title="Restore backup"
description="Restore library from backup file"
onPress={restoreFullBackup}
theme={theme}
/>
<List.Divider theme={theme} />
<List.Item
title="Restore old backup"
description="Restore library from backup file of version v1.1.2 or lower"
onPress={() => dispatch(restoreLibraryAction())}
theme={theme}
/>
<InfoItem
<List.InfoItem
title="Create backup may not work on devices with Android 9 or lower."
icon="information-outline"
description="Restore library from backup file"
theme={theme}
/>
</ListSection>
</List.Section>
</ScreenContainer>
</>
);
+57 -14
View File
@@ -1,6 +1,11 @@
import React, { useEffect } from "react";
import React, { useState } from "react";
import { FlatList, View, Text, StyleSheet } from "react-native";
import { FAB, ProgressBar } from "react-native-paper";
import {
FAB,
ProgressBar,
Appbar as MaterialAppbar,
Menu,
} from "react-native-paper";
import { useDispatch, useSelector } from "react-redux";
import { Appbar } from "../../components/Appbar";
@@ -9,10 +14,14 @@ import EmptyView from "../../components/EmptyView";
import { useTheme } from "../../hooks/reduxHooks";
import { cancelDownload } from "../../redux/downloads/downloads.actions";
import {
cancelDownload,
pauseDownloads,
resumeDownloads,
} from "../../redux/downloads/downloads.actions";
import BackgroundService from "react-native-background-actions";
import { downloadAllChaptersAction } from "../../redux/novel/novel.actions";
import { showToast } from "../../hooks/showToast";
const DownloadQueue = ({ navigation }) => {
const theme = useTheme();
@@ -20,15 +29,41 @@ const DownloadQueue = ({ navigation }) => {
const dispatch = useDispatch();
useEffect(() => {
if (!BackgroundService.isRunning() && downloadQueue.length > 0) {
dispatch(cancelDownload());
}
}, []);
const [visible, setVisible] = useState(false);
const openMenu = () => setVisible(true);
const closeMenu = () => setVisible(false);
const [fab, setFab] = useState(BackgroundService.isRunning());
return (
<ScreenContainer theme={theme}>
<Appbar title="Download queue" onBackAction={navigation.goBack} />
<Appbar title="Download queue" onBackAction={navigation.goBack}>
<Menu
visible={visible}
onDismiss={closeMenu}
anchor={
<MaterialAppbar.Action
icon="dots-vertical"
color={theme.textColorPrimary}
onPress={openMenu}
/>
}
contentStyle={{ backgroundColor: theme.menuColor }}
>
<Menu.Item
onPress={() => {
if (downloadQueue.length > 0) {
dispatch(cancelDownload());
showToast("Downloads cancelled.");
BackgroundService.stop();
}
closeMenu();
}}
title="Cancel downloads"
titleStyle={{ color: theme.textColorPrimary }}
/>
</Menu>
</Appbar>
<FlatList
contentContainerStyle={{ flexGrow: 1 }}
keyExtractor={(item) => item.chapterId.toString()}
@@ -39,7 +74,10 @@ const DownloadQueue = ({ navigation }) => {
{item.chapterName}
</Text>
<ProgressBar
indeterminate={true}
indeterminate={
BackgroundService.isRunning() ? true : false
}
progress={!BackgroundService.isRunning() && 0}
color={theme.colorAccent}
style={{ marginTop: 8 }}
/>
@@ -53,11 +91,16 @@ const DownloadQueue = ({ navigation }) => {
<FAB
style={[styles.fab, { backgroundColor: theme.colorAccent }]}
color={theme.textColorPrimary}
label="Cancel"
label={fab ? "Pause" : "Resume"}
uppercase={false}
small
icon="close"
onPress={() => dispatch(cancelDownload())}
icon={fab ? "pause" : "play"}
onPress={() => {
setFab((prevState) => !prevState);
BackgroundService.isRunning()
? dispatch(pauseDownloads())
: dispatch(resumeDownloads(downloadQueue));
}}
/>
)}
</ScreenContainer>
+159
View File
@@ -0,0 +1,159 @@
import React, { useCallback, useEffect, useState } from "react";
import { FlatList, StyleSheet } from "react-native";
import { Appbar as MaterialAppbar } from "react-native-paper";
import { useDispatch, useSelector } from "react-redux";
import { Appbar } from "../../components/Appbar";
import { ScreenContainer } from "../../components/Common";
import EmptyView from "../../components/EmptyView";
import { LoadingScreen } from "../../components/LoadingScreen/LoadingScreen";
import {
deleteDownloads,
getDownloadedChapters,
} from "../../database/queries/ChapterQueries";
import { useTheme } from "../../hooks/reduxHooks";
import {
deleteChapterAction,
downloadChapterAction,
} from "../../redux/novel/novel.actions";
import UpdatesItem from "../Updates/components/UpdatesItem";
import RemoveDownloadsDialog from "./components/RemoveDownloadsDialog";
const Downloads = ({ navigation }) => {
const theme = useTheme();
const dispatch = useDispatch();
const { downloadQueue } = useSelector((state) => state.downloadsReducer);
const [loading, setLoading] = useState(true);
const [chapters, setChapters] = useState([]);
/**
* Confirm Clear downloads Dialog
*/
const [visible, setVisible] = useState(false);
const showDialog = () => setVisible(true);
const hideDialog = () => setVisible(false);
const getChapters = async () => {
const res = await getDownloadedChapters();
setChapters(res);
setLoading(false);
};
const ListEmptyComponent = () =>
!loading && <EmptyView icon="(˘・_・˘)" description="No downloads" />;
useEffect(() => {
getChapters();
}, []);
const onPress = useCallback(
(item) =>
navigation.navigate("Chapter", {
chapterId: item.chapterId,
chapterUrl: item.chapterUrl,
sourceId: item.sourceId,
novelUrl: item.novelUrl,
chapterName: item.chapterName,
novelId: item.novelId,
novelName: item.novelName,
bookmark: item.bookmark,
}),
[]
);
const onPressCover = useCallback(
(item) =>
navigation.navigate("Novel", {
sourceId: item.sourceId,
novelUrl: item.novelUrl,
novelName: item.novelName,
novelCover: item.novelCover,
}),
[]
);
const downloadChapter = (
sourceId,
novelUrl,
chapterUrl,
chapterName,
chapterId
) =>
dispatch(
downloadChapterAction(
sourceId,
novelUrl,
chapterUrl,
chapterName,
chapterId
)
);
const deleteChapter = (chapterId, chapterName) => {
dispatch(deleteChapterAction(chapterId, chapterName));
setChapters((chapters) =>
chapters.filter((item) => item.chapterId !== chapterId)
);
};
const renderItem = ({ item }) => (
<UpdatesItem
item={item}
theme={theme}
onPress={() => onPress(item)}
onPressCover={() => onPressCover(item)}
downloadQueue={downloadQueue}
deleteChapter={deleteChapter}
downloadChapter={downloadChapter}
/>
);
return (
<ScreenContainer theme={theme}>
<Appbar title="Downloads" onBackAction={navigation.goBack}>
{chapters.length > 0 && (
<MaterialAppbar.Action
icon="delete-sweep"
color={theme.textColorPrimary}
onPress={showDialog}
/>
)}
</Appbar>
{loading ? (
<LoadingScreen theme={theme} />
) : (
<FlatList
contentContainerStyle={styles.flatList}
data={chapters}
keyExtractor={(item) => item.chapterId.toString()}
renderItem={renderItem}
ListEmptyComponent={<ListEmptyComponent />}
/>
)}
<RemoveDownloadsDialog
dialogVisible={visible}
hideDialog={hideDialog}
onSubmit={() => {
deleteDownloads();
setChapters([]);
hideDialog();
}}
theme={theme}
/>
</ScreenContainer>
);
};
export default Downloads;
const styles = StyleSheet.create({
container: { flex: 1 },
flatList: { flexGrow: 1, paddingVertical: 8 },
});
+153 -53
View File
@@ -2,12 +2,7 @@ import React, { useState } from "react";
import { useDispatch, useSelector } from "react-redux";
import { Appbar } from "../../components/Appbar";
import {
Divider,
ListItem,
ListSection,
ListSubHeader,
} from "../../components/List";
import { List } from "../../components/List";
import { ScreenContainer } from "../../components/Common";
import { useSettings, useTheme } from "../../hooks/reduxHooks";
@@ -16,10 +11,13 @@ import GridSizeModal from "./components/GridSizeModal";
import ThemeModal from "./components/ThemeModal";
import {
setAccentColor,
setAmoledMode,
setAppSettings,
setRippleColor,
} from "../../redux/settings/settings.actions";
import ColorPickerModal from "../../components/ColorPickerModal";
import SwitchSetting from "../../components/Switch/Switch";
import { ScrollView } from "react-native";
const GenralSettings = ({ navigation }) => {
const theme = useTheme();
@@ -29,13 +27,20 @@ const GenralSettings = ({ navigation }) => {
(state) => state.settingsReducer
);
const { updateLibraryOnLaunch = false } = useSettings();
const {
updateLibraryOnLaunch = false,
downloadNewChapters = false,
showHistoryTab = true,
showUpdatesTab = true,
showLabelsInNav = false,
} = useSettings();
const displayModeLabel = (displayMode) => {
const label = {
0: "Compact Grid",
1: "Comfortable Grid",
2: "List",
3: "No Title Grid",
};
return label[displayMode] ?? "Compact Grid";
@@ -69,54 +74,149 @@ const GenralSettings = ({ navigation }) => {
const showAccentColorModal = () => setAccentColorModal(true);
const hideAccentColorModal = () => setAccentColorModal(false);
const hexToRgb = (hex) => {
var result = /^#?([a-f\d]{2})([a-f\d]{2})([a-f\d]{2})$/i.exec(hex);
return result
? {
r: parseInt(result[1], 16),
g: parseInt(result[2], 16),
b: parseInt(result[3], 16),
}
: null;
};
const onSubmit = (val) => {
dispatch(setAccentColor(val));
const rgb = hexToRgb(val);
const rgbaColor = `rgba(${rgb.r},${rgb.g},${rgb.b},0.12)`;
dispatch(setRippleColor(rgbaColor));
};
return (
<ScreenContainer theme={theme}>
<Appbar title="General" onBackAction={() => navigation.goBack()} />
<ListSection>
<ListSubHeader theme={theme}>Display</ListSubHeader>
<ListItem
title="Display Mode"
description={displayModeLabel(displayMode)}
onPress={showDisplayModal}
theme={theme}
/>
<ListItem
title="Items per row in library"
description={`${novelsPerRow} items per row`}
onPress={showGridSizeModal}
theme={theme}
/>
<Divider theme={theme} />
<ListSubHeader theme={theme}>Library</ListSubHeader>
<SwitchSetting
label="Update library on launch"
value={updateLibraryOnLaunch}
onPress={() =>
dispatch(
setAppSettings(
"updateLibraryOnLaunch",
!updateLibraryOnLaunch
<Appbar title="General" onBackAction={navigation.goBack} />
<ScrollView>
<List.Section>
<SwitchSetting
label="Show updates in the nav"
value={showUpdatesTab}
onPress={() =>
dispatch(
setAppSettings(
"showUpdatesTab",
!showUpdatesTab
)
)
)
}
theme={theme}
/>
<ListSubHeader theme={theme}>Theme</ListSubHeader>
<ListItem
title="Theme"
description={theme.name}
onPress={showthemeModal}
theme={theme}
/>
<ListItem
title="Accent Color"
description={theme.colorAccent.toUpperCase()}
onPress={showAccentColorModal}
theme={theme}
iconColor={theme.colorAccent}
right="circle"
/>
</ListSection>
}
theme={theme}
/>
<SwitchSetting
label="Show history in the nav"
value={showHistoryTab}
onPress={() =>
dispatch(
setAppSettings(
"showHistoryTab",
!showHistoryTab
)
)
}
theme={theme}
/>
<SwitchSetting
label="Always show nav labels"
value={showLabelsInNav}
onPress={() =>
dispatch(
setAppSettings(
"showLabelsInNav",
!showLabelsInNav
)
)
}
theme={theme}
/>
<List.Divider theme={theme} />
<List.SubHeader theme={theme}>Display</List.SubHeader>
<List.Item
title="Display Mode"
description={displayModeLabel(displayMode)}
onPress={showDisplayModal}
theme={theme}
/>
<List.Item
title="Items per row in library"
description={`${novelsPerRow} items per row`}
onPress={showGridSizeModal}
theme={theme}
/>
<List.Divider theme={theme} />
<List.SubHeader theme={theme}>Library</List.SubHeader>
<SwitchSetting
label="Update library on launch"
value={updateLibraryOnLaunch}
onPress={() =>
dispatch(
setAppSettings(
"updateLibraryOnLaunch",
!updateLibraryOnLaunch
)
)
}
theme={theme}
/>
<List.Divider theme={theme} />
<List.SubHeader theme={theme}>Auto-download</List.SubHeader>
<SwitchSetting
label="Download new chapters"
value={downloadNewChapters}
onPress={() =>
dispatch(
setAppSettings(
"downloadNewChapters",
!downloadNewChapters
)
)
}
theme={theme}
/>
<List.Divider theme={theme} />
<List.SubHeader theme={theme}>Theme</List.SubHeader>
<List.Item
title="Theme"
description={theme.name}
onPress={showthemeModal}
theme={theme}
/>
{theme.statusBar === "light-content" && (
<SwitchSetting
label="Pure black dark mode"
value={
theme.colorPrimary === "#000000" &&
theme.colorPrimaryDark === "#000000"
}
onPress={() =>
dispatch(
setAmoledMode(
theme.id,
!(
theme.colorPrimary === "#000000" &&
theme.colorPrimaryDark === "#000000"
)
)
)
}
theme={theme}
/>
)}
<List.ColorItem
title="Accent Color"
description={theme.colorAccent.toUpperCase()}
onPress={showAccentColorModal}
theme={theme}
/>
</List.Section>
</ScrollView>
<DisplayModeModal
displayMode={displayMode}
displayModalVisible={displayModalVisible}
@@ -142,7 +242,7 @@ const GenralSettings = ({ navigation }) => {
modalVisible={accentColorModal}
hideModal={hideAccentColorModal}
color={theme.colorAccent}
onSubmit={(val) => dispatch(setAccentColor(val))}
onSubmit={onSubmit}
theme={theme}
/>
</ScreenContainer>
+87 -102
View File
@@ -1,13 +1,15 @@
import React from "react";
import { StyleSheet, View, Image, Pressable, Text } from "react-native";
import { List, Switch } from "react-native-paper";
import { Switch } from "react-native-paper";
import { useDispatch, useSelector } from "react-redux";
import { Appbar } from "../../components/Appbar";
import { Divider, ListItem, ListSection } from "../../components/List";
import { ScreenContainer } from "../../components/Common";
import { List } from "../../components/List";
import { useSettings, useTheme } from "../../hooks/reduxHooks";
import { setAppSettings } from "../../redux/settings/settings.actions";
import { MoreHeader } from "./components/MoreHeader";
const MoreScreen = ({ navigation }) => {
const theme = useTheme();
@@ -20,108 +22,91 @@ const MoreScreen = ({ navigation }) => {
};
return (
<>
<Appbar title="More" style={{ elevation: 0 }} />
<View style={{ flex: 1, backgroundColor: theme.colorPrimaryDark }}>
<View style={{ overflow: "hidden", paddingBottom: 4 }}>
<View
style={{
paddingTop: 10,
paddingBottom: 25,
alignItems: "center",
backgroundColor: theme.colorPrimary,
elevation: 4,
}}
>
<Image
source={require("../../../assets/logo.png")}
style={{
height: 80,
width: 80,
tintColor: theme.textColorPrimary,
}}
/>
</View>
</View>
<ListSection>
<Pressable
android_ripple={{ color: theme.rippleColor }}
style={{
paddingHorizontal: 16,
paddingVertical: 12,
flexDirection: "row",
justifyContent: "space-between",
}}
onPress={enableIncognitoMode}
>
<View style={{ flexDirection: "row" }}>
<List.Icon
color={theme.colorAccent}
icon="incognito"
style={{ margin: 0 }}
/>
<View style={{ marginLeft: 16 }}>
<Text
style={{
color: theme.textColorPrimary,
fontSize: 16,
}}
>
Incognito Mode
</Text>
<Text
style={{ color: theme.textColorSecondary }}
>
Pauses reading history
</Text>
</View>
<ScreenContainer theme={theme}>
<MoreHeader title="More" navigation={navigation} theme={theme} />
<List.Section>
<Pressable
android_ripple={{ color: theme.rippleColor }}
style={{
paddingHorizontal: 16,
paddingVertical: 12,
flexDirection: "row",
justifyContent: "space-between",
}}
onPress={enableIncognitoMode}
>
<View style={{ flexDirection: "row" }}>
<List.Icon theme={theme} icon="incognito" />
<View style={{ marginLeft: 16 }}>
<Text
style={{
color: theme.textColorPrimary,
fontSize: 16,
}}
>
Incognito Mode
</Text>
<Text style={{ color: theme.textColorSecondary }}>
Pauses reading history
</Text>
</View>
<Switch
value={incognitoMode}
onValueChange={enableIncognitoMode}
color={theme.colorAccent}
style={{ marginRight: 8 }}
/>
</Pressable>
</View>
<Switch
value={incognitoMode}
onValueChange={enableIncognitoMode}
color={theme.colorAccent}
style={{ marginRight: 8 }}
/>
</Pressable>
<List.Divider theme={theme} />
<List.Item
title="Download queue"
description={
downloadQueue.length > 0 &&
downloadQueue.length + " remaining"
}
icon="progress-download"
onPress={() =>
navigation.navigate("MoreStack", {
screen: "DownloadQueue",
})
}
theme={theme}
/>
<List.Item
title="Downloads"
icon="folder-download"
onPress={() =>
navigation.navigate("MoreStack", {
screen: "Downloads",
})
}
theme={theme}
/>
<List.Divider theme={theme} />
<List.Item
title="Settings"
icon="cog-outline"
onPress={() =>
navigation.navigate("MoreStack", {
screen: "SettingsStack",
})
}
theme={theme}
/>
<ListItem
title="Download queue"
description={
downloadQueue.length > 0 &&
downloadQueue.length + " remaining"
}
icon="download-outline"
onPress={() =>
navigation.navigate("MoreStack", {
screen: "DownloadQueue",
})
}
theme={theme}
/>
<ListItem
title="Settings"
icon="cog-outline"
onPress={() =>
navigation.navigate("MoreStack", {
screen: "SettingsStack",
})
}
theme={theme}
/>
<ListItem
title="About"
icon="information-outline"
onPress={() =>
navigation.navigate("MoreStack", {
screen: "About",
})
}
theme={theme}
/>
</ListSection>
</View>
</>
<List.Item
title="About"
icon="information-outline"
onPress={() =>
navigation.navigate("MoreStack", {
screen: "About",
})
}
theme={theme}
/>
</List.Section>
</ScreenContainer>
);
};

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