diff --git a/ui/src/App.js b/ui/src/App.js index 370e12412..00f2e05c0 100644 --- a/ui/src/App.js +++ b/ui/src/App.js @@ -19,7 +19,7 @@ import customRoutes from './routes' import { themeReducer, addToPlaylistDialogReducer, - playQueueReducer, + playerReducer, albumViewReducer, activityReducer, settingsReducer, @@ -48,7 +48,7 @@ const App = () => ( dataProvider, history, customReducers: { - queue: playQueueReducer, + player: playerReducer, albumView: albumViewReducer, theme: themeReducer, addToPlaylistDialog: addToPlaylistDialogReducer, diff --git a/ui/src/actions/index.js b/ui/src/actions/index.js index f087f97a9..206ac0fb3 100644 --- a/ui/src/actions/index.js +++ b/ui/src/actions/index.js @@ -1,4 +1,4 @@ -export * from './audioplayer' +export * from './player' export * from './themes' export * from './albumView' export * from './dialogs' diff --git a/ui/src/actions/audioplayer.js b/ui/src/actions/player.js similarity index 94% rename from ui/src/actions/audioplayer.js rename to ui/src/actions/player.js index 7f18738a4..798f32ae1 100644 --- a/ui/src/actions/audioplayer.js +++ b/ui/src/actions/player.js @@ -3,7 +3,6 @@ export const PLAYER_PLAY_NEXT = 'PLAYER_PLAY_NEXT' export const PLAYER_SET_TRACK = 'PLAYER_SET_TRACK' export const PLAYER_SYNC_QUEUE = 'PLAYER_SYNC_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_CURRENT = 'PLAYER_CURRENT' export const PLAYER_SET_VOLUME = 'PLAYER_SET_VOLUME' @@ -79,12 +78,6 @@ export const clearQueue = () => ({ type: PLAYER_CLEAR_QUEUE, }) -export const scrobble = (id, submit) => ({ - type: PLAYER_SCROBBLE, - id, - submit, -}) - export const currentPlaying = (audioInfo) => ({ type: PLAYER_CURRENT, data: audioInfo, diff --git a/ui/src/audioplayer/AudioTitle.js b/ui/src/audioplayer/AudioTitle.js new file mode 100644 index 000000000..878605196 --- /dev/null +++ b/ui/src/audioplayer/AudioTitle.js @@ -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 ( + + + + {song.title} + + {isDesktop && ( + + )} + + {!isMobile && ( +
+ + {`${song.artist} - ${song.album}`} + +
+ )} + + ) +}) + +export default AudioTitle diff --git a/ui/src/audioplayer/Player.js b/ui/src/audioplayer/Player.js index 01d4c3808..0d716dac9 100644 --- a/ui/src/audioplayer/Player.js +++ b/ui/src/audioplayer/Player.js @@ -1,180 +1,50 @@ import React, { useCallback, useMemo, useState } from 'react' -import ReactGA from 'react-ga' 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 ReactGA from 'react-ga' +import { GlobalHotKeys } from 'react-hotkeys' import ReactJkMusicPlayer from 'react-jinke-music-player' import 'react-jinke-music-player/assets/index.css' -import { - 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 useCurrentTheme from '../themes/useCurrentTheme' import config from '../config' +import useStyle from './styles' +import AudioTitle from './AudioTitle' +import { clearQueue, currentPlaying, setVolume, syncQueue } from '../actions' import PlayerToolbar from './PlayerToolbar' import { sendNotification } from '../utils' +import subsonic from '../subsonic' +import locale from './locale' import { keyMap } from '../hotkeys' -import useCurrentTheme from '../themes/useCurrentTheme' -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 ( - - - - {audioInfo.name} - - {isDesktop && ( - - )} - - {!isMobile && ( -
- - {`${audioInfo.singer} - ${audioInfo.album}`} - -
- )} - - ) -}) +import keyHandlers from './keyHandlers' const Player = () => { - const translate = useTranslate() const theme = useCurrentTheme() - const playerTheme = (theme.player && theme.player.theme) || 'dark' + const translate = useTranslate() + const playerTheme = theme.player?.theme || 'dark' const dataProvider = useDataProvider() + const playerState = useSelector((state) => state.player) 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 showNotifications = useSelector( - (state) => state.settings.notifications || false - ) - - const visible = authenticated && queue.queue.length > 0 + const visible = authenticated && playerState.queue.length > 0 const classes = useStyle({ visible, enableCoverAnimation: config.enableCoverAnimation, }) - // Match the medium breakpoint defined in the material-ui theme - // See https://material-ui.com/customization/breakpoints/#breakpoints - 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 showNotifications = useSelector( + (state) => state.settings.notifications || false + ) const defaultOptions = useMemo( () => ({ theme: playerTheme, bounds: 'body', mode: 'full', - autoPlay: false, - preload: true, - autoPlayInitLoadPlayList: true, loadAudioErrorPlayNext: false, clearPriorAudioLists: false, showDestroy: true, @@ -185,6 +55,7 @@ const Player = () => { showThemeSwitch: false, showMediaSession: true, restartCurrentOnPrev: true, + quietUpdate: true, defaultPosition: { top: 300, left: 120, @@ -193,48 +64,22 @@ const Player = () => { renderAudioTitle: (audioInfo, isMobile) => ( ), - locale: { - 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'), - }, - }, + locale: locale(translate), }), [isDesktop, playerTheme, translate] ) const options = useMemo(() => { - const current = queue.current || {} + const current = playerState.current || {} return { ...defaultOptions, - clearPriorAudioLists: queue.clear, - autoPlay: queue.clear || queue.playIndex === 0, - playIndex: queue.playIndex, - audioLists: queue.queue.map((item) => item), + audioLists: playerState.queue.map((item) => item), + playIndex: playerState.playIndex, + clearPriorAudioLists: playerState.clear, extendsContent: , - defaultVolume: queue.volume, + defaultVolume: playerState.volume, } - }, [queue, defaultOptions]) + }, [playerState, defaultOptions]) const onAudioListsChange = useCallback( (currentPlayIndex, audioLists) => @@ -248,19 +93,17 @@ const Player = () => { document.title = 'Navidrome' } - // See https://www.last.fm/api/scrobbling#when-is-a-scrobble-a-scrobble const progress = (info.currentTime / info.duration) * 100 if (isNaN(info.duration) || (progress < 50 && info.currentTime < 240)) { return } - const item = queue.queue.find((item) => item.trackId === info.trackId) - if (item && !item.scrobbled) { - dispatch(scrobble(info.trackId, true)) + if (!scrobbled) { subsonic.scrobble(info.trackId, true, startTime) + setScrobbled(true) } }, - [dispatch, queue.queue, startTime] + [startTime, scrobbled] ) const onAudioVolumeChange = useCallback( @@ -274,20 +117,21 @@ const Player = () => { dispatch(currentPlaying(info)) setStartTime(Date.now()) if (info.duration) { - document.title = `${info.name} - ${info.singer} - Navidrome` - dispatch(scrobble(info.trackId, false)) + const song = info.song + document.title = `${song.title} - ${song.artist} - Navidrome` subsonic.nowPlaying(info.trackId) + setScrobbled(false) if (config.gaTrackingId) { ReactGA.event({ category: 'Player', action: 'Play song', - label: `${info.name} - ${info.singer}`, + label: `${song.title} - ${song.artist}`, }) } if (showNotifications) { sendNotification( - info.name, - `${info.singer} - ${info.album}`, + song.title, + `${song.artist} - ${song.album}`, info.cover ) } @@ -312,8 +156,8 @@ const Player = () => { ) const onCoverClick = useCallback((mode, audioLists, audioInfo) => { - if (mode === 'full') { - window.location.href = `#/album/${audioInfo.albumId}/show` + if (mode === 'full' && audioInfo?.song?.albumId) { + window.location.href = `#/album/${audioInfo.song.albumId}/show` } }, []) @@ -328,25 +172,27 @@ const Player = () => { document.title = 'Navidrome' } + const handlers = useMemo( + () => keyHandlers(audioInstance, playerState), + [audioInstance, playerState] + ) + return ( { - audioInstance = instance - }} + getAudioInstance={setAudioInstance} /> - + ) } diff --git a/ui/src/audioplayer/keyHandlers.js b/ui/src/audioplayer/keyHandlers.js new file mode 100644 index 000000000..3f697a32f --- /dev/null +++ b/ui/src/audioplayer/keyHandlers.js @@ -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 diff --git a/ui/src/audioplayer/locale.js b/ui/src/audioplayer/locale.js new file mode 100644 index 000000000..4cc052abf --- /dev/null +++ b/ui/src/audioplayer/locale.js @@ -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 diff --git a/ui/src/audioplayer/styles.js b/ui/src/audioplayer/styles.js new file mode 100644 index 000000000..642a8d785 --- /dev/null +++ b/ui/src/audioplayer/styles.js @@ -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 diff --git a/ui/src/common/SongTitleField.js b/ui/src/common/SongTitleField.js index 438bed606..21ceed601 100644 --- a/ui/src/common/SongTitleField.js +++ b/ui/src/common/SongTitleField.js @@ -27,7 +27,7 @@ export const SongTitleField = ({ showTrackNumbers, ...props }) => { const theme = useTheme() const classes = useStyles() const { record } = props - const currentTrack = useSelector((state) => state?.queue?.current || {}) + const currentTrack = useSelector((state) => state?.player?.current || {}) const currentId = currentTrack.trackId const paused = currentTrack.paused const isCurrent = diff --git a/ui/src/layout/Layout.js b/ui/src/layout/Layout.js index 5a9e25d1b..e3f13d25f 100644 --- a/ui/src/layout/Layout.js +++ b/ui/src/layout/Layout.js @@ -14,8 +14,8 @@ const useStyles = makeStyles({ const Layout = (props) => { const theme = useCurrentTheme() - const queue = useSelector((state) => state.queue) - const classes = useStyles({ addPadding: queue.queue.length > 0 }) + const queue = useSelector((state) => state.player?.queue) + const classes = useStyles({ addPadding: queue.length > 0 }) const dispatch = useDispatch() const keyHandlers = { diff --git a/ui/src/reducers/index.js b/ui/src/reducers/index.js index 0abc119b4..4e3f5dd06 100644 --- a/ui/src/reducers/index.js +++ b/ui/src/reducers/index.js @@ -1,6 +1,6 @@ export * from './themeReducer' export * from './dialogReducer' -export * from './playQueue' +export * from './playerReducer' export * from './albumView' export * from './activityReducer' export * from './settingsReducer' diff --git a/ui/src/reducers/playQueue.js b/ui/src/reducers/playQueue.js deleted file mode 100644 index 06d855503..000000000 --- a/ui/src/reducers/playQueue.js +++ /dev/null @@ -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 - } -} diff --git a/ui/src/reducers/playerReducer.js b/ui/src/reducers/playerReducer.js new file mode 100644 index 000000000..34db1c784 --- /dev/null +++ b/ui/src/reducers/playerReducer.js @@ -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 + } +} diff --git a/ui/src/store/createAdminStore.js b/ui/src/store/createAdminStore.js index e3ea1ce8f..4a6c152fb 100644 --- a/ui/src/store/createAdminStore.js +++ b/ui/src/store/createAdminStore.js @@ -48,7 +48,7 @@ const createAdminStore = ({ const state = store.getState() saveState({ theme: state.theme, - queue: pick(state.queue, ['queue', 'volume']), + player: pick(state.player, ['queue', 'volume', 'playIndex']), albumView: state.albumView, settings: state.settings, })