forked from viewit/KX-Bridge-Release
Compare commits
15 Commits
web-upload
...
dryer-togg
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
fc89dfffa5 | ||
| ac695ecf36 | |||
| 23b8a69065 | |||
| 22dc58258c | |||
| e4b4d091f3 | |||
| ba209827ce | |||
| d26b37b332 | |||
| 6f269833d2 | |||
| d808cd3ea8 | |||
|
|
ecd444525a | ||
|
|
d4bb79a68f | ||
|
|
cdaf74985c | ||
|
|
8383c59b39 | ||
| 1645de4cad | |||
| 42898c385c |
2
.gitignore
vendored
2
.gitignore
vendored
@@ -15,3 +15,5 @@ config/config.ini
|
||||
config/*.ini
|
||||
!config/config.ini.example
|
||||
data/
|
||||
|
||||
!data/orca_filaments.json
|
||||
|
||||
128
CHANGELOG.de.md
128
CHANGELOG.de.md
@@ -1,5 +1,133 @@
|
||||
# Changelog
|
||||
|
||||
## [0.9.18] – 2026-05-31
|
||||
|
||||
### Neu
|
||||
- **🎉 Filament-Material und -Farbe pro AMS-Slot aus der Bridge an den
|
||||
Drucker senden:** Im Slot-Edit-Dialog gewählte Werte gehen jetzt
|
||||
tatsächlich an den Drucker und werden persistent übernommen — am
|
||||
Drucker-Display siehst du sofort dieselbe Belegung wie in der Bridge-UI.
|
||||
In 0.9.17 wurde der Befehl über das falsche MQTT-Topic
|
||||
(`slicer/printer/…`) gesendet, der Drucker hat ihn stillschweigend
|
||||
ignoriert. Jetzt geht er über `web/printer/…` wie der Anycubic
|
||||
Slicer Next es macht (verifiziert via Live-MQTT-Sniff +
|
||||
Workbench-Vue-Source). **Achtung:** der aktiv geladene Slot kann
|
||||
während des Drucks nicht umgeschrieben werden — vorher ausziehen.
|
||||
- **Mehrsprachiges UI – spanische Übersetzung von Muttersprachler
|
||||
überarbeitet (PR #40 von @pezfisk):** fehlende Akzente
|
||||
(impresión, cámara, después, animación, …), Begriffe vereinheitlicht
|
||||
(Pause → Pausa, Start → Iniciar, Layer → Capa). Plus neues
|
||||
`README.es.md` und Cross-Links in den drei READMEs.
|
||||
- **Z-Höhen-Anzeige in Obico** funktioniert jetzt. Der Drucker liefert
|
||||
keine echte Z-Position via MQTT (per Live-Sniff bestätigt), die Bridge
|
||||
schätzt sie aus `curr_layer × layer_height + first_layer_height`.
|
||||
Layer-Heights kommen aus dem GCode-Header beim Upload, persistiert
|
||||
im GCode-Store; Fallback aus dem OrcaSlicer-Default-Filename
|
||||
(`…_0.2_…gcode`). `/server/files/metadata` liefert zusätzlich
|
||||
`object_height` (Gesamt-Z), damit Obicos `mmProgress`-Widget
|
||||
`aktuelles Z / Gesamt-Z` anzeigt.
|
||||
- **Slot-Karte zeigt den OrcaSlicer-Profil-Vendor** unter dem Material
|
||||
(z.B. „PLA / Polymaker"), mit Profil-Namen + interner ID als
|
||||
Tooltip. So ist auf einen Blick erkennbar welcher Slot-Override
|
||||
aktiv ist.
|
||||
|
||||
### Fixes
|
||||
- **Slot-Profil-Auswahl im AMS-Dialog (Issue #39 von @harrygeier):**
|
||||
drei separate Bugs in 0.9.17 sorgten dafür dass die gewählte Marke
|
||||
nach dem Speichern verschwand und beim erneuten Öffnen ein falsches
|
||||
Material angezeigt wurde.
|
||||
- `multiColorBox/setInfo` über das falsche MQTT-Topic — siehe oben.
|
||||
- Speichern lief in zwei parallelen Requests (Profil-Override +
|
||||
Material/Farbe) → Race-Bedingung. Läuft jetzt sequenziell und
|
||||
reloaded den lokalen State bevor der Dialog geschlossen wird.
|
||||
- OrcaSlicer-Filament-IDs sind nicht eindeutig — `orca_filaments.json`
|
||||
hat 68 duplikate IDs, `OGFL99` allein ist 136 Vendor-Profilen
|
||||
zugeordnet (Erkenntnis von @gangoke). Der primäre Selector ist
|
||||
jetzt `(vendor, name)` — über alle 1002 Profile eindeutig.
|
||||
- **MQTT-Reconnect (Issue #33 von @icebear):** wurde der Drucker über
|
||||
Nacht ausgeschaltet, schlug die Bridge nach 5 Reconnect-Versuchen
|
||||
(~60 s gesamt) endgültig fehl — Filament-Sync ging morgens noch
|
||||
(weil das HTTP ist), aber Print-Start scheiterte mit
|
||||
„connection refused", User musste die Bridge selbst neu starten.
|
||||
Reader-Thread reconnectet jetzt **endlos** (Backoff cappt bei 60 s)
|
||||
bis der Drucker wieder antwortet, mit DEBUG-Logging nach den ersten
|
||||
5 Versuchen damit das Log nicht über Nacht zugemüllt wird.
|
||||
- **„Unknown child pid"-Warnungen im Log:** beim Killen der Kamera-
|
||||
`ffmpeg`-Prozesse fehlte das `wait()` — Children blieben als
|
||||
Zombies und asyncio meldete sie alle ~20 s. Gefixt im CameraCache
|
||||
+ `/api/camera/stream`.
|
||||
|
||||
### UI-Aufräumen
|
||||
- **Pause-Button als Toggle:** druckt der Drucker → `⏸ Pause`, ist
|
||||
pausiert → `▶ Weiter`. Der separate „Weiter"-Button entfällt.
|
||||
- **Pause + Stopp komplett ausgeblendet wenn Drucker idle** — bei
|
||||
Standby waren beide Buttons vorher dauerhaft sichtbar, was beim
|
||||
Idle-Drucker verwirrend wirkte.
|
||||
|
||||
### Build
|
||||
- **GCode-Store-Migration:** neue Spalten `layer_height` +
|
||||
`first_layer_height` in `gcode_files` (automatisch beim ersten
|
||||
Start von 0.9.18 angelegt).
|
||||
|
||||
## [0.9.17] – 2026-05-30
|
||||
|
||||
### Neu
|
||||
- **🧪 Obico-Anbindung (experimentell):** Die Bridge spielt jetzt einen
|
||||
Moonraker, der vom [moonraker-obico](https://github.com/TheSpaghettiDetective/moonraker-obico)
|
||||
Plugin akzeptiert wird. Damit funktionieren Time-Lapse, Layer-aligned
|
||||
First-Layer-Scan und WebRTC-Live-Stream gegen einen (selbst gehosteten oder
|
||||
Cloud-) Obico-Server. **Hinweis:** Das KI-Modell zur Spaghetti-Erkennung
|
||||
ist auf seitliche Kamera-Winkel (Ender/Voron) trainiert — wie zuverlässig
|
||||
es beim Kobra X mit Top-Down-Kamera funktioniert, muss empirisch getestet
|
||||
werden (bei uns ging es schon ganz gut). Stream, Time-Lapse und Telemetrie
|
||||
laufen, die Failure-Erkennung ist deshalb noch als experimentell markiert.
|
||||
- **Mehrsprachiges UI (PR #37 von @gangoke):** Inline-Translations sind raus,
|
||||
stattdessen wechselbares Sprach-Dropdown mit Globe-Icon. Auto-Auswahl nach
|
||||
Browser-Locale, manuelle Wahl wird im LocalStorage gemerkt. Sprachen: 🇩🇪 🇬🇧
|
||||
🇪🇸 🇨🇳 (ES + ZH-CN sind KI-übersetzt und noch nicht von Muttersprachlern
|
||||
geprüft).
|
||||
- **OrcaSlicer-Filament-Profil pro AMS-Slot:** Im Slot-Bearbeiten-Dialog kannst
|
||||
du jetzt ein konkretes OrcaSlicer-Profil (z.B. „PolyTerra PLA — Polymaker")
|
||||
pro Slot wählen — die Bridge sendet diese Information beim AMS-Sync mit,
|
||||
statt nur „Generic PLA". Die Profil-Liste wird aus dem OrcaSlicer-Source
|
||||
generiert (~1000 Profile, 43 Hersteller). Damit OrcaSlicer den Hint
|
||||
vollständig respektiert, wird ein passender Patch im OrcaSlicer-KX-Build
|
||||
folgen.
|
||||
- **H.264-Direkt-Stream:** Neuer Endpunkt `/api/camera/h264` liefert den
|
||||
Drucker-Kamera-Stream ohne Re-Encoding als MPEG-TS — Latenz drastisch
|
||||
reduziert, Bridge-CPU bei Obico-Stream von ~13 % auf ~3 %.
|
||||
|
||||
### Fixes
|
||||
- **Temperatur-Setzen über Bridge-UI / Obico löste Drucker-Systemfehler aus:**
|
||||
Per Live-MQTT-Sniff vom Anycubic Slicer Next korrigiert — der Befehl
|
||||
`tempature/set` braucht ein `type`-Feld (0=Nozzle, 1=Bett, 2=beide) und
|
||||
muss über das `web/printer/…`-Topic, nicht `slicer/printer/…`. Nozzle/Bett
|
||||
über die Bridge heizen jetzt sauber.
|
||||
- **Große GCode-Uploads (>50 MB) brachen mit Timeout ab:** Der
|
||||
Connect-Timeout vom Socket lief auch während des `sendall()` — bei ~200 MB
|
||||
über LAN brauchte das Schieben mehr als die 30 s und wurde fälschlich als
|
||||
Connect-Timeout abgebrochen. Jetzt sind Connect-, Send- und Read-Phase
|
||||
separat getimeoutet.
|
||||
- **Kamera-Snapshot war langsam und konnte sich mit dem Live-Stream blockieren:**
|
||||
Die Bridge hält nun einen zentralen Kamera-Cache (ein einziger ffmpeg-Prozess
|
||||
zieht vom Drucker, alle Konsumenten teilen sich den Stream). Snapshots
|
||||
kommen in ~1.3 ms aus dem RAM statt nach 1-2 s per neuer ffmpeg-Instanz.
|
||||
Behebt außerdem das Single-Client-Limit am Drucker (HTTP 429 bei parallelen
|
||||
Zugriffen).
|
||||
- **Sprachwechsel aktualisierte den GCode-Browser nicht:** Die in die
|
||||
File-Karten eingebackenen Texte („Drucken", „Schätzung", „Download") blieben
|
||||
in der alten Sprache. Beim Sprachwechsel werden die Karten jetzt neu
|
||||
gerendert.
|
||||
- **GCode Web-Upload + Download + Verify-Dialog (PR #32 von @gangoke):**
|
||||
Dateien können direkt im Browser hoch/runtergeladen werden, mit
|
||||
Warn-Dialog wenn ein nicht durch OrcaSlicer hochgeladener GCode gestartet
|
||||
wird.
|
||||
|
||||
### CI/Build
|
||||
- Multi-Arch Docker-Image (amd64 + arm64) per Gitea-Actions automatisiert.
|
||||
- Release-Build über lokalen CodeBuilder für alle drei Targets
|
||||
(linux-amd64, linux-arm64, windows.exe).
|
||||
|
||||
## [0.9.16] – 2026-05-22
|
||||
|
||||
### Neu
|
||||
|
||||
126
CHANGELOG.md
126
CHANGELOG.md
@@ -1,5 +1,131 @@
|
||||
# Changelog
|
||||
|
||||
## [0.9.18] – 2026-05-31
|
||||
|
||||
### New
|
||||
- **🎉 Push filament material and colour from the bridge to the
|
||||
printer:** The values you pick in the slot-edit dialog now actually
|
||||
reach the printer and stick — the printer display shows the same
|
||||
slot setup as the bridge UI right away. In 0.9.17 the command was
|
||||
sent over the wrong MQTT topic (`slicer/printer/…`) and the printer
|
||||
silently dropped it. It now goes via `web/printer/…` like the
|
||||
Anycubic Slicer Next does (verified by live MQTT sniff +
|
||||
Workbench-Vue source). **Note:** the currently loaded slot can not
|
||||
be overwritten during a print — unload it first.
|
||||
- **Spanish translation reviewed by a native speaker (PR #40 by
|
||||
@pezfisk):** missing accents (impresión, cámara, después,
|
||||
animación, …) and term consistency (Pause → Pausa, Start →
|
||||
Iniciar, Layer → Capa). New `README.es.md` and cross-links between
|
||||
the three READMEs.
|
||||
- **Z-height now shows up in Obico.** The printer does not report a
|
||||
real Z position over MQTT (live-sniff confirmed), so the bridge
|
||||
estimates it from `current_layer × layer_height + first_layer_height`.
|
||||
Layer heights are parsed from the gcode header at upload time and
|
||||
persisted in the gcode store; fallback for prints started directly
|
||||
from the slicer is the OrcaSlicer default filename pattern
|
||||
(`…_0.2_…gcode`). `/server/files/metadata` also serves
|
||||
`object_height` (total Z) so Obicos `mmProgress` widget can render
|
||||
`current Z / total Z`.
|
||||
- **Slot card shows the OrcaSlicer profile vendor** under the
|
||||
material (e.g. `PLA / Polymaker`), with the profile name + internal
|
||||
ID as tooltip. Lets you see at a glance which slot override is
|
||||
active.
|
||||
|
||||
### Fixes
|
||||
- **Slot profile picker in the AMS dialog (issue #39 by
|
||||
@harrygeier):** three separate bugs in 0.9.17 caused the chosen
|
||||
brand to disappear after save and a different material to show up
|
||||
on re-open.
|
||||
- `multiColorBox/setInfo` was sent on the wrong MQTT topic — see
|
||||
above.
|
||||
- Save fired two parallel requests (profile override + material/
|
||||
colour) → race. Now sequential, and the local state is reloaded
|
||||
before the dialog closes.
|
||||
- OrcaSlicer filament IDs are not unique — `orca_filaments.json`
|
||||
has 68 duplicate IDs, `OGFL99` alone is shared by 136 vendor
|
||||
profiles (caught by @gangoke). The primary selector is now
|
||||
`(vendor, name)` — unique across all 1002 profiles.
|
||||
- **MQTT reconnect (issue #33 by @icebear):** if the printer was
|
||||
powered off overnight the bridge gave up after 5 reconnect attempts
|
||||
(~60 s total) — filament sync still worked in the morning (its
|
||||
HTTP), but starting a print failed with `connection refused` and
|
||||
the user had to restart the bridge itself. The reader thread now
|
||||
reconnects **forever** (backoff caps at 60 s) until the printer
|
||||
responds again, with logs dropping to DEBUG after the first 5
|
||||
attempts so an overnight outage does not spam the log.
|
||||
- **`Unknown child pid` warnings in the log:** the camera ffmpeg
|
||||
helpers were killed without awaiting their `wait()` — children
|
||||
lingered as zombies and asyncio reported them every ~20 s. Fixed
|
||||
in CameraCache + `/api/camera/stream`.
|
||||
|
||||
### UI polish
|
||||
- **Pause button is now a toggle:** while printing → `⏸ Pause`,
|
||||
while paused → `▶ Resume`. The separate resume button is gone.
|
||||
- **Pause + stop hidden when the printer is idle** — both used to be
|
||||
visible at all times, which was confusing on a standby printer.
|
||||
|
||||
### Build
|
||||
- **gcode store migration:** new columns `layer_height` +
|
||||
`first_layer_height` on `gcode_files` (added automatically on first
|
||||
start of 0.9.18).
|
||||
|
||||
## [0.9.17] – 2026-05-30
|
||||
|
||||
### New
|
||||
- **🧪 Obico integration (experimental):** The bridge now exposes a
|
||||
Moonraker-compatible surface that the
|
||||
[moonraker-obico](https://github.com/TheSpaghettiDetective/moonraker-obico)
|
||||
plugin accepts. Time-lapses, layer-aligned first-layer scan and WebRTC
|
||||
live streaming work against a (self-hosted or cloud) Obico server.
|
||||
**Note:** the spaghetti-detection ML model is trained on side-view
|
||||
cameras (Ender/Voron); how well it works with the Kobra X's top-down
|
||||
camera is still to be evaluated empirically (it already looked
|
||||
promising in our tests). Stream, time-lapse and telemetry work — the
|
||||
failure-detection side stays flagged as experimental for now.
|
||||
- **Multi-language UI (PR #37 by @gangoke):** Inline translations have
|
||||
moved into JSON files; a globe-icon dropdown lets you switch language.
|
||||
Browser locale is auto-detected; manual choice persists in
|
||||
LocalStorage. Languages: 🇩🇪 🇬🇧 🇪🇸 🇨🇳 (ES + ZH-CN are AI-translated
|
||||
and not verified by native speakers yet).
|
||||
- **OrcaSlicer filament profile per AMS slot:** The slot-edit dialog now
|
||||
lets you pick a concrete OrcaSlicer profile (e.g. "PolyTerra PLA —
|
||||
Polymaker") per slot; the bridge sends it along on AMS sync instead
|
||||
of just "Generic PLA". Profile list is generated from the OrcaSlicer
|
||||
source (~1000 profiles, 43 vendors). A matching patch in
|
||||
OrcaSlicer-KX is on the way so OrcaSlicer fully honours the hint.
|
||||
- **H.264 direct stream:** New `/api/camera/h264` endpoint serves the
|
||||
printer camera stream as MPEG-TS without re-encoding — dramatically
|
||||
reduces latency, bridge CPU during Obico streaming drops from ~13 %
|
||||
to ~3 %.
|
||||
|
||||
### Fixes
|
||||
- **Setting temperature via bridge UI / Obico caused a printer system
|
||||
error:** Fixed via live MQTT capture from Anycubic Slicer Next — the
|
||||
`tempature/set` command needs a `type` field (0=nozzle, 1=bed,
|
||||
2=both) and must go over the `web/printer/…` topic, not
|
||||
`slicer/printer/…`. Nozzle/bed heating from the bridge now works.
|
||||
- **Large GCode uploads (>50 MB) timed out:** The socket connect timeout
|
||||
was active during `sendall()` too — pushing ~200 MB over LAN took
|
||||
more than 30 s and was falsely aborted. Connect / send / read phases
|
||||
are now timed out separately.
|
||||
- **Camera snapshots were slow and could collide with the live stream:**
|
||||
The bridge now keeps a central camera cache (one ffmpeg pulls from
|
||||
the printer, all consumers share it). Snapshots return in ~1.3 ms
|
||||
from RAM instead of 1–2 s per spawned ffmpeg. Also resolves the
|
||||
single-client limit on the printer (HTTP 429 on parallel access).
|
||||
- **Language switch did not refresh the GCode browser:** Strings baked
|
||||
into the file cards ("Print", "Estimate", "Download") stayed in the
|
||||
previous language. Cards are now re-rendered on language switch.
|
||||
- **GCode web upload + download + verify dialog (PR #32 by @gangoke):**
|
||||
Files can be uploaded / downloaded directly in the browser, with a
|
||||
warning dialog when starting a GCode that was not uploaded via
|
||||
OrcaSlicer.
|
||||
|
||||
### CI/Build
|
||||
- Multi-arch Docker image (amd64 + arm64) automated via Gitea Actions.
|
||||
- Release builds for all three targets (linux-amd64, linux-arm64,
|
||||
windows.exe) via the local CodeBuilder.
|
||||
|
||||
## [0.9.16] – 2026-05-22
|
||||
|
||||
### New
|
||||
|
||||
@@ -7,6 +7,9 @@ RUN pip install --no-cache-dir -r requirements.txt
|
||||
|
||||
COPY kobrax_moonraker_bridge.py .
|
||||
COPY web/ ./web/
|
||||
# Statische Daten (orca_filaments.json etc.) liegen in /app/static/, NICHT in
|
||||
# /app/data/ — letzteres wird vom User als Volume gemountet (Runtime-State).
|
||||
COPY data/ ./static/
|
||||
COPY config_loader.py .
|
||||
COPY env_loader.py .
|
||||
COPY kobrax_client.py .
|
||||
|
||||
@@ -8,7 +8,7 @@
|
||||
|
||||
Eine Moonraker-kompatible Bridge, die direkt mit dem Drucker spricht.
|
||||
|
||||
<sub>🇬🇧 <a href="README.md">English version</a></sub>
|
||||
<sub>🇬🇧 <a href="README.md">English version</a> · 🇪🇸 <a href="README.es.md">Versión española</a></sub>
|
||||
|
||||
<br>
|
||||
|
||||
|
||||
224
README.es.md
Normal file
224
README.es.md
Normal file
@@ -0,0 +1,224 @@
|
||||
<div align="center">
|
||||
|
||||
<img src="knlogo.png" alt="KX-Bridge" width="160"/>
|
||||
|
||||
# KX-Bridge
|
||||
|
||||
**Controla tu Anycubic Kobra X con OrcaSlicer — sin Klipper, sin Raspberry Pi.**
|
||||
|
||||
Un puente compatible con Moonraker que se comunica directamente con la impresora.
|
||||
|
||||
<sub>🇬🇧 <a href="README.md">English version</a> · 🇩🇪 <a href="README.de.md">Deutsche Version</a></sub>
|
||||
|
||||
<br>
|
||||
|
||||
[](https://ko-fi.com/viewitde)
|
||||
|
||||
[](https://gitea.it-drui.de/viewit/KX-Bridge-Release/releases)
|
||||
|
||||
[](https://gitea.it-drui.de/viewit/KX-Bridge-Release/releases)
|
||||
|
||||
[](https://www.youtube.com/watch?v=1Ql4wfH27fM)
|
||||
|
||||
<sub>¿Te gusta KX-Bridge? Un café en <a href="https://ko-fi.com/viewitde">Ko-fi</a> mantiene el proyecto vivo. ☕</sub>
|
||||
|
||||
</div>
|
||||
|
||||
---
|
||||
|
||||
## ✨ Características
|
||||
|
||||
| | |
|
||||
|---|---|
|
||||
| 🖨️ | **Control de impresora** — iniciar, pausar, reanudar, cancelar, temperaturas, velocidad de impresión |
|
||||
| 📊 | **Estado en tiempo real** — temperatura, progreso, capas, tiempo restante, transmisión de cámara |
|
||||
| 🎨 | **AMS / multicolor** — ranuras de filamento, reasignación por canal, emulación MMU para sincronización de filamento en OrcaSlicer |
|
||||
| 🗂️ | **Explorador de GCode** — archivos subidos con vistas previas, historial de impresión, búsqueda y filtros |
|
||||
| 🧩 | **Multi-impresora** — múltiples impresoras en **una** instancia del puente, cambia mediante un menú desplegable |
|
||||
| ➕ | **Añade una impresora con un clic** — solo introduce la IP, las credenciales se importan automáticamente |
|
||||
| 🔄 | **Actualización automática** — instala nuevas versiones directamente desde el navegador |
|
||||
| 🌐 | **OrcaSlicer** — protocolo Moonraker completo (HTTP + WebSocket), interfaz EN/ES |
|
||||
|
||||
---
|
||||
|
||||
## 🚀 Inicio rápido
|
||||
|
||||
### 1. Prepara la impresora
|
||||
|
||||
Activa el modo LAN en la Kobra X:
|
||||
**Pantalla de la impresora → Ajustes → Activar modo LAN**
|
||||
|
||||
### 2. Inicia el puente
|
||||
|
||||
**Docker (recomendado):**
|
||||
```bash
|
||||
docker compose up -d
|
||||
```
|
||||
|
||||
**Binario Linux (sin Docker):**
|
||||
```bash
|
||||
chmod +x kx-bridge && ./kx-bridge
|
||||
```
|
||||
|
||||
**EXE Windows (sin Docker):**
|
||||
```
|
||||
kx-bridge.exe
|
||||
```
|
||||
> `config\` y `data\` se crean junto al EXE — instalación portátil.
|
||||
|
||||
> Con los binarios de Linux y Windows, `config/` y `data/` (configuración, SQLite, almacén de GCode)
|
||||
> viven junto al programa. Copia toda la carpeta para mover la instalación.
|
||||
|
||||
**Python directamente:**
|
||||
```bash
|
||||
pip install -r bridge/requirements.txt
|
||||
python bridge/kobrax_moonraker_bridge.py
|
||||
```
|
||||
|
||||
### 3. Configura la impresora
|
||||
|
||||
Abre la interfaz web: **`http://IP-DEL-PUENTE:7125`**
|
||||
|
||||
En el primer inicio, la pestaña **Impresoras** muestra *"+ Añadir impresora"* — solo introduce la dirección IP
|
||||
de la impresora, el resto (usuario, contraseña, ID de dispositivo) se obtiene de la impresora y se desencripta
|
||||
automáticamente. Listo.
|
||||
|
||||
> ¿Más de una impresora? Simplemente haz clic en *"+ Añadir impresora"* de nuevo — cada una recibe su propio puerto
|
||||
> (7125, 7126, …) y se puede seleccionar desde el menú desplegable del encabezado.
|
||||
|
||||
### 4. Conecta OrcaSlicer
|
||||
|
||||
Impresora → Tipo de conexión **Moonraker** → Host: `http://IP-DEL-PUENTE:7125`
|
||||
|
||||
> ⚠️ El tipo de conexión debe ser **Moonraker** (no "Bambu" ni "Klipper").
|
||||
> Introduce la URL completa incluyendo `http://` y el puerto `:7125` en el campo de host.
|
||||
|
||||
---
|
||||
|
||||
## 📺 Vídeo tutorial
|
||||
|
||||
[](https://www.youtube.com/watch?v=1Ql4wfH27fM)
|
||||
|
||||
---
|
||||
|
||||
## 🎨 Slicer recomendado
|
||||
|
||||
Para la mejor experiencia con KX-Bridge ofrecemos una **versión modificada de OrcaSlicer** que
|
||||
incluye tres PRs abiertos de SoftFever/OrcaSlicer: el perfil de la impresora Anycubic Kobra
|
||||
X, una corrección de GCode multicolor y — lo más importante — una corrección de sincronización de
|
||||
filamento Moonraker/Happy-Hare que mantiene las posiciones de las ranuras AMS intactas incluso con una ranura vacía.
|
||||
|
||||
→ **[Lanzamientos de OrcaSlicer-KX](https://gitea.it-drui.de/viewit/OrcaSlicer-KX/releases/latest)** (Linux AppImage + Windows ZIP)
|
||||
|
||||
OrcaSlicer estándar también funciona; la versión modificada mejora principalmente el manejo de AMS.
|
||||
Es una versión basada en [OrcaSlicer](https://github.com/SoftFever/OrcaSlicer) (AGPL-3.0);
|
||||
el código fuente está disponible a través de los PRs enlazados.
|
||||
|
||||
---
|
||||
|
||||
## 🏠 Comunidad e integraciones
|
||||
|
||||
- **[Integración con Home Assistant](https://github.com/gangoke/kobrax-lan-hass-component)**
|
||||
por [@gangoke](https://github.com/gangoke) — expone sensores, controles de impresión,
|
||||
luz, cámara y la vista previa del GCode como entidades nativas de Home Assistant.
|
||||
|
||||
> Estos son **proyectos de la comunidad**, no mantenidos ni soportados por KX-Bridge.
|
||||
> Para preguntas o problemas, utiliza el repositorio enlazado.
|
||||
|
||||
---
|
||||
|
||||
## 🔧 Obtener credenciales manualmente
|
||||
|
||||
Normalmente no es necesario — *"+ Añadir impresora"* lo hace automáticamente. Si lo necesitas:
|
||||
|
||||
```bash
|
||||
fetch_credentials --ip 192.168.x.x --write-config
|
||||
```
|
||||
Obtiene las credenciales directamente de la impresora vía HTTP y las escribe en `config/config.ini`.
|
||||
Solo se requiere la IP de la impresora, no hace falta un slicer.
|
||||
|
||||
Alternativamente (si se desconoce la IP): abre AnycubicSlicerNext, conecta la impresora, luego ejecuta
|
||||
`extract_credentials` → muestra usuario, contraseña, ID de dispositivo y la IP de la impresora.
|
||||
|
||||
> **Descargas:** [Lanzamientos](https://gitea.it-drui.de/viewit/KX-Bridge-Release/releases) → `fetch_credentials` / `extract_credentials` (Linux y Windows)
|
||||
|
||||
---
|
||||
|
||||
## ⚙️ Comandos útiles
|
||||
|
||||
```bash
|
||||
docker compose logs -f # mostrar registros
|
||||
docker compose down # detener el puente
|
||||
docker compose pull && docker compose up -d # actualizar a la imagen publicada más reciente
|
||||
docker compose up -d --build # recompilar localmente (en lugar de descargar)
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 🩹 Solución de problemas
|
||||
|
||||
<details>
|
||||
<summary><b>"Credenciales MQTT incorrectas" al iniciar</b></summary>
|
||||
|
||||
- Vuelve a añadir la impresora mediante *"+ Añadir impresora"*, o ejecuta
|
||||
`fetch_credentials --ip <ip> --write-config` y reinicia el puente
|
||||
- Introduce solo la dirección IP, sin puerto (✗ `192.168.1.102:9883` → ✓ `192.168.1.102`)
|
||||
</details>
|
||||
|
||||
<details>
|
||||
<summary><b>Impresora no encontrada / modo LAN no activado</b></summary>
|
||||
|
||||
- En la pantalla de la impresora: Ajustes → Activar modo LAN
|
||||
- La impresora y el puente deben estar en la misma red
|
||||
</details>
|
||||
|
||||
<details>
|
||||
<summary><b>Docker: Permiso denegado</b></summary>
|
||||
|
||||
```bash
|
||||
sudo usermod -aG docker $USER # luego cierra la sesión y vuelve a iniciarla
|
||||
```
|
||||
</details>
|
||||
|
||||
<details>
|
||||
<summary><b>Actualizar desde 0.9.1 o anterior</b></summary>
|
||||
|
||||
A partir de 0.9.2, KX-Bridge almacena la configuración en `config/config.ini` en lugar de `.env`.
|
||||
La migración se ejecuta automáticamente en el primer inicio después de la actualización — no requiere acción.
|
||||
</details>
|
||||
|
||||
---
|
||||
|
||||
## 🔒 Seguridad
|
||||
|
||||
- El puente es accesible en la red local en `http://<IP-del-host>:7125` — **no** lo expongas a internet
|
||||
- `config/config.ini` contiene las credenciales de la impresora — no las compartas públicamente
|
||||
- Las credenciales **no** otorgan acceso a los servicios en la nube de Anycubic
|
||||
|
||||
---
|
||||
|
||||
## 📄 Licencia
|
||||
|
||||
[](LICENSE)
|
||||
|
||||
KX-Bridge se publica bajo la **GNU General Public License v3.0**. Consulta
|
||||
[LICENSE](LICENSE) para el texto completo. Las bifurcaciones y modificaciones deben
|
||||
permanecer bajo GPLv3 si se redistribuyen.
|
||||
|
||||
La implementación del protocolo MQTT es el resultado de una ingeniería inversa
|
||||
independiente con fines de interoperabilidad (§69e UrhG / Directiva de Software de la UE
|
||||
Art. 6). El material de terceros en el repositorio (certificados TLS de Anycubic)
|
||||
**no** está cubierto por GPLv3 y se incluye únicamente para permitir la
|
||||
autenticación contra impresoras que el usuario final ya posee. Consulta
|
||||
[NOTICE.md](NOTICE.md) para más detalles y el aviso legal.
|
||||
|
||||
Este proyecto es independiente y no está afiliado con Anycubic.
|
||||
|
||||
<div align="center">
|
||||
<br>
|
||||
|
||||
**Si KX-Bridge te ayuda, el proyecto agradece tu apoyo:**
|
||||
|
||||
[](https://ko-fi.com/viewitde)
|
||||
|
||||
</div>
|
||||
@@ -8,7 +8,7 @@
|
||||
|
||||
A Moonraker-compatible bridge that talks directly to the printer.
|
||||
|
||||
<sub>🇩🇪 <a href="README.de.md">Deutsche Version</a></sub>
|
||||
<sub>🇩🇪 <a href="README.de.md">Deutsche Version</a> · 🇪🇸 <a href="README.es.md">Versión española</a></sub>
|
||||
|
||||
<br>
|
||||
|
||||
|
||||
@@ -2,8 +2,10 @@
|
||||
# Kopiere diese Datei nach config.ini und trage deine Werte ein:
|
||||
# cp config.ini.example config.ini
|
||||
#
|
||||
# Credentials mit extract_credentials.exe (Windows) oder
|
||||
# extract_credentials (Linux) aus dem laufenden AnycubicSlicerNext auslesen.
|
||||
# Credentials automatisch eintragen:
|
||||
# python3 tools/fetch_credentials.py --ip 192.168.x.x --write-config
|
||||
# Alternativ (Windows, ohne Drucker-IP bekannt):
|
||||
# extract_credentials.exe --write-env (liest aus laufendem AnycubicSlicerNext)
|
||||
|
||||
[connection]
|
||||
# IP-Adresse des Druckers im lokalen Netzwerk
|
||||
@@ -29,9 +31,65 @@ default_ams_slot = auto
|
||||
# Auto-Leveling vor jedem Druck (1 = an, 0 = aus)
|
||||
auto_leveling = 1
|
||||
|
||||
# Kamera-Stream bei Druckstart automatisch einschalten (1 = an, 0 = aus)
|
||||
camera_on_print = 0
|
||||
|
||||
# Warnung vor Druck von Web-Uploads (1 = an, 0 = aus)
|
||||
web_upload_warning = 1
|
||||
|
||||
[bridge]
|
||||
# Poll-Intervall in Sekunden
|
||||
poll_interval = 3
|
||||
|
||||
# ─── Multi-Printer (optional) ──────────────────────────────────────────────────
|
||||
# Mehrere Drucker können als [printer_1], [printer_2], … definiert werden.
|
||||
# Jede Bridge-Instanz verbindet sich mit einem Drucker (je eigener Port).
|
||||
# bridge_url zeigt auf die jeweilige Bridge-Instanz (für den /kx/printers-Endpunkt).
|
||||
# Die [connection]-Sektion wird weiterhin als Fallback für diese Instanz verwendet.
|
||||
#
|
||||
# Beispiel:
|
||||
# [printer_1]
|
||||
# name = Kobra X Links
|
||||
# bridge_url = http://192.168.178.95:7125
|
||||
# printer_ip = 192.168.178.95
|
||||
# mqtt_port = 9883
|
||||
# username = userXXXXXXXXXX
|
||||
# password = XXXXXXXXXXXXXXX
|
||||
# device_id = xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx
|
||||
# mode_id = 20030
|
||||
#
|
||||
# [printer_2]
|
||||
# name = Kobra X Rechts
|
||||
# bridge_url = http://192.168.178.96:7125
|
||||
# printer_ip = 192.168.178.96
|
||||
# mqtt_port = 9883
|
||||
# username = userYYYYYYYYYY
|
||||
# password = YYYYYYYYYYYYYYY
|
||||
# device_id = yyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyy
|
||||
# mode_id = 20030
|
||||
|
||||
[ace_dry_presets]
|
||||
# Vordefinierte Dry-Set Presets (Temp in °C, Dauer in Sekunden)
|
||||
pla_temp = 45
|
||||
pla_duration_sec = 14400
|
||||
pla_plus_temp = 45
|
||||
pla_plus_duration_sec = 14400
|
||||
petg_temp = 50
|
||||
petg_duration_sec = 14400
|
||||
tpu_temp = 55
|
||||
tpu_duration_sec = 14400
|
||||
abs_asa_temp = 45
|
||||
abs_asa_duration_sec = 28800
|
||||
pa_pc_temp = 55
|
||||
pa_pc_duration_sec = 43200
|
||||
|
||||
# Custom Presets (Name + Temp + Dauer)
|
||||
custom_1_name = Custom 1
|
||||
custom_1_temp = 45
|
||||
custom_1_duration_sec = 14400
|
||||
custom_2_name = Custom 2
|
||||
custom_2_temp = 45
|
||||
custom_2_duration_sec = 14400
|
||||
custom_3_name = Custom 3
|
||||
custom_3_temp = 45
|
||||
custom_3_duration_sec = 14400
|
||||
|
||||
@@ -166,6 +166,84 @@ def list_printers() -> list[dict]:
|
||||
return printers
|
||||
|
||||
|
||||
def list_filament_profiles() -> dict[int, dict]:
|
||||
"""Liest die [filament_profiles]-Sektion aus config.ini.
|
||||
|
||||
Format pro AMS-Slot — primärer Selector ist (vendor, name), die `id` wird
|
||||
aus der orca_filaments.json beim Speichern nachgeschlagen und mitgeführt
|
||||
(als Hint für OrcaSlicer; das Orca-Datenmodell hat ~136 Profile mit
|
||||
derselben filament_id wie 'OGFL99', d.h. die ID ist nicht eindeutig):
|
||||
|
||||
[filament_profiles]
|
||||
slot_0_vendor = Polymaker
|
||||
slot_0_name = PolyTerra PLA
|
||||
slot_0_id = OGFL01
|
||||
|
||||
Gibt einen Dict {slot_index: {"id": ..., "vendor": ..., "name": ...}}
|
||||
zurück. Leere/fehlende Slots werden NICHT aufgenommen — das Default-Mapping
|
||||
(per filament_type) in der Bridge bleibt dann aktiv.
|
||||
|
||||
Backwards-Kompat: alte Configs mit nur (vendor, id) bleiben lesbar; `name`
|
||||
fehlt dann und der Aufrufer kann optional aus der orca_filaments.json
|
||||
rekonstruieren.
|
||||
"""
|
||||
path = _find_config_file()
|
||||
if not path:
|
||||
return {}
|
||||
cfg = configparser.ConfigParser()
|
||||
cfg.read(path, encoding="utf-8")
|
||||
if not cfg.has_section("filament_profiles"):
|
||||
return {}
|
||||
result: dict[int, dict] = {}
|
||||
for key, value in cfg.items("filament_profiles"):
|
||||
# Erwartet: slot_<idx>_id oder slot_<idx>_vendor oder slot_<idx>_name
|
||||
if not key.startswith("slot_"):
|
||||
continue
|
||||
parts = key.split("_", 2)
|
||||
if len(parts) < 3:
|
||||
continue
|
||||
try:
|
||||
slot_idx = int(parts[1])
|
||||
except ValueError:
|
||||
continue
|
||||
field = parts[2]
|
||||
if field not in ("id", "vendor", "name"):
|
||||
continue
|
||||
if not value.strip():
|
||||
continue
|
||||
result.setdefault(slot_idx, {})[field] = value.strip()
|
||||
return result
|
||||
|
||||
|
||||
def save_filament_profiles(profiles: dict[int, dict]) -> bool:
|
||||
"""Schreibt die übergebenen Slot-Profile in die [filament_profiles]-
|
||||
Sektion der config.ini. Existierende Einträge werden komplett ersetzt.
|
||||
|
||||
profiles: {slot_index: {"id": "OGFL01", "vendor": "Polymaker", "name": "PolyTerra PLA"}}
|
||||
Mindestens vendor+name müssen gesetzt sein; id ist optional (Hint).
|
||||
"""
|
||||
path = _find_config_file()
|
||||
if not path:
|
||||
return False
|
||||
cfg = configparser.ConfigParser()
|
||||
cfg.read(path, encoding="utf-8")
|
||||
if cfg.has_section("filament_profiles"):
|
||||
cfg.remove_section("filament_profiles")
|
||||
if profiles:
|
||||
cfg["filament_profiles"] = {}
|
||||
for slot_idx in sorted(profiles.keys()):
|
||||
entry = profiles[slot_idx] or {}
|
||||
if entry.get("vendor"):
|
||||
cfg["filament_profiles"][f"slot_{slot_idx}_vendor"] = entry["vendor"]
|
||||
if entry.get("name"):
|
||||
cfg["filament_profiles"][f"slot_{slot_idx}_name"] = entry["name"]
|
||||
if entry.get("id"):
|
||||
cfg["filament_profiles"][f"slot_{slot_idx}_id"] = entry["id"]
|
||||
with open(path, "w", encoding="utf-8") as f:
|
||||
cfg.write(f)
|
||||
return True
|
||||
|
||||
|
||||
def get(key: str, default: str = "") -> str:
|
||||
return os.environ.get(key, default)
|
||||
|
||||
|
||||
7016
data/orca_filaments.json
Normal file
7016
data/orca_filaments.json
Normal file
File diff suppressed because it is too large
Load Diff
@@ -179,10 +179,24 @@ class KobraXClient:
|
||||
def connect(self):
|
||||
self._do_connect()
|
||||
self._running = True
|
||||
t = threading.Thread(target=self._read_loop, daemon=True)
|
||||
t.start()
|
||||
self._ensure_reader()
|
||||
time.sleep(0.3)
|
||||
|
||||
def _ensure_reader(self):
|
||||
"""Stellt sicher dass der Reader-Thread lebt. Wenn der Reader nach einer
|
||||
früheren disconnect/reconnect-Sequenz oder einem unbehandelten Fehler
|
||||
gestorben ist, würden empfangene Replies sonst nie ankommen — publish()
|
||||
würde dann zwar senden, aber auf Antworten ewig warten."""
|
||||
if not self._running:
|
||||
return # gewollter disconnect
|
||||
t = getattr(self, "_reader_thread", None)
|
||||
if t is not None and t.is_alive():
|
||||
return
|
||||
self._reader_thread = threading.Thread(
|
||||
target=self._read_loop, daemon=True, name="kobrax-mqtt-reader",
|
||||
)
|
||||
self._reader_thread.start()
|
||||
|
||||
def disconnect(self):
|
||||
self._running = False
|
||||
try:
|
||||
@@ -191,20 +205,34 @@ class KobraXClient:
|
||||
pass
|
||||
|
||||
def _reconnect(self):
|
||||
"""Persistenter Reconnect: versucht endlos weiter bis der Drucker wieder
|
||||
antwortet oder disconnect() gerufen wurde. Backoff cappt bei 60 s. Die
|
||||
ersten 5 Versuche loggen als WARNING (akute Verbindungsstörung), danach
|
||||
nur DEBUG um Log-Spam bei langem Drucker-Ausfall (z.B. über Nacht
|
||||
ausgeschaltet) zu vermeiden."""
|
||||
log.warning("Verbindung verloren – reconnect…")
|
||||
try:
|
||||
self._sock.close()
|
||||
except Exception:
|
||||
pass
|
||||
for delay in [2, 4, 8, 15, 30]:
|
||||
delays = [2, 4, 8, 15, 30, 60]
|
||||
attempt = 0
|
||||
while self._running:
|
||||
delay = delays[min(attempt, len(delays) - 1)]
|
||||
try:
|
||||
self._do_connect()
|
||||
log.info("Reconnect erfolgreich")
|
||||
log.info("Reconnect erfolgreich (nach %d Versuchen)", attempt + 1)
|
||||
return True
|
||||
except Exception as e:
|
||||
log.warning("Reconnect fehlgeschlagen (%s), warte %ss…", e, delay)
|
||||
time.sleep(delay)
|
||||
return False
|
||||
attempt += 1
|
||||
lvl = log.warning if attempt <= 5 else log.debug
|
||||
lvl("Reconnect fehlgeschlagen (%s, Versuch %d), warte %ss…", e, attempt, delay)
|
||||
# Geteiltes Sleep damit disconnect() den Loop schneller bricht.
|
||||
slept = 0.0
|
||||
while slept < delay and self._running:
|
||||
time.sleep(min(0.5, delay - slept))
|
||||
slept += 0.5
|
||||
return False # nur wenn disconnect() gerufen wurde
|
||||
|
||||
def _subscribe(self, topic: str):
|
||||
with self._lock:
|
||||
@@ -348,6 +376,9 @@ class KobraXClient:
|
||||
# -- Publish + request/response ------------------------------------------
|
||||
|
||||
def publish(self, msg_type: str, action: str, data=None, timeout: float = 5.0) -> dict | None:
|
||||
# Falls Reader-Thread aus historischen Gründen tot ist, wiederbeleben —
|
||||
# sonst würden Replies nie ankommen und event.wait() läuft ins Timeout.
|
||||
self._ensure_reader()
|
||||
msgid = str(uuid.uuid4())
|
||||
payload = json.dumps({
|
||||
"type": msg_type,
|
||||
@@ -413,6 +444,7 @@ class KobraXClient:
|
||||
|
||||
def publish_web(self, msg_type: str, action: str, data=None) -> None:
|
||||
"""Fire-and-forget publish on the web/printer topic (used for runtime updates during print)."""
|
||||
self._ensure_reader()
|
||||
msgid = str(uuid.uuid4())
|
||||
payload = json.dumps({
|
||||
"type": msg_type,
|
||||
@@ -429,7 +461,14 @@ class KobraXClient:
|
||||
with self._lock:
|
||||
self._sock.sendall(_build_publish(topic, payload))
|
||||
except Exception as e:
|
||||
log.error("web send error: %s", e)
|
||||
log.error("web send error: %s, reconnecting…", e)
|
||||
# Reconnect triggern (analog zu publish()); ohne Retry weil
|
||||
# fire-and-forget — der nächste Aufruf wird auf den frischen Socket
|
||||
# treffen.
|
||||
try:
|
||||
self._reconnect()
|
||||
except Exception:
|
||||
pass
|
||||
|
||||
# -- High-level commands -------------------------------------------------
|
||||
|
||||
@@ -545,9 +584,15 @@ class KobraXClient:
|
||||
f"Connection: close\r\n\r\n"
|
||||
).encode()
|
||||
|
||||
sock = socket.create_connection((self.host, 18910), timeout=30)
|
||||
# Connect-Timeout kurz (LAN). Während sendall() darf der Socket so
|
||||
# lange brauchen wie nötig — bei großen Dateien (>100 MB) und
|
||||
# langsamerem WLAN am Drucker dauert das Schieben sonst >30 s und
|
||||
# würde den Connect-Timeout fälschlich auslösen. Read-Timeout danach
|
||||
# generös (Drucker verarbeitet die Datei bevor er antwortet).
|
||||
sock = socket.create_connection((self.host, 18910), timeout=10)
|
||||
sock.settimeout(None) # blocking während Send
|
||||
sock.sendall(headers + body)
|
||||
sock.settimeout(120) # große GCode-Dateien brauchen Zeit bis der Drucker antwortet
|
||||
sock.settimeout(180)
|
||||
response = b""
|
||||
try:
|
||||
while True:
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -6,7 +6,7 @@
|
||||
# ein → zur Laufzeit über sys._MEIPASS lesbar (_WEB_BASE in der Bridge).
|
||||
from PyInstaller.utils.hooks import collect_all
|
||||
|
||||
datas = [("web", "web")]
|
||||
datas = [("web", "web"), ("data", "static")] # bridge/data/ → static/ im _MEIPASS
|
||||
binaries = []
|
||||
hiddenimports = []
|
||||
|
||||
|
||||
@@ -89,144 +89,90 @@ function toggleTheme(){
|
||||
(function(){var t=localStorage.getItem('theme');if(t)document.documentElement.setAttribute('data-theme',t)})();
|
||||
|
||||
// ── i18n ──
|
||||
var LANG_DE={
|
||||
header_status_standby:'Bereit',header_status_printing:'Druckt',header_status_complete:'Fertig',header_status_error:'Fehler',
|
||||
kobra_free:'Bereit',kobra_busy:'Beschäftigt',kobra_printing:'Druckt',kobra_preheating:'Aufheizen',kobra_auto_leveling:'Nivellierung',kobra_checking:'Prüfung',kobra_updated:'Aktualisierung',kobra_init:'Initialisierung',kobra_pausing:'Pausiert...',kobra_paused:'Pausiert',kobra_resuming:'Fortsetzen...',kobra_resumed:'Fortgesetzt',kobra_stopping:'Stoppt...',kobra_stoped:'Gestoppt',kobra_finished:'Abgeschlossen',kobra_failed:'Fehler',kobra_canceled:'Abgebrochen',kobra_offline:'Offline',
|
||||
nav_dashboard:'Dashboard',nav_print:'Druck',nav_temps:'Temperaturen',nav_motion:'Achsen',nav_ams:'AMS',nav_extras:'Licht / Lüfter',nav_console:'Konsole',
|
||||
card_progress:'Fortschritt',card_temps:'Temperaturen',card_light_fan:'Lüfter',card_speed:'Druckgeschwindigkeit',card_cam:'Kamera',lbl_elapsed:'Verstrichen:',lbl_remaining:'Restzeit:',lbl_slicer_time:'Slicer-Schätzung:',lbl_layers:'Layer',
|
||||
speed_silent:'🐢 Leise',speed_normal:'⚡ Normal',speed_sport:'🚀 Sport',
|
||||
lbl_light:'💡 Licht',lbl_feed:'Einziehen',lbl_unload:'Ausziehen',
|
||||
card_ace_dry:'ACE Trocknung',ace_dry_dryer:'Trockner',ace_dry_status_off:'Status: Aus',ace_dry_status_on:'Status: Aktiv',ace_dry_status_remaining:'Rest',ace_dry_humidity:'Luftfeuchte',ace_dry_current_temp:'Temperatur',ace_dry_chart:'Verlauf (Temp/Feuchte)',ace_dry_temp:'Temperatur (°C)',ace_dry_duration:'Dauer (Min)',ace_dry_start:'▶ Start',ace_dry_stop:'■ Stop',ace_dry_auto_refill:'Auto-Nachschub',ace_dry_enable:'Trocknung aktivieren',ace_dry_temp_line:'Trocknungstemperatur',ace_dry_time_line:'Trocknungszeit',ace_dry_ui_pending:'(nur UI, Backend folgt)',ace_dry_dialog_title:'Trockner Temp/Zeit-Einstellungen',ace_dry_dialog_temp:'Temperatur (30-80°C)',ace_dry_dialog_time:'Restzeit (h:m:s)',ace_dry_dialog_confirm:'Bestätigen',ace_dry_dialog_cancel:'Abbrechen',ace_dry_dialog_save_restart:'Speichern & Neustart',ace_dry_dialog_custom_name:'Eigener Name',ace_dry_dialog_reset_default:'Auf Standard zurücksetzen',
|
||||
cam_placeholder:'📷 Kamera nicht gestartet',btn_cam_start:'▶ Kamera',btn_cam_stop:'◼ Kamera',
|
||||
btn_pause:'⏸ Pause',btn_resume:'▶ Weiter',btn_cancel:'✕ Stopp',
|
||||
label_nozzle:'Nozzle',label_bed:'Bett',label_fan:'🌀 Lüfter',label_light:'💡 Licht',label_on_off:'Ein / Aus',label_speed:'Geschwindigkeit',
|
||||
panel_print_title:'Drucksteuerung',panel_print_btn_pause:'⏸ Pause',panel_print_btn_resume:'▶ Fortsetzen',panel_print_btn_cancel:'✕ Abbrechen',panel_print_temps_live:'Temperaturen (Live)',
|
||||
label_set:'Setzen',label_off:'Aus',
|
||||
panel_temps_nozzle:'Nozzle',panel_temps_bed:'Heizbett',panel_temps_chart:'Verlauf (letzte 60 Messungen)',label_target_c:'Ziel:',
|
||||
panel_motion_xy:'XY-Achsen',panel_motion_z:'Z-Achse',label_step:'Schrittweite:',btn_home_z:'Home Z',btn_home_xy:'Home XY',btn_home_all:'Home All',btn_disable_motors:'Motoren aus',
|
||||
panel_ams_title:'Filament',card_ams:'Filament',ams_no_data:'Keine AMS-Daten empfangen',label_slot:'Slot',ams_empty:'Leer',
|
||||
panel_extras_light:'Licht',panel_extras_fan:'Lüfter',panel_extras_camera:'Kamera',btn_cam_start2:'▶ Start',btn_cam_stop2:'◼ Stop',
|
||||
panel_console_title:'Ereignis-Log',
|
||||
log_light_on:'Licht an',log_light_off:'Licht aus',log_fan:'Lüfter →',log_nozzle:'Nozzle →',log_bed:'Bett →',log_axis:'Achse',log_home:'Home',log_home_all:'Home All',log_cam_start:'Kamera gestartet:',log_cam_stop:'Kamera gestoppt',log_poll_error:'Poll-Fehler:',log_error:'Fehler:',
|
||||
confirm_cancel:'Druck wirklich abbrechen?',
|
||||
settings_title:'Einstellungen',settings_connection:'Verbindung',settings_print:'Druckeinstellungen',settings_poll:'Poll-Intervall',settings_version:'Version',
|
||||
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',
|
||||
lbl_conn_error:'Verbindungsfehler:',
|
||||
slot_edit_title:'Slot bearbeiten',slot_edit_color:'Farbe',slot_edit_material:'Material',
|
||||
slot_edit_load:'⬇ Einziehen',slot_edit_unload:'⬆ Ausziehen',
|
||||
slot_edit_save:'💾 Speichern',slot_edit_custom:'z.B. PLA, PETG, ABS…',
|
||||
slot_edit_ok:'AMS Slot',
|
||||
log_dir_all:'Alle',log_lvl_label:'Level:',
|
||||
file_ready_btn:'▶ Druck starten',
|
||||
file_slots_btn:'🎨 Slots wählen',
|
||||
file_cancel_btn:'✕ Abbrechen',
|
||||
nav_printers:'Drucker',
|
||||
skip_title:'✂ Objekte überspringen',skip_hint:'Objekte abwählen, die nicht weiter gedruckt werden sollen:',
|
||||
skip_btn_label:'Objekte',skip_no_objects:'Keine Objekte in diesem Druck.',
|
||||
skip_already:'übersprungen',skip_select_at_least_one:'Bitte mindestens ein Objekt wählen.',
|
||||
skip_sending:'Sende …',skip_success:'Objekte werden übersprungen.',
|
||||
fd_objects_hint:'Objekte überspringen (optional):',
|
||||
add_printer:'Drucker hinzufügen',apd_lbl_ip:'Drucker-IP',apd_lbl_name:'Name (optional)',
|
||||
apd_fetching:'Hole Daten vom Drucker…',apd_success:'Drucker hinzugefügt, Bridge startet neu…',apd_err_ip:'Bitte IP-Adresse eingeben',
|
||||
printers_remove:'Drucker entfernen',printers_remove_confirm:'Drucker "{name}" entfernen? Die Bridge startet neu.',
|
||||
printers_active:'● aktiv',
|
||||
printers_switch:'Wechseln →',
|
||||
printers_current:'Aktueller Drucker',
|
||||
printers_loading:'Lade…',
|
||||
printers_none:'Keine Drucker konfiguriert.',
|
||||
printers_empty_hint:'Noch kein Drucker eingerichtet.',
|
||||
nav_browser:'Browser',
|
||||
panel_browser_title:'Datei-Browser',
|
||||
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',
|
||||
ss_date:'↓ Datum',ss_name:'A–Z Name',ss_dur:'⏱ Druckzeit'
|
||||
};
|
||||
var LANG_EN={
|
||||
header_status_standby:'Ready',header_status_printing:'Printing',header_status_complete:'Complete',header_status_error:'Error',
|
||||
kobra_free:'Ready',kobra_busy:'Busy',kobra_printing:'Printing',kobra_preheating:'Preheating',kobra_auto_leveling:'Auto Leveling',kobra_checking:'Checking',kobra_updated:'Updating',kobra_init:'Initializing',kobra_pausing:'Pausing...',kobra_paused:'Paused',kobra_resuming:'Resuming...',kobra_resumed:'Resumed',kobra_stopping:'Stopping...',kobra_stoped:'Stopped',kobra_finished:'Finished',kobra_failed:'Error',kobra_canceled:'Cancelled',kobra_offline:'Offline',
|
||||
nav_dashboard:'Dashboard',nav_print:'Print',nav_temps:'Temperatures',nav_motion:'Motion',nav_ams:'AMS',nav_extras:'Light / Fan',nav_console:'Console',
|
||||
card_progress:'Progress',card_temps:'Temperatures',card_light_fan:'Fan',card_speed:'Print Speed',card_cam:'Camera',lbl_elapsed:'Elapsed:',lbl_remaining:'Remaining:',lbl_slicer_time:'Slicer estimate:',lbl_layers:'Layer',
|
||||
speed_silent:'🐢 Silent',speed_normal:'⚡ Normal',speed_sport:'🚀 Sport',
|
||||
lbl_light:'💡 Light',lbl_feed:'Load',lbl_unload:'Unload',
|
||||
card_ace_dry:'ACE Drying',ace_dry_dryer:'Dryer',ace_dry_status_off:'Status: Off',ace_dry_status_on:'Status: Active',ace_dry_status_remaining:'Remaining',ace_dry_humidity:'Humidity',ace_dry_current_temp:'Temperature',ace_dry_chart:'History (Temp/Humidity)',ace_dry_temp:'Temperature (°C)',ace_dry_duration:'Duration (min)',ace_dry_start:'▶ Start',ace_dry_stop:'■ Stop',ace_dry_auto_refill:'Auto Refill',ace_dry_enable:'Enable Drying',ace_dry_temp_line:'Drying Temperature',ace_dry_time_line:'Drying Time',ace_dry_ui_pending:'(UI only, backend next)',ace_dry_dialog_title:'Dryer Temp/Time Settings',ace_dry_dialog_temp:'Temperature (30-80°C)',ace_dry_dialog_time:'Rem. Time (h:m:s)',ace_dry_dialog_confirm:'Confirm',ace_dry_dialog_cancel:'Cancel',ace_dry_dialog_save_restart:'Save & Restart',ace_dry_dialog_custom_name:'Custom Name',ace_dry_dialog_reset_default:'Reset to Default',
|
||||
cam_placeholder:'📷 Camera not started',btn_cam_start:'▶ Camera',btn_cam_stop:'◼ Camera',
|
||||
btn_pause:'⏸ Pause',btn_resume:'▶ Resume',btn_cancel:'✕ Stop',
|
||||
label_nozzle:'Nozzle',label_bed:'Bed',label_fan:'🌀 Fan',label_light:'💡 Light',label_on_off:'On / Off',label_speed:'Speed',
|
||||
panel_print_title:'Print Control',panel_print_btn_pause:'⏸ Pause',panel_print_btn_resume:'▶ Resume',panel_print_btn_cancel:'✕ Cancel',panel_print_temps_live:'Temperatures (Live)',
|
||||
label_set:'Set',label_off:'Off',
|
||||
panel_temps_nozzle:'Nozzle',panel_temps_bed:'Heated Bed',panel_temps_chart:'History (last 60 readings)',label_target_c:'Target:',
|
||||
panel_motion_xy:'XY Axes',panel_motion_z:'Z Axis',label_step:'Step size:',btn_home_z:'Home Z',btn_home_xy:'Home XY',btn_home_all:'Home All',btn_disable_motors:'Motors Off',
|
||||
panel_ams_title:'Filament',card_ams:'Filament',ams_no_data:'No AMS data received',label_slot:'Slot',ams_empty:'Empty',
|
||||
panel_extras_light:'Light',panel_extras_fan:'Fan',panel_extras_camera:'Camera',btn_cam_start2:'▶ Start',btn_cam_stop2:'◼ Stop',
|
||||
panel_console_title:'Event Log',
|
||||
log_light_on:'Light on',log_light_off:'Light off',log_fan:'Fan →',log_nozzle:'Nozzle →',log_bed:'Bed →',log_axis:'Axis',log_home:'Home',log_home_all:'Home All',log_cam_start:'Camera started:',log_cam_stop:'Camera stopped',log_poll_error:'Poll error:',log_error:'Error:',
|
||||
confirm_cancel:'Really cancel the print?',
|
||||
settings_title:'Settings',settings_connection:'Connection',settings_print:'Print Settings',settings_poll:'Poll Interval',settings_version:'Version',
|
||||
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',
|
||||
lbl_conn_error:'Connection error:',
|
||||
slot_edit_title:'Edit Slot',slot_edit_color:'Color',slot_edit_material:'Material',
|
||||
slot_edit_load:'⬇ Load',slot_edit_unload:'⬆ Unload',
|
||||
slot_edit_save:'💾 Save',slot_edit_custom:'e.g. PLA, PETG, ABS…',
|
||||
slot_edit_ok:'AMS Slot',
|
||||
log_dir_all:'All',log_lvl_label:'Level:',
|
||||
file_ready_btn:'▶ Start Print',
|
||||
file_slots_btn:'🎨 Select Slots',
|
||||
file_cancel_btn:'✕ Cancel',
|
||||
nav_printers:'Printers',
|
||||
skip_title:'✂ Skip objects',skip_hint:'Uncheck objects you no longer want to print:',
|
||||
skip_btn_label:'Objects',skip_no_objects:'No objects in this print.',
|
||||
skip_already:'skipped',skip_select_at_least_one:'Please pick at least one object.',
|
||||
skip_sending:'Sending …',skip_success:'Objects will be skipped.',
|
||||
fd_objects_hint:'Skip objects (optional):',
|
||||
add_printer:'Add printer',apd_lbl_ip:'Printer IP',apd_lbl_name:'Name (optional)',
|
||||
apd_fetching:'Fetching data from printer…',apd_success:'Printer added, bridge restarting…',apd_err_ip:'Please enter an IP address',
|
||||
printers_remove:'Remove printer',printers_remove_confirm:'Remove printer "{name}"? The bridge will restart.',
|
||||
printers_active:'● active',
|
||||
printers_switch:'Switch →',
|
||||
printers_current:'Current printer',
|
||||
printers_loading:'Loading…',
|
||||
printers_none:'No printers configured.',
|
||||
printers_empty_hint:'No printer set up yet.',
|
||||
nav_browser:'Browser',
|
||||
panel_browser_title:'File Browser',
|
||||
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',
|
||||
ss_date:'↓ Date',ss_name:'A–Z Name',ss_dur:'⏱ Print time'
|
||||
};
|
||||
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==='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==='zh-cn')return l;
|
||||
|
||||
var base=l.split('-')[0];
|
||||
if(base==='de'||base==='en'||base==='es')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;
|
||||
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;
|
||||
@@ -285,17 +231,6 @@ document.addEventListener('click',function(e){
|
||||
if(menu)menu.style.display='none';
|
||||
}
|
||||
});
|
||||
|
||||
var currentLang='de';
|
||||
var T=LANG_DE;
|
||||
function toggleLang(){
|
||||
currentLang=currentLang==='de'?'en':'de';
|
||||
T=currentLang==='de'?LANG_DE:LANG_EN;
|
||||
localStorage.setItem('lang',currentLang);
|
||||
document.getElementById('lang-btn').textContent=currentLang==='de'?'EN':'DE';
|
||||
document.documentElement.setAttribute('lang',currentLang);
|
||||
applyLang();
|
||||
}
|
||||
function applyLang(){
|
||||
ensureAceDryCards();
|
||||
// Nav
|
||||
@@ -318,8 +253,17 @@ function applyLang(){
|
||||
setText('fd-objects-hint',T.fd_objects_hint);
|
||||
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);
|
||||
@@ -339,10 +283,11 @@ function applyLang(){
|
||||
setText('d-slicer-label',T.lbl_slicer_time);
|
||||
setText('d-lbl-layers',T.lbl_layers);
|
||||
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);
|
||||
@@ -391,31 +336,35 @@ function applyLang(){
|
||||
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)+' - '+(T.ace_dry_dryer||'Dryer'));
|
||||
setText('d-ace-auto-refill-label-'+i,T.ace_dry_auto_refill||'Auto Refill');
|
||||
setText('d-ace-drying-enable-label-'+i,T.ace_dry_enable||'Enable Drying');
|
||||
setText('d-ace-dry-humidity-label-'+i,(T.ace_dry_humidity||'Humidity')+':');
|
||||
setText('d-ace-dry-current-temp-label-'+i,(T.ace_dry_current_temp||'Current Temp')+':');
|
||||
setText('d-ace-dry-target-label-'+i,(T.ace_dry_temp_line||'Drying Temperature')+':');
|
||||
setText('d-ace-dry-time-label-'+i,(T.ace_dry_time_line||'Drying Time')+':');
|
||||
setText('d-ace-dry-chart-label-'+i,T.ace_dry_chart||'History (Temp/Humidity)');
|
||||
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',T.ace_dry_dialog_title||'Dryer Temp/Time Settings');
|
||||
setText('ace-dry-dialog-temp-label',T.ace_dry_dialog_temp||'Temperature (30-80°C)');
|
||||
setText('ace-dry-dialog-time-label',T.ace_dry_dialog_time||'Rem. Time (h:m:s)');
|
||||
setText('ace-dry-dialog-custom-name-label',T.ace_dry_dialog_custom_name||'Custom Name');
|
||||
setText('ace-dry-dialog-cancel',T.ace_dry_dialog_cancel||'Cancel');
|
||||
setText('ace-dry-dialog-confirm',T.ace_dry_dialog_confirm||'Confirm');
|
||||
setText('ace-dry-dialog-reset-default',T.ace_dry_dialog_reset_default||'Reset to Default');
|
||||
setText('ace-dry-dialog-save-preset',T.ace_dry_dialog_save_restart||'Save & Restart');
|
||||
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);
|
||||
@@ -425,7 +374,11 @@ function applyLang(){
|
||||
setText('file-ready-btn',T.file_ready_btn);
|
||||
setText('file-slots-btn',T.file_slots_btn);
|
||||
setText('file-cancel-btn',T.file_cancel_btn);
|
||||
setText('file-cancel-btn',T.file_cancel_btn);
|
||||
// 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;}
|
||||
|
||||
@@ -471,13 +424,14 @@ function ensureAceDryCards(){
|
||||
grid.setAttribute('data-init','1');
|
||||
}
|
||||
(function(){
|
||||
var l=localStorage.getItem('lang')||'de';
|
||||
currentLang=l;T=l==='de'?LANG_DE:LANG_EN;
|
||||
document.getElementById('lang-btn').textContent=l==='de'?'EN':'DE';
|
||||
document.documentElement.setAttribute('lang',l);
|
||||
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(){
|
||||
applyLang();
|
||||
setLanguage(currentLang).catch(function(){});
|
||||
// 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();}
|
||||
@@ -649,7 +603,7 @@ function applyState(){
|
||||
_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='⚠ '+(T.lbl_conn_error||'Connection error:')+' '+s.connection_error;banner.style.display='block';}else{banner.style.display='none';}}
|
||||
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 frb=document.getElementById('file-ready-banner');
|
||||
if(frb){
|
||||
if(s.file_ready&&s.print_state==='standby'){
|
||||
@@ -658,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');
|
||||
@@ -734,7 +691,7 @@ function applyState(){
|
||||
|
||||
var amsTitle=document.getElementById('d-card-ams');
|
||||
if(amsTitle){
|
||||
var baseTitle=T.card_ams||'Filament';
|
||||
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;
|
||||
@@ -823,18 +780,25 @@ function applyState(){
|
||||
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='Slot '+(globalIdx+1);
|
||||
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>'
|
||||
+'</div>';
|
||||
});
|
||||
if(bid===-1&&acePresent){
|
||||
html+='<div class="ams-slot ams-slot-bridge" aria-label="Slot 4 connected to ACE">'
|
||||
html+='<div class="ams-slot ams-slot-bridge" aria-label="'+T.label_slot+' 4">'
|
||||
+'<div class="bridge-chip">ACE</div>'
|
||||
+'</div>';
|
||||
}
|
||||
@@ -861,10 +825,10 @@ function updateConnBtn(){
|
||||
var offline=S.kobra_state==='offline';
|
||||
if(offline){
|
||||
btn.className='conn-btn disconnected';
|
||||
btn.textContent=T.btn_connect||'⚡ Verbinden';
|
||||
btn.textContent=tr('btn_connect');
|
||||
} else {
|
||||
btn.className='conn-btn connected';
|
||||
btn.textContent=T.btn_disconnect||'✕ Trennen';
|
||||
btn.textContent=tr('btn_disconnect');
|
||||
}
|
||||
}
|
||||
|
||||
@@ -956,7 +920,53 @@ function updateSlotEditFeedButton(){
|
||||
return;
|
||||
}
|
||||
btn.style.display='';
|
||||
btn.textContent=_slotEditLoaded?(T.slot_edit_unload||'⬆ Unload'):(T.slot_edit_load||'⬇ Load');
|
||||
btn.textContent=_slotEditLoaded?tr('slot_edit_unload'):tr('slot_edit_load');
|
||||
}
|
||||
var _orcaFilamentCache=null; // [{id,name,vendor,type,color}, …]
|
||||
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();
|
||||
var matched=profiles.filter(function(p){
|
||||
var pt=(p.type||'').toUpperCase();
|
||||
// PLA-CF, PLA-SILK etc. zählen auch zu PLA
|
||||
return matU==='' || pt===matU || pt.startsWith(matU+'-') || pt.startsWith(matU+' ');
|
||||
});
|
||||
sel.innerHTML='<option value="">'+tr('slot_edit_profile_default')+'</option>';
|
||||
// Gruppieren nach Vendor
|
||||
var byVendor={};
|
||||
matched.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){
|
||||
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.name;
|
||||
if(o.value===wantKey) o.selected=true;
|
||||
g.appendChild(o);
|
||||
});
|
||||
sel.appendChild(g);
|
||||
});
|
||||
});
|
||||
}
|
||||
function openSlotEdit(i){
|
||||
var slot=(window._amsSlots||[])[i]||{};
|
||||
@@ -977,6 +987,19 @@ function openSlotEdit(i){
|
||||
+'style="padding:4px 10px;border-radius:6px;border:1px solid var(--border);cursor:pointer;font-size:12px;'
|
||||
+(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 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');
|
||||
}
|
||||
@@ -1010,7 +1033,7 @@ function startReadyFile(){
|
||||
if(btn){btn.disabled=false;setText('file-ready-btn',T.file_ready_btn);}
|
||||
})
|
||||
.catch(function(e){
|
||||
clog((T.log_error||'Error:')+' '+e,'msg-err');
|
||||
clog(tr('log_error')+' '+e,'msg-err');
|
||||
if(btn){btn.disabled=false;setText('file-ready-btn',T.file_ready_btn);}
|
||||
});
|
||||
}
|
||||
@@ -1021,6 +1044,9 @@ function cancelReadyFile(){
|
||||
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){
|
||||
@@ -1028,6 +1054,8 @@ function highlightMatBtn(val){
|
||||
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);
|
||||
@@ -1037,13 +1065,56 @@ function saveSlotEdit(){
|
||||
var hex=document.getElementById('slot-edit-color').value;
|
||||
var mat=document.getElementById('slot-edit-mat').value.trim().toUpperCase()||'PLA';
|
||||
var color=hexToRgb(hex);
|
||||
post('/api/ams/set_slot',{index:_slotEditIndex,type:mat,color:color})
|
||||
.then(function(r){return r.json();})
|
||||
.then(function(r){
|
||||
closeSlotEdit();
|
||||
clog((T.slot_edit_ok||'AMS Slot')+' '+(_slotEditIndex+1)+': '+mat+' '+hex,'msg-ok');
|
||||
})
|
||||
.catch(function(e){clog('Fehler: '+e,'msg-err');});
|
||||
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');
|
||||
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(){
|
||||
@@ -1140,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);
|
||||
})();
|
||||
|
||||
@@ -1148,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
|
||||
@@ -1266,13 +1373,13 @@ function camStart(){
|
||||
img.style.display='none';
|
||||
ph.style.display='flex';
|
||||
camOn=false;
|
||||
document.getElementById('cam-toggle-btn').textContent=T.btn_cam_start||'▶ Kamera';
|
||||
clog((T.log_error||'Fehler:')+' Stream nicht verfügbar','msg-err');
|
||||
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();
|
||||
camOn=true;
|
||||
document.getElementById('cam-toggle-btn').textContent=T.btn_cam_stop||'◼ Kamera';
|
||||
clog((T.log_cam_start||'Kamera gestartet'),'msg-ok');
|
||||
document.getElementById('cam-toggle-btn').textContent=tr('btn_cam_stop');
|
||||
clog(tr('log_cam_start'),'msg-ok');
|
||||
// MJPEG liefert kein onload – Spinner nach kurzem Timeout ausblenden
|
||||
setTimeout(function(){
|
||||
sp.style.display='none';
|
||||
@@ -1281,7 +1388,7 @@ function camStart(){
|
||||
}).catch(function(e){
|
||||
sp.style.display='none';
|
||||
ph.style.display='flex';
|
||||
clog((T.log_error||'Fehler:')+' '+e,'msg-err');
|
||||
clog(tr('log_error')+' '+e,'msg-err');
|
||||
});
|
||||
}
|
||||
|
||||
@@ -1292,8 +1399,8 @@ function camStop(){
|
||||
img.style.display='none';
|
||||
document.getElementById('cam-placeholder').style.display='flex';
|
||||
camOn=false;
|
||||
document.getElementById('cam-toggle-btn').textContent=T.btn_cam_start||'▶ Kamera';
|
||||
clog((T.log_cam_stop||'Kamera gestoppt'),'msg-ok');
|
||||
document.getElementById('cam-toggle-btn').textContent=tr('btn_cam_start');
|
||||
clog(tr('log_cam_stop'),'msg-ok');
|
||||
}
|
||||
|
||||
function aceDryStart(aceId){
|
||||
@@ -1307,7 +1414,7 @@ function aceDryStart(aceId){
|
||||
.then(function(r){return r.json();})
|
||||
.then(function(r){
|
||||
if(r.error){throw new Error(r.error);}
|
||||
clog('ACE '+(aceId+1)+' - '+(T.ace_dry_dryer||'Dryer')+': '+(T.ace_dry_start||'start')+' ('+t+'°C, '+d+' min)','msg-ok');
|
||||
clog('ACE '+(aceId+1)+' - '+tr('ace_dry_dryer')+': '+tr('ace_dry_start')+' ('+t+'°C, '+d+' min)','msg-ok');
|
||||
poll();
|
||||
})
|
||||
.catch(function(e){clog('ACE-Fehler: '+e,'msg-err');});
|
||||
@@ -1323,7 +1430,7 @@ function aceAutoRefillToggle(aceId){
|
||||
.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)+' - '+(T.ace_dry_auto_refill||'Auto Refill')+': '+(on?'ON':'OFF'),'msg-ok');
|
||||
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;});
|
||||
}
|
||||
@@ -1351,7 +1458,7 @@ function openAceDryDialog(aceId){
|
||||
aceDryDialogUpdateSaveButton();
|
||||
aceDryDialogUpdateResetButton();
|
||||
var sb=document.getElementById('ace-dry-dialog-save-preset');
|
||||
if(sb){sb.disabled=false;sb.textContent=T.ace_dry_dialog_save_restart||'Save & Restart';}
|
||||
if(sb){sb.disabled=false;sb.textContent=tr('ace_dry_dialog_save_restart');}
|
||||
document.getElementById('ace-dry-dialog').classList.add('open');
|
||||
}
|
||||
|
||||
@@ -1509,11 +1616,11 @@ function saveAceDryPresetAndRestart(){
|
||||
};
|
||||
return post('/api/settings',d);
|
||||
}).then(function(){
|
||||
clog('ACE preset '+key+' '+(T.settings_save||'Save & Restart'),'msg-ok');
|
||||
clog('ACE preset '+key+' '+tr('settings_save'),'msg-ok');
|
||||
closeAceDryDialog();
|
||||
}).catch(function(e){
|
||||
btn.disabled=false;
|
||||
btn.textContent=T.ace_dry_dialog_save_restart||'Save & Restart';
|
||||
btn.textContent=tr('ace_dry_dialog_save_restart');
|
||||
clog('ACE-Preset Fehler: '+e,'msg-err');
|
||||
});
|
||||
}
|
||||
@@ -1549,7 +1656,7 @@ function aceDryStop(aceId){
|
||||
.then(function(r){return r.json();})
|
||||
.then(function(r){
|
||||
if(r.error){throw new Error(r.error);}
|
||||
clog('ACE '+(aceId+1)+' - '+(T.ace_dry_dryer||'Dryer')+': '+(T.ace_dry_stop||'stop'),'msg-ok');
|
||||
clog('ACE '+(aceId+1)+' - '+tr('ace_dry_dryer')+': '+tr('ace_dry_stop'),'msg-ok');
|
||||
poll();
|
||||
})
|
||||
.catch(function(e){clog('ACE-Fehler: '+e,'msg-err');});
|
||||
@@ -1567,7 +1674,7 @@ function uploadGcode(file){
|
||||
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(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();
|
||||
@@ -1579,7 +1686,7 @@ function uploadGcode(file){
|
||||
return r.json();
|
||||
})
|
||||
.then(function(){
|
||||
if(status){ status.textContent='✓ '+file.name; status.className='upload-status-ok'; }
|
||||
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='';}
|
||||
@@ -1588,7 +1695,7 @@ function uploadGcode(file){
|
||||
}, 3000);
|
||||
})
|
||||
.catch(function(e){
|
||||
if(status){ status.textContent='✗ '+e.message; status.className='upload-status-err'; }
|
||||
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('Upload-Fehler: '+e,'msg-err');
|
||||
@@ -1660,7 +1767,7 @@ function renderStore(){
|
||||
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">⏱ Schätzung: '+est+'</div>'+
|
||||
'<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="storePrint(\''+f.id+'\',\''+f.filename.replace(/'/g,"\\'")+'\')" '+
|
||||
@@ -1967,7 +2074,7 @@ function openFilamentDialog(slots){
|
||||
});
|
||||
|
||||
if(!_amsSlots.length){
|
||||
body.innerHTML='<p style="color:var(--txt2);font-size:13px;text-align:center;padding:16px 0">Keine belegten AMS-Slots.<br>Druck trotzdem starten?</p>';
|
||||
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);
|
||||
@@ -1981,18 +2088,18 @@ function openFilamentDialog(slots){
|
||||
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+'" '+sel+'>'+
|
||||
'● Slot '+(s.slot_index+1)+' · '+s.material+'</option>';
|
||||
'● '+T.fd_slot+' '+(s.slot_index+1)+' · '+s.material+'</option>';
|
||||
}).join('');
|
||||
if(!compatible.length){
|
||||
opts='<option value="-1" data-color="#888888" data-material="" selected>⚠ No matching material</option>';
|
||||
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">USED</span>'
|
||||
: '<span style="font-size:10px;color:var(--txt2);font-weight:700;min-width:32px;opacity:.75">USED</span>';
|
||||
? '<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>'+
|
||||
@@ -2061,7 +2168,7 @@ function confirmFilamentPrint(){
|
||||
if(btn){btn.disabled=false;setText('file-ready-btn',T.file_ready_btn);}
|
||||
})
|
||||
.catch(function(e){
|
||||
clog((T.log_error||'Error:')+' '+e,'msg-err');
|
||||
clog(tr('log_error')+' '+e,'msg-err');
|
||||
if(btn){btn.disabled=false;setText('file-ready-btn',T.file_ready_btn);}
|
||||
});
|
||||
} else {
|
||||
@@ -2165,13 +2272,13 @@ 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">'+(T.skip_no_objects||'Keine Objekte in diesem Druck.')+'</div>';
|
||||
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">'+(T.skip_already||'übersprungen')+'</span>':'';
|
||||
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+
|
||||
@@ -2222,13 +2329,13 @@ 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=T.skip_select_at_least_one||'Bitte mindestens ein Objekt wählen.';st.style.color='var(--warn)';return;}
|
||||
btn.disabled=true; st.textContent=T.skip_sending||'Sende …'; st.style.color='var(--txt2)';
|
||||
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)||'Fehler';st.style.color='var(--err)';btn.disabled=false;return;}
|
||||
st.textContent=T.skip_success||'Objekte werden übersprungen.';st.style.color='var(--ok)';
|
||||
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);
|
||||
})
|
||||
|
||||
@@ -32,7 +32,15 @@
|
||||
<span id="h-version" style="font-size:11px;opacity:.5;margin-left:6px"></span>
|
||||
<div class="hbadge" id="h-badge"><span class="dot"></span><span id="h-state">Standby</span></div>
|
||||
<button class="theme-btn" onclick="toggleTheme()">☀ / ☾</button>
|
||||
<button class="theme-btn" onclick="toggleLang()" id="lang-btn">EN</button>
|
||||
<div style="display:flex;align-items:center;gap:6px">
|
||||
<span aria-hidden="true" style="font-size:15px;line-height:1;opacity:.85">🌐</span>
|
||||
<select class="theme-btn" id="lang-select" onchange="setLanguageFromSelect()" style="padding:6px 10px">
|
||||
<option value="de">Deutsch</option>
|
||||
<option value="en">English</option>
|
||||
<option value="es">Espanol</option>
|
||||
<option value="zh-cn">中文(简体)</option>
|
||||
</select>
|
||||
</div>
|
||||
<button class="theme-btn" onclick="openSettings()" id="settings-btn" title="Einstellungen">⚙</button>
|
||||
<button class="conn-btn disconnected" id="conn-btn" onclick="toggleConnection()">⚡ Verbinden</button>
|
||||
</header>
|
||||
@@ -154,6 +162,15 @@
|
||||
oninput="highlightMatBtn(this.value)"
|
||||
style="margin-top:8px;width:100%;padding:6px 10px;background:var(--raised);border:1px solid var(--border);border-radius:6px;color:var(--txt);font-size:13px;box-sizing:border-box">
|
||||
</div>
|
||||
<!-- Orca-Filament-Profil-Override (für AMS-Sync) -->
|
||||
<div style="margin-bottom:20px">
|
||||
<div style="font-size:11px;color:var(--txt2);margin-bottom:6px" id="lbl-slot-profile"></div>
|
||||
<select id="slot-edit-profile"
|
||||
style="width:100%;padding:6px 10px;background:var(--raised);border:1px solid var(--border);border-radius:6px;color:var(--txt);font-size:13px;box-sizing:border-box">
|
||||
<option value="" id="slot-profile-default-opt"></option>
|
||||
</select>
|
||||
<div style="font-size:11px;color:var(--txt2);margin-top:4px" id="slot-profile-hint"></div>
|
||||
</div>
|
||||
<button class="btn" id="btn-slot-edit-feed" style="width:100%;margin-bottom:8px" onclick="slotEditFeed()"></button>
|
||||
<button class="modal-save" id="btn-slot-edit-save" onclick="saveSlotEdit()"></button>
|
||||
</div>
|
||||
@@ -226,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>
|
||||
@@ -239,7 +255,7 @@
|
||||
<div class="card-title"><span>⊙</span> <span id="d-card-temps">Temperaturen</span></div>
|
||||
<div class="temp-card-inner">
|
||||
<div class="temp-block">
|
||||
<div class="temp-label">Nozzle</div>
|
||||
<div class="temp-label" id="d-lbl-nozzle">Nozzle</div>
|
||||
<div class="temp-row">
|
||||
<div class="temp-val" id="d-nt">–</div>
|
||||
<div class="temp-unit">°C</div>
|
||||
@@ -411,7 +427,7 @@
|
||||
<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-label"><span id="store-upload-label-prefix">GCode hierher ziehen oder </span><u id="store-upload-label-browse">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">
|
||||
@@ -465,18 +481,18 @@
|
||||
</nav>
|
||||
|
||||
|
||||
<!-- Filament-Slot-Dialog -->
|
||||
<!-- Web-Upload-Verify-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>
|
||||
<p id="store-web-verify-msg" style="font-size:13px;color:var(--txt);margin-bottom:12px">Bitte bestätige, dass diese Datei für den Anycubic Kobra X erstellt wurde.</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>
|
||||
<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">Abbrechen</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">Bestätigen</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@@ -514,7 +530,7 @@
|
||||
<input type="text" id="apd-name" placeholder="z.B. Kobra X Wohnzimmer" style="width:100%;box-sizing:border-box;padding:8px;background:var(--raised);border:1px solid var(--border);border-radius:6px;color:var(--txt);margin-bottom:6px">
|
||||
<div id="apd-status" style="font-size:12px;margin:8px 0;min-height:16px;color:var(--txt2)"></div>
|
||||
<div style="display:flex;gap:8px;justify-content:flex-end">
|
||||
<button onclick="closeAddPrinterDialog()" style="padding:8px 16px;background:var(--raised);border:1px solid var(--border);border-radius:8px;color:var(--txt);cursor:pointer">Abbrechen</button>
|
||||
<button id="apd-cancel" onclick="closeAddPrinterDialog()" style="padding:8px 16px;background:var(--raised);border:1px solid var(--border);border-radius:8px;color:var(--txt);cursor:pointer">Abbrechen</button>
|
||||
<button id="apd-confirm" onclick="confirmAddPrinter()" style="padding:8px 18px;background:var(--accent);color:#fff;border:none;border-radius:8px;cursor:pointer;font-weight:600">Hinzufügen</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
234
web/translations/de.json
Normal file
234
web/translations/de.json
Normal file
@@ -0,0 +1,234 @@
|
||||
{
|
||||
"header_status_standby": "Bereit",
|
||||
"header_status_printing": "Druckt",
|
||||
"header_status_complete": "Fertig",
|
||||
"header_status_error": "Fehler",
|
||||
"kobra_free": "Bereit",
|
||||
"kobra_busy": "Beschäftigt",
|
||||
"kobra_printing": "Druckt",
|
||||
"kobra_preheating": "Aufheizen",
|
||||
"kobra_auto_leveling": "Nivellierung",
|
||||
"kobra_checking": "Prüfung",
|
||||
"kobra_updated": "Aktualisierung",
|
||||
"kobra_init": "Initialisierung",
|
||||
"kobra_pausing": "Pausiert...",
|
||||
"kobra_paused": "Pausiert",
|
||||
"kobra_resuming": "Fortsetzen...",
|
||||
"kobra_resumed": "Fortgesetzt",
|
||||
"kobra_stopping": "Stoppt...",
|
||||
"kobra_stoped": "Gestoppt",
|
||||
"kobra_finished": "Abgeschlossen",
|
||||
"kobra_failed": "Fehler",
|
||||
"kobra_canceled": "Abgebrochen",
|
||||
"kobra_offline": "Offline",
|
||||
"nav_dashboard": "Dashboard",
|
||||
"nav_print": "Druck",
|
||||
"nav_temps": "Temperaturen",
|
||||
"nav_motion": "Achsen",
|
||||
"nav_ams": "AMS",
|
||||
"nav_extras": "Licht / Lüfter",
|
||||
"nav_console": "Konsole",
|
||||
"card_progress": "Fortschritt",
|
||||
"card_temps": "Temperaturen",
|
||||
"card_light_fan": "Lüfter",
|
||||
"card_speed": "Druckgeschwindigkeit",
|
||||
"card_cam": "Kamera",
|
||||
"lbl_elapsed": "Verstrichen:",
|
||||
"lbl_remaining": "Restzeit:",
|
||||
"lbl_slicer_time": "Slicer-Schätzung:",
|
||||
"lbl_layers": "Layer",
|
||||
"speed_silent": "🐢 Leise",
|
||||
"speed_normal": "⚡ Normal",
|
||||
"speed_sport": "🚀 Sport",
|
||||
"lbl_light": "💡 Licht",
|
||||
"lbl_feed": "Einziehen",
|
||||
"lbl_unload": "Ausziehen",
|
||||
"card_ace_dry": "ACE Trocknung",
|
||||
"ace_dry_dryer": "Trockner",
|
||||
"ace_dry_status_off": "Status: Aus",
|
||||
"ace_dry_status_on": "Status: Aktiv",
|
||||
"ace_dry_status_remaining": "Rest",
|
||||
"ace_dry_humidity": "Luftfeuchte",
|
||||
"ace_dry_current_temp": "Temperatur",
|
||||
"ace_dry_chart": "Verlauf (Temp/Feuchte)",
|
||||
"ace_dry_temp": "Temperatur (°C)",
|
||||
"ace_dry_duration": "Dauer (Min)",
|
||||
"ace_dry_start": "▶ Start",
|
||||
"ace_dry_stop": "■ Stop",
|
||||
"ace_dry_auto_refill": "Auto-Nachschub",
|
||||
"ace_dry_enable": "Trocknung aktivieren",
|
||||
"ace_dry_temp_line": "Trocknungstemperatur",
|
||||
"ace_dry_time_line": "Trocknungszeit",
|
||||
"ace_dry_ui_pending": "(nur UI, Backend folgt)",
|
||||
"ace_dry_dialog_title": "Trockner Temp/Zeit-Einstellungen",
|
||||
"ace_dry_dialog_temp": "Temperatur (30-80°C)",
|
||||
"ace_dry_dialog_time": "Restzeit (h:m:s)",
|
||||
"ace_dry_dialog_confirm": "Bestätigen",
|
||||
"ace_dry_dialog_cancel": "Abbrechen",
|
||||
"ace_dry_dialog_save_restart": "Speichern & Neustart",
|
||||
"ace_dry_dialog_custom_name": "Eigener Name",
|
||||
"ace_dry_dialog_reset_default": "Auf Standard zurücksetzen",
|
||||
"cam_placeholder": "📷 Kamera nicht gestartet",
|
||||
"cam_stream_unavailable": "Stream nicht verfügbar",
|
||||
"btn_cam_start": "▶ Kamera",
|
||||
"btn_cam_stop": "◼ Kamera",
|
||||
"btn_pause": "⏸ Pause",
|
||||
"btn_resume": "▶ Weiter",
|
||||
"btn_cancel": "✕ Stopp",
|
||||
"label_nozzle": "Düse",
|
||||
"label_bed": "Bett",
|
||||
"label_fan": "🌀 Lüfter",
|
||||
"label_light": "💡 Licht",
|
||||
"label_on_off": "Ein / Aus",
|
||||
"label_speed": "Geschwindigkeit",
|
||||
"panel_print_title": "Drucksteuerung",
|
||||
"panel_print_btn_pause": "⏸ Pause",
|
||||
"panel_print_btn_resume": "▶ Fortsetzen",
|
||||
"panel_print_btn_cancel": "✕ Abbrechen",
|
||||
"panel_print_temps_live": "Temperaturen (Live)",
|
||||
"label_set": "Setzen",
|
||||
"label_off": "Aus",
|
||||
"panel_temps_nozzle": "Düse",
|
||||
"panel_temps_bed": "Heizbett",
|
||||
"panel_temps_chart": "Verlauf (letzte 60 Messungen)",
|
||||
"label_target_c": "Ziel:",
|
||||
"panel_motion_xy": "XY-Achsen",
|
||||
"panel_motion_z": "Z-Achse",
|
||||
"label_step": "Schrittweite:",
|
||||
"btn_home_z": "Home Z",
|
||||
"btn_home_xy": "Home XY",
|
||||
"btn_home_all": "Home All",
|
||||
"btn_disable_motors": "Motoren aus",
|
||||
"panel_ams_title": "Filament",
|
||||
"card_ams": "Filament",
|
||||
"ams_no_data": "Keine AMS-Daten empfangen",
|
||||
"label_slot": "Slot",
|
||||
"ams_empty": "Leer",
|
||||
"panel_extras_light": "Licht",
|
||||
"panel_extras_fan": "Lüfter",
|
||||
"panel_extras_camera": "Kamera",
|
||||
"btn_cam_start2": "▶ Start",
|
||||
"btn_cam_stop2": "◼ Stop",
|
||||
"panel_console_title": "Ereignis-Log",
|
||||
"log_light_on": "Licht an",
|
||||
"log_light_off": "Licht aus",
|
||||
"log_fan": "Lüfter →",
|
||||
"log_nozzle": "Düse →",
|
||||
"log_bed": "Bett →",
|
||||
"log_axis": "Achse",
|
||||
"log_home": "Home",
|
||||
"log_home_all": "Home All",
|
||||
"log_cam_start": "Kamera gestartet:",
|
||||
"log_cam_stop": "Kamera gestoppt",
|
||||
"log_poll_error": "Poll-Fehler:",
|
||||
"log_error": "Fehler:",
|
||||
"confirm_cancel": "Druck wirklich abbrechen?",
|
||||
"settings_title": "Einstellungen",
|
||||
"settings_connection": "Verbindung",
|
||||
"settings_print": "Druckeinstellungen",
|
||||
"settings_poll": "Poll-Intervall",
|
||||
"settings_version": "Version",
|
||||
"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",
|
||||
"lbl_conn_error": "Verbindungsfehler:",
|
||||
"slot_edit_title": "Slot bearbeiten",
|
||||
"slot_edit_color": "Farbe",
|
||||
"slot_edit_material": "Material",
|
||||
"slot_edit_load": "⬇ Einziehen",
|
||||
"slot_edit_unload": "⬆ Ausziehen",
|
||||
"slot_edit_save": "💾 Speichern",
|
||||
"slot_edit_custom": "z.B. PLA, PETG, ABS…",
|
||||
"slot_edit_ok": "AMS Slot",
|
||||
"slot_edit_profile": "OrcaSlicer-Profil",
|
||||
"slot_edit_profile_hint": "Sendet beim OrcaSlicer-Sync die konkrete Marke statt nur „Generic\"",
|
||||
"slot_edit_profile_default": "— Generic (Default) —",
|
||||
"log_dir_all": "Alle",
|
||||
"log_lvl_label": "Level:",
|
||||
"file_ready_btn": "▶ Druck starten",
|
||||
"file_slots_btn": "🎨 Slots wählen",
|
||||
"file_cancel_btn": "✕ Abbrechen",
|
||||
"nav_printers": "Drucker",
|
||||
"skip_title": "✂ Objekte überspringen",
|
||||
"skip_hint": "Objekte abwählen, die nicht weiter gedruckt werden sollen:",
|
||||
"skip_btn_label": "Objekte",
|
||||
"skip_no_objects": "Keine Objekte in diesem Druck.",
|
||||
"skip_already": "übersprungen",
|
||||
"skip_select_at_least_one": "Bitte mindestens ein Objekt wählen.",
|
||||
"skip_sending": "Sende …",
|
||||
"skip_success": "Objekte werden übersprungen.",
|
||||
"fd_objects_hint": "Objekte überspringen (optional):",
|
||||
"fd_slots_hint": "GCode-Kanal → AMS-Slot zuweisen:",
|
||||
"fd_cancel": "Abbrechen",
|
||||
"fd_print": "▶ Drucken",
|
||||
"fd_no_slots_msg": "Keine belegten AMS-Slots.{br}Druck trotzdem starten?",
|
||||
"fd_slot": "Slot",
|
||||
"fd_no_matching_material": "Kein passendes Material",
|
||||
"fd_used": "BELEGT",
|
||||
"add_printer": "Drucker hinzufügen",
|
||||
"apd_lbl_ip": "Drucker-IP",
|
||||
"apd_lbl_name": "Name (optional)",
|
||||
"apd_placeholder_name": "z.B. Kobra X Wohnzimmer",
|
||||
"apd_cancel": "Abbrechen",
|
||||
"apd_confirm": "Hinzufügen",
|
||||
"apd_fetching": "Hole Daten vom Drucker…",
|
||||
"apd_success": "Drucker hinzugefügt, Bridge startet neu…",
|
||||
"apd_err_ip": "Bitte IP-Adresse eingeben",
|
||||
"printers_remove": "Drucker entfernen",
|
||||
"printers_remove_confirm": "Drucker \"{name}\" entfernen? Die Bridge startet neu.",
|
||||
"printers_active": "● aktiv",
|
||||
"printers_switch": "Wechseln →",
|
||||
"printers_current": "Aktueller Drucker",
|
||||
"printers_loading": "Lade…",
|
||||
"printers_none": "Keine Drucker konfiguriert.",
|
||||
"printers_empty_hint": "Noch kein Drucker eingerichtet.",
|
||||
"nav_browser": "Browser",
|
||||
"panel_browser_title": "Datei-Browser",
|
||||
"store_search_placeholder": "🔍 Suche…",
|
||||
"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": "Bitte bestätige, dass diese Datei für den Anycubic Kobra X erstellt wurde.",
|
||||
"store_web_verify_confirm": "Bestätigen",
|
||||
"store_web_verify_abort": "Abbrechen",
|
||||
"store_no_results": "Keine Dateien gefunden.",
|
||||
"store_never": "noch nicht gedruckt",
|
||||
"store_estimate": "Schätzung",
|
||||
"store_upload_label_prefix": "GCode hierher ziehen oder ",
|
||||
"store_upload_label_browse": "durchsuchen",
|
||||
"store_upload_busy": "⏳ Hochladen…",
|
||||
"store_upload_success": "✓ {file}",
|
||||
"store_upload_error": "✗ {error}",
|
||||
"sf_all": "Alle",
|
||||
"sf_ok": "✓ Erfolgreich",
|
||||
"sf_err": "✗ Fehler",
|
||||
"sf_new": "Neu",
|
||||
"ss_date": "↓ Datum",
|
||||
"ss_name": "A–Z Name",
|
||||
"ss_dur": "⏱ Druckzeit"
|
||||
}
|
||||
234
web/translations/en.json
Normal file
234
web/translations/en.json
Normal file
@@ -0,0 +1,234 @@
|
||||
{
|
||||
"header_status_standby": "Ready",
|
||||
"header_status_printing": "Printing",
|
||||
"header_status_complete": "Complete",
|
||||
"header_status_error": "Error",
|
||||
"kobra_free": "Ready",
|
||||
"kobra_busy": "Busy",
|
||||
"kobra_printing": "Printing",
|
||||
"kobra_preheating": "Preheating",
|
||||
"kobra_auto_leveling": "Auto Leveling",
|
||||
"kobra_checking": "Checking",
|
||||
"kobra_updated": "Updating",
|
||||
"kobra_init": "Initializing",
|
||||
"kobra_pausing": "Pausing...",
|
||||
"kobra_paused": "Paused",
|
||||
"kobra_resuming": "Resuming...",
|
||||
"kobra_resumed": "Resumed",
|
||||
"kobra_stopping": "Stopping...",
|
||||
"kobra_stoped": "Stopped",
|
||||
"kobra_finished": "Finished",
|
||||
"kobra_failed": "Error",
|
||||
"kobra_canceled": "Cancelled",
|
||||
"kobra_offline": "Offline",
|
||||
"nav_dashboard": "Dashboard",
|
||||
"nav_print": "Print",
|
||||
"nav_temps": "Temperatures",
|
||||
"nav_motion": "Motion",
|
||||
"nav_ams": "AMS",
|
||||
"nav_extras": "Light / Fan",
|
||||
"nav_console": "Console",
|
||||
"card_progress": "Progress",
|
||||
"card_temps": "Temperatures",
|
||||
"card_light_fan": "Fan",
|
||||
"card_speed": "Print Speed",
|
||||
"card_cam": "Camera",
|
||||
"lbl_elapsed": "Elapsed:",
|
||||
"lbl_remaining": "Remaining:",
|
||||
"lbl_slicer_time": "Slicer estimate:",
|
||||
"lbl_layers": "Layer",
|
||||
"speed_silent": "🐢 Silent",
|
||||
"speed_normal": "⚡ Normal",
|
||||
"speed_sport": "🚀 Sport",
|
||||
"lbl_light": "💡 Light",
|
||||
"lbl_feed": "Load",
|
||||
"lbl_unload": "Unload",
|
||||
"card_ace_dry": "ACE Drying",
|
||||
"ace_dry_dryer": "Dryer",
|
||||
"ace_dry_status_off": "Status: Off",
|
||||
"ace_dry_status_on": "Status: Active",
|
||||
"ace_dry_status_remaining": "Remaining",
|
||||
"ace_dry_humidity": "Humidity",
|
||||
"ace_dry_current_temp": "Temperature",
|
||||
"ace_dry_chart": "History (Temp/Humidity)",
|
||||
"ace_dry_temp": "Temperature (°C)",
|
||||
"ace_dry_duration": "Duration (min)",
|
||||
"ace_dry_start": "▶ Start",
|
||||
"ace_dry_stop": "■ Stop",
|
||||
"ace_dry_auto_refill": "Auto Refill",
|
||||
"ace_dry_enable": "Enable Drying",
|
||||
"ace_dry_temp_line": "Drying Temperature",
|
||||
"ace_dry_time_line": "Drying Time",
|
||||
"ace_dry_ui_pending": "(UI only, backend next)",
|
||||
"ace_dry_dialog_title": "Dryer Temp/Time Settings",
|
||||
"ace_dry_dialog_temp": "Temperature (30-80°C)",
|
||||
"ace_dry_dialog_time": "Rem. Time (h:m:s)",
|
||||
"ace_dry_dialog_confirm": "Confirm",
|
||||
"ace_dry_dialog_cancel": "Cancel",
|
||||
"ace_dry_dialog_save_restart": "Save & Restart",
|
||||
"ace_dry_dialog_custom_name": "Custom Name",
|
||||
"ace_dry_dialog_reset_default": "Reset to Default",
|
||||
"cam_placeholder": "📷 Camera not started",
|
||||
"cam_stream_unavailable": "Stream unavailable",
|
||||
"btn_cam_start": "▶ Camera",
|
||||
"btn_cam_stop": "◼ Camera",
|
||||
"btn_pause": "⏸ Pause",
|
||||
"btn_resume": "▶ Resume",
|
||||
"btn_cancel": "✕ Stop",
|
||||
"label_nozzle": "Nozzle",
|
||||
"label_bed": "Bed",
|
||||
"label_fan": "🌀 Fan",
|
||||
"label_light": "💡 Light",
|
||||
"label_on_off": "On / Off",
|
||||
"label_speed": "Speed",
|
||||
"panel_print_title": "Print Control",
|
||||
"panel_print_btn_pause": "⏸ Pause",
|
||||
"panel_print_btn_resume": "▶ Resume",
|
||||
"panel_print_btn_cancel": "✕ Cancel",
|
||||
"panel_print_temps_live": "Temperatures (Live)",
|
||||
"label_set": "Set",
|
||||
"label_off": "Off",
|
||||
"panel_temps_nozzle": "Nozzle",
|
||||
"panel_temps_bed": "Heated Bed",
|
||||
"panel_temps_chart": "History (last 60 readings)",
|
||||
"label_target_c": "Target:",
|
||||
"panel_motion_xy": "XY Axes",
|
||||
"panel_motion_z": "Z Axis",
|
||||
"label_step": "Step size:",
|
||||
"btn_home_z": "Home Z",
|
||||
"btn_home_xy": "Home XY",
|
||||
"btn_home_all": "Home All",
|
||||
"btn_disable_motors": "Motors Off",
|
||||
"panel_ams_title": "Filament",
|
||||
"card_ams": "Filament",
|
||||
"ams_no_data": "No AMS data received",
|
||||
"label_slot": "Slot",
|
||||
"ams_empty": "Empty",
|
||||
"panel_extras_light": "Light",
|
||||
"panel_extras_fan": "Fan",
|
||||
"panel_extras_camera": "Camera",
|
||||
"btn_cam_start2": "▶ Start",
|
||||
"btn_cam_stop2": "◼ Stop",
|
||||
"panel_console_title": "Event Log",
|
||||
"log_light_on": "Light on",
|
||||
"log_light_off": "Light off",
|
||||
"log_fan": "Fan →",
|
||||
"log_nozzle": "Nozzle →",
|
||||
"log_bed": "Bed →",
|
||||
"log_axis": "Axis",
|
||||
"log_home": "Home",
|
||||
"log_home_all": "Home All",
|
||||
"log_cam_start": "Camera started:",
|
||||
"log_cam_stop": "Camera stopped",
|
||||
"log_poll_error": "Poll error:",
|
||||
"log_error": "Error:",
|
||||
"confirm_cancel": "Really cancel the print?",
|
||||
"settings_title": "Settings",
|
||||
"settings_connection": "Connection",
|
||||
"settings_print": "Print Settings",
|
||||
"settings_poll": "Poll Interval",
|
||||
"settings_version": "Version",
|
||||
"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",
|
||||
"lbl_conn_error": "Connection error:",
|
||||
"slot_edit_title": "Edit Slot",
|
||||
"slot_edit_color": "Color",
|
||||
"slot_edit_material": "Material",
|
||||
"slot_edit_load": "⬇ Load",
|
||||
"slot_edit_unload": "⬆ Unload",
|
||||
"slot_edit_save": "💾 Save",
|
||||
"slot_edit_custom": "e.g. PLA, PETG, ABS…",
|
||||
"slot_edit_ok": "AMS Slot",
|
||||
"slot_edit_profile": "OrcaSlicer profile",
|
||||
"slot_edit_profile_hint": "Sent on OrcaSlicer sync as the specific brand instead of just \"Generic\"",
|
||||
"slot_edit_profile_default": "— Generic (default) —",
|
||||
"log_dir_all": "All",
|
||||
"log_lvl_label": "Level:",
|
||||
"file_ready_btn": "▶ Start Print",
|
||||
"file_slots_btn": "🎨 Select Slots",
|
||||
"file_cancel_btn": "✕ Cancel",
|
||||
"nav_printers": "Printers",
|
||||
"skip_title": "✂ Skip objects",
|
||||
"skip_hint": "Uncheck objects you no longer want to print:",
|
||||
"skip_btn_label": "Objects",
|
||||
"skip_no_objects": "No objects in this print.",
|
||||
"skip_already": "skipped",
|
||||
"skip_select_at_least_one": "Please pick at least one object.",
|
||||
"skip_sending": "Sending …",
|
||||
"skip_success": "Objects will be skipped.",
|
||||
"fd_objects_hint": "Skip objects (optional):",
|
||||
"fd_slots_hint": "Assign GCode channel to AMS slot:",
|
||||
"fd_cancel": "Cancel",
|
||||
"fd_print": "▶ Print",
|
||||
"fd_no_slots_msg": "No loaded AMS slots.{br}Start print anyway?",
|
||||
"fd_slot": "Slot",
|
||||
"fd_no_matching_material": "No matching material",
|
||||
"fd_used": "USED",
|
||||
"add_printer": "Add printer",
|
||||
"apd_lbl_ip": "Printer IP",
|
||||
"apd_lbl_name": "Name (optional)",
|
||||
"apd_placeholder_name": "e.g. Kobra X Living Room",
|
||||
"apd_cancel": "Cancel",
|
||||
"apd_confirm": "Add",
|
||||
"apd_fetching": "Fetching data from printer…",
|
||||
"apd_success": "Printer added, bridge restarting…",
|
||||
"apd_err_ip": "Please enter an IP address",
|
||||
"printers_remove": "Remove printer",
|
||||
"printers_remove_confirm": "Remove printer \"{name}\"? The bridge will restart.",
|
||||
"printers_active": "● active",
|
||||
"printers_switch": "Switch →",
|
||||
"printers_current": "Current printer",
|
||||
"printers_loading": "Loading…",
|
||||
"printers_none": "No printers configured.",
|
||||
"printers_empty_hint": "No printer set up yet.",
|
||||
"nav_browser": "Browser",
|
||||
"panel_browser_title": "File Browser",
|
||||
"store_search_placeholder": "🔍 Search…",
|
||||
"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",
|
||||
"store_estimate": "Estimate",
|
||||
"store_upload_label_prefix": "Drag GCode here or ",
|
||||
"store_upload_label_browse": "browse",
|
||||
"store_upload_busy": "⏳ Uploading…",
|
||||
"store_upload_success": "✓ {file}",
|
||||
"store_upload_error": "✗ {error}",
|
||||
"sf_all": "All",
|
||||
"sf_ok": "✓ Completed",
|
||||
"sf_err": "✗ Failed",
|
||||
"sf_new": "New",
|
||||
"ss_date": "↓ Date",
|
||||
"ss_name": "A–Z Name",
|
||||
"ss_dur": "⏱ Print time"
|
||||
}
|
||||
234
web/translations/es.json
Normal file
234
web/translations/es.json
Normal file
@@ -0,0 +1,234 @@
|
||||
{
|
||||
"header_status_standby": "Listo",
|
||||
"header_status_printing": "Imprimiendo",
|
||||
"header_status_complete": "Completado",
|
||||
"header_status_error": "Error",
|
||||
"kobra_free": "Listo",
|
||||
"kobra_busy": "Ocupado",
|
||||
"kobra_printing": "Imprimiendo",
|
||||
"kobra_preheating": "Precalentando",
|
||||
"kobra_auto_leveling": "Autonivelado",
|
||||
"kobra_checking": "Comprobando",
|
||||
"kobra_updated": "Actualizando",
|
||||
"kobra_init": "Inicializando",
|
||||
"kobra_pausing": "Pausando...",
|
||||
"kobra_paused": "Pausado",
|
||||
"kobra_resuming": "Reanudando...",
|
||||
"kobra_resumed": "Reanudado",
|
||||
"kobra_stopping": "Deteniendo...",
|
||||
"kobra_stoped": "Detenido",
|
||||
"kobra_finished": "Finalizado",
|
||||
"kobra_failed": "Error",
|
||||
"kobra_canceled": "Cancelado",
|
||||
"kobra_offline": "Desconectada",
|
||||
"nav_dashboard": "Panel",
|
||||
"nav_print": "Impresión",
|
||||
"nav_temps": "Temperaturas",
|
||||
"nav_motion": "Movimiento",
|
||||
"nav_ams": "AMS",
|
||||
"nav_extras": "Luz / Ventilador",
|
||||
"nav_console": "Consola",
|
||||
"card_progress": "Progreso",
|
||||
"card_temps": "Temperaturas",
|
||||
"card_light_fan": "Ventilador",
|
||||
"card_speed": "Velocidad de impresión",
|
||||
"card_cam": "Cámara",
|
||||
"lbl_elapsed": "Transcurrido:",
|
||||
"lbl_remaining": "Restante:",
|
||||
"lbl_slicer_time": "Estimación del slicer:",
|
||||
"lbl_layers": "Capa",
|
||||
"speed_silent": "🐢 Silencioso",
|
||||
"speed_normal": "⚡ Normal",
|
||||
"speed_sport": "🚀 Sport",
|
||||
"lbl_light": "💡 Luz",
|
||||
"lbl_feed": "Cargar",
|
||||
"lbl_unload": "Descargar",
|
||||
"card_ace_dry": "Secado ACE",
|
||||
"ace_dry_dryer": "Secador",
|
||||
"ace_dry_status_off": "Estado: Apagado",
|
||||
"ace_dry_status_on": "Estado: Activo",
|
||||
"ace_dry_status_remaining": "Restante",
|
||||
"ace_dry_humidity": "Humedad",
|
||||
"ace_dry_current_temp": "Temperatura",
|
||||
"ace_dry_chart": "Historial (Temp/Humedad)",
|
||||
"ace_dry_temp": "Temperatura (°C)",
|
||||
"ace_dry_duration": "Duración (min)",
|
||||
"ace_dry_start": "▶ Iniciar",
|
||||
"ace_dry_stop": "■ Parar",
|
||||
"ace_dry_auto_refill": "Relleno automático",
|
||||
"ace_dry_enable": "Activar secado",
|
||||
"ace_dry_temp_line": "Temperatura de secado",
|
||||
"ace_dry_time_line": "Tiempo de secado",
|
||||
"ace_dry_ui_pending": "(solo UI, backend después)",
|
||||
"ace_dry_dialog_title": "Ajustes de temp/tiempo del secador",
|
||||
"ace_dry_dialog_temp": "Temperatura (30-80°C)",
|
||||
"ace_dry_dialog_time": "Tiempo restante (h:m:s)",
|
||||
"ace_dry_dialog_confirm": "Confirmar",
|
||||
"ace_dry_dialog_cancel": "Cancelar",
|
||||
"ace_dry_dialog_save_restart": "Guardar y reiniciar",
|
||||
"ace_dry_dialog_custom_name": "Nombre personalizado",
|
||||
"ace_dry_dialog_reset_default": "Restablecer valores predeterminados",
|
||||
"cam_placeholder": "📷 Cámara no iniciada",
|
||||
"cam_stream_unavailable": "Stream no disponible",
|
||||
"btn_cam_start": "▶ Cámara",
|
||||
"btn_cam_stop": "◼ Cámara",
|
||||
"btn_pause": "⏸ Pausa",
|
||||
"btn_resume": "▶ Reanudar",
|
||||
"btn_cancel": "✕ Detener",
|
||||
"label_nozzle": "Boquilla",
|
||||
"label_bed": "Cama",
|
||||
"label_fan": "🌀 Ventilador",
|
||||
"label_light": "💡 Luz",
|
||||
"label_on_off": "Encendido / Apagado",
|
||||
"label_speed": "Velocidad",
|
||||
"panel_print_title": "Control de impresión",
|
||||
"panel_print_btn_pause": "⏸ Pausa",
|
||||
"panel_print_btn_resume": "▶ Reanudar",
|
||||
"panel_print_btn_cancel": "✕ Cancelar",
|
||||
"panel_print_temps_live": "Temperaturas (en vivo)",
|
||||
"label_set": "Set",
|
||||
"label_off": "Apagado",
|
||||
"panel_temps_nozzle": "Boquilla",
|
||||
"panel_temps_bed": "Cama caliente",
|
||||
"panel_temps_chart": "Historial (últimas 60 lecturas)",
|
||||
"label_target_c": "Objetivo:",
|
||||
"panel_motion_xy": "Ejes XY",
|
||||
"panel_motion_z": "Eje Z",
|
||||
"label_step": "Tamaño del paso:",
|
||||
"btn_home_z": "Home Z",
|
||||
"btn_home_xy": "Home XY",
|
||||
"btn_home_all": "Home All",
|
||||
"btn_disable_motors": "Motores apagados",
|
||||
"panel_ams_title": "Filamento",
|
||||
"card_ams": "Filamento",
|
||||
"ams_no_data": "No se recibieron datos de AMS",
|
||||
"label_slot": "Ranura",
|
||||
"ams_empty": "Vacío",
|
||||
"panel_extras_light": "Luz",
|
||||
"panel_extras_fan": "Ventilador",
|
||||
"panel_extras_camera": "Cámara",
|
||||
"btn_cam_start2": "▶ Iniciar",
|
||||
"btn_cam_stop2": "◼ Detener",
|
||||
"panel_console_title": "Registro de eventos",
|
||||
"log_light_on": "Luz encendida",
|
||||
"log_light_off": "Luz apagada",
|
||||
"log_fan": "Ventilador →",
|
||||
"log_nozzle": "Boquilla →",
|
||||
"log_bed": "Cama →",
|
||||
"log_axis": "Eje",
|
||||
"log_home": "Home",
|
||||
"log_home_all": "Home All",
|
||||
"log_cam_start": "Cámara iniciada:",
|
||||
"log_cam_stop": "Cámara detenida",
|
||||
"log_poll_error": "Error de sondeo:",
|
||||
"log_error": "Error:",
|
||||
"confirm_cancel": "¿Realmente cancelar la impresión?",
|
||||
"settings_title": "Configuración",
|
||||
"settings_connection": "Conexión",
|
||||
"settings_print": "Ajustes de impresión",
|
||||
"settings_poll": "Intervalo de sondeo",
|
||||
"settings_version": "Versión",
|
||||
"settings_save": "Guardar y reiniciar",
|
||||
"settings_printer_name": "Nombre de impresora",
|
||||
"settings_printer_ip": "IP de impresora",
|
||||
"settings_mqtt_port": "MQTT Port",
|
||||
"settings_username": "Usuario MQTT",
|
||||
"settings_password": "Contraseña MQTT",
|
||||
"settings_device_id": "ID del dispositivo",
|
||||
"settings_mode_id": "ID de modo",
|
||||
"hint_ip_no_port": "Solo dirección IP, sin puerto (p. ej. 192.168.1.102)",
|
||||
"settings_default_slot": "Ranura predeterminada (un color)",
|
||||
"settings_slot_auto": "Auto (todos los slots cargados)",
|
||||
"settings_auto_leveling": "Autonivelado antes de imprimir",
|
||||
"settings_camera_on_print": "Encender cámara al iniciar impresión",
|
||||
"settings_web_upload_warning": "Mostrar advertencia al imprimir subidas web",
|
||||
"update_check": "Buscar actualizaciones",
|
||||
"update_checking": "Comprobando...",
|
||||
"update_available": "disponible",
|
||||
"update_none": "Ya actualizado",
|
||||
"update_apply": "Instalar ahora",
|
||||
"update_applying": "Descargando...",
|
||||
"update_restarting": "Reiniciando...",
|
||||
"update_error": "Error",
|
||||
"btn_connect": "⚡ Conectar",
|
||||
"btn_disconnect": "✕ Desconectar",
|
||||
"lbl_conn_error": "Error de conexión:",
|
||||
"slot_edit_title": "Editar slot",
|
||||
"slot_edit_color": "Color",
|
||||
"slot_edit_material": "Material",
|
||||
"slot_edit_load": "⬇ Cargar",
|
||||
"slot_edit_unload": "⬆ Descargar",
|
||||
"slot_edit_save": "💾 Guardar",
|
||||
"slot_edit_custom": "p. ej. PLA, PETG, ABS…",
|
||||
"slot_edit_ok": "Ranura AMS",
|
||||
"slot_edit_profile": "Perfil de OrcaSlicer",
|
||||
"slot_edit_profile_hint": "Envía al sincronizar con OrcaSlicer la marca concreta en lugar de solo \"Generic\"",
|
||||
"slot_edit_profile_default": "— Genérico (Predeterminado) —",
|
||||
"log_dir_all": "Todos",
|
||||
"log_lvl_label": "Nivel:",
|
||||
"file_ready_btn": "▶ Iniciar impresión",
|
||||
"file_slots_btn": "🎨 Seleccionar ranuras",
|
||||
"file_cancel_btn": "✕ Cancelar",
|
||||
"nav_printers": "Impresoras",
|
||||
"skip_title": "✂ Omitir objetos",
|
||||
"skip_hint": "Deselecciona los objetos que ya no quieras imprimir:",
|
||||
"skip_btn_label": "Objetos",
|
||||
"skip_no_objects": "No hay objetos en esta impresión.",
|
||||
"skip_already": "omitido",
|
||||
"skip_select_at_least_one": "Selecciona al menos un objeto.",
|
||||
"skip_sending": "Enviando …",
|
||||
"skip_success": "Se omitirán los objetos.",
|
||||
"fd_objects_hint": "Omitir objetos (opcional):",
|
||||
"fd_slots_hint": "Asignar canal GCode a la ranura AMS:",
|
||||
"fd_cancel": "Cancelar",
|
||||
"fd_print": "▶ Imprimir",
|
||||
"fd_no_slots_msg": "No hay slots AMS cargados.{br}¿Iniciar impresión de todos modos?",
|
||||
"fd_slot": "Ranura",
|
||||
"fd_no_matching_material": "No hay material compatible",
|
||||
"fd_used": "USADO",
|
||||
"add_printer": "Añadir impresora",
|
||||
"apd_lbl_ip": "IP de impresora",
|
||||
"apd_lbl_name": "Nombre (opcional)",
|
||||
"apd_placeholder_name": "p. ej. Kobra X Sala",
|
||||
"apd_cancel": "Cancelar",
|
||||
"apd_confirm": "Añadir",
|
||||
"apd_fetching": "Obteniendo datos de la impresora…",
|
||||
"apd_success": "Impresora añadida, reiniciando bridge…",
|
||||
"apd_err_ip": "Introduce una dirección IP",
|
||||
"printers_remove": "Eliminar impresora",
|
||||
"printers_remove_confirm": "¿Eliminar impresora \"{name}\"? El bridge se reiniciará.",
|
||||
"printers_active": "● activa",
|
||||
"printers_switch": "Cambiar →",
|
||||
"printers_current": "Impresora actual",
|
||||
"printers_loading": "Cargando…",
|
||||
"printers_none": "No hay impresoras configuradas.",
|
||||
"printers_empty_hint": "Aún no hay impresora configurada.",
|
||||
"nav_browser": "Explorador",
|
||||
"panel_browser_title": "Explorador de archivos",
|
||||
"store_search_placeholder": "🔍 Buscar…",
|
||||
"store_empty": "Aún no hay archivos subidos.",
|
||||
"store_refresh": "↻ Actualizar",
|
||||
"store_print": "▶ Imprimir",
|
||||
"store_download": "⬇ Descargar",
|
||||
"store_delete_confirm": "¿Eliminar archivo?",
|
||||
"store_print_confirm": "¿Imprimir archivo?",
|
||||
"store_web_verify_title": "Verificar archivo",
|
||||
"store_web_verify_msg": "Verifica que este archivo fue creado para Anycubic Kobra X.",
|
||||
"store_web_verify_confirm": "Confirmar",
|
||||
"store_web_verify_abort": "Abortar",
|
||||
"store_no_results": "No se encontraron archivos.",
|
||||
"store_never": "nunca impreso",
|
||||
"store_estimate": "Estimación",
|
||||
"store_upload_label_prefix": "Arrastra el GCode aquí o ",
|
||||
"store_upload_label_browse": "buscar",
|
||||
"store_upload_busy": "⏳ Subiendo…",
|
||||
"store_upload_success": "✓ {file}",
|
||||
"store_upload_error": "✗ {error}",
|
||||
"sf_all": "Todos",
|
||||
"sf_ok": "✓ Completado",
|
||||
"sf_err": "✗ Fallido",
|
||||
"sf_new": "Nuevo",
|
||||
"ss_date": "↓ Fecha",
|
||||
"ss_name": "A–Z Nombre",
|
||||
"ss_dur": "⏱ Tiempo de impresión"
|
||||
}
|
||||
234
web/translations/zh-cn.json
Normal file
234
web/translations/zh-cn.json
Normal file
@@ -0,0 +1,234 @@
|
||||
{
|
||||
"header_status_standby": "就绪",
|
||||
"header_status_printing": "打印中",
|
||||
"header_status_complete": "完成",
|
||||
"header_status_error": "错误",
|
||||
"kobra_free": "就绪",
|
||||
"kobra_busy": "忙碌",
|
||||
"kobra_printing": "打印中",
|
||||
"kobra_preheating": "预热中",
|
||||
"kobra_auto_leveling": "自动调平",
|
||||
"kobra_checking": "检查中",
|
||||
"kobra_updated": "更新中",
|
||||
"kobra_init": "初始化中",
|
||||
"kobra_pausing": "暂停中...",
|
||||
"kobra_paused": "已暂停",
|
||||
"kobra_resuming": "恢复中...",
|
||||
"kobra_resumed": "已恢复",
|
||||
"kobra_stopping": "停止中...",
|
||||
"kobra_stoped": "已停止",
|
||||
"kobra_finished": "已完成",
|
||||
"kobra_failed": "错误",
|
||||
"kobra_canceled": "已取消",
|
||||
"kobra_offline": "离线",
|
||||
"nav_dashboard": "仪表盘",
|
||||
"nav_print": "打印",
|
||||
"nav_temps": "温度",
|
||||
"nav_motion": "运动",
|
||||
"nav_ams": "AMS",
|
||||
"nav_extras": "灯光 / 风扇",
|
||||
"nav_console": "控制台",
|
||||
"card_progress": "进度",
|
||||
"card_temps": "温度",
|
||||
"card_light_fan": "风扇",
|
||||
"card_speed": "打印速度",
|
||||
"card_cam": "相机",
|
||||
"lbl_elapsed": "已用时间:",
|
||||
"lbl_remaining": "剩余时间:",
|
||||
"lbl_slicer_time": "切片预估:",
|
||||
"lbl_layers": "层",
|
||||
"speed_silent": "🐢 静音",
|
||||
"speed_normal": "⚡ 标准",
|
||||
"speed_sport": "🚀 运动",
|
||||
"lbl_light": "💡 灯光",
|
||||
"lbl_feed": "进料",
|
||||
"lbl_unload": "退料",
|
||||
"card_ace_dry": "ACE 烘干",
|
||||
"ace_dry_dryer": "烘干机",
|
||||
"ace_dry_status_off": "状态: 关闭",
|
||||
"ace_dry_status_on": "状态: 运行中",
|
||||
"ace_dry_status_remaining": "剩余",
|
||||
"ace_dry_humidity": "湿度",
|
||||
"ace_dry_current_temp": "温度",
|
||||
"ace_dry_chart": "历史 (温度/湿度)",
|
||||
"ace_dry_temp": "温度 (°C)",
|
||||
"ace_dry_duration": "时长 (分钟)",
|
||||
"ace_dry_start": "▶ 启动",
|
||||
"ace_dry_stop": "■ 停止",
|
||||
"ace_dry_auto_refill": "自动补料",
|
||||
"ace_dry_enable": "启用烘干",
|
||||
"ace_dry_temp_line": "烘干温度",
|
||||
"ace_dry_time_line": "烘干时间",
|
||||
"ace_dry_ui_pending": "(仅 UI,后端稍后支持)",
|
||||
"ace_dry_dialog_title": "烘干温度/时间设置",
|
||||
"ace_dry_dialog_temp": "温度 (30-80°C)",
|
||||
"ace_dry_dialog_time": "剩余时间 (h:m:s)",
|
||||
"ace_dry_dialog_confirm": "确认",
|
||||
"ace_dry_dialog_cancel": "取消",
|
||||
"ace_dry_dialog_save_restart": "保存并重启",
|
||||
"ace_dry_dialog_custom_name": "自定义名称",
|
||||
"ace_dry_dialog_reset_default": "恢复默认",
|
||||
"cam_placeholder": "📷 相机未启动",
|
||||
"cam_stream_unavailable": "视频流不可用",
|
||||
"btn_cam_start": "▶ 相机",
|
||||
"btn_cam_stop": "◼ 相机",
|
||||
"btn_pause": "⏸ 暂停",
|
||||
"btn_resume": "▶ 继续",
|
||||
"btn_cancel": "✕ 停止",
|
||||
"label_nozzle": "喷嘴",
|
||||
"label_bed": "热床",
|
||||
"label_fan": "🌀 风扇",
|
||||
"label_light": "💡 灯光",
|
||||
"label_on_off": "开 / 关",
|
||||
"label_speed": "速度",
|
||||
"panel_print_title": "打印控制",
|
||||
"panel_print_btn_pause": "⏸ 暂停",
|
||||
"panel_print_btn_resume": "▶ 继续",
|
||||
"panel_print_btn_cancel": "✕ 取消",
|
||||
"panel_print_temps_live": "温度 (实时)",
|
||||
"label_set": "设置",
|
||||
"label_off": "关闭",
|
||||
"panel_temps_nozzle": "喷嘴",
|
||||
"panel_temps_bed": "热床",
|
||||
"panel_temps_chart": "历史 (最近 60 次读数)",
|
||||
"label_target_c": "目标:",
|
||||
"panel_motion_xy": "XY 轴",
|
||||
"panel_motion_z": "Z 轴",
|
||||
"label_step": "步进:",
|
||||
"btn_home_z": "回零 Z",
|
||||
"btn_home_xy": "回零 XY",
|
||||
"btn_home_all": "全部回零",
|
||||
"btn_disable_motors": "关闭电机",
|
||||
"panel_ams_title": "耗材",
|
||||
"card_ams": "耗材",
|
||||
"ams_no_data": "未收到 AMS 数据",
|
||||
"label_slot": "槽位",
|
||||
"ams_empty": "空",
|
||||
"panel_extras_light": "灯光",
|
||||
"panel_extras_fan": "风扇",
|
||||
"panel_extras_camera": "相机",
|
||||
"btn_cam_start2": "▶ 启动",
|
||||
"btn_cam_stop2": "◼ 停止",
|
||||
"panel_console_title": "事件日志",
|
||||
"log_light_on": "灯光已开",
|
||||
"log_light_off": "灯光已关",
|
||||
"log_fan": "风扇 →",
|
||||
"log_nozzle": "喷嘴 →",
|
||||
"log_bed": "热床 →",
|
||||
"log_axis": "轴",
|
||||
"log_home": "回零",
|
||||
"log_home_all": "全部回零",
|
||||
"log_cam_start": "相机已启动:",
|
||||
"log_cam_stop": "相机已停止",
|
||||
"log_poll_error": "轮询错误:",
|
||||
"log_error": "错误:",
|
||||
"confirm_cancel": "确定要取消打印吗?",
|
||||
"settings_title": "设置",
|
||||
"settings_connection": "连接",
|
||||
"settings_print": "打印设置",
|
||||
"settings_poll": "轮询间隔",
|
||||
"settings_version": "版本",
|
||||
"settings_save": "保存并重启",
|
||||
"settings_printer_name": "打印机名称",
|
||||
"settings_printer_ip": "打印机 IP",
|
||||
"settings_mqtt_port": "MQTT 端口",
|
||||
"settings_username": "MQTT 用户名",
|
||||
"settings_password": "MQTT 密码",
|
||||
"settings_device_id": "设备 ID",
|
||||
"settings_mode_id": "模式 ID",
|
||||
"hint_ip_no_port": "仅填写 IP,不要端口 (例如 192.168.1.102)",
|
||||
"settings_default_slot": "默认槽位 (单色)",
|
||||
"settings_slot_auto": "自动 (所有已装载槽位)",
|
||||
"settings_auto_leveling": "打印前自动调平",
|
||||
"settings_camera_on_print": "打印开始时开启相机",
|
||||
"settings_web_upload_warning": "打印网页上传文件时显示警告",
|
||||
"update_check": "检查更新",
|
||||
"update_checking": "检查中...",
|
||||
"update_available": "可用",
|
||||
"update_none": "已是最新版本",
|
||||
"update_apply": "立即安装",
|
||||
"update_applying": "下载中...",
|
||||
"update_restarting": "重启中...",
|
||||
"update_error": "错误",
|
||||
"btn_connect": "⚡ 连接",
|
||||
"btn_disconnect": "✕ 断开",
|
||||
"lbl_conn_error": "连接错误:",
|
||||
"slot_edit_title": "编辑槽位",
|
||||
"slot_edit_color": "颜色",
|
||||
"slot_edit_material": "材料",
|
||||
"slot_edit_load": "⬇ 进料",
|
||||
"slot_edit_unload": "⬆ 退料",
|
||||
"slot_edit_save": "💾 保存",
|
||||
"slot_edit_custom": "例如 PLA, PETG, ABS…",
|
||||
"slot_edit_ok": "AMS 槽位",
|
||||
"slot_edit_profile": "OrcaSlicer 配置",
|
||||
"slot_edit_profile_hint": "在 OrcaSlicer 同步时发送具体品牌,而不仅仅是“Generic”",
|
||||
"slot_edit_profile_default": "— 通用 (默认) —",
|
||||
"log_dir_all": "全部",
|
||||
"log_lvl_label": "级别:",
|
||||
"file_ready_btn": "▶ 开始打印",
|
||||
"file_slots_btn": "🎨 选择槽位",
|
||||
"file_cancel_btn": "✕ 取消",
|
||||
"nav_printers": "打印机",
|
||||
"skip_title": "✂ 跳过对象",
|
||||
"skip_hint": "取消勾选不想继续打印的对象:",
|
||||
"skip_btn_label": "对象",
|
||||
"skip_no_objects": "此打印任务没有对象。",
|
||||
"skip_already": "已跳过",
|
||||
"skip_select_at_least_one": "请至少选择一个对象。",
|
||||
"skip_sending": "发送中 …",
|
||||
"skip_success": "对象将被跳过。",
|
||||
"fd_objects_hint": "跳过对象 (可选):",
|
||||
"fd_slots_hint": "将 GCode 通道分配到 AMS 槽位:",
|
||||
"fd_cancel": "取消",
|
||||
"fd_print": "▶ 打印",
|
||||
"fd_no_slots_msg": "没有已装载的 AMS 槽位。{br}仍要开始打印吗?",
|
||||
"fd_slot": "槽位",
|
||||
"fd_no_matching_material": "无匹配材料",
|
||||
"fd_used": "已用",
|
||||
"add_printer": "添加打印机",
|
||||
"apd_lbl_ip": "打印机 IP",
|
||||
"apd_lbl_name": "名称 (可选)",
|
||||
"apd_placeholder_name": "例如 Kobra X 客厅",
|
||||
"apd_cancel": "取消",
|
||||
"apd_confirm": "添加",
|
||||
"apd_fetching": "正在从打印机获取数据…",
|
||||
"apd_success": "打印机已添加,Bridge 正在重启…",
|
||||
"apd_err_ip": "请输入 IP 地址",
|
||||
"printers_remove": "移除打印机",
|
||||
"printers_remove_confirm": "移除打印机 \"{name}\"? Bridge 将重启。",
|
||||
"printers_active": "● 活动",
|
||||
"printers_switch": "切换 →",
|
||||
"printers_current": "当前打印机",
|
||||
"printers_loading": "加载中…",
|
||||
"printers_none": "未配置打印机。",
|
||||
"printers_empty_hint": "尚未设置打印机。",
|
||||
"nav_browser": "浏览器",
|
||||
"panel_browser_title": "文件浏览器",
|
||||
"store_search_placeholder": "🔍 搜索…",
|
||||
"store_empty": "尚未上传文件。",
|
||||
"store_refresh": "↻ 刷新",
|
||||
"store_print": "▶ 打印",
|
||||
"store_download": "⬇ 下载",
|
||||
"store_delete_confirm": "删除文件?",
|
||||
"store_print_confirm": "打印文件?",
|
||||
"store_web_verify_title": "验证文件",
|
||||
"store_web_verify_msg": "请确认此文件是为 Anycubic Kobra X 创建的。",
|
||||
"store_web_verify_confirm": "确认",
|
||||
"store_web_verify_abort": "取消",
|
||||
"store_no_results": "未找到文件。",
|
||||
"store_never": "从未打印",
|
||||
"store_estimate": "估算",
|
||||
"store_upload_label_prefix": "将 GCode 拖到这里或 ",
|
||||
"store_upload_label_browse": "浏览",
|
||||
"store_upload_busy": "⏳ 上传中…",
|
||||
"store_upload_success": "✓ {file}",
|
||||
"store_upload_error": "✗ {error}",
|
||||
"sf_all": "全部",
|
||||
"sf_ok": "✓ 已完成",
|
||||
"sf_err": "✗ 失败",
|
||||
"sf_new": "新",
|
||||
"ss_date": "↓ 日期",
|
||||
"ss_name": "A–Z 名称",
|
||||
"ss_dur": "⏱ 打印时间"
|
||||
}
|
||||
Reference in New Issue
Block a user