Upload per ZIP-Tool (1/3)
This commit is contained in:
1 parent
6746a7a5d4
commit
e3b021ee1d
9 files changed
+199
-11
No files matched your search
@@ -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):
|
**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`.
|
**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`).
|
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
|
## Hinweise
|
||||||
- Rot/Schwarz-Rollen werden nicht unterstützt (der Designer ist Schwarz/Weiß).
|
- Rot/Schwarz-Rollen werden nicht unterstützt (der Designer ist Schwarz/Weiß).
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
"""LabelPi – Etikettendruck für Brother-QL-Drucker per Weboberfläche (MIT-Lizenz)."""
|
"""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 flask import Flask, Response, jsonify, request, send_file, send_from_directory
|
||||||
from PIL import Image
|
from PIL import Image
|
||||||
import barcode
|
import barcode
|
||||||
@@ -13,8 +13,8 @@ from brother_ql.backends.helpers import send, discover
|
|||||||
from brother_ql.labels import ALL_LABELS
|
from brother_ql.labels import ALL_LABELS
|
||||||
from brother_ql.models import ALL_MODELS
|
from brother_ql.models import ALL_MODELS
|
||||||
|
|
||||||
VERSION = "0.6.0"
|
|
||||||
BASE = os.path.dirname(os.path.abspath(__file__))
|
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")
|
DB = os.path.join(BASE, "labels.db")
|
||||||
app = Flask(__name__, static_folder="static")
|
app = Flask(__name__, static_folder="static")
|
||||||
lock = threading.Lock()
|
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(markdown.markdown(open(p, encoding="utf-8").read(), extensions=["tables", "fenced_code"]), mimetype="text/html")
|
||||||
return Response("", 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")
|
@app.get("/api/backup")
|
||||||
def backup():
|
def backup():
|
||||||
t = tempfile.NamedTemporaryFile(suffix=".db", delete=False); t.close()
|
t = tempfile.NamedTemporaryFile(suffix=".db", delete=False); t.close()
|
||||||
|
|||||||
@@ -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.
|
**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.
|
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.
|
||||||
@@ -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.
|
**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.
|
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
@@ -28,7 +28,7 @@ Klappt `.local` nicht, die IP in den Geräteinfos des Hotspots nachsehen.
|
|||||||
## 3. Dateien auf den Pi bringen
|
## 3. Dateien auf den Pi bringen
|
||||||
**Variante A (ein Befehl, empfohlen):** direkt auf dem Pi
|
**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.
|
Damit sind Schritt 3 und 4 erledigt, weiter mit Schritt 5.
|
||||||
|
|
||||||
|
|||||||
@@ -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
@@ -1,8 +1,8 @@
|
|||||||
#!/bin/bash
|
#!/bin/bash
|
||||||
# LabelPi-Installation auf Raspberry Pi OS Lite.
|
# 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
|
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
|
if [ -f "$(dirname "$0")/app.py" ]; then
|
||||||
cd "$(dirname "$0")"
|
cd "$(dirname "$0")"
|
||||||
else
|
else
|
||||||
|
|||||||
+30
-4
@@ -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}
|
@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)}
|
#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}}
|
#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>
|
</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>
|
<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 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>
|
<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>
|
<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">
|
<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>
|
<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>
|
<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>
|
<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>
|
<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>
|
<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>
|
<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>
|
<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>
|
<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)}});
|
$('#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'))});
|
$('#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)};
|
$('#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));
|
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_()}
|
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={};
|
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='';
|
$('#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()});
|
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});
|
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=[]};
|
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 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()}}
|
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_()});
|
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();
|
(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));
|
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>
|
</script></body></html>
|
||||||
Reference in new issue
Block a user