diff --git a/README.md b/README.md index e22423b..f76d81a 100644 --- a/README.md +++ b/README.md @@ -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://.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ß). diff --git a/VERSION b/VERSION new file mode 100644 index 0000000..faef31a --- /dev/null +++ b/VERSION @@ -0,0 +1 @@ +0.7.0 diff --git a/app.py b/app.py index 3f51e7e..0f52d38 100644 --- a/app.py +++ b/app.py @@ -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/ 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() diff --git a/docs/ANLEITUNG.en.md b/docs/ANLEITUNG.en.md index 9be9048..0e8b7f9 100644 --- a/docs/ANLEITUNG.en.md +++ b/docs/ANLEITUNG.en.md @@ -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. diff --git a/docs/ANLEITUNG.md b/docs/ANLEITUNG.md index 5837b09..10988d9 100644 --- a/docs/ANLEITUNG.md +++ b/docs/ANLEITUNG.md @@ -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. diff --git a/docs/INSTALL.md b/docs/INSTALL.md index 5604dbc..14482a5 100644 --- a/docs/INSTALL.md +++ b/docs/INSTALL.md @@ -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. diff --git a/docs/RELEASING.md b/docs/RELEASING.md new file mode 100644 index 0000000..f085e48 --- /dev/null +++ b/docs/RELEASING.md @@ -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. diff --git a/install.sh b/install.sh index 1da8b99..eedefa8 100644 --- a/install.sh +++ b/install.sh @@ -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 diff --git a/static/index.html b/static/index.html index 4e77f61..2c62352 100644 --- a/static/index.html +++ b/static/index.html @@ -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}
-
+
Dateiname = Sprachcode (z. B. fr.json). Aufbau und Übersetzung der Anleitung: siehe Hilfe, Abschnitt „Sprachen“. -

Über

LabelPi · MIT-Lizenz · entstanden in Zusammenarbeit mit Claude (Anthropic)

+

Update

+
+
+

+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.
+

Über

LabelPi · MIT-Lizenz · entstanden in Zusammenarbeit mit Claude (Anthropic)

QR-Assistent

@@ -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))}})();