Spaces:
Running
Running
| class ScreenStreamApp { | |
| constructor() { | |
| this.thumbnailInterval = null; | |
| this.durationInterval = null; | |
| this.isRecording = false; | |
| this.init(); | |
| } | |
| init() { | |
| this.bindEvents(); | |
| this.loadRecordings(); | |
| this.checkRecordingStatus(); | |
| } | |
| bindEvents() { | |
| document.getElementById('startBtn').addEventListener('click', () => this.startRecording()); | |
| document.getElementById('stopBtn').addEventListener('click', () => this.stopRecording()); | |
| } | |
| async checkRecordingStatus() { | |
| try { | |
| const response = await fetch('/duration'); | |
| if (response.ok) { | |
| this.isRecording = true; | |
| this.updateUIForRecording(); | |
| this.startLivePreview(); | |
| this.startDurationTimer(); | |
| } else { | |
| this.isRecording = false; | |
| this.updateUIForIdle(); | |
| this.stopLivePreview(); | |
| this.stopDurationTimer(); | |
| } | |
| } catch (error) { | |
| console.error('Error checking recording status:', error); | |
| this.isRecording = false; | |
| this.updateUIForIdle(); | |
| } | |
| } | |
| async startRecording() { | |
| const filename = document.getElementById('filename').value.trim() || 'recording_' + Date.now(); | |
| const format = document.getElementById('format').value; | |
| try { | |
| const response = await fetch(`/start/${filename}?format=${format}`, { | |
| method: 'POST' | |
| }); | |
| if (response.status === 204) { | |
| this.isRecording = true; | |
| this.updateUIForRecording(); | |
| this.startLivePreview(); | |
| this.startDurationTimer(); | |
| this.showNotification('Recording started successfully!', 'success'); | |
| } else { | |
| this.showNotification('Failed to start recording', 'error'); | |
| } | |
| } catch (error) { | |
| console.error('Error starting recording:', error); | |
| this.showNotification('Error starting recording', 'error'); | |
| } | |
| } | |
| async stopRecording() { | |
| try { | |
| const response = await fetch('/stop', { | |
| method: 'POST' | |
| }); | |
| if (response.status === 204) { | |
| this.isRecording = false; | |
| this.updateUIForIdle(); | |
| this.stopLivePreview(); | |
| this.stopDurationTimer(); | |
| this.showNotification('Recording stopped successfully!', 'success'); | |
| this.loadRecordings(); | |
| } else { | |
| this.showNotification('Failed to stop recording', 'error'); | |
| } | |
| } catch (error) { | |
| console.error('Error stopping recording:', error); | |
| this.showNotification('Error stopping recording', 'error'); | |
| } | |
| } | |
| updateUIForRecording() { | |
| document.getElementById('startBtn').disabled = true; | |
| document.getElementById('stopBtn').disabled = false; | |
| document.getElementById('statusDot').classList.remove('hidden'); | |
| document.getElementById('statusText').textContent = 'Recording'; | |
| document.getElementById('thumbnail').classList.remove('hidden'); | |
| document.getElementById('noFeed').classList.add('hidden'); | |
| } | |
| updateUIForIdle() { | |
| document.getElementById('startBtn').disabled = false; | |
| document.getElementById('stopBtn').disabled = true; | |
| document.getElementById('statusDot').classList.add('hidden'); | |
| document.getElementById('statusText').textContent = 'Not Recording'; | |
| document.getElementById('thumbnail').classList.add('hidden'); | |
| document.getElementById('noFeed').classList.remove('hidden'); | |
| document.getElementById('duration').textContent = ''; | |
| } | |
| startLivePreview() { | |
| this.stopLivePreview(); | |
| this.updateThumbnail(); | |
| this.thumbnailInterval = setInterval(() => this.updateThumbnail(), 1000); | |
| } | |
| stopLivePreview() { | |
| if (this.thumbnailInterval) { | |
| clearInterval(this.thumbnailInterval); | |
| this.thumbnailInterval = null; | |
| } | |
| } | |
| async updateThumbnail() { | |
| const thumbnail = document.getElementById('thumbnail'); | |
| try { | |
| const response = await fetch('/thumbnail'); | |
| if (response.ok) { | |
| const blob = await response.blob(); | |
| const url = URL.createObjectURL(blob); | |
| thumbnail.src = url + '?t=' + Date.now(); | |
| } | |
| } catch (error) { | |
| console.error('Error updating thumbnail:', error); | |
| } | |
| } | |
| startDurationTimer() { | |
| this.stopDurationTimer(); | |
| this.updateDuration(); | |
| this.durationInterval = setInterval(() => this.updateDuration(), 1000); | |
| } | |
| stopDurationTimer() { | |
| if (this.durationInterval) { | |
| clearInterval(this.durationInterval); | |
| this.durationInterval = null; | |
| } | |
| } | |
| async updateDuration() { | |
| try { | |
| const response = await fetch('/duration'); | |
| if (response.ok) { | |
| const duration = await response.text(); | |
| document.getElementById('duration').textContent = this.formatDuration(parseInt(duration)); | |
| } | |
| } catch (error) { | |
| console.error('Error updating duration:', error); | |
| } | |
| } | |
| formatDuration(seconds) { | |
| const hours = Math.floor(seconds / 3600); | |
| const minutes = Math.floor((seconds % 3600) / 60); | |
| const secs = seconds % 60; | |
| return `${hours.toString().padStart(2, '0')}:${minutes.toString().padStart(2, '0')}:${secs.toString().padStart(2, '0')}`; | |
| } | |
| async loadRecordings() { | |
| try { | |
| const response = await fetch('/videos'); | |
| const videos = await response.json(); | |
| this.renderRecordings(videos); | |
| } catch (error) { | |
| console.error('Error loading recordings:', error); | |
| } | |
| } | |
| renderRecordings(videos) { | |
| const container = document.getElementById('recordingsList'); | |
| if (videos.length === 0) { | |
| container.innerHTML = ` | |
| <div class="text-center py-8 text-gray-500"> | |
| <i data-feather="folder" class="w-12 h-12 mx-auto mb-4"></i> | |
| <p>No recordings found</p> | |
| </div> | |
| `; | |
| return; | |
| } | |
| container.innerHTML = videos.map(video => ` | |
| <div class="flex items-center justify-between p-4 border border-gray-200 rounded-lg hover:border-primary transition-colors"> | |
| <div class="flex items-center gap-3"> | |
| <i data-feather="video" class="text-primary"></i> | |
| <div> | |
| <h3 class="font-semibold text-gray-800">${video}</h3> | |
| <p class="text-sm text-gray-500">Click to download</p> | |
| </div> | |
| </div> | |
| <div class="flex gap-2"> | |
| <a href="/recording/${video}" | |
| class="bg-primary hover:bg-blue-600 text-white px-4 py-2 rounded-lg text-sm font-medium transition-colors flex items-center gap-1"> | |
| <i data-feather="download"></i> | |
| Download | |
| </a> | |
| <button onclick="app.deleteRecording('${video}')" | |
| class="bg-red-500 hover:bg-red-600 text-white px-4 py-2 rounded-lg text-sm font-medium transition-colors flex items-center gap-1"> | |
| <i data-feather="trash-2"></i> | |
| Delete | |
| </button> | |
| </div> | |
| </div> | |
| `).join(''); | |
| feather.replace(); | |
| } | |
| async deleteRecording(filename) { | |
| if (!confirm(`Are you sure you want to delete "${filename}"?`)) { | |
| return; | |
| } | |
| try { | |
| const response = await fetch(`/recording/${filename}`, { | |
| method: 'DELETE' | |
| }); | |
| if (response.status === 204) { | |
| this.showNotification('Recording deleted successfully!', 'success'); | |
| this.loadRecordings(); | |
| } else { | |
| this.showNotification('Failed to delete recording', 'error'); | |
| } | |
| } catch (error) { | |
| console.error('Error deleting recording:', error); | |
| this.showNotification('Error deleting recording', 'error'); | |
| } | |
| } | |
| showNotification(message, type = 'info') { | |
| // Create notification element | |
| const notification = document.createElement('div'); | |
| notification.className = `fixed top-4 right-4 z-50 p-4 rounded-lg shadow-lg transform transition-transform duration-300 translate-x-full ${ | |
| type === 'success' ? 'bg-secondary' : | |
| type === 'error' ? 'bg-red-500' : | |
| 'bg-primary' | |
| } text-white font-medium`; | |
| notification.textContent = message; | |
| document.body.appendChild(notification); | |
| // Animate in | |
| setTimeout(() => { | |
| notification.classList.remove('translate-x-full'); | |
| }, 100); | |
| // Remove after delay | |
| setTimeout(() => { | |
| notification.classList.add('translate-x-full'); | |
| setTimeout(() => { | |
| document.body.removeChild(notification); | |
| }, 300); | |
| }, 3000); | |
| } | |
| } | |
| // Initialize the app when DOM is loaded | |
| document.addEventListener('DOMContentLoaded', () => { | |
| window.app = new ScreenStreamApp(); | |
| }); |