now playing header now shows context menu

fixed now playing image gradient
This commit is contained in:
austinried 2021-08-16 11:57:36 +09:00
parent ad25972774
commit b5392b6731
9 changed files with 77 additions and 40 deletions

View File

@ -1,6 +1,6 @@
import PressableOpacity from '@app/components/PressableOpacity' import PressableOpacity from '@app/components/PressableOpacity'
import { useStarred } from '@app/hooks/music' import { useStarred } from '@app/hooks/music'
import { AlbumListItem, Artist, Song } from '@app/models/music' import { AlbumListItem, Artist, Song, StarrableItemType } from '@app/models/music'
import { selectMusic } from '@app/state/music' import { selectMusic } from '@app/state/music'
import { useStore } from '@app/state/store' import { useStore } from '@app/state/store'
import colors from '@app/styles/colors' import colors from '@app/styles/colors'
@ -25,6 +25,7 @@ type ContextMenuProps = {
triggerOuterWrapperStyle?: StyleProp<ViewStyle> triggerOuterWrapperStyle?: StyleProp<ViewStyle>
triggerTouchableStyle?: StyleProp<ViewStyle> triggerTouchableStyle?: StyleProp<ViewStyle>
onPress?: () => any onPress?: () => any
triggerOnLongPress?: boolean
} }
type InternalContextMenuProps = ContextMenuProps & { type InternalContextMenuProps = ContextMenuProps & {
@ -41,6 +42,7 @@ const ContextMenu: React.FC<InternalContextMenuProps> = ({
menuHeader, menuHeader,
menuOptions, menuOptions,
children, children,
triggerOnLongPress,
}) => { }) => {
menuStyle = menuStyle || { flex: 1 } menuStyle = menuStyle || { flex: 1 }
triggerWrapperStyle = triggerWrapperStyle || { flex: 1 } triggerWrapperStyle = triggerWrapperStyle || { flex: 1 }
@ -49,7 +51,7 @@ const ContextMenu: React.FC<InternalContextMenuProps> = ({
return ( return (
<Menu renderer={SlideInMenu} style={menuStyle}> <Menu renderer={SlideInMenu} style={menuStyle}>
<MenuTrigger <MenuTrigger
triggerOnLongPress={true} triggerOnLongPress={triggerOnLongPress === undefined ? true : triggerOnLongPress}
customStyles={{ customStyles={{
triggerOuterWrapper: triggerOuterWrapperStyle, triggerOuterWrapper: triggerOuterWrapperStyle,
triggerWrapper: triggerWrapperStyle, triggerWrapper: triggerWrapperStyle,
@ -146,15 +148,16 @@ const MenuHeader = React.memo<{
const OptionStar = React.memo<{ const OptionStar = React.memo<{
id: string id: string
type: string type: StarrableItemType
}>(({ id, type }) => { additionalText?: string
}>(({ id, type, additionalText: text }) => {
const starred = useStarred(id, type) const starred = useStarred(id, type)
const setStarred = useStore(selectMusic.starItem) const setStarred = useStore(selectMusic.starItem)
return ( return (
<ContextMenuIconTextOption <ContextMenuIconTextOption
IconComponentRaw={<Star starred={starred} size={26} />} IconComponentRaw={<Star starred={starred} size={26} />}
text={starred ? 'Unstar' : 'Star'} text={(starred ? 'Unstar' : 'Star') + (text ? ` ${text}` : '')}
onSelect={() => setStarred(id, type, starred)} onSelect={() => setStarred(id, type, starred)}
/> />
) )
@ -277,6 +280,30 @@ export const ArtistContextPressable: React.FC<ArtistContextPressableProps> = pro
) )
} }
export type NowPlayingContextPressableProps = ContextMenuProps & {
song: Song
}
export const NowPlayingContextPressable: React.FC<NowPlayingContextPressableProps> = props => {
const navigation = useNavigation()
const { song, children } = props
return (
<ContextMenu
{...props}
menuHeader={<MenuHeader title={song.title} subtitle={song.artist} coverArt={song.coverArt} />}
menuOptions={
<>
<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} />
</>
}>
{children}
</ContextMenu>
)
}
const styles = StyleSheet.create({ const styles = StyleSheet.create({
optionsContainer: { optionsContainer: {
backgroundColor: 'rgba(45, 45, 45, 0.95)', backgroundColor: 'rgba(45, 45, 45, 0.95)',

View File

@ -8,13 +8,15 @@ import Animated from 'react-native-reanimated'
import PressableOpacity from './PressableOpacity' import PressableOpacity from './PressableOpacity'
import IconMat from 'react-native-vector-icons/MaterialIcons' import IconMat from 'react-native-vector-icons/MaterialIcons'
import { ReactComponentLike } from 'prop-types' import { ReactComponentLike } from 'prop-types'
import { Song } from '@app/models/music'
import { NowPlayingContextPressable } from './ContextMenu'
const HeaderBar = React.memo<{ const HeaderBar = React.memo<{
title?: string title?: string
headerStyle?: Animated.AnimatedStyleProp<ViewStyle> | Animated.AnimatedStyleProp<ViewStyle>[] headerStyle?: Animated.AnimatedStyleProp<ViewStyle> | Animated.AnimatedStyleProp<ViewStyle>[]
HeaderCenter?: ReactComponentLike HeaderCenter?: ReactComponentLike
onMore?: () => void contextItem?: Song
}>(({ title, headerStyle, HeaderCenter, onMore }) => { }>(({ title, headerStyle, HeaderCenter, contextItem }) => {
const navigation = useNavigation() const navigation = useNavigation()
const back = useCallback(() => { const back = useCallback(() => {
@ -23,9 +25,23 @@ const HeaderBar = React.memo<{
const _headerStyle = Array.isArray(headerStyle) ? headerStyle : [headerStyle] const _headerStyle = Array.isArray(headerStyle) ? headerStyle : [headerStyle]
const moreIcon = <IconMat name="more-vert" color="white" size={25} />
let more = <></>
if (contextItem) {
more = (
<NowPlayingContextPressable
menuStyle={styles.icons}
triggerWrapperStyle={styles.icons}
song={contextItem}
triggerOnLongPress={false}>
{moreIcon}
</NowPlayingContextPressable>
)
}
return ( return (
<Animated.View style={[styles.container, ..._headerStyle]}> <Animated.View style={[styles.container, ..._headerStyle]}>
<PressableOpacity onPress={back} style={styles.icons} ripple={true}> <PressableOpacity onPress={back} style={styles.icons}>
<IconMat name="arrow-back" color="white" size={25} /> <IconMat name="arrow-back" color="white" size={25} />
</PressableOpacity> </PressableOpacity>
<View style={styles.center}> <View style={styles.center}>
@ -37,13 +53,7 @@ const HeaderBar = React.memo<{
</Text> </Text>
)} )}
</View> </View>
{onMore ? ( {more}
<PressableOpacity style={styles.icons} ripple={true} onPress={onMore}>
<IconMat name="more-vert" color="white" size={25} />
</PressableOpacity>
) : (
<></>
)}
</Animated.View> </Animated.View>
) )
}) })
@ -62,6 +72,8 @@ const styles = StyleSheet.create({
height: 42, height: 42,
width: 42, width: 42,
marginHorizontal: 8, marginHorizontal: 8,
alignItems: 'center',
justifyContent: 'center',
}, },
center: { center: {
flex: 1, flex: 1,

View File

@ -18,7 +18,6 @@ const ImageGradientBackground: React.FC<{
useEffect(() => { useEffect(() => {
async function getColors() { async function getColors() {
console.log(`imagePath: ${imagePath}`)
if (imagePath === undefined) { if (imagePath === undefined) {
return return
} }
@ -27,7 +26,6 @@ const ImageGradientBackground: React.FC<{
let res: AndroidImageColors let res: AndroidImageColors
if (cachedResult) { if (cachedResult) {
console.log(`cachedResult: ${JSON.stringify(cachedResult)}`)
res = cachedResult as AndroidImageColors res = cachedResult as AndroidImageColors
} else { } else {
const path = `file://${imagePath}` const path = `file://${imagePath}`
@ -35,7 +33,6 @@ const ImageGradientBackground: React.FC<{
cache: true, cache: true,
key: imagePath, key: imagePath,
})) as AndroidImageColors })) as AndroidImageColors
console.log(`res: ${JSON.stringify(res)}`)
} }
if (res.muted && res.muted !== '#000000') { if (res.muted && res.muted !== '#000000') {

View File

@ -116,7 +116,6 @@ export const useArtistArtFile = (artistId: string) => {
useEffect(() => { useEffect(() => {
if (!file && artistInfo && artistInfo.largeImageUrl) { if (!file && artistInfo && artistInfo.largeImageUrl) {
console.log(artistInfo.largeImageUrl)
cacheItem('artistArt', artistId, artistInfo.largeImageUrl) cacheItem('artistArt', artistId, artistInfo.largeImageUrl)
} }
}, [artistId, artistInfo, artistInfo?.largeImageUrl, cacheItem, file]) }, [artistId, artistInfo, artistInfo?.largeImageUrl, cacheItem, file])

View File

@ -279,11 +279,11 @@ function mapSongToTrack(song: Song, coverArtPaths: { [coverArt: string]: string
album: song.album || 'Unknown Album', album: song.album || 'Unknown Album',
url: song.streamUri, url: song.streamUri,
artwork: artwork:
song.coverArt && coverArtPaths[song.coverArt] song.coverArt && coverArtPaths[song.coverArt] ? coverArtPaths[song.coverArt] : require('@res/fallback.png'),
? `file://${coverArtPaths[song.coverArt]}`
: require('@res/fallback.png'),
coverArt: song.coverArt, coverArt: song.coverArt,
duration: song.duration, duration: song.duration,
artistId: song.artistId,
albumId: song.albumId,
} }
} }
@ -297,5 +297,7 @@ export function mapTrackExtToSong(track: TrackExt): Song {
streamUri: track.url as string, streamUri: track.url as string,
coverArt: track.coverArt, coverArt: track.coverArt,
duration: track.duration, duration: track.duration,
artistId: track.artistId,
albumId: track.albumId,
} }
} }

View File

@ -80,6 +80,8 @@ export type ListableItem = Song | AlbumListItem | Artist | PlaylistListItem
export type HomeLists = { [key: string]: AlbumListItem[] } export type HomeLists = { [key: string]: AlbumListItem[] }
export type StarrableItemType = 'song' | 'album' | 'artist'
export enum CacheItemType { export enum CacheItemType {
coverArt, coverArt,
artistArt, artistArt,

View File

@ -5,6 +5,7 @@ import PressableOpacity from '@app/components/PressableOpacity'
import Star from '@app/components/Star' import Star from '@app/components/Star'
import { useStarred } from '@app/hooks/music' import { useStarred } from '@app/hooks/music'
import { import {
mapTrackExtToSong,
useNext, useNext,
usePause, usePause,
usePlay, usePlay,
@ -15,7 +16,7 @@ import {
} from '@app/hooks/trackplayer' } from '@app/hooks/trackplayer'
import { selectMusic } from '@app/state/music' import { selectMusic } from '@app/state/music'
import { useStore } from '@app/state/store' import { useStore } from '@app/state/store'
import { QueueContextType, selectTrackPlayer } from '@app/state/trackplayer' import { QueueContextType, selectTrackPlayer, TrackExt } from '@app/state/trackplayer'
import colors from '@app/styles/colors' import colors from '@app/styles/colors'
import font from '@app/styles/font' import font from '@app/styles/font'
import formatDuration from '@app/util/formatDuration' import formatDuration from '@app/util/formatDuration'
@ -45,34 +46,28 @@ function getContextName(type?: QueueContextType) {
} }
} }
const NowPlayingHeader = React.memo(() => { const NowPlayingHeader = React.memo<{
const navigation = useNavigation() track?: TrackExt
}>(({ track }) => {
const queueName = useStore(selectTrackPlayer.name) const queueName = useStore(selectTrackPlayer.name)
const queueContextType = useStore(selectTrackPlayer.queueContextType) const queueContextType = useStore(selectTrackPlayer.queueContextType)
const queueContextId = useStore(selectTrackPlayer.queueContextId)
if (!track) {
return <></>
}
let contextName = getContextName(queueContextType) let contextName = getContextName(queueContextType)
const goToContext = useCallback(() => {
if (!queueContextType || !queueContextId || queueContextType === 'song') {
return
}
navigation.navigate('library')
navigation.navigate(queueContextType, { id: queueContextId, title: queueName })
}, [navigation, queueContextId, queueContextType, queueName])
return ( return (
<HeaderBar <HeaderBar
headerStyle={{ backgroundColor: 'transparent' }} headerStyle={{ backgroundColor: 'transparent' }}
onMore={goToContext} contextItem={mapTrackExtToSong(track)}
HeaderCenter={() => ( HeaderCenter={() => (
<View style={headerStyles.center}> <View style={headerStyles.center}>
{contextName ? ( {contextName !== undefined && (
<Text numberOfLines={1} style={headerStyles.queueType}> <Text numberOfLines={1} style={headerStyles.queueType}>
{contextName} {contextName}
</Text> </Text>
) : (
<></>
)} )}
<Text numberOfLines={1} style={headerStyles.queueName}> <Text numberOfLines={1} style={headerStyles.queueName}>
{queueName || 'Nothing playing...'} {queueName || 'Nothing playing...'}
@ -395,7 +390,7 @@ const NowPlayingView: React.FC<NowPlayingProps> = ({ navigation }) => {
return ( return (
<View style={styles.container}> <View style={styles.container}>
<ImageGradientBackground imagePath={imagePath} /> <ImageGradientBackground imagePath={imagePath} />
<NowPlayingHeader /> <NowPlayingHeader track={track} />
<View style={styles.content}> <View style={styles.content}>
<SongCoverArt /> <SongCoverArt />
<SongInfo /> <SongInfo />

View File

@ -15,6 +15,7 @@ import {
PlaylistWithSongs, PlaylistWithSongs,
SearchResults, SearchResults,
Song, Song,
StarrableItemType,
} from '@app/models/music' } from '@app/models/music'
import { Store } from '@app/state/store' import { Store } from '@app/state/store'
import { GetAlbumList2Type, StarParams } from '@app/subsonic/params' import { GetAlbumList2Type, StarParams } from '@app/subsonic/params'
@ -65,7 +66,7 @@ export type MusicSlice = {
starredSongs: { [id: string]: boolean } starredSongs: { [id: string]: boolean }
starredAlbums: { [id: string]: boolean } starredAlbums: { [id: string]: boolean }
starredArtists: { [id: string]: boolean } starredArtists: { [id: string]: boolean }
starItem: (id: string, type: string, unstar?: boolean) => Promise<void> starItem: (id: string, type: StarrableItemType, unstar?: boolean) => Promise<void>
albumIdCoverArt: { [id: string]: string | undefined } albumIdCoverArt: { [id: string]: string | undefined }
albumIdCoverArtRequests: { [id: string]: Promise<void> } albumIdCoverArtRequests: { [id: string]: Promise<void> }

View File

@ -7,6 +7,8 @@ import { Store } from './store'
export type TrackExt = Track & { export type TrackExt = Track & {
id: string id: string
coverArt?: string coverArt?: string
artistId?: string
albumId?: string
} }
export type Progress = { export type Progress = {