Files
feedmill-recounter/static/app.js
T

199 lines
6.3 KiB
JavaScript

/**
* Feedmill Recounter — Upload page interactive features
* Progressive enhancement: form works without JS (basic file input).
*/
(function () {
'use strict';
const form = document.getElementById('upload-form');
const zone = document.getElementById('upload-zone');
const fileInput = document.getElementById('video-input');
const previewWrap = document.getElementById('video-preview');
const previewVideo = document.getElementById('preview-video');
const fileInfo = document.getElementById('file-info');
const fileName = document.getElementById('file-name');
const fileSize = document.getElementById('file-size');
const fileDuration = document.getElementById('file-duration');
const submitBtn = document.getElementById('submit-btn');
const submitHint = document.getElementById('submit-hint');
const selectAllBtn = document.getElementById('select-all-btn');
const deselectAllBtn = document.getElementById('deselect-all-btn');
const modelGrid = document.getElementById('model-grid');
if (!form || !zone || !fileInput) return;
/* ------------------------------------------------
Upload Zone — click, drag-drop, keyboard
------------------------------------------------ */
function initUploadZone() {
// Click anywhere in zone (including the hidden input) triggers browse
zone.addEventListener('keydown', function (e) {
if (e.key === 'Enter' || e.key === ' ') {
e.preventDefault();
fileInput.click();
}
});
// Drag events
['dragenter', 'dragover'].forEach(function (evt) {
zone.addEventListener(evt, function (e) {
e.preventDefault();
e.stopPropagation();
zone.classList.add('dragover');
});
});
['dragleave', 'drop'].forEach(function (evt) {
zone.addEventListener(evt, function (e) {
e.preventDefault();
e.stopPropagation();
zone.classList.remove('dragover');
});
});
zone.addEventListener('drop', function (e) {
var files = e.dataTransfer.files;
if (files.length > 0 && files[0].type.startsWith('video/')) {
fileInput.files = files;
handleFile(files[0]);
}
});
// File input change
fileInput.addEventListener('change', function () {
if (fileInput.files.length > 0) {
handleFile(fileInput.files[0]);
}
});
}
/* ------------------------------------------------
Video Preview & File Info
------------------------------------------------ */
function initVideoPreview() {
// Handled inside handleFile
}
function handleFile(file) {
if (!file || !file.type.startsWith('video/')) return;
// Show file info
fileName.textContent = file.name;
fileSize.textContent = formatBytes(file.size);
fileDuration.textContent = 'probing...';
fileInfo.classList.add('visible');
// Video preview via FileReader
var reader = new FileReader();
reader.onload = function (e) {
previewVideo.src = e.target.result;
previewVideo.load();
previewWrap.classList.add('visible');
previewVideo.onloadedmetadata = function () {
fileDuration.textContent = formatDuration(previewVideo.duration);
};
previewVideo.onerror = function () {
fileDuration.textContent = 'unknown';
};
};
reader.readAsDataURL(file);
updateSubmitState();
}
function formatBytes(bytes) {
if (bytes === 0) return '0 B';
var k = 1024;
var sizes = ['B', 'KB', 'MB', 'GB'];
var i = Math.floor(Math.log(bytes) / Math.log(k));
return parseFloat((bytes / Math.pow(k, i)).toFixed(1)) + ' ' + sizes[i];
}
function formatDuration(seconds) {
if (!isFinite(seconds)) return 'unknown';
var m = Math.floor(seconds / 60);
var s = Math.floor(seconds % 60);
return m + ':' + (s < 10 ? '0' : '') + s;
}
/* ------------------------------------------------
Model Cards — select/deselect, highlight
------------------------------------------------ */
function initModelCards() {
if (!modelGrid) return;
var cards = modelGrid.querySelectorAll('.model-card');
cards.forEach(function (card) {
var checkbox = card.querySelector('.model-card-check');
// Click card body to toggle (avoid double-toggle when clicking checkbox directly)
card.addEventListener('click', function (e) {
if (e.target === checkbox) return;
if (e.target.tagName === 'SELECT') return;
checkbox.checked = !checkbox.checked;
card.classList.toggle('selected', checkbox.checked);
updateSubmitState();
});
checkbox.addEventListener('change', function () {
card.classList.toggle('selected', checkbox.checked);
updateSubmitState();
});
});
// Select All / Deselect All
if (selectAllBtn) {
selectAllBtn.addEventListener('click', function () {
cards.forEach(function (card) {
var cb = card.querySelector('.model-card-check');
cb.checked = true;
card.classList.add('selected');
});
updateSubmitState();
});
}
if (deselectAllBtn) {
deselectAllBtn.addEventListener('click', function () {
cards.forEach(function (card) {
var cb = card.querySelector('.model-card-check');
cb.checked = false;
card.classList.remove('selected');
});
updateSubmitState();
});
}
}
/* ------------------------------------------------
Submit — enable/disable, loading state
------------------------------------------------ */
function initFormSubmit() {
submitBtn.addEventListener('click', function () {
submitBtn.disabled = true;
submitBtn.textContent = 'Starting...';
submitHint.textContent = 'Uploading and starting analysis...';
});
}
function updateSubmitState() {
var hasVideo = fileInput.files && fileInput.files.length > 0;
var hasModel = modelGrid && modelGrid.querySelector('.model-card-check:checked');
var enabled = hasVideo && !!hasModel;
submitBtn.disabled = !enabled;
submitHint.textContent = enabled
? 'Ready to start analysis.'
: 'Select a video and at least one model to begin.';
}
/* ------------------------------------------------
Init
------------------------------------------------ */
initUploadZone();
initVideoPreview();
initModelCards();
initFormSubmit();
})();