mirror of
https://github.com/austinried/subtracks.git
synced 2026-02-10 06:52:43 +01:00
first-pass context menu for albums
This commit is contained in:
212
app/components/ContextMenu.tsx
Normal file
212
app/components/ContextMenu.tsx
Normal 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',
|
||||
},
|
||||
})
|
||||
@@ -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,
|
||||
|
||||
@@ -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>
|
||||
|
||||
Reference in New Issue
Block a user