feat: add now playing display to header on avatar hover

- Show currently playing track info when hovering over avatar
- Display album artwork, artist name, and track/album name
- Add music note icons with pulse animation
- Maintain exact same container size as navigation
- Only shows when music is actively playing (avatar has headphones)

🤖 Generated with [Claude Code](https://claude.ai/code)

Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
Justin Edmund 2025-07-10 00:08:46 -07:00
parent ee6fd2f25e
commit e21d5eab9d

View file

@ -2,6 +2,9 @@
import Avatar from './Avatar.svelte'
import SegmentedController from './SegmentedController.svelte'
import NavDropdown from './NavDropdown.svelte'
import { albumStream } from '$lib/stores/album-stream'
import { nowPlayingStream } from '$lib/stores/now-playing-stream'
import type { Album } from '$lib/types/lastfm'
let scrollY = $state(0)
// Smooth gradient opacity from 0 to 1 over the first 100px of scroll
@ -9,6 +12,36 @@
// Padding transition happens more quickly
let paddingProgress = $derived(Math.min(scrollY / 50, 1))
// Now playing state
let isHoveringAvatar = $state(false)
let currentlyPlayingAlbum = $state<Album | null>(null)
let isPlayingMusic = $state(false)
// Subscribe to album updates
$effect(() => {
const unsubscribe = albumStream.subscribe((state) => {
const nowPlaying = state.albums.find((album) => album.isNowPlaying)
currentlyPlayingAlbum = nowPlaying || null
isPlayingMusic = !!nowPlaying
})
return unsubscribe
})
// Also check now playing stream for updates
$effect(() => {
const unsubscribe = nowPlayingStream.subscribe((state) => {
const hasNowPlaying = Array.from(state.updates.values()).some((update) => update.isNowPlaying)
if (!hasNowPlaying && currentlyPlayingAlbum) {
// Music stopped
currentlyPlayingAlbum = null
isPlayingMusic = false
}
})
return unsubscribe
})
$effect(() => {
let ticking = false
@ -30,6 +63,18 @@
window.addEventListener('scroll', handleScroll, { passive: true })
return () => window.removeEventListener('scroll', handleScroll)
})
// Get the best available album artwork
function getAlbumArtwork(album: Album): string {
if (album.appleMusicData?.highResArtwork) {
// Use smaller size for the header
return album.appleMusicData.highResArtwork.replace('3000x3000', '100x100')
}
if (album.images.itunes) {
return album.images.itunes.replace('3000x3000', '100x100')
}
return album.images.large || album.images.medium || ''
}
</script>
<header
@ -37,11 +82,34 @@
style="--gradient-opacity: {gradientOpacity}; --padding-progress: {paddingProgress}"
>
<div class="header-content">
<a href="/about" class="header-link" aria-label="@jedmund">
<a
href="/about"
class="header-link"
aria-label="@jedmund"
onmouseenter={() => isHoveringAvatar = true}
onmouseleave={() => isHoveringAvatar = false}
>
<Avatar />
</a>
<div class="nav-desktop">
{#if isHoveringAvatar && isPlayingMusic && currentlyPlayingAlbum}
<div class="now-playing">
<span class="music-note"></span>
{#if getAlbumArtwork(currentlyPlayingAlbum)}
<img
src={getAlbumArtwork(currentlyPlayingAlbum)}
alt="{currentlyPlayingAlbum.name} album cover"
class="album-thumbnail"
/>
{/if}
<span class="track-info">
{currentlyPlayingAlbum.artist.name} - {currentlyPlayingAlbum.nowPlayingTrack || currentlyPlayingAlbum.name}
</span>
<span class="music-note"></span>
</div>
{:else}
<SegmentedController />
{/if}
</div>
<div class="nav-mobile">
<NavDropdown />
@ -131,4 +199,51 @@
display: block;
}
}
.now-playing {
display: flex;
align-items: center;
gap: $unit-2x;
padding: $unit-2x $unit-3x;
background: rgba(255, 255, 255, 0.1);
backdrop-filter: blur(10px);
-webkit-backdrop-filter: blur(10px);
border-radius: $corner-radius-3xl;
border: 1px solid rgba(255, 255, 255, 0.2);
min-width: 300px;
height: 52px;
transition: all $transition-fast ease;
.music-note {
font-size: $font-size-md;
opacity: 0.8;
animation: pulse 2s ease-in-out infinite;
}
.album-thumbnail {
width: 28px;
height: 28px;
border-radius: $corner-radius-sm;
object-fit: cover;
}
.track-info {
flex: 1;
font-size: $font-size-sm;
font-weight: $font-weight-medium;
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
text-align: center;
}
}
@keyframes pulse {
0%, 100% {
opacity: 0.8;
}
50% {
opacity: 0.4;
}
}
</style>