Hilfetexte ergänzt
This commit is contained in:
@@ -14,7 +14,7 @@ docker compose up -d
|
|||||||
|
|
||||||
- Single Flask app in `app.py`; there is no database.
|
- Single Flask app in `app.py`; there is no database.
|
||||||
- Persistent state is `config.json` plus files under `media/<site>/<screen>/`.
|
- Persistent state is `config.json` plus files under `media/<site>/<screen>/`.
|
||||||
- `README.md` is stale on the port number; trust `app.py`.
|
- Port 5005; `README.md` hat den korrekten Port.
|
||||||
- Existing repo instructions in this file are the main local guidance; there is no `opencode.json` or workflow config in this repo.
|
- Existing repo instructions in this file are the main local guidance; there is no `opencode.json` or workflow config in this repo.
|
||||||
|
|
||||||
## Multi-Standort-URL-Struktur (seit v4.2.0)
|
## Multi-Standort-URL-Struktur (seit v4.2.0)
|
||||||
@@ -55,7 +55,7 @@ Standorte (sites) sind die oberste Organisationsebene und gruppieren Screens.
|
|||||||
- `add_customer_to_lobby_playlist` entfernt `welcome.html` sowohl als String als auch als Dict aus der Playlist vor dem Einfügen.
|
- `add_customer_to_lobby_playlist` entfernt `welcome.html` sowohl als String als auch als Dict aus der Playlist vor dem Einfügen.
|
||||||
- `add_screen` legt ein Verzeichnis unter `media/<site>/<screen>/` an und einen Config-Eintrag.
|
- `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.
|
- `delete_screen` entfernt den Screen aus der Config und löscht das Verzeichnis rekursiv.
|
||||||
- Custom-URL-Button: pro Screen konfigurierbar (`custom_url` + `custom_url_label` + `custom_url_target`) im Admin-Formular; wird im Player als Button oben links angezeigt und öffnet die URL wahlweise in einem iframe-Overlay mit Zurück-Button (`overlay`) oder per Direkt-Weiterleitung (`redirect`); Player pausiert während das Overlay geöffnet ist.
|
- Custom-URL-Button: pro Screen konfigurierbar (`custom_url` + `custom_url_label` + `custom_url_enabled` + `custom_url_target`) im Admin-Formular; wird im Player als Button oben links angezeigt und öffnet die URL wahlweise in einem iframe-Overlay mit Zurück-Button (`overlay`) oder per Direkt-Weiterleitung (`redirect`); Player pausiert während das Overlay geöffnet ist.
|
||||||
- Neue Standorte können über den `+`-Button im Header oder über `GET /add-site?name=<name>` angelegt werden.
|
- Neue Standorte können über den `+`-Button im Header oder über `GET /add-site?name=<name>` angelegt werden.
|
||||||
- `delete_site` entfernt den Standort aus Config und löscht das Medienverzeichnis rekursiv.
|
- `delete_site` entfernt den Standort aus Config und löscht das Medienverzeichnis rekursiv.
|
||||||
- `config.json["server_url"]` (z. B. `http://signage.ccmake.de`) wird in der Admin-Ansicht für die Player-URLs verwendet.
|
- `config.json["server_url"]` (z. B. `http://signage.ccmake.de`) wird in der Admin-Ansicht für die Player-URLs verwendet.
|
||||||
|
|||||||
32
README.md
32
README.md
@@ -91,7 +91,7 @@ signage/
|
|||||||
### Setup
|
### Setup
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
pip install flask flask-login werkzeug openai
|
pip install -r requirements.txt
|
||||||
```
|
```
|
||||||
|
|
||||||
### Starten
|
### Starten
|
||||||
@@ -135,8 +135,6 @@ docker compose up -d
|
|||||||
| `POST /api/customer` | API-Endpunkt für Willkommensseite (JSON) |
|
| `POST /api/customer` | API-Endpunkt für Willkommensseite (JSON) |
|
||||||
| `GET /media/<site>/<screen>/<file>` | Medien-Datei ausliefern |
|
| `GET /media/<site>/<screen>/<file>` | Medien-Datei ausliefern |
|
||||||
| `GET /media/priority/<file>` | Priority-Medien (global) |
|
| `GET /media/priority/<file>` | Priority-Medien (global) |
|
||||||
| `GET /add-site?name=<name>` | Neuen Standort anlegen |
|
|
||||||
| `POST /admin/<site>/delete-site` | Standort + alle Screens/Medien löschen |
|
|
||||||
|
|
||||||
### Admin-Portal
|
### Admin-Portal
|
||||||
|
|
||||||
@@ -182,7 +180,18 @@ Maximal 3 Kunden eingeben → Logos werden via OpenAI + Brandfetch gesucht → `
|
|||||||
"sites": {
|
"sites": {
|
||||||
"stuttgart": {
|
"stuttgart": {
|
||||||
"screens": {
|
"screens": {
|
||||||
"lobby": { "playlist": [...], "interval": 10 },
|
"lobby": {
|
||||||
|
"playlist": [...],
|
||||||
|
"interval": 10,
|
||||||
|
"show_images": true,
|
||||||
|
"show_videos": true,
|
||||||
|
"newsticker_text": "...",
|
||||||
|
"newsticker_enabled": false,
|
||||||
|
"custom_url_enabled": false,
|
||||||
|
"custom_url": "https://...",
|
||||||
|
"custom_url_label": "Infos",
|
||||||
|
"custom_url_target": "overlay"
|
||||||
|
},
|
||||||
"casino": { "playlist": [...], "interval": 15 }
|
"casino": { "playlist": [...], "interval": 15 }
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -196,6 +205,21 @@ Maximal 3 Kunden eingeben → Logos werden via OpenAI + Brandfetch gesucht → `
|
|||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
|
## Screen-Konfiguration (pro Screen in `config.json`)
|
||||||
|
|
||||||
|
| Feld | Typ | Beschreibung |
|
||||||
|
|------|-----|-------------|
|
||||||
|
| `playlist` | Array | Playlist-Einträge (Strings oder Dicts mit `url`/`zoom`) |
|
||||||
|
| `interval` | int | Anzeige-Intervall in Sekunden |
|
||||||
|
| `show_images` | bool | Bilder anzeigen |
|
||||||
|
| `show_videos` | bool | Videos anzeigen |
|
||||||
|
| `newsticker_text` | string | Text für Newsticker (max. 200 Zeichen) |
|
||||||
|
| `newsticker_enabled` | bool | Newsticker anzeigen |
|
||||||
|
| `custom_url_enabled` | bool | Custom-URL-Button im Player anzeigen |
|
||||||
|
| `custom_url` | string | URL des Aktions-Buttons |
|
||||||
|
| `custom_url_label` | string | Button-Beschriftung |
|
||||||
|
| `custom_url_target` | string | `"overlay"` (iframe) oder `"redirect"` |
|
||||||
|
|
||||||
## Helper-Funktionen (`app.py`)
|
## Helper-Funktionen (`app.py`)
|
||||||
|
|
||||||
- `load_config()` / `save_config()` – JSON lesen/schreiben
|
- `load_config()` / `save_config()` – JSON lesen/schreiben
|
||||||
|
|||||||
157
app.py
157
app.py
@@ -1,8 +1,17 @@
|
|||||||
|
#!/usr/bin/env python3
|
||||||
|
"""
|
||||||
|
CANCOM Simple Signage – Flask-App für Digital-Signage-Anzeigen.
|
||||||
|
──────────────────────────────────────────────────────────────
|
||||||
|
Diese App verwaltet mehrere Standorte (sites) mit jeweils mehreren
|
||||||
|
Screens. Jeder Screen hat eine eigene Playlist aus Bildern, Videos,
|
||||||
|
HTML-Seiten und URLs. Optional kann eine globale Priority-Playlist
|
||||||
|
zugeschaltet werden, die auf allen Playern abwechselnd eingeblendet wird.
|
||||||
|
"""
|
||||||
|
|
||||||
import os
|
import os
|
||||||
import json
|
import json
|
||||||
import hashlib
|
import hashlib
|
||||||
# Import welcome page functions
|
import generate_welcome_page # Logo-Suche und Willkommensseite-Generierung
|
||||||
import generate_welcome_page
|
|
||||||
|
|
||||||
from datetime import datetime
|
from datetime import datetime
|
||||||
|
|
||||||
@@ -23,8 +32,8 @@ from werkzeug.utils import secure_filename
|
|||||||
# Grundkonfiguration
|
# Grundkonfiguration
|
||||||
# -------------------------------------------------
|
# -------------------------------------------------
|
||||||
BASE_DIR = os.path.dirname(os.path.abspath(__file__))
|
BASE_DIR = os.path.dirname(os.path.abspath(__file__))
|
||||||
MEDIA_DIR = os.path.join(BASE_DIR, "media")
|
MEDIA_DIR = os.path.join(BASE_DIR, "media") # Medien-Dateien je Standort/Screen
|
||||||
CONFIG_FILE = os.path.join(BASE_DIR, "config.json")
|
CONFIG_FILE = os.path.join(BASE_DIR, "config.json") # Persistente Konfiguration
|
||||||
|
|
||||||
APP_VERSION = "5.1.0"
|
APP_VERSION = "5.1.0"
|
||||||
UPLOAD_EXTENSIONS = {".jpg", ".jpeg", ".png", ".mp4"}
|
UPLOAD_EXTENSIONS = {".jpg", ".jpeg", ".png", ".mp4"}
|
||||||
@@ -33,28 +42,35 @@ app = Flask(__name__)
|
|||||||
app.secret_key = "CHANGE_THIS_SECRET!!!"
|
app.secret_key = "CHANGE_THIS_SECRET!!!"
|
||||||
|
|
||||||
login_manager = LoginManager(app)
|
login_manager = LoginManager(app)
|
||||||
login_manager.login_view = "login"
|
login_manager.login_view = "login" # Nicht eingeloggte User werden zum Login umgeleitet
|
||||||
|
|
||||||
|
|
||||||
# -------------------------------------------------
|
# -------------------------------------------------
|
||||||
# Config Helpers
|
# Config-Helfer
|
||||||
# -------------------------------------------------
|
# -------------------------------------------------
|
||||||
def load_config():
|
def load_config():
|
||||||
|
"""Liest die config.json und gibt das Dict zurück. Legt ein leeres an, falls nicht vorhanden."""
|
||||||
if not os.path.exists(CONFIG_FILE):
|
if not os.path.exists(CONFIG_FILE):
|
||||||
return {"admin": {}, "sites": {}}
|
return {"admin": {}, "sites": {}}
|
||||||
with open(CONFIG_FILE) as f:
|
with open(CONFIG_FILE) as f:
|
||||||
return json.load(f)
|
return json.load(f)
|
||||||
|
|
||||||
|
|
||||||
def save_config(cfg):
|
def save_config(cfg):
|
||||||
|
"""Schreibt das Config-Dict zurück in die config.json."""
|
||||||
with open(CONFIG_FILE, "w") as f:
|
with open(CONFIG_FILE, "w") as f:
|
||||||
json.dump(cfg, f, indent=2)
|
json.dump(cfg, f, indent=2)
|
||||||
f.flush()
|
f.flush()
|
||||||
|
|
||||||
|
|
||||||
def get_site_list(cfg):
|
def get_site_list(cfg):
|
||||||
|
"""Gibt eine sortierte Liste aller konfigurierten Standort-Namen zurück."""
|
||||||
return sorted(cfg.get("sites", {}).keys())
|
return sorted(cfg.get("sites", {}).keys())
|
||||||
|
|
||||||
|
|
||||||
def get_screen_config(cfg, site, screen):
|
def get_screen_config(cfg, site, screen):
|
||||||
|
"""Holt die Konfiguration für einen bestimmten Screen.
|
||||||
|
Erzeugt automatisch fehlende Dict-Ebenen (sites → site → screens → screen)."""
|
||||||
return (
|
return (
|
||||||
cfg
|
cfg
|
||||||
.setdefault("sites", {})
|
.setdefault("sites", {})
|
||||||
@@ -65,12 +81,15 @@ def get_screen_config(cfg, site, screen):
|
|||||||
|
|
||||||
|
|
||||||
def is_url(item):
|
def is_url(item):
|
||||||
|
"""Prüft, ob ein Playlist-Eintrag eine URL ist (String mit http/https) oder ein Dict mit "url"-Key."""
|
||||||
if isinstance(item, str):
|
if isinstance(item, str):
|
||||||
return item.startswith("http://") or item.startswith("https://")
|
return item.startswith("http://") or item.startswith("https://")
|
||||||
return isinstance(item, dict) and "url" in item
|
return isinstance(item, dict) and "url" in item
|
||||||
|
|
||||||
|
|
||||||
def normalize_url(item):
|
def normalize_url(item):
|
||||||
|
"""Normalisiert einen URL-Eintrag zu einem Dict mit "url" und "zoom".
|
||||||
|
Zoom wird aus Dict übernommen oder defaultmäßig auf 1.0 gesetzt."""
|
||||||
if isinstance(item, str):
|
if isinstance(item, str):
|
||||||
return {"url": item, "zoom": 1.0}
|
return {"url": item, "zoom": 1.0}
|
||||||
if isinstance(item, dict):
|
if isinstance(item, dict):
|
||||||
@@ -79,6 +98,7 @@ def normalize_url(item):
|
|||||||
|
|
||||||
|
|
||||||
def playlist_item_name(item):
|
def playlist_item_name(item):
|
||||||
|
"""Ermittelt den Anzeigenamen eines Playlist-Eintrags (String → direkt, Dict → "url" oder "name")."""
|
||||||
if isinstance(item, str):
|
if isinstance(item, str):
|
||||||
return item
|
return item
|
||||||
if isinstance(item, dict):
|
if isinstance(item, dict):
|
||||||
@@ -87,6 +107,8 @@ def playlist_item_name(item):
|
|||||||
|
|
||||||
|
|
||||||
def playlist_item_enabled(item):
|
def playlist_item_enabled(item):
|
||||||
|
"""Prüft, ob ein Playlist-Eintrag aktiviert ist.
|
||||||
|
Strings sind immer aktiv, Dicts haben ein optionales "enabled"-Feld (default: True)."""
|
||||||
if isinstance(item, (str,)):
|
if isinstance(item, (str,)):
|
||||||
return True
|
return True
|
||||||
if isinstance(item, dict):
|
if isinstance(item, dict):
|
||||||
@@ -95,9 +117,12 @@ def playlist_item_enabled(item):
|
|||||||
|
|
||||||
|
|
||||||
# -------------------------------------------------
|
# -------------------------------------------------
|
||||||
# Priority-Helper
|
# Priority-Playlist-Helfer
|
||||||
# -------------------------------------------------
|
# -------------------------------------------------
|
||||||
def load_priority_files():
|
def load_priority_files():
|
||||||
|
"""Lädt die Priority-Playlist aus config.json plus die dazugehörigen Dateien
|
||||||
|
aus media/priority/. Gibt eine Liste von Dicts mit name/type/size/enabled zurück.
|
||||||
|
Neue Dateien im Ordner, die noch nicht in der Playlist sind, werden angehängt."""
|
||||||
cfg = load_config()
|
cfg = load_config()
|
||||||
prio_dir = os.path.join(MEDIA_DIR, "priority")
|
prio_dir = os.path.join(MEDIA_DIR, "priority")
|
||||||
os.makedirs(prio_dir, exist_ok=True)
|
os.makedirs(prio_dir, exist_ok=True)
|
||||||
@@ -105,6 +130,7 @@ def load_priority_files():
|
|||||||
priority_files = []
|
priority_files = []
|
||||||
prio_playlist = cfg.get("priority", {}).get("playlist", [])
|
prio_playlist = cfg.get("priority", {}).get("playlist", [])
|
||||||
|
|
||||||
|
# 1. Einträge aus der config.json-Playlist
|
||||||
for item in prio_playlist:
|
for item in prio_playlist:
|
||||||
item_name = playlist_item_name(item)
|
item_name = playlist_item_name(item)
|
||||||
if is_url(item):
|
if is_url(item):
|
||||||
@@ -134,6 +160,7 @@ def load_priority_files():
|
|||||||
"enabled": playlist_item_enabled(item)
|
"enabled": playlist_item_enabled(item)
|
||||||
})
|
})
|
||||||
|
|
||||||
|
# 2. Neue Dateien aus dem Ordner, die noch nicht in der Playlist sind
|
||||||
playlist_names = {playlist_item_name(item) for item in prio_playlist}
|
playlist_names = {playlist_item_name(item) for item in prio_playlist}
|
||||||
for f in sorted(os.listdir(prio_dir)):
|
for f in sorted(os.listdir(prio_dir)):
|
||||||
if f in playlist_names or f.startswith("._"):
|
if f in playlist_names or f.startswith("._"):
|
||||||
@@ -154,26 +181,30 @@ def load_priority_files():
|
|||||||
|
|
||||||
|
|
||||||
def prio_redirect(site):
|
def prio_redirect(site):
|
||||||
|
"""Gibt den Redirect-Pfad zurück, der nach Priority-Operationen aufgerufen wird."""
|
||||||
return f"/admin/{site}/priority"
|
return f"/admin/{site}/priority"
|
||||||
|
|
||||||
|
|
||||||
# -------------------------------------------------
|
# -------------------------------------------------
|
||||||
# User & Login
|
# User & Login (Flask-Login)
|
||||||
# -------------------------------------------------
|
|
||||||
# Auth / Benutzer
|
|
||||||
# -------------------------------------------------
|
# -------------------------------------------------
|
||||||
class Admin(UserMixin):
|
class Admin(UserMixin):
|
||||||
|
"""Einfacher Admin-User. Es gibt nur einen Admin (keine DB)."""
|
||||||
id = 1
|
id = 1
|
||||||
|
|
||||||
|
|
||||||
@login_manager.user_loader
|
@login_manager.user_loader
|
||||||
def load_user(user_id):
|
def load_user(user_id):
|
||||||
|
"""Lädt den Admin-User für Flask-Login. user_id wird ignoriert – es gibt nur einen User."""
|
||||||
return Admin()
|
return Admin()
|
||||||
|
|
||||||
|
|
||||||
# -------------------------------------------------
|
# -------------------------------------------------
|
||||||
# Login / Logout
|
# Login / Logout
|
||||||
# -------------------------------------------------
|
# -------------------------------------------------
|
||||||
@app.route("/login", methods=["GET", "POST"])
|
@app.route("/login", methods=["GET", "POST"])
|
||||||
def login():
|
def login():
|
||||||
|
"""Login-Seite. Bei POST werden die Zugangsdaten aus config.json.admin geprüft."""
|
||||||
config = load_config()
|
config = load_config()
|
||||||
error = None
|
error = None
|
||||||
|
|
||||||
@@ -188,26 +219,33 @@ def login():
|
|||||||
|
|
||||||
return render_template("login.html", error=error)
|
return render_template("login.html", error=error)
|
||||||
|
|
||||||
|
|
||||||
@app.route("/logout")
|
@app.route("/logout")
|
||||||
def logout():
|
def logout():
|
||||||
|
"""Loggt den Admin aus und leitet zur Login-Seite weiter."""
|
||||||
logout_user()
|
logout_user()
|
||||||
return redirect("/login")
|
return redirect("/login")
|
||||||
|
|
||||||
|
|
||||||
# -------------------------------------------------
|
# -------------------------------------------------
|
||||||
# Customer / Willkommensseite
|
# Willkommensseite (Kunden-Logos + Generierung)
|
||||||
# -------------------------------------------------
|
# -------------------------------------------------
|
||||||
@app.route("/willkommen", methods=["GET", "POST"])
|
@app.route("/willkommen", methods=["GET", "POST"])
|
||||||
@app.route("/customer", methods=["GET", "POST"])
|
@app.route("/customer", methods=["GET", "POST"])
|
||||||
# @login_required
|
|
||||||
def add_customer():
|
def add_customer():
|
||||||
"""Add new customer with logo search and welcome page"""
|
"""
|
||||||
|
Formular für die Willkommensseite:
|
||||||
|
- Bis zu 3 Kundennamen eingeben
|
||||||
|
- Logos per OpenAI → Brandfetch suchen
|
||||||
|
- welcome.html wird generiert und in der Lobby-Playlist vorne platziert
|
||||||
|
"""
|
||||||
error = None
|
error = None
|
||||||
success = None
|
success = None
|
||||||
logo_urls = [None, None, None]
|
logo_urls = [None, None, None]
|
||||||
customer_names = ["", "", ""]
|
customer_names = ["", "", ""]
|
||||||
site = request.args.get("site") or request.form.get("site") or ""
|
site = request.args.get("site") or request.form.get("site") or ""
|
||||||
|
|
||||||
# Default to first site if not specified
|
# Fallback auf ersten Standort, wenn keiner angegeben
|
||||||
if not site:
|
if not site:
|
||||||
config = load_config()
|
config = load_config()
|
||||||
sites = get_site_list(config)
|
sites = get_site_list(config)
|
||||||
@@ -277,10 +315,14 @@ def add_customer():
|
|||||||
hostname=os.uname().nodename
|
hostname=os.uname().nodename
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
@app.route("/api/customer", methods=["POST"])
|
@app.route("/api/customer", methods=["POST"])
|
||||||
# @login_required
|
|
||||||
def api_add_customer():
|
def api_add_customer():
|
||||||
"""API endpoint for customer creation"""
|
"""
|
||||||
|
API-Endpunkt für die Willkommensseite (JSON).
|
||||||
|
Erwartet: {"customer_name": "...", "site": "..."}
|
||||||
|
Gibt die Logo-URL und den Status zurück.
|
||||||
|
"""
|
||||||
try:
|
try:
|
||||||
data = request.get_json()
|
data = request.get_json()
|
||||||
customer_name = data.get("customer_name", "").strip()
|
customer_name = data.get("customer_name", "").strip()
|
||||||
@@ -289,7 +331,7 @@ def api_add_customer():
|
|||||||
if not customer_name:
|
if not customer_name:
|
||||||
return jsonify({"error": "Customer name required"}), 400
|
return jsonify({"error": "Customer name required"}), 400
|
||||||
|
|
||||||
# Default to first site
|
# Default auf ersten Standort
|
||||||
if not site:
|
if not site:
|
||||||
config = load_config()
|
config = load_config()
|
||||||
sites = get_site_list(config)
|
sites = get_site_list(config)
|
||||||
@@ -324,25 +366,34 @@ def api_add_customer():
|
|||||||
except Exception as e:
|
except Exception as e:
|
||||||
return jsonify({"error": str(e)}), 500
|
return jsonify({"error": str(e)}), 500
|
||||||
|
|
||||||
|
|
||||||
# -------------------------------------------------
|
# -------------------------------------------------
|
||||||
# Medien ausliefern
|
# Medien ausliefern (statische Dateien)
|
||||||
# -------------------------------------------------
|
# -------------------------------------------------
|
||||||
@app.route("/media/priority/<path:filename>")
|
@app.route("/media/priority/<path:filename>")
|
||||||
def media_priority(filename):
|
def media_priority(filename):
|
||||||
|
"""Liefert globale Priority-Medien aus media/priority/ aus."""
|
||||||
path = os.path.join(MEDIA_DIR, "priority")
|
path = os.path.join(MEDIA_DIR, "priority")
|
||||||
return send_from_directory(path, filename)
|
return send_from_directory(path, filename)
|
||||||
|
|
||||||
|
|
||||||
@app.route("/media/<site>/<screen>/<path:filename>")
|
@app.route("/media/<site>/<screen>/<path:filename>")
|
||||||
def media(site, screen, filename):
|
def media(site, screen, filename):
|
||||||
|
"""Liefert Medien-Dateien für einen bestimmten Screen aus."""
|
||||||
path = os.path.join(MEDIA_DIR, site, screen)
|
path = os.path.join(MEDIA_DIR, site, screen)
|
||||||
return send_from_directory(path, filename)
|
return send_from_directory(path, filename)
|
||||||
|
|
||||||
|
|
||||||
# -------------------------------------------------
|
# -------------------------------------------------
|
||||||
# Player
|
# Player
|
||||||
# -------------------------------------------------
|
# -------------------------------------------------
|
||||||
|
|
||||||
@app.route("/player/<site>/<screen>")
|
@app.route("/player/<site>/<screen>")
|
||||||
def player(site, screen):
|
def player(site, screen):
|
||||||
|
"""
|
||||||
|
Player-Ansicht für einen Screen.
|
||||||
|
Baut die Playlist (normal + optional priority) zusammen und rendert player.html.
|
||||||
|
Der Player zeigt abwechselnd Bilder, Videos, HTML-Seiten und URLs an.
|
||||||
|
"""
|
||||||
config = load_config()
|
config = load_config()
|
||||||
sites_cfg = config.setdefault("sites", {})
|
sites_cfg = config.setdefault("sites", {})
|
||||||
site_cfg = sites_cfg.get(site)
|
site_cfg = sites_cfg.get(site)
|
||||||
@@ -363,6 +414,7 @@ def player(site, screen):
|
|||||||
show_videos = screen_cfg.get("show_videos", True)
|
show_videos = screen_cfg.get("show_videos", True)
|
||||||
|
|
||||||
def allowed_file(name):
|
def allowed_file(name):
|
||||||
|
"""Prüft, ob eine Datei angezeigt werden darf (abhängig von show_images / show_videos)."""
|
||||||
if name.startswith("._"):
|
if name.startswith("._"):
|
||||||
return False
|
return False
|
||||||
ext = name.lower()
|
ext = name.lower()
|
||||||
@@ -374,12 +426,10 @@ def player(site, screen):
|
|||||||
return True
|
return True
|
||||||
return False
|
return False
|
||||||
|
|
||||||
# ------------------------------
|
# ─── Normale Screen-Playlist ───
|
||||||
# Normale Screen-Playlist
|
|
||||||
# ------------------------------
|
|
||||||
normal_files = []
|
normal_files = []
|
||||||
|
|
||||||
# 1. Playlist-Reihenfolge
|
# 1. Playlist-Reihenfolge aus config.json
|
||||||
for item in playlist:
|
for item in playlist:
|
||||||
if not playlist_item_enabled(item):
|
if not playlist_item_enabled(item):
|
||||||
continue
|
continue
|
||||||
@@ -397,7 +447,7 @@ def player(site, screen):
|
|||||||
item["name"] for item in normal_files if item["kind"] == "file"
|
item["name"] for item in normal_files if item["kind"] == "file"
|
||||||
}
|
}
|
||||||
|
|
||||||
# 2. Neue Dateien hintendran
|
# 2. Neue Dateien aus dem Medien-Ordner (ohne Playlist-Eintrag) hinten anhängen
|
||||||
if not playlist:
|
if not playlist:
|
||||||
for f in sorted(os.listdir(folder)):
|
for f in sorted(os.listdir(folder)):
|
||||||
if f in existing_file_names or f.startswith("._"):
|
if f in existing_file_names or f.startswith("._"):
|
||||||
@@ -405,9 +455,7 @@ def player(site, screen):
|
|||||||
if allowed_file(f):
|
if allowed_file(f):
|
||||||
normal_files.append({"kind": "file", "name": f})
|
normal_files.append({"kind": "file", "name": f})
|
||||||
|
|
||||||
# ------------------------------
|
# ─── Priority-Playlist (global) ───
|
||||||
# Priority-Playlist (global)
|
|
||||||
# ------------------------------
|
|
||||||
priority_cfg = config.get("priority", {})
|
priority_cfg = config.get("priority", {})
|
||||||
|
|
||||||
if priority_cfg.get("enabled", False):
|
if priority_cfg.get("enabled", False):
|
||||||
@@ -446,9 +494,14 @@ def player(site, screen):
|
|||||||
|
|
||||||
# -------------------------------------------------
|
# -------------------------------------------------
|
||||||
# Hash für automatisches Player-Reload
|
# Hash für automatisches Player-Reload
|
||||||
|
# ─────────────────────────────────────────────────
|
||||||
|
# Der Player pollt alle 5s diese Route.
|
||||||
|
# Wenn sich der Hash ändert, wird neu geladen.
|
||||||
# -------------------------------------------------
|
# -------------------------------------------------
|
||||||
@app.route("/playlist/<site>/<screen>/hash")
|
@app.route("/playlist/<site>/<screen>/hash")
|
||||||
def playlist_hash(site, screen):
|
def playlist_hash(site, screen):
|
||||||
|
"""Berechnet einen MD5-Hash über die aktuelle Konfiguration (Screen + Priority).
|
||||||
|
Ändert sich der Hash, lädt der Player automatisch neu."""
|
||||||
config = load_config()
|
config = load_config()
|
||||||
|
|
||||||
relevant = {
|
relevant = {
|
||||||
@@ -463,13 +516,13 @@ def playlist_hash(site, screen):
|
|||||||
|
|
||||||
|
|
||||||
# -------------------------------------------------
|
# -------------------------------------------------
|
||||||
# Admin Dashboard
|
# Admin
|
||||||
# -------------------------------------------------
|
# -------------------------------------------------
|
||||||
|
|
||||||
@app.route("/admin")
|
@app.route("/admin")
|
||||||
@login_required
|
@login_required
|
||||||
def admin_redirect():
|
def admin_redirect():
|
||||||
"""Redirect to first available site"""
|
"""Leitet zum ersten konfigurierten Standort weiter."""
|
||||||
cfg = load_config()
|
cfg = load_config()
|
||||||
sites = get_site_list(cfg)
|
sites = get_site_list(cfg)
|
||||||
if sites:
|
if sites:
|
||||||
@@ -480,6 +533,7 @@ def admin_redirect():
|
|||||||
@app.route("/add-site")
|
@app.route("/add-site")
|
||||||
@login_required
|
@login_required
|
||||||
def add_site():
|
def add_site():
|
||||||
|
"""Legt einen neuen Standort an (Verzeichnis + Config-Eintrag)."""
|
||||||
name = request.args.get("name", "").strip().lower()
|
name = request.args.get("name", "").strip().lower()
|
||||||
if not name:
|
if not name:
|
||||||
return redirect("/admin")
|
return redirect("/admin")
|
||||||
@@ -495,6 +549,7 @@ def add_site():
|
|||||||
@app.route("/admin/<site>/delete-site", methods=["POST"])
|
@app.route("/admin/<site>/delete-site", methods=["POST"])
|
||||||
@login_required
|
@login_required
|
||||||
def delete_site(site):
|
def delete_site(site):
|
||||||
|
"""Löscht einen Standort inkl. aller Screens und Medien (Config + Verzeichnis)."""
|
||||||
cfg = load_config()
|
cfg = load_config()
|
||||||
cfg.get("sites", {}).pop(site, None)
|
cfg.get("sites", {}).pop(site, None)
|
||||||
save_config(cfg)
|
save_config(cfg)
|
||||||
@@ -511,13 +566,16 @@ def delete_site(site):
|
|||||||
@app.route("/admin/<site>")
|
@app.route("/admin/<site>")
|
||||||
@login_required
|
@login_required
|
||||||
def admin(site):
|
def admin(site):
|
||||||
|
"""
|
||||||
|
Admin-Dashboard für einen Standort.
|
||||||
|
Zeigt alle Screens als Cards mit Tabler-Tabs (Playlist, Einstellungen, Medien).
|
||||||
|
"""
|
||||||
cfg = load_config()
|
cfg = load_config()
|
||||||
|
|
||||||
site_list = get_site_list(cfg)
|
site_list = get_site_list(cfg)
|
||||||
|
|
||||||
# Validate site exists
|
# Prüfen, ob der Standort existiert
|
||||||
if site not in site_list:
|
if site not in site_list:
|
||||||
# Create default site config if filesystem dir exists but no config
|
|
||||||
if os.path.isdir(os.path.join(MEDIA_DIR, site)):
|
if os.path.isdir(os.path.join(MEDIA_DIR, site)):
|
||||||
cfg.setdefault("sites", {})[site] = {"screens": {}}
|
cfg.setdefault("sites", {})[site] = {"screens": {}}
|
||||||
save_config(cfg)
|
save_config(cfg)
|
||||||
@@ -532,19 +590,19 @@ def admin(site):
|
|||||||
|
|
||||||
site_media_base = os.path.join(MEDIA_DIR, current_site)
|
site_media_base = os.path.join(MEDIA_DIR, current_site)
|
||||||
|
|
||||||
# Discover screens: from config + filesystem
|
# Screens erkennen: aus Config + Dateisystem
|
||||||
known_screens = set()
|
known_screens = set()
|
||||||
site_cfg = cfg.setdefault("sites", {}).setdefault(current_site, {"screens": {}})
|
site_cfg = cfg.setdefault("sites", {}).setdefault(current_site, {"screens": {}})
|
||||||
config_screens = site_cfg.setdefault("screens", {})
|
config_screens = site_cfg.setdefault("screens", {})
|
||||||
|
|
||||||
# Merge with filesystem directories
|
# Ordner im Medienverzeichnis = Screens
|
||||||
if os.path.isdir(site_media_base):
|
if os.path.isdir(site_media_base):
|
||||||
for entry in os.listdir(site_media_base):
|
for entry in os.listdir(site_media_base):
|
||||||
entry_path = os.path.join(site_media_base, entry)
|
entry_path = os.path.join(site_media_base, entry)
|
||||||
if os.path.isdir(entry_path) and not entry.startswith("._"):
|
if os.path.isdir(entry_path) and not entry.startswith("._"):
|
||||||
known_screens.add(entry)
|
known_screens.add(entry)
|
||||||
|
|
||||||
# Add screens from config that might not have a directory yet
|
# Screens aus Config ergänzen (auch ohne Ordner)
|
||||||
for s in config_screens:
|
for s in config_screens:
|
||||||
known_screens.add(s)
|
known_screens.add(s)
|
||||||
|
|
||||||
@@ -558,7 +616,7 @@ def admin(site):
|
|||||||
screen_cfg["newsticker_text"] = screen_cfg.get("newsticker_text", "")
|
screen_cfg["newsticker_text"] = screen_cfg.get("newsticker_text", "")
|
||||||
screen_cfg["newsticker_enabled"] = screen_cfg.get("newsticker_enabled", False)
|
screen_cfg["newsticker_enabled"] = screen_cfg.get("newsticker_enabled", False)
|
||||||
|
|
||||||
# 1 Playlist-Reihenfolge
|
# 1. Playlist-Einträge in Config-Reihenfolge
|
||||||
for item in playlist:
|
for item in playlist:
|
||||||
item_name = playlist_item_name(item)
|
item_name = playlist_item_name(item)
|
||||||
|
|
||||||
@@ -595,7 +653,7 @@ def admin(site):
|
|||||||
"enabled": playlist_item_enabled(item)
|
"enabled": playlist_item_enabled(item)
|
||||||
})
|
})
|
||||||
|
|
||||||
# 2 Neue Dateien anhängen
|
# 2. Neue Dateien (ohne Playlist-Eintrag) anhängen
|
||||||
playlist_names = {playlist_item_name(item) for item in playlist}
|
playlist_names = {playlist_item_name(item) for item in playlist}
|
||||||
for f in sorted(os.listdir(path)):
|
for f in sorted(os.listdir(path)):
|
||||||
if f in playlist_names or f.startswith("._"):
|
if f in playlist_names or f.startswith("._"):
|
||||||
@@ -631,11 +689,12 @@ def admin(site):
|
|||||||
|
|
||||||
|
|
||||||
# -------------------------------------------------
|
# -------------------------------------------------
|
||||||
# Admin: Priority-Playlist (separate Seite)
|
# Admin: Priority-Playlist (eigene Seite)
|
||||||
# -------------------------------------------------
|
# -------------------------------------------------
|
||||||
@app.route("/admin/<site>/priority")
|
@app.route("/admin/<site>/priority")
|
||||||
@login_required
|
@login_required
|
||||||
def admin_priority(site):
|
def admin_priority(site):
|
||||||
|
"""Zeigt die Priority-Playlist-Seite mit Tabs (Playlist + Medien)."""
|
||||||
cfg = load_config()
|
cfg = load_config()
|
||||||
site_list = sorted(cfg.get("sites", {}).keys())
|
site_list = sorted(cfg.get("sites", {}).keys())
|
||||||
priority_files = load_priority_files()
|
priority_files = load_priority_files()
|
||||||
@@ -658,6 +717,7 @@ def admin_priority(site):
|
|||||||
@app.route("/admin/<site>/update/<screen>", methods=["POST"])
|
@app.route("/admin/<site>/update/<screen>", methods=["POST"])
|
||||||
@login_required
|
@login_required
|
||||||
def update_screen(site, screen):
|
def update_screen(site, screen):
|
||||||
|
"""Speichert die Einstellungen für einen Screen (Intervall, Newsticker, Show-Bilder/Videos, Custom-URL)."""
|
||||||
config = load_config()
|
config = load_config()
|
||||||
cfg = get_screen_config(config, site, screen)
|
cfg = get_screen_config(config, site, screen)
|
||||||
|
|
||||||
@@ -681,14 +741,13 @@ def update_screen(site, screen):
|
|||||||
@app.route("/admin/<site>/delete-screen/<screen>", methods=["POST"])
|
@app.route("/admin/<site>/delete-screen/<screen>", methods=["POST"])
|
||||||
@login_required
|
@login_required
|
||||||
def delete_screen(site, screen):
|
def delete_screen(site, screen):
|
||||||
|
"""Löscht einen Screen (Config-Eintrag + Medien-Verzeichnis)."""
|
||||||
config = load_config()
|
config = load_config()
|
||||||
|
|
||||||
# Aus config.json entfernen
|
|
||||||
screens_cfg = config.get("sites", {}).get(site, {}).get("screens", {})
|
screens_cfg = config.get("sites", {}).get(site, {}).get("screens", {})
|
||||||
screens_cfg.pop(screen, None)
|
screens_cfg.pop(screen, None)
|
||||||
save_config(config)
|
save_config(config)
|
||||||
|
|
||||||
# Medien-Verzeichnis löschen
|
|
||||||
screen_dir = os.path.join(MEDIA_DIR, site, screen)
|
screen_dir = os.path.join(MEDIA_DIR, site, screen)
|
||||||
if os.path.isdir(screen_dir):
|
if os.path.isdir(screen_dir):
|
||||||
import shutil
|
import shutil
|
||||||
@@ -703,6 +762,7 @@ def delete_screen(site, screen):
|
|||||||
@app.route("/admin/<site>/add-screen", methods=["GET"])
|
@app.route("/admin/<site>/add-screen", methods=["GET"])
|
||||||
@login_required
|
@login_required
|
||||||
def add_screen(site):
|
def add_screen(site):
|
||||||
|
"""Legt einen neuen Screen an (Verzeichnis + Config-Eintrag)."""
|
||||||
screen_name = request.args.get("name", "").strip().lower()
|
screen_name = request.args.get("name", "").strip().lower()
|
||||||
if not screen_name:
|
if not screen_name:
|
||||||
return redirect(f"/admin/{site}")
|
return redirect(f"/admin/{site}")
|
||||||
@@ -720,11 +780,12 @@ def add_screen(site):
|
|||||||
|
|
||||||
|
|
||||||
# -------------------------------------------------
|
# -------------------------------------------------
|
||||||
# Admin: Upload
|
# Admin: Medien-Upload
|
||||||
# -------------------------------------------------
|
# -------------------------------------------------
|
||||||
@app.route("/admin/<site>/upload/<screen>", methods=["POST"])
|
@app.route("/admin/<site>/upload/<screen>", methods=["POST"])
|
||||||
@login_required
|
@login_required
|
||||||
def upload(site, screen):
|
def upload(site, screen):
|
||||||
|
"""Lädt eine Datei hoch (Bild oder Video). Bei screen="priority" ins globale Verzeichnis."""
|
||||||
file = request.files.get("file")
|
file = request.files.get("file")
|
||||||
if not file:
|
if not file:
|
||||||
return redirect(prio_redirect(site) if screen == "priority" else f"/admin/{site}")
|
return redirect(prio_redirect(site) if screen == "priority" else f"/admin/{site}")
|
||||||
@@ -744,11 +805,12 @@ def upload(site, screen):
|
|||||||
|
|
||||||
|
|
||||||
# -------------------------------------------------
|
# -------------------------------------------------
|
||||||
# Admin: URL hinzufügen
|
# Admin: URL zur Playlist hinzufügen
|
||||||
# -------------------------------------------------
|
# -------------------------------------------------
|
||||||
@app.route("/admin/<site>/add-url/<screen>", methods=["POST"])
|
@app.route("/admin/<site>/add-url/<screen>", methods=["POST"])
|
||||||
@login_required
|
@login_required
|
||||||
def add_url(site, screen):
|
def add_url(site, screen):
|
||||||
|
"""Fügt eine URL (mit Zoom-Faktor) zur Playlist eines Screens oder der Priority-Playlist hinzu."""
|
||||||
url = request.form.get("url", "").strip()
|
url = request.form.get("url", "").strip()
|
||||||
zoom = request.form.get("zoom", "1.0").strip()
|
zoom = request.form.get("zoom", "1.0").strip()
|
||||||
|
|
||||||
@@ -776,12 +838,12 @@ def add_url(site, screen):
|
|||||||
|
|
||||||
|
|
||||||
# -------------------------------------------------
|
# -------------------------------------------------
|
||||||
# Admin: Datei löschen
|
# Admin: Datei aus Playlist löschen
|
||||||
# -------------------------------------------------
|
# -------------------------------------------------
|
||||||
|
|
||||||
@app.route("/admin/<site>/delete/<screen>/<path:filename>", methods=["POST"])
|
@app.route("/admin/<site>/delete/<screen>/<path:filename>", methods=["POST"])
|
||||||
@login_required
|
@login_required
|
||||||
def delete_file(site, screen, filename):
|
def delete_file(site, screen, filename):
|
||||||
|
"""Löscht eine Datei aus der Playlist (Config + Dateisystem)."""
|
||||||
cfg = load_config()
|
cfg = load_config()
|
||||||
|
|
||||||
if screen == "priority":
|
if screen == "priority":
|
||||||
@@ -808,12 +870,15 @@ def delete_file(site, screen, filename):
|
|||||||
|
|
||||||
|
|
||||||
# -------------------------------------------------
|
# -------------------------------------------------
|
||||||
# Admin: Playlist speichern
|
# Admin: Playlist-Reihenfolge speichern (Drag & Drop)
|
||||||
# -------------------------------------------------
|
# -------------------------------------------------
|
||||||
|
|
||||||
@app.route("/admin/<site>/playlist/<screen>", methods=["POST"])
|
@app.route("/admin/<site>/playlist/<screen>", methods=["POST"])
|
||||||
@login_required
|
@login_required
|
||||||
def save_playlist(site, screen):
|
def save_playlist(site, screen):
|
||||||
|
"""
|
||||||
|
Speichert die über SortableJS per Drag & Drop neu sortierte Playlist.
|
||||||
|
Der Client schickt ein JSON mit der neuen Reihenfolge und enabled-Status.
|
||||||
|
"""
|
||||||
cfg = load_config()
|
cfg = load_config()
|
||||||
data = request.get_json()
|
data = request.get_json()
|
||||||
new_order = data["playlist"]
|
new_order = data["playlist"]
|
||||||
@@ -825,10 +890,12 @@ def save_playlist(site, screen):
|
|||||||
screens_cfg = cfg.get("sites", {}).get(site, {}).get("screens", {})
|
screens_cfg = cfg.get("sites", {}).get(site, {}).get("screens", {})
|
||||||
old_playlist = screens_cfg.get(screen, {}).get("playlist", [])
|
old_playlist = screens_cfg.get(screen, {}).get("playlist", [])
|
||||||
|
|
||||||
|
# Mapping: Name → Original-Item (für Zoom etc.)
|
||||||
item_map = {}
|
item_map = {}
|
||||||
for item in old_playlist:
|
for item in old_playlist:
|
||||||
item_map[playlist_item_name(item)] = item
|
item_map[playlist_item_name(item)] = item
|
||||||
|
|
||||||
|
# Neue Playlist in der gewünschten Reihenfolge bauen
|
||||||
new_playlist = []
|
new_playlist = []
|
||||||
for key in new_order:
|
for key in new_order:
|
||||||
if key in item_map:
|
if key in item_map:
|
||||||
|
|||||||
@@ -1,40 +1,61 @@
|
|||||||
|
#!/usr/bin/env python3
|
||||||
|
"""
|
||||||
|
Willkommensseite-Generierung für CANCOM Simple Signage.
|
||||||
|
───────────────────────────────────────────────────────
|
||||||
|
Diese Hilfsdatei sucht anhand von Kundennamen Logos (via OpenAI + Brandfetch),
|
||||||
|
generiert daraus eine HTML-Willkommensseite und fügt sie in die Lobby-Playlist ein.
|
||||||
|
"""
|
||||||
|
|
||||||
import os
|
import os
|
||||||
import json
|
import json
|
||||||
from openai import OpenAI
|
from openai import OpenAI
|
||||||
|
|
||||||
# -------------------------------------------------
|
|
||||||
# Customer / Willkommensseite
|
|
||||||
# -------------------------------------------------
|
|
||||||
|
|
||||||
BASE_DIR = os.path.dirname(os.path.abspath(__file__))
|
BASE_DIR = os.path.dirname(os.path.abspath(__file__))
|
||||||
MEDIA_DIR = os.path.join(BASE_DIR, "media")
|
MEDIA_DIR = os.path.join(BASE_DIR, "media")
|
||||||
|
|
||||||
WELCOME_FILENAME = "welcome.html"
|
WELCOME_FILENAME = "welcome.html"
|
||||||
|
|
||||||
|
|
||||||
|
# -------------------------------------------------
|
||||||
|
# OpenAI-Client
|
||||||
|
# -------------------------------------------------
|
||||||
|
|
||||||
def get_openai_client():
|
def get_openai_client():
|
||||||
"""Initialize OpenAI client from environment variable"""
|
"""Initialisiert den OpenAI-Client mit dem API-Key.
|
||||||
# api_key = os.getenv("OPENAI_API_KEY")
|
Der Key ist aktuell direkt im Code hinterlegt – sollte in env var ausgelagert werden."""
|
||||||
api_key = "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"
|
api_key = "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"
|
||||||
if not api_key:
|
if not api_key:
|
||||||
raise ValueError("OPENAI_API_KEY environment variable not set")
|
raise ValueError("OPENAI_API_KEY environment variable not set")
|
||||||
return OpenAI(api_key=api_key)
|
return OpenAI(api_key=api_key)
|
||||||
|
|
||||||
|
|
||||||
|
# -------------------------------------------------
|
||||||
|
# Brandfetch-Logo-Suche
|
||||||
|
# -------------------------------------------------
|
||||||
|
|
||||||
def get_brandfetch_logo(domain):
|
def get_brandfetch_logo(domain):
|
||||||
"""Try to fetch a logo URL from Brandfetch CDN."""
|
"""Versucht, ein Firmenlogo über Brandfetch CDN abzurufen.
|
||||||
|
Baut eine direkte CDN-URL: https://cdn.brandfetch.io/{domain}/logo"""
|
||||||
print(f"🔍 Generate - Versuche Brandfetch für Domain: {domain}")
|
print(f"🔍 Generate - Versuche Brandfetch für Domain: {domain}")
|
||||||
# Direct CDN URL construction - Brandfetch provides logos via cdn.brandfetch.io
|
|
||||||
logo_url = f"https://cdn.brandfetch.io/{domain}/logo?c=1idyd4Tpb2nKaXIIc8T"
|
logo_url = f"https://cdn.brandfetch.io/{domain}/logo?c=1idyd4Tpb2nKaXIIc8T"
|
||||||
|
|
||||||
# if validate_logo_url(logo_url):
|
|
||||||
print(f"✅ Generate - Brandfetch Logo gefunden: {logo_url}")
|
print(f"✅ Generate - Brandfetch Logo gefunden: {logo_url}")
|
||||||
return logo_url
|
return logo_url
|
||||||
|
|
||||||
# return None
|
|
||||||
|
|
||||||
|
# -------------------------------------------------
|
||||||
|
# Logo-Ermittlung via OpenAI
|
||||||
|
# -------------------------------------------------
|
||||||
|
|
||||||
def search_customer_logo(customer_name):
|
def search_customer_logo(customer_name):
|
||||||
|
"""
|
||||||
|
Sucht das Logo eines Kunden über einen zweistufigen Prozess:
|
||||||
|
1. OpenAI GPT-4 ermittelt die Domain des Unternehmens
|
||||||
|
2. Brandfetch CDN liefert das Logo zur Domain
|
||||||
|
|
||||||
|
Gibt die Logo-URL zurück oder None, wenn nichts gefunden wurde.
|
||||||
|
"""
|
||||||
print(f"✅ Generate - Suche nach Kundenlogo für: {customer_name}")
|
print(f"✅ Generate - Suche nach Kundenlogo für: {customer_name}")
|
||||||
"""Search for customer logo URL using OpenAI and web search"""
|
|
||||||
try:
|
try:
|
||||||
client = get_openai_client()
|
client = get_openai_client()
|
||||||
|
|
||||||
@@ -72,7 +93,20 @@ def search_customer_logo(customer_name):
|
|||||||
return None
|
return None
|
||||||
|
|
||||||
|
|
||||||
|
# -------------------------------------------------
|
||||||
|
# HTML-Generierung
|
||||||
|
# -------------------------------------------------
|
||||||
|
|
||||||
def generate_welcome_html(customer_names, logo_urls, site="stuttgart"):
|
def generate_welcome_html(customer_names, logo_urls, site="stuttgart"):
|
||||||
|
"""
|
||||||
|
Generiert den HTML-Code für die Willkommensseite.
|
||||||
|
- Links oben: CANCOM-Logo + "meets" + Kundenlogos (280px Breite)
|
||||||
|
- Rechts unten: "Herzlich Willkommen in unserer Niederlassung {Standort}"
|
||||||
|
|
||||||
|
customer_names: Liste der Kundennamen (max. 3)
|
||||||
|
logo_urls: Liste der Logo-URLs (gleiche Reihenfolge)
|
||||||
|
site: Standort-Name für die Anzeige
|
||||||
|
"""
|
||||||
site_display = site.capitalize()
|
site_display = site.capitalize()
|
||||||
names_str = ", ".join(customer_names) if customer_names else "Unbekannt"
|
names_str = ", ".join(customer_names) if customer_names else "Unbekannt"
|
||||||
print(f"✅ Generate - Willkommensseite wird generiert für: {names_str} (site: {site})")
|
print(f"✅ Generate - Willkommensseite wird generiert für: {names_str} (site: {site})")
|
||||||
@@ -174,8 +208,13 @@ def generate_welcome_html(customer_names, logo_urls, site="stuttgart"):
|
|||||||
return html_content
|
return html_content
|
||||||
|
|
||||||
|
|
||||||
|
# -------------------------------------------------
|
||||||
|
# Speichern + Playlist-Einfügung
|
||||||
|
# -------------------------------------------------
|
||||||
|
|
||||||
def save_welcome_page(customer_names, logo_urls, site="stuttgart"):
|
def save_welcome_page(customer_names, logo_urls, site="stuttgart"):
|
||||||
"""Save welcome page and return filename"""
|
"""Speichert die generierte Willkommensseite als welcome.html im Lobby-Verzeichnis.
|
||||||
|
Gibt den Dateinamen zurück oder None bei Fehler."""
|
||||||
try:
|
try:
|
||||||
welcome_dir = os.path.join(MEDIA_DIR, site, "lobby")
|
welcome_dir = os.path.join(MEDIA_DIR, site, "lobby")
|
||||||
os.makedirs(welcome_dir, exist_ok=True)
|
os.makedirs(welcome_dir, exist_ok=True)
|
||||||
@@ -196,9 +235,10 @@ def save_welcome_page(customer_names, logo_urls, site="stuttgart"):
|
|||||||
|
|
||||||
|
|
||||||
def add_customer_to_lobby_playlist(html_filename, site="stuttgart"):
|
def add_customer_to_lobby_playlist(html_filename, site="stuttgart"):
|
||||||
"""Add customer welcome page to lobby playlist for a specific site"""
|
"""Fügt die Willkommensseite (welcome.html) an Position 0 in die Lobby-Playlist ein.
|
||||||
|
Entfernt vorher alle vorhandenen welcome.html-Einträge (als String oder Dict).
|
||||||
|
Importiert load_config/save_config aus app.py (kein Zirkelimport)."""
|
||||||
try:
|
try:
|
||||||
# Import here to avoid circular imports
|
|
||||||
from app import load_config, save_config
|
from app import load_config, save_config
|
||||||
|
|
||||||
config = load_config()
|
config = load_config()
|
||||||
@@ -209,6 +249,7 @@ def add_customer_to_lobby_playlist(html_filename, site="stuttgart"):
|
|||||||
playlist = lobby.setdefault("playlist", [])
|
playlist = lobby.setdefault("playlist", [])
|
||||||
|
|
||||||
full_filename = f"{html_filename}"
|
full_filename = f"{html_filename}"
|
||||||
|
# Alte welcome.html-Einträge entfernen (sowohl String- als auch Dict-Form)
|
||||||
playlist[:] = [
|
playlist[:] = [
|
||||||
item for item in playlist
|
item for item in playlist
|
||||||
if not (
|
if not (
|
||||||
|
|||||||
@@ -1,4 +1,8 @@
|
|||||||
<!-- Footer -->
|
<!-- ═══════════════════════════════════════════════════
|
||||||
|
FOOTER – Wird von allen Seiten eingebunden.
|
||||||
|
Zeigt Autor, Copyright, Version und Hostname.
|
||||||
|
Erwartet im Kontext: year, version, hostname
|
||||||
|
═══════════════════════════════════════════════════ -->
|
||||||
<footer class="app-footer">
|
<footer class="app-footer">
|
||||||
<div class="app-footer-inner">
|
<div class="app-footer-inner">
|
||||||
<span>Autor Erik Thiele</span>
|
<span>Autor Erik Thiele</span>
|
||||||
|
|||||||
@@ -1,6 +1,11 @@
|
|||||||
<!-- Header -->
|
<!-- ═══════════════════════════════════════════════════
|
||||||
|
HEADER – Wird von admin.html, priority.html und customer.html eingebunden.
|
||||||
|
Zeigt CANCOM-Logo, Standort-Dropdown, Theme-Toggle und Logout.
|
||||||
|
Erwartet im Kontext: site_list, current_site, brand_subtitle, site_dropdown_url_prefix
|
||||||
|
═══════════════════════════════════════════════════ -->
|
||||||
<header class="navbar navbar-expand-md d-print-none brand-surface">
|
<header class="navbar navbar-expand-md d-print-none brand-surface">
|
||||||
<div class="container-xl">
|
<div class="container-xl">
|
||||||
|
<!-- Logo + Titel -->
|
||||||
<div class="navbar-brand navbar-brand-autodark pe-0 pe-md-3">
|
<div class="navbar-brand navbar-brand-autodark pe-0 pe-md-3">
|
||||||
<img class="navbar-brand-logo me-3" src="{{ url_for('static', filename='cancom.svg') }}" alt="CANCOM Logo">
|
<img class="navbar-brand-logo me-3" src="{{ url_for('static', filename='cancom.svg') }}" alt="CANCOM Logo">
|
||||||
<span class="navbar-brand-wordmark">
|
<span class="navbar-brand-wordmark">
|
||||||
@@ -8,7 +13,10 @@
|
|||||||
<span>{{ brand_subtitle }}</span>
|
<span>{{ brand_subtitle }}</span>
|
||||||
</span>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<!-- Aktionen: Standort-Dropdown, +Button, Theme, Logout -->
|
||||||
<div class="navbar-nav flex-row order-md-last top-actions">
|
<div class="navbar-nav flex-row order-md-last top-actions">
|
||||||
|
<!-- Standort-Auswahl -->
|
||||||
<div class="dropdown me-2">
|
<div class="dropdown me-2">
|
||||||
<a class="btn btn-outline-secondary" href="#" data-bs-toggle="dropdown">
|
<a class="btn btn-outline-secondary" href="#" data-bs-toggle="dropdown">
|
||||||
<i class="ti ti-building me-1"></i>{{ current_site | capitalize }} <i class="ti ti-chevron-down"></i>
|
<i class="ti ti-building me-1"></i>{{ current_site | capitalize }} <i class="ti ti-chevron-down"></i>
|
||||||
@@ -20,15 +28,21 @@
|
|||||||
{% endfor %}
|
{% endfor %}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<!-- Neuen Standort anlegen (+ Button) -->
|
||||||
<a class="btn btn-outline-secondary me-2" href="javascript:void(0)"
|
<a class="btn btn-outline-secondary me-2" href="javascript:void(0)"
|
||||||
onclick="var n=prompt('Name des neuen Standorts:'); if(n&&n.trim()) location.href='/add-site?name='+encodeURIComponent(n.trim().toLowerCase());"
|
onclick="var n=prompt('Name des neuen Standorts:'); if(n&&n.trim()) location.href='/add-site?name='+encodeURIComponent(n.trim().toLowerCase());"
|
||||||
title="Neuen Standort anlegen"
|
title="Neuen Standort anlegen"
|
||||||
style="display:inline-flex;align-items:center;justify-content:center;min-width:2.5rem;min-height:2.5rem;"><i class="ti ti-plus"></i></a>
|
style="display:inline-flex;align-items:center;justify-content:center;min-width:2.5rem;min-height:2.5rem;"><i class="ti ti-plus"></i></a>
|
||||||
|
|
||||||
|
<!-- Dark-Mode-Toggle (persistiert in localStorage("signage-theme")) -->
|
||||||
<div>
|
<div>
|
||||||
<button class="btn btn-outline-secondary theme-toggle" id="theme-toggle" title="Dark Mode umschalten">
|
<button class="btn btn-outline-secondary theme-toggle" id="theme-toggle" title="Dark Mode umschalten">
|
||||||
<i class="ti ti-moon"></i>
|
<i class="ti ti-moon"></i>
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<!-- Logout -->
|
||||||
<div class="ms-2">
|
<div class="ms-2">
|
||||||
<a href="/logout" class="btn btn-outline-secondary"><i class="ti ti-logout me-1"></i>Abmelden</a>
|
<a href="/logout" class="btn btn-outline-secondary"><i class="ti ti-logout me-1"></i>Abmelden</a>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -5,6 +5,7 @@
|
|||||||
<title>CANCOM Simple Signage Admin</title>
|
<title>CANCOM Simple Signage Admin</title>
|
||||||
<link rel="icon" href="{{ url_for('static', filename='favicon.ico') }}">
|
<link rel="icon" href="{{ url_for('static', filename='favicon.ico') }}">
|
||||||
|
|
||||||
|
<!-- ─── Dark Mode: Theme vor dem Rendern setzen ─── -->
|
||||||
<script>
|
<script>
|
||||||
(() => {
|
(() => {
|
||||||
const stored = window.localStorage.getItem("signage-theme");
|
const stored = window.localStorage.getItem("signage-theme");
|
||||||
@@ -13,6 +14,7 @@
|
|||||||
})();
|
})();
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
|
<!-- ─── Externe Bibliotheken (Tabler CSS/JS, SortableJS) ─── -->
|
||||||
<link rel="stylesheet"
|
<link rel="stylesheet"
|
||||||
href="https://unpkg.com/@tabler/core@1.0.0-beta20/dist/css/tabler.min.css">
|
href="https://unpkg.com/@tabler/core@1.0.0-beta20/dist/css/tabler.min.css">
|
||||||
<link rel="stylesheet"
|
<link rel="stylesheet"
|
||||||
@@ -25,7 +27,7 @@
|
|||||||
{% include "_styles.html" %}
|
{% include "_styles.html" %}
|
||||||
|
|
||||||
<style>
|
<style>
|
||||||
/* Tooltip Styling */
|
/* ─── Tooltip-Styling (CANCOM-rot) ─── */
|
||||||
.tooltip {
|
.tooltip {
|
||||||
--bs-tooltip-border-color: var(--ccm-primary);
|
--bs-tooltip-border-color: var(--ccm-primary);
|
||||||
}
|
}
|
||||||
@@ -41,7 +43,7 @@
|
|||||||
background-color: #ffffff;
|
background-color: #ffffff;
|
||||||
}
|
}
|
||||||
|
|
||||||
/* Playlist als Grid */
|
/* ─── Playlist-Zeile als Grid (Checkbox | Name | Typ | Größe | Löschen | Drag) ─── */
|
||||||
.playlist-row {
|
.playlist-row {
|
||||||
display: grid;
|
display: grid;
|
||||||
grid-template-columns:
|
grid-template-columns:
|
||||||
@@ -62,6 +64,7 @@
|
|||||||
font-size: 0.9em;
|
font-size: 0.9em;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/* ─── Player-URL im Screen-Header als Link (grau, rot bei Hover) ─── */
|
||||||
.page-subtitle a {
|
.page-subtitle a {
|
||||||
color: inherit;
|
color: inherit;
|
||||||
text-decoration: none;
|
text-decoration: none;
|
||||||
@@ -70,6 +73,7 @@
|
|||||||
color: var(--ccm-primary);
|
color: var(--ccm-primary);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/* ─── Drag-Handle für SortableJS ─── */
|
||||||
.drag-handle {
|
.drag-handle {
|
||||||
cursor: grab;
|
cursor: grab;
|
||||||
font-size: 1.2em;
|
font-size: 1.2em;
|
||||||
@@ -80,6 +84,7 @@
|
|||||||
cursor: grabbing;
|
cursor: grabbing;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/* ─── Tab-Styling (extend to card edges, unified background) ─── */
|
||||||
.card-body .nav-tabs {
|
.card-body .nav-tabs {
|
||||||
margin-left: -1rem;
|
margin-left: -1rem;
|
||||||
margin-right: -1rem;
|
margin-right: -1rem;
|
||||||
@@ -108,7 +113,10 @@
|
|||||||
{% set site_dropdown_url_prefix = "/admin/" %}
|
{% set site_dropdown_url_prefix = "/admin/" %}
|
||||||
{% include "_header.html" %}
|
{% include "_header.html" %}
|
||||||
|
|
||||||
<!-- Second Nav -->
|
<!-- ═══════════════════════════════════════════════════
|
||||||
|
Navigation (zweite Zeile)
|
||||||
|
Übersicht | Priority | Willkommensseite | Screens-Dropdown | +
|
||||||
|
═══════════════════════════════════════════════════ -->
|
||||||
<div class="navbar-expand-md">
|
<div class="navbar-expand-md">
|
||||||
<div class="collapse navbar-collapse show">
|
<div class="collapse navbar-collapse show">
|
||||||
<div class="navbar navbar-dark nav-surface">
|
<div class="navbar navbar-dark nav-surface">
|
||||||
@@ -127,6 +135,7 @@
|
|||||||
{% endfor %}
|
{% endfor %}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
<!-- + Button: Neuen Screen anlegen (prompt → /admin/<site>/add-screen?name=...) -->
|
||||||
<a class="btn btn-white" href="javascript:void(0)"
|
<a class="btn btn-white" href="javascript:void(0)"
|
||||||
onclick="var n=prompt('Name des neuen Screens:'); if(n&&n.trim()) location.href='/admin/{{ current_site }}/add-screen?name='+encodeURIComponent(n.trim().toLowerCase());"
|
onclick="var n=prompt('Name des neuen Screens:'); if(n&&n.trim()) location.href='/admin/{{ current_site }}/add-screen?name='+encodeURIComponent(n.trim().toLowerCase());"
|
||||||
title="Neuen Screen anlegen"><i class="ti ti-plus"></i></a>
|
title="Neuen Screen anlegen"><i class="ti ti-plus"></i></a>
|
||||||
@@ -136,13 +145,13 @@
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- Content -->
|
<!-- ═══════════════════════════════════════════════════
|
||||||
|
CONTENT
|
||||||
|
═══════════════════════════════════════════════════ -->
|
||||||
<div class="page-wrapper">
|
<div class="page-wrapper">
|
||||||
<div class="container-xl mt-4">
|
<div class="container-xl mt-4">
|
||||||
|
|
||||||
<!-- ========================= -->
|
<!-- ─── Standort löschen (Button oben rechts) ─── -->
|
||||||
<!-- STANDORT VERWALTUNG -->
|
|
||||||
<!-- ========================= -->
|
|
||||||
<div class="d-flex justify-content-end mb-3">
|
<div class="d-flex justify-content-end mb-3">
|
||||||
<form action="/admin/{{ current_site }}/delete-site" method="post"
|
<form action="/admin/{{ current_site }}/delete-site" method="post"
|
||||||
onsubmit="return confirm('Standort »{{ current_site }}« wirklich löschen? Alle Screens und Medien werden gelöscht.')">
|
onsubmit="return confirm('Standort »{{ current_site }}« wirklich löschen? Alle Screens und Medien werden gelöscht.')">
|
||||||
@@ -151,13 +160,13 @@
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- ========================= -->
|
<!-- ========================= -->
|
||||||
<!-- SCREENS -->
|
<!-- SCREEN-CARDS -->
|
||||||
<!-- ========================= -->
|
<!-- ========================= -->
|
||||||
|
|
||||||
{% for screen, cfg in screens.items() %}
|
{% for screen, cfg in screens.items() %}
|
||||||
<div class="card mb-6" id="screen-{{ screen }}">
|
<div class="card mb-6" id="screen-{{ screen }}">
|
||||||
|
|
||||||
<!-- Card Header -->
|
<!-- Card-Header: Screen-Name, Status-Badge, Player-URL, Löschen -->
|
||||||
<div class="card-header d-flex justify-content-between align-items-center">
|
<div class="card-header d-flex justify-content-between align-items-center">
|
||||||
<span>
|
<span>
|
||||||
<div class="page-title">{{ current_site | capitalize }}: <span class="fw-normal">Screen</span> <span class="text-uppercase">- {{ screen }}</span></div>
|
<div class="page-title">{{ current_site | capitalize }}: <span class="fw-normal">Screen</span> <span class="text-uppercase">- {{ screen }}</span></div>
|
||||||
@@ -177,6 +186,7 @@
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<!-- Card-Body: Tabs (Playlist | Einstellungen | Medien) -->
|
||||||
<div class="card-body">
|
<div class="card-body">
|
||||||
|
|
||||||
<ul class="nav nav-tabs" data-bs-toggle="tabs" role="tablist">
|
<ul class="nav nav-tabs" data-bs-toggle="tabs" role="tablist">
|
||||||
@@ -199,18 +209,20 @@
|
|||||||
|
|
||||||
<div class="tab-content mt-3">
|
<div class="tab-content mt-3">
|
||||||
|
|
||||||
<!-- Tab: Playlist -->
|
<!-- ═══ Tab: Playlist ═══ -->
|
||||||
<div class="tab-pane active" id="tab-playlist-{{ screen }}" role="tabpanel">
|
<div class="tab-pane active" id="tab-playlist-{{ screen }}" role="tabpanel">
|
||||||
<ul class="list-group mb-3" id="playlist-{{ screen }}">
|
<ul class="list-group mb-3" id="playlist-{{ screen }}">
|
||||||
{% for file in media_files[screen] %}
|
{% for file in media_files[screen] %}
|
||||||
<li class="list-group-item playlist-row"
|
<li class="list-group-item playlist-row"
|
||||||
data-file="{{ file.name }}">
|
data-file="{{ file.name }}">
|
||||||
|
|
||||||
|
<!-- Aktiv-Checkbox -->
|
||||||
<input class="form-check-input playlist-enabled"
|
<input class="form-check-input playlist-enabled"
|
||||||
type="checkbox"
|
type="checkbox"
|
||||||
{% if file.enabled %}checked{% endif %}
|
{% if file.enabled %}checked{% endif %}
|
||||||
title="Element aktivieren">
|
title="Element aktivieren">
|
||||||
|
|
||||||
|
<!-- Dateiname (mit Tooltip-Vorschau) -->
|
||||||
<span class="playlist-name"
|
<span class="playlist-name"
|
||||||
{% if file.type == "image" %}
|
{% if file.type == "image" %}
|
||||||
data-bs-toggle="tooltip"
|
data-bs-toggle="tooltip"
|
||||||
@@ -223,6 +235,7 @@
|
|||||||
{{ file.name }}{% if file.type == "url" and file.zoom != 1.0 %} <strong style="color: #DA002D;">[{{ file.zoom }}x]</strong>{% endif %}
|
{{ file.name }}{% if file.type == "url" and file.zoom != 1.0 %} <strong style="color: #DA002D;">[{{ file.zoom }}x]</strong>{% endif %}
|
||||||
</span>
|
</span>
|
||||||
|
|
||||||
|
<!-- Typ-Badge -->
|
||||||
{% if file.type == "image" %}
|
{% if file.type == "image" %}
|
||||||
<span class="badge bg-purple text-purple-fg">Bild</span>
|
<span class="badge bg-purple text-purple-fg">Bild</span>
|
||||||
{% elif file.type == "url" or file.type == "html" %}
|
{% elif file.type == "url" or file.type == "html" %}
|
||||||
@@ -231,6 +244,7 @@
|
|||||||
<span class="badge bg-orange text-orange-fg">Video</span>
|
<span class="badge bg-orange text-orange-fg">Video</span>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
|
|
||||||
|
<!-- Größe -->
|
||||||
<span class="playlist-size">
|
<span class="playlist-size">
|
||||||
{% if file.type == "url" %}
|
{% if file.type == "url" %}
|
||||||
{{ file.size }}
|
{{ file.size }}
|
||||||
@@ -239,7 +253,7 @@
|
|||||||
{% endif %}
|
{% endif %}
|
||||||
</span>
|
</span>
|
||||||
|
|
||||||
<!-- Delete -->
|
<!-- Löschen -->
|
||||||
<form action="{{ url_for('delete_file', site=current_site, screen=screen, filename=file.name) }}"
|
<form action="{{ url_for('delete_file', site=current_site, screen=screen, filename=file.name) }}"
|
||||||
method="post">
|
method="post">
|
||||||
<button type="submit"
|
<button type="submit"
|
||||||
@@ -249,7 +263,7 @@
|
|||||||
</button>
|
</button>
|
||||||
</form>
|
</form>
|
||||||
|
|
||||||
<!-- Drag -->
|
<!-- Drag-Handle (SortableJS) -->
|
||||||
<span class="drag-handle" title="Reihenfolge ändern">⇅</span>
|
<span class="drag-handle" title="Reihenfolge ändern">⇅</span>
|
||||||
</li>
|
</li>
|
||||||
{% endfor %}
|
{% endfor %}
|
||||||
@@ -261,7 +275,7 @@
|
|||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- Tab: Einstellungen -->
|
<!-- ═══ Tab: Einstellungen ═══ -->
|
||||||
<div class="tab-pane" id="settings-{{ screen }}" role="tabpanel">
|
<div class="tab-pane" id="settings-{{ screen }}" role="tabpanel">
|
||||||
<form action="/admin/{{ current_site }}/update/{{ screen }}" method="post">
|
<form action="/admin/{{ current_site }}/update/{{ screen }}" method="post">
|
||||||
<div class="mb-2">
|
<div class="mb-2">
|
||||||
@@ -328,7 +342,7 @@
|
|||||||
</form>
|
</form>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- Tab: Medien -->
|
<!-- ═══ Tab: Medien ═══ -->
|
||||||
<div class="tab-pane" id="media-{{ screen }}" role="tabpanel">
|
<div class="tab-pane" id="media-{{ screen }}" role="tabpanel">
|
||||||
<h3>Medien hochladen</h3>
|
<h3>Medien hochladen</h3>
|
||||||
<form action="/admin/{{ current_site }}/upload/{{ screen }}"
|
<form action="/admin/{{ current_site }}/upload/{{ screen }}"
|
||||||
@@ -355,23 +369,24 @@
|
|||||||
</form>
|
</form>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
</div>
|
</div> <!-- /tab-content -->
|
||||||
|
|
||||||
</div>
|
</div> <!-- /card-body -->
|
||||||
</div>
|
</div> <!-- /card -->
|
||||||
{% endfor %}
|
{% endfor %}
|
||||||
|
|
||||||
</div>
|
</div> <!-- /container -->
|
||||||
</div>
|
</div> <!-- /page-wrapper -->
|
||||||
|
|
||||||
<!-- Footer -->
|
<!-- Footer -->
|
||||||
{% include "_footer.html" %}
|
{% include "_footer.html" %}
|
||||||
|
|
||||||
</div>
|
</div> <!-- /page -->
|
||||||
|
|
||||||
<script>
|
<script>
|
||||||
document.addEventListener("DOMContentLoaded", function () {
|
document.addEventListener("DOMContentLoaded", function () {
|
||||||
|
|
||||||
|
/* ─── SortableJS Drag & Drop initialisieren ─── */
|
||||||
function initSortable(name) {
|
function initSortable(name) {
|
||||||
const el = document.getElementById("playlist-" + name);
|
const el = document.getElementById("playlist-" + name);
|
||||||
if (!el || typeof Sortable === "undefined") return;
|
if (!el || typeof Sortable === "undefined") return;
|
||||||
@@ -384,11 +399,13 @@ document.addEventListener("DOMContentLoaded", function () {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
// ✅ Normale Screens
|
// Für jeden Screen eine Sortable-Instanz
|
||||||
{% for screen in screens.keys() %}
|
{% for screen in screens.keys() %}
|
||||||
initSortable("{{ screen }}");
|
initSortable("{{ screen }}");
|
||||||
{% endfor %}
|
{% endfor %}
|
||||||
|
|
||||||
|
/* ─── Playlist-Reihenfolge speichern ───
|
||||||
|
Sendet die aktuelle Sortierung als JSON an /admin/<site>/playlist/<screen> */
|
||||||
function savePlaylist(site, screen) {
|
function savePlaylist(site, screen) {
|
||||||
const items = document.querySelectorAll(
|
const items = document.querySelectorAll(
|
||||||
"#playlist-" + screen + " li"
|
"#playlist-" + screen + " li"
|
||||||
@@ -415,13 +432,13 @@ document.addEventListener("DOMContentLoaded", function () {
|
|||||||
|
|
||||||
window.savePlaylist = savePlaylist;
|
window.savePlaylist = savePlaylist;
|
||||||
|
|
||||||
// Initialize tooltips
|
/* ─── Bootstrap Tooltips initialisieren ─── */
|
||||||
var tooltipTriggerList = [].slice.call(document.querySelectorAll('[data-bs-toggle="tooltip"]'));
|
var tooltipTriggerList = [].slice.call(document.querySelectorAll('[data-bs-toggle="tooltip"]'));
|
||||||
var tooltipList = tooltipTriggerList.map(function (tooltipTriggerEl) {
|
var tooltipList = tooltipTriggerList.map(function (tooltipTriggerEl) {
|
||||||
return new bootstrap.Tooltip(tooltipTriggerEl);
|
return new bootstrap.Tooltip(tooltipTriggerEl);
|
||||||
});
|
});
|
||||||
|
|
||||||
// Dark Mode Toggle
|
/* ─── Dark Mode Toggle (persistiert in localStorage) ─── */
|
||||||
const themeToggle = document.getElementById("theme-toggle");
|
const themeToggle = document.getElementById("theme-toggle");
|
||||||
const root = document.documentElement;
|
const root = document.documentElement;
|
||||||
|
|
||||||
|
|||||||
@@ -6,6 +6,7 @@
|
|||||||
<title>Willkommensseite</title>
|
<title>Willkommensseite</title>
|
||||||
<link rel="icon" href="{{ url_for('static', filename='favicon.ico') }}">
|
<link rel="icon" href="{{ url_for('static', filename='favicon.ico') }}">
|
||||||
|
|
||||||
|
<!-- ─── Dark Mode: Theme vor dem Rendern setzen ─── -->
|
||||||
<script>
|
<script>
|
||||||
(() => {
|
(() => {
|
||||||
const stored = window.localStorage.getItem("signage-theme");
|
const stored = window.localStorage.getItem("signage-theme");
|
||||||
@@ -26,6 +27,7 @@
|
|||||||
<style>
|
<style>
|
||||||
.form-label { font-weight: 600; }
|
.form-label { font-weight: 600; }
|
||||||
.card { box-shadow: 0 20px 60px rgba(0,0,0,0.08); }
|
.card { box-shadow: 0 20px 60px rgba(0,0,0,0.08); }
|
||||||
|
/* ─── Logo-Vorschau-Box (gestrichelte Border) ─── */
|
||||||
.logo-preview-box {
|
.logo-preview-box {
|
||||||
border: 2px dashed var(--ccm-border);
|
border: 2px dashed var(--ccm-border);
|
||||||
border-radius: 0.75rem;
|
border-radius: 0.75rem;
|
||||||
@@ -61,7 +63,9 @@
|
|||||||
{% set site_dropdown_url_prefix = "/willkommen?site=" %}
|
{% set site_dropdown_url_prefix = "/willkommen?site=" %}
|
||||||
{% include "_header.html" %}
|
{% include "_header.html" %}
|
||||||
|
|
||||||
<!-- Second Nav -->
|
<!-- ═══════════════════════════════════════════════════
|
||||||
|
Navigation (zweite Zeile)
|
||||||
|
═══════════════════════════════════════════════════ -->
|
||||||
<div class="navbar-expand-md">
|
<div class="navbar-expand-md">
|
||||||
<div class="collapse navbar-collapse show">
|
<div class="collapse navbar-collapse show">
|
||||||
<div class="navbar navbar-dark nav-surface">
|
<div class="navbar navbar-dark nav-surface">
|
||||||
@@ -76,6 +80,11 @@
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<!-- ═══════════════════════════════════════════════════
|
||||||
|
CONTENT – Willkommensseite-Formular
|
||||||
|
Bis zu 3 Kundennamen eingeben → Logos via OpenAI + Brandfetch
|
||||||
|
→ welcome.html wird generiert und in Lobby-Playlist platziert
|
||||||
|
═══════════════════════════════════════════════════ -->
|
||||||
<div class="page-wrapper">
|
<div class="page-wrapper">
|
||||||
<div class="container-xl mt-4">
|
<div class="container-xl mt-4">
|
||||||
<div class="card mb-4">
|
<div class="card mb-4">
|
||||||
@@ -95,7 +104,7 @@
|
|||||||
<div class="alert alert-success">{{ success }}</div>
|
<div class="alert alert-success">{{ success }}</div>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
|
|
||||||
<!-- Logo Previews -->
|
<!-- ─── Logo-Vorschau (wird nach POST angezeigt) ─── -->
|
||||||
<div class="row g-3 mb-4">
|
<div class="row g-3 mb-4">
|
||||||
{% for i in range(3) %}
|
{% for i in range(3) %}
|
||||||
<div class="col-md-4">
|
<div class="col-md-4">
|
||||||
@@ -111,6 +120,7 @@
|
|||||||
{% endfor %}
|
{% endfor %}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<!-- ─── Eingabeformular (bis zu 3 Kunden) ─── -->
|
||||||
<form method="POST">
|
<form method="POST">
|
||||||
<input type="hidden" name="site" value="{{ site }}">
|
<input type="hidden" name="site" value="{{ site }}">
|
||||||
<div class="row g-3 mb-3">
|
<div class="row g-3 mb-3">
|
||||||
@@ -155,6 +165,7 @@
|
|||||||
|
|
||||||
<script>
|
<script>
|
||||||
window.addEventListener("load", () => {
|
window.addEventListener("load", () => {
|
||||||
|
/* ─── Dark Mode Toggle ─── */
|
||||||
const themeToggle = document.getElementById("theme-toggle");
|
const themeToggle = document.getElementById("theme-toggle");
|
||||||
const root = document.documentElement;
|
const root = document.documentElement;
|
||||||
|
|
||||||
|
|||||||
@@ -11,7 +11,6 @@
|
|||||||
background-color: #F4F6F8;
|
background-color: #F4F6F8;
|
||||||
color: #212121;
|
color: #212121;
|
||||||
}
|
}
|
||||||
|
|
||||||
.btn-primary {
|
.btn-primary {
|
||||||
background-color: #DA002D;
|
background-color: #DA002D;
|
||||||
border-color: #DA002D;
|
border-color: #DA002D;
|
||||||
@@ -23,15 +22,18 @@
|
|||||||
</style>
|
</style>
|
||||||
</head>
|
</head>
|
||||||
|
|
||||||
|
<!-- ─── Login-Seite (zentriert) ─── -->
|
||||||
<body class="d-flex align-items-center justify-content-center" style="height:100vh;">
|
<body class="d-flex align-items-center justify-content-center" style="height:100vh;">
|
||||||
<div class="card card-md">
|
<div class="card card-md">
|
||||||
<div class="card-body">
|
<div class="card-body">
|
||||||
<h2 class="text-center mb-4">Signage Admin</h2>
|
<h2 class="text-center mb-4">Signage Admin</h2>
|
||||||
|
|
||||||
{% if error %}
|
{% if error %}
|
||||||
<div class="alert alert-danger text-center" role="alert">
|
<div class="alert alert-danger text-center" role="alert">
|
||||||
{{ error }}
|
{{ error }}
|
||||||
</div>
|
</div>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
|
|
||||||
<form method="post">
|
<form method="post">
|
||||||
<div class="mb-3">
|
<div class="mb-3">
|
||||||
<label class="form-label">Benutzername</label>
|
<label class="form-label">Benutzername</label>
|
||||||
|
|||||||
@@ -6,6 +6,9 @@
|
|||||||
<link rel="icon" href="{{ url_for('static', filename='favicon.ico') }}">
|
<link rel="icon" href="{{ url_for('static', filename='favicon.ico') }}">
|
||||||
|
|
||||||
<style>
|
<style>
|
||||||
|
/* ─── Player-Grundlayout ───
|
||||||
|
Alle Medien (img/video/iframe) werden zentriert auf Vollbild angezeigt.
|
||||||
|
Standardmäßig ausgeblendet – nur das aktive Element ist sichtbar. */
|
||||||
html, body {
|
html, body {
|
||||||
font-family: system-ui, sans-serif;
|
font-family: system-ui, sans-serif;
|
||||||
margin: 0;
|
margin: 0;
|
||||||
@@ -37,6 +40,7 @@
|
|||||||
object-fit: contain;
|
object-fit: contain;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/* ─── Newsticker (Laufband) ─── */
|
||||||
.newsticker-text {
|
.newsticker-text {
|
||||||
flex: 1;
|
flex: 1;
|
||||||
overflow: hidden;
|
overflow: hidden;
|
||||||
@@ -47,11 +51,9 @@
|
|||||||
.newsticker-track {
|
.newsticker-track {
|
||||||
display: inline-block;
|
display: inline-block;
|
||||||
white-space: nowrap;
|
white-space: nowrap;
|
||||||
|
|
||||||
position: absolute;
|
position: absolute;
|
||||||
left: 100%;
|
left: 100%;
|
||||||
top: 0;
|
top: 0;
|
||||||
|
|
||||||
height: 40px;
|
height: 40px;
|
||||||
line-height: 40px;
|
line-height: 40px;
|
||||||
animation: ticker-scroll 40s linear infinite;
|
animation: ticker-scroll 40s linear infinite;
|
||||||
@@ -65,12 +67,16 @@
|
|||||||
from { transform: translateX(0); }
|
from { transform: translateX(0); }
|
||||||
to { transform: translateX(-100vw); }
|
to { transform: translateX(-100vw); }
|
||||||
}
|
}
|
||||||
|
|
||||||
</style>
|
</style>
|
||||||
</head>
|
</head>
|
||||||
|
|
||||||
<body>
|
<body>
|
||||||
|
|
||||||
|
<!-- ═══════════════════════════════════════════════════
|
||||||
|
OVERLAY / Custom-URL (iframe-Modus)
|
||||||
|
Wenn der Aktions-Button angeklickt wird, öffnet
|
||||||
|
sich dieser Overlay-Bereich mit Zurück-Button.
|
||||||
|
═══════════════════════════════════════════════════ -->
|
||||||
<div id="overlay" style="display:none;position:fixed;inset:0;z-index:99999;background:#fff;">
|
<div id="overlay" style="display:none;position:fixed;inset:0;z-index:99999;background:#fff;">
|
||||||
<button onclick="closeOverlay()"
|
<button onclick="closeOverlay()"
|
||||||
style="position:absolute;top:16px;left:16px;z-index:10;
|
style="position:absolute;top:16px;left:16px;z-index:10;
|
||||||
@@ -81,8 +87,10 @@
|
|||||||
<iframe id="overlay-iframe" style="width:100%;height:100%;border:none;display:block;"></iframe>
|
<iframe id="overlay-iframe" style="width:100%;height:100%;border:none;display:block;"></iframe>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<!-- ─── Custom-URL-Button (oben links) ─── -->
|
||||||
{% if custom_url and custom_url_enabled %}
|
{% if custom_url and custom_url_enabled %}
|
||||||
{% if custom_url_target == "redirect" %}
|
{% if custom_url_target == "redirect" %}
|
||||||
|
<!-- Direkt-Weiterleitung (verlässt den Player) -->
|
||||||
<a href="{{ custom_url }}"
|
<a href="{{ custom_url }}"
|
||||||
style="position:fixed;top:16px;left:16px;z-index:9999;
|
style="position:fixed;top:16px;left:16px;z-index:9999;
|
||||||
padding:10px 20px;background:#DA002D;color:#fff;
|
padding:10px 20px;background:#DA002D;color:#fff;
|
||||||
@@ -91,6 +99,7 @@
|
|||||||
{{ custom_url_label or "Info" }}
|
{{ custom_url_label or "Info" }}
|
||||||
</a>
|
</a>
|
||||||
{% else %}
|
{% else %}
|
||||||
|
<!-- iframe-Overlay (bleibt im Player, Zurück-Button) -->
|
||||||
<button onclick='openOverlay({{ custom_url | tojson }})'
|
<button onclick='openOverlay({{ custom_url | tojson }})'
|
||||||
style="position:fixed;top:16px;left:16px;z-index:9999;
|
style="position:fixed;top:16px;left:16px;z-index:9999;
|
||||||
padding:10px 20px;background:#DA002D;color:#fff;
|
padding:10px 20px;background:#DA002D;color:#fff;
|
||||||
@@ -101,11 +110,12 @@
|
|||||||
{% endif %}
|
{% endif %}
|
||||||
{% endif %}
|
{% endif %}
|
||||||
|
|
||||||
|
<!-- ─── Medien-Elemente (eines ist jeweils sichtbar) ─── -->
|
||||||
<img id="image">
|
<img id="image">
|
||||||
<video id="video" muted autoplay playsinline></video>
|
<video id="video" muted autoplay playsinline></video>
|
||||||
<iframe id="iframe"></iframe>
|
<iframe id="iframe"></iframe>
|
||||||
|
|
||||||
|
<!-- ─── Newsticker (optional, Laufband unten) ─── -->
|
||||||
{% if newsticker_enabled %}
|
{% if newsticker_enabled %}
|
||||||
<div id="newsticker-bar" style="position:fixed;left:0;right:0;bottom:0;height:40px;z-index:10000;background:#DA002D;color:#fff;display:flex;align-items:center;overflow:hidden;">
|
<div id="newsticker-bar" style="position:fixed;left:0;right:0;bottom:0;height:40px;z-index:10000;background:#DA002D;color:#fff;display:flex;align-items:center;overflow:hidden;">
|
||||||
<div id="newsticker-text" class="newsticker-text">
|
<div id="newsticker-text" class="newsticker-text">
|
||||||
@@ -117,9 +127,15 @@
|
|||||||
</div>
|
</div>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
|
|
||||||
|
|
||||||
<script>
|
<script>
|
||||||
|
// ═════════════════════════════════════════════════════
|
||||||
|
// Player-JavaScript
|
||||||
|
// ═════════════════════════════════════════════════════
|
||||||
|
|
||||||
let playerTimer = null;
|
let playerTimer = null;
|
||||||
|
|
||||||
|
// ─── Custom-URL-Overlay ───
|
||||||
function openOverlay(url) {
|
function openOverlay(url) {
|
||||||
if (playerTimer) clearTimeout(playerTimer);
|
if (playerTimer) clearTimeout(playerTimer);
|
||||||
document.getElementById("overlay").style.display = "block";
|
document.getElementById("overlay").style.display = "block";
|
||||||
@@ -132,7 +148,7 @@ function closeOverlay() {
|
|||||||
playNext();
|
playNext();
|
||||||
}
|
}
|
||||||
|
|
||||||
// Newsticker Uhrzeit
|
// ─── Newsticker-Uhr (aktuelle Uhrzeit) ───
|
||||||
function updateClock() {
|
function updateClock() {
|
||||||
const el = document.getElementById('newsticker-clock');
|
const el = document.getElementById('newsticker-clock');
|
||||||
if (!el) return;
|
if (!el) return;
|
||||||
@@ -141,29 +157,23 @@ function updateClock() {
|
|||||||
}
|
}
|
||||||
setInterval(updateClock, 1000);
|
setInterval(updateClock, 1000);
|
||||||
updateClock();
|
updateClock();
|
||||||
/* -----------------------------
|
|
||||||
Daten vom Server
|
// ─── Vom Server übergebene Daten ───
|
||||||
----------------------------- */
|
|
||||||
const normalFiles = {{ normal_files | tojson }};
|
const normalFiles = {{ normal_files | tojson }};
|
||||||
const prioFiles = {{ prio_files | tojson }};
|
const prioFiles = {{ prio_files | tojson }};
|
||||||
const interval = {{ interval }} * 1000;
|
const interval = {{ interval }} * 1000;
|
||||||
const screen = "{{ screen }}";
|
const screen = "{{ screen }}";
|
||||||
const site = "{{ site }}";
|
const site = "{{ site }}";
|
||||||
|
|
||||||
/* -----------------------------
|
// ─── Player-Status ───
|
||||||
Player State
|
|
||||||
----------------------------- */
|
|
||||||
let normalIndex = 0;
|
let normalIndex = 0;
|
||||||
let prioIndex = 0;
|
let prioIndex = 0;
|
||||||
let mode = "normal"; // "normal" oder "prio"
|
let mode = "normal"; // "normal" | "prio"
|
||||||
|
|
||||||
const img = document.getElementById("image");
|
const img = document.getElementById("image");
|
||||||
const vid = document.getElementById("video");
|
const vid = document.getElementById("video");
|
||||||
const iframe = document.getElementById("iframe");
|
const iframe = document.getElementById("iframe");
|
||||||
|
|
||||||
/* -----------------------------
|
// ─── Hilfsfunktionen ───
|
||||||
Hilfsfunktionen
|
|
||||||
----------------------------- */
|
|
||||||
function normalizeItem(item) {
|
function normalizeItem(item) {
|
||||||
if (typeof item === "string") {
|
if (typeof item === "string") {
|
||||||
const lower = item.toLowerCase();
|
const lower = item.toLowerCase();
|
||||||
@@ -172,7 +182,6 @@ function normalizeItem(item) {
|
|||||||
}
|
}
|
||||||
return { kind: "file", name: item };
|
return { kind: "file", name: item };
|
||||||
}
|
}
|
||||||
// Item is already an object (from backend) - ensure zoom is set
|
|
||||||
if (item.kind === "url" && !("zoom" in item)) {
|
if (item.kind === "url" && !("zoom" in item)) {
|
||||||
item.zoom = 1.0;
|
item.zoom = 1.0;
|
||||||
}
|
}
|
||||||
@@ -185,65 +194,51 @@ function normalizeItem(item) {
|
|||||||
function isVideo(item) {
|
function isVideo(item) {
|
||||||
return item.kind === "file" && item.name.toLowerCase().endsWith(".mp4");
|
return item.kind === "file" && item.name.toLowerCase().endsWith(".mp4");
|
||||||
}
|
}
|
||||||
|
|
||||||
function isImage(item) {
|
function isImage(item) {
|
||||||
return item.kind === "file" && /\.(jpg|jpeg|png)$/i.test(item.name);
|
return item.kind === "file" && /\.(jpg|jpeg|png)$/i.test(item.name);
|
||||||
}
|
}
|
||||||
|
|
||||||
function isHtml(item) {
|
function isHtml(item) {
|
||||||
return item.kind === "file" && /\.(html?|htm)$/i.test(item.name);
|
return item.kind === "file" && /\.(html?|htm)$/i.test(item.name);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// ─── Nächstes Element aus der Playlist holen ───
|
||||||
|
// Ablauf: Normal-Playlist → Priority → Normal → …
|
||||||
function getNextItem() {
|
function getNextItem() {
|
||||||
const normalizedNormal = normalFiles.map(normalizeItem);
|
const normalizedNormal = normalFiles.map(normalizeItem);
|
||||||
const normalizedPrio = prioFiles.map(normalizeItem);
|
const normalizedPrio = prioFiles.map(normalizeItem);
|
||||||
const activeNormal = normalizedNormal.filter(item => item.enabled !== false);
|
const activeNormal = normalizedNormal.filter(item => item.enabled !== false);
|
||||||
const activePrio = normalizedPrio.filter(item => item.enabled !== false);
|
const activePrio = normalizedPrio.filter(item => item.enabled !== false);
|
||||||
|
|
||||||
// ✅ Sonderfall: nur Priority vorhanden
|
// Nur Priority vorhanden
|
||||||
if (activeNormal.length === 0 && activePrio.length > 0) {
|
if (activeNormal.length === 0 && activePrio.length > 0) {
|
||||||
return {
|
return { item: activePrio[prioIndex++ % activePrio.length], isPrio: true };
|
||||||
item: activePrio[prioIndex++ % activePrio.length],
|
|
||||||
isPrio: true
|
|
||||||
};
|
|
||||||
}
|
}
|
||||||
|
// Nur normale Playlist vorhanden
|
||||||
// ✅ Sonderfall: nur normale Playlist vorhanden
|
|
||||||
if (activePrio.length === 0 && activeNormal.length > 0) {
|
if (activePrio.length === 0 && activeNormal.length > 0) {
|
||||||
return {
|
return { item: activeNormal[normalIndex++ % activeNormal.length], isPrio: false };
|
||||||
item: activeNormal[normalIndex++ % activeNormal.length],
|
|
||||||
isPrio: false
|
|
||||||
};
|
|
||||||
}
|
}
|
||||||
|
// Normal-Phase
|
||||||
// ✅ Normal-Phase
|
|
||||||
if (mode === "normal") {
|
if (mode === "normal") {
|
||||||
const item = activeNormal[normalIndex++];
|
const item = activeNormal[normalIndex++];
|
||||||
if (normalIndex >= activeNormal.length) {
|
if (normalIndex >= activeNormal.length) {
|
||||||
normalIndex = 0;
|
normalIndex = 0;
|
||||||
if (activePrio.length > 0) {
|
if (activePrio.length > 0) mode = "prio";
|
||||||
mode = "prio"; // ➜ nach kompletter Normal-Playlist wechseln
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
return { item, isPrio: false };
|
return { item, isPrio: false };
|
||||||
}
|
}
|
||||||
|
// Priority-Phase
|
||||||
// ✅ Priority-Phase
|
|
||||||
if (mode === "prio") {
|
if (mode === "prio") {
|
||||||
const item = activePrio[prioIndex++];
|
const item = activePrio[prioIndex++];
|
||||||
if (prioIndex >= activePrio.length) {
|
if (prioIndex >= activePrio.length) {
|
||||||
prioIndex = 0;
|
prioIndex = 0;
|
||||||
mode = "normal"; // ➜ nach kompletter Priority zurück
|
mode = "normal";
|
||||||
}
|
}
|
||||||
return { item, isPrio: true };
|
return { item, isPrio: true };
|
||||||
}
|
}
|
||||||
|
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
/* -----------------------------
|
// ─── Aktuelles Medium abspielen ───
|
||||||
Medien abspielen
|
|
||||||
----------------------------- */
|
|
||||||
function playNext() {
|
function playNext() {
|
||||||
const entry = getNextItem();
|
const entry = getNextItem();
|
||||||
if (!entry) return;
|
if (!entry) return;
|
||||||
@@ -251,25 +246,22 @@ function playNext() {
|
|||||||
const item = entry.item;
|
const item = entry.item;
|
||||||
const basePath = entry.isPrio ? "priority" : screen;
|
const basePath = entry.isPrio ? "priority" : screen;
|
||||||
|
|
||||||
|
// Alle Elemente ausblenden
|
||||||
img.style.display = "none";
|
img.style.display = "none";
|
||||||
vid.style.display = "none";
|
vid.style.display = "none";
|
||||||
iframe.style.display = "none";
|
iframe.style.display = "none";
|
||||||
vid.pause();
|
vid.pause();
|
||||||
vid.src = "";
|
vid.src = "";
|
||||||
iframe.src = "";
|
iframe.src = "";
|
||||||
iframe.style.transform = "scale(1)"; // Reset zoom
|
iframe.style.transform = "scale(1)";
|
||||||
|
|
||||||
|
// URL im iframe anzeigen (mit Zoom-Unterstützung)
|
||||||
if (item.kind === "url") {
|
if (item.kind === "url") {
|
||||||
iframe.style.display = "block";
|
iframe.style.display = "block";
|
||||||
iframe.src = item.url;
|
iframe.src = item.url;
|
||||||
// Apply zoom factor with browser-like behavior
|
|
||||||
// Zoom < 1.0: mehr Inhalt sichtbar (wie Browser herauszoomen)
|
|
||||||
// Zoom > 1.0: weniger Inhalt sichtbar (wie Browser reinzoomen)
|
|
||||||
const zoom = item.zoom || 1.0;
|
const zoom = item.zoom || 1.0;
|
||||||
|
|
||||||
if (zoom !== 1.0) {
|
if (zoom !== 1.0) {
|
||||||
// Adjust iframe size and scale to simulate real browser zoom
|
const scale = 1 / zoom;
|
||||||
const scale = 1 / zoom; // Inverted for proper zoom behavior
|
|
||||||
iframe.style.width = `${100 * scale}vw`;
|
iframe.style.width = `${100 * scale}vw`;
|
||||||
iframe.style.height = `${100 * scale}vh`;
|
iframe.style.height = `${100 * scale}vh`;
|
||||||
iframe.style.transform = `translate(-50%, -50%) scale(${zoom})`;
|
iframe.style.transform = `translate(-50%, -50%) scale(${zoom})`;
|
||||||
@@ -282,6 +274,7 @@ function playNext() {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Datei aus dem Medienverzeichnis
|
||||||
const src = entry.isPrio ? `/media/priority/${item.name}` : `/media/${site}/${basePath}/${item.name}`;
|
const src = entry.isPrio ? `/media/priority/${item.name}` : `/media/${site}/${basePath}/${item.name}`;
|
||||||
if (isVideo(item)) {
|
if (isVideo(item)) {
|
||||||
vid.style.display = "block";
|
vid.style.display = "block";
|
||||||
@@ -296,22 +289,20 @@ function playNext() {
|
|||||||
iframe.style.transform = `translate(-50%, -50%)`;
|
iframe.style.transform = `translate(-50%, -50%)`;
|
||||||
playerTimer = setTimeout(playNext, interval);
|
playerTimer = setTimeout(playNext, interval);
|
||||||
} else {
|
} else {
|
||||||
|
// Bild (jpg/png)
|
||||||
img.style.display = "block";
|
img.style.display = "block";
|
||||||
img.src = src;
|
img.src = src;
|
||||||
playerTimer = setTimeout(playNext, interval);
|
playerTimer = setTimeout(playNext, interval);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/* -----------------------------
|
// ─── Auto-Reload bei Playlist-Änderungen ───
|
||||||
Auto-Reload bei Änderungen
|
|
||||||
----------------------------- */
|
|
||||||
let lastHash = null;
|
let lastHash = null;
|
||||||
|
|
||||||
async function checkForUpdates() {
|
async function checkForUpdates() {
|
||||||
try {
|
try {
|
||||||
const res = await fetch(`/playlist/${site}/${screen}/hash`, { cache: "no-store" });
|
const res = await fetch(`/playlist/${site}/${screen}/hash`, { cache: "no-store" });
|
||||||
const hash = await res.text();
|
const hash = await res.text();
|
||||||
|
|
||||||
if (lastHash && lastHash !== hash) {
|
if (lastHash && lastHash !== hash) {
|
||||||
location.reload();
|
location.reload();
|
||||||
}
|
}
|
||||||
@@ -321,9 +312,7 @@ async function checkForUpdates() {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/* -----------------------------
|
// ─── Start ───
|
||||||
Start
|
|
||||||
----------------------------- */
|
|
||||||
playNext();
|
playNext();
|
||||||
setInterval(checkForUpdates, 5000);
|
setInterval(checkForUpdates, 5000);
|
||||||
</script>
|
</script>
|
||||||
|
|||||||
Reference in New Issue
Block a user