Browse Source
refactor(app): Extract UI and session logic from main.js
refactor(app): Extract UI and session logic from main.js
This commit refactors the monolithic `main.js` file to improve modularity, maintainability, and separation of concerns. The core application logic is now organized into more focused modules. - **UI Logic (`ui.js`):** All general UI event listeners, including feature toggles, sliders, menu controls, and tooltips, have been moved from `main.js` into a new `src/ui.js` module. This centralizes UI-specific behavior. - **Session Management (`session.js`):** The functionality for saving and loading application sessions has been extracted into a dedicated `src/session.js` module. This isolates all logic related to session state serialization and file handling. - **`main.js` Simplification:** The `main.js` file now serves as a cleaner entry point, responsible for initializing all the different application modules in the correct order. **Bug Fixes during Refactoring:** - Corrected an issue where activating "God Mode" (`toggleCloseUp`) did not properly start the p5.js `loop()`, preventing the visualization from updating with mouse movement. - Ensured the `toggleConfirmedOnly` state is now correctly saved and restored as part of the session file.refactor/sync-centralize
3 changed files with 291 additions and 348 deletions
-
353steps/src/main.js
-
131steps/src/session.js
-
155steps/src/ui.js
@ -0,0 +1,131 @@ |
|||||
|
import { appState } from "./state.js"; |
||||
|
import { showModal } from "./modal.js"; |
||||
|
import { loadFreshFileFromDB } from "./db.js"; |
||||
|
import { |
||||
|
offsetInput, |
||||
|
speedSlider, |
||||
|
snrMinInput, |
||||
|
snrMaxInput, |
||||
|
toggleSnrColor, |
||||
|
toggleClusterColor, |
||||
|
toggleInlierColor, |
||||
|
toggleStationaryColor, |
||||
|
toggleVelocity, |
||||
|
toggleTracks, |
||||
|
toggleEgoSpeed, |
||||
|
toggleFrameNorm, |
||||
|
toggleDebugOverlay, |
||||
|
toggleDebug2Overlay, |
||||
|
toggleCloseUp, |
||||
|
togglePredictedPos, |
||||
|
toggleCovariance, |
||||
|
toggleConfirmedOnly, |
||||
|
saveSessionBtn, |
||||
|
loadSessionBtn, |
||||
|
sessionFileInput, |
||||
|
} from "./dom.js"; |
||||
|
|
||||
|
function saveSession() { |
||||
|
if (!appState.jsonFilename && !appState.videoFilename) { |
||||
|
showModal("Nothing to save. Please load data files first."); |
||||
|
return; |
||||
|
} |
||||
|
|
||||
|
const sessionState = { |
||||
|
version: 1, |
||||
|
jsonFilename: appState.jsonFilename, |
||||
|
videoFilename: appState.videoFilename, |
||||
|
offset: offsetInput.value, |
||||
|
playbackSpeed: speedSlider.value, |
||||
|
snrMin: snrMinInput.value, |
||||
|
snrMax: snrMaxInput.value, |
||||
|
toggles: { |
||||
|
snrColor: toggleSnrColor.checked, |
||||
|
clusterColor: toggleClusterColor.checked, |
||||
|
inlierColor: toggleInlierColor.checked, |
||||
|
stationaryColor: toggleStationaryColor.checked, |
||||
|
velocity: toggleVelocity.checked, |
||||
|
tracks: toggleTracks.checked, |
||||
|
egoSpeed: toggleEgoSpeed.checked, |
||||
|
frameNorm: toggleFrameNorm.checked, |
||||
|
debugOverlay: toggleDebugOverlay.checked, |
||||
|
debug2Overlay: toggleDebug2Overlay.checked, |
||||
|
closeUp: toggleCloseUp.checked, |
||||
|
predictedPos: togglePredictedPos.checked, |
||||
|
covariance: toggleCovariance.checked, |
||||
|
confirmedOnly: toggleConfirmedOnly.checked, |
||||
|
}, |
||||
|
}; |
||||
|
|
||||
|
const sessionString = JSON.stringify(sessionState, null, 2); |
||||
|
const blob = new Blob([sessionString], { type: "application/json" }); |
||||
|
const url = URL.createObjectURL(blob); |
||||
|
|
||||
|
const now = new Date(); |
||||
|
const pad = (num) => String(num).padStart(2, "0"); |
||||
|
const timestamp = `${now.getFullYear()}-${pad(now.getMonth() + 1)}-${pad( |
||||
|
now.getDate() |
||||
|
)}_${pad(now.getHours())}-${pad(now.getMinutes())}-${pad(now.getSeconds())}`;
|
||||
|
const defaultFilename = `visualizer-session_${timestamp}.json`; |
||||
|
|
||||
|
const a = document.createElement("a"); |
||||
|
a.href = url; |
||||
|
a.download = defaultFilename; |
||||
|
document.body.appendChild(a); |
||||
|
a.click(); |
||||
|
document.body.removeChild(a); |
||||
|
URL.revokeObjectURL(url); |
||||
|
} |
||||
|
|
||||
|
async function loadSession(file) { |
||||
|
if (!file) return; |
||||
|
|
||||
|
const reader = new FileReader(); |
||||
|
reader.onload = async (e) => { |
||||
|
try { |
||||
|
const sessionState = JSON.parse(e.target.result); |
||||
|
|
||||
|
if (sessionState.version !== 1 || !sessionState.jsonFilename) { |
||||
|
showModal("Error: Invalid or corrupted session file."); |
||||
|
return; |
||||
|
} |
||||
|
|
||||
|
const videoBlob = await loadFreshFileFromDB("video", sessionState.videoFilename); |
||||
|
const jsonBlob = await loadFreshFileFromDB("json", sessionState.jsonFilename); |
||||
|
|
||||
|
if (!jsonBlob || (sessionState.videoFilename && !videoBlob)) { |
||||
|
showModal(`Session load failed: The required data files are not in the application's cache.
|
||||
|
|
||||
|
Please manually load '${sessionState.jsonFilename}' and '${sessionState.videoFilename}' before loading this session.`);
|
||||
|
return; |
||||
|
} |
||||
|
|
||||
|
localStorage.setItem("jsonFilename", sessionState.jsonFilename || ""); |
||||
|
localStorage.setItem("videoFilename", sessionState.videoFilename || ""); |
||||
|
localStorage.setItem("visualizerOffset", sessionState.offset || "0"); |
||||
|
localStorage.setItem("playbackSpeed", sessionState.playbackSpeed || "1"); |
||||
|
localStorage.setItem("snrMin", sessionState.snrMin || ""); |
||||
|
localStorage.setItem("snrMax", sessionState.snrMax || ""); |
||||
|
if (sessionState.toggles) { |
||||
|
localStorage.setItem("togglesState", JSON.stringify(sessionState.toggles)); |
||||
|
} |
||||
|
|
||||
|
showModal("Session files found in cache. The application will now reload.").then(() => { |
||||
|
window.location.reload(); |
||||
|
}); |
||||
|
} catch (error) { |
||||
|
showModal("Error: Could not parse the session file. It may be invalid."); |
||||
|
console.error("Session load error:", error); |
||||
|
} |
||||
|
}; |
||||
|
reader.readAsText(file); |
||||
|
} |
||||
|
|
||||
|
export function initSessionManagement() { |
||||
|
saveSessionBtn.addEventListener("click", saveSession); |
||||
|
loadSessionBtn.addEventListener("click", () => sessionFileInput.click()); |
||||
|
sessionFileInput.addEventListener("change", (event) => { |
||||
|
loadSession(event.target.files[0]); |
||||
|
event.target.value = ""; // Clear the input for future loads.
|
||||
|
}); |
||||
|
} |
||||
@ -0,0 +1,155 @@ |
|||||
|
import { appState } from "./state.js"; |
||||
|
import { formatTime } from "./utils.js"; |
||||
|
import { showModal } from "./modal.js"; |
||||
|
import { pausePlayback } from "./sync.js"; |
||||
|
import { |
||||
|
videoPlayer, |
||||
|
timelineSlider, |
||||
|
speedSlider, |
||||
|
speedDisplay, |
||||
|
toggleSnrColor, |
||||
|
toggleClusterColor, |
||||
|
toggleInlierColor, |
||||
|
toggleStationaryColor, |
||||
|
toggleVelocity, |
||||
|
toggleEgoSpeed, |
||||
|
toggleFrameNorm, |
||||
|
toggleTracks, |
||||
|
toggleDebugOverlay, |
||||
|
toggleDebug2Overlay, |
||||
|
toggleCloseUp, |
||||
|
snrMinInput, |
||||
|
snrMaxInput, |
||||
|
applySnrBtn, |
||||
|
timelineTooltip, |
||||
|
playPauseBtn, |
||||
|
updatePersistentOverlays, |
||||
|
updateDebugOverlay, |
||||
|
collapsibleMenu, |
||||
|
toggleMenuBtn, |
||||
|
closeMenuBtn, |
||||
|
menuScrim, |
||||
|
fullscreenBtn, |
||||
|
toggleConfirmedOnly, |
||||
|
} from "./dom.js"; |
||||
|
|
||||
|
function toggleMenu(show) { |
||||
|
if (show) { |
||||
|
collapsibleMenu.classList.remove("-translate-x-full"); |
||||
|
menuScrim.classList.remove("hidden"); |
||||
|
} else { |
||||
|
collapsibleMenu.classList.add("-translate-x-full"); |
||||
|
menuScrim.classList.add("hidden"); |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
function handleColorToggles(e) { |
||||
|
const colorToggles = [ |
||||
|
toggleSnrColor, |
||||
|
toggleClusterColor, |
||||
|
toggleInlierColor, |
||||
|
toggleStationaryColor, |
||||
|
]; |
||||
|
if (e.target.checked) { |
||||
|
colorToggles.forEach((o) => { |
||||
|
if (o !== e.target) o.checked = false; |
||||
|
}); |
||||
|
} |
||||
|
if (appState.p5_instance) appState.p5_instance.redraw(); |
||||
|
updatePersistentOverlays(videoPlayer.currentTime); |
||||
|
} |
||||
|
|
||||
|
export function initUIEventListeners() { |
||||
|
// --- Menu and Fullscreen ---
|
||||
|
toggleMenuBtn.addEventListener("click", () => toggleMenu(true)); |
||||
|
closeMenuBtn.addEventListener("click", () => toggleMenu(false)); |
||||
|
menuScrim.addEventListener("click", () => toggleMenu(false)); |
||||
|
fullscreenBtn.addEventListener("click", () => { |
||||
|
if (!document.fullscreenElement) { |
||||
|
document.documentElement.requestFullscreen(); |
||||
|
} else if (document.exitFullscreen) { |
||||
|
document.exitFullscreen(); |
||||
|
} |
||||
|
}); |
||||
|
|
||||
|
// --- Timeline Tooltip ---
|
||||
|
timelineSlider.addEventListener("mouseover", () => { |
||||
|
if (appState.vizData) timelineTooltip.classList.remove("hidden"); |
||||
|
}); |
||||
|
timelineSlider.addEventListener("mouseout", () => { |
||||
|
timelineTooltip.classList.add("hidden"); |
||||
|
}); |
||||
|
timelineSlider.addEventListener("mousemove", (event) => { |
||||
|
if (!appState.vizData) return; |
||||
|
const rect = timelineSlider.getBoundingClientRect(); |
||||
|
const hoverFraction = (event.clientX - rect.left) / rect.width; |
||||
|
const sliderMax = parseInt(timelineSlider.max, 10) || appState.vizData.radarFrames.length - 1; |
||||
|
let frameIndex = Math.max(0, Math.min(Math.round(hoverFraction * sliderMax), sliderMax)); |
||||
|
const frameData = appState.vizData.radarFrames[frameIndex]; |
||||
|
if (!frameData) return; |
||||
|
const formattedTime = formatTime(frameData.relativeTimeSec * 1000); |
||||
|
timelineTooltip.innerHTML = `Frame: ${frameIndex + 1}<br>Time: ${formattedTime}`; |
||||
|
const tooltipX = event.clientX - rect.left; |
||||
|
timelineTooltip.style.left = `${tooltipX}px`; |
||||
|
}); |
||||
|
|
||||
|
// --- Speed Slider ---
|
||||
|
speedSlider.addEventListener("input", (event) => { |
||||
|
const speed = parseFloat(event.target.value); |
||||
|
videoPlayer.playbackRate = speed; |
||||
|
speedDisplay.textContent = `${speed.toFixed(1)}x`; |
||||
|
}); |
||||
|
|
||||
|
// --- SNR Controls ---
|
||||
|
applySnrBtn.addEventListener("click", () => { |
||||
|
const newMin = parseFloat(snrMinInput.value), newMax = parseFloat(snrMaxInput.value); |
||||
|
if (isNaN(newMin) || isNaN(newMax) || newMin >= newMax) { |
||||
|
showModal("Invalid SNR range."); |
||||
|
return; |
||||
|
} |
||||
|
appState.globalMinSnr = newMin; |
||||
|
appState.globalMaxSnr = newMax; |
||||
|
toggleFrameNorm.checked = false; |
||||
|
if (appState.p5_instance) { |
||||
|
appState.p5_instance.drawSnrLegendToBuffer(appState.globalMinSnr, appState.globalMaxSnr); |
||||
|
appState.p5_instance.redraw(); |
||||
|
} |
||||
|
}); |
||||
|
|
||||
|
// --- Feature Toggles ---
|
||||
|
[toggleSnrColor, toggleClusterColor, toggleInlierColor, toggleStationaryColor].forEach((t) => { |
||||
|
t.addEventListener("change", handleColorToggles); |
||||
|
}); |
||||
|
|
||||
|
[toggleVelocity, toggleEgoSpeed, toggleFrameNorm, toggleTracks, toggleDebugOverlay, toggleDebug2Overlay].forEach((t) => { |
||||
|
t.addEventListener("change", () => { |
||||
|
if (appState.p5_instance) appState.p5_instance.redraw(); |
||||
|
if (t === toggleDebugOverlay || t === toggleDebug2Overlay) { |
||||
|
updateDebugOverlay(videoPlayer.currentTime); |
||||
|
updatePersistentOverlays(videoPlayer.currentTime); |
||||
|
} |
||||
|
}); |
||||
|
}); |
||||
|
|
||||
|
toggleCloseUp.addEventListener("change", () => { |
||||
|
appState.isCloseUpMode = toggleCloseUp.checked; |
||||
|
if (appState.isCloseUpMode && appState.isPlaying) { |
||||
|
// If entering close-up mode while playing, automatically pause.
|
||||
|
pausePlayback(); |
||||
|
appState.isPlaying = false; |
||||
|
playPauseBtn.textContent = "Play"; |
||||
|
} |
||||
|
if (appState.p5_instance) { // Handle p5 loop state
|
||||
|
if (appState.isCloseUpMode) { |
||||
|
appState.p5_instance.loop(); // Start looping for mouse interaction.
|
||||
|
} else { |
||||
|
appState.p5_instance.noLoop(); // Stop looping when exiting.
|
||||
|
appState.p5_instance.redraw(); // Redraw one last time.
|
||||
|
} |
||||
|
} |
||||
|
}); |
||||
|
|
||||
|
toggleConfirmedOnly.addEventListener("change", () => { |
||||
|
if (appState.p5_instance) appState.p5_instance.redraw(); |
||||
|
}); |
||||
|
} |
||||
Write
Preview
Loading…
Cancel
Save
Reference in new issue