From a167a00a0a1b03c7e6e42f8ef9f4ffe8b5e11d49 Mon Sep 17 00:00:00 2001 From: Broque Thomas Date: Mon, 29 Dec 2025 09:56:32 -0800 Subject: [PATCH] Add enhanced search with categorized dropdown UI Implements an enhanced search endpoint in the backend that unifies Spotify and local database results, returning categorized artists, albums, and tracks. Updates the frontend with a new dropdown overlay for live search, debounced input, categorized result rendering, and direct integration with the main results area for album/track selection. Adds new CSS for the dropdown and result cards, and updates the Track dataclass to include image URLs for richer UI display. --- core/spotify_client.py | 14 +- web_server.py | 96 +++++ webui/index.html | 110 ++++-- webui/static/script.js | 406 ++++++++++++++++++++- webui/static/style.css | 787 +++++++++++++++++++++++++++++++++++++++++ 5 files changed, 1376 insertions(+), 37 deletions(-) diff --git a/core/spotify_client.py b/core/spotify_client.py index d3a63e5..6e3d496 100644 --- a/core/spotify_client.py +++ b/core/spotify_client.py @@ -61,9 +61,18 @@ class Track: popularity: int preview_url: Optional[str] = None external_urls: Optional[Dict[str, str]] = None - + image_url: Optional[str] = None + @classmethod def from_spotify_track(cls, track_data: Dict[str, Any]) -> 'Track': + # Extract album image (medium size preferred) + album_image_url = None + if 'album' in track_data and 'images' in track_data['album']: + images = track_data['album']['images'] + if images: + # Get medium size image (usually index 1), or largest if not available + album_image_url = images[1]['url'] if len(images) > 1 else images[0]['url'] + return cls( id=track_data['id'], name=track_data['name'], @@ -72,7 +81,8 @@ class Track: duration_ms=track_data['duration_ms'], popularity=track_data['popularity'], preview_url=track_data.get('preview_url'), - external_urls=track_data.get('external_urls') + external_urls=track_data.get('external_urls'), + image_url=album_image_url ) @dataclass diff --git a/web_server.py b/web_server.py index d1e354f..9f1c4e0 100644 --- a/web_server.py +++ b/web_server.py @@ -3224,6 +3224,102 @@ def search_music(): print(f"Search error: {e}") return jsonify({"error": str(e)}), 500 +@app.route('/api/enhanced-search', methods=['POST']) +def enhanced_search(): + """ + Unified search across Spotify and local database for enhanced search mode. + Returns categorized results: DB artists, Spotify artists, albums, and tracks. + """ + data = request.get_json() + query = data.get('query', '').strip() + + if not query: + return jsonify({ + "db_artists": [], + "spotify_artists": [], + "spotify_albums": [], + "spotify_tracks": [] + }) + + logger.info(f"Enhanced search initiated for: '{query}'") + + try: + # Search local database for artists + database = get_database() + db_artists_objs = database.search_artists(query, limit=5) + + # Convert database artists to dictionaries + db_artists = [] + for artist in db_artists_objs: + image_url = None + if hasattr(artist, 'thumb_url') and artist.thumb_url: + image_url = fix_artist_image_url(artist.thumb_url) + + db_artists.append({ + "id": artist.id, + "name": artist.name, + "image_url": image_url + }) + logger.debug(f"DB Artist: {artist.name}, thumb_url: {artist.thumb_url if hasattr(artist, 'thumb_url') else None}, fixed_url: {image_url}") + + # Search Spotify for artists, albums, tracks + spotify_artists = [] + spotify_albums = [] + spotify_tracks = [] + + if spotify_client and spotify_client.is_authenticated(): + # Search for artists + artist_objs = spotify_client.search_artists(query, limit=5) + for artist in artist_objs: + spotify_artists.append({ + "id": artist.id, + "name": artist.name, + "image_url": artist.image_url + }) + + # Search for albums + album_objs = spotify_client.search_albums(query, limit=10) + for album in album_objs: + # Album has 'artists' (list), convert to string + artist_name = ', '.join(album.artists) if album.artists else 'Unknown Artist' + + spotify_albums.append({ + "id": album.id, + "name": album.name, + "artist": artist_name, + "image_url": album.image_url, + "release_date": album.release_date, + "total_tracks": album.total_tracks + }) + + # Search for tracks + track_objs = spotify_client.search_tracks(query, limit=10) + for track in track_objs: + # Track has 'artists' (list), convert to string + artist_name = ', '.join(track.artists) if track.artists else 'Unknown Artist' + + spotify_tracks.append({ + "id": track.id, + "name": track.name, + "artist": artist_name, + "album": track.album, + "duration_ms": track.duration_ms, + "image_url": track.image_url + }) + + logger.info(f"Enhanced search results: {len(db_artists)} DB artists, {len(spotify_artists)} Spotify artists, {len(spotify_albums)} albums, {len(spotify_tracks)} tracks") + + return jsonify({ + "db_artists": db_artists, + "spotify_artists": spotify_artists, + "spotify_albums": spotify_albums, + "spotify_tracks": spotify_tracks + }) + + except Exception as e: + logger.error(f"Enhanced search error: {e}") + return jsonify({"error": str(e)}), 500 + @app.route('/api/download', methods=['POST']) def start_download(): """Simple download route""" diff --git a/webui/index.html b/webui/index.html index f77b35c..9c599f3 100644 --- a/webui/index.html +++ b/webui/index.html @@ -1273,36 +1273,94 @@
- -
-
-
- - -
- -
+ +
+
+
+
+ + +
+ +
+ + + - -
-
-

