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

@ -6,6 +6,7 @@ import { StatusBar, View } from 'react-native'
import ProgressHook from './components/ProgressHook'
import { useStore } from './state/store'
import { selectTrackPlayer } from './state/trackplayer'
import { MenuProvider } from 'react-native-popup-menu'
const Debug = () => {
const currentTrack = useStore(selectTrackPlayer.currentTrack)
@ -14,14 +15,16 @@ const Debug = () => {
}
const App = () => (
<View style={{ flex: 1, backgroundColor: colors.gradient.high }}>
<StatusBar animated={true} backgroundColor={'rgba(0, 0, 0, 0.4)'} barStyle={'light-content'} translucent={true} />
<SplashPage>
<ProgressHook />
<Debug />
<RootNavigator />
</SplashPage>
</View>
<MenuProvider>
<View style={{ flex: 1, backgroundColor: colors.gradient.high }}>
<StatusBar animated={true} backgroundColor={'rgba(0, 0, 0, 0.4)'} barStyle={'light-content'} translucent={true} />
<SplashPage>
<ProgressHook />
<Debug />
<RootNavigator />
</SplashPage>
</View>
</MenuProvider>
)
export default App

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>

View File

@ -32,6 +32,7 @@ export interface AlbumListItem {
id: string
name: string
artist?: string
artistId?: string
starred?: Date
coverArt?: string
}
@ -148,6 +149,7 @@ export function mapAlbumID3toAlbumListItem(album: AlbumID3Element): AlbumListIte
id: album.id,
name: album.name,
artist: album.artist,
artistId: album.artistId,
starred: album.starred,
coverArt: album.coverArt,
}

View File

