Compare commits
59 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| f732162688 | |||
| 81a5ea0aa7 | |||
| 8be78f7e92 | |||
| bb144936b5 | |||
| 724089d940 | |||
| c22440195d | |||
| dbb20e666f | |||
| 0a7f2e8948 | |||
| a83cdcc66e | |||
| ceca91fcab | |||
| ecc8c35bfb | |||
| 3522b7e06d | |||
| ab4fc42f69 | |||
| 47676a0de5 | |||
| 7854b20b1f | |||
| a825db2965 | |||
| 57cc5a7f42 | |||
| 15cd0b7b6b | |||
| ca1303f422 | |||
| e79563f2d0 | |||
| ffe5b04360 | |||
| 1c59964a67 | |||
| 5cd08a0d29 | |||
| 4bc61f425b | |||
| 6d631b8921 | |||
| 70833ef37f | |||
| cc4c242224 | |||
| 35731ec38f | |||
| 3735e8ca1b | |||
| e0746ff092 | |||
| d0c478b89f | |||
| 3f29aee4e6 | |||
| 76df79d9ca | |||
| 3f874e6550 | |||
| 84ec20ffb3 | |||
| 65027ee41e | |||
| 3425892a16 | |||
| 97890c1596 | |||
| c97e1b8e23 | |||
| c44537a1c0 | |||
| fd8fb367bd | |||
| a3d8aa5bcd | |||
| e7a1f8210f | |||
| 9423054b10 | |||
| d11348d9a3 | |||
| 8ec90ae8f7 | |||
| 5fde669106 | |||
| c6cdf1da0e | |||
| 8cb0a5eb73 | |||
| 09e15607d5 | |||
| a9d399ea6d | |||
| b9eddc2cc2 | |||
| bb68b2f407 | |||
| 5e0d19a9c9 | |||
| 0aa78e56c8 | |||
| 078e311995 | |||
| 9a22eba72c | |||
| 2b0032a4e4 | |||
| 708225c855 |
@@ -1,3 +1,7 @@
|
||||
import {enableFreeze} from 'react-native-screens';
|
||||
|
||||
enableFreeze(true);
|
||||
|
||||
import React, {useEffect} from 'react';
|
||||
import {StatusBar} from 'react-native';
|
||||
|
||||
|
||||
@@ -136,7 +136,7 @@ android {
|
||||
minSdkVersion rootProject.ext.minSdkVersion
|
||||
targetSdkVersion rootProject.ext.targetSdkVersion
|
||||
versionCode 1
|
||||
versionName "1.1.6"
|
||||
versionName "1.1.7"
|
||||
}
|
||||
splits {
|
||||
abi {
|
||||
@@ -188,6 +188,10 @@ dependencies {
|
||||
//noinspection GradleDynamicVersion
|
||||
implementation "com.facebook.react:react-native:+" // From node_modules
|
||||
implementation "androidx.swiperefreshlayout:swiperefreshlayout:1.0.0"
|
||||
|
||||
implementation 'com.github.bumptech.glide:glide:4.12.0'
|
||||
annotationProcessor 'com.github.bumptech.glide:compiler:4.12.0'
|
||||
|
||||
debugImplementation("com.facebook.flipper:flipper:${FLIPPER_VERSION}") {
|
||||
exclude group:'com.facebook.fbjni'
|
||||
}
|
||||
|
||||
Vendored
+10
@@ -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 *;
|
||||
}
|
||||
@@ -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>
|
||||
|
||||
Generated
+1668
-991
File diff suppressed because it is too large
Load Diff
+32
-30
@@ -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",
|
||||
|
||||
@@ -72,13 +72,7 @@ const InfoItem = ({title, icon, theme}) => (
|
||||
titleStyle={{color: theme.textColorSecondary, fontSize: 14}}
|
||||
titleNumberOfLines={5}
|
||||
left={() =>
|
||||
icon && (
|
||||
<PaperList.Icon
|
||||
color={theme.textColorSecondary}
|
||||
icon={icon}
|
||||
style={{marginVertical: 0}}
|
||||
/>
|
||||
)
|
||||
icon && <PaperList.Icon color={theme.textColorSecondary} icon={icon} />
|
||||
}
|
||||
/>
|
||||
);
|
||||
|
||||
@@ -29,7 +29,7 @@ const NovelList = ({
|
||||
}
|
||||
};
|
||||
|
||||
const keyExtractor = useCallback(item => item.novelUrl, []);
|
||||
const keyExtractor = useCallback(item => item.sourceId + item.novelUrl, []);
|
||||
|
||||
return (
|
||||
<FlatList
|
||||
|
||||
@@ -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>
|
||||
);
|
||||
|
||||
|
||||
@@ -73,7 +73,7 @@ const styles = StyleSheet.create({
|
||||
marginTop: StatusBar.currentHeight + 8,
|
||||
marginBottom: 8,
|
||||
marginHorizontal: 16,
|
||||
height: 48,
|
||||
height: 46,
|
||||
borderRadius: 50,
|
||||
overflow: 'hidden',
|
||||
},
|
||||
|
||||
@@ -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',
|
||||
},
|
||||
|
||||
@@ -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}`),
|
||||
);
|
||||
});
|
||||
|
||||
|
||||
@@ -1,6 +1,5 @@
|
||||
import {useState, useEffect} from 'react';
|
||||
|
||||
import {showToast} from './../hooks/showToast';
|
||||
import {appversion} from '../utils/utils';
|
||||
|
||||
export const useGithubUpdateChecker = () => {
|
||||
const latestReleaseUrl =
|
||||
@@ -24,20 +23,21 @@ export const useGithubUpdateChecker = () => {
|
||||
};
|
||||
|
||||
const isNewVersion = versionTag => {
|
||||
let currentVersion = 'v1.1.6';
|
||||
let currentVersion = `${appversion}`;
|
||||
const regex = /[^\\d.]/;
|
||||
|
||||
let newVersion = versionTag.replace(regex, '');
|
||||
currentVersion = currentVersion.replace(regex, '');
|
||||
currentVersion = currentVersion;
|
||||
|
||||
return newVersion !== currentVersion;
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
checkForRelease().catch(e => {
|
||||
showToast(`Could not connect to github:\n${`${e}`.split(':')[1].trim()}`);
|
||||
// console.error(e);
|
||||
});
|
||||
checkForRelease();
|
||||
// .catch(e => {
|
||||
// showToast(`Could not connect to github:\n${`${e}`.split(':')[1].trim()}`);
|
||||
// // console.error(e);
|
||||
// });
|
||||
}, []);
|
||||
|
||||
if (!checking) {
|
||||
|
||||
@@ -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}
|
||||
>
|
||||
|
||||
@@ -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,
|
||||
// });
|
||||
// }
|
||||
// };
|
||||
|
||||
@@ -17,7 +17,6 @@ import {
|
||||
MARK_PREVIOUS_CHAPTERS_UNREAD,
|
||||
} from './novel.types';
|
||||
|
||||
import {updateNovel} from '../../services/updates';
|
||||
import {fetchNovel} from '../../services/Source/source';
|
||||
import {
|
||||
followNovel,
|
||||
@@ -60,6 +59,7 @@ Notifications.setNotificationHandler({
|
||||
|
||||
import BackgroundService from 'react-native-background-actions';
|
||||
import {SET_DOWNLOAD_QUEUE} from '../downloads/donwloads.types';
|
||||
import {updateNovel} from '../../services/updates/LibraryUpdateQueries';
|
||||
|
||||
export const setNovel = novel => async dispatch => {
|
||||
dispatch({type: SET_NOVEL, payload: novel});
|
||||
@@ -113,7 +113,7 @@ export const getNovelAction =
|
||||
* Get novel from db.
|
||||
*/
|
||||
novel = await getNovel(sourceId, novelUrl);
|
||||
novel.chapters = await getChapters(novel.novelId);
|
||||
novel.chapters = await getChapters(novel.novelId, sort, filter);
|
||||
|
||||
dispatch({
|
||||
type: GET_NOVEL,
|
||||
@@ -425,10 +425,18 @@ export const deleteAllChaptersAction = chapters => async dispatch => {
|
||||
};
|
||||
|
||||
export const updateNovelAction =
|
||||
(sourceId, novelUrl, novelId, sort, filter) => async dispatch => {
|
||||
(sourceId, novelUrl, novelId, sort, filter) => async (dispatch, getState) => {
|
||||
dispatch({type: FETCHING_NOVEL});
|
||||
|
||||
await updateNovel(sourceId, novelUrl, novelId);
|
||||
const {downloadNewChapters = false, refreshNovelMetadata = false} =
|
||||
getState().settingsReducer;
|
||||
|
||||
const options = {
|
||||
downloadNewChapters,
|
||||
refreshNovelMetadata,
|
||||
};
|
||||
|
||||
await updateNovel(sourceId, novelUrl, novelId, options);
|
||||
|
||||
let novel = await getNovel(sourceId, novelUrl);
|
||||
let chapters = await getChapters(novel.novelId, sort, filter);
|
||||
|
||||
@@ -93,12 +93,14 @@ const initialState = {
|
||||
onlyUpdateOngoingNovels: false,
|
||||
updateLibraryOnLaunch: false,
|
||||
downloadNewChapters: false,
|
||||
refreshNovelMetadata: false,
|
||||
|
||||
/**
|
||||
* Novel settings
|
||||
*/
|
||||
|
||||
hideBackdrop: false,
|
||||
defaultChapterSort: 'ORDER BY chapterId ASC',
|
||||
|
||||
/**
|
||||
* Reader settings
|
||||
@@ -122,7 +124,7 @@ const initialState = {
|
||||
padding: 5,
|
||||
fontFamily: '',
|
||||
lineHeight: 1.5,
|
||||
customCSS: null,
|
||||
customCSS: '',
|
||||
},
|
||||
};
|
||||
|
||||
|
||||
@@ -1,9 +1,9 @@
|
||||
import {GET_UPDATES, SET_LAST_UPDATE_TIME} from './updates.types';
|
||||
|
||||
import {getUpdates} from '../../database/queries/UpdateQueries';
|
||||
import {updateAllNovels} from '../../services/updates';
|
||||
|
||||
import {showToast} from '../../hooks/showToast';
|
||||
import {updateLibrary} from '../../services/updates/updates';
|
||||
|
||||
export const getUpdatesAction = () => async dispatch => {
|
||||
const updates = await getUpdates();
|
||||
@@ -38,10 +38,17 @@ export const updateLibraryAction = () => async (dispatch, getState) => {
|
||||
|
||||
dispatch({type: SET_LAST_UPDATE_TIME, payload: Date.now()});
|
||||
|
||||
const {downloadNewChapters = false, onlyUpdateOngoingNovels = false} =
|
||||
getState().settingsReducer;
|
||||
const {
|
||||
downloadNewChapters = false,
|
||||
onlyUpdateOngoingNovels = false,
|
||||
refreshNovelMetadata = false,
|
||||
} = getState().settingsReducer;
|
||||
|
||||
await updateAllNovels({downloadNewChapters, onlyUpdateOngoingNovels});
|
||||
await updateLibrary({
|
||||
downloadNewChapters,
|
||||
onlyUpdateOngoingNovels,
|
||||
refreshNovelMetadata,
|
||||
});
|
||||
|
||||
dispatch(getUpdatesAction());
|
||||
};
|
||||
|
||||
@@ -89,6 +89,11 @@ const BrowseSettings = ({navigation}) => {
|
||||
}
|
||||
/>
|
||||
</Pressable>
|
||||
<List.InfoItem
|
||||
title="Searching a large number of sources may freeze the app till searching is finished."
|
||||
icon="information-outline"
|
||||
theme={theme}
|
||||
/>
|
||||
<List.Divider theme={theme} />
|
||||
<List.SubHeader theme={theme}>Browse</List.SubHeader>
|
||||
<Pressable
|
||||
|
||||
@@ -1,9 +1,5 @@
|
||||
import React, {useEffect, useRef, useState} from 'react';
|
||||
import {
|
||||
/* StyleSheet, */ View,
|
||||
FlatList,
|
||||
ActivityIndicator,
|
||||
} from 'react-native';
|
||||
import {FlatList} from 'react-native';
|
||||
|
||||
import {ProgressBar} from 'react-native-paper';
|
||||
import {useSelector} from 'react-redux';
|
||||
@@ -14,7 +10,6 @@ import EmptyView from '../../../components/EmptyView';
|
||||
import {ScreenContainer} from '../../../components/Common';
|
||||
import GlobalSearchSourceItem from './GlobalSearchSourceItem';
|
||||
|
||||
import {showToast} from '../../../hooks/showToast';
|
||||
import {useLibrary, useSettings, useTheme} from '../../../hooks/reduxHooks';
|
||||
import {getSource} from '../../../sources/sources';
|
||||
|
||||
@@ -38,7 +33,6 @@ const GlobalSearch = ({route, navigation}) => {
|
||||
);
|
||||
const {searchAllSources = false} = useSettings();
|
||||
|
||||
const [loading, setLoading] = useState(false);
|
||||
const [searchText, setSearchText] = useState(novelName);
|
||||
const [searchResults, setSearchResults] = useState([]);
|
||||
const [progress, setProgress] = useState(0);
|
||||
@@ -51,58 +45,64 @@ const GlobalSearch = ({route, navigation}) => {
|
||||
if (novelName) {
|
||||
onSubmitEditing();
|
||||
}
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
}, []);
|
||||
|
||||
useEffect(() => {
|
||||
return () => {
|
||||
isMounted.current = false;
|
||||
};
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
}, []);
|
||||
|
||||
const clearSearchbar = () => setSearchText('');
|
||||
const onChangeText = text => setSearchText(text);
|
||||
|
||||
const onSubmitEditing = async () => {
|
||||
setSearchResults([]);
|
||||
setProgress(0);
|
||||
|
||||
let globalSearchSources = searchAllSources ? sources : pinnedSources;
|
||||
|
||||
for (let i = 0; i < globalSearchSources.length; i++) {
|
||||
if (isMounted.current === false) {
|
||||
break;
|
||||
setSearchResults(
|
||||
globalSearchSources.map(item => ({
|
||||
sourceId: item.sourceId,
|
||||
sourceName: item.sourceName,
|
||||
lang: item.lang,
|
||||
loading: true,
|
||||
novels: [],
|
||||
error: null,
|
||||
})),
|
||||
);
|
||||
|
||||
globalSearchSources.map(async item => {
|
||||
if (isMounted.current === true) {
|
||||
try {
|
||||
const source = getSource(item.sourceId);
|
||||
const data = await source.searchNovels(encodeURI(searchText));
|
||||
|
||||
setSearchResults(prevState =>
|
||||
prevState.map(sourceItem =>
|
||||
sourceItem.sourceId === item.sourceId
|
||||
? {...sourceItem, novels: data, loading: false}
|
||||
: {...sourceItem},
|
||||
),
|
||||
);
|
||||
} catch (e) {
|
||||
setSearchResults(prevState =>
|
||||
prevState.map(sourceItem =>
|
||||
sourceItem.sourceId === item.sourceId
|
||||
? {
|
||||
...sourceItem,
|
||||
loading: false,
|
||||
error: e.message,
|
||||
}
|
||||
: sourceItem,
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
setProgress(before => before + 1 / globalSearchSources.length);
|
||||
}
|
||||
|
||||
try {
|
||||
setLoading(true);
|
||||
|
||||
const source = getSource(globalSearchSources[i].sourceId);
|
||||
const data = await source.searchNovels(encodeURI(searchText));
|
||||
|
||||
setSearchResults(before => [
|
||||
...before,
|
||||
{
|
||||
sourceId: globalSearchSources[i].sourceId,
|
||||
sourceName: globalSearchSources[i].sourceName,
|
||||
lang: globalSearchSources[i].lang,
|
||||
novels: data,
|
||||
},
|
||||
]);
|
||||
setLoading(false);
|
||||
} catch (error) {
|
||||
showToast(globalSearchSources[i].sourceName + ': ' + error.message);
|
||||
setSearchResults(before => [
|
||||
...before,
|
||||
{
|
||||
sourceId: globalSearchSources[i].sourceId,
|
||||
sourceName: globalSearchSources[i].sourceName,
|
||||
lang: globalSearchSources[i].lang,
|
||||
novels: [],
|
||||
},
|
||||
]);
|
||||
setLoading(false);
|
||||
}
|
||||
setProgress(before => before + 1 / globalSearchSources.length);
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
const renderItem = ({item}) => (
|
||||
@@ -130,42 +130,22 @@ const GlobalSearch = ({route, navigation}) => {
|
||||
<ProgressBar color={theme.colorAccent} progress={progress} />
|
||||
)}
|
||||
<FlatList
|
||||
contentContainerStyle={{
|
||||
flexGrow: 1,
|
||||
padding: 4,
|
||||
}}
|
||||
contentContainerStyle={{flexGrow: 1, padding: 4}}
|
||||
data={searchResults}
|
||||
keyExtractor={item => item.sourceId.toString()}
|
||||
renderItem={renderItem}
|
||||
extraData={pinned}
|
||||
ListEmptyComponent={
|
||||
<>
|
||||
{!loading && (
|
||||
<EmptyView
|
||||
icon="__φ(..)"
|
||||
description={`Search a novel in ${
|
||||
searchAllSources
|
||||
? 'all sources'
|
||||
: pinned.length === 0
|
||||
? 'pinned sources\n(No sources pinned)'
|
||||
: 'pinned sources'
|
||||
}`}
|
||||
/>
|
||||
)}
|
||||
</>
|
||||
}
|
||||
ListFooterComponent={
|
||||
loading && (
|
||||
<View
|
||||
style={{
|
||||
flex: 1,
|
||||
justifyContent: 'center',
|
||||
padding: 16,
|
||||
}}
|
||||
>
|
||||
<ActivityIndicator size="large" color={theme.colorAccent} />
|
||||
</View>
|
||||
)
|
||||
<EmptyView
|
||||
icon="__φ(..)"
|
||||
description={`Search a novel in ${
|
||||
searchAllSources
|
||||
? 'all sources'
|
||||
: pinned.length === 0
|
||||
? 'pinned sources\n(No sources pinned)'
|
||||
: 'pinned sources'
|
||||
}`}
|
||||
/>
|
||||
}
|
||||
/>
|
||||
</ScreenContainer>
|
||||
@@ -173,5 +153,3 @@ const GlobalSearch = ({route, navigation}) => {
|
||||
};
|
||||
|
||||
export default GlobalSearch;
|
||||
|
||||
// const styles = StyleSheet.create({});
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
import React from 'react';
|
||||
import {StyleSheet, View, Text, Image, Pressable} from 'react-native';
|
||||
import {defaultCoverUri} from '../../../sources/helpers/constants';
|
||||
|
||||
const GlobalSearchNovelCover = ({
|
||||
novel,
|
||||
@@ -10,32 +11,28 @@ const GlobalSearchNovelCover = ({
|
||||
}) => {
|
||||
const {novelName, novelCover} = novel;
|
||||
|
||||
const uri =
|
||||
novelCover && !novelCover.startsWith('/') ? novelCover : defaultCoverUri;
|
||||
|
||||
return (
|
||||
<View style={{flex: 1, borderRadius: 6, overflow: 'hidden'}}>
|
||||
<View style={styles.container}>
|
||||
<Pressable
|
||||
android_ripple={{color: theme.rippleColor}}
|
||||
style={styles.novelContainer}
|
||||
style={styles.pressable}
|
||||
onPress={onPress}
|
||||
onLongPress={onLongPress}
|
||||
>
|
||||
<>
|
||||
<Image
|
||||
source={{
|
||||
uri:
|
||||
novelCover && !novelCover.startsWith('/')
|
||||
? novelCover
|
||||
: 'https://github.com/LNReader/lnreader-sources/blob/main/src/coverNotAvailable.jpg?raw=true',
|
||||
}}
|
||||
style={[styles.novelCover, inLibrary && {opacity: 0.5}]}
|
||||
progressiveRenderingEnabled={true}
|
||||
/>
|
||||
<Text
|
||||
numberOfLines={2}
|
||||
style={[styles.title, {color: theme.textColorPrimary}]}
|
||||
>
|
||||
{novelName}
|
||||
</Text>
|
||||
</>
|
||||
<Image
|
||||
source={{uri}}
|
||||
style={[styles.novelCover, inLibrary && {opacity: 0.5}]}
|
||||
progressiveRenderingEnabled={true}
|
||||
/>
|
||||
<Text
|
||||
numberOfLines={2}
|
||||
style={[styles.title, {color: theme.textColorPrimary}]}
|
||||
>
|
||||
{novelName}
|
||||
</Text>
|
||||
</Pressable>
|
||||
</View>
|
||||
);
|
||||
@@ -44,10 +41,16 @@ const GlobalSearchNovelCover = ({
|
||||
export default GlobalSearchNovelCover;
|
||||
|
||||
const styles = StyleSheet.create({
|
||||
novelContainer: {
|
||||
container: {
|
||||
flex: 1,
|
||||
borderRadius: 6,
|
||||
overflow: 'hidden',
|
||||
},
|
||||
pressable: {
|
||||
paddingHorizontal: 5,
|
||||
paddingVertical: 4,
|
||||
borderRadius: 4,
|
||||
flex: 1,
|
||||
},
|
||||
novelCover: {
|
||||
height: 150,
|
||||
|
||||
@@ -29,7 +29,7 @@ const GlobalSearchNovelList = ({data, theme, library, navigation}) => {
|
||||
contentContainerStyle={styles.listContainer}
|
||||
horizontal={true}
|
||||
data={data}
|
||||
keyExtractor={item => item.novelUrl}
|
||||
keyExtractor={item => item.sourceId + item.novelUrl}
|
||||
renderItem={renderItem}
|
||||
ListEmptyComponent={ListEmptyComponent}
|
||||
/>
|
||||
|
||||
@@ -1,10 +1,14 @@
|
||||
import React from 'react';
|
||||
import {StyleSheet, View, Text} from 'react-native';
|
||||
import {StyleSheet, View, Text, ActivityIndicator} from 'react-native';
|
||||
|
||||
import GlobalSearchNovelList from './GlobalSearchNovelList';
|
||||
|
||||
const GlobalSearchSourceItem = ({source, library, theme, navigation}) => {
|
||||
const {sourceName, lang, novels} = source;
|
||||
const {sourceName, lang, loading, novels, error} = source;
|
||||
|
||||
const colorError = {
|
||||
color: theme.statusBar === 'dark-content' ? '#B3261E' : '#F2B8B5',
|
||||
};
|
||||
|
||||
return (
|
||||
<>
|
||||
@@ -14,12 +18,21 @@ const GlobalSearchSourceItem = ({source, library, theme, navigation}) => {
|
||||
{lang}
|
||||
</Text>
|
||||
</View>
|
||||
<GlobalSearchNovelList
|
||||
data={novels}
|
||||
theme={theme}
|
||||
library={library}
|
||||
navigation={navigation}
|
||||
/>
|
||||
{error ? (
|
||||
<Text style={[styles.error, colorError]}>{error}</Text>
|
||||
) : loading ? (
|
||||
<ActivityIndicator
|
||||
color={theme.colorAccent}
|
||||
style={{marginVertical: 16}}
|
||||
/>
|
||||
) : (
|
||||
<GlobalSearchNovelList
|
||||
data={novels}
|
||||
theme={theme}
|
||||
library={library}
|
||||
navigation={navigation}
|
||||
/>
|
||||
)}
|
||||
</>
|
||||
);
|
||||
};
|
||||
@@ -31,4 +44,8 @@ const styles = StyleSheet.create({
|
||||
padding: 8,
|
||||
paddingVertical: 16,
|
||||
},
|
||||
error: {
|
||||
paddingHorizontal: 8,
|
||||
paddingVertical: 16,
|
||||
},
|
||||
});
|
||||
|
||||
@@ -48,7 +48,7 @@ const MigrationNovelList = ({data, theme, library, navigation}) => {
|
||||
contentContainerStyle={styles.flatListCont}
|
||||
horizontal={true}
|
||||
data={data}
|
||||
keyExtractor={item => item.novelUrl}
|
||||
keyExtractor={item => item.sourceId + item.novelUrl}
|
||||
renderItem={renderItem}
|
||||
ListEmptyComponent={
|
||||
<Text
|
||||
|
||||
@@ -1,10 +1,10 @@
|
||||
import React, {useEffect, useState} from 'react';
|
||||
import {
|
||||
/* StyleSheet, */
|
||||
View,
|
||||
Text,
|
||||
FlatList,
|
||||
ActivityIndicator,
|
||||
StyleSheet,
|
||||
} from 'react-native';
|
||||
import {ProgressBar} from 'react-native-paper';
|
||||
import {useLibrary, useSettings, useTheme} from '../../../hooks/reduxHooks';
|
||||
@@ -14,7 +14,6 @@ import EmptyView from '../../../components/EmptyView';
|
||||
import MigrationNovelList from './MigrationNovelList';
|
||||
import {Appbar} from '../../../components/Appbar';
|
||||
|
||||
import {showToast} from '../../../hooks/showToast';
|
||||
import {ScreenContainer} from '../../../components/Common';
|
||||
import {getSource} from '../../../sources/sources';
|
||||
|
||||
@@ -25,7 +24,6 @@ const MigrationNovels = ({navigation, route}) => {
|
||||
const isMounted = React.useRef(true);
|
||||
|
||||
const [progress, setProgress] = useState(0);
|
||||
const [loading, setLoading] = useState(false);
|
||||
const [searchResults, setSearchResults] = useState('');
|
||||
const {searchAllSources = false} = useSettings();
|
||||
let {
|
||||
@@ -44,110 +42,110 @@ const MigrationNovels = ({navigation, route}) => {
|
||||
|
||||
const getSearchResults = async () => {
|
||||
let migrationSources = searchAllSources ? sources : pinnedSources;
|
||||
for (let i = 0; i < migrationSources.length; i++) {
|
||||
if (isMounted.current === false) {
|
||||
break;
|
||||
|
||||
setSearchResults(
|
||||
migrationSources.map(item => ({
|
||||
sourceId: item.sourceId,
|
||||
sourceName: item.sourceName,
|
||||
lang: item.lang,
|
||||
loading: true,
|
||||
novels: [],
|
||||
error: null,
|
||||
})),
|
||||
);
|
||||
|
||||
migrationSources.map(async item => {
|
||||
if (isMounted.current === true) {
|
||||
try {
|
||||
const source = getSource(item.sourceId);
|
||||
const data = await source.searchNovels(novelName);
|
||||
|
||||
setSearchResults(prevState =>
|
||||
prevState.map(sourceItem =>
|
||||
sourceItem.sourceId === item.sourceId
|
||||
? {...sourceItem, novels: data, loading: false}
|
||||
: {...sourceItem},
|
||||
),
|
||||
);
|
||||
} catch (e) {
|
||||
setSearchResults(prevState =>
|
||||
prevState.map(sourceItem =>
|
||||
sourceItem.sourceId === item.sourceId
|
||||
? {
|
||||
...sourceItem,
|
||||
loading: false,
|
||||
error: e.message,
|
||||
}
|
||||
: sourceItem,
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
setProgress(before => before + 1 / migrationSources.length);
|
||||
}
|
||||
|
||||
try {
|
||||
setLoading(true);
|
||||
|
||||
const source = getSource(migrationSources[i].sourceId);
|
||||
const data = await source.searchNovels(novelName);
|
||||
|
||||
setSearchResults(before => [
|
||||
...before,
|
||||
{
|
||||
sourceId: migrationSources[i].sourceId,
|
||||
sourceName: migrationSources[i].sourceName,
|
||||
sourceLanguage: migrationSources[i].lang,
|
||||
novels: data,
|
||||
},
|
||||
]);
|
||||
setLoading(false);
|
||||
} catch (error) {
|
||||
showToast(error.message);
|
||||
|
||||
setSearchResults(before => [
|
||||
...before,
|
||||
{
|
||||
sourceId: migrationSources[i].sourceId,
|
||||
sourceName: migrationSources[i].sourceName,
|
||||
sourceLanguage: migrationSources[i].lang,
|
||||
novels: [],
|
||||
},
|
||||
]);
|
||||
setLoading(false);
|
||||
}
|
||||
setProgress(before => before + 1 / migrationSources.length);
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
getSearchResults();
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
}, []);
|
||||
|
||||
useEffect(() => {
|
||||
return () => {
|
||||
isMounted.current = false;
|
||||
};
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
}, []);
|
||||
|
||||
const colorError = {
|
||||
color: theme.statusBar === 'dark-content' ? '#B3261E' : '#F2B8B5',
|
||||
};
|
||||
|
||||
const renderItem = ({item}) => (
|
||||
<>
|
||||
<View style={{padding: 8, paddingVertical: 16}}>
|
||||
<Text style={{color: theme.textColorPrimary}}>{item.sourceName}</Text>
|
||||
<Text style={{color: theme.textColorSecondary, fontSize: 12}}>
|
||||
{item.sourceLanguage}
|
||||
{item.lang}
|
||||
</Text>
|
||||
</View>
|
||||
<MigrationNovelList
|
||||
data={item.novels}
|
||||
theme={theme}
|
||||
library={library}
|
||||
navigation={navigation}
|
||||
/>
|
||||
{item.error ? (
|
||||
<Text style={[styles.error, colorError]}>{item.error}</Text>
|
||||
) : item.loading ? (
|
||||
<ActivityIndicator
|
||||
color={theme.colorAccent}
|
||||
style={{marginVertical: 16}}
|
||||
/>
|
||||
) : (
|
||||
<MigrationNovelList
|
||||
data={item.novels}
|
||||
theme={theme}
|
||||
library={library}
|
||||
navigation={navigation}
|
||||
/>
|
||||
)}
|
||||
</>
|
||||
);
|
||||
|
||||
return (
|
||||
<ScreenContainer theme={theme}>
|
||||
<Appbar title={novelName} onBackAction={() => navigation.goBack()} />
|
||||
<Appbar title={novelName} onBackAction={navigation.goBack} />
|
||||
{progress > 0 && (
|
||||
<ProgressBar color={theme.colorAccent} progress={progress} />
|
||||
)}
|
||||
<FlatList
|
||||
contentContainerStyle={{
|
||||
flexGrow: 1,
|
||||
padding: 4,
|
||||
}}
|
||||
contentContainerStyle={{flexGrow: 1, padding: 4}}
|
||||
data={searchResults}
|
||||
keyExtractor={item => item.sourceId.toString()}
|
||||
renderItem={renderItem}
|
||||
extraData={pinned}
|
||||
ListEmptyComponent={
|
||||
<>
|
||||
{!loading && (
|
||||
<EmptyView
|
||||
icon="__φ(..)"
|
||||
description={`Search a novel in your pinned sources ${
|
||||
pinned.length === 0 ? '(No sources pinned)' : ''
|
||||
}`}
|
||||
/>
|
||||
)}
|
||||
</>
|
||||
}
|
||||
ListFooterComponent={
|
||||
loading && (
|
||||
<View
|
||||
style={{
|
||||
flex: 1,
|
||||
justifyContent: 'center',
|
||||
padding: 16,
|
||||
}}
|
||||
>
|
||||
<ActivityIndicator size="large" color={theme.colorAccent} />
|
||||
</View>
|
||||
)
|
||||
<EmptyView
|
||||
icon="__φ(..)"
|
||||
description={`Search a novel in your pinned sources ${
|
||||
pinned.length === 0 ? '(No sources pinned)' : ''
|
||||
}`}
|
||||
/>
|
||||
}
|
||||
/>
|
||||
</ScreenContainer>
|
||||
@@ -156,4 +154,9 @@ const MigrationNovels = ({navigation, route}) => {
|
||||
|
||||
export default MigrationNovels;
|
||||
|
||||
// const styles = StyleSheet.create({});
|
||||
const styles = StyleSheet.create({
|
||||
error: {
|
||||
paddingHorizontal: 8,
|
||||
paddingVertical: 16,
|
||||
},
|
||||
});
|
||||
|
||||
@@ -74,7 +74,7 @@ const LibrarySheet = ({
|
||||
DESC: 'novels.novelId DESC',
|
||||
},
|
||||
{
|
||||
label: 'Aplhabetically',
|
||||
label: 'Alphabetically',
|
||||
ASC: 'novels.novelName ASC',
|
||||
DESC: 'novels.novelName DESC',
|
||||
},
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import React from 'react';
|
||||
import {ScrollView} from 'react-native';
|
||||
|
||||
import * as Linking from 'expo-linking';
|
||||
import {useSelector} from 'react-redux';
|
||||
@@ -6,7 +7,7 @@ import {useSelector} from 'react-redux';
|
||||
import {List} from '../../components/List';
|
||||
import {ScreenContainer} from '../../components/Common';
|
||||
import {MoreHeader} from './components/MoreHeader';
|
||||
import {ScrollView} from 'react-native';
|
||||
import {appversion, releasedt} from '../../utils/utils';
|
||||
|
||||
const AboutScreen = ({navigation}) => {
|
||||
const theme = useSelector(state => state.settingsReducer.theme);
|
||||
@@ -23,14 +24,14 @@ const AboutScreen = ({navigation}) => {
|
||||
<List.Section>
|
||||
<List.Item
|
||||
title="Version"
|
||||
description="Stable 1.1.6 (26/10/21 09:00 PM)"
|
||||
description={`Stable ${appversion} (${releasedt})`}
|
||||
theme={theme}
|
||||
/>
|
||||
<List.Item
|
||||
title="What's new"
|
||||
onPress={() =>
|
||||
Linking.openURL(
|
||||
'https://github.com/LNReader/lnreader/releases/tag/v1.1.6',
|
||||
`https://github.com/LNReader/lnreader/releases/tag/v${appversion}`,
|
||||
)
|
||||
}
|
||||
theme={theme}
|
||||
|
||||
@@ -56,6 +56,7 @@ import EditInfoModal from './components/EditInfoModal';
|
||||
import {pickCustomNovelCover} from '../../database/queries/NovelQueries';
|
||||
import FadeView from '../../components/Common/CrossFadeView';
|
||||
import {useModal} from '../../hooks/useModal';
|
||||
import DownloadCustomChapterModal from './components/DownloadCustomChapterModal';
|
||||
|
||||
const Novel = ({route, navigation}) => {
|
||||
const item = route.params;
|
||||
@@ -77,15 +78,18 @@ const Novel = ({route, navigation}) => {
|
||||
const deleteDownloadsSnackbar = useModal();
|
||||
|
||||
const {
|
||||
sort = 'ORDER BY chapterId ASC',
|
||||
useFabForContinueReading = false,
|
||||
defaultChapterSort = 'ORDER BY chapterId ASC',
|
||||
} = useSettings();
|
||||
|
||||
const {
|
||||
sort = defaultChapterSort,
|
||||
filter = '',
|
||||
showChapterTitles = false,
|
||||
} = usePreferences(novel.novelId);
|
||||
|
||||
let {lastReadChapter, position} = useContinueReading(chapters, novel.novelId);
|
||||
|
||||
const {useFabForContinueReading = lastReadChapter && false} = useSettings();
|
||||
|
||||
useEffect(() => {
|
||||
dispatch(
|
||||
getNovelAction(followed, sourceId, novelUrl, novelId, sort, filter),
|
||||
@@ -202,7 +206,7 @@ const Novel = ({route, navigation}) => {
|
||||
}}
|
||||
numberOfLines={1}
|
||||
>
|
||||
{chapter.releaseDate && '• '}
|
||||
{chapter.releaseDate ? '• ' : null}
|
||||
{'Progress ' + position[chapter.chapterId].percentage + '%'}
|
||||
</Text>
|
||||
);
|
||||
@@ -242,6 +246,7 @@ const Novel = ({route, navigation}) => {
|
||||
);
|
||||
|
||||
const [editInfoModal, showEditInfoModal] = useState(false);
|
||||
const downloadCustomChapterModal = useModal();
|
||||
|
||||
return (
|
||||
<Provider>
|
||||
@@ -347,7 +352,7 @@ const Novel = ({route, navigation}) => {
|
||||
}}
|
||||
>
|
||||
<Menu.Item
|
||||
title="All"
|
||||
title="Next chapter"
|
||||
style={{backgroundColor: theme.menuColor}}
|
||||
titleStyle={{
|
||||
color: theme.textColorPrimary,
|
||||
@@ -357,12 +362,73 @@ const Novel = ({route, navigation}) => {
|
||||
downloadAllChaptersAction(
|
||||
novel.sourceId,
|
||||
novel.novelUrl,
|
||||
chapters,
|
||||
[
|
||||
chapters.find(
|
||||
chapter =>
|
||||
!!chapter.read === false &&
|
||||
!!chapter.downloaded === false,
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
showDownloadMenu(false);
|
||||
}}
|
||||
/>
|
||||
<Menu.Item
|
||||
title="Next 5 chapter"
|
||||
style={{backgroundColor: theme.menuColor}}
|
||||
titleStyle={{
|
||||
color: theme.textColorPrimary,
|
||||
}}
|
||||
onPress={() => {
|
||||
dispatch(
|
||||
downloadAllChaptersAction(
|
||||
novel.sourceId,
|
||||
novel.novelUrl,
|
||||
chapters
|
||||
.filter(
|
||||
chapter =>
|
||||
!!chapter.read === false &&
|
||||
!!chapter.downloaded === false,
|
||||
)
|
||||
.slice(0, 5),
|
||||
),
|
||||
);
|
||||
showDownloadMenu(false);
|
||||
}}
|
||||
/>
|
||||
<Menu.Item
|
||||
title="Next 10 chapter"
|
||||
style={{backgroundColor: theme.menuColor}}
|
||||
titleStyle={{
|
||||
color: theme.textColorPrimary,
|
||||
}}
|
||||
onPress={() => {
|
||||
dispatch(
|
||||
downloadAllChaptersAction(
|
||||
novel.sourceId,
|
||||
novel.novelUrl,
|
||||
chapters
|
||||
.filter(
|
||||
chapter =>
|
||||
!!chapter.read === false &&
|
||||
!!chapter.downloaded === false,
|
||||
)
|
||||
.slice(0, 10),
|
||||
),
|
||||
);
|
||||
showDownloadMenu(false);
|
||||
}}
|
||||
/>
|
||||
<Menu.Item
|
||||
title="Custom"
|
||||
style={{backgroundColor: theme.menuColor}}
|
||||
titleStyle={{color: theme.textColorPrimary}}
|
||||
onPress={() => {
|
||||
downloadCustomChapterModal.showModal();
|
||||
showDownloadMenu(false);
|
||||
}}
|
||||
/>
|
||||
<Menu.Item
|
||||
title="Unread"
|
||||
style={{backgroundColor: theme.menuColor}}
|
||||
@@ -380,6 +446,23 @@ const Novel = ({route, navigation}) => {
|
||||
showDownloadMenu(false);
|
||||
}}
|
||||
/>
|
||||
<Menu.Item
|
||||
title="All"
|
||||
style={{backgroundColor: theme.menuColor}}
|
||||
titleStyle={{
|
||||
color: theme.textColorPrimary,
|
||||
}}
|
||||
onPress={() => {
|
||||
dispatch(
|
||||
downloadAllChaptersAction(
|
||||
novel.sourceId,
|
||||
novel.novelUrl,
|
||||
chapters,
|
||||
),
|
||||
);
|
||||
showDownloadMenu(false);
|
||||
}}
|
||||
/>
|
||||
<Menu.Item
|
||||
title="Delete downloads"
|
||||
style={{backgroundColor: theme.menuColor}}
|
||||
@@ -583,6 +666,14 @@ const Novel = ({route, navigation}) => {
|
||||
theme={theme}
|
||||
dispatch={dispatch}
|
||||
/>
|
||||
<DownloadCustomChapterModal
|
||||
modalVisible={downloadCustomChapterModal.visible}
|
||||
hideModal={downloadCustomChapterModal.hideModal}
|
||||
novel={novel}
|
||||
chapters={chapters}
|
||||
theme={theme}
|
||||
dispatch={dispatch}
|
||||
/>
|
||||
<NovelBottomSheet
|
||||
novelUrl={novelUrl}
|
||||
bottomSheetRef={novelBottomSheetRef}
|
||||
|
||||
@@ -66,7 +66,7 @@ const ChapterItem = ({
|
||||
marginTop: 5,
|
||||
}}
|
||||
>
|
||||
{releaseDate && (
|
||||
{releaseDate ? (
|
||||
<Text
|
||||
style={{
|
||||
color: read
|
||||
@@ -80,7 +80,7 @@ const ChapterItem = ({
|
||||
>
|
||||
{releaseDate}
|
||||
</Text>
|
||||
)}
|
||||
) : null}
|
||||
{showProgressPercentage(chapter)}
|
||||
</View>
|
||||
</View>
|
||||
|
||||
@@ -0,0 +1,115 @@
|
||||
import React, {useState} from 'react';
|
||||
import {StyleSheet, Text, View, TextInput} from 'react-native';
|
||||
|
||||
import {IconButton, Modal, Portal} from 'react-native-paper';
|
||||
import {downloadAllChaptersAction} from '../../../redux/novel/novel.actions';
|
||||
|
||||
const DownloadCustomChapterModal = ({
|
||||
theme,
|
||||
hideModal,
|
||||
modalVisible,
|
||||
novel,
|
||||
chapters,
|
||||
dispatch,
|
||||
}) => {
|
||||
const [text, setText] = useState(0);
|
||||
|
||||
const onDismiss = () => {
|
||||
hideModal();
|
||||
setText(0);
|
||||
};
|
||||
|
||||
const onSubmit = () => {
|
||||
dispatch(
|
||||
downloadAllChaptersAction(
|
||||
novel.sourceId,
|
||||
novel.novelUrl,
|
||||
chapters
|
||||
.filter(
|
||||
chapter =>
|
||||
!!chapter.read === false && !!chapter.downloaded === false,
|
||||
)
|
||||
.slice(0, text.toString()),
|
||||
),
|
||||
);
|
||||
hideModal();
|
||||
};
|
||||
|
||||
const onChangeText = txt => {
|
||||
if (Number(txt) >= 0) {
|
||||
setText(Number(txt));
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<Portal>
|
||||
<Modal
|
||||
visible={modalVisible}
|
||||
onDismiss={onDismiss}
|
||||
contentContainerStyle={[
|
||||
styles.modalContainer,
|
||||
{backgroundColor: theme.colorPrimary},
|
||||
]}
|
||||
>
|
||||
<Text style={[styles.modalTitle, {color: theme.textColorPrimary}]}>
|
||||
Download custom amount
|
||||
</Text>
|
||||
<View style={{flexDirection: 'row', justifyContent: 'center'}}>
|
||||
<IconButton
|
||||
icon="chevron-double-left"
|
||||
animated
|
||||
size={24}
|
||||
color={theme.colorAccent}
|
||||
onPress={() => text > 9 && setText(prevState => prevState - 10)}
|
||||
/>
|
||||
<IconButton
|
||||
icon="chevron-left"
|
||||
animated
|
||||
size={24}
|
||||
color={theme.colorAccent}
|
||||
onPress={() => text > 0 && setText(prevState => prevState - 1)}
|
||||
/>
|
||||
<TextInput
|
||||
value={text.toString()}
|
||||
style={{color: theme.textColorPrimary, marginHorizontal: 4}}
|
||||
keyboardType="numeric"
|
||||
onChangeText={onChangeText}
|
||||
onSubmitEditing={onSubmit}
|
||||
/>
|
||||
<IconButton
|
||||
icon="chevron-right"
|
||||
animated
|
||||
size={24}
|
||||
color={theme.colorAccent}
|
||||
onPress={() => setText(prevState => prevState + 1)}
|
||||
/>
|
||||
<IconButton
|
||||
icon="chevron-double-right"
|
||||
animated
|
||||
size={24}
|
||||
color={theme.colorAccent}
|
||||
onPress={() => setText(prevState => prevState + 10)}
|
||||
/>
|
||||
</View>
|
||||
</Modal>
|
||||
</Portal>
|
||||
);
|
||||
};
|
||||
|
||||
export default DownloadCustomChapterModal;
|
||||
|
||||
const styles = StyleSheet.create({
|
||||
modalContainer: {
|
||||
margin: 30,
|
||||
padding: 20,
|
||||
borderRadius: 8,
|
||||
},
|
||||
modalTitle: {
|
||||
fontSize: 16,
|
||||
marginBottom: 16,
|
||||
},
|
||||
errorText: {
|
||||
color: '#FF0033',
|
||||
paddingTop: 8,
|
||||
},
|
||||
});
|
||||
@@ -11,7 +11,7 @@ import {
|
||||
|
||||
import {IconButton, Portal} from 'react-native-paper';
|
||||
import {LinearGradient} from 'expo-linear-gradient';
|
||||
import easeGradient from 'react-native-easing-gradient';
|
||||
import {easeGradient} from 'react-native-easing-gradient';
|
||||
import FastImage from 'react-native-fast-image';
|
||||
import {TouchableWithoutFeedback} from 'react-native-gesture-handler';
|
||||
import {Chip} from '../../../../components/Chip/Chip';
|
||||
|
||||
@@ -237,9 +237,9 @@ const NovelInfoHeader = ({
|
||||
followed={novel.followed}
|
||||
theme={theme}
|
||||
/>
|
||||
{novel.genre !== null && novel.genre !== '' && (
|
||||
{novel.genre ? (
|
||||
<NovelGenres theme={theme} genre={novel.genre} />
|
||||
)}
|
||||
) : null}
|
||||
<ReadButton
|
||||
novel={novel}
|
||||
chapters={chapters}
|
||||
@@ -252,16 +252,14 @@ const NovelInfoHeader = ({
|
||||
onPress={() => novelBottomSheetRef.current.show()}
|
||||
android_ripple={{color: theme.rippleColor}}
|
||||
>
|
||||
<>
|
||||
<Text style={[{color: theme.textColorPrimary}, styles.chapters]}>
|
||||
{`${chapters.length} chapters`}
|
||||
</Text>
|
||||
<IconButton
|
||||
icon="filter-variant"
|
||||
color={filter ? theme.filterColor : theme.textColorPrimary}
|
||||
size={24}
|
||||
/>
|
||||
</>
|
||||
<Text style={[{color: theme.textColorPrimary}, styles.chapters]}>
|
||||
{`${chapters.length} chapters`}
|
||||
</Text>
|
||||
<IconButton
|
||||
icon="filter-variant"
|
||||
color={filter ? theme.filterColor : theme.textColorPrimary}
|
||||
size={24}
|
||||
/>
|
||||
</Pressable>
|
||||
</>
|
||||
)}
|
||||
|
||||
@@ -10,8 +10,8 @@ const NovelSummary = ({summary, followed, theme}) => {
|
||||
|
||||
const expandNovelSummary = () => setExpanded(!expanded);
|
||||
|
||||
return (
|
||||
<View style={[styles.summaryContainer, {paddingBottom: expanded ? 20 : 8}]}>
|
||||
return summary ? (
|
||||
<View style={[styles.summaryContainer, {paddingBottom: expanded ? 24 : 8}]}>
|
||||
<Text
|
||||
style={{
|
||||
color: theme.textColorSecondary,
|
||||
@@ -48,7 +48,7 @@ const NovelSummary = ({summary, followed, theme}) => {
|
||||
/>
|
||||
</View>
|
||||
</View>
|
||||
);
|
||||
) : null;
|
||||
};
|
||||
|
||||
export default NovelSummary;
|
||||
|
||||
@@ -37,7 +37,7 @@ const JumpToChapterModal = ({
|
||||
});
|
||||
hideModal();
|
||||
} else {
|
||||
setError(`Enter a valid chapter number (<= ${chapters.length})`);
|
||||
setError(`Enter a valid chapter number (≤ ${chapters.length})`);
|
||||
}
|
||||
} else {
|
||||
const chapter = chapters.find(chap =>
|
||||
@@ -89,7 +89,7 @@ const JumpToChapterModal = ({
|
||||
<TextInput
|
||||
value={text}
|
||||
placeholder={
|
||||
mode ? 'Chapter Name' : `Chapter Number (<= ${chapters.length})`
|
||||
mode ? 'Chapter Name' : `Chapter Number (≤ ${chapters.length})`
|
||||
}
|
||||
onChangeText={onChangeText}
|
||||
onSubmitEditing={onSubmit}
|
||||
|
||||
@@ -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),
|
||||
|
||||
@@ -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,
|
||||
},
|
||||
]}
|
||||
>
|
||||
|
||||
@@ -11,6 +11,7 @@ import {showToast} from '../../hooks/showToast';
|
||||
|
||||
import {deleteNovelCache} from '../../database/queries/NovelQueries';
|
||||
import {clearUpdates} from '../../database/queries/UpdateQueries';
|
||||
import {clearCoverCache} from '../../services/utils/coverCache';
|
||||
|
||||
const AdvancedSettings = ({navigation}) => {
|
||||
const theme = useTheme();
|
||||
@@ -22,11 +23,15 @@ const AdvancedSettings = ({navigation}) => {
|
||||
const showClearDatabaseDialog = () => setClearDatabaseDialog(true);
|
||||
const hideClearDatabaseDialog = () => setClearDatabaseDialog(false);
|
||||
|
||||
const [clearUpdatesDialog, setClearUpdatesDialog] = useState(false);
|
||||
const showClearUpdatesDialog = () => setClearUpdatesDialog(true);
|
||||
const hideClearUpdatesDialog = () => setClearUpdatesDialog(false);
|
||||
|
||||
return (
|
||||
<ScreenContainer theme={theme}>
|
||||
<Appbar title="Advanced" onBackAction={() => navigation.goBack()} />
|
||||
<List.Section>
|
||||
<List.SubHeader theme={theme}>Data</List.SubHeader>
|
||||
<List.SubHeader theme={theme}>Data Management</List.SubHeader>
|
||||
<List.Item
|
||||
title="Clear database"
|
||||
description="Delete history for novels not in your library"
|
||||
@@ -36,10 +41,13 @@ const AdvancedSettings = ({navigation}) => {
|
||||
<List.Item
|
||||
title="Clear updates tab"
|
||||
description="Clears chapter entries in updates tab"
|
||||
onPress={() => {
|
||||
clearUpdates();
|
||||
showToast('Updates cleared.');
|
||||
}}
|
||||
onPress={showClearUpdatesDialog}
|
||||
theme={theme}
|
||||
/>
|
||||
<List.Item
|
||||
title="Clean up cached covers"
|
||||
description="Delete all covers cached"
|
||||
onPress={clearCoverCache}
|
||||
theme={theme}
|
||||
/>
|
||||
</List.Section>
|
||||
@@ -83,6 +91,45 @@ const AdvancedSettings = ({navigation}) => {
|
||||
</Button>
|
||||
</Dialog.Actions>
|
||||
</Dialog>
|
||||
<Dialog
|
||||
visible={clearUpdatesDialog}
|
||||
onDismiss={hideClearUpdatesDialog}
|
||||
style={{
|
||||
borderRadius: 6,
|
||||
backgroundColor: theme.colorPrimary,
|
||||
}}
|
||||
>
|
||||
<Dialog.Title
|
||||
style={{
|
||||
letterSpacing: 0,
|
||||
fontSize: 16,
|
||||
lineHeight: 16 * 1.5,
|
||||
color: theme.textColorPrimary,
|
||||
}}
|
||||
>
|
||||
Are you sure? Updates tab will be cleared.
|
||||
</Dialog.Title>
|
||||
<Dialog.Actions>
|
||||
<Button
|
||||
uppercase={false}
|
||||
theme={{colors: {primary: theme.colorAccent}}}
|
||||
onPress={hideClearUpdatesDialog}
|
||||
>
|
||||
Cancel
|
||||
</Button>
|
||||
<Button
|
||||
uppercase={false}
|
||||
theme={{colors: {primary: theme.colorAccent}}}
|
||||
onPress={() => {
|
||||
clearUpdates();
|
||||
showToast('Updates cleared.');
|
||||
hideClearUpdatesDialog();
|
||||
}}
|
||||
>
|
||||
Ok
|
||||
</Button>
|
||||
</Dialog.Actions>
|
||||
</Dialog>
|
||||
</Portal>
|
||||
</ScreenContainer>
|
||||
);
|
||||
|
||||
@@ -31,13 +31,6 @@ const BackupSettings = ({navigation}) => {
|
||||
onPress={restoreFullBackup}
|
||||
theme={theme}
|
||||
/>
|
||||
{/* <List.Divider theme={theme} />
|
||||
<List.Item
|
||||
title="Restore old backup"
|
||||
description="Restore library from backup file of version v1.1.2 or lower"
|
||||
onPress={() => dispatch(restoreLibraryAction())}
|
||||
theme={theme}
|
||||
/> */}
|
||||
<List.InfoItem
|
||||
title="Create backup may not work on devices with Android 9 or lower."
|
||||
icon="information-outline"
|
||||
|
||||
@@ -14,6 +14,7 @@ import {useSettings, useTheme} from '../../hooks/reduxHooks';
|
||||
import {setAppSettings} from '../../redux/settings/settings.actions';
|
||||
import {SHOW_LAST_UPDATE_TIME} from '../../redux/updates/updates.types';
|
||||
import {useModal} from '../../hooks/useModal';
|
||||
import DefaultChapterSortModal from './components/DefaultChapterSortModal';
|
||||
|
||||
const GenralSettings = ({navigation}) => {
|
||||
const theme = useTheme();
|
||||
@@ -27,6 +28,8 @@ const GenralSettings = ({navigation}) => {
|
||||
updateLibraryOnLaunch = false,
|
||||
downloadNewChapters = false,
|
||||
onlyUpdateOngoingNovels = false,
|
||||
defaultChapterSort = 'ORDER BY chapterId ASC',
|
||||
refreshNovelMetadata = false,
|
||||
} = useSettings();
|
||||
|
||||
const {showLastUpdateTime = true} = useSelector(
|
||||
@@ -54,6 +57,8 @@ const GenralSettings = ({navigation}) => {
|
||||
*/
|
||||
const gridSizeModalRef = useModal();
|
||||
|
||||
const defaultChapterSortModal = useModal();
|
||||
|
||||
return (
|
||||
<ScreenContainer theme={theme}>
|
||||
<Appbar title="General" onBackAction={navigation.goBack} />
|
||||
@@ -85,6 +90,18 @@ const GenralSettings = ({navigation}) => {
|
||||
theme={theme}
|
||||
/>
|
||||
<List.Divider theme={theme} />
|
||||
<List.SubHeader theme={theme}>Novel</List.SubHeader>
|
||||
<List.Item
|
||||
title="Default chapter sort"
|
||||
description={`By source ${
|
||||
defaultChapterSort === 'ORDER BY chapterId ASC'
|
||||
? '(Ascending)'
|
||||
: '(Descending)'
|
||||
}`}
|
||||
onPress={defaultChapterSortModal.showModal}
|
||||
theme={theme}
|
||||
/>
|
||||
<List.Divider theme={theme} />
|
||||
<List.SubHeader theme={theme}>Global update</List.SubHeader>
|
||||
<SwitchSetting
|
||||
label="Only update ongoing novels"
|
||||
@@ -99,6 +116,17 @@ const GenralSettings = ({navigation}) => {
|
||||
}
|
||||
theme={theme}
|
||||
/>
|
||||
<SwitchSetting
|
||||
label="Automatically refresh metadata"
|
||||
description="Check for new cover and details when updating library"
|
||||
value={refreshNovelMetadata}
|
||||
onPress={() =>
|
||||
dispatch(
|
||||
setAppSettings('refreshNovelMetadata', !refreshNovelMetadata),
|
||||
)
|
||||
}
|
||||
theme={theme}
|
||||
/>
|
||||
<SwitchSetting
|
||||
label="Show last update time"
|
||||
value={showLastUpdateTime}
|
||||
@@ -131,6 +159,13 @@ const GenralSettings = ({navigation}) => {
|
||||
dispatch={dispatch}
|
||||
theme={theme}
|
||||
/>
|
||||
<DefaultChapterSortModal
|
||||
defaultChapterSort={defaultChapterSort}
|
||||
displayModalVisible={defaultChapterSortModal.visible}
|
||||
hideDisplayModal={defaultChapterSortModal.hideModal}
|
||||
dispatch={dispatch}
|
||||
theme={theme}
|
||||
/>
|
||||
<GridSizeModal
|
||||
dispatch={dispatch}
|
||||
novelsPerRow={novelsPerRow}
|
||||
|
||||
@@ -5,17 +5,16 @@ import {
|
||||
StyleSheet,
|
||||
Text,
|
||||
TextInput,
|
||||
useWindowDimensions,
|
||||
KeyboardAvoidingView,
|
||||
View,
|
||||
} from 'react-native';
|
||||
import {Button} from 'react-native-paper';
|
||||
import {Button, IconButton} from 'react-native-paper';
|
||||
import WebView from 'react-native-webview';
|
||||
import {useDispatch} from 'react-redux';
|
||||
import {TabView, TabBar} from 'react-native-tab-view';
|
||||
|
||||
import {Appbar} from '../../components/Appbar';
|
||||
import ColorPickerModal from '../../components/ColorPickerModal';
|
||||
import {Row, ScreenContainer} from '../../components/Common';
|
||||
import {Row} from '../../components/Common';
|
||||
import {
|
||||
ToggleButton,
|
||||
ToggleColorButton,
|
||||
@@ -27,230 +26,83 @@ import {
|
||||
setAppSettings,
|
||||
setReaderSettings,
|
||||
} from '../../redux/settings/settings.actions';
|
||||
import {readerBackground, readerTextColor} from '../reader/utils/readerStyles';
|
||||
import {
|
||||
readerBackground,
|
||||
readerLineHeight,
|
||||
readerTextColor,
|
||||
} from '../reader/utils/readerStyles';
|
||||
import {useModal} from '../../hooks/useModal';
|
||||
import SwitchSetting from '../../components/Switch/Switch';
|
||||
import FontPickerModal from './components/FontPickerModal';
|
||||
import {fonts} from '../../services/utils/constants';
|
||||
|
||||
const FirstRoute = React.memo(({theme, dispatch, reader}) => {
|
||||
const presetThemes = [
|
||||
{
|
||||
value: 1,
|
||||
backgroundColor: '#000000',
|
||||
textColor: 'rgba(255,255,255,0.7)',
|
||||
},
|
||||
{value: 2, backgroundColor: '#FFFFFF', textColor: '#111111'},
|
||||
{value: 3, backgroundColor: '#F7DFC6', textColor: '#593100'},
|
||||
{value: 4, backgroundColor: '#292832', textColor: '#CCCCCC'},
|
||||
{value: 5, backgroundColor: '#2B2C30', textColor: '#CCCCCC'},
|
||||
];
|
||||
const presetThemes = [
|
||||
{
|
||||
value: 1,
|
||||
backgroundColor: '#000000',
|
||||
textColor: 'rgba(255,255,255,0.7)',
|
||||
},
|
||||
{value: 2, backgroundColor: '#FFFFFF', textColor: '#111111'},
|
||||
{value: 3, backgroundColor: '#F7DFC6', textColor: '#593100'},
|
||||
{value: 4, backgroundColor: '#292832', textColor: '#CCCCCC'},
|
||||
{value: 5, backgroundColor: '#2B2C30', textColor: '#CCCCCC'},
|
||||
];
|
||||
|
||||
const textAlignments = [
|
||||
{value: 'left', icon: 'format-align-left'},
|
||||
{value: 'center', icon: 'format-align-center'},
|
||||
{value: 'justify', icon: 'format-align-justify'},
|
||||
{value: 'right', icon: 'format-align-right'},
|
||||
];
|
||||
const textAlignments = [
|
||||
{value: 'left', icon: 'format-align-left'},
|
||||
{value: 'center', icon: 'format-align-center'},
|
||||
{value: 'justify', icon: 'format-align-justify'},
|
||||
{value: 'right', icon: 'format-align-right'},
|
||||
];
|
||||
|
||||
const {autoScrollInterval = 10} = useSettings();
|
||||
const SettingsReaderScreen = ({navigation}) => {
|
||||
const theme = useTheme();
|
||||
const dispatch = useDispatch();
|
||||
const reader = useReaderSettings();
|
||||
|
||||
/**
|
||||
* Reader Background Color Modal
|
||||
*/
|
||||
const {
|
||||
useWebViewForChapter = false,
|
||||
verticalSeekbar = true,
|
||||
swipeGestures = true,
|
||||
fullScreenMode = true,
|
||||
showScrollPercentage = true,
|
||||
showBatteryAndTime = false,
|
||||
|
||||
autoScrollInterval = 10,
|
||||
} = useSettings();
|
||||
|
||||
const readerFontPickerModal = useModal();
|
||||
const readerBackgroundModal = useModal();
|
||||
|
||||
/**
|
||||
* Reader Text Color Modal
|
||||
*/
|
||||
const [readerTextColorModal, setReaderTextColorModal] = useState(false);
|
||||
const showReaderTextColorModal = () => setReaderTextColorModal(true);
|
||||
const hideReaderTextColorModal = () => setReaderTextColorModal(false);
|
||||
const readerTextColorModal = useModal();
|
||||
|
||||
const setReaderBackground = val => {
|
||||
dispatch(setReaderSettings('theme', val));
|
||||
};
|
||||
|
||||
const setReaderTextColor = val => {
|
||||
dispatch(setReaderSettings('textColor', val || reader.textColor));
|
||||
dispatch(setReaderSettings('textColor', val ?? reader.textColor));
|
||||
};
|
||||
|
||||
const [customHtmlCSS, setcustomHtmlCSS] = useState(reader.customCSS);
|
||||
const [customCSS, setcustomCSS] = useState(reader.customCSS);
|
||||
|
||||
return (
|
||||
<View style={{flex: 1}}>
|
||||
<ScrollView>
|
||||
<List.Section>
|
||||
<List.SubHeader theme={theme}>Preset</List.SubHeader>
|
||||
<View style={styles.pressableListItem}>
|
||||
<Text
|
||||
<>
|
||||
<Appbar title="Reader" onBackAction={navigation.goBack} />
|
||||
<KeyboardAvoidingView style={{flex: 1}} behavior="padding">
|
||||
<ScrollView
|
||||
style={{flex: 1}}
|
||||
contentContainerStyle={{paddingBottom: 40}}
|
||||
>
|
||||
{useWebViewForChapter ? (
|
||||
<WebView
|
||||
originWhitelist={['*']}
|
||||
style={{
|
||||
color: theme.textColorPrimary,
|
||||
fontSize: 16,
|
||||
height: 500,
|
||||
backgroundColor: readerBackground(reader.theme),
|
||||
}}
|
||||
>
|
||||
Preset Themes
|
||||
</Text>
|
||||
<ScrollView
|
||||
style={{marginLeft: 16}}
|
||||
horizontal={true}
|
||||
showsHorizontalScrollIndicator={false}
|
||||
>
|
||||
<Row>
|
||||
{presetThemes.map(item => (
|
||||
<ToggleColorButton
|
||||
key={item.value}
|
||||
selected={reader.theme === item.backgroundColor}
|
||||
backgroundColor={item.backgroundColor}
|
||||
textColor={item.textColor}
|
||||
onPress={() => {
|
||||
dispatch(
|
||||
setReaderSettings('theme', item.backgroundColor),
|
||||
);
|
||||
dispatch(setReaderSettings('textColor', item.textColor));
|
||||
}}
|
||||
/>
|
||||
))}
|
||||
</Row>
|
||||
</ScrollView>
|
||||
</View>
|
||||
<List.Divider theme={theme} />
|
||||
<List.SubHeader theme={theme}>Reader</List.SubHeader>
|
||||
<List.ColorItem
|
||||
title="Background Color"
|
||||
description={readerBackground(reader.theme).toUpperCase()}
|
||||
onPress={readerBackgroundModal.showModal}
|
||||
theme={theme}
|
||||
/>
|
||||
<List.ColorItem
|
||||
title="Text Color"
|
||||
description={
|
||||
reader.textColor.toUpperCase() || readerTextColor(reader.theme)
|
||||
}
|
||||
onPress={showReaderTextColorModal}
|
||||
theme={theme}
|
||||
/>
|
||||
<List.Divider theme={theme} />
|
||||
<List.SubHeader theme={theme}>Text</List.SubHeader>
|
||||
<Pressable style={styles.pressableListItem}>
|
||||
<View>
|
||||
<Text
|
||||
style={{
|
||||
color: theme.textColorPrimary,
|
||||
fontSize: 16,
|
||||
}}
|
||||
>
|
||||
Text Align
|
||||
</Text>
|
||||
<Text
|
||||
style={{
|
||||
color: theme.textColorSecondary,
|
||||
textTransform: 'capitalize',
|
||||
}}
|
||||
>
|
||||
{reader.textAlign}
|
||||
</Text>
|
||||
</View>
|
||||
<Row>
|
||||
{textAlignments.map(item => (
|
||||
<ToggleButton
|
||||
key={item.value}
|
||||
icon={item.icon}
|
||||
onPress={() => {
|
||||
dispatch(setReaderSettings('textAlign', item.value));
|
||||
}}
|
||||
selected={item.value === reader.textAlign}
|
||||
theme={theme}
|
||||
/>
|
||||
))}
|
||||
</Row>
|
||||
</Pressable>
|
||||
<List.Divider theme={theme} />
|
||||
<List.SubHeader theme={theme}>
|
||||
Custom CSS (Only works in webview mode)
|
||||
</List.SubHeader>
|
||||
<View style={{paddingHorizontal: 16, paddingBottom: 8}}>
|
||||
<TextInput
|
||||
style={{
|
||||
color: theme.textColorPrimary,
|
||||
fontSize: 16,
|
||||
}}
|
||||
value={customHtmlCSS}
|
||||
onChangeText={text => setcustomHtmlCSS(text)}
|
||||
placeholderTextColor={theme.textColorSecondary}
|
||||
placeholder="Example: body { color: red; }"
|
||||
multiline={true}
|
||||
/>
|
||||
<View style={{flexDirection: 'row-reverse'}}>
|
||||
<Button
|
||||
uppercase={false}
|
||||
color={theme.colorAccent}
|
||||
onPress={() =>
|
||||
dispatch(setReaderSettings('customCSS', customHtmlCSS))
|
||||
}
|
||||
>
|
||||
Save
|
||||
</Button>
|
||||
<Button
|
||||
uppercase={false}
|
||||
color={theme.colorAccent}
|
||||
onPress={() => {
|
||||
setcustomHtmlCSS('');
|
||||
dispatch(setReaderSettings('customCSS', ''));
|
||||
}}
|
||||
>
|
||||
Clear
|
||||
</Button>
|
||||
</View>
|
||||
</View>
|
||||
<List.Divider theme={theme} />
|
||||
<List.SubHeader theme={theme}>
|
||||
Auto scroll interval (in seconds)
|
||||
</List.SubHeader>
|
||||
<View style={{paddingHorizontal: 16, paddingBottom: 8}}>
|
||||
<TextInput
|
||||
style={{
|
||||
color: theme.textColorPrimary,
|
||||
fontSize: 16,
|
||||
}}
|
||||
defaultValue={autoScrollInterval.toString() ?? 10}
|
||||
keyboardType="numeric"
|
||||
onChangeText={text =>
|
||||
text !== '' &&
|
||||
dispatch(
|
||||
setAppSettings('autoScrollInterval', parseInt(text, 10)),
|
||||
)
|
||||
}
|
||||
/>
|
||||
</View>
|
||||
</List.Section>
|
||||
</ScrollView>
|
||||
<ColorPickerModal
|
||||
title="Reader background color"
|
||||
modalVisible={readerBackgroundModal.visible}
|
||||
color={readerBackground(reader.theme)}
|
||||
hideModal={readerBackgroundModal.hideModal}
|
||||
theme={theme}
|
||||
onSubmit={setReaderBackground}
|
||||
/>
|
||||
<ColorPickerModal
|
||||
title="Reader text color"
|
||||
modalVisible={readerTextColorModal}
|
||||
color={reader.textColor}
|
||||
hideModal={hideReaderTextColorModal}
|
||||
theme={theme}
|
||||
onSubmit={setReaderTextColor}
|
||||
/>
|
||||
</View>
|
||||
);
|
||||
});
|
||||
|
||||
const SecondRoute = React.memo(({reader}) => (
|
||||
<View style={{flex: 1}}>
|
||||
<WebView
|
||||
originWhitelist={['*']}
|
||||
style={{
|
||||
backgroundColor: readerBackground(reader.theme),
|
||||
}}
|
||||
source={{
|
||||
html: `
|
||||
nestedScrollEnabled={true}
|
||||
source={{
|
||||
html: `
|
||||
<html>
|
||||
<head>
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0, maximum-scale=1.0">
|
||||
@@ -291,69 +143,410 @@ const SecondRoute = React.memo(({reader}) => (
|
||||
</body>
|
||||
</html>
|
||||
`,
|
||||
}}
|
||||
/>
|
||||
</View>
|
||||
));
|
||||
}}
|
||||
/>
|
||||
) : (
|
||||
<View
|
||||
style={{
|
||||
backgroundColor: readerBackground(reader.theme),
|
||||
padding: `${reader.padding}%`,
|
||||
}}
|
||||
>
|
||||
<Text
|
||||
style={{
|
||||
color: reader.textColor,
|
||||
fontSize: reader.textSize,
|
||||
lineHeight: readerLineHeight(
|
||||
reader.textSize,
|
||||
reader.lineHeight,
|
||||
),
|
||||
fontFamily: reader.fontFamily,
|
||||
}}
|
||||
>
|
||||
Lorem ipsum dolor sit amet, consectetur adipiscing elit. Aenean
|
||||
lobortis, diam sed malesuada bibendum, nulla libero scelerisque
|
||||
sapien, nec interdum nisl ipsum ac ipsum. In lacinia eros ut
|
||||
quam commodo, in finibus augue ultricies. Vestibulum ex purus,
|
||||
condimentum eget sem at, molestie semper mi. Mauris ac feugiat
|
||||
quam. Pellentesque sagittis bibendum nibh eu lacinia. Aenean
|
||||
rhoncus, velit sit amet mollis egestas, diam turpis ornare
|
||||
velit, a dictum elit velit in erat. Quisque luctus in sem a
|
||||
vulputate.{'\n\n'}Pellentesque id tempus orci, non finibus
|
||||
tortor. Suspendisse in neque non eros eleifend hendrerit vitae a
|
||||
lacus. Vestibulum ante ipsum primis in faucibus orci luctus et
|
||||
ultrices posuere cubilia curae; Nunc interdum magna nulla, eget
|
||||
pharetra arcu dapibus eu. Pellentesque sed lectus sit amet sem
|
||||
porta lacinia. Curabitur consectetur dolor in nibh varius, ac
|
||||
placerat erat pellentesque. Proin in justo condimentum,
|
||||
fermentum lacus eget, ultrices ante. Fusce tempor blandit erat
|
||||
nec mollis. Vivamus in leo et nunc consectetur elementum nec ac
|
||||
lectus.
|
||||
</Text>
|
||||
</View>
|
||||
)}
|
||||
<SwitchSetting
|
||||
label="Render HTML"
|
||||
description="Render chapter as html"
|
||||
value={useWebViewForChapter}
|
||||
onPress={() =>
|
||||
dispatch(
|
||||
setAppSettings('useWebViewForChapter', !useWebViewForChapter),
|
||||
)
|
||||
}
|
||||
theme={theme}
|
||||
/>
|
||||
<SwitchSetting
|
||||
label="Horizontal seekbar"
|
||||
description="Show horizontal seekbar instead of vertical"
|
||||
value={verticalSeekbar}
|
||||
onPress={() =>
|
||||
dispatch(setAppSettings('verticalSeekbar', !verticalSeekbar))
|
||||
}
|
||||
theme={theme}
|
||||
/>
|
||||
<SwitchSetting
|
||||
label="Gesture navigation"
|
||||
description="Use left and right swipe to navigate to previous or next chapter"
|
||||
value={swipeGestures}
|
||||
onPress={() =>
|
||||
dispatch(setAppSettings('swipeGestures', !swipeGestures))
|
||||
}
|
||||
theme={theme}
|
||||
/>
|
||||
<List.Divider theme={theme} />
|
||||
<List.SubHeader theme={theme}>
|
||||
Auto scroll interval (in seconds)
|
||||
</List.SubHeader>
|
||||
<View style={{paddingHorizontal: 16, paddingBottom: 8}}>
|
||||
<TextInput
|
||||
style={{
|
||||
color: theme.textColorPrimary,
|
||||
fontSize: 16,
|
||||
}}
|
||||
defaultValue={autoScrollInterval.toString() ?? 10}
|
||||
keyboardType="numeric"
|
||||
onChangeText={text =>
|
||||
text !== '' &&
|
||||
dispatch(
|
||||
setAppSettings('autoScrollInterval', parseInt(text, 10)),
|
||||
)
|
||||
}
|
||||
/>
|
||||
</View>
|
||||
<List.Divider theme={theme} />
|
||||
<List.SubHeader theme={theme}>Display</List.SubHeader>
|
||||
<SwitchSetting
|
||||
label="Fullscreen"
|
||||
value={fullScreenMode}
|
||||
onPress={() =>
|
||||
dispatch(setAppSettings('fullScreenMode', !fullScreenMode))
|
||||
}
|
||||
theme={theme}
|
||||
/>
|
||||
<SwitchSetting
|
||||
label="Show progress percentage"
|
||||
value={showScrollPercentage}
|
||||
onPress={() =>
|
||||
dispatch(
|
||||
setAppSettings('showScrollPercentage', !showScrollPercentage),
|
||||
)
|
||||
}
|
||||
theme={theme}
|
||||
/>
|
||||
|
||||
const ReaderSettings = ({navigation}) => {
|
||||
const theme = useTheme();
|
||||
const dispatch = useDispatch();
|
||||
<SwitchSetting
|
||||
label="Show battery and time"
|
||||
value={showBatteryAndTime}
|
||||
onPress={() =>
|
||||
dispatch(
|
||||
setAppSettings('showBatteryAndTime', !showBatteryAndTime),
|
||||
)
|
||||
}
|
||||
theme={theme}
|
||||
/>
|
||||
<List.Divider theme={theme} />
|
||||
|
||||
const reader = useReaderSettings();
|
||||
<List.SubHeader theme={theme}>Reader</List.SubHeader>
|
||||
<View style={styles.pressableListItem}>
|
||||
<Text
|
||||
style={{
|
||||
color: theme.textColorPrimary,
|
||||
fontSize: 16,
|
||||
}}
|
||||
>
|
||||
Themes
|
||||
</Text>
|
||||
<ScrollView
|
||||
contentContainerStyle={{
|
||||
flexDirection: 'column-reverse',
|
||||
alignItems: 'flex-end',
|
||||
}}
|
||||
style={{marginLeft: 16}}
|
||||
horizontal={true}
|
||||
showsHorizontalScrollIndicator={false}
|
||||
>
|
||||
<Row>
|
||||
{presetThemes.map(item => (
|
||||
<ToggleColorButton
|
||||
key={item.value}
|
||||
selected={reader.theme === item.backgroundColor}
|
||||
backgroundColor={item.backgroundColor}
|
||||
textColor={item.textColor}
|
||||
onPress={() => {
|
||||
dispatch(
|
||||
setReaderSettings('theme', item.backgroundColor),
|
||||
);
|
||||
dispatch(setReaderSettings('textColor', item.textColor));
|
||||
}}
|
||||
/>
|
||||
))}
|
||||
</Row>
|
||||
</ScrollView>
|
||||
</View>
|
||||
<List.ColorItem
|
||||
title="Background Color"
|
||||
description={readerBackground(reader.theme).toUpperCase()}
|
||||
onPress={readerBackgroundModal.showModal}
|
||||
theme={theme}
|
||||
/>
|
||||
<List.ColorItem
|
||||
title="Text Color"
|
||||
description={
|
||||
reader.textColor.toUpperCase() || readerTextColor(reader.theme)
|
||||
}
|
||||
onPress={readerTextColorModal.showModal}
|
||||
theme={theme}
|
||||
/>
|
||||
<Pressable style={styles.pressableListItem}>
|
||||
<View>
|
||||
<Text style={{color: theme.textColorPrimary, fontSize: 16}}>
|
||||
Text Align
|
||||
</Text>
|
||||
<Text
|
||||
style={{
|
||||
color: theme.textColorSecondary,
|
||||
textTransform: 'capitalize',
|
||||
}}
|
||||
>
|
||||
{reader.textAlign}
|
||||
</Text>
|
||||
</View>
|
||||
<Row>
|
||||
{textAlignments.map(item => (
|
||||
<ToggleButton
|
||||
key={item.value}
|
||||
icon={item.icon}
|
||||
onPress={() => {
|
||||
dispatch(setReaderSettings('textAlign', item.value));
|
||||
}}
|
||||
selected={item.value === reader.textAlign}
|
||||
theme={theme}
|
||||
/>
|
||||
))}
|
||||
</Row>
|
||||
</Pressable>
|
||||
<List.Item
|
||||
title="Font"
|
||||
description={
|
||||
fonts.find(item => item.fontFamily === reader.fontFamily)?.name
|
||||
}
|
||||
onPress={readerFontPickerModal.showModal}
|
||||
theme={theme}
|
||||
/>
|
||||
<Row
|
||||
style={{
|
||||
justifyContent: 'space-between',
|
||||
marginHorizontal: 16,
|
||||
marginVertical: 8,
|
||||
}}
|
||||
>
|
||||
<Text style={{color: theme.textColorPrimary, fontSize: 16}}>
|
||||
Font size
|
||||
</Text>
|
||||
<Row>
|
||||
<IconButton
|
||||
icon="minus"
|
||||
color={theme.colorAccent}
|
||||
size={26}
|
||||
disabled={reader.textSize <= 1 ? true : false}
|
||||
onPress={() =>
|
||||
dispatch(setReaderSettings('textSize', reader.textSize - 1))
|
||||
}
|
||||
style={{marginVertical: 0}}
|
||||
/>
|
||||
<Text
|
||||
style={{
|
||||
color: theme.textColorPrimary,
|
||||
paddingHorizontal: 24,
|
||||
}}
|
||||
>
|
||||
{reader.textSize}
|
||||
</Text>
|
||||
<IconButton
|
||||
icon="plus"
|
||||
color={theme.colorAccent}
|
||||
size={26}
|
||||
onPress={() =>
|
||||
dispatch(setReaderSettings('textSize', reader.textSize + 1))
|
||||
}
|
||||
style={{marginVertical: 0}}
|
||||
/>
|
||||
</Row>
|
||||
</Row>
|
||||
<Row
|
||||
style={{
|
||||
justifyContent: 'space-between',
|
||||
marginHorizontal: 16,
|
||||
marginVertical: 8,
|
||||
}}
|
||||
>
|
||||
<Text style={{color: theme.textColorPrimary, fontSize: 16}}>
|
||||
Padding
|
||||
</Text>
|
||||
<Row>
|
||||
<IconButton
|
||||
icon="minus"
|
||||
color={theme.colorAccent}
|
||||
size={26}
|
||||
disabled={reader.padding <= 0 ? true : false}
|
||||
onPress={() =>
|
||||
dispatch(setReaderSettings('padding', reader.padding - 1))
|
||||
}
|
||||
style={{marginVertical: 0}}
|
||||
/>
|
||||
<Text
|
||||
style={{
|
||||
color: theme.textColorPrimary,
|
||||
paddingHorizontal: 24,
|
||||
}}
|
||||
>
|
||||
{`${reader.padding}%`}
|
||||
</Text>
|
||||
<IconButton
|
||||
icon="plus"
|
||||
color={theme.colorAccent}
|
||||
size={26}
|
||||
disabled={reader.padding >= 10 ? true : false}
|
||||
onPress={() =>
|
||||
dispatch(setReaderSettings('padding', reader.padding + 1))
|
||||
}
|
||||
style={{marginVertical: 0}}
|
||||
/>
|
||||
</Row>
|
||||
</Row>
|
||||
<Row
|
||||
style={{
|
||||
justifyContent: 'space-between',
|
||||
marginHorizontal: 16,
|
||||
marginVertical: 8,
|
||||
}}
|
||||
>
|
||||
<Text style={{color: theme.textColorPrimary, fontSize: 16}}>
|
||||
Line height
|
||||
</Text>
|
||||
<Row>
|
||||
<IconButton
|
||||
icon="minus"
|
||||
color={theme.colorAccent}
|
||||
size={26}
|
||||
disabled={reader.lineHeight <= 1.3 ? true : false}
|
||||
onPress={() =>
|
||||
dispatch(
|
||||
setReaderSettings('lineHeight', reader.lineHeight - 0.1),
|
||||
)
|
||||
}
|
||||
style={{marginVertical: 0}}
|
||||
/>
|
||||
<Text
|
||||
style={{
|
||||
color: theme.textColorPrimary,
|
||||
paddingHorizontal: 24,
|
||||
}}
|
||||
>
|
||||
{`${Math.round(reader.lineHeight * 10) / 10}%`}
|
||||
</Text>
|
||||
<IconButton
|
||||
icon="plus"
|
||||
color={theme.colorAccent}
|
||||
size={26}
|
||||
disabled={reader.lineHeight >= 2 ? true : false}
|
||||
onPress={() =>
|
||||
dispatch(
|
||||
setReaderSettings('lineHeight', reader.lineHeight + 0.1),
|
||||
)
|
||||
}
|
||||
style={{marginVertical: 0}}
|
||||
/>
|
||||
</Row>
|
||||
</Row>
|
||||
{useWebViewForChapter ? (
|
||||
<>
|
||||
<List.Divider theme={theme} />
|
||||
<List.SubHeader theme={theme}>Custom CSS</List.SubHeader>
|
||||
<View style={{paddingHorizontal: 16, paddingBottom: 8}}>
|
||||
<TextInput
|
||||
style={{color: theme.textColorPrimary, fontSize: 16}}
|
||||
value={customCSS}
|
||||
onChangeText={text => setcustomCSS(text)}
|
||||
placeholderTextColor={theme.textColorSecondary}
|
||||
placeholder="Example: body { color: red; }"
|
||||
multiline={true}
|
||||
/>
|
||||
<View style={{flexDirection: 'row-reverse'}}>
|
||||
<Button
|
||||
uppercase={false}
|
||||
color={theme.colorAccent}
|
||||
onPress={() =>
|
||||
dispatch(setReaderSettings('customCSS', customCSS))
|
||||
}
|
||||
>
|
||||
Save
|
||||
</Button>
|
||||
<Button
|
||||
uppercase={false}
|
||||
color={theme.colorAccent}
|
||||
onPress={() => {
|
||||
setcustomCSS('');
|
||||
dispatch(setReaderSettings('customCSS', ''));
|
||||
}}
|
||||
>
|
||||
Clear
|
||||
</Button>
|
||||
</View>
|
||||
</View>
|
||||
</>
|
||||
) : null}
|
||||
</ScrollView>
|
||||
</KeyboardAvoidingView>
|
||||
|
||||
const renderScene = ({route}) => {
|
||||
switch (route.key) {
|
||||
case 'first':
|
||||
return <FirstRoute dispatch={dispatch} theme={theme} reader={reader} />;
|
||||
case 'second':
|
||||
return <SecondRoute reader={reader} />;
|
||||
}
|
||||
};
|
||||
|
||||
const layout = useWindowDimensions();
|
||||
|
||||
const [index, setIndex] = useState(0);
|
||||
const [routes] = useState([
|
||||
{key: 'first', title: 'Settings'},
|
||||
{key: 'second', title: 'Preview'},
|
||||
]);
|
||||
|
||||
const renderTabBar = props => (
|
||||
<TabBar
|
||||
{...props}
|
||||
indicatorStyle={{backgroundColor: theme.colorAccent}}
|
||||
style={{backgroundColor: theme.colorPrimary}}
|
||||
renderLabel={({route, focused, color}) => (
|
||||
<Text style={{color}}>{route.title}</Text>
|
||||
)}
|
||||
inactiveColor={theme.textColorSecondary}
|
||||
activeColor={theme.colorAccent}
|
||||
pressColor={theme.rippleColor}
|
||||
/>
|
||||
);
|
||||
|
||||
return (
|
||||
<ScreenContainer theme={theme}>
|
||||
<Appbar
|
||||
title="Reader"
|
||||
onBackAction={navigation.goBack}
|
||||
style={{elevation: 0}}
|
||||
<ColorPickerModal
|
||||
title="Reader background color"
|
||||
modalVisible={readerBackgroundModal.visible}
|
||||
color={readerBackground(reader.theme)}
|
||||
hideModal={readerBackgroundModal.hideModal}
|
||||
theme={theme}
|
||||
onSubmit={setReaderBackground}
|
||||
/>
|
||||
|
||||
<TabView
|
||||
navigationState={{index, routes}}
|
||||
renderTabBar={renderTabBar}
|
||||
renderScene={renderScene}
|
||||
onIndexChange={setIndex}
|
||||
initialLayout={{width: layout.width}}
|
||||
swipeEnabled={false}
|
||||
<ColorPickerModal
|
||||
title="Reader text color"
|
||||
modalVisible={readerTextColorModal.visible}
|
||||
color={reader.textColor}
|
||||
hideModal={readerTextColorModal.hideModal}
|
||||
theme={theme}
|
||||
onSubmit={setReaderTextColor}
|
||||
/>
|
||||
</ScreenContainer>
|
||||
<FontPickerModal
|
||||
currentFont={reader.fontFamily}
|
||||
visible={readerFontPickerModal.visible}
|
||||
hideModal={readerFontPickerModal.hideModal}
|
||||
theme={theme}
|
||||
dispatch={dispatch}
|
||||
/>
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
export default ReaderSettings;
|
||||
export default SettingsReaderScreen;
|
||||
|
||||
const styles = StyleSheet.create({
|
||||
pressableListItem: {
|
||||
@@ -362,13 +555,14 @@ const styles = StyleSheet.create({
|
||||
justifyContent: 'space-between',
|
||||
alignItems: 'center',
|
||||
},
|
||||
presetToggleButton: {
|
||||
marginLeft: 12,
|
||||
borderRadius: 50,
|
||||
borderTopRightRadius: 50,
|
||||
borderBottomRightRadius: 50,
|
||||
borderTopLeftRadius: 50,
|
||||
borderBottomLeftRadius: 50,
|
||||
borderWidth: 0,
|
||||
fontContainer: {
|
||||
borderRadius: 8,
|
||||
overflow: 'hidden',
|
||||
marginHorizontal: 6,
|
||||
},
|
||||
fontContent: {
|
||||
borderRadius: 8,
|
||||
paddingHorizontal: 10,
|
||||
paddingVertical: 4,
|
||||
},
|
||||
});
|
||||
|
||||
@@ -0,0 +1,61 @@
|
||||
import React from 'react';
|
||||
import {StyleSheet} from 'react-native';
|
||||
|
||||
import {Portal, Modal} from 'react-native-paper';
|
||||
import {SortItem} from '../../../components/Checkbox/Checkbox';
|
||||
|
||||
import {setAppSettings} from '../../../redux/settings/settings.actions';
|
||||
|
||||
const DefaultChapterSortModal = ({
|
||||
theme,
|
||||
dispatch,
|
||||
defaultChapterSort,
|
||||
hideDisplayModal,
|
||||
displayModalVisible,
|
||||
}) => {
|
||||
return (
|
||||
<Portal>
|
||||
<Modal
|
||||
visible={displayModalVisible}
|
||||
onDismiss={hideDisplayModal}
|
||||
contentContainerStyle={[
|
||||
styles.containerStyle,
|
||||
{backgroundColor: theme.colorPrimaryDark},
|
||||
]}
|
||||
>
|
||||
<SortItem
|
||||
label="By source"
|
||||
theme={theme}
|
||||
status={
|
||||
defaultChapterSort === 'ORDER BY chapterId ASC' ? 'asc' : 'desc'
|
||||
}
|
||||
onPress={() =>
|
||||
defaultChapterSort === 'ORDER BY chapterId ASC'
|
||||
? dispatch(
|
||||
setAppSettings(
|
||||
'defaultChapterSort',
|
||||
'ORDER BY chapterId DESC',
|
||||
),
|
||||
)
|
||||
: dispatch(
|
||||
setAppSettings(
|
||||
'defaultChapterSort',
|
||||
'ORDER BY chapterId ASC',
|
||||
),
|
||||
)
|
||||
}
|
||||
/>
|
||||
</Modal>
|
||||
</Portal>
|
||||
);
|
||||
};
|
||||
|
||||
export default DefaultChapterSortModal;
|
||||
|
||||
const styles = StyleSheet.create({
|
||||
containerStyle: {
|
||||
paddingVertical: 20,
|
||||
margin: 20,
|
||||
borderRadius: 6,
|
||||
},
|
||||
});
|
||||
@@ -0,0 +1,53 @@
|
||||
import React from 'react';
|
||||
import {StyleSheet} from 'react-native';
|
||||
|
||||
import {Portal, Modal} from 'react-native-paper';
|
||||
|
||||
import {RadioButton} from '../../../components/RadioButton/RadioButton';
|
||||
|
||||
import {setReaderSettings} from '../../../redux/settings/settings.actions';
|
||||
import {fonts} from '../../../services/utils/constants';
|
||||
|
||||
const FontPickerModal = ({
|
||||
theme,
|
||||
dispatch,
|
||||
currentFont,
|
||||
hideModal,
|
||||
visible,
|
||||
}) => {
|
||||
return (
|
||||
<Portal>
|
||||
<Modal
|
||||
visible={visible}
|
||||
onDismiss={hideModal}
|
||||
contentContainerStyle={[
|
||||
styles.containerStyle,
|
||||
{backgroundColor: theme.colorPrimaryDark},
|
||||
]}
|
||||
>
|
||||
{fonts.map(item => (
|
||||
<RadioButton
|
||||
key={item.fontFamily}
|
||||
status={currentFont === item.fontFamily}
|
||||
onPress={() =>
|
||||
dispatch(setReaderSettings('fontFamily', item.fontFamily))
|
||||
}
|
||||
label={item.name}
|
||||
labelStyle={{fontFamily: item.fontFamily}}
|
||||
theme={theme}
|
||||
/>
|
||||
))}
|
||||
</Modal>
|
||||
</Portal>
|
||||
);
|
||||
};
|
||||
|
||||
export default FontPickerModal;
|
||||
|
||||
const styles = StyleSheet.create({
|
||||
containerStyle: {
|
||||
paddingVertical: 20,
|
||||
margin: 20,
|
||||
borderRadius: 6,
|
||||
},
|
||||
});
|
||||
@@ -87,12 +87,13 @@ const SourceScreen = ({navigation, route}) => {
|
||||
useEffect(() => {
|
||||
getNovels();
|
||||
|
||||
return () => {
|
||||
isMounted.current = false;
|
||||
};
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
}, [page]);
|
||||
|
||||
useEffect(() => {
|
||||
return () => (isMounted.current = false);
|
||||
}, []);
|
||||
|
||||
const getNextPage = () => {
|
||||
if (!searchText) {
|
||||
incrementPage();
|
||||
|
||||
@@ -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);
|
||||
// });
|
||||
// }
|
||||
// };
|
||||
@@ -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};
|
||||
@@ -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};
|
||||
@@ -0,0 +1,9 @@
|
||||
import FastImage from 'react-native-fast-image';
|
||||
import {showToast} from '../../hooks/showToast';
|
||||
|
||||
const clearCoverCache = () => {
|
||||
FastImage.clearDiskCache();
|
||||
showToast('Cleared cover cache.');
|
||||
};
|
||||
|
||||
export {clearCoverCache};
|
||||
+10
-23
@@ -7,7 +7,7 @@ const sourceName = 'ArNovel';
|
||||
const baseUrl = 'https://arnovel.me/';
|
||||
|
||||
const popularNovels = async page => {
|
||||
const totalPages = 21;
|
||||
const totalPages = 25;
|
||||
const url = baseUrl + 'novels/page/' + page;
|
||||
|
||||
const result = await fetch(url);
|
||||
@@ -20,11 +20,7 @@ const popularNovels = async page => {
|
||||
loadedCheerio('.page-item-detail').each(function () {
|
||||
const novelName = loadedCheerio(this).find('.h5 > a').text();
|
||||
const novelCover = defaultCoverUri;
|
||||
|
||||
let novelUrl = loadedCheerio(this)
|
||||
.find('.h5 > a')
|
||||
.attr('href')
|
||||
.split('/')[4];
|
||||
const novelUrl = loadedCheerio(this).find('.h5 > a').attr('href');
|
||||
|
||||
const novel = {
|
||||
sourceId,
|
||||
@@ -40,7 +36,7 @@ const popularNovels = async page => {
|
||||
};
|
||||
|
||||
const parseNovelAndChapters = async novelUrl => {
|
||||
const url = `${baseUrl}novel/${novelUrl}`;
|
||||
const url = novelUrl;
|
||||
|
||||
const result = await fetch(url);
|
||||
const body = await result.text();
|
||||
@@ -83,20 +79,15 @@ const parseNovelAndChapters = async novelUrl => {
|
||||
|
||||
let novelChapters = [];
|
||||
|
||||
const data = await fetch(`${url}/ajax/chapters/`, {method: 'POST'});
|
||||
const text = await data.text();
|
||||
const data = await fetch(`${url}ajax/chapters/`, {method: 'POST'});
|
||||
const chapterListRaw = await data.text();
|
||||
|
||||
loadedCheerio = cheerio.load(text);
|
||||
loadedCheerio = cheerio.load(chapterListRaw);
|
||||
|
||||
loadedCheerio('.wp-manga-chapter').each(function () {
|
||||
const chapterName = loadedCheerio(this).find('a').text().trim();
|
||||
const releaseDate = loadedCheerio(this).find('span').text().trim();
|
||||
|
||||
let chapterUrl = loadedCheerio(this).find('a').attr('href').split('/');
|
||||
|
||||
chapterUrl[6]
|
||||
? (chapterUrl = chapterUrl[5] + '/' + chapterUrl[6])
|
||||
: (chapterUrl = chapterUrl[5]);
|
||||
const releaseDate = null;
|
||||
const chapterUrl = loadedCheerio(this).find('a').attr('href');
|
||||
|
||||
novelChapters.push({chapterName, releaseDate, chapterUrl});
|
||||
});
|
||||
@@ -107,7 +98,7 @@ const parseNovelAndChapters = async novelUrl => {
|
||||
};
|
||||
|
||||
const parseChapter = async (novelUrl, chapterUrl) => {
|
||||
const url = `${baseUrl}novel/${novelUrl}/${chapterUrl}/`;
|
||||
const url = chapterUrl;
|
||||
|
||||
const result = await fetch(url);
|
||||
const body = await result.text();
|
||||
@@ -141,11 +132,7 @@ const searchNovels = async searchTerm => {
|
||||
loadedCheerio('.c-tabs-item__content').each(function () {
|
||||
const novelName = loadedCheerio(this).find('.h4 > a').text();
|
||||
const novelCover = defaultCoverUri;
|
||||
|
||||
let novelUrl = loadedCheerio(this)
|
||||
.find('.h4 > a')
|
||||
.attr('href')
|
||||
.split('/')[4];
|
||||
const novelUrl = loadedCheerio(this).find('.h4 > a').attr('href');
|
||||
|
||||
const novel = {
|
||||
sourceId,
|
||||
|
||||
@@ -0,0 +1,141 @@
|
||||
import cheerio from 'react-native-cheerio';
|
||||
import {Status} from '../helpers/constants';
|
||||
|
||||
const sourceId = 54;
|
||||
const sourceName = 'RewayatAr';
|
||||
|
||||
const baseUrl = 'https://rewayat-ar.site/';
|
||||
|
||||
const popularNovels = async page => {
|
||||
const totalPages = 2;
|
||||
const url = baseUrl + 'novels/topview/page/' + page;
|
||||
|
||||
const result = await fetch(url);
|
||||
const body = await result.text();
|
||||
|
||||
const loadedCheerio = cheerio.load(body);
|
||||
|
||||
let novels = [];
|
||||
|
||||
loadedCheerio('.update_item.list_category').each(function () {
|
||||
const novelName = loadedCheerio(this).find('h3 > a').text();
|
||||
const novelCover = loadedCheerio(this).find('img').attr('src');
|
||||
|
||||
let novelUrl = loadedCheerio(this).find('h3 > a').attr('href');
|
||||
|
||||
const novel = {
|
||||
sourceId,
|
||||
novelName,
|
||||
novelCover,
|
||||
novelUrl,
|
||||
};
|
||||
|
||||
novels.push(novel);
|
||||
});
|
||||
|
||||
return {totalPages, novels};
|
||||
};
|
||||
|
||||
const parseNovelAndChapters = async novelUrl => {
|
||||
const url = novelUrl;
|
||||
|
||||
const result = await fetch(url);
|
||||
const body = await result.text();
|
||||
|
||||
let loadedCheerio = cheerio.load(body);
|
||||
|
||||
let novel = {
|
||||
sourceId,
|
||||
sourceName,
|
||||
url,
|
||||
novelUrl,
|
||||
novelName: '',
|
||||
novelCover: '',
|
||||
author: '',
|
||||
status: Status.UNKNOWN,
|
||||
genre: '',
|
||||
summary: '',
|
||||
chapters: [],
|
||||
};
|
||||
|
||||
novel.novelName = loadedCheerio('.truyen_info_right h1').text().trim();
|
||||
novel.novelCover = loadedCheerio('.info_image img').attr('src');
|
||||
novel.summary = loadedCheerio('#noidungm').text()?.trim();
|
||||
novel.author = loadedCheerio(
|
||||
'#main_body > div.cotgiua > div.truyen_info_wrapper > div.truyen_info > div.entry-header > div.truyen_if_wrap > ul > li:nth-child(2) > a',
|
||||
)
|
||||
.text()
|
||||
?.trim();
|
||||
|
||||
let novelChapters = [];
|
||||
|
||||
loadedCheerio('.chapter-list div.row').each(function () {
|
||||
const chapterName = loadedCheerio(this).find('a').text().trim();
|
||||
const releaseDate = loadedCheerio(this)
|
||||
.find('span:nth-child(2)')
|
||||
.text()
|
||||
.trim();
|
||||
const chapterUrl = loadedCheerio(this).find('a').attr('href');
|
||||
|
||||
novelChapters.push({chapterName, releaseDate, chapterUrl});
|
||||
});
|
||||
|
||||
novel.chapters = novelChapters.reverse();
|
||||
|
||||
return novel;
|
||||
};
|
||||
|
||||
const parseChapter = async (novelUrl, chapterUrl) => {
|
||||
const url = chapterUrl;
|
||||
|
||||
const result = await fetch(url);
|
||||
const body = await result.text();
|
||||
|
||||
let loadedCheerio = cheerio.load(body);
|
||||
|
||||
const chapterName = loadedCheerio('h1.name_chapter').text();
|
||||
const chapterText = loadedCheerio('#vung_doc').html();
|
||||
|
||||
const chapter = {
|
||||
sourceId,
|
||||
novelUrl,
|
||||
chapterUrl,
|
||||
chapterName,
|
||||
chapterText,
|
||||
};
|
||||
|
||||
return chapter;
|
||||
};
|
||||
|
||||
const searchNovels = async searchTerm => {
|
||||
const url = `${baseUrl}search/${searchTerm}`;
|
||||
|
||||
const result = await fetch(url);
|
||||
const body = await result.text();
|
||||
|
||||
let loadedCheerio = cheerio.load(body);
|
||||
|
||||
let novels = [];
|
||||
|
||||
loadedCheerio('.update_item.list_category').each(function () {
|
||||
const novelName = loadedCheerio(this).find('h3 > a').text();
|
||||
const novelCover = loadedCheerio(this).find('img').attr('src');
|
||||
|
||||
let novelUrl = loadedCheerio(this).find('h3 > a').attr('href');
|
||||
|
||||
const novel = {sourceId, novelName, novelCover, novelUrl};
|
||||
|
||||
novels.push(novel);
|
||||
});
|
||||
|
||||
return novels;
|
||||
};
|
||||
|
||||
const RewayatArScraper = {
|
||||
popularNovels,
|
||||
parseNovelAndChapters,
|
||||
parseChapter,
|
||||
searchNovels,
|
||||
};
|
||||
|
||||
export default RewayatArScraper;
|
||||
@@ -89,7 +89,7 @@ const parseNovelAndChapters = async novelUrl => {
|
||||
|
||||
let chapters = [];
|
||||
|
||||
loadedCheerio('.bookshelf-list #chapter').each(function () {
|
||||
loadedCheerio('.bookshelf-list #chapter a').each(function () {
|
||||
let chapterUrl = loadedCheerio(this).attr('href').substring(1);
|
||||
let chapterName = loadedCheerio(this).text();
|
||||
let releaseDate = null;
|
||||
@@ -134,11 +134,11 @@ const parseChapter = async (novelUrl, chapterUrl) => {
|
||||
};
|
||||
|
||||
const searchNovels = async searchTerm => {
|
||||
const searchUrl = `${baseUrl}/search.html`;
|
||||
const searchUrl = `${baseUrl}/so.html`;
|
||||
|
||||
const result = await fetch(searchUrl, {
|
||||
method: 'post',
|
||||
body: JSON.stringify({keyword: searchTerm}),
|
||||
body: JSON.stringify({ss: searchTerm}),
|
||||
headers: {
|
||||
'User-Agent':
|
||||
"'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/83.0.4103.116 Safari/537.36",
|
||||
|
||||
@@ -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;
|
||||
@@ -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;
|
||||
@@ -0,0 +1,161 @@
|
||||
import cheerio from 'react-native-cheerio';
|
||||
import {defaultCoverUri, Status} from '../helpers/constants';
|
||||
|
||||
const sourceId = 89;
|
||||
const sourceName = 'IndoWebNovel';
|
||||
|
||||
const baseUrl = 'https://mtlcorner.com/';
|
||||
|
||||
const popularNovels = async page => {
|
||||
const totalPages = 1;
|
||||
const url = `${baseUrl}series/`;
|
||||
|
||||
const result = await fetch(url);
|
||||
const body = await result.text();
|
||||
|
||||
const loadedCheerio = cheerio.load(body);
|
||||
|
||||
let novels = [];
|
||||
|
||||
loadedCheerio('.wp-block-latest-posts__list li').each(function () {
|
||||
const novelName = loadedCheerio(this).find('a').text();
|
||||
const novelCover = defaultCoverUri;
|
||||
const novelUrl = loadedCheerio(this).find('a').attr('href');
|
||||
|
||||
const novel = {sourceId, novelName, novelCover, novelUrl};
|
||||
|
||||
novels.push(novel);
|
||||
});
|
||||
|
||||
return {totalPages, novels};
|
||||
};
|
||||
|
||||
const parseNovelAndChapters = async novelUrl => {
|
||||
const url = novelUrl;
|
||||
|
||||
const result = await fetch(url);
|
||||
const body = await result.text();
|
||||
|
||||
let loadedCheerio = cheerio.load(body);
|
||||
|
||||
let novel = {
|
||||
sourceId,
|
||||
sourceName,
|
||||
url,
|
||||
novelUrl,
|
||||
novelName: '',
|
||||
novelCover: '',
|
||||
author: '',
|
||||
status: '',
|
||||
genre: '',
|
||||
summary: '',
|
||||
chapters: [],
|
||||
};
|
||||
|
||||
const novelName = loadedCheerio('.entry-title').text().trim();
|
||||
novel.novelName = novelName;
|
||||
|
||||
novel.novelCover = loadedCheerio('figure > img').attr('src');
|
||||
|
||||
novel.summary = loadedCheerio('figcaption').text().trim();
|
||||
|
||||
novel.status = Status.UNKNOWN;
|
||||
|
||||
novel.author = loadedCheerio(
|
||||
' li.post-author.meta-wrapper > span.meta-text > a',
|
||||
)
|
||||
.text()
|
||||
?.trim();
|
||||
|
||||
let chapters = [];
|
||||
|
||||
loadedCheerio('.lcp_catlist li').each(function () {
|
||||
let chapterNameSuffix = loadedCheerio(this)
|
||||
.find('a')
|
||||
.text()
|
||||
?.trim()
|
||||
.match(/([a-zA-Z\s?]+)(\d+)-(\d+)/);
|
||||
|
||||
if (chapterNameSuffix) {
|
||||
let chaptersTemp = [];
|
||||
|
||||
let chapterUrlSuffix = loadedCheerio(this).find('a').attr('href');
|
||||
|
||||
let i = chapterNameSuffix[2];
|
||||
|
||||
while (i <= chapterNameSuffix[3]) {
|
||||
const chapterName = `${chapterNameSuffix[1].replace(
|
||||
novelName,
|
||||
'',
|
||||
)} ${i}`;
|
||||
const chapterUrl = `${chapterUrlSuffix}${i - chapterNameSuffix[2] + 1}`;
|
||||
const releaseDate = null;
|
||||
|
||||
chaptersTemp.push({chapterName, releaseDate, chapterUrl});
|
||||
i++;
|
||||
}
|
||||
|
||||
chapters.push(...chaptersTemp.reverse());
|
||||
}
|
||||
});
|
||||
|
||||
novel.chapters = chapters.reverse();
|
||||
|
||||
return novel;
|
||||
};
|
||||
|
||||
const parseChapter = async (novelUrl, chapterUrl) => {
|
||||
const url = chapterUrl;
|
||||
|
||||
const result = await fetch(url);
|
||||
const body = await result.text();
|
||||
|
||||
let loadedCheerio = cheerio.load(body);
|
||||
|
||||
const chapterName = '';
|
||||
const chapterText = loadedCheerio('.entry-content').html();
|
||||
|
||||
const chapter = {
|
||||
sourceId,
|
||||
novelUrl,
|
||||
chapterUrl,
|
||||
chapterName,
|
||||
chapterText,
|
||||
};
|
||||
|
||||
return chapter;
|
||||
};
|
||||
|
||||
const searchNovels = async searchTerm => {
|
||||
const url = `${baseUrl}series/`;
|
||||
|
||||
const result = await fetch(url);
|
||||
const body = await result.text();
|
||||
|
||||
const loadedCheerio = cheerio.load(body);
|
||||
|
||||
let novels = [];
|
||||
|
||||
loadedCheerio('.wp-block-latest-posts__list li').each(function () {
|
||||
const novelName = loadedCheerio(this).find('a').text();
|
||||
const novelCover = defaultCoverUri;
|
||||
const novelUrl = loadedCheerio(this).find('a').attr('href');
|
||||
|
||||
const novel = {sourceId, novelName, novelCover, novelUrl};
|
||||
|
||||
if (novelName.toLowerCase().includes(searchTerm.toLowerCase())) {
|
||||
novels.push(novel);
|
||||
}
|
||||
});
|
||||
|
||||
return novels;
|
||||
};
|
||||
|
||||
const MTLCornerScraper = {
|
||||
popularNovels,
|
||||
parseNovelAndChapters,
|
||||
parseChapter,
|
||||
searchNovels,
|
||||
};
|
||||
|
||||
export default MTLCornerScraper;
|
||||
@@ -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()
|
||||
|
||||
@@ -0,0 +1,166 @@
|
||||
import cheerio from 'react-native-cheerio';
|
||||
import {Status} from '../helpers/constants';
|
||||
|
||||
const sourceId = 90;
|
||||
const sourceName = 'NitroScans';
|
||||
|
||||
const baseUrl = 'https://nitroscans.com/';
|
||||
|
||||
const popularNovels = async page => {
|
||||
const totalPages = 2;
|
||||
const url = baseUrl + 'manga-genre/novel/page/' + page;
|
||||
|
||||
const result = await fetch(url);
|
||||
const body = await result.text();
|
||||
|
||||
const loadedCheerio = cheerio.load(body);
|
||||
|
||||
let novels = [];
|
||||
|
||||
loadedCheerio('.page-item-detail').each(function () {
|
||||
const novelName = loadedCheerio(this).find('h3 > a').text();
|
||||
const novelCover = loadedCheerio(this).find('img').attr('data-src');
|
||||
const novelUrl = loadedCheerio(this).find('h3 > a').attr('href');
|
||||
|
||||
const novel = {sourceId, novelName, novelCover, novelUrl};
|
||||
|
||||
novels.push(novel);
|
||||
});
|
||||
|
||||
return {totalPages, novels};
|
||||
};
|
||||
|
||||
const parseNovelAndChapters = async novelUrl => {
|
||||
const url = novelUrl;
|
||||
|
||||
const result = await fetch(url);
|
||||
const body = await result.text();
|
||||
|
||||
let loadedCheerio = cheerio.load(body);
|
||||
|
||||
let novel = {
|
||||
sourceId,
|
||||
sourceName,
|
||||
url,
|
||||
novelUrl,
|
||||
novelName: '',
|
||||
novelCover: '',
|
||||
author: '',
|
||||
status: Status.UNKNOWN,
|
||||
genre: '',
|
||||
summary: '',
|
||||
chapters: [],
|
||||
};
|
||||
|
||||
loadedCheerio('.manga-title-badges.custom.novel').remove();
|
||||
|
||||
novel.novelName = loadedCheerio('.post-title > h1').text().trim();
|
||||
novel.novelCover = loadedCheerio('.summary_image')
|
||||
.find('img')
|
||||
.attr('data-src');
|
||||
|
||||
novel.summary = loadedCheerio('.summary__content').text()?.trim();
|
||||
|
||||
loadedCheerio('.post-content_item').each(function () {
|
||||
const detailName = loadedCheerio(this)
|
||||
.find('.summary-heading > h5')
|
||||
.text()
|
||||
.trim();
|
||||
const detail = loadedCheerio(this).find('.summary-content').text().trim();
|
||||
|
||||
switch (detailName) {
|
||||
case 'Genre(s)':
|
||||
novel.genre = detail.replace(/[\t\n]/g, ',');
|
||||
break;
|
||||
case 'Author(s)':
|
||||
novel.author = detail;
|
||||
break;
|
||||
case 'Status':
|
||||
novel.status = detail.includes('OnGoing')
|
||||
? Status.ONGOING
|
||||
: Status.COMPLETED;
|
||||
break;
|
||||
}
|
||||
});
|
||||
let novelChapters = [];
|
||||
|
||||
const novelId = loadedCheerio('.rating-post-id').attr('value');
|
||||
|
||||
let formData = new FormData();
|
||||
formData.append('action', 'manga_get_chapters');
|
||||
formData.append('manga', novelId);
|
||||
|
||||
const data = await fetch(baseUrl + 'wp-admin/admin-ajax.php', {
|
||||
method: 'POST',
|
||||
body: formData,
|
||||
});
|
||||
const text = await data.text();
|
||||
|
||||
loadedCheerio = cheerio.load(text);
|
||||
|
||||
loadedCheerio('.wp-manga-chapter').each(function () {
|
||||
const chapterName = loadedCheerio(this).find('a').text().trim();
|
||||
const releaseDate = loadedCheerio(this).find('span').text().trim();
|
||||
const chapterUrl = loadedCheerio(this).find('a').attr('href');
|
||||
|
||||
novelChapters.push({chapterName, releaseDate, chapterUrl});
|
||||
});
|
||||
|
||||
novel.chapters = novelChapters.reverse();
|
||||
|
||||
return novel;
|
||||
};
|
||||
|
||||
const parseChapter = async (novelUrl, chapterUrl) => {
|
||||
const url = chapterUrl;
|
||||
|
||||
const result = await fetch(url);
|
||||
const body = await result.text();
|
||||
|
||||
let loadedCheerio = cheerio.load(body);
|
||||
|
||||
const chapterName = loadedCheerio('h1#chapter-heading').text();
|
||||
const chapterText = loadedCheerio('.reading-content').html();
|
||||
|
||||
const chapter = {
|
||||
sourceId,
|
||||
novelUrl,
|
||||
chapterUrl,
|
||||
chapterName,
|
||||
chapterText,
|
||||
};
|
||||
|
||||
return chapter;
|
||||
};
|
||||
|
||||
const searchNovels = async searchTerm => {
|
||||
const url = `${baseUrl}?s=${searchTerm}&post_type=wp-manga&genre%5B%5D=novel&op=&author=&artist=&release=&adult=`;
|
||||
|
||||
const result = await fetch(url);
|
||||
const body = await result.text();
|
||||
|
||||
let loadedCheerio = cheerio.load(body);
|
||||
|
||||
let novels = [];
|
||||
|
||||
loadedCheerio('.c-tabs-item__content').each(function () {
|
||||
const novelName = loadedCheerio(this).find('h3 > a').text();
|
||||
const novelCover = loadedCheerio(this).find('img').attr('data-src');
|
||||
const novelUrl = loadedCheerio(this).find('h3 > a').attr('href');
|
||||
|
||||
const novel = {sourceId, novelName, novelCover, novelUrl};
|
||||
|
||||
novels.push(novel);
|
||||
});
|
||||
|
||||
return novels;
|
||||
};
|
||||
|
||||
const NitroScansScraper = {
|
||||
popularNovels,
|
||||
parseNovelAndChapters,
|
||||
parseChapter,
|
||||
searchNovels,
|
||||
};
|
||||
|
||||
export default NitroScansScraper;
|
||||
@@ -0,0 +1,177 @@
|
||||
import cheerio from 'react-native-cheerio';
|
||||
|
||||
const sourceId = 92;
|
||||
const sourceName = 'NovelForest';
|
||||
|
||||
const baseUrl = 'https://novelforest.com/';
|
||||
|
||||
const popularNovels = async page => {
|
||||
const totalPages = 25;
|
||||
const url = `${baseUrl}popular?page=${page}`;
|
||||
|
||||
const result = await fetch(url);
|
||||
const body = await result.text();
|
||||
|
||||
const loadedCheerio = cheerio.load(body);
|
||||
|
||||
let novels = [];
|
||||
|
||||
loadedCheerio('.book-item').each(function () {
|
||||
const novelName = loadedCheerio(this).find('.title').text();
|
||||
const novelCover =
|
||||
'https:' + loadedCheerio(this).find('img').attr('data-src');
|
||||
const novelUrl =
|
||||
baseUrl + loadedCheerio(this).find('.title a').attr('href').substring(1);
|
||||
|
||||
const novel = {sourceId, novelName, novelCover, novelUrl};
|
||||
|
||||
novels.push(novel);
|
||||
});
|
||||
|
||||
return {totalPages, novels};
|
||||
};
|
||||
|
||||
const parseNovelAndChapters = async novelUrl => {
|
||||
const url = novelUrl;
|
||||
|
||||
const result = await fetch(url);
|
||||
const body = await result.text();
|
||||
|
||||
let loadedCheerio = cheerio.load(body);
|
||||
|
||||
let novel = {
|
||||
sourceId,
|
||||
sourceName,
|
||||
url,
|
||||
novelUrl,
|
||||
novelName: '',
|
||||
novelCover: '',
|
||||
author: '',
|
||||
status: '',
|
||||
genre: '',
|
||||
summary: '',
|
||||
chapters: [],
|
||||
};
|
||||
|
||||
novel.novelName = loadedCheerio('.name h1').text().trim();
|
||||
|
||||
novel.novelCover =
|
||||
'https:' + loadedCheerio('.img-cover img').attr('data-src');
|
||||
|
||||
novel.summary = loadedCheerio(
|
||||
'body > div.layout > div.main-container.book-details > div > div.row.no-gutters > div.col-lg-8 > div.mt-1 > div.section.box.mt-1.summary > div.section-body > p.content',
|
||||
)
|
||||
.text()
|
||||
.trim();
|
||||
|
||||
novel.author = loadedCheerio(
|
||||
'body > div.layout > div.main-container.book-details > div > div.row.no-gutters > div.col-lg-8 > div.book-info > div.detail > div.meta.box.mt-1.p-10 > p:nth-child(1) > a > span',
|
||||
)
|
||||
.text()
|
||||
?.trim();
|
||||
|
||||
novel.status = loadedCheerio(
|
||||
'body > div.layout > div.main-container.book-details > div > div.row.no-gutters > div.col-lg-8 > div.book-info > div.detail > div.meta.box.mt-1.p-10 > p:nth-child(2) > a > span',
|
||||
)
|
||||
.text()
|
||||
?.trim();
|
||||
|
||||
novel.genre = loadedCheerio(
|
||||
'body > div.layout > div.main-container.book-details > div > div.row.no-gutters > div.col-lg-8 > div.book-info > div.detail > div.meta.box.mt-1.p-10 > p:nth-child(3)',
|
||||
)
|
||||
.text()
|
||||
?.replace('Genres :', '')
|
||||
.replace(/[\s\n]+/g, ' ')
|
||||
.trim();
|
||||
|
||||
let chapters = [];
|
||||
|
||||
const chaptersUrl =
|
||||
novelUrl.replace(baseUrl, 'https://novelforest.com/api/novels/') +
|
||||
'/chapters?source=detail';
|
||||
|
||||
const chaptersRequest = await fetch(chaptersUrl);
|
||||
const chaptersHtml = await chaptersRequest.text();
|
||||
|
||||
loadedCheerio = cheerio.load(chaptersHtml);
|
||||
|
||||
loadedCheerio('li').each(function () {
|
||||
const chapterName = loadedCheerio(this)
|
||||
.find('.chapter-title')
|
||||
.text()
|
||||
.trim();
|
||||
|
||||
const releaseDate = loadedCheerio(this)
|
||||
.find('.chapter-update')
|
||||
.text()
|
||||
.trim();
|
||||
|
||||
const chapterUrl =
|
||||
baseUrl + loadedCheerio(this).find('a').attr('href').substring(1);
|
||||
|
||||
chapters.push({chapterName, releaseDate, chapterUrl});
|
||||
});
|
||||
|
||||
novel.chapters = chapters.reverse();
|
||||
|
||||
return novel;
|
||||
};
|
||||
|
||||
const parseChapter = async (novelUrl, chapterUrl) => {
|
||||
const url = chapterUrl;
|
||||
|
||||
const result = await fetch(url);
|
||||
const body = await result.text();
|
||||
|
||||
let loadedCheerio = cheerio.load(body);
|
||||
|
||||
loadedCheerio('#listen-chapter').remove();
|
||||
loadedCheerio('#google_translate_element').remove();
|
||||
|
||||
const chapterName = loadedCheerio('#chapter__content > h1').text();
|
||||
const chapterText = loadedCheerio('.chapter__content').html();
|
||||
|
||||
const chapter = {
|
||||
sourceId,
|
||||
novelUrl,
|
||||
chapterUrl,
|
||||
chapterName,
|
||||
chapterText,
|
||||
};
|
||||
|
||||
return chapter;
|
||||
};
|
||||
|
||||
const searchNovels = async searchTerm => {
|
||||
const url = `${baseUrl}search?q=${searchTerm}`;
|
||||
|
||||
const result = await fetch(url);
|
||||
const body = await result.text();
|
||||
|
||||
let loadedCheerio = cheerio.load(body);
|
||||
|
||||
let novels = [];
|
||||
|
||||
loadedCheerio('.book-item').each(function () {
|
||||
const novelName = loadedCheerio(this).find('.title').text();
|
||||
const novelCover =
|
||||
'https:' + loadedCheerio(this).find('img').attr('data-src');
|
||||
const novelUrl =
|
||||
baseUrl + loadedCheerio(this).find('.title a').attr('href').substring(1);
|
||||
|
||||
const novel = {sourceId, novelName, novelCover, novelUrl};
|
||||
|
||||
novels.push(novel);
|
||||
});
|
||||
|
||||
return novels;
|
||||
};
|
||||
|
||||
const NovelForestScraper = {
|
||||
popularNovels,
|
||||
parseNovelAndChapters,
|
||||
parseChapter,
|
||||
searchNovels,
|
||||
};
|
||||
|
||||
export default NovelForestScraper;
|
||||
@@ -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,7 +39,7 @@ const parseNovelAndChapters = async novelUrl => {
|
||||
const result = await fetch(url);
|
||||
const body = await result.text();
|
||||
|
||||
const loadedCheerio = cheerio.load(body);
|
||||
let loadedCheerio = cheerio.load(body);
|
||||
|
||||
let novel = {};
|
||||
|
||||
@@ -93,6 +93,11 @@ const parseNovelAndChapters = async novelUrl => {
|
||||
|
||||
let novelChapters = [];
|
||||
|
||||
const data = await fetch(`${url}ajax/chapters/`, {method: 'POST'});
|
||||
const text = await data.text();
|
||||
|
||||
loadedCheerio = cheerio.load(text);
|
||||
|
||||
loadedCheerio('.wp-manga-chapter').each(function () {
|
||||
const chapterName = loadedCheerio(this)
|
||||
.find('a')
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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();
|
||||
|
||||
@@ -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;
|
||||
@@ -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,
|
||||
|
||||
@@ -41,7 +41,7 @@ const parseNovelAndChapters = async novelUrl => {
|
||||
const result = await fetch(url);
|
||||
const body = await result.text();
|
||||
|
||||
const loadedCheerio = cheerio.load(body);
|
||||
let loadedCheerio = cheerio.load(body);
|
||||
|
||||
let novel = {};
|
||||
|
||||
@@ -96,6 +96,11 @@ const parseNovelAndChapters = async novelUrl => {
|
||||
|
||||
let novelChapters = [];
|
||||
|
||||
const data = await fetch(`${url}ajax/chapters/`, {method: 'POST'});
|
||||
const text = await data.text();
|
||||
|
||||
loadedCheerio = cheerio.load(text);
|
||||
|
||||
loadedCheerio('.wp-manga-chapter').each(function () {
|
||||
const chapterName = loadedCheerio(this)
|
||||
.find('a')
|
||||
@@ -165,10 +170,10 @@ const searchNovels = async searchTerm => {
|
||||
let novels = [];
|
||||
|
||||
loadedCheerio('.c-tabs-item__content').each(function () {
|
||||
const novelName = loadedCheerio(this).find('h4 > a').text();
|
||||
const novelName = loadedCheerio(this).find('.post-title a').text();
|
||||
const novelCover = loadedCheerio(this).find('img').attr('src');
|
||||
|
||||
let novelUrl = loadedCheerio(this).find('h4 > a').attr('href');
|
||||
let novelUrl = loadedCheerio(this).find('.post-title a').attr('href');
|
||||
novelUrl = novelUrl.replace(`${baseUrl}vipnovel/`, '');
|
||||
|
||||
const novel = {
|
||||
|
||||
@@ -0,0 +1,131 @@
|
||||
import cheerio from 'react-native-cheerio';
|
||||
|
||||
const sourceId = 88;
|
||||
const sourceName = 'Chireads';
|
||||
|
||||
const baseUrl = 'https://chireads.com/';
|
||||
|
||||
const popularNovels = async page => {
|
||||
const totalPages = 2;
|
||||
const url = `${baseUrl}category/translatedtales/page/${page}/`;
|
||||
|
||||
const result = await fetch(url);
|
||||
const body = await result.text();
|
||||
|
||||
const loadedCheerio = cheerio.load(body);
|
||||
|
||||
let novels = [];
|
||||
|
||||
loadedCheerio('#content li').each(function () {
|
||||
const novelName = loadedCheerio(this).find('.news-list-tit h5').text();
|
||||
const novelCover = loadedCheerio(this).find('img').attr('src');
|
||||
const novelUrl = loadedCheerio(this)
|
||||
.find('.news-list-tit h5 a')
|
||||
.attr('href');
|
||||
|
||||
const novel = {sourceId, novelName, novelCover, novelUrl};
|
||||
|
||||
novels.push(novel);
|
||||
});
|
||||
|
||||
return {totalPages, novels};
|
||||
};
|
||||
|
||||
const parseNovelAndChapters = async novelUrl => {
|
||||
const url = novelUrl;
|
||||
|
||||
const result = await fetch(url);
|
||||
const body = await result.text();
|
||||
|
||||
let loadedCheerio = cheerio.load(body);
|
||||
|
||||
let novel = {
|
||||
sourceId,
|
||||
sourceName,
|
||||
url,
|
||||
novelUrl,
|
||||
novelName: '',
|
||||
novelCover: '',
|
||||
author: '',
|
||||
status: '',
|
||||
genre: '',
|
||||
summary: '',
|
||||
chapters: [],
|
||||
};
|
||||
|
||||
novel.novelName = loadedCheerio('.inform-title').text().trim();
|
||||
|
||||
novel.novelCover = loadedCheerio('.inform-product img').attr('src');
|
||||
|
||||
novel.summary = loadedCheerio('.inform-inform-txt').text().trim();
|
||||
|
||||
let chapters = [];
|
||||
|
||||
loadedCheerio('.chapitre-table a').each(function () {
|
||||
const chapterName = loadedCheerio(this).text().trim();
|
||||
const releaseDate = null;
|
||||
const chapterUrl = loadedCheerio(this).attr('href');
|
||||
|
||||
chapters.push({chapterName, releaseDate, chapterUrl});
|
||||
});
|
||||
|
||||
novel.chapters = chapters;
|
||||
|
||||
return novel;
|
||||
};
|
||||
|
||||
const parseChapter = async (novelUrl, chapterUrl) => {
|
||||
const url = chapterUrl;
|
||||
|
||||
const result = await fetch(url);
|
||||
const body = await result.text();
|
||||
|
||||
let loadedCheerio = cheerio.load(body);
|
||||
|
||||
const chapterName = loadedCheerio('.article-title').text();
|
||||
const chapterText = loadedCheerio('#content').html();
|
||||
|
||||
const chapter = {
|
||||
sourceId,
|
||||
novelUrl,
|
||||
chapterUrl,
|
||||
chapterName,
|
||||
chapterText,
|
||||
};
|
||||
|
||||
return chapter;
|
||||
};
|
||||
|
||||
const searchNovels = async searchTerm => {
|
||||
const url = `${baseUrl}search?x=0&y=0&name=${searchTerm}`;
|
||||
|
||||
const result = await fetch(url);
|
||||
const body = await result.text();
|
||||
|
||||
let loadedCheerio = cheerio.load(body);
|
||||
|
||||
let novels = [];
|
||||
|
||||
loadedCheerio('#content li').each(function () {
|
||||
const novelName = loadedCheerio(this).find('.news-list-tit h5').text();
|
||||
const novelCover = loadedCheerio(this).find('img').attr('src');
|
||||
const novelUrl = loadedCheerio(this)
|
||||
.find('.news-list-tit h5 a')
|
||||
.attr('href');
|
||||
|
||||
const novel = {sourceId, novelName, novelCover, novelUrl};
|
||||
|
||||
novels.push(novel);
|
||||
});
|
||||
|
||||
return novels;
|
||||
};
|
||||
|
||||
const ChireadsScraper = {
|
||||
popularNovels,
|
||||
parseNovelAndChapters,
|
||||
parseChapter,
|
||||
searchNovels,
|
||||
};
|
||||
|
||||
export default ChireadsScraper;
|
||||
@@ -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;
|
||||
@@ -23,6 +23,8 @@ const getPath = extensionId => {
|
||||
63: {novels: 'novel', novel: 'novel', chapter: 'novel'},
|
||||
75: {novels: 'series', novel: 'series', chapter: 'series'},
|
||||
80: {novels: 'manga', novel: 'manga', chapter: 'manga'},
|
||||
91: {novels: 'novel', novel: 'novel', chapter: 'novel'},
|
||||
96: {novels: 'serie', novel: 'serie', chapter: 'serie'},
|
||||
};
|
||||
|
||||
return (
|
||||
@@ -134,6 +136,7 @@ export const LightNovelHeavenScraper = new MadaraScraper(
|
||||
'https://lightnovelheaven.com/',
|
||||
'LightNovelHeaven',
|
||||
getPath(57),
|
||||
true,
|
||||
);
|
||||
|
||||
export const LightNovelsHubScraper = new MadaraScraper(
|
||||
@@ -162,8 +165,8 @@ export const WebNovelLoverScraper = new MadaraScraper(
|
||||
|
||||
export const BoxNovelOnlineScraper = new MadaraScraper(
|
||||
63,
|
||||
'https://boxnovel.online/',
|
||||
'BoxNovel.online',
|
||||
'https://tipnovel.com/',
|
||||
'TipNovel',
|
||||
getPath(63),
|
||||
true,
|
||||
);
|
||||
@@ -211,3 +214,37 @@ export const MoreNovelScraper = new MadaraScraper(
|
||||
getPath(84),
|
||||
true,
|
||||
);
|
||||
|
||||
export const OnlyMTLScraper = new MadaraScraper(
|
||||
91,
|
||||
'https://www.onlymtl.com/',
|
||||
'OnlyMTL',
|
||||
getPath(91),
|
||||
);
|
||||
|
||||
export const HizoMangaScraper = new MadaraScraper(
|
||||
96,
|
||||
'https://hizomanga.com/',
|
||||
'HizoManga',
|
||||
getPath(96),
|
||||
true,
|
||||
56,
|
||||
);
|
||||
|
||||
export const LatestNovelScraper = new MadaraScraper(
|
||||
98,
|
||||
'https://latestnovel.net/',
|
||||
'LatestNovel',
|
||||
getPath(98),
|
||||
false,
|
||||
130,
|
||||
);
|
||||
|
||||
export const ArMTLScraper = new MadaraScraper(
|
||||
101,
|
||||
'https://ar-mtl.club/',
|
||||
'ArMTL',
|
||||
getPath(101),
|
||||
true,
|
||||
1,
|
||||
);
|
||||
|
||||
@@ -94,15 +94,19 @@ class MadaraScraper {
|
||||
|
||||
switch (detailName) {
|
||||
case 'Genre(s)':
|
||||
case 'التصنيفات':
|
||||
novel.genre = detail.replace(/[\t\n]/g, ',');
|
||||
break;
|
||||
case 'Author(s)':
|
||||
case 'المؤلف':
|
||||
novel.author = detail;
|
||||
break;
|
||||
case 'Status':
|
||||
novel.status = detail.includes('OnGoing')
|
||||
? Status.ONGOING
|
||||
: Status.COMPLETED;
|
||||
case 'الحالة':
|
||||
novel.status =
|
||||
detail.includes('OnGoing') || detail.includes('مستمرة')
|
||||
? Status.ONGOING
|
||||
: Status.COMPLETED;
|
||||
break;
|
||||
}
|
||||
});
|
||||
@@ -187,7 +191,8 @@ class MadaraScraper {
|
||||
loadedCheerio('.text-center').text() ||
|
||||
loadedCheerio('#chapter-heading').text();
|
||||
|
||||
let chapterText = loadedCheerio('.text-left').html();
|
||||
let chapterText =
|
||||
loadedCheerio('.text-left').html() || loadedCheerio('.text-right').html();
|
||||
|
||||
const chapter = {
|
||||
sourceId,
|
||||
|
||||
@@ -0,0 +1,21 @@
|
||||
import MultiSrcScraper from './ReadwnScraper';
|
||||
|
||||
const ReadwnScraper = new MultiSrcScraper(
|
||||
68,
|
||||
'https://www.readwn.com/',
|
||||
'Readwn.com',
|
||||
);
|
||||
|
||||
const NovelmtScraper = new MultiSrcScraper(
|
||||
99,
|
||||
'https://www.novelmt.com/',
|
||||
'Novelmt.com',
|
||||
);
|
||||
|
||||
const LtnovelScraper = new MultiSrcScraper(
|
||||
100,
|
||||
'https://www.ltnovel.com/',
|
||||
'Ltnovel.com',
|
||||
);
|
||||
|
||||
export {ReadwnScraper, NovelmtScraper, LtnovelScraper};
|
||||
@@ -0,0 +1,185 @@
|
||||
import cheerio from 'react-native-cheerio';
|
||||
import QueryString from 'qs';
|
||||
|
||||
class ReadwnScraper {
|
||||
constructor(sourceId, baseUrl, sourceName) {
|
||||
this.sourceId = sourceId;
|
||||
this.baseUrl = baseUrl;
|
||||
this.sourceName = sourceName;
|
||||
}
|
||||
|
||||
async popularNovels(page) {
|
||||
const baseUrl = this.baseUrl;
|
||||
const sourceId = this.sourceId;
|
||||
|
||||
const pageNo = page - 1;
|
||||
|
||||
const url = `${baseUrl}list/all/all-onclick-${pageNo}.html`;
|
||||
|
||||
const result = await fetch(url);
|
||||
const body = await result.text();
|
||||
|
||||
const loadedCheerio = cheerio.load(body);
|
||||
|
||||
let novels = [];
|
||||
|
||||
loadedCheerio('li.novel-item').each(function () {
|
||||
const novelName = loadedCheerio(this).find('h4').text();
|
||||
const novelUrl = baseUrl + loadedCheerio(this).find('a').attr('href');
|
||||
|
||||
const coverUri = loadedCheerio(this)
|
||||
.find('.novel-cover > img')
|
||||
.attr('data-src');
|
||||
|
||||
const novelCover = baseUrl + coverUri;
|
||||
|
||||
const novel = {sourceId, novelName, novelCover, novelUrl};
|
||||
|
||||
novels.push(novel);
|
||||
});
|
||||
|
||||
return {totalPages: this.totalPages, novels};
|
||||
}
|
||||
|
||||
async parseNovelAndChapters(novelUrl) {
|
||||
const sourceId = this.sourceId;
|
||||
const baseUrl = this.baseUrl;
|
||||
const sourceName = this.sourceName;
|
||||
|
||||
const url = novelUrl;
|
||||
|
||||
const result = await fetch(url);
|
||||
const body = await result.text();
|
||||
|
||||
let loadedCheerio = cheerio.load(body);
|
||||
|
||||
let novel = {
|
||||
sourceId: sourceId,
|
||||
sourceName: sourceName,
|
||||
url,
|
||||
novelUrl,
|
||||
};
|
||||
|
||||
novel.novelName = loadedCheerio('h1.novel-title').text();
|
||||
|
||||
const coverUri = loadedCheerio('figure.cover > img').attr('data-src');
|
||||
novel.novelCover = baseUrl + coverUri;
|
||||
|
||||
novel.summary = loadedCheerio('.summary')
|
||||
.text()
|
||||
.replace('Summary', '')
|
||||
.trim();
|
||||
|
||||
novel.genre = '';
|
||||
|
||||
loadedCheerio('div.categories > ul > li').each(function () {
|
||||
novel.genre += loadedCheerio(this).text().trim() + ',';
|
||||
});
|
||||
|
||||
loadedCheerio('div.header-stats > span').each(function () {
|
||||
if (loadedCheerio(this).find('small').text() === 'Status') {
|
||||
novel.status = loadedCheerio(this).find('strong').text();
|
||||
}
|
||||
});
|
||||
|
||||
novel.genre = novel.genre.slice(0, -1);
|
||||
|
||||
novel.author = loadedCheerio('span[itemprop=author]').text();
|
||||
|
||||
let novelChapters = [];
|
||||
|
||||
const novelId = novelUrl.replace('.html', '');
|
||||
|
||||
const latestChapterNo = loadedCheerio('.header-stats')
|
||||
.find('span > strong')
|
||||
.first()
|
||||
.text()
|
||||
.trim();
|
||||
|
||||
for (let i = 1; i <= latestChapterNo; i++) {
|
||||
const chapterName = `Chapter ${i}`;
|
||||
const chapterUrl = `${novelId}_${i}.html`;
|
||||
const releaseDate = null;
|
||||
|
||||
const chapter = {chapterName, releaseDate, chapterUrl};
|
||||
|
||||
novelChapters.push(chapter);
|
||||
}
|
||||
|
||||
novel.chapters = novelChapters;
|
||||
|
||||
return novel;
|
||||
}
|
||||
|
||||
async parseChapter(novelUrl, chapterUrl) {
|
||||
const url = chapterUrl;
|
||||
const sourceId = this.sourceId;
|
||||
|
||||
const result = await fetch(url);
|
||||
const body = await result.text();
|
||||
|
||||
const loadedCheerio = cheerio.load(body);
|
||||
|
||||
const chapterName = loadedCheerio('.titles > h2').text();
|
||||
const chapterText = loadedCheerio('.chapter-content').html();
|
||||
|
||||
const chapter = {
|
||||
sourceId,
|
||||
novelUrl,
|
||||
chapterUrl,
|
||||
chapterName,
|
||||
chapterText,
|
||||
};
|
||||
|
||||
return chapter;
|
||||
}
|
||||
|
||||
async searchNovels(searchTerm) {
|
||||
const baseUrl = this.baseUrl;
|
||||
const sourceId = this.sourceId;
|
||||
const searchUrl = `${baseUrl}e/search/index.php`;
|
||||
|
||||
const result = await fetch(searchUrl, {
|
||||
headers: {
|
||||
'Content-Type': 'application/x-www-form-urlencoded',
|
||||
Referer: `${baseUrl}search.html`,
|
||||
Origin: baseUrl,
|
||||
'user-agent':
|
||||
'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/92.0.4515.131 Safari/537.36',
|
||||
},
|
||||
method: 'POST',
|
||||
body: QueryString.stringify({
|
||||
show: 'title',
|
||||
tempid: 1,
|
||||
tbname: 'news',
|
||||
keyboard: searchTerm,
|
||||
}),
|
||||
});
|
||||
const body = await result.text();
|
||||
|
||||
const loadedCheerio = cheerio.load(body);
|
||||
|
||||
let novels = [];
|
||||
|
||||
loadedCheerio('li.novel-item').each(function () {
|
||||
const novelName = loadedCheerio(this).find('h4').text();
|
||||
const novelUrl = baseUrl + loadedCheerio(this).find('a').attr('href');
|
||||
|
||||
const coverUri = loadedCheerio(this).find('img').attr('data-src');
|
||||
const novelCover = baseUrl + coverUri;
|
||||
|
||||
const novel = {
|
||||
sourceId,
|
||||
novelName,
|
||||
novelCover,
|
||||
novelUrl,
|
||||
};
|
||||
|
||||
novels.push(novel);
|
||||
});
|
||||
|
||||
return novels;
|
||||
}
|
||||
}
|
||||
|
||||
export default ReadwnScraper;
|
||||
@@ -5,9 +5,3 @@ export const KolNovelScraper = new WPMangaStreamScraper(
|
||||
'https://alightnovel.com/',
|
||||
'KolNovel',
|
||||
);
|
||||
|
||||
export const RewayatArScraper = new WPMangaStreamScraper(
|
||||
54,
|
||||
'https://rewayat-ar.site/',
|
||||
'RewayatAr',
|
||||
);
|
||||
|
||||
+30
-25
@@ -90,24 +90,27 @@ const parseChapter = async (novelUrl, chapterUrl) => {
|
||||
let chapterName = loadedCheerio(
|
||||
'body > div.pusher.container_main > div:nth-child(5) > div:nth-child(1) > div.title-wrapper > h1',
|
||||
).text();
|
||||
|
||||
loadedCheerio(
|
||||
'body > div.pusher.container_main > div:nth-child(5) > div:nth-child(1) img',
|
||||
).each(function () {
|
||||
if (!loadedCheerio(this).attr('src')?.startsWith('http')) {
|
||||
const dataMediaId = loadedCheerio(this).attr('data-media-id');
|
||||
const newSrc = `https://ranobehub.org/api/media/${dataMediaId}`;
|
||||
|
||||
loadedCheerio(this).attr('src', newSrc);
|
||||
}
|
||||
});
|
||||
|
||||
let chapterText = loadedCheerio(
|
||||
'body > div.pusher.container_main > div:nth-child(5) > div:nth-child(1)',
|
||||
).html();
|
||||
|
||||
if (chapterText) {
|
||||
/**
|
||||
* Convert relative url to absolute
|
||||
*/
|
||||
|
||||
chapterText = chapterText
|
||||
.replace(/<\s*script[^>]*>[\s\S]*?<\/script>/gim, '')
|
||||
.replace(
|
||||
/data-src="(.*)"/g,
|
||||
`src="https://ranobehub.org/img/ranobe/content/${chapterId}/$1.jpg"`,
|
||||
);
|
||||
}
|
||||
|
||||
// console.log(chapterText);
|
||||
// Remove script tags
|
||||
chapterText = chapterText?.replace(
|
||||
/<\s*script[^>]*>[\s\S]*?<\/script>/gim,
|
||||
'',
|
||||
);
|
||||
|
||||
const chapter = {
|
||||
sourceId,
|
||||
@@ -121,23 +124,25 @@ const parseChapter = async (novelUrl, chapterUrl) => {
|
||||
};
|
||||
|
||||
const searchNovels = async searchTerm => {
|
||||
const url = 'https://ranobehub.org/api/fulltext/ranobe?query=' + searchTerm;
|
||||
const url = `https://ranobehub.org/api/fulltext/global?query=${searchTerm}&take=5`;
|
||||
|
||||
const result = await fetch(url);
|
||||
const data = await result.json();
|
||||
|
||||
let novels = [];
|
||||
|
||||
data.data.map(novel =>
|
||||
novels.push({
|
||||
sourceId,
|
||||
novelName: novel.names.rus,
|
||||
novelUrl: novel.url.match(
|
||||
/https:\/\/ranobehub\.org\/ranobe\/(.*?)\?utm_source=search_name&utm_medium=search&utm_campaign=search_using/,
|
||||
)[1],
|
||||
novelCover: novel.image,
|
||||
}),
|
||||
);
|
||||
data
|
||||
.find(item => item.meta.key === 'ranobe')
|
||||
?.data.map(novel =>
|
||||
novels.push({
|
||||
sourceId,
|
||||
novelName: novel.names.rus,
|
||||
novelUrl: novel.url.match(
|
||||
/https:\/\/ranobehub\.org\/ranobe\/(.*?)\?utm_source=search_name&utm_medium=search&utm_campaign=search_using/,
|
||||
)[1],
|
||||
novelCover: novel.image,
|
||||
}),
|
||||
);
|
||||
|
||||
return novels;
|
||||
};
|
||||
|
||||
@@ -0,0 +1,160 @@
|
||||
import cheerio from 'react-native-cheerio';
|
||||
|
||||
const sourceId = 93;
|
||||
const sourceName = 'RanobeLib';
|
||||
|
||||
const baseUrl = 'https://ranobelib.me/';
|
||||
|
||||
const popularNovels = async page => {
|
||||
const totalPages = 29;
|
||||
const url = `${baseUrl}manga-list?sort=rate&dir=desc&page=${page}`;
|
||||
|
||||
const result = await fetch(url);
|
||||
const body = await result.text();
|
||||
|
||||
const loadedCheerio = cheerio.load(body);
|
||||
|
||||
let novels = [];
|
||||
|
||||
loadedCheerio('.media-card-wrap').each(function () {
|
||||
const novelName = loadedCheerio(this).find('.media-card__title').text();
|
||||
const novelCover = loadedCheerio(this)
|
||||
.find('a.media-card')
|
||||
.attr('data-src');
|
||||
const novelUrl = loadedCheerio(this).find('a.media-card').attr('href');
|
||||
|
||||
const novel = {sourceId, novelName, novelCover, novelUrl};
|
||||
|
||||
novels.push(novel);
|
||||
});
|
||||
|
||||
return {totalPages, novels};
|
||||
};
|
||||
|
||||
const parseNovelAndChapters = async novelUrl => {
|
||||
const result = await fetch(novelUrl);
|
||||
const body = await result.text();
|
||||
|
||||
const loadedCheerio = cheerio.load(body);
|
||||
|
||||
let novel = {
|
||||
sourceId,
|
||||
sourceName,
|
||||
url: novelUrl,
|
||||
novelUrl,
|
||||
novelName: '',
|
||||
novelCover: '',
|
||||
author: null,
|
||||
status: null,
|
||||
genre: null,
|
||||
summary: null,
|
||||
chapters: [],
|
||||
};
|
||||
|
||||
novel.novelName = loadedCheerio('.media-name__main').text().trim();
|
||||
|
||||
novel.novelCover = loadedCheerio('.media-sidebar__cover img').attr('src');
|
||||
|
||||
novel.summary = loadedCheerio('.media-description__text').text().trim();
|
||||
|
||||
novel.author = loadedCheerio(
|
||||
'#main-page > div > div.container.container_responsive > div > div.media-sidebar > div.media-info-list.paper > a:nth-child(4) > div.media-info-list__value',
|
||||
)
|
||||
.text()
|
||||
.trim();
|
||||
|
||||
let chapters = [];
|
||||
|
||||
let chaptersJson = body.match(
|
||||
/window.__DATA__ = [\s\S]*?window._SITE_COLOR_/gm,
|
||||
);
|
||||
|
||||
chaptersJson = chaptersJson?.[0]
|
||||
?.replace('window.__DATA__ = ', '')
|
||||
?.replace('window._SITE_COLOR_', '')
|
||||
?.trim()
|
||||
?.slice(0, -1);
|
||||
|
||||
chaptersJson = JSON.parse(chaptersJson);
|
||||
|
||||
const novelSlug = chaptersJson.manga.slug;
|
||||
|
||||
chaptersJson.chapters.list.map(item => {
|
||||
const chapterName =
|
||||
`Том ${item.chapter_volume} Глава ${item.chapter_number} ${item.chapter_name}`?.trim();
|
||||
const releaseDate = item.chapter_created_at;
|
||||
const chapterUrl = `${baseUrl}${novelSlug}/v${item.chapter_volume}/c${item.chapter_number}`;
|
||||
|
||||
chapters.push({chapterName, releaseDate, chapterUrl});
|
||||
});
|
||||
|
||||
novel.chapters = chapters.reverse();
|
||||
|
||||
return novel;
|
||||
};
|
||||
|
||||
const parseChapter = async (novelUrl, chapterUrl) => {
|
||||
const result = await fetch(chapterUrl);
|
||||
const body = await result.text();
|
||||
|
||||
const loadedCheerio = cheerio.load(body);
|
||||
|
||||
loadedCheerio('.reader-container img').each(function () {
|
||||
if (!loadedCheerio(this).attr('src')?.startsWith('http')) {
|
||||
const src = loadedCheerio(this).attr('src');
|
||||
|
||||
loadedCheerio(this).attr('src', `http://ranobelib.me${src}`);
|
||||
}
|
||||
});
|
||||
|
||||
const chapterName = loadedCheerio(
|
||||
'body > div.reader.reader_text > div.header.header_reader.headroom.headroom--top.headroom--not-bottom > div > a > div:nth-child(3)',
|
||||
)
|
||||
.text()
|
||||
.trim();
|
||||
const chapterText = loadedCheerio('.reader-container').html();
|
||||
|
||||
const chapter = {
|
||||
sourceId,
|
||||
novelUrl,
|
||||
chapterUrl,
|
||||
chapterName,
|
||||
chapterText,
|
||||
};
|
||||
|
||||
return chapter;
|
||||
};
|
||||
|
||||
const searchNovels = async searchTerm => {
|
||||
const url = `${baseUrl}manga-list?sort=rate&dir=desc&page=1&name=${searchTerm}`;
|
||||
|
||||
const result = await fetch(url);
|
||||
const body = await result.text();
|
||||
|
||||
let loadedCheerio = cheerio.load(body);
|
||||
|
||||
let novels = [];
|
||||
|
||||
loadedCheerio('.media-card-wrap').each(function () {
|
||||
const novelName = loadedCheerio(this).find('.media-card__title').text();
|
||||
const novelCover = loadedCheerio(this)
|
||||
.find('a.media-card')
|
||||
.attr('data-src');
|
||||
const novelUrl = loadedCheerio(this).find('a.media-card').attr('href');
|
||||
|
||||
const novel = {sourceId, novelName, novelCover, novelUrl};
|
||||
|
||||
novels.push(novel);
|
||||
});
|
||||
|
||||
return novels;
|
||||
};
|
||||
|
||||
const RanobeLibScraper = {
|
||||
popularNovels,
|
||||
parseNovelAndChapters,
|
||||
parseChapter,
|
||||
searchNovels,
|
||||
};
|
||||
|
||||
export default RanobeLibScraper;
|
||||
+35
-5
@@ -33,12 +33,15 @@ import ScribbleHubScraper from './en/scribblehub';
|
||||
import SyosetuScraper from './jp/syosetu';
|
||||
import LNMTLScraper from './en/lnmtl';
|
||||
import {
|
||||
ArMTLScraper,
|
||||
BoxNovelOnlineScraper,
|
||||
BoxNovelScraper,
|
||||
ClickNovelScraper,
|
||||
DaoNovelScraper,
|
||||
FirstKissNovelScraper,
|
||||
FreeNovelScraper,
|
||||
HizoMangaScraper,
|
||||
LatestNovelScraper,
|
||||
LightNovelHeavenScraper,
|
||||
LightNovelsHubScraper,
|
||||
LunarLettersScraper,
|
||||
@@ -50,6 +53,7 @@ import {
|
||||
NovelMultiverseScraper,
|
||||
NovelsRockScraper,
|
||||
NovelTranslateScraper,
|
||||
OnlyMTLScraper,
|
||||
ReadWebNovelsScraper,
|
||||
SkyNovelScraper,
|
||||
SleepyTranslationsScraper,
|
||||
@@ -63,13 +67,9 @@ import WuxiaCityScraper from './en/wuxiacity';
|
||||
import NovelUpdatesScraper from './en/novelupdates';
|
||||
import RanobesScraper from './en/ranobes';
|
||||
import YushuboScraper from './ch/yushubo';
|
||||
import {
|
||||
KolNovelScraper,
|
||||
RewayatArScraper,
|
||||
} from './multisrc/wpmangastream/WPMangaStreamGenerator';
|
||||
import {KolNovelScraper} from './multisrc/wpmangastream/WPMangaStreamGenerator';
|
||||
import WLNUpdatesScraper from './en/wlnupdates';
|
||||
import ReaperScansScraper from './en/reaperscans';
|
||||
import ReadwnScraper from './en/readwn';
|
||||
import RanobeHubScraper from './ru/ranobehub';
|
||||
import ArNovelScraper from './ar/arnovel';
|
||||
import DivineDaoLibraryScraper from './en/divinedaolibrary';
|
||||
@@ -88,6 +88,21 @@ import MyLoveNovelScraper from './en/mylovenovel';
|
||||
import RiwayatScraper from './ar/riwayat';
|
||||
import NovelRinganScraper from './id/novelringan';
|
||||
import IndoWebNovelScraper from './id/indowebnovel';
|
||||
import ChireadsScraper from './fr/chireads';
|
||||
import RewayatArScraper from './ar/rewayatar';
|
||||
import MTLCornerScraper from './en/mtlcorner';
|
||||
import NitroScansScraper from './en/nitroscans';
|
||||
import NovelForestScraper from './en/novelforest';
|
||||
import RanobeLibScraper from './ru/ranobelib';
|
||||
import NovelPubScraper from './en/novelpub';
|
||||
import BestLightNovel from './en/bestlightnovel';
|
||||
import NovelFullMeScraper from './en/NovelFullMe';
|
||||
import {
|
||||
LtnovelScraper,
|
||||
NovelmtScraper,
|
||||
ReadwnScraper,
|
||||
} from './multisrc/readwn/ReadwnGenerator';
|
||||
import SakuraNovelScraper from './id/sakuranovel';
|
||||
|
||||
export const getSource = sourceId => {
|
||||
const scrapers = {
|
||||
@@ -177,6 +192,21 @@ export const getSource = sourceId => {
|
||||
85: RiwayatScraper,
|
||||
86: NovelRinganScraper,
|
||||
87: IndoWebNovelScraper,
|
||||
88: ChireadsScraper,
|
||||
89: MTLCornerScraper,
|
||||
90: NitroScansScraper,
|
||||
91: OnlyMTLScraper,
|
||||
92: NovelForestScraper,
|
||||
93: RanobeLibScraper,
|
||||
94: NovelPubScraper,
|
||||
95: BestLightNovel,
|
||||
96: HizoMangaScraper,
|
||||
97: NovelFullMeScraper,
|
||||
98: LatestNovelScraper,
|
||||
99: NovelmtScraper,
|
||||
100: LtnovelScraper,
|
||||
101: ArMTLScraper,
|
||||
103: SakuraNovelScraper,
|
||||
};
|
||||
|
||||
return scrapers[sourceId];
|
||||
|
||||
+106
-1
@@ -465,7 +465,7 @@
|
||||
"sourceId": 68,
|
||||
"url": "https://www.readwn.com/",
|
||||
"sourceName": "Readwn.com",
|
||||
"icon": "https://github.com/LNReader/lnreader-sources/blob/main/src/en/readwn/icon.png?raw=true",
|
||||
"icon": "https://github.com/LNReader/lnreader-sources/blob/main/multisrc/readwn/icons/readwn.png?raw=true",
|
||||
"lang": "English"
|
||||
},
|
||||
{
|
||||
@@ -600,5 +600,110 @@
|
||||
"sourceName": "IndoWebNovel",
|
||||
"icon": "https://github.com/LNReader/lnreader-sources/blob/main/src/id/indowebnovel/icon.png?raw=true",
|
||||
"lang": "Indonesian"
|
||||
},
|
||||
{
|
||||
"sourceId": 88,
|
||||
"url": "https://chireads.com/",
|
||||
"sourceName": "Chireads",
|
||||
"icon": "https://github.com/LNReader/lnreader-sources/blob/main/src/fr/chireads/icon.png?raw=true",
|
||||
"lang": "French"
|
||||
},
|
||||
{
|
||||
"sourceId": 89,
|
||||
"url": "https://mtlcorner.com/",
|
||||
"sourceName": "MTLCorner",
|
||||
"icon": "https://github.com/LNReader/lnreader-sources/blob/main/src/en/mtlcorner/icon.png?raw=true",
|
||||
"lang": "English"
|
||||
},
|
||||
{
|
||||
"sourceId": 90,
|
||||
"url": "https://nitroscans.com/",
|
||||
"sourceName": "NitroScans",
|
||||
"icon": "https://github.com/LNReader/lnreader-sources/blob/main/src/en/nitroscans/icon.png?raw=true",
|
||||
"lang": "English"
|
||||
},
|
||||
{
|
||||
"sourceId": 91,
|
||||
"url": "https://www.onlymtl.com/",
|
||||
"sourceName": "OnlyMTL",
|
||||
"icon": "https://github.com/LNReader/lnreader-sources/blob/main/multisrc/madara/icons/onlymtl.png?raw=true",
|
||||
"lang": "English"
|
||||
},
|
||||
{
|
||||
"sourceId": 92,
|
||||
"url": "https://novelforest.com/",
|
||||
"sourceName": "NovelForest",
|
||||
"icon": "https://github.com/LNReader/lnreader-sources/blob/main/src/en/novelforest/icon.png?raw=true",
|
||||
"lang": "English"
|
||||
},
|
||||
{
|
||||
"sourceId": 93,
|
||||
"url": "https://ranobelib.me/",
|
||||
"sourceName": "RanobeLib",
|
||||
"icon": "https://github.com/LNReader/lnreader-sources/blob/main/src/ru/ranobelib/icon.png?raw=true",
|
||||
"lang": "Russian"
|
||||
},
|
||||
{
|
||||
"sourceId": 94,
|
||||
"url": "https://www.novelpub.com/",
|
||||
"sourceName": "NovelPub",
|
||||
"icon": "https://github.com/LNReader/lnreader-sources/blob/main/src/en/novelpub/icon.png?raw=true",
|
||||
"lang": "English"
|
||||
},
|
||||
{
|
||||
"sourceId": 95,
|
||||
"url": "https://www.bestlightnovel.com/",
|
||||
"sourceName": "BestLightNovel",
|
||||
"icon": "https://github.com/LNReader/lnreader-sources/blob/main/src/en/bestlightnovel/icon.png?raw=true",
|
||||
"lang": "English"
|
||||
},
|
||||
{
|
||||
"sourceId": 96,
|
||||
"url": "https://hizomanga.com/",
|
||||
"sourceName": "HizoManga",
|
||||
"icon": "https://github.com/LNReader/lnreader-sources/blob/main/multisrc/madara/icons/hizomanga.png?raw=true",
|
||||
"lang": "Arabic"
|
||||
},
|
||||
{
|
||||
"sourceId": 97,
|
||||
"url": "https://novelfull.me/",
|
||||
"sourceName": "NovelFull.me",
|
||||
"icon": "https://github.com/LNReader/lnreader-sources/blob/main/src/en/novelfullme/icon.png?raw=true",
|
||||
"lang": "English"
|
||||
},
|
||||
{
|
||||
"sourceId": 98,
|
||||
"url": "https://latestnovel.net/",
|
||||
"sourceName": "LatestNovel",
|
||||
"icon": "https://github.com/LNReader/lnreader-sources/blob/main/multisrc/madara/icons/latestnovel.png?raw=true",
|
||||
"lang": "English"
|
||||
},
|
||||
{
|
||||
"sourceId": 99,
|
||||
"url": "https://www.novelmt.com/",
|
||||
"sourceName": "Novelmt.com",
|
||||
"icon": "https://github.com/LNReader/lnreader-sources/blob/main/multisrc/readwn/icons/novelmt.png?raw=true",
|
||||
"lang": "English"
|
||||
},
|
||||
{
|
||||
"sourceId": 100,
|
||||
"url": "https://www.ltnovel.com/",
|
||||
"sourceName": "Ltnovel.com",
|
||||
"icon": "https://github.com/LNReader/lnreader-sources/blob/main/multisrc/readwn/icons/ltnovel.png?raw=true",
|
||||
"lang": "English"
|
||||
},
|
||||
{
|
||||
"sourceId": 101,
|
||||
"url": "https://ar-mtl.club/",
|
||||
"sourceName": "ArMTL",
|
||||
"icon": "https://github.com/LNReader/lnreader-sources/blob/main/multisrc/madara/icons/armtl.png?raw=true",
|
||||
"lang": "Arabic"
|
||||
},
|
||||
{
|
||||
"sourceId": 103,
|
||||
"url": "https://sakuranovel.id/",
|
||||
"sourceName": "SakuraNovel",
|
||||
"icon": "https://github.com/LNReader/lnreader-sources/blob/main/src/id/sakuranovel/icon.png?raw=true",
|
||||
"lang": "Indonesian"
|
||||
}
|
||||
]
|
||||
|
||||
@@ -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;
|
||||
Reference in New Issue
Block a user