mirror of
https://github.com/austinried/subtracks.git
synced 2026-02-10 23:02:43 +01:00
React Query refactor (#91)
* 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
This commit is contained in:
@@ -5,15 +5,16 @@ import GradientScrollView from '@app/components/GradientScrollView'
|
||||
import Header from '@app/components/Header'
|
||||
import HeaderBar from '@app/components/HeaderBar'
|
||||
import ListItem from '@app/components/ListItem'
|
||||
import { useQueryArtist, useQueryArtistTopSongs } from '@app/hooks/query'
|
||||
import { useSetQueue } from '@app/hooks/trackplayer'
|
||||
import { Album, Song } from '@app/models/library'
|
||||
import { useStore, useStoreDeep } from '@app/state/store'
|
||||
import colors from '@app/styles/colors'
|
||||
import dimensions from '@app/styles/dimensions'
|
||||
import font from '@app/styles/font'
|
||||
import { mapById } from '@app/util/state'
|
||||
import { useLayout } from '@react-native-community/hooks'
|
||||
import { useNavigation } from '@react-navigation/native'
|
||||
import React, { useCallback, useEffect } from 'react'
|
||||
import equal from 'fast-deep-equal/es6/react'
|
||||
import React from 'react'
|
||||
import { ActivityIndicator, StyleSheet, Text, View } from 'react-native'
|
||||
import { useAnimatedScrollHandler, useAnimatedStyle, useSharedValue } from 'react-native-reanimated'
|
||||
|
||||
@@ -31,22 +32,21 @@ const AlbumItem = React.memo<{
|
||||
return (
|
||||
<AlbumContextPressable
|
||||
album={album}
|
||||
onPress={() => navigation.navigate('album', { id: album.id, title: album.name })}
|
||||
onPress={() => navigation.navigate('album', { id: album.id, title: album.name, album })}
|
||||
menuStyle={[styles.albumItem, { width }]}
|
||||
triggerOuterWrapperStyle={{ width }}>
|
||||
<CoverArt type="cover" coverArt={album.coverArt} style={{ height, width }} resizeMode={'cover'} />
|
||||
<CoverArt type="cover" coverArt={album.coverArt} style={{ height, width }} resizeMode="cover" size="thumbnail" />
|
||||
<Text style={styles.albumTitle}>{album.name}</Text>
|
||||
<Text style={styles.albumYear}> {album.year ? album.year : ''}</Text>
|
||||
</AlbumContextPressable>
|
||||
)
|
||||
})
|
||||
}, equal)
|
||||
|
||||
const TopSongs = React.memo<{
|
||||
songs: Song[]
|
||||
name: string
|
||||
artistId: string
|
||||
}>(({ songs, name, artistId }) => {
|
||||
const setQueue = useStore(store => store.setQueue)
|
||||
}>(({ songs, name }) => {
|
||||
const { setQueue, isReady, contextId } = useSetQueue('artist', songs)
|
||||
|
||||
return (
|
||||
<>
|
||||
@@ -55,16 +55,17 @@ const TopSongs = React.memo<{
|
||||
<ListItem
|
||||
key={i}
|
||||
item={s}
|
||||
contextId={artistId}
|
||||
contextId={contextId}
|
||||
queueId={i}
|
||||
showArt={true}
|
||||
subtitle={s.album}
|
||||
onPress={() => setQueue(songs, name, 'artist', artistId, i)}
|
||||
onPress={() => setQueue({ title: name, playTrack: i })}
|
||||
disabled={!isReady}
|
||||
/>
|
||||
))}
|
||||
</>
|
||||
)
|
||||
})
|
||||
}, equal)
|
||||
|
||||
const ArtistAlbums = React.memo<{
|
||||
albums: Album[]
|
||||
@@ -87,7 +88,7 @@ const ArtistAlbums = React.memo<{
|
||||
</View>
|
||||
</>
|
||||
)
|
||||
})
|
||||
}, equal)
|
||||
|
||||
const ArtistViewFallback = React.memo(() => (
|
||||
<GradientBackground style={styles.fallback}>
|
||||
@@ -96,18 +97,8 @@ const ArtistViewFallback = React.memo(() => (
|
||||
))
|
||||
|
||||
const ArtistView = React.memo<{ id: string; title: string }>(({ id, title }) => {
|
||||
const artist = useStoreDeep(useCallback(store => store.library.artists[id], [id]))
|
||||
const topSongIds = useStoreDeep(useCallback(store => store.library.artistNameTopSongs[artist?.name], [artist?.name]))
|
||||
const topSongs = useStoreDeep(
|
||||
useCallback(store => (topSongIds ? mapById(store.library.songs, topSongIds) : undefined), [topSongIds]),
|
||||
)
|
||||
const albumIds = useStoreDeep(useCallback(store => store.library.artistAlbums[id], [id]))
|
||||
const albums = useStoreDeep(
|
||||
useCallback(store => (albumIds ? mapById(store.library.albums, albumIds) : undefined), [albumIds]),
|
||||
)
|
||||
|
||||
const fetchArtist = useStore(store => store.fetchArtist)
|
||||
const fetchTopSongs = useStore(store => store.fetchArtistTopSongs)
|
||||
const { data: artistData } = useQueryArtist(id)
|
||||
const { data: topSongs, isError } = useQueryArtistTopSongs(artistData?.artist?.name)
|
||||
|
||||
const coverLayout = useLayout()
|
||||
const headerOpacity = useSharedValue(0)
|
||||
@@ -124,22 +115,12 @@ const ArtistView = React.memo<{ id: string; title: string }>(({ id, title }) =>
|
||||
}
|
||||
})
|
||||
|
||||
useEffect(() => {
|
||||
if (!artist || !albumIds) {
|
||||
fetchArtist(id)
|
||||
}
|
||||
}, [artist, albumIds, fetchArtist, id])
|
||||
|
||||
useEffect(() => {
|
||||
if (artist && !topSongIds) {
|
||||
fetchTopSongs(artist.name)
|
||||
}
|
||||
}, [artist, fetchTopSongs, topSongIds])
|
||||
|
||||
if (!artist) {
|
||||
if (!artistData) {
|
||||
return <ArtistViewFallback />
|
||||
}
|
||||
|
||||
const { artist, albums } = artistData
|
||||
|
||||
return (
|
||||
<View style={styles.container}>
|
||||
<HeaderBar title={title} headerStyle={[styles.header, animatedOpacity]} />
|
||||
@@ -149,15 +130,15 @@ const ArtistView = React.memo<{ id: string; title: string }>(({ id, title }) =>
|
||||
style={styles.scroll}
|
||||
contentContainerStyle={styles.scrollContent}
|
||||
onScroll={onScroll}>
|
||||
<CoverArt type="artist" size="original" artistId={artist.id} style={styles.artistCover} resizeMode={'cover'} />
|
||||
<CoverArt type="artist" size="original" artistId={artist.id} style={styles.artistCover} resizeMode="cover" />
|
||||
<View style={styles.titleContainer}>
|
||||
<Text style={styles.title}>{artist.name}</Text>
|
||||
</View>
|
||||
<View style={styles.contentContainer}>
|
||||
{topSongs && albums ? (
|
||||
topSongs.length > 0 ? (
|
||||
{(topSongs || isError) && artist ? (
|
||||
topSongs && topSongs.length > 0 ? (
|
||||
<>
|
||||
<TopSongs songs={topSongs} name={artist.name} artistId={artist.id} />
|
||||
<TopSongs songs={topSongs} name={artist.name} />
|
||||
<ArtistAlbums albums={albums} />
|
||||
</>
|
||||
) : (
|
||||
|
||||
@@ -3,18 +3,17 @@ import CoverArt from '@app/components/CoverArt'
|
||||
import GradientScrollView from '@app/components/GradientScrollView'
|
||||
import Header from '@app/components/Header'
|
||||
import NothingHere from '@app/components/NothingHere'
|
||||
import { useActiveServerRefresh } from '@app/hooks/settings'
|
||||
import { useStore, useStoreDeep } from '@app/state/store'
|
||||
import { useQueryHomeLists } from '@app/hooks/query'
|
||||
import { Album } from '@app/models/library'
|
||||
import { useStoreDeep } from '@app/state/store'
|
||||
import colors from '@app/styles/colors'
|
||||
import font from '@app/styles/font'
|
||||
import { GetAlbumList2TypeBase, GetAlbumListType } from '@app/subsonic/params'
|
||||
import { useNavigation } from '@react-navigation/native'
|
||||
import equal from 'fast-deep-equal/es6/react'
|
||||
import produce from 'immer'
|
||||
import React, { useCallback, useState } from 'react'
|
||||
import React from 'react'
|
||||
import { RefreshControl, ScrollView, StyleSheet, Text, View } from 'react-native'
|
||||
import { useSafeAreaInsets } from 'react-native-safe-area-context'
|
||||
import create, { StateSelector } from 'zustand'
|
||||
|
||||
const titles: { [key in GetAlbumListType]?: string } = {
|
||||
recent: 'Recently Played',
|
||||
@@ -24,25 +23,21 @@ const titles: { [key in GetAlbumListType]?: string } = {
|
||||
}
|
||||
|
||||
const AlbumItem = React.memo<{
|
||||
id: string
|
||||
}>(({ id }) => {
|
||||
album: Album
|
||||
}>(({ album }) => {
|
||||
const navigation = useNavigation()
|
||||
const album = useStoreDeep(useCallback(store => store.library.albums[id], [id]))
|
||||
|
||||
if (!album) {
|
||||
return <></>
|
||||
}
|
||||
|
||||
return (
|
||||
<AlbumContextPressable
|
||||
album={album}
|
||||
triggerWrapperStyle={styles.item}
|
||||
onPress={() => navigation.navigate('album', { id: album.id, title: album.name })}>
|
||||
onPress={() => navigation.navigate('album', { id: album.id, title: album.name, album })}>
|
||||
<CoverArt
|
||||
type="cover"
|
||||
coverArt={album.coverArt}
|
||||
style={{ height: styles.item.width, width: styles.item.width }}
|
||||
resizeMode={'cover'}
|
||||
resizeMode="cover"
|
||||
size="thumbnail"
|
||||
/>
|
||||
<Text style={styles.title} numberOfLines={1}>
|
||||
{album.name}
|
||||
@@ -52,13 +47,12 @@ const AlbumItem = React.memo<{
|
||||
</Text>
|
||||
</AlbumContextPressable>
|
||||
)
|
||||
})
|
||||
}, equal)
|
||||
|
||||
const Category = React.memo<{
|
||||
type: string
|
||||
}>(({ type }) => {
|
||||
const list = useHomeStoreDeep(useCallback(store => store.lists[type] || [], [type]))
|
||||
|
||||
albums: Album[]
|
||||
}>(({ type, albums }) => {
|
||||
const Albums = () => (
|
||||
<ScrollView
|
||||
horizontal={true}
|
||||
@@ -66,8 +60,8 @@ const Category = React.memo<{
|
||||
overScrollMode={'never'}
|
||||
style={styles.artScroll}
|
||||
contentContainerStyle={styles.artScrollContent}>
|
||||
{list.map(id => (
|
||||
<AlbumItem key={id} id={id} />
|
||||
{albums.map(a => (
|
||||
<AlbumItem key={a.id} album={a} />
|
||||
))}
|
||||
</ScrollView>
|
||||
)
|
||||
@@ -81,75 +75,33 @@ const Category = React.memo<{
|
||||
return (
|
||||
<View style={styles.category}>
|
||||
<Header style={styles.header}>{titles[type as GetAlbumListType] || ''}</Header>
|
||||
{list.length > 0 ? <Albums /> : <Nothing />}
|
||||
{albums.length > 0 ? <Albums /> : <Nothing />}
|
||||
</View>
|
||||
)
|
||||
})
|
||||
|
||||
interface HomeState {
|
||||
lists: { [type: string]: string[] }
|
||||
setList: (type: string, list: string[]) => void
|
||||
}
|
||||
|
||||
const useHomeStore = create<HomeState>(set => ({
|
||||
lists: {},
|
||||
|
||||
setList: (type, list) => {
|
||||
set(
|
||||
produce<HomeState>(state => {
|
||||
state.lists[type] = list
|
||||
}),
|
||||
)
|
||||
},
|
||||
}))
|
||||
|
||||
function useHomeStoreDeep<U>(stateSelector: StateSelector<HomeState, U>) {
|
||||
return useHomeStore(stateSelector, equal)
|
||||
}
|
||||
}, equal)
|
||||
|
||||
const Home = () => {
|
||||
const [refreshing, setRefreshing] = useState(false)
|
||||
const types = useStoreDeep(store => store.settings.screens.home.listTypes)
|
||||
const fetchAlbumList = useStore(store => store.fetchAlbumList)
|
||||
const setList = useHomeStore(store => store.setList)
|
||||
const listQueries = useQueryHomeLists(types as GetAlbumList2TypeBase[], 20)
|
||||
const paddingTop = useSafeAreaInsets().top
|
||||
|
||||
const refresh = useCallback(async () => {
|
||||
setRefreshing(true)
|
||||
|
||||
await Promise.all(
|
||||
types.map(async type => {
|
||||
const ids = await fetchAlbumList({ type: type as GetAlbumList2TypeBase, size: 20, offset: 0 })
|
||||
setList(type, ids)
|
||||
}),
|
||||
)
|
||||
|
||||
setRefreshing(false)
|
||||
}, [fetchAlbumList, setList, types])
|
||||
|
||||
useActiveServerRefresh(
|
||||
useCallback(() => {
|
||||
types.forEach(type => setList(type, []))
|
||||
refresh()
|
||||
}, [refresh, setList, types]),
|
||||
)
|
||||
|
||||
return (
|
||||
<GradientScrollView
|
||||
style={styles.scroll}
|
||||
contentContainerStyle={{ paddingTop }}
|
||||
refreshControl={
|
||||
<RefreshControl
|
||||
refreshing={refreshing}
|
||||
onRefresh={refresh}
|
||||
refreshing={listQueries.some(q => q.isLoading)}
|
||||
onRefresh={() => listQueries.forEach(q => q.refetch())}
|
||||
colors={[colors.accent, colors.accentLow]}
|
||||
progressViewOffset={paddingTop}
|
||||
/>
|
||||
}>
|
||||
<View style={styles.content}>
|
||||
{types.map(type => (
|
||||
<Category key={type} type={type} />
|
||||
))}
|
||||
{types.map(type => {
|
||||
const query = listQueries.find(list => list.data?.type === type)
|
||||
return <Category key={type} type={type} albums={query?.data?.albums || []} />
|
||||
})}
|
||||
</View>
|
||||
</GradientScrollView>
|
||||
)
|
||||
|
||||
@@ -2,21 +2,22 @@ import { AlbumContextPressable } from '@app/components/ContextMenu'
|
||||
import CoverArt from '@app/components/CoverArt'
|
||||
import FilterButton, { OptionData } from '@app/components/FilterButton'
|
||||
import GradientFlatList from '@app/components/GradientFlatList'
|
||||
import { useFetchPaginatedList } from '@app/hooks/list'
|
||||
import { useQueryAlbumList } from '@app/hooks/query'
|
||||
import { Album } from '@app/models/library'
|
||||
import { useStore, useStoreDeep } from '@app/state/store'
|
||||
import colors from '@app/styles/colors'
|
||||
import font from '@app/styles/font'
|
||||
import { GetAlbumList2Params, GetAlbumList2Type } from '@app/subsonic/params'
|
||||
import { GetAlbumList2Type, GetAlbumList2TypeBase } from '@app/subsonic/params'
|
||||
import { useNavigation } from '@react-navigation/native'
|
||||
import React, { useCallback } from 'react'
|
||||
import equal from 'fast-deep-equal/es6/react'
|
||||
import React from 'react'
|
||||
import { StyleSheet, Text, useWindowDimensions, View } from 'react-native'
|
||||
|
||||
const AlbumItem = React.memo<{
|
||||
id: string
|
||||
album: Album
|
||||
size: number
|
||||
height: number
|
||||
}>(({ id, size, height }) => {
|
||||
const album = useStoreDeep(useCallback(store => store.library.albums[id], [id]))
|
||||
}>(({ album, size, height }) => {
|
||||
const navigation = useNavigation()
|
||||
|
||||
if (!album) {
|
||||
@@ -28,8 +29,14 @@ const AlbumItem = React.memo<{
|
||||
album={album}
|
||||
menuStyle={[styles.itemMenu, { width: size }]}
|
||||
triggerWrapperStyle={[styles.itemWrapper, { height }]}
|
||||
onPress={() => navigation.navigate('album', { id: album.id, title: album.name })}>
|
||||
<CoverArt type="cover" coverArt={album.coverArt} style={{ height: size, width: size }} resizeMode={'cover'} />
|
||||
onPress={() => navigation.navigate('album', { id: album.id, title: album.name, album })}>
|
||||
<CoverArt
|
||||
type="cover"
|
||||
coverArt={album.coverArt}
|
||||
style={{ height: size, width: size }}
|
||||
resizeMode="cover"
|
||||
size="thumbnail"
|
||||
/>
|
||||
<View style={styles.itemDetails}>
|
||||
<Text style={styles.title} numberOfLines={1}>
|
||||
{album.name}
|
||||
@@ -40,11 +47,11 @@ const AlbumItem = React.memo<{
|
||||
</View>
|
||||
</AlbumContextPressable>
|
||||
)
|
||||
})
|
||||
}, equal)
|
||||
|
||||
const AlbumListRenderItem: React.FC<{
|
||||
item: { id: string; size: number; height: number }
|
||||
}> = ({ item }) => <AlbumItem id={item.id} size={item.size} height={item.height} />
|
||||
item: { album: Album; size: number; height: number }
|
||||
}> = ({ item }) => <AlbumItem album={item.album} size={item.size} height={item.height} />
|
||||
|
||||
const filterOptions: OptionData[] = [
|
||||
{ text: 'By Name', value: 'alphabeticalByName' },
|
||||
@@ -62,42 +69,7 @@ const AlbumsList = () => {
|
||||
const filter = useStoreDeep(store => store.settings.screens.library.albumsFilter)
|
||||
const setFilter = useStore(store => store.setLibraryAlbumFilter)
|
||||
|
||||
const fetchAlbumList = useStore(store => store.fetchAlbumList)
|
||||
const fetchPage = useCallback(
|
||||
(size: number, offset: number) => {
|
||||
let params: GetAlbumList2Params
|
||||
switch (filter.type) {
|
||||
case 'byYear':
|
||||
params = {
|
||||
size,
|
||||
offset,
|
||||
type: filter.type,
|
||||
fromYear: filter.fromYear,
|
||||
toYear: filter.toYear,
|
||||
}
|
||||
break
|
||||
case 'byGenre':
|
||||
params = {
|
||||
size,
|
||||
offset,
|
||||
type: filter.type,
|
||||
genre: filter.genre,
|
||||
}
|
||||
break
|
||||
default:
|
||||
params = {
|
||||
size,
|
||||
offset,
|
||||
type: filter.type,
|
||||
}
|
||||
break
|
||||
}
|
||||
return fetchAlbumList(params)
|
||||
},
|
||||
[fetchAlbumList, filter.fromYear, filter.genre, filter.toYear, filter.type],
|
||||
)
|
||||
|
||||
const { list, refreshing, refresh, fetchNextPage } = useFetchPaginatedList(fetchPage, 300)
|
||||
const { isLoading, data, fetchNextPage, refetch } = useQueryAlbumList(filter.type as GetAlbumList2TypeBase, 300)
|
||||
|
||||
const layout = useWindowDimensions()
|
||||
|
||||
@@ -107,15 +79,15 @@ const AlbumsList = () => {
|
||||
return (
|
||||
<View style={styles.container}>
|
||||
<GradientFlatList
|
||||
data={list.map(id => ({ id, size, height }))}
|
||||
data={data ? data.pages.flatMap(albums => albums.map(album => ({ album, size, height }))) : []}
|
||||
renderItem={AlbumListRenderItem}
|
||||
keyExtractor={item => item.id}
|
||||
keyExtractor={item => item.album.id}
|
||||
numColumns={3}
|
||||
removeClippedSubviews={true}
|
||||
refreshing={refreshing}
|
||||
onRefresh={refresh}
|
||||
refreshing={isLoading}
|
||||
onRefresh={refetch}
|
||||
overScrollMode="never"
|
||||
onEndReached={fetchNextPage}
|
||||
onEndReached={() => fetchNextPage()}
|
||||
onEndReachedThreshold={6}
|
||||
windowSize={5}
|
||||
/>
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import FilterButton, { OptionData } from '@app/components/FilterButton'
|
||||
import GradientFlatList from '@app/components/GradientFlatList'
|
||||
import ListItem from '@app/components/ListItem'
|
||||
import { useFetchList2 } from '@app/hooks/list'
|
||||
import { useQueryArtists } from '@app/hooks/query'
|
||||
import { Artist } from '@app/models/library'
|
||||
import { ArtistFilterType } from '@app/models/settings'
|
||||
import { useStore, useStoreDeep } from '@app/state/store'
|
||||
@@ -19,17 +19,19 @@ const filterOptions: OptionData[] = [
|
||||
]
|
||||
|
||||
const ArtistsList = () => {
|
||||
const fetchArtists = useStore(store => store.fetchArtists)
|
||||
const { refreshing, refresh } = useFetchList2(fetchArtists)
|
||||
const artists = useStoreDeep(store => store.library.artists)
|
||||
const artistOrder = useStoreDeep(store => store.library.artistOrder)
|
||||
|
||||
const filter = useStoreDeep(store => store.settings.screens.library.artistsFilter)
|
||||
const setFilter = useStore(store => store.setLibraryArtistFiler)
|
||||
|
||||
const { isLoading, data, refetch } = useQueryArtists()
|
||||
const [sortedList, setSortedList] = useState<Artist[]>([])
|
||||
|
||||
useEffect(() => {
|
||||
const list = Object.values(artists)
|
||||
if (!data) {
|
||||
setSortedList([])
|
||||
return
|
||||
}
|
||||
|
||||
const list = Object.values(data.byId)
|
||||
switch (filter.type) {
|
||||
case 'random':
|
||||
setSortedList([...list].sort(() => Math.random() - 0.5))
|
||||
@@ -38,13 +40,13 @@ const ArtistsList = () => {
|
||||
setSortedList([...list].filter(a => a.starred))
|
||||
break
|
||||
case 'alphabeticalByName':
|
||||
setSortedList(artistOrder.map(id => artists[id]))
|
||||
setSortedList(data.allIds.map(id => data.byId[id]))
|
||||
break
|
||||
default:
|
||||
setSortedList([...list])
|
||||
break
|
||||
}
|
||||
}, [filter.type, artists, artistOrder])
|
||||
}, [filter.type, data])
|
||||
|
||||
return (
|
||||
<View style={styles.container}>
|
||||
@@ -52,8 +54,8 @@ const ArtistsList = () => {
|
||||
data={sortedList}
|
||||
renderItem={ArtistRenderItem}
|
||||
keyExtractor={item => item.id}
|
||||
onRefresh={refresh}
|
||||
refreshing={refreshing}
|
||||
onRefresh={refetch}
|
||||
refreshing={isLoading}
|
||||
overScrollMode="never"
|
||||
windowSize={3}
|
||||
contentMarginTop={6}
|
||||
|
||||
@@ -1,8 +1,8 @@
|
||||
import GradientFlatList from '@app/components/GradientFlatList'
|
||||
import ListItem from '@app/components/ListItem'
|
||||
import { useFetchList2 } from '@app/hooks/list'
|
||||
import { useQueryPlaylists } from '@app/hooks/query'
|
||||
import { Playlist } from '@app/models/library'
|
||||
import { useStore, useStoreDeep } from '@app/state/store'
|
||||
import { mapById } from '@app/util/state'
|
||||
import React from 'react'
|
||||
import { StyleSheet } from 'react-native'
|
||||
|
||||
@@ -11,17 +11,15 @@ const PlaylistRenderItem: React.FC<{ item: Playlist }> = ({ item }) => (
|
||||
)
|
||||
|
||||
const PlaylistsList = () => {
|
||||
const fetchPlaylists = useStore(store => store.fetchPlaylists)
|
||||
const { refreshing, refresh } = useFetchList2(fetchPlaylists)
|
||||
const playlists = useStoreDeep(store => store.library.playlists)
|
||||
const { isLoading, data, refetch } = useQueryPlaylists()
|
||||
|
||||
return (
|
||||
<GradientFlatList
|
||||
data={Object.values(playlists)}
|
||||
data={data ? mapById(data?.byId, data?.allIds) : []}
|
||||
renderItem={PlaylistRenderItem}
|
||||
keyExtractor={item => item.id}
|
||||
onRefresh={refresh}
|
||||
refreshing={refreshing}
|
||||
onRefresh={refetch}
|
||||
refreshing={isLoading}
|
||||
overScrollMode="never"
|
||||
windowSize={5}
|
||||
contentMarginTop={6}
|
||||
|
||||
@@ -3,7 +3,8 @@ import ListItem from '@app/components/ListItem'
|
||||
import NowPlayingBar from '@app/components/NowPlayingBar'
|
||||
import { useSkipTo } from '@app/hooks/trackplayer'
|
||||
import { Song } from '@app/models/library'
|
||||
import { useStore, useStoreDeep } from '@app/state/store'
|
||||
import { mapTrackExtToSong } from '@app/models/map'
|
||||
import { useStoreDeep } from '@app/state/store'
|
||||
import React from 'react'
|
||||
import { StyleSheet, View } from 'react-native'
|
||||
|
||||
@@ -26,7 +27,6 @@ const SongRenderItem: React.FC<{
|
||||
|
||||
const NowPlayingQueue = React.memo<{}>(() => {
|
||||
const queue = useStoreDeep(store => store.queue)
|
||||
const mapTrackExtToSong = useStore(store => store.mapTrackExtToSong)
|
||||
const skipTo = useSkipTo()
|
||||
|
||||
return (
|
||||
|
||||
@@ -4,6 +4,7 @@ import ImageGradientBackground from '@app/components/ImageGradientBackground'
|
||||
import PressableOpacity from '@app/components/PressableOpacity'
|
||||
import { PressableStar } from '@app/components/Star'
|
||||
import { useNext, usePause, usePlay, usePrevious, useSeekTo } from '@app/hooks/trackplayer'
|
||||
import { mapTrackExtToSong } from '@app/models/map'
|
||||
import { QueueContextType, TrackExt } from '@app/models/trackplayer'
|
||||
import { useStore, useStoreDeep } from '@app/state/store'
|
||||
import colors from '@app/styles/colors'
|
||||
@@ -40,7 +41,6 @@ const NowPlayingHeader = React.memo<{
|
||||
}>(({ track }) => {
|
||||
const queueName = useStore(store => store.queueName)
|
||||
const queueContextType = useStore(store => store.queueContextType)
|
||||
const mapTrackExtToSong = useStore(store => store.mapTrackExtToSong)
|
||||
|
||||
if (!track) {
|
||||
return <></>
|
||||
|
||||
@@ -4,14 +4,14 @@ 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 { useQuerySearchResults } from '@app/hooks/query'
|
||||
import { useSetQueue } from '@app/hooks/trackplayer'
|
||||
import { Album, Artist, SearchResults, Song } 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 equal from 'fast-deep-equal/es6/react'
|
||||
import _ from 'lodash'
|
||||
import React, { useCallback, useMemo, useRef, useState } from 'react'
|
||||
import {
|
||||
ActivityIndicator,
|
||||
@@ -24,44 +24,27 @@ import {
|
||||
import { useSafeAreaInsets } from 'react-native-safe-area-context'
|
||||
|
||||
const SongItem = React.memo<{ item: Song }>(({ item }) => {
|
||||
const setQueue = useStore(store => store.setQueue)
|
||||
const { setQueue, isReady, contextId } = useSetQueue('song', [item])
|
||||
|
||||
return (
|
||||
<ListItem
|
||||
item={item}
|
||||
contextId={item.id}
|
||||
contextId={contextId}
|
||||
queueId={0}
|
||||
showArt={true}
|
||||
showStar={false}
|
||||
onPress={() => setQueue([item], item.title, 'song', item.id, 0)}
|
||||
onPress={() => setQueue({ title: item.title, playTrack: 0 })}
|
||||
disabled={!isReady}
|
||||
/>
|
||||
)
|
||||
})
|
||||
}, equal)
|
||||
|
||||
const ResultsCategory = React.memo<{
|
||||
name: string
|
||||
query: string
|
||||
ids: string[]
|
||||
items: (Artist | Album | Song)[]
|
||||
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],
|
||||
),
|
||||
)
|
||||
|
||||
}>(({ name, query, type, items }) => {
|
||||
const navigation = useNavigation()
|
||||
|
||||
if (items.length === 0) {
|
||||
@@ -88,7 +71,7 @@ const ResultsCategory = React.memo<{
|
||||
)}
|
||||
</>
|
||||
)
|
||||
})
|
||||
}, equal)
|
||||
|
||||
const Results = React.memo<{
|
||||
results: SearchResults
|
||||
@@ -96,17 +79,17 @@ const Results = React.memo<{
|
||||
}>(({ 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} />
|
||||
<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 fetchSearchResults = useStore(store => store.fetchSearchResults)
|
||||
const [results, setResults] = useState<SearchResults>({ artists: [], albums: [], songs: [] })
|
||||
const [refreshing, setRefreshing] = useState(false)
|
||||
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)
|
||||
@@ -116,42 +99,39 @@ const Search = () => {
|
||||
useCallback(() => {
|
||||
const task = InteractionManager.runAfterInteractions(() => {
|
||||
setTimeout(() => {
|
||||
if (text) {
|
||||
return
|
||||
}
|
||||
setText('')
|
||||
setResults({ artists: [], albums: [], songs: [] })
|
||||
setQuery('')
|
||||
searchBarRef.current?.focus()
|
||||
scrollRef.current?.scrollTo({ y: 0, animated: true })
|
||||
}, 50)
|
||||
})
|
||||
return () => task.cancel()
|
||||
}, [searchBarRef, scrollRef]),
|
||||
}, [text]),
|
||||
)
|
||||
|
||||
useActiveServerRefresh(
|
||||
useCallback(() => {
|
||||
setText('')
|
||||
setResults({ artists: [], albums: [], songs: [] })
|
||||
}, []),
|
||||
)
|
||||
|
||||
const debouncedonUpdateSearch = useMemo(
|
||||
const debouncedSetQuery = useMemo(
|
||||
() =>
|
||||
debounce(async (query: string) => {
|
||||
setRefreshing(true)
|
||||
setResults(await fetchSearchResults({ query, albumCount: 5, artistCount: 5, songCount: 5 }))
|
||||
setRefreshing(false)
|
||||
_.debounce((value: string) => {
|
||||
setQuery(value)
|
||||
}, 400),
|
||||
[fetchSearchResults],
|
||||
[],
|
||||
)
|
||||
|
||||
const onChangeText = useCallback(
|
||||
(value: string) => {
|
||||
setText(value)
|
||||
debouncedonUpdateSearch(value)
|
||||
debouncedSetQuery(value)
|
||||
},
|
||||
[setText, debouncedonUpdateSearch],
|
||||
[setText, debouncedSetQuery],
|
||||
)
|
||||
|
||||
const resultsCount = results.albums.length + results.artists.length + results.songs.length
|
||||
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 }}>
|
||||
@@ -164,14 +144,13 @@ const Search = () => {
|
||||
value={text}
|
||||
onChangeText={onChangeText}
|
||||
/>
|
||||
<ActivityIndicator
|
||||
animating={refreshing}
|
||||
size="small"
|
||||
color={colors.text.secondary}
|
||||
style={styles.activity}
|
||||
/>
|
||||
<ActivityIndicator animating={isLoading} size="small" color={colors.text.secondary} style={styles.activity} />
|
||||
</View>
|
||||
{resultsCount > 0 ? <Results results={results} query={text} /> : <NothingHere style={styles.noResults} />}
|
||||
{data !== undefined && resultsCount > 0 ? (
|
||||
<Results results={data.pages[0]} query={text} />
|
||||
) : (
|
||||
<NothingHere style={styles.noResults} />
|
||||
)}
|
||||
</View>
|
||||
</GradientScrollView>
|
||||
)
|
||||
|
||||
@@ -1,24 +1,34 @@
|
||||
import GradientFlatList from '@app/components/GradientFlatList'
|
||||
import ListItem from '@app/components/ListItem'
|
||||
import { useFetchPaginatedList } from '@app/hooks/list'
|
||||
import { useQuerySearchResults } from '@app/hooks/query'
|
||||
import { useSetQueue } from '@app/hooks/trackplayer'
|
||||
import { Album, Artist, Song } from '@app/models/library'
|
||||
import { useStore, useStoreDeep } from '@app/state/store'
|
||||
import { Search3Params } from '@app/subsonic/params'
|
||||
import { mapById } from '@app/util/state'
|
||||
import { useNavigation } from '@react-navigation/native'
|
||||
import React, { useCallback, useEffect } from 'react'
|
||||
import React, { useEffect } from 'react'
|
||||
import { StyleSheet } from 'react-native'
|
||||
|
||||
type SearchListItemType = Album | Song | Artist
|
||||
|
||||
const ResultsListItem: React.FC<{ item: SearchListItemType }> = ({ item }) => {
|
||||
const setQueue = useStore(store => store.setQueue)
|
||||
const SongResultsListItem: React.FC<{ item: Song }> = ({ item }) => {
|
||||
const { setQueue, isReady, contextId } = useSetQueue('song', [item])
|
||||
|
||||
let onPress
|
||||
if (item.itemType === 'song') {
|
||||
onPress = () => setQueue([item], item.title, 'song', item.id, 0)
|
||||
}
|
||||
return (
|
||||
<ListItem
|
||||
item={item}
|
||||
contextId={contextId}
|
||||
queueId={0}
|
||||
showArt={true}
|
||||
showStar={false}
|
||||
listStyle="small"
|
||||
onPress={() => setQueue({ title: item.title, playTrack: 0 })}
|
||||
style={styles.listItem}
|
||||
disabled={!isReady}
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
||||
const OtherResultsListItem: React.FC<{ item: SearchListItemType }> = ({ item }) => {
|
||||
return (
|
||||
<ListItem
|
||||
item={item}
|
||||
@@ -27,12 +37,19 @@ const ResultsListItem: React.FC<{ item: SearchListItemType }> = ({ item }) => {
|
||||
showArt={true}
|
||||
showStar={false}
|
||||
listStyle="small"
|
||||
onPress={onPress}
|
||||
style={styles.listItem}
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
||||
const ResultsListItem: React.FC<{ item: SearchListItemType }> = ({ item }) => {
|
||||
if (item.itemType === 'song') {
|
||||
return <SongResultsListItem item={item} />
|
||||
} else {
|
||||
return <OtherResultsListItem item={item} />
|
||||
}
|
||||
}
|
||||
|
||||
const SearchResultsRenderItem: React.FC<{ item: SearchListItemType }> = ({ item }) => <ResultsListItem item={item} />
|
||||
|
||||
const SearchResultsView: React.FC<{
|
||||
@@ -40,61 +57,28 @@ const SearchResultsView: React.FC<{
|
||||
type: 'album' | 'artist' | 'song'
|
||||
}> = ({ query, type }) => {
|
||||
const navigation = useNavigation()
|
||||
const fetchSearchResults = useStore(store => store.fetchSearchResults)
|
||||
const { list, refreshing, refresh, fetchNextPage } = useFetchPaginatedList(
|
||||
useCallback(
|
||||
async (size, offset) => {
|
||||
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 results = await fetchSearchResults(params)
|
||||
const size = 100
|
||||
const params: Search3Params = { query }
|
||||
|
||||
switch (type) {
|
||||
case 'album':
|
||||
return results.albums
|
||||
case 'artist':
|
||||
return results.artists
|
||||
case 'song':
|
||||
return results.songs
|
||||
default:
|
||||
return []
|
||||
}
|
||||
},
|
||||
[fetchSearchResults, query, type],
|
||||
),
|
||||
100,
|
||||
)
|
||||
if (type === 'album') {
|
||||
params.albumCount = size
|
||||
} else if (type === 'artist') {
|
||||
params.artistCount = size
|
||||
} else {
|
||||
params.songCount = size
|
||||
}
|
||||
|
||||
const items: SearchListItemType[] = useStoreDeep(
|
||||
useCallback(
|
||||
store => {
|
||||
switch (type) {
|
||||
case 'album':
|
||||
return mapById(store.library.albums, list)
|
||||
case 'artist':
|
||||
return mapById(store.library.artists, list)
|
||||
case 'song':
|
||||
return mapById(store.library.songs, list)
|
||||
default:
|
||||
return []
|
||||
}
|
||||
},
|
||||
[list, type],
|
||||
),
|
||||
)
|
||||
const { data, isLoading, refetch, fetchNextPage } = useQuerySearchResults(params)
|
||||
|
||||
const items: (Artist | Album | Song)[] = []
|
||||
if (type === 'album') {
|
||||
data && items.push(...data.pages.flatMap(p => p.albums))
|
||||
} else if (type === 'artist') {
|
||||
data && items.push(...data.pages.flatMap(p => p.artists))
|
||||
} else {
|
||||
data && items.push(...data.pages.flatMap(p => p.songs))
|
||||
}
|
||||
|
||||
useEffect(() => {
|
||||
navigation.setOptions({
|
||||
@@ -108,10 +92,10 @@ const SearchResultsView: React.FC<{
|
||||
data={items}
|
||||
renderItem={SearchResultsRenderItem}
|
||||
keyExtractor={(item, i) => i.toString()}
|
||||
onRefresh={refresh}
|
||||
refreshing={refreshing}
|
||||
onRefresh={refetch}
|
||||
refreshing={isLoading}
|
||||
overScrollMode="never"
|
||||
onEndReached={fetchNextPage}
|
||||
onEndReached={() => fetchNextPage}
|
||||
removeClippedSubviews={true}
|
||||
onEndReachedThreshold={2}
|
||||
contentMarginTop={6}
|
||||
|
||||
@@ -35,8 +35,6 @@ const ServerView: React.FC<{
|
||||
)
|
||||
|
||||
const [testing, setTesting] = useState(false)
|
||||
const [removing, setRemoving] = useState(false)
|
||||
const [saving, setSaving] = useState(false)
|
||||
|
||||
const validate = useCallback(() => {
|
||||
return !!address && !!username && !!password
|
||||
@@ -57,7 +55,7 @@ const ServerView: React.FC<{
|
||||
const createServer = useCallback<() => Server>(() => {
|
||||
if (usePlainPassword) {
|
||||
return {
|
||||
id: server?.id || (uuid.v4() as string),
|
||||
id: server?.id || '',
|
||||
usePlainPassword,
|
||||
plainPassword: password,
|
||||
address,
|
||||
@@ -77,7 +75,7 @@ const ServerView: React.FC<{
|
||||
}
|
||||
|
||||
return {
|
||||
id: server?.id || (uuid.v4() as string),
|
||||
id: server?.id || '',
|
||||
address,
|
||||
username,
|
||||
usePlainPassword,
|
||||
@@ -91,22 +89,15 @@ const ServerView: React.FC<{
|
||||
return
|
||||
}
|
||||
|
||||
setSaving(true)
|
||||
const update = createServer()
|
||||
|
||||
const waitForSave = async () => {
|
||||
try {
|
||||
if (id) {
|
||||
updateServer(update)
|
||||
} else {
|
||||
await addServer(update)
|
||||
}
|
||||
exit()
|
||||
} catch (err) {
|
||||
setSaving(false)
|
||||
}
|
||||
if (id) {
|
||||
updateServer(update)
|
||||
} else {
|
||||
addServer(update)
|
||||
}
|
||||
waitForSave()
|
||||
|
||||
exit()
|
||||
}, [addServer, createServer, exit, id, updateServer, validate])
|
||||
|
||||
const remove = useCallback(() => {
|
||||
@@ -114,16 +105,8 @@ const ServerView: React.FC<{
|
||||
return
|
||||
}
|
||||
|
||||
setRemoving(true)
|
||||
const waitForRemove = async () => {
|
||||
try {
|
||||
await removeServer(id as string)
|
||||
exit()
|
||||
} catch (err) {
|
||||
setRemoving(false)
|
||||
}
|
||||
}
|
||||
waitForRemove()
|
||||
removeServer(id as string)
|
||||
exit()
|
||||
}, [canRemove, exit, id, removeServer])
|
||||
|
||||
const togglePlainPassword = useCallback(
|
||||
@@ -162,8 +145,8 @@ const ServerView: React.FC<{
|
||||
}, [createServer, pingServer])
|
||||
|
||||
const disableControls = useCallback(() => {
|
||||
return !validate() || testing || removing || saving
|
||||
}, [validate, testing, removing, saving])
|
||||
return !validate() || testing
|
||||
}, [validate, testing])
|
||||
|
||||
const formatAddress = useCallback(() => {
|
||||
let addressFormatted = address.trim()
|
||||
|
||||
@@ -5,7 +5,7 @@ import PressableOpacity from '@app/components/PressableOpacity'
|
||||
import SettingsItem from '@app/components/SettingsItem'
|
||||
import SettingsSwitch from '@app/components/SettingsSwitch'
|
||||
import TextInput from '@app/components/TextInput'
|
||||
import { useSwitchActiveServer } from '@app/hooks/settings'
|
||||
import { useSwitchActiveServer, useResetImageCache } from '@app/hooks/settings'
|
||||
import { Server } from '@app/models/settings'
|
||||
import { useStore, useStoreDeep } from '@app/state/store'
|
||||
import colors from '@app/styles/colors'
|
||||
@@ -207,25 +207,16 @@ const SettingsContent = React.memo(() => {
|
||||
const maxBuffer = useStore(store => store.settings.maxBuffer)
|
||||
const setMaxBuffer = useStore(store => store.setMaxBuffer)
|
||||
|
||||
const clearImageCache = useStore(store => store.clearImageCache)
|
||||
const [clearing, setClearing] = useState(false)
|
||||
const resetImageCache = useResetImageCache()
|
||||
|
||||
const navigation = useNavigation()
|
||||
|
||||
const clear = useCallback(() => {
|
||||
const clear = useCallback(async () => {
|
||||
setClearing(true)
|
||||
|
||||
const waitForClear = async () => {
|
||||
try {
|
||||
await clearImageCache()
|
||||
} catch (err) {
|
||||
console.log(err)
|
||||
} finally {
|
||||
setClearing(false)
|
||||
}
|
||||
}
|
||||
waitForClear()
|
||||
}, [clearImageCache])
|
||||
await resetImageCache()
|
||||
setClearing(false)
|
||||
}, [resetImageCache])
|
||||
|
||||
const setMinBufferText = useCallback((text: string) => setMinBuffer(parseFloat(text)), [setMinBuffer])
|
||||
const setMaxBufferText = useCallback((text: string) => setMaxBuffer(parseFloat(text)), [setMaxBuffer])
|
||||
|
||||
@@ -5,12 +5,13 @@ import ImageGradientFlatList from '@app/components/ImageGradientFlatList'
|
||||
import ListItem from '@app/components/ListItem'
|
||||
import ListPlayerControls from '@app/components/ListPlayerControls'
|
||||
import NothingHere from '@app/components/NothingHere'
|
||||
import { useCoverArtFile } from '@app/hooks/cache'
|
||||
import { Song, Album, Playlist } from '@app/models/library'
|
||||
import { useStore, useStoreDeep } from '@app/state/store'
|
||||
import { useQueryAlbum, useQueryCoverArtPath, useQueryPlaylist } from '@app/hooks/query'
|
||||
import { useSetQueue } from '@app/hooks/trackplayer'
|
||||
import { Album, Playlist, Song } from '@app/models/library'
|
||||
import colors from '@app/styles/colors'
|
||||
import font from '@app/styles/font'
|
||||
import React, { useCallback, useEffect, useState } from 'react'
|
||||
import equal from 'fast-deep-equal/es6/react'
|
||||
import React, { useState } from 'react'
|
||||
import { ActivityIndicator, StyleSheet, Text, View } from 'react-native'
|
||||
|
||||
type SongListType = 'album' | 'playlist'
|
||||
@@ -29,6 +30,7 @@ const SongRenderItem: React.FC<{
|
||||
subtitle?: string
|
||||
onPress?: () => void
|
||||
showArt?: boolean
|
||||
disabled?: boolean
|
||||
}
|
||||
}> = ({ item }) => (
|
||||
<ListItem
|
||||
@@ -39,6 +41,7 @@ const SongRenderItem: React.FC<{
|
||||
onPress={item.onPress}
|
||||
showArt={item.showArt}
|
||||
style={styles.listItem}
|
||||
disabled={item.disabled}
|
||||
/>
|
||||
)
|
||||
|
||||
@@ -49,13 +52,8 @@ const SongListDetails = React.memo<{
|
||||
songs?: Song[]
|
||||
subtitle?: string
|
||||
}>(({ title, songList, songs, subtitle, type }) => {
|
||||
const coverArtFile = useCoverArtFile(songList?.coverArt, 'thumbnail')
|
||||
const { data: coverArtPath } = useQueryCoverArtPath(songList?.coverArt, 'thumbnail')
|
||||
const [headerColor, setHeaderColor] = useState<string | undefined>(undefined)
|
||||
const setQueue = useStore(store => store.setQueue)
|
||||
|
||||
if (!songList) {
|
||||
return <SongListDetailsFallback />
|
||||
}
|
||||
|
||||
const _songs = [...(songs || [])]
|
||||
let typeName = ''
|
||||
@@ -75,6 +73,16 @@ const SongListDetails = React.memo<{
|
||||
typeName = 'Playlist'
|
||||
}
|
||||
|
||||
const { setQueue, isReady, contextId } = useSetQueue(type, _songs)
|
||||
|
||||
if (!songList) {
|
||||
return <SongListDetailsFallback />
|
||||
}
|
||||
|
||||
const disabled = !isReady || _songs.length === 0
|
||||
const play = (track?: number, shuffle?: boolean) => () =>
|
||||
setQueue({ title: songList.name, playTrack: track, shuffle })
|
||||
|
||||
return (
|
||||
<View style={styles.container}>
|
||||
<HeaderBar
|
||||
@@ -85,16 +93,17 @@ const SongListDetails = React.memo<{
|
||||
<ImageGradientFlatList
|
||||
data={_songs.map((s, i) => ({
|
||||
song: s,
|
||||
contextId: songList.id,
|
||||
contextId,
|
||||
queueId: i,
|
||||
subtitle: s.artist,
|
||||
onPress: () => setQueue(_songs, songList.name, type, songList.id, i),
|
||||
onPress: play(i),
|
||||
showArt: songList.itemType === 'playlist',
|
||||
disabled: disabled,
|
||||
}))}
|
||||
renderItem={SongRenderItem}
|
||||
keyExtractor={(item, i) => i.toString()}
|
||||
backgroundProps={{
|
||||
imagePath: coverArtFile?.file?.path,
|
||||
imagePath: coverArtPath,
|
||||
style: styles.container,
|
||||
onGetColor: setHeaderColor,
|
||||
}}
|
||||
@@ -117,86 +126,66 @@ const SongListDetails = React.memo<{
|
||||
style={styles.controls}
|
||||
songs={_songs}
|
||||
typeName={typeName}
|
||||
queueName={songList.name}
|
||||
queueContextId={songList.id}
|
||||
queueContextType={type}
|
||||
play={play(undefined, false)}
|
||||
shuffle={play(undefined, true)}
|
||||
disabled={disabled}
|
||||
/>
|
||||
</View>
|
||||
}
|
||||
/>
|
||||
</View>
|
||||
)
|
||||
})
|
||||
}, equal)
|
||||
|
||||
const PlaylistView = React.memo<{
|
||||
id: string
|
||||
title: string
|
||||
}>(({ id, title }) => {
|
||||
const playlist = useStoreDeep(useCallback(store => store.library.playlists[id], [id]))
|
||||
const songs = useStoreDeep(
|
||||
useCallback(
|
||||
store => {
|
||||
const ids = store.library.playlistSongs[id]
|
||||
return ids ? ids.map(i => store.library.songs[i]) : undefined
|
||||
},
|
||||
[id],
|
||||
),
|
||||
)
|
||||
|
||||
const fetchPlaylist = useStore(store => store.fetchPlaylist)
|
||||
|
||||
useEffect(() => {
|
||||
if (!playlist || !songs) {
|
||||
fetchPlaylist(id)
|
||||
}
|
||||
}, [playlist, fetchPlaylist, id, songs])
|
||||
|
||||
return (
|
||||
<SongListDetails title={title} songList={playlist} songs={songs} subtitle={playlist?.comment} type="playlist" />
|
||||
)
|
||||
})
|
||||
|
||||
const AlbumView = React.memo<{
|
||||
id: string
|
||||
title: string
|
||||
}>(({ id, title }) => {
|
||||
const album = useStoreDeep(useCallback(store => store.library.albums[id], [id]))
|
||||
const songs = useStoreDeep(
|
||||
useCallback(
|
||||
store => {
|
||||
const ids = store.library.albumSongs[id]
|
||||
return ids ? ids.map(i => store.library.songs[i]) : undefined
|
||||
},
|
||||
[id],
|
||||
),
|
||||
)
|
||||
|
||||
const fetchAlbum = useStore(store => store.fetchAlbum)
|
||||
|
||||
useEffect(() => {
|
||||
if (!album || !songs) {
|
||||
fetchAlbum(id)
|
||||
}
|
||||
}, [album, fetchAlbum, id, songs])
|
||||
playlist?: Playlist
|
||||
}>(({ id, title, playlist }) => {
|
||||
const query = useQueryPlaylist(id, playlist)
|
||||
|
||||
return (
|
||||
<SongListDetails
|
||||
title={title}
|
||||
songList={album}
|
||||
songs={songs}
|
||||
subtitle={(album?.artist || '') + (album?.year ? ' • ' + album?.year : '')}
|
||||
songList={query.data?.playlist}
|
||||
songs={query.data?.songs}
|
||||
subtitle={query.data?.playlist?.comment}
|
||||
type="playlist"
|
||||
/>
|
||||
)
|
||||
}, equal)
|
||||
|
||||
const AlbumView = React.memo<{
|
||||
id: string
|
||||
title: string
|
||||
album?: Album
|
||||
}>(({ id, title, album }) => {
|
||||
const query = useQueryAlbum(id, album)
|
||||
|
||||
return (
|
||||
<SongListDetails
|
||||
title={title}
|
||||
songList={query.data?.album}
|
||||
songs={query.data?.songs}
|
||||
subtitle={(query.data?.album?.artist || '') + (query.data?.album?.year ? ' • ' + query.data?.album?.year : '')}
|
||||
type="album"
|
||||
/>
|
||||
)
|
||||
})
|
||||
}, equal)
|
||||
|
||||
const SongListView = React.memo<{
|
||||
id: string
|
||||
title: string
|
||||
type: SongListType
|
||||
}>(({ id, title, type }) => {
|
||||
return type === 'album' ? <AlbumView id={id} title={title} /> : <PlaylistView id={id} title={title} />
|
||||
})
|
||||
album?: Album
|
||||
playlist?: Playlist
|
||||
}>(({ id, title, type, album, playlist }) => {
|
||||
return type === 'album' ? (
|
||||
<AlbumView id={id} title={title} album={album} />
|
||||
) : (
|
||||
<PlaylistView id={id} title={title} playlist={playlist} />
|
||||
)
|
||||
}, equal)
|
||||
|
||||
const styles = StyleSheet.create({
|
||||
container: {
|
||||
|
||||
Reference in New Issue
Block a user