refactor: Browse Source Screen

This commit is contained in:
Rajarshee Chatterjee
2022-06-06 07:58:07 +05:30
parent df851bf9d1
commit 9b297620fe
9 changed files with 248 additions and 232 deletions
@@ -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<Props> = ({ theme }) => (
<ActivityIndicator color={theme.colorAccent} style={styles.indicator} />
);
export default LoadingMoreIndicator;
const styles = StyleSheet.create({
indicator: {
padding: 32,
flex: 1,
justifyContent: 'center',
alignItems: 'center',
},
});
+1
View File
@@ -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';
+2 -2
View File
@@ -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 = () => {
<Stack.Screen name="Novel" component={Novel} />
<Stack.Screen name="Chapter" component={Reader} />
<Stack.Screen name="MoreStack" component={MoreStack} />
<Stack.Screen name="SourceScreen" component={SourceScreen} />
<Stack.Screen name="SourceScreen" component={BrowseSourceScreen} />
<Stack.Screen name="BrowseMal" component={MalTopNovels} />
<Stack.Screen name="BrowseSettings" component={BrowseSettings} />
<Stack.Screen name="GlobalSearch" component={GlobalSearch} />
@@ -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<BrowseSourceScreenProps> = ({ 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 (
<Container>
<SearchbarV2
searchText={searchText}
leftIcon="magnify"
placeholder={`${getString('common.search')} ${sourceName}`}
onChangeText={onChangeText}
onSubmitEditing={onSubmitEditing}
clearSearchbar={handleClearSearchbar}
handleBackAction={goBack}
rightIcons={[{ iconName: 'earth', onPress: handleOpenWebView }]}
theme={theme}
/>
{isLoading || isSearching ? (
<LoadingScreenV2 theme={theme} />
) : errorMessage || novelList.length === 0 ? (
<ErrorScreenV2
error={errorMessage || getString('sourceScreen.noResultsFound')}
theme={theme}
/>
) : (
<NovelList
data={novelList}
renderItem={({ item }) => (
<NovelCover
item={item}
theme={theme}
libraryStatus={novelInLibrary(item.novelUrl)}
onPress={() => navigateToNovel(item)}
/>
)}
onEndReached={() => {
if (hasNextPage && !searchText) {
fetchNextPage();
}
}}
ListFooterComponent={
hasNextPage && !searchText && <LoadingMoreIndicator theme={theme} />
}
/>
)}
</Container>
);
};
export default BrowseSourceScreen;
@@ -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<SourceNovelItem[]>([]);
const [error, setError] = useState<string>();
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<SourceNovelItem[]>([]);
const [searchError, setSearchError] = useState<string>();
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,
};
};
-208
View File
@@ -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 }) => (
<NovelCover
item={item}
onPress={() =>
navigation.navigate('Novel', {
novelName: item.novelName,
novelCover: item.novelCover,
novelUrl: item.novelUrl,
sourceId,
})
}
libraryStatus={checkIFInLibrary(item.sourceId, item.novelUrl)}
theme={theme}
/>
);
const listEmptyComponent = () => (
<ErrorView
errorName={error || 'No results found'}
actions={[
{
name: 'Retry',
onPress: () => {
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 (
<View
style={[styles.container, { backgroundColor: theme.colorPrimaryDark }]}
>
<Searchbar
theme={theme}
placeholder={`Search ${sourceName}`}
backAction="arrow-left"
onBackAction={() => 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 ? (
<LoadingScreen theme={theme} />
) : (
<NovelList
data={novels}
renderItem={renderItem}
ListEmptyComponent={listEmptyComponent()}
onEndReached={getNextPage}
ListFooterComponent={
!searchText &&
page < totalPages &&
novels.length && (
<View style={{ paddingVertical: 16 }}>
<ActivityIndicator color={theme.colorAccent} />
</View>
)
}
/>
)}
</View>
);
};
export default SourceScreen;
const styles = StyleSheet.create({
container: {
flex: 1,
},
contentContainer: {
flex: 1,
},
});
-21
View File
@@ -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',
};
+5 -1
View File
@@ -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"
}
}
+2
View File
@@ -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';
}