added search results "more" screen

This commit is contained in:
austinried 2021-08-19 09:20:40 +09:00
parent ba2aea0fbe
commit 25b95a4b65
4 changed files with 187 additions and 59 deletions

View File

@ -13,12 +13,14 @@ import { RouteProp, StackActions } from '@react-navigation/native'
import React, { useEffect } from 'react'
import { StyleSheet } from 'react-native'
import { createNativeStackNavigator, NativeStackNavigationProp } from 'react-native-screens/native-stack'
import SearchResultsView from '@app/screens/SearchResultsView'
type TabStackParamList = {
main: undefined
album: { id: string; title: string }
artist: { id: string; title: string }
playlist: { id: string; title: string }
results: { query: string; type: 'album' | 'song' | 'artist' }
}
type AlbumScreenNavigationProp = NativeStackNavigationProp<TabStackParamList, 'album'>
@ -54,6 +56,17 @@ const PlaylistScreen: React.FC<PlaylistScreenProps> = ({ route }) => (
<SongListView id={route.params.id} title={route.params.title} type="playlist" />
)
type ResultsScreenNavigationProp = NativeStackNavigationProp<TabStackParamList, 'results'>
type ResultsScreenRouteProp = RouteProp<TabStackParamList, 'results'>
type ResultsScreenProps = {
route: ResultsScreenRouteProp
navigation: ResultsScreenNavigationProp
}
const ResultsScreen: React.FC<ResultsScreenProps> = ({ route }) => (
<SearchResultsView query={route.params.query} type={route.params.type} />
)
const styles = StyleSheet.create({
stackheaderStyle: {
backgroundColor: colors.gradient.high,
@ -92,6 +105,7 @@ function createTabStackNavigator(Component: React.ComponentType<any>) {
<Stack.Screen name="album" component={AlbumScreen} options={itemScreenOptions} />
<Stack.Screen name="artist" component={ArtistScreen} options={{ headerShown: false }} />
<Stack.Screen name="playlist" component={PlaylistScreen} options={itemScreenOptions} />
<Stack.Screen name="results" component={ResultsScreen} options={itemScreenOptions} />
</Stack.Navigator>
)
}

View File

@ -1,3 +1,4 @@
import Button from '@app/components/Button'
import GradientScrollView from '@app/components/GradientScrollView'
import Header from '@app/components/Header'
import ListItem from '@app/components/ListItem'
@ -9,6 +10,7 @@ import { useStore } from '@app/state/store'
import { selectTrackPlayer } from '@app/state/trackplayer'
import colors from '@app/styles/colors'
import font from '@app/styles/font'
import { useNavigation } from '@react-navigation/native'
import debounce from 'lodash.debounce'
import React, { useCallback, useMemo, useState } from 'react'
import { ActivityIndicator, StatusBar, StyleSheet, TextInput, View } from 'react-native'
@ -30,8 +32,11 @@ const SongItem = React.memo<{ item: Song }>(({ item }) => {
const ResultsCategory = React.memo<{
name: string
query: string
items: ListableItem[]
}>(({ name, items }) => {
}>(({ name, query, items }) => {
const navigation = useNavigation()
if (items.length === 0) {
return <></>
}
@ -46,39 +51,53 @@ const ResultsCategory = React.memo<{
<ListItem key={a.id} item={a} showArt={true} showStar={false} />
),
)}
{items.length === 5 && (
<Button
title="More..."
buttonStyle="hollow"
style={styles.more}
onPress={() => navigation.navigate('results', { query, type: items[0].itemType })}
/>
)}
</>
)
})
const Results = React.memo<{
results: SearchResults
}>(({ results }) => {
query: string
}>(({ results, query }) => {
return (
<>
<ResultsCategory name="Artists" items={results.artists} />
<ResultsCategory name="Albums" items={results.albums} />
<ResultsCategory name="Songs" items={results.songs} />
<ResultsCategory name="Artists" query={query} items={results.artists} />
<ResultsCategory name="Albums" query={query} items={results.albums} />
<ResultsCategory name="Songs" query={query} items={results.songs} />
</>
)
})
const Search = () => {
const updateSearch = useStore(selectMusic.fetchSearchResults)
const clearSearch = useStore(selectMusic.clearSearchResults)
const updating = useStore(selectMusic.searchResultsUpdating)
const results = useStore(selectMusic.searchResults)
const fetchSearchResults = useStore(selectMusic.fetchSearchResults)
const [results, setResults] = useState<SearchResults>({ artists: [], albums: [], songs: [] })
const [refreshing, setRefreshing] = useState(false)
const [text, setText] = useState('')
useActiveServerRefresh(
useCallback(() => {
setText('')
clearSearch()
}, [clearSearch]),
setResults({ artists: [], albums: [], songs: [] })
}, []),
)
const [text, setText] = useState('')
// eslint-disable-next-line react-hooks/exhaustive-deps
const debouncedonUpdateSearch = useMemo(() => debounce(updateSearch, 400), [])
const debouncedonUpdateSearch = useMemo(
() =>
debounce(async (query: string) => {
setRefreshing(true)
setResults(await fetchSearchResults(query))
setRefreshing(false)
}, 400),
[fetchSearchResults],
)
const onChangeText = useCallback(
(value: string) => {
@ -102,9 +121,14 @@ const Search = () => {
value={text}
onChangeText={onChangeText}
/>
<ActivityIndicator animating={updating} size="small" color={colors.text.secondary} style={styles.activity} />
<ActivityIndicator
animating={refreshing}
size="small"
color={colors.text.secondary}
style={styles.activity}
/>
</View>
{resultsCount > 0 ? <Results results={results} /> : <NothingHere style={styles.noResults} />}
{resultsCount > 0 ? <Results results={results} query={text} /> : <NothingHere style={styles.noResults} />}
</View>
</GradientScrollView>
)
@ -119,6 +143,7 @@ const styles = StyleSheet.create({
},
content: {
paddingHorizontal: 20,
paddingBottom: 20,
alignItems: 'stretch',
},
inputBar: {
@ -147,6 +172,10 @@ const styles = StyleSheet.create({
fontFamily: font.regular,
fontSize: 14,
},
more: {
marginTop: 5,
marginBottom: 10,
},
})
export default Search

View File

@ -0,0 +1,93 @@
import GradientFlatList from '@app/components/GradientFlatList'
import ListItem from '@app/components/ListItem'
import { useFetchPaginatedList } from '@app/hooks/list'
import { AlbumListItem, Artist, Song } from '@app/models/music'
import { selectMusic } from '@app/state/music'
import { useStore } from '@app/state/store'
import { selectTrackPlayer } from '@app/state/trackplayer'
import { useNavigation } from '@react-navigation/native'
import React, { useCallback, useEffect } from 'react'
import { StyleSheet } from 'react-native'
type SearchListItemType = AlbumListItem | Song | Artist
const ResultsListItem: React.FC<{ item: SearchListItemType }> = ({ item }) => {
const setQueue = useStore(selectTrackPlayer.setQueue)
let onPress
if (item.itemType === 'song') {
onPress = () => setQueue([item], item.title, 'song', item.id, 0)
}
return (
<ListItem
item={item}
contextId={item.id}
queueId={0}
showArt={true}
showStar={false}
listStyle="small"
onPress={onPress}
/>
)
}
const SearchResultsRenderItem: React.FC<{ item: SearchListItemType }> = ({ item }) => <ResultsListItem item={item} />
const SearchResultsView: React.FC<{
query: string
type: 'album' | 'artist' | 'song'
}> = ({ query, type }) => {
const navigation = useNavigation()
const fetchSearchResults = useStore(selectMusic.fetchSearchResults)
const { list, refreshing, refresh, fetchNextPage } = useFetchPaginatedList<SearchListItemType>(
useCallback(
(size, offset) =>
fetchSearchResults(query, type, size, offset).then(results => {
switch (type) {
case 'album':
return results.albums
case 'artist':
return results.artists
case 'song':
return results.songs
default:
return []
}
}),
[fetchSearchResults, query, type],
),
50,
)
useEffect(() => {
navigation.setOptions({
title: `Search: "${query}"`,
})
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [])
return (
<GradientFlatList
contentContainerStyle={styles.listContent}
data={list}
renderItem={SearchResultsRenderItem}
keyExtractor={item => item.id}
onRefresh={refresh}
refreshing={refreshing}
overScrollMode="never"
onEndReached={fetchNextPage}
onEndReachedThreshold={1}
/>
)
}
const styles = StyleSheet.create({
listContent: {
minHeight: '100%',
paddingHorizontal: 10,
paddingTop: 6,
},
})
export default SearchResultsView

View File

@ -10,7 +10,7 @@ import {
StarrableItemType,
} from '@app/models/music'
import { Store } from '@app/state/store'
import { GetAlbumList2Type, StarParams } from '@app/subsonic/params'
import { GetAlbumList2Type, Search3Params, StarParams } from '@app/subsonic/params'
import produce from 'immer'
import { GetState, SetState } from 'zustand'
@ -33,11 +33,12 @@ export type MusicSlice = {
fetchArtists: (size?: number, offset?: number) => Promise<Artist[]>
fetchPlaylists: () => Promise<PlaylistListItem[]>
fetchAlbums: () => Promise<AlbumListItem[]>
searchResults: SearchResults
searchResultsUpdating: boolean
fetchSearchResults: (query: string) => Promise<void>
clearSearchResults: () => void
fetchSearchResults: (
query: string,
type?: 'album' | 'song' | 'artist',
size?: number,
offset?: number,
) => Promise<SearchResults>
homeLists: HomeLists
homeListsUpdating: boolean
@ -66,11 +67,7 @@ export const selectMusic = {
fetchArtists: (store: MusicSlice) => store.fetchArtists,
fetchPlaylists: (store: MusicSlice) => store.fetchPlaylists,
fetchAlbums: (store: MusicSlice) => store.fetchAlbums,
searchResults: (store: MusicSlice) => store.searchResults,
searchResultsUpdating: (store: MusicSlice) => store.searchResultsUpdating,
fetchSearchResults: (store: MusicSlice) => store.fetchSearchResults,
clearSearchResults: (store: MusicSlice) => store.clearSearchResults,
homeLists: (store: MusicSlice) => store.homeLists,
homeListsUpdating: (store: MusicSlice) => store.homeListsUpdating,
@ -236,31 +233,34 @@ export const createMusicSlice = (set: SetState<Store>, get: GetState<Store>): Mu
}
},
searchResults: {
artists: [],
albums: [],
songs: [],
},
searchResultsUpdating: false,
fetchSearchResults: async query => {
fetchSearchResults: async (query, type, size, offset) => {
if (query.length < 2) {
return
return { artists: [], albums: [], songs: [] }
}
const client = get().client
if (!client) {
return
return { artists: [], albums: [], songs: [] }
}
if (get().searchResultsUpdating) {
return
}
set({ searchResultsUpdating: true })
try {
const response = await client.search3({ query })
const params: Search3Params = { query }
if (type === 'album') {
params.albumCount = size
params.albumOffset = offset
} else if (type === 'artist') {
params.artistCount = size
params.artistOffset = offset
} else if (type === 'song') {
params.songCount = size
params.songOffset = offset
} else {
params.albumCount = 5
params.artistCount = 5
params.songCount = 5
}
const response = await client.search3(params)
const artists = response.data.artists.map(get().mapArtistID3toArtist)
const albums = response.data.albums.map(get().mapAlbumID3toAlbumListItem)
@ -268,25 +268,17 @@ export const createMusicSlice = (set: SetState<Store>, get: GetState<Store>): Mu
set(
produce<MusicSlice>(state => {
state.searchResults = { artists, albums, songs }
state.starredSongs = reduceStarred(state.starredSongs, state.searchResults.songs)
state.starredArtists = reduceStarred(state.starredArtists, state.searchResults.artists)
state.starredAlbums = reduceStarred(state.starredAlbums, state.searchResults.albums)
state.starredSongs = reduceStarred(state.starredSongs, songs)
state.starredArtists = reduceStarred(state.starredArtists, artists)
state.starredAlbums = reduceStarred(state.starredAlbums, albums)
}),
)
} finally {
set({ searchResultsUpdating: false })
return { artists, albums, songs }
} catch {
return { artists: [], albums: [], songs: [] }
}
},
clearSearchResults: () => {
set({
searchResults: {
artists: [],
albums: [],
songs: [],
},
})
},
homeLists: {},
homeListsUpdating: false,