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 ProgressHook from './components/ProgressHook'
import { useStore } from './state/store' import { useStore } from './state/store'
import { selectTrackPlayer } from './state/trackplayer' import { selectTrackPlayer } from './state/trackplayer'
import { MenuProvider } from 'react-native-popup-menu'
const Debug = () => { const Debug = () => {
const currentTrack = useStore(selectTrackPlayer.currentTrack) const currentTrack = useStore(selectTrackPlayer.currentTrack)
@ -14,14 +15,16 @@ const Debug = () => {
} }
const App = () => ( const App = () => (
<View style={{ flex: 1, backgroundColor: colors.gradient.high }}> <MenuProvider>
<StatusBar animated={true} backgroundColor={'rgba(0, 0, 0, 0.4)'} barStyle={'light-content'} translucent={true} /> <View style={{ flex: 1, backgroundColor: colors.gradient.high }}>
<SplashPage> <StatusBar animated={true} backgroundColor={'rgba(0, 0, 0, 0.4)'} barStyle={'light-content'} translucent={true} />
<ProgressHook /> <SplashPage>
<Debug /> <ProgressHook />
<RootNavigator /> <Debug />
</SplashPage> <RootNavigator />
</View> </SplashPage>
</View>
</MenuProvider>
) )
export default App 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 { useStore } from '@app/state/store'
import { selectTrackPlayer } from '@app/state/trackplayer' import { selectTrackPlayer } from '@app/state/trackplayer'
import colors from '@app/styles/colors' import colors from '@app/styles/colors'
import font from '@app/styles/font' import font from '@app/styles/font'
import { useNavigation } from '@react-navigation/native' import { useNavigation } from '@react-navigation/native'
import React, { useState } from 'react' import React, { useCallback, useState } from 'react'
import { GestureResponderEvent, StyleSheet, Text, View } from 'react-native' import { StyleSheet, Text, View } from 'react-native'
import FastImage from 'react-native-fast-image' import FastImage from 'react-native-fast-image'
import IconFA from 'react-native-vector-icons/FontAwesome' import IconFA from 'react-native-vector-icons/FontAwesome'
import IconFA5 from 'react-native-vector-icons/FontAwesome5' import IconFA5 from 'react-native-vector-icons/FontAwesome5'
import IconMat from 'react-native-vector-icons/MaterialIcons' import IconMat from 'react-native-vector-icons/MaterialIcons'
import { AlbumContextPressable } from './ContextMenu'
import CoverArt from './CoverArt' import CoverArt from './CoverArt'
import PressableOpacity from './PressableOpacity' import PressableOpacity from './PressableOpacity'
@ -40,7 +41,7 @@ const TitleText = React.memo<{
const ListItem: React.FC<{ const ListItem: React.FC<{
item: ListableItem item: ListableItem
onPress?: (event: GestureResponderEvent) => void onPress?: () => void
showArt?: boolean showArt?: boolean
showStar?: boolean showStar?: boolean
listStyle?: 'big' | 'small' 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 ( return (
<View style={[styles.container, sizeStyle.container]}> <View style={[styles.container, sizeStyle.container]}>
<PressableOpacity onPress={onPress} style={styles.item}> <PressableComponent>
{showArt ? ( {showArt ? (
<CoverArt <CoverArt
{...artSource} {...artSource}
@ -117,7 +140,7 @@ const ListItem: React.FC<{
<></> <></>
)} )}
</View> </View>
</PressableOpacity> </PressableComponent>
<View style={styles.controls}> <View style={styles.controls}>
{showStar ? ( {showStar ? (
<PressableOpacity onPress={() => setStarred(!starred)} style={styles.controlItem}> <PressableOpacity onPress={() => setStarred(!starred)} style={styles.controlItem}>
@ -146,6 +169,7 @@ const styles = StyleSheet.create({
flex: 1, flex: 1,
flexDirection: 'row', flexDirection: 'row',
justifyContent: 'flex-start', justifyContent: 'flex-start',
alignItems: 'center',
}, },
art: { art: {
marginRight: 10, marginRight: 10,

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -28,6 +28,7 @@
"react-native-get-random-values": "^1.7.0", "react-native-get-random-values": "^1.7.0",
"react-native-image-colors": "^1.3.0", "react-native-image-colors": "^1.3.0",
"react-native-linear-gradient": "^2.5.6", "react-native-linear-gradient": "^2.5.6",
"react-native-popup-menu": "^0.15.11",
"react-native-reanimated": "^2.2.0", "react-native-reanimated": "^2.2.0",
"react-native-safe-area-context": "^3.2.0", "react-native-safe-area-context": "^3.2.0",
"react-native-screens": "^3.4.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" 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== 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: react-native-reanimated@^2.2.0:
version "2.2.0" version "2.2.0"
resolved "https://registry.yarnpkg.com/react-native-reanimated/-/react-native-reanimated-2.2.0.tgz#a6412c56b4e591d1f00fac949f62d0c72c357c78" resolved "https://registry.yarnpkg.com/react-native-reanimated/-/react-native-reanimated-2.2.0.tgz#a6412c56b4e591d1f00fac949f62d0c72c357c78"