Front end improvements

This commit is contained in:
2026-02-01 05:57:52 -05:00
parent a7aedd3b64
commit 2193c37a3e
3 changed files with 311 additions and 44 deletions

View File

@@ -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;
}
}