mirror of
https://github.com/austinried/subtracks.git
synced 2025-12-27 09:09:29 +01:00
trying to centralize all player logic around state
This commit is contained in:
parent
acc7759495
commit
eb4199de37
@ -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 (
|
||||
<View style={headerStyles.container}>
|
||||
@ -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 (
|
||||
<View style={controlsStyles.container}>
|
||||
<FastImage
|
||||
<PressableImage
|
||||
onPress={disabled ? undefined : previous}
|
||||
source={require('../../res/previous-fill.png')}
|
||||
tintColor="white"
|
||||
style={{ ...controlsStyles.skip, ...playPauseStyle }}
|
||||
style={controlsStyles.skip}
|
||||
disabled={disabled}
|
||||
/>
|
||||
<Pressable onPress={playPauseAction}>
|
||||
<FastImage source={playPauseIcon} tintColor="white" style={{ ...controlsStyles.play, ...playPauseStyle }} />
|
||||
</Pressable>
|
||||
<FastImage
|
||||
<PressableImage
|
||||
onPress={playPauseAction}
|
||||
source={playPauseIcon}
|
||||
style={controlsStyles.play}
|
||||
disabled={disabled}
|
||||
/>
|
||||
<PressableImage
|
||||
onPress={disabled ? undefined : next}
|
||||
source={require('../../res/next-fill.png')}
|
||||
tintColor="white"
|
||||
style={{ ...controlsStyles.skip, ...playPauseStyle }}
|
||||
style={controlsStyles.skip}
|
||||
disabled={disabled}
|
||||
/>
|
||||
</View>
|
||||
)
|
||||
@ -224,12 +232,6 @@ const controlsStyles = StyleSheet.create({
|
||||
height: 90,
|
||||
width: 90,
|
||||
},
|
||||
enabled: {
|
||||
opacity: 1,
|
||||
},
|
||||
disabled: {
|
||||
opacity: 0.35,
|
||||
},
|
||||
})
|
||||
|
||||
const NowPlayingLayout = () => {
|
||||
|
||||
@ -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 <AppActiveResponder update={update} />
|
||||
}
|
||||
|
||||
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 (
|
||||
<TrackPlayerEventResponder
|
||||
events={[
|
||||
Event.PlaybackQueueEnded,
|
||||
Event.PlaybackMetadataReceived,
|
||||
Event.RemoteDuck,
|
||||
Event.RemoteNext,
|
||||
Event.RemotePrevious,
|
||||
Event.RemoteStop,
|
||||
]}
|
||||
update={update}
|
||||
/>
|
||||
)
|
||||
|
||||
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 <TrackPlayerEventResponder events={[Event.PlaybackState]} update={update} />
|
||||
}
|
||||
|
||||
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 <TrackPlayerEventResponder events={[Event.RemoteStop]} update={update} />
|
||||
}
|
||||
|
||||
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 = () => (
|
||||
<View>
|
||||
<CurrentTrackState />
|
||||
<CurrentQueueName />
|
||||
<PlayerState />
|
||||
<QueueState />
|
||||
<Debug />
|
||||
</View>
|
||||
)
|
||||
|
||||
@ -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'
|
||||
|
||||
55
src/components/common/PressableImage.tsx
Normal file
55
src/components/common/PressableImage.tsx
Normal file
@ -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<LayoutRectangle | undefined>(undefined)
|
||||
|
||||
disabled = disabled === undefined ? false : disabled
|
||||
style = {
|
||||
...(style || {}),
|
||||
opacity,
|
||||
}
|
||||
|
||||
useEffect(() => {
|
||||
disabled ? setOpacity(0.3) : setOpacity(1)
|
||||
}, [disabled])
|
||||
|
||||
return (
|
||||
<Pressable
|
||||
style={style}
|
||||
onPress={onPress}
|
||||
disabled={disabled}
|
||||
onPressIn={() => {
|
||||
if (!disabled) {
|
||||
setOpacity(0.4)
|
||||
}
|
||||
}}
|
||||
onPressOut={() => {
|
||||
if (!disabled) {
|
||||
setOpacity(1)
|
||||
}
|
||||
}}
|
||||
onLayout={event => setDimensions(event.nativeEvent.layout)}>
|
||||
<FastImage
|
||||
style={{
|
||||
display: dimensions ? 'flex' : 'none',
|
||||
height: dimensions?.height,
|
||||
width: dimensions?.width,
|
||||
}}
|
||||
source={source}
|
||||
tintColor={tintColor || 'white'}
|
||||
resizeMode={FastImage.resizeMode.contain}
|
||||
/>
|
||||
</Pressable>
|
||||
)
|
||||
}
|
||||
|
||||
export default PressableImage
|
||||
@ -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<OptionalTrack>(undefined)
|
||||
export const currentTrackAtom = atom<OptionalTrack, OptionalTrack>(
|
||||
get => get(currentTrack),
|
||||
(get, set, value) => {
|
||||
if (!equal(get(currentTrack), value)) {
|
||||
set(currentTrack, value)
|
||||
}
|
||||
},
|
||||
)
|
||||
|
||||
type OptionalString = string | undefined
|
||||
|
||||
const currentQueueName = atom<OptionalString>(undefined)
|
||||
export const currentQueueNameAtom = atom<OptionalString, OptionalString>(
|
||||
get => get(currentQueueName),
|
||||
(get, set, value) => {
|
||||
if (get(currentQueueName) !== value) {
|
||||
set(currentQueueName, value)
|
||||
}
|
||||
},
|
||||
)
|
||||
type OptionalTrackExt = TrackExt | undefined
|
||||
|
||||
const playerState = atom<State>(State.None)
|
||||
export const playerStateAtom = atom<State, State>(
|
||||
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<OptionalTrackExt>(undefined)
|
||||
export const currentTrackAtom = atom<OptionalTrackExt, OptionalTrackExt>(
|
||||
get => get(currentTrack),
|
||||
(get, set, update) => {
|
||||
if (!equal(get(currentTrack), update)) {
|
||||
set(currentTrack, update)
|
||||
}
|
||||
},
|
||||
)
|
||||
|
||||
const _queue = atom<TrackExt[]>([])
|
||||
export const queueAtom = atom<TrackExt[]>(get => get(_queue))
|
||||
export const queueWriteAtom = atom<TrackExt[], TrackExt[]>(
|
||||
get => get(_queue),
|
||||
(get, set, update) => {
|
||||
if (get(_queue) !== update) {
|
||||
set(_queue, update)
|
||||
}
|
||||
},
|
||||
)
|
||||
|
||||
export const queueNameAtom = atom<string | undefined>(get => {
|
||||
const queue = get(_queue)
|
||||
if (queue.length > 0) {
|
||||
return queue[0].queueName
|
||||
}
|
||||
return undefined
|
||||
})
|
||||
|
||||
export const getQueue = async (): Promise<TrackExt[]> => {
|
||||
return ((await TrackPlayer.getQueue()) as TrackExt[]) || []
|
||||
}
|
||||
|
||||
export const getTrack = async (index: number): Promise<TrackExt> => {
|
||||
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,
|
||||
}
|
||||
}
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user