347 lines
11 KiB
JavaScript
347 lines
11 KiB
JavaScript
//! 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);
|
|
})();
|