VNC-8 Adding upload and download buttons
This commit is contained in:
parent
887662b0ad
commit
cba6e69af6
|
|
@ -0,0 +1,22 @@
|
|||
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
|
||||
<svg
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
width="25"
|
||||
height="25"
|
||||
viewBox="0 0 25 25"
|
||||
version="1.1">
|
||||
<g transform="translate(0.5,0.5)">
|
||||
<!-- Cloud/folder base -->
|
||||
<path
|
||||
style="opacity:1;fill:none;fill-opacity:1;stroke:#ffffff;stroke-width:2;stroke-linecap:round;stroke-linejoin:round;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1"
|
||||
d="M 21,15 21,19 C 21,20.104569 20.104569,21 19,21 L 5,21 C 3.8954305,21 3,20.104569 3,19 L 3,15" />
|
||||
<!-- Arrow shaft -->
|
||||
<path
|
||||
style="opacity:1;fill:none;fill-opacity:1;stroke:#ffffff;stroke-width:2;stroke-linecap:round;stroke-linejoin:round;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1"
|
||||
d="M 12,3 L 12,15" />
|
||||
<!-- Arrow head (pointing down) -->
|
||||
<path
|
||||
style="opacity:1;fill:none;fill-opacity:1;stroke:#ffffff;stroke-width:2;stroke-linecap:round;stroke-linejoin:round;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1"
|
||||
d="M 7,10 L 12,15 L 17,10" />
|
||||
</g>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 1.0 KiB |
313
app/ui.js
313
app/ui.js
|
|
@ -147,6 +147,8 @@ const UI = {
|
|||
UI.addMachineHandlers();
|
||||
UI.addConnectionControlHandlers();
|
||||
UI.addClipboardHandlers();
|
||||
UI.addUploadHandlers();
|
||||
UI.addDownloadHandlers();
|
||||
UI.addSettingsHandlers();
|
||||
UI.addDisplaysHandler();
|
||||
// UI.addMultiMonitorAddHandler();
|
||||
|
|
@ -532,6 +534,20 @@ const UI = {
|
|||
.addEventListener('click', UI.clipboardClear);
|
||||
},
|
||||
|
||||
addUploadHandlers() {
|
||||
UI.addClickHandle('noVNC_upload_button', UI.toggleUploadPanel);
|
||||
|
||||
document.getElementById("noVNC_file_input")
|
||||
.addEventListener('change', UI.handleFileSelect);
|
||||
},
|
||||
|
||||
addDownloadHandlers() {
|
||||
UI.addClickHandle('noVNC_download_button', UI.toggleDownloadPanel);
|
||||
|
||||
document.getElementById("noVNC_refresh_downloads_button")
|
||||
.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) {
|
||||
|
|
@ -1216,6 +1232,8 @@ const UI = {
|
|||
UI.closeSettingsPanel();
|
||||
UI.closePowerPanel();
|
||||
UI.closeClipboardPanel();
|
||||
UI.closeUploadPanel();
|
||||
UI.closeDownloadPanel();
|
||||
UI.closeExtraKeys();
|
||||
},
|
||||
|
||||
|
|
@ -1368,6 +1386,301 @@ 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();
|
||||
|
||||
document.getElementById('noVNC_download_panel')
|
||||
.classList.add("noVNC_open");
|
||||
document.getElementById('noVNC_download_button')
|
||||
.classList.add("noVNC_selected");
|
||||
|
||||
// Refresh file list when opening
|
||||
UI.refreshDownloadsList();
|
||||
},
|
||||
|
||||
closeDownloadPanel() {
|
||||
document.getElementById('noVNC_download_panel')
|
||||
.classList.remove("noVNC_open");
|
||||
document.getElementById('noVNC_download_button')
|
||||
.classList.remove("noVNC_selected");
|
||||
},
|
||||
|
||||
toggleDownloadPanel(e) {
|
||||
if (!UI.isControlPanelItemClick(e)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
if (document.getElementById('noVNC_download_panel')
|
||||
.classList.contains("noVNC_open")) {
|
||||
UI.closeDownloadPanel();
|
||||
} else {
|
||||
UI.openDownloadPanel();
|
||||
}
|
||||
},
|
||||
|
||||
refreshDownloadsList() {
|
||||
const downloadsList = document.getElementById('noVNC_download_files_list');
|
||||
|
||||
// Show loading message
|
||||
downloadsList.innerHTML = '<div style="padding: 10px; text-align: center;">Loading files...</div>';
|
||||
|
||||
// Fetch file list from API
|
||||
const xhr = new XMLHttpRequest();
|
||||
xhr.open('GET', '/api/downloads', true);
|
||||
|
||||
xhr.addEventListener('load', () => {
|
||||
if (xhr.status === 200) {
|
||||
try {
|
||||
const response = JSON.parse(xhr.responseText);
|
||||
|
||||
if (response.success && response.downloads && response.downloads.length > 0) {
|
||||
// Clear loading message
|
||||
downloadsList.innerHTML = '';
|
||||
|
||||
// Display each file
|
||||
response.downloads.forEach(file => {
|
||||
const fileItem = document.createElement('div');
|
||||
fileItem.className = 'noVNC_download_item';
|
||||
fileItem.style.marginBottom = '8px';
|
||||
fileItem.style.padding = '8px';
|
||||
fileItem.style.border = '1px solid #ccc';
|
||||
fileItem.style.borderRadius = '4px';
|
||||
fileItem.style.display = 'flex';
|
||||
fileItem.style.justifyContent = 'space-between';
|
||||
fileItem.style.alignItems = 'center';
|
||||
|
||||
const fileInfo = document.createElement('div');
|
||||
fileInfo.style.flex = '1';
|
||||
fileInfo.style.minWidth = '0';
|
||||
|
||||
const fileName = document.createElement('div');
|
||||
fileName.textContent = file.filename;
|
||||
fileName.style.fontSize = '13px';
|
||||
fileName.style.fontWeight = 'bold';
|
||||
fileName.style.color = '#ffffff';
|
||||
fileName.style.wordBreak = 'break-all';
|
||||
|
||||
const fileDetails = document.createElement('div');
|
||||
fileDetails.style.fontSize = '11px';
|
||||
fileDetails.style.color = '#cccccc';
|
||||
fileDetails.style.marginTop = '3px';
|
||||
|
||||
if (file.is_dir) {
|
||||
fileDetails.textContent = 'Directory';
|
||||
} else {
|
||||
fileDetails.textContent = UI.formatFileSize(file.size);
|
||||
}
|
||||
|
||||
fileInfo.appendChild(fileName);
|
||||
fileInfo.appendChild(fileDetails);
|
||||
|
||||
// Only add download button for files, not directories
|
||||
if (!file.is_dir) {
|
||||
const downloadBtn = document.createElement('button');
|
||||
downloadBtn.textContent = 'Download';
|
||||
downloadBtn.style.marginLeft = '10px';
|
||||
downloadBtn.style.padding = '5px 10px';
|
||||
downloadBtn.style.fontSize = '12px';
|
||||
downloadBtn.style.cursor = 'pointer';
|
||||
downloadBtn.addEventListener('click', () => {
|
||||
UI.downloadFile(file.filename);
|
||||
});
|
||||
fileItem.appendChild(fileInfo);
|
||||
fileItem.appendChild(downloadBtn);
|
||||
} else {
|
||||
fileItem.appendChild(fileInfo);
|
||||
}
|
||||
|
||||
downloadsList.appendChild(fileItem);
|
||||
});
|
||||
} else {
|
||||
downloadsList.innerHTML = '<div style="padding: 10px; text-align: center; color: #999;">No files available</div>';
|
||||
}
|
||||
} catch (e) {
|
||||
downloadsList.innerHTML = '<div style="padding: 10px; text-align: center; color: #f44336;">Error parsing response</div>';
|
||||
}
|
||||
} else {
|
||||
downloadsList.innerHTML = '<div style="padding: 10px; text-align: center; color: #f44336;">Failed to load files</div>';
|
||||
}
|
||||
});
|
||||
|
||||
xhr.addEventListener('error', () => {
|
||||
downloadsList.innerHTML = '<div style="padding: 10px; text-align: center; color: #f44336;">Network error</div>';
|
||||
});
|
||||
|
||||
xhr.send();
|
||||
},
|
||||
|
||||
downloadFile(filename) {
|
||||
// Create a temporary anchor element and trigger download
|
||||
const a = document.createElement('a');
|
||||
a.href = '/downloads/' + encodeURIComponent(filename);
|
||||
a.download = filename;
|
||||
document.body.appendChild(a);
|
||||
a.click();
|
||||
document.body.removeChild(a);
|
||||
},
|
||||
|
||||
clipboardReceive(e) {
|
||||
if (UI.rfb.clipboardDown) {
|
||||
var curvalue = document.getElementById('noVNC_clipboard_text').value;
|
||||
|
|
|
|||
38
index.html
38
index.html
|
|
@ -174,6 +174,44 @@
|
|||
</div>
|
||||
</div>
|
||||
|
||||
<!-- File Upload -->
|
||||
<div class="noVNC_button_div noVNC_hide_on_disconnect" >
|
||||
<input type="image" alt="Upload Files" src="app/images/upload.svg"
|
||||
id="noVNC_upload_button" class="noVNC_button"
|
||||
title="Upload Files">
|
||||
Upload Files
|
||||
<div class="noVNC_vcenter">
|
||||
<div id="noVNC_upload_panel" class="noVNC_panel">
|
||||
<div class="noVNC_heading">
|
||||
<img alt="" src="app/images/upload.svg"> Upload Files
|
||||
</div>
|
||||
<input type="file" id="noVNC_file_input" multiple style="margin: 10px 0;">
|
||||
<div id="noVNC_upload_files_list" style="max-height: 300px; overflow-y: auto;">
|
||||
<!-- Upload progress items will be added here dynamically -->
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- File Download -->
|
||||
<div class="noVNC_button_div noVNC_hide_on_disconnect" >
|
||||
<input type="image" alt="Download Files" src="app/images/download.svg"
|
||||
id="noVNC_download_button" class="noVNC_button"
|
||||
title="Download Files">
|
||||
Download Files
|
||||
<div class="noVNC_vcenter">
|
||||
<div id="noVNC_download_panel" class="noVNC_panel">
|
||||
<div class="noVNC_heading">
|
||||
<img alt="" src="app/images/download.svg"> Download Files
|
||||
</div>
|
||||
<button id="noVNC_refresh_downloads_button" style="margin: 10px 0; padding: 5px 10px;">Refresh File List</button>
|
||||
<div id="noVNC_download_files_list" style="max-height: 400px; overflow-y: auto; margin-top: 10px;">
|
||||
<!-- Download file list will be added here dynamically -->
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Toggle fullscreen -->
|
||||
<div class="noVNC_button_div noVNC_hidden" >
|
||||
<input type="image" alt="Fullscreen" src="app/images/fullscreen.svg"
|
||||
|
|
|
|||
Loading…
Reference in New Issue