This commit is contained in:
2026-09-07 11:39:39 +02:00
parent 59722ee255
commit d0e022484e
10 changed files with 440 additions and 30 deletions

View File

@@ -6,6 +6,7 @@ import { ThreeMFLoader } from "three/addons/loaders/3MFLoader.js";
const view = document.getElementById("view");
const crumbs = document.getElementById("crumbs");
const homeLink = document.getElementById("home-link");
const logoutBtn = document.getElementById("logout-btn");
const stlLoader = new STLLoader();
const mfLoader = new ThreeMFLoader();
@@ -37,6 +38,10 @@ function navigateToProject(name) {
async function api(path, options = {}) {
const res = await fetch(path, options);
if (res.status === 401) {
window.location.href = "/login.html";
throw new Error("Nicht angemeldet");
}
if (!res.ok) {
let detail = res.statusText;
try {
@@ -75,9 +80,6 @@ async function renderDashboard() {
<h1 class="view-title">Projekte</h1>
<div class="view-subtitle">Lege ein Projekt an, um STL- und 3MF-Dateien zu verwalten.</div>
</div>
<div class="header-actions">
<a class="btn" href="/api/download-all" download>Alle Projekte herunterladen</a>
</div>
`;
view.appendChild(header);
@@ -225,33 +227,102 @@ async function renderProject(name) {
await loadFileGrid(name, gridContainer);
}
function createProgressBar(afterNode, label) {
const wrap = document.createElement("div");
wrap.className = "upload-progress";
wrap.innerHTML = `
<div class="upload-progress-row">
<span class="upload-progress-name"></span>
<div class="upload-progress-track"><div class="upload-progress-fill"></div></div>
<span class="upload-progress-pct">0%</span>
</div>
`;
wrap.querySelector(".upload-progress-name").textContent = label;
afterNode.insertAdjacentElement("afterend", wrap);
const fill = wrap.querySelector(".upload-progress-fill");
const pct = wrap.querySelector(".upload-progress-pct");
return {
update(percent) {
fill.style.width = `${percent}%`;
pct.textContent = `${percent}%`;
},
done() {
fill.style.width = "100%";
fill.classList.add("is-done");
pct.textContent = "100%";
},
remove() {
wrap.remove();
},
};
}
async function uploadFiles(projectName, fileList) {
const formData = new FormData();
let hasValid = false;
const names = [];
for (const file of fileList) {
const ext = "." + file.name.split(".").pop().toLowerCase();
if (ext === ".stl" || ext === ".3mf") {
formData.append("files", file);
hasValid = true;
names.push(file.name);
}
}
if (!hasValid) {
if (!names.length) {
showToast("Nur .stl und .3mf Dateien werden unterstützt.", true);
return;
}
const dropzone = document.querySelector(".dropzone");
const label = names.length === 1 ? names[0] : `${names.length} Dateien`;
const progress = createProgressBar(dropzone, label);
try {
const result = await api(`/api/projects/${encodeURIComponent(projectName)}/upload`, {
method: "POST",
body: formData,
});
const result = await uploadWithProgress(projectName, formData, (percent) => progress.update(percent));
progress.done();
showToast(`${result.saved.length} Datei(en) hochgeladen`);
setTimeout(() => progress.remove(), 600);
const container = document.getElementById("file-grid-container");
if (container) await loadFileGrid(projectName, container);
} catch (err) {
progress.remove();
showToast("Upload fehlgeschlagen: " + err.message, true);
}
}
function uploadWithProgress(projectName, formData, onProgress) {
return new Promise((resolve, reject) => {
const xhr = new XMLHttpRequest();
xhr.open("POST", `/api/projects/${encodeURIComponent(projectName)}/upload`);
xhr.upload.addEventListener("progress", (e) => {
if (e.lengthComputable) {
onProgress(Math.round((e.loaded / e.total) * 100));
}
});
xhr.addEventListener("load", () => {
if (xhr.status >= 200 && xhr.status < 300) {
try {
resolve(JSON.parse(xhr.responseText));
} catch (err) {
reject(new Error("Ungültige Antwort vom Server"));
}
} else {
let message = xhr.statusText;
try {
message = JSON.parse(xhr.responseText).detail || message;
} catch (_) {}
reject(new Error(message));
}
});
xhr.addEventListener("error", () => reject(new Error("Netzwerkfehler")));
xhr.send(formData);
});
}
async function loadFileGrid(projectName, container) {
disposeViewers();
container.innerHTML = "";
@@ -436,4 +507,19 @@ function disposeViewers() {
// ---------- Init ----------
renderDashboard();
logoutBtn.addEventListener("click", async () => {
try {
await fetch("/api/logout", { method: "POST" });
} finally {
window.location.href = "/login.html";
}
});
(async function init() {
try {
await api("/api/me");
renderDashboard();
} catch (err) {
// api() already redirects to /login.html on 401
}
})();