Grotti 2.10.0: Orca-Senden, Slice-Ordner, ED-Strom, Hygiene.
Slice-Watch mit Ordnern/Filtern, Orca-Nachbearbeitung, Cockpit-Verlauf mit ED, Zeitzone im Browser. Tasten-Grenze und README-API angeglichen, toter Code entfernt, Requirements gepinnt.
This commit is contained in:
1 parent
efd0801cd1
commit
9e22cebb81
48 files changed
+2259
-422
No files matched your search
+267
-18
@@ -11,6 +11,7 @@
|
||||
</select>
|
||||
<label class="button ghost" for="upload-file"><svg class="ico"><use href="#i-upload"/></svg>Hochladen</label>
|
||||
<input id="upload-file" type="file" accept=".gcode,.3mf,.gcode.3mf" multiple hidden>
|
||||
<button type="button" class="ghost" id="folder-create" hidden><svg class="ico"><use href="#i-folder"/></svg>Ordner anlegen</button>
|
||||
<button type="button" class="ghost" id="rescan"><svg class="ico"><use href="#i-refresh"/></svg>Ordner neu lesen</button>
|
||||
</div>
|
||||
</div>
|
||||
@@ -22,7 +23,7 @@
|
||||
<span class="count" id="lib-count">0</span>
|
||||
</div>
|
||||
<div class="lib-tools">
|
||||
<input id="file-search" type="search" placeholder="Datei suchen…" autocomplete="off">
|
||||
<input id="file-search" type="search" placeholder="Name, Material, Projekt…" autocomplete="off">
|
||||
<select id="file-filter-family">
|
||||
<option value="">Alle Maschinen</option>
|
||||
<option value="kobra_3">Kobra 3</option>
|
||||
@@ -31,6 +32,20 @@
|
||||
<select id="file-filter-project">
|
||||
<option value="">Alle Projekte</option>
|
||||
</select>
|
||||
<select id="file-filter-material" title="Material">
|
||||
<option value="">Alle Materialien</option>
|
||||
</select>
|
||||
<select id="file-filter-folder" title="Ordner">
|
||||
<option value="">Alle Ordner</option>
|
||||
</select>
|
||||
<select id="file-sort" title="Sortierung">
|
||||
<option value="mtime">Neueste</option>
|
||||
<option value="name">Name A–Z</option>
|
||||
<option value="material">Material</option>
|
||||
<option value="folder">Ordner</option>
|
||||
<option value="time">Druckzeit</option>
|
||||
<option value="weight">Gewicht</option>
|
||||
</select>
|
||||
</div>
|
||||
<p id="file-note" class="tiny"></p>
|
||||
<div class="lib-list" id="lib-list">
|
||||
@@ -152,8 +167,11 @@
|
||||
const families = { kobra_s1: "Kobra S1", kobra_3: "Kobra 3" };
|
||||
const ABLAUF_KEY = "grotti-ablauf-v2";
|
||||
const SEND_KEY = "grotti-send-v1";
|
||||
const LIB_KEY = "grotti-lib-v1";
|
||||
const STEP_TITLES = { print: "Drucken", cool: "Abkühlen", sweep: "Schieben", check: "Tasten" };
|
||||
let files = [];
|
||||
let knownFolders = [];
|
||||
let hasWatchDir = false;
|
||||
let printers = [];
|
||||
let spools = [];
|
||||
let projects = [];
|
||||
@@ -470,17 +488,101 @@ function collectAceMaps() {
|
||||
|
||||
/* ---------- Bibliothek ---------- */
|
||||
|
||||
function projectNameOf(file) {
|
||||
const project = projects.find((p) => String(p.id) === String(file.project_id));
|
||||
return project ? project.name : "";
|
||||
}
|
||||
|
||||
function materialKey(file) {
|
||||
return materialText(file) || "";
|
||||
}
|
||||
|
||||
function folderKey(file) {
|
||||
const folder = file.folder;
|
||||
if (folder === "Upload") return "Upload";
|
||||
if (folder) return folder;
|
||||
return "";
|
||||
}
|
||||
|
||||
function folderLabel(folder) {
|
||||
if (folder === "Upload") return "Upload";
|
||||
if (folder) return folder;
|
||||
return "Wurzel";
|
||||
}
|
||||
|
||||
function saveLibMemory() {
|
||||
try {
|
||||
localStorage.setItem(LIB_KEY, JSON.stringify({
|
||||
search: document.getElementById("file-search").value || "",
|
||||
family: document.getElementById("file-filter-family").value || "",
|
||||
project: document.getElementById("file-filter-project").value || "",
|
||||
material: document.getElementById("file-filter-material").value || "",
|
||||
folder: document.getElementById("file-filter-folder").value || "",
|
||||
sort: document.getElementById("file-sort").value || "mtime",
|
||||
}));
|
||||
} catch (error) { /* ignore */ }
|
||||
}
|
||||
|
||||
function loadLibMemory() {
|
||||
try {
|
||||
const data = JSON.parse(localStorage.getItem(LIB_KEY) || "{}");
|
||||
const set = (id, value) => {
|
||||
const node = document.getElementById(id);
|
||||
if (!node || value == null) return;
|
||||
node.value = value;
|
||||
};
|
||||
set("file-search", data.search || "");
|
||||
set("file-filter-family", data.family || "");
|
||||
set("file-filter-project", data.project || "");
|
||||
set("file-filter-material", data.material || "");
|
||||
set("file-filter-folder", data.folder || "");
|
||||
set("file-sort", data.sort || "mtime");
|
||||
} catch (error) { /* ignore */ }
|
||||
}
|
||||
|
||||
function filteredFiles() {
|
||||
const q = (document.getElementById("file-search").value || "").trim().toLowerCase();
|
||||
const family = document.getElementById("file-filter-family").value;
|
||||
const project = document.getElementById("file-filter-project").value;
|
||||
return files.filter((file) => {
|
||||
const material = document.getElementById("file-filter-material").value;
|
||||
const folder = document.getElementById("file-filter-folder").value;
|
||||
const sort = document.getElementById("file-sort").value || "mtime";
|
||||
const shown = files.filter((file) => {
|
||||
if (family && file.printer_family !== family) return false;
|
||||
if (project === "none" && file.project_id) return false;
|
||||
if (project && project !== "none" && String(file.project_id) !== project) return false;
|
||||
if (material === "none" && materialKey(file)) return false;
|
||||
if (material && material !== "none") {
|
||||
const types = ((file.filaments) || []).map((f) => f.type).filter(Boolean);
|
||||
if (!types.includes(material)) return false;
|
||||
}
|
||||
if (folder === "root" && folderKey(file)) return false;
|
||||
if (folder && folder !== "root" && folderKey(file) !== folder) return false;
|
||||
if (!q) return true;
|
||||
return String(file.name || "").toLowerCase().includes(q);
|
||||
const hay = [
|
||||
file.name || "",
|
||||
materialKey(file),
|
||||
projectNameOf(file),
|
||||
folderKey(file),
|
||||
].join(" ").toLowerCase();
|
||||
return hay.includes(q);
|
||||
});
|
||||
const cmpStr = (a, b) => String(a || "").localeCompare(String(b || ""), "de", { sensitivity: "base" });
|
||||
shown.sort((a, b) => {
|
||||
if (sort === "name") return cmpStr(a.name, b.name) || (Number(b.mtime) || 0) - (Number(a.mtime) || 0);
|
||||
if (sort === "material") {
|
||||
return cmpStr(materialKey(a) || "\uffff", materialKey(b) || "\uffff")
|
||||
|| cmpStr(a.name, b.name);
|
||||
}
|
||||
if (sort === "folder") {
|
||||
return cmpStr(folderLabel(folderKey(a)), folderLabel(folderKey(b)))
|
||||
|| cmpStr(a.name, b.name);
|
||||
}
|
||||
if (sort === "time") return (Number(b.estimated_s) || 0) - (Number(a.estimated_s) || 0) || cmpStr(a.name, b.name);
|
||||
if (sort === "weight") return (Number(b.weight_g) || 0) - (Number(a.weight_g) || 0) || cmpStr(a.name, b.name);
|
||||
return (Number(b.mtime) || 0) - (Number(a.mtime) || 0) || cmpStr(a.name, b.name);
|
||||
});
|
||||
return shown;
|
||||
}
|
||||
|
||||
function fillProjectFilter() {
|
||||
@@ -496,6 +598,70 @@ function fillProjectFilter() {
|
||||
if ([...select.options].some((o) => o.value === current)) select.value = current;
|
||||
}
|
||||
|
||||
function fillMaterialFilter() {
|
||||
const select = document.getElementById("file-filter-material");
|
||||
const current = select.value;
|
||||
const types = [...new Set(files.flatMap((f) => ((f.filaments) || []).map((x) => x.type).filter(Boolean)))];
|
||||
types.sort((a, b) => a.localeCompare(b, "de", { sensitivity: "base" }));
|
||||
select.innerHTML = '<option value="">Alle Materialien</option><option value="none">Ohne Material</option>';
|
||||
types.forEach((type) => {
|
||||
const option = document.createElement("option");
|
||||
option.value = type;
|
||||
option.textContent = type;
|
||||
select.appendChild(option);
|
||||
});
|
||||
if ([...select.options].some((o) => o.value === current)) select.value = current;
|
||||
}
|
||||
|
||||
function allFolderNames() {
|
||||
const fromFiles = files.map((f) => folderKey(f)).filter((f) => f && f !== "Upload");
|
||||
return [...new Set([...knownFolders, ...fromFiles])]
|
||||
.sort((a, b) => folderLabel(a).localeCompare(folderLabel(b), "de", { sensitivity: "base" }));
|
||||
}
|
||||
|
||||
function fillFolderFilter() {
|
||||
const select = document.getElementById("file-filter-folder");
|
||||
const current = select.value;
|
||||
const folders = allFolderNames();
|
||||
select.innerHTML = '<option value="">Alle Ordner</option><option value="root">Wurzel</option>';
|
||||
folders.forEach((folder) => {
|
||||
const option = document.createElement("option");
|
||||
option.value = folder;
|
||||
option.textContent = folderLabel(folder);
|
||||
select.appendChild(option);
|
||||
});
|
||||
if ([...select.options].some((o) => o.value === current)) select.value = current;
|
||||
}
|
||||
|
||||
function moveSelectHtml(file) {
|
||||
if (!hasWatchDir || file.source !== "watch") return "";
|
||||
const current = folderKey(file);
|
||||
const opts = [
|
||||
`<option value="" selected disabled>Verschieben…</option>`,
|
||||
`<option value="__root__">Wurzel</option>`,
|
||||
...allFolderNames().map((folder) =>
|
||||
`<option value="${escapeHtml(folder)}"${folder === current ? " disabled" : ""}>${escapeHtml(folderLabel(folder))}</option>`
|
||||
),
|
||||
`<option value="__new__">Neuer Ordner…</option>`,
|
||||
];
|
||||
return `<select class="lib-move" data-move="${file.id}" title="In Ordner verschieben" onclick="event.stopPropagation()">${opts.join("")}</select>`;
|
||||
}
|
||||
|
||||
async function promptCreateFolder() {
|
||||
const name = window.prompt("Neuer Ordner unter dem Slice-Pfad (z. B. PETG oder PLA/tests):", "");
|
||||
if (name == null) return null;
|
||||
const trimmed = String(name).trim();
|
||||
if (!trimmed) {
|
||||
toast("Ordnername fehlt.", "bad");
|
||||
return null;
|
||||
}
|
||||
const result = await api("/dateien/ordner", { method: "POST", json: { folder: trimmed } });
|
||||
knownFolders = result.folders || [];
|
||||
fillFolderFilter();
|
||||
toast("Ordner „" + (result.folder || trimmed) + "“ angelegt.", "good");
|
||||
return result.folder || trimmed;
|
||||
}
|
||||
|
||||
function fillProjects() {
|
||||
const opts = projects.map((row) =>
|
||||
`<option value="${escapeHtml(row.id)}">${escapeHtml(row.name)}</option>`
|
||||
@@ -585,9 +751,21 @@ function thumbHtml(file) {
|
||||
: `<span class="ph">${icon("cube")}</span>`;
|
||||
}
|
||||
|
||||
function libItemHtml(file) {
|
||||
const project = projects.find((p) => String(p.id) === String(file.project_id));
|
||||
const del = file.source === "upload"
|
||||
? `<button type="button" class="icon-btn" data-delete="${file.id}" title="Hochgeladene Datei löschen">${icon("x")}</button>` : "";
|
||||
return `<div class="lib-item ${String(file.id) === String(sel.fileId) ? "on" : ""}" role="button" tabindex="0" data-file="${file.id}">
|
||||
${thumbHtml(file)}
|
||||
<span style="min-width:0"><span class="t" title="${escapeHtml(file.name)}">${escapeHtml(file.name)}</span><span class="m">${alwaysMeta(file, project)}</span></span>
|
||||
<span class="side">${moveSelectHtml(file)}${infoPopupHtml(file)}${del}</span>
|
||||
</div>`;
|
||||
}
|
||||
|
||||
function renderLibrary() {
|
||||
const list = document.getElementById("lib-list");
|
||||
const shown = filteredFiles();
|
||||
const sort = document.getElementById("file-sort").value || "mtime";
|
||||
document.getElementById("lib-count").textContent = shown.length === files.length ? String(files.length) : shown.length + "/" + files.length;
|
||||
if (!files.length) {
|
||||
list.innerHTML = '<div class="picked-empty">' + icon("file") + '<span>Noch keine Datei.</span><span class="tiny">Hochladen oder Slice-Ordner in den Einstellungen setzen.</span></div>';
|
||||
@@ -597,16 +775,19 @@ function renderLibrary() {
|
||||
list.innerHTML = '<div class="picked-empty">' + icon("search") + '<span>Keine Datei passt.</span></div>';
|
||||
return;
|
||||
}
|
||||
list.innerHTML = shown.map((file) => {
|
||||
const project = projects.find((p) => String(p.id) === String(file.project_id));
|
||||
const del = file.source === "upload"
|
||||
? `<button type="button" class="icon-btn" data-delete="${file.id}" title="Hochgeladene Datei löschen">${icon("x")}</button>` : "";
|
||||
return `<div class="lib-item ${String(file.id) === String(sel.fileId) ? "on" : ""}" role="button" tabindex="0" data-file="${file.id}">
|
||||
${thumbHtml(file)}
|
||||
<span style="min-width:0"><span class="t" title="${escapeHtml(file.name)}">${escapeHtml(file.name)}</span><span class="m">${alwaysMeta(file, project)}</span></span>
|
||||
<span class="side">${infoPopupHtml(file)}${del}</span>
|
||||
</div>`;
|
||||
}).join("");
|
||||
const parts = [];
|
||||
let lastGroup = null;
|
||||
shown.forEach((file) => {
|
||||
let group = null;
|
||||
if (sort === "material") group = materialKey(file) || "Ohne Material";
|
||||
else if (sort === "folder") group = folderLabel(folderKey(file));
|
||||
if (group != null && group !== lastGroup) {
|
||||
parts.push(`<div class="lib-group">${escapeHtml(group)}</div>`);
|
||||
lastGroup = group;
|
||||
}
|
||||
parts.push(libItemHtml(file));
|
||||
});
|
||||
list.innerHTML = parts.join("");
|
||||
}
|
||||
|
||||
/* ---------- Flow rechts ---------- */
|
||||
@@ -795,18 +976,23 @@ function selectFile(id) {
|
||||
/* ---------- Laden ---------- */
|
||||
|
||||
async function load() {
|
||||
const [settings, fileRows, printerRows, spoolRows, projectRows, defaults] = await Promise.all([
|
||||
const [settings, fileRows, printerRows, spoolRows, projectRows, defaults, folderPayload] = await Promise.all([
|
||||
api("/einstellungen"),
|
||||
api("/dateien"),
|
||||
api("/drucker?live=1"),
|
||||
api("/spulen"),
|
||||
api("/projekte").catch(() => []),
|
||||
api("/abraeumen-defaults").catch(() => null),
|
||||
api("/dateien/ordner").catch(() => ({ folders: [] })),
|
||||
]);
|
||||
document.getElementById("watch").textContent = settings.watch_dir
|
||||
hasWatchDir = !!(settings.watch_dir);
|
||||
document.getElementById("watch").textContent = hasWatchDir
|
||||
? "Slice-Ordner: " + settings.watch_dir
|
||||
: "Kein Slice-Ordner — Dateien hier hochladen.";
|
||||
const createBtn = document.getElementById("folder-create");
|
||||
if (createBtn) createBtn.hidden = !hasWatchDir;
|
||||
files = fileRows;
|
||||
knownFolders = (folderPayload && folderPayload.folders) || [];
|
||||
printers = printerRows;
|
||||
spools = spoolRows;
|
||||
projects = projectRows || [];
|
||||
@@ -816,11 +1002,26 @@ async function load() {
|
||||
if (sel.fileId && !selectedFile()) sel.fileId = null;
|
||||
fillProjects();
|
||||
fillProjectFilter();
|
||||
fillMaterialFilter();
|
||||
fillFolderFilter();
|
||||
loadLibMemory();
|
||||
fillProjectFilter();
|
||||
fillMaterialFilter();
|
||||
fillFolderFilter();
|
||||
renderLibrary();
|
||||
renderTabs();
|
||||
refreshFlow();
|
||||
const file = selectedFile();
|
||||
if (file) showToolpath(file, 0);
|
||||
// Deep-Link von Orca /orca_senden: Senden in den Blick, Optionen offen für ACE
|
||||
if (new URLSearchParams(location.search).get("file") && file) {
|
||||
sel.tab = "send";
|
||||
renderTabs();
|
||||
const opts = document.getElementById("options");
|
||||
if (opts) opts.open = true;
|
||||
const panel = document.querySelector(".send-panel");
|
||||
if (panel) panel.scrollIntoView({ behavior: "smooth", block: "start" });
|
||||
}
|
||||
}
|
||||
|
||||
async function refreshPrinters() {
|
||||
@@ -1161,6 +1362,10 @@ async function doAblauf() {
|
||||
/* ---------- Ereignisse ---------- */
|
||||
|
||||
document.getElementById("lib-list").addEventListener("click", (event) => {
|
||||
if (event.target.closest(".lib-move")) {
|
||||
event.stopPropagation();
|
||||
return;
|
||||
}
|
||||
const del = event.target.closest("[data-delete]");
|
||||
if (del) {
|
||||
event.stopPropagation();
|
||||
@@ -1179,6 +1384,29 @@ document.getElementById("lib-list").addEventListener("click", (event) => {
|
||||
const item = event.target.closest("[data-file]");
|
||||
if (item) selectFile(item.dataset.file);
|
||||
});
|
||||
|
||||
document.getElementById("lib-list").addEventListener("change", async (event) => {
|
||||
const select = event.target.closest("select.lib-move");
|
||||
if (!select) return;
|
||||
event.stopPropagation();
|
||||
const fileId = select.dataset.move;
|
||||
let folder = select.value;
|
||||
select.value = "";
|
||||
if (!folder || !fileId) return;
|
||||
try {
|
||||
if (folder === "__new__") {
|
||||
folder = await promptCreateFolder();
|
||||
if (!folder) return;
|
||||
} else if (folder === "__root__") {
|
||||
folder = "";
|
||||
}
|
||||
await api("/dateien/" + fileId + "/verschieben", { method: "POST", json: { folder } });
|
||||
toast(folder ? ("Nach „" + folder + "“ verschoben.") : "In die Wurzel verschoben.", "good");
|
||||
await load();
|
||||
} catch (error) {
|
||||
toast(error.message || String(error), "bad");
|
||||
}
|
||||
});
|
||||
document.getElementById("lib-list").addEventListener("keydown", (event) => {
|
||||
const item = event.target.closest("[data-file]");
|
||||
if (item && (event.key === "Enter" || event.key === " ")) {
|
||||
@@ -1186,9 +1414,22 @@ document.getElementById("lib-list").addEventListener("keydown", (event) => {
|
||||
selectFile(item.dataset.file);
|
||||
}
|
||||
});
|
||||
["file-search", "file-filter-family", "file-filter-project"].forEach((id) => {
|
||||
document.getElementById(id).addEventListener("input", renderLibrary);
|
||||
document.getElementById(id).addEventListener("change", renderLibrary);
|
||||
[
|
||||
"file-search",
|
||||
"file-filter-family",
|
||||
"file-filter-project",
|
||||
"file-filter-material",
|
||||
"file-filter-folder",
|
||||
"file-sort",
|
||||
].forEach((id) => {
|
||||
const node = document.getElementById(id);
|
||||
if (!node) return;
|
||||
const onChange = () => {
|
||||
saveLibMemory();
|
||||
renderLibrary();
|
||||
};
|
||||
node.addEventListener("input", onChange);
|
||||
node.addEventListener("change", onChange);
|
||||
});
|
||||
|
||||
document.getElementById("targets").addEventListener("click", (event) => {
|
||||
@@ -1323,6 +1564,14 @@ const libCard = document.getElementById("lib-card");
|
||||
}));
|
||||
libCard.addEventListener("drop", (event) => uploadFiles(event.dataTransfer.files));
|
||||
|
||||
document.getElementById("folder-create")?.addEventListener("click", async () => {
|
||||
try {
|
||||
await promptCreateFolder();
|
||||
} catch (error) {
|
||||
toast(error.message || String(error), "bad");
|
||||
}
|
||||
});
|
||||
|
||||
document.getElementById("rescan").addEventListener("click", async () => {
|
||||
try {
|
||||
const result = await api("/dateien/neu-lesen", { method: "POST" });
|
||||
|
||||
Reference in new issue
Block a user