feat: add basic queue management, add volume slider
This commit is contained in:
parent
14cdb06e15
commit
f2736c187c
@ -1,15 +1,26 @@
|
|||||||
<script>
|
<script lang="ts">
|
||||||
import Link from './a.svelte';
|
import Link from './a.svelte';
|
||||||
import IconMusic from '~icons/mdi/music';
|
import IconTrack from '~icons/mdi/music-box';
|
||||||
import IconSettings from '~icons/mdi/settings';
|
import IconSettings from '~icons/mdi/settings';
|
||||||
import IconArtist from '~icons/mdi/artist';
|
import IconArtist from '~icons/mdi/artist';
|
||||||
import IconAlbum from '~icons/mdi/album';
|
import IconAlbum from '~icons/mdi/album';
|
||||||
import IconPlaylist from '~icons/mdi/playlist-music';
|
import IconPlaylist from '~icons/mdi/playlist-music';
|
||||||
|
import IconQueue from '~icons/mdi/format-list-bulleted';
|
||||||
|
|
||||||
|
let links: { id: string; href?: string; icon: any }[] = [
|
||||||
|
{ id: 'queue', icon: IconQueue },
|
||||||
|
{ id: 'tracks', href: '/', icon: IconTrack },
|
||||||
|
{ id: 'albums', icon: IconAlbum },
|
||||||
|
{ id: 'artists', icon: IconArtist },
|
||||||
|
{ id: 'playlists', icon: IconPlaylist },
|
||||||
|
{ id: 'settings', icon: IconSettings }
|
||||||
|
];
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<nav class="flex">
|
<nav class="flex">
|
||||||
<Link title="tracks" href="/"><IconMusic class="w-7 h-7" /></Link>
|
{#each links as link}
|
||||||
<Link title="albums" href="/albums"><IconAlbum class="w-7 h-7" /></Link>
|
<Link title={link.id} href={link.href ?? `/${link.id}`}>
|
||||||
<Link title="artists" href="/artists"><IconArtist class="w-7 h-7" /></Link>
|
<svelte:component this={link.icon} class="w-7 h-7" />
|
||||||
<Link title="settings" href="/settings"><IconSettings class="w-7 h-7" /></Link>
|
</Link>
|
||||||
|
{/each}
|
||||||
</nav>
|
</nav>
|
||||||
|
@ -1,16 +1,29 @@
|
|||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
import { address, makeAudioUrl, makeThumbnailUrl, playingNow, token } from '../stores';
|
import {
|
||||||
|
makeAudioUrl,
|
||||||
|
makeThumbnailUrl,
|
||||||
|
currentTrack,
|
||||||
|
queuePosition,
|
||||||
|
queue,
|
||||||
|
volume
|
||||||
|
} from '../stores';
|
||||||
import IconPlay from '~icons/mdi/play';
|
import IconPlay from '~icons/mdi/play';
|
||||||
import IconPause from '~icons/mdi/pause';
|
import IconPause from '~icons/mdi/pause';
|
||||||
|
import IconMusic from '~icons/mdi/music';
|
||||||
import { RangeSlider } from '@skeletonlabs/skeleton';
|
import { RangeSlider } from '@skeletonlabs/skeleton';
|
||||||
|
import type { ResourceId } from '../types';
|
||||||
|
|
||||||
$: thumbUrl = $playingNow
|
$: track = $currentTrack?.track;
|
||||||
? makeThumbnailUrl($address, $token, $playingNow.track.thumbnail_id)
|
$: track_id = $currentTrack?.id;
|
||||||
: null;
|
$: thumbUrl = track ? makeThumbnailUrl(track.thumbnail_id) : null;
|
||||||
$: audioUrl = $playingNow ? makeAudioUrl($address, $token, $playingNow.id) : null;
|
$: audioUrl = track_id ? makeAudioUrl(track_id) : null;
|
||||||
|
|
||||||
function getAudioElement() {
|
function getAudioElement(id: ResourceId | null) {
|
||||||
return document.getElementById('audio-source')! as HTMLAudioElement;
|
const elem = document.getElementById(`audio-source-${id}`);
|
||||||
|
if (elem === null) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
return elem as HTMLAudioElement;
|
||||||
}
|
}
|
||||||
|
|
||||||
function calculateMinuteSecond(seconds: number) {
|
function calculateMinuteSecond(seconds: number) {
|
||||||
@ -32,68 +45,80 @@
|
|||||||
let showIcon = false;
|
let showIcon = false;
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
{#if $playingNow !== null}
|
<div class="flex gap-2 z-10">
|
||||||
<div class="flex gap-2 z-10">
|
{#if audioUrl !== null}
|
||||||
<audio
|
<audio
|
||||||
id="audio-source"
|
id="audio-source-{track_id}"
|
||||||
src={audioUrl}
|
src={audioUrl}
|
||||||
|
autoplay
|
||||||
bind:paused={isPaused}
|
bind:paused={isPaused}
|
||||||
bind:currentTime
|
bind:currentTime
|
||||||
bind:duration
|
bind:duration
|
||||||
autoplay
|
bind:volume={$volume}
|
||||||
/>
|
on:ended={(_) => {
|
||||||
<button
|
const pos = $queuePosition;
|
||||||
class="relative placeholder w-12 h-12"
|
if (pos !== null) {
|
||||||
on:pointerenter={(_) => (showIcon = true)}
|
const _newPos = pos + 1;
|
||||||
on:pointerleave={(_) => (showIcon = false)}
|
const newPos = _newPos < $queue.length ? _newPos : null;
|
||||||
on:click={(_) => {
|
duration = 0;
|
||||||
let elem = getAudioElement();
|
currentTime = 0;
|
||||||
if (isPaused) {
|
queuePosition.set(newPos);
|
||||||
elem.play();
|
|
||||||
} else {
|
|
||||||
elem.pause();
|
|
||||||
}
|
}
|
||||||
}}
|
}}
|
||||||
>
|
/>
|
||||||
<!-- svelte-ignore a11y-missing-attribute -->
|
{/if}
|
||||||
|
<button
|
||||||
|
class="relative placeholder w-12 h-12"
|
||||||
|
on:pointerenter={(_) => (showIcon = true)}
|
||||||
|
on:pointerleave={(_) => (showIcon = false)}
|
||||||
|
on:click={(_) => {
|
||||||
|
let elem = getAudioElement(track_id ?? null);
|
||||||
|
if (elem) {
|
||||||
|
elem.paused ? elem.play() : elem.pause();
|
||||||
|
}
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<IconMusic class="absolute top-1 left-1 w-10 h-10" />
|
||||||
|
<!-- svelte-ignore a11y-missing-attribute -->
|
||||||
|
{#if thumbUrl !== null}
|
||||||
<img
|
<img
|
||||||
src={thumbUrl}
|
src={thumbUrl}
|
||||||
class="child {isError ? 'hidden' : ''}"
|
class="child {isError ? 'hidden' : ''}"
|
||||||
on:error={() => (isError = true)}
|
on:error={() => (isError = true)}
|
||||||
on:load={() => (isError = false)}
|
on:load={() => (isError = false)}
|
||||||
/>
|
/>
|
||||||
<IconPlay class="child play-icon {showIcon && isPaused ? 'visible' : 'hidden'}" />
|
{/if}
|
||||||
<IconPause class="child play-icon {showIcon && !isPaused ? 'visible' : 'hidden'}" />
|
<IconPlay class="child play-icon {showIcon && isPaused ? 'opacity-100' : 'opacity-0'}" />
|
||||||
</button>
|
<IconPause class="child play-icon {showIcon && !isPaused ? 'opacity-100' : 'opacity-0'}" />
|
||||||
<div class="flex flex-col gap-1">
|
</button>
|
||||||
<div
|
<div class="flex flex-col gap-1">
|
||||||
title={$playingNow.track.title}
|
<div
|
||||||
class="w-80 max-md:w-44 max-sm:w-32 whitespace-nowrap overflow-ellipsis overflow-hidden"
|
title={track?.title ?? 'Not playing'}
|
||||||
>
|
class="w-80 max-md:w-44 max-sm:w-32 whitespace-nowrap overflow-ellipsis overflow-hidden"
|
||||||
{$playingNow.track.title}
|
>
|
||||||
</div>
|
{track?.title ?? 'Not playing'}
|
||||||
<div class="flex items-center gap-1">
|
</div>
|
||||||
<RangeSlider
|
<div class="flex items-center gap-1">
|
||||||
name="progress"
|
<RangeSlider
|
||||||
bind:value={currentTime}
|
name="progress"
|
||||||
max={duration}
|
bind:value={currentTime}
|
||||||
step={0.01}
|
max={duration}
|
||||||
class="w-72 max-md:w-36 max-sm:w-24"
|
step={0.01}
|
||||||
/>
|
class="w-72 max-md:w-36 max-sm:w-24"
|
||||||
<div class="text-xs opacity-70 w-8">{calculateMinuteSecond(currentTime)}</div>
|
/>
|
||||||
</div>
|
<div class="text-xs opacity-70 w-8">{calculateMinuteSecond(currentTime)}</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
{/if}
|
</div>
|
||||||
|
|
||||||
<style lang="postcss">
|
<style lang="postcss">
|
||||||
:global(.play-icon) {
|
button :global(.play-icon) {
|
||||||
@apply variant-glass-surface backdrop-blur-sm;
|
@apply transition-opacity variant-glass-surface backdrop-blur-sm;
|
||||||
}
|
}
|
||||||
:global(.child) {
|
button :global(.child) {
|
||||||
@apply absolute top-0 left-0 w-12 h-12;
|
@apply absolute top-0 left-0 w-12 h-12;
|
||||||
}
|
}
|
||||||
:global(.range-content) {
|
div :global(.range-content) {
|
||||||
@apply p-0;
|
@apply p-0;
|
||||||
}
|
}
|
||||||
</style>
|
</style>
|
||||||
|
@ -1,30 +1,46 @@
|
|||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
import type { TrackWithId } from '../types';
|
import type { TrackWithId } from '../types';
|
||||||
import { playingNow, makeThumbnailUrl, address, token } from '../stores';
|
import { queue, queuePosition, makeThumbnailUrl, currentTrack } from '../stores';
|
||||||
import Spinnny from '~icons/line-md/loading-loop';
|
import Spinnny from '~icons/line-md/loading-loop';
|
||||||
import IconPlay from '~icons/mdi/play';
|
import IconPlay from '~icons/mdi/play';
|
||||||
|
import IconMusic from '~icons/mdi/music';
|
||||||
|
|
||||||
export let track_with_id: TrackWithId;
|
export let track_with_id: TrackWithId;
|
||||||
let track = track_with_id.track;
|
let track = track_with_id.track;
|
||||||
|
let track_id = track_with_id.id;
|
||||||
|
|
||||||
$: url = makeThumbnailUrl($address, $token, track.thumbnail_id);
|
$: thumbUrl = makeThumbnailUrl(track.thumbnail_id);
|
||||||
let showSpinner = false;
|
let showSpinner = false;
|
||||||
let isError = false;
|
let isError = false;
|
||||||
let showPlayIcon = false;
|
let showPlayIcon = false;
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<div class="card flex gap-2 m-2 p-1 w-fit max-w-full">
|
<div class="flex gap-2 w-fit max-w-full">
|
||||||
<button
|
<button
|
||||||
class="relative placeholder rounded min-w-[3rem] min-h-[3rem]"
|
class="relative placeholder rounded min-w-[3rem] min-h-[3rem]"
|
||||||
on:click={(_) => playingNow.set(track_with_id)}
|
on:click={(_) => {
|
||||||
|
const position = $queue.indexOf(track_id);
|
||||||
|
if (position !== -1) {
|
||||||
|
queuePosition.set(position);
|
||||||
|
} else {
|
||||||
|
queue.update((q) => {
|
||||||
|
q.push(track_id);
|
||||||
|
return q;
|
||||||
|
});
|
||||||
|
queuePosition.set($queue.length - 1);
|
||||||
|
}
|
||||||
|
}}
|
||||||
on:pointerenter={(_) => (showPlayIcon = true)}
|
on:pointerenter={(_) => (showPlayIcon = true)}
|
||||||
on:pointerleave={(_) => (showPlayIcon = false)}
|
on:pointerleave={(_) => (showPlayIcon = false)}
|
||||||
>
|
>
|
||||||
<Spinnny class="top-1 left-1 w-10 h-10 {showSpinner ? 'visible' : 'hidden'}" />
|
<IconMusic class="absolute top-1 left-1 w-10 h-10" />
|
||||||
|
<Spinnny
|
||||||
|
class="absolute play-icon top-1 left-1 w-10 h-10 {showSpinner ? 'visible' : 'hidden'}"
|
||||||
|
/>
|
||||||
<!-- svelte-ignore a11y-missing-attribute -->
|
<!-- svelte-ignore a11y-missing-attribute -->
|
||||||
<img
|
<img
|
||||||
src={url}
|
src={thumbUrl}
|
||||||
class="top-0 left-0 w-12 h-12 rounded {showSpinner || isError ? 'hidden' : ''}"
|
class="child {showSpinner || isError ? 'hidden' : ''}"
|
||||||
on:error={() => {
|
on:error={() => {
|
||||||
isError = true;
|
isError = true;
|
||||||
showSpinner = false;
|
showSpinner = false;
|
||||||
@ -32,17 +48,27 @@
|
|||||||
on:loadstart={() => (showSpinner = true)}
|
on:loadstart={() => (showSpinner = true)}
|
||||||
on:load={() => (showSpinner = false)}
|
on:load={() => (showSpinner = false)}
|
||||||
/>
|
/>
|
||||||
<IconPlay
|
<IconPlay class="child play-icon {showPlayIcon ? 'opacity-100' : 'opacity-0'}" />
|
||||||
class="absolute top-0 left-0 w-12 h-12 rounded variant-glass-surface backdrop-blur-sm {showPlayIcon
|
|
||||||
? 'visible'
|
|
||||||
: 'hidden'}"
|
|
||||||
/>
|
|
||||||
</button>
|
</button>
|
||||||
<div class="whitespace-nowrap overflow-ellipsis overflow-hidden">
|
<div class="whitespace-nowrap overflow-ellipsis overflow-hidden">
|
||||||
#{track.track_num} - {track.title}
|
<span>#{track.track_num} - {track.title}</span>
|
||||||
|
<span
|
||||||
|
class="badge variant-filled-primary py-0.5 {$currentTrack?.id == track_id
|
||||||
|
? 'visible'
|
||||||
|
: 'hidden'}">playing</span
|
||||||
|
>
|
||||||
<div class="text-sm whitespace-nowrap overflow-ellipsis overflow-hidden">
|
<div class="text-sm whitespace-nowrap overflow-ellipsis overflow-hidden">
|
||||||
<span class="opacity-70">{track.album_title ? `from ${track.album_title}` : ''}</span>
|
<span class="opacity-70">{track.album_title ? `from ${track.album_title}` : ''}</span>
|
||||||
<span class="opacity-40">{track.artist_name ? `by ${track.artist_name}` : ''}</span>
|
<span class="opacity-40">{track.artist_name ? `by ${track.artist_name}` : ''}</span>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<style lang="postcss">
|
||||||
|
button :global(.play-icon) {
|
||||||
|
@apply transition-opacity variant-glass-surface backdrop-blur-sm;
|
||||||
|
}
|
||||||
|
button :global(.child) {
|
||||||
|
@apply rounded absolute top-0 left-0 w-12 h-12;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
25
src/components/volumeSlider.svelte
Normal file
25
src/components/volumeSlider.svelte
Normal file
@ -0,0 +1,25 @@
|
|||||||
|
<script lang="ts">
|
||||||
|
import IconVolumeHigh from '~icons/mdi/volume-high';
|
||||||
|
import IconVolumeMedium from '~icons/mdi/volume-medium';
|
||||||
|
import IconVolumeLow from '~icons/mdi/volume-low';
|
||||||
|
import { RangeSlider } from '@skeletonlabs/skeleton';
|
||||||
|
import type { ResourceId } from '../types';
|
||||||
|
import { currentTrack, volume } from '../stores';
|
||||||
|
|
||||||
|
function getAudioElement(id: ResourceId | null) {
|
||||||
|
const elem = document.getElementById(`audio-source-${id}`);
|
||||||
|
if (elem === null) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
return elem as HTMLAudioElement;
|
||||||
|
}
|
||||||
|
|
||||||
|
$: audioElem = getAudioElement($currentTrack?.id ?? null);
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<div class="flex items-center">
|
||||||
|
<div class="w-24">
|
||||||
|
<RangeSlider name="volume-slider" bind:value={$volume} step={0.01} max={1.0} />
|
||||||
|
</div>
|
||||||
|
<IconVolumeHigh class="w-7 h-7" />
|
||||||
|
</div>
|
@ -1,4 +1,4 @@
|
|||||||
<script>
|
<script lang="ts">
|
||||||
// Your selected Skeleton theme:
|
// Your selected Skeleton theme:
|
||||||
import '@skeletonlabs/skeleton/themes/theme-vintage.css';
|
import '@skeletonlabs/skeleton/themes/theme-vintage.css';
|
||||||
// This contains the bulk of Skeletons required styles:
|
// This contains the bulk of Skeletons required styles:
|
||||||
@ -6,13 +6,17 @@
|
|||||||
import '../app.css';
|
import '../app.css';
|
||||||
|
|
||||||
import { AppShell, Toast, toastStore } from '@skeletonlabs/skeleton';
|
import { AppShell, Toast, toastStore } from '@skeletonlabs/skeleton';
|
||||||
import { address, playingNow, token, tracks, tracksSorted } from '../stores';
|
import { address, currentTrack, queuePosition, token, tracks, tracksSorted } from '../stores';
|
||||||
import { _metadataComm as comm } from './+layout';
|
import { _metadataComm as comm } from './+layout';
|
||||||
import Navbar from '../components/navbar.svelte';
|
import Navbar from '../components/navbar.svelte';
|
||||||
import PlayingNow from '../components/playingnow.svelte';
|
import PlayingNow from '../components/playingnow.svelte';
|
||||||
|
import VolumeSlider from '../components/volumeSlider.svelte';
|
||||||
|
|
||||||
|
$: title = $currentTrack !== null ? `${$currentTrack.track.title} - musikspider` : `musikspider`;
|
||||||
|
|
||||||
comm.setCallbacks({
|
comm.setCallbacks({
|
||||||
onConnect: () => {
|
onConnect: () => {
|
||||||
|
toastStore.clear();
|
||||||
toastStore.trigger({
|
toastStore.trigger({
|
||||||
message: 'Successfully connected to the server',
|
message: 'Successfully connected to the server',
|
||||||
background: 'variant-filled-success'
|
background: 'variant-filled-success'
|
||||||
@ -25,7 +29,7 @@
|
|||||||
background: 'variant-filled-error',
|
background: 'variant-filled-error',
|
||||||
autohide: false
|
autohide: false
|
||||||
});
|
});
|
||||||
playingNow.set(null);
|
queuePosition.set(null);
|
||||||
},
|
},
|
||||||
onIncompatible: (reason) => {
|
onIncompatible: (reason) => {
|
||||||
toastStore.trigger({
|
toastStore.trigger({
|
||||||
@ -54,8 +58,6 @@
|
|||||||
remaining -= 500;
|
remaining -= 500;
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
$: title = $playingNow !== null ? `${$playingNow.track.title} - musikspider` : `musikspider`;
|
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<svelte:head>
|
<svelte:head>
|
||||||
@ -64,12 +66,12 @@
|
|||||||
|
|
||||||
<AppShell>
|
<AppShell>
|
||||||
<svelte:fragment slot="footer">
|
<svelte:fragment slot="footer">
|
||||||
<div class="w-screen">
|
<div class="flex w-screen place-content-end max-sm:place-content-center">
|
||||||
<div class="card fixed mr-4 bottom-14 right-0"><Navbar /></div>
|
<div class="card fixed bottom-14 sm:mr-4"><Navbar /></div>
|
||||||
<div class="card rounded-none fixed w-full bottom-0 flex items-center h-12">
|
</div>
|
||||||
<PlayingNow />
|
<div class="card rounded-none w-screen flex flex-grow items-center h-12">
|
||||||
<div class="ml-auto">volume</div>
|
<PlayingNow />
|
||||||
</div>
|
<div class="ml-auto"><VolumeSlider /></div>
|
||||||
</div>
|
</div>
|
||||||
</svelte:fragment>
|
</svelte:fragment>
|
||||||
<slot />
|
<slot />
|
||||||
|
@ -1,6 +1,6 @@
|
|||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
import VirtualList from 'svelte-tiny-virtual-list';
|
import VirtualList from 'svelte-tiny-virtual-list';
|
||||||
import { tracks, tracksSorted } from '../stores';
|
import { queue, tracks, tracksSorted } from '../stores';
|
||||||
import TrackComponent from '../components/track.svelte';
|
import TrackComponent from '../components/track.svelte';
|
||||||
import type { Track, TrackWithId } from '../types';
|
import type { Track, TrackWithId } from '../types';
|
||||||
|
|
||||||
@ -20,10 +20,12 @@
|
|||||||
height={listHeight}
|
height={listHeight}
|
||||||
itemSize={trackItemSize}
|
itemSize={trackItemSize}
|
||||||
itemCount={trackCount}
|
itemCount={trackCount}
|
||||||
overscanCount={1}
|
overscanCount={3}
|
||||||
|
getKey={(index) => $tracksSorted.get(index)}
|
||||||
>
|
>
|
||||||
<div slot="item" let:index let:style {style}>
|
<div slot="item" let:index let:style {style}>
|
||||||
<div class="max-sm:pr-4"><TrackComponent track_with_id={getTrack(index)} /></div>
|
<div class="pt-2 pl-2 max-sm:pr-4"><TrackComponent track_with_id={getTrack(index)} /></div>
|
||||||
|
<hr class="w-full mt-1.5" />
|
||||||
</div>
|
</div>
|
||||||
</VirtualList>
|
</VirtualList>
|
||||||
</div>
|
</div>
|
||||||
|
52
src/routes/queue/+page.svelte
Normal file
52
src/routes/queue/+page.svelte
Normal file
@ -0,0 +1,52 @@
|
|||||||
|
<script lang="ts">
|
||||||
|
import VirtualList from 'svelte-tiny-virtual-list';
|
||||||
|
import { currentTrack, queue, queuePosition, tracks } from '../../stores';
|
||||||
|
import TrackComponent from '../../components/track.svelte';
|
||||||
|
import type { TrackWithId } from '../../types';
|
||||||
|
import IconRemove from '~icons/mdi/minus-thick';
|
||||||
|
import { get } from 'svelte/store';
|
||||||
|
|
||||||
|
$: trackCount = $queue.length;
|
||||||
|
let trackItemSize = 62;
|
||||||
|
let listHeight = 0;
|
||||||
|
|
||||||
|
$: getTrack = (index: number): TrackWithId => {
|
||||||
|
let id = $queue.at(index)!;
|
||||||
|
let track = $tracks.get(id)!;
|
||||||
|
return { id, track };
|
||||||
|
};
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<div class="h-full" bind:offsetHeight={listHeight}>
|
||||||
|
<VirtualList
|
||||||
|
height={listHeight}
|
||||||
|
itemSize={trackItemSize}
|
||||||
|
itemCount={trackCount}
|
||||||
|
overscanCount={3}
|
||||||
|
getKey={(index) => $queue.at(index)}
|
||||||
|
>
|
||||||
|
<div slot="item" let:index let:style {style}>
|
||||||
|
<div class="flex pt-2 pl-2 max-sm:pr-4">
|
||||||
|
<TrackComponent track_with_id={getTrack(index)} />
|
||||||
|
<button
|
||||||
|
title="remove track"
|
||||||
|
class="mr-2 ml-auto btn px-3 hover:variant-soft-primary"
|
||||||
|
on:click={(_) => {
|
||||||
|
queue.update((q) => {
|
||||||
|
q.splice(index, 1);
|
||||||
|
return q;
|
||||||
|
});
|
||||||
|
const curTrack = $currentTrack;
|
||||||
|
if (curTrack !== null) {
|
||||||
|
const newPos = $queue.indexOf(curTrack.id);
|
||||||
|
queuePosition.set(newPos === -1 ? null : newPos);
|
||||||
|
}
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<IconRemove class="w-7 h-7" />
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
<hr class="w-full mt-1.5" />
|
||||||
|
</div>
|
||||||
|
</VirtualList>
|
||||||
|
</div>
|
@ -1,4 +1,4 @@
|
|||||||
import { writable } from 'svelte/store';
|
import { get, readable, writable, type Writable } from 'svelte/store';
|
||||||
import type { ResourceId, Track, TrackWithId } from './types';
|
import type { ResourceId, Track, TrackWithId } from './types';
|
||||||
|
|
||||||
function writableStorage(key: string, defaultValue: string) {
|
function writableStorage(key: string, defaultValue: string) {
|
||||||
@ -7,18 +7,43 @@ function writableStorage(key: string, defaultValue: string) {
|
|||||||
return store;
|
return store;
|
||||||
}
|
}
|
||||||
|
|
||||||
export function makeThumbnailUrl(address: string, token: string, id: ResourceId) {
|
|
||||||
return `http://${address}/thumbnail/${id}?token=${token}`;
|
|
||||||
}
|
|
||||||
|
|
||||||
export function makeAudioUrl(address: string, token: string, id: ResourceId) {
|
|
||||||
return `http://${address}/audio/id/${id}?token=${token}`;
|
|
||||||
}
|
|
||||||
|
|
||||||
export const address = writableStorage("address", "127.0.0.1:5505");
|
export const address = writableStorage("address", "127.0.0.1:5505");
|
||||||
export const token = writableStorage("token", "");
|
export const token = writableStorage("token", "");
|
||||||
|
|
||||||
|
export function makeThumbnailUrl(id: ResourceId) {
|
||||||
|
return `http://${get(address)}/thumbnail/${id}?token=${get(token)}`;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function makeAudioUrl(id: ResourceId) {
|
||||||
|
return `http://${get(address)}/audio/id/${id}?token=${get(token)}`;
|
||||||
|
}
|
||||||
|
|
||||||
|
export const currentTrack = writable<TrackWithId | null>(null);
|
||||||
|
|
||||||
|
export function getCurrentTrack(tracks: Map<ResourceId, Track>, queue: ResourceId[], position: number | null): TrackWithId | null {
|
||||||
|
if (position === null) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
const id = queue.at(position);
|
||||||
|
if (id === undefined) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
const track = tracks.get(id);
|
||||||
|
if (track === undefined) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
return {
|
||||||
|
track,
|
||||||
|
id,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
export const queuePosition = writable<number | null>(null);
|
||||||
|
export const queue = writable<ResourceId[]>([]);
|
||||||
export const tracks = writable<Map<ResourceId, Track>>(new Map());
|
export const tracks = writable<Map<ResourceId, Track>>(new Map());
|
||||||
export const tracksSorted = writable<Map<number, ResourceId>>(new Map());
|
export const tracksSorted = writable<Map<number, ResourceId>>(new Map());
|
||||||
|
|
||||||
export const playingNow = writable<TrackWithId | null>(null);
|
queuePosition.subscribe((pos) => currentTrack.set(getCurrentTrack(get(tracks), get(queue), pos)));
|
||||||
|
tracks.subscribe((newTracks) => currentTrack.set(getCurrentTrack(newTracks, get(queue), get(queuePosition))));
|
||||||
|
|
||||||
|
export const volume = writable<number>(1.0);
|
Loading…
Reference in New Issue
Block a user