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 React, { useEffect } from 'react'
import { StyleSheet } from 'react-native' import { StyleSheet } from 'react-native'
import { createNativeStackNavigator, NativeStackNavigationProp } from 'react-native-screens/native-stack' import { createNativeStackNavigator, NativeStackNavigationProp } from 'react-native-screens/native-stack'
import SearchResultsView from '@app/screens/SearchResultsView'
type TabStackParamList = { type TabStackParamList = {
main: undefined main: undefined
album: { id: string; title: string } album: { id: string; title: string }
artist: { id: string; title: string } artist: { id: string; title: string }
playlist: { id: string; title: string } playlist: { id: string; title: string }
results: { query: string; type: 'album' | 'song' | 'artist' }
} }
type AlbumScreenNavigationProp = NativeStackNavigationProp<TabStackParamList, 'album'> 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" /> <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({ const styles = StyleSheet.create({
stackheaderStyle: { stackheaderStyle: {
backgroundColor: colors.gradient.high, 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="album" component={AlbumScreen} options={itemScreenOptions} />
<Stack.Screen name="artist" component={ArtistScreen} options={{ headerShown: false }} /> <Stack.Screen name="artist" component={ArtistScreen} options={{ headerShown: false }} />
<Stack.Screen name="playlist" component={PlaylistScreen} options={itemScreenOptions} /> <Stack.Screen name="playlist" component={PlaylistScreen} options={itemScreenOptions} />
<Stack.Screen name="results" component={ResultsScreen} options={itemScreenOptions} />
</Stack.Navigator> </Stack.Navigator>
) )
} }

View File

