mirror of
https://github.com/austinried/subtracks.git
synced 2025-12-28 17:19:27 +01:00
impl starring everywhere
This commit is contained in:
parent
0a3d542156
commit
075286e939
@ -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} />
|
||||
</>
|
||||
}>
|
||||
|
||||
@ -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
14
app/components/Star.tsx
Normal 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
|
||||
@ -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 = () => {
|
||||
|
||||
@ -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>
|
||||
|
||||
@ -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) {
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user