forked from viewit/KX-Bridge-Release
Merge branch 'nightly_plus_notifs' into master_plus_notifs
This commit is contained in:
30
.editorconfig
Normal file
30
.editorconfig
Normal file
@ -0,0 +1,30 @@
|
||||
# EditorConfig helps maintain consistent coding styles across all files
|
||||
# https://editorconfig.org
|
||||
|
||||
root = true
|
||||
|
||||
# Unix-style newlines, UTF-8 encoding for all files
|
||||
[*]
|
||||
end_of_line = lf
|
||||
charset = utf-8
|
||||
insert_final_newline = true
|
||||
trim_trailing_whitespace = true
|
||||
|
||||
# Python: 4 spaces (PEP 8)
|
||||
[*.py]
|
||||
indent_style = space
|
||||
indent_size = 4
|
||||
|
||||
# JavaScript/JSON/YAML: 2 spaces (common web standard)
|
||||
[*.{js,json,yml,yaml}]
|
||||
indent_style = space
|
||||
indent_size = 2
|
||||
|
||||
# HTML/CSS: 2 spaces
|
||||
[*.{html,css}]
|
||||
indent_style = space
|
||||
indent_size = 2
|
||||
|
||||
# Markdown: preserve formatting
|
||||
[*.md]
|
||||
trim_trailing_whitespace = false
|
||||
2
.gitignore
vendored
2
.gitignore
vendored
@ -6,6 +6,8 @@ dist/
|
||||
*.spec
|
||||
releases/
|
||||
|
||||
node_modules/*
|
||||
|
||||
!kx-bridge.spec
|
||||
|
||||
# Laufzeit-Daten und Drucker-Credentials — nie committen
|
||||
|
||||
11
.prettierrc.json
Normal file
11
.prettierrc.json
Normal file
@ -0,0 +1,11 @@
|
||||
{
|
||||
"jsonRecursiveSort": false,
|
||||
"jsonSortOrder": "{\"/.*/\": \"lexical\"}",
|
||||
"plugins": ["prettier-plugin-sort-json"],
|
||||
"printWidth": 100,
|
||||
"semi": true,
|
||||
"singleQuote": false,
|
||||
"tabWidth": 2,
|
||||
"trailingComma": "es5",
|
||||
"useTabs": false
|
||||
}
|
||||
161
CLAUDE.md
Normal file
161
CLAUDE.md
Normal file
@ -0,0 +1,161 @@
|
||||
# CLAUDE.md
|
||||
|
||||
This file provides guidance to Claude Code (claude.ai/code) when working with code in this repository.
|
||||
|
||||
## Project Overview
|
||||
|
||||
KX-Bridge is a Python 3.11+ bridge that emulates the Klipper/Moonraker API to enable OrcaSlicer to control Anycubic Kobra X printers in LAN mode — no Klipper, no Raspberry Pi required. It reverse-engineers the Anycubic MQTT protocol to relay commands from slicers to the printer.
|
||||
|
||||
## Running
|
||||
|
||||
**Docker (recommended):**
|
||||
```bash
|
||||
docker compose up -d
|
||||
docker compose logs -f
|
||||
```
|
||||
|
||||
**Python directly:**
|
||||
```bash
|
||||
pip install -r requirements.txt
|
||||
python kobrax_moonraker_bridge.py --printer-ip 192.168.x.x
|
||||
```
|
||||
|
||||
**Build binary:**
|
||||
```bash
|
||||
pyinstaller kx-bridge.spec
|
||||
```
|
||||
|
||||
No test suite exists in this repository.
|
||||
|
||||
## Architecture
|
||||
|
||||
The system bridges three parties:
|
||||
|
||||
```
|
||||
OrcaSlicer / Browser
|
||||
↕ HTTP + WebSocket (port 7125+, Moonraker API)
|
||||
KX-Bridge (kobrax_moonraker_bridge.py)
|
||||
↕ MQTT over TLS (port 9883, Anycubic LAN protocol)
|
||||
Anycubic Kobra X Printer
|
||||
```
|
||||
|
||||
### Core Files
|
||||
|
||||
- [kobrax_moonraker_bridge.py](kobrax_moonraker_bridge.py) — Main server (~5,000 lines). Houses `KobraXBridge` (HTTP/WebSocket handlers, state management), `GCodeStore` (SQLite-backed file/print-history storage), and `CameraCache` (MJPEG stream caching). This is where all Moonraker API endpoints are implemented.
|
||||
- [kobrax_client.py](kobrax_client.py) — Low-level MQTT client for the Kobra X. Handles raw MQTT 3.1.1 framing, TLS certificate auth, topic subscriptions, and message dispatch.
|
||||
- [config_loader.py](config_loader.py) — Loads `config/config.ini` (primary); [env_loader.py](env_loader.py) handles `.env` fallback. Also exposes `list_notification_urls()`, `list_printers()`, `list_filament_profiles()`.
|
||||
- [fetch_credentials.py](fetch_credentials.py) — Standalone tool to pull printer credentials over HTTP from the Kobra X.
|
||||
- [extract_credentials.py](extract_credentials.py) — Reads credentials from a running AnycubicSlicerNext process (Windows/Linux).
|
||||
|
||||
### Web UI
|
||||
|
||||
Single-page app served by the bridge itself:
|
||||
- [web/themes/default/index.html](web/themes/default/index.html) — Shell HTML
|
||||
- [web/themes/default/app.js](web/themes/default/app.js) — All client-side logic (~2,500 lines): WebSocket handling, state rendering, UI events
|
||||
- [web/themes/default/style.css](web/themes/default/style.css) — Dark/light theme via CSS variables
|
||||
- [web/translations/](web/translations/) — i18n strings (DE, EN, ES, ZH-CN)
|
||||
|
||||
When adding UI text, all four translation files must be updated. The hint element for notifications uses `innerHTML` (not `textContent`) to support links — follow the same pattern used for `orca_profile_help_html` when translation values contain HTML.
|
||||
|
||||
The theme system is documented in [web/DOC/THEME-CSS-HOOKS.md](web/DOC/THEME-CSS-HOOKS.md) and [web/DOC/THEME-JS-ID-HOOKS.md](web/DOC/THEME-JS-ID-HOOKS.md).
|
||||
|
||||
### Configuration
|
||||
|
||||
Primary config: `config/config.ini` (copy from `config/config.ini.example`). Sections:
|
||||
- `[connection]` — Printer IP, MQTT credentials, device/mode IDs
|
||||
- `[printer_N]` — One section per printer for multi-printer setups (ports 7125–7130)
|
||||
- `[print]` — AMS slots, auto-leveling, camera
|
||||
- `[filament_profiles]` — Per-slot filament for AMS
|
||||
- `[bridge]` — Poll interval (1–5 s, default 3 s), printer name
|
||||
- `[notifications]` — Apprise notification URLs and interval settings (see below)
|
||||
|
||||
### Data Storage
|
||||
|
||||
- **SQLite** at `data/kx-bridge.db` — G-code metadata, print history, thumbnails (base64)
|
||||
- **`data/orca_filaments.json`** — OrcaSlicer filament database, loaded at startup
|
||||
|
||||
### Key Protocol Details
|
||||
|
||||
- **MQTT auth:** AES-256-CBC encrypted credentials + TLS certificates (`anycubic_slicer.crt/key` bundled via PyInstaller)
|
||||
- **Moonraker emulation:** Kobra states are mapped to Klipper states via `KOBRA_TO_KLIPPER_STATE` in `kobrax_moonraker_bridge.py`
|
||||
- **Real-time logs:** Streamed to the browser via Server-Sent Events (SSE), not WebSocket
|
||||
- **Multi-printer:** Each printer runs its own `KobraXBridge` instance on a separate port
|
||||
|
||||
### PyInstaller Build
|
||||
|
||||
[kx-bridge.spec](kx-bridge.spec) bundles the web UI, filament database, and Anycubic TLS certificates into a single binary. The `web/` tree maps to `static/` inside the binary. Both `pycryptodome` and `apprise` use `collect_all()` to capture dynamically loaded plugins.
|
||||
|
||||
### Notification System (Apprise)
|
||||
|
||||
Push notifications are sent via the [apprise](https://github.com/caronc/apprise) library (supports 60+ services via URL syntax: `discord://`, `telegram://`, `pover://`, `gotify://`, `slack://`, etc.).
|
||||
|
||||
**Config format** (`[notifications]` section):
|
||||
```ini
|
||||
url_1 = discord://webhook_id/webhook_token
|
||||
events_1 = started,finished,failed,cancelled,paused,progress
|
||||
image_1 = false
|
||||
url_2 = pover://USERKEY@TOKEN
|
||||
events_2 = finished,failed
|
||||
image_2 = true
|
||||
notify_every_minutes = 10
|
||||
notify_every_layers = 0
|
||||
```
|
||||
|
||||
**Key implementation points:**
|
||||
- `KobraXBridge._notify(event, filename)` — fires on state transitions detected in `_on_print()`. Splits matching URLs into plain and image groups; image group attaches a temp `.jpg` from `camera_cache.latest_jpeg` via `apprise.AppriseAttachment`.
|
||||
- `KobraXBridge._check_progress_notifications()` — called from `_poll_loop()` every poll tick; fires `progress` event when the time or layer threshold is crossed. Both counters are reset together when either fires, and are also reset when a new print starts.
|
||||
- `_prev_kobra_state` guards against duplicate notifications when the printer repeatedly reports the same state.
|
||||
- All notification dispatches run in `threading.Thread(daemon=True)` to avoid blocking the MQTT reader thread or the event loop.
|
||||
- The test endpoint (`POST /api/notifications/test`) runs apprise synchronously via `run_in_executor`.
|
||||
|
||||
**Supported events:** `started`, `finished`, `failed`, `cancelled`, `paused`, `progress`
|
||||
|
||||
**Settings UI:** Managed through the WebUI settings modal. Each URL entry has per-event checkboxes, a `📷 Image` toggle, and a Test button. Global repeat interval fields (minutes / layers) apply to all URLs subscribed to the `progress` event.
|
||||
|
||||
## Android App
|
||||
|
||||
A companion Android app lives in [android/](android/). It connects to a running KX-Bridge server and provides a mobile printer control panel.
|
||||
|
||||
### Building
|
||||
|
||||
```bash
|
||||
cd android
|
||||
./gradlew assembleDebug # APK at app/build/outputs/apk/debug/
|
||||
./gradlew assembleRelease # Minified release APK
|
||||
```
|
||||
|
||||
- **Min SDK:** 26 (Android 8.0), **Compile/Target SDK:** 34
|
||||
- **Language:** Kotlin 2.0.0 + Jetpack Compose (Material3)
|
||||
- **Java toolchain:** 17
|
||||
- No test suite exists.
|
||||
|
||||
### Architecture
|
||||
|
||||
```
|
||||
android/app/src/main/java/com/kxbridge/
|
||||
├── MainActivity.kt # Entry point; reads SharedPreferences for server URL
|
||||
├── data/
|
||||
│ ├── PrinterRepository.kt # OkHttp3 HTTP client; polls /api/state every 3 s
|
||||
│ └── model/PrinterState.kt # kotlinx-serialization data model (snake_case JSON)
|
||||
├── viewmodel/PrinterViewModel.kt # StateFlow<UiState> (Loading / Success / Error)
|
||||
└── ui/
|
||||
├── PrinterScreen.kt # Main control UI: temps, progress, pause/resume/cancel
|
||||
└── SetupScreen.kt # First-run server URL entry form
|
||||
```
|
||||
|
||||
`MainActivity` stores the server URL in SharedPreferences (`"kxbridge"` / key `"server_url"`) and switches between `SetupScreen` and `PrinterScreen` based on whether a URL is configured.
|
||||
|
||||
`PrinterViewModel` drives all network interaction: it holds a `PrinterRepository`, exposes a `StateFlow<UiState>`, and exposes command methods (`pause`, `resume`, `cancel`). `UiState` is a sealed interface.
|
||||
|
||||
### Communication with KX-Bridge
|
||||
|
||||
The app uses plain HTTP (cleartext allowed in the manifest):
|
||||
|
||||
| Method | Endpoint | Purpose |
|
||||
|--------|----------|---------|
|
||||
| GET | `/api/state` | Fetch `PrinterState` JSON (polled every 3 s) |
|
||||
| POST | `/printer/print/pause` | Pause active print |
|
||||
| POST | `/printer/print/resume` | Resume paused print |
|
||||
| POST | `/printer/print/cancel` | Cancel print |
|
||||
|
||||
`PrinterState` fields use `@SerialName` for snake_case JSON keys. ProGuard is configured to keep `com.kxbridge.data.model.**` to prevent serialization breakage in release builds.
|
||||
@ -194,7 +194,6 @@ def list_printers() -> list[dict]:
|
||||
idx += 1
|
||||
return printers
|
||||
|
||||
|
||||
def _filament_section(printer_id: Optional[str] = None) -> str:
|
||||
"""Section name holding a printer's filament-profile mapping.
|
||||
|
||||
@ -207,19 +206,14 @@ def _filament_section(printer_id: Optional[str] = None) -> str:
|
||||
if pid and pid != "0":
|
||||
return f"filament_profiles_{pid}"
|
||||
return "filament_profiles"
|
||||
|
||||
def list_filament_profiles() -> dict[int, dict]:
|
||||
"""Liest die [filament_profiles]-Sektion aus config.ini.
|
||||
|
||||
|
||||
def list_filament_profiles(printer_id: Optional[str] = None) -> dict[int, dict]:
|
||||
"""Reads the [filament_profiles] section from config.ini.
|
||||
|
||||
With ``printer_id`` set, reads the per-printer ``[filament_profiles_<id>]``
|
||||
section and falls back to the legacy global ``[filament_profiles]`` while
|
||||
that printer has no own section yet.
|
||||
|
||||
Format per AMS slot - the primary selector is (vendor, name); the `id` is
|
||||
looked up from orca_filaments.json on save and carried along
|
||||
(as a hint for OrcaSlicer; the Orca data model has ~136 profiles sharing
|
||||
the same filament_id like 'OGFL99', i.e. the ID is not unique):
|
||||
Format pro AMS-Slot — primärer Selector ist (vendor, name), die `id` wird
|
||||
aus der orca_filaments.json beim Speichern nachgeschlagen und mitgeführt
|
||||
(als Hint für OrcaSlicer; das Orca-Datenmodell hat ~136 Profile mit
|
||||
derselben filament_id wie 'OGFL99', d.h. die ID ist nicht eindeutig):
|
||||
|
||||
[filament_profiles]
|
||||
slot_0_vendor = Polymaker
|
||||
|
||||
30
eslint.config.js
Normal file
30
eslint.config.js
Normal file
@ -0,0 +1,30 @@
|
||||
import js from "@eslint/js";
|
||||
import globals from "globals";
|
||||
import prettier from "eslint-config-prettier";
|
||||
|
||||
export default [
|
||||
{
|
||||
ignores: ["node_modules/**", "data/**", "releases/**", "android/**", "web/translations/**"],
|
||||
},
|
||||
js.configs.recommended,
|
||||
{
|
||||
// Browser-side single-page app served by the bridge. Loaded as a classic
|
||||
// (non-module) script, so all top-level functions/vars share one global scope.
|
||||
files: ["web/**/*.js"],
|
||||
languageOptions: {
|
||||
ecmaVersion: 2022,
|
||||
sourceType: "script",
|
||||
globals: {
|
||||
...globals.browser,
|
||||
},
|
||||
},
|
||||
rules: {
|
||||
// Top-level functions/vars are intentional globals invoked from inline
|
||||
// HTML handlers, so only flag unused *local* bindings, not globals.
|
||||
"no-unused-vars": ["warn", { vars: "local", args: "none" }],
|
||||
// Empty catch blocks are used intentionally to swallow non-critical errors.
|
||||
"no-empty": ["error", { allowEmptyCatch: true }],
|
||||
},
|
||||
},
|
||||
prettier,
|
||||
];
|
||||
@ -1068,6 +1068,27 @@ class KobraXBridge:
|
||||
self._camera_autostarted: bool = False
|
||||
self._camera_user_stopped: bool = False # user manually stopped the camera during a print
|
||||
self.camera_cache: CameraCache = CameraCache()
|
||||
self._loop: "asyncio.AbstractEventLoop | None" = None
|
||||
self._prev_kobra_state: str = ""
|
||||
self._print_active: bool = False
|
||||
self._notify_every_minutes: int = 0
|
||||
self._notify_every_layers: int = 0
|
||||
self._notifications_paused: bool = False
|
||||
self._last_progress_notif_time: float = 0.0
|
||||
self._last_progress_notif_layer: int = 0
|
||||
try:
|
||||
import config_loader as _cl2
|
||||
self._notification_urls: list[dict] = _cl2.list_notification_urls()
|
||||
_np = _cl2._find_config_file()
|
||||
if _np:
|
||||
import configparser as _cparser
|
||||
_ncfg = _cparser.ConfigParser()
|
||||
_ncfg.read(_np, encoding="utf-8")
|
||||
self._notify_every_minutes = _ncfg.getint("notifications", "notify_every_minutes", fallback=0)
|
||||
self._notify_every_layers = _ncfg.getint("notifications", "notify_every_layers", fallback=0)
|
||||
self._notifications_paused = _ncfg.getboolean("notifications", "paused", fallback=False)
|
||||
except Exception:
|
||||
self._notification_urls = []
|
||||
|
||||
self._thumbnail_b64: str = ""
|
||||
self._ace_dry_presets: dict[str, dict] = self._load_ace_dry_presets_config()
|
||||
@ -1330,6 +1351,175 @@ class KobraXBridge:
|
||||
out[key]["name"] = name or str(d.get("name", "Custom"))
|
||||
return out
|
||||
|
||||
# -------------------------------------------------------------------------
|
||||
# Notifications (apprise)
|
||||
# -------------------------------------------------------------------------
|
||||
|
||||
async def _capture_notification_image(self, timeout: float = 15.0) -> bytes:
|
||||
"""Liefert einen aktuellen JPEG-Frame für Bild-Benachrichtigungen.
|
||||
|
||||
Falls die Kamera nicht aktiv streamt, wird sie kurz eingeschaltet, ein
|
||||
Frame abgegriffen und – sofern wir sie selbst gestartet haben – wieder
|
||||
ausgeschaltet (toggle on → snapshot → toggle off).
|
||||
"""
|
||||
loop = asyncio.get_event_loop()
|
||||
# 1) Bereits ein frischer Frame im Cache (z. B. WebUI schaut zu)? → direkt nutzen.
|
||||
if self.camera_cache.latest_jpeg and (time.time() - self.camera_cache.latest_jpeg_ts) < 3.0:
|
||||
return self.camera_cache.latest_jpeg
|
||||
|
||||
started_here = False
|
||||
try:
|
||||
# 2) Kamera am Drucker einschalten, falls sie nicht ohnehin läuft.
|
||||
if not self._camera_autostarted:
|
||||
try:
|
||||
await loop.run_in_executor(None, self.client.start_camera)
|
||||
started_here = True
|
||||
log.debug("Kamera für Bild-Benachrichtigung temporär gestartet")
|
||||
except Exception as e:
|
||||
log.warning(f"Kamera-Start für Benachrichtigung fehlgeschlagen: {e}")
|
||||
# rtspUrl aktualisieren (kommt im info/report mit, wenn Kamera streamt)
|
||||
try:
|
||||
info = await loop.run_in_executor(None, self.client.query_info)
|
||||
if info:
|
||||
self._on_info(info)
|
||||
except Exception:
|
||||
pass
|
||||
|
||||
url = self._state.get("camera_url", "")
|
||||
if not url:
|
||||
return b""
|
||||
self.camera_cache.set_url(url)
|
||||
await self.camera_cache.ensure_running()
|
||||
|
||||
# 3) Auf einen frischen Frame warten (ffmpeg + RTSP-Warmup).
|
||||
ref_ts = time.time()
|
||||
deadline = ref_ts + timeout
|
||||
while time.time() < deadline:
|
||||
if self.camera_cache.latest_jpeg and self.camera_cache.latest_jpeg_ts >= ref_ts:
|
||||
return self.camera_cache.latest_jpeg
|
||||
await asyncio.sleep(0.3)
|
||||
# Kein frischer Frame – notfalls den letzten vorhandenen nehmen.
|
||||
return self.camera_cache.latest_jpeg
|
||||
finally:
|
||||
# 4) Wieder ausschalten, wenn wir sie nur für den Snapshot gestartet
|
||||
# haben (nicht während eines laufenden Drucks mit Auto-Kamera).
|
||||
if started_here and not self._camera_autostarted:
|
||||
try:
|
||||
await loop.run_in_executor(None, self.client.stop_camera)
|
||||
log.debug("Kamera nach Bild-Benachrichtigung wieder gestoppt")
|
||||
except Exception as e:
|
||||
log.debug(f"Kamera-Stop nach Benachrichtigung fehlgeschlagen: {e}")
|
||||
|
||||
def _notify(self, event: str, filename: str = ""):
|
||||
if self._notifications_paused:
|
||||
return
|
||||
matching = [e for e in self._notification_urls
|
||||
if e.get("enabled", True) and event in e.get("events", [])]
|
||||
if not matching:
|
||||
return
|
||||
printer_name = self._state.get("printer_name", "KX-Bridge")
|
||||
titles = {
|
||||
"started": "Print Started ▶",
|
||||
"finished": "Print Finished ✓",
|
||||
"failed": "Print Failed ✗",
|
||||
"cancelled": "Print Cancelled",
|
||||
"paused": "Print Paused ⏸",
|
||||
"progress": "Print Progress",
|
||||
}
|
||||
title = titles.get(event, "KX-Bridge")
|
||||
if event == "progress":
|
||||
pct = int(self._state.get("progress", 0) * 100)
|
||||
curr = self._state.get("curr_layer", 0)
|
||||
total = self._state.get("total_layers", 0)
|
||||
rem = self._state.get("remain_time", 0)
|
||||
rem_str = f"{rem // 3600}h {(rem % 3600) // 60}m" if rem else ""
|
||||
parts = [f"{pct}%"]
|
||||
if total:
|
||||
parts.append(f"Layer {curr}/{total}")
|
||||
if rem_str:
|
||||
parts.append(f"Remaining: {rem_str}")
|
||||
if filename:
|
||||
parts.append(filename)
|
||||
body = " • ".join(parts) if parts else printer_name
|
||||
else:
|
||||
body = filename if filename else printer_name
|
||||
if filename and printer_name:
|
||||
body = f"{filename}\n{printer_name}"
|
||||
|
||||
urls_plain = [e["url"] for e in matching if not e.get("include_image")]
|
||||
urls_image = [e["url"] for e in matching if e.get("include_image")]
|
||||
|
||||
def _send():
|
||||
import apprise, os, tempfile
|
||||
try:
|
||||
if urls_plain:
|
||||
ap = apprise.Apprise()
|
||||
for u in urls_plain:
|
||||
ap.add(u)
|
||||
ap.notify(title=title, body=body)
|
||||
if urls_image:
|
||||
ap2 = apprise.Apprise()
|
||||
for u in urls_image:
|
||||
ap2.add(u)
|
||||
attach = None
|
||||
tmppath = None
|
||||
jpeg = b""
|
||||
if self._loop is not None:
|
||||
try:
|
||||
fut = asyncio.run_coroutine_threadsafe(
|
||||
self._capture_notification_image(timeout=15.0), self._loop)
|
||||
jpeg = fut.result(timeout=30.0) or b""
|
||||
except Exception as e:
|
||||
log.warning(f"Kamera-Snapshot für Benachrichtigung fehlgeschlagen: {e}")
|
||||
jpeg = self.camera_cache.latest_jpeg
|
||||
else:
|
||||
jpeg = self.camera_cache.latest_jpeg
|
||||
if jpeg:
|
||||
try:
|
||||
with tempfile.NamedTemporaryFile(suffix=".jpg", delete=False) as f:
|
||||
f.write(jpeg)
|
||||
tmppath = f.name
|
||||
attach = apprise.AppriseAttachment()
|
||||
attach.add(tmppath)
|
||||
except Exception as e:
|
||||
log.warning(f"Kamera-Anhang fehlgeschlagen: {e}")
|
||||
attach = None
|
||||
else:
|
||||
log.debug("Bild-Benachrichtigung ohne Anhang (kein Kamera-Frame verfügbar)")
|
||||
ap2.notify(title=title, body=body, attach=attach)
|
||||
if tmppath:
|
||||
try:
|
||||
os.unlink(tmppath)
|
||||
except Exception:
|
||||
pass
|
||||
log.info(f"Benachrichtigung '{event}' gesendet ({len(matching)} URL(s))")
|
||||
except Exception as e:
|
||||
log.warning(f"Benachrichtigung fehlgeschlagen: {e}")
|
||||
|
||||
threading.Thread(target=_send, daemon=True).start()
|
||||
|
||||
def _check_progress_notifications(self):
|
||||
if self._state.get("kobra_state") != "printing":
|
||||
return
|
||||
if not any("progress" in e.get("events", []) for e in self._notification_urls
|
||||
if e.get("enabled", True)):
|
||||
return
|
||||
if self._notify_every_minutes == 0 and self._notify_every_layers == 0:
|
||||
return
|
||||
now = time.monotonic()
|
||||
curr_layer = self._state.get("curr_layer", 0)
|
||||
fired = False
|
||||
if self._notify_every_minutes > 0:
|
||||
if now - self._last_progress_notif_time >= self._notify_every_minutes * 60:
|
||||
fired = True
|
||||
if not fired and self._notify_every_layers > 0 and curr_layer > 0:
|
||||
if curr_layer - self._last_progress_notif_layer >= self._notify_every_layers:
|
||||
fired = True
|
||||
if fired:
|
||||
self._last_progress_notif_time = now
|
||||
self._last_progress_notif_layer = curr_layer
|
||||
self._notify("progress", self._state.get("filename", ""))
|
||||
|
||||
# -------------------------------------------------------------------------
|
||||
# MQTT callbacks (called from reader thread)
|
||||
# -------------------------------------------------------------------------
|
||||
@ -1491,6 +1681,7 @@ class KobraXBridge:
|
||||
elif kobra_state in ("free", "finished", "stoped", "canceled"):
|
||||
self._camera_autostarted = False
|
||||
self._camera_user_stopped = False # release for the next print
|
||||
self._print_active = False
|
||||
if project:
|
||||
if "filename" in project:
|
||||
self._state["filename"] = project["filename"]
|
||||
@ -4135,7 +4326,7 @@ class KobraXBridge:
|
||||
except OSError:
|
||||
raise web.HTTPNotFound()
|
||||
if name == "app.js":
|
||||
raw = raw.replace("'__VERSION__'", f"'{self._read_version()}'")
|
||||
raw = raw.replace("__VERSION__", self._read_version())
|
||||
return web.Response(
|
||||
text=raw,
|
||||
content_type=ctype,
|
||||
@ -4882,9 +5073,13 @@ class KobraXBridge:
|
||||
"verbose_http_log": getattr(self._args, "verbose_http_log", 0),
|
||||
"filament_profiles": {str(k): v for k, v in self._filament_profiles.items()},
|
||||
"visible_vendors": self._visible_vendors,
|
||||
"ace_dry_presets": self._ace_dry_presets,
|
||||
"ace_dry_presets": self._ace_dry_presets,
|
||||
"spoolman_server": getattr(self._args, "spoolman_server", "") or "",
|
||||
"spoolman_sync_rate": getattr(self._args, "spoolman_sync_rate", 0),
|
||||
"notifications": self._notification_urls,
|
||||
"notify_every_minutes": self._notify_every_minutes,
|
||||
"notify_every_layers": self._notify_every_layers,
|
||||
"notifications_paused": self._notifications_paused,
|
||||
})
|
||||
|
||||
async def handle_api_settings_post(self, request):
|
||||
@ -4953,6 +5148,42 @@ class KobraXBridge:
|
||||
cfg.set("ace_dry_presets", f"{key}_name", str(val.get("name", key.replace("_", " ").title())))
|
||||
self._ace_dry_presets = presets
|
||||
|
||||
# Benachrichtigungen speichern
|
||||
if cfg.has_section("notifications"):
|
||||
cfg.remove_section("notifications")
|
||||
incoming_notifs = data.get("notifications")
|
||||
if isinstance(incoming_notifs, list):
|
||||
valid_events = {"started", "finished", "failed", "cancelled", "paused", "progress"}
|
||||
entries = []
|
||||
for entry in incoming_notifs:
|
||||
if not isinstance(entry, dict):
|
||||
continue
|
||||
url = str(entry.get("url", "")).strip()
|
||||
if not url:
|
||||
continue
|
||||
events = [e for e in entry.get("events", []) if e in valid_events]
|
||||
include_image = bool(entry.get("include_image", False))
|
||||
enabled = bool(entry.get("enabled", True))
|
||||
entries.append({"url": url, "events": events, "include_image": include_image, "enabled": enabled})
|
||||
cfg.add_section("notifications")
|
||||
for i, entry in enumerate(entries, 1):
|
||||
cfg.set("notifications", f"url_{i}", entry["url"])
|
||||
cfg.set("notifications", f"events_{i}", ",".join(entry["events"]))
|
||||
cfg.set("notifications", f"image_{i}", "true" if entry["include_image"] else "false")
|
||||
cfg.set("notifications", f"enabled_{i}", "true" if entry["enabled"] else "false")
|
||||
self._notification_urls = entries
|
||||
elif not cfg.has_section("notifications"):
|
||||
cfg.add_section("notifications")
|
||||
notify_every_minutes = max(0, int(data.get("notify_every_minutes") or 0))
|
||||
notify_every_layers = max(0, int(data.get("notify_every_layers") or 0))
|
||||
cfg.set("notifications", "notify_every_minutes", str(notify_every_minutes))
|
||||
cfg.set("notifications", "notify_every_layers", str(notify_every_layers))
|
||||
self._notify_every_minutes = notify_every_minutes
|
||||
self._notify_every_layers = notify_every_layers
|
||||
if "notifications_paused" in data:
|
||||
self._notifications_paused = bool(data.get("notifications_paused"))
|
||||
cfg.set("notifications", "paused", "true" if self._notifications_paused else "false")
|
||||
|
||||
with open(config_path, "w", encoding="utf-8") as f:
|
||||
f.write("# KX-Bridge Konfigurationsdatei\n\n")
|
||||
cfg.write(f)
|
||||
@ -4962,6 +5193,59 @@ class KobraXBridge:
|
||||
asyncio.get_event_loop().call_later(0.3, self._restart_bridge)
|
||||
return response
|
||||
|
||||
async def handle_api_notifications_pause(self, request):
|
||||
"""Toggle or set the global notifications-paused flag without restarting."""
|
||||
try:
|
||||
data = await request.json()
|
||||
except Exception:
|
||||
data = {}
|
||||
if isinstance(data, dict) and "paused" in data:
|
||||
self._notifications_paused = bool(data.get("paused"))
|
||||
else:
|
||||
self._notifications_paused = not self._notifications_paused
|
||||
try:
|
||||
import configparser
|
||||
config_path = self._find_config_path()
|
||||
config_path.parent.mkdir(parents=True, exist_ok=True)
|
||||
cfg = configparser.ConfigParser()
|
||||
if config_path.is_file():
|
||||
cfg.read(config_path, encoding="utf-8")
|
||||
if not cfg.has_section("notifications"):
|
||||
cfg.add_section("notifications")
|
||||
cfg.set("notifications", "paused", "true" if self._notifications_paused else "false")
|
||||
with open(config_path, "w", encoding="utf-8") as f:
|
||||
f.write("# KX-Bridge Konfigurationsdatei\n\n")
|
||||
cfg.write(f)
|
||||
except Exception as e:
|
||||
log.warning(f"Notif-Pause speichern fehlgeschlagen: {e}")
|
||||
log.info(f"Benachrichtigungen {'pausiert' if self._notifications_paused else 'aktiv'}")
|
||||
return self._json_cors({"status": "ok", "paused": self._notifications_paused})
|
||||
|
||||
async def handle_api_notifications_test(self, request):
|
||||
try:
|
||||
data = await request.json()
|
||||
except Exception:
|
||||
return self._json_cors({"status": "error", "message": "invalid json"}, status=400)
|
||||
url = str(data.get("url", "")).strip()
|
||||
if not url:
|
||||
return self._json_cors({"status": "error", "message": "url required"}, status=400)
|
||||
printer_name = self._state.get("printer_name", "KX-Bridge")
|
||||
try:
|
||||
import apprise
|
||||
ap = apprise.Apprise()
|
||||
ap.add(url)
|
||||
ok = await asyncio.get_event_loop().run_in_executor(
|
||||
None,
|
||||
lambda: ap.notify(title="KX-Bridge Test", body=f"Test notification from {printer_name}"),
|
||||
)
|
||||
if ok is False:
|
||||
return self._json_cors({"status": "error", "message": "Notification failed (check URL)"})
|
||||
return self._json_cors({"status": "ok"})
|
||||
except ImportError:
|
||||
return self._json_cors({"status": "error", "message": "apprise not installed"}, status=500)
|
||||
except Exception as e:
|
||||
return self._json_cors({"status": "error", "message": str(e)}, status=500)
|
||||
|
||||
async def handle_kx_printer_add(self, request):
|
||||
"""Adds a printer: fetches credentials via IP, writes [printer_N], restarts."""
|
||||
try:
|
||||
@ -5722,6 +6006,7 @@ class KobraXBridge:
|
||||
if now - self._spoolman_last_sync >= self._spoolman.sync_rate:
|
||||
self._spoolman_sync_midprint()
|
||||
self._spoolman_last_sync = now
|
||||
self._check_progress_notifications()
|
||||
box = self.client.query_multicolor_box()
|
||||
if box:
|
||||
data = box.get("data") or {}
|
||||
@ -5850,6 +6135,8 @@ def build_app(bridge: KobraXBridge) -> web.Application:
|
||||
r.add_get("/api/state", bridge.handle_api_state)
|
||||
r.add_get("/api/settings", bridge.handle_api_settings_get)
|
||||
r.add_post("/api/settings", bridge.handle_api_settings_post)
|
||||
r.add_post("/api/notifications/test", bridge.handle_api_notifications_test)
|
||||
r.add_post("/api/notifications/pause", bridge.handle_api_notifications_pause)
|
||||
r.add_get("/api/update/check", bridge.handle_api_update_check)
|
||||
r.add_post("/api/update/apply", bridge.handle_api_update_apply)
|
||||
r.add_post("/api/file_ready/clear", bridge.handle_api_file_ready_clear)
|
||||
@ -5964,6 +6251,7 @@ async def run_bridge(args):
|
||||
client, args=per_args, store=store,
|
||||
printer_id=pid, all_bridges=all_bridges,
|
||||
)
|
||||
bridge._loop = loop
|
||||
# Adopt printer_name from config.ini if set
|
||||
if p.get("name"):
|
||||
bridge._state["printer_name"] = p["name"]
|
||||
|
||||
@ -16,6 +16,12 @@ datas += _d
|
||||
binaries += _b
|
||||
hiddenimports += _h
|
||||
|
||||
# apprise — alle Notification-Plugins (dynamisch geladen via entry_points)
|
||||
_d, _b, _h = collect_all("apprise")
|
||||
datas += _d
|
||||
binaries += _b
|
||||
hiddenimports += _h
|
||||
|
||||
a = Analysis(
|
||||
["kobrax_moonraker_bridge.py"],
|
||||
pathex=[],
|
||||
|
||||
1158
package-lock.json
generated
Normal file
1158
package-lock.json
generated
Normal file
File diff suppressed because it is too large
Load Diff
22
package.json
Normal file
22
package.json
Normal file
@ -0,0 +1,22 @@
|
||||
{
|
||||
"name": "kx-bridge",
|
||||
"version": "0.9.0",
|
||||
"description": "Moonraker-compatible bridge for Anycubic Kobra X",
|
||||
"type": "module",
|
||||
"devDependencies": {
|
||||
"@eslint/js": "^9.0.0",
|
||||
"eslint": "^9.0.0",
|
||||
"eslint-config-prettier": "^9.1.0",
|
||||
"globals": "^15.0.0",
|
||||
"prettier": "^3.0.0",
|
||||
"prettier-plugin-sort-json": "^4.2.0"
|
||||
},
|
||||
"scripts": {
|
||||
"lint": "eslint .",
|
||||
"lint:fix": "eslint . --fix",
|
||||
"format:js": "prettier --write \"web/**/*.js\" \"*.js\"",
|
||||
"format:json": "prettier --write \"*.json\" \"web/**/*.json\"",
|
||||
"format:web": "prettier --write \"web/**/*.{js,json,html,css,yml,yaml,md}\"",
|
||||
"format": "npm run format:web"
|
||||
}
|
||||
}
|
||||
2
requirements-dev.txt
Normal file
2
requirements-dev.txt
Normal file
@ -0,0 +1,2 @@
|
||||
# Code formatting
|
||||
black>=24.1.0
|
||||
@ -2,3 +2,4 @@ aiohttp>=3.9
|
||||
imageio-ffmpeg>=0.4.9
|
||||
requests>=2.30.0
|
||||
pycryptodome>=3.20.0
|
||||
apprise>=1.8
|
||||
|
||||
@ -2,135 +2,135 @@
|
||||
|
||||
Referenzliste für CSS-/Layout-Anpassungen.
|
||||
|
||||
| ID | Verwendung |
|
||||
|---|---|
|
||||
| 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 |
|
||||
| `#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 |
|
||||
|
||||
@ -2,89 +2,89 @@
|
||||
|
||||
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 |
|
||||
| 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 |
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
@ -91,7 +91,6 @@
|
||||
"fd_print": "▶ Drucken",
|
||||
"fd_slot": "Slot",
|
||||
"fd_slots_hint": "GCode-Kanal → AMS-Slot zuweisen:",
|
||||
"fd_title": "Slot-Zuweisung",
|
||||
"fd_used": "BELEGT",
|
||||
"file_cancel_btn": "✕ Abbrechen",
|
||||
"file_ready_btn": "▶ Druck starten",
|
||||
@ -184,6 +183,9 @@
|
||||
"nav_print": "Druck",
|
||||
"nav_printers": "Drucker",
|
||||
"nav_settings": "Einstellungen",
|
||||
"obico_config_file_label": "Config-Datei:",
|
||||
"obico_info_configured_via": "Obico wird über den <code>moonraker-obico</code>-Container konfiguriert.",
|
||||
"panel_motion_z": "Z-Achse",
|
||||
"nav_temps": "Temperaturen",
|
||||
"orca_profile_done": "Importiert",
|
||||
"orca_profile_dropmsg": "Hierher ziehen oder klicken",
|
||||
@ -237,6 +239,7 @@
|
||||
"settings_cat_display": "Darstellung",
|
||||
"settings_cat_filament": "Filament",
|
||||
"settings_cat_language": "Sprache",
|
||||
"settings_cat_notifications": "Benachrichtigungen",
|
||||
"settings_cat_printer": "Drucker",
|
||||
"settings_cat_system": "System",
|
||||
"settings_cat_theme": "Hell / Dunkel umschalten",
|
||||
@ -249,6 +252,26 @@
|
||||
"settings_filament_mapping_hint": "Festes Orca-Profil pro AMS-Slot. Bei der Slicer-Synchronisierung sendet die Bridge dieses Profil statt \"Generic\".",
|
||||
"settings_filament_mapping_label": "Filament-Profil-Mapping (pro Slot)",
|
||||
"settings_filament_mapping_save": "Mapping speichern",
|
||||
"settings_notif_active": "Aktiv",
|
||||
"settings_notif_add": "Benachrichtigung hinzufügen",
|
||||
"settings_notif_empty": "Keine Benachrichtigungen konfiguriert.",
|
||||
"settings_notif_ev_cancelled": "Abgebrochen",
|
||||
"settings_notif_ev_failed": "Fehler",
|
||||
"settings_notif_ev_finished": "Fertig",
|
||||
"settings_notif_ev_paused": "Pausiert",
|
||||
"settings_notif_ev_progress": "Fortschritt",
|
||||
"settings_notif_ev_started": "Gestartet",
|
||||
"settings_notif_interval_lbl": "Wiederholungsintervall",
|
||||
"settings_notif_layers_unit": "Schichten",
|
||||
"settings_notif_min_unit": "Min",
|
||||
"settings_notif_paused": "Pausiert",
|
||||
"settings_notif_send_image": "Bild",
|
||||
"settings_notif_test": "Test",
|
||||
"settings_notif_test_fail": "Fehler",
|
||||
"settings_notif_test_ok": "Gesendet",
|
||||
"settings_notif_zero_off": "(0 = aus)",
|
||||
"settings_notifications": "Benachrichtigungen",
|
||||
"settings_notifications_hint": "Sende Benachrichtigungen über <a href=\"https://appriseit.com/getting-started/universal-syntax/\" target=\"_blank\" rel=\"noopener\">Apprise-URLs</a> (discord://, telegram://, gotify://, slack://, …)",
|
||||
"settings_filament_mapping_save_label": "Mapping speichern",
|
||||
"settings_file_ready_banner": "Druckleiste",
|
||||
"settings_file_ready_dialog": "Druckdialog",
|
||||
@ -282,7 +305,6 @@
|
||||
"settings_visible_vendors_hint": "Nur diese Hersteller erscheinen im Slot-Profil-Dropdown. Nichts ausgewählt = alle anzeigen. „Generic\" und eigene Profile sind immer sichtbar.",
|
||||
"settings_visible_vendors_label": "Sichtbare Hersteller (Profil-Dropdown)",
|
||||
"settings_visible_vendors_save": "Auswahl speichern",
|
||||
"settings_visible_vendors_save_label": "Auswahl speichern",
|
||||
"settings_web_upload_warning": "Warnung bei Web-Upload-Druck anzeigen",
|
||||
"sf_all": "Alle",
|
||||
"sf_err": "✗ Fehler",
|
||||
@ -314,6 +336,8 @@
|
||||
"speed_normal": "⚡ Normal",
|
||||
"speed_silent": "🐢 Leise",
|
||||
"speed_sport": "🚀 Sport",
|
||||
"spoolman_status_configured": "verbunden",
|
||||
"spoolman_status_not_configured": "nicht konfiguriert",
|
||||
"ss_date": "↓ Datum",
|
||||
"ss_dur": "⏱ Druckzeit",
|
||||
"ss_name": "A–Z Name",
|
||||
|
||||
@ -25,8 +25,8 @@
|
||||
"ace_dry_status_off": "Status: Off",
|
||||
"ace_dry_status_on": "Status: Active",
|
||||
"ace_dry_status_remaining": "Remaining",
|
||||
"ace_dry_stop": "■ Stop",
|
||||
"ace_dry_temp": "Temperature (°C)",
|
||||
"ace_dry_stop": "■ Stop",
|
||||
"ace_dry_temp_line": "Drying Temperature",
|
||||
"ace_dry_time_line": "Drying Time",
|
||||
"ace_dry_ui_pending": "(UI only, backend next)",
|
||||
@ -45,7 +45,6 @@
|
||||
"browser_tab_printer": "On Printer",
|
||||
"browser_tab_uploaded": "Uploaded",
|
||||
"btn_cam_start": "▶ Camera",
|
||||
"btn_cam_start2": "▶ Start",
|
||||
"btn_cam_stop": "◼ Camera",
|
||||
"btn_cam_stop2": "◼ Stop",
|
||||
"btn_cancel": "✕ Stop",
|
||||
@ -91,7 +90,6 @@
|
||||
"fd_print": "▶ Print",
|
||||
"fd_slot": "Slot",
|
||||
"fd_slots_hint": "Assign GCode channel to AMS slot:",
|
||||
"fd_title": "Slot Assignment",
|
||||
"fd_used": "USED",
|
||||
"file_cancel_btn": "✕ Cancel",
|
||||
"file_ready_btn": "▶ Start Print",
|
||||
@ -185,6 +183,8 @@
|
||||
"nav_printers": "Printers",
|
||||
"nav_settings": "Settings",
|
||||
"nav_temps": "Temperatures",
|
||||
"obico_config_file_label": "Config file:",
|
||||
"obico_info_configured_via": "Obico is configured via the <code>moonraker-obico</code> container.",
|
||||
"orca_profile_done": "Imported",
|
||||
"orca_profile_dropmsg": "Drop here or click",
|
||||
"orca_profile_help_html": "Upload a <b>ZIP</b> of your OrcaSlicer filament folder or single <b>.json</b> files.<br>In OrcaSlicer: <i>Help → Show Configuration Folder → user/<id>/filament/</i>",
|
||||
@ -204,7 +204,8 @@
|
||||
"panel_extras_camera": "Camera",
|
||||
"panel_extras_fan": "Fan",
|
||||
"panel_extras_light": "Light",
|
||||
"panel_motion_xy": "Axes Control",
|
||||
"panel_motion_xy": "XY Axes",
|
||||
"panel_motion_z": "Z Axis",
|
||||
"panel_print_btn_cancel": "✕ Cancel",
|
||||
"panel_print_btn_pause": "⏸ Pause",
|
||||
"panel_print_btn_resume": "▶ Resume",
|
||||
@ -236,10 +237,9 @@
|
||||
"settings_cat_connection": "Connection",
|
||||
"settings_cat_display": "Appearance",
|
||||
"settings_cat_filament": "Filament",
|
||||
"settings_cat_language": "Language",
|
||||
"settings_cat_notifications": "Notifications",
|
||||
"settings_cat_printer": "Printer",
|
||||
"settings_cat_system": "System",
|
||||
"settings_cat_theme": "Toggle light / dark",
|
||||
"settings_connection": "Connection",
|
||||
"settings_default_slot": "Default Slot (single color)",
|
||||
"settings_device_id": "Device ID",
|
||||
@ -259,6 +259,26 @@
|
||||
"settings_mode_id_placeholder": "20030",
|
||||
"settings_mqtt_port": "MQTT Port",
|
||||
"settings_mqtt_username_placeholder": "userXXXXXXXX",
|
||||
"settings_notif_active": "Active",
|
||||
"settings_notif_add": "Add notification",
|
||||
"settings_notif_empty": "No notifications configured.",
|
||||
"settings_notif_ev_cancelled": "Cancelled",
|
||||
"settings_notif_ev_failed": "Failed",
|
||||
"settings_notif_ev_finished": "Finished",
|
||||
"settings_notif_ev_paused": "Paused",
|
||||
"settings_notif_ev_progress": "Progress",
|
||||
"settings_notif_ev_started": "Started",
|
||||
"settings_notif_interval_lbl": "Repeat interval",
|
||||
"settings_notif_layers_unit": "layers",
|
||||
"settings_notif_min_unit": "min",
|
||||
"settings_notif_paused": "Paused",
|
||||
"settings_notif_send_image": "Image",
|
||||
"settings_notif_test": "Test",
|
||||
"settings_notif_test_fail": "Failed",
|
||||
"settings_notif_test_ok": "Sent",
|
||||
"settings_notif_zero_off": "(0 = off)",
|
||||
"settings_notifications": "Notifications",
|
||||
"settings_notifications_hint": "Send notifications via <a href=\"https://appriseit.com/getting-started/universal-syntax/\" target=\"_blank\" rel=\"noopener\">Apprise URLs</a> (discord://, telegram://, gotify://, slack://, …)",
|
||||
"settings_orca_profiles_import": "Import profiles",
|
||||
"settings_orca_profiles_label": "OrcaSlicer Profiles",
|
||||
"settings_password": "MQTT Password",
|
||||
@ -292,9 +312,6 @@
|
||||
"skip_btn_label": "Objects",
|
||||
"skip_cancel": "Cancel",
|
||||
"skip_confirm": "Skip",
|
||||
"skip_confirm_btn": "Skip",
|
||||
"skip_hint": "Uncheck objects you no longer want to print:",
|
||||
"skip_no_objects": "No objects in this print.",
|
||||
"skip_select_at_least_one": "Please pick at least one object.",
|
||||
"skip_sending": "Sending …",
|
||||
"skip_success": "Objects will be skipped.",
|
||||
@ -314,6 +331,8 @@
|
||||
"speed_normal": "⚡ Normal",
|
||||
"speed_silent": "🐢 Silent",
|
||||
"speed_sport": "🚀 Sport",
|
||||
"spoolman_status_configured": "connected",
|
||||
"spoolman_status_not_configured": "not configured",
|
||||
"ss_date": "↓ Date",
|
||||
"ss_dur": "⏱ Print time",
|
||||
"ss_name": "A–Z Name",
|
||||
|
||||
@ -91,7 +91,6 @@
|
||||
"fd_print": "▶ Imprimir",
|
||||
"fd_slot": "Ranura",
|
||||
"fd_slots_hint": "Asignar canal GCode a la ranura AMS:",
|
||||
"fd_title": "Asignación de ranura",
|
||||
"fd_used": "USADO",
|
||||
"file_cancel_btn": "✕ Cancelar",
|
||||
"file_ready_btn": "▶ Iniciar impresión",
|
||||
@ -184,6 +183,9 @@
|
||||
"nav_print": "Impresión",
|
||||
"nav_printers": "Impresoras",
|
||||
"nav_settings": "Ajustes",
|
||||
"obico_config_file_label": "Archivo de configuración:",
|
||||
"obico_info_configured_via": "Obico se configura mediante el contenedor <code>moonraker-obico</code>.",
|
||||
"panel_motion_z": "Eje Z",
|
||||
"nav_temps": "Temperaturas",
|
||||
"orca_profile_done": "Importado",
|
||||
"orca_profile_dropmsg": "Suelta aquí o haz clic",
|
||||
@ -237,6 +239,7 @@
|
||||
"settings_cat_display": "Apariencia",
|
||||
"settings_cat_filament": "Filamento",
|
||||
"settings_cat_language": "Idioma",
|
||||
"settings_cat_notifications": "Notificaciones",
|
||||
"settings_cat_printer": "Impresora",
|
||||
"settings_cat_system": "Sistema",
|
||||
"settings_cat_theme": "Alternar claro / oscuro",
|
||||
@ -249,6 +252,26 @@
|
||||
"settings_filament_mapping_hint": "Perfil Orca fijo por ranura AMS. Al sincronizar con el slicer, el bridge envía este perfil en lugar de \"Generic\".",
|
||||
"settings_filament_mapping_label": "Asignación de perfil de filamento (por ranura)",
|
||||
"settings_filament_mapping_save": "Guardar asignación",
|
||||
"settings_notif_active": "Activo",
|
||||
"settings_notif_add": "Agregar notificación",
|
||||
"settings_notif_empty": "Sin notificaciones configuradas.",
|
||||
"settings_notif_ev_cancelled": "Cancelado",
|
||||
"settings_notif_ev_failed": "Error",
|
||||
"settings_notif_ev_finished": "Finalizado",
|
||||
"settings_notif_ev_paused": "Pausado",
|
||||
"settings_notif_ev_progress": "Progreso",
|
||||
"settings_notif_ev_started": "Iniciado",
|
||||
"settings_notif_interval_lbl": "Intervalo de repetición",
|
||||
"settings_notif_layers_unit": "capas",
|
||||
"settings_notif_min_unit": "min",
|
||||
"settings_notif_paused": "Pausado",
|
||||
"settings_notif_send_image": "Imagen",
|
||||
"settings_notif_test": "Prueba",
|
||||
"settings_notif_test_fail": "Error",
|
||||
"settings_notif_test_ok": "Enviado",
|
||||
"settings_notif_zero_off": "(0 = desactivado)",
|
||||
"settings_notifications": "Notificaciones",
|
||||
"settings_notifications_hint": "Envía notificaciones a través de <a href=\"https://appriseit.com/getting-started/universal-syntax/\" target=\"_blank\" rel=\"noopener\">URLs de Apprise</a> (discord://, telegram://, gotify://, slack://, …)",
|
||||
"settings_filament_mapping_save_label": "Guardar asignación",
|
||||
"settings_file_ready_banner": "Barra de impresión",
|
||||
"settings_file_ready_dialog": "Diálogo de impresión",
|
||||
@ -282,7 +305,6 @@
|
||||
"settings_visible_vendors_hint": "Solo estos fabricantes aparecen en la lista de perfiles de ranura. Nada seleccionado = mostrar todos. «Generic» y tus propios perfiles siempre son visibles.",
|
||||
"settings_visible_vendors_label": "Fabricantes visibles (lista de perfiles)",
|
||||
"settings_visible_vendors_save": "Guardar selección",
|
||||
"settings_visible_vendors_save_label": "Guardar selección",
|
||||
"settings_web_upload_warning": "Mostrar advertencia al imprimir subidas web",
|
||||
"sf_all": "Todos",
|
||||
"sf_err": "✗ Fallido",
|
||||
@ -314,6 +336,8 @@
|
||||
"speed_normal": "⚡ Normal",
|
||||
"speed_silent": "🐢 Silencioso",
|
||||
"speed_sport": "🚀 Sport",
|
||||
"spoolman_status_configured": "conectado",
|
||||
"spoolman_status_not_configured": "no configurado",
|
||||
"ss_date": "↓ Fecha",
|
||||
"ss_dur": "⏱ Tiempo de impresión",
|
||||
"ss_name": "A–Z Nombre",
|
||||
|
||||
@ -172,6 +172,9 @@
|
||||
"nav_print": "Impression",
|
||||
"nav_printers": "Imprimantes",
|
||||
"nav_settings": "Paramètres",
|
||||
"obico_config_file_label": "Fichier de config :",
|
||||
"obico_info_configured_via": "Obico se configure via le conteneur <code>moonraker-obico</code>.",
|
||||
"panel_motion_z": "Axe Z",
|
||||
"nav_temps": "Températures",
|
||||
"orca_profile_done": "Importé",
|
||||
"orca_profile_dropmsg": "Déposez ici ou cliquez",
|
||||
@ -225,6 +228,7 @@
|
||||
"settings_cat_display": "Apparence",
|
||||
"settings_cat_filament": "Filament",
|
||||
"settings_cat_language": "Langue",
|
||||
"settings_cat_notifications": "Notifications",
|
||||
"settings_cat_printer": "Imprimante",
|
||||
"settings_cat_system": "Système",
|
||||
"settings_cat_theme": "Basculer clair / sombre",
|
||||
@ -237,6 +241,26 @@
|
||||
"settings_filament_mapping_hint": "Profil Orca fixe par emplacement AMS. Lors de la synchronisation du slicer, le bridge envoie ce profil au lieu de « Generic ».",
|
||||
"settings_filament_mapping_label": "Mappage du profil de filament (par emplacement)",
|
||||
"settings_filament_mapping_save": "Enregistrer le mappage",
|
||||
"settings_notif_active": "Actif",
|
||||
"settings_notif_add": "Ajouter une notification",
|
||||
"settings_notif_empty": "Aucune notification configurée.",
|
||||
"settings_notif_ev_cancelled": "Annulé",
|
||||
"settings_notif_ev_failed": "Erreur",
|
||||
"settings_notif_ev_finished": "Terminé",
|
||||
"settings_notif_ev_paused": "Pausé",
|
||||
"settings_notif_ev_progress": "Progression",
|
||||
"settings_notif_ev_started": "Démarré",
|
||||
"settings_notif_interval_lbl": "Intervalle de répétition",
|
||||
"settings_notif_layers_unit": "couches",
|
||||
"settings_notif_min_unit": "min",
|
||||
"settings_notif_paused": "En pause",
|
||||
"settings_notif_send_image": "Image",
|
||||
"settings_notif_test": "Test",
|
||||
"settings_notif_test_fail": "Erreur",
|
||||
"settings_notif_test_ok": "Envoyé",
|
||||
"settings_notif_zero_off": "(0 = désactivé)",
|
||||
"settings_notifications": "Notifications",
|
||||
"settings_notifications_hint": "Envoyer des notifications via des <a href=\"https://appriseit.com/getting-started/universal-syntax/\" target=\"_blank\" rel=\"noopener\">URLs Apprise</a> (discord://, telegram://, gotify://, slack://, …)",
|
||||
"settings_filament_mapping_save_label": "Enregistrer le mappage",
|
||||
"settings_file_ready_banner": "Barre d'impression",
|
||||
"settings_file_ready_dialog": "Dialogue d'impression",
|
||||
@ -268,7 +292,6 @@
|
||||
"settings_visible_vendors_hint": "Seuls ces fabricants apparaissent dans la liste des profils d'emplacement. Rien de sélectionné = tout afficher. « Generic » et vos propres profils sont toujours visibles.",
|
||||
"settings_visible_vendors_label": "Fabricants visibles (liste des profils)",
|
||||
"settings_visible_vendors_save": "Enregistrer la sélection",
|
||||
"settings_visible_vendors_save_label": "Enregistrer la sélection",
|
||||
"settings_web_upload_warning": "Afficher un avertissement lors de l'impression de fichiers web",
|
||||
"sf_all": "Tout",
|
||||
"sf_err": "✗ Échoués",
|
||||
@ -300,6 +323,8 @@
|
||||
"speed_normal": "⚡ Normal",
|
||||
"speed_silent": "🐢 Silencieux",
|
||||
"speed_sport": "🚀 Sport",
|
||||
"spoolman_status_configured": "connecté",
|
||||
"spoolman_status_not_configured": "non configuré",
|
||||
"ss_date": "↓ Date",
|
||||
"ss_dur": "⏱ Durée d'impression",
|
||||
"ss_name": "A–Z Nom",
|
||||
|
||||
@ -172,6 +172,9 @@
|
||||
"nav_print": "Stampa",
|
||||
"nav_printers": "Stampanti",
|
||||
"nav_settings": "Impostazioni",
|
||||
"obico_config_file_label": "File di configurazione:",
|
||||
"obico_info_configured_via": "Obico viene configurato tramite il container <code>moonraker-obico</code>.",
|
||||
"panel_motion_z": "Asse Z",
|
||||
"nav_temps": "Temperature",
|
||||
"orca_profile_done": "Importato",
|
||||
"orca_profile_dropmsg": "Trascina qui o fai clic",
|
||||
@ -225,7 +228,7 @@
|
||||
"settings_cat_display": "Aspetto",
|
||||
"settings_cat_filament": "Filamento",
|
||||
"settings_cat_language": "Lingua",
|
||||
"settings_cat_printer": "Stampante",
|
||||
"settings_cat_notifications": "Notifiche",
|
||||
"settings_cat_system": "Sistema",
|
||||
"settings_cat_theme": "Alterna chiaro / scuro",
|
||||
"settings_connection": "Connessione",
|
||||
@ -237,6 +240,26 @@
|
||||
"settings_filament_mapping_hint": "Profilo Orca fisso per slot AMS. Durante la sincronizzazione dello slicer, il bridge invia questo profilo al posto di \"Generic\".",
|
||||
"settings_filament_mapping_label": "Mappatura profilo filamento (per slot)",
|
||||
"settings_filament_mapping_save": "Salva mappatura",
|
||||
"settings_notif_active": "Attivo",
|
||||
"settings_notif_add": "Aggiungi notifica",
|
||||
"settings_notif_empty": "Nessuna notifica configurata.",
|
||||
"settings_notif_ev_cancelled": "Annullato",
|
||||
"settings_notif_ev_failed": "Errore",
|
||||
"settings_notif_ev_finished": "Completato",
|
||||
"settings_notif_ev_paused": "In pausa",
|
||||
"settings_notif_ev_progress": "Progresso",
|
||||
"settings_notif_ev_started": "Iniziato",
|
||||
"settings_notif_interval_lbl": "Intervallo di ripetizione",
|
||||
"settings_notif_layers_unit": "strati",
|
||||
"settings_notif_min_unit": "min",
|
||||
"settings_notif_paused": "In pausa",
|
||||
"settings_notif_send_image": "Immagine",
|
||||
"settings_notif_test": "Test",
|
||||
"settings_notif_test_fail": "Errore",
|
||||
"settings_notif_test_ok": "Inviato",
|
||||
"settings_notif_zero_off": "(0 = disabilitato)",
|
||||
"settings_notifications": "Notifiche",
|
||||
"settings_notifications_hint": "Invia notifiche tramite <a href=\"https://appriseit.com/getting-started/universal-syntax/\" target=\"_blank\" rel=\"noopener\">URL Apprise</a> (discord://, telegram://, gotify://, slack://, …)",
|
||||
"settings_filament_mapping_save_label": "Salva mappatura",
|
||||
"settings_file_ready_banner": "Barra di stampa",
|
||||
"settings_file_ready_dialog": "Finestra di dialogo stampa",
|
||||
@ -268,7 +291,6 @@
|
||||
"settings_visible_vendors_hint": "Solo questi produttori appariranno nel menu del profilo dello slot. Se non selezioni nulla = mostra tutti. I profili \"Generici\" e i tuoi personali sono sempre visibili.",
|
||||
"settings_visible_vendors_label": "Produttori visibili (menu del profilo)",
|
||||
"settings_visible_vendors_save": "Salva selezione",
|
||||
"settings_visible_vendors_save_label": "Salva selezione",
|
||||
"settings_web_upload_warning": "Mostra un avviso quando si stampano caricamenti web",
|
||||
"sf_all": "Tutti",
|
||||
"sf_err": "✗ Fallito",
|
||||
@ -300,6 +322,8 @@
|
||||
"speed_normal": "⚡ Normale",
|
||||
"speed_silent": "🐢 Silenzioso",
|
||||
"speed_sport": "🚀 Sport",
|
||||
"spoolman_status_configured": "connesso",
|
||||
"spoolman_status_not_configured": "non configurato",
|
||||
"ss_date": "↓ Data",
|
||||
"ss_dur": "⏱ Tempo di stampa",
|
||||
"ss_name": "Nome A–Z",
|
||||
|
||||
@ -91,7 +91,6 @@
|
||||
"fd_print": "▶ 打印",
|
||||
"fd_slot": "槽位",
|
||||
"fd_slots_hint": "将 GCode 通道分配到 AMS 槽位:",
|
||||
"fd_title": "槽位分配",
|
||||
"fd_used": "已用",
|
||||
"file_cancel_btn": "✕ 取消",
|
||||
"file_ready_btn": "▶ 开始打印",
|
||||
@ -184,6 +183,9 @@
|
||||
"nav_print": "打印",
|
||||
"nav_printers": "打印机",
|
||||
"nav_settings": "设置",
|
||||
"obico_config_file_label": "配置文件:",
|
||||
"obico_info_configured_via": "Obico 通过 <code>moonraker-obico</code> 容器进行配置。",
|
||||
"panel_motion_z": "Z 轴",
|
||||
"nav_temps": "温度",
|
||||
"orca_profile_done": "已导入",
|
||||
"orca_profile_dropmsg": "拖到此处或点击",
|
||||
@ -237,6 +239,7 @@
|
||||
"settings_cat_display": "外观",
|
||||
"settings_cat_filament": "耗材",
|
||||
"settings_cat_language": "语言",
|
||||
"settings_cat_notifications": "通知",
|
||||
"settings_cat_printer": "打印机",
|
||||
"settings_cat_system": "系统",
|
||||
"settings_cat_theme": "切换浅色 / 深色",
|
||||
@ -249,6 +252,26 @@
|
||||
"settings_filament_mapping_hint": "每个 AMS 槽位的固定 Orca 配置。在切片器同步时,Bridge 会发送此配置而不是“Generic”。",
|
||||
"settings_filament_mapping_label": "耗材配置映射(每槽位)",
|
||||
"settings_filament_mapping_save": "保存映射",
|
||||
"settings_notif_active": "已启用",
|
||||
"settings_notif_add": "添加通知",
|
||||
"settings_notif_empty": "未配置任何通知。",
|
||||
"settings_notif_ev_cancelled": "已取消",
|
||||
"settings_notif_ev_failed": "失败",
|
||||
"settings_notif_ev_finished": "已完成",
|
||||
"settings_notif_ev_paused": "已暂停",
|
||||
"settings_notif_ev_progress": "进度",
|
||||
"settings_notif_ev_started": "已开始",
|
||||
"settings_notif_interval_lbl": "重复间隔",
|
||||
"settings_notif_layers_unit": "层",
|
||||
"settings_notif_min_unit": "分钟",
|
||||
"settings_notif_paused": "已暂停",
|
||||
"settings_notif_send_image": "图像",
|
||||
"settings_notif_test": "测试",
|
||||
"settings_notif_test_fail": "失败",
|
||||
"settings_notif_test_ok": "已发送",
|
||||
"settings_notif_zero_off": "(0 = 关闭)",
|
||||
"settings_notifications": "通知",
|
||||
"settings_notifications_hint": "通过 <a href=\"https://appriseit.com/getting-started/universal-syntax/\" target=\"_blank\" rel=\"noopener\">Apprise URL</a> 发送通知 (discord://, telegram://, gotify://, slack://, …)",
|
||||
"settings_filament_mapping_save_label": "保存映射",
|
||||
"settings_file_ready_banner": "打印栏",
|
||||
"settings_file_ready_dialog": "打印对话框",
|
||||
@ -282,7 +305,6 @@
|
||||
"settings_visible_vendors_hint": "仅这些厂商会出现在槽位配置下拉框中。未选择 = 显示全部。“Generic”和您自己的配置始终可见。",
|
||||
"settings_visible_vendors_label": "可见厂商(配置下拉框)",
|
||||
"settings_visible_vendors_save": "保存选择",
|
||||
"settings_visible_vendors_save_label": "保存选择",
|
||||
"settings_web_upload_warning": "打印网页上传文件时显示警告",
|
||||
"sf_all": "全部",
|
||||
"sf_err": "✗ 失败",
|
||||
@ -314,6 +336,8 @@
|
||||
"speed_normal": "⚡ 标准",
|
||||
"speed_silent": "🐢 静音",
|
||||
"speed_sport": "🚀 运动",
|
||||
"spoolman_status_configured": "已连接",
|
||||
"spoolman_status_not_configured": "未配置",
|
||||
"ss_date": "↓ 日期",
|
||||
"ss_dur": "⏱ 打印时间",
|
||||
"ss_name": "A–Z 名称",
|
||||
|
||||
Reference in New Issue
Block a user