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