From bece88663ffeebd81925deca43b0b2439dd46c54 Mon Sep 17 00:00:00 2001 From: Rajarshee Chatterjee Date: Sat, 18 Jun 2022 12:32:15 +0530 Subject: [PATCH] refactor: Global Search Screen Closes #302 --- scripts/generateSource.cjs | 2 +- src/hooks/useSearch.ts | 4 +- src/navigators/Main.js | 13 +- .../GlobalSearchScreen/GlobalSearchScreen.tsx | 66 +++++++ .../components/GlobalSearchNovelItem.tsx | 78 +++++++++ .../components/GlobalSearchResultsList.tsx | 165 ++++++++++++++++++ .../hooks/useGlobalSearch.ts | 108 ++++++++++++ src/screens/browse/BrowseScreen.tsx | 2 +- src/screens/browse/discover/MalTopNovels.js | 2 +- .../browse/globalsearch/GlobalSearch.js | 149 ---------------- .../globalsearch/GlobalSearchNovelList.js | 51 ------ .../globalsearch/GlobalSearchSourceItem.js | 51 ------ src/screens/library/LibraryScreen.js | 4 +- .../novel/components/Info/NovelInfoHeader.js | 2 +- strings/languages/en/strings.json | 5 + strings/types/index.ts | 3 + 16 files changed, 443 insertions(+), 262 deletions(-) create mode 100644 src/screens/GlobalSearchScreen/GlobalSearchScreen.tsx create mode 100644 src/screens/GlobalSearchScreen/components/GlobalSearchNovelItem.tsx create mode 100644 src/screens/GlobalSearchScreen/components/GlobalSearchResultsList.tsx create mode 100644 src/screens/GlobalSearchScreen/hooks/useGlobalSearch.ts delete mode 100644 src/screens/browse/globalsearch/GlobalSearch.js delete mode 100644 src/screens/browse/globalsearch/GlobalSearchNovelList.js delete mode 100644 src/screens/browse/globalsearch/GlobalSearchSourceItem.js diff --git a/scripts/generateSource.cjs b/scripts/generateSource.cjs index 6326284e..5bd92c56 100644 --- a/scripts/generateSource.cjs +++ b/scripts/generateSource.cjs @@ -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 []; ` diff --git a/src/hooks/useSearch.ts b/src/hooks/useSearch.ts index 4d4ca7cb..01d7d961 100644 --- a/src/hooks/useSearch.ts +++ b/src/hooks/useSearch.ts @@ -1,7 +1,7 @@ import { useCallback, useState } from 'react'; -const useSearch = () => { - const [searchText, setSearchText] = useState(''); +const useSearch = (defaultSearchText?: string) => { + const [searchText, setSearchText] = useState(defaultSearchText || ''); const clearSearchbar = useCallback(() => setSearchText(''), []); return { searchText, setSearchText, clearSearchbar }; diff --git a/src/navigators/Main.js b/src/navigators/Main.js index 6c46c19f..b256f4b5 100644 --- a/src/navigators/Main.js +++ b/src/navigators/Main.js @@ -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 = () => { - + diff --git a/src/screens/GlobalSearchScreen/GlobalSearchScreen.tsx b/src/screens/GlobalSearchScreen/GlobalSearchScreen.tsx new file mode 100644 index 00000000..646e997d --- /dev/null +++ b/src/screens/GlobalSearchScreen/GlobalSearchScreen.tsx @@ -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 ( + <> + + {progress ? ( + + ) : null} + + } + /> + + ); +}; + +export default GlobalSearchScreen; diff --git a/src/screens/GlobalSearchScreen/components/GlobalSearchNovelItem.tsx b/src/screens/GlobalSearchScreen/components/GlobalSearchNovelItem.tsx new file mode 100644 index 00000000..ce2ff968 --- /dev/null +++ b/src/screens/GlobalSearchScreen/components/GlobalSearchNovelItem.tsx @@ -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 = ({ + novel, + navigateToNovel, + theme, +}) => { + const novelItemDimensions = useMemo( + () => ({ + width: Dimensions.get('window').width / 3 - 16, + height: ((Dimensions.get('window').width / 3 - 16) * 4) / 3, + }), + [], + ); + + return ( + + navigateToNovel(novel)} + > + + + {novel.novelName} + + + + ); +}; + +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, + }, +}); diff --git a/src/screens/GlobalSearchScreen/components/GlobalSearchResultsList.tsx b/src/screens/GlobalSearchScreen/components/GlobalSearchResultsList.tsx new file mode 100644 index 00000000..296aeadb --- /dev/null +++ b/src/screens/GlobalSearchScreen/components/GlobalSearchResultsList.tsx @@ -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 = ({ + searchResults, + ListEmptyComponent, +}) => { + const theme = useTheme(); + const navigation = useNavigation>(); + 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 ( + + keyExtractor={keyExtractor} + data={searchResults} + contentContainerStyle={styles.resultList} + renderItem={({ item }) => ( + <> + + + navigation.navigate('SourceScreen', { + sourceId: item.source.sourceId, + sourceName: item.source.sourceName, + url: item.source.url, + }) + } + > + + + {item.source.sourceName} + + + {item.source.lang} + + + + + {item.isLoading ? ( + + ) : item.error ? ( + + {item.error} + + ) : ( + + novelItem.novelUrl + novelItem.sourceId + } + data={item.novels} + ListEmptyComponent={ + + {getString('sourceScreen.noResultsFound')} + + } + renderItem={({ item: novelItem }) => ( + + )} + /> + )} + + + )} + 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, + }, +}); diff --git a/src/screens/GlobalSearchScreen/hooks/useGlobalSearch.ts b/src/screens/GlobalSearchScreen/hooks/useGlobalSearch.ts new file mode 100644 index 00000000..2649fc2b --- /dev/null +++ b/src/screens/GlobalSearchScreen/hooks/useGlobalSearch.ts @@ -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([]); + 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 }; +}; diff --git a/src/screens/browse/BrowseScreen.tsx b/src/screens/browse/BrowseScreen.tsx index 4a764212..06ccf36f 100644 --- a/src/screens/browse/BrowseScreen.tsx +++ b/src/screens/browse/BrowseScreen.tsx @@ -79,7 +79,7 @@ const BrowseScreen = () => { () => [ { iconName: 'book-search', - onPress: () => navigate('GlobalSearch' as never), + onPress: () => navigate('GlobalSearchScreen' as never), }, { iconName: 'swap-vertical-variant', diff --git a/src/screens/browse/discover/MalTopNovels.js b/src/screens/browse/discover/MalTopNovels.js index a24d0170..a900a78a 100644 --- a/src/screens/browse/discover/MalTopNovels.js +++ b/src/screens/browse/discover/MalTopNovels.js @@ -69,7 +69,7 @@ const BrowseMalScreen = ({ navigation, route }) => { novel={item} theme={theme} onPress={() => - navigation.navigate('GlobalSearch', { + navigation.navigate('GlobalSearchScreen', { novelName: item.novelName, }) } diff --git a/src/screens/browse/globalsearch/GlobalSearch.js b/src/screens/browse/globalsearch/GlobalSearch.js deleted file mode 100644 index b1fc25e4..00000000 --- a/src/screens/browse/globalsearch/GlobalSearch.js +++ /dev/null @@ -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 }) => ( - - ); - - return ( - - - {progress > 0 && ( - - )} - item.sourceId.toString()} - renderItem={renderItem} - extraData={pinnedSources} - ListEmptyComponent={ - - } - /> - - ); -}; - -export default GlobalSearch; diff --git a/src/screens/browse/globalsearch/GlobalSearchNovelList.js b/src/screens/browse/globalsearch/GlobalSearchNovelList.js deleted file mode 100644 index 87f818be..00000000 --- a/src/screens/browse/globalsearch/GlobalSearchNovelList.js +++ /dev/null @@ -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 }) => ( - navigateToNovel(item)} - inLibrary={inLibrary(item.sourceId, item.novelUrl)} - /> - ); - - const ListEmptyComponent = ( - - No results found - - ); - - return ( - 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, - }, -}); diff --git a/src/screens/browse/globalsearch/GlobalSearchSourceItem.js b/src/screens/browse/globalsearch/GlobalSearchSourceItem.js deleted file mode 100644 index 96538762..00000000 --- a/src/screens/browse/globalsearch/GlobalSearchSourceItem.js +++ /dev/null @@ -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 ( - <> - - {sourceName} - - {lang} - - - {error ? ( - {error} - ) : loading ? ( - - ) : ( - - )} - - ); -}; - -export default GlobalSearchSourceItem; - -const styles = StyleSheet.create({ - sourceContainer: { - padding: 8, - paddingVertical: 16, - }, - error: { - paddingHorizontal: 8, - paddingVertical: 16, - }, -}); diff --git a/src/screens/library/LibraryScreen.js b/src/screens/library/LibraryScreen.js index 64d542a7..7bf2243f 100644 --- a/src/screens/library/LibraryScreen.js +++ b/src/screens/library/LibraryScreen.js @@ -185,8 +185,8 @@ const LibraryScreen = ({ navigation }) => { - navigation.navigate('GlobalSearch', { - novelName: searchText, + navigation.navigate('GlobalSearchScreen', { + searchText, }) } android_ripple={{ color: theme.rippleColor }} diff --git a/src/screens/novel/components/Info/NovelInfoHeader.js b/src/screens/novel/components/Info/NovelInfoHeader.js index 483ea648..c64d20b1 100644 --- a/src/screens/novel/components/Info/NovelInfoHeader.js +++ b/src/screens/novel/components/Info/NovelInfoHeader.js @@ -70,7 +70,7 @@ const NovelInfoHeader = ({ - navigation.navigate('GlobalSearch', { + navigation.navigate('GlobalSearchScreen', { novelName: item.novelName, }) } diff --git a/strings/languages/en/strings.json b/strings/languages/en/strings.json index 20e25d6c..7a5e4748 100644 --- a/strings/languages/en/strings.json +++ b/strings/languages/en/strings.json @@ -119,5 +119,10 @@ }, "sourceScreen": { "noResultsFound": "No results found" + }, + "globalSearch": { + "searchIn": "Search a novel in", + "allSources": "all sources", + "pinnedSources": "pinned sources" } } diff --git a/strings/types/index.ts b/strings/types/index.ts index e60197a7..13d015cd 100644 --- a/strings/types/index.ts +++ b/strings/types/index.ts @@ -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'; }