diff --git a/app/components/ContextMenu.tsx b/app/components/ContextMenu.tsx index fe4ba94..560380d 100644 --- a/app/components/ContextMenu.tsx +++ b/app/components/ContextMenu.tsx @@ -1,5 +1,8 @@ import PressableOpacity from '@app/components/PressableOpacity' +import { useStarred } from '@app/hooks/music' import { AlbumListItem, Artist, 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 { NavigationProp, useNavigation } from '@react-navigation/native' @@ -12,6 +15,7 @@ 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' const { SlideInMenu } = renderers @@ -20,7 +24,7 @@ type ContextMenuProps = { triggerWrapperStyle?: StyleProp triggerOuterWrapperStyle?: StyleProp triggerTouchableStyle?: StyleProp - onPress?: () => void + onPress?: () => any } type InternalContextMenuProps = ContextMenuProps & { @@ -70,7 +74,7 @@ const ContextMenu: React.FC = ({ } type ContextMenuOptionProps = { - onSelect?: () => void + onSelect?: () => any } const ContextMenuOption: React.FC = ({ onSelect, children }) => ( @@ -80,22 +84,31 @@ const ContextMenuOption: React.FC = ({ onSelect, childre ) type ContextMenuIconTextOptionProps = ContextMenuOptionProps & { - IconComponent: ReactComponentLike - name: string - size: number + IconComponent?: ReactComponentLike + IconComponentRaw?: React.ReactNode + name?: string + size?: number color?: string text: string } const ContextMenuIconTextOption = React.memo( - ({ onSelect, IconComponent, name, color, size, text }) => ( - - - - - {text} - - ), + ({ onSelect, IconComponent, IconComponentRaw, name, color, size, text }) => { + let Icon: React.ReactNode + if (IconComponentRaw) { + Icon = IconComponentRaw + } else if (IconComponent) { + Icon = + } else { + Icon = <> + } + return ( + + {Icon} + {text} + + ) + }, ) const MenuHeader = React.memo<{ @@ -121,9 +134,21 @@ const MenuHeader = React.memo<{ )) -const OptionStar = React.memo(() => ( - -)) +const OptionStar = React.memo<{ + id: string + type: string +}>(({ id, type }) => { + const starred = useStarred(id, type) + const setStarred = useStore(selectMusic.starItem) + + return ( + } + text={starred ? 'Unstar' : 'Star'} + onSelect={() => setStarred(id, type, starred)} + /> + ) +}) const OptionViewArtist = React.memo<{ navigation: NavigationProp @@ -185,7 +210,7 @@ export const AlbumContextPressable: React.FC = props menuHeader={} menuOptions={ <> - + @@ -209,7 +234,7 @@ export const SongContextPressable: React.FC = props = menuHeader={} menuOptions={ <> - + @@ -233,7 +258,7 @@ export const ArtistContextPressable: React.FC = pro menuHeader={} menuOptions={ <> - + }> diff --git a/app/components/ListItem.tsx b/app/components/ListItem.tsx index 49611bb..c95d4d2 100644 --- a/app/components/ListItem.tsx +++ b/app/components/ListItem.tsx @@ -9,12 +9,12 @@ import { useNavigation } from '@react-navigation/native' import React, { useCallback } from 'react' import { StyleSheet, Text, View } from 'react-native' import FastImage from 'react-native-fast-image' -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 { AlbumContextPressable, ArtistContextPressable, SongContextPressable } from './ContextMenu' import CoverArt from './CoverArt' import PressableOpacity from './PressableOpacity' +import Star from './Star' const TitleTextSong = React.memo<{ id: string @@ -171,11 +171,7 @@ const ListItem: React.FC<{ {showStar ? ( - {starred ? ( - - ) : ( - - )} + ) : ( <> diff --git a/app/components/Star.tsx b/app/components/Star.tsx new file mode 100644 index 0000000..c58d5e9 --- /dev/null +++ b/app/components/Star.tsx @@ -0,0 +1,14 @@ +import colors from '@app/styles/colors' +import React from 'react' +import IconFA from 'react-native-vector-icons/FontAwesome' + +const Star = React.memo<{ + starred: boolean + size: number +}>(({ starred, size }) => { + return ( + + ) +}) + +export default Star diff --git a/app/hooks/music.ts b/app/hooks/music.ts index 53d0818..63d2693 100644 --- a/app/hooks/music.ts +++ b/app/hooks/music.ts @@ -39,18 +39,23 @@ export const usePlaylistWithSongs = (id: string) => { } export const useStarred = (id: string, type: string) => { - const starred = useStore( + return useStore( useCallback( (state: Store) => { - if (!(type in state.starred)) { - return false + switch (type) { + case 'song': + return state.starredSongs[id] + case 'album': + return state.starredAlbums[id] + case 'artist': + return state.starredArtists[id] + default: + return false } - return !!state.starred[type][id] }, [type, id], ), ) - return starred } export const useCoverArtUri = () => { diff --git a/app/screens/NowPlayingView.tsx b/app/screens/NowPlayingView.tsx index be587c2..9a94a09 100644 --- a/app/screens/NowPlayingView.tsx +++ b/app/screens/NowPlayingView.tsx @@ -18,6 +18,9 @@ import { useFocusEffect } from '@react-navigation/native' import { useStore } from '@app/state/store' import { selectTrackPlayer } from '@app/state/trackplayer' import { useNext, usePause, usePlay, usePrevious, useToggleRepeat, useToggleShuffle } from '@app/hooks/trackplayer' +import Star from '@app/components/Star' +import { useStarred } from '@app/hooks/music' +import { selectMusic } from '@app/state/music' const NowPlayingHeader = React.memo<{ backHandler: () => void @@ -85,6 +88,10 @@ 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 ( @@ -97,8 +104,8 @@ const SongInfo = () => { - - + setStarred(id, type, starred)}> + diff --git a/app/state/music.ts b/app/state/music.ts index 34f461b..0e3f85c 100644 --- a/app/state/music.ts +++ b/app/state/music.ts @@ -58,7 +58,9 @@ export type MusicSlice = { fetchHomeLists: () => Promise clearHomeLists: () => void - starred: { [type: string]: { [id: string]: boolean } } + starredSongs: { [id: string]: boolean } + starredAlbums: { [id: string]: boolean } + starredArtists: { [id: string]: boolean } starItem: (id: string, type: string, unstar?: boolean) => Promise } @@ -94,7 +96,7 @@ export const selectMusic = { function reduceStarred( starredType: { [id: string]: boolean }, - items: { id: string; starred?: Date }[], + items: { id: string; starred?: Date | boolean }[], ): { [id: string]: boolean } { return { ...starredType, @@ -130,9 +132,9 @@ export const createMusicSlice = (set: SetState, get: GetState): Mu set( produce(state => { state.artistInfo[id] = artistInfo - state.starred.song = reduceStarred(state.starred.song, artistInfo.topSongs) - state.starred.artist = reduceStarred(state.starred.artist, [artistInfo]) - state.starred.album = reduceStarred(state.starred.album, artistInfo.albums) + state.starredSongs = reduceStarred(state.starredSongs, artistInfo.topSongs) + state.starredArtists = reduceStarred(state.starredArtists, [artistInfo]) + state.starredAlbums = reduceStarred(state.starredAlbums, artistInfo.albums) }), ) return artistInfo @@ -156,8 +158,8 @@ export const createMusicSlice = (set: SetState, get: GetState): Mu set( produce(state => { state.albumsWithSongs[id] = album - state.starred.song = reduceStarred(state.starred.song, album.songs) - state.starred.album = reduceStarred(state.starred.album, [album]) + state.starredSongs = reduceStarred(state.starredSongs, album.songs) + state.starredAlbums = reduceStarred(state.starredAlbums, [album]) }), ) return album @@ -181,7 +183,7 @@ export const createMusicSlice = (set: SetState, get: GetState): Mu set( produce(state => { state.playlistsWithSongs[id] = playlist - state.starred.song = reduceStarred(state.starred.song, playlist.songs) + state.starredSongs = reduceStarred(state.starredSongs, playlist.songs) }), ) return playlist @@ -209,7 +211,7 @@ export const createMusicSlice = (set: SetState, get: GetState): Mu set( produce(state => { state.artists = response.data.artists.map(mapArtistID3toArtist) - state.starred.artist = reduceStarred(state.starred.artist, state.artists) + state.starredArtists = reduceStarred(state.starredArtists, state.artists) }), ) } finally { @@ -258,7 +260,7 @@ export const createMusicSlice = (set: SetState, get: GetState): Mu set( produce(state => { state.albums = response.data.albums.map(mapAlbumID3toAlbumListItem) - state.starred.albums = reduceStarred(state.starred.albums, state.albums) + state.starredAlbums = reduceStarred(state.starredAlbums, state.albums) }), ) } finally { @@ -298,9 +300,9 @@ export const createMusicSlice = (set: SetState, get: GetState): Mu albums: response.data.albums.map(mapAlbumID3toAlbumListItem), songs: response.data.songs.map(a => mapChildToSong(a, client)), } - state.starred.song = reduceStarred(state.starred.song, state.searchResults.songs) - state.starred.artist = reduceStarred(state.starred.artist, state.searchResults.artists) - state.starred.album = reduceStarred(state.starred.album, state.searchResults.albums) + state.starredSongs = reduceStarred(state.starredSongs, state.searchResults.songs) + state.starredArtists = reduceStarred(state.starredArtists, state.searchResults.artists) + state.starredAlbums = reduceStarred(state.starredAlbums, state.searchResults.albums) }), ) } finally { @@ -341,7 +343,7 @@ export const createMusicSlice = (set: SetState, get: GetState): Mu set( produce(state => { state.homeLists[type] = response.data.albums.map(mapAlbumID3toAlbumListItem) - state.starred.album = reduceStarred(state.starred.album, state.homeLists[type]) + state.starredAlbums = reduceStarred(state.starredAlbums, state.homeLists[type]) }), ) }), @@ -357,11 +359,9 @@ export const createMusicSlice = (set: SetState, get: GetState): Mu set({ homeLists: {} }) }, - starred: { - song: {}, - album: {}, - artist: {}, - }, + starredSongs: {}, + starredAlbums: {}, + starredArtists: {}, starItem: async (id, type, unstar = false) => { const client = get().client @@ -370,31 +370,43 @@ export const createMusicSlice = (set: SetState, get: GetState): Mu } 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 } - const setStarred = (starred: boolean) => { - set( - produce(state => { - state.starred[type] = { - ...state.starred[type], - [id]: starred, - } - }), - ) - } - try { setStarred(!unstar) if (unstar) {