Compare commits
20 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| ba209827ce | |||
| d26b37b332 | |||
| 6f269833d2 | |||
| d808cd3ea8 | |||
|
|
ecd444525a | ||
|
|
d4bb79a68f | ||
|
|
cdaf74985c | ||
|
|
8383c59b39 | ||
| 1645de4cad | |||
| 42898c385c | |||
| 6c5dd14dbd | |||
| c2d16270bc | |||
| fd4b9b1254 | |||
| 21cd356757 | |||
| 40a27a47fc | |||
| 7815c66a82 | |||
| 312b4083d2 | |||
| 534ea41816 | |||
| f1bfab969c | |||
| 81729c37a5 |
10
.gitignore
vendored
10
.gitignore
vendored
@@ -7,3 +7,13 @@ dist/
|
|||||||
releases/*/kx-bridge
|
releases/*/kx-bridge
|
||||||
releases/*/extract_credentials
|
releases/*/extract_credentials
|
||||||
releases/*/extract_credentials.exe
|
releases/*/extract_credentials.exe
|
||||||
|
|
||||||
|
!kx-bridge.spec
|
||||||
|
|
||||||
|
# Laufzeit-Daten und Drucker-Credentials — nie committen
|
||||||
|
config/config.ini
|
||||||
|
config/*.ini
|
||||||
|
!config/config.ini.example
|
||||||
|
data/
|
||||||
|
|
||||||
|
!data/orca_filaments.json
|
||||||
|
|||||||
184
CHANGELOG.de.md
184
CHANGELOG.de.md
@@ -1,5 +1,189 @@
|
|||||||
# Changelog
|
# Changelog
|
||||||
|
|
||||||
|
## [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
|
||||||
|
- **Kamera bei Druckstart automatisch einschalten:** neue Einstellung „Kamera bei
|
||||||
|
Druckstart einschalten" — die Bridge startet den Kamera-Stream automatisch, wenn
|
||||||
|
ein Druck beginnt (für OrcaSlicer und die Bridge-UI).
|
||||||
|
|
||||||
|
### Fixes
|
||||||
|
- **Einfarbiger Druck durch leeren AMS-Slot blockiert:** OrcaSlicer schreibt alle
|
||||||
|
konfigurierten Filamente in den GCode-Header, auch wenn das Modell nur eines
|
||||||
|
nutzt — die Bridge meldete dem Drucker dadurch alle Farben als nötig, und ein
|
||||||
|
leerer ungenutzter Slot brach den Druck ab. Die Bridge mappt jetzt nur die im
|
||||||
|
GCode tatsächlich genutzten Filamente.
|
||||||
|
- **Filament-Sync jetzt positionstreu:** Bei einem leeren Slot in der Mitte
|
||||||
|
(z.B. Slot 1 gelb, 2 leer, 3 rot, 4 weiß) zeigte OrcaSlicer die Farben auf den
|
||||||
|
falschen Slots. Behoben — leere Slots behalten ihre Position, und das
|
||||||
|
Sync-Farbformat folgt der Happy-Hare-Konvention (RRGGBB ohne `#`).
|
||||||
|
- **Slicer-Zeit + Thumbnail fehlten nach Browser-Reload** (oder bei Druckstart
|
||||||
|
direkt aus OrcaSlicer): beide werden jetzt aus dem GCode-Store anhand des
|
||||||
|
Dateinamens wiederhergestellt statt aus flüchtigem State.
|
||||||
|
- **Deutsche Übersetzungslücken** im ACE-Trockner-Dialog behoben.
|
||||||
|
|
||||||
|
### Logging
|
||||||
|
- Wiederholte Log-Zeilen werden als Zähler („×N") zusammengefasst statt zu spammen;
|
||||||
|
Status-Poll-Verkehr wird nicht mehr auf INFO geloggt.
|
||||||
|
- Neuer Level-Filter (Alle / Fehler / Warnungen), Toast bei neuen Fehlern, volle
|
||||||
|
Tracebacks im Browser-Log und ein Download-Dateiname mit Zeitstempel.
|
||||||
|
|
||||||
|
## [0.9.15] – 2026-05-21
|
||||||
|
|
||||||
|
### Fixes (Issue #29)
|
||||||
|
- **UI im OrcaSlicer-Device-Tab kaputt:** OrcaSlicers eingebetteter Webview lädt
|
||||||
|
nur das nackte HTML und ignoriert externe `<script>`/`<link>`-Tags — nach der
|
||||||
|
v0.9.14-Theme-Auslagerung funktionierte dort kein Button mehr. Die Bridge
|
||||||
|
bettet CSS + JS jetzt inline in die Seite ein — funktioniert in Browser UND
|
||||||
|
OrcaSlicer-Webview.
|
||||||
|
- **Dropdowns unlesbar (weiß auf weiß) im OrcaSlicer-Webview:** `color-scheme` +
|
||||||
|
explizite `select`/`option`-Farben ergänzt, damit die nativen Dropdowns in
|
||||||
|
Hell- und Dunkel-Theme korrekt dargestellt werden.
|
||||||
|
- **„Select slots"-Button tat direkt nach Upload nichts:** eine fehlende
|
||||||
|
Variablen-Deklaration (`storeFiles`) warf einen `ReferenceError`, wenn vor dem
|
||||||
|
Laden des Browser-Tabs geklickt wurde. Behoben.
|
||||||
|
- **Upload-Banner kam nach abgeschlossenem Druck zurück:** der „file ready"-Status
|
||||||
|
wurde nur bei Stop/Abbruch geleert, nicht bei `finished`. Jetzt auch nach
|
||||||
|
erfolgreichem Druckende geleert.
|
||||||
|
|
||||||
|
## [0.9.14] – 2026-05-21
|
||||||
|
|
||||||
|
### Neu
|
||||||
|
- **Theme-System (Community-Beitrag von @hirnwunde, PR #27):** Die Web-UI liegt
|
||||||
|
jetzt in echten Dateien unter `web/themes/<name>/` (`index.html` + `style.css`
|
||||||
|
+ `app.js`) statt im Python-Quelltext eingebettet. Theme umschalten mit
|
||||||
|
`--ui-theme <name>`. Für Theme-Autoren gibt es eine dokumentierte Hook-Referenz
|
||||||
|
(`web/DOC/THEME-CSS-HOOKS.md`, `THEME-JS-ID-HOOKS.md`). Das Default-Theme
|
||||||
|
enthält die komplette aktuelle UI (ACE2, Objekte überspringen, Filament-Dialog).
|
||||||
|
Für Nutzer keine Änderung — Binaries/Docker-Image liefern das Theme eingebettet.
|
||||||
|
- **Neustart über API (Community-Beitrag von @gangoke, PR #28):** neuer Endpoint
|
||||||
|
`POST /api/restart`, um die Bridge per API neu zu starten — z. B. für einen
|
||||||
|
Neustart-Button in der Home-Assistant-Integration.
|
||||||
|
|
||||||
|
### Intern
|
||||||
|
- Vereinheitlichter PyInstaller-Build (`kx-bridge.spec`) für Linux, Windows und
|
||||||
|
Docker — bindet `web/` (Themes) ins Onefile-Binary ein, zur Laufzeit aus
|
||||||
|
`sys._MEIPASS` gelesen. Theme-Einbettung in Linux-Binary und Windows-EXE verifiziert.
|
||||||
|
- `data/` in `.gitignore` aufgenommen.
|
||||||
|
|
||||||
|
## [0.9.13] – 2026-05-20
|
||||||
|
|
||||||
|
============================================================
|
||||||
|
STOPP — VOR DEM DRÜCKEN VON "UPDATE" LESEN
|
||||||
|
============================================================
|
||||||
|
|
||||||
|
Der "Update"-Button ist in 0.9.11 und 0.9.12 KAPUTT.
|
||||||
|
NICHT benutzen. Stattdessen einmalig manuell updaten —
|
||||||
|
ab 0.9.13 funktioniert er wieder.
|
||||||
|
|
||||||
|
>> WINDOWS-.EXE / LINUX-BINARY-Nutzer — GEFAHR:
|
||||||
|
Update ÜBERSCHREIBT deine kx-bridge.exe / kx-bridge mit
|
||||||
|
einer Textdatei. Das Programm STARTET DANN NICHT MEHR
|
||||||
|
und kann sich nicht selbst reparieren.
|
||||||
|
--> Manuell updaten: die 0.9.13
|
||||||
|
kx-bridge-windows.zip / kx-bridge-linux.zip von der
|
||||||
|
Releases-Seite laden und die alte Datei ersetzen.
|
||||||
|
Deine config/- und data/-Ordner bleiben erhalten.
|
||||||
|
|
||||||
|
>> DOCKER-Nutzer:
|
||||||
|
Update führt zur Crash-Loop des Containers
|
||||||
|
(ModuleNotFoundError: No module named '_web_assets').
|
||||||
|
--> Manuell updaten:
|
||||||
|
docker compose pull (oder docker compose up -d --build)
|
||||||
|
config- + data-Volumes bleiben erhalten.
|
||||||
|
|
||||||
|
Ab 0.9.13 ist der In-App-Updater repariert und wieder sicher.
|
||||||
|
============================================================
|
||||||
|
|
||||||
|
### Fixes
|
||||||
|
- **Self-Update war in 0.9.11 und 0.9.12 kaputt (kritisch):** Der In-App-Updater
|
||||||
|
ersetzte nur `kobrax_moonraker_bridge.py`. Zwei Probleme:
|
||||||
|
- **Binary/EXE-Modus:** Er überschrieb die laufende Programmdatei
|
||||||
|
(`sys.executable`) mit einer Python-Textdatei — übrig blieb ein nicht mehr
|
||||||
|
startbares Programm, das sich nicht selbst reparieren kann (manueller
|
||||||
|
Re-Download nötig).
|
||||||
|
- **Python/Docker-Modus:** Seit 0.9.12 importiert die Hauptdatei das
|
||||||
|
ausgelagerte `_web_assets.py` (gebündeltes Frontend), das der Updater nicht
|
||||||
|
mitlud → `ModuleNotFoundError: No module named '_web_assets'` → Crash-Loop.
|
||||||
|
Der Updater lädt jetzt **alle** Bridge-Module (Hauptdatei + `_web_assets.py` +
|
||||||
|
Client + Loader) erst vollständig herunter, ersetzt sie dann atomar und
|
||||||
|
**verweigert das Self-Update im Binary-Modus** (mit Verweis auf den manuellen
|
||||||
|
Download).
|
||||||
|
|
||||||
|
## [0.9.12] – 2026-05-20
|
||||||
|
|
||||||
|
### Fixes
|
||||||
|
- **Pause-Status** wird jetzt korrekt erkannt: Die Bridge las den Geräte-State
|
||||||
|
statt des verschachtelten Druckauftrags-States, dadurch wurde ein pausierter
|
||||||
|
Druck teils noch als „druckend" angezeigt. Layer/Fortschritt/Restzeit kommen
|
||||||
|
jetzt ebenfalls aus dem Auftrags-Report.
|
||||||
|
|
||||||
|
### Intern
|
||||||
|
- Frontend (HTML/CSS/JS) aus der Python-Datei nach `web/index.html` ausgelagert,
|
||||||
|
zur Build-Zeit wieder eingebettet — besser wartbar, für Nutzer keine Änderung.
|
||||||
|
|
||||||
|
### Doku
|
||||||
|
- Community-**Home-Assistant-Integration** von @gangoke verlinkt.
|
||||||
|
|
||||||
## [0.9.11] – 2026-05-20
|
## [0.9.11] – 2026-05-20
|
||||||
|
|
||||||
### Neu
|
### Neu
|
||||||
|
|||||||
179
CHANGELOG.md
179
CHANGELOG.md
@@ -1,5 +1,184 @@
|
|||||||
# Changelog
|
# Changelog
|
||||||
|
|
||||||
|
## [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
|
||||||
|
- **Auto-start camera on print:** new setting "Turn camera on at print start" —
|
||||||
|
when enabled, the bridge starts the camera stream automatically when a print
|
||||||
|
begins (works for both OrcaSlicer and the Bridge UI).
|
||||||
|
|
||||||
|
### Fixes
|
||||||
|
- **Single-color print blocked by an empty AMS slot:** OrcaSlicer writes all
|
||||||
|
configured filaments into the GCode header even when the model uses only one,
|
||||||
|
so the bridge told the printer it needed every color — and an empty unused slot
|
||||||
|
aborted the print. The bridge now maps only the filaments actually used by the
|
||||||
|
GCode.
|
||||||
|
- **Filament sync now position-accurate:** with an empty slot in the middle
|
||||||
|
(e.g. slot 1 yellow, 2 empty, 3 red, 4 white) OrcaSlicer showed the colors
|
||||||
|
shifted onto the wrong slots. Fixed — empty slots keep their position, and the
|
||||||
|
sync color format follows the Happy Hare convention (RRGGBB without `#`).
|
||||||
|
- **Slicer time + thumbnail missing after a browser reload** (or when a print was
|
||||||
|
started directly from OrcaSlicer): both are now restored from the GCode store
|
||||||
|
by filename instead of relying on volatile state.
|
||||||
|
- **German translation gaps** in the ACE dryer dialog fixed.
|
||||||
|
|
||||||
|
### Logging
|
||||||
|
- Repeated log lines are collapsed into a counter ("×N") instead of spamming the
|
||||||
|
console; status-poll traffic is no longer logged at INFO.
|
||||||
|
- New log level filter (All / Errors / Warnings), a toast on new errors, full
|
||||||
|
tracebacks forwarded to the browser log, and a timestamped download filename.
|
||||||
|
|
||||||
|
## [0.9.15] – 2026-05-21
|
||||||
|
|
||||||
|
### Fixes (Issue #29)
|
||||||
|
- **UI in the OrcaSlicer device tab was broken:** OrcaSlicer's embedded webview
|
||||||
|
only loads the bare HTML and ignores external `<script>`/`<link>` tags, so after
|
||||||
|
the v0.9.14 theme split none of the buttons worked in the device tab. The
|
||||||
|
bridge now inlines CSS + JS into the page — works in both the browser and the
|
||||||
|
OrcaSlicer webview.
|
||||||
|
- **Dropdowns unreadable (white-on-white) in the OrcaSlicer webview:** added
|
||||||
|
`color-scheme` + explicit `select`/`option` colors so the native dropdowns
|
||||||
|
render correctly in dark and light theme.
|
||||||
|
- **"Select slots" button did nothing right after an upload:** a missing variable
|
||||||
|
declaration (`storeFiles`) threw a `ReferenceError` when clicked before the
|
||||||
|
Browser tab had loaded. Fixed.
|
||||||
|
- **Upload banner came back after a finished print:** the "file ready" state was
|
||||||
|
only cleared on stop/cancel, not on `finished`. Now cleared on completion too.
|
||||||
|
|
||||||
|
## [0.9.14] – 2026-05-21
|
||||||
|
|
||||||
|
### New
|
||||||
|
- **Theme system (community contribution by @hirnwunde, PR #27):** the web UI now
|
||||||
|
lives in real files under `web/themes/<name>/` (`index.html` + `style.css` +
|
||||||
|
`app.js`) instead of being embedded in the Python source. Switch themes with
|
||||||
|
`--ui-theme <name>`. Theme authors get a documented hook reference
|
||||||
|
(`web/DOC/THEME-CSS-HOOKS.md`, `THEME-JS-ID-HOOKS.md`). The default theme
|
||||||
|
carries the full current UI (ACE2, skip objects, filament dialog). No change
|
||||||
|
for users — the bundled binaries/Docker image ship the theme embedded.
|
||||||
|
- **Restart over API (community contribution by @gangoke, PR #28):** new
|
||||||
|
`POST /api/restart` endpoint to restart the bridge remotely — e.g. a restart
|
||||||
|
button in the Home Assistant integration.
|
||||||
|
|
||||||
|
### Internal
|
||||||
|
- Unified PyInstaller build (`kx-bridge.spec`) for Linux, Windows and Docker —
|
||||||
|
embeds `web/` (themes) into the one-file binary, read at runtime from
|
||||||
|
`sys._MEIPASS`. Verified the theme ships in the Linux binary and the Windows EXE.
|
||||||
|
- `data/` added to `.gitignore`.
|
||||||
|
|
||||||
|
## [0.9.13] – 2026-05-20
|
||||||
|
|
||||||
|
============================================================
|
||||||
|
STOP — READ THIS BEFORE PRESSING "UPDATE"
|
||||||
|
============================================================
|
||||||
|
|
||||||
|
The in-app "Update" button is BROKEN in 0.9.11 and 0.9.12.
|
||||||
|
Do NOT use it. Update manually instead (one time), then it
|
||||||
|
works again from 0.9.13 onward.
|
||||||
|
|
||||||
|
>> WINDOWS .EXE / LINUX BINARY users — DANGER:
|
||||||
|
Pressing Update OVERWRITES your kx-bridge.exe / kx-bridge
|
||||||
|
with a text file. The program will NOT start anymore.
|
||||||
|
It cannot repair itself.
|
||||||
|
--> Update manually: download the 0.9.13
|
||||||
|
kx-bridge-windows.zip / kx-bridge-linux.zip from the
|
||||||
|
Releases page and replace your old file.
|
||||||
|
Your config/ and data/ folders are kept.
|
||||||
|
|
||||||
|
>> DOCKER users:
|
||||||
|
Pressing Update makes the container crash-loop
|
||||||
|
(ModuleNotFoundError: No module named '_web_assets').
|
||||||
|
--> Update manually:
|
||||||
|
docker compose pull (or docker compose up -d --build)
|
||||||
|
Your config + data volumes are kept.
|
||||||
|
|
||||||
|
From 0.9.13 on, the in-app updater is fixed and safe again.
|
||||||
|
============================================================
|
||||||
|
|
||||||
|
### Fixes
|
||||||
|
- **Self-update was broken in 0.9.11 and 0.9.12 (critical):** the in-app updater
|
||||||
|
only replaced `kobrax_moonraker_bridge.py`. Two problems:
|
||||||
|
- **Binary/EXE mode:** it overwrote the running executable (`sys.executable`)
|
||||||
|
with a Python text file, leaving an unstartable program that can't recover
|
||||||
|
itself — manual re-download required.
|
||||||
|
- **Python/Docker mode:** since 0.9.12 the main file imports the extracted
|
||||||
|
`_web_assets.py` (bundled frontend), which the updater didn't fetch →
|
||||||
|
`ModuleNotFoundError: No module named '_web_assets'` → crash loop.
|
||||||
|
The updater now downloads **all** bridge modules (main file + `_web_assets.py`
|
||||||
|
+ client + loaders) fully, then swaps them atomically, and **refuses to
|
||||||
|
self-update in binary mode** (pointing you to the manual download instead).
|
||||||
|
|
||||||
|
## [0.9.12] – 2026-05-20
|
||||||
|
|
||||||
|
### Fixes
|
||||||
|
- **Pause state** is now read correctly: the bridge was looking at the device-level
|
||||||
|
state instead of the nested print-job state, so a paused print sometimes still
|
||||||
|
showed as printing. Layer/progress/remaining-time are now also taken from the
|
||||||
|
job report.
|
||||||
|
|
||||||
|
### Internal
|
||||||
|
- Frontend (HTML/CSS/JS) extracted from the Python file into `web/index.html`,
|
||||||
|
bundled back in at build time — easier to maintain, no change for users.
|
||||||
|
|
||||||
|
### Docs
|
||||||
|
- Linked the community **Home Assistant integration** by @gangoke.
|
||||||
|
|
||||||
## [0.9.11] – 2026-05-20
|
## [0.9.11] – 2026-05-20
|
||||||
|
|
||||||
### New
|
### New
|
||||||
|
|||||||
@@ -6,6 +6,10 @@ COPY requirements.txt .
|
|||||||
RUN pip install --no-cache-dir -r requirements.txt
|
RUN pip install --no-cache-dir -r requirements.txt
|
||||||
|
|
||||||
COPY kobrax_moonraker_bridge.py .
|
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 config_loader.py .
|
||||||
COPY env_loader.py .
|
COPY env_loader.py .
|
||||||
COPY kobrax_client.py .
|
COPY kobrax_client.py .
|
||||||
|
|||||||
37
README.de.md
37
README.de.md
@@ -16,6 +16,8 @@ Eine Moonraker-kompatible Bridge, die direkt mit dem Drucker spricht.
|
|||||||
|
|
||||||
[](https://gitea.it-drui.de/viewit/KX-Bridge-Release/releases)
|
[](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)
|
[](https://www.youtube.com/watch?v=1Ql4wfH27fM)
|
||||||
|
|
||||||
<sub>Gefällt dir KX-Bridge? Ein Kaffee auf <a href="https://ko-fi.com/viewitde">Ko-fi</a> hält das Projekt am Leben. ☕</sub>
|
<sub>Gefällt dir KX-Bridge? Ein Kaffee auf <a href="https://ko-fi.com/viewitde">Ko-fi</a> hält das Projekt am Leben. ☕</sub>
|
||||||
@@ -99,6 +101,34 @@ Drucker → Verbindungstyp **Moonraker** → Host: `http://BRIDGE-IP:7125`
|
|||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
|
## 🎨 Empfohlener Slicer
|
||||||
|
|
||||||
|
Für die beste Erfahrung mit der KX-Bridge bieten wir einen **gepatchten
|
||||||
|
OrcaSlicer-Build**, der drei offene SoftFever/OrcaSlicer-PRs bündelt: das
|
||||||
|
Anycubic-Kobra-X-Druckerprofil, einen Multicolor-G-Code-Fix und — am wichtigsten
|
||||||
|
— einen Moonraker/Happy-Hare-Filament-Sync-Fix, der die AMS-Slot-Positionen auch
|
||||||
|
bei einem leeren Slot in der Mitte korrekt beibehält.
|
||||||
|
|
||||||
|
→ **[OrcaSlicer-KX Releases](https://gitea.it-drui.de/viewit/OrcaSlicer-KX/releases/latest)** (Linux AppImage + Windows ZIP)
|
||||||
|
|
||||||
|
Standard-OrcaSlicer funktioniert auch; der gepatchte Build verbessert
|
||||||
|
hauptsächlich das AMS-Handling. Es ist ein Build von
|
||||||
|
[OrcaSlicer](https://github.com/SoftFever/OrcaSlicer) (AGPL-3.0); der Quellcode
|
||||||
|
ist über die verlinkten PRs verfügbar.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 🏠 Community & Integrationen
|
||||||
|
|
||||||
|
- **[Home-Assistant-Integration](https://github.com/gangoke/kobrax-lan-hass-component)**
|
||||||
|
von [@gangoke](https://github.com/gangoke) — bindet Sensoren, Drucksteuerung,
|
||||||
|
Licht, Kamera und das GCode-Vorschaubild als native Home-Assistant-Entitäten ein.
|
||||||
|
|
||||||
|
> Dies sind **Community-Projekte**, die nicht von KX-Bridge betreut oder
|
||||||
|
> supportet werden. Bei Fragen oder Problemen bitte das verlinkte Repository nutzen.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
## 🔧 Zugangsdaten manuell ermitteln
|
## 🔧 Zugangsdaten manuell ermitteln
|
||||||
|
|
||||||
Normalerweise nicht nötig — *„+ Drucker hinzufügen"* macht das automatisch. Falls doch:
|
Normalerweise nicht nötig — *„+ Drucker hinzufügen"* macht das automatisch. Falls doch:
|
||||||
@@ -119,9 +149,10 @@ dann `extract_credentials` ausführen → gibt Username, Passwort, Device-ID und
|
|||||||
## ⚙️ Nützliche Befehle
|
## ⚙️ Nützliche Befehle
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
docker compose logs -f # Logs anzeigen
|
docker compose logs -f # Logs anzeigen
|
||||||
docker compose down # Bridge stoppen
|
docker compose down # Bridge stoppen
|
||||||
docker compose up -d --build # Bridge neu bauen & starten (nach Update)
|
docker compose pull && docker compose up -d # auf neueste veröffentlichte Version updaten
|
||||||
|
docker compose up -d --build # lokal selber bauen (statt zu pullen)
|
||||||
```
|
```
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|||||||
35
README.md
35
README.md
@@ -16,6 +16,8 @@ A Moonraker-compatible bridge that talks directly to the printer.
|
|||||||
|
|
||||||
[](https://gitea.it-drui.de/viewit/KX-Bridge-Release/releases)
|
[](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)
|
[](https://www.youtube.com/watch?v=1Ql4wfH27fM)
|
||||||
|
|
||||||
<sub>Like KX-Bridge? A coffee on <a href="https://ko-fi.com/viewitde">Ko-fi</a> keeps the project alive. ☕</sub>
|
<sub>Like KX-Bridge? A coffee on <a href="https://ko-fi.com/viewitde">Ko-fi</a> keeps the project alive. ☕</sub>
|
||||||
@@ -99,6 +101,32 @@ Printer → Connection type **Moonraker** → Host: `http://BRIDGE-IP:7125`
|
|||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
|
## 🎨 Recommended Slicer
|
||||||
|
|
||||||
|
For the best KX-Bridge experience we offer a **patched OrcaSlicer build** that
|
||||||
|
bundles three open SoftFever/OrcaSlicer PRs: the Anycubic Kobra X printer
|
||||||
|
profile, a multicolor G-code fix and — most importantly — a Moonraker/Happy-Hare
|
||||||
|
filament-sync fix that keeps AMS slot positions intact even with an empty slot.
|
||||||
|
|
||||||
|
→ **[OrcaSlicer-KX releases](https://gitea.it-drui.de/viewit/OrcaSlicer-KX/releases/latest)** (Linux AppImage + Windows ZIP)
|
||||||
|
|
||||||
|
Standard OrcaSlicer also works; the patched build mainly improves AMS handling.
|
||||||
|
It's a build of [OrcaSlicer](https://github.com/SoftFever/OrcaSlicer) (AGPL-3.0);
|
||||||
|
source is available via the linked PRs.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 🏠 Community & Integrations
|
||||||
|
|
||||||
|
- **[Home Assistant integration](https://github.com/gangoke/kobrax-lan-hass-component)**
|
||||||
|
by [@gangoke](https://github.com/gangoke) — exposes sensors, print controls,
|
||||||
|
light, camera and the GCode thumbnail as native Home Assistant entities.
|
||||||
|
|
||||||
|
> These are **community projects**, not maintained or supported by KX-Bridge.
|
||||||
|
> For questions or issues, please use the linked repository.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
## 🔧 Getting credentials manually
|
## 🔧 Getting credentials manually
|
||||||
|
|
||||||
Normally not needed — *"+ Add printer"* does this automatically. If you do need it:
|
Normally not needed — *"+ Add printer"* does this automatically. If you do need it:
|
||||||
@@ -119,9 +147,10 @@ Alternatively (if the IP is unknown): open AnycubicSlicerNext, connect the print
|
|||||||
## ⚙️ Useful commands
|
## ⚙️ Useful commands
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
docker compose logs -f # show logs
|
docker compose logs -f # show logs
|
||||||
docker compose down # stop the bridge
|
docker compose down # stop the bridge
|
||||||
docker compose up -d --build # rebuild & start (after an update)
|
docker compose pull && docker compose up -d # update to the latest published image
|
||||||
|
docker compose up -d --build # rebuild locally (instead of pulling)
|
||||||
```
|
```
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|||||||
@@ -2,8 +2,10 @@
|
|||||||
# Kopiere diese Datei nach config.ini und trage deine Werte ein:
|
# Kopiere diese Datei nach config.ini und trage deine Werte ein:
|
||||||
# cp config.ini.example config.ini
|
# cp config.ini.example config.ini
|
||||||
#
|
#
|
||||||
# Credentials mit extract_credentials.exe (Windows) oder
|
# Credentials automatisch eintragen:
|
||||||
# extract_credentials (Linux) aus dem laufenden AnycubicSlicerNext auslesen.
|
# 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]
|
[connection]
|
||||||
# IP-Adresse des Druckers im lokalen Netzwerk
|
# IP-Adresse des Druckers im lokalen Netzwerk
|
||||||
@@ -29,6 +31,65 @@ default_ams_slot = auto
|
|||||||
# Auto-Leveling vor jedem Druck (1 = an, 0 = aus)
|
# Auto-Leveling vor jedem Druck (1 = an, 0 = aus)
|
||||||
auto_leveling = 1
|
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]
|
[bridge]
|
||||||
# Poll-Intervall in Sekunden
|
# Poll-Intervall in Sekunden
|
||||||
poll_interval = 3
|
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
|
||||||
|
|||||||
@@ -59,6 +59,8 @@ def _load_config_file(path: pathlib.Path):
|
|||||||
"DEVICE_ID": (CONFIG_SECTION_CONNECTION, "device_id"),
|
"DEVICE_ID": (CONFIG_SECTION_CONNECTION, "device_id"),
|
||||||
"DEFAULT_AMS_SLOT": (CONFIG_SECTION_PRINT, "default_ams_slot"),
|
"DEFAULT_AMS_SLOT": (CONFIG_SECTION_PRINT, "default_ams_slot"),
|
||||||
"AUTO_LEVELING": (CONFIG_SECTION_PRINT, "auto_leveling"),
|
"AUTO_LEVELING": (CONFIG_SECTION_PRINT, "auto_leveling"),
|
||||||
|
"CAMERA_ON_PRINT": (CONFIG_SECTION_PRINT, "camera_on_print"),
|
||||||
|
"WEB_UPLOAD_WARNING": (CONFIG_SECTION_PRINT, "web_upload_warning"),
|
||||||
"BRIDGE_PRINTER_NAME": (CONFIG_SECTION_BRIDGE, "printer_name"),
|
"BRIDGE_PRINTER_NAME": (CONFIG_SECTION_BRIDGE, "printer_name"),
|
||||||
}
|
}
|
||||||
for env_key, (section, option) in mapping.items():
|
for env_key, (section, option) in mapping.items():
|
||||||
@@ -95,6 +97,8 @@ def migrate_env_to_config(env_path: pathlib.Path, config_path: pathlib.Path):
|
|||||||
cfg[CONFIG_SECTION_PRINT] = {
|
cfg[CONFIG_SECTION_PRINT] = {
|
||||||
"default_ams_slot": env_vals.get("DEFAULT_AMS_SLOT", "auto"),
|
"default_ams_slot": env_vals.get("DEFAULT_AMS_SLOT", "auto"),
|
||||||
"auto_leveling": env_vals.get("AUTO_LEVELING", "1"),
|
"auto_leveling": env_vals.get("AUTO_LEVELING", "1"),
|
||||||
|
"camera_on_print": env_vals.get("CAMERA_ON_PRINT", "0"),
|
||||||
|
"web_upload_warning": env_vals.get("WEB_UPLOAD_WARNING", "1"),
|
||||||
}
|
}
|
||||||
cfg[CONFIG_SECTION_BRIDGE] = {
|
cfg[CONFIG_SECTION_BRIDGE] = {
|
||||||
"poll_interval": "3",
|
"poll_interval": "3",
|
||||||
@@ -162,6 +166,77 @@ def list_printers() -> list[dict]:
|
|||||||
return printers
|
return printers
|
||||||
|
|
||||||
|
|
||||||
|
def list_filament_profiles() -> dict[int, dict]:
|
||||||
|
"""Liest die [filament_profiles]-Sektion aus config.ini.
|
||||||
|
|
||||||
|
Format pro AMS-Slot (slot_N_id + slot_N_vendor):
|
||||||
|
[filament_profiles]
|
||||||
|
slot_0_id = OGFL01
|
||||||
|
slot_0_vendor = Polymaker
|
||||||
|
slot_1_id = OGFG23
|
||||||
|
slot_1_vendor = Polymaker
|
||||||
|
|
||||||
|
Gibt einen Dict {slot_index: {"id": ..., "vendor": ...}} zurück.
|
||||||
|
Leere/fehlende Slots werden NICHT aufgenommen — das Default-Mapping
|
||||||
|
(per filament_type) in der Bridge bleibt dann aktiv.
|
||||||
|
"""
|
||||||
|
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
|
||||||
|
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"):
|
||||||
|
continue
|
||||||
|
if not value.strip():
|
||||||
|
continue
|
||||||
|
result.setdefault(slot_idx, {})[field] = value.strip()
|
||||||
|
# Leere Einträge (nur vendor ohne id oder umgekehrt) trotzdem behalten —
|
||||||
|
# der Aufrufer prüft selbst was er nutzt.
|
||||||
|
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"}}
|
||||||
|
"""
|
||||||
|
path = _find_config_file()
|
||||||
|
if not path:
|
||||||
|
return False
|
||||||
|
cfg = configparser.ConfigParser()
|
||||||
|
cfg.read(path, encoding="utf-8")
|
||||||
|
# Sektion neu aufbauen — entfernt damit auch alte/verwaiste Slots
|
||||||
|
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("id"):
|
||||||
|
cfg["filament_profiles"][f"slot_{slot_idx}_id"] = entry["id"]
|
||||||
|
if entry.get("vendor"):
|
||||||
|
cfg["filament_profiles"][f"slot_{slot_idx}_vendor"] = entry["vendor"]
|
||||||
|
with open(path, "w", encoding="utf-8") as f:
|
||||||
|
cfg.write(f)
|
||||||
|
return True
|
||||||
|
|
||||||
|
|
||||||
def get(key: str, default: str = "") -> str:
|
def get(key: str, default: str = "") -> str:
|
||||||
return os.environ.get(key, default)
|
return os.environ.get(key, default)
|
||||||
|
|
||||||
@@ -175,3 +250,5 @@ MODE_ID = get("MODE_ID", "")
|
|||||||
DEVICE_ID = get("DEVICE_ID", "")
|
DEVICE_ID = get("DEVICE_ID", "")
|
||||||
DEFAULT_AMS_SLOT = get("DEFAULT_AMS_SLOT", "auto")
|
DEFAULT_AMS_SLOT = get("DEFAULT_AMS_SLOT", "auto")
|
||||||
AUTO_LEVELING = int(get("AUTO_LEVELING","1"))
|
AUTO_LEVELING = int(get("AUTO_LEVELING","1"))
|
||||||
|
CAMERA_ON_PRINT = int(get("CAMERA_ON_PRINT","0"))
|
||||||
|
WEB_UPLOAD_WARNING = int(get("WEB_UPLOAD_WARNING", "1"))
|
||||||
|
|||||||
7016
data/orca_filaments.json
Normal file
7016
data/orca_filaments.json
Normal file
File diff suppressed because it is too large
Load Diff
@@ -1,7 +1,9 @@
|
|||||||
services:
|
services:
|
||||||
kx-bridge:
|
kx-bridge:
|
||||||
image: kx-bridge:latest
|
image: gitea.it-drui.de/viewit/kx-bridge:latest
|
||||||
build: .
|
# Selbst bauen statt das Registry-Image zu pullen?
|
||||||
|
# Dann image-Zeile auskommentieren und folgende aktivieren:
|
||||||
|
# build: .
|
||||||
volumes:
|
volumes:
|
||||||
- ./config:/app/config
|
- ./config:/app/config
|
||||||
- ./data:/app/data
|
- ./data:/app/data
|
||||||
|
|||||||
@@ -371,9 +371,12 @@ class KobraXClient:
|
|||||||
report_registered = True
|
report_registered = True
|
||||||
|
|
||||||
topic = self._pub_topic(msg_type)
|
topic = self._pub_topic(msg_type)
|
||||||
log.info("TX %-25s action=%-12s data=%s",
|
# Status-Poll-TX (query/getInfo) ist reines Rauschen (alle paar Sekunden) →
|
||||||
f"{msg_type}/request", action,
|
# auf DEBUG. Aktions-TX (start/set/control/move/…) bleibt INFO sichtbar.
|
||||||
json.dumps(data, ensure_ascii=False) if data else "null")
|
_tx_level = logging.DEBUG if action in ("query", "getInfo") else logging.INFO
|
||||||
|
log.log(_tx_level, "TX %-25s action=%-12s data=%s",
|
||||||
|
f"{msg_type}/request", action,
|
||||||
|
json.dumps(data, ensure_ascii=False) if data else "null")
|
||||||
try:
|
try:
|
||||||
with self._lock:
|
with self._lock:
|
||||||
self._sock.sendall(_build_publish(topic, payload))
|
self._sock.sendall(_build_publish(topic, payload))
|
||||||
@@ -542,9 +545,15 @@ class KobraXClient:
|
|||||||
f"Connection: close\r\n\r\n"
|
f"Connection: close\r\n\r\n"
|
||||||
).encode()
|
).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.sendall(headers + body)
|
||||||
sock.settimeout(120) # große GCode-Dateien brauchen Zeit bis der Drucker antwortet
|
sock.settimeout(180)
|
||||||
response = b""
|
response = b""
|
||||||
try:
|
try:
|
||||||
while True:
|
while True:
|
||||||
|
|||||||
File diff suppressed because it is too large
Load Diff
45
kx-bridge.spec
Normal file
45
kx-bridge.spec
Normal file
@@ -0,0 +1,45 @@
|
|||||||
|
# PyInstaller-Spec für kx-bridge — plattformneutral (Linux + Windows via PyBuilder).
|
||||||
|
# Wird relativ zum Repo-Root ausgeführt (`pyinstaller kx-bridge.spec`), wo
|
||||||
|
# kobrax_moonraker_bridge.py und web/ flach liegen (Release-Repo-Layout).
|
||||||
|
#
|
||||||
|
# Bindet das Web-Theme-System (web/themes/<name>/ + web/DOC/) ins Onefile-Binary
|
||||||
|
# ein → zur Laufzeit über sys._MEIPASS lesbar (_WEB_BASE in der Bridge).
|
||||||
|
from PyInstaller.utils.hooks import collect_all
|
||||||
|
|
||||||
|
datas = [("web", "web"), ("data", "static")] # bridge/data/ → static/ im _MEIPASS
|
||||||
|
binaries = []
|
||||||
|
hiddenimports = []
|
||||||
|
|
||||||
|
# pycryptodome vollständig einsammeln (Krypto für die Drucker-Auth)
|
||||||
|
_d, _b, _h = collect_all("pycryptodome")
|
||||||
|
datas += _d
|
||||||
|
binaries += _b
|
||||||
|
hiddenimports += _h
|
||||||
|
|
||||||
|
a = Analysis(
|
||||||
|
["kobrax_moonraker_bridge.py"],
|
||||||
|
pathex=[],
|
||||||
|
binaries=binaries,
|
||||||
|
datas=datas,
|
||||||
|
hiddenimports=hiddenimports,
|
||||||
|
hookspath=[],
|
||||||
|
runtime_hooks=[],
|
||||||
|
excludes=[],
|
||||||
|
noarchive=False,
|
||||||
|
)
|
||||||
|
pyz = PYZ(a.pure)
|
||||||
|
|
||||||
|
exe = EXE(
|
||||||
|
pyz,
|
||||||
|
a.scripts,
|
||||||
|
a.binaries,
|
||||||
|
a.datas,
|
||||||
|
[],
|
||||||
|
name="kx-bridge",
|
||||||
|
debug=False,
|
||||||
|
bootloader_ignore_signals=False,
|
||||||
|
strip=False,
|
||||||
|
upx=False,
|
||||||
|
console=True,
|
||||||
|
onefile=True,
|
||||||
|
)
|
||||||
136
web/DOC/THEME-CSS-HOOKS.md
Normal file
136
web/DOC/THEME-CSS-HOOKS.md
Normal file
@@ -0,0 +1,136 @@
|
|||||||
|
# KX-Bridge Theme – CSS-ID-Hooks
|
||||||
|
|
||||||
|
Referenzliste für CSS-/Layout-Anpassungen.
|
||||||
|
|
||||||
|
| ID | Verwendung |
|
||||||
|
|---|---|
|
||||||
|
| `#ace-dry-dialog-custom-name-label` | Hook / Selektor |
|
||||||
|
| `#ace-dry-dialog-custom-name-row` | Hook / Selektor |
|
||||||
|
| `#ace-dry-dialog-temp-label` | Hook / Selektor |
|
||||||
|
| `#ace-dry-dialog-time-label` | Hook / Selektor |
|
||||||
|
| `#ace-dry-dialog-title` | Hook / Selektor |
|
||||||
|
| `#add-printer-btn-label` | Hook / Selektor |
|
||||||
|
| `#ams-no-data` | Hook / Selektor |
|
||||||
|
| `#apd-ip` | Hook / Selektor |
|
||||||
|
| `#apd-lbl-ip` | Hook / Selektor |
|
||||||
|
| `#apd-lbl-name` | Hook / Selektor |
|
||||||
|
| `#apd-name` | Hook / Selektor |
|
||||||
|
| `#apd-status` | Hook / Selektor |
|
||||||
|
| `#apd-title` | Hook / Selektor |
|
||||||
|
| `#btn-log-dl` | Hook / Selektor |
|
||||||
|
| `#cam-fname` | Hook / Selektor |
|
||||||
|
| `#cam-img` | Hook / Selektor |
|
||||||
|
| `#cam-overlay` | Hook / Selektor |
|
||||||
|
| `#cam-placeholder` | Hook / Selektor |
|
||||||
|
| `#cam-placeholder-txt` | Hook / Selektor |
|
||||||
|
| `#cam-spinner` | Hook / Selektor |
|
||||||
|
| `#cam-wrap` | Hook / Selektor |
|
||||||
|
| `#conn-error-banner` | Hook / Selektor |
|
||||||
|
| `#d-ace-dry-grid` | Hook / Selektor |
|
||||||
|
| `#d-ace-dry-wrap` | Hook / Selektor |
|
||||||
|
| `#d-ams-card` | Hook / Selektor |
|
||||||
|
| `#d-bt-t` | Hook / Selektor |
|
||||||
|
| `#d-btbar` | Hook / Selektor |
|
||||||
|
| `#d-btn-skip-label` | Hook / Selektor |
|
||||||
|
| `#d-card-ams` | Hook / Selektor |
|
||||||
|
| `#d-card-cam` | Hook / Selektor |
|
||||||
|
| `#d-card-lightfan` | Hook / Selektor |
|
||||||
|
| `#d-card-progress` | Hook / Selektor |
|
||||||
|
| `#d-card-speed` | Hook / Selektor |
|
||||||
|
| `#d-card-temps` | Hook / Selektor |
|
||||||
|
| `#d-chart-label` | Hook / Selektor |
|
||||||
|
| `#d-ctrl-btns` | Hook / Selektor |
|
||||||
|
| `#d-elapsed` | Hook / Selektor |
|
||||||
|
| `#d-fname` | Hook / Selektor |
|
||||||
|
| `#d-layers` | Hook / Selektor |
|
||||||
|
| `#d-lbl-bed` | Hook / Selektor |
|
||||||
|
| `#d-lbl-elapsed` | Hook / Selektor |
|
||||||
|
| `#d-lbl-layers` | Hook / Selektor |
|
||||||
|
| `#d-lbl-light` | Hook / Selektor |
|
||||||
|
| `#d-lbl-remain` | Hook / Selektor |
|
||||||
|
| `#d-nt` | Hook / Selektor |
|
||||||
|
| `#d-nt-t` | Hook / Selektor |
|
||||||
|
| `#d-ntbar` | Hook / Selektor |
|
||||||
|
| `#d-pbar` | Hook / Selektor |
|
||||||
|
| `#d-pct` | Hook / Selektor |
|
||||||
|
| `#d-remain` | Hook / Selektor |
|
||||||
|
| `#d-slicer-label` | Hook / Selektor |
|
||||||
|
| `#d-slicer-row` | Hook / Selektor |
|
||||||
|
| `#d-slicer-time` | Hook / Selektor |
|
||||||
|
| `#d-spd-bar` | Hook / Selektor |
|
||||||
|
| `#d-spd-lbl-1` | Hook / Selektor |
|
||||||
|
| `#d-spd-lbl-2` | Hook / Selektor |
|
||||||
|
| `#d-spd-lbl-3` | Hook / Selektor |
|
||||||
|
| `#d-thumbnail` | Hook / Selektor |
|
||||||
|
| `#fd-objects` | Hook / Selektor |
|
||||||
|
| `#fd-objects-hint` | Hook / Selektor |
|
||||||
|
| `#fd-objects-section` | Hook / Selektor |
|
||||||
|
| `#fd-objects-svg` | Hook / Selektor |
|
||||||
|
| `#fd-slots-hint` | Hook / Selektor |
|
||||||
|
| `#fd-title` | Hook / Selektor |
|
||||||
|
| `#file-ready-banner` | Hook / Selektor |
|
||||||
|
| `#file-ready-name` | Hook / Selektor |
|
||||||
|
| `#h-badge` | Hook / Selektor |
|
||||||
|
| `#h-pname` | Hook / Selektor |
|
||||||
|
| `#h-pname-single` | Hook / Selektor |
|
||||||
|
| `#h-state` | Hook / Selektor |
|
||||||
|
| `#h-version` | Hook / Selektor |
|
||||||
|
| `#lbl-auto-leveling` | Hook / Selektor |
|
||||||
|
| `#lbl-default-slot` | Hook / Selektor |
|
||||||
|
| `#lbl-device-id` | Hook / Selektor |
|
||||||
|
| `#lbl-ip-hint` | Hook / Selektor |
|
||||||
|
| `#lbl-mode-id` | Hook / Selektor |
|
||||||
|
| `#lbl-mqtt-port` | Hook / Selektor |
|
||||||
|
| `#lbl-password` | Hook / Selektor |
|
||||||
|
| `#lbl-printer-ip` | Hook / Selektor |
|
||||||
|
| `#lbl-printer-name` | Hook / Selektor |
|
||||||
|
| `#lbl-slot-color` | Hook / Selektor |
|
||||||
|
| `#lbl-slot-material` | Hook / Selektor |
|
||||||
|
| `#lbl-update-apply` | Hook / Selektor |
|
||||||
|
| `#lbl-update-check` | Hook / Selektor |
|
||||||
|
| `#lbl-username` | Hook / Selektor |
|
||||||
|
| `#log-badge` | Hook / Selektor |
|
||||||
|
| `#log-badge-bot` | Hook / Selektor |
|
||||||
|
| `#modal-sec-connection` | Hook / Selektor |
|
||||||
|
| `#modal-sec-poll` | Hook / Selektor |
|
||||||
|
| `#modal-sec-print` | Hook / Selektor |
|
||||||
|
| `#modal-sec-version` | Hook / Selektor |
|
||||||
|
| `#modal-title-settings` | Hook / Selektor |
|
||||||
|
| `#opt-slot-0` | Hook / Selektor |
|
||||||
|
| `#opt-slot-1` | Hook / Selektor |
|
||||||
|
| `#opt-slot-2` | Hook / Selektor |
|
||||||
|
| `#opt-slot-3` | Hook / Selektor |
|
||||||
|
| `#opt-slot-auto` | Hook / Selektor |
|
||||||
|
| `#printer-dropdown-menu` | Hook / Selektor |
|
||||||
|
| `#printer-dropdown-wrap` | Hook / Selektor |
|
||||||
|
| `#printers-panel-title` | Hook / Selektor |
|
||||||
|
| `#ptitle-console` | Hook / Selektor |
|
||||||
|
| `#ptitle-motion-xy` | Hook / Selektor |
|
||||||
|
| `#ptitle-motion-z` | Hook / Selektor |
|
||||||
|
| `#s-auto-leveling` | Hook / Selektor |
|
||||||
|
| `#s-default-slot` | Hook / Selektor |
|
||||||
|
| `#s-device-id` | Hook / Selektor |
|
||||||
|
| `#s-mode-id` | Hook / Selektor |
|
||||||
|
| `#s-mqtt-port` | Hook / Selektor |
|
||||||
|
| `#s-password` | Hook / Selektor |
|
||||||
|
| `#s-printer-name` | Hook / Selektor |
|
||||||
|
| `#s-username` | Hook / Selektor |
|
||||||
|
| `#s-version-label` | Hook / Selektor |
|
||||||
|
| `#sf-all` | Hook / Selektor |
|
||||||
|
| `#sf-err` | Hook / Selektor |
|
||||||
|
| `#sf-new` | Hook / Selektor |
|
||||||
|
| `#sf-ok` | Hook / Selektor |
|
||||||
|
| `#skip-hint` | Hook / Selektor |
|
||||||
|
| `#skip-list` | Hook / Selektor |
|
||||||
|
| `#skip-status` | Hook / Selektor |
|
||||||
|
| `#skip-svg` | Hook / Selektor |
|
||||||
|
| `#skip-title` | Hook / Selektor |
|
||||||
|
| `#slot-edit-title` | Hook / Selektor |
|
||||||
|
| `#ss-date` | Hook / Selektor |
|
||||||
|
| `#ss-dur` | Hook / Selektor |
|
||||||
|
| `#ss-name` | Hook / Selektor |
|
||||||
|
| `#step-display` | Hook / Selektor |
|
||||||
|
| `#store-empty` | Hook / Selektor |
|
||||||
|
| `#store-panel-title` | Hook / Selektor |
|
||||||
|
| `#update-changelog` | Hook / Selektor |
|
||||||
|
| `#update-status` | Hook / Selektor |
|
||||||
90
web/DOC/THEME-JS-ID-HOOKS.md
Normal file
90
web/DOC/THEME-JS-ID-HOOKS.md
Normal file
@@ -0,0 +1,90 @@
|
|||||||
|
# KX-Bridge Theme – JavaScript-ID-Hooks
|
||||||
|
|
||||||
|
Referenzliste für JavaScript-/DOM-Hooks.
|
||||||
|
|
||||||
|
| ID | Verwendung |
|
||||||
|
|---|---|
|
||||||
|
| `#ace-dry-dialog` | Hook / Selektor |
|
||||||
|
| `#ace-dry-dialog-cancel` | Hook / Selektor |
|
||||||
|
| `#ace-dry-dialog-confirm` | Hook / Selektor |
|
||||||
|
| `#ace-dry-dialog-custom-name` | Hook / Selektor |
|
||||||
|
| `#ace-dry-dialog-h` | Hook / Selektor |
|
||||||
|
| `#ace-dry-dialog-m` | Hook / Selektor |
|
||||||
|
| `#ace-dry-dialog-reset-default` | Hook / Selektor |
|
||||||
|
| `#ace-dry-dialog-s` | Hook / Selektor |
|
||||||
|
| `#ace-dry-dialog-save-preset` | Hook / Selektor |
|
||||||
|
| `#ace-dry-dialog-temp` | Hook / Selektor |
|
||||||
|
| `#add-printer-dialog` | Hook / Selektor |
|
||||||
|
| `#ams-slots` | Hook / Selektor |
|
||||||
|
| `#apd-confirm` | Hook / Selektor |
|
||||||
|
| `#bnb-console` | Hook / Selektor |
|
||||||
|
| `#bnb-dashboard` | Hook / Selektor |
|
||||||
|
| `#bnb-printers` | Hook / Selektor |
|
||||||
|
| `#bnb-store` | Hook / Selektor |
|
||||||
|
| `#btn-autoscroll` | Hook / Selektor |
|
||||||
|
| `#btn-save-settings` | Hook / Selektor |
|
||||||
|
| `#btn-slot-edit-feed` | Hook / Selektor |
|
||||||
|
| `#btn-slot-edit-save` | Hook / Selektor |
|
||||||
|
| `#btn-update-apply` | Hook / Selektor |
|
||||||
|
| `#btn-update-check` | Hook / Selektor |
|
||||||
|
| `#cam-toggle-btn` | Hook / Selektor |
|
||||||
|
| `#conn-btn` | Hook / Selektor |
|
||||||
|
| `#console-log` | Hook / Selektor |
|
||||||
|
| `#d-bt` | Hook / Selektor |
|
||||||
|
| `#d-btn-cancel` | Hook / Selektor |
|
||||||
|
| `#d-btn-pause` | Hook / Selektor |
|
||||||
|
| `#d-btn-resume` | Hook / Selektor |
|
||||||
|
| `#d-btn-skip` | Hook / Selektor |
|
||||||
|
| `#d-chart` | Hook / Selektor |
|
||||||
|
| `#d-fan` | Hook / Selektor |
|
||||||
|
| `#d-fan-val` | Hook / Selektor |
|
||||||
|
| `#d-light-toggle` | Hook / Selektor |
|
||||||
|
| `#d-spd-1` | Hook / Selektor |
|
||||||
|
| `#d-spd-2` | Hook / Selektor |
|
||||||
|
| `#d-spd-3` | Hook / Selektor |
|
||||||
|
| `#fd-cancel` | Hook / Selektor |
|
||||||
|
| `#fd-print` | Hook / Selektor |
|
||||||
|
| `#fd-slots` | Hook / Selektor |
|
||||||
|
| `#filament-dialog` | Hook / Selektor |
|
||||||
|
| `#file-cancel-btn` | Hook / Selektor |
|
||||||
|
| `#file-ready-btn` | Hook / Selektor |
|
||||||
|
| `#file-slots-btn` | Hook / Selektor |
|
||||||
|
| `#lang-btn` | Hook / Selektor |
|
||||||
|
| `#log-filter` | Hook / Selektor |
|
||||||
|
| `#logdir-all` | Hook / Selektor |
|
||||||
|
| `#logdir-rx` | Hook / Selektor |
|
||||||
|
| `#logdir-tx` | Hook / Selektor |
|
||||||
|
| `#log-lbl-level` | i18n-Label "Level:" |
|
||||||
|
| `#loglvl-all` | onclick `setLogLevel('all')` |
|
||||||
|
| `#loglvl-err` | onclick `setLogLevel('err')` — nur Fehler |
|
||||||
|
| `#loglvl-warn` | onclick `setLogLevel('warn')` — Fehler + Warnungen |
|
||||||
|
| `#nb-console` | Hook / Selektor |
|
||||||
|
| `#nb-dashboard` | Hook / Selektor |
|
||||||
|
| `#nb-printers` | Hook / Selektor |
|
||||||
|
| `#nb-store` | Hook / Selektor |
|
||||||
|
| `#p-bed-inp` | Hook / Selektor |
|
||||||
|
| `#p-nozzle-inp` | Hook / Selektor |
|
||||||
|
| `#panel-console` | Hook / Selektor |
|
||||||
|
| `#panel-dashboard` | Hook / Selektor |
|
||||||
|
| `#panel-printers` | Hook / Selektor |
|
||||||
|
| `#panel-store` | Hook / Selektor |
|
||||||
|
| `#poll-1` | Hook / Selektor |
|
||||||
|
| `#poll-2` | Hook / Selektor |
|
||||||
|
| `#poll-5` | Hook / Selektor |
|
||||||
|
| `#printer-dropdown-btn` | Hook / Selektor |
|
||||||
|
| `#printers-grid` | Hook / Selektor |
|
||||||
|
| `#s-printer-ip` | Hook / Selektor |
|
||||||
|
| `#settings-btn` | Hook / Selektor |
|
||||||
|
| `#settings-modal` | Hook / Selektor |
|
||||||
|
| `#skip-confirm` | Hook / Selektor |
|
||||||
|
| `#skip-dialog` | Hook / Selektor |
|
||||||
|
| `#slot-edit-color` | Hook / Selektor |
|
||||||
|
| `#slot-edit-mat` | Hook / Selektor |
|
||||||
|
| `#slot-edit-modal` | Hook / Selektor |
|
||||||
|
| `#slot-edit-preview` | Hook / Selektor |
|
||||||
|
| `#slot-mat-btns` | Hook / Selektor |
|
||||||
|
| `#store-filter` | Hook / Selektor |
|
||||||
|
| `#store-grid` | Hook / Selektor |
|
||||||
|
| `#store-refresh-btn` | Hook / Selektor |
|
||||||
|
| `#store-search` | Hook / Selektor |
|
||||||
|
| `#store-sort` | Hook / Selektor |
|
||||||
2382
web/themes/default/app.js
Normal file
2382
web/themes/default/app.js
Normal file
File diff suppressed because it is too large
Load Diff
615
web/themes/default/index.html
Normal file
615
web/themes/default/index.html
Normal file
@@ -0,0 +1,615 @@
|
|||||||
|
<!DOCTYPE html>
|
||||||
|
<html lang="de" data-theme="dark">
|
||||||
|
<head>
|
||||||
|
<meta charset="utf-8">
|
||||||
|
<meta name="viewport" content="width=device-width,initial-scale=1">
|
||||||
|
<title>KX-Bridge</title>
|
||||||
|
<link rel="stylesheet" href="/kx/ui/style.css">
|
||||||
|
<body>
|
||||||
|
|
||||||
|
<div id="conn-error-banner" style="display:none;background:#c0392b;color:#fff;padding:10px 18px;font-size:14px;text-align:center;position:sticky;top:0;z-index:999;"></div>
|
||||||
|
<div id="file-ready-banner" style="display:none;background:#1a6e3c;color:#fff;padding:10px 18px;font-size:14px;text-align:center;position:sticky;top:0;z-index:998;display:none;align-items:center;justify-content:center;gap:12px;flex-wrap:wrap">
|
||||||
|
<span>📄 <span id="file-ready-name"></span></span>
|
||||||
|
<button id="file-ready-btn" onclick="startReadyFile()"
|
||||||
|
style="padding:5px 16px;background:#fff;color:#1a6e3c;border:none;border-radius:6px;font-weight:700;cursor:pointer;font-size:13px"></button>
|
||||||
|
<button id="file-slots-btn" onclick="startReadyFileWithSlots()"
|
||||||
|
style="padding:5px 16px;background:rgba(255,255,255,0.15);color:#fff;border:1px solid rgba(255,255,255,0.5);border-radius:6px;font-weight:700;cursor:pointer;font-size:13px"></button>
|
||||||
|
<button id="file-cancel-btn" onclick="cancelReadyFile()"
|
||||||
|
style="padding:5px 16px;background:rgba(255,255,255,0.15);color:#fff;border:1px solid rgba(255,255,255,0.5);border-radius:6px;font-weight:700;cursor:pointer;font-size:13px"></button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<header>
|
||||||
|
<div class="logo">⬡ KX-Bridge</div>
|
||||||
|
<div style="flex:1"></div>
|
||||||
|
<div id="printer-dropdown-wrap" style="display:none;position:relative">
|
||||||
|
<button id="printer-dropdown-btn" onclick="togglePrinterDropdown()" style="background:var(--raised);border:1px solid var(--border);border-radius:6px;padding:4px 10px;color:var(--txt);cursor:pointer;font-size:13px;display:flex;align-items:center;gap:6px">
|
||||||
|
<span id="h-pname">Anycubic Kobra X</span><span style="opacity:.5">▾</span>
|
||||||
|
</button>
|
||||||
|
<div id="printer-dropdown-menu" style="display:none;position:absolute;top:calc(100% + 4px);right:0;background:var(--card);border:1px solid var(--border);border-radius:8px;min-width:200px;z-index:200;box-shadow:0 4px 16px #0006;overflow:hidden">
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div id="h-pname-single" class="hname">Anycubic Kobra X</div>
|
||||||
|
<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>
|
||||||
|
<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>
|
||||||
|
|
||||||
|
<!-- ═══ SETTINGS MODAL ═══ -->
|
||||||
|
<div class="modal-overlay" id="settings-modal" onclick="if(event.target===this)closeSettings()">
|
||||||
|
<div class="modal-box">
|
||||||
|
<div class="modal-header">
|
||||||
|
<span class="modal-title" id="modal-title-settings">Einstellungen</span>
|
||||||
|
<button class="modal-close" onclick="closeSettings()">✕</button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div>
|
||||||
|
<div class="modal-field" style="margin-bottom:12px">
|
||||||
|
<label id="lbl-printer-name" style="font-weight:600">Drucker-Name</label>
|
||||||
|
<input type="text" id="s-printer-name" placeholder="z.B. Kobra X Links">
|
||||||
|
</div>
|
||||||
|
<div class="modal-section" id="modal-sec-connection">Verbindung</div>
|
||||||
|
<div class="modal-field">
|
||||||
|
<label id="lbl-printer-ip">Drucker-IP</label>
|
||||||
|
<input type="text" id="s-printer-ip" placeholder="192.168.x.x">
|
||||||
|
<small id="lbl-ip-hint" style="color:#f80;display:none"></small>
|
||||||
|
</div>
|
||||||
|
<div class="modal-field">
|
||||||
|
<label id="lbl-mqtt-port">MQTT-Port</label>
|
||||||
|
<input type="number" id="s-mqtt-port" placeholder="9883">
|
||||||
|
</div>
|
||||||
|
<div class="modal-field">
|
||||||
|
<label id="lbl-username">MQTT-Benutzername</label>
|
||||||
|
<input type="text" id="s-username" placeholder="userXXXXXXXX" autocomplete="new-password">
|
||||||
|
</div>
|
||||||
|
<div class="modal-field">
|
||||||
|
<label id="lbl-password">MQTT-Passwort</label>
|
||||||
|
<input type="password" id="s-password" autocomplete="new-password">
|
||||||
|
</div>
|
||||||
|
<div class="modal-field">
|
||||||
|
<label id="lbl-device-id">Device-ID</label>
|
||||||
|
<input type="text" id="s-device-id" placeholder="32 Hex-Zeichen">
|
||||||
|
</div>
|
||||||
|
<div class="modal-field">
|
||||||
|
<label id="lbl-mode-id">Mode-ID</label>
|
||||||
|
<input type="text" id="s-mode-id" placeholder="20030">
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div>
|
||||||
|
<div class="modal-section" id="modal-sec-print">Druckeinstellungen</div>
|
||||||
|
<div class="modal-field">
|
||||||
|
<label id="lbl-default-slot">Standard-Slot (Einfarbdruck)</label>
|
||||||
|
<select id="s-default-slot">
|
||||||
|
<option value="auto" id="opt-slot-auto">Auto (alle belegten Slots)</option>
|
||||||
|
<option value="0" id="opt-slot-0">Slot 1</option>
|
||||||
|
<option value="1" id="opt-slot-1">Slot 2</option>
|
||||||
|
<option value="2" id="opt-slot-2">Slot 3</option>
|
||||||
|
<option value="3" id="opt-slot-3">Slot 4</option>
|
||||||
|
</select>
|
||||||
|
</div>
|
||||||
|
<div class="modal-field" style="flex-direction:row;align-items:center;gap:10px">
|
||||||
|
<input type="checkbox" id="s-auto-leveling" style="width:auto;margin:0">
|
||||||
|
<label id="lbl-auto-leveling" style="margin:0;cursor:pointer" for="s-auto-leveling">Auto-Leveling vor Druck</label>
|
||||||
|
</div>
|
||||||
|
<div class="modal-field" style="flex-direction:row;align-items:center;gap:10px">
|
||||||
|
<input type="checkbox" id="s-camera-on-print" style="width:auto;margin:0">
|
||||||
|
<label id="lbl-camera-on-print" style="margin:0;cursor:pointer" for="s-camera-on-print">Kamera bei Druckstart einschalten</label>
|
||||||
|
</div>
|
||||||
|
<div class="modal-field" style="flex-direction:row;align-items:center;gap:10px">
|
||||||
|
<input type="checkbox" id="s-web-upload-warning" style="width:auto;margin:0">
|
||||||
|
<label id="lbl-web-upload-warning" style="margin:0;cursor:pointer" for="s-web-upload-warning">Warnung bei Web-Upload-Druck anzeigen</label>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div>
|
||||||
|
<div class="modal-section" id="modal-sec-poll">Poll-Intervall</div>
|
||||||
|
<div class="poll-btns">
|
||||||
|
<button class="poll-btn" onclick="setPoll(1000)" id="poll-1">1s</button>
|
||||||
|
<button class="poll-btn active" onclick="setPoll(2000)" id="poll-2">2s</button>
|
||||||
|
<button class="poll-btn" onclick="setPoll(5000)" id="poll-5">5s</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div>
|
||||||
|
<div class="modal-section" id="modal-sec-version">Version</div>
|
||||||
|
<div class="update-row">
|
||||||
|
<span id="s-version-label" style="font-size:13px;color:var(--txt)">–</span>
|
||||||
|
<button class="btn btn-sm" style="background:var(--raised);color:var(--txt)" onclick="checkUpdate()" id="btn-update-check">🔄 <span id="lbl-update-check">Auf Updates prüfen</span></button>
|
||||||
|
</div>
|
||||||
|
<div class="update-status" id="update-status" style="margin-top:6px"></div>
|
||||||
|
<button class="btn btn-sm btn-accent" id="btn-update-apply" style="display:none;margin-top:8px" onclick="applyUpdate()">
|
||||||
|
<span id="lbl-update-apply">Jetzt installieren</span>
|
||||||
|
</button>
|
||||||
|
<div id="update-changelog" style="display:none;margin-top:10px;background:var(--raised);border-radius:6px;padding:10px;font-size:11px;font-family:var(--mono);color:var(--txt2);white-space:pre-wrap;max-height:180px;overflow-y:auto;line-height:1.6"></div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<button class="modal-save" onclick="saveSettings()" id="btn-save-settings">Speichern & Neustart</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- ═══ AMS SLOT EDIT DIALOG ═══ -->
|
||||||
|
<div class="modal-overlay" id="slot-edit-modal" onclick="if(event.target===this)closeSlotEdit()">
|
||||||
|
<div class="modal-box" style="max-width:340px">
|
||||||
|
<div style="display:flex;justify-content:space-between;align-items:center;margin-bottom:16px">
|
||||||
|
<span class="modal-title" id="slot-edit-title"></span>
|
||||||
|
<button onclick="closeSlotEdit()" style="background:none;border:none;color:var(--txt2);font-size:20px;cursor:pointer;line-height:1">✕</button>
|
||||||
|
</div>
|
||||||
|
<div style="display:flex;align-items:center;gap:16px;margin-bottom:20px">
|
||||||
|
<div id="slot-edit-preview" style="width:56px;height:56px;border-radius:50%;border:3px solid rgba(255,255,255,.2);flex-shrink:0"></div>
|
||||||
|
<div style="flex:1">
|
||||||
|
<div style="font-size:11px;color:var(--txt2);margin-bottom:4px" id="lbl-slot-color"></div>
|
||||||
|
<input type="color" id="slot-edit-color"
|
||||||
|
oninput="document.getElementById('slot-edit-preview').style.background=this.value"
|
||||||
|
style="width:100%;height:36px;border:1px solid var(--border);border-radius:6px;background:var(--raised);cursor:pointer;padding:2px">
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div style="margin-bottom:20px">
|
||||||
|
<div style="font-size:11px;color:var(--txt2);margin-bottom:6px" id="lbl-slot-material"></div>
|
||||||
|
<div style="display:flex;flex-wrap:wrap;gap:6px" id="slot-mat-btns">
|
||||||
|
</div>
|
||||||
|
<input type="text" id="slot-edit-mat"
|
||||||
|
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>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="layout">
|
||||||
|
<nav class="sidebar">
|
||||||
|
<button class="nav-btn active" onclick="showPanel('dashboard')" id="nb-dashboard">
|
||||||
|
<span class="nav-icon">⊞</span><span class="nav-text">Dashboard</span></button>
|
||||||
|
<button class="nav-btn" onclick="showPanel('printers');loadPrinterTab()" id="nb-printers">
|
||||||
|
<span class="nav-icon">🖨</span><span class="nav-text">Drucker</span></button>
|
||||||
|
<button class="nav-btn" onclick="showPanel('store');loadStore()" id="nb-store">
|
||||||
|
<span class="nav-icon">🗂</span><span class="nav-text">Browser</span></button>
|
||||||
|
<button class="nav-btn" onclick="showPanel('console');clearLogBadge()" id="nb-console">
|
||||||
|
<span class="nav-icon">≡</span><span class="nav-text">Konsole</span><span id="log-badge" style="display:none;margin-left:4px;background:var(--err);color:#fff;border-radius:10px;font-size:10px;padding:1px 5px;font-weight:700"></span></button>
|
||||||
|
</nav>
|
||||||
|
|
||||||
|
<main>
|
||||||
|
<!-- ═══ DASHBOARD ═══ -->
|
||||||
|
<div class="panel active" id="panel-dashboard">
|
||||||
|
<div class="grid">
|
||||||
|
<!-- Kamera -->
|
||||||
|
<div class="card" style="grid-column:1/-1">
|
||||||
|
<div style="display:flex;align-items:center;justify-content:space-between;margin-bottom:10px">
|
||||||
|
<div class="card-title" style="margin-bottom:0"><span>📷</span> <span id="d-card-cam">Kamera</span></div>
|
||||||
|
<div style="display:flex;align-items:center;gap:10px">
|
||||||
|
<span id="d-lbl-light" style="font-size:12px;color:var(--txt2)">💡 Licht</span>
|
||||||
|
<label class="toggle">
|
||||||
|
<input type="checkbox" id="d-light-toggle" onchange="setLight()">
|
||||||
|
<span class="toggle-track"></span>
|
||||||
|
<span class="toggle-thumb"></span>
|
||||||
|
</label>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="cam-wrap" id="cam-wrap">
|
||||||
|
<div class="cam-placeholder" id="cam-placeholder"><span id="cam-placeholder-txt">📷 Kamera nicht gestartet</span></div>
|
||||||
|
<div class="cam-spinner" id="cam-spinner"></div>
|
||||||
|
<img id="cam-img" style="display:none;width:100%;height:auto" alt="Kamera">
|
||||||
|
<div class="cam-overlay" id="cam-overlay" style="display:none">
|
||||||
|
<div style="font-size:12px;color:#fff" id="cam-fname"></div>
|
||||||
|
</div>
|
||||||
|
<button class="cam-toggle" onclick="toggleCam()" id="cam-toggle-btn">▶ Kamera</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Fortschritt -->
|
||||||
|
<div class="card" style="grid-column:1/-1">
|
||||||
|
<div class="card-title"><span>◉</span> <span id="d-card-progress">Fortschritt</span></div>
|
||||||
|
<img id="d-thumbnail" src="" alt="" style="display:none;width:100%;max-height:160px;object-fit:contain;border-radius:8px;background:#111;margin-bottom:10px">
|
||||||
|
<div class="pct-big"><span id="d-pct">0</span><small>%</small></div>
|
||||||
|
<div style="display:flex;align-items:center;gap:10px;margin:8px 0">
|
||||||
|
<div class="progress-bar" style="flex:1;margin:0"><div class="progress-fill" id="d-pbar" style="width:0%"></div></div>
|
||||||
|
<div class="time-block" style="padding:6px 10px;min-width:72px;text-align:center;flex-shrink:0">
|
||||||
|
<div class="time-label" id="d-lbl-layers"></div>
|
||||||
|
<div class="time-val" style="font-size:16px" id="d-layers">–</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="time-grid">
|
||||||
|
<div class="time-block">
|
||||||
|
<div class="time-label" id="d-lbl-elapsed"></div>
|
||||||
|
<div class="time-val" id="d-elapsed">–</div>
|
||||||
|
</div>
|
||||||
|
<div class="time-block" id="d-slicer-row" style="display:none">
|
||||||
|
<div class="time-label" id="d-slicer-label"></div>
|
||||||
|
<div class="time-val" id="d-slicer-time">–</div>
|
||||||
|
</div>
|
||||||
|
<div class="time-block" style="color:var(--acc)">
|
||||||
|
<div class="time-label" id="d-lbl-remain"></div>
|
||||||
|
<div class="time-val" id="d-remain" style="color:var(--acc)">–</div>
|
||||||
|
</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>
|
||||||
|
<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>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Temperatursteuerung + Verlauf -->
|
||||||
|
<div class="card" style="grid-column:1/-1">
|
||||||
|
<div class="card-title"><span>⊙</span> <span id="d-card-temps">Temperaturen</span></div>
|
||||||
|
<div class="temp-card-inner">
|
||||||
|
<div class="temp-block">
|
||||||
|
<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>
|
||||||
|
</div>
|
||||||
|
<div class="temp-target">→ <span id="d-nt-t">0</span>°C</div>
|
||||||
|
<div class="progress-bar" style="margin:8px 0 0">
|
||||||
|
<div class="progress-fill" id="d-ntbar" style="width:0%;background:linear-gradient(90deg,var(--accent2),#ffb020)"></div>
|
||||||
|
</div>
|
||||||
|
<div class="temp-edit" style="margin-top:10px">
|
||||||
|
<input type="number" class="temp-input" id="p-nozzle-inp" placeholder="Ziel" min="0" max="300" style="flex:1">
|
||||||
|
<button class="btn btn-sm btn-accent" onclick="setNozzle()"><span class="lbl-set">Set</span></button>
|
||||||
|
<button class="btn btn-sm" style="background:var(--raised);color:var(--txt)" onclick="document.getElementById('p-nozzle-inp').value=0;setNozzle()"><span class="lbl-off">Aus</span></button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="temp-block">
|
||||||
|
<div class="temp-label" id="d-lbl-bed">Bett</div>
|
||||||
|
<div class="temp-row">
|
||||||
|
<div class="temp-val" id="d-bt">–</div>
|
||||||
|
<div class="temp-unit">°C</div>
|
||||||
|
</div>
|
||||||
|
<div class="temp-target">→ <span id="d-bt-t">0</span>°C</div>
|
||||||
|
<div class="progress-bar" style="margin:8px 0 0">
|
||||||
|
<div class="progress-fill" id="d-btbar" style="width:0%;background:linear-gradient(90deg,#ff6b35,var(--warn))"></div>
|
||||||
|
</div>
|
||||||
|
<div class="temp-edit" style="margin-top:10px">
|
||||||
|
<input type="number" class="temp-input" id="p-bed-inp" placeholder="Ziel" min="0" max="120" style="flex:1">
|
||||||
|
<button class="btn btn-sm btn-accent" onclick="setBed()"><span class="lbl-set">Set</span></button>
|
||||||
|
<button class="btn btn-sm" style="background:var(--raised);color:var(--txt)" onclick="document.getElementById('p-bed-inp').value=0;setBed()"><span class="lbl-off">Aus</span></button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div style="margin-top:14px">
|
||||||
|
<div style="font-size:10px;color:var(--txt2);margin-bottom:4px" id="d-chart-label">Verlauf (letzte 60 Messungen)</div>
|
||||||
|
<canvas id="d-chart" width="800" height="120" style="width:100%;height:120px;background:var(--raised);border-radius:8px"></canvas>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Achsensteuerung -->
|
||||||
|
<div class="card">
|
||||||
|
<div class="card-title"><span>✛</span> <span id="ptitle-motion-xy">XY-Achsen</span></div>
|
||||||
|
<div class="joypad">
|
||||||
|
<div></div>
|
||||||
|
<button class="joy" onclick="move(1,1,getStep())" title="Y+">▲</button>
|
||||||
|
<div></div>
|
||||||
|
<button class="joy" onclick="move(0,-1,getStep())" title="X−">◀</button>
|
||||||
|
<button class="joy home" onclick="homeAll()" title="Home All">⌂</button>
|
||||||
|
<button class="joy" onclick="move(0,1,getStep())" title="X+">▶</button>
|
||||||
|
<div></div>
|
||||||
|
<button class="joy" onclick="move(1,-1,getStep())" title="Y−">▼</button>
|
||||||
|
<div></div>
|
||||||
|
</div>
|
||||||
|
<div class="step-btns">
|
||||||
|
<button class="step-btn" onclick="setStep(this,0.1)">0.1</button>
|
||||||
|
<button class="step-btn active" onclick="setStep(this,1)">1</button>
|
||||||
|
<button class="step-btn" onclick="setStep(this,5)">5</button>
|
||||||
|
<button class="step-btn" onclick="setStep(this,10)">10 mm</button>
|
||||||
|
</div>
|
||||||
|
<div class="home-btns">
|
||||||
|
<button class="btn btn-sm" style="background:var(--raised);color:var(--txt)" onclick="homeZ()"><span class="lbl-home-z">Home Z</span></button>
|
||||||
|
<button class="btn btn-sm" style="background:var(--raised);color:var(--txt)" onclick="homeXY()"><span class="lbl-home-xy">Home XY</span></button>
|
||||||
|
<button class="btn btn-sm btn-accent" onclick="homeAll()"><span class="lbl-home-all">Home All</span></button>
|
||||||
|
<button class="btn btn-sm" style="background:var(--raised);color:var(--txt)" onclick="disableMotors()"><span class="lbl-disable-motors">Motors Off</span></button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="card">
|
||||||
|
<div class="card-title"><span>↕</span> <span id="ptitle-motion-z">Z-Achse</span></div>
|
||||||
|
<div class="joypad" style="grid-template-columns:52px;grid-template-rows:repeat(2,52px)">
|
||||||
|
<button class="joy" onclick="move(2,1,getStep())" title="Z+">▲</button>
|
||||||
|
<button class="joy" onclick="move(2,-1,getStep())" title="Z−">▼</button>
|
||||||
|
</div>
|
||||||
|
<div style="text-align:center;margin-top:8px;font-size:12px;color:var(--txt2)"><span class="lbl-step">Schrittweite:</span> <span id="step-display">1</span> mm</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Print Speed -->
|
||||||
|
<div class="card">
|
||||||
|
<div class="card-title"><span>🏎</span> <span id="d-card-speed">Druckgeschwindigkeit</span></div>
|
||||||
|
<div style="display:flex;gap:8px;margin-top:4px">
|
||||||
|
<button class="spd-btn" id="d-spd-1" onclick="setSpeed(1)">
|
||||||
|
<span class="spd-icon">🐢</span>
|
||||||
|
<span id="d-spd-lbl-1">Leise</span>
|
||||||
|
</button>
|
||||||
|
<button class="spd-btn spd-active" id="d-spd-2" onclick="setSpeed(2)">
|
||||||
|
<span class="spd-icon">⚡</span>
|
||||||
|
<span id="d-spd-lbl-2">Normal</span>
|
||||||
|
</button>
|
||||||
|
<button class="spd-btn" id="d-spd-3" onclick="setSpeed(3)">
|
||||||
|
<span class="spd-icon">🚀</span>
|
||||||
|
<span id="d-spd-lbl-3">Sport</span>
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
<div class="spd-bar" style="margin-top:12px">
|
||||||
|
<div class="spd-bar-fill" id="d-spd-bar" style="width:50%"></div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Lüfter -->
|
||||||
|
<div class="card">
|
||||||
|
<div class="card-title"><span>🌀</span> <span id="d-card-lightfan">Lüfter</span></div>
|
||||||
|
<div class="slider-row">
|
||||||
|
<input type="range" class="slider" min="0" max="100" value="0" id="d-fan" oninput="document.getElementById('d-fan-val').textContent=this.value" onchange="setFan()">
|
||||||
|
<span class="slider-val" id="d-fan-val">0</span>
|
||||||
|
</div>
|
||||||
|
<div style="margin-top:12px;display:flex;gap:8px;flex-wrap:wrap">
|
||||||
|
<button class="btn btn-sm" style="background:var(--raised);color:var(--txt)" onclick="quickFan(0)">Aus</button>
|
||||||
|
<button class="btn btn-sm" style="background:var(--raised);color:var(--txt)" onclick="quickFan(25)">25%</button>
|
||||||
|
<button class="btn btn-sm" style="background:var(--raised);color:var(--txt)" onclick="quickFan(50)">50%</button>
|
||||||
|
<button class="btn btn-sm" style="background:var(--raised);color:var(--txt)" onclick="quickFan(75)">75%</button>
|
||||||
|
<button class="btn btn-sm btn-accent" onclick="quickFan(100)">100%</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div id="d-ace-dry-wrap" style="display:none">
|
||||||
|
<div id="d-ace-dry-grid" style="display:contents"></div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- AMS -->
|
||||||
|
<div class="card" style="grid-column:1/-1" id="d-ams-card">
|
||||||
|
<div class="card-title"><span>◫</span> <span id="d-card-ams">Filament</span></div>
|
||||||
|
<div class="ams-slots" id="ams-slots">
|
||||||
|
<div style="grid-column:1/-1;text-align:center;color:var(--txt2);padding:20px" id="ams-no-data">Keine AMS-Daten empfangen</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- ═══ CONSOLE ═══ -->
|
||||||
|
<!-- ═══ DRUCKER ═══ -->
|
||||||
|
<div class="panel" id="panel-printers">
|
||||||
|
<div class="card">
|
||||||
|
<div class="card-title" style="display:flex;justify-content:space-between;align-items:center;margin-bottom:12px">
|
||||||
|
<span id="printers-panel-title">🖨 Drucker</span>
|
||||||
|
<div style="display:flex;gap:8px">
|
||||||
|
<button onclick="openAddPrinterDialog()" style="font-size:12px;padding:4px 12px;background:var(--accent);border:none;border-radius:6px;color:#fff;cursor:pointer;font-weight:600">+ <span id="add-printer-btn-label">Drucker hinzufügen</span></button>
|
||||||
|
<button onclick="loadPrinterTab()" style="font-size:12px;padding:4px 12px;background:var(--raised);border:1px solid var(--border);border-radius:6px;color:var(--txt2);cursor:pointer">↻</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div id="printers-grid" style="display:grid;grid-template-columns:repeat(auto-fill,minmax(260px,1fr));gap:14px"></div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- ═══ GCODE STORE ═══ -->
|
||||||
|
<div class="panel" id="panel-store">
|
||||||
|
<div class="card">
|
||||||
|
<div class="card-title" style="display:flex;justify-content:space-between;align-items:center;margin-bottom:10px">
|
||||||
|
<span id="store-panel-title">🗂 Datei-Browser</span>
|
||||||
|
<button id="store-refresh-btn" onclick="loadStore()" style="font-size:12px;padding:4px 12px;background:var(--raised);border:1px solid var(--border);border-radius:6px;color:var(--txt2);cursor:pointer">↻ Aktualisieren</button>
|
||||||
|
</div>
|
||||||
|
<div style="display:flex;gap:8px;margin-bottom:12px;flex-wrap:wrap">
|
||||||
|
<input id="store-search" type="text" placeholder="🔍 Suche…" oninput="renderStore()"
|
||||||
|
style="flex:1;min-width:140px;padding:6px 10px;background:var(--raised);border:1px solid var(--border);border-radius:6px;color:var(--txt);font-size:13px">
|
||||||
|
<select id="store-filter" onchange="renderStore()"
|
||||||
|
style="padding:6px 8px;background:var(--raised);border:1px solid var(--border);border-radius:6px;color:var(--txt);font-size:13px">
|
||||||
|
<option value="all" id="sf-all">Alle</option>
|
||||||
|
<option value="completed" id="sf-ok">✓ Erfolgreich</option>
|
||||||
|
<option value="failed" id="sf-err">✗ Fehler</option>
|
||||||
|
<option value="never" id="sf-new">Neu</option>
|
||||||
|
</select>
|
||||||
|
<select id="store-sort" onchange="renderStore()"
|
||||||
|
style="padding:6px 8px;background:var(--raised);border:1px solid var(--border);border-radius:6px;color:var(--txt);font-size:13px">
|
||||||
|
<option value="date_desc" id="ss-date">↓ Datum</option>
|
||||||
|
<option value="name_asc" id="ss-name">A–Z Name</option>
|
||||||
|
<option value="duration_asc" id="ss-dur">⏱ Druckzeit</option>
|
||||||
|
</select>
|
||||||
|
</div>
|
||||||
|
<div id="store-upload-zone" onclick="document.getElementById('store-upload-input').click()"
|
||||||
|
ondragover="event.preventDefault();this.classList.add('drag-over')"
|
||||||
|
ondragleave="this.classList.remove('drag-over')"
|
||||||
|
ondrop="event.preventDefault();this.classList.remove('drag-over');uploadGcode(event.dataTransfer.files[0])">
|
||||||
|
<input type="file" id="store-upload-input" accept=".gcode,.bgcode"
|
||||||
|
style="display:none" onchange="uploadGcode(this.files[0]);this.value=''">
|
||||||
|
<span id="store-upload-icon">⬆</span>
|
||||||
|
<span id="store-upload-label"><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">
|
||||||
|
</div>
|
||||||
|
<div id="store-grid" style="display:grid;grid-template-columns:repeat(auto-fill,minmax(200px,1fr));gap:14px"></div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="panel" id="panel-console">
|
||||||
|
<div class="card">
|
||||||
|
<div class="card-title" style="display:flex;justify-content:space-between;align-items:center">
|
||||||
|
<span><span>≡</span> <span id="ptitle-console">Ereignis-Log</span></span>
|
||||||
|
<a id="btn-log-dl" href="/api/log/download" download
|
||||||
|
style="font-size:12px;padding:4px 10px;background:var(--raised);border-radius:6px;color:var(--txt2);text-decoration:none">⬇ Download</a>
|
||||||
|
</div>
|
||||||
|
<div style="display:flex;gap:6px;margin-bottom:6px;flex-wrap:wrap;align-items:center">
|
||||||
|
<input id="log-filter" type="text" placeholder="Filter…"
|
||||||
|
oninput="renderLog()"
|
||||||
|
style="flex:1;min-width:120px;padding:5px 10px;background:var(--raised);border:1px solid var(--border);border-radius:6px;color:var(--txt);font-size:12px;font-family:var(--mono)">
|
||||||
|
<button id="btn-autoscroll" onclick="toggleAutoScroll()"
|
||||||
|
style="font-size:12px;padding:5px 10px;border-radius:6px;border:1px solid var(--border);background:var(--accent);color:#fff;cursor:pointer;white-space:nowrap">⬇ Auto</button>
|
||||||
|
<button onclick="consoleLogs=[];renderLog()"
|
||||||
|
style="font-size:12px;padding:5px 10px;border-radius:6px;border:1px solid var(--border);background:var(--raised);color:var(--txt2);cursor:pointer">✕ Clear</button>
|
||||||
|
</div>
|
||||||
|
<div style="display:flex;gap:5px;margin-bottom:8px;flex-wrap:wrap">
|
||||||
|
<span style="font-size:11px;color:var(--txt2);align-self:center;margin-right:2px">Dir:</span>
|
||||||
|
<button class="log-dir-btn active" id="logdir-all" onclick="setLogDir('all')" style="font-size:11px;padding:3px 9px;border-radius:5px;border:1px solid var(--border);cursor:pointer"></button>
|
||||||
|
<button class="log-dir-btn" id="logdir-rx" onclick="setLogDir('rx')" style="font-size:11px;padding:3px 9px;border-radius:5px;border:1px solid var(--border);cursor:pointer">RX</button>
|
||||||
|
<button class="log-dir-btn" id="logdir-tx" onclick="setLogDir('tx')" style="font-size:11px;padding:3px 9px;border-radius:5px;border:1px solid var(--border);cursor:pointer">TX</button>
|
||||||
|
<span style="font-size:11px;color:var(--txt2);align-self:center;margin-left:6px;margin-right:2px" id="log-lbl-level">Level:</span>
|
||||||
|
<button class="log-lvl-btn active" id="loglvl-all" onclick="setLogLevel('all')" style="font-size:11px;padding:3px 9px;border-radius:5px;border:1px solid var(--border);cursor:pointer"></button>
|
||||||
|
<button class="log-lvl-btn" id="loglvl-err" onclick="setLogLevel('err')" style="font-size:11px;padding:3px 9px;border-radius:5px;border:1px solid var(--border);cursor:pointer">⛔ Errors</button>
|
||||||
|
<button class="log-lvl-btn" id="loglvl-warn" onclick="setLogLevel('warn')" style="font-size:11px;padding:3px 9px;border-radius:5px;border:1px solid var(--border);cursor:pointer">⚠ Warn</button>
|
||||||
|
<span style="font-size:11px;color:var(--txt2);align-self:center;margin-left:6px;margin-right:2px">Topic:</span>
|
||||||
|
<button class="log-topic-btn" data-topic="multiColorBox" onclick="setLogTopic('multiColorBox')" style="font-size:11px;padding:3px 9px;border-radius:5px;border:1px solid var(--border);cursor:pointer">AMS</button>
|
||||||
|
<button class="log-topic-btn" data-topic="print" onclick="setLogTopic('print')" style="font-size:11px;padding:3px 9px;border-radius:5px;border:1px solid var(--border);cursor:pointer">print</button>
|
||||||
|
<button class="log-topic-btn" data-topic="info" onclick="setLogTopic('info')" style="font-size:11px;padding:3px 9px;border-radius:5px;border:1px solid var(--border);cursor:pointer">info</button>
|
||||||
|
<button class="log-topic-btn" data-topic="status" onclick="setLogTopic('status')" style="font-size:11px;padding:3px 9px;border-radius:5px;border:1px solid var(--border);cursor:pointer">status</button>
|
||||||
|
</div>
|
||||||
|
<div class="console" id="console-log" style="height:calc(100vh - 260px);min-height:160px" onscroll="onLogScroll()"></div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</main>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<nav class="bottom-nav">
|
||||||
|
<button class="bnav-btn active" onclick="showPanel('dashboard')" id="bnb-dashboard"><span class="bnav-icon">⊞</span>Dashboard</button>
|
||||||
|
<button class="bnav-btn" onclick="showPanel('printers');loadPrinterTab()" id="bnb-printers"><span class="bnav-icon">🖨</span>Drucker</button>
|
||||||
|
<button class="bnav-btn" onclick="showPanel('store');loadStore()" id="bnb-store"><span class="bnav-icon">🗂</span>Browser</button>
|
||||||
|
<button class="bnav-btn" onclick="showPanel('console');clearLogBadge()" id="bnb-console"><span class="bnav-icon">≡</span>Log<span id="log-badge-bot" style="display:none;margin-left:3px;background:var(--err);color:#fff;border-radius:10px;font-size:10px;padding:1px 4px;font-weight:700"></span></button>
|
||||||
|
</nav>
|
||||||
|
|
||||||
|
|
||||||
|
<!-- 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">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">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>
|
||||||
|
|
||||||
|
<!-- Filament-Slot-Dialog -->
|
||||||
|
<div class="modal-overlay" id="filament-dialog" onclick="if(event.target===this)closeFilamentDialog()">
|
||||||
|
<div class="modal-box" style="max-width:380px;width:100%">
|
||||||
|
<div class="modal-header" style="margin-bottom:14px">
|
||||||
|
<span class="modal-title" id="fd-title" style="font-size:14px;word-break:break-all"></span>
|
||||||
|
<button onclick="closeFilamentDialog()" style="background:none;border:none;font-size:18px;cursor:pointer;color:var(--txt2)">✕</button>
|
||||||
|
</div>
|
||||||
|
<p id="fd-slots-hint" style="font-size:12px;color:var(--txt2);margin-bottom:10px">GCode-Kanal → AMS-Slot zuweisen:</p>
|
||||||
|
<div id="fd-slots" style="display:flex;flex-direction:column;gap:8px;margin-bottom:16px"></div>
|
||||||
|
<div id="fd-objects-section" style="display:none;margin-bottom:16px">
|
||||||
|
<p id="fd-objects-hint" style="font-size:12px;color:var(--txt2);margin-bottom:8px">Objekte überspringen (optional):</p>
|
||||||
|
<div id="fd-objects-svg" style="display:none;background:var(--raised);border:1px solid var(--border);border-radius:8px;padding:6px;margin-bottom:8px;text-align:center"></div>
|
||||||
|
<div id="fd-objects" style="display:flex;flex-direction:column;gap:6px;max-height:140px;overflow-y:auto"></div>
|
||||||
|
</div>
|
||||||
|
<div style="display:flex;gap:8px;justify-content:flex-end">
|
||||||
|
<button id="fd-cancel" onclick="closeFilamentDialog()" style="padding:8px 16px;background:var(--raised);border:1px solid var(--border);border-radius:8px;color:var(--txt);cursor:pointer">Abbrechen</button>
|
||||||
|
<button id="fd-print" onclick="confirmFilamentPrint()" style="padding:8px 18px;background:var(--accent);color:#fff;border:none;border-radius:8px;cursor:pointer;font-weight:600">▶ Drucken</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<!-- Drucker-hinzufügen-Dialog -->
|
||||||
|
<div class="modal-overlay" id="add-printer-dialog" onclick="if(event.target===this)closeAddPrinterDialog()">
|
||||||
|
<div class="modal-box" style="max-width:380px;width:100%">
|
||||||
|
<div class="modal-header" style="margin-bottom:14px">
|
||||||
|
<span class="modal-title" id="apd-title">Drucker hinzufügen</span>
|
||||||
|
<button onclick="closeAddPrinterDialog()" style="background:none;border:none;font-size:18px;cursor:pointer;color:var(--txt2)">✕</button>
|
||||||
|
</div>
|
||||||
|
<label id="apd-lbl-ip" style="display:block;font-size:12px;color:var(--txt2);margin-bottom:4px">Drucker-IP</label>
|
||||||
|
<input type="text" id="apd-ip" placeholder="192.168.1.100" 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:10px">
|
||||||
|
<label id="apd-lbl-name" style="display:block;font-size:12px;color:var(--txt2);margin-bottom:4px">Name (optional)</label>
|
||||||
|
<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 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>
|
||||||
|
</div>
|
||||||
|
<!-- Mid-Print Skip-Dialog -->
|
||||||
|
<div class="modal-overlay" id="skip-dialog" onclick="if(event.target===this)closeSkipDialog()">
|
||||||
|
<div class="modal-box" style="max-width:420px;width:100%">
|
||||||
|
<div class="modal-header" style="margin-bottom:14px">
|
||||||
|
<span class="modal-title" id="skip-title">✂ Objekte überspringen</span>
|
||||||
|
<button onclick="closeSkipDialog()" style="background:none;border:none;font-size:18px;cursor:pointer;color:var(--txt2)">✕</button>
|
||||||
|
</div>
|
||||||
|
<p id="skip-hint" style="font-size:12px;color:var(--txt2);margin-bottom:10px">Objekte abwählen, die nicht weiter gedruckt werden sollen:</p>
|
||||||
|
<div id="skip-svg" style="display:none;background:var(--raised);border:1px solid var(--border);border-radius:8px;padding:6px;margin-bottom:10px;text-align:center"></div>
|
||||||
|
<div id="skip-list" style="display:flex;flex-direction:column;gap:6px;max-height:200px;overflow-y:auto;margin-bottom:12px"></div>
|
||||||
|
<div id="skip-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 onclick="closeSkipDialog()" style="padding:8px 16px;background:var(--raised);border:1px solid var(--border);border-radius:8px;color:var(--txt);cursor:pointer">Abbrechen</button>
|
||||||
|
<button id="skip-confirm" onclick="confirmSkip()" style="padding:8px 18px;background:var(--accent);color:#fff;border:none;border-radius:8px;cursor:pointer;font-weight:600">Überspringen</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- ACE Dryer Temp/Time Settings Dialog -->
|
||||||
|
<div class="modal-overlay" id="ace-dry-dialog" onclick="if(event.target===this)closeAceDryDialog()">
|
||||||
|
<div class="modal-box" style="max-width:560px;width:100%">
|
||||||
|
<div class="modal-header" style="margin-bottom:10px">
|
||||||
|
<span class="modal-title" id="ace-dry-dialog-title">Dryer Temp/Time Settings</span>
|
||||||
|
<button onclick="closeAceDryDialog()" style="background:none;border:none;font-size:18px;cursor:pointer;color:var(--txt2)">✕</button>
|
||||||
|
</div>
|
||||||
|
<div style="display:flex;align-items:center;gap:12px;margin-bottom:8px">
|
||||||
|
<label id="ace-dry-dialog-temp-label" style="min-width:190px;font-size:12px;color:var(--txt)">Temperature (30-80°C)</label>
|
||||||
|
<input id="ace-dry-dialog-temp" type="number" min="30" max="80" step="1"
|
||||||
|
oninput="aceDryDialogInputsChanged()"
|
||||||
|
style="width:130px;padding:8px;background:var(--raised);border:1px solid var(--border);border-radius:6px;color:var(--txt);text-align:center" value="45">
|
||||||
|
</div>
|
||||||
|
<div style="display:flex;align-items:center;gap:12px;margin-bottom:16px">
|
||||||
|
<label id="ace-dry-dialog-time-label" style="min-width:190px;font-size:12px;color:var(--txt)">Rem. Time (h:m:s)</label>
|
||||||
|
<div style="display:flex;align-items:center;gap:8px">
|
||||||
|
<input id="ace-dry-dialog-h" type="number" min="0" max="24" step="1" value="4"
|
||||||
|
oninput="aceDryDialogInputsChanged()"
|
||||||
|
style="width:70px;padding:8px;background:var(--raised);border:1px solid var(--border);border-radius:6px;color:var(--txt);text-align:center">
|
||||||
|
<span style="color:var(--txt2)">:</span>
|
||||||
|
<input id="ace-dry-dialog-m" type="number" min="0" max="59" step="1" value="0"
|
||||||
|
oninput="aceDryDialogInputsChanged()"
|
||||||
|
style="width:70px;padding:8px;background:var(--raised);border:1px solid var(--border);border-radius:6px;color:var(--txt);text-align:center">
|
||||||
|
<span style="color:var(--txt2)">:</span>
|
||||||
|
<input id="ace-dry-dialog-s" type="number" min="0" max="59" step="1" value="0"
|
||||||
|
oninput="aceDryDialogInputsChanged()"
|
||||||
|
style="width:70px;padding:8px;background:var(--raised);border:1px solid var(--border);border-radius:6px;color:var(--txt);text-align:center">
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div style="display:grid;grid-template-columns:repeat(3,1fr);gap:8px;margin-bottom:8px">
|
||||||
|
<button class="ace-dry-preset-btn" data-preset="pla" onclick="aceDryDialogPreset('pla')" style="padding:8px;border-radius:8px;border:1px solid var(--border);background:var(--raised);color:var(--txt2);cursor:pointer">PLA</button>
|
||||||
|
<button class="ace-dry-preset-btn" data-preset="pla_plus" onclick="aceDryDialogPreset('pla_plus')" style="padding:8px;border-radius:8px;border:1px solid var(--border);background:var(--raised);color:var(--txt2);cursor:pointer">PLA+</button>
|
||||||
|
<button class="ace-dry-preset-btn" data-preset="petg" onclick="aceDryDialogPreset('petg')" style="padding:8px;border-radius:8px;border:1px solid var(--border);background:var(--raised);color:var(--txt2);cursor:pointer">PETG</button>
|
||||||
|
<button class="ace-dry-preset-btn" data-preset="tpu" onclick="aceDryDialogPreset('tpu')" style="padding:8px;border-radius:8px;border:1px solid var(--border);background:var(--raised);color:var(--txt2);cursor:pointer">TPU</button>
|
||||||
|
<button class="ace-dry-preset-btn" data-preset="abs_asa" onclick="aceDryDialogPreset('abs_asa')" style="padding:8px;border-radius:8px;border:1px solid var(--border);background:var(--raised);color:var(--txt2);cursor:pointer">ABS / ASA</button>
|
||||||
|
<button class="ace-dry-preset-btn" data-preset="pa_pc" onclick="aceDryDialogPreset('pa_pc')" style="padding:8px;border-radius:8px;border:1px solid var(--border);background:var(--raised);color:var(--txt2);cursor:pointer">PA / PC</button>
|
||||||
|
<button class="ace-dry-preset-btn" data-preset="custom_1" onclick="aceDryDialogPreset('custom_1')" style="padding:8px;border-radius:8px;border:1px solid var(--border);background:var(--raised);color:var(--txt2);cursor:pointer">Custom 1</button>
|
||||||
|
<button class="ace-dry-preset-btn" data-preset="custom_2" onclick="aceDryDialogPreset('custom_2')" style="padding:8px;border-radius:8px;border:1px solid var(--border);background:var(--raised);color:var(--txt2);cursor:pointer">Custom 2</button>
|
||||||
|
<button class="ace-dry-preset-btn" data-preset="custom_3" onclick="aceDryDialogPreset('custom_3')" style="padding:8px;border-radius:8px;border:1px solid var(--border);background:var(--raised);color:var(--txt2);cursor:pointer">Custom 3</button>
|
||||||
|
</div>
|
||||||
|
<div id="ace-dry-dialog-custom-name-row" style="display:none;align-items:center;gap:12px;margin-bottom:14px">
|
||||||
|
<label id="ace-dry-dialog-custom-name-label" style="min-width:190px;font-size:12px;color:var(--txt)">Custom Name</label>
|
||||||
|
<input id="ace-dry-dialog-custom-name" type="text" maxlength="32" oninput="aceDryDialogInputsChanged()"
|
||||||
|
style="width:220px;padding:8px;background:var(--raised);border:1px solid var(--border);border-radius:6px;color:var(--txt)">
|
||||||
|
</div>
|
||||||
|
<div style="display:flex;justify-content:flex-end;gap:8px">
|
||||||
|
<button id="ace-dry-dialog-reset-default" onclick="resetAceDryPresetToDefault()" style="display:none;padding:8px 14px;background:var(--raised);border:1px solid var(--border);border-radius:8px;color:var(--txt);cursor:pointer">Reset to Default</button>
|
||||||
|
<button id="ace-dry-dialog-save-preset" onclick="saveAceDryPresetAndRestart()" style="display:none;padding:8px 14px;background:var(--warn);border:1px solid transparent;border-radius:8px;color:#fff;cursor:pointer">Save & Restart</button>
|
||||||
|
<button id="ace-dry-dialog-cancel" onclick="closeAceDryDialog()" style="padding:8px 14px;background:var(--raised);border:1px solid var(--border);border-radius:8px;color:var(--txt);cursor:pointer">Cancel</button>
|
||||||
|
<button id="ace-dry-dialog-confirm" onclick="confirmAceDryDialog()" style="padding:8px 16px;background:var(--accent);color:#fff;border:none;border-radius:8px;cursor:pointer;font-weight:600">Confirm</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<script src="/kx/ui/app.js"></script></head>
|
||||||
|
<footer style="text-align:center;padding:12px;font-size:11px;color:var(--txt2);border-top:1px solid var(--border);margin-top:auto">
|
||||||
|
© ViewIT 2026
|
||||||
|
</footer>
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
315
web/themes/default/style.css
Normal file
315
web/themes/default/style.css
Normal file
@@ -0,0 +1,315 @@
|
|||||||
|
:root{
|
||||||
|
color-scheme:dark; /* native Form-Controls (select) im Webview dunkel rendern */
|
||||||
|
--bg:#1a1a1f;--card:#24242c;--raised:#2e2e3a;--border:#3a3a4a;
|
||||||
|
--txt:#f0f0f5;--txt2:#8888aa;--accent:#00c8ff;--accent2:#ff6b35;
|
||||||
|
--ok:#4cde80;--err:#ff4d6d;--warn:#ffb020;
|
||||||
|
--font:-apple-system,BlinkMacSystemFont,"Segoe UI",sans-serif;
|
||||||
|
--mono:"JetBrains Mono","Fira Code",monospace;
|
||||||
|
}
|
||||||
|
[data-theme=light]{
|
||||||
|
color-scheme:light;
|
||||||
|
--bg:#f0f0f5;--card:#fff;--raised:#e8e8f0;--border:#d0d0e0;
|
||||||
|
--txt:#1a1a2e;--txt2:#666680;
|
||||||
|
}
|
||||||
|
*{box-sizing:border-box;margin:0;padding:0}
|
||||||
|
body{background:var(--bg);color:var(--txt);font-family:var(--font);font-size:14px;min-height:100vh;display:flex;flex-direction:column}
|
||||||
|
a{color:var(--accent);text-decoration:none}
|
||||||
|
/* select/option-Farben explizit setzen — OrcaSlicers Device-Tab-Webview erbt
|
||||||
|
sie sonst nicht und rendert weiße Schrift auf weißem Grund (Issue #29). */
|
||||||
|
select{background:var(--raised)!important;color:var(--txt)!important}
|
||||||
|
select option{background:var(--card)!important;color:var(--txt)!important}
|
||||||
|
|
||||||
|
/* ── HEADER ── */
|
||||||
|
header{background:var(--card);border-bottom:1px solid var(--border);
|
||||||
|
display:flex;align-items:center;gap:12px;padding:0 20px;height:52px;
|
||||||
|
position:sticky;top:0;z-index:100}
|
||||||
|
.logo{font-size:18px;font-weight:700;color:var(--accent);letter-spacing:-.02em}
|
||||||
|
.hname{font-size:13px;color:var(--txt2)}
|
||||||
|
.hbadge{display:flex;align-items:center;gap:6px;font-size:12px;font-weight:600;
|
||||||
|
padding:4px 10px;border-radius:20px;background:var(--raised);color:var(--txt2);
|
||||||
|
text-transform:uppercase;letter-spacing:.04em}
|
||||||
|
.hbadge.printing{background:#0d2d1a;color:var(--ok)}
|
||||||
|
.hbadge.complete{background:#0d1f38;color:#60b0ff}
|
||||||
|
.hbadge.error{background:#2d0d0d;color:var(--err)}
|
||||||
|
.hbadge .dot{width:7px;height:7px;border-radius:50%;background:currentColor}
|
||||||
|
.hbadge.printing .dot{animation:pulse 1.4s infinite}
|
||||||
|
@keyframes pulse{0%,100%{opacity:1}50%{opacity:.25}}
|
||||||
|
.theme-btn{background:none;border:1px solid var(--border);color:var(--txt2);
|
||||||
|
border-radius:8px;padding:6px 10px;cursor:pointer;font-size:13px;transition:.15s}
|
||||||
|
.theme-btn:hover{border-color:var(--accent);color:var(--accent)}
|
||||||
|
.conn-btn{border-radius:8px;padding:6px 12px;cursor:pointer;font-size:13px;
|
||||||
|
font-weight:600;border:none;transition:.15s}
|
||||||
|
.conn-btn.disconnected{background:var(--accent);color:#fff}
|
||||||
|
.conn-btn.disconnected:hover{opacity:.85}
|
||||||
|
.conn-btn.connected{background:transparent;border:1px solid var(--border);color:var(--txt2)}
|
||||||
|
.conn-btn.connected:hover{border-color:#e05;color:#e05}
|
||||||
|
|
||||||
|
/* ── LAYOUT ── */
|
||||||
|
.layout{display:flex;flex:1;min-height:0}
|
||||||
|
nav.sidebar{width:200px;background:var(--card);border-right:1px solid var(--border);
|
||||||
|
display:flex;flex-direction:column;padding:12px 8px;gap:2px;flex-shrink:0}
|
||||||
|
.nav-btn{background:none;border:none;color:var(--txt2);text-align:left;
|
||||||
|
padding:9px 12px;border-radius:8px;cursor:pointer;font-size:13px;
|
||||||
|
display:flex;align-items:center;gap:10px;transition:.12s;width:100%}
|
||||||
|
.nav-btn:hover{background:var(--raised);color:var(--txt)}
|
||||||
|
.nav-btn.active{background:var(--raised);color:var(--accent)}
|
||||||
|
.nav-icon{font-size:16px;width:20px;text-align:center}
|
||||||
|
main{flex:1;overflow-y:auto;padding:20px}
|
||||||
|
.grid{display:grid;grid-template-columns:repeat(auto-fill,minmax(320px,1fr));gap:16px}
|
||||||
|
|
||||||
|
/* ── CARD ── */
|
||||||
|
.card{background:var(--card);border:1px solid var(--border);border-radius:12px;
|
||||||
|
padding:18px;transition:box-shadow .15s,transform .15s}
|
||||||
|
.card:hover{box-shadow:0 4px 20px rgba(0,0,0,.3);transform:translateY(-1px)}
|
||||||
|
.card-title{font-size:11px;text-transform:uppercase;letter-spacing:.1em;color:var(--txt2);
|
||||||
|
margin-bottom:14px;display:flex;align-items:center;gap:8px}
|
||||||
|
.card-title span{font-size:14px}
|
||||||
|
|
||||||
|
/* ── HERO ── */
|
||||||
|
.hero{grid-column:1/-1;display:grid;grid-template-columns:1fr 320px;gap:16px}
|
||||||
|
@media(max-width:900px){.hero{grid-template-columns:1fr}}
|
||||||
|
.cam-wrap{background:#0a0a0e;border-radius:10px;overflow:hidden;
|
||||||
|
min-height:180px;max-height:320px;display:flex;align-items:center;justify-content:center;position:relative}
|
||||||
|
.cam-wrap img,.cam-wrap video{width:100%;max-height:320px;height:auto;display:block;object-fit:contain}
|
||||||
|
.cam-placeholder{color:var(--txt2);font-size:13px;text-align:center;padding:20px}
|
||||||
|
@keyframes spin{to{transform:rotate(360deg)}}
|
||||||
|
.cam-spinner{width:40px;height:40px;border:3px solid rgba(255,255,255,.15);
|
||||||
|
border-top-color:var(--accent);border-radius:50%;animation:spin .8s linear infinite;display:none}
|
||||||
|
.cam-overlay{position:absolute;bottom:0;left:0;right:0;
|
||||||
|
background:linear-gradient(transparent,rgba(0,0,0,.75));padding:14px}
|
||||||
|
.cam-toggle{position:absolute;top:10px;right:10px;background:rgba(0,0,0,.5);
|
||||||
|
border:1px solid rgba(255,255,255,.2);color:#fff;border-radius:8px;
|
||||||
|
padding:6px 10px;cursor:pointer;font-size:12px;backdrop-filter:blur(4px)}
|
||||||
|
.cam-toggle:hover{background:rgba(0,0,0,.7)}
|
||||||
|
|
||||||
|
/* ── PROGRESS ── */
|
||||||
|
.hero-info{display:flex;flex-direction:column;gap:12px}
|
||||||
|
.pct-big{font-size:52px;font-weight:700;line-height:1;color:var(--txt)}
|
||||||
|
.pct-big small{font-size:20px;font-weight:400;color:var(--txt2)}
|
||||||
|
.progress-bar{height:8px;background:var(--raised);border-radius:4px;overflow:hidden;margin:4px 0}
|
||||||
|
.progress-fill{height:100%;background:linear-gradient(90deg,var(--accent),#0080cc);
|
||||||
|
border-radius:4px;transition:width .6s ease}
|
||||||
|
.meta-row{display:flex;justify-content:space-between;font-size:12px;color:var(--txt2)}
|
||||||
|
.layer-badge{background:var(--raised);border-radius:6px;padding:4px 8px;
|
||||||
|
font-family:var(--mono);font-size:12px;color:var(--txt)}
|
||||||
|
.fname{font-size:12px;color:var(--txt2);white-space:nowrap;overflow:hidden;text-overflow:ellipsis;
|
||||||
|
background:var(--raised);border-radius:6px;padding:6px 8px}
|
||||||
|
|
||||||
|
/* ── PRINT CONTROLS ── */
|
||||||
|
.ctrl-btns{display:flex;gap:8px;flex-wrap:wrap}
|
||||||
|
.btn{border:none;border-radius:8px;padding:10px 16px;font-size:13px;font-weight:600;
|
||||||
|
cursor:pointer;transition:opacity .15s,transform .1s;white-space:nowrap}
|
||||||
|
.btn:hover{opacity:.85;transform:translateY(-1px)}
|
||||||
|
.btn:active{transform:translateY(0)}
|
||||||
|
.btn-start{background:var(--ok);color:#0d2010}
|
||||||
|
.btn-pause{background:var(--raised);color:var(--txt);border:1px solid var(--border)}
|
||||||
|
.btn-resume{background:#0d2d1a;color:var(--ok);border:1px solid var(--ok)}
|
||||||
|
.btn-skip{background:var(--raised);color:var(--warn);border:1px solid var(--warn)}
|
||||||
|
.btn-cancel{background:#2d0d0d;color:var(--err);border:1px solid var(--err)}
|
||||||
|
.btn-accent{background:var(--accent);color:#001a24}
|
||||||
|
.btn-sm{padding:7px 12px;font-size:12px}
|
||||||
|
.spd-btn{flex:1;border:1.5px solid var(--border);background:var(--raised);color:var(--txt);
|
||||||
|
border-radius:10px;padding:14px 8px;font-size:13px;font-weight:600;cursor:pointer;
|
||||||
|
transition:all .15s;display:flex;flex-direction:column;align-items:center;gap:4px}
|
||||||
|
.spd-btn:hover{border-color:var(--accent);color:var(--accent)}
|
||||||
|
.spd-btn.spd-active{border-color:var(--accent);background:rgba(0,200,255,.12);color:var(--accent)}
|
||||||
|
.spd-btn .spd-icon{font-size:22px}
|
||||||
|
.spd-bar{height:4px;border-radius:2px;background:var(--border);margin-top:10px;overflow:hidden}
|
||||||
|
.spd-bar-fill{height:100%;border-radius:2px;background:linear-gradient(90deg,var(--accent2),var(--accent));transition:width .3s}
|
||||||
|
|
||||||
|
/* ── TIME CARDS ── */
|
||||||
|
.time-grid{display:grid;grid-template-columns:1fr 1fr 1fr;gap:8px;margin-top:8px}
|
||||||
|
.time-block{background:var(--raised);border-radius:10px;padding:10px 12px}
|
||||||
|
.time-label{font-size:10px;text-transform:uppercase;letter-spacing:.08em;color:var(--txt2);margin-bottom:4px}
|
||||||
|
.time-val{font-size:20px;font-weight:700;font-family:var(--mono);color:var(--txt)}
|
||||||
|
/* ── TEMPS ── */
|
||||||
|
.temp-pair{display:grid;grid-template-columns:1fr 1fr;gap:12px}
|
||||||
|
.temp-card-inner{display:grid;grid-template-columns:1fr 1fr;gap:12px}
|
||||||
|
.temp-block{background:var(--raised);border-radius:10px;padding:14px;position:relative}
|
||||||
|
.temp-label{font-size:11px;text-transform:uppercase;letter-spacing:.08em;color:var(--txt2);margin-bottom:6px}
|
||||||
|
.temp-row{display:flex;align-items:baseline;gap:6px}
|
||||||
|
.temp-val{font-size:30px;font-weight:700;font-family:var(--mono)}
|
||||||
|
.temp-unit{font-size:14px;color:var(--txt2)}
|
||||||
|
.temp-target{font-size:11px;color:var(--txt2);margin-top:2px}
|
||||||
|
.temp-arc{position:absolute;top:12px;right:12px}
|
||||||
|
.temp-edit{display:flex;gap:6px;margin-top:10px}
|
||||||
|
.temp-input{background:var(--bg);border:1px solid var(--border);color:var(--txt);
|
||||||
|
border-radius:6px;padding:5px 8px;font-size:13px;font-family:var(--mono);width:70px}
|
||||||
|
.temp-input:focus{outline:none;border-color:var(--accent)}
|
||||||
|
.chart-wrap{margin-top:12px}
|
||||||
|
canvas.tchart{width:100%;height:60px;display:block;border-radius:6px;background:var(--raised)}
|
||||||
|
|
||||||
|
/* ── MOTION ── */
|
||||||
|
.joypad{display:grid;grid-template-columns:repeat(3,52px);
|
||||||
|
grid-template-rows:repeat(3,52px);gap:6px;justify-content:center;margin:8px auto}
|
||||||
|
.joy{background:var(--raised);border:1px solid var(--border);color:var(--txt);
|
||||||
|
border-radius:10px;font-size:18px;cursor:pointer;transition:.12s;
|
||||||
|
display:flex;align-items:center;justify-content:center}
|
||||||
|
.joy:hover{background:var(--accent);color:#001a24;border-color:var(--accent)}
|
||||||
|
.joy:active{transform:scale(.93)}
|
||||||
|
.joy.home{font-size:14px;background:var(--bg)}
|
||||||
|
.step-btns{display:flex;gap:6px;justify-content:center;flex-wrap:wrap;margin-top:10px}
|
||||||
|
.step-btn{background:var(--raised);border:1px solid var(--border);color:var(--txt2);
|
||||||
|
border-radius:6px;padding:5px 10px;font-size:12px;cursor:pointer;transition:.12s}
|
||||||
|
.step-btn.active,.step-btn:hover{background:var(--accent);color:#001a24;border-color:var(--accent)}
|
||||||
|
.home-btns{display:flex;gap:6px;flex-wrap:wrap;margin-top:10px;justify-content:center}
|
||||||
|
|
||||||
|
/* ── AMS ── */
|
||||||
|
.ams-slots{display:flex;flex-direction:column;gap:12px}
|
||||||
|
.ams-box-group{}
|
||||||
|
.ams-box-label{font-size:11px;font-weight:700;color:var(--txt2);text-transform:uppercase;letter-spacing:.06em;margin-bottom:6px;padding-left:2px}
|
||||||
|
.ams-box-slots{display:grid;grid-template-columns:repeat(4,1fr);gap:8px}
|
||||||
|
.ams-slot{background:var(--raised);border-radius:10px;padding:10px;text-align:center;
|
||||||
|
border:2px solid transparent;transition:.2s;position:relative}
|
||||||
|
.ams-slot.active{border-color:var(--slot-color,var(--accent));
|
||||||
|
box-shadow:0 0 12px rgba(var(--slot-rgb,0,200,255),.3)}
|
||||||
|
.ams-slot.loaded{border-color:var(--ok)!important;
|
||||||
|
box-shadow:0 0 0 2px rgba(64,220,120,.35),0 0 14px rgba(64,220,120,.35)}
|
||||||
|
.ams-slot.loading{border-color:var(--ok)!important;animation:amsPulseGreen 1s ease-in-out infinite}
|
||||||
|
.ams-slot.unloading{border-color:var(--err)!important;animation:amsPulseRed 1s ease-in-out infinite}
|
||||||
|
@keyframes amsPulseGreen{0%{box-shadow:0 0 0 0 rgba(64,220,120,.55)}50%{box-shadow:0 0 0 4px rgba(64,220,120,.25),0 0 18px rgba(64,220,120,.45)}100%{box-shadow:0 0 0 0 rgba(64,220,120,.55)}}
|
||||||
|
@keyframes amsPulseRed{0%{box-shadow:0 0 0 0 rgba(230,80,80,.55)}50%{box-shadow:0 0 0 4px rgba(230,80,80,.25),0 0 18px rgba(230,80,80,.45)}100%{box-shadow:0 0 0 0 rgba(230,80,80,.55)}}
|
||||||
|
.ams-slot-bridge{display:flex;flex-direction:column;align-items:center;justify-content:center;gap:6px;
|
||||||
|
border:1px dashed var(--border);background:linear-gradient(180deg,rgba(255,255,255,.03),rgba(255,255,255,.01));
|
||||||
|
color:var(--txt2);min-height:106px}
|
||||||
|
.ams-slot-bridge .bridge-chip{width:58px;height:58px;border:1px solid rgba(255,255,255,.14);border-radius:50%;
|
||||||
|
display:flex;align-items:center;justify-content:center;background:rgba(255,255,255,.04);color:var(--txt2);
|
||||||
|
font-size:13px;font-weight:700;letter-spacing:.04em}
|
||||||
|
.slot-circle{width:36px;height:36px;border-radius:50%;margin:0 auto 6px;border:2px solid rgba(255,255,255,.15)}
|
||||||
|
.slot-label{font-size:11px;color:var(--txt2);font-family:var(--mono)}
|
||||||
|
.slot-material{font-size:12px;font-weight:600;margin-bottom:2px}
|
||||||
|
|
||||||
|
/* ── LIGHT + FAN ── */
|
||||||
|
.toggle-row{display:flex;align-items:center;justify-content:space-between;margin-bottom:14px}
|
||||||
|
.toggle-label{font-size:13px;font-weight:600}
|
||||||
|
.toggle{position:relative;width:44px;height:24px;cursor:pointer}
|
||||||
|
.toggle input{opacity:0;width:0;height:0;position:absolute}
|
||||||
|
.toggle-track{width:44px;height:24px;background:var(--raised);border-radius:12px;
|
||||||
|
border:1px solid var(--border);transition:.25s;display:block}
|
||||||
|
.toggle input:checked+.toggle-track{background:var(--accent)}
|
||||||
|
.toggle-thumb{position:absolute;top:3px;left:3px;width:18px;height:18px;
|
||||||
|
background:#fff;border-radius:50%;transition:.25s;pointer-events:none}
|
||||||
|
.toggle input:checked~.toggle-thumb{transform:translateX(20px)}
|
||||||
|
.slider-row{display:flex;align-items:center;gap:10px;margin-top:8px}
|
||||||
|
.slider-label{font-size:12px;color:var(--txt2);width:80px}
|
||||||
|
.slider{flex:1;-webkit-appearance:none;height:4px;border-radius:2px;
|
||||||
|
background:var(--raised);outline:none;cursor:pointer}
|
||||||
|
.slider::-webkit-slider-thumb{-webkit-appearance:none;width:16px;height:16px;
|
||||||
|
border-radius:50%;background:var(--accent);cursor:pointer;transition:.1s}
|
||||||
|
.slider::-webkit-slider-thumb:hover{transform:scale(1.2)}
|
||||||
|
.slider-val{font-family:var(--mono);font-size:12px;color:var(--txt);width:30px;text-align:right}
|
||||||
|
|
||||||
|
/* ── CONSOLE ── */
|
||||||
|
.console{background:#0a0a0e;border-radius:8px;padding:10px;font-family:var(--mono);
|
||||||
|
font-size:11px;color:#8888aa;overflow-y:auto;line-height:1.6}
|
||||||
|
.console .ts{color:#444;margin-right:6px}
|
||||||
|
.console .msg-info{color:#8888aa}
|
||||||
|
.console .msg-ok{color:var(--ok)}
|
||||||
|
.console .msg-err{color:var(--err)}
|
||||||
|
.console .msg-warn{color:var(--warn)}
|
||||||
|
|
||||||
|
/* ── PANELS ── */
|
||||||
|
.panel{display:none}
|
||||||
|
.panel.active{display:block}
|
||||||
|
|
||||||
|
/* ── FILE BROWSER UPLOAD ZONE ── */
|
||||||
|
#store-upload-zone{
|
||||||
|
display:flex;flex-direction:column;align-items:center;justify-content:center;
|
||||||
|
gap:6px;padding:18px 12px;margin-bottom:14px;
|
||||||
|
border:2px dashed var(--border);border-radius:10px;
|
||||||
|
background:var(--raised);color:var(--txt2);
|
||||||
|
cursor:pointer;transition:border-color .15s,background .15s;
|
||||||
|
font-size:13px;text-align:center;user-select:none;
|
||||||
|
}
|
||||||
|
#store-upload-zone:hover{border-color:var(--accent);background:rgba(0,200,255,.06);color:var(--txt)}
|
||||||
|
#store-upload-zone.drag-over{border-color:var(--accent);background:rgba(0,200,255,.12);color:var(--accent)}
|
||||||
|
#store-upload-icon{font-size:22px;line-height:1}
|
||||||
|
.upload-status-busy{color:var(--txt2)}
|
||||||
|
.upload-status-ok{color:var(--ok)}
|
||||||
|
.upload-status-err{color:var(--err)}
|
||||||
|
|
||||||
|
/* ── MODAL ── */
|
||||||
|
.modal-overlay{display:none;position:fixed;inset:0;background:rgba(0,0,0,.6);
|
||||||
|
z-index:200;align-items:center;justify-content:center;padding:16px}
|
||||||
|
.modal-overlay.open{display:flex}
|
||||||
|
.modal-box{background:var(--card);border:1px solid var(--border);border-radius:14px;
|
||||||
|
width:100%;max-width:480px;max-height:90vh;overflow-y:auto;padding:24px;
|
||||||
|
display:flex;flex-direction:column;gap:18px}
|
||||||
|
.modal-header{display:flex;align-items:center;justify-content:space-between}
|
||||||
|
.modal-title{font-size:15px;font-weight:700;color:var(--txt)}
|
||||||
|
.modal-close{background:none;border:none;color:var(--txt2);font-size:20px;
|
||||||
|
cursor:pointer;padding:4px 8px;border-radius:6px}
|
||||||
|
.modal-close:hover{background:var(--raised);color:var(--txt)}
|
||||||
|
.modal-section{font-size:10px;text-transform:uppercase;letter-spacing:.1em;
|
||||||
|
color:var(--txt2);margin-bottom:6px;margin-top:4px}
|
||||||
|
.modal-field{display:flex;flex-direction:column;gap:4px;margin-bottom:10px}
|
||||||
|
.modal-field label{font-size:12px;color:var(--txt2)}
|
||||||
|
.modal-field input{background:var(--raised);border:1px solid var(--border);
|
||||||
|
border-radius:7px;color:var(--txt);padding:7px 10px;font-size:13px;width:100%}
|
||||||
|
.modal-field input:focus{outline:none;border-color:var(--accent)}
|
||||||
|
.poll-btns{display:flex;gap:8px}
|
||||||
|
.poll-btn{flex:1;padding:7px;background:var(--raised);border:1px solid var(--border);
|
||||||
|
border-radius:7px;color:var(--txt2);cursor:pointer;font-size:13px;transition:all .15s}
|
||||||
|
.poll-btn.active{background:var(--accent);border-color:var(--accent);color:#000;font-weight:600}
|
||||||
|
.update-row{display:flex;align-items:center;gap:10px;flex-wrap:wrap}
|
||||||
|
.update-status{font-size:12px;color:var(--txt2);flex:1;min-width:0}
|
||||||
|
.modal-save{width:100%;padding:10px;background:var(--accent);border:none;
|
||||||
|
border-radius:8px;color:#000;font-weight:700;font-size:14px;cursor:pointer;margin-top:4px}
|
||||||
|
.modal-save:hover{opacity:.88}
|
||||||
|
|
||||||
|
/* ── BOTTOM NAV (mobile) ── */
|
||||||
|
nav.bottom-nav{display:none;position:fixed;bottom:0;left:0;right:0;
|
||||||
|
background:var(--card);border-top:1px solid var(--border);
|
||||||
|
justify-content:space-around;padding:8px 0 max(8px,env(safe-area-inset-bottom))}
|
||||||
|
.bnav-btn{background:none;border:none;color:var(--txt2);display:flex;
|
||||||
|
flex-direction:column;align-items:center;gap:3px;cursor:pointer;font-size:10px;padding:4px 8px}
|
||||||
|
.bnav-btn.active{color:var(--accent)}
|
||||||
|
.bnav-icon{font-size:20px}
|
||||||
|
|
||||||
|
/* ── Tablet (769–1100px): schmale Sidebar ── */
|
||||||
|
@media(min-width:769px) and (max-width:1100px){
|
||||||
|
nav.sidebar{width:52px;padding:12px 4px}
|
||||||
|
.nav-btn .nav-text{display:none}
|
||||||
|
.nav-btn{justify-content:center;padding:10px}
|
||||||
|
.nav-icon{width:auto}
|
||||||
|
.grid{grid-template-columns:repeat(2,1fr)}
|
||||||
|
.hero{grid-template-columns:1fr}
|
||||||
|
}
|
||||||
|
|
||||||
|
/* ── Mobile (≤768px): Bottom-Nav, 1-Spalte ── */
|
||||||
|
@media(max-width:768px){
|
||||||
|
nav.sidebar{display:none}
|
||||||
|
nav.bottom-nav{display:flex}
|
||||||
|
main{padding:10px;padding-bottom:72px}
|
||||||
|
|
||||||
|
/* Header kompakt */
|
||||||
|
header{padding:0 12px;gap:8px}
|
||||||
|
.hname{display:none}
|
||||||
|
|
||||||
|
/* 1-Spalten-Grid, full-width spans funktionieren weiterhin */
|
||||||
|
.grid{grid-template-columns:1fr;gap:12px}
|
||||||
|
|
||||||
|
/* Hero: Kamera über Info */
|
||||||
|
.hero{grid-template-columns:1fr}
|
||||||
|
.cam-wrap{max-height:220px}
|
||||||
|
|
||||||
|
/* Temp-Pair und Temp-Card übereinander */
|
||||||
|
.temp-pair{grid-template-columns:1fr}
|
||||||
|
.temp-card-inner{grid-template-columns:1fr}
|
||||||
|
|
||||||
|
/* AMS: 2 Spalten auf kleinen Screens */
|
||||||
|
.ams-box-slots{grid-template-columns:repeat(2,1fr)}
|
||||||
|
|
||||||
|
/* Joypad etwas kleiner */
|
||||||
|
.joypad{grid-template-columns:repeat(3,44px);grid-template-rows:repeat(3,44px);gap:5px}
|
||||||
|
.joy{font-size:16px}
|
||||||
|
|
||||||
|
/* Buttons größere Touch-Targets */
|
||||||
|
.btn{padding:10px 14px;font-size:13px}
|
||||||
|
.btn-sm{padding:8px 12px}
|
||||||
|
.step-btn{padding:8px 12px;font-size:13px}
|
||||||
|
|
||||||
|
/* Modal vollbreite auf kleinen Screens */
|
||||||
|
.modal-box{padding:16px;border-radius:10px}
|
||||||
|
.poll-btns{gap:6px}
|
||||||
|
}
|
||||||
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": "Offline",
|
||||||
|
"nav_dashboard": "Panel",
|
||||||
|
"nav_print": "Impresion",
|
||||||
|
"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 impresion",
|
||||||
|
"card_cam": "Camara",
|
||||||
|
"lbl_elapsed": "Transcurrido:",
|
||||||
|
"lbl_remaining": "Restante:",
|
||||||
|
"lbl_slicer_time": "Estimacion del slicer:",
|
||||||
|
"lbl_layers": "Layer",
|
||||||
|
"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": "Duracion (min)",
|
||||||
|
"ace_dry_start": "▶ Start",
|
||||||
|
"ace_dry_stop": "■ Stop",
|
||||||
|
"ace_dry_auto_refill": "Relleno automatico",
|
||||||
|
"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 despues)",
|
||||||
|
"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 por defecto",
|
||||||
|
"cam_placeholder": "📷 Camara no iniciada",
|
||||||
|
"cam_stream_unavailable": "Stream no disponible",
|
||||||
|
"btn_cam_start": "▶ Camara",
|
||||||
|
"btn_cam_stop": "◼ Camara",
|
||||||
|
"btn_pause": "⏸ Pause",
|
||||||
|
"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 impresion",
|
||||||
|
"panel_print_btn_pause": "⏸ Pause",
|
||||||
|
"panel_print_btn_resume": "▶ Reanudar",
|
||||||
|
"panel_print_btn_cancel": "✕ Cancelar",
|
||||||
|
"panel_print_temps_live": "Temperaturas (en vivo)",
|
||||||
|
"label_set": "Set",
|
||||||
|
"label_off": "Off",
|
||||||
|
"panel_temps_nozzle": "Boquilla",
|
||||||
|
"panel_temps_bed": "Cama caliente",
|
||||||
|
"panel_temps_chart": "Historial (ultimas 60 lecturas)",
|
||||||
|
"label_target_c": "Objetivo:",
|
||||||
|
"panel_motion_xy": "Ejes XY",
|
||||||
|
"panel_motion_z": "Eje Z",
|
||||||
|
"label_step": "Tamano 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": "Vacio",
|
||||||
|
"panel_extras_light": "Luz",
|
||||||
|
"panel_extras_fan": "Ventilador",
|
||||||
|
"panel_extras_camera": "Camara",
|
||||||
|
"btn_cam_start2": "▶ Start",
|
||||||
|
"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": "Camara iniciada:",
|
||||||
|
"log_cam_stop": "Camara detenida",
|
||||||
|
"log_poll_error": "Error de sondeo:",
|
||||||
|
"log_error": "Error:",
|
||||||
|
"confirm_cancel": "Realmente cancelar la impresion?",
|
||||||
|
"settings_title": "Configuracion",
|
||||||
|
"settings_connection": "Conexion",
|
||||||
|
"settings_print": "Ajustes de impresion",
|
||||||
|
"settings_poll": "Intervalo de sondeo",
|
||||||
|
"settings_version": "Version",
|
||||||
|
"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": "Contrasena MQTT",
|
||||||
|
"settings_device_id": "ID del dispositivo",
|
||||||
|
"settings_mode_id": "Mode ID",
|
||||||
|
"hint_ip_no_port": "Solo direccion 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 camara al iniciar impresion",
|
||||||
|
"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 conexion:",
|
||||||
|
"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": "Level:",
|
||||||
|
"file_ready_btn": "▶ Iniciar impresion",
|
||||||
|
"file_slots_btn": "🎨 Seleccionar ranuras",
|
||||||
|
"file_cancel_btn": "✕ Cancelar",
|
||||||
|
"nav_printers": "Impresoras",
|
||||||
|
"skip_title": "✂ Omitir objetos",
|
||||||
|
"skip_hint": "Desmarca objetos que ya no quieras imprimir:",
|
||||||
|
"skip_btn_label": "Objetos",
|
||||||
|
"skip_no_objects": "No hay objetos en esta impresion.",
|
||||||
|
"skip_already": "omitido",
|
||||||
|
"skip_select_at_least_one": "Elige al menos un objeto.",
|
||||||
|
"skip_sending": "Enviando …",
|
||||||
|
"skip_success": "Se omitiran 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 impresion de todos modos?",
|
||||||
|
"fd_slot": "Ranura",
|
||||||
|
"fd_no_matching_material": "No hay material compatible",
|
||||||
|
"fd_used": "USADO",
|
||||||
|
"add_printer": "Agregar 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": "Agregar",
|
||||||
|
"apd_fetching": "Obteniendo datos de la impresora…",
|
||||||
|
"apd_success": "Impresora agregada, reiniciando bridge…",
|
||||||
|
"apd_err_ip": "Introduce una direccion IP",
|
||||||
|
"printers_remove": "Eliminar impresora",
|
||||||
|
"printers_remove_confirm": "Eliminar impresora \"{name}\"? El bridge se reiniciara.",
|
||||||
|
"printers_active": "● activa",
|
||||||
|
"printers_switch": "Cambiar →",
|
||||||
|
"printers_current": "Impresora actual",
|
||||||
|
"printers_loading": "Cargando…",
|
||||||
|
"printers_none": "No hay impresoras configuradas.",
|
||||||
|
"printers_empty_hint": "Aun no hay impresora configurada.",
|
||||||
|
"nav_browser": "Explorador",
|
||||||
|
"panel_browser_title": "Explorador de archivos",
|
||||||
|
"store_search_placeholder": "🔍 Buscar…",
|
||||||
|
"store_empty": "Aun 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": "Estimacion",
|
||||||
|
"store_upload_label_prefix": "Arrastra GCode aqui 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 impresion"
|
||||||
|
}
|
||||||
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