diff --git a/README.md b/README.md index 8cf294d..eb6c324 100644 --- a/README.md +++ b/README.md @@ -5,7 +5,10 @@ A REST API server for controlling system media playback on Windows, Linux, macOS ## Features - **Built-in Web UI** for real-time media control and monitoring +- **Installable PWA** - Add to home screen on mobile for a native app experience +- **Audio Visualizer** - Real-time spectrum analyzer with beat-reactive album art effects - **Media Browser** - Browse and play media files from configured folders +- **Display Control** - Monitor brightness and power management - **Quick Actions & Scripts** - Execute custom scripts with one click - **Callbacks** - Trigger commands on media events (play, pause, volume, etc.) - Control any media player via system-wide media transport controls @@ -36,10 +39,13 @@ The media server includes a built-in web interface for controlling and monitorin - **Mini player** - Sticky compact player that appears when scrolling away from the main player - **Connection status indicator** - Know when you're connected - **Token authentication** - Saved in browser localStorage -- **Responsive design** - Works on desktop and mobile -- **Dark and light themes** - Toggle between dark and light modes -- **Accent color picker** - Choose from 9 preset accent colors (green, blue, purple, pink, orange, red, teal, cyan, yellow) -- **Tab-based navigation** - Player, Browser, Quick Actions, Scripts, and Callbacks tabs +- **Audio spectrum visualizer** - Real-time frequency bars with beat-reactive album art scaling and glow (on-demand WASAPI loopback capture) +- **Display control** - Monitor brightness adjustment and power on/off +- **Installable PWA** - Add to home screen on mobile/desktop for standalone app experience with safe area support for notched phones +- **Responsive design** - Works on desktop, tablet, and mobile +- **Dark and light themes** - Toggle between dark and light modes with dynamic status bar theming +- **Accent color picker** - Choose from 9 preset accent colors or pick a custom color +- **Tab-based navigation** - Player, Display, Browser, Quick Actions, and Settings tabs - **Multi-language support** - English and Russian locales with automatic detection ### Accessing the Web UI @@ -58,6 +64,29 @@ The media server includes a built-in web interface for controlling and monitorin 4. Start playing media in any supported player and watch the UI update in real-time! +### Installing as a PWA + +The Web UI can be installed as a Progressive Web App for a native app-like experience: + +1. Open the Web UI in Chrome/Edge on your phone or desktop +2. Tap the **Install** icon in the address bar (or "Add to Home Screen" on mobile) +3. The app launches in standalone mode — no browser chrome, with proper safe area handling for notched phones + +### Audio Visualizer + +The Web UI includes a real-time audio spectrum visualizer that captures system audio output: + +- **On-demand capture** - Audio capture starts only when a client enables the visualizer, and stops when the last client disconnects +- **Beat-reactive effects** - Album art pulses and glows in response to bass frequencies +- **Configurable device** - Select which audio output device to capture in Settings + +Requires `soundcard` and `numpy` Python packages. Enable in `config.yaml`: + +```yaml +visualizer_enabled: true +# visualizer_device: "Speakers" # optional: specific device name +``` + ### Remote Access To access the Web UI from other devices on your network: diff --git a/media_server/main.py b/media_server/main.py index 598d0a0..8c396f0 100644 --- a/media_server/main.py +++ b/media_server/main.py @@ -154,6 +154,15 @@ def create_app() -> FastAPI: # Mount static files and serve UI at root static_dir = Path(__file__).parent / "static" if static_dir.exists(): + @app.get("/sw.js", include_in_schema=False) + async def serve_service_worker(): + """Serve service worker from root scope for PWA installability.""" + return FileResponse( + static_dir / "sw.js", + media_type="application/javascript", + headers={"Cache-Control": "no-cache"}, + ) + app.mount("/static", StaticFiles(directory=str(static_dir)), name="static") @app.get("/", include_in_schema=False) diff --git a/media_server/static/css/styles.css b/media_server/static/css/styles.css index f5d291e..983e22d 100644 --- a/media_server/static/css/styles.css +++ b/media_server/static/css/styles.css @@ -3067,6 +3067,10 @@ footer .separator { padding-top: calc(0.5rem + 2px); } + .mini-nav-btn { + display: none; + } + .mini-player-info { min-width: 120px; } @@ -3164,3 +3168,72 @@ footer .separator { justify-content: flex-start; } } + +/* ======================================== + PWA Standalone & Mobile Polish + ======================================== */ + +html { + overscroll-behavior: none; +} + +/* Safe area insets for notched phones (viewport-fit=cover) */ +.container { + padding-left: max(0.75rem, env(safe-area-inset-left)); + padding-right: max(0.75rem, env(safe-area-inset-right)); +} + +.mini-player { + padding-bottom: max(0.75rem, env(safe-area-inset-bottom)); + padding-left: max(1rem, env(safe-area-inset-left)); + padding-right: max(1rem, env(safe-area-inset-right)); +} + +footer { + padding-bottom: max(0.75rem, env(safe-area-inset-bottom)); +} + +body.mini-player-visible footer { + padding-bottom: calc(70px + env(safe-area-inset-bottom, 0px)); +} + +.connection-banner { + padding-top: max(10px, env(safe-area-inset-top)); +} + +/* Touch optimization: eliminate 300ms tap delay */ +.controls button, +.mini-controls button, +.mini-control-btn, +.tab-btn, +.header-btn, +.header-link, +.mute-btn, +.vinyl-toggle-btn, +.view-toggle-btn, +.browser-item, +.browser-list-item, +.script-btn, +.action-btn { + touch-action: manipulation; +} + +@media (max-width: 600px) { + .container { + padding-left: max(0.5rem, env(safe-area-inset-left)); + padding-right: max(0.5rem, env(safe-area-inset-right)); + } + + .mini-player { + padding-bottom: max(0.5rem, env(safe-area-inset-bottom)); + padding-left: max(0.75rem, env(safe-area-inset-left)); + padding-right: max(0.75rem, env(safe-area-inset-right)); + } +} + +@media (display-mode: standalone) { + body { + overscroll-behavior-y: none; + -webkit-overflow-scrolling: touch; + } +} diff --git a/media_server/static/icons/icon.svg b/media_server/static/icons/icon.svg new file mode 100644 index 0000000..d26b69a --- /dev/null +++ b/media_server/static/icons/icon.svg @@ -0,0 +1,10 @@ + diff --git a/media_server/static/index.html b/media_server/static/index.html index 8f54bd7..e45941f 100644 --- a/media_server/static/index.html +++ b/media_server/static/index.html @@ -2,9 +2,16 @@
- +