diff --git a/app/components/ContextMenu.tsx b/app/components/ContextMenu.tsx index 633d6be..913780b 100644 --- a/app/components/ContextMenu.tsx +++ b/app/components/ContextMenu.tsx @@ -1,8 +1,6 @@ import PressableOpacity from '@app/components/PressableOpacity' -import { useStarred } from '@app/hooks/music' +import { useStar } from '@app/hooks/music' import { AlbumListItem, Artist, Song, StarrableItemType } from '@app/models/music' -import { selectMusic } from '@app/state/music' -import { useStore } from '@app/state/store' import colors from '@app/styles/colors' import font from '@app/styles/font' import { NavigationProp, useNavigation } from '@react-navigation/native' @@ -12,9 +10,8 @@ import { ScrollView, StyleProp, StyleSheet, Text, View, ViewStyle } from 'react- import { Menu, MenuOption, MenuOptions, MenuTrigger, renderers } from 'react-native-popup-menu' import IconFA from 'react-native-vector-icons/FontAwesome' import IconFA5 from 'react-native-vector-icons/FontAwesome5' -// import IconMat from 'react-native-vector-icons/MaterialIcons' import CoverArt from './CoverArt' -import Star from './Star' +import { Star } from './Star' const { SlideInMenu } = renderers @@ -144,14 +141,13 @@ const OptionStar = React.memo<{ type: StarrableItemType additionalText?: string }>(({ id, type, additionalText: text }) => { - const starred = useStarred(id, type) - const setStarred = useStore(selectMusic.starItem) + const { starred, toggleStar } = useStar(id, type) return ( } text={(starred ? 'Unstar' : 'Star') + (text ? ` ${text}` : '')} - onSelect={() => setStarred(id, type, starred)} + onSelect={toggleStar} /> ) }) diff --git a/app/components/ListItem.tsx b/app/components/ListItem.tsx index 0e36e1a..329feef 100644 --- a/app/components/ListItem.tsx +++ b/app/components/ListItem.tsx @@ -1,8 +1,5 @@ -import { useStarred } from '@app/hooks/music' import { useIsPlaying } from '@app/hooks/trackplayer' import { AlbumListItem, Artist, ListableItem, Song } from '@app/models/music' -import { selectMusic } from '@app/state/music' -import { useStore } from '@app/state/store' import colors from '@app/styles/colors' import font from '@app/styles/font' import { useNavigation } from '@react-navigation/native' @@ -13,7 +10,7 @@ import IconMat from 'react-native-vector-icons/MaterialIcons' import { AlbumContextPressable, ArtistContextPressable, SongContextPressable } from './ContextMenu' import CoverArt from './CoverArt' import PressableOpacity from './PressableOpacity' -import Star from './Star' +import { PressableStar } from './Star' const TitleTextSong = React.memo<{ contextId?: string @@ -58,7 +55,6 @@ const ListItem: React.FC<{ style?: StyleProp }> = ({ item, contextId, queueId, onPress, showArt, showStar, subtitle, listStyle, style }) => { const navigation = useNavigation() - const starred = useStarred(item.id, item.itemType) showStar = showStar === undefined ? true : showStar listStyle = listStyle || 'small' @@ -133,13 +129,6 @@ const ListItem: React.FC<{ PressableComponent = artistPressable } - const starItem = useStore(selectMusic.starItem) - const toggleStarred = useCallback(() => { - if (item.itemType !== 'playlist') { - starItem(item.id, item.itemType, starred) - } - }, [item.id, item.itemType, starItem, starred]) - let title = <> if (item.itemType === 'song' && queueId !== undefined) { title = @@ -178,10 +167,8 @@ const ListItem: React.FC<{ - {showStar && ( - - - + {showStar && item.itemType !== 'playlist' && ( + )} diff --git a/app/components/Star.tsx b/app/components/Star.tsx index c58d5e9..75a70fd 100644 --- a/app/components/Star.tsx +++ b/app/components/Star.tsx @@ -1,8 +1,11 @@ +import { useStar } from '@app/hooks/music' import colors from '@app/styles/colors' import React from 'react' +import { PressableStateCallbackType, StyleProp, ViewStyle } from 'react-native' import IconFA from 'react-native-vector-icons/FontAwesome' +import PressableOpacity from './PressableOpacity' -const Star = React.memo<{ +export const Star = React.memo<{ starred: boolean size: number }>(({ starred, size }) => { @@ -11,4 +14,17 @@ const Star = React.memo<{ ) }) -export default Star +export const PressableStar = React.memo<{ + id: string + type: 'album' | 'artist' | 'song' + size: number + style?: StyleProp | ((state: PressableStateCallbackType) => StyleProp) | undefined +}>(({ id, type, size, style }) => { + const { starred, toggleStar } = useStar(id, type) + + return ( + + + + ) +}) diff --git a/app/hooks/music.ts b/app/hooks/music.ts index 4d3eae5..7f321e4 100644 --- a/app/hooks/music.ts +++ b/app/hooks/music.ts @@ -1,35 +1,63 @@ -import { Store, useStore, useStoreDeep } from '@app/state/store' +import { useStore } from '@app/state/store' +import { StarParams } from '@app/subsonic/params' import { useCallback, useEffect } from 'react' -export const useArtistInfo = (id: string) => { - const artistInfo = useStoreDeep(useCallback(store => store.entities.artistInfo[id], [id])) - const fetchArtistInfo = useStore(store => store.fetchLibraryArtistInfo) +type StarrableItem = 'album' | 'artist' | 'song' - useEffect(() => { - if (!artistInfo) { - fetchArtistInfo(id) - } - }, [artistInfo, fetchArtistInfo, id]) +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 artistInfo + return params } -export const useStarred = (id: string, type: string) => { - return useStore( +export const useStar = (id: string, type: StarrableItem) => { + const fetchAlbum = useStore(store => store.fetchLibraryAlbum) + const fetchArtist = useStore(store => store.fetchLibraryArtist) + const fetchSong = useStore(store => store.fetchLibrarySong) + + const _starred = 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 + store => { + if (type === 'album') { + return store.entities.albums[id] ? !!store.entities.albums[id].starred : null + } else if (type === 'artist') { + return store.entities.artists[id] ? !!store.entities.artists[id].starred : null + } else { + return store.entities.songs[id] ? !!store.entities.songs[id].starred : null } }, - [type, id], + [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 } } diff --git a/app/screens/NowPlayingView.tsx b/app/screens/NowPlayingView.tsx index 696a713..0fe6288 100644 --- a/app/screens/NowPlayingView.tsx +++ b/app/screens/NowPlayingView.tsx @@ -2,10 +2,8 @@ import CoverArt from '@app/components/CoverArt' import HeaderBar from '@app/components/HeaderBar' import ImageGradientBackground from '@app/components/ImageGradientBackground' import PressableOpacity from '@app/components/PressableOpacity' -import Star from '@app/components/Star' -import { useStarred } from '@app/hooks/music' +import { PressableStar } from '@app/components/Star' import { useNext, usePause, usePlay, usePrevious, useSeekTo } from '@app/hooks/trackplayer' -import { selectMusic } from '@app/state/music' import { useStore } from '@app/state/store' import { QueueContextType, selectTrackPlayer, TrackExt } from '@app/state/trackplayer' import { selectTrackPlayerMap } from '@app/state/trackplayermap' @@ -118,10 +116,6 @@ const coverArtStyles = StyleSheet.create({ const SongInfo = () => { const track = useStore(selectTrackPlayer.currentTrack) - const id = track?.id || '-1' - const type = 'song' - const starred = useStarred(id, type) - const setStarred = useStore(selectMusic.starItem) return ( @@ -134,9 +128,7 @@ const SongInfo = () => { - setStarred(id, type, starred)}> - - + ) diff --git a/app/state/library.ts b/app/state/library.ts index d7dfdfd..4021bee 100644 --- a/app/state/library.ts +++ b/app/state/library.ts @@ -15,6 +15,7 @@ import { GetArtistsResponse, GetPlaylistResponse, GetPlaylistsResponse, + GetSongResponse, GetTopSongsResponse, Search3Response, SubsonicResponse, @@ -184,6 +185,8 @@ export type LibrarySlice = { fetchLibraryPlaylists: () => Promise fetchLibraryPlaylist: (id: string) => Promise + fetchLibrarySong: (id: string) => Promise + fetchLibraryAlbumList: (params: GetAlbumList2Params) => Promise fetchLibrarySearchResults: (params: Search3Params) => Promise star: (params: StarParams) => Promise @@ -405,6 +408,28 @@ export const createLibrarySlice = (set: SetState, get: GetState): ) }, + fetchLibrarySong: async id => { + const client = get().client + if (!client) { + return + } + + let response: SubsonicResponse + try { + response = await client.getSong({ id }) + } catch { + return + } + + const song = mapSong(response.data.song) + + set( + produce(state => { + state.entities.songs[id] = song + }), + ) + }, + fetchLibraryAlbumList: async params => { const client = get().client if (!client) { diff --git a/app/state/music.ts b/app/state/music.ts index 55fbf4c..d2cc658 100644 --- a/app/state/music.ts +++ b/app/state/music.ts @@ -1,102 +1,15 @@ -import { StarrableItemType } from '@app/models/music' import { Store } from '@app/state/store' -import { StarParams } from '@app/subsonic/params' import produce from 'immer' import { GetState, SetState } from 'zustand' export type MusicSlice = { - // - // actions, etc. - // - starredSongs: { [id: string]: boolean } - starredAlbums: { [id: string]: boolean } - starredArtists: { [id: string]: boolean } - starItem: (id: string, type: StarrableItemType, unstar?: boolean) => Promise - albumIdCoverArt: { [id: string]: string | undefined } albumIdCoverArtRequests: { [id: string]: Promise } fetchAlbumCoverArt: (id: string) => Promise getAlbumCoverArt: (id: string | undefined) => Promise } -export const selectMusic = { - starItem: (store: MusicSlice) => store.starItem, -} - -function reduceStarred( - starredType: { [id: string]: boolean }, - items: { id: string; starred?: Date | boolean }[], -): { [id: string]: boolean } { - return { - ...starredType, - ...items.reduce((acc, val) => { - acc[val.id] = !!val.starred - return acc - }, {} as { [id: string]: boolean }), - } -} - export const createMusicSlice = (set: SetState, get: GetState): MusicSlice => ({ - starredSongs: {}, - starredAlbums: {}, - starredArtists: {}, - - starItem: async (id, type, unstar = false) => { - const client = get().client - if (!client) { - return - } - - let params: StarParams - let setStarred: (starred: boolean) => void - - switch (type) { - case 'song': - params = { id } - setStarred = starred => { - set( - produce(state => { - state.starredSongs = reduceStarred(state.starredSongs, [{ id, starred }]) - }), - ) - } - break - case 'album': - params = { albumId: id } - setStarred = starred => { - set( - produce(state => { - state.starredAlbums = reduceStarred(state.starredAlbums, [{ id, starred }]) - }), - ) - } - break - case 'artist': - params = { artistId: id } - setStarred = starred => { - set( - produce(state => { - state.starredArtists = reduceStarred(state.starredArtists, [{ id, starred }]) - }), - ) - } - break - default: - return - } - - try { - setStarred(!unstar) - if (unstar) { - await client.unstar(params) - } else { - await client.star(params) - } - } catch { - setStarred(unstar) - } - }, - albumIdCoverArt: {}, albumIdCoverArtRequests: {}, diff --git a/app/subsonic/api.ts b/app/subsonic/api.ts index 9921b5c..3552a72 100644 --- a/app/subsonic/api.ts +++ b/app/subsonic/api.ts @@ -11,6 +11,7 @@ import { GetMusicDirectoryParams, GetPlaylistParams, GetPlaylistsParams, + GetSongParams, GetTopSongsParams, ScrobbleParams, Search3Params, @@ -29,6 +30,7 @@ import { GetMusicDirectoryResponse, GetPlaylistResponse, GetPlaylistsResponse, + GetSongResponse, GetTopSongsResponse, Search3Response, SubsonicResponse, @@ -180,6 +182,11 @@ export class SubsonicApiClient { return new SubsonicResponse(xml, new GetTopSongsResponse(xml)) } + async getSong(params: GetSongParams): Promise> { + const xml = await this.apiGetXml('getSong', params) + return new SubsonicResponse(xml, new GetSongResponse(xml)) + } + // // Album/song lists // diff --git a/app/subsonic/params.ts b/app/subsonic/params.ts index ed2d773..116f4bd 100644 --- a/app/subsonic/params.ts +++ b/app/subsonic/params.ts @@ -27,6 +27,10 @@ export type GetArtistParams = { id: string } +export type GetSongParams = { + id: string +} + export type GetTopSongsParams = { artist: string count?: number diff --git a/app/subsonic/responses.ts b/app/subsonic/responses.ts index 0614ec7..41944ba 100644 --- a/app/subsonic/responses.ts +++ b/app/subsonic/responses.ts @@ -129,6 +129,14 @@ export class GetTopSongsResponse { } } +export class GetSongResponse { + song: ChildElement + + constructor(xml: Document) { + this.song = new ChildElement(xml.getElementsByTagName('song')[0]) + } +} + // // Album/song lists //