mirror of
https://github.com/austinried/subtracks.git
synced 2025-12-27 09:09:29 +01:00
* initial react-query experiments * use queries for item screens send the data we do have over routing to prepopulate (album/playlist) use number for starred because sending Date freaks out react-navigation * add in equiv. song cover art fix * reorg, switch artistview over start mapping song cover art when any are available * refactor useStar to queries fix caching for starred items and album cover art * add hook to reset queries on server change * refactor search to use query * fix song cover art setting * use query for artistInfo * remove last bits of library state * cleanup * use query key factory already fixed one wrong key... * require coverart size * let's try no promise queues on these for now * image cache uses query * perf fix for playlist parsing also use placeholder data so we don't have to deal with staleness * drill that disabled also list controls doesn't need its own songs hook/copy * switch to react-native-blob-util for downloads slightly slower but allows us to use DownloadManager, which backgrounds downloads so they are no longer corrupted when the app suspends * add a fake "top songs" based on artist search then sorted by play count/ratings artistview should load now even if topSongs fails * try not to swap between topSongs/search on refetch set queueContext by song list so the index isn't off if the list changes * add content type validation for file fetching also try to speed up existing file return by limiting fs ops * if the HEAD fails, don't queue the download * clean up params * reimpl clear image cache * precompute contextId prevents wrong "is playing" when any mismatch between queue and list * clear images from all servers use external files dir instead of cache * fix pressable disabled flicker don't retry topsongs on failure try to optimize setqueue and fixcoverart a bit * wait for queries during clear * break out fetchExistingFile from fetchFile allows to tell if file is coming from disk or not only show placeholder/loading spinner if actually fetching image * forgot these wouldn't do anything with objects * remove query cache when switching servers * add content-disposition extension gathering add support for progress hook (needs native support still) * added custom RNBU pkg with progress changes * fully unmount tabs when server changes prevents unwanted requests, gives fresh start on switch fix fixCoverArt not re-rendering in certain cases on search * use serverId from fetch deps * fix lint * update licenses * just use the whole lodash package * make using cache buster optional
196 lines
5.1 KiB
TypeScript
196 lines
5.1 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 { 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 {
|
|
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, isReady, contextId } = useSetQueue('song', [item])
|
|
|
|
return (
|
|
<ListItem
|
|
item={item}
|
|
contextId={contextId}
|
|
queueId={0}
|
|
showArt={true}
|
|
showStar={false}
|
|
onPress={() => setQueue({ title: item.title, playTrack: 0 })}
|
|
disabled={!isReady}
|
|
/>
|
|
)
|
|
}, equal)
|
|
|
|
const ResultsCategory = React.memo<{
|
|
name: string
|
|
query: string
|
|
items: (Artist | Album | Song)[]
|
|
type: 'artist' | 'album' | 'song'
|
|
}>(({ name, query, type, items }) => {
|
|
const navigation = useNavigation()
|
|
|
|
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="More..."
|
|
buttonStyle="hollow"
|
|
style={styles.more}
|
|
onPress={() => navigation.navigate('results', { query, type: items[0].itemType })}
|
|
/>
|
|
)}
|
|
</>
|
|
)
|
|
}, equal)
|
|
|
|
const Results = React.memo<{
|
|
results: SearchResults
|
|
query: string
|
|
}>(({ results, query }) => {
|
|
return (
|
|
<>
|
|
<ResultsCategory name="Artists" query={query} type={'artist'} items={results.artists} />
|
|
<ResultsCategory name="Albums" query={query} type={'album'} items={results.albums} />
|
|
<ResultsCategory name="Songs" query={query} type={'song'} items={results.songs} />
|
|
</>
|
|
)
|
|
}, equal)
|
|
|
|
const Search = () => {
|
|
const [query, setQuery] = useState('')
|
|
const { data, isLoading } = useQuerySearchResults({ query, albumCount: 5, artistCount: 5, songCount: 5 })
|
|
|
|
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="Search"
|
|
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
|