Version 1.0.0 - Frontend

This commit is contained in:
2025-08-29 00:22:08 +02:00
parent b5dc2a9e37
commit 01b089f1f6
83 changed files with 5613 additions and 245 deletions

View 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>