diff --git a/client/dive-common/apispec.ts b/client/dive-common/apispec.ts index 2de12986b..3745841c4 100644 --- a/client/dive-common/apispec.ts +++ b/client/dive-common/apispec.ts @@ -323,7 +323,7 @@ interface Api { saveAttributeTrackFilters(datasetId: string, args: SaveAttributeTrackFilterArgs): Promise; // Non-Endpoint shared functions - openFromDisk(datasetType: DatasetType | 'bulk' | 'calibration' | 'annotation' | 'text' | 'zip' | 'transform', directory?: boolean): + openFromDisk(datasetType: DatasetType | 'bulk' | 'calibration' | 'annotation' | 'frameMetadata' | 'text' | 'zip' | 'transform', directory?: boolean): Promise<{canceled?: boolean; filePaths: string[]; fileList?: File[]; root?: string}>; /** Desktop: immediate child directory names under a parent folder (multicam subfolder import). */ listImmediateSubfolders?(parentPath: string): Promise; @@ -363,6 +363,8 @@ interface Api { }): Promise; importAnnotationFile(id: string, path: string, file?: File, additive?: boolean, additivePrepend?: string, set?: string): Promise; + /** Store an arbitrary-named frame metadata sidecar with the dataset; resolves true on success. */ + importFrameMetadataFile?(id: string, path: string, file?: File): Promise; // Desktop-only calibration persistence functions getLastCalibration?(): Promise; saveCalibration?(path: string): Promise<{ savedPath: string; updatedDatasetIds: string[] }>; diff --git a/client/dive-common/components/ImportAnnotations.vue b/client/dive-common/components/ImportAnnotations.vue index 7e4bc1351..6f41ca224 100644 --- a/client/dive-common/components/ImportAnnotations.vue +++ b/client/dive-common/components/ImportAnnotations.vue @@ -5,6 +5,8 @@ import { import { useApi } from 'dive-common/apispec'; import { usePrompt } from 'dive-common/vue-utilities/prompt-service'; import { clientSettings } from 'dive-common/store/settings'; +import { invalidateFrameMetadata } from 'dive-common/use/useFrameMetadata'; +import isFrameMetadataSourceName from 'dive-common/frameMetadata/naming'; import clearLengthAttributes from 'dive-common/utils/clearLengthAttributes'; import warpAnnotationsAcrossCameras from 'dive-common/utils/warpAnnotationsAcrossCameras'; import { cloneDeep } from 'lodash'; @@ -26,6 +28,10 @@ export default defineComponent({ type: String as PropType, default: null, }, + mediaType: { + type: String as PropType, + default: null, + }, calibrationFile: { type: String as PropType, default: null, @@ -191,6 +197,12 @@ export default defineComponent({ if (importFile) { await reloadAnnotations(); + // Importing a reserved-name frame-metadata.csv/.txt through this path creates an + // in-viewer sidecar; drop the session cache so the Frame Metadata panel shows it + // without a viewer reload, matching the explicit Import Frame Metadata button. + if (isFrameMetadataSourceName(ret.fileList?.[0]?.name ?? path)) { + invalidateFrameMetadata(); + } if ( warpToAllCameras.value && canWarpToAllCameras.value @@ -219,6 +231,46 @@ export default defineComponent({ processing.value = false; } }; + // Frame metadata is only meaningful for image-sequence datasets (single-camera or a + // multicam rig of image sequences); mirror the sibling sections and gate on media type + // so it never renders on video/large-image datasets, where an import can only fail. + const frameMetadataSupported = computed( + () => !!api.importFrameMetadataFile + && (props.mediaType === 'image-sequence' || isMulticamDataset.value), + ); + const openFrameMetadataUpload = async () => { + if (!api.importFrameMetadataFile) return; + try { + const ret = await openFromDisk('frameMetadata'); + if (ret.canceled || !ret.filePaths.length) return; + menuOpen.value = false; + processing.value = true; + const result = await api.importFrameMetadataFile( + props.datasetId, + ret.filePaths[0], + ret.fileList?.[0], + ); + processing.value = false; + if (result === false) { + await prompt({ + title: 'Frame Metadata Import Failed', + text: ['Could not import the frame metadata file.'], + positiveButton: 'OK', + }); + return; + } + // The Frame Metadata panel reads through a session cache; drop it so the new + // sidecar shows up without reloading the viewer. + invalidateFrameMetadata(); + } catch (error) { + processing.value = false; + prompt({ + title: 'Frame Metadata Import Failed', + text: [getResponseError(error)], + positiveButton: 'OK', + }); + } + }; const openCalibrationUpload = async () => { if (!api.importCalibrationFile) return; try { @@ -357,6 +409,8 @@ export default defineComponent({ }; return { openUpload, + frameMetadataSupported, + openFrameMetadataUpload, openCalibrationUpload, openRegistrationUpload, applyLastCalibration, @@ -533,6 +587,37 @@ export default defineComponent({ +