@ -1,8 +1,9 @@
import { AlbumContextPressable } from '@app/components/ContextMenu'
import CoverArt from '@app/components/CoverArt'
import GradientBackground from '@app/components/GradientBackground'
import GradientScrollView from '@app/components/GradientScrollView'
import Header from '@app/components/Header'
import ListItem from '@app/components/ListItem'
import PressableOpacity from '@app/components/PressableOpacity'
import { useArtistInfo } from '@app/hooks/music'
import { useSetQueue } from '@app/hooks/trackplayer'
import { Album, Song } from '@app/models/music'
@ -11,7 +12,7 @@ import font from '@app/styles/font'
import { useLayout } from '@react-native-community/hooks'
import { useNavigation } from '@react-navigation/native'
import React, { useEffect } from 'react'
import { StyleSheet, Text, View } from 'react-native'
import { ActivityIndicator, StyleSheet, Text, View } from 'react-native'
import FastImage from 'react-native-fast-image'
const AlbumItem = React.memo<{
@ -21,14 +22,20 @@ const AlbumItem = React.memo<{
}>(({ album, height, width }) => {
const navigation = useNavigation()
if (height <= 0 || width <= 0) {
return <></>
}
return (
<PressableOpacity
<AlbumContextPressable
album={album}
onPress={() => navigation.navigate('album', { id: album.id, title: album.name })}
style={[styles.albumItem, { width }]}>
menuStyle={[styles.albumItem, { width }]}
triggerOuterWrapperStyle={{ width }}>
<CoverArt coverArt={album.coverArt} style={{ height, width }} resizeMode={FastImage.resizeMode.cover} />
<Text style={styles.albumTitle}>{album.name}</Text>
<Text style={styles.albumYear}> {album.year ? album.year : ''}</Text>
</PressableOpacity>
</AlbumContextPressable>
)
})
@ -54,6 +61,12 @@ const TopSongs = React.memo<{
)
})
const ArtistDetailsFallback = React.memo(() => (
<GradientBackground style={styles.fallback}>
<ActivityIndicator size="large" color={colors.accent} />
</GradientBackground>
))
const ArtistDetails: React.FC<{ id: string }> = ({ id }) => {
const artist = useArtistInfo(id)
const albumsLayout = useLayout()
@ -62,7 +75,7 @@ const ArtistDetails: React.FC<{ id: string }> = ({ id }) => {
const albumSize = albumsLayout.width / 2 - styles.container.paddingHorizontal / 2
if (!artist) {
return <></>
return <ArtistDetailsFallback />
}
const _albums = [...artist.albums]
@ -117,6 +130,10 @@ const styles = StyleSheet.create({
scroll: {
flex: 1,
},
fallback: {
alignItems: 'center',
paddingTop: 100,
},
scrollContent: {
alignItems: 'center',
},

View File

@ -1,8 +1,8 @@
import { AlbumContextPressable } from '@app/components/ContextMenu'
import CoverArt from '@app/components/CoverArt'
import GradientScrollView from '@app/components/GradientScrollView'
import Header from '@app/components/Header'
import NothingHere from '@app/components/NothingHere'
import PressableOpacity from '@app/components/PressableOpacity'
import { useActiveListRefresh2 } from '@app/hooks/server'
import { AlbumListItem } from '@app/models/music'
import { selectMusic } from '@app/state/music'
@ -14,6 +14,7 @@ import { GetAlbumListType } from '@app/subsonic/params'
import { useNavigation } from '@react-navigation/native'
import React, { useCallback } from 'react'
import { RefreshControl, ScrollView, StatusBar, StyleSheet, Text, View } from 'react-native'
import FastImage from 'react-native-fast-image'
const titles: { [key in GetAlbumListType]?: string } = {
recent: 'Recent Albums',
@ -28,18 +29,22 @@ const AlbumItem = React.memo<{
const navigation = useNavigation()
return (
<PressableOpacity
onPress={() => navigation.navigate('album', { id: album.id, title: album.name })}
key={album.id}
style={styles.item}>
<CoverArt coverArt={album.coverArt} style={{ height: styles.item.width, width: styles.item.width }} />
<AlbumContextPressable
album={album}
triggerWrapperStyle={styles.item}
onPress={() => navigation.navigate('album', { id: album.id, title: album.name })}>
<CoverArt
coverArt={album.coverArt}
style={{ height: styles.item.width, width: styles.item.width }}
resizeMode={FastImage.resizeMode.cover}
/>
<Text style={styles.title} numberOfLines={1}>
{album.name}
</Text>
<Text style={styles.subtitle} numberOfLines={1}>
{album.artist}
</Text>
</PressableOpacity>
</AlbumContextPressable>
)
})
@ -138,9 +143,9 @@ const styles = StyleSheet.create({
paddingLeft: 20,
},
item: {
flex: 1,
marginRight: 10,
width: 150,
alignItems: 'flex-start',
},
title: {
fontFamily: font.semiBold,

View File

@ -1,8 +1,8 @@
import { AlbumContextPressable } from '@app/components/ContextMenu'
import CoverArt from '@app/components/CoverArt'
import GradientFlatList from '@app/components/GradientFlatList'
import PressableOpacity from '@app/components/PressableOpacity'
import { useActiveListRefresh2 } from '@app/hooks/server'
import { Album } from '@app/models/music'
import { Album, AlbumListItem } from '@app/models/music'
import { selectMusic } from '@app/state/music'
import { useStore } from '@app/state/store'
import colors from '@app/styles/colors'
@ -13,44 +13,37 @@ import { StyleSheet, Text, useWindowDimensions, View } from 'react-native'
import FastImage from 'react-native-fast-image'
const AlbumItem = React.memo<{
id: string
name: string
album: AlbumListItem
size: number
height: number
artist?: string
coverArt?: string
}>(({ id, name, artist, size, height, coverArt }) => {
}>(({ album, size, height }) => {
const navigation = useNavigation()
return (
<PressableOpacity
style={[styles.item, { maxWidth: size, height }]}
onPress={() => navigation.navigate('album', { id, title: name })}>
<CoverArt coverArt={coverArt} style={{ height: size, width: size }} resizeMode={FastImage.resizeMode.cover} />
<AlbumContextPressable
album={album}
triggerWrapperStyle={[styles.item, { maxWidth: size, height }]}
onPress={() => navigation.navigate('album', { id: album.id, title: album.name })}>
<CoverArt
coverArt={album.coverArt}
style={{ height: size, width: size }}
resizeMode={FastImage.resizeMode.cover}
/>
<View style={styles.itemDetails}>
<Text style={styles.title} numberOfLines={1}>
{name}
{album.name}
</Text>
<Text style={styles.subtitle} numberOfLines={1}>
{artist}
{album.artist}
</Text>
</View>
</PressableOpacity>
</AlbumContextPressable>
)
})
const AlbumListRenderItem: React.FC<{
item: { album: Album; size: number; height: number }
}> = ({ item }) => (
<AlbumItem
id={item.album.id}
coverArt={item.album.coverArt}
name={item.album.name}
artist={item.album.artist}
size={item.size}
height={item.height}
/>
)
}> = ({ item }) => <AlbumItem album={item.album} size={item.size} height={item.height} />
const AlbumsList = () => {
const list = useStore(selectMusic.albums)
@ -96,14 +89,14 @@ const styles = StyleSheet.create({
flex: 1,
},
item: {
alignItems: 'center',
// alignItems: 'center',
marginVertical: 4,
marginHorizontal: 3,
flex: 1 / 3,
},
itemDetails: {
flex: 1,
width: '100%',
// width: '100%',
},
title: {
fontSize: 12,

View File

@ -28,6 +28,7 @@
"react-native-get-random-values": "^1.7.0",
"react-native-image-colors": "^1.3.0",
"react-native-linear-gradient": "^2.5.6",
"react-native-popup-menu": "^0.15.11",
"react-native-reanimated": "^2.2.0",
"react-native-safe-area-context": "^3.2.0",
"react-native-screens": "^3.4.0",

View File

@ -5549,6 +5549,11 @@ react-native-linear-gradient@^2.5.6:
resolved "https://registry.yarnpkg.com/react-native-linear-gradient/-/react-native-linear-gradient-2.5.6.tgz#96215cbc5ec7a01247a20890888aa75b834d44a0"
integrity sha512-HDwEaXcQIuXXCV70O+bK1rizFong3wj+5Q/jSyifKFLg0VWF95xh8XQgfzXwtq0NggL9vNjPKXa016KuFu+VFg==
react-native-popup-menu@^0.15.11:
version "0.15.11"
resolved "https://registry.yarnpkg.com/react-native-popup-menu/-/react-native-popup-menu-0.15.11.tgz#df96b1a909ecbba84487821061ce6e29e7c7bb20"
integrity sha512-f5q2GoDN99bkA24wHiwasaErcdQEgyqYZ8IYuZPOrZNFr66E4rg6f4LElSVBA3EZJTSq5OddVeaOcU340bSTEg==
react-native-reanimated@^2.2.0:
version "2.2.0"
resolved "https://registry.yarnpkg.com/react-native-reanimated/-/react-native-reanimated-2.2.0.tgz#a6412c56b4e591d1f00fac949f62d0c72c357c78"