/** * CatCut - Edit by Narrative * Main Application Logic * Text Controls Video. */ class CatCutApp { constructor() { this.currentView = 'story'; // 'story' or 'visual' this.isPlaying = false; this.currentTime = 0; this.duration = 0; this.segments = []; this.clips = []; this.lockedClips = new Set(); this.init(); } init() { this.setupEventListeners(); this.setupVideoPlayer(); this.setupTextEditor(); this.setupFab(); this.setupExport(); this.setupVoiceMode(); this.simulateLoading(); this.initializeMockData(); } simulateLoading() { setTimeout(() => { const overlay = document.getElementById('loading-overlay'); overlay.style.opacity = '0'; setTimeout(() => { overlay.style.display = 'none'; }, 300); }, 1500); } initializeMockData() { // Mock story segments this.segments = [ { id: 'segment-1', clipId: 'clip-1', timestamp: 0, text: 'The morning sun filters through the window as I start my daily routine.', keywords: ['morning', 'sun', 'window', 'routine'], locked: false, matched: true }, { id: 'segment-2', clipId: 'clip-2', timestamp: 5, text: 'A perfect cup of coffee, the aroma filling the quiet space.', keywords: ['coffee', 'aroma', 'quiet', 'space'], locked: false, matched: true }, { id: 'segment-3', clipId: 'clip-3', timestamp: 10, text: 'Time to focus and create something meaningful today.', keywords: ['focus', 'create', 'meaningful', 'today'], locked: false, matched: false } ]; // Mock video clips this.clips = [ { id: 'clip-1', name: 'Morning Window', duration: 5, thumbnail: 'http://static.photos/nature/120x67/42', matched: true, keywords: ['morning', 'sun', 'window'] }, { id: 'clip-2', name: 'Coffee Aroma', duration: 5, thumbnail: 'http://static.photos/food/120x67/133', matched: true, keywords: ['coffee', 'aroma', 'steam'] }, { id: 'clip-3', name: 'Workspace', duration: 8, thumbnail: 'http://static.photos/workspace/120x67/99', matched: false, keywords: ['work', 'desk', 'computer'] } ]; this.renderClips(); } setupEventListeners() { // View switching document.addEventListener('viewChanged', (e) => { this.switchView(e.detail.view); }); // Theme switching const themeToggle = document.querySelector('#theme-toggle'); if (themeToggle) { themeToggle.addEventListener('click', () => this.toggleTheme()); } // Import button const importBtn = document.getElementById('import-btn'); if (importBtn) { importBtn.addEventListener('click', () => this.openImport()); } // Add segment button const addSegmentBtn = document.getElementById('add-segment-btn'); if (addSegmentBtn) { addSegmentBtn.addEventListener('click', () => this.addSegment()); } // AI Polish button const aiPolishBtn = document.getElementById('ai-polish-btn'); if (aiPolishBtn) { aiPolishBtn.addEventListener('click', () => this.aiPolish()); } // Lock all button const lockAllBtn = document.getElementById('lock-all-btn'); if (lockAllBtn) { lockAllBtn.addEventListener('click', () => this.toggleLockAll()); } // Segment locking document.addEventListener('click', (e) => { if (e.target.closest('.lock-btn')) { const btn = e.target.closest('.lock-btn'); this.toggleSegmentLock(btn); } }); // AI suggestion application document.addEventListener('click', (e) => { if (e.target.textContent === 'Apply →') { this.applyAISuggestion(e.target); } }); // Modal controls const exportCancel = document.getElementById('export-cancel'); const exportConfirm = document.getElementById('export-confirm'); if (exportCancel) exportCancel.addEventListener('click', () => this.closeExport()); if (exportConfirm) exportConfirm.addEventListener('click', () => this.confirmExport()); } setupVideoPlayer() { const video = document.getElementById('main-video'); const playBtn = document.getElementById('play-btn'); const progress = document.getElementById('video-progress'); const emptyState = document.getElementById('empty-state'); if (!video) return; // Video loaded video.addEventListener('loadedmetadata', () => { this.duration = video.duration; if (emptyState) emptyState.style.display = 'none'; }); // Play/Pause toggle if (playBtn) { playBtn.addEventListener('click', () => { if (this.isPlaying) { video.pause(); } else { video.play(); } }); } // Video playing video.addEventListener('play', () => { this.isPlaying = true; if (playBtn) { playBtn.innerHTML = ''; feather.replace(); } }); video.addEventListener('pause', () => { this.isPlaying = false; if (playBtn) { playBtn.innerHTML = ''; feather.replace(); } }); // Time update video.addEventListener('timeupdate', () => { this.currentTime = video.currentTime; const progressPercent = (this.currentTime / this.duration) * 100; if (progress) { progress.style.width = `${progressPercent}%`; } this.highlightActiveSegment(); }); // Video ended video.addEventListener('ended', () => { this.isPlaying = false; if (progress) progress.style.width = '0%'; if (playBtn) { playBtn.innerHTML = ''; feather.replace(); } }); } setupTextEditor() { const editor = document.getElementById('text-editor'); if (!editor) return; // Make segments editable editor.addEventListener('input', (e) => { if (e.target.classList.contains('segment-text')) { this.updateSegmentText(e.target); } }); // Segment clicking editor.addEventListener('click', (e) => { const segment = e.target.closest('.story-segment'); if (segment) { this.selectSegment(segment); } }); } setupFab() { const fab = document.getElementById('fab-toggle-view'); if (!fab) return; fab.addEventListener('click', () => { this.toggleView(); }); } setupExport() { const exportBtn = document.querySelector('#export-btn'); if (exportBtn) { exportBtn.addEventListener('click', () => this.openExport()); } // Platform selection document.addEventListener('click', (e) => { if (e.target.closest('.platform-btn')) { const btn = e.target.closest('.platform-btn'); this.selectPlatform(btn); } }); } setupVoiceMode() { const voiceBtn = document.getElementById('voice-record-btn'); if (!voiceBtn) return; let isRecording = false; voiceBtn.addEventListener('click', () => { isRecording = !isRecording; if (isRecording) { voiceBtn.classList.add('voice-recording'); voiceBtn.innerHTML = 'Stop Recording'; feather.replace(); this.startVoiceRecording(); } else { voiceBtn.classList.remove('voice-recording'); voiceBtn.innerHTML = 'Record Narration'; feather.replace(); this.stopVoiceRecording(); } }); } toggleView() { const newView = this.currentView === 'story' ? 'visual' : 'story'; this.currentView = newView; // Dispatch custom event for components document.dispatchEvent(new CustomEvent('viewChanged', { detail: { view: newView } })); // Update FAB icon this.updateFabIcon(); } switchView(view) { const narrativeDeck = document.getElementById('narrative-deck'); const videoStage = document.getElementById('video-stage'); if (view === 'visual') { // Switch to visual/timeline view narrativeDeck.style.flex = '0.8'; videoStage.style.flex = '1.5'; // Replace narrative deck content with timeline if (!document.querySelector('clip-timeline')) { const timeline = document.createElement('clip-timeline'); narrativeDeck.appendChild(timeline); } } else { // Switch back to story view narrativeDeck.style.flex = '1'; videoStage.style.flex = '1.2'; // Remove timeline if exists const timeline = document.querySelector('clip-timeline'); if (timeline) { timeline.remove(); } } } updateFabIcon() { const storyIcon = document.getElementById('story-icon'); const visualIcon = document.getElementById('visual-icon'); if (this.currentView === 'story') { storyIcon.classList.add('active'); storyIcon.classList.remove('inactive'); visualIcon.classList.add('inactive'); visualIcon.classList.remove('active'); } else { visualIcon.classList.add('active'); visualIcon.classList.remove('inactive'); storyIcon.classList.add('inactive'); storyIcon.classList.remove('active'); } } selectSegment(segmentEl) { // Remove active from all segments document.querySelectorAll('.story-segment').forEach(s => s.classList.remove('active')); // Add active to clicked segment segmentEl.classList.add('active'); // Sync video to segment const timestamp = parseFloat(segmentEl.dataset.timestamp); const video = document.getElementById('main-video'); if (video && !isNaN(timestamp)) { video.currentTime = timestamp; } } highlightActiveSegment() { const segments = document.querySelectorAll('.story-segment'); segments.forEach((segment, index) => { const startTime = parseFloat(segment.dataset.timestamp); const endTime = index < segments.length - 1 ? parseFloat(segments[index + 1].dataset.timestamp) : this.duration; if (this.currentTime >= startTime && this.currentTime < endTime) { segment.classList.add('active'); } else { segment.classList.remove('active'); } }); } toggleSegmentLock(btn) { const segment = btn.closest('.story-segment'); const clipId = segment.dataset.clipId; const isLocked = btn.dataset.locked === 'true'; btn.dataset.locked = !isLocked; btn.innerHTML = !isLocked ? '' : ''; feather.replace(); if (!isLocked) { this.lockedClips.add(clipId); segment.classList.add('locked'); } else { this.lockedClips.delete(clipId); segment.classList.remove('locked'); } } updateSegmentText(textEl) { const segment = textEl.closest('.story-segment'); const segmentId = segment.dataset.clipId; // Simulate AI re-matching this.simulateAIMatching(segment, textEl.textContent); // Update segment data const segmentData = this.segments.find(s => s.clipId === segmentId); if (segmentData) { segmentData.text = textEl.textContent; } } simulateAIMatching(segment, text) { const matchStatus = segment.querySelector('.match-status'); if (matchStatus) { matchStatus.textContent = '🔍 Analyzing...'; matchStatus.className = 'match-status searching'; setTimeout(() => { matchStatus.textContent = '✓ Re-matched: ' + text.split(' ').slice(0, 3).join(', '); matchStatus.className = 'match-status matched'; }, 1000); } } renderClips() { // This would be called by the timeline component // For now, just a placeholder } addSegment() { const editor = document.getElementById('text-editor'); const newSegment = document.createElement('div'); newSegment.className = 'story-segment'; newSegment.dataset.timestamp = this.segments.length * 5; newSegment.dataset.clipId = `clip-${this.segments.length + 1}`; newSegment.innerHTML = `

New story segment. Describe what should appear here.

🔍 Searching for matches...
`; editor.appendChild(newSegment); feather.replace(); // Add to segments array this.segments.push({ id: `segment-${this.segments.length + 1}`, clipId: `clip-${this.segments.length + 1}`, timestamp: this.segments.length * 5, text: 'New story segment. Describe what should appear here.', keywords: [], locked: false, matched: false }); } aiPolish() { const btn = document.getElementById('ai-polish-btn'); const originalText = btn.innerHTML; btn.innerHTML = 'Polishing...'; feather.replace(); setTimeout(() => { // Simulate AI rewriting text const segments = document.querySelectorAll('.segment-text'); segments.forEach(textEl => { if (!textEl.closest('.locked')) { const currentText = textEl.textContent; textEl.textContent = this.polishText(currentText); } }); btn.innerHTML = originalText; feather.replace(); // Show success feedback this.showNotification('AI successfully polished your story!', 'success'); }, 2000); } polishText(text) { // Simple text enhancement simulation const enhancements = [ 'As golden rays illuminate the space, ', 'With aromatic steam rising, ', 'In this moment of clarity, ', 'The serene atmosphere whispers ', 'Every detail tells a story of ' ]; const randomEnhancement = enhancements[Math.floor(Math.random() * enhancements.length)]; return randomEnhancement + text.toLowerCase(); } toggleLockAll() { const lockAllBtn = document.getElementById('lock-all-btn'); const isCurrentlyLocked = lockAllBtn.dataset.locked === 'true'; const lockButtons = document.querySelectorAll('.lock-btn'); lockButtons.forEach(btn => { const segmentLocked = btn.dataset.locked === 'true'; if (!isCurrentlyLocked && !segmentLocked) { btn.click(); } else if (isCurrentlyLocked && segmentLocked) { btn.click(); } }); lockAllBtn.dataset.locked = !isCurrentlyLocked; lockAllBtn.innerHTML = !isCurrentlyLocked ? 'Unlock All' : 'Lock All'; feather.replace(); } applyAISuggestion(btn) { const card = btn.closest('.ai-suggestion-card'); card.style.opacity = '0.5'; btn.textContent = '✓ Applied'; setTimeout(() => { card.style.opacity = '1'; }, 500); this.showNotification('AI suggestion applied successfully!', 'success'); } startVoiceRecording() { // Simulate voice recording this.showNotification('Recording started... Speak your story!', 'info'); } stopVoiceRecording() { // Simulate transcription this.showNotification('Transcribing your voice...', 'info'); setTimeout(() => { const editor = document.getElementById('text-editor'); const voiceText = 'I woke up feeling energized today. The sunlight was perfect for filming. I grabbed my coffee and started creating.'; // Clear existing segments editor.innerHTML = ''; this.segments = []; // Add new segments from voice const sentences = voiceText.split('.').filter(s => s.trim()); sentences.forEach((sentence, index) => { if (sentence.trim()) { const newSegment = document.createElement('div'); newSegment.className = 'story-segment'; newSegment.dataset.timestamp = index * 4; newSegment.dataset.clipId = `clip-${index + 1}`; newSegment.innerHTML = `

${sentence.trim()}.

✓ Matched from voice
`; editor.appendChild(newSegment); this.segments.push({ id: `segment-${index + 1}`, clipId: `clip-${index + 1}`, timestamp: index * 4, text: sentence.trim() + '.', keywords: sentence.trim().toLowerCase().split(' '), locked: false, matched: true }); } }); feather.replace(); this.showNotification('Voice transcription complete! Video synced to your narration.', 'success'); }, 3000); } openImport() { this.showNotification('Import dialog opened (simulation)', 'info'); // In real app, would open file picker } toggleTheme() { const html = document.documentElement; const currentTheme = html.getAttribute('data-theme'); const newTheme = currentTheme === 'light' ? 'dark' : 'light'; html.setAttribute('data-theme', newTheme); this.showNotification(`Switched to ${newTheme} mode`, 'info'); } openExport() { const modal = document.getElementById('export-modal'); modal.classList.add('active'); } closeExport() { const modal = document.getElementById('export-modal'); modal.classList.remove('active'); } confirmExport() { this.closeExport(); this.showNotification('Export started! Preparing your story...', 'info'); // Simulate export progress let progress = 0; const interval = setInterval(() => { progress += Math.random() * 20; if (progress >= 100) { progress = 100; clearInterval(interval); this.showNotification('Export complete! Your video is ready.', 'success'); } }, 500); } selectPlatform(btn) { document.querySelectorAll('.platform-btn').forEach(b => b.classList.remove('active')); btn.classList.add('active'); } showNotification(message, type = 'info') { // Create notification element const notification = document.createElement('div'); notification.className = `fixed top-4 right-4 z-50 px-4 py-3 rounded-lg shadow-lg transform translate-x-full transition-transform duration-300`; // Set colors based on type const colors = { success: 'bg-green-500 text-white', error: 'bg-red-500 text-white', info: 'bg-amber-500 text-white', warning: 'bg-yellow-500 text-black' }; notification.className += ` ${colors[type] || colors.info}`; notification.textContent = message; document.body.appendChild(notification); // Animate in setTimeout(() => { notification.style.transform = 'translateX(0)'; }, 100); // Auto remove setTimeout(() => { notification.style.transform = 'translateX(full)'; setTimeout(() => notification.remove(), 300); }, 3000); } } // Initialize app when DOM is loaded document.addEventListener('DOMContentLoaded', () => { window.catCutApp = new CatCutApp(); });