- Android: the artist page's main list and the "Alle anzeigen" album overlay (grid scroll + search query) are hoisted into the Artist/Explore ViewModels, so drilling into an album/artist and back keeps position + query. Covers the library artist and Deezer (Explore) artist screens. - Web: the "Android App" download section now sits at the very bottom of Settings, below the Navidrome-Server (account) section. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
1214 lines
79 KiB
HTML
1214 lines
79 KiB
HTML
<!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="mobile-web-app-capable" content="yes">
|
||
<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">
|
||
<link rel="manifest" href="manifest.json">
|
||
<link rel="icon" href="icon.svg" type="image/svg+xml">
|
||
<link rel="icon" href="icon-192.png" type="image/png" sizes="192x192">
|
||
<link rel="apple-touch-icon" href="icon.svg">
|
||
<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">
|
||
<script>window.NS_SERVER_URL='';window.NS_BACKEND_URL='';</script>
|
||
<script src="config.js" onerror="0"></script>
|
||
<script type="module" src="/src/main.js"></script>
|
||
</head>
|
||
<body>
|
||
<script>(function(){try{if(localStorage.getItem('ns_cfg'))document.documentElement.classList.add('ns-authed');}catch(e){}})()</script>
|
||
|
||
<!-- ─── 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-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>
|
||
<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">
|
||
<img src="icon.svg" width="30" height="30" style="border-radius:8px;flex-shrink:0">
|
||
<span>Nova<em>Sonic</em></span>
|
||
</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('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 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>
|
||
<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" id="nav-abs-section" style="display:none">
|
||
<div class="nav-section-title">Hörbücher</div>
|
||
<div class="nav-item" onclick="navigate('audiobooks')" data-page="audiobooks">
|
||
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><path d="M2 3h6a4 4 0 0 1 4 4v14a3 3 0 0 0-3-3H2z"/><path d="M22 3h-6a4 4 0 0 0-4 4v14a3 3 0 0 1 3-3h7z"/></svg>Bibliothek
|
||
</div>
|
||
</div>
|
||
</nav>
|
||
</aside>
|
||
|
||
<div class="mobile-overlay" id="mobile-overlay" onclick="closeSidebar()"></div>
|
||
|
||
<!-- MAIN -->
|
||
<main class="main" id="main-content">
|
||
|
||
<!-- Mobile header -->
|
||
<div id="mobile-header" style="position:sticky;top:0;background:var(--bg);z-index:10;border-bottom:1px solid var(--border)">
|
||
<div style="display:flex;align-items:center;gap:8px;padding:10px 12px">
|
||
<div class="header-search-wrap">
|
||
<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="header-search-input" type="text" placeholder="Suchen…" oninput="onHeaderSearch(this.value)" autocomplete="off">
|
||
<button id="header-search-clear" onclick="closeHeaderSearch()" style="display:none;background:none;border:none;color:var(--text3);cursor:pointer;font-size:15px;padding:0;line-height:1;flex-shrink:0">✕</button>
|
||
</div>
|
||
<div style="display:flex;align-items:center;gap:2px;flex-shrink:0">
|
||
<button class="hbtn notif-bell-btn" onclick="toggleNotifPanel()" title="Benachrichtigungen" style="padding:8px;color:var(--text2)">
|
||
<svg width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><path d="M18 8A6 6 0 0 0 6 8c0 7-3 9-3 9h18s-3-2-3-9"/><path d="M13.73 21a2 2 0 0 1-3.46 0"/></svg>
|
||
<span class="notif-badge" style="display:none">0</span>
|
||
</button>
|
||
<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>
|
||
</div>
|
||
<!-- Results panel: spans the full main-content width (anchored to the header, which already
|
||
sits right of the sidebar) so it never covers the sidebar and leaves no dead scroll strip -->
|
||
<div id="header-search-overlay" style="display:none">
|
||
<div id="header-search-results"></div>
|
||
</div>
|
||
</div>
|
||
|
||
<!-- HOME -->
|
||
<div class="page active" id="page-home">
|
||
<div id="home-greeting" class="home-greeting"></div>
|
||
<div id="home-hero" class="home-hero"></div>
|
||
<div class="section-header" style="margin-top:20px"><span class="section-title">Genres</span><span class="section-more" id="home-genres-more" onclick="toggleAllGenres()" style="display:none">Alle anzeigen</span></div>
|
||
<div id="home-genres" class="genre-grid"></div>
|
||
<div class="section-header" style="margin-top:20px"><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">Zuletzt gespielt</span></div>
|
||
<div class="scroll-row" id="home-recent"></div>
|
||
<div class="section-header"><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">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-bg" id="artist-hero-bg"></div><div class="artist-hero-overlay"><div class="artist-hero-pfp-wrap" id="artist-hero-pfp-wrap"><span class="artist-hero-pfp-initial" id="artist-hero-pfp-initial"></span></div><div class="artist-hero-info"><div class="artist-hero-name" id="artist-detail-name"></div><div class="artist-hero-fans" id="artist-detail-fans"></div></div></div></div>
|
||
<div id="artist-action-bar" class="artist-action-bar"></div>
|
||
<div id="artist-bio-section"></div>
|
||
<div id="artist-similar-section"></div>
|
||
<div id="artist-topsongs-section"></div>
|
||
<div id="artist-albums-section"></div>
|
||
</div>
|
||
|
||
<!-- ARTIST ALL SONGS -->
|
||
<div class="page" id="page-artist-all-songs">
|
||
<button class="back-btn" id="artist-songs-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="page-header" style="margin-top:8px"><div class="page-title" id="artist-songs-title">Alle Songs</div></div>
|
||
<div id="artist-songs-list"></div>
|
||
</div>
|
||
|
||
<!-- DEEZER ALBUM DETAIL -->
|
||
<div class="page" id="page-deezer-album-detail">
|
||
<button class="back-btn" id="deezer-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="deezer-album-cover-wrap"><div class="no-art">💿</div></div>
|
||
<div class="album-meta">
|
||
<div class="album-type">Deezer</div>
|
||
<div class="album-name" id="deezer-album-detail-name">—</div>
|
||
<div class="album-artist-name" id="deezer-album-detail-artist"></div>
|
||
<div class="album-stats" id="deezer-album-detail-stats"></div>
|
||
<div class="album-actions">
|
||
<button class="btn-outline" id="deezer-album-dl-btn"><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>Album laden</button>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
<div id="deezer-album-songs"></div>
|
||
</div>
|
||
|
||
<!-- PARTIAL ALBUM DETAIL (incomplete album: downloaded + missing tracks) -->
|
||
<div class="page" id="page-partial-album-detail">
|
||
<button class="back-btn" id="partial-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="partial-album-cover-wrap"><div class="no-art">💿</div></div>
|
||
<div class="album-meta">
|
||
<div class="album-type">Unvollständig</div>
|
||
<div class="album-name" id="partial-album-detail-name">—</div>
|
||
<div class="album-artist-name" id="partial-album-detail-artist"></div>
|
||
<div class="album-stats" id="partial-album-detail-stats"></div>
|
||
<div class="album-actions">
|
||
<button class="btn-play-all" id="partial-album-play-btn"><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="partial-album-dl-btn"><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>Fehlende laden</button>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
<div id="partial-album-downloaded"></div>
|
||
<div id="partial-album-missing"></div>
|
||
</div>
|
||
|
||
<!-- ALBUMS -->
|
||
<div class="page" id="page-albums">
|
||
<div class="page-header"><div class="page-title">Alben</div></div>
|
||
<div id="albums-sort-chips" class="sort-chips">
|
||
<button class="sort-chip active" data-sort="alphabeticalByName" onclick="loadAlbums('alphabeticalByName')">A–Z</button>
|
||
<button class="sort-chip" data-sort="newest" onclick="loadAlbums('newest')">Neueste</button>
|
||
<button class="sort-chip" data-sort="recent" onclick="loadAlbums('recent')">Zuletzt gespielt</button>
|
||
<button class="sort-chip" data-sort="frequent" onclick="loadAlbums('frequent')">Meistgespielt</button>
|
||
</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>
|
||
|
||
<!-- GENRE DETAIL -->
|
||
<div class="page" id="page-genre-detail">
|
||
<button class="back-btn" id="genre-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="genre-cover-wrap" style="display:flex;align-items:center;justify-content:center;font-size:40px">🎵</div>
|
||
<div class="album-meta">
|
||
<div class="album-type">Genre</div>
|
||
<div class="album-name" id="genre-detail-name">—</div>
|
||
<div class="album-stats" id="genre-detail-stats"></div>
|
||
<div class="album-actions">
|
||
<button class="btn-play-all" id="btn-play-genre"><svg width="13" height="13" viewBox="0 0 24 24" fill="currentColor"><polygon points="5 3 19 12 5 21 5 3"/></svg>Shuffle</button>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
<div class="song-list" id="genre-songs"></div>
|
||
</div>
|
||
|
||
<!-- STARRED -->
|
||
<div class="page" id="page-starred">
|
||
<div class="page-header"><div class="page-title">Favoriten ⭐</div></div>
|
||
<div id="starred-sort-chips" class="sort-chips">
|
||
<button class="sort-chip active" data-sort="newest" onclick="loadStarred('newest')">Neueste</button>
|
||
<button class="sort-chip" data-sort="oldest" onclick="loadStarred('oldest')">Älteste</button>
|
||
<button class="sort-chip" data-sort="az" onclick="loadStarred('az')">A–Z</button>
|
||
<button class="sort-chip" data-sort="artist" onclick="loadStarred('artist')">Künstler</button>
|
||
<button class="sort-chip" data-sort="plays" onclick="loadStarred('plays')">Meistgespielt</button>
|
||
</div>
|
||
<div class="list-search-wrap">
|
||
<svg width="14" height="14" 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>
|
||
<input id="starred-search" class="list-search" type="text" placeholder="In Favoriten suchen…" oninput="filterStarred(this.value)">
|
||
</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="playlist-detail-desc" id="playlist-detail-desc"></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>
|
||
<button class="btn-outline" id="btn-edit-playlist"><svg width="13" height="13" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><path d="M11 4H4a2 2 0 0 0-2 2v14a2 2 0 0 0 2 2h14a2 2 0 0 0 2-2v-7"/><path d="M18.5 2.5a2.121 2.121 0 0 1 3 3L12 15l-4 1 1-4 9.5-9.5z"/></svg>Bearbeiten</button>
|
||
<button class="btn-outline" id="btn-reorder-playlist"><svg width="13" height="13" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><line x1="3" y1="6" x2="21" y2="6"/><line x1="3" y1="12" x2="21" y2="12"/><line x1="3" y1="18" x2="21" y2="18"/></svg>Neu anordnen</button>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
<div class="list-search-wrap">
|
||
<svg width="14" height="14" 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>
|
||
<input id="playlist-search" class="list-search" type="text" placeholder="In Playlist suchen…" oninput="filterPlaylist(this.value)">
|
||
</div>
|
||
<div class="pl-reorder-bar" id="pl-reorder-bar" style="display:none">
|
||
<span>Reihenfolge ändern</span>
|
||
<div style="display:flex;gap:8px">
|
||
<button class="edit-pl-btn-cancel" onclick="plReorderCancel()">Abbrechen</button>
|
||
<button class="edit-pl-btn-save" onclick="plReorderSave()">Speichern</button>
|
||
</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) — Deezer search + link importer unified in ONE field -->
|
||
<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-deezer" style="font-size:11px;padding:4px 10px">Deezer</span>
|
||
</div>
|
||
<div class="search-wrap" style="padding:0 0 12px">
|
||
<div style="display:flex;gap:8px">
|
||
<div class="search-box" style="background:var(--surface);border-color:var(--border2);flex:1">
|
||
<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>
|
||
<div id="importer-badge" style="display:none;font-size:10px;font-weight:700;padding:2px 7px;border-radius:10px;background:var(--surface2);color:var(--text3);white-space:nowrap;flex-shrink:0">URL</div>
|
||
<input id="global-search-input" type="text" placeholder="Künstler, Titel, Album — oder Link zum Importieren einfügen…" oninput="onGlobalInput(this.value)" onkeydown="if(event.key==='Enter'){event.preventDefault();tryImporterResolve()}" style="font-size:15px">
|
||
</div>
|
||
<button id="importer-btn" class="btn-outline" style="display:none;padding:0 16px;font-size:13px;font-weight:700;white-space:nowrap;flex-shrink:0" onclick="resolveImporterLink()">Importieren</button>
|
||
</div>
|
||
</div>
|
||
|
||
<!-- Link importer — auto-detects Spotify / SoundCloud / YouTube; shown only when a link is in the field -->
|
||
<div id="importer-area" style="display:none">
|
||
<div id="importer-hint" style="background:var(--surface);border:1px solid var(--border);border-radius:12px;padding:14px;font-size:12px;color:var(--text2);line-height:1.8;margin-bottom:8px">
|
||
<div style="display:flex;gap:16px;flex-wrap:wrap">
|
||
<div style="display:flex;align-items:center;gap:6px"><span style="color:#1DB954;font-weight:700">Spotify</span> — Track, Album oder Playlist</div>
|
||
<div style="display:flex;align-items:center;gap:6px"><span style="color:#ff5500;font-weight:700">SoundCloud</span> — Track oder Playlist</div>
|
||
<div style="display:flex;align-items:center;gap:6px"><span style="color:#FF0000;font-weight:700">YouTube</span> — Video oder Playlist</div>
|
||
</div>
|
||
<div style="margin-top:6px;color:var(--text3);font-size:11px">Link einfügen → Metadaten werden geladen → via yt-dlp in die Navidrome Library herunterladen.</div>
|
||
</div>
|
||
|
||
<!-- hidden inputs kept for compat with spotify.js / soundcloud.js resolvers -->
|
||
<input id="importer-link-input" type="hidden">
|
||
<input id="spotify-link-input" type="hidden">
|
||
<input id="sc-link-input" type="hidden">
|
||
|
||
<!-- SPOTIFY RESULTS -->
|
||
<div id="spotify-preview" style="display:none;margin-bottom:8px">
|
||
<div id="spotify-preview-inner"></div>
|
||
<div id="spotify-enrich-status" style="display:none"></div>
|
||
<div id="spotify-tracks-section" style="display:none">
|
||
<div class="section-header" style="margin-top:8px">
|
||
<span class="section-title" id="spotify-tracks-title">Tracks</span>
|
||
</div>
|
||
<div id="spotify-tracks-list" class="song-list"></div>
|
||
</div>
|
||
</div>
|
||
|
||
<!-- SOUNDCLOUD RESULTS -->
|
||
<div id="sc-preview" style="display:none;margin-bottom:8px">
|
||
<div id="sc-preview-inner"></div>
|
||
<div id="sc-tracks-section" style="display:none">
|
||
<div class="section-header" style="margin-top:8px">
|
||
<span class="section-title" id="sc-tracks-title">Tracks</span>
|
||
</div>
|
||
<div id="sc-tracks-list" class="song-list"></div>
|
||
</div>
|
||
</div>
|
||
|
||
<!-- YOUTUBE RESULTS -->
|
||
<div id="yt-preview" style="display:none;margin-bottom:8px">
|
||
<div id="yt-preview-inner"></div>
|
||
<div id="yt-tracks-section" style="display:none">
|
||
<div class="section-header" style="margin-top:8px">
|
||
<span class="section-title" id="yt-tracks-title">Tracks</span>
|
||
</div>
|
||
<div id="yt-tracks-list" class="song-list"></div>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
|
||
<div id="global-search-results">
|
||
<div class="empty-state"><div class="icon">🔍</div><p>Gib einen Suchbegriff ein — oder füge einen Spotify-/SoundCloud-/YouTube-Link ein zum Importieren</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-deezer" style="font-size:11px;padding:4px 10px">Deezer 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>
|
||
|
||
<!-- AUDIOBOOKS -->
|
||
<div class="page" id="page-audiobooks">
|
||
<div class="page-header">
|
||
<div class="page-title">Hörbücher <em>📚</em></div>
|
||
</div>
|
||
<div class="search-wrap" style="padding:0 0 16px">
|
||
<div class="search-box" style="background:var(--surface);border-color:var(--border2)">
|
||
<svg width="14" height="14" 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="abs-search-input" type="text" placeholder="Titel oder Autor suchen…" oninput="absOnSearch(this.value)">
|
||
</div>
|
||
</div>
|
||
<div id="abs-books-grid" class="abs-books-grid"></div>
|
||
</div>
|
||
|
||
<!-- AUDIOBOOK DETAIL -->
|
||
<div class="page" id="page-abs-book-detail">
|
||
<button class="back-btn" onclick="navigate('audiobooks')">
|
||
<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>Hörbücher
|
||
</button>
|
||
<div id="abs-book-detail-content"></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-drag-handle"></div>
|
||
<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-wrap">
|
||
<div class="player-info">
|
||
<div class="player-title" id="player-title">Kein Titel</div>
|
||
<div class="player-artist">
|
||
<span id="player-album-name" onclick="playerBarGoAlbum()" style="cursor:pointer" title="Zum Album"></span>
|
||
</div>
|
||
<div class="player-artist" id="player-artist">
|
||
<span id="player-artist-name">—</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>
|
||
<button id="btn-star-current" onclick="starCurrent()" title="Favorit" style="background:none;border:none;cursor:pointer;padding:4px;flex-shrink:0;display:flex;align-items:center;color:var(--text3)">
|
||
<svg id="star-icon" width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" 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>
|
||
<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"><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 volume-slider-sync" id="volume-slider" min="0" max="100" value="50" oninput="setVolume(this.value)">
|
||
</div>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
|
||
<!-- ABS MINI BAR (overlays the music player bar when an audiobook is active) -->
|
||
<div id="abs-mini-bar" class="abs-mini-bar">
|
||
<div class="abs-mini-prog-strip"><div id="abs-mini-prog-fill" class="abs-mini-prog-fill"></div></div>
|
||
<div id="abs-mini-cover" class="abs-mini-cover" onclick="absOpenPlayer()">📚</div>
|
||
<div class="abs-mini-info" onclick="absOpenPlayer()">
|
||
<div id="abs-mini-title" class="abs-mini-title">—</div>
|
||
<div id="abs-mini-chapter" class="abs-mini-chapter"></div>
|
||
</div>
|
||
<button class="abs-mini-play" id="abs-mini-play" onclick="absTogglePlay()">
|
||
<svg width="22" height="22" viewBox="0 0 24 24" fill="currentColor"><polygon points="5 3 19 12 5 21 5 3"/></svg>
|
||
</button>
|
||
<button class="abs-mini-open" onclick="absOpenPlayer()" title="Öffnen">
|
||
<svg width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.5"><polyline points="18 15 12 9 6 15"/></svg>
|
||
</button>
|
||
</div>
|
||
|
||
<!-- PREVIEW BAR (Deezer 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">Deezer</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),var(--accent-dark));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(--surface2);display:inline-flex;align-items:center;justify-content:center;font-size:12px">🎨</span>
|
||
Theme
|
||
</h3>
|
||
<div id="theme-picker" class="theme-picker"></div>
|
||
<div class="edit-pl-toggle-row" style="margin-top:14px">
|
||
<span>Einfarbiger Player-Hintergrund</span>
|
||
<div id="s-cover-color-bg" class="toggle" onclick="this.classList.toggle('on')"></div>
|
||
</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(--surface2);display:inline-flex;align-items:center;justify-content:center;font-size:12px">📊</span>
|
||
Bibliothek
|
||
</h3>
|
||
<div class="edit-pl-toggle-row">
|
||
<span>Abspielhäufigkeit anzeigen</span>
|
||
<div id="s-show-playcount" class="toggle" onclick="this.classList.toggle('on')"></div>
|
||
</div>
|
||
<div class="edit-pl-toggle-row" style="margin-top:12px">
|
||
<span>Romaji anzeigen</span>
|
||
<div id="s-show-romaji" class="toggle" onclick="this.classList.toggle('on')"></div>
|
||
</div>
|
||
<div class="edit-pl-toggle-row" style="margin-top:12px">
|
||
<span>Scrobble senden nach</span>
|
||
<select id="s-scrobble-mode" style="background:var(--surface2);color:var(--text);border:1px solid var(--border);border-radius:8px;padding:6px 10px;font-family:var(--font);font-size:13px;cursor:pointer">
|
||
<option value="15s">15 Sekunden</option>
|
||
<option value="30s">30 Sekunden</option>
|
||
<option value="1/3">1/3 der Song-Dauer</option>
|
||
<option value="2/3">2/3 der Song-Dauer</option>
|
||
<option value="end">Song-Ende</option>
|
||
</select>
|
||
</div>
|
||
<div style="display:flex;align-items:center;justify-content:space-between;margin-top:14px">
|
||
<div style="font-size:13px;font-weight:600">Romaji Korrekturen</div>
|
||
<button onclick="openRomajiOverridesModal()" class="btn-outline" style="padding:7px 14px;font-size:13px;font-weight:600;flex-shrink:0">Liste öffnen</button>
|
||
</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(--green-dim);display:inline-flex;align-items:center;justify-content:center;font-size:12px">📚</span>
|
||
Audiobookshelf
|
||
</h3>
|
||
<div class="field">
|
||
<label>Server URL</label>
|
||
<input id="abs-s-url" type="url" placeholder="https://books.example.com">
|
||
</div>
|
||
<div class="field">
|
||
<label>API Key</label>
|
||
<input id="abs-s-apikey" type="password" placeholder="Aus ABS: Profil → API Token">
|
||
<div class="hint">Einstellungen → Profil → API Token in Audiobookshelf</div>
|
||
</div>
|
||
<div id="abs-lib-picker" style="display:none" class="field">
|
||
<label>Bibliothek</label>
|
||
<select id="abs-lib-select" onchange="absSetLibrary(this.value)" style="width:100%;padding:10px 12px;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"></select>
|
||
</div>
|
||
<div class="settings-row" style="margin-top:8px;border-radius:9px">
|
||
<div>
|
||
<div style="font-size:13px;font-weight:500">Audiobookshelf Status</div>
|
||
<div class="hint" id="abs-s-status">Nicht verbunden</div>
|
||
</div>
|
||
<div style="display:flex;align-items:center;gap:8px">
|
||
<div class="status-dot" id="abs-s-dot" style="display:none"></div>
|
||
<button class="btn-outline" id="abs-s-connect-btn" style="padding:6px 12px;font-size:12px;font-weight:600" onclick="absToggleConnection()">Verbinden</button>
|
||
</div>
|
||
</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 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" id="s-apk-section" style="display:none">
|
||
<h3 style="display:flex;align-items:center;gap:8px;margin-bottom:12px">
|
||
<span style="width:24px;height:24px;border-radius:6px;background:var(--surface2);display:inline-flex;align-items:center;justify-content:center;font-size:12px">🤖</span>
|
||
Android App
|
||
</h3>
|
||
<div style="font-size:12px;color:var(--text2);margin-bottom:10px;line-height:1.6">Installiere die NovaSonic Android-App direkt aufs Handy. <span id="s-apk-meta" style="color:var(--text3)"></span></div>
|
||
<a id="s-apk-btn" href="#" download style="display:inline-flex;align-items:center;gap:8px;padding:10px 16px;border-radius:10px;background:linear-gradient(135deg,var(--accent),var(--accent-dark));color:#fff;font-weight:700;font-size:13px;text-decoration:none;box-shadow:0 4px 12px var(--accent-glow)">
|
||
<svg width="16" height="16" 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>
|
||
APK herunterladen
|
||
</a>
|
||
<div style="font-size:11px;color:var(--text3);margin-top:8px">Danach im Handy „Installation aus unbekannten Quellen" für den Browser erlauben.</div>
|
||
</div>
|
||
</div>
|
||
<!-- Sticky footer -->
|
||
<div style="display:flex;flex-direction:column;gap:8px;padding:12px 24px 16px;border-top:1px solid var(--border);flex-shrink:0;background:var(--surface);border-radius:0 0 18px 18px">
|
||
<div style="display:flex;gap:8px">
|
||
<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),var(--accent-dark));color:#000" onclick="saveSettings()">Speichern</button>
|
||
</div>
|
||
<button style="width:100%;padding:10px;border-radius:9px;font-family:var(--font);font-size:13px;font-weight:600;cursor:pointer;background:none;border:1px solid var(--border);color:var(--text2);transition:background .15s" onmouseover="this.style.background='var(--surface2)'" onmouseout="this.style.background='none'" onclick="closeSettings(); openStats()">📊 Meine Statistiken</button>
|
||
<button style="width:100%;padding:10px;border-radius:9px;font-family:var(--font);font-size:13px;font-weight:600;cursor:pointer;background:none;border:1px solid var(--border);color:var(--text2);transition:background .15s" onmouseover="this.style.background='var(--surface2)'" onmouseout="this.style.background='none'" onclick="closeSettings(); openCompare()">🎧 Musik-Match</button>
|
||
<button style="width:100%;padding:10px;border-radius:9px;font-family:var(--font);font-size:13px;font-weight:600;cursor:pointer;background:none;border:1px solid rgba(224,85,85,0.3);color:var(--danger);transition:background .15s" onmouseover="this.style.background='rgba(224,85,85,0.1)'" onmouseout="this.style.background='none'" onclick="confirmDisconnect()">Abmelden</button>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
|
||
<!-- ROMAJI OVERRIDES MODAL -->
|
||
<div class="modal-overlay" id="romaji-overrides-modal">
|
||
<div class="modal" style="padding:0;display:flex;flex-direction:column;max-height:80vh;width:min(460px,calc(100vw - 32px))">
|
||
<div style="display:flex;align-items:center;justify-content:space-between;padding:18px 20px 14px;border-bottom:1px solid var(--border);flex-shrink:0">
|
||
<div style="font-size:16px;font-weight:700">Romaji Korrekturen</div>
|
||
<button onclick="closeRomajiOverridesModal()" style="background:var(--surface2);border:none;color:var(--text2);width:30px;height:30px;border-radius:50%;cursor:pointer;font-size:15px;display:flex;align-items:center;justify-content:center">✕</button>
|
||
</div>
|
||
<div style="flex:1;overflow-y:auto;overflow-x:hidden;padding:14px 20px">
|
||
<div class="hint" style="margin-bottom:12px">Kanji → korrektes Romaji<br>Wird sofort in der Anzeige, bei Downloads und direkt auf bestehende Songs in der Bibliothek (Sort-Tags + Dateiname) angewendet.</div>
|
||
<div id="romaji-overrides-list" style="display:flex;flex-direction:column;gap:5px;margin-bottom:14px"></div>
|
||
<div style="display:flex;gap:6px;align-items:center;min-width:0">
|
||
<input id="ro-kanji" placeholder="米津玄師" style="flex:1;min-width:0;padding:8px 10px;background:var(--bg2);border:1px solid var(--border2);border-radius:8px;color:var(--text);font-family:var(--font);font-size:13px;outline:none" onkeydown="if(event.key==='Enter')addRomajiOverrideUI()">
|
||
<span style="color:var(--text2);flex-shrink:0">→</span>
|
||
<input id="ro-romaji" placeholder="Yonezu Kenshi" style="flex:1.2;min-width:0;padding:8px 10px;background:var(--bg2);border:1px solid var(--border2);border-radius:8px;color:var(--text);font-family:var(--font);font-size:13px;outline:none" onkeydown="if(event.key==='Enter')addRomajiOverrideUI()">
|
||
<button onclick="addRomajiOverrideUI()" style="flex-shrink:0;padding:8px 12px;border-radius:8px;border:none;background:linear-gradient(135deg,var(--accent),var(--accent-dark));color:#000;font-family:var(--font);font-size:14px;font-weight:700;cursor:pointer">+</button>
|
||
</div>
|
||
</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" preload="auto" playsinline crossorigin="anonymous"></audio>
|
||
<audio id="audio-element-b" preload="auto" playsinline crossorigin="anonymous"></audio>
|
||
<audio id="preview-audio"></audio>
|
||
<audio id="abs-audio"></audio>
|
||
|
||
<!-- AUDIOBOOKSHELF PLAYER OVERLAY -->
|
||
<div id="abs-player" class="abs-player-overlay">
|
||
<!-- Header -->
|
||
<div class="abs-player-header">
|
||
<button class="abs-hbtn" onclick="absClosePlayer()">
|
||
<svg width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.5"><polyline points="6 9 12 15 18 9"/></svg>
|
||
</button>
|
||
<span class="abs-player-label">Hörbuch</span>
|
||
<div style="width:36px"></div>
|
||
</div>
|
||
|
||
<!-- Cover -->
|
||
<div class="abs-cover-area">
|
||
<div class="abs-cover-wrap">
|
||
<img id="abs-cover" src="" alt="" class="abs-cover-img" style="display:none">
|
||
<div id="abs-cover-placeholder" class="abs-cover-ph">📚</div>
|
||
</div>
|
||
</div>
|
||
|
||
<!-- Info -->
|
||
<div class="abs-player-info">
|
||
<div id="abs-title" class="abs-player-title">—</div>
|
||
<div id="abs-author" class="abs-player-author"></div>
|
||
<div id="abs-chapter-name" class="abs-player-chapter"></div>
|
||
</div>
|
||
|
||
<!-- Progress bar -->
|
||
<div class="abs-progress-area">
|
||
<div class="abs-prog-bar" id="abs-prog-bar"
|
||
onmousedown="absProgDown(event)" onmousemove="absProgMove(event)" onmouseup="absProgUp(event)"
|
||
ontouchstart="absProgDown(event)" ontouchmove="absProgMove(event)" ontouchend="absProgUp(event)">
|
||
<div class="abs-prog-track">
|
||
<div class="abs-prog-fill" id="abs-prog-fill"></div>
|
||
<div class="abs-prog-thumb" id="abs-prog-thumb"></div>
|
||
</div>
|
||
</div>
|
||
<div class="abs-prog-times">
|
||
<span id="abs-time-cur">0:00</span>
|
||
<span id="abs-time-rem">-0:00</span>
|
||
</div>
|
||
</div>
|
||
|
||
<!-- Main controls -->
|
||
<div class="abs-controls">
|
||
<button class="abs-ctrl-btn" onclick="absPrevChapter()" title="Voriges Kapitel">
|
||
<svg width="22" height="22" 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.5" stroke-linecap="round"/></svg>
|
||
</button>
|
||
<button class="abs-ctrl-btn abs-skip-btn" onclick="absSkipSecs(-10)" title="-10s">
|
||
<svg width="26" height="26" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.8">
|
||
<path d="M12 22C6.477 22 2 17.523 2 12S6.477 2 12 2s10 4.477 10 10"/>
|
||
<polyline points="15 9 12 6 9 9"/>
|
||
<path d="M12 6v4"/>
|
||
</svg>
|
||
<span class="abs-skip-label">10</span>
|
||
</button>
|
||
<button class="abs-play-pause" id="abs-playpause-btn" onclick="absTogglePlay()">
|
||
<svg width="28" height="28" viewBox="0 0 24 24" fill="currentColor"><polygon points="5 3 19 12 5 21 5 3"/></svg>
|
||
</button>
|
||
<button class="abs-ctrl-btn abs-skip-btn" onclick="absSkipSecs(30)" title="+30s">
|
||
<svg width="26" height="26" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.8">
|
||
<path d="M12 22C17.523 22 22 17.523 22 12S17.523 2 12 2 2 6.477 2 12"/>
|
||
<polyline points="9 9 12 6 15 9"/>
|
||
<path d="M12 6v4"/>
|
||
</svg>
|
||
<span class="abs-skip-label">30</span>
|
||
</button>
|
||
<button class="abs-ctrl-btn" onclick="absNextChapter()" title="Nächstes Kapitel">
|
||
<svg width="22" height="22" 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.5" stroke-linecap="round"/></svg>
|
||
</button>
|
||
</div>
|
||
|
||
<!-- Extra controls -->
|
||
<div class="abs-extra-controls">
|
||
<!-- Speed -->
|
||
<div class="abs-picker-wrap">
|
||
<button class="abs-extra-btn" id="abs-speed-btn" onclick="absToggleSpeedPicker()">
|
||
<svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><circle cx="12" cy="12" r="10"/><polyline points="12 6 12 12 16 14"/></svg>
|
||
<span id="abs-speed-val">1×</span>
|
||
</button>
|
||
<div id="abs-speed-picker" class="abs-picker">
|
||
<button data-speed="0.75" onclick="absSetSpeed(0.75)">0.75×</button>
|
||
<button data-speed="1" onclick="absSetSpeed(1)">1×</button>
|
||
<button data-speed="1.25" onclick="absSetSpeed(1.25)">1.25×</button>
|
||
<button data-speed="1.5" onclick="absSetSpeed(1.5)">1.5×</button>
|
||
<button data-speed="1.75" onclick="absSetSpeed(1.75)">1.75×</button>
|
||
<button data-speed="2" onclick="absSetSpeed(2)">2×</button>
|
||
</div>
|
||
</div>
|
||
|
||
<!-- Sleep timer -->
|
||
<div class="abs-picker-wrap">
|
||
<button class="abs-extra-btn" id="abs-sleep-btn" onclick="absToggleSleepPicker()">
|
||
<svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><path d="M21 12.79A9 9 0 1 1 11.21 3 7 7 0 0 0 21 12.79z"/></svg>
|
||
</button>
|
||
<div id="abs-sleep-picker" class="abs-picker abs-picker-right">
|
||
<button onclick="absSleepIn(0)">Aus</button>
|
||
<button onclick="absSleepIn(5)">5 Min</button>
|
||
<button onclick="absSleepIn(10)">10 Min</button>
|
||
<button onclick="absSleepIn(15)">15 Min</button>
|
||
<button onclick="absSleepIn(30)">30 Min</button>
|
||
<button onclick="absSleepIn(45)">45 Min</button>
|
||
<button onclick="absSleepIn(60)">60 Min</button>
|
||
<div class="abs-sleep-custom-row">
|
||
<span class="abs-sleep-custom-label">Eigene Zeit</span>
|
||
<div class="abs-sleep-custom-controls">
|
||
<input id="abs-sleep-custom-input" type="number" min="1" max="999" placeholder="Min" onkeydown="if(event.key==='Enter'){var v=parseInt(this.value);if(v>0)absSleepIn(v);}">
|
||
<button class="abs-sleep-custom-ok" onclick="var v=parseInt(document.getElementById('abs-sleep-custom-input').value);if(v>0)absSleepIn(v);">OK</button>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
|
||
<!-- Chapter list toggle -->
|
||
<button class="abs-extra-btn" onclick="absToggleChapters()">
|
||
<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>
|
||
Kapitel
|
||
</button>
|
||
|
||
<!-- Chapter track mode -->
|
||
<button class="abs-extra-btn" id="abs-chtrack-btn" onclick="absToggleChapterTrack()" title="Kapitel-Track: Fortschritt nur im aktuellen Kapitel anzeigen">
|
||
<svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><circle cx="12" cy="12" r="9"/><polyline points="12 7 12 12"/><line x1="12" y1="12" x2="15" y2="15"/><line x1="3" y1="12" x2="6" y2="12" stroke-width="2.5"/></svg>
|
||
Kap.Track
|
||
</button>
|
||
</div>
|
||
|
||
<!-- Volume -->
|
||
<div class="abs-volume-row">
|
||
<svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" style="color:var(--text3);flex-shrink:0"><polygon points="11 5 6 9 2 9 2 15 6 15 11 19 11 5"/></svg>
|
||
<input type="range" class="abs-vol-slider volume-slider-sync" min="0" max="100" value="50" oninput="setVolume(this.value)">
|
||
<svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" style="color:var(--text3);flex-shrink:0"><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>
|
||
|
||
<!-- Chapter list -->
|
||
<div id="abs-chapter-list" class="abs-chapter-list"></div>
|
||
</div>
|
||
|
||
<!-- 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),var(--accent-dark));color:#000" onclick="createAndAddPlaylist()">Erstellen & Hinzufügen</button>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
|
||
<!-- EDIT PLAYLIST MODAL -->
|
||
<div class="edit-pl-overlay" id="edit-pl-overlay" onclick="if(event.target===this)closeEditPlModal()">
|
||
<div class="edit-pl-box">
|
||
<div class="edit-pl-header">
|
||
<span>Playlist bearbeiten</span>
|
||
<button class="edit-pl-close" onclick="closeEditPlModal()">
|
||
<svg width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><line x1="18" y1="6" x2="6" y2="18"/><line x1="6" y1="6" x2="18" y2="18"/></svg>
|
||
</button>
|
||
</div>
|
||
<div class="edit-pl-body">
|
||
<div class="edit-pl-cover-wrap">
|
||
<div class="edit-pl-cover" id="edit-pl-cover"><div class="no-art">🎶</div></div>
|
||
<div class="edit-pl-cover-actions">
|
||
<button class="edit-pl-cover-btn" onclick="document.getElementById('edit-pl-file').click()" title="Bild hochladen">
|
||
<svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><path d="M23 19a2 2 0 0 1-2 2H3a2 2 0 0 1-2-2V8a2 2 0 0 1 2-2h4l2-3h6l2 3h4a2 2 0 0 1 2 2z"/><circle cx="12" cy="13" r="4"/></svg>
|
||
</button>
|
||
|
||
</div>
|
||
<input type="file" id="edit-pl-file" accept="image/*" style="display:none" onchange="editPlCoverSelected(this)">
|
||
</div>
|
||
<div class="edit-pl-fields">
|
||
<label class="edit-pl-label">Name <span style="color:var(--accent)">*</span></label>
|
||
<input class="edit-pl-input" id="edit-pl-name" type="text" placeholder="Playlist-Name">
|
||
<label class="edit-pl-label">Beschreibung</label>
|
||
<textarea class="edit-pl-input edit-pl-textarea" id="edit-pl-desc" placeholder="Optionale Beschreibung…" rows="3"></textarea>
|
||
<div class="edit-pl-toggle-row">
|
||
<span>Öffentlich</span>
|
||
<div class="toggle" id="edit-pl-public-toggle" onclick="this.classList.toggle('on')"></div>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
<div class="edit-pl-footer">
|
||
<button class="edit-pl-btn-delete" onclick="deletePlaylist()">Löschen</button>
|
||
<div style="display:flex;gap:10px">
|
||
<button class="edit-pl-btn-cancel" onclick="closeEditPlModal()">Abbrechen</button>
|
||
<button class="edit-pl-btn-save" onclick="saveEditPlaylist()">Speichern</button>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
|
||
<!-- CONFIRM DIALOG -->
|
||
<div class="edit-pl-overlay" id="confirm-overlay" onclick="if(event.target===this)_confirmCancel()">
|
||
<div class="confirm-box">
|
||
<div class="confirm-title" id="confirm-title"></div>
|
||
<div class="confirm-msg" id="confirm-msg"></div>
|
||
<div class="confirm-footer">
|
||
<button class="edit-pl-btn-cancel" onclick="_confirmCancel()">Abbrechen</button>
|
||
<button class="edit-pl-btn-delete" id="confirm-ok-btn" onclick="_confirmOk()">Bestätigen</button>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
|
||
<!-- EVENTS MODAL -->
|
||
<div class="modal-overlay" id="events-modal" onclick="if(event.target===this)closeEventsModal()">
|
||
<div class="modal events-modal">
|
||
<div class="events-modal-header">
|
||
<div id="events-modal-title" class="events-modal-title"></div>
|
||
<button class="events-modal-close" onclick="closeEventsModal()">✕</button>
|
||
</div>
|
||
<div id="events-modal-list" class="events-modal-list"></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 style="flex:1;min-width:0"><div class="info-top-title" id="info-top-title">—</div><div class="info-top-sub" id="info-top-sub">—</div></div>
|
||
<button class="info-edit-toggle" id="info-edit-toggle" onclick="toggleInfoEdit()" title="Bearbeiten">
|
||
<svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><path d="M11 4H4a2 2 0 0 0-2 2v14a2 2 0 0 0 2 2h14a2 2 0 0 0 2-2v-7"/><path d="M18.5 2.5a2.121 2.121 0 0 1 3 3L12 15l-4 1 1-4 9.5-9.5z"/></svg>
|
||
</button>
|
||
</div>
|
||
|
||
<!-- VIEW MODE -->
|
||
<div id="info-view-mode">
|
||
<div class="info-rows" id="info-rows"></div>
|
||
<button class="info-close" onclick="closeInfoModal()">Schließen</button>
|
||
</div>
|
||
|
||
<!-- EDIT MODE -->
|
||
<div id="info-edit-mode" style="display:none">
|
||
<div class="info-edit-art-row">
|
||
<div class="info-edit-art-wrap">
|
||
<div class="info-cover info-edit-art-preview" id="info-edit-art-preview"></div>
|
||
<button class="info-edit-art-btn" onclick="document.getElementById('info-art-file').click()" title="Bild hochladen">
|
||
<svg width="12" height="12" 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="17 8 12 3 7 8"/><line x1="12" y1="3" x2="12" y2="15"/></svg>
|
||
Upload
|
||
</button>
|
||
<input type="file" id="info-art-file" accept="image/*" style="display:none" onchange="onInfoArtFile(this)">
|
||
</div>
|
||
<button class="btn-outline blue" style="font-size:11px;padding:6px 10px;height:fit-content" onclick="autofillMusicBrainz()">
|
||
<svg width="11" height="11" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" style="margin-right:4px"><circle cx="11" cy="11" r="8"/><line x1="21" y1="21" x2="16.65" y2="16.65"/></svg>
|
||
MusicBrainz
|
||
</button>
|
||
<button class="btn-outline blue" style="font-size:11px;padding:6px 10px;height:fit-content" onclick="autofillDeezer()">
|
||
<svg width="11" height="11" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" style="margin-right:4px"><circle cx="11" cy="11" r="8"/><line x1="21" y1="21" x2="16.65" y2="16.65"/></svg>
|
||
Deezer
|
||
</button>
|
||
</div>
|
||
<div class="info-edit-fields">
|
||
<label class="info-edit-label">Titel<input class="info-edit-input" id="ie-title" type="text" placeholder="Titel"></label>
|
||
<label class="info-edit-label">Künstler<span class="info-edit-hint">Mehrere mit <code>;</code> oder <code>,</code> trennen — wird in einzelne Künstler aufgeteilt</span><input class="info-edit-input" id="ie-artist" type="text" placeholder="Künstler"></label>
|
||
<label class="info-edit-label">Album-Künstler<span class="info-edit-hint">Mehrere mit <code>;</code> oder <code>,</code> trennen — bleibt als ein Album-Credit zusammen</span><input class="info-edit-input" id="ie-albumartist" type="text" placeholder="Album-Künstler"></label>
|
||
<label class="info-edit-label">Album<input class="info-edit-input" id="ie-album" type="text" placeholder="Album"></label>
|
||
<label class="info-edit-label">Genre<input class="info-edit-input" id="ie-genre" type="text" placeholder="Genre"></label>
|
||
<div style="display:grid;grid-template-columns:1fr 1fr;gap:8px">
|
||
<label class="info-edit-label">Jahr<input class="info-edit-input" id="ie-year" type="number" placeholder="2024" min="1900" max="2099"></label>
|
||
<label class="info-edit-label">Track #<input class="info-edit-input" id="ie-track" type="number" placeholder="1" min="1"></label>
|
||
<label class="info-edit-label">Disc #<input class="info-edit-input" id="ie-disc" type="number" placeholder="1" min="1"></label>
|
||
<label class="info-edit-label">Tracks ges.<input class="info-edit-input" id="ie-totaltracks" type="number" placeholder="12" min="1"></label>
|
||
</div>
|
||
</div>
|
||
<details class="ie-sort-details">
|
||
<summary>Sort-Tags (Japanisch / Englisch)</summary>
|
||
<div class="info-edit-fields" style="margin-top:8px">
|
||
<label class="info-edit-label">Titel-Sort<input class="info-edit-input" id="ie-titlesort" type="text" placeholder="Romaji / Englisch"></label>
|
||
<label class="info-edit-label">Künstler-Sort<input class="info-edit-input" id="ie-artistsort" type="text" placeholder="Romaji / Englisch"></label>
|
||
<label class="info-edit-label">Album-Künstler-Sort<input class="info-edit-input" id="ie-albumartistsort" type="text" placeholder="Romaji / Englisch"></label>
|
||
<label class="info-edit-label">Album-Sort<input class="info-edit-input" id="ie-albumsort" type="text" placeholder="Romaji / Englisch"></label>
|
||
</div>
|
||
</details>
|
||
<div id="info-edit-status" style="font-size:12px;color:var(--text3);padding:0 20px 8px;min-height:18px"></div>
|
||
<div style="display:flex;gap:8px;padding:0 20px 20px">
|
||
<button class="info-close" style="flex:1;margin:0" onclick="toggleInfoEdit()">Abbrechen</button>
|
||
<button class="info-close" style="flex:1;margin:0;background:var(--accent);color:#fff" onclick="saveMetadata()">Speichern</button>
|
||
</div>
|
||
<div style="padding:0 20px 20px">
|
||
<button class="info-close" style="width:100%;margin:0;background:none;border:1px solid #c0392b;color:#c0392b;display:flex;align-items:center;justify-content:center;gap:6px" onclick="deleteSong()">
|
||
<svg width="13" height="13" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><polyline points="3 6 5 6 21 6"/><path d="M19 6l-1 14a2 2 0 0 1-2 2H8a2 2 0 0 1-2-2L5 6"/><path d="M10 11v6"/><path d="M14 11v6"/><path d="M9 6V4h6v2"/></svg>
|
||
Song löschen
|
||
</button>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
|
||
|
||
<!-- NOW PLAYING OVERLAY -->
|
||
<div class="np-overlay" id="np-overlay">
|
||
<div class="np-bg" id="np-bg-a"></div>
|
||
<div class="np-bg" id="np-bg-b" style="opacity:0"></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')">Player</button>
|
||
<button class="np-tab" id="np-tab-lyrics" onclick="switchNpTab('lyrics')">Lyrics</button>
|
||
<button class="np-tab" id="np-tab-queue" onclick="switchNpTab('queue')">Queue</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-row">
|
||
<div class="np-title" id="np-title">Kein Titel</div>
|
||
</div>
|
||
<div class="np-artist">
|
||
<span id="np-album-name" onclick="npGoAlbum()" style="cursor:pointer;opacity:.8">—</span>
|
||
</div>
|
||
<div class="np-artist" id="np-artist">
|
||
<span id="np-artist-name">—</span>
|
||
</div>
|
||
</div>
|
||
<button class="np-star" id="np-star-btn" onclick="npToggleStar()">
|
||
<svg id="np-star-icon" width="22" height="22" 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 volume-slider-sync" id="np-vol-slider" min="0" max="100" value="50" oninput="setVolume(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-lyrics-view">
|
||
<div id="np-lyrics-content"></div>
|
||
</div>
|
||
|
||
<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('albums')" data-page="albums">
|
||
<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>
|
||
Alben
|
||
</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>
|
||
<button class="bnav-item" id="bnav-audiobooks" onclick="navigate('audiobooks')" data-page="audiobooks" style="display:none">
|
||
<svg width="22" height="22" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><path d="M2 3h6a4 4 0 0 1 4 4v14a3 3 0 0 0-3-3H2z"/><path d="M22 3h-6a4 4 0 0 0-4 4v14a3 3 0 0 1 3-3h7z"/></svg>
|
||
Bücher
|
||
</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>
|
||
|
||
<!-- NOTIFICATION PANEL -->
|
||
<div id="notif-overlay" class="notif-overlay" onclick="toggleNotifPanel()"></div>
|
||
<div id="notif-panel" class="notif-panel">
|
||
<div class="notif-panel-hdr">
|
||
<span class="notif-panel-title">Benachrichtigungen</span>
|
||
<button class="notif-clear-btn" onclick="clearNotifs()">Alle löschen</button>
|
||
</div>
|
||
<div id="notif-list" class="notif-list"></div>
|
||
</div>
|
||
|
||
</body>
|
||
</html> |