778 lines
41 KiB
HTML
778 lines
41 KiB
HTML
<!DOCTYPE html>
|
||
<html lang="en">
|
||
<head>
|
||
<meta charset="UTF-8">
|
||
<meta name="viewport" content="width=device-width, initial-scale=1.0, viewport-fit=cover, user-scalable=no">
|
||
<meta name="theme-color" content="#1a1a2e">
|
||
<meta name="apple-mobile-web-app-capable" content="yes">
|
||
<meta name="apple-mobile-web-app-status-bar-style" content="black-translucent">
|
||
<title>Freedify</title>
|
||
<link rel="manifest" href="/manifest.json">
|
||
<link rel="icon" href="/static/icon.png" type="image/png">
|
||
<link rel="apple-touch-icon" href="/static/icon.png">
|
||
<link rel="preconnect" href="https://fonts.googleapis.com">
|
||
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
|
||
<link href="https://fonts.googleapis.com/css2?family=Inter:wght@400;500;600;700&family=Orbitron:wght@400;700;900&family=Permanent+Marker&display=swap" rel="stylesheet">
|
||
<link rel="stylesheet" href="/static/styles.css?t=1736053100">
|
||
<!-- Google API for Drive sync -->
|
||
<script src="https://accounts.google.com/gsi/client" async defer></script>
|
||
<script src="https://apis.google.com/js/api.js" async defer></script>
|
||
<!-- Butterchurn (MilkDrop WebGL visualizer) -->
|
||
<script src="https://cdn.jsdelivr.net/npm/butterchurn@2.6.7/lib/butterchurn.min.js"></script>
|
||
<script src="https://cdn.jsdelivr.net/npm/butterchurn-presets@2.4.7/lib/butterchurnPresets.min.js"></script>
|
||
</head>
|
||
<body>
|
||
<!-- Toast Container -->
|
||
<div id="toast-container" class="toast-container"></div>
|
||
|
||
<!-- Keyboard Shortcuts Help -->
|
||
<div id="shortcuts-help" class="shortcuts-help hidden">
|
||
<div class="shortcuts-content">
|
||
<div class="shortcuts-header">
|
||
<h3>⌨️ Keyboard Shortcuts</h3>
|
||
<button id="shortcuts-close" class="shortcuts-close">×</button>
|
||
</div>
|
||
<div class="shortcuts-grid">
|
||
<div class="shortcut"><kbd>Space</kbd><span>Play/Pause</span></div>
|
||
<div class="shortcut"><kbd>→</kbd><span>Next Track</span></div>
|
||
<div class="shortcut"><kbd>←</kbd><span>Previous Track</span></div>
|
||
<div class="shortcut"><kbd>Shift + →</kbd><span>Seek +10s</span></div>
|
||
<div class="shortcut"><kbd>Shift + ←</kbd><span>Seek -10s</span></div>
|
||
<div class="shortcut"><kbd>↑</kbd><span>Volume Up</span></div>
|
||
<div class="shortcut"><kbd>↓</kbd><span>Volume Down</span></div>
|
||
<div class="shortcut"><kbd>M</kbd><span>Mute/Unmute</span></div>
|
||
<div class="shortcut"><kbd>S</kbd><span>Shuffle Queue</span></div>
|
||
<div class="shortcut"><kbd>R</kbd><span>Toggle Repeat</span></div>
|
||
<div class="shortcut"><kbd>F</kbd><span>Fullscreen</span></div>
|
||
<div class="shortcut"><kbd>Q</kbd><span>Toggle Queue</span></div>
|
||
<div class="shortcut"><kbd>E</kbd><span>Toggle EQ</span></div>
|
||
<div class="shortcut"><kbd>P</kbd><span>Add to Playlist</span></div>
|
||
<div class="shortcut"><kbd>H</kbd><span>HiFi/Hi-Res</span></div>
|
||
<div class="shortcut"><kbd>D</kbd><span>Download Track</span></div>
|
||
<div class="shortcut"><kbd>A</kbd><span>AI Radio</span></div>
|
||
<div class="shortcut"><kbd>L</kbd><span>Lyrics</span></div>
|
||
<div class="shortcut"><kbd>V</kbd><span>Music Video</span></div>
|
||
<div class="shortcut"><kbd>Shift + S</kbd><span>Sync to Drive</span></div>
|
||
<div class="shortcut"><kbd>?</kbd><span>This Help</span></div>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
|
||
<div class="app-container">
|
||
<!-- Header -->
|
||
<header class="header">
|
||
<h1 class="logo"><span class="brand-text">Freedify</span></h1>
|
||
<div class="header-actions">
|
||
<button id="hifi-btn" class="header-btn" title="HiFi Mode - Stream lossless FLAC (faster startup, more data)">HiFi</button>
|
||
<label for="file-input" class="header-btn" title="Add Local Songs (MP3/FLAC)" style="cursor: pointer; display: inline-flex; align-items: center; justify-content: center;">📂</label>
|
||
<button id="dj-mode-btn" class="header-btn" title="DJ Mode">🎧</button>
|
||
<button id="sync-btn" class="header-btn" title="Sync with Google Drive">☁️</button>
|
||
<button id="theme-btn" class="theme-btn" title="Change Theme">🎨</button>
|
||
</div>
|
||
</header>
|
||
|
||
<!-- Theme Picker Dropdown -->
|
||
<div id="theme-picker" class="theme-picker hidden">
|
||
<div class="theme-option" data-theme="">🌙 Default</div>
|
||
<div class="theme-option" data-theme="theme-purple">💜 Purple</div>
|
||
<div class="theme-option" data-theme="theme-blue">💙 Blue</div>
|
||
<div class="theme-option" data-theme="theme-green">💚 Green</div>
|
||
<div class="theme-option" data-theme="theme-pink">💕 Pink</div>
|
||
<div class="theme-option" data-theme="theme-orange">🧡 Orange</div>
|
||
</div>
|
||
|
||
<!-- Search Section -->
|
||
<section class="search-section">
|
||
<div class="search-container">
|
||
<input
|
||
type="text"
|
||
id="search-input"
|
||
class="search-input"
|
||
placeholder="Search music or paste ANY link (Spotify, Bandcamp, SoundCloud...)"
|
||
autocomplete="off"
|
||
title="Search or Import"
|
||
>
|
||
<button id="search-clear" class="search-clear" title="Clear search">×</button>
|
||
</div>
|
||
|
||
<!-- Search Type Selector -->
|
||
<div class="search-type-selector">
|
||
<button class="type-btn active" data-type="track">Songs</button>
|
||
<button class="type-btn" data-type="album">Albums</button>
|
||
<button class="type-btn" data-type="artist">Artists</button>
|
||
<button class="type-btn" data-type="podcast">Podcasts</button>
|
||
<button id="search-more-btn" class="type-btn more-btn">⋮ More</button>
|
||
</div>
|
||
|
||
<!-- Search More Menu -->
|
||
<div id="search-more-menu" class="search-more-menu hidden">
|
||
<button class="menu-item" id="ai-menu-btn">Smart Playlist</button>
|
||
<button class="menu-item" id="concert-search-menu-btn">Concert Search</button>
|
||
<div class="menu-divider"></div>
|
||
<button class="menu-item type-btn-menu" data-type="ytmusic">YT Music</button>
|
||
<button class="menu-item type-btn-menu" data-type="setlist">Setlists</button>
|
||
<button class="menu-item type-btn-menu" data-type="rec">For You</button>
|
||
<button class="menu-item type-btn-menu" data-type="favorites">Playlists</button>
|
||
</div>
|
||
</section>
|
||
|
||
<!-- Loading Overlay -->
|
||
<div id="loading-overlay" class="loading-overlay hidden">
|
||
<div class="loading-content">
|
||
<div class="spinner"></div>
|
||
<p id="loading-text">Loading...</p>
|
||
</div>
|
||
</div>
|
||
|
||
<!-- Error Message -->
|
||
<div id="error-message" class="error-message hidden">
|
||
<span class="error-icon">😕</span>
|
||
<p id="error-text">Something went wrong</p>
|
||
<button id="error-retry" class="btn-retry">Try Again</button>
|
||
</div>
|
||
|
||
<!-- Download Modal -->
|
||
<div id="download-modal" class="modal hidden">
|
||
<div class="modal-content">
|
||
<h3>Download Track</h3>
|
||
<p id="download-track-name">Track Name</p>
|
||
<p id="download-source-hint" class="download-hint hidden"></p>
|
||
|
||
<div class="format-selector">
|
||
<label>Select Format:</label>
|
||
<select id="download-format">
|
||
<optgroup label="Lossy">
|
||
<option value="mp3" data-min-quality="lossy">MP3 (320kbps)</option>
|
||
</optgroup>
|
||
<optgroup label="Lossless (16-bit)">
|
||
<option value="flac" data-min-quality="lossless">FLAC (16-bit)</option>
|
||
<option value="aiff" data-min-quality="lossless">AIFF (16-bit)</option>
|
||
<option value="wav" data-min-quality="lossless">WAV (16-bit)</option>
|
||
<option value="alac" data-min-quality="lossless">ALAC (Apple Lossless)</option>
|
||
</optgroup>
|
||
<optgroup id="hires-formats" label="Hi-Res (24-bit)">
|
||
<option value="flac_24" data-min-quality="hires">FLAC (24-bit Hi-Res)</option>
|
||
<option value="aiff_24" data-min-quality="hires">AIFF (24-bit)</option>
|
||
<option value="wav_24" data-min-quality="hires">WAV (24-bit)</option>
|
||
</optgroup>
|
||
</select>
|
||
</div>
|
||
|
||
<div class="modal-actions">
|
||
<button id="download-cancel-btn" class="btn-secondary">Cancel</button>
|
||
<button id="download-drive-btn" class="btn-secondary" title="Save to Google Drive">☁️ Save to Drive</button>
|
||
<button id="download-confirm-btn" class="btn-primary">Download</button>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
|
||
<!-- Results Section -->
|
||
<section id="results-section" class="results-section">
|
||
<div id="results-container" class="results-container">
|
||
<!-- Search results will be inserted here -->
|
||
<div class="empty-state">
|
||
<span class="empty-icon">🔍</span>
|
||
<p>Search for your favorite music</p>
|
||
<p class="hint">Or paste a Spotify link to an album or playlist</p>
|
||
</div>
|
||
</div>
|
||
<button id="load-more-btn" class="load-more-btn hidden">Load More Results</button>
|
||
</section>
|
||
|
||
<!-- Album/Playlist Detail View -->
|
||
<section id="detail-view" class="detail-view hidden">
|
||
<div class="detail-header">
|
||
<button id="back-btn" class="back-btn">← Back</button>
|
||
<div class="detail-actions">
|
||
<button id="shuffle-btn" class="shuffle-btn" title="Shuffle & Play">Shuffle</button>
|
||
<button id="queue-all-btn" class="queue-all-btn">Add All to Queue</button>
|
||
<button id="download-all-btn" class="download-all-btn">Download ZIP</button>
|
||
</div>
|
||
</div>
|
||
<div id="detail-info" class="detail-info">
|
||
<!-- Album/playlist info will be inserted here -->
|
||
</div>
|
||
<div id="detail-tracks" class="detail-tracks">
|
||
<!-- Tracks will be inserted here -->
|
||
</div>
|
||
</section>
|
||
|
||
<!-- Queue Section -->
|
||
<section id="queue-section" class="queue-section hidden">
|
||
<div class="queue-header">
|
||
<h3>Queue <span id="queue-count">(0)</span></h3>
|
||
<div class="queue-controls">
|
||
<button id="queue-clear" class="queue-clear">Clear</button>
|
||
<button id="queue-close" class="queue-close-btn" title="Close Queue">×</button>
|
||
</div>
|
||
</div>
|
||
|
||
<!-- Crossfade Toggle -->
|
||
<div class="crossfade-toggle">
|
||
<span class="crossfade-icon">◐</span>
|
||
<div class="crossfade-info">
|
||
<span class="crossfade-title">1s Crossfade</span>
|
||
<span class="crossfade-desc">Smooth transition between tracks</span>
|
||
</div>
|
||
<label class="toggle-switch">
|
||
<input type="checkbox" id="crossfade-checkbox">
|
||
<span class="toggle-slider"></span>
|
||
</label>
|
||
</div>
|
||
|
||
<div id="queue-container" class="queue-container">
|
||
<!-- Queue items will be inserted here -->
|
||
</div>
|
||
</section>
|
||
|
||
<!-- Album Details Modal -->
|
||
<div id="album-modal" class="album-modal hidden">
|
||
<div class="album-modal-overlay"></div>
|
||
<div class="album-modal-content">
|
||
<div class="album-modal-header">
|
||
<button id="album-modal-close" class="album-modal-close" title="Close">×</button>
|
||
<h2>Album Details</h2>
|
||
</div>
|
||
|
||
<div class="album-modal-body">
|
||
<div class="album-modal-info">
|
||
<img id="album-modal-art" class="album-modal-art" src="" alt="Album Art">
|
||
<div class="album-modal-meta">
|
||
<h3 id="album-modal-title" class="album-modal-title">Album Title</h3>
|
||
<p id="album-modal-artist" class="album-modal-artist">Artist Name</p>
|
||
|
||
<!-- Metadata Pills -->
|
||
<div class="album-meta-pills">
|
||
<span id="album-modal-date" class="meta-pill">📅 2024-01-01</span>
|
||
<span id="album-modal-trackcount" class="meta-pill">🎵 12 tracks</span>
|
||
<span id="album-modal-duration" class="meta-pill">⏱️ 45 min</span>
|
||
</div>
|
||
|
||
<!-- Action Buttons -->
|
||
<div class="album-action-buttons">
|
||
<button id="album-play-btn" class="album-action-btn primary">▶ Play Album</button>
|
||
<button id="album-queue-btn" class="album-action-btn">+ Add to Queue</button>
|
||
<button id="album-download-btn" class="album-action-btn">⬇ Download Album</button>
|
||
<button id="album-playlist-btn" class="album-action-btn">♡ Add to Playlist</button>
|
||
</div>
|
||
|
||
<!-- Quality Badge -->
|
||
<div id="album-modal-quality" class="album-quality-badge">
|
||
🎵 FLAC • 16bit / 44.1kHz
|
||
</div>
|
||
</div>
|
||
</div>
|
||
|
||
|
||
|
||
<!-- Tabs -->
|
||
<div class="album-tabs">
|
||
<button class="album-tab active" data-tab="tracks">Tracks</button>
|
||
<button class="album-tab" data-tab="info">Album Info</button>
|
||
</div>
|
||
|
||
<!-- Track List -->
|
||
<div id="album-modal-tracks" class="album-modal-tracks">
|
||
<!-- Tracks will be inserted here -->
|
||
</div>
|
||
|
||
<!-- Album Info (hidden by default) -->
|
||
<div id="album-modal-info-tab" class="album-modal-info-tab hidden">
|
||
<p id="album-modal-description">Album description and additional info...</p>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
|
||
<!-- AI Assistant Modal -->
|
||
<div id="ai-modal" class="ai-modal hidden">
|
||
<div class="ai-modal-overlay"></div>
|
||
<div class="ai-modal-content">
|
||
<div class="ai-modal-header">
|
||
<h2>🧠 Smart Playlist</h2>
|
||
<button id="ai-modal-close" class="ai-modal-close" title="Close">×</button>
|
||
</div>
|
||
|
||
<!-- Playlist Generator Content -->
|
||
<div class="ai-tab-content active">
|
||
<p class="ai-tab-desc">Describe your perfect playlist and I'll create it...</p>
|
||
<textarea id="ai-playlist-input" class="ai-input" placeholder="e.g., 'A 30-minute morning coffee playlist with jazz and bossa nova'" rows="3"></textarea>
|
||
|
||
<div class="ai-duration-row">
|
||
<label>Duration:</label>
|
||
<input type="range" id="ai-duration-slider" min="15" max="120" value="60" step="15">
|
||
<span id="ai-duration-label">60 min</span>
|
||
</div>
|
||
|
||
<button id="ai-playlist-gen-btn" class="ai-action-btn">🎵 Generate Playlist</button>
|
||
<div id="ai-playlist-results" class="ai-results"></div>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
|
||
<!-- Bottom Player -->
|
||
<div id="player-bar" class="player-bar hidden">
|
||
<!-- Main visible row (Art + Info + Primary Controls) -->
|
||
<div class="player-main-row">
|
||
<div class="player-info">
|
||
<img id="player-art" class="player-art" src="" alt="Album art">
|
||
<div class="player-details">
|
||
<p id="player-title" class="player-title">No track playing</p>
|
||
<div class="player-meta-row">
|
||
<span id="player-artist" class="player-artist clickable" title="Search artist">-</span>
|
||
<span class="player-separator">•</span>
|
||
<span id="player-album" class="player-album clickable" title="View album">-</span>
|
||
<span id="player-year" class="player-year"></span>
|
||
<span id="audio-format-badge" class="audio-format-badge hidden">MP3</span>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
|
||
<div class="player-controls-primary">
|
||
<button id="prev-btn" class="control-btn" title="Previous">⏮</button>
|
||
<button id="play-btn" class="control-btn play-btn" title="Play/Pause">▶</button>
|
||
<button id="next-btn" class="control-btn" title="Next">⏭</button>
|
||
<button id="fs-toggle-btn" class="control-btn" title="Full Screen">⤢</button>
|
||
<button id="more-controls-btn" class="control-btn" title="More Options">⋮</button>
|
||
</div>
|
||
</div>
|
||
|
||
<!-- Progress Bar -->
|
||
<div class="player-progress">
|
||
<span id="current-time" class="time">0:00</span>
|
||
<input type="range" id="progress-bar" class="progress-bar" min="0" max="100" value="0" title="Seek">
|
||
<span id="duration" class="time">0:00</span>
|
||
</div>
|
||
|
||
<!-- More Menu (Popup) - 2x5 Grid -->
|
||
<div id="player-more-menu" class="player-more-menu hidden">
|
||
<div class="more-menu-grid four-col">
|
||
<!-- Row 1 -->
|
||
<button id="shuffle-queue-btn" class="control-btn" title="Shuffle Queue">⤭</button>
|
||
<button id="repeat-btn" class="control-btn" title="Repeat: Off">⟳</button>
|
||
<button id="download-current-btn" class="control-btn" title="Download">⬇</button>
|
||
<button id="queue-btn" class="control-btn queue-toggle" title="Queue">☰</button>
|
||
<!-- Row 2 -->
|
||
<button id="mute-btn" class="control-btn volume-btn" title="Mute">🔊</button>
|
||
<button id="eq-toggle-btn" class="control-btn eq-btn" title="Equalizer">🎛️</button>
|
||
<button id="ai-radio-btn" class="control-btn ai-radio-btn" title="AI Radio">📻</button>
|
||
<button id="mini-player-btn" class="control-btn" title="Popout Winamp Player">⧉</button>
|
||
<!-- Row 3 -->
|
||
<button id="add-to-playlist-btn" class="control-btn" title="Add to Playlist">🩷</button>
|
||
<button id="lyrics-btn" class="control-btn lyrics-btn" title="Lyrics">📝</button>
|
||
<button id="video-btn" class="control-btn" title="Music Video">🎬</button>
|
||
<button id="menu-visualizer-btn" class="control-btn" title="Visualizer">🌈</button>
|
||
</div>
|
||
<!-- Volume slider inside menu for mobile compactness -->
|
||
<div class="more-menu-volume">
|
||
<input type="range" id="volume-slider" class="volume-slider" min="0" max="100" value="100" title="Volume">
|
||
</div>
|
||
</div>
|
||
</div>
|
||
|
||
<!-- Equalizer Panel -->
|
||
<div id="eq-panel" class="eq-panel hidden">
|
||
<div class="eq-header">
|
||
<h3>🎛️ Equalizer</h3>
|
||
<button id="eq-close-btn" class="eq-close-btn">×</button>
|
||
</div>
|
||
|
||
<div class="eq-presets">
|
||
<button class="eq-preset active" data-preset="flat">Flat</button>
|
||
<button class="eq-preset" data-preset="bass">Bass Boost</button>
|
||
<button class="eq-preset" data-preset="treble">Treble</button>
|
||
<button class="eq-preset" data-preset="vocal">Vocal</button>
|
||
</div>
|
||
|
||
<div class="eq-sliders">
|
||
<div class="eq-band">
|
||
<input type="range" id="eq-60" class="eq-slider" min="-12" max="12" value="0" orient="vertical">
|
||
<span class="eq-label">60Hz</span>
|
||
</div>
|
||
<div class="eq-band">
|
||
<input type="range" id="eq-230" class="eq-slider" min="-12" max="12" value="0">
|
||
<span class="eq-label">230Hz</span>
|
||
</div>
|
||
<div class="eq-band">
|
||
<input type="range" id="eq-910" class="eq-slider" min="-12" max="12" value="0">
|
||
<span class="eq-label">910Hz</span>
|
||
</div>
|
||
<div class="eq-band">
|
||
<input type="range" id="eq-3600" class="eq-slider" min="-12" max="12" value="0">
|
||
<span class="eq-label">3.6kHz</span>
|
||
</div>
|
||
<div class="eq-band">
|
||
<input type="range" id="eq-7500" class="eq-slider" min="-12" max="12" value="0">
|
||
<span class="eq-label">7.5kHz</span>
|
||
</div>
|
||
</div>
|
||
|
||
<div class="eq-extras">
|
||
<div class="eq-extra">
|
||
<label for="bass-boost">Bass Boost</label>
|
||
<input type="range" id="bass-boost" class="eq-boost-slider" min="0" max="12" value="0">
|
||
<span id="bass-boost-val">0dB</span>
|
||
</div>
|
||
<div class="eq-extra">
|
||
<label for="volume-boost">Volume Boost</label>
|
||
<input type="range" id="volume-boost" class="eq-boost-slider" min="0" max="6" value="0">
|
||
<span id="volume-boost-val">0dB</span>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
|
||
<!-- Audio Elements (dual for gapless/crossfade) -->
|
||
<audio id="audio-player" preload="auto"></audio>
|
||
<audio id="audio-player-2" preload="auto"></audio>
|
||
</div>
|
||
|
||
<!-- Fullscreen Player Overlay (outside .app for proper fixed positioning) -->
|
||
<div id="fullscreen-player" class="fullscreen-player hidden">
|
||
<div class="fs-backdrop"></div>
|
||
<div class="fs-content">
|
||
<div class="fs-header">
|
||
<button id="fs-close-btn" class="fs-close-btn">×</button>
|
||
</div>
|
||
<div class="fs-art-container">
|
||
<img id="fs-art" src="/static/icon.svg" alt="Album Art">
|
||
<button id="fs-lyrics-btn" class="fs-art-lyrics-btn" title="Lyrics">📝</button>
|
||
</div>
|
||
<div class="fs-info">
|
||
<h2 id="fs-title">No Track Playing</h2>
|
||
<p id="fs-artist">Select music to play</p>
|
||
<div id="fs-dj-info" class="fs-dj-info hidden"></div>
|
||
</div>
|
||
<div class="fs-progress-container">
|
||
<span id="fs-current-time">0:00</span>
|
||
<input type="range" id="fs-progress-bar" class="progress-bar" min="0" max="100" value="0" title="Seek">
|
||
<span id="fs-duration">0:00</span>
|
||
</div>
|
||
<div class="fs-controls">
|
||
<button id="fs-heart-btn" class="fs-control-btn" title="Add to Playlist">🩷</button>
|
||
<button id="fs-prev-btn" class="fs-control-btn">⏮</button>
|
||
<button id="fs-play-btn" class="fs-control-btn play-btn">▶</button>
|
||
<button id="fs-next-btn" class="fs-control-btn">⏭</button>
|
||
<button id="fs-download-btn" class="fs-control-btn" title="Download">⬇</button>
|
||
<button id="fs-visualizer-btn" class="fs-control-btn" title="Visualizer">🌈</button>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
|
||
<!-- Visualizer Overlay -->
|
||
<div id="visualizer-overlay" class="visualizer-overlay hidden">
|
||
<canvas id="visualizer-canvas"></canvas>
|
||
<canvas id="visualizer-canvas-webgl" class="hidden"></canvas>
|
||
<div class="visualizer-controls">
|
||
<div class="visualizer-track-info">
|
||
<span id="viz-track-name">No Track</span>
|
||
<span id="viz-track-artist"></span>
|
||
</div>
|
||
<div class="visualizer-mode-selector">
|
||
<button id="viz-prev-preset" class="viz-action-btn" title="Previous Preset (P)" style="display: none;">⏮ Prev</button>
|
||
<button class="viz-mode-btn" data-mode="milkdrop">MilkDrop</button>
|
||
<button id="viz-next-preset" class="viz-action-btn" title="Next Preset (N)" style="display: none;">Next ⏭</button>
|
||
<button class="viz-mode-btn active" data-mode="bars">Bars</button>
|
||
<button class="viz-mode-btn" data-mode="wave">Wave</button>
|
||
<button class="viz-mode-btn" data-mode="particles">Particles</button>
|
||
</div>
|
||
<button id="visualizer-close" class="visualizer-close-btn">✕ Exit</button>
|
||
</div>
|
||
<div class="visualizer-hint">Press ESC or click to exit</div>
|
||
</div>
|
||
|
||
<!-- Podcast Episode Details Modal -->
|
||
<div id="podcast-modal" class="podcast-modal hidden">
|
||
<div class="podcast-modal-content">
|
||
<button id="podcast-modal-close" class="podcast-modal-close">×</button>
|
||
<img id="podcast-modal-art" class="podcast-modal-art" src="" alt="Episode Art">
|
||
<h2 id="podcast-modal-title" class="podcast-modal-title"></h2>
|
||
<p id="podcast-modal-date" class="podcast-modal-date"></p>
|
||
<p id="podcast-modal-duration" class="podcast-modal-duration"></p>
|
||
<div id="podcast-modal-description" class="podcast-modal-description"></div>
|
||
<div class="podcast-modal-actions">
|
||
<button id="podcast-modal-play" class="podcast-modal-play">▶ Play Episode</button>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
|
||
<!-- Lyrics Modal -->
|
||
<div id="lyrics-modal" class="lyrics-modal hidden">
|
||
<div class="lyrics-modal-content">
|
||
<button id="lyrics-modal-close" class="lyrics-modal-close">×</button>
|
||
<div class="lyrics-modal-header">
|
||
<img id="lyrics-modal-art" class="lyrics-modal-art" src="" alt="Album Art">
|
||
<div class="lyrics-modal-info">
|
||
<h2 id="lyrics-modal-title">Song Title</h2>
|
||
<p id="lyrics-modal-artist">Artist</p>
|
||
<p id="lyrics-modal-album" class="lyrics-modal-album"></p>
|
||
</div>
|
||
</div>
|
||
<div class="lyrics-tabs">
|
||
<button class="lyrics-tab active" data-tab="lyrics">Lyrics</button>
|
||
<button class="lyrics-tab" data-tab="about">About</button>
|
||
<button class="lyrics-tab" data-tab="annotations">Annotations</button>
|
||
</div>
|
||
<div class="lyrics-tab-content">
|
||
<div id="lyrics-panel" class="lyrics-panel active">
|
||
<div id="lyrics-loading" class="lyrics-loading hidden">
|
||
<div class="lyrics-spinner"></div>
|
||
<p>Fetching lyrics...</p>
|
||
</div>
|
||
<div id="lyrics-text" class="lyrics-text"></div>
|
||
<div id="lyrics-not-found" class="lyrics-not-found hidden">
|
||
<p>😢 Lyrics not found for this track</p>
|
||
<a id="lyrics-search-link" href="#" target="_blank" class="lyrics-search-link">Search on Genius →</a>
|
||
</div>
|
||
</div>
|
||
<div id="about-panel" class="lyrics-panel">
|
||
<div id="about-content" class="about-content">
|
||
<div id="about-description" class="about-description"></div>
|
||
<div id="about-credits" class="about-credits">
|
||
<p id="about-release"></p>
|
||
<p id="about-writers"></p>
|
||
<p id="about-producers"></p>
|
||
</div>
|
||
<a id="genius-link" href="#" target="_blank" class="genius-link">View on Genius →</a>
|
||
</div>
|
||
</div>
|
||
<div id="annotations-panel" class="lyrics-panel">
|
||
<div id="annotations-loading" class="lyrics-loading hidden">
|
||
<div class="lyrics-spinner"></div>
|
||
<p>Loading annotations...</p>
|
||
</div>
|
||
<div id="annotations-list" class="annotations-list"></div>
|
||
<div id="annotations-empty" class="lyrics-not-found hidden">
|
||
<p>No annotations available for this track</p>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
|
||
<!-- Concerts Modal -->
|
||
<div id="concerts-modal" class="concerts-modal hidden">
|
||
<div class="concerts-modal-content">
|
||
<button id="concerts-modal-close" class="concerts-modal-close">×</button>
|
||
<div class="concerts-modal-header">
|
||
<h2>🎤 Upcoming Concerts</h2>
|
||
</div>
|
||
<div class="concerts-settings">
|
||
<label>My Cities (comma-separated):</label>
|
||
<input type="text" id="concerts-cities" class="concerts-cities-input" placeholder="San Francisco, Los Angeles, Seattle...">
|
||
<button id="concerts-save-cities" class="concerts-save-btn">Save</button>
|
||
</div>
|
||
<div class="concerts-tabs">
|
||
<button class="concerts-tab active" data-source="queue">From Queue</button>
|
||
<button class="concerts-tab" data-source="search">Search Artist</button>
|
||
</div>
|
||
<div id="concerts-search-section" class="concerts-search-section hidden">
|
||
<input type="text" id="concerts-artist-search" class="concerts-artist-input" placeholder="Search artist...">
|
||
<button id="concerts-search-btn" class="concerts-search-btn">🔍</button>
|
||
</div>
|
||
<div id="concerts-loading" class="lyrics-loading hidden">
|
||
<div class="lyrics-spinner"></div>
|
||
<p>Finding concerts...</p>
|
||
</div>
|
||
<div id="concerts-list" class="concerts-list"></div>
|
||
<div id="concerts-empty" class="concerts-empty hidden">
|
||
<p>No upcoming concerts found</p>
|
||
<p class="concerts-empty-hint">Try adding more artists to your queue or adjusting your cities</p>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
|
||
<!-- DJ Setlist Modal -->
|
||
<div id="dj-setlist-modal" class="dj-modal hidden">
|
||
<div class="dj-modal-content">
|
||
<div class="dj-modal-header">
|
||
<h2>🎧 AI DJ Setlist</h2>
|
||
<button id="dj-modal-close" class="dj-modal-close">×</button>
|
||
</div>
|
||
<div class="dj-modal-body">
|
||
<div class="dj-style-selector">
|
||
<label>Set Style:</label>
|
||
<select id="dj-style-select">
|
||
<option value="progressive">Progressive (Build Energy)</option>
|
||
<option value="peak-time">Peak Time (High Energy)</option>
|
||
<option value="chill">Chill (Low-Med Energy)</option>
|
||
<option value="journey">Journey (Wave Pattern)</option>
|
||
</select>
|
||
</div>
|
||
<div id="dj-setlist-loading" class="dj-loading hidden">
|
||
<div class="spinner"></div>
|
||
<p>Analyzing tracks and generating setlist...</p>
|
||
</div>
|
||
<div id="dj-setlist-results" class="dj-results hidden">
|
||
<div id="dj-ordered-tracks" class="dj-ordered-tracks"></div>
|
||
</div>
|
||
</div>
|
||
<div class="dj-modal-actions">
|
||
<button id="dj-generate-btn" class="btn-primary">✨ Generate Setlist</button>
|
||
<button id="dj-apply-btn" class="btn-secondary hidden">Apply to Queue</button>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
|
||
<!-- Hidden File Input (Moved to body end for reliability) -->
|
||
<input
|
||
type="file"
|
||
id="file-input"
|
||
multiple
|
||
accept="audio/*,.flac,.mp3,.wav,.aiff,.aac,.ogg,.m4a"
|
||
style="position: fixed; top: -100px; left: -100px; opacity: 0; pointer-events: none;"
|
||
onclick="this.value=null"
|
||
onchange="if(window.handleFiles) { window.handleFiles(this.files); } else { alert('Error: handleFiles not loaded'); }"
|
||
>
|
||
|
||
<!-- Playlist Selection Modal -->
|
||
<div id="playlist-modal" class="modal hidden">
|
||
<div class="modal-content playlist-modal-content">
|
||
<h3>Add to Playlist</h3>
|
||
<div id="playlist-list" class="playlist-list"></div>
|
||
<div class="create-playlist-row">
|
||
<input type="text" id="new-playlist-input" placeholder="New playlist name..." class="new-playlist-input">
|
||
<button id="create-playlist-btn" class="btn-primary">+</button>
|
||
</div>
|
||
<button id="playlist-modal-close" class="btn-secondary" style="width:100%; margin-top:12px;">Cancel</button>
|
||
</div>
|
||
</div>
|
||
|
||
|
||
|
||
<!-- Setlist Detail Modal -->
|
||
<div id="setlist-modal" class="modal hidden">
|
||
<div class="modal-content setlist-modal-content" style="max-height: 80vh; overflow-y: auto;">
|
||
<div class="modal-header">
|
||
<h3>Setlist</h3>
|
||
<button id="setlist-close-btn" class="modal-close-btn">×</button>
|
||
</div>
|
||
|
||
<div id="setlist-info" class="setlist-header-info">
|
||
<!-- Artist at Venue - Date inserted here -->
|
||
</div>
|
||
|
||
<div id="setlist-tracks" class="setlist-tracks-list">
|
||
<!-- Tracks inserted here -->
|
||
</div>
|
||
|
||
<div class="modal-actions" style="margin-top: 16px;">
|
||
<button id="setlist-play-btn" class="btn-primary" style="width: 100%;">
|
||
🎧 Listen to Show
|
||
</button>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
|
||
<!-- Drive Sync Modal -->
|
||
<div id="drive-sync-modal" class="modal hidden">
|
||
<div class="modal-content drive-modal-content">
|
||
<div class="modal-header">
|
||
<h2>Google Drive Sync</h2>
|
||
<button id="drive-modal-close-top" class="modal-close-btn">×</button>
|
||
</div>
|
||
|
||
<!-- Auth Section -->
|
||
<div id="drive-auth-section">
|
||
<p style="margin-bottom: 20px; color: var(--text-secondary);">Sign in to sync your library across devices.</p>
|
||
<div class="drive-signin-container">
|
||
<button id="drive-signin-btn" class="drive-auth-btn">
|
||
<svg width="20" height="20" viewBox="0 0 24 24"><path fill="currentColor" d="M21.35 11.1h-9.17v2.73h6.51c-.33 3.81-3.5 5.44-6.5 5.44C8.36 19.27 5 16.25 5 12c0-4.1 3.2-7.27 7.2-7.27 3.09 0 4.9 1.97 4.9 1.97L19 4.72S16.56 2 12.1 2C6.42 2 2.03 6.8 2.03 12c0 5.05 4.13 10 10.22 10 5.35 0 9.25-3.67 9.25-9.09 0-1.15-.15-1.81-.15-1.81z"/></svg>
|
||
Sign in with Google
|
||
</button>
|
||
<div id="drive-loading" class="hidden">Connecting...</div>
|
||
</div>
|
||
</div>
|
||
|
||
<!-- Options Section -->
|
||
<div id="drive-options-section" class="hidden">
|
||
<div class="drive-user-info">
|
||
<span id="drive-user-email">Connected</span>
|
||
<button id="drive-signout-btn" class="text-link">Sign Out</button>
|
||
</div>
|
||
|
||
<div class="drive-actions-grid">
|
||
<!-- Upload Group -->
|
||
<div class="sync-group upload-group">
|
||
<h3>☁️ Upload (Save)</h3>
|
||
<p class="sync-desc">Save your current library to the cloud.</p>
|
||
<div class="action-buttons">
|
||
<button id="drive-up-all" class="action-btn upload">
|
||
<span class="btn-icon">⬆️</span>
|
||
<span class="btn-text">Everything</span>
|
||
</button>
|
||
<button id="drive-up-playlists" class="action-btn upload secondary">
|
||
<span class="btn-text">Playlists Only</span>
|
||
</button>
|
||
<button id="drive-up-queue" class="action-btn upload secondary">
|
||
<span class="btn-text">Queue Only</span>
|
||
</button>
|
||
</div>
|
||
</div>
|
||
|
||
<!-- Download Group -->
|
||
<div class="sync-group download-group">
|
||
<h3>⬇️ Download (Load)</h3>
|
||
<p class="sync-desc">Restore your library from the cloud.</p>
|
||
<div class="action-buttons">
|
||
<button id="drive-down-all" class="action-btn download">
|
||
<span class="btn-icon">⬇️</span>
|
||
<span class="btn-text">Everything</span>
|
||
</button>
|
||
<button id="drive-down-playlists" class="action-btn download secondary">
|
||
<span class="btn-text">Playlists Only</span>
|
||
</button>
|
||
<button id="drive-down-queue" class="action-btn download secondary">
|
||
<span class="btn-text">Queue Only</span>
|
||
</button>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
|
||
<!-- Concert Alerts Modal -->
|
||
<div id="concert-modal" class="modal hidden">
|
||
<div class="modal-content concert-modal-content">
|
||
<div class="modal-header">
|
||
<h3>🎫 Concert Alerts</h3>
|
||
<button class="modal-close" id="concert-modal-close">×</button>
|
||
</div>
|
||
|
||
<!-- Tabs: Recent Artists | Search -->
|
||
<div class="concert-tabs">
|
||
<button class="concert-tab active" data-tab="recent">Recent Artists</button>
|
||
<button class="concert-tab" data-tab="search">Search Artist</button>
|
||
</div>
|
||
|
||
<!-- Recent Artists Tab (default) -->
|
||
<div id="concert-recent-section" class="concert-tab-content">
|
||
<p class="concert-hint">Showing concerts for artists you've listened to recently</p>
|
||
</div>
|
||
|
||
<!-- Search Tab -->
|
||
<div id="concert-search-section" class="concert-tab-content hidden">
|
||
<div class="concert-search-wrapper">
|
||
<input type="text" id="concert-artist-search" placeholder="Search for an artist...">
|
||
<button id="concert-search-btn" class="btn-primary">Search</button>
|
||
</div>
|
||
</div>
|
||
|
||
<!-- Results -->
|
||
<div id="concert-results" class="concert-results"></div>
|
||
<div id="concert-loading" class="concert-loading hidden">
|
||
<span class="loading-spinner"></span> Finding concerts...
|
||
</div>
|
||
<div id="concert-empty" class="concert-empty hidden">
|
||
<span>🎵</span>
|
||
<p>No upcoming concerts found</p>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
|
||
<script src="static/jsmediatags.min.js"></script>
|
||
<script src="https://apis.google.com/js/api.js"></script>
|
||
<script src="https://accounts.google.com/gsi/client"></script>
|
||
<script src="/static/app.js?t=1736053100"></script></script>
|
||
</body>
|
||
</html>
|