Version 1.0.0 - Frontend
This commit is contained in:
275
src/components/UI/DurationBar.vue
Normal file
275
src/components/UI/DurationBar.vue
Normal file
@@ -0,0 +1,275 @@
|
||||
<template>
|
||||
<div v-if="!buffering" class="duration-bar">
|
||||
<p v-if="!offline && localCurrentTime != 0">{{ getVideoDuration(localCurrentTime) }}</p>
|
||||
<p class="offline" v-else>-:--</p>
|
||||
<div ref="progressBar" :class="{'progress-bar': true, 'progress-bar-mobile': mobile}" @click="seek" @mousemove="hoverSeek" @mouseleave="hoverPercent = null" @pointerdown="startDrag">
|
||||
<span class="progress" :style="{ width: percent + '%' }"></span>
|
||||
<span class="progress-hover" v-if="hoverPercent !== null" :style="{ width: hoverPercent + '%' }"></span>
|
||||
<span :class="{ 'progress-selector': true, 'progress-selector-active': !props.offline }" :style="{ left: percent + '%' }"></span>
|
||||
</div>
|
||||
<p v-if="!offline && localTotalDuration == 0"><Icon color="var(--text-error)" icon="fa-circle"/> LIVE</p>
|
||||
<p v-else-if="!offline">{{ getVideoDuration(localTotalDuration) }}</p>
|
||||
<p class="offline" v-else>-:--</p>
|
||||
</div>
|
||||
<div v-else>
|
||||
<p class="offline">-:--</p>
|
||||
<div class="multi-loader">
|
||||
<span></span>
|
||||
<span></span>
|
||||
<span></span>
|
||||
<span></span>
|
||||
<span></span>
|
||||
</div>
|
||||
<p class="offline">-:--</p>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { getVideoDuration } from "@/utils/TimeConverter"
|
||||
import { ref, computed, onMounted, watch, onBeforeUnmount } from "vue"
|
||||
|
||||
const props = defineProps({
|
||||
currentTime: Number,
|
||||
totalDuration: Number,
|
||||
pause: Boolean,
|
||||
offline: Boolean,
|
||||
mobile: Boolean,
|
||||
buffering: Boolean
|
||||
})
|
||||
|
||||
const localCurrentTime = ref(props.offline ? 0 : props.currentTime)
|
||||
const localTotalDuration = ref(props.offline ? 0 : props.totalDuration)
|
||||
const progressBar = ref(null)
|
||||
const isDragging = ref(false)
|
||||
|
||||
const emit = defineEmits(["seek"])
|
||||
|
||||
const percent = computed(() => {
|
||||
if (!localTotalDuration.value) return 0
|
||||
const p = (localCurrentTime.value / localTotalDuration.value) * 100
|
||||
return Math.max(0, Math.min(100, p))
|
||||
})
|
||||
|
||||
const hoverPercent = ref(null)
|
||||
|
||||
function seek(e) {
|
||||
if(props.offline) return
|
||||
const rect = e.currentTarget.getBoundingClientRect()
|
||||
const clickPos = (e.clientX - rect.left) / rect.width
|
||||
const newTime = clickPos * localTotalDuration.value
|
||||
localCurrentTime.value = newTime
|
||||
emit("seek", newTime)
|
||||
}
|
||||
|
||||
function hoverSeek(e) {
|
||||
if(props.offline) return
|
||||
const rect = e.currentTarget.getBoundingClientRect()
|
||||
let hoverCalcPercent = ((e.clientX - rect.left) / rect.width) * 100
|
||||
hoverCalcPercent = Math.max(0, Math.min(100, hoverCalcPercent))
|
||||
hoverPercent.value = hoverCalcPercent
|
||||
// Add a tooltip or some indication of the current time like a title attribute
|
||||
e.currentTarget.title = getVideoDuration((hoverCalcPercent / 100) * localTotalDuration.value)
|
||||
}
|
||||
|
||||
// === DRAG LOGIC ===
|
||||
function startDrag(e) {
|
||||
if(props.offline) return
|
||||
isDragging.value = true
|
||||
updateDrag(e)
|
||||
|
||||
window.addEventListener("pointermove", updateDrag)
|
||||
window.addEventListener("pointerup", stopDrag)
|
||||
}
|
||||
|
||||
function updateDrag(e) {
|
||||
if(props.offline) return
|
||||
if (!isDragging.value) return
|
||||
const rect = e.currentTarget?.getBoundingClientRect?.()
|
||||
|| progressBar.value.getBoundingClientRect()
|
||||
|
||||
let pos = (e.clientX - rect.left) / rect.width
|
||||
pos = Math.max(0, Math.min(1, pos))
|
||||
|
||||
const newTime = pos * localTotalDuration.value
|
||||
localCurrentTime.value = newTime
|
||||
}
|
||||
|
||||
function stopDrag(e) {
|
||||
if(props.offline) return
|
||||
if (!isDragging.value) return
|
||||
isDragging.value = false
|
||||
|
||||
const rect = progressBar.value.getBoundingClientRect()
|
||||
let pos = (e.clientX - rect.left) / rect.width
|
||||
pos = Math.max(0, Math.min(1, pos))
|
||||
|
||||
const newTime = pos * localTotalDuration.value
|
||||
localCurrentTime.value = newTime
|
||||
emit("seek", newTime)
|
||||
|
||||
window.removeEventListener("pointermove", updateDrag)
|
||||
window.removeEventListener("pointerup", stopDrag)
|
||||
}
|
||||
|
||||
// === INIT ===
|
||||
onMounted(() => {
|
||||
if(!props.offline) {
|
||||
localCurrentTime.value = props.currentTime
|
||||
localTotalDuration.value = props.totalDuration
|
||||
}
|
||||
|
||||
watch(() => props.offline, (newOffline) => {
|
||||
if (newOffline) {
|
||||
localCurrentTime.value = 0
|
||||
localTotalDuration.value = 0
|
||||
}
|
||||
})
|
||||
|
||||
setInterval(() => {
|
||||
if(!isDragging.value && localCurrentTime.value < localTotalDuration.value && !props.pause) {
|
||||
localCurrentTime.value += 1
|
||||
}
|
||||
}, 1000)
|
||||
})
|
||||
|
||||
watch(() => props.currentTime, (newTime) => {
|
||||
if(props.offline) return
|
||||
if (!isDragging.value) localCurrentTime.value = newTime
|
||||
})
|
||||
|
||||
watch(() => props.totalDuration, (newDuration) => {
|
||||
if(props.offline) return
|
||||
localTotalDuration.value = newDuration
|
||||
})
|
||||
|
||||
|
||||
onBeforeUnmount(() => {
|
||||
window.removeEventListener("pointermove", updateDrag)
|
||||
window.removeEventListener("pointerup", stopDrag)
|
||||
})
|
||||
|
||||
function updateLocalValues() {
|
||||
if(props.offline) return
|
||||
localCurrentTime.value = props.currentTime
|
||||
localTotalDuration.value = props.totalDuration
|
||||
}
|
||||
|
||||
defineExpose({
|
||||
updateLocalValues
|
||||
})
|
||||
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
|
||||
.offline {
|
||||
color: var(--text-secondary);
|
||||
}
|
||||
|
||||
.duration-bar {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 10px;
|
||||
font-family: monospace;
|
||||
}
|
||||
|
||||
.progress-bar {
|
||||
position: relative;
|
||||
flex: 1;
|
||||
height: 6px;
|
||||
background: var(--tertiary);
|
||||
border-radius: 3px;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.progress-bar-mobile {
|
||||
background: var(--quaternary);
|
||||
}
|
||||
|
||||
.progress {
|
||||
display: block;
|
||||
position: absolute;
|
||||
height: 100%;
|
||||
max-width: 100%;
|
||||
background: var(--main);
|
||||
border-radius: 3px;
|
||||
user-select: none;
|
||||
pointer-events: none;
|
||||
}
|
||||
|
||||
.progress-selector {
|
||||
position: absolute;
|
||||
top: 50%;
|
||||
transform: translate(-50%, -50%);
|
||||
width: 12px;
|
||||
height: 12px;
|
||||
background: var(--main);
|
||||
display: none;
|
||||
border-radius: 50%;
|
||||
animation: zoomIn 0.2s ease;
|
||||
user-select: none;
|
||||
pointer-events: none;
|
||||
}
|
||||
|
||||
@keyframes zoomIn {
|
||||
0% {
|
||||
transform: translate(-50%, -50%) scale(0);
|
||||
}
|
||||
100% {
|
||||
transform: translate(-50%, -50%) scale(1);
|
||||
}
|
||||
}
|
||||
|
||||
.progress-bar:hover .progress-selector-active {
|
||||
display: block;
|
||||
}
|
||||
|
||||
.progress-hover {
|
||||
display: block;
|
||||
height: 100%;
|
||||
max-width: 100%;
|
||||
background: var(--quaternary);
|
||||
transition: left 0.1s ease;
|
||||
border-radius: 3px;
|
||||
}
|
||||
.multi-loader {
|
||||
position: relative;
|
||||
width: 100%;
|
||||
flex: 1;
|
||||
height: 6px;
|
||||
background: var(--tertiary);
|
||||
border-radius: 4px;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.multi-loader span {
|
||||
position: absolute;
|
||||
top: 0;
|
||||
width: 40px;
|
||||
height: 100%;
|
||||
background: var(--main);
|
||||
border-radius: 4px;
|
||||
animation: move 2.5s cubic-bezier(0.4, 0, 0.2, 1) infinite;
|
||||
}
|
||||
|
||||
@media screen and (max-width: 768px), screen and (max-height: 607px) {
|
||||
.multi-loader {
|
||||
background: var(--quaternary);
|
||||
}
|
||||
}
|
||||
|
||||
.multi-loader span:nth-child(1) { animation-delay: 0s; }
|
||||
.multi-loader span:nth-child(2) { animation-delay: 0.3s; }
|
||||
.multi-loader span:nth-child(3) { animation-delay: 0.6s; }
|
||||
.multi-loader span:nth-child(4) { animation-delay: 0.9s; }
|
||||
.multi-loader span:nth-child(5) { animation-delay: 1.2s; }
|
||||
|
||||
@keyframes move {
|
||||
0% { left: -50px; width: 0px; opacity: 0; }
|
||||
15% { opacity: 1; width: 40px; }
|
||||
50% { width: 80px; } /* plus long au milieu */
|
||||
85% { opacity: 1; width: 40px; }
|
||||
90% { left: 100%; width: 10px; opacity: 0; }
|
||||
100% { left: 100%; width: 0px; opacity: 0; }
|
||||
}
|
||||
</style>
|
Reference in New Issue
Block a user