feat: Logo-Upload mit automatischem Freistellen
Findet die automatische Suche kein Logo, kann pro Kunde eine Bilddatei hochgeladen werden. Ein Upload hat Vorrang vor der Suche. Freistellen: - remove_logo_background() entfernt den Hintergrund per Flood Fill von den Bildraendern her. Bewusst kein globaler Farbvergleich - sonst verschwindet auch Weiss innerhalb des Logos (etwa Text in einer Flaeche). Weiche Kante gegen Treppeneffekte. - Umgesetzt mit Pillow, das ueber qrcode[pil] ohnehin vorhanden ist. Bewusst kein ML-Modell wie rembg: der Container laeuft mit read-only Root-FS, ein Modell-Cache zur Laufzeit waere dort nicht moeglich. - Bereits transparente Bilder bleiben unangetastet. Ist der Bildrand nicht einfarbig, wird nicht freigestellt und die Oberflaeche weist darauf hin, statt ein beschaedigtes Logo zu liefern. - Checkbox "Hintergrund behalten" schaltet das Freistellen pro Kunde ab. - Gespeichert wird immer als PNG (JPEG kann keine Transparenz), grosse Bilder werden auf 800px verkleinert. Ablage unter media/<site>/logos/logo_<slot>.png, neue Route /media/<site>/logos/<datei>. Die URL traegt einen Zeitstempel, damit Player kein altes Logo aus dem Cache zeigen. Nebenbei behoben: welcome_data speicherte nur die erfolgreichen Kunden statt der Felder 1-3. Fiel Kunde 1 aus, rutschte Kunde 2 beim naechsten Aufruf in Feld 1 und Logo wie Ansprechpartner verschoben sich mit. Jetzt feldweise - dadurch bleibt auch ein hochgeladenes Logo beim erneuten Generieren erhalten, solange der Kundenname im selben Feld steht. Die drei Kundenbloecke im Formular sind zu einer Schleife zusammengefasst, damit die Upload-Felder nicht dreifach gepflegt werden muessen. README, AGENTS.md und Hilfeseite nachgezogen. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
@@ -121,6 +121,9 @@ Standorte (sites) sind die oberste Organisationsebene und gruppieren Screens.
|
||||
- Gemeinsame HTML-Bausteine: `_header.html`, `_footer.html`, `_styles.html` (CSS-Variablen `--ccm-*`, Dark Mode, Card-Border-Radius 1rem).
|
||||
- `add_customer_to_playlists` (früher `add_customer_to_lobby_playlist`) entfernt `welcome.html` als String *und* als Dict aus **allen** Screens des Standorts und fügt sie danach nur in den Zielscreens an Position 0 wieder ein – ein abgewählter Screen bleibt so nicht mit einer alten Begrüßung stehen. Eingefügt wird ein Dict, kein String, damit `enabled` und `schedule` mitgeführt werden können.
|
||||
- `welcome_target_screens(config, site)` fällt nur dann auf `lobby` zurück, wenn **kein** Screen des Standorts den Schlüssel `welcome_enabled` besitzt. Wurde er überall bewusst auf `False` gesetzt, ist die leere Liste das gewollte Ergebnis.
|
||||
- **Logo-Upload**: Pro Kunde kann im Willkommensseiten-Formular eine Bilddatei hochgeladen werden (`logo_file_1..3`); ein Upload hat Vorrang vor `search_customer_logo`. Ablage als `media/<site>/logos/logo_<slot>.png`, Auslieferung über `/media/<site>/logos/<datei>`. Die URL traegt einen `?v=<timestamp>`, damit Player kein altes Logo aus dem Cache zeigen.
|
||||
- `remove_logo_background()` arbeitet mit Pillow (Flood Fill von den Raendern, weiche Kante). **Bewusst kein ML-Modell** wie rembg: der Container laeuft mit read-only Root-FS ohne Modell-Cache. Flood Fill statt globalem Farbvergleich ist wesentlich - sonst verschwindet auch Weiss *innerhalb* des Logos. Bereits transparente Bilder werden unveraendert durchgereicht; bei unruhigem Bildrand bleibt das Bild unangetastet und die Funktion gibt einen Hinweis zurueck.
|
||||
- `welcome_data` in der Site-Config wird **feldweise** gespeichert (Slot 1-3, auch leere), nicht nur die erfolgreichen Kunden - sonst verschiebt sich die Zuordnung von Name, Logo und Ansprechpartner beim naechsten Aufruf. Ein zuvor hochgeladenes Logo wird beim erneuten Generieren wiederverwendet, solange der Kundenname im selben Feld unveraendert ist.
|
||||
- Weder `save_welcome_page` noch `add_customer_to_playlists` legen Screens an. Das frühere `screens.setdefault("lobby", {})` erzeugte an Standorten ohne Lobby stillschweigend einen Screen.
|
||||
- `add_screen` legt ein Verzeichnis unter `media/<site>/<screen>/` an und einen Config-Eintrag.
|
||||
- `delete_screen` entfernt den Screen aus der Config und löscht das Verzeichnis rekursiv.
|
||||
|
||||
@@ -10,7 +10,7 @@ Browserbasiertes Digital-Signage-System für interne Info-Screens.
|
||||
- Bilder, Videos (MP4/H.264), HTML-Seiten, URL-Playlist-Einträge
|
||||
- **Multi-Standort**: Standorte (sites) gruppieren Screens
|
||||
- **Priority-Playlist**: globale Inhalte wirken auf alle Player
|
||||
- **Willkommensseite**: bis zu 3 Kundenlogos via OpenAI + Brandfetch, pro Standort konfigurierbares Hintergrundbild; erscheint auf allen Screens mit `welcome_enabled` (mehrere möglich), optional mit Ablaufdatum
|
||||
- **Willkommensseite**: bis zu 3 Kundenlogos via OpenAI + Brandfetch oder **manueller Upload mit automatischem Freistellen** (transparenter Hintergrund), pro Standort konfigurierbares Hintergrundbild; erscheint auf allen Screens mit `welcome_enabled` (mehrere möglich), optional mit Ablaufdatum
|
||||
- Pro Screen eigene Playlist mit Drag-&-Drop-Reihenfolge
|
||||
- Auto-Reload bei Playlist-Änderungen
|
||||
- Newsticker pro Screen
|
||||
@@ -221,6 +221,7 @@ Server läuft auf `http://localhost:5005` (gunicorn mit 4 Workern, non-root User
|
||||
| `POST /admin/users/edit/<email>` | User bearbeiten speichern (Admin) |
|
||||
| `GET /media/<site>/<screen>/<file>` | Medien-Datei ausliefern |
|
||||
| `GET /media/priority/<file>` | Priority-Medien (global) |
|
||||
| `GET /media/<site>/logos/<file>` | Manuell hochgeladenes Kundenlogo |
|
||||
| `GET /media/<site>/background/<filename>` | Hintergrundbild der Willkommensseite |
|
||||
|
||||
### Heartbeat / Online-Status
|
||||
@@ -295,6 +296,10 @@ Maximal 3 Kunden eingeben → Logos werden via OpenAI + Brandfetch gesucht → `
|
||||
|
||||
**Zielscreens:** Jeder Screen mit `welcome_enabled` (Schalter „Willkommensseite hier anzeigen" im Tab Einstellungen). Besitzt kein Screen des Standorts den Schlüssel, greift das alte Verhalten und die Seite geht nur in `lobby`. Wird ein Screen später abgewählt, verschwindet die Willkommensseite dort beim nächsten Generieren.
|
||||
|
||||
**Logo-Upload:** Findet die automatische Suche kein Logo (oder passt es nicht), kann pro Kunde eine Bilddatei hochgeladen werden. Ein Upload hat Vorrang vor der Suche. Die Datei wird als `media/<site>/logos/logo_<slot>.png` abgelegt und über `/media/<site>/logos/<datei>` ausgeliefert.
|
||||
|
||||
**Freistellen:** Beim Upload wird der Hintergrund automatisch entfernt, damit das Logo nicht in einem weissen Kasten auf dem Hintergrundbild steht. Verfahren: Flood Fill von den Bildrändern her (bewusst kein globaler Farbvergleich – so bleibt Weiss *innerhalb* des Logos erhalten), weiche Kante gegen Treppeneffekte, Speicherung immer als PNG. Bereits transparente Bilder werden unverändert übernommen; ist der Bildrand nicht einfarbig, bleibt das Bild unangetastet und die Oberfläche weist darauf hin. Mit der Checkbox „Hintergrund behalten" lässt sich das Freistellen pro Kunde abschalten. Umgesetzt mit Pillow – bewusst ohne ML-Modell, da der Container mit read-only Root-FS läuft und kein Modell-Cache zur Verfügung steht.
|
||||
|
||||
**Anzeigen bis:** Optionales Datum im Formular. Ist es gesetzt, bekommt der Eintrag eine Zeitsteuerung (`daily 00:00–23:59` mit `valid_until`) und verschwindet danach auf allen Screens von selbst.
|
||||
|
||||
**Ansprechpartner:** Pro Kunde können Kontaktdaten eingegeben werden (Markdown-Format).
|
||||
@@ -502,6 +507,8 @@ Optional pro Eintrag. Ohne `schedule` läuft der Eintrag immer (sofern `enabled`
|
||||
- `schedule_text(schedule)` – Jinja-Filter: Klartext einer Zeitsteuerung für Tooltips
|
||||
- `load_priority_files()` – Priority-Playlist + Dateien laden
|
||||
- `welcome_target_screens(config, site)` – Zielscreens der Willkommensseite (Fallback `lobby`)
|
||||
- `save_uploaded_logo(file, site, slot, freistellen=True)` – hochgeladenes Logo freistellen und als PNG ablegen
|
||||
- `remove_logo_background(image)` – Hintergrund per Flood Fill entfernen, gibt `(bild, hinweis)` zurück
|
||||
- `prio_redirect(site)` – Redirect-Pfad zur Priority-Seite
|
||||
- `get_background_url(site)` – URL zum benutzerdefinierten Hintergrundbild oder `None`
|
||||
- `load_users()` / `save_users()` / `get_user()` – User aus `users.json`
|
||||
|
||||
82
app.py
82
app.py
@@ -493,6 +493,14 @@ def media_background(site, filename):
|
||||
return send_from_directory(path, filename)
|
||||
|
||||
|
||||
@app.route("/media/<site>/logos/<path:filename>")
|
||||
def media_logo(site, filename):
|
||||
"""Liefert ein manuell hochgeladenes Kundenlogo aus.
|
||||
Ohne Login erreichbar wie die übrigen Medien - der Player ist nicht angemeldet."""
|
||||
path = os.path.join(MEDIA_DIR, site, generate_welcome_page.LOGO_DIRNAME)
|
||||
return send_from_directory(path, filename)
|
||||
|
||||
|
||||
# -------------------------------------------------
|
||||
# User-Verwaltung (Flask-Login mit users.json)
|
||||
# -------------------------------------------------
|
||||
@@ -901,6 +909,7 @@ def add_customer():
|
||||
customer_names = ["", "", ""]
|
||||
contacts = ["", "", ""]
|
||||
valid_until = (request.form.get("valid_until") or "").strip()
|
||||
logo_hinweise = []
|
||||
site = request.args.get("site") or request.form.get("site") or ""
|
||||
|
||||
# Fallback auf ersten für den User zugänglichen Standort
|
||||
@@ -934,31 +943,52 @@ def add_customer():
|
||||
error = "Kein Standort ausgewählt"
|
||||
else:
|
||||
try:
|
||||
found_logos = []
|
||||
for name in filled_names:
|
||||
logo = generate_welcome_page.search_customer_logo(name)
|
||||
found_logos.append(logo)
|
||||
# Logo je Kunde bestimmen: ein hochgeladenes Logo hat Vorrang vor
|
||||
# der automatischen Suche - der Upload ist eine bewusste Entscheidung.
|
||||
# Zuletzt verwendete Logos, um einen früheren Upload nicht bei jedem
|
||||
# erneuten Generieren zu verlieren
|
||||
frueher = load_config().get("sites", {}).get(site, {}).get("welcome_data", {})
|
||||
frueher_namen = frueher.get("names", [])
|
||||
frueher_urls = frueher.get("logo_urls", [])
|
||||
logo_prefix = f"/media/{site}/{generate_welcome_page.LOGO_DIRNAME}/"
|
||||
|
||||
valid = [(n, l) for n, l in zip(filled_names, found_logos) if l]
|
||||
found_logos = [None, None, None]
|
||||
for i, name in enumerate(customer_names):
|
||||
if not name:
|
||||
continue
|
||||
datei = request.files.get(f"logo_file_{i + 1}")
|
||||
if datei and datei.filename:
|
||||
url, hinweis = generate_welcome_page.save_uploaded_logo(
|
||||
datei, site, i + 1,
|
||||
freistellen=(f"logo_keep_bg_{i + 1}" not in request.form))
|
||||
found_logos[i] = url
|
||||
if hinweis:
|
||||
logo_hinweise.append(f"{name}: {hinweis}")
|
||||
elif not url:
|
||||
logo_hinweise.append(f"{name}: Logo konnte nicht übernommen werden")
|
||||
continue
|
||||
|
||||
# Kein neuer Upload: ein zuvor hochgeladenes Logo weiterverwenden,
|
||||
# solange der Kundenname in diesem Feld derselbe geblieben ist
|
||||
alt_url = frueher_urls[i] if i < len(frueher_urls) else None
|
||||
alt_name = frueher_namen[i] if i < len(frueher_namen) else None
|
||||
if alt_url and alt_name == name and str(alt_url).startswith(logo_prefix):
|
||||
found_logos[i] = alt_url
|
||||
continue
|
||||
|
||||
found_logos[i] = generate_welcome_page.search_customer_logo(name)
|
||||
|
||||
logo_urls = list(found_logos)
|
||||
valid = [(customer_names[i], found_logos[i], contacts[i])
|
||||
for i in range(len(customer_names))
|
||||
if customer_names[i] and found_logos[i]]
|
||||
|
||||
if not valid:
|
||||
error = "Kein Logo konnte gefunden werden"
|
||||
error = "Kein Logo gefunden – bitte für die betroffenen Kunden ein Logo hochladen"
|
||||
else:
|
||||
valid_names = [n for n, l in valid]
|
||||
valid_urls = [l for n, l in valid]
|
||||
valid_contacts = []
|
||||
for name in valid_names:
|
||||
for i, cn in enumerate(customer_names):
|
||||
if cn == name:
|
||||
valid_contacts.append(contacts[i] if i < len(contacts) else "")
|
||||
break
|
||||
else:
|
||||
valid_contacts.append("")
|
||||
for i, name in enumerate(customer_names):
|
||||
for j, (vn, vu) in enumerate(zip(valid_names, valid_urls)):
|
||||
if vn == name:
|
||||
logo_urls[i] = vu
|
||||
break
|
||||
valid_names = [n for n, u, k in valid]
|
||||
valid_urls = [u for n, u, k in valid]
|
||||
valid_contacts = [k for n, u, k in valid]
|
||||
|
||||
bg_url = get_background_url(site)
|
||||
targets = generate_welcome_page.welcome_target_screens(load_config(), site)
|
||||
@@ -978,7 +1008,14 @@ def add_customer():
|
||||
# Letzte Kundendaten für Background-Update speichern
|
||||
config = load_config()
|
||||
site_cfg = config.setdefault("sites", {}).setdefault(site, {})
|
||||
site_cfg["welcome_data"] = {"names": valid_names, "logo_urls": valid_urls, "contacts": valid_contacts}
|
||||
# Feldweise speichern (Slot 1-3), nicht nur die erfolgreichen -
|
||||
# sonst rutscht Kunde 2 beim naechsten Aufruf in Feld 1 und die
|
||||
# Zuordnung von Logo und Ansprechpartner verschiebt sich mit.
|
||||
site_cfg["welcome_data"] = {
|
||||
"names": list(customer_names),
|
||||
"logo_urls": list(found_logos),
|
||||
"contacts": list(contacts)
|
||||
}
|
||||
save_config(config)
|
||||
add_history_entry("customer_added", f"Kunden ({', '.join(valid_names)}) zur Willkommensseite an Standort '{site}' hinzugefügt (Screens: {', '.join(targets)})", current_user.email)
|
||||
success = (f"✅ Willkommensseite mit {len(valid_names)} Kunden erstellt "
|
||||
@@ -1019,6 +1056,7 @@ def add_customer():
|
||||
customer_names=customer_names,
|
||||
contacts=contacts,
|
||||
welcome_screens=generate_welcome_page.welcome_target_screens(config, site) if site else [],
|
||||
logo_hinweise=logo_hinweise,
|
||||
site=site,
|
||||
site_list=site_list,
|
||||
current_site=site,
|
||||
|
||||
@@ -9,6 +9,7 @@ Zielscreens ein (Screens mit "welcome_enabled", Fallback "lobby").
|
||||
|
||||
import os
|
||||
import json
|
||||
import time
|
||||
from openai import OpenAI
|
||||
|
||||
BASE_DIR = os.path.dirname(os.path.abspath(__file__))
|
||||
@@ -32,6 +33,159 @@ def get_openai_client():
|
||||
# Brandfetch-Logo-Suche
|
||||
# -------------------------------------------------
|
||||
|
||||
# -------------------------------------------------
|
||||
# Logo-Upload: Freistellen
|
||||
# -------------------------------------------------
|
||||
|
||||
LOGO_MAX_SIZE = 800 # Kantenlänge, auf die grosse Uploads verkleinert werden
|
||||
LOGO_TOLERANCE = 32 # Farbabstand, bis zu dem ein Pixel als Hintergrund gilt
|
||||
LOGO_BORDER_UNIFORMITY = 48 # Streuung, bis zu der ein Rand als einfarbig durchgeht
|
||||
|
||||
|
||||
def _color_distance(a, b):
|
||||
"""Abstand zweier RGB-Werte (Manhattan über die Kanäle, gemittelt)."""
|
||||
return (abs(a[0] - b[0]) + abs(a[1] - b[1]) + abs(a[2] - b[2])) / 3.0
|
||||
|
||||
|
||||
def _border_color(px, w, h):
|
||||
"""Ermittelt die Hintergrundfarbe aus den Randpixeln.
|
||||
Gibt (farbe, einfarbig) zurück - einfarbig=False, wenn der Rand zu
|
||||
stark streut und ein Freistellen deshalb nicht sinnvoll ist."""
|
||||
rand = []
|
||||
schritt = max(1, min(w, h) // 100)
|
||||
for x in range(0, w, schritt):
|
||||
rand.append(px[x, 0][:3])
|
||||
rand.append(px[x, h - 1][:3])
|
||||
for y in range(0, h, schritt):
|
||||
rand.append(px[0, y][:3])
|
||||
rand.append(px[w - 1, y][:3])
|
||||
|
||||
mitte = tuple(sorted(k[i] for k in rand)[len(rand) // 2] for i in range(3))
|
||||
streuung = sum(_color_distance(k, mitte) for k in rand) / len(rand)
|
||||
return mitte, streuung <= LOGO_BORDER_UNIFORMITY
|
||||
|
||||
|
||||
def remove_logo_background(image, tolerance=LOGO_TOLERANCE):
|
||||
"""Stellt ein Logo frei: entfernt den zusammenhängenden Hintergrund von den
|
||||
Rändern her und macht ihn transparent.
|
||||
|
||||
Bewusst ein Flood Fill von den Rändern und kein globaler Farbvergleich -
|
||||
so bleibt Weiss *innerhalb* des Logos (etwa Text in einer Fläche) erhalten.
|
||||
|
||||
Gibt (bild, hinweis) zurück. hinweis ist None bei Erfolg, sonst ein Text
|
||||
für die Oberfläche - das Bild wird dann unverändert zurückgegeben.
|
||||
"""
|
||||
from collections import deque
|
||||
from PIL import Image
|
||||
|
||||
img = image.convert("RGBA")
|
||||
|
||||
if max(img.size) > LOGO_MAX_SIZE:
|
||||
img.thumbnail((LOGO_MAX_SIZE, LOGO_MAX_SIZE), Image.LANCZOS)
|
||||
|
||||
# Bereits transparent? Dann nicht anfassen.
|
||||
alpha = img.getchannel("A")
|
||||
if alpha.getextrema()[0] < 255:
|
||||
return img, None
|
||||
|
||||
w, h = img.size
|
||||
px = img.load()
|
||||
bg, einfarbig = _border_color(px, w, h)
|
||||
|
||||
if not einfarbig:
|
||||
return img, ("Der Bildrand ist nicht einfarbig – das Logo wurde unverändert "
|
||||
"übernommen. Am besten ein PNG mit transparentem Hintergrund hochladen.")
|
||||
|
||||
# Flood Fill von allen Randpixeln aus
|
||||
besucht = bytearray(w * h)
|
||||
queue = deque()
|
||||
for x in range(w):
|
||||
for y in (0, h - 1):
|
||||
queue.append((x, y))
|
||||
for y in range(h):
|
||||
for x in (0, w - 1):
|
||||
queue.append((x, y))
|
||||
|
||||
while queue:
|
||||
x, y = queue.popleft()
|
||||
i = y * w + x
|
||||
if besucht[i]:
|
||||
continue
|
||||
besucht[i] = 1
|
||||
if _color_distance(px[x, y], bg) > tolerance:
|
||||
continue
|
||||
px[x, y] = (0, 0, 0, 0)
|
||||
if x > 0: queue.append((x - 1, y))
|
||||
if x < w - 1: queue.append((x + 1, y))
|
||||
if y > 0: queue.append((x, y - 1))
|
||||
if y < h - 1: queue.append((x, y + 1))
|
||||
|
||||
# Weiche Kante: Pixel direkt an der Freistellung teilweise durchscheinen
|
||||
# lassen, sonst bleibt ein harter Treppenrand stehen.
|
||||
for y in range(h):
|
||||
for x in range(w):
|
||||
r, g, b, a = px[x, y]
|
||||
if a == 0:
|
||||
continue
|
||||
nachbar_frei = (
|
||||
(x > 0 and px[x - 1, y][3] == 0) or
|
||||
(x < w - 1 and px[x + 1, y][3] == 0) or
|
||||
(y > 0 and px[x, y - 1][3] == 0) or
|
||||
(y < h - 1 and px[x, y + 1][3] == 0)
|
||||
)
|
||||
if not nachbar_frei:
|
||||
continue
|
||||
d = _color_distance((r, g, b), bg)
|
||||
if d < tolerance * 2:
|
||||
px[x, y] = (r, g, b, int(255 * max(0.0, (d - tolerance) / tolerance)))
|
||||
|
||||
if not any(px[x, y][3] == 0 for x in range(0, w, max(1, w // 50)) for y in (0, h - 1)):
|
||||
return img, "Es wurde kein zusammenhängender Hintergrund gefunden."
|
||||
|
||||
return img, None
|
||||
|
||||
|
||||
LOGO_DIRNAME = "logos"
|
||||
LOGO_EXTENSIONS = (".png", ".jpg", ".jpeg", ".gif", ".webp", ".bmp")
|
||||
|
||||
|
||||
def save_uploaded_logo(file_storage, site, slot, freistellen=True):
|
||||
"""Nimmt ein hochgeladenes Logo entgegen, stellt es optional frei und legt es
|
||||
unter media/<site>/logos/logo_<slot>.png ab.
|
||||
|
||||
Gespeichert wird immer als PNG - JPEG kann keine Transparenz.
|
||||
Gibt (url, hinweis) zurück; url ist None, wenn die Datei unbrauchbar war.
|
||||
Der Zeitstempel in der URL verhindert, dass Player ein altes Logo
|
||||
aus dem Cache anzeigen."""
|
||||
from PIL import Image, UnidentifiedImageError
|
||||
|
||||
name = (file_storage.filename or "").lower()
|
||||
if not name.endswith(LOGO_EXTENSIONS):
|
||||
return None, f"Format nicht unterstützt ({', '.join(LOGO_EXTENSIONS)})."
|
||||
|
||||
try:
|
||||
img = Image.open(file_storage.stream)
|
||||
img.load()
|
||||
except (UnidentifiedImageError, OSError) as e:
|
||||
return None, f"Datei konnte nicht gelesen werden: {e}"
|
||||
|
||||
hinweis = None
|
||||
if freistellen:
|
||||
img, hinweis = remove_logo_background(img)
|
||||
else:
|
||||
img = img.convert("RGBA")
|
||||
|
||||
logo_dir = os.path.join(MEDIA_DIR, site, LOGO_DIRNAME)
|
||||
os.makedirs(logo_dir, exist_ok=True)
|
||||
dateiname = f"logo_{slot}.png"
|
||||
img.save(os.path.join(logo_dir, dateiname), "PNG")
|
||||
|
||||
url = f"/media/{site}/{LOGO_DIRNAME}/{dateiname}?v={int(time.time())}"
|
||||
print(f"✅ Generate - Logo hochgeladen: {url}"
|
||||
+ (f" (Hinweis: {hinweis})" if hinweis else ""))
|
||||
return url, hinweis
|
||||
|
||||
|
||||
def get_brandfetch_logo(domain):
|
||||
"""Versucht, ein Firmenlogo über Brandfetch CDN abzurufen.
|
||||
Baut eine direkte CDN-URL: https://cdn.brandfetch.io/{domain}/logo"""
|
||||
|
||||
@@ -104,6 +104,14 @@
|
||||
{% if success %}
|
||||
<div class="alert alert-success">{{ success }}</div>
|
||||
{% endif %}
|
||||
{% if logo_hinweise %}
|
||||
<div class="alert alert-warning">
|
||||
<strong>Hinweise zu den hochgeladenen Logos:</strong>
|
||||
<ul class="mb-0">
|
||||
{% for h in logo_hinweise %}<li>{{ h }}</li>{% endfor %}
|
||||
</ul>
|
||||
</div>
|
||||
{% endif %}
|
||||
|
||||
<!-- ─── Logo-Vorschau (wird nach POST angezeigt) ─── -->
|
||||
<div class="row g-3 mb-4">
|
||||
@@ -122,36 +130,46 @@
|
||||
</div>
|
||||
|
||||
<!-- ─── Eingabeformular (bis zu 3 Kunden) ─── -->
|
||||
<form method="POST">
|
||||
<form method="POST" enctype="multipart/form-data">
|
||||
<input type="hidden" name="site" value="{{ site }}">
|
||||
|
||||
{# Echte Zeilenumbrueche statt - in einer Variablen wuerde Jinja
|
||||
das kaufmaennische Und escapen und die Entity waere als Text sichtbar. #}
|
||||
{% set slots = [
|
||||
("z. B. Beispiel GmbH", "**Max Mustermann**\n+49 123 4567890\n*E-Mail*: max@beispiel.de"),
|
||||
("z. B. Muster AG", "**Maria Schmidt**\n+49 456 7890123\n*E-Mail*: maria@muster.de"),
|
||||
("z. B. Firma GmbH", "**Sabine Weber**\n+49 789 0123456\n*E-Mail*: sabine@firma.de")
|
||||
] %}
|
||||
|
||||
<div class="row g-3 mb-3">
|
||||
{% for i in range(3) %}
|
||||
<div class="col-md-4">
|
||||
<label class="form-label" for="customer_name_1">Kunde 1</label>
|
||||
<input type="text" class="form-control" id="customer_name_1"
|
||||
name="customer_name_1" placeholder="z. B. Beispiel GmbH"
|
||||
value="{{ customer_names[0] if customer_names else '' }}">
|
||||
<label class="form-label mt-2" for="contact_1">Ansprechpartner (optional)</label>
|
||||
<textarea class="form-control" id="contact_1" name="contact_1" rows="4"
|
||||
placeholder="**Max Mustermann** +49 123 4567890 *E-Mail*: max@beispiel.de">{{ contacts[0] if contacts else '' }}</textarea>
|
||||
<label class="form-label" for="customer_name_{{ i + 1 }}">Kunde {{ i + 1 }}{% if i > 0 %} (optional){% endif %}</label>
|
||||
<input type="text" class="form-control" id="customer_name_{{ i + 1 }}"
|
||||
name="customer_name_{{ i + 1 }}" placeholder="{{ slots[i][0] }}"
|
||||
value="{{ customer_names[i] if customer_names else '' }}">
|
||||
|
||||
<label class="form-label mt-2" for="contact_{{ i + 1 }}">Ansprechpartner (optional)</label>
|
||||
<textarea class="form-control" id="contact_{{ i + 1 }}" name="contact_{{ i + 1 }}" rows="4"
|
||||
placeholder="{{ slots[i][1] }}">{{ contacts[i] if contacts else '' }}</textarea>
|
||||
|
||||
<label class="form-label mt-2" for="logo_file_{{ i + 1 }}">Logo hochladen (optional)</label>
|
||||
<input type="file" class="form-control" id="logo_file_{{ i + 1 }}"
|
||||
name="logo_file_{{ i + 1 }}" accept="image/*">
|
||||
<div class="form-check mt-1">
|
||||
<input class="form-check-input" type="checkbox"
|
||||
id="logo_keep_bg_{{ i + 1 }}" name="logo_keep_bg_{{ i + 1 }}">
|
||||
<label class="form-check-label small" for="logo_keep_bg_{{ i + 1 }}">Hintergrund behalten</label>
|
||||
</div>
|
||||
<div class="col-md-4">
|
||||
<label class="form-label" for="customer_name_2">Kunde 2 (optional)</label>
|
||||
<input type="text" class="form-control" id="customer_name_2"
|
||||
name="customer_name_2" placeholder="z. B. Muster AG"
|
||||
value="{{ customer_names[1] if customer_names else '' }}">
|
||||
<label class="form-label mt-2" for="contact_2">Ansprechpartner (optional)</label>
|
||||
<textarea class="form-control" id="contact_2" name="contact_2" rows="4"
|
||||
placeholder="**Maria Schmidt** +49 456 7890123 *E-Mail*: maria@muster.de">{{ contacts[1] if contacts else '' }}</textarea>
|
||||
</div>
|
||||
<div class="col-md-4">
|
||||
<label class="form-label" for="customer_name_3">Kunde 3 (optional)</label>
|
||||
<input type="text" class="form-control" id="customer_name_3"
|
||||
name="customer_name_3" placeholder="z. B. Firma GmbH"
|
||||
value="{{ customer_names[2] if customer_names else '' }}">
|
||||
<label class="form-label mt-2" for="contact_3">Ansprechpartner (optional)</label>
|
||||
<textarea class="form-control" id="contact_3" name="contact_3" rows="4"
|
||||
placeholder="**Sabine Weber** +49 789 0123456 *E-Mail*: sabine@firma.de">{{ contacts[2] if contacts else '' }}</textarea>
|
||||
{% endfor %}
|
||||
</div>
|
||||
|
||||
<div class="text-muted small mb-3">
|
||||
<i class="ti ti-info-circle me-1"></i>Ein hochgeladenes Logo hat Vorrang vor der automatischen Suche.
|
||||
Der Hintergrund wird automatisch freigestellt, damit das Logo auf der Willkommensseite nicht in einem
|
||||
weissen Kasten steht. Am besten funktioniert das bei einem Logo auf einfarbigem Grund; ein PNG mit
|
||||
bereits transparentem Hintergrund wird unverändert übernommen.
|
||||
</div>
|
||||
|
||||
<hr class="mt-4 mb-3">
|
||||
|
||||
@@ -252,6 +252,9 @@
|
||||
<li><strong>Zielscreens:</strong> Die generierte <code>welcome.html</code> wird automatisch an den Anfang der Playlist <em>jedes</em> Screens gesetzt, bei dem im Tab "Einstellungen" die Option <em>Willkommensseite hier anzeigen</em> aktiv ist. So lässt sich dieselbe Begrüßung auf mehreren Screens zeigen, ohne sie mehrfach anzulegen. Ist an einem Standort nichts eingestellt, geht sie wie bisher in die Lobby.</li>
|
||||
<li>Wird ein Screen später abgewählt, verschwindet die Willkommensseite dort beim nächsten Generieren automatisch.</li>
|
||||
<li><strong>Anzeigen bis:</strong> Optionales Datum im Formular. Ist es gesetzt, verschwindet die Willkommensseite nach diesem Tag auf allen Screens von selbst – kein manuelles Aufräumen nach dem Besuch. Ohne Angabe bleibt sie stehen.</li>
|
||||
<li><strong>Logo hochladen:</strong> Findet die automatische Suche kein Logo oder passt es nicht, kann pro Kunde eine eigene Bilddatei hochgeladen werden. Ein Upload hat immer Vorrang vor der automatischen Suche.</li>
|
||||
<li><strong>Freistellen:</strong> Der Hintergrund eines hochgeladenen Logos wird automatisch entfernt, damit es auf der Willkommensseite nicht in einem weissen Kasten steht. Das gelingt am besten bei einem Logo auf einfarbigem Grund – ein PNG mit bereits transparentem Hintergrund wird unverändert übernommen. Bei unruhigem Bildrand bleibt das Bild unangetastet und es erscheint ein Hinweis. Mit „Hintergrund behalten" lässt sich das Freistellen abschalten.</li>
|
||||
<li>Ein einmal hochgeladenes Logo bleibt beim erneuten Generieren erhalten, solange der Kundenname im selben Feld steht.</li>
|
||||
<li>Pro Standort kann ein eigenes Hintergrundbild hochgeladen werden (Tab "Medien" im Standort-Dashboard)</li>
|
||||
<li>Pro Kunde können <strong>Ansprechpartner</strong> eingegeben werden (Markdown-Format):
|
||||
<ul>
|
||||
|
||||
Reference in New Issue
Block a user