diff --git a/res/next.png b/res/next.png new file mode 100644 index 0000000..2036056 Binary files /dev/null and b/res/next.png differ diff --git a/res/pause-fill.png b/res/pause-fill.png new file mode 100644 index 0000000..ebd877a Binary files /dev/null and b/res/pause-fill.png differ diff --git a/res/play-fill.png b/res/play-fill.png new file mode 100644 index 0000000..04b82a4 Binary files /dev/null and b/res/play-fill.png differ diff --git a/src/components/NowPlayingBar.tsx b/src/components/NowPlayingBar.tsx new file mode 100644 index 0000000..9d04826 --- /dev/null +++ b/src/components/NowPlayingBar.tsx @@ -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 ( + + + + + ) +} + +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 ( + navigation.navigate('Now Playing')} + style={{ ...styles.container, display: track ? 'flex' : 'none' }}> + + + hi} + height={styles.subContainer.height} + width={styles.subContainer.height} + coverArtUri={track?.artwork as string} + /> + + + {track?.title} + + + {track?.artist} + + + + + + + + ) +} + +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 diff --git a/src/components/NowPlayingLayout.tsx b/src/components/NowPlayingLayout.tsx index 121a710..9b45139 100644 --- a/src/components/NowPlayingLayout.tsx +++ b/src/components/NowPlayingLayout.tsx @@ -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 diff --git a/src/components/TrackPlayerState.tsx b/src/components/TrackPlayerState.tsx index 630fdfc..fc5b725 100644 --- a/src/components/TrackPlayerState.tsx +++ b/src/components/TrackPlayerState.tsx @@ -127,7 +127,7 @@ const ProgressState = () => { return ( <> - + ) } diff --git a/src/components/common/BottomTabBar.tsx b/src/components/common/BottomTabBar.tsx index e187ea6..edf89b8 100644 --- a/src/components/common/BottomTabBar.tsx +++ b/src/components/common/BottomTabBar.tsx @@ -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 = ({ state, descriptors, navigation }) => { return ( - - {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 + + + + {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 ( - - ) - })} + return ( + + ) + })} + ) } diff --git a/src/components/common/CoverArt.tsx b/src/components/common/CoverArt.tsx index acbe84b..f1757db 100644 --- a/src/components/common/CoverArt.tsx +++ b/src/components/common/CoverArt.tsx @@ -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 }) => ( - - + const Image = () => ( + { + setLoading(false) + setPlaceholderVisible(true) + }} + onLoadEnd={() => setLoading(false)} + /> + ) + + return ( + + {coverArtUri ? : <>} + + + + ) - - const Art = () => ( - <> - - - { - setLoading(false) - setPlaceholderVisible(true) - }} - onLoadEnd={() => setLoading(false)} - /> - - ) - - return {!coverArtUri ? : } } +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) diff --git a/src/components/common/PressableImage.tsx b/src/components/common/PressableImage.tsx index e32bfc5..d347f1b 100644 --- a/src/components/common/PressableImage.tsx +++ b/src/components/common/PressableImage.tsx @@ -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(undefined) @@ -27,6 +28,7 @@ const PressableImage: React.FC<{ style={style} onPress={onPress} disabled={disabled} + hitSlop={hitSlop} onPressIn={() => { if (!disabled) { setOpacity(0.4) diff --git a/src/hooks/trackplayer.ts b/src/hooks/trackplayer.ts deleted file mode 100644 index 74d7d72..0000000 --- a/src/hooks/trackplayer.ts +++ /dev/null @@ -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))}`); - } - } -} diff --git a/src/state/trackplayer.ts b/src/state/trackplayer.ts index 9dcdcaf..5459846 100644 --- a/src/state/trackplayer.ts +++ b/src/state/trackplayer.ts @@ -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, diff --git a/src/styles/colors.ts b/src/styles/colors.ts index 50bd00d..6de496d 100644 --- a/src/styles/colors.ts +++ b/src/styles/colors.ts @@ -5,7 +5,6 @@ export default { }, gradient: { high: '#2d2d2d', - mid: '#191919', low: '#000000', }, accent: '#b134db',