@ -1,3 +1,4 @@
import Button from '@app/components/Button'
import GradientScrollView from '@app/components/GradientScrollView' import GradientScrollView from '@app/components/GradientScrollView'
import Header from '@app/components/Header' import Header from '@app/components/Header'
import ListItem from '@app/components/ListItem' import ListItem from '@app/components/ListItem'
@ -9,6 +10,7 @@ import { useStore } from '@app/state/store'
import { selectTrackPlayer } from '@app/state/trackplayer' import { selectTrackPlayer } from '@app/state/trackplayer'
import colors from '@app/styles/colors' import colors from '@app/styles/colors'
import font from '@app/styles/font' import font from '@app/styles/font'
import { useNavigation } from '@react-navigation/native'
import debounce from 'lodash.debounce' import debounce from 'lodash.debounce'
import React, { useCallback, useMemo, useState } from 'react' import React, { useCallback, useMemo, useState } from 'react'
import { ActivityIndicator, StatusBar, StyleSheet, TextInput, View } from 'react-native' import { ActivityIndicator, StatusBar, StyleSheet, TextInput, View } from 'react-native'
@ -30,8 +32,11 @@ const SongItem = React.memo<{ item: Song }>(({ item }) => {
const ResultsCategory = React.memo<{ const ResultsCategory = React.memo<{
name: string name: string
query: string
items: ListableItem[] items: ListableItem[]
}>(({ name, items }) => { }>(({ name, query, items }) => {
const navigation = useNavigation()
if (items.length === 0) { if (items.length === 0) {
return <></> return <></>
} }
@ -46,39 +51,53 @@ const ResultsCategory = React.memo<{
<ListItem key={a.id} item={a} showArt={true} showStar={false} /> <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<{ const Results = React.memo<{
results: SearchResults results: SearchResults
}>(({ results }) => { query: string
}>(({ results, query }) => {
return ( return (
<> <>
<ResultsCategory name="Artists" items={results.artists} /> <ResultsCategory name="Artists" query={query} items={results.artists} />
<ResultsCategory name="Albums" items={results.albums} /> <ResultsCategory name="Albums" query={query} items={results.albums} />
<ResultsCategory name="Songs" items={results.songs} /> <ResultsCategory name="Songs" query={query} items={results.songs} />
</> </>
) )
}) })
const Search = () => { const Search = () => {
const updateSearch = useStore(selectMusic.fetchSearchResults) const fetchSearchResults = useStore(selectMusic.fetchSearchResults)
const clearSearch = useStore(selectMusic.clearSearchResults) const [results, setResults] = useState<SearchResults>({ artists: [], albums: [], songs: [] })
const updating = useStore(selectMusic.searchResultsUpdating) const [refreshing, setRefreshing] = useState(false)
const results = useStore(selectMusic.searchResults) const [text, setText] = useState('')
useActiveServerRefresh( useActiveServerRefresh(
useCallback(() => { useCallback(() => {
setText('') setText('')
clearSearch() setResults({ artists: [], albums: [], songs: [] })
}, [clearSearch]), }, []),
) )
const [text, setText] = useState('') const debouncedonUpdateSearch = useMemo(
() =>
// eslint-disable-next-line react-hooks/exhaustive-deps debounce(async (query: string) => {
const debouncedonUpdateSearch = useMemo(() => debounce(updateSearch, 400), []) setRefreshing(true)
setResults(await fetchSearchResults(query))
setRefreshing(false)
}, 400),
[fetchSearchResults],
)
const onChangeText = useCallback( const onChangeText = useCallback(
(value: string) => { (value: string) => {
@ -102,9 +121,14 @@ const Search = () => {
value={text} value={text}
onChangeText={onChangeText} 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> </View>
{resultsCount > 0 ? <Results results={results} /> : <NothingHere style={styles.noResults} />} {resultsCount > 0 ? <Results results={results} query={text} /> : <NothingHere style={styles.noResults} />}
</View> </View>
</GradientScrollView> </GradientScrollView>
) )
@ -119,6 +143,7 @@ const styles = StyleSheet.create({
}, },
content: { content: {
paddingHorizontal: 20, paddingHorizontal: 20,
paddingBottom: 20,
alignItems: 'stretch', alignItems: 'stretch',
}, },
inputBar: { inputBar: {
@ -147,6 +172,10 @@ const styles = StyleSheet.create({
fontFamily: font.regular, fontFamily: font.regular,
fontSize: 14, fontSize: 14,
}, },
more: {
marginTop: 5,
marginBottom: 10,
},
}) })
export default Search 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, StarrableItemType,
} from '@app/models/music' } from '@app/models/music'
import { Store } from '@app/state/store' 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 produce from 'immer'
import { GetState, SetState } from 'zustand' import { GetState, SetState } from 'zustand'
@ -33,11 +33,12 @@ export type MusicSlice = {
fetchArtists: (size?: number, offset?: number) => Promise<Artist[]> fetchArtists: (size?: number, offset?: number) => Promise<Artist[]>
fetchPlaylists: () => Promise<PlaylistListItem[]> fetchPlaylists: () => Promise<PlaylistListItem[]>
fetchAlbums: () => Promise<AlbumListItem[]> fetchAlbums: () => Promise<AlbumListItem[]>
fetchSearchResults: (
searchResults: SearchResults query: string,
searchResultsUpdating: boolean type?: 'album' | 'song' | 'artist',
fetchSearchResults: (query: string) => Promise<void> size?: number,
clearSearchResults: () => void offset?: number,
) => Promise<SearchResults>
homeLists: HomeLists homeLists: HomeLists
homeListsUpdating: boolean homeListsUpdating: boolean
@ -66,11 +67,7 @@ export const selectMusic = {
fetchArtists: (store: MusicSlice) => store.fetchArtists, fetchArtists: (store: MusicSlice) => store.fetchArtists,
fetchPlaylists: (store: MusicSlice) => store.fetchPlaylists, fetchPlaylists: (store: MusicSlice) => store.fetchPlaylists,
fetchAlbums: (store: MusicSlice) => store.fetchAlbums, fetchAlbums: (store: MusicSlice) => store.fetchAlbums,
searchResults: (store: MusicSlice) => store.searchResults,
searchResultsUpdating: (store: MusicSlice) => store.searchResultsUpdating,
fetchSearchResults: (store: MusicSlice) => store.fetchSearchResults, fetchSearchResults: (store: MusicSlice) => store.fetchSearchResults,
clearSearchResults: (store: MusicSlice) => store.clearSearchResults,
homeLists: (store: MusicSlice) => store.homeLists, homeLists: (store: MusicSlice) => store.homeLists,
homeListsUpdating: (store: MusicSlice) => store.homeListsUpdating, homeListsUpdating: (store: MusicSlice) => store.homeListsUpdating,
@ -236,31 +233,34 @@ export const createMusicSlice = (set: SetState<Store>, get: GetState<Store>): Mu
} }
}, },
searchResults: { fetchSearchResults: async (query, type, size, offset) => {
artists: [],
albums: [],
songs: [],
},
searchResultsUpdating: false,
fetchSearchResults: async query => {
if (query.length < 2) { if (query.length < 2) {
return return { artists: [], albums: [], songs: [] }
} }
const client = get().client const client = get().client
if (!client) { if (!client) {
return return { artists: [], albums: [], songs: [] }
} }
if (get().searchResultsUpdating) {
return
}
set({ searchResultsUpdating: true })
try { 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 artists = response.data.artists.map(get().mapArtistID3toArtist)
const albums = response.data.albums.map(get().mapAlbumID3toAlbumListItem) const albums = response.data.albums.map(get().mapAlbumID3toAlbumListItem)
@ -268,25 +268,17 @@ export const createMusicSlice = (set: SetState<Store>, get: GetState<Store>): Mu
set( set(
produce<MusicSlice>(state => { produce<MusicSlice>(state => {
state.searchResults = { artists, albums, songs } state.starredSongs = reduceStarred(state.starredSongs, songs)
state.starredSongs = reduceStarred(state.starredSongs, state.searchResults.songs) state.starredArtists = reduceStarred(state.starredArtists, artists)
state.starredArtists = reduceStarred(state.starredArtists, state.searchResults.artists) state.starredAlbums = reduceStarred(state.starredAlbums, albums)
state.starredAlbums = reduceStarred(state.starredAlbums, state.searchResults.albums)
}), }),
) )
} finally {
set({ searchResultsUpdating: false }) return { artists, albums, songs }
} catch {
return { artists: [], albums: [], songs: [] }
} }
}, },
clearSearchResults: () => {
set({
searchResults: {
artists: [],
albums: [],
songs: [],
},
})
},
homeLists: {}, homeLists: {},
homeListsUpdating: false, homeListsUpdating: false,