Files
chopin-frontend/src/components/Widget/VideoComposable.vue
2025-08-29 00:22:08 +02:00

246 lines
8.0 KiB
Vue

<template>
<Video :video="video" ref="videoContainer">
<div ref="controls" class="controls">
<span v-if="globalStore.currentChannel" title="Ajouter à la liste de lecture" @click="disableAction(); playSong(false)" class="control-icon"><AddList /></span>
<span v-if="globalStore.currentChannel" title="Lire maintenant" @click="disableAction(); playSong(true)" class="control-icon"><Icon icon="fa-play" /></span>
<span v-if="!props.delete && video.type != 'attachment'" title="Enregistrer dans une playlist" @click="disableAction(); saveModal.open()" class="control-icon"><Icon icon="fa-save" /></span>
<span v-if="props.delete" title="Supprimer" @click="disableAction(); Events.emit('video:delete', { video: props.video })" class="control-icon"><Icon icon="fa-trash" /></span>
</div>
</Video>
<Modal icon="fa-solid fa-video" title="Actions" ref="modal">
<Video :video="video"/>
<Button v-if="globalStore.currentChannel" @click="playSong(false)"><AddList /> Ajouter à la liste de lecture</Button>
<Button v-if="globalStore.currentChannel" @click="playSong(true)"><Icon icon="fa-solid fa-play"/> Lire maintenant</Button>
<div v-else>
<p class="text-secondary">Connectez vous à un salon audio sur le serveur {{ globalStore.actualServer ? globalStore.actualServer.name : '' }}, pour lancer un titre</p>
<ActualChannel/>
</div>
<Button v-if="!props.delete && video.type != 'attachment'" @click="saveModal.open()"><Icon icon="fa-solid fa-save" /> Enregistrer dans une playlist</Button>
<Button v-if="props.delete" @click="Events.emit('video:delete', { video: props.video })"><Icon icon="fa-solid fa-trash" /> Supprimer</Button>
</Modal>
<Modal ref="saveModal" icon="fa-save" title="Sauvegarder dans une playlist">
<Video class="save-video" :video="video"/>
<Selector ref="playlistSelector" v-if="userStore.playlists?.length > 0">
<p class="selectorp" :value="playlist.playlistId" v-for="playlist in userStore.playlists" :key="playlist.playlistId"><Icon :icon="playlist.type === 'youtube' ? 'fa-brands fa-youtube' : 'fa-solid fa-music'" /> {{ playlist.title }}</p>
</Selector>
<p v-else class="info-no">Vous n'avez pas encore de playlist. Créez-en une pour sauvegarder ce titre.</p>
<Button :disabled="userStore.playlists?.length === 0" @click="Events.emit('video:add', { video: props.video, playlistId: playlistSelector.firstSlot().props.value }); saveModal.close()"><Icon icon="fa-solid fa-save" /> Sauvegarder</Button>
</Modal>
</template>
<script setup>
import { onMounted, onBeforeUnmount, ref } from 'vue';
import Modal from '@/components/UI/Modal.vue';
import Button from '@/components/UI/Button.vue';
import Video from '@/components/UI/Video.vue';
import Events from '@/utils/Events';
import Selector from '../UI/Selector.vue';
import { useUserStore } from '@/stores/userStore';
import { IORequest } from '@/utils/IORequest';
import { useGlobalStore } from '@/stores/globalStore';
import ActualChannel from './View/Player/ActualChannel.vue';
import AddList from '@/assets/Icons/AddList.vue';
const controls = ref(null)
const thumbnailContainer = ref(null)
const videoContainer = ref(null)
const modal = ref(null)
const saveModal = ref(null)
const userStore = useUserStore();
const playlistSelector = ref(null)
const globalStore = useGlobalStore();
const props = defineProps({
video: {
type: Object,
required: true
},
delete: {
type: Boolean,
default: false
}
});
let nativeVideo = {}
function disableAction() {
controls.value.style.display = "none";
}
let touchStartX = 0;
let touchStartY = 0;
let isSliding = false;
let activePointerId = null;
const SLIDE_THRESHOLD = 10; // ajuster si besoin
function playSong(now) {
IORequest("/SEARCH/PLAY", (data) => {
modal.value.close();
}, {song: nativeVideo, now: now})
}
onMounted(() => {
Object.assign(nativeVideo, props.video);
if(props.video.createdAt) {
props.video.author = 'Ajoutée le ' + new Date(props.video.createdAt).toLocaleString()
props.video.thumbnail = '/src/assets/default_thumbnail.png';
nativeVideo.author = userStore.userInfo.identity.username;
}
if(!videoContainer.value) return
thumbnailContainer.value = videoContainer.value.getThumbnailContainer();
videoContainer.value = videoContainer.value.getVideoContainer();
thumbnailContainer.value.addEventListener('pointerenter', (ev) => {
if(!ev) return;
if (ev.pointerType === 'mouse') {
controls.value.style.display = "flex";
}
});
thumbnailContainer.value.addEventListener('pointerleave', (ev) => {
if(!ev) return;
if (ev.pointerType === 'mouse') {
controls.value.style.display = "none";
}
});
const onPointerMove = (ev) => {
if (ev.pointerId !== activePointerId) return;
const dx = Math.abs(ev.clientX - touchStartX);
const dy = Math.abs(ev.clientY - touchStartY);
if (dx > SLIDE_THRESHOLD || dy > SLIDE_THRESHOLD) {
isSliding = true;
}
};
const cleanupPointerListeners = () => {
try {
if (activePointerId != null) videoContainer.value.releasePointerCapture(activePointerId);
} catch (e) { /* ignore */ }
if(!videoContainer.value) return
videoContainer.value.removeEventListener('pointermove', onPointerMove);
videoContainer.value.removeEventListener('pointerup', onPointerUp);
videoContainer.value.removeEventListener('pointercancel', onPointerCancel);
activePointerId = null;
};
const onPointerUp = (ev) => {
if (ev.pointerId !== activePointerId) return;
// fin du geste : si ce n'est pas un slide, on ouvre le modal
const wasSliding = isSliding;
cleanupPointerListeners();
isSliding = false;
if (!wasSliding) {
modal.value.open();
}
};
const onPointerCancel = (ev) => {
if (ev.pointerId !== activePointerId) return;
cleanupPointerListeners();
isSliding = false;
};
const onPointerDown = (ev) => {
if (!ev) return;
if (ev.pointerType !== 'touch') return; // on gère seulement le touch ici
touchStartX = ev.clientX;
touchStartY = ev.clientY;
isSliding = false;
activePointerId = ev.pointerId;
if(!videoContainer.value) return
try { videoContainer.value.setPointerCapture(activePointerId); } catch (e) { /* ignore */ }
videoContainer.value.addEventListener('pointermove', onPointerMove);
videoContainer.value.addEventListener('pointerup', onPointerUp);
videoContainer.value.addEventListener('pointercancel', onPointerCancel);
};
videoContainer.value.addEventListener('pointerdown', onPointerDown);
const onResize = () => {
if (modal.value) modal.value.close();
};
window.addEventListener('resize', onResize);
onBeforeUnmount(() => {
// cleanup
if(!videoContainer.value) return
window.removeEventListener('resize', onResize);
});
});
</script>
<style scoped>
.selectorp {
margin: 0;
display: flex;
align-items: center;
gap: 3px;
}
.info-no {
color: var(--text-secondary);
font-size: 0.8em;
text-align: center;
}
.save-video {
max-width: 50%;
margin-left: auto;
margin-right: auto;
}
.controls {
position: absolute;
display: none;
justify-content: center;
align-items: center;
gap: 25px;
width: 100%;
bottom: 32%;
animation: fadeInMap 0.2s ease-in-out;
}
.control-icon {
font-size: 1.2em;
background-color: var(--text);
color: var(--text-inverse);
border-radius: 100%;
width: 40px;
height: 40px;
display: flex;
align-items: center;
justify-content: center;
opacity: 0.8;
}
.video:hover .controls {
opacity: 1;
}
.text-secondary {
text-align: center;
color: var(--text-secondary);
font-size: 0.8em;
}
@keyframes fadeInMap {
0% {
opacity: 0;
gap: 5px;
}
100% {
opacity: 1;
gap: 25px;
}
}
</style>