//! 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); })();