use immer as middleware

This commit is contained in:
austinried
2022-03-24 12:00:06 +09:00
parent 1a920e195f
commit 8412c33923
6 changed files with 291 additions and 309 deletions

View File

@@ -1,10 +1,8 @@
import { CacheFile, CacheImageSize, CacheItemType, CacheItemTypeKey, CacheRequest } from '@app/models/cache' import { CacheFile, CacheImageSize, CacheItemType, CacheItemTypeKey, CacheRequest } from '@app/models/cache'
import { mkdir, rmdir } from '@app/util/fs' import { mkdir, rmdir } from '@app/util/fs'
import PromiseQueue from '@app/util/PromiseQueue' import PromiseQueue from '@app/util/PromiseQueue'
import produce from 'immer'
import RNFS from 'react-native-fs' import RNFS from 'react-native-fs'
import { GetState, SetState } from 'zustand' import { GetStore, SetStore } from './store'
import { Store } from './store'
const queues: Record<CacheItemTypeKey, PromiseQueue> = { const queues: Record<CacheItemTypeKey, PromiseQueue> = {
coverArt: new PromiseQueue(5), coverArt: new PromiseQueue(5),
@@ -47,7 +45,7 @@ export const selectCache = {
clearImageCache: (store: CacheSlice) => store.clearImageCache, clearImageCache: (store: CacheSlice) => store.clearImageCache,
} }
export const createCacheSlice = (set: SetState<Store>, get: GetState<Store>): CacheSlice => ({ export const createCacheSlice = (set: SetStore, get: GetStore): CacheSlice => ({
// cache: {}, // cache: {},
cacheDirs: {}, cacheDirs: {},
cacheFiles: {}, cacheFiles: {},
@@ -105,34 +103,28 @@ export const createCacheSlice = (set: SetState<Store>, get: GetState<Store>): Ca
// }, // },
}).promise }).promise
set( set(state => {
produce<CacheSlice>(state => { state.cacheRequests[activeServerId][key][itemId].progress = 1
state.cacheRequests[activeServerId][key][itemId].progress = 1 delete state.cacheRequests[activeServerId][key][itemId].promise
delete state.cacheRequests[activeServerId][key][itemId].promise })
}),
)
} catch { } catch {
set( set(state => {
produce<CacheSlice>(state => { delete state.cacheFiles[activeServerId][key][itemId]
delete state.cacheFiles[activeServerId][key][itemId] delete state.cacheRequests[activeServerId][key][itemId]
delete state.cacheRequests[activeServerId][key][itemId] })
}), }
) })
set(state => {
state.cacheFiles[activeServerId][key][itemId] = {
path,
date: Date.now(),
permanent: false,
}
state.cacheRequests[activeServerId][key][itemId] = {
progress: 0,
promise,
} }
}) })
set(
produce<Store>(state => {
state.cacheFiles[activeServerId][key][itemId] = {
path,
date: Date.now(),
permanent: false,
}
state.cacheRequests[activeServerId][key][itemId] = {
progress: 0,
promise,
}
}),
)
return await promise return await promise
}, },
@@ -173,54 +165,48 @@ export const createCacheSlice = (set: SetState<Store>, get: GetState<Store>): Ca
await mkdir(`${RNFS.DocumentDirectoryPath}/servers/${serverId}/${type}`) await mkdir(`${RNFS.DocumentDirectoryPath}/servers/${serverId}/${type}`)
} }
set( set(state => {
produce<CacheSlice>(state => { state.cacheFiles[serverId] = {
state.cacheFiles[serverId] = { song: {},
coverArt: {},
coverArtThumb: {},
artistArt: {},
artistArtThumb: {},
}
})
get().prepareCache(serverId)
},
prepareCache: serverId => {
set(state => {
if (!state.cacheDirs[serverId]) {
state.cacheDirs[serverId] = {
song: `${RNFS.DocumentDirectoryPath}/servers/${serverId}/song`,
coverArt: `${RNFS.DocumentDirectoryPath}/servers/${serverId}/coverArt`,
coverArtThumb: `${RNFS.DocumentDirectoryPath}/servers/${serverId}/coverArtThumb`,
artistArt: `${RNFS.DocumentDirectoryPath}/servers/${serverId}/artistArt`,
artistArtThumb: `${RNFS.DocumentDirectoryPath}/servers/${serverId}/artistArtThumb`,
}
}
if (!state.cacheRequests[serverId]) {
state.cacheRequests[serverId] = {
song: {}, song: {},
coverArt: {}, coverArt: {},
coverArtThumb: {}, coverArtThumb: {},
artistArt: {}, artistArt: {},
artistArtThumb: {}, artistArtThumb: {},
} }
}), }
) })
get().prepareCache(serverId)
},
prepareCache: serverId => {
set(
produce<CacheSlice>(state => {
if (!state.cacheDirs[serverId]) {
state.cacheDirs[serverId] = {
song: `${RNFS.DocumentDirectoryPath}/servers/${serverId}/song`,
coverArt: `${RNFS.DocumentDirectoryPath}/servers/${serverId}/coverArt`,
coverArtThumb: `${RNFS.DocumentDirectoryPath}/servers/${serverId}/coverArtThumb`,
artistArt: `${RNFS.DocumentDirectoryPath}/servers/${serverId}/artistArt`,
artistArtThumb: `${RNFS.DocumentDirectoryPath}/servers/${serverId}/artistArtThumb`,
}
}
if (!state.cacheRequests[serverId]) {
state.cacheRequests[serverId] = {
song: {},
coverArt: {},
coverArtThumb: {},
artistArt: {},
artistArtThumb: {},
}
}
}),
)
}, },
pendingRemoval: {}, pendingRemoval: {},
removeCache: async serverId => { removeCache: async serverId => {
set( set(state => {
produce<CacheSlice>(state => { state.pendingRemoval[serverId] = true
state.pendingRemoval[serverId] = true })
}),
)
const cacheRequests = get().cacheRequests[serverId] const cacheRequests = get().cacheRequests[serverId]
const pendingRequests: Promise<void>[] = [] const pendingRequests: Promise<void>[] = []
@@ -235,21 +221,19 @@ export const createCacheSlice = (set: SetState<Store>, get: GetState<Store>): Ca
await Promise.all(pendingRequests) await Promise.all(pendingRequests)
await rmdir(`${RNFS.DocumentDirectoryPath}/servers/${serverId}`) await rmdir(`${RNFS.DocumentDirectoryPath}/servers/${serverId}`)
set( set(state => {
produce<CacheSlice>(state => { delete state.pendingRemoval[serverId]
delete state.pendingRemoval[serverId]
if (state.cacheDirs[serverId]) { if (state.cacheDirs[serverId]) {
delete state.cacheDirs[serverId] delete state.cacheDirs[serverId]
} }
if (state.cacheFiles[serverId]) { if (state.cacheFiles[serverId]) {
delete state.cacheFiles[serverId] delete state.cacheFiles[serverId]
} }
if (state.cacheRequests[serverId]) { if (state.cacheRequests[serverId]) {
delete state.cacheRequests[serverId] delete state.cacheRequests[serverId]
} }
}), })
)
}, },
clearImageCache: async () => { clearImageCache: async () => {
@@ -270,14 +254,12 @@ export const createCacheSlice = (set: SetState<Store>, get: GetState<Store>): Ca
await rmdir(get().cacheDirs[serverId].artistArt) await rmdir(get().cacheDirs[serverId].artistArt)
await mkdir(get().cacheDirs[serverId].artistArt) await mkdir(get().cacheDirs[serverId].artistArt)
set( set(state => {
produce<CacheSlice>(state => { state.cacheFiles[serverId].coverArt = {}
state.cacheFiles[serverId].coverArt = {} state.cacheFiles[serverId].coverArtThumb = {}
state.cacheFiles[serverId].coverArtThumb = {} state.cacheFiles[serverId].artistArt = {}
state.cacheFiles[serverId].artistArt = {} state.cacheFiles[serverId].artistArtThumb = {}
state.cacheFiles[serverId].artistArtThumb = {} })
}),
)
} }
}, },
}) })

