<style> .drop-zone { height: 420px !important; } .file-line-name label { text-overflow: ellipsis; width: 165px; overflow: hidden; text-wrap-mode: nowrap; margin-left: .25rem; } @media screen and (max-width: 1024px) { .file-line-name label { width: 100%; }} @media screen and (max-width: 900px) { .file-line-name label { width: 410px; }} @media screen and (max-width: 600px) { .file-line-name label { width: 255px; }} @media screen and (max-width: 480px) { .file-line-name label { width: 210px; }} @media screen and (max-width: 450px) { .file-line-name label { width: 185px; }} @media screen and (max-width: 425px) { .file-line-name label { width: 165px; }} @media screen and (max-width: 400px) { .file-line-name label { width: 145px; }} @media screen and (max-width: 350px) { .file-line-name label { width: 120px; }} .file-download { display: none !important; justify-content: flex-start !important; } @media screen and (max-width: 400px) { .file-line .file-line-name { margin-right: 100%; } .file-line .file-line-name label { width: auto; } .file-line .file-line-controls { display:none !important; } .file-line.focus .file-line-controls { display: inherit !important; } .file-download { display: inherit !important; } } .file-line-controls div.btn { min-width: 2rem; display: inline-block; white-space: nowrap; } div #refresh { display: inline; } div #mkdir { display: inline; } div #abort { display: none; } div #progress { display: none; } div.upload #refresh { display: none; } div.upload #mkdir { display: none; } div.upload #abort { display: inline; } div.upload #progress { display: inline; } </style> <div style="height: 470px; overflow-y: clip;"> <button class="btn m-1" id="uploadBtn">Upload</button> <button class="btn emergency-btn m-1" disabled id="abort">Abort</button> <button class="btn m-1" id="refresh">Refresh</button> <button class="btn m-1" id="mkdir">New Directory</button> <input type="file" class="d-none" id="uploadField" multiple="multiple"/> <progress id="progress"></progress> <div class="m-2" id="output"> </div> <div class="drop-zone files-list m-1" id="list"></div> <div id="cancel"></div> </div> <div class="files-list-footer"> <div id="stats" style="display: flex; align-items: center; flex-wrap: wrap; justify-content: space-between;"> <div class="flex-pack" id="total"></div> <div class="m-1">-</div> <div class="flex-pack m-2" id="used"></div> <div class="flex-pack m-1"> <div class="bar bar-sm" style="width: 4rem;"> <div class="bar-item" id="used-perc" role="progressbar" aria-valuenow="0" aria-valuemin="0" aria-valuemax="100"></div> </div> <span class="m-1" id="used-perc-txt"></span> </div> </div> </div> <script type="text/javascript"> //const get_args = Object.assign({}, ...window.location.search.substr(1).split('&').map((e)=>{const s=e.split('=', 2); return s==""?null:{[s[0]]:s[1]};})); //const SD_BASE_URL = 'http://' + (get_args['host'] || window.location.host); const SD_BASE_URL = 'http://192.168.1.27'; function sendMessage(msg){ window.parent.postMessage(msg, '*'); } function byteSizeToString(size) { if (-1 === size || isNaN(size)) { return ''; } let n = 0; for (; size >= 1024; ++n) { size /= 1024; } return `${size.toFixed(2)} ${['B', 'KB', 'MB', 'GB'][n]}` } addEventListener("DOMContentLoaded", () => { const fileInput = document.getElementById("uploadField"); const uploadBtn = document.getElementById('uploadBtn'); const progressBar = document.querySelector("progress"); const log = document.getElementById("output"); const list = document.getElementById("list"); const abortButton = document.getElementById("abort"); const cancelButton = document.getElementById("cancel"); const refreshButton = document.getElementById("refresh"); const mkdirButton = document.getElementById("mkdir"); let current_dir = [ ]; uploadBtn.addEventListener("click", () => { fileInput.value = ''; fileInput.click(); }); fileInput.addEventListener("change", () => { uploadFiles(Array.from(fileInput.files)); }); function uploadFiles(files) { const xhr = new XMLHttpRequest(); // Link abort button abortButton.addEventListener("click", () => { xhr.abort(); }, { once: true } ); const queued = (files.length > 1) ? ` + ${files.length-1} queued` : ''; // When the upload starts, we display the progress bar xhr.upload.addEventListener("loadstart", (event) => { progressBar.parentNode.classList.add("upload"); progressBar.value = 0; progressBar.max = event.total; log.textContent = "Uploading (0%)..." + queued; abortButton.disabled = false; }); // Each time a progress event is received, we update the bar xhr.upload.addEventListener("progress", (event) => { progressBar.value = event.loaded; progressBar.max = event.total; log.textContent = `Uploading (${( (event.loaded / event.total) * 100 ).toFixed(2)}%)...` + queued; }); // When the upload is finished, we hide the progress bar. xhr.upload.addEventListener("loadend", (event) => { progressBar.parentNode.classList.remove("upload"); abortButton.disabled = true; }); xhr.addEventListener("load", (event) => { const file = JSON.parse(event.target.responseText)['item']; if (file) { add_file_entry(file, true); } if (files.length > 1) { uploadFiles(files.slice(1)); } }); // When the upload has been aborted xhr.upload.addEventListener("aborted", (event) => { log.textContent = "Upload aborted."; }); // When the upload is completed xhr.upload.addEventListener("load", (event) => { //log.textContent = "Upload finished."; log.textContent = ''; }); // In case of an error, an abort, or a timeout, we hide the progress bar // Note that these events can be listened to on the xhr object too function errorAction(event) { progressBar.parentNode.classList.remove("upload"); log.textContent = `Upload failed: ${event.type}`; } xhr.upload.addEventListener("error", errorAction); xhr.upload.addEventListener("abort", errorAction); xhr.upload.addEventListener("timeout", errorAction); // Build the payload const fileData = new FormData(); fileData.append("file", files[0]); // Theoretically, event listeners could be set after the open() call // but browsers are buggy here xhr.open("POST", SD_BASE_URL + '/upload?path=' + current_dir.concat(files[0].name).join('/'), true); // Note that the event listener must be set before sending (as it is a preflighted request) xhr.send(fileData); } function is_hidden_controls() { return window.screen.width < 400; } let file_list = {}; function add_file_entry(file, first) { const file_line = document.createElement('div') file_line.setAttribute('class', 'file-line form-control'); // If an element exists with the same name, delete it from DOM if (file_list[file.sfn]) { file_list[file.sfn].remove(); } file_list[file.sfn] = file_line; if (first) { list.insertBefore(file_line, list.childNodes[(current_dir.length > 0) ? 1 : 0]); } else { list.appendChild(file_line); } const file_line_name = file_line.appendChild(document.createElement('div')); file_line_name.setAttribute('class', 'feather-icon-container file-line-name file-line-action'); if (file.type != 'dir') { file_line_name.innerHTML = '<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" data-darkreader-inline-stroke="" style="--darkreader-inline-stroke: currentColor;"><path d="M13 2H6a2 2 0 0 0-2 2v16a2 2 0 0 0 2 2h12a2 2 0 0 0 2-2V9z"></path><polyline points="13 2 13 9 20 9"></polyline></svg>'; } else { file_line_name.innerHTML = '<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M22 19a2 2 0 0 1-2 2H4a2 2 0 0 1-2-2V5a2 2 0 0 1 2-2h5l2 3h9a2 2 0 0 1 2 2z"></path></svg>'; } const label = file_line_name.appendChild(document.createElement('label')); label.setAttribute('title', file.name); label.innerText = file.name; const file_download_controls = file_line.appendChild(document.createElement('div')); file_download_controls.setAttribute('class', 'file-line-controls file-download'); const file_line_controls = file_line.appendChild(document.createElement('div')); file_line_controls.setAttribute('class', 'file-line-controls'); if (file.type != 'dir') { const size = file_line_controls.appendChild(document.createElement('label')); size.innerText = byteSizeToString(file.size); const button_download = file_download_controls.appendChild(document.createElement('button')); button_download.setAttribute('class', 'btn m-1 tooltip tooltip-right feather-icon-container'); button_download.setAttribute('data-tooltip', 'Download file'); const button_download_a = button_download.appendChild(document.createElement('a')); button_download_a.setAttribute('href', SD_BASE_URL + '/download?path=' + current_dir.concat(file.name).join('/') ); button_download_a.setAttribute('download', ''); button_download_a.setAttribute('hidden', ''); button_download.addEventListener('click', () => { button_download_a.click(); }); const button_download_icon_div = button_download.appendChild(document.createElement('div')); button_download_icon_div.setAttribute('style', 'pointer-events: none;'); button_download_icon_div.innerHTML = '<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M21 15v4a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2v-4"></path><polyline points="7 10 12 15 17 10"></polyline><line x1="12" y1="15" x2="12" y2="3"></line></svg>' if (file.name.toLowerCase().match('.gco[^.]*')) { const button_play = file_line_controls.appendChild(document.createElement('button')); button_play.setAttribute('class', 'btn m-1 tooltip tooltip-left feather-icon-container'); button_play.setAttribute('data-tooltip', 'Play file'); button_play.addEventListener('click', () => { if (!is_hidden_controls() || confirm('Print ' + file.name + '?')) { sendMessage({target: 'webui', type:'cmd', content: 'M23 /' + current_dir.concat(file.sfn).join('/') + '\nM24'}); } }); const button_play_icon_div = button_play.appendChild(document.createElement('div')); button_play_icon_div.setAttribute('style', 'pointer-events: none;'); button_play_icon_div.innerHTML = '<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" data-darkreader-inline-stroke="" style="--darkreader-inline-stroke: currentColor;"><polygon points="5 3 19 12 5 21 5 3"></polygon></svg>'; } else { const button_play = file_line_controls.appendChild(document.createElement('div')); button_play.setAttribute('style', 'width: 2rem;'); } label.addEventListener('click', () => { if (!is_hidden_controls()) { button_download_a.click(); } else { Array.from(document.getElementsByClassName('file-line')).forEach((e)=>{ e.classList.remove('focus'); }); file_line.classList.add('focus'); } }); } else { label.addEventListener('click', () => { current_dir.push(file.name); updateList(); }); } const button_delete = file_line_controls.appendChild(document.createElement('button')); button_delete.setAttribute('class', 'btn m-1 tooltip tooltip-left feather-icon-container'); button_delete.setAttribute('data-tooltip', 'Delete file'); button_delete.addEventListener('click', () => { if (is_hidden_controls() && !confirm('Delete ' + file.name + '?')) { return; } const xhr2 = new XMLHttpRequest(); xhr2.timeout = 10000; // 10 seconds xhr2.addEventListener("load", (event) => { if (event.target.status == 200) { list.removeChild(file_line); } }); xhr2.addEventListener("error", (event) => { updateList(); }); if (file.type != 'dir') { xhr2.open("GET", SD_BASE_URL + '/remove?path=' + current_dir.concat(file.name).join('/'), true); } else { xhr2.open("GET", SD_BASE_URL + '/rmdir?path=' + current_dir.concat(file.name).join('/'), true); } xhr2.send(); }); const button_delete_icon_div = button_delete.appendChild(document.createElement('div')); button_delete_icon_div.setAttribute('style', 'pointer-events: none;'); button_delete_icon_div.innerHTML = '<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" data-darkreader-inline-stroke="" style="--darkreader-inline-stroke: currentColor;"><polyline points="3 6 5 6 21 6"></polyline><path d="M19 6v14a2 2 0 0 1-2 2H7a2 2 0 0 1-2-2V6m3 0V4a2 2 0 0 1 2-2h4a2 2 0 0 1 2 2v2"></path><line x1="10" y1="11" x2="10" y2="17"></line><line x1="14" y1="11" x2="14" y2="17"></line></svg>'; } let xhr_list = new XMLHttpRequest(); function updateList() { if (xhr_list != null) { xhr_list.abort(); } xhr_list = new XMLHttpRequest(); xhr_list.timeout = 30000; // 30 seconds file_list = {}; list.setAttribute('hidden', ''); list.innerHTML = ''; cancel.setAttribute('hidden', ''); cancel.innerHTML = ''; log.textContent = ''; const center = cancel.appendChild(document.createElement('center')); center.setAttribute('disabled', 'true'); const center_div = center.appendChild(document.createElement('div')); center_div.setAttribute('class', 'loading m-2'); center_div.setAttribute('disabled', 'true'); const cancel_button = center.appendChild(document.createElement('button')); cancel_button.setAttribute('class', 'btn do-not-disable tooltip feather-icon-container'); cancel_button.setAttribute('data-tooltip', 'Cancel'); cancel_button.setAttribute('style', 'min-width: 2rem; display: inline-block; white-space: nowrap;'); cancel_button.addEventListener("click", (event) => { xhr_list.abort(); }); const cancel_div = cancel_button.appendChild(document.createElement('div')); cancel_div.setAttribute('style', 'white-space: nowrap; cursor: pointer; pointer-events: none; overflow: hidden !important; text-overflow: ellipsis !important;'); cancel_div.innerHTML = '<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" data-darkreader-inline-stroke="" style="--darkreader-inline-stroke: currentColor;"><circle cx="12" cy="12" r="10"></circle><line x1="15" y1="9" x2="9" y2="15"></line><line x1="9" y1="9" x2="15" y2="15"></line></svg>' const cancel_label = cancel_div.appendChild(document.createElement('label')); cancel_label.innerText = 'Cancel'; cancel.removeAttribute('hidden'); xhr_list.addEventListener("load", (event) => { if (current_dir.length > 0) { const up_line = list.appendChild(document.createElement('div')); up_line.setAttribute('class', 'file-line file-line-name'); const up_line_action = up_line.appendChild(document.createElement('div')); up_line_action.setAttribute('class', 'form-control file-line-name file-line-action'); up_line_action.setAttribute('style', 'height: 2rem !important;'); up_line_action.innerHTML = '<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><polyline points="10 9 15 4 20 9"></polyline><path d="M4 20h7a4 4 0 0 0 4-4V4"></path></svg>' const up_label = up_line_action.appendChild(document.createElement('label')); up_label.setAttribute('class', 'p-2'); up_label.innerText = '...'; up_label.addEventListener('click', () => { current_dir.pop(); updateList(); }); } JSON.parse(event.target.responseText).toSorted((a, b) => { if (a.type == b.type) { return b.last - a.last; } else if (a.type == "dir") { return -1; } else { return 1; } }).forEach((file) => { add_file_entry(file); }); list.removeAttribute('hidden'); xhr_list = null; }); xhr_list.addEventListener("loadend", (event) => { cancel.setAttribute('hidden', true); }); xhr_list.addEventListener("error", (event) => { log.textContent = "Error"; }); xhr_list.addEventListener("timeout", (event) => { log.textContent = "Timeout"; }); xhr_list.open("GET", SD_BASE_URL + '/list?path=' + current_dir.join('/'), true); xhr_list.send(); } function updateUsage() { const stats = document.getElementById("stats"); const usedDiv = document.getElementById("used"); const totalDiv = document.getElementById("total"); const percProgress = document.getElementById("used-perc"); const percSpan = document.getElementById("used-perc-txt"); stats.classList.add('d-none'); const xhr = new XMLHttpRequest(); xhr.timeout = 10000; // 10 seconds xhr.addEventListener("load", (event) => { const sysinfo = JSON.parse(event.target.responseText); if (sysinfo.info && sysinfo.info.filesystem && sysinfo.info.filesystem.usedbytes > 0 && sysinfo.info.filesystem.totalbytes > 0) { usedDiv.innerText = 'Used: ' + byteSizeToString(sysinfo.info.filesystem.usedbytes); totalDiv.innerText = 'Total: ' + byteSizeToString(sysinfo.info.filesystem.totalbytes); const perc = (sysinfo.info.filesystem.usedbytes / sysinfo.info.filesystem.totalbytes * 100).toFixed(0); percProgress.setAttribute('aria-valuenow', perc); percProgress.setAttribute('style', 'width: ' + perc + '%;'); percSpan.innerText = perc + '%'; stats.classList.remove('d-none'); } }); xhr.open("GET", SD_BASE_URL + '/sysinfo', true); xhr.send(); } refreshButton.addEventListener("click", () => { updateUsage(); updateList(); } ); mkdirButton.addEventListener('click', () => { const name = (prompt('Directory name:') || '').replaceAll('/',''); if (name.length > 0) { const xhr = new XMLHttpRequest(); xhr.timeout = 10000; // 10 seconds xhr.addEventListener("load", (event) => { const obj = JSON.parse(event.target.responseText); const file = obj['item']; if (file) { add_file_entry(file, true); } else { updateList(); log.textContent(obj['error']); } }); xhr.open("GET", SD_BASE_URL + '/mkdir?path=' + current_dir.concat(name).join('/'), true); xhr.send(); } }); updateUsage(); updateList(); }); </script>