diff --git a/app/images/download.svg b/app/images/download.svg new file mode 100644 index 000000000..53ce315ef --- /dev/null +++ b/app/images/download.svg @@ -0,0 +1,22 @@ + + diff --git a/app/images/upload.svg b/app/images/upload.svg new file mode 100644 index 000000000..1d22b95f8 --- /dev/null +++ b/app/images/upload.svg @@ -0,0 +1,22 @@ + + diff --git a/app/ui.js b/app/ui.js index 320a7353d..06fd687ac 100644 --- a/app/ui.js +++ b/app/ui.js @@ -87,6 +87,8 @@ const UI = { monitorStartX: 0, monitorStartY: 0, + currentDownloadPath: '', // Track current folder path for downloads + supportsBroadcastChannel: (typeof BroadcastChannel !== "undefined"), codecDetector: null, forcedCodecs: [], @@ -166,6 +168,8 @@ const UI = { UI.addMachineHandlers(); UI.addConnectionControlHandlers(); UI.addClipboardHandlers(); + UI.addUploadHandlers(); + UI.addDownloadHandlers(); UI.addSettingsHandlers(); UI.addDisplaysHandler(); // UI.addMultiMonitorAddHandler(); @@ -586,6 +590,28 @@ const UI = { .addEventListener('click', UI.clipboardClear); }, + addUploadHandlers() { + if (document.getElementById('noVNC_upload_button')) { + UI.addClickHandle('noVNC_upload_button', UI.toggleUploadPanel); + } + + const fileInput = document.getElementById("noVNC_file_input"); + if (fileInput) { + fileInput.addEventListener('change', UI.handleFileSelect); + } + }, + + addDownloadHandlers() { + if (document.getElementById('noVNC_download_button')) { + UI.addClickHandle('noVNC_download_button', UI.toggleDownloadPanel); + } + + const refreshButton = document.getElementById("noVNC_refresh_downloads_button"); + if (refreshButton) { + refreshButton.addEventListener('click', UI.refreshDownloadsList); + } + }, + // Add a call to save settings when the element changes, // unless the optional parameter changeFunc is used instead. addSettingChangeHandler(name, changeFunc) { @@ -1492,6 +1518,8 @@ const UI = { UI.closeSettingsPanel(); UI.closePowerPanel(); UI.closeClipboardPanel(); + UI.closeUploadPanel(); + UI.closeDownloadPanel(); UI.closeExtraKeys(); }, @@ -1644,6 +1672,416 @@ const UI = { } }, + openUploadPanel() { + UI.closeAllPanels(); + UI.openControlbar(); + + document.getElementById('noVNC_upload_panel') + .classList.add("noVNC_open"); + document.getElementById('noVNC_upload_button') + .classList.add("noVNC_selected"); + }, + + closeUploadPanel() { + document.getElementById('noVNC_upload_panel') + .classList.remove("noVNC_open"); + document.getElementById('noVNC_upload_button') + .classList.remove("noVNC_selected"); + }, + + toggleUploadPanel(e) { + if (!UI.isControlPanelItemClick(e)) { + return false; + } + + if (document.getElementById('noVNC_upload_panel') + .classList.contains("noVNC_open")) { + UI.closeUploadPanel(); + } else { + UI.openUploadPanel(); + } + }, + + handleFileSelect(e) { + const files = e.target.files; + if (!files || files.length === 0) return; + + for (let i = 0; i < files.length; i++) { + UI.uploadFile(files[i]); + } + + // Clear the input so the same file can be selected again + e.target.value = ''; + }, + + formatFileSize(bytes) { + if (bytes === 0) return '0 Bytes'; + const k = 1024; + const sizes = ['Bytes', 'KB', 'MB', 'GB']; + const i = Math.floor(Math.log(bytes) / Math.log(k)); + return Math.round((bytes / Math.pow(k, i)) * 100) / 100 + ' ' + sizes[i]; + }, + + uploadFile(file) { + const uploadsList = document.getElementById('noVNC_upload_files_list'); + + // Create progress item + const progressItem = document.createElement('div'); + progressItem.className = 'noVNC_upload_item'; + progressItem.style.marginBottom = '10px'; + progressItem.style.padding = '8px'; + progressItem.style.border = '1px solid #ccc'; + progressItem.style.borderRadius = '4px'; + + const fileName = document.createElement('div'); + fileName.textContent = file.name + ' (' + UI.formatFileSize(file.size) + ')'; + fileName.style.fontSize = '13px'; + fileName.style.fontWeight = 'bold'; + fileName.style.marginBottom = '8px'; + fileName.style.wordBreak = 'break-all'; + fileName.style.color = '#ffffff'; + + const progressBarContainer = document.createElement('div'); + progressBarContainer.style.width = '100%'; + progressBarContainer.style.height = '20px'; + progressBarContainer.style.backgroundColor = '#f0f0f0'; + progressBarContainer.style.borderRadius = '10px'; + progressBarContainer.style.overflow = 'hidden'; + + const progressBar = document.createElement('div'); + progressBar.style.height = '100%'; + progressBar.style.width = '0%'; + progressBar.style.backgroundColor = '#4CAF50'; + progressBar.style.transition = 'width 0.3s'; + + const progressText = document.createElement('div'); + progressText.textContent = '0%'; + progressText.style.fontSize = '11px'; + progressText.style.marginTop = '3px'; + progressText.style.textAlign = 'center'; + + progressBarContainer.appendChild(progressBar); + progressItem.appendChild(fileName); + progressItem.appendChild(progressBarContainer); + progressItem.appendChild(progressText); + uploadsList.appendChild(progressItem); + + // Prepare FormData + const formData = new FormData(); + formData.append('file', file); + + // Create XMLHttpRequest + const xhr = new XMLHttpRequest(); + + // Progress handler + xhr.upload.addEventListener('progress', (e) => { + if (e.lengthComputable) { + const percentComplete = (e.loaded / e.total) * 100; + progressBar.style.width = percentComplete + '%'; + progressText.textContent = Math.round(percentComplete) + '%'; + } + }); + + // Completion handler + xhr.addEventListener('load', () => { + if (xhr.status === 200) { + progressBar.style.backgroundColor = '#4CAF50'; + progressText.textContent = 'Complete!'; + + // Remove after 5 seconds + setTimeout(() => { + progressItem.style.transition = 'opacity 0.5s'; + progressItem.style.opacity = '0'; + setTimeout(() => { + progressItem.remove(); + }, 500); + }, 5000); + } else { + progressBar.style.backgroundColor = '#f44336'; + progressText.textContent = 'Failed!'; + + // Remove after 5 seconds + setTimeout(() => { + progressItem.style.transition = 'opacity 0.5s'; + progressItem.style.opacity = '0'; + setTimeout(() => { + progressItem.remove(); + }, 500); + }, 5000); + } + }); + + // Error handler + xhr.addEventListener('error', () => { + progressBar.style.backgroundColor = '#f44336'; + progressText.textContent = 'Error!'; + + // Remove after 5 seconds + setTimeout(() => { + progressItem.style.transition = 'opacity 0.5s'; + progressItem.style.opacity = '0'; + setTimeout(() => { + progressItem.remove(); + }, 500); + }, 5000); + }); + + // Send request + xhr.open('POST', '/upload', true); + xhr.send(formData); + }, + + openDownloadPanel() { + UI.closeAllPanels(); + UI.openControlbar(); + + const panel = document.getElementById('noVNC_download_panel'); + const button = document.getElementById('noVNC_download_button'); + + if (panel) { + panel.classList.add("noVNC_open"); + } + if (button) { + button.classList.add("noVNC_selected"); + } + + // Reset to root folder when opening + UI.currentDownloadPath = ''; + + // Refresh file list when opening + UI.refreshDownloadsList(); + }, + + closeDownloadPanel() { + const panel = document.getElementById('noVNC_download_panel'); + const button = document.getElementById('noVNC_download_button'); + + if (panel) { + panel.classList.remove("noVNC_open"); + } + if (button) { + button.classList.remove("noVNC_selected"); + } + }, + + toggleDownloadPanel(e) { + if (!UI.isControlPanelItemClick(e)) { + return false; + } + + const panel = document.getElementById('noVNC_download_panel'); + if (panel && panel.classList.contains("noVNC_open")) { + UI.closeDownloadPanel(); + } else { + UI.openDownloadPanel(); + } + }, + + refreshDownloadsList() { + const downloadsList = document.getElementById('noVNC_download_files_list'); + + if (!downloadsList) { + console.log('Download files list element not found'); + return; + } + + // Show loading message + downloadsList.innerHTML = '