diff --git a/src/components/ColorPickerModal.js b/src/components/ColorPickerModal.js index a2b3b943..57f77816 100644 --- a/src/components/ColorPickerModal.js +++ b/src/components/ColorPickerModal.js @@ -10,6 +10,7 @@ const ColorPickerModal = ({ onSubmit, hideModal, modalVisible, + showAccentColors, }) => { const [text, setText] = useState(color); const [error, setError] = useState(); diff --git a/src/components/NovelCover.js b/src/components/NovelCover.js index bd0fa3f7..0bdbfc1c 100644 --- a/src/components/NovelCover.js +++ b/src/components/NovelCover.js @@ -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 && ( - - {item.novelName} - - ); - - const compactTitle = () => - displayMode === 0 && ( - - - - {item.novelName} - - - - ); - - const unreadBadge = () => - showUnreadBadges && - item.chaptersUnread && ( - - {item.chaptersUnread} - - ); - - const downloadBadge = () => - showDownloadBadges && - item.chaptersDownloaded && ( - - {item.chaptersDownloaded} - - ); - - const inLibraryBadge = () => - libraryStatus && ( - - In library - - ); + const getHeight = useCallback( + () => (window.width / getNovelsPerRow()) * (4 / 3), + [] + ); return displayMode !== 2 ? ( { onPress={onPress} > <> - - {inLibraryBadge()} - {downloadBadge()} - {unreadBadge()} + + {libraryStatus && } + {showDownloadBadges && item.chaptersDownloaded && ( + + )} + {showUnreadBadges && item.chaptersUnread && ( + + )} { libraryStatus && { opacity: 0.5 }, ]} /> - - {compactTitle()} + + {displayMode === 0 && ( + + )} - {comfortableTitle()} + {displayMode === 1 && ( + + )} ) : ( + ) + } + unreadBadge={ + showUnreadBadges && + item.chaptersUnread && ( + + ) + } + inLibraryBadge={libraryStatus && } theme={theme} onPress={onPress} /> @@ -182,6 +116,99 @@ const NovelCover = ({ item, onPress, libraryStatus }) => { export default NovelCover; +const ComfortableTitle = ({ theme, novelName }) => ( + + {novelName} + +); + +const CompactTitle = ({ novelName }) => ( + + + + {novelName} + + + +); + +const InLibraryBadge = ({ theme }) => ( + + In library + +); + +const UnreadBadge = ({ + chaptersDownloaded, + chaptersUnread, + showDownloadBadges, + theme, +}) => ( + + {chaptersUnread} + +); + +const DownloadBadge = ({ + chaptersDownloaded, + showUnreadBadges, + chaptersUnread, +}) => ( + + {chaptersDownloaded} + +); + 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", + }, }); diff --git a/src/database/queries/ChapterQueries.js b/src/database/queries/ChapterQueries.js index a1885e5d..3810b6d8 100644 --- a/src/database/queries/ChapterQueries.js +++ b/src/database/queries/ChapterQueries.js @@ -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) + ); + }) + ); +}; diff --git a/src/navigators/More.js b/src/navigators/More.js index 3f6f53ff..4f4d66bc 100644 --- a/src/navigators/More.js +++ b/src/navigators/More.js @@ -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 = () => ( + ); diff --git a/src/redux/settings/settings.reducer.js b/src/redux/settings/settings.reducer.js index 97c81edb..1a0a883b 100644 --- a/src/redux/settings/settings.reducer.js +++ b/src/redux/settings/settings.reducer.js @@ -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) => { diff --git a/src/redux/updates/updates.actions.js b/src/redux/updates/updates.actions.js index d23868d9..8c380b85 100644 --- a/src/redux/updates/updates.actions.js +++ b/src/redux/updates/updates.actions.js @@ -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()); }; diff --git a/src/screens/Browse/BrowseMal.js b/src/screens/Browse/BrowseMal.js index 1b7b2a92..a19afbe4 100644 --- a/src/screens/Browse/BrowseMal.js +++ b/src/screens/Browse/BrowseMal.js @@ -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(); diff --git a/src/screens/Browse/BrowseSettings.js b/src/screens/Browse/BrowseSettings.js index 99e0cd83..42f70967 100644 --- a/src/screens/Browse/BrowseSettings.js +++ b/src/screens/Browse/BrowseSettings.js @@ -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 }) => { diff --git a/src/screens/Browse/components/SourceCard.js b/src/screens/Browse/components/SourceCard.js index e984b939..5275637b 100644 --- a/src/screens/Browse/components/SourceCard.js +++ b/src/screens/Browse/components/SourceCard.js @@ -21,11 +21,7 @@ const ExtensionCard = ({ item, theme, isPinned, dispatch, navigation }) => { onPress={navigateToExtension} android_ripple={{ color: theme.rippleColor }} > - + @@ -88,7 +88,7 @@ const ReaderSeekbar = ({ /> diff --git a/src/screens/Chapter/TextToComponent.js b/src/screens/Chapter/TextToComponent.js index e14822c9..ca3da18a 100644 --- a/src/screens/Chapter/TextToComponent.js +++ b/src/screens/Chapter/TextToComponent.js @@ -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, +}) => ( { 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 ( - - ); + return ; case "hr": return ( - + ); } } else { @@ -218,7 +220,7 @@ const TextToComponents = ({ text={part} textStyle={textStyle} textSelectable={textSelectable} - index={index} + key={index} /> ); } diff --git a/src/screens/Chapter/components/VerticalScrollbar.js b/src/screens/Chapter/components/VerticalScrollbar.js index 8f50219f..22114b6d 100644 --- a/src/screens/Chapter/components/VerticalScrollbar.js +++ b/src/screens/Chapter/components/VerticalScrollbar.js @@ -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 = ({ @@ -90,7 +88,7 @@ const VerticalScrollbar = ({ /> diff --git a/src/screens/Extension/Extension.js b/src/screens/Extension/Extension.js index 50d37c02..b7550f11 100644 --- a/src/screens/Extension/Extension.js +++ b/src/screens/Extension/Extension.js @@ -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} /> ); diff --git a/src/screens/Library/Library.js b/src/screens/Library/Library.js index 31970b93..06271bb4 100644 --- a/src/screens/Library/Library.js +++ b/src/screens/Library/Library.js @@ -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} /> ), [] diff --git a/src/screens/More/DownloadQueue.js b/src/screens/More/DownloadQueue.js index 00da1c89..c3fba450 100644 --- a/src/screens/More/DownloadQueue.js +++ b/src/screens/More/DownloadQueue.js @@ -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 }) => { > { - dispatch(cancelDownload()); - showToast("Downloads cancelled."); - BackgroundService.stop(); + if (downloadQueue.length > 0) { + dispatch(cancelDownload()); + showToast("Downloads cancelled."); + BackgroundService.stop(); + } closeMenu(); }} title="Cancel downloads" diff --git a/src/screens/More/Downloads.js b/src/screens/More/Downloads.js new file mode 100644 index 00000000..0a5cc72a --- /dev/null +++ b/src/screens/More/Downloads.js @@ -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 && ; + + 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 }) => ( + onPress(item)} + onPressCover={() => onPressCover(item)} + downloadQueue={downloadQueue} + deleteChapter={deleteChapter} + downloadChapter={downloadChapter} + /> + ); + + return ( + + + {loading ? ( + + ) : ( + item.chapterId.toString()} + renderItem={renderItem} + ListEmptyComponent={} + /> + )} + + ); +}; + +export default Downloads; + +const styles = StyleSheet.create({ + container: { flex: 1 }, + flatList: { flexGrow: 1, paddingVertical: 8 }, +}); diff --git a/src/screens/More/GeneralSettings.js b/src/screens/More/GeneralSettings.js index b01eb76c..287b62cb 100644 --- a/src/screens/More/GeneralSettings.js +++ b/src/screens/More/GeneralSettings.js @@ -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} /> + Auto-download + + dispatch( + setAppSettings( + "downloadNewChapters", + !downloadNewChapters + ) + ) + } + theme={theme} + /> Theme { 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} /> + + navigation.navigate("MoreStack", { + screen: "Downloads", + }) + } + theme={theme} + /> + { - const [text, setText] = useState(color); - const [error, setError] = useState(); - - return ( - - { - hideAccentColorModal(); - setText(); - setError(); - }} - contentContainerStyle={[ - styles.containerStyle, - { backgroundColor: theme.colorPrimary }, - ]} - > - - Hex Color Code - - 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} - /> - {error} - - - ); -}; - -export default AccentColorModal; - -const styles = StyleSheet.create({ - containerStyle: { - margin: 30, - padding: 20, - borderRadius: 8, - }, - dialogTitle: { - fontSize: 18, - marginBottom: 16, - }, -}); diff --git a/src/screens/Novel/Novel.js b/src/screens/Novel/Novel.js index 2ba08f23..03b10f30 100644 --- a/src/screens/Novel/Novel.js +++ b/src/screens/Novel/Novel.js @@ -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 }) => ( ); diff --git a/src/screens/Novel/components/Chapter/ChapterDownloadButtons.js b/src/screens/Novel/components/Chapter/ChapterDownloadButtons.js index 79772bb6..abffd23d 100644 --- a/src/screens/Novel/components/Chapter/ChapterDownloadButtons.js +++ b/src/screens/Novel/components/Chapter/ChapterDownloadButtons.js @@ -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 ; + } else if (chapter.downloaded === 1) { + return ( + + } + contentStyle={{ backgroundColor: theme.menuColor }} + > + + deleteChapter(chapter.chapterId, chapter.chapterName) + } + title="Delete" + titleStyle={{ color: theme.textColorPrimary }} + /> + + ); + } else { + return ( + + downloadChapter( + chapter.chapterUrl, + chapter.chapterName, + chapter.chapterId + ) + } + /> + ); + } +}; export const ChapterDownloadingButton = ({ theme }) => ( { + 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 ; - } else if (downloaded === 1) { - return ( - - } - contentStyle={{ backgroundColor: theme.menuColor }} - > - - - ); - } else { - return ( - - ); - } - }; - - 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 }} > @@ -208,7 +123,16 @@ const ChapterItem = ({ - {renderDownloadIcon()} + ); }; diff --git a/src/screens/Updates/Updates.js b/src/screens/Updates/Updates.js index 600c0dbe..556c7dc2 100644 --- a/src/screens/Updates/Updates.js +++ b/src/screens/Updates/Updates.js @@ -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 }) => ( onPress(item)} onPressCover={() => onPressCover(item)} + downloadQueue={downloadQueue} + deleteChapter={deleteChapter} + downloadChapter={downloadChapter} /> ); diff --git a/src/screens/Updates/components/Appbar.js b/src/screens/Updates/components/Appbar.js deleted file mode 100644 index 64d214ac..00000000 --- a/src/screens/Updates/components/Appbar.js +++ /dev/null @@ -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 }) => ( - - - dispatch(updateLibraryAction())} - /> - -); diff --git a/src/screens/Updates/components/DownloadButton.js b/src/screens/Updates/components/DownloadButton.js new file mode 100644 index 00000000..428ad5bb --- /dev/null +++ b/src/screens/Updates/components/DownloadButton.js @@ -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 ; + } else if (chapter.downloaded === 1) { + return ( + + } + contentStyle={{ backgroundColor: theme.menuColor }} + > + + deleteChapter(chapter.chapterId, chapter.chapterName) + } + title="Delete" + titleStyle={{ color: theme.textColorPrimary }} + /> + + ); + } else { + return ( + + downloadChapter( + chapter.sourceId, + chapter.novelUrl, + chapter.chapterUrl, + chapter.chapterName, + chapter.chapterId + ) + } + /> + ); + } +}; diff --git a/src/screens/Updates/components/UpdatesItem.js b/src/screens/Updates/components/UpdatesItem.js index 06c79fc8..57eebdbc 100644 --- a/src/screens/Updates/components/UpdatesItem.js +++ b/src/screens/Updates/components/UpdatesItem.js @@ -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 ( { style={styles.updateIcon} /> - - - {item.novelName} - - - {item.chapterName} - + + + + {item.novelName} + + + {item.chapterName} + + + diff --git a/src/services/updates.js b/src/services/updates.js index 58f7138b..053bf009 100644 --- a/src/services/updates.js +++ b/src/services/updates.js @@ -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({ diff --git a/src/sources/multisrc/wpmangastream/WPMangaStreamGenerator.js b/src/sources/multisrc/wpmangastream/WPMangaStreamGenerator.js new file mode 100644 index 00000000..8139632a --- /dev/null +++ b/src/sources/multisrc/wpmangastream/WPMangaStreamGenerator.js @@ -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" +); diff --git a/src/sources/multisrc/wpmangastream/WPMangaStreamScraper.js b/src/sources/multisrc/wpmangastream/WPMangaStreamScraper.js new file mode 100644 index 00000000..ece1c7b2 --- /dev/null +++ b/src/sources/multisrc/wpmangastream/WPMangaStreamScraper.js @@ -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; diff --git a/src/sources/sources.js b/src/sources/sources.js index 74b9b5ce..d2aa4dd8 100644 --- a/src/sources/sources.js +++ b/src/sources/sources.js @@ -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]; diff --git a/src/sources/sources.json b/src/sources/sources.json index d695b4be..9e5603e8 100644 --- a/src/sources/sources.json +++ b/src/sources/sources.json @@ -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" } ]