feat(artist): redesign Popular Tracks section

- Remove solid background container
- Show 5 tracks by default with progressive loading (5 → 20 → 50)
- Add "Load More" link that disappears after 50 tracks
- Replace text Play All button with circular icon button
- Add context menu (...) with: Play Next, Play Later, Shuffle, Add to Playlist
- All actions apply to full track list (up to 50)
- Remove divider line before section
This commit is contained in:
vicrodh committed 2026-01-23 18:43:53 -06:00
1 parent 1185d307f5
commit 2f026fa181
1 file changed
+168 -31
+168 -31
View File
@@ -1,6 +1,6 @@
<script lang="ts">
import { invoke } from '@tauri-apps/api/core';
import { ArrowLeft, User, ChevronDown, ChevronUp, Play, Music, Heart, Search, X, ChevronLeft, ChevronRight, Radio } from 'lucide-svelte';
import { ArrowLeft, User, ChevronDown, ChevronUp, Play, Music, Heart, Search, X, ChevronLeft, ChevronRight, Radio, MoreHorizontal } from 'lucide-svelte';
import type { ArtistDetail, QobuzArtist } from '$lib/types';
import AlbumCard from '../AlbumCard.svelte';
import TrackMenu from '../TrackMenu.svelte';
@@ -144,6 +144,22 @@
let albumSortMode = $state<AlbumSortMode>('default');
let showAlbumSortMenu = $state(false);
// Popular tracks display state
let visibleTracksCount = $state(5);
let showTracksContextMenu = $state(false);
// Computed visible tracks
let visibleTracks = $derived(topTracks.slice(0, visibleTracksCount));
let canLoadMoreTracks = $derived(visibleTracksCount < 50 && topTracks.length > visibleTracksCount);
function loadMoreTracks() {
if (visibleTracksCount === 5) {
visibleTracksCount = 20;
} else if (visibleTracksCount === 20) {
visibleTracksCount = 50;
}
}
// Download status tracking
let albumDownloadStatuses = $state<Map<string, boolean>>(new Map());
@@ -515,6 +531,40 @@
}
}
function handlePlayAllTracksNext() {
if (!onTrackPlayNext) return;
// Add all tracks to play next (in reverse order so first track plays first)
for (let i = topTracks.length - 1; i >= 0; i--) {
onTrackPlayNext(topTracks[i]);
}
}
function handlePlayAllTracksLater() {
if (!onTrackPlayLater) return;
// Add all tracks to play later
for (const track of topTracks) {
onTrackPlayLater(track);
}
}
async function handleShuffleAllTracks() {
if (topTracks.length === 0 || !onTrackPlay) return;
// Shuffle and play from random position
const randomIndex = Math.floor(Math.random() * topTracks.length);
try {
await handleTrackPlay(topTracks[randomIndex], randomIndex);
} catch (err) {
console.error('Failed to shuffle tracks:', err);
}
}
function handleAddAllTracksToPlaylist() {
if (!onTrackAddToPlaylist || topTracks.length === 0) return;
// Add first track to playlist (this opens the playlist picker)
// The UI typically handles adding multiple tracks through a different flow
onTrackAddToPlaylist(topTracks[0].id);
}
function handleImageError() {
imageError = true;
}
@@ -950,24 +1000,44 @@
</div>
{/if}
<!-- Divider -->
<div class="divider"></div>
<!-- Top Tracks Section -->
{#if topTracks.length > 0 || tracksLoading}
<div class="top-tracks-section section-anchor" bind:this={topTracksSection}>
<div class="section-header">
<div class="section-header-left">
<h2 class="section-title">Popular Tracks</h2>
{#if topTracks.length > 0}
<span class="section-count">{topTracks.length}</span>
{/if}
</div>
{#if topTracks.length > 0}
<button class="play-all-btn" onclick={handlePlayAllTracks}>
<Play size={14} fill="white" color="white" />
<span>Play All</span>
</button>
<div class="section-header-actions">
<button class="action-btn-circle primary" onclick={handlePlayAllTracks} title="Play All">
<Play size={20} fill="white" color="white" />
</button>
<div class="context-menu-wrapper">
<button
class="action-btn-circle"
onclick={() => showTracksContextMenu = !showTracksContextMenu}
title="More options"
>
<MoreHorizontal size={20} />
</button>
{#if showTracksContextMenu}
<div class="context-menu">
<button class="context-menu-item" onclick={() => { handlePlayAllTracksNext(); showTracksContextMenu = false; }}>
Play Next
</button>
<button class="context-menu-item" onclick={() => { handlePlayAllTracksLater(); showTracksContextMenu = false; }}>
Play Later
</button>
<button class="context-menu-item" onclick={() => { handleShuffleAllTracks(); showTracksContextMenu = false; }}>
Shuffle
</button>
<button class="context-menu-item" onclick={() => { handleAddAllTracksToPlaylist(); showTracksContextMenu = false; }}>
Add to Playlist
</button>
</div>
{/if}
</div>
</div>
{/if}
</div>
@@ -975,7 +1045,7 @@
<div class="tracks-loading">Loading tracks...</div>
{:else}
<div class="tracks-list">
{#each topTracks as track, index}
{#each visibleTracks as track, index}
{@const isActiveTrack = isPlaybackActive && activeTrackId === track.id}
<div
class="track-row"
@@ -1058,10 +1128,13 @@
</div>
{/each}
</div>
{#if canLoadMoreTracks}
<button class="load-more-link" onclick={loadMoreTracks}>
Load More
</button>
{/if}
{/if}
</div>
<div class="divider"></div>
{/if}
<!-- Discography Section -->
@@ -2157,29 +2230,93 @@
/* Top Tracks */
.top-tracks-section {
padding: 24px;
background: var(--bg-secondary);
border-radius: 16px;
margin-bottom: 0;
margin-bottom: 32px;
}
.play-all-btn {
.section-header-actions {
display: flex;
align-items: center;
gap: 8px;
padding: 8px 16px;
background-color: var(--accent-primary);
border: none;
border-radius: 8px;
color: white;
font-size: 13px;
font-weight: 500;
cursor: pointer;
transition: background-color 150ms ease;
gap: 12px;
}
.play-all-btn:hover {
background-color: var(--accent-hover);
.action-btn-circle {
display: flex;
align-items: center;
justify-content: center;
width: 44px;
height: 44px;
border: none;
border-radius: 50%;
background: var(--bg-tertiary);
color: var(--text-muted);
cursor: pointer;
transition: background 150ms ease, color 150ms ease;
}
.action-btn-circle:hover {
background: var(--bg-hover);
color: var(--text-primary);
}
.action-btn-circle.primary {
background: var(--accent-primary);
color: white;
}
.action-btn-circle.primary:hover {
background: var(--accent-hover);
}
.context-menu-wrapper {
position: relative;
}
.context-menu {
position: absolute;
top: 100%;
right: 0;
margin-top: 8px;
min-width: 160px;
background: var(--bg-secondary);
border: 1px solid var(--bg-tertiary);
border-radius: 8px;
padding: 4px 0;
z-index: 100;
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.3);
}
.context-menu-item {
display: block;
width: 100%;
padding: 10px 16px;
background: none;
border: none;
text-align: left;
font-size: 13px;
color: var(--text-primary);
cursor: pointer;
transition: background 150ms ease;
}
.context-menu-item:hover {
background: var(--bg-tertiary);
}
.load-more-link {
display: block;
width: 100%;
padding: 16px;
background: none;
border: none;
text-align: center;
font-size: 13px;
color: var(--text-muted);
cursor: pointer;
transition: color 150ms ease;
}
.load-more-link:hover {
color: var(--text-primary);
}
.tracks-loading {