impl starring everywhere

This commit is contained in:
austinried 2021-08-10 09:42:20 +09:00
parent 0a3d542156
commit 075286e939
6 changed files with 121 additions and 62 deletions

View File

@ -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<ViewStyle>
triggerOuterWrapperStyle?: StyleProp<ViewStyle>
triggerTouchableStyle?: StyleProp<ViewStyle>
onPress?: () => void
onPress?: () => any
}
type InternalContextMenuProps = ContextMenuProps & {
@ -70,7 +74,7 @@ const ContextMenu: React.FC<InternalContextMenuProps> = ({
}
type ContextMenuOptionProps = {
onSelect?: () => void
onSelect?: () => any
}
const ContextMenuOption: React.FC<ContextMenuOptionProps> = ({ onSelect, children }) => (
@ -80,22 +84,31 @@ const ContextMenuOption: React.FC<ContextMenuOptionProps> = ({ 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<ContextMenuIconTextOptionProps>(
({ onSelect, IconComponent, name, color, size, text }) => (
<ContextMenuOption onSelect={onSelect}>
<View style={styles.icon}>
<IconComponent name={name} size={size} color={color || colors.text.primary} />
</View>
<Text style={styles.optionText}>{text}</Text>
</ContextMenuOption>
),
({ onSelect, IconComponent, IconComponentRaw, name, color, size, text }) => {
let Icon: React.ReactNode
if (IconComponentRaw) {
Icon = IconComponentRaw
} else if (IconComponent) {
Icon = <IconComponent name={name} size={size} color={color || colors.text.primary} />
} else {
Icon = <></>
}
return (
<ContextMenuOption onSelect={onSelect}>
<View style={styles.icon}>{Icon}</View>
<Text style={styles.optionText}>{text}</Text>
</ContextMenuOption>
)
},
)
const MenuHeader = React.memo<{
@ -121,9 +134,21 @@ const MenuHeader = React.memo<{
</View>
))
const OptionStar = React.memo(() => (
<ContextMenuIconTextOption IconComponent={IconFA} name="star-o" size={26} text="Star" />
))
const OptionStar = React.memo<{
id: string
type: string
}>(({ id, type }) => {
const starred = useStarred(id, type)
const setStarred = useStore(selectMusic.starItem)
return (
<ContextMenuIconTextOption
IconComponentRaw={<Star starred={starred} size={26} />}
text={starred ? 'Unstar' : 'Star'}
onSelect={() => setStarred(id, type, starred)}
/>
)
})
const OptionViewArtist = React.memo<{
navigation: NavigationProp<any>
@ -185,7 +210,7 @@ export const AlbumContextPressable: React.FC<AlbumContextPressableProps> = props
menuHeader={<MenuHeader title={album.name} subtitle={album.artist} coverArt={album.coverArt} />}
menuOptions={
<>
<OptionStar />
<OptionStar id={album.id} type={album.itemType} />
<OptionViewArtist artist={album.artist} artistId={album.artistId} navigation={navigation} />
<OptionDownload itemType={album.itemType} />
</>
@ -209,7 +234,7 @@ export const SongContextPressable: React.FC<SongContextPressableProps> = props =
menuHeader={<MenuHeader title={song.title} subtitle={song.artist} coverArt={song.coverArt} />}
menuOptions={
<>
<OptionStar />
<OptionStar id={song.id} type={song.itemType} />
<OptionViewArtist artist={song.artist} artistId={song.artistId} navigation={navigation} />
<OptionViewAlbum album={song.album} albumId={song.albumId} navigation={navigation} />
<OptionDownload itemType={song.itemType} />
@ -233,7 +258,7 @@ export const ArtistContextPressable: React.FC<ArtistContextPressableProps> = pro
menuHeader={<MenuHeader title={artist.name} artistId={artist.id} />}
menuOptions={
<>
<OptionStar />
<OptionStar id={artist.id} type={artist.itemType} />
<OptionDownload itemType={artist.itemType} />
</>
}>

View File

@ -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<{
<View style={styles.controls}>
{showStar ? (
<PressableOpacity onPress={toggleStarred} style={styles.controlItem}>
{starred ? (
<IconFA name="star" size={26} color={colors.accent} />
) : (
<IconFA name="star-o" size={26} color={colors.text.secondary} />
)}
<Star size={26} starred={starred} />
</PressableOpacity>
) : (
<></>

14
app/components/Star.tsx Normal file
View File

@ -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 (
<IconFA name={starred ? 'star' : 'star-o'} color={starred ? colors.accent : colors.text.secondary} size={size} />
)
})
export default Star

View File

@ -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 = () => {

View File

@ -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 (
<View style={infoStyles.container}>
@ -97,8 +104,8 @@ const SongInfo = () => {
</Text>
</View>
<View style={infoStyles.controls}>
<PressableOpacity onPress={undefined}>
<IconFA name="star-o" size={32} color={colors.text.secondary} />
<PressableOpacity onPress={() => setStarred(id, type, starred)}>
<Star size={32} starred={starred} />
</PressableOpacity>
</View>
</View>

View File

@ -58,7 +58,9 @@ export type MusicSlice = {
fetchHomeLists: () => Promise<void>
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<void>
}
@ -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<Store>, get: GetState<Store>): Mu
set(
produce<MusicSlice>(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<Store>, get: GetState<Store>): Mu
set(
produce<MusicSlice>(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<Store>, get: GetState<Store>): Mu
set(
produce<MusicSlice>(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<Store>, get: GetState<Store>): Mu
set(
produce<MusicSlice>(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<Store>, get: GetState<Store>): Mu
set(
produce<MusicSlice>(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<Store>, get: GetState<Store>): 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<Store>, get: GetState<Store>): Mu
set(
produce<MusicSlice>(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<Store>, get: GetState<Store>): 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<Store>, get: GetState<Store>): Mu
}
let params: StarParams
let setStarred: (starred: boolean) => void
switch (type) {
case 'song':
params = { id }
setStarred = starred => {
set(
produce<MusicSlice>(state => {
state.starredSongs = reduceStarred(state.starredSongs, [{ id, starred }])
}),
)
}
break
case 'album':
params = { albumId: id }
setStarred = starred => {
set(
produce<MusicSlice>(state => {
state.starredAlbums = reduceStarred(state.starredAlbums, [{ id, starred }])
}),
)
}
break
case 'artist':
params = { artistId: id }
setStarred = starred => {
set(
produce<MusicSlice>(state => {
state.starredArtists = reduceStarred(state.starredArtists, [{ id, starred }])
}),
)
}
break
default:
return
}
const setStarred = (starred: boolean) => {
set(
produce<MusicSlice>(state => {
state.starred[type] = {
...state.starred[type],
[id]: starred,
}
}),
)
}
try {
setStarred(!unstar)
if (unstar) {