Enhanced Results

-
0 results
+ +
+
+

Search Results

-
-
-
-

Enhanced Search Ready

-

Intelligent filtering • Smart ranking • Better results

+
+
+

Search results will appear here when you select an album or track.

diff --git a/webui/static/script.js b/webui/static/script.js index 1f9bf1c..03d0271 100644 --- a/webui/static/script.js +++ b/webui/static/script.js @@ -2373,31 +2373,419 @@ function initializeSearchModeToggle() { }); }); - // Initialize enhanced search input handlers + // Initialize enhanced search const enhancedInput = document.getElementById('enhanced-search-input'); const enhancedSearchBtn = document.getElementById('enhanced-search-btn'); const enhancedCancelBtn = document.getElementById('enhanced-cancel-btn'); + const enhancedDropdown = document.getElementById('enhanced-dropdown'); + const loadingState = document.getElementById('enhanced-loading'); + const emptyState = document.getElementById('enhanced-empty'); + const resultsContainer = document.getElementById('enhanced-results-container'); - if (enhancedSearchBtn && enhancedInput) { - enhancedSearchBtn.addEventListener('click', () => { - console.log('Enhanced search clicked - functionality to be implemented'); - showToast('Enhanced search coming soon!', 'info'); + let debounceTimer = null; + let abortController = null; + + // Live search with debouncing + if (enhancedInput) { + enhancedInput.addEventListener('input', (e) => { + const query = e.target.value.trim(); + + // Show/hide cancel button + if (enhancedCancelBtn) { + enhancedCancelBtn.classList.toggle('hidden', query.length === 0); + } + + // Clear debounce timer + clearTimeout(debounceTimer); + + // Hide dropdown if query too short + if (query.length < 2) { + hideDropdown(); + return; + } + + // Debounce search + debounceTimer = setTimeout(() => { + performEnhancedSearch(query); + }, 300); }); enhancedInput.addEventListener('keypress', (e) => { if (e.key === 'Enter') { - console.log('Enhanced search Enter pressed - functionality to be implemented'); - showToast('Enhanced search coming soon!', 'info'); + const query = e.target.value.trim(); + if (query.length >= 2) { + clearTimeout(debounceTimer); + performEnhancedSearch(query); + } + } + }); + } + + if (enhancedSearchBtn) { + enhancedSearchBtn.addEventListener('click', () => { + const query = enhancedInput.value.trim(); + if (query.length >= 2) { + performEnhancedSearch(query); + } else { + showToast('Please enter at least 2 characters', 'error'); } }); } if (enhancedCancelBtn) { enhancedCancelBtn.addEventListener('click', () => { - console.log('Enhanced search cancelled'); - // Cancel logic will be added when functionality is implemented + enhancedInput.value = ''; + enhancedCancelBtn.classList.add('hidden'); + hideDropdown(); + }); + } + + // Close dropdown when clicking outside + document.addEventListener('click', (e) => { + if (enhancedDropdown && !enhancedDropdown.classList.contains('hidden')) { + const isClickInside = e.target.closest('.enhanced-search-input-wrapper'); + if (!isClickInside) { + hideDropdown(); + } + } + }); + + async function performEnhancedSearch(query) { + console.log('Enhanced search:', query); + + // Show loading state + showDropdown(); + loadingState.classList.remove('hidden'); + emptyState.classList.add('hidden'); + resultsContainer.classList.add('hidden'); + + // Abort previous request + if (abortController) { + abortController.abort(); + } + abortController = new AbortController(); + + try { + const response = await fetch('/api/enhanced-search', { + method: 'POST', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify({ query }), + signal: abortController.signal + }); + + if (!response.ok) throw new Error('Search failed'); + + const data = await response.json(); + console.log('Enhanced results:', data); + + // Calculate total + const total = (data.db_artists?.length || 0) + + (data.spotify_artists?.length || 0) + + (data.spotify_albums?.length || 0) + + (data.spotify_tracks?.length || 0); + + // Hide loading + loadingState.classList.add('hidden'); + + if (total === 0) { + emptyState.classList.remove('hidden'); + } else { + renderDropdownResults(data); + resultsContainer.classList.remove('hidden'); + } + + } catch (error) { + if (error.name !== 'AbortError') { + console.error('Enhanced search error:', error); + loadingState.classList.add('hidden'); + emptyState.classList.remove('hidden'); + } + } + } + + function renderDropdownResults(data) { + // Render DB Artists + renderCompactSection( + 'enh-db-artists-section', + 'enh-db-artists-list', + 'enh-db-artists-count', + data.db_artists || [], + (artist) => ({ + image: artist.image_url, + placeholder: '📚', + name: artist.name, + meta: 'In Your Library', + badge: { text: 'Library', class: 'enh-badge-library' }, + onClick: () => { + hideDropdown(); + navigateToPage('library'); + } + }) + ); + + // Render Spotify Artists + renderCompactSection( + 'enh-spotify-artists-section', + 'enh-spotify-artists-list', + 'enh-spotify-artists-count', + data.spotify_artists || [], + (artist) => ({ + image: artist.image_url, + placeholder: '🎤', + name: artist.name, + meta: 'Artist', + badge: { text: 'Spotify', class: 'enh-badge-spotify' }, + onClick: () => { + hideDropdown(); + navigateToPage('artists'); + setTimeout(() => { + const input = document.getElementById('artist-search-input'); + if (input) { + input.value = artist.name; + input.dispatchEvent(new Event('input', { bubbles: true })); + } + }, 100); + } + }) + ); + + // Render Albums + renderCompactSection( + 'enh-albums-section', + 'enh-albums-list', + 'enh-albums-count', + data.spotify_albums || [], + (album) => ({ + image: album.image_url, + placeholder: '💿', + name: album.name, + meta: `${album.artist} • ${album.release_date ? album.release_date.substring(0, 4) : 'N/A'}`, + onClick: () => searchSlskdFor('album', album) + }) + ); + + // Render Tracks + renderCompactSection( + 'enh-tracks-section', + 'enh-tracks-list', + 'enh-tracks-count', + data.spotify_tracks || [], + (track) => ({ + image: track.image_url, + placeholder: '🎵', + name: track.name, + meta: `${track.artist} • ${track.album}`, + onClick: () => searchSlskdFor('track', track) + }) + ); + } + + function renderCompactSection(sectionId, listId, countId, items, mapItem) { + const section = document.getElementById(sectionId); + const list = document.getElementById(listId); + const count = document.getElementById(countId); + + if (!list) return; + + list.innerHTML = ''; + + if (!items || items.length === 0) { + section.classList.add('hidden'); + return; + } + + section.classList.remove('hidden'); + count.textContent = items.length; + + // Determine type based on section ID + const isArtist = sectionId.includes('artists'); + const isAlbum = sectionId.includes('albums'); + const isTrack = sectionId.includes('tracks'); + + // Add appropriate grid class to list + if (isArtist) { + list.classList.add('artists-grid'); + } else if (isAlbum) { + list.classList.add('albums-grid'); + } else if (isTrack) { + list.classList.add('tracks-list'); + } + + items.forEach(item => { + const config = mapItem(item); + const elem = document.createElement('div'); + + // Add appropriate card class + if (isArtist) { + elem.className = 'enh-compact-item artist-card'; + } else if (isAlbum) { + elem.className = 'enh-compact-item album-card'; + } else if (isTrack) { + elem.className = 'enh-compact-item track-item'; + } + + // Build image HTML with type-specific classes + let imageClass = 'enh-item-image'; + let placeholderClass = 'enh-item-image-placeholder'; + + if (isArtist) { + imageClass += ' artist-image'; + placeholderClass += ' artist-placeholder'; + } else if (isAlbum) { + imageClass += ' album-cover'; + placeholderClass += ' album-placeholder'; + } else if (isTrack) { + imageClass += ' track-cover'; + placeholderClass += ' track-placeholder'; + } + + const imageHtml = config.image + ? `${escapeHtml(config.name)}` + : `
${config.placeholder}
`; + + const badgeHtml = config.badge + ? `
${config.badge.text}
` + : ''; + + elem.innerHTML = ` + ${imageHtml} +
+
${escapeHtml(config.name)}
+
${escapeHtml(config.meta)}
+
+ ${badgeHtml} + `; + + elem.addEventListener('click', config.onClick); + list.appendChild(elem); }); } + + async function searchSlskdFor(type, item) { + const mainResultsArea = document.getElementById('enhanced-main-results-area'); + if (!mainResultsArea) return; + + // Show loading in main results area + mainResultsArea.innerHTML = ` +
+
+

