/* ================================================================
Facebook Automation — app.js v12
================================================================ */
'use strict';
const state = {
statsFiles: 0,
statsClips: 0,
};
function $(id) { return document.getElementById(id); }
function $q(sel) { return document.querySelector(sel); }
let toastTimeout = null;
function showToast(msg, type = 'success', duration = 4000) {
const t = $('toast');
if (!t) return;
if (toastTimeout) {
clearTimeout(toastTimeout);
toastTimeout = null;
}
t.innerHTML = `
${msg}
`;
t.className = `toast ${type} show`;
t.querySelector('#toast-close-btn')?.addEventListener('click', () => {
t.classList.remove('show');
});
// Keep error toasts visible until explicitly dismissed
if (type !== 'error') {
toastTimeout = setTimeout(() => {
t.classList.remove('show');
}, duration);
}
}
function animateCount(el, target) {
if (!el) return;
const start = parseInt(el.textContent) || 0;
const diff = target - start;
if (diff === 0) return;
const step = Math.ceil(Math.abs(diff) / 10) || 1;
const dir = diff > 0 ? 1 : -1;
let cur = start;
const iv = setInterval(() => {
cur += dir * step;
if ((dir > 0 && cur >= target) || (dir < 0 && cur <= target)) {
cur = target;
clearInterval(iv);
}
el.textContent = cur;
}, 30);
}
// ─────────────────────────────────────────────────────────────────
// GALLERY / LIBRARY
// ─────────────────────────────────────────────────────────────────
async function loadGallery() {
const container = $('gallery-container');
if (!container) return;
const searchVal = ($('library-search')?.value || '').toLowerCase();
try {
const res = await fetch('/api/outputs');
const data = await res.json();
const filtered = searchVal
? data.filter(f => f.filename.toLowerCase().includes(searchVal))
: data;
if (!filtered.length) {
container.innerHTML = `
${searchVal ? 'No results found' : 'No exported clips'}
${searchVal ? 'Try a different search term.' : 'Run a clipping operation to begin.'}
`;
animateCount($('stat-files'), 0);
return;
}
state.statsFiles = data.length;
animateCount($('stat-files'), data.length);
container.innerHTML = filtered.map(file => {
const isClip = file.filename.includes('clip_');
const iconSvg = isClip
? ``
: ``;
return `
${iconSvg} ${file.filename}
${file.duration} · ${file.size}
`;
}).join('');
// Wire up play / download
container.querySelectorAll('.play-action').forEach(btn => {
btn.addEventListener('click', () => openVideoModal(btn.dataset.fn));
});
container.querySelectorAll('.download-action').forEach(btn => {
btn.addEventListener('click', () => {
const a = document.createElement('a');
a.href = `/api/outputs/${btn.dataset.fn}`;
a.download = btn.dataset.fn;
a.click();
});
});
} catch (e) {
console.error('Error loading library:', e);
container.innerHTML = ``;
}
}
// ─────────────────────────────────────────────────────────────────
// CLEAR LIBRARY
// ─────────────────────────────────────────────────────────────────
async function clearLibrary() {
const confirmed = confirm('🗑️ Delete ALL exported files from disk?\n\nThis cannot be undone.');
if (!confirmed) return;
const btn = $('clear-library-btn');
if (btn) btn.disabled = true;
try {
const res = await fetch('/api/clear-library', { method: 'POST' });
const data = await res.json();
showToast(`✅ Cleared ${data.deleted} file${data.deleted !== 1 ? 's' : ''} from library`, 'success');
animateCount($('stat-files'), 0);
animateCount($('stat-clips'), 0);
await loadGallery();
} catch (e) {
showToast('❌ Failed to clear library', 'error');
} finally {
if (btn) btn.disabled = false;
}
}
// ─────────────────────────────────────────────────────────────────
// VIDEO PLAYER MODAL
// ─────────────────────────────────────────────────────────────────
function openVideoModal(filename) {
$('modal-title').textContent = `▶ ${filename}`;
$('modal-player').src = `/api/outputs/${filename}`;
$('video-modal').classList.add('active');
$('modal-player').play();
}
function closeVideoModal() {
$('modal-player').pause();
$('modal-player').src = '';
$('video-modal').classList.remove('active');
}
// ─────────────────────────────────────────────────────────────────
// SLICING TOGGLE
// ─────────────────────────────────────────────────────────────────
function initAudioToggle() {
const clipToggles = document.querySelectorAll('.segment-tab-btn');
clipToggles.forEach(btn => {
btn.addEventListener('click', () => {
clipToggles.forEach(b => b.classList.remove('active'));
btn.classList.add('active');
const type = btn.dataset.type;
$('auto-clip-panel')?.classList.toggle('active', type === 'auto');
$('timestamps-clip-panel')?.classList.toggle('active', type === 'timestamps');
});
});
}
function startProgress(fillId, pctId, stepId, steps, onDone) {
const fill = $(fillId);
const pct = $(pctId);
const step = $(stepId);
if (!fill) return null;
let idx = 0;
let cur = 0;
const iv = setInterval(() => {
if (idx >= steps.length) { clearInterval(iv); onDone?.(); return; }
const { pct: target, label, dur } = steps[idx];
step.textContent = label;
const speed = (target - cur) / (dur / 120);
const inner = setInterval(() => {
cur = Math.min(cur + speed, target);
fill.style.width = `${cur}%`;
pct.textContent = `${Math.round(cur)}%`;
if (cur >= target) { clearInterval(inner); idx++; }
}, 120);
}, 0);
return iv;
}
function showProgress(progressId) {
$(progressId)?.classList.add('visible');
}
function hideProgress(progressId) {
$(progressId)?.classList.remove('visible');
}
// ─────────────────────────────────────────────────────────────────
// CLIP FORM SUBMISSION
// ─────────────────────────────────────────────────────────────────
function initClipForm() {
const form = $('clip-form');
if (!form) return;
// Import source tab switching logic
const sourceToggles = document.querySelectorAll('.source-tabs .segment-tab-btn');
let activeSource = 'youtube'; // default
sourceToggles.forEach(btn => {
btn.addEventListener('click', () => {
sourceToggles.forEach(b => b.classList.remove('active'));
btn.classList.add('active');
activeSource = btn.dataset.source;
$('source-panel-youtube')?.classList.toggle('active', activeSource === 'youtube');
$('source-panel-upload')?.classList.toggle('active', activeSource === 'upload');
});
});
$('video_file')?.addEventListener('change', e => {
const file = e.target.files[0];
const label = $('file-name-label');
if (label) label.textContent = file ? file.name : 'No file selected';
});
form.addEventListener('submit', async e => {
e.preventDefault();
if (activeSource === 'youtube') {
const url = $('url')?.value.trim();
if (!url || (!url.includes('youtube.com') && !url.includes('youtu.be'))) {
showToast('⚠️ Please enter a valid YouTube URL', 'error'); return;
}
} else {
const fileInput = $('video_file');
if (!fileInput || !fileInput.files || fileInput.files.length === 0) {
showToast('⚠️ Please select a local video file to upload', 'error'); return;
}
}
const btn = $('clip-submit-btn');
btn.disabled = true; btn.classList.add('loading');
showProgress('clip-progress');
const steps = [
{ pct: 20, label: activeSource === 'youtube' ? '⬇️ Downloading video…' : '⬇️ Uploading video…', dur: 3000 },
{ pct: 50, label: '✂️ Slicing into clips…', dur: 2500 },
{ pct: 75, label: '🎨 Applying safety filters…', dur: 2000 },
{ pct: 90, label: '💾 Saving clips to library…', dur: 1200 },
{ pct: 99, label: '📦 Cleaning up temp files…', dur: 600 },
];
startProgress('clip-fill', 'clip-pct', 'clip-step', steps);
try {
const fd = new FormData(form);
const activeMethodBtn = $q('.segment-tab-btn.active');
if (activeMethodBtn) {
fd.set('slicing_method', activeMethodBtn.dataset.type);
}
// Explicitly set source type
fd.set('import_source', activeSource);
const res = await fetch('/api/clip', { method: 'POST', body: fd });
const data = await res.json();
$('clip-fill').style.width = '100%';
$('clip-pct').textContent = '100%';
$('clip-step').textContent = '✅ Done!';
if (data.success) {
showToast(`✅ ${data.message}`, 'success', 5000);
state.statsClips += data.filenames?.length || 0;
animateCount($('stat-clips'), state.statsClips);
await loadGallery();
} else {
showToast(`❌ ${data.error}`, 'error', 7000);
}
} catch (err) {
showToast('❌ Network error — check server logs', 'error');
} finally {
btn.disabled = false; btn.classList.remove('loading');
setTimeout(() => hideProgress('clip-progress'), 2000);
}
});
}
// ─────────────────────────────────────────────────────────────────
// THEME SWITCHER
// ─────────────────────────────────────────────────────────────────
function initThemeSwitcher() {
const toggleBtn = $('theme-toggle');
if (!toggleBtn) return;
const sunIcon = toggleBtn.querySelector('.sun-icon');
const moonIcon = toggleBtn.querySelector('.moon-icon');
// Load initial theme from localStorage
const savedTheme = localStorage.getItem('theme') || 'dark';
if (savedTheme === 'light') {
document.body.classList.add('light-theme');
if (sunIcon) sunIcon.style.display = 'none';
if (moonIcon) moonIcon.style.display = 'block';
}
toggleBtn.addEventListener('click', () => {
const isLight = document.body.classList.toggle('light-theme');
localStorage.setItem('theme', isLight ? 'light' : 'dark');
if (isLight) {
if (sunIcon) sunIcon.style.display = 'none';
if (moonIcon) moonIcon.style.display = 'block';
showToast('☀️ Switched to Light theme', 'success', 2000);
} else {
if (sunIcon) sunIcon.style.display = 'block';
if (moonIcon) moonIcon.style.display = 'none';
showToast('🌙 Switched to Dark theme', 'success', 2000);
}
});
}
// ─────────────────────────────────────────────────────────────────
// INITIALIZATION
// ─────────────────────────────────────────────────────────────────
document.addEventListener('DOMContentLoaded', () => {
initThemeSwitcher();
initAudioToggle();
initClipForm();
$('modal-close')?.addEventListener('click', closeVideoModal);
$('modal-close-btn')?.addEventListener('click', closeVideoModal);
document.addEventListener('keydown', e => { if (e.key === 'Escape') closeVideoModal(); });
// Ratio card visual selection
document.querySelectorAll('.aspect-card input[type=radio]').forEach(radio => {
radio.addEventListener('change', () => {
document.querySelectorAll('.aspect-card').forEach(c => c.classList.remove('selected'));
radio.closest('.aspect-card').classList.add('selected');
});
if (radio.checked) radio.closest('.aspect-card').classList.add('selected');
});
$('refresh-gallery')?.addEventListener('click', loadGallery);
$('download-all-btn')?.addEventListener('click', () => {
window.location.href = '/api/download-all';
});
$('clear-library-btn')?.addEventListener('click', clearLibrary);
$('library-search')?.addEventListener('input', loadGallery);
loadGallery();
// Auto-refresh gallery list every 30 seconds
setInterval(loadGallery, 30000);
});