feat(search): add carousel for artists and view more cards

- Albums: 30 results with view more card if total > 30
- Artists: 12 results with carousel navigation
- Artists carousel: same pagination logic as albums
- View more cards: 160x220px dashed border, navigate to tab
- Artists gap: reduced to 16px (same as albums)
- Separate page tracking for albums and artists

cp
This commit is contained in:
vicrodh committed 2026-01-15 15:04:15 -06:00
1 parent c466a0817c
commit db49168623
2 files changed
+160 -37

No files matched your search

+2 -2
View File
@@ -64,9 +64,9 @@ pub async fn search_all(
let client = state.client.lock().await;
let (albums_result, tracks_result, artists_result) = tokio::join!(
client.search_albums(&query, 20, 0),
client.search_albums(&query, 30, 0),
client.search_tracks(&query, 8, 0),
client.search_artists(&query, 5, 0)
client.search_artists(&query, 12, 0)
);
Ok(SearchAllResults {
+158 -35
View File
@@ -8,21 +8,30 @@
import { t } from '$lib/i18n';
let searchInput: HTMLInputElement | null = null;
let albumsCarouselRef: HTMLDivElement | null = null;
let albumsCarouselContainer: HTMLDivElement | null = null;
let currentPage = $state(0);
let artistsCarouselContainer: HTMLDivElement | null = null;
let currentAlbumPage = $state(0);
let currentArtistPage = $state(0);
let albumsPerPage = $state(5);
let totalPages = $derived(allResults ? Math.ceil(allResults.albums.items.length / albumsPerPage) : 0);
let artistsPerPage = $state(5);
let totalAlbumPages = $derived(allResults ? Math.ceil(allResults.albums.items.length / albumsPerPage) : 0);
let totalArtistPages = $derived(allResults ? Math.ceil(allResults.artists.items.length / artistsPerPage) : 0);
onMount(async () => {
console.log('SearchView mounted!');
await tick();
searchInput?.focus();
calculateAlbumsPerPage();
window.addEventListener('resize', calculateAlbumsPerPage);
return () => window.removeEventListener('resize', calculateAlbumsPerPage);
calculateArtistsPerPage();
window.addEventListener('resize', handleResize);
return () => window.removeEventListener('resize', handleResize);
});
function handleResize() {
calculateAlbumsPerPage();
calculateArtistsPerPage();
}
function calculateAlbumsPerPage() {
if (!albumsCarouselContainer) return;
const containerWidth = albumsCarouselContainer.clientWidth;
@@ -30,7 +39,17 @@
const cardWidth = 160;
const cols = Math.floor((containerWidth + gap) / (cardWidth + gap));
albumsPerPage = Math.max(2, cols);
console.log(`Container width: ${containerWidth}px, Albums per page: ${albumsPerPage}`);
console.log(`Albums - Container width: ${containerWidth}px, Albums per page: ${albumsPerPage}`);
}
function calculateArtistsPerPage() {
if (!artistsCarouselContainer) return;
const containerWidth = artistsCarouselContainer.clientWidth;
const gap = 16;
const cardWidth = 160;
const cols = Math.floor((containerWidth + gap) / (cardWidth + gap));
artistsPerPage = Math.max(2, cols);
console.log(`Artists - Container width: ${containerWidth}px, Artists per page: ${artistsPerPage}`);
}
// Track which images have failed to load
@@ -175,8 +194,12 @@
$effect(() => {
if (allResults) {
currentPage = 0;
setTimeout(() => calculateAlbumsPerPage(), 100);
currentAlbumPage = 0;
currentArtistPage = 0;
setTimeout(() => {
calculateAlbumsPerPage();
calculateArtistsPerPage();
}, 100);
}
});
@@ -358,18 +381,35 @@
function scrollAlbumsCarousel(direction: 'left' | 'right') {
if (direction === 'left') {
currentPage = Math.max(0, currentPage - 1);
currentAlbumPage = Math.max(0, currentAlbumPage - 1);
} else {
currentPage = Math.min(totalPages - 1, currentPage + 1);
currentAlbumPage = Math.min(totalAlbumPages - 1, currentAlbumPage + 1);
}
}
let canScrollLeft = $derived(currentPage > 0);
let canScrollRight = $derived(currentPage < totalPages - 1);
function scrollArtistsCarousel(direction: 'left' | 'right') {
if (direction === 'left') {
currentArtistPage = Math.max(0, currentArtistPage - 1);
} else {
currentArtistPage = Math.min(totalArtistPages - 1, currentArtistPage + 1);
}
}
let canScrollAlbumsLeft = $derived(currentAlbumPage > 0);
let canScrollAlbumsRight = $derived(currentAlbumPage < totalAlbumPages - 1);
let canScrollArtistsLeft = $derived(currentArtistPage > 0);
let canScrollArtistsRight = $derived(currentArtistPage < totalArtistPages - 1);
let visibleAlbums = $derived(
allResults ? allResults.albums.items.slice(currentPage * albumsPerPage, (currentPage + 1) * albumsPerPage) : []
allResults ? allResults.albums.items.slice(currentAlbumPage * albumsPerPage, (currentAlbumPage + 1) * albumsPerPage) : []
);
let visibleArtists = $derived(
allResults ? allResults.artists.items.slice(currentArtistPage * artistsPerPage, (currentArtistPage + 1) * artistsPerPage) : []
);
let showAlbumsViewMore = $derived(allResults ? allResults.albums.total > 30 : false);
let showArtistsViewMore = $derived(allResults ? allResults.artists.total > 12 : false);
</script>
<div class="search-view">
@@ -513,26 +553,54 @@
<div class="artists-section">
<div class="section-header">
<h3>Artists</h3>
<button class="view-all-link" onclick={() => handleTabChange('artists')}>
View all ({allResults.artists.total})
</button>
</div>
<div class="artists-grid-compact">
{#each allResults.artists.items as artist}
<button class="artist-card" onclick={() => onArtistClick?.(artist.id)}>
{#if failedArtistImages.has(artist.id) || !getArtistImage(artist)}
<div class="artist-image-placeholder">
<User size={40} />
</div>
{:else}
<img src={getArtistImage(artist)} alt={artist.name} class="artist-image" onerror={() => handleArtistImageError(artist.id)} />
{/if}
<div class="artist-name">{artist.name}</div>
{#if artist.albums_count}
<div class="artist-albums">{$t('library.albumCount', { values: { count: artist.albums_count } })}</div>
{/if}
<div class="carousel-controls">
<button
class="carousel-btn"
onclick={() => scrollArtistsCarousel('left')}
disabled={!canScrollArtistsLeft}
aria-label="Previous artists"
>
<ChevronLeft size={20} />
</button>
{/each}
<button
class="carousel-btn"
onclick={() => scrollArtistsCarousel('right')}
disabled={!canScrollArtistsRight}
aria-label="Next artists"
>
<ChevronRight size={20} />
</button>
<button class="view-all-link" onclick={() => handleTabChange('artists')}>
View all ({allResults.artists.total})
</button>
</div>
</div>
<div class="artists-carousel-wrapper" bind:this={artistsCarouselContainer}>
<div class="artists-carousel">
{#each visibleArtists as artist}
<button class="artist-card" onclick={() => onArtistClick?.(artist.id)}>
{#if failedArtistImages.has(artist.id) || !getArtistImage(artist)}
<div class="artist-image-placeholder">
<User size={40} />
</div>
{:else}
<img src={getArtistImage(artist)} alt={artist.name} class="artist-image" onerror={() => handleArtistImageError(artist.id)} />
{/if}
<div class="artist-name">{artist.name}</div>
{#if artist.albums_count}
<div class="artist-albums">{$t('library.albumCount', { values: { count: artist.albums_count } })}</div>
{/if}
</button>
{/each}
{#if showArtistsViewMore}
<button class="view-more-card" onclick={() => handleTabChange('artists')}>
<div class="view-more-content">
<span>View more</span>
<ChevronRight size={24} />
</div>
</button>
{/if}
</div>
</div>
</div>
</div>
@@ -548,7 +616,7 @@
<button
class="carousel-btn"
onclick={() => scrollAlbumsCarousel('left')}
disabled={!canScrollLeft}
disabled={!canScrollAlbumsLeft}
aria-label="Previous albums"
>
<ChevronLeft size={20} />
@@ -556,7 +624,7 @@
<button
class="carousel-btn"
onclick={() => scrollAlbumsCarousel('right')}
disabled={!canScrollRight}
disabled={!canScrollAlbumsRight}
aria-label="Next albums"
>
<ChevronRight size={20} />
@@ -590,6 +658,16 @@
/>
</div>
{/each}
{#if showAlbumsViewMore}
<div class="album-card-wrapper">
<button class="view-more-card" onclick={() => handleTabChange('albums')}>
<div class="view-more-content">
<span>View more</span>
<ChevronRight size={24} />
</div>
</button>
</div>
{/if}
</div>
</div>
</div>
@@ -1293,12 +1371,22 @@
.artists-grid-compact {
display: grid;
grid-template-columns: repeat(auto-fill, minmax(160px, 1fr));
gap: 24px;
gap: 16px;
overflow-y: auto;
max-height: 400px;
padding-right: 8px;
}
.artists-carousel-wrapper {
position: relative;
overflow: hidden;
}
.artists-carousel {
display: flex;
gap: 16px;
}
.artists-grid-compact .artist-card {
width: 160px;
height: 220px;
@@ -1352,6 +1440,41 @@
.album-card-wrapper {
min-width: 160px;
flex-shrink: 0;
}
.view-more-card {
width: 160px;
min-width: 160px;
height: 220px;
display: flex;
align-items: center;
justify-content: center;
background-color: var(--bg-secondary);
border: 2px dashed var(--bg-tertiary);
border-radius: 12px;
cursor: pointer;
transition: all 150ms ease;
flex-shrink: 0;
}
.view-more-card:hover {
background-color: var(--bg-tertiary);
border-color: var(--accent-primary);
}
.view-more-content {
display: flex;
flex-direction: column;
align-items: center;
gap: 8px;
color: var(--text-muted);
font-size: 14px;
font-weight: 500;
}
.view-more-card:hover .view-more-content {
color: var(--accent-primary);
}
.tracks-section {