Files
ppsspp/assets/upload/index.html
2025-10-27 12:20:24 +01:00

410 lines
11 KiB
HTML

<!DOCTYPE html>
<!--upload-->
<!--TODO: Make localizable somehow.-->
<html lang="en">
<head>
<meta charset="UTF-8">
<title>File Uploader</title>
<style>
body {
font-family: system-ui, sans-serif;
background: #e7f1ff;
color: #333;
padding: 2em;
max-width: 600px;
margin: auto;
}
h1 {
text-align: center;
color: #444;
}
.section {
margin-bottom: 1em;
}
input[type="file"] {
display: block;
margin: 0.5em 0;
}
button {
display: inline-block;
padding: 0.6em 1.2em;
font-size: 1em;
border: none;
border-radius: 6px;
background-color: #4a90e2;
color: white;
cursor: pointer;
transition: background 0.2s;
margin-right: 0.5em;
}
#uploadBtn:enabled {
background-color: #3aa270;
}
button:disabled {
background-color: #ccc;
cursor: not-allowed;
}
button:hover:not(:disabled) {
background-color: #357ab8;
}
.btn {
display: inline-block;
padding: 0.6em 1.2em;
font-size: 1em;
border: none;
border-radius: 6px;
background-color: #4a90e2;
color: white;
cursor: pointer;
transition: background 0.2s;
margin-right: 0.5em;
}
.btn:hover {
background-color: #357ab8;
}
#fileList {
margin-top: 0.5em;
font-size: 0.95em;
color: #333;
}
progress {
width: 100%;
height: 20px;
margin-top: 0.5em;
display: none;
}
#status {
margin-top: 0.5em;
font-weight: 500;
}
#error {
color: #d33;
margin-top: 0.5em;
}
.btn input[type="file"] {
display: none; /* hide the native control */
}
#resetBtn {
display: none; /* initially hidden */
}
.drag-over {
border: 2px dashed #4a90e2;
background-color: #f0f8ff;
}
.drop-zone {
border: 2px dashed #ccc;
border-radius: 10px;
padding: 40px 20px;
text-align: center;
margin: 1em 0;
background-color: #fafafa;
transition: all 0.3s ease;
display: none; /* initially hidden, shown only on supported platforms */
}
.drop-zone-text {
color: #666;
font-size: 1.1em;
margin: 0.5em 0;
}
.fixed-width {
font-family: monospace;
background: #eee;
padding: 2px 4px;
border-radius: 4px;
}
</style>
</head>
<body>
<h1>Upload files to {{ device_name }}</h1>
<p>Destination folder: <span class="fixed-width">{{ upload_path }}</span></p>
<div class="section">
<label class="btn">
Choose individual files...
<input type="file" id="fileInput" multiple hidden>
</label>
<label class="btn">
Choose folder...
<input type="file" id="folderInput" webkitdirectory multiple>
</label>
<button id="resetBtn">
Reset
</button>
</div>
<div id="dropZone" class="drop-zone">
<div class="drop-zone-text">
📁 Drop files here or use the buttons above
</div>
</div>
<div id="fileList"></div>
<div id="uploadedSection" style="display: none;">
<h3>Uploaded Files:</h3>
<div id="uploadedList"></div>
</div>
<div class="section">
<button id="uploadBtn" disabled>Start upload!</button>
</div>
<progress id="progressBar" value="0" max="100"></progress>
<div id="status"></div>
<div id="error"></div>
<script>
(() => {
const fileInput = document.getElementById("fileInput");
const folderInput = document.getElementById("folderInput");
const uploadBtn = document.getElementById("uploadBtn");
const resetBtn = document.getElementById("resetBtn");
const fileListDiv = document.getElementById("fileList");
const uploadedSection = document.getElementById("uploadedSection");
const uploadedListDiv = document.getElementById("uploadedList");
const dropZone = document.getElementById("dropZone");
const progressBar = document.getElementById("progressBar");
const statusDiv = document.getElementById("status");
const errorDiv = document.getElementById("error");
let files = [];
let uploadedFiles = [];
let droppedFiles = [];
// Feature detection for drag and drop support
function isDragDropSupported() {
// Check for basic drag and drop API support
const div = document.createElement('div');
const hasBasicSupport = ('draggable' in div) && ('ondrop' in div);
// Check if it's a mobile device (where drag/drop from file system is limited)
const isMobile = /Android|webOS|iPhone|iPad|iPod|BlackBerry|IEMobile|Opera Mini/i.test(navigator.userAgent);
// Check if it's a touch-only device
const isTouchOnly = ('ontouchstart' in window) && !window.matchMedia('(pointer: fine)').matches;
return hasBasicSupport && !isMobile && !isTouchOnly;
}
// Show drop zone only if drag and drop is supported
if (isDragDropSupported()) {
dropZone.style.display = 'block';
}
function formatBytes(bytes, decimals = 2) {
if (bytes === 0) return '0 B';
const k = 1024;
const units = ['B', 'KB', 'MB', 'GB', 'TB', 'PB'];
const i = Math.floor(Math.log(bytes) / Math.log(k));
const size = parseFloat((bytes / Math.pow(k, i)).toFixed(decimals));
return `${size} ${units[i]}`;
}
function updateUploadedList() {
uploadedListDiv.innerHTML = "";
if (uploadedFiles.length > 0) {
uploadedSection.style.display = "block";
const list = document.createElement("ul");
uploadedFiles.forEach(f => {
const li = document.createElement("li");
const formattedBytes = formatBytes(f.size, 2);
const name = f.webkitRelativePath || f.name;
li.textContent = `${name} (${formattedBytes}) - ✓ Uploaded`;
li.style.color = "#2d7a2d";
list.appendChild(li);
});
uploadedListDiv.appendChild(list);
} else {
uploadedSection.style.display = "none";
}
}
function updateFileList() {
files = [...fileInput.files, ...folderInput.files, ...droppedFiles];
fileListDiv.innerHTML = "";
if (files.length > 0) {
uploadBtn.disabled = false;
resetBtn.style.display = "inline-block";
const list = document.createElement("ul");
files.forEach(f => {
const li = document.createElement("li");
const formattedBytes = formatBytes(f.size, 2);
const name = f.webkitRelativePath || f.name;
li.textContent = `${name} (${formattedBytes} bytes)`;
list.appendChild(li);
});
fileListDiv.appendChild(list);
} else {
uploadBtn.disabled = true;
resetBtn.style.display = "none";
}
}
fileInput.addEventListener("change", updateFileList);
folderInput.addEventListener("change", updateFileList);
// Drag and drop functionality
dropZone.addEventListener("dragover", (e) => {
e.preventDefault();
dropZone.classList.add("drag-over");
});
dropZone.addEventListener("dragleave", (e) => {
e.preventDefault();
dropZone.classList.remove("drag-over");
});
dropZone.addEventListener("drop", (e) => {
e.preventDefault();
dropZone.classList.remove("drag-over");
const dt = e.dataTransfer;
const allFiles = Array.from(dt.files);
// Filter out zero-byte entries (usually folders or invalid entries)
const validFiles = allFiles.filter(file => file.size > 0);
// Show a message if some files were filtered out
if (allFiles.length > validFiles.length) {
const filteredCount = allFiles.length - validFiles.length;
statusDiv.textContent = `Note: ${filteredCount} folder(s) or empty file(s) were skipped. Only files can be drag/dropped.`;
setTimeout(() => {
if (statusDiv.textContent.includes("folder(s) or empty file(s) were skipped")) {
statusDiv.textContent = "";
}
}, 8000);
}
// Add valid dropped files to the existing array
droppedFiles = [...droppedFiles, ...validFiles];
updateFileList();
});
// Also handle drag/drop on the entire document to prevent browser default behavior
document.addEventListener("dragover", (e) => {
e.preventDefault();
});
document.addEventListener("drop", (e) => {
e.preventDefault();
});
resetBtn.addEventListener("click", () => {
// Clear the file inputs
fileInput.value = "";
folderInput.value = "";
// Clear all arrays and update displays
files = [];
droppedFiles = [];
uploadedFiles = [];
updateFileList();
updateUploadedList();
});
uploadBtn.addEventListener("click", async () => {
if (files.length === 0) return;
uploadBtn.disabled = true;
errorDiv.textContent = "";
statusDiv.textContent = "";
progressBar.style.display = "block";
progressBar.value = 0;
const filesToUpload = [...files]; // Create a copy to iterate over
// Clear all file sources so files disappear from pending list as they upload
fileInput.value = "";
folderInput.value = "";
droppedFiles = [];
files = [];
for (let i = 0; i < filesToUpload.length; i++) {
const file = filesToUpload[i];
statusDiv.textContent = `Uploading ${file.webkitRelativePath || file.name} (${i + 1}/${filesToUpload.length})`;
try {
await uploadFile(file);
// Move successfully uploaded file to uploaded list
uploadedFiles.push(file);
updateUploadedList();
} catch (err) {
// If upload fails, add remaining files back to pending list
files = filesToUpload.slice(i);
updateFileList();
errorDiv.textContent = err.message;
progressBar.style.display = "none";
uploadBtn.disabled = false;
return;
}
}
// Update the pending files list (should be empty now)
updateFileList();
statusDiv.textContent = "All files uploaded successfully.";
progressBar.style.display = "none";
// Note: uploadBtn state is managed by updateFileList() - disabled when no files
});
function uploadFile(file) {
return new Promise((resolve, reject) => {
const xhr = new XMLHttpRequest();
xhr.open("POST", "/upload_file");
// Add custom metadata headers if needed:
xhr.setRequestHeader("X-Client-Version", "1.0");
xhr.setRequestHeader("X-Upload-Timestamp", new Date().toISOString());
xhr.setRequestHeader("X-File-Size", file.size.toString());
xhr.upload.onprogress = e => {
if (e.lengthComputable) {
progressBar.value = (e.loaded / e.total) * 100;
}
};
xhr.onload = () => {
if (xhr.status >= 200 && xhr.status < 300) {
resolve();
} else {
let msg;
switch (xhr.status) {
case 400: msg = "Malformed request (boundary or headers invalid)."; break;
case 500: msg = "Target is probably out of disk space."; break;
default: msg = `Upload failed with status ${xhr.status}.`; break;
}
reject(new Error(msg));
}
};
xhr.onerror = () => reject(new Error("Connection error during upload."));
// Try to preserve any directory structure, if supported.
const formData = new FormData();
formData.append("file", file, file.webkitRelativePath || file.name);
xhr.send(formData);
});
}
})();
</script>
</body>
</html>