diff --git a/frontend/app.js b/frontend/app.js index a8698c9..e23552e 100644 --- a/frontend/app.js +++ b/frontend/app.js @@ -346,14 +346,30 @@ class EncryptedVideoScroller { this.preloadNextVideo(); } - preloadNextVideo() { + async preloadNextVideo() { const nextIndex = this.currentIndex + 1; - if (nextIndex < this.videos.length && nextIndex > this.currentIndex + 1) { - // Preload video metadata + if (nextIndex < this.videos.length) { const nextVideo = this.videos[nextIndex]; - if (nextVideo) { - const img = new Image(); - img.src = `${this.apiUrl}${nextVideo.url}`; + // Only preload if not already cached + if (nextVideo && !this.decryptedBlobs[nextVideo.id]) { + try { + // Fetch encrypted video in background + const response = await fetch( + `${this.auth.apiUrl}/api/decrypt-video/${nextVideo.id}`, + { + headers: { 'Authorization': this.auth.getAuthHeader() } + } + ); + if (response.ok) { + // Cache the decrypted blob for smooth playback + const blob = await response.blob(); + const blobUrl = URL.createObjectURL(blob); + this.decryptedBlobs[nextVideo.id] = blobUrl; + console.debug(`Preloaded video ${nextVideo.id}`); + } + } catch (error) { + console.debug('Preload of next video failed (non-critical):', error); + } } } } @@ -482,6 +498,9 @@ class TickyApp { const closeUploadBtn = document.getElementById('closeUploadBtn'); const uploadForm = document.getElementById('uploadForm'); const uploadError = document.getElementById('uploadError'); + const fileInput = document.getElementById('fileInput'); + const dropZone = document.getElementById('dropZone'); + const dropOverlay = document.getElementById('dropOverlay'); logoutBtn.addEventListener('click', () => { this.auth.logout(); @@ -502,43 +521,179 @@ class TickyApp { } }); - uploadForm.addEventListener('submit', async (e) => { - e.preventDefault(); - const fileInput = document.getElementById('fileInput'); - const file = fileInput.files[0]; + // Drop zone click to select files + dropZone.addEventListener('click', () => { + fileInput.click(); + }); - uploadError.textContent = ''; - uploadError.style.display = 'none'; - - try { - const formData = new FormData(); - formData.append('file', file); - - const response = await fetch(`${this.auth.apiUrl}/api/upload`, { - method: 'POST', - headers: { - 'Authorization': this.auth.getAuthHeader() - }, - body: formData - }); - - if (!response.ok) { - throw new Error('Upload failed'); - } - - uploadForm.reset(); - uploadModal.classList.remove('show'); - - // Reload videos - await this.scroller.loadVideos(); - this.scroller.renderVideos(); - - alert('Video uploaded and encrypted successfully!'); - } catch (error) { - uploadError.textContent = 'Upload failed: ' + error.message; - uploadError.style.display = 'block'; + // File input change + fileInput.addEventListener('change', (e) => { + if (e.target.files.length > 0) { + this.handleFilesSelected(e.target.files); } }); + + // Drag and drop on drop zone + dropZone.addEventListener('dragover', (e) => { + e.preventDefault(); + e.stopPropagation(); + dropZone.classList.add('drag-over'); + }); + + dropZone.addEventListener('dragleave', (e) => { + e.preventDefault(); + e.stopPropagation(); + dropZone.classList.remove('drag-over'); + }); + + dropZone.addEventListener('drop', (e) => { + e.preventDefault(); + e.stopPropagation(); + dropZone.classList.remove('drag-over'); + if (e.dataTransfer.files.length > 0) { + this.handleFilesSelected(e.dataTransfer.files); + } + }); + + // Drag and drop on entire window + document.addEventListener('dragover', (e) => { + if (uploadModal.classList.contains('show')) { + e.preventDefault(); + dropOverlay.classList.add('active'); + } + }); + + document.addEventListener('dragleave', (e) => { + if (e.clientX === 0 && e.clientY === 0) { + dropOverlay.classList.remove('active'); + } + }); + + document.addEventListener('drop', (e) => { + e.preventDefault(); + dropOverlay.classList.remove('active'); + }); + + uploadForm.addEventListener('submit', async (e) => { + e.preventDefault(); + if (fileInput.files.length === 0) { + uploadError.textContent = 'Please select at least one video'; + uploadError.style.display = 'block'; + return; + } + await this.uploadMultipleFiles(fileInput.files, uploadForm, uploadError); + }); + } + + async handleFilesSelected(files) { + const uploadForm = document.getElementById('uploadForm'); + const uploadError = document.getElementById('uploadError'); + uploadError.textContent = ''; + uploadError.style.display = 'none'; + await this.uploadMultipleFiles(files, uploadForm, uploadError); + } + + async uploadMultipleFiles(files, uploadForm, uploadError) { + const progressContainer = document.getElementById('uploadProgress'); + const progressList = document.getElementById('progressList'); + + if (files.length === 0) { + uploadError.textContent = 'Please select at least one video'; + uploadError.style.display = 'block'; + return; + } + + progressList.innerHTML = ''; + progressContainer.style.display = 'block'; + + let successCount = 0; + let errorCount = 0; + + for (let i = 0; i < files.length; i++) { + const file = files[i]; + const progressItem = document.createElement('div'); + progressItem.className = 'progress-item'; + progressItem.innerHTML = ` +