Version 3 mit URL und Newsticker
Co-authored-by: Copilot <copilot@github.com>
This commit is contained in:
123
app.py
123
app.py
@@ -23,7 +23,7 @@ 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")
|
||||||
CONFIG_FILE = os.path.join(BASE_DIR, "config.json")
|
CONFIG_FILE = os.path.join(BASE_DIR, "config.json")
|
||||||
|
|
||||||
APP_VERSION = "2.1.0"
|
APP_VERSION = "3.1.0"
|
||||||
UPLOAD_EXTENSIONS = {".jpg", ".jpeg", ".png", ".mp4"}
|
UPLOAD_EXTENSIONS = {".jpg", ".jpeg", ".png", ".mp4"}
|
||||||
|
|
||||||
app = Flask(__name__)
|
app = Flask(__name__)
|
||||||
@@ -48,6 +48,14 @@ def save_config(cfg):
|
|||||||
os.fsync(f.fileno())
|
os.fsync(f.fileno())
|
||||||
print("✅ config.json gespeichert")
|
print("✅ config.json gespeichert")
|
||||||
|
|
||||||
|
|
||||||
|
def is_url(value):
|
||||||
|
return (
|
||||||
|
isinstance(value, str)
|
||||||
|
and value.lower().startswith(("http://", "https://"))
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
# -------------------------------------------------
|
# -------------------------------------------------
|
||||||
# Auth / Benutzer
|
# Auth / Benutzer
|
||||||
# -------------------------------------------------
|
# -------------------------------------------------
|
||||||
@@ -64,6 +72,7 @@ def load_user(user_id):
|
|||||||
@app.route("/login", methods=["GET", "POST"])
|
@app.route("/login", methods=["GET", "POST"])
|
||||||
def login():
|
def login():
|
||||||
config = load_config()
|
config = load_config()
|
||||||
|
error = None
|
||||||
|
|
||||||
if request.method == "POST":
|
if request.method == "POST":
|
||||||
if (
|
if (
|
||||||
@@ -72,8 +81,9 @@ def login():
|
|||||||
):
|
):
|
||||||
login_user(Admin())
|
login_user(Admin())
|
||||||
return redirect("/admin")
|
return redirect("/admin")
|
||||||
|
error = "Ungültige Zugangsdaten"
|
||||||
|
|
||||||
return render_template("login.html")
|
return render_template("login.html", error=error)
|
||||||
|
|
||||||
@app.route("/logout")
|
@app.route("/logout")
|
||||||
def logout():
|
def logout():
|
||||||
@@ -124,13 +134,23 @@ def player(screen):
|
|||||||
|
|
||||||
# 1. Playlist-Reihenfolge
|
# 1. Playlist-Reihenfolge
|
||||||
for name in playlist:
|
for name in playlist:
|
||||||
|
if is_url(name):
|
||||||
|
normal_files.append({"kind": "url", "url": name})
|
||||||
|
continue
|
||||||
|
|
||||||
if os.path.exists(os.path.join(folder, name)) and allowed_file(name):
|
if os.path.exists(os.path.join(folder, name)) and allowed_file(name):
|
||||||
normal_files.append(name)
|
normal_files.append({"kind": "file", "name": name})
|
||||||
|
|
||||||
|
existing_file_names = {
|
||||||
|
item["name"] for item in normal_files if item["kind"] == "file"
|
||||||
|
}
|
||||||
|
|
||||||
# 2. Neue Dateien hintendran
|
# 2. Neue Dateien hintendran
|
||||||
for f in sorted(os.listdir(folder)):
|
for f in sorted(os.listdir(folder)):
|
||||||
if f not in normal_files and allowed_file(f):
|
if f in existing_file_names or f.startswith("._"):
|
||||||
normal_files.append(f)
|
continue
|
||||||
|
if allowed_file(f):
|
||||||
|
normal_files.append({"kind": "file", "name": f})
|
||||||
|
|
||||||
# ------------------------------
|
# ------------------------------
|
||||||
# Priority-Playlist (global)
|
# Priority-Playlist (global)
|
||||||
@@ -139,7 +159,15 @@ def player(screen):
|
|||||||
priority_cfg = config.get("priority", {})
|
priority_cfg = config.get("priority", {})
|
||||||
|
|
||||||
if priority_cfg.get("enabled", False):
|
if priority_cfg.get("enabled", False):
|
||||||
prio_files = priority_cfg.get("playlist", [])
|
prio_files = []
|
||||||
|
for name in priority_cfg.get("playlist", []):
|
||||||
|
if is_url(name):
|
||||||
|
prio_files.append({"kind": "url", "url": name})
|
||||||
|
continue
|
||||||
|
|
||||||
|
file_path = os.path.join(MEDIA_DIR, "priority", name)
|
||||||
|
if os.path.exists(file_path):
|
||||||
|
prio_files.append({"kind": "file", "name": name})
|
||||||
else:
|
else:
|
||||||
prio_files = []
|
prio_files = []
|
||||||
|
|
||||||
@@ -148,7 +176,9 @@ def player(screen):
|
|||||||
screen=screen,
|
screen=screen,
|
||||||
normal_files=normal_files, # ✅ explizit
|
normal_files=normal_files, # ✅ explizit
|
||||||
prio_files=prio_files, # ✅ IMMER Liste
|
prio_files=prio_files, # ✅ IMMER Liste
|
||||||
interval=screen_cfg.get("interval", 10)
|
interval=screen_cfg.get("interval", 10),
|
||||||
|
newsticker_text=screen_cfg.get("newsticker_text", ""),
|
||||||
|
newsticker_enabled=screen_cfg.get("newsticker_enabled", False)
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
@@ -193,10 +223,26 @@ def admin():
|
|||||||
continue
|
continue
|
||||||
|
|
||||||
files = []
|
files = []
|
||||||
playlist = cfg["screens"][screen].get("playlist", [])
|
screen_cfg = cfg.setdefault("screens", {}).setdefault(screen, {
|
||||||
|
"interval": 10,
|
||||||
|
"show_images": True,
|
||||||
|
"show_videos": True,
|
||||||
|
"playlist": []
|
||||||
|
})
|
||||||
|
playlist = screen_cfg.get("playlist", [])
|
||||||
|
screen_cfg["newsticker_text"] = screen_cfg.get("newsticker_text", "")
|
||||||
|
screen_cfg["newsticker_enabled"] = screen_cfg.get("newsticker_enabled", False)
|
||||||
|
|
||||||
# 1️⃣ Playlist-Reihenfolge
|
# 1️⃣ Playlist-Reihenfolge
|
||||||
for name in playlist:
|
for name in playlist:
|
||||||
|
if is_url(name):
|
||||||
|
files.append({
|
||||||
|
"name": name,
|
||||||
|
"type": "url",
|
||||||
|
"size": "URL"
|
||||||
|
})
|
||||||
|
continue
|
||||||
|
|
||||||
file_path = os.path.join(path, name)
|
file_path = os.path.join(path, name)
|
||||||
if not os.path.exists(file_path):
|
if not os.path.exists(file_path):
|
||||||
continue
|
continue
|
||||||
@@ -227,7 +273,7 @@ def admin():
|
|||||||
"size": size
|
"size": size
|
||||||
})
|
})
|
||||||
|
|
||||||
screens[screen] = cfg["screens"][screen]
|
screens[screen] = screen_cfg
|
||||||
media_files[screen] = files
|
media_files[screen] = files
|
||||||
screen_status[screen] = "active" if files else "empty"
|
screen_status[screen] = "active" if files else "empty"
|
||||||
|
|
||||||
@@ -241,7 +287,15 @@ def admin():
|
|||||||
|
|
||||||
# 1️⃣ Playlist-Reihenfolge
|
# 1️⃣ Playlist-Reihenfolge
|
||||||
for name in prio_playlist:
|
for name in prio_playlist:
|
||||||
file_path = os.path.join(MEDIA_DIR, "priority")
|
if is_url(name):
|
||||||
|
priority_files.append({
|
||||||
|
"name": name,
|
||||||
|
"type": "url",
|
||||||
|
"size": "URL"
|
||||||
|
})
|
||||||
|
continue
|
||||||
|
|
||||||
|
file_path = os.path.join(prio_dir, name)
|
||||||
if not os.path.exists(file_path):
|
if not os.path.exists(file_path):
|
||||||
continue
|
continue
|
||||||
|
|
||||||
@@ -291,11 +345,18 @@ def admin():
|
|||||||
@login_required
|
@login_required
|
||||||
def update_screen(screen):
|
def update_screen(screen):
|
||||||
config = load_config()
|
config = load_config()
|
||||||
cfg = config["screens"][screen]
|
cfg = config.setdefault("screens", {}).setdefault(screen, {
|
||||||
|
"interval": 10,
|
||||||
|
"show_images": True,
|
||||||
|
"show_videos": True,
|
||||||
|
"playlist": []
|
||||||
|
})
|
||||||
|
|
||||||
cfg["interval"] = int(request.form.get("interval", 10))
|
cfg["interval"] = int(request.form.get("interval", 10))
|
||||||
cfg["show_images"] = "show_images" in request.form
|
cfg["show_images"] = "show_images" in request.form
|
||||||
cfg["show_videos"] = "show_videos" in request.form
|
cfg["show_videos"] = "show_videos" in request.form
|
||||||
|
cfg["newsticker_text"] = request.form.get("newsticker_text", "")
|
||||||
|
cfg["newsticker_enabled"] = "newsticker_enabled" in request.form
|
||||||
|
|
||||||
save_config(config)
|
save_config(config)
|
||||||
return redirect("/admin")
|
return redirect("/admin")
|
||||||
@@ -319,11 +380,38 @@ def upload(screen):
|
|||||||
|
|
||||||
return redirect("/admin")
|
return redirect("/admin")
|
||||||
|
|
||||||
|
|
||||||
|
# -------------------------------------------------
|
||||||
|
# Admin: URL hinzufügen
|
||||||
|
# -------------------------------------------------
|
||||||
|
@app.route("/admin/add-url/<screen>", methods=["POST"])
|
||||||
|
@login_required
|
||||||
|
def add_url(screen):
|
||||||
|
url = request.form.get("url", "").strip()
|
||||||
|
if not is_url(url):
|
||||||
|
return redirect("/admin")
|
||||||
|
|
||||||
|
config = load_config()
|
||||||
|
if screen == "priority":
|
||||||
|
config.setdefault("priority", {}).setdefault("playlist", []).append(url)
|
||||||
|
else:
|
||||||
|
screen_cfg = config.setdefault("screens", {}).setdefault(screen, {
|
||||||
|
"interval": 10,
|
||||||
|
"show_images": True,
|
||||||
|
"show_videos": True,
|
||||||
|
"playlist": []
|
||||||
|
})
|
||||||
|
screen_cfg.setdefault("playlist", []).append(url)
|
||||||
|
|
||||||
|
save_config(config)
|
||||||
|
return redirect("/admin")
|
||||||
|
|
||||||
|
|
||||||
# -------------------------------------------------
|
# -------------------------------------------------
|
||||||
# Admin: Datei löschen
|
# Admin: Datei löschen
|
||||||
# -------------------------------------------------
|
# -------------------------------------------------
|
||||||
|
|
||||||
@app.route("/admin/delete/<screen>/<filename>", methods=["POST"])
|
@app.route("/admin/delete/<screen>/<path:filename>", methods=["POST"])
|
||||||
@login_required
|
@login_required
|
||||||
def delete_file(screen, filename):
|
def delete_file(screen, filename):
|
||||||
cfg = load_config()
|
cfg = load_config()
|
||||||
@@ -358,9 +446,18 @@ def save_playlist(screen):
|
|||||||
data = request.get_json()
|
data = request.get_json()
|
||||||
|
|
||||||
if screen == "priority":
|
if screen == "priority":
|
||||||
|
cfg.setdefault("priority", {}).setdefault("playlist", [])
|
||||||
cfg["priority"]["playlist"] = data["playlist"]
|
cfg["priority"]["playlist"] = data["playlist"]
|
||||||
else:
|
else:
|
||||||
cfg["screens"][screen]["playlist"] = data["playlist"]
|
screen_cfg = cfg.setdefault("screens", {}).setdefault(screen, {
|
||||||
|
"interval": 10,
|
||||||
|
"show_images": True,
|
||||||
|
"show_videos": True,
|
||||||
|
"playlist": [],
|
||||||
|
"newsticker_text": "",
|
||||||
|
"newsticker_enabled": False
|
||||||
|
})
|
||||||
|
screen_cfg["playlist"] = data["playlist"]
|
||||||
|
|
||||||
save_config(cfg)
|
save_config(cfg)
|
||||||
return "", 204
|
return "", 204
|
||||||
|
|||||||
26
config.json
26
config.json
@@ -1,27 +1,33 @@
|
|||||||
{
|
{
|
||||||
"priority": {
|
"priority": {
|
||||||
"enabled": true,
|
"enabled": true,
|
||||||
"playlist": []
|
"playlist": [
|
||||||
|
"https://www.meteoblue.com/en/meteotv/d7b0fd"
|
||||||
|
]
|
||||||
},
|
},
|
||||||
"screens": {
|
"screens": {
|
||||||
"lobby": {
|
"lobby": {
|
||||||
"interval": 10,
|
"interval": 15,
|
||||||
"show_images": true,
|
"show_images": true,
|
||||||
"show_videos": true,
|
"show_videos": false,
|
||||||
"playlist": [
|
"playlist": [
|
||||||
"HilfeKI.jpg",
|
"https://wbxroompresence.cancom.io/standort?find=Stuttgart",
|
||||||
"QRCode.png",
|
"Hilfe_KI.jpg",
|
||||||
"Will_AI_Replace_Developers_1.jpg",
|
"e00a687f-d82a-446d-b8f3-07895fbc7309.png",
|
||||||
"Video CANCOM Jahresr\u00fcckblick 2025.MP4"
|
"Video_CANCOM_LIVE_2025_Stuttgart.MP4"
|
||||||
]
|
],
|
||||||
|
"newsticker_text": "Hallo dies ist der Newsticker",
|
||||||
|
"newsticker_enabled": false
|
||||||
},
|
},
|
||||||
"casino": {
|
"casino": {
|
||||||
"interval": 10,
|
"interval": 10,
|
||||||
"show_images": true,
|
"show_images": true,
|
||||||
"show_videos": true,
|
"show_videos": true,
|
||||||
"playlist": [
|
"playlist": [
|
||||||
"JET-Design.JPG"
|
"https://wbxroompresence.cancom.io/standort?find=Stuttgart"
|
||||||
]
|
],
|
||||||
|
"newsticker_text": "Hallo dies ist der Newsticker",
|
||||||
|
"newsticker_enabled": false
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"admin": {
|
"admin": {
|
||||||
|
|||||||
@@ -38,6 +38,7 @@
|
|||||||
background-color: #eeeeee;
|
background-color: #eeeeee;
|
||||||
border-left: 6px solid #DA002D;
|
border-left: 6px solid #DA002D;
|
||||||
padding-left: 1rem;
|
padding-left: 1rem;
|
||||||
|
font-size: 1.1rem; /* leicht größer */
|
||||||
font-weight: 600;
|
font-weight: 600;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -93,6 +94,22 @@
|
|||||||
.drag-handle:active {
|
.drag-handle:active {
|
||||||
cursor: grabbing;
|
cursor: grabbing;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/* CANCOM CI Tooltip Styling */
|
||||||
|
.tooltip {
|
||||||
|
--bs-tooltip-border-color: #DA002D;
|
||||||
|
}
|
||||||
|
.tooltip .tooltip-inner {
|
||||||
|
border: 1px solid #DA002D;
|
||||||
|
background-color: #ffffff;
|
||||||
|
color: #212121;
|
||||||
|
box-shadow: none;
|
||||||
|
}
|
||||||
|
.tooltip .tooltip-arrow::before {
|
||||||
|
border-top-color: #DA002D;
|
||||||
|
border-bottom-color: #DA002D;
|
||||||
|
background-color: #ffffff;
|
||||||
|
}
|
||||||
</style>
|
</style>
|
||||||
</head>
|
</head>
|
||||||
|
|
||||||
@@ -116,7 +133,7 @@
|
|||||||
Admin Dashboard
|
Admin Dashboard
|
||||||
<h2 class="page-title text-white">Simple Signage</h2>
|
<h2 class="page-title text-white">Simple Signage</h2>
|
||||||
</span>
|
</span>
|
||||||
</a>s
|
</a>
|
||||||
|
|
||||||
<div class="d-none d-sm-inline">
|
<div class="d-none d-sm-inline">
|
||||||
<a href="/logout" class="btn">Logout</a>
|
<a href="/logout" class="btn">Logout</a>
|
||||||
@@ -139,7 +156,7 @@
|
|||||||
⚠ PRIORITY (global)
|
⚠ PRIORITY (global)
|
||||||
</strong>
|
</strong>
|
||||||
</div>
|
</div>
|
||||||
<span class="badge bg-success">wirkt auf alle Player</span>
|
<span class="badge bg-green text-green-fg">wirkt auf alle Player</span>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="card-body">
|
<div class="card-body">
|
||||||
@@ -161,6 +178,14 @@
|
|||||||
</div>
|
</div>
|
||||||
</form>
|
</form>
|
||||||
|
|
||||||
|
<form action="/admin/add-url/priority" method="post" class="mb-3">
|
||||||
|
<div class="input-group">
|
||||||
|
<input type="url" name="url" class="form-control"
|
||||||
|
placeholder="https://example.com" required>
|
||||||
|
<button class="btn btn-secondary" type="submit">URL hinzufügen</button>
|
||||||
|
</div>
|
||||||
|
</form>
|
||||||
|
|
||||||
<!-- Playlist -->
|
<!-- Playlist -->
|
||||||
<h4>Playlist Reihenfolge</h4>
|
<h4>Playlist Reihenfolge</h4>
|
||||||
|
|
||||||
@@ -169,21 +194,37 @@
|
|||||||
<li class="list-group-item playlist-row"
|
<li class="list-group-item playlist-row"
|
||||||
data-file="{{ file.name }}">
|
data-file="{{ file.name }}">
|
||||||
|
|
||||||
<span class="playlist-name">{{ file.name }}</span>
|
<span class="playlist-name"
|
||||||
|
{% if file.type == "image" %}
|
||||||
|
data-bs-toggle="tooltip"
|
||||||
|
data-bs-html="true"
|
||||||
|
title="<img src='/media/priority/{{ file.name }}' style='max-width: 200px; max-height: 150px; border: none; background: white; padding: 2px;' alt='Vorschau'>"
|
||||||
|
{% elif file.type == "url" %}
|
||||||
|
data-bs-toggle="tooltip"
|
||||||
|
title="URL: {{ file.name }}"
|
||||||
|
{% endif %}>
|
||||||
|
{{ file.name }}
|
||||||
|
</span>
|
||||||
|
|
||||||
{% if file.type == "image" %}
|
{% if file.type == "image" %}
|
||||||
<span class="badge bg-blue-lt">Bild</span>
|
<span class="badge bg-blue-lt">Bild</span>
|
||||||
|
{% elif file.type == "url" %}
|
||||||
|
<span class="badge bg-info">URL</span>
|
||||||
{% else %}
|
{% else %}
|
||||||
<span class="badge bg-orange-lt">Video</span>
|
<span class="badge bg-orange-lt">Video</span>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
|
|
||||||
<span class="playlist-size">{{ file.size }} KB</span>
|
<span class="playlist-size">
|
||||||
|
{% if file.type == "url" %}
|
||||||
|
{{ file.size }}
|
||||||
|
{% else %}
|
||||||
|
{{ file.size }} KB
|
||||||
|
{% endif %}
|
||||||
|
</span>
|
||||||
|
|
||||||
<!-- Delete -->
|
<!-- Delete -->
|
||||||
|
|
||||||
<form action="{{url_for('delete_file',
|
<form action="{{ url_for('delete_file', screen='priority', filename=file.name) }}"
|
||||||
screen='priority',
|
|
||||||
filename=file.name) }}"
|
|
||||||
method="post"
|
method="post"
|
||||||
class="d-inline"
|
class="d-inline"
|
||||||
onmousedown="event.stopPropagation()">
|
onmousedown="event.stopPropagation()">
|
||||||
@@ -215,9 +256,9 @@
|
|||||||
|
|
||||||
<!-- Card Header -->
|
<!-- Card Header -->
|
||||||
<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>Screen: {{ screen }}</span>
|
<span>Screen: https://signage.ccmake.de/player/{{ screen }}</span>
|
||||||
{% if screen_status[screen] == "active" %}
|
{% if screen_status[screen] == "active" %}
|
||||||
<span class="badge bg-success">Aktiv</span>
|
<span class="badge bg-green text-green-fg">Aktiv</span>
|
||||||
{% else %}
|
{% else %}
|
||||||
<span class="badge bg-warning text-dark">Leer</span>
|
<span class="badge bg-warning text-dark">Leer</span>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
@@ -226,13 +267,26 @@
|
|||||||
<div class="card-body">
|
<div class="card-body">
|
||||||
|
|
||||||
<!-- Einstellungen -->
|
<!-- Einstellungen -->
|
||||||
|
<h2>Einstellungen</h2>
|
||||||
<form action="/admin/update/{{ screen }}" method="post">
|
<form action="/admin/update/{{ screen }}" method="post">
|
||||||
<div class="mb-3">
|
<div class="mb-2">
|
||||||
<label class="form-label">Intervall (Sekunden)</label>
|
<label class="form-label">Intervall (Sekunden)</label>
|
||||||
<input type="number" class="form-control"
|
<input type="number" class="form-control"
|
||||||
name="interval" min="1" value="{{ cfg.interval }}">
|
name="interval" min="1" value="{{ cfg.interval }}">
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<div class="mb-4">
|
||||||
|
<label class="form-label">Newsticker-Text</label>
|
||||||
|
<input type="text" class="form-control" name="newsticker_text" value="{{ cfg.newsticker_text|default('') }}" maxlength="200">
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="form-check form-switch mb-2">
|
||||||
|
<input class="form-check-input" type="checkbox"
|
||||||
|
name="newsticker_enabled"
|
||||||
|
{% if cfg.newsticker_enabled %}checked{% endif %}>
|
||||||
|
<label class="form-check-label">Newsticker anzeigen</label>
|
||||||
|
</div>
|
||||||
|
|
||||||
<div class="form-check form-switch mb-2">
|
<div class="form-check form-switch mb-2">
|
||||||
<input class="form-check-input" type="checkbox"
|
<input class="form-check-input" type="checkbox"
|
||||||
name="show_images"
|
name="show_images"
|
||||||
@@ -240,7 +294,7 @@
|
|||||||
<label class="form-check-label">Bilder anzeigen</label>
|
<label class="form-check-label">Bilder anzeigen</label>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="form-check form-switch mb-3">
|
<div class="form-check form-switch mb-4">
|
||||||
<input class="form-check-input" type="checkbox"
|
<input class="form-check-input" type="checkbox"
|
||||||
name="show_videos"
|
name="show_videos"
|
||||||
{% if cfg.show_videos %}checked{% endif %}>
|
{% if cfg.show_videos %}checked{% endif %}>
|
||||||
@@ -255,7 +309,7 @@
|
|||||||
<hr>
|
<hr>
|
||||||
|
|
||||||
<!-- Upload -->
|
<!-- Upload -->
|
||||||
<h4>Medien hochladen</h4>
|
<h2>Medien hochladen / URL hinzufügen</h2>
|
||||||
<form action="/admin/upload/{{ screen }}"
|
<form action="/admin/upload/{{ screen }}"
|
||||||
method="post" enctype="multipart/form-data">
|
method="post" enctype="multipart/form-data">
|
||||||
<div class="input-group mb-3">
|
<div class="input-group mb-3">
|
||||||
@@ -264,26 +318,54 @@
|
|||||||
</div>
|
</div>
|
||||||
</form>
|
</form>
|
||||||
|
|
||||||
|
<form action="/admin/add-url/{{ screen }}" method="post" class="mb-4">
|
||||||
|
<div class="input-group">
|
||||||
|
<input type="url" name="url" class="form-control"
|
||||||
|
placeholder="https://example.com" required>
|
||||||
|
<button class="btn btn-secondary" type="submit">URL hinzufügen</button>
|
||||||
|
</div>
|
||||||
|
</form>
|
||||||
|
|
||||||
|
<hr>
|
||||||
|
|
||||||
<!-- Playlist -->
|
<!-- Playlist -->
|
||||||
<h4>Playlist Reihenfolge</h4>
|
<h2>Playlist Reihenfolge</h2>
|
||||||
|
|
||||||
<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 }}">
|
||||||
|
|
||||||
<span class="playlist-name">{{ file.name }}</span>
|
<span class="playlist-name"
|
||||||
|
{% if file.type == "image" %}
|
||||||
|
data-bs-toggle="tooltip"
|
||||||
|
data-bs-html="true"
|
||||||
|
title="<img src='/media/{{ screen }}/{{ file.name }}' style='max-width: 200px; max-height: 150px; border: none; background: white; padding: 2px;' alt='Vorschau'>"
|
||||||
|
{% elif file.type == "url" %}
|
||||||
|
data-bs-toggle="tooltip"
|
||||||
|
title="URL: {{ file.name }}"
|
||||||
|
{% endif %}>
|
||||||
|
{{ file.name }}
|
||||||
|
</span>
|
||||||
|
|
||||||
{% if file.type == "image" %}
|
{% if file.type == "image" %}
|
||||||
<span class="badge bg-blue-lt">Bild</span>
|
<span class="badge bg-purple text-purple-fg">Bild</span>
|
||||||
|
{% elif file.type == "url" %}
|
||||||
|
<span class="badge bg-blue text-blue-fg">URL</span>
|
||||||
{% else %}
|
{% else %}
|
||||||
<span class="badge bg-orange-lt">Video</span>
|
<span class="badge bg-orange text-orange-fg">Video</span>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
|
|
||||||
<span class="playlist-size">{{ file.size }} KB</span>
|
<span class="playlist-size">
|
||||||
|
{% if file.type == "url" %}
|
||||||
|
{{ file.size }}
|
||||||
|
{% else %}
|
||||||
|
{{ file.size }} KB
|
||||||
|
{% endif %}
|
||||||
|
</span>
|
||||||
|
|
||||||
<!-- Delete -->
|
<!-- Delete -->
|
||||||
<form action="/admin/delete/{{ screen }}/{{ file.name }}"
|
<form action="{{ url_for('delete_file', screen=screen, filename=file.name) }}"
|
||||||
method="post">
|
method="post">
|
||||||
<button type="submit"
|
<button type="submit"
|
||||||
class="btn btn-outline-danger btn-sm"
|
class="btn btn-outline-danger btn-sm"
|
||||||
@@ -360,9 +442,11 @@ document.addEventListener("DOMContentLoaded", function () {
|
|||||||
|
|
||||||
window.savePlaylist = savePlaylist;
|
window.savePlaylist = savePlaylist;
|
||||||
|
|
||||||
{% for screen in screens.keys() %}
|
// Initialize tooltips
|
||||||
initSortable("{{ screen }}");
|
var tooltipTriggerList = [].slice.call(document.querySelectorAll('[data-bs-toggle="tooltip"]'));
|
||||||
{% endfor %}
|
var tooltipList = tooltipTriggerList.map(function (tooltipTriggerEl) {
|
||||||
|
return new bootstrap.Tooltip(tooltipTriggerEl);
|
||||||
|
});
|
||||||
|
|
||||||
});
|
});
|
||||||
</script>
|
</script>
|
||||||
|
|||||||
@@ -2,14 +2,36 @@
|
|||||||
<html lang="de">
|
<html lang="de">
|
||||||
<head>
|
<head>
|
||||||
<meta charset="utf-8">
|
<meta charset="utf-8">
|
||||||
<title>Signage Login</title>
|
<title>CANCOM Simple Signage Admin</title>
|
||||||
<link href="https://unpkg.com/@tabler/core@latest/dist/css/tabler.min.css" rel="stylesheet">
|
<link rel="icon" href="{{ url_for('static', filename='favicon.ico') }}">
|
||||||
|
<link href="https://unpkg.com/@tabler/core@1.0.0-beta20/dist/css/tabler.min.css" rel="stylesheet">
|
||||||
|
|
||||||
|
<style>
|
||||||
|
body {
|
||||||
|
background-color: #F4F6F8;
|
||||||
|
color: #212121;
|
||||||
|
}
|
||||||
|
|
||||||
|
.btn-primary {
|
||||||
|
background-color: #DA002D;
|
||||||
|
border-color: #DA002D;
|
||||||
|
}
|
||||||
|
.btn-primary:hover {
|
||||||
|
background-color: #B00024;
|
||||||
|
border-color: #B00024;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
</head>
|
</head>
|
||||||
|
|
||||||
<body class="d-flex align-items-center justify-content-center bg-dark" 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 %}
|
||||||
|
<div class="alert alert-danger text-center" role="alert">
|
||||||
|
{{ error }}
|
||||||
|
</div>
|
||||||
|
{% 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>
|
||||||
|
|||||||
@@ -7,6 +7,7 @@
|
|||||||
|
|
||||||
<style>
|
<style>
|
||||||
html, body {
|
html, body {
|
||||||
|
font-family: system-ui, sans-serif;
|
||||||
margin: 0;
|
margin: 0;
|
||||||
padding: 0;
|
padding: 0;
|
||||||
width: 100%;
|
width: 100%;
|
||||||
@@ -15,22 +16,86 @@
|
|||||||
overflow: hidden;
|
overflow: hidden;
|
||||||
}
|
}
|
||||||
|
|
||||||
img, video {
|
img, video, iframe {
|
||||||
width: 100vw;
|
width: 100vw;
|
||||||
height: 100vh;
|
height: 100vh;
|
||||||
object-fit: contain;
|
|
||||||
background: black;
|
background: black;
|
||||||
display: none;
|
display: none;
|
||||||
|
position: fixed;
|
||||||
|
top: 0;
|
||||||
|
left: 0;
|
||||||
|
z-index: 1;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
img, iframe {
|
||||||
|
object-fit: contain;
|
||||||
|
border: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
video {
|
||||||
|
object-fit: contain;
|
||||||
|
}
|
||||||
|
|
||||||
|
.newsticker-text {
|
||||||
|
flex: 1;
|
||||||
|
overflow: hidden;
|
||||||
|
position: relative;
|
||||||
|
height: 100%;
|
||||||
|
}
|
||||||
|
|
||||||
|
.newsticker-track {
|
||||||
|
display: inline-block;
|
||||||
|
white-space: nowrap;
|
||||||
|
|
||||||
|
position: absolute;
|
||||||
|
left: 100%;
|
||||||
|
top: 0;
|
||||||
|
|
||||||
|
height: 40px;
|
||||||
|
line-height: 40px;
|
||||||
|
animation: ticker-scroll 40s linear infinite;
|
||||||
|
}
|
||||||
|
|
||||||
|
.newsticker-track span {
|
||||||
|
padding-right: 4rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
@keyframes ticker-scroll {
|
||||||
|
from { transform: translateX(0); }
|
||||||
|
to { transform: translateX(-100vw); }
|
||||||
|
}
|
||||||
|
|
||||||
</style>
|
</style>
|
||||||
</head>
|
</head>
|
||||||
|
|
||||||
<body>
|
<body>
|
||||||
|
|
||||||
|
|
||||||
<img id="image">
|
<img id="image">
|
||||||
<video id="video" muted autoplay playsinline></video>
|
<video id="video" muted autoplay playsinline></video>
|
||||||
|
<iframe id="iframe"></iframe>
|
||||||
|
|
||||||
|
{% 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-text" class="newsticker-text">
|
||||||
|
<div class="newsticker-track">
|
||||||
|
<span>{{ newsticker_text }}</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div id="newsticker-clock" style="padding:0 18px 0 24px;font-size:1.1em;font-family:monospace;min-width:90px;text-align:right;"></div>
|
||||||
|
</div>
|
||||||
|
{% endif %}
|
||||||
|
|
||||||
<script>
|
<script>
|
||||||
|
// Newsticker Uhrzeit
|
||||||
|
function updateClock() {
|
||||||
|
const el = document.getElementById('newsticker-clock');
|
||||||
|
if (!el) return;
|
||||||
|
const now = new Date();
|
||||||
|
el.textContent = now.toLocaleTimeString('de-DE', { hour: '2-digit', minute: '2-digit', second: '2-digit' });
|
||||||
|
}
|
||||||
|
setInterval(updateClock, 1000);
|
||||||
|
updateClock();
|
||||||
/* -----------------------------
|
/* -----------------------------
|
||||||
Daten vom Server
|
Daten vom Server
|
||||||
----------------------------- */
|
----------------------------- */
|
||||||
@@ -44,85 +109,110 @@ const screen = "{{ screen }}";
|
|||||||
----------------------------- */
|
----------------------------- */
|
||||||
let normalIndex = 0;
|
let normalIndex = 0;
|
||||||
let prioIndex = 0;
|
let prioIndex = 0;
|
||||||
let playPrioNext = false;
|
|
||||||
let mode = "normal"; // "normal" oder "prio"
|
let mode = "normal"; // "normal" oder "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");
|
||||||
|
|
||||||
/* -----------------------------
|
/* -----------------------------
|
||||||
Hilfsfunktionen
|
Hilfsfunktionen
|
||||||
----------------------------- */
|
----------------------------- */
|
||||||
function isVideo(file) {
|
function normalizeItem(item) {
|
||||||
return file.toLowerCase().endsWith(".mp4");
|
if (typeof item === "string") {
|
||||||
|
const lower = item.toLowerCase();
|
||||||
|
if (lower.startsWith("http://") || lower.startsWith("https://")) {
|
||||||
|
return { kind: "url", url: item };
|
||||||
|
}
|
||||||
|
return { kind: "file", name: item };
|
||||||
|
}
|
||||||
|
return item;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function isVideo(item) {
|
||||||
|
return item.kind === "file" && item.name.toLowerCase().endsWith(".mp4");
|
||||||
|
}
|
||||||
|
|
||||||
|
function isImage(item) {
|
||||||
|
return item.kind === "file" && /\.(jpg|jpeg|png)$/i.test(item.name);
|
||||||
|
}
|
||||||
|
|
||||||
function getNextItem() {
|
function getNextItem() {
|
||||||
|
const normalizedNormal = normalFiles.map(normalizeItem);
|
||||||
|
const normalizedPrio = prioFiles.map(normalizeItem);
|
||||||
|
|
||||||
// ✅ Sonderfall: nur Priority vorhanden
|
// ✅ Sonderfall: nur Priority vorhanden
|
||||||
if (normalFiles.length === 0 && prioFiles.length > 0) {
|
if (normalizedNormal.length === 0 && normalizedPrio.length > 0) {
|
||||||
return {
|
return {
|
||||||
file: prioFiles[prioIndex++ % prioFiles.length],
|
item: normalizedPrio[prioIndex++ % normalizedPrio.length],
|
||||||
isPrio: true
|
isPrio: true
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
// ✅ Sonderfall: nur normale Playlist vorhanden
|
// ✅ Sonderfall: nur normale Playlist vorhanden
|
||||||
if (prioFiles.length === 0 && normalFiles.length > 0) {
|
if (normalizedPrio.length === 0 && normalizedNormal.length > 0) {
|
||||||
return {
|
return {
|
||||||
file: normalFiles[normalIndex++ % normalFiles.length],
|
item: normalizedNormal[normalIndex++ % normalizedNormal.length],
|
||||||
isPrio: false
|
isPrio: false
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
// ✅ Normal-Phase
|
// ✅ Normal-Phase
|
||||||
if (mode === "normal") {
|
if (mode === "normal") {
|
||||||
const file = normalFiles[normalIndex++];
|
const item = normalizedNormal[normalIndex++];
|
||||||
if (normalIndex >= normalFiles.length) {
|
if (normalIndex >= normalizedNormal.length) {
|
||||||
normalIndex = 0;
|
normalIndex = 0;
|
||||||
if (prioFiles.length > 0) {
|
if (normalizedPrio.length > 0) {
|
||||||
mode = "prio"; // ➜ nach kompletter Normal-Playlist wechseln
|
mode = "prio"; // ➜ nach kompletter Normal-Playlist wechseln
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
return { file, isPrio: false };
|
return { item, isPrio: false };
|
||||||
}
|
}
|
||||||
|
|
||||||
// ✅ Priority-Phase
|
// ✅ Priority-Phase
|
||||||
if (mode === "prio") {
|
if (mode === "prio") {
|
||||||
const file = prioFiles[prioIndex++];
|
const item = normalizedPrio[prioIndex++];
|
||||||
if (prioIndex >= prioFiles.length) {
|
if (prioIndex >= normalizedPrio.length) {
|
||||||
prioIndex = 0;
|
prioIndex = 0;
|
||||||
mode = "normal"; // ➜ nach kompletter Priority zurück
|
mode = "normal"; // ➜ nach kompletter Priority zurück
|
||||||
}
|
}
|
||||||
return { file, isPrio: true };
|
return { item, isPrio: true };
|
||||||
}
|
}
|
||||||
|
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
/* -----------------------------
|
/* -----------------------------
|
||||||
Medien abspielen
|
Medien abspielen
|
||||||
----------------------------- */
|
----------------------------- */
|
||||||
function playNext() {
|
function playNext() {
|
||||||
const item = getNextItem();
|
const entry = getNextItem();
|
||||||
if (!item) return;
|
if (!entry) return;
|
||||||
|
|
||||||
const basePath = item.isPrio ? "priority" : screen;
|
const item = entry.item;
|
||||||
const src = `/media/${basePath}/${item.file}`;
|
const basePath = entry.isPrio ? "priority" : screen;
|
||||||
|
|
||||||
if (isVideo(item.file)) {
|
img.style.display = "none";
|
||||||
img.style.display = "none";
|
vid.style.display = "none";
|
||||||
|
iframe.style.display = "none";
|
||||||
|
vid.pause();
|
||||||
|
vid.src = "";
|
||||||
|
iframe.src = "";
|
||||||
|
|
||||||
|
if (item.kind === "url") {
|
||||||
|
iframe.style.display = "block";
|
||||||
|
iframe.src = item.url;
|
||||||
|
setTimeout(playNext, interval);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const src = `/media/${basePath}/${item.name}`;
|
||||||
|
if (isVideo(item)) {
|
||||||
vid.style.display = "block";
|
vid.style.display = "block";
|
||||||
vid.src = src;
|
vid.src = src;
|
||||||
vid.onended = playNext;
|
vid.onended = playNext;
|
||||||
vid.play();
|
vid.play();
|
||||||
} else {
|
} else {
|
||||||
vid.pause();
|
|
||||||
vid.style.display = "none";
|
|
||||||
img.style.display = "block";
|
img.style.display = "block";
|
||||||
img.src = src;
|
img.src = src;
|
||||||
setTimeout(playNext, interval);
|
setTimeout(playNext, interval);
|
||||||
|
|||||||
Reference in New Issue
Block a user