Front end improvements
This commit is contained in:
235
frontend/app.js
235
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 = `
|
||||
<div class="progress-item-name">${this.escapeHtml(file.name)}</div>
|
||||
<div class="progress-item-bar">
|
||||
<div class="progress-item-fill" style="width: 0%"></div>
|
||||
</div>
|
||||
`;
|
||||
progressList.appendChild(progressItem);
|
||||
|
||||
try {
|
||||
await this.uploadSingleFile(file, progressItem);
|
||||
progressItem.classList.add('success');
|
||||
successCount++;
|
||||
} catch (error) {
|
||||
console.error('Upload error:', error);
|
||||
progressItem.classList.add('error');
|
||||
errorCount++;
|
||||
}
|
||||
}
|
||||
|
||||
setTimeout(() => {
|
||||
progressContainer.style.display = 'none';
|
||||
uploadForm.reset();
|
||||
|
||||
if (errorCount === 0) {
|
||||
const msg = `Successfully uploaded ${successCount} video${successCount !== 1 ? 's' : ''}!`;
|
||||
alert(msg);
|
||||
document.getElementById('uploadModal').classList.remove('show');
|
||||
|
||||
// Reload videos
|
||||
this.scroller.loadVideos().then(() => {
|
||||
this.scroller.renderVideos();
|
||||
});
|
||||
} else {
|
||||
uploadError.textContent = `Upload complete: ${successCount} succeeded, ${errorCount} failed`;
|
||||
uploadError.style.display = 'block';
|
||||
}
|
||||
}, 1000);
|
||||
}
|
||||
|
||||
async uploadSingleFile(file, progressItem) {
|
||||
const progressFill = progressItem.querySelector('.progress-item-fill');
|
||||
const formData = new FormData();
|
||||
formData.append('file', file);
|
||||
|
||||
return new Promise((resolve, reject) => {
|
||||
const xhr = new XMLHttpRequest();
|
||||
|
||||
xhr.upload.addEventListener('progress', (e) => {
|
||||
if (e.lengthComputable) {
|
||||
const percentComplete = (e.loaded / e.total) * 100;
|
||||
progressFill.style.width = percentComplete + '%';
|
||||
}
|
||||
});
|
||||
|
||||
xhr.addEventListener('load', () => {
|
||||
if (xhr.status >= 200 && xhr.status < 300) {
|
||||
progressFill.style.width = '100%';
|
||||
resolve();
|
||||
} else {
|
||||
reject(new Error('Upload failed: ' + xhr.statusText));
|
||||
}
|
||||
});
|
||||
|
||||
xhr.addEventListener('error', () => {
|
||||
reject(new Error('Network error'));
|
||||
});
|
||||
|
||||
xhr.addEventListener('abort', () => {
|
||||
reject(new Error('Upload cancelled'));
|
||||
});
|
||||
|
||||
xhr.open('POST', `${this.auth.apiUrl}/api/upload`);
|
||||
xhr.setRequestHeader('Authorization', this.auth.getAuthHeader());
|
||||
xhr.send(formData);
|
||||
});
|
||||
}
|
||||
|
||||
escapeHtml(text) {
|
||||
const div = document.createElement('div');
|
||||
div.textContent = text;
|
||||
return div.innerHTML;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user