diff --git a/src/components/LoadingMoreIndicator/LoadingMoreIndicator.tsx b/src/components/LoadingMoreIndicator/LoadingMoreIndicator.tsx new file mode 100644 index 00000000..048935f0 --- /dev/null +++ b/src/components/LoadingMoreIndicator/LoadingMoreIndicator.tsx @@ -0,0 +1,22 @@ +import React from 'react'; +import { ActivityIndicator, StyleSheet } from 'react-native'; +import { ThemeType } from '../../theme/types'; + +interface Props { + theme: ThemeType; +} + +const LoadingMoreIndicator: React.FC = ({ theme }) => ( + +); + +export default LoadingMoreIndicator; + +const styles = StyleSheet.create({ + indicator: { + padding: 32, + flex: 1, + justifyContent: 'center', + alignItems: 'center', + }, +}); diff --git a/src/components/index.ts b/src/components/index.ts index 4c3b1870..3b9c892a 100644 --- a/src/components/index.ts +++ b/src/components/index.ts @@ -11,3 +11,4 @@ export { default as Appbar } from './Appbar/Appbar'; export { default as SwitchItem } from './SwitchItem/SwitchItem'; export { default as List } from './List/List'; export { default as ColorPreferenceItem } from './ColorPreferenceItem/ColorPreferenceItem'; +export { default as LoadingMoreIndicator } from './LoadingMoreIndicator/LoadingMoreIndicator'; diff --git a/src/navigators/Main.js b/src/navigators/Main.js index ffc3c386..6c46c19f 100644 --- a/src/navigators/Main.js +++ b/src/navigators/Main.js @@ -19,7 +19,7 @@ import MoreStack from './MoreStack'; */ import Novel from '../screens/novel/NovelScreen'; import Reader from '../screens/reader/ReaderScreen'; -import SourceScreen from '../screens/source/SourceScreen'; +import BrowseSourceScreen from '../screens/BrowseSourceScreen/BrowseSourceScreen'; import GlobalSearch from '../screens/browse/globalsearch/GlobalSearch'; import Migration from '../screens/browse/migration/Migration'; import SourceNovels from '../screens/browse/SourceNovels'; @@ -53,7 +53,7 @@ const MainNavigator = () => { - + diff --git a/src/screens/BrowseSourceScreen/BrowseSourceScreen.tsx b/src/screens/BrowseSourceScreen/BrowseSourceScreen.tsx new file mode 100644 index 00000000..8cc0d6cc --- /dev/null +++ b/src/screens/BrowseSourceScreen/BrowseSourceScreen.tsx @@ -0,0 +1,133 @@ +import React, { useCallback } from 'react'; +import { useNavigation } from '@react-navigation/native'; +import * as WebBrowser from 'expo-web-browser'; + +import { + Container, + LoadingScreenV2, + LoadingMoreIndicator, + ErrorScreenV2, + SearchbarV2, +} from '@components/index'; +import NovelList from '@components/NovelList'; +import NovelCover from '@components/NovelCover'; + +import { useSearch } from '../../hooks'; +import { useTheme } from '../../redux/hooks'; +import { useBrowseSource, useSearchSource } from './useBrowseSource'; +import { useLibrary } from '@hooks/reduxHooks'; + +import { LibraryNovelInfo } from '../../database/types'; +import { SourceNovelItem } from '../../sources/types'; +import { getString } from '@strings/translations'; + +interface BrowseSourceScreenProps { + route: { + params: { + sourceId: number; + sourceName: string; + url: string; + }; + }; +} + +const BrowseSourceScreen: React.FC = ({ route }) => { + const theme = useTheme(); + const { navigate, goBack } = useNavigation(); + + const { sourceId, sourceName, url: sourceUrl } = route.params; + + const { isLoading, novels, hasNextPage, fetchNextPage, error } = + useBrowseSource(sourceId); + + const { + isSearching, + searchResults, + searchSource, + clearSearchResults, + searchError, + } = useSearchSource(sourceId); + + const novelList = searchResults.length > 0 ? searchResults : novels; + const errorMessage = error || searchError; + + const { searchText, setSearchText, clearSearchbar } = useSearch(); + const onChangeText = (text: string) => setSearchText(text); + const onSubmitEditing = () => searchSource(searchText); + const handleClearSearchbar = () => { + clearSearchbar(); + clearSearchResults(); + }; + + const handleOpenWebView = async () => { + WebBrowser.openBrowserAsync(sourceUrl); + }; + + const library = useLibrary(); + + const novelInLibrary = (novelUrl: string) => + library?.some( + (novel: LibraryNovelInfo) => + novel.novelUrl === novelUrl && novel.sourceId === sourceId, + ); + + const navigateToNovel = useCallback( + (item: SourceNovelItem) => + navigate( + 'Novel' as never, + { + novelName: item.novelName, + novelCover: item.novelCover, + novelUrl: item.novelUrl, + sourceId, + } as never, + ), + [sourceId], + ); + + return ( + + + {isLoading || isSearching ? ( + + ) : errorMessage || novelList.length === 0 ? ( + + ) : ( + ( + navigateToNovel(item)} + /> + )} + onEndReached={() => { + if (hasNextPage && !searchText) { + fetchNextPage(); + } + }} + ListFooterComponent={ + hasNextPage && !searchText && + } + /> + )} + + ); +}; + +export default BrowseSourceScreen; diff --git a/src/screens/BrowseSourceScreen/useBrowseSource.ts b/src/screens/BrowseSourceScreen/useBrowseSource.ts new file mode 100644 index 00000000..bc008c3f --- /dev/null +++ b/src/screens/BrowseSourceScreen/useBrowseSource.ts @@ -0,0 +1,83 @@ +import { useState, useEffect, useCallback, useRef } from 'react'; +import { sourceManager } from '../../sources/sourceManager'; +import { SourceNovelItem } from '../../sources/types'; + +export const useBrowseSource = (sourceId: number) => { + const [isLoading, setIsLoading] = useState(true); + const [novels, setNovels] = useState([]); + const [error, setError] = useState(); + + const [totalPages, setTotalPages] = useState(1); + const [currentPage, setCurrentPage] = useState(1); + + const isScreenMounted = useRef(true); + + const hasNextPage = currentPage <= totalPages; + + const fetchNovels = useCallback( + async (page: number) => { + if (isScreenMounted.current === true) { + try { + const res = await sourceManager(sourceId).popularNovels(page); + setNovels(prevState => [...prevState, ...res.novels]); + setTotalPages(res.totalPages); + } catch (err: unknown) { + setError(`Error: ${err}`); + } finally { + setIsLoading(false); + } + } + }, + [sourceId], + ); + + const fetchNextPage = () => { + hasNextPage && setCurrentPage(prevState => prevState + 1); + }; + + useEffect(() => { + fetchNovels(currentPage); + }, [fetchNovels, currentPage]); + + /** + * On screen unmount + */ + useEffect(() => { + return () => { + isScreenMounted.current = false; + }; + }, []); + + return { isLoading, novels, hasNextPage, fetchNextPage, error }; +}; + +export const useSearchSource = (sourceId: number) => { + const [isSearching, setIsSearching] = useState(false); + const [searchResults, setSearchResults] = useState([]); + const [searchError, setSearchError] = useState(); + + const searchSource = useCallback( + async (searchTerm: string) => { + try { + setIsSearching(true); + const res = await sourceManager(sourceId).searchNovels(searchTerm); + setSearchResults(res); + } catch (err) { + setSearchError(`Error: ${err}`); + } finally { + setIsSearching(false); + } + }, + [sourceId], + ); + + const clearSearchResults = useCallback(() => setSearchResults([]), []); + + return { + isSearching, + searchResults, + searchSource, + clearSearchResults, + searchError, + }; +}; diff --git a/src/screens/source/SourceScreen.js b/src/screens/source/SourceScreen.js deleted file mode 100644 index ec52330c..00000000 --- a/src/screens/source/SourceScreen.js +++ /dev/null @@ -1,208 +0,0 @@ -import React, { useEffect, useRef, useState } from 'react'; -import { StyleSheet, View, ActivityIndicator } from 'react-native'; - -import * as WebBrowser from 'expo-web-browser'; -import { useDispatch } from 'react-redux'; - -import { useTheme, useLibrary, useSettings } from '../../hooks/reduxHooks'; -import { setAppSettings } from '../../redux/settings/settings.actions'; -import { sourceManager } from '../../sources/sourceManager'; -import { showToast } from '../../hooks/showToast'; - -import { Searchbar } from '../../components/Searchbar/Searchbar'; -import { LoadingScreen } from '../../components/LoadingScreen/LoadingScreen'; -import NovelCover from '../../components/NovelCover'; -import NovelList from '../../components/NovelList'; -import { ErrorView } from '../../components/ErrorView/ErrorView'; - -const SourceScreen = ({ navigation, route }) => { - const { sourceId, sourceName, url } = route.params; - - const theme = useTheme(); - const library = useLibrary(); - - const dispatch = useDispatch(); - - const settings = useSettings(); - const { displayMode } = settings; - - const [loading, setLoading] = useState(true); - const [novels, setNovels] = useState([]); - const [page, setPage] = useState(1); - const [error, setError] = useState(); - const [totalPages, setTotalPages] = useState(1); - - const [searchText, setSearchText] = useState(''); - - const incrementPage = () => setPage(prevState => prevState + 1); - - const clearSearchbar = () => { - setNovels([]); - setPage(1); - setLoading(true); - setSearchText(''); - }; - - const isMounted = useRef(true); - - const getNovels = async () => { - if (isMounted.current === true) { - try { - if (page <= totalPages) { - const source = sourceManager(sourceId); - const res = await source.popularNovels(page); - setNovels(prevState => [...prevState, ...res.novels]); - setTotalPages(res.totalPages); - setLoading(false); - } - } catch (e) { - setError(e.message); - showToast(e.message); - setNovels([]); - setLoading(false); - } - } - }; - - const getSearchResults = async () => { - try { - setLoading(true); - const source = sourceManager(sourceId); - const res = await source.searchNovels(searchText); - setNovels(res); - setLoading(false); - } catch (e) { - setError(e.message); - showToast(e.message); - setNovels([]); - setLoading(false); - } - }; - - const checkIFInLibrary = (novelSourceId, novelUrl) => - library.some( - obj => obj.novelUrl === novelUrl && obj.sourceId === novelSourceId, - ); - - useEffect(() => { - getNovels(); - - // eslint-disable-next-line react-hooks/exhaustive-deps - }, [page]); - - useEffect(() => { - return () => (isMounted.current = false); - }, []); - - const getNextPage = () => { - if (!searchText) { - incrementPage(); - } - }; - - const renderItem = ({ item }) => ( - - navigation.navigate('Novel', { - novelName: item.novelName, - novelCover: item.novelCover, - novelUrl: item.novelUrl, - sourceId, - }) - } - libraryStatus={checkIFInLibrary(item.sourceId, item.novelUrl)} - theme={theme} - /> - ); - - const listEmptyComponent = () => ( - { - getNovels(); - setLoading(true); - setError(); - }, - icon: 'reload', - }, - ]} - theme={theme} - /> - ); - - const displayMenuIcon = () => { - const icons = { - 0: 'view-module', - 1: 'view-list', - 2: 'view-module', - }; - - return icons[displayMode]; - }; - - const getNextDisplayMode = () => - displayMode === 0 ? 1 : displayMode === 1 ? 2 : displayMode === 2 && 0; - - return ( - - navigation.goBack()} - searchText={searchText} - onChangeText={text => setSearchText(text)} - onSubmitEditing={getSearchResults} - clearSearchbar={clearSearchbar} - actions={[ - { - icon: displayMenuIcon(), - onPress: () => - dispatch(setAppSettings('displayMode', getNextDisplayMode())), - }, - { - icon: 'earth', - onPress: () => WebBrowser.openBrowserAsync(url), - }, - ]} - /> - - {loading ? ( - - ) : ( - - - - ) - } - /> - )} - - ); -}; - -export default SourceScreen; - -const styles = StyleSheet.create({ - container: { - flex: 1, - }, - contentContainer: { - flex: 1, - }, -}); diff --git a/src/services/utils/constants.js b/src/services/utils/constants.js deleted file mode 100644 index f4ec8cb9..00000000 --- a/src/services/utils/constants.js +++ /dev/null @@ -1,21 +0,0 @@ -export const fonts = [ - { fontFamily: '', name: 'Original' }, - { fontFamily: 'lora', name: 'Lora' }, - { fontFamily: 'nunito', name: 'Nunito' }, - { fontFamily: 'noto-sans', name: 'Noto Sans' }, - { fontFamily: 'open-sans', name: 'Open Sans' }, - { fontFamily: 'arbutus-slab', name: 'Arbutus Slab' }, - { fontFamily: 'domine', name: 'Domine' }, - { fontFamily: 'lato', name: 'Lato' }, - { fontFamily: 'pt-serif', name: 'PT Serif' }, - { fontFamily: 'OpenDyslexic3-Regular', name: 'OpenDyslexic' }, -]; - -export const dateFormat = { - sameDay: '[Today]', - nextDay: '[Tomorrow]', - nextWeek: 'dddd', - lastDay: '[Yesterday]', - lastWeek: '[Last] dddd', - sameElse: 'DD/MM/YYYY', -}; diff --git a/strings/languages/en/strings.json b/strings/languages/en/strings.json index f322da8f..ee19b6f4 100644 --- a/strings/languages/en/strings.json +++ b/strings/languages/en/strings.json @@ -4,7 +4,8 @@ "ok": "Ok", "save": "Save", "clear": "Clear", - "reset": "Reset" + "reset": "Reset", + "search": "Search" }, "library": "Library", "libraryScreen": { @@ -114,5 +115,8 @@ "searchAllWarning": "Searching a large number of sources may freeze the app till searching is finished." } } + }, + "sourceScreen": { + "noResultsFound": "No results found" } } diff --git a/strings/types/index.ts b/strings/types/index.ts index 050493dc..ab77e392 100644 --- a/strings/types/index.ts +++ b/strings/types/index.ts @@ -8,6 +8,7 @@ export interface StringMap { 'common.save': 'string'; 'common.clear': 'string'; 'common.reset': 'string'; + 'common.search': 'string'; 'library': 'string'; 'libraryScreen.searchbar': 'string'; 'libraryScreen.empty': 'string'; @@ -84,4 +85,5 @@ export interface StringMap { 'moreScreen.settingsScreen.browseSettingsScreen.languages': 'string'; 'moreScreen.settingsScreen.browseSettingsScreen.searchAllSources': 'string'; 'moreScreen.settingsScreen.browseSettingsScreen.searchAllWarning': 'string'; + 'sourceScreen.noResultsFound': 'string'; }