jedmund-svelte/src/lib/components/Album.svelte
Justin Edmund 639a4a2429 fix: update components to use Svelte 5 snippets and fix editor content loading
- Convert Button component to use snippets instead of slots
- Update BaseDropdown and StatusDropdown to use new Button snippet syntax
- Add effect to watch for data changes in ComposerCore and update editor content
- Fix SVG component usage in Album component for Svelte 5 compatibility

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

Co-Authored-By: Claude <noreply@anthropic.com>
2025-06-26 09:22:19 -04:00

306 lines
6.7 KiB
Svelte

<script lang="ts">
import { Spring } from 'svelte/motion'
import type { Album } from '$lib/types/lastfm'
import { audioPreview } from '$lib/stores/audio-preview'
import { nowPlayingStream } from '$lib/stores/now-playing-stream'
import NowPlaying from './NowPlaying.svelte'
import PlayIcon from '$icons/play.svg'
import PauseIcon from '$icons/pause.svg'
interface AlbumProps {
album?: Album
albumId?: string
hoveredAlbumId?: string | null
onHover?: (albumId: string | null) => void
}
let { album = undefined, albumId = '', hoveredAlbumId = null, onHover }: AlbumProps = $props()
let isHovering = $state(false)
let audio: HTMLAudioElement | null = $state(null)
// Create a unique ID for this album if not provided
const currentAlbumId = $derived(albumId || (album ? `${album.artist.name}-${album.name}` : ''))
// Subscribe to the store to know if this album is playing
let isPlaying = $state(false)
$effect(() => {
const unsubscribe = audioPreview.subscribe((state) => {
isPlaying = state.currentAlbumId === currentAlbumId && state.isPlaying
})
return unsubscribe
})
const scale = new Spring(1, {
stiffness: 0.2,
damping: 0.12
})
// Determine if this album should shrink
const shouldShrink = $derived(hoveredAlbumId !== null && hoveredAlbumId !== currentAlbumId)
$effect(() => {
if (isHovering) {
scale.target = 1.1
} else if (shouldShrink) {
scale.target = 0.95
} else {
scale.target = 1
}
})
async function togglePreview(e: Event) {
e.preventDefault()
e.stopPropagation()
if (!audio && album?.appleMusicData?.previewUrl) {
audio = new Audio(album.appleMusicData.previewUrl)
audio.addEventListener('ended', () => {
audioPreview.stop()
})
}
if (audio) {
if (isPlaying) {
audioPreview.stop()
} else {
// Update the store first, then play
audioPreview.play(audio, currentAlbumId)
try {
await audio.play()
} catch (error) {
console.error('Failed to play preview:', error)
audioPreview.stop()
}
}
}
}
$effect(() => {
// Cleanup audio when component unmounts
return () => {
if (audio && isPlaying) {
audioPreview.stop()
}
}
})
// Use high-res artwork if available
const artworkUrl = $derived(
album?.appleMusicData?.highResArtwork ||
album?.images?.itunes ||
album?.images?.mega ||
album?.images?.extralarge ||
album?.images?.large ||
album?.images?.medium ||
album?.images?.small ||
''
)
const hasPreview = $derived(!!album?.appleMusicData?.previewUrl)
// Subscribe to real-time now playing updates
let realtimeNowPlaying = $state<{ isNowPlaying: boolean; nowPlayingTrack?: string } | null>(null)
$effect(() => {
if (album) {
const unsubscribe = nowPlayingStream.isAlbumPlaying.subscribe((checkAlbum) => {
const status = checkAlbum(album.artist.name, album.name)
if (status !== null) {
realtimeNowPlaying = status
}
})
return unsubscribe
}
})
// Combine initial state with real-time updates
const isNowPlaying = $derived(realtimeNowPlaying?.isNowPlaying ?? album?.isNowPlaying ?? false)
const nowPlayingTrack = $derived(realtimeNowPlaying?.nowPlayingTrack ?? album?.nowPlayingTrack)
// Debug logging
$effect(() => {
if (album && isNowPlaying) {
console.log(`Album "${album.name}" is now playing:`, {
fromRealtime: realtimeNowPlaying?.isNowPlaying,
fromAlbum: album?.isNowPlaying,
track: nowPlayingTrack
})
}
})
</script>
<div class="album">
{#if album}
<div class="album-wrapper">
<a
href={album.url}
target="_blank"
rel="noopener noreferrer"
onmouseenter={() => {
isHovering = true
onHover?.(currentAlbumId)
}}
onmouseleave={() => {
isHovering = false
onHover?.(null)
}}
>
<div class="artwork-container">
<img
src={artworkUrl}
alt={album.name}
style="transform: scale({scale.current})"
loading="lazy"
/>
{#if isNowPlaying}
<NowPlaying trackName={nowPlayingTrack} />
{/if}
{#if hasPreview && (isHovering || isPlaying)}
<button
class="preview-button"
class:corner={isPlaying && !isHovering}
onclick={togglePreview}
aria-label={isPlaying ? 'Pause preview' : 'Play preview'}
class:playing={isPlaying}
>
{#if isPlaying}
<svelte:component this={PauseIcon} />
{:else}
<svelte:component this={PlayIcon} />
{/if}
</button>
{/if}
</div>
<div class="info">
<span class="album-name">
{album.name}
</span>
<p class="artist-name">
{album.artist.name}
</p>
</div>
</a>
</div>
{:else}
<p>No album provided</p>
{/if}
</div>
<style lang="scss">
.album {
width: 100%;
height: 100%;
.album-wrapper {
display: flex;
flex-direction: column;
gap: $unit;
width: 100%;
height: 100%;
}
a {
display: flex;
flex-direction: column;
gap: $unit * 1.5;
text-decoration: none;
transition: gap $transition-fast ease-in-out;
width: 100%;
height: 100%;
.artwork-container {
position: relative;
width: 100%;
aspect-ratio: 1 / 1;
background-color: $gray-5;
border-radius: $unit;
}
img {
border: 1px solid $shadow-light;
border-radius: $unit;
box-shadow: 0 0 8px $shadow-light;
width: 100%;
height: 100%;
object-fit: cover;
display: block;
}
.preview-button {
position: absolute;
top: 50%;
left: 50%;
transform: translate(-50%, -50%);
width: 60px;
height: 60px;
background: rgba(0, 0, 0, 0.4);
color: white;
border: none;
border-radius: 50%;
cursor: pointer;
display: flex;
align-items: center;
justify-content: center;
font-size: 24px;
transition: all $transition-medium ease;
backdrop-filter: blur(10px);
&.corner {
top: auto;
left: $unit * 1.5;
bottom: $unit-2x;
transform: none;
width: 40px;
height: 40px;
font-size: 18px;
}
&:hover {
background: $overlay-medium;
transform: translate(-50%, -50%) scale(1.1);
&.corner {
transform: scale(1.1);
}
}
&.playing {
background: $accent-color;
}
}
.info {
display: flex;
flex-direction: column;
gap: $unit-fourth;
p {
padding: 0;
margin: 0;
}
.album-name {
font-size: $font-size;
font-weight: $font-weight-med;
color: $accent-color;
overflow: hidden;
text-overflow: ellipsis;
display: -webkit-box;
-webkit-line-clamp: 2;
-webkit-box-orient: vertical;
}
.artist-name {
font-size: $font-size-extra-small;
font-weight: $font-weight-med;
color: $gray-40;
}
}
}
.preview-container {
margin-top: $unit;
}
}
</style>