mirror of
https://github.com/austinried/subtracks.git
synced 2025-12-27 09:09:29 +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 { 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
|
||||
|
||||
@ -127,7 +127,7 @@ const ProgressState = () => {
|
||||
return (
|
||||
<>
|
||||
<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 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>
|
||||
)
|
||||
}
|
||||
|
||||
@ -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)
|
||||
|
||||
@ -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)
|
||||
|
||||
@ -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 = () => {
|
||||
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,
|
||||
|
||||
@ -5,7 +5,6 @@ export default {
|
||||
},
|
||||
gradient: {
|
||||
high: '#2d2d2d',
|
||||
mid: '#191919',
|
||||
low: '#000000',
|
||||
},
|
||||
accent: '#b134db',
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user