diff --git a/app/hooks/cache.ts b/app/hooks/cache.ts index 71e74ca..3fbd6af 100644 --- a/app/hooks/cache.ts +++ b/app/hooks/cache.ts @@ -1,9 +1,7 @@ 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) => { @@ -61,28 +59,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.fetchLibraryArtistInfo) + const artistInfo = useStoreDeep(store => store.entities.artistInfo[artistId]) const { file, request } = useFileRequest(type, artistId) const cacheItem = useStore(selectCache.cacheItem) useEffect(() => { + if (!artistInfo) { + fetchArtistInfo(artistId) + return + } + if (!file) { 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 } } diff --git a/app/hooks/music.ts b/app/hooks/music.ts index 26fc962..7e25c43 100644 --- a/app/hooks/music.ts +++ b/app/hooks/music.ts @@ -1,10 +1,10 @@ import { selectMusic } from '@app/state/music' -import { Store, useStore } from '@app/state/store' +import { Store, useStore, useStoreDeep } 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) + const artistInfo = useStoreDeep(useCallback(store => store.entities.artistInfo[id], [id])) + const fetchArtistInfo = useStore(store => store.fetchLibraryArtistInfo) useEffect(() => { if (!artistInfo) { diff --git a/app/playbackservice.ts b/app/playbackservice.ts index fa1b1bb..c926944 100644 --- a/app/playbackservice.ts +++ b/app/playbackservice.ts @@ -1,4 +1,4 @@ -import { getCurrentTrack, getPlayerState, TrackExt, 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' @@ -44,13 +44,12 @@ let serviceCreated = false const createService = async () => { useStore.subscribe( - (currentTrack?: TrackExt) => { - if (currentTrack) { - useStore.getState().scrobbleTrack(currentTrack.id) + state => state.currentTrack?.id, + (currentTrackId?: string) => { + if (currentTrackId) { + useStore.getState().scrobbleTrack(currentTrackId) } }, - state => state.currentTrack, - (prev, next) => prev?.id === next?.id, ) NetInfo.fetch().then(state => { diff --git a/app/screens/ArtistView.tsx b/app/screens/ArtistView.tsx index 516e227..325961b 100644 --- a/app/screens/ArtistView.tsx +++ b/app/screens/ArtistView.tsx @@ -6,7 +6,7 @@ import Header from '@app/components/Header' import HeaderBar from '@app/components/HeaderBar' import ListItem from '@app/components/ListItem' import { Album, Song } from '@app/models/music' -import { useStore } from '@app/state/store' +import { useStore, useStoreDeep } from '@app/state/store' import { selectTrackPlayer } from '@app/state/trackplayer' import colors from '@app/styles/colors' import dimensions from '@app/styles/dimensions' @@ -70,7 +70,7 @@ const TopSongs = React.memo<{ const ArtistAlbums = React.memo<{ id: string }>(({ id }) => { - const albums = useStore( + const albums = useStoreDeep( useCallback( store => { const ids = store.entities.artistAlbums[id] @@ -114,8 +114,8 @@ const ArtistViewFallback = React.memo(() => ( )) const ArtistView = React.memo<{ id: string; title: string }>(({ id, title }) => { - const artist = useStore(useCallback(store => store.entities.artists[id], [id])) - const artistInfo = useStore(useCallback(store => store.entities.artistInfo[id], [id])) + const artist = useStoreDeep(useCallback(store => store.entities.artists[id], [id])) + const artistInfo = useStoreDeep(useCallback(store => store.entities.artistInfo[id], [id])) const fetchArtist = useStore(store => store.fetchLibraryArtist) const fetchArtistInfo = useStore(store => store.fetchLibraryArtistInfo) diff --git a/app/screens/Home.tsx b/app/screens/Home.tsx index ceb59f4..2cdf3b1 100644 --- a/app/screens/Home.tsx +++ b/app/screens/Home.tsx @@ -3,21 +3,20 @@ 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 { useFetchPaginatedList } from '@app/hooks/list' import { useActiveServerRefresh } from '@app/hooks/server' import { AlbumListItem } from '@app/models/music' -import { Album, mapById } from '@app/state/library' -import { selectMusic } from '@app/state/music' +import { mapById } from '@app/state/library' import { selectSettings } from '@app/state/settings' -import { Store, useStore } from '@app/state/store' +import { useStore, useStoreDeep } from '@app/state/store' import colors from '@app/styles/colors' import font from '@app/styles/font' -import { GetAlbumList2Params, GetAlbumList2TypeBase, GetAlbumListType } from '@app/subsonic/params' +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, { createContext, useCallback, useContext, useEffect, useState } from 'react' +import React, { useCallback, useState } from 'react' import { RefreshControl, ScrollView, StatusBar, StyleSheet, Text, View } from 'react-native' -import create from 'zustand' +import create, { StateSelector } from 'zustand' const titles: { [key in GetAlbumListType]?: string } = { recent: 'Recently Played', @@ -55,8 +54,8 @@ const AlbumItem = React.memo<{ const Category = React.memo<{ type: string }>(({ type }) => { - const list = useHomeStore(useCallback(store => store.lists[type] || [], [type])) - const albums = useStore(useCallback(store => mapById(store.entities.albums, list), [list])) + const list = useHomeStoreDeep(useCallback(store => store.lists[type] || [], [type])) + const albums = useStoreDeep(useCallback(store => mapById(store.entities.albums, list), [list])) const Albums = () => ( void } -const useHomeStore = create((set, get) => ({ +const useHomeStore = create(set => ({ lists: {}, setList: (type, list) => { @@ -102,6 +101,10 @@ const useHomeStore = create((set, get) => ({ }, })) +function useHomeStoreDeep(stateSelector: StateSelector) { + return useHomeStore(stateSelector, equal) +} + const Home = () => { const [refreshing, setRefreshing] = useState(false) const types = useStore(selectSettings.homeLists) @@ -113,9 +116,7 @@ const Home = () => { await Promise.all( types.map(async type => { - console.log('fetch', type) const ids = await fetchAlbumList({ type: type as GetAlbumList2TypeBase, size: 20, offset: 0 }) - console.log('set', type) setList(type, ids) }), ) diff --git a/app/screens/LibraryAlbums.tsx b/app/screens/LibraryAlbums.tsx index 42052b7..f0f8eed 100644 --- a/app/screens/LibraryAlbums.tsx +++ b/app/screens/LibraryAlbums.tsx @@ -6,13 +6,12 @@ import { useFetchPaginatedList } from '@app/hooks/list' import { Album, AlbumListItem } from '@app/models/music' import { mapById } from '@app/state/library' import { selectSettings } from '@app/state/settings' -import { Store, useStore } from '@app/state/store' +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 { useNavigation } from '@react-navigation/native' -import pick from 'lodash.pick' -import React, { useCallback, useEffect } from 'react' +import React, { useCallback } from 'react' import { StyleSheet, Text, useWindowDimensions, View } from 'react-native' const AlbumItem = React.memo<{ @@ -97,7 +96,7 @@ const AlbumsList = () => { ) const { list, refreshing, refresh, fetchNextPage } = useFetchPaginatedList(fetchPage, 300) - const albums = useStore(useCallback(store => mapById(store.entities.albums, list), [list])) + const albums = useStoreDeep(useCallback(store => mapById(store.entities.albums, list), [list])) const layout = useWindowDimensions() diff --git a/app/screens/LibraryArtists.tsx b/app/screens/LibraryArtists.tsx index a75b82c..939c846 100644 --- a/app/screens/LibraryArtists.tsx +++ b/app/screens/LibraryArtists.tsx @@ -5,7 +5,7 @@ import { useFetchList2 } from '@app/hooks/list' import { Artist } from '@app/models/music' import { ArtistFilterType } from '@app/models/settings' import { selectSettings } from '@app/state/settings' -import { Store, useStore } from '@app/state/store' +import { useStore, useStoreDeep } from '@app/state/store' import React, { useEffect, useState } from 'react' import { StyleSheet, View } from 'react-native' @@ -19,12 +19,10 @@ const filterOptions: OptionData[] = [ { text: 'Random', value: 'random' }, ] -const selectArtists = (store: Store) => store.entities.artists - const ArtistsList = () => { const fetchArtists = useStore(store => store.fetchLibraryArtists) const { refreshing, refresh } = useFetchList2(fetchArtists) - const artists = useStore(selectArtists) + const artists = useStoreDeep(store => store.entities.artists) const filter = useStore(selectSettings.libraryArtistFilter) const setFilter = useStore(selectSettings.setLibraryArtistFiler) diff --git a/app/screens/LibraryPlaylists.tsx b/app/screens/LibraryPlaylists.tsx index ab21981..6c9920b 100644 --- a/app/screens/LibraryPlaylists.tsx +++ b/app/screens/LibraryPlaylists.tsx @@ -1,10 +1,9 @@ import GradientFlatList from '@app/components/GradientFlatList' import ListItem from '@app/components/ListItem' -import { useFetchList, useFetchList2 } from '@app/hooks/list' +import { useFetchList2 } from '@app/hooks/list' import { PlaylistListItem } from '@app/models/music' -import { selectMusic } from '@app/state/music' -import { useStore } from '@app/state/store' -import React, { useCallback, useState } from 'react' +import { useStore, useStoreDeep } from '@app/state/store' +import React from 'react' import { StyleSheet } from 'react-native' const PlaylistRenderItem: React.FC<{ item: PlaylistListItem }> = ({ item }) => ( @@ -14,7 +13,7 @@ const PlaylistRenderItem: React.FC<{ item: PlaylistListItem }> = ({ item }) => ( const PlaylistsList = () => { const fetchPlaylists = useStore(store => store.fetchLibraryPlaylists) const { refreshing, refresh } = useFetchList2(fetchPlaylists) - const playlists = useStore(store => store.entities.playlists) + const playlists = useStoreDeep(store => store.entities.playlists) return ( (({ id, title }) => { // const album = useAlbumWithSongs(id) - const album = useStore(useCallback(store => store.entities.albums[id], [id])) - const songs = useStore( + const album = useStoreDeep(useCallback(store => store.entities.albums[id], [id])) + const songs = useStoreDeep( useCallback( store => { const ids = store.entities.albumSongs[id] diff --git a/app/state/cache.ts b/app/state/cache.ts index 9bc53e8..430cc54 100644 --- a/app/state/cache.ts +++ b/app/state/cache.ts @@ -20,16 +20,6 @@ export type CacheDirsByServer = Record> export type CacheFilesByServer = Record>> export type CacheRequestsByServer = Record>> -// export type DownloadedItemsByServer = Record< -// string, -// { -// songs: { [songId: string]: DownloadedSong } -// albums: { [albumId: string]: DownloadedAlbum } -// artists: { [songId: string]: DownloadedArtist } -// playlists: { [playlistId: string]: DownloadedPlaylist } -// } -// > - export type CacheSlice = { cacheItem: ( key: CacheItemTypeKey, diff --git a/app/state/library.ts b/app/state/library.ts index 065f586..d7dfdfd 100644 --- a/app/state/library.ts +++ b/app/state/library.ts @@ -166,10 +166,6 @@ export type LibrarySlice = { albums: ById albumSongs: OneToMany - // todo: remove these and store in component state - albumsList: PaginatedList - albumsListSize: number - playlists: ById playlistSongs: OneToMany @@ -216,8 +212,6 @@ const defaultEntities = () => ({ artistNameTopSongs: {}, albums: {}, - albumsList: {}, - albumsListSize: 300, albumSongs: {}, playlists: {}, diff --git a/app/state/store.ts b/app/state/store.ts index 69b37bf..c000496 100644 --- a/app/state/store.ts +++ b/app/state/store.ts @@ -1,11 +1,12 @@ import { createMusicSlice, MusicSlice } from '@app/state/music' import { createSettingsSlice, SettingsSlice } from '@app/state/settings' import AsyncStorage from '@react-native-async-storage/async-storage' -import create from 'zustand' -import { persist, StateStorage } from 'zustand/middleware' +import equal from 'fast-deep-equal/es6/react' +import create, { GetState, Mutate, SetState, StateSelector, StoreApi } from 'zustand' +import { persist, subscribeWithSelector } from 'zustand/middleware' import { CacheSlice, createCacheSlice } from './cache' -import migrations from './migrations' import { createLibrarySlice, LibrarySlice } from './library' +import migrations from './migrations' import { createMusicMapSlice, MusicMapSlice } from './musicmap' import { createTrackPlayerSlice, TrackPlayerSlice } from './trackplayer' import { createTrackPlayerMapSlice, TrackPlayerMapSlice } from './trackplayermap' @@ -23,60 +24,52 @@ export type Store = SettingsSlice & setHydrated: (hydrated: boolean) => void } -const storage: StateStorage = { - getItem: async name => { - try { - return await AsyncStorage.getItem(name) - } catch (err) { - console.error(`getItem error (key: ${name})`, err) - return null - } - }, - setItem: async (name, item) => { - try { - await AsyncStorage.setItem(name, item) - } catch (err) { - console.error(`setItem error (key: ${name})`, err) - } - }, -} +export const useStore = create< + Store, + SetState, + GetState, + Mutate, [['zustand/subscribeWithSelector', never], ['zustand/persist', Partial]]> +>( + subscribeWithSelector( + persist( + (set, get) => ({ + ...createSettingsSlice(set, get), + ...createMusicSlice(set, get), + ...createLibrarySlice(set, get), + ...createMusicMapSlice(set, get), + ...createTrackPlayerSlice(set, get), + ...createTrackPlayerMapSlice(set, get), + ...createCacheSlice(set, get), -export const useStore = create( - persist( - (set, get) => ({ - ...createSettingsSlice(set, get), - ...createMusicSlice(set, get), - ...createLibrarySlice(set, get), - ...createMusicMapSlice(set, get), - ...createTrackPlayerSlice(set, get), - ...createTrackPlayerMapSlice(set, get), - ...createCacheSlice(set, get), + hydrated: false, + setHydrated: hydrated => set({ hydrated }), + }), + { + name: '@appStore', + version: DB_VERSION, + getStorage: () => AsyncStorage, + // whitelist: ['settings', 'cacheFiles'], + partialize: state => ({ settings: state.settings, cacheFiles: state.cacheFiles }), + onRehydrateStorage: _preState => { + return async (postState, _error) => { + await postState?.setActiveServer(postState.settings.activeServer, true) + postState?.setHydrated(true) + } + }, + migrate: (persistedState, version) => { + if (version > DB_VERSION) { + throw new Error('cannot migrate db on a downgrade, delete all data first') + } - hydrated: false, - setHydrated: hydrated => set({ hydrated }), - }), - { - name: '@appStore', - version: DB_VERSION, - getStorage: () => storage, - whitelist: ['settings', 'cacheFiles'], - onRehydrateStorage: _preState => { - return async (postState, _error) => { - await postState?.setActiveServer(postState.settings.activeServer, true) - postState?.setHydrated(true) - } + for (let i = version; i < DB_VERSION; i++) { + persistedState = migrations[i](persistedState) + } + + return persistedState + }, }, - migrate: (persistedState, version) => { - if (version > DB_VERSION) { - throw new Error('cannot migrate db on a downgrade, delete all data first') - } - - for (let i = version; i < DB_VERSION; i++) { - persistedState = migrations[i](persistedState) - } - - return persistedState - }, - }, + ), ), ) + +export const useStoreDeep = (stateSelector: StateSelector) => useStore(stateSelector, equal) diff --git a/app/state/trackplayermap.ts b/app/state/trackplayermap.ts index 2062900..0ba5725 100644 --- a/app/state/trackplayermap.ts +++ b/app/state/trackplayermap.ts @@ -17,14 +17,12 @@ export const selectTrackPlayerMap = { export const createTrackPlayerMapSlice = (set: SetState, get: GetState): TrackPlayerMapSlice => ({ mapSongtoTrackExt: async song => { let artwork = require('@res/fallback.png') - // if (song.coverArt) { - // const filePath = await get().fetchCoverArtFilePath(song.coverArt) - // if (filePath) { - // artwork = filePath - // } - // } - - console.log('mapping', song.title) + if (song.coverArt) { + const filePath = await get().fetchCoverArtFilePath(song.coverArt) + if (filePath) { + artwork = filePath + } + } return { id: song.id, diff --git a/package.json b/package.json index fe719f6..15a7e19 100644 --- a/package.json +++ b/package.json @@ -29,6 +29,7 @@ "@react-navigation/native": "^5.9.4", "@types/react": "^17", "@xmldom/xmldom": "^0.7.0", + "fast-deep-equal": "^3.1.3", "immer": "^9.0.6", "lodash.debounce": "^4.0.8", "lodash.merge": "^4.6.2", @@ -50,7 +51,7 @@ "react-native-vector-icons": "^8.1.0", "react-native-webview": "^11.13.0", "uuid": "^8.3.2", - "zustand": "^3.5.7" + "zustand": "^3.7.1" }, "devDependencies": { "@babel/core": "^7.12.9", diff --git a/yarn.lock b/yarn.lock index 9575132..8460abf 100644 --- a/yarn.lock +++ b/yarn.lock @@ -7629,7 +7629,7 @@ yargs@^16.1.1: y18n "^5.0.5" yargs-parser "^20.2.2" -zustand@^3.5.7: - version "3.6.9" - resolved "https://registry.yarnpkg.com/zustand/-/zustand-3.6.9.tgz#f61a756ddea9f95c7ee7cfd3af2f88c10078afbc" - integrity sha512-OvDNu/jEWpRnEC7k8xh8GKjqYog7td6FZrLMuHs/IeI8WhrCwV+FngVuwMIFhp5kysZXr6emaeReMqjLGaldAQ== +zustand@^3.7.1: + version "3.7.1" + resolved "https://registry.yarnpkg.com/zustand/-/zustand-3.7.1.tgz#7388f0a7175a6c2fd9a2880b383a4bf6cdf6b7c6" + integrity sha512-wHBCZlKj+bg03/hP+Tzv24YhnqqP8MCeN9ECPDXoF01062SIbnfl3j9O0znkDw1lNTY0a8WN3F///a0UhhaEqg==