first-pass context menu for albums

This commit is contained in:
austinried
2021-08-07 18:30:21 +09:00
parent f3341355e1
commit 0416c0ad0d
10 changed files with 322 additions and 54 deletions

View File

@@ -0,0 +1,212 @@
import PressableOpacity from '@app/components/PressableOpacity'
import { AlbumListItem } from '@app/models/music'
import colors from '@app/styles/colors'
import font from '@app/styles/font'
import { useNavigation } from '@react-navigation/native'
import { ReactComponentLike } from 'prop-types'
import React from 'react'
import { ScrollView, StyleProp, StyleSheet, Text, View, ViewStyle } from 'react-native'
import FastImage from 'react-native-fast-image'
import { Menu, MenuOption, MenuOptions, MenuTrigger, renderers } from 'react-native-popup-menu'
import IconFA from 'react-native-vector-icons/FontAwesome'
import IconMat from 'react-native-vector-icons/MaterialIcons'
import CoverArt from './CoverArt'
const { SlideInMenu } = renderers
type ContextMenuProps = {
menuStyle?: StyleProp<ViewStyle>
triggerWrapperStyle?: StyleProp<ViewStyle>
triggerOuterWrapperStyle?: StyleProp<ViewStyle>
triggerTouchableStyle?: StyleProp<ViewStyle>
onPress?: () => void
}
type InternalContextMenuProps = ContextMenuProps & {
menuHeader: React.ReactNode
menuOptions: React.ReactNode
}
const ContextMenu: React.FC<InternalContextMenuProps> = ({
menuStyle,
triggerWrapperStyle,
triggerOuterWrapperStyle,
triggerTouchableStyle,
onPress,
menuHeader,
menuOptions,
children,
}) => {
menuStyle = menuStyle || { flex: 1 }
triggerWrapperStyle = triggerWrapperStyle || { flex: 1 }
triggerOuterWrapperStyle = triggerOuterWrapperStyle || { flex: 1 }
triggerTouchableStyle = triggerTouchableStyle || { flex: 1 }
return (
<Menu renderer={SlideInMenu} style={menuStyle}>
<MenuTrigger
triggerOnLongPress={true}
customStyles={{
triggerOuterWrapper: triggerOuterWrapperStyle,
triggerWrapper: triggerWrapperStyle,
triggerTouchable: { style: triggerTouchableStyle },
TriggerTouchableComponent: PressableOpacity,
}}
onAlternativeAction={onPress}>
{children}
</MenuTrigger>
<MenuOptions
customStyles={styles}
renderOptionsContainer={(options: any) => (
<ScrollView>
{menuHeader}
{options}
</ScrollView>
)}>
{menuOptions}
</MenuOptions>
</Menu>
)
}
type ContextMenuOptionProps = {
onSelect?: () => void
}
const ContextMenuOption: React.FC<ContextMenuOptionProps> = ({ onSelect, children }) => (
<MenuOption style={styles.option} onSelect={onSelect}>
{children}
</MenuOption>
)
type ContextMenuIconTextOptionProps = ContextMenuOptionProps & {
IconComponent: ReactComponentLike
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>
),
)
export type AlbumContextPressableProps = ContextMenuProps & {
album: AlbumListItem
}
export const AlbumContextPressable: React.FC<AlbumContextPressableProps> = ({
menuStyle,
triggerWrapperStyle,
triggerOuterWrapperStyle,
triggerTouchableStyle,
onPress,
album,
children,
}) => {
const navigation = useNavigation()
return (
<ContextMenu
menuStyle={menuStyle}
triggerWrapperStyle={triggerWrapperStyle}
triggerOuterWrapperStyle={triggerOuterWrapperStyle}
triggerTouchableStyle={triggerTouchableStyle}
onPress={onPress}
menuHeader={
<View style={styles.menuHeader}>
<CoverArt coverArt={album.coverArt} style={styles.coverArt} resizeMode={FastImage.resizeMode.cover} />
<View style={styles.menuHeaderText}>
<Text numberOfLines={1} style={styles.menuTitle}>
{album.name}
</Text>
{album.artist ? (
<Text numberOfLines={1} style={styles.menuSubtitle}>
{album.artist}
</Text>
) : (
<></>
)}
</View>
</View>
}
menuOptions={
<>
<ContextMenuIconTextOption IconComponent={IconFA} name="star-o" size={26} text="Star" />
<ContextMenuIconTextOption
IconComponent={IconFA}
name="microphone"
size={26}
text="View artist"
onSelect={() => navigation.navigate('artist', { id: album.artistId, title: album.artist })}
/>
<ContextMenuIconTextOption IconComponent={IconMat} name="file-download" size={26} text="Download album" />
</>
}>
{children}
</ContextMenu>
)
}
const styles = StyleSheet.create({
optionsContainer: {
backgroundColor: 'rgba(45, 45, 45, 0.95)',
maxHeight: 365,
},
optionsWrapper: {
// marginBottom: 10,
},
menuHeader: {
paddingTop: 14,
paddingBottom: 10,
paddingHorizontal: 20,
flex: 1,
flexDirection: 'row',
justifyContent: 'center',
alignItems: 'center',
},
coverArt: {
width: 42,
height: 42,
},
menuHeaderText: {
flex: 1,
marginLeft: 10,
},
menuTitle: {
fontFamily: font.semiBold,
fontSize: 16,
color: colors.text.primary,
},
menuSubtitle: {
fontFamily: font.regular,
fontSize: 14,
color: colors.text.secondary,
},
option: {
paddingVertical: 10,
paddingHorizontal: 20,
flexDirection: 'row',
alignItems: 'center',
},
icon: {
marginRight: 10,
width: 32,
height: 32,
justifyContent: 'center',
alignItems: 'center',
// backgroundColor: 'red',
},
optionText: {
fontFamily: font.semiBold,
fontSize: 16,
color: colors.text.primary,
// backgroundColor: 'green',
},
})

