tabs for sounds added

This commit is contained in:
oberon 2026-02-15 22:03:15 +01:00
parent 0eacdbf973
commit 702eba2370
6 changed files with 256 additions and 157 deletions

37
app.py
View File

@ -3,10 +3,16 @@ import os
import json import json
import logging import logging
import shutil import shutil
import threading
from logging.handlers import TimedRotatingFileHandler from logging.handlers import TimedRotatingFileHandler
from datetime import datetime, timedelta from datetime import datetime, timedelta
from flask import Flask, render_template, redirect, url_for, send_from_directory, request, jsonify from flask import Flask, render_template, redirect, url_for, send_from_directory, request, jsonify
import pygame
# pygame einmalig initialisieren (am besten global)
pygame.mixer.init(frequency=44100, size=-16, channels=2, buffer=512)
pygame.mixer.music.set_volume(0.8) # Standard 80 %
# Logging-Verzeichnis (muss VOR cleanup_old_log_dirs definiert werden!) # Logging-Verzeichnis (muss VOR cleanup_old_log_dirs definiert werden!)
LOG_DIR = 'logs' LOG_DIR = 'logs'
os.makedirs(LOG_DIR, exist_ok=True) os.makedirs(LOG_DIR, exist_ok=True)
@ -540,12 +546,6 @@ def api_reconnect():
return jsonify({"success": False, "message": str(e)}), 500 return jsonify({"success": False, "message": str(e)}), 500
import pygame
import threading
# pygame einmalig initialisieren (am besten global)
pygame.mixer.init()
@app.route('/api/play_sound', methods=['POST']) @app.route('/api/play_sound', methods=['POST'])
def api_play_sound(): def api_play_sound():
try: try:
@ -588,6 +588,31 @@ def api_play_sound():
return jsonify({"success": False, "message": str(e)}), 500 return jsonify({"success": False, "message": str(e)}), 500
@app.route('/api/set_volume', methods=['POST'])
def api_set_volume():
try:
data = request.get_json()
vol = float(data.get('volume', 0.8))
vol = max(0.0, min(1.0, vol))
pygame.mixer.music.set_volume(vol)
logger.info(f"Volume auf {vol} gesetzt")
return jsonify({"success": True})
except Exception as e:
logger.error(f"Volume-Fehler: {e}")
return jsonify({"success": False, "message": str(e)}), 500
@app.route('/api/stop_sound', methods=['POST'])
def api_stop_sound():
try:
pygame.mixer.music.stop()
logger.info("Aktueller Sound gestoppt")
return jsonify({"success": True})
except Exception as e:
logger.error(f"Stop-Sound-Fehler: {e}")
return jsonify({"success": False, "message": str(e)}), 500
if __name__ == '__main__': if __name__ == '__main__':
app.run(host='0.0.0.0', port=5000, debug=True) app.run(host='0.0.0.0', port=5000, debug=True)

View File

@ -1,22 +1,16 @@
{ {
"global_sounds": [ "global_sounds": [
{ {
"id": "pfeife_lang", "id": "dampflok-fahrt",
"file": "pfeife_lang.wav", "file": "Dampflok-mit-Waggons-schnauft-heran-und-rollt-vorüber.mp3",
"name": "Pfeife lang", "name": "Dampflok Ankunft und Vorbeifahrt",
"description": "Langgezogene Dampflok-Pfeife" "description": "Dampflok mit Waggons schnauft heran und rollt vorüber"
}, },
{ {
"id": "dampf_ausstoß", "id": "wind_western1",
"file": "dampf_ausstoß.mp3", "file": "freesound_community-wind-western-64661.mp3",
"name": "Dampfausstoß", "name": "Wind Western",
"description": "Kurzes Zischen" "description": "einfache Windgeräusche"
},
{
"id": "bahnhof_ankunft",
"file": "bahnhof_ankunft.mp3",
"name": "Bahnhofsankunft",
"description": "Stationsansage + Bremsquietschen"
} }
] ]
} }

Binary file not shown.

View File

