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.
pull/97/head
Broque Thomas 4 months ago
parent 14a5944ae1
commit a167a00a0a

@ -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

@ -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"""

@ -1273,36 +1273,94 @@
<!-- Enhanced Search Section (New) -->
<div id="enhanced-search-section" class="search-section">
<!-- Enhanced Search Bar -->
<div class="enhanced-search-bar-container">
<div class="enhanced-search-wrapper">
<div class="enhanced-search-icon"></div>
<input type="text" id="enhanced-search-input" placeholder="Enter artist, album, or track name for intelligent search...">
<button id="enhanced-cancel-btn" class="enhanced-cancel-btn hidden"></button>
</div>
<button id="enhanced-search-btn" class="enhanced-search-btn">
<span class="btn-icon">🔍</span>
<span class="btn-text">Search</span>
</button>
</div>
<!-- Enhanced Search Bar with Dropdown -->
<div class="enhanced-search-input-wrapper">
<div class="enhanced-search-bar-container">
<div class="enhanced-search-wrapper">
<div class="enhanced-search-icon"></div>
<input type="text" id="enhanced-search-input" placeholder="Search for artists, albums, or tracks...">
<button id="enhanced-cancel-btn" class="enhanced-cancel-btn hidden"></button>
</div>
<button id="enhanced-search-btn" class="enhanced-search-btn">
<span class="btn-icon">🔍</span>
<span class="btn-text">Search</span>
</button>
</div>
<!-- Enhanced Search Dropdown (Overlay Panel) -->
<div id="enhanced-dropdown" class="enhanced-dropdown hidden">
<div class="enhanced-dropdown-content">
<!-- Loading State -->
<div id="enhanced-loading" class="enhanced-loading hidden">
<div class="spinner"></div>
<p>Searching across Spotify and your library...</p>
</div>
<!-- Empty State -->
<div id="enhanced-empty" class="enhanced-empty hidden">
<div class="empty-icon">🔍</div>
<p>No results found</p>
</div>
<!-- Results Container -->
<div id="enhanced-results-container" class="enhanced-results-container hidden">
<!-- Artists Container (Side by Side) -->
<div class="enh-artists-wrapper">
<!-- DB Artists -->
<div id="enh-db-artists-section" class="enh-dropdown-section enh-artist-section hidden">
<div class="enh-section-header">
<span class="enh-section-icon">📚</span>
<h4 class="enh-section-title">In Your Library</h4>
<span class="enh-section-count" id="enh-db-artists-count">0</span>
</div>
<div class="enh-compact-list" id="enh-db-artists-list"></div>
</div>
<!-- Spotify Artists -->
<div id="enh-spotify-artists-section" class="enh-dropdown-section enh-artist-section hidden">
<div class="enh-section-header">
<span class="enh-section-icon">🎤</span>
<h4 class="enh-section-title">Artists</h4>
<span class="enh-section-count" id="enh-spotify-artists-count">0</span>
</div>
<div class="enh-compact-list" id="enh-spotify-artists-list"></div>
</div>
</div>
<!-- Albums -->
<div id="enh-albums-section" class="enh-dropdown-section hidden">
<div class="enh-section-header">
<span class="enh-section-icon">💿</span>
<h4 class="enh-section-title">Albums</h4>
<span class="enh-section-count" id="enh-albums-count">0</span>
</div>
<div class="enh-compact-list" id="enh-albums-list"></div>
</div>
<!-- Enhanced Search Status -->
<div class="enhanced-search-status">
<div class="enhanced-status-indicator"></div>
<p id="enhanced-search-status-text">Ready • Enhanced search provides intelligent filtering and ranking</p>
<!-- Tracks -->
<div id="enh-tracks-section" class="enh-dropdown-section hidden">
<div class="enh-section-header">
<span class="enh-section-icon">🎵</span>
<h4 class="enh-section-title">Tracks</h4>
<span class="enh-section-count" id="enh-tracks-count">0</span>
</div>
<div class="enh-compact-list" id="enh-tracks-list"></div>
</div>
</div>
</div>
</div>
</div>
<!-- Enhanced Search Results -->
<div class="enhanced-search-results-container">
<div class="enhanced-results-header">
<h3>Enhanced Results</h3>
<div class="results-count-badge" id="enhanced-results-count">0 results</div>
<!-- Main Search Results Area (for slskd results) -->
<div class="search-results-container">
<div class="search-results-header">
<h3>Search Results</h3>
</div>
<div class="enhanced-results-scroll-area" id="enhanced-search-results-area">
<div class="enhanced-results-placeholder">
<div class="placeholder-icon"></div>
<p class="placeholder-title">Enhanced Search Ready</p>
<p class="placeholder-subtitle">Intelligent filtering • Smart ranking • Better results</p>
<div class="search-results-scroll-area" id="enhanced-main-results-area">
<div class="search-results-placeholder">
<p>Search results will appear here when you select an album or track.</p>
</div>
</div>
</div>

@ -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
? `<img src="${escapeHtml(config.image)}" class="${imageClass}" alt="${escapeHtml(config.name)}">`
: `<div class="${placeholderClass}">${config.placeholder}</div>`;
const badgeHtml = config.badge
? `<div class="enh-item-badge ${config.badge.class}">${config.badge.text}</div>`
: '';
elem.innerHTML = `
${imageHtml}
<div class="enh-item-info">
<div class="enh-item-name">${escapeHtml(config.name)}</div>
<div class="enh-item-meta">${escapeHtml(config.meta)}</div>
</div>
${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 = `
<div style="text-align: center; padding: 60px 20px; color: rgba(255,255,255,0.7);">
<div style="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;"></div>
<p>Searching for ${type === 'album' ? 'album' : 'track'}...</p>
</div>
`;
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 = '<div class="search-results-placeholder"><p>Search failed. Please try again.</p></div>';
}
}
function renderSlskdInMainArea(results, type, originalItem) {
const mainResultsArea = document.getElementById('enhanced-main-results-area');
if (!mainResultsArea) return;
if (!results || results.length === 0) {
mainResultsArea.innerHTML = '<div class="search-results-placeholder"><p>No matches found for this ' + type + '.</p></div>';
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 `
<div class="result-card">
<div class="result-card-header">
<h4 class="result-title">${escapeHtml(title)}</h4>
<button class="download-result-btn" data-result='${JSON.stringify(result).replace(/'/g, "&#39;")}' data-type="${type}">
💾 Download
</button>
</div>
<div class="result-meta">
${result.bitrate ? `<span class="meta-badge">${result.bitrate} kbps</span>` : ''}
${result.format ? `<span class="meta-badge">${result.format.toUpperCase()}</span>` : ''}
${result.size ? `<span class="meta-badge">${(result.size / 1024 / 1024).toFixed(1)} MB</span>` : ''}
${result.username ? `<span class="meta-badge">👤 ${escapeHtml(result.username)}</span>` : ''}
</div>
</div>
`;
}).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() {

@ -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;
}

Loading…
Cancel
Save