Initial commit: split single-file PWA into index.html, styles.css, app.js, sw.js
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
commit
a3ac8bef30
6 files changed
+3018
No files matched your search
@@ -0,0 +1,3 @@
|
||||
.DS_Store
|
||||
*.log
|
||||
node_modules/
|
||||
@@ -0,0 +1,20 @@
|
||||
# NovaSonic
|
||||
|
||||
A self-hosted music streaming PWA for Navidrome / Subsonic-compatible backends.
|
||||
|
||||
## Stack
|
||||
|
||||
- `index.html` — app shell
|
||||
- `styles.css` — all styles
|
||||
- `app.js` — frontend logic
|
||||
- `sw.js` — service worker (offline caching)
|
||||
|
||||
## Setup
|
||||
|
||||
Point the app at your Navidrome server URL on first launch.
|
||||
|
||||
## Docker
|
||||
|
||||
```bash
|
||||
docker compose up -d
|
||||
```
|
||||
+626
@@ -0,0 +1,626 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="de">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0, viewport-fit=cover">
|
||||
<meta name="theme-color" content="#07070f">
|
||||
<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="NovaSonic">
|
||||
<title>NovaSonic</title>
|
||||
<link rel="preconnect" href="https://fonts.googleapis.com">
|
||||
<link href="https://fonts.googleapis.com/css2?family=Outfit:wght@300;400;500;600;700;800&family=JetBrains+Mono:wght@300;400;500&display=swap" rel="stylesheet">
|
||||
<link rel="stylesheet" href="styles.css">
|
||||
</head>
|
||||
<body>
|
||||
|
||||
<!-- ─── SETUP ─── -->
|
||||
<div id="setup-screen">
|
||||
<div class="setup-card">
|
||||
<div class="setup-logo">
|
||||
<div class="logo-icon">♫</div>
|
||||
<h1>Nova<em>Sonic</em></h1>
|
||||
</div>
|
||||
<div class="setup-tabs">
|
||||
<button class="setup-tab active" onclick="switchSetupTab('navidrome')">Navidrome</button>
|
||||
<button class="setup-tab" onclick="switchSetupTab('discover')">Discover APIs</button>
|
||||
</div>
|
||||
<div class="setup-pane active" id="pane-navidrome">
|
||||
<div class="field"><label>Server URL</label><input id="srv-url" type="url" placeholder="https://music.example.com"></div>
|
||||
<div class="field"><label>Benutzername</label><input id="srv-user" type="text" placeholder="admin"></div>
|
||||
<div class="field"><label>Passwort</label><input id="srv-pass" type="password" placeholder="••••••••"></div>
|
||||
</div>
|
||||
<div class="setup-pane" id="pane-discover">
|
||||
<div class="field">
|
||||
<label>Last.fm API Key <span style="color:var(--text3);font-weight:400;text-transform:none">(optional)</span></label>
|
||||
<input id="lastfm-key" type="text" placeholder="Kostenlos auf last.fm/api">
|
||||
<div class="hint">Für Empfehlungen & Similar Artists. Kostenlos registrieren unter <strong>last.fm/api/account/create</strong></div>
|
||||
</div>
|
||||
<div class="field">
|
||||
<label>NovaSonic Backend URL <span style="color:var(--text3);font-weight:400;text-transform:none">(optional)</span></label>
|
||||
<input id="backend-url" type="url" placeholder="http://192.168.1.x:3700">
|
||||
<div class="hint">Für Download-to-Server Funktion. Backend separat starten (novasonic-server.js)</div>
|
||||
</div>
|
||||
</div>
|
||||
<button class="btn-primary" id="btn-connect" onclick="doConnect()">Verbinden</button>
|
||||
<div class="setup-error" id="setup-error"></div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- ─── APP ─── -->
|
||||
<div id="app">
|
||||
<div class="app-body">
|
||||
|
||||
<!-- Sidebar -->
|
||||
<aside class="sidebar" id="sidebar">
|
||||
<div class="sidebar-header">
|
||||
<div class="sidebar-logo">
|
||||
<div class="icon">♫</div>
|
||||
<span>Nova<em>Sonic</em></span>
|
||||
</div>
|
||||
<div class="header-btns">
|
||||
<button class="hbtn" onclick="openSettings()" title="Einstellungen">
|
||||
<svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><circle cx="12" cy="12" r="3"/><path d="M19.07 4.93a10 10 0 0 1 0 14.14M14.24 9.76a4 4 0 0 1 0 4.48M4.93 4.93a10 10 0 0 0 0 14.14M9.76 9.76a4 4 0 0 0 0 4.48"/></svg>
|
||||
</button>
|
||||
<button class="hbtn danger" onclick="disconnect()" title="Trennen">
|
||||
<svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><path d="M18.36 6.64a9 9 0 1 1-12.73 0"/><line x1="12" y1="2" x2="12" y2="12"/></svg>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
<div class="search-wrap">
|
||||
<div class="search-box" onclick="document.getElementById('search-input').focus()">
|
||||
<svg width="13" height="13" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><circle cx="11" cy="11" r="8"/><path d="m21 21-4.35-4.35"/></svg>
|
||||
<input id="search-input" type="text" placeholder="Lokale Suche…" oninput="onLocalSearch(this.value)">
|
||||
</div>
|
||||
</div>
|
||||
<nav>
|
||||
<div class="nav-section">
|
||||
<div class="nav-section-title">Bibliothek</div>
|
||||
<div class="nav-item active" onclick="navigate('home')" data-page="home">
|
||||
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><path d="M3 9l9-7 9 7v11a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2z"/><polyline points="9 22 9 12 15 12 15 22"/></svg>Home
|
||||
</div>
|
||||
<div class="nav-item" onclick="navigate('artists')" data-page="artists">
|
||||
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><path d="M17 21v-2a4 4 0 0 0-4-4H5a4 4 0 0 0-4 4v2"/><circle cx="9" cy="7" r="4"/><path d="M23 21v-2a4 4 0 0 0-3-3.87"/><path d="M16 3.13a4 4 0 0 1 0 7.75"/></svg>Künstler
|
||||
</div>
|
||||
<div class="nav-item" onclick="navigate('albums')" data-page="albums">
|
||||
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><circle cx="12" cy="12" r="10"/><circle cx="12" cy="12" r="3"/></svg>Alben
|
||||
</div>
|
||||
<div class="nav-item" onclick="navigate('starred')" data-page="starred">
|
||||
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><polygon points="12 2 15.09 8.26 22 9.27 17 14.14 18.18 21.02 12 17.77 5.82 21.02 7 14.14 2 9.27 8.91 8.26 12 2"/></svg>Favoriten
|
||||
</div>
|
||||
<div class="nav-item" onclick="navigate('playlists')" data-page="playlists">
|
||||
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><line x1="8" y1="6" x2="21" y2="6"/><line x1="8" y1="12" x2="21" y2="12"/><line x1="8" y1="18" x2="21" y2="18"/><line x1="3" y1="6" x2="3.01" y2="6"/><line x1="3" y1="12" x2="3.01" y2="12"/><line x1="3" y1="18" x2="3.01" y2="18"/></svg>Playlists
|
||||
</div>
|
||||
</div>
|
||||
<div class="nav-section">
|
||||
<div class="nav-section-title">Entdecken</div>
|
||||
<div class="nav-item" onclick="navigate('discover')" data-page="discover">
|
||||
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><circle cx="12" cy="12" r="10"/><polygon points="10 8 16 12 10 16 10 8"/></svg>Global Suche
|
||||
</div>
|
||||
<div class="nav-item" onclick="navigate('new-releases')" data-page="new-releases">
|
||||
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><path d="M12 22s8-4 8-10V5l-8-3-8 3v7c0 6 8 10 8 10z"/></svg>Neue Releases
|
||||
</div>
|
||||
<div class="nav-item" onclick="navigate('recommendations')" data-page="recommendations">
|
||||
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><path d="M20.84 4.61a5.5 5.5 0 0 0-7.78 0L12 5.67l-1.06-1.06a5.5 5.5 0 0 0-7.78 7.78l1.06 1.06L12 21.23l7.78-7.78 1.06-1.06a5.5 5.5 0 0 0 0-7.78z"/></svg>Für dich
|
||||
</div>
|
||||
<div class="nav-item" onclick="navigate('random')" data-page="random">
|
||||
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><polyline points="16 3 21 3 21 8"/><line x1="4" y1="20" x2="21" y2="3"/><polyline points="21 16 21 21 16 21"/><line x1="15" y1="15" x2="21" y2="21"/></svg>Zufällig
|
||||
</div>
|
||||
</div>
|
||||
<div class="nav-section">
|
||||
<div class="nav-section-title">Player</div>
|
||||
<div class="nav-item" onclick="toggleQueue()">
|
||||
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><line x1="8" y1="6" x2="21" y2="6"/><line x1="8" y1="12" x2="21" y2="12"/><line x1="8" y1="18" x2="21" y2="18"/><line x1="3" y1="6" x2="3.01" y2="6"/><line x1="3" y1="12" x2="3.01" y2="12"/><line x1="3" y1="18" x2="3.01" y2="18"/></svg>Warteschlange
|
||||
</div>
|
||||
</div>
|
||||
</nav>
|
||||
</aside>
|
||||
|
||||
<div class="mobile-overlay" id="mobile-overlay" onclick="closeSidebar()"></div>
|
||||
|
||||
<!-- MAIN -->
|
||||
<main class="main" id="main-content">
|
||||
|
||||
<!-- Mobile header -->
|
||||
<div style="display:flex;align-items:center;justify-content:space-between;padding:12px 14px;position:sticky;top:0;background:var(--bg);z-index:10;border-bottom:1px solid var(--border)" id="mobile-header">
|
||||
<div class="sidebar-logo">
|
||||
<div class="icon" style="width:26px;height:26px;font-size:12px;border-radius:7px">♫</div>
|
||||
<span style="font-size:15px;font-weight:700">Nova<em style="color:var(--accent);font-style:normal">Sonic</em></span>
|
||||
</div>
|
||||
<button class="hbtn" onclick="openSettings()" title="Einstellungen" style="padding:8px;color:var(--text2)">
|
||||
<svg width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><circle cx="12" cy="12" r="3"/><path d="M19.4 15a1.65 1.65 0 0 0 .33 1.82l.06.06a2 2 0 0 1 0 2.83 2 2 0 0 1-2.83 0l-.06-.06a1.65 1.65 0 0 0-1.82-.33 1.65 1.65 0 0 0-1 1.51V21a2 2 0 0 1-2 2 2 2 0 0 1-2-2v-.09A1.65 1.65 0 0 0 9 19.4a1.65 1.65 0 0 0-1.82.33l-.06.06a2 2 0 0 1-2.83 0 2 2 0 0 1 0-2.83l.06-.06A1.65 1.65 0 0 0 4.68 15a1.65 1.65 0 0 0-1.51-1H3a2 2 0 0 1-2-2 2 2 0 0 1 2-2h.09A1.65 1.65 0 0 0 4.6 9a1.65 1.65 0 0 0-.33-1.82l-.06-.06a2 2 0 0 1 0-2.83 2 2 0 0 1 2.83 0l.06.06A1.65 1.65 0 0 0 9 4.68a1.65 1.65 0 0 0 1-1.51V3a2 2 0 0 1 2-2 2 2 0 0 1 2 2v.09a1.65 1.65 0 0 0 1 1.51 1.65 1.65 0 0 0 1.82-.33l.06-.06a2 2 0 0 1 2.83 0 2 2 0 0 1 0 2.83l-.06.06A1.65 1.65 0 0 0 19.4 9a1.65 1.65 0 0 0 1.51 1H21a2 2 0 0 1 2 2 2 2 0 0 1-2 2h-.09a1.65 1.65 0 0 0-1.51 1z"/></svg>
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<!-- HOME -->
|
||||
<div class="page active" id="page-home">
|
||||
<div class="section-header" style="margin-top:8px"><span class="section-title">Zufällig</span><span class="section-more" onclick="navigate('random')">Mehr</span></div>
|
||||
<div class="scroll-row" id="home-random"></div>
|
||||
<div class="section-header"><span class="section-title">Neueste Alben</span><span class="section-more" onclick="navigate('albums')">Mehr</span></div>
|
||||
<div class="scroll-row" id="home-newest"></div>
|
||||
<div class="section-header"><span class="section-title">Häufig gespielt</span></div>
|
||||
<div class="scroll-row" id="home-frequent"></div>
|
||||
</div>
|
||||
|
||||
<!-- ARTISTS -->
|
||||
<div class="page" id="page-artists">
|
||||
<div class="page-header"><div class="page-title">Künstler</div></div>
|
||||
<div id="artists-grid" class="grid grid-4"></div>
|
||||
</div>
|
||||
|
||||
<!-- ARTIST DETAIL -->
|
||||
<div class="page" id="page-artist-detail">
|
||||
<button class="back-btn" id="artist-back-btn"><svg width="13" height="13" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><polyline points="15 18 9 12 15 6"/></svg>Alle Künstler</button>
|
||||
<div class="artist-hero" id="artist-detail-hero"><div class="artist-hero-overlay"><div class="artist-hero-name" id="artist-detail-name"></div></div></div>
|
||||
<div id="artist-bio-section"></div>
|
||||
<div id="artist-similar-section"></div>
|
||||
<div id="artist-topsongs-section"></div>
|
||||
<div class="section-title" style="margin-bottom:12px;margin-top:8px">Alben</div>
|
||||
<div id="artist-albums-grid" class="grid grid-4"></div>
|
||||
</div>
|
||||
|
||||
<!-- ALBUMS -->
|
||||
<div class="page" id="page-albums">
|
||||
<div class="page-header"><div class="page-title">Alben</div></div>
|
||||
<div id="albums-grid" class="grid grid-4"></div>
|
||||
</div>
|
||||
|
||||
<!-- ALBUM DETAIL -->
|
||||
<div class="page" id="page-album-detail">
|
||||
<button class="back-btn" id="album-back-btn"><svg width="13" height="13" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><polyline points="15 18 9 12 15 6"/></svg>Zurück</button>
|
||||
<div class="album-header">
|
||||
<div class="album-cover" id="album-cover-wrap"><div class="no-art">💿</div></div>
|
||||
<div class="album-meta">
|
||||
<div class="album-type">Album</div>
|
||||
<div class="album-name" id="album-detail-name">—</div>
|
||||
<div class="album-artist-name" id="album-detail-artist"></div>
|
||||
<div class="album-stats" id="album-detail-stats"></div>
|
||||
<div class="album-actions">
|
||||
<button class="btn-play-all" id="btn-play-album"><svg width="13" height="13" viewBox="0 0 24 24" fill="currentColor"><polygon points="5 3 19 12 5 21 5 3"/></svg>Abspielen</button>
|
||||
<button class="btn-outline" id="btn-dl-album"><svg width="13" height="13" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><path d="M21 15v4a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2v-4"/><polyline points="7 10 12 15 17 10"/><line x1="12" y1="15" x2="12" y2="3"/></svg>Download</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="song-list" id="album-songs"></div>
|
||||
</div>
|
||||
|
||||
<!-- STARRED -->
|
||||
<div class="page" id="page-starred">
|
||||
<div class="page-header"><div class="page-title">Favoriten ⭐</div></div>
|
||||
<div id="starred-songs" class="song-list"></div>
|
||||
</div>
|
||||
|
||||
<!-- PLAYLISTS -->
|
||||
<div class="page" id="page-playlists">
|
||||
<div class="page-header">
|
||||
<div class="page-title">Playlists 🎶</div>
|
||||
<button class="btn-outline" style="font-size:12px;padding:8px 14px" onclick="createPlaylistPrompt()">
|
||||
<svg width="13" height="13" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><line x1="12" y1="5" x2="12" y2="19"/><line x1="5" y1="12" x2="19" y2="12"/></svg>Neue Playlist
|
||||
</button>
|
||||
</div>
|
||||
<div id="playlists-grid" class="grid grid-4"></div>
|
||||
</div>
|
||||
|
||||
<!-- PLAYLIST DETAIL -->
|
||||
<div class="page" id="page-playlist-detail">
|
||||
<button class="back-btn" id="playlist-back-btn" onclick="navigate('playlists')">
|
||||
<svg width="13" height="13" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><polyline points="15 18 9 12 15 6"/></svg>Alle Playlists
|
||||
</button>
|
||||
<div class="album-header">
|
||||
<div class="album-cover" id="playlist-cover"><div class="no-art">🎶</div></div>
|
||||
<div class="album-meta">
|
||||
<div class="album-type">Playlist</div>
|
||||
<div class="album-name" id="playlist-detail-name">—</div>
|
||||
<div class="album-stats" id="playlist-detail-stats"></div>
|
||||
<div class="album-actions">
|
||||
<button class="btn-play-all" id="btn-play-playlist"><svg width="13" height="13" viewBox="0 0 24 24" fill="currentColor"><polygon points="5 3 19 12 5 21 5 3"/></svg>Abspielen</button>
|
||||
<button class="btn-outline" id="btn-shuffle-playlist"><svg width="13" height="13" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><polyline points="16 3 21 3 21 8"/><line x1="4" y1="20" x2="21" y2="3"/><polyline points="21 16 21 21 16 21"/><line x1="15" y1="15" x2="21" y2="21"/></svg>Shuffle</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="song-list" id="playlist-songs"></div>
|
||||
</div>
|
||||
|
||||
<!-- RANDOM -->
|
||||
<div class="page" id="page-random">
|
||||
<div class="page-header">
|
||||
<div class="page-title">Zufällig <em>♪</em></div>
|
||||
<button class="btn-outline" onclick="loadRandom()" style="font-size:12px;padding:8px 14px"><svg width="12" height="12" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><polyline points="16 3 21 3 21 8"/><line x1="4" y1="20" x2="21" y2="3"/><polyline points="21 16 21 21 16 21"/><line x1="15" y1="15" x2="21" y2="21"/></svg>Neu mischen</button>
|
||||
</div>
|
||||
<div id="random-songs" class="song-list"></div>
|
||||
</div>
|
||||
|
||||
<!-- LOCAL SEARCH -->
|
||||
<div class="page" id="page-search">
|
||||
<div class="page-header"><div class="page-title">Suche</div></div>
|
||||
<div id="search-results"></div>
|
||||
</div>
|
||||
|
||||
<!-- DISCOVER (Global) -->
|
||||
<div class="page" id="page-discover">
|
||||
<div class="page-header">
|
||||
<div class="page-title"><span class="blue">Global</span> Suche</div>
|
||||
<span class="badge badge-itunes" style="font-size:11px;padding:4px 10px">iTunes Store + Local</span>
|
||||
</div>
|
||||
<div class="search-wrap" style="padding:0 0 20px">
|
||||
<div class="search-box" style="background:var(--surface);border-color:var(--border2)">
|
||||
<svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><circle cx="11" cy="11" r="8"/><path d="m21 21-4.35-4.35"/></svg>
|
||||
<input id="global-search-input" type="text" placeholder="Künstler, Titel, Album suchen…" oninput="onGlobalSearch(this.value)" style="font-size:15px">
|
||||
</div>
|
||||
</div>
|
||||
<div id="global-search-results">
|
||||
<div class="empty-state"><div class="icon">🔍</div><p>Gib einen Suchbegriff ein um loszulegen</p></div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- NEW RELEASES -->
|
||||
<div class="page" id="page-new-releases">
|
||||
<div class="page-header">
|
||||
<div class="page-title">Neue <em>Releases</em></div>
|
||||
<span class="badge badge-itunes" style="font-size:11px;padding:4px 10px">iTunes Charts</span>
|
||||
</div>
|
||||
<div class="section-header" style="margin-top:0"><span class="section-title">Top Alben weltweit</span></div>
|
||||
<div id="nr-albums" class="grid grid-4"></div>
|
||||
<div class="section-header"><span class="section-title">Top Songs</span></div>
|
||||
<div id="nr-songs" class="song-list"></div>
|
||||
</div>
|
||||
|
||||
<!-- RECOMMENDATIONS -->
|
||||
<div class="page" id="page-recommendations">
|
||||
<div class="page-header">
|
||||
<div class="page-title">Für <em>dich</em></div>
|
||||
</div>
|
||||
<div id="reco-content">
|
||||
<div class="empty-state"><div class="icon">🎵</div><p>Spiele zuerst Musik in deiner Bibliothek,<br>dann zeigen wir dir ähnliche Künstler und Tracks</p></div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
</main>
|
||||
</div>
|
||||
|
||||
<!-- QUEUE PANEL -->
|
||||
<div class="queue-panel" id="queue-panel">
|
||||
<div class="queue-title">Warteschlange</div>
|
||||
<div class="queue-list" id="queue-list"></div>
|
||||
</div>
|
||||
|
||||
<!-- PLAYER -->
|
||||
<div class="player">
|
||||
<div class="player-track">
|
||||
<div class="player-art" id="player-art" style="cursor:pointer" title="Jetzt läuft öffnen"><div class="no-art">♪</div></div>
|
||||
<div class="player-info">
|
||||
<div style="display:flex;align-items:center;gap:6px">
|
||||
<div class="player-title" id="player-title" onclick="playerBarGoAlbum()" style="cursor:pointer" title="Zum Album">Kein Titel</div>
|
||||
<button id="btn-star-current" onclick="starCurrent()" title="Favorit" style="background:none;border:none;cursor:pointer;padding:2px;flex-shrink:0;display:flex;align-items:center">
|
||||
<svg id="star-icon" width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="var(--text3)" stroke-width="2" style="transition:fill .2s,stroke .2s"><polygon points="12 2 15.09 8.26 22 9.27 17 14.14 18.18 21.02 12 17.77 5.82 21.02 7 14.14 2 9.27 8.91 8.26 12 2"/></svg>
|
||||
</button>
|
||||
</div>
|
||||
<div class="player-artist" id="player-artist">
|
||||
<span id="player-artist-name" onclick="playerBarGoArtist()" style="cursor:pointer" title="Zum Künstler">—</span><span id="player-album-sep" style="color:var(--text3)"> · </span><span id="player-album-name" onclick="playerBarGoAlbum()" style="cursor:pointer" title="Zum Album"></span>
|
||||
</div>
|
||||
<div class="player-status-row">
|
||||
<span class="status-badge badge-shuffle" id="badge-shuffle">🔀 Shuffle</span>
|
||||
<span class="status-badge badge-repeat" id="badge-repeat">🔁 Repeat</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="player-controls">
|
||||
<div class="ctrl-buttons">
|
||||
<button class="ctrl-btn" id="btn-shuffle" onclick="toggleShuffle()" title="Shuffle">
|
||||
<svg width="15" height="15" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><polyline points="16 3 21 3 21 8"/><line x1="4" y1="20" x2="21" y2="3"/><polyline points="21 16 21 21 16 21"/><line x1="15" y1="15" x2="21" y2="21"/></svg>
|
||||
</button>
|
||||
<button class="ctrl-btn" onclick="prevSong()">
|
||||
<svg width="17" height="17" viewBox="0 0 24 24" fill="currentColor"><polygon points="19 20 9 12 19 4 19 20"/><line x1="5" y1="19" x2="5" y2="5" stroke="currentColor" stroke-width="2"/></svg>
|
||||
</button>
|
||||
<button class="play-pause-btn" id="btn-play-pause" onclick="togglePlay()">
|
||||
<svg id="play-icon" width="15" height="15" viewBox="0 0 24 24" fill="currentColor"><polygon points="5 3 19 12 5 21 5 3"/></svg>
|
||||
</button>
|
||||
<button class="ctrl-btn" onclick="nextSong()">
|
||||
<svg width="17" height="17" viewBox="0 0 24 24" fill="currentColor"><polygon points="5 4 15 12 5 20 5 4"/><line x1="19" y1="5" x2="19" y2="19" stroke="currentColor" stroke-width="2"/></svg>
|
||||
</button>
|
||||
<button class="ctrl-btn" id="btn-repeat" onclick="toggleRepeat()" title="Wiederholen">
|
||||
<svg width="15" height="15" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><polyline points="17 1 21 5 17 9"/><path d="M3 11V9a4 4 0 0 1 4-4h14"/><polyline points="7 23 3 19 7 15"/><path d="M21 13v2a4 4 0 0 1-4 4H3"/></svg>
|
||||
</button>
|
||||
</div>
|
||||
<div class="progress-bar-wrap">
|
||||
<span class="progress-time" id="time-cur">0:00</span>
|
||||
<div class="progress-bar" id="progress-bar" onclick="seekTo(event)"><div class="progress-fill" id="progress-fill"></div></div>
|
||||
<span class="progress-time right" id="time-total">0:00</span>
|
||||
</div>
|
||||
</div>
|
||||
<div class="player-right">
|
||||
<button class="icon-btn" onclick="downloadCurrent()" title="Herunterladen">
|
||||
<svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><path d="M21 15v4a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2v-4"/><polyline points="7 10 12 15 17 10"/><line x1="12" y1="15" x2="12" y2="3"/></svg>
|
||||
</button>
|
||||
<button class="icon-btn" onclick="toggleQueue()" title="Warteschlange">
|
||||
<svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><line x1="8" y1="6" x2="21" y2="6"/><line x1="8" y1="12" x2="21" y2="12"/><line x1="8" y1="18" x2="21" y2="18"/><line x1="3" y1="6" x2="3.01" y2="6"/><line x1="3" y1="12" x2="3.01" y2="12"/><line x1="3" y1="18" x2="3.01" y2="18"/></svg>
|
||||
</button>
|
||||
<button class="icon-btn" id="btn-radio-mode" onclick="toggleRadioMode()" title="Radio-Modus: endloser Shuffle">
|
||||
<svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><circle cx="12" cy="12" r="2"/><path d="M4.93 4.93a10 10 0 0 0 0 14.14"/><path d="M19.07 4.93a10 10 0 0 1 0 14.14"/><path d="M7.76 7.76a6 6 0 0 0 0 8.49"/><path d="M16.24 7.76a6 6 0 0 1 0 8.49"/></svg>
|
||||
</button>
|
||||
<div class="volume-wrap">
|
||||
<svg width="13" height="13" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" style="color:var(--text3)"><polygon points="11 5 6 9 2 9 2 15 6 15 11 19 11 5"/><path d="M19.07 4.93a10 10 0 0 1 0 14.14"/><path d="M15.54 8.46a5 5 0 0 1 0 7.07"/></svg>
|
||||
<input type="range" class="volume-slider" id="volume-slider" min="0" max="100" value="80" oninput="setVolume(this.value)">
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- PREVIEW BAR (iTunes 30s) -->
|
||||
<div class="preview-bar" id="preview-bar">
|
||||
<div class="player-art" id="preview-art" style="width:42px;height:42px;border-radius:6px"><div class="no-art" style="font-size:16px">♪</div></div>
|
||||
<div class="preview-info">
|
||||
<div class="preview-title" id="preview-title">—<span class="preview-badge">30s Preview</span></div>
|
||||
<div class="preview-sub" id="preview-sub">iTunes Store</div>
|
||||
</div>
|
||||
<div class="preview-prog"><div class="preview-fill" id="preview-fill"></div></div>
|
||||
<button class="preview-close" onclick="stopPreview()">✕</button>
|
||||
</div>
|
||||
|
||||
<!-- DOWNLOAD MODAL -->
|
||||
<div class="modal-overlay" id="download-modal">
|
||||
<div class="modal">
|
||||
<h2>Auf Server herunterladen</h2>
|
||||
<p class="modal-sub">Der NovaSonic Backend lädt die Musik via yt-dlp herunter und speichert sie direkt in deinem Navidrome-Musikordner.</p>
|
||||
<div class="modal-track-info" id="dl-modal-info"></div>
|
||||
<div class="modal-opts" id="dl-modal-opts">
|
||||
<div class="modal-opt selected" data-type="track" onclick="selectDlType(this,'track')">
|
||||
<svg width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><circle cx="12" cy="12" r="10"/><polygon points="10 8 16 12 10 16 10 8"/></svg>
|
||||
<div><div class="opt-label">Einzelner Track</div><div class="opt-sub">Nur diesen Titel herunterladen</div></div>
|
||||
</div>
|
||||
<div class="modal-opt" data-type="album" onclick="selectDlType(this,'album')">
|
||||
<svg width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><circle cx="12" cy="12" r="10"/><circle cx="12" cy="12" r="3"/></svg>
|
||||
<div><div class="opt-label">Ganzes Album</div><div class="opt-sub" id="dl-album-sub">Album herunterladen</div></div>
|
||||
</div>
|
||||
<div class="modal-opt" data-type="artist" onclick="selectDlType(this,'artist')">
|
||||
<svg width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><path d="M17 21v-2a4 4 0 0 0-4-4H5a4 4 0 0 0-4 4v2"/><circle cx="9" cy="7" r="4"/></svg>
|
||||
<div><div class="opt-label">Ganzer Künstler</div><div class="opt-sub">Alle Top-Alben des Künstlers</div></div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="modal-disclaimer">
|
||||
ℹ️ Bitte lade nur Musik herunter, die du besitzt oder für die du eine Lizenz hast. Dieses Tool ist für Backup-Zwecke und eigene Inhalte gedacht.
|
||||
</div>
|
||||
<div class="modal-btns">
|
||||
<button class="modal-btns btn-cancel" style="flex:1;padding:12px;border-radius:9px;font-family:var(--font);font-size:14px;font-weight:600;cursor:pointer;border:none;background:var(--surface2);color:var(--text2)" onclick="closeDlModal()">Abbrechen</button>
|
||||
<button class="modal-btns btn-download" style="flex:1;padding:12px;border-radius:9px;font-family:var(--font);font-size:14px;font-weight:600;cursor:pointer;border:none;background:linear-gradient(135deg,var(--blue),#2d6ad4);color:#fff" id="btn-do-download" onclick="executeDownload()">Herunterladen</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- SETTINGS MODAL -->
|
||||
<div class="modal-overlay" id="settings-modal">
|
||||
<div class="modal" style="padding:0;display:flex;flex-direction:column;max-height:85vh;overflow:hidden;width:min(480px,calc(100vw - 32px))">
|
||||
<!-- Sticky header -->
|
||||
<div style="display:flex;align-items:center;justify-content:space-between;padding:20px 24px 16px;border-bottom:1px solid var(--border);flex-shrink:0">
|
||||
<div style="display:flex;align-items:center;gap:12px">
|
||||
<div style="width:36px;height:36px;border-radius:10px;background:linear-gradient(135deg,var(--accent),#c07010);display:flex;align-items:center;justify-content:center;font-size:17px;box-shadow:0 4px 12px var(--accent-glow)">⚙</div>
|
||||
<div>
|
||||
<div style="font-size:17px;font-weight:800;letter-spacing:-.3px">Einstellungen</div>
|
||||
<div style="font-size:11px;color:var(--text2);margin-top:1px">NovaSonic</div>
|
||||
</div>
|
||||
</div>
|
||||
<button onclick="closeSettings()" style="background:var(--surface2);border:none;color:var(--text2);width:32px;height:32px;border-radius:50%;cursor:pointer;display:flex;align-items:center;justify-content:center;font-size:15px;flex-shrink:0;transition:background .15s" onmouseover="this.style.background='var(--surface)'" onmouseout="this.style.background='var(--surface2)'">✕</button>
|
||||
</div>
|
||||
<!-- Scrollable body -->
|
||||
<div style="flex:1;overflow-y:auto;padding:18px 24px 8px">
|
||||
<div class="settings-section">
|
||||
<h3 style="display:flex;align-items:center;gap:8px;margin-bottom:12px">
|
||||
<span style="width:24px;height:24px;border-radius:6px;background:var(--blue-dim);display:inline-flex;align-items:center;justify-content:center;font-size:12px">🌐</span>
|
||||
Navidrome Server
|
||||
</h3>
|
||||
<div class="field"><label>Server URL</label><input id="s-url" type="url" placeholder="https://music.example.com"></div>
|
||||
<div class="field"><label>Benutzername</label><input id="s-user" type="text" placeholder="admin"></div>
|
||||
<div class="field"><label>Passwort</label><input id="s-pass" type="password" placeholder="(unverändert lassen)"></div>
|
||||
</div>
|
||||
<div class="settings-section">
|
||||
<h3 style="display:flex;align-items:center;gap:8px;margin-bottom:12px">
|
||||
<span style="width:24px;height:24px;border-radius:6px;background:var(--accent-dim);display:inline-flex;align-items:center;justify-content:center;font-size:12px">🔑</span>
|
||||
Discover APIs
|
||||
</h3>
|
||||
<div class="field">
|
||||
<label>Last.fm API Key</label>
|
||||
<input id="s-lastfm" type="text" placeholder="Für Künstler-Infos & Empfehlungen">
|
||||
<div class="hint">Kostenlos unter last.fm/api/account/create</div>
|
||||
</div>
|
||||
<div class="field">
|
||||
<label>NovaSonic Backend URL</label>
|
||||
<input id="s-backend" type="url" placeholder="http://192.168.x.x:3700">
|
||||
<div class="hint">Für Download-to-Server via yt-dlp</div>
|
||||
</div>
|
||||
<div class="settings-row" style="margin-top:8px;border-radius:9px">
|
||||
<div>
|
||||
<div style="font-size:13px;font-weight:500">Backend Status</div>
|
||||
<div class="hint" id="backend-status-text">Nicht verbunden</div>
|
||||
</div>
|
||||
<div style="display:flex;align-items:center;gap:8px">
|
||||
<div class="status-dot" id="backend-status-dot"></div>
|
||||
<button class="btn-outline" style="padding:6px 12px;font-size:12px" onclick="checkBackend()">Testen</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<!-- Sticky footer -->
|
||||
<div style="display:flex;gap:8px;padding:12px 24px 16px;border-top:1px solid var(--border);flex-shrink:0;background:var(--surface);border-radius:0 0 18px 18px">
|
||||
<button style="flex:1;padding:12px;border-radius:9px;font-family:var(--font);font-size:14px;font-weight:600;cursor:pointer;border:none;background:var(--surface2);color:var(--text2)" onclick="closeSettings()">Abbrechen</button>
|
||||
<button style="flex:1;padding:12px;border-radius:9px;font-family:var(--font);font-size:14px;font-weight:600;cursor:pointer;border:none;background:linear-gradient(135deg,var(--accent),#c07010);color:#000" onclick="saveSettings()">Speichern</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- JOB PANEL -->
|
||||
<div class="jobs-panel" id="jobs-panel"></div>
|
||||
|
||||
<div class="toast-container" id="toast-container"></div>
|
||||
<audio id="audio-element"></audio>
|
||||
<audio id="preview-audio"></audio>
|
||||
|
||||
<script src="app.js" defer></script>
|
||||
<!-- CONTEXT MENU -->
|
||||
<div class="ctx-menu" id="ctx-menu" style="display:none"></div>
|
||||
|
||||
<!-- ADD TO PLAYLIST MODAL -->
|
||||
<div class="pl-modal" id="pl-modal">
|
||||
<div class="inner">
|
||||
<h3 id="pl-modal-title">Zu Playlist hinzufügen</h3>
|
||||
<div class="pl-list" id="pl-list"></div>
|
||||
<div class="pl-new-row">
|
||||
<input class="pl-input" id="pl-new-name" placeholder="Neue Playlist erstellen…" type="text">
|
||||
<div class="pl-btns">
|
||||
<button style="background:var(--surface2);color:var(--text2)" onclick="closePlModal()">Abbrechen</button>
|
||||
<button style="background:linear-gradient(135deg,var(--accent),#c07010);color:#000" onclick="createAndAddPlaylist()">Erstellen & Hinzufügen</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- SONG INFO MODAL -->
|
||||
<div class="info-modal" id="info-modal">
|
||||
<div class="inner">
|
||||
<div class="info-header">
|
||||
<div class="info-cover" id="info-cover"><div class="no-art" style="width:100%;height:100%;display:flex;align-items:center;justify-content:center;font-size:32px">🎵</div></div>
|
||||
<div><div class="info-top-title" id="info-top-title">—</div><div class="info-top-sub" id="info-top-sub">—</div></div>
|
||||
</div>
|
||||
<div class="info-rows" id="info-rows"></div>
|
||||
<button class="info-close" onclick="closeInfoModal()">Schließen</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
|
||||
<!-- NOW PLAYING OVERLAY -->
|
||||
<div class="np-overlay" id="np-overlay">
|
||||
<div class="np-bg" id="np-bg"></div>
|
||||
<div class="np-inner">
|
||||
<div class="np-topbar">
|
||||
<button class="np-close" onclick="closeNowPlaying()" title="Schließen">
|
||||
<svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.5"><polyline points="6 9 12 15 18 9"/></svg>
|
||||
</button>
|
||||
<div class="np-tabs">
|
||||
<button class="np-tab active" id="np-tab-player" onclick="switchNpTab('player')">Jetzt läuft</button>
|
||||
<button class="np-tab" id="np-tab-queue" onclick="switchNpTab('queue')">Warteschlange</button>
|
||||
</div>
|
||||
<button class="np-menu-btn" onclick="openNpMenu()" title="Optionen">
|
||||
<svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><circle cx="12" cy="5" r="1.5" fill="currentColor"/><circle cx="12" cy="12" r="1.5" fill="currentColor"/><circle cx="12" cy="19" r="1.5" fill="currentColor"/></svg>
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<div id="np-player-view">
|
||||
<div class="np-cover" id="np-cover">
|
||||
<div class="no-art">♪</div>
|
||||
</div>
|
||||
|
||||
<div class="np-info">
|
||||
<div class="np-text">
|
||||
<div class="np-title" id="np-title">Kein Titel</div>
|
||||
<div class="np-artist" id="np-artist">
|
||||
<span id="np-artist-name" onclick="npGoArtist()" style="cursor:pointer">—</span><span id="np-album-sep" style="opacity:.5"> · </span><span id="np-album-name" onclick="npGoAlbum()" style="cursor:pointer;opacity:.8">—</span>
|
||||
</div>
|
||||
</div>
|
||||
<button class="np-star" id="np-star-btn" onclick="npToggleStar()">
|
||||
<svg id="np-star-icon" width="26" height="26" viewBox="0 0 24 24" fill="none" stroke="rgba(255,255,255,.5)" stroke-width="2" style="transition:fill .2s,stroke .2s"><polygon points="12 2 15.09 8.26 22 9.27 17 14.14 18.18 21.02 12 17.77 5.82 21.02 7 14.14 2 9.27 8.91 8.26 12 2"/></svg>
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<div class="np-progress">
|
||||
<div class="np-bar" id="np-bar" onclick="npSeek(event)">
|
||||
<div class="np-bar-fill" id="np-fill"></div>
|
||||
</div>
|
||||
<div class="np-times">
|
||||
<span id="np-cur">0:00</span>
|
||||
<span id="np-tot">0:00</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="np-controls">
|
||||
<button class="np-ctrl" id="np-shuffle-btn" onclick="toggleShuffle()" title="Shuffle">
|
||||
<svg width="22" height="22" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><polyline points="16 3 21 3 21 8"/><line x1="4" y1="20" x2="21" y2="3"/><polyline points="21 16 21 21 16 21"/><line x1="15" y1="15" x2="21" y2="21"/></svg>
|
||||
</button>
|
||||
<button class="np-ctrl" onclick="prevSong()">
|
||||
<svg width="30" height="30" viewBox="0 0 24 24" fill="currentColor"><polygon points="19 20 9 12 19 4 19 20"/><line x1="5" y1="19" x2="5" y2="5" stroke="currentColor" stroke-width="2"/></svg>
|
||||
</button>
|
||||
<button class="np-play" id="np-play-btn" onclick="togglePlay()">
|
||||
<svg id="np-play-icon" width="26" height="26" viewBox="0 0 24 24" fill="currentColor"><polygon points="5 3 19 12 5 21 5 3"/></svg>
|
||||
</button>
|
||||
<button class="np-ctrl" onclick="nextSong()">
|
||||
<svg width="30" height="30" viewBox="0 0 24 24" fill="currentColor"><polygon points="5 4 15 12 5 20 5 4"/><line x1="19" y1="5" x2="19" y2="19" stroke="currentColor" stroke-width="2"/></svg>
|
||||
</button>
|
||||
<button class="np-ctrl" id="np-repeat-btn" onclick="toggleRepeat()" title="Wiederholen">
|
||||
<svg width="22" height="22" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><polyline points="17 1 21 5 17 9"/><path d="M3 11V9a4 4 0 0 1 4-4h14"/><polyline points="7 23 3 19 7 15"/><path d="M21 13v2a4 4 0 0 1-4 4H3"/></svg>
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<div class="np-extras">
|
||||
<button class="np-icon-btn" onclick="downloadCurrent()" title="Download">
|
||||
<svg width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><path d="M21 15v4a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2v-4"/><polyline points="7 10 12 15 17 10"/><line x1="12" y1="15" x2="12" y2="3"/></svg>
|
||||
</button>
|
||||
<div class="np-vol">
|
||||
<svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" style="color:rgba(255,255,255,.45)"><polygon points="11 5 6 9 2 9 2 15 6 15 11 19 11 5"/></svg>
|
||||
<input type="range" class="np-vol-slider" id="np-vol-slider" min="0" max="100" value="80" oninput="setVolume(this.value);document.getElementById('volume-slider').value=this.value">
|
||||
<svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" style="color:rgba(255,255,255,.45)"><polygon points="11 5 6 9 2 9 2 15 6 15 11 19 11 5"/><path d="M19.07 4.93a10 10 0 0 1 0 14.14"/><path d="M15.54 8.46a5 5 0 0 1 0 7.07"/></svg>
|
||||
</div>
|
||||
<button class="np-icon-btn" id="np-radio-btn" onclick="toggleRadioMode()" title="Radio-Modus: endloser Shuffle">
|
||||
<svg width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><circle cx="12" cy="12" r="2"/><path d="M4.93 4.93a10 10 0 0 0 0 14.14"/><path d="M19.07 4.93a10 10 0 0 1 0 14.14"/><path d="M7.76 7.76a6 6 0 0 0 0 8.49"/><path d="M16.24 7.76a6 6 0 0 1 0 8.49"/></svg>
|
||||
</button>
|
||||
</div>
|
||||
</div><!-- /np-player-view -->
|
||||
|
||||
<div id="np-queue-view">
|
||||
<div id="np-queue-list"></div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- NP ACTION SHEET -->
|
||||
<div class="np-sheet-overlay" id="np-sheet-overlay" onclick="closeNpMenu()"></div>
|
||||
<div class="np-sheet" id="np-sheet">
|
||||
<div class="np-sheet-handle"></div>
|
||||
<div class="np-sheet-song" id="np-sheet-song"></div>
|
||||
<div id="np-sheet-items"></div>
|
||||
</div>
|
||||
|
||||
<nav class="bottom-nav" id="bottom-nav">
|
||||
<div class="bottom-nav-inner">
|
||||
<div class="bnav-tabs">
|
||||
<button class="bnav-tab active" id="bnav-tab-library" onclick="switchNavMode('library')">Bibliothek</button>
|
||||
<button class="bnav-tab" id="bnav-tab-discover" onclick="switchNavMode('discover')">Entdecken</button>
|
||||
</div>
|
||||
<div class="bnav-pages" id="bnav-library">
|
||||
<button class="bnav-item active" onclick="navigate('home')" data-page="home">
|
||||
<svg width="22" height="22" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><path d="M3 9l9-7 9 7v11a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2z"/><polyline points="9 22 9 12 15 12 15 22"/></svg>
|
||||
Home
|
||||
</button>
|
||||
<button class="bnav-item" onclick="navigate('artists')" data-page="artists">
|
||||
<svg width="22" height="22" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><path d="M17 21v-2a4 4 0 0 0-4-4H5a4 4 0 0 0-4 4v2"/><circle cx="9" cy="7" r="4"/><path d="M23 21v-2a4 4 0 0 0-3-3.87"/><path d="M16 3.13a4 4 0 0 1 0 7.75"/></svg>
|
||||
Künstler
|
||||
</button>
|
||||
<button class="bnav-item" onclick="navigate('playlists')" data-page="playlists">
|
||||
<svg width="22" height="22" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><line x1="8" y1="6" x2="21" y2="6"/><line x1="8" y1="12" x2="21" y2="12"/><line x1="8" y1="18" x2="21" y2="18"/><line x1="3" y1="6" x2="3.01" y2="6"/><line x1="3" y1="12" x2="3.01" y2="12"/><line x1="3" y1="18" x2="3.01" y2="18"/></svg>
|
||||
Playlists
|
||||
</button>
|
||||
<button class="bnav-item" onclick="navigate('starred')" data-page="starred">
|
||||
<svg width="22" height="22" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><polygon points="12 2 15.09 8.26 22 9.27 17 14.14 18.18 21.02 12 17.77 5.82 21.02 7 14.14 2 9.27 8.91 8.26 12 2"/></svg>
|
||||
Favoriten
|
||||
</button>
|
||||
</div>
|
||||
<div class="bnav-pages" id="bnav-discover" style="display:none">
|
||||
<button class="bnav-item" onclick="navigate('discover')" data-page="discover">
|
||||
<svg width="22" height="22" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><circle cx="11" cy="11" r="8"/><line x1="21" y1="21" x2="16.65" y2="16.65"/></svg>
|
||||
Suche
|
||||
</button>
|
||||
<button class="bnav-item" onclick="navigate('new-releases')" data-page="new-releases">
|
||||
<svg width="22" height="22" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><circle cx="12" cy="12" r="10"/><circle cx="12" cy="12" r="3"/></svg>
|
||||
Releases
|
||||
</button>
|
||||
<button class="bnav-item" onclick="navigate('recommendations')" data-page="recommendations">
|
||||
<svg width="22" height="22" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><path d="M20.84 4.61a5.5 5.5 0 0 0-7.78 0L12 5.67l-1.06-1.06a5.5 5.5 0 0 0-7.78 7.78l1.06 1.06L12 21.23l7.78-7.78 1.06-1.06a5.5 5.5 0 0 0 0-7.78z"/></svg>
|
||||
Für dich
|
||||
</button>
|
||||
<button class="bnav-item" onclick="navigate('random')" data-page="random">
|
||||
<svg width="22" height="22" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><polyline points="16 3 21 3 21 8"/><line x1="4" y1="20" x2="21" y2="3"/><polyline points="21 16 21 21 16 21"/><line x1="15" y1="15" x2="21" y2="21"/></svg>
|
||||
Zufällig
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</nav>
|
||||
|
||||
</body>
|
||||
</html>
|
||||
+695
@@ -0,0 +1,695 @@
|
||||
:root {
|
||||
--bg:#07070f;--bg2:#0d0d1a;--bg3:#12121f;
|
||||
--surface:#1a1a2e;--surface2:#22223a;
|
||||
--border:rgba(255,255,255,0.07);--border2:rgba(255,255,255,0.12);
|
||||
--accent:#e8a020;--accent2:#f0b840;
|
||||
--accent-dim:rgba(232,160,32,0.15);--accent-glow:rgba(232,160,32,0.3);
|
||||
--blue:#4f8ef7;--blue-dim:rgba(79,142,247,0.15);
|
||||
--green:#4caf7d;--green-dim:rgba(76,175,125,0.15);
|
||||
--text:#f0eefc;--text2:#8888aa;--text3:#555577;
|
||||
--danger:#e05555;
|
||||
--radius:12px;--radius-sm:8px;
|
||||
--player-h:112px;--sidebar-w:240px;
|
||||
--font:'Outfit',sans-serif;--mono:'JetBrains Mono',monospace;
|
||||
}
|
||||
*,*::before,*::after{box-sizing:border-box;margin:0;padding:0}
|
||||
html,body{height:100%;font-family:var(--font);background:var(--bg);color:var(--text);overflow:hidden}
|
||||
::-webkit-scrollbar{width:4px}::-webkit-scrollbar-track{background:transparent}::-webkit-scrollbar-thumb{background:var(--surface2);border-radius:2px}
|
||||
|
||||
/* ── SETUP ── */
|
||||
#setup-screen{position:fixed;inset:0;z-index:1000;display:flex;align-items:center;justify-content:center;background:var(--bg);background-image:radial-gradient(ellipse 60% 50% at 30% 50%,rgba(232,160,32,.08) 0%,transparent 70%),radial-gradient(ellipse 40% 60% at 80% 30%,rgba(80,60,160,.1) 0%,transparent 70%)}
|
||||
.setup-card{width:min(420px,calc(100vw - 32px));background:var(--surface);border:1px solid var(--border2);border-radius:20px;padding:40px 36px;box-shadow:0 40px 80px rgba(0,0,0,.6),0 0 0 1px rgba(255,255,255,.04)}
|
||||
.setup-logo{display:flex;align-items:center;gap:12px;margin-bottom:28px}
|
||||
.logo-icon{width:44px;height:44px;border-radius:12px;background:linear-gradient(135deg,var(--accent),#c07010);display:flex;align-items:center;justify-content:center;font-size:20px;box-shadow:0 8px 24px var(--accent-glow)}
|
||||
.setup-logo h1{font-size:24px;font-weight:700;letter-spacing:-.5px}
|
||||
.setup-logo h1 em{color:var(--accent);font-style:normal}
|
||||
.setup-tabs{display:flex;gap:4px;background:var(--bg2);border-radius:8px;padding:3px;margin-bottom:24px}
|
||||
.setup-tab{flex:1;padding:8px;border:none;background:none;color:var(--text2);font-family:var(--font);font-size:13px;font-weight:500;border-radius:6px;cursor:pointer;transition:background .2s,color .2s}
|
||||
.setup-tab.active{background:var(--surface);color:var(--text)}
|
||||
.field{margin-bottom:14px}
|
||||
.field label{display:block;font-size:11px;font-weight:600;color:var(--text2);margin-bottom:5px;letter-spacing:.5px;text-transform:uppercase}
|
||||
.field input{width:100%;padding:11px 14px;background:var(--bg2);border:1px solid var(--border2);border-radius:var(--radius-sm);color:var(--text);font-family:var(--font);font-size:14px;outline:none;transition:border-color .2s,box-shadow .2s}
|
||||
.field input:focus{border-color:var(--accent);box-shadow:0 0 0 3px var(--accent-dim)}
|
||||
.field input::placeholder{color:var(--text3)}
|
||||
.field .hint{font-size:11px;color:var(--text3);margin-top:4px;line-height:1.4}
|
||||
.btn-primary{width:100%;margin-top:20px;padding:13px;background:linear-gradient(135deg,var(--accent),#c07010);color:#000;font-family:var(--font);font-size:15px;font-weight:700;border:none;border-radius:var(--radius-sm);cursor:pointer;transition:opacity .2s,transform .15s;letter-spacing:.2px}
|
||||
.btn-primary:hover{opacity:.9;transform:translateY(-1px)}
|
||||
.btn-primary:disabled{opacity:.5;cursor:default;transform:none}
|
||||
.setup-error{margin-top:12px;padding:10px 14px;background:rgba(224,85,85,.12);border:1px solid rgba(224,85,85,.3);border-radius:var(--radius-sm);color:#ff8a8a;font-size:13px;display:none}
|
||||
.setup-pane{display:none}.setup-pane.active{display:block}
|
||||
|
||||
/* ── APP LAYOUT ── */
|
||||
#app{display:none;height:100vh;flex-direction:column}
|
||||
.app-body{display:flex;flex:1;overflow:hidden}
|
||||
|
||||
/* ── SIDEBAR ── */
|
||||
.sidebar{width:var(--sidebar-w);flex-shrink:0;background:var(--bg2);border-right:1px solid var(--border);display:flex;flex-direction:column;overflow:hidden;padding-bottom:var(--player-h)}
|
||||
.sidebar-header{padding:18px 18px 10px;display:flex;align-items:center;justify-content:space-between}
|
||||
.sidebar-logo{display:flex;align-items:center;gap:9px}
|
||||
.sidebar-logo .icon{width:30px;height:30px;border-radius:8px;background:linear-gradient(135deg,var(--accent),#c07010);display:flex;align-items:center;justify-content:center;font-size:14px;box-shadow:0 4px 12px var(--accent-glow)}
|
||||
.sidebar-logo span{font-size:16px;font-weight:700}
|
||||
.sidebar-logo em{color:var(--accent);font-style:normal}
|
||||
.header-btns{display:flex;gap:4px}
|
||||
.hbtn{background:none;border:none;color:var(--text3);cursor:pointer;font-size:14px;padding:5px;border-radius:6px;transition:color .2s;display:flex;align-items:center}
|
||||
.hbtn:hover{color:var(--text)}
|
||||
.hbtn.danger:hover{color:var(--danger)}
|
||||
|
||||
.search-wrap{padding:0 12px 10px}
|
||||
.search-box{display:flex;align-items:center;gap:8px;background:var(--bg3);border:1px solid var(--border);border-radius:var(--radius-sm);padding:8px 12px;cursor:text;transition:border-color .2s}
|
||||
.search-box:focus-within{border-color:var(--border2)}
|
||||
.search-box input{flex:1;background:none;border:none;outline:none;color:var(--text);font-family:var(--font);font-size:13px}
|
||||
.search-box input::placeholder{color:var(--text3)}
|
||||
.search-box svg{flex-shrink:0;color:var(--text3)}
|
||||
|
||||
nav{flex:1;overflow-y:auto;padding:4px 10px}
|
||||
.nav-section{margin-bottom:18px}
|
||||
.nav-section-title{font-size:10px;font-weight:600;letter-spacing:1px;text-transform:uppercase;color:var(--text3);padding:0 8px;margin-bottom:3px}
|
||||
.nav-item{display:flex;align-items:center;gap:10px;padding:8px 10px;border-radius:8px;cursor:pointer;font-size:13px;font-weight:500;color:var(--text2);transition:background .15s,color .15s;user-select:none}
|
||||
.nav-item:hover{background:var(--surface);color:var(--text)}
|
||||
.nav-item.active{background:var(--accent-dim);color:var(--accent)}
|
||||
.nav-item.active-blue{background:var(--blue-dim);color:var(--blue)}
|
||||
.nav-item svg{flex-shrink:0;width:15px;height:15px}
|
||||
|
||||
/* ── MAIN ── */
|
||||
.main{flex:1;overflow-y:auto;overflow-x:hidden;padding-bottom:var(--player-h);background:var(--bg);background-image:radial-gradient(ellipse 80% 40% at 50% 0%,rgba(232,160,32,.04) 0%,transparent 60%)}
|
||||
.page{display:none;padding:24px 28px}
|
||||
.page.active{display:block}
|
||||
.page-header{display:flex;align-items:center;justify-content:space-between;margin-bottom:24px;flex-wrap:wrap;gap:10px}
|
||||
.page-title{font-size:24px;font-weight:800;letter-spacing:-.5px}
|
||||
.page-title em{color:var(--accent);font-style:normal}
|
||||
.page-title .blue{color:var(--blue)}
|
||||
|
||||
/* ── SECTION ── */
|
||||
.section-header{display:flex;align-items:center;justify-content:space-between;margin-bottom:12px;margin-top:24px}
|
||||
.section-title{font-size:16px;font-weight:700}
|
||||
.section-more{font-size:12px;color:var(--text2);cursor:pointer;transition:color .15s}
|
||||
.section-more:hover{color:var(--accent)}
|
||||
|
||||
/* ── GRID ── */
|
||||
.grid{display:grid;gap:14px}
|
||||
.grid-4{grid-template-columns:repeat(auto-fill,minmax(150px,1fr))}
|
||||
.scroll-row{display:flex;gap:12px;overflow-x:auto;padding-bottom:8px;scrollbar-width:none}
|
||||
.scroll-row::-webkit-scrollbar{display:none}
|
||||
.scroll-row .card{flex-shrink:0;width:145px}
|
||||
|
||||
/* ── CARDS ── */
|
||||
.card{background:var(--surface);border:1px solid var(--border);border-radius:var(--radius);overflow:hidden;cursor:pointer;transition:transform .2s,box-shadow .2s,border-color .2s}
|
||||
.card:hover{transform:translateY(-3px);box-shadow:0 12px 32px rgba(0,0,0,.4);border-color:var(--border2)}
|
||||
.card-art{aspect-ratio:1;background:var(--surface2);position:relative;overflow:hidden}
|
||||
.card-art img{width:100%;height:100%;object-fit:cover;transition:transform .4s}
|
||||
.card:hover .card-art img{transform:scale(1.05)}
|
||||
.card-art .no-art{width:100%;height:100%;display:flex;align-items:center;justify-content:center;font-size:34px;color:var(--text3);background:linear-gradient(135deg,var(--surface2),var(--bg3))}
|
||||
.card-art .play-overlay{position:absolute;inset:0;display:flex;align-items:center;justify-content:center;background:rgba(0,0,0,.45);opacity:0;transition:opacity .2s;gap:8px}
|
||||
.card:hover .play-overlay{opacity:1}
|
||||
.play-btn{width:40px;height:40px;border-radius:50%;background:var(--accent);border:none;cursor:pointer;display:flex;align-items:center;justify-content:center;transform:scale(.8);transition:transform .2s;box-shadow:0 4px 16px rgba(0,0,0,.5)}
|
||||
.card:hover .play-btn{transform:scale(1)}
|
||||
.play-btn svg{color:#000}
|
||||
.dl-btn-overlay{width:36px;height:36px;border-radius:50%;background:rgba(255,255,255,.15);border:1px solid rgba(255,255,255,.3);cursor:pointer;display:flex;align-items:center;justify-content:center;transform:scale(.8);transition:transform .2s,background .2s;backdrop-filter:blur(4px)}
|
||||
.card:hover .dl-btn-overlay{transform:scale(1)}
|
||||
.dl-btn-overlay:hover{background:rgba(255,255,255,.3)}
|
||||
.dl-btn-overlay svg{color:#fff}
|
||||
.card-info{padding:10px}
|
||||
.card-title{font-size:12px;font-weight:600;white-space:nowrap;overflow:hidden;text-overflow:ellipsis}
|
||||
.card-sub{font-size:11px;color:var(--text2);margin-top:2px;white-space:nowrap;overflow:hidden;text-overflow:ellipsis}
|
||||
|
||||
/* ── BADGES ── */
|
||||
.badge{display:inline-flex;align-items:center;gap:4px;font-size:10px;font-weight:600;padding:3px 8px;border-radius:20px;letter-spacing:.3px}
|
||||
.badge-new{background:var(--blue-dim);color:var(--blue)}
|
||||
.badge-itunes{background:rgba(252,100,100,.12);color:#fc6464}
|
||||
.badge-local{background:var(--green-dim);color:var(--green)}
|
||||
|
||||
/* ── GLOBAL SEARCH RESULTS ── */
|
||||
.gtrack-item{display:grid;grid-template-columns:52px 1fr auto;gap:12px;align-items:center;padding:8px 12px;border-radius:9px;cursor:pointer;transition:background .15s}
|
||||
.gtrack-item:hover{background:var(--surface)}
|
||||
.gtrack-art{width:52px;height:52px;border-radius:6px;overflow:hidden;background:var(--surface2);flex-shrink:0}
|
||||
.gtrack-art img{width:100%;height:100%;object-fit:cover}
|
||||
.gtrack-info .gtrack-title{font-size:14px;font-weight:600}
|
||||
.gtrack-info .gtrack-sub{font-size:12px;color:var(--text2);margin-top:2px}
|
||||
.gtrack-actions{display:flex;align-items:center;gap:6px;opacity:0;transition:opacity .15s}
|
||||
.gtrack-item:hover .gtrack-actions{opacity:1}
|
||||
|
||||
/* ── ICON BTN ── */
|
||||
.icon-btn{background:none;border:none;color:var(--text2);cursor:pointer;padding:6px;border-radius:6px;display:flex;align-items:center;transition:color .15s,background .15s}
|
||||
.icon-btn:hover{color:var(--accent);background:var(--accent-dim)}
|
||||
.icon-btn.starred{color:var(--accent)}
|
||||
.icon-btn.blue:hover{color:var(--blue);background:var(--blue-dim)}
|
||||
.icon-btn.green:hover{color:var(--green);background:var(--green-dim)}
|
||||
|
||||
/* ── SONG LIST ── */
|
||||
.song-list{display:flex;flex-direction:column;gap:2px}
|
||||
.song-item{display:grid;grid-template-columns:32px 44px 1fr auto;align-items:center;gap:10px;padding:6px 10px;border-radius:8px;cursor:pointer;transition:background .15s}
|
||||
.song-thumb{width:44px;height:44px;border-radius:6px;overflow:hidden;background:var(--surface2);flex-shrink:0}
|
||||
.song-thumb img{width:100%;height:100%;object-fit:cover}
|
||||
.song-thumb .no-art{width:100%;height:100%;display:flex;align-items:center;justify-content:center;font-size:18px;color:var(--text3)}
|
||||
.song-item:hover{background:var(--surface)}
|
||||
.song-item.playing{background:var(--accent-dim)}
|
||||
.song-item.playing .song-title{color:var(--accent)}
|
||||
.song-num{font-size:12px;color:var(--text3);text-align:center;font-family:var(--mono)}
|
||||
.song-item.playing .song-num{color:var(--accent)}
|
||||
.song-info .song-title{font-size:13px;font-weight:500}.song-link{cursor:pointer;transition:color .15s}.song-link:hover{color:var(--accent);text-decoration:underline}
|
||||
.song-info .song-artist{font-size:11px;color:var(--text2);margin-top:1px}
|
||||
.song-meta{display:flex;align-items:center;gap:6px}
|
||||
.song-dur{font-size:11px;color:var(--text3);font-family:var(--mono)}
|
||||
.song-actions{display:flex;gap:2px;opacity:0;transition:opacity .15s}
|
||||
.song-item:hover .song-actions{opacity:1}
|
||||
|
||||
/* ── ALBUM DETAIL ── */
|
||||
.back-btn{background:none;border:none;color:var(--text2);cursor:pointer;display:flex;align-items:center;gap:6px;font-family:var(--font);font-size:12px;padding:6px 0;margin-bottom:16px;transition:color .15s}
|
||||
.back-btn:hover{color:var(--text)}
|
||||
.album-header{display:flex;gap:24px;align-items:flex-end;margin-bottom:28px}
|
||||
.album-cover{width:170px;height:170px;border-radius:12px;background:var(--surface2);flex-shrink:0;overflow:hidden;box-shadow:0 16px 48px rgba(0,0,0,.5)}
|
||||
.album-cover img{width:100%;height:100%;object-fit:cover}
|
||||
.album-cover .no-art{width:100%;height:100%;display:flex;align-items:center;justify-content:center;font-size:50px}
|
||||
.album-type{font-size:10px;text-transform:uppercase;letter-spacing:1px;color:var(--text2);margin-bottom:5px}
|
||||
.album-name{font-size:28px;font-weight:800;letter-spacing:-1px;margin-bottom:5px;line-height:1.1}
|
||||
.album-artist-name{font-size:14px;color:var(--text2);margin-bottom:10px;cursor:pointer}
|
||||
.album-artist-name:hover{color:var(--accent)}
|
||||
.album-stats{font-size:12px;color:var(--text3)}
|
||||
.album-actions{display:flex;gap:8px;margin-top:16px;flex-wrap:wrap}
|
||||
.btn-play-all{display:flex;align-items:center;gap:7px;padding:10px 20px;border-radius:25px;background:var(--accent);color:#000;border:none;font-family:var(--font);font-size:13px;font-weight:700;cursor:pointer;transition:opacity .15s,transform .15s}
|
||||
.btn-play-all:hover{opacity:.9;transform:scale(1.02)}
|
||||
.btn-outline{display:flex;align-items:center;gap:7px;padding:10px 18px;border-radius:25px;background:var(--surface);border:1px solid var(--border2);color:var(--text);font-family:var(--font);font-size:13px;font-weight:600;cursor:pointer;transition:background .15s}
|
||||
.btn-outline:hover{background:var(--surface2)}
|
||||
.btn-outline.blue{border-color:rgba(79,142,247,.4);color:var(--blue)}
|
||||
.btn-outline.blue:hover{background:var(--blue-dim)}
|
||||
|
||||
/* ── PLAYER ── */
|
||||
.player{position:fixed;bottom:0;left:0;right:0;height:var(--player-h);background:rgba(8,8,20,.93);backdrop-filter:blur(28px);border-top:1px solid var(--border2);display:grid;grid-template-columns:1fr auto 1fr;align-items:center;gap:0;padding:0 20px;z-index:100}
|
||||
.player-track{min-width:0;display:flex;align-items:center;gap:10px;overflow:hidden}
|
||||
.player-art{width:52px;height:52px;border-radius:8px;background:var(--surface2);flex-shrink:0;overflow:hidden;position:relative}
|
||||
.player-art img{width:100%;height:100%;object-fit:cover}
|
||||
.player-art .no-art{width:100%;height:100%;display:flex;align-items:center;justify-content:center;font-size:20px;color:var(--text3)}
|
||||
.player-art.spinning img{animation:spin 8s linear infinite}
|
||||
@keyframes spin{to{transform:rotate(360deg)}}
|
||||
.player-info{min-width:0}
|
||||
.player-title{font-size:13px;font-weight:600;white-space:nowrap;overflow:hidden;text-overflow:ellipsis;transition:color .15s}
|
||||
/* Player bar clickable title/artist */
|
||||
.player-title:hover{color:var(--accent);text-decoration:underline}
|
||||
#player-artist-name{color:var(--text2)}
|
||||
#player-artist-name:hover{color:var(--accent-2,#f0b840)}
|
||||
#player-album-name{color:var(--text3)}
|
||||
#player-album-name:hover{color:var(--accent)}
|
||||
|
||||
/* NP action sheet */
|
||||
.np-sheet-overlay{position:fixed;inset:0;background:rgba(0,0,0,.5);z-index:400;opacity:0;pointer-events:none;transition:opacity .25s;backdrop-filter:blur(2px)}
|
||||
.np-sheet-overlay.open{opacity:1;pointer-events:all}
|
||||
.np-sheet{position:fixed;bottom:0;left:0;right:0;background:var(--surface);border-radius:20px 20px 0 0;padding:16px 0 max(24px,env(safe-area-inset-bottom));z-index:401;transform:translateY(100%);transition:transform .3s cubic-bezier(.32,.72,0,1)}
|
||||
.np-sheet.open{transform:translateY(0)}
|
||||
.np-sheet-handle{width:36px;height:4px;background:var(--surface2);border-radius:2px;margin:0 auto 16px}
|
||||
.np-sheet-song{display:flex;align-items:center;gap:12px;padding:0 20px 16px;border-bottom:1px solid var(--border)}
|
||||
.np-sheet-song .thumb{width:44px;height:44px;border-radius:8px;overflow:hidden;background:var(--surface2);flex-shrink:0}
|
||||
.np-sheet-song .thumb img{width:100%;height:100%;object-fit:cover}
|
||||
.np-sheet-song .title{font-size:14px;font-weight:600}
|
||||
.np-sheet-song .sub{font-size:12px;color:var(--text2);margin-top:2px}
|
||||
.np-sheet-item{display:flex;align-items:center;gap:14px;padding:13px 20px;cursor:pointer;transition:background .12s;font-size:14px;font-weight:500}
|
||||
.np-sheet-item:hover{background:var(--surface2)}
|
||||
.np-sheet-item svg{color:var(--text2);flex-shrink:0}
|
||||
.np-sheet-item.danger{color:var(--danger)}
|
||||
.np-sheet-item.danger svg{color:var(--danger)}
|
||||
|
||||
.player-artist{font-size:11px;color:var(--text2);margin-top:2px}
|
||||
.player-badge{display:inline-block;font-size:9px;font-weight:700;padding:1px 5px;border-radius:3px;margin-left:5px;vertical-align:middle}
|
||||
.player-controls{display:flex;flex-direction:column;align-items:center;gap:7px;width:420px;padding:0 24px}
|
||||
.ctrl-buttons{display:flex;align-items:center;gap:10px}
|
||||
.ctrl-btn{background:none;border:none;color:var(--text2);cursor:pointer;padding:5px;border-radius:7px;display:flex;align-items:center;transition:color .15s}
|
||||
#btn-repeat{position:relative}
|
||||
#btn-repeat.repeat-one::after,#np-repeat-btn.repeat-one::after{content:'1';position:absolute;top:1px;right:1px;font-size:7px;font-weight:700;line-height:11px;text-align:center;width:11px;height:11px;border-radius:50%;background:var(--accent);color:#000;pointer-events:none}
|
||||
#np-repeat-btn{position:relative}
|
||||
.ctrl-btn:hover{color:var(--text)}
|
||||
.ctrl-btn.active{color:var(--accent)}
|
||||
.play-pause-btn{width:38px;height:38px;border-radius:50%;background:var(--accent);border:none;cursor:pointer;display:flex;align-items:center;justify-content:center;transition:transform .15s,background .15s;box-shadow:0 0 18px var(--accent-glow)}
|
||||
.play-pause-btn:hover{transform:scale(1.08);background:var(--accent2)}
|
||||
.play-pause-btn svg{color:#000}
|
||||
.progress-bar-wrap{width:100%;display:flex;align-items:center;gap:7px}
|
||||
.progress-time{font-size:10px;color:var(--text3);font-family:var(--mono);min-width:32px}
|
||||
.progress-time.right{text-align:right}
|
||||
.progress-bar{flex:1;height:6px;background:var(--surface2);border-radius:3px;cursor:pointer;position:relative}
|
||||
.progress-fill{height:100%;background:var(--accent);border-radius:3px;width:0%;transition:width .1s linear;position:relative}
|
||||
.progress-fill::after{content:'';position:absolute;right:-7px;top:50%;transform:translateY(-50%);width:14px;height:14px;border-radius:50%;background:var(--accent2);opacity:0;transition:opacity .15s;box-shadow:0 0 6px var(--accent-glow)}
|
||||
.progress-bar:hover .progress-fill::after{opacity:1}
|
||||
.player-right{display:flex;align-items:center;justify-content:flex-end;gap:6px}
|
||||
.volume-wrap{display:flex;align-items:center;gap:7px}
|
||||
.volume-slider{width:72px;height:3px;background:var(--surface2);border-radius:2px;cursor:pointer;appearance:none;-webkit-appearance:none;outline:none}
|
||||
.volume-slider::-webkit-slider-thumb{-webkit-appearance:none;width:11px;height:11px;border-radius:50%;background:var(--accent);cursor:pointer}
|
||||
|
||||
/* ── PREVIEW BAR ── */
|
||||
.preview-bar{position:fixed;bottom:var(--player-h);left:0;right:0;background:rgba(15,15,35,.95);backdrop-filter:blur(16px);border-top:1px solid var(--blue);padding:10px 20px;display:flex;align-items:center;gap:14px;z-index:99;transform:translateY(100%);transition:transform .3s;font-size:13px}
|
||||
.preview-bar.visible{transform:translateY(0)}
|
||||
.preview-info{flex:1;min-width:0}
|
||||
.preview-title{font-weight:600;white-space:nowrap;overflow:hidden;text-overflow:ellipsis}
|
||||
.preview-sub{font-size:11px;color:var(--text2);margin-top:2px}
|
||||
.preview-badge{font-size:10px;font-weight:700;color:var(--blue);background:var(--blue-dim);padding:2px 6px;border-radius:4px;margin-left:6px}
|
||||
.preview-prog{flex:1;height:2px;background:var(--surface2);border-radius:2px;overflow:hidden;max-width:200px}
|
||||
.preview-fill{height:100%;background:var(--blue);border-radius:2px;width:0%;transition:width .5s linear}
|
||||
.preview-close{background:none;border:none;color:var(--text3);cursor:pointer;font-size:18px;line-height:1;padding:2px 6px;border-radius:4px;transition:color .15s}
|
||||
.preview-close:hover{color:var(--text)}
|
||||
|
||||
/* ── QUEUE PANEL ── */
|
||||
.queue-panel{position:fixed;right:0;top:0;bottom:var(--player-h);width:290px;background:var(--bg2);border-left:1px solid var(--border);transform:translateX(100%);transition:transform .3s;z-index:90;display:flex;flex-direction:column;padding:18px 0}
|
||||
.queue-panel.open{transform:translateX(0)}
|
||||
.queue-title{font-size:15px;font-weight:700;padding:0 18px 14px;border-bottom:1px solid var(--border)}
|
||||
.queue-list{flex:1;overflow-y:auto;padding:6px 10px}
|
||||
.queue-item{display:flex;align-items:center;gap:9px;padding:7px 8px;border-radius:7px;cursor:pointer;transition:background .15s}
|
||||
.queue-item:hover{background:var(--surface)}
|
||||
.queue-item.active{background:var(--accent-dim)}
|
||||
.queue-item.active .qi-title{color:var(--accent)}
|
||||
.qi-idx{font-size:10px;color:var(--text3);width:16px;text-align:center;flex-shrink:0;font-family:var(--mono)}
|
||||
.qi-info{flex:1;min-width:0}
|
||||
.qi-title{font-size:12px;font-weight:500;white-space:nowrap;overflow:hidden;text-overflow:ellipsis}
|
||||
.qi-artist{font-size:10px;color:var(--text2)}
|
||||
|
||||
/* ── DOWNLOAD MODAL ── */
|
||||
.modal-overlay{position:fixed;inset:0;background:rgba(0,0,0,.7);backdrop-filter:blur(4px);z-index:200;display:flex;align-items:center;justify-content:center;opacity:0;pointer-events:none;transition:opacity .2s}
|
||||
.modal-overlay.open{opacity:1;pointer-events:all}
|
||||
.modal{width:min(460px,calc(100vw - 32px));background:var(--surface);border:1px solid var(--border2);border-radius:18px;padding:28px;box-shadow:0 40px 80px rgba(0,0,0,.7)}
|
||||
.modal h2{font-size:18px;font-weight:700;margin-bottom:6px}
|
||||
.modal .modal-sub{font-size:13px;color:var(--text2);margin-bottom:20px;line-height:1.5}
|
||||
.modal-art{width:64px;height:64px;border-radius:8px;overflow:hidden;background:var(--surface2);flex-shrink:0}
|
||||
.modal-art img{width:100%;height:100%;object-fit:cover}
|
||||
.modal-track-info{display:flex;gap:14px;align-items:center;padding:14px;background:var(--bg2);border-radius:10px;margin-bottom:18px}
|
||||
.mti-name{font-size:14px;font-weight:600}
|
||||
.mti-sub{font-size:12px;color:var(--text2);margin-top:3px}
|
||||
.modal-opts{display:flex;flex-direction:column;gap:8px;margin-bottom:20px}
|
||||
.modal-opt{display:flex;align-items:center;gap:10px;padding:12px 14px;background:var(--bg2);border:1px solid var(--border);border-radius:9px;cursor:pointer;transition:border-color .2s,background .2s}
|
||||
.modal-opt:hover{border-color:var(--border2);background:var(--bg3)}
|
||||
.modal-opt.selected{border-color:var(--blue);background:var(--blue-dim)}
|
||||
.modal-opt svg{color:var(--text2)}
|
||||
.modal-opt.selected svg{color:var(--blue)}
|
||||
.opt-label{font-size:13px;font-weight:600}
|
||||
.opt-sub{font-size:11px;color:var(--text2);margin-top:2px}
|
||||
.modal-disclaimer{font-size:11px;color:var(--text3);line-height:1.5;padding:10px 12px;background:var(--bg3);border-radius:7px;margin-bottom:16px}
|
||||
.modal-btns{display:flex;gap:8px}
|
||||
.modal-btns button{flex:1;padding:12px;border-radius:9px;font-family:var(--font);font-size:14px;font-weight:600;cursor:pointer;border:none;transition:opacity .15s}
|
||||
.btn-cancel{background:var(--surface2);color:var(--text2)}
|
||||
.btn-cancel:hover{opacity:.8}
|
||||
.btn-download{background:linear-gradient(135deg,var(--blue),#2d6ad4);color:#fff}
|
||||
.btn-download:hover{opacity:.9}
|
||||
.btn-download:disabled{opacity:.5;cursor:default}
|
||||
|
||||
/* ── DOWNLOAD JOBS ── */
|
||||
.jobs-panel{position:fixed;bottom:calc(var(--player-h) + 16px);right:20px;width:310px;display:flex;flex-direction:column;gap:8px;z-index:150}
|
||||
.job-card{background:var(--surface);border:1px solid var(--border2);border-radius:10px;padding:12px 14px;box-shadow:0 8px 24px rgba(0,0,0,.4);animation:slideUp .25s ease}
|
||||
@keyframes slideUp{from{opacity:0;transform:translateY(12px)}to{opacity:1;transform:translateY(0)}}
|
||||
.job-header{display:flex;align-items:center;justify-content:space-between;margin-bottom:8px}
|
||||
.job-title{font-size:12px;font-weight:600;flex:1;min-width:0;white-space:nowrap;overflow:hidden;text-overflow:ellipsis}
|
||||
.job-status{font-size:10px;color:var(--text2)}
|
||||
.job-bar{height:3px;background:var(--surface2);border-radius:2px;overflow:hidden}
|
||||
.job-fill{height:100%;background:linear-gradient(90deg,var(--blue),var(--accent));border-radius:2px;width:0%;transition:width .4s ease}
|
||||
.job-fill.done{background:var(--green);width:100%!important}
|
||||
.job-fill.error{background:var(--danger);width:100%!important}
|
||||
.job-sub{font-size:10px;color:var(--text3);margin-top:5px}
|
||||
|
||||
/* ── RECOMMENDATIONS ── */
|
||||
.reco-grid{display:flex;flex-direction:column;gap:3px;margin-bottom:24px}
|
||||
.reco-artist-chip{display:inline-flex;align-items:center;gap:7px;padding:7px 12px;background:var(--surface);border:1px solid var(--border);border-radius:8px;cursor:pointer;font-size:13px;font-weight:500;transition:border-color .2s,background .2s;margin:3px}
|
||||
.reco-artist-chip:hover{border-color:var(--blue);background:var(--blue-dim);color:var(--blue)}
|
||||
|
||||
|
||||
/* ── CONTEXT MENU ── */
|
||||
.ctx-menu{position:fixed;background:var(--surface);border:1px solid var(--border2);border-radius:10px;padding:5px;z-index:500;min-width:200px;box-shadow:0 16px 40px rgba(0,0,0,.6);animation:ctxIn .12s ease}
|
||||
@keyframes ctxIn{from{opacity:0;transform:scale(.95)}to{opacity:1;transform:scale(1)}}
|
||||
.ctx-item{display:flex;align-items:center;gap:10px;padding:9px 12px;border-radius:7px;cursor:pointer;font-size:13px;font-weight:500;color:var(--text);transition:background .12s}
|
||||
.ctx-item:hover{background:var(--surface2)}
|
||||
.ctx-item.danger{color:var(--danger)}
|
||||
.ctx-item.danger:hover{background:rgba(224,85,85,.12)}
|
||||
.ctx-item svg{flex-shrink:0;color:var(--text2)}
|
||||
.ctx-item:hover svg{color:inherit}
|
||||
.ctx-separator{height:1px;background:var(--border);margin:4px 0}
|
||||
.ctx-submenu-wrap{position:relative}
|
||||
.ctx-submenu{position:absolute;left:100%;top:0;background:var(--surface);border:1px solid var(--border2);border-radius:10px;padding:5px;min-width:180px;max-height:260px;overflow-y:auto;box-shadow:0 16px 40px rgba(0,0,0,.6);display:none;z-index:501}
|
||||
.ctx-submenu-wrap:hover .ctx-submenu{display:block}
|
||||
.ctx-submenu .ctx-item{white-space:nowrap;overflow:hidden;text-overflow:ellipsis}
|
||||
|
||||
/* ── PLAYLIST CREATE / ADD MODAL ── */
|
||||
.pl-modal{position:fixed;inset:0;background:rgba(0,0,0,.65);backdrop-filter:blur(4px);z-index:400;display:flex;align-items:center;justify-content:center;opacity:0;pointer-events:none;transition:opacity .2s}
|
||||
.pl-modal.open{opacity:1;pointer-events:all}
|
||||
.pl-modal .inner{width:min(380px,calc(100vw - 32px));background:var(--surface);border:1px solid var(--border2);border-radius:16px;padding:24px;box-shadow:0 32px 64px rgba(0,0,0,.7)}
|
||||
.pl-modal h3{font-size:17px;font-weight:700;margin-bottom:16px}
|
||||
.pl-list{display:flex;flex-direction:column;gap:4px;max-height:240px;overflow-y:auto;margin-bottom:14px}
|
||||
.pl-row{display:flex;align-items:center;gap:10px;padding:9px 10px;border-radius:8px;cursor:pointer;transition:background .12s;font-size:13px;font-weight:500}
|
||||
.pl-row:hover{background:var(--surface2)}
|
||||
.pl-row svg{color:var(--text2);flex-shrink:0}
|
||||
.pl-new-row{border-top:1px solid var(--border);margin-top:6px;padding-top:10px}
|
||||
.pl-input{width:100%;padding:9px 12px;background:var(--bg2);border:1px solid var(--border2);border-radius:8px;color:var(--text);font-family:var(--font);font-size:13px;outline:none;margin-bottom:10px;transition:border-color .2s}
|
||||
.pl-input:focus{border-color:var(--accent)}
|
||||
.pl-btns{display:flex;gap:8px}
|
||||
.pl-btns button{flex:1;padding:10px;border-radius:8px;font-family:var(--font);font-size:13px;font-weight:600;cursor:pointer;border:none}
|
||||
|
||||
/* ── SONG INFO MODAL ── */
|
||||
.info-modal{position:fixed;inset:0;background:rgba(0,0,0,.65);backdrop-filter:blur(4px);z-index:400;display:flex;align-items:center;justify-content:center;opacity:0;pointer-events:none;transition:opacity .2s}
|
||||
.info-modal.open{opacity:1;pointer-events:all}
|
||||
.info-modal .inner{width:min(420px,calc(100vw - 32px));background:var(--surface);border:1px solid var(--border2);border-radius:16px;padding:0;overflow:hidden;box-shadow:0 32px 64px rgba(0,0,0,.7)}
|
||||
.info-header{display:flex;gap:16px;padding:20px;background:var(--bg2)}
|
||||
.info-cover{width:80px;height:80px;border-radius:8px;overflow:hidden;background:var(--surface2);flex-shrink:0}
|
||||
.info-cover img{width:100%;height:100%;object-fit:cover}
|
||||
.info-top-title{font-size:18px;font-weight:800;margin-bottom:4px}
|
||||
.info-top-sub{font-size:13px;color:var(--text2)}
|
||||
.info-rows{padding:16px 20px;display:flex;flex-direction:column;gap:10px}
|
||||
.info-row{display:flex;justify-content:space-between;align-items:baseline;font-size:13px;border-bottom:1px solid var(--border);padding-bottom:8px}
|
||||
.info-row:last-child{border:none;padding:0}
|
||||
.info-label{color:var(--text2);font-weight:500;min-width:90px}
|
||||
.info-val{color:var(--text);text-align:right;word-break:break-all}
|
||||
.info-close{display:block;width:calc(100% - 40px);margin:0 20px 20px;padding:11px;border-radius:9px;background:var(--surface2);border:none;font-family:var(--font);font-size:14px;font-weight:600;color:var(--text);cursor:pointer}
|
||||
.info-close:hover{background:var(--surface)}
|
||||
|
||||
|
||||
/* ── BOTTOM NAV (PWA Mobile) ── */
|
||||
.bottom-nav{
|
||||
display:none;
|
||||
position:fixed;bottom:0;left:0;right:0;
|
||||
height:80px;
|
||||
background:rgba(8,8,20,.97);
|
||||
backdrop-filter:blur(28px);
|
||||
border-top:1px solid var(--border2);
|
||||
z-index:200;
|
||||
padding:0 4px;
|
||||
padding-bottom:env(safe-area-inset-bottom);
|
||||
}
|
||||
.bottom-nav-inner{
|
||||
display:flex;flex-direction:column;align-items:stretch;
|
||||
height:100%;max-width:500px;margin:0 auto;
|
||||
}
|
||||
.bnav-item{
|
||||
display:flex;flex-direction:column;align-items:center;gap:4px;
|
||||
padding:8px 12px;border-radius:12px;cursor:pointer;
|
||||
font-size:10px;font-weight:600;color:var(--text3);
|
||||
letter-spacing:.3px;min-width:52px;text-align:center;
|
||||
transition:color .15s,background .15s;
|
||||
border:none;background:none;font-family:var(--font);
|
||||
-webkit-tap-highlight-color:transparent;
|
||||
}
|
||||
.bnav-item.active{color:var(--accent)}
|
||||
.bnav-item.active svg{filter:drop-shadow(0 0 6px var(--accent-glow))}
|
||||
.bnav-item.active-blue{color:var(--blue)}
|
||||
.bnav-item svg{flex-shrink:0;transition:transform .15s}
|
||||
.bnav-item:active svg{transform:scale(.88)}
|
||||
/* Tab strip */
|
||||
.bnav-tabs{display:flex;justify-content:center;gap:6px;padding:5px 16px 2px;flex-shrink:0}
|
||||
.bnav-tab{flex:1;padding:3px 0;border-radius:7px;border:none;font-family:var(--font);font-size:10px;font-weight:700;letter-spacing:.5px;text-transform:uppercase;color:var(--text3);cursor:pointer;background:none;transition:color .15s,background .15s;-webkit-tap-highlight-color:transparent}
|
||||
.bnav-tab.active{background:var(--surface2);color:var(--text)}
|
||||
.bnav-pages{display:grid;grid-template-columns:repeat(4,1fr);align-items:center;width:100%;gap:6px;flex:1}
|
||||
|
||||
@media(max-width:768px){
|
||||
.bottom-nav{display:flex;flex-direction:column;justify-content:flex-start}
|
||||
/* player sits above bottom nav */
|
||||
.player{bottom:80px}
|
||||
/* push content above player + bottom nav */
|
||||
.main{padding-bottom:calc(var(--player-h) + 80px + env(safe-area-inset-bottom) + 64px)}
|
||||
.page{padding-bottom:calc(var(--player-h) + 80px + env(safe-area-inset-bottom) + 96px)}
|
||||
.queue-panel{bottom:calc(var(--player-h) + 80px)}
|
||||
.preview-bar{bottom:calc(var(--player-h) + 80px)}
|
||||
.toast-container{bottom:calc(var(--player-h) + 80px + 16px)}
|
||||
.jobs-panel{bottom:calc(var(--player-h) + 80px + 16px)}
|
||||
.bnav-pages{grid-template-columns:repeat(4,1fr);gap:6px}
|
||||
.bnav-item{min-width:0;width:100%;padding:8px 0 6px;box-sizing:border-box}
|
||||
#settings-modal{align-items:flex-start;padding-top:max(env(safe-area-inset-top),16px);padding-bottom:calc(var(--player-h) + 80px + env(safe-area-inset-bottom) + 8px)}
|
||||
#settings-modal .modal{max-height:100%!important;overflow:hidden}
|
||||
}
|
||||
|
||||
|
||||
/* ── NOW PLAYING FULL SCREEN ── */
|
||||
.np-overlay{
|
||||
position:fixed;inset:0;z-index:300;
|
||||
display:flex;flex-direction:column;
|
||||
background:var(--bg);
|
||||
transform:translateY(100%);
|
||||
transition:transform .4s cubic-bezier(.32,.72,0,1);
|
||||
overflow:hidden;
|
||||
}
|
||||
.np-overlay.open{transform:translateY(0)}
|
||||
|
||||
/* Blurred album art as background */
|
||||
.np-bg{
|
||||
position:absolute;inset:-40px;
|
||||
background-size:cover;background-position:center;
|
||||
filter:blur(60px) brightness(.35) saturate(1.4);
|
||||
transform:scale(1.1);
|
||||
transition:background-image .6s ease;
|
||||
z-index:0;
|
||||
}
|
||||
|
||||
.np-inner{
|
||||
position:relative;z-index:1;
|
||||
display:flex;flex-direction:column;
|
||||
align-items:center;
|
||||
height:100%;
|
||||
padding:env(safe-area-inset-top,20px) 32px 32px;
|
||||
padding-top:max(env(safe-area-inset-top),20px);
|
||||
gap:0;
|
||||
max-width:480px;
|
||||
margin:0 auto;
|
||||
width:100%;
|
||||
}
|
||||
|
||||
/* Top bar */
|
||||
.np-topbar{
|
||||
display:flex;align-items:center;justify-content:space-between;
|
||||
width:100%;margin-bottom:28px;
|
||||
}
|
||||
.np-close{
|
||||
background:rgba(255,255,255,.1);border:none;
|
||||
width:36px;height:36px;border-radius:50%;
|
||||
display:flex;align-items:center;justify-content:center;
|
||||
cursor:pointer;color:var(--text);
|
||||
backdrop-filter:blur(8px);
|
||||
transition:background .2s;
|
||||
}
|
||||
.np-close:hover{background:rgba(255,255,255,.18)}
|
||||
.np-close svg{pointer-events:none}
|
||||
.np-label{font-size:12px;font-weight:600;letter-spacing:1px;text-transform:uppercase;color:rgba(255,255,255,.5)}
|
||||
.np-menu-btn{
|
||||
background:rgba(255,255,255,.1);border:none;
|
||||
width:36px;height:36px;border-radius:50%;
|
||||
display:flex;align-items:center;justify-content:center;
|
||||
cursor:pointer;color:var(--text);backdrop-filter:blur(8px);
|
||||
transition:background .2s;
|
||||
}
|
||||
.np-menu-btn:hover{background:rgba(255,255,255,.18)}
|
||||
|
||||
/* Big cover art */
|
||||
.np-cover{
|
||||
width:min(300px, calc(100vw - 80px));
|
||||
height:min(300px, calc(100vw - 80px));
|
||||
border-radius:16px;
|
||||
overflow:hidden;
|
||||
background:var(--surface2);
|
||||
box-shadow:0 32px 80px rgba(0,0,0,.7);
|
||||
margin-bottom:36px;
|
||||
flex-shrink:0;
|
||||
transition:transform .3s;
|
||||
}
|
||||
.np-cover img{width:100%;height:100%;object-fit:cover}
|
||||
.np-cover.playing{animation:npPulse 4s ease-in-out infinite}
|
||||
@keyframes npPulse{0%,100%{transform:scale(1)}50%{transform:scale(1.025)}}
|
||||
.np-cover .no-art{width:100%;height:100%;display:flex;align-items:center;justify-content:center;font-size:80px}
|
||||
|
||||
/* Song info */
|
||||
.np-info{width:100%;display:flex;align-items:center;justify-content:space-between;margin-bottom:24px}
|
||||
.np-text{flex:1;min-width:0}
|
||||
.np-title{font-size:22px;font-weight:800;letter-spacing:-.4px;white-space:nowrap;overflow:hidden;text-overflow:ellipsis;margin-bottom:5px}
|
||||
.np-artist{font-size:15px;color:rgba(255,255,255,.65);white-space:nowrap;overflow:hidden;text-overflow:ellipsis;transition:color .15s}
|
||||
#np-artist-name:hover,#np-album-name:hover{color:var(--accent);cursor:pointer}
|
||||
.np-star{
|
||||
background:none;border:none;cursor:pointer;
|
||||
padding:8px;margin-left:8px;flex-shrink:0;
|
||||
display:flex;align-items:center;
|
||||
transition:transform .15s;
|
||||
}
|
||||
.np-star:hover{transform:scale(1.15)}
|
||||
|
||||
/* Progress */
|
||||
.np-progress{width:100%;margin-bottom:10px}
|
||||
.np-bar{
|
||||
width:100%;height:4px;background:rgba(255,255,255,.2);
|
||||
border-radius:2px;cursor:pointer;position:relative;margin-bottom:10px;
|
||||
}
|
||||
.np-bar-fill{
|
||||
height:100%;background:#fff;border-radius:2px;
|
||||
width:0%;transition:width .1s linear;position:relative;
|
||||
}
|
||||
.np-bar-fill::after{
|
||||
content:'';position:absolute;right:-7px;top:50%;
|
||||
transform:translateY(-50%);
|
||||
width:14px;height:14px;border-radius:50%;background:#fff;
|
||||
opacity:0;transition:opacity .15s;
|
||||
box-shadow:0 0 8px rgba(255,255,255,.6);
|
||||
}
|
||||
.np-bar:hover .np-bar-fill::after{opacity:1}
|
||||
.np-times{display:flex;justify-content:space-between;font-size:11px;color:rgba(255,255,255,.45);font-family:var(--mono)}
|
||||
|
||||
/* Controls */
|
||||
.np-controls{display:flex;align-items:center;justify-content:space-between;width:100%;margin-bottom:28px}
|
||||
.np-ctrl{
|
||||
background:none;border:none;color:rgba(255,255,255,.7);
|
||||
cursor:pointer;padding:10px;border-radius:50%;
|
||||
display:flex;align-items:center;justify-content:center;
|
||||
transition:color .15s,transform .15s,background .15s;
|
||||
}
|
||||
.np-ctrl:hover{color:#fff;background:rgba(255,255,255,.08)}
|
||||
.np-ctrl.active{color:var(--accent)}
|
||||
.np-play{
|
||||
width:64px;height:64px;border-radius:50%;
|
||||
background:#fff;border:none;cursor:pointer;
|
||||
display:flex;align-items:center;justify-content:center;
|
||||
box-shadow:0 8px 32px rgba(0,0,0,.4);
|
||||
transition:transform .15s;
|
||||
}
|
||||
.np-play:hover{transform:scale(1.06)}
|
||||
.np-play:active{transform:scale(.95)}
|
||||
.np-play svg{color:#000}
|
||||
|
||||
/* Volume + extras */
|
||||
.np-extras{display:flex;align-items:center;justify-content:space-between;width:100%;gap:12px}
|
||||
.np-vol{display:flex;align-items:center;gap:10px;flex:1}
|
||||
.np-vol-slider{
|
||||
flex:1;height:3px;background:rgba(255,255,255,.2);
|
||||
border-radius:2px;cursor:pointer;
|
||||
appearance:none;-webkit-appearance:none;outline:none;
|
||||
}
|
||||
.np-vol-slider::-webkit-slider-thumb{
|
||||
-webkit-appearance:none;width:13px;height:13px;
|
||||
border-radius:50%;background:#fff;cursor:pointer;
|
||||
}
|
||||
.np-icon-btn{
|
||||
background:none;border:none;color:rgba(255,255,255,.6);
|
||||
cursor:pointer;padding:8px;border-radius:50%;
|
||||
display:flex;align-items:center;
|
||||
transition:color .15s,background .15s;
|
||||
}
|
||||
.np-icon-btn:hover{color:#fff;background:rgba(255,255,255,.08)}
|
||||
/* NP tabs */
|
||||
.np-tabs{display:flex;gap:2px;background:rgba(255,255,255,.08);border-radius:20px;padding:3px}
|
||||
.np-tab{background:none;border:none;color:rgba(255,255,255,.45);font-size:11px;font-weight:700;letter-spacing:.5px;text-transform:uppercase;padding:5px 16px;border-radius:16px;cursor:pointer;transition:all .2s;font-family:var(--font)}
|
||||
.np-tab.active{background:rgba(255,255,255,.15);color:#fff}
|
||||
/* NP queue view */
|
||||
#np-player-view{display:flex;flex-direction:column;align-items:center;width:100%;flex:1;min-height:0}
|
||||
#np-queue-view{display:none;flex-direction:column;width:100%;flex:1;overflow-y:auto;padding-top:8px}
|
||||
.np-overlay.queue-mode #np-player-view{display:none}
|
||||
.np-overlay.queue-mode #np-queue-view{display:flex}
|
||||
.np-queue-item{display:flex;align-items:center;gap:10px;padding:9px 6px;border-radius:10px;cursor:pointer;transition:background .15s}
|
||||
.np-queue-item:hover{background:rgba(255,255,255,.06)}
|
||||
.np-queue-item.nq-active .nqi-title{color:var(--accent)}
|
||||
.nqi-num{font-size:11px;color:rgba(255,255,255,.28);width:20px;text-align:center;flex-shrink:0;font-family:var(--mono)}
|
||||
.nqi-art{width:38px;height:38px;border-radius:7px;overflow:hidden;background:rgba(255,255,255,.08);flex-shrink:0;display:flex;align-items:center;justify-content:center;font-size:16px}
|
||||
.nqi-art img{width:100%;height:100%;object-fit:cover}
|
||||
.nqi-info{flex:1;min-width:0}
|
||||
.nqi-title{font-size:13px;font-weight:500;white-space:nowrap;overflow:hidden;text-overflow:ellipsis;color:rgba(255,255,255,.9)}
|
||||
.nqi-artist{font-size:11px;color:rgba(255,255,255,.4);margin-top:2px}
|
||||
|
||||
/* player-art clickable */
|
||||
#player-art{cursor:pointer}
|
||||
#player-art:hover img{opacity:.85}
|
||||
|
||||
/* ── RADIO SHUFFLE BUTTON ── */
|
||||
.btn-radio{display:flex;align-items:center;gap:7px;padding:9px 18px;border-radius:25px;background:linear-gradient(135deg,#4f8ef7,#2d6ad4);color:#fff;border:none;font-family:var(--font);font-size:13px;font-weight:700;cursor:pointer;transition:opacity .15s,transform .15s;box-shadow:0 0 18px rgba(79,142,247,.35)}
|
||||
.btn-radio:hover{opacity:.9;transform:scale(1.02)}
|
||||
.btn-radio.active{background:linear-gradient(135deg,var(--danger),#c03030);box-shadow:0 0 18px rgba(224,85,85,.4)}
|
||||
|
||||
/* ── ARTIST PAGE ── */
|
||||
|
||||
/* ── SHUFFLE/REPEAT STATUS BADGES ── */
|
||||
.player-status-row{display:flex;align-items:center;gap:6px;margin-top:3px;min-height:16px}
|
||||
.status-badge{font-size:9px;font-weight:700;letter-spacing:.4px;padding:2px 6px;border-radius:10px;text-transform:uppercase;opacity:0;transition:opacity .2s,transform .2s;transform:scale(.8);pointer-events:none}
|
||||
.status-badge.on{opacity:1;transform:scale(1)}
|
||||
.badge-shuffle{background:var(--accent-dim);color:var(--accent)}
|
||||
.badge-repeat{background:var(--blue-dim);color:var(--blue)}
|
||||
|
||||
/* ── LAST.FM ARTIST INFO ── */
|
||||
.artist-bio{font-size:13px;color:var(--text2);line-height:1.65;margin-bottom:20px;padding:16px;background:var(--surface);border-radius:10px;border:1px solid var(--border)}
|
||||
.artist-bio a{color:var(--accent);text-decoration:none}
|
||||
.similar-chips{display:flex;flex-wrap:wrap;gap:6px;margin-bottom:24px}
|
||||
.similar-chip{padding:6px 12px;background:var(--surface);border:1px solid var(--border2);border-radius:20px;font-size:12px;font-weight:600;cursor:pointer;transition:border-color .15s,color .15s}
|
||||
.similar-chip:hover{border-color:var(--accent);color:var(--accent)}
|
||||
.topsong-item{display:grid;grid-template-columns:28px 44px 1fr auto;align-items:center;gap:10px;padding:7px 10px;border-radius:8px;cursor:pointer;transition:background .15s}
|
||||
.topsong-item:hover{background:var(--surface)}
|
||||
.topsong-num{font-size:11px;color:var(--text3);font-family:var(--mono);text-align:center}
|
||||
.topsong-thumb{width:44px;height:44px;border-radius:6px;overflow:hidden;background:var(--surface2)}
|
||||
.topsong-thumb img{width:100%;height:100%;object-fit:cover}
|
||||
.topsong-title{font-size:13px;font-weight:500}
|
||||
.topsong-plays{font-size:11px;color:var(--text3);font-family:var(--mono)}
|
||||
|
||||
/* ── NP MOBILE SWIPE ── */
|
||||
.np-drag-handle{width:100%;height:36px;display:flex;align-items:center;justify-content:center;cursor:grab;flex-shrink:0;touch-action:none}
|
||||
.np-drag-handle-bar{width:40px;height:4px;background:rgba(255,255,255,.25);border-radius:2px}
|
||||
|
||||
.artist-hero{height:180px;border-radius:14px;overflow:hidden;position:relative;margin-bottom:24px;background:linear-gradient(135deg,var(--surface2),var(--surface))}
|
||||
.artist-hero-overlay{position:absolute;inset:0;background:linear-gradient(to right,rgba(7,7,15,.85) 0%,transparent 100%);display:flex;align-items:flex-end;padding:20px}
|
||||
.artist-hero-name{font-size:32px;font-weight:800;letter-spacing:-1px}
|
||||
|
||||
/* ── MISC ── */
|
||||
.spinner{width:28px;height:28px;border:3px solid var(--surface2);border-top-color:var(--accent);border-radius:50%;animation:spin .8s linear infinite;margin:50px auto;display:block}
|
||||
.empty-state{display:flex;flex-direction:column;align-items:center;justify-content:center;padding:70px 20px;color:var(--text3);text-align:center}
|
||||
.empty-state .icon{font-size:44px;margin-bottom:14px}
|
||||
.empty-state p{font-size:14px}
|
||||
.toast-container{position:fixed;bottom:calc(var(--player-h) + 16px);left:50%;transform:translateX(-50%);z-index:201;display:flex;flex-direction:column;align-items:center;gap:6px}
|
||||
.toast{background:var(--surface);border:1px solid var(--border2);color:var(--text);font-size:12px;padding:9px 14px;border-radius:8px;box-shadow:0 8px 24px rgba(0,0,0,.4);animation:toastIn .25s ease,toastOut .25s ease 2.75s forwards;white-space:nowrap}
|
||||
@keyframes toastIn{from{opacity:0;transform:translateY(8px)}to{opacity:1;transform:translateY(0)}}
|
||||
@keyframes toastOut{to{opacity:0;transform:translateY(8px)}}
|
||||
|
||||
/* ── SETTINGS MODAL ── */
|
||||
.settings-section{margin-bottom:20px}
|
||||
.settings-section h3{font-size:12px;font-weight:600;text-transform:uppercase;letter-spacing:.5px;color:var(--text2);margin-bottom:10px}
|
||||
.settings-row{display:flex;align-items:center;justify-content:space-between;padding:10px 12px;background:var(--bg2);border-radius:8px;margin-bottom:6px}
|
||||
.settings-row label{font-size:13px;font-weight:500}
|
||||
.settings-row .hint{font-size:11px;color:var(--text3);margin-top:2px}
|
||||
.toggle{width:36px;height:20px;background:var(--surface2);border-radius:10px;cursor:pointer;position:relative;transition:background .2s;flex-shrink:0}
|
||||
.toggle.on{background:var(--accent)}
|
||||
.toggle::after{content:'';position:absolute;width:14px;height:14px;background:#fff;border-radius:50%;top:3px;left:3px;transition:transform .2s}
|
||||
.toggle.on::after{transform:translateX(16px)}
|
||||
.status-dot{width:8px;height:8px;border-radius:50%;background:var(--surface2);flex-shrink:0}
|
||||
.status-dot.ok{background:var(--green)}
|
||||
.status-dot.err{background:var(--danger)}
|
||||
|
||||
/* ── RESPONSIVE ── */
|
||||
@media(max-width:768px){
|
||||
:root{--sidebar-w:0px}
|
||||
.sidebar{position:fixed;left:-260px;width:260px;height:100vh;z-index:150;transition:left .3s;border-right:1px solid var(--border2)}
|
||||
.sidebar.mobile-open{left:0;box-shadow:10px 0 40px rgba(0,0,0,.6)}
|
||||
.page{padding:16px 14px}
|
||||
.album-header{flex-direction:column;align-items:flex-start}
|
||||
.album-cover{width:130px;height:130px}
|
||||
.album-name{font-size:22px}
|
||||
.player-controls{width:auto;padding:0 8px;min-width:160px}
|
||||
.player-right .volume-wrap{display:none}
|
||||
.mobile-menu-btn{display:flex!important}
|
||||
.jobs-panel{left:16px;right:16px;width:auto}
|
||||
}
|
||||
.mobile-menu-btn{display:none;background:none;border:none;color:var(--text);cursor:pointer;padding:8px}
|
||||
@media(max-width:768px){
|
||||
:root{--player-h:62px}
|
||||
/* Single-row compact player: [art][info+star][prev][play][next] + thin progress strip */
|
||||
.player{
|
||||
display:grid!important;
|
||||
grid-template-columns:1fr auto!important;
|
||||
grid-template-rows:1fr!important;
|
||||
padding:0 10px 14px;
|
||||
gap:0 8px;
|
||||
align-items:center;
|
||||
}
|
||||
.player .player-art{width:36px!important;height:36px!important;border-radius:6px;flex-shrink:0}
|
||||
.player-track{max-width:none!important;width:100%!important;gap:8px}
|
||||
.player-info{flex:1;min-width:0;position:relative}
|
||||
.player-info>div:first-child{justify-content:flex-start!important;padding-right:24px}
|
||||
.player-title{flex:1;min-width:0;font-size:12px!important}
|
||||
.player-artist{font-size:10px!important;margin-top:1px}
|
||||
#btn-star-current{position:absolute;right:0;top:50%;transform:translateY(-50%);flex-shrink:0}
|
||||
.player-status-row{display:none}
|
||||
/* Controls: horizontal row with only prev/play/next */
|
||||
.player-controls{display:flex!important;flex-direction:row!important;width:auto!important;padding:0!important;gap:0!important;align-items:center;flex-shrink:0;min-width:0!important}
|
||||
.ctrl-buttons{gap:2px}
|
||||
#btn-shuffle,#btn-repeat{display:none!important}
|
||||
.play-pause-btn{width:40px;height:40px}
|
||||
/* Thin progress strip pinned to bottom of player bar */
|
||||
.progress-bar-wrap{position:absolute;bottom:0;left:0;right:0;height:14px;padding:0 10px;gap:5px;display:flex;align-items:center}
|
||||
#time-total{display:none!important}
|
||||
.progress-time{font-size:9px!important;min-width:22px!important;color:var(--text3)}
|
||||
.progress-bar{height:2px!important;border-radius:0}
|
||||
.progress-fill::after{display:none}
|
||||
.player-right{display:none!important}
|
||||
}
|
||||
.mobile-overlay{display:none;position:fixed;inset:0;background:rgba(0,0,0,.6);z-index:140}
|
||||
.mobile-overlay.active{display:block}
|
||||
|
||||
/* ── SEARCH page ── */
|
||||
.search-section{margin-bottom:24px}
|
||||
.search-section-title{font-size:11px;font-weight:700;color:var(--text2);text-transform:uppercase;letter-spacing:.5px;margin-bottom:8px}
|
||||
@@ -0,0 +1,32 @@
|
||||
const CACHE = 'ns-v12';
|
||||
const PRECACHE = ['/', 'index.html', 'styles.css', 'app.js', 'sw.js'];
|
||||
const SKIP_URLS = ['/rest/stream', '/rest/download'];
|
||||
|
||||
self.addEventListener('install', e => {
|
||||
e.waitUntil(caches.open(CACHE).then(c => c.addAll(PRECACHE)));
|
||||
self.skipWaiting();
|
||||
});
|
||||
|
||||
self.addEventListener('activate', e => {
|
||||
e.waitUntil(
|
||||
caches.keys().then(keys =>
|
||||
Promise.all(keys.filter(k => k !== CACHE).map(k => caches.delete(k)))
|
||||
)
|
||||
);
|
||||
self.clients.claim();
|
||||
});
|
||||
|
||||
self.addEventListener('fetch', e => {
|
||||
if (SKIP_URLS.some(u => e.request.url.includes(u))) return;
|
||||
e.respondWith(
|
||||
caches.match(e.request).then(cached =>
|
||||
cached || fetch(e.request).then(res => {
|
||||
if (res.status === 200 && e.request.method === 'GET') {
|
||||
const clone = res.clone();
|
||||
caches.open(CACHE).then(c => c.put(e.request, clone));
|
||||
}
|
||||
return res;
|
||||
}).catch(() => cached)
|
||||
)
|
||||
);
|
||||
});
|
||||
Reference in new issue
Block a user