View File

@@ -1,15 +1,16 @@
import { ListableItem } from '@app/models/music'
import { AlbumListItem, ListableItem } from '@app/models/music'
import { useStore } from '@app/state/store'
import { selectTrackPlayer } from '@app/state/trackplayer'
import colors from '@app/styles/colors'
import font from '@app/styles/font'
import { useNavigation } from '@react-navigation/native'
import React, { useState } from 'react'
import { GestureResponderEvent, StyleSheet, Text, View } from 'react-native'
import React, { useCallback, useState } 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 } from './ContextMenu'
import CoverArt from './CoverArt'
import PressableOpacity from './PressableOpacity'
@@ -40,7 +41,7 @@ const TitleText = React.memo<{
const ListItem: React.FC<{
item: ListableItem
onPress?: (event: GestureResponderEvent) => void
onPress?: () => void
showArt?: boolean
showStar?: boolean
listStyle?: 'big' | 'small'
@@ -81,9 +82,31 @@ const ListItem: React.FC<{
}
}
const itemPressable = useCallback(
({ children }) => (
<PressableOpacity onPress={onPress} style={styles.item}>
{children}
</PressableOpacity>
),
[onPress],
)
const albumPressable = useCallback(
({ children }) => (
<AlbumContextPressable album={item as AlbumListItem} onPress={onPress} triggerWrapperStyle={styles.item}>
{children}
</AlbumContextPressable>
),
[item, onPress],
)
let PressableComponent = itemPressable
if (item.itemType === 'album') {
PressableComponent = albumPressable
}
return (
<View style={[styles.container, sizeStyle.container]}>
<PressableOpacity onPress={onPress} style={styles.item}>
<PressableComponent>
{showArt ? (
<CoverArt
{...artSource}
@@ -117,7 +140,7 @@ const ListItem: React.FC<{
<></>
)}
</View>
</PressableOpacity>
</PressableComponent>
<View style={styles.controls}>
{showStar ? (
<PressableOpacity onPress={() => setStarred(!starred)} style={styles.controlItem}>
@@ -146,6 +169,7 @@ const styles = StyleSheet.create({
flex: 1,
flexDirection: 'row',
justifyContent: 'flex-start',
alignItems: 'center',
},
art: {
marginRight: 10,

View File

@@ -43,6 +43,12 @@ const PressableOpacity: React.FC<PressableOpacityProps> = props => {
if (!props.disabled) {
setOpacity(1)
}
}}
onLongPress={data => {
if (!props.disabled) {
setOpacity(1)
props.onLongPress ? props.onLongPress(data) : null
}
}}>
{props.children}
</Pressable>