Add app.js
This commit is contained in:
346
web/app.js
Normal file
346
web/app.js
Normal file
@@ -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);
|
||||
})();
|
||||
Reference in New Issue
Block a user