298 lines
8.4 KiB
TypeScript
298 lines
8.4 KiB
TypeScript
import { Store, Module } from 'vuex'
|
|
import { shuffle, trackListEquals } from '@/shared/utils'
|
|
import { API } from '@/shared/api'
|
|
|
|
const audio = new Audio()
|
|
const storedQueue = JSON.parse(localStorage.getItem('queue') || '[]')
|
|
const storedQueueIndex = parseInt(localStorage.getItem('queueIndex') || '-1')
|
|
if (storedQueueIndex > -1 && storedQueueIndex < storedQueue.length) {
|
|
audio.src = storedQueue[storedQueueIndex].url
|
|
}
|
|
const storedVolume = parseFloat(localStorage.getItem('player.volume') || '1.0')
|
|
const storedMuteState = localStorage.getItem('player.mute') === 'true'
|
|
audio.volume = storedMuteState ? 0.0 : storedVolume
|
|
const mediaSession: MediaSession | undefined = navigator.mediaSession
|
|
|
|
interface State {
|
|
queue: any[];
|
|
queueIndex: number;
|
|
scrobbled: boolean;
|
|
isPlaying: boolean;
|
|
duration: number; // duration of current track in seconds
|
|
currentTime: number; // position of current track in seconds
|
|
repeat: boolean;
|
|
shuffle: boolean;
|
|
mute: boolean;
|
|
volume: number; // integer between 0 and 1 representing the volume of the player
|
|
}
|
|
|
|
export const playerModule: Module<State, any> = {
|
|
namespaced: true,
|
|
state: {
|
|
queue: storedQueue,
|
|
queueIndex: storedQueueIndex,
|
|
scrobbled: false,
|
|
isPlaying: false,
|
|
duration: 0,
|
|
currentTime: 0,
|
|
repeat: localStorage.getItem('player.repeat') !== 'false',
|
|
shuffle: localStorage.getItem('player.shuffle') === 'true',
|
|
mute: storedMuteState,
|
|
volume: storedVolume,
|
|
},
|
|
|
|
mutations: {
|
|
setPlaying(state) {
|
|
state.isPlaying = true
|
|
if (mediaSession) {
|
|
mediaSession.playbackState = 'playing'
|
|
}
|
|
},
|
|
setPaused(state) {
|
|
state.isPlaying = false
|
|
if (mediaSession) {
|
|
mediaSession.playbackState = 'paused'
|
|
}
|
|
},
|
|
setRepeat(state, enable) {
|
|
state.repeat = enable
|
|
localStorage.setItem('player.repeat', enable)
|
|
},
|
|
setShuffle(state, enable) {
|
|
state.shuffle = enable
|
|
localStorage.setItem('player.shuffle', enable)
|
|
},
|
|
setMute(state, enable) {
|
|
state.mute = enable
|
|
localStorage.setItem('player.mute', enable)
|
|
},
|
|
setQueue(state, queue) {
|
|
state.queue = queue
|
|
state.queueIndex = -1
|
|
localStorage.setItem('queue', JSON.stringify(queue))
|
|
},
|
|
setQueueIndex(state, index) {
|
|
if (state.queue.length === 0) {
|
|
return
|
|
}
|
|
index = Math.max(0, index)
|
|
index = index < state.queue.length ? index : 0
|
|
state.queueIndex = index
|
|
localStorage.setItem('queueIndex', index)
|
|
state.scrobbled = false
|
|
const track = state.queue[index]
|
|
audio.src = track.url
|
|
document.title = `${track.title} • ${track.artist}`
|
|
if (mediaSession) {
|
|
mediaSession.metadata = new MediaMetadata({
|
|
title: track.title,
|
|
artist: track.artist,
|
|
album: track.album,
|
|
artwork: track.image ? [{ src: track.image, sizes: '300x300' }] : undefined,
|
|
})
|
|
}
|
|
},
|
|
addToQueue(state, track) {
|
|
state.queue.push(track)
|
|
},
|
|
removeFromQueue(state, index) {
|
|
state.queue.splice(index, 1)
|
|
if (index < state.queueIndex) {
|
|
state.queueIndex--
|
|
}
|
|
},
|
|
setNextInQueue(state, track) {
|
|
state.queue.splice(state.queueIndex + 1, 0, track)
|
|
},
|
|
setCurrentTime(state, value: any) {
|
|
state.currentTime = value
|
|
},
|
|
setDuration(state, value: any) {
|
|
state.duration = value
|
|
},
|
|
setScrobbled(state) {
|
|
state.scrobbled = true
|
|
},
|
|
setVolume(state, value: number) {
|
|
state.volume = value
|
|
state.mute = value <= 0.0
|
|
localStorage.setItem('player.volume', String(value))
|
|
},
|
|
},
|
|
|
|
actions: {
|
|
async playTrackList({ commit, state }, { tracks, index }) {
|
|
if (trackListEquals(state.queue, tracks)) {
|
|
commit('setQueueIndex', index)
|
|
commit('setPlaying')
|
|
await audio.play()
|
|
return
|
|
}
|
|
tracks = [...tracks]
|
|
if (state.shuffle) {
|
|
const selected = tracks[index]
|
|
tracks.splice(index, 1)
|
|
tracks = [selected, ...shuffle(tracks)]
|
|
commit('setQueue', tracks)
|
|
commit('setQueueIndex', 0)
|
|
} else {
|
|
commit('setQueue', tracks)
|
|
commit('setQueueIndex', index)
|
|
}
|
|
commit('setPlaying')
|
|
await audio.play()
|
|
},
|
|
async resume({ commit }) {
|
|
commit('setPlaying')
|
|
await audio.play()
|
|
},
|
|
async pause({ commit }) {
|
|
audio.pause()
|
|
commit('setPaused')
|
|
},
|
|
async playPause({ state, dispatch }) {
|
|
if (state.isPlaying) {
|
|
return dispatch('pause')
|
|
}
|
|
return dispatch('resume')
|
|
},
|
|
async next({ commit, state, getters, dispatch }) {
|
|
if (!state.repeat && !getters.hasNext) {
|
|
return dispatch('resetQueue')
|
|
}
|
|
commit('setQueueIndex', state.queueIndex + 1)
|
|
commit('setPlaying')
|
|
await audio.play()
|
|
},
|
|
async previous({ commit, state }) {
|
|
commit('setQueueIndex',
|
|
audio.currentTime > 3 ? state.queueIndex : state.queueIndex - 1)
|
|
commit('setPlaying')
|
|
await audio.play()
|
|
},
|
|
seek({ state }, value) {
|
|
if (isFinite(state.duration)) {
|
|
audio.currentTime = state.duration * value
|
|
}
|
|
},
|
|
resetQueue({ commit }) {
|
|
audio.pause()
|
|
commit('setQueueIndex', 0)
|
|
commit('setPaused')
|
|
},
|
|
toggleRepeat({ commit, state }) {
|
|
commit('setRepeat', !state.repeat)
|
|
},
|
|
toggleShuffle({ commit, state }) {
|
|
commit('setShuffle', !state.shuffle)
|
|
},
|
|
toggleMute({ commit, state }) {
|
|
commit('setMute', !state.mute)
|
|
audio.volume = state.mute ? 0.0 : state.volume
|
|
},
|
|
addToQueue({ commit }, track) {
|
|
commit('addToQueue', track)
|
|
},
|
|
setNextInQueue({ commit }, track) {
|
|
commit('setNextInQueue', track)
|
|
},
|
|
setVolume({ commit }, value) {
|
|
audio.volume = value
|
|
commit('setVolume', value)
|
|
},
|
|
},
|
|
|
|
getters: {
|
|
track(state) {
|
|
if (state.queueIndex !== -1) {
|
|
return state.queue[state.queueIndex]
|
|
}
|
|
return null
|
|
},
|
|
trackId(state, getters): number {
|
|
return getters.track ? getters.track.id : -1
|
|
},
|
|
isPlaying(state): boolean {
|
|
return state.isPlaying
|
|
},
|
|
progress(state) {
|
|
if (state.currentTime > -1 && state.duration > 0) {
|
|
return (state.currentTime / state.duration) * 100
|
|
}
|
|
return 0
|
|
},
|
|
hasNext(state) {
|
|
return state.queueIndex < state.queue.length - 1
|
|
},
|
|
hasPrevious(state) {
|
|
return state.queueIndex > 0
|
|
},
|
|
},
|
|
}
|
|
|
|
export function setupAudio(store: Store<any>, api: API) {
|
|
audio.ontimeupdate = () => {
|
|
store.commit('player/setCurrentTime', audio.currentTime)
|
|
|
|
// Scrobble
|
|
if (store.state.player.scrobbled === false &&
|
|
audio.duration > 30 &&
|
|
audio.currentTime / audio.duration > 0.7) {
|
|
const id = store.getters['player/trackId']
|
|
store.commit('player/setScrobbled')
|
|
api.scrobble(id)
|
|
}
|
|
}
|
|
audio.ondurationchange = () => {
|
|
store.commit('player/setDuration', audio.duration)
|
|
}
|
|
audio.onerror = () => {
|
|
store.commit('player/setPaused')
|
|
store.commit('setError', audio.error)
|
|
}
|
|
audio.onended = () => {
|
|
store.dispatch('player/next')
|
|
}
|
|
|
|
if (mediaSession) {
|
|
mediaSession.setActionHandler('play', () => {
|
|
store.dispatch('player/resume')
|
|
})
|
|
mediaSession.setActionHandler('pause', () => {
|
|
store.dispatch('player/pause')
|
|
})
|
|
mediaSession.setActionHandler('nexttrack', () => {
|
|
store.dispatch('player/next')
|
|
})
|
|
mediaSession.setActionHandler('previoustrack', () => {
|
|
store.dispatch('player/previous')
|
|
})
|
|
mediaSession.setActionHandler('stop', () => {
|
|
store.dispatch('player/pause')
|
|
})
|
|
mediaSession.setActionHandler('seekto', (details) => {
|
|
if (details.seekTime) {
|
|
audio.currentTime = details.seekTime
|
|
}
|
|
})
|
|
mediaSession.setActionHandler('seekforward', (details) => {
|
|
const offset = details.seekOffset || 10
|
|
audio.currentTime = Math.min(audio.currentTime + offset, audio.duration)
|
|
})
|
|
mediaSession.setActionHandler('seekbackward', (details) => {
|
|
const offset = details.seekOffset || 10
|
|
audio.currentTime = Math.max(audio.currentTime - offset, 0)
|
|
})
|
|
// FIXME
|
|
// function updatePositionState() {
|
|
// if (mediaSession && mediaSession.setPositionState) {
|
|
// mediaSession.setPositionState({
|
|
// duration: audio.duration || 0,
|
|
// playbackRate: audio.playbackRate,
|
|
// position: audio.currentTime,
|
|
// });
|
|
// }
|
|
// }
|
|
}
|
|
}
|