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>
|
||||||
@@ -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[] {
|
||||||
|
|||||||
@@ -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=""
|
||||||
|
/>
|
||||||
Reference in New Issue
Block a user