246 lines
8.0 KiB
Vue
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> |