added music player component
This commit is contained in:
229
src/lib/media/player.svelte
Normal file
229
src/lib/media/player.svelte
Normal file
@@ -0,0 +1,229 @@
|
||||
<script lang="ts">
|
||||
export interface Track {
|
||||
title: string;
|
||||
link: string;
|
||||
}
|
||||
|
||||
let {
|
||||
tracks,
|
||||
cover, // album cover (optional, currently unused)
|
||||
}: {
|
||||
tracks: Track[];
|
||||
cover?: string;
|
||||
} = $props();
|
||||
|
||||
let selectedTrackId: number = $state(0);
|
||||
let time: number = $state(0);
|
||||
let duration: number = $state(0);
|
||||
let paused: boolean = $state(true);
|
||||
|
||||
function playTrack(id: number) {
|
||||
paused = true;
|
||||
selectedTrackId = id;
|
||||
time = 0;
|
||||
paused = false;
|
||||
}
|
||||
|
||||
function format(time: number) {
|
||||
if (isNaN(time)) return '...';
|
||||
|
||||
const minutes = Math.floor(time / 60);
|
||||
const seconds = Math.floor(time % 60);
|
||||
|
||||
return `${minutes}:${seconds < 10 ? `0${seconds}` : seconds}`;
|
||||
}
|
||||
|
||||
function playNextTrack() {
|
||||
if (selectedTrackId < (tracks.length - 1)) {
|
||||
playTrack(selectedTrackId + 1)
|
||||
} else {
|
||||
selectedTrackId = 0;
|
||||
time = 0;
|
||||
paused = true;
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<div class="media-player">
|
||||
<div class="now-playing">
|
||||
<audio
|
||||
src={tracks[selectedTrackId].link}
|
||||
bind:currentTime={time}
|
||||
bind:duration
|
||||
bind:paused
|
||||
onended={() => {
|
||||
playNextTrack();
|
||||
}}
|
||||
></audio>
|
||||
|
||||
<button class="now-playing-button icon"
|
||||
aria-label={paused ? 'play' : 'pause'}
|
||||
onclick={() => paused = !paused}
|
||||
>
|
||||
{#if paused}
|
||||
<svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" stroke-width="1.5" stroke="currentColor" class="size-6">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" d="M21 12a9 9 0 1 1-18 0 9 9 0 0 1 18 0Z" />
|
||||
<path stroke-linecap="round" stroke-linejoin="round" d="M15.91 11.672a.375.375 0 0 1 0 .656l-5.603 3.113a.375.375 0 0 1-.557-.328V8.887c0-.286.307-.466.557-.327l5.603 3.112Z" />
|
||||
</svg>
|
||||
{:else}
|
||||
<svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" stroke-width="1.5" stroke="currentColor" class="size-6">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" d="M14.25 9v6m-4.5 0V9M21 12a9 9 0 1 1-18 0 9 9 0 0 1 18 0Z" />
|
||||
</svg>
|
||||
{/if}
|
||||
</button>
|
||||
|
||||
<div>
|
||||
<p class="now-playing-hint">now playing:</p>
|
||||
<p class="now-playing-title">{tracks[selectedTrackId].title}</p>
|
||||
</div>
|
||||
|
||||
<p class="now-playing-duration">
|
||||
{format(time)} // {duration ? format(duration) : '--:--'}
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div class="progress-container">
|
||||
<div class="slider"
|
||||
onpointerdown={(e: MouseEvent) => {
|
||||
const div = e.currentTarget as Element;
|
||||
|
||||
function seek(e: MouseEvent) {
|
||||
const { left, width } = div.getBoundingClientRect();
|
||||
|
||||
let p = (e.clientX - left) / width;
|
||||
if (p < 0) p = 0;
|
||||
if (p > 1) p = 1;
|
||||
|
||||
time = p * duration;
|
||||
}
|
||||
|
||||
seek(e);
|
||||
|
||||
window.addEventListener('pointermove', seek);
|
||||
|
||||
window.addEventListener('pointerup', () => {
|
||||
window.removeEventListener('pointermove', seek);
|
||||
}, {
|
||||
once: true
|
||||
});
|
||||
}}
|
||||
>
|
||||
<div class="progress-bar" style="--progress: {time / duration}%"></div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="track-list">
|
||||
{#each tracks as t, index}
|
||||
<div class="track-container">
|
||||
<button class="icon" aria-label="play" onclick={() => {
|
||||
playTrack(index);
|
||||
}}>
|
||||
<svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" stroke-width="1.5" stroke="currentColor" class="size-6">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" d="M21 12a9 9 0 1 1-18 0 9 9 0 0 1 18 0Z" />
|
||||
<path stroke-linecap="round" stroke-linejoin="round" d="M15.91 11.672a.375.375 0 0 1 0 .656l-5.603 3.113a.375.375 0 0 1-.557-.328V8.887c0-.286.307-.466.557-.327l5.603 3.112Z" />
|
||||
</svg>
|
||||
</button>
|
||||
|
||||
<p class="track-title">{t.title}</p>
|
||||
|
||||
<div class="spacer"></div>
|
||||
|
||||
<a class="icon" href={t.link} aria-label="download">
|
||||
<svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" stroke-width="1.5" stroke="currentColor" class="size-6">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" d="M3 16.5v2.25A2.25 2.25 0 0 0 5.25 21h13.5A2.25 2.25 0 0 0 21 18.75V16.5M16.5 12 12 16.5m0 0L7.5 12m4.5 4.5V3" />
|
||||
</svg>
|
||||
</a>
|
||||
</div>
|
||||
{/each}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<style>
|
||||
.icon {
|
||||
width: 20px;
|
||||
height: 20px;
|
||||
color: var(--color-text);
|
||||
transition: color var(--duration-animation) var(--anim-curve);
|
||||
}
|
||||
.icon:hover {
|
||||
cursor: pointer;
|
||||
color: var(--color-highlight-alt);
|
||||
}
|
||||
|
||||
.media-player {
|
||||
width: 100%;
|
||||
border-radius: var(--border-radius);
|
||||
background-color: var(--color-background-highlight-alt);
|
||||
padding: 8px 12px;
|
||||
}
|
||||
|
||||
.media-player p, .media-player a {
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
.now-playing {
|
||||
display: grid;
|
||||
grid-template-columns: min-content auto min-content;
|
||||
gap: 4px;
|
||||
}
|
||||
|
||||
.now-playing-button {
|
||||
align-self: center;
|
||||
width: 32px;
|
||||
height: 32px;
|
||||
}
|
||||
|
||||
.now-playing-hint, .now-playing-duration {
|
||||
font-size: 0.8rem;
|
||||
line-height: 1.0rem;
|
||||
}
|
||||
|
||||
.now-playing-title {
|
||||
font-weight: 700;
|
||||
}
|
||||
|
||||
.now-playing-duration {
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
align-items: end;
|
||||
font-family: var(--font-mono);
|
||||
font-weight: 500;
|
||||
}
|
||||
|
||||
.progress-container {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
margin: 4px 0;
|
||||
}
|
||||
|
||||
.slider {
|
||||
flex: 1;
|
||||
height: 6px;
|
||||
background: var(--color-background-highlight-alt);
|
||||
border-radius: 6px;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.progress-bar {
|
||||
width: calc(100 * var(--progress));
|
||||
height: 100%;
|
||||
background: var(--color-highlight-alt);
|
||||
transition: width var(--duration-animation) var(--anim-curve);
|
||||
}
|
||||
|
||||
.track-title {
|
||||
margin: 0 4px !important;
|
||||
line-height: 1.0rem;
|
||||
}
|
||||
|
||||
.track-container {
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
align-items: center;
|
||||
margin: 8px 0;
|
||||
}
|
||||
|
||||
.spacer {
|
||||
margin: 0 auto;
|
||||
}
|
||||
</style>
|
||||
Reference in New Issue
Block a user