optimize album and cover art data
This commit is contained in:
parent
62ad5a9cd9
commit
5ffce6bb59
3 changed files with 232 additions and 111 deletions
|
|
@ -8707,10 +8707,25 @@ def _run_full_missing_tracks_process(batch_id, playlist_id, tracks_json):
|
||||||
|
|
||||||
download_batches[batch_id]['phase'] = 'downloading'
|
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:
|
for res in missing_tracks:
|
||||||
task_id = str(uuid.uuid4())
|
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] = {
|
download_tasks[task_id] = {
|
||||||
'status': 'pending', 'track_info': res['track'],
|
'status': 'pending', 'track_info': track_info,
|
||||||
'playlist_id': playlist_id, 'batch_id': batch_id,
|
'playlist_id': playlist_id, 'batch_id': batch_id,
|
||||||
'track_index': res['track_index'], 'retry_count': 0,
|
'track_index': res['track_index'], 'retry_count': 0,
|
||||||
'cached_candidates': [], 'used_sources': set(),
|
'cached_candidates': [], 'used_sources': set(),
|
||||||
|
|
@ -9314,9 +9329,48 @@ def _attempt_download_with_candidates(task_id, candidates, track, batch_id=None)
|
||||||
# Update task status to downloading
|
# Update task status to downloading
|
||||||
_update_task_status(task_id, 'downloading')
|
_update_task_status(task_id, 'downloading')
|
||||||
|
|
||||||
# Prepare download (using existing infrastructure)
|
# Prepare download - check if we have explicit album context from artist page
|
||||||
spotify_artist_context = {'id': 'from_sync_modal', 'name': track.artists[0] if track.artists else 'Unknown', 'genres': []}
|
track_info = None
|
||||||
spotify_album_context = {'id': 'from_sync_modal', 'name': track.album, 'release_date': '', 'image_url': 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__
|
download_payload = candidate.__dict__
|
||||||
|
|
||||||
username = download_payload.get('username')
|
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
|
enhanced_payload['track_number'] = 1
|
||||||
print(f"⚠️ [Context] No track.id available, using fallback 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
|
# Determine if this should be treated as album download
|
||||||
is_album_context = (
|
# First check if we have explicit album context from artist page
|
||||||
track.album and
|
if has_explicit_context:
|
||||||
track.album.strip() and
|
is_album_context = True
|
||||||
track.album != "Unknown Album" and
|
print(f"✅ [Context] Using explicit album context flag from artist page")
|
||||||
track.album.lower() != track.name.lower() # Album different from track
|
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:
|
else:
|
||||||
# Fallback to original data
|
# Fallback to original data
|
||||||
enhanced_payload['spotify_clean_title'] = enhanced_payload.get('title', '')
|
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')
|
playlist_name = data.get('playlist_name', 'Unknown Playlist')
|
||||||
force_download_all = data.get('force_download_all', False)
|
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:
|
if not tracks:
|
||||||
return jsonify({"success": False, "error": "No tracks provided"}), 400
|
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
|
# Limit concurrent analysis processes to prevent resource exhaustion
|
||||||
with tasks_lock:
|
with tasks_lock:
|
||||||
active_analysis_count = sum(1 for batch in download_batches.values()
|
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_total': len(tracks),
|
||||||
'analysis_processed': 0,
|
'analysis_processed': 0,
|
||||||
'analysis_results': [],
|
'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
|
# Link YouTube playlist to download process if this is a YouTube playlist
|
||||||
|
|
|
||||||
|
|
@ -4536,14 +4536,28 @@ async function startMissingTracksProcess(playlistId) {
|
||||||
forceToggleContainer.style.display = 'none';
|
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`, {
|
const response = await fetch(`/api/playlists/${playlistId}/start-missing-process`, {
|
||||||
method: 'POST',
|
method: 'POST',
|
||||||
headers: { 'Content-Type': 'application/json' },
|
headers: { 'Content-Type': 'application/json' },
|
||||||
body: JSON.stringify({
|
body: JSON.stringify(requestBody)
|
||||||
tracks: process.tracks,
|
|
||||||
playlist_name: process.playlist.name,
|
|
||||||
force_download_all: forceDownloadAll
|
|
||||||
})
|
|
||||||
});
|
});
|
||||||
|
|
||||||
const data = await response.json();
|
const data = await response.json();
|
||||||
|
|
@ -16621,14 +16635,22 @@ async function createArtistAlbumVirtualPlaylist(album, albumType) {
|
||||||
throw new Error('No tracks found for this album');
|
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
|
// 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
|
// Open download missing tracks modal with formatted tracks
|
||||||
// Pass false for showLoadingOverlay since we already have one from handleArtistAlbumClick
|
// 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
|
// Track this download for artist bubble management
|
||||||
registerArtistDownload(artist, album, virtualPlaylistId, albumType);
|
registerArtistDownload(artist, album, virtualPlaylistId, albumType);
|
||||||
|
|
|
||||||
|
|
@ -36,24 +36,26 @@ body {
|
||||||
.sidebar {
|
.sidebar {
|
||||||
width: 240px;
|
width: 240px;
|
||||||
|
|
||||||
/* Premium glassmorphic foundation */
|
/* Apple-style liquid glassmorphic foundation */
|
||||||
background: linear-gradient(135deg,
|
background: linear-gradient(135deg,
|
||||||
rgba(20, 20, 20, 0.95) 0%,
|
rgba(20, 20, 20, 0.65) 0%,
|
||||||
rgba(12, 12, 12, 0.98) 100%);
|
rgba(12, 12, 12, 0.75) 100%);
|
||||||
backdrop-filter: blur(20px) saturate(1.2);
|
backdrop-filter: blur(40px) saturate(1.8);
|
||||||
|
-webkit-backdrop-filter: blur(40px) saturate(1.8);
|
||||||
|
|
||||||
/* Enhanced borders */
|
/* Soft translucent borders */
|
||||||
border-right: 1px solid rgba(255, 255, 255, 0.12);
|
border-right: 1px solid rgba(255, 255, 255, 0.08);
|
||||||
border-top: 1px solid rgba(255, 255, 255, 0.18);
|
border-top: 1px solid rgba(255, 255, 255, 0.12);
|
||||||
border-top-right-radius: 20px;
|
border-top-right-radius: 24px;
|
||||||
border-bottom-right-radius: 20px;
|
border-bottom-right-radius: 24px;
|
||||||
|
|
||||||
/* Premium shadow effect */
|
/* Soft floating shadow with inner glow */
|
||||||
box-shadow:
|
box-shadow:
|
||||||
0 20px 60px rgba(0, 0, 0, 0.6),
|
0 8px 32px rgba(0, 0, 0, 0.3),
|
||||||
0 8px 32px rgba(0, 0, 0, 0.4),
|
0 4px 16px rgba(0, 0, 0, 0.2),
|
||||||
0 0 40px rgba(29, 185, 84, 0.1),
|
0 0 60px rgba(29, 185, 84, 0.06),
|
||||||
inset 0 1px 0 rgba(255, 255, 255, 0.15);
|
inset 0 1px 0 rgba(255, 255, 255, 0.1),
|
||||||
|
inset 0 -1px 0 rgba(0, 0, 0, 0.2);
|
||||||
|
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
|
|
@ -62,7 +64,7 @@ body {
|
||||||
|
|
||||||
/* Custom scrollbar styling */
|
/* Custom scrollbar styling */
|
||||||
scrollbar-width: thin;
|
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 */
|
/* Sidebar scrollbar webkit styling */
|
||||||
|
|
@ -157,44 +159,47 @@ body {
|
||||||
|
|
||||||
.nav-button:hover {
|
.nav-button:hover {
|
||||||
background: linear-gradient(135deg,
|
background: linear-gradient(135deg,
|
||||||
rgba(255, 255, 255, 0.08) 0%,
|
rgba(255, 255, 255, 0.06) 0%,
|
||||||
rgba(255, 255, 255, 0.04) 100%);
|
rgba(255, 255, 255, 0.03) 100%);
|
||||||
border: 1px solid rgba(255, 255, 255, 0.12);
|
backdrop-filter: blur(20px);
|
||||||
|
-webkit-backdrop-filter: blur(20px);
|
||||||
|
border: 1px solid rgba(255, 255, 255, 0.1);
|
||||||
transform: translateX(4px);
|
transform: translateX(4px);
|
||||||
box-shadow:
|
box-shadow:
|
||||||
0 4px 16px rgba(0, 0, 0, 0.3),
|
0 4px 12px rgba(0, 0, 0, 0.2),
|
||||||
0 2px 8px rgba(0, 0, 0, 0.2),
|
inset 0 1px 0 rgba(255, 255, 255, 0.08);
|
||||||
inset 0 1px 0 rgba(255, 255, 255, 0.1);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.nav-button.active {
|
.nav-button.active {
|
||||||
background: linear-gradient(135deg,
|
background: linear-gradient(135deg,
|
||||||
rgba(29, 185, 84, 0.20) 0%,
|
rgba(29, 185, 84, 0.16) 0%,
|
||||||
rgba(29, 185, 84, 0.15) 50%,
|
rgba(29, 185, 84, 0.12) 50%,
|
||||||
rgba(29, 185, 84, 0.10) 100%);
|
rgba(29, 185, 84, 0.08) 100%);
|
||||||
border: 1px solid rgba(29, 185, 84, 0.3);
|
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);
|
transform: translateX(6px);
|
||||||
box-shadow:
|
box-shadow:
|
||||||
0 6px 20px rgba(29, 185, 84, 0.4),
|
0 4px 16px rgba(29, 185, 84, 0.25),
|
||||||
0 4px 12px rgba(29, 185, 84, 0.3),
|
0 2px 8px rgba(0, 0, 0, 0.2),
|
||||||
0 2px 6px rgba(0, 0, 0, 0.3),
|
inset 0 1px 0 rgba(255, 255, 255, 0.12),
|
||||||
inset 0 1px 0 rgba(255, 255, 255, 0.15),
|
inset -1px 0 0 rgba(29, 185, 84, 0.6);
|
||||||
inset -1px 0 0 rgba(29, 185, 84, 0.8);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.nav-button.active:hover {
|
.nav-button.active:hover {
|
||||||
background: linear-gradient(135deg,
|
background: linear-gradient(135deg,
|
||||||
rgba(29, 185, 84, 0.28) 0%,
|
rgba(29, 185, 84, 0.22) 0%,
|
||||||
rgba(29, 185, 84, 0.22) 50%,
|
rgba(29, 185, 84, 0.18) 50%,
|
||||||
rgba(29, 185, 84, 0.15) 100%);
|
rgba(29, 185, 84, 0.12) 100%);
|
||||||
border: 1px solid rgba(29, 185, 84, 0.4);
|
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);
|
transform: translateX(8px);
|
||||||
box-shadow:
|
box-shadow:
|
||||||
0 8px 24px rgba(29, 185, 84, 0.5),
|
0 6px 20px rgba(29, 185, 84, 0.3),
|
||||||
0 6px 16px rgba(29, 185, 84, 0.4),
|
0 3px 10px rgba(0, 0, 0, 0.2),
|
||||||
0 3px 8px rgba(0, 0, 0, 0.3),
|
inset 0 1px 0 rgba(255, 255, 255, 0.15),
|
||||||
inset 0 1px 0 rgba(255, 255, 255, 0.2),
|
inset -1px 0 0 rgba(29, 185, 84, 0.7);
|
||||||
inset -1px 0 0 rgba(29, 185, 84, 0.9);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.nav-icon {
|
.nav-icon {
|
||||||
|
|
@ -205,29 +210,33 @@ body {
|
||||||
justify-content: center;
|
justify-content: center;
|
||||||
font-size: 16px;
|
font-size: 16px;
|
||||||
font-weight: 600;
|
font-weight: 600;
|
||||||
border-radius: 14px;
|
border-radius: 10px;
|
||||||
background: linear-gradient(135deg,
|
background: linear-gradient(135deg,
|
||||||
rgba(255, 255, 255, 0.10) 0%,
|
rgba(255, 255, 255, 0.08) 0%,
|
||||||
rgba(255, 255, 255, 0.06) 100%);
|
rgba(255, 255, 255, 0.04) 100%);
|
||||||
border: 1px solid rgba(255, 255, 255, 0.08);
|
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);
|
color: rgba(255, 255, 255, 0.8);
|
||||||
transition: all 0.3s cubic-bezier(0.4, 0, 0.2, 1);
|
transition: all 0.3s cubic-bezier(0.4, 0, 0.2, 1);
|
||||||
box-shadow:
|
box-shadow:
|
||||||
0 2px 6px rgba(0, 0, 0, 0.2),
|
0 2px 4px rgba(0, 0, 0, 0.15),
|
||||||
inset 0 1px 0 rgba(255, 255, 255, 0.1);
|
inset 0 1px 0 rgba(255, 255, 255, 0.08);
|
||||||
}
|
}
|
||||||
|
|
||||||
.nav-button.active .nav-icon {
|
.nav-button.active .nav-icon {
|
||||||
color: #1ed760;
|
color: #1ed760;
|
||||||
background: linear-gradient(135deg,
|
background: linear-gradient(135deg,
|
||||||
rgba(29, 185, 84, 0.30) 0%,
|
rgba(29, 185, 84, 0.25) 0%,
|
||||||
rgba(30, 215, 96, 0.25) 100%);
|
rgba(30, 215, 96, 0.20) 100%);
|
||||||
border: 1px solid rgba(29, 185, 84, 0.4);
|
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;
|
font-weight: 700;
|
||||||
box-shadow:
|
box-shadow:
|
||||||
0 4px 12px rgba(29, 185, 84, 0.4),
|
0 3px 8px rgba(29, 185, 84, 0.3),
|
||||||
0 2px 6px rgba(29, 185, 84, 0.3),
|
0 1px 4px rgba(29, 185, 84, 0.2),
|
||||||
inset 0 1px 0 rgba(255, 255, 255, 0.2);
|
inset 0 1px 0 rgba(255, 255, 255, 0.15);
|
||||||
}
|
}
|
||||||
|
|
||||||
.nav-text {
|
.nav-text {
|
||||||
|
|
@ -883,10 +892,11 @@ body {
|
||||||
.main-content {
|
.main-content {
|
||||||
flex: 1;
|
flex: 1;
|
||||||
background: linear-gradient(135deg,
|
background: linear-gradient(135deg,
|
||||||
rgba(12, 12, 12, 0.95) 0%,
|
rgba(12, 12, 12, 0.6) 0%,
|
||||||
rgba(16, 16, 16, 0.98) 50%,
|
rgba(16, 16, 16, 0.7) 50%,
|
||||||
rgba(8, 8, 8, 0.95) 100%);
|
rgba(8, 8, 8, 0.6) 100%);
|
||||||
backdrop-filter: blur(20px) saturate(1.2);
|
backdrop-filter: blur(40px) saturate(1.8);
|
||||||
|
-webkit-backdrop-filter: blur(40px) saturate(1.8);
|
||||||
overflow: auto;
|
overflow: auto;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -979,11 +989,26 @@ body {
|
||||||
}
|
}
|
||||||
|
|
||||||
.stat-card {
|
.stat-card {
|
||||||
background: linear-gradient(135deg, rgba(29, 185, 84, 0.1) 0%, rgba(255, 255, 255, 0.02) 100%);
|
background: linear-gradient(135deg, rgba(29, 185, 84, 0.08) 0%, rgba(255, 255, 255, 0.03) 100%);
|
||||||
border: 1px solid rgba(29, 185, 84, 0.2);
|
backdrop-filter: blur(20px) saturate(1.5);
|
||||||
border-radius: 12px;
|
-webkit-backdrop-filter: blur(20px) saturate(1.5);
|
||||||
|
border: 1px solid rgba(29, 185, 84, 0.15);
|
||||||
|
border-radius: 16px;
|
||||||
padding: 24px;
|
padding: 24px;
|
||||||
text-align: center;
|
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 {
|
.stat-value {
|
||||||
|
|
@ -3684,24 +3709,24 @@ body {
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
gap: 25px; /* Spacing between sections */
|
gap: 25px; /* Spacing between sections */
|
||||||
padding: 28px 24px 30px 24px; /* Match modal padding */
|
padding: 28px 24px 30px 24px;
|
||||||
|
|
||||||
/* Enhanced glassmorphic foundation matching modal */
|
/* Apple-style liquid glassmorphic foundation */
|
||||||
background: linear-gradient(135deg,
|
background: linear-gradient(135deg,
|
||||||
rgba(20, 20, 20, 0.85) 0%,
|
rgba(20, 20, 20, 0.55) 0%,
|
||||||
rgba(12, 12, 12, 0.92) 100%);
|
rgba(12, 12, 12, 0.65) 100%);
|
||||||
backdrop-filter: blur(20px) saturate(1.2);
|
backdrop-filter: blur(40px) saturate(1.8);
|
||||||
border-radius: 20px;
|
-webkit-backdrop-filter: blur(40px) saturate(1.8);
|
||||||
border: 1px solid rgba(255, 255, 255, 0.12);
|
border-radius: 24px;
|
||||||
border-top: 1px solid rgba(255, 255, 255, 0.18);
|
border: 1px solid rgba(255, 255, 255, 0.08);
|
||||||
|
border-top: 1px solid rgba(255, 255, 255, 0.12);
|
||||||
margin: 20px;
|
margin: 20px;
|
||||||
|
|
||||||
/* Premium shadow effect matching modal */
|
/* Soft floating shadow */
|
||||||
box-shadow:
|
box-shadow:
|
||||||
0 20px 60px rgba(0, 0, 0, 0.6),
|
0 8px 32px rgba(0, 0, 0, 0.3),
|
||||||
0 8px 32px rgba(0, 0, 0, 0.4),
|
0 4px 16px rgba(0, 0, 0, 0.2),
|
||||||
0 0 40px rgba(29, 185, 84, 0.1),
|
inset 0 1px 0 rgba(255, 255, 255, 0.08);
|
||||||
inset 0 1px 0 rgba(255, 255, 255, 0.15);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.dashboard-section {
|
.dashboard-section {
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue