Browse Source

feat(offset): Improve manual offset workflow and persistence

Improves the time offset feature by making manual offsets persistent and enhancing the user workflow.

- **Persistent Manual Offsets**: Manual offsets are now saved to IndexedDB, keyed by filename. They are automatically applied when the same file is loaded in a future session.

- **Click to Revert & Forget**: The "Manual" status indicator is now clickable. Clicking it reverts to the auto-calculated offset and, crucially, deletes the saved manual offset from the database. This ensures future sessions will correctly default to the automatic calculation.

- **Consistent UI**: The "Manual" indicator now appears instantly with a consistent gray style, whether set manually or loaded from cache, removing UI ambiguity.

- **Refactored Sync Logic**: The `revertToAutoOffset` function has been refactored to reuse the core `forceResyncWithOffset` logic. This reduces code duplication and centralizes the synchronization process, making it more robust and easier to maintain.
refactor/sync-centralize
RUSHIL AMBARISH KADU 6 months ago
parent
commit
9df5006f85
  1. 79
      steps/src/db.js
  2. 5
      steps/src/dom.js
  3. 92
      steps/src/fileLoader.js
  4. 13
      steps/src/main.js
  5. 15
      steps/src/sync.js

79
steps/src/db.js

@ -10,13 +10,17 @@ dbReadyPromise = new Promise((resolve) => {
// Initializes the IndexedDB database. // Initializes the IndexedDB database.
export function initDB(callback) { export function initDB(callback) {
const request = indexedDB.open("visualizerDB", 1);
const request = indexedDB.open("visualizerDB", 2); // Increment version to 2
request.onupgradeneeded = function (event) { request.onupgradeneeded = function (event) {
const db = event.target.result; const db = event.target.result;
if (!db.objectStoreNames.contains("files")) { if (!db.objectStoreNames.contains("files")) {
db.createObjectStore("files"); db.createObjectStore("files");
} }
// Create the new store for manual offsets
if (!db.objectStoreNames.contains("manualOffsets")) {
db.createObjectStore("manualOffsets");
}
}; };
request.onsuccess = function (event) { request.onsuccess = function (event) {
@ -84,6 +88,79 @@ export function saveFileWithMetadata(key, file) {
} }
// Saves a manual offset for a specific filename.
export function saveManualOffset(filename, offset) {
return new Promise(async (resolve, reject) => {
const database = await getDB();
if (!database) {
resolve(); // Fail silently if DB is not available
return;
}
const transaction = database.transaction(["manualOffsets"], "readwrite");
const store = transaction.objectStore("manualOffsets");
const request = store.put(offset, filename); // Key is filename, value is offset
request.onsuccess = () => {
console.log(`Manual offset ${offset}ms saved for '${filename}'.`);
resolve();
};
request.onerror = (e) => {
console.warn("Failed to save manual offset:", e);
resolve(); // Resolve anyway to prevent blocking
};
});
}
// Loads a manual offset for a specific filename.
export function loadManualOffset(filename) {
return new Promise(async (resolve) => {
const database = await getDB();
if (!database) {
resolve(null);
return;
}
const transaction = database.transaction(["manualOffsets"], "readonly");
const store = transaction.objectStore("manualOffsets");
const request = store.get(filename);
request.onsuccess = () => {
const result = request.result;
if (result !== undefined) {
console.log(`Found saved manual offset for '${filename}': ${result}ms`);
resolve(result);
} else {
resolve(null);
}
};
request.onerror = () => {
resolve(null);
};
});
}
// Deletes a manual offset for a specific filename.
export function deleteManualOffset(filename) {
return new Promise(async (resolve) => {
const database = await getDB();
if (!database) {
resolve();
return;
}
const transaction = database.transaction(["manualOffsets"], "readwrite");
const store = transaction.objectStore("manualOffsets");
const request = store.delete(filename);
request.onsuccess = () => {
console.log(`Manual offset for '${filename}' deleted.`);
resolve();
};
request.onerror = (e) => {
console.warn("Failed to delete manual offset:", e);
resolve();
};
});
}
// Loads a file from IndexedDB, performing checks for filename and size to ensure data integrity. // Loads a file from IndexedDB, performing checks for filename and size to ensure data integrity.
export function loadFreshFileFromDB(key, expectedFilename) { export function loadFreshFileFromDB(key, expectedFilename) {
return new Promise(async (resolve) => { return new Promise(async (resolve) => {

5
steps/src/dom.js

@ -135,6 +135,11 @@ export function resetUIForNewLoad(isNewVideo = true) {
// Always hide radar overlay initially // Always hide radar overlay initially
radarInfoOverlay.classList.add('hidden'); radarInfoOverlay.classList.add('hidden');
// Reset offset indicator state
autoOffsetIndicator.classList.add("hidden");
autoOffsetIndicator.textContent = "";
autoOffsetIndicator.className = "text-xs font-bold ml-2 hidden"; // Reset classes
// Remove the p5 sketches completely // Remove the p5 sketches completely
if (appState.p5_instance) { if (appState.p5_instance) {
appState.p5_instance.remove(); appState.p5_instance.remove();

92
steps/src/fileLoader.js

@ -1,6 +1,6 @@
import { appState } from "./state.js"; import { appState } from "./state.js";
import { debugFlags } from "./debug.js"; import { debugFlags } from "./debug.js";
import { saveFileWithMetadata } from "./db.js";
import { saveFileWithMetadata, loadManualOffset, deleteManualOffset } from "./db.js";
import { parseVisualizationJson } from "./fileParsers.js"; import { parseVisualizationJson } from "./fileParsers.js";
import { import {
showLoadingModal, showLoadingModal,
@ -33,6 +33,7 @@ import {
resetUIForNewLoad, resetUIForNewLoad,
} from "./dom.js"; } from "./dom.js";
import { forceResyncWithOffset } from "./sync.js";
/** /**
* This is the main handler for both manual clicks and drag-and-drop. * This is the main handler for both manual clicks and drag-and-drop.
* It identifies the files and triggers the unified processing pipeline. * It identifies the files and triggers the unified processing pipeline.
@ -101,7 +102,7 @@ async function processFilePipeline(jsonFile, videoFile, fromCache) {
// --- PART B: Calculate Offset (Moved Up) --- // --- PART B: Calculate Offset (Moved Up) ---
// Critical: This must run BEFORE JSON parsing so valid start times are available. // Critical: This must run BEFORE JSON parsing so valid start times are available.
calculateAndSetOffset();
await calculateAndSetOffset();
// --- PART C: Handle JSON Parsing --- // --- PART C: Handle JSON Parsing ---
if (jsonFile) { if (jsonFile) {
@ -374,7 +375,7 @@ function setupVideoPlayer(fileURL) {
} }
} }
function calculateAndSetOffset() {
async function calculateAndSetOffset() {
const jsonTimestampInfo = extractTimestampInfo(appState.jsonFilename); const jsonTimestampInfo = extractTimestampInfo(appState.jsonFilename);
const videoTimestampInfo = extractTimestampInfo(appState.videoFilename); const videoTimestampInfo = extractTimestampInfo(appState.videoFilename);
@ -395,6 +396,30 @@ function calculateAndSetOffset() {
); );
} }
// 1. Try to load a manually saved offset for this specific file pair.
// We use the JSON filename as the primary key, but ideally, it should be a combo.
// For now, sticking to the user request: "if the user uploads a similarly named file".
// We'll use the JSON filename as the key.
const savedOffset = await loadManualOffset(appState.jsonFilename);
if (savedOffset !== null) {
console.log(`Applying saved manual offset: ${savedOffset}ms`);
appState.offset = savedOffset;
if (jsonDate) {
appState.radarStartTimeMs = jsonDate.getTime();
}
// Update UI
offsetInput.value = appState.offset;
// Show "Manual" indicator
autoOffsetIndicator.textContent = "Manual";
autoOffsetIndicator.className = "text-xs font-bold ml-2 text-gray-500"; // Gray for manual
autoOffsetIndicator.classList.remove("hidden");
localStorage.setItem("visualizerOffset", appState.offset);
return; // Exit early, skipping auto-calc
}
let calculatedOffset = 0; let calculatedOffset = 0;
// We need both dates to calculate an offset. // We need both dates to calculate an offset.
if (jsonDate && videoDate) { if (jsonDate && videoDate) {
@ -404,17 +429,78 @@ function calculateAndSetOffset() {
if (isNaN(offset) || Math.abs(offset) > 30000) { if (isNaN(offset) || Math.abs(offset) > 30000) {
console.warn(`Calculated offset of ${offset}ms is invalid or exceeds 30s threshold. Defaulting to 0.`); console.warn(`Calculated offset of ${offset}ms is invalid or exceeds 30s threshold. Defaulting to 0.`);
calculatedOffset = 0; calculatedOffset = 0;
// Show "Default" or "Out of Range" indicator
autoOffsetIndicator.textContent = isNaN(offset) ? "Default" : "Out of Range";
autoOffsetIndicator.className = "text-xs font-bold ml-2 text-yellow-600"; // Dark Yellow
autoOffsetIndicator.classList.remove("hidden");
} else { } else {
calculatedOffset = offset; calculatedOffset = offset;
// Show "Auto" indicator
autoOffsetIndicator.textContent = "Auto";
autoOffsetIndicator.className = "text-xs font-bold ml-2 text-green-500"; // Green
autoOffsetIndicator.classList.remove("hidden"); autoOffsetIndicator.classList.remove("hidden");
console.log(`Auto-calculated offset: ${calculatedOffset} ms`); console.log(`Auto-calculated offset: ${calculatedOffset} ms`);
} }
} else if (jsonDate) { } else if (jsonDate) {
// If we have JSON but no video, we set start time but offset is 0 // If we have JSON but no video, we set start time but offset is 0
appState.radarStartTimeMs = jsonDate.getTime(); appState.radarStartTimeMs = jsonDate.getTime();
// No specific indicator needed for JSON-only default 0, or could show "Default"
autoOffsetIndicator.classList.add("hidden");
} }
appState.offset = calculatedOffset; appState.offset = calculatedOffset;
offsetInput.value = appState.offset; offsetInput.value = appState.offset;
localStorage.setItem("visualizerOffset", appState.offset); localStorage.setItem("visualizerOffset", appState.offset);
} }
/**
* Re-calculates and applies the automatic offset based on filenames.
* This function is triggered by user actions like clicking the 'Manual' indicator
* or using a keyboard shortcut to revert a manual offset.
*/
export function revertToAutoOffset() {
// 1. Calculate the automatic offset.
const jsonTimestampInfo = extractTimestampInfo(appState.jsonFilename);
const videoTimestampInfo = extractTimestampInfo(appState.videoFilename);
let calculatedOffset = 0;
let indicatorText = "Default";
let indicatorClass = "text-xs font-bold ml-2 text-yellow-600"; // Default to yellow
if (jsonTimestampInfo && videoTimestampInfo) {
const jsonDate = parseTimestamp(jsonTimestampInfo.timestampStr, jsonTimestampInfo.format);
const videoDate = parseTimestamp(videoTimestampInfo.timestampStr, videoTimestampInfo.format);
if (jsonDate && videoDate) {
const offset = jsonDate.getTime() - videoDate.getTime();
if (isNaN(offset) || Math.abs(offset) > 30000) {
calculatedOffset = 0;
indicatorText = "Out of Range";
} else {
calculatedOffset = offset;
indicatorText = "Auto";
indicatorClass = "text-xs font-bold ml-2 text-green-500";
}
}
}
// 2. Update the input box with the new value.
offsetInput.value = calculatedOffset;
// 3. Delete any saved manual offset so future loads default to "Auto" logic.
if (appState.jsonFilename) {
deleteManualOffset(appState.jsonFilename);
}
// 4. Call the resync function with saveToDb = false.
forceResyncWithOffset(false);
// 5. After resyncing, set the correct indicator text and style.
autoOffsetIndicator.textContent = indicatorText;
autoOffsetIndicator.className = indicatorClass;
autoOffsetIndicator.classList.remove("hidden");
}

13
steps/src/main.js

@ -86,9 +86,9 @@ import {
import { initializeTheme } from "./theme.js"; import { initializeTheme } from "./theme.js";
import { initDB, loadFreshFileFromDB } from "./db.js";
import { initDB, loadFreshFileFromDB, saveManualOffset } from "./db.js";
import { initKeyboardShortcuts } from "./keyboard.js"; import { initKeyboardShortcuts } from "./keyboard.js";
import { handleFiles } from "./fileLoader.js";
import { handleFiles, revertToAutoOffset } from "./fileLoader.js";
// Wire up the manual file inputs to the new handler // Wire up the manual file inputs to the new handler
jsonFileInput.addEventListener("change", (event) => jsonFileInput.addEventListener("change", (event) =>
@ -481,6 +481,15 @@ offsetInput.addEventListener("keydown", (event) => {
} }
}); });
// --- [START] NEW: Revert to Auto-Offset Logic ---
autoOffsetIndicator.addEventListener("click", () => {
// Only allow reverting if the indicator shows "Manual".
if (autoOffsetIndicator.textContent === "Manual") {
revertToAutoOffset();
}
});
// --- [END] NEW: Revert to Auto-Offset Logic ---
// --- [START] CORRECTED INITIALIZATION LOGIC --- // --- [START] CORRECTED INITIALIZATION LOGIC ---
document.addEventListener("DOMContentLoaded", () => { document.addEventListener("DOMContentLoaded", () => {
initializeTheme(); initializeTheme();

15
steps/src/sync.js

@ -12,11 +12,13 @@ import {
toggleEgoSpeed, toggleEgoSpeed,
egoSpeedDisplay, egoSpeedDisplay,
canSpeedDisplay, canSpeedDisplay,
autoOffsetIndicator,
} from "./dom.js"; } from "./dom.js";
import { VIDEO_FPS } from "./constants.js"; import { VIDEO_FPS } from "./constants.js";
import { findRadarFrameIndexForTime, precomputeRadarVideoSync } from "./utils.js"; import { findRadarFrameIndexForTime, precomputeRadarVideoSync } from "./utils.js";
import { throttledUpdateExplorer, isExplorerOpen } from "./dataExplorer.js"; import { throttledUpdateExplorer, isExplorerOpen } from "./dataExplorer.js";
import { debugFlags } from "./debug.js"; import { debugFlags } from "./debug.js";
import { saveManualOffset } from "./db.js";
// --- [START] MOVED FROM DOM.JS --- // --- [START] MOVED FROM DOM.JS ---
@ -47,17 +49,26 @@ export function pausePlayback() {
} }
} }
export function forceResyncWithOffset() {
export function forceResyncWithOffset(saveToDb = true) {
// Make sure visualization data is loaded before proceeding // Make sure visualization data is loaded before proceeding
if (!appState.vizData) return; if (!appState.vizData) return;
const newOffset = parseFloat(offsetInput.value) || 0; const newOffset = parseFloat(offsetInput.value) || 0;
appState.offset = newOffset; // Update the central state appState.offset = newOffset; // Update the central state
localStorage.setItem("visualizerOffset", newOffset); // Persist it
// Persist the manual offset to IndexedDB for this specific file
if (saveToDb && appState.jsonFilename) {
saveManualOffset(appState.jsonFilename, newOffset);
}
// Re-Bake: Overwrite the pre-calculated sync times with the new offset. // Re-Bake: Overwrite the pre-calculated sync times with the new offset.
precomputeRadarVideoSync(appState.vizData, appState.offset); precomputeRadarVideoSync(appState.vizData, appState.offset);
// --- START: Manual Offset UI Update ---
// When the user manually sets an offset, we need to update the UI immediately.
autoOffsetIndicator.textContent = "Manual"; // Set text
autoOffsetIndicator.className = "text-xs font-bold ml-2 text-gray-500"; // Use consistent gray styling
// --- END: Manual Offset UI Update ---
console.log(`Forcing resync with new offset: ${appState.offset}ms`); console.log(`Forcing resync with new offset: ${appState.offset}ms`);
// If the video is playing, pause it to allow for precise frame tuning. // If the video is playing, pause it to allow for precise frame tuning.

Loading…
Cancel
Save