mirror of
https://github.com/austinried/subtracks.git
synced 2025-12-29 09:29:29 +01:00
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
This commit is contained in:
parent
1944add558
commit
a92ad7bfc9
@ -119,11 +119,13 @@ const ContextMenuIconTextOption = React.memo<ContextMenuIconTextOptionProps>(
|
|||||||
const MenuHeader = React.memo<{
|
const MenuHeader = React.memo<{
|
||||||
coverArt?: string
|
coverArt?: string
|
||||||
artistId?: string
|
artistId?: string
|
||||||
|
albumId?: string
|
||||||
title: string
|
title: string
|
||||||
subtitle?: string
|
subtitle?: string
|
||||||
}>(({ coverArt, artistId, title, subtitle }) => (
|
}>(({ coverArt, artistId, albumId, title, subtitle }) => {
|
||||||
<View style={styles.menuHeader}>
|
let CoverArtComponent = <></>
|
||||||
{artistId ? (
|
if (artistId) {
|
||||||
|
CoverArtComponent = (
|
||||||
<CoverArt
|
<CoverArt
|
||||||
type="artist"
|
type="artist"
|
||||||
artistId={artistId}
|
artistId={artistId}
|
||||||
@ -133,7 +135,20 @@ const MenuHeader = React.memo<{
|
|||||||
size="thumbnail"
|
size="thumbnail"
|
||||||
fadeDuration={0}
|
fadeDuration={0}
|
||||||
/>
|
/>
|
||||||
) : (
|
)
|
||||||
|
} else if (albumId) {
|
||||||
|
CoverArtComponent = (
|
||||||
|
<CoverArt
|
||||||
|
type="album"
|
||||||
|
albumId={albumId}
|
||||||
|
style={styles.coverArt}
|
||||||
|
resizeMode="cover"
|
||||||
|
size="thumbnail"
|
||||||
|
fadeDuration={0}
|
||||||
|
/>
|
||||||
|
)
|
||||||
|
} else {
|
||||||
|
CoverArtComponent = (
|
||||||
<CoverArt
|
<CoverArt
|
||||||
type="cover"
|
type="cover"
|
||||||
coverArt={coverArt}
|
coverArt={coverArt}
|
||||||
@ -142,7 +157,12 @@ const MenuHeader = React.memo<{
|
|||||||
size="thumbnail"
|
size="thumbnail"
|
||||||
fadeDuration={0}
|
fadeDuration={0}
|
||||||
/>
|
/>
|
||||||
)}
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<View style={styles.menuHeader}>
|
||||||
|
{CoverArtComponent}
|
||||||
<View style={styles.menuHeaderText}>
|
<View style={styles.menuHeaderText}>
|
||||||
<Text numberOfLines={1} style={styles.menuTitle}>
|
<Text numberOfLines={1} style={styles.menuTitle}>
|
||||||
{title}
|
{title}
|
||||||
@ -156,7 +176,8 @@ const MenuHeader = React.memo<{
|
|||||||
)}
|
)}
|
||||||
</View>
|
</View>
|
||||||
</View>
|
</View>
|
||||||
))
|
)
|
||||||
|
})
|
||||||
|
|
||||||
const OptionStar = withSuspenseMemo<{
|
const OptionStar = withSuspenseMemo<{
|
||||||
id: string
|
id: string
|
||||||
@ -260,7 +281,7 @@ export const SongContextPressable: React.FC<SongContextPressableProps> = props =
|
|||||||
return (
|
return (
|
||||||
<ContextMenu
|
<ContextMenu
|
||||||
{...props}
|
{...props}
|
||||||
menuHeader={<MenuHeader title={song.title} subtitle={song.artist} coverArt={song.coverArt} />}
|
menuHeader={<MenuHeader title={song.title} subtitle={song.artist} albumId={song.albumId} />}
|
||||||
menuOptions={
|
menuOptions={
|
||||||
<>
|
<>
|
||||||
<OptionStar id={song.id} type={song.itemType} />
|
<OptionStar id={song.id} type={song.itemType} />
|
||||||
@ -307,7 +328,7 @@ export const NowPlayingContextPressable: React.FC<NowPlayingContextPressableProp
|
|||||||
return (
|
return (
|
||||||
<ContextMenu
|
<ContextMenu
|
||||||
{...props}
|
{...props}
|
||||||
menuHeader={<MenuHeader title={song.title} subtitle={song.artist} coverArt={song.coverArt} />}
|
menuHeader={<MenuHeader title={song.title} subtitle={song.artist} albumId={song.albumId} />}
|
||||||
menuOptions={
|
menuOptions={
|
||||||
<>
|
<>
|
||||||
<OptionStar id={song.id} type={song.itemType} />
|
<OptionStar id={song.id} type={song.itemType} />
|
||||||
|
|||||||
@ -1,4 +1,4 @@
|
|||||||
import { useQueryArtistArtPath, useQueryCoverArtPath } from '@app/hooks/query'
|
import { useQueryAlbumCoverArtPath, useQueryArtistArtPath, useQueryCoverArtPath } from '@app/hooks/query'
|
||||||
import { CacheImageSize } from '@app/models/cache'
|
import { CacheImageSize } from '@app/models/cache'
|
||||||
import colors from '@app/styles/colors'
|
import colors from '@app/styles/colors'
|
||||||
import React, { useState } from 'react'
|
import React, { useState } from 'react'
|
||||||
@ -32,6 +32,11 @@ type CoverArtProps = BaseProps & {
|
|||||||
coverArt?: string
|
coverArt?: string
|
||||||
}
|
}
|
||||||
|
|
||||||
|
type AlbumIdProps = BaseProps & {
|
||||||
|
type: 'album'
|
||||||
|
albumId?: string
|
||||||
|
}
|
||||||
|
|
||||||
type ImageSourceProps = BaseProps & {
|
type ImageSourceProps = BaseProps & {
|
||||||
data?: string
|
data?: string
|
||||||
isFetching: boolean
|
isFetching: boolean
|
||||||
@ -82,7 +87,13 @@ const CoverArtImage = React.memo<CoverArtProps>(props => {
|
|||||||
return <ImageSource data={data} isFetching={isFetching} isExistingFetching={isExistingFetching} {...props} />
|
return <ImageSource data={data} isFetching={isFetching} isExistingFetching={isExistingFetching} {...props} />
|
||||||
})
|
})
|
||||||
|
|
||||||
const CoverArt = React.memo<CoverArtProps | ArtistCoverArtProps>(props => {
|
const AlbumIdIamge = React.memo<AlbumIdProps>(props => {
|
||||||
|
const { data, isFetching, isExistingFetching } = useQueryAlbumCoverArtPath(props.albumId, props.size)
|
||||||
|
|
||||||
|
return <ImageSource data={data} isFetching={isFetching} isExistingFetching={isExistingFetching} {...props} />
|
||||||
|
})
|
||||||
|
|
||||||
|
const CoverArt = React.memo<CoverArtProps | ArtistCoverArtProps | AlbumIdProps>(props => {
|
||||||
const viewStyles = [props.style]
|
const viewStyles = [props.style]
|
||||||
if (props.round) {
|
if (props.round) {
|
||||||
viewStyles.push(styles.round)
|
viewStyles.push(styles.round)
|
||||||
@ -93,6 +104,9 @@ const CoverArt = React.memo<CoverArtProps | ArtistCoverArtProps>(props => {
|
|||||||
case 'artist':
|
case 'artist':
|
||||||
imageComponent = <ArtistImage {...(props as ArtistCoverArtProps)} />
|
imageComponent = <ArtistImage {...(props as ArtistCoverArtProps)} />
|
||||||
break
|
break
|
||||||
|
case 'album':
|
||||||
|
imageComponent = <AlbumIdIamge {...(props as AlbumIdProps)} />
|
||||||
|
break
|
||||||
default:
|
default:
|
||||||
imageComponent = <CoverArtImage {...(props as CoverArtProps)} />
|
imageComponent = <CoverArtImage {...(props as CoverArtProps)} />
|
||||||
break
|
break
|
||||||
|
|||||||
@ -160,6 +160,10 @@ const ListItem: React.FC<{
|
|||||||
size="thumbnail"
|
size="thumbnail"
|
||||||
/>
|
/>
|
||||||
)
|
)
|
||||||
|
} else if (item.itemType === 'song') {
|
||||||
|
coverArt = (
|
||||||
|
<CoverArt type="album" albumId={item.albumId} style={artStyle} resizeMode={resizeMode} size="thumbnail" />
|
||||||
|
)
|
||||||
} else {
|
} else {
|
||||||
coverArt = (
|
coverArt = (
|
||||||
<CoverArt type="cover" coverArt={item.coverArt} style={artStyle} resizeMode={resizeMode} size="thumbnail" />
|
<CoverArt type="cover" coverArt={item.coverArt} style={artStyle} resizeMode={resizeMode} size="thumbnail" />
|
||||||
|
|||||||
@ -79,7 +79,7 @@ const Controls = React.memo(() => {
|
|||||||
const NowPlayingBar = React.memo(() => {
|
const NowPlayingBar = React.memo(() => {
|
||||||
const navigation = useNavigation()
|
const navigation = useNavigation()
|
||||||
const currentTrackExists = useStore(store => !!store.currentTrack)
|
const currentTrackExists = useStore(store => !!store.currentTrack)
|
||||||
const coverArt = useStore(store => store.currentTrack?.coverArt)
|
const albumId = useStore(store => store.currentTrack?.albumId)
|
||||||
const title = useStore(store => store.currentTrack?.title)
|
const title = useStore(store => store.currentTrack?.title)
|
||||||
const artist = useStore(store => store.currentTrack?.artist)
|
const artist = useStore(store => store.currentTrack?.artist)
|
||||||
|
|
||||||
@ -90,9 +90,9 @@ const NowPlayingBar = React.memo(() => {
|
|||||||
<ProgressBar />
|
<ProgressBar />
|
||||||
<View style={styles.subContainer}>
|
<View style={styles.subContainer}>
|
||||||
<CoverArt
|
<CoverArt
|
||||||
type="cover"
|
type="album"
|
||||||
style={{ height: styles.subContainer.height, width: styles.subContainer.height }}
|
style={{ height: styles.subContainer.height, width: styles.subContainer.height }}
|
||||||
coverArt={coverArt}
|
albumId={albumId}
|
||||||
size="thumbnail"
|
size="thumbnail"
|
||||||
fadeDuration={0}
|
fadeDuration={0}
|
||||||
/>
|
/>
|
||||||
|
|||||||
@ -1,8 +1,9 @@
|
|||||||
import { CacheItemTypeKey } from '@app/models/cache'
|
import { CacheItemTypeKey } from '@app/models/cache'
|
||||||
import { Album, AlbumCoverArt, Playlist, Song } from '@app/models/library'
|
import { Album, Playlist, Song } from '@app/models/library'
|
||||||
import { mapAlbum, mapArtist, mapArtistInfo, mapPlaylist, mapSong } from '@app/models/map'
|
import { mapAlbum, mapArtist, mapArtistInfo, mapPlaylist, mapSong } from '@app/models/map'
|
||||||
import queryClient from '@app/queryClient'
|
import queryClient from '@app/queryClient'
|
||||||
import { useStore } from '@app/state/store'
|
import { useStore } from '@app/state/store'
|
||||||
|
import { SubsonicApiClient } from '@app/subsonic/api'
|
||||||
import { GetAlbumList2TypeBase, Search3Params, StarParams } from '@app/subsonic/params'
|
import { GetAlbumList2TypeBase, Search3Params, StarParams } from '@app/subsonic/params'
|
||||||
import { cacheDir } from '@app/util/fs'
|
import { cacheDir } from '@app/util/fs'
|
||||||
import { mapCollectionById } from '@app/util/state'
|
import { mapCollectionById } from '@app/util/state'
|
||||||
@ -31,7 +32,7 @@ function cacheStarredData<T extends { id: string; starred?: undefined | any }>(i
|
|||||||
}
|
}
|
||||||
|
|
||||||
function cacheAlbumCoverArtData<T extends { id: string; coverArt?: string }>(item: T) {
|
function cacheAlbumCoverArtData<T extends { id: string; coverArt?: string }>(item: T) {
|
||||||
queryClient.setQueryData<AlbumCoverArt>(qk.albumCoverArt(item.id), { albumId: item.id, coverArt: item.coverArt })
|
queryClient.setQueryData<string | undefined>(qk.albumCoverArt(item.id), item.coverArt)
|
||||||
}
|
}
|
||||||
|
|
||||||
export const useFetchArtists = () => {
|
export const useFetchArtists = () => {
|
||||||
@ -109,11 +110,8 @@ export const useFetchPlaylist = () => {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
export const useFetchAlbum = () => {
|
export async function fetchAlbum(id: string, client: SubsonicApiClient): Promise<{ album: Album; songs?: Song[] }> {
|
||||||
const client = useClient()
|
const res = await client.getAlbum({ id })
|
||||||
|
|
||||||
return async (id: string): Promise<{ album: Album; songs?: Song[] }> => {
|
|
||||||
const res = await client().getAlbum({ id })
|
|
||||||
|
|
||||||
cacheStarredData(res.data.album)
|
cacheStarredData(res.data.album)
|
||||||
res.data.songs.forEach(cacheStarredData)
|
res.data.songs.forEach(cacheStarredData)
|
||||||
@ -124,7 +122,11 @@ export const useFetchAlbum = () => {
|
|||||||
album: mapAlbum(res.data.album),
|
album: mapAlbum(res.data.album),
|
||||||
songs: res.data.songs.map(mapSong),
|
songs: res.data.songs.map(mapSong),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export const useFetchAlbum = () => {
|
||||||
|
const client = useClient()
|
||||||
|
return async (id: string) => fetchAlbum(id, client())
|
||||||
}
|
}
|
||||||
|
|
||||||
export const useFetchAlbumList = () => {
|
export const useFetchAlbumList = () => {
|
||||||
@ -196,17 +198,23 @@ export type FetchExisingFileOptions = {
|
|||||||
itemId: string
|
itemId: string
|
||||||
}
|
}
|
||||||
|
|
||||||
export const useFetchExistingFile: () => (options: FetchExisingFileOptions) => Promise<string | undefined> = () => {
|
export async function fetchExistingFile(
|
||||||
const serverId = useStore(store => store.settings.activeServerId)
|
options: FetchExisingFileOptions,
|
||||||
|
serverId: string | undefined,
|
||||||
return async ({ itemType, itemId }) => {
|
): Promise<string | undefined> {
|
||||||
|
const { itemType, itemId } = options
|
||||||
const fileDir = cacheDir(serverId, itemType, itemId)
|
const fileDir = cacheDir(serverId, itemType, itemId)
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const dir = await RNFS.readDir(fileDir)
|
const dir = await RNFS.readDir(fileDir)
|
||||||
console.log('existing file:', dir[0].path)
|
console.log('existing file:', dir[0].path)
|
||||||
return dir[0].path
|
return dir[0].path
|
||||||
} catch {}
|
} catch {}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export const useFetchExistingFile = () => {
|
||||||
|
const serverId = useStore(store => store.settings.activeServerId)
|
||||||
|
return async (options: FetchExisingFileOptions) => fetchExistingFile(options, serverId)
|
||||||
}
|
}
|
||||||
|
|
||||||
function assertMimeType(expected?: string, actual?: string) {
|
function assertMimeType(expected?: string, actual?: string) {
|
||||||
@ -237,10 +245,8 @@ export type FetchFileOptions = FetchExisingFileOptions & {
|
|||||||
progress?: (received: number, total: number) => void
|
progress?: (received: number, total: number) => void
|
||||||
}
|
}
|
||||||
|
|
||||||
export const useFetchFile: () => (options: FetchFileOptions) => Promise<string> = () => {
|
export async function fetchFile(options: FetchFileOptions, serverId: string | undefined): Promise<string> {
|
||||||
const serverId = useStore(store => store.settings.activeServerId)
|
let { itemType, itemId, fromUrl, useCacheBuster, expectedContentType, progress } = options
|
||||||
|
|
||||||
return async ({ itemType, itemId, fromUrl, useCacheBuster, expectedContentType, progress }) => {
|
|
||||||
useCacheBuster = useCacheBuster === undefined ? true : useCacheBuster
|
useCacheBuster = useCacheBuster === undefined ? true : useCacheBuster
|
||||||
|
|
||||||
const fileDir = cacheDir(serverId, itemType, itemId)
|
const fileDir = cacheDir(serverId, itemType, itemId)
|
||||||
@ -301,5 +307,9 @@ export const useFetchFile: () => (options: FetchFileOptions) => Promise<string>
|
|||||||
|
|
||||||
console.log('downloaded file:', downloadPath)
|
console.log('downloaded file:', downloadPath)
|
||||||
return downloadPath
|
return downloadPath
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export const useFetchFile = () => {
|
||||||
|
const serverId = useStore(store => store.settings.activeServerId)
|
||||||
|
return async (options: FetchFileOptions) => fetchFile(options, serverId)
|
||||||
}
|
}
|
||||||
|
|||||||
@ -1,19 +1,11 @@
|
|||||||
import { CacheImageSize, CacheItemTypeKey } from '@app/models/cache'
|
import { CacheImageSize, CacheItemTypeKey } from '@app/models/cache'
|
||||||
import { Album, AlbumCoverArt, Artist, Playlist, Song, StarrableItemType } from '@app/models/library'
|
import { Album, Artist, Playlist, Song, StarrableItemType } from '@app/models/library'
|
||||||
import { CollectionById } from '@app/models/state'
|
import { CollectionById } from '@app/models/state'
|
||||||
import queryClient from '@app/queryClient'
|
import queryClient from '@app/queryClient'
|
||||||
import { useStore } from '@app/state/store'
|
import { useStore } from '@app/state/store'
|
||||||
import { GetAlbumList2TypeBase, Search3Params, StarParams } from '@app/subsonic/params'
|
import { GetAlbumList2TypeBase, Search3Params, StarParams } from '@app/subsonic/params'
|
||||||
import _ from 'lodash'
|
import _ from 'lodash'
|
||||||
import {
|
import { useInfiniteQuery, useMutation, useQueries, useQuery } from 'react-query'
|
||||||
InfiniteData,
|
|
||||||
useInfiniteQuery,
|
|
||||||
UseInfiniteQueryResult,
|
|
||||||
useMutation,
|
|
||||||
useQueries,
|
|
||||||
useQuery,
|
|
||||||
UseQueryResult,
|
|
||||||
} from 'react-query'
|
|
||||||
import {
|
import {
|
||||||
useFetchAlbum,
|
useFetchAlbum,
|
||||||
useFetchAlbumList,
|
useFetchAlbumList,
|
||||||
@ -88,7 +80,7 @@ export const useQueryArtistTopSongs = (artistName?: string) => {
|
|||||||
},
|
},
|
||||||
)
|
)
|
||||||
|
|
||||||
return useFixCoverArt(querySuccess ? query : backupQuery)
|
return querySuccess ? query : backupQuery
|
||||||
}
|
}
|
||||||
|
|
||||||
export const useQueryPlaylists = () => useQuery(qk.playlists, useFetchPlaylists())
|
export const useQueryPlaylists = () => useQuery(qk.playlists, useFetchPlaylists())
|
||||||
@ -109,7 +101,7 @@ export const useQueryPlaylist = (id: string, placeholderPlaylist?: Playlist) =>
|
|||||||
},
|
},
|
||||||
})
|
})
|
||||||
|
|
||||||
return useFixCoverArt(query)
|
return query
|
||||||
}
|
}
|
||||||
|
|
||||||
export const useQueryAlbum = (id: string, placeholderAlbum?: Album) => {
|
export const useQueryAlbum = (id: string, placeholderAlbum?: Album) => {
|
||||||
@ -120,7 +112,7 @@ export const useQueryAlbum = (id: string, placeholderAlbum?: Album) => {
|
|||||||
placeholderAlbum ? { album: placeholderAlbum } : undefined,
|
placeholderAlbum ? { album: placeholderAlbum } : undefined,
|
||||||
})
|
})
|
||||||
|
|
||||||
return useFixCoverArt(query)
|
return query
|
||||||
}
|
}
|
||||||
|
|
||||||
export const useQueryAlbumList = (type: GetAlbumList2TypeBase, size: number) => {
|
export const useQueryAlbumList = (type: GetAlbumList2TypeBase, size: number) => {
|
||||||
@ -172,7 +164,7 @@ export const useQuerySearchResults = (params: Search3Params) => {
|
|||||||
},
|
},
|
||||||
)
|
)
|
||||||
|
|
||||||
return useFixCoverArt(query)
|
return query
|
||||||
}
|
}
|
||||||
|
|
||||||
export const useQueryHomeLists = (types: GetAlbumList2TypeBase[], size: number) => {
|
export const useQueryHomeLists = (types: GetAlbumList2TypeBase[], size: number) => {
|
||||||
@ -314,93 +306,18 @@ export const useQueryArtistArtPath = (artistId: string, size: CacheImageSize = '
|
|||||||
return { ...query, data: existing.data || query.data, isExistingFetching: existing.isFetching }
|
return { ...query, data: existing.data || query.data, isExistingFetching: existing.isFetching }
|
||||||
}
|
}
|
||||||
|
|
||||||
type WithSongs = Song[] | { songs?: Song[] }
|
export const useQueryAlbumCoverArtPath = (albumId?: string, size: CacheImageSize = 'thumbnail') => {
|
||||||
type InfiniteWithSongs = { songs: Song[] }
|
|
||||||
type AnyDataWithSongs = WithSongs | InfiniteData<InfiniteWithSongs>
|
|
||||||
type AnyQueryWithSongs = UseQueryResult<WithSongs> | UseInfiniteQueryResult<{ songs: Song[] }>
|
|
||||||
|
|
||||||
function getSongs<T extends AnyDataWithSongs>(data: T | undefined): Song[] {
|
|
||||||
if (!data) {
|
|
||||||
return []
|
|
||||||
}
|
|
||||||
|
|
||||||
if (Array.isArray(data)) {
|
|
||||||
return data
|
|
||||||
}
|
|
||||||
|
|
||||||
if ('pages' in data) {
|
|
||||||
return data.pages.flatMap(p => p.songs)
|
|
||||||
}
|
|
||||||
|
|
||||||
return data.songs || []
|
|
||||||
}
|
|
||||||
|
|
||||||
function setSongCoverArt<T extends AnyQueryWithSongs>(query: T, coverArts: UseQueryResult<AlbumCoverArt>[]): T {
|
|
||||||
if (!query.data) {
|
|
||||||
return query
|
|
||||||
}
|
|
||||||
|
|
||||||
const mapSongCoverArt = (song: Song) => ({
|
|
||||||
...song,
|
|
||||||
coverArt: coverArts.find(c => c.data?.albumId === song.albumId)?.data?.coverArt,
|
|
||||||
})
|
|
||||||
|
|
||||||
if (Array.isArray(query.data)) {
|
|
||||||
return {
|
|
||||||
...query,
|
|
||||||
data: query.data.map(mapSongCoverArt),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if ('pages' in query.data) {
|
|
||||||
return {
|
|
||||||
...query,
|
|
||||||
data: {
|
|
||||||
pages: query.data.pages.map(p => ({
|
|
||||||
...p,
|
|
||||||
songs: p.songs.map(mapSongCoverArt),
|
|
||||||
})),
|
|
||||||
},
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if (query.data.songs) {
|
|
||||||
return {
|
|
||||||
...query,
|
|
||||||
data: {
|
|
||||||
...query.data,
|
|
||||||
songs: query.data.songs.map(mapSongCoverArt),
|
|
||||||
},
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return query
|
|
||||||
}
|
|
||||||
|
|
||||||
// song cover art comes back from the api as a unique id per song even if it all points to the same
|
|
||||||
// album art, which prevents us from caching it once, so we need to use the album's cover art
|
|
||||||
const useFixCoverArt = <T extends AnyQueryWithSongs>(query: T) => {
|
|
||||||
const fetchAlbum = useFetchAlbum()
|
const fetchAlbum = useFetchAlbum()
|
||||||
|
|
||||||
const songs = getSongs(query.data)
|
const query = useQuery(
|
||||||
const albumIds = _.uniq((songs || []).map(s => s.albumId).filter((id): id is string => id !== undefined))
|
qk.albumCoverArt(albumId || '-1'),
|
||||||
|
async () => (await fetchAlbum(albumId || '-1')).album.coverArt,
|
||||||
const coverArts = useQueries(
|
{
|
||||||
albumIds.map(id => ({
|
enabled: !!albumId,
|
||||||
queryKey: qk.albumCoverArt(id),
|
|
||||||
queryFn: async (): Promise<AlbumCoverArt> => {
|
|
||||||
const res = await fetchAlbum(id)
|
|
||||||
return { albumId: res.album.id, coverArt: res.album.coverArt }
|
|
||||||
},
|
|
||||||
staleTime: Infinity,
|
staleTime: Infinity,
|
||||||
cacheTime: Infinity,
|
cacheTime: Infinity,
|
||||||
notifyOnChangeProps: ['data', 'isFetched'] as any,
|
},
|
||||||
})),
|
|
||||||
)
|
)
|
||||||
|
|
||||||
if (coverArts.every(c => c.isFetched)) {
|
return useQueryCoverArtPath(query.data, size)
|
||||||
return setSongCoverArt(query, coverArts)
|
|
||||||
}
|
|
||||||
|
|
||||||
return query
|
|
||||||
}
|
}
|
||||||
|
|||||||
@ -1,13 +1,11 @@
|
|||||||
import { Song } from '@app/models/library'
|
import { Song } from '@app/models/library'
|
||||||
import { QueueContextType, TrackExt } from '@app/models/trackplayer'
|
import { QueueContextType, TrackExt } from '@app/models/trackplayer'
|
||||||
import queryClient from '@app/queryClient'
|
import queryClient from '@app/queryClient'
|
||||||
|
import queueService from '@app/queueservice'
|
||||||
import { useStore, useStoreDeep } from '@app/state/store'
|
import { useStore, useStoreDeep } from '@app/state/store'
|
||||||
import { getQueue, SetQueueOptions, trackPlayerCommands } from '@app/state/trackplayer'
|
import { getQueue, SetQueueOptions, trackPlayerCommands } from '@app/state/trackplayer'
|
||||||
import userAgent from '@app/util/userAgent'
|
import userAgent from '@app/util/userAgent'
|
||||||
import _ from 'lodash'
|
|
||||||
import TrackPlayer from 'react-native-track-player'
|
import TrackPlayer from 'react-native-track-player'
|
||||||
import { useQueries } from 'react-query'
|
|
||||||
import { useFetchExistingFile, useFetchFile } from './fetch'
|
|
||||||
import qk from './queryKeys'
|
import qk from './queryKeys'
|
||||||
|
|
||||||
export const usePlay = () => {
|
export const usePlay = () => {
|
||||||
@ -92,87 +90,50 @@ export const useIsPlaying = (contextId: string | undefined, track: number) => {
|
|||||||
return contextId === queueContextId && track === currentTrackIdx
|
return contextId === queueContextId && track === currentTrackIdx
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export function mapSongToTrackExt(song: Song): TrackExt {
|
||||||
|
return {
|
||||||
|
id: song.id,
|
||||||
|
title: song.title,
|
||||||
|
artist: song.artist || 'Unknown Artist',
|
||||||
|
album: song.album || 'Unknown Album',
|
||||||
|
url: useStore.getState().buildStreamUri(song.id),
|
||||||
|
artwork: require('@res/fallback.png'),
|
||||||
|
userAgent,
|
||||||
|
duration: song.duration,
|
||||||
|
artistId: song.artistId,
|
||||||
|
albumId: song.albumId,
|
||||||
|
track: song.track,
|
||||||
|
discNumber: song.discNumber,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
export const useSetQueue = (type: QueueContextType, songs?: Song[]) => {
|
export const useSetQueue = (type: QueueContextType, songs?: Song[]) => {
|
||||||
const _setQueue = useStore(store => store.setQueue)
|
const _setQueue = useStore(store => store.setQueue)
|
||||||
const client = useStore(store => store.client)
|
|
||||||
const buildStreamUri = useStore(store => store.buildStreamUri)
|
|
||||||
const fetchFile = useFetchFile()
|
|
||||||
const fetchExistingFile = useFetchExistingFile()
|
|
||||||
|
|
||||||
const songCoverArt = _.uniq((songs || []).map(s => s.coverArt)).filter((c): c is string => c !== undefined)
|
|
||||||
|
|
||||||
const coverArtPaths = useQueries(
|
|
||||||
songCoverArt.map(coverArt => ({
|
|
||||||
queryKey: qk.coverArt(coverArt, 'thumbnail'),
|
|
||||||
queryFn: async () => {
|
|
||||||
if (!client) {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
const itemType = 'coverArtThumb'
|
|
||||||
|
|
||||||
const existingCache = queryClient.getQueryData<string | undefined>(qk.existingFiles(itemType, coverArt))
|
|
||||||
if (existingCache) {
|
|
||||||
return existingCache
|
|
||||||
}
|
|
||||||
|
|
||||||
const existingDisk = await fetchExistingFile({ itemId: coverArt, itemType })
|
|
||||||
if (existingDisk) {
|
|
||||||
return existingDisk
|
|
||||||
}
|
|
||||||
|
|
||||||
const fromUrl = client.getCoverArtUri({ id: coverArt, size: '256' })
|
|
||||||
return await fetchFile({
|
|
||||||
itemType,
|
|
||||||
itemId: coverArt,
|
|
||||||
fromUrl,
|
|
||||||
expectedContentType: 'image',
|
|
||||||
})
|
|
||||||
},
|
|
||||||
enabled: !!client && !!songs,
|
|
||||||
staleTime: Infinity,
|
|
||||||
cacheTime: Infinity,
|
|
||||||
notifyOnChangeProps: ['data', 'isFetched'] as any,
|
|
||||||
})),
|
|
||||||
)
|
|
||||||
|
|
||||||
const songCoverArtToPath = _.zipObject(
|
|
||||||
songCoverArt,
|
|
||||||
coverArtPaths.map(c => c.data),
|
|
||||||
)
|
|
||||||
|
|
||||||
const mapSongToTrackExt = (s: Song): TrackExt => {
|
|
||||||
let artwork = require('@res/fallback.png')
|
|
||||||
if (s.coverArt) {
|
|
||||||
const filePath = songCoverArtToPath[s.coverArt]
|
|
||||||
if (filePath) {
|
|
||||||
artwork = `file://${filePath}`
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return {
|
|
||||||
id: s.id,
|
|
||||||
title: s.title,
|
|
||||||
artist: s.artist || 'Unknown Artist',
|
|
||||||
album: s.album || 'Unknown Album',
|
|
||||||
url: buildStreamUri(s.id),
|
|
||||||
userAgent,
|
|
||||||
artwork,
|
|
||||||
coverArt: s.coverArt,
|
|
||||||
duration: s.duration,
|
|
||||||
artistId: s.artistId,
|
|
||||||
albumId: s.albumId,
|
|
||||||
track: s.track,
|
|
||||||
discNumber: s.discNumber,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
const contextId = `${type}-${songs?.map(s => s.id).join('-')}`
|
const contextId = `${type}-${songs?.map(s => s.id).join('-')}`
|
||||||
|
|
||||||
const setQueue = async (options: SetQueueOptions) => {
|
const setQueue = async (options: SetQueueOptions) => {
|
||||||
const queue = (songs || []).map(mapSongToTrackExt)
|
if (!songs || songs.length === 0) {
|
||||||
return await _setQueue({ queue, type, contextId, ...options })
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
return { setQueue, contextId, isReady: coverArtPaths.every(c => c.isFetched) }
|
const queue = songs.map(mapSongToTrackExt)
|
||||||
|
const first = queue[options.playTrack || 0]
|
||||||
|
|
||||||
|
if (!first.albumId) {
|
||||||
|
first.artwork = require('@res/fallback.png')
|
||||||
|
} else {
|
||||||
|
const albumCoverArt = queryClient.getQueryData<string>(qk.albumCoverArt(first.albumId))
|
||||||
|
const existingFile = queryClient.getQueryData<string>(qk.existingFiles('coverArtThumb', albumCoverArt))
|
||||||
|
const downloadFile = queryClient.getQueryData<string>(qk.coverArt(albumCoverArt, 'thumbnail'))
|
||||||
|
if (existingFile || downloadFile) {
|
||||||
|
first.artwork = `file://${existingFile || downloadFile}`
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
await _setQueue({ queue, type, contextId, ...options })
|
||||||
|
queueService.emit('set', { queue })
|
||||||
|
}
|
||||||
|
|
||||||
|
return { setQueue, contextId }
|
||||||
}
|
}
|
||||||
|
|||||||
@ -43,7 +43,6 @@ export interface Song {
|
|||||||
discNumber?: number
|
discNumber?: number
|
||||||
duration?: number
|
duration?: number
|
||||||
starred?: number
|
starred?: number
|
||||||
coverArt?: string
|
|
||||||
playCount?: number
|
playCount?: number
|
||||||
userRating?: number
|
userRating?: number
|
||||||
averageRating?: number
|
averageRating?: number
|
||||||
|
|||||||
@ -75,7 +75,6 @@ export function mapTrackExtToSong(track: TrackExt): Song {
|
|||||||
title: track.title as string,
|
title: track.title as string,
|
||||||
artist: track.artist,
|
artist: track.artist,
|
||||||
album: track.album,
|
album: track.album,
|
||||||
coverArt: track.coverArt,
|
|
||||||
duration: track.duration,
|
duration: track.duration,
|
||||||
artistId: track.artistId,
|
artistId: track.artistId,
|
||||||
albumId: track.albumId,
|
albumId: track.albumId,
|
||||||
|
|||||||
@ -1,8 +1,14 @@
|
|||||||
import { getCurrentTrack, getPlayerState, trackPlayerCommands } from '@app/state/trackplayer'
|
import { getCurrentTrack, getPlayerState, trackPlayerCommands } from '@app/state/trackplayer'
|
||||||
import TrackPlayer, { Event, State } from 'react-native-track-player'
|
|
||||||
import { useStore } from './state/store'
|
|
||||||
import { unstable_batchedUpdates } from 'react-native'
|
|
||||||
import NetInfo, { NetInfoStateType } from '@react-native-community/netinfo'
|
import NetInfo, { NetInfoStateType } from '@react-native-community/netinfo'
|
||||||
|
import _ from 'lodash'
|
||||||
|
import { unstable_batchedUpdates } from 'react-native'
|
||||||
|
import TrackPlayer, { Event, State } from 'react-native-track-player'
|
||||||
|
import { fetchAlbum, FetchExisingFileOptions, fetchExistingFile, fetchFile, FetchFileOptions } from './hooks/fetch'
|
||||||
|
import qk from './hooks/queryKeys'
|
||||||
|
import queryClient from './queryClient'
|
||||||
|
import queueService from './queueservice'
|
||||||
|
import { useStore } from './state/store'
|
||||||
|
import { ReturnedPromiseResolvedType } from './util/types'
|
||||||
|
|
||||||
const reset = () => {
|
const reset = () => {
|
||||||
unstable_batchedUpdates(() => {
|
unstable_batchedUpdates(() => {
|
||||||
@ -34,12 +40,81 @@ const rebuildQueue = (forcePlay?: boolean) => {
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const updateQueue = () => {
|
||||||
|
unstable_batchedUpdates(() => {
|
||||||
|
useStore.getState().updateQueue()
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
const setDuckPaused = (duckPaused: boolean) => {
|
const setDuckPaused = (duckPaused: boolean) => {
|
||||||
unstable_batchedUpdates(() => {
|
unstable_batchedUpdates(() => {
|
||||||
useStore.getState().setDuckPaused(duckPaused)
|
useStore.getState().setDuckPaused(duckPaused)
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const setQueryDataAlbum = (queryKey: any, data: ReturnedPromiseResolvedType<typeof fetchAlbum>) => {
|
||||||
|
unstable_batchedUpdates(() => {
|
||||||
|
queryClient.setQueryData(queryKey, data)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
const setQueryDataExistingFiles = (queryKey: any, data: ReturnedPromiseResolvedType<typeof fetchExistingFile>) => {
|
||||||
|
unstable_batchedUpdates(() => {
|
||||||
|
queryClient.setQueryData(queryKey, data)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
const setQueryDataCoverArt = (queryKey: any, data: ReturnedPromiseResolvedType<typeof fetchFile>) => {
|
||||||
|
unstable_batchedUpdates(() => {
|
||||||
|
queryClient.setQueryData(queryKey, data)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
function getClient() {
|
||||||
|
const client = useStore.getState().client
|
||||||
|
if (!client) {
|
||||||
|
throw new Error('no client!')
|
||||||
|
}
|
||||||
|
|
||||||
|
return client
|
||||||
|
}
|
||||||
|
|
||||||
|
async function getAlbum(id: string) {
|
||||||
|
try {
|
||||||
|
const res = await fetchAlbum(id, getClient())
|
||||||
|
setQueryDataAlbum(qk.album(id), res)
|
||||||
|
return res
|
||||||
|
} catch {}
|
||||||
|
}
|
||||||
|
|
||||||
|
async function getCoverArtThumbExisting(coverArt: string) {
|
||||||
|
const serverId = useStore.getState().settings.activeServerId
|
||||||
|
const options: FetchExisingFileOptions = { itemType: 'coverArtThumb', itemId: coverArt }
|
||||||
|
|
||||||
|
try {
|
||||||
|
const res = await fetchExistingFile(options, serverId)
|
||||||
|
setQueryDataExistingFiles(qk.existingFiles(options.itemType, options.itemId), res)
|
||||||
|
return res
|
||||||
|
} catch {}
|
||||||
|
}
|
||||||
|
|
||||||
|
async function getCoverArtThumb(coverArt: string) {
|
||||||
|
const serverId = useStore.getState().settings.activeServerId
|
||||||
|
const fromUrl = getClient().getCoverArtUri({ id: coverArt, size: '256' })
|
||||||
|
const options: FetchFileOptions = {
|
||||||
|
itemType: 'coverArtThumb',
|
||||||
|
itemId: coverArt,
|
||||||
|
fromUrl,
|
||||||
|
expectedContentType: 'image',
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
const res = await fetchFile(options, serverId)
|
||||||
|
setQueryDataCoverArt(qk.coverArt(coverArt, 'thumbnail'), res)
|
||||||
|
return res
|
||||||
|
} catch {}
|
||||||
|
}
|
||||||
|
|
||||||
let serviceCreated = false
|
let serviceCreated = false
|
||||||
|
|
||||||
const createService = async () => {
|
const createService = async () => {
|
||||||
@ -142,6 +217,78 @@ const createService = async () => {
|
|||||||
rebuildQueue(true)
|
rebuildQueue(true)
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|
||||||
|
queueService.addListener('set', async ({ queue }) => {
|
||||||
|
const contextId = useStore.getState().queueContextId
|
||||||
|
const throwIfQueueChanged = () => {
|
||||||
|
if (contextId !== useStore.getState().queueContextId) {
|
||||||
|
throw 'queue-changed'
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const albumIds = _.uniq(queue.map(s => s.albumId)).filter((id): id is string => id !== undefined)
|
||||||
|
|
||||||
|
const albumIdImagePath: { [albumId: string]: string | undefined } = {}
|
||||||
|
for (const albumId of albumIds) {
|
||||||
|
let coverArt = queryClient.getQueryData<string>(qk.albumCoverArt(albumId))
|
||||||
|
if (!coverArt) {
|
||||||
|
throwIfQueueChanged()
|
||||||
|
console.log('no cached coverArt for album', albumId, 'getting album...')
|
||||||
|
coverArt = (await getAlbum(albumId))?.album.coverArt
|
||||||
|
if (!coverArt) {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
let imagePath =
|
||||||
|
queryClient.getQueryData<string>(qk.existingFiles('coverArtThumb', coverArt)) ||
|
||||||
|
queryClient.getQueryData<string>(qk.coverArt(coverArt, 'thumbnail'))
|
||||||
|
if (!imagePath) {
|
||||||
|
throwIfQueueChanged()
|
||||||
|
console.log('no cached image for', coverArt, 'getting file...')
|
||||||
|
imagePath = (await getCoverArtThumbExisting(coverArt)) || (await getCoverArtThumb(coverArt))
|
||||||
|
if (!imagePath) {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
albumIdImagePath[albumId] = imagePath
|
||||||
|
}
|
||||||
|
|
||||||
|
for (let i = 0; i < queue.length; i++) {
|
||||||
|
const track = queue[i]
|
||||||
|
if (typeof track.artwork === 'string') {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!track.albumId) {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
let imagePath = albumIdImagePath[track.albumId]
|
||||||
|
if (!imagePath) {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
throwIfQueueChanged()
|
||||||
|
|
||||||
|
let trackIdx = i
|
||||||
|
const shuffleOrder = useStore.getState().shuffleOrder
|
||||||
|
if (shuffleOrder) {
|
||||||
|
trackIdx = shuffleOrder.indexOf(i)
|
||||||
|
}
|
||||||
|
|
||||||
|
await TrackPlayer.updateMetadataForTrack(trackIdx, { ...track, artwork: `file://${imagePath}` })
|
||||||
|
} catch {
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
await trackPlayerCommands.enqueue(async () => {
|
||||||
|
updateQueue()
|
||||||
|
})
|
||||||
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
module.exports = async function () {
|
module.exports = async function () {
|
||||||
|
|||||||
18
app/queueservice.ts
Normal file
18
app/queueservice.ts
Normal file
@ -0,0 +1,18 @@
|
|||||||
|
/* eslint-disable no-dupe-class-members */
|
||||||
|
import { EmitterSubscription, NativeEventEmitter } from 'react-native'
|
||||||
|
import { TrackExt } from './models/trackplayer'
|
||||||
|
|
||||||
|
class QueueService extends NativeEventEmitter {
|
||||||
|
addListener(eventType: 'set', listener: (event: { queue: TrackExt[] }) => void): EmitterSubscription
|
||||||
|
addListener(eventType: string, listener: (event: any) => void, context?: Object): EmitterSubscription {
|
||||||
|
return super.addListener(eventType, listener, context)
|
||||||
|
}
|
||||||
|
|
||||||
|
emit(eventType: 'set', event: { queue: TrackExt[] }): void
|
||||||
|
emit(eventType: string, ...params: any[]): void {
|
||||||
|
super.emit(eventType, ...params)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const queueService = new QueueService()
|
||||||
|
export default queueService
|
||||||
@ -49,7 +49,7 @@ const TopSongs = withSuspenseMemo<{
|
|||||||
name: string
|
name: string
|
||||||
}>(
|
}>(
|
||||||
({ songs, name }) => {
|
({ songs, name }) => {
|
||||||
const { setQueue, isReady, contextId } = useSetQueue('artist', songs)
|
const { setQueue, contextId } = useSetQueue('artist', songs)
|
||||||
const { t } = useTranslation()
|
const { t } = useTranslation()
|
||||||
|
|
||||||
return (
|
return (
|
||||||
@ -64,7 +64,6 @@ const TopSongs = withSuspenseMemo<{
|
|||||||
showArt={true}
|
showArt={true}
|
||||||
subtitle={s.album}
|
subtitle={s.album}
|
||||||
onPress={() => setQueue({ title: name, playTrack: i })}
|
onPress={() => setQueue({ title: name, playTrack: i })}
|
||||||
disabled={!isReady}
|
|
||||||
/>
|
/>
|
||||||
))}
|
))}
|
||||||
</>
|
</>
|
||||||
|
|||||||
@ -90,11 +90,11 @@ const headerStyles = StyleSheet.create({
|
|||||||
})
|
})
|
||||||
|
|
||||||
const SongCoverArt = () => {
|
const SongCoverArt = () => {
|
||||||
const coverArt = useStore(store => store.currentTrack?.coverArt)
|
const albumId = useStore(store => store.currentTrack?.albumId)
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<View style={coverArtStyles.container}>
|
<View style={coverArtStyles.container}>
|
||||||
<CoverArt type="cover" size="original" coverArt={coverArt} style={coverArtStyles.image} />
|
<CoverArt type="album" size="original" albumId={albumId} style={coverArtStyles.image} />
|
||||||
</View>
|
</View>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|||||||
@ -26,7 +26,7 @@ import {
|
|||||||
import { useSafeAreaInsets } from 'react-native-safe-area-context'
|
import { useSafeAreaInsets } from 'react-native-safe-area-context'
|
||||||
|
|
||||||
const SongItem = React.memo<{ item: Song }>(({ item }) => {
|
const SongItem = React.memo<{ item: Song }>(({ item }) => {
|
||||||
const { setQueue, isReady, contextId } = useSetQueue('song', [item])
|
const { setQueue, contextId } = useSetQueue('song', [item])
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<ListItem
|
<ListItem
|
||||||
@ -36,7 +36,6 @@ const SongItem = React.memo<{ item: Song }>(({ item }) => {
|
|||||||
showArt={true}
|
showArt={true}
|
||||||
showStar={false}
|
showStar={false}
|
||||||
onPress={() => setQueue({ title: item.title, playTrack: 0 })}
|
onPress={() => setQueue({ title: item.title, playTrack: 0 })}
|
||||||
disabled={!isReady}
|
|
||||||
/>
|
/>
|
||||||
)
|
)
|
||||||
}, equal)
|
}, equal)
|
||||||
|
|||||||
@ -13,7 +13,7 @@ import { StyleSheet } from 'react-native'
|
|||||||
type SearchListItemType = Album | Song | Artist
|
type SearchListItemType = Album | Song | Artist
|
||||||
|
|
||||||
const SongResultsListItem: React.FC<{ item: Song }> = ({ item }) => {
|
const SongResultsListItem: React.FC<{ item: Song }> = ({ item }) => {
|
||||||
const { setQueue, isReady, contextId } = useSetQueue('song', [item])
|
const { setQueue, contextId } = useSetQueue('song', [item])
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<ListItem
|
<ListItem
|
||||||
@ -25,7 +25,6 @@ const SongResultsListItem: React.FC<{ item: Song }> = ({ item }) => {
|
|||||||
listStyle="small"
|
listStyle="small"
|
||||||
onPress={() => setQueue({ title: item.title, playTrack: 0 })}
|
onPress={() => setQueue({ title: item.title, playTrack: 0 })}
|
||||||
style={styles.listItem}
|
style={styles.listItem}
|
||||||
disabled={!isReady}
|
|
||||||
/>
|
/>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|||||||
@ -69,13 +69,13 @@ const SongListDetails = React.memo<{
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
const { setQueue, isReady, contextId } = useSetQueue(type, _songs)
|
const { setQueue, contextId } = useSetQueue(type, _songs)
|
||||||
|
|
||||||
if (!songList) {
|
if (!songList) {
|
||||||
return <SongListDetailsFallback />
|
return <SongListDetailsFallback />
|
||||||
}
|
}
|
||||||
|
|
||||||
const disabled = !isReady || _songs.length === 0
|
const disabled = _songs.length === 0
|
||||||
const play = (track?: number, shuffle?: boolean) => () =>
|
const play = (track?: number, shuffle?: boolean) => () =>
|
||||||
setQueue({ title: songList.name, playTrack: track, shuffle })
|
setQueue({ title: songList.name, playTrack: track, shuffle })
|
||||||
|
|
||||||
|
|||||||
@ -55,6 +55,7 @@ export type TrackPlayerSlice = {
|
|||||||
setNetState: (netState: 'mobile' | 'wifi') => Promise<void>
|
setNetState: (netState: 'mobile' | 'wifi') => Promise<void>
|
||||||
|
|
||||||
rebuildQueue: (forcePlay?: boolean) => Promise<void>
|
rebuildQueue: (forcePlay?: boolean) => Promise<void>
|
||||||
|
updateQueue: () => Promise<void>
|
||||||
buildStreamUri: (id: string) => string
|
buildStreamUri: (id: string) => string
|
||||||
resetTrackPlayerState: () => void
|
resetTrackPlayerState: () => void
|
||||||
|
|
||||||
@ -314,6 +315,17 @@ export const createTrackPlayerSlice = (set: SetStore, get: GetStore): TrackPlaye
|
|||||||
})
|
})
|
||||||
},
|
},
|
||||||
|
|
||||||
|
updateQueue: async () => {
|
||||||
|
const newQueue = await getQueue()
|
||||||
|
const currentTrack = await getCurrentTrack()
|
||||||
|
set(state => {
|
||||||
|
state.queue = newQueue
|
||||||
|
if (currentTrack !== undefined) {
|
||||||
|
state.currentTrack = newQueue[currentTrack]
|
||||||
|
}
|
||||||
|
})
|
||||||
|
},
|
||||||
|
|
||||||
buildStreamUri: id => {
|
buildStreamUri: id => {
|
||||||
const client = get().client
|
const client = get().client
|
||||||
if (!client) {
|
if (!client) {
|
||||||
|
|||||||
2
app/util/types.ts
Normal file
2
app/util/types.ts
Normal file
@ -0,0 +1,2 @@
|
|||||||
|
export type PromiseResolvedType<T> = T extends Promise<infer R> ? R : never
|
||||||
|
export type ReturnedPromiseResolvedType<T extends (...args: any) => any> = PromiseResolvedType<ReturnType<T>>
|
||||||
Loading…
x
Reference in New Issue
Block a user