added the now playing bar

This commit is contained in:
austinried 2021-07-07 11:09:24 +09:00
parent 49b5ce3f6c
commit e1fa63beed
12 changed files with 241 additions and 129 deletions

BIN
res/next.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 5.9 KiB

BIN
res/pause-fill.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.0 KiB

BIN
res/play-fill.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.9 KiB

View File

@ -0,0 +1,140 @@
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 '../state/trackplayer'
import CoverArt from './common/CoverArt'
import colors from '../styles/colors'
import { Font } from '../styles/text'
import PressableImage from './common/PressableImage'
import { State } from 'react-native-track-player'
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: 1,
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: number
let playPauseAction: () => void
switch (playerState) {
case State.Playing:
case State.Buffering:
case State.Connecting:
playPauseIcon = require('../../res/pause-fill.png')
playPauseAction = pause
break
default:
playPauseIcon = require('../../res/play-fill.png')
playPauseAction = play
break
}
return (
<Pressable
onPress={() => navigation.navigate('Now Playing')}
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?.artwork as string}
/>
<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}>
<PressableImage
onPress={playPauseAction}
source={playPauseIcon}
style={styles.play}
tintColor="white"
hitSlop={14}
/>
</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,
// backgroundColor: 'green',
},
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,
},
play: {
height: 32,
width: 32,
},
})
export default NowPlayingBar

View File

