optimize album and cover art data
This commit is contained in:
parent
62ad5a9cd9
commit
5ffce6bb59
3 changed files with 232 additions and 111 deletions
100
web_server.py
100
web_server.py
|
|
@ -8707,10 +8707,25 @@ def _run_full_missing_tracks_process(batch_id, playlist_id, tracks_json):
|
|||
|
||||
download_batches[batch_id]['phase'] = 'downloading'
|
||||
|
||||
# Get batch album context (if this is an artist album download)
|
||||
batch = download_batches[batch_id]
|
||||
batch_album_context = batch.get('album_context')
|
||||
batch_artist_context = batch.get('artist_context')
|
||||
batch_is_album = batch.get('is_album_download', False)
|
||||
|
||||
for res in missing_tracks:
|
||||
task_id = str(uuid.uuid4())
|
||||
track_info = res['track'].copy()
|
||||
|
||||
# Add explicit album context to track_info for artist album downloads
|
||||
if batch_is_album and batch_album_context and batch_artist_context:
|
||||
track_info['_explicit_album_context'] = batch_album_context
|
||||
track_info['_explicit_artist_context'] = batch_artist_context
|
||||
track_info['_is_explicit_album_download'] = True
|
||||
print(f"🎵 [Task Creation] Added explicit album context for: {track_info.get('name')}")
|
||||
|
||||
download_tasks[task_id] = {
|
||||
'status': 'pending', 'track_info': res['track'],
|
||||
'status': 'pending', 'track_info': track_info,
|
||||
'playlist_id': playlist_id, 'batch_id': batch_id,
|
||||
'track_index': res['track_index'], 'retry_count': 0,
|
||||
'cached_candidates': [], 'used_sources': set(),
|
||||
|
|
@ -9313,10 +9328,49 @@ def _attempt_download_with_candidates(task_id, candidates, track, batch_id=None)
|
|||
try:
|
||||
# Update task status to downloading
|
||||
_update_task_status(task_id, 'downloading')
|
||||
|
||||
# Prepare download (using existing infrastructure)
|
||||
spotify_artist_context = {'id': 'from_sync_modal', 'name': track.artists[0] if track.artists else 'Unknown', 'genres': []}
|
||||
spotify_album_context = {'id': 'from_sync_modal', 'name': track.album, 'release_date': '', 'image_url': None}
|
||||
|
||||
# Prepare download - check if we have explicit album context from artist page
|
||||
track_info = None
|
||||
with tasks_lock:
|
||||
if task_id in download_tasks:
|
||||
track_info = download_tasks[task_id].get('track_info', {})
|
||||
|
||||
# Use explicit album/artist context if available (from artist album downloads)
|
||||
has_explicit_context = track_info and track_info.get('_is_explicit_album_download', False)
|
||||
|
||||
if has_explicit_context:
|
||||
# Use the real Spotify album/artist data from the UI
|
||||
explicit_album = track_info.get('_explicit_album_context', {})
|
||||
explicit_artist = track_info.get('_explicit_artist_context', {})
|
||||
|
||||
spotify_artist_context = {
|
||||
'id': explicit_artist.get('id', 'explicit_artist'),
|
||||
'name': explicit_artist.get('name', track.artists[0] if track.artists else 'Unknown'),
|
||||
'genres': explicit_artist.get('genres', [])
|
||||
}
|
||||
# Handle both image_url formats (direct string or images array)
|
||||
album_image_url = None
|
||||
if explicit_album.get('image_url'):
|
||||
# Backend API returns image_url as direct string
|
||||
album_image_url = explicit_album.get('image_url')
|
||||
elif explicit_album.get('images'):
|
||||
# Fallback: images array format from Spotify API
|
||||
album_image_url = explicit_album.get('images', [{}])[0].get('url')
|
||||
|
||||
spotify_album_context = {
|
||||
'id': explicit_album.get('id', 'explicit_album'),
|
||||
'name': explicit_album.get('name', track.album),
|
||||
'release_date': explicit_album.get('release_date', ''),
|
||||
'image_url': album_image_url,
|
||||
'total_tracks': explicit_album.get('total_tracks', 0),
|
||||
'album_type': explicit_album.get('album_type', 'album')
|
||||
}
|
||||
print(f"🎵 [Explicit Context] Using real album data: '{spotify_album_context['name']}' ({spotify_album_context['album_type']})")
|
||||
else:
|
||||
# Fallback to generic context for playlists/wishlists
|
||||
spotify_artist_context = {'id': 'from_sync_modal', 'name': track.artists[0] if track.artists else 'Unknown', 'genres': []}
|
||||
spotify_album_context = {'id': 'from_sync_modal', 'name': track.album, 'release_date': '', 'image_url': None}
|
||||
|
||||
download_payload = candidate.__dict__
|
||||
|
||||
username = download_payload.get('username')
|
||||
|
|
@ -9377,13 +9431,19 @@ def _attempt_download_with_candidates(task_id, candidates, track, batch_id=None)
|
|||
enhanced_payload['track_number'] = 1
|
||||
print(f"⚠️ [Context] No track.id available, using fallback track_number: 1")
|
||||
|
||||
# Determine if this should be treated as album download based on clean data
|
||||
is_album_context = (
|
||||
track.album and
|
||||
track.album.strip() and
|
||||
track.album != "Unknown Album" and
|
||||
track.album.lower() != track.name.lower() # Album different from track
|
||||
)
|
||||
# Determine if this should be treated as album download
|
||||
# First check if we have explicit album context from artist page
|
||||
if has_explicit_context:
|
||||
is_album_context = True
|
||||
print(f"✅ [Context] Using explicit album context flag from artist page")
|
||||
else:
|
||||
# Fall back to guessing based on clean data
|
||||
is_album_context = (
|
||||
track.album and
|
||||
track.album.strip() and
|
||||
track.album != "Unknown Album" and
|
||||
track.album.lower() != track.name.lower() # Album different from track
|
||||
)
|
||||
else:
|
||||
# Fallback to original data
|
||||
enhanced_payload['spotify_clean_title'] = enhanced_payload.get('title', '')
|
||||
|
|
@ -10480,9 +10540,19 @@ def start_missing_tracks_process(playlist_id):
|
|||
playlist_name = data.get('playlist_name', 'Unknown Playlist')
|
||||
force_download_all = data.get('force_download_all', False)
|
||||
|
||||
# Get album/artist context for artist album downloads
|
||||
is_album_download = data.get('is_album_download', False)
|
||||
album_context = data.get('album_context', None)
|
||||
artist_context = data.get('artist_context', None)
|
||||
|
||||
if not tracks:
|
||||
return jsonify({"success": False, "error": "No tracks provided"}), 400
|
||||
|
||||
# Log album context if provided
|
||||
if is_album_download and album_context and artist_context:
|
||||
print(f"🎵 [Artist Album] Received album context: '{album_context.get('name')}' by '{artist_context.get('name')}' ({album_context.get('album_type', 'album')})")
|
||||
print(f" Release: {album_context.get('release_date', 'Unknown')}, Tracks: {album_context.get('total_tracks', len(tracks))}")
|
||||
|
||||
# Limit concurrent analysis processes to prevent resource exhaustion
|
||||
with tasks_lock:
|
||||
active_analysis_count = sum(1 for batch in download_batches.values()
|
||||
|
|
@ -10510,7 +10580,11 @@ def start_missing_tracks_process(playlist_id):
|
|||
'analysis_total': len(tracks),
|
||||
'analysis_processed': 0,
|
||||
'analysis_results': [],
|
||||
'force_download_all': force_download_all # Pass the force flag to the batch
|
||||
'force_download_all': force_download_all, # Pass the force flag to the batch
|
||||
# Album context for artist album downloads (explicit folder structure)
|
||||
'is_album_download': is_album_download,
|
||||
'album_context': album_context,
|
||||
'artist_context': artist_context
|
||||
}
|
||||
|
||||
# Link YouTube playlist to download process if this is a YouTube playlist
|
||||
|
|
|
|||
|
|
@ -4536,14 +4536,28 @@ async function startMissingTracksProcess(playlistId) {
|
|||
forceToggleContainer.style.display = 'none';
|
||||
}
|
||||
|
||||
// Prepare request body - add album/artist context for artist album downloads
|
||||
const requestBody = {
|
||||
tracks: process.tracks,
|
||||
force_download_all: forceDownloadAll
|
||||
};
|
||||
|
||||
// If this is an artist album download, use album name and include full context
|
||||
if (playlistId.startsWith('artist_album_')) {
|
||||
requestBody.playlist_name = process.album?.name || process.playlist.name;
|
||||
requestBody.is_album_download = true;
|
||||
requestBody.album_context = process.album; // Full Spotify album object
|
||||
requestBody.artist_context = process.artist; // Full Spotify artist object
|
||||
console.log(`🎵 [Artist Album] Sending album context: ${process.album?.name} by ${process.artist?.name}`);
|
||||
} else {
|
||||
// For playlists/wishlists, use the virtual playlist name
|
||||
requestBody.playlist_name = process.playlist.name;
|
||||
}
|
||||
|
||||
const response = await fetch(`/api/playlists/${playlistId}/start-missing-process`, {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify({
|
||||
tracks: process.tracks,
|
||||
playlist_name: process.playlist.name,
|
||||
force_download_all: forceDownloadAll
|
||||
})
|
||||
body: JSON.stringify(requestBody)
|
||||
});
|
||||
|
||||
const data = await response.json();
|
||||
|
|
@ -16616,19 +16630,27 @@ async function createArtistAlbumVirtualPlaylist(album, albumType) {
|
|||
}
|
||||
|
||||
const data = await response.json();
|
||||
|
||||
|
||||
if (!data.success || !data.tracks || data.tracks.length === 0) {
|
||||
throw new Error('No tracks found for this album');
|
||||
}
|
||||
|
||||
console.log(`✅ Loaded ${data.tracks.length} tracks for ${album.name}`);
|
||||
|
||||
|
||||
console.log(`✅ Loaded ${data.tracks.length} tracks`);
|
||||
console.log(`📊 [DEBUG] Backend album data:`, data.album);
|
||||
console.log(`📊 [DEBUG] Album name from backend:`, data.album?.name);
|
||||
console.log(`📊 [DEBUG] Original album param:`, album);
|
||||
|
||||
// Use album data from API response (has complete data including images array)
|
||||
const fullAlbumData = data.album;
|
||||
|
||||
// Format playlist name with artist and album info
|
||||
const playlistName = `[${artist.name}] ${album.name}`;
|
||||
|
||||
const playlistName = `[${artist.name}] ${fullAlbumData.name}`;
|
||||
console.log(`📊 [DEBUG] Playlist name created:`, playlistName);
|
||||
|
||||
// Open download missing tracks modal with formatted tracks
|
||||
// Pass false for showLoadingOverlay since we already have one from handleArtistAlbumClick
|
||||
await openDownloadMissingModalForArtistAlbum(virtualPlaylistId, playlistName, data.tracks, album, artist, false);
|
||||
// Use fullAlbumData from API response instead of album parameter
|
||||
await openDownloadMissingModalForArtistAlbum(virtualPlaylistId, playlistName, data.tracks, fullAlbumData, artist, false);
|
||||
|
||||
// Track this download for artist bubble management
|
||||
registerArtistDownload(artist, album, virtualPlaylistId, albumType);
|
||||
|
|
|
|||
|
|
@ -36,24 +36,26 @@ body {
|
|||
.sidebar {
|
||||
width: 240px;
|
||||
|
||||
/* Premium glassmorphic foundation */
|
||||
/* Apple-style liquid glassmorphic foundation */
|
||||
background: linear-gradient(135deg,
|
||||
rgba(20, 20, 20, 0.95) 0%,
|
||||
rgba(12, 12, 12, 0.98) 100%);
|
||||
backdrop-filter: blur(20px) saturate(1.2);
|
||||
rgba(20, 20, 20, 0.65) 0%,
|
||||
rgba(12, 12, 12, 0.75) 100%);
|
||||
backdrop-filter: blur(40px) saturate(1.8);
|
||||
-webkit-backdrop-filter: blur(40px) saturate(1.8);
|
||||
|
||||
/* Enhanced borders */
|
||||
border-right: 1px solid rgba(255, 255, 255, 0.12);
|
||||
border-top: 1px solid rgba(255, 255, 255, 0.18);
|
||||
border-top-right-radius: 20px;
|
||||
border-bottom-right-radius: 20px;
|
||||
/* Soft translucent borders */
|
||||
border-right: 1px solid rgba(255, 255, 255, 0.08);
|
||||
border-top: 1px solid rgba(255, 255, 255, 0.12);
|
||||
border-top-right-radius: 24px;
|
||||
border-bottom-right-radius: 24px;
|
||||
|
||||
/* Premium shadow effect */
|
||||
/* Soft floating shadow with inner glow */
|
||||
box-shadow:
|
||||
0 20px 60px rgba(0, 0, 0, 0.6),
|
||||
0 8px 32px rgba(0, 0, 0, 0.4),
|
||||
0 0 40px rgba(29, 185, 84, 0.1),
|
||||
inset 0 1px 0 rgba(255, 255, 255, 0.15);
|
||||
0 8px 32px rgba(0, 0, 0, 0.3),
|
||||
0 4px 16px rgba(0, 0, 0, 0.2),
|
||||
0 0 60px rgba(29, 185, 84, 0.06),
|
||||
inset 0 1px 0 rgba(255, 255, 255, 0.1),
|
||||
inset 0 -1px 0 rgba(0, 0, 0, 0.2);
|
||||
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
|
|
@ -62,7 +64,7 @@ body {
|
|||
|
||||
/* Custom scrollbar styling */
|
||||
scrollbar-width: thin;
|
||||
scrollbar-color: rgba(29, 185, 84, 0.5) rgba(255, 255, 255, 0.05);
|
||||
scrollbar-color: rgba(29, 185, 84, 0.4) rgba(255, 255, 255, 0.03);
|
||||
}
|
||||
|
||||
/* Sidebar scrollbar webkit styling */
|
||||
|
|
@ -156,45 +158,48 @@ body {
|
|||
}
|
||||
|
||||
.nav-button:hover {
|
||||
background: linear-gradient(135deg,
|
||||
rgba(255, 255, 255, 0.08) 0%,
|
||||
rgba(255, 255, 255, 0.04) 100%);
|
||||
border: 1px solid rgba(255, 255, 255, 0.12);
|
||||
background: linear-gradient(135deg,
|
||||
rgba(255, 255, 255, 0.06) 0%,
|
||||
rgba(255, 255, 255, 0.03) 100%);
|
||||
backdrop-filter: blur(20px);
|
||||
-webkit-backdrop-filter: blur(20px);
|
||||
border: 1px solid rgba(255, 255, 255, 0.1);
|
||||
transform: translateX(4px);
|
||||
box-shadow:
|
||||
0 4px 16px rgba(0, 0, 0, 0.3),
|
||||
0 2px 8px rgba(0, 0, 0, 0.2),
|
||||
inset 0 1px 0 rgba(255, 255, 255, 0.1);
|
||||
box-shadow:
|
||||
0 4px 12px rgba(0, 0, 0, 0.2),
|
||||
inset 0 1px 0 rgba(255, 255, 255, 0.08);
|
||||
}
|
||||
|
||||
.nav-button.active {
|
||||
background: linear-gradient(135deg,
|
||||
rgba(29, 185, 84, 0.20) 0%,
|
||||
rgba(29, 185, 84, 0.15) 50%,
|
||||
rgba(29, 185, 84, 0.10) 100%);
|
||||
border: 1px solid rgba(29, 185, 84, 0.3);
|
||||
background: linear-gradient(135deg,
|
||||
rgba(29, 185, 84, 0.16) 0%,
|
||||
rgba(29, 185, 84, 0.12) 50%,
|
||||
rgba(29, 185, 84, 0.08) 100%);
|
||||
backdrop-filter: blur(20px) saturate(1.6);
|
||||
-webkit-backdrop-filter: blur(20px) saturate(1.6);
|
||||
border: 1px solid rgba(29, 185, 84, 0.25);
|
||||
transform: translateX(6px);
|
||||
box-shadow:
|
||||
0 6px 20px rgba(29, 185, 84, 0.4),
|
||||
0 4px 12px rgba(29, 185, 84, 0.3),
|
||||
0 2px 6px rgba(0, 0, 0, 0.3),
|
||||
inset 0 1px 0 rgba(255, 255, 255, 0.15),
|
||||
inset -1px 0 0 rgba(29, 185, 84, 0.8);
|
||||
box-shadow:
|
||||
0 4px 16px rgba(29, 185, 84, 0.25),
|
||||
0 2px 8px rgba(0, 0, 0, 0.2),
|
||||
inset 0 1px 0 rgba(255, 255, 255, 0.12),
|
||||
inset -1px 0 0 rgba(29, 185, 84, 0.6);
|
||||
}
|
||||
|
||||
.nav-button.active:hover {
|
||||
background: linear-gradient(135deg,
|
||||
rgba(29, 185, 84, 0.28) 0%,
|
||||
rgba(29, 185, 84, 0.22) 50%,
|
||||
rgba(29, 185, 84, 0.15) 100%);
|
||||
border: 1px solid rgba(29, 185, 84, 0.4);
|
||||
background: linear-gradient(135deg,
|
||||
rgba(29, 185, 84, 0.22) 0%,
|
||||
rgba(29, 185, 84, 0.18) 50%,
|
||||
rgba(29, 185, 84, 0.12) 100%);
|
||||
backdrop-filter: blur(20px) saturate(1.8);
|
||||
-webkit-backdrop-filter: blur(20px) saturate(1.8);
|
||||
border: 1px solid rgba(29, 185, 84, 0.35);
|
||||
transform: translateX(8px);
|
||||
box-shadow:
|
||||
0 8px 24px rgba(29, 185, 84, 0.5),
|
||||
0 6px 16px rgba(29, 185, 84, 0.4),
|
||||
0 3px 8px rgba(0, 0, 0, 0.3),
|
||||
inset 0 1px 0 rgba(255, 255, 255, 0.2),
|
||||
inset -1px 0 0 rgba(29, 185, 84, 0.9);
|
||||
box-shadow:
|
||||
0 6px 20px rgba(29, 185, 84, 0.3),
|
||||
0 3px 10px rgba(0, 0, 0, 0.2),
|
||||
inset 0 1px 0 rgba(255, 255, 255, 0.15),
|
||||
inset -1px 0 0 rgba(29, 185, 84, 0.7);
|
||||
}
|
||||
|
||||
.nav-icon {
|
||||
|
|
@ -205,29 +210,33 @@ body {
|
|||
justify-content: center;
|
||||
font-size: 16px;
|
||||
font-weight: 600;
|
||||
border-radius: 14px;
|
||||
background: linear-gradient(135deg,
|
||||
rgba(255, 255, 255, 0.10) 0%,
|
||||
rgba(255, 255, 255, 0.06) 100%);
|
||||
border: 1px solid rgba(255, 255, 255, 0.08);
|
||||
border-radius: 10px;
|
||||
background: linear-gradient(135deg,
|
||||
rgba(255, 255, 255, 0.08) 0%,
|
||||
rgba(255, 255, 255, 0.04) 100%);
|
||||
backdrop-filter: blur(10px);
|
||||
-webkit-backdrop-filter: blur(10px);
|
||||
border: 1px solid rgba(255, 255, 255, 0.06);
|
||||
color: rgba(255, 255, 255, 0.8);
|
||||
transition: all 0.3s cubic-bezier(0.4, 0, 0.2, 1);
|
||||
box-shadow:
|
||||
0 2px 6px rgba(0, 0, 0, 0.2),
|
||||
inset 0 1px 0 rgba(255, 255, 255, 0.1);
|
||||
box-shadow:
|
||||
0 2px 4px rgba(0, 0, 0, 0.15),
|
||||
inset 0 1px 0 rgba(255, 255, 255, 0.08);
|
||||
}
|
||||
|
||||
.nav-button.active .nav-icon {
|
||||
color: #1ed760;
|
||||
background: linear-gradient(135deg,
|
||||
rgba(29, 185, 84, 0.30) 0%,
|
||||
rgba(30, 215, 96, 0.25) 100%);
|
||||
border: 1px solid rgba(29, 185, 84, 0.4);
|
||||
background: linear-gradient(135deg,
|
||||
rgba(29, 185, 84, 0.25) 0%,
|
||||
rgba(30, 215, 96, 0.20) 100%);
|
||||
backdrop-filter: blur(10px) saturate(1.6);
|
||||
-webkit-backdrop-filter: blur(10px) saturate(1.6);
|
||||
border: 1px solid rgba(29, 185, 84, 0.3);
|
||||
font-weight: 700;
|
||||
box-shadow:
|
||||
0 4px 12px rgba(29, 185, 84, 0.4),
|
||||
0 2px 6px rgba(29, 185, 84, 0.3),
|
||||
inset 0 1px 0 rgba(255, 255, 255, 0.2);
|
||||
box-shadow:
|
||||
0 3px 8px rgba(29, 185, 84, 0.3),
|
||||
0 1px 4px rgba(29, 185, 84, 0.2),
|
||||
inset 0 1px 0 rgba(255, 255, 255, 0.15);
|
||||
}
|
||||
|
||||
.nav-text {
|
||||
|
|
@ -882,11 +891,12 @@ body {
|
|||
|
||||
.main-content {
|
||||
flex: 1;
|
||||
background: linear-gradient(135deg,
|
||||
rgba(12, 12, 12, 0.95) 0%,
|
||||
rgba(16, 16, 16, 0.98) 50%,
|
||||
rgba(8, 8, 8, 0.95) 100%);
|
||||
backdrop-filter: blur(20px) saturate(1.2);
|
||||
background: linear-gradient(135deg,
|
||||
rgba(12, 12, 12, 0.6) 0%,
|
||||
rgba(16, 16, 16, 0.7) 50%,
|
||||
rgba(8, 8, 8, 0.6) 100%);
|
||||
backdrop-filter: blur(40px) saturate(1.8);
|
||||
-webkit-backdrop-filter: blur(40px) saturate(1.8);
|
||||
overflow: auto;
|
||||
}
|
||||
|
||||
|
|
@ -979,11 +989,26 @@ body {
|
|||
}
|
||||
|
||||
.stat-card {
|
||||
background: linear-gradient(135deg, rgba(29, 185, 84, 0.1) 0%, rgba(255, 255, 255, 0.02) 100%);
|
||||
border: 1px solid rgba(29, 185, 84, 0.2);
|
||||
border-radius: 12px;
|
||||
background: linear-gradient(135deg, rgba(29, 185, 84, 0.08) 0%, rgba(255, 255, 255, 0.03) 100%);
|
||||
backdrop-filter: blur(20px) saturate(1.5);
|
||||
-webkit-backdrop-filter: blur(20px) saturate(1.5);
|
||||
border: 1px solid rgba(29, 185, 84, 0.15);
|
||||
border-radius: 16px;
|
||||
padding: 24px;
|
||||
text-align: center;
|
||||
box-shadow:
|
||||
0 4px 16px rgba(0, 0, 0, 0.2),
|
||||
inset 0 1px 0 rgba(255, 255, 255, 0.1);
|
||||
transition: all 0.3s cubic-bezier(0.4, 0, 0.2, 1);
|
||||
}
|
||||
|
||||
.stat-card:hover {
|
||||
transform: translateY(-2px);
|
||||
box-shadow:
|
||||
0 8px 24px rgba(0, 0, 0, 0.3),
|
||||
0 0 20px rgba(29, 185, 84, 0.1),
|
||||
inset 0 1px 0 rgba(255, 255, 255, 0.15);
|
||||
border-color: rgba(29, 185, 84, 0.25);
|
||||
}
|
||||
|
||||
.stat-value {
|
||||
|
|
@ -3684,24 +3709,24 @@ body {
|
|||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 25px; /* Spacing between sections */
|
||||
padding: 28px 24px 30px 24px; /* Match modal padding */
|
||||
|
||||
/* Enhanced glassmorphic foundation matching modal */
|
||||
background: linear-gradient(135deg,
|
||||
rgba(20, 20, 20, 0.85) 0%,
|
||||
rgba(12, 12, 12, 0.92) 100%);
|
||||
backdrop-filter: blur(20px) saturate(1.2);
|
||||
border-radius: 20px;
|
||||
border: 1px solid rgba(255, 255, 255, 0.12);
|
||||
border-top: 1px solid rgba(255, 255, 255, 0.18);
|
||||
padding: 28px 24px 30px 24px;
|
||||
|
||||
/* Apple-style liquid glassmorphic foundation */
|
||||
background: linear-gradient(135deg,
|
||||
rgba(20, 20, 20, 0.55) 0%,
|
||||
rgba(12, 12, 12, 0.65) 100%);
|
||||
backdrop-filter: blur(40px) saturate(1.8);
|
||||
-webkit-backdrop-filter: blur(40px) saturate(1.8);
|
||||
border-radius: 24px;
|
||||
border: 1px solid rgba(255, 255, 255, 0.08);
|
||||
border-top: 1px solid rgba(255, 255, 255, 0.12);
|
||||
margin: 20px;
|
||||
|
||||
/* Premium shadow effect matching modal */
|
||||
box-shadow:
|
||||
0 20px 60px rgba(0, 0, 0, 0.6),
|
||||
0 8px 32px rgba(0, 0, 0, 0.4),
|
||||
0 0 40px rgba(29, 185, 84, 0.1),
|
||||
inset 0 1px 0 rgba(255, 255, 255, 0.15);
|
||||
|
||||
/* Soft floating shadow */
|
||||
box-shadow:
|
||||
0 8px 32px rgba(0, 0, 0, 0.3),
|
||||
0 4px 16px rgba(0, 0, 0, 0.2),
|
||||
inset 0 1px 0 rgba(255, 255, 255, 0.08);
|
||||
}
|
||||
|
||||
.dashboard-section {
|
||||
|
|
|
|||
Loading…
Reference in a new issue