From eb4199de3752f068ea681c68acdc35a5f8c2cbf1 Mon Sep 17 00:00:00 2001 From: austinried <4966622+austinried@users.noreply.github.com> Date: Tue, 6 Jul 2021 09:49:15 +0900 Subject: [PATCH] trying to centralize all player logic around state --- src/components/NowPlayingLayout.tsx | 50 ++++--- src/components/TrackPlayerState.tsx | 156 +++++++++----------- src/components/common/AlbumView.tsx | 3 +- src/components/common/PressableImage.tsx | 55 +++++++ src/state/trackplayer.ts | 179 +++++++++++++++++++---- 5 files changed, 302 insertions(+), 141 deletions(-) create mode 100644 src/components/common/PressableImage.tsx diff --git a/src/components/NowPlayingLayout.tsx b/src/components/NowPlayingLayout.tsx index 7a45de6..173ba71 100644 --- a/src/components/NowPlayingLayout.tsx +++ b/src/components/NowPlayingLayout.tsx @@ -1,17 +1,18 @@ import { useAtomValue } from 'jotai/utils' import React from 'react' -import { Pressable, 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 TrackPlayer, { State, useProgress } from 'react-native-track-player' -import { currentQueueNameAtom, currentTrackAtom, playerStateAtom } from '../state/trackplayer' +import { queueNameAtom, currentTrackAtom, playerStateAtom, useNext, usePrevious } from '../state/trackplayer' import colors from '../styles/colors' import text, { Font } from '../styles/text' import { formatDuration } from '../util' import CoverArt from './common/CoverArt' import ImageGradientBackground from './common/ImageGradientBackground' +import PressableImage from './common/PressableImage' const NowPlayingHeader = () => { - const queueName = useAtomValue(currentQueueNameAtom) + const queueName = useAtomValue(queueNameAtom) return ( @@ -163,45 +164,52 @@ const seekStyles = StyleSheet.create({ const PlayerControls = () => { const state = useAtomValue(playerStateAtom) + const next = useNext() + const previous = usePrevious() let playPauseIcon: number - let playPauseStyle: any - let playPauseAction: () => void + let playPauseAction: undefined | (() => void) + let disabled: boolean switch (state) { case State.Playing: case State.Buffering: case State.Connecting: + disabled = false playPauseIcon = require('../../res/pause_circle-fill.png') - playPauseStyle = controlsStyles.enabled playPauseAction = () => TrackPlayer.pause() break case State.Paused: + disabled = false playPauseIcon = require('../../res/play_circle-fill.png') - playPauseStyle = controlsStyles.enabled playPauseAction = () => TrackPlayer.play() break default: + disabled = true playPauseIcon = require('../../res/play_circle-fill.png') - playPauseStyle = controlsStyles.disabled - playPauseAction = () => {} + playPauseAction = undefined break } return ( - - - - - + ) @@ -224,12 +232,6 @@ const controlsStyles = StyleSheet.create({ height: 90, width: 90, }, - enabled: { - opacity: 1, - }, - disabled: { - opacity: 0.35, - }, }) const NowPlayingLayout = () => { diff --git a/src/components/TrackPlayerState.tsx b/src/components/TrackPlayerState.tsx index 7ac509f..91b6f14 100644 --- a/src/components/TrackPlayerState.tsx +++ b/src/components/TrackPlayerState.tsx @@ -1,19 +1,47 @@ -import React, { useCallback, useEffect } from 'react' -import TrackPlayer, { Event, State, useTrackPlayerEvents } from 'react-native-track-player' import { useAppState } from '@react-native-community/hooks' -import { useUpdateAtom, useAtomValue } from 'jotai/utils' -import { currentQueueNameAtom, currentTrackAtom, playerStateAtom } from '../state/trackplayer' +import { useAtomValue, useUpdateAtom } from 'jotai/utils' +import React, { useEffect } from 'react' import { View } from 'react-native' +import TrackPlayer, { Event, useTrackPlayerEvents } from 'react-native-track-player' +import { currentTrackAtom, getQueue, getTrack, playerStateAtom, queueWriteAtom } from '../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 +} const CurrentTrackState = () => { const setCurrentTrack = useUpdateAtom(currentTrackAtom) - const appState = useAppState() - const update = useCallback(async () => { + const update = async (payload?: Payload) => { + if (payload?.type === Event.PlaybackQueueEnded && 'track' in payload) { + setCurrentTrack(undefined) + return + } + const index = await TrackPlayer.getCurrentTrack() - if (index !== null && index >= 0) { - const track = await TrackPlayer.getTrack(index) + const track = await getTrack(index) if (track !== null) { setCurrentTrack(track) return @@ -21,102 +49,52 @@ const CurrentTrackState = () => { } setCurrentTrack(undefined) - }, [setCurrentTrack]) + } - useTrackPlayerEvents( - [ - // Event.PlaybackState, - // Event.PlaybackTrackChanged, - Event.PlaybackQueueEnded, - Event.PlaybackMetadataReceived, - Event.RemoteDuck, - Event.RemoteNext, - Event.RemotePrevious, - Event.RemoteStop, - ], - event => { - if (event.type === Event.PlaybackQueueEnded && 'track' in event) { - setCurrentTrack(undefined) - return - } - update() - }, + return ( + ) - - useEffect(() => { - if (appState === 'active') { - update() - } - }, [appState, update]) - - return <> -} - -const CurrentQueueName = () => { - const setCurrentQueueName = useUpdateAtom(currentQueueNameAtom) - const appState = useAppState() - - const update = useCallback(async () => { - const queue = await TrackPlayer.getQueue() - - if (queue !== null && queue.length > 0) { - setCurrentQueueName(queue[0].queueName) - return - } - - setCurrentQueueName(undefined) - }, [setCurrentQueueName]) - - useTrackPlayerEvents( - [Event.PlaybackState, Event.PlaybackQueueEnded, Event.PlaybackMetadataReceived, Event.RemoteDuck, Event.RemoteStop], - event => { - if (event.type === Event.PlaybackState) { - if (event.state === State.Stopped || event.state === State.None) { - return - } - } - update() - }, - ) - - useEffect(() => { - if (appState === 'active') { - update() - } - }, [appState, update]) - - return <> } const PlayerState = () => { const setPlayerState = useUpdateAtom(playerStateAtom) - const appState = useAppState() - const update = useCallback( - async (state?: State) => { - setPlayerState(state || (await TrackPlayer.getState())) - }, - [setPlayerState], - ) + const update = async (payload?: Payload) => { + setPlayerState(payload?.state || (await TrackPlayer.getState())) + } - useTrackPlayerEvents([Event.PlaybackState], event => { - update(event.state) - }) + return +} - useEffect(() => { - if (appState === 'active') { - update() +const QueueState = () => { + const setQueue = useUpdateAtom(queueWriteAtom) + + const update = async (payload?: Payload) => { + if (payload) { + setQueue([]) + return } - }, [appState, update]) + setQueue(await getQueue()) + } - return <> + return } const Debug = () => { - const value = useAtomValue(currentQueueNameAtom) + const value = useAtomValue(queueWriteAtom) useEffect(() => { - console.log(value) + console.log(value.map(t => t.title)) }, [value]) return <> @@ -125,8 +103,8 @@ const Debug = () => { const TrackPlayerState = () => ( - + ) diff --git a/src/components/common/AlbumView.tsx b/src/components/common/AlbumView.tsx index 0603eac..85a569e 100644 --- a/src/components/common/AlbumView.tsx +++ b/src/components/common/AlbumView.tsx @@ -10,9 +10,8 @@ import { useWindowDimensions, View, } from 'react-native' -import { useSetQueue } from '../../hooks/trackplayer' import { albumAtomFamily } from '../../state/music' -import { currentTrackAtom } from '../../state/trackplayer' +import { currentTrackAtom, useSetQueue } from '../../state/trackplayer' import colors from '../../styles/colors' import text from '../../styles/text' import AlbumArt from './AlbumArt' diff --git a/src/components/common/PressableImage.tsx b/src/components/common/PressableImage.tsx new file mode 100644 index 0000000..e32bfc5 --- /dev/null +++ b/src/components/common/PressableImage.tsx @@ -0,0 +1,55 @@ +import React, { useEffect, useState } from 'react' +import { GestureResponderEvent, LayoutRectangle, Pressable, ViewStyle } from 'react-native' +import FastImage, { Source } from 'react-native-fast-image' + +const PressableImage: React.FC<{ + source: Source | number + onPress?: (event: GestureResponderEvent) => void + style?: ViewStyle + tintColor?: string + disabled?: boolean +}> = ({ source, onPress, style, tintColor, disabled }) => { + const [opacity, setOpacity] = useState(1) + const [dimensions, setDimensions] = useState(undefined) + + disabled = disabled === undefined ? false : disabled + style = { + ...(style || {}), + opacity, + } + + useEffect(() => { + disabled ? setOpacity(0.3) : setOpacity(1) + }, [disabled]) + + return ( + { + if (!disabled) { + setOpacity(0.4) + } + }} + onPressOut={() => { + if (!disabled) { + setOpacity(1) + } + }} + onLayout={event => setDimensions(event.nativeEvent.layout)}> + + + ) +} + +export default PressableImage diff --git a/src/state/trackplayer.ts b/src/state/trackplayer.ts index fd59fa4..3711cd3 100644 --- a/src/state/trackplayer.ts +++ b/src/state/trackplayer.ts @@ -1,37 +1,164 @@ import { atom } from 'jotai' -import { State, Track } from 'react-native-track-player' +import TrackPlayer, { State, Track } from 'react-native-track-player' import equal from 'fast-deep-equal' +import { useUpdateAtom } from 'jotai/utils' +import { Song } from '../models/music' -type OptionalTrack = Track | undefined +type TrackExt = Track & { + id: string + queueName: string +} -const currentTrack = atom(undefined) -export const currentTrackAtom = atom( - get => get(currentTrack), - (get, set, value) => { - if (!equal(get(currentTrack), value)) { - set(currentTrack, value) - } - }, -) - -type OptionalString = string | undefined - -const currentQueueName = atom(undefined) -export const currentQueueNameAtom = atom( - get => get(currentQueueName), - (get, set, value) => { - if (get(currentQueueName) !== value) { - set(currentQueueName, value) - } - }, -) +type OptionalTrackExt = TrackExt | undefined const playerState = atom(State.None) export const playerStateAtom = atom( get => get(playerState), - (get, set, value) => { - if (get(playerState) !== value) { - set(playerState, value) + (get, set, update) => { + if (get(playerState) !== update) { + set(playerState, update) } }, ) + +const currentTrack = atom(undefined) +export const currentTrackAtom = atom( + get => get(currentTrack), + (get, set, update) => { + if (!equal(get(currentTrack), update)) { + set(currentTrack, update) + } + }, +) + +const _queue = atom([]) +export const queueAtom = atom(get => get(_queue)) +export const queueWriteAtom = atom( + get => get(_queue), + (get, set, update) => { + if (get(_queue) !== update) { + set(_queue, update) + } + }, +) + +export const queueNameAtom = atom(get => { + const queue = get(_queue) + if (queue.length > 0) { + return queue[0].queueName + } + return undefined +}) + +export const getQueue = async (): Promise => { + return ((await TrackPlayer.getQueue()) as TrackExt[]) || [] +} + +export const getTrack = async (index: number): Promise => { + return ((await TrackPlayer.getTrack(index)) as TrackExt) || undefined +} + +export const usePrevious = () => { + const setCurrentTrack = useUpdateAtom(currentTrackAtom) + + return async () => { + try { + const [current, queue] = await Promise.all([await TrackPlayer.getCurrentTrack(), await getQueue()]) + if (current > 0) { + await TrackPlayer.skipToPrevious() + setCurrentTrack(queue[current - 1]) + } else { + await TrackPlayer.seekTo(0) + } + await TrackPlayer.play() + } catch {} + } +} + +export const useNext = () => { + const setCurrentTrack = useUpdateAtom(currentTrackAtom) + + return async () => { + try { + const [current, queue] = await Promise.all([await TrackPlayer.getCurrentTrack(), await getQueue()]) + if (current >= queue.length - 1) { + await TrackPlayer.skip(0) + await TrackPlayer.pause() + setCurrentTrack(queue[0]) + } else { + await TrackPlayer.skipToNext() + setCurrentTrack(queue[current + 1]) + await TrackPlayer.play() + } + } catch {} + } +} + +export const useAdd = () => { + const setQueue = useUpdateAtom(queueWriteAtom) + const setCurrentTrack = useUpdateAtom(currentTrackAtom) + + return async (tracks: TrackExt | TrackExt[], insertBeforeindex?: number) => { + await TrackPlayer.add(tracks, insertBeforeindex) + + const queue = await getQueue() + setQueue(queue) + setCurrentTrack(queue.length > 0 ? queue[await TrackPlayer.getCurrentTrack()] : undefined) + } +} + +export const useReset = () => { + const setQueue = useUpdateAtom(queueWriteAtom) + const setCurrentTrack = useUpdateAtom(currentTrackAtom) + + return async () => { + await TrackPlayer.reset() + setQueue([]) + setCurrentTrack(undefined) + } +} + +export const useSetQueue = () => { + const setCurrentTrack = useUpdateAtom(currentTrackAtom) + const setQueue = useUpdateAtom(queueWriteAtom) + + return async (songs: Song[], name: string, playId?: string) => { + await TrackPlayer.reset() + const tracks = songs.map(s => mapSongToTrack(s, 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) + } + + setQueue(await getQueue()) + } +} + +function mapSongToTrack(song: Song, queueName: string): TrackExt { + 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, + } +}