Compare commits

...

27 Commits

Author SHA1 Message Date
Rajarshee Chatterjee 1a8d640db1 Add delete downloads dialog and refactor themes 2021-07-17 07:13:00 +05:30
Rajarshee Chatterjee dc6b2deddd Minor fixes 2021-07-16 22:05:49 +05:30
Rajarshee Chatterjee a9fcf2fe08 Fix NovelCover 2021-07-16 17:46:04 +05:30
Rajarshee Chatterjee f195b9e6df Minor fixes 2021-07-16 15:50:01 +05:30
Rajarshee Chatterjee d07fd26cb3 Refactor 2021-07-16 15:22:36 +05:30
Rajarshee Chatterjee 5aee0d1b04 Refactor 2021-07-15 22:51:50 +05:30
Rajarshee Chatterjee c4457d74c4 Parse links in chapter 2021-07-15 08:52:04 +05:30
Rajarshee Chatterjee ba90a6fb84 Refactor sources and show headings 2021-07-14 12:27:07 +05:30
Bread Genie e7424cea06 Fix: source images (#50)
One of the source image was linked to a fork and rest were linked to the old extension repo (this would still work, but for consistency's sake every image is linked to the repo under the org).
2021-07-14 10:19:16 +05:30
Rajarshee Chatterjee e274feda6e Minor fixes 2021-07-13 17:07:55 +05:30
Bread Genie 8ec2378191 feat(source): Add Yushubo (#48) 2021-07-13 17:05:42 +05:30
Rajarshee Chatterjee ef57a98681 Minor fixes 2021-07-12 09:49:05 +05:30
Rajarshee Chatterjee bdda959f36 Improve novel updates 2021-07-11 16:14:17 +05:30
Rajarshee Chatterjee 04f3ba2b45 Refactor themes 2021-07-11 09:43:12 +05:30
Rajarshee Chatterjee f1a261ab28 Add ranobes.net 2021-07-10 10:52:26 +05:30
Rajarshee Chatterjee da2ee3b883 Pause/Resume downloads in queue 2021-07-09 17:58:19 +05:30
Marcin Majewski b98d3861b4 format: Setting Prettier formating in package.json (#47)
Because of different formatting on some computers
Prettier settings were set in package.json file so that everyone
will format their code in the same way using Prettier
2021-07-09 15:13:56 +05:30
Rajarshee Chatterjee ff59330d20 Add Novel Updates 2021-07-09 14:24:26 +05:30
Rajarshee Chatterjee b0639e79a8 Add new sources 2021-07-09 08:51:08 +05:30
Rajarshee Chatterjee 5931a0dc07 Cleanup 2021-07-08 11:22:45 +05:30
Rajarshee Chatterjee cf40acb725 Create multisrc 2021-07-07 14:56:01 +05:30
Rajarshee Chatterjee a2974ccb44 Merge 2021-07-06 20:11:45 +05:30
Rajarshee Chatterjee 5eda558f09 Add LNMTL 2021-07-06 20:07:45 +05:30
Marcin Majewski cf7f1402d3 Fixed Syosetu name and Oneshot not working (#46)
* Fixed Syosetu name and Oneshot not working

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

* Remove unnecessary console.logs
2021-07-06 20:06:36 +05:30
Rajarshee Chatterjee 834a8d9e2e Minor fix 2021-07-06 06:16:28 +05:30
Rajarshee Chatterjee fe5b3f4a5b Add Syosetu 2021-07-06 06:11:30 +05:30
Rajarshee Chatterjee ec4eaf4d49 Remove mipmap-anydpi-v26 folder 2021-07-05 16:15:20 +05:30
100 changed files with 24617 additions and 11244 deletions
-4
View File
@@ -1,4 +0,0 @@
{
"12bb71342c6255bbf50437ec8f4441c083f47cdb74bd89160c15e4f43e52a1cb": true,
"40b842e832070c58deac6aa9e08fa459302ee3f9da492c7e77d93d2fbf4a56fd": true
}
+1 -1
View File
@@ -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 = () => {
+1 -1
View File
@@ -11,7 +11,7 @@
<uses-permission android:name="android.permission.WAKE_LOCK"/>
<uses-permission android:name="android.permission.ACCESS_NETWORK_STATE"/>
<uses-permission android:name="com.google.android.c2dm.permission.RECEIVE"/>
<application android:name=".MainApplication" android:label="@string/app_name" android:icon="@mipmap/ic_launcher" android:roundIcon="@mipmap/ic_launcher_round" android:allowBackup="true" android:theme="@style/AppTheme">
<application android:name=".MainApplication" android:label="@string/app_name" android:icon="@mipmap/ic_launcher" android:roundIcon="@mipmap/ic_launcher_round" android:allowBackup="true" android:theme="@style/AppTheme" android:usesCleartextTraffic="true">
<meta-data android:name="expo.modules.updates.EXPO_UPDATE_URL" android:value="https://exp.host/@rajarsheechatterjee/LNReader"/>
<meta-data android:name="expo.modules.updates.EXPO_SDK_VERSION" android:value="41.0.0"/>
<meta-data android:name="expo.modules.updates.ENABLED" android:value="false"/>
@@ -1,62 +0,0 @@
// FullScreenModule.java
package com.rajarsheechatterjee.LNReader;
import android.os.Build;
import android.view.View;
import android.view.WindowManager;
import android.app.Activity;
import com.facebook.react.bridge.NativeModule;
import com.facebook.react.bridge.ReactApplicationContext;
import com.facebook.react.bridge.ReactContext;
import com.facebook.react.bridge.UiThreadUtil;
import com.facebook.react.bridge.ReactContextBaseJavaModule;
import com.facebook.react.bridge.ReactMethod;
public class FullScreenModule extends ReactContextBaseJavaModule {
@Override
public String getName() {
return "FullScreen";
}
@ReactMethod
public void enable() {
UiThreadUtil.runOnUiThread(
new Runnable() {
@Override
public void run() {
getCurrentActivity().getWindow().getDecorView().setSystemUiVisibility(
View.SYSTEM_UI_FLAG_LAYOUT_FULLSCREEN |
View.SYSTEM_UI_FLAG_LAYOUT_HIDE_NAVIGATION |
View.SYSTEM_UI_FLAG_FULLSCREEN |
View.SYSTEM_UI_FLAG_HIDE_NAVIGATION |
View.SYSTEM_UI_FLAG_IMMERSIVE_STICKY
);
}
}
);
}
@ReactMethod
public void disable() {
UiThreadUtil.runOnUiThread(
new Runnable() {
@Override
public void run() {
getCurrentActivity().getWindow().getDecorView().setSystemUiVisibility(
View.SYSTEM_UI_FLAG_LAYOUT_FULLSCREEN |
View.SYSTEM_UI_FLAG_LAYOUT_HIDE_NAVIGATION |
View.SYSTEM_UI_FLAG_LAYOUT_STABLE
);
}
}
);
}
FullScreenModule(ReactApplicationContext reactContext) {
super(reactContext);
}
}
@@ -1,29 +0,0 @@
package com.rajarsheechatterjee.LNReader;
import com.facebook.react.ReactPackage;
import com.facebook.react.bridge.NativeModule;
import com.facebook.react.bridge.ReactApplicationContext;
import com.facebook.react.uimanager.ViewManager;
import java.util.ArrayList;
import java.util.Collections;
import java.util.List;
public class FullScreenPackage implements ReactPackage {
@Override
public List<ViewManager> createViewManagers(ReactApplicationContext reactContext) {
return Collections.emptyList();
}
@Override
public List<NativeModule> createNativeModules(
ReactApplicationContext reactContext) {
List<NativeModule> modules = new ArrayList<>();
modules.add(new FullScreenModule(reactContext));
return modules;
}
}
@@ -31,9 +31,6 @@ import java.util.Arrays;
import java.util.List;
import javax.annotation.Nullable;
import com.rajarsheechatterjee.LNReader.FullScreenPackage;
public class MainApplication extends Application implements ReactApplication {
private final ReactModuleRegistryProvider mModuleRegistryProvider = new ReactModuleRegistryProvider(
new BasePackageList().getPackageList()
@@ -49,7 +46,6 @@ public class MainApplication extends Application implements ReactApplication {
protected List<ReactPackage> getPackages() {
List<ReactPackage> packages = new PackageList(this).getPackages();
packages.add(new ModuleRegistryAdapter(mModuleRegistryProvider));
packages.add(new FullScreenPackage());
return packages;
}
@@ -8,7 +8,6 @@ public class BasePackageList {
public List<Package> getPackageList() {
return Arrays.<Package>asList(
new expo.modules.application.ApplicationPackage(),
new expo.modules.backgroundfetch.BackgroundFetchPackage(),
new expo.modules.constants.ConstantsPackage(),
new expo.modules.documentpicker.DocumentPickerPackage(),
new expo.modules.errorrecovery.ErrorRecoveryPackage(),
@@ -19,10 +18,8 @@ public class BasePackageList {
new expo.modules.lineargradient.LinearGradientPackage(),
new expo.modules.notifications.NotificationsPackage(),
new expo.modules.permissions.PermissionsPackage(),
new expo.modules.sharing.SharingPackage(),
new expo.modules.splashscreen.SplashScreenPackage(),
new expo.modules.sqlite.SQLitePackage(),
new expo.modules.taskManager.TaskManagerPackage(),
new expo.modules.updates.UpdatesPackage(),
new expo.modules.webbrowser.WebBrowserPackage()
);
Vendored Regular → Executable
View File
-35
View File
@@ -3,44 +3,9 @@
"name": "LNReader",
"slug": "LNReader",
"scheme": "lnreader",
"version": "1.1.1",
"orientation": "default",
"icon": "./assets/icon.png",
"splash": {
"image": "./assets/splash.png",
"resizeMode": "cover",
"backgroundColor": "#000000"
},
"updates": {
"fallbackToCacheTimeout": 0,
"enabled": false
},
"notification": {
"icon": "./assets/notification.png",
"color": "#00adb5"
},
"assetBundlePatterns": ["**/*"],
"ios": {
"supportsTablet": true
},
"android": {
"adaptiveIcon": {
"foregroundImage": "./assets/icon.png",
"backgroundColor": "#FFFFFF"
},
"package": "com.rajarsheechatterjee.LNReader"
},
"androidStatusBar": {
"barStyle": "light-content",
"backgroundColor": "#00000000",
"translucent": true
},
"androidNavigationBar": {
"barStyle": "light-content",
"backgroundColor": "#000000"
},
"web": {
"favicon": "./assets/favicon.png"
}
}
}
+19712 -8700
View File
File diff suppressed because it is too large Load Diff
+10 -10
View File
@@ -15,19 +15,14 @@
"@react-navigation/native": "^5.8.10",
"@react-navigation/stack": "^5.12.8",
"expo": "^41.0.1",
"expo-background-fetch": "~9.1.0",
"expo-document-picker": "^9.1.2",
"expo-file-system": "~11.0.2",
"expo-font": "~9.1.0",
"expo-linear-gradient": "~9.1.0",
"expo-linking": "~2.2.3",
"expo-notifications": "~0.11.6",
"expo-permissions": "^12.0.1",
"expo-sharing": "~9.1.2",
"expo-splash-screen": "~0.10.2",
"expo-splash-screen": "^0.11.2",
"expo-sqlite": "~9.1.0",
"expo-status-bar": "~1.0.4",
"expo-task-manager": "~9.1.0",
"expo-updates": "~0.5.4",
"expo-web-browser": "~9.1.0",
"moment": "^2.29.1",
@@ -37,15 +32,12 @@
"react-native": "^0.64.2",
"react-native-background-actions": "^2.6.1",
"react-native-cheerio": "^1.0.0-rc.4",
"react-native-collapsible-header-views": "^1.1.2",
"react-native-device-info": "^8.1.3",
"react-native-easing-gradient": "^0.1.2",
"react-native-fast-image": "^8.3.4",
"react-native-fit-image": "^1.5.5",
"react-native-gesture-handler": "~1.10.2",
"react-native-immersive-mode": "^2.0.0",
"react-native-navigation-bar-color": "^2.0.1",
"react-native-offline": "^5.7.0",
"react-native-pager-view": "^5.1.10",
"react-native-paper": "^4.5.0",
"react-native-reanimated": "^2.1.0",
@@ -59,7 +51,6 @@
"react-native-webview": "^11.6.2",
"react-redux": "^7.2.2",
"redux": "^4.0.5",
"redux-devtools-extension": "^2.13.9",
"redux-persist": "^6.0.0",
"redux-thunk": "^2.3.0",
"rn-sliding-up-panel": "^2.4.4"
@@ -67,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"
+12 -48
View File
@@ -1,52 +1,8 @@
import React from "react";
import { StyleSheet } from "react-native";
import { Dimensions, StyleSheet, View } from "react-native";
import { IconButton } from "react-native-paper";
const actions = [
{
icon: "download-outline",
onPress: () => {
dispatch(
downloadAllChaptersAction(
novel.sourceId,
novel.novelUrl,
selected
)
);
setSelected([]);
},
},
{
icon: "trash-can-outline",
onPress: () => {
dispatch(deleteAllChaptersAction(selected));
setSelected([]);
},
},
{
icon: "bookmark-outline",
onPress: () => {
dispatch(bookmarkChapterAction(selected));
setSelected([]);
},
},
{
icon: "check",
onPress: () => {
dispatch(markChaptersRead(selected, novel.novelId, sort, filter));
setSelected([]);
},
},
{
icon: "check-outline",
onPress: () => {
dispatch(markChapterUnreadAction(selected, novel.novelId));
setSelected([]);
},
},
];
export const Actionbar = ({ actions, theme }) => {
return (
<View
@@ -55,9 +11,17 @@ export const Actionbar = ({ actions, theme }) => {
{ backgroundColor: theme.colorPrimary },
]}
>
{actions.map((action) => (
<IconButton icon={action.icon} onPress={action.onPress} />
))}
{actions.map(
(action, index) =>
action.icon && (
<IconButton
key={index}
icon={action.icon}
color={theme.textColorPrimary}
onPress={action.onPress}
/>
)
)}
</View>
);
};
+1
View File
@@ -10,6 +10,7 @@ const ColorPickerModal = ({
onSubmit,
hideModal,
modalVisible,
showAccentColors,
}) => {
const [text, setText] = useState(color);
const [error, setError] = useState();
+2 -2
View File
@@ -10,7 +10,7 @@ const EmptyView = ({ icon, description, style, children, iconStyle }) => {
<Text
style={[
styles.emptyViewIcon,
{ color: theme.textColorSecondary },
{ color: theme.textColorHint },
style,
iconStyle,
]}
@@ -20,7 +20,7 @@ const EmptyView = ({ icon, description, style, children, iconStyle }) => {
<Text
style={[
styles.emptyViewText,
{ color: theme.textColorSecondary },
{ color: theme.textColorHint },
style,
]}
>
+1 -1
View File
@@ -17,7 +17,7 @@ const ErrorView = ({ theme, onRetry, error, openWebView }) => (
icon="reload"
size={25}
style={{ margin: 0, marginTop: 24 }}
color={theme.textColorSecondary}
color={theme.text}
onPress={onRetry}
/>
<Text style={{ color: theme.textColorPrimary }}>Retry</Text>
+4 -1
View File
@@ -22,7 +22,10 @@ const ListView = ({
<>
<FastImage
source={{ uri: item.novelCover }}
style={styles.extensionIcon}
style={[
styles.extensionIcon,
inLibraryBadge && { opacity: 0.5 },
]}
/>
<Text
style={[
@@ -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>
);
+186 -134
View File
@@ -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,121 +26,42 @@ 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.7)"]}
style={styles.linearGradient}
>
<Text
numberOfLines={2}
style={[styles.title, { color: "rgba(255,255,255,1)" }]}
>
{item.novelName}
</Text>
</LinearGradient>
</View>
);
const unreadBadge = () =>
showUnreadBadges &&
item.chaptersUnread && (
<Text
style={[
styles.unreadBadge,
!item.chaptersDownloaded && {
borderTopLeftRadius: 4,
borderBottomLeftRadius: 4,
},
!showDownloadBadges && {
borderRadius: 4,
},
{
backgroundColor: theme.colorAccent,
color: theme.colorButtonText,
},
]}
>
{item.chaptersUnread}
</Text>
);
const downloadBadge = () =>
showDownloadBadges &&
item.chaptersDownloaded && (
<Text
style={[
styles.downloadBadge,
!item.chaptersUnread && {
borderTopRightRadius: 4,
borderBottomRightRadius: 4,
},
!showUnreadBadges && {
borderRadius: 4,
},
]}
>
{item.chaptersDownloaded}
</Text>
);
const inLibraryBadge = () =>
libraryStatus && (
<Text
style={[
styles.inLibraryBadge,
{
backgroundColor: theme.colorAccent,
color: theme.colorButtonText,
borderRadius: 4,
},
]}
>
In library
</Text>
);
const 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={{
position: "absolute",
zIndex: 1,
top: 8,
left: 8,
flexDirection: "row",
}}
>
{inLibraryBadge()}
{downloadBadge()}
{unreadBadge()}
<View style={styles.badgeContainer}>
{libraryStatus && <InLibraryBadge theme={theme} />}
{showDownloadBadges && item.chaptersDownloaded && (
<DownloadBadge
showUnreadBadges={showUnreadBadges}
chaptersDownloaded={item.chaptersDownloaded}
chaptersUnread={item.chaptersUnread}
/>
)}
{showUnreadBadges && item.chaptersUnread && (
<UnreadBadge
theme={theme}
chaptersDownloaded={item.chaptersDownloaded}
chaptersUnread={item.chaptersUnread}
showDownloadBadges={showDownloadBadges}
/>
)}
</View>
<FastImage
source={{ uri: item.novelCover }}
@@ -144,26 +70,45 @@ const NovelCover = ({ item, onPress, libraryStatus }) => {
libraryStatus && { opacity: 0.5 },
]}
/>
<View
style={{
position: "absolute",
bottom: 4,
left: 4,
right: 4,
}}
>
{compactTitle()}
<View style={styles.compactTitleContainer}>
{displayMode === 0 && (
<CompactTitle novelName={item.novelName} />
)}
</View>
{comfortableTitle()}
{displayMode === 1 && (
<ComfortableTitle
novelName={item.novelName}
theme={theme}
/>
)}
</>
</TouchableRipple>
</Pressable>
</View>
) : (
<ListView
item={item}
downloadBadge={downloadBadge()}
unreadBadge={unreadBadge()}
inLibraryBadge={inLibraryBadge()}
downloadBadge={
showDownloadBadges &&
item.chaptersDownloaded && (
<DownloadBadge
showUnreadBadges={showUnreadBadges}
chaptersDownloaded={item.chaptersDownloaded}
chaptersUnread={item.chaptersUnread}
/>
)
}
unreadBadge={
showUnreadBadges &&
item.chaptersUnread && (
<UnreadBadge
theme={theme}
chaptersDownloaded={item.chaptersDownloaded}
chaptersUnread={item.chaptersUnread}
showDownloadBadges={showDownloadBadges}
/>
)
}
inLibraryBadge={libraryStatus && <InLibraryBadge theme={theme} />}
theme={theme}
onPress={onPress}
/>
@@ -172,6 +117,99 @@ 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,
@@ -187,9 +225,10 @@ const styles = StyleSheet.create({
borderRadius: 4,
},
opac: {
paddingHorizontal: 4.5,
paddingHorizontal: 4,
paddingVertical: 4,
borderRadius: 4,
flex: 1,
},
extensionIcon: {
width: 42,
@@ -227,4 +266,17 @@ const styles = StyleSheet.create({
paddingHorizontal: 4,
fontSize: 12,
},
compactTitleContainer: {
position: "absolute",
bottom: 4,
left: 4,
right: 4,
},
badgeContainer: {
position: "absolute",
zIndex: 1,
top: 8,
left: 8,
flexDirection: "row",
},
});
+4 -2
View File
@@ -1,4 +1,4 @@
import React from "react";
import React, { useCallback } from "react";
import { StyleSheet, FlatList } from "react-native";
import { useSettings } from "../hooks/reduxHooks";
@@ -29,13 +29,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,
+40 -14
View File
@@ -17,15 +17,15 @@ export const insertChapters = async (novelId, chapters) => {
novelId,
],
(txObj, res) => {},
(txObj, error) =>
console.log(
"Error ",
error,
" Chapter URL: ",
chapter.chapterUrl,
" Novel ID ",
novelId
)
(txObj, error) => {}
// console.log(
// "Error ",
// error,
// " Chapter URL: ",
// chapter.chapterUrl,
// " Novel ID ",
// novelId
// )
),
(txObj, res) => {
console.log("Success");
@@ -35,13 +35,10 @@ export const insertChapters = async (novelId, chapters) => {
});
};
const getChaptersQuery = (sort, filter) =>
`SELECT * FROM chapters WHERE novelId = ? ${filter || ""} ${
sort || "ORDER BY chapterId ASC"
}`;
const getChaptersQuery = (sort = "ORDER BY chapterId ASC", filter = "") =>
`SELECT * FROM chapters WHERE novelId = ? ${filter} ${sort}`;
export const getChapters = (novelId, sort, filter) => {
// console.log(getChaptersQuery(sort, filter));
return new Promise((resolve, reject) =>
db.transaction((tx) => {
tx.executeSql(
@@ -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 -3
View File
@@ -85,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)
);
});
-10
View File
@@ -1,10 +0,0 @@
import { setStatusBarStyle as setExpoStatusBarStyle } from "expo-status-bar";
import { useTheme } from "../hooks/reduxHooks";
import changeNavigationBarColor from "react-native-navigation-bar-color";
export const setStatusBarStyle = () => {
const theme = useTheme();
setExpoStatusBarStyle(theme.id === 1 ? "dark" : "light");
changeNavigationBarColor(theme.colorPrimary);
};
-35
View File
@@ -1,35 +0,0 @@
import { useState, useEffect } from "react";
import { getSource } from "../sources/sources";
const usePopularNovels = (sourceId) => {
const [loading, setLoading] = useState(true);
const [pageNumber, setPage] = useState(1);
const [novels, setNovels] = useState([]);
const [error, setError] = useState();
const source = getSource(sourceId);
useEffect(() => {
const getNextPage = async (p) => {
try {
if (p === 1) setLoading(true);
const res = await source.popularNovels(p);
setHasMoreNovels(res.totalPages > pageNumber);
setNovels((novels) => [...novels, ...data.novels]);
setLoading(false);
} catch (err) {
setError(err.message);
}
};
getNextPage(pageNumber);
}, [pageNumber, sourceId]);
const incrementPage = () => {
setPage((pageNumber) => pageNumber + 1);
};
return { novels, incrementPage, loading, error };
};
export { usePopularNovels };
+70 -73
View File
@@ -1,5 +1,4 @@
import React from "react";
import { View } from "react-native";
import { createMaterialBottomTabNavigator } from "@react-navigation/material-bottom-tabs";
import MaterialCommunityIcons from "react-native-vector-icons/MaterialCommunityIcons";
@@ -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
View File
@@ -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
View File
@@ -1,9 +1,8 @@
import React from "react";
import { View } from "react-native";
import { useTheme } from "../hooks/reduxHooks";
import { createStackNavigator } from "@react-navigation/stack";
// Screens
import About from "../screens/More/About";
import Settings from "../screens/More/Settings";
import TrackerSettings from "../screens/More/TrackerSettings";
@@ -12,55 +11,30 @@ import BackupSettings from "../screens/More/BackupSettings";
import AdvancedSettings from "../screens/More/AdvancedSettings";
import GeneralSettings from "../screens/More/GeneralSettings";
import DownloadQueue from "../screens/More/DownloadQueue";
import Downloads from "../screens/More/Downloads";
const Stack = createStackNavigator();
const stackNavigatorConfig = { headerShown: false };
const SettingsStack = () => {
const theme = useTheme();
const SettingsStack = () => (
<Stack.Navigator screenOptions={stackNavigatorConfig}>
<Stack.Screen name="Settings" component={Settings} />
<Stack.Screen name="GeneralSettings" component={GeneralSettings} />
<Stack.Screen name="ReaderSettings" component={ReaderSettings} />
<Stack.Screen name="TrackerSettings" component={TrackerSettings} />
<Stack.Screen name="BackupSettings" component={BackupSettings} />
<Stack.Screen name="AdvancedSettings" component={AdvancedSettings} />
</Stack.Navigator>
);
return (
<View style={{ flex: 1, backgroundColor: theme.colorPrimaryDark }}>
<Stack.Navigator screenOptions={stackNavigatorConfig}>
<Stack.Screen name="Settings" component={Settings} />
<Stack.Screen
name="GeneralSettings"
component={GeneralSettings}
/>
<Stack.Screen
name="ReaderSettings"
component={ReaderSettings}
/>
<Stack.Screen
name="TrackerSettings"
component={TrackerSettings}
/>
<Stack.Screen
name="BackupSettings"
component={BackupSettings}
/>
<Stack.Screen
name="AdvancedSettings"
component={AdvancedSettings}
/>
</Stack.Navigator>
</View>
);
};
const MoreStack = () => {
const theme = useTheme();
return (
<View style={{ flex: 1, backgroundColor: theme.colorPrimaryDark }}>
<Stack.Navigator screenOptions={stackNavigatorConfig}>
<Stack.Screen name="SettingsStack" component={SettingsStack} />
<Stack.Screen name="About" component={About} />
<Stack.Screen name="DownloadQueue" component={DownloadQueue} />
</Stack.Navigator>
</View>
);
};
const MoreStack = () => (
<Stack.Navigator screenOptions={stackNavigatorConfig}>
<Stack.Screen name="SettingsStack" component={SettingsStack} />
<Stack.Screen name="About" component={About} />
<Stack.Screen name="DownloadQueue" component={DownloadQueue} />
<Stack.Screen name="Downloads" component={Downloads} />
</Stack.Navigator>
);
export default MoreStack;
+2
View File
@@ -1,2 +1,4 @@
export const SET_DOWNLOAD_QUEUE = "SET_DOWNLOAD_QUEUE";
export const CANCEL_DOWNLOAD = "CANCEL_DOWNLOAD";
export const PAUSE_DOWNLOADS = "PAUSE_DOWNLOADS";
export const RESUME_DOWNLOADS = "RESUME_DOWNLOADS";
+94 -1
View File
@@ -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();
+6
View File
@@ -307,6 +307,12 @@ export const downloadAllChaptersAction =
try {
let chapters = chaps.filter((chapter) => chapter.downloaded === 0);
chapters = chapters.map((chapter) => ({
...chapter,
sourceId,
novelUrl,
}));
dispatch({ type: SET_DOWNLOAD_QUEUE, payload: chapters });
const options = {
taskName: "Library Update",
+16
View File
@@ -4,6 +4,8 @@ import {
SET_READER_SETTINGS,
SET_APP_SETTINGS,
SET_ACCENT_COLOR,
SET_RIPPLE_COLOR,
SET_AMOLED_MODE,
} from "./settings.types";
export const setAppTheme = (val) => (dispatch) => {
@@ -40,3 +42,17 @@ export const setAccentColor = (val) => (dispatch) => {
payload: val,
});
};
export const setRippleColor = (val) => (dispatch) => {
dispatch({
type: SET_RIPPLE_COLOR,
payload: val,
});
};
export const setAmoledMode = (id, val) => (dispatch) => {
dispatch({
type: SET_AMOLED_MODE,
payload: { id, val },
});
};
+27 -2
View File
@@ -4,13 +4,15 @@ 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,
@@ -21,7 +23,7 @@ const themes = {
1: lightTheme,
2: darkTheme,
3: midnightDuskTheme,
4: limeTheme,
4: greenAppleTheme,
5: irisBlueTheme,
6: hotPinkTheme,
7: strawberryDaiquiri,
@@ -65,6 +67,7 @@ const initialState = {
searchAllSources: false,
textSelectable: false,
updateLibraryOnLaunch: false,
downloadNewChapters: false,
};
const settingsReducer = (state = initialState, action) => {
@@ -86,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,
+2
View File
@@ -3,3 +3,5 @@ export const SET_APP_THEME = "SET_APP_THEME";
export const SET_NOVELS_PER_ROW = "SET_NOVELS_PER_ROW";
export const SET_READER_SETTINGS = "SET_READER_SETTINGS";
export const SET_ACCENT_COLOR = "SET_ACCENT_COLOR";
export const SET_RIPPLE_COLOR = "SET_RIPPLE_COLOR";
export const SET_AMOLED_MODE = "SET_AMOLED_MODE";
+12 -2
View File
@@ -6,11 +6,21 @@ import {
SEARCH_SOURCES,
} from "./source.types";
import { showToast } from "../../hooks/showToast";
import { sources } from "../../sources/sources";
import sources from "../../sources/sources.json";
export const getSourcesAction = () => async (dispatch) => {
try {
dispatch({ type: GET_SOURCES, payload: sources });
/**
* Sort sources aplhabetically
*/
let sortedSources = sources.sort((a, b) =>
a.sourceName.localeCompare(b.sourceName)
);
dispatch({
type: GET_SOURCES,
payload: sortedSources,
});
} catch (error) {
showToast(error.message);
}
-2
View File
@@ -12,7 +12,6 @@ const initialState = {
search: [],
pinned: [],
filters: [],
showNovelUpdates: true,
showMyAnimeList: true,
loading: true,
};
@@ -44,7 +43,6 @@ const sourceReducer = (state = initialState, action) => {
if (!state.filters) {
state.filters = [];
}
return {
...state,
filters:
+6 -2
View File
@@ -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());
};
+30 -54
View File
@@ -7,17 +7,18 @@ import {
ActivityIndicator,
RefreshControl,
} from "react-native";
import { useTheme } from "../../hooks/reduxHooks";
import { useDispatch, useSelector } from "react-redux";
import {
getSourcesAction,
searchSourcesAction,
} from "../../redux/source/source.actions";
import SourceCard from "./components/SourceCard";
import { useTheme } from "../../hooks/reduxHooks";
import { showToast } from "../../hooks/showToast";
import { Searchbar } from "../../components/Searchbar";
import { Searchbar } from "../../components/Searchbar/Searchbar";
import SourceCard from "./components/SourceCard";
import EmptyView from "../../components/EmptyView";
import DiscoverCard from "./discover/DiscoverCard";
@@ -31,7 +32,6 @@ const Browse = ({ navigation }) => {
loading,
pinned,
filters = [],
showNovelUpdates = true,
showMyAnimeList = true,
} = useSelector((state) => state.sourceReducer);
const theme = useTheme();
@@ -42,6 +42,8 @@ const Browse = ({ navigation }) => {
sources = sources.filter((source) => filters.indexOf(source.lang) === -1);
let pinnedSources = sources.filter((source) => isPinned(source.sourceId));
useEffect(() => {
dispatch(getSourcesAction());
}, [getSourcesAction]);
@@ -72,6 +74,14 @@ const Browse = ({ navigation }) => {
/>
);
const SourceHeader = ({ title }) => (
<Text
style={[styles.sourcesHeader, { color: theme.textColorSecondary }]}
>
{title}
</Text>
);
return (
<View
style={[
@@ -107,32 +117,12 @@ const Browse = ({ navigation }) => {
data={!searchText ? sources : search}
keyExtractor={(item) => item.sourceId.toString()}
renderItem={renderItem}
extraData={pinned}
extraData={pinnedSources}
ListHeaderComponent={
<View>
{(showNovelUpdates || showMyAnimeList) && (
{showMyAnimeList && (
<>
<Text
style={{
color: theme.textColorSecondary,
paddingHorizontal: 16,
paddingVertical: 8,
}}
>
Discover
</Text>
{showNovelUpdates && (
<DiscoverCard
label="Novel Updates"
onPress={() =>
navigation.navigate(
"BrowseNovelUpdates"
)
}
icon={require("../../../assets/novelupdates.png")}
theme={theme}
/>
)}
<SourceHeader title="Discover" />
{showMyAnimeList && (
<DiscoverCard
label="MyAnimeList"
@@ -145,41 +135,21 @@ const Browse = ({ navigation }) => {
)}
</>
)}
{pinned.length > 0 && (
{pinnedSources.length > 0 && (
<FlatList
contentContainerStyle={{ paddingBottom: 16 }}
data={sources.filter((source) =>
isPinned(source.sourceId)
)}
data={pinnedSources}
keyExtractor={(item) =>
item.sourceId.toString()
}
renderItem={renderItem}
extraData={pinned}
extraData={pinnedSources}
ListHeaderComponent={
<Text
style={{
color: theme.textColorSecondary,
paddingHorizontal: 16,
paddingVertical: 8,
}}
>
Pinned
</Text>
<SourceHeader title="Pinned" />
}
/>
)}
{sources.length > 0 && (
<Text
style={{
color: theme.textColorSecondary,
paddingHorizontal: 16,
paddingVertical: 8,
}}
>
Sources
</Text>
)}
{sources.length > 0 && <SourceHeader title="Sources" />}
</View>
}
ListEmptyComponent={
@@ -214,5 +184,11 @@ const Browse = ({ navigation }) => {
export default Browse;
const styles = StyleSheet.create({
container: { flex: 1 },
container: {
flex: 1,
},
sourcesHeader: {
paddingHorizontal: 16,
paddingVertical: 8,
},
});
+4 -2
View File
@@ -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();
+5 -27
View File
@@ -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={{
+1 -5
View File
@@ -21,11 +21,7 @@ const ExtensionCard = ({ item, theme, isPinned, dispatch, navigation }) => {
onPress={navigateToExtension}
android_ripple={{ color: theme.rippleColor }}
>
<Image
source={{ uri: icon }}
style={styles.extensionIcon}
// resizeMode="contain"
/>
<Image source={{ uri: icon }} style={styles.extensionIcon} />
<View style={styles.extensionDetails}>
<View>
<Text
@@ -1,127 +0,0 @@
import React, { useState, useEffect } from "react";
import {
ActivityIndicator,
Button,
FlatList,
StyleSheet,
Text,
View,
} from "react-native";
import * as WebBrowser from "expo-web-browser";
import { ScreenContainer } from "../../../components/Common";
import { Searchbar } from "../../../components/Searchbar";
import { useTheme } from "../../../hooks/reduxHooks";
import { scrapeSearchResults, scrapeTopNovels } from "./NovelUpdatesScraper";
import NovelUpdatesNovelCard from "./NovelUpdatesNovelCard";
const BrowseNovelUpdates = ({ navigation }) => {
const theme = useTheme();
const [novels, setNovels] = useState([]);
const [pageNo, setPageNo] = useState(1);
const [loading, setLoading] = useState(true);
const [searchText, setSearchText] = useState("");
const getNovels = async (pageNo) => {
const data = await scrapeTopNovels(pageNo);
setNovels((novels) => novels.concat(data));
setLoading(false);
};
useEffect(() => {
getNovels(1);
}, []);
const getSearchResults = async () => {
setLoading(true);
const data = await scrapeSearchResults(searchText);
setNovels(data);
setLoading(false);
};
const clearSearchbar = () => {
getNovels();
setLoading(true);
setSearchText("");
};
const isCloseToBottom = ({
layoutMeasurement,
contentOffset,
contentSize,
}) => {
const paddingToBottom = 20;
return (
layoutMeasurement.height + contentOffset.y >=
contentSize.height - paddingToBottom
);
};
return (
<ScreenContainer theme={theme}>
<Searchbar
theme={theme}
placeholder="Search Novel Updates"
backAction="arrow-left"
onBackAction={navigation.goBack}
searchText={searchText}
onChangeText={(text) => setSearchText(text)}
onSubmitEditing={getSearchResults}
clearSearchbar={clearSearchbar}
actions={[
{
icon: "earth",
onPress: () =>
WebBrowser.openBrowserAsync(
"https://www.novelupdates.com/"
),
},
]}
/>
{loading ? (
<View style={{ flex: 1, justifyContent: "center" }}>
<ActivityIndicator size={60} color={theme.colorAccent} />
</View>
) : (
<FlatList
data={novels}
keyExtractor={(item) => item.novelName.toString()}
renderItem={({ item }) => (
<NovelUpdatesNovelCard
novel={item}
theme={theme}
onPress={() =>
navigation.navigate("GlobalSearch", {
novelName: item.novelName,
})
}
/>
)}
onScroll={({ nativeEvent }) => {
if (!searchText && isCloseToBottom(nativeEvent)) {
getNovels(pageNo + 1);
setPageNo((pageNo) => pageNo + 1);
}
}}
ListFooterComponent={
!searchText && (
<View style={{ paddingVertical: 16 }}>
<ActivityIndicator color={theme.colorAccent} />
</View>
)
}
/>
)}
</ScreenContainer>
);
};
export default BrowseNovelUpdates;
const styles = StyleSheet.create({});
+1 -1
View File
@@ -3,7 +3,7 @@ import { StyleSheet, View, ActivityIndicator, FlatList } from "react-native";
import * as WebBrowser from "expo-web-browser";
import { Searchbar } from "../../../components/Searchbar";
import { Searchbar } from "../../../components/Searchbar/Searchbar";
import ErrorView from "../../../components/ErrorView";
import { useTheme, useSettings } from "../../../hooks/reduxHooks";
@@ -1,5 +0,0 @@
import React from "react";
const NovelUpdatesBottomSheet = () => {};
export default NovelUpdatesBottomSheet;
@@ -1,122 +0,0 @@
import React from "react";
import { StyleSheet, Pressable, Text, View, FlatList } from "react-native";
import FastImage from "react-native-fast-image";
import { Row } from "../../../components/Common";
const GenreChip = ({ children, theme }) => (
<View style={{ flexDirection: "row" }}>
<Text
style={[styles.genreChip, { color: theme.textColorSecondary }]}
numberOfLines={2}
>
<Text
style={[
styles.genreChip,
{
color: theme.textColorPrimary,
fontSize: 14,
},
]}
>
{`${"Genre "}`}
</Text>
{children}
</Text>
</View>
);
const NovelUpdatesNovelCard = ({ novel, theme, onPress }) => {
return (
<View
style={{
flex: 1,
backgroundColor: theme.colorPrimary,
borderRadius: 8,
elevation: 1,
marginHorizontal: 12,
marginVertical: 4,
}}
>
<Pressable
style={styles.novelUpdatesCard}
onPress={onPress}
android_ripple={{ color: theme.rippleColor, borderless: true }}
>
<FastImage
source={{ uri: novel.novelCover }}
style={{
width: 130,
height: 180,
borderTopLeftRadius: 8,
borderBottomLeftRadius: 8,
}}
/>
<View
style={{
flex: 1,
paddingHorizontal: 8,
paddingVertical: 8,
}}
>
<Text
style={{ color: theme.textColorPrimary, fontSize: 17 }}
>
{novel.novelName}
</Text>
<GenreChip theme={theme}>{novel.genres}</GenreChip>
<View style={{ flexDirection: "row", marginBottom: 4 }}>
<Text
style={[
styles.genreChip,
{ color: theme.textColorSecondary },
]}
numberOfLines={2}
>
<Text
style={[
styles.genreChip,
{
color: theme.textColorPrimary,
fontSize: 14,
},
]}
>
{`${"Chapter Count "}`}
</Text>
{novel.chapterCount}
</Text>
</View>
<Text
style={{
fontSize: 12,
color: theme.textColorSecondary,
marginTop: 8,
}}
numberOfLines={4}
>
{novel.novelSummary}
</Text>
</View>
</Pressable>
</View>
);
};
export default NovelUpdatesNovelCard;
const styles = StyleSheet.create({
novelUpdatesCard: {
flex: 1,
flexDirection: "row",
},
contentContainer: {
flex: 1,
},
genreChip: {
marginTop: 4,
fontSize: 13,
// fontStyle: "italic",
},
});
@@ -1,130 +0,0 @@
import cheerio from "react-native-cheerio";
const scrapeTopNovels = async (pageNo) => {
const url =
"https://www.novelupdates.com/series-ranking/?rank=sixmonths&pg=" +
pageNo;
let headers = new Headers({
Accept: "application/json",
"Content-Type": "application/json",
"User-Agent":
"'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/83.0.4103.116 Safari/537.36",
});
const res = await fetch(url, {
method: "GET",
headers: headers,
});
const body = await res.text();
const $ = cheerio.load(body);
const novels = [];
$("div.search_main_box_nu").each(function (res) {
const novelCover = $(this).find("img").attr("src");
const novelName = $(this).find(".search_title > a").text();
const genres = $(this)
.find(".search_genre")
.text()
.replace(/\s/g, ", ");
const novelSummary =
$(this).find(".search_genre")[0].nextSibling.nodeValue;
let chapterCount = "";
$(this)
.find(".search_stats > span")
.each(function () {
let detail = $(this).text();
if (detail.includes("Chapters")) {
chapterCount = detail.replace(" Chapters", "");
}
});
// const chapterCount = $(this)
// .find('i[title="Chapter Count"]')
// const updateFrequency = $(this)
// .find('i[title="Updates Frequency"]')
const novel = {
novelName,
novelCover,
genres,
novelSummary,
chapterCount,
// updateFrequency,
};
novels.push(novel);
});
return novels;
};
const scrapeSearchResults = async (searchTerm) => {
const url =
"https://www.novelupdates.com/?s=" +
searchTerm +
"&post_type=seriesplans";
let headers = new Headers({
Accept: "application/json",
"Content-Type": "application/json",
"User-Agent":
"'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/83.0.4103.116 Safari/537.36",
});
const res = await fetch(url, {
method: "GET",
headers: headers,
});
const body = await res.text();
const $ = cheerio.load(body);
const novels = [];
$("div.search_main_box_nu").each(function (res) {
const novelCover = $(this).find("img").attr("src");
const novelName = $(this).find(".search_title > a").text();
const genres = $(this)
.find(".search_genre")
.text()
.replace(/\s/g, ", ");
const novelSummary =
$(this).find(".search_genre")[0].nextSibling.nodeValue;
let chapterCount = "";
$(this)
.find(".search_stats > span")
.each(function () {
let detail = $(this).text();
if (detail.includes("Chapters")) {
chapterCount = detail.replace(" Chapters", "");
}
});
const novel = {
novelName,
novelCover,
genres,
novelSummary,
chapterCount,
};
novels.push(novel);
});
return novels;
};
export { scrapeTopNovels, scrapeSearchResults };
@@ -4,7 +4,7 @@ import { StyleSheet, View, FlatList, ActivityIndicator } from "react-native";
import { ProgressBar } from "react-native-paper";
import { useSelector } from "react-redux";
import { Searchbar } from "../../../components/Searchbar";
import { Searchbar } from "../../../components/Searchbar/Searchbar";
import EmptyView from "../../../components/EmptyView";
import { ScreenContainer } from "../../../components/Common";
@@ -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>
+135 -139
View File
@@ -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);
@@ -92,8 +94,6 @@ const Chapter = ({ route, navigation }) => {
const [contentSize, setContentSize] = useState(0);
const [images, setImages] = useState([]);
const getChapter = async (chapterId) => {
try {
if (chapterId) {
@@ -101,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);
}
@@ -125,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({});
@@ -149,7 +135,6 @@ const Chapter = ({ route, navigation }) => {
useEffect(() => {
getChapter(chapterId);
setPrevAndNextChap();
!incognitoMode && dispatch(insertHistoryAction(novelId, chapterId));
return () => {
StatusBar.setHidden(false);
@@ -157,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);
@@ -185,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;
@@ -205,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({
@@ -219,25 +211,19 @@ const Chapter = ({ route, navigation }) => {
});
setFirstLayout(false);
}
};
}, []);
const hideHeader = () => {
if (!hidden) {
StatusBar.setHidden(true);
hideNavigationBar();
}
!hidden && setImmersiveMode();
setHidden(!hidden);
};
const readerStyles = [
{
paddingVertical: 16,
paddingBottom: 32,
fontSize: reader.textSize,
color: reader.textColor || readerTextColor(reader.theme),
lineHeight: readerLineHeight(reader.textSize, reader.lineHeight),
textAlign: reader.textAlign,
paddingHorizontal: `${reader.padding}%`,
},
reader.fontFamily && {
fontFamily: reader.fontFamily,
@@ -261,7 +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
@@ -275,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) }}
>
@@ -333,80 +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={textSelectable}
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={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>
)}
<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>
)}
</>
</>
);
};
+4 -4
View File
@@ -29,7 +29,7 @@ const ReaderSeekbar = ({
<View style={styles.verticalSliderContainer}>
<Text
style={{
color: theme.textColorPrimary,
color: "#FFFFFF",
marginLeft: 16,
transform: [{ rotate: "-90deg" }],
}}
@@ -52,7 +52,7 @@ const ReaderSeekbar = ({
/>
<Text
style={{
color: theme.textColorPrimary,
color: "#FFFFFF",
marginRight: 16,
transform: [{ rotate: "-90deg" }],
}}
@@ -66,7 +66,7 @@ const ReaderSeekbar = ({
<View style={styles.horizontalSliderContainer}>
<Text
style={{
color: theme.textColorPrimary,
color: "#FFFFFF",
marginLeft: 16,
}}
>
@@ -88,7 +88,7 @@ const ReaderSeekbar = ({
/>
<Text
style={{
color: theme.textColorPrimary,
color: "#FFFFFF",
marginRight: 16,
}}
>
+257
View File
@@ -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);
+45 -12
View File
@@ -1,16 +1,19 @@
import React, { useState } from "react";
import { StyleSheet, View, Text, Dimensions, Pressable } from "react-native";
import { ToggleButton, IconButton, Menu, Switch } from "react-native-paper";
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,
@@ -30,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 (
@@ -45,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
@@ -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,
@@ -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,
}}
>
+12 -6
View File
@@ -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) => {
+25 -18
View File
@@ -1,27 +1,30 @@
import React, { useEffect, useState } from "react";
import { StyleSheet, View, ActivityIndicator, Text } from "react-native";
import { StyleSheet, View, ActivityIndicator } from "react-native";
import * as WebBrowser from "expo-web-browser";
import NovelCover from "../../components/NovelCover";
import EmptyView from "../../components/EmptyView";
import { Searchbar } from "../../components/Searchbar";
import { useTheme, useLibrary, useSettings } from "../../hooks/reduxHooks";
import NovelList from "../../components/NovelList";
import { showToast } from "../../hooks/showToast";
import ErrorView from "../../components/ErrorView";
import { useDispatch } from "react-redux";
import { setAppSettings } from "../../redux/settings/settings.actions";
import { useTheme, useLibrary, useSettings } from "../../hooks/reduxHooks";
import { setAppSettings } from "../../redux/settings/settings.actions";
import { getSource } from "../../sources/sources";
import { showToast } from "../../hooks/showToast";
import { Searchbar } from "../../components/Searchbar/Searchbar";
import { LoadingScreen } from "../../components/LoadingScreen/LoadingScreen";
import NovelCover from "../../components/NovelCover";
import NovelList from "../../components/NovelList";
import ErrorView from "../../components/ErrorView";
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}
+3 -6
View File
@@ -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,11 +87,7 @@ const History = ({ navigation }) => {
]}
/>
<FlatList
contentContainerStyle={{
flexGrow: 1,
paddingVertical: 8,
paddingHorizontal: 16,
}}
contentContainerStyle={{ flexGrow: 1 }}
data={searchText ? searchResults : history}
keyExtractor={(item, index) => item.date}
renderItem={({ item }) => (
@@ -104,6 +100,7 @@ const History = ({ navigation }) => {
style={{
textTransform: "uppercase",
paddingVertical: 8,
paddingHorizontal: 16,
color: theme.textColorSecondary,
}}
>
+12 -13
View File
@@ -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,
+15 -10
View File
@@ -9,7 +9,7 @@ import {
import { useFocusEffect } from "@react-navigation/native";
import { useSelector, useDispatch } from "react-redux";
import { Searchbar } from "../../components/Searchbar";
import { Searchbar } from "../../components/Searchbar/Searchbar";
import NovelList from "../../components/NovelList";
import NovelCover from "../../components/NovelCover";
import EmptyView from "../../components/EmptyView";
@@ -27,11 +27,12 @@ import LibraryFilterSheet from "./components/LibraryFilterSheet";
const LibraryScreen = ({ navigation }) => {
const theme = useTheme();
const dispatch = useDispatch();
const settings = useSettings();
const {
showNumberOfNovels,
incognitoMode = false,
updateLibraryOnLaunch = false,
} = useSettings();
} = settings;
const libraryFilterSheetRef = useRef(null);
@@ -71,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 = () => (
+5 -5
View File
@@ -29,7 +29,7 @@ const AboutScreen = ({ navigation }) => {
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}
+57 -14
View File
@@ -1,6 +1,11 @@
import React, { useEffect } from "react";
import React, { useState } from "react";
import { FlatList, View, Text, StyleSheet } from "react-native";
import { FAB, ProgressBar } from "react-native-paper";
import {
FAB,
ProgressBar,
Appbar as MaterialAppbar,
Menu,
} from "react-native-paper";
import { useDispatch, useSelector } from "react-redux";
import { Appbar } from "../../components/Appbar";
@@ -9,10 +14,14 @@ import EmptyView from "../../components/EmptyView";
import { useTheme } from "../../hooks/reduxHooks";
import { cancelDownload } from "../../redux/downloads/downloads.actions";
import {
cancelDownload,
pauseDownloads,
resumeDownloads,
} from "../../redux/downloads/downloads.actions";
import BackgroundService from "react-native-background-actions";
import { downloadAllChaptersAction } from "../../redux/novel/novel.actions";
import { showToast } from "../../hooks/showToast";
const DownloadQueue = ({ navigation }) => {
const theme = useTheme();
@@ -20,15 +29,41 @@ const DownloadQueue = ({ navigation }) => {
const dispatch = useDispatch();
useEffect(() => {
if (!BackgroundService.isRunning() && downloadQueue.length > 0) {
dispatch(cancelDownload());
}
}, []);
const [visible, setVisible] = useState(false);
const openMenu = () => setVisible(true);
const closeMenu = () => setVisible(false);
const [fab, setFab] = useState(BackgroundService.isRunning());
return (
<ScreenContainer theme={theme}>
<Appbar title="Download queue" onBackAction={navigation.goBack} />
<Appbar title="Download queue" onBackAction={navigation.goBack}>
<Menu
visible={visible}
onDismiss={closeMenu}
anchor={
<MaterialAppbar.Action
icon="dots-vertical"
color={theme.textColorPrimary}
onPress={openMenu}
/>
}
contentStyle={{ backgroundColor: theme.menuColor }}
>
<Menu.Item
onPress={() => {
if (downloadQueue.length > 0) {
dispatch(cancelDownload());
showToast("Downloads cancelled.");
BackgroundService.stop();
}
closeMenu();
}}
title="Cancel downloads"
titleStyle={{ color: theme.textColorPrimary }}
/>
</Menu>
</Appbar>
<FlatList
contentContainerStyle={{ flexGrow: 1 }}
keyExtractor={(item) => item.chapterId.toString()}
@@ -39,7 +74,10 @@ const DownloadQueue = ({ navigation }) => {
{item.chapterName}
</Text>
<ProgressBar
indeterminate={true}
indeterminate={
BackgroundService.isRunning() ? true : false
}
progress={!BackgroundService.isRunning() && 0}
color={theme.colorAccent}
style={{ marginTop: 8 }}
/>
@@ -53,11 +91,16 @@ const DownloadQueue = ({ navigation }) => {
<FAB
style={[styles.fab, { backgroundColor: theme.colorAccent }]}
color={theme.textColorPrimary}
label="Cancel"
label={fab ? "Pause" : "Resume"}
uppercase={false}
small
icon="close"
onPress={() => dispatch(cancelDownload())}
icon={fab ? "pause" : "play"}
onPress={() => {
setFab((prevState) => !prevState);
BackgroundService.isRunning()
? dispatch(pauseDownloads())
: dispatch(resumeDownloads(downloadQueue));
}}
/>
)}
</ScreenContainer>
+159
View File
@@ -0,0 +1,159 @@
import React, { useCallback, useEffect, useState } from "react";
import { FlatList, StyleSheet } from "react-native";
import { Appbar as MaterialAppbar } from "react-native-paper";
import { useDispatch, useSelector } from "react-redux";
import { Appbar } from "../../components/Appbar";
import { ScreenContainer } from "../../components/Common";
import EmptyView from "../../components/EmptyView";
import { LoadingScreen } from "../../components/LoadingScreen/LoadingScreen";
import {
deleteDownloads,
getDownloadedChapters,
} from "../../database/queries/ChapterQueries";
import { useTheme } from "../../hooks/reduxHooks";
import {
deleteChapterAction,
downloadChapterAction,
} from "../../redux/novel/novel.actions";
import UpdatesItem from "../Updates/components/UpdatesItem";
import RemoveDownloadsDialog from "./components/RemoveDownloadsDialog";
const Downloads = ({ navigation }) => {
const theme = useTheme();
const dispatch = useDispatch();
const { downloadQueue } = useSelector((state) => state.downloadsReducer);
const [loading, setLoading] = useState(true);
const [chapters, setChapters] = useState([]);
/**
* Confirm Clear downloads Dialog
*/
const [visible, setVisible] = useState(false);
const showDialog = () => setVisible(true);
const hideDialog = () => setVisible(false);
const getChapters = async () => {
const res = await getDownloadedChapters();
setChapters(res);
setLoading(false);
};
const ListEmptyComponent = () =>
!loading && <EmptyView icon="(˘・_・˘)" description="No downloads" />;
useEffect(() => {
getChapters();
}, []);
const onPress = useCallback(
(item) =>
navigation.navigate("Chapter", {
chapterId: item.chapterId,
chapterUrl: item.chapterUrl,
sourceId: item.sourceId,
novelUrl: item.novelUrl,
chapterName: item.chapterName,
novelId: item.novelId,
novelName: item.novelName,
bookmark: item.bookmark,
}),
[]
);
const onPressCover = useCallback(
(item) =>
navigation.navigate("Novel", {
sourceId: item.sourceId,
novelUrl: item.novelUrl,
novelName: item.novelName,
novelCover: item.novelCover,
}),
[]
);
const downloadChapter = (
sourceId,
novelUrl,
chapterUrl,
chapterName,
chapterId
) =>
dispatch(
downloadChapterAction(
sourceId,
novelUrl,
chapterUrl,
chapterName,
chapterId
)
);
const deleteChapter = (chapterId, chapterName) => {
dispatch(deleteChapterAction(chapterId, chapterName));
setChapters((chapters) =>
chapters.filter((item) => item.chapterId !== chapterId)
);
};
const renderItem = ({ item }) => (
<UpdatesItem
item={item}
theme={theme}
onPress={() => onPress(item)}
onPressCover={() => onPressCover(item)}
downloadQueue={downloadQueue}
deleteChapter={deleteChapter}
downloadChapter={downloadChapter}
/>
);
return (
<ScreenContainer theme={theme}>
<Appbar title="Downloads" onBackAction={navigation.goBack}>
{chapters.length > 0 && (
<MaterialAppbar.Action
icon="delete-sweep"
color={theme.textColorPrimary}
onPress={showDialog}
/>
)}
</Appbar>
{loading ? (
<LoadingScreen theme={theme} />
) : (
<FlatList
contentContainerStyle={styles.flatList}
data={chapters}
keyExtractor={(item) => item.chapterId.toString()}
renderItem={renderItem}
ListEmptyComponent={<ListEmptyComponent />}
/>
)}
<RemoveDownloadsDialog
dialogVisible={visible}
hideDialog={hideDialog}
onSubmit={() => {
deleteDownloads();
setChapters([]);
hideDialog();
}}
theme={theme}
/>
</ScreenContainer>
);
};
export default Downloads;
const styles = StyleSheet.create({
container: { flex: 1 },
flatList: { flexGrow: 1, paddingVertical: 8 },
});
+59 -3
View File
@@ -16,7 +16,9 @@ import GridSizeModal from "./components/GridSizeModal";
import ThemeModal from "./components/ThemeModal";
import {
setAccentColor,
setAmoledMode,
setAppSettings,
setRippleColor,
} from "../../redux/settings/settings.actions";
import ColorPickerModal from "../../components/ColorPickerModal";
import SwitchSetting from "../../components/Switch/Switch";
@@ -29,7 +31,8 @@ const GenralSettings = ({ navigation }) => {
(state) => state.settingsReducer
);
const { updateLibraryOnLaunch = false } = useSettings();
const { updateLibraryOnLaunch = false, downloadNewChapters = false } =
useSettings();
const displayModeLabel = (displayMode) => {
const label = {
@@ -69,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
@@ -101,6 +122,20 @@ const GenralSettings = ({ navigation }) => {
}
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"
@@ -116,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}
@@ -142,7 +198,7 @@ const GenralSettings = ({ navigation }) => {
modalVisible={accentColorModal}
hideModal={hideAccentColorModal}
color={theme.colorAccent}
onSubmit={(val) => dispatch(setAccentColor(val))}
onSubmit={onSubmit}
theme={theme}
/>
</ScreenContainer>
+12 -1
View File
@@ -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"
+5 -4
View File
@@ -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,
},
});
@@ -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;
+9 -16
View File
@@ -1,21 +1,18 @@
import React from "react";
import { Pressable, StyleSheet, View, Text } from "react-native";
import { Portal, Modal, Checkbox } from "react-native-paper";
import { setStatusBarStyle } from "expo-status-bar";
import { Portal, Modal } from "react-native-paper";
import { setAppTheme } from "../../../redux/settings/settings.actions";
import ImmersiveMode from "react-native-immersive-mode";
const ThemeModal = ({ themeModalVisible, hidethemeModal, dispatch, theme }) => {
const themes = [
{ id: 1, name: "Light", statusBar: "dark" },
{ id: 7, name: "Strawberry Daiquiri", statusBar: "dark" },
{ id: 2, name: "Dark", statusBar: "light" },
{ id: 3, name: "Midnight Dusk", statusBar: "light" },
{ id: 4, name: "Green Apple", statusBar: "light" },
{ id: 5, name: "Iris Blue", statusBar: "light" },
{ id: 0, name: "AMOLED Dark", statusBar: "light" },
{ id: 6, name: "AMOLED Hot Pink", 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 ThemeItem = ({ item }) => (
@@ -40,11 +37,7 @@ const ThemeModal = ({ themeModalVisible, hidethemeModal, dispatch, theme }) => {
paddingVertical: 12,
justifyContent: "space-between",
}}
onPress={() => {
dispatch(setAppTheme(item.id));
setStatusBarStyle(item.statusBar);
ImmersiveMode.setBarColor(item.colorPrimary);
}}
onPress={() => dispatch(setAppTheme(item.id))}
>
<Text style={{ color: theme.textColorPrimary }}>
{item.name}
+95 -93
View File
@@ -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,9 +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;
@@ -46,7 +47,7 @@ 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([]);
@@ -55,7 +56,6 @@ const Novel = ({ route, navigation }) => {
let flatlistRef = useRef(null);
let chaptersSettingsSheetRef = useRef(null);
let trackerSheetRef = useRef(null);
let chapterActionsSheetRef = useRef(null);
const {
sort = "ORDER BY chapterId ASC",
@@ -90,25 +90,67 @@ 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}
/>
);
@@ -250,7 +292,7 @@ const Novel = ({ route, navigation }) => {
<FlatList
ref={(ref) => (flatlistRef.current = ref)}
data={!loading && chapters}
keyExtractor={(item) => item.chapterId.toString()}
keyExtractor={keyExtractor}
removeClippedSubviews={true}
maxToRenderPerBatch={5}
windowSize={15}
@@ -274,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,
@@ -302,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,
@@ -338,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,
@@ -354,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,
@@ -369,10 +382,10 @@ const Novel = ({ route, navigation }) => {
)
);
setSelected([]);
}}
/>
)}
</View>
},
},
]}
/>
)}
{!loading && (
<Portal>
@@ -401,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,
},
});
+75 -181
View File
@@ -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}
@@ -160,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}
/>
);
};
+19 -10
View File
@@ -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}
@@ -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: {
+68 -19
View File
@@ -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>
-17
View File
@@ -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
)
}
/>
);
}
};
+65 -30
View File
@@ -1,8 +1,22 @@
import React from "react";
import React, { useState } from "react";
import { View, Text, StyleSheet, Pressable } from "react-native";
import FastImage from "react-native-fast-image";
const UpdateCard = ({ item, theme, onPress }) => {
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);
return (
<Pressable
style={styles.updateCard}
@@ -10,33 +24,54 @@ const UpdateCard = ({ item, theme, onPress }) => {
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>
</>
</Pressable>
+3 -16
View File
@@ -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;
};
+60 -41
View File
@@ -6,6 +6,8 @@ import { fetchChapters, fetchNovel } from "../services/Source/source";
import * as Notifications from "expo-notifications";
import BackgroundService from "react-native-background-actions";
import { showToast } from "../hooks/showToast";
import { downloadChapter } from "../database/queries/ChapterQueries";
Notifications.setNotificationHandler({
handleNotification: async () => {
@@ -58,12 +60,12 @@ export const updateNovel = async (sourceId, novelUrl, novelId) => {
"INSERT OR IGNORE INTO updates (chapterId, novelId, updateTime) values (?, ?, (datetime('now','localtime')))",
[insertId, novelId],
(txObj, res) => {
console.log(
"Inserted Chapter Id -> " +
insertId +
" Novel Id " +
novelId
);
// console.log(
// "Inserted Chapter Id -> " +
// insertId +
// " Novel Id " +
// novelId
// );
},
(txObj, error) => console.log("Error ", error)
);
@@ -75,9 +77,16 @@ export const updateNovel = async (sourceId, novelUrl, novelId) => {
});
};
export const updateNovelChapters = async (sourceId, novelUrl, novelId) => {
export const updateNovelChapters = async (
sourceId,
novelUrl,
novelId,
updateSettings
) => {
let chapters = await fetchChapters(sourceId, novelUrl);
const { downloadNewChapters = false } = updateSettings;
db.transaction((tx) => {
chapters.map((chapter) =>
tx.executeSql(
@@ -90,14 +99,14 @@ export const updateNovelChapters = async (sourceId, novelUrl, novelId) => {
],
(txObj, { insertId }) => {
if (insertId !== -1) {
console.log(
"Inserted Chapter " +
chapter.chapterUrl +
" Chapter Name " +
chapter.chapterName +
" Novel Id " +
novelId
);
downloadNewChapters &&
downloadChapter(
sourceId,
novelUrl,
chapter.chapterUrl,
insertId
);
tx.executeSql(
"INSERT OR IGNORE INTO updates (chapterId, novelId, updateTime) values (?, ?, (datetime('now','localtime')))",
[insertId, novelId],
@@ -112,7 +121,7 @@ export const updateNovelChapters = async (sourceId, novelUrl, novelId) => {
});
};
export const updateAllNovels = async () => {
export const updateAllNovels = async (updateSettings) => {
const libraryNovels = await getLibrary();
const options = {
@@ -143,34 +152,44 @@ export const updateAllNovels = async () => {
BackgroundService.isRunning() && i < libraryNovels.length;
i++
) {
if (BackgroundService.isRunning()) {
await updateNovelChapters(
libraryNovels[i].sourceId,
libraryNovels[i].novelUrl,
libraryNovels[i].novelId
);
console.log(libraryNovels[i].novelName + " Updated");
await BackgroundService.updateNotification({
taskTitle: libraryNovels[i].novelName,
taskDesc: i + 1 + "/" + libraryNovels.length,
progressBar: {
max: libraryNovels.length,
value: i + 1,
},
});
if (libraryNovels.length === i + 1) {
resolve();
await BackgroundService.stop();
Notifications.scheduleNotificationAsync({
content: {
title: "Library Updated",
body: libraryNovels.length + " novels updated",
try {
if (BackgroundService.isRunning()) {
await updateNovelChapters(
libraryNovels[i].sourceId,
libraryNovels[i].novelUrl,
libraryNovels[i].novelId,
updateSettings
);
// console.log(libraryNovels[i].novelName + " Updated");
await BackgroundService.updateNotification({
taskTitle: libraryNovels[i].novelName,
taskDesc: i + 1 + "/" + libraryNovels.length,
progressBar: {
max: libraryNovels.length,
value: i + 1,
},
trigger: null,
});
}
if (libraryNovels.length === i + 1) {
resolve();
await BackgroundService.stop();
Notifications.scheduleNotificationAsync({
content: {
title: "Library Updated",
body:
libraryNovels.length +
" novels updated",
},
trigger: null,
});
}
await sleep(taskData.delay);
await sleep(taskData.delay);
}
} catch (error) {
showToast(
libraryNovels[i].novelName + ": " + error.message
);
break;
}
}
});
+191
View File
@@ -0,0 +1,191 @@
import cheerio from "react-native-cheerio";
const baseUrl = "https://m.yushubo.com";
const popularNovels = async (page) => {
const url = `${baseUrl}/all/order/hits_week+desc.html?page=${page}`;
const totalPages = 5846;
const result = await fetch(url, {
headers: {
"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 body = await result.text();
$ = cheerio.load(body);
let novels = [];
$(".clearfix > li").each(function (result) {
const novelUrl =
$(this).find("a").attr("href").replace(".html", "").substring(1) +
"/";
const novelName = $(this).find("a").text();
let novelCover = $(this).find("img").attr("src");
novelCover = novelCover.includes("https://tva1.sinaimg.cn/")
? `${novelCover}`
: `${baseUrl}${novelCover}`;
const novel = {
sourceId: 52,
novelUrl,
novelName,
novelCover,
};
novels.push(novel);
});
return { totalPages, novels };
};
const parseNovelAndChapters = async (novelUrl) => {
const url = `${baseUrl}/${novelUrl.slice(0, -1)}.html`;
const result = await fetch(url, {
headers: {
"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 body = await result.text();
$ = cheerio.load(body);
let novel = {};
novel.sourceId = 52;
novel.sourceName = "Yushubo";
novel.url = url;
novel.novelUrl = novelUrl;
novel.novelName = $("dd > h2").text();
novel.novelCover = $("dt > img").attr("src");
novel.summary = $("div.content > p").text();
let info = [];
$(".info a").each(function (result) {
info.push($(this).text());
});
novel.genre = info[0];
novel.author = info[1];
novel.artist = null;
novel.status = "Unknown";
let chapters = [];
$(".bookshelf-list a").each(function (result) {
let chapterUrl = $(this).attr("href").substring(1);
let chapterName = $(this).attr("title");
let releaseDate = null;
chapters.push({
chapterName,
releaseDate,
chapterUrl,
});
});
novel.chapters = chapters;
return novel;
};
const parseChapter = async (novelUrl, chapterUrl) => {
const url = `${baseUrl}/${chapterUrl}`;
const result = await fetch(url, {
headers: {
"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 body = await result.text();
$ = cheerio.load(body);
const chapterName = $(".read-section h3").text();
let chapterText = "";
$(".read-section p").each(function (result) {
chapterText += `${$(this).text()}\n`;
});
const nextChapter = null;
const prevChapter = null;
const chapter = {
sourceId: 52,
novelUrl,
chapterUrl,
chapterName,
chapterText,
nextChapter,
prevChapter,
};
return chapter;
};
const searchNovels = async (searchTerm) => {
const searchUrl = `${baseUrl}/search.html`;
const result = await fetch(searchUrl, {
method: "post",
body: JSON.stringify({ keyword: searchTerm }),
headers: {
"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",
"Content-Type": "application/json",
},
});
const body = await result.text();
$ = cheerio.load(body);
let novels = [];
$(".clearfix > li").each(function (result) {
let novelUrl =
$(this).find("a").attr("href").replace(".html", "").substring(1) +
"/";
const novelName = $(this).find("a").text();
let novelCover = $(this).find("img").attr("src");
novelCover = novelCover.includes("https://tva1.sinaimg.cn/")
? `${novelCover}`
: `${baseUrl}${novelCover}`;
const novel = {
sourceId: 52,
novelUrl,
novelName,
novelCover,
};
novels.push(novel);
});
return novels;
};
const YushuboScraper = {
popularNovels,
parseNovelAndChapters,
parseChapter,
searchNovels,
};
export default YushuboScraper;
+73 -73
View File
@@ -9,7 +9,7 @@ const baseUrl = "https://comrademao.com/";
const popularNovels = async (page) => {
let url = baseUrl + "novel/page/" + page;
const totalPages = 108;
const totalPages = 111;
const result = await fetch(url);
const body = await result.text();
@@ -17,22 +17,24 @@ const popularNovels = async (page) => {
let novels = [];
$("article.blog-article").each(function (result) {
const novelName = $(this).find("h5 > a").text();
const novelCover = $(this).find("img").attr("src");
$(".newbox")
.find("li")
.each(function () {
const novelName = $(this).find("h3").text();
const novelCover = $(this).find("img").attr("src");
let novelUrl = $(this).find("h5 > a").attr("href");
novelUrl = novelUrl.replace(baseUrl + "novel/", "");
let novelUrl = $(this).find("a").attr("href");
novelUrl = novelUrl.replace(baseUrl + "novel/", "");
const novel = {
sourceId,
novelName,
novelCover,
novelUrl,
};
const novel = {
sourceId,
novelName,
novelCover,
novelUrl,
};
novels.push(novel);
});
novels.push(novel);
});
return { novels, totalPages };
};
@@ -56,45 +58,54 @@ const parseNovelAndChapters = async (novelUrl) => {
novel.novelUrl = novelUrl;
function getNovelName(y) {
return y
.split("-")
.map((part) => {
return part.charAt(0).toUpperCase() + part.slice(1);
})
.join(" ")
.replace("/", "");
}
novel.novelName = $(".booknav2 > h1").text();
novelName = getNovelName(novelUrl);
console.log(novelName);
novel.novelName = novelName;
novel.novelCover = $(".bookimg2 > img").attr("src");
novel.novelCover = $("#thumbnail > img").attr("src");
novel.summary = $("div#Description")
novel.summary = $(".mybox")
.find(".tabsnav > div > div.qustime > span")
.text()
.replace("Description", "")
.trim();
novel.genre = $("div#Genre")
novel.genre = $(".booknav2 > p:nth-child(2)")
.text()
.replace(/Genre:|\s/g, "");
.replace(/Genre(s):|\s/g, "");
novel.status = $("div#Status")
novel.status = $(".booknav2 > p:nth-child(5)")
.text()
.replace(/Status:|\s/g, "");
.replace(/Status:|\s/g, "")
.trim();
novel.author = $("div#Publisher")
novel.author = $(".booknav2 > p:nth-child(4)")
.text()
.replace(/Publisher:|\s/g, "");
.replace(/Publisher:|\s/g, "")
.trim();
let novelChapters = [];
let chapterUrlPrefix = $("tbody > tr")
// $(".qustime")
// .find("li")
// .each(function () {
// const releaseDate = $(this).find("small").text();
// $(this).find("small").remove();
// const chapterName = $(this).find("span").text();
// const chapterUrl = $(this).find("a").attr("href").split("/")[5];
// novelChapters.push({
// chapterName,
// chapterUrl,
// releaseDate,
// });
// });
$("small").remove();
let chapterUrlPrefix = $(".qustime")
.find("li")
.first()
.next()
.find("td > a")
.find("a")
.text()
.split(" ");
@@ -102,10 +113,10 @@ const parseNovelAndChapters = async (novelUrl) => {
chapterUrlPrefix = chapterUrlPrefix.join("-").toLowerCase();
const latestChapter = $("tbody > tr")
let latestChapter = $(".qustime")
.find("li")
.first()
.next()
.find("td > a")
.find("a")
.text()
.replace(/^\D+/g, "");
@@ -122,6 +133,7 @@ const parseNovelAndChapters = async (novelUrl) => {
novelChapters.push(chapter);
}
novel.chapters = novelChapters;
return novel;
@@ -137,31 +149,17 @@ const parseChapter = async (novelUrl, chapterUrl) => {
$ = cheerio.load(body);
let chapterName = $(".entry-title").text();
let chapterName = "";
let chapterText = $(".entry-content").html();
let chapterText = $(".txtnav").html();
chapterText = htmlToText(chapterText);
let nextChapter = null;
if ($(".nav-next").find("a").length) {
nextChapter = $(".nav-next")
.find("a")
.attr("href")
.replace(baseUrl + "mtl/" + novelUrl + "/", "");
}
let prevChapter = null;
if ($(".nav-previous").find("a").length) {
prevChapter = $(".nav-previous")
.find("a")
.attr("href")
.replace(baseUrl + "mtl/" + novelUrl + "/", "");
}
novelUrl = novelUrl + "/";
chapterUrl = chapterUrl + "/";
novelUrl = novelUrl;
chapterUrl = chapterUrl;
const chapter = {
sourceId,
@@ -186,22 +184,24 @@ const searchNovels = async (searchTerm) => {
let novels = [];
$("article.blog-article").each(function (result) {
const novelName = $(this).find("h5 > a").text();
const novelCover = $(this).find("img").attr("src");
$(".newbox")
.find("li")
.each(function () {
const novelName = $(this).find("h3").text();
const novelCover = $(this).find("img").attr("src");
let novelUrl = $(this).find("h5 > a").attr("href");
novelUrl = novelUrl.replace(baseUrl + "novel/", "");
let novelUrl = $(this).find("a").attr("href");
novelUrl = novelUrl.replace(baseUrl + "novel/", "");
const novel = {
sourceId,
novelName,
novelCover,
novelUrl,
};
const novel = {
sourceId,
novelName,
novelCover,
novelUrl,
};
novels.push(novel);
});
novels.push(novel);
});
return novels;
};
+200
View File
@@ -0,0 +1,200 @@
import cheerio from "react-native-cheerio";
import { showToast } from "../../hooks/showToast";
import { htmlToText } from "../helpers/htmlToText";
const baseUrl = "https://lnmtl.com/";
const popularNovels = async (page) => {
const totalPages = 63;
let url = baseUrl + "novel?page=" + page;
const result = await fetch(url);
const body = await result.text();
$ = cheerio.load(body);
let novels = [];
$(".media").each(function (result) {
const novelName = $(this).find("h4").text();
const novelCover = $(this).find("img").attr("src");
let novelUrl = $(this).find("h4 > a").attr("href");
novelUrl = novelUrl.replace(baseUrl + "novel/", "");
const novel = {
sourceId: 37,
novelName,
novelCover,
novelUrl,
};
novels.push(novel);
});
return { totalPages, novels };
};
const parseNovelAndChapters = async (novelUrl) => {
showToast("LNMTL might take around 20-30 seconds.");
const url = baseUrl + "novel/" + novelUrl;
const result = await fetch(url);
const body = await result.text();
$ = cheerio.load(body);
let novel = {};
novel.sourceId = 37;
novel.sourceName = "LNMTL";
novel.url = url;
novel.novelUrl = novelUrl;
novel.novelName = $(".novel-name").text();
novel.novelCover = $("div.novel").find("img").attr("src");
novel.summary = $("div.description").text().trim();
novel.author = $(
"main > div:nth-child(3) > div > div.col-lg-3.col-md-4 > div:nth-child(2) > div.panel-body > dl:nth-child(1) > dd > span"
).text();
novel.status = $(
"main > div:nth-child(3) > div > div.col-lg-3.col-md-4 > div:nth-child(2) > div.panel-body > dl:nth-child(2) > dd"
)
.text()
.trim();
novel.genre = $(
"main > div.container > div > div.col-lg-3.col-md-4 > div:nth-child(4) > div.panel-body > ul"
)
.text()
.trim()
.replace(/\s\s/g, ",");
let volumes = JSON.parse(
$("main")
.next()
.html()
.match(/lnmtl.volumes = \[(.*?)\]/)[0]
.replace("lnmtl.volumes = ", "")
);
let chapters = [];
volumes = volumes.map((volume) => volume.id);
for (const volume of volumes) {
let volumeData = await fetch(
`https://lnmtl.com/chapter?page=1&volumeId=${volume}`
);
volumeData = await volumeData.json();
// volumeData = volumeData.data.map((volume) => volume.slug);
for (let i = 1; i <= volumeData.last_page; i++) {
let chapterData = await fetch(
`https://lnmtl.com/chapter?page=${i}&volumeId=${volume}`
);
chapterData = await chapterData.json();
chapterData = chapterData.data.map((chapter) => ({
chapterName: `#${chapter.number} ${chapter.title}`,
chapterUrl: chapter.slug,
releaseDate: chapter.created_at,
}));
chapters.push(...chapterData);
}
}
novel.chapters = chapters;
return novel;
};
const parseChapter = async (novelUrl, chapterUrl) => {
const url = `${baseUrl}chapter/${chapterUrl}`;
console.log(url);
const result = await fetch(url);
const body = await result.text();
$ = cheerio.load(body);
let chapterName = $("h3 > span.chapter-title").text().trim();
$(".original").remove();
let chapterText = $(".chapter-body").html();
if (!chapterText) {
chapterText = $(".alert.alert-warning").text();
}
chapterText =
chapterName +
"\n\n" +
htmlToText(chapterText, { removeLineBreaks: false });
let nextChapter = null;
let prevChapter = null;
const chapter = {
sourceId: 37,
novelUrl,
chapterUrl,
chapterName,
chapterText,
nextChapter,
prevChapter,
};
return chapter;
};
const searchNovels = async (searchTerm) => {
const url = "https://lnmtl.com/term";
const result = await fetch(url);
const body = await result.text();
$ = cheerio.load(body);
let novels = $("footer")
.next()
.next()
.html()
.match(/local: \[(.*?)\]/)[0]
.replace("local: ", "");
novels = JSON.parse(novels);
novels = novels.filter((novel) =>
novel.name.toLowerCase().includes(searchTerm.toLowerCase())
);
novels = novels.map((novel) => ({
sourceId: 37,
novelName: novel.name,
novelUrl: novel.slug,
novelCover: novel.image,
}));
return novels;
};
const LNMTLScraper = {
popularNovels,
parseNovelAndChapters,
parseChapter,
searchNovels,
};
export default LNMTLScraper;
+311
View File
@@ -0,0 +1,311 @@
import cheerio from "react-native-cheerio";
import { htmlToText } from "../helpers/htmlToText";
const sourceId = 50;
const sourceName = "Novel Updates";
const baseUrl = "https://www.novelupdates.com/";
let headers = new Headers({
"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 popularNovels = async (page) => {
const totalPages = 100;
let url = `${baseUrl}series-ranking/?rank=week&pg=` + page;
const result = await fetch(url, {
method: "GET",
headers: headers,
});
const body = await result.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 novelUrl = $(this)
.find(".search_title > a")
.attr("href")
.split("/")[4];
const novel = {
sourceId,
novelName,
novelCover,
novelUrl,
};
novels.push(novel);
});
return { totalPages, novels };
};
const parseNovelAndChapters = async (novelUrl) => {
const url = `${baseUrl}series/${novelUrl}`;
const result = await fetch(url, {
method: "GET",
headers: headers,
});
const body = await result.text();
$ = cheerio.load(body);
let novel = {
sourceId,
sourceName,
url,
novelUrl,
};
novel.novelName = $(".seriestitlenu").text();
novel.novelCover = $(".seriesimg > img").attr("src");
novel.author = $("#showauthors").text().trim();
novel.genre = $("#seriesgenre").text().trim().replace(/\s/g, ",");
novel.status = $("#editstatus").text().includes("Ongoing")
? "Ongoing"
: "Completed";
let type = $("#showtype").text().trim();
let summary = $("#editdescription").text().trim();
novel.summary = summary + `\nType: ${type}`;
let novelChapters = [];
const novelId = $("input#mypostid").attr("value");
let formData = new FormData();
formData.append("action", "nd_getchapters");
formData.append("mygrr", 0);
formData.append("mypostid", parseInt(novelId));
const data = await fetch(
"https://www.novelupdates.com/wp-admin/admin-ajax.php",
{
method: "POST",
headers,
body: formData,
}
);
const text = await data.text();
$ = cheerio.load(text);
$("li.sp_li_chp").each(function () {
const chapterName = $(this).text().trim();
const releaseDate = null;
const chapterUrl =
"https:" + $(this).find("a").first().next().attr("href");
novelChapters.push({ chapterName, releaseDate, chapterUrl });
});
novel.chapters = novelChapters.reverse();
return novel;
};
const parseChapter = async (novelUrl, chapterUrl) => {
const url = chapterUrl;
// console.log("Original URL: ", url);
let result, body;
let chapterName = "";
try {
result = await fetch(url, {
method: "GET",
headers: headers,
});
body = await result.text();
// console.log("Redirected URL: ", result.url);
$ = cheerio.load(body);
let isWuxiaWorld = result.url.toLowerCase().includes("wuxiaworld");
let isBlogspot = result.url.toLowerCase().includes("blogspot");
let isTumblr = result.url.toLowerCase().includes("tumblr");
/**
* Checks if its a wwordpress site
*/
let isWordPress =
$('meta[name="generator"]').attr("content") || $("footer").text();
if (isWordPress) {
isWordPress =
isWordPress.toLowerCase().includes("wordpress") ||
isWordPress.includes("Site Kit by Google") ||
$(".powered-by").text().toLowerCase().includes("wordpress") ||
$(".entry-content").html() ||
$("section.content").html()
? true
: false;
}
let isRainOfSnow = result.url.toLowerCase().includes("rainofsnow");
let isWebNovel = result.url.toLowerCase().includes("webnovel");
let isHostedNovel = result.url.toLowerCase().includes("hostednovel");
let isScribbleHub = result.url.toLowerCase().includes("scribblehub");
if (isWuxiaWorld) {
chapterText = $("#chapter-content").html();
} else if (isRainOfSnow) {
chapterText = $("div.content").html();
} else if (isTumblr) {
chapterText = $(".post").html();
} else if (isBlogspot) {
chapterText = $(".entry-content").html();
} else if (isHostedNovel) {
chapterText = $(".chapter").html();
} else if (isScribbleHub) {
chapterText = $("div.chp_raw").html();
} else if (isWordPress) {
/**
* Remove wordpress bloat tags
*/
const bloatClasses = [
".c-ads",
"#madara-comments",
"#comments",
".sharedaddy",
".wp-dark-mode-switcher",
".wp-next-post-navi",
".wp-block-buttons",
".wp-block-columns",
".post-cats",
".sidebar",
".author-avatar",
];
bloatClasses.map((tag) => $(tag).remove());
chapterText =
$(".entry-content").html() ||
$(".single_post").html() ||
$(".post-entry").html() ||
$("article.post").html() ||
$(".content").html() ||
$("#content").html();
} else if (isWebNovel) {
chapterText = $(".cha-words").html();
if (!chapterText) {
chapterText = $("._content").html();
}
} else {
/**
* Remove unnecessary tags
*/
const tags = ["nav", "header", "footer", ".hidden"];
tags.map((tag) => $(tag).remove());
chapterText = $("body").html();
}
if (chapterText) {
/**
* Remove patreon/discord links
*/
chapterText = chapterText.replace(
/<\s*a[^>]*href=['"](https:\/\/(?:www.|patreon|discord).*?)['"][^>]*>([\s\S]*?)<\/\s*a\s*>/gi,
""
);
chapterText = htmlToText(chapterText);
// console.log(chapterText);
} else {
if (!chapterText) {
chapterText =
"Chapter not available.\n\nReport if it's available in webview.";
}
}
} catch (error) {
chapterText = `Chapter not available (Error: ${error.message}).\n\nReport if it's available in webview.`;
}
let nextChapter = null;
let prevChapter = null;
const chapter = {
sourceId,
novelUrl,
chapterUrl,
chapterName,
chapterText,
nextChapter,
prevChapter,
};
return chapter;
};
const searchNovels = async (searchTerm) => {
const url =
"https://www.novelupdates.com/?s=" +
searchTerm +
"&post_type=seriesplans";
const res = await fetch(url, {
method: "GET",
headers: headers,
});
const body = await res.text();
$ = cheerio.load(body);
let 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 novelUrl = $(this)
.find(".search_title > a")
.attr("href")
.split("/")[4];
const novel = {
sourceId,
novelName,
novelCover,
novelUrl,
};
novels.push(novel);
});
return novels;
};
const NovelUpdatesScraper = {
popularNovels,
parseNovelAndChapters,
parseChapter,
searchNovels,
};
export default NovelUpdatesScraper;
+235
View File
@@ -0,0 +1,235 @@
import cheerio from "react-native-cheerio";
import { htmlToText } from "../helpers/htmlToText";
import moment from "moment";
import { showToast } from "../../hooks/showToast";
const sourceId = 51;
const sourceName = "Ranobes";
const baseUrl = "https://ranobes.net/";
const popularNovels = async (page) => {
const totalPages = 152;
let url = `${baseUrl}novels/page/${page}`;
let headers = new Headers({
referer: "https://ranobes.net/",
"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 result = await fetch(url, { method: "GET", headers: headers });
const body = await result.text();
let $ = cheerio.load(body);
let novels = [];
$("article.block.story.shortstory.mod-poster").each(function () {
const novelName = $(this).find("h2.title").text();
let novelCover = $(this).find("figure").attr("style");
let novelUrl = $(this)
.find("a")
.attr("href")
.replace(baseUrl + "novels/", "");
novelCover = novelCover
.match(/background-image: url(.*);/)[1]
.slice(1, -1);
const novel = {
sourceId,
novelName,
novelCover,
novelUrl,
};
novels.push(novel);
});
return { totalPages, novels };
};
const parseNovelAndChapters = async (novelUrl) => {
showToast("Ranobes: Only has latest 25 chapters.");
const url = `${baseUrl}novels/${novelUrl}`;
const result = await fetch(url);
const body = await result.text();
let $ = cheerio.load(body);
let novel = {
sourceId,
sourceName,
url,
novelUrl,
};
novel.novelName = $('meta[property="og:title"]').attr("content");
novel.novelCover = baseUrl + $(".poster > a > img").attr("src");
novel.author = $("dl.author > dd").text();
novel.genre = $(".book-generes").text().trim();
novel.summary = $('div[itemprop="description"]').text().trim();
novel.author = $('span[itemprop="author"]').text();
novel.genre = $('span[itemprop="genre"]').text().replace(", ", ",");
let tempNovelUrl = $(".r-fullstory-chapters-foot")
.find("a")
.next()
.next()
.attr("href")
.split("/")[2];
const chapterListUrl = `${baseUrl}up/${tempNovelUrl}/page/1`;
const chaptersHtml = await fetch(chapterListUrl);
const chapterHtmlToString = await chaptersHtml.text();
$ = cheerio.load(chapterHtmlToString);
// let lastPage = $("div.pages > a").last().text();
let novelChapters = [];
$("#dle-content > div.cat_block.cat_line").each(function () {
const chapterName = $(this).find(".title").text();
let releaseDate = new Date();
let timeAgo = $(this).find("small").text();
timeAgo = timeAgo.match(/\d+/)[0];
if (timeAgo.includes("hours ago")) {
releaseDate.setTime(releaseDate.getTime() - timeAgo);
}
if (timeAgo.includes("months ago")) {
releaseDate.setMonth(releaseDate.getMonth() - timeAgo);
}
if (timeAgo.includes("days ago")) {
releaseDate.setDate(releaseDate.getDate() - timeAgo);
}
releaseDate = moment(releaseDate).format("MM/DD/YY");
const chapterUrl = $(this)
.find("a")
.attr("href")
.replace(baseUrl + "up/", "");
const chapter = {
chapterName,
releaseDate,
chapterUrl,
};
novelChapters.push(chapter);
});
// for (i = 1; i <= lastPage; i++) {
// const chapterPageUrl = `${baseUrl}up/${tempNovelUrl}/page/${i}`;
// const chaptersHtml = await fetch(chapterPageUrl);
// const chapterHtmlToString = await chaptersHtml.text();
// $ = cheerio.load(chapterHtmlToString);
// $("#dle-content > div.cat_block.cat_line").each(function () {
// const chapterName = $(this).find(".title").text();
// let releaseDate = new Date();
// let timeAgo = $(this).find("small").text();
// timeAgo = timeAgo.match(/\d+/)[0];
// if (timeAgo.includes("hours ago")) {
// releaseDate.setTime(releaseDate.getTime() - timeAgo);
// }
// if (timeAgo.includes("months ago")) {
// releaseDate.setMonth(releaseDate.getMonth() - timeAgo);
// }
// if (timeAgo.includes("days ago")) {
// releaseDate.setDate(releaseDate.getDate() - timeAgo);
// }
// releaseDate = moment(releaseDate).format("MM/DD/YY");
// const chapterUrl = $(this)
// .find("a")
// .attr("href")
// .replace(baseUrl + "up/", "");
// const chapter = {
// chapterName,
// releaseDate,
// chapterUrl,
// };
// novelChapters.push(chapter);
// });
// }
novel.chapters = novelChapters.reverse();
return novel;
};
const parseChapter = async (novelUrl, chapterUrl) => {
const url = `${baseUrl}up/${chapterUrl}`;
const result = await fetch(url);
const body = await result.text();
$ = cheerio.load(body);
let chapterName = $("h1.title").text();
let chapterText = $("#arrticle").html();
chapterText = htmlToText(chapterText);
let nextChapter = null;
let prevChapter = null;
const chapter = {
sourceId,
novelUrl,
chapterUrl,
chapterName,
chapterText,
nextChapter,
prevChapter,
};
return chapter;
};
const searchNovels = async (searchTerm) => {
/**
* TODO
*/
showToast("Ranobes: Search does not work");
return [];
};
const RanobesScraper = {
popularNovels,
parseNovelAndChapters,
parseChapter,
searchNovels,
};
export default RanobesScraper;
-8
View File
@@ -143,17 +143,9 @@ const parseChapter = async (novelUrl, chapterUrl) => {
const chapterName = $(".block-title > h1").text().replace(" - ", "");
$(".desc")
.find("hr")
.each(function (result) {
$(this).remove();
});
$(".alert").remove();
$(".hidden").remove();
var re = new RegExp(String.fromCharCode(160), "g");
let chapterText = $(".desc").html();
chapterText = htmlToText(chapterText).replace(
+11 -8
View File
@@ -3,8 +3,14 @@ import { htmlToText } from "../helpers/htmlToText";
const baseUrl = "http://www.tapread.com";
let headers = new Headers({
"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 popularNovels = async (page) => {
let totalPages = 1;
let totalPages = 0;
const result = await fetch(baseUrl);
const body = await result.text();
@@ -18,7 +24,7 @@ const popularNovels = async (page) => {
let novelUrl = $(this).attr("data-id");
const novel = {
sourceId: 17,
extensionId: 17,
novelName,
novelCover,
novelUrl,
@@ -116,12 +122,9 @@ const parseChapter = async (novelUrl, chapterUrl) => {
const chapterName = body.result.chapterName;
const chapterText = htmlToText(body.result.content);
const nextChapter =
body.result.nextChapterId === 0
? null
: body.result.nextChapterId.toString();
const prevChapter = body.result.preChapterId.toString();
novelUrl += "/";
const nextChapter = null;
const prevChapter = null;
const chapter = {
sourceId: 17,
+212
View File
@@ -0,0 +1,212 @@
import cheerio from "react-native-cheerio";
import { htmlToText } from "../helpers/htmlToText";
const sourceId = 48;
const sourceName = "Wuxia.Blog";
const baseUrl = "https://www.wuxia.blog/";
const popularNovels = async (page) => {
const totalPages = 0;
let url = `${baseUrl}listNovels`;
const result = await fetch(url);
const body = await result.text();
$ = cheerio.load(body);
let novels = [];
$("div.panel-body > table")
.find("tr")
.each(function () {
const novelName = $(this).find("td.novel").text();
if (novelName) {
const novelId = $(this).find("td:nth-child(1)").text();
const novelCover = baseUrl + "/data/image/" + novelId + ".jpg";
let novelUrl = $(this)
.find("td.novel > a")
.attr("href")
.replace("/novel/", "");
const novel = {
sourceId,
novelName,
novelCover,
novelUrl,
};
novels.push(novel);
}
});
return { totalPages, novels };
};
const parseNovelAndChapters = async (novelUrl) => {
const url = `${baseUrl}novel/${novelUrl}/`;
const result = await fetch(url);
const body = await result.text();
$ = cheerio.load(body);
let novel = {
sourceId,
sourceName,
url,
novelUrl,
};
novel.novelName = $(
"body > div.container-fluid.text-center > div.row.content > div.col-sm-8.text-left > div:nth-child(6) > div.panel-heading.clearfix > h4"
).text();
novel.novelCover = $('img[itemprop="image"]').attr("src");
novel.author = $(
"div.col-md-6 > div > div:nth-child(2) > div > div:nth-child(2) > a"
).text();
novel.genre = $("div.row > div.col-md-6 > div > a:nth-child(4)").text();
novel.summary = $('div[itemprop="description"]').find("p").text().trim();
let novelChapters = [];
$("table#chplist > #chapters > tr").each(function () {
const chapterName = $(this).find("a").text();
const releaseDate = $(this)
.find("td:nth-child(2)")
.text()
.replace(/-/g, "/");
const chapterUrl = $(this).find("a").attr("href").replace(url, "");
const chapter = {
chapterName,
releaseDate,
chapterUrl,
};
novelChapters.push(chapter);
});
let moreChaptersUrl =
baseUrl +
"temphtml/_tempChapterList_all_" +
$("div#more").attr("data-nid") +
".html";
let moreChapters = await fetch(moreChaptersUrl);
let moreChaptersString = await moreChapters.text();
$ = cheerio.load(moreChaptersString);
$("a").each(function () {
const chapterName = $(this).text();
const releaseDate = $(this)[0]
.nextSibling.nodeValue.replace(/-/g, "/")
.trim();
const chapterUrl = $(this).attr("href").replace(url, "");
const chapter = {
chapterName,
releaseDate,
chapterUrl,
};
novelChapters.push(chapter);
});
novel.chapters = novelChapters.reverse();
return novel;
};
const parseChapter = async (novelUrl, chapterUrl) => {
const url = `${baseUrl}novel/${novelUrl}/${chapterUrl}`;
const result = await fetch(url);
const body = await result.text();
$ = cheerio.load(body);
let chapterName = $("div.panel-body.article").find("h4").text();
$("ul.pager").remove();
let chapterText = $("div.panel-body.article")
.html()
.replace(/<span(.*?)>(.*?)<\/span>/g, "");
chapterText = htmlToText(chapterText);
let nextChapter = null;
let prevChapter = null;
const chapter = {
sourceId,
novelUrl,
chapterUrl,
chapterName,
chapterText,
nextChapter,
prevChapter,
};
return chapter;
};
const searchNovels = async (searchTerm) => {
const url = `${baseUrl}?search=${searchTerm}`;
const result = await fetch(url);
const body = await result.text();
$ = cheerio.load(body);
let novels = [];
$("#table")
.find("tr")
.each(function () {
const novelName = $(this).find("a").text().trim();
if (novelName) {
const novelCover = $(this).find("img").attr("src");
let novelUrl = $(this)
.find("a")
.attr("href")
.replace(baseUrl + "novel/", "");
const novel = {
sourceId,
novelName,
novelCover,
novelUrl,
};
novels.push(novel);
}
});
return novels;
};
const WuxiaBlogScraper = {
popularNovels,
parseNovelAndChapters,
parseChapter,
searchNovels,
};
export default WuxiaBlogScraper;
+180
View File
@@ -0,0 +1,180 @@
import cheerio from "react-native-cheerio";
import { htmlToText } from "../helpers/htmlToText";
import moment from "moment";
const sourceId = 49;
const sourceName = "wuxia.city";
const baseUrl = "https://wuxia.city/";
const popularNovels = async (page) => {
const totalPages = 133;
let url = `${baseUrl}genre/all?page=` + page;
const result = await fetch(url);
const body = await result.text();
$ = cheerio.load(body);
let novels = [];
$(".section-item.card").each(function () {
const novelName = $(this).find(".book-name").text();
const novelCover = $(this).find("img").attr("src");
const novelUrl = $(this).find("a").attr("href").replace("/book/", "");
const novel = {
sourceId,
novelName,
novelCover,
novelUrl,
};
novels.push(novel);
});
return { totalPages, novels };
};
const parseNovelAndChapters = async (novelUrl) => {
const url = `${baseUrl}book/${novelUrl}`;
const result = await fetch(url);
const body = await result.text();
let $ = cheerio.load(body);
let novel = {
sourceId,
sourceName,
url,
novelUrl,
};
novel.novelName = $("h1.book-name").text();
novel.novelCover = $("div.book-img > img").attr("src");
novel.author = $("dl.author > dd").text();
novel.genre = $(".book-generes").text().trim();
novel.summary = $(".book-synopsis").text().replace("Synopsis", "").trim();
const chapterListUrl = `${baseUrl}book/${novelUrl}/chapter-list`;
const chaptersHtml = await fetch(chapterListUrl);
const chapterHtmlToString = await chaptersHtml.text();
$ = cheerio.load(chapterHtmlToString);
let novelChapters = [];
$("ul.chapters > li").each(function () {
const chapterName = $(this)
.find(".chapter-name")
.text()
.trim()
.replace(/\n/, " ");
let releaseDate = new Date();
let timeAgo = $(this).find(".chapter-time").text();
timeAgo = timeAgo.replace("one", 1);
timeAgo = timeAgo.match(/\d+/)[0];
if (timeAgo.includes("months ago") || timeAgo.includes("month ago")) {
releaseDate.setMonth(releaseDate.getMonth() - timeAgo);
}
if (timeAgo.includes("days ago")) {
releaseDate.setDate(releaseDate.getDate() - timeAgo);
}
releaseDate = moment(releaseDate).format("MM/DD/YY");
const chapterUrl = $(this).find("a").attr("href").split("/").pop();
const chapter = {
chapterName,
releaseDate,
chapterUrl,
};
novelChapters.push(chapter);
});
novel.chapters = novelChapters.reverse();
return novel;
};
const parseChapter = async (novelUrl, chapterUrl) => {
const url = `${baseUrl}book/${novelUrl}/${chapterUrl}`;
const result = await fetch(url);
const body = await result.text();
$ = cheerio.load(body);
let chapterName = $(".chapter-name").text();
let chapterText = $(".chapter-content").html();
chapterText = htmlToText(chapterText);
let nextChapter = null;
let prevChapter = null;
const chapter = {
sourceId,
novelUrl,
chapterUrl,
chapterName,
chapterText,
nextChapter,
prevChapter,
};
return chapter;
};
const searchNovels = async (searchTerm) => {
const url = `${baseUrl}search?q=${searchTerm}`;
const result = await fetch(url);
const body = await result.text();
$ = cheerio.load(body);
let novels = [];
$(".section-item").each(function () {
const novelName = $(this).find(".book-name").text().trim();
const novelCover = $(this).find("img").attr("src");
let novelUrl = $(this).find("a").attr("href").replace("/book/", "");
const novel = {
sourceId,
novelName,
novelCover,
novelUrl,
};
novels.push(novel);
});
return novels;
};
const WuxiaCityScraper = {
popularNovels,
parseNovelAndChapters,
parseChapter,
searchNovels,
};
export default WuxiaCityScraper;
+112 -37
View File
@@ -1,40 +1,115 @@
const htmlToText = (html) => {
return (
html
// Remove line breaks
.replace(/(?:\n|\r\n|\r)/gi, "")
// Turn <br>'s into single line breaks.
.replace(/<\s*br[^>]*>/gi, "\n")
// Turn </li>'s into line breaks.
.replace(/<\s*\/li[^>]*>/gi, "\n")
// Turn <p>'s into double line breaks.
.replace(/<\s*p[^>]*>/gi, "\n\n")
// Remove content in script tags.
.replace(/<\s*script[^>]*>[\s\S]*?<\/script>/gim, "")
// Remove content in style tags.
.replace(/<\s*style[^>]*>[\s\S]*?<\/style>/gim, "")
// Remove content in comments.
.replace(/<!--.*?-->/gim, "")
// Format anchor tags properly.
.replace(
/<\s*a[^>]*href=['"](.*?)['"][^>]*>([\s\S]*?)<\/\s*a\s*>/gi,
"$2"
)
.replace(/<\s*img[^>]*src=['"](.*?)['"][^>]*>/gi, "[$1]")
// Remove all remaining tags.
.replace(/(<([^>]+)>)/gi, "")
// Make sure there are never more than two
// consecutive linebreaks.
.replace(/\n{2,}/g, "\n\n")
// Remove tabs.
.replace(/\t/g, "")
// Remove newlines at the beginning of the text.
.replace(/^\n+/m, "")
// Replace multiple spaces with a single space.
.replace(/ {2,}/g, " ")
// Decode HTML entities.
.replace(/&([^;]+);/g, decodeHtmlEntity)
);
const htmlToText = (html, options = {}) => {
const { removeLineBreaks = true } = options;
text = removeLineBreaks && html.replace(/(?:\n|\r\n|\r)/gi, "");
text = html
/**
* <br> -> \n
*/
.replace(/<\s*br[^>]*>/gi, "\n")
/**
* <hr> -> [hr]()
*/
.replace(/<\s*hr[^>]*>/gi, "[hr]()")
/**
* <li> -> \n
*/
.replace(/<\s*\/li[^>]*>/gi, "\n")
/**
* <p> -> \n\n
*/
.replace(/<\s*p[^>]*>/gi, "\n\n")
/**
* Remove script and noscript tags
*/
.replace(/<\s*script[^>]*>[\s\S]*?<\/script>/gim, "")
.replace(/<\s*noscript[^>]*>[\s\S]*?<\/noscript>/gim, "")
/**
* Remove style tags
*/
.replace(/<\s*style[^>]*>[\s\S]*?<\/style>/gim, "")
/**
* Remove comments
*/
.replace(/<!--.*?-->/gim, "")
/**
* <a href='link'>Text</a> -> [Text](link)
*/
.replace(
/<\s*a[^>]*href=['"](http.*?)['"][^>]*>([\s\S]*?)<\/\s*a\s*>/gi,
"[Anchor: $2]($1)"
)
/**
* <em>Text</em> -> _Text_
*/
.replace(/<em>(.*?)<\/em>/gi, "[em]($1)")
/**
* <h1>Text</h1> -> [h1](Text)
*/
.replace(/<(h[1-6])>(.*?)<\/h[1-6]>/gi, "[$1]($2)")
.replace(/<strong>(.*?)<strong>/gi, "[strong]($1)")
/**
* <img> -> [img](src)
*/
.replace(/<\s*img[^>]*src=['"](.*?)['"][^>]*>/gi, "[img]($1)")
/**
* Remove remaining tags
*/
.replace(/(<([^>]+)>)/gi, "")
/**
* Remove tabs.
*/
.replace(/\t/g, "")
/**
* Remove newlines at the beginning of the text.
*/
.replace(/^\n+/m, "")
/**
* Replace multiple spaces with a single space.
*/
.replace(/ {2,}/g, " ")
/**
* Make sure there are never more than two consecutive linebreaks.
*/
.replace(/\n{2,}/g, "\n\n")
/**
* Decode HTML entities.
*/
.replace(/&([^;]+);/g, decodeHtmlEntity);
return text;
};
const decodeHtmlEntity = (m, n) => {
+276
View File
@@ -0,0 +1,276 @@
import cheerio from "react-native-cheerio";
import { htmlToText } from "../helpers/htmlToText";
import moment from "moment";
const baseUrl = "https://syosetu.com"; // base url for syosetu.com
// get given page of search (if pagenum is 0 or >100 (max possible on site) see first page)
const searchUrl = (pagenum, order) => {
return `https://yomou.syosetu.com/search.php?order=${order || "hyoka"}${
!isNaN((pagenum = parseInt(pagenum))) // check if pagenum is a number
? `&p=${pagenum <= 1 || pagenum > 100 ? "1" : pagenum}` // check if pagenum is between 1 and 100
: "" // if isn't don't set ?p
}`;
};
// get the Syosetu ID of the chapter
const getLastPartOfUrl = (url) =>
(ex = /.*(?=\/)(.{2,})$/.exec(url)) ? ex[1].replace(/\//g, "") : null;
// get novelUrl from Syosetu ID
const getNovelUrl = (id) => `https://ncode.syosetu.com/${id}`;
// get chapterUrl from Syosetu ID and chapter ID
const getChapterUrl = (id, chn) =>
`https://ncode.syosetu.com/${id}${chn === "oneshot" ? "" : `/${chn}`}`;
// Because syosetu does not have any covers, I did a replacement for them
const novelCover =
"https://raw.githubusercontent.com/skillgg/lnreader-sources/main/src/jp/syosetu/not_found.png";
// ID of this extension
const sourceId = 36;
// Name of this extension
const sourceName = "Syosetu";
// there are 20 mangas per page
// this is number of pages loaded in the first "batch" of loads
// TOOD: in-app dynamic loading on scroll
const maxPageLoad = 3;
const popularNovels = async (page) => {
const totalPages = 100;
// array of all the novels
let novels = [];
// returns list of novels from given page
let getNovelsFromPage = async (pagenumber) => {
// load page
const result = await fetch(searchUrl(pagenumber || null));
const body = await result.text();
// Cheerio it!
const cheerioQuery = cheerio.load(body, { decodeEntities: false });
let pageNovels = [];
// find class=searchkekka_box
cheerioQuery(".searchkekka_box").each(function (i, e) {
// get div with link and name
const novelDIV = cheerioQuery(this).find(".novel_h");
// get link element
const novelA = novelDIV.children()[0];
// add new novel to array
pageNovels.push({
novelName: novelDIV.text(), // get the name
novelUrl: getLastPartOfUrl(novelA.attribs.href), // get last part of the link
sourceId,
novelCover, // TODO: IDK what to do about covers... On Syo they don't have them
});
});
// return all novels from this page
return pageNovels;
};
novels = await getNovelsFromPage(page);
/** Use
* novels.push(...(await getNovelsFromPage(pageNumber)))
* if you want to load more
*/
// respond with novels!
return { totalPages, novels };
};
const parseNovelAndChapters = async (novelUrl) => {
const url = getNovelUrl(novelUrl);
let chapters = [];
const result = await fetch(url);
const body = await result.text();
const cheerioQuery = cheerio.load(body, { decodeEntities: false });
// create novel object
let novel = {
sourceId,
sourceName,
url: url,
novelUrl,
novelName: cheerioQuery(".novel_title").text(),
author: cheerioQuery(".novel_writername").text().replace("作者:", ""),
novelCover,
};
// Get all the chapters
const cqGetChapters = cheerioQuery(".novel_sublist2");
if (cqGetChapters.length !== 0) {
// has more than 1 chapter
novel.summary = cheerioQuery("#novel_ex")
.text()
.replace(/<\s*br.*?>/g, "\n");
cqGetChapters.each(function (i, e) {
const chapterA = cheerioQuery(this).find("a");
const [chapterName, releaseDate, chapterUrl] = [
// set the variables
chapterA.text(),
cheerioQuery(this)
.find("dt") // get title
.text() // get text
.replace(/(.)/g, "") // remove "(edited)" mark
.trim(), // trim spaces
getLastPartOfUrl(chapterA.attr("href")),
];
chapters.push({ chapterName, releaseDate, chapterUrl });
});
} else {
/**
* Because there are oneshots on the site, they have to be treated with special care
* that's what pisses me off in Shosetsu app. They have this extension,
* but every oneshot is set as "there are no chapters" and all contents are thrown into the description!!
*/
// get summary for oneshot chapter
const result = await fetch(searchUrl() + `&word=${novel.novelName}`);
const body = await result.text();
const summaryQuery = cheerio.load(body, { decodeEntities: false });
const foundText = summaryQuery(".searchkekka_box")
.first()
.find(".ex")
.text()
.replace(/\s{2,}/g, "\n");
novel.summary = foundText;
// add single chapter
chapters.push({
chapterName: novel.novelName,
releaseDate: cheerioQuery("head")
.find("meta[name='WWWC']")
.attr("content"), // get date from metadata
chapterUrl: "oneshot", // set chapterUrl to oneshot so that chapterScraper knows it's a one-shot
});
}
novel.chapters = chapters;
return novel;
};
let parseChapter = async (novelUrl, chapterUrl) => {
const url = getChapterUrl(novelUrl, chapterUrl); // get Url
const result = await fetch(url);
const body = await result.text();
// create cheerioQuery
const cheerioQuery = cheerio.load(body, {
decodeEntities: false,
});
let chapterText = cheerioQuery("#novel_honbun") // get chapter text
.html();
chapterText = htmlToText(chapterText);
// create chapter data structure
let chapter = {
sourceId: 36,
novelUrl,
chapterUrl,
chapterName: "",
chapterText,
nextChapter: null,
prevChapter: null,
};
if (chapterUrl === "oneshot")
// oneshot get name
chapter.chapterName = cheerioQuery("#novel_title").text();
else {
// single chapter
// get name
chapter.chapterName = cheerioQuery(".novel_subtitle").first().text();
// get next/prev buttons
const chapterButtons = cheerioQuery(
"#novel_contents .novel_bn"
).first();
if (chapterButtons.length === 1) {
const button = chapterButtons.find("a");
if (button.text().match(/次/))
chapter.nextChapter = getLastPartOfUrl(button.attr("href"));
else chapter.prevChapter = getLastPartOfUrl(button.attr("href"));
} else {
const firstButton = chapterButtons.find("a").first();
const lastButton = chapterButtons.find("a").last();
chapter.prevChapter = getLastPartOfUrl(firstButton.attribs.href);
chapter.nextChapter = getLastPartOfUrl(lastButton.attribs.href);
}
}
return chapter;
};
let searchNovels = async (searchTerm) => {
// const orderBy = req.query.o;
// array of all the novels
let novels = [];
let isNext = true;
// returns list of novels from given page
let getNovelsFromPage = async (pagenumber) => {
// load page
const result = await fetch(
searchUrl(pagenumber || null, null) + `&word=${searchTerm}`
);
const body = await result.text();
// Cheerio it!
const cheerioQuery = cheerio.load(body, { decodeEntities: false });
if (cheerioQuery(".nextlink").length === 0) isNext = false;
let pageNovels = [];
// find class=searchkekka_box
cheerioQuery(".searchkekka_box").each(function (i, e) {
// get div with link and name
const novelDIV = cheerioQuery(this).find(".novel_h");
// get link element
const novelA = novelDIV.children()[0];
// add new novel to array
pageNovels.push({
novelName: novelDIV.text(), // get the name
novelUrl: getLastPartOfUrl(novelA.attribs.href), // get last part of the link
sourceId,
novelCover, // TODO: IDK what to do about covers... On Syo they don't have them
});
});
// return all novels from this page
return pageNovels;
};
// counter of loaded pages
// let pagesLoaded = 0;
// do {
// // always load first one
// novels.push(...(await getNovelsFromPage(pagesLoaded + 1)));
// pagesLoaded++;
// } while (pagesLoaded < maxPageLoad && isNext); // check if we should load more
novels = await getNovelsFromPage(1);
/** Use
* novels.push(...(await getNovelsFromPage(pageNumber)))
* if you want to load more
*/
// respond with novels!
return novels;
};
const SyosetuScraper = {
popularNovels,
parseNovelAndChapters,
parseChapter,
searchNovels,
};
export default SyosetuScraper;
@@ -0,0 +1,88 @@
import MadaraScraper from "./MadaraScraper";
const getPath = (extensionId) => {
const path = {
38: { novels: "manga", novel: "manga", chapter: "manga" },
39: { novels: "series", novel: "series", chapter: "series" },
40: { novels: "novels", novel: "novel", chapter: "novel" },
41: { novels: "manga", novel: "manga", chapter: "manga" },
42: { novels: "all-novels", novel: "manga", chapter: "manga" },
43: { novels: "series", novel: "series", chapter: "series" },
44: { novels: "series", novel: "series", chapter: "series" },
45: { novels: "novel", novel: "novel", chapter: "novel" },
46: { novels: "novel", novel: "novel", chapter: "novel" },
47: { novels: "novel-list", novel: "novel", chapter: "novel" },
};
return path[extensionId];
};
export const SkyNovelScraper = new MadaraScraper(
38,
"https://skynovel.org/",
"SkyNovel",
getPath(38)
);
export const NovelCakeScraper = new MadaraScraper(
39,
"https://novelcake.com/",
"NovelCake",
getPath(39)
);
export const NovelsRockScraper = new MadaraScraper(
40,
"https://novelsrock.com/",
"NovelsRock",
getPath(40)
);
export const ZinnNovelScraper = new MadaraScraper(
41,
"https://zinnovel.com/",
"ZinnNovel",
getPath(41)
);
export const NovelTranslateScraper = new MadaraScraper(
42,
"https://noveltranslate.com/",
"NovelTranslate",
getPath(42)
);
export const LunarLettersScraper = new MadaraScraper(
43,
"https://www.lunarletters.com/",
"LunarLetters",
getPath(43)
);
export const SleepyTranslationsScraper = new MadaraScraper(
44,
"https://sleepytranslations.com/",
"SleepyTranslations",
getPath(44)
);
export const FreeNovelScraper = new MadaraScraper(
45,
"https://freenovel.me/",
"FreeNovelMe",
getPath(45)
);
export const FirstKissNovelScraper = new MadaraScraper(
46,
"https://1stkissnovel.love/",
"1stKissNovel",
getPath(46)
);
export const DaoNovelScraper = new MadaraScraper(
47,
"https://daonovel.com/",
"DaoNovel",
getPath(47)
);
@@ -0,0 +1,213 @@
import cheerio from "react-native-cheerio";
import { htmlToText } from "../../helpers/htmlToText";
class MadaraScraper {
constructor(sourceId, baseUrl, sourceName, path) {
this.sourceId = sourceId;
this.baseUrl = baseUrl;
this.sourceName = sourceName;
this.path = path;
}
async popularNovels(page) {
const totalPages = 100;
let url =
this.baseUrl +
this.path.novels +
"/page/" +
page +
"/?m_orderby=rating";
let sourceId = this.sourceId;
const result = await fetch(url);
const body = await result.text();
const $ = cheerio.load(body);
let novels = [];
$(".manga-title-badges").remove();
$(".page-item-detail").each(function (result) {
const novelName = $(this).find(".post-title").text().trim();
let image = $(this).find("img");
const novelCover = image.attr("data-src") || image.attr("src");
let novelUrl = $(this)
.find(".post-title")
.find("a")
.attr("href")
.split("/")[4];
const novel = {
sourceId,
novelName,
novelCover,
novelUrl,
};
novels.push(novel);
});
return { totalPages, novels };
}
async parseNovelAndChapters(novelUrl) {
const url = `${this.baseUrl}${this.path.novel}/${novelUrl}/`;
const result = await fetch(url);
const body = await result.text();
let $ = cheerio.load(body);
let novel = {};
novel.sourceId = this.sourceId;
novel.sourceName = this.sourceName;
novel.url = url;
novel.novelUrl = novelUrl;
$(".manga-title-badges").remove();
novel.novelName = $(".post-title > h1").text().trim();
novel.novelCover =
$(".summary_image > a > img").attr("data-src") ||
$(".summary_image > a > img").attr("src");
$(".post-content_item").each(function (result) {
const detailName = $(this)
.find(".summary-heading > h5")
.text()
.trim();
const detail = $(this).find(".summary-content").text().trim();
switch (detailName) {
case "Genre(s)":
novel.genre = detail.replace(/[\t\n]/g, ",");
break;
case "Author(s)":
novel.author = detail;
break;
case "Status":
novel.status = detail;
break;
}
});
novel.summary = $("div.summary__content").text().trim();
let novelChapters = [];
const novelId =
$(".rating-post-id").attr("value") ||
$("#manga-chapters-holder").attr("data-id");
let formData = new FormData();
formData.append("action", "manga_get_chapters");
formData.append("manga", novelId);
const data = await fetch(this.baseUrl + "wp-admin/admin-ajax.php", {
method: "POST",
body: formData,
});
const text = await data.text();
$ = cheerio.load(text);
$(".wp-manga-chapter").each(function (result) {
const chapterName = $(this)
.find("a")
.text()
.replace(/[\t\n]/g, "")
.trim();
const releaseDate = $(this)
.find("span")
.text()
.replace("Free", "")
.trim();
const chapterUrl = $(this).find("a").attr("href").split("/")[5];
novelChapters.push({ chapterName, releaseDate, chapterUrl });
});
novel.chapters = novelChapters.reverse();
return novel;
}
async parseChapter(novelUrl, chapterUrl) {
let sourceId = this.sourceId;
const url = `${this.baseUrl}${this.path.chapter}/${novelUrl}/${chapterUrl}`;
const result = await fetch(url);
const body = await result.text();
const $ = cheerio.load(body);
let chapterName =
$(".text-center").text() || $("#chapter-heading").text();
let chapterText = $(".text-left").html();
chapterText = htmlToText(chapterText);
let nextChapter = null;
let prevChapter = null;
const chapter = {
sourceId,
novelUrl,
chapterUrl,
chapterName,
chapterText,
nextChapter,
prevChapter,
};
return chapter;
}
async searchNovels(searchTerm) {
const url = `${this.baseUrl}?s=${searchTerm}&post_type=wp-manga`;
const result = await fetch(url);
const body = await result.text();
const $ = cheerio.load(body);
let novels = [];
let sourceId = this.sourceId;
$(".c-tabs-item__content").each(function (result) {
const novelName = $(this).find(".post-title").text().trim();
let image = $(this).find("img");
const novelCover = image.attr("data-src") || image.attr("src");
let novelUrl = $(this)
.find(".post-title")
.find("a")
.attr("href")
.split("/")[4];
const novel = {
sourceId,
novelName,
novelCover,
novelUrl,
};
novels.push(novel);
});
return novels;
}
}
export default MadaraScraper;
@@ -0,0 +1,13 @@
import WPMangaStreamScraper from "./WPMangaStreamScraper";
export const KolNovelScraper = new WPMangaStreamScraper(
53,
"https://kolnovel.com/",
"KolNovel"
);
export const RewayatArScraper = new WPMangaStreamScraper(
54,
"https://rewayat-ar.site/",
"RewayatAr"
);
@@ -0,0 +1,174 @@
import cheerio from "react-native-cheerio";
import { htmlToText } from "../../helpers/htmlToText";
class WPMangaStreamScraper {
constructor(sourceId, baseUrl, sourceName) {
this.sourceId = sourceId;
this.baseUrl = baseUrl;
this.sourceName = sourceName;
}
async popularNovels(page) {
let totalPages = 100;
let url = this.baseUrl + "series/?page=" + page + "?m_orderby=popular";
let sourceId = this.sourceId;
const result = await fetch(url);
const body = await result.text();
const $ = cheerio.load(body);
let novels = [];
$("article.bs").each(function () {
const novelName = $(this).find(".ntitle").text().trim();
let image = $(this).find("img");
const novelCover = image.attr("src");
let novelUrl = $(this).find("a").attr("href").split("/")[4];
const novel = {
sourceId,
novelName,
novelCover,
novelUrl,
};
novels.push(novel);
});
return { totalPages, novels };
}
async parseNovelAndChapters(novelUrl) {
const url = `${this.baseUrl}series/${novelUrl}/`;
const result = await fetch(url);
const body = await result.text();
let $ = cheerio.load(body);
let novel = {};
novel.sourceId = this.sourceId;
novel.sourceName = this.sourceName;
novel.url = url;
novel.novelUrl = novelUrl;
novel.novelName = $(".entry-title").text();
novel.novelCover = $("img.wp-post-image").attr("src");
$("div.spe > span").each(function () {
const detailName = $(this).find("b").text().trim();
const detail = $(this).find("b").next().text().trim();
if ($(this).text().includes("الحالة:")) {
novel.status = $(this).text().replace("الحالة: ", "");
}
switch (detailName) {
case "المؤلف:":
novel.auhtor = detail;
break;
}
});
novel.genre = $(".genxed").text().replace(/\s/g, ",");
novel.summary = $('div[itemprop="description"]').text().trim();
let novelChapters = [];
$(".eplister")
.find("li")
.each(function () {
const chapterName =
$(this).find(".epl-num").text() +
" - " +
$(this).find(".epl-title").text();
const releaseDate = $(this).find(".epl-date").text().trim();
const chapterUrl = $(this).find("a").attr("href").split("/")[3];
novelChapters.push({ chapterName, releaseDate, chapterUrl });
});
novel.chapters = novelChapters.reverse();
return novel;
}
async parseChapter(novelUrl, chapterUrl) {
let sourceId = this.sourceId;
const url = `${this.baseUrl}${chapterUrl}`;
const result = await fetch(url);
const body = await result.text();
const $ = cheerio.load(body);
let chapterName = $(".entry-title").text();
let chapterText = $("div.epcontent").text();
if (chapterText) {
chapterText = htmlToText(chapterText);
}
let nextChapter = null;
let prevChapter = null;
const chapter = {
sourceId,
novelUrl,
chapterUrl,
chapterName,
chapterText,
nextChapter,
prevChapter,
};
// console.log(chapter);
return chapter;
}
async searchNovels(searchTerm) {
const url = `${this.baseUrl}?s=${searchTerm}`;
const result = await fetch(url);
const body = await result.text();
const $ = cheerio.load(body);
let novels = [];
let sourceId = this.sourceId;
$("article.bs").each(function () {
const novelName = $(this).find(".ntitle").text().trim();
let image = $(this).find("img");
const novelCover = image.attr("src");
let novelUrl = $(this).find("a").attr("href").split("/")[4];
const novel = {
sourceId,
novelName,
novelCover,
novelUrl,
};
novels.push(novel);
});
return novels;
}
}
module.exports = WPMangaStreamScraper;
+80 -354
View File
@@ -32,360 +32,86 @@ import HasuTlScraper from "./es/hasutl";
import NovelPassionScraper from "./en/novelpassion";
import RoyalRoadScraper from "./en/royalroad";
import ScribbleHubScraper from "./en/scribblehub";
export const sources = [
/**
* English
*/
{
sourceId: 1,
url: "https://boxnovel.com/",
sourceName: "BoxNovel",
icon: "https://github.com/rajarsheechatterjee/lnreader-extensions/blob/main/src/en/boxnovel/icon.png?raw=true",
lang: "English",
},
{
sourceId: 2,
sourceName: "ReadLightNovel",
url: "https://www.readlightnovel.org",
lang: "English",
icon: "https://github.com/rajarsheechatterjee/lnreader-extensions/blob/main/src/en/readlightnovel/icon.png?raw=true",
},
{
sourceId: 3,
url: "https://fastnovel.net/",
sourceName: "FastNovel",
icon: "https://github.com/rajarsheechatterjee/lnreader-extensions/blob/main/src/en/fastnovel/icon.png?raw=true",
lang: "English",
},
{
sourceId: 4,
url: "https://readnovelfull.com/",
sourceName: "ReadNovelFull",
icon: "https://github.com/rajarsheechatterjee/lnreader-extensions/blob/main/src/en/readnovelfull/icon.png?raw=true",
lang: "English",
},
{
sourceId: 5,
url: "https://www.mtlnovel.com/",
sourceName: "MTLNovel",
icon: "https://github.com/rajarsheechatterjee/lnreader-extensions/blob/main/src/en/mtlnovel/icon.png?raw=true",
lang: "English",
},
{
sourceId: 6,
url: "https://www.novelhall.com/",
sourceName: "NovelHall",
icon: "https://github.com/rajarsheechatterjee/lnreader-extensions/blob/main/src/en/novelhall/icon.png?raw=true",
lang: "English",
},
{
sourceId: 7,
url: "https://www.wuxiaworld.com/",
sourceName: "WuxiaWorld",
icon: "https://github.com/rajarsheechatterjee/lnreader-extensions/blob/main/src/en/wuxiaworld/icon.png?raw=true",
lang: "English",
},
{
sourceId: 8,
url: "https://novelfull.com/",
sourceName: "NovelFull",
icon: "https://github.com/rajarsheechatterjee/lnreader-extensions/blob/main/src/en/novelfull/icon.png?raw=true",
lang: "English",
},
{
sourceId: 9,
url: "https://noveltrench.com/",
sourceName: "NovelTrench",
icon: "https://github.com/rajarsheechatterjee/lnreader-extensions/blob/main/src/en/noveltrench/icon.png?raw=true",
lang: "English",
},
{
sourceId: 10,
url: "https://vipnovel.com/",
sourceName: "VipNovel",
icon: "https://github.com/rajarsheechatterjee/lnreader-extensions/blob/main/src/en/vipnovel/icon.png?raw=true",
lang: "English",
},
{
sourceId: 11,
url: "https://kisslightnovels.info/",
sourceName: "KissLightNovels",
icon: "https://github.com/rajarsheechatterjee/lnreader-extensions/blob/main/src/en/kisslightnovels/icon.png?raw=true",
lang: "English",
},
{
sourceId: 12,
url: "https://wuxiaworldsite.co/",
sourceName: "WuxiaWorldSite",
icon: "https://github.com/rajarsheechatterjee/lnreader-extensions/blob/main/src/en/wuxiaworldsite/icon.png?raw=true",
lang: "English",
},
{
sourceId: 13,
url: "https://freewebnovel.com/",
sourceName: "FreeWebNovel",
icon: "https://github.com/rajarsheechatterjee/lnreader-extensions/blob/main/src/en/freewebnovel/icon.png?raw=true",
lang: "English",
},
{
sourceId: 14,
url: "https://jpmtl.com/",
sourceName: "JPMTL",
icon: "https://github.com/rajarsheechatterjee/lnreader-extensions/blob/main/src/en/jpmtl/icon.png?raw=true",
lang: "English",
},
{
sourceId: 15,
url: "https://www.lightnovelpub.com/",
sourceName: "LightNovelPub",
icon: "https://github.com/rajarsheechatterjee/lnreader-extensions/blob/main/src/en/lightnovelpub/icon.png?raw=true",
lang: "English",
},
{
sourceId: 16,
url: "https://www.wuxiaworld.co/",
sourceName: "WuxiaWorld.co",
icon: "https://github.com/rajarsheechatterjee/lnreader-extensions/blob/main/src/en/wuxiaworldco/icon.png?raw=true",
lang: "English",
},
{
sourceId: 17,
url: "http://www.tapread.com/",
sourceName: "TapRead",
icon: "https://github.com/rajarsheechatterjee/lnreader-extensions/blob/main/src/en/tapread/icon.png?raw=true",
lang: "English",
},
{
sourceId: 18,
url: "https://www.novelupdates.cc/",
sourceName: "NovelUpdates.cc",
icon: "https://github.com/rajarsheechatterjee/lnreader-extensions/blob/main/src/en/novelupdatescc/icon.png?raw=true",
lang: "English",
},
{
sourceId: 19,
url: "https://m.readlightnovel.cc/",
sourceName: "ReadLightNovel.cc",
icon: "https://github.com/rajarsheechatterjee/lnreader-extensions/blob/main/src/en/readlightnovelcc/icon.png?raw=true",
lang: "English",
},
{
sourceId: 20,
url: "http://wuxiaworld.cloud/",
sourceName: "WuxiaWorld.cloud",
icon: "https://github.com/rajarsheechatterjee/lnreader-extensions/blob/main/src/en/wuxiaworldcloud/icon.png?raw=true",
lang: "English",
},
{
sourceId: 21,
url: "https://woopread.com/",
sourceName: "WoopRead",
icon: "https://github.com/rajarsheechatterjee/lnreader-extensions/blob/main/src/en/woopread/icon.png?raw=true",
lang: "English",
},
{
sourceId: 22,
url: "https://foxaholic.com/",
sourceName: "Foxaholic",
icon: "https://github.com/rajarsheechatterjee/lnreader-extensions/blob/main/src/en/foxaholic/icon.png?raw=true",
lang: "English",
},
{
sourceId: 27,
url: "https://comrademao.com/",
sourceName: "ComradeMao",
icon: "https://github.com/rajarsheechatterjee/lnreader-extensions/blob/main/src/en/comrademao/icon.png?raw=true",
lang: "English",
},
{
sourceId: 33,
url: "https://www.novelpassion.com/",
sourceName: "NovelPassion",
icon: "https://github.com/rajarsheechatterjee/lnreader-extensions/blob/main/src/en/novelpassion/icon.png?raw=true",
lang: "English",
},
{
sourceId: 34,
url: "https://www.royalroad.com/",
sourceName: "Royal Road",
icon: "https://github.com/rajarsheechatterjee/lnreader-extensions/blob/main/src/en/royalroad/icon.png?raw=true",
lang: "English",
},
/**
* Spanish
*/
{
sourceId: 23,
url: "https://tunovelaligera.com/",
sourceName: "Tunovelaligera",
icon: "https://github.com/rajarsheechatterjee/lnreader-extensions/blob/main/src/es/tunovelaligera/icon.png?raw=true",
lang: "Spanish",
},
{
sourceId: 24,
url: "https://www.skynovels.net/",
sourceName: "SkyNovels",
icon: "https://github.com/rajarsheechatterjee/lnreader-extensions/blob/main/src/es/skynovels/icon.png?raw=true",
lang: "Spanish",
},
{
sourceId: 25,
url: "https://einherjarproject.net/",
sourceName: "EinherjarProject",
icon: "https://github.com/rajarsheechatterjee/lnreader-extensions/blob/main/src/es/einherjarproject/icon.png?raw=true",
lang: "Spanish",
},
{
sourceId: 26,
url: "https://novelasligera.com/",
sourceName: "NovelasLigera",
icon: "https://github.com/rajarsheechatterjee/lnreader-extensions/blob/main/src/es/novelasligera/icon.png?raw=true",
lang: "Spanish",
},
{
sourceId: 28,
url: "https://yuukitls.com/",
sourceName: "YuukiTls",
icon: "https://github.com/rajarsheechatterjee/lnreader-extensions/blob/main/src/es/yuukitls/icon.png?raw=true",
lang: "Spanish",
},
{
sourceId: 29,
url: "https://hasutl.wordpress.com/",
sourceName: "Hasu Translations",
icon: "https://github.com/rajarsheechatterjee/lnreader-extensions/blob/main/src/es/hasutl/icon.png?raw=true",
lang: "Spanish",
},
{
sourceId: 30,
url: "https://oasistranslations.wordpress.com/",
sourceName: "Oasis Translations",
icon: "https://github.com/rajarsheechatterjee/lnreader-extensions/blob/main/src/es/oasistranslations/icon.png?raw=true",
lang: "Spanish",
},
{
sourceId: 31,
url: "http://www.novelawuxia.com/",
sourceName: "Novela Wuxia",
icon: "https://github.com/rajarsheechatterjee/lnreader-extensions/blob/main/src/es/novelawuxia/icon.png?raw=true",
lang: "Spanish",
},
{
sourceId: 35,
url: "https://www.scribblehub.com/",
sourceName: "Scribble Hub",
icon: "https://github.com/rajarsheechatterjee/lnreader-extensions/blob/main/src/en/scribblehub/icon.png?raw=true",
lang: "English",
},
];
import SyosetuScraper from "./jp/syosetu";
import LNMTLScraper from "./en/lnmtl";
import {
DaoNovelScraper,
FirstKissNovelScraper,
FreeNovelScraper,
LunarLettersScraper,
NovelCakeScraper,
NovelsRockScraper,
NovelTranslateScraper,
SkyNovelScraper,
SleepyTranslationsScraper,
ZinnNovelScraper,
} from "./multisrc/madara/MadaraGenerator";
import WuxiaBlogScraper from "./en/wuxiablog";
import WuxiaCityScraper from "./en/wuxiacity";
import NovelUpdatesScraper from "./en/novelupdates";
import RanobesScraper from "./en/ranobes";
import YushuboScraper from "./ch/yushubo";
import {
KolNovelScraper,
RewayatArScraper,
} from "./multisrc/wpmangastream/WPMangaStreamGenerator";
export const getSource = (sourceId) => {
switch (sourceId) {
case 1: {
return BoxNovelScraper;
}
case 2: {
return ReadLightNovelScraper;
}
case 3: {
return fastNovelScraper;
}
case 4: {
return readNovelFullScraper;
}
case 5: {
return mtlNovelScraper;
}
case 6: {
return novelhallScraper;
}
case 7: {
return WuxiaWorldScraper;
}
case 8: {
return novelFullScraper;
}
case 9: {
return novelTrenchScraper;
}
case 10: {
return vipNovelScraper;
}
case 11: {
return kissLightNovelScraper;
}
case 12: {
return WuxiaWorldSiteScraper;
}
case 13: {
return FreeWebNovelScraper;
}
case 14: {
return JPMTLScraper;
}
case 15: {
return lightNovelPubScraper;
}
case 16: {
return WuxiaWorldCoScraper;
}
case 17: {
return tapReadScraper;
}
case 18: {
return novelUpdatesCcScraper;
}
case 19: {
return readLightNovelCcScraper;
}
case 20: {
return WuxiaWorldCloudScraper;
}
case 21: {
return WoopReadScraper;
}
case 22: {
return FoxaHolicScraper;
}
case 23: {
return TuNovelaLigeraScraper;
}
case 24: {
return SkyNovelsScraper;
}
case 25: {
return EinharjarProjectScraper;
}
case 26: {
return NovelasLigeraScraper;
}
case 27: {
return ComradeMaoScraper;
}
case 28: {
return YuukiTlsScraper;
}
case 29: {
return HasuTlScraper;
}
case 30: {
return OasisTranslationsScraper;
}
case 31: {
return NovelaWuxiaScraper;
}
case 33: {
return NovelPassionScraper;
}
case 34: {
return RoyalRoadScraper;
}
case 35: {
return ScribbleHubScraper;
}
}
const scrapers = {
1: BoxNovelScraper,
2: ReadLightNovelScraper,
3: fastNovelScraper,
4: readNovelFullScraper,
5: mtlNovelScraper,
6: novelhallScraper,
7: WuxiaWorldScraper,
8: novelFullScraper,
9: novelTrenchScraper,
10: vipNovelScraper,
11: kissLightNovelScraper,
12: WuxiaWorldSiteScraper,
13: FreeWebNovelScraper,
14: JPMTLScraper,
15: lightNovelPubScraper,
16: WuxiaWorldCoScraper,
17: tapReadScraper,
18: novelUpdatesCcScraper,
19: readLightNovelCcScraper,
20: WuxiaWorldCloudScraper,
21: WoopReadScraper,
22: FoxaHolicScraper,
23: TuNovelaLigeraScraper,
24: SkyNovelsScraper,
25: EinharjarProjectScraper,
26: NovelasLigeraScraper,
27: ComradeMaoScraper,
28: YuukiTlsScraper,
29: HasuTlScraper,
30: OasisTranslationsScraper,
31: NovelaWuxiaScraper,
33: NovelPassionScraper,
34: RoyalRoadScraper,
35: ScribbleHubScraper,
36: SyosetuScraper,
37: LNMTLScraper,
38: SkyNovelScraper,
39: NovelCakeScraper,
40: NovelsRockScraper,
41: ZinnNovelScraper,
42: NovelTranslateScraper,
43: LunarLettersScraper,
44: SleepyTranslationsScraper,
45: FreeNovelScraper,
46: FirstKissNovelScraper,
47: DaoNovelScraper,
48: WuxiaBlogScraper,
49: WuxiaCityScraper,
50: NovelUpdatesScraper,
51: RanobesScraper,
52: YushuboScraper,
53: KolNovelScraper,
54: RewayatArScraper,
};
return scrapers[sourceId];
};
+373
View File
@@ -0,0 +1,373 @@
[
{
"sourceId": 1,
"url": "https://boxnovel.com/",
"sourceName": "BoxNovel",
"icon": "https://github.com/LNReader/lnreader-sources/blob/main/src/en/boxnovel/icon.png?raw=true",
"lang": "English"
},
{
"sourceId": 2,
"sourceName": "ReadLightNovel",
"url": "https://www.readlightnovel.org",
"lang": "English",
"icon": "https://github.com/LNReader/lnreader-sources/blob/main/src/en/readlightnovel/icon.png?raw=true"
},
{
"sourceId": 3,
"url": "https://fastnovel.net/",
"sourceName": "FastNovel",
"icon": "https://github.com/LNReader/lnreader-sources/blob/main/src/en/fastnovel/icon.png?raw=true",
"lang": "English"
},
{
"sourceId": 4,
"url": "https://readnovelfull.com/",
"sourceName": "ReadNovelFull",
"icon": "https://github.com/LNReader/lnreader-sources/blob/main/src/en/readnovelfull/icon.png?raw=true",
"lang": "English"
},
{
"sourceId": 5,
"url": "https://www.mtlnovel.com/",
"sourceName": "MTLNovel",
"icon": "https://github.com/LNReader/lnreader-sources/blob/main/src/en/mtlnovel/icon.png?raw=true",
"lang": "English"
},
{
"sourceId": 6,
"url": "https://www.novelhall.com/",
"sourceName": "NovelHall",
"icon": "https://github.com/LNReader/lnreader-sources/blob/main/src/en/novelhall/icon.png?raw=true",
"lang": "English"
},
{
"sourceId": 7,
"url": "https://www.wuxiaworld.com/",
"sourceName": "WuxiaWorld",
"icon": "https://github.com/LNReader/lnreader-sources/blob/main/src/en/wuxiaworld/icon.png?raw=true",
"lang": "English"
},
{
"sourceId": 8,
"url": "https://novelfull.com/",
"sourceName": "NovelFull",
"icon": "https://github.com/LNReader/lnreader-sources/blob/main/src/en/novelfull/icon.png?raw=true",
"lang": "English"
},
{
"sourceId": 9,
"url": "https://noveltrench.com/",
"sourceName": "NovelTrench",
"icon": "https://github.com/LNReader/lnreader-sources/blob/main/src/en/noveltrench/icon.png?raw=true",
"lang": "English"
},
{
"sourceId": 10,
"url": "https://vipnovel.com/",
"sourceName": "VipNovel",
"icon": "https://github.com/LNReader/lnreader-sources/blob/main/src/en/vipnovel/icon.png?raw=true",
"lang": "English"
},
{
"sourceId": 11,
"url": "https://kisslightnovels.info/",
"sourceName": "KissLightNovels",
"icon": "https://github.com/LNReader/lnreader-sources/blob/main/src/en/kisslightnovels/icon.png?raw=true",
"lang": "English"
},
{
"sourceId": 12,
"url": "https://wuxiaworldsite.co/",
"sourceName": "WuxiaWorldSite",
"icon": "https://github.com/LNReader/lnreader-sources/blob/main/src/en/wuxiaworldsite/icon.png?raw=true",
"lang": "English"
},
{
"sourceId": 13,
"url": "https://freewebnovel.com/",
"sourceName": "FreeWebNovel",
"icon": "https://github.com/LNReader/lnreader-sources/blob/main/src/en/freewebnovel/icon.png?raw=true",
"lang": "English"
},
{
"sourceId": 14,
"url": "https://jpmtl.com/",
"sourceName": "JPMTL",
"icon": "https://github.com/LNReader/lnreader-sources/blob/main/src/en/jpmtl/icon.png?raw=true",
"lang": "English"
},
{
"sourceId": 15,
"url": "https://www.lightnovelpub.com/",
"sourceName": "LightNovelPub",
"icon": "https://github.com/LNReader/lnreader-sources/blob/main/src/en/lightnovelpub/icon.png?raw=true",
"lang": "English"
},
{
"sourceId": 16,
"url": "https://www.wuxiaworld.co/",
"sourceName": "WuxiaWorld.co",
"icon": "https://github.com/LNReader/lnreader-sources/blob/main/src/en/wuxiaworldco/icon.png?raw=true",
"lang": "English"
},
{
"sourceId": 17,
"url": "http://www.tapread.com/",
"sourceName": "TapRead",
"icon": "https://github.com/LNReader/lnreader-sources/blob/main/src/en/tapread/icon.png?raw=true",
"lang": "English"
},
{
"sourceId": 18,
"url": "https://www.novelupdates.cc/",
"sourceName": "NovelUpdates.cc",
"icon": "https://github.com/LNReader/lnreader-sources/blob/main/src/en/novelupdatescc/icon.png?raw=true",
"lang": "English"
},
{
"sourceId": 19,
"url": "https://m.readlightnovel.cc/",
"sourceName": "ReadLightNovel.cc",
"icon": "https://github.com/LNReader/lnreader-sources/blob/main/src/en/readlightnovelcc/icon.png?raw=true",
"lang": "English"
},
{
"sourceId": 20,
"url": "http://wuxiaworld.cloud/",
"sourceName": "WuxiaWorld.cloud",
"icon": "https://github.com/LNReader/lnreader-sources/blob/main/src/en/wuxiaworldcloud/icon.png?raw=true",
"lang": "English"
},
{
"sourceId": 21,
"url": "https://woopread.com/",
"sourceName": "WoopRead",
"icon": "https://github.com/LNReader/lnreader-sources/blob/main/src/en/woopread/icon.png?raw=true",
"lang": "English"
},
{
"sourceId": 22,
"url": "https://foxaholic.com/",
"sourceName": "Foxaholic",
"icon": "https://github.com/LNReader/lnreader-sources/blob/main/src/en/foxaholic/icon.png?raw=true",
"lang": "English"
},
{
"sourceId": 23,
"url": "https://tunovelaligera.com/",
"sourceName": "Tunovelaligera",
"icon": "https://github.com/LNReader/lnreader-sources/blob/main/src/es/tunovelaligera/icon.png?raw=true",
"lang": "Spanish"
},
{
"sourceId": 24,
"url": "https://www.skynovels.net/",
"sourceName": "SkyNovels",
"icon": "https://github.com/LNReader/lnreader-sources/blob/main/src/es/skynovels/icon.png?raw=true",
"lang": "Spanish"
},
{
"sourceId": 25,
"url": "https://einherjarproject.net/",
"sourceName": "EinherjarProject",
"icon": "https://github.com/LNReader/lnreader-sources/blob/main/src/es/einherjarproject/icon.png?raw=true",
"lang": "Spanish"
},
{
"sourceId": 26,
"url": "https://novelasligera.com/",
"sourceName": "NovelasLigera",
"icon": "https://github.com/LNReader/lnreader-sources/blob/main/src/es/novelasligera/icon.png?raw=true",
"lang": "Spanish"
},
{
"sourceId": 27,
"url": "https://comrademao.com/",
"sourceName": "ComradeMao",
"icon": "https://github.com/LNReader/lnreader-sources/blob/main/src/en/comrademao/icon.png?raw=true",
"lang": "English"
},
{
"sourceId": 28,
"url": "https://yuukitls.com/",
"sourceName": "YuukiTls",
"icon": "https://github.com/LNReader/lnreader-sources/blob/main/src/es/yuukitls/icon.png?raw=true",
"lang": "Spanish"
},
{
"sourceId": 29,
"url": "https://hasutl.wordpress.com/",
"sourceName": "Hasu Translations",
"icon": "https://github.com/LNReader/lnreader-sources/blob/main/src/es/hasutl/icon.png?raw=true",
"lang": "Spanish"
},
{
"sourceId": 30,
"url": "https://oasistranslations.wordpress.com/",
"sourceName": "Oasis Translations",
"icon": "https://github.com/LNReader/lnreader-sources/blob/main/src/es/oasistranslations/icon.png?raw=true",
"lang": "Spanish"
},
{
"sourceId": 31,
"url": "http://www.novelawuxia.com/",
"sourceName": "Novela Wuxia",
"icon": "https://github.com/LNReader/lnreader-sources/blob/main/src/es/novelawuxia/icon.png?raw=true",
"lang": "Spanish"
},
{
"sourceId": 33,
"url": "https://www.novelpassion.com/",
"sourceName": "NovelPassion",
"icon": "https://github.com/LNReader/lnreader-sources/blob/main/src/en/novelpassion/icon.png?raw=true",
"lang": "English"
},
{
"sourceId": 34,
"url": "https://www.royalroad.com/",
"sourceName": "Royal Road",
"icon": "https://github.com/LNReader/lnreader-sources/blob/main/src/en/royalroad/icon.png?raw=true",
"lang": "English"
},
{
"sourceId": 35,
"url": "https://www.scribblehub.com/",
"sourceName": "Scribble Hub",
"icon": "https://github.com/LNReader/lnreader-sources/blob/main/src/en/scribblehub/icon.png?raw=true",
"lang": "English"
},
{
"sourceId": 36,
"url": "https://syosetu.com",
"sourceName": "Syosetu",
"icon": "https://github.com/LNReader/lnreader-sources/blob/main/src/jp/syosetu/icon.png?raw=true",
"lang": "Japanese"
},
{
"sourceId": 37,
"url": "https://lnmtl.com/",
"sourceName": "LNMTL",
"icon": "https://github.com/LNReader/lnreader-sources/blob/main/src/en/lnmtl/icon.png?raw=true",
"lang": "English"
},
{
"sourceId": 38,
"sourceUrl": "https://skynovel.org/",
"sourceName": "SkyNovel",
"icon": "https://github.com/LNReader/lnreader-sources/blob/main/multisrc/madara/icons/skynovel.png?raw=true",
"lang": "English"
},
{
"sourceId": 39,
"sourceUrl": "https://novelcake.com/",
"sourceName": "NovelCake",
"icon": "https://github.com/LNReader/lnreader-sources/blob/main/multisrc/madara/icons/novelcake.png?raw=true",
"lang": "English"
},
{
"sourceId": 40,
"sourceUrl": "https://novelsrock.com/",
"sourceName": "NovelsRock",
"icon": "https://github.com/LNReader/lnreader-sources/blob/main/multisrc/madara/icons/novelsrock.png?raw=true",
"lang": "English"
},
{
"sourceId": 41,
"sourceUrl": "https://zinnovel.com/",
"sourceName": "ZinnNovel",
"icon": "https://github.com/LNReader/lnreader-sources/blob/main/multisrc/madara/icons/zinnovel.png?raw=true",
"lang": "English"
},
{
"sourceId": 42,
"sourceUrl": "https://noveltranslate.com/",
"sourceName": "NovelTranslate",
"icon": "https://github.com/LNReader/lnreader-sources/blob/main/multisrc/madara/icons/noveltranslate.png?raw=true",
"lang": "English"
},
{
"sourceId": 43,
"sourceUrl": "https://www.lunarletters.com/",
"sourceName": "LunarLetters",
"icon": "https://github.com/LNReader/lnreader-sources/blob/main/multisrc/madara/icons/lunarletters.png?raw=true",
"lang": "English"
},
{
"sourceId": 44,
"sourceUrl": "https://sleepytranslations.com/",
"sourceName": "SleepyTranslations",
"icon": "https://github.com/LNReader/lnreader-sources/blob/main/multisrc/madara/icons/sleepytranslations.png?raw=true",
"lang": "English"
},
{
"sourceId": 45,
"sourceUrl": "https://freenovel.me/",
"sourceName": "FreeNovelMe",
"icon": "https://github.com/LNReader/lnreader-sources/blob/main/multisrc/madara/icons/freenovel.png?raw=true",
"lang": "English"
},
{
"sourceId": 46,
"sourceUrl": "https://1stkissnovel.love/",
"sourceName": "1stKissNovel",
"icon": "https://github.com/LNReader/lnreader-sources/blob/main/multisrc/madara/icons/1stkissnovel.png?raw=true",
"lang": "English"
},
{
"sourceId": 47,
"sourceUrl": "https://daonovel.com/",
"sourceName": "DaoNovel",
"icon": "https://github.com/LNReader/lnreader-sources/blob/main/multisrc/madara/icons/daonovel.png?raw=true",
"lang": "English"
},
{
"sourceId": 48,
"sourceUrl": "https://www.wuxia.blog/",
"sourceName": "Wuxia.Blog",
"icon": "https://github.com/LNReader/lnreader-sources/blob/main/src/en/wuxiablog/icon.png?raw=true",
"lang": "English"
},
{
"sourceId": 49,
"sourceUrl": "https://wuxia.city/",
"sourceName": "Wuxia.City",
"icon": "https://github.com/LNReader/lnreader-sources/blob/main/src/en/wuxiacity/icon.png?raw=true",
"lang": "English"
},
{
"sourceId": 50,
"sourceUrl": "https://www.novelupdates.com/",
"sourceName": "Novel Updates",
"icon": "https://github.com/LNReader/lnreader-sources/blob/main/src/en/novelupdates/icon.png?raw=true",
"lang": "English"
},
{
"sourceId": 51,
"sourceUrl": "https://ranobes.net/",
"sourceName": "Ranobes",
"icon": "https://github.com/LNReader/lnreader-sources/blob/main/src/en/ranobes/icon.png?raw=true",
"lang": "English"
},
{
"sourceId": 52,
"url": "https://m.yushubo.com",
"sourceName": "Yushubo",
"icon": "https://github.com/LNReader/lnreader-sources/blob/main/src/ch/yushubo/icon.png?raw=true",
"lang": "Chinese"
},
{
"sourceId": 53,
"sourceUrl": "https://kolnovel.com/",
"sourceName": "KolNovel",
"icon": "https://github.com/LNReader/lnreader-sources/blob/main/multisrc/wpmangastream/icons/kolnovel.png?raw=true",
"lang": "Arabic"
},
{
"sourceId": 54,
"sourceUrl": "https://rewayat-ar.site/",
"sourceName": "RewayatAr",
"icon": "https://github.com/LNReader/lnreader-sources/blob/main/multisrc/wpmangastream/icons/rewayatar.png?raw=true",
"lang": "Arabic"
}
]
+67
View File
@@ -0,0 +1,67 @@
const AccentColors = {
default: "#2979FF",
defaultDark: "#3399FF",
midnightDusk: "#F02475",
greenApple: "#48E484",
strawberryDaiquiri: "#ED4A65",
irisBlue: "#00adb5",
hotPink: "#FF3399",
};
const RippleColors = {
default: "rgba(41,121,255,0.12)",
midnightDusk: "rgba(240,36,117,0.12)",
greenApple: "rgba(72,228,132,0.12)",
strawberryDaiquiri: "rgba(237,74,101,0.12)",
irisBlue: "rgba(0,173,181,0.12)",
hotPink: "rgba(255,51,153,0.12)",
};
const StatusBarStyle = {
LIGHT: "light-content",
DARK: "dark-content",
};
const TextColors = {
light: {
textColorPrimary: "#000000",
textColorSecondary: "rgba(0,0,0,0.54)",
textColorHint: "rgba(0,0,0,0.38)",
},
dark: {
textColorPrimary: "#FFFFFF",
textColorSecondary: "rgba(255,255,255,0.7)",
textColorHint: "rgba(255,255,255,0.5)",
},
};
const PrimaryColors = {
light: {
colorPrimary: "#FFFFFF",
colorPrimaryDark: "#FAFAFA",
},
dark: {
colorPrimary: "#242529",
colorPrimaryDark: "#202125",
},
amoled: {
colorPrimary: "#000000",
colorPrimaryDark: "#000000",
},
midnightDusk: {
colorPrimary: "#201F27",
colorPrimaryDark: "#16151D",
},
irisBlue: {
colorPrimary: "#393e46",
colorPrimaryDark: "#222831",
},
};
export {
AccentColors,
RippleColors,
StatusBarStyle,
TextColors,
PrimaryColors,
};
+56 -85
View File
@@ -1,15 +1,10 @@
/**
* Application Colors
*/
export const theme = {
colorPrimary: "#000000",
colorPrimaryDark: "#000000",
colorAccent: "#3399FF",
textColorPrimary: "#FFFFFF",
textColorSecondary: "rgba(255,255,255,0.7)",
textColorHint: "rgba(255,255,255,0.5)",
rippleColor: "rgba(255,255,255,0.06)",
};
import {
AccentColors,
PrimaryColors,
RippleColors,
StatusBarStyle,
TextColors,
} from "./colors";
/**
* Light
@@ -17,20 +12,17 @@ export const theme = {
export const lightTheme = {
id: 1,
name: "Light",
colorPrimary: "#FFFFFF",
colorPrimaryDark: "#FAFAFA",
colorAccent: "#2979FF",
textColorPrimary: "#000000",
textColorSecondary: "rgba(0,0,0,0.54)",
textColorHint: "rgba(0,0,0,0.38)",
rippleColor: "rgba(41,121,255,0.12)",
...PrimaryColors.light,
...TextColors.light,
colorAccent: AccentColors.default,
rippleColor: RippleColors.default,
searchBarColor: "#FFFFFF",
menuColor: "#FFFFFF",
dividerColor: "rgba(0,0,0,0.1)",
filterColor: "#FFC107",
colorDisabled: "rgba(220,220,220,255)",
colorButtonText: "#FFFFFF",
statusBar: "dark",
statusBar: StatusBarStyle.DARK,
};
/**
@@ -39,20 +31,17 @@ export const lightTheme = {
export const darkTheme = {
id: 2,
name: "Dark",
colorPrimary: "#242529",
colorPrimaryDark: "#202125",
colorAccent: "#3399FF",
textColorPrimary: "#FFFFFF",
textColorSecondary: "rgba(255,255,255,0.7)",
textColorHint: "rgba(255,255,255,0.5)",
rippleColor: "rgba(41,121,255,0.12)",
...PrimaryColors.dark,
...TextColors.dark,
colorAccent: AccentColors.defaultDark,
rippleColor: RippleColors.default,
searchBarColor: "#303135",
menuColor: "#242529",
filterColor: "#FFEB3B",
dividerColor: "rgba(255,255,255,0.1)",
colorDisabled: "rgba(57,57,57,255)",
colorButtonText: "#FFFFFF",
statusBar: "light",
statusBar: StatusBarStyle.LIGHT,
};
/**
@@ -61,20 +50,17 @@ export const darkTheme = {
export const amoledDarkTheme = {
id: 0,
name: "AMOLED Dark",
colorPrimary: "#000000",
colorPrimaryDark: "#000000",
colorAccent: "#3399FF",
textColorPrimary: "#FFFFFF",
textColorSecondary: "rgba(255,255,255,0.7)",
textColorHint: "rgba(255,255,255,0.5)",
rippleColor: "rgba(41,121,255,0.12)",
...PrimaryColors.amoled,
colorAccent: AccentColors.defaultDark,
rippleColor: RippleColors.default,
searchBarColor: "#1F1F1F",
menuColor: "#242529",
filterColor: "#FFEB3B",
dividerColor: "rgba(255,255,255,0.15)",
colorDisabled: "rgba(33,33,33,255)",
colorButtonText: "#FFFFFF",
statusBar: "light",
statusBar: StatusBarStyle.LIGHT,
...TextColors.dark,
};
/**
@@ -83,43 +69,37 @@ export const amoledDarkTheme = {
export const midnightDuskTheme = {
id: 3,
name: "Midnight Dusk",
colorPrimary: "#201F27",
colorPrimaryDark: "#16151D",
colorAccent: "#F02475",
textColorPrimary: "#FFFFFF",
textColorSecondary: "rgba(255,255,255,0.7)",
textColorHint: "rgba(255,255,255,0.5)",
rippleColor: "rgba(240,36,117,0.12)",
...PrimaryColors.midnightDusk,
colorAccent: AccentColors.midnightDusk,
rippleColor: RippleColors.midnightDusk,
searchBarColor: "#201F27",
menuColor: "#201F27",
filterColor: "#FFEB3B",
dividerColor: "rgba(255,255,255,0.1)",
colorDisabled: "rgba(57,57,57,255)",
colorButtonText: "#FFFFFF",
statusBar: "light",
statusBar: StatusBarStyle.LIGHT,
...TextColors.dark,
};
/**
* Lime
*/
export const limeTheme = {
export const greenAppleTheme = {
id: 4,
name: "Green Apple",
colorPrimary: "#201F27",
colorPrimaryDark: "#16151D",
colorAccent: "#48E484",
textColorPrimary: "#FFFFFF",
textColorSecondary: "rgba(255,255,255,0.7)",
textColorHint: "rgba(255,255,255,0.5)",
rippleColor: "rgba(72,228,132,0.12)",
searchBarColor: "#201F27",
menuColor: "#201F27",
...PrimaryColors.dark,
...TextColors.dark,
colorAccent: AccentColors.greenApple,
rippleColor: RippleColors.greenApple,
searchBarColor: "#303135",
menuColor: "#242529",
filterColor: "#FFEB3B",
dividerColor: "rgba(255,255,255,0.1)",
colorDisabled: "rgba(57,57,57,255)",
colorButtonText: "#16151D",
statusBar: "light",
colorButtonText: "#000000",
statusBar: StatusBarStyle.LIGHT,
};
/**
@@ -129,20 +109,17 @@ export const limeTheme = {
export const hotPinkTheme = {
id: 6,
name: "AMOLED Hot Pink",
colorPrimary: "#000000",
colorPrimaryDark: "#000000",
colorAccent: "#FF3399",
textColorPrimary: "#FFFFFF",
textColorSecondary: "rgba(255,255,255,0.7)",
textColorHint: "rgba(255,255,255,0.5)",
rippleColor: "rgba(255,51,153,0.12)",
...PrimaryColors.amoled,
...TextColors.dark,
colorAccent: AccentColors.hotPink,
rippleColor: RippleColors.hotPink,
searchBarColor: "#1F1F1F",
menuColor: "#242529",
filterColor: "#FFEB3B",
dividerColor: "rgba(255,255,255,0.15)",
colorDisabled: "rgba(33,33,33,255)",
colorButtonText: "#FFFFFF",
statusBar: "light",
statusBar: StatusBarStyle.LIGHT,
};
/**
@@ -152,37 +129,31 @@ export const hotPinkTheme = {
export const strawberryDaiquiri = {
id: 7,
name: "Strawberry Daiquiri",
colorPrimary: "#FFFFFF",
colorPrimaryDark: "#FAFAFA",
colorAccent: "#ED4A65",
textColorPrimary: "#000000",
textColorSecondary: "rgba(0,0,0,0.54)",
textColorHint: "rgba(0,0,0,0.38)",
rippleColor: "rgba(237,74,101,0.12)",
searchBarColor: "#FFFFFF",
menuColor: "#FFFFFF",
dividerColor: "rgba(0,0,0,0.1)",
filterColor: "#FFC107",
colorDisabled: "rgba(220,220,220,255)",
...PrimaryColors.dark,
colorAccent: AccentColors.strawberryDaiquiri,
rippleColor: RippleColors.strawberryDaiquiri,
searchBarColor: "#303135",
menuColor: "#242529",
filterColor: "#FFEB3B",
dividerColor: "rgba(255,255,255,0.1)",
colorDisabled: "rgba(57,57,57,255)",
colorButtonText: "#FFFFFF",
statusBar: "dark",
statusBar: StatusBarStyle.LIGHT,
...TextColors.dark,
};
export const irisBlueTheme = {
id: 5,
name: "Iris Blue",
colorPrimary: "#393e46",
colorPrimaryDark: "#222831",
colorAccent: "#00adb5",
textColorPrimary: "#FFFFFF",
textColorSecondary: "rgba(255,255,255,0.7)",
textColorHint: "rgba(255,255,255,0.5)",
rippleColor: "rgba(0,173,181,0.12)",
...PrimaryColors.irisBlue,
...TextColors.dark,
colorAccent: AccentColors.irisBlue,
rippleColor: RippleColors.irisBlue,
searchBarColor: "#393e46",
menuColor: "#393e46",
filterColor: "#FFEB3B",
dividerColor: "rgba(255,255,255,0.1)",
colorDisabled: "#393e46",
colorButtonText: "#16151D",
statusBar: "light",
statusBar: StatusBarStyle.LIGHT,
};
+14
View File
@@ -0,0 +1,14 @@
import { StatusBar } from "react-native";
import changeNavigationBarColor from "react-native-navigation-bar-color";
/**
* Sets status and navigation bar color.
*
* @param theme
*/
export const setBarColor = (theme) => {
changeNavigationBarColor(theme.colorPrimary);
StatusBar.setBarStyle(theme.statusBar);
};