build: sources for v0.9.18

This commit is contained in:
2026-05-31 19:53:36 +02:00
parent 23b8a69065
commit ac695ecf36
8 changed files with 614 additions and 112 deletions

View File

@@ -285,9 +285,9 @@ function applyLang(){
setText('d-lbl-light',T.lbl_light);
setText('d-lbl-nozzle',T.label_nozzle);
setText('d-lbl-bed',T.label_bed);
// Dashboard buttons
setText('d-btn-pause',T.btn_pause);
setText('d-btn-resume',T.btn_resume);
// 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);
@@ -612,11 +612,14 @@ function applyState(){
}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){
var printing=(s.print_state==='printing'||s.print_state==='paused');
skipBtn.style.display=printing?'':'none';
}
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();
// header
var b=document.getElementById('h-badge');
@@ -778,10 +781,17 @@ function applyState(){
var activity=(slot.activity||'');
var pct=empty?T.ams_empty:(slot.consumables_percent!=null?slot.consumables_percent+'%':'');
var slotLabel=T.label_slot+' '+(globalIdx+1);
var profile=(window._slotProfileMap||{})[globalIdx];
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>';
}
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">'+(empty?'':(slot.type||slot.material_type||''))+'</div>'
+vendorBadge
+'<div class="slot-label">'+slotLabel+'</div>'
+'<div class="slot-label" style="font-size:10px;color:var(--txt2)">'+pct+'</div>'
+'<div style="font-size:9px;color:var(--txt2);margin-top:2px">✏</div>'
@@ -920,9 +930,16 @@ function _loadOrcaFilaments(cb){
cb(_orcaFilamentCache);
}).catch(function(){ cb([]); });
}
function _fillSlotProfileDropdown(material, currentId){
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();
@@ -939,9 +956,12 @@ function _fillSlotProfileDropdown(material, currentId){
var g=document.createElement('optgroup'); g.label=v;
byVendor[v].forEach(function(p){
var o=document.createElement('option');
o.value=p.id; o.dataset.vendor=p.vendor;
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.name;
if(p.id===currentId) o.selected=true;
if(o.value===wantKey) o.selected=true;
g.appendChild(o);
});
sel.appendChild(g);
@@ -968,15 +988,18 @@ function openSlotEdit(i){
+(m===mat?'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 filament_id+filament_vendor).
// Mit dem material-Filter (PLA→PLA*) wird die Liste auf passende Profile reduziert.
// 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||''};
_fillSlotProfileDropdown(mat, entry.filament_id||'');
}).catch(function(){ _fillSlotProfileDropdown(mat,''); });
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');
}
@@ -1023,7 +1046,7 @@ function selectMatPreset(m){
highlightMatBtn(m);
// Filament-Profile-Dropdown an neues Material anpassen
// (vorherige Selektion zurücksetzen — andere Material-Profile passen nicht)
_fillSlotProfileDropdown(m, '');
_fillSlotProfileDropdown(m, '', '');
}
function highlightMatBtn(val){
document.querySelectorAll('.mat-preset-btn').forEach(function(b){
@@ -1032,7 +1055,7 @@ function highlightMatBtn(val){
b.style.color=on?'#fff':'var(--txt2)';
});
// Auch bei manueller Eingabe ins Material-Textfeld: Dropdown refreshen.
if(val) _fillSlotProfileDropdown(val, '');
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);
@@ -1043,31 +1066,55 @@ function saveSlotEdit(){
var mat=document.getElementById('slot-edit-mat').value.trim().toUpperCase()||'PLA';
var color=hexToRgb(hex);
var slotIdx=_slotEditIndex;
// OrcaSlicer-Profil-Override: parallel persistieren (Profile bleiben auch
// erhalten wenn der User nur Farbe/Material ändert)
var profSel=document.getElementById('slot-edit-profile');
var newProfId=profSel?profSel.value:'';
var newProfVendor='';
if(profSel && profSel.selectedOptions && profSel.selectedOptions[0]){
newProfVendor=profSel.selectedOptions[0].dataset.vendor||'';
}
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({id:newProfId,vendor:newProfVendor})
}).then(function(r){return r.json();}).then(function(){
body:JSON.stringify({vendor:newProfVendor, name:newProfName, id:newProfId})
})
.then(function(r){return r.json();})
.then(function(){
window._slotProfileMap=window._slotProfileMap||{};
if(newProfId){ window._slotProfileMap[slotIdx]={id:newProfId,vendor:newProfVendor}; }
else delete window._slotProfileMap[slotIdx];
}).catch(function(e){clog('Profil-Speichern fehlgeschlagen: '+e,'msg-err');});
post('/api/ams/set_slot',{index:slotIdx,type:mat,color:color})
.then(function(r){return r.json();})
.then(function(r){
closeSlotEdit();
var profSuffix=newProfId?(' ['+newProfId+']'):'';
clog(tr('slot_edit_ok')+' '+(slotIdx+1)+': '+mat+' '+hex+profSuffix,'msg-ok');
})
.catch(function(e){clog('Fehler: '+e,'msg-err');});
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');
if(typeof poll==='function') poll();
})
.catch(function(e){clog('Fehler: '+e,'msg-err');});
}
document.addEventListener('DOMContentLoaded',function(){
document.getElementById('s-printer-ip').addEventListener('input',function(){
@@ -1164,6 +1211,21 @@ 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);
})();
@@ -1172,7 +1234,28 @@ function printAction(a){
post('/printer/print/'+a,{}).then(function(){clog('Druck: '+a,'msg-ok');poll()})
.catch(function(e){clog('Fehler: '+e,'msg-err')});
}
function confirmCancel(){if(confirm('Druck wirklich abbrechen?'))printAction('cancel')}
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

View File

@@ -243,9 +243,8 @@
</div>
</div>
<div class="fname" id="d-fname" title="" style="margin-top:6px"></div>
<div class="ctrl-btns" id="d-ctrl-btns" style="margin-top:12px">
<button class="btn btn-pause btn-sm" id="d-btn-pause" onclick="printAction('pause')">⏸ Pause</button>
<button class="btn btn-resume btn-sm" id="d-btn-resume" onclick="printAction('resume')">▶ Weiter</button>
<div class="ctrl-btns" id="d-ctrl-btns" style="margin-top:12px;display:none">
<button class="btn btn-pause btn-sm" id="d-btn-pause" onclick="togglePauseResume()">⏸ Pause</button>
<button class="btn btn-skip btn-sm" id="d-btn-skip" onclick="openSkipDialog()" style="display:none"><span id="d-btn-skip-label">Objekte</span></button>
<button class="btn btn-cancel btn-sm" id="d-btn-cancel" onclick="confirmCancel()">✕ Stopp</button>
</div>