This commit is contained in:
Rajarshee Chatterjee
2021-07-16 15:22:36 +05:30
parent 5aee0d1b04
commit d07fd26cb3
32 changed files with 917 additions and 445 deletions
+1
View File
@@ -10,6 +10,7 @@ const ColorPickerModal = ({
onSubmit,
hideModal,
modalVisible,
showAccentColors,
}) => {
const [text, setText] = useState(color);
const [error, setError] = useState();
+166 -126
View File
@@ -1,4 +1,4 @@
import React from "react";
import React, { useCallback } from "react";
import {
StyleSheet,
View,
@@ -6,19 +6,17 @@ import {
useWindowDimensions,
Pressable,
} from "react-native";
import { LinearGradient } from "expo-linear-gradient";
import { TouchableRipple } from "react-native-paper";
import { useSelector } from "react-redux";
import { useSettings, useTheme } from "../hooks/reduxHooks";
import ListView from "./ListView";
import { LinearGradient } from "expo-linear-gradient";
import FastImage from "react-native-fast-image";
import ListView from "./ListView";
import { getDeviceOrientation } from "../services/utils/helpers";
const NovelCover = ({ item, onPress, libraryStatus }) => {
const theme = useTheme();
const NovelCover = ({ item, onPress, libraryStatus, theme, settings }) => {
const { displayMode, novelsPerRow, showDownloadBadges, showUnreadBadges } =
useSettings();
settings;
const window = useWindowDimensions();
@@ -27,98 +25,10 @@ const NovelCover = ({ item, onPress, libraryStatus }) => {
const getNovelsPerRow = () =>
orientation === "landscape" ? 6 : novelsPerRow;
const getHeight = () => (window.width / getNovelsPerRow()) * (4 / 3);
const comfortableTitle = () =>
displayMode === 1 && (
<Text
numberOfLines={2}
style={[
styles.title,
{
color: theme.textColorPrimary,
padding: 4,
},
]}
>
{item.novelName}
</Text>
);
const compactTitle = () =>
displayMode === 0 && (
<View style={styles.titleContainer}>
<LinearGradient
colors={["transparent", "rgba(0,0,0,0.7)"]}
style={styles.linearGradient}
>
<Text
numberOfLines={2}
style={[styles.title, { color: "rgba(255,255,255,1)" }]}
>
{item.novelName}
</Text>
</LinearGradient>
</View>
);
const unreadBadge = () =>
showUnreadBadges &&
item.chaptersUnread && (
<Text
style={[
styles.unreadBadge,
!item.chaptersDownloaded && {
borderTopLeftRadius: 4,
borderBottomLeftRadius: 4,
},
!showDownloadBadges && {
borderRadius: 4,
},
{
backgroundColor: theme.colorAccent,
color: theme.colorButtonText,
},
]}
>
{item.chaptersUnread}
</Text>
);
const downloadBadge = () =>
showDownloadBadges &&
item.chaptersDownloaded && (
<Text
style={[
styles.downloadBadge,
!item.chaptersUnread && {
borderTopRightRadius: 4,
borderBottomRightRadius: 4,
},
!showUnreadBadges && {
borderRadius: 4,
},
]}
>
{item.chaptersDownloaded}
</Text>
);
const inLibraryBadge = () =>
libraryStatus && (
<Text
style={[
styles.inLibraryBadge,
{
backgroundColor: theme.colorAccent,
color: theme.colorButtonText,
borderRadius: 4,
},
]}
>
In library
</Text>
);
const getHeight = useCallback(
() => (window.width / getNovelsPerRow()) * (4 / 3),
[]
);
return displayMode !== 2 ? (
<View
@@ -134,18 +44,23 @@ const NovelCover = ({ item, onPress, libraryStatus }) => {
onPress={onPress}
>
<>
<View
style={{
position: "absolute",
zIndex: 1,
top: 8,
left: 8,
flexDirection: "row",
}}
>
{inLibraryBadge()}
{downloadBadge()}
{unreadBadge()}
<View style={styles.badgeContainer}>
{libraryStatus && <InLibraryBadge theme={theme} />}
{showDownloadBadges && item.chaptersDownloaded && (
<DownloadBadge
showUnreadBadges={showUnreadBadges}
chaptersDownloaded={item.chaptersDownloaded}
chaptersUnread={item.chaptersUnread}
/>
)}
{showUnreadBadges && item.chaptersUnread && (
<UnreadBadge
theme={theme}
chaptersDownloaded={item.chaptersDownloaded}
chaptersUnread={item.chaptersUnread}
showDownloadBadges={showDownloadBadges}
/>
)}
</View>
<FastImage
source={{ uri: item.novelCover }}
@@ -154,26 +69,45 @@ const NovelCover = ({ item, onPress, libraryStatus }) => {
libraryStatus && { opacity: 0.5 },
]}
/>
<View
style={{
position: "absolute",
bottom: 4,
left: 4,
right: 4,
}}
>
{compactTitle()}
<View style={styles.compactTitleContainer}>
{displayMode === 0 && (
<CompactTitle novelName={item.novelName} />
)}
</View>
{comfortableTitle()}
{displayMode === 1 && (
<ComfortableTitle
novelName={item.novelName}
theme={theme}
/>
)}
</>
</Pressable>
</View>
) : (
<ListView
item={item}
downloadBadge={downloadBadge()}
unreadBadge={unreadBadge()}
inLibraryBadge={inLibraryBadge()}
downloadBadge={
showDownloadBadges &&
item.chaptersDownloaded && (
<DownloadBadge
showUnreadBadges={showUnreadBadges}
chaptersDownloaded={item.chaptersDownloaded}
chaptersUnread={item.chaptersUnread}
/>
)
}
unreadBadge={
showUnreadBadges &&
item.chaptersUnread && (
<UnreadBadge
theme={theme}
chaptersDownloaded={item.chaptersDownloaded}
chaptersUnread={item.chaptersUnread}
showDownloadBadges={showDownloadBadges}
/>
)
}
inLibraryBadge={libraryStatus && <InLibraryBadge theme={theme} />}
theme={theme}
onPress={onPress}
/>
@@ -182,6 +116,99 @@ const NovelCover = ({ item, onPress, libraryStatus }) => {
export default NovelCover;
const ComfortableTitle = ({ theme, novelName }) => (
<Text
numberOfLines={2}
style={[
styles.title,
{
color: theme.textColorPrimary,
padding: 4,
},
]}
>
{novelName}
</Text>
);
const CompactTitle = ({ novelName }) => (
<View style={styles.titleContainer}>
<LinearGradient
colors={["transparent", "rgba(0,0,0,0.7)"]}
style={styles.linearGradient}
>
<Text
numberOfLines={2}
style={[styles.title, { color: "rgba(255,255,255,1)" }]}
>
{novelName}
</Text>
</LinearGradient>
</View>
);
const InLibraryBadge = ({ theme }) => (
<Text
style={[
styles.inLibraryBadge,
{
backgroundColor: theme.colorAccent,
color: theme.colorButtonText,
borderRadius: 4,
},
]}
>
In library
</Text>
);
const UnreadBadge = ({
chaptersDownloaded,
chaptersUnread,
showDownloadBadges,
theme,
}) => (
<Text
style={[
styles.unreadBadge,
!chaptersDownloaded && {
borderTopLeftRadius: 4,
borderBottomLeftRadius: 4,
},
!showDownloadBadges && {
borderRadius: 4,
},
{
backgroundColor: theme.colorAccent,
color: theme.colorButtonText,
},
]}
>
{chaptersUnread}
</Text>
);
const DownloadBadge = ({
chaptersDownloaded,
showUnreadBadges,
chaptersUnread,
}) => (
<Text
style={[
styles.downloadBadge,
!chaptersUnread && {
borderTopRightRadius: 4,
borderBottomRightRadius: 4,
},
!showUnreadBadges && {
borderRadius: 4,
},
]}
>
{chaptersDownloaded}
</Text>
);
const styles = StyleSheet.create({
titleContainer: {
flex: 1,
@@ -238,4 +265,17 @@ const styles = StyleSheet.create({
paddingHorizontal: 4,
fontSize: 12,
},
compactTitleContainer: {
position: "absolute",
bottom: 4,
left: 4,
right: 4,
},
badgeContainer: {
position: "absolute",
zIndex: 1,
top: 8,
left: 8,
flexDirection: "row",
},
});
+33 -14
View File
@@ -17,15 +17,15 @@ export const insertChapters = async (novelId, chapters) => {
novelId,
],
(txObj, res) => {},
(txObj, error) =>
console.log(
"Error ",
error,
" Chapter URL: ",
chapter.chapterUrl,
" Novel ID ",
novelId
)
(txObj, error) => {}
// console.log(
// "Error ",
// error,
// " Chapter URL: ",
// chapter.chapterUrl,
// " Novel ID ",
// novelId
// )
),
(txObj, res) => {
console.log("Success");
@@ -35,13 +35,10 @@ export const insertChapters = async (novelId, chapters) => {
});
};
const getChaptersQuery = (sort, filter) =>
`SELECT * FROM chapters WHERE novelId = ? ${filter || ""} ${
sort || "ORDER BY chapterId ASC"
}`;
const getChaptersQuery = (sort = "ORDER BY chapterId ASC", filter = "") =>
`SELECT * FROM chapters WHERE novelId = ? ${filter} ${sort}`;
export const getChapters = (novelId, sort, filter) => {
// console.log(getChaptersQuery(sort, filter));
return new Promise((resolve, reject) =>
db.transaction((tx) => {
tx.executeSql(
@@ -262,3 +259,25 @@ export const markPreviousChaptersUnread = async (chapterId, novelId) => {
);
});
};
const getDownloadedChaptersQuery = `
SELECT chapters.*, novels.sourceId, novels.novelName, novels.novelCover
FROM chapters
JOIN novels
ON chapters.novelId = novels.novelId
WHERE chapters.downloaded = 1`;
export const getDownloadedChapters = () => {
return new Promise((resolve, reject) =>
db.transaction((tx) => {
tx.executeSql(
getDownloadedChaptersQuery,
null,
(txObj, { rows: { _array } }) => {
resolve(_array);
},
(txObj, error) => console.log("Error ", error)
);
})
);
};
+2
View File
@@ -11,6 +11,7 @@ import BackupSettings from "../screens/More/BackupSettings";
import AdvancedSettings from "../screens/More/AdvancedSettings";
import GeneralSettings from "../screens/More/GeneralSettings";
import DownloadQueue from "../screens/More/DownloadQueue";
import Downloads from "../screens/More/Downloads";
const Stack = createStackNavigator();
@@ -32,6 +33,7 @@ const MoreStack = () => (
<Stack.Screen name="SettingsStack" component={SettingsStack} />
<Stack.Screen name="About" component={About} />
<Stack.Screen name="DownloadQueue" component={DownloadQueue} />
<Stack.Screen name="Downloads" component={Downloads} />
</Stack.Navigator>
);
+1 -1
View File
@@ -17,7 +17,6 @@ import {
strawberryDaiquiri,
palenightTheme,
} from "../../theme/theme";
import { setRippleColor } from "./settings.actions";
const themes = {
0: amoledDarkTheme,
@@ -69,6 +68,7 @@ const initialState = {
searchAllSources: false,
textSelectable: false,
updateLibraryOnLaunch: false,
downloadNewChapters: false,
};
const settingsReducer = (state = initialState, action) => {
+6 -2
View File
@@ -1,5 +1,7 @@
import { GET_UPDATES } from "./updates.types";
import store from "../store";
import { getUpdates } from "../../database/queries/UpdateQueries";
import { updateAllNovels } from "../../services/updates";
@@ -33,10 +35,12 @@ export const getUpdatesAction = () => async (dispatch) => {
dispatch({ type: GET_UPDATES, payload: groupedUpdates });
};
export const updateLibraryAction = () => async (dispatch) => {
export const updateLibraryAction = () => async (dispatch, getState) => {
showToast("Updating library");
await updateAllNovels();
const { downloadNewChapters = false } = getState().settingsReducer;
await updateAllNovels({ downloadNewChapters });
dispatch(getUpdatesAction());
};
+4 -1
View File
@@ -14,6 +14,7 @@ import { scrapeSearchResults } from "./discover/MyAnimeListScraper";
const BrowseMalScreen = ({ navigation, route }) => {
const theme = useTheme();
const settings = useSettings();
const [loading, setLoading] = useState(true);
const [novels, setNovels] = useState();
@@ -75,10 +76,12 @@ const BrowseMalScreen = ({ navigation, route }) => {
novelName: item.title,
})
}
theme={theme}
settings={settings}
/>
);
const { displayMode, novelsPerRow } = useSettings();
const { displayMode, novelsPerRow } = settings;
const orientation = getDeviceOrientation();
+1 -1
View File
@@ -21,7 +21,7 @@ const BrowseSettings = ({ navigation }) => {
const { filters = [], showMyAnimeList = true } = useSelector(
(state) => state.sourceReducer
);
const languages = ["English", "Spanish", "Japanese", "Chinese"];
const languages = ["English", "Spanish", "Japanese", "Chinese", "Arabic"];
const { searchAllSources = false } = useSettings();
const renderItem = ({ item }) => {
+1 -5
View File
@@ -21,11 +21,7 @@ const ExtensionCard = ({ item, theme, isPinned, dispatch, navigation }) => {
onPress={navigateToExtension}
android_ripple={{ color: theme.rippleColor }}
>
<Image
source={{ uri: icon }}
style={styles.extensionIcon}
// resizeMode="contain"
/>
<Image source={{ uri: icon }} style={styles.extensionIcon} />
<View style={styles.extensionDetails}>
<View>
<Text
-3
View File
@@ -3,10 +3,8 @@ import {
StyleSheet,
View,
Text,
ActivityIndicator,
StatusBar,
ScrollView,
Image,
Pressable,
} from "react-native";
@@ -46,7 +44,6 @@ import { parseChapterNumber } from "../../services/updates";
import ChapterAppbar from "./components/ChapterAppbar";
import ReaderSheet from "./components/ReaderSheet";
import EmptyView from "../../components/EmptyView";
import FitImage from "react-native-fit-image";
import ChapterFooter from "./components/ChapterFooter";
import VerticalScrollbar from "./components/VerticalScrollbar";
+4 -4
View File
@@ -29,7 +29,7 @@ const ReaderSeekbar = ({
<View style={styles.verticalSliderContainer}>
<Text
style={{
color: theme.textColorPrimary,
color: "#FFFFFF",
marginLeft: 16,
transform: [{ rotate: "-90deg" }],
}}
@@ -52,7 +52,7 @@ const ReaderSeekbar = ({
/>
<Text
style={{
color: theme.textColorPrimary,
color: "#FFFFFF",
marginRight: 16,
transform: [{ rotate: "-90deg" }],
}}
@@ -66,7 +66,7 @@ const ReaderSeekbar = ({
<View style={styles.horizontalSliderContainer}>
<Text
style={{
color: theme.textColorPrimary,
color: "#FFFFFF",
marginLeft: 16,
}}
>
@@ -88,7 +88,7 @@ const ReaderSeekbar = ({
/>
<Text
style={{
color: theme.textColorPrimary,
color: "#FFFFFF",
marginRight: 16,
}}
>
+22 -20
View File
@@ -1,4 +1,4 @@
import React, { memo, useCallback } from "react";
import React, { memo, useCallback, useEffect } from "react";
import { Text, View } from "react-native";
import FitImage from "react-native-fit-image";
@@ -57,7 +57,14 @@ const HorizontalRule = ({ textColor }) => (
/>
);
const Link = ({ link, text, textStyle, textSelectable, onPressLink }) => (
const Link = ({
link,
text,
textStyle,
textSelectable,
onPressLink,
theme,
}) => (
<Text
style={[
textStyle,
@@ -87,8 +94,6 @@ const TextToComponents = ({
let parseText = text.trim().split(regex);
console.log("Rerendered");
return parseText.map((part, index) => {
if (part) {
let match = part.match(/\[(.*?)\]\((.*?)\)/);
@@ -110,7 +115,7 @@ const TextToComponents = ({
text={text}
textStyle={textStyle}
textSelectable={textSelectable}
index={index}
key={index}
textSize={textSize}
/>
);
@@ -121,7 +126,7 @@ const TextToComponents = ({
text={text}
textStyle={textStyle}
textSelectable={textSelectable}
index={index}
key={index}
textSize={textSize}
/>
);
@@ -132,7 +137,7 @@ const TextToComponents = ({
text={text}
textStyle={textStyle}
textSelectable={textSelectable}
index={index}
key={index}
textSize={textSize}
/>
);
@@ -143,7 +148,7 @@ const TextToComponents = ({
text={text}
textStyle={textStyle}
textSelectable={textSelectable}
index={index}
key={index}
textSize={textSize}
/>
);
@@ -154,7 +159,7 @@ const TextToComponents = ({
text={text}
textStyle={textStyle}
textSelectable={textSelectable}
index={index}
key={index}
textSize={textSize}
/>
);
@@ -165,7 +170,7 @@ const TextToComponents = ({
text={text}
textStyle={textStyle}
textSelectable={textSelectable}
index={index}
key={index}
textSize={textSize}
/>
);
@@ -176,7 +181,7 @@ const TextToComponents = ({
text={text}
textStyle={textStyle}
textSelectable={textSelectable}
index={index}
key={index}
textSize={textSize}
/>
);
@@ -186,7 +191,7 @@ const TextToComponents = ({
text={text}
textStyle={textStyle}
textSelectable={textSelectable}
index={index}
key={index}
/>
);
@@ -198,18 +203,15 @@ const TextToComponents = ({
textSelectable={textSelectable}
text={link}
onPressLink={onPressLink}
theme={theme}
key={index}
/>
);
case "img":
return (
<FitImage source={{ uri: text }} index={index} />
);
return <FitImage source={{ uri: text }} key={index} />;
case "hr":
return (
<HorizontalRule
index={index}
textColor={textColor}
/>
<HorizontalRule key={index} textColor={textColor} />
);
}
} else {
@@ -218,7 +220,7 @@ const TextToComponents = ({
text={part}
textStyle={textStyle}
textSelectable={textSelectable}
index={index}
key={index}
/>
);
}
@@ -11,11 +11,9 @@ const VerticalScrollbar = ({
scrollPercentage,
setLoading,
scrollViewRef,
setScrollPercentage,
}) => {
const onSlidingComplete = (value) => {
setLoading(true);
// setScrollPercentage(value);
scrollViewRef.current.scrollTo({
x: 0,
y: Math.round((value * contentSize) / 100),
@@ -31,7 +29,7 @@ const VerticalScrollbar = ({
<View style={styles.verticalSliderContainer}>
<Text
style={{
color: theme.textColorPrimary,
color: "#FFFFFF",
marginLeft: 16,
transform: [{ rotate: "-90deg" }],
}}
@@ -50,11 +48,11 @@ const VerticalScrollbar = ({
onSlidingComplete={onSlidingComplete}
thumbTintColor={theme.colorAccent}
minimumTrackTintColor={theme.colorAccent}
maximumTrackTintColor={theme.textColorPrimary}
maximumTrackTintColor="#FFFFFF"
/>
<Text
style={{
color: theme.textColorPrimary,
color: "#FFFFFF",
marginRight: 16,
transform: [{ rotate: "-90deg" }],
}}
@@ -68,7 +66,7 @@ const VerticalScrollbar = ({
<View style={styles.horizontalSliderContainer}>
<Text
style={{
color: theme.textColorPrimary,
color: "#FFFFFF",
marginLeft: 16,
}}
>
@@ -90,7 +88,7 @@ const VerticalScrollbar = ({
/>
<Text
style={{
color: theme.textColorPrimary,
color: "#FFFFFF",
marginRight: 16,
}}
>
+6 -1
View File
@@ -20,8 +20,11 @@ const Extension = ({ navigation, route }) => {
const theme = useTheme();
const library = useLibrary();
const dispatch = useDispatch();
const { displayMode } = useSettings();
const settings = useSettings();
const { displayMode } = settings;
const [loading, setLoading] = useState(true);
const [novels, setNovels] = useState([]);
@@ -117,6 +120,8 @@ const Extension = ({ navigation, route }) => {
})
}
libraryStatus={checkIFInLibrary(item.sourceId, item.novelUrl)}
settings={settings}
theme={theme}
/>
);
+4 -1
View File
@@ -27,11 +27,12 @@ import LibraryFilterSheet from "./components/LibraryFilterSheet";
const LibraryScreen = ({ navigation }) => {
const theme = useTheme();
const dispatch = useDispatch();
const settings = useSettings();
const {
showNumberOfNovels,
incognitoMode = false,
updateLibraryOnLaunch = false,
} = useSettings();
} = settings;
const libraryFilterSheetRef = useRef(null);
@@ -79,6 +80,8 @@ const LibraryScreen = ({ navigation }) => {
navigation.navigate("Novel", item);
dispatch(setNovel(item));
}}
settings={settings}
theme={theme}
/>
),
[]
+6 -12
View File
@@ -1,4 +1,4 @@
import React, { useEffect, useState } from "react";
import React, { useState } from "react";
import { FlatList, View, Text, StyleSheet } from "react-native";
import {
FAB,
@@ -21,7 +21,6 @@ import {
} from "../../redux/downloads/downloads.actions";
import BackgroundService from "react-native-background-actions";
import { downloadAllChaptersAction } from "../../redux/novel/novel.actions";
import { showToast } from "../../hooks/showToast";
const DownloadQueue = ({ navigation }) => {
@@ -34,13 +33,6 @@ const DownloadQueue = ({ navigation }) => {
const openMenu = () => setVisible(true);
const closeMenu = () => setVisible(false);
useEffect(() => {
// if (!BackgroundService.isRunning() && downloadQueue.length > 0) {
// dispatch(cancelDownload());
// }
console.log(downloadQueue);
}, []);
const [fab, setFab] = useState(BackgroundService.isRunning());
return (
@@ -60,9 +52,11 @@ const DownloadQueue = ({ navigation }) => {
>
<Menu.Item
onPress={() => {
dispatch(cancelDownload());
showToast("Downloads cancelled.");
BackgroundService.stop();
if (downloadQueue.length > 0) {
dispatch(cancelDownload());
showToast("Downloads cancelled.");
BackgroundService.stop();
}
closeMenu();
}}
title="Cancel downloads"
+128
View File
@@ -0,0 +1,128 @@
import React, { useCallback, useEffect, useState } from "react";
import { FlatList, StyleSheet } from "react-native";
import { useDispatch, useSelector } from "react-redux";
import { Appbar } from "../../components/Appbar";
import { ScreenContainer } from "../../components/Common";
import EmptyView from "../../components/EmptyView";
import { LoadingScreen } from "../../components/LoadingScreen/LoadingScreen";
import { getDownloadedChapters } from "../../database/queries/ChapterQueries";
import { useTheme } from "../../hooks/reduxHooks";
import {
deleteChapterAction,
downloadChapterAction,
} from "../../redux/novel/novel.actions";
import UpdatesItem from "../Updates/components/UpdatesItem";
const Downloads = ({ navigation }) => {
const theme = useTheme();
const dispatch = useDispatch();
const { downloadQueue } = useSelector((state) => state.downloadsReducer);
const [loading, setLoading] = useState(true);
const [chapters, setChapters] = useState([]);
const getChapters = async () => {
const res = await getDownloadedChapters();
setChapters(res);
setLoading(false);
};
const ListEmptyComponent = () =>
!loading && <EmptyView icon="(˘・_・˘)" description="No downloads" />;
useEffect(() => {
getChapters();
}, []);
const onPress = useCallback(
(item) =>
navigation.navigate("Chapter", {
chapterId: item.chapterId,
chapterUrl: item.chapterUrl,
sourceId: item.sourceId,
novelUrl: item.novelUrl,
chapterName: item.chapterName,
novelId: item.novelId,
novelName: item.novelName,
bookmark: item.bookmark,
}),
[]
);
const onPressCover = useCallback(
(item) =>
navigation.navigate("Novel", {
sourceId: item.sourceId,
novelUrl: item.novelUrl,
novelName: item.novelName,
novelCover: item.novelCover,
}),
[]
);
const downloadChapter = (
sourceId,
novelUrl,
chapterUrl,
chapterName,
chapterId
) =>
dispatch(
downloadChapterAction(
sourceId,
novelUrl,
chapterUrl,
chapterName,
chapterId
)
);
const deleteChapter = (chapterId, chapterName) => {
dispatch(deleteChapterAction(chapterId, chapterName));
setChapters((chapters) =>
chapters.filter((item) => item.chapterId !== chapterId)
);
};
const renderItem = ({ item }) => (
<UpdatesItem
item={item}
theme={theme}
onPress={() => onPress(item)}
onPressCover={() => onPressCover(item)}
downloadQueue={downloadQueue}
deleteChapter={deleteChapter}
downloadChapter={downloadChapter}
/>
);
return (
<ScreenContainer theme={theme}>
<Appbar title="Downloads" onBackAction={navigation.goBack} />
{loading ? (
<LoadingScreen theme={theme} />
) : (
<FlatList
contentContainerStyle={styles.flatList}
data={chapters}
keyExtractor={(item) => item.chapterId.toString()}
renderItem={renderItem}
ListEmptyComponent={<ListEmptyComponent />}
/>
)}
</ScreenContainer>
);
};
export default Downloads;
const styles = StyleSheet.create({
container: { flex: 1 },
flatList: { flexGrow: 1, paddingVertical: 8 },
});
+16 -1
View File
@@ -30,7 +30,8 @@ const GenralSettings = ({ navigation }) => {
(state) => state.settingsReducer
);
const { updateLibraryOnLaunch = false } = useSettings();
const { updateLibraryOnLaunch = false, downloadNewChapters = false } =
useSettings();
const displayModeLabel = (displayMode) => {
const label = {
@@ -120,6 +121,20 @@ const GenralSettings = ({ navigation }) => {
}
theme={theme}
/>
<ListSubHeader theme={theme}>Auto-download</ListSubHeader>
<SwitchSetting
label="Download new chapters"
value={downloadNewChapters}
onPress={() =>
dispatch(
setAppSettings(
"downloadNewChapters",
!downloadNewChapters
)
)
}
theme={theme}
/>
<ListSubHeader theme={theme}>Theme</ListSubHeader>
<ListItem
title="Theme"
+12 -1
View File
@@ -90,7 +90,7 @@ const MoreScreen = ({ navigation }) => {
downloadQueue.length > 0 &&
downloadQueue.length + " remaining"
}
icon="download-outline"
icon="progress-download"
onPress={() =>
navigation.navigate("MoreStack", {
screen: "DownloadQueue",
@@ -98,6 +98,17 @@ const MoreScreen = ({ navigation }) => {
}
theme={theme}
/>
<ListItem
title="Downloads"
icon="folder-download"
onPress={() =>
navigation.navigate("MoreStack", {
screen: "Downloads",
})
}
theme={theme}
/>
<ListItem
title="Settings"
icon="cog-outline"
@@ -1,80 +0,0 @@
import React, { useState } from "react";
import { StyleSheet, Text } from "react-native";
import { Modal, Portal, TextInput } from "react-native-paper";
const AccentColorModal = ({
accentColorModal,
hideAccentColorModal,
theme,
onPress,
color,
}) => {
const [text, setText] = useState(color);
const [error, setError] = useState();
return (
<Portal>
<Modal
visible={accentColorModal}
onDismiss={() => {
hideAccentColorModal();
setText();
setError();
}}
contentContainerStyle={[
styles.containerStyle,
{ backgroundColor: theme.colorPrimary },
]}
>
<Text
style={[
styles.dialogTitle,
{ color: theme.textColorPrimary },
]}
>
Hex Color Code
</Text>
<TextInput
value={text}
defaultValue={color}
onChangeText={(text) => setText(text)}
onSubmitEditing={() => {
if (text.match(/^#([0-9a-f]{6}|[0-9a-f]{3})$/i)) {
onPress(text);
hideAccentColorModal();
} else {
setError("Enter a valid hex color code");
}
}}
mode="outlined"
theme={{
colors: {
primary: color,
placeholder: theme.textColorHint,
text: theme.textColorPrimary,
background: "transparent",
},
}}
underlineColor={theme.textColorHint}
dense
error={error}
/>
<Text style={{ color: "#FF0033", paddingTop: 8 }}>{error}</Text>
</Modal>
</Portal>
);
};
export default AccentColorModal;
const styles = StyleSheet.create({
containerStyle: {
margin: 30,
padding: 20,
borderRadius: 8,
},
dialogTitle: {
fontSize: 18,
marginBottom: 16,
},
});
+53 -12
View File
@@ -15,7 +15,9 @@ import { useDispatch, useSelector } from "react-redux";
import {
bookmarkChapterAction,
deleteAllChaptersAction,
deleteChapterAction,
downloadAllChaptersAction,
downloadChapterAction,
getNovelAction,
markChaptersRead,
markChapterUnreadAction,
@@ -45,7 +47,7 @@ const Novel = ({ route, navigation }) => {
const theme = useTheme();
const dispatch = useDispatch();
const { novel, chapters, loading, updating, downloading } = useNovel();
const { novel, chapters, loading, updating } = useNovel();
const { downloadQueue } = useSelector((state) => state.downloadsReducer);
const [selected, setSelected] = useState([]);
@@ -54,7 +56,6 @@ const Novel = ({ route, navigation }) => {
let flatlistRef = useRef(null);
let chaptersSettingsSheetRef = useRef(null);
let trackerSheetRef = useRef(null);
let chapterActionsSheetRef = useRef(null);
const {
sort = "ORDER BY chapterId ASC",
@@ -91,25 +92,65 @@ const Novel = ({ route, navigation }) => {
const keyExtractor = useCallback((item) => item.chapterId.toString(), []);
const downloadChapter = (chapterUrl, chapterName, chapterId) =>
dispatch(
downloadChapterAction(
sourceId,
novelUrl,
chapterUrl,
chapterName,
chapterId
)
);
const deleteChapter = (chapterId, chapterName) =>
dispatch(deleteChapterAction(chapterId, chapterName));
const isSelected = (chapterId) => {
return selected.some((obj) => obj.chapterId === chapterId);
};
const onSelectPress = (chapter, navigateToChapter) => {
if (selected.length === 0) {
navigateToChapter();
} else {
if (isSelected(chapter.chapterId)) {
setSelected((selected) =>
selected.filter(
(item) => item.chapterId !== chapter.chapterId
)
);
} else {
setSelected((selected) => [...selected, chapter]);
}
}
};
const onSelectLongPress = (chapter) => {
if (isSelected(chapter.chapterId)) {
setSelected((selected) =>
selected.filter((item) => item.chapterId !== chapter.chapterId)
);
} else {
setSelected((selected) => [...selected, chapter]);
}
};
const renderItem = ({ item, index }) => (
<ChapterItem
theme={theme}
chapter={item}
index={index}
novelUrl={novelUrl}
dispatch={dispatch}
sourceId={sourceId}
novelData={{ sourceId, novelUrl, novelName }}
navigation={navigation}
lastReadChapter={lastReadChapter}
position={position}
downloading={downloading}
selected={selected}
novelName={novelName}
selected={selected}
setSelected={setSelected}
showChapterTitles={showChapterTitles}
chapterActionsSheetRef={chapterActionsSheetRef}
downloadQueue={downloadQueue}
deleteChapter={deleteChapter}
downloadChapter={downloadChapter}
isSelected={isSelected}
onSelectPress={onSelectPress}
onSelectLongPress={onSelectLongPress}
/>
);
@@ -1,7 +1,57 @@
import React from "react";
import { ActivityIndicator } from "react-native";
import { IconButton } from "react-native-paper";
import { IconButton, Menu } from "react-native-paper";
export const DownloadButton = ({
downloadQueue,
chapter,
theme,
deleteChapter,
downloadChapter,
hideDeleteChapterMenu,
showDeleteChapterMenu,
deleteChapterMenu,
}) => {
if (downloadQueue.some((chap) => chap.chapterId === chapter.chapterId)) {
return <ChapterDownloadingButton theme={theme} />;
} else if (chapter.downloaded === 1) {
return (
<Menu
visible={deleteChapterMenu}
onDismiss={hideDeleteChapterMenu}
anchor={
<DeleteChapterButton
theme={theme}
onPress={showDeleteChapterMenu}
/>
}
contentStyle={{ backgroundColor: theme.menuColor }}
>
<Menu.Item
onPress={() =>
deleteChapter(chapter.chapterId, chapter.chapterName)
}
title="Delete"
titleStyle={{ color: theme.textColorPrimary }}
/>
</Menu>
);
} else {
return (
<DownloadChapterButton
theme={theme}
onPress={() =>
downloadChapter(
chapter.chapterUrl,
chapter.chapterName,
chapter.chapterId
)
}
/>
);
}
};
export const ChapterDownloadingButton = ({ theme }) => (
<ActivityIndicator
+21 -97
View File
@@ -1,36 +1,30 @@
import React, { memo, useCallback, useState } from "react";
import React, { memo, useState } from "react";
import { View, Text, StyleSheet, Pressable } from "react-native";
import { Menu } from "react-native-paper";
import { Row } from "../../../components/Common";
import {
deleteChapterAction,
downloadChapterAction,
} from "../../../redux/novel/novel.actions";
import { parseChapterNumber } from "../../../services/utils/helpers";
import {
ChapterBookmarkButton,
ChapterDownloadingButton,
DeleteChapterButton,
DownloadChapterButton,
DownloadButton,
} from "./Chapter/ChapterDownloadButtons";
const ChapterItem = ({
novelUrl,
novelName,
novelData,
chapter,
sourceId,
dispatch,
theme,
navigation,
index,
position,
selected,
setSelected,
showChapterTitles,
downloadQueue,
downloadChapter,
deleteChapter,
isSelected,
onSelectPress,
onSelectLongPress,
}) => {
const { sourceId, novelName, novelUrl } = novelData;
const {
novelId,
chapterId,
@@ -39,7 +33,6 @@ const ChapterItem = ({
read,
releaseDate,
bookmark,
downloaded,
} = chapter;
const [deleteChapterMenu, setDeleteChapterMenu] = useState(false);
@@ -58,84 +51,6 @@ const ChapterItem = ({
bookmark,
});
const downloadChapter = useCallback(
() =>
dispatch(
downloadChapterAction(
sourceId,
novelUrl,
chapterUrl,
chapterName,
chapterId
)
),
[]
);
const deleteChapter = () =>
dispatch(deleteChapterAction(chapterId, chapterName));
const renderDownloadIcon = () => {
if (downloadQueue.some((chap) => chap.chapterId === chapterId)) {
return <ChapterDownloadingButton theme={theme} />;
} else if (downloaded === 1) {
return (
<Menu
visible={deleteChapterMenu}
onDismiss={hideDeleteChapterMenu}
anchor={
<DeleteChapterButton
theme={theme}
onPress={showDeleteChapterMenu}
/>
}
contentStyle={{ backgroundColor: theme.menuColor }}
>
<Menu.Item
onPress={deleteChapter}
title="Delete"
titleStyle={{ color: theme.textColorPrimary }}
/>
</Menu>
);
} else {
return (
<DownloadChapterButton
theme={theme}
onPress={downloadChapter}
/>
);
}
};
const isSelected = (chapterId) => {
return selected.some((obj) => obj.chapterId === chapterId);
};
const onPress = () => {
if (selected.length === 0) {
navigateToChapter();
} else {
if (isSelected(chapterId)) {
setSelected((selected) =>
selected.filter((item) => item.chapterId !== chapterId)
);
} else {
setSelected((selected) => [...selected, chapter]);
}
}
};
const onLongPress = () => {
if (isSelected(chapterId)) {
setSelected((selected) =>
selected.filter((item) => item.chapterId !== chapterId)
);
} else {
setSelected((selected) => [...selected, chapter]);
}
};
const renderProgressPercentage = () => {
const savedProgress =
position && position[chapterId] && position[chapterId].percentage;
@@ -164,8 +79,8 @@ const ChapterItem = ({
backgroundColor: theme.rippleColor,
},
]}
onPress={onPress}
onLongPress={onLongPress}
onPress={() => onSelectPress(chapter, navigateToChapter)}
onLongPress={() => onSelectLongPress(chapter)}
android_ripple={{ color: theme.rippleColor }}
>
<Row>
@@ -208,7 +123,16 @@ const ChapterItem = ({
</View>
</View>
</Row>
<View>{renderDownloadIcon()}</View>
<DownloadButton
downloadQueue={downloadQueue}
chapter={chapter}
theme={theme}
deleteChapter={deleteChapter}
downloadChapter={downloadChapter}
hideDeleteChapterMenu={hideDeleteChapterMenu}
showDeleteChapterMenu={showDeleteChapterMenu}
deleteChapterMenu={deleteChapterMenu}
/>
</Pressable>
);
};
+29 -1
View File
@@ -20,11 +20,16 @@ import { Searchbar } from "../../components/Searchbar/Searchbar";
import moment from "moment";
import { dateFormat } from "../../services/utils/constants";
import {
deleteChapterAction,
downloadChapterAction,
} from "../../redux/novel/novel.actions";
const Updates = ({ navigation }) => {
const theme = useTheme();
const dispatch = useDispatch();
const { updates, loading } = useSelector((state) => state.updatesReducer);
const { downloadQueue } = useSelector((state) => state.downloadsReducer);
const [searchText, setSearchText] = useState("");
const [searchResults, setSearchResults] = useState([]);
@@ -33,7 +38,7 @@ const Updates = ({ navigation }) => {
useFocusEffect(
useCallback(() => {
dispatch(getUpdatesAction());
}, [getUpdatesAction])
}, [getUpdatesAction, downloadQueue])
);
const onRefresh = async () => {
@@ -68,12 +73,35 @@ const Updates = ({ navigation }) => {
[]
);
const downloadChapter = (
sourceId,
novelUrl,
chapterUrl,
chapterName,
chapterId
) =>
dispatch(
downloadChapterAction(
sourceId,
novelUrl,
chapterUrl,
chapterName,
chapterId
)
);
const deleteChapter = (chapterId, chapterName) =>
dispatch(deleteChapterAction(chapterId, chapterName));
const renderItem = ({ item }) => (
<UpdatesItem
item={item}
theme={theme}
onPress={() => onPress(item)}
onPressCover={() => onPressCover(item)}
downloadQueue={downloadQueue}
deleteChapter={deleteChapter}
downloadChapter={downloadChapter}
/>
);
-17
View File
@@ -1,17 +0,0 @@
import React from "react";
import { Appbar as MaterialAppbar } from "react-native-paper";
import { updateLibraryAction } from "../../../redux/updates/updates.actions";
export const Appbar = ({ theme, dispatch }) => (
<MaterialAppbar.Header style={{ backgroundColor: theme.colorPrimary }}>
<MaterialAppbar.Content
title="Updates"
titleStyle={{ color: theme.textColorPrimary }}
/>
<MaterialAppbar.Action
icon="reload"
onPress={() => dispatch(updateLibraryAction())}
/>
</MaterialAppbar.Header>
);
@@ -0,0 +1,60 @@
import React from "react";
import { Menu } from "react-native-paper";
import {
ChapterDownloadingButton,
DeleteChapterButton,
DownloadChapterButton,
} from "../../Novel/components/Chapter/ChapterDownloadButtons";
export const DownloadButton = ({
downloadQueue,
theme,
chapter,
downloadChapter,
deleteChapter,
deleteChapterMenu,
showDeleteChapterMenu,
hideDeleteChapterMenu,
}) => {
if (downloadQueue.some((chap) => chap.chapterId === chapter.chapterId)) {
return <ChapterDownloadingButton theme={theme} />;
} else if (chapter.downloaded === 1) {
return (
<Menu
visible={deleteChapterMenu}
onDismiss={hideDeleteChapterMenu}
anchor={
<DeleteChapterButton
theme={theme}
onPress={showDeleteChapterMenu}
/>
}
contentStyle={{ backgroundColor: theme.menuColor }}
>
<Menu.Item
onPress={() =>
deleteChapter(chapter.chapterId, chapter.chapterName)
}
title="Delete"
titleStyle={{ color: theme.textColorPrimary }}
/>
</Menu>
);
} else {
return (
<DownloadChapterButton
theme={theme}
onPress={() =>
downloadChapter(
chapter.sourceId,
chapter.novelUrl,
chapter.chapterUrl,
chapter.chapterName,
chapter.chapterId
)
}
/>
);
}
};
+59 -26
View File
@@ -1,8 +1,22 @@
import React from "react";
import React, { useState } from "react";
import { View, Text, StyleSheet, Pressable } from "react-native";
import FastImage from "react-native-fast-image";
const UpdateCard = ({ item, theme, onPress, onPressCover }) => {
import FastImage from "react-native-fast-image";
import { DownloadButton } from "./DownloadButton";
const UpdateCard = ({
item,
theme,
onPress,
onPressCover,
downloadQueue,
downloadChapter,
deleteChapter,
}) => {
const [deleteChapterMenu, setDeleteChapterMenu] = useState(false);
const showDeleteChapterMenu = () => setDeleteChapterMenu(true);
const hideDeleteChapterMenu = () => setDeleteChapterMenu(false);
return (
<Pressable
style={styles.updateCard}
@@ -16,29 +30,48 @@ const UpdateCard = ({ item, theme, onPress, onPressCover }) => {
style={styles.updateIcon}
/>
</Pressable>
<View style={styles.chapterDetails}>
<Text
style={[
{ color: theme.textColorPrimary, fontSize: 14 },
item.read && {
color: theme.textColorHint,
},
]}
numberOfLines={1}
>
{item.novelName}
</Text>
<Text
style={[
{ color: theme.textColorPrimary, fontSize: 12 },
item.read && {
color: theme.textColorHint,
},
]}
numberOfLines={1}
>
{item.chapterName}
</Text>
<View
style={{
flex: 1,
flexDirection: "row",
justifyContent: "space-between",
alignItems: "center",
}}
>
<View style={styles.chapterDetails}>
<Text
style={[
{ color: theme.textColorPrimary, fontSize: 14 },
item.read && {
color: theme.textColorHint,
},
]}
numberOfLines={1}
>
{item.novelName}
</Text>
<Text
style={[
{ color: theme.textColorPrimary, fontSize: 12 },
item.read && {
color: theme.textColorHint,
},
]}
numberOfLines={1}
>
{item.chapterName}
</Text>
</View>
<DownloadButton
chapter={item}
deleteChapter={deleteChapter}
downloadChapter={downloadChapter}
downloadQueue={downloadQueue}
showDeleteChapterMenu={showDeleteChapterMenu}
deleteChapterMenu={deleteChapterMenu}
hideDeleteChapterMenu={hideDeleteChapterMenu}
theme={theme}
/>
</View>
</>
</Pressable>
+19 -11
View File
@@ -76,9 +76,16 @@ export const updateNovel = async (sourceId, novelUrl, novelId) => {
});
};
export const updateNovelChapters = async (sourceId, novelUrl, novelId) => {
export const updateNovelChapters = async (
sourceId,
novelUrl,
novelId,
updateSettings
) => {
let chapters = await fetchChapters(sourceId, novelUrl);
const { downloadNewChapters = false } = updateSettings;
db.transaction((tx) => {
chapters.map((chapter) =>
tx.executeSql(
@@ -91,14 +98,14 @@ export const updateNovelChapters = async (sourceId, novelUrl, novelId) => {
],
(txObj, { insertId }) => {
if (insertId !== -1) {
// console.log(
// "Inserted Chapter " +
// chapter.chapterUrl +
// " Chapter Name " +
// chapter.chapterName +
// " Novel Id " +
// novelId
// );
downloadNewChapters &&
downloadChapter(
sourceId,
novelUrl,
chapter.chapterUrl,
insertId
);
tx.executeSql(
"INSERT OR IGNORE INTO updates (chapterId, novelId, updateTime) values (?, ?, (datetime('now','localtime')))",
[insertId, novelId],
@@ -113,7 +120,7 @@ export const updateNovelChapters = async (sourceId, novelUrl, novelId) => {
});
};
export const updateAllNovels = async () => {
export const updateAllNovels = async (updateSettings) => {
const libraryNovels = await getLibrary();
const options = {
@@ -149,7 +156,8 @@ export const updateAllNovels = async () => {
await updateNovelChapters(
libraryNovels[i].sourceId,
libraryNovels[i].novelUrl,
libraryNovels[i].novelId
libraryNovels[i].novelId,
updateSettings
);
// console.log(libraryNovels[i].novelName + " Updated");
await BackgroundService.updateNotification({
@@ -0,0 +1,13 @@
import WPMangaStreamScraper from "./WPMangaStreamScraper";
export const KolNovelScraper = new WPMangaStreamScraper(
53,
"https://kolnovel.com/",
"KolNovel"
);
export const RewayatArScraper = new WPMangaStreamScraper(
54,
"https://rewayat-ar.site/",
"RewayatAr"
);
@@ -0,0 +1,174 @@
import cheerio from "react-native-cheerio";
import { htmlToText } from "../../helpers/htmlToText";
class WPMangaStreamScraper {
constructor(sourceId, baseUrl, sourceName) {
this.sourceId = sourceId;
this.baseUrl = baseUrl;
this.sourceName = sourceName;
}
async popularNovels(page) {
let totalPages = 100;
let url = this.baseUrl + "series/?page=" + page + "?m_orderby=popular";
let sourceId = this.sourceId;
const result = await fetch(url);
const body = await result.text();
const $ = cheerio.load(body);
let novels = [];
$("article.bs").each(function () {
const novelName = $(this).find(".ntitle").text().trim();
let image = $(this).find("img");
const novelCover = image.attr("src");
let novelUrl = $(this).find("a").attr("href").split("/")[4];
const novel = {
sourceId,
novelName,
novelCover,
novelUrl,
};
novels.push(novel);
});
return { totalPages, novels };
}
async parseNovelAndChapters(novelUrl) {
const url = `${this.baseUrl}series/${novelUrl}/`;
const result = await fetch(url);
const body = await result.text();
let $ = cheerio.load(body);
let novel = {};
novel.sourceId = this.sourceId;
novel.sourceName = this.sourceName;
novel.url = url;
novel.novelUrl = novelUrl;
novel.novelName = $(".entry-title").text();
novel.novelCover = $("img.wp-post-image").attr("src");
$("div.spe > span").each(function () {
const detailName = $(this).find("b").text().trim();
const detail = $(this).find("b").next().text().trim();
if ($(this).text().includes("الحالة:")) {
novel.status = $(this).text().replace("الحالة: ", "");
}
switch (detailName) {
case "المؤلف:":
novel.auhtor = detail;
break;
}
});
novel.genre = $(".genxed").text().replace(/\s/g, ",");
novel.summary = $('div[itemprop="description"]').text().trim();
let novelChapters = [];
$(".eplister")
.find("li")
.each(function () {
const chapterName =
$(this).find(".epl-num").text() +
" - " +
$(this).find(".epl-title").text();
const releaseDate = $(this).find(".epl-date").text().trim();
const chapterUrl = $(this).find("a").attr("href").split("/")[3];
novelChapters.push({ chapterName, releaseDate, chapterUrl });
});
novel.chapters = novelChapters.reverse();
return novel;
}
async parseChapter(novelUrl, chapterUrl) {
let sourceId = this.sourceId;
const url = `${this.baseUrl}${chapterUrl}`;
const result = await fetch(url);
const body = await result.text();
const $ = cheerio.load(body);
let chapterName = $(".entry-title").text();
let chapterText = $("div.epcontent").text();
if (chapterText) {
chapterText = htmlToText(chapterText);
}
let nextChapter = null;
let prevChapter = null;
const chapter = {
sourceId,
novelUrl,
chapterUrl,
chapterName,
chapterText,
nextChapter,
prevChapter,
};
console.log(chapter);
return chapter;
}
async searchNovels(searchTerm) {
const url = `${this.baseUrl}?s=${searchTerm}`;
const result = await fetch(url);
const body = await result.text();
const $ = cheerio.load(body);
let novels = [];
let sourceId = this.sourceId;
$("article.bs").each(function () {
const novelName = $(this).find(".ntitle").text().trim();
let image = $(this).find("img");
const novelCover = image.attr("src");
let novelUrl = $(this).find("a").attr("href").split("/")[4];
const novel = {
sourceId,
novelName,
novelCover,
novelUrl,
};
novels.push(novel);
});
return novels;
}
}
module.exports = WPMangaStreamScraper;
+6
View File
@@ -51,6 +51,10 @@ import WuxiaCityScraper from "./en/wuxiacity";
import NovelUpdatesScraper from "./en/novelupdates";
import RanobesScraper from "./en/ranobes";
import YushuboScraper from "./ch/yushubo";
import {
KolNovelScraper,
RewayatArScraper,
} from "./multisrc/wpmangastream/WPMangaStreamGenerator";
export const getSource = (sourceId) => {
const scrapers = {
@@ -105,6 +109,8 @@ export const getSource = (sourceId) => {
50: NovelUpdatesScraper,
51: RanobesScraper,
52: YushuboScraper,
53: KolNovelScraper,
54: RewayatArScraper,
};
return scrapers[sourceId];
+14
View File
@@ -355,5 +355,19 @@
"sourceName": "Yushubo",
"icon": "https://github.com/LNReader/lnreader-sources/blob/main/src/ch/yushubo/icon.png?raw=true",
"lang": "Chinese"
},
{
"sourceId": 53,
"sourceUrl": "https://kolnovel.com/",
"sourceName": "KolNovel",
"icon": "https://github.com/LNReader/lnreader-sources/blob/main/multisrc/wpmangastream/icons/kolnovel.png?raw=true",
"lang": "Arabic"
},
{
"sourceId": 54,
"sourceUrl": "https://rewayat-ar.site/",
"sourceName": "RewayatAr",
"icon": "https://github.com/LNReader/lnreader-sources/blob/main/multisrc/wpmangastream/icons/rewayatar.png?raw=true",
"lang": "Arabic"
}
]