mirror of
https://github.com/austinried/subtracks.git
synced 2025-12-27 09:09:29 +01:00
added search results "more" screen
This commit is contained in:
parent
ba2aea0fbe
commit
25b95a4b65
@ -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>
|
||||
)
|
||||
}
|
||||
|
||||
@ -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
|
||||
|
||||
93
app/screens/SearchResultsView.tsx
Normal file
93
app/screens/SearchResultsView.tsx
Normal 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
|
||||
@ -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,
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user