Spaces:
Running
Running
<template> | |
<div | |
class="object-item" | |
:class="{ 'selected': isSelected }" | |
@click="toggleSelection" | |
> | |
<div class="object-id"> | |
<span>{{ objectId }}</span> | |
</div> | |
<div class="object-timeline" ref="timelineRef"> | |
<div class="timeline-line"></div> | |
<!-- Points pour chaque annotation --> | |
<div | |
v-for="(frameKey, index) in annotatedFrames" | |
:key="index" | |
class="annotation-point" | |
:style="{ | |
left: `${calculatePositionExact(parseInt(frameKey))}%`, | |
backgroundColor: getObjectColor | |
}" | |
:title="`Frame ${frameKey}`" | |
@click.stop="goToFrame(parseInt(frameKey))" | |
></div> | |
</div> | |
</div> | |
<!-- Popup de confirmation simplifiée --> | |
<div v-if="showDeleteConfirm" class="delete-overlay" @click="cancelDelete"> | |
<div class="delete-modal" @click.stop> | |
<h3>Supprimer {{ objectId }} ?</h3> | |
<p>Cette action est irréversible.</p> | |
<div class="delete-actions"> | |
<button class="btn-cancel" @click="cancelDelete">Annuler</button> | |
<button class="btn-delete" @click="confirmDelete">Supprimer</button> | |
</div> | |
</div> | |
</div> | |
</template> | |
<script> | |
import { useAnnotationStore } from '@/stores/annotationStore' | |
import { useVideoStore } from '@/stores/videoStore' | |
import { computed, ref, onMounted, onUnmounted } from 'vue' | |
export default { | |
name: 'ObjectItem', | |
props: { | |
objectId: { | |
type: String, | |
default: 'object1' | |
}, | |
colorIndex: { | |
type: Number, | |
default: 0 | |
} | |
}, | |
setup(props) { | |
const annotationStore = useAnnotationStore() | |
const videoStore = useVideoStore() | |
const timelineRef = ref(null) | |
const showDeleteConfirm = ref(false) | |
// Keyboard shortcut handler | |
const handleKeyDown = (event) => { | |
// Add new object when pressing 'N' key | |
if (event.key === 'n' || event.key === 'N') { | |
// Prevent default behavior (like typing 'n' in an input field) | |
event.preventDefault() | |
// Only process the event if this is the first object item | |
// This prevents multiple objects from being created when multiple ObjectItems exist | |
if (props.objectId !== Object.keys(annotationStore.objects)[0]) { | |
return; | |
} | |
// Check available methods and use the correct one | |
if (typeof annotationStore.addObject === 'function') { | |
annotationStore.addObject(); | |
} else if (typeof annotationStore.createNewObject === 'function') { | |
annotationStore.createNewObject(); | |
} else { | |
// Fallback: Create a new object ID based on the last object ID + 1 | |
const objectIds = Object.keys(annotationStore.objects); | |
let lastId = 0; | |
// Find the highest numeric ID | |
objectIds.forEach(id => { | |
// Extract numeric part from objectX format | |
const numericPart = parseInt(id.replace('object', '')); | |
if (!isNaN(numericPart) && numericPart > lastId) { | |
lastId = numericPart; | |
} | |
}); | |
// Create new object with ID = last ID + 1 | |
const newObjectId = `object${lastId + 1}`; | |
annotationStore.objects[newObjectId] = { | |
id: newObjectId, | |
color: annotationStore.getNextColor(), | |
// Add any other required properties | |
}; | |
console.log(`Created new object: ${newObjectId}`); | |
} | |
} | |
// Delete selected object when pressing Ctrl+Delete key | |
if (event.key === 'Delete' && event.ctrlKey && annotationStore.selectedObjectId === props.objectId) { | |
event.preventDefault() | |
showDeleteConfirm.value = true | |
} | |
// Fermer la popup avec Escape | |
if (event.key === 'Escape' && showDeleteConfirm.value) { | |
event.preventDefault() | |
showDeleteConfirm.value = false | |
} | |
} | |
// Add and remove event listeners | |
onMounted(() => { | |
window.addEventListener('keydown', handleKeyDown) | |
}) | |
onUnmounted(() => { | |
window.removeEventListener('keydown', handleKeyDown) | |
}) | |
// Vérifier si cet objet est actuellement sélectionné | |
const isSelected = computed(() => { | |
return annotationStore.selectedObjectId === props.objectId | |
}) | |
// Fonction pour basculer la sélection de l'objet | |
const toggleSelection = () => { | |
if (isSelected.value) { | |
annotationStore.deselectObject() | |
} else { | |
annotationStore.selectObject(props.objectId) | |
} | |
} | |
// Fonctions pour la popup de confirmation | |
const confirmDelete = () => { | |
annotationStore.deleteObject(props.objectId) | |
showDeleteConfirm.value = false | |
} | |
const cancelDelete = () => { | |
showDeleteConfirm.value = false | |
} | |
// Récupérer toutes les frames où cet objet a des annotations | |
const annotatedFrames = computed(() => { | |
const frames = [] | |
Object.keys(annotationStore.frameAnnotations).forEach(frameKey => { | |
const hasObjectAnnotation = annotationStore.frameAnnotations[frameKey].some( | |
annotation => annotation.objectId === props.objectId | |
) | |
if (hasObjectAnnotation) { | |
frames.push(frameKey) | |
} | |
}) | |
return frames | |
}) | |
// Obtenir la couleur de l'objet | |
const getObjectColor = computed(() => { | |
return annotationStore.objects[props.objectId]?.color || '#4CAF50' | |
}) | |
// Calculer la position en pourcentage pour une frame donnée | |
const calculatePositionExact = (frameNumber) => { | |
const frameRate = annotationStore.currentSession.frameRate || 30 | |
const timeInSeconds = frameNumber / frameRate | |
const videoDuration = videoStore.duration || videoStore.selectedVideo?.duration || 0 | |
if (!videoDuration || videoDuration <= 0) { | |
console.warn('Attention: Durée de vidéo non disponible, utilisation d\'une valeur par défaut') | |
return 0 // Ou retourner une position par défaut | |
} | |
return (timeInSeconds / videoDuration) * 100 | |
} | |
// Fonction pour naviguer vers une frame spécifique | |
const goToFrame = (frameNumber) => { | |
// Convertir le numéro de frame en temps (secondes) | |
const frameRate = annotationStore.currentSession.frameRate || 30 | |
// Utiliser une valeur exacte pour le temps en secondes | |
// Ajouter un petit décalage (0.001) pour éviter les problèmes d'arrondi | |
const timeInSeconds = frameNumber / frameRate + 0.001 | |
// Mettre à jour le temps courant dans le videoStore | |
videoStore.setCurrentTime(timeInSeconds) | |
// Utiliser la méthode seek si disponible | |
if (videoStore.seek) { | |
videoStore.seek(timeInSeconds) | |
} else { | |
// Fallback: essayer d'accéder directement à l'élément vidéo | |
const videoElement = document.querySelector('video') | |
if (videoElement) { | |
videoElement.currentTime = timeInSeconds | |
} | |
} | |
// Forcer la mise à jour de l'interface | |
videoStore.updateProgressBar(timeInSeconds) | |
} | |
return { | |
annotatedFrames, | |
calculatePositionExact, | |
getObjectColor, | |
timelineRef, | |
isSelected, | |
toggleSelection, | |
goToFrame, | |
showDeleteConfirm, | |
confirmDelete, | |
cancelDelete | |
} | |
} | |
} | |
</script> | |
<style scoped> | |
.object-item { | |
display: flex; | |
height: 24px; | |
margin-bottom: 18px; | |
align-items: center; | |
gap: 14px; | |
cursor: pointer; | |
transition: background-color 0.2s ease; | |
border-radius: 4px; | |
padding: 2px 4px; | |
position: relative; | |
} | |
.object-item:hover { | |
background-color: rgba(255, 255, 255, 0.1); | |
} | |
.object-item.selected { | |
background-color: rgba(255, 255, 255, 0.2); | |
box-shadow: 0 0 0 1px rgba(255, 255, 255, 0.5); | |
} | |
.object-id { | |
width: 35px; | |
font-weight: bold; | |
font-size: 0.9rem; | |
overflow: hidden; | |
text-overflow: ellipsis; | |
white-space: nowrap; | |
color: white; | |
} | |
.object-timeline { | |
flex-grow: 1; | |
height: 100%; | |
position: relative; | |
border-radius: 4px; | |
display: flex; | |
align-items: center; | |
border-color: white; | |
} | |
.timeline-line { | |
height: 1px; | |
width: 100%; | |
background-color: white; | |
} | |
.annotation-point { | |
position: absolute; | |
width: 8px; | |
height: 8px; | |
background-color: #4CAF50; | |
border-radius: 50%; | |
transform: translateX(-50%); | |
z-index: 2; | |
} | |
/* Popup de confirmation simplifiée */ | |
.delete-overlay { | |
position: fixed; | |
top: 0; | |
left: 0; | |
right: 0; | |
bottom: 0; | |
background-color: rgba(0, 0, 0, 0.6); | |
display: flex; | |
align-items: center; | |
justify-content: center; | |
z-index: 1000; | |
} | |
.delete-modal { | |
background: #2c2c2c; | |
border-radius: 8px; | |
padding: 20px; | |
max-width: 300px; | |
width: 90%; | |
text-align: center; | |
color: white; | |
} | |
.delete-modal h3 { | |
margin: 0 0 12px 0; | |
font-size: 1.1rem; | |
color: #fff; | |
} | |
.delete-modal p { | |
margin: 0 0 20px 0; | |
color: #ccc; | |
font-size: 0.9rem; | |
} | |
.delete-actions { | |
display: flex; | |
gap: 12px; | |
justify-content: center; | |
} | |
.btn-cancel, | |
.btn-delete { | |
padding: 8px 16px; | |
border: none; | |
border-radius: 4px; | |
font-size: 0.9rem; | |
cursor: pointer; | |
transition: all 0.2s ease; | |
} | |
.btn-cancel { | |
background: #555; | |
color: white; | |
} | |
.btn-cancel:hover { | |
background: #666; | |
} | |
.btn-delete { | |
background: #dc3545; | |
color: white; | |
} | |
.btn-delete:hover { | |
background: #c82333; | |
} | |
</style> |