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 (
+
+ )
+}
+
+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"