@ -11,6 +11,9 @@ document.addEventListener('DOMContentLoaded', () => {
const stopAllBtn = document.getElementById('stop-all-btn'); const stopAllBtn = document.getElementById('stop-all-btn');
const reconnectSection = document.getElementById('reconnect-section'); const reconnectSection = document.getElementById('reconnect-section');
const reconnectBtn = document.getElementById('reconnect-btn'); const reconnectBtn = document.getElementById('reconnect-btn');
const statusBadge = document.getElementById('connection-status');
const volumeSlider = document.getElementById('volume-slider');
const volumeDisplay = document.getElementById('volume-display');
if (!connectBtn) { if (!connectBtn) {
console.warn('Nicht auf der Steuerseite Connect-Button nicht gefunden'); console.warn('Nicht auf der Steuerseite Connect-Button nicht gefunden');
@ -18,8 +21,68 @@ document.addEventListener('DOMContentLoaded', () => {
} }
// ── Config aus Template ──────────────────────────────────────────────────── // ── Config aus Template ────────────────────────────────────────────────────
const config = window.mkConfig || {}; const config = {{ config | tojson | safe }};
console.log("app.js verwendet config:", config); const globalSounds = {{ global_sounds | tojson | safe }};
window.mkConfig = config; // global für Konsistenz
console.log("Config im JS:", config);
console.log("Globale Sounds:", globalSounds);
// ── Status-Anzeige ─────────────────────────────────────────────────────────
function updateStatus(connected, message = '') {
if (!statusBadge) return;
if (connected) {
statusBadge.className = 'badge bg-success px-3 py-2 fs-6';
statusBadge.innerHTML = '<i class="bi bi-circle-fill me-2"></i> Verbunden' + (message ? ` ${message}` : '');
if (reconnectSection) reconnectSection.style.display = 'none';
} else {
statusBadge.className = 'badge bg-danger px-3 py-2 fs-6';
statusBadge.innerHTML = '<i class="bi bi-circle-fill me-2"></i> Getrennt' + (message ? ` ${message}` : '');
if (reconnectSection) reconnectSection.style.display = 'block';
}
}
// Initialer Status
updateStatus(false);
// ── Automatische Verbindungsprüfung ────────────────────────────────────────
let connectionCheckInterval = null;
let failedChecks = 0;
const MAX_FAILED_CHECKS = 3;
function startConnectionCheck() {
if (connectionCheckInterval) clearInterval(connectionCheckInterval);
connectionCheckInterval = setInterval(async () => {
try {
console.log("→ Status-Check ...");
const res = await fetch('/api/status');
const data = await res.json();
if (data.connected) {
failedChecks = 0;
updateStatus(true);
} else {
failedChecks++;
console.warn(`Status-Check fehlgeschlagen (${failedChecks}/${MAX_FAILED_CHECKS}):`, data.message);
if (failedChecks >= MAX_FAILED_CHECKS) {
updateStatus(false, data.message || 'Mehrere Checks fehlgeschlagen');
}
}
} catch (err) {
failedChecks++;
console.warn(`Status-Check Netzwerkfehler (${failedChecks}/${MAX_FAILED_CHECKS}):`, err);
if (failedChecks >= MAX_FAILED_CHECKS) {
updateStatus(false, 'Keine Antwort vom Hub/Server');
}
}
}, 6000); // 6 Sekunden
}
window.addEventListener('beforeunload', () => {
if (connectionCheckInterval) clearInterval(connectionCheckInterval);
});
// ── Connect-Button ──────────────────────────────────────────────────────── // ── Connect-Button ────────────────────────────────────────────────────────
connectBtn.addEventListener('click', async () => { connectBtn.addEventListener('click', async () => {
@ -43,16 +106,13 @@ document.addEventListener('DOMContentLoaded', () => {
controlSection.style.display = 'block'; controlSection.style.display = 'block';
reconnectSection.style.display = 'none'; reconnectSection.style.display = 'none';
// Status aktualisieren
updateStatus(true); updateStatus(true);
startConnectionCheck();
console.log("→ Rufe renderChannels() auf"); console.log("→ Rufe renderChannels() auf");
renderChannels(); renderChannels();
console.log("→ renderChannels() abgeschlossen"); console.log("→ renderChannels() abgeschlossen");
// Automatische Prüfung starten
startConnectionCheck();
} else { } else {
console.warn("→ Connect fehlgeschlagen:", result.message); console.warn("→ Connect fehlgeschlagen:", result.message);
alert('Verbindung fehlgeschlagen:\n' + (result.message || 'Unbekannter Fehler')); alert('Verbindung fehlgeschlagen:\n' + (result.message || 'Unbekannter Fehler'));
@ -179,7 +239,6 @@ document.addEventListener('DOMContentLoaded', () => {
</div> </div>
`; `;
} else { } else {
// Licht, Sound, Fogger etc. → Toggle
controlHTML = ` controlHTML = `
<label class="form-label fw-bold">${channel.name} (${channel.port})</label> <label class="form-label fw-bold">${channel.name} (${channel.port})</label>
<button class="btn btn-outline-secondary w-100 toggle-btn" <button class="btn btn-outline-secondary w-100 toggle-btn"
@ -276,10 +335,8 @@ document.addEventListener('DOMContentLoaded', () => {
} }
} catch (err) { } catch (err) {
console.error('sendControl Fehler:', err); console.error('sendControl Fehler:', err);
failedChecks++; // Bei Fehler: Verbindungsprüfung triggern
if (failedChecks >= MAX_FAILED_CHECKS) { updateStatus(false, 'Fehler beim Senden');
updateStatus(false, 'Mehrere Steuerbefehle fehlgeschlagen');
}
} }
} }
@ -291,28 +348,23 @@ document.addEventListener('DOMContentLoaded', () => {
} }
} }
// ── Status-Anzeige ─────────────────────────────────────────────────────────
const statusBadge = document.getElementById('connection-status');
function updateStatus(connected, message = '') {
if (!statusBadge) return;
if (connected) {
statusBadge.className = 'badge bg-success px-3 py-2 fs-6';
statusBadge.innerHTML = '<i class="bi bi-circle-fill me-2"></i> Verbunden';
} else {
statusBadge.className = 'badge bg-danger px-3 py-2 fs-6';
statusBadge.innerHTML = '<i class="bi bi-circle-fill me-2"></i> Getrennt' + (message ? ` ${message}` : '');
showReconnect();
}
}
// ── Soundboard Buttons ───────────────────────────────────────────────────── // ── Soundboard Buttons ─────────────────────────────────────────────────────
let currentSoundButton = null;
document.querySelectorAll('.play-sound-btn').forEach(btn => { document.querySelectorAll('.play-sound-btn').forEach(btn => {
btn.addEventListener('click', async () => { btn.addEventListener('click', async () => {
const soundId = btn.dataset.soundId; const soundId = btn.dataset.soundId;
// Alten Play-Button zurücksetzen
if (currentSoundButton && currentSoundButton !== btn) {
currentSoundButton.classList.remove('btn-success');
currentSoundButton.classList.add('btn-outline-primary', 'btn-outline-secondary');
currentSoundButton.innerHTML = currentSoundButton.dataset.originalText || '<i class="bi bi-play-fill me-2"></i> Abspielen';
}
btn.disabled = true; btn.disabled = true;
const originalText = btn.innerHTML; const originalText = btn.innerHTML;
btn.dataset.originalText = originalText;
btn.innerHTML = '<span class="spinner-border spinner-border-sm me-2"></span> Spielt...'; btn.innerHTML = '<span class="spinner-border spinner-border-sm me-2"></span> Spielt...';
try { try {
@ -328,73 +380,61 @@ document.querySelectorAll('.play-sound-btn').forEach(btn => {
btn.classList.remove('btn-outline-primary', 'btn-outline-secondary'); btn.classList.remove('btn-outline-primary', 'btn-outline-secondary');
btn.classList.add('btn-success'); btn.classList.add('btn-success');
btn.innerHTML = '<i class="bi bi-check-lg me-2"></i> Gespielt'; btn.innerHTML = '<i class="bi bi-check-lg me-2"></i> Gespielt';
setTimeout(() => { currentSoundButton = btn;
btn.classList.remove('btn-success');
btn.classList.add('btn-outline-primary'); // oder secondary
btn.innerHTML = originalText;
btn.disabled = false;
}, 2000);
} else { } else {
alert('Fehler: ' + (data.message || 'Unbekannt')); alert('Fehler: ' + (data.message || 'Unbekannt'));
btn.disabled = false;
btn.innerHTML = originalText; btn.innerHTML = originalText;
btn.disabled = false;
} }
} catch (err) { } catch (err) {
console.error('Sound-Play-Fehler:', err); console.error('Sound-Play-Fehler:', err);
alert('Netzwerkfehler beim Abspielen'); alert('Netzwerkfehler beim Abspielen');
btn.disabled = false;
btn.innerHTML = originalText; btn.innerHTML = originalText;
btn.disabled = false;
} }
}); });
}); });
// Initialer Status // ── Stop-Button für aktuellen Sound
updateStatus(false); document.querySelectorAll('.stop-sound-btn').forEach(btn => {
btn.addEventListener('click', async () => {
// ── Automatische Verbindungsprüfung ────────────────────────────────────────
let connectionCheckInterval = null;
let failedChecks = 0;
const MAX_FAILED_CHECKS = 3; // erst nach 3 Fehlschlägen in Folge rot
function startConnectionCheck() {
if (connectionCheckInterval) clearInterval(connectionCheckInterval);
connectionCheckInterval = setInterval(async () => {
try { try {
console.log("→ Status-Check ..."); const res = await fetch('/api/stop_sound', { method: 'POST' });
const res = await fetch('/api/status');
const data = await res.json(); const data = await res.json();
if (data.success) {
if (data.connected) { if (currentSoundButton) {
failedChecks = 0; currentSoundButton.classList.remove('btn-success');
updateStatus(true); currentSoundButton.classList.add('btn-outline-primary', 'btn-outline-secondary');
} else { currentSoundButton.innerHTML = currentSoundButton.dataset.originalText || '<i class="bi bi-play-fill me-2"></i> Abspielen';
failedChecks++; currentSoundButton.disabled = false;
console.warn(`Status-Check fehlgeschlagen (${failedChecks}/${MAX_FAILED_CHECKS}):`, data.message); currentSoundButton = null;
if (failedChecks >= MAX_FAILED_CHECKS) {
updateStatus(false, data.message || 'Mehrere Checks fehlgeschlagen');
} }
console.log('Sound gestoppt');
} else {
alert('Fehler: ' + data.message);
} }
} catch (err) { } catch (err) {
failedChecks++; console.error('Stop-Sound-Fehler:', err);
console.warn(`Status-Check Netzwerkfehler (${failedChecks}/${MAX_FAILED_CHECKS}):`, err); alert('Netzwerkfehler beim Stoppen');
if (failedChecks >= MAX_FAILED_CHECKS) {
updateStatus(false, 'Keine Antwort vom Hub/Server');
} }
} });
}, 6000); // 6 Sekunden aggressiver als 8 s
}
// In Connect- und Reconnect-Handler nach erfolgreichem Connect/Reconnect:
startConnectionCheck();
updateStatus(true);
window.addEventListener('beforeunload', () => {
if (connectionCheckInterval) clearInterval(connectionCheckInterval);
}); });
// Optional: Bei Seitenlade den Check starten (falls schon verbunden) // ── Volume-Regler
// startConnectionCheck(); // ← auskommentiert, da nach Connect gestartet if (volumeSlider && volumeDisplay) {
volumeSlider.addEventListener('input', () => {
const vol = volumeSlider.value / 100;
volumeDisplay.textContent = `${volumeSlider.value} %`;
try {
fetch('/api/set_volume', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ volume: vol })
}).catch(err => console.error('Volume-Set-Fehler:', err));
} catch (err) {
console.error('Volume-Fehler:', err);
}
});
}
}); });