View File

@@ -1,6 +1,6 @@
import { Album, Artist, ArtistInfo, Playlist, SearchResults, Song } from '@app/models/library' import { Album, Artist, ArtistInfo, Playlist, SearchResults, Song } from '@app/models/library'
import { ById, OneToMany } from '@app/models/state' import { ById, OneToMany } from '@app/models/state'
import { Store } from '@app/state/store' import { GetStore, SetStore, Store } from '@app/state/store'
import { import {
AlbumID3Element, AlbumID3Element,
ArtistID3Element, ArtistID3Element,
@@ -24,10 +24,8 @@ import {
} from '@app/subsonic/responses' } from '@app/subsonic/responses'
import PromiseQueue from '@app/util/PromiseQueue' import PromiseQueue from '@app/util/PromiseQueue'
import { reduceById, mergeById } from '@app/util/state' import { reduceById, mergeById } from '@app/util/state'
import produce from 'immer'
import { WritableDraft } from 'immer/dist/types/types-external' import { WritableDraft } from 'immer/dist/types/types-external'
import pick from 'lodash.pick' import pick from 'lodash.pick'
import { GetState, SetState } from 'zustand'
const songCoverArtQueue = new PromiseQueue(2) const songCoverArtQueue = new PromiseQueue(2)
@@ -84,7 +82,7 @@ const defaultEntities = () => ({
songs: {}, songs: {},
}) })
export const createLibrarySlice = (set: SetState<Store>, get: GetState<Store>): LibrarySlice => ({ export const createLibrarySlice = (set: SetStore, get: GetStore): LibrarySlice => ({
entities: defaultEntities(), entities: defaultEntities(),
resetLibrary: state => { resetLibrary: state => {
@@ -92,9 +90,7 @@ export const createLibrarySlice = (set: SetState<Store>, get: GetState<Store>):
state.entities = defaultEntities() state.entities = defaultEntities()
return return
} }
set(store => { set(store => (store.entities = defaultEntities()))
store.entities = defaultEntities()
})
}, },
fetchArtists: async () => { fetchArtists: async () => {
@@ -113,12 +109,10 @@ export const createLibrarySlice = (set: SetState<Store>, get: GetState<Store>):
const artists = response.data.artists.map(mapArtist) const artists = response.data.artists.map(mapArtist)
const artistsById = reduceById(artists) const artistsById = reduceById(artists)
set( set(state => {
produce<LibrarySlice>(state => { state.entities.artists = artistsById
state.entities.artists = artistsById state.entities.artistAlbums = pick(state.entities.artistAlbums, mapId(artists))
state.entities.artistAlbums = pick(state.entities.artistAlbums, mapId(artists)) })
}),
)
}, },
fetchArtist: async id => { fetchArtist: async id => {
@@ -138,13 +132,11 @@ export const createLibrarySlice = (set: SetState<Store>, get: GetState<Store>):
const albums = response.data.albums.map(mapAlbum) const albums = response.data.albums.map(mapAlbum)
const albumsById = reduceById(albums) const albumsById = reduceById(albums)
set( set(state => {
produce<LibrarySlice>(state => { state.entities.artists[id] = artist
state.entities.artists[id] = artist state.entities.artistAlbums[id] = mapId(albums)
state.entities.artistAlbums[id] = mapId(albums) mergeById(state.entities.albums, albumsById)
mergeById(state.entities.albums, albumsById) })
}),
)
}, },
fetchArtistInfo: async id => { fetchArtistInfo: async id => {
@@ -162,11 +154,9 @@ export const createLibrarySlice = (set: SetState<Store>, get: GetState<Store>):
const info = mapArtistInfo(id, response.data.artistInfo) const info = mapArtistInfo(id, response.data.artistInfo)
set( set(state => {
produce<LibrarySlice>(state => { state.entities.artistInfo[id] = info
state.entities.artistInfo[id] = info })
}),
)
}, },
fetchArtistTopSongs: async artistName => { fetchArtistTopSongs: async artistName => {
@@ -187,12 +177,10 @@ export const createLibrarySlice = (set: SetState<Store>, get: GetState<Store>):
get()._fixSongCoverArt(topSongs) get()._fixSongCoverArt(topSongs)
set( set(state => {
produce<LibrarySlice>(state => { mergeById(state.entities.songs, topSongsById)
mergeById(state.entities.songs, topSongsById) state.entities.artistNameTopSongs[artistName] = mapId(topSongs)
state.entities.artistNameTopSongs[artistName] = mapId(topSongs) })
}),
)
}, },
fetchAlbum: async id => { fetchAlbum: async id => {
@@ -214,13 +202,11 @@ export const createLibrarySlice = (set: SetState<Store>, get: GetState<Store>):
get()._fixSongCoverArt(songs) get()._fixSongCoverArt(songs)
set( set(state => {
produce<LibrarySlice>(state => { state.entities.albums[id] = album
state.entities.albums[id] = album state.entities.albumSongs[id] = mapId(songs)
state.entities.albumSongs[id] = mapId(songs) mergeById(state.entities.songs, songsById)
mergeById(state.entities.songs, songsById) })
}),
)
}, },
fetchPlaylists: async () => { fetchPlaylists: async () => {
@@ -239,12 +225,10 @@ export const createLibrarySlice = (set: SetState<Store>, get: GetState<Store>):
const playlists = response.data.playlists.map(mapPlaylist) const playlists = response.data.playlists.map(mapPlaylist)
const playlistsById = reduceById(playlists) const playlistsById = reduceById(playlists)
set( set(state => {
produce<LibrarySlice>(state => { state.entities.playlists = playlistsById
state.entities.playlists = playlistsById state.entities.playlistSongs = pick(state.entities.playlistSongs, mapId(playlists))
state.entities.playlistSongs = pick(state.entities.playlistSongs, mapId(playlists)) })
}),
)
}, },
fetchPlaylist: async id => { fetchPlaylist: async id => {
@@ -266,13 +250,11 @@ export const createLibrarySlice = (set: SetState<Store>, get: GetState<Store>):
get()._fixSongCoverArt(songs) get()._fixSongCoverArt(songs)
set( set(state => {
produce<LibrarySlice>(state => { state.entities.playlists[id] = playlist
state.entities.playlists[id] = playlist state.entities.playlistSongs[id] = mapId(songs)
state.entities.playlistSongs[id] = mapId(songs) mergeById(state.entities.songs, songsById)
mergeById(state.entities.songs, songsById) })
}),
)
}, },
fetchSong: async id => { fetchSong: async id => {
@@ -292,11 +274,9 @@ export const createLibrarySlice = (set: SetState<Store>, get: GetState<Store>):
get()._fixSongCoverArt([song]) get()._fixSongCoverArt([song])
set( set(state => {
produce<LibrarySlice>(state => { state.entities.songs[id] = song
state.entities.songs[id] = song })
}),
)
}, },
fetchAlbumList: async params => { fetchAlbumList: async params => {
@@ -315,11 +295,7 @@ export const createLibrarySlice = (set: SetState<Store>, get: GetState<Store>):
const albums = response.data.albums.map(mapAlbum) const albums = response.data.albums.map(mapAlbum)
const albumsById = reduceById(albums) const albumsById = reduceById(albums)
set( set(state => mergeById(state.entities.albums, albumsById))
produce<LibrarySlice>(state => {
mergeById(state.entities.albums, albumsById)
}),
)
return mapId(albums) return mapId(albums)
}, },
@@ -348,13 +324,11 @@ export const createLibrarySlice = (set: SetState<Store>, get: GetState<Store>):
get()._fixSongCoverArt(songs) get()._fixSongCoverArt(songs)
set( set(state => {
produce<LibrarySlice>(state => { mergeById(state.entities.artists, artistsById)
mergeById(state.entities.artists, artistsById) mergeById(state.entities.albums, albumsById)
mergeById(state.entities.albums, albumsById) mergeById(state.entities.songs, songsById)
mergeById(state.entities.songs, songsById) })
}),
)
return { return {
artists: mapId(artists), artists: mapId(artists),
@@ -387,22 +361,18 @@ export const createLibrarySlice = (set: SetState<Store>, get: GetState<Store>):
const item = get().entities[entity][id] const item = get().entities[entity][id]
const originalValue = item ? item.starred : null const originalValue = item ? item.starred : null
set( set(state => {
produce<LibrarySlice>(state => { state.entities[entity][id].starred = new Date()
state.entities[entity][id].starred = new Date() })
}),
)
try { try {
await client.star(params) await client.star(params)
} catch { } catch {
set( set(state => {
produce<LibrarySlice>(state => { if (originalValue !== null) {
if (originalValue !== null) { state.entities[entity][id].starred = originalValue
state.entities[entity][id].starred = originalValue }
} })
}),
)
} }
}, },
@@ -430,22 +400,18 @@ export const createLibrarySlice = (set: SetState<Store>, get: GetState<Store>):
const item = get().entities[entity][id] const item = get().entities[entity][id]
const originalValue = item ? item.starred : null const originalValue = item ? item.starred : null
set( set(state => {
produce<LibrarySlice>(state => { state.entities[entity][id].starred = undefined
state.entities[entity][id].starred = undefined })
}),
)
try { try {
await client.unstar(params) await client.unstar(params)
} catch { } catch {
set( set(state => {
produce<LibrarySlice>(state => { if (originalValue !== null) {
if (originalValue !== null) { state.entities[entity][id].starred = originalValue
state.entities[entity][id].starred = originalValue }
} })
}),
)
} }
}, },
@@ -479,14 +445,12 @@ export const createLibrarySlice = (set: SetState<Store>, get: GetState<Store>):
.then(res => { .then(res => {
const album = mapAlbum(res.data.album) const album = mapAlbum(res.data.album)
set( set(state => {
produce<LibrarySlice>(state => { state.entities.albums[album.id] = album
state.entities.albums[album.id] = album for (const song of albumsToGet[album.id]) {
for (const song of albumsToGet[album.id]) { state.entities.songs[song.id].coverArt = album.coverArt
state.entities.songs[song.id].coverArt = album.coverArt }
} })
}),
)
}) })
} }
}, },

