subtracks/app/screens/Search.tsx
austinried a92ad7bfc9
Bugfix/large playlist crash (#111)
* get all song coverArt as they are rendered

doing it all up front was too heavy
temporarily disabled mapping artwork in setQueue, need to fix this

* use cache data for track artwork when available

* fix round art in context menu for songs

* set only the first artwork at play time

then set the rest in the playback service

* handle both cached images and fetching images

* remove commented code

* fix shuffle

fix first thumbnail not being updated on shuffle for now playing background
2022-04-21 14:58:35 +09:00

224 lines
5.7 KiB
TypeScript

import Button from '@app/components/Button'
import GradientScrollView from '@app/components/GradientScrollView'
import Header from '@app/components/Header'
import ListItem from '@app/components/ListItem'
import NothingHere from '@app/components/NothingHere'
import TextInput from '@app/components/TextInput'
import { withSuspense, withSuspenseMemo } from '@app/components/withSuspense'
import { useQuerySearchResults } from '@app/hooks/query'
import { useSetQueue } from '@app/hooks/trackplayer'
import { Album, Artist, SearchResults, Song } from '@app/models/library'
import colors from '@app/styles/colors'
import font from '@app/styles/font'
import { useFocusEffect, useNavigation } from '@react-navigation/native'
import equal from 'fast-deep-equal/es6/react'
import _ from 'lodash'
import React, { useCallback, useMemo, useRef, useState } from 'react'
import { useTranslation } from 'react-i18next'
import {
ActivityIndicator,
InteractionManager,
ScrollView,
StyleSheet,
TextInput as ReactTextInput,
View,
} from 'react-native'
import { useSafeAreaInsets } from 'react-native-safe-area-context'
const SongItem = React.memo<{ item: Song }>(({ item }) => {
const { setQueue, contextId } = useSetQueue('song', [item])
return (
<ListItem
item={item}
contextId={contextId}
queueId={0}
showArt={true}
showStar={false}
onPress={() => setQueue({ title: item.title, playTrack: 0 })}
/>
)
}, equal)
const ResultsCategory = withSuspenseMemo<{
name: string
query: string
items: (Artist | Album | Song)[]
type: 'artist' | 'album' | 'song'
}>(
({ name, query, type, items }) => {
const navigation = useNavigation()
const { t } = useTranslation()
if (items.length === 0) {
return <></>
}
return (
<>
<Header>{name}</Header>
{items.map(a =>
type === 'song' ? (
<SongItem key={a.id} item={a as Song} />
) : (
<ListItem key={a.id} item={a} showArt={true} showStar={false} />
),
)}
{items.length === 5 && (
<Button
title={t('search.moreResults')}
buttonStyle="hollow"
style={styles.more}
onPress={() => navigation.navigate('results', { query, type: items[0].itemType })}
/>
)}
</>
)
},
null,
equal,
)
const Results = withSuspenseMemo<{
results: SearchResults
query: string
}>(
({ results, query }) => {
const { t } = useTranslation()
return (
<>
<ResultsCategory
name={t('resources.artist.name', { count: 2 })}
query={query}
type={'artist'}
items={results.artists}
/>
<ResultsCategory
name={t('resources.album.name', { count: 2 })}
query={query}
type={'album'}
items={results.albums}
/>
<ResultsCategory
name={t('resources.song.name', { count: 2 })}
query={query}
type={'song'}
items={results.songs}
/>
</>
)
},
null,
equal,
)
const Search = withSuspense(() => {
const [query, setQuery] = useState('')
const { data, isLoading } = useQuerySearchResults({ query, albumCount: 5, artistCount: 5, songCount: 5 })
const { t } = useTranslation()
const [text, setText] = useState('')
const searchBarRef = useRef<ReactTextInput>(null)
const scrollRef = useRef<ScrollView>(null)
const paddingTop = useSafeAreaInsets().top
useFocusEffect(
useCallback(() => {
const task = InteractionManager.runAfterInteractions(() => {
setTimeout(() => {
if (text) {
return
}
setText('')
setQuery('')
searchBarRef.current?.focus()
scrollRef.current?.scrollTo({ y: 0, animated: true })
}, 50)
})
return () => task.cancel()
}, [text]),
)
const debouncedSetQuery = useMemo(
() =>
_.debounce((value: string) => {
setQuery(value)
}, 400),
[],
)
const onChangeText = useCallback(
(value: string) => {
setText(value)
debouncedSetQuery(value)
},
[setText, debouncedSetQuery],
)
const resultsCount =
(data ? data.pages.reduce((acc, val) => (acc += val.artists.length), 0) : 0) +
(data ? data.pages.reduce((acc, val) => (acc += val.albums.length), 0) : 0) +
(data ? data.pages.reduce((acc, val) => (acc += val.songs.length), 0) : 0)
return (
<GradientScrollView ref={scrollRef} style={styles.scroll} contentContainerStyle={{ paddingTop }}>
<View style={styles.content}>
<View style={styles.inputBar}>
<TextInput
ref={searchBarRef}
style={styles.textInput}
placeholder={t('search.inputPlaceholder')}
value={text}
onChangeText={onChangeText}
/>
<ActivityIndicator animating={isLoading} size="small" color={colors.text.secondary} style={styles.activity} />
</View>
{data !== undefined && resultsCount > 0 ? (
<Results results={data.pages[0]} query={text} />
) : (
<NothingHere style={styles.noResults} />
)}
</View>
</GradientScrollView>
)
})
const styles = StyleSheet.create({
scroll: {
flex: 1,
},
content: {
paddingHorizontal: 20,
paddingBottom: 20,
alignItems: 'stretch',
},
inputBar: {
justifyContent: 'center',
},
activity: {
position: 'absolute',
right: 16,
bottom: 15,
},
noResults: {
width: '100%',
},
itemText: {
color: colors.text.primary,
fontFamily: font.regular,
fontSize: 14,
},
more: {
marginTop: 5,
marginBottom: 10,
},
textInput: {
marginTop: 20,
paddingHorizontal: 12,
paddingRight: 46,
},
})
export default Search