refactor: Browse Source Screen
This commit is contained in:
@@ -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',
|
||||
},
|
||||
});
|
||||
@@ -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';
|
||||
|
||||
@@ -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,
|
||||
};
|
||||
};
|
||||
@@ -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,
|
||||
},
|
||||
});
|
||||
@@ -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',
|
||||
};
|
||||
@@ -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"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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';
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user