diff --git a/steps/src/db.js b/steps/src/db.js index 53792e8..c06ceb7 100644 --- a/steps/src/db.js +++ b/steps/src/db.js @@ -10,13 +10,17 @@ dbReadyPromise = new Promise((resolve) => { // Initializes the IndexedDB database. export function initDB(callback) { - const request = indexedDB.open("visualizerDB", 1); + const request = indexedDB.open("visualizerDB", 2); // Increment version to 2 request.onupgradeneeded = function (event) { const db = event.target.result; if (!db.objectStoreNames.contains("files")) { db.createObjectStore("files"); } + // Create the new store for manual offsets + if (!db.objectStoreNames.contains("manualOffsets")) { + db.createObjectStore("manualOffsets"); + } }; 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. export function loadFreshFileFromDB(key, expectedFilename) { return new Promise(async (resolve) => { diff --git a/steps/src/dom.js b/steps/src/dom.js index 72cbf47..6419291 100644 --- a/steps/src/dom.js +++ b/steps/src/dom.js @@ -135,6 +135,11 @@ export function resetUIForNewLoad(isNewVideo = true) { // Always hide radar overlay initially 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 if (appState.p5_instance) { appState.p5_instance.remove(); diff --git a/steps/src/fileLoader.js b/steps/src/fileLoader.js index 1527f2a..8b9c3e0 100644 --- a/steps/src/fileLoader.js +++ b/steps/src/fileLoader.js @@ -1,6 +1,6 @@ import { appState } from "./state.js"; import { debugFlags } from "./debug.js"; -import { saveFileWithMetadata } from "./db.js"; +import { saveFileWithMetadata, loadManualOffset, deleteManualOffset } from "./db.js"; import { parseVisualizationJson } from "./fileParsers.js"; import { showLoadingModal, @@ -33,6 +33,7 @@ import { resetUIForNewLoad, } from "./dom.js"; +import { forceResyncWithOffset } from "./sync.js"; /** * This is the main handler for both manual clicks and drag-and-drop. * 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) --- // Critical: This must run BEFORE JSON parsing so valid start times are available. - calculateAndSetOffset(); + await calculateAndSetOffset(); // --- PART C: Handle JSON Parsing --- if (jsonFile) { @@ -374,7 +375,7 @@ function setupVideoPlayer(fileURL) { } } -function calculateAndSetOffset() { +async function calculateAndSetOffset() { const jsonTimestampInfo = extractTimestampInfo(appState.jsonFilename); 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; // We need both dates to calculate an offset. if (jsonDate && videoDate) { @@ -404,17 +429,78 @@ function calculateAndSetOffset() { if (isNaN(offset) || Math.abs(offset) > 30000) { console.warn(`Calculated offset of ${offset}ms is invalid or exceeds 30s threshold. Defaulting to 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 { calculatedOffset = offset; + + // Show "Auto" indicator + autoOffsetIndicator.textContent = "Auto"; + autoOffsetIndicator.className = "text-xs font-bold ml-2 text-green-500"; // Green autoOffsetIndicator.classList.remove("hidden"); + console.log(`Auto-calculated offset: ${calculatedOffset} ms`); } } else if (jsonDate) { // If we have JSON but no video, we set start time but offset is 0 appState.radarStartTimeMs = jsonDate.getTime(); + // No specific indicator needed for JSON-only default 0, or could show "Default" + autoOffsetIndicator.classList.add("hidden"); } appState.offset = calculatedOffset; offsetInput.value = 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"); } \ No newline at end of file diff --git a/steps/src/main.js b/steps/src/main.js index d97eebe..5619823 100644 --- a/steps/src/main.js +++ b/steps/src/main.js @@ -86,9 +86,9 @@ import { import { initializeTheme } from "./theme.js"; -import { initDB, loadFreshFileFromDB } from "./db.js"; +import { initDB, loadFreshFileFromDB, saveManualOffset } from "./db.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 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 --- document.addEventListener("DOMContentLoaded", () => { initializeTheme(); diff --git a/steps/src/sync.js b/steps/src/sync.js index 3c4e7f6..6443899 100644 --- a/steps/src/sync.js +++ b/steps/src/sync.js @@ -12,11 +12,13 @@ import { toggleEgoSpeed, egoSpeedDisplay, canSpeedDisplay, + autoOffsetIndicator, } from "./dom.js"; import { VIDEO_FPS } from "./constants.js"; import { findRadarFrameIndexForTime, precomputeRadarVideoSync } from "./utils.js"; import { throttledUpdateExplorer, isExplorerOpen } from "./dataExplorer.js"; import { debugFlags } from "./debug.js"; +import { saveManualOffset } from "./db.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 if (!appState.vizData) return; const newOffset = parseFloat(offsetInput.value) || 0; 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. 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`); // If the video is playing, pause it to allow for precise frame tuning.