feat: statistics upgrade, playlists redesign, artist cards, and UX improvements (v1.4.0)

- Statistics page: library stat cards, recently played, most played, highest rated, genre chart
- Playlists page: list layout with sort (Name/Tracks/Duration) and filter input
- Favorites songs: full tracklist layout with artist column, context menu, enqueue-all button
- AlbumDetail: extracted AlbumHeader and AlbumTrackList components
- Artist cards: square cover, same sizing as album cards (clamp 140-180px)
- Random Albums: removed renderKey remount, added loadingRef guard, fixed manual refresh race
- Context menu: "Go to Album" option for song and queue-item types
- Queue panel meta box: artist → artist page, album → album page, removed year
- Random Mix: hover persistence via .context-active class while context menu is open
- i18n: "Warteschlange" consistently used for queue in German

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
Psychotoxical
2026-03-16 17:36:58 +01:00
parent f666f84479
commit d3ffa30bf5
18 changed files with 1073 additions and 551 deletions
+239 -23
View File
@@ -185,17 +185,12 @@
.artist-card {
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
padding: var(--space-4);
background: var(--bg-card);
border-radius: var(--radius-lg);
border: 1px solid var(--border-subtle);
cursor: pointer;
transition: all var(--transition-base);
text-align: center;
gap: var(--space-3);
height: 100%; /* fill grid row */
overflow: hidden;
transition: transform var(--transition-base), box-shadow var(--transition-base), border-color var(--transition-base);
}
.artist-card:hover {
transform: translateY(-3px);
@@ -203,38 +198,49 @@
border-color: var(--border);
}
.artist-card-avatar {
width: 100px;
height: 100px;
border-radius: 50%;
position: relative;
aspect-ratio: 1;
overflow: hidden;
background: var(--bg-hover);
display: flex;
align-items: center;
justify-content: center;
color: var(--text-muted);
box-shadow: var(--shadow-sm);
}
.artist-card-avatar img {
width: 100%;
height: 100%;
object-fit: cover;
transition: transform var(--transition-slow);
}
.artist-card:hover .artist-card-avatar img { transform: scale(1.04); }
.artist-card-avatar-initial {
background: var(--bg-card);
border: 2px solid;
box-shadow: none;
aspect-ratio: 1;
display: flex;
align-items: center;
justify-content: center;
overflow: hidden;
}
.artist-card-avatar-initial span {
font-size: 2.5rem;
font-size: 3rem;
font-weight: 800;
font-family: var(--font-display);
line-height: 1;
user-select: none;
}
.artist-card-info {
padding: var(--space-3) var(--space-3) var(--space-2);
display: flex;
flex-direction: column;
gap: 2px;
}
.artist-card-name {
font-weight: 600;
font-size: 14px;
font-size: 13px;
color: var(--text-primary);
display: -webkit-box;
-webkit-line-clamp: 2;
line-clamp: 2;
-webkit-box-orient: vertical;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
}
.artist-card-meta {
font-size: 12px;
@@ -324,7 +330,8 @@
.album-grid-wrap { grid-template-columns: repeat(auto-fill, minmax(160px, 1fr)); }
}
.album-grid .album-card {
.album-grid .album-card,
.album-grid .artist-card {
flex: 0 0 clamp(140px, 15vw, 180px);
scroll-snap-align: start;
}
@@ -519,6 +526,14 @@
}
.album-detail-info { display: flex; flex-wrap: wrap; gap: var(--space-2); color: var(--text-muted); font-size: 13px; margin: var(--space-2) 0 var(--space-4); }
.album-detail-actions { display: flex; flex-wrap: wrap; gap: var(--space-2); align-items: center; row-gap: var(--space-2); }
.album-detail-actions-primary { display: flex; gap: 8px; flex-wrap: wrap; }
.album-detail-content { position: relative; z-index: 1; }
.album-detail-badge { margin-bottom: 0.5rem; }
.album-detail-back { margin-bottom: 1rem; gap: 6px; }
.album-info-dot { margin: 0 4px; }
.album-related { padding: 0 var(--space-6) var(--space-8); }
.album-related-divider { border-top: 1px solid var(--border-subtle); margin-bottom: 2rem; }
.album-related-title { margin-bottom: 1rem; }
.download-hint {
display: flex;
@@ -568,6 +583,8 @@
/* ─ Tracklist ─ */
.tracklist { padding: 0 var(--space-6) var(--space-6); }
.col-center { text-align: center; }
.tracklist-header {
display: grid;
grid-template-columns: 36px minmax(100px, 3fr) 70px 80px 60px 120px;
@@ -630,7 +647,8 @@
}
.tracklist-total.tracklist-va .tracklist-total-label { grid-column: 1 / 6; }
.tracklist-total.tracklist-va .tracklist-total-value { grid-column: 6 / 7; }
.track-row:hover { background: var(--bg-hover); }
.track-row:hover,
.track-row.context-active { background: var(--bg-hover); }
.track-row.active { background: var(--accent-dim); animation: track-pulse 3s ease-in-out infinite; }
.track-row.active:hover { background: var(--accent-dim); animation: none; }
@keyframes track-pulse {
@@ -706,7 +724,11 @@
text-overflow: ellipsis;
}
.track-size { color: var(--ctp-overlay0); }
.track-duration { font-size: 12px; color: var(--text-muted); font-variant-numeric: tabular-nums; }
.track-duration { font-size: 12px; color: var(--text-muted); font-variant-numeric: tabular-nums; text-align: center; }
.track-meta { display: flex; align-items: center; }
.track-meta .track-codec { margin-top: 0; }
.track-star-cell { display: flex; justify-content: center; }
.track-star-btn { padding: 4px; height: auto; min-height: unset; }
/* ─ Modal ─ */
.modal-overlay {
@@ -736,6 +758,7 @@
}
.modal-close { position: absolute; top: var(--space-4); right: var(--space-4); color: var(--text-muted); }
.modal-close:hover { color: var(--text-primary); }
.modal-title { margin-bottom: 1rem; font-family: var(--font-display); }
.artist-bio { font-size: 14px; line-height: 1.7; color: var(--text-secondary); }
.artist-bio a { color: var(--accent); text-decoration: underline; }
@@ -1467,3 +1490,196 @@
bottom: auto;
}
/* ─ Playlists Page ─ */
.playlist-page-header {
display: flex;
align-items: center;
justify-content: space-between;
gap: 1rem;
margin-bottom: 1.5rem;
}
.playlist-page-header .page-title { margin-bottom: 0; }
.playlist-filter-input {
background: var(--bg-surface);
border: 1px solid var(--border);
border-radius: var(--radius-md);
color: var(--text-primary);
font-size: 13px;
padding: 6px 12px;
width: 220px;
outline: none;
transition: border-color 0.15s;
}
.playlist-filter-input:focus { border-color: var(--accent); }
.playlist-filter-input::placeholder { color: var(--text-muted); }
.playlist-list { display: flex; flex-direction: column; }
.playlist-list-header {
display: grid;
grid-template-columns: 36px 1fr 90px 90px 44px;
gap: var(--space-3);
padding: 6px var(--space-3);
border-bottom: 1px solid var(--border-subtle);
margin-bottom: 4px;
}
.playlist-sort-btn {
display: flex;
align-items: center;
gap: 4px;
font-size: 11px;
font-weight: 600;
letter-spacing: 0.06em;
text-transform: uppercase;
color: var(--text-muted);
background: none;
border: none;
cursor: pointer;
padding: 0;
transition: color 0.15s;
}
.playlist-sort-btn:hover,
.playlist-sort-btn.active { color: var(--accent); }
.playlist-row {
display: grid;
grid-template-columns: 36px 1fr 90px 90px 44px;
gap: var(--space-3);
align-items: center;
padding: 6px var(--space-3);
border-radius: var(--radius-md);
transition: background 0.15s;
}
.playlist-row:hover { background: var(--bg-hover); }
.playlist-play-icon {
display: flex;
align-items: center;
justify-content: center;
width: 28px;
height: 28px;
border-radius: 50%;
background: var(--accent);
color: var(--ctp-base);
border: none;
cursor: pointer;
opacity: 0;
transition: opacity 0.15s, transform 0.15s;
flex-shrink: 0;
}
.playlist-row:hover .playlist-play-icon { opacity: 1; }
.playlist-play-icon:hover { transform: scale(1.1); }
.playlist-name {
font-size: 14px;
font-weight: 500;
color: var(--text-primary);
}
.playlist-meta {
font-size: 12px;
color: var(--text-muted);
font-variant-numeric: tabular-nums;
}
.playlist-delete-btn {
opacity: 0;
color: var(--ctp-red) !important;
padding: 4px;
height: auto;
min-height: unset;
transition: opacity 0.15s;
justify-self: center;
}
.playlist-row:hover .playlist-delete-btn { opacity: 1; }
/* ─ Statistics Page ─ */
.stats-page {
display: flex;
flex-direction: column;
gap: 3rem;
}
.stats-overview {
display: grid;
grid-template-columns: repeat(4, 1fr);
gap: var(--space-4);
}
@media (max-width: 600px) {
.stats-overview { grid-template-columns: repeat(2, 1fr); }
}
.stats-card {
background: var(--bg-card);
border: 1px solid var(--border-subtle);
border-radius: var(--radius-lg);
padding: var(--space-5) var(--space-4);
display: flex;
flex-direction: column;
gap: 4px;
align-items: center;
text-align: center;
}
.stats-card-value {
font-family: var(--font-display);
font-size: 2rem;
font-weight: 800;
color: var(--accent);
line-height: 1;
}
.stats-card-label {
font-size: 12px;
font-weight: 600;
letter-spacing: 0.06em;
text-transform: uppercase;
color: var(--text-muted);
}
/* Genre chart */
.genre-chart {
background: var(--bg-card);
border: 1px solid var(--border-subtle);
border-radius: var(--radius-lg);
padding: var(--space-5);
display: flex;
flex-direction: column;
gap: var(--space-4);
}
.genre-row { display: flex; flex-direction: column; gap: 6px; }
.genre-row-header {
display: flex;
justify-content: space-between;
align-items: baseline;
gap: var(--space-3);
}
.genre-name {
font-size: 13px;
font-weight: 500;
color: var(--text-primary);
min-width: 0;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
}
.genre-counts {
font-size: 12px;
color: var(--text-muted);
white-space: nowrap;
flex-shrink: 0;
}
.genre-bar-track {
width: 100%;
height: 6px;
background: var(--bg-hover);
border-radius: 3px;
overflow: hidden;
}
.genre-bar-fill {
height: 100%;
background: var(--accent);
border-radius: 3px;
transition: width 0.8s ease-out;
}