diff --git a/client/dive-common/components/TrackSettingsPanel.vue b/client/dive-common/components/TrackSettingsPanel.vue index 71b1ec1b2..40a1291c7 100644 --- a/client/dive-common/components/TrackSettingsPanel.vue +++ b/client/dive-common/components/TrackSettingsPanel.vue @@ -33,6 +33,7 @@ export default defineComponent({ filterTracksByFrame: 'Filter the track list by those with detections in the current frame', autoZoom: 'Automatically zoom to the track when selected', showMultiCamToolbar: 'Show multi-camera tools in the top toolbar when a track is selected', + autoSave: 'Automatically save annotation changes after a short delay. Changes are batched to reduce server requests.', }); const modes = ref(['Track', 'Detection']); // Add unknown as the default type to the typeList @@ -362,6 +363,45 @@ export default defineComponent({ + +
+ Auto-Save Settings +
+ + + + + + + + {{ help.autoSave }} + + + diff --git a/client/dive-common/components/Viewer.vue b/client/dive-common/components/Viewer.vue index b3da641be..676e61105 100644 --- a/client/dive-common/components/Viewer.vue +++ b/client/dive-common/components/Viewer.vue @@ -412,10 +412,11 @@ export default defineComponent({ handler.stopLinking(); } }); - async function save(setVal?: string) { - // If editing the track, disable editing mode before save + async function save(setVal?: string, exitEditingMode = false) { + // Only exit editing mode if explicitly requested (e.g., manual save) + // Auto-save should NOT disrupt the user's editing session saveInProgress.value = true; - if (editingTrack.value) { + if (exitEditingMode && editingTrack.value) { handler.trackSelect(selectedTrackId.value, false); } const saveSet = setVal === 'default' ? undefined : setVal; @@ -467,6 +468,32 @@ export default defineComponent({ watch(imageEnhancements, debouncedSaveImageEnhancements, { deep: true }); + // Auto-save annotations when enabled + const debouncedAutoSave = debounce( + async () => { + if (readonlyState.value) return; + if (pendingSaveCount.value === 0) return; + if (saveInProgress.value) return; + await save(props.currentSet); + }, + 2000, + { trailing: true, leading: false }, + ); + + watch( + pendingSaveCount, + (newCount, oldCount) => { + if ( + clientSettings.autoSaveSettings.enabled + && newCount > oldCount + && newCount > 0 + && !readonlyState.value + ) { + debouncedAutoSave(); + } + }, + ); + // Navigation Guards used by parent component async function warnBrowserExit(event: BeforeUnloadEvent) { if (pendingSaveCount.value === 0) return; @@ -766,6 +793,7 @@ export default defineComponent({ handleResize(); }); onBeforeUnmount(() => { + debouncedAutoSave.cancel(); if (controlsRef.value) observer.unobserve(controlsRef.value.$el); }); @@ -1081,7 +1109,7 @@ export default defineComponent({ @click="save(currentSet)" > - mdi-content-save + {{ clientSettings.autoSaveSettings.enabled ? 'mdi-content-save-cog' : 'mdi-content-save' }} diff --git a/client/dive-common/store/settings.ts b/client/dive-common/store/settings.ts index adab2e24f..9a80ed72c 100644 --- a/client/dive-common/store/settings.ts +++ b/client/dive-common/store/settings.ts @@ -48,6 +48,9 @@ interface AnnotationSettings { multiCamSettings: { showToolbar: boolean; }; + autoSaveSettings: { + enabled: boolean; + }; } const defaultSettings: AnnotationSettings = { @@ -105,6 +108,9 @@ const defaultSettings: AnnotationSettings = { multiCamSettings: { showToolbar: true, }, + autoSaveSettings: { + enabled: false, // Disabled by default for backward compatibility + }, }; // Utility to safely load from localStorage diff --git a/client/platform/desktop/frontend/components/Settings.vue b/client/platform/desktop/frontend/components/Settings.vue index 93ce64c5e..b3b40736c 100644 --- a/client/platform/desktop/frontend/components/Settings.vue +++ b/client/platform/desktop/frontend/components/Settings.vue @@ -196,6 +196,18 @@ export default defineComponent({ /> + + + + + Platform support diff --git a/client/platform/web-girder/views/Settings.vue b/client/platform/web-girder/views/Settings.vue index 957fd7337..9a594f4f0 100644 --- a/client/platform/web-girder/views/Settings.vue +++ b/client/platform/web-girder/views/Settings.vue @@ -1,8 +1,14 @@ @@ -10,8 +16,34 @@ export default Vue.extend({ - + Annotation Settings + + + + + + + + + + + +