Compare commits

...

36 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
52 changed files with 4032 additions and 1895 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';
+4
View File
@@ -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",
+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}`),
);
});
+3 -4
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,11 +23,11 @@ export const useGithubUpdateChecker = () => {
};
const isNewVersion = versionTag => {
let currentVersion = 'v1.1.7';
let currentVersion = `${appversion}`;
const regex = /[^\\d.]/;
let newVersion = versionTag.replace(regex, '');
currentVersion = currentVersion.replace(regex, '');
currentVersion = currentVersion;
return newVersion !== currentVersion;
};
+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,
// });
// }
// };
+11 -3
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});
@@ -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 -2
View File
@@ -93,13 +93,14 @@ const initialState = {
onlyUpdateOngoingNovels: false,
updateLibraryOnLaunch: false,
downloadNewChapters: false,
refreshNovelMetadata: false,
/**
* Novel settings
*/
hideBackdrop: false,
defaultChapterSort: null,
defaultChapterSort: 'ORDER BY chapterId ASC',
/**
* Reader settings
@@ -123,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());
};
+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.7 (01/11/21 10:00 AM)"
description={`Stable ${appversion} (${releasedt})`}
theme={theme}
/>
<List.Item
title="What's new"
onPress={() =>
Linking.openURL(
'https://github.com/LNReader/lnreader/releases/tag/v1.1.7',
`https://github.com/LNReader/lnreader/releases/tag/v${appversion}`,
)
}
theme={theme}
+90 -2
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;
@@ -245,6 +246,7 @@ const Novel = ({route, navigation}) => {
);
const [editInfoModal, showEditInfoModal] = useState(false);
const downloadCustomChapterModal = useModal();
return (
<Provider>
@@ -350,7 +352,7 @@ const Novel = ({route, navigation}) => {
}}
>
<Menu.Item
title="All"
title="Next chapter"
style={{backgroundColor: theme.menuColor}}
titleStyle={{
color: theme.textColorPrimary,
@@ -360,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}}
@@ -383,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}}
@@ -586,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}
@@ -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';
@@ -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;
+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"
@@ -29,6 +29,7 @@ const GenralSettings = ({navigation}) => {
downloadNewChapters = false,
onlyUpdateOngoingNovels = false,
defaultChapterSort = 'ORDER BY chapterId ASC',
refreshNovelMetadata = false,
} = useSettings();
const {showLastUpdateTime = true} = useSelector(
@@ -115,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}
+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,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,
},
});
-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};
+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;
+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()
+25 -30
View File
@@ -58,7 +58,11 @@ const parseNovelAndChapters = async novelUrl => {
novel.novelCover =
'https:' + loadedCheerio('.img-cover img').attr('data-src');
novel.summary = loadedCheerio('#info > p').text().trim();
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',
@@ -82,42 +86,33 @@ const parseNovelAndChapters = async novelUrl => {
let chapters = [];
const delay = ms => new Promise(res => setTimeout(res, ms));
const chaptersUrl =
novelUrl.replace(baseUrl, 'https://novelforest.com/api/novels/') +
'/chapters?source=detail';
const lastPage = loadedCheerio('label:contains("❯❯")').attr('id');
const chaptersRequest = await fetch(chaptersUrl);
const chaptersHtml = await chaptersRequest.text();
for (let i = 1; i <= lastPage; i++) {
const chaptersUrl =
novelUrl.replace(baseUrl, 'https://novelforest.com/api/novels/') +
'/chapters?page=' +
i;
loadedCheerio = cheerio.load(chaptersHtml);
const chaptersRequest = await fetch(chaptersUrl);
const chaptersJSON = await chaptersRequest.json();
loadedCheerio('li').each(function () {
const chapterName = loadedCheerio(this)
.find('.chapter-title')
.text()
.trim();
loadedCheerio = cheerio.load(chaptersJSON.chapters);
const releaseDate = loadedCheerio(this)
.find('.chapter-update')
.text()
.trim();
loadedCheerio('li').each(function () {
const chapterName = loadedCheerio(this)
.find('.chapter-title')
.text()
.trim();
const chapterUrl =
baseUrl + loadedCheerio(this).find('a').attr('href').substring(1);
const releaseDate = loadedCheerio(this)
.find('.chapter-update')
.text()
.trim();
chapters.push({chapterName, releaseDate, chapterUrl});
});
const chapterUrl =
baseUrl + loadedCheerio(this).find('a').attr('href').substring(1);
chapters.push({chapterName, releaseDate, chapterUrl});
delay(1000);
});
}
novel.chapters = chapters;
novel.chapters = chapters.reverse();
return novel;
};
+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;
+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,
+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;
+29 -1
View File
@@ -23,7 +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: 'manga', novel: 'manga', chapter: 'manga'},
91: {novels: 'novel', novel: 'novel', chapter: 'novel'},
96: {novels: 'serie', novel: 'serie', chapter: 'serie'},
};
return (
@@ -220,3 +221,30 @@ export const OnlyMTLScraper = new MadaraScraper(
'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;
+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;
+23 -1
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,
@@ -67,7 +70,6 @@ import YushuboScraper from './ch/yushubo';
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';
@@ -91,6 +93,16 @@ 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 = {
@@ -185,6 +197,16 @@ export const getSource = sourceId => {
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];
+71 -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"
},
{
@@ -635,5 +635,75 @@
"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;