mirror of
https://github.com/hrydgard/ppsspp.git
synced 2026-07-11 09:35:09 +02:00
410 lines
11 KiB
HTML
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>
|