subtracks/app/screens/Search.tsx
austinried 081251061d
Library store refactor (#76)
* start of music store refactor

moving stuff into a state cache
better separate it from view logic

* added paginated list/album list

* reworked fetchAlbumList to remove ui state

refactored home screen to use new method
i broke playing songs somehow, JS thread goes into a loop

* don't reset parts manually, do it all at once

* fixed perf issue related to too many rerenders

rerenders were caused by strict equality check on object/array picks
switched artistInfo to new store
updated zustand and fixed deprecation warnings

* update typescript

and use workspace tsc version for vscode

* remove old artistInfo

* switched to new playlist w/songs

removed more unused stuff

* remove unused + (slightly) rework search

* refactor star

* use only original/large imges for covers/artist

fix view artist from context menu
add loading indicators to song list and artist views (show info we have right away)

* set starred/unstar assuming it works

and correct state on error

* reorg, remove old music slice files

* added back fix for song cover art

* sort artists by localCompare name

* update licenses

* fix now playing background grey bar

* update react-native-gesture-handler

for node-fetch security alert

* fix another gradient height grey bar issue

* update licenses again

* remove thumbnail cache

* rename to remove "Library" from methods

* Revert "remove thumbnail cache"

This reverts commit e0db4931f11bbf4cd8e73102d06505c6ae85f4a6.

* use ids for lists, pull state later

* Revert "use only original/large imges for covers/artist"

This reverts commit c9aea9065ce6ebe3c8b09c10dd74d4de153d76fd.

* deep equal ListItem props for now

this needs a bigger refactor

* use immer as middleware

* refactor api client to use string method

hoping to use this for requestKey/deduping next

* use thumbnails in list items

* Revert "refactor api client to use string method"

This reverts commit 234326135b7af96cb91b941e7ca515f45c632556.

* rename/cleanup

* store servers by id

* get rid of settings selectors

* renames for clarity

remove unused estimateContentLength setting

* remove trackplayer selectors

* fix migration for library filter settings

* fixed shuffle order reporting wrong track/queue

* removed the other selectors

* don't actually need es6/react for our state

* fix slow artist sort on star

localeCompare is too slow for large lists
2022-03-28 13:30:57 +09:00

219 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 { useActiveServerRefresh } from '@app/hooks/settings'
import { Song, Album, Artist, SearchResults } from '@app/models/library'
import { useStore, useStoreDeep } from '@app/state/store'
import colors from '@app/styles/colors'
import font from '@app/styles/font'
import { mapById } from '@app/util/state'
import { useFocusEffect, useNavigation } from '@react-navigation/native'
import debounce from 'lodash.debounce'
import React, { useCallback, useMemo, useRef, useState } from 'react'
import {
ActivityIndicator,
InteractionManager,
ScrollView,
StatusBar,
StyleSheet,
TextInput as ReactTextInput,
View,
} from 'react-native'
const SongItem = React.memo<{ item: Song }>(({ item }) => {
const setQueue = useStore(store => store.setQueue)
return (
<ListItem
item={item}
contextId={item.id}
queueId={0}
showArt={true}
showStar={false}
onPress={() => setQueue([item], item.title, 'song', item.id, 0)}
/>
)
})
const ResultsCategory = React.memo<{
name: string
query: string
ids: string[]
type: 'artist' | 'album' | 'song'
}>(({ name, query, type, ids }) => {
const items: (Album | Artist | Song)[] = useStoreDeep(
useCallback(
store => {
switch (type) {
case 'album':
return mapById(store.library.albums, ids)
case 'artist':
return mapById(store.library.artists, ids)
case 'song':
return mapById(store.library.songs, ids)
default:
return []
}
},
[ids, type],
),
)
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 })}
/>
)}
</>
)
})
const Results = React.memo<{
results: SearchResults
query: string
}>(({ results, query }) => {
return (
<>
<ResultsCategory name="Artists" query={query} type={'artist'} ids={results.artists} />
<ResultsCategory name="Albums" query={query} type={'album'} ids={results.albums} />
<ResultsCategory name="Songs" query={query} type={'song'} ids={results.songs} />
</>
)
})
const Search = () => {
const fetchSearchResults = useStore(store => store.fetchSearchResults)
const [results, setResults] = useState<SearchResults>({ artists: [], albums: [], songs: [] })
const [refreshing, setRefreshing] = useState(false)
const [text, setText] = useState('')
const searchBarRef = useRef<ReactTextInput>(null)
const scrollRef = useRef<ScrollView>(null)
useFocusEffect(
useCallback(() => {
const task = InteractionManager.runAfterInteractions(() => {
setTimeout(() => {
setText('')
setResults({ artists: [], albums: [], songs: [] })
searchBarRef.current?.focus()
scrollRef.current?.scrollTo({ y: 0, animated: true })
}, 50)
})
return () => task.cancel()
}, [searchBarRef, scrollRef]),
)
useActiveServerRefresh(
useCallback(() => {
setText('')
setResults({ artists: [], albums: [], songs: [] })
}, []),
)
const debouncedonUpdateSearch = useMemo(
() =>
debounce(async (query: string) => {
setRefreshing(true)
setResults(await fetchSearchResults({ query, albumCount: 5, artistCount: 5, songCount: 5 }))
setRefreshing(false)
}, 400),
[fetchSearchResults],
)
const onChangeText = useCallback(
(value: string) => {
setText(value)
debouncedonUpdateSearch(value)
},
[setText, debouncedonUpdateSearch],
)
const resultsCount = results.albums.length + results.artists.length + results.songs.length
return (
<GradientScrollView ref={scrollRef} style={styles.scroll} contentContainerStyle={styles.scrollContentContainer}>
<View style={styles.content}>
<View style={styles.inputBar}>
<TextInput
ref={searchBarRef}
style={styles.textInput}
placeholder="Search"
value={text}
onChangeText={onChangeText}
/>
<ActivityIndicator
animating={refreshing}
size="small"
color={colors.text.secondary}
style={styles.activity}
/>
</View>
{resultsCount > 0 ? <Results results={results} query={text} /> : <NothingHere style={styles.noResults} />}
</View>
</GradientScrollView>
)
}
const styles = StyleSheet.create({
scroll: {
flex: 1,
},
scrollContentContainer: {
paddingTop: StatusBar.currentHeight,
},
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