From a21d2ccbc4c18397ae8561dc7c0e5836fd67d9d7 Mon Sep 17 00:00:00 2001 From: bernd Date: Fri, 2 Oct 2026 22:08:20 +0200 Subject: [PATCH] editor.py aktualisiert --- editor.py | 284 +++++++++++++++++++++++++++++++++++++++++++++++++----- 1 file changed, 259 insertions(+), 25 deletions(-) diff --git a/editor.py b/editor.py index 11376e4..0eb5f65 100644 --- a/editor.py +++ b/editor.py @@ -28,23 +28,47 @@ WEB_PORT = int(os.environ.get("WEB_PORT", "8080")) # --------------------------------------------------------------------------- # Branding / theme configuration -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 – Editor") -PAGE_HEADING = os.environ.get("PAGE_HEADING", "Event Audio – Editor") -EDIT_PAGE_TITLE = os.environ.get("EDIT_PAGE_TITLE", "Event Audio – Aufnahme bearbeiten") -EDIT_PAGE_HEADING = os.environ.get("EDIT_PAGE_HEADING", "Aufnahme bearbeiten") 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 -GIT_REPO_URL = os.environ.get("GIT_REPO_URL", "") -USER_URL = os.environ.get("USER_URL", "") +SETTINGS_FILE = Path(os.environ.get("SETTINGS_FILE", "/config/settings.json")) + +DEFAULT_SETTINGS = { + "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 – Editor"), + "page_heading": os.environ.get("PAGE_HEADING", "Event Audio – Editor"), + "edit_page_title": os.environ.get("EDIT_PAGE_TITLE", "Event Audio – Aufnahme bearbeiten"), + "edit_page_heading": os.environ.get("EDIT_PAGE_HEADING", "Aufnahme bearbeiten"), + "git_repo_url": os.environ.get("GIT_REPO_URL", ""), + "user_url": os.environ.get("USER_URL", ""), + "editor_url": os.environ.get("EDITOR_URL", ""), + "logo_file": os.environ.get("LOGO_FILE", ""), + "favicon_file": os.environ.get("FAVICON_FILE", ""), +} 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', 'table_text': '#eee', 'footer': '#666', 'white': '#fff','surface_modal':'#202020','overlay':'rgba(0,0,0,.78)'} +def load_settings(): + """Load UI/branding settings, falling back to environment defaults.""" + settings = dict(DEFAULT_SETTINGS) + if SETTINGS_FILE.is_file(): + try: + data = json.loads(SETTINGS_FILE.read_text(encoding="utf-8")) + if isinstance(data, dict): + for key in DEFAULT_SETTINGS: + if key in data and isinstance(data[key], str): + settings[key] = data[key] + if isinstance(data.get("colors"), dict): + settings["colors"] = dict(data["colors"]) + except Exception as exc: + print(f"[branding] Einstellungen konnten nicht geladen werden: {exc}", flush=True) + return settings + + def load_theme(): - """Load semantic theme colors from JSON, falling back to built-in defaults.""" + """Load semantic theme colors from settings, then theme.json, then defaults.""" theme = dict(DEFAULT_THEME) + + # Keep the existing theme.json as a backwards-compatible fallback. if THEME_FILE.is_file(): try: data = json.loads(THEME_FILE.read_text(encoding="utf-8")) @@ -56,29 +80,50 @@ def load_theme(): theme[key] = value.strip() except Exception as exc: print(f"[branding] Theme konnte nicht geladen werden: {exc}", flush=True) + + settings = load_settings() + colors = settings.get("colors") if isinstance(settings.get("colors"), dict) else {} + 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() 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(): + settings = load_settings() + logo_file = Path(settings["logo_file"]) if settings.get("logo_file") else None + favicon_file = Path(settings["favicon_file"]) if settings.get("favicon_file") else None return { - "site_name": SITE_NAME, - "site_subtitle": SITE_SUBTITLE, - "page_title": PAGE_TITLE, - "page_heading": PAGE_HEADING, - "edit_page_title": EDIT_PAGE_TITLE, - "edit_page_heading": EDIT_PAGE_HEADING, + "site_name": settings["site_name"], + "site_subtitle": settings["site_subtitle"], + "page_title": settings["page_title"], + "page_heading": settings["page_heading"], + "edit_page_title": settings["edit_page_title"], + "edit_page_heading": settings["edit_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, - "git_repo_url": GIT_REPO_URL, - "user_url": USER_URL, + "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, + "git_repo_url": settings["git_repo_url"], + "user_url": settings["user_url"], + "editor_url": settings["editor_url"], } + +def save_settings(data): + """Atomically persist the editable page configuration.""" + SETTINGS_FILE.parent.mkdir(parents=True, exist_ok=True) + temp = SETTINGS_FILE.with_name(SETTINGS_FILE.name + ".tmp") + temp.write_text(json.dumps(data, ensure_ascii=False, indent=2) + "\n", encoding="utf-8") + os.replace(temp, SETTINGS_FILE) + NORMALIZE = os.environ.get("NORMALIZE", "true").lower() not in ("0", "false", "no") NORMALIZE_TARGET = os.environ.get("NORMALIZE_TARGET", "-16") NORMALIZE_TRUE_PEAK = os.environ.get("NORMALIZE_TRUE_PEAK", "-1.5") @@ -1367,6 +1412,7 @@ render(); {% if user_url %} Benutzerseite {% endif %} + ⚙ Einstellungen {% endif %} @@ -1505,6 +1551,7 @@ th { color:var(--color-text-muted); font-weight:normal; }
Fertige Aufnahmen aus {{ recordings_dir }}
@@ -1853,22 +1900,209 @@ async function deleteFile(kind, filename) { """ +SETTINGS_HTML = """ + + + + + +Einstellungen – {{ site_name }} +{% if favicon_url %}{% endif %} + + + +
+
+← Editor +{% if logo_url %}{% endif %} +

Einstellungen

+
+{% if saved %}
Einstellungen wurden gespeichert.
{% endif %} +
+
+

Seiten und Branding

+
+
+
+
+
+
+
+
+
+
+
+
+
+
Logo und Favicon werden nicht hochgeladen, sondern als Pfad innerhalb des Containers angegeben.
+
+ +
+

Farbschema

+
+{% for key, label, value, picker in color_fields %} +
+
+ {% if picker %}{% else %}{% endif %} +
+{% endfor %} +
+
Erlaubt sind Hex-Farben sowie rgba()/rgb()-Werte. Änderungen werden nach dem Speichern auf Editor und Benutzerseite verwendet.
+
+ +
Abbrechen
+
+
+ + + +""" + + # --------------------------------------------------------------------------- # Routes # --------------------------------------------------------------------------- +COLOR_LABELS = { + "background": "Hintergrund", + "surface": "Fläche", + "surface_alt": "Alternative Fläche", + "surface_dark": "Dunkle Fläche", + "text": "Text", + "text_secondary": "Sekundärtext", + "text_muted": "Gedämpfter Text", + "text_faint": "Schwacher Text", + "border": "Rahmen", + "border_subtle": "Feiner Rahmen", + "control": "Steuerelement", + "control_hover": "Steuerelement Hover", + "primary": "Primärfarbe", + "primary_hover": "Primärfarbe Hover", + "accent": "Akzent", + "success": "Erfolg", + "success_text": "Erfolgstext", + "warning": "Warnung", + "danger": "Gefahr", + "danger_soft": "Gefahr Fläche", + "danger_soft_hover": "Gefahr Fläche Hover", + "error": "Fehler", + "error_text": "Fehlertext", + "recording": "Aufnahme", + "recording_border": "Aufnahme Rahmen", + "selection": "Auswahl", + "shadow": "Schatten", + "scroll_thumb": "Scrollbalken", + "scroll_thumb_hover": "Scrollbalken Hover", + "scroll_thumb_active": "Scrollbalken aktiv", + "live_surface": "Live-Fläche", + "table_header": "Tabellenkopf", + "table_text": "Tabellentext", + "footer": "Footer", + "white": "Weiß", + "surface_modal": "Dialogfläche", + "overlay": "Overlay", +} + + +def build_color_fields(colors): + fields = [] + for key in DEFAULT_THEME: + value = colors.get(key, DEFAULT_THEME[key]) if isinstance(colors, dict) else DEFAULT_THEME[key] + picker = value if re.fullmatch(r"#[0-9a-fA-F]{6}", value) else None + fields.append((key, COLOR_LABELS.get(key, key), value, picker)) + return fields + +@app.route("/settings", methods=["GET", "POST"]) +def settings(): + settings = load_settings() + saved = False + + if request.method == "POST": + for key in DEFAULT_SETTINGS: + settings[key] = request.form.get(key, "").strip() + + colors = {} + current_theme = load_theme() + for key in DEFAULT_THEME: + value = request.form.get(f"color_{key}", current_theme[key]).strip() + if re.fullmatch(r"(?:#[0-9a-fA-F]{3,8}|rgba?\([^)]*\))", value): + colors[key] = value + else: + colors[key] = current_theme[key] + settings["colors"] = colors + + try: + save_settings(settings) + except OSError as exc: + return render_template_string( + SETTINGS_HTML, + settings=settings, + saved=False, + color_fields=build_color_fields(colors), + **branding_context(), + error=f"Einstellungen konnten nicht gespeichert werden: {exc}", + ), 500 + saved = True + + colors = settings.get("colors") if isinstance(settings.get("colors"), dict) else load_theme() + return render_template_string( + SETTINGS_HTML, + settings=settings, + saved=saved, + color_fields=build_color_fields(colors), + **branding_context(), + ) + + @app.route("/branding/logo") def branding_logo(): - if LOGO_FILE is None or not LOGO_FILE.is_file(): + settings = load_settings() + path = Path(settings["logo_file"]) if settings.get("logo_file") else None + if path is None or not path.is_file(): abort(404) - return send_file(LOGO_FILE) + return send_file(path) @app.route("/branding/favicon") def branding_favicon(): - if FAVICON_FILE is None or not FAVICON_FILE.is_file(): + settings = load_settings() + path = Path(settings["favicon_file"]) if settings.get("favicon_file") else None + if path is None or not path.is_file(): abort(404) - return send_file(FAVICON_FILE) + return send_file(path) @app.route("/")