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;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -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">×</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>
|
||||
|
||||
@@ -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;
|
||||
|
||||
Reference in New Issue
Block a user