2nzi commited on
Commit
6b7479e
·
verified ·
1 Parent(s): 4147607

update side bar

Browse files
Files changed (1) hide show
  1. src/components/SegmentationSidebar.vue +1086 -279
src/components/SegmentationSidebar.vue CHANGED
@@ -1,280 +1,1087 @@
1
- <template>
2
- <div class="segmentation-sidebar">
3
- <button class="upload-video-btn" @click="uploadVideo">
4
- <span>Upload Video</span>
5
- <svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor">
6
- <path d="M21 15v4a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2v-4"/>
7
- <path d="m17 8-5-5-5 5"/>
8
- <path d="M12 3v12"/>
9
- </svg>
10
- </button>
11
-
12
- <!-- Input file caché -->
13
- <input
14
- ref="fileInput"
15
- type="file"
16
- accept="video/*"
17
- @change="handleVideoUpload"
18
- style="display: none;"
19
- />
20
-
21
- <div class="video-list" v-if="videoStore.videos.length">
22
- <div
23
- v-for="video in videoStore.videos"
24
- :key="video.path"
25
- class="video-item"
26
- :class="{ active: videoStore.selectedVideo?.path === video.path }"
27
- @click="selectVideo(video)"
28
- >
29
- {{ video.name }}
30
- </div>
31
- </div>
32
-
33
- <!-- Sélecteur de FPS -->
34
- <div class="fps-selector">
35
- <select
36
- id="fps-select"
37
- v-model="selectedFps"
38
- @change="updateFps"
39
- class="fps-select"
40
- >
41
- <option value="15">15 FPS</option>
42
- <option value="24">24 FPS</option>
43
- <option value="25">25 FPS</option>
44
- <option value="30">30 FPS</option>
45
- <option value="50">50 FPS</option>
46
- <option value="60">60 FPS</option>
47
- <option value="120">120 FPS</option>
48
- </select>
49
- </div>
50
-
51
- </div>
52
- </template>
53
-
54
- <script>
55
- import { useVideoStore } from '../stores/videoStore'
56
-
57
- export default {
58
- name: 'SegmentationSidebar',
59
-
60
- data() {
61
- const videoStore = useVideoStore()
62
- return {
63
- videoStore,
64
- selectedFps: videoStore.fps || 25 // Utiliser la valeur du store ou 25 par défaut
65
- }
66
- },
67
-
68
- watch: {
69
- 'videoStore.selectedVideo': {
70
- handler(newVideo) {
71
- console.log('Vidéo sélectionnée:', newVideo)
72
- },
73
- deep: true
74
- }
75
- },
76
-
77
- mounted() {
78
- // Plus besoin de charger un dossier par défaut - l'utilisateur uploadera une vidéo
79
- console.log('SegmentationSidebar mounted - prêt pour l\'upload de vidéo')
80
- },
81
-
82
- methods: {
83
- uploadVideo() {
84
- // Déclencher le clic sur l'input file caché
85
- this.$refs.fileInput.click()
86
- },
87
-
88
- handleVideoUpload(event) {
89
- const file = event.target.files[0]
90
- if (file) {
91
- console.log('Vidéo sélectionnée:', file.name)
92
-
93
- // Créer un URL blob pour la vidéo
94
- const videoUrl = URL.createObjectURL(file)
95
-
96
- // Créer un objet vidéo pour le store
97
- const videoObject = {
98
- name: file.name,
99
- path: videoUrl,
100
- file: file,
101
- size: file.size,
102
- type: file.type
103
- }
104
-
105
- // Mettre à jour le store avec la nouvelle vidéo
106
- this.videoStore.setVideos([videoObject])
107
- this.videoStore.setSelectedVideo(videoObject)
108
-
109
- // Émettre l'événement pour informer les autres composants
110
- this.$emit('video-selected', videoObject)
111
-
112
- console.log('Vidéo uploadée et sélectionnée:', videoObject)
113
- }
114
- },
115
-
116
- selectVideo(video) {
117
- this.videoStore.setSelectedVideo(video)
118
- this.$emit('video-selected', video)
119
- },
120
-
121
- updateFps() {
122
- this.videoStore.setFps(parseInt(this.selectedFps))
123
- }
124
- }
125
- }
126
- </script>
127
-
128
- <style scoped>
129
- .segmentation-sidebar {
130
- background: #363636;
131
- height: 100%;
132
- width: 200px;
133
- padding: 16px;
134
- display: flex;
135
- flex-direction: column;
136
- gap: 16px;
137
- }
138
-
139
- .navigation-menu {
140
- display: flex;
141
- flex-direction: column;
142
- gap: 8px;
143
- margin-bottom: 8px;
144
- }
145
-
146
- .nav-item {
147
- display: flex;
148
- align-items: center;
149
- padding: 12px;
150
- color: white;
151
- text-decoration: none;
152
- border-radius: 8px;
153
- transition: all 0.2s ease;
154
- background: #424242;
155
- }
156
-
157
- .nav-item:hover {
158
- background: #4a4a4a;
159
- }
160
-
161
- .nav-item.active {
162
- background: #3a3a3a;
163
- color: #4CAF50;
164
- }
165
-
166
- .nav-icon {
167
- margin-right: 12px;
168
- font-size: 1.2rem;
169
- }
170
-
171
- .nav-text {
172
- font-size: 0.9rem;
173
- }
174
-
175
- .upload-video-btn {
176
- background: #424242;
177
- border: none;
178
- border-radius: 8px;
179
- color: white;
180
- padding: 12px 16px;
181
- cursor: pointer;
182
- display: flex;
183
- align-items: center;
184
- justify-content: space-between;
185
- width: 100%;
186
- font-size: 1rem;
187
- flex-shrink: 0;
188
- transition: background-color 0.2s ease;
189
- }
190
-
191
- .upload-video-btn:hover {
192
- background: #4a4a4a;
193
- }
194
-
195
- .video-list {
196
- height: 20vh;
197
- background: #424242;
198
- border-radius: 8px;
199
- overflow-y: auto;
200
- display: flex;
201
- flex-direction: column;
202
- gap: 4px;
203
- padding: 4px;
204
- }
205
-
206
- /* Styles pour Firefox */
207
- .video-list {
208
- scrollbar-width: thin;
209
- scrollbar-color: rgba(255, 255, 255, 0.3) transparent;
210
- }
211
-
212
- /* Styles pour Chrome/Safari/Edge */
213
- .video-list::-webkit-scrollbar {
214
- width: 4px;
215
- }
216
-
217
- .video-list::-webkit-scrollbar-track {
218
- background: transparent;
219
- }
220
-
221
- .video-list::-webkit-scrollbar-thumb {
222
- background: rgba(255, 255, 255, 0.3);
223
- border-radius: 2px;
224
- }
225
-
226
- .video-item {
227
- padding: 8px 12px;
228
- border-radius: 4px;
229
- cursor: pointer;
230
- color: white;
231
- transition: background-color 0.2s;
232
- }
233
-
234
- .video-item:hover {
235
- background: #4a4a4a;
236
- }
237
-
238
- .video-item.active {
239
- background: #3a3a3a;
240
- }
241
-
242
- .fps-selector {
243
- display: flex;
244
- flex-direction: column;
245
- gap: 8px;
246
- padding: 12px;
247
- border-radius: 8px;
248
- }
249
-
250
- .fps-label {
251
- color: white;
252
- font-size: 0.9rem;
253
- font-weight: 500;
254
- }
255
-
256
- .fps-select {
257
- background: #363636;
258
- border: 1px solid #555;
259
- border-radius: 4px;
260
- color: white;
261
- padding: 8px;
262
- font-size: 0.9rem;
263
- cursor: pointer;
264
- transition: border-color 0.2s ease;
265
- }
266
-
267
- .fps-select:hover {
268
- border-color: #777;
269
- }
270
-
271
- .fps-select:focus {
272
- outline: none;
273
- border-color: #4CAF50;
274
- }
275
-
276
- .fps-select option {
277
- background: #363636;
278
- color: white;
279
- }
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
280
  </style>
 
