feat: GCode Web-Upload + Download + Verify-Dialog (PR #32)
Übernommen mit Anpassungen aus PR #32 von @gangoke: - Drag&Drop GCode-Upload - Download-Button pro Datei - Web-Upload-Verify-Dialog (persistent Flag, global abschaltbar) Review-Fixes: - Content-Disposition mit RFC5987 filename*= + ASCII-Fallback - DE-Strings übersetzt Dev-Repo: viewit/KX-Bridge@7c834bc Co-authored-by: gangoke <gangoke@noreply.localhost> Co-committed-by: gangoke <gangoke@noreply.localhost>
This commit is contained in:
@@ -29,6 +29,9 @@ default_ams_slot = auto
|
||||
# Auto-Leveling vor jedem Druck (1 = an, 0 = aus)
|
||||
auto_leveling = 1
|
||||
|
||||
# Warnung vor Druck von Web-Uploads (1 = an, 0 = aus)
|
||||
web_upload_warning = 1
|
||||
|
||||
[bridge]
|
||||
# Poll-Intervall in Sekunden
|
||||
poll_interval = 3
|
||||
|
||||
@@ -60,6 +60,7 @@ def _load_config_file(path: pathlib.Path):
|
||||
"DEFAULT_AMS_SLOT": (CONFIG_SECTION_PRINT, "default_ams_slot"),
|
||||
"AUTO_LEVELING": (CONFIG_SECTION_PRINT, "auto_leveling"),
|
||||
"CAMERA_ON_PRINT": (CONFIG_SECTION_PRINT, "camera_on_print"),
|
||||
"WEB_UPLOAD_WARNING": (CONFIG_SECTION_PRINT, "web_upload_warning"),
|
||||
"BRIDGE_PRINTER_NAME": (CONFIG_SECTION_BRIDGE, "printer_name"),
|
||||
}
|
||||
for env_key, (section, option) in mapping.items():
|
||||
@@ -97,6 +98,7 @@ def migrate_env_to_config(env_path: pathlib.Path, config_path: pathlib.Path):
|
||||
"default_ams_slot": env_vals.get("DEFAULT_AMS_SLOT", "auto"),
|
||||
"auto_leveling": env_vals.get("AUTO_LEVELING", "1"),
|
||||
"camera_on_print": env_vals.get("CAMERA_ON_PRINT", "0"),
|
||||
"web_upload_warning": env_vals.get("WEB_UPLOAD_WARNING", "1"),
|
||||
}
|
||||
cfg[CONFIG_SECTION_BRIDGE] = {
|
||||
"poll_interval": "3",
|
||||
@@ -178,3 +180,4 @@ DEVICE_ID = get("DEVICE_ID", "")
|
||||
DEFAULT_AMS_SLOT = get("DEFAULT_AMS_SLOT", "auto")
|
||||
AUTO_LEVELING = int(get("AUTO_LEVELING","1"))
|
||||
CAMERA_ON_PRINT = int(get("CAMERA_ON_PRINT","0"))
|
||||
WEB_UPLOAD_WARNING = int(get("WEB_UPLOAD_WARNING", "1"))
|
||||
|
||||
@@ -362,7 +362,8 @@ class GCodeStore:
|
||||
layer_count INTEGER,
|
||||
gcode_filaments TEXT,
|
||||
objects_skip_parts TEXT,
|
||||
svg_image TEXT
|
||||
svg_image TEXT,
|
||||
web_unverified INTEGER NOT NULL DEFAULT 0
|
||||
);
|
||||
CREATE TABLE IF NOT EXISTS print_jobs (
|
||||
id TEXT PRIMARY KEY,
|
||||
@@ -389,10 +390,17 @@ class GCodeStore:
|
||||
self._conn.commit()
|
||||
except Exception:
|
||||
pass
|
||||
# Migration: Flag für Web-Uploads (Warnhinweis vor Druck)
|
||||
try:
|
||||
self._conn.execute("ALTER TABLE gcode_files ADD COLUMN web_unverified INTEGER NOT NULL DEFAULT 0")
|
||||
self._conn.commit()
|
||||
except Exception:
|
||||
pass
|
||||
|
||||
def save_file(self, file_id: str, filename: str, data: bytes,
|
||||
est_time_sec: int = 0, thumbnail_b64: str = "",
|
||||
gcode_filaments: list | None = None) -> str:
|
||||
gcode_filaments: list | None = None,
|
||||
web_unverified: bool = False) -> str:
|
||||
"""Speichert GCode-Datei auf Disk und in DB. Gibt Pfad zurück."""
|
||||
safe_name = os.path.basename(filename)
|
||||
path = os.path.join(self._gcode_dir, safe_name)
|
||||
@@ -403,9 +411,9 @@ class GCodeStore:
|
||||
filaments_json = json.dumps(gcode_filaments) if gcode_filaments else None
|
||||
self._conn.execute(
|
||||
"""INSERT OR REPLACE INTO gcode_files
|
||||
(id, filename, path, size_bytes, uploaded_at, thumbnail_b64, est_print_time_sec, gcode_filaments)
|
||||
VALUES (?,?,?,?,?,?,?,?)""",
|
||||
(file_id, filename, path, len(data), now, thumbnail_b64 or None, est_time_sec or None, filaments_json)
|
||||
(id, filename, path, size_bytes, uploaded_at, thumbnail_b64, est_print_time_sec, gcode_filaments, web_unverified)
|
||||
VALUES (?,?,?,?,?,?,?,?,?)""",
|
||||
(file_id, filename, path, len(data), now, thumbnail_b64 or None, est_time_sec or None, filaments_json, 1 if web_unverified else 0)
|
||||
)
|
||||
self._conn.commit()
|
||||
return path
|
||||
@@ -453,6 +461,15 @@ class GCodeStore:
|
||||
)
|
||||
self._conn.commit()
|
||||
|
||||
def clear_web_unverified(self, file_id: str) -> bool:
|
||||
with self._lock:
|
||||
cur = self._conn.execute(
|
||||
"UPDATE gcode_files SET web_unverified=0 WHERE id=?",
|
||||
(file_id,),
|
||||
)
|
||||
self._conn.commit()
|
||||
return cur.rowcount > 0
|
||||
|
||||
def delete_file(self, file_id: str) -> bool:
|
||||
row = self.get_file(file_id)
|
||||
if not row:
|
||||
@@ -1484,6 +1501,7 @@ class KobraXBridge:
|
||||
for j in reversed(jobs):
|
||||
last_job[j["gcode_file_id"]] = j
|
||||
for f in files:
|
||||
f["web_unverified"] = bool(f.get("web_unverified"))
|
||||
lj = last_job.get(f["id"])
|
||||
f["last_print_status"] = lj["status"] if lj else None
|
||||
f["last_print_duration"] = lj["duration_sec"] if lj else None
|
||||
@@ -1496,6 +1514,25 @@ class KobraXBridge:
|
||||
return self._json_cors({"result": "ok"})
|
||||
return self._json_cors({"error": "not found"}, status=404)
|
||||
|
||||
async def handle_kx_file_download(self, request):
|
||||
file_id = request.match_info["file_id"]
|
||||
f = self._store.get_file(file_id)
|
||||
if not f:
|
||||
return self._json_cors({"error": "not found"}, status=404)
|
||||
path = f.get("path") or ""
|
||||
if not path or not os.path.isfile(path):
|
||||
return self._json_cors({"error": "not found"}, status=404)
|
||||
filename = os.path.basename(f.get("filename") or path)
|
||||
return web.FileResponse(path, headers={
|
||||
"Content-Disposition": f'attachment; filename="{filename}"'
|
||||
})
|
||||
|
||||
async def handle_kx_file_verify(self, request):
|
||||
file_id = request.match_info["file_id"]
|
||||
if self._store.clear_web_unverified(file_id):
|
||||
return self._json_cors({"result": "ok"})
|
||||
return self._json_cors({"error": "not found"}, status=404)
|
||||
|
||||
async def handle_kx_filament_slots(self, request):
|
||||
slots = []
|
||||
for i, s in enumerate(self._ams_slots):
|
||||
@@ -1811,6 +1848,7 @@ class KobraXBridge:
|
||||
if "multipart" not in ct:
|
||||
return web.json_response({"error": "expected multipart"}, status=400)
|
||||
auto_print = False
|
||||
web_upload = False
|
||||
reader = await request.multipart()
|
||||
file_data = None
|
||||
remote_filename = self._last_uploaded_file or "upload.gcode"
|
||||
@@ -1827,6 +1865,9 @@ class KobraXBridge:
|
||||
elif part.name == "print":
|
||||
val = (await part.read()).decode("utf-8", errors="replace").strip().lower()
|
||||
auto_print = val == "true"
|
||||
elif part.name == "web_upload":
|
||||
val = (await part.read()).decode("utf-8", errors="replace").strip().lower()
|
||||
web_upload = val == "true"
|
||||
else:
|
||||
log.debug(f"Unbekanntes Multipart-Feld: {part.name}")
|
||||
|
||||
@@ -1850,6 +1891,7 @@ class KobraXBridge:
|
||||
est_time_sec=est_time,
|
||||
thumbnail_b64=thumbnail_b64,
|
||||
gcode_filaments=gcode_filaments or None,
|
||||
web_unverified=web_upload,
|
||||
)
|
||||
serve_path = os.path.join(self._serve_dir_path, os.path.basename(remote_filename))
|
||||
del file_data # RAM freigeben
|
||||
@@ -2651,6 +2693,7 @@ class KobraXBridge:
|
||||
"camera_url": s["camera_url"],
|
||||
"fan_speed": s["fan_speed"],
|
||||
"print_speed_mode": s["print_speed_mode"],
|
||||
"web_upload_warning": getattr(self._args, "web_upload_warning", 1),
|
||||
"light_on": s["light_on"],
|
||||
"light_brightness": s["light_brightness"],
|
||||
"ams_slots": self._ams_slots,
|
||||
@@ -2743,7 +2786,8 @@ class KobraXBridge:
|
||||
"default_ams_slot": getattr(self._args, "default_ams_slot", "auto"),
|
||||
"auto_leveling": getattr(self._args, "auto_leveling", 1),
|
||||
"camera_on_print": getattr(self._args, "camera_on_print", 0),
|
||||
"ace_dry_presets": self._ace_dry_presets,
|
||||
"web_upload_warning": getattr(self._args, "web_upload_warning", 1),
|
||||
"ace_dry_presets": self._ace_dry_presets,
|
||||
})
|
||||
|
||||
async def handle_api_settings_post(self, request):
|
||||
@@ -2772,6 +2816,7 @@ class KobraXBridge:
|
||||
cfg.set("print", "default_ams_slot", str(data.get("default_ams_slot", getattr(self._args, "default_ams_slot", "auto"))))
|
||||
cfg.set("print", "auto_leveling", str(data.get("auto_leveling", getattr(self._args, "auto_leveling", 1))))
|
||||
cfg.set("print", "camera_on_print", str(int(bool(data.get("camera_on_print", getattr(self._args, "camera_on_print", 0))))))
|
||||
cfg.set("print", "web_upload_warning", str(int(bool(data.get("web_upload_warning", getattr(self._args, "web_upload_warning", 1))))))
|
||||
if not cfg.has_option("bridge", "poll_interval"):
|
||||
cfg.set("bridge", "poll_interval", "3")
|
||||
printer_name = str(data.get("printer_name", "")).strip()
|
||||
@@ -3451,6 +3496,8 @@ def build_app(bridge: KobraXBridge) -> web.Application:
|
||||
r.add_post("/kx/print", bridge.handle_kx_print)
|
||||
r.add_get("/kx/files", bridge.handle_kx_files)
|
||||
r.add_delete("/kx/files/{file_id}", bridge.handle_kx_file_delete)
|
||||
r.add_get("/kx/files/{file_id}/download", bridge.handle_kx_file_download)
|
||||
r.add_post("/kx/files/{file_id}/verify", bridge.handle_kx_file_verify)
|
||||
r.add_get("/kx/filament/slots", bridge.handle_kx_filament_slots)
|
||||
r.add_get("/kx/history", bridge.handle_kx_history)
|
||||
r.add_get("/kx/ui/{name}", bridge.handle_kx_ui_asset)
|
||||
@@ -3617,6 +3664,7 @@ def main():
|
||||
parser.add_argument("--default-ams-slot",default=env_loader.DEFAULT_AMS_SLOT)
|
||||
parser.add_argument("--auto-leveling", type=int, default=env_loader.AUTO_LEVELING)
|
||||
parser.add_argument("--camera-on-print", type=int, default=env_loader.CAMERA_ON_PRINT)
|
||||
parser.add_argument("--web-upload-warning", type=int, default=env_loader.WEB_UPLOAD_WARNING)
|
||||
|
||||
parser.add_argument("--host", default="0.0.0.0",
|
||||
help="Bind-Adresse für den Bridge-Server")
|
||||
|
||||
@@ -3,7 +3,7 @@ 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,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:[]}};
|
||||
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 currentStep=1;
|
||||
@@ -113,6 +113,7 @@ var LANG_DE={
|
||||
settings_save:'Speichern & Neustart',settings_printer_name:'Drucker-Name',settings_printer_ip:'Drucker-IP',settings_mqtt_port:'MQTT-Port',
|
||||
settings_username:'MQTT-Benutzername',settings_password:'MQTT-Passwort',settings_device_id:'Device-ID',settings_mode_id:'Mode-ID',hint_ip_no_port:'Nur IP-Adresse, kein Port (z.B. 192.168.1.102)',
|
||||
settings_default_slot:'Standard-Slot (Einfarbdruck)',settings_slot_auto:'Auto (alle belegten Slots)',settings_auto_leveling:'Auto-Leveling vor Druck',settings_camera_on_print:'Kamera bei Druckstart einschalten',
|
||||
settings_web_upload_warning:'Warnung bei Web-Upload-Druck anzeigen',
|
||||
update_check:'Auf Updates prüfen',update_checking:'Prüfe...',update_available:'verfügbar',update_none:'Bereits aktuell',
|
||||
update_apply:'Jetzt installieren',update_applying:'Lade herunter...',update_restarting:'Starte neu...',update_error:'Fehler',
|
||||
btn_connect:'⚡ Verbinden',btn_disconnect:'✕ Trennen',
|
||||
@@ -145,8 +146,13 @@ var LANG_DE={
|
||||
store_empty:'Noch keine Dateien hochgeladen.',
|
||||
store_refresh:'↻ Aktualisieren',
|
||||
store_print:'▶ Drucken',
|
||||
store_download:'⬇ Download',
|
||||
store_delete_confirm:'Datei löschen?',
|
||||
store_print_confirm:'Datei drucken?',
|
||||
store_web_verify_title:'Datei verifizieren',
|
||||
store_web_verify_msg:'Please verify this file was made for Anycubic Kobra X.',
|
||||
store_web_verify_confirm:'Confirm',
|
||||
store_web_verify_abort:'Abort',
|
||||
store_no_results:'Keine Dateien gefunden.',
|
||||
store_never:'noch nicht gedruckt',
|
||||
sf_all:'Alle',sf_ok:'✓ Erfolgreich',sf_err:'✗ Fehler',sf_new:'Neu',
|
||||
@@ -176,6 +182,7 @@ var LANG_EN={
|
||||
settings_save:'Save & Restart',settings_printer_name:'Printer Name',settings_printer_ip:'Printer IP',settings_mqtt_port:'MQTT Port',
|
||||
settings_username:'MQTT Username',settings_password:'MQTT Password',settings_device_id:'Device ID',settings_mode_id:'Mode ID',hint_ip_no_port:'IP address only, no port (e.g. 192.168.1.102)',
|
||||
settings_default_slot:'Default Slot (single color)',settings_slot_auto:'Auto (all loaded slots)',settings_auto_leveling:'Auto-Leveling before print',settings_camera_on_print:'Turn camera on at print start',
|
||||
settings_web_upload_warning:'Show warning when printing web uploads',
|
||||
update_check:'Check for Updates',update_checking:'Checking...',update_available:'available',update_none:'Already up to date',
|
||||
update_apply:'Install Now',update_applying:'Downloading...',update_restarting:'Restarting...',update_error:'Error',
|
||||
btn_connect:'⚡ Connect',btn_disconnect:'✕ Disconnect',
|
||||
@@ -208,8 +215,13 @@ var LANG_EN={
|
||||
store_empty:'No files uploaded yet.',
|
||||
store_refresh:'↻ Refresh',
|
||||
store_print:'▶ Print',
|
||||
store_download:'⬇ Download',
|
||||
store_delete_confirm:'Delete file?',
|
||||
store_print_confirm:'Print file?',
|
||||
store_web_verify_title:'Verify file',
|
||||
store_web_verify_msg:'Please verify this file was made for Anycubic Kobra X.',
|
||||
store_web_verify_confirm:'Confirm',
|
||||
store_web_verify_abort:'Abort',
|
||||
store_no_results:'No files found.',
|
||||
store_never:'never printed',
|
||||
sf_all:'All',sf_ok:'✓ Completed',sf_err:'✗ Failed',sf_new:'New',
|
||||
@@ -311,6 +323,10 @@ function applyLang(){
|
||||
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);
|
||||
// Dashboard card titles
|
||||
setText('d-card-progress',T.card_progress);
|
||||
setText('d-card-temps',T.card_temps);
|
||||
@@ -363,6 +379,7 @@ function applyLang(){
|
||||
setText('opt-slot-auto',T.settings_slot_auto);
|
||||
setText('lbl-auto-leveling',T.settings_auto_leveling);
|
||||
setText('lbl-camera-on-print',T.settings_camera_on_print);
|
||||
setText('lbl-web-upload-warning',T.settings_web_upload_warning);
|
||||
|
||||
setText('lbl-update-check',T.update_check);
|
||||
setText('lbl-update-apply',T.update_apply);
|
||||
@@ -910,6 +927,7 @@ function openSettings(){
|
||||
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 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 v=localStorage.getItem('pollInterval')||'2000';
|
||||
document.querySelectorAll('.poll-btn').forEach(function(b){b.classList.remove('active')});
|
||||
@@ -978,6 +996,11 @@ function slotEditFeed(){
|
||||
.catch(function(){});
|
||||
}
|
||||
function startReadyFile(){
|
||||
var currentFile=(storeFiles||[]).find(function(f){return f.filename===S.file_ready;});
|
||||
if(currentFile && currentFile.web_unverified && webUploadWarningEnabled()){
|
||||
maybeGateWebUpload(currentFile, function(){ startReadyFile(); });
|
||||
return;
|
||||
}
|
||||
var btn=document.getElementById('file-ready-btn');
|
||||
if(btn){btn.disabled=true;btn.textContent='…';}
|
||||
post('/printer/print/start',{filename:S.file_ready})
|
||||
@@ -1040,6 +1063,8 @@ function setPoll(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;
|
||||
post('/api/settings',{
|
||||
printer_name: document.getElementById('s-printer-name').value,
|
||||
printer_ip: document.getElementById('s-printer-ip').value,
|
||||
@@ -1051,6 +1076,7 @@ function saveSettings(){
|
||||
default_ams_slot: document.getElementById('s-default-slot').value,
|
||||
auto_leveling: document.getElementById('s-auto-leveling').checked?1:0,
|
||||
camera_on_print: (document.getElementById('s-camera-on-print')||{}).checked?1:0,
|
||||
web_upload_warning:webUploadWarning,
|
||||
}).then(function(){
|
||||
btn.textContent=T.update_restarting;
|
||||
setTimeout(function(){
|
||||
@@ -1536,6 +1562,39 @@ function loadStore(){
|
||||
}).catch(function(e){clog('Store-Fehler: '+e,'msg-err')});
|
||||
}
|
||||
|
||||
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');
|
||||
if(status) { status.textContent='⏳ Hochladen…'; 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='✓ '+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='✗ '+e.message; status.className='upload-status-err'; }
|
||||
if(label) label.style.display='';
|
||||
if(zone) zone.style.pointerEvents='';
|
||||
clog('Upload-Fehler: '+e,'msg-err');
|
||||
});
|
||||
}
|
||||
|
||||
function renderStore(){
|
||||
var grid=document.getElementById('store-grid');
|
||||
var empty=document.getElementById('store-empty');
|
||||
@@ -1606,6 +1665,8 @@ function renderStore(){
|
||||
'<div style="display:flex;gap:6px;margin-top:auto">'+
|
||||
'<button onclick="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="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="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>'+
|
||||
@@ -1621,6 +1682,9 @@ function formatDur(sec){
|
||||
var _storeFileId=null;
|
||||
var _storeFilename=null;
|
||||
var _filamentDialogMode='store'; // 'store' oder 'banner'
|
||||
var _pendingWebVerifyFileId=null;
|
||||
var _pendingWebVerifyFilename='';
|
||||
var _pendingWebVerifyAction=null;
|
||||
// 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).
|
||||
@@ -1646,15 +1710,120 @@ function storePrint(fileId, filename){
|
||||
_storeFileId=fileId;
|
||||
_storeFilename=filename;
|
||||
_filamentDialogMode='store';
|
||||
// GCode-Filamente aus Store-Datei holen (für Vorschau im Dialog)
|
||||
var fileObj=storeFiles.find(function(f){return f.id===fileId;});
|
||||
_setGcodeFilamentsFromFileObj(fileObj);
|
||||
fetch(_apiUrl('/kx/filament/slots')).then(function(r){return r.json()}).then(function(d){
|
||||
openFilamentDialog(d.result||[]);
|
||||
}).catch(function(){openFilamentDialog([]);});
|
||||
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('Verifizierungs-Fehler: '+e,'msg-err');
|
||||
});
|
||||
}
|
||||
|
||||
function maybeGateWebUpload(fileObj, onContinue){
|
||||
if(!fileObj || !fileObj.web_unverified){
|
||||
if(onContinue) onContinue();
|
||||
return;
|
||||
}
|
||||
if(!webUploadWarningEnabled()){
|
||||
if(onContinue) onContinue();
|
||||
return;
|
||||
}
|
||||
openWebVerifyDialog(fileObj.id, fileObj.filename, function(){
|
||||
clearWebUploadWarningFlag(fileObj.id, onContinue);
|
||||
});
|
||||
}
|
||||
|
||||
function openWebVerifyDialog(fileId, filename, onConfirm){
|
||||
_pendingWebVerifyFileId=fileId;
|
||||
_pendingWebVerifyFilename=filename;
|
||||
_pendingWebVerifyAction=onConfirm||null;
|
||||
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');}
|
||||
_pendingWebVerifyFileId=null;
|
||||
_pendingWebVerifyFilename='';
|
||||
_pendingWebVerifyAction=null;
|
||||
}
|
||||
|
||||
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;}
|
||||
_pendingWebVerifyFileId=null;
|
||||
_pendingWebVerifyFilename='';
|
||||
_pendingWebVerifyAction=null;
|
||||
closeStoreWebVerifyDialog();
|
||||
loadStore();
|
||||
if(typeof action==='function') action();
|
||||
})
|
||||
.catch(function(e){
|
||||
if(status){status.textContent='✗ '+e.message;}
|
||||
clog('Verifizierungs-Fehler: '+e,'msg-err');
|
||||
});
|
||||
}
|
||||
|
||||
function startReadyFileWithSlots(){
|
||||
var currentFile=(storeFiles||[]).find(function(f){return f.filename===S.file_ready;});
|
||||
if(currentFile && currentFile.web_unverified && webUploadWarningEnabled()){
|
||||
maybeGateWebUpload(currentFile, function(){ startReadyFileWithSlots(); });
|
||||
return;
|
||||
}
|
||||
_filamentDialogMode='banner';
|
||||
_storeFilename=S.file_ready||'';
|
||||
// Banner must never reuse stale store-file context.
|
||||
@@ -2074,6 +2243,15 @@ function storeDelete(fileId){
|
||||
});
|
||||
}
|
||||
|
||||
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='';
|
||||
|
||||
@@ -98,6 +98,10 @@
|
||||
<input type="checkbox" id="s-camera-on-print" style="width:auto;margin:0">
|
||||
<label id="lbl-camera-on-print" style="margin:0;cursor:pointer" for="s-camera-on-print">Kamera bei Druckstart einschalten</label>
|
||||
</div>
|
||||
<div class="modal-field" style="flex-direction:row;align-items:center;gap:10px">
|
||||
<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>
|
||||
|
||||
<div>
|
||||
@@ -400,6 +404,16 @@
|
||||
<option value="duration_asc" id="ss-dur">⏱ Druckzeit</option>
|
||||
</select>
|
||||
</div>
|
||||
<div id="store-upload-zone" onclick="document.getElementById('store-upload-input').click()"
|
||||
ondragover="event.preventDefault();this.classList.add('drag-over')"
|
||||
ondragleave="this.classList.remove('drag-over')"
|
||||
ondrop="event.preventDefault();this.classList.remove('drag-over');uploadGcode(event.dataTransfer.files[0])">
|
||||
<input type="file" id="store-upload-input" accept=".gcode,.bgcode"
|
||||
style="display:none" onchange="uploadGcode(this.files[0]);this.value=''">
|
||||
<span id="store-upload-icon">⬆</span>
|
||||
<span id="store-upload-label">GCode hierher ziehen oder <u>durchsuchen</u></span>
|
||||
<span id="store-upload-status" style="display:none"></span>
|
||||
</div>
|
||||
<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>
|
||||
@@ -451,6 +465,22 @@
|
||||
</nav>
|
||||
|
||||
|
||||
<!-- Filament-Slot-Dialog -->
|
||||
<div class="modal-overlay" id="store-web-verify-dialog" onclick="if(event.target===this)closeStoreWebVerifyDialog()">
|
||||
<div class="modal-box" style="max-width:420px;width:100%">
|
||||
<div class="modal-header" style="margin-bottom:14px">
|
||||
<span class="modal-title" id="store-web-verify-title">Datei verifizieren</span>
|
||||
<button onclick="closeStoreWebVerifyDialog()" style="background:none;border:none;font-size:18px;cursor:pointer;color:var(--txt2)">✕</button>
|
||||
</div>
|
||||
<p id="store-web-verify-msg" style="font-size:13px;color:var(--txt);margin-bottom:12px">Please verify this file was made for Anycubic Kobra X.</p>
|
||||
<div id="store-web-verify-status" style="font-size:12px;color:var(--txt2);min-height:16px;margin-bottom:8px"></div>
|
||||
<div style="display:flex;gap:8px;justify-content:flex-end">
|
||||
<button id="store-web-verify-abort" onclick="closeStoreWebVerifyDialog()" style="padding:8px 16px;background:var(--raised);border:1px solid var(--border);border-radius:8px;color:var(--txt);cursor:pointer">Abort</button>
|
||||
<button id="store-web-verify-confirm" onclick="confirmStoreWebVerify()" style="padding:8px 18px;background:var(--accent);color:#fff;border:none;border-radius:8px;cursor:pointer;font-weight:600">Confirm</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Filament-Slot-Dialog -->
|
||||
<div class="modal-overlay" id="filament-dialog" onclick="if(event.target===this)closeFilamentDialog()">
|
||||
<div class="modal-box" style="max-width:380px;width:100%">
|
||||
|
||||
@@ -212,6 +212,22 @@ canvas.tchart{width:100%;height:60px;display:block;border-radius:6px;background:
|
||||
.panel{display:none}
|
||||
.panel.active{display:block}
|
||||
|
||||
/* ── FILE BROWSER UPLOAD ZONE ── */
|
||||
#store-upload-zone{
|
||||
display:flex;flex-direction:column;align-items:center;justify-content:center;
|
||||
gap:6px;padding:18px 12px;margin-bottom:14px;
|
||||
border:2px dashed var(--border);border-radius:10px;
|
||||
background:var(--raised);color:var(--txt2);
|
||||
cursor:pointer;transition:border-color .15s,background .15s;
|
||||
font-size:13px;text-align:center;user-select:none;
|
||||
}
|
||||
#store-upload-zone:hover{border-color:var(--accent);background:rgba(0,200,255,.06);color:var(--txt)}
|
||||
#store-upload-zone.drag-over{border-color:var(--accent);background:rgba(0,200,255,.12);color:var(--accent)}
|
||||
#store-upload-icon{font-size:22px;line-height:1}
|
||||
.upload-status-busy{color:var(--txt2)}
|
||||
.upload-status-ok{color:var(--ok)}
|
||||
.upload-status-err{color:var(--err)}
|
||||
|
||||
/* ── MODAL ── */
|
||||
.modal-overlay{display:none;position:fixed;inset:0;background:rgba(0,0,0,.6);
|
||||
z-index:200;align-items:center;justify-content:center;padding:16px}
|
||||
|
||||
Reference in New Issue
Block a user