Upload per ZIP-Tool (1/3)

This commit is contained in:
olli1986 committed 2026-10-25 14:29:16 +01:00
1 parent 6746a7a5d4
commit e3b021ee1d
9 files changed
+199 -11

No files matched your search

+2 -2
View File
@@ -23,13 +23,13 @@ Bedienung, Platzhalter, Serien, Barcodes: [docs/ANLEITUNG.md](docs/ANLEITUNG.md)
**Ein Befehl** (installiert nach `~/labelpi`, richtet Dienst und USB-Rechte ein):
```
curl -fsSL https://raw.githubusercontent.com/DEIN-USER/labelpi/main/install.sh | bash
curl -fsSL https://raw.githubusercontent.com/olli1986/labelpi/main/install.sh | bash
```
**Oder manuell:** Ordner per `scp -r` auf den Pi kopieren, dann `cd labelpi && bash install.sh`.
Danach im Browser: `http://<hostname>.local` (z. B. `http://labelpi.local`).
**Update:** `cd ~/labelpi && git pull && sudo systemctl restart labelpi`
**Update:** in der Weboberfläche unter *Einstellungen → Update* (ein Hinweis erscheint oben, sobald eine neue Version verfügbar ist). Alternativ per `cd ~/labelpi && git pull && sudo systemctl restart labelpi`. Für Maintainer: [docs/RELEASING.md](docs/RELEASING.md)
## Hinweise
- Rot/Schwarz-Rollen werden nicht unterstützt (der Designer ist Schwarz/Weiß).
+1
View File
@@ -0,0 +1 @@
0.7.0
+117 -2
View File
@@ -1,5 +1,5 @@
"""LabelPi – Etikettendruck für Brother-QL-Drucker per Weboberfläche (MIT-Lizenz)."""
import base64, glob, io, json, os, re, socket, sqlite3, subprocess, tempfile, threading, time
import base64, glob, io, json, os, re, shutil, socket, sqlite3, subprocess, sys, tarfile, tempfile, threading, time, urllib.error, urllib.parse, urllib.request
from flask import Flask, Response, jsonify, request, send_file, send_from_directory
from PIL import Image
import barcode
@@ -13,8 +13,8 @@ from brother_ql.backends.helpers import send, discover
from brother_ql.labels import ALL_LABELS
from brother_ql.models import ALL_MODELS
VERSION = "0.6.0"
BASE = os.path.dirname(os.path.abspath(__file__))
VERSION = open(os.path.join(BASE, "VERSION")).read().strip() if os.path.isfile(os.path.join(BASE, "VERSION")) else "0.7.0"
DB = os.path.join(BASE, "labels.db")
app = Flask(__name__, static_folder="static")
lock = threading.Lock()
@@ -417,6 +417,121 @@ def help_():
return Response(markdown.markdown(open(p, encoding="utf-8").read(), extensions=["tables", "fenced_code"]), mimetype="text/html")
return Response("", mimetype="text/html")
# ---------- Update über die Weboberfläche (GitHub-Releases oder main) ----------
REPO = os.environ.get("LABELPI_REPO_SLUG", "olli1986/labelpi")
BK = os.path.join(BASE, ".update-backup")
PROTECT = {".venv", ".git", "labels.db", "lang", ".update-backup", "__pycache__"} # bleiben beim Update unberührt
UPD = {"state": "idle", "log": []}; _chk = {"t": 0, "v": None}
RESTART = lambda: os._exit(0) # systemd (Restart=always) startet LabelPi danach neu
def _get(url, timeout=10):
return urllib.request.urlopen(urllib.request.Request(url, headers={"User-Agent": "LabelPi/" + VERSION}), timeout=timeout)
def _ver(s):
n = [int(x) for x in re.findall(r"\d+", s or "")[:3]]; return tuple(n + [0] * (3 - len(n)))
def is_git(): return os.path.isdir(os.path.join(BASE, ".git")) and bool(shutil.which("git"))
def update_info(force=False):
if not force and _chk["v"] and time.time() - _chk["t"] < 6 * 3600: return _chk["v"]
info = dict(current=VERSION, latest=VERSION, available=False, tag="", notes="", source="", repo=REPO, backup=os.path.isdir(BK), method="git" if is_git() else "archive")
try:
# Release-Tag ohne API (keine Rate-Limits, z. B. hinter Mobilfunk-NAT): /releases/latest leitet auf /releases/tag/<tag> um
m = re.search(r"/releases/tag/([^/?#]+)", _get(f"https://github.com/{REPO}/releases/latest").geturl())
if m:
tag = urllib.parse.unquote(m.group(1)); info.update(latest=tag.lstrip("vV"), tag=tag, source="release")
try: # Release-Beschreibung (optional, die API kann begrenzt sein)
req = urllib.request.Request(f"https://api.github.com/repos/{REPO}/releases/tags/{m.group(1)}", headers={"User-Agent": "LabelPi/" + VERSION, "Accept": "application/vnd.github+json"})
info["notes"] = (json.load(urllib.request.urlopen(req, timeout=5)).get("body") or "")[:1500]
except Exception: pass
else: # kein Release vorhanden: VERSION-Datei auf main
try: v = _get(f"https://raw.githubusercontent.com/{REPO}/main/VERSION").read().decode().strip()
except urllib.error.HTTPError:
v = re.search(r'VERSION\s*=\s*"([^"]+)"', _get(f"https://raw.githubusercontent.com/{REPO}/main/app.py").read().decode()).group(1)
info.update(latest=v, source="main")
info["available"] = _ver(info["latest"]) > _ver(VERSION)
_chk.update(t=time.time(), v=info)
except Exception as e: info["error"] = str(e)[:200]
return info
def _copy_in(src, skip=PROTECT):
for n in os.listdir(src):
if n in skip: continue
a, b = os.path.join(src, n), os.path.join(BASE, n)
if os.path.isdir(a): shutil.copytree(a, b, dirs_exist_ok=True, ignore=shutil.ignore_patterns("__pycache__"))
else: shutil.copy2(a, b)
def _restore():
if os.path.isdir(BK): _copy_in(BK, {"__pycache__"})
def pip_install():
r = subprocess.run([sys.executable, "-m", "pip", "install", "-r", os.path.join(BASE, "requirements.txt"), "--disable-pip-version-check"], capture_output=True, text=True, timeout=1800)
if r.returncode: raise RuntimeError((r.stderr or r.stdout)[-300:])
def _read(p, mode="r"):
try:
with open(p, mode) as f: return f.read()
except OSError: return None
def do_update():
log = UPD["log"]
try:
info = update_info(True)
if info.get("error") or not info["available"]: raise RuntimeError(info.get("error") or "Keine neuere Version verfügbar")
req, inst = os.path.join(BASE, "requirements.txt"), os.path.join(BASE, "install.sh")
old_req, old_inst = _read(req), _read(inst, "rb")
log.append("Sichere aktuelle Version …")
shutil.rmtree(BK, ignore_errors=True); os.makedirs(BK)
for n in os.listdir(BASE):
if n not in PROTECT:
a = os.path.join(BASE, n)
if os.path.isdir(a): shutil.copytree(a, os.path.join(BK, n), ignore=shutil.ignore_patterns("__pycache__"))
else: shutil.copy2(a, BK)
log.append(f"Lade Version {info['latest']} herunter …")
if is_git():
log.append("Hole Änderungen per git …")
for cmd in (["fetch", "--tags", "--force", "origin"], ["reset", "--hard", info["tag"] or "origin/main"]):
r = subprocess.run(["git", "-C", BASE] + cmd, capture_output=True, text=True, timeout=300)
if r.returncode: raise RuntimeError(r.stderr[-300:])
else:
with tempfile.TemporaryDirectory() as t:
url = f"https://codeload.github.com/{REPO}/tar.gz/refs/" + (f"tags/{info['tag']}" if info["tag"] else "heads/main")
tgz = os.path.join(t, "src.tgz")
with open(tgz, "wb") as f: f.write(_get(url, 120).read())
log.append("Entpacke …")
with tarfile.open(tgz) as tf:
for m in tf.getmembers():
if not (m.isfile() or m.isdir()) or os.path.normpath(m.name).startswith(("/", "..")): continue
try: tf.extract(m, t, filter="data")
except TypeError: tf.extract(m, t)
root = next((os.path.join(t, d) for d in os.listdir(t) if os.path.isfile(os.path.join(t, d, "app.py"))), None)
if not root or not os.path.isfile(os.path.join(root, "static", "index.html")): raise RuntimeError("Die heruntergeladene Datei enthält kein LabelPi")
log.append("Kopiere neue Dateien …"); _copy_in(root)
if _read(req) != old_req:
log.append("Installiere Python-Pakete (kann einige Minuten dauern) …")
try: pip_install()
except Exception as e: _restore(); raise RuntimeError("Installation der Python-Pakete fehlgeschlagen, vorherige Version wiederhergestellt: " + str(e))
if _read(inst, "rb") != old_inst: log.append("Hinweis: install.sh hat sich geändert – bitte danach einmal „bash install.sh“ ausführen (Systempakete).")
log.append("Fertig – LabelPi startet neu …"); UPD["state"] = "done"; threading.Timer(2, RESTART).start()
except Exception as e:
log.append(f"Fehler: {e}"); UPD["state"] = "error"
@app.get("/api/update/check")
def u_check(): return jsonify(update_info(request.args.get("force") == "1"))
@app.post("/api/update/apply")
def u_apply():
if UPD["state"] == "running": return jsonify(error="Update läuft bereits"), 409
UPD.update(state="running", log=[]); threading.Thread(target=do_update, daemon=True).start(); return jsonify(ok=True)
@app.get("/api/update/status")
def u_status(): return jsonify(state=UPD["state"], log=UPD["log"], version=VERSION)
@app.post("/api/update/rollback")
def u_rollback():
if not os.path.isdir(BK): return jsonify(error="Keine Sicherung vorhanden"), 404
_restore(); UPD.update(state="done", log=["Vorherige Version wiederhergestellt – LabelPi startet neu …"]); threading.Timer(2, RESTART).start(); return jsonify(ok=True)
@app.get("/api/backup")
def backup():
t = tempfile.NamedTemporaryFile(suffix=".db", delete=False); t.close()
+7
View File
@@ -140,3 +140,10 @@ The interface is available in German and English. Choose the language in the bar
**Translating this guide (optional):** put it into the `lang/` folder (or `docs/`) as `ANLEITUNG.fr.md` – it is then shown under *Help* when that language is active. Without a translation the German guide is shown.
Placeholders such as `{datum}` keep their names; English short forms exist (`{date}` …). Dates, weekdays and month names appear in the selected language. If you would like to share your translation, the project welcomes a pull request.
## 12. Update
- **Notice:** If a newer version exists, a blue bar *“Update available”* appears at the top (✕ dismisses it).
- **Updating:** *Settings → Update → Update now*. LabelPi downloads the new version from GitHub, replaces the program files and restarts (about 1–2 minutes; the page reloads by itself).
- Templates, history, counters, password and custom languages are kept. The previous version is backed up; *Restore previous version* undoes the update.
- If `install.sh` changes (new system packages) a note appears: run `bash install.sh` once on the Pi.
- For security, **password protection** should be on when used in a network – without it anyone on the network can trigger an update.
+7
View File
@@ -140,3 +140,10 @@ Die Oberfläche gibt es in Deutsch und Englisch. Oben rechts in der Leiste wähl
**Anleitung übersetzen (optional):** Diese Anleitung als `ANLEITUNG.fr.md` im Ordner `lang/` (oder `docs/`) ablegen – sie erscheint dann unter *Hilfe*, wenn die Sprache aktiv ist. Ohne Übersetzung wird die deutsche Anleitung gezeigt.
Platzhalter wie `{datum}` behalten ihre Namen; für Englisch gibt es Kurzformen (`{date}` …). Datum, Wochentag und Monatsnamen erscheinen in der gewählten Sprache. Möchtest du deine Übersetzung teilen, freut sich das Projekt über einen Pull Request.
## 12. Update
- **Hinweis:** Gibt es eine neuere Version, erscheint oben ein blauer Balken *„Update verfügbar“* (mit ✕ zum Ausblenden).
- **Aktualisieren:** *Einstellungen → Update → Jetzt aktualisieren*. LabelPi lädt die neue Version von GitHub, ersetzt die Programmdateien und startet sich neu (ca. 1–2 Minuten, die Seite lädt danach von selbst neu).
- Vorlagen, Verlauf, Zähler, Passwort und eigene Sprachen bleiben erhalten. Die vorherige Version wird gesichert; *Vorherige Version wiederherstellen* macht das Update rückgängig.
- Ändert sich `install.sh` (neue Systempakete), erscheint ein Hinweis: dann einmal `bash install.sh` auf dem Pi ausführen.
- Aus Sicherheitsgründen sollte bei Nutzung im Netzwerk der **Passwortschutz** aktiv sein – ohne Schutz kann jeder im Netzwerk ein Update anstoßen.
+1 -1
View File
@@ -28,7 +28,7 @@ Klappt `.local` nicht, die IP in den Geräteinfos des Hotspots nachsehen.
## 3. Dateien auf den Pi bringen
**Variante A (ein Befehl, empfohlen):** direkt auf dem Pi
```
curl -fsSL https://raw.githubusercontent.com/DEIN-USER/labelpi/main/install.sh | bash
curl -fsSL https://raw.githubusercontent.com/olli1986/labelpi/main/install.sh | bash
```
Damit sind Schritt 3 und 4 erledigt, weiter mit Schritt 5.
+32
View File
@@ -0,0 +1,32 @@
# Updates veröffentlichen (für Maintainer)
LabelPi prüft auf GitHub, ob es eine neuere Version gibt, und zeigt dann in der Weboberfläche den Update-Hinweis an.
Welche Version „die neueste“ ist, wird so bestimmt:
1. Gibt es im Repository mindestens ein **Release**, gilt das **neueste Release** (Tag z. B. `v0.7.1`). Nur diese Versionen werden angeboten – halbfertige Änderungen auf `main` erreichen die Nutzer nicht.
2. Gibt es **kein** Release, gilt die Zahl in der Datei **`VERSION`** auf dem `main`-Zweig.
## Empfohlener Ablauf (ohne Git-Kenntnisse, nur im Browser)
1. **Dateien ändern:** auf GitHub im Repository *Add file → Upload files* (bestehende Dateien werden überschrieben) oder eine Datei öffnen und mit dem Stift-Symbol bearbeiten. Mit *Commit changes* speichern.
2. **Version hochzählen:** Datei `VERSION` bearbeiten, z. B. von `0.7.0` auf `0.7.1` (Schema `Haupt.Neben.Fehlerkorrektur`).
3. **Release erstellen:** *Releases → Draft a new release* → bei *Choose a tag* `v0.7.1` eintippen und *Create new tag* wählen → Titel und Beschreibung eintragen (die Beschreibung erscheint den Nutzern als Update-Hinweis) → *Publish release*.
Danach sehen alle Installationen innerhalb von ca. 6 Stunden den Hinweis (oder sofort über *Einstellungen → Update → Nach Updates suchen*).
> Tipp: Der Tag (`v0.7.1`) und die Zahl in `VERSION` (`0.7.1`) müssen übereinstimmen.
## Mit Git auf dem PC
```
git add -A
git commit -m "Beschreibung der Änderung"
git push
git tag v0.7.1
git push --tags
```
Das Release mit Beschreibung legst du danach wie in Schritt 3 auf GitHub an.
## Wichtig
- Nur Dateien im Repository werden verteilt. `labels.db` (Daten) und der Ordner `lang/` (eigene Sprachen) liegen nur auf dem Pi und werden nie überschrieben.
- Neue Python-Pakete in `requirements.txt` werden beim Update automatisch installiert. Neue **Systempakete** (apt) gehören in `install.sh` – dann erscheint für die Nutzer der Hinweis, es einmal auszuführen.
- Zusätzliche Sprachen: Datei `static/lang/xx.json` (Aufbau siehe Anleitung, Abschnitt Sprachen) ins Repository legen.
- Gitea: Die Update-Funktion nutzt aktuell die GitHub-Schnittstellen.
+2 -2
View File
@@ -1,8 +1,8 @@
#!/bin/bash
# LabelPi-Installation auf Raspberry Pi OS Lite.
# Ein-Befehl-Variante: curl -fsSL https://raw.githubusercontent.com/DEIN-USER/labelpi/main/install.sh | bash
# Ein-Befehl-Variante: curl -fsSL https://raw.githubusercontent.com/olli1986/labelpi/main/install.sh | bash
set -e
REPO="${LABELPI_REPO:-https://github.com/DEIN-USER/labelpi.git}"
REPO="${LABELPI_REPO:-https://github.com/olli1986/labelpi.git}"
if [ -f "$(dirname "$0")/app.py" ]; then
cd "$(dirname "$0")"
else
+30 -4
View File
@@ -51,13 +51,15 @@ details.fo>summary small{color:var(--mu);font-weight:400}
@media(min-width:1100px){#td{display:grid;grid-template-columns:minmax(0,1fr) 340px;align-items:start}#td>.bar{grid-column:1/-1}
#wrap{max-height:calc(100vh - var(--toph,100px) - var(--tbh,52px) - 4px)}
#panel{position:sticky;top:calc(var(--toph,100px) + var(--tbh,52px));bottom:auto;max-height:calc(100vh - var(--toph,100px) - var(--tbh,52px));box-shadow:none;border-top:0;border-left:1px solid var(--ln);grid-template-columns:repeat(2,1fr);align-content:start}#panel .w{grid-column:1/-1}}
#upd{display:flex;gap:10px;align-items:center;padding:6px 14px;background:#1f3a5f;border-bottom:1px solid var(--ln);font-size:13px}#upd button{padding:3px 10px;font-size:12px}#upd #updx{border:0;background:none}
#ulog,#unotes{background:var(--p2);padding:10px;border-radius:8px;white-space:pre-wrap;max-height:220px;overflow:auto;font-size:12px;margin:8px 0}#uv1,#uv2{margin-right:14px}
</style></head><body>
<div id="top"><header><b class="logo">🏷️ LabelPi</b><span id="st" title="Drucker"></span><select id="pnt" class="hide"></select><select id="lab"></select>
<label id="lenw">Länge <input id="len" type="number" min="25" max="900" value="100"> mm</label>
<label>Kopien <input id="cp" type="number" min="1" max="50" value="1" style="width:60px"></label>
<span class="adv" id="advh"><label><input id="rot" type="checkbox"> gedreht</label><label><input id="dit" type="checkbox"> Dithering</label><label id="cutw" class="hide"><input id="cut" type="checkbox"> Autoschnitt</label><label id="cmw" class="hide"><input id="cmk" type="checkbox"> Schnittmarke</label></span><button class="p" id="pr">Drucken</button><button id="hlp">? Hilfe</button></header>
<nav class="bar"><button data-t="d" class="on">Designer</button><button data-t="t">Vorlagen</button><button data-t="h">Verlauf</button><button data-t="s">Einstellungen</button><span style="flex:1"></span><label class="advl"><input type="checkbox" id="advc"> Erweiterte Ansicht</label><select id="lng"></select><button id="rb">Neustart</button><button id="sd" class="d">Herunterfahren</button></nav></div>
<nav class="bar"><button data-t="d" class="on">Designer</button><button data-t="t">Vorlagen</button><button data-t="h">Verlauf</button><button data-t="s">Einstellungen</button><span style="flex:1"></span><label class="advl"><input type="checkbox" id="advc"> Erweiterte Ansicht</label><select id="lng"></select><button id="rb">Neustart</button><button id="sd" class="d">Herunterfahren</button></nav><div id="upd" class="hide"><span id="updt"></span><button id="updb" class="p">Zum Update</button><button id="updx" title="Ausblenden">✕</button></div></div>
<section id="td"><div class="bar">
<button data-a="text">+ Text</button><button data-a="qr">+ QR</button><button data-a="table">+ Tabelle</button><button data-a="bc">+ Barcode</button><button id="addimg">+ Bild</button>
<select id="addmore"><option value="">+ Mehr …</option><option value="pdf">PDF importieren</option><option value="rect">Rechteck</option>
@@ -84,7 +86,12 @@ details.fo>summary small{color:var(--mu);font-weight:400}
<small class="mu">Mit {lfd} oder {lfd.name} nummeriert LabelPi fortlaufend und merkt sich die nächste Nummer – auch nach Neustart und Gerätewechsel.</small>
<h3>Sprachen</h3><div class="bar"><button id="lup">Sprachdatei hinzufügen (.json)</button><input type="file" id="lupf" accept=".json" class="hide"><button id="ltpl">Vorlage für neue Sprache herunterladen</button><button id="ldel" class="d">Eigene Sprache entfernen</button></div>
<small class="mu">Dateiname = Sprachcode (z. B. fr.json). Aufbau und Übersetzung der Anleitung: siehe Hilfe, Abschnitt „Sprachen“.</small>
<h3>Über</h3><p style="color:var(--mu)">LabelPi · MIT-Lizenz · entstanden in Zusammenarbeit mit Claude (Anthropic)</p></div></section>
<h3>Update</h3>
<div class="bar"><span id="uv1"></span><span id="uv2"></span><b id="umsg"></b></div>
<div class="bar"><button id="uck">Nach Updates suchen</button><button id="uap" class="p hide">Jetzt aktualisieren</button><button id="urb" class="hide">Vorherige Version wiederherstellen</button></div>
<pre id="unotes" class="hide" data-notr></pre><pre id="ulog" class="hide" data-notr></pre>
<small class="mu">LabelPi prüft auf GitHub, ob eine neuere Version vorhanden ist. Beim Update werden nur Programmdateien ersetzt – Vorlagen, Verlauf, Zähler und eigene Sprachen bleiben erhalten. Die vorherige Version wird gesichert.</small>
<h3>Über</h3><p style="color:var(--mu)">LabelPi · MIT-Lizenz · entstanden in Zusammenarbeit mit Claude (Anthropic)</p><p class="mu" id="verl"></p></div></section>
<dialog id="qdlg"><form method="dialog" id="qf"><h3>QR-Assistent</h3>
<label>Typ<select id="qt"><option value="url">Link (URL)</option><option value="wifi">WLAN-Zugang</option><option value="vcard">Kontakt (vCard)</option><option value="mail">E-Mail</option><option value="tel">Telefon</option><option value="sms">SMS</option><option value="geo">Standort</option><option value="text">Freitext</option></select></label>
<div id="qfld"></div><div class="row"><button type="button" onclick="$('#qdlg').close()">Abbrechen</button><button class="p" value="ok">Übernehmen</button></div></form></dialog>
@@ -225,7 +232,7 @@ async function doPrint(say){const pid=$('#pnt').value||'x',lr=ROLL[pid],nm=id=>t
$('#pr').onclick=T(async()=>{const b=$('#pr');b.disabled=true;b.textContent='Drucke…';try{if(await doPrint(x=>b.textContent=x))b.textContent='Gedruckt ✓'}finally{setPrev();draw();b.disabled=false;setTimeout(()=>b.textContent='Drucken',1500)}});
$('#sv').onclick=T(async()=>{const r=await ask('Als Vorlage speichern','',lastFolder);if(!r||!r.name)return;lastFolder=r.folder;await post('/api/templates',{name:r.name,folder:r.folder,label:S.label,design:dsg(),png:shot()});alert(tt('Vorlage gespeichert'))});
$('#nw').onclick=()=>{if(confirm(tt('Aktuelles Design verwerfen?')))fresh(S.label)};
function go(k){document.querySelectorAll('nav button').forEach(x=>x.classList.toggle('on',x.dataset.t==k));for(const j of['d','t','h','s','f'])$('#t'+j).classList.toggle('hide',j!=k);if(k=='d')sync();if(k=='t')tpl();if(k=='h')hist();if(k=='s')setp();if(k=='f')loadHelp()}
function go(k){document.querySelectorAll('nav button').forEach(x=>x.classList.toggle('on',x.dataset.t==k));for(const j of['d','t','h','s','f'])$('#t'+j).classList.toggle('hide',j!=k);if(k=='d')sync();if(k=='t')tpl();if(k=='h')hist();if(k=='s'){setp();updUI()}if(k=='f')loadHelp()}
document.querySelectorAll('nav button').forEach(b=>b.onclick=()=>go(b.dataset.t));
function open_(d){S=JSON.parse(d);if(!LB.find(l=>l.id==S.label))S.label='62';sel=null;go('d');resetU();sv_()}
let lastFolder='',hm=false,hs=new Set(),HL=[],TP={};
@@ -406,6 +413,25 @@ const quick=T(async id=>{const d=JSON.parse((await api('/api/templates/'+id)).de
$('#kfld').oninput=prev;prev();dg.returnValue='';
dg.onclose=T(async()=>{if(dg.returnValue!='ok')return;const old=$('#cp').value;$('#cp').value=$('#kcp').value;try{await withS(d,async()=>{setR();await doPrint(()=>{})})}finally{$('#cp').value=old}});dg.showModal()});
Object.entries({'#cmk':'Gestrichelte Linie am Etikettenende zum manuellen Abschneiden (Endlosrollen ohne Cutter)','#pvp':'Vorheriges Etikett der Serie anzeigen','#pvn':'Nächstes Etikett der Serie anzeigen','#lng':'Sprache der Oberfläche'}).forEach(([s,x])=>{const el=$(s);if(el)el.title=x});
let UPD=null;
async function updCheck(force){try{UPD=await api('/api/update/check'+(force?'?force=1':''))}catch(e){UPD=null}updUI()}
function updUI(){const u=UPD;let dis='';try{dis=localStorage.getItem('updx')}catch(e){}
$('#upd').classList.toggle('hide',!(u&&u.available&&dis!=u.latest));if(!u)return;
$('#updt').textContent=`Update verfügbar: Version ${u.latest} (installiert: ${u.current})`;$('#uv1').textContent=`Installiert: ${u.current}`;
$('#uv2').textContent=u.error?'':`Neueste Version: ${u.latest}`;$('#umsg').textContent=u.error?'Prüfung nicht möglich (Internet?)':u.available?'Update verfügbar.':'LabelPi ist auf dem neuesten Stand.';
$('#uap').classList.toggle('hide',!u.available);$('#urb').classList.toggle('hide',!u.backup);const n=$('#unotes');n.textContent=u.notes||'';n.classList.toggle('hide',!u.notes||!u.available);$('#verl').textContent=`Version ${u.current}`}
$('#updx').onclick=()=>{try{localStorage.setItem('updx',UPD.latest)}catch(e){}updUI()};
$('#updb').onclick=()=>{go('s');$('#uv1').scrollIntoView()};
$('#uck').onclick=T(async()=>{await updCheck(true)});
async function pollUpd(target){const lg=$('#ulog');lg.classList.remove('hide');$('#uap').disabled=true;let down=false;
for(let i=0;i<300;i++){await new Promise(r=>setTimeout(r,2000));
try{const s=await api('/api/update/status');if(s.log.length)lg.textContent=s.log.map(x=>tt(x)).join('\n');
if(s.state=='error'){alert(tt('Update fehlgeschlagen')+': '+tt(s.log[s.log.length-1]||''));$('#uap').disabled=false;return}
if(down||(s.state=='idle'&&i>2)){await updCheck(true);if(!target||UPD.current==target){alert(tt('Update abgeschlossen'));location.reload();return}}}
catch(e){if(!down)lg.textContent+='\n'+tt('LabelPi startet neu …');down=true}}
$('#uap').disabled=false}
$('#uap').onclick=T(async()=>{if(!confirm(tt('LabelPi jetzt auf Version {} aktualisieren? Danach startet das Programm neu (ca. 1–2 Minuten).',UPD.latest)))return;await post('/api/update/apply',{});pollUpd(UPD.latest)});
$('#urb').onclick=T(async()=>{if(!confirm(tt('Vorherige Version wiederherstellen? Das Programm startet danach neu.')))return;await post('/api/update/rollback',{});pollUpd(null)});
const resetU=()=>{U=[JSON.stringify(S.els)];RD=[]};
function commit(){const j=JSON.stringify(S.els);if(j!=U[U.length-1]){U.push(j);if(U.length>25)U.shift();RD=[]}}
function undo(){if(U.length>1){RD.push(U.pop());S.els=JSON.parse(U[U.length-1]);sel=null;draw();panel()}}
@@ -434,5 +460,5 @@ $('#pdff').onchange=T(async ev=>{const f=ev.target.files[0];ev.target.value='';i
if(pages)S.ser={mode:'pdf',pages};add({t:'img',pg:pages?1:0,x:0,y:0,w:Math.round(nw*k),h:Math.round(nh*k),src:r.png});sync();sv_()});
(async()=>{try{await initLang();const c=await api('/api/settings').catch(()=>({}));await loadP(c.printer);await loadL();
fresh(LB.find(l=>l.id==c.label)?c.label:(LB.find(l=>l.id=='62')?'62':LB[0].id));
if(c.len)S.len=Math.min(900,Math.max(25,+c.len||100));if(typeof c.rot=='boolean')S.rot=c.rot;ROLL=c.roll||{};$('#cmk').checked=!!c.cmk;CNT=await api('/api/counters').catch(()=>({}));if(c.cw)S.cw=c.cw;if(c.grid)S.grid=c.grid;$('#dit').checked=!!c.dither;$('#cut').checked=!!c.cut;sync();st();setInterval(st,20000)}catch(e){alert(tt('Fehler')+': '+tt(e))}})();
if(c.len)S.len=Math.min(900,Math.max(25,+c.len||100));if(typeof c.rot=='boolean')S.rot=c.rot;ROLL=c.roll||{};$('#cmk').checked=!!c.cmk;CNT=await api('/api/counters').catch(()=>({}));if(c.cw)S.cw=c.cw;if(c.grid)S.grid=c.grid;$('#dit').checked=!!c.dither;$('#cut').checked=!!c.cut;sync();st();setInterval(st,20000);updCheck(false);setInterval(()=>updCheck(false),6*3600*1000)}catch(e){alert(tt('Fehler')+': '+tt(e))}})();
</script></body></html>