konfigurierbare farben und texte
This commit is contained in:
+100
-52
@@ -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,
|
||||
)
|
||||
)
|
||||
|
||||
Reference in New Issue
Block a user