<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"> <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: () => {} } }, 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: { immediate: true, handler() { this.setChapterTicks() } } }, computed: { 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 var offsetX = e.offsetX var perc = offsetX / this.trackWidth const duration = this.useChapterTrack ? this.currentChapterDuration : this.duration var time = 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() { var 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 var 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) => { var perc = chap.start / this.duration return { title: chap.title, left: perc * this.trackWidth } }) }, mousemoveTrack(e) { var offsetX = e.offsetX const duration = this.useChapterTrack ? this.currentChapterDuration : this.duration const time = (offsetX / this.trackWidth) * duration 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(time) var chapter = this.chapters.find((chapter) => chapter.start <= time && time < 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() } }, mounted() { this.setTrackWidth() window.addEventListener('resize', this.windowResize) }, beforeDestroy() { window.removeEventListener('resize', this.windowResize) } } </script>