mkcontrol-app/templates/soundboard.html
2026-02-19 09:22:38 +01:00

295 lines
11 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.

{% extends "base.html" %}
{% block title %}Soundboard Themen{% endblock %}
{% block content %}
<div class="container my-5">
<h1>Soundboard</h1>
<p class="lead mb-4">Themenbezogene Soundsets, unabhängig vom Hub.</p>
<div class="row mb-4">
<div class="col-md-6">
<label class="form-label fw-bold">Thema wählen</label>
<select id="soundboard-select" class="form-select">
{% for sb in soundboard_configs %}
<option value="{{ sb.filename }}">{{ sb.name }}</option>
{% endfor %}
</select>
</div>
<div class="col-md-6 d-flex align-items-end">
<div class="btn-group">
<button id="btn-load-sb" class="btn btn-primary">Laden</button>
<button id="btn-play-random" class="btn btn-outline-secondary" disabled>1x Zufällig</button>
</div>
</div>
</div>
<div id="sb-content" style="display:none">
<div class="mb-4">
<label class="form-label fw-bold">Lautstärke</label>
<input type="range" class="form-range" id="sb-volume" min="0" max="100" value="80" step="5">
<small class="text-muted">Wirkt global auf alle Soundboard-Kanäle.</small>
</div>
<div id="sb-backgrounds" class="mb-4"></div>
<div id="sb-sounds" class="mb-4"></div>
<div id="sb-random" class="mb-4"></div>
<div id="sb-auto" class="mb-4"></div>
</div>
<div class="mt-4">
<a href="{{ url_for('main.index') }}" class="btn btn-secondary">Zurück</a>
</div>
</div>
{% endblock %}
{% block scripts %}
<script>
const selectEl = document.getElementById('soundboard-select');
const loadBtn = document.getElementById('btn-load-sb');
const randBtn = document.getElementById('btn-play-random');
const content = document.getElementById('sb-content');
let currentSB = null;
const statusBox = (msg, type='info') => {
console.log(msg);
};
async function loadSoundboard(filename) {
loadBtn.disabled = true;
try {
const res = await fetch('/api/soundboard/load', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ filename })
});
const data = await res.json();
if (!data.success) throw new Error(data.message || 'Load failed');
currentSB = data.soundboard;
renderSoundboard(currentSB);
content.style.display = 'block';
randBtn.disabled = false;
} catch (e) {
alert('Konnte Soundboard nicht laden: ' + e.message);
} finally {
loadBtn.disabled = false;
}
}
function renderSoundboard(sb) {
renderBackgrounds(sb.backgrounds || []);
renderSounds(sb.sounds || []);
renderRandom(sb.random_pool || []);
updateAutoStatus();
}
function renderBackgrounds(list) {
const container = document.getElementById('sb-backgrounds');
if (!list.length) { container.innerHTML=''; return; }
container.innerHTML = '<h4>Hintergrund (Loop)</h4><div class="d-flex flex-wrap gap-2"></div>';
const wrap = container.querySelector('div');
list.forEach(s => {
const btn = document.createElement('button');
btn.className = 'btn btn-outline-secondary';
btn.textContent = s.name || s.id || s.file;
btn.onclick = () => playSound(s);
const stop = document.createElement('button');
stop.className = 'btn btn-outline-danger';
stop.textContent = 'Stop';
stop.onclick = () => stopSound(s.channel);
wrap.append(btn, stop);
});
}
function renderSounds(list) {
const container = document.getElementById('sb-sounds');
if (!list.length) { container.innerHTML=''; return; }
container.innerHTML = '<h4>Einzelsounds</h4><div class="d-flex flex-wrap gap-2"></div>';
const wrap = container.querySelector('div');
list.forEach(s => {
const btn = document.createElement('button');
btn.className = 'btn btn-primary';
btn.textContent = s.name || s.id || s.file;
btn.onclick = () => playSound(s);
wrap.append(btn);
});
}
function renderRandom(list) {
const container = document.getElementById('sb-random');
if (!list.length) { container.innerHTML=''; randBtn.disabled=true; return; }
const items = list.map(s => `<li>${s.name || s.id || s.file}</li>`).join('');
container.innerHTML = '<h4>Zufallspool</h4><p class="text-muted">max 2x pro Stunde je Datei.</p><ul>' + items + '</ul>';
renderAutoRandomControls();
}
function renderAutoRandomControls() {
const container = document.getElementById('sb-auto');
container.innerHTML = `
<h4>Automatik (Random)</h4>
<div class="row g-3 align-items-end">
<div class="col-md-3">
<label class="form-label">Intervall min (Minuten)</label>
<input id="auto-interval-min" type="number" class="form-control" value="5" min="1" max="60">
</div>
<div class="col-md-3">
<label class="form-label">Intervall max (Minuten)</label>
<input id="auto-interval-max" type="number" class="form-control" value="10" min="1" max="120">
</div>
<div class="col-md-3">
<label class="form-label">Startverzögerung min (Minuten)</label>
<input id="auto-delay-min" type="number" class="form-control" value="3" min="0" max="60">
</div>
<div class="col-md-3">
<label class="form-label">Startverzögerung max (Minuten)</label>
<input id="auto-delay-max" type="number" class="form-control" value="12" min="0" max="120">
</div>
</div>
<div class="mt-3 d-flex gap-2 align-items-center">
<button id="auto-start" class="btn btn-success" ${!randBtn.disabled ? '' : 'disabled'}>Auto-Start</button>
<button id="auto-stop" class="btn btn-outline-danger" disabled>Stop</button>
<span id="auto-status" class="text-muted ms-2">Aus</span>
</div>
`;
const start = document.getElementById('auto-start');
const stop = document.getElementById('auto-stop');
const status = document.getElementById('auto-status');
start.onclick = async () => {
const imin = Math.max(1, parseInt(document.getElementById('auto-interval-min').value, 10) || 5);
const imax = Math.max(imin, parseInt(document.getElementById('auto-interval-max').value, 10) || 10);
const dmin = Math.max(0, parseInt(document.getElementById('auto-delay-min').value, 10) || 3);
const dmax = Math.max(dmin, parseInt(document.getElementById('auto-delay-max').value, 10) || 12);
const res = await fetch('/api/soundboard/auto_start', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ interval_min: imin, interval_max: imax, delay_min: dmin, delay_max: dmax })
});
const data = await res.json();
if (data.success) {
if (data.next_seconds !== undefined && data.next_seconds !== null) {
status.textContent = `Aktiv (nächster in ${(data.next_seconds/60).toFixed(1)} min)`;
} else {
status.textContent = 'Aktiv';
}
start.disabled = true;
stop.disabled = false;
} else {
statusBox(data.message || 'Auto-Start fehlgeschlagen', 'warn');
}
};
stop.onclick = async () => {
const res = await fetch('/api/soundboard/auto_stop', { method: 'POST' });
const data = await res.json();
if (data.success) {
status.textContent = 'Aus';
start.disabled = false;
stop.disabled = true;
}
};
}
async function updateAutoStatus() {
const res = await fetch('/api/soundboard/status');
const data = await res.json();
const status = document.getElementById('auto-status');
const start = document.getElementById('auto-start');
const stop = document.getElementById('auto-stop');
if (!status || !start || !stop) return;
if (data.active) {
if (data.next_seconds !== null && data.next_seconds !== undefined) {
status.textContent = `Aktiv (nächster in ${(data.next_seconds/60).toFixed(1)} min)`;
} else {
status.textContent = 'Aktiv';
}
start.disabled = true;
stop.disabled = false;
// optional: Formularwerte mit Serverparametern füllen
if (data.interval_min) document.getElementById('auto-interval-min').value = data.interval_min;
if (data.interval_max) document.getElementById('auto-interval-max').value = data.interval_max;
if (data.delay_min !== undefined && data.delay_min !== null) document.getElementById('auto-delay-min').value = data.delay_min;
if (data.delay_max !== undefined && data.delay_max !== null) document.getElementById('auto-delay-max').value = data.delay_max;
} else {
status.textContent = 'Aus';
start.disabled = false;
stop.disabled = true;
}
}
async function playSound(sound) {
const payload = {
sound_id: sound.id || sound.file,
channel: sound.channel ?? null,
loop: !!sound.loop
};
const res = await fetch('/api/play_sound', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(payload)
});
const data = await res.json();
if (!data.success) statusBox(data.message || 'Fehler beim Abspielen', 'warn');
}
async function stopSound(channel) {
const res = await fetch('/api/stop_sound', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ channel })
});
const data = await res.json();
if (!data.success) statusBox(data.message || 'Fehler beim Stoppen', 'warn');
}
loadBtn.onclick = () => loadSoundboard(selectEl.value);
randBtn.onclick = async () => {
await playRandom();
};
async function playRandom() {
const res = await fetch('/api/soundboard/play_random', { method: 'POST' });
const data = await res.json();
if (!data.success) statusBox(data.message || 'Random fehlgeschlagen', 'warn');
}
// Lautstärke
const vol = document.getElementById('sb-volume');
if (vol) {
vol.addEventListener('input', async () => {
const v = parseInt(vol.value, 10) / 100;
await fetch('/api/set_volume', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ volume: v })
});
});
}
// Autoload erstes Thema (falls vorhanden)
// Reihenfolge: Status prüfen, dann ggf. Theme laden
(async () => {
await updateAutoStatus();
if (selectEl && selectEl.value) {
// Wenn bereits ein Theme aktiv ist, nicht erneut laden (würde Auto stoppen)
const statusRes = await fetch('/api/soundboard/status');
const statusData = await statusRes.json();
if (!statusData.active || statusData.current_theme !== selectEl.value) {
loadSoundboard(selectEl.value);
} else {
// Theme-Daten nachladen ohne Auto zu stoppen
const cur = await fetch('/api/soundboard/current');
const curData = await cur.json();
if (curData.soundboard) {
currentSB = curData.soundboard;
renderSoundboard(currentSB);
content.style.display = 'block';
}
}
}
})();
</script>
{% endblock %}