mirror of
https://github.com/austinried/subtracks.git
synced 2026-02-10 06:52:43 +01:00
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 commite0db4931f1. * use ids for lists, pull state later * Revert "use only original/large imges for covers/artist" This reverts commitc9aea9065c. * 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 commit234326135b. * 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
This commit is contained in:
@@ -1,16 +1,12 @@
|
||||
import { CacheImageSize, CacheItemTypeKey } from '@app/models/cache'
|
||||
import { ArtistInfo } from '@app/models/music'
|
||||
import { selectCache } from '@app/state/cache'
|
||||
import { selectMusic } from '@app/state/music'
|
||||
import { selectSettings } from '@app/state/settings'
|
||||
import { useStore, Store } from '@app/state/store'
|
||||
import { Store, useStore, useStoreDeep } from '@app/state/store'
|
||||
import { useCallback, useEffect } from 'react'
|
||||
|
||||
const useFileRequest = (key: CacheItemTypeKey, id: string) => {
|
||||
const file = useStore(
|
||||
useCallback(
|
||||
(store: Store) => {
|
||||
const activeServerId = store.settings.activeServer
|
||||
const activeServerId = store.settings.activeServerId
|
||||
if (!activeServerId) {
|
||||
return
|
||||
}
|
||||
@@ -23,7 +19,7 @@ const useFileRequest = (key: CacheItemTypeKey, id: string) => {
|
||||
const request = useStore(
|
||||
useCallback(
|
||||
(store: Store) => {
|
||||
const activeServerId = store.settings.activeServer
|
||||
const activeServerId = store.settings.activeServerId
|
||||
if (!activeServerId) {
|
||||
return
|
||||
}
|
||||
@@ -40,8 +36,8 @@ const useFileRequest = (key: CacheItemTypeKey, id: string) => {
|
||||
export const useCoverArtFile = (coverArt = '-1', size: CacheImageSize = 'thumbnail') => {
|
||||
const type: CacheItemTypeKey = size === 'original' ? 'coverArt' : 'coverArtThumb'
|
||||
const { file, request } = useFileRequest(type, coverArt)
|
||||
const client = useStore(selectSettings.client)
|
||||
const cacheItem = useStore(selectCache.cacheItem)
|
||||
const client = useStore(store => store.client)
|
||||
const cacheItem = useStore(store => store.cacheItem)
|
||||
|
||||
useEffect(() => {
|
||||
if (!file && client) {
|
||||
@@ -61,28 +57,25 @@ export const useCoverArtFile = (coverArt = '-1', size: CacheImageSize = 'thumbna
|
||||
|
||||
export const useArtistArtFile = (artistId: string, size: CacheImageSize = 'thumbnail') => {
|
||||
const type: CacheItemTypeKey = size === 'original' ? 'artistArt' : 'artistArtThumb'
|
||||
const fetchArtistInfo = useStore(selectMusic.fetchArtistInfo)
|
||||
const fetchArtistInfo = useStore(store => store.fetchArtistInfo)
|
||||
const artistInfo = useStoreDeep(store => store.library.artistInfo[artistId])
|
||||
const { file, request } = useFileRequest(type, artistId)
|
||||
const cacheItem = useStore(selectCache.cacheItem)
|
||||
const cacheItem = useStore(store => store.cacheItem)
|
||||
|
||||
useEffect(() => {
|
||||
if (!file) {
|
||||
if (!artistInfo) {
|
||||
fetchArtistInfo(artistId)
|
||||
return
|
||||
}
|
||||
|
||||
if (!file && artistInfo) {
|
||||
cacheItem(type, artistId, async () => {
|
||||
let artistInfo: ArtistInfo | undefined
|
||||
const cachedArtistInfo = useStore.getState().artistInfo[artistId]
|
||||
|
||||
if (cachedArtistInfo) {
|
||||
artistInfo = cachedArtistInfo
|
||||
} else {
|
||||
artistInfo = await fetchArtistInfo(artistId)
|
||||
}
|
||||
|
||||
return type === 'artistArtThumb' ? artistInfo?.smallImageUrl : artistInfo?.largeImageUrl
|
||||
})
|
||||
}
|
||||
// intentionally leaving file out so it doesn't re-render if the request fails
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
}, [artistId, cacheItem, fetchArtistInfo, type])
|
||||
}, [artistId, cacheItem, fetchArtistInfo, type, artistInfo])
|
||||
|
||||
return { file, request }
|
||||
}
|
||||
|
||||
63
app/hooks/library.ts
Normal file
63
app/hooks/library.ts
Normal file
@@ -0,0 +1,63 @@
|
||||
import { useStore } from '@app/state/store'
|
||||
import { StarParams } from '@app/subsonic/params'
|
||||
import { useCallback, useEffect } from 'react'
|
||||
|
||||
type StarrableItem = 'album' | 'artist' | 'song'
|
||||
|
||||
function starParams(id: string, type: StarrableItem): StarParams {
|
||||
const params: StarParams = {}
|
||||
if (type === 'album') {
|
||||
params.albumId = id
|
||||
} else if (type === 'artist') {
|
||||
params.artistId = id
|
||||
} else {
|
||||
params.id = id
|
||||
}
|
||||
|
||||
return params
|
||||
}
|
||||
|
||||
export const useStar = (id: string, type: StarrableItem) => {
|
||||
const fetchAlbum = useStore(store => store.fetchAlbum)
|
||||
const fetchArtist = useStore(store => store.fetchArtist)
|
||||
const fetchSong = useStore(store => store.fetchSong)
|
||||
|
||||
const _starred = useStore(
|
||||
useCallback(
|
||||
store => {
|
||||
if (type === 'album') {
|
||||
return store.library.albums[id] ? !!store.library.albums[id].starred : null
|
||||
} else if (type === 'artist') {
|
||||
return store.library.artists[id] ? !!store.library.artists[id].starred : null
|
||||
} else {
|
||||
return store.library.songs[id] ? !!store.library.songs[id].starred : null
|
||||
}
|
||||
},
|
||||
[id, type],
|
||||
),
|
||||
)
|
||||
|
||||
useEffect(() => {
|
||||
if (_starred === null) {
|
||||
if (type === 'album') {
|
||||
fetchAlbum(id)
|
||||
} else if (type === 'artist') {
|
||||
fetchArtist(id)
|
||||
} else {
|
||||
fetchSong(id)
|
||||
}
|
||||
}
|
||||
}, [fetchAlbum, fetchArtist, fetchSong, id, _starred, type])
|
||||
|
||||
const starred = !!_starred
|
||||
|
||||
const _star = useStore(store => store.star)
|
||||
const _unstar = useStore(store => store.unstar)
|
||||
|
||||
const star = useCallback(() => _star(starParams(id, type)), [_star, id, type])
|
||||
const unstar = useCallback(() => _unstar(starParams(id, type)), [_unstar, id, type])
|
||||
|
||||
const toggleStar = useCallback(() => (starred ? unstar() : star()), [star, starred, unstar])
|
||||
|
||||
return { star, unstar, toggleStar, starred }
|
||||
}
|
||||
@@ -1,5 +1,5 @@
|
||||
import { useState, useCallback } from 'react'
|
||||
import { useActiveServerRefresh } from './server'
|
||||
import { useActiveServerRefresh } from './settings'
|
||||
|
||||
export const useFetchList = <T>(fetchList: () => Promise<T[]>) => {
|
||||
const [list, setList] = useState<T[]>([])
|
||||
@@ -28,8 +28,26 @@ export const useFetchList = <T>(fetchList: () => Promise<T[]>) => {
|
||||
return { list, refreshing, refresh, reset }
|
||||
}
|
||||
|
||||
export const useFetchList2 = (fetchList: () => Promise<void>) => {
|
||||
const [refreshing, setRefreshing] = useState(false)
|
||||
|
||||
const refresh = useCallback(async () => {
|
||||
setRefreshing(true)
|
||||
await fetchList()
|
||||
setRefreshing(false)
|
||||
}, [fetchList])
|
||||
|
||||
useActiveServerRefresh(
|
||||
useCallback(async () => {
|
||||
await refresh()
|
||||
}, [refresh]),
|
||||
)
|
||||
|
||||
return { refreshing, refresh }
|
||||
}
|
||||
|
||||
export const useFetchPaginatedList = <T>(
|
||||
fetchList: (size?: number, offset?: number) => Promise<T[]>,
|
||||
fetchList: (size: number, offset: number) => Promise<T[]>,
|
||||
pageSize: number,
|
||||
) => {
|
||||
const [list, setList] = useState<T[]>([])
|
||||
@@ -53,8 +71,8 @@ export const useFetchPaginatedList = <T>(
|
||||
|
||||
useActiveServerRefresh(
|
||||
useCallback(() => {
|
||||
reset()
|
||||
}, [reset]),
|
||||
refresh()
|
||||
}, [refresh]),
|
||||
)
|
||||
|
||||
const fetchNextPage = useCallback(() => {
|
||||
|
||||
@@ -1,62 +0,0 @@
|
||||
import { selectMusic } from '@app/state/music'
|
||||
import { Store, useStore } from '@app/state/store'
|
||||
import { useCallback, useEffect } from 'react'
|
||||
|
||||
export const useArtistInfo = (id: string) => {
|
||||
const artistInfo = useStore(useCallback((state: Store) => state.artistInfo[id], [id]))
|
||||
const fetchArtistInfo = useStore(selectMusic.fetchArtistInfo)
|
||||
|
||||
useEffect(() => {
|
||||
if (!artistInfo) {
|
||||
fetchArtistInfo(id)
|
||||
}
|
||||
}, [artistInfo, fetchArtistInfo, id])
|
||||
|
||||
return artistInfo
|
||||
}
|
||||
|
||||
export const useAlbumWithSongs = (id: string) => {
|
||||
const album = useStore(useCallback((state: Store) => state.albumsWithSongs[id], [id]))
|
||||
const fetchAlbum = useStore(selectMusic.fetchAlbumWithSongs)
|
||||
|
||||
useEffect(() => {
|
||||
if (!album) {
|
||||
fetchAlbum(id)
|
||||
}
|
||||
}, [album, fetchAlbum, id])
|
||||
|
||||
return album
|
||||
}
|
||||
|
||||
export const usePlaylistWithSongs = (id: string) => {
|
||||
const playlist = useStore(useCallback((state: Store) => state.playlistsWithSongs[id], [id]))
|
||||
const fetchPlaylist = useStore(selectMusic.fetchPlaylistWithSongs)
|
||||
|
||||
useEffect(() => {
|
||||
if (!playlist) {
|
||||
fetchPlaylist(id)
|
||||
}
|
||||
}, [fetchPlaylist, id, playlist])
|
||||
|
||||
return playlist
|
||||
}
|
||||
|
||||
export const useStarred = (id: string, type: string) => {
|
||||
return useStore(
|
||||
useCallback(
|
||||
(state: Store) => {
|
||||
switch (type) {
|
||||
case 'song':
|
||||
return state.starredSongs[id]
|
||||
case 'album':
|
||||
return state.starredAlbums[id]
|
||||
case 'artist':
|
||||
return state.starredArtists[id]
|
||||
default:
|
||||
return false
|
||||
}
|
||||
},
|
||||
[type, id],
|
||||
),
|
||||
)
|
||||
}
|
||||
@@ -1,15 +1,14 @@
|
||||
import { useReset } from '@app/hooks/trackplayer'
|
||||
import { selectSettings } from '@app/state/settings'
|
||||
import { useStore } from '@app/state/store'
|
||||
import { useEffect } from 'react'
|
||||
|
||||
export const useSwitchActiveServer = () => {
|
||||
const activeServer = useStore(selectSettings.activeServer)
|
||||
const setActiveServer = useStore(selectSettings.setActiveServer)
|
||||
const activeServerId = useStore(store => store.settings.activeServerId)
|
||||
const setActiveServer = useStore(store => store.setActiveServer)
|
||||
const resetPlayer = useReset()
|
||||
|
||||
return async (id: string) => {
|
||||
if (id === activeServer?.id) {
|
||||
if (id === activeServerId) {
|
||||
return
|
||||
}
|
||||
|
||||
@@ -19,11 +18,15 @@ export const useSwitchActiveServer = () => {
|
||||
}
|
||||
|
||||
export const useActiveServerRefresh = (refresh: () => void) => {
|
||||
const activeServer = useStore(selectSettings.activeServer)
|
||||
const activeServerId = useStore(store => store.settings.activeServerId)
|
||||
|
||||
useEffect(() => {
|
||||
if (activeServer) {
|
||||
if (activeServerId) {
|
||||
refresh()
|
||||
}
|
||||
}, [activeServer, refresh])
|
||||
}, [activeServerId, refresh])
|
||||
}
|
||||
|
||||
export const useFirstRun = () => {
|
||||
return useStore(store => Object.keys(store.settings.servers).length === 0)
|
||||
}
|
||||
@@ -1,5 +1,5 @@
|
||||
import { useStore } from '@app/state/store'
|
||||
import { getQueue, selectTrackPlayer, trackPlayerCommands } from '@app/state/trackplayer'
|
||||
import { useStore, useStoreDeep } from '@app/state/store'
|
||||
import { getQueue, trackPlayerCommands } from '@app/state/trackplayer'
|
||||
import TrackPlayer from 'react-native-track-player'
|
||||
|
||||
export const usePlay = () => {
|
||||
@@ -57,7 +57,7 @@ export const useSeekTo = () => {
|
||||
}
|
||||
|
||||
export const useReset = (enqueue = true) => {
|
||||
const resetStore = useStore(selectTrackPlayer.resetTrackPlayerState)
|
||||
const resetStore = useStore(store => store.resetTrackPlayerState)
|
||||
|
||||
const reset = async () => {
|
||||
await TrackPlayer.reset()
|
||||
@@ -68,9 +68,9 @@ export const useReset = (enqueue = true) => {
|
||||
}
|
||||
|
||||
export const useIsPlaying = (contextId: string | undefined, track: number) => {
|
||||
const queueContextId = useStore(selectTrackPlayer.queueContextId)
|
||||
const currentTrackIdx = useStore(selectTrackPlayer.currentTrackIdx)
|
||||
const shuffleOrder = useStore(selectTrackPlayer.shuffleOrder)
|
||||
const queueContextId = useStore(store => store.queueContextId)
|
||||
const currentTrackIdx = useStore(store => store.currentTrackIdx)
|
||||
const shuffleOrder = useStoreDeep(store => store.shuffleOrder)
|
||||
|
||||
if (contextId === undefined) {
|
||||
return track === currentTrackIdx
|
||||
|
||||
Reference in New Issue
Block a user