Searching for ${type === 'album' ? 'album' : 'track'}...

+
+ `; + + const query = `${item.artist} ${item.name}`; + + try { + const response = await fetch('/api/search', { + method: 'POST', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify({ query }) + }); + + const data = await response.json(); + + if (data.error) { + showToast(`Search error: ${data.error}`, 'error'); + return; + } + + // Filter results + const filtered = data.results.filter(r => r.result_type === type); + + // Render slskd results in main area + renderSlskdInMainArea(filtered, type, item); + + } catch (error) { + console.error('Slskd search error:', error); + showToast('Search failed', 'error'); + mainResultsArea.innerHTML = '

Search failed. Please try again.

'; + } + } + + function renderSlskdInMainArea(results, type, originalItem) { + const mainResultsArea = document.getElementById('enhanced-main-results-area'); + if (!mainResultsArea) return; + + if (!results || results.length === 0) { + mainResultsArea.innerHTML = '

No matches found for this ' + type + '.

'; + return; + } + + // Render results using same style as basic search + mainResultsArea.innerHTML = results.map(result => { + const title = type === 'album' + ? `${result.album_title} (${result.tracks ? result.tracks.length : 0} tracks)` + : result.title; + + return ` +
+
+

${escapeHtml(title)}

+ +
+
+ ${result.bitrate ? `${result.bitrate} kbps` : ''} + ${result.format ? `${result.format.toUpperCase()}` : ''} + ${result.size ? `${(result.size / 1024 / 1024).toFixed(1)} MB` : ''} + ${result.username ? `👤 ${escapeHtml(result.username)}` : ''} +
+
+ `; + }).join(''); + + // Attach download handlers + mainResultsArea.querySelectorAll('.download-result-btn').forEach(btn => { + btn.addEventListener('click', async function() { + const result = JSON.parse(this.dataset.result); + const type = this.dataset.type; + + this.disabled = true; + this.textContent = 'Downloading...'; + + try { + const downloadData = type === 'album' + ? { result_type: 'album', tracks: result.tracks || [] } + : { result_type: 'track', username: result.username, filename: result.filename, size: result.size }; + + const response = await fetch('/api/download', { + method: 'POST', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify(downloadData) + }); + + const data = await response.json(); + + if (data.error) { + showToast(`Download error: ${data.error}`, 'error'); + this.disabled = false; + this.innerHTML = '💾 Download'; + } else { + showToast('Download started!', 'success'); + this.innerHTML = '✅ Added'; + } + } catch (error) { + console.error('Download error:', error); + showToast('Download failed', 'error'); + this.disabled = false; + this.innerHTML = '💾 Download'; + } + }); + }); + } + + function showDropdown() { + if (enhancedDropdown) { + enhancedDropdown.classList.remove('hidden'); + } + } + + function hideDropdown() { + if (enhancedDropdown) { + enhancedDropdown.classList.add('hidden'); + } + } } async function performSearch() { diff --git a/webui/static/style.css b/webui/static/style.css index fde88b5..249db82 100644 --- a/webui/static/style.css +++ b/webui/static/style.css @@ -18885,6 +18885,12 @@ body { /* ENHANCED SEARCH STYLING */ /* ========================================= */ +/* Enhanced Search Input Wrapper (relative for dropdown positioning) */ +.enhanced-search-input-wrapper { + position: relative; + margin-bottom: 20px; +} + /* Enhanced Search Bar */ .enhanced-search-bar-container { background: linear-gradient(135deg, rgba(138, 43, 226, 0.15), rgba(75, 0, 130, 0.15)); @@ -19110,3 +19116,784 @@ body { margin: 0; } +/* ========================================= */ +/* ENHANCED SEARCH DROPDOWN OVERLAY */ +/* ========================================= */ + +.enhanced-dropdown { + position: absolute; + top: 100%; + left: 0; + right: 0; + margin-top: 8px; + background: rgba(24, 24, 24, 0.98); + border: 1px solid rgba(255, 255, 255, 0.1); + border-radius: 8px; + box-shadow: 0 8px 24px rgba(0, 0, 0, 0.6); + max-height: 600px; + overflow: hidden; + z-index: 1000; + backdrop-filter: blur(40px); + animation: slideDown 0.15s ease-out; +} + +@keyframes slideDown { + from { + opacity: 0; + transform: translateY(-10px); + } + to { + opacity: 1; + transform: translateY(0); + } +} + +.enhanced-dropdown-content { + max-height: 600px; + overflow-y: auto; + padding: 16px 20px; +} + +.enhanced-dropdown-content::-webkit-scrollbar { + width: 6px; +} + +.enhanced-dropdown-content::-webkit-scrollbar-track { + background: transparent; +} + +.enhanced-dropdown-content::-webkit-scrollbar-thumb { + background: rgba(255, 255, 255, 0.2); + border-radius: 3px; +} + +.enhanced-dropdown-content::-webkit-scrollbar-thumb:hover { + background: rgba(255, 255, 255, 0.3); +} + +/* Loading and Empty States */ +.enhanced-loading, +.enhanced-empty { + text-align: center; + padding: 40px 20px; + color: rgba(255, 255, 255, 0.7); +} + +.enhanced-loading .spinner { + width: 40px; + height: 40px; + margin: 0 auto 16px; + border: 3px solid rgba(138, 43, 226, 0.2); + border-top-color: rgba(138, 43, 226, 0.8); + border-radius: 50%; + animation: spin 1s linear infinite; +} + +@keyframes spin { + to { transform: rotate(360deg); } +} + +.empty-icon { + font-size: 48px; + margin-bottom: 12px; + opacity: 0.5; +} + +/* Dropdown Sections */ +.enh-dropdown-section { + margin-bottom: 24px; +} + +.enh-dropdown-section:last-child { + margin-bottom: 0; +} + +.enh-section-header { + display: flex; + align-items: center; + gap: 0; + padding: 0; + margin-bottom: 12px; +} + +.enh-section-icon { + display: none; +} + +.enh-section-title { + flex-grow: 1; + font-size: 16px; + font-weight: 700; + color: #ffffff; + margin: 0; + text-transform: none; + letter-spacing: -0.2px; +} + +.enh-section-count { + display: none; +} + +/* ========================================= */ +/* ARTIST CARDS - Clean Spotify Style */ +/* ========================================= */ + +.enh-artists-wrapper { + display: grid; + grid-template-columns: 1fr 1fr; + gap: 20px; + margin-bottom: 24px; +} + +.enh-artist-section { + margin-bottom: 0 !important; +} + +.enh-compact-list.artists-grid { + display: flex; + gap: 16px; + flex-wrap: nowrap; + overflow-x: auto; + padding-bottom: 4px; +} + +.enh-compact-list.artists-grid::-webkit-scrollbar { + height: 4px; +} + +.enh-compact-list.artists-grid::-webkit-scrollbar-thumb { + background: rgba(255, 255, 255, 0.2); + border-radius: 2px; +} + +.enh-compact-item.artist-card { + display: flex; + flex-direction: column; + position: relative; + width: 170px; + height: 170px; + background: linear-gradient(135deg, #1e1e1e 0%, #2a2a2a 100%); + border-radius: 8px; + cursor: pointer; + transition: all 0.2s ease; + border: 1px solid rgba(255, 255, 255, 0.1); + flex-shrink: 0; + overflow: hidden; +} + +.enh-compact-item.artist-card:hover { + transform: translateY(-2px); + box-shadow: 0 8px 16px rgba(0, 0, 0, 0.4); + border-color: rgba(255, 255, 255, 0.2); +} + +.enh-item-image.artist-image { + width: 100%; + height: 110px; + object-fit: cover; + border-radius: 8px 8px 0 0; + border: none; +} + +.enh-item-image-placeholder.artist-placeholder { + width: 100%; + height: 110px; + background: rgba(40, 40, 40, 1); + display: flex; + align-items: center; + justify-content: center; + font-size: 40px; + border: none; + border-radius: 8px 8px 0 0; +} + +.enh-compact-item.artist-card .enh-item-info { + position: absolute; + bottom: 0; + left: 0; + right: 0; + padding: 10px 12px; + background: linear-gradient(to top, rgba(0, 0, 0, 0.9) 0%, rgba(0, 0, 0, 0.6) 70%, transparent 100%); +} + +.enh-compact-item.artist-card .enh-item-name { + font-size: 14px; + font-weight: 600; + color: #ffffff; + margin-bottom: 2px; + overflow: hidden; + text-overflow: ellipsis; + white-space: nowrap; + width: 100%; +} + +.enh-compact-item.artist-card .enh-item-meta { + display: block; + font-size: 12px; + font-weight: 500; + color: rgba(255, 255, 255, 0.7); + overflow: hidden; + text-overflow: ellipsis; + white-space: nowrap; +} + +.enh-compact-item.artist-card .enh-item-badge { + display: none; +} + + +/* ========================================= */ +/* ALBUM CARDS - Clean Grid */ +/* ========================================= */ + +.enh-compact-list.albums-grid { + display: grid; + grid-template-columns: repeat(auto-fill, minmax(170px, 1fr)); + gap: 16px; +} + +.enh-compact-item.album-card { + display: flex; + flex-direction: column; + padding: 12px; + background: transparent; + border-radius: 8px; + cursor: pointer; + transition: background 0.2s ease; + border: none; +} + +.enh-compact-item.album-card:hover { + background: rgba(255, 255, 255, 0.1); +} + +.enh-item-image.album-cover { + width: 100%; + aspect-ratio: 1; + border-radius: 4px; + object-fit: cover; + margin-bottom: 12px; + box-shadow: 0 4px 12px rgba(0, 0, 0, 0.5); +} + +.enh-item-image-placeholder.album-placeholder { + width: 100%; + aspect-ratio: 1; + border-radius: 4px; + background: rgba(40, 40, 40, 1); + display: flex; + align-items: center; + justify-content: center; + font-size: 56px; + margin-bottom: 12px; + box-shadow: 0 4px 12px rgba(0, 0, 0, 0.5); +} + +.enh-compact-item.album-card .enh-item-name { + font-size: 14px; + font-weight: 600; + color: #ffffff; + margin-bottom: 4px; + overflow: hidden; + text-overflow: ellipsis; + display: -webkit-box; + -webkit-line-clamp: 2; + -webkit-box-orient: vertical; + line-height: 1.3; + max-height: 2.6em; +} + +.enh-compact-item.album-card .enh-item-meta { + font-size: 13px; + color: rgba(255, 255, 255, 0.6); + overflow: hidden; + text-overflow: ellipsis; + display: -webkit-box; + -webkit-line-clamp: 2; + -webkit-box-orient: vertical; + line-height: 1.3; +} + +/* ========================================= */ +/* TRACK ITEMS - Two Column Grid */ +/* ========================================= */ + +.enh-compact-list.tracks-list { + display: grid; + grid-template-columns: repeat(2, 1fr); + gap: 4px 12px; +} + +.enh-compact-item.track-item { + display: flex; + align-items: center; + gap: 12px; + padding: 8px 12px; + background: transparent; + border-radius: 4px; + cursor: pointer; + transition: background 0.2s ease; + border: none; +} + +.enh-compact-item.track-item:hover { + background: rgba(255, 255, 255, 0.1); +} + +.enh-item-image.track-cover { + width: 48px; + height: 48px; + border-radius: 4px; + object-fit: cover; + flex-shrink: 0; + box-shadow: 0 2px 8px rgba(0, 0, 0, 0.4); +} + +.enh-item-image-placeholder.track-placeholder { + width: 48px; + height: 48px; + border-radius: 4px; + flex-shrink: 0; + background: rgba(40, 40, 40, 1); + display: flex; + align-items: center; + justify-content: center; + font-size: 24px; + box-shadow: 0 2px 8px rgba(0, 0, 0, 0.4); +} + +.enh-item-info { + flex-grow: 1; + min-width: 0; +} + +.enh-compact-item.track-item .enh-item-name { + font-size: 14px; + font-weight: 500; + color: #ffffff; + margin-bottom: 4px; + overflow: hidden; + text-overflow: ellipsis; + white-space: nowrap; +} + +.enh-compact-item.track-item .enh-item-meta { + font-size: 13px; + color: rgba(255, 255, 255, 0.6); + overflow: hidden; + text-overflow: ellipsis; + white-space: nowrap; +} + +/* ========================================= */ +/* ENHANCED SEARCH CATEGORIZED RESULTS */ +/* (OLD - REMOVE IF NOT NEEDED) */ +/* ========================================= */ + +/* Categorized Container */ +.enh-sr-categorized-container { + display: flex; + flex-direction: column; + gap: 24px; +} + +/* Category Section */ +.enh-sr-category-section { + background: rgba(30, 30, 30, 0.5); + border-radius: 12px; + padding: 20px; + border: 1px solid rgba(138, 43, 226, 0.2); +} + +.enh-sr-category-header { + display: flex; + align-items: center; + gap: 12px; + margin-bottom: 16px; + padding-bottom: 12px; + border-bottom: 1px solid rgba(138, 43, 226, 0.2); +} + +.enh-sr-category-icon { + font-size: 20px; +} + +.enh-sr-category-title { + flex-grow: 1; + font-size: 16px; + font-weight: 600; + color: rgba(255, 255, 255, 0.95); + margin: 0; +} + +.enh-sr-category-count { + background: linear-gradient(135deg, rgba(138, 43, 226, 0.4), rgba(123, 31, 162, 0.4)); + border: 1px solid rgba(138, 43, 226, 0.5); + border-radius: 12px; + padding: 4px 10px; + font-size: 12px; + font-weight: 600; + color: rgba(255, 255, 255, 0.9); +} + +/* Artist Grid (for both DB and Spotify artists) */ +.enh-sr-artist-grid { + display: grid; + grid-template-columns: repeat(auto-fill, minmax(140px, 1fr)); + gap: 16px; +} + +.enh-sr-artist-card { + background: linear-gradient(135deg, rgba(40, 40, 40, 0.8), rgba(30, 30, 30, 0.8)); + border-radius: 12px; + padding: 16px; + cursor: pointer; + transition: all 0.3s ease; + border: 2px solid transparent; + display: flex; + flex-direction: column; + align-items: center; + text-align: center; +} + +.enh-sr-artist-card:hover { + transform: translateY(-4px); + border-color: rgba(138, 43, 226, 0.6); + box-shadow: 0 8px 24px rgba(138, 43, 226, 0.3); +} + +.enh-sr-artist-image-container { + width: 80px; + height: 80px; + border-radius: 50%; + overflow: hidden; + margin-bottom: 12px; + border: 3px solid rgba(138, 43, 226, 0.3); +} + +.enh-sr-artist-image { + width: 100%; + height: 100%; + object-fit: cover; +} + +.enh-sr-artist-placeholder { + width: 100%; + height: 100%; + display: flex; + align-items: center; + justify-content: center; + background: linear-gradient(135deg, rgba(138, 43, 226, 0.4), rgba(123, 31, 162, 0.4)); + font-size: 36px; +} + +.enh-sr-artist-name { + font-size: 14px; + font-weight: 600; + color: rgba(255, 255, 255, 0.95); + margin-bottom: 6px; + word-wrap: break-word; +} + +.enh-sr-artist-badge { + font-size: 11px; + padding: 4px 8px; + border-radius: 8px; + font-weight: 600; +} + +.enh-sr-artist-badge-library { + background: linear-gradient(135deg, rgba(29, 185, 84, 0.3), rgba(24, 156, 71, 0.3)); + border: 1px solid rgba(29, 185, 84, 0.5); + color: rgba(29, 185, 84, 1); +} + +.enh-sr-artist-badge-spotify { + background: linear-gradient(135deg, rgba(138, 43, 226, 0.3), rgba(123, 31, 162, 0.3)); + border: 1px solid rgba(138, 43, 226, 0.5); + color: rgba(138, 43, 226, 1); +} + +/* Album Grid */ +.enh-sr-album-grid { + display: grid; + grid-template-columns: repeat(auto-fill, minmax(160px, 1fr)); + gap: 16px; +} + +.enh-sr-album-card { + background: linear-gradient(135deg, rgba(40, 40, 40, 0.8), rgba(30, 30, 30, 0.8)); + border-radius: 12px; + padding: 12px; + cursor: pointer; + transition: all 0.3s ease; + border: 2px solid transparent; +} + +.enh-sr-album-card:hover { + transform: translateY(-4px); + border-color: rgba(138, 43, 226, 0.6); + box-shadow: 0 8px 24px rgba(138, 43, 226, 0.3); +} + +.enh-sr-album-cover { + width: 100%; + aspect-ratio: 1; + border-radius: 8px; + overflow: hidden; + margin-bottom: 12px; + background: linear-gradient(135deg, rgba(138, 43, 226, 0.3), rgba(123, 31, 162, 0.3)); +} + +.enh-sr-album-cover img { + width: 100%; + height: 100%; + object-fit: cover; +} + +.enh-sr-album-placeholder { + width: 100%; + height: 100%; + display: flex; + align-items: center; + justify-content: center; + font-size: 48px; +} + +.enh-sr-album-name { + font-size: 14px; + font-weight: 600; + color: rgba(255, 255, 255, 0.95); + margin-bottom: 4px; + overflow: hidden; + text-overflow: ellipsis; + white-space: nowrap; +} + +.enh-sr-album-artist { + font-size: 12px; + color: rgba(255, 255, 255, 0.6); + overflow: hidden; + text-overflow: ellipsis; + white-space: nowrap; + margin-bottom: 6px; +} + +.enh-sr-album-meta { + font-size: 11px; + color: rgba(255, 255, 255, 0.5); + display: flex; + justify-content: space-between; +} + +/* Track List */ +.enh-sr-track-list { + display: flex; + flex-direction: column; + gap: 8px; +} + +.enh-sr-track-item { + background: linear-gradient(135deg, rgba(40, 40, 40, 0.6), rgba(30, 30, 30, 0.6)); + border-radius: 10px; + padding: 12px 16px; + cursor: pointer; + transition: all 0.2s ease; + border: 2px solid transparent; + display: flex; + align-items: center; + gap: 12px; +} + +.enh-sr-track-item:hover { + border-color: rgba(138, 43, 226, 0.6); + background: linear-gradient(135deg, rgba(50, 50, 50, 0.8), rgba(40, 40, 40, 0.8)); + transform: translateX(4px); +} + +.enh-sr-track-cover { + width: 48px; + height: 48px; + border-radius: 6px; + overflow: hidden; + flex-shrink: 0; + background: linear-gradient(135deg, rgba(138, 43, 226, 0.3), rgba(123, 31, 162, 0.3)); +} + +.enh-sr-track-cover img { + width: 100%; + height: 100%; + object-fit: cover; +} + +.enh-sr-track-placeholder { + width: 100%; + height: 100%; + display: flex; + align-items: center; + justify-content: center; + font-size: 24px; +} + +.enh-sr-track-info { + flex-grow: 1; + min-width: 0; +} + +.enh-sr-track-name { + font-size: 14px; + font-weight: 600; + color: rgba(255, 255, 255, 0.95); + margin-bottom: 4px; + overflow: hidden; + text-overflow: ellipsis; + white-space: nowrap; +} + +.enh-sr-track-meta { + font-size: 12px; + color: rgba(255, 255, 255, 0.6); + overflow: hidden; + text-overflow: ellipsis; + white-space: nowrap; +} + +.enh-sr-track-duration { + font-size: 12px; + color: rgba(255, 255, 255, 0.5); + flex-shrink: 0; + margin-left: 12px; +} + +/* ========================================= */ +/* SLSKD SEARCH RESULTS VIEW */ +/* ========================================= */ + +.enh-sr-slskd-view { + display: flex; + flex-direction: column; + gap: 16px; +} + +.enh-sr-back-btn { + background: rgba(40, 40, 40, 0.8); + border: 1px solid rgba(138, 43, 226, 0.4); + border-radius: 10px; + padding: 10px 20px; + color: rgba(255, 255, 255, 0.9); + font-size: 14px; + font-weight: 600; + cursor: pointer; + display: flex; + align-items: center; + gap: 8px; + transition: all 0.2s ease; + align-self: flex-start; +} + +.enh-sr-back-btn:hover { + background: rgba(50, 50, 50, 0.9); + border-color: rgba(138, 43, 226, 0.6); + transform: translateX(-4px); +} + +.enh-sr-back-icon { + font-size: 16px; +} + +.enh-sr-slskd-header { + background: linear-gradient(135deg, rgba(138, 43, 226, 0.15), rgba(75, 0, 130, 0.15)); + border-radius: 12px; + padding: 20px; + display: flex; + justify-content: space-between; + align-items: center; + border: 1px solid rgba(138, 43, 226, 0.3); +} + +.enh-sr-slskd-title { + font-size: 16px; + font-weight: 600; + color: rgba(255, 255, 255, 0.95); + margin: 0; +} + +.enh-sr-slskd-badge { + background: linear-gradient(135deg, rgba(138, 43, 226, 0.4), rgba(123, 31, 162, 0.4)); + border: 1px solid rgba(138, 43, 226, 0.5); + border-radius: 12px; + padding: 6px 12px; + font-size: 12px; + font-weight: 600; + color: rgba(255, 255, 255, 0.9); +} + +.enh-sr-slskd-list { + display: flex; + flex-direction: column; + gap: 12px; +} + +/* Slskd Result Item (reuses download manager card styles with enh-sr prefix) */ +.enh-sr-download-card { + background: linear-gradient(135deg, rgba(40, 40, 40, 0.8), rgba(30, 30, 30, 0.8)); + border-radius: 12px; + padding: 16px; + border: 2px solid rgba(138, 43, 226, 0.2); + transition: all 0.3s ease; +} + +.enh-sr-download-card:hover { + border-color: rgba(138, 43, 226, 0.5); + transform: translateY(-2px); + box-shadow: 0 6px 20px rgba(138, 43, 226, 0.2); +} + +.enh-sr-download-card-header { + display: flex; + justify-content: space-between; + align-items: flex-start; + margin-bottom: 12px; +} + +.enh-sr-download-title { + font-size: 14px; + font-weight: 600; + color: rgba(255, 255, 255, 0.95); + margin: 0; + flex-grow: 1; +} + +.enh-sr-download-btn { + background: linear-gradient(135deg, rgba(138, 43, 226, 0.8), rgba(123, 31, 162, 0.8)); + border: none; + border-radius: 8px; + padding: 8px 16px; + color: #ffffff; + font-size: 12px; + font-weight: 600; + cursor: pointer; + transition: all 0.2s ease; + margin-left: 12px; +} + +.enh-sr-download-btn:hover { + background: linear-gradient(135deg, rgba(138, 43, 226, 1), rgba(123, 31, 162, 1)); + transform: translateY(-2px); + box-shadow: 0 4px 12px rgba(138, 43, 226, 0.4); +} + +.enh-sr-download-meta { + display: flex; + flex-wrap: wrap; + gap: 8px; + font-size: 11px; + color: rgba(255, 255, 255, 0.6); +} + +.enh-sr-meta-badge { + background: rgba(50, 50, 50, 0.6); + border-radius: 6px; + padding: 4px 8px; +} +