feat(print): add option to delete file from printer after successful print
Frees up the printer's own limited storage automatically once a print finishes, while keeping the file safely in the bridge's own GCode store. Deliberately scoped to files uploaded through the bridge itself only (matched via the GCode store, same lookup the job-history feature already uses) - a print started directly from the printer or Anycubic Slicer has no backup anywhere else, so it's never touched regardless of the setting. Only triggers on a clean "finished" state, not on stopped/canceled prints, since the user may want to retry those. Off by default. The delete request is fire-and-forget, sent directly from _on_print() (which runs on the MQTT reader thread) rather than through the existing _wait_for_file_action() helper - that helper blocks waiting for a reply dispatched from that same thread, which would deadlock if called from within it.
This commit is contained in:
@@ -1,3 +1,4 @@
|
||||
## Changes in this build
|
||||
|
||||
- Fix: **combined ACE-RFID filament tags (e.g. "GEEETECH PLA Bas" from third-party RFID tools) still weren't auto-matching to imported OrcaSlicer profiles**, even after the first attempt at this in a previous nightly — the matching logic was only ever wired into the OrcaSlicer slicer-sync endpoint, never into the actual MQTT status path that feeds the dashboard and Happy-Hare gate data, so the dashboard kept showing the raw unmatched RFID string. Centralized the matching so all three places that resolve a slot's filament profile benefit identically. Also added variant-token disambiguation (e.g. "Bas" vs. "Matte") for when a vendor has multiple profiles of the same material, and fixed a related edge case where a manual per-slot override could be incorrectly treated as stale on an RFID-tagged spool (Issue #101, thanks @Blaim for the extensive debugging that pinpointed this).
|
||||
- Feat: new setting under Settings → Print — "Delete file from printer after successful print" — automatically removes a GCode file from the printer's own storage once it finishes printing successfully, keeping only the copy in the bridge's own GCode store. Off by default, and only ever applies to files that were uploaded through the bridge itself (so there's always a backup); files started directly from the printer or Anycubic Slicer are never touched.
|
||||
|
||||
@@ -67,6 +67,7 @@ CONFIG_ENV_MAPPING = {
|
||||
"VIBRATION_COMPENSATION": (CONFIG_SECTION_PRINT, "vibration_compensation"),
|
||||
"CAMERA_ON_PRINT": (CONFIG_SECTION_PRINT, "camera_on_print"),
|
||||
"WEB_UPLOAD_WARNING": (CONFIG_SECTION_PRINT, "web_upload_warning"),
|
||||
"DELETE_PRINTER_FILE_AFTER_PRINT": (CONFIG_SECTION_PRINT, "delete_printer_file_after_print"),
|
||||
"PRINT_START_DIALOG": (CONFIG_SECTION_PRINT, "print_start_dialog"),
|
||||
"BRIDGE_PRINTER_NAME": (CONFIG_SECTION_BRIDGE, "printer_name"),
|
||||
"BRIDGE_HOST_IP": (CONFIG_SECTION_BRIDGE, "host_ip"),
|
||||
@@ -459,6 +460,7 @@ AUTO_LEVELING = int(get("AUTO_LEVELING", "1"))
|
||||
VIBRATION_COMPENSATION = int(get("VIBRATION_COMPENSATION", "0"))
|
||||
CAMERA_ON_PRINT = int(get("CAMERA_ON_PRINT", "0"))
|
||||
WEB_UPLOAD_WARNING = int(get("WEB_UPLOAD_WARNING", "1"))
|
||||
DELETE_PRINTER_FILE_AFTER_PRINT = int(get("DELETE_PRINTER_FILE_AFTER_PRINT", "0"))
|
||||
PRINT_START_DIALOG = int(get("PRINT_START_DIALOG", get("FILE_READY_DIALOG", "1")))
|
||||
SPOOLMAN_SERVER = get("SPOOLMAN_SERVER", "")
|
||||
SPOOLMAN_SYNC_RATE = int(get("SPOOLMAN_SYNC_RATE", "0"))
|
||||
|
||||
@@ -54,5 +54,6 @@ AUTO_LEVELING = int(get("AUTO_LEVELING", "1"))
|
||||
VIBRATION_COMPENSATION = int(get("VIBRATION_COMPENSATION", "0"))
|
||||
CAMERA_ON_PRINT = int(get("CAMERA_ON_PRINT", "0"))
|
||||
WEB_UPLOAD_WARNING = int(get("WEB_UPLOAD_WARNING", "1"))
|
||||
DELETE_PRINTER_FILE_AFTER_PRINT = int(get("DELETE_PRINTER_FILE_AFTER_PRINT", "0"))
|
||||
PRINT_START_DIALOG = int(get("PRINT_START_DIALOG", get("FILE_READY_DIALOG", "1")))
|
||||
BRIDGE_HOST_IP = get("BRIDGE_HOST_IP", "")
|
||||
|
||||
@@ -1073,6 +1073,11 @@ class KobraXBridge:
|
||||
self._store = store if store is not None else GCodeStore(args.data_dir)
|
||||
self._serve_dir_path: str = self._store._gcode_dir
|
||||
self._current_job_id: str = ""
|
||||
# Filename of the file backing _current_job_id, kept alongside it so
|
||||
# the "finished" handler can still delete it from the printer's own
|
||||
# storage (Issue: delete-after-print) after self._state["filename"]
|
||||
# has already been cleared as part of the terminal-state reset below.
|
||||
self._current_job_filename: str = ""
|
||||
self._camera_autostarted: bool = False
|
||||
self._camera_user_stopped: bool = False # user manually stopped the camera during a print
|
||||
self.camera_cache: CameraCache = CameraCache()
|
||||
@@ -1396,6 +1401,7 @@ class KobraXBridge:
|
||||
gcode_file_id=gf["id"],
|
||||
printer_id=self._printer_id,
|
||||
)
|
||||
self._current_job_filename = filename
|
||||
log.info(f"Job started: {self._current_job_id} for {filename}")
|
||||
self._spoolman_slot_usage = {}
|
||||
self._spoolman_slot_reported = {}
|
||||
@@ -1408,11 +1414,21 @@ class KobraXBridge:
|
||||
log.info(f"Job abgeschlossen: {self._current_job_id}")
|
||||
self._spoolman_notify_end()
|
||||
self._current_job_id = ""
|
||||
# Optional cleanup (Settings -> Print): only for files that are
|
||||
# also backed by the bridge's own GCode store - never for prints
|
||||
# started directly from the printer/Anycubic Slicer, which would
|
||||
# otherwise be deleted with no copy left anywhere (Issue: delete
|
||||
# printer file after successful print). Deliberately only on a
|
||||
# clean "finished" - stoped/canceled prints keep their file.
|
||||
if getattr(self._args, "delete_printer_file_after_print", 0) and self._current_job_filename:
|
||||
self._delete_printer_file_fire_and_forget(self._current_job_filename)
|
||||
self._current_job_filename = ""
|
||||
elif kobra_state in ("stoped", "canceled") and self._current_job_id:
|
||||
self._store.finish_job(self._current_job_id, status="cancelled")
|
||||
log.info(f"Job abgebrochen: {self._current_job_id}")
|
||||
self._spoolman_notify_end()
|
||||
self._current_job_id = ""
|
||||
self._current_job_filename = ""
|
||||
|
||||
# Terminal states (successful finish AND stop/cancel) must leave the
|
||||
# same clean end state - a "finished" print used to only clear
|
||||
@@ -1579,6 +1595,25 @@ class KobraXBridge:
|
||||
if payload.get("state") == "done" or payload.get("code") == 200:
|
||||
log.info(f"Skip response: state={payload.get('state')} code={payload.get('code')} skipped={skipped}")
|
||||
|
||||
def _delete_printer_file_fire_and_forget(self, filename: str) -> None:
|
||||
"""Deletes a file from the printer's own storage without waiting for
|
||||
the response - called from _on_print(), which runs on the MQTT
|
||||
reader thread itself, so blocking here (like _wait_for_file_action
|
||||
does) would deadlock: the file/report reply that would unblock it is
|
||||
dispatched from that same thread. Fire-and-forget is safe because the
|
||||
bridge's own copy in the GCode store is what matters for correctness
|
||||
here; a failed delete just leaves the printer's storage as it is
|
||||
(Settings -> Print -> "Delete file from printer after successful print")."""
|
||||
try:
|
||||
self.client.publish(
|
||||
"file", "deleteBatch",
|
||||
{"root": "local", "files": [{"path": "/", "filename": filename}]},
|
||||
timeout=0,
|
||||
)
|
||||
log.info(f"Requested printer-storage delete for {filename} after successful print")
|
||||
except Exception as e:
|
||||
log.warning(f"Delete-after-print request failed for {filename}: {e}")
|
||||
|
||||
def _wait_for_file_action(self, action: str, send_fn, timeout: float = 8.0) -> dict | None:
|
||||
"""Sends a file/* MQTT request (via send_fn, which must call
|
||||
self.client.publish(..., timeout=0) fire-and-forget) and blocks the
|
||||
@@ -3595,6 +3630,7 @@ class KobraXBridge:
|
||||
printer_id=getattr(self._args, "device_id", "unknown"),
|
||||
filament_assignments=assignments,
|
||||
)
|
||||
self._current_job_filename = filename
|
||||
|
||||
return self._json_cors({"result": "ok", "filename": filename})
|
||||
|
||||
@@ -5044,6 +5080,7 @@ class KobraXBridge:
|
||||
"vibration_compensation": getattr(self._args, "vibration_compensation", 0),
|
||||
"camera_on_print": getattr(self._args, "camera_on_print", 0),
|
||||
"web_upload_warning": getattr(self._args, "web_upload_warning", 1),
|
||||
"delete_printer_file_after_print": getattr(self._args, "delete_printer_file_after_print", 0),
|
||||
"print_start_dialog": getattr(self._args, "print_start_dialog", 1),
|
||||
"poll_interval": getattr(self._args, "poll_interval", 3),
|
||||
"verbose_http_log": getattr(self._args, "verbose_http_log", 0),
|
||||
@@ -5085,6 +5122,7 @@ class KobraXBridge:
|
||||
cfg.set("print", "vibration_compensation", str(int(bool(data.get("vibration_compensation", getattr(self._args, "vibration_compensation", 0))))))
|
||||
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))))))
|
||||
cfg.set("print", "delete_printer_file_after_print", str(int(bool(data.get("delete_printer_file_after_print", getattr(self._args, "delete_printer_file_after_print", 0))))))
|
||||
cfg.set("print", "print_start_dialog", str(int(bool(data.get("print_start_dialog", getattr(self._args, "print_start_dialog", 1))))))
|
||||
if "poll_interval" in data:
|
||||
try:
|
||||
@@ -6267,6 +6305,10 @@ def main():
|
||||
parser.add_argument("--vibration-compensation", type=int, default=env_loader.VIBRATION_COMPENSATION)
|
||||
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("--delete-printer-file-after-print", type=int,
|
||||
default=env_loader.DELETE_PRINTER_FILE_AFTER_PRINT,
|
||||
help="After a successful print, delete the file from the printer's "
|
||||
"own storage if it's also in the bridge's own GCode store")
|
||||
parser.add_argument("--print-start-dialog", dest="print_start_dialog", type=int, default=env_loader.PRINT_START_DIALOG)
|
||||
parser.add_argument("--file-ready-dialog", dest="print_start_dialog", type=int)
|
||||
parser.add_argument("--spoolman-server", default=env_loader.SPOOLMAN_SERVER,
|
||||
|
||||
157
tests/test_delete_printer_file_after_print.py
Normal file
157
tests/test_delete_printer_file_after_print.py
Normal file
@@ -0,0 +1,157 @@
|
||||
"""Optional auto-delete of a printed file from the printer's own storage
|
||||
after a successful print (Settings -> Print -> "Delete file from printer
|
||||
after successful print").
|
||||
|
||||
Only applies to files that are also backed by the bridge's own GCode store
|
||||
(otherwise the file would be gone with no copy left anywhere) and only on a
|
||||
clean "finished" state - not on stoped/canceled prints, and never when the
|
||||
setting is off (the default).
|
||||
"""
|
||||
import argparse
|
||||
import tempfile
|
||||
from unittest.mock import MagicMock
|
||||
|
||||
from kobrax_moonraker_bridge import GCodeStore, KobraXBridge
|
||||
|
||||
|
||||
def _bridge(delete_after_print=1):
|
||||
c = MagicMock()
|
||||
c.callbacks = {}
|
||||
c.connected = False
|
||||
args = argparse.Namespace(
|
||||
printer_ip="", mqtt_port=9883, username="", password="",
|
||||
mode_id="20030", device_id="", host="127.0.0.1", port=7125,
|
||||
data_dir=tempfile.mkdtemp(prefix="kxdelafterprint-"),
|
||||
delete_printer_file_after_print=delete_after_print,
|
||||
)
|
||||
store = GCodeStore(args.data_dir)
|
||||
b = KobraXBridge(c, args=args, store=store)
|
||||
return b, c
|
||||
|
||||
|
||||
def _seed_file(bridge, filename="test.gcode"):
|
||||
file_id = "abc123"
|
||||
bridge._store.save_file(file_id, filename, b"; gcode content")
|
||||
return file_id
|
||||
|
||||
|
||||
def _print_report(state, filename=None):
|
||||
payload = {"state": state, "data": {}}
|
||||
if filename is not None:
|
||||
payload["data"]["filename"] = filename
|
||||
return payload
|
||||
|
||||
|
||||
def test_finished_print_deletes_printer_file_when_enabled_and_in_store():
|
||||
b, c = _bridge(delete_after_print=1)
|
||||
_seed_file(b, "test.gcode")
|
||||
|
||||
b._on_print(_print_report("printing", "test.gcode"))
|
||||
assert b._current_job_id
|
||||
assert b._current_job_filename == "test.gcode"
|
||||
|
||||
b._on_print(_print_report("finished"))
|
||||
|
||||
delete_calls = [
|
||||
call for call in c.publish.call_args_list
|
||||
if call.args[:2] == ("file", "deleteBatch")
|
||||
]
|
||||
assert len(delete_calls) == 1
|
||||
payload = delete_calls[0].args[2]
|
||||
assert payload == {"root": "local", "files": [{"path": "/", "filename": "test.gcode"}]}
|
||||
|
||||
|
||||
def test_finished_print_no_delete_when_setting_disabled():
|
||||
b, c = _bridge(delete_after_print=0)
|
||||
_seed_file(b, "test.gcode")
|
||||
|
||||
b._on_print(_print_report("printing", "test.gcode"))
|
||||
b._on_print(_print_report("finished"))
|
||||
|
||||
delete_calls = [
|
||||
call for call in c.publish.call_args_list
|
||||
if call.args[:2] == ("file", "deleteBatch")
|
||||
]
|
||||
assert delete_calls == []
|
||||
|
||||
|
||||
def test_finished_print_no_delete_when_file_not_in_bridge_store():
|
||||
"""Files started directly from the printer/Anycubic Slicer aren't in the
|
||||
bridge's own GCode store - must never be deleted, since that would leave
|
||||
no copy anywhere."""
|
||||
b, c = _bridge(delete_after_print=1)
|
||||
# No _seed_file() call - the file is not in the store.
|
||||
|
||||
b._on_print(_print_report("printing", "not_in_store.gcode"))
|
||||
assert not b._current_job_id # no store match -> no job tracked either
|
||||
|
||||
b._on_print(_print_report("finished"))
|
||||
|
||||
delete_calls = [
|
||||
call for call in c.publish.call_args_list
|
||||
if call.args[:2] == ("file", "deleteBatch")
|
||||
]
|
||||
assert delete_calls == []
|
||||
|
||||
|
||||
def test_canceled_print_does_not_delete_file():
|
||||
"""Only a clean "finished" triggers the delete - a stopped/canceled
|
||||
print keeps its file, since the user may want to retry it."""
|
||||
b, c = _bridge(delete_after_print=1)
|
||||
_seed_file(b, "test.gcode")
|
||||
|
||||
b._on_print(_print_report("printing", "test.gcode"))
|
||||
b._on_print(_print_report("canceled"))
|
||||
|
||||
delete_calls = [
|
||||
call for call in c.publish.call_args_list
|
||||
if call.args[:2] == ("file", "deleteBatch")
|
||||
]
|
||||
assert delete_calls == []
|
||||
assert b._current_job_filename == ""
|
||||
|
||||
|
||||
def test_stoped_print_does_not_delete_file():
|
||||
b, c = _bridge(delete_after_print=1)
|
||||
_seed_file(b, "test.gcode")
|
||||
|
||||
b._on_print(_print_report("printing", "test.gcode"))
|
||||
b._on_print(_print_report("stoped"))
|
||||
|
||||
delete_calls = [
|
||||
call for call in c.publish.call_args_list
|
||||
if call.args[:2] == ("file", "deleteBatch")
|
||||
]
|
||||
assert delete_calls == []
|
||||
|
||||
|
||||
def test_current_job_filename_reset_after_finished():
|
||||
"""Regression guard: _current_job_filename must not leak into the next
|
||||
print's finished-handling if that next print isn't itself tracked."""
|
||||
b, c = _bridge(delete_after_print=1)
|
||||
_seed_file(b, "test.gcode")
|
||||
|
||||
b._on_print(_print_report("printing", "test.gcode"))
|
||||
b._on_print(_print_report("finished"))
|
||||
assert b._current_job_filename == ""
|
||||
|
||||
# A second "finished" with no new job in between must not re-trigger a delete.
|
||||
c.publish.reset_mock()
|
||||
b._on_print(_print_report("finished"))
|
||||
delete_calls = [
|
||||
call for call in c.publish.call_args_list
|
||||
if call.args[:2] == ("file", "deleteBatch")
|
||||
]
|
||||
assert delete_calls == []
|
||||
|
||||
|
||||
def test_delete_publish_failure_does_not_raise():
|
||||
"""A broken MQTT send during the delete request must not propagate out
|
||||
of _on_print() - it runs on the MQTT reader thread, and an unhandled
|
||||
exception there would break processing of subsequent messages."""
|
||||
b, c = _bridge(delete_after_print=1)
|
||||
_seed_file(b, "test.gcode")
|
||||
c.publish.side_effect = RuntimeError("send failed")
|
||||
|
||||
b._on_print(_print_report("printing", "test.gcode"))
|
||||
b._on_print(_print_report("finished")) # must not raise
|
||||
@@ -464,6 +464,8 @@ function applyLang(){
|
||||
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('lbl-delete-printer-file-after-print',T.settings_delete_printer_file_after_print||'Delete file from printer after successful print');
|
||||
setText('lbl-delete-printer-file-after-print-hint',T.settings_delete_printer_file_after_print_hint||'Only applies to prints started through this bridge (files it uploaded itself) - prints started directly from the printer or Anycubic Slicer are never deleted, since no copy of those exists anywhere else.');
|
||||
setText('fd-options-title',T.fd_options_title);
|
||||
setText('fd-lbl-auto-leveling',T.print_auto_leveling);
|
||||
|
||||
@@ -1141,6 +1143,7 @@ function openSettings(){
|
||||
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);
|
||||
var dpfap=document.getElementById('s-delete-printer-file-after-print');if(dpfap)dpfap.checked=!!d.delete_printer_file_after_print;
|
||||
// Poll-Intervall (Sekunden) — Backend hat Vorrang vor localStorage
|
||||
var pi=document.getElementById('s-poll-interval');
|
||||
if(pi){
|
||||
@@ -1912,6 +1915,7 @@ function saveSettings(){
|
||||
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,
|
||||
delete_printer_file_after_print: (document.getElementById('s-delete-printer-file-after-print')||{}).checked?1:0,
|
||||
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,
|
||||
spoolman_server: (document.getElementById('s-spoolman-url')||{}).value||'',
|
||||
|
||||
@@ -603,6 +603,11 @@
|
||||
<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-delete-printer-file-after-print" style="width:auto;margin:0">
|
||||
<label id="lbl-delete-printer-file-after-print" style="margin:0;cursor:pointer" for="s-delete-printer-file-after-print">Delete file from printer after successful print</label>
|
||||
</div>
|
||||
<small id="lbl-delete-printer-file-after-print-hint" style="color:var(--txt2)"></small>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
|
||||
@@ -293,6 +293,8 @@
|
||||
"settings_visible_vendors_save": "Auswahl speichern",
|
||||
"settings_visible_vendors_save_label": "Auswahl speichern",
|
||||
"settings_web_upload_warning": "Warnung bei Web-Upload-Druck anzeigen",
|
||||
"settings_delete_printer_file_after_print": "Datei nach erfolgreichem Druck vom Drucker löschen",
|
||||
"settings_delete_printer_file_after_print_hint": "Gilt nur für Drucke, die über diese Bridge gestartet wurden (selbst hochgeladene Dateien) - direkt am Drucker oder über Anycubic Slicer gestartete Drucke werden nie gelöscht, da davon sonst keine Kopie mehr existiert.",
|
||||
"sf_all": "Alle",
|
||||
"sf_err": "✗ Fehler",
|
||||
"sf_new": "Neu",
|
||||
|
||||
@@ -293,6 +293,8 @@
|
||||
"settings_visible_vendors_save": "Save selection",
|
||||
"settings_visible_vendors_save_label": "Save selection",
|
||||
"settings_web_upload_warning": "Show warning when printing web uploads",
|
||||
"settings_delete_printer_file_after_print": "Delete file from printer after successful print",
|
||||
"settings_delete_printer_file_after_print_hint": "Only applies to prints started through this bridge (files it uploaded itself) - prints started directly from the printer or Anycubic Slicer are never deleted, since no copy of those exists anywhere else.",
|
||||
"sf_all": "All",
|
||||
"sf_err": "✗ Failed",
|
||||
"sf_new": "New",
|
||||
|
||||
@@ -293,6 +293,8 @@
|
||||
"settings_visible_vendors_save": "Guardar selección",
|
||||
"settings_visible_vendors_save_label": "Guardar selección",
|
||||
"settings_web_upload_warning": "Mostrar advertencia al imprimir subidas web",
|
||||
"settings_delete_printer_file_after_print": "Eliminar archivo de la impresora tras una impresión exitosa",
|
||||
"settings_delete_printer_file_after_print_hint": "Solo aplica a impresiones iniciadas a través de este bridge (archivos que él mismo subió) - las impresiones iniciadas directamente desde la impresora o Anycubic Slicer nunca se eliminan, ya que no existe ninguna copia en otro lugar.",
|
||||
"sf_all": "Todos",
|
||||
"sf_err": "✗ Fallido",
|
||||
"sf_new": "Nuevo",
|
||||
|
||||
@@ -279,6 +279,8 @@
|
||||
"settings_visible_vendors_save": "Enregistrer la sélection",
|
||||
"settings_visible_vendors_save_label": "Enregistrer la sélection",
|
||||
"settings_web_upload_warning": "Afficher un avertissement lors de l'impression de fichiers web",
|
||||
"settings_delete_printer_file_after_print": "Supprimer le fichier de l'imprimante après une impression réussie",
|
||||
"settings_delete_printer_file_after_print_hint": "S'applique uniquement aux impressions lancées via ce bridge (fichiers qu'il a lui-même téléversés) - les impressions lancées directement depuis l'imprimante ou Anycubic Slicer ne sont jamais supprimées, car aucune copie n'existe ailleurs.",
|
||||
"sf_all": "Tout",
|
||||
"sf_err": "✗ Échoués",
|
||||
"sf_new": "Nouveau",
|
||||
|
||||
@@ -279,6 +279,8 @@
|
||||
"settings_visible_vendors_save": "Salva selezione",
|
||||
"settings_visible_vendors_save_label": "Salva selezione",
|
||||
"settings_web_upload_warning": "Mostra un avviso quando si stampano caricamenti web",
|
||||
"settings_delete_printer_file_after_print": "Elimina il file dalla stampante dopo una stampa riuscita",
|
||||
"settings_delete_printer_file_after_print_hint": "Si applica solo alle stampe avviate tramite questo bridge (file caricati da esso) - le stampe avviate direttamente dalla stampante o da Anycubic Slicer non vengono mai eliminate, poiché non ne esiste alcuna copia altrove.",
|
||||
"sf_all": "Tutti",
|
||||
"sf_err": "✗ Fallito",
|
||||
"sf_new": "Nuovo",
|
||||
|
||||
@@ -293,6 +293,8 @@
|
||||
"settings_visible_vendors_save": "保存选择",
|
||||
"settings_visible_vendors_save_label": "保存选择",
|
||||
"settings_web_upload_warning": "打印网页上传文件时显示警告",
|
||||
"settings_delete_printer_file_after_print": "打印成功后从打印机删除文件",
|
||||
"settings_delete_printer_file_after_print_hint": "仅适用于通过此网桥启动的打印(即由网桥自己上传的文件)——直接从打印机或 Anycubic Slicer 启动的打印任务永远不会被删除,因为它们没有其他备份。",
|
||||
"sf_all": "全部",
|
||||
"sf_err": "✗ 失败",
|
||||
"sf_new": "新",
|
||||
|
||||
Reference in New Issue
Block a user