Hilfetexte ergänzt
This commit is contained in:
@@ -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">
|
||||
<div class="app-footer-inner">
|
||||
<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">
|
||||
<div class="container-xl">
|
||||
<!-- Logo + Titel -->
|
||||
<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">
|
||||
<span class="navbar-brand-wordmark">
|
||||
@@ -8,7 +13,10 @@
|
||||
<span>{{ brand_subtitle }}</span>
|
||||
</span>
|
||||
</div>
|
||||
|
||||
<!-- Aktionen: Standort-Dropdown, +Button, Theme, Logout -->
|
||||
<div class="navbar-nav flex-row order-md-last top-actions">
|
||||
<!-- Standort-Auswahl -->
|
||||
<div class="dropdown me-2">
|
||||
<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>
|
||||
@@ -20,15 +28,21 @@
|
||||
{% endfor %}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Neuen Standort anlegen (+ Button) -->
|
||||
<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());"
|
||||
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>
|
||||
|
||||
<!-- Dark-Mode-Toggle (persistiert in localStorage("signage-theme")) -->
|
||||
<div>
|
||||
<button class="btn btn-outline-secondary theme-toggle" id="theme-toggle" title="Dark Mode umschalten">
|
||||
<i class="ti ti-moon"></i>
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<!-- Logout -->
|
||||
<div class="ms-2">
|
||||
<a href="/logout" class="btn btn-outline-secondary"><i class="ti ti-logout me-1"></i>Abmelden</a>
|
||||
</div>
|
||||
|
||||
@@ -5,6 +5,7 @@
|
||||
<title>CANCOM Simple Signage Admin</title>
|
||||
<link rel="icon" href="{{ url_for('static', filename='favicon.ico') }}">
|
||||
|
||||
<!-- ─── Dark Mode: Theme vor dem Rendern setzen ─── -->
|
||||
<script>
|
||||
(() => {
|
||||
const stored = window.localStorage.getItem("signage-theme");
|
||||
@@ -13,6 +14,7 @@
|
||||
})();
|
||||
</script>
|
||||
|
||||
<!-- ─── Externe Bibliotheken (Tabler CSS/JS, SortableJS) ─── -->
|
||||
<link rel="stylesheet"
|
||||
href="https://unpkg.com/@tabler/core@1.0.0-beta20/dist/css/tabler.min.css">
|
||||
<link rel="stylesheet"
|
||||
@@ -25,7 +27,7 @@
|
||||
{% include "_styles.html" %}
|
||||
|
||||
<style>
|
||||
/* Tooltip Styling */
|
||||
/* ─── Tooltip-Styling (CANCOM-rot) ─── */
|
||||
.tooltip {
|
||||
--bs-tooltip-border-color: var(--ccm-primary);
|
||||
}
|
||||
@@ -41,7 +43,7 @@
|
||||
background-color: #ffffff;
|
||||
}
|
||||
|
||||
/* Playlist als Grid */
|
||||
/* ─── Playlist-Zeile als Grid (Checkbox | Name | Typ | Größe | Löschen | Drag) ─── */
|
||||
.playlist-row {
|
||||
display: grid;
|
||||
grid-template-columns:
|
||||
@@ -62,6 +64,7 @@
|
||||
font-size: 0.9em;
|
||||
}
|
||||
|
||||
/* ─── Player-URL im Screen-Header als Link (grau, rot bei Hover) ─── */
|
||||
.page-subtitle a {
|
||||
color: inherit;
|
||||
text-decoration: none;
|
||||
@@ -70,6 +73,7 @@
|
||||
color: var(--ccm-primary);
|
||||
}
|
||||
|
||||
/* ─── Drag-Handle für SortableJS ─── */
|
||||
.drag-handle {
|
||||
cursor: grab;
|
||||
font-size: 1.2em;
|
||||
@@ -80,6 +84,7 @@
|
||||
cursor: grabbing;
|
||||
}
|
||||
|
||||
/* ─── Tab-Styling (extend to card edges, unified background) ─── */
|
||||
.card-body .nav-tabs {
|
||||
margin-left: -1rem;
|
||||
margin-right: -1rem;
|
||||
@@ -108,7 +113,10 @@
|
||||
{% set site_dropdown_url_prefix = "/admin/" %}
|
||||
{% include "_header.html" %}
|
||||
|
||||
<!-- Second Nav -->
|
||||
<!-- ═══════════════════════════════════════════════════
|
||||
Navigation (zweite Zeile)
|
||||
Übersicht | Priority | Willkommensseite | Screens-Dropdown | +
|
||||
═══════════════════════════════════════════════════ -->
|
||||
<div class="navbar-expand-md">
|
||||
<div class="collapse navbar-collapse show">
|
||||
<div class="navbar navbar-dark nav-surface">
|
||||
@@ -127,6 +135,7 @@
|
||||
{% endfor %}
|
||||
</div>
|
||||
</div>
|
||||
<!-- + Button: Neuen Screen anlegen (prompt → /admin/<site>/add-screen?name=...) -->
|
||||
<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());"
|
||||
title="Neuen Screen anlegen"><i class="ti ti-plus"></i></a>
|
||||
@@ -136,13 +145,13 @@
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Content -->
|
||||
<!-- ═══════════════════════════════════════════════════
|
||||
CONTENT
|
||||
═══════════════════════════════════════════════════ -->
|
||||
<div class="page-wrapper">
|
||||
<div class="container-xl mt-4">
|
||||
|
||||
<!-- ========================= -->
|
||||
<!-- STANDORT VERWALTUNG -->
|
||||
<!-- ========================= -->
|
||||
<!-- ─── Standort löschen (Button oben rechts) ─── -->
|
||||
<div class="d-flex justify-content-end mb-3">
|
||||
<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.')">
|
||||
@@ -151,13 +160,13 @@
|
||||
</div>
|
||||
|
||||
<!-- ========================= -->
|
||||
<!-- SCREENS -->
|
||||
<!-- SCREEN-CARDS -->
|
||||
<!-- ========================= -->
|
||||
|
||||
{% for screen, cfg in screens.items() %}
|
||||
<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">
|
||||
<span>
|
||||
<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>
|
||||
|
||||
<!-- Card-Body: Tabs (Playlist | Einstellungen | Medien) -->
|
||||
<div class="card-body">
|
||||
|
||||
<ul class="nav nav-tabs" data-bs-toggle="tabs" role="tablist">
|
||||
@@ -199,18 +209,20 @@
|
||||
|
||||
<div class="tab-content mt-3">
|
||||
|
||||
<!-- Tab: Playlist -->
|
||||
<!-- ═══ Tab: Playlist ═══ -->
|
||||
<div class="tab-pane active" id="tab-playlist-{{ screen }}" role="tabpanel">
|
||||
<ul class="list-group mb-3" id="playlist-{{ screen }}">
|
||||
{% for file in media_files[screen] %}
|
||||
<li class="list-group-item playlist-row"
|
||||
data-file="{{ file.name }}">
|
||||
|
||||
<!-- Aktiv-Checkbox -->
|
||||
<input class="form-check-input playlist-enabled"
|
||||
type="checkbox"
|
||||
{% if file.enabled %}checked{% endif %}
|
||||
title="Element aktivieren">
|
||||
|
||||
<!-- Dateiname (mit Tooltip-Vorschau) -->
|
||||
<span class="playlist-name"
|
||||
{% if file.type == "image" %}
|
||||
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 %}
|
||||
</span>
|
||||
|
||||
<!-- Typ-Badge -->
|
||||
{% if file.type == "image" %}
|
||||
<span class="badge bg-purple text-purple-fg">Bild</span>
|
||||
{% elif file.type == "url" or file.type == "html" %}
|
||||
@@ -231,6 +244,7 @@
|
||||
<span class="badge bg-orange text-orange-fg">Video</span>
|
||||
{% endif %}
|
||||
|
||||
<!-- Größe -->
|
||||
<span class="playlist-size">
|
||||
{% if file.type == "url" %}
|
||||
{{ file.size }}
|
||||
@@ -239,7 +253,7 @@
|
||||
{% endif %}
|
||||
</span>
|
||||
|
||||
<!-- Delete -->
|
||||
<!-- Löschen -->
|
||||
<form action="{{ url_for('delete_file', site=current_site, screen=screen, filename=file.name) }}"
|
||||
method="post">
|
||||
<button type="submit"
|
||||
@@ -249,7 +263,7 @@
|
||||
</button>
|
||||
</form>
|
||||
|
||||
<!-- Drag -->
|
||||
<!-- Drag-Handle (SortableJS) -->
|
||||
<span class="drag-handle" title="Reihenfolge ändern">⇅</span>
|
||||
</li>
|
||||
{% endfor %}
|
||||
@@ -261,7 +275,7 @@
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<!-- Tab: Einstellungen -->
|
||||
<!-- ═══ Tab: Einstellungen ═══ -->
|
||||
<div class="tab-pane" id="settings-{{ screen }}" role="tabpanel">
|
||||
<form action="/admin/{{ current_site }}/update/{{ screen }}" method="post">
|
||||
<div class="mb-2">
|
||||
@@ -328,7 +342,7 @@
|
||||
</form>
|
||||
</div>
|
||||
|
||||
<!-- Tab: Medien -->
|
||||
<!-- ═══ Tab: Medien ═══ -->
|
||||
<div class="tab-pane" id="media-{{ screen }}" role="tabpanel">
|
||||
<h3>Medien hochladen</h3>
|
||||
<form action="/admin/{{ current_site }}/upload/{{ screen }}"
|
||||
@@ -355,23 +369,24 @@
|
||||
</form>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
</div> <!-- /tab-content -->
|
||||
|
||||
</div>
|
||||
</div>
|
||||
</div> <!-- /card-body -->
|
||||
</div> <!-- /card -->
|
||||
{% endfor %}
|
||||
|
||||
</div>
|
||||
</div>
|
||||
</div> <!-- /container -->
|
||||
</div> <!-- /page-wrapper -->
|
||||
|
||||
<!-- Footer -->
|
||||
{% include "_footer.html" %}
|
||||
|
||||
</div>
|
||||
</div> <!-- /page -->
|
||||
|
||||
<script>
|
||||
document.addEventListener("DOMContentLoaded", function () {
|
||||
|
||||
/* ─── SortableJS Drag & Drop initialisieren ─── */
|
||||
function initSortable(name) {
|
||||
const el = document.getElementById("playlist-" + name);
|
||||
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() %}
|
||||
initSortable("{{ screen }}");
|
||||
{% endfor %}
|
||||
|
||||
/* ─── Playlist-Reihenfolge speichern ───
|
||||
Sendet die aktuelle Sortierung als JSON an /admin/<site>/playlist/<screen> */
|
||||
function savePlaylist(site, screen) {
|
||||
const items = document.querySelectorAll(
|
||||
"#playlist-" + screen + " li"
|
||||
@@ -415,13 +432,13 @@ document.addEventListener("DOMContentLoaded", function () {
|
||||
|
||||
window.savePlaylist = savePlaylist;
|
||||
|
||||
// Initialize tooltips
|
||||
/* ─── Bootstrap Tooltips initialisieren ─── */
|
||||
var tooltipTriggerList = [].slice.call(document.querySelectorAll('[data-bs-toggle="tooltip"]'));
|
||||
var tooltipList = tooltipTriggerList.map(function (tooltipTriggerEl) {
|
||||
return new bootstrap.Tooltip(tooltipTriggerEl);
|
||||
});
|
||||
|
||||
// Dark Mode Toggle
|
||||
/* ─── Dark Mode Toggle (persistiert in localStorage) ─── */
|
||||
const themeToggle = document.getElementById("theme-toggle");
|
||||
const root = document.documentElement;
|
||||
|
||||
|
||||
@@ -6,6 +6,7 @@
|
||||
<title>Willkommensseite</title>
|
||||
<link rel="icon" href="{{ url_for('static', filename='favicon.ico') }}">
|
||||
|
||||
<!-- ─── Dark Mode: Theme vor dem Rendern setzen ─── -->
|
||||
<script>
|
||||
(() => {
|
||||
const stored = window.localStorage.getItem("signage-theme");
|
||||
@@ -26,6 +27,7 @@
|
||||
<style>
|
||||
.form-label { font-weight: 600; }
|
||||
.card { box-shadow: 0 20px 60px rgba(0,0,0,0.08); }
|
||||
/* ─── Logo-Vorschau-Box (gestrichelte Border) ─── */
|
||||
.logo-preview-box {
|
||||
border: 2px dashed var(--ccm-border);
|
||||
border-radius: 0.75rem;
|
||||
@@ -61,7 +63,9 @@
|
||||
{% set site_dropdown_url_prefix = "/willkommen?site=" %}
|
||||
{% include "_header.html" %}
|
||||
|
||||
<!-- Second Nav -->
|
||||
<!-- ═══════════════════════════════════════════════════
|
||||
Navigation (zweite Zeile)
|
||||
═══════════════════════════════════════════════════ -->
|
||||
<div class="navbar-expand-md">
|
||||
<div class="collapse navbar-collapse show">
|
||||
<div class="navbar navbar-dark nav-surface">
|
||||
@@ -76,6 +80,11 @@
|
||||
</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="container-xl mt-4">
|
||||
<div class="card mb-4">
|
||||
@@ -95,7 +104,7 @@
|
||||
<div class="alert alert-success">{{ success }}</div>
|
||||
{% endif %}
|
||||
|
||||
<!-- Logo Previews -->
|
||||
<!-- ─── Logo-Vorschau (wird nach POST angezeigt) ─── -->
|
||||
<div class="row g-3 mb-4">
|
||||
{% for i in range(3) %}
|
||||
<div class="col-md-4">
|
||||
@@ -111,6 +120,7 @@
|
||||
{% endfor %}
|
||||
</div>
|
||||
|
||||
<!-- ─── Eingabeformular (bis zu 3 Kunden) ─── -->
|
||||
<form method="POST">
|
||||
<input type="hidden" name="site" value="{{ site }}">
|
||||
<div class="row g-3 mb-3">
|
||||
@@ -155,6 +165,7 @@
|
||||
|
||||
<script>
|
||||
window.addEventListener("load", () => {
|
||||
/* ─── Dark Mode Toggle ─── */
|
||||
const themeToggle = document.getElementById("theme-toggle");
|
||||
const root = document.documentElement;
|
||||
|
||||
|
||||
@@ -11,7 +11,6 @@
|
||||
background-color: #F4F6F8;
|
||||
color: #212121;
|
||||
}
|
||||
|
||||
.btn-primary {
|
||||
background-color: #DA002D;
|
||||
border-color: #DA002D;
|
||||
@@ -23,15 +22,18 @@
|
||||
</style>
|
||||
</head>
|
||||
|
||||
<!-- ─── Login-Seite (zentriert) ─── -->
|
||||
<body class="d-flex align-items-center justify-content-center" style="height:100vh;">
|
||||
<div class="card card-md">
|
||||
<div class="card-body">
|
||||
<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">
|
||||
<div class="mb-3">
|
||||
<label class="form-label">Benutzername</label>
|
||||
@@ -46,4 +48,4 @@
|
||||
</div>
|
||||
</div>
|
||||
</body>
|
||||
</html>
|
||||
</html>
|
||||
|
||||
@@ -6,6 +6,9 @@
|
||||
<link rel="icon" href="{{ url_for('static', filename='favicon.ico') }}">
|
||||
|
||||
<style>
|
||||
/* ─── Player-Grundlayout ───
|
||||
Alle Medien (img/video/iframe) werden zentriert auf Vollbild angezeigt.
|
||||
Standardmäßig ausgeblendet – nur das aktive Element ist sichtbar. */
|
||||
html, body {
|
||||
font-family: system-ui, sans-serif;
|
||||
margin: 0;
|
||||
@@ -37,6 +40,7 @@
|
||||
object-fit: contain;
|
||||
}
|
||||
|
||||
/* ─── Newsticker (Laufband) ─── */
|
||||
.newsticker-text {
|
||||
flex: 1;
|
||||
overflow: hidden;
|
||||
@@ -47,11 +51,9 @@
|
||||
.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;
|
||||
@@ -65,12 +67,16 @@
|
||||
from { transform: translateX(0); }
|
||||
to { transform: translateX(-100vw); }
|
||||
}
|
||||
|
||||
</style>
|
||||
</head>
|
||||
|
||||
<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;">
|
||||
<button onclick="closeOverlay()"
|
||||
style="position:absolute;top:16px;left:16px;z-index:10;
|
||||
@@ -81,31 +87,35 @@
|
||||
<iframe id="overlay-iframe" style="width:100%;height:100%;border:none;display:block;"></iframe>
|
||||
</div>
|
||||
|
||||
<!-- ─── Custom-URL-Button (oben links) ─── -->
|
||||
{% if custom_url and custom_url_enabled %}
|
||||
{% if custom_url_target == "redirect" %}
|
||||
<a href="{{ custom_url }}"
|
||||
style="position:fixed;top:16px;left:16px;z-index:9999;
|
||||
padding:10px 20px;background:#DA002D;color:#fff;
|
||||
border:none;border-radius:8px;font-size:1rem;cursor:pointer;
|
||||
text-decoration:none;box-shadow:0 2px 8px rgba(0,0,0,0.3);">
|
||||
{{ custom_url_label or "Info" }}
|
||||
</a>
|
||||
{% else %}
|
||||
<button onclick='openOverlay({{ custom_url | tojson }})'
|
||||
style="position:fixed;top:16px;left:16px;z-index:9999;
|
||||
padding:10px 20px;background:#DA002D;color:#fff;
|
||||
border:none;border-radius:8px;font-size:1rem;cursor:pointer;
|
||||
box-shadow:0 2px 8px rgba(0,0,0,0.3);">
|
||||
{{ custom_url_label or "Info" }}
|
||||
</button>
|
||||
{% endif %}
|
||||
{% if custom_url_target == "redirect" %}
|
||||
<!-- Direkt-Weiterleitung (verlässt den Player) -->
|
||||
<a href="{{ custom_url }}"
|
||||
style="position:fixed;top:16px;left:16px;z-index:9999;
|
||||
padding:10px 20px;background:#DA002D;color:#fff;
|
||||
border:none;border-radius:8px;font-size:1rem;cursor:pointer;
|
||||
text-decoration:none;box-shadow:0 2px 8px rgba(0,0,0,0.3);">
|
||||
{{ custom_url_label or "Info" }}
|
||||
</a>
|
||||
{% else %}
|
||||
<!-- iframe-Overlay (bleibt im Player, Zurück-Button) -->
|
||||
<button onclick='openOverlay({{ custom_url | tojson }})'
|
||||
style="position:fixed;top:16px;left:16px;z-index:9999;
|
||||
padding:10px 20px;background:#DA002D;color:#fff;
|
||||
border:none;border-radius:8px;font-size:1rem;cursor:pointer;
|
||||
box-shadow:0 2px 8px rgba(0,0,0,0.3);">
|
||||
{{ custom_url_label or "Info" }}
|
||||
</button>
|
||||
{% endif %}
|
||||
{% endif %}
|
||||
|
||||
|
||||
<!-- ─── Medien-Elemente (eines ist jeweils sichtbar) ─── -->
|
||||
<img id="image">
|
||||
<video id="video" muted autoplay playsinline></video>
|
||||
<iframe id="iframe"></iframe>
|
||||
|
||||
<!-- ─── Newsticker (optional, Laufband unten) ─── -->
|
||||
{% 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">
|
||||
@@ -117,9 +127,15 @@
|
||||
</div>
|
||||
{% endif %}
|
||||
|
||||
|
||||
<script>
|
||||
// ═════════════════════════════════════════════════════
|
||||
// Player-JavaScript
|
||||
// ═════════════════════════════════════════════════════
|
||||
|
||||
let playerTimer = null;
|
||||
|
||||
// ─── Custom-URL-Overlay ───
|
||||
function openOverlay(url) {
|
||||
if (playerTimer) clearTimeout(playerTimer);
|
||||
document.getElementById("overlay").style.display = "block";
|
||||
@@ -132,7 +148,7 @@ function closeOverlay() {
|
||||
playNext();
|
||||
}
|
||||
|
||||
// Newsticker Uhrzeit
|
||||
// ─── Newsticker-Uhr (aktuelle Uhrzeit) ───
|
||||
function updateClock() {
|
||||
const el = document.getElementById('newsticker-clock');
|
||||
if (!el) return;
|
||||
@@ -141,29 +157,23 @@ function updateClock() {
|
||||
}
|
||||
setInterval(updateClock, 1000);
|
||||
updateClock();
|
||||
/* -----------------------------
|
||||
Daten vom Server
|
||||
----------------------------- */
|
||||
|
||||
// ─── Vom Server übergebene Daten ───
|
||||
const normalFiles = {{ normal_files | tojson }};
|
||||
const prioFiles = {{ prio_files | tojson }};
|
||||
const interval = {{ interval }} * 1000;
|
||||
const screen = "{{ screen }}";
|
||||
const site = "{{ site }}";
|
||||
|
||||
/* -----------------------------
|
||||
Player State
|
||||
----------------------------- */
|
||||
// ─── Player-Status ───
|
||||
let normalIndex = 0;
|
||||
let prioIndex = 0;
|
||||
let mode = "normal"; // "normal" oder "prio"
|
||||
|
||||
let mode = "normal"; // "normal" | "prio"
|
||||
const img = document.getElementById("image");
|
||||
const vid = document.getElementById("video");
|
||||
const iframe = document.getElementById("iframe");
|
||||
|
||||
/* -----------------------------
|
||||
Hilfsfunktionen
|
||||
----------------------------- */
|
||||
// ─── Hilfsfunktionen ───
|
||||
function normalizeItem(item) {
|
||||
if (typeof item === "string") {
|
||||
const lower = item.toLowerCase();
|
||||
@@ -172,7 +182,6 @@ function normalizeItem(item) {
|
||||
}
|
||||
return { kind: "file", name: item };
|
||||
}
|
||||
// Item is already an object (from backend) - ensure zoom is set
|
||||
if (item.kind === "url" && !("zoom" in item)) {
|
||||
item.zoom = 1.0;
|
||||
}
|
||||
@@ -185,65 +194,51 @@ function normalizeItem(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 isHtml(item) {
|
||||
return item.kind === "file" && /\.(html?|htm)$/i.test(item.name);
|
||||
}
|
||||
|
||||
// ─── Nächstes Element aus der Playlist holen ───
|
||||
// Ablauf: Normal-Playlist → Priority → Normal → …
|
||||
function getNextItem() {
|
||||
const normalizedNormal = normalFiles.map(normalizeItem);
|
||||
const normalizedPrio = prioFiles.map(normalizeItem);
|
||||
const activeNormal = normalizedNormal.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) {
|
||||
return {
|
||||
item: activePrio[prioIndex++ % activePrio.length],
|
||||
isPrio: true
|
||||
};
|
||||
return { item: activePrio[prioIndex++ % activePrio.length], isPrio: true };
|
||||
}
|
||||
|
||||
// ✅ Sonderfall: nur normale Playlist vorhanden
|
||||
// Nur normale Playlist vorhanden
|
||||
if (activePrio.length === 0 && activeNormal.length > 0) {
|
||||
return {
|
||||
item: activeNormal[normalIndex++ % activeNormal.length],
|
||||
isPrio: false
|
||||
};
|
||||
return { item: activeNormal[normalIndex++ % activeNormal.length], isPrio: false };
|
||||
}
|
||||
|
||||
// ✅ Normal-Phase
|
||||
// Normal-Phase
|
||||
if (mode === "normal") {
|
||||
const item = activeNormal[normalIndex++];
|
||||
if (normalIndex >= activeNormal.length) {
|
||||
normalIndex = 0;
|
||||
if (activePrio.length > 0) {
|
||||
mode = "prio"; // ➜ nach kompletter Normal-Playlist wechseln
|
||||
}
|
||||
if (activePrio.length > 0) mode = "prio";
|
||||
}
|
||||
return { item, isPrio: false };
|
||||
}
|
||||
|
||||
// ✅ Priority-Phase
|
||||
// Priority-Phase
|
||||
if (mode === "prio") {
|
||||
const item = activePrio[prioIndex++];
|
||||
if (prioIndex >= activePrio.length) {
|
||||
prioIndex = 0;
|
||||
mode = "normal"; // ➜ nach kompletter Priority zurück
|
||||
mode = "normal";
|
||||
}
|
||||
return { item, isPrio: true };
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
/* -----------------------------
|
||||
Medien abspielen
|
||||
----------------------------- */
|
||||
// ─── Aktuelles Medium abspielen ───
|
||||
function playNext() {
|
||||
const entry = getNextItem();
|
||||
if (!entry) return;
|
||||
@@ -251,25 +246,22 @@ function playNext() {
|
||||
const item = entry.item;
|
||||
const basePath = entry.isPrio ? "priority" : screen;
|
||||
|
||||
// Alle Elemente ausblenden
|
||||
img.style.display = "none";
|
||||
vid.style.display = "none";
|
||||
iframe.style.display = "none";
|
||||
vid.pause();
|
||||
vid.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") {
|
||||
iframe.style.display = "block";
|
||||
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;
|
||||
|
||||
if (zoom !== 1.0) {
|
||||
// Adjust iframe size and scale to simulate real browser zoom
|
||||
const scale = 1 / zoom; // Inverted for proper zoom behavior
|
||||
const scale = 1 / zoom;
|
||||
iframe.style.width = `${100 * scale}vw`;
|
||||
iframe.style.height = `${100 * scale}vh`;
|
||||
iframe.style.transform = `translate(-50%, -50%) scale(${zoom})`;
|
||||
@@ -282,6 +274,7 @@ function playNext() {
|
||||
return;
|
||||
}
|
||||
|
||||
// Datei aus dem Medienverzeichnis
|
||||
const src = entry.isPrio ? `/media/priority/${item.name}` : `/media/${site}/${basePath}/${item.name}`;
|
||||
if (isVideo(item)) {
|
||||
vid.style.display = "block";
|
||||
@@ -296,22 +289,20 @@ function playNext() {
|
||||
iframe.style.transform = `translate(-50%, -50%)`;
|
||||
playerTimer = setTimeout(playNext, interval);
|
||||
} else {
|
||||
// Bild (jpg/png)
|
||||
img.style.display = "block";
|
||||
img.src = src;
|
||||
playerTimer = setTimeout(playNext, interval);
|
||||
}
|
||||
}
|
||||
|
||||
/* -----------------------------
|
||||
Auto-Reload bei Änderungen
|
||||
----------------------------- */
|
||||
// ─── Auto-Reload bei Playlist-Änderungen ───
|
||||
let lastHash = null;
|
||||
|
||||
async function checkForUpdates() {
|
||||
try {
|
||||
const res = await fetch(`/playlist/${site}/${screen}/hash`, { cache: "no-store" });
|
||||
const hash = await res.text();
|
||||
|
||||
if (lastHash && lastHash !== hash) {
|
||||
location.reload();
|
||||
}
|
||||
@@ -321,9 +312,7 @@ async function checkForUpdates() {
|
||||
}
|
||||
}
|
||||
|
||||
/* -----------------------------
|
||||
Start
|
||||
----------------------------- */
|
||||
// ─── Start ───
|
||||
playNext();
|
||||
setInterval(checkForUpdates, 5000);
|
||||
</script>
|
||||
|
||||
Reference in New Issue
Block a user