Add search re-focus, quality badges in artist view, fix favorites quality label

1. Search: Re-clicking Search button in sidebar scrolls to top and focuses input
   - Added triggerSearchFocus mechanism in searchState store
   - SearchView subscribes to focus trigger for scroll + focus

2. ArtistDetailView: Added quality badge and favorite button to Popular Tracks
   - Added getQualityLabel function with bit depth/sample rate
   - Added quality-badge element with hires styling
   - Added favorite button in track actions

3. FavoritesView: Fixed getAlbumQualityLabel to show detailed format
   - Now shows "24bit/96kHz" instead of generic "Hi-Res"
This commit is contained in:
vicrodh committed 2026-01-28 18:45:21 -06:00
1 parent 5b380ba7da
commit 0354917221
5 files changed
+97 -3

No files matched your search

@@ -643,6 +643,13 @@
return `${mins}:${secs.toString().padStart(2, '0')}`;
}
function getQualityLabel(track: Track): string {
if (track.hires_streamable && track.maximum_bit_depth && track.maximum_sampling_rate) {
return `${track.maximum_bit_depth}bit/${track.maximum_sampling_rate}kHz`;
}
return 'CD Quality';
}
function handlePausePlayback(event: MouseEvent) {
event.stopPropagation();
void togglePlay();
@@ -1418,8 +1425,25 @@
<div class="track-album">{track.album?.title || ''}</div>
{/if}
</div>
<div class="track-quality">
<span class="quality-badge" class:hires={track.hires_streamable}>
{getQualityLabel(track)}
</span>
</div>
<div class="track-duration">{formatDuration(track.duration)}</div>
<div class="track-actions">
{#if onTrackAddFavorite}
<button
class="favorite-btn"
onclick={(e) => {
e.stopPropagation();
onTrackAddFavorite(track.id);
}}
title="Add to favorites"
>
<Heart size={16} />
</button>
{/if}
<TrackMenu
onPlayNow={() => handleTrackPlay(track, index)}
onPlayNext={onTrackPlayNext ? () => onTrackPlayNext(track) : undefined}
@@ -3406,6 +3430,25 @@
text-underline-offset: 2px;
}
.track-quality {
display: flex;
align-items: center;
}
.track-quality .quality-badge {
font-size: 11px;
padding: 3px 8px;
border-radius: 4px;
background-color: var(--bg-tertiary);
color: var(--text-muted);
white-space: nowrap;
}
.track-quality .quality-badge.hires {
background-color: rgba(168, 85, 247, 0.15);
color: #a855f7;
}
.track-duration {
font-size: 13px;
color: var(--text-muted);
@@ -3415,6 +3458,7 @@
.track-actions {
display: flex;
align-items: center;
gap: 4px;
margin-left: 8px;
opacity: 0.7;
transition: opacity 150ms ease;
@@ -3424,6 +3468,25 @@
opacity: 1;
}
.favorite-btn {
display: flex;
align-items: center;
justify-content: center;
width: 28px;
height: 28px;
border: none;
border-radius: 4px;
background: transparent;
color: var(--text-muted);
cursor: pointer;
transition: all 150ms ease;
}
.favorite-btn:hover {
background-color: var(--bg-tertiary);
color: var(--accent-primary);
}
/* Responsive */
@media (max-width: 768px) {
.artist-header {
@@ -608,6 +608,9 @@
}
function getAlbumQualityLabel(album: FavoriteAlbum): string {
if (album.hires && album.maximum_bit_depth && album.maximum_sampling_rate) {
return `${album.maximum_bit_depth}bit/${album.maximum_sampling_rate}kHz`;
}
return album.hires ? 'Hi-Res' : 'CD Quality';
}
+11 -2
View File
@@ -4,7 +4,7 @@
import { Search, Disc3, Music, Mic2, User, X, ChevronLeft, ChevronRight, Crown } from 'lucide-svelte';
import AlbumCard from '../AlbumCard.svelte';
import TrackMenu from '../TrackMenu.svelte';
import { getSearchState, setSearchState, type SearchResults, type SearchAllResults, type SearchTab, type SearchFilterType } from '$lib/stores/searchState';
import { getSearchState, setSearchState, subscribeSearchFocus, type SearchResults, type SearchAllResults, type SearchTab, type SearchFilterType } from '$lib/stores/searchState';
import { setPlaybackContext } from '$lib/stores/playbackContextStore';
import { togglePlay } from '$lib/stores/playerStore';
import { t } from '$lib/i18n';
@@ -34,7 +34,16 @@
performSearch();
}
return () => window.removeEventListener('resize', handleResize);
// Subscribe to focus trigger (when user re-clicks Search in sidebar)
const unsubscribeFocus = subscribeSearchFocus(() => {
scrollContainer?.scrollTo({ top: 0, behavior: 'smooth' });
searchInput?.focus();
});
return () => {
window.removeEventListener('resize', handleResize);
unsubscribeFocus();
};
});
function handleScroll(event: Event) {
+14
View File
@@ -43,3 +43,17 @@ export function getSearchState<Album, Track, Artist>(): SearchState<Album, Track
export function setSearchState<Album, Track, Artist>(next: SearchState<Album, Track, Artist>): void {
searchState = next as SearchState<unknown, unknown, unknown>;
}
// Signal to trigger scroll-to-top and focus on search input
let focusTrigger = 0;
const focusListeners: Set<() => void> = new Set();
export function triggerSearchFocus(): void {
focusTrigger++;
focusListeners.forEach(fn => fn());
}
export function subscribeSearchFocus(callback: () => void): () => void {
focusListeners.add(callback);
return () => focusListeners.delete(callback);
}
+6 -1
View File
@@ -27,7 +27,7 @@
} from '$lib/stores/toastStore';
// Search state for performer search
import { setSearchState } from '$lib/stores/searchState';
import { setSearchState, triggerSearchFocus } from '$lib/stores/searchState';
// Playback context and preferences
import {
@@ -544,6 +544,11 @@
navigateToFavorites(favoritesDefaultTab);
return;
}
// If already on search, trigger scroll to top and focus
if (view === 'search' && activeView === 'search') {
triggerSearchFocus();
return;
}
navTo(view as ViewType);
}