Some checks are pending
CI Pipeline / Run Tests (push) Waiting to run
CI Pipeline / Lint Code (push) Waiting to run
CI Pipeline / Security Scan (push) Waiting to run
CI Pipeline / Build Docker Images (push) Blocked by required conditions
CI Pipeline / E2E Tests (push) Blocked by required conditions
692 lines
25 KiB
JavaScript
692 lines
25 KiB
JavaScript
// Upload functionality and UI management
|
|
class GatewayUI {
|
|
constructor() {
|
|
this.currentUpload = null;
|
|
this.recentUploads = JSON.parse(localStorage.getItem('recentUploads') || '[]');
|
|
this.serviceStatus = {};
|
|
|
|
this.initializeElements();
|
|
this.attachEventListeners();
|
|
this.initializeTheme();
|
|
this.loadRecentUploads();
|
|
this.checkServiceStatus();
|
|
this.loadServerFiles();
|
|
|
|
// Update service status every 30 seconds
|
|
setInterval(() => this.checkServiceStatus(), 30000);
|
|
}
|
|
|
|
initializeElements() {
|
|
// Upload elements
|
|
this.uploadArea = document.getElementById('upload-area');
|
|
this.fileInput = document.getElementById('file-input');
|
|
this.uploadProgress = document.getElementById('upload-progress');
|
|
this.progressFill = document.getElementById('progress-fill');
|
|
this.progressPercent = document.getElementById('progress-percent');
|
|
this.progressSpeed = document.getElementById('progress-speed');
|
|
this.progressEta = document.getElementById('progress-eta');
|
|
this.uploadFilename = document.getElementById('upload-filename');
|
|
|
|
// Options
|
|
this.announceDht = document.getElementById('announce-dht');
|
|
this.storeBlossom = document.getElementById('store-blossom');
|
|
|
|
// Lists
|
|
this.uploadsList = document.getElementById('uploads-list');
|
|
|
|
// Toast container
|
|
this.toastContainer = document.getElementById('toast-container');
|
|
}
|
|
|
|
attachEventListeners() {
|
|
// File upload - only add if not already attached
|
|
if (!this.uploadArea.hasAttribute('data-events-attached')) {
|
|
this.uploadArea.addEventListener('click', (e) => {
|
|
// Prevent double clicks
|
|
if (e.detail === 1) {
|
|
this.fileInput.click();
|
|
}
|
|
});
|
|
this.uploadArea.setAttribute('data-events-attached', 'true');
|
|
}
|
|
|
|
this.fileInput.addEventListener('change', (e) => this.handleFileSelect(e.target.files));
|
|
|
|
// Drag and drop
|
|
this.uploadArea.addEventListener('dragover', (e) => this.handleDragOver(e));
|
|
this.uploadArea.addEventListener('dragleave', (e) => this.handleDragLeave(e));
|
|
this.uploadArea.addEventListener('drop', (e) => this.handleDrop(e));
|
|
|
|
// Prevent default drag behaviors on document
|
|
document.addEventListener('dragover', (e) => e.preventDefault());
|
|
document.addEventListener('drop', (e) => e.preventDefault());
|
|
}
|
|
|
|
initializeTheme() {
|
|
const savedTheme = localStorage.getItem('theme') || 'light';
|
|
document.documentElement.setAttribute('data-theme', savedTheme);
|
|
}
|
|
|
|
handleDragOver(e) {
|
|
e.preventDefault();
|
|
e.stopPropagation();
|
|
this.uploadArea.classList.add('drag-over');
|
|
}
|
|
|
|
handleDragLeave(e) {
|
|
e.preventDefault();
|
|
e.stopPropagation();
|
|
this.uploadArea.classList.remove('drag-over');
|
|
}
|
|
|
|
handleDrop(e) {
|
|
e.preventDefault();
|
|
e.stopPropagation();
|
|
this.uploadArea.classList.remove('drag-over');
|
|
|
|
const files = Array.from(e.dataTransfer.files);
|
|
this.handleFileSelect(files);
|
|
}
|
|
|
|
handleFileSelect(files) {
|
|
if (files.length === 0) return;
|
|
|
|
// For now, handle one file at a time
|
|
const file = files[0];
|
|
|
|
// Validate file
|
|
const validation = this.validateFile(file);
|
|
if (!validation.valid) {
|
|
this.showToast(validation.message, 'error');
|
|
this.fileInput.value = ''; // Clear the input
|
|
return;
|
|
}
|
|
|
|
this.uploadFile(file);
|
|
}
|
|
|
|
validateFile(file) {
|
|
// Check file existence
|
|
if (!file) {
|
|
return { valid: false, message: 'No file selected' };
|
|
}
|
|
|
|
// Check file size (10GB default limit - server will enforce actual limit)
|
|
const maxSize = 10 * 1024 * 1024 * 1024; // 10GB
|
|
if (file.size > maxSize) {
|
|
return {
|
|
valid: false,
|
|
message: `File too large. Maximum size is ${this.formatBytes(maxSize)} (selected: ${this.formatBytes(file.size)})`
|
|
};
|
|
}
|
|
|
|
if (file.size === 0) {
|
|
return { valid: false, message: 'Cannot upload empty file' };
|
|
}
|
|
|
|
// Check filename
|
|
if (!file.name || file.name.trim() === '') {
|
|
return { valid: false, message: 'File must have a valid name' };
|
|
}
|
|
|
|
if (file.name.length > 255) {
|
|
return { valid: false, message: 'Filename too long (max 255 characters)' };
|
|
}
|
|
|
|
// Check for dangerous characters in filename
|
|
const dangerousChars = ['..', '/', '\\', ':', '*', '?', '"', '<', '>', '|'];
|
|
for (const char of dangerousChars) {
|
|
if (file.name.includes(char)) {
|
|
return {
|
|
valid: false,
|
|
message: `Filename cannot contain '${char}' character`
|
|
};
|
|
}
|
|
}
|
|
|
|
// Check file type (basic validation)
|
|
const allowedTypes = [
|
|
// Video
|
|
'video/mp4', 'video/avi', 'video/mkv', 'video/mov', 'video/webm',
|
|
// Audio
|
|
'audio/mp3', 'audio/wav', 'audio/flac', 'audio/m4a', 'audio/ogg',
|
|
// Images
|
|
'image/jpeg', 'image/png', 'image/gif', 'image/webp', 'image/bmp',
|
|
// Documents
|
|
'application/pdf', 'text/plain', 'application/zip', 'application/x-rar-compressed',
|
|
// Archives
|
|
'application/x-7z-compressed', 'application/x-tar', 'application/gzip'
|
|
];
|
|
|
|
// If type is provided and not in allowed list, show warning but allow
|
|
if (file.type && !allowedTypes.includes(file.type) && !file.type.startsWith('application/')) {
|
|
console.warn(`Unusual file type: ${file.type}`);
|
|
}
|
|
|
|
return { valid: true };
|
|
}
|
|
|
|
async uploadFile(file) {
|
|
if (this.currentUpload) {
|
|
this.showToast('Another upload is in progress', 'warning');
|
|
return;
|
|
}
|
|
|
|
// Create FormData
|
|
const formData = new FormData();
|
|
formData.append('file', file);
|
|
|
|
// Add options
|
|
if (this.announceDht.checked) {
|
|
formData.append('announce_dht', 'true');
|
|
}
|
|
if (this.storeBlossom.checked) {
|
|
formData.append('store_blossom', 'true');
|
|
}
|
|
|
|
// Show progress
|
|
this.showUploadProgress(file.name);
|
|
|
|
try {
|
|
this.currentUpload = {
|
|
file: file,
|
|
startTime: Date.now(),
|
|
abort: new AbortController()
|
|
};
|
|
|
|
const headers = {};
|
|
if (window.nostrAuth && window.nostrAuth.sessionToken) {
|
|
headers['Authorization'] = `Bearer ${window.nostrAuth.sessionToken}`;
|
|
console.log('Upload with auth token:', window.nostrAuth.sessionToken.substring(0, 20) + '...');
|
|
} else {
|
|
console.log('Upload without auth - nostrAuth:', !!window.nostrAuth, 'sessionToken:', !!window.nostrAuth?.sessionToken);
|
|
}
|
|
|
|
const response = await fetch('/api/upload', {
|
|
method: 'POST',
|
|
headers: headers,
|
|
body: formData,
|
|
signal: this.currentUpload.abort.signal
|
|
});
|
|
|
|
if (!response.ok) {
|
|
throw new Error(`Upload failed: ${response.status} ${response.statusText}`);
|
|
}
|
|
|
|
const result = await response.json();
|
|
this.handleUploadSuccess(file, result);
|
|
|
|
} catch (error) {
|
|
if (error.name === 'AbortError') {
|
|
this.showToast('Upload cancelled', 'warning');
|
|
} else {
|
|
console.error('Upload error:', error);
|
|
|
|
// Provide user-friendly error messages
|
|
let message = 'Upload failed';
|
|
if (error.message.includes('413') || error.message.includes('too large')) {
|
|
message = 'File too large. Please choose a smaller file.';
|
|
} else if (error.message.includes('415') || error.message.includes('unsupported')) {
|
|
message = 'File type not supported. Please try a different file.';
|
|
} else if (error.message.includes('429') || error.message.includes('rate limit')) {
|
|
message = 'Upload rate limit exceeded. Please wait and try again.';
|
|
} else if (error.message.includes('401') || error.message.includes('unauthorized')) {
|
|
message = 'Please login to upload files.';
|
|
} else if (error.message.includes('403') || error.message.includes('forbidden')) {
|
|
message = 'Upload not allowed. Check your permissions.';
|
|
} else if (error.message.includes('507') || error.message.includes('storage')) {
|
|
message = 'Server storage full. Please try again later.';
|
|
} else if (error.message.includes('NetworkError') || error.message.includes('fetch')) {
|
|
message = 'Network error. Please check your connection and try again.';
|
|
} else if (error.message.includes('timeout')) {
|
|
message = 'Upload timed out. Please try again with a smaller file.';
|
|
}
|
|
|
|
this.showToast(message, 'error');
|
|
}
|
|
} finally {
|
|
this.hideUploadProgress();
|
|
this.currentUpload = null;
|
|
}
|
|
}
|
|
|
|
showUploadProgress(filename) {
|
|
this.uploadFilename.textContent = filename;
|
|
this.uploadProgress.classList.remove('hidden');
|
|
this.uploadArea.style.display = 'none';
|
|
|
|
// Start progress simulation (since we can't track real progress easily)
|
|
this.simulateProgress();
|
|
}
|
|
|
|
simulateProgress() {
|
|
let progress = 0;
|
|
const startTime = Date.now();
|
|
|
|
const updateProgress = () => {
|
|
if (!this.currentUpload) return;
|
|
|
|
// Simulate realistic progress curve
|
|
progress += (100 - progress) * 0.05;
|
|
|
|
const elapsed = (Date.now() - startTime) / 1000;
|
|
const speed = (this.currentUpload.file.size * (progress / 100)) / elapsed;
|
|
const remaining = (this.currentUpload.file.size - (this.currentUpload.file.size * (progress / 100))) / speed;
|
|
|
|
this.progressFill.style.width = `${progress}%`;
|
|
this.progressPercent.textContent = `${Math.round(progress)}%`;
|
|
this.progressSpeed.textContent = this.formatBytes(speed) + '/s';
|
|
this.progressEta.textContent = this.formatTime(remaining);
|
|
|
|
if (progress < 95 && this.currentUpload) {
|
|
setTimeout(updateProgress, 100);
|
|
}
|
|
};
|
|
|
|
updateProgress();
|
|
}
|
|
|
|
hideUploadProgress() {
|
|
this.uploadProgress.classList.add('hidden');
|
|
this.uploadArea.style.display = 'block';
|
|
this.progressFill.style.width = '0%';
|
|
this.fileInput.value = '';
|
|
}
|
|
|
|
handleUploadSuccess(file, result) {
|
|
this.showToast('File uploaded successfully!', 'success');
|
|
|
|
// Add to recent uploads
|
|
const uploadRecord = {
|
|
id: result.file_hash || result.hash,
|
|
name: file.name,
|
|
size: file.size,
|
|
hash: result.file_hash || result.hash,
|
|
torrentHash: result.torrent_hash,
|
|
magnetLink: result.magnet_link,
|
|
timestamp: Date.now(),
|
|
type: file.type,
|
|
isVideo: file.type.startsWith('video/')
|
|
};
|
|
|
|
this.recentUploads.unshift(uploadRecord);
|
|
this.recentUploads = this.recentUploads.slice(0, 10); // Keep only last 10
|
|
localStorage.setItem('recentUploads', JSON.stringify(this.recentUploads));
|
|
|
|
this.loadServerFiles();
|
|
}
|
|
|
|
async loadServerFiles() {
|
|
// Show loading state
|
|
if (this.uploadsList) {
|
|
this.uploadsList.innerHTML = '<div class="loading-state"><div class="spinner"></div>Loading files...</div>';
|
|
}
|
|
|
|
try {
|
|
const response = await fetch('/api/files');
|
|
if (response.ok) {
|
|
const data = await response.json();
|
|
if (data.files && data.files.length > 0) {
|
|
// Merge server files with local uploads, avoiding duplicates
|
|
const allFiles = [...data.files];
|
|
|
|
// Add local uploads that might not be on server yet
|
|
this.recentUploads.forEach(localFile => {
|
|
if (!allFiles.find(f => f.file_hash === localFile.hash)) {
|
|
allFiles.unshift({
|
|
file_hash: localFile.hash,
|
|
name: localFile.name,
|
|
size: localFile.size,
|
|
is_video: localFile.isVideo,
|
|
torrent_hash: localFile.torrentHash,
|
|
magnet_link: localFile.magnetLink
|
|
});
|
|
}
|
|
});
|
|
|
|
this.displayFiles(allFiles);
|
|
return;
|
|
}
|
|
}
|
|
} catch (error) {
|
|
console.log('Could not load server files, showing local only:', error);
|
|
if (this.uploadsList) {
|
|
this.uploadsList.innerHTML = '<div class="error-state">Failed to load server files. Showing local uploads only.</div>';
|
|
setTimeout(() => this.loadRecentUploads(), 2000);
|
|
return;
|
|
}
|
|
}
|
|
|
|
// Fallback to local uploads only
|
|
this.loadRecentUploads();
|
|
}
|
|
|
|
loadRecentUploads() {
|
|
if (this.recentUploads.length === 0) {
|
|
this.uploadsList.innerHTML = '<p class="empty-state">No recent uploads</p>';
|
|
return;
|
|
}
|
|
|
|
const files = this.recentUploads.map(upload => ({
|
|
file_hash: upload.hash,
|
|
name: upload.name,
|
|
size: upload.size,
|
|
is_video: upload.isVideo,
|
|
torrent_hash: upload.torrentHash,
|
|
magnet_link: upload.magnetLink
|
|
}));
|
|
|
|
this.displayFiles(files);
|
|
}
|
|
|
|
displayFiles(files) {
|
|
if (files.length === 0) {
|
|
this.uploadsList.innerHTML = '<p class="empty-state">No files uploaded</p>';
|
|
return;
|
|
}
|
|
|
|
this.uploadsList.innerHTML = files.map(file => `
|
|
<div class="upload-item" data-hash="${file.file_hash}">
|
|
<div class="upload-item-header">
|
|
<div class="upload-item-title">${this.escapeHtml(file.name)}</div>
|
|
<div class="upload-item-meta">
|
|
${this.formatBytes(file.size)} • Hash: ${file.file_hash.substring(0, 8)}...
|
|
</div>
|
|
</div>
|
|
<div class="upload-item-actions">
|
|
<button class="action-btn" onclick="gatewayUI.downloadFile('${file.file_hash}')">
|
|
⬇️ Download
|
|
</button>
|
|
<button class="action-btn" onclick="gatewayUI.getTorrent('${file.file_hash}')">
|
|
🧲 Torrent
|
|
</button>
|
|
${file.is_video ? `
|
|
<button class="action-btn" onclick="gatewayUI.playVideo('${file.file_hash}', '${this.escapeHtml(file.name)}')">
|
|
▶️ Play
|
|
</button>
|
|
` : ''}
|
|
<button class="action-btn" onclick="gatewayUI.shareFile('${file.file_hash}', '${this.escapeHtml(file.name)}')">
|
|
📋 Share
|
|
</button>
|
|
<button class="action-btn" onclick="gatewayUI.deleteFile('${file.file_hash}', '${this.escapeHtml(file.name)}')" style="background-color: var(--danger); color: white;">
|
|
🗑️ Delete
|
|
</button>
|
|
</div>
|
|
</div>
|
|
`).join('');
|
|
}
|
|
|
|
async checkServiceStatus() {
|
|
try {
|
|
// Use the stats API which provides comprehensive service information
|
|
const response = await fetch('/api/stats');
|
|
|
|
if (response.ok) {
|
|
const data = await response.json();
|
|
|
|
// Update service status based on stats data
|
|
this.updateServiceStatus('gateway', data.gateway && data.gateway.status === 'healthy');
|
|
this.updateServiceStatus('blossom', data.blossom && data.blossom.status === 'healthy');
|
|
this.updateServiceStatus('dht', data.dht && data.dht.status === 'healthy');
|
|
} else {
|
|
// If stats API fails, assume all services are down
|
|
this.updateServiceStatus('gateway', false);
|
|
this.updateServiceStatus('blossom', false);
|
|
this.updateServiceStatus('dht', false);
|
|
}
|
|
} catch (error) {
|
|
console.error('Service status check failed:', error);
|
|
// If stats API fails, assume all services are down
|
|
this.updateServiceStatus('gateway', false);
|
|
this.updateServiceStatus('blossom', false);
|
|
this.updateServiceStatus('dht', false);
|
|
}
|
|
|
|
this.updateSystemInfo();
|
|
}
|
|
|
|
updateServiceStatus(serviceName, isOnline) {
|
|
this.serviceStatus[serviceName] = isOnline;
|
|
|
|
const statusElement = document.getElementById(`${serviceName}-status`);
|
|
if (statusElement) {
|
|
statusElement.textContent = isOnline ? '🟢' : '🔴';
|
|
statusElement.className = `status-indicator ${isOnline ? 'online' : 'offline'}`;
|
|
}
|
|
}
|
|
|
|
updateSystemInfo() {
|
|
// Update system information display
|
|
const mode = Object.keys(this.serviceStatus).filter(s => this.serviceStatus[s]).length === 3
|
|
? 'unified' : 'partial';
|
|
|
|
const systemMode = document.getElementById('system-mode');
|
|
if (systemMode) systemMode.textContent = mode;
|
|
|
|
const totalStorage = document.getElementById('system-storage');
|
|
if (totalStorage) {
|
|
const totalSize = this.recentUploads.reduce((sum, upload) => sum + upload.size, 0);
|
|
totalStorage.textContent = this.formatBytes(totalSize);
|
|
}
|
|
|
|
const gatewayUploads = document.getElementById('gateway-uploads');
|
|
if (gatewayUploads) gatewayUploads.textContent = this.recentUploads.length;
|
|
}
|
|
|
|
cancelUpload() {
|
|
if (this.currentUpload) {
|
|
this.currentUpload.abort.abort();
|
|
}
|
|
}
|
|
|
|
downloadFile(hash) {
|
|
const url = `/api/download/${hash}`;
|
|
const a = document.createElement('a');
|
|
a.href = url;
|
|
a.download = '';
|
|
document.body.appendChild(a);
|
|
a.click();
|
|
document.body.removeChild(a);
|
|
}
|
|
|
|
getTorrent(hash) {
|
|
const url = `/api/torrent/${hash}`;
|
|
const a = document.createElement('a');
|
|
a.href = url;
|
|
a.download = `${hash}.torrent`;
|
|
document.body.appendChild(a);
|
|
a.click();
|
|
document.body.removeChild(a);
|
|
}
|
|
|
|
playVideo(hash, name) {
|
|
const url = `/player.html?hash=${hash}&name=${encodeURIComponent(name)}`;
|
|
window.open(url, '_blank');
|
|
}
|
|
|
|
shareFile(hash, name) {
|
|
const baseUrl = window.location.origin;
|
|
const shareText = `${name}\n\nDownload: ${baseUrl}/api/download/${hash}\nStream: ${baseUrl}/api/stream/${hash}/playlist.m3u8\nTorrent: ${baseUrl}/api/torrent/${hash}`;
|
|
|
|
if (navigator.clipboard && navigator.clipboard.writeText) {
|
|
navigator.clipboard.writeText(shareText).then(() => {
|
|
this.showToast('Share links copied to clipboard!', 'success');
|
|
});
|
|
} else {
|
|
// Fallback for older browsers
|
|
const textarea = document.createElement('textarea');
|
|
textarea.value = shareText;
|
|
document.body.appendChild(textarea);
|
|
textarea.select();
|
|
document.execCommand('copy');
|
|
document.body.removeChild(textarea);
|
|
this.showToast('Share links copied to clipboard!', 'success');
|
|
}
|
|
}
|
|
|
|
async deleteFile(hash, name) {
|
|
if (!confirm(`Are you sure you want to delete "${name}"?\n\nThis action cannot be undone.`)) {
|
|
return;
|
|
}
|
|
|
|
try {
|
|
const headers = {
|
|
'Accept': 'application/json'
|
|
};
|
|
if (window.nostrAuth && window.nostrAuth.sessionToken) {
|
|
headers['Authorization'] = `Bearer ${window.nostrAuth.sessionToken}`;
|
|
}
|
|
|
|
const response = await fetch(`/api/delete/${hash}`, {
|
|
method: 'DELETE',
|
|
headers: headers
|
|
});
|
|
|
|
if (response.ok) {
|
|
const result = await response.json();
|
|
this.showToast(`File "${name}" deleted successfully!`, 'success');
|
|
|
|
// Remove from local storage if it exists
|
|
this.recentUploads = this.recentUploads.filter(upload => upload.hash !== hash);
|
|
localStorage.setItem('recentUploads', JSON.stringify(this.recentUploads));
|
|
|
|
// Refresh the file list
|
|
this.loadServerFiles();
|
|
} else {
|
|
const error = await response.json();
|
|
this.showToast(`Failed to delete file: ${error.error?.message || 'Unknown error'}`, 'error');
|
|
}
|
|
} catch (error) {
|
|
console.error('Delete error:', error);
|
|
this.showToast(`Error deleting file: ${error.message}`, 'error');
|
|
}
|
|
}
|
|
|
|
showToast(message, type = 'info') {
|
|
const toast = document.createElement('div');
|
|
toast.className = `toast ${type}`;
|
|
toast.textContent = message;
|
|
|
|
this.toastContainer.appendChild(toast);
|
|
|
|
// Remove toast after 3 seconds
|
|
setTimeout(() => {
|
|
toast.remove();
|
|
}, 3000);
|
|
}
|
|
|
|
// Utility functions
|
|
formatBytes(bytes) {
|
|
if (bytes === 0) return '0 B';
|
|
const k = 1024;
|
|
const sizes = ['B', 'KB', 'MB', 'GB', 'TB'];
|
|
const i = Math.floor(Math.log(bytes) / Math.log(k));
|
|
return parseFloat((bytes / Math.pow(k, i)).toFixed(1)) + ' ' + sizes[i];
|
|
}
|
|
|
|
formatTime(seconds) {
|
|
if (!isFinite(seconds) || seconds < 0) return '--:--';
|
|
|
|
const hours = Math.floor(seconds / 3600);
|
|
const minutes = Math.floor((seconds % 3600) / 60);
|
|
const secs = Math.floor(seconds % 60);
|
|
|
|
if (hours > 0) {
|
|
return `${hours}:${minutes.toString().padStart(2, '0')}:${secs.toString().padStart(2, '0')}`;
|
|
}
|
|
return `${minutes}:${secs.toString().padStart(2, '0')}`;
|
|
}
|
|
|
|
formatDate(timestamp) {
|
|
const date = new Date(timestamp);
|
|
const now = new Date();
|
|
const diff = now - date;
|
|
|
|
if (diff < 60000) return 'just now';
|
|
if (diff < 3600000) return `${Math.floor(diff / 60000)}m ago`;
|
|
if (diff < 86400000) return `${Math.floor(diff / 3600000)}h ago`;
|
|
|
|
return date.toLocaleDateString();
|
|
}
|
|
|
|
escapeHtml(text) {
|
|
const div = document.createElement('div');
|
|
div.textContent = text;
|
|
return div.innerHTML;
|
|
}
|
|
}
|
|
|
|
// Global functions for navigation and theme
|
|
function showServices() {
|
|
hideAllSections();
|
|
document.getElementById('services-section').classList.add('active');
|
|
gatewayUI.checkServiceStatus();
|
|
}
|
|
|
|
function showAbout() {
|
|
hideAllSections();
|
|
document.getElementById('about-section').classList.add('active');
|
|
}
|
|
|
|
function hideAllSections() {
|
|
document.querySelectorAll('.section').forEach(section => {
|
|
section.classList.remove('active');
|
|
});
|
|
}
|
|
|
|
function showUpload() {
|
|
hideAllSections();
|
|
document.getElementById('upload-section').classList.add('active');
|
|
}
|
|
|
|
function toggleTheme() {
|
|
const currentTheme = document.documentElement.getAttribute('data-theme');
|
|
const newTheme = currentTheme === 'dark' ? 'light' : 'dark';
|
|
|
|
document.documentElement.setAttribute('data-theme', newTheme);
|
|
localStorage.setItem('theme', newTheme);
|
|
}
|
|
|
|
function refreshDHTStats() {
|
|
gatewayUI.showToast('DHT stats refreshed', 'success');
|
|
// In a real implementation, this would fetch DHT statistics
|
|
// from a dedicated endpoint
|
|
}
|
|
|
|
function cancelUpload() {
|
|
gatewayUI.cancelUpload();
|
|
}
|
|
|
|
function copyToClipboard(elementId) {
|
|
const element = document.getElementById(elementId);
|
|
element.select();
|
|
document.execCommand('copy');
|
|
gatewayUI.showToast('Copied to clipboard!', 'success');
|
|
}
|
|
|
|
// Initialize the UI when the page loads
|
|
let gatewayUI;
|
|
document.addEventListener('DOMContentLoaded', () => {
|
|
gatewayUI = new GatewayUI();
|
|
});
|
|
|
|
// Handle browser navigation
|
|
window.addEventListener('hashchange', () => {
|
|
const hash = window.location.hash.slice(1);
|
|
switch (hash) {
|
|
case 'services':
|
|
showServices();
|
|
break;
|
|
case 'about':
|
|
showAbout();
|
|
break;
|
|
case 'upload':
|
|
showUpload();
|
|
break;
|
|
case 'files':
|
|
showFiles();
|
|
break;
|
|
default:
|
|
showAbout(); // Default to About page instead of Upload
|
|
}
|
|
});
|