Files
signage/templates/priority.html
Erik Thiele f540458b74 fix: Zeitsteuerung reparieren und vervollstaendigen
Der vorige Commit 26efed8 war nicht lauffaehig - fehlgeschlagene
Patch-Anwendungen hatten Zeilen aus ihren Bloecken geschoben.

Reparaturen:
- admin(): zerschossene Playlist-Schleife neu aufgebaut
- player(): return render_template war in den else-Zweig gerutscht,
  wodurch die View bei aktiver Priority-Playlist None lieferte (HTTP 500)
- playlist_item_enabled() existierte nicht mehr (umbenannt zu
  is_item_active), zwei Aufrufe im Player blieben stehen -> NameError
- update_playlist_item(): Absturz bei new_url=null, und der
  enabled-Status ging beim Bearbeiten verloren
- Dateityp-Erkennung mit else-Fallback, ftype konnte ungebunden bleiben

Zeitsteuerung:
- Zeitfenster ueber Mitternacht (22:00-02:00) funktionieren; bei weekly
  gehoert ein solches Fenster zum Wochentag seines Starts
- Optionaler Gueltigkeitszeitraum (valid_from/valid_until) fuer daily
  und weekly, beide Grenzen einzeln optional und inklusiv
- daily/weekly speichern nur noch die Uhrzeit ("08:00") statt eines
  Zeitstempels mit bedeutungslosem Datumsanteil; _schedule_time() liest
  weiterhin auch Altbestaende mit vollem ISO-Zeitstempel
- playlist_hash() bezieht den ausgewerteten Aktiv-Zustand ein, sonst
  bemerkt der Player ein ablaufendes Zeitfenster erst beim naechsten
  Reload aus anderem Grund
- Zeitsteuerung auch fuer Datei-Eintraege, nicht nur URLs
- Neuer Filter schedule_text fuer den Tooltip am "Zeit"-Badge

Oberflaeche:
- required lag auf ausgeblendeten Feldern und blockierte das Absenden
  des Formulars "URL hinzufuegen" ohne sichtbare Meldung
- updateScheduleDaysVisibility() war innerhalb von DOMContentLoaded
  definiert, der Edit-Handler darueber -> ReferenceError bei jedem Klick
- data-schedule mit tojson zerlegte das Attribut, da tojson " nicht
  escaped
- Zeitsteuerungs-Block als schlichte Formularzeilen statt als card, die
  global den roten Rand und grauen Header erhaelt
- Zeitsteuerung auch in der Priority-Playlist

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-29 19:42:03 +02:00

