Made the Player behaviour more consistent

This commit is contained in:
Deluan 2021-07-02 14:07:03 -04:00
parent 26b5e6b1b4
commit ace5c905eb
14 changed files with 374 additions and 377 deletions

View file

@ -19,7 +19,7 @@ import customRoutes from './routes'
import { import {
themeReducer, themeReducer,
addToPlaylistDialogReducer, addToPlaylistDialogReducer,
playQueueReducer, playerReducer,
albumViewReducer, albumViewReducer,
activityReducer, activityReducer,
settingsReducer, settingsReducer,
@ -48,7 +48,7 @@ const App = () => (
dataProvider, dataProvider,
history, history,
customReducers: { customReducers: {
queue: playQueueReducer, player: playerReducer,
albumView: albumViewReducer, albumView: albumViewReducer,
theme: themeReducer, theme: themeReducer,
addToPlaylistDialog: addToPlaylistDialogReducer, addToPlaylistDialog: addToPlaylistDialogReducer,

View file

@ -1,4 +1,4 @@
export * from './audioplayer' export * from './player'
export * from './themes' export * from './themes'
export * from './albumView' export * from './albumView'
export * from './dialogs' export * from './dialogs'

View file

@ -3,7 +3,6 @@ export const PLAYER_PLAY_NEXT = 'PLAYER_PLAY_NEXT'
export const PLAYER_SET_TRACK = 'PLAYER_SET_TRACK' export const PLAYER_SET_TRACK = 'PLAYER_SET_TRACK'
export const PLAYER_SYNC_QUEUE = 'PLAYER_SYNC_QUEUE' export const PLAYER_SYNC_QUEUE = 'PLAYER_SYNC_QUEUE'
export const PLAYER_CLEAR_QUEUE = 'PLAYER_CLEAR_QUEUE' export const PLAYER_CLEAR_QUEUE = 'PLAYER_CLEAR_QUEUE'
export const PLAYER_SCROBBLE = 'PLAYER_SCROBBLE'
export const PLAYER_PLAY_TRACKS = 'PLAYER_PLAY_TRACKS' export const PLAYER_PLAY_TRACKS = 'PLAYER_PLAY_TRACKS'
export const PLAYER_CURRENT = 'PLAYER_CURRENT' export const PLAYER_CURRENT = 'PLAYER_CURRENT'
export const PLAYER_SET_VOLUME = 'PLAYER_SET_VOLUME' export const PLAYER_SET_VOLUME = 'PLAYER_SET_VOLUME'
@ -79,12 +78,6 @@ export const clearQueue = () => ({
type: PLAYER_CLEAR_QUEUE, type: PLAYER_CLEAR_QUEUE,
}) })
export const scrobble = (id, submit) => ({
type: PLAYER_SCROBBLE,
id,
submit,
})
export const currentPlaying = (audioInfo) => ({ export const currentPlaying = (audioInfo) => ({
type: PLAYER_CURRENT, type: PLAYER_CURRENT,
data: audioInfo, data: audioInfo,

View file

@ -0,0 +1,41 @@
import React from 'react'
import { useMediaQuery } from '@material-ui/core'
import { Link } from 'react-router-dom'
import clsx from 'clsx'
import { QualityInfo } from '../common'
import useStyle from './styles'
const AudioTitle = React.memo(({ audioInfo, isMobile }) => {
const classes = useStyle()
const className = classes.audioTitle
const isDesktop = useMediaQuery('(min-width:810px)')
if (!audioInfo.song) {
return ''
}
const song = audioInfo.song
const qi = { suffix: song.suffix, bitRate: song.bitRate }
return (
<Link to={`/album/${song.albumId}/show`} className={className}>
<span>
<span className={clsx(classes.songTitle, 'songTitle')}>
{song.title}
</span>
{isDesktop && (
<QualityInfo record={qi} className={classes.qualityInfo} />
)}
</span>
{!isMobile && (
<div className={classes.artistAlbum}>
<span className={clsx(classes.songInfo, 'songInfo')}>
{`${song.artist} - ${song.album}`}
</span>
</div>
)}
</Link>
)
})
export default AudioTitle

View file

@ -1,180 +1,50 @@
import React, { useCallback, useMemo, useState } from 'react' import React, { useCallback, useMemo, useState } from 'react'
import ReactGA from 'react-ga'
import { useDispatch, useSelector } from 'react-redux' import { useDispatch, useSelector } from 'react-redux'
import { Link } from 'react-router-dom' import { createMuiTheme, ThemeProvider } from '@material-ui/core/styles'
import { useMediaQuery } from '@material-ui/core'
import { useAuthState, useDataProvider, useTranslate } from 'react-admin' import { useAuthState, useDataProvider, useTranslate } from 'react-admin'
import ReactGA from 'react-ga'
import { GlobalHotKeys } from 'react-hotkeys'
import ReactJkMusicPlayer from 'react-jinke-music-player' import ReactJkMusicPlayer from 'react-jinke-music-player'
import 'react-jinke-music-player/assets/index.css' import 'react-jinke-music-player/assets/index.css'
import { import useCurrentTheme from '../themes/useCurrentTheme'
createMuiTheme,
makeStyles,
ThemeProvider,
} from '@material-ui/core/styles'
import { useMediaQuery } from '@material-ui/core'
import { GlobalHotKeys } from 'react-hotkeys'
import clsx from 'clsx'
import subsonic from '../subsonic'
import {
scrobble,
syncQueue,
currentPlaying,
setVolume,
clearQueue,
} from '../actions'
import config from '../config' import config from '../config'
import useStyle from './styles'
import AudioTitle from './AudioTitle'
import { clearQueue, currentPlaying, setVolume, syncQueue } from '../actions'
import PlayerToolbar from './PlayerToolbar' import PlayerToolbar from './PlayerToolbar'
import { sendNotification } from '../utils' import { sendNotification } from '../utils'
import subsonic from '../subsonic'
import locale from './locale'
import { keyMap } from '../hotkeys' import { keyMap } from '../hotkeys'
import useCurrentTheme from '../themes/useCurrentTheme' import keyHandlers from './keyHandlers'
import { QualityInfo } from '../common'
const useStyle = makeStyles(
(theme) => ({
audioTitle: {
textDecoration: 'none',
color: theme.palette.primary.dark,
},
songTitle: {
fontWeight: 'bold',
'&:hover + $qualityInfo': {
opacity: 1,
},
},
songInfo: {
display: 'block',
},
qualityInfo: {
marginTop: '-4px',
opacity: 0,
transition: 'all 500ms ease-out',
},
player: {
display: (props) => (props.visible ? 'block' : 'none'),
'@media screen and (max-width:810px)': {
'& .sound-operation': {
display: 'none',
},
},
'& .progress-bar-content': {
display: 'flex',
flexDirection: 'column',
},
'& .play-mode-title': {
'pointer-events': 'none',
},
'& .music-player-panel .panel-content div.img-rotate': {
'animation-duration': (props) =>
props.enableCoverAnimation ? null : '0s',
},
},
artistAlbum: {
marginTop: '2px',
},
}),
{ name: 'NDAudioPlayer' }
)
let audioInstance = null
const AudioTitle = React.memo(({ audioInfo, isMobile }) => {
const classes = useStyle()
const className = classes.audioTitle
const isDesktop = useMediaQuery('(min-width:810px)')
if (!audioInfo.name) {
return ''
}
const qi = { suffix: audioInfo.suffix, bitRate: audioInfo.bitRate }
return (
<Link to={`/album/${audioInfo.albumId}/show`} className={className}>
<span>
<span className={clsx(classes.songTitle, 'songTitle')}>
{audioInfo.name}
</span>
{isDesktop && (
<QualityInfo record={qi} className={classes.qualityInfo} />
)}
</span>
{!isMobile && (
<div className={classes.artistAlbum}>
<span className={clsx(classes.songInfo, 'songInfo')}>
{`${audioInfo.singer} - ${audioInfo.album}`}
</span>
</div>
)}
</Link>
)
})
const Player = () => { const Player = () => {
const translate = useTranslate()
const theme = useCurrentTheme() const theme = useCurrentTheme()
const playerTheme = (theme.player && theme.player.theme) || 'dark' const translate = useTranslate()
const playerTheme = theme.player?.theme || 'dark'
const dataProvider = useDataProvider() const dataProvider = useDataProvider()
const playerState = useSelector((state) => state.player)
const dispatch = useDispatch() const dispatch = useDispatch()
const queue = useSelector((state) => state.queue) const [startTime, setStartTime] = useState(null)
const [scrobbled, setScrobbled] = useState(false)
const [audioInstance, setAudioInstance] = useState(null)
const isDesktop = useMediaQuery('(min-width:810px)')
const { authenticated } = useAuthState() const { authenticated } = useAuthState()
const showNotifications = useSelector( const visible = authenticated && playerState.queue.length > 0
(state) => state.settings.notifications || false
)
const visible = authenticated && queue.queue.length > 0
const classes = useStyle({ const classes = useStyle({
visible, visible,
enableCoverAnimation: config.enableCoverAnimation, enableCoverAnimation: config.enableCoverAnimation,
}) })
// Match the medium breakpoint defined in the material-ui theme const showNotifications = useSelector(
// See https://material-ui.com/customization/breakpoints/#breakpoints (state) => state.settings.notifications || false
const isDesktop = useMediaQuery('(min-width:810px)')
const [startTime, setStartTime] = useState(null)
const nextSong = useCallback(() => {
const idx = queue.queue.findIndex(
(item) => item.uuid === queue.current.uuid
) )
return idx !== null ? queue.queue[idx + 1] : null
}, [queue])
const prevSong = useCallback(() => {
const idx = queue.queue.findIndex(
(item) => item.uuid === queue.current.uuid
)
return idx !== null ? queue.queue[idx - 1] : null
}, [queue])
const keyHandlers = {
TOGGLE_PLAY: (e) => {
e.preventDefault()
audioInstance && audioInstance.togglePlay()
},
VOL_UP: () =>
(audioInstance.volume = Math.min(1, audioInstance.volume + 0.1)),
VOL_DOWN: () =>
(audioInstance.volume = Math.max(0, audioInstance.volume - 0.1)),
PREV_SONG: useCallback(
(e) => {
if (!e.metaKey && prevSong()) audioInstance && audioInstance.playPrev()
},
[prevSong]
),
NEXT_SONG: useCallback(
(e) => {
if (!e.metaKey && nextSong()) audioInstance && audioInstance.playNext()
},
[nextSong]
),
}
const defaultOptions = useMemo( const defaultOptions = useMemo(
() => ({ () => ({
theme: playerTheme, theme: playerTheme,
bounds: 'body', bounds: 'body',
mode: 'full', mode: 'full',
autoPlay: false,
preload: true,
autoPlayInitLoadPlayList: true,
loadAudioErrorPlayNext: false, loadAudioErrorPlayNext: false,
clearPriorAudioLists: false, clearPriorAudioLists: false,
showDestroy: true, showDestroy: true,
@ -185,6 +55,7 @@ const Player = () => {
showThemeSwitch: false, showThemeSwitch: false,
showMediaSession: true, showMediaSession: true,
restartCurrentOnPrev: true, restartCurrentOnPrev: true,
quietUpdate: true,
defaultPosition: { defaultPosition: {
top: 300, top: 300,
left: 120, left: 120,
@ -193,48 +64,22 @@ const Player = () => {
renderAudioTitle: (audioInfo, isMobile) => ( renderAudioTitle: (audioInfo, isMobile) => (
<AudioTitle audioInfo={audioInfo} isMobile={isMobile} /> <AudioTitle audioInfo={audioInfo} isMobile={isMobile} />
), ),
locale: { locale: locale(translate),
playListsText: translate('player.playListsText'),
openText: translate('player.openText'),
closeText: translate('player.closeText'),
notContentText: translate('player.notContentText'),
clickToPlayText: translate('player.clickToPlayText'),
clickToPauseText: translate('player.clickToPauseText'),
nextTrackText: translate('player.nextTrackText'),
previousTrackText: translate('player.previousTrackText'),
reloadText: translate('player.reloadText'),
volumeText: translate('player.volumeText'),
toggleLyricText: translate('player.toggleLyricText'),
toggleMiniModeText: translate('player.toggleMiniModeText'),
destroyText: translate('player.destroyText'),
downloadText: translate('player.downloadText'),
removeAudioListsText: translate('player.removeAudioListsText'),
clickToDeleteText: (name) =>
translate('player.clickToDeleteText', { name }),
emptyLyricText: translate('player.emptyLyricText'),
playModeText: {
order: translate('player.playModeText.order'),
orderLoop: translate('player.playModeText.orderLoop'),
singleLoop: translate('player.playModeText.singleLoop'),
shufflePlay: translate('player.playModeText.shufflePlay'),
},
},
}), }),
[isDesktop, playerTheme, translate] [isDesktop, playerTheme, translate]
) )
const options = useMemo(() => { const options = useMemo(() => {
const current = queue.current || {} const current = playerState.current || {}
return { return {
...defaultOptions, ...defaultOptions,
clearPriorAudioLists: queue.clear, audioLists: playerState.queue.map((item) => item),
autoPlay: queue.clear || queue.playIndex === 0, playIndex: playerState.playIndex,
playIndex: queue.playIndex, clearPriorAudioLists: playerState.clear,
audioLists: queue.queue.map((item) => item),
extendsContent: <PlayerToolbar id={current.trackId} />, extendsContent: <PlayerToolbar id={current.trackId} />,
defaultVolume: queue.volume, defaultVolume: playerState.volume,
} }
}, [queue, defaultOptions]) }, [playerState, defaultOptions])
const onAudioListsChange = useCallback( const onAudioListsChange = useCallback(
(currentPlayIndex, audioLists) => (currentPlayIndex, audioLists) =>
@ -248,19 +93,17 @@ const Player = () => {
document.title = 'Navidrome' document.title = 'Navidrome'
} }
// See https://www.last.fm/api/scrobbling#when-is-a-scrobble-a-scrobble
const progress = (info.currentTime / info.duration) * 100 const progress = (info.currentTime / info.duration) * 100
if (isNaN(info.duration) || (progress < 50 && info.currentTime < 240)) { if (isNaN(info.duration) || (progress < 50 && info.currentTime < 240)) {
return return
} }
const item = queue.queue.find((item) => item.trackId === info.trackId) if (!scrobbled) {
if (item && !item.scrobbled) {
dispatch(scrobble(info.trackId, true))
subsonic.scrobble(info.trackId, true, startTime) subsonic.scrobble(info.trackId, true, startTime)
setScrobbled(true)
} }
}, },
[dispatch, queue.queue, startTime] [startTime, scrobbled]
) )
const onAudioVolumeChange = useCallback( const onAudioVolumeChange = useCallback(
@ -274,20 +117,21 @@ const Player = () => {
dispatch(currentPlaying(info)) dispatch(currentPlaying(info))
setStartTime(Date.now()) setStartTime(Date.now())
if (info.duration) { if (info.duration) {
document.title = `${info.name} - ${info.singer} - Navidrome` const song = info.song
dispatch(scrobble(info.trackId, false)) document.title = `${song.title} - ${song.artist} - Navidrome`
subsonic.nowPlaying(info.trackId) subsonic.nowPlaying(info.trackId)
setScrobbled(false)
if (config.gaTrackingId) { if (config.gaTrackingId) {
ReactGA.event({ ReactGA.event({
category: 'Player', category: 'Player',
action: 'Play song', action: 'Play song',
label: `${info.name} - ${info.singer}`, label: `${song.title} - ${song.artist}`,
}) })
} }
if (showNotifications) { if (showNotifications) {
sendNotification( sendNotification(
info.name, song.title,
`${info.singer} - ${info.album}`, `${song.artist} - ${song.album}`,
info.cover info.cover
) )
} }
@ -312,8 +156,8 @@ const Player = () => {
) )
const onCoverClick = useCallback((mode, audioLists, audioInfo) => { const onCoverClick = useCallback((mode, audioLists, audioInfo) => {
if (mode === 'full') { if (mode === 'full' && audioInfo?.song?.albumId) {
window.location.href = `#/album/${audioInfo.albumId}/show` window.location.href = `#/album/${audioInfo.song.albumId}/show`
} }
}, []) }, [])
@ -328,25 +172,27 @@ const Player = () => {
document.title = 'Navidrome' document.title = 'Navidrome'
} }
const handlers = useMemo(
() => keyHandlers(audioInstance, playerState),
[audioInstance, playerState]
)
return ( return (
<ThemeProvider theme={createMuiTheme(theme)}> <ThemeProvider theme={createMuiTheme(theme)}>
<ReactJkMusicPlayer <ReactJkMusicPlayer
{...options} {...options}
quietUpdate
className={classes.player} className={classes.player}
onAudioListsChange={onAudioListsChange} onAudioListsChange={onAudioListsChange}
onAudioVolumeChange={onAudioVolumeChange}
onAudioProgress={onAudioProgress} onAudioProgress={onAudioProgress}
onAudioPlay={onAudioPlay} onAudioPlay={onAudioPlay}
onAudioPause={onAudioPause} onAudioPause={onAudioPause}
onAudioEnded={onAudioEnded} onAudioEnded={onAudioEnded}
onAudioVolumeChange={onAudioVolumeChange}
onCoverClick={onCoverClick} onCoverClick={onCoverClick}
onBeforeDestroy={onBeforeDestroy} onBeforeDestroy={onBeforeDestroy}
getAudioInstance={(instance) => { getAudioInstance={setAudioInstance}
audioInstance = instance
}}
/> />
<GlobalHotKeys handlers={keyHandlers} keyMap={keyMap} allowChanges /> <GlobalHotKeys handlers={handlers} keyMap={keyMap} allowChanges />
</ThemeProvider> </ThemeProvider>
) )
} }

View file

@ -0,0 +1,35 @@
const keyHandlers = (audioInstance, playerState) => {
const nextSong = () => {
const idx = playerState.queue.findIndex(
(item) => item.uuid === playerState.current.uuid
)
return idx !== null ? playerState.queue[idx + 1] : null
}
const prevSong = () => {
const idx = playerState.queue.findIndex(
(item) => item.uuid === playerState.current.uuid
)
return idx !== null ? playerState.queue[idx - 1] : null
}
return {
TOGGLE_PLAY: (e) => {
e.preventDefault()
audioInstance && audioInstance.togglePlay()
},
VOL_UP: () =>
(audioInstance.volume = Math.min(1, audioInstance.volume + 0.1)),
VOL_DOWN: () =>
(audioInstance.volume = Math.max(0, audioInstance.volume - 0.1)),
PREV_SONG: (e) => {
if (!e.metaKey && prevSong()) audioInstance && audioInstance.playPrev()
},
NEXT_SONG: (e) => {
if (!e.metaKey && nextSong()) audioInstance && audioInstance.playNext()
},
}
}
export default keyHandlers

View file

@ -0,0 +1,27 @@
const locale = (translate) => ({
playListsText: translate('player.playListsText'),
openText: translate('player.openText'),
closeText: translate('player.closeText'),
notContentText: translate('player.notContentText'),
clickToPlayText: translate('player.clickToPlayText'),
clickToPauseText: translate('player.clickToPauseText'),
nextTrackText: translate('player.nextTrackText'),
previousTrackText: translate('player.previousTrackText'),
reloadText: translate('player.reloadText'),
volumeText: translate('player.volumeText'),
toggleLyricText: translate('player.toggleLyricText'),
toggleMiniModeText: translate('player.toggleMiniModeText'),
destroyText: translate('player.destroyText'),
downloadText: translate('player.downloadText'),
removeAudioListsText: translate('player.removeAudioListsText'),
clickToDeleteText: (name) => translate('player.clickToDeleteText', { name }),
emptyLyricText: translate('player.emptyLyricText'),
playModeText: {
order: translate('player.playModeText.order'),
orderLoop: translate('player.playModeText.orderLoop'),
singleLoop: translate('player.playModeText.singleLoop'),
shufflePlay: translate('player.playModeText.shufflePlay'),
},
})
export default locale

View file

@ -0,0 +1,49 @@
import { makeStyles } from '@material-ui/core/styles'
const useStyle = makeStyles(
(theme) => ({
audioTitle: {
textDecoration: 'none',
color: theme.palette.primary.dark,
},
songTitle: {
fontWeight: 'bold',
'&:hover + $qualityInfo': {
opacity: 1,
},
},
songInfo: {
display: 'block',
},
qualityInfo: {
marginTop: '-4px',
opacity: 0,
transition: 'all 500ms ease-out',
},
player: {
display: (props) => (props.visible ? 'block' : 'none'),
'@media screen and (max-width:810px)': {
'& .sound-operation': {
display: 'none',
},
},
'& .progress-bar-content': {
display: 'flex',
flexDirection: 'column',
},
'& .play-mode-title': {
'pointer-events': 'none',
},
'& .music-player-panel .panel-content div.img-rotate': {
'animation-duration': (props) =>
props.enableCoverAnimation ? null : '0s',
},
},
artistAlbum: {
marginTop: '2px',
},
}),
{ name: 'NDAudioPlayer' }
)
export default useStyle

View file

@ -27,7 +27,7 @@ export const SongTitleField = ({ showTrackNumbers, ...props }) => {
const theme = useTheme() const theme = useTheme()
const classes = useStyles() const classes = useStyles()
const { record } = props const { record } = props
const currentTrack = useSelector((state) => state?.queue?.current || {}) const currentTrack = useSelector((state) => state?.player?.current || {})
const currentId = currentTrack.trackId const currentId = currentTrack.trackId
const paused = currentTrack.paused const paused = currentTrack.paused
const isCurrent = const isCurrent =

View file

@ -14,8 +14,8 @@ const useStyles = makeStyles({
const Layout = (props) => { const Layout = (props) => {
const theme = useCurrentTheme() const theme = useCurrentTheme()
const queue = useSelector((state) => state.queue) const queue = useSelector((state) => state.player?.queue)
const classes = useStyles({ addPadding: queue.queue.length > 0 }) const classes = useStyles({ addPadding: queue.length > 0 })
const dispatch = useDispatch() const dispatch = useDispatch()
const keyHandlers = { const keyHandlers = {

View file

@ -1,6 +1,6 @@
export * from './themeReducer' export * from './themeReducer'
export * from './dialogReducer' export * from './dialogReducer'
export * from './playQueue' export * from './playerReducer'
export * from './albumView' export * from './albumView'
export * from './activityReducer' export * from './activityReducer'
export * from './settingsReducer' export * from './settingsReducer'

View file

@ -1,159 +0,0 @@
import { v4 as uuidv4 } from 'uuid'
import subsonic from '../subsonic'
import config from '../config'
import {
PLAYER_CLEAR_QUEUE,
PLAYER_SET_VOLUME,
PLAYER_CURRENT,
PLAYER_ADD_TRACKS,
PLAYER_PLAY_NEXT,
PLAYER_SET_TRACK,
PLAYER_SYNC_QUEUE,
PLAYER_SCROBBLE,
PLAYER_PLAY_TRACKS,
} from '../actions'
const mapToAudioLists = (item) => {
// If item comes from a playlist, id is mediaFileId
const id = item.mediaFileId || item.id
return {
trackId: id,
name: item.title,
singer: item.artist,
album: item.album,
albumId: item.albumId,
artistId: item.albumArtistId,
duration: item.duration,
suffix: item.suffix,
bitRate: item.bitRate,
musicSrc: subsonic.streamUrl(id),
cover: subsonic.getCoverArtUrl(
{
coverArtId: config.devFastAccessCoverArt ? item.albumId : id,
updatedAt: item.updatedAt,
},
300
),
scrobbled: false,
uuid: uuidv4(),
}
}
const initialState = {
queue: [],
clear: true,
current: {},
volume: 1,
playIndex: 0,
}
export const playQueueReducer = (previousState = initialState, payload) => {
let queue, current
let newQueue
const { type, data } = payload
switch (type) {
case PLAYER_CLEAR_QUEUE:
return initialState
case PLAYER_SET_VOLUME:
return {
...previousState,
playIndex: undefined,
volume: data.volume,
}
case PLAYER_CURRENT:
queue = previousState.queue
current = data.ended
? {}
: {
trackId: data.trackId,
uuid: data.uuid,
paused: data.paused,
}
return {
...previousState,
current,
playIndex: undefined,
volume: data.volume,
}
case PLAYER_ADD_TRACKS:
queue = previousState.queue
Object.keys(data).forEach((id) => {
queue.push(mapToAudioLists(data[id]))
})
return { ...previousState, queue, clear: false, playIndex: undefined }
case PLAYER_PLAY_NEXT:
current = previousState.current?.uuid || ''
newQueue = []
let foundPos = false
previousState.queue.forEach((item) => {
newQueue.push(item)
if (item.uuid === current) {
foundPos = true
Object.keys(data).forEach((id) => {
newQueue.push(mapToAudioLists(data[id]))
})
}
})
if (!foundPos) {
Object.keys(data).forEach((id) => {
newQueue.push(mapToAudioLists(data[id]))
})
}
return {
...previousState,
queue: newQueue,
clear: true,
playIndex: undefined,
}
case PLAYER_SET_TRACK:
return {
...previousState,
queue: [mapToAudioLists(data)],
clear: true,
playIndex: 0,
}
case PLAYER_SYNC_QUEUE:
current = data.length > 0 ? previousState.current : {}
return {
...previousState,
queue: data,
clear: false,
playIndex: undefined,
current,
}
case PLAYER_SCROBBLE:
newQueue = previousState.queue.map((item) => {
return {
...item,
scrobbled:
item.scrobbled || (item.trackId === payload.id && payload.submit),
}
})
return {
...previousState,
queue: newQueue,
playIndex: undefined,
clear: false,
}
case PLAYER_PLAY_TRACKS:
queue = []
let match = false
Object.keys(data).forEach((id) => {
if (id === payload.id) {
match = true
}
if (match) {
queue.push(mapToAudioLists(data[id]))
}
})
return {
...previousState,
queue,
playIndex: 0,
clear: true,
}
default:
return previousState
}
}

View file

@ -0,0 +1,165 @@
import { v4 as uuidv4 } from 'uuid'
import subsonic from '../subsonic'
import {
PLAYER_ADD_TRACKS,
PLAYER_CLEAR_QUEUE,
PLAYER_CURRENT,
PLAYER_PLAY_NEXT,
PLAYER_PLAY_TRACKS,
PLAYER_SET_TRACK,
PLAYER_SET_VOLUME,
PLAYER_SYNC_QUEUE,
} from '../actions'
import config from '../config'
const initialState = {
queue: [],
current: {},
clear: false,
volume: 1,
}
const mapToAudioLists = (item) => {
// If item comes from a playlist, trackId is mediaFileId
const trackId = item.mediaFileId || item.id
return {
trackId,
uuid: uuidv4(),
song: item,
name: item.title,
singer: item.artist,
duration: item.duration,
musicSrc: subsonic.streamUrl(trackId),
cover: subsonic.getCoverArtUrl(
{
coverArtId: config.devFastAccessCoverArt ? item.albumId : trackId,
updatedAt: item.updatedAt,
},
300
),
}
}
const reduceClearQueue = () => ({ ...initialState, clear: true })
const reducePlayTracks = (state, { data, id }) => {
let playIndex = 0
const queue = Object.keys(data).map((key, idx) => {
if (key === id) {
playIndex = idx
}
return mapToAudioLists(data[key])
})
return {
...state,
queue,
playIndex,
clear: true,
}
}
const reduceSyncQueue = (state, { data }) => {
const current = data.length > 0 ? state.current : {}
return {
...state,
current,
queue: data,
}
}
const reduceSetTrack = (state, { data }) => {
return {
...state,
queue: [mapToAudioLists(0, data)],
playIndex: 0,
clear: true,
}
}
const reduceAddTracks = (state, { data }) => {
const queue = state.queue
Object.keys(data).forEach((id) => {
queue.push(mapToAudioLists(data[id]))
})
return { ...state, queue, clear: false }
}
const reducePlayNext = (state, { data }) => {
const newQueue = []
const current = state.current || {}
let foundPos = false
state.queue.forEach((item) => {
newQueue.push(item)
if (item.uuid === current.uuid) {
foundPos = true
Object.keys(data).forEach((id) => {
newQueue.push(mapToAudioLists(data[id]))
})
}
})
if (!foundPos) {
Object.keys(data).forEach((id) => {
newQueue.push(mapToAudioLists(data[id]))
})
}
const playIndex = state.queue.findIndex((item) => item.uuid === current.uuid)
return {
...state,
queue: newQueue,
// TODO: This is a workaround for a bug in the player that resets the playIndex to 0 when the current playing
// song is not the first one. It is still not great, as it resets the current playing song
playIndex,
clear: true,
}
}
const reduceSetVolume = (state, { data: { volume } }) => {
return {
...state,
volume,
}
}
const reduceCurrent = (state, { data }) => {
const current = data.ended
? {}
: {
idx: data.idx,
trackId: data.trackId,
paused: data.paused,
uuid: data.uuid,
song: data.song,
}
const playIndex = state.queue.findIndex((item) => item.uuid === current.uuid)
return {
...state,
current,
playIndex: playIndex > -1 ? playIndex : undefined,
volume: data.volume,
}
}
export const playerReducer = (previousState = initialState, payload) => {
const { type } = payload
switch (type) {
case PLAYER_CLEAR_QUEUE:
return reduceClearQueue()
case PLAYER_PLAY_TRACKS:
return reducePlayTracks(previousState, payload)
case PLAYER_SET_TRACK:
return reduceSetTrack(previousState, payload)
case PLAYER_ADD_TRACKS:
return reduceAddTracks(previousState, payload)
case PLAYER_PLAY_NEXT:
return reducePlayNext(previousState, payload)
case PLAYER_SYNC_QUEUE:
return reduceSyncQueue(previousState, payload)
case PLAYER_SET_VOLUME:
return reduceSetVolume(previousState, payload)
case PLAYER_CURRENT:
return reduceCurrent(previousState, payload)
default:
return previousState
}
}

View file

@ -48,7 +48,7 @@ const createAdminStore = ({
const state = store.getState() const state = store.getState()
saveState({ saveState({
theme: state.theme, theme: state.theme,
queue: pick(state.queue, ['queue', 'volume']), player: pick(state.player, ['queue', 'volume', 'playIndex']),
albumView: state.albumView, albumView: state.albumView,
settings: state.settings, settings: state.settings,
}) })