added themes to soundboard
This commit is contained in:
+128
-95
@@ -1,117 +1,150 @@
|
||||
{% extends "base.html" %}
|
||||
|
||||
{% block title %}Soundboard – {{ config.name }}{% endblock %}
|
||||
{% block title %}Soundboard – Themen{% endblock %}
|
||||
|
||||
{% block content %}
|
||||
<div class="container my-5">
|
||||
<h1>Soundboard – {{ config.name }}</h1>
|
||||
<p class="lead mb-4">Wähle einen Sound aus – wird direkt über den Raspberry Pi ausgegeben.</p>
|
||||
<h1>Soundboard</h1>
|
||||
<p class="lead mb-4">Themenbezogene Soundsets, unabhängig vom Hub.</p>
|
||||
|
||||
{% set has_local = sounds_local and sounds_local|length > 0 %}
|
||||
{% set has_global = sounds_global and sounds_global|length > 0 %}
|
||||
|
||||
{% if has_local %}
|
||||
<h4 class="mt-3 mb-3">Lok-spezifische Sounds</h4>
|
||||
<div class="row g-3">
|
||||
{% for sound in sounds_local %}
|
||||
<div class="col-md-4 col-lg-3">
|
||||
<div class="card h-100 shadow-sm">
|
||||
<div class="card-body text-center">
|
||||
<h5 class="card-title">{{ sound.name }}</h5>
|
||||
{% if sound.description %}
|
||||
<p class="card-text text-muted small">{{ sound.description }}</p>
|
||||
{% endif %}
|
||||
<button class="btn btn-primary mt-3 play-sound-btn"
|
||||
data-sound-id="{{ sound.id }}"
|
||||
data-channel="{{ sound.channel | default('') }}"
|
||||
data-loop="{{ 1 if sound.loop else 0 }}">
|
||||
<i class="bi bi-play-fill me-2"></i> Abspielen
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{% endfor %}
|
||||
<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>
|
||||
{% endif %}
|
||||
|
||||
{% if has_global %}
|
||||
<h4 class="mt-5 mb-3">Standard-Sounds</h4>
|
||||
<div class="row g-3">
|
||||
{% for sound in sounds_global %}
|
||||
<div class="col-md-4 col-lg-3">
|
||||
<div class="card h-100 shadow-sm">
|
||||
<div class="card-body text-center">
|
||||
<h5 class="card-title">{{ sound.name }}</h5>
|
||||
{% if sound.description %}
|
||||
<p class="card-text text-muted small">{{ sound.description }}</p>
|
||||
{% endif %}
|
||||
<button class="btn btn-outline-secondary mt-3 play-sound-btn"
|
||||
data-sound-id="{{ sound.id }}"
|
||||
data-channel="{{ sound.channel | default('') }}"
|
||||
data-loop="{{ 1 if sound.loop else 0 }}">
|
||||
<i class="bi bi-play-fill me-2"></i> Abspielen
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{% endfor %}
|
||||
<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>Zufällig (max 2/h pro Datei)</button>
|
||||
</div>
|
||||
</div>
|
||||
{% endif %}
|
||||
</div>
|
||||
|
||||
{% if not has_local and not has_global %}
|
||||
<div class="alert alert-info">
|
||||
In dieser Konfiguration sind keine Sounds definiert und keine Default-Sounds vorhanden.
|
||||
</div>
|
||||
{% endif %}
|
||||
<div id="sb-content" style="display:none">
|
||||
<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>
|
||||
|
||||
<div class="mt-5 text-center">
|
||||
<a href="{{ url_for('main.control_page') }}" class="btn btn-secondary">Zurück zur Steuerung</a>
|
||||
<div class="mt-4">
|
||||
<a href="{{ url_for('main.index') }}" class="btn btn-secondary">Zurück</a>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{% endblock %}
|
||||
|
||||
{% block scripts %}
|
||||
<script>
|
||||
document.querySelectorAll('.play-sound-btn').forEach(btn => {
|
||||
btn.addEventListener('click', async () => {
|
||||
const soundId = btn.dataset.soundId;
|
||||
const channel = btn.dataset.channel || null;
|
||||
const loopFlag = btn.dataset.loop === '1' || btn.dataset.loop === 'true';
|
||||
btn.disabled = true;
|
||||
btn.innerHTML = '<span class="spinner-border spinner-border-sm me-2"></span> Spielt...';
|
||||
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');
|
||||
|
||||
try {
|
||||
const res = await fetch('/api/play_sound', {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify({ sound_id: soundId, channel, loop: loopFlag })
|
||||
});
|
||||
let currentSB = null;
|
||||
|
||||
const data = await res.json();
|
||||
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;
|
||||
}
|
||||
}
|
||||
|
||||
if (data.success) {
|
||||
btn.classList.remove('btn-primary');
|
||||
btn.classList.add('btn-success');
|
||||
btn.innerHTML = '<i class="bi bi-check-lg me-2"></i> Gespielt';
|
||||
setTimeout(() => {
|
||||
btn.classList.remove('btn-success');
|
||||
btn.classList.add('btn-primary');
|
||||
btn.innerHTML = '<i class="bi bi-play-fill me-2"></i> Abspielen';
|
||||
btn.disabled = false;
|
||||
}, 2000);
|
||||
} else {
|
||||
alert('Fehler: ' + (data.message || 'Unbekannt'));
|
||||
btn.disabled = false;
|
||||
btn.innerHTML = '<i class="bi bi-play-fill me-2"></i> Abspielen';
|
||||
}
|
||||
} catch (err) {
|
||||
console.error('Sound-Play-Fehler:', err);
|
||||
alert('Netzwerkfehler beim Abspielen');
|
||||
btn.disabled = false;
|
||||
btn.innerHTML = '<i class="bi bi-play-fill me-2"></i> Abspielen';
|
||||
}
|
||||
function renderSoundboard(sb) {
|
||||
renderBackgrounds(sb.backgrounds || []);
|
||||
renderSounds(sb.sounds || []);
|
||||
renderRandom(sb.random_pool || []);
|
||||
}
|
||||
|
||||
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; }
|
||||
container.innerHTML = '<p class="text-muted">Zufallspool: ' + list.length + ' Dateien, max 2x pro Stunde je Datei.</p>';
|
||||
}
|
||||
|
||||
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) alert(data.message || 'Fehler beim Abspielen');
|
||||
}
|
||||
|
||||
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) alert(data.message || 'Fehler beim Stoppen');
|
||||
}
|
||||
|
||||
loadBtn.onclick = () => loadSoundboard(selectEl.value);
|
||||
randBtn.onclick = async () => {
|
||||
const res = await fetch('/api/soundboard/play_random', { method: 'POST' });
|
||||
const data = await res.json();
|
||||
if (!data.success) alert(data.message || 'Random fehlgeschlagen');
|
||||
};
|
||||
|
||||
// Autoload erstes Thema (falls vorhanden)
|
||||
if (selectEl && selectEl.value) {
|
||||
loadSoundboard(selectEl.value);
|
||||
}
|
||||
</script>
|
||||
{% endblock %}
|
||||
|
||||
Reference in New Issue
Block a user