988 lines
24 KiB
Vue
988 lines
24 KiB
Vue
<template>
|
|
<div
|
|
class="aplayer"
|
|
:class="{
|
|
'aplayer-narrow': isMiniMode,
|
|
'aplayer-withlist' : !isMiniMode && musicList.length > 0,
|
|
'aplayer-withlrc': !isMiniMode && (!!$slots.display || shouldShowLrc),
|
|
'aplayer-float': isFloatMode,
|
|
'aplayer-loading': isPlaying && isLoading
|
|
}"
|
|
:style="floatStyleObj"
|
|
>
|
|
<div class="aplayer-body">
|
|
<thumbnail
|
|
:pic="currentMusic.pic"
|
|
:playing="isPlaying"
|
|
:enable-drag="isFloatMode"
|
|
:theme="currentTheme"
|
|
@toggleplay="toggle"
|
|
@dragbegin="onDragBegin"
|
|
@dragging="onDragAround"
|
|
/>
|
|
<div class="aplayer-info" v-show="!isMiniMode">
|
|
<div class="aplayer-music">
|
|
<span class="aplayer-title">{{ currentMusic.title || 'Untitled' }}</span>
|
|
<span class="aplayer-author">{{ currentMusic.artist || currentMusic.author || 'Unknown' }}</span>
|
|
</div>
|
|
<slot name="display" :current-music="currentMusic" :play-stat="playStat">
|
|
<lyrics :current-music="currentMusic" :play-stat="playStat" v-if="shouldShowLrc" />
|
|
</slot>
|
|
<controls
|
|
:shuffle="shouldShuffle"
|
|
:repeat="repeatMode"
|
|
:stat="playStat"
|
|
:volume="audioVolume"
|
|
:muted="isAudioMuted"
|
|
:theme="currentTheme"
|
|
@toggleshuffle="shouldShuffle = !shouldShuffle"
|
|
@togglelist="showList = !showList"
|
|
@togglemute="toggleMute"
|
|
@setvolume="setAudioVolume"
|
|
@dragbegin="onProgressDragBegin"
|
|
@dragend="onProgressDragEnd"
|
|
@dragging="onProgressDragging"
|
|
@nextmode="setNextMode"
|
|
/>
|
|
</div>
|
|
</div>
|
|
<audio ref="audio"></audio>
|
|
<music-list
|
|
:show="showList && !isMiniMode"
|
|
:current-music="currentMusic"
|
|
:music-list="musicList"
|
|
:play-index="playIndex"
|
|
:listmaxheight="listmaxheight || listMaxHeight"
|
|
:theme="currentTheme"
|
|
@selectsong="onSelectSong"
|
|
/>
|
|
</div>
|
|
</template>
|
|
<script>
|
|
import Thumbnail from './components/aplayer-thumbnail.vue'
|
|
import MusicList from './components/aplayer-list.vue'
|
|
import Controls from './components/aplayer-controller.vue'
|
|
import Lyrics from './components/aplayer-lrc.vue'
|
|
import { deprecatedProp, warn } from './utils'
|
|
|
|
/**
|
|
* memorize self-adapting theme for cover image urls
|
|
* @type {Object.<url, rgb()>}
|
|
*/
|
|
const picThemeCache = {}
|
|
|
|
// mutex playing instance
|
|
let activeMutex = null
|
|
|
|
|
|
const REPEAT = {
|
|
NONE: 'none',
|
|
MUSIC: 'music',
|
|
LIST: 'list',
|
|
NO_REPEAT: 'no_repeat',
|
|
REPEAT_ONE: 'repeat_one',
|
|
REPEAT_ALL: 'repeat_all',
|
|
};
|
|
|
|
const VueAPlayer = {
|
|
name: 'APlayer',
|
|
disableVersionBadge: false,
|
|
components: {
|
|
Thumbnail,
|
|
Controls,
|
|
MusicList,
|
|
Lyrics,
|
|
},
|
|
props: {
|
|
music: {
|
|
type: Object,
|
|
required: true,
|
|
validator (song) {
|
|
if (song.url) {
|
|
deprecatedProp('music.url', '1.4.0', 'music.src')
|
|
}
|
|
if (song.author) {
|
|
deprecatedProp('music.author', '1.4.1', 'music.artist')
|
|
}
|
|
return song.src || song.url
|
|
},
|
|
},
|
|
list: {
|
|
type: Array,
|
|
default () {
|
|
return []
|
|
},
|
|
},
|
|
mini: {
|
|
type: Boolean,
|
|
default: false,
|
|
},
|
|
showLrc: {
|
|
type: Boolean,
|
|
default: false,
|
|
},
|
|
mutex: {
|
|
type: Boolean,
|
|
default: true,
|
|
},
|
|
theme: {
|
|
type: String,
|
|
default: '#41b883',
|
|
},
|
|
|
|
listMaxHeight: String,
|
|
/**
|
|
* @since 1.4.1
|
|
* Fold playlist initially
|
|
*/
|
|
listFolded: {
|
|
type: Boolean,
|
|
default: false,
|
|
},
|
|
|
|
/**
|
|
* @since 1.2.0 Float mode
|
|
*/
|
|
float: {
|
|
type: Boolean,
|
|
default: false,
|
|
},
|
|
|
|
// Audio attributes as props
|
|
// since 1.4.0
|
|
// autoplay controls muted preload volume
|
|
// autoplay is not observable
|
|
|
|
/**
|
|
* @since 1.4.0
|
|
* not observable
|
|
*/
|
|
autoplay: {
|
|
type: Boolean,
|
|
default: false,
|
|
},
|
|
|
|
/**
|
|
* @since 1.4.0
|
|
* whether to show native audio controls below Vue-APlayer
|
|
* only work in development environment and not mini mode
|
|
*
|
|
* observable
|
|
*/
|
|
controls: {
|
|
type: Boolean,
|
|
default: false,
|
|
},
|
|
|
|
/**
|
|
* @since 1.4.0
|
|
* observable, sync
|
|
*/
|
|
muted: {
|
|
type: Boolean,
|
|
default: false,
|
|
},
|
|
/**
|
|
* @since 1.4.0
|
|
* observable
|
|
*/
|
|
preload: String,
|
|
|
|
/**
|
|
* @since 1.4.0
|
|
* observable, sync
|
|
*/
|
|
volume: {
|
|
type: Number,
|
|
default: 0.8,
|
|
validator (value) {
|
|
return value >= 0 && value <= 1
|
|
},
|
|
},
|
|
|
|
// play order control
|
|
// since 1.5.0
|
|
|
|
/**
|
|
* @since 1.5.0
|
|
* @see https://support.apple.com/en-us/HT207230
|
|
* twoWay
|
|
*/
|
|
shuffle: {
|
|
type: Boolean,
|
|
default: false,
|
|
},
|
|
/**
|
|
* @since 1.5.0
|
|
* @see https://support.apple.com/en-us/HT207230
|
|
* twoWay
|
|
*/
|
|
repeat: {
|
|
type: String,
|
|
default: REPEAT.NO_REPEAT,
|
|
},
|
|
|
|
|
|
// deprecated props
|
|
|
|
/**
|
|
* @deprecated since 1.1.2, use listMaxHeight instead
|
|
*/
|
|
listmaxheight: {
|
|
type: String,
|
|
validator (value) {
|
|
if (value) {
|
|
deprecatedProp('listmaxheight', '1.1.2', 'listMaxHeight')
|
|
}
|
|
return true
|
|
},
|
|
},
|
|
/**
|
|
* @deprecated since 1.1.2, use mini instead
|
|
*/
|
|
narrow: {
|
|
type: Boolean,
|
|
default: false,
|
|
validator (value) {
|
|
if (value) {
|
|
deprecatedProp('narrow', '1.1.2', 'mini')
|
|
}
|
|
return true
|
|
},
|
|
},
|
|
/**
|
|
* @deprecated since 1.2.2
|
|
*/
|
|
showlrc: {
|
|
type: Boolean,
|
|
default: false,
|
|
validator (value) {
|
|
if (value) {
|
|
deprecatedProp('showlrc', '1.2.2', 'showLrc')
|
|
}
|
|
return true
|
|
},
|
|
},
|
|
/**
|
|
* @deprecated and REMOVED since 1.5.0
|
|
*/
|
|
// mode: {
|
|
// type: String,
|
|
// default: 'circulation',
|
|
// validator (value) {
|
|
// if (value) {
|
|
// deprecatedProp('mode', '1.5.0', 'shuffle and repeat')
|
|
// }
|
|
// return true
|
|
// }
|
|
// },
|
|
},
|
|
data () {
|
|
return {
|
|
internalMusic: this.music,
|
|
isPlaying: false,
|
|
isSeeking: false,
|
|
wasPlayingBeforeSeeking: false,
|
|
isMobile: /mobile/i.test(window.navigator.userAgent),
|
|
playStat: {
|
|
duration: 0,
|
|
loadedTime: 0,
|
|
playedTime: 0,
|
|
},
|
|
showList: !this.listFolded,
|
|
|
|
// handle Promise returned from audio.play()
|
|
// @see https://developer.mozilla.org/en-US/docs/Web/API/HTMLMediaElement/play
|
|
audioPlayPromise: Promise.resolve(),
|
|
|
|
|
|
// @since 1.2.0 float mode
|
|
|
|
floatOriginX: 0,
|
|
floatOriginY: 0,
|
|
floatOffsetLeft: 0,
|
|
floatOffsetTop: 0,
|
|
|
|
|
|
// @since 1.3.0 self adapting theme
|
|
selfAdaptingTheme: null,
|
|
|
|
|
|
// @since 1.4.0
|
|
// sync muted, volume
|
|
|
|
internalMuted: this.muted,
|
|
internalVolume: this.volume,
|
|
|
|
// @since 1.4.1
|
|
// Loading indicator
|
|
isLoading: false,
|
|
|
|
|
|
// @since 1.5.1
|
|
// sync shuffle, repeat
|
|
internalShuffle: this.shuffle,
|
|
internalRepeat: this.repeat,
|
|
// for shuffling
|
|
shuffledList: [],
|
|
}
|
|
},
|
|
computed: {
|
|
// alias for $refs.audio
|
|
audio () {
|
|
return this.$refs.audio
|
|
},
|
|
|
|
// sync music
|
|
currentMusic: {
|
|
get () {
|
|
return this.internalMusic
|
|
},
|
|
set (val) {
|
|
this.$emit('update:music', val)
|
|
this.internalMusic = val
|
|
},
|
|
},
|
|
// compatible for deprecated props
|
|
isMiniMode () {
|
|
return this.mini || this.narrow
|
|
},
|
|
shouldShowLrc () {
|
|
return this.showLrc || this.showlrc
|
|
},
|
|
|
|
// props wrappers
|
|
|
|
currentTheme () {
|
|
return this.selfAdaptingTheme || this.currentMusic.theme || this.theme
|
|
},
|
|
isFloatMode () {
|
|
return this.float && !this.isMobile
|
|
},
|
|
shouldAutoplay () {
|
|
if (this.isMobile) return false
|
|
return this.autoplay
|
|
},
|
|
musicList () {
|
|
return this.list
|
|
},
|
|
shouldShowNativeControls () {
|
|
return process.env.NODE_ENV !== 'production' &&
|
|
this.controls &&
|
|
!this.isMiniMode
|
|
},
|
|
|
|
// useful
|
|
|
|
floatStyleObj () {
|
|
// transform: translate(floatOffsetLeft, floatOffsetY)
|
|
return {
|
|
transform: `translate(${this.floatOffsetLeft}px, ${this.floatOffsetTop}px)`,
|
|
webkitTransform: `translate(${this.floatOffsetLeft}px, ${this.floatOffsetTop}px)`,
|
|
}
|
|
},
|
|
currentPicStyleObj () {
|
|
if (this.currentMusic && this.currentMusic.pic) {
|
|
return {
|
|
backgroundImage: `url(${this.currentMusic.pic})`,
|
|
}
|
|
}
|
|
return {}
|
|
},
|
|
loadProgress () {
|
|
if (this.playStat.duration === 0) return 0
|
|
return this.playStat.loadedTime / this.playStat.duration
|
|
},
|
|
playProgress () {
|
|
if (this.playStat.duration === 0) return 0
|
|
return this.playStat.playedTime / this.playStat.duration
|
|
},
|
|
playIndex: {
|
|
get () {
|
|
return this.shuffledList.indexOf(this.currentMusic)
|
|
},
|
|
set (val) {
|
|
this.currentMusic = this.shuffledList[val % this.shuffledList.length]
|
|
},
|
|
},
|
|
shouldRepeat () {
|
|
return this.repeatMode !== REPEAT.NO_REPEAT
|
|
},
|
|
|
|
// since 1.4.0
|
|
// sync muted, volume
|
|
|
|
isAudioMuted: {
|
|
get () {
|
|
return this.internalMuted
|
|
},
|
|
set (val) {
|
|
this.$emit('update:muted', val)
|
|
this.internalMuted = val
|
|
},
|
|
},
|
|
audioVolume: {
|
|
get () {
|
|
return this.internalVolume
|
|
},
|
|
set (val) {
|
|
this.$emit('update:volume', val)
|
|
this.internalVolume = val
|
|
},
|
|
},
|
|
|
|
|
|
// since 1.5.0
|
|
// sync shuffle, repeat
|
|
shouldShuffle: {
|
|
get () {
|
|
return this.internalShuffle
|
|
},
|
|
set (val) {
|
|
this.$emit('update:shuffle', val)
|
|
this.internalShuffle = val
|
|
},
|
|
},
|
|
repeatMode: {
|
|
get () {
|
|
switch (this.internalRepeat) {
|
|
case REPEAT.NONE:
|
|
case REPEAT.NO_REPEAT:
|
|
return REPEAT.NO_REPEAT
|
|
case REPEAT.MUSIC:
|
|
case REPEAT.REPEAT_ONE:
|
|
return REPEAT.REPEAT_ONE
|
|
default:
|
|
return REPEAT.REPEAT_ALL
|
|
}
|
|
},
|
|
set (val) {
|
|
this.$emit('update:repeat', val)
|
|
this.internalRepeat = val
|
|
},
|
|
},
|
|
},
|
|
methods: {
|
|
// Float mode
|
|
|
|
onDragBegin () {
|
|
this.floatOriginX = this.floatOffsetLeft
|
|
this.floatOriginY = this.floatOffsetTop
|
|
},
|
|
onDragAround ({ offsetLeft, offsetTop }) {
|
|
this.floatOffsetLeft = this.floatOriginX + offsetLeft
|
|
this.floatOffsetTop = this.floatOriginY + offsetTop
|
|
},
|
|
|
|
// functions
|
|
|
|
setNextMode () {
|
|
if (this.repeatMode === REPEAT.REPEAT_ALL) {
|
|
this.repeatMode = REPEAT.REPEAT_ONE
|
|
} else if (this.repeatMode === REPEAT.REPEAT_ONE) {
|
|
this.repeatMode = REPEAT.NO_REPEAT
|
|
} else {
|
|
this.repeatMode = REPEAT.REPEAT_ALL
|
|
}
|
|
},
|
|
thenPlay () {
|
|
this.$nextTick(() => {
|
|
this.play()
|
|
})
|
|
},
|
|
|
|
// controls
|
|
|
|
// play/pause
|
|
|
|
toggle () {
|
|
if (!this.audio.paused) {
|
|
this.pause()
|
|
} else {
|
|
this.play()
|
|
}
|
|
},
|
|
play () {
|
|
if (this.mutex) {
|
|
if (activeMutex && activeMutex !== this) {
|
|
activeMutex.pause()
|
|
}
|
|
activeMutex = this
|
|
}
|
|
// handle .play() Promise
|
|
const audioPlayPromise = this.audio.play()
|
|
if (audioPlayPromise) {
|
|
return this.audioPlayPromise = new Promise((resolve, reject) => {
|
|
// rejectPlayPromise is to force reject audioPlayPromise if it's still pending when pause() is called
|
|
this.rejectPlayPromise = reject
|
|
audioPlayPromise.then((res) => {
|
|
this.rejectPlayPromise = null
|
|
resolve(res)
|
|
}).catch(warn)
|
|
})
|
|
}
|
|
},
|
|
pause () {
|
|
this.audioPlayPromise
|
|
.then(() => {
|
|
this.audio.pause()
|
|
})
|
|
// Avoid force rejection throws Uncaught
|
|
.catch(() => {
|
|
this.audio.pause()
|
|
})
|
|
|
|
// audioPlayPromise is still pending
|
|
if (this.rejectPlayPromise) {
|
|
// force reject playPromise
|
|
this.rejectPlayPromise()
|
|
this.rejectPlayPromise = null
|
|
}
|
|
},
|
|
switch(index) {
|
|
this.playIndex = index
|
|
this.thenPlay()
|
|
},
|
|
// progress bar
|
|
|
|
onProgressDragBegin (val) {
|
|
this.wasPlayingBeforeSeeking = this.isPlaying
|
|
this.pause()
|
|
this.isSeeking = true
|
|
|
|
// handle load failures
|
|
if (!isNaN(this.audio.duration)) {
|
|
this.audio.currentTime = this.audio.duration * val
|
|
}
|
|
},
|
|
onProgressDragging (val) {
|
|
if (isNaN(this.audio.duration)) {
|
|
this.playStat.playedTime = 0
|
|
} else {
|
|
this.audio.currentTime = this.audio.duration * val
|
|
}
|
|
},
|
|
onProgressDragEnd (val) {
|
|
this.isSeeking = false
|
|
|
|
if (this.wasPlayingBeforeSeeking) {
|
|
this.thenPlay()
|
|
}
|
|
},
|
|
|
|
// volume
|
|
|
|
toggleMute () {
|
|
this.setAudioMuted(!this.audio.muted)
|
|
},
|
|
setAudioMuted (val) {
|
|
this.audio.muted = val
|
|
},
|
|
setAudioVolume (val) {
|
|
this.audio.volume = val
|
|
if (val > 0) {
|
|
this.setAudioMuted(false)
|
|
}
|
|
},
|
|
|
|
// playlist
|
|
|
|
getShuffledList () {
|
|
if (!this.list.length) {
|
|
return [this.internalMusic]
|
|
}
|
|
let unshuffled = [...this.list]
|
|
if (!this.internalShuffle || unshuffled.length <= 1) {
|
|
return unshuffled
|
|
}
|
|
|
|
let indexOfCurrentMusic = unshuffled.indexOf(this.internalMusic)
|
|
if (unshuffled.length === 2 && indexOfCurrentMusic !== -1) {
|
|
if (indexOfCurrentMusic === 0) {
|
|
return unshuffled
|
|
} else {
|
|
return [this.internalMusic, unshuffled[0]]
|
|
}
|
|
}
|
|
// shuffle list
|
|
// @see https://stackoverflow.com/questions/6274339/how-can-i-shuffle-an-array
|
|
for (let i = unshuffled.length - 1; i > 0; i--) {
|
|
const j = Math.floor(Math.random() * (i + 1))
|
|
const tmp = unshuffled[i]
|
|
unshuffled[i] = unshuffled[j]
|
|
unshuffled[j] = tmp
|
|
}
|
|
|
|
// take currentMusic to first
|
|
if (indexOfCurrentMusic !== -1) {
|
|
indexOfCurrentMusic = unshuffled.indexOf(this.internalMusic)
|
|
if (indexOfCurrentMusic !== 0) {
|
|
[unshuffled[0], unshuffled[indexOfCurrentMusic]] = [unshuffled[indexOfCurrentMusic], unshuffled[0]]
|
|
}
|
|
}
|
|
|
|
return unshuffled
|
|
},
|
|
|
|
onSelectSong (song) {
|
|
if (this.currentMusic === song) {
|
|
this.toggle()
|
|
} else {
|
|
this.currentMusic = song
|
|
this.thenPlay()
|
|
}
|
|
},
|
|
|
|
// event handlers
|
|
// for keeping up with audio states
|
|
|
|
onAudioPlay () {
|
|
this.isPlaying = true
|
|
},
|
|
onAudioPause () {
|
|
this.isPlaying = false
|
|
},
|
|
onAudioWaiting () {
|
|
this.isLoading = true
|
|
},
|
|
onAudioCanplay () {
|
|
this.isLoading = false
|
|
},
|
|
onAudioDurationChange () {
|
|
if (this.audio.duration !== 1) {
|
|
this.playStat.duration = this.audio.duration
|
|
}
|
|
},
|
|
onAudioProgress () {
|
|
if (this.audio.buffered.length) {
|
|
this.playStat.loadedTime = this.audio.buffered.end(this.audio.buffered.length - 1)
|
|
} else {
|
|
this.playStat.loadedTime = 0
|
|
}
|
|
},
|
|
onAudioTimeUpdate () {
|
|
this.playStat.playedTime = this.audio.currentTime
|
|
},
|
|
onAudioSeeking () {
|
|
this.playStat.playedTime = this.audio.currentTime
|
|
},
|
|
onAudioSeeked () {
|
|
this.playStat.playedTime = this.audio.currentTime
|
|
},
|
|
onAudioVolumeChange () {
|
|
this.audioVolume = this.audio.volume
|
|
this.isAudioMuted = this.audio.muted
|
|
},
|
|
onAudioEnded () {
|
|
// determine next song according to shuffle and repeat
|
|
if (this.repeatMode === REPEAT.REPEAT_ALL) {
|
|
if (this.shouldShuffle && this.playIndex === this.shuffledList.length - 1) {
|
|
this.shuffledList = this.getShuffledList()
|
|
}
|
|
this.playIndex++
|
|
this.thenPlay()
|
|
} else if (this.repeatMode === REPEAT.REPEAT_ONE) {
|
|
this.thenPlay()
|
|
} else {
|
|
this.playIndex++
|
|
if (this.playIndex !== 0) {
|
|
this.thenPlay()
|
|
} else if (this.shuffledList.length === 1) {
|
|
this.audio.currentTime = 0
|
|
}
|
|
}
|
|
},
|
|
|
|
initAudio () {
|
|
|
|
// since 1.4.0 Audio attributes as props
|
|
|
|
this.audio.controls = this.shouldShowNativeControls
|
|
this.audio.muted = this.muted
|
|
this.audio.preload = this.preload
|
|
this.audio.volume = this.volume
|
|
|
|
|
|
// since 1.4.0 Emit as many native audio events
|
|
// @see https://developer.mozilla.org/en-US/docs/Web/Guide/Events/Media_events
|
|
|
|
const mediaEvents = [
|
|
'abort',
|
|
'canplay', 'canplaythrough',
|
|
'durationchange',
|
|
'emptied', 'encrypted', 'ended', 'error',
|
|
'interruptbegin', 'interruptend',
|
|
'loadeddata', 'loadedmetadata', 'loadstart',
|
|
'mozaudioavailable',
|
|
'pause', 'play', 'playing', 'progress',
|
|
'ratechange',
|
|
'seeked', 'seeking', 'stalled', 'suspend',
|
|
'timeupdate',
|
|
'volumechange',
|
|
'waiting',
|
|
]
|
|
mediaEvents.forEach(event => {
|
|
this.audio.addEventListener(event, e => this.$emit(event, e))
|
|
})
|
|
|
|
|
|
// event handlers
|
|
// they don't emit native media events
|
|
|
|
this.audio.addEventListener('play', this.onAudioPlay)
|
|
this.audio.addEventListener('pause', this.onAudioPause)
|
|
this.audio.addEventListener('abort', this.onAudioPause)
|
|
this.audio.addEventListener('waiting', this.onAudioWaiting)
|
|
this.audio.addEventListener('canplay', this.onAudioCanplay)
|
|
this.audio.addEventListener('progress', this.onAudioProgress)
|
|
this.audio.addEventListener('durationchange', this.onAudioDurationChange)
|
|
this.audio.addEventListener('seeking', this.onAudioSeeking)
|
|
this.audio.addEventListener('seeked', this.onAudioSeeked)
|
|
this.audio.addEventListener('timeupdate', this.onAudioTimeUpdate)
|
|
this.audio.addEventListener('volumechange', this.onAudioVolumeChange)
|
|
this.audio.addEventListener('ended', this.onAudioEnded)
|
|
|
|
|
|
if (this.currentMusic) {
|
|
this.audio.src = this.currentMusic.src || this.currentMusic.url
|
|
}
|
|
},
|
|
|
|
setSelfAdaptingTheme () {
|
|
// auto theme according to current music cover image
|
|
if ((this.currentMusic.theme || this.theme) === 'pic') {
|
|
const pic = this.currentMusic.pic
|
|
// use cache
|
|
if (picThemeCache[pic]) {
|
|
this.selfAdaptingTheme = picThemeCache[pic]
|
|
} else {
|
|
try {
|
|
new ColorThief().getColorAsync(pic, ([r, g, b]) => {
|
|
picThemeCache[pic] = `rgb(${r}, ${g}, ${b})`
|
|
this.selfAdaptingTheme = `rgb(${r}, ${g}, ${b})`
|
|
})
|
|
} catch (e) {
|
|
warn('color-thief is required to support self-adapting theme')
|
|
}
|
|
}
|
|
} else {
|
|
this.selfAdaptingTheme = null
|
|
}
|
|
},
|
|
},
|
|
watch: {
|
|
music (music) {
|
|
this.internalMusic = music
|
|
},
|
|
|
|
currentMusic: {
|
|
handler (music) {
|
|
// async
|
|
this.setSelfAdaptingTheme()
|
|
|
|
const src = music.src || music.url
|
|
// HLS support
|
|
if (/\.m3u8(?=(#|\?|$))/.test(src)) {
|
|
if (this.audio.canPlayType('application/x-mpegURL') || this.audio.canPlayType('application/vnd.apple.mpegURL')) {
|
|
this.audio.src = src
|
|
} else {
|
|
try {
|
|
const Hls = require('hls.js')
|
|
if (Hls.isSupported()) {
|
|
if (!this.hls) {
|
|
this.hls = new Hls()
|
|
}
|
|
this.hls.loadSource(src)
|
|
this.hls.attachMedia(this.audio)
|
|
} else {
|
|
warn('HLS is not supported on your browser')
|
|
this.audio.src = src
|
|
}
|
|
} catch (e) {
|
|
warn('hls.js is required to support m3u8')
|
|
this.audio.src = src
|
|
}
|
|
}
|
|
} else {
|
|
this.audio.src = src
|
|
}
|
|
// self-adapting theme color
|
|
},
|
|
},
|
|
|
|
// since 1.4.0
|
|
// observe controls, muted, preload, volume
|
|
|
|
shouldShowNativeControls (val) {
|
|
this.audio.controls = val
|
|
},
|
|
isAudioMuted (val) {
|
|
this.audio.muted = val
|
|
},
|
|
preload (val) {
|
|
this.audio.preload = val
|
|
},
|
|
audioVolume (val) {
|
|
this.audio.volume = val
|
|
},
|
|
|
|
// sync muted, volume
|
|
|
|
muted (val) {
|
|
this.internalMuted = val
|
|
},
|
|
volume (val) {
|
|
this.internalVolume = val
|
|
},
|
|
|
|
|
|
// sync shuffle, repeat
|
|
shuffle (val) {
|
|
this.internalShuffle = val
|
|
},
|
|
repeat (val) {
|
|
this.internalRepeat = val
|
|
},
|
|
},
|
|
created () {
|
|
this.shuffledList = this.getShuffledList()
|
|
},
|
|
mounted () {
|
|
this.initAudio()
|
|
this.setSelfAdaptingTheme()
|
|
if (this.autoplay) this.play()
|
|
},
|
|
beforeDestroy () {
|
|
if (activeMutex === this) {
|
|
activeMutex = null
|
|
}
|
|
if (this.hls) {
|
|
this.hls.destroy()
|
|
}
|
|
},
|
|
}
|
|
|
|
export default VueAPlayer
|
|
|
|
</script>
|
|
|
|
<style lang="less" scoped>
|
|
@import "./less/variables";
|
|
|
|
.aplayer {
|
|
font-family: Arial, Helvetica, sans-serif;
|
|
color: #000;
|
|
background-color: #fff;
|
|
margin: 5px;
|
|
box-shadow: 0 2px 2px 0 rgba(0, 0, 0, 0.07), 0 1px 5px 0 rgba(0, 0, 0, 0.1);
|
|
border-radius: 2px;
|
|
overflow: hidden;
|
|
user-select: none;
|
|
line-height: initial;
|
|
|
|
* {
|
|
box-sizing: content-box;
|
|
}
|
|
|
|
.aplayer-lrc-content {
|
|
display: none;
|
|
}
|
|
|
|
.aplayer-body {
|
|
display: flex;
|
|
|
|
position: relative;
|
|
.aplayer-info {
|
|
flex-grow: 1;
|
|
display: flex;
|
|
flex-direction: column;
|
|
|
|
text-align: start;
|
|
padding: 14px 7px 0 10px;
|
|
height: @aplayer-height;
|
|
box-sizing: border-box;
|
|
overflow: hidden;
|
|
|
|
.aplayer-music {
|
|
flex-grow: 1;
|
|
|
|
overflow: hidden;
|
|
white-space: nowrap;
|
|
text-overflow: ellipsis;
|
|
margin-left: 5px;
|
|
user-select: text;
|
|
cursor: default;
|
|
padding-bottom: 2px;
|
|
|
|
.aplayer-title {
|
|
font-size: 14px;
|
|
}
|
|
|
|
.aplayer-author {
|
|
font-size: 12px;
|
|
color: #666;
|
|
margin-left: 10px;
|
|
}
|
|
}
|
|
|
|
.aplayer-lrc {
|
|
z-index: 0;
|
|
}
|
|
}
|
|
}
|
|
|
|
audio[controls] {
|
|
display: block;
|
|
width: 100%;
|
|
}
|
|
|
|
// Mini mode
|
|
&.aplayer-narrow {
|
|
width: @aplayer-height;
|
|
}
|
|
|
|
&.aplayer-withlrc {
|
|
.aplayer-body {
|
|
.aplayer-pic {
|
|
height: @aplayer-height-lrc;
|
|
width: @aplayer-height-lrc;
|
|
}
|
|
|
|
.aplayer-info {
|
|
height: @aplayer-height-lrc;
|
|
}
|
|
|
|
.aplayer-info {
|
|
padding: 10px 7px 0 7px;
|
|
}
|
|
}
|
|
}
|
|
|
|
&.aplayer-withlist {
|
|
.aplayer-body {
|
|
.aplayer-info {
|
|
border-bottom: 1px solid #e9e9e9;
|
|
}
|
|
|
|
.aplayer-controller .aplayer-time .aplayer-icon.aplayer-icon-menu {
|
|
display: block;
|
|
}
|
|
}
|
|
}
|
|
|
|
/* floating player on top */
|
|
position: relative;
|
|
&.aplayer-float {
|
|
z-index: 1;
|
|
}
|
|
}
|
|
|
|
@keyframes aplayer-roll {
|
|
0% {
|
|
left: 0
|
|
}
|
|
100% {
|
|
left: -100%
|
|
}
|
|
}
|
|
</style> |