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:
parent
ee6fd2f25e
commit
e21d5eab9d
1 changed files with 117 additions and 2 deletions
|
|
@ -2,6 +2,9 @@
|
||||||
import Avatar from './Avatar.svelte'
|
import Avatar from './Avatar.svelte'
|
||||||
import SegmentedController from './SegmentedController.svelte'
|
import SegmentedController from './SegmentedController.svelte'
|
||||||
import NavDropdown from './NavDropdown.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)
|
let scrollY = $state(0)
|
||||||
// Smooth gradient opacity from 0 to 1 over the first 100px of scroll
|
// Smooth gradient opacity from 0 to 1 over the first 100px of scroll
|
||||||
|
|
@ -9,6 +12,36 @@
|
||||||
// Padding transition happens more quickly
|
// Padding transition happens more quickly
|
||||||
let paddingProgress = $derived(Math.min(scrollY / 50, 1))
|
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(() => {
|
$effect(() => {
|
||||||
let ticking = false
|
let ticking = false
|
||||||
|
|
||||||
|
|
@ -30,6 +63,18 @@
|
||||||
window.addEventListener('scroll', handleScroll, { passive: true })
|
window.addEventListener('scroll', handleScroll, { passive: true })
|
||||||
return () => window.removeEventListener('scroll', handleScroll)
|
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>
|
</script>
|
||||||
|
|
||||||
<header
|
<header
|
||||||
|
|
@ -37,11 +82,34 @@
|
||||||
style="--gradient-opacity: {gradientOpacity}; --padding-progress: {paddingProgress}"
|
style="--gradient-opacity: {gradientOpacity}; --padding-progress: {paddingProgress}"
|
||||||
>
|
>
|
||||||
<div class="header-content">
|
<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 />
|
<Avatar />
|
||||||
</a>
|
</a>
|
||||||
<div class="nav-desktop">
|
<div class="nav-desktop">
|
||||||
<SegmentedController />
|
{#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>
|
||||||
<div class="nav-mobile">
|
<div class="nav-mobile">
|
||||||
<NavDropdown />
|
<NavDropdown />
|
||||||
|
|
@ -131,4 +199,51 @@
|
||||||
display: block;
|
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>
|
</style>
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue