mirror of
https://github.com/austinried/subtracks.git
synced 2025-12-29 17:39:27 +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 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>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|||||||
@ -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
|
||||||
|
|||||||
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,
|
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,
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user