|
| 1 | +import { orderedMultiCamCameraNames } from 'dive-common/multicamDisplay'; |
1 | 2 | import { orderedHeadTail } from 'vue-media-annotator/headTail'; |
2 | 3 | /** |
3 | 4 | * State behind the Review page: the datasets under review (with their |
@@ -47,7 +48,8 @@ export interface ReviewGeometryEdit { |
47 | 48 |
|
48 | 49 | export type ReviewApi = Pick<Api, |
49 | 50 | 'loadConfig' | 'peekConfig' | 'loadDetections' | 'loadReviewTracks' | 'saveDetections' |
50 | | - | 'listScoringDatasets' | 'pickScoringDataset'>; |
| 51 | + | 'listScoringDatasets' | 'pickScoringDataset' | 'listReviewDatasets' | 'pickReviewDataset' |
| 52 | + | 'resolveReviewDatasetId'>; |
51 | 53 |
|
52 | 54 | export interface ReviewServiceDeps { |
53 | 55 | api: ReviewApi; |
@@ -322,9 +324,10 @@ function createScopedReviewService(deps: ReviewServiceDeps): ReviewService { |
322 | 324 | } |
323 | 325 |
|
324 | 326 | async function refreshAvailable() { |
325 | | - if (!api.listScoringDatasets) return; |
| 327 | + const listDatasets = api.listReviewDatasets ?? api.listScoringDatasets; |
| 328 | + if (!listDatasets) return; |
326 | 329 | try { |
327 | | - const result = await requests.run(() => api.listScoringDatasets!()); |
| 330 | + const result = await requests.run(() => listDatasets()); |
328 | 331 | if (!disposed) available.value = result; |
329 | 332 | } catch (err) { |
330 | 333 | fail(err, 'Could not list datasets'); |
@@ -365,14 +368,28 @@ function createScopedReviewService(deps: ReviewServiceDeps): ReviewService { |
365 | 368 | const isCurrent = () => loadTokens.get(id) === token && !!entry(id); |
366 | 369 | loading.value = true; |
367 | 370 | try { |
| 371 | + // Normalize user selections, but keep expanded cameras separate internally |
| 372 | + // so media, annotations, and writes continue using their own folders. |
| 373 | + if (api.resolveReviewDatasetId && !memberships.has(id)) { |
| 374 | + const resolvedId = await requests.run(() => api.resolveReviewDatasetId!(id)); |
| 375 | + if (!isCurrent()) return; |
| 376 | + if (resolvedId !== id) { |
| 377 | + datasets.value = datasets.value.filter((d) => d.id !== id); |
| 378 | + await addDataset(resolvedId); |
| 379 | + return; |
| 380 | + } |
| 381 | + } |
368 | 382 | const config = await requests.run(() => { |
369 | 383 | if (!isCurrent()) throw new Error('Dataset removed'); |
370 | 384 | return loadConfig(id); |
371 | 385 | }); |
372 | 386 | if (!isCurrent()) return; |
373 | 387 | if (config.type === 'multi') { |
374 | 388 | // Load each camera separately while exposing the parent as one selected sequence. |
375 | | - const cameras = Object.keys(config.multiCamMedia?.cameras || {}); |
| 389 | + const cameras = [...new Set([ |
| 390 | + ...orderedMultiCamCameraNames(config.multiCamMedia), |
| 391 | + ...Object.keys(config.multiCamMedia?.cameras || {}), |
| 392 | + ])]; |
376 | 393 | const parentName = entry(id)?.name || config.name; |
377 | 394 | if (!cameras.length) throw new Error('This sequence has no cameras'); |
378 | 395 | parentNames.set(id, parentName); |
@@ -429,20 +446,36 @@ function createScopedReviewService(deps: ReviewServiceDeps): ReviewService { |
429 | 446 | /** |
430 | 447 | * Add a dataset; with `defer` it only joins the list and loads on the |
431 | 448 | * next `loadQueued`, so picking many datasets costs nothing until the |
432 | | - * results are actually wanted. |
| 449 | + * results are actually wanted. Deferred picks still resolve camera folders |
| 450 | + * to their sequence so a browse pick cannot sit beside an already-loaded rig. |
433 | 451 | */ |
434 | 452 | async function addDataset(id: string, summary?: ScoringDatasetSummary, options: { defer?: boolean } = {}) { |
435 | 453 | if (disposed || !id || entry(id) || selectedDatasets.value.some((dataset) => dataset.id === id)) return; |
| 454 | + let selectedId = id; |
| 455 | + let selectedSummary = summary; |
| 456 | + if (options.defer && api.resolveReviewDatasetId) { |
| 457 | + try { |
| 458 | + const resolvedId = await api.resolveReviewDatasetId(id); |
| 459 | + if (resolvedId !== id) { |
| 460 | + selectedId = resolvedId; |
| 461 | + // Drop the camera-folder summary; the parent owns the sequence name. |
| 462 | + selectedSummary = undefined; |
| 463 | + } |
| 464 | + } catch { |
| 465 | + // Keep the original id; load() will surface the error. |
| 466 | + } |
| 467 | + } |
| 468 | + if (entry(selectedId) || selectedDatasets.value.some((dataset) => dataset.id === selectedId)) return; |
436 | 469 | datasets.value = [...datasets.value, { |
437 | | - id, |
438 | | - name: summary?.name || datasetName(id), |
439 | | - type: summary?.type, |
| 470 | + id: selectedId, |
| 471 | + name: selectedSummary?.name || datasetName(selectedId), |
| 472 | + type: selectedSummary?.type, |
440 | 473 | status: options.defer ? 'queued' : 'loading', |
441 | 474 | trackCount: 0, |
442 | 475 | croppable: false, |
443 | 476 | }]; |
444 | 477 | if (options.defer) return; |
445 | | - await load(id); |
| 478 | + await load(selectedId); |
446 | 479 | } |
447 | 480 |
|
448 | 481 | /** Load every queued dataset; annotations are read and the query rerun as each arrives. */ |
|
0 commit comments