From 0416c0ad0d3b954e44c73e5c7fcaa8420b033d94 Mon Sep 17 00:00:00 2001 From: austinried <4966622+austinried@users.noreply.github.com> Date: Sat, 7 Aug 2021 18:30:21 +0900 Subject: [PATCH] first-pass context menu for albums --- app/App.tsx | 19 +-- app/components/ContextMenu.tsx | 212 ++++++++++++++++++++++++++++ app/components/ListItem.tsx | 36 ++++- app/components/PressableOpacity.tsx | 6 + app/models/music.ts | 2 + app/screens/ArtistView.tsx | 29 +++- app/screens/Home.tsx | 21 +-- app/screens/LibraryAlbums.tsx | 45 +++--- package.json | 1 + yarn.lock | 5 + 10 files changed, 322 insertions(+), 54 deletions(-) create mode 100644 app/components/ContextMenu.tsx diff --git a/app/App.tsx b/app/App.tsx index 45c3de2..68f54cb 100644 --- a/app/App.tsx +++ b/app/App.tsx @@ -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 = () => ( - - - - - - - - + + + + + + + + + + ) export default App diff --git a/app/components/ContextMenu.tsx b/app/components/ContextMenu.tsx new file mode 100644 index 0000000..604071c --- /dev/null +++ b/app/components/ContextMenu.tsx @@ -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 + triggerWrapperStyle?: StyleProp + triggerOuterWrapperStyle?: StyleProp + triggerTouchableStyle?: StyleProp + onPress?: () => void +} + +type InternalContextMenuProps = ContextMenuProps & { + menuHeader: React.ReactNode + menuOptions: React.ReactNode +} + +const ContextMenu: React.FC = ({ + 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 ( + + + {children} + + ( + + {menuHeader} + {options} + + )}> + {menuOptions} + + + ) +} + +type ContextMenuOptionProps = { + onSelect?: () => void +} + +const ContextMenuOption: React.FC = ({ onSelect, children }) => ( + + {children} + +) + +type ContextMenuIconTextOptionProps = ContextMenuOptionProps & { + IconComponent: ReactComponentLike + name: string + size: number + color?: string + text: string +} + +const ContextMenuIconTextOption = React.memo( + ({ onSelect, IconComponent, name, color, size, text }) => ( + + + + + {text} + + ), +) + +export type AlbumContextPressableProps = ContextMenuProps & { + album: AlbumListItem +} + +export const AlbumContextPressable: React.FC = ({ + menuStyle, + triggerWrapperStyle, + triggerOuterWrapperStyle, + triggerTouchableStyle, + onPress, + album, + children, +}) => { + const navigation = useNavigation() + + return ( + + + + + {album.name} + + {album.artist ? ( + + {album.artist} + + ) : ( + <> + )} + + + } + menuOptions={ + <> + + navigation.navigate('artist', { id: album.artistId, title: album.artist })} + /> + + + }> + {children} + + ) +} + +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', + }, +}) diff --git a/app/components/ListItem.tsx b/app/components/ListItem.tsx index ad71a43..a3ad1ba 100644 --- a/app/components/ListItem.tsx +++ b/app/components/ListItem.tsx @@ -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 }) => ( + + {children} + + ), + [onPress], + ) + const albumPressable = useCallback( + ({ children }) => ( + + {children} + + ), + [item, onPress], + ) + + let PressableComponent = itemPressable + if (item.itemType === 'album') { + PressableComponent = albumPressable + } + return ( - + {showArt ? ( )} - + {showStar ? ( 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, diff --git a/app/components/PressableOpacity.tsx b/app/components/PressableOpacity.tsx index 89e3190..cdd0045 100644 --- a/app/components/PressableOpacity.tsx +++ b/app/components/PressableOpacity.tsx @@ -43,6 +43,12 @@ const PressableOpacity: React.FC = props => { if (!props.disabled) { setOpacity(1) } + }} + onLongPress={data => { + if (!props.disabled) { + setOpacity(1) + props.onLongPress ? props.onLongPress(data) : null + } }}> {props.children} diff --git a/app/models/music.ts b/app/models/music.ts index 9b918af..b69c89e 100644 --- a/app/models/music.ts +++ b/app/models/music.ts @@ -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, } diff --git a/app/screens/ArtistView.tsx b/app/screens/ArtistView.tsx index 5240e00..ecd0fa7 100644 --- a/app/screens/ArtistView.tsx +++ b/app/screens/ArtistView.tsx @@ -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 ( - navigation.navigate('album', { id: album.id, title: album.name })} - style={[styles.albumItem, { width }]}> + menuStyle={[styles.albumItem, { width }]} + triggerOuterWrapperStyle={{ width }}> {album.name} {album.year ? album.year : ''} - + ) }) @@ -54,6 +61,12 @@ const TopSongs = React.memo<{ ) }) +const ArtistDetailsFallback = React.memo(() => ( + + + +)) + 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 } const _albums = [...artist.albums] @@ -117,6 +130,10 @@ const styles = StyleSheet.create({ scroll: { flex: 1, }, + fallback: { + alignItems: 'center', + paddingTop: 100, + }, scrollContent: { alignItems: 'center', }, diff --git a/app/screens/Home.tsx b/app/screens/Home.tsx index a6c1622..c4f8889 100644 --- a/app/screens/Home.tsx +++ b/app/screens/Home.tsx @@ -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 ( - navigation.navigate('album', { id: album.id, title: album.name })} - key={album.id} - style={styles.item}> - + navigation.navigate('album', { id: album.id, title: album.name })}> + {album.name} {album.artist} - + ) }) @@ -138,9 +143,9 @@ const styles = StyleSheet.create({ paddingLeft: 20, }, item: { + flex: 1, marginRight: 10, width: 150, - alignItems: 'flex-start', }, title: { fontFamily: font.semiBold, diff --git a/app/screens/LibraryAlbums.tsx b/app/screens/LibraryAlbums.tsx index 46fbdf4..6ee39c0 100644 --- a/app/screens/LibraryAlbums.tsx +++ b/app/screens/LibraryAlbums.tsx @@ -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 ( - navigation.navigate('album', { id, title: name })}> - + navigation.navigate('album', { id: album.id, title: album.name })}> + - {name} + {album.name} - {artist} + {album.artist} - + ) }) const AlbumListRenderItem: React.FC<{ item: { album: Album; size: number; height: number } -}> = ({ item }) => ( - -) +}> = ({ item }) => 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, diff --git a/package.json b/package.json index cabda6d..02d01c6 100644 --- a/package.json +++ b/package.json @@ -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", diff --git a/yarn.lock b/yarn.lock index ada50b2..2ee33cc 100644 --- a/yarn.lock +++ b/yarn.lock @@ -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"