Compare commits
45 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 1a8d640db1 | |||
| dc6b2deddd | |||
| a9fcf2fe08 | |||
| f195b9e6df | |||
| d07fd26cb3 | |||
| 5aee0d1b04 | |||
| c4457d74c4 | |||
| ba90a6fb84 | |||
| e7424cea06 | |||
| e274feda6e | |||
| 8ec2378191 | |||
| ef57a98681 | |||
| bdda959f36 | |||
| 04f3ba2b45 | |||
| f1a261ab28 | |||
| da2ee3b883 | |||
| b98d3861b4 | |||
| ff59330d20 | |||
| b0639e79a8 | |||
| 5931a0dc07 | |||
| cf40acb725 | |||
| a2974ccb44 | |||
| 5eda558f09 | |||
| cf7f1402d3 | |||
| 834a8d9e2e | |||
| fe5b3f4a5b | |||
| ec4eaf4d49 | |||
| 0c0b599452 | |||
| ac26c67361 | |||
| d205572a5e | |||
| e5d52fcd9d | |||
| 61424e533a | |||
| c3ef170385 | |||
| 430c7a7341 | |||
| e4b934061b | |||
| 3579671ade | |||
| 893d229c25 | |||
| ea3f5ac7ee | |||
| e4027efbb1 | |||
| f5dd7153cc | |||
| 38b0097261 | |||
| 2f7215512f | |||
| 9a96a34cda | |||
| a7f6d2cd3c | |||
| 5dfe6a7ab8 |
@@ -1,4 +0,0 @@
|
||||
{
|
||||
"12bb71342c6255bbf50437ec8f4441c083f47cdb74bd89160c15e4f43e52a1cb": true,
|
||||
"40b842e832070c58deac6aa9e08fa459302ee3f9da492c7e77d93d2fbf4a56fd": true
|
||||
}
|
||||
@@ -0,0 +1,45 @@
|
||||
name: CI
|
||||
on:
|
||||
push:
|
||||
branches:
|
||||
- main
|
||||
jobs:
|
||||
install-and-test:
|
||||
name: Install dependencies
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v2
|
||||
- name: Install npm dependencies
|
||||
run: |
|
||||
npm install
|
||||
|
||||
build-android:
|
||||
name: Build app
|
||||
needs: install-and-test
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v2
|
||||
- name: Install Dependencies
|
||||
run: |
|
||||
npm install
|
||||
|
||||
- name: Cache Gradle Wrapper
|
||||
uses: actions/cache@v2
|
||||
with:
|
||||
path: ~/.gradle/wrapper
|
||||
key: ${{ runner.os }}-gradle-wrapper-${{ hashFiles('gradle/wrapper/gradle-wrapper.properties') }}
|
||||
|
||||
- name: Cache Gradle Dependencies
|
||||
uses: actions/cache@v2
|
||||
with:
|
||||
path: ~/.gradle/caches
|
||||
key: ${{ runner.os }}-gradle-caches-${{ hashFiles('gradle/wrapper/gradle-wrapper.properties') }}
|
||||
restore-keys: |
|
||||
${{ runner.os }}-gradle-caches-
|
||||
|
||||
- name: Make Gradlew Executable
|
||||
run: cd android && chmod +x ./gradlew
|
||||
|
||||
- name: Build Android Release
|
||||
run: |
|
||||
cd android && ./gradlew assembleRelease --no-daemon
|
||||
@@ -9,7 +9,7 @@ import { PersistGate } from "redux-persist/lib/integration/react";
|
||||
|
||||
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 = () => {
|
||||
|
||||
@@ -136,7 +136,7 @@ android {
|
||||
minSdkVersion rootProject.ext.minSdkVersion
|
||||
targetSdkVersion rootProject.ext.targetSdkVersion
|
||||
versionCode 1
|
||||
versionName "1.1.0"
|
||||
versionName "1.1.1"
|
||||
}
|
||||
splits {
|
||||
abi {
|
||||
|
||||
@@ -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"/>
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
|
||||
-3
@@ -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()
|
||||
);
|
||||
|
||||
@@ -3,44 +3,9 @@
|
||||
"name": "LNReader",
|
||||
"slug": "LNReader",
|
||||
"scheme": "lnreader",
|
||||
"version": "1.1.0",
|
||||
"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.
Generated
+19712
-8700
File diff suppressed because it is too large
Load Diff
+10
-10
@@ -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,6 +58,15 @@
|
||||
"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",
|
||||
"version": "1.0.0"
|
||||
|
||||
@@ -0,0 +1,42 @@
|
||||
import React from "react";
|
||||
import { Dimensions, StyleSheet, View } from "react-native";
|
||||
|
||||
import { IconButton } from "react-native-paper";
|
||||
|
||||
export const Actionbar = ({ actions, theme }) => {
|
||||
return (
|
||||
<View
|
||||
style={[
|
||||
styles.actionbarContainer,
|
||||
{ backgroundColor: theme.colorPrimary },
|
||||
]}
|
||||
>
|
||||
{actions.map(
|
||||
(action, index) =>
|
||||
action.icon && (
|
||||
<IconButton
|
||||
key={index}
|
||||
icon={action.icon}
|
||||
color={theme.textColorPrimary}
|
||||
onPress={action.onPress}
|
||||
/>
|
||||
)
|
||||
)}
|
||||
</View>
|
||||
);
|
||||
};
|
||||
|
||||
const styles = StyleSheet.create({
|
||||
actionbarContainer: {
|
||||
position: "absolute",
|
||||
width: Dimensions.get("window").width - 32,
|
||||
bottom: 0,
|
||||
margin: 16,
|
||||
borderRadius: 6,
|
||||
flexDirection: "row",
|
||||
justifyContent: "space-between",
|
||||
paddingHorizontal: 8,
|
||||
paddingVertical: 4,
|
||||
elevation: 2,
|
||||
},
|
||||
});
|
||||
@@ -10,6 +10,7 @@ const ColorPickerModal = ({
|
||||
onSubmit,
|
||||
hideModal,
|
||||
modalVisible,
|
||||
showAccentColors,
|
||||
}) => {
|
||||
const [text, setText] = useState(color);
|
||||
const [error, setError] = useState();
|
||||
|
||||
@@ -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,
|
||||
]}
|
||||
>
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -3,7 +3,14 @@ import { StyleSheet, View, Text, Image } from "react-native";
|
||||
import FastImage from "react-native-fast-image";
|
||||
import { TouchableRipple } from "react-native-paper";
|
||||
|
||||
const ListView = ({ item, downloadBadge, unreadBadge, theme, onPress }) => {
|
||||
const ListView = ({
|
||||
item,
|
||||
downloadBadge,
|
||||
unreadBadge,
|
||||
inLibraryBadge,
|
||||
theme,
|
||||
onPress,
|
||||
}) => {
|
||||
return (
|
||||
<TouchableRipple
|
||||
borderless
|
||||
@@ -15,7 +22,10 @@ const ListView = ({ item, downloadBadge, unreadBadge, theme, onPress }) => {
|
||||
<>
|
||||
<FastImage
|
||||
source={{ uri: item.novelCover }}
|
||||
style={styles.extensionIcon}
|
||||
style={[
|
||||
styles.extensionIcon,
|
||||
inLibraryBadge && { opacity: 0.5 },
|
||||
]}
|
||||
/>
|
||||
<Text
|
||||
style={[
|
||||
@@ -29,6 +39,7 @@ const ListView = ({ item, downloadBadge, unreadBadge, theme, onPress }) => {
|
||||
<View style={styles.badgeContainer}>
|
||||
{downloadBadge}
|
||||
{unreadBadge}
|
||||
{inLibraryBadge}
|
||||
</View>
|
||||
</>
|
||||
</TouchableRipple>
|
||||
|
||||
@@ -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>
|
||||
);
|
||||
+194
-101
@@ -1,16 +1,21 @@
|
||||
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 }) => {
|
||||
const { displayMode, novelsPerRow, showDownloadBadges, showUnreadBadges } =
|
||||
useSettings();
|
||||
|
||||
@@ -21,112 +26,89 @@ const NovelCover = ({ item, onPress, libraryStatus }) => {
|
||||
const getNovelsPerRow = () =>
|
||||
orientation === "landscape" ? 6 : novelsPerRow;
|
||||
|
||||
const getHeight = () => (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.6)"]}
|
||||
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 },
|
||||
]}
|
||||
>
|
||||
{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 getHeight = useCallback(
|
||||
() => (window.width / getNovelsPerRow()) * (4 / 3),
|
||||
[]
|
||||
);
|
||||
|
||||
return displayMode !== 2 ? (
|
||||
<View style={{ flex: 1 / getNovelsPerRow() }}>
|
||||
<TouchableRipple
|
||||
borderless
|
||||
centered
|
||||
rippleColor={theme.rippleColor}
|
||||
<View
|
||||
style={{
|
||||
flex: 1 / getNovelsPerRow(),
|
||||
borderRadius: 4,
|
||||
overflow: "hidden",
|
||||
}}
|
||||
>
|
||||
<Pressable
|
||||
android_ripple={{ color: theme.colorAccent }}
|
||||
style={styles.opac}
|
||||
onPress={onPress}
|
||||
>
|
||||
<>
|
||||
<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 }}
|
||||
style={[
|
||||
{ height: getHeight(), borderRadius: 4 },
|
||||
libraryStatus && { opacity: 0.5 },
|
||||
]}
|
||||
>
|
||||
<View style={{ flexDirection: "row", margin: 4 }}>
|
||||
{downloadBadge()}
|
||||
{unreadBadge()}
|
||||
</View>
|
||||
{compactTitle()}
|
||||
</FastImage>
|
||||
{comfortableTitle()}
|
||||
/>
|
||||
<View style={styles.compactTitleContainer}>
|
||||
{displayMode === 0 && (
|
||||
<CompactTitle novelName={item.novelName} />
|
||||
)}
|
||||
</View>
|
||||
{displayMode === 1 && (
|
||||
<ComfortableTitle
|
||||
novelName={item.novelName}
|
||||
theme={theme}
|
||||
/>
|
||||
)}
|
||||
</>
|
||||
</TouchableRipple>
|
||||
</Pressable>
|
||||
</View>
|
||||
) : (
|
||||
<ListView
|
||||
item={item}
|
||||
downloadBadge={downloadBadge()}
|
||||
unreadBadge={unreadBadge()}
|
||||
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}
|
||||
/>
|
||||
@@ -135,10 +117,102 @@ const NovelCover = ({ item, onPress, libraryStatus }) => {
|
||||
|
||||
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,
|
||||
justifyContent: "flex-end",
|
||||
borderRadius: 4,
|
||||
},
|
||||
title: {
|
||||
@@ -151,9 +225,10 @@ const styles = StyleSheet.create({
|
||||
borderRadius: 4,
|
||||
},
|
||||
opac: {
|
||||
paddingHorizontal: 4.5,
|
||||
paddingHorizontal: 4,
|
||||
paddingVertical: 4,
|
||||
borderRadius: 4,
|
||||
flex: 1,
|
||||
},
|
||||
extensionIcon: {
|
||||
width: 42,
|
||||
@@ -186,4 +261,22 @@ const styles = StyleSheet.create({
|
||||
paddingHorizontal: 4,
|
||||
fontSize: 12,
|
||||
},
|
||||
inLibraryBadge: {
|
||||
paddingVertical: 2,
|
||||
paddingHorizontal: 4,
|
||||
fontSize: 12,
|
||||
},
|
||||
compactTitleContainer: {
|
||||
position: "absolute",
|
||||
bottom: 4,
|
||||
left: 4,
|
||||
right: 4,
|
||||
},
|
||||
badgeContainer: {
|
||||
position: "absolute",
|
||||
zIndex: 1,
|
||||
top: 8,
|
||||
left: 8,
|
||||
flexDirection: "row",
|
||||
},
|
||||
});
|
||||
|
||||
@@ -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,15 @@ const NovelList = ({
|
||||
}
|
||||
};
|
||||
|
||||
const keyExtractor = useCallback((item) => item.novelUrl, []);
|
||||
|
||||
return (
|
||||
<FlatList
|
||||
contentContainerStyle={styles.flatListCont}
|
||||
numColumns={getNovelsPerRow()}
|
||||
key={[orientation, getNovelsPerRow()]}
|
||||
data={data}
|
||||
keyExtractor={(item) => item.novelUrl}
|
||||
keyExtractor={keyExtractor}
|
||||
renderItem={renderItem}
|
||||
refreshControl={refreshControl}
|
||||
ListEmptyComponent={ListEmptyComponent}
|
||||
|
||||
@@ -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,
|
||||
@@ -0,0 +1,56 @@
|
||||
import React from "react";
|
||||
import { Pressable, StyleSheet, View, Text } from "react-native";
|
||||
|
||||
import { Switch, List } from "react-native-paper";
|
||||
|
||||
const SwitchSetting = ({ label, description, onPress, theme, icon, value }) => (
|
||||
<Pressable
|
||||
android_ripple={{ color: theme.rippleColor }}
|
||||
style={styles.container}
|
||||
onPress={onPress}
|
||||
>
|
||||
<View style={{ flexDirection: "row", flex: 1 }}>
|
||||
{icon && (
|
||||
<List.Icon
|
||||
color={theme.colorAccent}
|
||||
icon={icon}
|
||||
style={{ margin: 0 }}
|
||||
/>
|
||||
)}
|
||||
<View style={[icon && { marginLeft: 16 }]}>
|
||||
<Text
|
||||
style={{
|
||||
color: theme.textColorPrimary,
|
||||
fontSize: 16,
|
||||
flex: 1,
|
||||
textAlignVertical: "center",
|
||||
}}
|
||||
>
|
||||
{label}
|
||||
</Text>
|
||||
{description && (
|
||||
<Text style={{ color: theme.textColorSecondary }}>
|
||||
{description}
|
||||
</Text>
|
||||
)}
|
||||
</View>
|
||||
</View>
|
||||
<Switch
|
||||
value={value}
|
||||
onValueChange={onPress}
|
||||
color={theme.colorAccent}
|
||||
style={{ marginRight: 8 }}
|
||||
/>
|
||||
</Pressable>
|
||||
);
|
||||
|
||||
export default SwitchSetting;
|
||||
|
||||
const styles = StyleSheet.create({
|
||||
container: {
|
||||
paddingHorizontal: 16,
|
||||
paddingVertical: 12,
|
||||
flexDirection: "row",
|
||||
justifyContent: "space-between",
|
||||
},
|
||||
});
|
||||
@@ -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(
|
||||
@@ -262,3 +259,32 @@ export const markPreviousChaptersUnread = async (chapterId, novelId) => {
|
||||
);
|
||||
});
|
||||
};
|
||||
|
||||
const getDownloadedChaptersQuery = `
|
||||
SELECT chapters.*, novels.sourceId, novels.novelName, novels.novelCover
|
||||
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,9 +1,13 @@
|
||||
import * as SQLite from "expo-sqlite";
|
||||
const db = SQLite.openDatabase("lnreader.db");
|
||||
|
||||
import BackgroundService from "react-native-background-actions";
|
||||
|
||||
import { fetchChapters, fetchNovel } from "../../services/Source/source";
|
||||
import { insertChapters } from "./ChapterQueries";
|
||||
|
||||
import { showToast } from "../../hooks/showToast";
|
||||
|
||||
const insertNovelQuery = `INSERT INTO novels (novelUrl, sourceUrl, sourceId, source, novelName, novelCover, novelSummary, author, artist, status, genre) VALUES(?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`;
|
||||
|
||||
export const insertNovel = async (novel) => {
|
||||
@@ -81,9 +85,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)
|
||||
);
|
||||
});
|
||||
@@ -127,33 +129,67 @@ export const restoreLibrary = async (novel) => {
|
||||
const migrateNovelQuery = `INSERT INTO novels (novelUrl, sourceUrl, sourceId, source, novelName, novelCover, novelSummary, author, artist, status, genre, followed) VALUES(?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`;
|
||||
|
||||
export const migrateNovel = async (sourceId, novelUrl) => {
|
||||
const novel = await fetchNovel(sourceId, novelUrl);
|
||||
try {
|
||||
const novel = await fetchNovel(sourceId, novelUrl);
|
||||
|
||||
db.transaction((tx) =>
|
||||
tx.executeSql(
|
||||
migrateNovelQuery,
|
||||
[
|
||||
novel.novelUrl,
|
||||
novel.sourceUrl,
|
||||
novel.sourceId,
|
||||
novel.source,
|
||||
novel.novelName,
|
||||
novel.novelCover,
|
||||
novel.novelSummary,
|
||||
novel.author,
|
||||
novel.artist,
|
||||
novel.status,
|
||||
novel.genre,
|
||||
1,
|
||||
],
|
||||
async (txObj, { insertId }) => {
|
||||
const chapters = await fetchChapters(
|
||||
novel.sourceId,
|
||||
novel.novelUrl
|
||||
);
|
||||
await insertChapters(insertId, chapters);
|
||||
const options = {
|
||||
taskName: "Migration",
|
||||
taskTitle: `Migrating ${novel.novelName} to new source`,
|
||||
taskDesc: novel.source,
|
||||
taskIcon: {
|
||||
name: "notification_icon",
|
||||
type: "drawable",
|
||||
},
|
||||
(txObj, error) => console.log("Error ", error)
|
||||
)
|
||||
);
|
||||
color: "#00adb5",
|
||||
parameters: {
|
||||
delay: 1000,
|
||||
},
|
||||
progressBar: {
|
||||
max: 1,
|
||||
value: 0,
|
||||
indeterminate: true,
|
||||
},
|
||||
};
|
||||
|
||||
const veryIntensiveTask = async () => {
|
||||
await new Promise(async (resolve) => {
|
||||
db.transaction((tx) =>
|
||||
tx.executeSql(
|
||||
migrateNovelQuery,
|
||||
[
|
||||
novel.novelUrl,
|
||||
novel.sourceUrl,
|
||||
novel.sourceId,
|
||||
novel.source,
|
||||
novel.novelName,
|
||||
novel.novelCover,
|
||||
novel.novelSummary,
|
||||
novel.author,
|
||||
novel.artist,
|
||||
novel.status,
|
||||
novel.genre,
|
||||
1,
|
||||
],
|
||||
async (txObj, { insertId }) => {
|
||||
const chapters = await fetchChapters(
|
||||
novel.sourceId,
|
||||
novel.novelUrl
|
||||
);
|
||||
await insertChapters(insertId, chapters);
|
||||
resolve();
|
||||
},
|
||||
(txObj, error) => console.log("Error ", error)
|
||||
)
|
||||
);
|
||||
});
|
||||
};
|
||||
|
||||
await BackgroundService.start(veryIntensiveTask, options);
|
||||
await BackgroundService.updateNotification({
|
||||
progressBar: { value: 1, indeterminate: false },
|
||||
});
|
||||
await BackgroundService.stop();
|
||||
} catch (error) {
|
||||
showToast(error.message);
|
||||
}
|
||||
};
|
||||
|
||||
@@ -22,7 +22,7 @@ export const githubUpdateChecker = () => {
|
||||
};
|
||||
|
||||
const isNewVersion = (versionTag) => {
|
||||
let currentVersion = "v1.1.0";
|
||||
let currentVersion = "v1.1.1";
|
||||
const regex = /[^\\d.]/;
|
||||
|
||||
let newVersion = versionTag.replace(regex, "");
|
||||
|
||||
@@ -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);
|
||||
};
|
||||
@@ -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 };
|
||||
@@ -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";
|
||||
@@ -18,78 +17,76 @@ const BottomNavigator = () => {
|
||||
const theme = useTheme();
|
||||
|
||||
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.Screen
|
||||
name="Updates"
|
||||
component={Updates}
|
||||
options={{
|
||||
tabBarIcon: ({ color }) => (
|
||||
<MaterialCommunityIcons
|
||||
name="alert-decagram-outline"
|
||||
color={color}
|
||||
size={24}
|
||||
/>
|
||||
),
|
||||
}}
|
||||
/>
|
||||
<Tab.Screen
|
||||
name="History"
|
||||
component={History}
|
||||
options={{
|
||||
tabBarIcon: ({ color }) => (
|
||||
<MaterialCommunityIcons
|
||||
name="history"
|
||||
color={color}
|
||||
size={24}
|
||||
/>
|
||||
),
|
||||
}}
|
||||
/>
|
||||
<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.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.Screen
|
||||
name="Updates"
|
||||
component={Updates}
|
||||
options={{
|
||||
tabBarIcon: ({ color }) => (
|
||||
<MaterialCommunityIcons
|
||||
name="alert-decagram-outline"
|
||||
color={color}
|
||||
size={24}
|
||||
/>
|
||||
),
|
||||
}}
|
||||
/>
|
||||
<Tab.Screen
|
||||
name="History"
|
||||
component={History}
|
||||
options={{
|
||||
tabBarIcon: ({ color }) => (
|
||||
<MaterialCommunityIcons
|
||||
name="history"
|
||||
color={color}
|
||||
size={24}
|
||||
/>
|
||||
),
|
||||
}}
|
||||
/>
|
||||
<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>
|
||||
);
|
||||
};
|
||||
|
||||
|
||||
+18
-21
@@ -1,10 +1,17 @@
|
||||
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";
|
||||
|
||||
// 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 +21,18 @@ 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(() => {
|
||||
setBarColor(theme);
|
||||
}, [theme]);
|
||||
|
||||
const { isNewVersion, latestRelease } = githubUpdateChecker() || {};
|
||||
|
||||
return (
|
||||
@@ -40,7 +41,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 +55,6 @@ const MainNavigator = () => {
|
||||
name="BrowseSettings"
|
||||
component={BrowseSettings}
|
||||
/>
|
||||
<Stack.Screen
|
||||
name="BrowseNovelUpdates"
|
||||
component={BrowseNovelUpdates}
|
||||
/>
|
||||
<Stack.Screen
|
||||
name="GlobalSearch"
|
||||
component={GlobalSearch}
|
||||
|
||||
+20
-46
@@ -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;
|
||||
|
||||
@@ -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";
|
||||
|
||||
@@ -1,5 +1,98 @@
|
||||
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,
|
||||
},
|
||||
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();
|
||||
|
||||
@@ -26,7 +26,7 @@ const historyReducer = (state = initialState, action) => {
|
||||
// ),
|
||||
// };
|
||||
case CLEAR_HISTORY:
|
||||
return { ...state, loading: [] };
|
||||
return { ...state, loading: false, history: [] };
|
||||
default:
|
||||
return state;
|
||||
}
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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 },
|
||||
});
|
||||
};
|
||||
|
||||
@@ -4,14 +4,18 @@ 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,
|
||||
} from "../../theme/theme";
|
||||
|
||||
const themes = {
|
||||
@@ -19,8 +23,10 @@ const themes = {
|
||||
1: lightTheme,
|
||||
2: darkTheme,
|
||||
3: midnightDuskTheme,
|
||||
4: limeTheme,
|
||||
4: greenAppleTheme,
|
||||
5: irisBlueTheme,
|
||||
6: hotPinkTheme,
|
||||
7: strawberryDaiquiri,
|
||||
};
|
||||
|
||||
/**
|
||||
@@ -59,6 +65,9 @@ const initialState = {
|
||||
swipeGestures: true,
|
||||
incognitoMode: false,
|
||||
searchAllSources: false,
|
||||
textSelectable: false,
|
||||
updateLibraryOnLaunch: false,
|
||||
downloadNewChapters: false,
|
||||
};
|
||||
|
||||
const settingsReducer = (state = initialState, action) => {
|
||||
@@ -80,6 +89,28 @@ 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
|
||||
? {
|
||||
...themes[0],
|
||||
colorAccent: state.theme.colorAccent,
|
||||
rippleColor: state.theme.rippleColor,
|
||||
id: state.theme.id,
|
||||
name: state.theme.name,
|
||||
}
|
||||
: themes[payload.id],
|
||||
};
|
||||
|
||||
case SET_APP_SETTINGS:
|
||||
return {
|
||||
...state,
|
||||
|
||||
@@ -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";
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
@@ -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:
|
||||
|
||||
@@ -1,5 +1,7 @@
|
||||
import { GET_UPDATES } from "./updates.types";
|
||||
|
||||
import store from "../store";
|
||||
|
||||
import { getUpdates } from "../../database/queries/UpdateQueries";
|
||||
import { updateAllNovels } from "../../services/updates";
|
||||
|
||||
@@ -33,10 +35,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());
|
||||
};
|
||||
|
||||
@@ -7,33 +7,31 @@ 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 MalCard from "./components/MalCard";
|
||||
import NovelUpdatesCard from "./components/NovelUpdatesCard";
|
||||
|
||||
import { Searchbar } from "../../components/Searchbar/Searchbar";
|
||||
import SourceCard from "./components/SourceCard";
|
||||
import EmptyView from "../../components/EmptyView";
|
||||
import DiscoverCard from "./discover/DiscoverCard";
|
||||
|
||||
const Browse = ({ navigation }) => {
|
||||
const [refreshing, setRefreshing] = useState(false);
|
||||
const [searchText, setSearchText] = useState("");
|
||||
// const [globalSearch, setGlobalSearch] = useState(false);
|
||||
// const [searchResult, setSearchResults] = useState();
|
||||
|
||||
let {
|
||||
sources,
|
||||
search,
|
||||
loading,
|
||||
pinned,
|
||||
filters = [],
|
||||
showNovelUpdates = true,
|
||||
showMyAnimeList = true,
|
||||
} = useSelector((state) => state.sourceReducer);
|
||||
const theme = useTheme();
|
||||
@@ -44,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]);
|
||||
@@ -64,16 +64,24 @@ const Browse = ({ navigation }) => {
|
||||
dispatch(searchSourcesAction(text));
|
||||
};
|
||||
|
||||
// const onSubmitEditing = () => {};
|
||||
|
||||
const renderItem = ({ item }) => (
|
||||
<SourceCard
|
||||
item={item}
|
||||
isPinned={isPinned(item.sourceId)}
|
||||
dispatch={dispatch}
|
||||
navigation={navigation}
|
||||
theme={theme}
|
||||
/>
|
||||
);
|
||||
|
||||
const SourceHeader = ({ title }) => (
|
||||
<Text
|
||||
style={[styles.sourcesHeader, { color: theme.textColorSecondary }]}
|
||||
>
|
||||
{title}
|
||||
</Text>
|
||||
);
|
||||
|
||||
return (
|
||||
<View
|
||||
style={[
|
||||
@@ -109,61 +117,39 @@ 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 && (
|
||||
<NovelUpdatesCard theme={theme} />
|
||||
<SourceHeader title="Discover" />
|
||||
{showMyAnimeList && (
|
||||
<DiscoverCard
|
||||
label="MyAnimeList"
|
||||
onPress={() =>
|
||||
navigation.navigate("BrowseMal")
|
||||
}
|
||||
icon={require("../../../assets/mal.png")}
|
||||
theme={theme}
|
||||
/>
|
||||
)}
|
||||
{showMyAnimeList && <MalCard theme={theme} />}
|
||||
</>
|
||||
)}
|
||||
{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={
|
||||
@@ -198,5 +184,11 @@ const Browse = ({ navigation }) => {
|
||||
export default Browse;
|
||||
|
||||
const styles = StyleSheet.create({
|
||||
container: { flex: 1 },
|
||||
container: {
|
||||
flex: 1,
|
||||
},
|
||||
sourcesHeader: {
|
||||
paddingHorizontal: 16,
|
||||
paddingVertical: 8,
|
||||
},
|
||||
});
|
||||
|
||||
@@ -4,7 +4,7 @@ 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 { Searchbar } from "../../components/Searchbar/Searchbar";
|
||||
import ErrorView from "../../components/ErrorView";
|
||||
|
||||
import { useTheme, useSettings } from "../../hooks/reduxHooks";
|
||||
@@ -14,6 +14,7 @@ import { scrapeSearchResults } from "./discover/MyAnimeListScraper";
|
||||
|
||||
const BrowseMalScreen = ({ navigation, route }) => {
|
||||
const theme = useTheme();
|
||||
const settings = useSettings();
|
||||
|
||||
const [loading, setLoading] = useState(true);
|
||||
const [novels, setNovels] = useState();
|
||||
@@ -75,10 +76,11 @@ const BrowseMalScreen = ({ navigation, route }) => {
|
||||
novelName: item.title,
|
||||
})
|
||||
}
|
||||
theme={theme}
|
||||
/>
|
||||
);
|
||||
|
||||
const { displayMode, novelsPerRow } = useSettings();
|
||||
const { displayMode, novelsPerRow } = settings;
|
||||
|
||||
const orientation = getDeviceOrientation();
|
||||
|
||||
|
||||
@@ -18,12 +18,10 @@ 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"];
|
||||
const { searchAllSources = false } = useSettings();
|
||||
|
||||
const renderItem = ({ item }) => {
|
||||
@@ -87,27 +85,7 @@ 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>
|
||||
|
||||
<Pressable
|
||||
android_ripple={{ color: theme.rippleColor }}
|
||||
style={{
|
||||
|
||||
@@ -53,25 +53,6 @@ const SourceNovels = ({ navigation, route }) => {
|
||||
</Text>
|
||||
}
|
||||
/>
|
||||
{/* <Portal>
|
||||
<MigrationDialog
|
||||
migrationDialog={migrationDialog}
|
||||
hideMigrationDialog={hideMigrationDIalog}
|
||||
theme={theme}
|
||||
selectedNovel={selectedNovel}
|
||||
loading={loading}
|
||||
migrationNovel={migrationNovel}
|
||||
/>
|
||||
<SourceToMigrateDialog
|
||||
selectedNovel={selectedNovel}
|
||||
sourceToMigrateDialog={sourceToMigrateDialog}
|
||||
hideSourceToMigrateDialog={hideSourceToMigrateDialog}
|
||||
sources={sources}
|
||||
theme={theme}
|
||||
showMigrationDialog={showMigrationDialog}
|
||||
selectSourceToMigrate={selectSourceToMigrate}
|
||||
/>
|
||||
</Portal> */}
|
||||
</View>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -1,80 +0,0 @@
|
||||
import React from "react";
|
||||
import { View, Text, Image, StyleSheet } from "react-native";
|
||||
|
||||
import { TouchableRipple, Button } from "react-native-paper";
|
||||
import { useNavigation } from "@react-navigation/native";
|
||||
|
||||
const MalCard = ({ theme }) => {
|
||||
const navigation = useNavigation();
|
||||
|
||||
const navigateToExtension = () => navigation.navigate("BrowseMal");
|
||||
|
||||
return (
|
||||
<TouchableRipple
|
||||
style={styles.extensionCard}
|
||||
onPress={navigateToExtension}
|
||||
rippleColor={theme.rippleColor}
|
||||
>
|
||||
<>
|
||||
<Image
|
||||
source={require("../../../../assets/mal.png")}
|
||||
style={styles.extensionIcon}
|
||||
resizeMode="contain"
|
||||
/>
|
||||
<View style={styles.extensionDetails}>
|
||||
<View>
|
||||
<Text
|
||||
style={{
|
||||
color: theme.textColorPrimary,
|
||||
fontSize: 14,
|
||||
}}
|
||||
>
|
||||
MyAnimeList
|
||||
</Text>
|
||||
</View>
|
||||
<View
|
||||
style={{ flexDirection: "row", alignItems: "center" }}
|
||||
>
|
||||
<Button
|
||||
labelStyle={{ letterSpacing: 0 }}
|
||||
uppercase={false}
|
||||
color={theme.colorAccent}
|
||||
onPress={navigateToExtension}
|
||||
>
|
||||
Browse
|
||||
</Button>
|
||||
</View>
|
||||
</View>
|
||||
</>
|
||||
</TouchableRipple>
|
||||
);
|
||||
};
|
||||
|
||||
export default MalCard;
|
||||
|
||||
const styles = StyleSheet.create({
|
||||
extensionCard: {
|
||||
flexDirection: "row",
|
||||
alignItems: "center",
|
||||
marginVertical: 4,
|
||||
paddingVertical: 8,
|
||||
paddingHorizontal: 20,
|
||||
},
|
||||
extensionIcon: {
|
||||
width: 40,
|
||||
height: 40,
|
||||
borderRadius: 8,
|
||||
},
|
||||
extensionDetails: {
|
||||
flex: 1,
|
||||
flexDirection: "row",
|
||||
justifyContent: "space-between",
|
||||
alignItems: "center",
|
||||
marginLeft: 16,
|
||||
},
|
||||
sourceStatus: {
|
||||
color: "#C14033",
|
||||
fontSize: 12,
|
||||
marginLeft: 5,
|
||||
},
|
||||
});
|
||||
@@ -1,80 +0,0 @@
|
||||
import React from "react";
|
||||
import { View, Text, Image, StyleSheet } from "react-native";
|
||||
|
||||
import { TouchableRipple, Button } from "react-native-paper";
|
||||
import { useNavigation } from "@react-navigation/native";
|
||||
|
||||
const NovelUpdatesCard = ({ theme }) => {
|
||||
const navigation = useNavigation();
|
||||
|
||||
const navigateToExtension = () => navigation.navigate("BrowseNovelUpdates");
|
||||
|
||||
return (
|
||||
<TouchableRipple
|
||||
style={styles.extensionCard}
|
||||
onPress={navigateToExtension}
|
||||
rippleColor={theme.rippleColor}
|
||||
>
|
||||
<>
|
||||
<Image
|
||||
source={require("../../../../assets/novelupdates.png")}
|
||||
style={styles.extensionIcon}
|
||||
resizeMode="contain"
|
||||
/>
|
||||
<View style={styles.extensionDetails}>
|
||||
<View>
|
||||
<Text
|
||||
style={{
|
||||
color: theme.textColorPrimary,
|
||||
fontSize: 14,
|
||||
}}
|
||||
>
|
||||
Novel Updates
|
||||
</Text>
|
||||
</View>
|
||||
<View
|
||||
style={{ flexDirection: "row", alignItems: "center" }}
|
||||
>
|
||||
<Button
|
||||
labelStyle={{ letterSpacing: 0 }}
|
||||
uppercase={false}
|
||||
color={theme.colorAccent}
|
||||
onPress={navigateToExtension}
|
||||
>
|
||||
Browse
|
||||
</Button>
|
||||
</View>
|
||||
</View>
|
||||
</>
|
||||
</TouchableRipple>
|
||||
);
|
||||
};
|
||||
|
||||
export default NovelUpdatesCard;
|
||||
|
||||
const styles = StyleSheet.create({
|
||||
extensionCard: {
|
||||
flexDirection: "row",
|
||||
alignItems: "center",
|
||||
marginVertical: 4,
|
||||
paddingVertical: 8,
|
||||
paddingHorizontal: 20,
|
||||
},
|
||||
extensionIcon: {
|
||||
width: 40,
|
||||
height: 40,
|
||||
borderRadius: 8,
|
||||
},
|
||||
extensionDetails: {
|
||||
flex: 1,
|
||||
flexDirection: "row",
|
||||
justifyContent: "space-between",
|
||||
alignItems: "center",
|
||||
marginLeft: 16,
|
||||
},
|
||||
sourceStatus: {
|
||||
color: "#C14033",
|
||||
fontSize: 12,
|
||||
marginLeft: 5,
|
||||
},
|
||||
});
|
||||
@@ -1,82 +1,70 @@
|
||||
import React from "react";
|
||||
import { View, Text, Image, StyleSheet } from "react-native";
|
||||
import { TouchableRipple, Button, IconButton } from "react-native-paper";
|
||||
import { useNavigation } from "@react-navigation/native";
|
||||
import { useDispatch } from "react-redux";
|
||||
import { View, Text, Image, StyleSheet, Pressable } from "react-native";
|
||||
|
||||
import { Button, IconButton } from "react-native-paper";
|
||||
|
||||
import { pinSourceAction } from "../../../redux/source/source.actions";
|
||||
import FastImage from "react-native-fast-image";
|
||||
|
||||
const ExtensionCard = ({ item, theme, isPinned }) => {
|
||||
const ExtensionCard = ({ item, theme, isPinned, dispatch, navigation }) => {
|
||||
const { sourceId, sourceName, icon, lang } = item;
|
||||
|
||||
const navigation = useNavigation();
|
||||
const dispatch = useDispatch();
|
||||
|
||||
const navigateToExtension = () =>
|
||||
navigation.navigate("Extension", {
|
||||
sourceId: item.sourceId,
|
||||
sourceName: item.sourceName,
|
||||
sourceUrl: item.url,
|
||||
url: item.url,
|
||||
});
|
||||
|
||||
return (
|
||||
<TouchableRipple
|
||||
<Pressable
|
||||
style={styles.extensionCard}
|
||||
onPress={navigateToExtension}
|
||||
rippleColor={theme.rippleColor}
|
||||
android_ripple={{ color: theme.rippleColor }}
|
||||
>
|
||||
<>
|
||||
<Image
|
||||
source={{ uri: icon }}
|
||||
style={styles.extensionIcon}
|
||||
resizeMode="contain"
|
||||
/>
|
||||
<View style={styles.extensionDetails}>
|
||||
<View>
|
||||
<Text
|
||||
style={{
|
||||
color: theme.textColorPrimary,
|
||||
fontSize: 14,
|
||||
}}
|
||||
>
|
||||
{sourceName}
|
||||
</Text>
|
||||
<Text
|
||||
style={{
|
||||
color: theme.textColorSecondary,
|
||||
fontSize: 12,
|
||||
}}
|
||||
>
|
||||
{lang}
|
||||
</Text>
|
||||
</View>
|
||||
<View
|
||||
style={{ flexDirection: "row", alignItems: "center" }}
|
||||
<Image source={{ uri: icon }} style={styles.extensionIcon} />
|
||||
<View style={styles.extensionDetails}>
|
||||
<View>
|
||||
<Text
|
||||
style={{
|
||||
color: theme.textColorPrimary,
|
||||
fontSize: 14,
|
||||
}}
|
||||
>
|
||||
<Button
|
||||
labelStyle={{ letterSpacing: 0 }}
|
||||
uppercase={false}
|
||||
color={theme.colorAccent}
|
||||
onPress={navigateToExtension}
|
||||
>
|
||||
Browse
|
||||
</Button>
|
||||
<IconButton
|
||||
icon={isPinned ? "pin" : "pin-outline"}
|
||||
animated
|
||||
size={21}
|
||||
onPress={() => dispatch(pinSourceAction(sourceId))}
|
||||
color={
|
||||
isPinned
|
||||
? theme.colorAccent
|
||||
: theme.textColorSecondary
|
||||
}
|
||||
style={{ margin: 2 }}
|
||||
/>
|
||||
</View>
|
||||
{sourceName}
|
||||
</Text>
|
||||
<Text
|
||||
style={{
|
||||
color: theme.textColorSecondary,
|
||||
fontSize: 12,
|
||||
}}
|
||||
>
|
||||
{lang}
|
||||
</Text>
|
||||
</View>
|
||||
</>
|
||||
</TouchableRipple>
|
||||
<View style={{ flexDirection: "row", alignItems: "center" }}>
|
||||
<Button
|
||||
labelStyle={{ letterSpacing: 0 }}
|
||||
uppercase={false}
|
||||
color={theme.colorAccent}
|
||||
onPress={navigateToExtension}
|
||||
>
|
||||
Browse
|
||||
</Button>
|
||||
<IconButton
|
||||
icon={isPinned ? "pin" : "pin-outline"}
|
||||
animated
|
||||
size={21}
|
||||
onPress={() => dispatch(pinSourceAction(sourceId))}
|
||||
color={
|
||||
isPinned
|
||||
? theme.colorAccent
|
||||
: theme.textColorSecondary
|
||||
}
|
||||
style={{ margin: 2 }}
|
||||
/>
|
||||
</View>
|
||||
</View>
|
||||
</Pressable>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -93,7 +81,7 @@ const styles = StyleSheet.create({
|
||||
extensionIcon: {
|
||||
width: 40,
|
||||
height: 40,
|
||||
borderRadius: 8,
|
||||
borderRadius: 4,
|
||||
},
|
||||
extensionDetails: {
|
||||
flex: 1,
|
||||
|
||||
@@ -0,0 +1,142 @@
|
||||
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,
|
||||
},
|
||||
]}
|
||||
>
|
||||
{`${"Score "}`}
|
||||
</Text>
|
||||
{children}
|
||||
</Text>
|
||||
</View>
|
||||
);
|
||||
|
||||
const BrowseMalCard = ({ novel, theme, onPress }) => {
|
||||
return (
|
||||
<View
|
||||
style={{
|
||||
flex: 1,
|
||||
backgroundColor: theme.colorPrimary,
|
||||
borderRadius: 8,
|
||||
elevation: 1,
|
||||
marginVertical: 4,
|
||||
marginHorizontal: 8,
|
||||
}}
|
||||
>
|
||||
<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.score}</GenreChip>
|
||||
{novel.info && (
|
||||
<>
|
||||
<View style={{ flexDirection: "row" }}>
|
||||
<Text
|
||||
style={[
|
||||
styles.genreChip,
|
||||
{ color: theme.textColorSecondary },
|
||||
]}
|
||||
>
|
||||
<Text
|
||||
style={[
|
||||
styles.genreChip,
|
||||
{
|
||||
color: theme.textColorPrimary,
|
||||
fontSize: 14,
|
||||
},
|
||||
]}
|
||||
>
|
||||
{`${"Type "}`}
|
||||
</Text>
|
||||
{novel.info[1]}
|
||||
</Text>
|
||||
</View>
|
||||
<View
|
||||
style={{
|
||||
flexDirection: "row",
|
||||
marginBottom: 4,
|
||||
}}
|
||||
>
|
||||
<Text
|
||||
style={[
|
||||
styles.genreChip,
|
||||
{ color: theme.textColorSecondary },
|
||||
]}
|
||||
>
|
||||
<Text
|
||||
style={[
|
||||
styles.genreChip,
|
||||
{
|
||||
color: theme.textColorPrimary,
|
||||
fontSize: 14,
|
||||
},
|
||||
]}
|
||||
>
|
||||
{`${"Published "}`}
|
||||
</Text>
|
||||
{novel.info[2]}
|
||||
</Text>
|
||||
</View>
|
||||
</>
|
||||
)}
|
||||
</View>
|
||||
</Pressable>
|
||||
</View>
|
||||
);
|
||||
};
|
||||
|
||||
export default BrowseMalCard;
|
||||
|
||||
const styles = StyleSheet.create({
|
||||
novelUpdatesCard: {
|
||||
flex: 1,
|
||||
flexDirection: "row",
|
||||
},
|
||||
contentContainer: {
|
||||
flex: 1,
|
||||
},
|
||||
genreChip: {
|
||||
marginTop: 4,
|
||||
fontSize: 13,
|
||||
// fontStyle: "italic",
|
||||
},
|
||||
});
|
||||
@@ -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({});
|
||||
@@ -0,0 +1,58 @@
|
||||
import React from "react";
|
||||
import { View, Text, Image, StyleSheet, Pressable } from "react-native";
|
||||
|
||||
import { Button } from "react-native-paper";
|
||||
|
||||
const DiscoverCard = ({ icon, label, onPress, theme }) => {
|
||||
return (
|
||||
<Pressable
|
||||
style={styles.discoverCard}
|
||||
onPress={onPress}
|
||||
android_ripple={{ color: theme.rippleColor }}
|
||||
>
|
||||
<Image source={icon} style={styles.discoverIcon} />
|
||||
<View style={styles.label}>
|
||||
<Text
|
||||
style={{
|
||||
color: theme.textColorPrimary,
|
||||
fontSize: 14,
|
||||
}}
|
||||
>
|
||||
{label}
|
||||
</Text>
|
||||
<Button
|
||||
labelStyle={{ letterSpacing: 0 }}
|
||||
uppercase={false}
|
||||
color={theme.colorAccent}
|
||||
onPress={onPress}
|
||||
>
|
||||
Browse
|
||||
</Button>
|
||||
</View>
|
||||
</Pressable>
|
||||
);
|
||||
};
|
||||
|
||||
export default DiscoverCard;
|
||||
|
||||
const styles = StyleSheet.create({
|
||||
discoverCard: {
|
||||
flexDirection: "row",
|
||||
alignItems: "center",
|
||||
marginVertical: 4,
|
||||
paddingVertical: 8,
|
||||
paddingHorizontal: 20,
|
||||
},
|
||||
discoverIcon: {
|
||||
width: 40,
|
||||
height: 40,
|
||||
borderRadius: 4,
|
||||
},
|
||||
label: {
|
||||
flex: 1,
|
||||
flexDirection: "row",
|
||||
justifyContent: "space-between",
|
||||
alignItems: "center",
|
||||
marginLeft: 16,
|
||||
},
|
||||
});
|
||||
@@ -1,22 +1,16 @@
|
||||
import React, { useEffect, useState } from "react";
|
||||
import {
|
||||
StyleSheet,
|
||||
View,
|
||||
ActivityIndicator,
|
||||
FlatList,
|
||||
Button,
|
||||
} from "react-native";
|
||||
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 { Searchbar } from "../../../components/Searchbar/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, scrapeTopNovels } from "./MyAnimeListScraper";
|
||||
import BrowseMalCard from "./BrowseMalCard";
|
||||
|
||||
const BrowseMalScreen = ({ navigation, route }) => {
|
||||
const theme = useTheme();
|
||||
@@ -28,7 +22,6 @@ const BrowseMalScreen = ({ navigation, route }) => {
|
||||
|
||||
const [searchText, setSearchText] = useState("");
|
||||
|
||||
const baseUrl = "https://api.jikan.moe/v3/";
|
||||
const malUrl = "https://myanimelist.net/topmanga.php?type=lightnovels";
|
||||
|
||||
const getNovels = async (limit) => {
|
||||
@@ -72,8 +65,9 @@ const BrowseMalScreen = ({ navigation, route }) => {
|
||||
}, []);
|
||||
|
||||
const renderItem = ({ item }) => (
|
||||
<NovelCover
|
||||
item={{ novelName: item.novelName, novelCover: item.novelCover }}
|
||||
<BrowseMalCard
|
||||
novel={item}
|
||||
theme={theme}
|
||||
onPress={() =>
|
||||
navigation.navigate("GlobalSearch", {
|
||||
novelName: item.novelName,
|
||||
@@ -153,8 +147,6 @@ const BrowseMalScreen = ({ navigation, route }) => {
|
||||
) : (
|
||||
<FlatList
|
||||
contentContainerStyle={styles.novelsContainer}
|
||||
numColumns={getNovelsPerRow()}
|
||||
key={getNovelsPerRow()}
|
||||
data={novels}
|
||||
keyExtractor={(item) => item.novelName}
|
||||
renderItem={renderItem}
|
||||
@@ -189,7 +181,7 @@ const styles = StyleSheet.create({
|
||||
},
|
||||
novelsContainer: {
|
||||
flexGrow: 1,
|
||||
paddingVertical: 8,
|
||||
paddingBottom: 8,
|
||||
paddingHorizontal: 4,
|
||||
},
|
||||
});
|
||||
|
||||
@@ -22,7 +22,12 @@ const scrapeTopNovels = async (pageNo) => {
|
||||
".jpg";
|
||||
const novelName = $(this).find("h3").text();
|
||||
|
||||
const novel = { novelName, novelCover };
|
||||
const score = $(this)
|
||||
.find(".js-top-ranking-score-col > span.score-label")
|
||||
.text();
|
||||
|
||||
const info = $(this).find("div.information").text().split(/\s\s+/);
|
||||
const novel = { novelName, novelCover, score, info };
|
||||
|
||||
novels.push(novel);
|
||||
});
|
||||
@@ -58,7 +63,9 @@ const scrapeSearchResults = async (searchTerm) => {
|
||||
novelId[8].split(".")[0] +
|
||||
".jpg";
|
||||
|
||||
const novel = { novelName, novelCover };
|
||||
const score = $(this).find("td:nth-child(5)").text().trim();
|
||||
|
||||
const novel = { novelName, novelCover, score };
|
||||
|
||||
novels.push(novel);
|
||||
}
|
||||
|
||||
@@ -1,5 +0,0 @@
|
||||
import React from "react";
|
||||
|
||||
const NovelUpdatesBottomSheet = () => {};
|
||||
|
||||
export default NovelUpdatesBottomSheet;
|
||||
@@ -1,72 +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 }) => (
|
||||
<Text
|
||||
style={[
|
||||
styles.genreChip,
|
||||
{
|
||||
color: theme.colorAccent,
|
||||
borderColor: theme.colorAccent,
|
||||
},
|
||||
]}
|
||||
numberOfLines={1}
|
||||
>
|
||||
{children}
|
||||
</Text>
|
||||
);
|
||||
|
||||
const NovelUpdatesNovelCard = ({ novel, theme, onPress }) => {
|
||||
return (
|
||||
<Pressable
|
||||
style={styles.novelUpdatesCard}
|
||||
onPress={onPress}
|
||||
android_ripple={{ color: theme.rippleColor }}
|
||||
>
|
||||
<View style={{ flex: 1, flexDirection: "row" }}>
|
||||
<FastImage
|
||||
source={{ uri: novel.novelCover }}
|
||||
style={{ width: 85, height: 120, borderRadius: 4 }}
|
||||
/>
|
||||
<View style={{ flex: 1, paddingLeft: 16 }}>
|
||||
<Text
|
||||
style={{ color: theme.textColorPrimary, fontSize: 16 }}
|
||||
>
|
||||
{novel.novelName}
|
||||
</Text>
|
||||
|
||||
<GenreChip theme={theme}>{novel.genres}</GenreChip>
|
||||
|
||||
<Text
|
||||
style={{
|
||||
fontSize: 13,
|
||||
color: theme.textColorPrimary,
|
||||
}}
|
||||
numberOfLines={3}
|
||||
>
|
||||
{novel.novelSummary}
|
||||
</Text>
|
||||
</View>
|
||||
</View>
|
||||
</Pressable>
|
||||
);
|
||||
};
|
||||
|
||||
export default NovelUpdatesNovelCard;
|
||||
|
||||
const styles = StyleSheet.create({
|
||||
novelUpdatesCard: {
|
||||
flex: 1,
|
||||
padding: 16,
|
||||
},
|
||||
contentContainer: {
|
||||
flex: 1,
|
||||
},
|
||||
genreChip: {
|
||||
marginVertical: 4,
|
||||
fontSize: 12,
|
||||
// fontStyle: "italic",
|
||||
},
|
||||
});
|
||||
@@ -1,88 +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;
|
||||
|
||||
const novel = { novelName, novelCover, genres, novelSummary };
|
||||
|
||||
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;
|
||||
|
||||
const novel = { novelName, novelCover, genres, novelSummary };
|
||||
|
||||
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";
|
||||
@@ -42,7 +42,10 @@ const GlobalSearch = ({ route, navigation }) => {
|
||||
const library = useLibrary();
|
||||
|
||||
useEffect(() => {
|
||||
let mounted = true;
|
||||
novelName && onSubmitEditing();
|
||||
|
||||
return () => (mounted = false);
|
||||
}, []);
|
||||
|
||||
const clearSearchbar = () => setSearchText("");
|
||||
@@ -50,44 +53,46 @@ const GlobalSearch = ({ route, navigation }) => {
|
||||
|
||||
const onSubmitEditing = async () => {
|
||||
setSearchResults([]);
|
||||
setProgress(0);
|
||||
|
||||
let globalSearchSources = searchAllSources ? sources : pinnedSources;
|
||||
|
||||
globalSearchSources.map((item, index) =>
|
||||
setTimeout(async () => {
|
||||
try {
|
||||
setLoading(true);
|
||||
for (let i = 0; i < globalSearchSources.length; i++) {
|
||||
try {
|
||||
setLoading(true);
|
||||
|
||||
const source = getSource(item.sourceId);
|
||||
const data = await source.searchNovels(
|
||||
encodeURI(searchText)
|
||||
);
|
||||
const source = getSource(globalSearchSources[i].sourceId);
|
||||
const data = await source.searchNovels(encodeURI(searchText));
|
||||
|
||||
setSearchResults((searchResults) => [
|
||||
...searchResults,
|
||||
{
|
||||
sourceId: item.sourceId,
|
||||
sourceName: item.sourceName,
|
||||
lang: item.lang,
|
||||
novels: data,
|
||||
},
|
||||
]);
|
||||
setLoading(false);
|
||||
} catch (error) {
|
||||
showToast(error.message);
|
||||
setSearchResults((searchResults) => [
|
||||
...searchResults,
|
||||
{
|
||||
sourceId: item.sourceId,
|
||||
sourceName: item.sourceName,
|
||||
lang: item.lang,
|
||||
novels: [],
|
||||
},
|
||||
]);
|
||||
setLoading(false);
|
||||
}
|
||||
}, 1000 * index)
|
||||
);
|
||||
setSearchResults((searchResults) => [
|
||||
...searchResults,
|
||||
{
|
||||
sourceId: globalSearchSources[i].sourceId,
|
||||
sourceName: globalSearchSources[i].sourceName,
|
||||
lang: globalSearchSources[i].lang,
|
||||
novels: data,
|
||||
},
|
||||
]);
|
||||
setLoading(false);
|
||||
setProgress(
|
||||
(progress) => progress + 1 / globalSearchSources.length
|
||||
);
|
||||
} catch (error) {
|
||||
showToast(
|
||||
globalSearchSources[i].sourceName + ": " + error.message
|
||||
);
|
||||
setSearchResults((searchResults) => [
|
||||
...searchResults,
|
||||
{
|
||||
sourceId: globalSearchSources[i].sourceId,
|
||||
sourceName: globalSearchSources[i].sourceName,
|
||||
lang: globalSearchSources[i].lang,
|
||||
novels: [],
|
||||
},
|
||||
]);
|
||||
setLoading(false);
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
const renderItem = ({ item }) => (
|
||||
@@ -111,15 +116,8 @@ const GlobalSearch = ({ route, navigation }) => {
|
||||
onSubmitEditing={onSubmitEditing}
|
||||
clearSearchbar={clearSearchbar}
|
||||
/>
|
||||
{progress < 1 && progress > 0 && pinned && (
|
||||
<ProgressBar
|
||||
color={
|
||||
progress > 0.9
|
||||
? theme.colorPrimaryDark
|
||||
: theme.colorAccent
|
||||
}
|
||||
progress={progress}
|
||||
/>
|
||||
{progress > 0 && (
|
||||
<ProgressBar color={theme.colorAccent} progress={progress} />
|
||||
)}
|
||||
<FlatList
|
||||
contentContainerStyle={{
|
||||
@@ -135,12 +133,12 @@ const GlobalSearch = ({ route, navigation }) => {
|
||||
{!loading && (
|
||||
<EmptyView
|
||||
icon="__φ(..)"
|
||||
description={`Search a novel in your pinned sources\n${
|
||||
description={`Search a novel in ${
|
||||
searchAllSources
|
||||
? "(Search all sources)"
|
||||
? "all sources"
|
||||
: pinned.length === 0
|
||||
? "(No sources pinned)"
|
||||
: ""
|
||||
? "pinned sources\n(No sources pinned)"
|
||||
: "pinned sources"
|
||||
}`}
|
||||
/>
|
||||
)}
|
||||
|
||||
@@ -111,12 +111,12 @@ const MigrationNovelList = ({ data, theme, library, navigation }) => {
|
||||
textTransform: "none",
|
||||
}}
|
||||
theme={{ colors: { primary: theme.colorAccent } }}
|
||||
onPress={() => {
|
||||
migrateNovel(
|
||||
onPress={async () => {
|
||||
hideMigrateNovelDialog();
|
||||
await migrateNovel(
|
||||
selectedNovel.sourceId,
|
||||
selectedNovel.novelUrl
|
||||
);
|
||||
hideMigrateNovelDialog();
|
||||
showToast(
|
||||
`${selectedNovel.novelName} migrated to new source.`
|
||||
);
|
||||
|
||||
@@ -7,7 +7,7 @@ import {
|
||||
ActivityIndicator,
|
||||
} from "react-native";
|
||||
import { ProgressBar } from "react-native-paper";
|
||||
import { useLibrary, useTheme } from "../../../hooks/reduxHooks";
|
||||
import { useLibrary, useSettings, useTheme } from "../../../hooks/reduxHooks";
|
||||
import { useSelector } from "react-redux";
|
||||
|
||||
import EmptyView from "../../../components/EmptyView";
|
||||
@@ -26,7 +26,13 @@ const MigrationNovels = ({ navigation, route }) => {
|
||||
const [progress, setProgress] = useState(0);
|
||||
const [loading, setLoading] = useState(false);
|
||||
const [searchResults, setSearchResults] = useState("");
|
||||
const { sources, pinned } = useSelector((state) => state.sourceReducer);
|
||||
const { searchAllSources = false } = useSettings();
|
||||
let {
|
||||
sources,
|
||||
pinned,
|
||||
filters = [],
|
||||
} = useSelector((state) => state.sourceReducer);
|
||||
sources = sources.filter((source) => filters.indexOf(source.lang) === -1);
|
||||
|
||||
const library = useLibrary();
|
||||
|
||||
@@ -36,41 +42,43 @@ const MigrationNovels = ({ navigation, route }) => {
|
||||
source.sourceId !== sourceId
|
||||
);
|
||||
|
||||
const getSearchResults = () => {
|
||||
pinnedSources.map((item, index) =>
|
||||
setTimeout(async () => {
|
||||
try {
|
||||
setLoading(true);
|
||||
const getSearchResults = async () => {
|
||||
let migrationSources = searchAllSources ? sources : pinnedSources;
|
||||
for (let i = 0; i < migrationSources.length; i++) {
|
||||
try {
|
||||
setLoading(true);
|
||||
|
||||
const source = getSource(item.sourceId);
|
||||
const data = await source.searchNovels(novelName);
|
||||
const source = getSource(migrationSources[i].sourceId);
|
||||
const data = await source.searchNovels(novelName);
|
||||
|
||||
setSearchResults((searchResults) => [
|
||||
...searchResults,
|
||||
{
|
||||
sourceId: item.sourceId,
|
||||
sourceName: item.sourceName,
|
||||
sourceLanguage: item.sourceLanguage,
|
||||
novels: data,
|
||||
},
|
||||
]);
|
||||
setLoading(false);
|
||||
} catch (error) {
|
||||
showToast(error.message);
|
||||
setSearchResults((searchResults) => [
|
||||
...searchResults,
|
||||
{
|
||||
sourceId: migrationSources[i].sourceId,
|
||||
sourceName: migrationSources[i].sourceName,
|
||||
sourceLanguage: migrationSources[i].lang,
|
||||
novels: data,
|
||||
},
|
||||
]);
|
||||
setLoading(false);
|
||||
setProgress(
|
||||
(progress) => progress + 1 / migrationSources.length
|
||||
);
|
||||
} catch (error) {
|
||||
showToast(error.message);
|
||||
|
||||
setSearchResults((searchResults) => [
|
||||
...searchResults,
|
||||
{
|
||||
sourceId: item.sourceId,
|
||||
sourceName: item.sourceName,
|
||||
sourceLanguage: item.sourceLanguage,
|
||||
novels: [],
|
||||
},
|
||||
]);
|
||||
setLoading(false);
|
||||
}
|
||||
}, 1000 * index)
|
||||
);
|
||||
setSearchResults((searchResults) => [
|
||||
...searchResults,
|
||||
{
|
||||
sourceId: migrationSources[i].sourceId,
|
||||
sourceName: migrationSources[i].sourceName,
|
||||
sourceLanguage: migrationSources[i].lang,
|
||||
novels: [],
|
||||
},
|
||||
]);
|
||||
setLoading(false);
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
@@ -102,7 +110,7 @@ const MigrationNovels = ({ navigation, route }) => {
|
||||
title={novelName}
|
||||
onBackAction={() => navigation.goBack()}
|
||||
/>
|
||||
{progress < 1 && pinned && (
|
||||
{progress > 0 && (
|
||||
<ProgressBar color={theme.colorAccent} progress={progress} />
|
||||
)}
|
||||
<FlatList
|
||||
|
||||
@@ -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>
|
||||
|
||||
+144
-138
@@ -1,11 +1,11 @@
|
||||
import React, { useState, useEffect, useRef } from "react";
|
||||
import React, { useState, useEffect, useRef, useCallback } from "react";
|
||||
import {
|
||||
StyleSheet,
|
||||
View,
|
||||
Text,
|
||||
ActivityIndicator,
|
||||
StatusBar,
|
||||
ScrollView,
|
||||
Pressable,
|
||||
} from "react-native";
|
||||
|
||||
import { useDispatch, useSelector } from "react-redux";
|
||||
@@ -44,11 +44,12 @@ 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 TextToComponents from "./TextToComponent";
|
||||
import { LoadingScreen } from "../../components/LoadingScreen/LoadingScreen";
|
||||
|
||||
const Chapter = ({ route, navigation }) => {
|
||||
const {
|
||||
@@ -61,6 +62,7 @@ const Chapter = ({ route, navigation }) => {
|
||||
chapterName,
|
||||
bookmark,
|
||||
} = route.params;
|
||||
|
||||
let scrollViewRef = useRef(null);
|
||||
let readerSheetRef = useRef(null);
|
||||
|
||||
@@ -70,7 +72,11 @@ const Chapter = ({ route, navigation }) => {
|
||||
const showScrollPercentage = useSelector(
|
||||
(state) => state.settingsReducer.showScrollPercentage
|
||||
);
|
||||
const { swipeGestures = true, incognitoMode = false } = useSettings();
|
||||
const {
|
||||
swipeGestures = true,
|
||||
incognitoMode = false,
|
||||
textSelectable = false,
|
||||
} = useSettings();
|
||||
|
||||
const [hidden, setHidden] = useState(true);
|
||||
|
||||
@@ -85,12 +91,9 @@ const Chapter = ({ route, navigation }) => {
|
||||
|
||||
const [scrollPercentage, setScrollPercentage] = useState(0);
|
||||
const [firstLayout, setFirstLayout] = useState(true);
|
||||
const [selectText, setSelectText] = useState(false);
|
||||
|
||||
const [contentSize, setContentSize] = useState(0);
|
||||
|
||||
const [images, setImages] = useState([]);
|
||||
|
||||
const getChapter = async (chapterId) => {
|
||||
try {
|
||||
if (chapterId) {
|
||||
@@ -98,19 +101,16 @@ 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);
|
||||
}
|
||||
|
||||
@@ -122,17 +122,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({});
|
||||
|
||||
@@ -146,7 +135,6 @@ const Chapter = ({ route, navigation }) => {
|
||||
|
||||
useEffect(() => {
|
||||
getChapter(chapterId);
|
||||
setPrevAndNextChap();
|
||||
!incognitoMode && dispatch(insertHistoryAction(novelId, chapterId));
|
||||
return () => {
|
||||
StatusBar.setHidden(false);
|
||||
@@ -154,17 +142,20 @@ const Chapter = ({ route, navigation }) => {
|
||||
};
|
||||
}, []);
|
||||
|
||||
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);
|
||||
@@ -182,7 +173,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;
|
||||
@@ -202,11 +193,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({
|
||||
@@ -216,17 +211,19 @@ const Chapter = ({ route, navigation }) => {
|
||||
});
|
||||
setFirstLayout(false);
|
||||
}
|
||||
}, []);
|
||||
|
||||
const hideHeader = () => {
|
||||
!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,
|
||||
@@ -250,7 +247,7 @@ const Chapter = ({ route, navigation }) => {
|
||||
novelName,
|
||||
bookmark: prevChapter.bookmark,
|
||||
})
|
||||
: showToast("'There's no previous chapter");
|
||||
: showToast("There's no previous chapter");
|
||||
|
||||
const navigateToNextChapter = () =>
|
||||
nextChapter
|
||||
@@ -264,40 +261,42 @@ const Chapter = ({ route, navigation }) => {
|
||||
novelName,
|
||||
bookmark: nextChapter.bookmark,
|
||||
})
|
||||
: showToast("'There's no next chapter");
|
||||
: showToast("There's no next chapter");
|
||||
|
||||
const onPressLink = async (link) => {
|
||||
setLoading(true);
|
||||
const res = await fetchChapter(sourceId, novelUrl, link);
|
||||
setChapter(res);
|
||||
setLoading(false);
|
||||
};
|
||||
|
||||
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}
|
||||
>
|
||||
<GestureRecognizer
|
||||
onSwipeRight={swipeGestures && navigateToPrevChapter}
|
||||
onSwipeLeft={swipeGestures && navigateToNextChapter}
|
||||
config={config}
|
||||
style={{ flex: 1 }}
|
||||
<>
|
||||
<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}
|
||||
>
|
||||
{error ? (
|
||||
<View style={{ flex: 1, justifyContent: "center" }}>
|
||||
<EmptyView
|
||||
icon="(-_-;)・・・"
|
||||
icon="Σ(ಠ_ಠ)"
|
||||
description={error}
|
||||
style={{ color: errorTextColor(reader.theme) }}
|
||||
>
|
||||
@@ -322,81 +321,88 @@ const Chapter = ({ route, navigation }) => {
|
||||
</EmptyView>
|
||||
</View>
|
||||
) : loading ? (
|
||||
<View style={{ flex: 1, justifyContent: "center" }}>
|
||||
<ActivityIndicator
|
||||
size={50}
|
||||
color={theme.colorAccent}
|
||||
/>
|
||||
</View>
|
||||
<LoadingScreen theme={theme} />
|
||||
) : (
|
||||
<View style={{ flex: 1 }}>
|
||||
{images.length > 0 &&
|
||||
images.map((image, index) => (
|
||||
<FitImage
|
||||
source={{ uri: image }}
|
||||
key={index}
|
||||
/>
|
||||
))}
|
||||
|
||||
<Text
|
||||
style={readerStyles}
|
||||
<GestureRecognizer
|
||||
onSwipeRight={
|
||||
swipeGestures && navigateToPrevChapter
|
||||
}
|
||||
onSwipeLeft={swipeGestures && navigateToNextChapter}
|
||||
config={config}
|
||||
style={{ flex: 1 }}
|
||||
>
|
||||
<Pressable
|
||||
style={{
|
||||
flex: 1,
|
||||
paddingVertical: 16,
|
||||
paddingBottom: 32,
|
||||
paddingHorizontal: `${reader.padding}%`,
|
||||
}}
|
||||
onLayout={scrollToSavedProgress}
|
||||
selectable={selectText}
|
||||
onPress={() => setHidden(!hidden)}
|
||||
onPress={hideHeader}
|
||||
>
|
||||
{chapter.chapterText
|
||||
.trim()
|
||||
.replace(/\[https.*?\]/g, "")}
|
||||
</Text>
|
||||
</View>
|
||||
<TextToComponents
|
||||
text={chapter.chapterText}
|
||||
textStyle={readerStyles}
|
||||
textSize={reader.textSize}
|
||||
textColor={
|
||||
reader.textColor ||
|
||||
readerTextColor(reader.theme)
|
||||
}
|
||||
textSelectable={textSelectable}
|
||||
onPressLink={onPressLink}
|
||||
theme={theme}
|
||||
/>
|
||||
</Pressable>
|
||||
</GestureRecognizer>
|
||||
)}
|
||||
</GestureRecognizer>
|
||||
<Portal>
|
||||
<ReaderSheet
|
||||
theme={theme}
|
||||
reader={reader}
|
||||
dispatch={dispatch}
|
||||
bottomSheetRef={readerSheetRef}
|
||||
selectText={selectText}
|
||||
setSelectText={setSelectText}
|
||||
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>
|
||||
)}
|
||||
<Portal>
|
||||
<ReaderSheet
|
||||
theme={theme}
|
||||
reader={reader}
|
||||
dispatch={dispatch}
|
||||
navigation={navigation}
|
||||
bottomSheetRef={readerSheetRef}
|
||||
selectText={textSelectable}
|
||||
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>
|
||||
)}
|
||||
</>
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -0,0 +1,129 @@
|
||||
import React from "react";
|
||||
import { Dimensions, View, Text, StyleSheet } from "react-native";
|
||||
|
||||
import Slider from "@react-native-community/slider";
|
||||
import { getDeviceOrientation } from "../../../services/utils/helpers";
|
||||
|
||||
const ReaderSeekbar = ({
|
||||
theme,
|
||||
hide,
|
||||
contentSize,
|
||||
scrollPercentage,
|
||||
setLoading,
|
||||
scrollViewRef,
|
||||
}) => {
|
||||
const onSlidingComplete = (value) => {
|
||||
setLoading(true);
|
||||
scrollViewRef.current.scrollTo({
|
||||
x: 0,
|
||||
y: Math.round((value * contentSize) / 100),
|
||||
animated: false,
|
||||
});
|
||||
setLoading(false);
|
||||
};
|
||||
|
||||
if (hide) {
|
||||
return null;
|
||||
} else if (getDeviceOrientation() === "potrait") {
|
||||
return (
|
||||
<View style={styles.verticalSliderContainer}>
|
||||
<Text
|
||||
style={{
|
||||
color: "#FFFFFF",
|
||||
marginLeft: 16,
|
||||
transform: [{ rotate: "-90deg" }],
|
||||
}}
|
||||
>
|
||||
{scrollPercentage}
|
||||
</Text>
|
||||
<Slider
|
||||
style={{
|
||||
flex: 1,
|
||||
height: 40,
|
||||
}}
|
||||
minimumValue={0}
|
||||
maximumValue={100}
|
||||
step={1}
|
||||
value={scrollPercentage}
|
||||
onSlidingComplete={onSlidingComplete}
|
||||
thumbTintColor={theme.colorAccent}
|
||||
minimumTrackTintColor={theme.colorAccent}
|
||||
maximumTrackTintColor={theme.textColorPrimary}
|
||||
/>
|
||||
<Text
|
||||
style={{
|
||||
color: "#FFFFFF",
|
||||
marginRight: 16,
|
||||
transform: [{ rotate: "-90deg" }],
|
||||
}}
|
||||
>
|
||||
100
|
||||
</Text>
|
||||
</View>
|
||||
);
|
||||
} else {
|
||||
return (
|
||||
<View style={styles.horizontalSliderContainer}>
|
||||
<Text
|
||||
style={{
|
||||
color: "#FFFFFF",
|
||||
marginLeft: 16,
|
||||
}}
|
||||
>
|
||||
{scrollPercentage}
|
||||
</Text>
|
||||
<Slider
|
||||
style={{
|
||||
flex: 1,
|
||||
height: 40,
|
||||
}}
|
||||
minimumValue={0}
|
||||
maximumValue={100}
|
||||
step={1}
|
||||
value={scrollPercentage}
|
||||
onSlidingComplete={onSlidingComplete}
|
||||
thumbTintColor={theme.colorAccent}
|
||||
minimumTrackTintColor={theme.colorAccent}
|
||||
maximumTrackTintColor={theme.textColorPrimary}
|
||||
/>
|
||||
<Text
|
||||
style={{
|
||||
color: "#FFFFFF",
|
||||
marginRight: 16,
|
||||
}}
|
||||
>
|
||||
100
|
||||
</Text>
|
||||
</View>
|
||||
);
|
||||
}
|
||||
};
|
||||
|
||||
export default ReaderSeekbar;
|
||||
|
||||
const styles = StyleSheet.create({
|
||||
verticalSliderContainer: {
|
||||
position: "absolute",
|
||||
zIndex: 2,
|
||||
right: -(Dimensions.get("window").width / 2) + 40,
|
||||
bottom: 300,
|
||||
backgroundColor: "rgba(0,0,0,0.7)",
|
||||
flexDirection: "row",
|
||||
justifyContent: "flex-end",
|
||||
alignItems: "center",
|
||||
borderRadius: 50,
|
||||
marginHorizontal: 8,
|
||||
transform: [{ rotate: "90deg" }],
|
||||
},
|
||||
horizontalSliderContainer: {
|
||||
position: "absolute",
|
||||
zIndex: 2,
|
||||
bottom: 100,
|
||||
backgroundColor: "rgba(0,0,0,0.7)",
|
||||
flexDirection: "row",
|
||||
justifyContent: "flex-end",
|
||||
alignItems: "center",
|
||||
borderRadius: 50,
|
||||
marginHorizontal: 8,
|
||||
},
|
||||
});
|
||||
@@ -0,0 +1,257 @@
|
||||
import React, { memo, useCallback, useEffect } from "react";
|
||||
import { Text, View } from "react-native";
|
||||
|
||||
import FitImage from "react-native-fit-image";
|
||||
import { showToast } from "../../hooks/showToast";
|
||||
|
||||
const getHeadingSize = (size) => {
|
||||
const headings = {
|
||||
h1: 6,
|
||||
h2: 5,
|
||||
h3: 4,
|
||||
h4: 3,
|
||||
h5: 2,
|
||||
h6: 1,
|
||||
strong: 0,
|
||||
};
|
||||
|
||||
return headings[size];
|
||||
};
|
||||
|
||||
const Header = ({ size, text, textStyle, textSelectable, textSize }) => (
|
||||
<Text
|
||||
style={[
|
||||
textStyle,
|
||||
{
|
||||
fontSize: textSize + getHeadingSize(size),
|
||||
fontWeight: "bold",
|
||||
},
|
||||
]}
|
||||
selectable={textSelectable}
|
||||
>
|
||||
{text}
|
||||
</Text>
|
||||
);
|
||||
|
||||
const DefaultText = ({ text, textStyle, textSelectable }) => (
|
||||
<Text style={textStyle} selectable={textSelectable}>
|
||||
{text}
|
||||
</Text>
|
||||
);
|
||||
|
||||
const ItalicText = ({ text, textStyle, textSelectable }) => (
|
||||
<Text
|
||||
style={[textStyle, { fontStyle: "italic" }]}
|
||||
selectable={textSelectable}
|
||||
>
|
||||
{text}
|
||||
</Text>
|
||||
);
|
||||
|
||||
const HorizontalRule = ({ textColor }) => (
|
||||
<View
|
||||
style={{
|
||||
borderBottomWidth: 1,
|
||||
flex: 1,
|
||||
borderBottomColor: textColor,
|
||||
marginVertical: 16,
|
||||
}}
|
||||
/>
|
||||
);
|
||||
|
||||
const Link = ({
|
||||
link,
|
||||
text,
|
||||
textStyle,
|
||||
textSelectable,
|
||||
onPressLink,
|
||||
theme,
|
||||
}) => (
|
||||
<Text
|
||||
style={[
|
||||
textStyle,
|
||||
{
|
||||
alignSelf: "flex-start",
|
||||
color: theme.colorAccent,
|
||||
textDecorationLine: "underline",
|
||||
},
|
||||
]}
|
||||
selectable={textSelectable}
|
||||
onPress={() => onPressLink(link)}
|
||||
>
|
||||
{text}
|
||||
</Text>
|
||||
);
|
||||
|
||||
const TextToComponents = ({
|
||||
text,
|
||||
textStyle,
|
||||
textSelectable,
|
||||
textSize,
|
||||
textColor,
|
||||
onPressLink,
|
||||
theme,
|
||||
}) => {
|
||||
try {
|
||||
regex = /(\[(?:\n|.)*?\]\(.*?\))/;
|
||||
|
||||
let parseText = text.trim().split(regex);
|
||||
return parseText.map((part, index) => {
|
||||
if (part) {
|
||||
let match = part.match(/\[((?:\n|.)*?)\]\((.*?)\)/);
|
||||
|
||||
if (match) {
|
||||
tag = match[1];
|
||||
text = match[2];
|
||||
|
||||
if (tag.startsWith("Anchor:")) {
|
||||
link = tag.replace("Anchor: ", "");
|
||||
tag = "a";
|
||||
}
|
||||
|
||||
switch (tag) {
|
||||
case "h1":
|
||||
return (
|
||||
<Header
|
||||
size="h1"
|
||||
text={text}
|
||||
textStyle={textStyle}
|
||||
textSelectable={textSelectable}
|
||||
key={index}
|
||||
textSize={textSize}
|
||||
/>
|
||||
);
|
||||
case "h2":
|
||||
return (
|
||||
<Header
|
||||
size="h2"
|
||||
text={text}
|
||||
textStyle={textStyle}
|
||||
textSelectable={textSelectable}
|
||||
key={index}
|
||||
textSize={textSize}
|
||||
/>
|
||||
);
|
||||
case "h3":
|
||||
return (
|
||||
<Header
|
||||
size="h3"
|
||||
text={text}
|
||||
textStyle={textStyle}
|
||||
textSelectable={textSelectable}
|
||||
key={index}
|
||||
textSize={textSize}
|
||||
/>
|
||||
);
|
||||
case "h4":
|
||||
return (
|
||||
<Header
|
||||
size="h4"
|
||||
text={text}
|
||||
textStyle={textStyle}
|
||||
textSelectable={textSelectable}
|
||||
key={index}
|
||||
textSize={textSize}
|
||||
/>
|
||||
);
|
||||
case "h5":
|
||||
return (
|
||||
<Header
|
||||
size="h5"
|
||||
text={text}
|
||||
textStyle={textStyle}
|
||||
textSelectable={textSelectable}
|
||||
key={index}
|
||||
textSize={textSize}
|
||||
/>
|
||||
);
|
||||
case "h6":
|
||||
return (
|
||||
<Header
|
||||
size="h6"
|
||||
text={text}
|
||||
textStyle={textStyle}
|
||||
textSelectable={textSelectable}
|
||||
key={index}
|
||||
textSize={textSize}
|
||||
/>
|
||||
);
|
||||
case "strong":
|
||||
return (
|
||||
<Header
|
||||
size="strong"
|
||||
text={text}
|
||||
textStyle={textStyle}
|
||||
textSelectable={textSelectable}
|
||||
key={index}
|
||||
textSize={textSize}
|
||||
/>
|
||||
);
|
||||
case "em":
|
||||
return (
|
||||
<ItalicText
|
||||
text={text}
|
||||
textStyle={textStyle}
|
||||
textSelectable={textSelectable}
|
||||
key={index}
|
||||
/>
|
||||
);
|
||||
|
||||
case "a":
|
||||
return (
|
||||
<Link
|
||||
link={text}
|
||||
textStyle={textStyle}
|
||||
textSelectable={textSelectable}
|
||||
text={link}
|
||||
onPressLink={onPressLink}
|
||||
theme={theme}
|
||||
key={index}
|
||||
/>
|
||||
);
|
||||
case "img":
|
||||
return (
|
||||
<FitImage source={{ uri: text }} key={index} />
|
||||
);
|
||||
case "hr":
|
||||
return (
|
||||
<HorizontalRule
|
||||
key={index}
|
||||
textColor={textColor}
|
||||
/>
|
||||
);
|
||||
default:
|
||||
return (
|
||||
<DefaultText
|
||||
text={part}
|
||||
textStyle={textStyle}
|
||||
textSelectable={textSelectable}
|
||||
key={index}
|
||||
/>
|
||||
);
|
||||
}
|
||||
} else {
|
||||
return (
|
||||
<DefaultText
|
||||
text={part}
|
||||
textStyle={textStyle}
|
||||
textSelectable={textSelectable}
|
||||
key={index}
|
||||
/>
|
||||
);
|
||||
}
|
||||
}
|
||||
});
|
||||
} catch (error) {
|
||||
return (
|
||||
<DefaultText
|
||||
text={text}
|
||||
textStyle={textStyle}
|
||||
textSelectable={textSelectable}
|
||||
key={index}
|
||||
/>
|
||||
);
|
||||
}
|
||||
};
|
||||
|
||||
export default memo(TextToComponents);
|
||||
@@ -1,16 +1,19 @@
|
||||
import React, { useState } from "react";
|
||||
import { StyleSheet, View, Text, Dimensions } from "react-native";
|
||||
import { ToggleButton, IconButton, Menu, Switch } from "react-native-paper";
|
||||
import { StyleSheet, View, Text, Dimensions, Pressable } from "react-native";
|
||||
|
||||
import Slider from "@react-native-community/slider";
|
||||
import { ToggleButton, 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";
|
||||
|
||||
const ReaderSheet = ({
|
||||
theme,
|
||||
@@ -18,7 +21,6 @@ const ReaderSheet = ({
|
||||
dispatch,
|
||||
bottomSheetRef,
|
||||
selectText,
|
||||
setSelectText,
|
||||
showScrollPercentage,
|
||||
}) => {
|
||||
const [fontMenu, setFontMenu] = useState(false);
|
||||
@@ -31,9 +33,10 @@ const ReaderSheet = ({
|
||||
|
||||
const textColor = {
|
||||
1: "rgba(255,255,255,0.7)",
|
||||
2: "#000000",
|
||||
3: "#000000",
|
||||
4: "#FFFFFF",
|
||||
2: "#111111",
|
||||
3: "#593100",
|
||||
4: "#CCCCCC",
|
||||
5: "#CCCCCC",
|
||||
};
|
||||
|
||||
return (
|
||||
@@ -46,6 +49,22 @@ const ReaderSheet = ({
|
||||
>
|
||||
<View style={styles.contentContainer}>
|
||||
<BottomSheetHandle theme={theme} />
|
||||
{/* <IconButton
|
||||
icon="cog-outline"
|
||||
color="#FFFFFF"
|
||||
size={24}
|
||||
onPress={() =>
|
||||
navigation.navigate("SettingsStack", {
|
||||
screen: "ReaderSettings",
|
||||
})
|
||||
}
|
||||
style={{
|
||||
marginVertical: 0,
|
||||
position: "absolute",
|
||||
top: 16,
|
||||
right: 16,
|
||||
}}
|
||||
/> */}
|
||||
<View style={styles.readerSettingsContainer}>
|
||||
<ReaderSettingTitle title="Text Size" />
|
||||
<Slider
|
||||
@@ -69,6 +88,7 @@ const ReaderSheet = ({
|
||||
justifyContent: "space-between",
|
||||
width: "100%",
|
||||
paddingHorizontal: 16,
|
||||
paddingVertical: 8,
|
||||
}}
|
||||
>
|
||||
<ReaderSettingTitle title="Reader Theme" />
|
||||
@@ -92,7 +112,7 @@ const ReaderSheet = ({
|
||||
>
|
||||
<ToggleButton
|
||||
icon={reader.theme === 1 && "check"}
|
||||
color="#FFFFFF"
|
||||
color="rgba(255,255,255,0.7)"
|
||||
value={1}
|
||||
style={{
|
||||
backgroundColor: "#000000",
|
||||
@@ -103,12 +123,25 @@ const ReaderSheet = ({
|
||||
borderBottomRightRadius: 50,
|
||||
}}
|
||||
/>
|
||||
{/* <ToggleButton
|
||||
icon={reader.theme === 5 && "check"}
|
||||
color="#CCCCCC"
|
||||
value={5}
|
||||
style={{
|
||||
backgroundColor: "#2B2C30",
|
||||
marginHorizontal: 10,
|
||||
borderWidth: 0,
|
||||
borderRadius: 50,
|
||||
borderTopRightRadius: 50,
|
||||
borderBottomRightRadius: 50,
|
||||
}}
|
||||
/> */}
|
||||
<ToggleButton
|
||||
icon={reader.theme === 4 && "check"}
|
||||
color="#FFFFFF"
|
||||
color="#CCCCCC"
|
||||
value={4}
|
||||
style={{
|
||||
backgroundColor: "#444444",
|
||||
backgroundColor: "#292832",
|
||||
marginHorizontal: 10,
|
||||
borderWidth: 0,
|
||||
borderRadius: 50,
|
||||
@@ -118,7 +151,7 @@ const ReaderSheet = ({
|
||||
/>
|
||||
<ToggleButton
|
||||
icon={reader.theme === 2 && "check"}
|
||||
color="#000000"
|
||||
color="#111111"
|
||||
value={2}
|
||||
style={{
|
||||
backgroundColor: "#FFFFFF",
|
||||
@@ -128,10 +161,10 @@ const ReaderSheet = ({
|
||||
/>
|
||||
<ToggleButton
|
||||
icon={reader.theme === 3 && "check"}
|
||||
color="#000000"
|
||||
color="#593100"
|
||||
value={3}
|
||||
style={{
|
||||
backgroundColor: "#F4ECD8",
|
||||
backgroundColor: "#F7DFC6",
|
||||
marginHorizontal: 10,
|
||||
borderRadius: 50,
|
||||
borderTopLeftRadius: 50,
|
||||
@@ -146,7 +179,7 @@ const ReaderSheet = ({
|
||||
justifyContent: "space-between",
|
||||
width: "100%",
|
||||
paddingHorizontal: 16,
|
||||
marginTop: 16,
|
||||
paddingVertical: 8,
|
||||
}}
|
||||
>
|
||||
<ReaderSettingTitle title="Text Align" />
|
||||
@@ -217,7 +250,7 @@ const ReaderSheet = ({
|
||||
justifyContent: "space-between",
|
||||
width: "100%",
|
||||
paddingHorizontal: 16,
|
||||
marginTop: 16,
|
||||
paddingVertical: 8,
|
||||
}}
|
||||
>
|
||||
<ReaderSettingTitle title="Padding" />
|
||||
@@ -267,7 +300,7 @@ const ReaderSheet = ({
|
||||
justifyContent: "space-between",
|
||||
width: "100%",
|
||||
paddingHorizontal: 16,
|
||||
marginTop: 16,
|
||||
paddingVertical: 8,
|
||||
}}
|
||||
>
|
||||
<ReaderSettingTitle title="Line Height" />
|
||||
@@ -314,13 +347,17 @@ const ReaderSheet = ({
|
||||
/>
|
||||
</Row>
|
||||
</Row>
|
||||
<Row
|
||||
<Pressable
|
||||
style={{
|
||||
flexDirection: "row",
|
||||
alignItems: "center",
|
||||
justifyContent: "space-between",
|
||||
width: "100%",
|
||||
paddingHorizontal: 16,
|
||||
marginTop: 16,
|
||||
paddingVertical: 8,
|
||||
}}
|
||||
android_ripple={{ color: theme.colorAccent }}
|
||||
onPress={openFontMenu}
|
||||
>
|
||||
<ReaderSettingTitle title="Font Style" />
|
||||
<Menu
|
||||
@@ -329,12 +366,11 @@ const ReaderSheet = ({
|
||||
contentStyle={{ backgroundColor: theme.menuColor }}
|
||||
anchor={
|
||||
<Text
|
||||
onPress={openFontMenu}
|
||||
style={{
|
||||
color: "white",
|
||||
paddingHorizontal: 16,
|
||||
fontSize: 16,
|
||||
fontFamily: reader.fontFamily,
|
||||
paddingHorizontal: 16,
|
||||
}}
|
||||
>
|
||||
{fonts.find(
|
||||
@@ -368,14 +404,25 @@ const ReaderSheet = ({
|
||||
/>
|
||||
))}
|
||||
</Menu>
|
||||
</Row>
|
||||
<Row
|
||||
</Pressable>
|
||||
<Pressable
|
||||
style={{
|
||||
flexDirection: "row",
|
||||
alignItems: "center",
|
||||
justifyContent: "space-between",
|
||||
width: "100%",
|
||||
paddingHorizontal: 16,
|
||||
marginTop: 16,
|
||||
paddingVertical: 8,
|
||||
}}
|
||||
android_ripple={{ color: theme.colorAccent }}
|
||||
onPress={() =>
|
||||
dispatch(
|
||||
setAppSettings(
|
||||
"showScrollPercentage",
|
||||
!showScrollPercentage
|
||||
)
|
||||
)
|
||||
}
|
||||
>
|
||||
<ReaderSettingTitle title="Show Progress Percentage" />
|
||||
<Switch
|
||||
@@ -390,14 +437,22 @@ const ReaderSheet = ({
|
||||
}
|
||||
color={theme.colorAccent}
|
||||
/>
|
||||
</Row>
|
||||
<Row
|
||||
</Pressable>
|
||||
<Pressable
|
||||
style={{
|
||||
flexDirection: "row",
|
||||
alignItems: "center",
|
||||
justifyContent: "space-between",
|
||||
width: "100%",
|
||||
paddingHorizontal: 16,
|
||||
marginTop: 16,
|
||||
paddingVertical: 8,
|
||||
}}
|
||||
android_ripple={{ color: theme.colorAccent }}
|
||||
onPress={() =>
|
||||
dispatch(
|
||||
setAppSettings("textSelectable", !selectText)
|
||||
)
|
||||
}
|
||||
>
|
||||
<View>
|
||||
<ReaderSettingTitle title="Select Text " />
|
||||
@@ -412,10 +467,17 @@ const ReaderSheet = ({
|
||||
</View>
|
||||
<Switch
|
||||
value={selectText}
|
||||
onValueChange={setSelectText}
|
||||
onValueChange={() =>
|
||||
dispatch(
|
||||
setAppSettings(
|
||||
"textSelectable",
|
||||
!selectText
|
||||
)
|
||||
)
|
||||
}
|
||||
color={theme.colorAccent}
|
||||
/>
|
||||
</Row>
|
||||
</Pressable>
|
||||
</View>
|
||||
</View>
|
||||
</Bottomsheet>
|
||||
|
||||
@@ -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,
|
||||
}}
|
||||
>
|
||||
|
||||
@@ -2,18 +2,24 @@ export const readerBackground = (val) => {
|
||||
const backgroundColor = {
|
||||
1: "#000000",
|
||||
2: "#FFFFFF",
|
||||
3: "#F4ECD8",
|
||||
4: "#444444",
|
||||
3: "#F7DFC6",
|
||||
4: "#292832",
|
||||
5: "#2B2C30",
|
||||
};
|
||||
|
||||
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",
|
||||
};
|
||||
|
||||
return textColor ?? val;
|
||||
return textColor[val] || val;
|
||||
};
|
||||
|
||||
export const readerLineHeight = (fontSize, lineHeightMultiplier) => {
|
||||
|
||||
@@ -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";
|
||||
|
||||
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,6 +120,7 @@ const Extension = ({ navigation, route }) => {
|
||||
})
|
||||
}
|
||||
libraryStatus={checkIFInLibrary(item.sourceId, item.novelUrl)}
|
||||
theme={theme}
|
||||
/>
|
||||
);
|
||||
|
||||
@@ -141,7 +147,7 @@ const Extension = ({ navigation, route }) => {
|
||||
return icons[displayMode];
|
||||
};
|
||||
|
||||
const getDisplayMode = () =>
|
||||
const getNextDisplayMode = () =>
|
||||
displayMode === 0 ? 1 : displayMode === 1 ? 2 : displayMode === 2 && 0;
|
||||
|
||||
return (
|
||||
@@ -166,7 +172,10 @@ const Extension = ({ navigation, route }) => {
|
||||
color: theme.textColorSecondary,
|
||||
onPress: () =>
|
||||
dispatch(
|
||||
setAppSettings("displayMode", getDisplayMode())
|
||||
setAppSettings(
|
||||
"displayMode",
|
||||
getNextDisplayMode()
|
||||
)
|
||||
),
|
||||
},
|
||||
{
|
||||
@@ -178,9 +187,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}
|
||||
@@ -189,7 +196,8 @@ const Extension = ({ navigation, route }) => {
|
||||
onScroll={onScroll}
|
||||
ListFooterComponent={
|
||||
!searchText &&
|
||||
page < totalPages && (
|
||||
page < totalPages &&
|
||||
novels.length && (
|
||||
<View style={{ paddingVertical: 16 }}>
|
||||
<ActivityIndicator color={theme.colorAccent} />
|
||||
</View>
|
||||
|
||||
@@ -8,7 +8,7 @@ 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";
|
||||
@@ -87,16 +87,12 @@ const History = ({ navigation }) => {
|
||||
]}
|
||||
/>
|
||||
<FlatList
|
||||
contentContainerStyle={{
|
||||
flexGrow: 1,
|
||||
paddingVertical: 8,
|
||||
paddingHorizontal: 16,
|
||||
}}
|
||||
contentContainerStyle={{ flexGrow: 1 }}
|
||||
data={searchText ? searchResults : history}
|
||||
keyExtractor={(index) => index.toString()}
|
||||
keyExtractor={(item, index) => item.date}
|
||||
renderItem={({ item }) => (
|
||||
<FlatList
|
||||
keyExtractor={(item, index) => index.toString()}
|
||||
keyExtractor={(item, index) => item.novelId.toString()}
|
||||
data={item.novels}
|
||||
renderItem={renderHistoryCard}
|
||||
ListHeaderComponent={
|
||||
@@ -104,6 +100,7 @@ const History = ({ navigation }) => {
|
||||
style={{
|
||||
textTransform: "uppercase",
|
||||
paddingVertical: 8,
|
||||
paddingHorizontal: 16,
|
||||
color: theme.textColorSecondary,
|
||||
}}
|
||||
>
|
||||
|
||||
@@ -46,12 +46,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,
|
||||
@@ -106,8 +106,8 @@ const HistoryItem = ({ history, theme, dispatch, navigation }) => {
|
||||
onPress={navigateToChapter}
|
||||
/>
|
||||
</View>
|
||||
</Pressable>
|
||||
</View>
|
||||
</View>
|
||||
</Pressable>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -115,14 +115,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,
|
||||
|
||||
@@ -1,17 +1,15 @@
|
||||
import React, { useState, useCallback, useRef } from "react";
|
||||
import React, { useState, useCallback, useRef, useEffect } from "react";
|
||||
import {
|
||||
StyleSheet,
|
||||
Text,
|
||||
View,
|
||||
RefreshControl,
|
||||
ActivityIndicator,
|
||||
TouchableOpacity,
|
||||
} from "react-native";
|
||||
import { useFocusEffect } from "@react-navigation/native";
|
||||
import { useSelector, useDispatch } from "react-redux";
|
||||
import MaterialCommunityIcons from "react-native-vector-icons/MaterialCommunityIcons";
|
||||
|
||||
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";
|
||||
@@ -29,7 +27,12 @@ import LibraryFilterSheet from "./components/LibraryFilterSheet";
|
||||
const LibraryScreen = ({ navigation }) => {
|
||||
const theme = useTheme();
|
||||
const dispatch = useDispatch();
|
||||
const { showNumberOfNovels, incognitoMode = false } = useSettings();
|
||||
const settings = useSettings();
|
||||
const {
|
||||
showNumberOfNovels,
|
||||
incognitoMode = false,
|
||||
updateLibraryOnLaunch = false,
|
||||
} = settings;
|
||||
|
||||
const libraryFilterSheetRef = useRef(null);
|
||||
|
||||
@@ -49,6 +52,10 @@ const LibraryScreen = ({ navigation }) => {
|
||||
}, [getLibraryAction, sort, filter])
|
||||
);
|
||||
|
||||
useEffect(() => {
|
||||
updateLibraryOnLaunch && dispatch(updateLibraryAction());
|
||||
}, []);
|
||||
|
||||
const onRefresh = () => {
|
||||
setRefreshing(true);
|
||||
dispatch(updateLibraryAction());
|
||||
@@ -65,14 +72,18 @@ const LibraryScreen = ({ navigation }) => {
|
||||
dispatch(searchLibraryAction(text, sort, filter));
|
||||
};
|
||||
|
||||
const renderItem = ({ item }) => (
|
||||
<NovelCover
|
||||
item={item}
|
||||
onPress={() => {
|
||||
navigation.navigate("Novel", item);
|
||||
dispatch(setNovel(item));
|
||||
}}
|
||||
/>
|
||||
const renderItem = useCallback(
|
||||
({ item }) => (
|
||||
<NovelCover
|
||||
item={item}
|
||||
onPress={() => {
|
||||
navigation.navigate("Novel", item);
|
||||
dispatch(setNovel(item));
|
||||
}}
|
||||
theme={theme}
|
||||
/>
|
||||
),
|
||||
[]
|
||||
);
|
||||
|
||||
const refreshControl = () => (
|
||||
|
||||
@@ -17,19 +17,19 @@ const AboutScreen = ({ navigation }) => {
|
||||
<ListSection>
|
||||
<ListItem
|
||||
title="Version"
|
||||
description="Stable 1.1.0"
|
||||
description="Stable 1.1.1"
|
||||
theme={theme}
|
||||
/>
|
||||
<ListItem
|
||||
title="Build Time"
|
||||
description="20-06-21 21:45 PM"
|
||||
description="04-07-21 21:00 PM"
|
||||
theme={theme}
|
||||
/>
|
||||
<ListItem
|
||||
title="What's new"
|
||||
onPress={() =>
|
||||
Linking.openURL(
|
||||
"https://github.com/rajarsheechatterjee/lnreader/commits/main"
|
||||
"https://github.com/LNReader/lnreader/commits/main"
|
||||
)
|
||||
}
|
||||
theme={theme}
|
||||
@@ -45,20 +45,20 @@ const AboutScreen = ({ navigation }) => {
|
||||
/>
|
||||
<ListItem
|
||||
title="Github"
|
||||
description="https://github.com/rajarsheechatterjee/lnreader"
|
||||
description="https://github.com/LNReader/lnreader"
|
||||
onPress={() =>
|
||||
Linking.openURL(
|
||||
"https://github.com/rajarsheechatterjee/lnreader"
|
||||
"https://github.com/LNReader/lnreader"
|
||||
)
|
||||
}
|
||||
theme={theme}
|
||||
/>
|
||||
<ListItem
|
||||
title="Sources"
|
||||
description="https://github.com/rajarsheechatterjee/lnreader-sources"
|
||||
description="https://github.com/LNReader/lnreader-sources"
|
||||
onPress={() =>
|
||||
Linking.openURL(
|
||||
"https://github.com/rajarsheechatterjee/lnreader-sources"
|
||||
"https://github.com/LNReader/lnreader-sources"
|
||||
)
|
||||
}
|
||||
theme={theme}
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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 },
|
||||
});
|
||||
@@ -10,12 +10,18 @@ import {
|
||||
} from "../../components/List";
|
||||
import { ScreenContainer } from "../../components/Common";
|
||||
|
||||
import { useTheme } from "../../hooks/reduxHooks";
|
||||
import { useSettings, useTheme } from "../../hooks/reduxHooks";
|
||||
import DisplayModeModal from "./components/DisplayModeModal";
|
||||
import GridSizeModal from "./components/GridSizeModal";
|
||||
import ThemeModal from "./components/ThemeModal";
|
||||
import { setAccentColor } from "../../redux/settings/settings.actions";
|
||||
import {
|
||||
setAccentColor,
|
||||
setAmoledMode,
|
||||
setAppSettings,
|
||||
setRippleColor,
|
||||
} from "../../redux/settings/settings.actions";
|
||||
import ColorPickerModal from "../../components/ColorPickerModal";
|
||||
import SwitchSetting from "../../components/Switch/Switch";
|
||||
|
||||
const GenralSettings = ({ navigation }) => {
|
||||
const theme = useTheme();
|
||||
@@ -25,6 +31,9 @@ const GenralSettings = ({ navigation }) => {
|
||||
(state) => state.settingsReducer
|
||||
);
|
||||
|
||||
const { updateLibraryOnLaunch = false, downloadNewChapters = false } =
|
||||
useSettings();
|
||||
|
||||
const displayModeLabel = (displayMode) => {
|
||||
const label = {
|
||||
0: "Compact Grid",
|
||||
@@ -63,9 +72,27 @@ 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()} />
|
||||
<Appbar title="General" onBackAction={navigation.goBack} />
|
||||
<ListSection>
|
||||
<ListSubHeader theme={theme}>Display</ListSubHeader>
|
||||
<ListItem
|
||||
@@ -81,6 +108,34 @@ const GenralSettings = ({ navigation }) => {
|
||||
theme={theme}
|
||||
/>
|
||||
<Divider theme={theme} />
|
||||
<ListSubHeader theme={theme}>Library</ListSubHeader>
|
||||
<SwitchSetting
|
||||
label="Update library on launch"
|
||||
value={updateLibraryOnLaunch}
|
||||
onPress={() =>
|
||||
dispatch(
|
||||
setAppSettings(
|
||||
"updateLibraryOnLaunch",
|
||||
!updateLibraryOnLaunch
|
||||
)
|
||||
)
|
||||
}
|
||||
theme={theme}
|
||||
/>
|
||||
<ListSubHeader theme={theme}>Auto-download</ListSubHeader>
|
||||
<SwitchSetting
|
||||
label="Download new chapters"
|
||||
value={downloadNewChapters}
|
||||
onPress={() =>
|
||||
dispatch(
|
||||
setAppSettings(
|
||||
"downloadNewChapters",
|
||||
!downloadNewChapters
|
||||
)
|
||||
)
|
||||
}
|
||||
theme={theme}
|
||||
/>
|
||||
<ListSubHeader theme={theme}>Theme</ListSubHeader>
|
||||
<ListItem
|
||||
title="Theme"
|
||||
@@ -96,6 +151,27 @@ const GenralSettings = ({ navigation }) => {
|
||||
iconColor={theme.colorAccent}
|
||||
right="circle"
|
||||
/>
|
||||
{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}
|
||||
/>
|
||||
)}
|
||||
</ListSection>
|
||||
<DisplayModeModal
|
||||
displayMode={displayMode}
|
||||
@@ -122,7 +198,7 @@ const GenralSettings = ({ navigation }) => {
|
||||
modalVisible={accentColorModal}
|
||||
hideModal={hideAccentColorModal}
|
||||
color={theme.colorAccent}
|
||||
onSubmit={(val) => dispatch(setAccentColor(val))}
|
||||
onSubmit={onSubmit}
|
||||
theme={theme}
|
||||
/>
|
||||
</ScreenContainer>
|
||||
|
||||
@@ -90,7 +90,7 @@ const MoreScreen = ({ navigation }) => {
|
||||
downloadQueue.length > 0 &&
|
||||
downloadQueue.length + " remaining"
|
||||
}
|
||||
icon="download-outline"
|
||||
icon="progress-download"
|
||||
onPress={() =>
|
||||
navigation.navigate("MoreStack", {
|
||||
screen: "DownloadQueue",
|
||||
@@ -98,6 +98,17 @@ const MoreScreen = ({ navigation }) => {
|
||||
}
|
||||
theme={theme}
|
||||
/>
|
||||
<ListItem
|
||||
title="Downloads"
|
||||
icon="folder-download"
|
||||
onPress={() =>
|
||||
navigation.navigate("MoreStack", {
|
||||
screen: "Downloads",
|
||||
})
|
||||
}
|
||||
theme={theme}
|
||||
/>
|
||||
|
||||
<ListItem
|
||||
title="Settings"
|
||||
icon="cog-outline"
|
||||
|
||||
@@ -26,7 +26,7 @@ const ReaderSettings = ({ navigation }) => {
|
||||
1: "rgba(255,255,255,0.7)",
|
||||
2: "#000000",
|
||||
3: "#000000",
|
||||
4: "#FFFFFF",
|
||||
4: "#CCCCCC",
|
||||
};
|
||||
|
||||
/**
|
||||
@@ -75,9 +75,10 @@ const ReaderSettings = ({ navigation }) => {
|
||||
backgroundColor: "#000000",
|
||||
textColor: "rgba(255,255,255,0.7)",
|
||||
},
|
||||
{ value: 2, backgroundColor: "#FFFFFF", textColor: "#000000" },
|
||||
{ value: 3, backgroundColor: "#F4ECD8", textColor: "#000000" },
|
||||
{ value: 4, backgroundColor: "#444444", textColor: "#FFFFFF" },
|
||||
{ value: 2, backgroundColor: "#FFFFFF", textColor: "#111111" },
|
||||
{ value: 3, backgroundColor: "#F7DFC6", textColor: "#593100" },
|
||||
{ value: 4, backgroundColor: "#292832", textColor: "#CCCCCC" },
|
||||
{ value: 5, backgroundColor: "#2B2C30", textColor: "#CCCCCC" },
|
||||
];
|
||||
|
||||
const onValueChange = (value) => {
|
||||
|
||||
@@ -1,80 +0,0 @@
|
||||
import React, { useState } from "react";
|
||||
import { StyleSheet, Text } from "react-native";
|
||||
import { Modal, Portal, TextInput } from "react-native-paper";
|
||||
|
||||
const AccentColorModal = ({
|
||||
accentColorModal,
|
||||
hideAccentColorModal,
|
||||
theme,
|
||||
onPress,
|
||||
color,
|
||||
}) => {
|
||||
const [text, setText] = useState(color);
|
||||
const [error, setError] = useState();
|
||||
|
||||
return (
|
||||
<Portal>
|
||||
<Modal
|
||||
visible={accentColorModal}
|
||||
onDismiss={() => {
|
||||
hideAccentColorModal();
|
||||
setText();
|
||||
setError();
|
||||
}}
|
||||
contentContainerStyle={[
|
||||
styles.containerStyle,
|
||||
{ backgroundColor: theme.colorPrimary },
|
||||
]}
|
||||
>
|
||||
<Text
|
||||
style={[
|
||||
styles.dialogTitle,
|
||||
{ color: theme.textColorPrimary },
|
||||
]}
|
||||
>
|
||||
Hex Color Code
|
||||
</Text>
|
||||
<TextInput
|
||||
value={text}
|
||||
defaultValue={color}
|
||||
onChangeText={(text) => setText(text)}
|
||||
onSubmitEditing={() => {
|
||||
if (text.match(/^#([0-9a-f]{6}|[0-9a-f]{3})$/i)) {
|
||||
onPress(text);
|
||||
hideAccentColorModal();
|
||||
} else {
|
||||
setError("Enter a valid hex color code");
|
||||
}
|
||||
}}
|
||||
mode="outlined"
|
||||
theme={{
|
||||
colors: {
|
||||
primary: color,
|
||||
placeholder: theme.textColorHint,
|
||||
text: theme.textColorPrimary,
|
||||
background: "transparent",
|
||||
},
|
||||
}}
|
||||
underlineColor={theme.textColorHint}
|
||||
dense
|
||||
error={error}
|
||||
/>
|
||||
<Text style={{ color: "#FF0033", paddingTop: 8 }}>{error}</Text>
|
||||
</Modal>
|
||||
</Portal>
|
||||
);
|
||||
};
|
||||
|
||||
export default AccentColorModal;
|
||||
|
||||
const styles = StyleSheet.create({
|
||||
containerStyle: {
|
||||
margin: 30,
|
||||
padding: 20,
|
||||
borderRadius: 8,
|
||||
},
|
||||
dialogTitle: {
|
||||
fontSize: 18,
|
||||
marginBottom: 16,
|
||||
},
|
||||
});
|
||||
@@ -1,5 +1,5 @@
|
||||
import React from "react";
|
||||
import { StyleSheet } from "react-native";
|
||||
import { StyleSheet, View, Text, Pressable } from "react-native";
|
||||
import { Portal, Modal, Checkbox } from "react-native-paper";
|
||||
import { setAppSettings } from "../../../redux/settings/settings.actions";
|
||||
|
||||
@@ -18,20 +18,39 @@ const DisplayModeModal = ({
|
||||
|
||||
const renderCheckboxes = () => {
|
||||
return displayModes.map((mode) => (
|
||||
<Checkbox.Item
|
||||
<View
|
||||
key={mode.displayMode}
|
||||
label={mode.label}
|
||||
labelStyle={{ color: theme.textColorPrimary }}
|
||||
status={
|
||||
displayMode === mode.displayMode ? "checked" : "unchecked"
|
||||
}
|
||||
mode="ios"
|
||||
uncheckedColor={theme.textColorSecondary}
|
||||
color={theme.colorAccent}
|
||||
onPress={() =>
|
||||
dispatch(setAppSettings("displayMode", mode.displayMode))
|
||||
}
|
||||
/>
|
||||
style={[
|
||||
{
|
||||
borderRadius: 8,
|
||||
overflow: "hidden",
|
||||
marginVertical: 4,
|
||||
},
|
||||
displayMode === mode.displayMode && {
|
||||
backgroundColor: theme.rippleColor,
|
||||
},
|
||||
]}
|
||||
>
|
||||
<Pressable
|
||||
android_ripple={{
|
||||
color: theme.rippleColor,
|
||||
}}
|
||||
style={{
|
||||
paddingHorizontal: 16,
|
||||
paddingVertical: 12,
|
||||
justifyContent: "space-between",
|
||||
}}
|
||||
onPress={() =>
|
||||
dispatch(
|
||||
setAppSettings("displayMode", mode.displayMode)
|
||||
)
|
||||
}
|
||||
>
|
||||
<Text style={{ color: theme.textColorPrimary }}>
|
||||
{mode.label}
|
||||
</Text>
|
||||
</Pressable>
|
||||
</View>
|
||||
));
|
||||
};
|
||||
|
||||
|
||||
@@ -46,17 +46,6 @@ const GridSizeModal = ({
|
||||
>
|
||||
{`${novelsPerRow} per row`}
|
||||
</Text>
|
||||
{/* <Slider
|
||||
style={styles.slider}
|
||||
value={novelsPerRow}
|
||||
minimumValue={1}
|
||||
maximumValue={5}
|
||||
step={1}
|
||||
minimumTrackTintColor={theme.colorAccent}
|
||||
maximumTrackTintColor="#000000"
|
||||
thumbTintColor={theme.colorAccent}
|
||||
onValueChange={(value) => dispatch(setNovelsPerRow(value))}
|
||||
/> */}
|
||||
<RadioButtonGroup
|
||||
onValueChange={(value) => dispatch(setNovelsPerRow(value))}
|
||||
value={novelsPerRow}
|
||||
|
||||
@@ -0,0 +1,53 @@
|
||||
import React from "react";
|
||||
|
||||
import { Button, Dialog, Portal } from "react-native-paper";
|
||||
|
||||
const RemoveDownloadsDialog = ({
|
||||
dialogVisible,
|
||||
hideDialog,
|
||||
theme,
|
||||
onSubmit,
|
||||
}) => {
|
||||
return (
|
||||
<Portal>
|
||||
<Dialog
|
||||
visible={dialogVisible}
|
||||
onDismiss={hideDialog}
|
||||
style={{
|
||||
borderRadius: 6,
|
||||
backgroundColor: theme.colorPrimary,
|
||||
}}
|
||||
>
|
||||
<Dialog.Title
|
||||
style={{
|
||||
letterSpacing: 0,
|
||||
fontSize: 16,
|
||||
color: theme.textColorPrimary,
|
||||
}}
|
||||
>
|
||||
Are you sure? All downloaded chapters will be deleted.
|
||||
</Dialog.Title>
|
||||
<Dialog.Actions>
|
||||
<Button
|
||||
uppercase={false}
|
||||
theme={{ colors: { primary: theme.colorAccent } }}
|
||||
onPress={hideDialog}
|
||||
labelStyle={{ letterSpacing: 0 }}
|
||||
>
|
||||
Cancel
|
||||
</Button>
|
||||
<Button
|
||||
uppercase={false}
|
||||
theme={{ colors: { primary: theme.colorAccent } }}
|
||||
onPress={onSubmit}
|
||||
labelStyle={{ letterSpacing: 0 }}
|
||||
>
|
||||
Ok
|
||||
</Button>
|
||||
</Dialog.Actions>
|
||||
</Dialog>
|
||||
</Portal>
|
||||
);
|
||||
};
|
||||
|
||||
export default RemoveDownloadsDialog;
|
||||
@@ -1,37 +1,53 @@
|
||||
import React from "react";
|
||||
import { StyleSheet } from "react-native";
|
||||
import { Portal, Modal, Checkbox } from "react-native-paper";
|
||||
import { Pressable, StyleSheet, View, Text } from "react-native";
|
||||
|
||||
import { Portal, Modal } from "react-native-paper";
|
||||
|
||||
import { setStatusBarStyle } from "expo-status-bar";
|
||||
import { setAppTheme } from "../../../redux/settings/settings.actions";
|
||||
import ImmersiveMode from "react-native-immersive-mode";
|
||||
|
||||
const ThemeModal = ({ themeModalVisible, hidethemeModal, dispatch, theme }) => {
|
||||
const themes = [
|
||||
{ id: 0, name: "AMOLED Dark", statusBar: "light" },
|
||||
{ id: 1, name: "Light", statusBar: "dark" },
|
||||
{ id: 2, name: "Dark", statusBar: "light" },
|
||||
{ id: 3, name: "Midnight Dusk", statusBar: "light" },
|
||||
{ id: 4, name: "Lime", statusBar: "light" },
|
||||
{ id: 5, name: "Iris Blue", statusBar: "light" },
|
||||
{ id: 1, name: "Light" },
|
||||
{ id: 2, name: "Dark" },
|
||||
{ id: 3, name: "Midnight Dusk" },
|
||||
{ id: 4, name: "Green Apple" },
|
||||
{ id: 5, name: "Iris Blue" },
|
||||
{ id: 7, name: "Strawberry Daiquiri" },
|
||||
];
|
||||
|
||||
const renderThemeCheckboxes = () =>
|
||||
themes.map((item) => (
|
||||
<Checkbox.Item
|
||||
key={item.id}
|
||||
label={item.name}
|
||||
labelStyle={{ color: theme.textColorPrimary }}
|
||||
status={theme.id === item.id ? "checked" : "unchecked"}
|
||||
mode="ios"
|
||||
color={theme.colorAccent}
|
||||
onPress={() => {
|
||||
dispatch(setAppTheme(item.id));
|
||||
// setStatusBarStyle(item.statusBar);
|
||||
ImmersiveMode.setBarColor(item.colorPrimary);
|
||||
const ThemeItem = ({ item }) => (
|
||||
<View
|
||||
style={[
|
||||
{
|
||||
borderRadius: 8,
|
||||
overflow: "hidden",
|
||||
marginVertical: 4,
|
||||
},
|
||||
theme.id === item.id && {
|
||||
backgroundColor: theme.rippleColor,
|
||||
},
|
||||
]}
|
||||
>
|
||||
<Pressable
|
||||
android_ripple={{
|
||||
color: theme.rippleColor,
|
||||
}}
|
||||
/>
|
||||
));
|
||||
style={{
|
||||
paddingHorizontal: 16,
|
||||
paddingVertical: 12,
|
||||
justifyContent: "space-between",
|
||||
}}
|
||||
onPress={() => dispatch(setAppTheme(item.id))}
|
||||
>
|
||||
<Text style={{ color: theme.textColorPrimary }}>
|
||||
{item.name}
|
||||
</Text>
|
||||
</Pressable>
|
||||
</View>
|
||||
);
|
||||
|
||||
const renderThemeCheckboxes = () =>
|
||||
themes.map((item) => <ThemeItem item={item} key={item.id} />);
|
||||
|
||||
return (
|
||||
<Portal>
|
||||
|
||||
+117
-93
@@ -1,4 +1,4 @@
|
||||
import React, { useEffect, useRef, useState } from "react";
|
||||
import React, { useCallback, useEffect, useRef, useState } from "react";
|
||||
import {
|
||||
StyleSheet,
|
||||
View,
|
||||
@@ -15,9 +15,10 @@ import { useDispatch, useSelector } from "react-redux";
|
||||
import {
|
||||
bookmarkChapterAction,
|
||||
deleteAllChaptersAction,
|
||||
deleteChapterAction,
|
||||
downloadAllChaptersAction,
|
||||
downloadChapterAction,
|
||||
getNovelAction,
|
||||
markChapterReadAction,
|
||||
markChaptersRead,
|
||||
markChapterUnreadAction,
|
||||
markPreviousChaptersReadAction,
|
||||
@@ -36,8 +37,9 @@ import ChapterItem from "./components/ChapterItem";
|
||||
import NovelInfoHeader from "./components/NovelHeader";
|
||||
import ChaptersSettingsSheet from "./components/ChaptersSettingsSheet";
|
||||
import TrackSheet from "./components/Tracker/TrackSheet";
|
||||
import ChapterActionsSheet from "./components/ChapterActionsSheet";
|
||||
import { Row } from "../../components/Common";
|
||||
import JumpToChapterModal from "./components/JumpToChapterModal";
|
||||
import { Actionbar } from "../../components/Actionbar/Actionbar";
|
||||
|
||||
const Novel = ({ route, navigation }) => {
|
||||
const item = route.params;
|
||||
@@ -45,15 +47,15 @@ const Novel = ({ route, navigation }) => {
|
||||
|
||||
const theme = useTheme();
|
||||
const dispatch = useDispatch();
|
||||
const { novel, chapters, loading, updating, downloading } = useNovel();
|
||||
const { novel, chapters, loading, updating } = useNovel();
|
||||
const { downloadQueue } = useSelector((state) => state.downloadsReducer);
|
||||
|
||||
const [selected, setSelected] = useState([]);
|
||||
const [downloadMenu, showDownloadMenu] = useState(false);
|
||||
|
||||
let flatlistRef = useRef(null);
|
||||
let chaptersSettingsSheetRef = useRef(null);
|
||||
let trackerSheetRef = useRef(null);
|
||||
let chapterActionsSheetRef = useRef(null);
|
||||
|
||||
const {
|
||||
sort = "ORDER BY chapterId ASC",
|
||||
@@ -86,25 +88,69 @@ const Novel = ({ route, navigation }) => {
|
||||
/>
|
||||
);
|
||||
|
||||
const [jumpToChapterModal, showJumpToChapterModal] = useState(false);
|
||||
|
||||
const keyExtractor = useCallback((item) => item.chapterId.toString(), []);
|
||||
|
||||
const downloadChapter = (chapterUrl, chapterName, chapterId) =>
|
||||
dispatch(
|
||||
downloadChapterAction(
|
||||
sourceId,
|
||||
novelUrl,
|
||||
chapterUrl,
|
||||
chapterName,
|
||||
chapterId
|
||||
)
|
||||
);
|
||||
|
||||
const deleteChapter = (chapterId, chapterName) =>
|
||||
dispatch(deleteChapterAction(chapterId, chapterName));
|
||||
|
||||
const isSelected = (chapterId) => {
|
||||
return selected.some((obj) => obj.chapterId === chapterId);
|
||||
};
|
||||
|
||||
const onSelectPress = (chapter, navigateToChapter) => {
|
||||
if (selected.length === 0) {
|
||||
navigateToChapter();
|
||||
} else {
|
||||
if (isSelected(chapter.chapterId)) {
|
||||
setSelected((selected) =>
|
||||
selected.filter(
|
||||
(item) => item.chapterId !== chapter.chapterId
|
||||
)
|
||||
);
|
||||
} else {
|
||||
setSelected((selected) => [...selected, chapter]);
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
const onSelectLongPress = (chapter) => {
|
||||
if (isSelected(chapter.chapterId)) {
|
||||
setSelected((selected) =>
|
||||
selected.filter((item) => item.chapterId !== chapter.chapterId)
|
||||
);
|
||||
} else {
|
||||
setSelected((selected) => [...selected, chapter]);
|
||||
}
|
||||
};
|
||||
|
||||
const renderItem = ({ item, index }) => (
|
||||
<ChapterItem
|
||||
theme={theme}
|
||||
chapter={item}
|
||||
index={index}
|
||||
novelUrl={novelUrl}
|
||||
dispatch={dispatch}
|
||||
sourceId={sourceId}
|
||||
novelData={{ sourceId, novelUrl, novelName }}
|
||||
navigation={navigation}
|
||||
lastReadChapter={lastReadChapter}
|
||||
position={position}
|
||||
downloading={downloading}
|
||||
selected={selected}
|
||||
novelName={novelName}
|
||||
selected={selected}
|
||||
setSelected={setSelected}
|
||||
showChapterTitles={showChapterTitles}
|
||||
chapterActionsSheetRef={chapterActionsSheetRef}
|
||||
downloadQueue={downloadQueue}
|
||||
deleteChapter={deleteChapter}
|
||||
downloadChapter={downloadChapter}
|
||||
isSelected={isSelected}
|
||||
onSelectPress={onSelectPress}
|
||||
onSelectLongPress={onSelectLongPress}
|
||||
/>
|
||||
);
|
||||
|
||||
@@ -217,6 +263,15 @@ const Novel = ({ route, navigation }) => {
|
||||
}
|
||||
/>
|
||||
</Menu>
|
||||
<IconButton
|
||||
icon="text-box-search-outline"
|
||||
color="white"
|
||||
size={21}
|
||||
style={{
|
||||
marginTop: StatusBar.currentHeight + 8,
|
||||
}}
|
||||
onPress={() => showJumpToChapterModal(true)}
|
||||
/>
|
||||
<IconButton
|
||||
icon="share-variant"
|
||||
color="white"
|
||||
@@ -235,8 +290,9 @@ const Novel = ({ route, navigation }) => {
|
||||
</View>
|
||||
)}
|
||||
<FlatList
|
||||
ref={(ref) => (flatlistRef.current = ref)}
|
||||
data={!loading && chapters}
|
||||
keyExtractor={(item) => item.chapterId.toString()}
|
||||
keyExtractor={keyExtractor}
|
||||
removeClippedSubviews={true}
|
||||
maxToRenderPerBatch={5}
|
||||
windowSize={15}
|
||||
@@ -260,26 +316,12 @@ const Novel = ({ route, navigation }) => {
|
||||
refreshControl={refreshControl()}
|
||||
/>
|
||||
{selected.length > 0 && (
|
||||
<View
|
||||
style={{
|
||||
position: "absolute",
|
||||
width: Dimensions.get("window").width - 32,
|
||||
bottom: 0,
|
||||
margin: 16,
|
||||
backgroundColor: theme.colorPrimary,
|
||||
borderRadius: 6,
|
||||
flexDirection: "row",
|
||||
justifyContent: "space-between",
|
||||
paddingHorizontal: 8,
|
||||
paddingVertical: 4,
|
||||
elevation: 2,
|
||||
}}
|
||||
>
|
||||
{selected.some((obj) => obj.downloaded === 0) && (
|
||||
<Appbar.Action
|
||||
icon="download-outline"
|
||||
color={theme.textColorPrimary}
|
||||
onPress={() => {
|
||||
<Actionbar
|
||||
theme={theme}
|
||||
actions={[
|
||||
selected.some((obj) => obj.downloaded === 0) && {
|
||||
icon: "download-outline",
|
||||
onPress: () => {
|
||||
dispatch(
|
||||
downloadAllChaptersAction(
|
||||
novel.sourceId,
|
||||
@@ -288,33 +330,25 @@ const Novel = ({ route, navigation }) => {
|
||||
)
|
||||
);
|
||||
setSelected([]);
|
||||
}}
|
||||
/>
|
||||
)}
|
||||
{selected.some((obj) => obj.downloaded === 1) && (
|
||||
<Appbar.Action
|
||||
icon="trash-can-outline"
|
||||
color={theme.textColorPrimary}
|
||||
onPress={() => {
|
||||
},
|
||||
},
|
||||
selected.some((obj) => obj.downloaded === 1) && {
|
||||
icon: "trash-can-outline",
|
||||
onPress: () => {
|
||||
dispatch(deleteAllChaptersAction(selected));
|
||||
setSelected([]);
|
||||
}}
|
||||
/>
|
||||
)}
|
||||
<Appbar.Action
|
||||
icon="bookmark-outline"
|
||||
color={theme.textColorPrimary}
|
||||
onPress={() => {
|
||||
dispatch(bookmarkChapterAction(selected));
|
||||
setSelected([]);
|
||||
}}
|
||||
/>
|
||||
|
||||
{selected.some((obj) => obj.read === 0) && (
|
||||
<Appbar.Action
|
||||
icon="check"
|
||||
color={theme.textColorPrimary}
|
||||
onPress={() => {
|
||||
},
|
||||
},
|
||||
{
|
||||
icon: "bookmark-outline",
|
||||
onPress: () => {
|
||||
dispatch(bookmarkChapterAction(selected));
|
||||
setSelected([]);
|
||||
},
|
||||
},
|
||||
selected.some((obj) => obj.read === 0) && {
|
||||
icon: "check",
|
||||
onPress: () => {
|
||||
dispatch(
|
||||
markChaptersRead(
|
||||
selected,
|
||||
@@ -324,15 +358,11 @@ const Novel = ({ route, navigation }) => {
|
||||
)
|
||||
);
|
||||
setSelected([]);
|
||||
}}
|
||||
/>
|
||||
)}
|
||||
|
||||
{selected.some((obj) => obj.read === 1) && (
|
||||
<Appbar.Action
|
||||
icon="check-outline"
|
||||
color={theme.textColorPrimary}
|
||||
onPress={() => {
|
||||
},
|
||||
},
|
||||
selected.some((obj) => obj.read === 1) && {
|
||||
icon: "check-outline",
|
||||
onPress: () => {
|
||||
dispatch(
|
||||
markChapterUnreadAction(
|
||||
selected,
|
||||
@@ -340,14 +370,11 @@ const Novel = ({ route, navigation }) => {
|
||||
)
|
||||
);
|
||||
setSelected([]);
|
||||
}}
|
||||
/>
|
||||
)}
|
||||
{selected.length === 1 && (
|
||||
<Appbar.Action
|
||||
icon="playlist-check"
|
||||
color={theme.textColorPrimary}
|
||||
onPress={() => {
|
||||
},
|
||||
},
|
||||
selected.length === 1 && {
|
||||
icon: "playlist-check",
|
||||
onPress: () => {
|
||||
dispatch(
|
||||
markPreviousChaptersReadAction(
|
||||
selected[0].chapterId,
|
||||
@@ -355,13 +382,21 @@ const Novel = ({ route, navigation }) => {
|
||||
)
|
||||
);
|
||||
setSelected([]);
|
||||
}}
|
||||
/>
|
||||
)}
|
||||
</View>
|
||||
},
|
||||
},
|
||||
]}
|
||||
/>
|
||||
)}
|
||||
{!loading && (
|
||||
<Portal>
|
||||
<JumpToChapterModal
|
||||
modalVisible={jumpToChapterModal}
|
||||
hideModal={() => showJumpToChapterModal(false)}
|
||||
theme={theme}
|
||||
chapters={chapters}
|
||||
novel={novel}
|
||||
navigation={navigation}
|
||||
/>
|
||||
<ChaptersSettingsSheet
|
||||
novelUrl={novelUrl}
|
||||
bottomSheetRef={chaptersSettingsSheetRef}
|
||||
@@ -379,17 +414,6 @@ const Novel = ({ route, navigation }) => {
|
||||
novelName={novel.novelName}
|
||||
theme={theme}
|
||||
/>
|
||||
{/* <ChapterActionsSheet
|
||||
theme={theme}
|
||||
selected={selected}
|
||||
chapters={chapters}
|
||||
dispatch={dispatch}
|
||||
setSelected={setSelected}
|
||||
bottomSheetRef={chapterActionsSheetRef}
|
||||
markChapterReadAction={markChapterReadAction}
|
||||
markChapterUnreadAction={markChapterUnreadAction}
|
||||
bookmarkChapterAction={bookmarkChapterAction}
|
||||
/> */}
|
||||
</Portal>
|
||||
)}
|
||||
</View>
|
||||
|
||||
@@ -0,0 +1,93 @@
|
||||
import React from "react";
|
||||
import { ActivityIndicator } from "react-native";
|
||||
|
||||
import { IconButton, Menu } from "react-native-paper";
|
||||
|
||||
export const DownloadButton = ({
|
||||
downloadQueue,
|
||||
chapter,
|
||||
theme,
|
||||
deleteChapter,
|
||||
downloadChapter,
|
||||
hideDeleteChapterMenu,
|
||||
showDeleteChapterMenu,
|
||||
deleteChapterMenu,
|
||||
}) => {
|
||||
if (downloadQueue.some((chap) => chap.chapterId === chapter.chapterId)) {
|
||||
return <ChapterDownloadingButton theme={theme} />;
|
||||
} else if (chapter.downloaded === 1) {
|
||||
return (
|
||||
<Menu
|
||||
visible={deleteChapterMenu}
|
||||
onDismiss={hideDeleteChapterMenu}
|
||||
anchor={
|
||||
<DeleteChapterButton
|
||||
theme={theme}
|
||||
onPress={showDeleteChapterMenu}
|
||||
/>
|
||||
}
|
||||
contentStyle={{ backgroundColor: theme.menuColor }}
|
||||
>
|
||||
<Menu.Item
|
||||
onPress={() =>
|
||||
deleteChapter(chapter.chapterId, chapter.chapterName)
|
||||
}
|
||||
title="Delete"
|
||||
titleStyle={{ color: theme.textColorPrimary }}
|
||||
/>
|
||||
</Menu>
|
||||
);
|
||||
} else {
|
||||
return (
|
||||
<DownloadChapterButton
|
||||
theme={theme}
|
||||
onPress={() =>
|
||||
downloadChapter(
|
||||
chapter.chapterUrl,
|
||||
chapter.chapterName,
|
||||
chapter.chapterId
|
||||
)
|
||||
}
|
||||
/>
|
||||
);
|
||||
}
|
||||
};
|
||||
|
||||
export const ChapterDownloadingButton = ({ theme }) => (
|
||||
<ActivityIndicator
|
||||
color={theme.textColorHint}
|
||||
size={25}
|
||||
style={{ margin: 3.5, padding: 5 }}
|
||||
/>
|
||||
);
|
||||
|
||||
export const DownloadChapterButton = ({ theme, onPress }) => (
|
||||
<IconButton
|
||||
icon="arrow-down-circle-outline"
|
||||
animated
|
||||
color={theme.textColorHint}
|
||||
size={25}
|
||||
onPress={onPress}
|
||||
style={{ margin: 2 }}
|
||||
/>
|
||||
);
|
||||
|
||||
export const DeleteChapterButton = ({ theme, onPress }) => (
|
||||
<IconButton
|
||||
icon="check-circle"
|
||||
animated
|
||||
color={theme.textColorPrimary}
|
||||
size={25}
|
||||
onPress={onPress}
|
||||
style={{ margin: 2 }}
|
||||
/>
|
||||
);
|
||||
|
||||
export const ChapterBookmarkButton = ({ theme }) => (
|
||||
<IconButton
|
||||
icon="bookmark"
|
||||
color={theme.colorAccent}
|
||||
size={18}
|
||||
style={{ marginLeft: 2 }}
|
||||
/>
|
||||
);
|
||||
@@ -1,176 +0,0 @@
|
||||
import React, { useState } from "react";
|
||||
import { StyleSheet, View, Text, Animated } from "react-native";
|
||||
import { List } from "react-native-paper";
|
||||
|
||||
import Bottomsheet from "rn-sliding-up-panel";
|
||||
import BottomSheetHandle from "../../../components/BottomSheetHandle";
|
||||
import { ListItem } from "../../../components/List";
|
||||
import {
|
||||
markPreviousChaptersReadAction,
|
||||
markPreviousChaptersUnreadAction,
|
||||
} from "../../../redux/novel/novel.actions";
|
||||
|
||||
const ChapterActionsSheet = ({
|
||||
bottomSheetRef,
|
||||
selected,
|
||||
theme,
|
||||
markChapterReadAction,
|
||||
markChapterUnreadAction,
|
||||
dispatch,
|
||||
bookmarkChapterAction,
|
||||
setSelected,
|
||||
}) => {
|
||||
const [animatedValue] = useState(new Animated.Value(0));
|
||||
|
||||
return (
|
||||
<Bottomsheet
|
||||
animatedValue={animatedValue}
|
||||
ref={bottomSheetRef}
|
||||
draggableRange={{ top: 270, bottom: 0 }}
|
||||
snappingPoints={[0, 270]}
|
||||
>
|
||||
<View
|
||||
style={[
|
||||
styles.contentContainer,
|
||||
{ backgroundColor: theme.colorPrimary },
|
||||
]}
|
||||
>
|
||||
<Text
|
||||
style={{
|
||||
color: theme.textColorPrimary,
|
||||
textAlign: "center",
|
||||
paddingTop: 16,
|
||||
paddingBottom: 8,
|
||||
fontSize: 16,
|
||||
}}
|
||||
numberOfLines={1}
|
||||
>
|
||||
{selected?.chapterName}
|
||||
</Text>
|
||||
<List.Item
|
||||
title="Mark previous chapters as read"
|
||||
titleStyle={{ color: theme.textColorPrimary, fontSize: 15 }}
|
||||
left={() => (
|
||||
<List.Icon
|
||||
color={theme.textColorPrimary}
|
||||
icon="eye-check"
|
||||
style={{ marginVertical: 0 }}
|
||||
/>
|
||||
)}
|
||||
onPress={() => {
|
||||
dispatch(
|
||||
markPreviousChaptersReadAction(
|
||||
selected?.chapterId,
|
||||
selected?.novelId
|
||||
)
|
||||
);
|
||||
// bottomSheetRef.current.hide();
|
||||
}}
|
||||
rippleColor={theme.rippleColor}
|
||||
/>
|
||||
<List.Item
|
||||
title="Mark previous chapters as unread"
|
||||
titleStyle={{ color: theme.textColorPrimary, fontSize: 15 }}
|
||||
left={() => (
|
||||
<List.Icon
|
||||
color={theme.textColorPrimary}
|
||||
icon="eye-off"
|
||||
style={{ marginVertical: 0 }}
|
||||
/>
|
||||
)}
|
||||
onPress={() => {
|
||||
dispatch(
|
||||
markPreviousChaptersUnreadAction(
|
||||
selected?.chapterId,
|
||||
selected?.novelId
|
||||
)
|
||||
);
|
||||
// bottomSheetRef.current.hide();
|
||||
}}
|
||||
rippleColor={theme.rippleColor}
|
||||
/>
|
||||
<List.Item
|
||||
title={
|
||||
selected?.read
|
||||
? "Mark chapter as unread"
|
||||
: "Mark chapter as read"
|
||||
}
|
||||
titleStyle={{ color: theme.textColorPrimary, fontSize: 15 }}
|
||||
left={() => (
|
||||
<List.Icon
|
||||
color={theme.textColorPrimary}
|
||||
icon={selected?.read ? "eye-off" : "eye"}
|
||||
style={{ marginVertical: 0 }}
|
||||
/>
|
||||
)}
|
||||
onPress={() => {
|
||||
if (selected?.read) {
|
||||
dispatch(
|
||||
markChapterUnreadAction(selected?.chapterId)
|
||||
);
|
||||
setSelected((selected) => ({
|
||||
...selected,
|
||||
read: 0,
|
||||
}));
|
||||
} else {
|
||||
dispatch(
|
||||
markChapterReadAction(
|
||||
selected?.chapterId,
|
||||
selected?.novelId
|
||||
)
|
||||
);
|
||||
setSelected((selected) => ({
|
||||
...selected,
|
||||
read: 1,
|
||||
}));
|
||||
}
|
||||
// bottomSheetRef.current.hide();
|
||||
}}
|
||||
rippleColor={theme.rippleColor}
|
||||
/>
|
||||
<List.Item
|
||||
title={
|
||||
selected?.bookmark
|
||||
? "Remove bookmark"
|
||||
: "Bookmark chapter"
|
||||
}
|
||||
titleStyle={{ color: theme.textColorPrimary, fontSize: 15 }}
|
||||
left={() => (
|
||||
<List.Icon
|
||||
color={theme.textColorPrimary}
|
||||
icon={
|
||||
selected?.bookmark ? "bookmark-off" : "bookmark"
|
||||
}
|
||||
style={{ marginVertical: 0 }}
|
||||
/>
|
||||
)}
|
||||
onPress={() => {
|
||||
dispatch(
|
||||
bookmarkChapterAction(
|
||||
selected?.bookmark,
|
||||
selected?.chapterId
|
||||
)
|
||||
);
|
||||
setSelected((selected) => ({
|
||||
...selected,
|
||||
bookmark: !selected.bookmark,
|
||||
}));
|
||||
|
||||
// bottomSheetRef.current.hide();
|
||||
}}
|
||||
rippleColor={theme.rippleColor}
|
||||
/>
|
||||
</View>
|
||||
</Bottomsheet>
|
||||
);
|
||||
};
|
||||
|
||||
export default ChapterActionsSheet;
|
||||
|
||||
const styles = StyleSheet.create({
|
||||
contentContainer: {
|
||||
flex: 1,
|
||||
borderTopRightRadius: 8,
|
||||
borderTopLeftRadius: 8,
|
||||
},
|
||||
});
|
||||
@@ -1,38 +1,40 @@
|
||||
import React, { useState } from "react";
|
||||
import {
|
||||
View,
|
||||
Text,
|
||||
ActivityIndicator,
|
||||
StyleSheet,
|
||||
Pressable,
|
||||
} from "react-native";
|
||||
import React, { memo, useState } from "react";
|
||||
import { View, Text, StyleSheet, Pressable } from "react-native";
|
||||
|
||||
import { TouchableRipple, IconButton, Menu } from "react-native-paper";
|
||||
import { Row } from "../../../components/Common";
|
||||
|
||||
import {
|
||||
deleteChapterAction,
|
||||
downloadChapterAction,
|
||||
} from "../../../redux/novel/novel.actions";
|
||||
import { parseChapterNumber } from "../../../services/utils/helpers";
|
||||
import {
|
||||
ChapterBookmarkButton,
|
||||
DownloadButton,
|
||||
} from "./Chapter/ChapterDownloadButtons";
|
||||
|
||||
const ChapterItem = ({
|
||||
novelUrl,
|
||||
novelName,
|
||||
novelData,
|
||||
chapter,
|
||||
sourceId,
|
||||
dispatch,
|
||||
theme,
|
||||
navigation,
|
||||
downloading,
|
||||
index,
|
||||
position,
|
||||
selected,
|
||||
setSelected,
|
||||
showChapterTitles,
|
||||
chapterActionsSheetRef,
|
||||
downloadQueue,
|
||||
downloadChapter,
|
||||
deleteChapter,
|
||||
isSelected,
|
||||
onSelectPress,
|
||||
onSelectLongPress,
|
||||
}) => {
|
||||
const { sourceId, novelName, novelUrl } = novelData;
|
||||
const {
|
||||
novelId,
|
||||
chapterId,
|
||||
chapterUrl,
|
||||
chapterName,
|
||||
read,
|
||||
releaseDate,
|
||||
bookmark,
|
||||
} = chapter;
|
||||
|
||||
const [deleteChapterMenu, setDeleteChapterMenu] = useState(false);
|
||||
const showDeleteChapterMenu = () => setDeleteChapterMenu(true);
|
||||
const hideDeleteChapterMenu = () => setDeleteChapterMenu(false);
|
||||
@@ -41,120 +43,31 @@ const ChapterItem = ({
|
||||
navigation.navigate("Chapter", {
|
||||
sourceId,
|
||||
novelUrl,
|
||||
novelId: chapter.novelId,
|
||||
chapterId: chapter.chapterId,
|
||||
chapterUrl: chapter.chapterUrl,
|
||||
chapterName: chapter.chapterName,
|
||||
novelName: novelName,
|
||||
bookmark: chapter.bookmark,
|
||||
novelId,
|
||||
chapterId,
|
||||
chapterUrl,
|
||||
chapterName,
|
||||
novelName,
|
||||
bookmark,
|
||||
});
|
||||
|
||||
const displayDownloadButton = () => {
|
||||
if (
|
||||
downloadQueue.some((chap) => chap.chapterId === chapter.chapterId)
|
||||
) {
|
||||
const renderProgressPercentage = () => {
|
||||
const savedProgress =
|
||||
position && position[chapterId] && position[chapterId].percentage;
|
||||
if (savedProgress < 100 && savedProgress > 0 && !read) {
|
||||
return (
|
||||
<ActivityIndicator
|
||||
color={theme.textColorHint}
|
||||
size={25}
|
||||
style={{ margin: 3.5, padding: 5 }}
|
||||
/>
|
||||
);
|
||||
} else if (chapter.downloaded === 1) {
|
||||
return (
|
||||
<Menu
|
||||
visible={deleteChapterMenu}
|
||||
onDismiss={hideDeleteChapterMenu}
|
||||
anchor={
|
||||
<IconButton
|
||||
icon="check-circle"
|
||||
animated
|
||||
color={theme.textColorPrimary}
|
||||
size={25}
|
||||
onPress={showDeleteChapterMenu}
|
||||
style={{ margin: 2 }}
|
||||
/>
|
||||
}
|
||||
contentStyle={{ backgroundColor: theme.menuColor }}
|
||||
<Text
|
||||
style={{
|
||||
color: theme.textColorHint,
|
||||
fontSize: 12,
|
||||
marginLeft: releaseDate ? 5 : 0,
|
||||
}}
|
||||
numberOfLines={1}
|
||||
>
|
||||
<Menu.Item
|
||||
onPress={() =>
|
||||
dispatch(
|
||||
deleteChapterAction(
|
||||
chapter.chapterId,
|
||||
chapter.chapterName
|
||||
)
|
||||
)
|
||||
}
|
||||
title="Delete"
|
||||
titleStyle={{ color: theme.textColorPrimary }}
|
||||
/>
|
||||
</Menu>
|
||||
{releaseDate && "• "}
|
||||
{"Progress " + position[chapterId].percentage + "%"}
|
||||
</Text>
|
||||
);
|
||||
} else {
|
||||
return (
|
||||
<IconButton
|
||||
icon="arrow-down-circle-outline"
|
||||
animated
|
||||
color={theme.textColorHint}
|
||||
size={25}
|
||||
onPress={() =>
|
||||
dispatch(
|
||||
downloadChapterAction(
|
||||
sourceId,
|
||||
novelUrl,
|
||||
chapter.chapterUrl,
|
||||
chapter.chapterName,
|
||||
chapter.chapterId
|
||||
)
|
||||
)
|
||||
}
|
||||
style={{ margin: 2 }}
|
||||
/>
|
||||
);
|
||||
}
|
||||
};
|
||||
|
||||
const bookmarkButton = () => {
|
||||
if (chapter.bookmark) {
|
||||
return (
|
||||
<IconButton
|
||||
icon="bookmark"
|
||||
color={theme.colorAccent}
|
||||
size={18}
|
||||
style={{ marginLeft: 2 }}
|
||||
/>
|
||||
);
|
||||
}
|
||||
};
|
||||
|
||||
const isSelected = (chapterId) => {
|
||||
return selected.some((obj) => obj.chapterId === chapterId);
|
||||
};
|
||||
|
||||
const onPress = () => {
|
||||
if (selected.length === 0) {
|
||||
navigateToChapter();
|
||||
} else {
|
||||
if (isSelected(chapter.chapterId)) {
|
||||
setSelected((selected) =>
|
||||
selected.filter(
|
||||
(item) => item.chapterId !== chapter.chapterId
|
||||
)
|
||||
);
|
||||
} else {
|
||||
setSelected((selected) => [...selected, chapter]);
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
const onLongPress = () => {
|
||||
if (isSelected(chapter.chapterId)) {
|
||||
setSelected((selected) =>
|
||||
selected.filter((item) => item.chapterId !== chapter.chapterId)
|
||||
);
|
||||
} else {
|
||||
setSelected((selected) => [...selected, chapter]);
|
||||
}
|
||||
};
|
||||
|
||||
@@ -162,32 +75,30 @@ const ChapterItem = ({
|
||||
<Pressable
|
||||
style={[
|
||||
styles.chapterCardContainer,
|
||||
isSelected(chapter.chapterId) && {
|
||||
isSelected(chapterId) && {
|
||||
backgroundColor: theme.rippleColor,
|
||||
},
|
||||
]}
|
||||
onPress={onPress}
|
||||
onLongPress={onLongPress}
|
||||
onPress={() => onSelectPress(chapter, navigateToChapter)}
|
||||
onLongPress={() => onSelectLongPress(chapter)}
|
||||
android_ripple={{ color: theme.rippleColor }}
|
||||
>
|
||||
<Row>
|
||||
{bookmarkButton()}
|
||||
{bookmark === true && <ChapterBookmarkButton theme={theme} />}
|
||||
<View>
|
||||
<Text
|
||||
style={[
|
||||
{ color: theme.textColorPrimary },
|
||||
chapter.read && {
|
||||
color: theme.textColorHint,
|
||||
},
|
||||
]}
|
||||
style={{
|
||||
color: read
|
||||
? theme.textColorHint
|
||||
: theme.textColorPrimary,
|
||||
}}
|
||||
numberOfLines={1}
|
||||
>
|
||||
{showChapterTitles
|
||||
? parseChapterNumber(chapter.chapterName)
|
||||
? "Chapter " +
|
||||
parseChapterNumber(chapter.chapterName)
|
||||
? parseChapterNumber(chapterName)
|
||||
? "Chapter " + parseChapterNumber(chapterName)
|
||||
: "Chapter " + index
|
||||
: chapter.chapterName.substring(0, 50)}
|
||||
: chapterName.substring(0, 50)}
|
||||
</Text>
|
||||
<View
|
||||
style={{
|
||||
@@ -195,55 +106,38 @@ const ChapterItem = ({
|
||||
marginTop: 5,
|
||||
}}
|
||||
>
|
||||
{chapter.releaseDate && (
|
||||
{releaseDate && (
|
||||
<Text
|
||||
style={[
|
||||
{
|
||||
color: theme.textColorSecondary,
|
||||
fontSize: 12,
|
||||
},
|
||||
chapter.read && {
|
||||
color: theme.textColorHint,
|
||||
},
|
||||
]}
|
||||
style={{
|
||||
color: read
|
||||
? theme.textColorHint
|
||||
: theme.textColorSecondary,
|
||||
fontSize: 12,
|
||||
}}
|
||||
numberOfLines={1}
|
||||
>
|
||||
{chapter.releaseDate}
|
||||
{releaseDate}
|
||||
</Text>
|
||||
)}
|
||||
{position &&
|
||||
!chapter.read &&
|
||||
position[chapter.chapterId] &&
|
||||
position[chapter.chapterId].percentage < 100 &&
|
||||
position[chapter.chapterId].percentage > 0 && (
|
||||
<Text
|
||||
style={[
|
||||
{
|
||||
color: theme.textColorHint,
|
||||
fontSize: 12,
|
||||
marginLeft: 0,
|
||||
},
|
||||
chapter.releaseDate && {
|
||||
marginLeft: 5,
|
||||
},
|
||||
]}
|
||||
numberOfLines={1}
|
||||
>
|
||||
{chapter.releaseDate && "• "}
|
||||
{"Progress " +
|
||||
position[chapter.chapterId].percentage +
|
||||
"%"}
|
||||
</Text>
|
||||
)}
|
||||
{renderProgressPercentage()}
|
||||
</View>
|
||||
</View>
|
||||
</Row>
|
||||
<View>{displayDownloadButton()}</View>
|
||||
<DownloadButton
|
||||
downloadQueue={downloadQueue}
|
||||
chapter={chapter}
|
||||
theme={theme}
|
||||
deleteChapter={deleteChapter}
|
||||
downloadChapter={downloadChapter}
|
||||
hideDeleteChapterMenu={hideDeleteChapterMenu}
|
||||
showDeleteChapterMenu={showDeleteChapterMenu}
|
||||
deleteChapterMenu={deleteChapterMenu}
|
||||
/>
|
||||
</Pressable>
|
||||
);
|
||||
};
|
||||
|
||||
export default ChapterItem;
|
||||
export default memo(ChapterItem);
|
||||
|
||||
const styles = StyleSheet.create({
|
||||
chapterCardContainer: {
|
||||
|
||||
@@ -9,7 +9,6 @@ import {
|
||||
import { TabView, SceneMap, TabBar } from "react-native-tab-view";
|
||||
|
||||
import Bottomsheet from "rn-sliding-up-panel";
|
||||
import BottomSheetHandle from "../../../components/BottomSheetHandle";
|
||||
import { showChapterTitlesAction } from "../../../redux/novel/novel.actions";
|
||||
import { ListItem } from "../../../components/List";
|
||||
import { RadioButton, RadioButtonGroup } from "../../../components/RadioButton";
|
||||
|
||||
@@ -66,7 +66,10 @@ const NovelAuthor = ({ theme, children }) => (
|
||||
|
||||
const NovelInfo = ({ theme, children }) => (
|
||||
<Text
|
||||
style={[styles.novelInfo, { color: theme.textColorSecondary }]}
|
||||
style={[
|
||||
styles.novelInfo,
|
||||
{ color: theme.textColorSecondary, textTransform: "capitalize" },
|
||||
]}
|
||||
numberOfLines={1}
|
||||
>
|
||||
{children}
|
||||
@@ -89,16 +92,14 @@ const FollowButton = ({ theme, onPress, followed }) => (
|
||||
>
|
||||
<IconButton
|
||||
icon={followed ? "heart" : "heart-outline"}
|
||||
color={followed ? theme.colorAccent : theme.textColorSecondary}
|
||||
color={followed ? theme.colorAccent : theme.textColorHint}
|
||||
size={24}
|
||||
style={{ margin: 0 }}
|
||||
/>
|
||||
<Text
|
||||
style={{
|
||||
fontSize: 12,
|
||||
color: followed
|
||||
? theme.colorAccent
|
||||
: theme.textColorSecondary,
|
||||
color: followed ? theme.colorAccent : theme.textColorHint,
|
||||
}}
|
||||
>
|
||||
{followed ? "In Library" : "Add to library"}
|
||||
@@ -123,16 +124,14 @@ const TrackerButton = ({ theme, isTracked, onPress }) => (
|
||||
>
|
||||
<IconButton
|
||||
icon={isTracked ? "check" : "sync"}
|
||||
color={isTracked ? theme.colorAccent : theme.textColorSecondary}
|
||||
color={isTracked ? theme.colorAccent : theme.textColorHint}
|
||||
size={24}
|
||||
style={{ margin: 0 }}
|
||||
/>
|
||||
<Text
|
||||
style={{
|
||||
fontSize: 12,
|
||||
color: isTracked
|
||||
? theme.colorAccent
|
||||
: theme.textColorSecondary,
|
||||
color: isTracked ? theme.colorAccent : theme.textColorHint,
|
||||
}}
|
||||
>
|
||||
{isTracked ? "Tracked" : "Tracking"}
|
||||
@@ -164,16 +163,14 @@ const NovelGenres = ({ theme, genre }) => {
|
||||
);
|
||||
|
||||
return (
|
||||
genre !== "" && (
|
||||
<FlatList
|
||||
contentContainerStyle={styles.novelGenres}
|
||||
horizontal
|
||||
data={data}
|
||||
keyExtractor={(item) => item}
|
||||
renderItem={renderItem}
|
||||
showsHorizontalScrollIndicator={false}
|
||||
/>
|
||||
)
|
||||
<FlatList
|
||||
contentContainerStyle={styles.novelGenres}
|
||||
horizontal
|
||||
data={data}
|
||||
keyExtractor={(item) => item}
|
||||
renderItem={renderItem}
|
||||
showsHorizontalScrollIndicator={false}
|
||||
/>
|
||||
);
|
||||
};
|
||||
|
||||
|
||||
@@ -38,7 +38,8 @@ export default NovelSummary;
|
||||
const styles = StyleSheet.create({
|
||||
summaryContainer: {
|
||||
paddingHorizontal: 16,
|
||||
marginVertical: 16,
|
||||
marginTop: 8,
|
||||
marginBottom: 16,
|
||||
},
|
||||
// summaryHeader: {
|
||||
// marginTop: 5,
|
||||
|
||||
@@ -24,7 +24,7 @@ const ReadButton = ({ navigation, novel, chapters, theme, lastRead }) => {
|
||||
sourceId: novel.sourceId,
|
||||
chapterName: lastRead.chapterName,
|
||||
novelName: novel.novelName,
|
||||
novelName: lastRead.bookmark,
|
||||
bookmark: lastRead.bookmark,
|
||||
});
|
||||
}}
|
||||
>
|
||||
|
||||
@@ -0,0 +1,149 @@
|
||||
import React, { useState } from "react";
|
||||
import { StyleSheet, Text, View } from "react-native";
|
||||
|
||||
import { Modal, Portal, Switch, TextInput } from "react-native-paper";
|
||||
|
||||
const JumpToChapterModal = ({
|
||||
theme,
|
||||
hideModal,
|
||||
modalVisible,
|
||||
chapters,
|
||||
navigation,
|
||||
novel,
|
||||
}) => {
|
||||
const [mode, setMode] = useState(0);
|
||||
|
||||
const [text, setText] = useState();
|
||||
const [error, setError] = useState();
|
||||
|
||||
const onDismiss = () => {
|
||||
hideModal();
|
||||
setText();
|
||||
setError();
|
||||
};
|
||||
|
||||
const onSubmit = () => {
|
||||
if (!mode) {
|
||||
if (text > 0 && text <= chapters.length) {
|
||||
navigation.navigate("Chapter", {
|
||||
sourceId: novel.sourceId,
|
||||
novelUrl: novel.novelUrl,
|
||||
novelId: novel.novelId,
|
||||
chapterId: chapters[text - 1].chapterId,
|
||||
chapterUrl: chapters[text - 1].chapterUrl,
|
||||
chapterName: chapters[text - 1].chapterName,
|
||||
novelName: novel.novelName,
|
||||
bookmark: chapters[text - 1].bookmark,
|
||||
});
|
||||
hideModal();
|
||||
} else {
|
||||
setError(
|
||||
`Enter a valid chapter number (<= ${chapters.length})`
|
||||
);
|
||||
}
|
||||
} else {
|
||||
const chapter = chapters.find((chap) =>
|
||||
chap.chapterName.toLowerCase().includes(text.toLowerCase())
|
||||
);
|
||||
|
||||
if (chapter) {
|
||||
navigation.navigate("Chapter", {
|
||||
sourceId: novel.sourceId,
|
||||
novelUrl: novel.novelUrl,
|
||||
novelId: chapter.novelId,
|
||||
chapterId: chapter.chapterId,
|
||||
chapterUrl: chapter.chapterUrl,
|
||||
chapterName: chapter.chapterName,
|
||||
novelName: novel.novelName,
|
||||
bookmark: chapter.bookmark,
|
||||
});
|
||||
hideModal();
|
||||
} else {
|
||||
setError(`Enter a valid chapter name`);
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
const onChangeText = (text) => setText(text);
|
||||
|
||||
const textInputTheme = {
|
||||
colors: {
|
||||
primary: theme.colorAccent,
|
||||
placeholder: theme.textColorHint,
|
||||
text: theme.textColorPrimary,
|
||||
background: theme.colorPrimary,
|
||||
},
|
||||
};
|
||||
|
||||
return (
|
||||
<Portal>
|
||||
<Modal
|
||||
visible={modalVisible}
|
||||
onDismiss={onDismiss}
|
||||
contentContainerStyle={[
|
||||
styles.modalContainer,
|
||||
{ backgroundColor: theme.colorPrimary },
|
||||
]}
|
||||
>
|
||||
<Text
|
||||
style={[
|
||||
styles.modalTitle,
|
||||
{ color: theme.textColorPrimary },
|
||||
]}
|
||||
>
|
||||
Jump to Chapter
|
||||
</Text>
|
||||
<TextInput
|
||||
value={text}
|
||||
placeholder={
|
||||
mode
|
||||
? "Chapter Name"
|
||||
: `Chapter Number (<= ${chapters.length})`
|
||||
}
|
||||
onChangeText={onChangeText}
|
||||
onSubmitEditing={onSubmit}
|
||||
mode="outlined"
|
||||
theme={textInputTheme}
|
||||
underlineColor={theme.textColorHint}
|
||||
dense
|
||||
error={error}
|
||||
/>
|
||||
<Text style={styles.errorText}>{error}</Text>
|
||||
<View
|
||||
style={{
|
||||
flexDirection: "row",
|
||||
justifyContent: "space-between",
|
||||
marginTop: 4,
|
||||
}}
|
||||
>
|
||||
<Text style={{ color: theme.textColorPrimary }}>
|
||||
Chapter Name
|
||||
</Text>
|
||||
<Switch
|
||||
value={mode}
|
||||
onValueChange={() => setMode(!mode)}
|
||||
color={theme.colorAccent}
|
||||
/>
|
||||
</View>
|
||||
</Modal>
|
||||
</Portal>
|
||||
);
|
||||
};
|
||||
|
||||
export default JumpToChapterModal;
|
||||
|
||||
const styles = StyleSheet.create({
|
||||
modalContainer: {
|
||||
margin: 30,
|
||||
padding: 20,
|
||||
borderRadius: 8,
|
||||
},
|
||||
modalTitle: {
|
||||
fontSize: 18,
|
||||
marginBottom: 16,
|
||||
},
|
||||
errorText: {
|
||||
color: "#FF0033",
|
||||
paddingTop: 8,
|
||||
},
|
||||
});
|
||||
@@ -1,8 +1,15 @@
|
||||
import React from "react";
|
||||
import { View, Text, StyleSheet, Clipboard, Pressable } from "react-native";
|
||||
import React, { memo } from "react";
|
||||
import {
|
||||
View,
|
||||
Text,
|
||||
StyleSheet,
|
||||
Clipboard,
|
||||
Pressable,
|
||||
Button,
|
||||
} from "react-native";
|
||||
|
||||
import * as WebBrowser from "expo-web-browser";
|
||||
import { TouchableRipple, IconButton } from "react-native-paper";
|
||||
import { IconButton } from "react-native-paper";
|
||||
|
||||
import { followNovelAction } from "../../../redux/novel/novel.actions";
|
||||
import { useTrackingStatus } from "../../../hooks/reduxHooks";
|
||||
@@ -69,10 +76,10 @@ const NovelInfoHeader = ({
|
||||
{!loading && (
|
||||
<>
|
||||
<NovelAuthor theme={theme}>
|
||||
{novel.author}
|
||||
{novel.author ?? "Unknown author"}
|
||||
</NovelAuthor>
|
||||
<NovelInfo theme={theme}>
|
||||
{novel.status}
|
||||
{novel.status ?? "Unknown status"}
|
||||
</NovelInfo>
|
||||
<NovelInfo theme={theme}>
|
||||
{novel.source}
|
||||
@@ -129,14 +136,14 @@ const NovelInfoHeader = ({
|
||||
>
|
||||
<IconButton
|
||||
icon="swap-vertical-variant"
|
||||
color={theme.textColorSecondary}
|
||||
color={theme.textColorHint}
|
||||
size={24}
|
||||
style={{ margin: 0 }}
|
||||
/>
|
||||
<Text
|
||||
style={{
|
||||
fontSize: 12,
|
||||
color: theme.textColorSecondary,
|
||||
color: theme.textColorHint,
|
||||
}}
|
||||
>
|
||||
Migrate
|
||||
@@ -166,14 +173,14 @@ const NovelInfoHeader = ({
|
||||
>
|
||||
<IconButton
|
||||
icon="earth"
|
||||
color={theme.textColorSecondary}
|
||||
color={theme.textColorHint}
|
||||
size={24}
|
||||
style={{ margin: 0 }}
|
||||
/>
|
||||
<Text
|
||||
style={{
|
||||
fontSize: 12,
|
||||
color: theme.textColorSecondary,
|
||||
color: theme.textColorHint,
|
||||
}}
|
||||
>
|
||||
WebView
|
||||
@@ -186,7 +193,9 @@ const NovelInfoHeader = ({
|
||||
followed={novel.followed}
|
||||
theme={theme}
|
||||
/>
|
||||
<NovelGenres theme={theme} genre={novel.genre} />
|
||||
{novel.genre !== null && novel.genre !== "" && (
|
||||
<NovelGenres theme={theme} genre={novel.genre} />
|
||||
)}
|
||||
<ReadButton
|
||||
novel={novel}
|
||||
chapters={chapters}
|
||||
@@ -194,10 +203,10 @@ const NovelInfoHeader = ({
|
||||
theme={theme}
|
||||
lastRead={lastRead}
|
||||
/>
|
||||
<TouchableRipple
|
||||
<Pressable
|
||||
style={styles.bottomsheet}
|
||||
onPress={() => chaptersSettingsSheetRef.current.show()}
|
||||
rippleColor={theme.rippleColor}
|
||||
android_ripple={{ color: theme.rippleColor }}
|
||||
>
|
||||
<>
|
||||
<Text
|
||||
@@ -218,14 +227,14 @@ const NovelInfoHeader = ({
|
||||
size={24}
|
||||
/>
|
||||
</>
|
||||
</TouchableRipple>
|
||||
</Pressable>
|
||||
</>
|
||||
)}
|
||||
</View>
|
||||
);
|
||||
};
|
||||
|
||||
export default NovelInfoHeader;
|
||||
export default memo(NovelInfoHeader);
|
||||
|
||||
const styles = StyleSheet.create({
|
||||
novelDetails: {
|
||||
|
||||
@@ -1,36 +1,40 @@
|
||||
import React from "react";
|
||||
import { StyleSheet, View, Text, Image } from "react-native";
|
||||
import { StyleSheet, View, Text, Image, Pressable } from "react-native";
|
||||
import { TouchableRipple, IconButton } from "react-native-paper";
|
||||
import { useDispatch } from "react-redux";
|
||||
import { untrackNovel } from "../../../../redux/tracker/tracker.actions";
|
||||
|
||||
export const AddMalTrackingCard = ({ theme, setTrackSearchDialog }) => (
|
||||
<View
|
||||
style={[
|
||||
styles.addCardContainer,
|
||||
{ backgroundColor: theme.colorPrimary },
|
||||
]}
|
||||
>
|
||||
<View style={styles.addCardContainer}>
|
||||
<Image
|
||||
source={require("../../../../../assets/mal.png")}
|
||||
style={styles.trackerIcon}
|
||||
/>
|
||||
<TouchableRipple
|
||||
style={styles.rippleContainer}
|
||||
borderless
|
||||
onPress={() => setTrackSearchDialog(true)}
|
||||
rippleColor={theme.colorAccent}
|
||||
|
||||
<View
|
||||
style={{
|
||||
flex: 1,
|
||||
flexDirection: "row",
|
||||
borderRadius: 4,
|
||||
overflow: "hidden",
|
||||
marginHorizontal: 16,
|
||||
}}
|
||||
>
|
||||
<Text
|
||||
style={{
|
||||
flex: 1,
|
||||
textAlignVertical: "center",
|
||||
color: theme.colorAccent,
|
||||
}}
|
||||
<Pressable
|
||||
style={styles.rippleContainer}
|
||||
android_ripple={{ color: theme.rippleColor, borderless: true }}
|
||||
onPress={() => setTrackSearchDialog(true)}
|
||||
>
|
||||
Add Tracking
|
||||
</Text>
|
||||
</TouchableRipple>
|
||||
<Text
|
||||
style={{
|
||||
textAlignVertical: "center",
|
||||
color: theme.colorAccent,
|
||||
}}
|
||||
>
|
||||
Add Tracking
|
||||
</Text>
|
||||
</Pressable>
|
||||
</View>
|
||||
</View>
|
||||
);
|
||||
|
||||
@@ -59,7 +63,7 @@ export const MalTrackItemCard = ({
|
||||
>
|
||||
<Image
|
||||
source={require("../../../../../assets/mal.png")}
|
||||
style={[styles.trackerIcon, { borderBottomLeftRadius: 0 }]}
|
||||
style={styles.trackerIcon}
|
||||
/>
|
||||
<View style={styles.listItemContainer}>
|
||||
<Text
|
||||
@@ -145,21 +149,17 @@ const styles = StyleSheet.create({
|
||||
trackerIcon: {
|
||||
height: 50,
|
||||
width: 50,
|
||||
borderTopLeftRadius: 4,
|
||||
borderBottomLeftRadius: 4,
|
||||
borderRadius: 8,
|
||||
},
|
||||
rippleContainer: {
|
||||
flex: 1,
|
||||
paddingVertical: 8,
|
||||
alignItems: "center",
|
||||
borderTopRightRadius: 4,
|
||||
borderBottomRightRadius: 4,
|
||||
},
|
||||
addCardContainer: {
|
||||
flexDirection: "row",
|
||||
alignItems: "center",
|
||||
margin: 8,
|
||||
borderRadius: 4,
|
||||
elevation: 2,
|
||||
margin: 16,
|
||||
},
|
||||
cardContainer: {
|
||||
margin: 8,
|
||||
|
||||
@@ -5,6 +5,7 @@ import { searchNovels } from "../../../../services/Trackers/myAnimeList";
|
||||
import { useSelector, useDispatch } from "react-redux";
|
||||
import { ScrollView } from "react-native-gesture-handler";
|
||||
import { trackNovel } from "../../../../redux/tracker/tracker.actions";
|
||||
import MaterialCommunityIcons from "react-native-vector-icons/MaterialCommunityIcons";
|
||||
|
||||
const TrackSearchDialog = ({
|
||||
trackSearchDialog,
|
||||
@@ -48,6 +49,19 @@ const TrackSearchDialog = ({
|
||||
borderless
|
||||
>
|
||||
<>
|
||||
{selectedNovel && selectedNovel.id === item.node.id && (
|
||||
<MaterialCommunityIcons
|
||||
name="check-circle"
|
||||
color={theme.colorAccent}
|
||||
size={24}
|
||||
style={{
|
||||
position: "absolute",
|
||||
top: 8,
|
||||
right: 8,
|
||||
zIndex: 1,
|
||||
}}
|
||||
/>
|
||||
)}
|
||||
<Image
|
||||
source={{ uri: item.node.main_picture.large }}
|
||||
style={{ height: 150, width: 100, borderRadius: 4 }}
|
||||
|
||||
@@ -1,11 +1,10 @@
|
||||
import React, { useState, useEffect, useCallback } from "react";
|
||||
import React, { useState, useCallback } from "react";
|
||||
import {
|
||||
StyleSheet,
|
||||
View,
|
||||
FlatList,
|
||||
ActivityIndicator,
|
||||
RefreshControl,
|
||||
Button,
|
||||
Text,
|
||||
} from "react-native";
|
||||
import { useFocusEffect } from "@react-navigation/native";
|
||||
@@ -15,18 +14,22 @@ import {
|
||||
getUpdatesAction,
|
||||
updateLibraryAction,
|
||||
} from "../../redux/updates/updates.actions";
|
||||
import { Appbar } from "./components/Appbar";
|
||||
import UpdatesItem from "./components/UpdatesItem";
|
||||
import { useTheme } from "../../hooks/reduxHooks";
|
||||
import { Searchbar } from "../../components/Searchbar";
|
||||
import { Searchbar } from "../../components/Searchbar/Searchbar";
|
||||
|
||||
import moment from "moment";
|
||||
import { dateFormat } from "../../services/utils/constants";
|
||||
import {
|
||||
deleteChapterAction,
|
||||
downloadChapterAction,
|
||||
} from "../../redux/novel/novel.actions";
|
||||
|
||||
const Updates = ({ navigation }) => {
|
||||
const theme = useTheme();
|
||||
const dispatch = useDispatch();
|
||||
const { updates, loading } = useSelector((state) => state.updatesReducer);
|
||||
const { downloadQueue } = useSelector((state) => state.downloadsReducer);
|
||||
|
||||
const [searchText, setSearchText] = useState("");
|
||||
const [searchResults, setSearchResults] = useState([]);
|
||||
@@ -44,20 +47,64 @@ const Updates = ({ navigation }) => {
|
||||
setRefreshing(false);
|
||||
};
|
||||
|
||||
const onPress = (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 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));
|
||||
dispatch(getUpdatesAction());
|
||||
};
|
||||
|
||||
const renderItem = ({ item }) => (
|
||||
<UpdatesItem item={item} theme={theme} onPress={() => onPress(item)} />
|
||||
<UpdatesItem
|
||||
item={item}
|
||||
theme={theme}
|
||||
onPress={() => onPress(item)}
|
||||
onPressCover={() => onPressCover(item)}
|
||||
downloadQueue={downloadQueue}
|
||||
deleteChapter={deleteChapter}
|
||||
downloadChapter={downloadChapter}
|
||||
/>
|
||||
);
|
||||
|
||||
const ListFooterComponent = () =>
|
||||
@@ -103,6 +150,8 @@ const Updates = ({ navigation }) => {
|
||||
setSearchResults(results);
|
||||
};
|
||||
|
||||
const keyExtractor = useCallback((item) => item.updateId.toString(), []);
|
||||
|
||||
return (
|
||||
<View
|
||||
style={[
|
||||
@@ -143,12 +192,12 @@ const Updates = ({ navigation }) => {
|
||||
</Text>
|
||||
}
|
||||
data={item.chapters}
|
||||
keyExtractor={(item) => item.updateId.toString()}
|
||||
keyExtractor={keyExtractor}
|
||||
renderItem={renderItem}
|
||||
/>
|
||||
)}
|
||||
ListFooterComponent={ListFooterComponent()}
|
||||
ListEmptyComponent={ListEmptyComponent()}
|
||||
ListFooterComponent={<ListFooterComponent />}
|
||||
ListEmptyComponent={<ListEmptyComponent />}
|
||||
refreshControl={refreshControl()}
|
||||
/>
|
||||
</View>
|
||||
|
||||
@@ -1,17 +0,0 @@
|
||||
import React from "react";
|
||||
import { Appbar as MaterialAppbar } from "react-native-paper";
|
||||
|
||||
import { updateLibraryAction } from "../../../redux/updates/updates.actions";
|
||||
|
||||
export const Appbar = ({ theme, dispatch }) => (
|
||||
<MaterialAppbar.Header style={{ backgroundColor: theme.colorPrimary }}>
|
||||
<MaterialAppbar.Content
|
||||
title="Updates"
|
||||
titleStyle={{ color: theme.textColorPrimary }}
|
||||
/>
|
||||
<MaterialAppbar.Action
|
||||
icon="reload"
|
||||
onPress={() => dispatch(updateLibraryAction())}
|
||||
/>
|
||||
</MaterialAppbar.Header>
|
||||
);
|
||||
@@ -0,0 +1,60 @@
|
||||
import React from "react";
|
||||
|
||||
import { Menu } from "react-native-paper";
|
||||
import {
|
||||
ChapterDownloadingButton,
|
||||
DeleteChapterButton,
|
||||
DownloadChapterButton,
|
||||
} from "../../Novel/components/Chapter/ChapterDownloadButtons";
|
||||
|
||||
export const DownloadButton = ({
|
||||
downloadQueue,
|
||||
theme,
|
||||
chapter,
|
||||
downloadChapter,
|
||||
deleteChapter,
|
||||
deleteChapterMenu,
|
||||
showDeleteChapterMenu,
|
||||
hideDeleteChapterMenu,
|
||||
}) => {
|
||||
if (downloadQueue.some((chap) => chap.chapterId === chapter.chapterId)) {
|
||||
return <ChapterDownloadingButton theme={theme} />;
|
||||
} else if (chapter.downloaded === 1) {
|
||||
return (
|
||||
<Menu
|
||||
visible={deleteChapterMenu}
|
||||
onDismiss={hideDeleteChapterMenu}
|
||||
anchor={
|
||||
<DeleteChapterButton
|
||||
theme={theme}
|
||||
onPress={showDeleteChapterMenu}
|
||||
/>
|
||||
}
|
||||
contentStyle={{ backgroundColor: theme.menuColor }}
|
||||
>
|
||||
<Menu.Item
|
||||
onPress={() =>
|
||||
deleteChapter(chapter.chapterId, chapter.chapterName)
|
||||
}
|
||||
title="Delete"
|
||||
titleStyle={{ color: theme.textColorPrimary }}
|
||||
/>
|
||||
</Menu>
|
||||
);
|
||||
} else {
|
||||
return (
|
||||
<DownloadChapterButton
|
||||
theme={theme}
|
||||
onPress={() =>
|
||||
downloadChapter(
|
||||
chapter.sourceId,
|
||||
chapter.novelUrl,
|
||||
chapter.chapterUrl,
|
||||
chapter.chapterName,
|
||||
chapter.chapterId
|
||||
)
|
||||
}
|
||||
/>
|
||||
);
|
||||
}
|
||||
};
|
||||
@@ -1,47 +1,80 @@
|
||||
import React from "react";
|
||||
import { View, Text, Image, StyleSheet } from "react-native";
|
||||
import FastImage from "react-native-fast-image";
|
||||
import { TouchableRipple } from "react-native-paper";
|
||||
import React, { useState } from "react";
|
||||
import { View, Text, StyleSheet, Pressable } from "react-native";
|
||||
|
||||
import FastImage from "react-native-fast-image";
|
||||
import { DownloadButton } from "./DownloadButton";
|
||||
|
||||
const UpdateCard = ({
|
||||
item,
|
||||
theme,
|
||||
onPress,
|
||||
onPressCover,
|
||||
downloadQueue,
|
||||
downloadChapter,
|
||||
deleteChapter,
|
||||
}) => {
|
||||
const [deleteChapterMenu, setDeleteChapterMenu] = useState(false);
|
||||
const showDeleteChapterMenu = () => setDeleteChapterMenu(true);
|
||||
const hideDeleteChapterMenu = () => setDeleteChapterMenu(false);
|
||||
|
||||
const UpdateCard = ({ item, theme, onPress }) => {
|
||||
return (
|
||||
<TouchableRipple
|
||||
<Pressable
|
||||
style={styles.updateCard}
|
||||
onPress={onPress}
|
||||
rippleColor={theme.rippleColor}
|
||||
borderless
|
||||
android_ripple={{ color: theme.rippleColor }}
|
||||
>
|
||||
<>
|
||||
<FastImage
|
||||
source={{ uri: item.novelCover }}
|
||||
style={styles.updateIcon}
|
||||
/>
|
||||
<View style={styles.chapterDetails}>
|
||||
<Text
|
||||
style={[
|
||||
{ color: theme.textColorPrimary, fontSize: 14 },
|
||||
item.read && {
|
||||
color: theme.textColorHint,
|
||||
},
|
||||
]}
|
||||
numberOfLines={1}
|
||||
>
|
||||
{item.novelName}
|
||||
</Text>
|
||||
<Text
|
||||
style={[
|
||||
{ color: theme.textColorPrimary, fontSize: 12 },
|
||||
item.read && {
|
||||
color: theme.textColorHint,
|
||||
},
|
||||
]}
|
||||
numberOfLines={1}
|
||||
>
|
||||
{item.chapterName}
|
||||
</Text>
|
||||
<Pressable onPress={onPressCover}>
|
||||
<FastImage
|
||||
source={{ uri: item.novelCover }}
|
||||
style={styles.updateIcon}
|
||||
/>
|
||||
</Pressable>
|
||||
<View
|
||||
style={{
|
||||
flex: 1,
|
||||
flexDirection: "row",
|
||||
justifyContent: "space-between",
|
||||
alignItems: "center",
|
||||
}}
|
||||
>
|
||||
<View style={styles.chapterDetails}>
|
||||
<Text
|
||||
style={[
|
||||
{ color: theme.textColorPrimary, fontSize: 14 },
|
||||
item.read && {
|
||||
color: theme.textColorHint,
|
||||
},
|
||||
]}
|
||||
numberOfLines={1}
|
||||
>
|
||||
{item.novelName}
|
||||
</Text>
|
||||
<Text
|
||||
style={[
|
||||
{ color: theme.textColorPrimary, fontSize: 12 },
|
||||
item.read && {
|
||||
color: theme.textColorHint,
|
||||
},
|
||||
]}
|
||||
numberOfLines={1}
|
||||
>
|
||||
{item.chapterName}
|
||||
</Text>
|
||||
</View>
|
||||
<DownloadButton
|
||||
chapter={item}
|
||||
deleteChapter={deleteChapter}
|
||||
downloadChapter={downloadChapter}
|
||||
downloadQueue={downloadQueue}
|
||||
showDeleteChapterMenu={showDeleteChapterMenu}
|
||||
deleteChapterMenu={deleteChapterMenu}
|
||||
hideDeleteChapterMenu={hideDeleteChapterMenu}
|
||||
theme={theme}
|
||||
/>
|
||||
</View>
|
||||
</>
|
||||
</TouchableRipple>
|
||||
</Pressable>
|
||||
);
|
||||
};
|
||||
|
||||
|
||||
@@ -1,15 +1,5 @@
|
||||
import { showToast } from "../../hooks/showToast";
|
||||
import { getSource } from "../../sources/sources";
|
||||
|
||||
export const fetchSources = async () => {
|
||||
const url = `https://lnreader-extensions.vercel.app/api/`;
|
||||
|
||||
let res = await fetch(url);
|
||||
let sources = await res.json();
|
||||
|
||||
return sources;
|
||||
};
|
||||
|
||||
export const fetchNovel = async (sourceId, novelUrl) => {
|
||||
const source = getSource(sourceId);
|
||||
|
||||
@@ -31,8 +21,6 @@ export const fetchNovel = async (sourceId, novelUrl) => {
|
||||
chapters: res.chapters,
|
||||
};
|
||||
|
||||
// console.log(novel);
|
||||
|
||||
return novel;
|
||||
};
|
||||
|
||||
@@ -45,12 +33,11 @@ export const fetchChapter = async (sourceId, novelUrl, chapterUrl) => {
|
||||
};
|
||||
|
||||
export const fetchChapters = async (sourceId, novelUrl) => {
|
||||
const url = `https://lnreader-extensions.vercel.app/api/${sourceId}/novel/${novelUrl}`;
|
||||
const source = getSource(sourceId);
|
||||
|
||||
let data = await fetch(url);
|
||||
let res = await data.json();
|
||||
const res = await source.parseNovelAndChapters(novelUrl);
|
||||
|
||||
const chapters = res.novelChapters;
|
||||
const chapters = res.chapters;
|
||||
|
||||
return chapters;
|
||||
};
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user