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