Add PWA support: installable standalone app with safe area handling
- Service worker, manifest, and SVG icon for PWA installability - Root /sw.js route for full-scope service worker registration - Meta tags: theme-color, apple-mobile-web-app, viewport-fit=cover - Safe area insets for notched phones (container, mini-player, footer, banner) - Dynamic theme-color sync on light/dark toggle - Overscroll prevention and touch-action optimization - Hide mini-player prev/next buttons on small screens - Updated README with PWA and new feature documentation Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
37
README.md
37
README.md
@@ -5,7 +5,10 @@ A REST API server for controlling system media playback on Windows, Linux, macOS
|
|||||||
## Features
|
## Features
|
||||||
|
|
||||||
- **Built-in Web UI** for real-time media control and monitoring
|
- **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
|
- **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
|
- **Quick Actions & Scripts** - Execute custom scripts with one click
|
||||||
- **Callbacks** - Trigger commands on media events (play, pause, volume, etc.)
|
- **Callbacks** - Trigger commands on media events (play, pause, volume, etc.)
|
||||||
- Control any media player via system-wide media transport controls
|
- 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
|
- **Mini player** - Sticky compact player that appears when scrolling away from the main player
|
||||||
- **Connection status indicator** - Know when you're connected
|
- **Connection status indicator** - Know when you're connected
|
||||||
- **Token authentication** - Saved in browser localStorage
|
- **Token authentication** - Saved in browser localStorage
|
||||||
- **Responsive design** - Works on desktop and mobile
|
- **Audio spectrum visualizer** - Real-time frequency bars with beat-reactive album art scaling and glow (on-demand WASAPI loopback capture)
|
||||||
- **Dark and light themes** - Toggle between dark and light modes
|
- **Display control** - Monitor brightness adjustment and power on/off
|
||||||
- **Accent color picker** - Choose from 9 preset accent colors (green, blue, purple, pink, orange, red, teal, cyan, yellow)
|
- **Installable PWA** - Add to home screen on mobile/desktop for standalone app experience with safe area support for notched phones
|
||||||
- **Tab-based navigation** - Player, Browser, Quick Actions, Scripts, and Callbacks tabs
|
- **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
|
- **Multi-language support** - English and Russian locales with automatic detection
|
||||||
|
|
||||||
### Accessing the Web UI
|
### 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!
|
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
|
### Remote Access
|
||||||
|
|
||||||
To access the Web UI from other devices on your network:
|
To access the Web UI from other devices on your network:
|
||||||
|
|||||||
@@ -154,6 +154,15 @@ def create_app() -> FastAPI:
|
|||||||
# Mount static files and serve UI at root
|
# Mount static files and serve UI at root
|
||||||
static_dir = Path(__file__).parent / "static"
|
static_dir = Path(__file__).parent / "static"
|
||||||
if static_dir.exists():
|
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.mount("/static", StaticFiles(directory=str(static_dir)), name="static")
|
||||||
|
|
||||||
@app.get("/", include_in_schema=False)
|
@app.get("/", include_in_schema=False)
|
||||||
|
|||||||
@@ -3067,6 +3067,10 @@ footer .separator {
|
|||||||
padding-top: calc(0.5rem + 2px);
|
padding-top: calc(0.5rem + 2px);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.mini-nav-btn {
|
||||||
|
display: none;
|
||||||
|
}
|
||||||
|
|
||||||
.mini-player-info {
|
.mini-player-info {
|
||||||
min-width: 120px;
|
min-width: 120px;
|
||||||
}
|
}
|
||||||
@@ -3164,3 +3168,72 @@ footer .separator {
|
|||||||
justify-content: flex-start;
|
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;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
10
media_server/static/icons/icon.svg
Normal file
10
media_server/static/icons/icon.svg
Normal file
@@ -0,0 +1,10 @@
|
|||||||
|
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 100 100">
|
||||||
|
<defs>
|
||||||
|
<linearGradient id="grad" x1="0%" y1="0%" x2="100%" y2="100%">
|
||||||
|
<stop offset="0%" style="stop-color:#1db954;stop-opacity:1" />
|
||||||
|
<stop offset="100%" style="stop-color:#1ed760;stop-opacity:1" />
|
||||||
|
</linearGradient>
|
||||||
|
</defs>
|
||||||
|
<circle cx="50" cy="50" r="45" fill="url(#grad)"/>
|
||||||
|
<path fill="white" d="M35 25 L35 75 L75 50 Z"/>
|
||||||
|
</svg>
|
||||||
|
After Width: | Height: | Size: 421 B |
@@ -2,9 +2,16 @@
|
|||||||
<html lang="en">
|
<html lang="en">
|
||||||
<head>
|
<head>
|
||||||
<meta charset="UTF-8">
|
<meta charset="UTF-8">
|
||||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
<meta name="viewport" content="width=device-width, initial-scale=1.0, viewport-fit=cover">
|
||||||
<title>Media Server</title>
|
<title>Media Server</title>
|
||||||
<link rel="icon" type="image/svg+xml" href="data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 100 100'%3E%3Cdefs%3E%3ClinearGradient id='grad' x1='0%25' y1='0%25' x2='100%25' y2='100%25'%3E%3Cstop offset='0%25' style='stop-color:%231db954;stop-opacity:1' /%3E%3Cstop offset='100%25' style='stop-color:%231ed760;stop-opacity:1' /%3E%3C/linearGradient%3E%3C/defs%3E%3Ccircle cx='50' cy='50' r='45' fill='url(%23grad)'/%3E%3Cpath fill='white' d='M35 25 L35 75 L75 50 Z'/%3E%3C/svg%3E">
|
<meta name="description" content="Remote media player control and file browser">
|
||||||
|
<meta name="theme-color" content="#121212">
|
||||||
|
<meta name="apple-mobile-web-app-capable" content="yes">
|
||||||
|
<meta name="apple-mobile-web-app-status-bar-style" content="black-translucent">
|
||||||
|
<meta name="apple-mobile-web-app-title" content="Media Server">
|
||||||
|
<link rel="manifest" href="/static/manifest.json">
|
||||||
|
<link rel="icon" type="image/svg+xml" href="/static/icons/icon.svg">
|
||||||
|
<link rel="apple-touch-icon" href="/static/icons/icon.svg">
|
||||||
<link rel="stylesheet" href="/static/css/styles.css">
|
<link rel="stylesheet" href="/static/css/styles.css">
|
||||||
</head>
|
</head>
|
||||||
<body class="loading-translations">
|
<body class="loading-translations">
|
||||||
|
|||||||
@@ -10,6 +10,11 @@ window.addEventListener('DOMContentLoaded', async () => {
|
|||||||
initTheme();
|
initTheme();
|
||||||
initAccentColor();
|
initAccentColor();
|
||||||
|
|
||||||
|
// Register service worker for PWA installability
|
||||||
|
if ('serviceWorker' in navigator) {
|
||||||
|
navigator.serviceWorker.register('/sw.js').catch(() => {});
|
||||||
|
}
|
||||||
|
|
||||||
// Initialize vinyl mode
|
// Initialize vinyl mode
|
||||||
applyVinylMode();
|
applyVinylMode();
|
||||||
|
|
||||||
|
|||||||
@@ -94,6 +94,11 @@ function setTheme(theme) {
|
|||||||
sunIcon.style.display = 'block';
|
sunIcon.style.display = 'block';
|
||||||
moonIcon.style.display = 'none';
|
moonIcon.style.display = 'none';
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const metaThemeColor = document.querySelector('meta[name="theme-color"]');
|
||||||
|
if (metaThemeColor) {
|
||||||
|
metaThemeColor.setAttribute('content', theme === 'light' ? '#ffffff' : '#121212');
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
function toggleTheme() {
|
function toggleTheme() {
|
||||||
|
|||||||
23
media_server/static/manifest.json
Normal file
23
media_server/static/manifest.json
Normal file
@@ -0,0 +1,23 @@
|
|||||||
|
{
|
||||||
|
"name": "Media Server",
|
||||||
|
"short_name": "Media",
|
||||||
|
"description": "Remote media player control and file browser",
|
||||||
|
"start_url": "/",
|
||||||
|
"display": "standalone",
|
||||||
|
"orientation": "any",
|
||||||
|
"background_color": "#121212",
|
||||||
|
"theme_color": "#121212",
|
||||||
|
"icons": [
|
||||||
|
{
|
||||||
|
"src": "/static/icons/icon.svg",
|
||||||
|
"sizes": "any",
|
||||||
|
"type": "image/svg+xml"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"src": "/static/icons/icon.svg",
|
||||||
|
"sizes": "any",
|
||||||
|
"type": "image/svg+xml",
|
||||||
|
"purpose": "maskable"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
15
media_server/static/sw.js
Normal file
15
media_server/static/sw.js
Normal file
@@ -0,0 +1,15 @@
|
|||||||
|
// Minimal service worker for PWA installability.
|
||||||
|
// This app requires a live WebSocket connection, so offline caching is not useful.
|
||||||
|
// All fetch requests are passed through to the network.
|
||||||
|
|
||||||
|
self.addEventListener('install', () => {
|
||||||
|
self.skipWaiting();
|
||||||
|
});
|
||||||
|
|
||||||
|
self.addEventListener('activate', (event) => {
|
||||||
|
event.waitUntil(self.clients.claim());
|
||||||
|
});
|
||||||
|
|
||||||
|
self.addEventListener('fetch', (event) => {
|
||||||
|
event.respondWith(fetch(event.request));
|
||||||
|
});
|
||||||
Reference in New Issue
Block a user