Compare commits

...

59 Commits

Author SHA1 Message Date
Rajarshee Chatterjee f732162688 Minor fixes
Lint / ESLint (push) Has been cancelled
2021-12-29 10:53:59 +05:30
Rajarshee Chatterjee 81a5ea0aa7 Add SakuraNovel (Closes LNReader/lnreader-sources#131) 2021-12-29 10:52:40 +05:30
Rajarshee Chatterjee 8be78f7e92 Fix ReadLightNovel 2021-12-29 10:47:32 +05:30
Rajarshee Chatterjee bb144936b5 Fix initial custom css 2021-12-26 15:13:02 +05:30
Rajarshee Chatterjee 724089d940 TravisTranslations: Fix chapter content (Closes LNReader/lnreader-sources#139) 2021-12-26 11:14:31 +05:30
Rajarshee Chatterjee c22440195d MyLoveNovel: Fix description (Closes LNReader/lnreader-sources#137) 2021-12-26 10:48:08 +05:30
Rajarshee Chatterjee dbb20e666f Add ArMTL (Closes LNReader/lnreader-sources#144) 2021-12-26 10:00:28 +05:30
Rajarshee Chatterjee 0a7f2e8948 Add Novelmt.com and Ltnovel.com 2021-12-26 09:37:29 +05:30
Rajarshee Chatterjee a83cdcc66e Add LatestNovel (Closes LNReader/lnreader-sources#136) 2021-12-21 18:13:12 +05:30
Rajarshee Chatterjee ceca91fcab Add option to clear cover cache 2021-12-21 18:03:00 +05:30
Pushpender Singh ecc8c35bfb Keep screen awake in reader (#128)
Closes #121
2021-12-21 16:37:18 +05:30
Pushpender Singh 3522b7e06d Utils for version (#129)
* ESLint Error Fixed

* Utils for version

* Remove useKeepAwake

* Update Main.js

Co-authored-by: Rajarshee Chatterjee <rajarshee.adm@gmail.com>
2021-12-21 16:26:20 +05:30
Pushpender Singh ab4fc42f69 Fix react-native-fast-image crash (#126) 2021-12-20 21:47:57 +05:30
Pushpender Singh 47676a0de5 Fix java.lang.OutOfMemoryError (#125) 2021-12-20 21:21:35 +05:30
Rajarshee Chatterjee 7854b20b1f Fix NovelPub 2021-12-13 11:01:05 +05:30
Rajarshee Chatterjee a825db2965 Add NovelFull.me (Closes LNReader/lnreader-sources#129) 2021-12-13 10:45:56 +05:30
Rajarshee Chatterjee 57cc5a7f42 Rename "Use WebView" to "Render Html" 2021-12-13 10:29:54 +05:30
Rajarshee Chatterjee 15cd0b7b6b Add HizoManga (Closes LNReader/lnreader-sources#126) 2021-12-13 10:20:19 +05:30
Rajarshee Chatterjee ca1303f422 Reorganize reader settings (Closes #110) 2021-11-21 11:44:04 +05:30
Rajarshee Chatterjee e79563f2d0 Fix custom covers 2021-11-17 14:46:18 +05:30
Rajarshee Chatterjee ffe5b04360 Minor fixes 2021-11-17 13:59:13 +05:30
Rajarshee Chatterjee 1c59964a67 Downgrade eslint 2021-11-17 13:29:21 +05:30
Rajarshee Chatterjee 5cd08a0d29 Add more download options (Closes #103) 2021-11-17 12:52:36 +05:30
Rajarshee Chatterjee 4bc61f425b Minor fixes 2021-11-17 12:05:46 +05:30
Rajarshee Chatterjee 6d631b8921 Update dependencies 2021-11-17 12:03:40 +05:30
Rajarshee Chatterjee 70833ef37f Fix NovelForest 2021-11-16 11:49:01 +05:30
Rajarshee Chatterjee cc4c242224 Add clear updates dialog 2021-11-16 11:48:37 +05:30
Rajarshee Chatterjee 35731ec38f Add option to update novel metadata (Closes #105) 2021-11-16 11:28:04 +05:30
Rajarshee Chatterjee 3735e8ca1b Fix OnlyMTL 2021-11-16 10:27:33 +05:30
Rajarshee Chatterjee e0746ff092 Add BestLightNovel (Closes LNReader/lnreader-sources#121) 2021-11-14 11:15:49 +05:30
Rajarshee Chatterjee d0c478b89f Fix RainOfSnow 2021-11-14 11:03:33 +05:30
Rajarshee Chatterjee 3f29aee4e6 Fix RanobeLib (Closes LNReader/lnreader-sources#110) 2021-11-14 10:37:04 +05:30
Rajarshee Chatterjee 76df79d9ca Add NovelPub (Closes LNReader/lnreader-sources#119) 2021-11-14 10:30:01 +05:30
Rajarshee Chatterjee 3f874e6550 Fix default chapter sort order 2021-11-09 17:37:16 +05:30
Rajarshee Chatterjee 84ec20ffb3 Enable react-freeze 2021-11-09 17:36:36 +05:30
Rajarshee Chatterjee 65027ee41e Add RanobeLib (Closes LNReader/lnreader-sources#108) 2021-11-02 10:42:44 +05:30
Rajarshee Chatterjee 3425892a16 Release v1.1.7
Lint / ESLint (push) Has been cancelled
2021-11-01 13:43:21 +05:30
Rajarshee Chatterjee 97890c1596 Add NovelForest (Closes LNReader/lnreader-sources#102) 2021-11-01 08:41:28 +05:30
Rajarshee Chatterjee c97e1b8e23 Fix VipNovel search 2021-11-01 07:43:20 +05:30
Rajarshee Chatterjee c44537a1c0 Fix default sort order 2021-11-01 07:43:10 +05:30
Rajarshee Chatterjee fd8fb367bd Allow setting default sort order for chapters (Closes #100) 2021-10-31 15:17:16 +05:30
Rajarshee Chatterjee a3d8aa5bcd Fix RanobeHub images (Closes LNReader/lnreader-sources#107) 2021-10-31 14:47:18 +05:30
Rajarshee Chatterjee e7a1f8210f Fix Madara sources 2021-10-30 12:23:53 +05:30
Rajarshee Chatterjee 9423054b10 Add OnlyMTL 2021-10-30 11:07:41 +05:30
Rajarshee Chatterjee d11348d9a3 Add parallel searching in global search and migration 2021-10-30 09:59:25 +05:30
Rajarshee Chatterjee 8ec90ae8f7 Fix NitroScans novelName 2021-10-29 16:01:14 +05:30
Rajarshee Chatterjee 5fde669106 Add NitroScans 2021-10-29 15:58:12 +05:30
Rajarshee Chatterjee c6cdf1da0e Add MTLCorner (Closes LNReader/lnreader-sources#63) 2021-10-29 14:52:39 +05:30
Rajarshee Chatterjee 8cb0a5eb73 Fix RanobeHub search (Closes LNReader/lnreader-sources#101) 2021-10-29 13:57:59 +05:30
Rajarshee Chatterjee 09e15607d5 Fix useEffect cleanup in global search and migration 2021-10-29 13:57:05 +05:30
Rajarshee Chatterjee a9d399ea6d Fix crash when genre is an empty string 2021-10-29 13:54:48 +05:30
Rajarshee Chatterjee b9eddc2cc2 Fix Yushubo (Closes LNReader/lnreader-sources#100) 2021-10-29 13:47:46 +05:30
Rajarshee Chatterjee bb68b2f407 Fix crash when chapter release date is empty string 2021-10-28 17:53:52 +05:30
Rajarshee Chatterjee 5e0d19a9c9 Fix crash when chapter release date is empty string 2021-10-28 17:26:28 +05:30
Rajarshee Chatterjee 0aa78e56c8 Fix duplicate entries in library 2021-10-28 17:23:51 +05:30
Rajarshee Chatterjee 078e311995 Fix RewayatAr (Closes LNReader/lnreader-sources#96) 2021-10-28 17:19:52 +05:30
Rajarshee Chatterjee 9a22eba72c Fix next page not loading in browse 2021-10-28 08:34:14 +05:30
Rajarshee Chatterjee 2b0032a4e4 Fix ArNovel (Closes LNReader/lnreader-sources#95) 2021-10-28 08:19:49 +05:30
Rajarshee Chatterjee 708225c855 Add Chireads (Closes LNReader/lnreader-sources#98) 2021-10-28 07:33:03 +05:30
77 changed files with 5206 additions and 2152 deletions
+4
View File
@@ -1,3 +1,7 @@
import {enableFreeze} from 'react-native-screens';
enableFreeze(true);
import React, {useEffect} from 'react';
import {StatusBar} from 'react-native';
+5 -1
View File
@@ -136,7 +136,7 @@ android {
minSdkVersion rootProject.ext.minSdkVersion
targetSdkVersion rootProject.ext.targetSdkVersion
versionCode 1
versionName "1.1.6"
versionName "1.1.7"
}
splits {
abi {
@@ -188,6 +188,10 @@ dependencies {
//noinspection GradleDynamicVersion
implementation "com.facebook.react:react-native:+" // From node_modules
implementation "androidx.swiperefreshlayout:swiperefreshlayout:1.0.0"
implementation 'com.github.bumptech.glide:glide:4.12.0'
annotationProcessor 'com.github.bumptech.glide:compiler:4.12.0'
debugImplementation("com.facebook.flipper:flipper:${FLIPPER_VERSION}") {
exclude group:'com.facebook.fbjni'
}
+10
View File
@@ -8,3 +8,13 @@
# http://developer.android.com/guide/developing/tools/proguard.html
# Add any project specific keep options here:
-keep class com.facebook.hermes.unicode.** { *; }
-keep class com.facebook.jni.** { *; }
-keep public class com.dylanvann.fastimage.* {*;}
-keep public class com.dylanvann.fastimage.** {*;}
-keep public class * implements com.bumptech.glide.module.GlideModule
-keep public class * extends com.bumptech.glide.module.AppGlideModule
-keep public enum com.bumptech.glide.load.ImageHeaderParser$** {
**[] $VALUES;
public *;
}
+3 -2
View File
@@ -22,7 +22,8 @@
<uses-permission android:name="com.google.android.c2dm.permission.RECEIVE"/>
<application
android:name=".MainApplication"
android:name=".MainApplication"
android:largeHeap="true"
android:label="@string/app_name"
android:icon="@mipmap/ic_launcher"
android:roundIcon="@mipmap/ic_launcher_round"
@@ -86,4 +87,4 @@
<service android:name="com.asterinet.react.bgactions.RNBackgroundActionsTask" />
</application>
</manifest>
</manifest>
+1668 -991
View File
File diff suppressed because it is too large Load Diff
+32 -30
View File
@@ -6,23 +6,24 @@
"lint": "eslint ."
},
"dependencies": {
"@react-native-async-storage/async-storage": "^1.13.0",
"@react-native-community/masked-view": "0.1.10",
"@react-native-community/netinfo": "6.0.0",
"@react-native-community/slider": "3.0.3",
"@react-navigation/bottom-tabs": "^5.11.15",
"@react-navigation/material-bottom-tabs": "^5.3.10",
"@react-navigation/native": "^5.8.10",
"@react-navigation/stack": "^5.12.8",
"@react-native-async-storage/async-storage": "^1.15.11",
"@react-native-community/masked-view": "^0.1.11",
"@react-native-community/netinfo": "^7.1.1",
"@react-native-community/slider": "^4.1.12",
"@react-navigation/bottom-tabs": "^6.0.9",
"@react-navigation/material-bottom-tabs": "^6.0.9",
"@react-navigation/native": "^6.0.6",
"@react-navigation/stack": "^6.0.11",
"expo": "^41.0.1",
"expo-document-picker": "^9.1.2",
"expo-document-picker": "^9.2.4",
"expo-file-system": "~11.0.2",
"expo-font": "~9.1.0",
"expo-haptics": "~10.0.0",
"expo-keep-awake": "~9.1.2",
"expo-linear-gradient": "~9.1.0",
"expo-linking": "~2.2.3",
"expo-notifications": "~0.11.6",
"expo-splash-screen": "^0.11.2",
"expo-splash-screen": "^0.11.4",
"expo-sqlite": "~9.1.0",
"expo-updates": "~0.5.4",
"expo-web-browser": "~9.1.0",
@@ -31,35 +32,36 @@
"qs": "^6.10.1",
"react": "16.13.1",
"react-dom": "16.13.1",
"react-native": "^0.64.2",
"react-native-background-actions": "^2.6.1",
"react-native": "^0.64.3",
"react-native-background-actions": "^2.6.5",
"react-native-cheerio": "^1.0.0-rc.4",
"react-native-device-info": "^8.1.3",
"react-native-easing-gradient": "^0.1.2",
"react-native-fast-image": "^8.3.4",
"react-native-device-info": "^8.4.7",
"react-native-easing-gradient": "^1.0.0",
"react-native-fast-image": "^8.5.11",
"react-native-gesture-handler": "~1.10.2",
"react-native-pager-view": "^5.1.10",
"react-native-paper": "^4.5.0",
"react-native-reanimated": "^2.1.0",
"react-native-safe-area-context": "3.2.0",
"react-native-screens": "~3.0.0",
"react-native-pager-view": "^5.4.9",
"react-native-paper": "^4.10.1",
"react-native-reanimated": "^2.2.4",
"react-native-safe-area-context": "^3.2.0",
"react-native-screens": "^3.9.0",
"react-native-swipe-gestures": "^1.0.5",
"react-native-tab-view": "^3.0.1",
"react-native-tab-view": "^3.1.1",
"react-native-tts": "^4.1.0",
"react-native-unimodules": "~0.13.3",
"react-native-vector-icons": "^7.1.0",
"react-native-web": "^0.16.5",
"react-native-webview": "^11.6.2",
"react-redux": "^7.2.2",
"redux": "^4.0.5",
"react-native-unimodules": "^0.13.3",
"react-native-vector-icons": "^9.0.0",
"react-native-web": "^0.17.5",
"react-native-webview": "^11.14.3",
"react-redux": "^7.2.6",
"redux": "^4.1.2",
"redux-persist": "^6.0.0",
"redux-thunk": "^2.3.0",
"redux-thunk": "^2.4.0",
"rn-sliding-up-panel": "^2.4.4"
},
"devDependencies": {
"@babel/core": "~7.9.0",
"@babel/core": "^7.16.0",
"@react-native-community/eslint-config": "^3.0.1",
"eslint": "^7.32.0"
"eslint": "^7.32.0",
"eslint-plugin-react-native": "^3.11.0"
},
"private": true,
"name": "lnreader",
+1 -7
View File
@@ -72,13 +72,7 @@ const InfoItem = ({title, icon, theme}) => (
titleStyle={{color: theme.textColorSecondary, fontSize: 14}}
titleNumberOfLines={5}
left={() =>
icon && (
<PaperList.Icon
color={theme.textColorSecondary}
icon={icon}
style={{marginVertical: 0}}
/>
)
icon && <PaperList.Icon color={theme.textColorSecondary} icon={icon} />
}
/>
);
+1 -1
View File
@@ -29,7 +29,7 @@ const NovelList = ({
}
};
const keyExtractor = useCallback(item => item.novelUrl, []);
const keyExtractor = useCallback(item => item.sourceId + item.novelUrl, []);
return (
<FlatList
+11 -2
View File
@@ -2,7 +2,14 @@ import React from 'react';
import {Pressable, StyleSheet, Text} from 'react-native';
import {RadioButton as PaperRadioButton} from 'react-native-paper';
export const RadioButton = ({label, status, onPress, style, theme}) => (
export const RadioButton = ({
label,
status,
onPress,
style,
labelStyle,
theme,
}) => (
<Pressable
android_ripple={{color: theme.rippleColor}}
style={[styles.pressable, style]}
@@ -14,7 +21,9 @@ export const RadioButton = ({label, status, onPress, style, theme}) => (
color={theme.colorAccent}
uncheckedColor={theme.textColorSecondary}
/>
<Text style={{color: theme.textColorPrimary, marginLeft: 12}}>{label}</Text>
<Text style={[labelStyle, {color: theme.textColorPrimary, marginLeft: 12}]}>
{label}
</Text>
</Pressable>
);
+1 -1
View File
@@ -73,7 +73,7 @@ const styles = StyleSheet.create({
marginTop: StatusBar.currentHeight + 8,
marginBottom: 8,
marginHorizontal: 16,
height: 48,
height: 46,
borderRadius: 50,
overflow: 'hidden',
},
+2 -3
View File
@@ -6,7 +6,7 @@ import {Switch, List} from 'react-native-paper';
const SwitchSetting = ({label, description, onPress, theme, icon, value}) => (
<Pressable
android_ripple={{color: theme.rippleColor}}
style={styles.container}
style={[styles.container, {paddingVertical: description ? 16 : 12}]}
onPress={onPress}
>
<View style={{flexDirection: 'row', flex: 1}}>
@@ -33,7 +33,7 @@ const SwitchSetting = ({label, description, onPress, theme, icon, value}) => (
value={value}
onValueChange={onPress}
color={theme.colorAccent}
style={{marginRight: 8}}
style={{marginLeft: 8}}
/>
</Pressable>
);
@@ -43,7 +43,6 @@ export default SwitchSetting;
const styles = StyleSheet.create({
container: {
paddingHorizontal: 16,
paddingVertical: 12,
flexDirection: 'row',
justifyContent: 'space-between',
},
+4 -4
View File
@@ -245,14 +245,14 @@ export const pickCustomNovelCover = async novelId => {
const image = await DocumentPicker.getDocumentAsync({type: 'image/*'});
if (image.type === 'success' && image.uri) {
const uri = 'file://' + image.uri;
db.transaction(tx => {
tx.executeSql(
'UPDATE novels SET novelCover = ? WHERE novelId = ?',
[image.uri, novelId],
[uri, novelId],
(txObj, res) => {},
(txObj, error) => {
// console.log('Error ', error)
},
(txObj, error) => showToast(`Error: ${error}`),
);
});
+8 -8
View File
@@ -1,6 +1,5 @@
import {useState, useEffect} from 'react';
import {showToast} from './../hooks/showToast';
import {appversion} from '../utils/utils';
export const useGithubUpdateChecker = () => {
const latestReleaseUrl =
@@ -24,20 +23,21 @@ export const useGithubUpdateChecker = () => {
};
const isNewVersion = versionTag => {
let currentVersion = 'v1.1.6';
let currentVersion = `${appversion}`;
const regex = /[^\\d.]/;
let newVersion = versionTag.replace(regex, '');
currentVersion = currentVersion.replace(regex, '');
currentVersion = currentVersion;
return newVersion !== currentVersion;
};
useEffect(() => {
checkForRelease().catch(e => {
showToast(`Could not connect to github:\n${`${e}`.split(':')[1].trim()}`);
// console.error(e);
});
checkForRelease();
// .catch(e => {
// showToast(`Could not connect to github:\n${`${e}`.split(':')[1].trim()}`);
// // console.error(e);
// });
}, []);
if (!checking) {
+1 -3
View File
@@ -2,7 +2,6 @@ import React from 'react';
import {createMaterialBottomTabNavigator} from '@react-navigation/material-bottom-tabs';
import MaterialCommunityIcons from 'react-native-vector-icons/MaterialCommunityIcons';
import {useSafeAreaInsets} from 'react-native-safe-area-context';
import Library from '../screens/library/LibraryScreen';
import Updates from '../screens/updates/UpdatesScreen';
@@ -16,7 +15,6 @@ const Tab = createMaterialBottomTabNavigator();
const BottomNavigator = () => {
const theme = useTheme();
const {bottom} = useSafeAreaInsets();
const {
showHistoryTab = true,
@@ -26,7 +24,7 @@ const BottomNavigator = () => {
return (
<Tab.Navigator
barStyle={{backgroundColor: theme.colorPrimary, paddingBottom: bottom}}
barStyle={{backgroundColor: theme.colorPrimary}}
activeColor={theme.colorAccent}
shifting={!showLabelsInNav}
>
+11 -12
View File
@@ -5,7 +5,6 @@ import {
SORT_FILTER_LIBRARY,
} from './library.types';
import {getLibrary, searchLibrary} from '../../database/queries/LibraryQueries';
import {restoreBackup} from '../../services/backup';
export const getLibraryAction =
(sort, filter) => async (dispatch, getState) => {
@@ -43,17 +42,17 @@ export const filterLibrary = (sort, filter) => async dispatch => {
});
};
export const restoreLibraryAction = () => async dispatch => {
dispatch({type: SET_LIBRARY_LOADING});
// export const restoreLibraryAction = () => async dispatch => {
// dispatch({type: SET_LIBRARY_LOADING});
const res = await restoreBackup();
// const res = await restoreBackup();
if (res) {
const novels = await getLibrary();
// if (res) {
// const novels = await getLibrary();
dispatch({
type: GET_LIBRARY_NOVELS,
payload: novels,
});
}
};
// dispatch({
// type: GET_LIBRARY_NOVELS,
// payload: novels,
// });
// }
// };
+12 -4
View File
@@ -17,7 +17,6 @@ import {
MARK_PREVIOUS_CHAPTERS_UNREAD,
} from './novel.types';
import {updateNovel} from '../../services/updates';
import {fetchNovel} from '../../services/Source/source';
import {
followNovel,
@@ -60,6 +59,7 @@ Notifications.setNotificationHandler({
import BackgroundService from 'react-native-background-actions';
import {SET_DOWNLOAD_QUEUE} from '../downloads/donwloads.types';
import {updateNovel} from '../../services/updates/LibraryUpdateQueries';
export const setNovel = novel => async dispatch => {
dispatch({type: SET_NOVEL, payload: novel});
@@ -113,7 +113,7 @@ export const getNovelAction =
* Get novel from db.
*/
novel = await getNovel(sourceId, novelUrl);
novel.chapters = await getChapters(novel.novelId);
novel.chapters = await getChapters(novel.novelId, sort, filter);
dispatch({
type: GET_NOVEL,
@@ -425,10 +425,18 @@ export const deleteAllChaptersAction = chapters => async dispatch => {
};
export const updateNovelAction =
(sourceId, novelUrl, novelId, sort, filter) => async dispatch => {
(sourceId, novelUrl, novelId, sort, filter) => async (dispatch, getState) => {
dispatch({type: FETCHING_NOVEL});
await updateNovel(sourceId, novelUrl, novelId);
const {downloadNewChapters = false, refreshNovelMetadata = false} =
getState().settingsReducer;
const options = {
downloadNewChapters,
refreshNovelMetadata,
};
await updateNovel(sourceId, novelUrl, novelId, options);
let novel = await getNovel(sourceId, novelUrl);
let chapters = await getChapters(novel.novelId, sort, filter);
+3 -1
View File
@@ -93,12 +93,14 @@ const initialState = {
onlyUpdateOngoingNovels: false,
updateLibraryOnLaunch: false,
downloadNewChapters: false,
refreshNovelMetadata: false,
/**
* Novel settings
*/
hideBackdrop: false,
defaultChapterSort: 'ORDER BY chapterId ASC',
/**
* Reader settings
@@ -122,7 +124,7 @@ const initialState = {
padding: 5,
fontFamily: '',
lineHeight: 1.5,
customCSS: null,
customCSS: '',
},
};
+11 -4
View File
@@ -1,9 +1,9 @@
import {GET_UPDATES, SET_LAST_UPDATE_TIME} from './updates.types';
import {getUpdates} from '../../database/queries/UpdateQueries';
import {updateAllNovels} from '../../services/updates';
import {showToast} from '../../hooks/showToast';
import {updateLibrary} from '../../services/updates/updates';
export const getUpdatesAction = () => async dispatch => {
const updates = await getUpdates();
@@ -38,10 +38,17 @@ export const updateLibraryAction = () => async (dispatch, getState) => {
dispatch({type: SET_LAST_UPDATE_TIME, payload: Date.now()});
const {downloadNewChapters = false, onlyUpdateOngoingNovels = false} =
getState().settingsReducer;
const {
downloadNewChapters = false,
onlyUpdateOngoingNovels = false,
refreshNovelMetadata = false,
} = getState().settingsReducer;
await updateAllNovels({downloadNewChapters, onlyUpdateOngoingNovels});
await updateLibrary({
downloadNewChapters,
onlyUpdateOngoingNovels,
refreshNovelMetadata,
});
dispatch(getUpdatesAction());
};
@@ -89,6 +89,11 @@ const BrowseSettings = ({navigation}) => {
}
/>
</Pressable>
<List.InfoItem
title="Searching a large number of sources may freeze the app till searching is finished."
icon="information-outline"
theme={theme}
/>
<List.Divider theme={theme} />
<List.SubHeader theme={theme}>Browse</List.SubHeader>
<Pressable
+55 -77
View File
@@ -1,9 +1,5 @@
import React, {useEffect, useRef, useState} from 'react';
import {
/* StyleSheet, */ View,
FlatList,
ActivityIndicator,
} from 'react-native';
import {FlatList} from 'react-native';
import {ProgressBar} from 'react-native-paper';
import {useSelector} from 'react-redux';
@@ -14,7 +10,6 @@ import EmptyView from '../../../components/EmptyView';
import {ScreenContainer} from '../../../components/Common';
import GlobalSearchSourceItem from './GlobalSearchSourceItem';
import {showToast} from '../../../hooks/showToast';
import {useLibrary, useSettings, useTheme} from '../../../hooks/reduxHooks';
import {getSource} from '../../../sources/sources';
@@ -38,7 +33,6 @@ const GlobalSearch = ({route, navigation}) => {
);
const {searchAllSources = false} = useSettings();
const [loading, setLoading] = useState(false);
const [searchText, setSearchText] = useState(novelName);
const [searchResults, setSearchResults] = useState([]);
const [progress, setProgress] = useState(0);
@@ -51,58 +45,64 @@ const GlobalSearch = ({route, navigation}) => {
if (novelName) {
onSubmitEditing();
}
// eslint-disable-next-line react-hooks/exhaustive-deps
}, []);
useEffect(() => {
return () => {
isMounted.current = false;
};
// eslint-disable-next-line react-hooks/exhaustive-deps
}, []);
const clearSearchbar = () => setSearchText('');
const onChangeText = text => setSearchText(text);
const onSubmitEditing = async () => {
setSearchResults([]);
setProgress(0);
let globalSearchSources = searchAllSources ? sources : pinnedSources;
for (let i = 0; i < globalSearchSources.length; i++) {
if (isMounted.current === false) {
break;
setSearchResults(
globalSearchSources.map(item => ({
sourceId: item.sourceId,
sourceName: item.sourceName,
lang: item.lang,
loading: true,
novels: [],
error: null,
})),
);
globalSearchSources.map(async item => {
if (isMounted.current === true) {
try {
const source = getSource(item.sourceId);
const data = await source.searchNovels(encodeURI(searchText));
setSearchResults(prevState =>
prevState.map(sourceItem =>
sourceItem.sourceId === item.sourceId
? {...sourceItem, novels: data, loading: false}
: {...sourceItem},
),
);
} catch (e) {
setSearchResults(prevState =>
prevState.map(sourceItem =>
sourceItem.sourceId === item.sourceId
? {
...sourceItem,
loading: false,
error: e.message,
}
: sourceItem,
),
);
}
setProgress(before => before + 1 / globalSearchSources.length);
}
try {
setLoading(true);
const source = getSource(globalSearchSources[i].sourceId);
const data = await source.searchNovels(encodeURI(searchText));
setSearchResults(before => [
...before,
{
sourceId: globalSearchSources[i].sourceId,
sourceName: globalSearchSources[i].sourceName,
lang: globalSearchSources[i].lang,
novels: data,
},
]);
setLoading(false);
} catch (error) {
showToast(globalSearchSources[i].sourceName + ': ' + error.message);
setSearchResults(before => [
...before,
{
sourceId: globalSearchSources[i].sourceId,
sourceName: globalSearchSources[i].sourceName,
lang: globalSearchSources[i].lang,
novels: [],
},
]);
setLoading(false);
}
setProgress(before => before + 1 / globalSearchSources.length);
}
});
};
const renderItem = ({item}) => (
@@ -130,42 +130,22 @@ const GlobalSearch = ({route, navigation}) => {
<ProgressBar color={theme.colorAccent} progress={progress} />
)}
<FlatList
contentContainerStyle={{
flexGrow: 1,
padding: 4,
}}
contentContainerStyle={{flexGrow: 1, padding: 4}}
data={searchResults}
keyExtractor={item => item.sourceId.toString()}
renderItem={renderItem}
extraData={pinned}
ListEmptyComponent={
<>
{!loading && (
<EmptyView
icon="__φ(..)"
description={`Search a novel in ${
searchAllSources
? 'all sources'
: pinned.length === 0
? 'pinned sources\n(No sources pinned)'
: 'pinned sources'
}`}
/>
)}
</>
}
ListFooterComponent={
loading && (
<View
style={{
flex: 1,
justifyContent: 'center',
padding: 16,
}}
>
<ActivityIndicator size="large" color={theme.colorAccent} />
</View>
)
<EmptyView
icon="__φ(..)"
description={`Search a novel in ${
searchAllSources
? 'all sources'
: pinned.length === 0
? 'pinned sources\n(No sources pinned)'
: 'pinned sources'
}`}
/>
}
/>
</ScreenContainer>
@@ -173,5 +153,3 @@ const GlobalSearch = ({route, navigation}) => {
};
export default GlobalSearch;
// const styles = StyleSheet.create({});
@@ -1,5 +1,6 @@
import React from 'react';
import {StyleSheet, View, Text, Image, Pressable} from 'react-native';
import {defaultCoverUri} from '../../../sources/helpers/constants';
const GlobalSearchNovelCover = ({
novel,
@@ -10,32 +11,28 @@ const GlobalSearchNovelCover = ({
}) => {
const {novelName, novelCover} = novel;
const uri =
novelCover && !novelCover.startsWith('/') ? novelCover : defaultCoverUri;
return (
<View style={{flex: 1, borderRadius: 6, overflow: 'hidden'}}>
<View style={styles.container}>
<Pressable
android_ripple={{color: theme.rippleColor}}
style={styles.novelContainer}
style={styles.pressable}
onPress={onPress}
onLongPress={onLongPress}
>
<>
<Image
source={{
uri:
novelCover && !novelCover.startsWith('/')
? novelCover
: 'https://github.com/LNReader/lnreader-sources/blob/main/src/coverNotAvailable.jpg?raw=true',
}}
style={[styles.novelCover, inLibrary && {opacity: 0.5}]}
progressiveRenderingEnabled={true}
/>
<Text
numberOfLines={2}
style={[styles.title, {color: theme.textColorPrimary}]}
>
{novelName}
</Text>
</>
<Image
source={{uri}}
style={[styles.novelCover, inLibrary && {opacity: 0.5}]}
progressiveRenderingEnabled={true}
/>
<Text
numberOfLines={2}
style={[styles.title, {color: theme.textColorPrimary}]}
>
{novelName}
</Text>
</Pressable>
</View>
);
@@ -44,10 +41,16 @@ const GlobalSearchNovelCover = ({
export default GlobalSearchNovelCover;
const styles = StyleSheet.create({
novelContainer: {
container: {
flex: 1,
borderRadius: 6,
overflow: 'hidden',
},
pressable: {
paddingHorizontal: 5,
paddingVertical: 4,
borderRadius: 4,
flex: 1,
},
novelCover: {
height: 150,
@@ -29,7 +29,7 @@ const GlobalSearchNovelList = ({data, theme, library, navigation}) => {
contentContainerStyle={styles.listContainer}
horizontal={true}
data={data}
keyExtractor={item => item.novelUrl}
keyExtractor={item => item.sourceId + item.novelUrl}
renderItem={renderItem}
ListEmptyComponent={ListEmptyComponent}
/>
@@ -1,10 +1,14 @@
import React from 'react';
import {StyleSheet, View, Text} from 'react-native';
import {StyleSheet, View, Text, ActivityIndicator} from 'react-native';
import GlobalSearchNovelList from './GlobalSearchNovelList';
const GlobalSearchSourceItem = ({source, library, theme, navigation}) => {
const {sourceName, lang, novels} = source;
const {sourceName, lang, loading, novels, error} = source;
const colorError = {
color: theme.statusBar === 'dark-content' ? '#B3261E' : '#F2B8B5',
};
return (
<>
@@ -14,12 +18,21 @@ const GlobalSearchSourceItem = ({source, library, theme, navigation}) => {
{lang}
</Text>
</View>
<GlobalSearchNovelList
data={novels}
theme={theme}
library={library}
navigation={navigation}
/>
{error ? (
<Text style={[styles.error, colorError]}>{error}</Text>
) : loading ? (
<ActivityIndicator
color={theme.colorAccent}
style={{marginVertical: 16}}
/>
) : (
<GlobalSearchNovelList
data={novels}
theme={theme}
library={library}
navigation={navigation}
/>
)}
</>
);
};
@@ -31,4 +44,8 @@ const styles = StyleSheet.create({
padding: 8,
paddingVertical: 16,
},
error: {
paddingHorizontal: 8,
paddingVertical: 16,
},
});
@@ -48,7 +48,7 @@ const MigrationNovelList = ({data, theme, library, navigation}) => {
contentContainerStyle={styles.flatListCont}
horizontal={true}
data={data}
keyExtractor={item => item.novelUrl}
keyExtractor={item => item.sourceId + item.novelUrl}
renderItem={renderItem}
ListEmptyComponent={
<Text
+79 -76
View File
@@ -1,10 +1,10 @@
import React, {useEffect, useState} from 'react';
import {
/* StyleSheet, */
View,
Text,
FlatList,
ActivityIndicator,
StyleSheet,
} from 'react-native';
import {ProgressBar} from 'react-native-paper';
import {useLibrary, useSettings, useTheme} from '../../../hooks/reduxHooks';
@@ -14,7 +14,6 @@ import EmptyView from '../../../components/EmptyView';
import MigrationNovelList from './MigrationNovelList';
import {Appbar} from '../../../components/Appbar';
import {showToast} from '../../../hooks/showToast';
import {ScreenContainer} from '../../../components/Common';
import {getSource} from '../../../sources/sources';
@@ -25,7 +24,6 @@ const MigrationNovels = ({navigation, route}) => {
const isMounted = React.useRef(true);
const [progress, setProgress] = useState(0);
const [loading, setLoading] = useState(false);
const [searchResults, setSearchResults] = useState('');
const {searchAllSources = false} = useSettings();
let {
@@ -44,110 +42,110 @@ const MigrationNovels = ({navigation, route}) => {
const getSearchResults = async () => {
let migrationSources = searchAllSources ? sources : pinnedSources;
for (let i = 0; i < migrationSources.length; i++) {
if (isMounted.current === false) {
break;
setSearchResults(
migrationSources.map(item => ({
sourceId: item.sourceId,
sourceName: item.sourceName,
lang: item.lang,
loading: true,
novels: [],
error: null,
})),
);
migrationSources.map(async item => {
if (isMounted.current === true) {
try {
const source = getSource(item.sourceId);
const data = await source.searchNovels(novelName);
setSearchResults(prevState =>
prevState.map(sourceItem =>
sourceItem.sourceId === item.sourceId
? {...sourceItem, novels: data, loading: false}
: {...sourceItem},
),
);
} catch (e) {
setSearchResults(prevState =>
prevState.map(sourceItem =>
sourceItem.sourceId === item.sourceId
? {
...sourceItem,
loading: false,
error: e.message,
}
: sourceItem,
),
);
}
setProgress(before => before + 1 / migrationSources.length);
}
try {
setLoading(true);
const source = getSource(migrationSources[i].sourceId);
const data = await source.searchNovels(novelName);
setSearchResults(before => [
...before,
{
sourceId: migrationSources[i].sourceId,
sourceName: migrationSources[i].sourceName,
sourceLanguage: migrationSources[i].lang,
novels: data,
},
]);
setLoading(false);
} catch (error) {
showToast(error.message);
setSearchResults(before => [
...before,
{
sourceId: migrationSources[i].sourceId,
sourceName: migrationSources[i].sourceName,
sourceLanguage: migrationSources[i].lang,
novels: [],
},
]);
setLoading(false);
}
setProgress(before => before + 1 / migrationSources.length);
}
});
};
useEffect(() => {
getSearchResults();
// eslint-disable-next-line react-hooks/exhaustive-deps
}, []);
useEffect(() => {
return () => {
isMounted.current = false;
};
// eslint-disable-next-line react-hooks/exhaustive-deps
}, []);
const colorError = {
color: theme.statusBar === 'dark-content' ? '#B3261E' : '#F2B8B5',
};
const renderItem = ({item}) => (
<>
<View style={{padding: 8, paddingVertical: 16}}>
<Text style={{color: theme.textColorPrimary}}>{item.sourceName}</Text>
<Text style={{color: theme.textColorSecondary, fontSize: 12}}>
{item.sourceLanguage}
{item.lang}
</Text>
</View>
<MigrationNovelList
data={item.novels}
theme={theme}
library={library}
navigation={navigation}
/>
{item.error ? (
<Text style={[styles.error, colorError]}>{item.error}</Text>
) : item.loading ? (
<ActivityIndicator
color={theme.colorAccent}
style={{marginVertical: 16}}
/>
) : (
<MigrationNovelList
data={item.novels}
theme={theme}
library={library}
navigation={navigation}
/>
)}
</>
);
return (
<ScreenContainer theme={theme}>
<Appbar title={novelName} onBackAction={() => navigation.goBack()} />
<Appbar title={novelName} onBackAction={navigation.goBack} />
{progress > 0 && (
<ProgressBar color={theme.colorAccent} progress={progress} />
)}
<FlatList
contentContainerStyle={{
flexGrow: 1,
padding: 4,
}}
contentContainerStyle={{flexGrow: 1, padding: 4}}
data={searchResults}
keyExtractor={item => item.sourceId.toString()}
renderItem={renderItem}
extraData={pinned}
ListEmptyComponent={
<>
{!loading && (
<EmptyView
icon="__φ(..)"
description={`Search a novel in your pinned sources ${
pinned.length === 0 ? '(No sources pinned)' : ''
}`}
/>
)}
</>
}
ListFooterComponent={
loading && (
<View
style={{
flex: 1,
justifyContent: 'center',
padding: 16,
}}
>
<ActivityIndicator size="large" color={theme.colorAccent} />
</View>
)
<EmptyView
icon="__φ(..)"
description={`Search a novel in your pinned sources ${
pinned.length === 0 ? '(No sources pinned)' : ''
}`}
/>
}
/>
</ScreenContainer>
@@ -156,4 +154,9 @@ const MigrationNovels = ({navigation, route}) => {
export default MigrationNovels;
// const styles = StyleSheet.create({});
const styles = StyleSheet.create({
error: {
paddingHorizontal: 8,
paddingVertical: 16,
},
});
@@ -74,7 +74,7 @@ const LibrarySheet = ({
DESC: 'novels.novelId DESC',
},
{
label: 'Aplhabetically',
label: 'Alphabetically',
ASC: 'novels.novelName ASC',
DESC: 'novels.novelName DESC',
},
+4 -3
View File
@@ -1,4 +1,5 @@
import React from 'react';
import {ScrollView} from 'react-native';
import * as Linking from 'expo-linking';
import {useSelector} from 'react-redux';
@@ -6,7 +7,7 @@ import {useSelector} from 'react-redux';
import {List} from '../../components/List';
import {ScreenContainer} from '../../components/Common';
import {MoreHeader} from './components/MoreHeader';
import {ScrollView} from 'react-native';
import {appversion, releasedt} from '../../utils/utils';
const AboutScreen = ({navigation}) => {
const theme = useSelector(state => state.settingsReducer.theme);
@@ -23,14 +24,14 @@ const AboutScreen = ({navigation}) => {
<List.Section>
<List.Item
title="Version"
description="Stable 1.1.6 (26/10/21 09:00 PM)"
description={`Stable ${appversion} (${releasedt})`}
theme={theme}
/>
<List.Item
title="What's new"
onPress={() =>
Linking.openURL(
'https://github.com/LNReader/lnreader/releases/tag/v1.1.6',
`https://github.com/LNReader/lnreader/releases/tag/v${appversion}`,
)
}
theme={theme}
+97 -6
View File
@@ -56,6 +56,7 @@ import EditInfoModal from './components/EditInfoModal';
import {pickCustomNovelCover} from '../../database/queries/NovelQueries';
import FadeView from '../../components/Common/CrossFadeView';
import {useModal} from '../../hooks/useModal';
import DownloadCustomChapterModal from './components/DownloadCustomChapterModal';
const Novel = ({route, navigation}) => {
const item = route.params;
@@ -77,15 +78,18 @@ const Novel = ({route, navigation}) => {
const deleteDownloadsSnackbar = useModal();
const {
sort = 'ORDER BY chapterId ASC',
useFabForContinueReading = false,
defaultChapterSort = 'ORDER BY chapterId ASC',
} = useSettings();
const {
sort = defaultChapterSort,
filter = '',
showChapterTitles = false,
} = usePreferences(novel.novelId);
let {lastReadChapter, position} = useContinueReading(chapters, novel.novelId);
const {useFabForContinueReading = lastReadChapter && false} = useSettings();
useEffect(() => {
dispatch(
getNovelAction(followed, sourceId, novelUrl, novelId, sort, filter),
@@ -202,7 +206,7 @@ const Novel = ({route, navigation}) => {
}}
numberOfLines={1}
>
{chapter.releaseDate && '• '}
{chapter.releaseDate ? '• ' : null}
{'Progress ' + position[chapter.chapterId].percentage + '%'}
</Text>
);
@@ -242,6 +246,7 @@ const Novel = ({route, navigation}) => {
);
const [editInfoModal, showEditInfoModal] = useState(false);
const downloadCustomChapterModal = useModal();
return (
<Provider>
@@ -347,7 +352,7 @@ const Novel = ({route, navigation}) => {
}}
>
<Menu.Item
title="All"
title="Next chapter"
style={{backgroundColor: theme.menuColor}}
titleStyle={{
color: theme.textColorPrimary,
@@ -357,12 +362,73 @@ const Novel = ({route, navigation}) => {
downloadAllChaptersAction(
novel.sourceId,
novel.novelUrl,
chapters,
[
chapters.find(
chapter =>
!!chapter.read === false &&
!!chapter.downloaded === false,
),
],
),
);
showDownloadMenu(false);
}}
/>
<Menu.Item
title="Next 5 chapter"
style={{backgroundColor: theme.menuColor}}
titleStyle={{
color: theme.textColorPrimary,
}}
onPress={() => {
dispatch(
downloadAllChaptersAction(
novel.sourceId,
novel.novelUrl,
chapters
.filter(
chapter =>
!!chapter.read === false &&
!!chapter.downloaded === false,
)
.slice(0, 5),
),
);
showDownloadMenu(false);
}}
/>
<Menu.Item
title="Next 10 chapter"
style={{backgroundColor: theme.menuColor}}
titleStyle={{
color: theme.textColorPrimary,
}}
onPress={() => {
dispatch(
downloadAllChaptersAction(
novel.sourceId,
novel.novelUrl,
chapters
.filter(
chapter =>
!!chapter.read === false &&
!!chapter.downloaded === false,
)
.slice(0, 10),
),
);
showDownloadMenu(false);
}}
/>
<Menu.Item
title="Custom"
style={{backgroundColor: theme.menuColor}}
titleStyle={{color: theme.textColorPrimary}}
onPress={() => {
downloadCustomChapterModal.showModal();
showDownloadMenu(false);
}}
/>
<Menu.Item
title="Unread"
style={{backgroundColor: theme.menuColor}}
@@ -380,6 +446,23 @@ const Novel = ({route, navigation}) => {
showDownloadMenu(false);
}}
/>
<Menu.Item
title="All"
style={{backgroundColor: theme.menuColor}}
titleStyle={{
color: theme.textColorPrimary,
}}
onPress={() => {
dispatch(
downloadAllChaptersAction(
novel.sourceId,
novel.novelUrl,
chapters,
),
);
showDownloadMenu(false);
}}
/>
<Menu.Item
title="Delete downloads"
style={{backgroundColor: theme.menuColor}}
@@ -583,6 +666,14 @@ const Novel = ({route, navigation}) => {
theme={theme}
dispatch={dispatch}
/>
<DownloadCustomChapterModal
modalVisible={downloadCustomChapterModal.visible}
hideModal={downloadCustomChapterModal.hideModal}
novel={novel}
chapters={chapters}
theme={theme}
dispatch={dispatch}
/>
<NovelBottomSheet
novelUrl={novelUrl}
bottomSheetRef={novelBottomSheetRef}
+2 -2
View File
@@ -66,7 +66,7 @@ const ChapterItem = ({
marginTop: 5,
}}
>
{releaseDate && (
{releaseDate ? (
<Text
style={{
color: read
@@ -80,7 +80,7 @@ const ChapterItem = ({
>
{releaseDate}
</Text>
)}
) : null}
{showProgressPercentage(chapter)}
</View>
</View>
@@ -0,0 +1,115 @@
import React, {useState} from 'react';
import {StyleSheet, Text, View, TextInput} from 'react-native';
import {IconButton, Modal, Portal} from 'react-native-paper';
import {downloadAllChaptersAction} from '../../../redux/novel/novel.actions';
const DownloadCustomChapterModal = ({
theme,
hideModal,
modalVisible,
novel,
chapters,
dispatch,
}) => {
const [text, setText] = useState(0);
const onDismiss = () => {
hideModal();
setText(0);
};
const onSubmit = () => {
dispatch(
downloadAllChaptersAction(
novel.sourceId,
novel.novelUrl,
chapters
.filter(
chapter =>
!!chapter.read === false && !!chapter.downloaded === false,
)
.slice(0, text.toString()),
),
);
hideModal();
};
const onChangeText = txt => {
if (Number(txt) >= 0) {
setText(Number(txt));
}
};
return (
<Portal>
<Modal
visible={modalVisible}
onDismiss={onDismiss}
contentContainerStyle={[
styles.modalContainer,
{backgroundColor: theme.colorPrimary},
]}
>
<Text style={[styles.modalTitle, {color: theme.textColorPrimary}]}>
Download custom amount
</Text>
<View style={{flexDirection: 'row', justifyContent: 'center'}}>
<IconButton
icon="chevron-double-left"
animated
size={24}
color={theme.colorAccent}
onPress={() => text > 9 && setText(prevState => prevState - 10)}
/>
<IconButton
icon="chevron-left"
animated
size={24}
color={theme.colorAccent}
onPress={() => text > 0 && setText(prevState => prevState - 1)}
/>
<TextInput
value={text.toString()}
style={{color: theme.textColorPrimary, marginHorizontal: 4}}
keyboardType="numeric"
onChangeText={onChangeText}
onSubmitEditing={onSubmit}
/>
<IconButton
icon="chevron-right"
animated
size={24}
color={theme.colorAccent}
onPress={() => setText(prevState => prevState + 1)}
/>
<IconButton
icon="chevron-double-right"
animated
size={24}
color={theme.colorAccent}
onPress={() => setText(prevState => prevState + 10)}
/>
</View>
</Modal>
</Portal>
);
};
export default DownloadCustomChapterModal;
const styles = StyleSheet.create({
modalContainer: {
margin: 30,
padding: 20,
borderRadius: 8,
},
modalTitle: {
fontSize: 16,
marginBottom: 16,
},
errorText: {
color: '#FF0033',
paddingTop: 8,
},
});
@@ -11,7 +11,7 @@ import {
import {IconButton, Portal} from 'react-native-paper';
import {LinearGradient} from 'expo-linear-gradient';
import easeGradient from 'react-native-easing-gradient';
import {easeGradient} from 'react-native-easing-gradient';
import FastImage from 'react-native-fast-image';
import {TouchableWithoutFeedback} from 'react-native-gesture-handler';
import {Chip} from '../../../../components/Chip/Chip';
@@ -237,9 +237,9 @@ const NovelInfoHeader = ({
followed={novel.followed}
theme={theme}
/>
{novel.genre !== null && novel.genre !== '' && (
{novel.genre ? (
<NovelGenres theme={theme} genre={novel.genre} />
)}
) : null}
<ReadButton
novel={novel}
chapters={chapters}
@@ -252,16 +252,14 @@ const NovelInfoHeader = ({
onPress={() => novelBottomSheetRef.current.show()}
android_ripple={{color: theme.rippleColor}}
>
<>
<Text style={[{color: theme.textColorPrimary}, styles.chapters]}>
{`${chapters.length} chapters`}
</Text>
<IconButton
icon="filter-variant"
color={filter ? theme.filterColor : theme.textColorPrimary}
size={24}
/>
</>
<Text style={[{color: theme.textColorPrimary}, styles.chapters]}>
{`${chapters.length} chapters`}
</Text>
<IconButton
icon="filter-variant"
color={filter ? theme.filterColor : theme.textColorPrimary}
size={24}
/>
</Pressable>
</>
)}
@@ -10,8 +10,8 @@ const NovelSummary = ({summary, followed, theme}) => {
const expandNovelSummary = () => setExpanded(!expanded);
return (
<View style={[styles.summaryContainer, {paddingBottom: expanded ? 20 : 8}]}>
return summary ? (
<View style={[styles.summaryContainer, {paddingBottom: expanded ? 24 : 8}]}>
<Text
style={{
color: theme.textColorSecondary,
@@ -48,7 +48,7 @@ const NovelSummary = ({summary, followed, theme}) => {
/>
</View>
</View>
);
) : null;
};
export default NovelSummary;
@@ -37,7 +37,7 @@ const JumpToChapterModal = ({
});
hideModal();
} else {
setError(`Enter a valid chapter number (<= ${chapters.length})`);
setError(`Enter a valid chapter number (≤ ${chapters.length})`);
}
} else {
const chapter = chapters.find(chap =>
@@ -89,7 +89,7 @@ const JumpToChapterModal = ({
<TextInput
value={text}
placeholder={
mode ? 'Chapter Name' : `Chapter Number (<= ${chapters.length})`
mode ? 'Chapter Name' : `Chapter Number (≤ ${chapters.length})`
}
onChangeText={onChangeText}
onSubmitEditing={onSubmit}
+4 -1
View File
@@ -12,6 +12,7 @@ import {
import {useDispatch} from 'react-redux';
import {IconButton, Portal} from 'react-native-paper';
import Tts from 'react-native-tts';
import {useKeepAwake} from 'expo-keep-awake';
import {
changeNavigationBarColor,
hideNavigationBar,
@@ -36,7 +37,7 @@ import {updateChaptersRead} from '../../redux/tracker/tracker.actions';
import {readerBackground, readerTextColor} from './utils/readerStyles';
import {markChapterReadAction} from '../../redux/novel/novel.actions';
import {saveScrollPosition} from '../../redux/preferences/preference.actions';
import {parseChapterNumber} from '../../services/updates';
import {parseChapterNumber} from '../../services/utils/helpers';
import ReaderAppbar from './components/ReaderAppbar';
import ReaderBottomSheet from './components/ReaderBottomSheet/ReaderBottomSheet';
@@ -59,6 +60,8 @@ import {cleanHtml} from '../../sources/helpers/cleanHtml';
import WebView from 'react-native-webview';
const Chapter = ({route, navigation}) => {
useKeepAwake();
const {
sourceId,
chapterId,
@@ -243,7 +243,7 @@ const ReaderSheet = ({
<ReaderBottomSheetFontPicker reader={reader} theme={theme} />
</View>
<ReaderBottomSheetSwitch
label="Use WebView"
label="Render HTML"
onPress={() =>
dispatch(
setAppSettings('useWebViewForChapter', !useWebViewForChapter),
+10 -1
View File
@@ -1,6 +1,13 @@
import React from 'react';
import PropTypes from 'prop-types';
import {Pressable, StatusBar, StyleSheet, Text, View} from 'react-native';
import {
Pressable,
StatusBar,
StyleSheet,
Text,
View,
Dimensions,
} from 'react-native';
import {readerLineHeight, readerTextColor} from '../utils/readerStyles';
import {htmlToText} from '../../../sources/helpers/htmlToText';
@@ -25,6 +32,7 @@ const TextReader = ({
fontFamily: reader.fontFamily,
};
const readerHeight = Dimensions.get('window').height + 80;
const chapterText = htmlToText(text);
return (
@@ -34,6 +42,7 @@ const TextReader = ({
{
paddingHorizontal: `${reader.padding}%`,
paddingTop: StatusBar.currentHeight,
minHeight: readerHeight,
},
]}
>
+52 -5
View File
@@ -11,6 +11,7 @@ import {showToast} from '../../hooks/showToast';
import {deleteNovelCache} from '../../database/queries/NovelQueries';
import {clearUpdates} from '../../database/queries/UpdateQueries';
import {clearCoverCache} from '../../services/utils/coverCache';
const AdvancedSettings = ({navigation}) => {
const theme = useTheme();
@@ -22,11 +23,15 @@ const AdvancedSettings = ({navigation}) => {
const showClearDatabaseDialog = () => setClearDatabaseDialog(true);
const hideClearDatabaseDialog = () => setClearDatabaseDialog(false);
const [clearUpdatesDialog, setClearUpdatesDialog] = useState(false);
const showClearUpdatesDialog = () => setClearUpdatesDialog(true);
const hideClearUpdatesDialog = () => setClearUpdatesDialog(false);
return (
<ScreenContainer theme={theme}>
<Appbar title="Advanced" onBackAction={() => navigation.goBack()} />
<List.Section>
<List.SubHeader theme={theme}>Data</List.SubHeader>
<List.SubHeader theme={theme}>Data Management</List.SubHeader>
<List.Item
title="Clear database"
description="Delete history for novels not in your library"
@@ -36,10 +41,13 @@ const AdvancedSettings = ({navigation}) => {
<List.Item
title="Clear updates tab"
description="Clears chapter entries in updates tab"
onPress={() => {
clearUpdates();
showToast('Updates cleared.');
}}
onPress={showClearUpdatesDialog}
theme={theme}
/>
<List.Item
title="Clean up cached covers"
description="Delete all covers cached"
onPress={clearCoverCache}
theme={theme}
/>
</List.Section>
@@ -83,6 +91,45 @@ const AdvancedSettings = ({navigation}) => {
</Button>
</Dialog.Actions>
</Dialog>
<Dialog
visible={clearUpdatesDialog}
onDismiss={hideClearUpdatesDialog}
style={{
borderRadius: 6,
backgroundColor: theme.colorPrimary,
}}
>
<Dialog.Title
style={{
letterSpacing: 0,
fontSize: 16,
lineHeight: 16 * 1.5,
color: theme.textColorPrimary,
}}
>
Are you sure? Updates tab will be cleared.
</Dialog.Title>
<Dialog.Actions>
<Button
uppercase={false}
theme={{colors: {primary: theme.colorAccent}}}
onPress={hideClearUpdatesDialog}
>
Cancel
</Button>
<Button
uppercase={false}
theme={{colors: {primary: theme.colorAccent}}}
onPress={() => {
clearUpdates();
showToast('Updates cleared.');
hideClearUpdatesDialog();
}}
>
Ok
</Button>
</Dialog.Actions>
</Dialog>
</Portal>
</ScreenContainer>
);
@@ -31,13 +31,6 @@ const BackupSettings = ({navigation}) => {
onPress={restoreFullBackup}
theme={theme}
/>
{/* <List.Divider theme={theme} />
<List.Item
title="Restore old backup"
description="Restore library from backup file of version v1.1.2 or lower"
onPress={() => dispatch(restoreLibraryAction())}
theme={theme}
/> */}
<List.InfoItem
title="Create backup may not work on devices with Android 9 or lower."
icon="information-outline"
@@ -14,6 +14,7 @@ import {useSettings, useTheme} from '../../hooks/reduxHooks';
import {setAppSettings} from '../../redux/settings/settings.actions';
import {SHOW_LAST_UPDATE_TIME} from '../../redux/updates/updates.types';
import {useModal} from '../../hooks/useModal';
import DefaultChapterSortModal from './components/DefaultChapterSortModal';
const GenralSettings = ({navigation}) => {
const theme = useTheme();
@@ -27,6 +28,8 @@ const GenralSettings = ({navigation}) => {
updateLibraryOnLaunch = false,
downloadNewChapters = false,
onlyUpdateOngoingNovels = false,
defaultChapterSort = 'ORDER BY chapterId ASC',
refreshNovelMetadata = false,
} = useSettings();
const {showLastUpdateTime = true} = useSelector(
@@ -54,6 +57,8 @@ const GenralSettings = ({navigation}) => {
*/
const gridSizeModalRef = useModal();
const defaultChapterSortModal = useModal();
return (
<ScreenContainer theme={theme}>
<Appbar title="General" onBackAction={navigation.goBack} />
@@ -85,6 +90,18 @@ const GenralSettings = ({navigation}) => {
theme={theme}
/>
<List.Divider theme={theme} />
<List.SubHeader theme={theme}>Novel</List.SubHeader>
<List.Item
title="Default chapter sort"
description={`By source ${
defaultChapterSort === 'ORDER BY chapterId ASC'
? '(Ascending)'
: '(Descending)'
}`}
onPress={defaultChapterSortModal.showModal}
theme={theme}
/>
<List.Divider theme={theme} />
<List.SubHeader theme={theme}>Global update</List.SubHeader>
<SwitchSetting
label="Only update ongoing novels"
@@ -99,6 +116,17 @@ const GenralSettings = ({navigation}) => {
}
theme={theme}
/>
<SwitchSetting
label="Automatically refresh metadata"
description="Check for new cover and details when updating library"
value={refreshNovelMetadata}
onPress={() =>
dispatch(
setAppSettings('refreshNovelMetadata', !refreshNovelMetadata),
)
}
theme={theme}
/>
<SwitchSetting
label="Show last update time"
value={showLastUpdateTime}
@@ -131,6 +159,13 @@ const GenralSettings = ({navigation}) => {
dispatch={dispatch}
theme={theme}
/>
<DefaultChapterSortModal
defaultChapterSort={defaultChapterSort}
displayModalVisible={defaultChapterSortModal.visible}
hideDisplayModal={defaultChapterSortModal.hideModal}
dispatch={dispatch}
theme={theme}
/>
<GridSizeModal
dispatch={dispatch}
novelsPerRow={novelsPerRow}
+467 -273
View File
@@ -5,17 +5,16 @@ import {
StyleSheet,
Text,
TextInput,
useWindowDimensions,
KeyboardAvoidingView,
View,
} from 'react-native';
import {Button} from 'react-native-paper';
import {Button, IconButton} from 'react-native-paper';
import WebView from 'react-native-webview';
import {useDispatch} from 'react-redux';
import {TabView, TabBar} from 'react-native-tab-view';
import {Appbar} from '../../components/Appbar';
import ColorPickerModal from '../../components/ColorPickerModal';
import {Row, ScreenContainer} from '../../components/Common';
import {Row} from '../../components/Common';
import {
ToggleButton,
ToggleColorButton,
@@ -27,230 +26,83 @@ import {
setAppSettings,
setReaderSettings,
} from '../../redux/settings/settings.actions';
import {readerBackground, readerTextColor} from '../reader/utils/readerStyles';
import {
readerBackground,
readerLineHeight,
readerTextColor,
} from '../reader/utils/readerStyles';
import {useModal} from '../../hooks/useModal';
import SwitchSetting from '../../components/Switch/Switch';
import FontPickerModal from './components/FontPickerModal';
import {fonts} from '../../services/utils/constants';
const FirstRoute = React.memo(({theme, dispatch, reader}) => {
const presetThemes = [
{
value: 1,
backgroundColor: '#000000',
textColor: 'rgba(255,255,255,0.7)',
},
{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 presetThemes = [
{
value: 1,
backgroundColor: '#000000',
textColor: 'rgba(255,255,255,0.7)',
},
{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 textAlignments = [
{value: 'left', icon: 'format-align-left'},
{value: 'center', icon: 'format-align-center'},
{value: 'justify', icon: 'format-align-justify'},
{value: 'right', icon: 'format-align-right'},
];
const textAlignments = [
{value: 'left', icon: 'format-align-left'},
{value: 'center', icon: 'format-align-center'},
{value: 'justify', icon: 'format-align-justify'},
{value: 'right', icon: 'format-align-right'},
];
const {autoScrollInterval = 10} = useSettings();
const SettingsReaderScreen = ({navigation}) => {
const theme = useTheme();
const dispatch = useDispatch();
const reader = useReaderSettings();
/**
* Reader Background Color Modal
*/
const {
useWebViewForChapter = false,
verticalSeekbar = true,
swipeGestures = true,
fullScreenMode = true,
showScrollPercentage = true,
showBatteryAndTime = false,
autoScrollInterval = 10,
} = useSettings();
const readerFontPickerModal = useModal();
const readerBackgroundModal = useModal();
/**
* Reader Text Color Modal
*/
const [readerTextColorModal, setReaderTextColorModal] = useState(false);
const showReaderTextColorModal = () => setReaderTextColorModal(true);
const hideReaderTextColorModal = () => setReaderTextColorModal(false);
const readerTextColorModal = useModal();
const setReaderBackground = val => {
dispatch(setReaderSettings('theme', val));
};
const setReaderTextColor = val => {
dispatch(setReaderSettings('textColor', val || reader.textColor));
dispatch(setReaderSettings('textColor', val ?? reader.textColor));
};
const [customHtmlCSS, setcustomHtmlCSS] = useState(reader.customCSS);
const [customCSS, setcustomCSS] = useState(reader.customCSS);
return (
<View style={{flex: 1}}>
<ScrollView>
<List.Section>
<List.SubHeader theme={theme}>Preset</List.SubHeader>
<View style={styles.pressableListItem}>
<Text
<>
<Appbar title="Reader" onBackAction={navigation.goBack} />
<KeyboardAvoidingView style={{flex: 1}} behavior="padding">
<ScrollView
style={{flex: 1}}
contentContainerStyle={{paddingBottom: 40}}
>
{useWebViewForChapter ? (
<WebView
originWhitelist={['*']}
style={{
color: theme.textColorPrimary,
fontSize: 16,
height: 500,
backgroundColor: readerBackground(reader.theme),
}}
>
Preset Themes
</Text>
<ScrollView
style={{marginLeft: 16}}
horizontal={true}
showsHorizontalScrollIndicator={false}
>
<Row>
{presetThemes.map(item => (
<ToggleColorButton
key={item.value}
selected={reader.theme === item.backgroundColor}
backgroundColor={item.backgroundColor}
textColor={item.textColor}
onPress={() => {
dispatch(
setReaderSettings('theme', item.backgroundColor),
);
dispatch(setReaderSettings('textColor', item.textColor));
}}
/>
))}
</Row>
</ScrollView>
</View>
<List.Divider theme={theme} />
<List.SubHeader theme={theme}>Reader</List.SubHeader>
<List.ColorItem
title="Background Color"
description={readerBackground(reader.theme).toUpperCase()}
onPress={readerBackgroundModal.showModal}
theme={theme}
/>
<List.ColorItem
title="Text Color"
description={
reader.textColor.toUpperCase() || readerTextColor(reader.theme)
}
onPress={showReaderTextColorModal}
theme={theme}
/>
<List.Divider theme={theme} />
<List.SubHeader theme={theme}>Text</List.SubHeader>
<Pressable style={styles.pressableListItem}>
<View>
<Text
style={{
color: theme.textColorPrimary,
fontSize: 16,
}}
>
Text Align
</Text>
<Text
style={{
color: theme.textColorSecondary,
textTransform: 'capitalize',
}}
>
{reader.textAlign}
</Text>
</View>
<Row>
{textAlignments.map(item => (
<ToggleButton
key={item.value}
icon={item.icon}
onPress={() => {
dispatch(setReaderSettings('textAlign', item.value));
}}
selected={item.value === reader.textAlign}
theme={theme}
/>
))}
</Row>
</Pressable>
<List.Divider theme={theme} />
<List.SubHeader theme={theme}>
Custom CSS (Only works in webview mode)
</List.SubHeader>
<View style={{paddingHorizontal: 16, paddingBottom: 8}}>
<TextInput
style={{
color: theme.textColorPrimary,
fontSize: 16,
}}
value={customHtmlCSS}
onChangeText={text => setcustomHtmlCSS(text)}
placeholderTextColor={theme.textColorSecondary}
placeholder="Example: body { color: red; }"
multiline={true}
/>
<View style={{flexDirection: 'row-reverse'}}>
<Button
uppercase={false}
color={theme.colorAccent}
onPress={() =>
dispatch(setReaderSettings('customCSS', customHtmlCSS))
}
>
Save
</Button>
<Button
uppercase={false}
color={theme.colorAccent}
onPress={() => {
setcustomHtmlCSS('');
dispatch(setReaderSettings('customCSS', ''));
}}
>
Clear
</Button>
</View>
</View>
<List.Divider theme={theme} />
<List.SubHeader theme={theme}>
Auto scroll interval (in seconds)
</List.SubHeader>
<View style={{paddingHorizontal: 16, paddingBottom: 8}}>
<TextInput
style={{
color: theme.textColorPrimary,
fontSize: 16,
}}
defaultValue={autoScrollInterval.toString() ?? 10}
keyboardType="numeric"
onChangeText={text =>
text !== '' &&
dispatch(
setAppSettings('autoScrollInterval', parseInt(text, 10)),
)
}
/>
</View>
</List.Section>
</ScrollView>
<ColorPickerModal
title="Reader background color"
modalVisible={readerBackgroundModal.visible}
color={readerBackground(reader.theme)}
hideModal={readerBackgroundModal.hideModal}
theme={theme}
onSubmit={setReaderBackground}
/>
<ColorPickerModal
title="Reader text color"
modalVisible={readerTextColorModal}
color={reader.textColor}
hideModal={hideReaderTextColorModal}
theme={theme}
onSubmit={setReaderTextColor}
/>
</View>
);
});
const SecondRoute = React.memo(({reader}) => (
<View style={{flex: 1}}>
<WebView
originWhitelist={['*']}
style={{
backgroundColor: readerBackground(reader.theme),
}}
source={{
html: `
nestedScrollEnabled={true}
source={{
html: `
<html>
<head>
<meta name="viewport" content="width=device-width, initial-scale=1.0, maximum-scale=1.0">
@@ -291,69 +143,410 @@ const SecondRoute = React.memo(({reader}) => (
</body>
</html>
`,
}}
/>
</View>
));
}}
/>
) : (
<View
style={{
backgroundColor: readerBackground(reader.theme),
padding: `${reader.padding}%`,
}}
>
<Text
style={{
color: reader.textColor,
fontSize: reader.textSize,
lineHeight: readerLineHeight(
reader.textSize,
reader.lineHeight,
),
fontFamily: reader.fontFamily,
}}
>
Lorem ipsum dolor sit amet, consectetur adipiscing elit. Aenean
lobortis, diam sed malesuada bibendum, nulla libero scelerisque
sapien, nec interdum nisl ipsum ac ipsum. In lacinia eros ut
quam commodo, in finibus augue ultricies. Vestibulum ex purus,
condimentum eget sem at, molestie semper mi. Mauris ac feugiat
quam. Pellentesque sagittis bibendum nibh eu lacinia. Aenean
rhoncus, velit sit amet mollis egestas, diam turpis ornare
velit, a dictum elit velit in erat. Quisque luctus in sem a
vulputate.{'\n\n'}Pellentesque id tempus orci, non finibus
tortor. Suspendisse in neque non eros eleifend hendrerit vitae a
lacus. Vestibulum ante ipsum primis in faucibus orci luctus et
ultrices posuere cubilia curae; Nunc interdum magna nulla, eget
pharetra arcu dapibus eu. Pellentesque sed lectus sit amet sem
porta lacinia. Curabitur consectetur dolor in nibh varius, ac
placerat erat pellentesque. Proin in justo condimentum,
fermentum lacus eget, ultrices ante. Fusce tempor blandit erat
nec mollis. Vivamus in leo et nunc consectetur elementum nec ac
lectus.
</Text>
</View>
)}
<SwitchSetting
label="Render HTML"
description="Render chapter as html"
value={useWebViewForChapter}
onPress={() =>
dispatch(
setAppSettings('useWebViewForChapter', !useWebViewForChapter),
)
}
theme={theme}
/>
<SwitchSetting
label="Horizontal seekbar"
description="Show horizontal seekbar instead of vertical"
value={verticalSeekbar}
onPress={() =>
dispatch(setAppSettings('verticalSeekbar', !verticalSeekbar))
}
theme={theme}
/>
<SwitchSetting
label="Gesture navigation"
description="Use left and right swipe to navigate to previous or next chapter"
value={swipeGestures}
onPress={() =>
dispatch(setAppSettings('swipeGestures', !swipeGestures))
}
theme={theme}
/>
<List.Divider theme={theme} />
<List.SubHeader theme={theme}>
Auto scroll interval (in seconds)
</List.SubHeader>
<View style={{paddingHorizontal: 16, paddingBottom: 8}}>
<TextInput
style={{
color: theme.textColorPrimary,
fontSize: 16,
}}
defaultValue={autoScrollInterval.toString() ?? 10}
keyboardType="numeric"
onChangeText={text =>
text !== '' &&
dispatch(
setAppSettings('autoScrollInterval', parseInt(text, 10)),
)
}
/>
</View>
<List.Divider theme={theme} />
<List.SubHeader theme={theme}>Display</List.SubHeader>
<SwitchSetting
label="Fullscreen"
value={fullScreenMode}
onPress={() =>
dispatch(setAppSettings('fullScreenMode', !fullScreenMode))
}
theme={theme}
/>
<SwitchSetting
label="Show progress percentage"
value={showScrollPercentage}
onPress={() =>
dispatch(
setAppSettings('showScrollPercentage', !showScrollPercentage),
)
}
theme={theme}
/>
const ReaderSettings = ({navigation}) => {
const theme = useTheme();
const dispatch = useDispatch();
<SwitchSetting
label="Show battery and time"
value={showBatteryAndTime}
onPress={() =>
dispatch(
setAppSettings('showBatteryAndTime', !showBatteryAndTime),
)
}
theme={theme}
/>
<List.Divider theme={theme} />
const reader = useReaderSettings();
<List.SubHeader theme={theme}>Reader</List.SubHeader>
<View style={styles.pressableListItem}>
<Text
style={{
color: theme.textColorPrimary,
fontSize: 16,
}}
>
Themes
</Text>
<ScrollView
contentContainerStyle={{
flexDirection: 'column-reverse',
alignItems: 'flex-end',
}}
style={{marginLeft: 16}}
horizontal={true}
showsHorizontalScrollIndicator={false}
>
<Row>
{presetThemes.map(item => (
<ToggleColorButton
key={item.value}
selected={reader.theme === item.backgroundColor}
backgroundColor={item.backgroundColor}
textColor={item.textColor}
onPress={() => {
dispatch(
setReaderSettings('theme', item.backgroundColor),
);
dispatch(setReaderSettings('textColor', item.textColor));
}}
/>
))}
</Row>
</ScrollView>
</View>
<List.ColorItem
title="Background Color"
description={readerBackground(reader.theme).toUpperCase()}
onPress={readerBackgroundModal.showModal}
theme={theme}
/>
<List.ColorItem
title="Text Color"
description={
reader.textColor.toUpperCase() || readerTextColor(reader.theme)
}
onPress={readerTextColorModal.showModal}
theme={theme}
/>
<Pressable style={styles.pressableListItem}>
<View>
<Text style={{color: theme.textColorPrimary, fontSize: 16}}>
Text Align
</Text>
<Text
style={{
color: theme.textColorSecondary,
textTransform: 'capitalize',
}}
>
{reader.textAlign}
</Text>
</View>
<Row>
{textAlignments.map(item => (
<ToggleButton
key={item.value}
icon={item.icon}
onPress={() => {
dispatch(setReaderSettings('textAlign', item.value));
}}
selected={item.value === reader.textAlign}
theme={theme}
/>
))}
</Row>
</Pressable>
<List.Item
title="Font"
description={
fonts.find(item => item.fontFamily === reader.fontFamily)?.name
}
onPress={readerFontPickerModal.showModal}
theme={theme}
/>
<Row
style={{
justifyContent: 'space-between',
marginHorizontal: 16,
marginVertical: 8,
}}
>
<Text style={{color: theme.textColorPrimary, fontSize: 16}}>
Font size
</Text>
<Row>
<IconButton
icon="minus"
color={theme.colorAccent}
size={26}
disabled={reader.textSize <= 1 ? true : false}
onPress={() =>
dispatch(setReaderSettings('textSize', reader.textSize - 1))
}
style={{marginVertical: 0}}
/>
<Text
style={{
color: theme.textColorPrimary,
paddingHorizontal: 24,
}}
>
{reader.textSize}
</Text>
<IconButton
icon="plus"
color={theme.colorAccent}
size={26}
onPress={() =>
dispatch(setReaderSettings('textSize', reader.textSize + 1))
}
style={{marginVertical: 0}}
/>
</Row>
</Row>
<Row
style={{
justifyContent: 'space-between',
marginHorizontal: 16,
marginVertical: 8,
}}
>
<Text style={{color: theme.textColorPrimary, fontSize: 16}}>
Padding
</Text>
<Row>
<IconButton
icon="minus"
color={theme.colorAccent}
size={26}
disabled={reader.padding <= 0 ? true : false}
onPress={() =>
dispatch(setReaderSettings('padding', reader.padding - 1))
}
style={{marginVertical: 0}}
/>
<Text
style={{
color: theme.textColorPrimary,
paddingHorizontal: 24,
}}
>
{`${reader.padding}%`}
</Text>
<IconButton
icon="plus"
color={theme.colorAccent}
size={26}
disabled={reader.padding >= 10 ? true : false}
onPress={() =>
dispatch(setReaderSettings('padding', reader.padding + 1))
}
style={{marginVertical: 0}}
/>
</Row>
</Row>
<Row
style={{
justifyContent: 'space-between',
marginHorizontal: 16,
marginVertical: 8,
}}
>
<Text style={{color: theme.textColorPrimary, fontSize: 16}}>
Line height
</Text>
<Row>
<IconButton
icon="minus"
color={theme.colorAccent}
size={26}
disabled={reader.lineHeight <= 1.3 ? true : false}
onPress={() =>
dispatch(
setReaderSettings('lineHeight', reader.lineHeight - 0.1),
)
}
style={{marginVertical: 0}}
/>
<Text
style={{
color: theme.textColorPrimary,
paddingHorizontal: 24,
}}
>
{`${Math.round(reader.lineHeight * 10) / 10}%`}
</Text>
<IconButton
icon="plus"
color={theme.colorAccent}
size={26}
disabled={reader.lineHeight >= 2 ? true : false}
onPress={() =>
dispatch(
setReaderSettings('lineHeight', reader.lineHeight + 0.1),
)
}
style={{marginVertical: 0}}
/>
</Row>
</Row>
{useWebViewForChapter ? (
<>
<List.Divider theme={theme} />
<List.SubHeader theme={theme}>Custom CSS</List.SubHeader>
<View style={{paddingHorizontal: 16, paddingBottom: 8}}>
<TextInput
style={{color: theme.textColorPrimary, fontSize: 16}}
value={customCSS}
onChangeText={text => setcustomCSS(text)}
placeholderTextColor={theme.textColorSecondary}
placeholder="Example: body { color: red; }"
multiline={true}
/>
<View style={{flexDirection: 'row-reverse'}}>
<Button
uppercase={false}
color={theme.colorAccent}
onPress={() =>
dispatch(setReaderSettings('customCSS', customCSS))
}
>
Save
</Button>
<Button
uppercase={false}
color={theme.colorAccent}
onPress={() => {
setcustomCSS('');
dispatch(setReaderSettings('customCSS', ''));
}}
>
Clear
</Button>
</View>
</View>
</>
) : null}
</ScrollView>
</KeyboardAvoidingView>
const renderScene = ({route}) => {
switch (route.key) {
case 'first':
return <FirstRoute dispatch={dispatch} theme={theme} reader={reader} />;
case 'second':
return <SecondRoute reader={reader} />;
}
};
const layout = useWindowDimensions();
const [index, setIndex] = useState(0);
const [routes] = useState([
{key: 'first', title: 'Settings'},
{key: 'second', title: 'Preview'},
]);
const renderTabBar = props => (
<TabBar
{...props}
indicatorStyle={{backgroundColor: theme.colorAccent}}
style={{backgroundColor: theme.colorPrimary}}
renderLabel={({route, focused, color}) => (
<Text style={{color}}>{route.title}</Text>
)}
inactiveColor={theme.textColorSecondary}
activeColor={theme.colorAccent}
pressColor={theme.rippleColor}
/>
);
return (
<ScreenContainer theme={theme}>
<Appbar
title="Reader"
onBackAction={navigation.goBack}
style={{elevation: 0}}
<ColorPickerModal
title="Reader background color"
modalVisible={readerBackgroundModal.visible}
color={readerBackground(reader.theme)}
hideModal={readerBackgroundModal.hideModal}
theme={theme}
onSubmit={setReaderBackground}
/>
<TabView
navigationState={{index, routes}}
renderTabBar={renderTabBar}
renderScene={renderScene}
onIndexChange={setIndex}
initialLayout={{width: layout.width}}
swipeEnabled={false}
<ColorPickerModal
title="Reader text color"
modalVisible={readerTextColorModal.visible}
color={reader.textColor}
hideModal={readerTextColorModal.hideModal}
theme={theme}
onSubmit={setReaderTextColor}
/>
</ScreenContainer>
<FontPickerModal
currentFont={reader.fontFamily}
visible={readerFontPickerModal.visible}
hideModal={readerFontPickerModal.hideModal}
theme={theme}
dispatch={dispatch}
/>
</>
);
};
export default ReaderSettings;
export default SettingsReaderScreen;
const styles = StyleSheet.create({
pressableListItem: {
@@ -362,13 +555,14 @@ const styles = StyleSheet.create({
justifyContent: 'space-between',
alignItems: 'center',
},
presetToggleButton: {
marginLeft: 12,
borderRadius: 50,
borderTopRightRadius: 50,
borderBottomRightRadius: 50,
borderTopLeftRadius: 50,
borderBottomLeftRadius: 50,
borderWidth: 0,
fontContainer: {
borderRadius: 8,
overflow: 'hidden',
marginHorizontal: 6,
},
fontContent: {
borderRadius: 8,
paddingHorizontal: 10,
paddingVertical: 4,
},
});
@@ -0,0 +1,61 @@
import React from 'react';
import {StyleSheet} from 'react-native';
import {Portal, Modal} from 'react-native-paper';
import {SortItem} from '../../../components/Checkbox/Checkbox';
import {setAppSettings} from '../../../redux/settings/settings.actions';
const DefaultChapterSortModal = ({
theme,
dispatch,
defaultChapterSort,
hideDisplayModal,
displayModalVisible,
}) => {
return (
<Portal>
<Modal
visible={displayModalVisible}
onDismiss={hideDisplayModal}
contentContainerStyle={[
styles.containerStyle,
{backgroundColor: theme.colorPrimaryDark},
]}
>
<SortItem
label="By source"
theme={theme}
status={
defaultChapterSort === 'ORDER BY chapterId ASC' ? 'asc' : 'desc'
}
onPress={() =>
defaultChapterSort === 'ORDER BY chapterId ASC'
? dispatch(
setAppSettings(
'defaultChapterSort',
'ORDER BY chapterId DESC',
),
)
: dispatch(
setAppSettings(
'defaultChapterSort',
'ORDER BY chapterId ASC',
),
)
}
/>
</Modal>
</Portal>
);
};
export default DefaultChapterSortModal;
const styles = StyleSheet.create({
containerStyle: {
paddingVertical: 20,
margin: 20,
borderRadius: 6,
},
});
@@ -0,0 +1,53 @@
import React from 'react';
import {StyleSheet} from 'react-native';
import {Portal, Modal} from 'react-native-paper';
import {RadioButton} from '../../../components/RadioButton/RadioButton';
import {setReaderSettings} from '../../../redux/settings/settings.actions';
import {fonts} from '../../../services/utils/constants';
const FontPickerModal = ({
theme,
dispatch,
currentFont,
hideModal,
visible,
}) => {
return (
<Portal>
<Modal
visible={visible}
onDismiss={hideModal}
contentContainerStyle={[
styles.containerStyle,
{backgroundColor: theme.colorPrimaryDark},
]}
>
{fonts.map(item => (
<RadioButton
key={item.fontFamily}
status={currentFont === item.fontFamily}
onPress={() =>
dispatch(setReaderSettings('fontFamily', item.fontFamily))
}
label={item.name}
labelStyle={{fontFamily: item.fontFamily}}
theme={theme}
/>
))}
</Modal>
</Portal>
);
};
export default FontPickerModal;
const styles = StyleSheet.create({
containerStyle: {
paddingVertical: 20,
margin: 20,
borderRadius: 6,
},
});
+4 -3
View File
@@ -87,12 +87,13 @@ const SourceScreen = ({navigation, route}) => {
useEffect(() => {
getNovels();
return () => {
isMounted.current = false;
};
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [page]);
useEffect(() => {
return () => (isMounted.current = false);
}, []);
const getNextPage = () => {
if (!searchText) {
incrementPage();
-46
View File
@@ -1,46 +0,0 @@
// import * as DocumentPicker from 'expo-document-picker';
// import {StorageAccessFramework} from 'expo-file-system';
// import {restoreLibrary} from '../database/queries/NovelQueries';
// import * as Notifications from 'expo-notifications';
// Notifications.setNotificationHandler({
// handleNotification: async () => {
// return {
// shouldShowAlert: true,
// shouldPlaySound: true,
// shouldSetBadge: true,
// };
// },
// });
// export const restoreBackup = async () => {
// const backup = await DocumentPicker.getDocumentAsync();
// Notifications.scheduleNotificationAsync({
// content: {title: 'Restoring backup'},
// trigger: null,
// });
// if (backup.uri) {
// let novels = await StorageAccessFramework.readAsStringAsync(backup.uri);
// novels = await JSON.parse(novels);
// novels.map((novel, index) => {
// setTimeout(async () => {
// if (index + 1 === novels.length) {
// Notifications.scheduleNotificationAsync({
// content: {
// title: 'Library Restored',
// body: novels.length + ' novels restored',
// },
// trigger: null,
// });
// }
// await restoreLibrary(novel);
// }, 1000 * index);
// });
// }
// };
-233
View File
@@ -1,233 +0,0 @@
import * as SQLite from 'expo-sqlite';
const db = SQLite.openDatabase('lnreader.db');
import {getLibrary} from '../database/queries/LibraryQueries';
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 () => {
return {
shouldShowAlert: true,
shouldPlaySound: true,
shouldSetBadge: true,
};
},
});
const updateNovelDetails = async (novel, novelId) => {
db.transaction(tx => {
tx.executeSql(
'UPDATE novels SET novelName = ?, novelCover = ?, novelSummary = ?, author = ?, artist = ?, genre = ?, status = ? WHERE novelId = ?',
[
novel.novelName,
novel.novelCover,
novel.novelSummary,
novel.author,
novel.artist,
novel.genre,
novel.status,
novelId,
],
(txObj, res) => {},
(txObj, error) => {
// console.log('Error ', error)
},
);
});
};
export const updateNovel = async (sourceId, novelUrl, novelId) => {
let novel = await fetchNovel(sourceId, novelUrl);
await updateNovelDetails(novel, novelId);
db.transaction(tx => {
novel.chapters.map(chapter =>
tx.executeSql(
'INSERT OR IGNORE INTO chapters (chapterUrl, chapterName, releaseDate, novelId) values (?, ?, ?, ?)',
[chapter.chapterUrl, chapter.chapterName, chapter.releaseDate, novelId],
(txObj, {insertId}) => {
if (insertId !== -1) {
tx.executeSql(
"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
// );
},
(txOBJ, error) => {
// console.log('Error ', error)
},
);
}
},
(txObj, error) => {
// console.log('Error ', error)
},
),
);
});
};
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(
'INSERT OR IGNORE INTO chapters (chapterUrl, chapterName, releaseDate, novelId) values (?, ?, ?, ?)',
[chapter.chapterUrl, chapter.chapterName, chapter.releaseDate, novelId],
(txObj, {insertId}) => {
if (insertId !== -1) {
if (downloadNewChapters) {
downloadChapter(sourceId, novelUrl, chapter.chapterUrl, insertId);
}
tx.executeSql(
"INSERT OR IGNORE INTO updates (chapterId, novelId, updateTime) values (?, ?, (datetime('now','localtime')))",
[insertId, novelId],
(txOBJ, res) => {},
(txOBJ, error) => {
// console.log('Error ', error)
},
);
}
},
(txObj, error) => {
// console.log('Error ', error)
},
),
);
});
};
const sleep = time => new Promise(resolve => setTimeout(() => resolve(), time));
export const updateAllNovels = async updateSettings => {
let libraryNovels = await getLibrary();
const {onlyUpdateOngoingNovels = false} = updateSettings;
if (onlyUpdateOngoingNovels) {
libraryNovels = libraryNovels.filter(item => item.status !== 'Completed');
}
const options = {
taskName: 'Library Update',
taskTitle: 'Updating library',
taskDesc: '(0/' + libraryNovels.length + ')',
taskIcon: {
name: 'notification_icon',
type: 'drawable',
},
color: '#00adb5',
parameters: {
delay: 1000,
},
linkingURI: 'lnreader://updates',
progressBar: {
max: libraryNovels.length,
value: 0,
},
};
const veryIntensiveTask = async taskData =>
await new Promise(async resolve => {
for (
let i = 0;
BackgroundService.isRunning() && i < libraryNovels.length;
i++
) {
try {
if (BackgroundService.isRunning()) {
/**
* Update chapters
*/
await updateNovelChapters(
libraryNovels[i].sourceId,
libraryNovels[i].novelUrl,
libraryNovels[i].novelId,
updateSettings,
);
/**
* Update notification
*/
await BackgroundService.updateNotification({
taskTitle: libraryNovels[i].novelName,
taskDesc: '(' + (i + 1) + '/' + libraryNovels.length + ')',
progressBar: {
max: libraryNovels.length,
value: i + 1,
},
});
/**
* When updating library is finished
*/
if (libraryNovels.length === i + 1) {
resolve();
Notifications.scheduleNotificationAsync({
content: {
title: 'Library Updated',
body: libraryNovels.length + ' novels updated',
},
trigger: null,
});
}
const nextNovelIndex = i + 1;
if (
nextNovelIndex in libraryNovels &&
libraryNovels[nextNovelIndex].sourceId ===
libraryNovels[i].sourceId
) {
await sleep(taskData.delay);
}
}
} catch (error) {
showToast(libraryNovels[i].novelName + ': ' + error.message);
continue;
}
}
});
if (libraryNovels.length > 0) {
await BackgroundService.start(veryIntensiveTask, options);
}
};
export const parseChapterNumber = chapterName => {
chapterName = chapterName.toLowerCase();
chapterName = chapterName.replace(/volume (\d+)/, '');
const basic = chapterName.match(/ch (\d+)/);
const occurrence = chapterName.match(/\d+/);
if (basic) {
return basic[0];
} else if (occurrence) {
return occurrence[0];
} else {
return 0;
}
};
@@ -0,0 +1,83 @@
import {showToast} from '../../hooks/showToast';
import {fetchNovel} from '../Source/source';
import {downloadChapter} from '../../database/queries/ChapterQueries';
import * as SQLite from 'expo-sqlite';
const db = SQLite.openDatabase('lnreader.db');
const updateNovelMetadata = async (novelId, novel) => {
const {novelName, novelCover, novelSummary, author, artist, genre, status} =
novel;
db.transaction(tx => {
tx.executeSql(
'UPDATE novels SET novelName = ?, novelCover = ?, novelSummary = ?, author = ?, artist = ?, genre = ?, status = ? WHERE novelId = ?',
[
novelName,
novelCover,
novelSummary,
author,
artist,
genre,
status,
novelId,
],
(txObj, res) => {},
(txObj, error) => showToast(`Error: ${error}`),
);
});
};
const updateNovelCover = async (novelId, novel) => {
const {novelCover} = novel;
db.transaction(tx => {
tx.executeSql(
'UPDATE novels SET novelCover = ? WHERE novelId = ?',
[novelCover, novelId],
(txObj, res) => {},
(txObj, error) => showToast(`Error: ${error}`),
);
});
};
const insertUpdate = async (tx, chapterId, novelId) =>
tx.executeSql(
"INSERT OR IGNORE INTO updates (chapterId, novelId, updateTime) values (?, ?, (datetime('now','localtime')))",
[chapterId, novelId],
(txOBJ, res) => {},
(txOBJ, error) => {},
);
const updateNovel = async (sourceId, novelUrl, novelId, options) => {
const {downloadNewChapters, refreshNovelMetadata} = options;
let novel = await fetchNovel(sourceId, novelUrl);
if (refreshNovelMetadata) {
updateNovelMetadata(novelId, novel);
}
db.transaction(tx => {
novel.chapters.map(chapter => {
const {chapterName, chapterUrl, releaseDate} = chapter;
tx.executeSql(
'INSERT OR IGNORE INTO chapters (chapterUrl, chapterName, releaseDate, novelId) values (?, ?, ?, ?)',
[chapterUrl, chapterName, releaseDate, novelId],
(txObj, {insertId}) => {
if (insertId !== -1) {
if (downloadNewChapters) {
downloadChapter(sourceId, novelUrl, chapterUrl, insertId);
}
insertUpdate(tx, insertId, novelId);
}
},
(txObj, error) => {},
);
});
});
};
export {updateNovel};
+109
View File
@@ -0,0 +1,109 @@
import * as Notifications from 'expo-notifications';
import BackgroundService from 'react-native-background-actions';
import {getLibrary} from '../../database/queries/LibraryQueries';
import {showToast} from '../../hooks/showToast';
import {updateNovel} from './LibraryUpdateQueries';
Notifications.setNotificationHandler({
handleNotification: async () => {
return {
shouldShowAlert: true,
shouldPlaySound: true,
shouldSetBadge: true,
};
},
});
const sleep = time => new Promise(resolve => setTimeout(() => resolve(), time));
const updateLibrary = async options => {
const {onlyUpdateOngoingNovels} = options;
let libraryNovels = await getLibrary();
if (onlyUpdateOngoingNovels) {
libraryNovels = libraryNovels.filter(item => item.status !== 'Completed');
}
const notificationOptions = {
taskName: 'Library Update',
taskTitle: 'Updating library',
taskDesc: '(0/' + libraryNovels.length + ')',
taskIcon: {name: 'notification_icon', type: 'drawable'},
color: '#00adb5',
parameters: {delay: 1000},
linkingURI: 'lnreader://updates',
progressBar: {max: libraryNovels.length, value: 0},
};
const libraryUpdateBackgroundAction = async taskData =>
await new Promise(async resolve => {
for (
let i = 0;
BackgroundService.isRunning() && i < libraryNovels.length;
i++
) {
try {
if (BackgroundService.isRunning()) {
/**
* Update chapters
*/
await updateNovel(
libraryNovels[i].sourceId,
libraryNovels[i].novelUrl,
libraryNovels[i].novelId,
options,
);
/**
* Update notification
*/
await BackgroundService.updateNotification({
taskTitle: libraryNovels[i].novelName,
taskDesc: '(' + (i + 1) + '/' + libraryNovels.length + ')',
progressBar: {max: libraryNovels.length, value: i + 1},
});
/**
* When updating library is finished
*/
if (libraryNovels.length === i + 1) {
resolve();
Notifications.scheduleNotificationAsync({
content: {
title: 'Library Updated',
body: libraryNovels.length + ' novels updated',
},
trigger: null,
});
}
const nextNovelIndex = i + 1;
if (
nextNovelIndex in libraryNovels &&
libraryNovels[nextNovelIndex].sourceId ===
libraryNovels[i].sourceId
) {
await sleep(taskData.delay);
}
}
} catch (error) {
showToast(libraryNovels[i].novelName + ': ' + error.message);
continue;
}
}
});
if (libraryNovels.length > 0) {
await BackgroundService.start(
libraryUpdateBackgroundAction,
notificationOptions,
);
}
};
export {updateLibrary};
+9
View File
@@ -0,0 +1,9 @@
import FastImage from 'react-native-fast-image';
import {showToast} from '../../hooks/showToast';
const clearCoverCache = () => {
FastImage.clearDiskCache();
showToast('Cleared cover cache.');
};
export {clearCoverCache};
+10 -23
View File
@@ -7,7 +7,7 @@ const sourceName = 'ArNovel';
const baseUrl = 'https://arnovel.me/';
const popularNovels = async page => {
const totalPages = 21;
const totalPages = 25;
const url = baseUrl + 'novels/page/' + page;
const result = await fetch(url);
@@ -20,11 +20,7 @@ const popularNovels = async page => {
loadedCheerio('.page-item-detail').each(function () {
const novelName = loadedCheerio(this).find('.h5 > a').text();
const novelCover = defaultCoverUri;
let novelUrl = loadedCheerio(this)
.find('.h5 > a')
.attr('href')
.split('/')[4];
const novelUrl = loadedCheerio(this).find('.h5 > a').attr('href');
const novel = {
sourceId,
@@ -40,7 +36,7 @@ const popularNovels = async page => {
};
const parseNovelAndChapters = async novelUrl => {
const url = `${baseUrl}novel/${novelUrl}`;
const url = novelUrl;
const result = await fetch(url);
const body = await result.text();
@@ -83,20 +79,15 @@ const parseNovelAndChapters = async novelUrl => {
let novelChapters = [];
const data = await fetch(`${url}/ajax/chapters/`, {method: 'POST'});
const text = await data.text();
const data = await fetch(`${url}ajax/chapters/`, {method: 'POST'});
const chapterListRaw = await data.text();
loadedCheerio = cheerio.load(text);
loadedCheerio = cheerio.load(chapterListRaw);
loadedCheerio('.wp-manga-chapter').each(function () {
const chapterName = loadedCheerio(this).find('a').text().trim();
const releaseDate = loadedCheerio(this).find('span').text().trim();
let chapterUrl = loadedCheerio(this).find('a').attr('href').split('/');
chapterUrl[6]
? (chapterUrl = chapterUrl[5] + '/' + chapterUrl[6])
: (chapterUrl = chapterUrl[5]);
const releaseDate = null;
const chapterUrl = loadedCheerio(this).find('a').attr('href');
novelChapters.push({chapterName, releaseDate, chapterUrl});
});
@@ -107,7 +98,7 @@ const parseNovelAndChapters = async novelUrl => {
};
const parseChapter = async (novelUrl, chapterUrl) => {
const url = `${baseUrl}novel/${novelUrl}/${chapterUrl}/`;
const url = chapterUrl;
const result = await fetch(url);
const body = await result.text();
@@ -141,11 +132,7 @@ const searchNovels = async searchTerm => {
loadedCheerio('.c-tabs-item__content').each(function () {
const novelName = loadedCheerio(this).find('.h4 > a').text();
const novelCover = defaultCoverUri;
let novelUrl = loadedCheerio(this)
.find('.h4 > a')
.attr('href')
.split('/')[4];
const novelUrl = loadedCheerio(this).find('.h4 > a').attr('href');
const novel = {
sourceId,
+141
View File
@@ -0,0 +1,141 @@
import cheerio from 'react-native-cheerio';
import {Status} from '../helpers/constants';
const sourceId = 54;
const sourceName = 'RewayatAr';
const baseUrl = 'https://rewayat-ar.site/';
const popularNovels = async page => {
const totalPages = 2;
const url = baseUrl + 'novels/topview/page/' + page;
const result = await fetch(url);
const body = await result.text();
const loadedCheerio = cheerio.load(body);
let novels = [];
loadedCheerio('.update_item.list_category').each(function () {
const novelName = loadedCheerio(this).find('h3 > a').text();
const novelCover = loadedCheerio(this).find('img').attr('src');
let novelUrl = loadedCheerio(this).find('h3 > a').attr('href');
const novel = {
sourceId,
novelName,
novelCover,
novelUrl,
};
novels.push(novel);
});
return {totalPages, novels};
};
const parseNovelAndChapters = async novelUrl => {
const url = novelUrl;
const result = await fetch(url);
const body = await result.text();
let loadedCheerio = cheerio.load(body);
let novel = {
sourceId,
sourceName,
url,
novelUrl,
novelName: '',
novelCover: '',
author: '',
status: Status.UNKNOWN,
genre: '',
summary: '',
chapters: [],
};
novel.novelName = loadedCheerio('.truyen_info_right h1').text().trim();
novel.novelCover = loadedCheerio('.info_image img').attr('src');
novel.summary = loadedCheerio('#noidungm').text()?.trim();
novel.author = loadedCheerio(
'#main_body > div.cotgiua > div.truyen_info_wrapper > div.truyen_info > div.entry-header > div.truyen_if_wrap > ul > li:nth-child(2) > a',
)
.text()
?.trim();
let novelChapters = [];
loadedCheerio('.chapter-list div.row').each(function () {
const chapterName = loadedCheerio(this).find('a').text().trim();
const releaseDate = loadedCheerio(this)
.find('span:nth-child(2)')
.text()
.trim();
const chapterUrl = loadedCheerio(this).find('a').attr('href');
novelChapters.push({chapterName, releaseDate, chapterUrl});
});
novel.chapters = novelChapters.reverse();
return novel;
};
const parseChapter = async (novelUrl, chapterUrl) => {
const url = chapterUrl;
const result = await fetch(url);
const body = await result.text();
let loadedCheerio = cheerio.load(body);
const chapterName = loadedCheerio('h1.name_chapter').text();
const chapterText = loadedCheerio('#vung_doc').html();
const chapter = {
sourceId,
novelUrl,
chapterUrl,
chapterName,
chapterText,
};
return chapter;
};
const searchNovels = async searchTerm => {
const url = `${baseUrl}search/${searchTerm}`;
const result = await fetch(url);
const body = await result.text();
let loadedCheerio = cheerio.load(body);
let novels = [];
loadedCheerio('.update_item.list_category').each(function () {
const novelName = loadedCheerio(this).find('h3 > a').text();
const novelCover = loadedCheerio(this).find('img').attr('src');
let novelUrl = loadedCheerio(this).find('h3 > a').attr('href');
const novel = {sourceId, novelName, novelCover, novelUrl};
novels.push(novel);
});
return novels;
};
const RewayatArScraper = {
popularNovels,
parseNovelAndChapters,
parseChapter,
searchNovels,
};
export default RewayatArScraper;
+3 -3
View File
@@ -89,7 +89,7 @@ const parseNovelAndChapters = async novelUrl => {
let chapters = [];
loadedCheerio('.bookshelf-list #chapter').each(function () {
loadedCheerio('.bookshelf-list #chapter a').each(function () {
let chapterUrl = loadedCheerio(this).attr('href').substring(1);
let chapterName = loadedCheerio(this).text();
let releaseDate = null;
@@ -134,11 +134,11 @@ const parseChapter = async (novelUrl, chapterUrl) => {
};
const searchNovels = async searchTerm => {
const searchUrl = `${baseUrl}/search.html`;
const searchUrl = `${baseUrl}/so.html`;
const result = await fetch(searchUrl, {
method: 'post',
body: JSON.stringify({keyword: searchTerm}),
body: JSON.stringify({ss: 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",
+177
View File
@@ -0,0 +1,177 @@
import cheerio from 'react-native-cheerio';
const sourceId = 97;
const sourceName = 'NovelFull.me';
const baseUrl = 'https://novelfull.me/';
const popularNovels = async page => {
const totalPages = 25;
const url = `${baseUrl}popular?page=${page}`;
const result = await fetch(url);
const body = await result.text();
const loadedCheerio = cheerio.load(body);
let novels = [];
loadedCheerio('.book-item').each(function () {
const novelName = loadedCheerio(this).find('.title').text();
const novelCover =
'https:' + loadedCheerio(this).find('img').attr('data-src');
const novelUrl =
baseUrl + loadedCheerio(this).find('.title a').attr('href').substring(1);
const novel = {sourceId, novelName, novelCover, novelUrl};
novels.push(novel);
});
return {totalPages, novels};
};
const parseNovelAndChapters = async novelUrl => {
const url = novelUrl;
const result = await fetch(url);
const body = await result.text();
let loadedCheerio = cheerio.load(body);
let novel = {
sourceId,
sourceName,
url,
novelUrl,
novelName: '',
novelCover: '',
author: '',
status: '',
genre: '',
summary: '',
chapters: [],
};
novel.novelName = loadedCheerio('.name h1').text().trim();
novel.novelCover =
'https:' + loadedCheerio('.img-cover img').attr('data-src');
novel.summary = loadedCheerio(
'body > div.layout > div.main-container.book-details > div > div.row.no-gutters > div.col-lg-8 > div.mt-1 > div.section.box.mt-1.summary > div.section-body > p.content',
)
.text()
.trim();
novel.author = loadedCheerio(
'body > div.layout > div.main-container.book-details > div > div.row.no-gutters > div.col-lg-8 > div.book-info > div.detail > div.meta.box.mt-1.p-10 > p:nth-child(1) > a > span',
)
.text()
?.trim();
novel.status = loadedCheerio(
'body > div.layout > div.main-container.book-details > div > div.row.no-gutters > div.col-lg-8 > div.book-info > div.detail > div.meta.box.mt-1.p-10 > p:nth-child(2) > a > span',
)
.text()
?.trim();
novel.genre = loadedCheerio(
'body > div.layout > div.main-container.book-details > div > div.row.no-gutters > div.col-lg-8 > div.book-info > div.detail > div.meta.box.mt-1.p-10 > p:nth-child(3)',
)
.text()
?.replace('Genres :', '')
.replace(/[\s\n]+/g, ' ')
.trim();
let chapters = [];
const chaptersUrl =
novelUrl.replace(baseUrl, 'https://novelfull.me/api/novels/') +
'/chapters?source=detail';
const chaptersRequest = await fetch(chaptersUrl);
const chaptersHtml = await chaptersRequest.text();
loadedCheerio = cheerio.load(chaptersHtml);
loadedCheerio('li').each(function () {
const chapterName = loadedCheerio(this)
.find('.chapter-title')
.text()
.trim();
const releaseDate = loadedCheerio(this)
.find('.chapter-update')
.text()
.trim();
const chapterUrl =
baseUrl + loadedCheerio(this).find('a').attr('href').substring(1);
chapters.push({chapterName, releaseDate, chapterUrl});
});
novel.chapters = chapters.reverse();
return novel;
};
const parseChapter = async (novelUrl, chapterUrl) => {
const url = chapterUrl;
const result = await fetch(url);
const body = await result.text();
let loadedCheerio = cheerio.load(body);
loadedCheerio('#listen-chapter').remove();
loadedCheerio('#google_translate_element').remove();
const chapterName = loadedCheerio('#chapter__content > h1').text();
const chapterText = loadedCheerio('.chapter__content').html();
const chapter = {
sourceId,
novelUrl,
chapterUrl,
chapterName,
chapterText,
};
return chapter;
};
const searchNovels = async searchTerm => {
const url = `${baseUrl}search?q=${searchTerm}`;
const result = await fetch(url);
const body = await result.text();
let loadedCheerio = cheerio.load(body);
let novels = [];
loadedCheerio('.book-item').each(function () {
const novelName = loadedCheerio(this).find('.title').text();
const novelCover =
'https:' + loadedCheerio(this).find('img').attr('data-src');
const novelUrl =
baseUrl + loadedCheerio(this).find('.title a').attr('href').substring(1);
const novel = {sourceId, novelName, novelCover, novelUrl};
novels.push(novel);
});
return novels;
};
const NovelFullMeScraper = {
popularNovels,
parseNovelAndChapters,
parseChapter,
searchNovels,
};
export default NovelFullMeScraper;
+148
View File
@@ -0,0 +1,148 @@
import cheerio from 'react-native-cheerio';
import {Status} from '../helpers/constants';
const sourceId = 95;
const sourceName = 'BestLightNovel';
const baseUrl = 'https://bestlightnovel.com/';
const popularNovels = async page => {
const totalPages = 2156;
const url =
baseUrl + 'novel_list?type=topview&category=all&state=all&page=1' + page;
const result = await fetch(url);
const body = await result.text();
const loadedCheerio = cheerio.load(body);
let novels = [];
loadedCheerio('.update_item.list_category').each(function () {
const novelName = loadedCheerio(this).find('h3 > a').text();
const novelCover = loadedCheerio(this).find('img').attr('src');
const novelUrl = loadedCheerio(this).find('h3 > a').attr('href');
const novel = {sourceId, novelName, novelCover, novelUrl};
novels.push(novel);
});
return {totalPages, novels};
};
const parseNovelAndChapters = async novelUrl => {
const url = novelUrl;
const result = await fetch(url);
const body = await result.text();
let loadedCheerio = cheerio.load(body);
let novel = {
sourceId,
sourceName,
url,
novelUrl,
novelName: '',
novelCover: '',
author: '',
status: Status.UNKNOWN,
genre: '',
summary: '',
chapters: [],
};
novel.novelName = loadedCheerio('.truyen_info_right h1').text().trim();
novel.novelCover = loadedCheerio('.info_image img').attr('src');
novel.summary = loadedCheerio('#noidungm').text()?.trim();
novel.author = loadedCheerio(
'#main_body > div.cotgiua > div.truyen_info_wrapper > div.truyen_info > div.entry-header > div.truyen_if_wrap > ul > li:nth-child(2) > a',
)
.text()
?.trim();
let status = loadedCheerio(
'#main_body > div.cotgiua > div.truyen_info_wrapper > div.truyen_info > div.entry-header > div.truyen_if_wrap > ul > li:nth-child(4) > a',
)
.text()
?.trim();
novel.status =
status === 'ONGOING'
? Status.ONGOING
: status === 'COMPLETED'
? Status.COMPLETED
: Status.UNKNOWN;
let novelChapters = [];
loadedCheerio('.chapter-list div.row').each(function () {
const chapterName = loadedCheerio(this).find('a').text().trim();
const releaseDate = loadedCheerio(this)
.find('span:nth-child(2)')
.text()
.trim();
const chapterUrl = loadedCheerio(this).find('a').attr('href');
novelChapters.push({chapterName, releaseDate, chapterUrl});
});
novel.chapters = novelChapters.reverse();
return novel;
};
const parseChapter = async (novelUrl, chapterUrl) => {
const url = chapterUrl;
const result = await fetch(url);
const body = await result.text();
let loadedCheerio = cheerio.load(body);
const chapterName = loadedCheerio('h1.name_chapter').text();
const chapterText = loadedCheerio('#vung_doc').html();
const chapter = {
sourceId,
novelUrl,
chapterUrl,
chapterName,
chapterText,
};
return chapter;
};
const searchNovels = async searchTerm => {
const url = `${baseUrl}search_novels/${searchTerm}`;
const result = await fetch(url);
const body = await result.text();
let loadedCheerio = cheerio.load(body);
let novels = [];
loadedCheerio('.update_item.list_category').each(function () {
const novelName = loadedCheerio(this).find('h3 > a').text();
const novelCover = loadedCheerio(this).find('img').attr('src');
const novelUrl = loadedCheerio(this).find('h3 > a').attr('href');
const novel = {sourceId, novelName, novelCover, novelUrl};
novels.push(novel);
});
return novels;
};
const BestLightNovel = {
popularNovels,
parseNovelAndChapters,
parseChapter,
searchNovels,
};
export default BestLightNovel;
+161
View File
@@ -0,0 +1,161 @@
import cheerio from 'react-native-cheerio';
import {defaultCoverUri, Status} from '../helpers/constants';
const sourceId = 89;
const sourceName = 'IndoWebNovel';
const baseUrl = 'https://mtlcorner.com/';
const popularNovels = async page => {
const totalPages = 1;
const url = `${baseUrl}series/`;
const result = await fetch(url);
const body = await result.text();
const loadedCheerio = cheerio.load(body);
let novels = [];
loadedCheerio('.wp-block-latest-posts__list li').each(function () {
const novelName = loadedCheerio(this).find('a').text();
const novelCover = defaultCoverUri;
const novelUrl = loadedCheerio(this).find('a').attr('href');
const novel = {sourceId, novelName, novelCover, novelUrl};
novels.push(novel);
});
return {totalPages, novels};
};
const parseNovelAndChapters = async novelUrl => {
const url = novelUrl;
const result = await fetch(url);
const body = await result.text();
let loadedCheerio = cheerio.load(body);
let novel = {
sourceId,
sourceName,
url,
novelUrl,
novelName: '',
novelCover: '',
author: '',
status: '',
genre: '',
summary: '',
chapters: [],
};
const novelName = loadedCheerio('.entry-title').text().trim();
novel.novelName = novelName;
novel.novelCover = loadedCheerio('figure > img').attr('src');
novel.summary = loadedCheerio('figcaption').text().trim();
novel.status = Status.UNKNOWN;
novel.author = loadedCheerio(
' li.post-author.meta-wrapper > span.meta-text > a',
)
.text()
?.trim();
let chapters = [];
loadedCheerio('.lcp_catlist li').each(function () {
let chapterNameSuffix = loadedCheerio(this)
.find('a')
.text()
?.trim()
.match(/([a-zA-Z\s?]+)(\d+)-(\d+)/);
if (chapterNameSuffix) {
let chaptersTemp = [];
let chapterUrlSuffix = loadedCheerio(this).find('a').attr('href');
let i = chapterNameSuffix[2];
while (i <= chapterNameSuffix[3]) {
const chapterName = `${chapterNameSuffix[1].replace(
novelName,
'',
)} ${i}`;
const chapterUrl = `${chapterUrlSuffix}${i - chapterNameSuffix[2] + 1}`;
const releaseDate = null;
chaptersTemp.push({chapterName, releaseDate, chapterUrl});
i++;
}
chapters.push(...chaptersTemp.reverse());
}
});
novel.chapters = chapters.reverse();
return novel;
};
const parseChapter = async (novelUrl, chapterUrl) => {
const url = chapterUrl;
const result = await fetch(url);
const body = await result.text();
let loadedCheerio = cheerio.load(body);
const chapterName = '';
const chapterText = loadedCheerio('.entry-content').html();
const chapter = {
sourceId,
novelUrl,
chapterUrl,
chapterName,
chapterText,
};
return chapter;
};
const searchNovels = async searchTerm => {
const url = `${baseUrl}series/`;
const result = await fetch(url);
const body = await result.text();
const loadedCheerio = cheerio.load(body);
let novels = [];
loadedCheerio('.wp-block-latest-posts__list li').each(function () {
const novelName = loadedCheerio(this).find('a').text();
const novelCover = defaultCoverUri;
const novelUrl = loadedCheerio(this).find('a').attr('href');
const novel = {sourceId, novelName, novelCover, novelUrl};
if (novelName.toLowerCase().includes(searchTerm.toLowerCase())) {
novels.push(novel);
}
});
return novels;
};
const MTLCornerScraper = {
popularNovels,
parseNovelAndChapters,
parseChapter,
searchNovels,
};
export default MTLCornerScraper;
+4 -2
View File
@@ -53,7 +53,7 @@ const parseNovelAndChapters = async novelUrl => {
author: '',
status: Status.UNKNOWN,
genre: '',
summary: '',
summary: null,
chapters: [],
};
@@ -61,7 +61,9 @@ const parseNovelAndChapters = async novelUrl => {
novel.novelCover = loadedCheerio('img.bookimg').attr('src');
novel.summary = loadedCheerio('.intro').text().trim();
novel.summary = loadedCheerio('meta[name="description"]')
.attr('content')
.trim();
novel.author = loadedCheerio('#info > div.main > div.detail > p:nth-child(3)')
.text()
+166
View File
@@ -0,0 +1,166 @@
import cheerio from 'react-native-cheerio';
import {Status} from '../helpers/constants';
const sourceId = 90;
const sourceName = 'NitroScans';
const baseUrl = 'https://nitroscans.com/';
const popularNovels = async page => {
const totalPages = 2;
const url = baseUrl + 'manga-genre/novel/page/' + page;
const result = await fetch(url);
const body = await result.text();
const loadedCheerio = cheerio.load(body);
let novels = [];
loadedCheerio('.page-item-detail').each(function () {
const novelName = loadedCheerio(this).find('h3 > a').text();
const novelCover = loadedCheerio(this).find('img').attr('data-src');
const novelUrl = loadedCheerio(this).find('h3 > a').attr('href');
const novel = {sourceId, novelName, novelCover, novelUrl};
novels.push(novel);
});
return {totalPages, novels};
};
const parseNovelAndChapters = async novelUrl => {
const url = novelUrl;
const result = await fetch(url);
const body = await result.text();
let loadedCheerio = cheerio.load(body);
let novel = {
sourceId,
sourceName,
url,
novelUrl,
novelName: '',
novelCover: '',
author: '',
status: Status.UNKNOWN,
genre: '',
summary: '',
chapters: [],
};
loadedCheerio('.manga-title-badges.custom.novel').remove();
novel.novelName = loadedCheerio('.post-title > h1').text().trim();
novel.novelCover = loadedCheerio('.summary_image')
.find('img')
.attr('data-src');
novel.summary = loadedCheerio('.summary__content').text()?.trim();
loadedCheerio('.post-content_item').each(function () {
const detailName = loadedCheerio(this)
.find('.summary-heading > h5')
.text()
.trim();
const detail = loadedCheerio(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.includes('OnGoing')
? Status.ONGOING
: Status.COMPLETED;
break;
}
});
let novelChapters = [];
const novelId = loadedCheerio('.rating-post-id').attr('value');
let formData = new FormData();
formData.append('action', 'manga_get_chapters');
formData.append('manga', novelId);
const data = await fetch(baseUrl + 'wp-admin/admin-ajax.php', {
method: 'POST',
body: formData,
});
const text = await data.text();
loadedCheerio = cheerio.load(text);
loadedCheerio('.wp-manga-chapter').each(function () {
const chapterName = loadedCheerio(this).find('a').text().trim();
const releaseDate = loadedCheerio(this).find('span').text().trim();
const chapterUrl = loadedCheerio(this).find('a').attr('href');
novelChapters.push({chapterName, releaseDate, chapterUrl});
});
novel.chapters = novelChapters.reverse();
return novel;
};
const parseChapter = async (novelUrl, chapterUrl) => {
const url = chapterUrl;
const result = await fetch(url);
const body = await result.text();
let loadedCheerio = cheerio.load(body);
const chapterName = loadedCheerio('h1#chapter-heading').text();
const chapterText = loadedCheerio('.reading-content').html();
const chapter = {
sourceId,
novelUrl,
chapterUrl,
chapterName,
chapterText,
};
return chapter;
};
const searchNovels = async searchTerm => {
const url = `${baseUrl}?s=${searchTerm}&post_type=wp-manga&genre%5B%5D=novel&op=&author=&artist=&release=&adult=`;
const result = await fetch(url);
const body = await result.text();
let loadedCheerio = cheerio.load(body);
let novels = [];
loadedCheerio('.c-tabs-item__content').each(function () {
const novelName = loadedCheerio(this).find('h3 > a').text();
const novelCover = loadedCheerio(this).find('img').attr('data-src');
const novelUrl = loadedCheerio(this).find('h3 > a').attr('href');
const novel = {sourceId, novelName, novelCover, novelUrl};
novels.push(novel);
});
return novels;
};
const NitroScansScraper = {
popularNovels,
parseNovelAndChapters,
parseChapter,
searchNovels,
};
export default NitroScansScraper;
+177
View File
@@ -0,0 +1,177 @@
import cheerio from 'react-native-cheerio';
const sourceId = 92;
const sourceName = 'NovelForest';
const baseUrl = 'https://novelforest.com/';
const popularNovels = async page => {
const totalPages = 25;
const url = `${baseUrl}popular?page=${page}`;
const result = await fetch(url);
const body = await result.text();
const loadedCheerio = cheerio.load(body);
let novels = [];
loadedCheerio('.book-item').each(function () {
const novelName = loadedCheerio(this).find('.title').text();
const novelCover =
'https:' + loadedCheerio(this).find('img').attr('data-src');
const novelUrl =
baseUrl + loadedCheerio(this).find('.title a').attr('href').substring(1);
const novel = {sourceId, novelName, novelCover, novelUrl};
novels.push(novel);
});
return {totalPages, novels};
};
const parseNovelAndChapters = async novelUrl => {
const url = novelUrl;
const result = await fetch(url);
const body = await result.text();
let loadedCheerio = cheerio.load(body);
let novel = {
sourceId,
sourceName,
url,
novelUrl,
novelName: '',
novelCover: '',
author: '',
status: '',
genre: '',
summary: '',
chapters: [],
};
novel.novelName = loadedCheerio('.name h1').text().trim();
novel.novelCover =
'https:' + loadedCheerio('.img-cover img').attr('data-src');
novel.summary = loadedCheerio(
'body > div.layout > div.main-container.book-details > div > div.row.no-gutters > div.col-lg-8 > div.mt-1 > div.section.box.mt-1.summary > div.section-body > p.content',
)
.text()
.trim();
novel.author = loadedCheerio(
'body > div.layout > div.main-container.book-details > div > div.row.no-gutters > div.col-lg-8 > div.book-info > div.detail > div.meta.box.mt-1.p-10 > p:nth-child(1) > a > span',
)
.text()
?.trim();
novel.status = loadedCheerio(
'body > div.layout > div.main-container.book-details > div > div.row.no-gutters > div.col-lg-8 > div.book-info > div.detail > div.meta.box.mt-1.p-10 > p:nth-child(2) > a > span',
)
.text()
?.trim();
novel.genre = loadedCheerio(
'body > div.layout > div.main-container.book-details > div > div.row.no-gutters > div.col-lg-8 > div.book-info > div.detail > div.meta.box.mt-1.p-10 > p:nth-child(3)',
)
.text()
?.replace('Genres :', '')
.replace(/[\s\n]+/g, ' ')
.trim();
let chapters = [];
const chaptersUrl =
novelUrl.replace(baseUrl, 'https://novelforest.com/api/novels/') +
'/chapters?source=detail';
const chaptersRequest = await fetch(chaptersUrl);
const chaptersHtml = await chaptersRequest.text();
loadedCheerio = cheerio.load(chaptersHtml);
loadedCheerio('li').each(function () {
const chapterName = loadedCheerio(this)
.find('.chapter-title')
.text()
.trim();
const releaseDate = loadedCheerio(this)
.find('.chapter-update')
.text()
.trim();
const chapterUrl =
baseUrl + loadedCheerio(this).find('a').attr('href').substring(1);
chapters.push({chapterName, releaseDate, chapterUrl});
});
novel.chapters = chapters.reverse();
return novel;
};
const parseChapter = async (novelUrl, chapterUrl) => {
const url = chapterUrl;
const result = await fetch(url);
const body = await result.text();
let loadedCheerio = cheerio.load(body);
loadedCheerio('#listen-chapter').remove();
loadedCheerio('#google_translate_element').remove();
const chapterName = loadedCheerio('#chapter__content > h1').text();
const chapterText = loadedCheerio('.chapter__content').html();
const chapter = {
sourceId,
novelUrl,
chapterUrl,
chapterName,
chapterText,
};
return chapter;
};
const searchNovels = async searchTerm => {
const url = `${baseUrl}search?q=${searchTerm}`;
const result = await fetch(url);
const body = await result.text();
let loadedCheerio = cheerio.load(body);
let novels = [];
loadedCheerio('.book-item').each(function () {
const novelName = loadedCheerio(this).find('.title').text();
const novelCover =
'https:' + loadedCheerio(this).find('img').attr('data-src');
const novelUrl =
baseUrl + loadedCheerio(this).find('.title a').attr('href').substring(1);
const novel = {sourceId, novelName, novelCover, novelUrl};
novels.push(novel);
});
return novels;
};
const NovelForestScraper = {
popularNovels,
parseNovelAndChapters,
parseChapter,
searchNovels,
};
export default NovelForestScraper;
+195
View File
@@ -0,0 +1,195 @@
import cheerio from 'react-native-cheerio';
const baseUrl = 'https://www.novelpub.com/';
const sourceName = 'NovelPub';
const sourceId = 94;
const popularNovels = async page => {
let totalPages = 40;
let url = baseUrl + 'browse/all/popular/all/' + page;
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 result = await fetch(url, {method: 'GET', headers: headers});
const body = await result.text();
const loadedCheerio = cheerio.load(body);
let novels = [];
loadedCheerio('.novel-item.ads').remove();
loadedCheerio('.novel-item').each(function () {
const novelName = loadedCheerio(this).find('.novel-title').text().trim();
const novelCover = loadedCheerio(this).find('img').attr('data-src');
const novelUrl =
baseUrl +
loadedCheerio(this).find('.novel-title > a').attr('href').substring(1);
const novel = {sourceId, novelName, novelCover, novelUrl};
novels.push(novel);
});
return {totalPages, novels};
};
const parseNovelAndChapters = async novelUrl => {
const url = novelUrl;
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 result = await fetch(url, {method: 'GET', headers: headers});
const body = await result.text();
let loadedCheerio = cheerio.load(body);
let novel = {url, novelUrl, sourceId, sourceName, genre: ''};
novel.novelName = loadedCheerio('h1.novel-title').text().trim();
novel.novelCover = loadedCheerio('figure.cover > img').attr('data-src');
loadedCheerio('div.categories > ul > li').each(function () {
novel.genre +=
loadedCheerio(this)
.text()
.replace(/[\t\n]/g, '') + ',';
});
loadedCheerio('div.header-stats > span').each(function () {
if (loadedCheerio(this).find('small').text() === 'Status') {
novel.status = loadedCheerio(this).find('strong').text();
}
});
novel.genre = novel.genre.slice(0, -1);
novel.author = loadedCheerio('.author > a > span').text();
novel.summary = loadedCheerio('.summary > .content').text().trim();
let novelChapters = [];
const delay = ms => new Promise(res => setTimeout(res, ms));
let lastPage = 1;
lastPage = loadedCheerio(
'#novel > header > div.header-body.container > div.novel-info > div.header-stats > span:nth-child(1) > strong',
)
.text()
?.trim();
lastPage = Math.ceil(lastPage / 100);
for (let i = 1; i <= lastPage; i++) {
const chaptersUrl = `${novelUrl}/chapters/page-${i}`;
const chaptersRequest = await fetch(chaptersUrl);
const chaptersHtml = await chaptersRequest.text();
loadedCheerio = cheerio.load(chaptersHtml);
loadedCheerio('.chapter-list li').each(function () {
const chapterName = loadedCheerio(this)
.find('.chapter-title')
.text()
.trim();
const releaseDate = loadedCheerio(this)
.find('.chapter-update')
.text()
.trim();
const chapterUrl =
baseUrl + loadedCheerio(this).find('a').attr('href').substring(1);
novelChapters.push({chapterName, releaseDate, chapterUrl});
});
delay(1000);
}
novel.chapters = novelChapters;
return novel;
};
const parseChapter = async (novelUrl, chapterUrl) => {
const url = chapterUrl;
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 result = await fetch(url, {method: 'GET', headers: headers});
const body = await result.text();
const loadedCheerio = cheerio.load(body);
const chapterName = loadedCheerio('h2').text();
const chapterText = loadedCheerio('#chapter-container').html();
const chapter = {sourceId, novelUrl, chapterUrl, chapterName, chapterText};
return chapter;
};
const searchNovels = async searchTerm => {
const url = `${baseUrl}lnwsearchlive?inputContent=${searchTerm}`;
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 result = await fetch(url, {method: 'GET', headers: headers});
const body = await result.text();
let loadedCheerio = cheerio.load(body);
let novels = [];
let results = JSON.parse(loadedCheerio('body').text());
loadedCheerio = cheerio.load(results.resultview);
loadedCheerio('.novel-item').each(function () {
const novelName = loadedCheerio(this).find('.novel-title').text().trim();
const novelCover = loadedCheerio(this).find('img').attr('src');
const novelUrl =
baseUrl + loadedCheerio(this).find('a').attr('href').substring(1);
const novel = {sourceId, novelName, novelCover, novelUrl};
novels.push(novel);
});
return novels;
};
const NovelPubScraper = {
popularNovels,
parseNovelAndChapters,
parseChapter,
searchNovels,
};
export default NovelPubScraper;
+6 -1
View File
@@ -39,7 +39,7 @@ const parseNovelAndChapters = async novelUrl => {
const result = await fetch(url);
const body = await result.text();
const loadedCheerio = cheerio.load(body);
let loadedCheerio = cheerio.load(body);
let novel = {};
@@ -93,6 +93,11 @@ const parseNovelAndChapters = async novelUrl => {
let novelChapters = [];
const data = await fetch(`${url}ajax/chapters/`, {method: 'POST'});
const text = await data.text();
loadedCheerio = cheerio.load(text);
loadedCheerio('.wp-manga-chapter').each(function () {
const chapterName = loadedCheerio(this)
.find('a')
+39 -2
View File
@@ -43,7 +43,7 @@ const parseNovelAndChapters = async novelUrl => {
const result = await fetch(url);
const body = await result.text();
const loadedCheerio = cheerio.load(body);
let loadedCheerio = cheerio.load(body);
let novel = {sourceId, sourceName, url, novelUrl};
@@ -78,9 +78,46 @@ const parseNovelAndChapters = async novelUrl => {
const releaseDate = loadedCheerio(this).find('small').text();
const chapterUrl = loadedCheerio(this).find('a').attr('href');
novelChapters.push({chapterName, releaseDate, chapterUrl});
if (chapterUrl && chapterName) {
novelChapters.push({chapterName, releaseDate, chapterUrl});
}
});
const delay = ms => new Promise(res => setTimeout(res, ms));
let page = 1;
let nextPageExists = loadedCheerio('.next.page-numbers').length;
while (nextPageExists) {
const chaptersUrl = `${url}page/${++page}/#chapter`;
const chaptersRequest = await fetch(chaptersUrl);
const chaptersHtml = await chaptersRequest.text();
loadedCheerio = cheerio.load(chaptersHtml);
nextPageExists = loadedCheerio('.next.page-numbers').length;
loadedCheerio('#chapter')
.find('li')
.each(function () {
const chapterName = loadedCheerio(this)
.find('.chapter')
.first()
.text()
.trim();
const releaseDate = loadedCheerio(this).find('small').text();
const chapterUrl = loadedCheerio(this).find('a').attr('href');
if (chapterUrl && chapterName) {
novelChapters.push({chapterName, releaseDate, chapterUrl});
}
});
delay(1000);
}
novel.chapters = novelChapters;
return novel;
+1 -1
View File
@@ -10,7 +10,7 @@ const searchUrl = 'https://www.readlightnovel.me/detailed-search';
const popularNovels = async page => {
let totalPages = 1751;
const url = `${baseUrl}/top-novel/${page}`;
const url = `${baseUrl}/top-novels/most-viewed/${page}`;
const result = await fetch(url);
const body = await result.text();
-202
View File
@@ -1,202 +0,0 @@
import QueryString from 'qs';
import cheerio from 'react-native-cheerio';
const sourceId = 68;
const sourceName = 'Readwn.com';
const baseUrl = 'https://www.readwn.com/';
const popularNovels = async page => {
let url = `${baseUrl}list/all/all-onclick-${page - 1}.html`;
const totalPages = 281;
const headers = new Headers({
'user-agent':
'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/92.0.4515.131 Safari/537.36',
});
const result = await fetch(url, {method: 'GET', headers});
const body = await result.text();
// console.log(body);
const loadedCheerio = cheerio.load(body);
let novels = [];
loadedCheerio('li.novel-item').each(function () {
const novelName = loadedCheerio(this).find('h4').text();
const novelUrl = loadedCheerio(this)
.find('a')
.attr('href')
.split('/')
.pop();
const novelCover =
baseUrl + loadedCheerio(this).find('.novel-cover > img').attr('data-src');
const novel = {
sourceId,
novelName,
novelCover,
novelUrl,
};
novels.push(novel);
});
return {novels, totalPages};
};
const parseNovelAndChapters = async novelUrl => {
const url = `${baseUrl}novel/${novelUrl}`;
const headers = new Headers({
'user-agent':
'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/92.0.4515.131 Safari/537.36',
});
const result = await fetch(url, {method: 'GET', headers});
const body = await result.text();
const loadedCheerio = cheerio.load(body);
let novel = {
sourceId,
sourceName,
url,
novelUrl,
};
novel.novelUrl = novelUrl;
novel.novelName = loadedCheerio('h1.novel-title').text();
novel.novelCover =
baseUrl + loadedCheerio('figure.cover > img').attr('data-src');
novel.summary = loadedCheerio('p.description').text().trim();
novel.genre = '';
loadedCheerio('div.categories > ul > li').each(function () {
novel.genre += loadedCheerio(this).text().trim() + ',';
});
loadedCheerio('div.header-stats > span').each(function () {
if (loadedCheerio(this).find('small').text() === 'Status') {
novel.status = loadedCheerio(this).find('strong').text();
}
});
novel.genre = novel.genre.slice(0, -1);
novel.author = loadedCheerio('span[itemprop=author]').text();
let novelChapters = [];
const novelId = novelUrl.split('/').pop().replace('.html', '');
const latestChapterNumber = loadedCheerio('.header-stats')
.find('span > strong')
.first()
.text()
.trim();
for (let i = 1; i <= latestChapterNumber; i++) {
const chapterName = `Chapter ${i}`;
const chapterUrl = `${novelId}_${i}`;
const releaseDate = null;
const chapter = {chapterName, releaseDate, chapterUrl};
novelChapters.push(chapter);
}
novel.chapters = novelChapters;
return novel;
};
const parseChapter = async (novelUrl, chapterUrl) => {
const url = `${baseUrl}novel/${chapterUrl}.html`;
const headers = new Headers({
'user-agent':
'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/92.0.4515.131 Safari/537.36',
});
const result = await fetch(url, {method: 'GET', headers});
const body = await result.text();
const loadedCheerio = cheerio.load(body);
const chapterName = loadedCheerio('.titles > h2').text();
let chapterText = loadedCheerio('.chapter-content').html();
const chapter = {
sourceId,
novelUrl,
chapterUrl,
chapterName,
chapterText,
};
return chapter;
};
const searchNovels = async searchTerm => {
const searchUrl = 'https://www.readwn.com/e/search/index.php';
const result = await fetch(searchUrl, {
headers: {
'Content-Type': 'application/x-www-form-urlencoded',
Referer: 'https://www.readwn.com/search.html',
Origin: 'https://www.readwn.com',
'user-agent':
'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/92.0.4515.131 Safari/537.36',
},
method: 'POST',
body: QueryString.stringify({
show: 'title',
tempid: 1,
tbname: 'news',
keyboard: searchTerm,
}),
});
const body = await result.text();
const loadedCheerio = cheerio.load(body);
let novels = [];
loadedCheerio('li.novel-item').each(function () {
const novelName = loadedCheerio(this).find('h4').text();
const novelCover = baseUrl + loadedCheerio(this).find('img').attr('src');
const novelUrl = loadedCheerio(this)
.find('a')
.attr('href')
.split('/')
.pop();
const novel = {
sourceId,
novelName,
novelCover,
novelUrl,
};
novels.push(novel);
});
return novels;
};
const ReadwnScraper = {
popularNovels,
parseNovelAndChapters,
parseChapter,
searchNovels,
};
export default ReadwnScraper;
+5 -14
View File
@@ -62,15 +62,13 @@ const parseNovelAndChapters = async novelUrl => {
loadedCheerio('div.flex.flex-col.mt-3.text-center > ul > li').each(
function () {
novel.genre +=
loadedCheerio(this)
.text()
.replace(/[\t\n]/g, '') + ',';
novel.genre += loadedCheerio(this).text().trim() + ',';
},
);
loadedCheerio('div.header-stats > span').each(function () {
if (loadedCheerio(this).find('small').text() === 'Status') {
novel.status = loadedCheerio(this).find('strong').text();
novel.status = loadedCheerio(this).find('strong').text().trim();
}
});
@@ -104,15 +102,8 @@ const parseChapter = async (novelUrl, chapterUrl) => {
const loadedCheerio = cheerio.load(body);
const chapterId = loadedCheerio('input[name=id]').attr('value');
const url = 'https://travistranslations.com/wp-json/wp/v2/posts/' + chapterId;
const data = await fetch(url);
body = await data.json();
let chapterName = body.title.rendered;
let chapterText = body.content.rendered;
let chapterName = loadedCheerio('.header').text().trim();
let chapterText = loadedCheerio('.reader-content').html();
const chapter = {
sourceId,
+8 -3
View File
@@ -41,7 +41,7 @@ const parseNovelAndChapters = async novelUrl => {
const result = await fetch(url);
const body = await result.text();
const loadedCheerio = cheerio.load(body);
let loadedCheerio = cheerio.load(body);
let novel = {};
@@ -96,6 +96,11 @@ const parseNovelAndChapters = async novelUrl => {
let novelChapters = [];
const data = await fetch(`${url}ajax/chapters/`, {method: 'POST'});
const text = await data.text();
loadedCheerio = cheerio.load(text);
loadedCheerio('.wp-manga-chapter').each(function () {
const chapterName = loadedCheerio(this)
.find('a')
@@ -165,10 +170,10 @@ const searchNovels = async searchTerm => {
let novels = [];
loadedCheerio('.c-tabs-item__content').each(function () {
const novelName = loadedCheerio(this).find('h4 > a').text();
const novelName = loadedCheerio(this).find('.post-title a').text();
const novelCover = loadedCheerio(this).find('img').attr('src');
let novelUrl = loadedCheerio(this).find('h4 > a').attr('href');
let novelUrl = loadedCheerio(this).find('.post-title a').attr('href');
novelUrl = novelUrl.replace(`${baseUrl}vipnovel/`, '');
const novel = {
+131
View File
@@ -0,0 +1,131 @@
import cheerio from 'react-native-cheerio';
const sourceId = 88;
const sourceName = 'Chireads';
const baseUrl = 'https://chireads.com/';
const popularNovels = async page => {
const totalPages = 2;
const url = `${baseUrl}category/translatedtales/page/${page}/`;
const result = await fetch(url);
const body = await result.text();
const loadedCheerio = cheerio.load(body);
let novels = [];
loadedCheerio('#content li').each(function () {
const novelName = loadedCheerio(this).find('.news-list-tit h5').text();
const novelCover = loadedCheerio(this).find('img').attr('src');
const novelUrl = loadedCheerio(this)
.find('.news-list-tit h5 a')
.attr('href');
const novel = {sourceId, novelName, novelCover, novelUrl};
novels.push(novel);
});
return {totalPages, novels};
};
const parseNovelAndChapters = async novelUrl => {
const url = novelUrl;
const result = await fetch(url);
const body = await result.text();
let loadedCheerio = cheerio.load(body);
let novel = {
sourceId,
sourceName,
url,
novelUrl,
novelName: '',
novelCover: '',
author: '',
status: '',
genre: '',
summary: '',
chapters: [],
};
novel.novelName = loadedCheerio('.inform-title').text().trim();
novel.novelCover = loadedCheerio('.inform-product img').attr('src');
novel.summary = loadedCheerio('.inform-inform-txt').text().trim();
let chapters = [];
loadedCheerio('.chapitre-table a').each(function () {
const chapterName = loadedCheerio(this).text().trim();
const releaseDate = null;
const chapterUrl = loadedCheerio(this).attr('href');
chapters.push({chapterName, releaseDate, chapterUrl});
});
novel.chapters = chapters;
return novel;
};
const parseChapter = async (novelUrl, chapterUrl) => {
const url = chapterUrl;
const result = await fetch(url);
const body = await result.text();
let loadedCheerio = cheerio.load(body);
const chapterName = loadedCheerio('.article-title').text();
const chapterText = loadedCheerio('#content').html();
const chapter = {
sourceId,
novelUrl,
chapterUrl,
chapterName,
chapterText,
};
return chapter;
};
const searchNovels = async searchTerm => {
const url = `${baseUrl}search?x=0&y=0&name=${searchTerm}`;
const result = await fetch(url);
const body = await result.text();
let loadedCheerio = cheerio.load(body);
let novels = [];
loadedCheerio('#content li').each(function () {
const novelName = loadedCheerio(this).find('.news-list-tit h5').text();
const novelCover = loadedCheerio(this).find('img').attr('src');
const novelUrl = loadedCheerio(this)
.find('.news-list-tit h5 a')
.attr('href');
const novel = {sourceId, novelName, novelCover, novelUrl};
novels.push(novel);
});
return novels;
};
const ChireadsScraper = {
popularNovels,
parseNovelAndChapters,
parseChapter,
searchNovels,
};
export default ChireadsScraper;
+143
View File
@@ -0,0 +1,143 @@
import cheerio from 'react-native-cheerio';
const sourceId = 103;
const sourceName = 'SakuraNovel';
const baseUrl = 'https://sakuranovel.id/';
const popularNovels = async page => {
const totalPages = 8;
const url = `${baseUrl}advanced-search/page/${page}/?title&author&yearx&status&type&order=rating&country%5B0%5D=china&country%5B1%5D=jepang&country%5B2%5D=unknown`;
const result = await fetch(url);
const body = await result.text();
const loadedCheerio = cheerio.load(body);
let novels = [];
loadedCheerio('.flexbox2-item').each(function () {
const novelName = loadedCheerio(this)
.find('.flexbox2-title span')
.first()
.text();
const novelCover = loadedCheerio(this).find('img').attr('data-src');
const novelUrl = loadedCheerio(this)
.find('.flexbox2-content > a')
.attr('href');
const novel = {sourceId, novelName, novelCover, novelUrl};
novels.push(novel);
});
return {totalPages, novels};
};
const parseNovelAndChapters = async novelUrl => {
const result = await fetch(novelUrl);
const body = await result.text();
let loadedCheerio = cheerio.load(body);
let novel = {
sourceId,
sourceName,
url: novelUrl,
novelUrl,
novelName: '',
novelCover: '',
author: null,
status: null,
genre: null,
summary: null,
chapters: [],
};
novel.novelName = loadedCheerio('.series-title h2').text().trim();
novel.novelCover = loadedCheerio('.series-thumb img').attr('data-src');
novel.summary = loadedCheerio('.series-synops').text().trim();
let chapters = [];
loadedCheerio('.series-chapterlist li').each(function () {
const chapterName = loadedCheerio(this)
.find('a span')
.first()
.text()
.replace(`${novel.novelName} – `, '')
.replace('Bahasa Indonesia', '')
.trim();
const releaseDate = loadedCheerio(this)
.find('a span')
.first()
.next()
.text();
const chapterUrl = loadedCheerio(this).find('a').attr('href');
chapters.push({chapterName, releaseDate, chapterUrl});
});
novel.chapters = chapters.reverse();
return novel;
};
const parseChapter = async (novelUrl, chapterUrl) => {
const result = await fetch(chapterUrl);
const body = await result.text();
let loadedCheerio = cheerio.load(body);
const chapterName = loadedCheerio('.title-chapter').text();
const chapterText = loadedCheerio('.reader-areas').html();
const chapter = {
sourceId,
novelUrl,
chapterUrl,
chapterName,
chapterText,
};
return chapter;
};
const searchNovels = async searchTerm => {
const url = `${baseUrl}?s=${searchTerm}`;
const result = await fetch(url);
const body = await result.text();
let loadedCheerio = cheerio.load(body);
let novels = [];
loadedCheerio('.flexbox2-item').each(function () {
const novelName = loadedCheerio(this)
.find('.flexbox2-title span')
.first()
.text();
const novelCover = loadedCheerio(this).find('img').attr('data-src');
const novelUrl = loadedCheerio(this)
.find('.flexbox2-content > a')
.attr('href');
const novel = {sourceId, novelName, novelCover, novelUrl};
novels.push(novel);
});
return novels;
};
const SakuraNovelScraper = {
popularNovels,
parseNovelAndChapters,
parseChapter,
searchNovels,
};
export default SakuraNovelScraper;
+39 -2
View File
@@ -23,6 +23,8 @@ const getPath = extensionId => {
63: {novels: 'novel', novel: 'novel', chapter: 'novel'},
75: {novels: 'series', novel: 'series', chapter: 'series'},
80: {novels: 'manga', novel: 'manga', chapter: 'manga'},
91: {novels: 'novel', novel: 'novel', chapter: 'novel'},
96: {novels: 'serie', novel: 'serie', chapter: 'serie'},
};
return (
@@ -134,6 +136,7 @@ export const LightNovelHeavenScraper = new MadaraScraper(
'https://lightnovelheaven.com/',
'LightNovelHeaven',
getPath(57),
true,
);
export const LightNovelsHubScraper = new MadaraScraper(
@@ -162,8 +165,8 @@ export const WebNovelLoverScraper = new MadaraScraper(
export const BoxNovelOnlineScraper = new MadaraScraper(
63,
'https://boxnovel.online/',
'BoxNovel.online',
'https://tipnovel.com/',
'TipNovel',
getPath(63),
true,
);
@@ -211,3 +214,37 @@ export const MoreNovelScraper = new MadaraScraper(
getPath(84),
true,
);
export const OnlyMTLScraper = new MadaraScraper(
91,
'https://www.onlymtl.com/',
'OnlyMTL',
getPath(91),
);
export const HizoMangaScraper = new MadaraScraper(
96,
'https://hizomanga.com/',
'HizoManga',
getPath(96),
true,
56,
);
export const LatestNovelScraper = new MadaraScraper(
98,
'https://latestnovel.net/',
'LatestNovel',
getPath(98),
false,
130,
);
export const ArMTLScraper = new MadaraScraper(
101,
'https://ar-mtl.club/',
'ArMTL',
getPath(101),
true,
1,
);
+9 -4
View File
@@ -94,15 +94,19 @@ class MadaraScraper {
switch (detailName) {
case 'Genre(s)':
case 'التصنيفات':
novel.genre = detail.replace(/[\t\n]/g, ',');
break;
case 'Author(s)':
case 'المؤلف':
novel.author = detail;
break;
case 'Status':
novel.status = detail.includes('OnGoing')
? Status.ONGOING
: Status.COMPLETED;
case 'الحالة':
novel.status =
detail.includes('OnGoing') || detail.includes('مستمرة')
? Status.ONGOING
: Status.COMPLETED;
break;
}
});
@@ -187,7 +191,8 @@ class MadaraScraper {
loadedCheerio('.text-center').text() ||
loadedCheerio('#chapter-heading').text();
let chapterText = loadedCheerio('.text-left').html();
let chapterText =
loadedCheerio('.text-left').html() || loadedCheerio('.text-right').html();
const chapter = {
sourceId,
@@ -0,0 +1,21 @@
import MultiSrcScraper from './ReadwnScraper';
const ReadwnScraper = new MultiSrcScraper(
68,
'https://www.readwn.com/',
'Readwn.com',
);
const NovelmtScraper = new MultiSrcScraper(
99,
'https://www.novelmt.com/',
'Novelmt.com',
);
const LtnovelScraper = new MultiSrcScraper(
100,
'https://www.ltnovel.com/',
'Ltnovel.com',
);
export {ReadwnScraper, NovelmtScraper, LtnovelScraper};
@@ -0,0 +1,185 @@
import cheerio from 'react-native-cheerio';
import QueryString from 'qs';
class ReadwnScraper {
constructor(sourceId, baseUrl, sourceName) {
this.sourceId = sourceId;
this.baseUrl = baseUrl;
this.sourceName = sourceName;
}
async popularNovels(page) {
const baseUrl = this.baseUrl;
const sourceId = this.sourceId;
const pageNo = page - 1;
const url = `${baseUrl}list/all/all-onclick-${pageNo}.html`;
const result = await fetch(url);
const body = await result.text();
const loadedCheerio = cheerio.load(body);
let novels = [];
loadedCheerio('li.novel-item').each(function () {
const novelName = loadedCheerio(this).find('h4').text();
const novelUrl = baseUrl + loadedCheerio(this).find('a').attr('href');
const coverUri = loadedCheerio(this)
.find('.novel-cover > img')
.attr('data-src');
const novelCover = baseUrl + coverUri;
const novel = {sourceId, novelName, novelCover, novelUrl};
novels.push(novel);
});
return {totalPages: this.totalPages, novels};
}
async parseNovelAndChapters(novelUrl) {
const sourceId = this.sourceId;
const baseUrl = this.baseUrl;
const sourceName = this.sourceName;
const url = novelUrl;
const result = await fetch(url);
const body = await result.text();
let loadedCheerio = cheerio.load(body);
let novel = {
sourceId: sourceId,
sourceName: sourceName,
url,
novelUrl,
};
novel.novelName = loadedCheerio('h1.novel-title').text();
const coverUri = loadedCheerio('figure.cover > img').attr('data-src');
novel.novelCover = baseUrl + coverUri;
novel.summary = loadedCheerio('.summary')
.text()
.replace('Summary', '')
.trim();
novel.genre = '';
loadedCheerio('div.categories > ul > li').each(function () {
novel.genre += loadedCheerio(this).text().trim() + ',';
});
loadedCheerio('div.header-stats > span').each(function () {
if (loadedCheerio(this).find('small').text() === 'Status') {
novel.status = loadedCheerio(this).find('strong').text();
}
});
novel.genre = novel.genre.slice(0, -1);
novel.author = loadedCheerio('span[itemprop=author]').text();
let novelChapters = [];
const novelId = novelUrl.replace('.html', '');
const latestChapterNo = loadedCheerio('.header-stats')
.find('span > strong')
.first()
.text()
.trim();
for (let i = 1; i <= latestChapterNo; i++) {
const chapterName = `Chapter ${i}`;
const chapterUrl = `${novelId}_${i}.html`;
const releaseDate = null;
const chapter = {chapterName, releaseDate, chapterUrl};
novelChapters.push(chapter);
}
novel.chapters = novelChapters;
return novel;
}
async parseChapter(novelUrl, chapterUrl) {
const url = chapterUrl;
const sourceId = this.sourceId;
const result = await fetch(url);
const body = await result.text();
const loadedCheerio = cheerio.load(body);
const chapterName = loadedCheerio('.titles > h2').text();
const chapterText = loadedCheerio('.chapter-content').html();
const chapter = {
sourceId,
novelUrl,
chapterUrl,
chapterName,
chapterText,
};
return chapter;
}
async searchNovels(searchTerm) {
const baseUrl = this.baseUrl;
const sourceId = this.sourceId;
const searchUrl = `${baseUrl}e/search/index.php`;
const result = await fetch(searchUrl, {
headers: {
'Content-Type': 'application/x-www-form-urlencoded',
Referer: `${baseUrl}search.html`,
Origin: baseUrl,
'user-agent':
'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/92.0.4515.131 Safari/537.36',
},
method: 'POST',
body: QueryString.stringify({
show: 'title',
tempid: 1,
tbname: 'news',
keyboard: searchTerm,
}),
});
const body = await result.text();
const loadedCheerio = cheerio.load(body);
let novels = [];
loadedCheerio('li.novel-item').each(function () {
const novelName = loadedCheerio(this).find('h4').text();
const novelUrl = baseUrl + loadedCheerio(this).find('a').attr('href');
const coverUri = loadedCheerio(this).find('img').attr('data-src');
const novelCover = baseUrl + coverUri;
const novel = {
sourceId,
novelName,
novelCover,
novelUrl,
};
novels.push(novel);
});
return novels;
}
}
export default ReadwnScraper;
@@ -5,9 +5,3 @@ export const KolNovelScraper = new WPMangaStreamScraper(
'https://alightnovel.com/',
'KolNovel',
);
export const RewayatArScraper = new WPMangaStreamScraper(
54,
'https://rewayat-ar.site/',
'RewayatAr',
);
+30 -25
View File
@@ -90,24 +90,27 @@ const parseChapter = async (novelUrl, chapterUrl) => {
let chapterName = loadedCheerio(
'body > div.pusher.container_main > div:nth-child(5) > div:nth-child(1) > div.title-wrapper > h1',
).text();
loadedCheerio(
'body > div.pusher.container_main > div:nth-child(5) > div:nth-child(1) img',
).each(function () {
if (!loadedCheerio(this).attr('src')?.startsWith('http')) {
const dataMediaId = loadedCheerio(this).attr('data-media-id');
const newSrc = `https://ranobehub.org/api/media/${dataMediaId}`;
loadedCheerio(this).attr('src', newSrc);
}
});
let chapterText = loadedCheerio(
'body > div.pusher.container_main > div:nth-child(5) > div:nth-child(1)',
).html();
if (chapterText) {
/**
* Convert relative url to absolute
*/
chapterText = chapterText
.replace(/<\s*script[^>]*>[\s\S]*?<\/script>/gim, '')
.replace(
/data-src="(.*)"/g,
`src="https://ranobehub.org/img/ranobe/content/${chapterId}/$1.jpg"`,
);
}
// console.log(chapterText);
// Remove script tags
chapterText = chapterText?.replace(
/<\s*script[^>]*>[\s\S]*?<\/script>/gim,
'',
);
const chapter = {
sourceId,
@@ -121,23 +124,25 @@ const parseChapter = async (novelUrl, chapterUrl) => {
};
const searchNovels = async searchTerm => {
const url = 'https://ranobehub.org/api/fulltext/ranobe?query=' + searchTerm;
const url = `https://ranobehub.org/api/fulltext/global?query=${searchTerm}&take=5`;
const result = await fetch(url);
const data = await result.json();
let novels = [];
data.data.map(novel =>
novels.push({
sourceId,
novelName: novel.names.rus,
novelUrl: novel.url.match(
/https:\/\/ranobehub\.org\/ranobe\/(.*?)\?utm_source=search_name&utm_medium=search&utm_campaign=search_using/,
)[1],
novelCover: novel.image,
}),
);
data
.find(item => item.meta.key === 'ranobe')
?.data.map(novel =>
novels.push({
sourceId,
novelName: novel.names.rus,
novelUrl: novel.url.match(
/https:\/\/ranobehub\.org\/ranobe\/(.*?)\?utm_source=search_name&utm_medium=search&utm_campaign=search_using/,
)[1],
novelCover: novel.image,
}),
);
return novels;
};
+160
View File
@@ -0,0 +1,160 @@
import cheerio from 'react-native-cheerio';
const sourceId = 93;
const sourceName = 'RanobeLib';
const baseUrl = 'https://ranobelib.me/';
const popularNovels = async page => {
const totalPages = 29;
const url = `${baseUrl}manga-list?sort=rate&dir=desc&page=${page}`;
const result = await fetch(url);
const body = await result.text();
const loadedCheerio = cheerio.load(body);
let novels = [];
loadedCheerio('.media-card-wrap').each(function () {
const novelName = loadedCheerio(this).find('.media-card__title').text();
const novelCover = loadedCheerio(this)
.find('a.media-card')
.attr('data-src');
const novelUrl = loadedCheerio(this).find('a.media-card').attr('href');
const novel = {sourceId, novelName, novelCover, novelUrl};
novels.push(novel);
});
return {totalPages, novels};
};
const parseNovelAndChapters = async novelUrl => {
const result = await fetch(novelUrl);
const body = await result.text();
const loadedCheerio = cheerio.load(body);
let novel = {
sourceId,
sourceName,
url: novelUrl,
novelUrl,
novelName: '',
novelCover: '',
author: null,
status: null,
genre: null,
summary: null,
chapters: [],
};
novel.novelName = loadedCheerio('.media-name__main').text().trim();
novel.novelCover = loadedCheerio('.media-sidebar__cover img').attr('src');
novel.summary = loadedCheerio('.media-description__text').text().trim();
novel.author = loadedCheerio(
'#main-page > div > div.container.container_responsive > div > div.media-sidebar > div.media-info-list.paper > a:nth-child(4) > div.media-info-list__value',
)
.text()
.trim();
let chapters = [];
let chaptersJson = body.match(
/window.__DATA__ = [\s\S]*?window._SITE_COLOR_/gm,
);
chaptersJson = chaptersJson?.[0]
?.replace('window.__DATA__ = ', '')
?.replace('window._SITE_COLOR_', '')
?.trim()
?.slice(0, -1);
chaptersJson = JSON.parse(chaptersJson);
const novelSlug = chaptersJson.manga.slug;
chaptersJson.chapters.list.map(item => {
const chapterName =
`Том ${item.chapter_volume} Глава ${item.chapter_number} ${item.chapter_name}`?.trim();
const releaseDate = item.chapter_created_at;
const chapterUrl = `${baseUrl}${novelSlug}/v${item.chapter_volume}/c${item.chapter_number}`;
chapters.push({chapterName, releaseDate, chapterUrl});
});
novel.chapters = chapters.reverse();
return novel;
};
const parseChapter = async (novelUrl, chapterUrl) => {
const result = await fetch(chapterUrl);
const body = await result.text();
const loadedCheerio = cheerio.load(body);
loadedCheerio('.reader-container img').each(function () {
if (!loadedCheerio(this).attr('src')?.startsWith('http')) {
const src = loadedCheerio(this).attr('src');
loadedCheerio(this).attr('src', `http://ranobelib.me${src}`);
}
});
const chapterName = loadedCheerio(
'body > div.reader.reader_text > div.header.header_reader.headroom.headroom--top.headroom--not-bottom > div > a > div:nth-child(3)',
)
.text()
.trim();
const chapterText = loadedCheerio('.reader-container').html();
const chapter = {
sourceId,
novelUrl,
chapterUrl,
chapterName,
chapterText,
};
return chapter;
};
const searchNovels = async searchTerm => {
const url = `${baseUrl}manga-list?sort=rate&dir=desc&page=1&name=${searchTerm}`;
const result = await fetch(url);
const body = await result.text();
let loadedCheerio = cheerio.load(body);
let novels = [];
loadedCheerio('.media-card-wrap').each(function () {
const novelName = loadedCheerio(this).find('.media-card__title').text();
const novelCover = loadedCheerio(this)
.find('a.media-card')
.attr('data-src');
const novelUrl = loadedCheerio(this).find('a.media-card').attr('href');
const novel = {sourceId, novelName, novelCover, novelUrl};
novels.push(novel);
});
return novels;
};
const RanobeLibScraper = {
popularNovels,
parseNovelAndChapters,
parseChapter,
searchNovels,
};
export default RanobeLibScraper;
+35 -5
View File
@@ -33,12 +33,15 @@ import ScribbleHubScraper from './en/scribblehub';
import SyosetuScraper from './jp/syosetu';
import LNMTLScraper from './en/lnmtl';
import {
ArMTLScraper,
BoxNovelOnlineScraper,
BoxNovelScraper,
ClickNovelScraper,
DaoNovelScraper,
FirstKissNovelScraper,
FreeNovelScraper,
HizoMangaScraper,
LatestNovelScraper,
LightNovelHeavenScraper,
LightNovelsHubScraper,
LunarLettersScraper,
@@ -50,6 +53,7 @@ import {
NovelMultiverseScraper,
NovelsRockScraper,
NovelTranslateScraper,
OnlyMTLScraper,
ReadWebNovelsScraper,
SkyNovelScraper,
SleepyTranslationsScraper,
@@ -63,13 +67,9 @@ 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';
import {KolNovelScraper} from './multisrc/wpmangastream/WPMangaStreamGenerator';
import WLNUpdatesScraper from './en/wlnupdates';
import ReaperScansScraper from './en/reaperscans';
import ReadwnScraper from './en/readwn';
import RanobeHubScraper from './ru/ranobehub';
import ArNovelScraper from './ar/arnovel';
import DivineDaoLibraryScraper from './en/divinedaolibrary';
@@ -88,6 +88,21 @@ import MyLoveNovelScraper from './en/mylovenovel';
import RiwayatScraper from './ar/riwayat';
import NovelRinganScraper from './id/novelringan';
import IndoWebNovelScraper from './id/indowebnovel';
import ChireadsScraper from './fr/chireads';
import RewayatArScraper from './ar/rewayatar';
import MTLCornerScraper from './en/mtlcorner';
import NitroScansScraper from './en/nitroscans';
import NovelForestScraper from './en/novelforest';
import RanobeLibScraper from './ru/ranobelib';
import NovelPubScraper from './en/novelpub';
import BestLightNovel from './en/bestlightnovel';
import NovelFullMeScraper from './en/NovelFullMe';
import {
LtnovelScraper,
NovelmtScraper,
ReadwnScraper,
} from './multisrc/readwn/ReadwnGenerator';
import SakuraNovelScraper from './id/sakuranovel';
export const getSource = sourceId => {
const scrapers = {
@@ -177,6 +192,21 @@ export const getSource = sourceId => {
85: RiwayatScraper,
86: NovelRinganScraper,
87: IndoWebNovelScraper,
88: ChireadsScraper,
89: MTLCornerScraper,
90: NitroScansScraper,
91: OnlyMTLScraper,
92: NovelForestScraper,
93: RanobeLibScraper,
94: NovelPubScraper,
95: BestLightNovel,
96: HizoMangaScraper,
97: NovelFullMeScraper,
98: LatestNovelScraper,
99: NovelmtScraper,
100: LtnovelScraper,
101: ArMTLScraper,
103: SakuraNovelScraper,
};
return scrapers[sourceId];
+106 -1
View File
@@ -465,7 +465,7 @@
"sourceId": 68,
"url": "https://www.readwn.com/",
"sourceName": "Readwn.com",
"icon": "https://github.com/LNReader/lnreader-sources/blob/main/src/en/readwn/icon.png?raw=true",
"icon": "https://github.com/LNReader/lnreader-sources/blob/main/multisrc/readwn/icons/readwn.png?raw=true",
"lang": "English"
},
{
@@ -600,5 +600,110 @@
"sourceName": "IndoWebNovel",
"icon": "https://github.com/LNReader/lnreader-sources/blob/main/src/id/indowebnovel/icon.png?raw=true",
"lang": "Indonesian"
},
{
"sourceId": 88,
"url": "https://chireads.com/",
"sourceName": "Chireads",
"icon": "https://github.com/LNReader/lnreader-sources/blob/main/src/fr/chireads/icon.png?raw=true",
"lang": "French"
},
{
"sourceId": 89,
"url": "https://mtlcorner.com/",
"sourceName": "MTLCorner",
"icon": "https://github.com/LNReader/lnreader-sources/blob/main/src/en/mtlcorner/icon.png?raw=true",
"lang": "English"
},
{
"sourceId": 90,
"url": "https://nitroscans.com/",
"sourceName": "NitroScans",
"icon": "https://github.com/LNReader/lnreader-sources/blob/main/src/en/nitroscans/icon.png?raw=true",
"lang": "English"
},
{
"sourceId": 91,
"url": "https://www.onlymtl.com/",
"sourceName": "OnlyMTL",
"icon": "https://github.com/LNReader/lnreader-sources/blob/main/multisrc/madara/icons/onlymtl.png?raw=true",
"lang": "English"
},
{
"sourceId": 92,
"url": "https://novelforest.com/",
"sourceName": "NovelForest",
"icon": "https://github.com/LNReader/lnreader-sources/blob/main/src/en/novelforest/icon.png?raw=true",
"lang": "English"
},
{
"sourceId": 93,
"url": "https://ranobelib.me/",
"sourceName": "RanobeLib",
"icon": "https://github.com/LNReader/lnreader-sources/blob/main/src/ru/ranobelib/icon.png?raw=true",
"lang": "Russian"
},
{
"sourceId": 94,
"url": "https://www.novelpub.com/",
"sourceName": "NovelPub",
"icon": "https://github.com/LNReader/lnreader-sources/blob/main/src/en/novelpub/icon.png?raw=true",
"lang": "English"
},
{
"sourceId": 95,
"url": "https://www.bestlightnovel.com/",
"sourceName": "BestLightNovel",
"icon": "https://github.com/LNReader/lnreader-sources/blob/main/src/en/bestlightnovel/icon.png?raw=true",
"lang": "English"
},
{
"sourceId": 96,
"url": "https://hizomanga.com/",
"sourceName": "HizoManga",
"icon": "https://github.com/LNReader/lnreader-sources/blob/main/multisrc/madara/icons/hizomanga.png?raw=true",
"lang": "Arabic"
},
{
"sourceId": 97,
"url": "https://novelfull.me/",
"sourceName": "NovelFull.me",
"icon": "https://github.com/LNReader/lnreader-sources/blob/main/src/en/novelfullme/icon.png?raw=true",
"lang": "English"
},
{
"sourceId": 98,
"url": "https://latestnovel.net/",
"sourceName": "LatestNovel",
"icon": "https://github.com/LNReader/lnreader-sources/blob/main/multisrc/madara/icons/latestnovel.png?raw=true",
"lang": "English"
},
{
"sourceId": 99,
"url": "https://www.novelmt.com/",
"sourceName": "Novelmt.com",
"icon": "https://github.com/LNReader/lnreader-sources/blob/main/multisrc/readwn/icons/novelmt.png?raw=true",
"lang": "English"
},
{
"sourceId": 100,
"url": "https://www.ltnovel.com/",
"sourceName": "Ltnovel.com",
"icon": "https://github.com/LNReader/lnreader-sources/blob/main/multisrc/readwn/icons/ltnovel.png?raw=true",
"lang": "English"
},
{
"sourceId": 101,
"url": "https://ar-mtl.club/",
"sourceName": "ArMTL",
"icon": "https://github.com/LNReader/lnreader-sources/blob/main/multisrc/madara/icons/armtl.png?raw=true",
"lang": "Arabic"
},
{
"sourceId": 103,
"url": "https://sakuranovel.id/",
"sourceName": "SakuraNovel",
"icon": "https://github.com/LNReader/lnreader-sources/blob/main/src/id/sakuranovel/icon.png?raw=true",
"lang": "Indonesian"
}
]
+5
View File
@@ -0,0 +1,5 @@
const app_version = '1.1.7';
const release_dt = '01/11/21 10:00 AM';
export const releasedt = release_dt;
export const appversion = app_version;