Add parallel searching in global search and migration

This commit is contained in:
Rajarshee Chatterjee
2021-10-30 09:56:44 +05:30
parent 8ec90ae8f7
commit d11348d9a3
8 changed files with 185 additions and 191 deletions
+1 -7
View File
@@ -72,13 +72,7 @@ const InfoItem = ({title, icon, theme}) => (
titleStyle={{color: theme.textColorSecondary, fontSize: 14}}
titleNumberOfLines={5}
left={() =>
icon && (
<PaperList.Icon
color={theme.textColorSecondary}
icon={icon}
style={{marginVertical: 0}}
/>
)
icon && <PaperList.Icon color={theme.textColorSecondary} icon={icon} />
}
/>
);
@@ -89,6 +89,11 @@ const BrowseSettings = ({navigation}) => {
}
/>
</Pressable>
<List.InfoItem
title="Searching a large number of sources may freeze the app till searching is finished."
icon="information-outline"
theme={theme}
/>
<List.Divider theme={theme} />
<List.SubHeader theme={theme}>Browse</List.SubHeader>
<Pressable
+52 -77
View File
@@ -1,9 +1,5 @@
import React, {useEffect, useRef, useState} from 'react';
import {
/* StyleSheet, */ View,
FlatList,
ActivityIndicator,
} from 'react-native';
import {FlatList} from 'react-native';
import {ProgressBar} from 'react-native-paper';
import {useSelector} from 'react-redux';
@@ -14,7 +10,6 @@ import EmptyView from '../../../components/EmptyView';
import {ScreenContainer} from '../../../components/Common';
import GlobalSearchSourceItem from './GlobalSearchSourceItem';
import {showToast} from '../../../hooks/showToast';
import {useLibrary, useSettings, useTheme} from '../../../hooks/reduxHooks';
import {getSource} from '../../../sources/sources';
@@ -38,7 +33,6 @@ const GlobalSearch = ({route, navigation}) => {
);
const {searchAllSources = false} = useSettings();
const [loading, setLoading] = useState(false);
const [searchText, setSearchText] = useState(novelName);
const [searchResults, setSearchResults] = useState([]);
const [progress, setProgress] = useState(0);
@@ -51,7 +45,6 @@ const GlobalSearch = ({route, navigation}) => {
if (novelName) {
onSubmitEditing();
}
// eslint-disable-next-line react-hooks/exhaustive-deps
}, []);
@@ -65,47 +58,51 @@ const GlobalSearch = ({route, navigation}) => {
const onChangeText = text => setSearchText(text);
const onSubmitEditing = async () => {
setSearchResults([]);
setProgress(0);
let globalSearchSources = searchAllSources ? sources : pinnedSources;
for (let i = 0; i < globalSearchSources.length; i++) {
if (isMounted.current === false) {
break;
setSearchResults(
globalSearchSources.map(item => ({
sourceId: item.sourceId,
sourceName: item.sourceName,
lang: item.lang,
loading: true,
novels: [],
error: null,
})),
);
globalSearchSources.map(async item => {
if (isMounted.current === true) {
try {
const source = getSource(item.sourceId);
const data = await source.searchNovels(encodeURI(searchText));
setSearchResults(prevState =>
prevState.map(sourceItem =>
sourceItem.sourceId === item.sourceId
? {...sourceItem, novels: data, loading: false}
: {...sourceItem},
),
);
} catch (e) {
setSearchResults(prevState =>
prevState.map(sourceItem =>
sourceItem.sourceId === item.sourceId
? {
...sourceItem,
loading: false,
error: e.message,
}
: sourceItem,
),
);
}
setProgress(before => before + 1 / globalSearchSources.length);
}
try {
setLoading(true);
const source = getSource(globalSearchSources[i].sourceId);
const data = await source.searchNovels(encodeURI(searchText));
setSearchResults(before => [
...before,
{
sourceId: globalSearchSources[i].sourceId,
sourceName: globalSearchSources[i].sourceName,
lang: globalSearchSources[i].lang,
novels: data,
},
]);
setLoading(false);
} catch (error) {
showToast(globalSearchSources[i].sourceName + ': ' + error.message);
setSearchResults(before => [
...before,
{
sourceId: globalSearchSources[i].sourceId,
sourceName: globalSearchSources[i].sourceName,
lang: globalSearchSources[i].lang,
novels: [],
},
]);
setLoading(false);
}
setProgress(before => before + 1 / globalSearchSources.length);
}
});
};
const renderItem = ({item}) => (
@@ -133,42 +130,22 @@ const GlobalSearch = ({route, navigation}) => {
<ProgressBar color={theme.colorAccent} progress={progress} />
)}
<FlatList
contentContainerStyle={{
flexGrow: 1,
padding: 4,
}}
contentContainerStyle={{flexGrow: 1, padding: 4}}
data={searchResults}
keyExtractor={item => item.sourceId.toString()}
renderItem={renderItem}
extraData={pinned}
ListEmptyComponent={
<>
{!loading && (
<EmptyView
icon="__φ(..)"
description={`Search a novel in ${
searchAllSources
? 'all sources'
: pinned.length === 0
? 'pinned sources\n(No sources pinned)'
: 'pinned sources'
}`}
/>
)}
</>
}
ListFooterComponent={
loading && (
<View
style={{
flex: 1,
justifyContent: 'center',
padding: 16,
}}
>
<ActivityIndicator size="large" color={theme.colorAccent} />
</View>
)
<EmptyView
icon="__φ(..)"
description={`Search a novel in ${
searchAllSources
? 'all sources'
: pinned.length === 0
? 'pinned sources\n(No sources pinned)'
: 'pinned sources'
}`}
/>
}
/>
</ScreenContainer>
@@ -176,5 +153,3 @@ const GlobalSearch = ({route, navigation}) => {
};
export default GlobalSearch;
// const styles = StyleSheet.create({});
@@ -1,5 +1,6 @@
import React from 'react';
import {StyleSheet, View, Text, Image, Pressable} from 'react-native';
import {defaultCoverUri} from '../../../sources/helpers/constants';
const GlobalSearchNovelCover = ({
novel,
@@ -10,32 +11,28 @@ const GlobalSearchNovelCover = ({
}) => {
const {novelName, novelCover} = novel;
const uri =
novelCover && !novelCover.startsWith('/') ? novelCover : defaultCoverUri;
return (
<View style={{flex: 1, borderRadius: 6, overflow: 'hidden'}}>
<View style={styles.container}>
<Pressable
android_ripple={{color: theme.rippleColor}}
style={styles.novelContainer}
style={styles.pressable}
onPress={onPress}
onLongPress={onLongPress}
>
<>
<Image
source={{
uri:
novelCover && !novelCover.startsWith('/')
? novelCover
: 'https://github.com/LNReader/lnreader-sources/blob/main/src/coverNotAvailable.jpg?raw=true',
}}
style={[styles.novelCover, inLibrary && {opacity: 0.5}]}
progressiveRenderingEnabled={true}
/>
<Text
numberOfLines={2}
style={[styles.title, {color: theme.textColorPrimary}]}
>
{novelName}
</Text>
</>
<Image
source={{uri}}
style={[styles.novelCover, inLibrary && {opacity: 0.5}]}
progressiveRenderingEnabled={true}
/>
<Text
numberOfLines={2}
style={[styles.title, {color: theme.textColorPrimary}]}
>
{novelName}
</Text>
</Pressable>
</View>
);
@@ -44,10 +41,16 @@ const GlobalSearchNovelCover = ({
export default GlobalSearchNovelCover;
const styles = StyleSheet.create({
novelContainer: {
container: {
flex: 1,
borderRadius: 6,
overflow: 'hidden',
},
pressable: {
paddingHorizontal: 5,
paddingVertical: 4,
borderRadius: 4,
flex: 1,
},
novelCover: {
height: 150,
@@ -29,7 +29,7 @@ const GlobalSearchNovelList = ({data, theme, library, navigation}) => {
contentContainerStyle={styles.listContainer}
horizontal={true}
data={data}
keyExtractor={item => item.novelUrl}
keyExtractor={item => item.sourceId + item.novelUrl}
renderItem={renderItem}
ListEmptyComponent={ListEmptyComponent}
/>
@@ -1,10 +1,14 @@
import React from 'react';
import {StyleSheet, View, Text} from 'react-native';
import {StyleSheet, View, Text, ActivityIndicator} from 'react-native';
import GlobalSearchNovelList from './GlobalSearchNovelList';
const GlobalSearchSourceItem = ({source, library, theme, navigation}) => {
const {sourceName, lang, novels} = source;
const {sourceName, lang, loading, novels, error} = source;
const colorError = {
color: theme.statusBar === 'dark-content' ? '#B3261E' : '#F2B8B5',
};
return (
<>
@@ -14,12 +18,21 @@ const GlobalSearchSourceItem = ({source, library, theme, navigation}) => {
{lang}
</Text>
</View>
<GlobalSearchNovelList
data={novels}
theme={theme}
library={library}
navigation={navigation}
/>
{error ? (
<Text style={[styles.error, colorError]}>{error}</Text>
) : loading ? (
<ActivityIndicator
color={theme.colorAccent}
style={{marginVertical: 16}}
/>
) : (
<GlobalSearchNovelList
data={novels}
theme={theme}
library={library}
navigation={navigation}
/>
)}
</>
);
};
@@ -31,4 +44,8 @@ const styles = StyleSheet.create({
padding: 8,
paddingVertical: 16,
},
error: {
paddingHorizontal: 8,
paddingVertical: 16,
},
});
@@ -48,7 +48,7 @@ const MigrationNovelList = ({data, theme, library, navigation}) => {
contentContainerStyle={styles.flatListCont}
horizontal={true}
data={data}
keyExtractor={item => item.novelUrl}
keyExtractor={item => item.sourceId + item.novelUrl}
renderItem={renderItem}
ListEmptyComponent={
<Text
+76 -76
View File
@@ -1,10 +1,10 @@
import React, {useEffect, useState} from 'react';
import {
/* StyleSheet, */
View,
Text,
FlatList,
ActivityIndicator,
StyleSheet,
} from 'react-native';
import {ProgressBar} from 'react-native-paper';
import {useLibrary, useSettings, useTheme} from '../../../hooks/reduxHooks';
@@ -14,7 +14,6 @@ import EmptyView from '../../../components/EmptyView';
import MigrationNovelList from './MigrationNovelList';
import {Appbar} from '../../../components/Appbar';
import {showToast} from '../../../hooks/showToast';
import {ScreenContainer} from '../../../components/Common';
import {getSource} from '../../../sources/sources';
@@ -25,7 +24,6 @@ const MigrationNovels = ({navigation, route}) => {
const isMounted = React.useRef(true);
const [progress, setProgress] = useState(0);
const [loading, setLoading] = useState(false);
const [searchResults, setSearchResults] = useState('');
const {searchAllSources = false} = useSettings();
let {
@@ -44,48 +42,52 @@ const MigrationNovels = ({navigation, route}) => {
const getSearchResults = async () => {
let migrationSources = searchAllSources ? sources : pinnedSources;
for (let i = 0; i < migrationSources.length; i++) {
if (isMounted.current === false) {
break;
setSearchResults(
migrationSources.map(item => ({
sourceId: item.sourceId,
sourceName: item.sourceName,
lang: item.lang,
loading: true,
novels: [],
error: null,
})),
);
migrationSources.map(async item => {
if (isMounted.current === true) {
try {
const source = getSource(item.sourceId);
const data = await source.searchNovels(novelName);
setSearchResults(prevState =>
prevState.map(sourceItem =>
sourceItem.sourceId === item.sourceId
? {...sourceItem, novels: data, loading: false}
: {...sourceItem},
),
);
} catch (e) {
setSearchResults(prevState =>
prevState.map(sourceItem =>
sourceItem.sourceId === item.sourceId
? {
...sourceItem,
loading: false,
error: e.message,
}
: sourceItem,
),
);
}
setProgress(before => before + 1 / migrationSources.length);
}
try {
setLoading(true);
const source = getSource(migrationSources[i].sourceId);
const data = await source.searchNovels(novelName);
setSearchResults(before => [
...before,
{
sourceId: migrationSources[i].sourceId,
sourceName: migrationSources[i].sourceName,
sourceLanguage: migrationSources[i].lang,
novels: data,
},
]);
setLoading(false);
} catch (error) {
showToast(error.message);
setSearchResults(before => [
...before,
{
sourceId: migrationSources[i].sourceId,
sourceName: migrationSources[i].sourceName,
sourceLanguage: migrationSources[i].lang,
novels: [],
},
]);
setLoading(false);
}
setProgress(before => before + 1 / migrationSources.length);
}
});
};
useEffect(() => {
getSearchResults();
// eslint-disable-next-line react-hooks/exhaustive-deps
}, []);
@@ -95,62 +97,55 @@ const MigrationNovels = ({navigation, route}) => {
};
}, []);
const colorError = {
color: theme.statusBar === 'dark-content' ? '#B3261E' : '#F2B8B5',
};
const renderItem = ({item}) => (
<>
<View style={{padding: 8, paddingVertical: 16}}>
<Text style={{color: theme.textColorPrimary}}>{item.sourceName}</Text>
<Text style={{color: theme.textColorSecondary, fontSize: 12}}>
{item.sourceLanguage}
{item.lang}
</Text>
</View>
<MigrationNovelList
data={item.novels}
theme={theme}
library={library}
navigation={navigation}
/>
{item.error ? (
<Text style={[styles.error, colorError]}>{item.error}</Text>
) : item.loading ? (
<ActivityIndicator
color={theme.colorAccent}
style={{marginVertical: 16}}
/>
) : (
<MigrationNovelList
data={item.novels}
theme={theme}
library={library}
navigation={navigation}
/>
)}
</>
);
return (
<ScreenContainer theme={theme}>
<Appbar title={novelName} onBackAction={() => navigation.goBack()} />
<Appbar title={novelName} onBackAction={navigation.goBack} />
{progress > 0 && (
<ProgressBar color={theme.colorAccent} progress={progress} />
)}
<FlatList
contentContainerStyle={{
flexGrow: 1,
padding: 4,
}}
contentContainerStyle={{flexGrow: 1, padding: 4}}
data={searchResults}
keyExtractor={item => item.sourceId.toString()}
renderItem={renderItem}
extraData={pinned}
ListEmptyComponent={
<>
{!loading && (
<EmptyView
icon="__φ(..)"
description={`Search a novel in your pinned sources ${
pinned.length === 0 ? '(No sources pinned)' : ''
}`}
/>
)}
</>
}
ListFooterComponent={
loading && (
<View
style={{
flex: 1,
justifyContent: 'center',
padding: 16,
}}
>
<ActivityIndicator size="large" color={theme.colorAccent} />
</View>
)
<EmptyView
icon="__φ(..)"
description={`Search a novel in your pinned sources ${
pinned.length === 0 ? '(No sources pinned)' : ''
}`}
/>
}
/>
</ScreenContainer>
@@ -159,4 +154,9 @@ const MigrationNovels = ({navigation, route}) => {
export default MigrationNovels;
// const styles = StyleSheet.create({});
const styles = StyleSheet.create({
error: {
paddingHorizontal: 8,
paddingVertical: 16,
},
});