mirror of
https://github.com/austinried/subtracks.git
synced 2026-02-10 23:02:43 +01:00
reorg again, absolute (module) imports
This commit is contained in:
57
app/components/AlbumArt.tsx
Normal file
57
app/components/AlbumArt.tsx
Normal file
@@ -0,0 +1,57 @@
|
||||
import { useAtomValue } from 'jotai/utils'
|
||||
import React from 'react'
|
||||
import { ActivityIndicator, View } from 'react-native'
|
||||
import FastImage from 'react-native-fast-image'
|
||||
import LinearGradient from 'react-native-linear-gradient'
|
||||
import { albumArtAtomFamily } from '@app/state/music'
|
||||
import colors from '@app/styles/colors'
|
||||
import CoverArt from '@app/components/CoverArt'
|
||||
|
||||
interface AlbumArtProps {
|
||||
id: string
|
||||
height: number
|
||||
width: number
|
||||
}
|
||||
|
||||
const AlbumArt: React.FC<AlbumArtProps> = ({ id, height, width }) => {
|
||||
const albumArt = useAtomValue(albumArtAtomFamily(id))
|
||||
|
||||
const Placeholder = () => (
|
||||
<LinearGradient colors={[colors.accent, colors.accentLow]}>
|
||||
<FastImage
|
||||
source={require('../../res/record.png')}
|
||||
style={{ height, width }}
|
||||
resizeMode={FastImage.resizeMode.contain}
|
||||
/>
|
||||
</LinearGradient>
|
||||
)
|
||||
|
||||
return (
|
||||
<CoverArt
|
||||
PlaceholderComponent={Placeholder}
|
||||
height={height}
|
||||
width={width}
|
||||
coverArtUri={width > 128 ? albumArt?.uri : albumArt?.thumbUri}
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
||||
const AlbumArtFallback: React.FC<AlbumArtProps> = ({ height, width }) => (
|
||||
<View
|
||||
style={{
|
||||
height,
|
||||
width,
|
||||
alignItems: 'center',
|
||||
justifyContent: 'center',
|
||||
}}>
|
||||
<ActivityIndicator size="small" color={colors.accent} />
|
||||
</View>
|
||||
)
|
||||
|
||||
const AlbumArtLoader: React.FC<AlbumArtProps> = props => (
|
||||
<React.Suspense fallback={<AlbumArtFallback {...props} />}>
|
||||
<AlbumArt {...props} />
|
||||
</React.Suspense>
|
||||
)
|
||||
|
||||
export default React.memo(AlbumArtLoader)
|
||||
201
app/components/ArtistArt.tsx
Normal file
201
app/components/ArtistArt.tsx
Normal file
@@ -0,0 +1,201 @@
|
||||
import { useAtomValue } from 'jotai/utils'
|
||||
import React from 'react'
|
||||
import { ActivityIndicator, View } from 'react-native'
|
||||
import FastImage from 'react-native-fast-image'
|
||||
import LinearGradient from 'react-native-linear-gradient'
|
||||
import { artistArtAtomFamily } from '@app/state/music'
|
||||
import colors from '@app/styles/colors'
|
||||
import CoverArt from '@app/components/CoverArt'
|
||||
|
||||
interface ArtistArtSizeProps {
|
||||
height: number
|
||||
width: number
|
||||
}
|
||||
|
||||
interface ArtistArtXUpProps extends ArtistArtSizeProps {
|
||||
coverArtUris: string[]
|
||||
}
|
||||
|
||||
interface ArtistArtProps extends ArtistArtSizeProps {
|
||||
id: string
|
||||
}
|
||||
|
||||
const PlaceholderContainer: React.FC<ArtistArtSizeProps> = ({ height, width, children }) => (
|
||||
<LinearGradient
|
||||
colors={[colors.accent, colors.accentLow]}
|
||||
style={{
|
||||
height,
|
||||
width,
|
||||
alignItems: 'center',
|
||||
justifyContent: 'center',
|
||||
}}>
|
||||
{children}
|
||||
</LinearGradient>
|
||||
)
|
||||
|
||||
const FourUp: React.FC<ArtistArtXUpProps> = ({ height, width, coverArtUris }) => {
|
||||
const halfHeight = height / 2
|
||||
const halfWidth = width / 2
|
||||
|
||||
return (
|
||||
<PlaceholderContainer height={height} width={width}>
|
||||
<View style={{ width, height: halfHeight, flexDirection: 'row' }}>
|
||||
<FastImage
|
||||
source={{ uri: coverArtUris[0] }}
|
||||
style={{ height: halfHeight, width: halfWidth }}
|
||||
resizeMode={FastImage.resizeMode.cover}
|
||||
/>
|
||||
<FastImage
|
||||
source={{ uri: coverArtUris[1] }}
|
||||
style={{ height: halfHeight, width: halfWidth }}
|
||||
resizeMode={FastImage.resizeMode.cover}
|
||||
/>
|
||||
</View>
|
||||
<View style={{ width, height: halfHeight, flexDirection: 'row' }}>
|
||||
<FastImage
|
||||
source={{ uri: coverArtUris[2] }}
|
||||
style={{ height: halfHeight, width: halfWidth }}
|
||||
resizeMode={FastImage.resizeMode.cover}
|
||||
/>
|
||||
<FastImage
|
||||
source={{ uri: coverArtUris[3] }}
|
||||
style={{ height: halfHeight, width: halfWidth }}
|
||||
resizeMode={FastImage.resizeMode.cover}
|
||||
/>
|
||||
</View>
|
||||
</PlaceholderContainer>
|
||||
)
|
||||
}
|
||||
|
||||
const ThreeUp: React.FC<ArtistArtXUpProps> = ({ height, width, coverArtUris }) => {
|
||||
const halfHeight = height / 2
|
||||
const halfWidth = width / 2
|
||||
|
||||
return (
|
||||
<PlaceholderContainer height={height} width={width}>
|
||||
<View style={{ width, height: halfHeight, flexDirection: 'row' }}>
|
||||
<FastImage
|
||||
source={{ uri: coverArtUris[0] }}
|
||||
style={{ height: halfHeight, width }}
|
||||
resizeMode={FastImage.resizeMode.cover}
|
||||
/>
|
||||
</View>
|
||||
<View style={{ width, height: halfHeight, flexDirection: 'row' }}>
|
||||
<FastImage
|
||||
source={{ uri: coverArtUris[1] }}
|
||||
style={{ height: halfHeight, width: halfWidth }}
|
||||
resizeMode={FastImage.resizeMode.cover}
|
||||
/>
|
||||
<FastImage
|
||||
source={{ uri: coverArtUris[2] }}
|
||||
style={{ height: halfHeight, width: halfWidth }}
|
||||
resizeMode={FastImage.resizeMode.cover}
|
||||
/>
|
||||
</View>
|
||||
</PlaceholderContainer>
|
||||
)
|
||||
}
|
||||
|
||||
const TwoUp: React.FC<ArtistArtXUpProps> = ({ height, width, coverArtUris }) => {
|
||||
const halfHeight = height / 2
|
||||
|
||||
return (
|
||||
<PlaceholderContainer height={height} width={width}>
|
||||
<View style={{ width, height: halfHeight, flexDirection: 'row' }}>
|
||||
<FastImage
|
||||
source={{ uri: coverArtUris[0] }}
|
||||
style={{ height: halfHeight, width }}
|
||||
resizeMode={FastImage.resizeMode.cover}
|
||||
/>
|
||||
</View>
|
||||
<View style={{ width, height: halfHeight, flexDirection: 'row' }}>
|
||||
<FastImage
|
||||
source={{ uri: coverArtUris[1] }}
|
||||
style={{ height: halfHeight, width }}
|
||||
resizeMode={FastImage.resizeMode.cover}
|
||||
/>
|
||||
</View>
|
||||
</PlaceholderContainer>
|
||||
)
|
||||
}
|
||||
|
||||
const OneUp: React.FC<ArtistArtXUpProps> = ({ height, width, coverArtUris }) => {
|
||||
return (
|
||||
<PlaceholderContainer height={height} width={width}>
|
||||
<FastImage source={{ uri: coverArtUris[0] }} style={{ height, width }} resizeMode={FastImage.resizeMode.cover} />
|
||||
</PlaceholderContainer>
|
||||
)
|
||||
}
|
||||
|
||||
const NoneUp: React.FC<ArtistArtSizeProps> = ({ height, width }) => {
|
||||
return (
|
||||
<PlaceholderContainer height={height} width={width}>
|
||||
<FastImage
|
||||
source={require('../../res/mic_on-fill.png')}
|
||||
style={{
|
||||
height: height - height / 4,
|
||||
width: width - width / 4,
|
||||
}}
|
||||
resizeMode={FastImage.resizeMode.cover}
|
||||
/>
|
||||
</PlaceholderContainer>
|
||||
)
|
||||
}
|
||||
|
||||
const ArtistArt: React.FC<ArtistArtProps> = ({ id, height, width }) => {
|
||||
const artistArt = useAtomValue(artistArtAtomFamily(id))
|
||||
|
||||
const Placeholder = () => {
|
||||
const none = <NoneUp height={height} width={width} />
|
||||
|
||||
if (!artistArt || !artistArt.coverArtUris) {
|
||||
return none
|
||||
}
|
||||
const { coverArtUris } = artistArt
|
||||
|
||||
if (coverArtUris.length >= 4) {
|
||||
return <FourUp height={height} width={width} coverArtUris={coverArtUris} />
|
||||
}
|
||||
if (coverArtUris.length === 3) {
|
||||
return <ThreeUp height={height} width={width} coverArtUris={coverArtUris} />
|
||||
}
|
||||
if (coverArtUris.length === 2) {
|
||||
return <TwoUp height={height} width={width} coverArtUris={coverArtUris} />
|
||||
}
|
||||
if (coverArtUris.length === 1) {
|
||||
return <OneUp height={height} width={width} coverArtUris={coverArtUris} />
|
||||
}
|
||||
|
||||
return none
|
||||
}
|
||||
|
||||
return (
|
||||
<View
|
||||
style={{
|
||||
borderRadius: height / 2,
|
||||
overflow: 'hidden',
|
||||
}}>
|
||||
<CoverArt PlaceholderComponent={Placeholder} height={height} width={width} coverArtUri={artistArt?.uri} />
|
||||
</View>
|
||||
)
|
||||
}
|
||||
|
||||
const ArtistArtFallback: React.FC<ArtistArtProps> = ({ height, width }) => (
|
||||
<View
|
||||
style={{
|
||||
height,
|
||||
width,
|
||||
alignItems: 'center',
|
||||
justifyContent: 'center',
|
||||
}}>
|
||||
<ActivityIndicator size="small" color={colors.accent} />
|
||||
</View>
|
||||
)
|
||||
|
||||
const ArtistArtLoader: React.FC<ArtistArtProps> = props => (
|
||||
<React.Suspense fallback={<ArtistArtFallback {...props} />}>
|
||||
<ArtistArt {...props} />
|
||||
</React.Suspense>
|
||||
)
|
||||
|
||||
export default React.memo(ArtistArtLoader)
|
||||
31
app/components/Button.tsx
Normal file
31
app/components/Button.tsx
Normal file
@@ -0,0 +1,31 @@
|
||||
import React, { useState } from 'react'
|
||||
import { GestureResponderEvent, Pressable, Text } from 'react-native'
|
||||
import colors from '@app/styles/colors'
|
||||
import text from '@app/styles/text'
|
||||
|
||||
const Button: React.FC<{
|
||||
title: string
|
||||
onPress: (event: GestureResponderEvent) => void
|
||||
}> = ({ title, onPress }) => {
|
||||
const [opacity, setOpacity] = useState(1)
|
||||
|
||||
return (
|
||||
<Pressable
|
||||
onPress={onPress}
|
||||
onPressIn={() => setOpacity(0.6)}
|
||||
onPressOut={() => setOpacity(1)}
|
||||
onLongPress={() => setOpacity(1)}
|
||||
style={{
|
||||
backgroundColor: colors.accent,
|
||||
paddingHorizontal: 24,
|
||||
minHeight: 42,
|
||||
justifyContent: 'center',
|
||||
borderRadius: 1000,
|
||||
opacity,
|
||||
}}>
|
||||
<Text style={{ ...text.button }}>{title}</Text>
|
||||
</Pressable>
|
||||
)
|
||||
}
|
||||
|
||||
export default Button
|
||||
63
app/components/CoverArt.tsx
Normal file
63
app/components/CoverArt.tsx
Normal file
@@ -0,0 +1,63 @@
|
||||
import React, { useEffect, useState } from 'react'
|
||||
import { ActivityIndicator, StyleSheet, View } from 'react-native'
|
||||
import FastImage from 'react-native-fast-image'
|
||||
import colors from '@app/styles/colors'
|
||||
|
||||
const CoverArt: React.FC<{
|
||||
PlaceholderComponent: () => JSX.Element
|
||||
height?: string | number
|
||||
width?: string | number
|
||||
coverArtUri?: string
|
||||
}> = ({ PlaceholderComponent, height, width, coverArtUri }) => {
|
||||
const [placeholderVisible, setPlaceholderVisible] = useState(false)
|
||||
const [loading, setLoading] = useState(true)
|
||||
|
||||
useEffect(() => {
|
||||
if (!coverArtUri) {
|
||||
setLoading(false)
|
||||
}
|
||||
}, [coverArtUri, setLoading])
|
||||
|
||||
const Image = () => (
|
||||
<FastImage
|
||||
source={{ uri: coverArtUri, priority: 'high' }}
|
||||
style={{ ...styles.image, opacity: placeholderVisible ? 0 : 1 }}
|
||||
resizeMode={FastImage.resizeMode.contain}
|
||||
onError={() => {
|
||||
setLoading(false)
|
||||
setPlaceholderVisible(true)
|
||||
}}
|
||||
onLoadEnd={() => setLoading(false)}
|
||||
/>
|
||||
)
|
||||
|
||||
return (
|
||||
<View style={{ ...styles.container, height, width }}>
|
||||
{coverArtUri ? <Image /> : <></>}
|
||||
<View style={{ ...styles.placeholderContainer, opacity: placeholderVisible ? 1 : 0 }}>
|
||||
<PlaceholderComponent />
|
||||
</View>
|
||||
<ActivityIndicator style={styles.indicator} animating={loading} size={'large'} color={colors.accent} />
|
||||
</View>
|
||||
)
|
||||
}
|
||||
|
||||
const styles = StyleSheet.create({
|
||||
container: {},
|
||||
image: {
|
||||
height: '100%',
|
||||
width: '100%',
|
||||
},
|
||||
placeholderContainer: {
|
||||
height: '100%',
|
||||
width: '100%',
|
||||
position: 'absolute',
|
||||
},
|
||||
indicator: {
|
||||
height: '100%',
|
||||
width: '100%',
|
||||
position: 'absolute',
|
||||
},
|
||||
})
|
||||
|
||||
export default React.memo(CoverArt)
|
||||
31
app/components/GradientBackground.tsx
Normal file
31
app/components/GradientBackground.tsx
Normal file
@@ -0,0 +1,31 @@
|
||||
import React from 'react'
|
||||
import { useWindowDimensions, ViewStyle } from 'react-native'
|
||||
import LinearGradient from 'react-native-linear-gradient'
|
||||
import colorStyles from '@app/styles/colors'
|
||||
|
||||
const GradientBackground: React.FC<{
|
||||
height?: number | string
|
||||
width?: number | string
|
||||
position?: 'relative' | 'absolute'
|
||||
style?: ViewStyle
|
||||
colors?: string[]
|
||||
locations?: number[]
|
||||
}> = ({ height, width, position, style, colors, locations, children }) => {
|
||||
const layout = useWindowDimensions()
|
||||
|
||||
return (
|
||||
<LinearGradient
|
||||
colors={colors || [colorStyles.gradient.high, colorStyles.gradient.low]}
|
||||
locations={locations || [0.01, 0.7]}
|
||||
style={{
|
||||
...style,
|
||||
width: width || '100%',
|
||||
height: height || layout.height,
|
||||
position: position || 'absolute',
|
||||
}}>
|
||||
{children}
|
||||
</LinearGradient>
|
||||
)
|
||||
}
|
||||
|
||||
export default GradientBackground
|
||||
24
app/components/GradientFlatList.tsx
Normal file
24
app/components/GradientFlatList.tsx
Normal file
@@ -0,0 +1,24 @@
|
||||
import React from 'react'
|
||||
import { FlatList, FlatListProps, useWindowDimensions } from 'react-native'
|
||||
import colors from '@app/styles/colors'
|
||||
import GradientBackground from '@app/components/GradientBackground'
|
||||
|
||||
function GradientFlatList<ItemT>(props: FlatListProps<ItemT>) {
|
||||
const layout = useWindowDimensions()
|
||||
|
||||
return (
|
||||
<FlatList
|
||||
{...props}
|
||||
style={{
|
||||
...(props.style as any),
|
||||
backgroundColor: colors.gradient.low,
|
||||
}}
|
||||
ListHeaderComponent={() => <GradientBackground position="relative" />}
|
||||
ListHeaderComponentStyle={{
|
||||
marginBottom: -layout.height,
|
||||
}}
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
||||
export default GradientFlatList
|
||||
18
app/components/GradientScrollView.tsx
Normal file
18
app/components/GradientScrollView.tsx
Normal file
@@ -0,0 +1,18 @@
|
||||
import React from 'react'
|
||||
import { ScrollView, ScrollViewProps, ViewStyle } from 'react-native'
|
||||
import colors from '@app/styles/colors'
|
||||
import GradientBackground from '@app/components/GradientBackground'
|
||||
|
||||
const GradientScrollView: React.FC<ScrollViewProps> = props => {
|
||||
props.style = props.style || {}
|
||||
;(props.style as ViewStyle).backgroundColor = colors.gradient.low
|
||||
|
||||
return (
|
||||
<ScrollView overScrollMode="never" {...props}>
|
||||
<GradientBackground />
|
||||
{props.children}
|
||||
</ScrollView>
|
||||
)
|
||||
}
|
||||
|
||||
export default GradientScrollView
|
||||
70
app/components/ImageGradientBackground.tsx
Normal file
70
app/components/ImageGradientBackground.tsx
Normal file
@@ -0,0 +1,70 @@
|
||||
import { useNavigation } from '@react-navigation/native'
|
||||
import React, { useEffect, useState } from 'react'
|
||||
import { ViewStyle } from 'react-native'
|
||||
import FastImage from 'react-native-fast-image'
|
||||
import ImageColors from 'react-native-image-colors'
|
||||
import { AndroidImageColors } from 'react-native-image-colors/lib/typescript/types'
|
||||
import colors from '@app/styles/colors'
|
||||
import GradientBackground from '@app/components/GradientBackground'
|
||||
|
||||
const ImageGradientBackground: React.FC<{
|
||||
height?: number | string
|
||||
width?: number | string
|
||||
position?: 'relative' | 'absolute'
|
||||
style?: ViewStyle
|
||||
imageUri?: string
|
||||
imageKey?: string
|
||||
}> = ({ height, width, position, style, imageUri, imageKey, children }) => {
|
||||
const [highColor, setHighColor] = useState<string>(colors.gradient.high)
|
||||
const navigation = useNavigation()
|
||||
|
||||
useEffect(() => {
|
||||
async function getColors() {
|
||||
if (imageUri === undefined) {
|
||||
return
|
||||
}
|
||||
|
||||
const cachedResult = ImageColors.cache.getItem(imageKey ? imageKey : imageUri)
|
||||
|
||||
let res: AndroidImageColors
|
||||
if (cachedResult) {
|
||||
res = cachedResult as AndroidImageColors
|
||||
} else {
|
||||
const path = await FastImage.getCachePath({ uri: imageUri })
|
||||
res = (await ImageColors.getColors(path ? `file://${path}` : imageUri, {
|
||||
cache: true,
|
||||
key: imageKey ? imageKey : imageUri,
|
||||
})) as AndroidImageColors
|
||||
}
|
||||
|
||||
if (res.muted && res.muted !== '#000000') {
|
||||
setHighColor(res.muted)
|
||||
} else if (res.darkMuted && res.darkMuted !== '#000000') {
|
||||
setHighColor(res.darkMuted)
|
||||
}
|
||||
}
|
||||
getColors()
|
||||
}, [imageUri, imageKey])
|
||||
|
||||
useEffect(() => {
|
||||
navigation.setOptions({
|
||||
headerStyle: {
|
||||
backgroundColor: highColor,
|
||||
},
|
||||
})
|
||||
}, [navigation, highColor])
|
||||
|
||||
return (
|
||||
<GradientBackground
|
||||
height={height}
|
||||
width={width}
|
||||
position={position}
|
||||
style={style}
|
||||
colors={[highColor, colors.gradient.low]}
|
||||
locations={[0.1, 1.0]}>
|
||||
{children}
|
||||
</GradientBackground>
|
||||
)
|
||||
}
|
||||
|
||||
export default ImageGradientBackground
|
||||
30
app/components/ImageGradientScrollView.tsx
Normal file
30
app/components/ImageGradientScrollView.tsx
Normal file
@@ -0,0 +1,30 @@
|
||||
import React, { useState } from 'react'
|
||||
import { LayoutRectangle, ScrollView, ScrollViewProps } from 'react-native'
|
||||
import colors from '@app/styles/colors'
|
||||
import ImageGradientBackground from '@app/components/ImageGradientBackground'
|
||||
|
||||
const ImageGradientScrollView: React.FC<ScrollViewProps & { imageUri?: string; imageKey?: string }> = props => {
|
||||
const [layout, setLayout] = useState<LayoutRectangle | undefined>(undefined)
|
||||
|
||||
props.style = props.style || {}
|
||||
if (typeof props.style === 'object' && props.style !== null) {
|
||||
props.style = {
|
||||
...props.style,
|
||||
backgroundColor: colors.gradient.low,
|
||||
}
|
||||
}
|
||||
|
||||
return (
|
||||
<ScrollView
|
||||
overScrollMode="never"
|
||||
{...props}
|
||||
onLayout={event => {
|
||||
setLayout(event.nativeEvent.layout)
|
||||
}}>
|
||||
<ImageGradientBackground height={layout?.height} imageUri={props.imageUri} imageKey={props.imageKey} />
|
||||
{props.children}
|
||||
</ScrollView>
|
||||
)
|
||||
}
|
||||
|
||||
export default ImageGradientScrollView
|
||||
132
app/components/NowPlayingBar.tsx
Normal file
132
app/components/NowPlayingBar.tsx
Normal file
@@ -0,0 +1,132 @@
|
||||
import React from 'react'
|
||||
import { Pressable, StyleSheet, Text, View } from 'react-native'
|
||||
import { useNavigation } from '@react-navigation/native'
|
||||
import { useAtomValue } from 'jotai/utils'
|
||||
import { currentTrackAtom, playerStateAtom, usePause, usePlay, useProgress } from '@app/state/trackplayer'
|
||||
import CoverArt from '@app/components/CoverArt'
|
||||
import colors from '@app/styles/colors'
|
||||
import { Font } from '@app/styles/text'
|
||||
import { State } from 'react-native-track-player'
|
||||
import PressableOpacity from '@app/components/PressableOpacity'
|
||||
import IconFA5 from 'react-native-vector-icons/FontAwesome5'
|
||||
|
||||
const ProgressBar = () => {
|
||||
const { position, duration } = useProgress()
|
||||
|
||||
let progress = 0
|
||||
if (duration > 0) {
|
||||
progress = position / duration
|
||||
}
|
||||
|
||||
return (
|
||||
<View style={progressStyles.container}>
|
||||
<View style={{ ...progressStyles.left, flex: progress }} />
|
||||
<View style={{ ...progressStyles.right, flex: 1 - progress }} />
|
||||
</View>
|
||||
)
|
||||
}
|
||||
|
||||
const progressStyles = StyleSheet.create({
|
||||
container: {
|
||||
height: 2,
|
||||
flexDirection: 'row',
|
||||
},
|
||||
left: {
|
||||
backgroundColor: colors.text.primary,
|
||||
},
|
||||
right: {
|
||||
backgroundColor: '#595959',
|
||||
},
|
||||
})
|
||||
|
||||
const NowPlayingBar = () => {
|
||||
const navigation = useNavigation()
|
||||
const track = useAtomValue(currentTrackAtom)
|
||||
const playerState = useAtomValue(playerStateAtom)
|
||||
const play = usePlay()
|
||||
const pause = usePause()
|
||||
|
||||
let playPauseIcon: string
|
||||
let playPauseAction: () => void
|
||||
|
||||
switch (playerState) {
|
||||
case State.Playing:
|
||||
case State.Buffering:
|
||||
case State.Connecting:
|
||||
playPauseIcon = 'pause'
|
||||
playPauseAction = pause
|
||||
break
|
||||
default:
|
||||
playPauseIcon = 'play'
|
||||
playPauseAction = play
|
||||
break
|
||||
}
|
||||
|
||||
return (
|
||||
<Pressable
|
||||
onPress={() => navigation.navigate('NowPlaying')}
|
||||
style={{ ...styles.container, display: track ? 'flex' : 'none' }}>
|
||||
<ProgressBar />
|
||||
<View style={styles.subContainer}>
|
||||
<CoverArt
|
||||
PlaceholderComponent={() => <Text>hi</Text>}
|
||||
height={styles.subContainer.height}
|
||||
width={styles.subContainer.height}
|
||||
coverArtUri={track?.artworkThumb}
|
||||
/>
|
||||
<View style={styles.detailsContainer}>
|
||||
<Text numberOfLines={1} style={styles.detailsTitle}>
|
||||
{track?.title}
|
||||
</Text>
|
||||
<Text numberOfLines={1} style={styles.detailsAlbum}>
|
||||
{track?.artist}
|
||||
</Text>
|
||||
</View>
|
||||
<View style={styles.controls}>
|
||||
<PressableOpacity onPress={playPauseAction} hitSlop={14}>
|
||||
<IconFA5 name={playPauseIcon} size={28} color="white" />
|
||||
</PressableOpacity>
|
||||
</View>
|
||||
</View>
|
||||
</Pressable>
|
||||
)
|
||||
}
|
||||
|
||||
const styles = StyleSheet.create({
|
||||
container: {
|
||||
width: '100%',
|
||||
backgroundColor: colors.gradient.high,
|
||||
borderBottomColor: colors.gradient.low,
|
||||
borderBottomWidth: 1,
|
||||
},
|
||||
subContainer: {
|
||||
height: 60,
|
||||
flexDirection: 'row',
|
||||
},
|
||||
detailsContainer: {
|
||||
flex: 1,
|
||||
height: '100%',
|
||||
justifyContent: 'center',
|
||||
marginLeft: 10,
|
||||
},
|
||||
detailsTitle: {
|
||||
fontFamily: Font.semiBold,
|
||||
fontSize: 13,
|
||||
color: colors.text.primary,
|
||||
},
|
||||
detailsAlbum: {
|
||||
fontFamily: Font.regular,
|
||||
fontSize: 13,
|
||||
color: colors.text.secondary,
|
||||
},
|
||||
controls: {
|
||||
flexDirection: 'row',
|
||||
height: '100%',
|
||||
justifyContent: 'center',
|
||||
alignItems: 'center',
|
||||
marginRight: 18,
|
||||
marginLeft: 12,
|
||||
},
|
||||
})
|
||||
|
||||
export default NowPlayingBar
|
||||
44
app/components/PressableOpacity.tsx
Normal file
44
app/components/PressableOpacity.tsx
Normal file
@@ -0,0 +1,44 @@
|
||||
import React, { useEffect, useState } from 'react'
|
||||
import { LayoutRectangle, Pressable, PressableProps } from 'react-native'
|
||||
|
||||
type PressableOpacityProps = PressableProps & {
|
||||
ripple?: boolean
|
||||
}
|
||||
|
||||
const PressableOpacity: React.FC<PressableOpacityProps> = props => {
|
||||
const [opacity, setOpacity] = useState(1)
|
||||
const [dimensions, setDimensions] = useState<LayoutRectangle | undefined>(undefined)
|
||||
|
||||
useEffect(() => {
|
||||
props.disabled === true ? setOpacity(0.3) : setOpacity(1)
|
||||
}, [props.disabled])
|
||||
|
||||
return (
|
||||
<Pressable
|
||||
{...props}
|
||||
style={[{ justifyContent: 'center', alignItems: 'center' }, props.style as any, { opacity }]}
|
||||
android_ripple={
|
||||
props.ripple
|
||||
? {
|
||||
color: 'rgba(255,255,255,0.26)',
|
||||
radius: dimensions ? dimensions.width / 2 : undefined,
|
||||
}
|
||||
: undefined
|
||||
}
|
||||
onLayout={event => setDimensions(event.nativeEvent.layout)}
|
||||
onPressIn={() => {
|
||||
if (!props.disabled) {
|
||||
setOpacity(0.4)
|
||||
}
|
||||
}}
|
||||
onPressOut={() => {
|
||||
if (!props.disabled) {
|
||||
setOpacity(1)
|
||||
}
|
||||
}}>
|
||||
{props.children}
|
||||
</Pressable>
|
||||
)
|
||||
}
|
||||
|
||||
export default React.memo(PressableOpacity)
|
||||
157
app/components/TrackPlayerState.tsx
Normal file
157
app/components/TrackPlayerState.tsx
Normal file
@@ -0,0 +1,157 @@
|
||||
import { useAppState } from '@react-native-community/hooks'
|
||||
import { useAtomValue, useUpdateAtom } from 'jotai/utils'
|
||||
import React, { useEffect } from 'react'
|
||||
import { View } from 'react-native'
|
||||
import { Event, State, useProgress, useTrackPlayerEvents } from 'react-native-track-player'
|
||||
import {
|
||||
currentTrackAtom,
|
||||
playerStateAtom,
|
||||
progressAtom,
|
||||
progressSubsAtom,
|
||||
queueWriteAtom,
|
||||
useRefreshCurrentTrack,
|
||||
useRefreshPlayerState,
|
||||
useRefreshProgress,
|
||||
useRefreshQueue,
|
||||
} from '@app/state/trackplayer'
|
||||
|
||||
const AppActiveResponder: React.FC<{
|
||||
update: () => void
|
||||
}> = ({ update }) => {
|
||||
const appState = useAppState()
|
||||
|
||||
useEffect(() => {
|
||||
if (appState === 'active') {
|
||||
update()
|
||||
}
|
||||
}, [appState, update])
|
||||
|
||||
return <></>
|
||||
}
|
||||
|
||||
type Payload = { type: Event; [key: string]: any }
|
||||
|
||||
const TrackPlayerEventResponder: React.FC<{
|
||||
update: (payload?: Payload) => void
|
||||
events: Event[]
|
||||
}> = ({ update, events }) => {
|
||||
useTrackPlayerEvents(events, update)
|
||||
|
||||
return <AppActiveResponder update={update} />
|
||||
}
|
||||
|
||||
const CurrentTrackState = () => {
|
||||
const setCurrentTrack = useUpdateAtom(currentTrackAtom)
|
||||
const refreshCurrentTrack = useRefreshCurrentTrack()
|
||||
|
||||
const update = async (payload?: Payload) => {
|
||||
const queueEnded = payload?.type === Event.PlaybackQueueEnded && 'track' in payload
|
||||
const remoteStop = payload?.type === Event.RemoteStop
|
||||
if (queueEnded || remoteStop) {
|
||||
setCurrentTrack(undefined)
|
||||
return
|
||||
}
|
||||
await refreshCurrentTrack()
|
||||
}
|
||||
|
||||
return (
|
||||
<TrackPlayerEventResponder
|
||||
events={[
|
||||
Event.PlaybackQueueEnded,
|
||||
Event.PlaybackMetadataReceived,
|
||||
Event.RemoteDuck,
|
||||
Event.RemoteNext,
|
||||
Event.RemotePrevious,
|
||||
Event.RemoteStop,
|
||||
]}
|
||||
update={update}
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
||||
const PlayerState = () => {
|
||||
const setPlayerState = useUpdateAtom(playerStateAtom)
|
||||
const refreshPlayerState = useRefreshPlayerState()
|
||||
|
||||
const update = async (payload?: Payload) => {
|
||||
if (payload?.type === Event.RemoteStop) {
|
||||
setPlayerState(State.None)
|
||||
return
|
||||
}
|
||||
await refreshPlayerState()
|
||||
}
|
||||
|
||||
return <TrackPlayerEventResponder events={[Event.PlaybackState, Event.RemoteStop]} update={update} />
|
||||
}
|
||||
|
||||
const QueueState = () => {
|
||||
const setQueue = useUpdateAtom(queueWriteAtom)
|
||||
const refreshQueue = useRefreshQueue()
|
||||
|
||||
const update = async (payload?: Payload) => {
|
||||
if (payload) {
|
||||
setQueue([])
|
||||
return
|
||||
}
|
||||
await refreshQueue()
|
||||
}
|
||||
|
||||
return <TrackPlayerEventResponder events={[Event.RemoteStop]} update={update} />
|
||||
}
|
||||
|
||||
const ProgressHook = () => {
|
||||
const setProgress = useUpdateAtom(progressAtom)
|
||||
const progress = useProgress(250)
|
||||
|
||||
useEffect(() => {
|
||||
setProgress(progress)
|
||||
}, [setProgress, progress])
|
||||
|
||||
return <></>
|
||||
}
|
||||
|
||||
const ProgressState = () => {
|
||||
const setProgress = useUpdateAtom(progressAtom)
|
||||
const refreshProgress = useRefreshProgress()
|
||||
const progressSubs = useAtomValue(progressSubsAtom)
|
||||
|
||||
const update = async (payload?: Payload) => {
|
||||
if (payload) {
|
||||
setProgress({ position: 0, duration: 0, buffered: 0 })
|
||||
return
|
||||
}
|
||||
await refreshProgress()
|
||||
}
|
||||
|
||||
if (progressSubs > 0) {
|
||||
return (
|
||||
<>
|
||||
<ProgressHook />
|
||||
<TrackPlayerEventResponder events={[Event.RemoteStop, Event.PlaybackTrackChanged]} update={update} />
|
||||
</>
|
||||
)
|
||||
}
|
||||
return <TrackPlayerEventResponder events={[Event.RemoteStop]} update={update} />
|
||||
}
|
||||
|
||||
const Debug = () => {
|
||||
const value = useAtomValue(currentTrackAtom)
|
||||
|
||||
useEffect(() => {
|
||||
// ToastAndroid.show(value?.title || 'undefined', 1)
|
||||
}, [value])
|
||||
|
||||
return <></>
|
||||
}
|
||||
|
||||
const TrackPlayerState = () => (
|
||||
<View>
|
||||
<CurrentTrackState />
|
||||
<PlayerState />
|
||||
<QueueState />
|
||||
<ProgressState />
|
||||
<Debug />
|
||||
</View>
|
||||
)
|
||||
|
||||
export default TrackPlayerState
|
||||
Reference in New Issue
Block a user