1
+ <template>
2
+ <div class="segmentation-sidebar">
3
+ <!-- Navigation header -->
4
+ <div class="sidebar-header">
5
+ <div class="view-navigation">
6
+ <button
7
+ class="nav-button"
8
+ @click="previousView"
9
+ >
10
+ <svg width="16" height="16" viewBox="0 0 24 24" fill="currentColor">
11
+ <path d="M15.41 7.41L14 6l-6 6 6 6 1.41-1.41L10.83 12z"/>
12
+ </svg>
13
+ </button>
14
+
15
+ <span class="view-indicator">
16
+ {{ currentViewIndex + 1 }} / {{ views.length }}
17
+ </span>
18
+
19
+ <button
20
+ class="nav-button"
21
+ @click="nextView"
22
+ >
23
+ <svg width="16" height="16" viewBox="0 0 24 24" fill="currentColor">
24
+ <path d="M10 6L8.59 7.41 13.17 12l-4.58 4.59L10 18l6-6z"/>
25
+ </svg>
26
+ </button>
27
+ </div>
28
+ </div>
29
+
30
+ <!-- Content area with transitions -->
31
+ <div class="sidebar-content">
32
+ <transition :name="transitionName" mode="out-in">
33
+ <div :key="currentViewIndex" class="view-content">
34
+ <!-- Page 1: Video Upload and FPS -->
35
+ <div v-if="currentViewIndex === 0" class="video-upload-page">
36
+ <button class="upload-video-btn" @click="uploadVideo">
37
+ <span>Upload Video</span>
38
+ <svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor">
39
+ <path d="M21 15v4a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2v-4"/>
40
+ <path d="m17 8-5-5-5 5"/>
41
+ <path d="M12 3v12"/>
42
+ </svg>
43
+ </button>
44
+
45
+ <!-- Input file caché pour vidéo -->
46
+ <input
47
+ ref="videoFileInput"
48
+ type="file"
49
+ accept="video/*"
50
+ @change="handleVideoUpload"
51
+ style="display: none;"
52
+ />
53
+
54
+ <div class="video-list" v-if="videoStore.videos.length">
55
+ <div
56
+ v-for="video in videoStore.videos"
57
+ :key="video.path"
58
+ class="video-item"
59
+ :class="{ active: videoStore.selectedVideo?.path === video.path }"
60
+ @click="selectVideo(video)"
61
+ >
62
+ {{ video.name }}
63
+ </div>
64
+ </div>
65
+
66
+ <!-- Sélecteur de FPS -->
67
+ <div class="fps-selector">
68
+ <select
69
+ id="fps-select"
70
+ v-model="selectedFps"
71
+ @change="updateFps"
72
+ class="fps-select"
73
+ >
74
+ <option value="15">15 FPS</option>
75
+ <option value="24">24 FPS</option>
76
+ <option value="25">25 FPS</option>
77
+ <option value="30">30 FPS</option>
78
+ <option value="50">50 FPS</option>
79
+ <option value="60">60 FPS</option>
80
+ <option value="120">120 FPS</option>
81
+ </select>
82
+ </div>
83
+ </div>
84
+
85
+ <!-- Page 2: Config Upload -->
86
+ <div v-if="currentViewIndex === 1" class="config-upload-page">
87
+ <button class="upload-config-btn" @click="uploadConfig">
88
+ <span>Upload Config</span>
89
+ <svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor">
90
+ <path d="M21 15v4a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2v-4"/>
91
+ <path d="m17 8-5-5-5 5"/>
92
+ <path d="M12 3v12"/>
93
+ </svg>
94
+ </button>
95
+
96
+ <!-- Input file caché pour config -->
97
+ <input
98
+ ref="configFileInput"
99
+ type="file"
100
+ accept=".json,.yaml,.yml,.txt"
101
+ @change="handleConfigUpload"
102
+ style="display: none;"
103
+ />
104
+
105
+ <div class="config-info" v-if="uploadedConfig">
106
+ <div class="config-item">
107
+ <strong>Config loaded:</strong> {{ uploadedConfig.name }}
108
+ </div>
109
+ <div class="config-item" v-if="configInfo">
110
+ <strong>Objects found:</strong> {{ configInfo.objectCount }}
111
+ </div>
112
+ <div class="config-item" v-if="configInfo">
113
+ <strong>Frames with annotations:</strong> {{ configInfo.frameCount }}
114
+ </div>
115
+ <div class="config-item" v-if="configInfo">
116
+ <strong>Total annotations:</strong> {{ configInfo.annotationCount }}
117
+ </div>
118
+ <div class="config-actions">
119
+ <button
120
+ class="load-config-btn"
121
+ @click="loadConfigData"
122
+ :disabled="!uploadedConfig"
123
+ >
124
+ Load Annotations
125
+ </button>
126
+ <button
127
+ class="clear-config-btn"
128
+ @click="clearConfigData"
129
+ :disabled="!configInfo"
130
+ >
131
+ Clear
132
+ </button>
133
+ </div>
134
+ </div>
135
+ </div>
136
+
137
+ <!-- Page 3: CSV Analysis -->
138
+ <div v-if="currentViewIndex === 2" class="csv-analysis-page">
139
+ <button class="upload-csv-btn" @click="uploadCSV">
140
+ <span>Upload CSV</span>
141
+ <svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor">
142
+ <path d="M21 15v4a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2v-4"/>
143
+ <path d="m17 8-5-5-5 5"/>
144
+ <path d="M12 3v12"/>
145
+ </svg>
146
+ </button>
147
+
148
+ <!-- Input file caché pour CSV -->
149
+ <input
150
+ ref="csvFileInput"
151
+ type="file"
152
+ accept=".csv"
153
+ @change="handleCSVUpload"
154
+ style="display: none;"
155
+ />
156
+
157
+ <div class="csv-controls" v-if="uploadedCSV && csvColumns.length">
158
+ <div class="control-group">
159
+ <label>Row Column:</label>
160
+ <select v-model="selectedRowColumn" @change="updateFilteredTimestamps">
161
+ <option v-for="column in csvColumns" :key="column" :value="column">
162
+ {{ column }}
163
+ </option>
164
+ </select>
165
+ </div>
166
+
167
+ <div class="control-group">
168
+ <label>Timestamp Column:</label>
169
+ <select v-model="selectedTimestampColumn" @change="updateFilteredTimestamps">
170
+ <option v-for="column in csvColumns" :key="column" :value="column">
171
+ {{ column }}
172
+ </option>
173
+ </select>
174
+ </div>
175
+
176
+ <div class="control-group">
177
+ <label>Filter Keyword:</label>
178
+ <input
179
+ type="text"
180
+ v-model="filterKeyword"
181
+ @input="onFilterInput"
182
+ @keydown="onFilterKeydown"
183
+ @keyup="onFilterKeyup"
184
+ @paste="onFilterPaste"
185
+ @focus="onFilterFocus"
186
+ @blur="onFilterBlur"
187
+ placeholder="Enter keyword to filter"
188
+ />
189
+ </div>
190
+ </div>
191
+
192
+ <div class="csv-results" v-if="filteredTimestamps.length">
193
+ <h4>Filtered Timestamps:</h4>
194
+ <div class="timestamp-list">
195
+ <div
196
+ v-for="(timestamp, index) in filteredTimestamps"
197
+ :key="index"
198
+ class="timestamp-item"
199
+ @click="gotoTimestamp(timestamp)"
200
+ >
201
+ <span class="timestamp-value">{{ timestamp }}</span>
202
+ </div>
203
+ </div>
204
+ </div>
205
+
206
+ <div class="csv-info" v-if="uploadedCSV">
207
+ <div class="csv-item">
208
+ <strong>CSV loaded:</strong> {{ uploadedCSV.name }}
209
+ </div>
210
+ <div class="csv-item" v-if="csvData">
211
+ <strong>Total rows:</strong> {{ csvData.length }}
212
+ </div>
213
+ <div class="csv-item" v-if="csvColumns.length">
214
+ <strong>Columns:</strong> {{ csvColumns.join(', ') }}
215
+ </div>
216
+ <div class="csv-item" v-if="filteredTimestamps.length">
217
+ <strong>Filtered results:</strong> {{ filteredTimestamps.length }}
218
+ </div>
219
+ </div>
220
+ </div>
221
+ </div>
222
+ </transition>
223
+ </div>
224
+ </div>
225
+ </template>
226
+
227
+ <script>
228
+ import { useVideoStore } from '../stores/videoStore'
229
+ import { useAnnotationStore } from '../stores/annotationStore'
230
+
231
+ export default {
232
+ name: 'SegmentationSidebar',
233
+
234
+ data() {
235
+ const videoStore = useVideoStore()
236
+ const annotationStore = useAnnotationStore()
237
+ return {
238
+ videoStore,
239
+ annotationStore,
240
+ selectedFps: videoStore.fps || 25,
241
+ currentViewIndex: 0,
242
+ transitionName: 'slide-right',
243
+ uploadedConfig: null,
244
+ configInfo: null,
245
+ // CSV Analysis data
246
+ uploadedCSV: null,
247
+ csvData: null,
248
+ csvColumns: [],
249
+ selectedRowColumn: 'Row',
250
+ selectedTimestampColumn: 'Start time',
251
+ filterKeyword: 'Possession',
252
+ filteredTimestamps: [],
253
+ views: [
254
+ {
255
+ name: 'Video Upload',
256
+ component: 'VideoUploadView'
257
+ },
258
+ {
259
+ name: 'Config Upload',
260
+ component: 'ConfigUploadView'
261
+ },
262
+ {
263
+ name: 'CSV Analysis',
264
+ component: 'CSVAnalysisView'
265
+ }
266
+ ]
267
+ }
268
+ },
269
+
270
+ computed: {
271
+ currentView() {
272
+ return this.views[this.currentViewIndex]
273
+ }
274
+ },
275
+
276
+ watch: {
277
+ 'videoStore.selectedVideo': {
278
+ handler(newVideo) {
279
+ console.log('Vidéo sélectionnée:', newVideo)
280
+ },
281
+ deep: true
282
+ },
283
+ 'filterKeyword': {
284
+ handler() {
285
+ this.updateFilteredTimestamps()
286
+ }
287
+ }
288
+ },
289
+
290
+ mounted() {
291
+ console.log('SegmentationSidebar mounted - prêt pour l\'upload de vidéo')
292
+ },
293
+
294
+ methods: {
295
+ // Navigation methods
296
+ nextView() {
297
+ this.transitionName = 'slide-right'
298
+ this.currentViewIndex = (this.currentViewIndex + 1) % this.views.length
299
+ },
300
+
301
+ previousView() {
302
+ this.transitionName = 'slide-left'
303
+ this.currentViewIndex = this.currentViewIndex === 0
304
+ ? this.views.length - 1
305
+ : this.currentViewIndex - 1
306
+ },
307
+
308
+ // Video upload methods
309
+ uploadVideo() {
310
+ this.$refs.videoFileInput.click()
311
+ },
312
+
313
+ handleVideoUpload(event) {
314
+ const file = event.target.files[0]
315
+ if (file) {
316
+ console.log('Vidéo sélectionnée:', file.name)
317
+
318
+ // Créer un URL blob pour la vidéo
319
+ const videoUrl = URL.createObjectURL(file)
320
+
321
+ // Créer un objet vidéo pour le store
322
+ const videoObject = {
323
+ name: file.name,
324
+ path: videoUrl,
325
+ file: file,
326
+ size: file.size,
327
+ type: file.type
328
+ }
329
+
330
+ // Mettre à jour le store avec la nouvelle vidéo
331
+ this.videoStore.setVideos([videoObject])
332
+ this.videoStore.setSelectedVideo(videoObject)
333
+
334
+ // Émettre l'événement pour informer les autres composants
335
+ this.$emit('video-selected', videoObject)
336
+
337
+ console.log('Vidéo uploadée et sélectionnée:', videoObject)
338
+ }
339
+ },
340
+
341
+ selectVideo(video) {
342
+ this.videoStore.setSelectedVideo(video)
343
+ this.$emit('video-selected', video)
344
+ },
345
+
346
+ updateFps() {
347
+ this.videoStore.setFps(parseInt(this.selectedFps))
348
+ },
349
+
350
+ // Config upload methods
351
+ uploadConfig() {
352
+ this.$refs.configFileInput.click()
353
+ },
354
+
355
+ handleConfigUpload(event) {
356
+ const file = event.target.files[0]
357
+ if (file) {
358
+ console.log('Config sélectionnée:', file.name)
359
+
360
+ // Lire le contenu du fichier
361
+ const reader = new FileReader()
362
+ reader.onload = (e) => {
363
+ try {
364
+ const configContent = e.target.result
365
+ this.uploadedConfig = {
366
+ name: file.name,
367
+ content: configContent,
368
+ size: file.size,
369
+ type: file.type
370
+ }
371
+
372
+ // Parser le contenu pour extraire les informations
373
+ this.parseConfigContent(configContent)
374
+
375
+ // Émettre l'événement pour informer les autres composants
376
+ this.$emit('config-uploaded', this.uploadedConfig)
377
+
378
+ console.log('Config uploadée:', this.uploadedConfig)
379
+ } catch (error) {
380
+ console.error('Erreur lors de la lecture du fichier config:', error)
381
+ }
382
+ }
383
+ reader.readAsText(file)
384
+ }
385
+ },
386
+
387
+ parseConfigContent(content) {
388
+ try {
389
+ let configData
390
+
391
+ // Essayer de parser comme JSON
392
+ if (content.trim().startsWith('{') || content.trim().startsWith('[')) {
393
+ configData = JSON.parse(content)
394
+ } else {
395
+ // Essayer de parser comme YAML (format simple)
396
+ configData = this.parseYamlLike(content)
397
+ }
398
+
399
+ // Analyser les données pour extraire les informations
400
+ this.configInfo = this.analyzeConfigData(configData)
401
+
402
+ console.log('Config parsée:', configData)
403
+ console.log('Informations extraites:', this.configInfo)
404
+
405
+ } catch (error) {
406
+ console.error('Erreur lors du parsing du fichier config:', error)
407
+ this.configInfo = null
408
+ }
409
+ },
410
+
411
+ parseYamlLike(content) {
412
+ // Parser simple pour les fichiers YAML-like
413
+ const lines = content.split('\n')
414
+ const result = {}
415
+ let currentSection = null
416
+
417
+ for (const line of lines) {
418
+ const trimmedLine = line.trim()
419
+ if (!trimmedLine || trimmedLine.startsWith('#')) continue
420
+
421
+ if (trimmedLine.endsWith(':')) {
422
+ currentSection = trimmedLine.slice(0, -1)
423
+ result[currentSection] = {}
424
+ } else if (currentSection && trimmedLine.includes(':')) {
425
+ const [key, value] = trimmedLine.split(':', 2)
426
+ result[currentSection][key.trim()] = value.trim()
427
+ }
428
+ }
429
+
430
+ return result
431
+ },
432
+
433
+ analyzeConfigData(data) {
434
+ let objectCount = 0
435
+ let frameCount = 0
436
+ let annotationCount = 0
437
+
438
+ // Analyser les objets
439
+ if (data.objects) {
440
+ objectCount = Object.keys(data.objects).length
441
+ }
442
+
443
+ // Analyser les annotations initiales par frame
444
+ if (data.initial_annotations) {
445
+ frameCount = Object.keys(data.initial_annotations).length
446
+ annotationCount = Object.values(data.initial_annotations).reduce((total, frameAnnotations) => {
447
+ return total + (Array.isArray(frameAnnotations) ? frameAnnotations.length : 0)
448
+ }, 0)
449
+ }
450
+
451
+ return {
452
+ objectCount,
453
+ frameCount,
454
+ annotationCount,
455
+ rawData: data
456
+ }
457
+ },
458
+
459
+ loadConfigData() {
460
+ if (!this.configInfo || !this.configInfo.rawData) {
461
+ console.error('Aucune donnée de configuration à charger')
462
+ return
463
+ }
464
+
465
+ try {
466
+ const data = this.configInfo.rawData
467
+
468
+ // Charger les objets
469
+ if (data.objects) {
470
+ this.annotationStore.objects = { ...data.objects }
471
+ console.log('Objets chargés:', this.annotationStore.objects)
472
+ }
473
+
474
+ // Charger les annotations initiales
475
+ if (data.initial_annotations) {
476
+ this.annotationStore.frameAnnotations = { ...data.initial_annotations }
477
+ console.log('Annotations initiales chargées:', this.annotationStore.frameAnnotations)
478
+ }
479
+
480
+ // Mettre à jour les métadonnées si disponibles
481
+ if (data.metadata) {
482
+ this.annotationStore.updateVideoMetadata(data.metadata)
483
+ console.log('Métadonnées mises à jour:', data.metadata)
484
+ }
485
+
486
+ // Mettre à jour le compteur d'objets
487
+ if (data.objects) {
488
+ const maxId = Math.max(...Object.keys(data.objects).map(id => parseInt(id)), 0)
489
+ this.annotationStore.objectIdCounter = maxId + 1
490
+ }
491
+
492
+ console.log('Configuration chargée avec succès!')
493
+ this.$emit('config-loaded', data)
494
+
495
+ } catch (error) {
496
+ console.error('Erreur lors du chargement de la configuration:', error)
497
+ }
498
+ },
499
+
500
+ clearConfigData() {
501
+ this.uploadedConfig = null
502
+ this.configInfo = null
503
+ console.log('Données de configuration effacées')
504
+ },
505
+
506
+ // CSV upload and analysis methods
507
+ uploadCSV() {
508
+ this.$refs.csvFileInput.click()
509
+ },
510
+
511
+ // Filter event handlers
512
+ onFilterInput() {
513
+ this.updateFilteredTimestamps()
514
+ },
515
+
516
+ onFilterKeydown(event) {
517
+ // Empêcher la propagation pour éviter les conflits avec d'autres raccourcis
518
+ event.stopPropagation()
519
+ },
520
+
521
+ onFilterKeyup(event) {
522
+ // Empêcher la propagation pour éviter les conflits avec d'autres raccourcis
523
+ event.stopPropagation()
524
+ this.updateFilteredTimestamps()
525
+ },
526
+
527
+ onFilterPaste() {
528
+ // Laisser le paste se faire naturellement, puis mettre à jour
529
+ setTimeout(() => {
530
+ this.updateFilteredTimestamps()
531
+ }, 0)
532
+ },
533
+
534
+ onFilterFocus(event) {
535
+ // S'assurer que l'input capture tous les événements clavier
536
+ event.target.setAttribute('data-focused', 'true')
537
+ },
538
+
539
+ onFilterBlur(event) {
540
+ event.target.removeAttribute('data-focused')
541
+ },
542
+
543
+ handleCSVUpload(event) {
544
+ const file = event.target.files[0]
545
+ if (file) {
546
+ console.log('CSV sélectionné:', file.name)
547
+
548
+ // Lire le contenu du fichier
549
+ const reader = new FileReader()
550
+ reader.onload = (e) => {
551
+ try {
552
+ const csvContent = e.target.result
553
+ this.uploadedCSV = {
554
+ name: file.name,
555
+ content: csvContent,
556
+ size: file.size,
557
+ type: file.type
558
+ }
559
+
560
+ // Parser le contenu CSV
561
+ this.parseCSVContent(csvContent)
562
+
563
+ // Émettre l'événement pour informer les autres composants
564
+ this.$emit('csv-uploaded', this.uploadedCSV)
565
+
566
+ console.log('CSV uploadé:', this.uploadedCSV)
567
+ } catch (error) {
568
+ console.error('Erreur lors de la lecture du fichier CSV:', error)
569
+ }
570
+ }
571
+ reader.readAsText(file)
572
+ }
573
+ },
574
+
575
+ parseCSVContent(content) {
576
+ try {
577
+ const lines = content.split('\n').filter(line => line.trim())
578
+ if (lines.length === 0) {
579
+ console.error('Fichier CSV vide')
580
+ return
581
+ }
582
+
583
+ // Parser l'en-tête pour obtenir les colonnes
584
+ const header = lines[0].split(',').map(col => col.trim().replace(/"/g, ''))
585
+ this.csvColumns = header
586
+
587
+ // Parser les données
588
+ this.csvData = []
589
+ for (let i = 1; i < lines.length; i++) {
590
+ const values = lines[i].split(',').map(val => val.trim().replace(/"/g, ''))
591
+ const row = {}
592
+ header.forEach((col, index) => {
593
+ row[col] = values[index] || ''
594
+ })
595
+ this.csvData.push(row)
596
+ }
597
+
598
+ console.log('CSV parsé:', this.csvData)
599
+ console.log('Colonnes:', this.csvColumns)
600
+
601
+ // Mettre à jour les timestamps filtrés
602
+ this.updateFilteredTimestamps()
603
+
604
+ } catch (error) {
605
+ console.error('Erreur lors du parsing du fichier CSV:', error)
606
+ this.csvData = null
607
+ this.csvColumns = []
608
+ }
609
+ },
610
+
611
+ updateFilteredTimestamps() {
612
+ if (!this.csvData || !this.selectedRowColumn || !this.selectedTimestampColumn) {
613
+ this.filteredTimestamps = []
614
+ return
615
+ }
616
+
617
+ this.filteredTimestamps = this.csvData
618
+ .filter(row => {
619
+ const rowValue = row[this.selectedRowColumn] || ''
620
+ return rowValue.toLowerCase().includes(this.filterKeyword.toLowerCase())
621
+ })
622
+ .map(row => row[this.selectedTimestampColumn])
623
+ .filter(timestamp => timestamp && timestamp.trim())
624
+ .sort((a, b) => {
625
+ // Convertir les timestamps en secondes pour le tri
626
+ const secondsA = this.parseTimestamp(a)
627
+ const secondsB = this.parseTimestamp(b)
628
+
629
+ // Si les deux timestamps sont valides, les trier
630
+ if (secondsA !== null && secondsB !== null) {
631
+ return secondsA - secondsB
632
+ }
633
+
634
+ // Si un seul est valide, le mettre en premier
635
+ if (secondsA !== null) return -1
636
+ if (secondsB !== null) return 1
637
+
638
+ // Sinon, trier alphabétiquement
639
+ return a.localeCompare(b)
640
+ })
641
+ },
642
+
643
+ gotoTimestamp(timestamp) {
644
+ // Convertir le timestamp en secondes et naviguer vers cette position
645
+ const seconds = this.parseTimestamp(timestamp)
646
+ if (seconds !== null) {
647
+ this.videoStore.setCurrentTime(seconds)
648
+ }
649
+ },
650
+
651
+ parseTimestamp(timestamp) {
652
+ // Parser différents formats de timestamp
653
+ if (!timestamp) return null
654
+
655
+ // Format MM:SS ou HH:MM:SS
656
+ const timeParts = timestamp.split(':')
657
+ if (timeParts.length === 2) {
658
+ // Format MM:SS
659
+ const minutes = parseInt(timeParts[0])
660
+ const seconds = parseInt(timeParts[1])
661
+ return minutes * 60 + seconds
662
+ } else if (timeParts.length === 3) {
663
+ // Format HH:MM:SS
664
+ const hours = parseInt(timeParts[0])
665
+ const minutes = parseInt(timeParts[1])
666
+ const seconds = parseInt(timeParts[2])
667
+ return hours * 3600 + minutes * 60 + seconds
668
+ }
669
+
670
+ // Essayer de parser comme nombre de secondes
671
+ const seconds = parseFloat(timestamp)
672
+ if (!isNaN(seconds)) {
673
+ return seconds
674
+ }
675
+
676
+ return null
677
+ }
678
+ }
679
+ }
680
+ </script>
681
+
682
+ <style scoped>
683
+ .segmentation-sidebar {
684
+ background: #363636;
685
+ height: 100%;
686
+ width: 200px;
687
+ display: flex;
688
+ flex-direction: column;
689
+ overflow: hidden;
690
+ }
691
+
692
+ .sidebar-header {
693
+ padding: 8px 12px;
694
+ background: #3c3c3c;
695
+ border-bottom: 1px solid #4a4a4a;
696
+ display: flex;
697
+ justify-content: center;
698
+ align-items: center;
699
+ }
700
+
701
+ .view-navigation {
702
+ display: flex;
703
+ align-items: center;
704
+ gap: 8px;
705
+ }
706
+
707
+ .nav-button {
708
+ background: #4a4a4a;
709
+ border: none;
710
+ border-radius: 4px;
711
+ color: white;
712
+ width: 24px;
713
+ height: 24px;
714
+ display: flex;
715
+ align-items: center;
716
+ justify-content: center;
717
+ cursor: pointer;
718
+ transition: all 0.2s;
719
+ }
720
+
721
+ .nav-button:hover:not(:disabled) {
722
+ background: #5a5a5a;
723
+ }
724
+
725
+ .view-indicator {
726
+ color: #ccc;
727
+ font-size: 0.8rem;
728
+ min-width: 40px;
729
+ text-align: center;
730
+ }
731
+
732
+ .sidebar-content {
733
+ flex: 1;
734
+ position: relative;
735
+ overflow: hidden;
736
+ padding: 16px;
737
+ }
738
+
739
+ .view-content {
740
+ position: absolute;
741
+ top: 0;
742
+ left: 0;
743
+ right: 0;
744
+ bottom: 0;
745
+ padding: 16px;
746
+ display: flex;
747
+ flex-direction: column;
748
+ gap: 16px;
749
+ }
750
+
751
+ /* Video Upload Page Styles */
752
+ .video-upload-page {
753
+ display: flex;
754
+ flex-direction: column;
755
+ gap: 16px;
756
+ height: 100%;
757
+ }
758
+
759
+ .upload-video-btn {
760
+ background: #424242;
761
+ border: none;
762
+ border-radius: 8px;
763
+ color: white;
764
+ padding: 12px 16px;
765
+ cursor: pointer;
766
+ display: flex;
767
+ align-items: center;
768
+ justify-content: space-between;
769
+ width: 100%;
770
+ font-size: 1rem;
771
+ flex-shrink: 0;
772
+ transition: background-color 0.2s ease;
773
+ }
774
+
775
+ .upload-video-btn:hover {
776
+ background: #4a4a4a;
777
+ }
778
+
779
+ .video-list {
780
+ height: 20vh;
781
+ background: #424242;
782
+ border-radius: 8px;
783
+ overflow-y: auto;
784
+ display: flex;
785
+ flex-direction: column;
786
+ gap: 4px;
787
+ padding: 4px;
788
+ }
789
+
790
+ /* Styles pour Firefox */
791
+ .video-list {
792
+ scrollbar-width: thin;
793
+ scrollbar-color: rgba(255, 255, 255, 0.3) transparent;
794
+ }
795
+
796
+ /* Styles pour Chrome/Safari/Edge */
797
+ .video-list::-webkit-scrollbar {
798
+ width: 4px;
799
+ }
800
+
801
+ .video-list::-webkit-scrollbar-track {
802
+ background: transparent;
803
+ }
804
+
805
+ .video-list::-webkit-scrollbar-thumb {
806
+ background: rgba(255, 255, 255, 0.3);
807
+ border-radius: 2px;
808
+ }
809
+
810
+ .video-item {
811
+ padding: 8px 12px;
812
+ border-radius: 4px;
813
+ cursor: pointer;
814
+ color: white;
815
+ transition: background-color 0.2s;
816
+ }
817
+
818
+ .video-item:hover {
819
+ background: #4a4a4a;
820
+ }
821
+
822
+ .video-item.active {
823
+ background: #3a3a3a;
824
+ }
825
+
826
+ .fps-selector {
827
+ display: flex;
828
+ flex-direction: column;
829
+ gap: 8px;
830
+ padding: 12px;
831
+ border-radius: 8px;
832
+ }
833
+
834
+ .fps-select {
835
+ background: #363636;
836
+ border: 1px solid #555;
837
+ border-radius: 4px;
838
+ color: white;
839
+ padding: 8px 12px;
840
+ font-size: 0.9rem;
841
+ width: 100%;
842
+ }
843
+
844
+ .fps-select:focus {
845
+ outline: none;
846
+ border-color: #4CAF50;
847
+ }
848
+
849
+ /* Config Upload Page Styles */
850
+ .config-upload-page {
851
+ display: flex;
852
+ flex-direction: column;
853
+ gap: 16px;
854
+ height: 100%;
855
+ }
856
+
857
+ .upload-config-btn {
858
+ background: #424242;
859
+ border: none;
860
+ border-radius: 8px;
861
+ color: white;
862
+ padding: 12px 16px;
863
+ cursor: pointer;
864
+ display: flex;
865
+ align-items: center;
866
+ justify-content: space-between;
867
+ width: 100%;
868
+ font-size: 1rem;
869
+ flex-shrink: 0;
870
+ transition: background-color 0.2s ease;
871
+ }
872
+
873
+ .upload-config-btn:hover {
874
+ background: #4a4a4a;
875
+ }
876
+
877
+ .config-info {
878
+ background: #424242;
879
+ border-radius: 8px;
880
+ padding: 12px;
881
+ color: white;
882
+ }
883
+
884
+ .config-item {
885
+ font-size: 0.9rem;
886
+ margin-bottom: 8px;
887
+ color: #ccc;
888
+ }
889
+
890
+ .config-actions {
891
+ display: flex;
892
+ flex-direction: column;
893
+ gap: 8px;
894
+ margin-top: 12px;
895
+ }
896
+
897
+ .load-config-btn, .clear-config-btn {
898
+ background: #4CAF50;
899
+ border: none;
900
+ border-radius: 4px;
901
+ color: white;
902
+ padding: 8px 12px;
903
+ cursor: pointer;
904
+ font-size: 0.9rem;
905
+ transition: background-color 0.2s ease;
906
+ }
907
+
908
+ .load-config-btn:hover:not(:disabled) {
909
+ background: #45a049;
910
+ }
911
+
912
+ .load-config-btn:disabled {
913
+ background: #666;
914
+ cursor: not-allowed;
915
+ }
916
+
917
+ .clear-config-btn {
918
+ background: #f44336;
919
+ }
920
+
921
+ .clear-config-btn:hover:not(:disabled) {
922
+ background: #da190b;
923
+ }
924
+
925
+ .clear-config-btn:disabled {
926
+ background: #666;
927
+ cursor: not-allowed;
928
+ }
929
+
930
+ /* CSV Analysis Page Styles */
931
+ .csv-analysis-page {
932
+ display: flex;
933
+ flex-direction: column;
934
+ gap: 16px;
935
+ height: 100%;
936
+ }
937
+
938
+ .upload-csv-btn {
939
+ background: #424242;
940
+ border: none;
941
+ border-radius: 8px;
942
+ color: white;
943
+ padding: 12px 16px;
944
+ cursor: pointer;
945
+ display: flex;
946
+ align-items: center;
947
+ justify-content: space-between;
948
+ width: 100%;
949
+ font-size: 1rem;
950
+ flex-shrink: 0;
951
+ transition: background-color 0.2s ease;
952
+ }
953
+
954
+ .upload-csv-btn:hover {
955
+ background: #4a4a4a;
956
+ }
957
+
958
+ .csv-controls {
959
+ background: #424242;
960
+ border-radius: 8px;
961
+ padding: 12px;
962
+ display: flex;
963
+ flex-direction: column;
964
+ gap: 12px;
965
+ }
966
+
967
+ .control-group {
968
+ display: flex;
969
+ flex-direction: column;
970
+ gap: 4px;
971
+ }
972
+
973
+ .control-group label {
974
+ color: #ccc;
975
+ font-size: 0.8rem;
976
+ font-weight: 500;
977
+ }
978
+
979
+ .control-group select,
980
+ .control-group input {
981
+ background: #363636;
982
+ border: 1px solid #555;
983
+ border-radius: 4px;
984
+ color: white;
985
+ padding: 6px 8px;
986
+ font-size: 0.9rem;
987
+ width: 100%;
988
+ }
989
+
990
+ .control-group select:focus,
991
+ .control-group input:focus {
992
+ outline: none;
993
+ border-color: #4CAF50;
994
+ }
995
+
996
+ .csv-results {
997
+ background: #424242;
998
+ border-radius: 8px;
999
+ padding: 12px;
1000
+ max-height: 200px;
1001
+ overflow-y: auto;
1002
+ }
1003
+
1004
+ .csv-results h4 {
1005
+ color: white;
1006
+ margin: 0 0 8px 0;
1007
+ font-size: 0.9rem;
1008
+ }
1009
+
1010
+ .timestamp-list {
1011
+ display: flex;
1012
+ flex-direction: column;
1013
+ gap: 4px;
1014
+ }
1015
+
1016
+ .timestamp-item {
1017
+ display: flex;
1018
+ justify-content: center;
1019
+ align-items: center;
1020
+ padding: 4px 8px;
1021
+ background: #363636;
1022
+ border-radius: 4px;
1023
+ cursor: pointer;
1024
+ transition: background-color 0.2s ease;
1025
+ }
1026
+
1027
+ .timestamp-item:hover {
1028
+ background: #4a4a4a;
1029
+ }
1030
+
1031
+ .timestamp-value {
1032
+ color: #ccc;
1033
+ font-size: 0.8rem;
1034
+ }
1035
+
1036
+ .goto-timestamp-btn {
1037
+ background: #4CAF50;
1038
+ border: none;
1039
+ border-radius: 3px;
1040
+ color: white;
1041
+ padding: 2px 6px;
1042
+ font-size: 0.7rem;
1043
+ cursor: pointer;
1044
+ transition: background-color 0.2s ease;
1045
+ }
1046
+
1047
+ .goto-timestamp-btn:hover {
1048
+ background: #45a049;
1049
+ }
1050
+
1051
+ .csv-info {
1052
+ background: #424242;
1053
+ border-radius: 8px;
1054
+ padding: 12px;
1055
+ color: white;
1056
+ }
1057
+
1058
+ .csv-item {
1059
+ font-size: 0.9rem;
1060
+ margin-bottom: 8px;
1061
+ color: #ccc;
1062
+ }
1063
+
1064
+ /* Animations de transition */
1065
+ .slide-right-enter-active, .slide-right-leave-active,
1066
+ .slide-left-enter-active, .slide-left-leave-active {
1067
+ transition: transform 0.3s ease;
1068
+ }
1069
+
1070
+ /* Animation vers la droite */
1071
+ .slide-right-enter-from {
1072
+ transform: translateX(100%);
1073
+ }
1074
+
1075
+ .slide-right-leave-to {
1076
+ transform: translateX(-100%);
1077
+ }
1078
+
1079
+ /* Animation vers la gauche */
1080
+ .slide-left-enter-from {
1081
+ transform: translateX(-100%);
1082
+ }
1083
+
1084
+ .slide-left-leave-to {
1085
+ transform: translateX(100%);
1086
+ }
1087
  </style>