diff --git a/app.py b/app.py index 5ffe70e..bf46cf7 100644 --- a/app.py +++ b/app.py @@ -1,6 +1,7 @@ from __future__ import annotations import os +import time from pathlib import Path from flask import Flask, abort, jsonify, redirect, render_template, request, send_from_directory, url_for @@ -17,6 +18,17 @@ ALLOWED_EXTENSIONS = { app = Flask(__name__) app.config["UPLOAD_FOLDER"] = str(UPLOAD_DIR) +APP_VERSION = "1.0.0" + + +@app.context_processor +def inject_globals(): + return { + "app_year": time.localtime().tm_year, + "app_version": APP_VERSION, + "app_host": request.host.split(":")[0], + } + # ── Player State ────────────────────────────────────────────────────── player_state = { @@ -27,6 +39,9 @@ player_state = { "volume": 1.0, } +_display_last_seen = 0.0 +DISPLAY_TIMEOUT = 8 + def _bump(): player_state["version"] += 1 @@ -102,7 +117,9 @@ def controller(): # ── Player API ──────────────────────────────────────────────────────── @app.route("/api/state") def api_state(): - return jsonify(player_state) + resp = dict(player_state) + resp["display_online"] = (time.time() - _display_last_seen) < DISPLAY_TIMEOUT + return jsonify(resp) @app.route("/api/play", methods=["POST"]) @@ -179,6 +196,13 @@ def api_seek_ack(): return jsonify({"ok": True}) +@app.route("/api/display/ping", methods=["POST"]) +def api_display_ping(): + global _display_last_seen + _display_last_seen = time.time() + return jsonify({"ok": True}) + + @app.route("/api/volume", methods=["POST"]) def api_volume(): data = request.get_json(silent=True) or {} diff --git a/templates/controller.html b/templates/controller.html index 083f31d..aaa7405 100644 --- a/templates/controller.html +++ b/templates/controller.html @@ -150,8 +150,12 @@ color: var(--ccm-text); } .media-list .list-group-item.active { - background: var(--ccm-primary); - border-color: var(--ccm-primary); + background: #e9ecef; + border-color: #d9dee3; + } + [data-bs-theme="dark"] .media-list .list-group-item.active { + background: #2b3440; + border-color: #3a4555; } .upload-dropzone { border: 1.5px dashed var(--ccm-border); @@ -202,9 +206,31 @@ .control-btn { min-width: 3.2rem; } + .app-footer { + margin-top: 1rem; + padding: 0.65rem 1.5rem 0.95rem; + color: var(--ccm-muted); + font-size: 0.82rem; + background: transparent; + } + .app-footer-inner { + width: 100%; + max-width: 1320px; + margin: 0 auto; + display: flex; + flex-wrap: wrap; + gap: 0.2rem 0.75rem; + justify-content: flex-start; + align-items: center; + text-align: left; + } + .app-footer-separator { + color: #b0b7c3; + } @media (max-width: 575.98px) { .control-btn { min-width: 2.8rem; font-size: 0.85rem; } .control-btn i { font-size: 1.1rem; } + .app-footer-inner { gap: 0.1rem 0.5rem; font-size: 0.75rem; } } @@ -217,8 +243,8 @@