forked from viewit/KX-Bridge-Release
5464 lines
205 KiB
JavaScript
5464 lines
205 KiB
JavaScript
// ── State ──
|
||
var S = {
|
||
nozzle_temp: 0,
|
||
nozzle_target: 0,
|
||
bed_temp: 0,
|
||
bed_target: 0,
|
||
print_state: "standby",
|
||
filename: "",
|
||
progress: 0,
|
||
print_duration: 0,
|
||
remain_time: 0,
|
||
curr_layer: 0,
|
||
total_layers: 0,
|
||
z_mm: 0,
|
||
printer_name: "Kobra X",
|
||
firmware_version: "–",
|
||
camera_url: "",
|
||
fan_speed: 0,
|
||
print_speed_mode: 2,
|
||
light_on: false,
|
||
light_brightness: 80,
|
||
ams_slots: [],
|
||
filament_mode: "toolhead",
|
||
ace_units: [],
|
||
ace_dry_presets: null,
|
||
ace_drying: {
|
||
status: 0,
|
||
target_temp: 0,
|
||
duration: 0,
|
||
remain_time: 0,
|
||
humidity: null,
|
||
current_temp: null,
|
||
units: [],
|
||
},
|
||
web_upload_warning: 1,
|
||
};
|
||
var tempHistory = { n: [], b: [] };
|
||
var camOn = false;
|
||
var camUserStopped = false; // user stopped camera manually — suppress auto-restart for this print
|
||
var _camPollInterval = null; // snapshot-polling interval for Android (no MJPEG support)
|
||
var _lastLoadedFile = null; // zuletzt geladene/gedruckte Datei für Progress-Karten-Aktionen (Issue #55)
|
||
var _idleCleared = false; // User hat idle-Datei explizit „geleert" → kein Nachladen von s.filename (Issue #57)
|
||
var _fdDialogOpen = false; // Dialog ist gerade offen
|
||
var _fdAutoOpenedFile = sessionStorage.getItem("fdAutoOpenedFile") || null;
|
||
var _fdUserCancelled = sessionStorage.getItem("fdUserCancelled") === "1";
|
||
var currentStep = 1;
|
||
var currentPanel = "dashboard";
|
||
var aceAutoRefillPrefs = (function () {
|
||
try {
|
||
return JSON.parse(localStorage.getItem("aceAutoRefillPrefs") || "{}") || {};
|
||
} catch (_) {
|
||
return {};
|
||
}
|
||
})();
|
||
var aceDryProfiles = (function () {
|
||
try {
|
||
return JSON.parse(localStorage.getItem("aceDryProfiles") || "{}") || {};
|
||
} catch (_) {
|
||
return {};
|
||
}
|
||
})();
|
||
var _aceDryDialogAceId = -1;
|
||
var _aceDryDialogPresetKey = "";
|
||
var _aceDryDialogPresetOriginals = {};
|
||
var ACE_DRY_PRESET_DEFAULTS = {
|
||
pla: { temp: 45, duration_sec: 4 * 3600 },
|
||
pla_plus: { temp: 45, duration_sec: 4 * 3600 },
|
||
petg: { temp: 50, duration_sec: 4 * 3600 },
|
||
tpu: { temp: 55, duration_sec: 4 * 3600 },
|
||
abs_asa: { temp: 45, duration_sec: 8 * 3600 },
|
||
pa_pc: { temp: 55, duration_sec: 12 * 3600 },
|
||
};
|
||
var ACE_DRY_PRESETS = {
|
||
pla: { temp: 45, duration_sec: 4 * 3600 },
|
||
pla_plus: { temp: 45, duration_sec: 4 * 3600 },
|
||
petg: { temp: 50, duration_sec: 4 * 3600 },
|
||
tpu: { temp: 55, duration_sec: 4 * 3600 },
|
||
abs_asa: { temp: 45, duration_sec: 8 * 3600 },
|
||
pa_pc: { temp: 55, duration_sec: 12 * 3600 },
|
||
custom_1: { name: "Custom 1", temp: 45, duration_sec: 4 * 3600 },
|
||
custom_2: { name: "Custom 2", temp: 45, duration_sec: 4 * 3600 },
|
||
custom_3: { name: "Custom 3", temp: 45, duration_sec: 4 * 3600 },
|
||
};
|
||
|
||
// Spoolman state
|
||
var _spoolmanStatus={configured:false,reachable:false,server:'',sync_rate:0,slot_spools:{}};
|
||
var _spoolmanSpools=[];
|
||
var _slotSpoolMap={}; // {String(global_index): spoolman_spool_id} — last committed assignment
|
||
|
||
function _loadSpoolmanStatus() {
|
||
fetch(_apiUrl('/kx/spoolman/status')).then(function (r) { return r.json(); }).then(function (d) {
|
||
_spoolmanStatus = d;
|
||
_slotSpoolMap = d.slot_spools || {};
|
||
_updateSpoolmanStatusDot();
|
||
_buildSpoolmanSection();
|
||
renderSpoolmanSlotCard();
|
||
if (d.configured) {
|
||
fetch(_apiUrl('/kx/spoolman/spools')).then(function (r) { return r.json(); }).then(function (sd) {
|
||
_spoolmanSpools = sd.spools || [];
|
||
});
|
||
}
|
||
}).catch(function () { });
|
||
}
|
||
function _updateSpoolmanStatusDot(){
|
||
var dot=document.getElementById('spoolman-status-dot');
|
||
var lbl=document.getElementById('spoolman-status-lbl');
|
||
if(!dot||!lbl)return;
|
||
if(!_spoolmanStatus.configured){
|
||
dot.style.color='var(--txt2)';lbl.textContent='nicht konfiguriert';
|
||
} else if(_spoolmanStatus.reachable){
|
||
dot.style.color='var(--ok)';lbl.textContent=_spoolmanStatus.server||'verbunden';
|
||
} else {
|
||
dot.style.color='var(--err)';lbl.textContent=(_spoolmanStatus.server||'')+' (nicht erreichbar)';
|
||
}
|
||
}
|
||
|
||
function _buildSpoolmanSection() {
|
||
var sec = document.getElementById("fd-spoolman-section");
|
||
var rows = document.getElementById("fd-spoolman-rows");
|
||
var loading = document.getElementById("fd-spoolman-loading");
|
||
if (!sec || !rows) return;
|
||
if (!_spoolmanStatus.configured) {
|
||
sec.style.display = "none";
|
||
return;
|
||
}
|
||
sec.style.display = "";
|
||
rows.innerHTML = "";
|
||
if (loading) loading.style.display = "";
|
||
|
||
var usedSlots = {};
|
||
(_amsSlots || []).forEach(function (slot) {
|
||
usedSlots[slot.slot_index] = slot;
|
||
});
|
||
|
||
fetch(_apiUrl('/kx/spoolman/spools')).then(function(r){return r.json();}).then(function(d){
|
||
if(loading)loading.style.display='none';
|
||
_spoolmanSpools=d.spools||[];
|
||
var slotKeys=Object.keys(usedSlots).map(Number).sort(function(a,b){return a-b;});
|
||
if(!slotKeys.length){rows.innerHTML='<span style="font-size:11px;color:var(--txt2)">–</span>';return;}
|
||
rows.innerHTML=slotKeys.map(function(idx){
|
||
var slot=usedSlots[idx];
|
||
var col=(slot.color_hex||'#888');
|
||
var currentSpool=_slotSpoolMap[String(idx)]||'';
|
||
var opts='<option value="">–</option>'+_spoolmanSpools.map(function(sp){
|
||
var rem=sp.remaining_weight!=null?' ('+sp.remaining_weight.toFixed(0)+'g)':'';
|
||
var vendor=sp.filament&&sp.filament.vendor?sp.filament.vendor.name+' ':'';
|
||
var name=sp.filament&&sp.filament.name?sp.filament.name:'Spool';
|
||
return '<option value="'+sp.id+'"'+(sp.id==currentSpool?' selected':'')+'>'+
|
||
escHtml('#'+sp.id+' '+vendor+name+rem)+'</option>';
|
||
}).join('');
|
||
return '<div style="display:flex;align-items:center;gap:8px;font-size:12px">'+
|
||
'<span style="display:inline-block;width:14px;height:14px;border-radius:50%;background:'+col+';border:1px solid var(--border);flex-shrink:0"></span>'+
|
||
'<span style="color:var(--txt2);min-width:46px">Slot '+(idx+1)+'</span>'+
|
||
'<select data-spool-slot="'+idx+'" style="flex:1;padding:3px 6px;border-radius:6px;border:1px solid var(--border);background:var(--raised);color:var(--txt);font-size:12px">'+
|
||
opts+'</select></div>';
|
||
}).join('');
|
||
}).catch(function(){if(loading)loading.style.display='none';});
|
||
}
|
||
|
||
function _aceAutoRefillGet(aceId) {
|
||
return !!aceAutoRefillPrefs[String(aceId)];
|
||
}
|
||
function _aceAutoRefillSet(aceId, on) {
|
||
aceAutoRefillPrefs[String(aceId)] = !!on;
|
||
localStorage.setItem("aceAutoRefillPrefs", JSON.stringify(aceAutoRefillPrefs));
|
||
}
|
||
function _aceDryProfileGet(aceId) {
|
||
var p = aceDryProfiles[String(aceId)] || {};
|
||
var temp = parseInt(p.temp, 10);
|
||
var dur = parseInt(p.duration_sec, 10);
|
||
if (!Number.isFinite(temp)) temp = 45;
|
||
if (!Number.isFinite(dur)) dur = 4 * 3600;
|
||
temp = Math.max(30, Math.min(80, temp));
|
||
dur = Math.max(10 * 60, Math.min(24 * 3600, dur));
|
||
return { temp: temp, duration_sec: dur, preset: p.preset || "" };
|
||
}
|
||
function _aceDryProfileSet(aceId, temp, durationSec, preset) {
|
||
aceDryProfiles[String(aceId)] = {
|
||
temp: Math.max(30, Math.min(80, parseInt(temp, 10) || 45)),
|
||
duration_sec: Math.max(10 * 60, Math.min(24 * 3600, parseInt(durationSec, 10) || 4 * 3600)),
|
||
preset: preset || "",
|
||
};
|
||
localStorage.setItem("aceDryProfiles", JSON.stringify(aceDryProfiles));
|
||
}
|
||
function _aceDryDurationMinFromSec(sec) {
|
||
var minutes = Math.round((parseInt(sec, 10) || 0) / 60);
|
||
return Math.max(10, Math.min(1440, minutes));
|
||
}
|
||
function _syncAceDryPresetsFromServer(raw) {
|
||
if (!raw || typeof raw !== "object") return;
|
||
Object.keys(ACE_DRY_PRESETS).forEach(function (k) {
|
||
var p = raw[k];
|
||
if (!p || typeof p !== "object") return;
|
||
var t = parseInt(p.temp, 10);
|
||
var d = parseInt(p.duration_sec, 10);
|
||
if (Number.isFinite(t)) ACE_DRY_PRESETS[k].temp = Math.max(30, Math.min(80, t));
|
||
if (Number.isFinite(d))
|
||
ACE_DRY_PRESETS[k].duration_sec = Math.max(10 * 60, Math.min(24 * 3600, d));
|
||
if (/^custom_[123]$/.test(k) && typeof p.name === "string") {
|
||
var n = p.name.trim();
|
||
ACE_DRY_PRESETS[k].name = n || "Custom " + k.slice(-1);
|
||
}
|
||
});
|
||
}
|
||
|
||
// ── Theme ──
|
||
function toggleTheme() {
|
||
var h = document.documentElement;
|
||
h.setAttribute("data-theme", h.getAttribute("data-theme") === "dark" ? "light" : "dark");
|
||
localStorage.setItem("theme", h.getAttribute("data-theme"));
|
||
}
|
||
(function () {
|
||
var t = localStorage.getItem("theme");
|
||
if (t) document.documentElement.setAttribute("data-theme", t);
|
||
})();
|
||
|
||
// ── i18n ──
|
||
var currentLang = "de";
|
||
var T = {};
|
||
var _langCache = {};
|
||
|
||
function tr(key, fallback) {
|
||
var v = T && T[key];
|
||
return typeof v === "string" && v.length ? v : fallback !== undefined ? fallback : "";
|
||
}
|
||
|
||
function _langToggleLabel(lang) {
|
||
if (lang === "de") return "Deutsch";
|
||
if (lang === "en") return "English";
|
||
if (lang === "fr") return "Français";
|
||
if (lang === "it") return "Italiano";
|
||
if (lang === "zh-cn") return "简体中文";
|
||
return "Espanol";
|
||
}
|
||
|
||
function _mapSupportedLang(lang) {
|
||
if (!lang) return "";
|
||
var l = String(lang).toLowerCase().replace(/_/g, "-").trim();
|
||
if (l === "de" || l === "en" || l === "es" || l === "fr" || l === "it" || l === "zh-cn") return l;
|
||
|
||
var base = l.split("-")[0];
|
||
if (base === "de" || base === "en" || base === "es" || base === "fr" || base === "it")
|
||
return base;
|
||
|
||
if (base === "zh") {
|
||
if (l.indexOf("cn") >= 0 || l.indexOf("hans") >= 0 || l === "zh") return "zh-cn";
|
||
}
|
||
return "";
|
||
}
|
||
|
||
function _normalizeLang(lang) {
|
||
return _mapSupportedLang(lang) || "de";
|
||
}
|
||
|
||
function _detectBrowserLanguage() {
|
||
var prefs = [];
|
||
if (Array.isArray(navigator.languages) && navigator.languages.length) prefs = navigator.languages;
|
||
else if (navigator.language) prefs = [navigator.language];
|
||
for (var i = 0; i < prefs.length; i++) {
|
||
var mapped = _mapSupportedLang(prefs[i]);
|
||
if (mapped) return mapped;
|
||
}
|
||
return "";
|
||
}
|
||
|
||
function _resolveInitialLanguage() {
|
||
var saved = localStorage.getItem("lang");
|
||
var mappedSaved = _mapSupportedLang(saved);
|
||
if (mappedSaved) return mappedSaved;
|
||
return _detectBrowserLanguage() || "de";
|
||
}
|
||
|
||
async function _loadLanguage(lang) {
|
||
var l = _normalizeLang(lang);
|
||
if (_langCache[l]) return _langCache[l];
|
||
var res = await fetch("/kx/ui/translations/" + l + ".json");
|
||
if (!res.ok) throw new Error("failed to load translations: " + l);
|
||
var data = await res.json();
|
||
_langCache[l] = data || {};
|
||
return _langCache[l];
|
||
}
|
||
|
||
async function setLanguage(lang) {
|
||
var l = _normalizeLang(lang);
|
||
try {
|
||
T = await _loadLanguage(l);
|
||
} catch (_) {
|
||
var fb = l === "de" ? "en" : "de";
|
||
T = await _loadLanguage(fb);
|
||
l = fb;
|
||
}
|
||
currentLang = l;
|
||
localStorage.setItem("lang", l);
|
||
var langSel = document.getElementById("lang-select");
|
||
if (langSel) langSel.value = l;
|
||
var sLangSel = document.getElementById("s-lang-select");
|
||
if (sLangSel) sLangSel.value = l;
|
||
document.documentElement.setAttribute("lang", l);
|
||
applyLang();
|
||
}
|
||
|
||
function setLanguageFromSelect() {
|
||
var langSel = document.getElementById("lang-select");
|
||
var next = langSel ? langSel.value : "de";
|
||
setLanguage(next).catch(function () { });
|
||
}
|
||
// Multi-Printer: BASE_URL aus Pathname (/printer2 → andere Bridge-Instanz)
|
||
var _printers = [];
|
||
var _activePrinter = null;
|
||
(function () {
|
||
var path = window.location.pathname.replace(/\/+$/, "");
|
||
var m = path.match(/^\/printer(\d+)$/);
|
||
var idx = m ? parseInt(m[1]) : 1;
|
||
window._printerIndex = idx;
|
||
})();
|
||
function _apiUrl(path) {
|
||
if (_activePrinter && _activePrinter.bridge_url) {
|
||
return _activePrinter.bridge_url.replace(/\/+$/, "") + path;
|
||
}
|
||
return path;
|
||
}
|
||
function initPrinters() {
|
||
fetch("/kx/printers")
|
||
.then(function (r) {
|
||
return r.json();
|
||
})
|
||
.then(function (d) {
|
||
// immer lokale Instanz für Drucker-Liste
|
||
_printers = d.result || [];
|
||
var idx = window._printerIndex || 1;
|
||
_activePrinter =
|
||
_printers.find(function (p) {
|
||
return String(p.id) === String(idx);
|
||
}) ||
|
||
_printers[0] ||
|
||
null;
|
||
renderPrinterDropdown();
|
||
})
|
||
.catch(function () { });
|
||
}
|
||
function renderPrinterDropdown(){
|
||
var wrap=document.getElementById('printer-dropdown-wrap');
|
||
var single=document.getElementById('h-pname-single');
|
||
var name=_printers.length===0?'–':(_activePrinter?(_activePrinter.name||'Kobra X'):'Kobra X');
|
||
var pname=document.getElementById('h-pname');
|
||
if(pname)pname.textContent=name;
|
||
if(single)single.textContent=name;
|
||
if(_printers.length>1){
|
||
if(wrap)wrap.style.display='';
|
||
if(single)single.style.display='none';
|
||
var menu=document.getElementById('printer-dropdown-menu');
|
||
if(menu){
|
||
menu.innerHTML=_printers.map(function(p){
|
||
var active=_activePrinter&&String(p.id)===String(_activePrinter.id);
|
||
var num=p.id;
|
||
return '<a href="'+((p.bridge_url||'').replace(/\/+$/,''))+'/printer'+num+'" style="display:block;padding:10px 14px;color:'+(active?'var(--accent)':'var(--txt)')+';text-decoration:none;font-size:13px;border-bottom:1px solid var(--border)" '+(active?'style="font-weight:600"':'')+'>'+
|
||
(active?'▶ ':'')+p.name+'</a>';
|
||
}).join('');
|
||
}
|
||
} else {
|
||
if (wrap) wrap.style.display = "none";
|
||
if (single) single.style.display = "";
|
||
}
|
||
}
|
||
function togglePrinterDropdown() {
|
||
var menu = document.getElementById("printer-dropdown-menu");
|
||
if (menu) menu.style.display = menu.style.display === "none" ? "block" : "none";
|
||
}
|
||
document.addEventListener("click", function (e) {
|
||
var wrap = document.getElementById("printer-dropdown-wrap");
|
||
if (wrap && !wrap.contains(e.target)) {
|
||
var menu = document.getElementById("printer-dropdown-menu");
|
||
if (menu) menu.style.display = "none";
|
||
}
|
||
});
|
||
function applyLang() {
|
||
ensureAceDryCards();
|
||
// Nav
|
||
var nb = document.getElementById("nb-dashboard");
|
||
if (nb) nb.querySelector(".nav-text").textContent = T.nav_dashboard;
|
||
nb = document.getElementById("nb-console");
|
||
if (nb) nb.querySelector(".nav-text").textContent = T.nav_console;
|
||
nb = document.getElementById("nb-printers");
|
||
if (nb) nb.querySelector(".nav-text").textContent = T.nav_printers;
|
||
nb = document.getElementById("nb-store");
|
||
if (nb) nb.querySelector(".nav-text").textContent = T.nav_browser;
|
||
// Bottom nav
|
||
var bnb = document.getElementById("bnb-dashboard");
|
||
if (bnb) bnb.lastChild.textContent = T.nav_dashboard;
|
||
bnb = document.getElementById("bnb-console");
|
||
if (bnb) bnb.lastChild.textContent = T.nav_console;
|
||
bnb = document.getElementById("bnb-printers");
|
||
if (bnb) bnb.lastChild.textContent = T.nav_printers;
|
||
bnb = document.getElementById("bnb-store");
|
||
if (bnb) bnb.lastChild.textContent = T.nav_browser;
|
||
// Browser panel
|
||
setText('printers-panel-title','🖨 '+T.nav_printers);
|
||
setText('add-printer-btn-label',T.add_printer);
|
||
setText('apd-title',T.add_printer);
|
||
setText('skip-title',T.skip_title);
|
||
setText('skip-hint',T.skip_hint);
|
||
setText('d-btn-skip-label',T.skip_btn_label);
|
||
setText('fd-objects-toggle-lbl',T.fd_objects_toggle);
|
||
setText('apd-lbl-ip',T.apd_lbl_ip);
|
||
setText('apd-lbl-name',T.apd_lbl_name);
|
||
var apn=document.getElementById('apd-name');if(apn)apn.setAttribute('placeholder',T.apd_placeholder_name);
|
||
setText('apd-cancel',T.apd_cancel);
|
||
setText('apd-confirm',T.apd_confirm);
|
||
setText('fd-slots-hint',T.fd_slots_hint);
|
||
setText('fd-cancel',T.fd_cancel);
|
||
setText('fd-print',T.fd_print);
|
||
setText('store-panel-title','🗂 '+T.panel_browser_title);
|
||
var srb=document.getElementById('store-refresh-btn');if(srb)srb.textContent=T.store_refresh;
|
||
var ssp=document.getElementById('store-search');if(ssp)ssp.setAttribute('placeholder',T.store_search_placeholder);
|
||
setText('store-upload-label-prefix',T.store_upload_label_prefix);
|
||
setText('store-upload-label-browse',T.store_upload_label_browse);
|
||
setText('store-empty',T.store_empty);
|
||
setText('sf-all',T.sf_all);setText('sf-ok',T.sf_ok);setText('sf-err',T.sf_err);setText('sf-new',T.sf_new);
|
||
setText('ss-date',T.ss_date);setText('ss-name',T.ss_name);setText('ss-dur',T.ss_dur);
|
||
setText('store-web-verify-title',T.store_web_verify_title);
|
||
setText('store-web-verify-msg',T.store_web_verify_msg);
|
||
setText('store-web-verify-confirm',T.store_web_verify_confirm);
|
||
setText('store-web-verify-abort',T.store_web_verify_abort);
|
||
setText('store-lbl-select-all',T.store_select_all||'Select All');
|
||
setText('store-lbl-delete-selected',T.store_delete_selected||'Delete Selected');
|
||
setText('store-lbl-exit-select',T.store_exit_select||'Cancel');
|
||
setText('btab-lbl-uploaded',T.browser_tab_uploaded||'Uploaded');
|
||
setText('btab-lbl-printer',T.browser_tab_printer||'On Printer');
|
||
setText('printer-store-lbl-select-all',T.store_select_all||'Select All');
|
||
setText('printer-store-lbl-delete-selected',T.store_delete_selected||'Delete Selected');
|
||
setText('printer-store-lbl-exit-select',T.store_exit_select||'Cancel');
|
||
// Dashboard card titles
|
||
setText("d-card-progress", T.card_progress);
|
||
setText("d-card-temps", T.card_temps);
|
||
setText("d-card-lightfan", T.card_light_fan);
|
||
setText("d-card-speed", T.card_speed);
|
||
setText("d-card-cam", T.card_cam);
|
||
setText("d-card-ams", T.panel_ams_title);
|
||
setText("d-lbl-elapsed", T.lbl_elapsed);
|
||
setText("d-lbl-remain", T.lbl_remaining);
|
||
setText("d-slicer-label", T.lbl_slicer_time);
|
||
setText("d-lbl-layers", T.lbl_layers);
|
||
setText("d-lbl-zpos", T.lbl_zpos);
|
||
setText("d-lbl-light", T.lbl_light);
|
||
setText("d-lbl-nozzle", T.label_nozzle);
|
||
setText("d-lbl-bed", T.label_bed);
|
||
// Dashboard buttons — Pause-Button wird zur Toggle-Action; Resume-Beschriftung
|
||
// wird in updatePauseResumeBtn() je nach Druckerzustand gesetzt.
|
||
updatePauseResumeBtn();
|
||
setText("d-btn-cancel", T.btn_cancel);
|
||
setText("cam-toggle-btn", camOn ? T.btn_cam_stop : T.btn_cam_start);
|
||
setText("cam-placeholder-txt", T.cam_placeholder);
|
||
// Temp labels
|
||
document.querySelectorAll(".lbl-set").forEach((e) => (e.textContent = T.label_set));
|
||
document.querySelectorAll(".lbl-off").forEach((e) => (e.textContent = T.label_off));
|
||
setText("d-chart-label", T.panel_temps_chart);
|
||
// Axis labels
|
||
setText('ptitle-motion-xy', T.panel_motion_xy);
|
||
document.querySelectorAll('.lbl-home-z').forEach(e => e.textContent = T.btn_home_z);
|
||
document.querySelectorAll('.lbl-home-xy').forEach(e => e.textContent = T.btn_home_xy);
|
||
document.querySelectorAll('.lbl-home-all').forEach(e => e.textContent = T.btn_home_all);
|
||
document.querySelectorAll('.lbl-disable-motors').forEach(e => e.textContent = T.btn_disable_motors);
|
||
document.querySelectorAll('.temp-input').forEach(e => e.setAttribute('placeholder', T.label_target_c.replace(':', '')));
|
||
// Console
|
||
setText("ptitle-console", T.panel_console_title);
|
||
// Settings-Panel
|
||
setText("modal-sec-connection", T.settings_connection);
|
||
setText("modal-sec-print", T.settings_print);
|
||
setText("modal-sec-notifications", T.settings_notifications);
|
||
var nhEl = document.getElementById("notif-hint");
|
||
if (nhEl && T.settings_notifications_hint) nhEl.innerHTML = T.settings_notifications_hint;
|
||
setText("lbl-notif-add", T.settings_notif_add);
|
||
notifUpdateToggleBtn();
|
||
setText("lbl-notif-interval", T.settings_notif_interval_lbl);
|
||
setText("lbl-notif-min-unit", T.settings_notif_min_unit);
|
||
setText("lbl-notif-layers-unit", T.settings_notif_layers_unit);
|
||
setText("lbl-notif-zero-off", T.settings_notif_zero_off);
|
||
setText("modal-sec-version", T.settings_version);
|
||
// Nav + Kategorie-Labels (mit Fallback falls i18n-Key noch fehlt)
|
||
setText("nav-settings", T.nav_settings || "Einstellungen");
|
||
setText("setcat-lbl-connection", T.settings_connection || "Verbindung");
|
||
setText("setcat-lbl-printer", T.settings_print || "Drucker");
|
||
setText("setcat-lbl-display", T.settings_cat_display || "Darstellung");
|
||
setText("setcat-lbl-display2", T.settings_cat_display || "Darstellung");
|
||
setText("setcat-lbl-filament", T.settings_cat_filament || "Filament");
|
||
setText("setcat-lbl-integrations", T.settings_integrations || "Integrationen");
|
||
setText("modal-sec-spoolman", T.modal_sec_spoolman || "Spoolman");
|
||
setText("lbl-spoolman-url", T.lbl_spoolman_url || "Server-URL");
|
||
setText("lbl-spoolman-sync-rate", T.lbl_spoolman_sync_rate || "Sync-Rate (s, 0=aus)");
|
||
setText("modal-sec-obico", T.modal_sec_obico || "Obico");
|
||
var obicoBox = document.getElementById("obico-info-box");
|
||
if (obicoBox) {
|
||
var cfgEl = document.getElementById("obico-cfg-path");
|
||
var cfgPath = cfgEl ? cfgEl.textContent : "";
|
||
obicoBox.innerHTML =
|
||
(T.obico_info_configured_via ||
|
||
"Obico wird über den <code>moonraker-obico</code>-Container konfiguriert.") +
|
||
"<br>" +
|
||
(T.obico_config_file_label || "Config-Datei:") +
|
||
' <code id="obico-cfg-path">' +
|
||
cfgPath +
|
||
"</code>";
|
||
}
|
||
setText("setcat-lbl-notifications", T.settings_cat_notifications || "Benachrichtigungen");
|
||
setText("setcat-lbl-system", T.settings_cat_system || "System");
|
||
setText("lbl-set-lang", T.settings_cat_language || "Sprache");
|
||
setText("lbl-set-theme", T.settings_cat_theme || "Hell / Dunkel umschalten");
|
||
setText("lbl-poll-interval", T.settings_poll || "Poll-Intervall (Sekunden)");
|
||
var pollHint = document.getElementById("lbl-poll-hint");
|
||
if (pollHint)
|
||
pollHint.textContent =
|
||
T.settings_poll_interval_hint || "Wie oft die Bridge den Drucker-Status abfragt";
|
||
setText('lbl-verbose-http-log',T.settings_verbose_http_log||'Log every HTTP request (verbose)');
|
||
var dashLbl=document.getElementById('dash-lbl-edit');
|
||
if(dashLbl)dashLbl.textContent=_dashEditing?(T.dash_done||'Done'):(T.dash_edit||'Customize dashboard');
|
||
setText('dash-lbl-reset',T.dash_reset||'Reset');
|
||
setText('dash-lbl-save-preset',T.dash_save_preset||'Save as preset');
|
||
var dashPresetStd=document.getElementById('dash-preset-standard');
|
||
if(dashPresetStd)dashPresetStd.textContent=T.dash_preset_standard||'Standard';
|
||
var dashPresetWide=document.getElementById('dash-preset-wide89');
|
||
if(dashPresetWide)dashPresetWide.textContent=T.dash_preset_wide89||'Wide desktop';
|
||
if(document.getElementById('dash-hidden-bar'))_dashRenderHiddenBar();
|
||
setText('lbl-filament-mapping',T.settings_filament_mapping||'Filament-Profil-Mapping (pro Slot)');
|
||
setText('lbl-filament-mapping-save',T.settings_filament_mapping_save||'Mapping speichern');
|
||
setText('lbl-visible-vendors',T.settings_visible_vendors||'Sichtbare Hersteller (Profil-Dropdown)');
|
||
setText('visible-vendors-hint',T.settings_visible_vendors_hint||'Nur diese Hersteller erscheinen im Slot-Profil-Dropdown. Nichts ausgewählt = alle anzeigen. „Generic" und eigene Profile sind immer sichtbar.');
|
||
setText('lbl-visible-vendors-save',T.settings_visible_vendors_save||'Auswahl speichern');
|
||
// Custom-Profile-Import (Issue #41)
|
||
setText("modal-sec-orca-profiles", T.orca_profile_section);
|
||
setText("orca-profiles-hint", T.orca_profile_hint);
|
||
setText("lbl-orca-profiles-import", T.orca_profile_import_btn);
|
||
setText("lbl-slot-profile-import", T.orca_profile_import_link);
|
||
setText("profile-import-title", T.orca_profile_import_title);
|
||
setText("profile-import-dropmsg", T.orca_profile_dropmsg);
|
||
setText("profile-import-list-label", T.orca_profile_list_label);
|
||
// Hilfe-Text mit Inline-HTML — innerHTML statt setText
|
||
var helpEl = document.getElementById("profile-import-help");
|
||
if (helpEl && T.orca_profile_help_html) helpEl.innerHTML = T.orca_profile_help_html;
|
||
setText("btn-save-settings", T.settings_save);
|
||
setText("lbl-printer-name", T.settings_printer_name);
|
||
setText("lbl-printer-ip", T.settings_printer_ip);
|
||
setText("lbl-mqtt-port", T.settings_mqtt_port);
|
||
setText("lbl-username", T.settings_username);
|
||
setText("lbl-password", T.settings_password);
|
||
setText("lbl-device-id", T.settings_device_id);
|
||
setText("lbl-mode-id", T.settings_mode_id);
|
||
setText("lbl-default-slot", T.settings_default_slot);
|
||
setText("opt-slot-auto", T.settings_slot_auto);
|
||
setText("lbl-auto-leveling", T.settings_auto_leveling);
|
||
setText('lbl-vibration-compensation',T.settings_vibration_compensation);
|
||
setText("lbl-file-ready-mode", T.settings_file_ready_mode);
|
||
setText("opt-file-ready-dialog", T.settings_file_ready_dialog);
|
||
setText("opt-file-ready-banner", T.settings_file_ready_banner);
|
||
setText("lbl-camera-on-print", T.settings_camera_on_print);
|
||
setText("lbl-web-upload-warning", T.settings_web_upload_warning);
|
||
setText("fd-options-title", T.fd_options_title);
|
||
setText("fd-lbl-auto-leveling", T.print_auto_leveling);
|
||
|
||
setText("lbl-update-check", T.update_check);
|
||
setText("lbl-update-apply", T.update_apply);
|
||
// Progress-Karten-Aktionen für geladene/idle Datei (Issue #55)
|
||
setText("d-idle-print-lbl", T.progress_action_print || "Drucken");
|
||
setText("d-idle-slots-lbl", T.progress_action_slots || "Slots zuordnen");
|
||
setText("d-idle-clear-lbl", T.progress_action_clear || "Leeren");
|
||
// Speed buttons
|
||
setText("d-spd-lbl-1", T.speed_silent.replace(/^\S+\s/, ""));
|
||
setText("d-spd-lbl-2", T.speed_normal.replace(/^\S+\s/, ""));
|
||
setText("d-spd-lbl-3", T.speed_sport.replace(/^\S+\s/, ""));
|
||
// AMS feed/unload
|
||
document.querySelectorAll(".lbl-feed").forEach((e) => (e.textContent = T.lbl_feed));
|
||
document.querySelectorAll(".lbl-unload").forEach((e) => (e.textContent = T.lbl_unload));
|
||
for (var i = 0; i < 4; i++) {
|
||
setText("d-card-ace-dry-" + i, "ACE " + (i + 1) + " - " + tr("ace_dry_dryer"));
|
||
setText("d-ace-auto-refill-label-" + i, tr("ace_dry_auto_refill"));
|
||
setText("d-ace-drying-enable-label-" + i, tr("ace_dry_enable"));
|
||
setText("d-ace-dry-humidity-label-" + i, tr("ace_dry_humidity") + ":");
|
||
setText("d-ace-dry-current-temp-label-" + i, tr("ace_dry_current_temp") + ":");
|
||
setText("d-ace-dry-target-label-" + i, tr("ace_dry_temp_line") + ":");
|
||
setText("d-ace-dry-time-label-" + i, tr("ace_dry_time_line") + ":");
|
||
setText("d-ace-dry-chart-label-" + i, tr("ace_dry_chart"));
|
||
var adTemp = document.getElementById("ace-dry-temp-" + i);
|
||
if (adTemp) adTemp.setAttribute("placeholder", T.ace_dry_temp);
|
||
var adDur = document.getElementById("ace-dry-duration-" + i);
|
||
if (adDur) adDur.setAttribute("placeholder", T.ace_dry_duration);
|
||
}
|
||
setText("ace-dry-dialog-title", tr("ace_dry_dialog_title"));
|
||
setText("ace-dry-dialog-temp-label", tr("ace_dry_dialog_temp"));
|
||
setText("ace-dry-dialog-time-label", tr("ace_dry_dialog_time"));
|
||
setText("ace-dry-dialog-custom-name-label", tr("ace_dry_dialog_custom_name"));
|
||
setText("ace-dry-dialog-cancel", tr("ace_dry_dialog_cancel"));
|
||
setText("ace-dry-dialog-confirm", tr("ace_dry_dialog_confirm"));
|
||
setText("ace-dry-dialog-reset-default", tr("ace_dry_dialog_reset_default"));
|
||
setText("ace-dry-dialog-save-preset", tr("ace_dry_dialog_save_restart"));
|
||
aceDryDialogSyncCustomButtonNames();
|
||
// conn-btn text (nur wenn nicht im Übergangszustand)
|
||
updateConnBtn();
|
||
// Slot-Edit-Dialog
|
||
setText("lbl-slot-color", T.slot_edit_color);
|
||
setText("lbl-slot-material", T.slot_edit_material);
|
||
setText("lbl-slot-profile", T.slot_edit_profile);
|
||
setText("slot-profile-hint", T.slot_edit_profile_hint);
|
||
var defOpt = document.getElementById("slot-profile-default-opt");
|
||
if (defOpt) defOpt.textContent = T.slot_edit_profile_default;
|
||
setText("btn-slot-edit-save", T.slot_edit_save);
|
||
updateSlotEditFeedButton();
|
||
var mi = document.getElementById("slot-edit-mat");
|
||
if (mi) mi.setAttribute("placeholder", T.slot_edit_custom);
|
||
setText("logdir-all", T.log_dir_all);
|
||
setText("loglvl-all", T.log_dir_all);
|
||
setText("log-lbl-level", T.log_lvl_label);
|
||
setText("file-ready-btn", T.file_ready_btn);
|
||
setText("file-slots-btn", T.file_slots_btn);
|
||
setText("file-cancel-btn", T.file_cancel_btn);
|
||
// Elements not yet covered by setText above
|
||
var settingsBtn = document.getElementById("settings-btn");
|
||
if (settingsBtn)
|
||
settingsBtn.title = T.settings_btn_tooltip || T.settings_title || "Einstellungen";
|
||
var snpEl = document.getElementById("s-printer-name");
|
||
if (snpEl) snpEl.placeholder = T.settings_printer_name_placeholder || "z.B. Kobra X Links";
|
||
var sdidEl = document.getElementById("s-device-id");
|
||
if (sdidEl) sdidEl.placeholder = T.settings_device_id_placeholder || "32 Hex-Zeichen";
|
||
setText("d-fan-off", T.label_off || "Aus");
|
||
setText("skip-confirm", T.skip_confirm_btn || "Überspringen");
|
||
setText("ams-no-data", T.ams_no_data || "Keine AMS-Daten empfangen");
|
||
// Dialog: Add Printer
|
||
setText("apd-title", T.apd_title || "Drucker hinzufügen");
|
||
setText("apd-cancel", T.apd_cancel || "Abbrechen");
|
||
setText("apd-confirm", T.apd_confirm || "Hinzufügen");
|
||
// Dialog: File Ready (Filament/Slot selection)
|
||
setText("fd-options-title", T.fd_options_title || "Druckoptionen");
|
||
setText("fd-cancel", T.fd_cancel || "Abbrechen");
|
||
setText("fd-print", T.fd_print || "▶ Drucken");
|
||
// Dialog: Web Upload Verify
|
||
setText("store-web-verify-title", T.store_web_verify_title || "Datei verifizieren");
|
||
setText(
|
||
"store-web-verify-msg",
|
||
T.store_web_verify_msg ||
|
||
"Bitte bestätige, dass diese Datei für den Anycubic Kobra X erstellt wurde."
|
||
);
|
||
setText("store-web-verify-confirm", T.store_web_verify_confirm || "Bestätigen");
|
||
setText("store-web-verify-abort", T.store_web_verify_abort || "Abbrechen");
|
||
// GCode-Browser-Karten: Texte sind via innerHTML eingebacken,
|
||
// bei Sprachwechsel komplett neu rendern.
|
||
if (typeof renderStore === "function" && typeof storeFiles !== "undefined") {
|
||
try {
|
||
renderStore();
|
||
} catch (e) { }
|
||
}
|
||
}
|
||
function setText(id, txt) {
|
||
var el = document.getElementById(id);
|
||
if (el) el.textContent = txt;
|
||
}
|
||
|
||
function ensureAceDryCards() {
|
||
var grid = document.getElementById("d-ace-dry-grid");
|
||
if (!grid || grid.getAttribute("data-init") === "1") return;
|
||
var html = "";
|
||
for (var i = 0; i < 4; i++) {
|
||
html +=
|
||
'<div class="card" id="d-ace-dry-card-' +
|
||
i +
|
||
'" style="display:none">' +
|
||
'<div class="card-title"><span>♨</span> <span id="d-card-ace-dry-' +
|
||
i +
|
||
'">ACE ' +
|
||
(i + 1) +
|
||
" - Dryer</span></div>" +
|
||
'<div style="display:flex;justify-content:space-between;gap:10px;font-size:14px;color:var(--txt2);margin-bottom:10px">' +
|
||
'<span><span id="d-ace-dry-current-temp-label-' +
|
||
i +
|
||
'">Temperature:</span> <span id="d-ace-dry-current-temp-' +
|
||
i +
|
||
'" style="font-size:30px;font-weight:700;color:#ff8c2f;line-height:1">-</span></span>' +
|
||
'<span><span id="d-ace-dry-humidity-label-' +
|
||
i +
|
||
'">Humidity:</span> <span id="d-ace-dry-humidity-' +
|
||
i +
|
||
'" style="font-size:30px;font-weight:700;color:#3aa8ff;line-height:1">-</span></span>' +
|
||
"</div>" +
|
||
'<div style="height:1px;background:var(--border);margin-bottom:10px"></div>' +
|
||
'<div style="display:flex;justify-content:space-between;gap:10px;font-size:14px;color:var(--txt2);margin-bottom:10px">' +
|
||
'<span><span id="d-ace-dry-target-label-' +
|
||
i +
|
||
'">Drying Temperature:</span> <span id="d-ace-dry-target-' +
|
||
i +
|
||
'" style="font-size:30px;font-weight:700;color:#ff8c2f;line-height:1">-</span></span>' +
|
||
'<span><span id="d-ace-dry-time-label-' +
|
||
i +
|
||
'">Drying Time:</span> <span id="d-ace-dry-time-' +
|
||
i +
|
||
'" style="font-size:30px;font-weight:700;color:#fff;line-height:1">-</span></span>' +
|
||
"</div>" +
|
||
'<div style="margin-bottom:10px">' +
|
||
'<button onclick="openAceDryDialog(' +
|
||
i +
|
||
')" title="Edit Dryer Temp/Time Settings" style="width:100%;padding:8px 10px;border-radius:8px;border:1px solid var(--accent);background:var(--accent);color:#000;cursor:pointer;font-size:13px;font-weight:600;line-height:1.2">Set Temp/Time</button>' +
|
||
"</div>" +
|
||
'<div style="height:1px;background:var(--border);margin-bottom:10px"></div>' +
|
||
'<div class="toggle-row" style="margin-bottom:10px">' +
|
||
'<span class="toggle-label" id="d-ace-auto-refill-label-' +
|
||
i +
|
||
'">Auto Refill</span>' +
|
||
'<label class="toggle">' +
|
||
'<input type="checkbox" id="ace-auto-refill-toggle-' +
|
||
i +
|
||
'" onchange="aceAutoRefillToggle(' +
|
||
i +
|
||
')">' +
|
||
'<span class="toggle-track"></span>' +
|
||
'<span class="toggle-thumb"></span>' +
|
||
"</label>" +
|
||
"</div>" +
|
||
'<div class="toggle-row" style="margin-bottom:8px">' +
|
||
'<span class="toggle-label" id="d-ace-drying-enable-label-' +
|
||
i +
|
||
'">Enable Drying</span>' +
|
||
'<label class="toggle">' +
|
||
'<input type="checkbox" id="ace-dry-enable-toggle-' +
|
||
i +
|
||
'" onchange="aceDryToggle(' +
|
||
i +
|
||
',this.checked)">' +
|
||
'<span class="toggle-track"></span>' +
|
||
'<span class="toggle-thumb"></span>' +
|
||
"</label>" +
|
||
"</div>" +
|
||
"</div>";
|
||
}
|
||
grid.innerHTML = html;
|
||
grid.setAttribute("data-init", "1");
|
||
}
|
||
(function () {
|
||
var l = _resolveInitialLanguage();
|
||
currentLang = _normalizeLang(l);
|
||
var langSel = document.getElementById("lang-select");
|
||
if (langSel) langSel.value = currentLang;
|
||
document.documentElement.setAttribute("lang", currentLang);
|
||
// defer until DOM ready
|
||
window.addEventListener('DOMContentLoaded', function () {
|
||
setLanguage(currentLang).catch(function () { });
|
||
_loadSpoolmanStatus();
|
||
// Kein Drucker konfiguriert? → direkt in den Drucker-Tab (zeigt "+ Drucker hinzufügen")
|
||
fetch("/kx/printers")
|
||
.then(function (r) {
|
||
return r.json();
|
||
})
|
||
.then(function (d) {
|
||
if (!d.result || !d.result.length) {
|
||
showPanel("printers");
|
||
loadPrinterTab();
|
||
}
|
||
})
|
||
.catch(function () { });
|
||
});
|
||
})();
|
||
|
||
// ── Panel nav ──
|
||
function showPanel(id) {
|
||
document.querySelectorAll(".panel").forEach((p) => p.classList.remove("active"));
|
||
document.getElementById("panel-" + id).classList.add("active");
|
||
document.querySelectorAll(".nav-btn,.bnav-btn").forEach((b) => b.classList.remove("active"));
|
||
var nb = document.getElementById("nb-" + id);
|
||
if (nb) nb.classList.add("active");
|
||
var bnb = document.getElementById("bnb-" + id);
|
||
if (bnb) bnb.classList.add("active");
|
||
currentPanel = id;
|
||
if (id === "settings") openSettings();
|
||
}
|
||
|
||
// Settings-Kategorie umschalten (Master-Detail)
|
||
function showSettingsCat(name){
|
||
document.querySelectorAll('.set-group').forEach(g=>g.classList.remove('active'));
|
||
document.querySelectorAll('.set-cat').forEach(b=>b.classList.remove('active'));
|
||
var g=document.getElementById('setgrp-'+name);if(g)g.classList.add('active');
|
||
var c=document.getElementById('setcat-'+name);if(c)c.classList.add('active');
|
||
}
|
||
|
||
// Browser-Sub-Tab umschalten: hochgeladene Dateien (Bridge-Store) vs. Dateien
|
||
// auf dem Drucker selbst (interner Speicher, via listLocal MQTT).
|
||
var _printerFilesLoaded=false;
|
||
function showBrowserTab(name){
|
||
document.querySelectorAll('.browser-group').forEach(g=>g.classList.remove('active'));
|
||
document.querySelectorAll('.browser-tab').forEach(b=>b.classList.remove('active'));
|
||
var g=document.getElementById('browser-group-'+name);if(g)g.classList.add('active');
|
||
var t=document.getElementById('btab-'+name);if(t)t.classList.add('active');
|
||
if(name==='printer'&&!_printerFilesLoaded)loadPrinterFiles();
|
||
}
|
||
|
||
// ── Console log ──
|
||
var consoleLogs = [];
|
||
var logAutoScroll = true;
|
||
var logBadgeCount = 0;
|
||
var logDirFilter = "all"; // 'all'|'rx'|'tx'
|
||
var logLevelFilter = "all"; // 'all'|'err'|'warn'
|
||
var logTopicFilter = ""; // '' = no topic filter
|
||
|
||
function clog(msg, cls) {
|
||
cls = cls || "msg-info";
|
||
var ts = new Date().toLocaleTimeString("de", {
|
||
hour: "2-digit",
|
||
minute: "2-digit",
|
||
second: "2-digit",
|
||
});
|
||
_appendLog({ ts: ts, lvl: "", name: "ui", msg: msg }, cls);
|
||
}
|
||
function _lvlCls(lvl) {
|
||
if (lvl === "ERROR" || lvl === "CRITICAL") return "msg-err";
|
||
if (lvl === "WARNING") return "msg-warn";
|
||
if (lvl === "DEBUG") return "msg-info";
|
||
return "msg-ok";
|
||
}
|
||
function _appendLog(entry, forceCls) {
|
||
var cls = forceCls || _lvlCls(entry.lvl);
|
||
var label = entry.name ? "[" + entry.name + "] " : "";
|
||
var fullMsg = label + entry.msg;
|
||
// Wiederholungen als Zähler zusammenfassen (×N) statt N identische Zeilen.
|
||
var last = consoleLogs[consoleLogs.length - 1];
|
||
if (last && last.msg === fullMsg && last.cls === cls) {
|
||
last.count = (last.count || 1) + 1;
|
||
last.ts = entry.ts; // letzte Sichtung
|
||
renderLog();
|
||
return;
|
||
}
|
||
consoleLogs.push({ ts: entry.ts, msg: fullMsg, cls: cls, count: 1 });
|
||
if (consoleLogs.length > 500) consoleLogs.shift();
|
||
// Badge + Toast wenn Tab nicht aktiv und Fehler/Warnungen
|
||
if (currentPanel !== "console" && (cls === "msg-err" || cls === "msg-warn")) {
|
||
logBadgeCount++;
|
||
var bc = logBadgeCount > 99 ? "99+" : logBadgeCount;
|
||
["log-badge", "log-badge-bot"].forEach(function (id) {
|
||
var b = document.getElementById(id);
|
||
if (b) {
|
||
b.style.display = "inline";
|
||
b.textContent = bc;
|
||
}
|
||
});
|
||
}
|
||
if (cls === "msg-err") showToast(entry.msg.split("\n")[0]);
|
||
renderLog();
|
||
}
|
||
// Kurze rote Snackbar bei Fehlern (auch wenn Konsole-Tab nicht offen).
|
||
var _toastTimer = null;
|
||
function showToast(msg) {
|
||
var t = document.getElementById("kx-toast");
|
||
if (!t) {
|
||
t = document.createElement("div");
|
||
t.id = "kx-toast";
|
||
t.style.cssText =
|
||
"position:fixed;bottom:20px;left:50%;transform:translateX(-50%);background:var(--err);color:#fff;padding:10px 18px;border-radius:8px;font-size:13px;z-index:9999;max-width:90vw;box-shadow:0 4px 16px rgba(0,0,0,.4);cursor:pointer";
|
||
t.onclick = function () {
|
||
showPanel("console");
|
||
t.style.display = "none";
|
||
};
|
||
document.body.appendChild(t);
|
||
}
|
||
t.textContent = "⚠ " + msg;
|
||
t.style.display = "block";
|
||
clearTimeout(_toastTimer);
|
||
_toastTimer = setTimeout(function () {
|
||
t.style.display = "none";
|
||
}, 6000);
|
||
}
|
||
function setLogDir(dir) {
|
||
logDirFilter = dir;
|
||
document.querySelectorAll(".log-dir-btn").forEach(function (b) {
|
||
b.style.background = b.id === "logdir-" + dir ? "var(--accent)" : "var(--raised)";
|
||
b.style.color = b.id === "logdir-" + dir ? "#fff" : "var(--txt2)";
|
||
});
|
||
renderLog();
|
||
}
|
||
function setLogLevel(lvl) {
|
||
logLevelFilter = lvl;
|
||
document.querySelectorAll(".log-lvl-btn").forEach(function (b) {
|
||
b.style.background = b.id === "loglvl-" + lvl ? "var(--accent)" : "var(--raised)";
|
||
b.style.color = b.id === "loglvl-" + lvl ? "#fff" : "var(--txt2)";
|
||
});
|
||
renderLog();
|
||
}
|
||
function setLogTopic(topic) {
|
||
var inp = document.getElementById("log-filter");
|
||
var active = inp.value === topic;
|
||
inp.value = active ? "" : topic;
|
||
document.querySelectorAll(".log-topic-btn").forEach(function (b) {
|
||
var on = !active && b.getAttribute("data-topic") === topic;
|
||
b.style.background = on ? "var(--accent)" : "var(--raised)";
|
||
b.style.color = on ? "#fff" : "var(--txt2)";
|
||
});
|
||
renderLog();
|
||
}
|
||
function renderLog() {
|
||
var el = document.getElementById("console-log");
|
||
if (!el) return;
|
||
var filter = (document.getElementById("log-filter") || {}).value || "";
|
||
var fl = filter.toLowerCase();
|
||
var rows = consoleLogs.filter(function (l) {
|
||
var m = l.msg;
|
||
if (logDirFilter === "rx" && !/ RX[ (]/.test(m)) return false;
|
||
if (logDirFilter === "tx" && !/ TX[ (]/.test(m)) return false;
|
||
if (logLevelFilter === "err" && l.cls !== "msg-err") return false;
|
||
if (logLevelFilter === "warn" && l.cls !== "msg-err" && l.cls !== "msg-warn") return false;
|
||
if (fl && !m.toLowerCase().includes(fl)) return false;
|
||
return true;
|
||
});
|
||
var savedScroll = logAutoScroll ? null : el.scrollTop;
|
||
el.innerHTML = rows
|
||
.map(function (l) {
|
||
var cnt = l.count && l.count > 1 ? ' <span style="opacity:.7">(×' + l.count + ")</span>" : "";
|
||
return (
|
||
'<div><span class="ts">' +
|
||
l.ts +
|
||
'</span><span class="' +
|
||
l.cls +
|
||
'">' +
|
||
escHtml(l.msg) +
|
||
"</span>" +
|
||
cnt +
|
||
"</div>"
|
||
);
|
||
})
|
||
.join("");
|
||
if (logAutoScroll) el.scrollTop = el.scrollHeight;
|
||
else if (savedScroll !== null) el.scrollTop = savedScroll;
|
||
}
|
||
function onLogScroll() {
|
||
var el = document.getElementById("console-log");
|
||
if (!el) return;
|
||
var atBottom = el.scrollHeight - el.scrollTop - el.clientHeight < 30;
|
||
if (!atBottom && logAutoScroll) {
|
||
setAutoScroll(false);
|
||
}
|
||
}
|
||
function toggleAutoScroll() {
|
||
setAutoScroll(!logAutoScroll);
|
||
if (logAutoScroll) {
|
||
var el = document.getElementById("console-log");
|
||
if (el) el.scrollTop = el.scrollHeight;
|
||
}
|
||
}
|
||
function setAutoScroll(on) {
|
||
logAutoScroll = on;
|
||
var btn = document.getElementById("btn-autoscroll");
|
||
if (btn) {
|
||
btn.style.background = on ? "var(--accent)" : "var(--raised)";
|
||
btn.style.color = on ? "#fff" : "var(--txt2)";
|
||
}
|
||
}
|
||
function clearLogBadge() {
|
||
logBadgeCount = 0;
|
||
["log-badge", "log-badge-bot"].forEach(function (id) {
|
||
var b = document.getElementById(id);
|
||
if (b) b.style.display = "none";
|
||
});
|
||
}
|
||
function escHtml(s) {
|
||
return s.replace(/&/g, "&").replace(/</g, "<").replace(/>/g, ">");
|
||
}
|
||
// SSE server-log stream
|
||
(function () {
|
||
function connect() {
|
||
var es = new EventSource("/api/log/stream");
|
||
es.onmessage = function (e) {
|
||
try {
|
||
_appendLog(JSON.parse(e.data));
|
||
} catch (_) { }
|
||
};
|
||
es.onerror = function () {
|
||
es.close();
|
||
setTimeout(connect, 3000);
|
||
};
|
||
}
|
||
window.addEventListener("DOMContentLoaded", connect);
|
||
})();
|
||
|
||
// ── Helpers ──
|
||
function fmtTime(s) {
|
||
if (!s || s < 0) return "–";
|
||
var m = Math.floor(s / 60),
|
||
h = Math.floor(m / 60);
|
||
m %= 60;
|
||
return h > 0 ? h + "h " + m + "m" : m + "m";
|
||
}
|
||
function fmtHmsFromSec(total) {
|
||
total = Math.max(0, parseInt(total || 0, 10));
|
||
var h = Math.floor(total / 3600);
|
||
var mm = Math.floor((total % 3600) / 60);
|
||
var ss = total % 60;
|
||
return h + ":" + String(mm).padStart(2, "0") + ":" + String(ss).padStart(2, "0");
|
||
}
|
||
function post(url, body) {
|
||
return fetch(_apiUrl(url), {
|
||
method: "POST",
|
||
headers: { "Content-Type": "application/json" },
|
||
body: JSON.stringify(body),
|
||
});
|
||
}
|
||
function clamp(v, lo, hi) {
|
||
return Math.min(hi, Math.max(lo, v));
|
||
}
|
||
|
||
// ── Apply state to DOM ──
|
||
function applyState() {
|
||
var s = S;
|
||
_syncAceDryPresetsFromServer(s.ace_dry_presets);
|
||
// connection error banner – nur wenn überhaupt ein Drucker konfiguriert ist
|
||
var banner=document.getElementById('conn-error-banner');
|
||
if(banner){if(s.connection_error&&_printers.length>0){banner.textContent='⚠ '+tr('lbl_conn_error')+' '+s.connection_error;banner.style.display='block';}else{banner.style.display='none';}}
|
||
var pauseBanner=document.getElementById('pause-msg-banner');
|
||
if(pauseBanner){
|
||
if(s.pause_msg && s.print_state==='paused'){
|
||
var codePart = (s.error_code==0) ? ' ' :
|
||
' [<a href="https://wiki.anycubic.com/en/error-codes/'+s.error_code+'-code" target="_blank">'+s.error_code+'</a>] ';
|
||
pauseBanner.innerHTML='⏸ '+tr('lbl_pause_reason')+codePart+s.pause_msg;
|
||
pauseBanner.style.display='block';
|
||
}else{
|
||
pauseBanner.style.display='none';
|
||
}
|
||
}
|
||
var bannerVisible=false;
|
||
var frb=document.getElementById('file-ready-banner');
|
||
if(frb){
|
||
var shouldAutoOpen=(s.print_start_dialog===undefined?true:!!s.print_start_dialog);
|
||
if(s.file_ready&&s.print_state==='standby'){
|
||
document.getElementById('file-ready-name').textContent=s.file_ready;
|
||
// Neue Datei → Abbruch-Sperre aufheben
|
||
if (_fdAutoOpenedFile && _fdAutoOpenedFile !== s.file_ready) {
|
||
_fdUserCancelled = false;
|
||
sessionStorage.removeItem("fdUserCancelled");
|
||
sessionStorage.removeItem("fdAutoOpenedFile");
|
||
}
|
||
if (shouldAutoOpen) {
|
||
// Dialog-Modus: Banner niemals anzeigen.
|
||
frb.style.display = 'none';
|
||
if (!_fdDialogOpen && !_fdUserCancelled && _fdAutoOpenedFile !== s.file_ready) {
|
||
_fdAutoOpenedFile = s.file_ready;
|
||
startReadyFileWithSlots(s.file_ready, true, s.filament_mismatch || null);
|
||
}
|
||
} else {
|
||
frb.style.display = "flex";
|
||
bannerVisible = true;
|
||
}
|
||
} else {
|
||
frb.style.display = "none";
|
||
}
|
||
}
|
||
// skip-button (mid-print) – nur sichtbar wenn aktuell gedruckt wird
|
||
var printing = s.print_state === "printing" || s.print_state === "paused";
|
||
var skipBtn = document.getElementById("d-btn-skip");
|
||
if (skipBtn) skipBtn.style.display = printing ? "" : "none";
|
||
// Pause/Stopp-Buttons nur bei aktivem Druck zeigen (sonst verwirrend wenn
|
||
// der Drucker idle ist). Pause-Button rendert sich passend zum State um.
|
||
var ctrlBtns = document.getElementById("d-ctrl-btns");
|
||
if (ctrlBtns) ctrlBtns.style.display = printing ? "" : "none";
|
||
updatePauseResumeBtn();
|
||
// Zuletzt geladene Datei merken (Issue #55): solange sie über den State
|
||
// sichtbar ist. Beim Druckende/Abbruch leert die Bridge file_ready+filename
|
||
// (Issue #29) — die gemerkte Referenz bleibt für die Karten-Aktionen.
|
||
// Echte ready-Datei oder laufender Druck hebt einen vorherigen „Clear" auf.
|
||
if (s.file_ready || printing) _idleCleared = false;
|
||
if (s.file_ready) _lastLoadedFile = s.file_ready;
|
||
else if (s.filename && !_idleCleared) _lastLoadedFile = s.filename;
|
||
else if (_idleCleared) _lastLoadedFile = null;
|
||
// Idle-Aktionen (Drucken/Slots/Leeren) nur wenn nicht gedruckt wird, eine
|
||
// Datei bekannt ist und der grüne Banner nicht ohnehin schon dieselbe Aktion
|
||
// anbietet.
|
||
var idleBtns = document.getElementById("d-idle-btns");
|
||
if (idleBtns) {
|
||
var showIdle = !printing && _lastLoadedFile && !bannerVisible;
|
||
idleBtns.style.display = showIdle ? "" : "none";
|
||
if (showIdle) {
|
||
var dfn = document.getElementById("d-fname");
|
||
if (dfn && (!dfn.textContent || dfn.textContent === "–")) {
|
||
dfn.textContent = _lastLoadedFile;
|
||
dfn.title = _lastLoadedFile;
|
||
}
|
||
}
|
||
}
|
||
|
||
// header
|
||
var b = document.getElementById("h-badge");
|
||
b.className = "hbadge " + s.print_state;
|
||
document.getElementById("h-state").textContent =
|
||
T["kobra_" + s.kobra_state] || s.kobra_state || T.header_status_standby;
|
||
var _pn =
|
||
_printers.length === 0 ? "–" : (_activePrinter && _activePrinter.name) || s.printer_name;
|
||
var _el = document.getElementById("h-pname");
|
||
if (_el) _el.textContent = _pn;
|
||
var _el2 = document.getElementById("h-pname-single");
|
||
if (_el2) _el2.textContent = _pn;
|
||
var hv = document.getElementById("h-version");
|
||
if (hv && s.version) hv.textContent = "v" + s.version;
|
||
|
||
// temps
|
||
var nt = document.getElementById("d-nt");
|
||
if (nt) nt.textContent = s.nozzle_temp.toFixed(1);
|
||
var ntt = document.getElementById("d-nt-t");
|
||
if (ntt) ntt.textContent = s.nozzle_target.toFixed(0);
|
||
var bt = document.getElementById("d-bt");
|
||
if (bt) bt.textContent = s.bed_temp.toFixed(1);
|
||
var btt = document.getElementById("d-bt-t");
|
||
if (btt) btt.textContent = s.bed_target.toFixed(0);
|
||
|
||
// temp bars (dashboard)
|
||
var nb = document.getElementById("d-ntbar");
|
||
if (nb) nb.style.width = clamp((s.nozzle_temp / 300) * 100, 0, 100) + "%";
|
||
var bb = document.getElementById("d-btbar");
|
||
if (bb) bb.style.width = clamp((s.bed_temp / 120) * 100, 0, 100) + "%";
|
||
|
||
// progress
|
||
var pct = Math.round(s.progress * 100);
|
||
var dpct = document.getElementById("d-pct");
|
||
if (dpct) dpct.textContent = pct;
|
||
var dpbar = document.getElementById("d-pbar");
|
||
if (dpbar) dpbar.style.width = pct + "%";
|
||
|
||
var layers = s.curr_layer && s.total_layers ? "L " + s.curr_layer + " / " + s.total_layers : "–";
|
||
var dlayers = document.getElementById("d-layers");
|
||
if (dlayers) dlayers.textContent = layers;
|
||
var dzpos = document.getElementById("d-zpos");
|
||
if (dzpos) dzpos.textContent = s.z_mm > 0 ? s.z_mm.toFixed(2) + " mm" : "–";
|
||
|
||
var delapsed = document.getElementById("d-elapsed");
|
||
if (delapsed) delapsed.textContent = fmtTime(s.print_duration);
|
||
var dremain = document.getElementById("d-remain");
|
||
if (dremain) dremain.textContent = s.remain_time > 0 ? fmtTime(s.remain_time) : "–";
|
||
var dslrow = document.getElementById("d-slicer-row");
|
||
var dsltime = document.getElementById("d-slicer-time");
|
||
if (dslrow && dsltime) {
|
||
if (s.slicer_time > 0) {
|
||
dslrow.style.display = "";
|
||
dsltime.textContent = fmtTime(s.slicer_time);
|
||
} else {
|
||
dslrow.style.display = "none";
|
||
}
|
||
}
|
||
|
||
var fn = s.filename || "–";
|
||
var dfname = document.getElementById("d-fname");
|
||
if (dfname) {
|
||
dfname.textContent = fn;
|
||
dfname.title = fn;
|
||
}
|
||
var pfname = document.getElementById("p-fname");
|
||
if (pfname) {
|
||
pfname.textContent = fn;
|
||
pfname.title = fn;
|
||
}
|
||
var cfo = document.getElementById("cam-fname");
|
||
if (cfo) cfo.textContent = fn !== "–" ? fn : "";
|
||
|
||
// thumbnail
|
||
var thumb = document.getElementById("d-thumbnail");
|
||
if (thumb) {
|
||
if (s.thumbnail) {
|
||
thumb.src = "data:image/png;base64," + s.thumbnail;
|
||
thumb.style.display = "block";
|
||
} else {
|
||
thumb.style.display = "none";
|
||
thumb.src = "";
|
||
}
|
||
}
|
||
|
||
// light/fan sync
|
||
document.getElementById("d-light-toggle").checked = s.light_on;
|
||
var dfan = document.getElementById("d-fan");
|
||
if (dfan) dfan.value = s.fan_speed;
|
||
var dfanval = document.getElementById("d-fan-val");
|
||
if (dfanval) dfanval.textContent = s.fan_speed;
|
||
|
||
// speed mode buttons
|
||
var spdWidths = { 1: 25, 2: 55, 3: 90 };
|
||
[1, 2, 3].forEach(function (m) {
|
||
var b = document.getElementById("d-spd-" + m);
|
||
if (b) b.classList.toggle("spd-active", s.print_speed_mode === m);
|
||
});
|
||
var spdBar = document.getElementById("d-spd-bar");
|
||
if (spdBar) spdBar.style.width = (spdWidths[s.print_speed_mode] || 55) + "%";
|
||
|
||
var amsTitle = document.getElementById("d-card-ams");
|
||
if (amsTitle) {
|
||
var baseTitle = tr("card_ams");
|
||
var modeMap = {
|
||
toolhead: "Toolhead",
|
||
ace_direct: "ACE Direct",
|
||
ace_hub: "ACE Hub",
|
||
};
|
||
var modeTxt = modeMap[s.filament_mode] || "";
|
||
amsTitle.textContent = modeTxt ? baseTitle + " - " + modeTxt : baseTitle;
|
||
}
|
||
|
||
ensureAceDryCards();
|
||
var dry = s.ace_drying || {
|
||
status: 0,
|
||
target_temp: 0,
|
||
duration: 0,
|
||
remain_time: 0,
|
||
humidity: null,
|
||
current_temp: null,
|
||
units: [],
|
||
};
|
||
var units = dry.units || [];
|
||
var unitMap = {};
|
||
units.forEach(function (u) {
|
||
var id = Number(u.id);
|
||
if (id >= 0 && id <= 3) unitMap[id] = u;
|
||
});
|
||
var aceMode = s.filament_mode === "ace_direct" || s.filament_mode === "ace_hub";
|
||
var detected = (s.ace_units || []).filter(function (id) {
|
||
return id >= 0 && id <= 3;
|
||
});
|
||
if (!detected.length) {
|
||
Object.keys(unitMap).forEach(function (k) {
|
||
detected.push(Number(k));
|
||
});
|
||
}
|
||
if (!detected.length) {
|
||
(s.ams_slots || []).forEach(function (sl) {
|
||
var id = Number(sl.box_id);
|
||
if (id >= 0 && id <= 3 && detected.indexOf(id) < 0) detected.push(id);
|
||
});
|
||
}
|
||
detected.sort(function (a, b) {
|
||
return a - b;
|
||
});
|
||
var aceWrap = document.getElementById("d-ace-dry-wrap");
|
||
if (aceWrap) aceWrap.style.display = aceMode && detected.length ? "contents" : "none";
|
||
for (var i = 0; i < 4; i++) {
|
||
var card = document.getElementById("d-ace-dry-card-" + i);
|
||
if (!card) continue;
|
||
var show = aceMode && detected.indexOf(i) >= 0;
|
||
card.style.display = show ? "" : "none";
|
||
if (!show) continue;
|
||
var ud = unitMap[i] || dry;
|
||
var refillToggle = document.getElementById("ace-auto-refill-toggle-" + i);
|
||
var autoFeedMap = s.ace_auto_feed || {};
|
||
if (refillToggle && !_aceAutoFeedPending[i]) {
|
||
var afVal = Object.prototype.hasOwnProperty.call(autoFeedMap, String(i))
|
||
? Number(autoFeedMap[String(i)])
|
||
: _aceAutoRefillGet(i)
|
||
? 1
|
||
: 0;
|
||
refillToggle.checked = afVal === 1;
|
||
}
|
||
var dryToggle = document.getElementById("ace-dry-enable-toggle-" + i);
|
||
if (dryToggle) dryToggle.checked = Number(ud.status || 0) > 0;
|
||
var hh = document.getElementById("d-ace-dry-humidity-" + i);
|
||
if (hh) {
|
||
var hum =
|
||
ud.humidity === null || ud.humidity === undefined || ud.humidity === ""
|
||
? null
|
||
: Number(ud.humidity);
|
||
hh.textContent = hum === null || Number.isNaN(hum) ? "-" : Math.round(hum) + "%";
|
||
}
|
||
var ht = document.getElementById("d-ace-dry-current-temp-" + i);
|
||
if (ht) {
|
||
var ct =
|
||
ud.current_temp === null || ud.current_temp === undefined || ud.current_temp === ""
|
||
? null
|
||
: Number(ud.current_temp);
|
||
ht.textContent = ct === null || Number.isNaN(ct) ? "-" : ct.toFixed(1) + "°C";
|
||
}
|
||
var prof = _aceDryProfileGet(i);
|
||
var useSec =
|
||
Number(ud.status || 0) > 0 && Number(ud.remain_time) > 0
|
||
? Number(ud.remain_time || 0) * 60
|
||
: prof.duration_sec;
|
||
var showTemp =
|
||
Number(ud.status || 0) > 0 && Number(ud.target_temp) > 0 ? Number(ud.target_temp) : prof.temp;
|
||
var dryTempEl = document.getElementById("d-ace-dry-target-" + i);
|
||
if (dryTempEl) dryTempEl.textContent = showTemp + "°C";
|
||
var dryTimeEl = document.getElementById("d-ace-dry-time-" + i);
|
||
if (dryTimeEl) dryTimeEl.textContent = fmtHmsFromSec(useSec);
|
||
}
|
||
|
||
// AMS
|
||
if (s.ams_slots && s.ams_slots.length) {
|
||
window._amsSlots = s.ams_slots;
|
||
// Group by box_id (-1=Toolhead, 0=ACE 1, 1=ACE 2, ...)
|
||
var boxMap = {};
|
||
s.ams_slots.forEach(function (slot, i) {
|
||
var bid = slot.box_id != null ? slot.box_id : -1;
|
||
if (!boxMap[bid]) boxMap[bid] = [];
|
||
boxMap[bid].push({ slot: slot, arrIdx: i });
|
||
});
|
||
var boxIds = Object.keys(boxMap)
|
||
.map(Number)
|
||
.sort(function (a, b) {
|
||
return a - b;
|
||
});
|
||
var acePresent = boxIds.some(function (b) {
|
||
return b >= 0;
|
||
});
|
||
var html = "";
|
||
boxIds.forEach(function (bid) {
|
||
var entries = boxMap[bid];
|
||
var label =
|
||
bid === -1 ? (acePresent ? "Toolhead (Slots 1–3)" : "Toolhead") : "ACE " + (bid + 1);
|
||
html +=
|
||
'<div class="ams-box-group">' +
|
||
'<div class="ams-box-label">' +
|
||
label +
|
||
"</div>" +
|
||
'<div class="ams-box-slots">';
|
||
entries.forEach(function (e) {
|
||
var slot = e.slot;
|
||
var i = e.arrIdx;
|
||
var empty = slot.status !== 5;
|
||
var rgb = empty ? [80, 80, 80] : Array.isArray(slot.color) ? slot.color : [128, 128, 128];
|
||
var col = "rgb(" + rgb[0] + "," + rgb[1] + "," + rgb[2] + ")";
|
||
var globalIdx = slot.global_index != null ? slot.global_index : i;
|
||
var active = slot.status === 1 || slot.active;
|
||
var loaded =
|
||
s.ams_loaded_slot != null && s.ams_loaded_slot >= 0 && globalIdx === s.ams_loaded_slot;
|
||
var activity = slot.activity || "";
|
||
var pct = empty
|
||
? T.ams_empty
|
||
: slot.consumables_percent != null
|
||
? slot.consumables_percent + "%"
|
||
: "–";
|
||
var slotLabel = T.label_slot + " " + (globalIdx + 1);
|
||
// Gemapptes Profil nur für belegte Slots verwenden — sonst zeigt ein
|
||
// verwaistes Mapping (Slot wurde geleert) ein „Geister"-Profil (Issue #57).
|
||
var profile = empty ? null : (window._slotProfileMap || {})[globalIdx];
|
||
var genericType = slot.type || slot.material_type || "–";
|
||
// Material-Label: bei belegtem Slot mit Mapping den konkreten Profilnamen
|
||
// (z.B. „eSUN PLA+") statt nur des generischen Typs zeigen (Issue #57 Punkt 4).
|
||
var materialLabel = empty ? "–" : profile && profile.name ? profile.name : genericType;
|
||
var vendorBadge = "";
|
||
if (!empty && profile && profile.vendor) {
|
||
var tt = (profile.name || "") + (profile.id ? " (" + profile.id + ")" : "");
|
||
vendorBadge =
|
||
'<div class="slot-label" style="font-size:9px;color:var(--accent);font-weight:600;margin-top:1px" title="' +
|
||
tt +
|
||
'">' +
|
||
profile.vendor +
|
||
"</div>";
|
||
}
|
||
var spoolSel = '';
|
||
if (_spoolmanStatus.configured && !empty && _spoolmanSpools.length) {
|
||
var curSpool = _slotSpoolMap[String(globalIdx)] || '';
|
||
var spoolOpts = '<option value="">–</option>' + _spoolmanSpools.map(function (sp) {
|
||
var vendor = sp.filament && sp.filament.vendor ? sp.filament.vendor.name + ' ' : '';
|
||
var name = sp.filament ? sp.filament.name : '#' + sp.id;
|
||
var rem = sp.remaining_weight != null ? ' ' + sp.remaining_weight.toFixed(0) + 'g' : '';
|
||
return '<option value="' + sp.id + '"' + (String(sp.id) == String(curSpool) ? ' selected' : '') + '>' +
|
||
escHtml(vendor + name + rem) + '</option>';
|
||
}).join('');
|
||
spoolSel = '<div onclick="event.stopPropagation()" style="margin-top:6px;border-top:1px solid rgba(255,255,255,.07);padding-top:5px">'
|
||
+ '<div style="font-size:8px;font-weight:700;color:var(--accent);text-transform:uppercase;letter-spacing:.06em;margin-bottom:3px">🧵 Spoolman</div>'
|
||
+ '<select data-spool-slot="' + globalIdx + '" onchange="onAmsSpoolChange(this)" '
|
||
+ 'style="width:100%;padding:3px 5px;font-size:10px;border-radius:5px;border:1px solid var(--border);background:var(--card);color:var(--txt);cursor:pointer">' + spoolOpts + '</select>'
|
||
+ '</div>';
|
||
}
|
||
html += '<div class="ams-slot' + (active ? ' active' : '') + (loaded ? ' loaded' : '') + (activity ? ' ' + activity : '') + (empty ? ' empty' : '')
|
||
+ '" style="--slot-color:' + col + ';opacity:' + (empty ? 0.4 : 1) + ';cursor:pointer" onclick="openSlotEdit(' + i + ')">'
|
||
+ '<div class="slot-circle" style="background:' + col + '"></div>'
|
||
+ '<div class="slot-material" title="' + (empty ? '' : genericType) + '">' + materialLabel + '</div>'
|
||
+ vendorBadge
|
||
+ '<div class="slot-label">' + slotLabel + '</div>'
|
||
+ '<div class="slot-label" style="font-size:10px;color:var(--txt2)">' + pct + '</div>'
|
||
+ spoolSel
|
||
+ '<div style="font-size:9px;color:var(--txt2);margin-top:2px">✏</div>'
|
||
+ '</div>';
|
||
});
|
||
if (bid === -1 && acePresent) {
|
||
html +=
|
||
'<div class="ams-slot ams-slot-bridge" aria-label="' +
|
||
T.label_slot +
|
||
' 4">' +
|
||
'<div class="bridge-chip">ACE</div>' +
|
||
"</div>";
|
||
}
|
||
html += "</div></div>";
|
||
});
|
||
// Nicht rendern wenn ein Spool-Dropdown gerade offen ist (verhindert Schließen beim Poll)
|
||
var activeEl=document.activeElement;
|
||
var spoolOpen=activeEl&&activeEl.tagName==='SELECT'&&activeEl.dataset.spoolSlot!=null;
|
||
if(!spoolOpen) document.getElementById('ams-slots').innerHTML=html;
|
||
}
|
||
|
||
// camera overlay
|
||
var co = document.getElementById("cam-overlay");
|
||
if (co) co.style.display = s.print_state === "printing" && camOn ? "block" : "none";
|
||
|
||
// auto-start camera during print (unless user explicitly stopped it)
|
||
if (
|
||
s.print_state === "printing" &&
|
||
!camOn &&
|
||
s.camera_url &&
|
||
!camUserStopped &&
|
||
s.camera_on_print
|
||
) {
|
||
camStart();
|
||
}
|
||
// reset user-stopped flag when print ends so next print auto-starts again
|
||
if (s.print_state !== "printing") {
|
||
camUserStopped = false;
|
||
}
|
||
|
||
updateConnBtn();
|
||
}
|
||
|
||
function updateConnBtn() {
|
||
var btn = document.getElementById("conn-btn");
|
||
if (!btn) return;
|
||
var offline = S.kobra_state === "offline";
|
||
if (offline) {
|
||
btn.className = "conn-btn disconnected";
|
||
btn.textContent = tr("btn_connect");
|
||
} else {
|
||
btn.className = "conn-btn connected";
|
||
btn.textContent = tr("btn_disconnect");
|
||
}
|
||
}
|
||
|
||
function toggleConnection() {
|
||
var btn = document.getElementById("conn-btn");
|
||
var offline = S.kobra_state === "offline";
|
||
btn.disabled = true;
|
||
btn.textContent = "…";
|
||
var url = offline ? "/api/connect" : "/api/disconnect";
|
||
post(url, {})
|
||
.then(function (r) {
|
||
return r.json();
|
||
})
|
||
.then(function (r) {
|
||
btn.disabled = false;
|
||
if (r.error) _appendLog("Error: " + r.error);
|
||
})
|
||
.catch(function () {
|
||
btn.disabled = false;
|
||
});
|
||
}
|
||
|
||
// ── Temp history + chart ──
|
||
function updateHistory() {
|
||
tempHistory.n.push(S.nozzle_temp);
|
||
tempHistory.b.push(S.bed_temp);
|
||
if (tempHistory.n.length > 60) tempHistory.n.shift();
|
||
if (tempHistory.b.length > 60) tempHistory.b.shift();
|
||
drawChart("d-chart", tempHistory, [
|
||
{ data: tempHistory.n, color: "#00c8ff", max: 300 },
|
||
{ data: tempHistory.b, color: "#ff6b35", max: 120 },
|
||
]);
|
||
}
|
||
function drawChart(id, _, series) {
|
||
var canvas = document.getElementById(id);
|
||
if (!canvas) return;
|
||
var ctx = canvas.getContext("2d");
|
||
var W = canvas.offsetWidth * window.devicePixelRatio || canvas.width;
|
||
var H = canvas.offsetHeight * window.devicePixelRatio || canvas.height;
|
||
canvas.width = W;
|
||
canvas.height = H;
|
||
ctx.clearRect(0, 0, W, H);
|
||
series.forEach(function (s) {
|
||
var data = s.data;
|
||
if (!data.length) return;
|
||
var max = s.max;
|
||
ctx.beginPath();
|
||
ctx.strokeStyle = s.color;
|
||
ctx.lineWidth = 2;
|
||
ctx.lineJoin = "round";
|
||
data.forEach(function (v, i) {
|
||
var x = (i / Math.max(data.length - 1, 1)) * (W - 4) + 2;
|
||
var y = H - 4 - (v / max) * (H - 8);
|
||
if (i === 0) ctx.moveTo(x, y);
|
||
else ctx.lineTo(x, y);
|
||
});
|
||
ctx.stroke();
|
||
});
|
||
}
|
||
|
||
// ── Notifications ──
|
||
var _notifRows = [];
|
||
var _notifPaused = false;
|
||
var _NOTIF_EVENTS = ["started", "finished", "failed", "cancelled", "paused", "progress"];
|
||
function notifUpdateToggleBtn() {
|
||
var btn = document.getElementById("btn-notif-toggle");
|
||
var lbl = document.getElementById("lbl-notif-toggle");
|
||
if (!btn) return;
|
||
if (_notifPaused) {
|
||
if (lbl) lbl.textContent = T.settings_notif_paused || "Paused";
|
||
btn.style.background = "var(--warn, #c08a2d)";
|
||
btn.style.color = "#fff";
|
||
} else {
|
||
if (lbl) lbl.textContent = T.settings_notif_active || "Active";
|
||
btn.style.background = "var(--ok, #2e9e4f)";
|
||
btn.style.color = "#fff";
|
||
}
|
||
}
|
||
function notifTogglePaused() {
|
||
var target = !_notifPaused;
|
||
var btn = document.getElementById("btn-notif-toggle");
|
||
if (btn) btn.disabled = true;
|
||
post("/api/notifications/pause", { paused: target })
|
||
.then(function (r) {
|
||
return r.json();
|
||
})
|
||
.then(function (d) {
|
||
if (btn) btn.disabled = false;
|
||
_notifPaused = d && typeof d.paused === "boolean" ? d.paused : target;
|
||
notifUpdateToggleBtn();
|
||
})
|
||
.catch(function () {
|
||
if (btn) btn.disabled = false;
|
||
});
|
||
}
|
||
function notifRenderList(entries) {
|
||
_notifRows = entries.map(function (e) {
|
||
return {
|
||
url: e.url || "",
|
||
events: e.events || [],
|
||
include_image: !!e.include_image,
|
||
enabled: e.enabled === undefined ? true : !!e.enabled,
|
||
};
|
||
});
|
||
notifRefreshDOM();
|
||
}
|
||
function notifRefreshDOM() {
|
||
var container = document.getElementById("notif-list");
|
||
if (!container) return;
|
||
if (!_notifRows.length) {
|
||
container.innerHTML =
|
||
'<div style="font-size:11px;color:var(--txt2);padding:4px 0" id="notif-empty">' +
|
||
(T.settings_notif_empty || "No notifications configured.") +
|
||
"</div>";
|
||
return;
|
||
}
|
||
container.innerHTML = _notifRows
|
||
.map(function (row, idx) {
|
||
var evChecks = _NOTIF_EVENTS
|
||
.map(function (ev) {
|
||
var checked = row.events.indexOf(ev) >= 0 ? "checked" : "";
|
||
var lbl = T["settings_notif_ev_" + ev] || ev;
|
||
return (
|
||
'<label style="display:inline-flex;align-items:center;gap:3px;font-size:11px;cursor:pointer">' +
|
||
'<input type="checkbox" data-notif-idx="' +
|
||
idx +
|
||
'" data-notif-ev="' +
|
||
ev +
|
||
'" ' +
|
||
checked +
|
||
' onchange="notifToggleEvent(' +
|
||
idx +
|
||
",'" +
|
||
ev +
|
||
'\')" style="width:auto;margin:0"> ' +
|
||
lbl +
|
||
"</label>"
|
||
);
|
||
})
|
||
.join(" ");
|
||
var imgCheck =
|
||
'<label style="display:inline-flex;align-items:center;gap:3px;font-size:11px;cursor:pointer;margin-left:4px;padding-left:8px;border-left:1px solid var(--border)" title="' +
|
||
(T.settings_notif_send_image || "Send image") +
|
||
'">' +
|
||
'<input type="checkbox" ' +
|
||
(row.include_image ? "checked" : "") +
|
||
' onchange="notifToggleImage(' +
|
||
idx +
|
||
')" style="width:auto;margin:0"> ' +
|
||
"📷 " +
|
||
(T.settings_notif_send_image || "Image") +
|
||
"</label>";
|
||
return (
|
||
'<div style="background:var(--raised);border-radius:6px;padding:8px;margin-bottom:6px' +
|
||
(row.enabled ? "" : ";opacity:0.5") +
|
||
'">' +
|
||
'<div style="display:flex;gap:6px;align-items:center;margin-bottom:6px">' +
|
||
'<button onclick="notifToggleEnabled(' +
|
||
idx +
|
||
')" title="' +
|
||
(row.enabled ? T.settings_notif_active || "Active" : T.settings_notif_paused || "Paused") +
|
||
'" style="background:' +
|
||
(row.enabled ? "var(--ok,#2e9e4f)" : "var(--warn,#c08a2d)") +
|
||
';border:none;color:#fff;border-radius:4px;cursor:pointer;padding:2px 8px;font-size:11px;white-space:nowrap">' +
|
||
(row.enabled ? T.settings_notif_active || "Active" : T.settings_notif_paused || "Paused") +
|
||
"</button>" +
|
||
'<input type="text" value="' +
|
||
_escHtml(row.url) +
|
||
'" placeholder="discord://… telegram://… gotify://…"' +
|
||
' oninput="notifSetUrl(' +
|
||
idx +
|
||
',this.value)" style="flex:1;font-family:var(--mono);font-size:11px">' +
|
||
'<button onclick="notifTest(' +
|
||
idx +
|
||
')" style="background:var(--raised2,var(--raised));border:1px solid var(--border);color:var(--txt);border-radius:4px;cursor:pointer;padding:2px 8px;font-size:11px" id="notif-test-btn-' +
|
||
idx +
|
||
'">' +
|
||
(T.settings_notif_test || "Test") +
|
||
"</button>" +
|
||
'<button onclick="notifRemoveRow(' +
|
||
idx +
|
||
')" style="background:none;border:none;color:var(--err);cursor:pointer;font-size:16px;line-height:1" title="remove">✕</button>' +
|
||
"</div>" +
|
||
'<div style="display:flex;flex-wrap:wrap;gap:8px;align-items:center">' +
|
||
evChecks +
|
||
imgCheck +
|
||
"</div>" +
|
||
'<div id="notif-test-status-' +
|
||
idx +
|
||
'" style="font-size:11px;margin-top:4px"></div>' +
|
||
"</div>"
|
||
);
|
||
})
|
||
.join("");
|
||
}
|
||
function _escHtml(s) {
|
||
return (s || "")
|
||
.replace(/&/g, "&")
|
||
.replace(/</g, "<")
|
||
.replace(/>/g, ">")
|
||
.replace(/"/g, """);
|
||
}
|
||
function notifAddRow() {
|
||
_notifRows.push({
|
||
url: "",
|
||
events: ["finished", "failed"],
|
||
include_image: false,
|
||
enabled: true,
|
||
});
|
||
notifRefreshDOM();
|
||
}
|
||
function notifRemoveRow(idx) {
|
||
_notifRows.splice(idx, 1);
|
||
notifRefreshDOM();
|
||
}
|
||
function notifSetUrl(idx, val) {
|
||
if (_notifRows[idx]) _notifRows[idx].url = val;
|
||
}
|
||
function notifToggleEvent(idx, ev) {
|
||
if (!_notifRows[idx]) return;
|
||
var evs = _notifRows[idx].events;
|
||
var pos = evs.indexOf(ev);
|
||
if (pos >= 0) evs.splice(pos, 1);
|
||
else evs.push(ev);
|
||
}
|
||
function notifToggleImage(idx) {
|
||
if (_notifRows[idx]) _notifRows[idx].include_image = !_notifRows[idx].include_image;
|
||
}
|
||
function notifToggleEnabled(idx) {
|
||
if (!_notifRows[idx]) return;
|
||
_notifRows[idx].enabled = !_notifRows[idx].enabled;
|
||
notifRefreshDOM();
|
||
}
|
||
function notifCollect() {
|
||
return _notifRows
|
||
.filter(function (r) {
|
||
return r.url.trim();
|
||
})
|
||
.map(function (r) {
|
||
return {
|
||
url: r.url.trim(),
|
||
events: r.events,
|
||
include_image: !!r.include_image,
|
||
enabled: r.enabled === undefined ? true : !!r.enabled,
|
||
};
|
||
});
|
||
}
|
||
function notifTest(idx) {
|
||
var row = _notifRows[idx];
|
||
if (!row || !row.url.trim()) return;
|
||
var btn = document.getElementById("notif-test-btn-" + idx);
|
||
var status = document.getElementById("notif-test-status-" + idx);
|
||
if (btn) btn.disabled = true;
|
||
if (status) {
|
||
status.textContent = "…";
|
||
status.style.color = "var(--txt2)";
|
||
}
|
||
post("/api/notifications/test", { url: row.url.trim() })
|
||
.then(function (r) {
|
||
return r.json();
|
||
})
|
||
.then(function (d) {
|
||
if (btn) btn.disabled = false;
|
||
if (status) {
|
||
if (d && d.status === "ok") {
|
||
status.textContent = "✓ " + (T.settings_notif_test_ok || "Sent");
|
||
status.style.color = "var(--ok)";
|
||
} else {
|
||
status.textContent =
|
||
"✗ " + (d && d.message ? d.message : T.settings_notif_test_fail || "Failed");
|
||
status.style.color = "var(--err)";
|
||
}
|
||
}
|
||
})
|
||
.catch(function (e) {
|
||
if (btn) btn.disabled = false;
|
||
if (status) {
|
||
status.textContent = "✗ " + e;
|
||
status.style.color = "var(--err)";
|
||
}
|
||
});
|
||
}
|
||
|
||
// ── Settings Modal ──
|
||
var _updateTag = "";
|
||
var _updateUrl = "";
|
||
function openSettings() {
|
||
fetch(_apiUrl("/api/settings"))
|
||
.then(function (r) {
|
||
return r.json();
|
||
})
|
||
.then(function (d) {
|
||
document.getElementById("s-printer-name").value = d.printer_name || "";
|
||
document.getElementById("s-printer-ip").value = d.printer_ip || "";
|
||
document.getElementById("s-mqtt-port").value = d.mqtt_port || 9883;
|
||
document.getElementById("s-username").value = d.username || "";
|
||
document.getElementById("s-password").value = d.password || "";
|
||
document.getElementById("s-device-id").value = d.device_id || "";
|
||
document.getElementById("s-mode-id").value = d.mode_id || "";
|
||
document.getElementById("s-default-slot").value = d.default_ams_slot || "auto";
|
||
document.getElementById("s-auto-leveling").checked =
|
||
d.auto_leveling === undefined ? true : !!d.auto_leveling;
|
||
var vc=document.getElementById('s-vibration-compensation');if(vc)vc.checked=!!d.vibration_compensation;
|
||
var cop = document.getElementById("s-camera-on-print");
|
||
if (cop) cop.checked = !!d.camera_on_print;
|
||
var frm = document.getElementById("s-file-ready-mode");
|
||
if (frm)
|
||
frm.value = d.print_start_dialog === undefined ? "1" : String(d.print_start_dialog ? 1 : 0);
|
||
var wuw = document.getElementById("s-web-upload-warning");
|
||
if (wuw) wuw.checked = d.web_upload_warning === undefined ? true : !!d.web_upload_warning;
|
||
notifRenderList(d.notifications || []);
|
||
_notifPaused = !!d.notifications_paused;
|
||
notifUpdateToggleBtn();
|
||
var enm = document.getElementById("s-notif-every-min");
|
||
if (enm) enm.value = d.notify_every_minutes || 0;
|
||
var enl = document.getElementById("s-notif-every-layers");
|
||
if (enl) enl.value = d.notify_every_layers || 0;
|
||
// Poll-Intervall (Sekunden) — Backend hat Vorrang vor localStorage
|
||
var pi = document.getElementById("s-poll-interval");
|
||
if (pi) {
|
||
var sec =
|
||
d.poll_interval ||
|
||
Math.round(parseInt(localStorage.getItem("pollInterval") || "2000") / 1000) ||
|
||
3;
|
||
pi.value = sec;
|
||
}
|
||
var vhl=document.getElementById('s-verbose-http-log');if(vhl)vhl.checked=!!d.verbose_http_log;
|
||
renderFilamentMapping(d.filament_profiles || {});
|
||
renderSpoolmanSlotCard();
|
||
// Spoolman
|
||
var su = document.getElementById("s-spoolman-url");
|
||
if (su) su.value = d.spoolman_server || "";
|
||
var sr = document.getElementById("s-spoolman-sync-rate");
|
||
if (sr) sr.value = d.spoolman_sync_rate !== undefined ? d.spoolman_sync_rate : 30;
|
||
_updateSpoolmanStatusDot();
|
||
});
|
||
// Sprach-Select im Settings-Panel mit aktueller Sprache spiegeln
|
||
var ls = document.getElementById("s-lang-select");
|
||
if (ls) ls.value = localStorage.getItem("lang") || document.documentElement.lang || "de";
|
||
var appVersion = "__VERSION__";
|
||
document.getElementById("s-version-label").textContent = "v" + (appVersion || "?");
|
||
document.getElementById("update-status").textContent = "";
|
||
document.getElementById("btn-update-apply").style.display = "none";
|
||
var cl = document.getElementById("update-changelog");
|
||
if (cl) cl.style.display = "none";
|
||
_updateTag = "";
|
||
_updateUrl = "";
|
||
// Custom-Profile-Liste laden (Issue #41)
|
||
refreshUserProfileList();
|
||
// Vendor-Sichtbarkeitsfilter (Issue #41 Option A)
|
||
loadVendorChecklist();
|
||
}
|
||
function closeSettings() {
|
||
// Panel-Variante: zurück zum Dashboard
|
||
showPanel("dashboard");
|
||
}
|
||
|
||
// Poll-Intervall-Feld → Live-Poll sofort übernehmen (Persistenz erst beim Speichern)
|
||
function onPollIntervalInput() {
|
||
var pi = document.getElementById("s-poll-interval");
|
||
if (!pi) return;
|
||
var sec = parseInt(pi.value, 10);
|
||
if (sec >= 1 && sec <= 60) setPoll(sec * 1000);
|
||
}
|
||
|
||
// ── Filament-Profil-Mapping pro Slot ([filament_profiles]) ──
|
||
// Pro Slot ein einzelnes Profil-Dropdown (vendor+name gemeinsam, gekeyt per
|
||
// _profileKey). Kein Freitext mehr → das (vendor,name)→id-Matching kann nicht
|
||
// mehr durch manuelle Eingabe brechen (Issue #57 Punkt 1). Optionen werden aus
|
||
// /kx/filament/profiles geladen, nach Vendor gruppiert, User-Profile zuerst,
|
||
// mit demselben Vendor-Sichtbarkeitsfilter wie das Slot-Edit-Dropdown.
|
||
function renderFilamentMapping(map) {
|
||
var el = document.getElementById("filament-mapping-list");
|
||
if (!el) return;
|
||
var rows = "";
|
||
for (var i = 0; i < 4; i++) {
|
||
var m = map[i] || map[String(i)] || {};
|
||
var idHint = m.id
|
||
? ' <span style="color:var(--txt2);font-size:11px">(' + m.id + ")</span>"
|
||
: "";
|
||
rows +=
|
||
'<div class="modal-field" style="margin-bottom:8px">' +
|
||
"<label>Slot " +
|
||
(i + 1) +
|
||
idHint +
|
||
"</label>" +
|
||
'<select id="fmap-' +
|
||
i +
|
||
'" data-vendor="' +
|
||
(m.vendor || "") +
|
||
'" data-name="' +
|
||
(m.name || "") +
|
||
'" style="width:100%"></select>' +
|
||
"</div>";
|
||
}
|
||
el.innerHTML = rows;
|
||
// Dropdowns befüllen (async, geteilter Profil-Cache + Vendor-Filter)
|
||
for (var j = 0; j < 4; j++) {
|
||
_fillMappingDropdown(j);
|
||
}
|
||
}
|
||
function _fillMappingDropdown(slot) {
|
||
var sel = document.getElementById("fmap-" + slot);
|
||
if (!sel) return;
|
||
var wantKey = _profileKey(sel.dataset.vendor, sel.dataset.name);
|
||
_loadOrcaFilaments(function (profiles) {
|
||
sel.innerHTML =
|
||
'<option value="">' + (tr("slot_edit_profile_default") || "Generic (Standard)") + "</option>";
|
||
var userProfs = profiles.filter(function (p) {
|
||
return p.is_user;
|
||
});
|
||
var systemProfs = profiles.filter(function (p) {
|
||
return !p.is_user;
|
||
});
|
||
function _opt(g, p) {
|
||
var o = document.createElement("option");
|
||
o.value = _profileKey(p.vendor, p.name);
|
||
o.dataset.vendor = p.vendor;
|
||
o.dataset.name = p.name;
|
||
o.dataset.id = p.id || "";
|
||
o.textContent = (p.is_user ? "★ " : "") + p.name + (p.vendor ? " — " + p.vendor : "");
|
||
if (o.value === wantKey) o.selected = true;
|
||
g.appendChild(o);
|
||
}
|
||
if (userProfs.length) {
|
||
var gUser = document.createElement("optgroup");
|
||
gUser.label = "★ " + (tr("orca_profile_user_label") || "Eigene Profile");
|
||
userProfs.forEach(function (p) {
|
||
_opt(gUser, p);
|
||
});
|
||
sel.appendChild(gUser);
|
||
}
|
||
_loadVisibleVendors(function (vis) {
|
||
var filtered = systemProfs;
|
||
if (vis && vis.length) {
|
||
var allow = {};
|
||
vis.forEach(function (v) {
|
||
allow[v] = 1;
|
||
});
|
||
allow["Generic"] = 1;
|
||
filtered = systemProfs.filter(function (p) {
|
||
return allow[p.vendor];
|
||
});
|
||
}
|
||
var byVendor = {};
|
||
filtered.forEach(function (p) {
|
||
(byVendor[p.vendor] = byVendor[p.vendor] || []).push(p);
|
||
});
|
||
Object.keys(byVendor)
|
||
.sort()
|
||
.forEach(function (v) {
|
||
var g = document.createElement("optgroup");
|
||
g.label = v;
|
||
byVendor[v].forEach(function (p) {
|
||
_opt(g, p);
|
||
});
|
||
sel.appendChild(g);
|
||
});
|
||
});
|
||
});
|
||
}
|
||
function saveFilamentMapping() {
|
||
// Nutzt den per-Slot-Endpoint (vendor,name → ID-Lookup im Backend).
|
||
// Leere Auswahl ("") = Mapping entfernen.
|
||
var chain = Promise.resolve();
|
||
for (var i = 0; i < 4; i++) {
|
||
(function (slot) {
|
||
var sel = document.getElementById("fmap-" + slot);
|
||
var opt = sel ? sel.options[sel.selectedIndex] : null;
|
||
var vendor = (opt && opt.dataset.vendor) || "";
|
||
var name = (opt && opt.dataset.name) || "";
|
||
chain = chain.then(function () {
|
||
return fetch(_apiUrl("/kx/filament/slots/" + slot + "/profile"), {
|
||
method: "POST",
|
||
headers: { "Content-Type": "application/json" },
|
||
body: JSON.stringify({ vendor: vendor, name: name }),
|
||
});
|
||
});
|
||
})(i);
|
||
}
|
||
chain
|
||
.then(function () {
|
||
clog(tr("log_filament_mapping_saved") || "Filament-Mapping gespeichert", "msg-ok");
|
||
openSettings(); // neu laden → ID-Hints aktualisieren
|
||
})
|
||
.catch(function (e) {
|
||
clog("Mapping-Fehler: " + e, "msg-err");
|
||
});
|
||
}
|
||
|
||
// ── Vendor-Sichtbarkeitsfilter (Issue #41 Option A) ──
|
||
var _vendorChecklistSel = {}; // {vendor:true} — laufende Auswahl im UI
|
||
function loadVendorChecklist() {
|
||
// aktuelle Auswahl aus Backend, dann alle verfügbaren Vendoren rendern
|
||
_visibleVendors = null; // Cache invalidieren
|
||
_loadVisibleVendors(function (vis) {
|
||
_vendorChecklistSel = {};
|
||
(vis || []).forEach(function (v) {
|
||
_vendorChecklistSel[v] = true;
|
||
});
|
||
renderVendorChecklist();
|
||
});
|
||
}
|
||
function renderVendorChecklist() {
|
||
var el = document.getElementById("visible-vendors-list");
|
||
if (!el) return;
|
||
_loadOrcaFilaments(function (profiles) {
|
||
// alle System-Vendoren (ohne Generic — der ist immer sichtbar) sammeln
|
||
var set = {};
|
||
profiles.forEach(function (p) {
|
||
if (!p.is_user && p.vendor && p.vendor !== "Generic") set[p.vendor] = 1;
|
||
});
|
||
var vendors = Object.keys(set).sort();
|
||
var q = ((document.getElementById("vendor-filter-search") || {}).value || "").toLowerCase();
|
||
if (q)
|
||
vendors = vendors.filter(function (v) {
|
||
return v.toLowerCase().indexOf(q) >= 0;
|
||
});
|
||
el.innerHTML =
|
||
vendors
|
||
.map(function (v) {
|
||
var ck = _vendorChecklistSel[v] ? "checked" : "";
|
||
var safe = v.replace(/"/g, """);
|
||
return (
|
||
'<label style="display:flex;align-items:center;gap:8px;padding:3px 0;cursor:pointer;font-size:13px">' +
|
||
'<input type="checkbox" data-vendor="' +
|
||
safe +
|
||
'" ' +
|
||
ck +
|
||
' onchange="_vendorCheck(this)" style="width:auto;margin:0"> ' +
|
||
v +
|
||
"</label>"
|
||
);
|
||
})
|
||
.join("") || '<i style="color:var(--txt2)">–</i>';
|
||
});
|
||
}
|
||
function _vendorCheck(cb) {
|
||
var v = cb.getAttribute("data-vendor");
|
||
if (cb.checked) _vendorChecklistSel[v] = true;
|
||
else delete _vendorChecklistSel[v];
|
||
}
|
||
function renderSpoolmanSlotCard() {
|
||
var card = document.getElementById('spoolman-slot-card');
|
||
var rows = document.getElementById('spoolman-slot-rows');
|
||
if (!card || !rows) return;
|
||
if (!_spoolmanStatus.configured) { card.style.display = 'none'; return; }
|
||
card.style.display = '';
|
||
Promise.all([
|
||
fetch(_apiUrl('/kx/spoolman/spools')).then(function (r) { return r.json(); }),
|
||
fetch(_apiUrl('/kx/filament/slots')).then(function (r) { return r.json(); })
|
||
]).then(function (res) {
|
||
var spools = res[0].spools || [];
|
||
var slots = (res[1].result || []).sort(function (a, b) { return a.slot_index - b.slot_index; });
|
||
if (!slots.length) { rows.innerHTML = '<span style="font-size:11px;color:var(--txt2)">Keine AMS-Slots bekannt.</span>'; return; }
|
||
rows.innerHTML = slots.map(function (slot) {
|
||
var idx = parseInt(slot.slot_index);
|
||
var col = slot.color_hex || '#888';
|
||
var mat = slot.material || '';
|
||
var current = _slotSpoolMap[String(idx)] || '';
|
||
var opts = '<option value="">–</option>' + spools.map(function (sp) {
|
||
var rem = sp.remaining_weight != null ? ' (' + sp.remaining_weight.toFixed(0) + 'g)' : '';
|
||
var vendor = sp.filament && sp.filament.vendor ? sp.filament.vendor.name + ' ' : '';
|
||
var name = sp.filament ? sp.filament.name : 'Spool #' + sp.id;
|
||
var mat2 = sp.filament && sp.filament.material ? ' · ' + sp.filament.material : '';
|
||
return '<option value="' + sp.id + '"' + (String(sp.id) == String(current) ? ' selected' : '') + '>' +
|
||
escHtml('#' + sp.id + ' ' + vendor + name + mat2 + rem) + '</option>';
|
||
}).join('');
|
||
return '<div style="display:flex;align-items:center;gap:8px;font-size:12px">' +
|
||
'<span style="display:inline-block;width:14px;height:14px;border-radius:50%;background:' + col + ';border:1px solid var(--border);flex-shrink:0"></span>' +
|
||
'<span style="color:var(--txt2);min-width:60px">Slot ' + (idx + 1) + ' <span style="color:var(--txt2);font-size:10px">' + escHtml(mat) + '</span></span>' +
|
||
'<select data-spool-slot="' + idx + '" style="flex:1;padding:3px 6px;border-radius:6px;border:1px solid var(--border);background:var(--raised);color:var(--txt);font-size:12px">' + opts + '</select></div>';
|
||
}).join('');
|
||
}).catch(function () { rows.innerHTML = '<span style="font-size:11px;color:var(--err)">Spoolman nicht erreichbar</span>'; });
|
||
}
|
||
|
||
function onAmsSpoolChange(sel) {
|
||
var idx = sel.getAttribute('data-spool-slot');
|
||
var val = sel.value;
|
||
if (val) _slotSpoolMap[String(idx)] = parseInt(val);
|
||
else delete _slotSpoolMap[String(idx)];
|
||
var mapping = {};
|
||
Object.keys(_slotSpoolMap).forEach(function (k) { mapping[k] = _slotSpoolMap[k]; });
|
||
fetch(_apiUrl('/kx/spoolman/active-spool'), {
|
||
method: 'POST',
|
||
headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ slot_spools: mapping })
|
||
});
|
||
}
|
||
|
||
function saveSpoolmanSlots() {
|
||
var mapping = {};
|
||
document.querySelectorAll('#spoolman-slot-rows select[data-spool-slot]').forEach(function (sel) {
|
||
var idx = sel.getAttribute('data-spool-slot');
|
||
var val = sel.value;
|
||
if (val) mapping[idx] = parseInt(val);
|
||
});
|
||
fetch(_apiUrl('/kx/spoolman/active-spool'), {
|
||
method: 'POST', headers: { 'Content-Type': 'application/json' },
|
||
body: JSON.stringify({ slot_spools: mapping })
|
||
}).then(function (r) { return r.json(); }).then(function (d) {
|
||
_slotSpoolMap = d.slot_spools || {};
|
||
});
|
||
}
|
||
|
||
function saveVisibleVendors() {
|
||
var vendors = Object.keys(_vendorChecklistSel);
|
||
fetch(_apiUrl('/kx/filament/visible_vendors'), {
|
||
method: 'POST', headers: { 'Content-Type': 'application/json' },
|
||
body: JSON.stringify({ vendors: vendors })
|
||
}).then(function (r) { return r.json(); }).then(function () {
|
||
_visibleVendors = vendors.slice(); // Dropdown-Cache aktualisieren
|
||
clog(tr('log_visible_vendors_saved') || 'Hersteller-Auswahl gespeichert', 'msg-ok');
|
||
}).catch(function (e) { clog('Vendor-Filter-Fehler: ' + e, 'msg-err'); });
|
||
}
|
||
|
||
// ── Custom Filament Profile Import (Issue #41) ──
|
||
function refreshUserProfileList() {
|
||
var listEl = document.getElementById("orca-profiles-list");
|
||
if (!listEl) return;
|
||
fetch(_apiUrl("/kx/filament/profiles/user"))
|
||
.then(function (r) {
|
||
return r.json();
|
||
})
|
||
.then(function (d) {
|
||
var profs = (d && d.result) || [];
|
||
if (!profs.length) {
|
||
listEl.innerHTML =
|
||
'<i style="color:var(--txt2)">' + (tr("orca_profile_user_empty") || "– keine –") + "</i>";
|
||
return;
|
||
}
|
||
listEl.innerHTML = profs
|
||
.map(function (p) {
|
||
var label = p.vendor + " / " + p.name + " (" + p.type + ")";
|
||
return (
|
||
'<div style="display:flex;justify-content:space-between;align-items:center;padding:3px 0;border-bottom:1px solid var(--border)">' +
|
||
"<span>★ " +
|
||
label +
|
||
"</span>" +
|
||
"<button onclick=\"deleteUserProfile('" +
|
||
encodeURIComponent(p.vendor) +
|
||
"','" +
|
||
encodeURIComponent(p.name) +
|
||
"')\" " +
|
||
'style="background:none;border:none;color:var(--err);cursor:pointer;font-size:14px" title="' +
|
||
tr("btn_delete", "Löschen") +
|
||
'">🗑</button>' +
|
||
"</div>"
|
||
);
|
||
})
|
||
.join("");
|
||
})
|
||
.catch(function () { });
|
||
}
|
||
function deleteUserProfile(vendor, name) {
|
||
fetch(_apiUrl("/kx/filament/profiles/user?vendor=" + vendor + "&name=" + name), {
|
||
method: "DELETE",
|
||
})
|
||
.then(function (r) {
|
||
return r.json();
|
||
})
|
||
.then(function () {
|
||
_orcaFilamentCache = null;
|
||
refreshUserProfileList();
|
||
// Falls Import-Dialog offen ist, dort auch refreshen
|
||
refreshImportDialogList();
|
||
});
|
||
}
|
||
function openProfileImport() {
|
||
document.getElementById("profile-import-status").textContent = "";
|
||
refreshImportDialogList();
|
||
document.getElementById("profile-import-modal").classList.add("open");
|
||
}
|
||
function closeProfileImport() {
|
||
document.getElementById("profile-import-modal").classList.remove("open");
|
||
}
|
||
function refreshImportDialogList() {
|
||
var el = document.getElementById("profile-import-list");
|
||
if (!el) return;
|
||
fetch(_apiUrl("/kx/filament/profiles/user"))
|
||
.then(function (r) {
|
||
return r.json();
|
||
})
|
||
.then(function (d) {
|
||
var profs = (d && d.result) || [];
|
||
if (!profs.length) {
|
||
el.innerHTML =
|
||
'<i style="color:var(--txt2)">' + (tr("orca_profile_user_empty") || "– keine –") + "</i>";
|
||
return;
|
||
}
|
||
el.innerHTML = profs
|
||
.map(function (p) {
|
||
var label = p.vendor + " / " + p.name + " (" + p.type + ")";
|
||
return (
|
||
'<div style="display:flex;justify-content:space-between;align-items:center;padding:4px 6px;border-bottom:1px solid var(--border)">' +
|
||
"<span>★ " +
|
||
label +
|
||
"</span>" +
|
||
"<button onclick=\"deleteUserProfile('" +
|
||
encodeURIComponent(p.vendor) +
|
||
"','" +
|
||
encodeURIComponent(p.name) +
|
||
"')\" " +
|
||
'style="background:none;border:none;color:var(--err);cursor:pointer;font-size:14px" title="' +
|
||
tr("btn_delete", "Löschen") +
|
||
'">🗑</button>' +
|
||
"</div>"
|
||
);
|
||
})
|
||
.join("");
|
||
})
|
||
.catch(function () { });
|
||
}
|
||
function doProfileImportUpload(files) {
|
||
if (!files || !files.length) return;
|
||
var status = document.getElementById("profile-import-status");
|
||
status.textContent = tr("orca_profile_uploading") || "Lade hoch…";
|
||
status.style.color = "var(--txt2)";
|
||
var done = 0,
|
||
totalAdded = 0,
|
||
totalSkipped = 0;
|
||
function _one(idx) {
|
||
if (idx >= files.length) {
|
||
status.textContent =
|
||
(tr("orca_profile_done") || "Importiert") +
|
||
": " +
|
||
totalAdded +
|
||
(totalSkipped
|
||
? " / " + totalSkipped + " " + (tr("orca_profile_skipped") || "übersprungen")
|
||
: "");
|
||
status.style.color = "var(--ok)";
|
||
_orcaFilamentCache = null;
|
||
refreshImportDialogList();
|
||
refreshUserProfileList();
|
||
// Vendor-Checkliste neu aufbauen — ein Import kann einen bisher
|
||
// unbekannten System-Vendor mitbringen (Issue #41).
|
||
if (document.getElementById("visible-vendors-list")) renderVendorChecklist();
|
||
// Wenn Slot-Edit offen ist, Dropdown gleich neu befüllen
|
||
var mat = document.getElementById("slot-edit-mat");
|
||
if (mat && document.getElementById("slot-edit-modal").classList.contains("open")) {
|
||
_fillSlotProfileDropdown(mat.value, "", "");
|
||
}
|
||
return;
|
||
}
|
||
var fd = new FormData();
|
||
fd.append("file", files[idx]);
|
||
fetch(_apiUrl("/kx/filament/profiles/user"), { method: "POST", body: fd })
|
||
.then(function (r) {
|
||
return r.json();
|
||
})
|
||
.then(function (d) {
|
||
totalAdded += d.added || 0;
|
||
totalSkipped += d.skipped || 0;
|
||
done++;
|
||
_one(idx + 1);
|
||
})
|
||
.catch(function (e) {
|
||
status.textContent = tr("log_error", "Fehler:") + " " + e;
|
||
status.style.color = "var(--err)";
|
||
});
|
||
}
|
||
_one(0);
|
||
}
|
||
|
||
// ── AMS Slot Edit ──
|
||
var _slotEditIndex=-1;
|
||
var _slotEditLoaded=false;
|
||
var _MAT_PRESETS=['PLA','PLA+','PLA SILK','PLA MATTE','PETG','ABS','ASA','TPU','PA','PC','HIPS'];
|
||
function _normalizeMat(m){
|
||
var s=m.toUpperCase().trim().replace(/-/g,' ').replace(/_/g,' ');
|
||
var aliases={'PLAPLUS':'PLA+','PLA PLUS':'PLA+','SILK PLA':'PLA SILK','PLASILK':'PLA SILK',
|
||
'PLA MATTE':'PLA','PLA MARBLE':'PLA','PLA WOOD':'PLA','TPE':'TPU',
|
||
'PETG PLUS':'PETG+','PA6':'PA','PA12':'PA','PA66':'PA'};
|
||
return aliases[s]||s;
|
||
}
|
||
var _BASE_MATERIAL_TYPES=['PLA','PETG','ABS','ASA','TPU','TPE','PA','PC','HIPS','PEI','PEEK'];
|
||
function updateSlotEditFeedButton(){
|
||
var btn=document.getElementById('btn-slot-edit-feed');
|
||
if(!btn)return;
|
||
if(_slotEditIndex<0){
|
||
btn.style.display='none';
|
||
return;
|
||
}
|
||
btn.style.display = "";
|
||
btn.textContent = _slotEditLoaded ? tr("slot_edit_unload") : tr("slot_edit_load");
|
||
}
|
||
var _orcaFilamentCache = null; // [{id,name,vendor,type,color}, …]
|
||
var _visibleVendors = null; // Vendor-Sichtbarkeitsfilter (Issue #41); [] = alle
|
||
function _loadVisibleVendors(cb) {
|
||
if (_visibleVendors !== null) {
|
||
cb(_visibleVendors);
|
||
return;
|
||
}
|
||
fetch(_apiUrl("/kx/filament/visible_vendors"))
|
||
.then(function (r) {
|
||
return r.json();
|
||
})
|
||
.then(function (d) {
|
||
_visibleVendors = d.result || [];
|
||
cb(_visibleVendors);
|
||
})
|
||
.catch(function () {
|
||
_visibleVendors = [];
|
||
cb([]);
|
||
});
|
||
}
|
||
function _loadOrcaFilaments(cb) {
|
||
if (_orcaFilamentCache) {
|
||
cb(_orcaFilamentCache);
|
||
return;
|
||
}
|
||
fetch(_apiUrl("/kx/filament/profiles"))
|
||
.then(function (r) {
|
||
return r.json();
|
||
})
|
||
.then(function (d) {
|
||
_orcaFilamentCache = d.result || [];
|
||
cb(_orcaFilamentCache);
|
||
})
|
||
.catch(function () {
|
||
cb([]);
|
||
});
|
||
}
|
||
function _profileKey(vendor, name) {
|
||
// Eindeutiger Selector: (vendor, name). IDs aus orca_filaments.json sind
|
||
// NICHT eindeutig (z.B. 136 Profile mit OGFL99). Wir kodieren beide in den
|
||
// <option>-Value-String mit | als Trenner.
|
||
return (vendor || "") + "|" + (name || "");
|
||
}
|
||
function _fillSlotProfileDropdown(material, currentVendor, currentName) {
|
||
var sel = document.getElementById("slot-edit-profile");
|
||
if (!sel) return;
|
||
var wantKey = _profileKey(currentVendor, currentName);
|
||
_loadOrcaFilaments(function (profiles) {
|
||
// Type-Filter: nur Profile vom passenden material zeigen (z.B. PLA → alle PLA-Varianten)
|
||
var matU=(material||'').toUpperCase().trim();
|
||
// PLA-Varianten: Drucker meldet "PLA SILK"/"PLA+"/"PLA MATTE", OrcaSlicer
|
||
// speichert alle unter type=PLA — Namens-Keyword als Zusatzfilter.
|
||
var _PLA_VARIANT_KW={'PLA SILK':'silk','PLA+':'pla+','PLA MATTE':'matte',
|
||
'PLA MARBLE':'marble','PLA WOOD':'wood'};
|
||
var variantKw=_PLA_VARIANT_KW[matU]||null;
|
||
var baseMat=variantKw?'PLA':matU;
|
||
var matched=profiles.filter(function(p){
|
||
var pt=(p.type||'').toUpperCase();
|
||
var nameL=(p.name||'').toLowerCase();
|
||
// Basis-Typ muss passen
|
||
var typeOk=baseMat===''||pt===baseMat||pt.startsWith(baseMat+'-')||pt.startsWith(baseMat+' ');
|
||
if(!typeOk) return false;
|
||
// Bei Variante: Name muss Keyword enthalten (z.B. "silk", "matte", "pla+")
|
||
if(variantKw) return nameL.indexOf(variantKw)!==-1;
|
||
return true;
|
||
});
|
||
sel.innerHTML = '<option value="">' + tr("slot_edit_profile_default") + "</option>";
|
||
// User-Profile (is_user) zuerst — eigene Optgroup '★ Eigene' an erster Stelle.
|
||
var userProfs = matched.filter(function (p) {
|
||
return p.is_user;
|
||
});
|
||
var systemProfs = matched.filter(function (p) {
|
||
return !p.is_user;
|
||
});
|
||
function _appendOption(g, p) {
|
||
var o = document.createElement("option");
|
||
o.value = _profileKey(p.vendor, p.name);
|
||
o.dataset.vendor = p.vendor;
|
||
o.dataset.name = p.name;
|
||
o.dataset.id = p.id || "";
|
||
o.textContent = (p.is_user ? "★ " : "") + p.name;
|
||
if (o.value === wantKey) o.selected = true;
|
||
g.appendChild(o);
|
||
}
|
||
if (userProfs.length) {
|
||
var gUser = document.createElement("optgroup");
|
||
gUser.label = "★ " + (tr("orca_profile_user_label") || "Eigene Profile");
|
||
userProfs.forEach(function (p) {
|
||
_appendOption(gUser, p);
|
||
});
|
||
sel.appendChild(gUser);
|
||
}
|
||
// Vendor-Sichtbarkeitsfilter (Issue #41 Option A): nur gewählte Vendoren +
|
||
// Generic. Leere Liste = alle (rückwärtskompatibel). Eigene Profile (is_user)
|
||
// sind oben bereits unkonditional drin.
|
||
_loadVisibleVendors(function (vis) {
|
||
var filtered = systemProfs;
|
||
if (vis && vis.length) {
|
||
var allow = {};
|
||
vis.forEach(function (v) {
|
||
allow[v] = 1;
|
||
});
|
||
allow["Generic"] = 1;
|
||
filtered = systemProfs.filter(function (p) {
|
||
return allow[p.vendor];
|
||
});
|
||
}
|
||
var byVendor = {};
|
||
filtered.forEach(function (p) {
|
||
(byVendor[p.vendor] = byVendor[p.vendor] || []).push(p);
|
||
});
|
||
Object.keys(byVendor)
|
||
.sort()
|
||
.forEach(function (v) {
|
||
var g = document.createElement("optgroup");
|
||
g.label = v;
|
||
byVendor[v].forEach(function (p) {
|
||
_appendOption(g, p);
|
||
});
|
||
sel.appendChild(g);
|
||
});
|
||
});
|
||
});
|
||
}
|
||
// ── Pickr color picker ──────────────────────────────────────────────────────
|
||
var _pickr=null;
|
||
|
||
function _initPickr(hex){
|
||
// destroy previous instance if exists
|
||
if(_pickr){ try{ _pickr.destroyAndRemove(); }catch(e){} _pickr=null; }
|
||
var anchor=document.getElementById('slot-pickr-anchor');
|
||
if(!anchor||typeof Pickr==='undefined') return;
|
||
// fresh button element so Pickr can mount
|
||
anchor.innerHTML='<div id="slot-pickr-btn"></div>';
|
||
_pickr=Pickr.create({
|
||
el:'#slot-pickr-btn',
|
||
theme:'nano',
|
||
default: hex||'#808080',
|
||
inline: true,
|
||
showAlways: true,
|
||
components:{
|
||
preview:true, opacity:false, hue:true,
|
||
interaction:{ hex:true, rgba:false, input:true, save:false, clear:false }
|
||
}
|
||
});
|
||
_pickr.on('change',function(color){
|
||
var h=color.toHEXA().toString().slice(0,7);
|
||
document.getElementById('slot-edit-color').value=h;
|
||
document.getElementById('slot-edit-preview').style.background=h;
|
||
});
|
||
// Theme anpassen: Pickr benutzt eigene CSS-Variablen, wir überschreiben via style
|
||
requestAnimationFrame(function(){
|
||
var el=anchor.querySelector('.pickr');
|
||
if(el) el.style.cssText='width:100%';
|
||
var app=anchor.querySelector('.pcr-app');
|
||
if(app){
|
||
app.style.cssText='position:relative;width:100%;box-shadow:none;background:transparent';
|
||
var btn=app.querySelector('.pcr-result');
|
||
if(btn) btn.style.cssText='background:var(--raised);border:1px solid var(--border);color:var(--txt);border-radius:6px;font-size:12px';
|
||
}
|
||
});
|
||
}
|
||
|
||
// ── Color swatches (localStorage, max 16) ──────────────────────────────────
|
||
var _SWATCH_KEY='kxb_color_swatches';
|
||
var _SWATCH_MAX=16;
|
||
|
||
function _loadSwatches(){
|
||
try{ return JSON.parse(localStorage.getItem(_SWATCH_KEY)||'[]'); }catch(e){ return []; }
|
||
}
|
||
function _saveSwatches(arr){ try{ localStorage.setItem(_SWATCH_KEY, JSON.stringify(arr)); }catch(e){} }
|
||
|
||
function _addSwatch(hex){
|
||
var arr=_loadSwatches().filter(function(c){ return c.toLowerCase()!==hex.toLowerCase(); });
|
||
arr.unshift(hex);
|
||
if(arr.length>_SWATCH_MAX) arr=arr.slice(0,_SWATCH_MAX);
|
||
_saveSwatches(arr);
|
||
}
|
||
|
||
function _renderSwatches(){
|
||
var el=document.getElementById('slot-color-swatches');
|
||
if(!el) return;
|
||
var arr=_loadSwatches();
|
||
if(!arr.length){ el.style.display='none'; return; }
|
||
el.style.display='flex';
|
||
el.innerHTML=arr.map(function(c){
|
||
return '<div title="'+c+'" onclick="slotPickSwatch(\''+c+'\')" style="width:22px;height:22px;border-radius:4px;background:'+c+
|
||
';border:2px solid rgba(255,255,255,.2);cursor:pointer;flex-shrink:0"></div>';
|
||
}).join('');
|
||
}
|
||
|
||
function slotPickSwatch(hex){
|
||
if(_pickr){ _pickr.setColor(hex); }
|
||
var ci=document.getElementById('slot-edit-color');
|
||
if(ci) ci.value=hex;
|
||
document.getElementById('slot-edit-preview').style.background=hex;
|
||
}
|
||
|
||
// ── Copy color from other slot ──────────────────────────────────────────────
|
||
function _renderCopyFromSlot(currentGlobalIdx){
|
||
var slots=(window._amsSlots||[]).filter(function(s){
|
||
return s.global_index!==currentGlobalIdx && s.status==5 && Array.isArray(s.color);
|
||
});
|
||
var row=document.getElementById('slot-copy-row');
|
||
var sel=document.getElementById('slot-copy-select');
|
||
if(!row||!sel) return;
|
||
if(!slots.length){ row.style.display='none'; return; }
|
||
row.style.display='';
|
||
var ph=document.getElementById('lbl-slot-copy-from');
|
||
var phTxt=ph?ph.textContent:(T.slot_copy_from||'Copy color from slot…');
|
||
sel.innerHTML='<option value="">'+phTxt+'</option>'+slots.map(function(s){
|
||
var rgb=s.color;
|
||
var hex='#'+rgb.map(function(v){return('0'+Math.min(255,v).toString(16)).slice(-2)}).join('');
|
||
return '<option value="'+hex+'">Slot '+(s.global_index+1)+' — '+(s.type||'?')+' '+hex+'</option>';
|
||
}).join('');
|
||
}
|
||
|
||
function slotCopyColor(sel){
|
||
if(!sel.value) return;
|
||
var ci=document.getElementById('slot-edit-color');
|
||
if(!ci) return;
|
||
ci.value=sel.value;
|
||
document.getElementById('slot-edit-preview').style.background=sel.value;
|
||
sel.selectedIndex=0;
|
||
}
|
||
|
||
// ───────────────────────────────────────────────────────────────────────────
|
||
|
||
function openSlotEdit(i){
|
||
var slot=(window._amsSlots||[])[i]||{};
|
||
var globalIdx=slot.global_index!=null?slot.global_index:(slot.index!=null?slot.index:i);
|
||
_slotEditIndex=globalIdx;
|
||
_slotEditLoaded=(S.ams_loaded_slot!=null&&S.ams_loaded_slot===globalIdx);
|
||
document.getElementById('slot-edit-title').textContent=T.slot_edit_title+' '+(globalIdx+1);
|
||
var rgb=Array.isArray(slot.color)?slot.color:[128,128,128];
|
||
var hex='#'+rgb.map(function(v){return('0'+Math.min(255,v).toString(16)).slice(-2)}).join('');
|
||
var ci=document.getElementById('slot-edit-color');
|
||
ci.value=hex;
|
||
document.getElementById('slot-edit-preview').style.background=hex;
|
||
_initPickr(hex);
|
||
_renderSwatches();
|
||
_renderCopyFromSlot(globalIdx);
|
||
var mat=(slot.type||'PLA').toUpperCase();
|
||
document.getElementById('slot-edit-mat').value=mat;
|
||
// Normalisieren für Button-Highlighting: PLA-Varianten auf nächsten Preset mappen
|
||
var matNorm=_normalizeMat(mat);
|
||
var btns=document.getElementById('slot-mat-btns');
|
||
btns.innerHTML=_MAT_PRESETS.map(function(m){
|
||
var active=m===mat||m===matNorm;
|
||
return '<button class="mat-preset-btn" data-mat="'+m+'" onclick="selectMatPreset(\''+m+'\')" '
|
||
+'style="padding:4px 10px;border-radius:6px;border:1px solid var(--border);cursor:pointer;font-size:12px;'
|
||
+(active?'background:var(--accent);color:#fff':'background:var(--raised);color:var(--txt2)')+'">'+m+'</button>';
|
||
}).join('');
|
||
// OrcaSlicer-Profil-Dropdown: aktuellen User-Override für diesen Slot
|
||
// aus /kx/filament/slots holen (enthält vendor+name+id).
|
||
fetch(_apiUrl("/kx/filament/slots"))
|
||
.then(function (r) {
|
||
return r.json();
|
||
})
|
||
.then(function (d) {
|
||
var arr = d.result || [];
|
||
var entry =
|
||
arr.find(function (x) {
|
||
return x.slot_index === globalIdx;
|
||
}) || {};
|
||
window._slotProfileMap = window._slotProfileMap || {};
|
||
window._slotProfileMap[globalIdx] = {
|
||
id: entry.filament_id || "",
|
||
vendor: entry.filament_vendor || "",
|
||
name: entry.filament_name || "",
|
||
};
|
||
_fillSlotProfileDropdown(mat, entry.filament_vendor || "", entry.filament_name || "");
|
||
})
|
||
.catch(function () {
|
||
_fillSlotProfileDropdown(mat, "", "");
|
||
});
|
||
updateSlotEditFeedButton();
|
||
document.getElementById("slot-edit-modal").classList.add("open");
|
||
}
|
||
function closeSlotEdit() {
|
||
_slotEditIndex = -1;
|
||
document.getElementById("slot-edit-modal").classList.remove("open");
|
||
}
|
||
function slotEditFeed() {
|
||
if (_slotEditIndex < 0) return;
|
||
var type = _slotEditLoaded ? 2 : 1;
|
||
amsFeed(type, _slotEditIndex)
|
||
.then(function () {
|
||
_slotEditLoaded = !_slotEditLoaded;
|
||
updateSlotEditFeedButton();
|
||
poll();
|
||
})
|
||
.catch(function () { });
|
||
}
|
||
function startReadyFile(filename) {
|
||
var fn = filename || S.file_ready;
|
||
function _doStartReadyFile() {
|
||
var btn = document.getElementById("file-ready-btn");
|
||
if (btn) {
|
||
btn.disabled = true;
|
||
btn.textContent = "…";
|
||
}
|
||
post("/printer/print/start", { filename: fn })
|
||
.then(function (r) {
|
||
return r.json();
|
||
})
|
||
.then(function () {
|
||
document.getElementById("file-ready-banner").style.display = "none";
|
||
if (btn) {
|
||
btn.disabled = false;
|
||
setText("file-ready-btn", T.file_ready_btn);
|
||
}
|
||
})
|
||
.catch(function (e) {
|
||
clog(tr("log_error", "Fehler:") + " " + e, "msg-err");
|
||
if (btn) {
|
||
btn.disabled = false;
|
||
setText("file-ready-btn", T.file_ready_btn);
|
||
}
|
||
});
|
||
}
|
||
function _gateAndStart(fileObj) {
|
||
if (fileObj && fileObj.web_unverified && webUploadWarningEnabled()) {
|
||
maybeGateWebUpload(fileObj, function () {
|
||
startReadyFile(fn);
|
||
});
|
||
return;
|
||
}
|
||
_doStartReadyFile();
|
||
}
|
||
var currentFile = (storeFiles || []).find(function (f) {
|
||
return f.filename === fn;
|
||
});
|
||
if (currentFile) {
|
||
_gateAndStart(currentFile);
|
||
return;
|
||
}
|
||
fetch(_apiUrl("/kx/files"))
|
||
.then(function (r) {
|
||
return r.json();
|
||
})
|
||
.then(function (d) {
|
||
storeFiles = d.result || [];
|
||
var refreshed =
|
||
(storeFiles || []).find(function (f) {
|
||
return f.filename === fn;
|
||
}) || null;
|
||
_gateAndStart(refreshed);
|
||
})
|
||
.catch(function () {
|
||
_doStartReadyFile();
|
||
});
|
||
}
|
||
function cancelReadyFile() {
|
||
post("/api/file_ready/clear", {}).then(function () {
|
||
document.getElementById("file-ready-banner").style.display = "none";
|
||
});
|
||
}
|
||
|
||
// ── Aktionen für geladene/idle Datei in der Progress-Karte (Issue #55) ──
|
||
function startIdleFile() {
|
||
if (_lastLoadedFile) startReadyFile(_lastLoadedFile);
|
||
}
|
||
function startIdleFileWithSlots() {
|
||
if (_lastLoadedFile) startReadyFileWithSlots(_lastLoadedFile);
|
||
}
|
||
function clearIdleFile() {
|
||
_lastLoadedFile = null;
|
||
_idleCleared = true; // verhindert Nachladen von s.filename im nächsten poll() (Issue #57)
|
||
_fdAutoOpenedFile = null; // nächster Upload derselben Datei soll Dialog wieder öffnen
|
||
_fdUserCancelled = false;
|
||
_fdDialogOpen = false;
|
||
sessionStorage.removeItem("fdAutoOpenedFile");
|
||
sessionStorage.removeItem("fdUserCancelled");
|
||
sessionStorage.removeItem("webVerifyCancelledFileId");
|
||
S.file_ready = "";
|
||
S.filename = "";
|
||
S.thumbnail = ""; // sofort lokal leeren, kein Warten auf nächsten Poll
|
||
var ib = document.getElementById("d-idle-btns");
|
||
if (ib) ib.style.display = "none";
|
||
var fn = document.getElementById("d-fname");
|
||
if (fn) {
|
||
fn.textContent = "–";
|
||
fn.title = "";
|
||
}
|
||
var thumb = document.getElementById("d-thumbnail");
|
||
if (thumb) {
|
||
thumb.style.display = "none";
|
||
thumb.src = "";
|
||
}
|
||
post("/api/file_ready/clear", {}).catch(function () { });
|
||
}
|
||
function selectMatPreset(m) {
|
||
document.getElementById("slot-edit-mat").value = m;
|
||
highlightMatBtn(m);
|
||
// Filament-Profile-Dropdown an neues Material anpassen
|
||
// (vorherige Selektion zurücksetzen — andere Material-Profile passen nicht)
|
||
_fillSlotProfileDropdown(m, "", "");
|
||
}
|
||
function highlightMatBtn(val) {
|
||
document.querySelectorAll(".mat-preset-btn").forEach(function (b) {
|
||
var on = b.getAttribute("data-mat") === val.toUpperCase();
|
||
b.style.background = on ? "var(--accent)" : "var(--raised)";
|
||
b.style.color = on ? "#fff" : "var(--txt2)";
|
||
});
|
||
// Auch bei manueller Eingabe ins Material-Textfeld: Dropdown refreshen.
|
||
if (val) _fillSlotProfileDropdown(val, "", "");
|
||
}
|
||
function hexToRgb(hex) {
|
||
var r = parseInt(hex.slice(1, 3), 16),
|
||
g = parseInt(hex.slice(3, 5), 16),
|
||
b = parseInt(hex.slice(5, 7), 16);
|
||
return [r, g, b];
|
||
}
|
||
function saveSlotEdit(){
|
||
var hex=document.getElementById('slot-edit-color').value;
|
||
_addSwatch(hex);
|
||
var mat=document.getElementById('slot-edit-mat').value.trim().toUpperCase()||'PLA';
|
||
var color=hexToRgb(hex);
|
||
var slotIdx=_slotEditIndex;
|
||
var profSel=document.getElementById('slot-edit-profile');
|
||
var sel=profSel && profSel.selectedOptions && profSel.selectedOptions[0];
|
||
// Primärer Selector: (vendor, name). id ist nur Hint (aus dem JSON-data-attr
|
||
// mitgegeben — Backend lookt sie nochmal selber nach um veraltete Hints zu
|
||
// korrigieren).
|
||
var newProfVendor = sel ? sel.dataset.vendor || "" : "";
|
||
var newProfName = sel ? sel.dataset.name || "" : "";
|
||
var newProfId = sel ? sel.dataset.id || "" : "";
|
||
// Sequenziell speichern: erst Profil-Override (config.ini), dann Material/Farbe
|
||
// (MQTT zum Drucker). Sonst können beide Pfade sich überholen und der Slot-State
|
||
// ist beim nächsten Re-Open inkonsistent.
|
||
fetch(_apiUrl("/kx/filament/slots/" + slotIdx + "/profile"), {
|
||
method: "POST",
|
||
headers: { "Content-Type": "application/json" },
|
||
body: JSON.stringify({
|
||
vendor: newProfVendor,
|
||
name: newProfName,
|
||
id: newProfId,
|
||
}),
|
||
})
|
||
.then(function (r) {
|
||
return r.json();
|
||
})
|
||
.then(function () {
|
||
window._slotProfileMap = window._slotProfileMap || {};
|
||
if (newProfVendor && newProfName) {
|
||
window._slotProfileMap[slotIdx] = {
|
||
id: newProfId,
|
||
vendor: newProfVendor,
|
||
name: newProfName,
|
||
};
|
||
} else {
|
||
delete window._slotProfileMap[slotIdx];
|
||
}
|
||
return post("/api/ams/set_slot", {
|
||
index: slotIdx,
|
||
type: mat,
|
||
color: color,
|
||
});
|
||
})
|
||
.then(function (r) {
|
||
return r ? r.json() : null;
|
||
})
|
||
.then(function () {
|
||
// Slot-Map refreshen damit die Karte sofort den Vendor zeigt.
|
||
return fetch(_apiUrl("/kx/filament/slots")).then(function (r) {
|
||
return r.json();
|
||
});
|
||
})
|
||
.then(function (d) {
|
||
var arr = (d && d.result) || [];
|
||
window._slotProfileMap = {};
|
||
arr.forEach(function (e) {
|
||
if (e.filament_vendor && e.filament_name) {
|
||
window._slotProfileMap[e.slot_index] = {
|
||
id: e.filament_id || "",
|
||
vendor: e.filament_vendor,
|
||
name: e.filament_name,
|
||
};
|
||
}
|
||
});
|
||
closeSlotEdit();
|
||
var profSuffix = newProfName ? " [" + newProfVendor + " " + newProfName + "]" : "";
|
||
clog(
|
||
tr("slot_edit_ok") + " " + (slotIdx + 1) + ": " + mat + " " + hex + profSuffix,
|
||
"msg-ok"
|
||
);
|
||
// Sofortiges Re-Render mit aktuellem _slotProfileMap (poll() ist async
|
||
// und re-rendert beim nächsten Tick — wir wollen aber dass die Vendor-
|
||
// Badge JETZT direkt sichtbar wird).
|
||
if (typeof applyState === "function") applyState();
|
||
if (typeof poll === "function") poll();
|
||
})
|
||
.catch(function (e) {
|
||
clog(tr("log_error", "Fehler:") + " " + e, "msg-err");
|
||
});
|
||
}
|
||
document.addEventListener("DOMContentLoaded", function () {
|
||
document.getElementById("s-printer-ip").addEventListener("input", function () {
|
||
var hint = document.getElementById("lbl-ip-hint");
|
||
if (this.value.includes(":")) {
|
||
hint.textContent = T.hint_ip_no_port;
|
||
hint.style.display = "block";
|
||
} else {
|
||
hint.style.display = "none";
|
||
}
|
||
});
|
||
});
|
||
function setPoll(ms) {
|
||
localStorage.setItem("pollInterval", ms);
|
||
clearInterval(pollTimer);
|
||
pollTimer = setInterval(poll, ms);
|
||
}
|
||
function saveSettings() {
|
||
var btn = document.getElementById("btn-save-settings");
|
||
btn.disabled = true;
|
||
btn.textContent = "…";
|
||
var webUploadWarning = (document.getElementById("s-web-upload-warning") || {}).checked ? 1 : 0;
|
||
S.web_upload_warning = webUploadWarning;
|
||
// Start-Print-Behavior-Wechsel könnte den Auto-Open sonst dauerhaft blockieren
|
||
// (alter _fdUserCancelled bei gleicher file_ready) → Dialog-State zurücksetzen (Issue #57).
|
||
_fdUserCancelled = false;
|
||
_fdAutoOpenedFile = null;
|
||
sessionStorage.removeItem("fdUserCancelled");
|
||
sessionStorage.removeItem("fdAutoOpenedFile");
|
||
sessionStorage.removeItem("webVerifyCancelledFileId");
|
||
post("/api/settings", {
|
||
printer_name: document.getElementById("s-printer-name").value,
|
||
printer_ip: document.getElementById("s-printer-ip").value,
|
||
mqtt_port: parseInt(document.getElementById("s-mqtt-port").value) || 9883,
|
||
username: document.getElementById("s-username").value,
|
||
password: document.getElementById("s-password").value,
|
||
device_id: document.getElementById("s-device-id").value,
|
||
mode_id: document.getElementById("s-mode-id").value,
|
||
default_ams_slot: document.getElementById("s-default-slot").value,
|
||
auto_leveling: document.getElementById("s-auto-leveling").checked ? 1 : 0,
|
||
vibration_compensation: (document.getElementById('s-vibration-compensation')||{}).checked?1:0,
|
||
camera_on_print: (document.getElementById("s-camera-on-print") || {}).checked ? 1 : 0,
|
||
print_start_dialog: parseInt(
|
||
(document.getElementById("s-file-ready-mode") || {}).value || "1",
|
||
10
|
||
),
|
||
web_upload_warning: webUploadWarning,
|
||
poll_interval: Math.min(
|
||
60,
|
||
Math.max(1, parseInt((document.getElementById("s-poll-interval") || {}).value, 10) || 3)
|
||
),
|
||
verbose_http_log: (document.getElementById('s-verbose-http-log')||{}).checked?1:0,
|
||
notifications: notifCollect(),
|
||
notifications_paused: _notifPaused,
|
||
notify_every_minutes:
|
||
parseInt((document.getElementById("s-notif-every-min") || {}).value || "0", 10) || 0,
|
||
notify_every_layers:
|
||
parseInt((document.getElementById("s-notif-every-layers") || {}).value || "0", 10) || 0,
|
||
spoolman_server: ((document.getElementById("s-spoolman-url") || {}).value || "").trim(),
|
||
spoolman_sync_rate: Math.min(
|
||
3600,
|
||
Math.max(0, parseInt((document.getElementById("s-spoolman-sync-rate") || {}).value, 10) || 0)
|
||
),
|
||
})
|
||
.then(function () {
|
||
btn.textContent = T.update_restarting;
|
||
setTimeout(function () {
|
||
btn.disabled = false;
|
||
setText("btn-save-settings", T.settings_save);
|
||
closeSettings();
|
||
_loadSpoolmanStatus();
|
||
poll();
|
||
}, 4000);
|
||
})
|
||
.catch(function (e) {
|
||
btn.disabled = false;
|
||
setText("btn-save-settings", T.settings_save);
|
||
clog(T.settings_title + " " + tr("log_error", "Fehler:") + " " + e, "msg-err");
|
||
});
|
||
}
|
||
var _updateDockerOnly = false;
|
||
function checkUpdate() {
|
||
var sb = document.getElementById('update-status');
|
||
sb.textContent = T.update_checking;
|
||
document.getElementById('btn-update-apply').style.display = 'none';
|
||
_updateTag = ''; _updateUrl = ''; _updateDockerOnly = false;
|
||
fetch(_apiUrl('/api/update/check')).then(function (r) { return r.json() }).then(function (d) {
|
||
if (d.error) { sb.textContent = T.update_error + ': ' + d.error; return; }
|
||
var cl = document.getElementById('update-changelog');
|
||
if (d.changelog && d.changelog.trim()) {
|
||
// Changelog als Markdown-Text (pre-formatiert) anzeigen
|
||
cl.textContent = d.changelog; cl.style.display = 'block';
|
||
} else { cl.style.display = 'none'; }
|
||
_updateDockerOnly = !!d.docker_only;
|
||
if (d.update_available) {
|
||
sb.textContent = d.latest + ' ' + T.update_available;
|
||
sb.style.color = 'var(--ok)';
|
||
_updateTag = d.tag; _updateUrl = d.download_url || '';
|
||
var btn = document.getElementById('btn-update-apply');
|
||
if (_updateDockerOnly) {
|
||
btn.textContent = T.update_docker || 'docker compose pull';
|
||
btn.title = 'docker compose pull && docker compose up -d';
|
||
} else {
|
||
btn.textContent = T.update_apply;
|
||
btn.title = '';
|
||
}
|
||
btn.style.display = 'inline-block';
|
||
} else {
|
||
sb.textContent = T.update_none;
|
||
sb.style.color = 'var(--txt2)';
|
||
}
|
||
}).catch(function (e) { sb.textContent = T.update_error + ': ' + e; });
|
||
}
|
||
function applyUpdate() {
|
||
var sb = document.getElementById('update-status');
|
||
var btn = document.getElementById('btn-update-apply');
|
||
if (_updateDockerOnly) {
|
||
// Nightly: kein Self-Update, Docker-Befehl in Zwischenablage kopieren
|
||
var cmd = 'docker compose pull && docker compose up -d';
|
||
if (navigator.clipboard) {
|
||
navigator.clipboard.writeText(cmd).then(function () {
|
||
sb.textContent = T.update_docker_copied || 'Befehl kopiert: ' + cmd;
|
||
});
|
||
} else {
|
||
sb.textContent = cmd;
|
||
}
|
||
return;
|
||
}
|
||
if (!_updateUrl) return;
|
||
btn.disabled = true; sb.textContent = T.update_applying;
|
||
post('/api/update/apply', { download_url: _updateUrl, tag: _updateTag }).then(function () {
|
||
sb.textContent = T.update_restarting;
|
||
closeSettings();
|
||
setTimeout(function () { poll(); }, 5000);
|
||
}).catch(function (e) {
|
||
btn.disabled = false; sb.textContent = T.update_error + ': ' + e;
|
||
});
|
||
}
|
||
|
||
// ── Poll ──
|
||
async function poll() {
|
||
try {
|
||
var r = await fetch(_apiUrl("/api/state"));
|
||
if (!r.ok) return;
|
||
var d = await r.json();
|
||
Object.assign(S, d);
|
||
applyState();
|
||
updateHistory();
|
||
} catch (e) {
|
||
clog(T.log_poll_error + " " + e, "msg-err");
|
||
}
|
||
}
|
||
var pollTimer;
|
||
(function () {
|
||
var ms = parseInt(localStorage.getItem("pollInterval") || "2000");
|
||
initPrinters();
|
||
// Slot-Profile-Map initial laden, sonst zeigen die Karten beim ersten
|
||
// Render keine Vendor-Badge obwohl in der config.ini ein Override steht.
|
||
fetch(_apiUrl('/kx/filament/slots')).then(function(r){return r.json();}).then(function(d){
|
||
var arr=(d && d.result)||[];
|
||
window._slotProfileMap={};
|
||
arr.forEach(function(e){
|
||
if(e.filament_vendor && e.filament_name){
|
||
window._slotProfileMap[e.slot_index]={
|
||
id: e.filament_id||'',
|
||
vendor:e.filament_vendor,
|
||
name: e.filament_name,
|
||
};
|
||
}
|
||
});
|
||
}).catch(function(){});
|
||
poll();pollTimer=setInterval(poll,ms);
|
||
setInterval(_loadSpoolmanStatus,30000);
|
||
// initDashGrid() is called at the very end of the file, after all the
|
||
// DASH_* var declarations below have executed (they are hoisted but not yet
|
||
// assigned at this point in the IIFE).
|
||
})();
|
||
|
||
// ── Dashboard Free Grid (Issue #89) ────────────────────────────────────────
|
||
// User-customizable dashboard powered by GridStack (v10, docs: gridstack.js).
|
||
// Cards move + resize on a 12-column snap grid; layout persisted per browser
|
||
// in localStorage. Built on documented APIs only:
|
||
// - float:false (default) -> compact packing, no holes between cards
|
||
// - staticGrid:true -> locked by default, setStatic(false) in edit mode
|
||
// - grid.save(false)/load(layout,false) -> serialize/restore by gs-id
|
||
// - draggable.cancel -> inputs/buttons/img excluded from drag start
|
||
// - columnOpts.breakpoints -> 1-column below 700px GRID width (sidebar-aware)
|
||
var DASH_STORAGE_KEY='dashLayout';
|
||
var DASH_LAYOUT_VERSION=3; // v1/v2 = older editors; discard those states
|
||
var DASH_CARD_KEYS=['camera','progress','temps','motion','speed','fan','ams'];
|
||
// Layout arrays in GridStack save()/load() format. cellHeight=60px.
|
||
var DASH_DEFAULT_LAYOUT=[
|
||
{id:'camera', x:0,y:0, w:12,h:7},
|
||
{id:'progress',x:0,y:7, w:12,h:6},
|
||
{id:'temps', x:0,y:13,w:6, h:7},
|
||
{id:'motion', x:6,y:13,w:6, h:7},
|
||
{id:'speed', x:0,y:20,w:6, h:3},
|
||
{id:'fan', x:6,y:20,w:6, h:3},
|
||
{id:'ams', x:0,y:23,w:12,h:4},
|
||
];
|
||
// "Wide desktop" preset (Blaim, Issue #89): big camera left, settings center,
|
||
// motion right.
|
||
var DASH_PRESET_WIDE=[
|
||
{id:'progress',x:0, y:0, w:12,h:4},
|
||
{id:'camera', x:0, y:4, w:6, h:11},
|
||
{id:'temps', x:6, y:4, w:3, h:7},
|
||
{id:'speed', x:6, y:11,w:3, h:2},
|
||
{id:'fan', x:6, y:13,w:3, h:2},
|
||
{id:'motion', x:9, y:4, w:3, h:11},
|
||
{id:'ams', x:0, y:15,w:12,h:4},
|
||
];
|
||
var _dashGrid=null;
|
||
var _dashEditing=false;
|
||
var _dashHidden=[]; // [{id,x,y,w,h}] cards currently hidden (position kept for re-show)
|
||
|
||
// GridStack requires .grid-stack-item > .grid-stack-item-content around each
|
||
// card — wrap the existing .card elements in place (IDs/event handlers survive,
|
||
// appendChild moves nodes without recreating them).
|
||
function _dashWrapCards(){
|
||
var grid=document.getElementById('dash-grid');
|
||
if(!grid)return;
|
||
DASH_CARD_KEYS.forEach(function(key){
|
||
var card=grid.querySelector('[data-card="'+key+'"]');
|
||
if(!card||card.parentNode.classList.contains('grid-stack-item-content'))return;
|
||
var item=document.createElement('div');
|
||
item.className='grid-stack-item';
|
||
item.setAttribute('gs-id',key);
|
||
var content=document.createElement('div');
|
||
content.className='grid-stack-item-content';
|
||
grid.insertBefore(item,card);
|
||
item.appendChild(content);
|
||
content.appendChild(card);
|
||
});
|
||
}
|
||
|
||
function _dashItem(key){
|
||
return document.querySelector('#dash-grid .grid-stack-item[gs-id="'+key+'"]');
|
||
}
|
||
|
||
function _dashLoadState(){
|
||
try{
|
||
var raw=localStorage.getItem(DASH_STORAGE_KEY);
|
||
if(!raw)return null;
|
||
var s=JSON.parse(raw);
|
||
if(!s||s.version!==DASH_LAYOUT_VERSION||!Array.isArray(s.layout))return null;
|
||
return s;
|
||
}catch(e){return null;}
|
||
}
|
||
function _dashSaveState(){
|
||
if(!_dashGrid)return;
|
||
localStorage.setItem(DASH_STORAGE_KEY,JSON.stringify({
|
||
version:DASH_LAYOUT_VERSION,
|
||
layout:_dashGrid.save(false), // [{id,x,y,w,h},…] — visible widgets only
|
||
hidden:_dashHidden.slice(),
|
||
}));
|
||
}
|
||
|
||
function initDashGrid(){
|
||
var el=document.getElementById('dash-grid');
|
||
if(!el||typeof GridStack==='undefined')return;
|
||
_dashWrapCards();
|
||
_dashGrid=GridStack.init({
|
||
cellHeight:60,
|
||
margin:8,
|
||
staticGrid:true, // locked by default; setStatic(false) enables drag+resize
|
||
columnOpts:{breakpoints:[{w:700,c:1}]}, // grid-width based (sidebar-aware)
|
||
draggable:{cancel:'input,textarea,button,select,option,img,.slider'},
|
||
},el);
|
||
var state=_dashLoadState();
|
||
if(state){
|
||
_dashHidden=Array.isArray(state.hidden)?state.hidden:[];
|
||
_dashGrid.load(state.layout,false); // update matching ids, no add/remove
|
||
_dashHidden.forEach(function(n){
|
||
var item=_dashItem(n.id);
|
||
if(item){_dashGrid.removeWidget(item,false);item.style.display='none';}
|
||
});
|
||
}else{
|
||
_dashGrid.load(DASH_DEFAULT_LAYOUT,false);
|
||
}
|
||
_dashGrid.on('change',function(){if(_dashEditing)_dashSaveState();});
|
||
_dashRefreshPresetDropdown();
|
||
}
|
||
|
||
function toggleDashEdit(){
|
||
if(!_dashGrid)return;
|
||
_dashEditing=!_dashEditing;
|
||
_dashGrid.setStatic(!_dashEditing);
|
||
document.getElementById('dash-grid').classList.toggle('editing',_dashEditing);
|
||
document.getElementById('dash-lbl-edit').textContent=_dashEditing?(T.dash_done||'Done'):(T.dash_edit||'Customize dashboard');
|
||
document.getElementById('dash-reset-btn').style.display=_dashEditing?'':'none';
|
||
document.getElementById('dash-preset').style.display=_dashEditing?'':'none';
|
||
document.getElementById('dash-preset-save-btn').style.display=_dashEditing?'':'none';
|
||
_dashUpdatePresetDeleteBtn();
|
||
if(_dashEditing)_dashInjectControls(); else _dashRemoveControls();
|
||
_dashRenderHiddenBar();
|
||
if(!_dashEditing)_dashSaveState();
|
||
}
|
||
|
||
// ── Custom presets (user-saved layouts) ────────────────────────────────────
|
||
var DASH_CUSTOM_PRESETS_KEY='dashCustomPresets';
|
||
var DASH_BUILTIN_PRESET_IDS=['standard','wide89'];
|
||
|
||
function _dashLoadCustomPresets(){
|
||
try{
|
||
var raw=localStorage.getItem(DASH_CUSTOM_PRESETS_KEY);
|
||
var obj=raw?JSON.parse(raw):{};
|
||
return (obj&&typeof obj==='object')?obj:{};
|
||
}catch(e){return {};}
|
||
}
|
||
function _dashSaveCustomPresets(presets){
|
||
localStorage.setItem(DASH_CUSTOM_PRESETS_KEY,JSON.stringify(presets));
|
||
}
|
||
|
||
function _dashRefreshPresetDropdown(){
|
||
var sel=document.getElementById('dash-preset');
|
||
if(!sel)return;
|
||
var current=sel.value;
|
||
var customs=_dashLoadCustomPresets();
|
||
// Remove previously-rendered custom <option>s, keep the two built-ins.
|
||
Array.prototype.slice.call(sel.querySelectorAll('option')).forEach(function(o){
|
||
if(DASH_BUILTIN_PRESET_IDS.indexOf(o.value)===-1)sel.removeChild(o);
|
||
});
|
||
Object.keys(customs).sort().forEach(function(name){
|
||
var opt=document.createElement('option');
|
||
opt.value='custom:'+name;
|
||
opt.textContent=name;
|
||
sel.appendChild(opt);
|
||
});
|
||
if(Array.prototype.some.call(sel.options,function(o){return o.value===current;}))sel.value=current;
|
||
_dashUpdatePresetDeleteBtn();
|
||
}
|
||
|
||
function _dashUpdatePresetDeleteBtn(){
|
||
var sel=document.getElementById('dash-preset');
|
||
var delBtn=document.getElementById('dash-preset-delete-btn');
|
||
if(!sel||!delBtn)return;
|
||
var isCustom=sel.value.indexOf('custom:')===0;
|
||
delBtn.style.display=(_dashEditing&&isCustom)?'':'none';
|
||
}
|
||
|
||
function saveCurrentAsDashPreset(){
|
||
if(!_dashGrid)return;
|
||
var name=(prompt(T.dash_preset_name_prompt||'Preset name:')||'').trim();
|
||
if(!name)return;
|
||
var customs=_dashLoadCustomPresets();
|
||
customs[name]=_dashGrid.save(false);
|
||
_dashSaveCustomPresets(customs);
|
||
_dashRefreshPresetDropdown();
|
||
var sel=document.getElementById('dash-preset');
|
||
if(sel)sel.value='custom:'+name;
|
||
_dashUpdatePresetDeleteBtn();
|
||
}
|
||
|
||
function deleteCurrentDashPreset(){
|
||
var sel=document.getElementById('dash-preset');
|
||
if(!sel||sel.value.indexOf('custom:')!==0)return;
|
||
var name=sel.value.slice(7);
|
||
if(!confirm((T.dash_preset_delete_confirm||'Delete preset "{name}"?').replace('{name}',name)))return;
|
||
var customs=_dashLoadCustomPresets();
|
||
delete customs[name];
|
||
_dashSaveCustomPresets(customs);
|
||
sel.value='standard';
|
||
_dashRefreshPresetDropdown();
|
||
resetDashLayout();
|
||
}
|
||
|
||
function _dashInjectControls(){
|
||
DASH_CARD_KEYS.forEach(function(key){
|
||
if(_dashHidden.some(function(n){return n.id===key;}))return;
|
||
var content=document.querySelector('#dash-grid .grid-stack-item[gs-id="'+key+'"] .grid-stack-item-content');
|
||
if(!content||content.querySelector('.dash-card-controls'))return;
|
||
var ctrl=document.createElement('div');
|
||
ctrl.className='dash-card-controls';
|
||
var hideBtn=document.createElement('button');
|
||
hideBtn.className='dash-card-ctrl-btn';
|
||
hideBtn.title=T.dash_hide||'Hide';
|
||
hideBtn.textContent='👁';
|
||
hideBtn.addEventListener('click',function(e){e.stopPropagation();dashHideCard(key);});
|
||
ctrl.appendChild(hideBtn);
|
||
content.appendChild(ctrl);
|
||
});
|
||
}
|
||
function _dashRemoveControls(){
|
||
var nodes=document.querySelectorAll('#dash-grid .dash-card-controls');
|
||
Array.prototype.forEach.call(nodes,function(n){n.parentNode&&n.parentNode.removeChild(n);});
|
||
}
|
||
|
||
function _dashCardLabel(key){
|
||
var card=document.querySelector('[data-card="'+key+'"]');
|
||
if(!card)return key;
|
||
var t=card.querySelector('.card-title');
|
||
return t?t.textContent.trim():key;
|
||
}
|
||
|
||
function _dashRenderHiddenBar(){
|
||
var bar=document.getElementById('dash-hidden-bar');
|
||
if(!bar)return;
|
||
bar.innerHTML='';
|
||
if(!_dashEditing||!_dashHidden.length){bar.classList.remove('show');return;}
|
||
bar.classList.add('show');
|
||
var label=document.createElement('span');
|
||
label.textContent=(T.dash_hidden_cards||'Hidden cards')+': ';
|
||
bar.appendChild(label);
|
||
_dashHidden.forEach(function(n){
|
||
var chip=document.createElement('span');
|
||
chip.className='dash-hidden-chip';
|
||
chip.appendChild(document.createTextNode(_dashCardLabel(n.id)+' '));
|
||
var btn=document.createElement('button');
|
||
btn.textContent='👁';
|
||
btn.title=T.dash_show||'Show';
|
||
btn.addEventListener('click',function(){dashShowCard(n.id);});
|
||
chip.appendChild(btn);
|
||
bar.appendChild(chip);
|
||
});
|
||
}
|
||
|
||
function dashHideCard(key){
|
||
if(_dashHidden.some(function(n){return n.id===key;}))return;
|
||
var item=_dashItem(key);
|
||
if(!item||!_dashGrid)return;
|
||
// Remember position/size so re-show can restore it.
|
||
var n=item.gridstackNode||{};
|
||
_dashHidden.push({id:key,x:n.x,y:n.y,w:n.w,h:n.h});
|
||
_dashGrid.removeWidget(item,false); // keep DOM element, drop the widget
|
||
item.style.display='none';
|
||
_dashSaveState();
|
||
_dashRenderHiddenBar();
|
||
}
|
||
function dashShowCard(key){
|
||
var entry=null;
|
||
_dashHidden=_dashHidden.filter(function(n){if(n.id===key){entry=n;return false;}return true;});
|
||
var item=_dashItem(key);
|
||
if(item&&_dashGrid){
|
||
item.style.display='';
|
||
_dashGrid.makeWidget(item);
|
||
if(entry&&entry.w)_dashGrid.update(item,{x:entry.x,y:entry.y,w:entry.w,h:entry.h});
|
||
if(_dashEditing)_dashInjectControls();
|
||
}
|
||
_dashSaveState();
|
||
_dashRenderHiddenBar();
|
||
}
|
||
|
||
function _dashShowAllHidden(){
|
||
_dashHidden.slice().forEach(function(n){dashShowCard(n.id);});
|
||
_dashHidden=[];
|
||
}
|
||
|
||
function resetDashLayout(){
|
||
if(!_dashGrid)return;
|
||
localStorage.removeItem(DASH_STORAGE_KEY);
|
||
var sel=document.getElementById('dash-preset');if(sel)sel.value='standard';
|
||
_dashShowAllHidden();
|
||
_dashGrid.load(DASH_DEFAULT_LAYOUT,false);
|
||
if(_dashEditing){_dashInjectControls();_dashSaveState();}
|
||
_dashRenderHiddenBar();
|
||
}
|
||
function applyDashPreset(name){
|
||
if(name==='standard'){resetDashLayout();return;}
|
||
if(!_dashGrid)return;
|
||
var layout=null;
|
||
if(name==='wide89'){
|
||
layout=DASH_PRESET_WIDE;
|
||
}else if(name.indexOf('custom:')===0){
|
||
var customs=_dashLoadCustomPresets();
|
||
layout=customs[name.slice(7)];
|
||
}
|
||
if(!layout)return;
|
||
var sel=document.getElementById('dash-preset');
|
||
if(sel)sel.value=name; // keep the dropdown in sync even when called directly
|
||
_dashShowAllHidden();
|
||
_dashGrid.load(layout,false);
|
||
_dashUpdatePresetDeleteBtn();
|
||
if(_dashEditing)_dashInjectControls();
|
||
_dashSaveState();
|
||
_dashRenderHiddenBar();
|
||
}
|
||
|
||
// Init now — all DASH_* declarations above have run, and app.js loads at
|
||
// </body>, so the DOM is ready.
|
||
initDashGrid();
|
||
// ── Print actions ──
|
||
function printAction(a) {
|
||
post("/printer/print/" + a, {})
|
||
.then(function () {
|
||
clog(tr("log_print_action", "Druck:") + " " + a, "msg-ok");
|
||
poll();
|
||
})
|
||
.catch(function (e) {
|
||
clog(tr("log_error", "Fehler:") + " " + e, "msg-err");
|
||
});
|
||
}
|
||
function togglePauseResume() {
|
||
// Druckt → pause; Pausiert → resume. Status kommt aus dem zuletzt gepollten
|
||
// print_state in S; bei Unklarheit (kein State) Pause als Default.
|
||
var state = (S && S.print_state) || "";
|
||
if (state === "paused") printAction("resume");
|
||
else printAction("pause");
|
||
}
|
||
function updatePauseResumeBtn() {
|
||
var btn = document.getElementById("d-btn-pause");
|
||
if (!btn) return;
|
||
var state = (S && S.print_state) || "";
|
||
if (state === "paused") {
|
||
btn.textContent = T.btn_resume || "▶ Weiter";
|
||
btn.classList.add("btn-resume");
|
||
btn.classList.remove("btn-pause");
|
||
} else {
|
||
btn.textContent = T.btn_pause || "⏸ Pause";
|
||
btn.classList.add("btn-pause");
|
||
btn.classList.remove("btn-resume");
|
||
}
|
||
}
|
||
function confirmCancel() {
|
||
if (confirm(T.confirm_cancel || "Druck wirklich abbrechen?")) printAction("cancel");
|
||
}
|
||
|
||
// ── Axis motion ──
|
||
// axis codes: 0=X, 1=Y, 2=Z
|
||
// move_type 1=relative, distance positive/negative
|
||
function getStep() { return currentStep }
|
||
function setStep(btn, v) {
|
||
currentStep = v;
|
||
document.querySelectorAll('.step-btn').forEach(b => b.classList.remove('active'));
|
||
btn.classList.add('active');
|
||
var ci = document.getElementById('step-custom');
|
||
if (ci) ci.value = '';
|
||
}
|
||
function setStepCustom(inp) {
|
||
var v = parseFloat(inp.value);
|
||
if (!isNaN(v) && v > 0) {
|
||
currentStep = v;
|
||
document.querySelectorAll('.step-btn').forEach(b => b.classList.remove('active'));
|
||
}
|
||
}
|
||
function move(axis, dir, dist) {
|
||
// axis: 0=X,1=Y,2=Z → printer axis codes: 1=X,2=Y,3=Z
|
||
var axisMap = { 0: 1, 1: 2, 2: 3 };
|
||
post("/api/axis", { axis: axisMap[axis], move_type: 1, distance: dir * dist })
|
||
.then(function () {
|
||
clog(
|
||
tr("log_axis", "Achse") +
|
||
" " +
|
||
(axis === 0 ? "X" : axis === 1 ? "Y" : "Z") +
|
||
" " +
|
||
(dir > 0 ? "+" : "") +
|
||
dir * dist +
|
||
"mm",
|
||
"msg-ok"
|
||
);
|
||
})
|
||
.catch(function (e) {
|
||
clog(tr("log_axis", "Achse") + "-" + tr("log_error", "Fehler:") + " " + e, "msg-err");
|
||
});
|
||
}
|
||
function homeAll() {
|
||
post("/api/axis", { axis: 5, move_type: 2, distance: 0 })
|
||
.then(function () {
|
||
clog(tr("log_home_all", "Home All"), "msg-ok");
|
||
})
|
||
.catch(function (e) {
|
||
clog(tr("log_home_all", "Home All") + " " + tr("log_error", "Fehler:") + " " + e, "msg-err");
|
||
});
|
||
}
|
||
function homeXY() {
|
||
post("/api/axis", { axis: 4, move_type: 2, distance: 0 })
|
||
.then(function () {
|
||
clog(tr("btn_home_xy", "Home XY"), "msg-ok");
|
||
})
|
||
.catch(function (e) {
|
||
clog(tr("btn_home_xy", "Home XY") + " " + tr("log_error", "Fehler:") + " " + e, "msg-err");
|
||
});
|
||
}
|
||
function homeZ() {
|
||
post("/api/axis", { axis: 3, move_type: 2, distance: 0 })
|
||
.then(function () {
|
||
clog(tr("btn_home_z", "Home Z"), "msg-ok");
|
||
})
|
||
.catch(function (e) {
|
||
clog(tr("btn_home_z", "Home Z") + " " + tr("log_error", "Fehler:") + " " + e, "msg-err");
|
||
});
|
||
}
|
||
function disableMotors() {
|
||
post("/api/axis", { action: "turnOff" })
|
||
.then(function () {
|
||
clog(tr("btn_disable_motors", "Motors Off"), "msg-ok");
|
||
})
|
||
.catch(function (e) {
|
||
clog(
|
||
tr("btn_disable_motors", "Motors Off") + " " + tr("log_error", "Fehler:") + " " + e,
|
||
"msg-err"
|
||
);
|
||
});
|
||
}
|
||
|
||
// ── Temperature ──
|
||
function setNozzle() {
|
||
var v = parseFloat(document.getElementById("p-nozzle-inp").value || 0);
|
||
post("/api/temperature", { nozzle: v, bed: S.bed_target })
|
||
.then(function () {
|
||
clog(tr("log_nozzle", "Nozzle → ") + v + "°C", "msg-ok");
|
||
})
|
||
.catch(function (e) {
|
||
clog(tr("label_nozzle", "Düse") + " " + tr("log_error", "Fehler:") + " " + e, "msg-err");
|
||
});
|
||
}
|
||
function setBed() {
|
||
var v = parseFloat(document.getElementById("p-bed-inp").value || 0);
|
||
post("/api/temperature", { nozzle: S.nozzle_target, bed: v })
|
||
.then(function () {
|
||
clog(tr("log_bed", "Bett → ") + v + "°C", "msg-ok");
|
||
})
|
||
.catch(function (e) {
|
||
clog(tr("label_bed", "Bett") + " " + tr("log_error", "Fehler:") + " " + e, "msg-err");
|
||
});
|
||
}
|
||
|
||
// ── Light ──
|
||
function setLight() {
|
||
var on = document.getElementById("d-light-toggle").checked;
|
||
post("/api/light", { on: on, brightness: 80 })
|
||
.then(function () {
|
||
clog("Licht " + (on ? "an, 80%" : "aus"), "msg-ok");
|
||
})
|
||
|
||
.catch(function (e) {
|
||
clog("Licht-Fehler: " + e, "msg-err");
|
||
});
|
||
}
|
||
|
||
// ── Print Speed ──
|
||
function setSpeed(mode) {
|
||
S.print_speed_mode = mode;
|
||
[1, 2, 3].forEach(function (m) {
|
||
var b = document.getElementById("d-spd-" + m);
|
||
if (b) b.classList.toggle("spd-active", m === mode);
|
||
});
|
||
post("/api/speed", { mode: mode }).catch(function (e) {
|
||
clog(
|
||
tr("label_speed", "Geschwindigkeit") + " " + tr("log_error", "Fehler:") + " " + e,
|
||
"msg-err"
|
||
);
|
||
});
|
||
}
|
||
|
||
// ── Fan ──
|
||
function setFan() {
|
||
var v = parseInt(document.getElementById("d-fan").value);
|
||
document.getElementById("d-fan-val").textContent = v;
|
||
post("/api/fan", { speed: v })
|
||
.then(function () {
|
||
clog(tr("log_fan", "Lüfter → ") + v + "%", "msg-ok");
|
||
})
|
||
.catch(function (e) {
|
||
clog(tr("log_fan", "Lüfter → ") + tr("log_error", "Fehler:") + " " + e, "msg-err");
|
||
});
|
||
}
|
||
function quickFan(v) {
|
||
document.getElementById("d-fan").value = v;
|
||
document.getElementById("d-fan-val").textContent = v;
|
||
post("/api/fan", { speed: v })
|
||
.then(function () {
|
||
clog(tr("log_fan", "Lüfter → ") + v + "%", "msg-ok");
|
||
})
|
||
.catch(function (e) {
|
||
clog(tr("log_fan", "Lüfter → ") + tr("log_error", "Fehler:") + " " + e, "msg-err");
|
||
});
|
||
}
|
||
|
||
// ── AMS ──
|
||
function amsFeed(type, slotIndex) {
|
||
var globalIdx;
|
||
if (typeof slotIndex === "number" && slotIndex >= 0) {
|
||
globalIdx = slotIndex;
|
||
} else {
|
||
var i = parseInt(document.getElementById("ams-slot-sel").value);
|
||
var slot = (window._amsSlots || [])[i] || {};
|
||
globalIdx = slot.global_index != null ? slot.global_index : i;
|
||
}
|
||
return post("/api/ams/feed", { slot_index: globalIdx, type: type })
|
||
.then(function () {
|
||
clog((type === 1 ? T.lbl_feed : T.lbl_unload) + " Slot " + (globalIdx + 1), "msg-ok");
|
||
})
|
||
.catch(function (e) {
|
||
clog("AMS " + tr("log_error", "Fehler:") + " " + e, "msg-err");
|
||
throw e;
|
||
});
|
||
}
|
||
|
||
// ── Camera ──
|
||
function camStart() {
|
||
var img = document.getElementById('cam-img');
|
||
var ph = document.getElementById('cam-placeholder');
|
||
var sp = document.getElementById('cam-spinner');
|
||
ph.style.display = 'none';
|
||
img.style.display = 'none';
|
||
sp.style.display = 'block';
|
||
post('/api/camera/start', {}).then(function () {
|
||
camOn = true;
|
||
document.getElementById('cam-toggle-btn').textContent = tr('btn_cam_stop');
|
||
var rb = document.getElementById('cam-reset-btn'); if (rb) rb.style.display = '';
|
||
clog(tr('log_cam_start'), 'msg-ok');
|
||
setTimeout(function () { sp.style.display = 'none'; img.style.display = 'block'; }, 1200);
|
||
if (/Android/i.test(navigator.userAgent)) {
|
||
// Android browsers don't support multipart/x-mixed-replace (MJPEG) — poll snapshots instead
|
||
_camPollInterval = setInterval(function () { img.src = '/api/camera/snapshot?t=' + Date.now(); }, 200);
|
||
} else {
|
||
img.onerror = function () {
|
||
sp.style.display = 'none';
|
||
img.style.display = 'none';
|
||
ph.style.display = 'flex';
|
||
camOn = false;
|
||
document.getElementById('cam-toggle-btn').textContent = tr('btn_cam_start');
|
||
clog(tr('log_error') + ' ' + tr('cam_stream_unavailable'), 'msg-err');
|
||
};
|
||
img.src = '/api/camera/stream?t=' + Date.now();
|
||
}
|
||
}).catch(function (e) {
|
||
sp.style.display = 'none';
|
||
ph.style.display = 'flex';
|
||
clog(tr('log_error') + ' ' + e, 'msg-err');
|
||
});
|
||
}
|
||
|
||
function camStop() {
|
||
var img = document.getElementById('cam-img');
|
||
if (_camPollInterval) { clearInterval(_camPollInterval); _camPollInterval = null; }
|
||
img.onerror = null; // deregister error handler before clearing src to avoid spurious error toast
|
||
post('/api/camera/stop', {}).catch(function () { });
|
||
img.src = '';
|
||
img.style.display = 'none';
|
||
document.getElementById('cam-placeholder').style.display = 'flex';
|
||
camOn = false;
|
||
camUserStopped = true; // suppress auto-restart for remainder of this print
|
||
document.getElementById('cam-toggle-btn').textContent = tr('btn_cam_start');
|
||
var rb = document.getElementById('cam-reset-btn'); if (rb) rb.style.display = 'none';
|
||
clog(tr('log_cam_stop'), 'msg-ok');
|
||
}
|
||
|
||
function aceDryStart(aceId) {
|
||
aceId = typeof aceId === "number" && aceId >= 0 ? aceId : 0;
|
||
var prof = _aceDryProfileGet(aceId);
|
||
var t = parseInt(prof.temp, 10);
|
||
var d = _aceDryDurationMinFromSec(prof.duration_sec);
|
||
t = Math.max(30, Math.min(80, t));
|
||
d = Math.max(10, Math.min(1440, d));
|
||
return post("/api/ace/dry", {
|
||
action: "start",
|
||
target_temp: t,
|
||
duration: d,
|
||
ace_id: aceId,
|
||
})
|
||
.then(function (r) {
|
||
return r.json();
|
||
})
|
||
.then(function (r) {
|
||
if (r.error) {
|
||
throw new Error(r.error);
|
||
}
|
||
clog(
|
||
"ACE " +
|
||
(aceId + 1) +
|
||
" - " +
|
||
tr("ace_dry_dryer") +
|
||
": " +
|
||
tr("ace_dry_start") +
|
||
" (" +
|
||
t +
|
||
"°C, " +
|
||
d +
|
||
" min)",
|
||
"msg-ok"
|
||
);
|
||
poll();
|
||
})
|
||
.catch(function (e) {
|
||
clog("ACE " + tr("log_error", "Fehler:") + " " + e, "msg-err");
|
||
});
|
||
}
|
||
|
||
var _aceAutoFeedPending = {};
|
||
function aceAutoRefillToggle(aceId) {
|
||
aceId = typeof aceId === "number" && aceId >= 0 ? aceId : 0;
|
||
var on = !!(document.getElementById("ace-auto-refill-toggle-" + aceId) || {}).checked;
|
||
_aceAutoFeedPending[aceId] = true;
|
||
fetch("/api/ace/auto_feed", {
|
||
method: "POST",
|
||
headers: { "Content-Type": "application/json" },
|
||
body: JSON.stringify({ ace_id: aceId, on: on ? 1 : 0 }),
|
||
})
|
||
.then(function (r) {
|
||
return r.json();
|
||
})
|
||
.then(function (d) {
|
||
delete _aceAutoFeedPending[aceId];
|
||
if (d.error) {
|
||
clog("Auto Refill error: " + d.error, "msg-err");
|
||
var t = document.getElementById("ace-auto-refill-toggle-" + aceId);
|
||
if (t) t.checked = !on;
|
||
return;
|
||
}
|
||
clog(
|
||
"ACE " + (aceId + 1) + " - " + tr("ace_dry_auto_refill") + ": " + (on ? "ON" : "OFF"),
|
||
"msg-ok"
|
||
);
|
||
})
|
||
.catch(function (e) {
|
||
delete _aceAutoFeedPending[aceId];
|
||
clog("Auto Refill error: " + e, "msg-err");
|
||
var t = document.getElementById("ace-auto-refill-toggle-" + aceId);
|
||
if (t) t.checked = !on;
|
||
});
|
||
}
|
||
|
||
function openAceDryDialog(aceId) {
|
||
aceId = typeof aceId === "number" && aceId >= 0 ? aceId : 0;
|
||
_aceDryDialogAceId = aceId;
|
||
_syncAceDryPresetsFromServer(S.ace_dry_presets);
|
||
_aceDryDialogPresetOriginals = JSON.parse(JSON.stringify(ACE_DRY_PRESETS));
|
||
aceDryDialogSyncCustomButtonNames();
|
||
var hasStored = Object.prototype.hasOwnProperty.call(aceDryProfiles, String(aceId));
|
||
var prof = _aceDryProfileGet(aceId);
|
||
if (hasStored && prof.preset && ACE_DRY_PRESETS[prof.preset]) {
|
||
aceDryDialogPreset(prof.preset);
|
||
} else if (hasStored) {
|
||
var sec = prof.duration_sec;
|
||
document.getElementById("ace-dry-dialog-temp").value = prof.temp;
|
||
document.getElementById("ace-dry-dialog-h").value = Math.floor(sec / 3600);
|
||
document.getElementById("ace-dry-dialog-m").value = Math.floor((sec % 3600) / 60);
|
||
document.getElementById("ace-dry-dialog-s").value = sec % 60;
|
||
aceDryDialogHighlightPreset("");
|
||
} else {
|
||
aceDryDialogPreset("pla");
|
||
}
|
||
aceDryDialogUpdateSaveButton();
|
||
aceDryDialogUpdateResetButton();
|
||
var sb = document.getElementById("ace-dry-dialog-save-preset");
|
||
if (sb) {
|
||
sb.disabled = false;
|
||
sb.textContent = tr("ace_dry_dialog_save_restart");
|
||
}
|
||
document.getElementById("ace-dry-dialog").classList.add("open");
|
||
}
|
||
|
||
function closeAceDryDialog() {
|
||
_aceDryDialogAceId = -1;
|
||
_aceDryDialogPresetOriginals = {};
|
||
var sb = document.getElementById("ace-dry-dialog-save-preset");
|
||
if (sb) sb.style.display = "none";
|
||
var rb = document.getElementById("ace-dry-dialog-reset-default");
|
||
if (rb) rb.style.display = "none";
|
||
document.getElementById("ace-dry-dialog").classList.remove("open");
|
||
}
|
||
|
||
function aceDryDialogIsCustomPreset(key) {
|
||
return /^custom_[123]$/.test(String(key || ""));
|
||
}
|
||
|
||
function aceDryDialogSyncCustomButtonNames() {
|
||
["custom_1", "custom_2", "custom_3"].forEach(function (k) {
|
||
var b = document.querySelector('.ace-dry-preset-btn[data-preset="' + k + '"]');
|
||
if (b)
|
||
b.textContent = (ACE_DRY_PRESETS[k] && ACE_DRY_PRESETS[k].name) || "Custom " + k.slice(-1);
|
||
});
|
||
}
|
||
|
||
function aceDryDialogUpdateCustomNameUi() {
|
||
var row = document.getElementById("ace-dry-dialog-custom-name-row");
|
||
var input = document.getElementById("ace-dry-dialog-custom-name");
|
||
if (!row || !input) return;
|
||
if (!aceDryDialogIsCustomPreset(_aceDryDialogPresetKey)) {
|
||
row.style.display = "none";
|
||
return;
|
||
}
|
||
row.style.display = "flex";
|
||
input.value =
|
||
(ACE_DRY_PRESETS[_aceDryDialogPresetKey] && ACE_DRY_PRESETS[_aceDryDialogPresetKey].name) || "";
|
||
}
|
||
|
||
function aceDryDialogCurrentValues() {
|
||
var t = parseInt(document.getElementById("ace-dry-dialog-temp").value || 45, 10);
|
||
var h = parseInt(document.getElementById("ace-dry-dialog-h").value || 0, 10);
|
||
var m = parseInt(document.getElementById("ace-dry-dialog-m").value || 0, 10);
|
||
var s = parseInt(document.getElementById("ace-dry-dialog-s").value || 0, 10);
|
||
t = Math.max(30, Math.min(80, t));
|
||
h = Math.max(0, Math.min(24, h));
|
||
m = Math.max(0, Math.min(59, m));
|
||
s = Math.max(0, Math.min(59, s));
|
||
var totalSec = h * 3600 + m * 60 + s;
|
||
totalSec = Math.max(10 * 60, Math.min(24 * 3600, totalSec));
|
||
return { temp: t, duration_sec: totalSec };
|
||
}
|
||
|
||
function aceDryDialogUpdateSaveButton() {
|
||
var btn = document.getElementById("ace-dry-dialog-save-preset");
|
||
if (!btn) return;
|
||
var key = _aceDryDialogPresetKey || "";
|
||
if (!key || !ACE_DRY_PRESETS[key]) {
|
||
btn.style.display = "none";
|
||
return;
|
||
}
|
||
var p = _aceDryDialogPresetOriginals[key] || ACE_DRY_PRESETS[key];
|
||
var cur = aceDryDialogCurrentValues();
|
||
var changed = cur.temp !== Number(p.temp) || cur.duration_sec !== Number(p.duration_sec);
|
||
if (aceDryDialogIsCustomPreset(key)) {
|
||
var nameInp = document.getElementById("ace-dry-dialog-custom-name");
|
||
var n = ((nameInp && nameInp.value) || "").trim();
|
||
var old = (p && p.name ? String(p.name) : "Custom " + key.slice(-1)).trim();
|
||
if ((n || old) !== old) changed = true;
|
||
}
|
||
btn.style.display = changed ? "" : "none";
|
||
}
|
||
|
||
function aceDryDialogUpdateResetButton() {
|
||
var btn = document.getElementById("ace-dry-dialog-reset-default");
|
||
if (!btn) return;
|
||
var key = _aceDryDialogPresetKey || "";
|
||
var d = ACE_DRY_PRESET_DEFAULTS[key];
|
||
if (!key || !d) {
|
||
btn.style.display = "none";
|
||
return;
|
||
}
|
||
var cur = aceDryDialogCurrentValues();
|
||
var changed = cur.temp !== Number(d.temp) || cur.duration_sec !== Number(d.duration_sec);
|
||
btn.style.display = changed ? "" : "none";
|
||
}
|
||
|
||
function aceDryDialogInputsChanged() {
|
||
if (aceDryDialogIsCustomPreset(_aceDryDialogPresetKey)) {
|
||
var b = document.querySelector(
|
||
'.ace-dry-preset-btn[data-preset="' + _aceDryDialogPresetKey + '"]'
|
||
);
|
||
var i = document.getElementById("ace-dry-dialog-custom-name");
|
||
if (b && i) {
|
||
var t = (i.value || "").trim();
|
||
b.textContent =
|
||
t ||
|
||
(ACE_DRY_PRESETS[_aceDryDialogPresetKey] && ACE_DRY_PRESETS[_aceDryDialogPresetKey].name) ||
|
||
"Custom " + _aceDryDialogPresetKey.slice(-1);
|
||
}
|
||
}
|
||
aceDryDialogUpdateSaveButton();
|
||
aceDryDialogUpdateResetButton();
|
||
}
|
||
|
||
function aceDryDialogHighlightPreset(presetKey) {
|
||
_aceDryDialogPresetKey = presetKey || "";
|
||
document.querySelectorAll(".ace-dry-preset-btn").forEach(function (btn) {
|
||
var on = btn.getAttribute("data-preset") === presetKey;
|
||
btn.style.background = on ? "var(--accent)" : "var(--raised)";
|
||
btn.style.color = on ? "#fff" : "var(--txt2)";
|
||
btn.style.borderColor = on ? "var(--accent)" : "var(--border)";
|
||
});
|
||
aceDryDialogUpdateCustomNameUi();
|
||
}
|
||
|
||
function aceDryDialogPreset(presetKey) {
|
||
var p = ACE_DRY_PRESETS[presetKey];
|
||
if (!p) return;
|
||
var sec = p.duration_sec;
|
||
document.getElementById("ace-dry-dialog-temp").value = p.temp;
|
||
document.getElementById("ace-dry-dialog-h").value = Math.floor(sec / 3600);
|
||
document.getElementById("ace-dry-dialog-m").value = Math.floor((sec % 3600) / 60);
|
||
document.getElementById("ace-dry-dialog-s").value = sec % 60;
|
||
aceDryDialogHighlightPreset(presetKey);
|
||
aceDryDialogSyncCustomButtonNames();
|
||
aceDryDialogUpdateSaveButton();
|
||
aceDryDialogUpdateResetButton();
|
||
}
|
||
|
||
function resetAceDryPresetToDefault() {
|
||
var key = _aceDryDialogPresetKey || "";
|
||
var d = ACE_DRY_PRESET_DEFAULTS[key];
|
||
if (!key || !d) return;
|
||
var sec = Number(d.duration_sec) || 0;
|
||
document.getElementById("ace-dry-dialog-temp").value = Number(d.temp) || 45;
|
||
document.getElementById("ace-dry-dialog-h").value = Math.floor(sec / 3600);
|
||
document.getElementById("ace-dry-dialog-m").value = Math.floor((sec % 3600) / 60);
|
||
document.getElementById("ace-dry-dialog-s").value = sec % 60;
|
||
aceDryDialogInputsChanged();
|
||
}
|
||
|
||
function saveAceDryPresetAndRestart() {
|
||
var key = _aceDryDialogPresetKey || "";
|
||
var btn = document.getElementById("ace-dry-dialog-save-preset");
|
||
if (!key || !ACE_DRY_PRESETS[key] || !btn) return;
|
||
var cur = aceDryDialogCurrentValues();
|
||
if (!ACE_DRY_PRESETS[key]) ACE_DRY_PRESETS[key] = {};
|
||
ACE_DRY_PRESETS[key].temp = cur.temp;
|
||
ACE_DRY_PRESETS[key].duration_sec = cur.duration_sec;
|
||
if (aceDryDialogIsCustomPreset(key)) {
|
||
var nameInp = document.getElementById("ace-dry-dialog-custom-name");
|
||
var nm = ((nameInp && nameInp.value) || "").trim();
|
||
ACE_DRY_PRESETS[key].name = nm || "Custom " + key.slice(-1);
|
||
}
|
||
btn.disabled = true;
|
||
btn.textContent = "…";
|
||
fetch(_apiUrl("/api/settings"))
|
||
.then(function (r) {
|
||
return r.json();
|
||
})
|
||
.then(function (d) {
|
||
d.ace_dry_presets = {
|
||
pla: {
|
||
temp: ACE_DRY_PRESETS.pla.temp,
|
||
duration_sec: ACE_DRY_PRESETS.pla.duration_sec,
|
||
},
|
||
pla_plus: {
|
||
temp: ACE_DRY_PRESETS.pla_plus.temp,
|
||
duration_sec: ACE_DRY_PRESETS.pla_plus.duration_sec,
|
||
},
|
||
petg: {
|
||
temp: ACE_DRY_PRESETS.petg.temp,
|
||
duration_sec: ACE_DRY_PRESETS.petg.duration_sec,
|
||
},
|
||
tpu: {
|
||
temp: ACE_DRY_PRESETS.tpu.temp,
|
||
duration_sec: ACE_DRY_PRESETS.tpu.duration_sec,
|
||
},
|
||
abs_asa: {
|
||
temp: ACE_DRY_PRESETS.abs_asa.temp,
|
||
duration_sec: ACE_DRY_PRESETS.abs_asa.duration_sec,
|
||
},
|
||
pa_pc: {
|
||
temp: ACE_DRY_PRESETS.pa_pc.temp,
|
||
duration_sec: ACE_DRY_PRESETS.pa_pc.duration_sec,
|
||
},
|
||
custom_1: {
|
||
name: ACE_DRY_PRESETS.custom_1.name,
|
||
temp: ACE_DRY_PRESETS.custom_1.temp,
|
||
duration_sec: ACE_DRY_PRESETS.custom_1.duration_sec,
|
||
},
|
||
custom_2: {
|
||
name: ACE_DRY_PRESETS.custom_2.name,
|
||
temp: ACE_DRY_PRESETS.custom_2.temp,
|
||
duration_sec: ACE_DRY_PRESETS.custom_2.duration_sec,
|
||
},
|
||
custom_3: {
|
||
name: ACE_DRY_PRESETS.custom_3.name,
|
||
temp: ACE_DRY_PRESETS.custom_3.temp,
|
||
duration_sec: ACE_DRY_PRESETS.custom_3.duration_sec,
|
||
},
|
||
};
|
||
return post("/api/settings", d);
|
||
})
|
||
.then(function () {
|
||
clog("ACE preset " + key + " " + tr("settings_save"), "msg-ok");
|
||
closeAceDryDialog();
|
||
})
|
||
.catch(function (e) {
|
||
btn.disabled = false;
|
||
btn.textContent = tr("ace_dry_dialog_save_restart");
|
||
clog("ACE preset " + tr("log_error", "Fehler:") + " " + e, "msg-err");
|
||
});
|
||
}
|
||
|
||
function confirmAceDryDialog() {
|
||
if (_aceDryDialogAceId < 0) return;
|
||
var t = parseInt(document.getElementById("ace-dry-dialog-temp").value || 45, 10);
|
||
var h = parseInt(document.getElementById("ace-dry-dialog-h").value || 0, 10);
|
||
var m = parseInt(document.getElementById("ace-dry-dialog-m").value || 0, 10);
|
||
var s = parseInt(document.getElementById("ace-dry-dialog-s").value || 0, 10);
|
||
t = Math.max(30, Math.min(80, t));
|
||
h = Math.max(0, Math.min(24, h));
|
||
m = Math.max(0, Math.min(59, m));
|
||
s = Math.max(0, Math.min(59, s));
|
||
var totalSec = h * 3600 + m * 60 + s;
|
||
totalSec = Math.max(10 * 60, Math.min(24 * 3600, totalSec));
|
||
var preset = _aceDryDialogPresetKey || "";
|
||
_aceDryProfileSet(_aceDryDialogAceId, t, totalSec, preset);
|
||
closeAceDryDialog();
|
||
applyState();
|
||
}
|
||
|
||
function aceDryToggle(aceId, on) {
|
||
if (on) return aceDryStart(aceId);
|
||
return aceDryStop(aceId);
|
||
}
|
||
|
||
function toggleCam() { if (camOn) camStop(); else camStart() }
|
||
|
||
function resetCamera() {
|
||
post('/api/camera/reset', {}).then(function () {
|
||
var btn = document.getElementById('cam-reset-btn');
|
||
if (btn) { btn.textContent = '↻'; setTimeout(function () { btn.textContent = '↺'; }, 1000); }
|
||
}).catch(function () { });
|
||
}
|
||
|
||
function aceDryStop(aceId) {
|
||
aceId = typeof aceId === "number" && aceId >= 0 ? aceId : 0;
|
||
return post("/api/ace/dry", { action: "stop", ace_id: aceId })
|
||
.then(function (r) {
|
||
return r.json();
|
||
})
|
||
.then(function (r) {
|
||
if (r.error) {
|
||
throw new Error(r.error);
|
||
}
|
||
clog(
|
||
"ACE " + (aceId + 1) + " - " + tr("ace_dry_dryer") + ": " + tr("ace_dry_stop"),
|
||
"msg-ok"
|
||
);
|
||
poll();
|
||
})
|
||
.catch(function (e) {
|
||
clog("ACE " + tr("log_error", "Fehler:") + " " + e, "msg-err");
|
||
});
|
||
}
|
||
|
||
function loadStore(){
|
||
fetch(_apiUrl('/kx/files')).then(function(r){return r.json()}).then(function(d){
|
||
storeFiles=d.result||[];
|
||
// Reset any pending selection - stale ids from a deleted/renamed file
|
||
// should never carry over into a fresh file list.
|
||
storeExitSelectMode();
|
||
renderStore();
|
||
}).catch(function(e){clog('Store-Fehler: '+e,'msg-err')});
|
||
}
|
||
|
||
// Applies the store's search/filter/sort controls to storeFiles. Shared by
|
||
// renderStore() and storeToggleSelectAll() so "Select All" only selects what
|
||
// the user can currently see, not the entire (possibly filtered-out) list.
|
||
function _storeFilteredFiles(){
|
||
var q=(document.getElementById('store-search')||{value:''}).value.toLowerCase().trim();
|
||
var filter=(document.getElementById('store-filter')||{value:'all'}).value;
|
||
var sort=(document.getElementById('store-sort')||{value:'date_desc'}).value;
|
||
|
||
var files=storeFiles.filter(function(f){
|
||
if(q&&f.filename.toLowerCase().indexOf(q)===-1) return false;
|
||
if(filter==='completed'&&f.last_print_status!=='completed') return false;
|
||
if(filter==='failed'&&(f.last_print_status!=='cancelled'&&f.last_print_status!=='failed')) return false;
|
||
if(filter==='never'&&f.last_print_status) return false;
|
||
return true;
|
||
});
|
||
|
||
files.sort(function(a,b){
|
||
if(sort==='name_asc') return a.filename.localeCompare(b.filename);
|
||
if(sort==='duration_asc'){
|
||
var da=a.last_print_duration||a.est_print_time_sec||0;
|
||
var db=b.last_print_duration||b.est_print_time_sec||0;
|
||
return da-db;
|
||
}
|
||
// date_desc (default)
|
||
return (b.uploaded_at||'').localeCompare(a.uploaded_at||'');
|
||
});
|
||
return files;
|
||
}
|
||
|
||
// Applies the store's search/filter/sort controls to storeFiles. Shared by
|
||
// renderStore() and storeToggleSelectAll() so "Select All" only selects what
|
||
// the user can currently see, not the entire (possibly filtered-out) list.
|
||
function _storeFilteredFiles(){
|
||
var q=(document.getElementById('store-search')||{value:''}).value.toLowerCase().trim();
|
||
var filter=(document.getElementById('store-filter')||{value:'all'}).value;
|
||
var sort=(document.getElementById('store-sort')||{value:'date_desc'}).value;
|
||
|
||
var files=storeFiles.filter(function(f){
|
||
if(q&&f.filename.toLowerCase().indexOf(q)===-1) return false;
|
||
if(filter==='completed'&&f.last_print_status!=='completed') return false;
|
||
if(filter==='failed'&&(f.last_print_status!=='cancelled'&&f.last_print_status!=='failed')) return false;
|
||
if(filter==='never'&&f.last_print_status) return false;
|
||
return true;
|
||
});
|
||
|
||
files.sort(function(a,b){
|
||
if(sort==='name_asc') return a.filename.localeCompare(b.filename);
|
||
if(sort==='duration_asc'){
|
||
var da=a.last_print_duration||a.est_print_time_sec||0;
|
||
var db=b.last_print_duration||b.est_print_time_sec||0;
|
||
return da-db;
|
||
}
|
||
// date_desc (default)
|
||
return (b.uploaded_at||'').localeCompare(a.uploaded_at||'');
|
||
});
|
||
return files;
|
||
}
|
||
|
||
function uploadGcode(file){
|
||
if(!file) return;
|
||
var zone=document.getElementById('store-upload-zone');
|
||
var status=document.getElementById('store-upload-status');
|
||
var label=document.getElementById('store-upload-label');
|
||
// Nur druckbare Dateien zulassen (Issue #59) — Drag&Drop umgeht das
|
||
// accept-Attribut, daher hier explizit prüfen.
|
||
var _fn = (file.name || "").toLowerCase();
|
||
if (!/\.(gcode|gcode\.3mf|3mf|bgcode)$/.test(_fn)) {
|
||
if (status) {
|
||
status.textContent = T.store_upload_only_gcode || "Only GCode files allowed";
|
||
status.style.display = "";
|
||
status.className = "upload-status-err";
|
||
}
|
||
clog("Upload abgelehnt (kein GCode): " + file.name, "msg-err");
|
||
return;
|
||
}
|
||
if (status) {
|
||
status.textContent = T.store_upload_busy;
|
||
status.style.display = "";
|
||
status.className = "upload-status-busy";
|
||
}
|
||
if (label) label.style.display = "none";
|
||
if (zone) zone.style.pointerEvents = "none";
|
||
var fd = new FormData();
|
||
fd.append("file", file);
|
||
fd.append("web_upload", "true");
|
||
fetch(_apiUrl("/api/files/local"), { method: "POST", body: fd })
|
||
.then(function (r) {
|
||
if (!r.ok)
|
||
return r.text().then(function (t) {
|
||
throw new Error(r.status + ": " + t);
|
||
});
|
||
return r.json();
|
||
})
|
||
.then(function () {
|
||
if (status) {
|
||
status.textContent = T.store_upload_success.replace("{file}", file.name);
|
||
status.className = "upload-status-ok";
|
||
}
|
||
loadStore();
|
||
setTimeout(function () {
|
||
if (status) {
|
||
status.style.display = "none";
|
||
status.className = "";
|
||
}
|
||
if (label) label.style.display = "";
|
||
if (zone) zone.style.pointerEvents = "";
|
||
}, 3000);
|
||
})
|
||
.catch(function (e) {
|
||
if (status) {
|
||
status.textContent = T.store_upload_error.replace("{error}", e.message);
|
||
status.className = "upload-status-err";
|
||
}
|
||
if (label) label.style.display = "";
|
||
if (zone) zone.style.pointerEvents = "";
|
||
clog(tr("log_error", "Fehler:") + " " + e, "msg-err");
|
||
});
|
||
}
|
||
|
||
function renderStore(){
|
||
var grid=document.getElementById('store-grid');
|
||
var empty=document.getElementById('store-empty');
|
||
var files=_storeFilteredFiles();
|
||
|
||
if (!storeFiles.length) {
|
||
empty.textContent = T.store_empty;
|
||
grid.innerHTML = "";
|
||
empty.style.display = "block";
|
||
return;
|
||
}
|
||
if (!files.length) {
|
||
empty.textContent = T.store_no_results;
|
||
grid.innerHTML = "";
|
||
empty.style.display = "block";
|
||
return;
|
||
}
|
||
empty.style.display='none';
|
||
grid.innerHTML=files.map(function(f){
|
||
var thumb=f.thumbnail_b64
|
||
? '<img src="data:image/png;base64,'+f.thumbnail_b64+'" style="width:100%;height:130px;object-fit:cover;border-radius:6px;display:block;margin-bottom:8px">'
|
||
: '<div style="width:100%;height:130px;background:var(--raised);border-radius:6px;display:flex;align-items:center;justify-content:center;margin-bottom:8px;font-size:32px">🖨</div>';
|
||
var name=f.filename.length>28?f.filename.slice(0,25)+'…':f.filename;
|
||
var date=f.uploaded_at?f.uploaded_at.replace('T',' ').slice(0,16):'';
|
||
var est=f.est_print_time_sec?formatDur(f.est_print_time_sec):'–';
|
||
var statusBadge='';
|
||
var lastInfo='';
|
||
if(f.last_print_status==='completed'){
|
||
statusBadge='<span style="background:#2a7a3b;color:#fff;border-radius:4px;padding:1px 6px;font-size:10px;margin-left:4px">✓</span>';
|
||
if(f.last_print_duration) lastInfo='<div style="font-size:11px;color:var(--ok);margin-bottom:2px">✓ '+formatDur(f.last_print_duration)+'</div>';
|
||
} else if(f.last_print_status==='cancelled'||f.last_print_status==='failed'){
|
||
statusBadge='<span style="background:#a04020;color:#fff;border-radius:4px;padding:1px 6px;font-size:10px;margin-left:4px">✗</span>';
|
||
lastInfo='<div style="font-size:11px;color:var(--err);margin-bottom:2px">✗ '+f.last_print_status+'</div>';
|
||
} else if(!f.last_print_status){
|
||
lastInfo='<div style="font-size:11px;color:var(--txt2);margin-bottom:2px;opacity:.6">'+T.store_never+'</div>';
|
||
}
|
||
var isSelected=!!_storeSelected[f.id];
|
||
var selectBorder=isSelected?'border:2px solid var(--accent);padding:9px':'border:1px solid var(--border);padding:10px';
|
||
// Always visible (not just once select mode is on) - otherwise there is
|
||
// no way to ever enter select mode, since it's normally entered by
|
||
// clicking this very checkbox. White circle behind it so it stays
|
||
// legible against any thumbnail.
|
||
var checkbox='<span style="position:absolute;top:6px;left:6px;width:22px;height:22px;'+
|
||
'border-radius:50%;background:rgba(255,255,255,.85);z-index:2;display:flex;'+
|
||
'align-items:center;justify-content:center">'+
|
||
'<input type="checkbox" class="store-card-cb" '+(isSelected?'checked':'')+
|
||
' onclick="event.stopPropagation();storeToggleSelect(\''+f.id+'\')" '+
|
||
'style="width:16px;height:16px;margin:0"></span>';
|
||
var cardClick=_storeSelectMode?'onclick="storeToggleSelect(\''+f.id+'\')" style="cursor:pointer;position:relative;background:var(--raised);border-radius:8px;'+selectBorder+';display:flex;flex-direction:column"':
|
||
'style="position:relative;background:var(--raised);border-radius:8px;'+selectBorder+';display:flex;flex-direction:column"';
|
||
return '<div '+cardClick+'>'+
|
||
checkbox+
|
||
thumb+
|
||
'<div title="'+f.filename+'" style="font-size:12px;font-weight:600;margin-bottom:4px;color:var(--txt)">'+name+statusBadge+'</div>'+
|
||
lastInfo+
|
||
'<div style="font-size:11px;color:var(--txt2);margin-bottom:2px">⏱ '+T.store_estimate+': '+est+'</div>'+
|
||
'<div style="font-size:11px;color:var(--txt2);margin-bottom:8px">📅 '+date+'</div>'+
|
||
'<div style="display:flex;gap:6px;margin-top:auto">'+
|
||
'<button onclick="event.stopPropagation();storePrint(\''+f.id+'\',\''+f.filename.replace(/'/g,"\\'")+'\')" '+
|
||
'style="flex:1;font-size:12px;padding:5px;background:var(--accent);color:#fff;border:none;border-radius:6px;cursor:pointer">'+T.store_print+'</button>'+
|
||
'<button onclick="event.stopPropagation();storeDownload(\''+f.id+'\')" title="'+T.store_download+'" '+
|
||
'style="font-size:12px;padding:5px 8px;background:var(--raised);border:1px solid var(--border);border-radius:6px;color:var(--txt2);cursor:pointer">⬇</button>'+
|
||
'<button onclick="event.stopPropagation();storeDelete(\''+f.id+'\')" '+
|
||
'style="font-size:12px;padding:5px 8px;background:var(--raised);border:1px solid var(--border);border-radius:6px;color:var(--txt2);cursor:pointer">🗑</button>'+
|
||
'</div>'+
|
||
'</div>';
|
||
}).join('');
|
||
|
||
_storeUpdateSelectBar(files);
|
||
}
|
||
|
||
// Reflects the current selection onto the select-all checkbox (incl.
|
||
// indeterminate state), the selected-count label, and the delete button's
|
||
// disabled state. `files` is the currently-filtered/visible list.
|
||
function _storeUpdateSelectBar(files){
|
||
var selectAll=document.getElementById('store-select-all');
|
||
var countEl=document.getElementById('store-selected-count');
|
||
var delBtn=document.getElementById('store-delete-selected-btn');
|
||
if(!selectAll||!countEl||!delBtn)return;
|
||
var visibleIds=files.map(function(f){return f.id;});
|
||
var selectedVisible=visibleIds.filter(function(id){return _storeSelected[id];});
|
||
var n=selectedVisible.length;
|
||
selectAll.checked=n>0&&n===visibleIds.length;
|
||
selectAll.indeterminate=n>0&&n<visibleIds.length;
|
||
countEl.textContent=n>0?(T.store_selected_count||'{n} selected').replace('{n}',n):'';
|
||
delBtn.disabled=n===0;
|
||
}
|
||
|
||
function formatDur(sec) {
|
||
var h = Math.floor(sec / 3600),
|
||
m = Math.floor((sec % 3600) / 60);
|
||
return h ? h + "h " + m + "m" : m + "m";
|
||
}
|
||
|
||
var _storeFileId = null;
|
||
var _storeFilename = null;
|
||
var _filamentDialogMode = "store"; // 'store' oder 'banner'
|
||
var _pendingWebVerifyFileId = null;
|
||
var _pendingWebVerifyFilename = "";
|
||
var _pendingWebVerifyAction = null;
|
||
var _pendingWebVerifyAutoOpen = false;
|
||
// GCode-Store-Dateiliste. MUSS deklariert sein – sonst ReferenceError, wenn
|
||
// "Slots wählen" im Banner geklickt wird, bevor der Browser-Tab je geladen
|
||
// wurde (Issue #29 / Theme-Auslagerung PR #27).
|
||
var storeFiles=[];
|
||
|
||
// Multi-select state for the GCode browser (Issue #94).
|
||
var _storeSelectMode=false;
|
||
var _storeSelected={}; // file.id -> true
|
||
|
||
function storeToggleSelect(id){
|
||
if(!_storeSelectMode){
|
||
_storeSelectMode=true;
|
||
var bar=document.getElementById('store-select-bar');
|
||
if(bar)bar.style.display='flex';
|
||
}
|
||
if(_storeSelected[id])delete _storeSelected[id];
|
||
else _storeSelected[id]=true;
|
||
renderStore();
|
||
}
|
||
|
||
function storeToggleSelectAll(checked){
|
||
// Only the currently filtered/visible files are affected, so search/filter
|
||
// never causes "Select All" to silently pick up hidden files too.
|
||
var visible=_storeFilteredFiles();
|
||
_storeSelected={};
|
||
if(checked)visible.forEach(function(f){_storeSelected[f.id]=true;});
|
||
renderStore();
|
||
}
|
||
|
||
function storeExitSelectMode(){
|
||
_storeSelectMode=false;
|
||
_storeSelected={};
|
||
var bar=document.getElementById('store-select-bar');
|
||
if(bar)bar.style.display='none';
|
||
renderStore();
|
||
}
|
||
|
||
function storeDeleteSelected(){
|
||
var ids=Object.keys(_storeSelected);
|
||
if(!ids.length)return;
|
||
if(!confirm((T.store_delete_selected_confirm||'Delete {n} selected files?').replace('{n}',ids.length)))return;
|
||
Promise.all(ids.map(function(id){
|
||
return fetch(_apiUrl('/kx/files/'+id),{method:'DELETE'}).then(function(r){return{id:id,ok:r.ok};});
|
||
})).then(function(results){
|
||
var failed=results.filter(function(r){return !r.ok;});
|
||
if(failed.length)clog((T.log_delete_failed||'Delete failed')+': '+failed.length,'msg-err');
|
||
storeExitSelectMode();
|
||
loadStore();
|
||
});
|
||
}
|
||
|
||
// ── Files on the printer's own internal storage (Issue: 2nd Browser tab) ──
|
||
// Uses filename as the identity key (the printer has no numeric file id like
|
||
// the bridge's own GCodeStore) and a single batch-delete call, since the
|
||
// printer's file/deleteBatch MQTT action natively accepts a filename list.
|
||
var printerFiles=[];
|
||
var _printerFilesSelectMode=false;
|
||
var _printerFilesSelected={}; // filename -> true
|
||
|
||
function loadPrinterFiles(){
|
||
var errEl=document.getElementById('printer-store-error');
|
||
if(errEl)errEl.style.display='none';
|
||
fetch(_apiUrl('/kx/printer-files')).then(function(r){return r.json()}).then(function(d){
|
||
_printerFilesLoaded=true;
|
||
if(d.error){
|
||
printerFiles=[];
|
||
if(errEl){errEl.textContent=T.printer_store_unreachable||d.error;errEl.style.display='block';}
|
||
renderPrinterFiles();
|
||
return;
|
||
}
|
||
printerFiles=d.result||[];
|
||
_printerFilesSelected={};
|
||
_printerFilesSelectMode=false;
|
||
var bar=document.getElementById('printer-store-select-bar');
|
||
if(bar)bar.style.display='none';
|
||
renderPrinterFiles();
|
||
}).catch(function(e){
|
||
_printerFilesLoaded=true;
|
||
if(errEl){errEl.textContent=T.printer_store_unreachable||String(e);errEl.style.display='block';}
|
||
});
|
||
}
|
||
|
||
function renderPrinterFiles(){
|
||
var grid=document.getElementById('printer-store-grid');
|
||
var empty=document.getElementById('printer-store-empty');
|
||
if(!grid||!empty)return;
|
||
if(!printerFiles.length){
|
||
empty.textContent=T.printer_store_empty||'No files on the printer.';
|
||
grid.innerHTML='';
|
||
empty.style.display='block';
|
||
return;
|
||
}
|
||
empty.style.display='none';
|
||
grid.innerHTML=printerFiles.map(function(f,idx){
|
||
var name=f.filename.length>28?f.filename.slice(0,25)+'…':f.filename;
|
||
var sizeKb=f.size?(f.size/1024).toFixed(0)+' KB':'–';
|
||
var date=f.timestamp?new Date(f.timestamp).toISOString().replace('T',' ').slice(0,16):'';
|
||
var isSelected=!!_printerFilesSelected[f.filename];
|
||
var selectBorder=isSelected?'border:2px solid var(--accent);padding:9px':'border:1px solid var(--border);padding:10px';
|
||
var checkbox='<span style="position:absolute;top:6px;left:6px;width:22px;height:22px;'+
|
||
'border-radius:50%;background:rgba(255,255,255,.85);z-index:2;display:flex;'+
|
||
'align-items:center;justify-content:center">'+
|
||
'<input type="checkbox" class="store-card-cb" '+(isSelected?'checked':'')+
|
||
' onclick="event.stopPropagation();printerFileToggleSelect(\''+f.filename.replace(/'/g,"\\'")+'\')" '+
|
||
'style="width:16px;height:16px;margin:0"></span>';
|
||
var cardClick=_printerFilesSelectMode?'onclick="printerFileToggleSelect(\''+f.filename.replace(/'/g,"\\'")+'\')" style="cursor:pointer;position:relative;background:var(--raised);border-radius:8px;'+selectBorder+';display:flex;flex-direction:column"':
|
||
'style="position:relative;background:var(--raised);border-radius:8px;'+selectBorder+';display:flex;flex-direction:column"';
|
||
var thumbId='pft-'+idx;
|
||
var cachedThumb=_printerThumbCache[f.filename];
|
||
var thumbHtml=cachedThumb
|
||
? '<img id="'+thumbId+'" src="data:image/png;base64,'+cachedThumb+'" style="width:100%;height:60px;object-fit:contain;border-radius:6px;background:var(--raised);margin-bottom:8px">'
|
||
: '<div id="'+thumbId+'" data-filename="'+encodeURIComponent(f.filename)+'" style="width:100%;height:60px;background:var(--raised);border-radius:6px;display:flex;align-items:center;justify-content:center;margin-bottom:8px;font-size:28px">🖨</div>';
|
||
return '<div '+cardClick+'>'+
|
||
checkbox+
|
||
thumbHtml+
|
||
'<div title="'+f.filename+'" style="font-size:12px;font-weight:600;margin-bottom:4px;color:var(--txt)">'+name+'</div>'+
|
||
'<div style="font-size:11px;color:var(--txt2);margin-bottom:2px">💾 '+sizeKb+'</div>'+
|
||
'<div style="font-size:11px;color:var(--txt2);margin-bottom:8px">📅 '+date+'</div>'+
|
||
'<div style="display:flex;gap:6px;margin-top:auto">'+
|
||
'<button onclick="event.stopPropagation();printerFileDelete(\''+f.filename.replace(/'/g,"\\'")+'\')" '+
|
||
'style="flex:1;font-size:12px;padding:5px 8px;background:var(--raised);border:1px solid var(--border);border-radius:6px;color:var(--txt2);cursor:pointer">🗑</button>'+
|
||
'</div>'+
|
||
'</div>';
|
||
}).join('');
|
||
_printerFilesUpdateSelectBar();
|
||
_loadVisiblePrinterThumbnails();
|
||
}
|
||
|
||
// Thumbnails are fetched lazily, one at a time, only for cards not already
|
||
// cached - fetching all of them upfront would mean one MQTT roundtrip per
|
||
// file (145+ files is common), overwhelming the single MQTT connection.
|
||
var _printerThumbCache={}; // filename -> base64 PNG string ("" = no thumbnail)
|
||
var _printerThumbQueue=[];
|
||
var _printerThumbLoading=false;
|
||
|
||
function _loadVisiblePrinterThumbnails(){
|
||
var placeholders=document.querySelectorAll('#printer-store-grid [data-filename]');
|
||
_printerThumbQueue=Array.prototype.slice.call(placeholders);
|
||
_pumpPrinterThumbQueue();
|
||
}
|
||
|
||
function _pumpPrinterThumbQueue(){
|
||
if(_printerThumbLoading||!_printerThumbQueue.length)return;
|
||
var el=_printerThumbQueue.shift();
|
||
if(!el||!el.isConnected){_pumpPrinterThumbQueue();return;}
|
||
var encodedName=el.getAttribute('data-filename');
|
||
_printerThumbLoading=true;
|
||
fetch(_apiUrl('/kx/printer-files/'+encodedName+'/thumbnail'))
|
||
.then(function(r){return r.json()})
|
||
.then(function(d){
|
||
var thumb=(d.result&&d.result.thumbnail)||'';
|
||
_printerThumbCache[decodeURIComponent(encodedName)]=thumb;
|
||
if(thumb&&el.isConnected){
|
||
var img=document.createElement('img');
|
||
img.src='data:image/png;base64,'+thumb;
|
||
img.style.cssText='width:100%;height:60px;object-fit:contain;border-radius:6px;background:var(--raised);margin-bottom:8px';
|
||
img.id=el.id;
|
||
el.replaceWith(img);
|
||
}
|
||
})
|
||
.catch(function(){/* keep the placeholder icon on failure */})
|
||
.finally(function(){
|
||
_printerThumbLoading=false;
|
||
_pumpPrinterThumbQueue();
|
||
});
|
||
}
|
||
|
||
function _printerFilesUpdateSelectBar(){
|
||
var selectAll=document.getElementById('printer-store-select-all');
|
||
var countEl=document.getElementById('printer-store-selected-count');
|
||
var delBtn=document.getElementById('printer-store-delete-selected-btn');
|
||
if(!selectAll||!countEl||!delBtn)return;
|
||
var allNames=printerFiles.map(function(f){return f.filename;});
|
||
var selected=allNames.filter(function(n){return _printerFilesSelected[n];});
|
||
var n=selected.length;
|
||
selectAll.checked=n>0&&n===allNames.length;
|
||
selectAll.indeterminate=n>0&&n<allNames.length;
|
||
countEl.textContent=n>0?(T.store_selected_count||'{n} selected').replace('{n}',n):'';
|
||
delBtn.disabled=n===0;
|
||
}
|
||
|
||
function printerFileToggleSelect(filename){
|
||
if(!_printerFilesSelectMode){
|
||
_printerFilesSelectMode=true;
|
||
var bar=document.getElementById('printer-store-select-bar');
|
||
if(bar)bar.style.display='flex';
|
||
}
|
||
if(_printerFilesSelected[filename])delete _printerFilesSelected[filename];
|
||
else _printerFilesSelected[filename]=true;
|
||
renderPrinterFiles();
|
||
}
|
||
|
||
function printerFileToggleSelectAll(checked){
|
||
_printerFilesSelected={};
|
||
if(checked)printerFiles.forEach(function(f){_printerFilesSelected[f.filename]=true;});
|
||
renderPrinterFiles();
|
||
}
|
||
|
||
function printerFileExitSelectMode(){
|
||
_printerFilesSelectMode=false;
|
||
_printerFilesSelected={};
|
||
var bar=document.getElementById('printer-store-select-bar');
|
||
if(bar)bar.style.display='none';
|
||
renderPrinterFiles();
|
||
}
|
||
|
||
function _printerFilesDeleteRequest(filenames){
|
||
return fetch(_apiUrl('/kx/printer-files/delete'),{
|
||
method:'POST',
|
||
headers:{'Content-Type':'application/json'},
|
||
body:JSON.stringify({filenames:filenames}),
|
||
}).then(function(r){return r.json().then(function(d){return{ok:r.ok,body:d};});});
|
||
}
|
||
|
||
function printerFileDelete(filename){
|
||
if(!confirm(T.printer_store_delete_confirm||'Delete file?'))return;
|
||
_printerFilesDeleteRequest([filename]).then(function(res){
|
||
if(!res.ok){clog((T.log_delete_failed||'Delete failed')+': '+(res.body.error||''),'msg-err');return;}
|
||
loadPrinterFiles();
|
||
});
|
||
}
|
||
|
||
function printerFileDeleteSelected(){
|
||
var names=Object.keys(_printerFilesSelected);
|
||
if(!names.length)return;
|
||
if(!confirm((T.printer_store_delete_selected_confirm||'Delete {n} selected files?').replace('{n}',names.length)))return;
|
||
_printerFilesDeleteRequest(names).then(function(res){
|
||
if(!res.ok){clog((T.log_delete_failed||'Delete failed')+': '+(res.body.error||''),'msg-err');return;}
|
||
printerFileExitSelectMode();
|
||
loadPrinterFiles();
|
||
});
|
||
}
|
||
|
||
var _gcodeFilaments=[];
|
||
|
||
function _setGcodeFilamentsFromFileObj(fileObj){
|
||
try{
|
||
if(fileObj&&Array.isArray(fileObj.gcode_filaments)){
|
||
_gcodeFilaments=fileObj.gcode_filaments;
|
||
}else if(fileObj&&typeof fileObj.gcode_filaments==='string'&&fileObj.gcode_filaments){
|
||
_gcodeFilaments=JSON.parse(fileObj.gcode_filaments);
|
||
}else{
|
||
_gcodeFilaments=[];
|
||
}
|
||
} catch (e) {
|
||
_gcodeFilaments = [];
|
||
}
|
||
}
|
||
|
||
function storePrint(fileId, filename) {
|
||
_storeFileId = fileId;
|
||
_storeFilename = filename;
|
||
_filamentDialogMode = "store";
|
||
var fileObj = storeFiles.find(function (f) {
|
||
return f.id === fileId;
|
||
});
|
||
openStorePrintDialog(fileId, filename, fileObj);
|
||
}
|
||
|
||
function openStorePrintDialog(fileId, filename, fileObj) {
|
||
_storeFileId = fileId;
|
||
_storeFilename = filename;
|
||
_filamentDialogMode = "store";
|
||
maybeGateWebUpload(fileObj, function () {
|
||
// GCode-Filamente aus Store-Datei holen (für Vorschau im Dialog)
|
||
_setGcodeFilamentsFromFileObj(fileObj);
|
||
fetch(_apiUrl("/kx/filament/slots"))
|
||
.then(function (r) {
|
||
return r.json();
|
||
})
|
||
.then(function (d) {
|
||
openFilamentDialog(d.result || []);
|
||
})
|
||
.catch(function () {
|
||
openFilamentDialog([]);
|
||
});
|
||
});
|
||
}
|
||
|
||
function webUploadWarningEnabled() {
|
||
return S.web_upload_warning === undefined ? true : !!S.web_upload_warning;
|
||
}
|
||
|
||
function clearWebUploadWarningFlag(fileId, onDone) {
|
||
if (!fileId) {
|
||
if (onDone) onDone();
|
||
return;
|
||
}
|
||
fetch(_apiUrl("/kx/files/" + encodeURIComponent(fileId) + "/verify"), {
|
||
method: "POST",
|
||
})
|
||
.then(function (r) {
|
||
if (!r.ok)
|
||
return r.text().then(function (t) {
|
||
throw new Error(r.status + ": " + t);
|
||
});
|
||
return r.json();
|
||
})
|
||
.then(function () {
|
||
var fileObj = (storeFiles || []).find(function (f) {
|
||
return f.id === fileId;
|
||
});
|
||
if (fileObj) {
|
||
fileObj.web_unverified = false;
|
||
}
|
||
if (onDone) onDone();
|
||
loadStore();
|
||
})
|
||
.catch(function (e) {
|
||
clog(tr("log_error", "Fehler:") + " " + e, "msg-err");
|
||
});
|
||
}
|
||
|
||
function maybeGateWebUpload(fileObj, onContinue, opts) {
|
||
opts = opts || {};
|
||
if (!fileObj || !fileObj.web_unverified) {
|
||
if (onContinue) onContinue();
|
||
return;
|
||
}
|
||
if (!webUploadWarningEnabled()) {
|
||
if (onContinue) onContinue();
|
||
return;
|
||
}
|
||
var cancelledId = sessionStorage.getItem("webVerifyCancelledFileId") || "";
|
||
if (opts.autoOpen && cancelledId && cancelledId === String(fileObj.id || "")) {
|
||
return;
|
||
}
|
||
openWebVerifyDialog(
|
||
fileObj.id,
|
||
fileObj.filename,
|
||
function () {
|
||
clearWebUploadWarningFlag(fileObj.id, onContinue);
|
||
},
|
||
!!opts.autoOpen
|
||
);
|
||
}
|
||
|
||
function openWebVerifyDialog(fileId, filename, onConfirm, autoOpen) {
|
||
_pendingWebVerifyFileId = fileId;
|
||
_pendingWebVerifyFilename = filename;
|
||
_pendingWebVerifyAction = onConfirm || null;
|
||
_pendingWebVerifyAutoOpen = !!autoOpen;
|
||
var status = document.getElementById("store-web-verify-status");
|
||
if (status) {
|
||
status.textContent = "";
|
||
}
|
||
openStoreWebVerifyDialog();
|
||
}
|
||
|
||
function openStoreWebVerifyDialog() {
|
||
var modal = document.getElementById("store-web-verify-dialog");
|
||
if (modal) {
|
||
modal.classList.add("open");
|
||
}
|
||
}
|
||
|
||
function closeStoreWebVerifyDialog() {
|
||
var modal = document.getElementById("store-web-verify-dialog");
|
||
if (modal) {
|
||
modal.classList.remove("open");
|
||
}
|
||
if (_pendingWebVerifyAutoOpen && _pendingWebVerifyFileId) {
|
||
sessionStorage.setItem("webVerifyCancelledFileId", String(_pendingWebVerifyFileId));
|
||
}
|
||
_pendingWebVerifyFileId = null;
|
||
_pendingWebVerifyFilename = "";
|
||
_pendingWebVerifyAction = null;
|
||
_pendingWebVerifyAutoOpen = false;
|
||
}
|
||
|
||
function confirmStoreWebVerify() {
|
||
if (!_pendingWebVerifyFileId || !_pendingWebVerifyFilename) {
|
||
closeStoreWebVerifyDialog();
|
||
return;
|
||
}
|
||
var fileId = _pendingWebVerifyFileId;
|
||
var action = _pendingWebVerifyAction;
|
||
var status = document.getElementById("store-web-verify-status");
|
||
if (status) {
|
||
status.textContent = "…";
|
||
}
|
||
fetch(_apiUrl("/kx/files/" + encodeURIComponent(fileId) + "/verify"), {
|
||
method: "POST",
|
||
})
|
||
.then(function (r) {
|
||
if (!r.ok)
|
||
return r.text().then(function (t) {
|
||
throw new Error(r.status + ": " + t);
|
||
});
|
||
return r.json();
|
||
})
|
||
.then(function () {
|
||
var fileObj = (storeFiles || []).find(function (f) {
|
||
return f.id === fileId;
|
||
});
|
||
if (fileObj) {
|
||
fileObj.web_unverified = false;
|
||
}
|
||
sessionStorage.removeItem("webVerifyCancelledFileId");
|
||
_pendingWebVerifyFileId = null;
|
||
_pendingWebVerifyFilename = "";
|
||
_pendingWebVerifyAction = null;
|
||
_pendingWebVerifyAutoOpen = false;
|
||
closeStoreWebVerifyDialog();
|
||
loadStore();
|
||
if (typeof action === "function") action();
|
||
})
|
||
.catch(function (e) {
|
||
if (status) {
|
||
status.textContent = "✗ " + e.message;
|
||
}
|
||
clog(tr("log_error", "Fehler:") + " " + e, "msg-err");
|
||
});
|
||
}
|
||
|
||
function _showMismatchWarn(mismatches) {
|
||
var el = document.getElementById('fd-mismatch-warn');
|
||
if (!el) return;
|
||
if (!mismatches || !mismatches.length) { el.style.display = 'none'; el.innerHTML = ''; return; }
|
||
var lines = mismatches.map(function (m) {
|
||
var slot = 'Slot ' + (m.slot_index + 1);
|
||
if (m.reason === 'empty')
|
||
return '⚠ ' + slot + ': GCode needs ' + m.gcode_material + ' — slot is empty';
|
||
return '⚠ ' + slot + ': GCode needs ' + m.gcode_material + ', loaded: ' + (m.ams_material || '?');
|
||
});
|
||
el.innerHTML = '<strong>Filament mismatch detected</strong><br>' + lines.join('<br>');
|
||
el.style.display = '';
|
||
}
|
||
function startReadyFileWithSlots(filename, _autoOpen, _mismatch) {
|
||
if (!_autoOpen) _fdAutoOpenedFile = null; // manueller Aufruf → Auto-Open-Sperre aufheben
|
||
var fn = filename || S.file_ready;
|
||
var currentFile = (storeFiles || []).find(function (f) { return f.filename === fn; });
|
||
if (currentFile && currentFile.web_unverified && webUploadWarningEnabled()) {
|
||
maybeGateWebUpload(currentFile, function () { startReadyFileWithSlots(fn, _autoOpen); }, { autoOpen: !!_autoOpen });
|
||
return;
|
||
}
|
||
_filamentDialogMode = "banner";
|
||
_storeFilename = fn || "";
|
||
// Banner must never reuse stale store-file context.
|
||
_storeFileId = null;
|
||
_gcodeFilaments = [];
|
||
|
||
var _autoOpenFile = _autoOpen ? fn : null;
|
||
if (_autoOpen) _fdDialogOpen = true; // bereits während Fetch sperren
|
||
function openWithSlots() {
|
||
fetch(_apiUrl('/kx/filament/slots')).then(function (r) { return r.json() }).then(function (d) {
|
||
if (_autoOpenFile && _fdUserCancelled) { _fdDialogOpen = false; return; }
|
||
openFilamentDialog(d.result || []);
|
||
_showMismatchWarn(_mismatch || null);
|
||
}).catch(function () {
|
||
if (_autoOpenFile && _fdUserCancelled) { _fdDialogOpen = false; return; }
|
||
openFilamentDialog([]);
|
||
_showMismatchWarn(_mismatch || null);
|
||
});
|
||
}
|
||
|
||
function _proceedWithFileObj(fileObj) {
|
||
if (fileObj && fileObj.web_unverified && webUploadWarningEnabled()) {
|
||
// Verify-Gate war beim ersten Lookup noch nicht aktiv (storeFiles leer) — jetzt prüfen.
|
||
if (_autoOpen) {
|
||
_fdDialogOpen = false;
|
||
}
|
||
maybeGateWebUpload(
|
||
fileObj,
|
||
function () {
|
||
startReadyFileWithSlots(fn, _autoOpen);
|
||
},
|
||
{ autoOpen: !!_autoOpen }
|
||
);
|
||
return;
|
||
}
|
||
if (fileObj) {
|
||
_storeFileId = fileObj.id;
|
||
_setGcodeFilamentsFromFileObj(fileObj);
|
||
}
|
||
openWithSlots();
|
||
}
|
||
|
||
var fileObj = (storeFiles || []).find(function (f) {
|
||
return f.filename === _storeFilename;
|
||
});
|
||
if (fileObj) {
|
||
_proceedWithFileObj(fileObj);
|
||
return;
|
||
}
|
||
|
||
// Fallback: refresh file list, then resolve current file by filename.
|
||
fetch(_apiUrl("/kx/files"))
|
||
.then(function (r) {
|
||
return r.json();
|
||
})
|
||
.then(function (d) {
|
||
storeFiles = d.result || [];
|
||
var refreshed =
|
||
(storeFiles || []).find(function (f) {
|
||
return f.filename === _storeFilename;
|
||
}) || null;
|
||
_proceedWithFileObj(refreshed);
|
||
})
|
||
.catch(function () {
|
||
openWithSlots();
|
||
});
|
||
}
|
||
|
||
var _amsSlots = [];
|
||
var _printObjects = []; // [{name, skip}] für aktuell offenen Dialog
|
||
var _printObjectsSvg = ""; // base64-SVG aus DB für Visualisierung
|
||
|
||
// Hilfsfunktionen für farbige Kanal/Slot-Marker (Issue #23)
|
||
function _contrastText(hex) {
|
||
// Helle Farbe → dunkler Text, dunkle Farbe → heller Text
|
||
var c = (hex || "").replace("#", "");
|
||
if (c.length === 3) c = c[0] + c[0] + c[1] + c[1] + c[2] + c[2];
|
||
if (c.length < 6) return "#fff";
|
||
var r = parseInt(c.slice(0, 2), 16),
|
||
g = parseInt(c.slice(2, 4), 16),
|
||
b = parseInt(c.slice(4, 6), 16);
|
||
// YIQ-Helligkeit
|
||
var y = (r * 299 + g * 587 + b * 114) / 1000;
|
||
return y >= 140 ? "#111" : "#fff";
|
||
}
|
||
function _normalizeMaterialKey(material) {
|
||
var key = (material || "").toUpperCase().replace(/[^A-Z0-9+]/g, "");
|
||
// Orca often uses PLA for PLA+, while AMS may report PLA+.
|
||
if (key === "PLA+" || key === "PLAPLUS") return "PLA";
|
||
// Handle modifier+base patterns in either order: "Matte PLA", "Silk PETG",
|
||
// "PLA Silk", "PLA Matte". OrcaSlicer always writes the base type in GCode
|
||
// (filament_type = PLA), but users label slots with the full product-style name.
|
||
var trimmed = (material || "").trim();
|
||
if (trimmed.indexOf(" ") >= 0) {
|
||
var words = trimmed.toUpperCase().split(/\s+/);
|
||
for (var i = 0; i < words.length; i++) {
|
||
var w = words[i].replace(/[^A-Z0-9+]/g, "");
|
||
if (_BASE_MATERIAL_TYPES.indexOf(w) >= 0) return w;
|
||
}
|
||
}
|
||
return key;
|
||
}
|
||
function _materialsCompatible(a, b) {
|
||
return _normalizeMaterialKey(a) === _normalizeMaterialKey(b);
|
||
}
|
||
// Issue #57 Punkt 4: konkreter Profilname (User-Override) statt generischem Typ.
|
||
// Fällt auf den Material-Typ zurück wenn kein Profil gemappt ist.
|
||
function _slotProfileLabel(slot) {
|
||
if (!slot) return "";
|
||
if (slot.filament_name) {
|
||
return slot.filament_name + (slot.filament_vendor ? " — " + slot.filament_vendor : "");
|
||
}
|
||
return slot.material || "";
|
||
}
|
||
function _escAttr(s) {
|
||
return String(s || "")
|
||
.replace(/&/g, "&")
|
||
.replace(/"/g, """)
|
||
.replace(/</g, "<")
|
||
.replace(/>/g, ">");
|
||
}
|
||
function _updateSlotMarker(sel) {
|
||
var opt = sel.options[sel.selectedIndex];
|
||
var color = opt && opt.dataset.color ? opt.dataset.color : "#888";
|
||
var slotIdx = parseInt(opt.value);
|
||
var paintIdx = sel.dataset.paint;
|
||
var marker = document.querySelector('.fd-slot-marker[data-for-paint="' + paintIdx + '"]');
|
||
if (marker) {
|
||
marker.style.background = color;
|
||
marker.style.color = _contrastText(color);
|
||
marker.textContent = slotIdx + 1;
|
||
marker.title = opt && opt.dataset.profile ? opt.dataset.profile : "";
|
||
}
|
||
}
|
||
|
||
function openFilamentDialog(slots) {
|
||
_amsSlots = slots
|
||
.filter(function (s) {
|
||
return s.status === "loaded";
|
||
})
|
||
.sort(function (a, b) {
|
||
return (a.slot_index || 0) - (b.slot_index || 0);
|
||
});
|
||
_loadSpoolmanStatus();
|
||
var dlg = document.getElementById("filament-dialog");
|
||
var title = document.getElementById("fd-title");
|
||
var body = document.getElementById("fd-slots");
|
||
if (title) title.textContent = "▶ " + _storeFilename;
|
||
// Auto-Leveling-Checkbox mit globalem Default vorbelegen
|
||
var fdAl = document.getElementById("fd-auto-leveling");
|
||
if (fdAl) fdAl.checked = S.auto_leveling === undefined ? true : !!S.auto_leveling;
|
||
// Objekt-Liste laden — sobald eine File-ID auflösbar ist (Issue #57 Punkt 3:
|
||
// Skip-Parität auch im Banner-/Upload-Modus, nicht nur im Store-Modus).
|
||
// startReadyFileWithSlots() setzt _storeFileId auch im Banner-Modus per
|
||
// filename→fileObj-Lookup, daher reicht hier die _storeFileId-Prüfung.
|
||
_printObjects = [];
|
||
_printObjectsSvg = "";
|
||
var objSection = document.getElementById("fd-objects-section");
|
||
var objBody = document.getElementById("fd-objects-body");
|
||
var objArrow = document.getElementById("fd-objects-arrow");
|
||
if (objSection) objSection.style.display = "none";
|
||
if (objBody) objBody.style.display = "none"; // immer eingeklappt starten
|
||
if (objArrow) objArrow.style.transform = "";
|
||
if (_storeFileId) {
|
||
// Bei frischem Orca-/Web-Upload liefert der Drucker die Objektliste
|
||
// (objects_skip_parts) erst per fileDetails nach → ist im Store kurz leer.
|
||
// Daher mehrfach nachfragen, bis Objekte da sind (Issue #57 Skip-Parität).
|
||
var _objFid = _storeFileId;
|
||
var _objTries = 0;
|
||
(function _loadObjects() {
|
||
if (_objFid !== _storeFileId) return; // Dialog wechselte Datei → abbrechen
|
||
fetch(_apiUrl("/kx/files/" + encodeURIComponent(_objFid) + "/objects"))
|
||
.then(function (r) {
|
||
return r.json();
|
||
})
|
||
.then(function (d) {
|
||
var names = (d.result && d.result.names) || [];
|
||
var svg = (d.result && d.result.svg_b64) || "";
|
||
if (names.length >= 2) {
|
||
_printObjectsSvg = svg;
|
||
_printObjects = names.map(function (n) {
|
||
return { name: n, skip: false };
|
||
});
|
||
renderObjectChecklist();
|
||
renderObjectSvg();
|
||
var cnt = document.getElementById("fd-objects-count");
|
||
if (cnt) cnt.textContent = "(" + names.length + ")";
|
||
if (objSection) objSection.style.display = "block";
|
||
} else if (_objTries++ < 6) {
|
||
setTimeout(_loadObjects, 1000); // bis ~6s auf fileDetails warten
|
||
}
|
||
})
|
||
.catch(function () { });
|
||
})();
|
||
}
|
||
|
||
// GCode-Kanäle: bevorzugt aus _gcodeFilaments, sonst aus belegten AMS-Slots ableiten
|
||
var channels = _gcodeFilaments.length
|
||
? _gcodeFilaments
|
||
: _amsSlots.map(function (s, i) {
|
||
return { slot_index: i, color_hex: s.color_hex, material: s.material };
|
||
});
|
||
|
||
// Default mapping strategy:
|
||
// 1) keep order where possible (row i -> nearest compatible slot i)
|
||
// 2) keep defaults unique while compatible slots are available
|
||
// 3) use color proximity as tie-breaker
|
||
function _hexToRgb(hex) {
|
||
var c = (hex || "").replace("#", "");
|
||
if (c.length === 3) c = c[0] + c[0] + c[1] + c[1] + c[2] + c[2];
|
||
if (c.length < 6) return [255, 255, 255];
|
||
return [parseInt(c.slice(0, 2), 16), parseInt(c.slice(2, 4), 16), parseInt(c.slice(4, 6), 16)];
|
||
}
|
||
function _colorDist(a, b) {
|
||
var ar = _hexToRgb(a),
|
||
br = _hexToRgb(b);
|
||
var dr = ar[0] - br[0],
|
||
dg = ar[1] - br[1],
|
||
db = ar[2] - br[2];
|
||
return dr * dr + dg * dg + db * db;
|
||
}
|
||
var defaultSlotByPaint = {};
|
||
var usedDefaultSlot = {};
|
||
channels.forEach(function (gc, i) {
|
||
var compatible = _amsSlots.filter(function (s) {
|
||
return _materialsCompatible(gc.material, s.material);
|
||
});
|
||
if (!compatible.length) {
|
||
defaultSlotByPaint[i] = -1;
|
||
return;
|
||
}
|
||
|
||
var ranked = compatible.slice().sort(function (a, b) {
|
||
var da = Math.abs((a.slot_index || 0) - i),
|
||
db = Math.abs((b.slot_index || 0) - i);
|
||
if (da !== db) return da - db;
|
||
var ca = _colorDist(gc.color_hex, a.color_hex),
|
||
cb = _colorDist(gc.color_hex, b.color_hex);
|
||
if (ca !== cb) return ca - cb;
|
||
return (a.slot_index || 0) - (b.slot_index || 0);
|
||
});
|
||
|
||
var chosen =
|
||
ranked.find(function (s) {
|
||
return !usedDefaultSlot[s.slot_index];
|
||
}) || ranked[0];
|
||
defaultSlotByPaint[i] = chosen ? chosen.slot_index : -1;
|
||
if (chosen) usedDefaultSlot[chosen.slot_index] = 1;
|
||
});
|
||
|
||
if (!_amsSlots.length) {
|
||
body.innerHTML =
|
||
'<p style="color:var(--txt2);font-size:13px;text-align:center;padding:16px 0">' +
|
||
T.fd_no_slots_msg.replace("{br}", "<br>") +
|
||
"</p>";
|
||
} else {
|
||
body.innerHTML = channels
|
||
.map(function (gc, i) {
|
||
var isUsed = gc && gc.is_used !== false;
|
||
// Only allow material-compatible slots.
|
||
var compatible = _amsSlots.filter(function (s) {
|
||
return _materialsCompatible(gc.material, s.material);
|
||
});
|
||
|
||
var defaultSlotIndex = Object.prototype.hasOwnProperty.call(defaultSlotByPaint, i)
|
||
? defaultSlotByPaint[i]
|
||
: -1;
|
||
var defaultSlot =
|
||
compatible.find(function (s) {
|
||
return s.slot_index === defaultSlotIndex;
|
||
}) || null;
|
||
var opts = compatible
|
||
.map(function (s) {
|
||
var sel = defaultSlot && s.slot_index === defaultSlot.slot_index ? "selected" : "";
|
||
return (
|
||
'<option value="' +
|
||
s.slot_index +
|
||
'" data-color="' +
|
||
s.color_hex +
|
||
'" data-material="' +
|
||
s.material +
|
||
'" data-profile="' +
|
||
_escAttr(_slotProfileLabel(s)) +
|
||
'" ' +
|
||
sel +
|
||
">" +
|
||
"● " +
|
||
T.fd_slot +
|
||
" " +
|
||
(s.slot_index + 1) +
|
||
" · " +
|
||
_slotProfileLabel(s) +
|
||
"</option>"
|
||
);
|
||
})
|
||
.join("");
|
||
if (!compatible.length) {
|
||
opts =
|
||
'<option value="-1" data-color="#888888" data-material="" selected>⚠ ' +
|
||
T.fd_no_matching_material +
|
||
"</option>";
|
||
}
|
||
// Kanal-Box (links): farbige Box mit Nummer + auto Kontrast-Text
|
||
var txt = _contrastText(gc.color_hex);
|
||
var slotColor = defaultSlot ? defaultSlot.color_hex : "#888";
|
||
var slotTxt = _contrastText(slotColor);
|
||
var usedBadge = isUsed
|
||
? '<span style="font-size:10px;color:var(--ok);font-weight:700;min-width:32px">' +
|
||
T.fd_used +
|
||
"</span>"
|
||
: '<span style="font-size:10px;color:var(--txt2);font-weight:700;min-width:32px;opacity:.75">' +
|
||
T.fd_used +
|
||
"</span>";
|
||
return (
|
||
'<div style="display:flex;align-items:center;gap:8px;padding:8px;border-radius:6px;background:var(--raised);border:1px solid var(--border)">' +
|
||
'<span style="display:inline-flex;align-items:center;justify-content:center;width:28px;height:28px;border-radius:6px;background:' +
|
||
gc.color_hex +
|
||
";color:" +
|
||
txt +
|
||
';font-weight:700;font-size:13px;border:1px solid var(--border);flex-shrink:0">' +
|
||
(i + 1) +
|
||
"</span>" +
|
||
'<span style="font-size:11px;color:var(--txt2);min-width:36px">' +
|
||
gc.material +
|
||
"</span>" +
|
||
usedBadge +
|
||
'<span style="font-size:16px;color:var(--txt2)">→</span>' +
|
||
'<span class="fd-slot-marker" data-for-paint="' +
|
||
i +
|
||
'" title="' +
|
||
_escAttr(defaultSlot ? _slotProfileLabel(defaultSlot) : "") +
|
||
'" style="display:inline-flex;align-items:center;justify-content:center;width:24px;height:24px;border-radius:5px;background:' +
|
||
slotColor +
|
||
";color:" +
|
||
slotTxt +
|
||
';font-weight:700;font-size:12px;border:1px solid var(--border);flex-shrink:0">' +
|
||
(defaultSlot ? defaultSlot.slot_index + 1 : "?") +
|
||
"</span>" +
|
||
'<select data-paint="' +
|
||
i +
|
||
'" data-paint-color="' +
|
||
gc.color_hex +
|
||
'" data-is-used="' +
|
||
(isUsed ? "1" : "0") +
|
||
'" data-has-compatible="' +
|
||
(compatible.length ? "1" : "0") +
|
||
'" ' +
|
||
(compatible.length ? "" : "disabled") +
|
||
' onchange="_updateSlotMarker(this)" style="flex:1;min-width:0;padding:4px 6px;border-radius:6px;border:1px solid var(--border);background:var(--raised);color:var(--txt);font-size:12px">' +
|
||
opts +
|
||
"</select>" +
|
||
"</div>"
|
||
);
|
||
})
|
||
.join("");
|
||
}
|
||
if (dlg) dlg.classList.add("open");
|
||
// Spoolman-Section nach Slot-Render aufbauen (braucht die selects im DOM)
|
||
setTimeout(_buildSpoolmanSection, 0);
|
||
}
|
||
|
||
function closeFilamentDialog() {
|
||
var dlg = document.getElementById('filament-dialog');
|
||
if (dlg) dlg.classList.remove('open');
|
||
_showMismatchWarn(null);
|
||
_fdDialogOpen = false;
|
||
if (_fdAutoOpenedFile) {
|
||
_fdUserCancelled = true;
|
||
sessionStorage.setItem('fdUserCancelled', '1');
|
||
sessionStorage.setItem('fdAutoOpenedFile', _fdAutoOpenedFile);
|
||
}
|
||
}
|
||
|
||
function confirmFilamentPrint() {
|
||
var selects = document.querySelectorAll("#fd-slots select");
|
||
var assignments = [];
|
||
var missingCompatible = 0;
|
||
selects.forEach(function (sel) {
|
||
var paintIdx = parseInt(sel.dataset.paint);
|
||
var paintColor = sel.dataset.paintColor;
|
||
var isUsed = sel.dataset.isUsed === "1";
|
||
var hasCompatible = sel.dataset.hasCompatible === "1";
|
||
var opt = sel.options[sel.selectedIndex];
|
||
var amsIdx = parseInt(opt && opt.value);
|
||
if (!hasCompatible || Number.isNaN(amsIdx) || amsIdx < 0) {
|
||
if (isUsed) missingCompatible += 1;
|
||
amsIdx = -1;
|
||
}
|
||
var amsSlot =
|
||
_amsSlots.find(function (s) {
|
||
return s.slot_index === amsIdx;
|
||
}) || {};
|
||
// Farbe als [R,G,B,255]
|
||
function hexToRgba(h) {
|
||
var c = h.replace("#", "");
|
||
if (c.length === 3) c = c[0] + c[0] + c[1] + c[1] + c[2] + c[2];
|
||
return [
|
||
parseInt(c.slice(0, 2), 16),
|
||
parseInt(c.slice(2, 4), 16),
|
||
parseInt(c.slice(4, 6), 16),
|
||
255,
|
||
];
|
||
}
|
||
assignments.push({
|
||
paint_index: paintIdx,
|
||
is_used: isUsed,
|
||
slot_index: amsIdx,
|
||
material: opt.dataset.material || "PLA",
|
||
paint_color: hexToRgba(paintColor || "#ffffff"),
|
||
ams_color: hexToRgba(amsSlot.color_hex || "#ffffff"),
|
||
});
|
||
});
|
||
if (missingCompatible > 0) {
|
||
clog(
|
||
"Cannot start print: " + missingCompatible + " used paint(s) have no matching material slot",
|
||
"msg-err"
|
||
);
|
||
return;
|
||
}
|
||
// Pre-Print Skip: Namen der abgehakten Objekte sammeln
|
||
var excludedObjects = _printObjects
|
||
.filter(function (o) {
|
||
return o.skip;
|
||
})
|
||
.map(function (o) {
|
||
return o.name;
|
||
});
|
||
var fdAlEl = document.getElementById("fd-auto-leveling");
|
||
var fdAutoLeveling = fdAlEl
|
||
? fdAlEl.checked
|
||
? 1
|
||
: 0
|
||
: S.auto_leveling === undefined
|
||
? 1
|
||
: S.auto_leveling
|
||
? 1
|
||
: 0;
|
||
// Spoolman: Slot→Spool-Mapping aus Dialog sammeln und senden
|
||
if (_spoolmanStatus.configured) {
|
||
var slotMap = {};
|
||
document.querySelectorAll("[data-spool-slot]").forEach(function (sel) {
|
||
var idx = sel.dataset.spoolSlot;
|
||
var val = parseInt(sel.value);
|
||
if (val > 0) slotMap[idx] = val;
|
||
});
|
||
_slotSpoolMap = slotMap;
|
||
post("/kx/spoolman/active-spool", { slot_map: slotMap }).catch(function () { });
|
||
}
|
||
closeFilamentDialog();
|
||
if (_filamentDialogMode === "banner") {
|
||
// Banner-Modus: /kx/print bevorzugen wenn _storeFileId bekannt (gleicher Pfad wie File-Browser).
|
||
var btn = document.getElementById("file-ready-btn");
|
||
if (btn) {
|
||
btn.disabled = true;
|
||
btn.textContent = "…";
|
||
}
|
||
var startPromise;
|
||
if (_storeFileId) {
|
||
startPromise = fetch(_apiUrl("/kx/print"), {
|
||
method: "POST",
|
||
headers: { "Content-Type": "application/json" },
|
||
body: JSON.stringify({
|
||
file_id: _storeFileId,
|
||
filament_assignments: assignments,
|
||
excluded_objects: excludedObjects,
|
||
auto_leveling: fdAutoLeveling,
|
||
}),
|
||
});
|
||
} else {
|
||
startPromise = post("/printer/print/start", {
|
||
filename: S.file_ready || _storeFilename,
|
||
filament_assignments: assignments,
|
||
excluded_objects: excludedObjects,
|
||
auto_leveling: fdAutoLeveling,
|
||
});
|
||
}
|
||
startPromise
|
||
.then(function (r) {
|
||
if (!r.ok) {
|
||
return r.text().then(function (t) {
|
||
throw new Error(t || "HTTP " + r.status);
|
||
});
|
||
}
|
||
return r.json();
|
||
})
|
||
.then(function (d) {
|
||
if (d && d.error) throw new Error(d.error);
|
||
if (d && d.result && d.result !== "ok") throw new Error(String(d.result));
|
||
document.getElementById("file-ready-banner").style.display = "none";
|
||
if (btn) {
|
||
btn.disabled = false;
|
||
setText("file-ready-btn", T.file_ready_btn);
|
||
}
|
||
})
|
||
.catch(function (e) {
|
||
clog(tr("log_error", "Fehler:") + " " + e, "msg-err");
|
||
if (btn) {
|
||
btn.disabled = false;
|
||
setText("file-ready-btn", T.file_ready_btn);
|
||
}
|
||
});
|
||
} else {
|
||
// Store-Modus: POST /kx/print
|
||
fetch(_apiUrl("/kx/print"), {
|
||
method: "POST",
|
||
headers: { "Content-Type": "application/json" },
|
||
body: JSON.stringify({
|
||
file_id: _storeFileId,
|
||
filament_assignments: assignments,
|
||
excluded_objects: excludedObjects,
|
||
auto_leveling: fdAutoLeveling,
|
||
}),
|
||
})
|
||
.then(function (r) {
|
||
return r.json();
|
||
})
|
||
.then(function (d) {
|
||
if (d.result === "ok") {
|
||
clog(tr("log_print_start", "Druckstart:") + " " + _storeFilename, "msg-ok");
|
||
showPanel("dashboard");
|
||
} else {
|
||
clog(tr("log_error", "Fehler:") + " " + (d.error || "?"), "msg-err");
|
||
}
|
||
})
|
||
.catch(function (e) {
|
||
clog(tr("log_error", "Fehler:") + " " + e, "msg-err");
|
||
});
|
||
}
|
||
}
|
||
|
||
function renderObjectChecklist() {
|
||
var box = document.getElementById("fd-objects");
|
||
if (!box) return;
|
||
box.innerHTML = _printObjects
|
||
.map(function (o, i) {
|
||
var label = o.name;
|
||
// Klipper-Namen sind oft "Datei.stl_id_N_copy_M" → schöner darstellen
|
||
var m = label.match(/^(.+)\.stl_id_(\d+)_copy_(\d+)$/);
|
||
if (m)
|
||
label =
|
||
m[1] +
|
||
" #" +
|
||
(parseInt(m[2]) + 1) +
|
||
(m[3] !== "0" ? " (" + (parseInt(m[3]) + 1) + ")" : "");
|
||
return (
|
||
'<label style="display:flex;align-items:center;gap:8px;padding:6px 8px;border-radius:6px;background:var(--raised);border:1px solid var(--border);cursor:pointer;font-size:12px">' +
|
||
'<input type="checkbox" data-idx="' +
|
||
i +
|
||
'" ' +
|
||
(o.skip ? "checked" : "") +
|
||
' onchange="_toggleObjectSkip(' +
|
||
i +
|
||
',this.checked)">' +
|
||
'<span style="word-break:break-all">' +
|
||
label +
|
||
"</span>" +
|
||
"</label>"
|
||
);
|
||
})
|
||
.join("");
|
||
}
|
||
function _toggleObjectSkip(idx, val) {
|
||
if (_printObjects[idx]) _printObjects[idx].skip = !!val;
|
||
renderObjectSvg();
|
||
}
|
||
// Issue #57 Punkt 3: Skip-Objekte-Bereich ein-/ausklappen
|
||
function toggleFdObjects() {
|
||
var body = document.getElementById("fd-objects-body");
|
||
var arrow = document.getElementById("fd-objects-arrow");
|
||
if (!body) return;
|
||
var open = body.style.display !== "none";
|
||
body.style.display = open ? "none" : "block";
|
||
if (arrow) arrow.style.transform = open ? "" : "rotate(90deg)";
|
||
}
|
||
function renderObjectSvg() {
|
||
var box = document.getElementById("fd-objects-svg");
|
||
if (!box) return;
|
||
if (!_printObjectsSvg || !_printObjects.length) {
|
||
box.style.display = "none";
|
||
box.innerHTML = "";
|
||
return;
|
||
}
|
||
box.style.display = "block";
|
||
var svg = "";
|
||
try {
|
||
svg = atob(_printObjectsSvg);
|
||
} catch (e) {
|
||
box.style.display = "none";
|
||
return;
|
||
}
|
||
box.innerHTML = svg;
|
||
var svgEl = box.querySelector("svg");
|
||
if (!svgEl) return;
|
||
svgEl.style.width = "100%";
|
||
svgEl.style.maxHeight = "200px";
|
||
svgEl.style.height = "auto";
|
||
_printObjects.forEach(function (o, i) {
|
||
var g = svgEl.querySelector('g[id="' + CSS.escape(o.name) + '"]');
|
||
if (!g) return;
|
||
var path = g.querySelector("path");
|
||
g.style.cursor = "pointer";
|
||
g.setAttribute("opacity", o.skip ? "0.8" : "0.35");
|
||
if (path) {
|
||
path.setAttribute("fill", o.skip ? "#ff5e5b" : "#5fa7ff");
|
||
path.setAttribute("fill-opacity", o.skip ? "0.4" : "0.18");
|
||
}
|
||
g.onclick = function () {
|
||
_printObjects[i].skip = !_printObjects[i].skip;
|
||
renderObjectChecklist();
|
||
renderObjectSvg();
|
||
};
|
||
});
|
||
}
|
||
|
||
// ── Mid-Print Skip ──
|
||
var _skipObjects = []; // [{name, skipped, willSkip}]
|
||
var _skipSvg = "";
|
||
var _skipPollTimer = null;
|
||
function _applySkipDialogState(s) {
|
||
s = s || {};
|
||
_skipSvg = s.svg_b64 || "";
|
||
var skipped = s.skipped || [];
|
||
// Pending-Auswahl (willSkip) beim Refresh erhalten.
|
||
var prevWillSkip = {};
|
||
(_skipObjects || []).forEach(function (o) {
|
||
if (o && o.name) prevWillSkip[o.name] = !!o.willSkip;
|
||
});
|
||
_skipObjects = (s.objects || []).map(function (n) {
|
||
var isSkipped = skipped.indexOf(n) >= 0;
|
||
return {
|
||
name: n,
|
||
skipped: isSkipped,
|
||
willSkip: isSkipped ? false : !!prevWillSkip[n],
|
||
};
|
||
});
|
||
renderSkipList();
|
||
renderSkipSvg();
|
||
}
|
||
function openSkipDialog() {
|
||
document.getElementById("skip-status").textContent = "";
|
||
document.getElementById("skip-confirm").disabled = false;
|
||
_refreshSkipDialog();
|
||
if (_skipPollTimer) clearInterval(_skipPollTimer);
|
||
_skipPollTimer = setInterval(function () {
|
||
var dlg = document.getElementById("skip-dialog");
|
||
if (!(dlg && dlg.classList.contains("open"))) return;
|
||
_refreshSkipDialog();
|
||
}, 2000);
|
||
document.getElementById("skip-dialog").classList.add("open");
|
||
}
|
||
function _refreshSkipDialog() {
|
||
// query-Endpoint wartet intern auf frischen skip/report (bis 1.5s).
|
||
fetch(_apiUrl("/kx/skip/query"), { method: "POST" })
|
||
.then(function (r) {
|
||
return r.json();
|
||
})
|
||
.then(function (d) {
|
||
_applySkipDialogState(d.result || {});
|
||
})
|
||
.catch(function () {
|
||
fetch(_apiUrl("/kx/skip/state"))
|
||
.then(function (r) {
|
||
return r.json();
|
||
})
|
||
.then(function (d) {
|
||
_applySkipDialogState(d.result || {});
|
||
})
|
||
.catch(function () { });
|
||
});
|
||
}
|
||
function closeSkipDialog() {
|
||
if (_skipPollTimer) {
|
||
clearInterval(_skipPollTimer);
|
||
_skipPollTimer = null;
|
||
}
|
||
document.getElementById("skip-dialog").classList.remove("open");
|
||
}
|
||
function _shortLabel(name) {
|
||
var m = name.match(/^(.+)\.[sS][tT][lL]_id_(\d+)_copy_(\d+)$/);
|
||
if (!m) return name;
|
||
return (
|
||
m[1] + " #" + (parseInt(m[2]) + 1) + (m[3] !== "0" ? " (" + (parseInt(m[3]) + 1) + ")" : "")
|
||
);
|
||
}
|
||
function renderSkipList() {
|
||
var box = document.getElementById("skip-list");
|
||
if (!box) return;
|
||
if (!_skipObjects.length) {
|
||
box.innerHTML =
|
||
'<div style="color:var(--txt2);font-size:12px;padding:12px;text-align:center">' +
|
||
tr("skip_no_objects") +
|
||
"</div>";
|
||
return;
|
||
}
|
||
box.innerHTML = _skipObjects
|
||
.map(function (o, i) {
|
||
var label = _shortLabel(o.name);
|
||
var dis = o.skipped ? "disabled" : "";
|
||
var note = o.skipped
|
||
? '<span style="font-size:11px;color:var(--warn);margin-left:auto">' +
|
||
tr("skip_already") +
|
||
"</span>"
|
||
: "";
|
||
return (
|
||
'<label style="display:flex;align-items:center;gap:8px;padding:6px 8px;border-radius:6px;background:var(--raised);border:1px solid var(--border);font-size:12px;' +
|
||
(o.skipped ? "opacity:0.5" : "") +
|
||
'">' +
|
||
'<input type="checkbox" data-idx="' +
|
||
i +
|
||
'" ' +
|
||
(o.willSkip ? "checked" : "") +
|
||
" " +
|
||
dis +
|
||
' onchange="_toggleWillSkip(' +
|
||
i +
|
||
',this.checked)">' +
|
||
'<span style="word-break:break-all">' +
|
||
label +
|
||
"</span>" +
|
||
note +
|
||
"</label>"
|
||
);
|
||
})
|
||
.join("");
|
||
}
|
||
function renderSkipSvg() {
|
||
var box = document.getElementById("skip-svg");
|
||
if (!box) return;
|
||
if (!_skipSvg || !_skipObjects.length) {
|
||
box.style.display = "none";
|
||
box.innerHTML = "";
|
||
return;
|
||
}
|
||
box.style.display = "block";
|
||
// SVG aus base64 dekodieren
|
||
var svg = "";
|
||
try {
|
||
svg = atob(_skipSvg);
|
||
} catch (e) {
|
||
box.style.display = "none";
|
||
return;
|
||
}
|
||
box.innerHTML = svg;
|
||
// Polygone interaktiv machen: jeder <g id="..."> entspricht einem Objekt
|
||
var svgEl = box.querySelector("svg");
|
||
if (!svgEl) return;
|
||
svgEl.style.width = "100%";
|
||
svgEl.style.maxHeight = "280px";
|
||
svgEl.style.height = "auto";
|
||
_skipObjects.forEach(function (o, i) {
|
||
var g = svgEl.querySelector('g[id="' + CSS.escape(o.name) + '"]');
|
||
if (!g) return;
|
||
var path = g.querySelector("path");
|
||
if (o.skipped) {
|
||
// bereits übersprungen → ausgegraut, kein Klick
|
||
g.setAttribute("opacity", "0.25");
|
||
if (path) {
|
||
path.setAttribute("fill", "#888");
|
||
path.setAttribute("fill-opacity", "0.3");
|
||
}
|
||
g.style.cursor = "not-allowed";
|
||
} else {
|
||
g.style.cursor = "pointer";
|
||
g.setAttribute("opacity", o.willSkip ? "0.8" : "0.35");
|
||
if (path) {
|
||
path.setAttribute("fill", o.willSkip ? "#ff5e5b" : "#5fa7ff");
|
||
path.setAttribute("fill-opacity", o.willSkip ? "0.4" : "0.18");
|
||
}
|
||
g.onclick = function () {
|
||
_skipObjects[i].willSkip = !_skipObjects[i].willSkip;
|
||
renderSkipList();
|
||
renderSkipSvg();
|
||
};
|
||
}
|
||
});
|
||
}
|
||
function _toggleWillSkip(idx, val) {
|
||
if (_skipObjects[idx]) _skipObjects[idx].willSkip = !!val;
|
||
renderSkipSvg();
|
||
}
|
||
function confirmSkip() {
|
||
var names = _skipObjects
|
||
.filter(function (o) {
|
||
return o.willSkip;
|
||
})
|
||
.map(function (o) {
|
||
return o.name;
|
||
});
|
||
var st = document.getElementById("skip-status");
|
||
var btn = document.getElementById("skip-confirm");
|
||
if (!names.length) {
|
||
st.textContent = tr("skip_select_at_least_one");
|
||
st.style.color = "var(--warn)";
|
||
return;
|
||
}
|
||
btn.disabled = true;
|
||
st.textContent = tr("skip_sending");
|
||
st.style.color = "var(--txt2)";
|
||
fetch(_apiUrl("/kx/skip"), {
|
||
method: "POST",
|
||
headers: { "Content-Type": "application/json" },
|
||
body: JSON.stringify({ names: names }),
|
||
})
|
||
.then(function (r) {
|
||
return r.json().then(function (j) {
|
||
return { ok: r.ok, j: j };
|
||
});
|
||
})
|
||
.then(function (res) {
|
||
if (!res.ok) {
|
||
st.textContent = (res.j && res.j.error) || tr("log_error", "Fehler:");
|
||
st.style.color = "var(--err)";
|
||
btn.disabled = false;
|
||
return;
|
||
}
|
||
st.textContent = tr("skip_success");
|
||
st.style.color = "var(--ok)";
|
||
// Dialog offen lassen + neu laden damit der "übersprungen"-Status erscheint
|
||
setTimeout(function () {
|
||
_refreshSkipDialog();
|
||
btn.disabled = false;
|
||
st.textContent = "";
|
||
}, 1500);
|
||
})
|
||
.catch(function (e) {
|
||
st.textContent = "" + e;
|
||
st.style.color = "var(--err)";
|
||
btn.disabled = false;
|
||
});
|
||
}
|
||
|
||
function storeDelete(fileId) {
|
||
if (!confirm(T.store_delete_confirm)) return;
|
||
fetch(_apiUrl("/kx/files/" + fileId), { method: "DELETE" }).then(function (r) {
|
||
if (r.ok) {
|
||
loadStore();
|
||
} else {
|
||
clog(tr("log_delete_failed", "Löschen fehlgeschlagen"), "msg-err");
|
||
}
|
||
});
|
||
}
|
||
|
||
function storeDownload(fileId) {
|
||
var a = document.createElement("a");
|
||
a.href = _apiUrl("/kx/files/" + encodeURIComponent(fileId) + "/download");
|
||
a.style.display = "none";
|
||
document.body.appendChild(a);
|
||
a.click();
|
||
a.remove();
|
||
}
|
||
|
||
// ── Drucker hinzufügen ──
|
||
function openAddPrinterDialog() {
|
||
document.getElementById("apd-ip").value = "";
|
||
document.getElementById("apd-name").value = "";
|
||
var st = document.getElementById("apd-status");
|
||
st.textContent = "";
|
||
st.style.color = "var(--txt2)";
|
||
document.getElementById("apd-confirm").disabled = false;
|
||
document.getElementById("add-printer-dialog").classList.add("open");
|
||
}
|
||
function closeAddPrinterDialog() {
|
||
document.getElementById("add-printer-dialog").classList.remove("open");
|
||
}
|
||
function confirmAddPrinter() {
|
||
var ip = document.getElementById("apd-ip").value.trim();
|
||
var name = document.getElementById("apd-name").value.trim();
|
||
var st = document.getElementById("apd-status"),
|
||
btn = document.getElementById("apd-confirm");
|
||
if (!ip) {
|
||
st.textContent = T.apd_err_ip;
|
||
st.style.color = "var(--err)";
|
||
return;
|
||
}
|
||
st.textContent = T.apd_fetching;
|
||
st.style.color = "var(--txt2)";
|
||
btn.disabled = true;
|
||
fetch("/kx/printers/add", {
|
||
method: "POST",
|
||
headers: { "Content-Type": "application/json" },
|
||
body: JSON.stringify({ printer_ip: ip, name: name }),
|
||
})
|
||
.then(function (r) {
|
||
return r.json().then(function (j) {
|
||
return { ok: r.ok, j: j };
|
||
});
|
||
})
|
||
.then(function (res) {
|
||
if (!res.ok) {
|
||
st.textContent = (res.j && res.j.error) || tr("log_error", "Fehler:");
|
||
st.style.color = "var(--err)";
|
||
btn.disabled = false;
|
||
return;
|
||
}
|
||
st.textContent = T.apd_success;
|
||
st.style.color = "var(--ok)";
|
||
setTimeout(function () {
|
||
location.reload();
|
||
}, 2500);
|
||
})
|
||
.catch(function (e) {
|
||
st.textContent = "" + e;
|
||
st.style.color = "var(--err)";
|
||
btn.disabled = false;
|
||
});
|
||
}
|
||
function removePrinter(id, name) {
|
||
if (!confirm(T.printers_remove_confirm.replace("{name}", name))) return;
|
||
fetch("/kx/printers/" + encodeURIComponent(id), { method: "DELETE" })
|
||
.then(function (r) {
|
||
return r.json().then(function (j) {
|
||
return { ok: r.ok, j: j };
|
||
});
|
||
})
|
||
.then(function (res) {
|
||
if (!res.ok) {
|
||
alert((res.j && res.j.error) || tr("log_error", "Fehler:"));
|
||
return;
|
||
}
|
||
setTimeout(function () {
|
||
location.href = "/printer1";
|
||
}, 2000);
|
||
})
|
||
.catch(function (e) {
|
||
alert("" + e);
|
||
});
|
||
}
|
||
|
||
// ── Drucker-Tab ──
|
||
function loadPrinterTab() {
|
||
var grid = document.getElementById("printers-grid");
|
||
if (grid)
|
||
grid.innerHTML =
|
||
'<div style="color:var(--txt2);font-size:13px;padding:20px">' + T.printers_loading + "</div>";
|
||
// Drucker-Liste von lokaler Instanz holen
|
||
fetch('/kx/printers').then(function(r){return r.json()}).then(function(d){
|
||
var printers=d.result||[];
|
||
if(!printers.length){
|
||
if(grid)grid.innerHTML='<div style="grid-column:1/-1;text-align:center;padding:40px 20px;color:var(--txt2)">'+
|
||
'<div style="font-size:32px;margin-bottom:8px">🖨</div>'+
|
||
'<div style="font-size:14px;margin-bottom:14px">'+T.printers_empty_hint+'</div>'+
|
||
'<button onclick="openAddPrinterDialog()" style="font-size:13px;padding:8px 18px;background:var(--accent);border:none;border-radius:8px;color:#fff;cursor:pointer;font-weight:600">+ '+T.add_printer+'</button>'+
|
||
'</div>';
|
||
return;
|
||
}
|
||
// Status jedes Druckers parallel abrufen
|
||
var fetches=printers.map(function(p){
|
||
var url=(p.bridge_url||'').replace(/\/+$/,'');
|
||
return fetch(url+'/api/state',{signal:AbortSignal.timeout(3000)})
|
||
.then(function(r){return r.json()})
|
||
.then(function(s){return {printer:p,state:s,online:true};})
|
||
.catch(function(){return {printer:p,state:{},online:false};});
|
||
});
|
||
Promise.all(fetches).then(function(results){
|
||
var activeId=_activePrinter?String(_activePrinter.id):null;
|
||
if(grid)grid.innerHTML=results.map(function(res){
|
||
var p=res.printer,s=res.state,online=res.online;
|
||
var isActive=String(p.id)===activeId;
|
||
var url=(p.bridge_url||'').replace(/\/+$/,'');
|
||
var printerNum=p.id;
|
||
var ks=online?(s.kobra_state||'free'):'offline';
|
||
var stateKey='kobra_'+ks;
|
||
var stateLabel=T[stateKey]||ks;
|
||
var stateColor=ks==='free'?'var(--ok)':ks==='printing'?'var(--accent)':ks==='offline'?'var(--txt2)':'var(--warn)';
|
||
var progress=online&&s.progress?Math.round(s.progress*100):null;
|
||
var filename=online&&s.filename?s.filename:'';
|
||
var nt=online&&s.nozzle_temp?s.nozzle_temp.toFixed(1):'–';
|
||
var bt=online&&s.bed_temp?s.bed_temp.toFixed(1):'–';
|
||
var border=isActive?'2px solid var(--accent)':'1px solid var(--border)';
|
||
var nameEsc=String(p.name).replace(/\\/g,'\\\\').replace(/'/g,"\\'");
|
||
return '<div style="background:var(--raised);border:'+border+';border-radius:10px;padding:14px;display:flex;flex-direction:column;gap:8px">'+
|
||
'<div style="display:flex;align-items:center;justify-content:space-between;gap:8px">'+
|
||
'<span style="font-weight:700;font-size:14px;overflow:hidden;text-overflow:ellipsis;white-space:nowrap">🖨 '+p.name+'</span>'+
|
||
'<span style="display:flex;align-items:center;gap:8px;flex-shrink:0">'+
|
||
(isActive?'<span style="font-size:11px;color:var(--accent);font-weight:600">'+T.printers_active+'</span>':'')+
|
||
'<button onclick="removePrinter(\''+printerNum+'\',\''+nameEsc+'\')" title="'+T.printers_remove+'" style="background:none;border:none;color:var(--txt2);font-size:16px;cursor:pointer;line-height:1;padding:0">✕</button>'+
|
||
'</span>'+
|
||
'</div>'+
|
||
'<div style="display:flex;align-items:center;gap:6px">'+
|
||
'<span style="width:8px;height:8px;border-radius:50%;background:'+stateColor+';display:inline-block"></span>'+
|
||
'<span style="font-size:13px;color:var(--txt2)">'+stateLabel+'</span>'+
|
||
'</div>'+
|
||
(p.printer_ip?'<div style="font-size:12px;color:var(--txt2)">🌐 '+p.printer_ip+'</div>':'')+
|
||
(filename?'<div style="font-size:12px;color:var(--txt2);white-space:nowrap;overflow:hidden;text-overflow:ellipsis" title="'+filename+'">📄 '+filename+'</div>':'')+
|
||
(progress!==null?'<div style="background:var(--border);border-radius:4px;height:6px;overflow:hidden"><div style="background:var(--accent);height:100%;width:'+progress+'%"></div></div>':'')+
|
||
'<div style="font-size:12px;color:var(--txt2);display:flex;gap:12px">'+
|
||
'<span>🌡 '+nt+'°C</span><span>🛏 '+bt+'°C</span>'+
|
||
'</div>'+
|
||
(!isActive?'<a href="'+url+'/printer'+printerNum+'" style="display:block;text-align:center;padding:7px;background:var(--accent);color:#fff;border-radius:7px;font-size:13px;font-weight:600;text-decoration:none;margin-top:4px">'+T.printers_switch+'</a>':'<div style="text-align:center;padding:7px;font-size:12px;color:var(--txt2)">'+T.printers_current+'</div>')+
|
||
'</div>';
|
||
}).join('');
|
||
});
|
||
}).catch(function(e){
|
||
if(grid)grid.innerHTML='<div style="color:var(--err);font-size:13px;padding:20px">Fehler: '+e+'</div>';
|
||
});
|
||
}
|