Add parallel searching in global search and migration
This commit is contained in:
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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,
|
||||
},
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user