View File

@ -14,7 +14,7 @@
</div> </div>
<div class="row"> <div class="row">
<!-- Linke Spalte: Kanal-Steuerung (8/12 auf lg) --> <!-- Linke Spalte: Kanal-Steuerung (8/12) -->
<div class="col-lg-8"> <div class="col-lg-8">
<div class="row mb-4 align-items-center"> <div class="row mb-4 align-items-center">
<div class="col-md-8"> <div class="col-md-8">
@ -65,7 +65,7 @@
</div> </div>
</div> </div>
<!-- Reconnect-Bereich wird nur bei Verbindungsverlust eingeblendet --> <!-- Reconnect-Bereich -->
<div id="reconnect-section" class="text-center mt-5" style="display: none;"> <div id="reconnect-section" class="text-center mt-5" style="display: none;">
<div class="alert alert-warning"> <div class="alert alert-warning">
<strong>Verbindung unterbrochen</strong><br> <strong>Verbindung unterbrochen</strong><br>
@ -77,45 +77,82 @@
</div> </div>
</div> </div>
<!-- Rechte Spalte: Soundboard (4/12 auf lg) --> <!-- Rechte Spalte: Soundboard (4/12) -->
<div class="col-lg-4"> <div class="col-lg-4">
<div class="card shadow sticky-top" style="top: 20px;"> <div class="card shadow sticky-top" style="top: 20px;">
<div class="card-header bg-info text-white"> <div class="card-header bg-info text-white">
<h5 class="mb-0">Soundboard</h5> <h5 class="mb-0">Soundboard</h5>
</div> </div>
<div class="card-body" style="max-height: 70vh; overflow-y: auto;"> <div class="card-body">
<!-- Volume-Regler (global) -->
<div class="mb-4">
<label for="volume-slider" class="form-label fw-bold">Lautstärke</label>
<input type="range" class="form-range" id="volume-slider" min="0" max="100" value="80" step="5">
<div class="text-center mt-1">
<span id="volume-display">80 %</span>
</div>
</div>
<!-- Tabs -->
<ul class="nav nav-tabs mb-3" id="soundTabs" role="tablist">
{% if config.sounds and config.sounds|length > 0 %}
<li class="nav-item" role="presentation">
<button class="nav-link active" id="lok-tab" data-bs-toggle="tab" data-bs-target="#lok-sounds" type="button" role="tab">
Lok-spezifisch
</button>
</li>
{% endif %}
<li class="nav-item" role="presentation">
<button class="nav-link {% if not config.sounds or config.sounds|length == 0 %}active{% endif %}" id="global-tab" data-bs-toggle="tab" data-bs-target="#global-sounds" type="button" role="tab">
Standard-Sounds
</button>
</li>
</ul>
<div class="tab-content" id="soundTabContent" style="max-height: 50vh; overflow-y: auto;">
<!-- Lok-spezifische Sounds --> <!-- Lok-spezifische Sounds -->
{% if config.sounds and config.sounds|length > 0 %} {% if config.sounds and config.sounds|length > 0 %}
<h6 class="mt-0 mb-3">Lok-spezifisch</h6> <div class="tab-pane fade show active" id="lok-sounds" role="tabpanel">
<div class="d-grid gap-2 mb-4"> <div class="d-grid gap-2">
{% for sound in config.sounds %} {% for sound in config.sounds %}
<button class="btn btn-outline-primary play-sound-btn" <div class="d-flex gap-2">
<button class="btn btn-outline-primary flex-grow-1 play-sound-btn"
data-sound-id="{{ sound.id }}"> data-sound-id="{{ sound.id }}">
{{ sound.name }} {{ sound.name }}
{% if sound.description %} {% if sound.description %}
<small class="d-block text-muted">{{ sound.description }}</small> <small class="d-block text-muted">{{ sound.description }}</small>
{% endif %} {% endif %}
</button> </button>
<button class="btn btn-outline-danger stop-sound-btn" title="Aktuellen Sound stoppen">
<i class="bi bi-stop-fill"></i>
</button>
</div>
{% endfor %} {% endfor %}
</div> </div>
</div>
{% endif %} {% endif %}
<!-- Globale / Standard-Sounds --> <!-- Globale Standard-Sounds -->
{% if global_sounds and global_sounds|length > 0 %} <div class="tab-pane fade {% if not config.sounds or config.sounds|length == 0 %}show active{% endif %}" id="global-sounds" role="tabpanel">
<h6 class="mt-4 mb-3">Standard-Sounds</h6>
<div class="d-grid gap-2"> <div class="d-grid gap-2">
{% for sound in global_sounds %} {% for sound in global_sounds %}
<button class="btn btn-outline-secondary play-sound-btn" <div class="d-flex gap-2">
<button class="btn btn-outline-secondary flex-grow-1 play-sound-btn"
data-sound-id="{{ sound.id }}"> data-sound-id="{{ sound.id }}">
{{ sound.name }} {{ sound.name }}
{% if sound.description %} {% if sound.description %}
<small class="d-block text-muted">{{ sound.description }}</small> <small class="d-block text-muted">{{ sound.description }}</small>
{% endif %} {% endif %}
</button> </button>
<button class="btn btn-outline-danger stop-sound-btn" title="Aktuellen Sound stoppen">
<i class="bi bi-stop-fill"></i>
</button>
</div>
{% endfor %} {% endfor %}
</div> </div>
{% endif %} </div>
</div>
{% if (not config.sounds or config.sounds|length == 0) and (not global_sounds or global_sounds|length == 0) %} {% if (not config.sounds or config.sounds|length == 0) and (not global_sounds or global_sounds|length == 0) %}
<p class="text-muted text-center py-4">Keine Sounds konfiguriert</p> <p class="text-muted text-center py-4">Keine Sounds konfiguriert</p>
@ -134,5 +171,8 @@
const config = {{ config | tojson | safe }}; const config = {{ config | tojson | safe }};
const globalSounds = {{ global_sounds | tojson | safe }}; const globalSounds = {{ global_sounds | tojson | safe }};
window.mkConfig = config; // falls du es global brauchst window.mkConfig = config; // falls du es global brauchst
console.log("Config im JS:", config);
console.log("Globale Sounds:", globalSounds);
</script> </script>
{% endblock %} {% endblock %}