Add app.js

This commit is contained in:
2026-03-19 01:12:24 +08:00
parent 482879ba09
commit 24655cfc42

346
web/app.js Normal file
View 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);
})();