feat: 2-column dashboard layout toggle + local timelapse
2-column layout: settings toggle switches dashboard between 1-col (current behaviour) and 2-col (Camera|Progress, Temps|AMS side by side, motion cards in 2×2). Preference stored in localStorage only (per-browser, not printer config). Mobile always stays 1-col via CSS. Local timelapse: new TimelapseSpool class captures JPEG frames from the camera stream during a print at a configurable interval (default 4s) and encodes them to MP4 with ffmpeg on print end. Stored in data/timelapses/, indexed in SQLite. New /kx/timelapses API + browser tab in the file store panel with download and delete. Also wires the undocumented printer-native timelapse MQTT field as an experimental opt-in setting. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -327,6 +327,14 @@ function applyLang(){
|
||||
setText('lbl-flow-calibration',T.settings_flow_calibration);
|
||||
setText('lbl-camera-on-print',T.settings_camera_on_print);
|
||||
setText('lbl-web-upload-warning',T.settings_web_upload_warning);
|
||||
setText('lbl-timelapse-local',T.settings_timelapse_local);
|
||||
setText('lbl-timelapse-interval',T.settings_timelapse_interval);
|
||||
setText('lbl-timelapse-printer',T.settings_timelapse_printer);
|
||||
setText('lbl-layout',T.settings_layout);
|
||||
setText('opt-layout-1col',T.settings_layout_1col);
|
||||
setText('opt-layout-2col',T.settings_layout_2col);
|
||||
setText('store-tab-files-label',T.store_tab_files||'Dateien');
|
||||
setText('store-tab-tl-label',T.timelapse_tab);
|
||||
|
||||
setText('lbl-update-check',T.update_check);
|
||||
setText('lbl-update-apply',T.update_apply);
|
||||
@@ -896,6 +904,10 @@ function openSettings(){
|
||||
var fc=document.getElementById('s-flow-calibration');if(fc)fc.checked=!!d.flow_calibration;
|
||||
var cop=document.getElementById('s-camera-on-print');if(cop)cop.checked=!!d.camera_on_print;
|
||||
var wuw=document.getElementById('s-web-upload-warning');if(wuw)wuw.checked=(d.web_upload_warning===undefined?true:!!d.web_upload_warning);
|
||||
var tlLocal=document.getElementById('s-timelapse-local');if(tlLocal){tlLocal.checked=!!d.timelapse_local;_toggleTlInterval();}
|
||||
var tlInt=document.getElementById('s-timelapse-interval');if(tlInt)tlInt.value=d.timelapse_interval_sec||4;
|
||||
var tlPrinter=document.getElementById('s-timelapse-printer');if(tlPrinter)tlPrinter.checked=!!d.timelapse_printer;
|
||||
var lay=document.getElementById('s-layout');if(lay)lay.value=localStorage.getItem('layout')||'1col';
|
||||
});
|
||||
var v=localStorage.getItem('pollInterval')||'2000';
|
||||
document.querySelectorAll('.poll-btn').forEach(function(b){b.classList.remove('active')});
|
||||
@@ -912,6 +924,85 @@ function closeSettings(){
|
||||
document.getElementById('settings-modal').classList.remove('open');
|
||||
}
|
||||
|
||||
// ── Layout ──
|
||||
function applyLayout(mode){
|
||||
var panel=document.getElementById('panel-dashboard');
|
||||
if(!panel)return;
|
||||
var fullWidth=['d-cam-card','d-progress-card','d-temps-card','d-ams-card'];
|
||||
if(mode==='2col'){
|
||||
panel.classList.add('layout-2col');
|
||||
fullWidth.forEach(function(id){var el=document.getElementById(id);if(el)el.style.gridColumn='auto';});
|
||||
}else{
|
||||
panel.classList.remove('layout-2col');
|
||||
fullWidth.forEach(function(id){var el=document.getElementById(id);if(el)el.style.gridColumn='1/-1';});
|
||||
}
|
||||
localStorage.setItem('layout',mode);
|
||||
}
|
||||
|
||||
function _toggleTlInterval(){
|
||||
var cb=document.getElementById('s-timelapse-local');
|
||||
var row=document.getElementById('timelapse-interval-row');
|
||||
if(row)row.style.display=(cb&&cb.checked)?'':'none';
|
||||
}
|
||||
|
||||
// ── Store tabs ──
|
||||
var _currentStoreTab='files';
|
||||
var _timelapseData=[];
|
||||
|
||||
function switchStoreTab(tab){
|
||||
_currentStoreTab=tab;
|
||||
var filesSection=document.getElementById('store-files-section');
|
||||
var tlSection=document.getElementById('store-timelapses-section');
|
||||
var tabFiles=document.getElementById('store-tab-files');
|
||||
var tabTl=document.getElementById('store-tab-timelapses');
|
||||
if(filesSection)filesSection.style.display=tab==='files'?'':'none';
|
||||
if(tlSection)tlSection.style.display=tab==='timelapses'?'':'none';
|
||||
if(tabFiles){tabFiles.classList.toggle('active',tab==='files');}
|
||||
if(tabTl){tabTl.classList.toggle('active',tab==='timelapses');}
|
||||
if(tab==='timelapses')loadTimelapses();
|
||||
}
|
||||
|
||||
function refreshStoreTab(){
|
||||
if(_currentStoreTab==='timelapses')loadTimelapses();else loadStore();
|
||||
}
|
||||
|
||||
function loadTimelapses(){
|
||||
fetch(_apiUrl('/kx/timelapses')).then(function(r){return r.json();}).then(function(d){
|
||||
_timelapseData=Array.isArray(d)?d:[];
|
||||
renderTimelapses();
|
||||
}).catch(function(e){clog('Timelapse-Fehler: '+e,'msg-err');});
|
||||
}
|
||||
|
||||
function renderTimelapses(){
|
||||
var list=document.getElementById('timelapse-list');
|
||||
if(!list)return;
|
||||
if(!_timelapseData.length){
|
||||
list.innerHTML='<div class="tl-row"><span style="color:var(--txt2);padding:20px">'+(T.timelapse_empty||'Keine Timelapses vorhanden')+'</span></div>';
|
||||
return;
|
||||
}
|
||||
list.innerHTML=_timelapseData.map(function(t){
|
||||
var statusCls=t.status==='completed'?'tl-status-ok':t.status==='failed'?'tl-status-err':'tl-status-busy';
|
||||
var statusTxt=t.status==='recording'?(T.timelapse_recording||'Aufnahme…'):t.status==='processing'?(T.timelapse_processing||'Kodierung…'):t.status;
|
||||
var dur=t.duration_sec>0?Math.floor(t.duration_sec/60)+'m ':' ';
|
||||
var dlBtn=t.status==='completed'?'<a href="'+_apiUrl('/kx/timelapse/'+t.id+'/download')+'" style="flex-shrink:0;padding:6px 12px;background:var(--accent);color:#000;border-radius:6px;font-size:12px;font-weight:600;text-decoration:none" download>⬇ Download</a>':'';
|
||||
return '<div class="tl-row">'+
|
||||
'<div class="tl-icon">🎬</div>'+
|
||||
'<div class="tl-info">'+
|
||||
'<div class="tl-name" title="'+t.filename+'">'+t.filename+'</div>'+
|
||||
'<div class="tl-meta '+statusCls+'">'+statusTxt+'</div>'+
|
||||
'<div class="tl-meta">'+t.frame_count+' frames • '+dur+t.created_at.slice(0,10)+'</div>'+
|
||||
'</div>'+
|
||||
dlBtn+
|
||||
'<button onclick="deleteTimelapse(\''+t.id+'\')" style="flex-shrink:0;padding:6px 10px;background:var(--raised);border:1px solid var(--border);border-radius:6px;color:var(--err);cursor:pointer;font-size:12px">✕</button>'+
|
||||
'</div>';
|
||||
}).join('');
|
||||
}
|
||||
|
||||
function deleteTimelapse(id){
|
||||
if(!confirm(T.confirm_delete_timelapse||'Dieses Timelapse löschen?'))return;
|
||||
fetch(_apiUrl('/kx/timelapse/'+id),{method:'DELETE'}).then(function(){loadTimelapses();});
|
||||
}
|
||||
|
||||
// ── AMS Slot Edit ──
|
||||
var _slotEditIndex=-1;
|
||||
var _slotEditLoaded=false;
|
||||
@@ -1155,7 +1246,11 @@ function saveSettings(){
|
||||
flow_calibration: (document.getElementById('s-flow-calibration')||{}).checked?1:0,
|
||||
camera_on_print: (document.getElementById('s-camera-on-print')||{}).checked?1:0,
|
||||
web_upload_warning:webUploadWarning,
|
||||
timelapse_local: (document.getElementById('s-timelapse-local')||{}).checked?1:0,
|
||||
timelapse_interval_sec: parseInt((document.getElementById('s-timelapse-interval')||{value:'4'}).value)||4,
|
||||
timelapse_printer: (document.getElementById('s-timelapse-printer')||{}).checked?1:0,
|
||||
}).then(function(){
|
||||
applyLayout((document.getElementById('s-layout')||{value:'1col'}).value);
|
||||
btn.textContent=T.update_restarting;
|
||||
setTimeout(function(){
|
||||
btn.disabled=false;
|
||||
@@ -1234,6 +1329,7 @@ var pollTimer;
|
||||
});
|
||||
}).catch(function(){});
|
||||
poll();pollTimer=setInterval(poll,ms);
|
||||
applyLayout(localStorage.getItem('layout')||'1col');
|
||||
})();
|
||||
|
||||
// ── Print actions ──
|
||||
|
||||
@@ -118,6 +118,29 @@
|
||||
<input type="checkbox" id="s-web-upload-warning" style="width:auto;margin:0">
|
||||
<label id="lbl-web-upload-warning" style="margin:0;cursor:pointer" for="s-web-upload-warning">Warnung bei Web-Upload-Druck anzeigen</label>
|
||||
</div>
|
||||
<div class="modal-field" style="flex-direction:row;align-items:center;gap:10px">
|
||||
<input type="checkbox" id="s-timelapse-local" style="width:auto;margin:0" onchange="_toggleTlInterval()">
|
||||
<label id="lbl-timelapse-local" style="margin:0;cursor:pointer" for="s-timelapse-local">Lokales Timelapse während Druck</label>
|
||||
</div>
|
||||
<div class="modal-field" id="timelapse-interval-row" style="display:none">
|
||||
<label id="lbl-timelapse-interval" style="font-size:12px;color:var(--txt2)">Aufnahme-Intervall (Sekunden)</label>
|
||||
<input type="number" id="s-timelapse-interval" min="1" max="60" value="4" style="width:80px">
|
||||
</div>
|
||||
<div class="modal-field" style="flex-direction:row;align-items:center;gap:10px">
|
||||
<input type="checkbox" id="s-timelapse-printer" style="width:auto;margin:0">
|
||||
<label id="lbl-timelapse-printer" style="margin:0;cursor:pointer" for="s-timelapse-printer">Drucker-Timelapse aktivieren (experimentell)</label>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<div class="modal-section" id="modal-sec-layout">Layout</div>
|
||||
<div class="modal-field" style="flex-direction:row;align-items:center;gap:10px;flex-wrap:wrap">
|
||||
<label id="lbl-layout" style="margin:0;font-size:12px;color:var(--txt2)">Dashboard-Layout</label>
|
||||
<select id="s-layout" style="width:auto;margin:0">
|
||||
<option value="1col" id="opt-layout-1col">1 Spalte</option>
|
||||
<option value="2col" id="opt-layout-2col">2 Spalten</option>
|
||||
</select>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
@@ -201,7 +224,7 @@
|
||||
<div class="panel active" id="panel-dashboard">
|
||||
<div class="grid">
|
||||
<!-- Kamera -->
|
||||
<div class="card" style="grid-column:1/-1">
|
||||
<div class="card" id="d-cam-card" style="grid-column:1/-1">
|
||||
<div style="display:flex;align-items:center;justify-content:space-between;margin-bottom:10px">
|
||||
<div class="card-title" style="margin-bottom:0"><span>📷</span> <span id="d-card-cam">Kamera</span></div>
|
||||
<div style="display:flex;align-items:center;gap:10px">
|
||||
@@ -225,7 +248,7 @@
|
||||
</div>
|
||||
|
||||
<!-- Fortschritt -->
|
||||
<div class="card" style="grid-column:1/-1">
|
||||
<div class="card" id="d-progress-card" style="grid-column:1/-1">
|
||||
<div class="card-title"><span>◉</span> <span id="d-card-progress">Fortschritt</span></div>
|
||||
<img id="d-thumbnail" src="" alt="" style="display:none;width:100%;max-height:160px;object-fit:contain;border-radius:8px;background:#111;margin-bottom:10px">
|
||||
<div class="pct-big"><span id="d-pct">0</span><small>%</small></div>
|
||||
@@ -259,7 +282,7 @@
|
||||
</div>
|
||||
|
||||
<!-- Temperatursteuerung + Verlauf -->
|
||||
<div class="card" style="grid-column:1/-1">
|
||||
<div class="card" id="d-temps-card" style="grid-column:1/-1">
|
||||
<div class="card-title"><span>⊙</span> <span id="d-card-temps">Temperaturen</span></div>
|
||||
<div class="temp-card-inner">
|
||||
<div class="temp-block">
|
||||
@@ -409,8 +432,15 @@
|
||||
<div class="card">
|
||||
<div class="card-title" style="display:flex;justify-content:space-between;align-items:center;margin-bottom:10px">
|
||||
<span id="store-panel-title">🗂 Datei-Browser</span>
|
||||
<button id="store-refresh-btn" onclick="loadStore()" style="font-size:12px;padding:4px 12px;background:var(--raised);border:1px solid var(--border);border-radius:6px;color:var(--txt2);cursor:pointer">↻ Aktualisieren</button>
|
||||
<div style="display:flex;gap:6px;align-items:center">
|
||||
<button id="store-tab-files" class="store-tab-btn active" onclick="switchStoreTab('files')">📁 <span id="store-tab-files-label">Dateien</span></button>
|
||||
<button id="store-tab-timelapses" class="store-tab-btn" onclick="switchStoreTab('timelapses')">🎬 <span id="store-tab-tl-label">Timelapses</span></button>
|
||||
<button id="store-refresh-btn" onclick="refreshStoreTab()" style="font-size:12px;padding:4px 12px;background:var(--raised);border:1px solid var(--border);border-radius:6px;color:var(--txt2);cursor:pointer">↻</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Dateien-Bereich -->
|
||||
<div id="store-files-section">
|
||||
<div style="display:flex;gap:8px;margin-bottom:12px;flex-wrap:wrap">
|
||||
<input id="store-search" type="text" placeholder="🔍 Suche…" oninput="renderStore()"
|
||||
style="flex:1;min-width:140px;padding:6px 10px;background:var(--raised);border:1px solid var(--border);border-radius:6px;color:var(--txt);font-size:13px">
|
||||
@@ -441,6 +471,12 @@
|
||||
<div id="store-empty" style="display:none;color:var(--txt2);text-align:center;padding:40px 0;font-size:14px">
|
||||
</div>
|
||||
<div id="store-grid" style="display:grid;grid-template-columns:repeat(auto-fill,minmax(200px,1fr));gap:14px"></div>
|
||||
</div><!-- /store-files-section -->
|
||||
|
||||
<!-- Timelapse-Bereich -->
|
||||
<div id="store-timelapses-section" style="display:none">
|
||||
<div id="timelapse-list"><div style="text-align:center;padding:40px;color:var(--txt2)" id="timelapse-empty-msg">Keine Timelapses vorhanden</div></div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
|
||||
@@ -313,3 +313,25 @@ nav.bottom-nav{display:none;position:fixed;bottom:0;left:0;right:0;
|
||||
.modal-box{padding:16px;border-radius:10px}
|
||||
.poll-btns{gap:6px}
|
||||
}
|
||||
|
||||
/* ── 2-Column Dashboard Layout ── */
|
||||
#panel-dashboard.layout-2col .grid{grid-template-columns:1fr 1fr}
|
||||
@media(max-width:768px){
|
||||
#panel-dashboard.layout-2col .grid{grid-template-columns:1fr}
|
||||
}
|
||||
|
||||
/* ── Store tabs ── */
|
||||
.store-tab-btn{padding:6px 16px;border-radius:6px;border:1px solid var(--border);
|
||||
background:var(--raised);color:var(--txt2);cursor:pointer;font-size:13px;font-weight:600;transition:.12s}
|
||||
.store-tab-btn.active{background:var(--accent);border-color:var(--accent);color:#000}
|
||||
|
||||
/* ── Timelapse list ── */
|
||||
.tl-row{display:flex;align-items:center;gap:12px;padding:10px;
|
||||
border:1px solid var(--border);border-radius:8px;margin-bottom:8px}
|
||||
.tl-icon{font-size:28px;color:var(--txt2);flex-shrink:0}
|
||||
.tl-info{flex:1;min-width:0}
|
||||
.tl-name{font-size:13px;white-space:nowrap;overflow:hidden;text-overflow:ellipsis}
|
||||
.tl-meta{font-size:11px;color:var(--txt2);margin-top:2px}
|
||||
.tl-status-ok{color:var(--ok)}
|
||||
.tl-status-err{color:var(--err)}
|
||||
.tl-status-busy{color:var(--warn)}
|
||||
|
||||
@@ -144,6 +144,18 @@
|
||||
"settings_flow_calibration": "Flow-Kalibrierung vor Druck",
|
||||
"settings_camera_on_print": "Kamera bei Druckstart einschalten",
|
||||
"settings_web_upload_warning": "Warnung bei Web-Upload-Druck anzeigen",
|
||||
"settings_timelapse_local": "Lokales Timelapse während Druck",
|
||||
"settings_timelapse_interval": "Aufnahme-Intervall (Sekunden)",
|
||||
"settings_timelapse_printer": "Drucker-Timelapse aktivieren (experimentell)",
|
||||
"settings_layout": "Dashboard-Layout",
|
||||
"settings_layout_1col": "1 Spalte",
|
||||
"settings_layout_2col": "2 Spalten",
|
||||
"store_tab_files": "Dateien",
|
||||
"timelapse_tab": "Timelapses",
|
||||
"timelapse_empty": "Noch keine Timelapses vorhanden",
|
||||
"timelapse_recording": "Aufnahme läuft…",
|
||||
"timelapse_processing": "Kodierung läuft…",
|
||||
"confirm_delete_timelapse": "Dieses Timelapse löschen?",
|
||||
"update_check": "Auf Updates prüfen",
|
||||
"update_checking": "Prüfe...",
|
||||
"update_available": "verfügbar",
|
||||
|
||||
@@ -144,6 +144,18 @@
|
||||
"settings_flow_calibration": "Flow Calibration before print",
|
||||
"settings_camera_on_print": "Turn camera on at print start",
|
||||
"settings_web_upload_warning": "Show warning when printing web uploads",
|
||||
"settings_timelapse_local": "Local timelapse during print",
|
||||
"settings_timelapse_interval": "Capture interval (seconds)",
|
||||
"settings_timelapse_printer": "Enable printer timelapse (experimental)",
|
||||
"settings_layout": "Dashboard layout",
|
||||
"settings_layout_1col": "1 column",
|
||||
"settings_layout_2col": "2 columns",
|
||||
"store_tab_files": "Files",
|
||||
"timelapse_tab": "Timelapses",
|
||||
"timelapse_empty": "No timelapses yet",
|
||||
"timelapse_recording": "Recording…",
|
||||
"timelapse_processing": "Encoding…",
|
||||
"confirm_delete_timelapse": "Delete this timelapse?",
|
||||
"update_check": "Check for Updates",
|
||||
"update_checking": "Checking...",
|
||||
"update_available": "available",
|
||||
|
||||
@@ -144,6 +144,18 @@
|
||||
"settings_flow_calibration": "Calibración de flujo antes de imprimir",
|
||||
"settings_camera_on_print": "Encender cámara al iniciar impresión",
|
||||
"settings_web_upload_warning": "Mostrar advertencia al imprimir subidas web",
|
||||
"settings_timelapse_local": "Timelapse local durante la impresión",
|
||||
"settings_timelapse_interval": "Intervalo de captura (segundos)",
|
||||
"settings_timelapse_printer": "Activar timelapse de impresora (experimental)",
|
||||
"settings_layout": "Diseño del panel",
|
||||
"settings_layout_1col": "1 columna",
|
||||
"settings_layout_2col": "2 columnas",
|
||||
"store_tab_files": "Archivos",
|
||||
"timelapse_tab": "Timelapses",
|
||||
"timelapse_empty": "Aún no hay timelapses",
|
||||
"timelapse_recording": "Grabando…",
|
||||
"timelapse_processing": "Codificando…",
|
||||
"confirm_delete_timelapse": "¿Eliminar este timelapse?",
|
||||
"update_check": "Buscar actualizaciones",
|
||||
"update_checking": "Comprobando...",
|
||||
"update_available": "disponible",
|
||||
|
||||
@@ -144,6 +144,18 @@
|
||||
"settings_flow_calibration": "打印前流量校准",
|
||||
"settings_camera_on_print": "打印开始时开启相机",
|
||||
"settings_web_upload_warning": "打印网页上传文件时显示警告",
|
||||
"settings_timelapse_local": "打印期间本地延时摄影",
|
||||
"settings_timelapse_interval": "拍摄间隔(秒)",
|
||||
"settings_timelapse_printer": "启用打印机延时摄影(实验性)",
|
||||
"settings_layout": "仪表板布局",
|
||||
"settings_layout_1col": "单列",
|
||||
"settings_layout_2col": "双列",
|
||||
"store_tab_files": "文件",
|
||||
"timelapse_tab": "延时视频",
|
||||
"timelapse_empty": "暂无延时视频",
|
||||
"timelapse_recording": "录制中…",
|
||||
"timelapse_processing": "编码中…",
|
||||
"confirm_delete_timelapse": "删除此延时视频?",
|
||||
"update_check": "检查更新",
|
||||
"update_checking": "检查中...",
|
||||
"update_available": "可用",
|
||||
|
||||
Reference in New Issue
Block a user