696 lines
28 KiB
HTML
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
<!doctype html>
<html lang="de">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1, viewport-fit=cover">
<title>CANCOM Simple Signage – Priority Playlist</title>
<link rel="icon" href="{{ url_for('static', filename='favicon.ico') }}">
<script>
(() => {
const stored = window.localStorage.getItem("signage-theme");
const theme = stored === "dark" || stored === "light" ? stored : "light";
document.documentElement.setAttribute("data-bs-theme", theme);
})();
</script>
<link rel="stylesheet"
href="https://unpkg.com/@tabler/core@1.0.0-beta20/dist/css/tabler.min.css">
<link rel="stylesheet"
href="https://cdn.jsdelivr.net/npm/@tabler/icons-webfont@latest/tabler-icons.min.css">
<script defer
src="https://unpkg.com/@tabler/core@1.0.0-beta20/dist/js/tabler.min.js"></script>
<script defer
src="https://cdn.jsdelivr.net/npm/sortablejs@1.15.0/Sortable.min.js"></script>
{% include "_styles.html" %}
<style>
.playlist-row {
display: grid;
grid-template-columns:
28px 1fr 90px 80px 50px 40px 32px;
align-items: center;
gap: 0.75rem;
}
.playlist-name {
font-weight: 600;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
}
.playlist-size {
color: #6c757d;
font-size: 0.9em;
}
.drag-handle {
cursor: grab;
font-size: 1.2em;
text-align: center;
user-select: none;
}
.drag-handle:active {
cursor: grabbing;
}
.card-body .nav-tabs {
margin-left: -1rem;
margin-right: -1rem;
padding: 0 1rem;
border-bottom-color: var(--ccm-border);
}
.card-body .nav-tabs .nav-link {
background: transparent;
color: var(--ccm-muted);
}
.card-body .nav-tabs .nav-link.active {
background: var(--ccm-surface);
border-color: var(--ccm-border) var(--ccm-border) var(--ccm-surface);
color: var(--ccm-text);
}
.card-body .tab-content {
background: var(--ccm-surface);
padding: 1rem;
}
</style>
</head>
<body>
<div class="page">
{% set brand_subtitle = "Priority Playlist" %}
{% set site_dropdown_url_prefix = "/admin/" %}
{% include "_header.html" %}
<!-- Second Nav -->
<div class="navbar-expand-md">
<div class="collapse navbar-collapse show">
<div class="navbar navbar-dark nav-surface">
<div class="container-xl">
<div class="d-flex flex-wrap gap-2 py-2">
<a class="btn btn-white" href="/admin/{{ current_site }}"><i class="ti ti-layout-dashboard me-1"></i>Übersicht</a>
<a class="btn btn-white active" href="/admin/{{ current_site }}/priority"><i class="ti ti-alert-triangle me-1"></i>Priority Playlist</a>
<a class="btn btn-white" href="/willkommen?site={{ current_site }}"><i class="ti ti-file-plus me-1"></i>Willkommensseite</a>
</div>
</div>
</div>
</div>
</div>
<!-- Content -->
<div class="page-wrapper">
<div class="container-xl mt-4">
<!-- ========================= -->
<!-- PRIORITY / GLOBAL PLAYLIST -->
<!-- ========================= -->
<div class="card mb-6">
<div class="card-header d-flex justify-content-between align-items-center">
<span>
<div class="page-title" style="color:#DA002D;">⚠ PRIORITY Playlist</div>
<div class="page-subtitle">wirkt auf alle Player</div>
</span>
</div>
<div class="card-body">
<ul class="nav nav-tabs" data-bs-toggle="tabs" role="tablist">
<li class="nav-item" role="presentation">
<a href="#tab-prio-playlist" class="nav-link active" data-bs-toggle="tab" role="tab">
<i class="ti ti-list me-1"></i>Playlist
</a>
</li>
<li class="nav-item" role="presentation">
<a href="#tab-prio-media" class="nav-link" data-bs-toggle="tab" role="tab">
<i class="ti ti-upload me-1"></i>Medien
</a>
</li>
</ul>
<div class="tab-content">
<!-- Tab: Playlist -->
<div class="tab-pane active" id="tab-prio-playlist" role="tabpanel">
<ul class="list-group mb-3" id="playlist-priority">
{% for file in priority_files %}
<li class="list-group-item playlist-row"
data-file="{{ file.name }}">
<input class="form-check-input playlist-enabled"
type="checkbox"
{% if file.enabled %}checked{% endif %}
title="Element aktivieren">
<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 }}{% if file.zoom %} (Zoom: {{ file.zoom }}x){% endif %}"
{% endif %}>
{{ file.name }}{% if file.type == "url" and file.zoom != 1.0 %} <strong style="color: #DA002D;">[{{ file.zoom }}x]</strong>{% endif %}{% if file.type == "url" %}{% if file.get('passthrough', False) %} <i class="ti ti-link" style="color:#6c757d;" title="Query-Parameter werden übergeben"></i>{% else %} <i class="ti ti-link-off" style="color:#adb5bd;" title="Query-Parameter werden nicht übergeben"></i>{% endif %}{% endif %}
{% if file.schedule %}<span class="badge bg-info text-white ms-1" title="{{ file.schedule | schedule_text }}">Zeit</span>{% endif %}
</span>
{% if file.type == "image" %}
<span class="badge bg-purple text-purple-fg">Bild</span>
{% elif file.type == "url" or file.type == "html" %}
<span class="badge bg-blue text-blue-fg">URL</span>
{% else %}
<span class="badge bg-orange text-orange-fg">Video</span>
{% endif %}
<span class="playlist-size">
{% if file.type == "url" %}
{{ file.size }}
{% else %}
{{ file.size }} KB
{% endif %}
</span>
<!-- Delete -->
<form action="{{ url_for('delete_file', site=current_site, screen='priority', filename=file.name) }}"
method="post"
class="d-inline"
onmousedown="event.stopPropagation()">
<button type="submit"
class="btn btn-outline-danger btn-sm"
onclick="return confirm('Datei wirklich löschen?')">
Löschen
</button>
</form>
<!-- Bearbeiten -->
<button type="button"
class="btn btn-outline-secondary btn-sm edit-url-btn"
title="Bearbeiten"
data-name="{{ file.name | e }}"
data-type="{{ file.type }}"
data-zoom="{{ file.zoom | default(1.0) }}"
data-site="{{ current_site }}"
data-screen="priority"
data-oldname="{{ file.name | e }}"
data-passthrough="{{ 'true' if file.get('passthrough', False) else 'false' }}"
data-schedule='{{ file.schedule | tojson }}'>
Edit
</button>
<!-- Drag -->
<span class="drag-handle" title="Reihenfolge ändern">⇅</span>
</li>
{% endfor %}
</ul>
<button class="btn btn-primary"
onclick="savePlaylist('{{ current_site }}', 'priority')">
Priority-Playlist speichern
</button>
</div>
<!-- Tab: Medien -->
<div class="tab-pane" id="tab-prio-media" role="tabpanel">
<h3>Medien hochladen</h3>
<form action="/admin/{{ current_site }}/upload/priority"
method="post" enctype="multipart/form-data">
<div class="input-group mb-3">
<input type="file" name="file" class="form-control" required>
<button class="btn btn-primary" type="submit">Upload</button>
</div>
</form>
<hr>
<h3>URL hinzufügen</h3>
<form action="/admin/{{ current_site }}/add-url/priority" method="post" class="mb-3">
<div class="mb-2">
<label class="form-label">URL</label>
<input type="url" name="url" class="form-control"
placeholder="https://example.com" required>
</div>
<div class="mb-2">
<label class="form-label">Zoom Faktor (z.B. 1.0, 1.5, 2.0)</label>
<input type="number" name="zoom" class="form-control" step="0.1" value="1.0" min="0.1">
</div>
<!-- Zeitsteuerung -->
<hr class="mt-4 mb-3">
<h3>Zeitsteuerung</h3>
<div class="mb-2 form-check">
<input class="form-check-input" type="checkbox" id="schedule-enabled-priority" name="schedule_enabled">
<label class="form-check-label" for="schedule-enabled-priority">Zeitgesteuerte Wiedergabe</label>
</div>
<div class="mb-3" id="schedule-options-priority" style="display: none;">
<div class="mb-3">
<label class="form-label">Wiederholung</label>
<select class="form-select" name="schedule_mode">
<option value="once">Einmalig</option>
<option value="daily">Täglich</option>
<option value="weekly">Wöchentlich</option>
</select>
</div>
<!-- Einmalig: Datum + Uhrzeit -->
<div class="row mb-3" id="schedule-datetime-priority">
<div class="col">
<label class="form-label">Start</label>
<input type="datetime-local" class="form-control" name="schedule_start">
</div>
<div class="col">
<label class="form-label">Ende</label>
<input type="datetime-local" class="form-control" name="schedule_end">
</div>
</div>
<!-- Täglich / Wöchentlich: nur Uhrzeit -->
<div class="row mb-3" id="schedule-time-priority" style="display: none;">
<div class="col">
<label class="form-label">Von</label>
<input type="time" class="form-control" name="schedule_start_time">
</div>
<div class="col">
<label class="form-label">Bis</label>
<input type="time" class="form-control" name="schedule_end_time">
</div>
</div>
<div class="mb-3" id="schedule-validity-priority" style="display: none;">
<label class="form-label">Gültig von / bis <span class="text-muted">(optional)</span></label>
<div class="row">
<div class="col">
<input type="date" class="form-control" name="schedule_valid_from">
</div>
<div class="col">
<input type="date" class="form-control" name="schedule_valid_until">
</div>
</div>
<small class="form-hint">Ohne Angabe läuft die Wiedergabe unbefristet.</small>
</div>
<div class="mb-3" id="schedule-days-priority" style="display: none;">
<label class="form-label">Wochentage</label>
<div class="d-flex gap-2">
{% for nr, tag in [(0, 'Mo'), (1, 'Di'), (2, 'Mi'), (3, 'Do'), (4, 'Fr'), (5, 'Sa'), (6, 'So')] %}
<div class="form-check">
<input class="form-check-input" type="checkbox" name="schedule_days" value="{{ nr }}" id="prio-day-{{ nr }}">
<label class="form-check-label" for="prio-day-{{ nr }}">{{ tag }}</label>
</div>
{% endfor %}
</div>
</div>
</div>
<button class="btn btn-primary" type="submit">URL hinzufügen</button>
</form>
</div>
</div>
</div>
</div>
</div>
</div>
<!-- Footer -->
{% include "_footer.html" %}
</div>
<script>
/* ─── Global: editState ─── */
var editState = { site: "", screen: "", oldName: "", type: "url" };
/* Uhrzeit aus "HH:MM" oder einem vollen ISO-Zeitstempel herausziehen */
function scheduleClock(value) {
if (!value) return "";
var text = String(value).replace(" ", "T");
var idx = text.indexOf("T");
if (idx !== -1) text = text.substring(idx + 1);
return text.substring(0, 5);
}
/* Sichtbarkeit der Zeitsteuerungs-Felder im Modal an den Modus koppeln.
Muss auf Top-Level stehen - der Edit-Handler unten läuft ausserhalb
von DOMContentLoaded. */
function updateScheduleDaysVisibility() {
var modeEl = document.getElementById("edit-schedule-mode");
if (!modeEl) return;
var wiederkehrend = modeEl.value === "daily" || modeEl.value === "weekly";
var daysEl = document.getElementById("edit-schedule-days");
var validityEl = document.getElementById("edit-schedule-validity");
var datetimeEl = document.getElementById("edit-schedule-datetime");
var timeEl = document.getElementById("edit-schedule-time");
// Wochentage nur bei "wöchentlich" - bei "täglich" gilt ohnehin jeder Tag
if (daysEl) daysEl.style.display = modeEl.value === "weekly" ? "" : "none";
// Gültigkeitszeitraum ergänzt die Tageszeit; bei "einmalig" steckt das
// Datum bereits in Start und Ende.
if (validityEl) validityEl.style.display = wiederkehrend ? "" : "none";
if (datetimeEl) datetimeEl.style.display = wiederkehrend ? "none" : "";
if (timeEl) timeEl.style.display = wiederkehrend ? "" : "none";
}
/* Event-Delegation für Edit-Buttons (global, sofort aktiv) */
document.addEventListener("click", function (e) {
var btn = e.target.closest(".edit-url-btn");
if (!btn) return;
e.preventDefault();
editState.site = btn.dataset.site;
editState.screen = btn.dataset.screen;
editState.oldName = btn.dataset.oldname;
editState.type = btn.dataset.type || "url";
var isUrl = editState.type === "url";
document.getElementById("edit-url-input").value = btn.dataset.oldname;
document.getElementById("edit-zoom-input").value = btn.dataset.zoom;
document.getElementById("edit-passthrough-input").checked = btn.dataset.passthrough === "true";
/* Bei Dateien gibt es keine URL, keinen Zoom und kein Passthrough -
dann bleibt nur die Zeitsteuerung übrig. */
document.getElementById("edit-url-row").style.display = isUrl ? "" : "none";
document.getElementById("edit-zoom-row").style.display = isUrl ? "" : "none";
document.getElementById("edit-passthrough-row").style.display = isUrl ? "" : "none";
document.getElementById("edit-url-modal-title").textContent =
isUrl ? "URL bearbeiten" : "Zeitsteuerung: " + btn.dataset.oldname;
var schedule = JSON.parse(btn.dataset.schedule || "null");
var enabledEl = document.getElementById("edit-schedule-enabled");
var optionsEl = document.getElementById("edit-schedule-options");
if (schedule) {
enabledEl.checked = true;
optionsEl.style.display = "";
document.getElementById("edit-schedule-mode").value = schedule.mode || "once";
var wiederkehrend = (schedule.mode === "daily" || schedule.mode === "weekly");
document.getElementById("edit-schedule-start").value = wiederkehrend ? "" : (schedule.start || "");
document.getElementById("edit-schedule-end").value = wiederkehrend ? "" : (schedule.end || "");
document.getElementById("edit-schedule-start-time").value = scheduleClock(schedule.start);
document.getElementById("edit-schedule-end-time").value = scheduleClock(schedule.end);
document.getElementById("edit-schedule-valid-from").value = schedule.valid_from || "";
document.getElementById("edit-schedule-valid-until").value = schedule.valid_until || "";
var days = schedule.days || [];
for (var i = 0; i < 7; i++) {
document.getElementById("edit-prio-day-" + i).checked = days.indexOf(String(i)) !== -1;
}
} else {
enabledEl.checked = false;
optionsEl.style.display = "none";
document.getElementById("edit-schedule-start").value = "";
document.getElementById("edit-schedule-end").value = "";
document.getElementById("edit-schedule-start-time").value = "";
document.getElementById("edit-schedule-end-time").value = "";
document.getElementById("edit-schedule-valid-from").value = "";
document.getElementById("edit-schedule-valid-until").value = "";
for (var j = 0; j < 7; j++) {
document.getElementById("edit-prio-day-" + j).checked = false;
}
}
updateScheduleDaysVisibility();
document.getElementById("edit-url-error").style.display = "none";
new bootstrap.Modal(document.getElementById("edit-url-modal")).show();
});
document.addEventListener("DOMContentLoaded", function () {
function initSortable(name) {
const el = document.getElementById("playlist-" + name);
if (!el || typeof Sortable === "undefined") return;
new Sortable(el, {
animation: 150,
handle: ".drag-handle",
ghostClass: "bg-light",
chosenClass: "bg-light"
});
}
initSortable("priority");
function savePlaylist(site, screen) {
const items = document.querySelectorAll(
"#playlist-" + screen + " li"
);
const playlist = [];
const enabled = {};
items.forEach((item) => {
const checkbox = item.querySelector(".playlist-enabled");
const fileName = item.dataset.file;
enabled[fileName] = !!(checkbox && checkbox.checked);
playlist.push(fileName);
});
fetch("/admin/" + site + "/playlist/" + screen, {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({
playlist: playlist,
enabled: enabled
})
}).then(() => location.reload());
}
window.savePlaylist = savePlaylist;
/* ─── Bootstrap Tooltips ─── */
var tooltipTriggerList = [].slice.call(document.querySelectorAll('[data-bs-toggle="tooltip"]'));
var tooltipList = tooltipTriggerList.map(function (tooltipTriggerEl) {
return new bootstrap.Tooltip(tooltipTriggerEl);
});
/* ─── Zeitsteuerung: Änderungs-Events im Modal ─── */
document.getElementById("edit-schedule-enabled").addEventListener("change", function () {
document.getElementById("edit-schedule-options").style.display = this.checked ? "" : "none";
});
document.getElementById("edit-schedule-mode").addEventListener("change", updateScheduleDaysVisibility);
updateScheduleDaysVisibility();
/* ─── "URL hinzufügen"-Formular: Zeitsteuerungs-Felder ein-/ausblenden ─── */
(function () {
var cb = document.getElementById("schedule-enabled-priority");
if (!cb) return;
var optionsEl = document.getElementById("schedule-options-priority");
var daysEl = document.getElementById("schedule-days-priority");
var validityEl = document.getElementById("schedule-validity-priority");
var datetimeEl = document.getElementById("schedule-datetime-priority");
var timeEl = document.getElementById("schedule-time-priority");
var modeEl = cb.closest("form").querySelector('[name="schedule_mode"]');
function sync() {
var mode = modeEl ? modeEl.value : "once";
var wiederkehrend = (mode === "daily" || mode === "weekly");
if (optionsEl) optionsEl.style.display = cb.checked ? "" : "none";
if (daysEl) daysEl.style.display = (cb.checked && mode === "weekly") ? "" : "none";
if (validityEl) validityEl.style.display = (cb.checked && wiederkehrend) ? "" : "none";
if (datetimeEl) datetimeEl.style.display = wiederkehrend ? "none" : "";
if (timeEl) timeEl.style.display = wiederkehrend ? "" : "none";
}
cb.addEventListener("change", sync);
if (modeEl) modeEl.addEventListener("change", sync);
sync();
})();
/* ─── Edit-URL-Modal: Speichern-Button ─── */
document.getElementById("edit-url-save").addEventListener("click", function () {
const isUrl = editState.type === "url";
const newUrl = document.getElementById("edit-url-input").value.trim();
const zoom = parseFloat(document.getElementById("edit-zoom-input").value) || 1.0;
const errorEl = document.getElementById("edit-url-error");
if (isUrl && !newUrl.startsWith("http://") && !newUrl.startsWith("https://")) {
errorEl.textContent = "Ungültige URL (muss mit http:// oder https:// beginnen)";
errorEl.style.display = "block";
return;
}
errorEl.style.display = "none";
// Zeitsteuerung sammeln
const scheduleEnabled = document.getElementById("edit-schedule-enabled").checked;
let schedule = null;
if (scheduleEnabled) {
const mode = document.getElementById("edit-schedule-mode").value;
const wiederkehrend = (mode === "daily" || mode === "weekly");
// Einmalig arbeitet mit Datum + Uhrzeit, täglich/wöchentlich nur mit Uhrzeit
const start = wiederkehrend
? document.getElementById("edit-schedule-start-time").value
: document.getElementById("edit-schedule-start").value;
const end = wiederkehrend
? document.getElementById("edit-schedule-end-time").value
: document.getElementById("edit-schedule-end").value;
if (!start || !end) {
errorEl.textContent = wiederkehrend
? "Von- und Bis-Uhrzeit müssen angegeben werden"
: "Start und Ende müssen angegeben werden";
errorEl.style.display = "block";
return;
}
schedule = { mode: mode, start: start, end: end };
if (wiederkehrend) {
const validFrom = document.getElementById("edit-schedule-valid-from").value;
const validUntil = document.getElementById("edit-schedule-valid-until").value;
if (validFrom && validUntil && validFrom > validUntil) {
errorEl.textContent = "\"Gültig von\" liegt nach \"Gültig bis\"";
errorEl.style.display = "block";
return;
}
if (validFrom) schedule.valid_from = validFrom;
if (validUntil) schedule.valid_until = validUntil;
}
if (mode === "weekly") {
const days = [];
for (var i = 0; i < 7; i++) {
if (document.getElementById("edit-prio-day-" + i).checked) days.push(String(i));
}
schedule.days = days;
}
}
fetch("/admin/" + editState.site + "/update-playlist-item/" + editState.screen, {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({
old_name: editState.oldName,
new_url: isUrl ? newUrl : null,
zoom: zoom,
passthrough: isUrl ? document.getElementById("edit-passthrough-input").checked : null,
schedule: schedule || {}
})
}).then(function (r) {
if (r.ok) {
location.reload();
} else {
return r.json().then(function (d) {
errorEl.textContent = d.error || "Fehler beim Speichern";
errorEl.style.display = "block";
});
}
}).catch(function () {
errorEl.textContent = "Netzwerkfehler";
errorEl.style.display = "block";
});
});
});
</script>
<!-- Modal: URL bearbeiten -->
<div class="modal fade" id="edit-url-modal" tabindex="-1">
<div class="modal-dialog">
<div class="modal-content">
<div class="modal-header">
<h5 class="modal-title" id="edit-url-modal-title">URL bearbeiten</h5>
<button type="button" class="btn-close" data-bs-dismiss="modal"></button>
</div>
<div class="modal-body">
<div class="mb-3" id="edit-url-row">
<label class="form-label">URL</label>
<input type="url" class="form-control" id="edit-url-input">
</div>
<div class="mb-3" id="edit-zoom-row">
<label class="form-label">Zoom Faktor (z.B. 1.0, 1.5, 2.0)</label>
<input type="number" class="form-control" id="edit-zoom-input" step="0.1" min="0.1" value="1.0">
</div>
<div class="mb-3 form-check" id="edit-passthrough-row">
<input type="checkbox" class="form-check-input" id="edit-passthrough-input">
<label class="form-check-label" for="edit-passthrough-input">Query-Parameter an diese URL übergeben</label>
</div>
<!-- Zeitsteuerung -->
<div class="mb-3 form-check">
<input class="form-check-input" type="checkbox" id="edit-schedule-enabled">
<label class="form-check-label" for="edit-schedule-enabled">Zeitgesteuerte Wiedergabe</label>
</div>
<div id="edit-schedule-options" style="display: none;">
<div class="mb-3">
<label class="form-label">Wiederholung</label>
<select class="form-select" id="edit-schedule-mode">
<option value="once">Einmalig</option>
<option value="daily">Täglich</option>
<option value="weekly">Wöchentlich</option>
</select>
</div>
<!-- Einmalig: Datum + Uhrzeit -->
<div class="row mb-3" id="edit-schedule-datetime">
<div class="col">
<label class="form-label">Start</label>
<input type="datetime-local" class="form-control" id="edit-schedule-start">
</div>
<div class="col">
<label class="form-label">Ende</label>
<input type="datetime-local" class="form-control" id="edit-schedule-end">
</div>
</div>
<!-- Täglich / Wöchentlich: nur Uhrzeit -->
<div class="row mb-3" id="edit-schedule-time" style="display: none;">
<div class="col">
<label class="form-label">Von</label>
<input type="time" class="form-control" id="edit-schedule-start-time">
</div>
<div class="col">
<label class="form-label">Bis</label>
<input type="time" class="form-control" id="edit-schedule-end-time">
</div>
</div>
<div class="mb-3" id="edit-schedule-validity" style="display: none;">
<label class="form-label">Gültig von / bis <span class="text-muted">(optional)</span></label>
<div class="row">
<div class="col">
<input type="date" class="form-control" id="edit-schedule-valid-from">
</div>
<div class="col">
<input type="date" class="form-control" id="edit-schedule-valid-until">
</div>
</div>
<small class="form-hint">Ohne Angabe läuft die Wiedergabe unbefristet.</small>
</div>
<div class="mb-3" id="edit-schedule-days" style="display: none;">
<label class="form-label">Wochentage</label>
<div class="d-flex gap-2">
{% for nr, tag in [(0, 'Mo'), (1, 'Di'), (2, 'Mi'), (3, 'Do'), (4, 'Fr'), (5, 'Sa'), (6, 'So')] %}
<div class="form-check">
<input class="form-check-input" type="checkbox" value="{{ nr }}" id="edit-prio-day-{{ nr }}">
<label class="form-check-label" for="edit-prio-day-{{ nr }}">{{ tag }}</label>
</div>
{% endfor %}
</div>
</div>
</div>
<div class="alert alert-danger py-2" id="edit-url-error" style="display:none"></div>
</div>
<div class="modal-footer">
<button type="button" class="btn btn-secondary" data-bs-dismiss="modal">Abbrechen</button>
<button type="button" class="btn btn-primary" id="edit-url-save">Speichern</button>
</div>
</div>
</div>
</div>
</body>
</html>