@ -2,12 +2,14 @@ import { useAtomValue } from 'jotai/utils'
import React from 'react'
import { StatusBar, StyleSheet, Text, useWindowDimensions, View } from 'react-native'
import FastImage from 'react-native-fast-image'
import TrackPlayer, { State } from 'react-native-track-player'
import { State } from 'react-native-track-player'
import {
queueNameAtom,
currentTrackAtom,
playerStateAtom,
queueNameAtom,
useNext,
usePause,
usePlay,
usePrevious,
useProgress,
} from '../state/trackplayer'
@ -171,6 +173,8 @@ const seekStyles = StyleSheet.create({
const PlayerControls = () => {
const state = useAtomValue(playerStateAtom)
const play = usePlay()
const pause = usePause()
const next = useNext()
const previous = usePrevious()
@ -184,12 +188,12 @@ const PlayerControls = () => {
case State.Connecting:
disabled = false
playPauseIcon = require('../../res/pause_circle-fill.png')
playPauseAction = () => TrackPlayer.pause()
playPauseAction = pause
break
case State.Paused:
disabled = false
playPauseIcon = require('../../res/play_circle-fill.png')
playPauseAction = () => TrackPlayer.play()
playPauseAction = play
break
default:
disabled = true

View File

@ -127,7 +127,7 @@ const ProgressState = () => {
return (
<>
<ProgressHook />
<TrackPlayerEventResponder events={[Event.RemoteStop]} update={update} />
<TrackPlayerEventResponder events={[Event.RemoteStop, Event.PlaybackTrackChanged]} update={update} />
</>
)
}

View File

@ -4,6 +4,7 @@ import { BottomTabBarProps } from '@react-navigation/bottom-tabs'
import textStyles from '../../styles/text'
import colors from '../../styles/colors'
import FastImage from 'react-native-fast-image'
import NowPlayingBar from '../NowPlayingBar'
const icons: { [key: string]: any } = {
home: {
@ -77,36 +78,39 @@ const BottomTabButton: React.FC<{
const BottomTabBar: React.FC<BottomTabBarProps> = ({ state, descriptors, navigation }) => {
return (
<View
style={{
height: 54,
backgroundColor: colors.gradient.high,
flexDirection: 'row',
alignItems: 'center',
justifyContent: 'space-around',
paddingHorizontal: 28,
}}>
{state.routes.map((route, index) => {
const { options } = descriptors[route.key] as any
const label =
options.tabBarLabel !== undefined
? (options.tabBarLabel as string)
: options.title !== undefined
? options.title
: route.name
<View>
<NowPlayingBar />
<View
style={{
height: 54,
backgroundColor: colors.gradient.high,
flexDirection: 'row',
alignItems: 'center',
justifyContent: 'space-around',
paddingHorizontal: 28,
}}>
{state.routes.map((route, index) => {
const { options } = descriptors[route.key] as any
const label =
options.tabBarLabel !== undefined
? (options.tabBarLabel as string)
: options.title !== undefined
? options.title
: route.name
return (
<BottomTabButton
key={route.key}
routeKey={route.key}
label={label}
name={route.name}
isFocused={state.index === index}
img={icons[options.icon]}
navigation={navigation}
/>
)
})}
return (
<BottomTabButton
key={route.key}
routeKey={route.key}
label={label}
name={route.name}
isFocused={state.index === index}
img={icons[options.icon]}
navigation={navigation}
/>
)
})}
</View>
</View>
)
}

View File

@ -1,58 +1,63 @@
import React, { useState } from 'react'
import { ActivityIndicator, View } from 'react-native'
import React, { useEffect, useState } from 'react'
import { ActivityIndicator, StyleSheet, View } from 'react-native'
import FastImage from 'react-native-fast-image'
import colors from '../../styles/colors'
const CoverArt: React.FC<{
PlaceholderComponent: () => JSX.Element
height: number
width: number
height?: string | number
width?: string | number
coverArtUri?: string
}> = ({ PlaceholderComponent, height, width, coverArtUri }) => {
const [placeholderVisible, setPlaceholderVisible] = useState(false)
const [loading, setLoading] = useState(true)
const indicatorSize = height > 130 ? 'large' : 'small'
const halfIndicatorHeight = indicatorSize === 'large' ? 18 : 10
useEffect(() => {
if (!coverArtUri) {
setLoading(false)
}
}, [coverArtUri, setLoading])
const Placeholder: React.FC<{ visible: boolean }> = ({ visible }) => (
<View
style={{
opacity: visible ? 100 : 0,
}}>
<PlaceholderComponent />
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 Art = () => (
<>
<Placeholder visible={placeholderVisible} />
<ActivityIndicator
animating={loading}
size={indicatorSize}
color={colors.accent}
style={{
top: -height / 2 - halfIndicatorHeight,
}}
/>
<FastImage
source={{ uri: coverArtUri, priority: 'high' }}
style={{
height,
width,
marginTop: -height - halfIndicatorHeight * 2,
}}
resizeMode={FastImage.resizeMode.contain}
onError={() => {
setLoading(false)
setPlaceholderVisible(true)
}}
onLoadEnd={() => setLoading(false)}
/>
</>
)
return <View style={{ height, width }}>{!coverArtUri ? <Placeholder visible={true} /> : <Art />}</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)

View File

@ -8,7 +8,8 @@ const PressableImage: React.FC<{
style?: ViewStyle
tintColor?: string
disabled?: boolean
}> = ({ source, onPress, style, tintColor, disabled }) => {
hitSlop?: number
}> = ({ source, onPress, style, tintColor, disabled, hitSlop }) => {
const [opacity, setOpacity] = useState(1)
const [dimensions, setDimensions] = useState<LayoutRectangle | undefined>(undefined)
@ -27,6 +28,7 @@ const PressableImage: React.FC<{
style={style}
onPress={onPress}
disabled={disabled}
hitSlop={hitSlop}
onPressIn={() => {
if (!disabled) {
setOpacity(0.4)

View File

@ -1,51 +0,0 @@
import { useUpdateAtom } from 'jotai/utils'
import TrackPlayer, { Track } from 'react-native-track-player'
import { Song } from '../models/music'
import { currentQueueNameAtom, currentTrackAtom } from '../state/trackplayer'
function mapSongToTrack(song: Song, queueName: string): Track {
return {
id: song.id,
queueName,
title: song.title,
artist: song.artist || 'Unknown Artist',
url: song.streamUri,
artwork: song.coverArtUri,
artworkThumb: song.coverArtThumbUri,
duration: song.duration,
}
}
export const useSetQueue = () => {
const setCurrentTrack = useUpdateAtom(currentTrackAtom)
const setCurrentQueueName = useUpdateAtom(currentQueueNameAtom)
return async (songs: Song[], name: string, playId?: string) => {
await TrackPlayer.reset()
const tracks = songs.map(s => mapSongToTrack(s, name))
setCurrentQueueName(name)
if (playId) {
setCurrentTrack(tracks.find(t => t.id === playId))
}
if (!playId) {
await TrackPlayer.add(tracks)
} else if (playId === tracks[0].id) {
await TrackPlayer.add(tracks)
await TrackPlayer.play()
} else {
const playIndex = tracks.findIndex(t => t.id === playId)
const tracks1 = tracks.slice(0, playIndex)
const tracks2 = tracks.slice(playIndex)
await TrackPlayer.add(tracks2)
await TrackPlayer.play()
await TrackPlayer.add(tracks1, 0)
// const queue = await TrackPlayer.getQueue();
// console.log(`queue: ${JSON.stringify(queue.map(x => x.title))}`);
}
}
}

View File

@ -145,6 +145,14 @@ export const useRefreshProgress = () => {
})
}
export const usePlay = () => {
return () => trackPlayerCommands.enqueue(() => TrackPlayer.play())
}
export const usePause = () => {
return () => trackPlayerCommands.enqueue(() => TrackPlayer.pause())
}
export const usePrevious = () => {
const setCurrentTrack = useUpdateAtom(currentTrackAtom)
@ -259,6 +267,7 @@ function mapSongToTrack(song: Song, queueName: string): TrackExt {
queueName,
title: song.title,
artist: song.artist || 'Unknown Artist',
album: song.album || 'Unknown Album',
url: song.streamUri,
artwork: song.coverArtUri,
artworkThumb: song.coverArtThumbUri,

View File

@ -5,7 +5,6 @@ export default {
},
gradient: {
high: '#2d2d2d',
mid: '#191919',
low: '#000000',
},
accent: '#b134db',