konfigurierbare farben und texte

This commit is contained in:
Bernd Reuther
2026-09-30 18:30:05 +02:00
parent 3a94fe829b
commit b87f34a9ca
4 changed files with 337 additions and 112 deletions
+100 -52
View File
@@ -3,13 +3,14 @@ import signal
import subprocess
import threading
import json
import re
import time
from datetime import datetime
from pathlib import Path
from zoneinfo import ZoneInfo
import requests
from flask import Flask, render_template_string, send_from_directory, abort, Response, request, stream_with_context
from flask import Flask, render_template_string, send_from_directory, abort, Response, request, stream_with_context, send_file
# ---------------------------------------------------------------------------
@@ -48,21 +49,51 @@ WEB_PORT = int(
os.environ.get("WEB_PORT", "8080")
)
TIMEZONE = ZoneInfo(
os.environ.get("TIMEZONE", "Europe/Berlin")
)
# ---------------------------------------------------------------------------
# Branding / theme configuration
START_DELAY = float(
os.environ.get("START_DELAY", "3")
)
SITE_NAME = os.environ.get("SITE_NAME", "Event Audio")
SITE_SUBTITLE = os.environ.get("SITE_SUBTITLE", "Audiostream-Aufzeichnungen")
PAGE_TITLE = os.environ.get("PAGE_TITLE", "Event Audio – Aufnahmen")
PAGE_HEADING = os.environ.get("PAGE_HEADING", "EVENT AUDIO – Aufnahmen")
THEME_FILE = Path(os.environ.get("THEME_FILE", "/config/theme.json"))
LOGO_FILE = Path(os.environ["LOGO_FILE"]) if os.environ.get("LOGO_FILE") else None
FAVICON_FILE = Path(os.environ["FAVICON_FILE"]) if os.environ.get("FAVICON_FILE") else None
STOP_DELAY = float(
os.environ.get("STOP_DELAY", "10")
)
DEFAULT_THEME = {'background': '#111', 'surface': '#181818', 'surface_alt': '#222', 'surface_dark': '#0b0b0b', 'text': '#eee', 'text_secondary': '#ccc', 'text_muted': '#aaa', 'text_faint': '#777', 'border': '#333', 'border_subtle': '#2b2b2b', 'control': '#333', 'control_hover': '#444', 'primary': '#1769aa', 'primary_hover': '#1f7fc2', 'accent': '#ffcc33', 'success': '#173d24', 'success_text': '#b8f5c9', 'warning': '#6b4e00', 'danger': '#8b0000', 'danger_soft': '#4a2020', 'danger_soft_hover': '#6a2b2b', 'error': '#4a1b1b', 'error_text': '#ffd0d0', 'recording': '#b00000', 'recording_border': '#3a2222', 'selection': 'rgba(80,150,255,.16)', 'shadow': '#000', 'scroll_thumb': '#777', 'scroll_thumb_hover': '#999', 'scroll_thumb_active': '#aaa', 'live_surface': '#1d1111', 'table_header': '#252525', 'footer': '#666', 'white': '#fff','surface_modal':'#202020','overlay':'rgba(0,0,0,.78)'}
POLL_INTERVAL = float(
os.environ.get("POLL_INTERVAL", "2")
)
def load_theme():
"""Load semantic theme colors from JSON, falling back to built-in defaults."""
theme = dict(DEFAULT_THEME)
if THEME_FILE.is_file():
try:
data = json.loads(THEME_FILE.read_text(encoding="utf-8"))
colors = data.get("colors", data) if isinstance(data, dict) else {}
if isinstance(colors, dict):
for key in theme:
value = colors.get(key)
if isinstance(value, str) and re.fullmatch(r"(?:#[0-9a-fA-F]{3,8}|rgba?\([^)]*\))", value.strip()):
theme[key] = value.strip()
except Exception as exc:
print(f"[branding] Theme konnte nicht geladen werden: {exc}", flush=True)
return theme
def theme_css():
theme = load_theme()
return ":root {\n" + "\n".join(
f" --color-{key.replace('_', '-')}: {value};" for key, value in theme.items()
) + "\n}"
def branding_context():
return {
"site_name": SITE_NAME,
"site_subtitle": SITE_SUBTITLE,
"page_title": PAGE_TITLE,
"page_heading": PAGE_HEADING,
"theme_css": theme_css(),
"logo_url": "/branding/logo" if LOGO_FILE and LOGO_FILE.is_file() else None,
"favicon_url": "/branding/favicon" if FAVICON_FILE and FAVICON_FILE.is_file() else None,
}
NORMALIZE = os.environ.get("NORMALIZE", "true").lower() not in ("0", "false", "no")
NORMALIZE_TARGET = os.environ.get("NORMALIZE_TARGET", "-16")
@@ -569,18 +600,21 @@ HTML = """
<title>Event Audio – Aufnahmen</title>
<title>{{ page_title }}</title>
{% if favicon_url %}<link rel="icon" href="{{ favicon_url }}">{% endif %}
<style>
{{ theme_css|safe }}
* {
box-sizing: border-box;
}
.site-logo { max-width:220px; max-height:72px; object-fit:contain; display:block; margin-bottom:12px; }
body {
margin: 0;
padding: 24px;
background: #111;
color: #eee;
background: var(--color-background);
color: var(--color-text);
font-family: Arial, Helvetica, sans-serif;
}
@@ -590,7 +624,7 @@ h1 {
}
.subtitle {
color: #aaa;
color: var(--color-text-muted);
margin-bottom: 24px;
}
@@ -603,23 +637,23 @@ h1 {
}
.status.recording {
background: #8b0000;
color: white;
background: var(--color-danger);
color:var(--color-white);
}
.status.idle {
background: #333;
color: #ccc;
background: var(--color-border);
color: var(--color-text-secondary);
}
.status.error {
background: #6b4e00;
color: white;
background: var(--color-warning);
color:var(--color-white);
}
.recording {
border: 1px solid #b00000;
background: #1d1111;
border: 1px solid var(--color-recording);
background: var(--color-live-surface);
border-radius: 8px;
padding: 16px;
margin-bottom: 24px;
@@ -632,15 +666,15 @@ h1 {
}
.recording-info {
color: #bbb;
color: var(--color-text-secondary);
margin-bottom: 12px;
}
.live-panel {
display: flex;
gap: 16px;
border: 1px solid #b00000;
background: #1d1111;
border: 1px solid var(--color-recording);
background: var(--color-live-surface);
border-radius: 8px;
padding: 16px;
margin-bottom: 24px;
@@ -652,7 +686,7 @@ h1 {
}
.live-item + .live-item {
border-left: 1px solid #3a2222;
border-left: 1px solid var(--color-recording-border);
padding-left: 16px;
}
@@ -663,7 +697,7 @@ h1 {
}
.live-info {
color: #aaa;
color: var(--color-text-muted);
font-size: 14px;
margin-bottom: 10px;
}
@@ -676,7 +710,7 @@ h1 {
}
.filter-bar label {
color: #aaa;
color: var(--color-text-muted);
font-size: 13px;
}
@@ -685,9 +719,9 @@ h1 {
width: 100%;
margin-top: 5px;
padding: 9px;
background: #111;
color: #eee;
border: 1px solid #555;
background: var(--color-background);
color: var(--color-text);
border: 1px solid var(--color-control-hover);
border-radius: 5px;
font-size: 15px;
}
@@ -697,7 +731,7 @@ h1 {
}
.filter-info {
color: #999;
color: var(--color-text-muted);
font-size: 13px;
margin-bottom: 10px;
}
@@ -705,21 +739,21 @@ h1 {
table {
width: 100%;
border-collapse: collapse;
background: #181818;
background: var(--color-surface);
border-radius: 8px;
overflow: hidden;
}
th {
text-align: left;
background: #252525;
background: var(--color-table-header);
padding: 12px;
color: #ccc;
color: var(--color-text-secondary);
}
td {
padding: 12px;
border-top: 1px solid #2b2b2b;
border-top: 1px solid var(--color-border-subtle);
vertical-align: middle;
}
@@ -744,25 +778,25 @@ a.button {
display: inline-block;
padding: 8px 12px;
margin-left: 8px;
background: #333;
color: #fff;
background: var(--color-border);
color: var(--color-white);
text-decoration: none;
border-radius: 5px;
}
a.button:hover {
background: #444;
background: var(--color-control-hover);
}
.empty {
padding: 30px;
text-align: center;
color: #777;
color: var(--color-text-faint);
}
.footer {
margin-top: 24px;
color: #666;
color: var(--color-footer);
font-size: 13px;
}
@@ -790,7 +824,7 @@ a.button:hover {
}
tr {
border-bottom: 1px solid #333;
border-bottom: 1px solid var(--color-border);
padding: 12px 0;
}
@@ -809,7 +843,7 @@ a.button:hover {
.live-item + .live-item {
border-left: 0;
border-top: 1px solid #3a2222;
border-top: 1px solid var(--color-recording-border);
padding-left: 0;
padding-top: 16px;
margin-top: 16px;
@@ -825,11 +859,10 @@ a.button:hover {
<body>
<h1>EVENT AUDIO – Aufnahmen</h1>
{% if logo_url %}<img class="site-logo" src="{{ logo_url }}" alt="{{ site_name }}">{% endif %}
<h1>{{ page_heading }}</h1>
<div class="subtitle">
Audiostream-Aufzeichnungen
</div>
<div class="subtitle">{{ site_subtitle }}</div>
<div id="dynamic-content">
@@ -1219,9 +1252,24 @@ def render_index():
normalizing_filename=normalizing_filename,
status_error=status_error,
icecast_public_url=ICECAST_PUBLIC_URL,
**branding_context(),
)
@app.route("/branding/logo")
def branding_logo():
if LOGO_FILE is None or not LOGO_FILE.is_file():
abort(404)
return send_file(LOGO_FILE)
@app.route("/branding/favicon")
def branding_favicon():
if FAVICON_FILE is None or not FAVICON_FILE.is_file():
abort(404)
return send_file(FAVICON_FILE)
@app.route("/")
def index():
return render_index()
@@ -1380,4 +1428,4 @@ if __name__ == "__main__":
app.run(
host="0.0.0.0",
port=WEB_PORT,
)
)