221 lines
7.8 KiB
Vue
221 lines
7.8 KiB
Vue
<template>
|
|
<div class="relative">
|
|
<!-- Track -->
|
|
<div ref="track" class="w-full h-2 bg-gray-700 relative cursor-pointer transform duration-100 hover:scale-y-125 overflow-hidden" @mousemove="mousemoveTrack" @mouseleave="mouseleaveTrack" @click.stop="clickTrack">
|
|
<div ref="readyTrack" class="h-full bg-gray-600 absolute top-0 left-0 pointer-events-none" />
|
|
<div ref="bufferTrack" class="h-full bg-gray-500 absolute top-0 left-0 pointer-events-none" />
|
|
<div ref="playedTrack" class="h-full bg-gray-200 absolute top-0 left-0 pointer-events-none" />
|
|
<div ref="trackCursor" class="h-full w-0.5 bg-gray-50 absolute top-0 left-0 opacity-0 pointer-events-none" />
|
|
<div v-if="loading" class="h-full w-1/4 absolute left-0 top-0 loadingTrack pointer-events-none bg-white bg-opacity-25" />
|
|
</div>
|
|
<div class="w-full h-2 relative overflow-hidden" :class="useChapterTrack ? 'opacity-0' : ''">
|
|
<template v-for="(tick, index) in chapterTicks">
|
|
<div :key="index" :style="{ left: tick.left + 'px' }" class="absolute top-0 w-px bg-white bg-opacity-30 h-1 pointer-events-none" />
|
|
</template>
|
|
</div>
|
|
|
|
<!-- Hover timestamp -->
|
|
<div ref="hoverTimestamp" class="absolute -top-8 left-0 bg-white text-black rounded-full opacity-0 pointer-events-none z-10">
|
|
<p ref="hoverTimestampText" class="text-xs font-mono text-center px-2 py-0.5 truncate whitespace-nowrap">00:00</p>
|
|
</div>
|
|
<div ref="hoverTimestampArrow" class="absolute -top-3 left-0 bg-white text-black rounded-full opacity-0 pointer-events-none">
|
|
<div class="absolute -bottom-1.5 left-0 right-0 w-full flex justify-center">
|
|
<div class="arrow-down" />
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</template>
|
|
|
|
<script>
|
|
export default {
|
|
props: {
|
|
loading: Boolean,
|
|
duration: Number,
|
|
chapters: {
|
|
type: Array,
|
|
default: () => []
|
|
},
|
|
currentChapter: {
|
|
type: Object,
|
|
default: () => {}
|
|
},
|
|
playbackRate: Number
|
|
},
|
|
data() {
|
|
return {
|
|
trackWidth: 0,
|
|
currentTime: 0,
|
|
percentReady: 0,
|
|
bufferTime: 0,
|
|
chapterTicks: [],
|
|
trackOffsetLeft: 16, // Track is 16px from edge
|
|
playedTrackWidth: 0,
|
|
readyTrackWidth: 0,
|
|
bufferTrackWidth: 0,
|
|
useChapterTrack: false
|
|
}
|
|
},
|
|
watch: {
|
|
duration: {
|
|
handler() {
|
|
this.setChapterTicks()
|
|
}
|
|
}
|
|
},
|
|
computed: {
|
|
_playbackRate() {
|
|
if (!this.playbackRate || isNaN(this.playbackRate)) return 1
|
|
return this.playbackRate
|
|
},
|
|
currentChapterDuration() {
|
|
if (!this.currentChapter) return 0
|
|
return this.currentChapter.end - this.currentChapter.start
|
|
},
|
|
currentChapterStart() {
|
|
if (!this.currentChapter) return 0
|
|
return this.currentChapter.start
|
|
}
|
|
},
|
|
methods: {
|
|
setUseChapterTrack(useChapterTrack) {
|
|
this.useChapterTrack = useChapterTrack
|
|
this.updateBufferTrack()
|
|
this.updatePlayedTrackWidth()
|
|
},
|
|
clickTrack(e) {
|
|
if (this.loading) return
|
|
|
|
const offsetX = e.offsetX
|
|
const perc = offsetX / this.trackWidth
|
|
const baseTime = this.useChapterTrack ? this.currentChapterStart : 0
|
|
const duration = this.useChapterTrack ? this.currentChapterDuration : this.duration
|
|
const time = baseTime + perc * duration
|
|
if (isNaN(time) || time === null) {
|
|
console.error('Invalid time', perc, time)
|
|
return
|
|
}
|
|
this.$emit('seek', time)
|
|
},
|
|
setBufferTime(time) {
|
|
this.bufferTime = time
|
|
this.updateBufferTrack()
|
|
},
|
|
updateBufferTrack() {
|
|
const time = this.useChapterTrack ? Math.max(0, this.bufferTime - this.currentChapterStart) : this.bufferTime
|
|
const duration = this.useChapterTrack ? this.currentChapterDuration : this.duration
|
|
|
|
var bufferlen = (time / duration) * this.trackWidth
|
|
bufferlen = Math.round(bufferlen)
|
|
if (this.bufferTrackWidth === bufferlen || !this.$refs.bufferTrack) return
|
|
if (this.$refs.bufferTrack) this.$refs.bufferTrack.style.width = bufferlen + 'px'
|
|
this.bufferTrackWidth = bufferlen
|
|
},
|
|
setPercentageReady(percent) {
|
|
this.percentReady = percent
|
|
this.updateReadyTrack()
|
|
},
|
|
updateReadyTrack() {
|
|
const widthReady = Math.round(this.trackWidth * this.percentReady)
|
|
if (this.readyTrackWidth === widthReady) return
|
|
this.readyTrackWidth = widthReady
|
|
if (this.$refs.readyTrack) this.$refs.readyTrack.style.width = widthReady + 'px'
|
|
},
|
|
setCurrentTime(time) {
|
|
this.currentTime = time
|
|
this.updatePlayedTrackWidth()
|
|
},
|
|
updatePlayedTrackWidth() {
|
|
const time = this.useChapterTrack ? Math.max(0, this.currentTime - this.currentChapterStart) : this.currentTime
|
|
const duration = this.useChapterTrack ? this.currentChapterDuration : this.duration
|
|
|
|
const ptWidth = Math.round((time / duration) * this.trackWidth)
|
|
if (this.playedTrackWidth === ptWidth) {
|
|
return
|
|
}
|
|
if (this.$refs.playedTrack) this.$refs.playedTrack.style.width = ptWidth + 'px'
|
|
this.playedTrackWidth = ptWidth
|
|
},
|
|
setChapterTicks() {
|
|
this.chapterTicks = this.chapters.map((chap) => {
|
|
const perc = chap.start / this.duration
|
|
return {
|
|
title: chap.title,
|
|
left: perc * this.trackWidth
|
|
}
|
|
})
|
|
},
|
|
mousemoveTrack(e) {
|
|
const offsetX = e.offsetX
|
|
|
|
const baseTime = this.useChapterTrack ? this.currentChapterStart : 0
|
|
const duration = this.useChapterTrack ? this.currentChapterDuration : this.duration
|
|
const progressTime = (offsetX / this.trackWidth) * duration
|
|
const totalTime = baseTime + progressTime
|
|
|
|
if (this.$refs.hoverTimestamp) {
|
|
var width = this.$refs.hoverTimestamp.clientWidth
|
|
this.$refs.hoverTimestamp.style.opacity = 1
|
|
var posLeft = offsetX - width / 2
|
|
if (posLeft + width + this.trackOffsetLeft > window.innerWidth) {
|
|
posLeft = window.innerWidth - width - this.trackOffsetLeft
|
|
} else if (posLeft < -this.trackOffsetLeft) {
|
|
posLeft = -this.trackOffsetLeft
|
|
}
|
|
this.$refs.hoverTimestamp.style.left = posLeft + 'px'
|
|
}
|
|
|
|
if (this.$refs.hoverTimestampArrow) {
|
|
var width = this.$refs.hoverTimestampArrow.clientWidth
|
|
var posLeft = offsetX - width / 2
|
|
this.$refs.hoverTimestampArrow.style.opacity = 1
|
|
this.$refs.hoverTimestampArrow.style.left = posLeft + 'px'
|
|
}
|
|
if (this.$refs.hoverTimestampText) {
|
|
var hoverText = this.$secondsToTimestamp(progressTime / this._playbackRate)
|
|
|
|
var chapter = this.chapters.find((chapter) => chapter.start <= totalTime && totalTime < chapter.end)
|
|
if (chapter && chapter.title) {
|
|
hoverText += ` - ${chapter.title}`
|
|
}
|
|
this.$refs.hoverTimestampText.innerText = hoverText
|
|
}
|
|
if (this.$refs.trackCursor) {
|
|
this.$refs.trackCursor.style.opacity = 1
|
|
this.$refs.trackCursor.style.left = offsetX - 1 + 'px'
|
|
}
|
|
},
|
|
mouseleaveTrack() {
|
|
if (this.$refs.hoverTimestamp) {
|
|
this.$refs.hoverTimestamp.style.opacity = 0
|
|
}
|
|
if (this.$refs.hoverTimestampArrow) {
|
|
this.$refs.hoverTimestampArrow.style.opacity = 0
|
|
}
|
|
if (this.$refs.trackCursor) {
|
|
this.$refs.trackCursor.style.opacity = 0
|
|
}
|
|
},
|
|
setTrackWidth() {
|
|
if (this.$refs.track) {
|
|
this.trackWidth = this.$refs.track.clientWidth
|
|
} else {
|
|
console.error('Track not loaded', this.$refs)
|
|
}
|
|
},
|
|
windowResize() {
|
|
this.setTrackWidth()
|
|
this.setChapterTicks()
|
|
this.updatePlayedTrackWidth()
|
|
this.updateBufferTrack()
|
|
}
|
|
},
|
|
mounted() {
|
|
this.setTrackWidth()
|
|
this.setChapterTicks()
|
|
window.addEventListener('resize', this.windowResize)
|
|
},
|
|
beforeDestroy() {
|
|
window.removeEventListener('resize', this.windowResize)
|
|
}
|
|
}
|
|
</script> |