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 {
}
});
// Drop zone click to select files
dropZone.addEventListener('click', () => {
fileInput.click();
});
// 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();
const fileInput = document.getElementById('fileInput');
const file = fileInput.files[0];
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);
const response = await fetch(`${this.auth.apiUrl}/api/upload`, {
method: 'POST',
headers: {
'Authorization': this.auth.getAuthHeader()
},
body: formData
});
return new Promise((resolve, reject) => {
const xhr = new XMLHttpRequest();
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';
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;
}
}

View File

@@ -48,19 +48,27 @@
<div id="uploadModal" class="modal">
<div class="modal-content">
<div class="modal-header">
<h3>Upload Encrypted Video</h3>
<h3>Upload Encrypted Videos</h3>
<button class="close-btn" id="closeUploadBtn">&times;</button>
</div>
<form id="uploadForm">
<input type="file" id="fileInput" accept="video/*" required>
<div class="file-drop-zone" id="dropZone">
<p>Drag and drop videos here or click to select</p>
<input type="file" id="fileInput" accept="video/*" multiple style="display: none;">
</div>
<button type="submit">Encrypt & Upload</button>
</form>
<div id="uploadError" class="error-message"></div>
<div id="uploadProgress" class="progress-bar" style="display: none;">
<div class="progress-fill"></div>
<div id="uploadProgress" style="display: none;">
<div id="progressList"></div>
</div>
</div>
</div>
<!-- Drop Zone Overlay -->
<div id="dropOverlay" class="drop-overlay">
<div class="drop-indicator">Drop files to upload</div>
</div>
</div>
<script src="app.js"></script>

View File

@@ -585,6 +585,110 @@ video::-webkit-media-controls {
transition: width 0.2s;
}
.file-drop-zone {
border: 2px dashed rgba(0, 245, 255, 0.5);
border-radius: 12px;
padding: 40px 20px;
text-align: center;
cursor: pointer;
transition: all 0.3s;
background: rgba(0, 245, 255, 0.05);
}
.file-drop-zone:hover {
border-color: rgba(0, 245, 255, 0.8);
background: rgba(0, 245, 255, 0.1);
}
.file-drop-zone.drag-over {
border-color: #00f5ff;
background: rgba(0, 245, 255, 0.2);
transform: scale(1.02);
}
.file-drop-zone p {
color: rgba(255, 255, 255, 0.7);
font-size: 14px;
margin: 0;
}
.drop-overlay {
position: fixed;
top: 0;
left: 0;
width: 100%;
height: 100%;
background: rgba(0, 0, 0, 0.8);
display: none;
align-items: center;
justify-content: center;
z-index: 9999;
pointer-events: none;
}
.drop-overlay.active {
display: flex;
pointer-events: auto;
}
.drop-indicator {
color: white;
font-size: 32px;
font-weight: bold;
text-shadow: 0 0 20px rgba(0, 245, 255, 0.5);
animation: pulse 1s infinite;
}
@keyframes pulse {
0%, 100% { opacity: 1; }
50% { opacity: 0.7; }
}
#progressList {
display: flex;
flex-direction: column;
gap: 12px;
}
.progress-item {
padding: 10px;
background: rgba(255, 255, 255, 0.05);
border-radius: 6px;
border-left: 3px solid #00f5ff;
}
.progress-item-name {
font-size: 12px;
color: rgba(255, 255, 255, 0.8);
margin-bottom: 6px;
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
}
.progress-item-bar {
width: 100%;
height: 4px;
background: rgba(255, 255, 255, 0.1);
border-radius: 2px;
overflow: hidden;
}
.progress-item-fill {
height: 100%;
background: linear-gradient(90deg, #ff006e, #00f5ff);
width: 0%;
transition: width 0.2s ease;
}
.progress-item.success {
border-left-color: #00ff00;
}
.progress-item.error {
border-left-color: #ff0000;
}
#loginError,
#uploadError {
color: #ff6b6b;