Version 1.0.0 - Frontend
This commit is contained in:
246
src/components/Widget/VideoComposable.vue
Normal file
246
src/components/Widget/VideoComposable.vue
Normal file
@@ -0,0 +1,246 @@
|
||||
<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>
|
Reference in New Issue
Block a user