diff --git a/web/app.js b/web/app.js new file mode 100644 index 0000000..bc5e54a --- /dev/null +++ b/web/app.js @@ -0,0 +1,346 @@ +//! MoMentry Playground - Frontend JavaScript + +(function() { + 'use strict'; + + const state = { + playing: false, + currentFrame: 0, + totalFrames: 0, + currentTimeMs: 0, + durationMs: 0, + fps: 0, + volume: 1.0, + muted: false, + speed: 1.0, + showSubtitle: false, + showYolo: false, + showChunks: false, + zoom: 1.0, + panX: 0, + panY: 0 + }; + + const elements = {}; + + function init() { + cacheElements(); + bindEvents(); + updateUI(); + } + + function cacheElements() { + elements.btnOpen = document.getElementById('btn-open'); + elements.inputFile = document.getElementById('input-file'); + elements.btnPlay = document.getElementById('btn-play'); + elements.btnPrev = document.getElementById('btn-prev'); + elements.btnNext = document.getElementById('btn-next'); + elements.btnMute = document.getElementById('btn-mute'); + elements.volumeSlider = document.getElementById('volume-slider'); + elements.selectSpeed = document.getElementById('select-speed'); + elements.btnSubtitle = document.getElementById('btn-subtitle'); + elements.btnYolo = document.getElementById('btn-yolo'); + elements.btnChunks = document.getElementById('btn-chunks'); + elements.progressBar = document.getElementById('progress-bar'); + elements.currentTime = document.getElementById('current-time'); + elements.totalTime = document.getElementById('total-time'); + elements.statusPlayback = document.getElementById('status-playback'); + elements.statusFrame = document.getElementById('status-frame'); + elements.statusFps = document.getElementById('status-fps'); + elements.statusZoom = document.getElementById('status-zoom'); + elements.subtitleOverlay = document.getElementById('subtitle-overlay'); + elements.subtitleText = document.getElementById('subtitle-text'); + elements.mediaContainer = document.getElementById('media-container'); + elements.videoCanvas = document.getElementById('video-canvas'); + } + + function bindEvents() { + elements.btnOpen.addEventListener('click', onOpenClick); + elements.btnPlay.addEventListener('click', onPlayClick); + elements.btnPrev.addEventListener('click', onPrevClick); + elements.btnNext.addEventListener('click', onNextClick); + elements.btnMute.addEventListener('click', onMuteClick); + elements.volumeSlider.addEventListener('input', onVolumeChange); + elements.selectSpeed.addEventListener('change', onSpeedChange); + elements.btnSubtitle.addEventListener('click', () => toggleOverlay('subtitle')); + elements.btnYolo.addEventListener('click', () => toggleOverlay('yolo')); + elements.btnChunks.addEventListener('click', () => toggleOverlay('chunks')); + elements.progressBar.addEventListener('input', onSeek); + + document.addEventListener('keydown', onKeyDown); + elements.inputFile.addEventListener('keypress', (e) => { + if (e.key === 'Enter') { + openFile(elements.inputFile.value); + } + }); + } + + function onOpenClick() { + const path = elements.inputFile.value.trim(); + if (path) { + openFile(path); + } + } + + function openFile(path) { + sendCommand({ type: 'OpenFile', data: path }); + } + + function onPlayClick() { + if (state.playing) { + sendCommand({ type: 'Pause' }); + } else { + sendCommand({ type: 'Play' }); + } + } + + function onPrevClick() { + sendCommand({ type: 'StepBackward' }); + } + + function onNextClick() { + sendCommand({ type: 'StepForward' }); + } + + function onMuteClick() { + state.muted = !state.muted; + sendCommand({ type: 'SetMuted', data: state.muted }); + updateMuteButton(); + } + + function onVolumeChange(e) { + state.volume = parseFloat(e.target.value) / 100; + sendCommand({ type: 'SetVolume', data: state.volume }); + } + + function onSpeedChange(e) { + state.speed = parseFloat(e.target.value); + sendCommand({ type: 'SetSpeed', data: state.speed }); + } + + function onSeek(e) { + const percent = parseFloat(e.target.value); + const timeMs = (percent / 100) * state.durationMs; + sendCommand({ type: 'SeekTime', data: timeMs }); + } + + function toggleOverlay(type) { + switch (type) { + case 'subtitle': + state.showSubtitle = !state.showSubtitle; + elements.btnSubtitle.dataset.active = state.showSubtitle; + sendCommand({ type: 'ToggleSubtitle' }); + break; + case 'yolo': + state.showYolo = !state.showYolo; + elements.btnYolo.dataset.active = state.showYolo; + sendCommand({ type: 'ToggleYolo' }); + break; + case 'chunks': + state.showChunks = !state.showChunks; + elements.btnChunks.dataset.active = state.showChunks; + sendCommand({ type: 'ToggleChunks' }); + break; + } + } + + function onKeyDown(e) { + switch (e.key) { + case ' ': + e.preventDefault(); + onPlayClick(); + break; + case 'ArrowLeft': + e.preventDefault(); + if (e.shiftKey) { + sendCommand({ type: 'SeekTime', data: state.currentTimeMs - 1000 }); + } else { + onPrevClick(); + } + break; + case 'ArrowRight': + e.preventDefault(); + if (e.shiftKey) { + sendCommand({ type: 'SeekTime', data: state.currentTimeMs + 1000 }); + } else { + onNextClick(); + } + break; + case 's': + case 'S': + toggleOverlay('subtitle'); + break; + case 'y': + case 'Y': + toggleOverlay('yolo'); + break; + case 'c': + case 'C': + toggleOverlay('chunks'); + break; + case 'm': + case 'M': + onMuteClick(); + break; + case '+': + case '=': + zoomIn(); + break; + case '-': + zoomOut(); + break; + case '0': + resetZoom(); + break; + case 'r': + case 'R': + resetView(); + break; + case 'f': + case 'F': + toggleFullscreen(); + break; + case '[': + sendCommand({ type: 'PrevChunk' }); + break; + case ']': + sendCommand({ type: 'NextChunk' }); + break; + case '/': + showSearchPanel(); + break; + case 'Escape': + hideSearchPanel(); + break; + } + } + + function zoomIn() { + state.zoom = Math.min(10, state.zoom * 1.25); + updateTransform(); + updateZoomDisplay(); + } + + function zoomOut() { + state.zoom = Math.max(0.1, state.zoom / 1.25); + updateTransform(); + updateZoomDisplay(); + } + + function resetZoom() { + state.zoom = 1.0; + updateTransform(); + updateZoomDisplay(); + } + + function resetView() { + state.zoom = 1.0; + state.panX = 0; + state.panY = 0; + updateTransform(); + updateZoomDisplay(); + } + + function updateTransform() { + elements.mediaContainer.style.transform = + `translate(${state.panX}px, ${state.panY}px) scale(${state.zoom})`; + } + + function updateZoomDisplay() { + elements.statusZoom.textContent = `${Math.round(state.zoom * 100)}%`; + } + + function toggleFullscreen() { + if (document.fullscreenElement) { + document.exitFullscreen(); + } else { + document.documentElement.requestFullscreen(); + } + } + + function showSearchPanel() { + document.getElementById('search-panel').style.display = 'block'; + document.getElementById('search-input').focus(); + } + + function hideSearchPanel() { + document.getElementById('search-panel').style.display = 'none'; + } + + function updateMuteButton() { + elements.btnMute.textContent = state.muted ? '🔇' : '🔊'; + } + + function updateUI() { + updatePlayButton(); + updateTimeDisplay(); + updateProgressBar(); + updateStatusBar(); + } + + function updatePlayButton() { + elements.btnPlay.textContent = state.playing ? '⏸' : '▶'; + elements.statusPlayback.textContent = state.playing ? 'Playing' : 'Paused'; + } + + function updateTimeDisplay() { + elements.currentTime.textContent = formatTime(state.currentTimeMs); + elements.totalTime.textContent = formatTime(state.durationMs); + } + + function updateProgressBar() { + if (state.durationMs > 0) { + const percent = (state.currentTimeMs / state.durationMs) * 100; + elements.progressBar.value = percent; + } + } + + function updateStatusBar() { + elements.statusFrame.textContent = `Frame: ${state.currentFrame}/${state.totalFrames}`; + elements.statusFps.textContent = `| ${state.fps.toFixed(2)} fps`; + } + + function formatTime(ms) { + const totalSecs = Math.floor(ms / 1000); + const hours = Math.floor(totalSecs / 3600); + const minutes = Math.floor((totalSecs % 3600) / 60); + const seconds = totalSecs % 60; + return `${pad(hours)}:${pad(minutes)}:${pad(seconds)}`; + } + + function pad(num) { + return num.toString().padStart(2, '0'); + } + + function updateSubtitle(text) { + if (text) { + elements.subtitleText.textContent = text; + elements.subtitleOverlay.classList.add('visible'); + } else { + elements.subtitleOverlay.classList.remove('visible'); + } + } + + function updateState(newState) { + Object.assign(state, newState); + updateUI(); + } + + function sendCommand(cmd) { + if (window.momentrySendCommand) { + window.momentrySendCommand(JSON.stringify(cmd)); + } else { + console.log('Command (no bridge):', cmd); + } + } + + function handleResponse(response) { + console.log('Response:', response); + } + + window.momentryUpdateState = updateState; + window.momentryUpdateSubtitle = updateSubtitle; + window.momentryHandleResponse = handleResponse; + + document.addEventListener('DOMContentLoaded', init); +})();