added music player component

This commit is contained in:
2026-04-18 22:44:50 +02:00
parent 46ba3965e9
commit 46cd759a5e
3 changed files with 256 additions and 2 deletions

229
src/lib/media/player.svelte Normal file
View 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)}&nbsp;//&nbsp;{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>

View File

@@ -8,7 +8,6 @@
function setFilter(tag: BlogPostTag) { function setFilter(tag: BlogPostTag) {
filter = tag; filter = tag;
console.log(filter);
} }
function filterPosts(): BlogPostLink[] { function filterPosts(): BlogPostLink[] {

View File

@@ -1,5 +1,31 @@
<script lang="ts">
import Player from "$lib/media/player.svelte";
</script>
**A New Beginning** is an EP I wrote back in 2018 in an effort to change up my production style. Originally, this EP was released as *A New Beginning (3-Track)*, featuring A New Beginning as *Nowy Początek* and *Farewell* in two different versions, one being an instrumental titled *Trzymajcie Się*, and the other one being a bootleg of Kelly Clarkson's *Behind These Hazel Eyes* called *Behind These Hazel Eyes (D4rkn355 'Farewell' Bootleg)*! For copyright reasons, the bootleg never made it onto streaming services. **A New Beginning** is an EP I wrote back in 2018 in an effort to change up my production style. Originally, this EP was released as *A New Beginning (3-Track)*, featuring A New Beginning as *Nowy Początek* and *Farewell* in two different versions, one being an instrumental titled *Trzymajcie Się*, and the other one being a bootleg of Kelly Clarkson's *Behind These Hazel Eyes* called *Behind These Hazel Eyes (D4rkn355 'Farewell' Bootleg)*! For copyright reasons, the bootleg never made it onto streaming services.
This EP, to me, represents the start of a deviation in tone, production quality, and musical style from my previous works. While it was only a start, I am quite proud of the works I produced. This EP, to me, represents the start of a deviation in tone, production quality, and musical style from my previous works. While it was only a start, I am quite proud of the works I produced.
The EP is available for download on [my copyparty instance](https://files.natconf.dev/public/my_tracks/A%20New%20Beginning/). The EP is available for download on [my copyparty instance](https://files.natconf.dev/public/my_tracks/A%20New%20Beginning/).
<Player
tracks={[
{
title: "A New Beginning",
link: "https://files.natconf.dev/public/my_tracks/A%20New%20Beginning/01%20F%C3%A6ls%20-%20A%20New%20Beginning.flac",
},
{
title: "Hope",
link: "https://files.natconf.dev/public/my_tracks/A%20New%20Beginning/02%20F%C3%A6ls%20-%20Hope.flac",
},
{
title: "Farewell",
link: "https://files.natconf.dev/public/my_tracks/A%20New%20Beginning/03%20F%C3%A6ls%20-%20Farewell.flac",
},
{
title: "Behind These Hazel Eyes (D4rkn355 'Farewell' Bootleg)",
link: "https://files.natconf.dev/public/my_tracks/A%20New%20Beginning/04%20Kelly%20Clarkson%20-%20Behind%20These%20Hazel%20Eyes%20%28D4rkn355%20%27Farewell%27%20Bootleg%29.flac",
},
]}
cover=""
/>