@@ -254,7 +254,7 @@ try {
|
||||
|
||||
return novels;`
|
||||
: `
|
||||
showToast('Search is not available in this source')
|
||||
showToast('${source.sourceName}: Search is not available in this source')
|
||||
|
||||
return [];
|
||||
`
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import { useCallback, useState } from 'react';
|
||||
|
||||
const useSearch = () => {
|
||||
const [searchText, setSearchText] = useState<string>('');
|
||||
const useSearch = (defaultSearchText?: string) => {
|
||||
const [searchText, setSearchText] = useState<string>(defaultSearchText || '');
|
||||
const clearSearchbar = useCallback(() => setSearchText(''), []);
|
||||
|
||||
return { searchText, setSearchText, clearSearchbar };
|
||||
|
||||
+10
-3
@@ -20,7 +20,7 @@ import MoreStack from './MoreStack';
|
||||
import Novel from '../screens/novel/NovelScreen';
|
||||
import Reader from '../screens/reader/ReaderScreen';
|
||||
import BrowseSourceScreen from '../screens/BrowseSourceScreen/BrowseSourceScreen';
|
||||
import GlobalSearch from '../screens/browse/globalsearch/GlobalSearch';
|
||||
import GlobalSearchScreen from '../screens/GlobalSearchScreen/GlobalSearchScreen';
|
||||
import Migration from '../screens/browse/migration/Migration';
|
||||
import SourceNovels from '../screens/browse/SourceNovels';
|
||||
import MigrateNovel from '../screens/browse/migration/MigrationNovels';
|
||||
@@ -35,10 +35,14 @@ const MainNavigator = () => {
|
||||
const theme = useTheme();
|
||||
|
||||
useEffect(() => {
|
||||
setTimeout(async () => {
|
||||
const timer = setTimeout(async () => {
|
||||
await SplashScreen.hideAsync();
|
||||
setBarColor(theme);
|
||||
}, 500);
|
||||
|
||||
return () => {
|
||||
clearTimeout(timer);
|
||||
};
|
||||
}, [theme]);
|
||||
|
||||
const { isNewVersion, latestRelease } = useGithubUpdateChecker() || {};
|
||||
@@ -56,7 +60,10 @@ const MainNavigator = () => {
|
||||
<Stack.Screen name="SourceScreen" component={BrowseSourceScreen} />
|
||||
<Stack.Screen name="BrowseMal" component={MalTopNovels} />
|
||||
<Stack.Screen name="BrowseSettings" component={BrowseSettings} />
|
||||
<Stack.Screen name="GlobalSearch" component={GlobalSearch} />
|
||||
<Stack.Screen
|
||||
name="GlobalSearchScreen"
|
||||
component={GlobalSearchScreen}
|
||||
/>
|
||||
<Stack.Screen name="Migration" component={Migration} />
|
||||
<Stack.Screen name="SourceNovels" component={SourceNovels} />
|
||||
<Stack.Screen name="MigrateNovel" component={MigrateNovel} />
|
||||
|
||||
@@ -0,0 +1,66 @@
|
||||
import React from 'react';
|
||||
|
||||
import { ProgressBar } from 'react-native-paper';
|
||||
|
||||
import { EmptyView, SearchbarV2 } from '@components/index';
|
||||
import GlobalSearchResultsList from './components/GlobalSearchResultsList';
|
||||
|
||||
import useSearch from '@hooks/useSearch';
|
||||
import { useTheme } from '@redux/hooks';
|
||||
import { getString } from '@strings/translations';
|
||||
import { useGlobalSearch } from './hooks/useGlobalSearch';
|
||||
|
||||
interface Props {
|
||||
route?: {
|
||||
params?: {
|
||||
searchText?: string;
|
||||
};
|
||||
};
|
||||
}
|
||||
|
||||
const GlobalSearchScreen = (props: Props) => {
|
||||
const theme = useTheme();
|
||||
const { searchText, setSearchText, clearSearchbar } = useSearch(
|
||||
props?.route?.params?.searchText,
|
||||
);
|
||||
const onChangeText = (text: string) => setSearchText(text);
|
||||
const onSubmitEditing = () => globalSearch(searchText);
|
||||
|
||||
const { searchResults, globalSearch, searchAllSources, progress } =
|
||||
useGlobalSearch({
|
||||
defaultSearchText: searchText,
|
||||
});
|
||||
|
||||
return (
|
||||
<>
|
||||
<SearchbarV2
|
||||
searchText={searchText}
|
||||
placeholder={getString('browseScreen.globalSearch')}
|
||||
leftIcon="magnify"
|
||||
onChangeText={onChangeText}
|
||||
onSubmitEditing={onSubmitEditing}
|
||||
clearSearchbar={clearSearchbar}
|
||||
theme={theme}
|
||||
/>
|
||||
{progress ? (
|
||||
<ProgressBar color={theme.colorAccent} progress={progress} />
|
||||
) : null}
|
||||
<GlobalSearchResultsList
|
||||
searchResults={searchResults}
|
||||
ListEmptyComponent={
|
||||
<EmptyView
|
||||
icon="__φ(..)"
|
||||
description={`${getString('globalSearch.searchIn')} ${getString(
|
||||
searchAllSources
|
||||
? 'globalSearch.allSources'
|
||||
: 'globalSearch.pinnedSources',
|
||||
)}`}
|
||||
theme={theme}
|
||||
/>
|
||||
}
|
||||
/>
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
export default GlobalSearchScreen;
|
||||
@@ -0,0 +1,78 @@
|
||||
import { Dimensions, Pressable, StyleSheet, Text, View } from 'react-native';
|
||||
import React, { useMemo } from 'react';
|
||||
|
||||
import FastImage from 'react-native-fast-image';
|
||||
|
||||
import { coverPlaceholderColor } from '../../../theme/colors';
|
||||
|
||||
import { SourceNovelItem } from '../../../sources/types';
|
||||
import { ThemeType } from '../../../theme/types';
|
||||
|
||||
interface Props {
|
||||
novel: SourceNovelItem;
|
||||
navigateToNovel: (novel: SourceNovelItem) => void;
|
||||
theme: ThemeType;
|
||||
}
|
||||
|
||||
const GlobalSearchNovelItem: React.FC<Props> = ({
|
||||
novel,
|
||||
navigateToNovel,
|
||||
theme,
|
||||
}) => {
|
||||
const novelItemDimensions = useMemo(
|
||||
() => ({
|
||||
width: Dimensions.get('window').width / 3 - 16,
|
||||
height: ((Dimensions.get('window').width / 3 - 16) * 4) / 3,
|
||||
}),
|
||||
[],
|
||||
);
|
||||
|
||||
return (
|
||||
<View style={styles.novelItem}>
|
||||
<Pressable
|
||||
style={styles.pressable}
|
||||
android_ripple={{ color: theme.rippleColor }}
|
||||
onPress={() => navigateToNovel(novel)}
|
||||
>
|
||||
<FastImage
|
||||
source={{ uri: novel.novelCover }}
|
||||
style={[styles.novelCover, { ...novelItemDimensions }]}
|
||||
/>
|
||||
<Text
|
||||
style={[
|
||||
{ color: theme.textColorPrimary, width: novelItemDimensions.width },
|
||||
styles.novelName,
|
||||
]}
|
||||
numberOfLines={2}
|
||||
>
|
||||
{novel.novelName}
|
||||
</Text>
|
||||
</Pressable>
|
||||
</View>
|
||||
);
|
||||
};
|
||||
|
||||
export default GlobalSearchNovelItem;
|
||||
|
||||
const styles = StyleSheet.create({
|
||||
novelItem: {
|
||||
flex: 1,
|
||||
borderRadius: 6,
|
||||
overflow: 'hidden',
|
||||
},
|
||||
pressable: {
|
||||
padding: 4,
|
||||
flex: 1,
|
||||
},
|
||||
novelName: {
|
||||
paddingHorizontal: 4,
|
||||
paddingVertical: 8,
|
||||
flexWrap: 'wrap',
|
||||
fontSize: 12,
|
||||
fontWeight: 'bold',
|
||||
},
|
||||
novelCover: {
|
||||
borderRadius: 4,
|
||||
backgroundColor: coverPlaceholderColor,
|
||||
},
|
||||
});
|
||||
@@ -0,0 +1,165 @@
|
||||
import {
|
||||
ActivityIndicator,
|
||||
FlatList,
|
||||
Pressable,
|
||||
StyleSheet,
|
||||
Text,
|
||||
View,
|
||||
} from 'react-native';
|
||||
import React, { useCallback, useMemo } from 'react';
|
||||
|
||||
import { StackNavigationProp } from '@react-navigation/stack';
|
||||
import { useNavigation } from '@react-navigation/native';
|
||||
import MaterialCommunityIcons from 'react-native-vector-icons/MaterialCommunityIcons';
|
||||
|
||||
import { getString } from '@strings/translations';
|
||||
import { useTheme } from '@redux/hooks';
|
||||
import { GlobalSearchResult } from '../hooks/useGlobalSearch';
|
||||
import GlobalSearchNovelItem from './GlobalSearchNovelItem';
|
||||
|
||||
interface GlobalSearchResultsListProps {
|
||||
searchResults: GlobalSearchResult[];
|
||||
ListEmptyComponent?: JSX.Element;
|
||||
}
|
||||
|
||||
const GlobalSearchResultsList: React.FC<GlobalSearchResultsListProps> = ({
|
||||
searchResults,
|
||||
ListEmptyComponent,
|
||||
}) => {
|
||||
const theme = useTheme();
|
||||
const navigation = useNavigation<StackNavigationProp<any>>();
|
||||
const keyExtractor = useCallback(item => item.source.sourceId.toString(), []);
|
||||
|
||||
const errorColor = useMemo(
|
||||
() => (theme.statusBar === 'dark-content' ? '#B3261E' : '#F2B8B5'),
|
||||
[],
|
||||
);
|
||||
|
||||
const navigateToNovel = useCallback(
|
||||
item => navigation.push('Novel', item),
|
||||
[],
|
||||
);
|
||||
|
||||
return (
|
||||
<FlatList<GlobalSearchResult>
|
||||
keyExtractor={keyExtractor}
|
||||
data={searchResults}
|
||||
contentContainerStyle={styles.resultList}
|
||||
renderItem={({ item }) => (
|
||||
<>
|
||||
<View>
|
||||
<Pressable
|
||||
android_ripple={{ color: theme.rippleColor }}
|
||||
style={styles.sourceHeader}
|
||||
onPress={() =>
|
||||
navigation.navigate('SourceScreen', {
|
||||
sourceId: item.source.sourceId,
|
||||
sourceName: item.source.sourceName,
|
||||
url: item.source.url,
|
||||
})
|
||||
}
|
||||
>
|
||||
<View>
|
||||
<Text
|
||||
style={[styles.sourceName, { color: theme.textColorPrimary }]}
|
||||
>
|
||||
{item.source.sourceName}
|
||||
</Text>
|
||||
<Text
|
||||
style={[styles.language, { color: theme.textColorSecondary }]}
|
||||
>
|
||||
{item.source.lang}
|
||||
</Text>
|
||||
</View>
|
||||
<MaterialCommunityIcons
|
||||
name="arrow-right"
|
||||
size={24}
|
||||
color={theme.textColorPrimary}
|
||||
/>
|
||||
</Pressable>
|
||||
{item.isLoading ? (
|
||||
<ActivityIndicator
|
||||
color={theme.colorAccent}
|
||||
style={styles.loading}
|
||||
/>
|
||||
) : item.error ? (
|
||||
<Text style={[styles.error, { color: errorColor }]}>
|
||||
{item.error}
|
||||
</Text>
|
||||
) : (
|
||||
<FlatList
|
||||
horizontal
|
||||
contentContainerStyle={styles.novelsContainer}
|
||||
keyExtractor={novelItem =>
|
||||
novelItem.novelUrl + novelItem.sourceId
|
||||
}
|
||||
data={item.novels}
|
||||
ListEmptyComponent={
|
||||
<Text
|
||||
style={[
|
||||
styles.listEmpty,
|
||||
{ color: theme.textColorSecondary },
|
||||
]}
|
||||
>
|
||||
{getString('sourceScreen.noResultsFound')}
|
||||
</Text>
|
||||
}
|
||||
renderItem={({ item: novelItem }) => (
|
||||
<GlobalSearchNovelItem
|
||||
novel={novelItem}
|
||||
navigateToNovel={navigateToNovel}
|
||||
theme={theme}
|
||||
/>
|
||||
)}
|
||||
/>
|
||||
)}
|
||||
</View>
|
||||
</>
|
||||
)}
|
||||
ListEmptyComponent={ListEmptyComponent}
|
||||
/>
|
||||
);
|
||||
};
|
||||
|
||||
export default GlobalSearchResultsList;
|
||||
|
||||
const styles = StyleSheet.create({
|
||||
resultList: {
|
||||
flexGrow: 1,
|
||||
paddingTop: 8,
|
||||
paddingBottom: 60,
|
||||
},
|
||||
loading: {
|
||||
flex: 1,
|
||||
justifyContent: 'center',
|
||||
alignItems: 'center',
|
||||
paddingVertical: 16,
|
||||
},
|
||||
sourceHeader: {
|
||||
flexDirection: 'row',
|
||||
justifyContent: 'space-between',
|
||||
alignItems: 'center',
|
||||
paddingRight: 8,
|
||||
},
|
||||
sourceName: {
|
||||
marginTop: 8,
|
||||
marginBottom: 4,
|
||||
paddingHorizontal: 16,
|
||||
},
|
||||
language: {
|
||||
fontSize: 12,
|
||||
marginBottom: 8,
|
||||
paddingHorizontal: 16,
|
||||
},
|
||||
error: {
|
||||
padding: 16,
|
||||
marginBottom: 16,
|
||||
},
|
||||
novelsContainer: {
|
||||
padding: 8,
|
||||
},
|
||||
listEmpty: {
|
||||
paddingHorizontal: 8,
|
||||
marginBottom: 16,
|
||||
},
|
||||
});
|
||||
@@ -0,0 +1,108 @@
|
||||
import { useCallback, useEffect, useMemo, useRef, useState } from 'react';
|
||||
|
||||
import { useBrowseSettings, useSourcesReducer } from '@redux/hooks';
|
||||
import { Source, SourceNovelItem } from '../../../sources/types';
|
||||
import { sourceManager } from '../../../sources/sourceManager';
|
||||
|
||||
interface Props {
|
||||
defaultSearchText?: string;
|
||||
}
|
||||
|
||||
export interface GlobalSearchResult {
|
||||
isLoading: boolean;
|
||||
source: Source;
|
||||
novels: SourceNovelItem[];
|
||||
error?: string | null;
|
||||
}
|
||||
|
||||
export const useGlobalSearch = ({ defaultSearchText }: Props) => {
|
||||
const isMounted = useRef(true);
|
||||
const { searchAllSources = false } = useBrowseSettings();
|
||||
|
||||
const { allSources, pinnedSourceIds = [] } = useSourcesReducer();
|
||||
|
||||
const isPinned = useCallback(
|
||||
(sourceId: number) => pinnedSourceIds.indexOf(sourceId) > -1,
|
||||
[pinnedSourceIds],
|
||||
);
|
||||
|
||||
const pinnedSources = useMemo(
|
||||
() => allSources.filter(source => isPinned(source.sourceId)),
|
||||
[allSources, isPinned],
|
||||
);
|
||||
|
||||
const [searchResults, setSearchResults] = useState<GlobalSearchResult[]>([]);
|
||||
const [progress, setProgress] = useState(0);
|
||||
|
||||
const globalSearch = (searchText: string) => {
|
||||
const sources = searchAllSources ? allSources : pinnedSources;
|
||||
|
||||
const defaultResult: GlobalSearchResult[] = sources.map(source => ({
|
||||
isLoading: true,
|
||||
source,
|
||||
novels: [],
|
||||
error: null,
|
||||
}));
|
||||
|
||||
setSearchResults(defaultResult);
|
||||
|
||||
sources.forEach(async source => {
|
||||
if (isMounted.current) {
|
||||
try {
|
||||
const res = await sourceManager(source.sourceId).searchNovels(
|
||||
searchText,
|
||||
);
|
||||
|
||||
setSearchResults(prevState =>
|
||||
prevState.map(prevResult =>
|
||||
prevResult.source.sourceId === source.sourceId
|
||||
? { ...prevResult, novels: res, isLoading: false }
|
||||
: { ...prevResult },
|
||||
),
|
||||
);
|
||||
|
||||
setSearchResults(prevState =>
|
||||
prevState.sort(
|
||||
(
|
||||
{ novels: a, source: { sourceName: aName } },
|
||||
{ novels: b, source: { sourceName: bName } },
|
||||
) => {
|
||||
if (!a.length) {
|
||||
return 1;
|
||||
}
|
||||
if (!b.length) {
|
||||
return -1;
|
||||
}
|
||||
|
||||
return aName.localeCompare(bName);
|
||||
},
|
||||
),
|
||||
);
|
||||
} catch (error) {
|
||||
setSearchResults(prevState =>
|
||||
prevState.map(prevResult =>
|
||||
prevResult.source.sourceId === source.sourceId
|
||||
? {
|
||||
...prevResult,
|
||||
novels: [],
|
||||
isLoading: false,
|
||||
error: error.message,
|
||||
}
|
||||
: { ...prevResult },
|
||||
),
|
||||
);
|
||||
} finally {
|
||||
setProgress(prevState => prevState + 1 / sources.length);
|
||||
}
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
if (defaultSearchText) {
|
||||
globalSearch(defaultSearchText);
|
||||
}
|
||||
}, []);
|
||||
|
||||
return { searchResults, globalSearch, searchAllSources, progress };
|
||||
};
|
||||
@@ -79,7 +79,7 @@ const BrowseScreen = () => {
|
||||
() => [
|
||||
{
|
||||
iconName: 'book-search',
|
||||
onPress: () => navigate('GlobalSearch' as never),
|
||||
onPress: () => navigate('GlobalSearchScreen' as never),
|
||||
},
|
||||
{
|
||||
iconName: 'swap-vertical-variant',
|
||||
|
||||
@@ -69,7 +69,7 @@ const BrowseMalScreen = ({ navigation, route }) => {
|
||||
novel={item}
|
||||
theme={theme}
|
||||
onPress={() =>
|
||||
navigation.navigate('GlobalSearch', {
|
||||
navigation.navigate('GlobalSearchScreen', {
|
||||
novelName: item.novelName,
|
||||
})
|
||||
}
|
||||
|
||||
@@ -1,149 +0,0 @@
|
||||
import React, { useEffect, useRef, useState } from 'react';
|
||||
import { FlatList } from 'react-native';
|
||||
|
||||
import { ProgressBar } from 'react-native-paper';
|
||||
|
||||
import { Searchbar } from '../../../components/Searchbar/Searchbar';
|
||||
import EmptyView from '../../../components/EmptyView';
|
||||
|
||||
import { ScreenContainer } from '../../../components/Common';
|
||||
import GlobalSearchSourceItem from './GlobalSearchSourceItem';
|
||||
|
||||
import { useLibrary, useTheme } from '../../../hooks/reduxHooks';
|
||||
import { sourceManager } from '../../../sources/sourceManager';
|
||||
import { useBrowseSettings, useSourcesReducer } from '../../../redux/hooks';
|
||||
|
||||
const GlobalSearch = ({ route, navigation }) => {
|
||||
const theme = useTheme();
|
||||
|
||||
let novelName = '';
|
||||
if (route.params) {
|
||||
novelName = route.params.novelName;
|
||||
}
|
||||
const { allSources, pinnedSourceIds = [] } = useSourcesReducer();
|
||||
|
||||
const isPinned = sourceId => pinnedSourceIds.indexOf(sourceId) > -1;
|
||||
const pinnedSources = allSources.filter(source => isPinned(source.sourceId));
|
||||
|
||||
const { searchAllSources = false } = useBrowseSettings();
|
||||
|
||||
const [searchText, setSearchText] = useState(novelName);
|
||||
const [searchResults, setSearchResults] = useState([]);
|
||||
const [progress, setProgress] = useState(0);
|
||||
|
||||
const library = useLibrary();
|
||||
|
||||
const isMounted = useRef(true);
|
||||
|
||||
useEffect(() => {
|
||||
if (novelName) {
|
||||
onSubmitEditing();
|
||||
}
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
}, []);
|
||||
|
||||
useEffect(() => {
|
||||
return () => {
|
||||
isMounted.current = false;
|
||||
};
|
||||
}, []);
|
||||
|
||||
const clearSearchbar = () => setSearchText('');
|
||||
const onChangeText = text => setSearchText(text);
|
||||
|
||||
const onSubmitEditing = async () => {
|
||||
setProgress(0);
|
||||
|
||||
let globalSearchSources = searchAllSources ? allSources : pinnedSources;
|
||||
|
||||
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 = sourceManager(item.sourceId);
|
||||
const data = await source.searchNovels(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);
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
const renderItem = ({ item }) => (
|
||||
<GlobalSearchSourceItem
|
||||
source={item}
|
||||
library={library}
|
||||
theme={theme}
|
||||
navigation={navigation}
|
||||
/>
|
||||
);
|
||||
|
||||
return (
|
||||
<ScreenContainer theme={theme}>
|
||||
<Searchbar
|
||||
theme={theme}
|
||||
placeholder="Global Search"
|
||||
backAction="arrow-left"
|
||||
onBackAction={navigation.goBack}
|
||||
searchText={searchText}
|
||||
onChangeText={onChangeText}
|
||||
onSubmitEditing={onSubmitEditing}
|
||||
clearSearchbar={clearSearchbar}
|
||||
/>
|
||||
{progress > 0 && (
|
||||
<ProgressBar color={theme.colorAccent} progress={progress} />
|
||||
)}
|
||||
<FlatList
|
||||
contentContainerStyle={{ flexGrow: 1, padding: 4 }}
|
||||
data={searchResults}
|
||||
keyExtractor={item => item.sourceId.toString()}
|
||||
renderItem={renderItem}
|
||||
extraData={pinnedSources}
|
||||
ListEmptyComponent={
|
||||
<EmptyView
|
||||
icon="__φ(..)"
|
||||
description={`Search a novel in ${
|
||||
searchAllSources
|
||||
? 'all sources'
|
||||
: pinnedSources.length === 0
|
||||
? 'pinned sources\n(No sources pinned)'
|
||||
: 'pinned sources'
|
||||
}`}
|
||||
/>
|
||||
}
|
||||
/>
|
||||
</ScreenContainer>
|
||||
);
|
||||
};
|
||||
|
||||
export default GlobalSearch;
|
||||
@@ -1,51 +0,0 @@
|
||||
import React from 'react';
|
||||
import { StyleSheet, FlatList, Text } from 'react-native';
|
||||
|
||||
import GlobalSearchNovelCover from './GlobalSearchNovelCover';
|
||||
|
||||
const GlobalSearchNovelList = ({ data, theme, library, navigation }) => {
|
||||
const inLibrary = (sourceId, novelUrl) =>
|
||||
library.some(obj => obj.novelUrl === novelUrl && obj.sourceId === sourceId);
|
||||
|
||||
const navigateToNovel = item => navigation.push('Novel', item);
|
||||
|
||||
const renderItem = ({ item }) => (
|
||||
<GlobalSearchNovelCover
|
||||
novel={item}
|
||||
theme={theme}
|
||||
onPress={() => navigateToNovel(item)}
|
||||
inLibrary={inLibrary(item.sourceId, item.novelUrl)}
|
||||
/>
|
||||
);
|
||||
|
||||
const ListEmptyComponent = (
|
||||
<Text style={[{ color: theme.textColorSecondary }, styles.emptyList]}>
|
||||
No results found
|
||||
</Text>
|
||||
);
|
||||
|
||||
return (
|
||||
<FlatList
|
||||
contentContainerStyle={styles.listContainer}
|
||||
horizontal={true}
|
||||
data={data}
|
||||
keyExtractor={item => item.sourceId + item.novelUrl}
|
||||
renderItem={renderItem}
|
||||
ListEmptyComponent={ListEmptyComponent}
|
||||
/>
|
||||
);
|
||||
};
|
||||
|
||||
export default GlobalSearchNovelList;
|
||||
|
||||
const styles = StyleSheet.create({
|
||||
listContainer: {
|
||||
flexGrow: 1,
|
||||
paddingVertical: 8,
|
||||
paddingHorizontal: 4,
|
||||
},
|
||||
emptyList: {
|
||||
padding: 8,
|
||||
paddingVertical: 4,
|
||||
},
|
||||
});
|
||||
@@ -1,51 +0,0 @@
|
||||
import React from 'react';
|
||||
import { StyleSheet, View, Text, ActivityIndicator } from 'react-native';
|
||||
|
||||
import GlobalSearchNovelList from './GlobalSearchNovelList';
|
||||
|
||||
const GlobalSearchSourceItem = ({ source, library, theme, navigation }) => {
|
||||
const { sourceName, lang, loading, novels, error } = source;
|
||||
|
||||
const colorError = {
|
||||
color: theme.statusBar === 'dark-content' ? '#B3261E' : '#F2B8B5',
|
||||
};
|
||||
|
||||
return (
|
||||
<>
|
||||
<View style={styles.sourceContainer}>
|
||||
<Text style={{ color: theme.textColorPrimary }}>{sourceName}</Text>
|
||||
<Text style={{ color: theme.textColorSecondary, fontSize: 12 }}>
|
||||
{lang}
|
||||
</Text>
|
||||
</View>
|
||||
{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}
|
||||
/>
|
||||
)}
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
export default GlobalSearchSourceItem;
|
||||
|
||||
const styles = StyleSheet.create({
|
||||
sourceContainer: {
|
||||
padding: 8,
|
||||
paddingVertical: 16,
|
||||
},
|
||||
error: {
|
||||
paddingHorizontal: 8,
|
||||
paddingVertical: 16,
|
||||
},
|
||||
});
|
||||
@@ -185,8 +185,8 @@ const LibraryScreen = ({ navigation }) => {
|
||||
<View style={styles.globalSearch}>
|
||||
<Pressable
|
||||
onPress={() =>
|
||||
navigation.navigate('GlobalSearch', {
|
||||
novelName: searchText,
|
||||
navigation.navigate('GlobalSearchScreen', {
|
||||
searchText,
|
||||
})
|
||||
}
|
||||
android_ripple={{ color: theme.rippleColor }}
|
||||
|
||||
@@ -70,7 +70,7 @@ const NovelInfoHeader = ({
|
||||
<NovelTitle
|
||||
theme={theme}
|
||||
onPress={() =>
|
||||
navigation.navigate('GlobalSearch', {
|
||||
navigation.navigate('GlobalSearchScreen', {
|
||||
novelName: item.novelName,
|
||||
})
|
||||
}
|
||||
|
||||
@@ -119,5 +119,10 @@
|
||||
},
|
||||
"sourceScreen": {
|
||||
"noResultsFound": "No results found"
|
||||
},
|
||||
"globalSearch": {
|
||||
"searchIn": "Search a novel in",
|
||||
"allSources": "all sources",
|
||||
"pinnedSources": "pinned sources"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -87,4 +87,7 @@ export interface StringMap {
|
||||
'moreScreen.settingsScreen.browseSettingsScreen.searchAllSources': 'string';
|
||||
'moreScreen.settingsScreen.browseSettingsScreen.searchAllWarning': 'string';
|
||||
'sourceScreen.noResultsFound': 'string';
|
||||
'globalSearch.searchIn': 'string';
|
||||
'globalSearch.allSources': 'string';
|
||||
'globalSearch.pinnedSources': 'string';
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user