mirror of
https://github.com/austinried/subtracks.git
synced 2025-12-28 17:19:27 +01:00
added the now playing bar
This commit is contained in:
parent
49b5ce3f6c
commit
e1fa63beed
BIN
res/next.png
Normal file
BIN
res/next.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 5.9 KiB |
BIN
res/pause-fill.png
Normal file
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
BIN
res/play-fill.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 3.9 KiB |
140
src/components/NowPlayingBar.tsx
Normal file
140
src/components/NowPlayingBar.tsx
Normal 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
|
||||||
@ -2,12 +2,14 @@ import { useAtomValue } from 'jotai/utils'
|
|||||||
import React from 'react'
|
import React from 'react'
|
||||||
import { StatusBar, StyleSheet, Text, useWindowDimensions, View } from 'react-native'
|
import { StatusBar, StyleSheet, Text, useWindowDimensions, View } from 'react-native'
|
||||||
import FastImage from 'react-native-fast-image'
|
import FastImage from 'react-native-fast-image'
|
||||||
import TrackPlayer, { State } from 'react-native-track-player'
|
import { State } from 'react-native-track-player'
|
||||||
import {
|
import {
|
||||||
queueNameAtom,
|
|
||||||
currentTrackAtom,
|
currentTrackAtom,
|
||||||
playerStateAtom,
|
playerStateAtom,
|
||||||
|
queueNameAtom,
|
||||||
useNext,
|
useNext,
|
||||||
|
usePause,
|
||||||
|
usePlay,
|
||||||
usePrevious,
|
usePrevious,
|
||||||
useProgress,
|
useProgress,
|
||||||
} from '../state/trackplayer'
|
} from '../state/trackplayer'
|
||||||
@ -171,6 +173,8 @@ const seekStyles = StyleSheet.create({
|
|||||||
|
|
||||||
const PlayerControls = () => {
|
const PlayerControls = () => {
|
||||||
const state = useAtomValue(playerStateAtom)
|
const state = useAtomValue(playerStateAtom)
|
||||||
|
const play = usePlay()
|
||||||
|
const pause = usePause()
|
||||||
const next = useNext()
|
const next = useNext()
|
||||||
const previous = usePrevious()
|
const previous = usePrevious()
|
||||||
|
|
||||||
@ -184,12 +188,12 @@ const PlayerControls = () => {
|
|||||||
case State.Connecting:
|
case State.Connecting:
|
||||||
disabled = false
|
disabled = false
|
||||||
playPauseIcon = require('../../res/pause_circle-fill.png')
|
playPauseIcon = require('../../res/pause_circle-fill.png')
|
||||||
playPauseAction = () => TrackPlayer.pause()
|
playPauseAction = pause
|
||||||
break
|
break
|
||||||
case State.Paused:
|
case State.Paused:
|
||||||
disabled = false
|
disabled = false
|
||||||
playPauseIcon = require('../../res/play_circle-fill.png')
|
playPauseIcon = require('../../res/play_circle-fill.png')
|
||||||
playPauseAction = () => TrackPlayer.play()
|
playPauseAction = play
|
||||||
break
|
break
|
||||||
default:
|
default:
|
||||||
disabled = true
|
disabled = true
|
||||||
|
|||||||
@ -127,7 +127,7 @@ const ProgressState = () => {
|
|||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<ProgressHook />
|
<ProgressHook />
|
||||||
<TrackPlayerEventResponder events={[Event.RemoteStop]} update={update} />
|
<TrackPlayerEventResponder events={[Event.RemoteStop, Event.PlaybackTrackChanged]} update={update} />
|
||||||
</>
|
</>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|||||||
@ -4,6 +4,7 @@ import { BottomTabBarProps } from '@react-navigation/bottom-tabs'
|
|||||||
import textStyles from '../../styles/text'
|
import textStyles from '../../styles/text'
|
||||||
import colors from '../../styles/colors'
|
import colors from '../../styles/colors'
|
||||||
import FastImage from 'react-native-fast-image'
|
import FastImage from 'react-native-fast-image'
|
||||||
|
import NowPlayingBar from '../NowPlayingBar'
|
||||||
|
|
||||||
const icons: { [key: string]: any } = {
|
const icons: { [key: string]: any } = {
|
||||||
home: {
|
home: {
|
||||||
@ -77,36 +78,39 @@ const BottomTabButton: React.FC<{
|
|||||||
|
|
||||||
const BottomTabBar: React.FC<BottomTabBarProps> = ({ state, descriptors, navigation }) => {
|
const BottomTabBar: React.FC<BottomTabBarProps> = ({ state, descriptors, navigation }) => {
|
||||||
return (
|
return (
|
||||||
<View
|
<View>
|
||||||
style={{
|
<NowPlayingBar />
|
||||||
height: 54,
|
<View
|
||||||
backgroundColor: colors.gradient.high,
|
style={{
|
||||||
flexDirection: 'row',
|
height: 54,
|
||||||
alignItems: 'center',
|
backgroundColor: colors.gradient.high,
|
||||||
justifyContent: 'space-around',
|
flexDirection: 'row',
|
||||||
paddingHorizontal: 28,
|
alignItems: 'center',
|
||||||
}}>
|
justifyContent: 'space-around',
|
||||||
{state.routes.map((route, index) => {
|
paddingHorizontal: 28,
|
||||||
const { options } = descriptors[route.key] as any
|
}}>
|
||||||
const label =
|
{state.routes.map((route, index) => {
|
||||||
options.tabBarLabel !== undefined
|
const { options } = descriptors[route.key] as any
|
||||||
? (options.tabBarLabel as string)
|
const label =
|
||||||
: options.title !== undefined
|
options.tabBarLabel !== undefined
|
||||||
? options.title
|
? (options.tabBarLabel as string)
|
||||||
: route.name
|
: options.title !== undefined
|
||||||
|
? options.title
|
||||||
|
: route.name
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<BottomTabButton
|
<BottomTabButton
|
||||||
key={route.key}
|
key={route.key}
|
||||||
routeKey={route.key}
|
routeKey={route.key}
|
||||||
label={label}
|
label={label}
|
||||||
name={route.name}
|
name={route.name}
|
||||||
isFocused={state.index === index}
|
isFocused={state.index === index}
|
||||||
img={icons[options.icon]}
|
img={icons[options.icon]}
|
||||||
navigation={navigation}
|
navigation={navigation}
|
||||||
/>
|
/>
|
||||||
)
|
)
|
||||||
})}
|
})}
|
||||||
|
</View>
|
||||||
</View>
|
</View>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|||||||
@ -1,58 +1,63 @@
|
|||||||
import React, { useState } from 'react'
|
import React, { useEffect, useState } from 'react'
|
||||||
import { ActivityIndicator, View } from 'react-native'
|
import { ActivityIndicator, StyleSheet, View } from 'react-native'
|
||||||
import FastImage from 'react-native-fast-image'
|
import FastImage from 'react-native-fast-image'
|
||||||
import colors from '../../styles/colors'
|
import colors from '../../styles/colors'
|
||||||
|
|
||||||
const CoverArt: React.FC<{
|
const CoverArt: React.FC<{
|
||||||
PlaceholderComponent: () => JSX.Element
|
PlaceholderComponent: () => JSX.Element
|
||||||
height: number
|
height?: string | number
|
||||||
width: number
|
width?: string | number
|
||||||
coverArtUri?: string
|
coverArtUri?: string
|
||||||
}> = ({ PlaceholderComponent, height, width, coverArtUri }) => {
|
}> = ({ PlaceholderComponent, height, width, coverArtUri }) => {
|
||||||
const [placeholderVisible, setPlaceholderVisible] = useState(false)
|
const [placeholderVisible, setPlaceholderVisible] = useState(false)
|
||||||
const [loading, setLoading] = useState(true)
|
const [loading, setLoading] = useState(true)
|
||||||
|
|
||||||
const indicatorSize = height > 130 ? 'large' : 'small'
|
useEffect(() => {
|
||||||
const halfIndicatorHeight = indicatorSize === 'large' ? 18 : 10
|
if (!coverArtUri) {
|
||||||
|
setLoading(false)
|
||||||
|
}
|
||||||
|
}, [coverArtUri, setLoading])
|
||||||
|
|
||||||
const Placeholder: React.FC<{ visible: boolean }> = ({ visible }) => (
|
const Image = () => (
|
||||||
<View
|
<FastImage
|
||||||
style={{
|
source={{ uri: coverArtUri, priority: 'high' }}
|
||||||
opacity: visible ? 100 : 0,
|
style={{ ...styles.image, opacity: placeholderVisible ? 0 : 1 }}
|
||||||
}}>
|
resizeMode={FastImage.resizeMode.contain}
|
||||||
<PlaceholderComponent />
|
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>
|
</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)
|
export default React.memo(CoverArt)
|
||||||
|
|||||||
@ -8,7 +8,8 @@ const PressableImage: React.FC<{
|
|||||||
style?: ViewStyle
|
style?: ViewStyle
|
||||||
tintColor?: string
|
tintColor?: string
|
||||||
disabled?: boolean
|
disabled?: boolean
|
||||||
}> = ({ source, onPress, style, tintColor, disabled }) => {
|
hitSlop?: number
|
||||||
|
}> = ({ source, onPress, style, tintColor, disabled, hitSlop }) => {
|
||||||
const [opacity, setOpacity] = useState(1)
|
const [opacity, setOpacity] = useState(1)
|
||||||
const [dimensions, setDimensions] = useState<LayoutRectangle | undefined>(undefined)
|
const [dimensions, setDimensions] = useState<LayoutRectangle | undefined>(undefined)
|
||||||
|
|
||||||
@ -27,6 +28,7 @@ const PressableImage: React.FC<{
|
|||||||
style={style}
|
style={style}
|
||||||
onPress={onPress}
|
onPress={onPress}
|
||||||
disabled={disabled}
|
disabled={disabled}
|
||||||
|
hitSlop={hitSlop}
|
||||||
onPressIn={() => {
|
onPressIn={() => {
|
||||||
if (!disabled) {
|
if (!disabled) {
|
||||||
setOpacity(0.4)
|
setOpacity(0.4)
|
||||||
|
|||||||
@ -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))}`);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@ -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 = () => {
|
export const usePrevious = () => {
|
||||||
const setCurrentTrack = useUpdateAtom(currentTrackAtom)
|
const setCurrentTrack = useUpdateAtom(currentTrackAtom)
|
||||||
|
|
||||||
@ -259,6 +267,7 @@ function mapSongToTrack(song: Song, queueName: string): TrackExt {
|
|||||||
queueName,
|
queueName,
|
||||||
title: song.title,
|
title: song.title,
|
||||||
artist: song.artist || 'Unknown Artist',
|
artist: song.artist || 'Unknown Artist',
|
||||||
|
album: song.album || 'Unknown Album',
|
||||||
url: song.streamUri,
|
url: song.streamUri,
|
||||||
artwork: song.coverArtUri,
|
artwork: song.coverArtUri,
|
||||||
artworkThumb: song.coverArtThumbUri,
|
artworkThumb: song.coverArtThumbUri,
|
||||||
|
|||||||
@ -5,7 +5,6 @@ export default {
|
|||||||
},
|
},
|
||||||
gradient: {
|
gradient: {
|
||||||
high: '#2d2d2d',
|
high: '#2d2d2d',
|
||||||
mid: '#191919',
|
|
||||||
low: '#000000',
|
low: '#000000',
|
||||||
},
|
},
|
||||||
accent: '#b134db',
|
accent: '#b134db',
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user