View File

@@ -1,8 +1,6 @@
import { AppSettings, ArtistFilterSettings, AlbumFilterSettings, Server } from '@app/models/settings' import { AlbumFilterSettings, AppSettings, ArtistFilterSettings, Server } from '@app/models/settings'
import { Store } from '@app/state/store' import { GetStore, SetStore } from '@app/state/store'
import { SubsonicApiClient } from '@app/subsonic/api' import { SubsonicApiClient } from '@app/subsonic/api'
import produce from 'immer'
import { GetState, SetState } from 'zustand'
export type SettingsSlice = { export type SettingsSlice = {
settings: AppSettings settings: AppSettings
@@ -66,7 +64,7 @@ export const selectSettings = {
libraryArtistFilter: (state: SettingsSlice) => state.settings.screens.library.artists, libraryArtistFilter: (state: SettingsSlice) => state.settings.screens.library.artists,
} }
export const createSettingsSlice = (set: SetState<Store>, get: GetState<Store>): SettingsSlice => ({ export const createSettingsSlice = (set: SetStore, get: GetStore): SettingsSlice => ({
settings: { settings: {
servers: [], servers: [],
screens: { screens: {
@@ -99,8 +97,8 @@ export const createSettingsSlice = (set: SetState<Store>, get: GetState<Store>):
const newActiveServer = servers.find(s => s.id === id) const newActiveServer = servers.find(s => s.id === id)
if (!newActiveServer) { if (!newActiveServer) {
set({ set(state => {
client: undefined, state.client = undefined
}) })
return return
} }
@@ -111,13 +109,11 @@ export const createSettingsSlice = (set: SetState<Store>, get: GetState<Store>):
get().prepareCache(newActiveServer.id) get().prepareCache(newActiveServer.id)
set( set(state => {
produce<Store>(state => { state.settings.activeServer = newActiveServer.id
state.settings.activeServer = newActiveServer.id state.client = new SubsonicApiClient(newActiveServer)
state.client = new SubsonicApiClient(newActiveServer) get().resetLibrary(state)
get().resetLibrary(state) })
}),
)
}, },
getActiveServer: () => get().settings.servers.find(s => s.id === get().settings.activeServer), getActiveServer: () => get().settings.servers.find(s => s.id === get().settings.activeServer),
@@ -125,11 +121,7 @@ export const createSettingsSlice = (set: SetState<Store>, get: GetState<Store>):
addServer: async server => { addServer: async server => {
await get().createCache(server.id) await get().createCache(server.id)
set( set(state => state.settings.servers.push(server))
produce<SettingsSlice>(state => {
state.settings.servers.push(server)
}),
)
if (get().settings.servers.length === 1) { if (get().settings.servers.length === 1) {
get().setActiveServer(server.id) get().setActiveServer(server.id)
@@ -139,23 +131,19 @@ export const createSettingsSlice = (set: SetState<Store>, get: GetState<Store>):
removeServer: async id => { removeServer: async id => {
await get().removeCache(id) await get().removeCache(id)
set( set(state => {
produce<SettingsSlice>(state => { state.settings.servers = state.settings.servers.filter(s => s.id !== id)
state.settings.servers = state.settings.servers.filter(s => s.id !== id) })
}),
)
}, },
updateServer: server => { updateServer: server => {
set( set(state => {
produce<SettingsSlice>(state => { state.settings.servers = replaceIndex(
state.settings.servers = replaceIndex( state.settings.servers,
state.settings.servers, state.settings.servers.findIndex(s => s.id === server.id),
state.settings.servers.findIndex(s => s.id === server.id), server,
server, )
) })
}),
)
if (get().settings.activeServer === server.id) { if (get().settings.activeServer === server.id) {
get().setActiveServer(server.id, true) get().setActiveServer(server.id, true)
@@ -163,29 +151,22 @@ export const createSettingsSlice = (set: SetState<Store>, get: GetState<Store>):
}, },
setScrobble: scrobble => { setScrobble: scrobble => {
set( set(state => {
produce<SettingsSlice>(state => { state.settings.scrobble = scrobble
state.settings.scrobble = scrobble })
}),
)
}, },
setEstimateContentLength: estimateContentLength => { setEstimateContentLength: estimateContentLength => {
set( set(state => {
produce<SettingsSlice>(state => { state.settings.estimateContentLength = estimateContentLength
state.settings.estimateContentLength = estimateContentLength })
}),
)
get().rebuildQueue() get().rebuildQueue()
}, },
setMaxBitrateWifi: maxBitrateWifi => { setMaxBitrateWifi: maxBitrateWifi => {
set( set(state => {
produce<SettingsSlice>(state => { state.settings.maxBitrateWifi = maxBitrateWifi
state.settings.maxBitrateWifi = maxBitrateWifi })
}),
)
if (get().netState === 'wifi') { if (get().netState === 'wifi') {
get().rebuildQueue() get().rebuildQueue()
@@ -193,11 +174,9 @@ export const createSettingsSlice = (set: SetState<Store>, get: GetState<Store>):
}, },
setMaxBitrateMobile: maxBitrateMobile => { setMaxBitrateMobile: maxBitrateMobile => {
set( set(state => {
produce<SettingsSlice>(state => { state.settings.maxBitrateMobile = maxBitrateMobile
state.settings.maxBitrateMobile = maxBitrateMobile })
}),
)
if (get().netState === 'mobile') { if (get().netState === 'mobile') {
get().rebuildQueue() get().rebuildQueue()
@@ -209,11 +188,9 @@ export const createSettingsSlice = (set: SetState<Store>, get: GetState<Store>):
return return
} }
set( set(state => {
produce<SettingsSlice>(state => { state.settings.minBuffer = Math.max(1, Math.min(minBuffer, state.settings.maxBuffer / 2))
state.settings.minBuffer = Math.max(1, Math.min(minBuffer, state.settings.maxBuffer / 2)) })
}),
)
get().rebuildQueue() get().rebuildQueue()
}, },
@@ -223,11 +200,9 @@ export const createSettingsSlice = (set: SetState<Store>, get: GetState<Store>):
return return
} }
set( set(state => {
produce<SettingsSlice>(state => { state.settings.maxBuffer = Math.min(5 * 60, Math.max(maxBuffer, state.settings.minBuffer * 2))
state.settings.maxBuffer = Math.min(5 * 60, Math.max(maxBuffer, state.settings.minBuffer * 2)) })
}),
)
get().rebuildQueue() get().rebuildQueue()
}, },
@@ -253,19 +228,15 @@ export const createSettingsSlice = (set: SetState<Store>, get: GetState<Store>):
}, },
setLibraryAlbumFilter: filter => { setLibraryAlbumFilter: filter => {
set( set(state => {
produce<SettingsSlice>(state => { state.settings.screens.library.albums = filter
state.settings.screens.library.albums = filter })
}),
)
}, },
setLibraryArtistFiler: filter => { setLibraryArtistFiler: filter => {
set( set(state => {
produce<SettingsSlice>(state => { state.settings.screens.library.artists = filter
state.settings.screens.library.artists = filter })
}),
)
}, },
}) })

View File

@@ -1,13 +1,15 @@
import { createSettingsSlice, SettingsSlice } from '@app/state/settings' import { createSettingsSlice, SettingsSlice } from '@app/state/settings'
import AsyncStorage from '@react-native-async-storage/async-storage' import AsyncStorage from '@react-native-async-storage/async-storage'
import equal from 'fast-deep-equal/es6/react' import equal from 'fast-deep-equal/es6/react'
import create, { GetState, Mutate, SetState, StateSelector, StoreApi } from 'zustand' import create, { GetState, Mutate, SetState, State, StateCreator, StateSelector, StoreApi } from 'zustand'
import { persist, subscribeWithSelector } from 'zustand/middleware' import { persist, subscribeWithSelector } from 'zustand/middleware'
import { CacheSlice, createCacheSlice } from './cache' import { CacheSlice, createCacheSlice } from './cache'
import { createLibrarySlice, LibrarySlice } from './library' import { createLibrarySlice, LibrarySlice } from './library'
import migrations from './migrations' import migrations from './migrations'
import { createTrackPlayerSlice, TrackPlayerSlice } from './trackplayer' import { createTrackPlayerSlice, TrackPlayerSlice } from './trackplayer'
import { createTrackPlayerMapSlice, TrackPlayerMapSlice } from './trackplayermap' import { createTrackPlayerMapSlice, TrackPlayerMapSlice } from './trackplayermap'
import produce, { Draft } from 'immer'
import { WritableDraft } from 'immer/dist/internal'
const DB_VERSION = migrations.length const DB_VERSION = migrations.length
@@ -20,6 +22,37 @@ export type Store = SettingsSlice &
setHydrated: (hydrated: boolean) => void setHydrated: (hydrated: boolean) => void
} }
// taken from zustand test examples:
// https://github.com/pmndrs/zustand/blob/v3.7.1/tests/middlewareTypes.test.tsx#L20
const immer =
<
T extends State,
CustomSetState extends SetState<T>,
CustomGetState extends GetState<T>,
CustomStoreApi extends StoreApi<T>,
>(
config: StateCreator<
T,
(partial: ((draft: Draft<T>) => void) | T, replace?: boolean) => void,
CustomGetState,
CustomStoreApi
>,
): StateCreator<T, CustomSetState, CustomGetState, CustomStoreApi> =>
(set, get, api) =>
config(
(partial, replace) => {
const nextState = typeof partial === 'function' ? produce(partial as (state: Draft<T>) => T) : (partial as T)
return set(nextState, replace)
},
get,
api,
)
export type SetStore = (partial: Store | ((draft: WritableDraft<Store>) => void), replace?: boolean | undefined) => void
export type GetStore = GetState<Store>
// types taken from zustand test examples:
// https://github.com/pmndrs/zustand/blob/v3.7.1/tests/middlewareTypes.test.tsx#L584
export const useStore = create< export const useStore = create<
Store, Store,
SetState<Store>, SetState<Store>,
@@ -28,7 +61,7 @@ export const useStore = create<
>( >(
subscribeWithSelector( subscribeWithSelector(
persist( persist(
(set, get) => ({ immer((set, get) => ({
...createSettingsSlice(set, get), ...createSettingsSlice(set, get),
...createLibrarySlice(set, get), ...createLibrarySlice(set, get),
...createTrackPlayerSlice(set, get), ...createTrackPlayerSlice(set, get),
@@ -36,13 +69,15 @@ export const useStore = create<
...createCacheSlice(set, get), ...createCacheSlice(set, get),
hydrated: false, hydrated: false,
setHydrated: hydrated => set({ hydrated }), setHydrated: hydrated =>
}), set(state => {
state.hydrated = hydrated
}),
})),
{ {
name: '@appStore', name: '@appStore',
version: DB_VERSION, version: DB_VERSION,
getStorage: () => AsyncStorage, getStorage: () => AsyncStorage,
// whitelist: ['settings', 'cacheFiles'],
partialize: state => ({ settings: state.settings, cacheFiles: state.cacheFiles }), partialize: state => ({ settings: state.settings, cacheFiles: state.cacheFiles }),
onRehydrateStorage: _preState => { onRehydrateStorage: _preState => {
return async (postState, _error) => { return async (postState, _error) => {

View File

@@ -3,8 +3,7 @@ import { Song } from '@app/models/library'
import PromiseQueue from '@app/util/PromiseQueue' import PromiseQueue from '@app/util/PromiseQueue'
import produce from 'immer' import produce from 'immer'
import TrackPlayer, { PlayerOptions, RepeatMode, State, Track } from 'react-native-track-player' import TrackPlayer, { PlayerOptions, RepeatMode, State, Track } from 'react-native-track-player'
import { GetState, SetState } from 'zustand' import { GetStore, SetStore } from './store'
import { Store } from './store'
export type TrackExt = Track & { export type TrackExt = Track & {
id: string id: string
@@ -114,15 +113,24 @@ export const selectTrackPlayer = {
export const trackPlayerCommands = new PromiseQueue(1) export const trackPlayerCommands = new PromiseQueue(1)
export const createTrackPlayerSlice = (set: SetState<Store>, get: GetState<Store>): TrackPlayerSlice => ({ export const createTrackPlayerSlice = (set: SetStore, get: GetStore): TrackPlayerSlice => ({
queueName: undefined, queueName: undefined,
setQueueName: name => set({ queueName: name }), setQueueName: name =>
set(state => {
state.queueName = name
}),
queueContextType: undefined, queueContextType: undefined,
setQueueContextType: queueContextType => set({ queueContextType }), setQueueContextType: queueContextType =>
set(state => {
state.queueContextType = queueContextType
}),
queueContextId: undefined, queueContextId: undefined,
setQueueContextId: queueContextId => set({ queueContextId }), setQueueContextId: queueContextId =>
set(state => {
state.queueContextId = queueContextId
}),
shuffleOrder: undefined, shuffleOrder: undefined,
toggleShuffle: async () => { toggleShuffle: async () => {
@@ -140,7 +148,9 @@ export const createTrackPlayerSlice = (set: SetState<Store>, get: GetState<Store
} }
await TrackPlayer.add(tracks) await TrackPlayer.add(tracks)
set({ shuffleOrder }) set(state => {
state.shuffleOrder = shuffleOrder
})
} else { } else {
const tracks = unshuffleTracks(queue, queueShuffleOrder) const tracks = unshuffleTracks(queue, queueShuffleOrder)
@@ -155,10 +165,14 @@ export const createTrackPlayerSlice = (set: SetState<Store>, get: GetState<Store
await TrackPlayer.add(tracks) await TrackPlayer.add(tracks)
} }
set({ shuffleOrder: undefined }) set(state => {
state.shuffleOrder = undefined
})
} }
set({ queue: await getQueue() }) set(async state => {
state.queue = await getQueue()
})
get().setCurrentTrackIdx(await getCurrentTrack()) get().setCurrentTrackIdx(await getCurrentTrack())
}) })
}, },
@@ -182,12 +196,17 @@ export const createTrackPlayerSlice = (set: SetState<Store>, get: GetState<Store
} }
await TrackPlayer.setRepeatMode(nextMode) await TrackPlayer.setRepeatMode(nextMode)
set({ repeatMode: nextMode }) set(state => {
state.repeatMode = nextMode
})
}) })
}, },
playerState: State.None, playerState: State.None,
setPlayerState: playerState => set({ playerState }), setPlayerState: playerState =>
set(state => {
state.playerState = playerState
}),
currentTrack: undefined, currentTrack: undefined,
currentTrackIdx: undefined, currentTrackIdx: undefined,
@@ -201,7 +220,10 @@ export const createTrackPlayerSlice = (set: SetState<Store>, get: GetState<Store
}, },
duckPaused: false, duckPaused: false,
setDuckPaused: duckPaused => set({ duckPaused }), setDuckPaused: duckPaused =>
set(state => {
state.duckPaused = duckPaused
}),
queue: [], queue: [],
setQueue: async (songs, name, contextType, contextId, playTrack, shuffle) => { setQueue: async (songs, name, contextType, contextId, playTrack, shuffle) => {
@@ -219,21 +241,25 @@ export const createTrackPlayerSlice = (set: SetState<Store>, get: GetState<Store
if (shuffled) { if (shuffled) {
const { tracks, shuffleOrder } = shuffleTracks(queue, playTrack) const { tracks, shuffleOrder } = shuffleTracks(queue, playTrack)
set({ shuffleOrder }) set(state => {
state.shuffleOrder = shuffleOrder
})
queue = tracks queue = tracks
playTrack = 0 playTrack = 0
} else { } else {
set({ shuffleOrder: undefined }) set(state => {
state.shuffleOrder = undefined
})
} }
playTrack = playTrack || 0 playTrack = playTrack || 0
try { try {
set({ set(state => {
queue, state.queue = queue
queueName: name, state.queueName = name
queueContextType: contextType, state.queueContextType = contextType
queueContextId: contextId, state.queueContextId = contextId
}) })
get().setCurrentTrackIdx(playTrack) get().setCurrentTrackIdx(playTrack)
@@ -256,7 +282,10 @@ export const createTrackPlayerSlice = (set: SetState<Store>, get: GetState<Store
}, },
progress: { position: 0, duration: 0, buffered: 0 }, progress: { position: 0, duration: 0, buffered: 0 },
setProgress: progress => set({ progress }), setProgress: progress =>
set(state => {
state.progress = progress
}),
scrobbleTrack: async id => { scrobbleTrack: async id => {
const client = get().client const client = get().client
@@ -278,7 +307,9 @@ export const createTrackPlayerSlice = (set: SetState<Store>, get: GetState<Store
if (netState === get().netState) { if (netState === get().netState) {
return return
} }
set({ netState }) set(state => {
state.netState = netState
})
get().rebuildQueue() get().rebuildQueue()
}, },
@@ -290,7 +321,7 @@ export const createTrackPlayerSlice = (set: SetState<Store>, get: GetState<Store
} }
const currentTrack = await getCurrentTrack() const currentTrack = await getCurrentTrack()
const state = await getPlayerState() const playerState = await getPlayerState()
const position = (await TrackPlayer.getPosition()) || 0 const position = (await TrackPlayer.getPosition()) || 0
const queueName = get().queueName const queueName = get().queueName
@@ -308,11 +339,11 @@ export const createTrackPlayerSlice = (set: SetState<Store>, get: GetState<Store
return return
} }
set({ set(state => {
queue, state.queue = queue
queueName, state.queueName = queueName
queueContextId, state.queueContextType = queueContextType
queueContextType, state.queueContextId = queueContextId
}) })
get().setCurrentTrackIdx(currentTrack) get().setCurrentTrackIdx(currentTrack)
@@ -324,7 +355,7 @@ export const createTrackPlayerSlice = (set: SetState<Store>, get: GetState<Store
await TrackPlayer.seekTo(position) await TrackPlayer.seekTo(position)
if (state === State.Playing || forcePlay) { if (playerState === State.Playing || forcePlay) {
await TrackPlayer.play() await TrackPlayer.play()
} }
}) })
@@ -344,17 +375,17 @@ export const createTrackPlayerSlice = (set: SetState<Store>, get: GetState<Store
}, },
resetTrackPlayerState: () => { resetTrackPlayerState: () => {
set({ set(state => {
queueName: undefined, state.queueName = undefined
queueContextType: undefined, state.queueContextType = undefined
queueContextId: undefined, state.queueContextId = undefined
shuffleOrder: undefined, state.shuffleOrder = undefined
repeatMode: RepeatMode.Off, state.repeatMode = RepeatMode.Off
playerState: State.None, state.playerState = State.None
currentTrack: undefined, state.currentTrack = undefined
currentTrackIdx: undefined, state.currentTrackIdx = undefined
queue: [], state.queue = []
progress: { position: 0, duration: 0, buffered: 0 }, state.progress = { position: 0, duration: 0, buffered: 0 }
}) })
}, },

View File

@@ -1,7 +1,6 @@
import { Song } from '@app/models/library' import { Song } from '@app/models/library'
import userAgent from '@app/util/userAgent' import userAgent from '@app/util/userAgent'
import { GetState, SetState } from 'zustand' import { GetStore, SetStore } from './store'
import { Store } from './store'
import { TrackExt } from './trackplayer' import { TrackExt } from './trackplayer'
export type TrackPlayerMapSlice = { export type TrackPlayerMapSlice = {
@@ -14,7 +13,7 @@ export const selectTrackPlayerMap = {
mapTrackExtToSong: (store: TrackPlayerMapSlice) => store.mapTrackExtToSong, mapTrackExtToSong: (store: TrackPlayerMapSlice) => store.mapTrackExtToSong,
} }
export const createTrackPlayerMapSlice = (set: SetState<Store>, get: GetState<Store>): TrackPlayerMapSlice => ({ export const createTrackPlayerMapSlice = (set: SetStore, get: GetStore): TrackPlayerMapSlice => ({
mapSongtoTrackExt: async song => { mapSongtoTrackExt: async song => {
let artwork = require('@res/fallback.png') let artwork = require('@res/fallback.png')
if (song.coverArt) { if (song.coverArt) {