feat: currently playing + volume + seek position + toggle pause
This commit is contained in:
@@ -2,3 +2,5 @@
|
|||||||
package-lock.json
|
package-lock.json
|
||||||
pnpm-lock.yaml
|
pnpm-lock.yaml
|
||||||
yarn.lock
|
yarn.lock
|
||||||
|
src/lib/proto/
|
||||||
|
src/lib/components/ui/
|
||||||
|
|||||||
@@ -8,7 +8,8 @@
|
|||||||
"preview": "vite preview",
|
"preview": "vite preview",
|
||||||
"check": "svelte-kit sync && svelte-check --tsconfig ./tsconfig.json",
|
"check": "svelte-kit sync && svelte-check --tsconfig ./tsconfig.json",
|
||||||
"check:watch": "svelte-kit sync && svelte-check --tsconfig ./tsconfig.json --watch",
|
"check:watch": "svelte-kit sync && svelte-check --tsconfig ./tsconfig.json --watch",
|
||||||
"format": "prettier --write ."
|
"format": "prettier --write .",
|
||||||
|
"build:proto": "bunx protoc --ts_out ./src/lib/proto/ --proto_path protos protos/*"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@sveltejs/adapter-auto": "^3.0.0",
|
"@sveltejs/adapter-auto": "^3.0.0",
|
||||||
|
|||||||
@@ -17,4 +17,5 @@ message Track {
|
|||||||
string name = 2;
|
string name = 2;
|
||||||
string artist_name = 3;
|
string artist_name = 3;
|
||||||
uint64 artist_id = 4;
|
uint64 artist_id = 4;
|
||||||
|
uint64 duration = 5;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,13 +1,18 @@
|
|||||||
syntax = "proto3";
|
syntax = "proto3";
|
||||||
|
|
||||||
import 'google/protobuf/empty.proto';
|
import 'google/protobuf/empty.proto';
|
||||||
|
import 'library.proto';
|
||||||
|
|
||||||
package player;
|
package player;
|
||||||
|
|
||||||
service Player {
|
service Player {
|
||||||
rpc PlayTrack(PlayTrackRequest) returns (PlayTrackResponse);
|
rpc PlayTrack(PlayTrackRequest) returns (PlayTrackResponse);
|
||||||
rpc ResumeTrack(google.protobuf.Empty) returns (google.protobuf.Empty);
|
rpc ResumeTrack(google.protobuf.Empty) returns (PauseState);
|
||||||
rpc PauseTrack(google.protobuf.Empty) returns (google.protobuf.Empty);
|
rpc PauseTrack(google.protobuf.Empty) returns (PauseState);
|
||||||
|
rpc TogglePause(google.protobuf.Empty) returns (PauseState);
|
||||||
|
rpc GetStatus(google.protobuf.Empty) returns (stream PlayerStatus);
|
||||||
|
rpc SeekPosition(SeekPositionRequest) returns (SeekPositionResponse);
|
||||||
|
rpc SetVolume(SetVolumeRequest) returns (SetVolumeResponse);
|
||||||
}
|
}
|
||||||
|
|
||||||
message PlayTrackRequest {
|
message PlayTrackRequest {
|
||||||
@@ -15,4 +20,33 @@ message PlayTrackRequest {
|
|||||||
}
|
}
|
||||||
|
|
||||||
message PlayTrackResponse {
|
message PlayTrackResponse {
|
||||||
|
library.Track track = 1;
|
||||||
|
uint64 position = 2;
|
||||||
|
}
|
||||||
|
|
||||||
|
message PlayerStatus {
|
||||||
|
optional library.Track currently_playing = 1;
|
||||||
|
bool is_paused = 2;
|
||||||
|
float volume = 3;
|
||||||
|
uint64 progress = 4;
|
||||||
|
}
|
||||||
|
|
||||||
|
message PauseState {
|
||||||
|
bool is_paused = 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
message SeekPositionRequest {
|
||||||
|
uint64 position = 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
message SeekPositionResponse {
|
||||||
|
uint64 position = 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
message SetVolumeRequest {
|
||||||
|
float volume = 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
message SetVolumeResponse {
|
||||||
|
float volume = 1;
|
||||||
}
|
}
|
||||||
|
|||||||
18
src/app.css
18
src/app.css
@@ -26,10 +26,10 @@
|
|||||||
|
|
||||||
--sidebar-background: 0 0% 98%;
|
--sidebar-background: 0 0% 98%;
|
||||||
--sidebar-foreground: 240 5.3% 26.1%;
|
--sidebar-foreground: 240 5.3% 26.1%;
|
||||||
--sidebar-primary: 240 5.9% 10%;
|
--sidebar-primary: 217.2 91.2% 59.8%;
|
||||||
--sidebar-primary-foreground: 0 0% 98%;
|
--sidebar-primary-foreground: 222.2 47.4% 11.2%;
|
||||||
--sidebar-accent: 240 4.8% 95.9%;
|
--sidebar-accent: 217.2 91.2% 59.8%;
|
||||||
--sidebar-accent-foreground: 240 5.9% 10%;
|
--sidebar-accent-foreground: 222.2 47.4% 11.2%;
|
||||||
--sidebar-border: 220 13% 91%;
|
--sidebar-border: 220 13% 91%;
|
||||||
--sidebar-ring: 217.2 91.2% 59.8%;
|
--sidebar-ring: 217.2 91.2% 59.8%;
|
||||||
|
|
||||||
@@ -56,12 +56,12 @@
|
|||||||
--input: 217.2 32.6% 17.5%;
|
--input: 217.2 32.6% 17.5%;
|
||||||
--ring: 224.3 76.3% 48%;
|
--ring: 224.3 76.3% 48%;
|
||||||
|
|
||||||
--sidebar-background: 240 5.9% 10%;
|
--sidebar-background: 223 84% 3%;
|
||||||
--sidebar-foreground: 240 4.8% 95.9%;
|
--sidebar-foreground: 240 4.8% 95.9%;
|
||||||
--sidebar-primary: 224.3 76.3% 48%;
|
--sidebar-primary: 217.2 91.2% 59.8%;
|
||||||
--sidebar-primary-foreground: 0 0% 100%;
|
--sidebar-primary-foreground: 222.2 47.4% 11.2%;
|
||||||
--sidebar-accent: 240 3.7% 15.9%;
|
--sidebar-accent: 217.2 91.2% 59.8%;
|
||||||
--sidebar-accent-foreground: 240 4.8% 95.9%;
|
--sidebar-accent-foreground: 222.2 47.4% 11.2%;
|
||||||
--sidebar-border: 240 3.7% 15.9%;
|
--sidebar-border: 240 3.7% 15.9%;
|
||||||
--sidebar-ring: 217.2 91.2% 59.8%;
|
--sidebar-ring: 217.2 91.2% 59.8%;
|
||||||
}
|
}
|
||||||
|
|||||||
5
src/hooks.client.ts
Normal file
5
src/hooks.client.ts
Normal file
@@ -0,0 +1,5 @@
|
|||||||
|
import { GrpcWebFetchTransport } from '@protobuf-ts/grpcweb-transport';
|
||||||
|
|
||||||
|
export const protoTransport = new GrpcWebFetchTransport({
|
||||||
|
baseUrl: 'http://[::1]:39993'
|
||||||
|
});
|
||||||
@@ -9,6 +9,9 @@
|
|||||||
<Sidebar.Menu>
|
<Sidebar.Menu>
|
||||||
<Sidebar.MenuItem>
|
<Sidebar.MenuItem>
|
||||||
<Sidebar.MenuButton isActive={$page.url.pathname === '/'} class="transition-all">
|
<Sidebar.MenuButton isActive={$page.url.pathname === '/'} class="transition-all">
|
||||||
|
{#snippet tooltipContent()}
|
||||||
|
Home
|
||||||
|
{/snippet}
|
||||||
{#snippet child({ props })}
|
{#snippet child({ props })}
|
||||||
<a href="/" {...props}>
|
<a href="/" {...props}>
|
||||||
<Home />
|
<Home />
|
||||||
@@ -25,6 +28,9 @@
|
|||||||
<Sidebar.Menu>
|
<Sidebar.Menu>
|
||||||
<Sidebar.MenuItem>
|
<Sidebar.MenuItem>
|
||||||
<Sidebar.MenuButton isActive={$page.url.pathname === '/songs'} class="transition-all">
|
<Sidebar.MenuButton isActive={$page.url.pathname === '/songs'} class="transition-all">
|
||||||
|
{#snippet tooltipContent()}
|
||||||
|
Songs
|
||||||
|
{/snippet}
|
||||||
{#snippet child({ props })}
|
{#snippet child({ props })}
|
||||||
<a href="/songs" {...props}>
|
<a href="/songs" {...props}>
|
||||||
<Music2 />
|
<Music2 />
|
||||||
@@ -35,6 +41,9 @@
|
|||||||
</Sidebar.MenuItem>
|
</Sidebar.MenuItem>
|
||||||
<Sidebar.MenuItem>
|
<Sidebar.MenuItem>
|
||||||
<Sidebar.MenuButton isActive={$page.url.pathname === '/albums'} class="transition-all">
|
<Sidebar.MenuButton isActive={$page.url.pathname === '/albums'} class="transition-all">
|
||||||
|
{#snippet tooltipContent()}
|
||||||
|
Albums
|
||||||
|
{/snippet}
|
||||||
{#snippet child({ props })}
|
{#snippet child({ props })}
|
||||||
<a href="/albums" {...props}>
|
<a href="/albums" {...props}>
|
||||||
<Library />
|
<Library />
|
||||||
@@ -45,6 +54,9 @@
|
|||||||
</Sidebar.MenuItem>
|
</Sidebar.MenuItem>
|
||||||
<Sidebar.MenuItem>
|
<Sidebar.MenuItem>
|
||||||
<Sidebar.MenuButton isActive={$page.url.pathname === '/playlists'} class="transition-all">
|
<Sidebar.MenuButton isActive={$page.url.pathname === '/playlists'} class="transition-all">
|
||||||
|
{#snippet tooltipContent()}
|
||||||
|
Playlists
|
||||||
|
{/snippet}
|
||||||
{#snippet child({ props })}
|
{#snippet child({ props })}
|
||||||
<a href="/playlists" {...props}>
|
<a href="/playlists" {...props}>
|
||||||
<ListMusic />
|
<ListMusic />
|
||||||
@@ -60,6 +72,9 @@
|
|||||||
<Sidebar.Menu>
|
<Sidebar.Menu>
|
||||||
<Sidebar.MenuItem>
|
<Sidebar.MenuItem>
|
||||||
<Sidebar.MenuButton isActive={$page.url.pathname === '/settings'} class="transition-all">
|
<Sidebar.MenuButton isActive={$page.url.pathname === '/settings'} class="transition-all">
|
||||||
|
{#snippet tooltipContent()}
|
||||||
|
Settings
|
||||||
|
{/snippet}
|
||||||
{#snippet child({ props })}
|
{#snippet child({ props })}
|
||||||
<a href="/settings" {...props}>
|
<a href="/settings" {...props}>
|
||||||
<Settings />
|
<Settings />
|
||||||
|
|||||||
@@ -16,65 +16,117 @@
|
|||||||
import { Slider } from '$lib/components/ui/slider';
|
import { Slider } from '$lib/components/ui/slider';
|
||||||
import * as Popover from '$lib/components/ui/popover';
|
import * as Popover from '$lib/components/ui/popover';
|
||||||
import { Separator } from '$lib/components/ui/separator';
|
import { Separator } from '$lib/components/ui/separator';
|
||||||
import { currentlyPlaying, volume } from '$lib/stores/player';
|
import { getPlayerState } from '$lib/player.svelte';
|
||||||
import { enhance } from '$app/forms';
|
import { enhance } from '$app/forms';
|
||||||
|
import { getCoverUrl } from '$lib/covers';
|
||||||
|
import type { SubmitFunction } from '../../../routes/player/$types';
|
||||||
|
|
||||||
dayjs.extend(duration);
|
dayjs.extend(duration);
|
||||||
|
|
||||||
let progressValue = $state(0);
|
const player = getPlayerState();
|
||||||
|
|
||||||
let isPlaying = $state(false);
|
let trackDuration = $derived(
|
||||||
|
player.currentlyPlaying ? Number(player.currentlyPlaying.duration) : 0
|
||||||
|
);
|
||||||
|
|
||||||
/* async function seekProgressValue(e: MouseEvent) {
|
let mouse = $state({
|
||||||
if (!$currentlyPlaying) {
|
offsetX: 0,
|
||||||
|
offsetY: 0
|
||||||
|
});
|
||||||
|
|
||||||
|
let progressBar = $state<HTMLElement | null>(null);
|
||||||
|
let volumeForm = $state<HTMLFormElement | null>(null);
|
||||||
|
|
||||||
|
const seekProgressValue: SubmitFunction = ({ formData, cancel }) => {
|
||||||
|
if (!player.currentlyPlaying || !progressBar) {
|
||||||
|
cancel();
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
const target = e.currentTarget! as HTMLElement;
|
const targetProgress = Math.round(
|
||||||
const _targetProgress = Math.max(0.0, Math.min(1.0, e.offsetX / target.offsetWidth));
|
Math.max(0.0, Math.min(1.0, mouse.offsetX / progressBar.offsetWidth)) *
|
||||||
} */
|
Number(player.currentlyPlaying.duration)
|
||||||
|
);
|
||||||
|
|
||||||
|
formData.append('position', targetProgress.toString());
|
||||||
|
|
||||||
|
return async ({ update, result }) => {
|
||||||
|
await update({
|
||||||
|
invalidateAll: false
|
||||||
|
});
|
||||||
|
|
||||||
|
if (result.type === 'success' && result.data && 'position' in result.data) {
|
||||||
|
player.progress = result.data.position;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
async function skipToQueueIndex(index: number) {
|
async function skipToQueueIndex(index: number) {
|
||||||
console.log(`SKIP TO QUEUE INDEX ${index}`);
|
console.log(`SKIP TO QUEUE INDEX ${index}`);
|
||||||
}
|
}
|
||||||
|
|
||||||
let volumeValue = {
|
const submitTogglePause: SubmitFunction = async () => {
|
||||||
|
return async ({ update, result }) => {
|
||||||
|
await update({
|
||||||
|
invalidateAll: false
|
||||||
|
});
|
||||||
|
|
||||||
|
if (result.type === 'success' && result.data && 'isPaused' in result.data) {
|
||||||
|
player.isPaused = result.data.isPaused;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
|
function onMouseMove(e: MouseEvent) {
|
||||||
|
mouse.offsetX = e.offsetX;
|
||||||
|
mouse.offsetY = e.offsetY;
|
||||||
|
}
|
||||||
|
|
||||||
|
let volume = $state({
|
||||||
get value() {
|
get value() {
|
||||||
return [$volume];
|
return [player.volume];
|
||||||
},
|
},
|
||||||
set value(v: number[]) {
|
set value(v: number[]) {
|
||||||
$volume = v[0];
|
player.volume = v[0];
|
||||||
// TODO: Send message to player
|
|
||||||
}
|
}
|
||||||
};
|
});
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<footer class="border border-x-0 border-t border-border/40 bg-background/95 px-4">
|
<footer class="border border-x-0 border-t border-border/40 bg-background/95 px-4">
|
||||||
<nav class="my-2 grid w-full grid-cols-5 items-center">
|
<nav class="my-2 grid w-full grid-cols-5 items-center">
|
||||||
<div class="col-span-1 flex items-end gap-2">
|
<div class="col-span-1 flex items-end gap-2">
|
||||||
{#if $currentlyPlaying}
|
{#if player.currentlyPlaying}
|
||||||
<img
|
<img
|
||||||
class="w-16 rounded-md shadow-2xl shadow-primary/50"
|
class="aspect-square w-16 rounded-md shadow-2xl shadow-primary/50"
|
||||||
src="https://i.scdn.co/image/ab67616d0000b2732c0ead8ce0dd1c6e2fca817f"
|
src={getCoverUrl(player.currentlyPlaying.hash)}
|
||||||
alt={$currentlyPlaying}
|
alt={player.currentlyPlaying.name}
|
||||||
/>
|
/>
|
||||||
<div class="space-y-1 text-sm">
|
<div class="space-y-1 text-sm">
|
||||||
<h3 class="font-medium leading-none">Song name</h3>
|
<h3 class="font-medium leading-none">{player.currentlyPlaying.name}</h3>
|
||||||
<p class="text-xs text-muted-foreground">Artist name</p>
|
<p class="text-xs text-muted-foreground">{player.currentlyPlaying.artistName}</p>
|
||||||
</div>
|
</div>
|
||||||
{:else}
|
{:else}
|
||||||
<div class="h-16 w-16 rounded-md bg-muted shadow-2xl shadow-primary/50"></div>
|
<div class="h-16 w-16 rounded-md bg-muted shadow-2xl shadow-primary/50"></div>
|
||||||
{/if}
|
{/if}
|
||||||
</div>
|
</div>
|
||||||
<form class="col-span-3 flex justify-center gap-1" method="POST" use:enhance>
|
<form
|
||||||
|
class="col-span-3 flex justify-center gap-1"
|
||||||
|
method="POST"
|
||||||
|
use:enhance={submitTogglePause}
|
||||||
|
>
|
||||||
<Button variant="outline" size="icon">
|
<Button variant="outline" size="icon">
|
||||||
<SkipBack />
|
<SkipBack />
|
||||||
</Button>
|
</Button>
|
||||||
<Button type="submit" formaction="/player?/pause" variant="outline" size="icon">
|
<Button
|
||||||
{#if isPlaying}
|
type="submit"
|
||||||
<Pause />
|
formaction="/player?/{player.isPaused ? 'resume' : 'pause'}"
|
||||||
{:else}
|
variant="outline"
|
||||||
|
size="icon"
|
||||||
|
>
|
||||||
|
{#if player.isPaused}
|
||||||
<Play />
|
<Play />
|
||||||
|
{:else}
|
||||||
|
<Pause />
|
||||||
{/if}
|
{/if}
|
||||||
</Button>
|
</Button>
|
||||||
<Button variant="outline" size="icon">
|
<Button variant="outline" size="icon">
|
||||||
@@ -129,32 +181,67 @@
|
|||||||
|
|
||||||
<div class="flex grow flex-row items-center gap-4">
|
<div class="flex grow flex-row items-center gap-4">
|
||||||
<div class="min-w-6 text-muted-foreground">
|
<div class="min-w-6 text-muted-foreground">
|
||||||
{#if $volume <= 0}
|
{#if player.volume <= 0.0}
|
||||||
<VolumeX class="size-full" />
|
<VolumeX class="size-full" />
|
||||||
{:else if $volume < 50.0}
|
{:else if player.volume < 0.5}
|
||||||
<Volume1 class="size-full" />
|
<Volume1 class="size-full" />
|
||||||
{:else}
|
{:else}
|
||||||
<Volume2 class="size-full" />
|
<Volume2 class="size-full" />
|
||||||
{/if}
|
{/if}
|
||||||
</div>
|
</div>
|
||||||
<div class="flex-1">
|
<form
|
||||||
<Slider class="w-full" min={0} max={1} step={0.01} bind:value={volumeValue.value} />
|
bind:this={volumeForm}
|
||||||
</div>
|
class="flex-1"
|
||||||
|
method="POST"
|
||||||
|
action="/player?/volume"
|
||||||
|
use:enhance
|
||||||
|
>
|
||||||
|
<input type="hidden" name="volume" value={volume.value[0]} />
|
||||||
|
<Slider
|
||||||
|
class="w-full"
|
||||||
|
min={0}
|
||||||
|
max={1}
|
||||||
|
step={0.01}
|
||||||
|
bind:value={volume.value}
|
||||||
|
onValueChange={() => {
|
||||||
|
volumeForm?.requestSubmit();
|
||||||
|
}}
|
||||||
|
onValueCommit={() => {
|
||||||
|
volumeForm?.requestSubmit();
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
</form>
|
||||||
<div class="min-w-10 self-center">
|
<div class="min-w-10 self-center">
|
||||||
<p class="text-sm text-muted-foreground">
|
<p class="text-sm text-muted-foreground">
|
||||||
{Math.round($volume * 100.0)}%
|
{Math.round(player.volume * 100.0)}%
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</nav>
|
</nav>
|
||||||
<div class="mb-2 flex flex-row items-center gap-2">
|
<div class="mb-2 flex flex-row items-center gap-2">
|
||||||
<span class="w-12 text-left text-sm">{dayjs.duration(0, 'seconds').format('mm:ss')}</span>
|
<span class="w-12 text-left text-sm"
|
||||||
<button class="w-full">
|
>{dayjs.duration(Number(player.progress), 'milliseconds').format('mm:ss')}</span
|
||||||
{#key progressValue}
|
>
|
||||||
<Progress class="pointer-events-none w-full" value={progressValue} max={100} />
|
<form
|
||||||
{/key}
|
class="w-full"
|
||||||
</button>
|
method="POST"
|
||||||
<span class="w-12 text-right text-sm">{dayjs.duration(0, 'seconds').format('mm:ss')}</span>
|
action="/player?/seek"
|
||||||
|
use:enhance={seekProgressValue}
|
||||||
|
bind:this={progressBar}
|
||||||
|
>
|
||||||
|
<button class="w-full" type="submit">
|
||||||
|
<Progress
|
||||||
|
class="pointer-events-none h-3 w-full"
|
||||||
|
value={Number(player.progress)}
|
||||||
|
max={player.currentlyPlaying ? Number(player.currentlyPlaying.duration) : 1}
|
||||||
|
/>
|
||||||
|
</button>
|
||||||
|
</form>
|
||||||
|
<span class="w-12 text-right text-sm"
|
||||||
|
>{dayjs.duration(trackDuration, 'milliseconds').format('mm:ss')}</span
|
||||||
|
>
|
||||||
</div>
|
</div>
|
||||||
</footer>
|
</footer>
|
||||||
|
|
||||||
|
<svelte:window onmousemove={onMouseMove} />
|
||||||
|
|||||||
@@ -1,9 +1,11 @@
|
|||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
import { enhance } from '$app/forms';
|
import { enhance } from '$app/forms';
|
||||||
import { getCoverUrl } from '$lib/covers';
|
import { getCoverUrl } from '$lib/covers';
|
||||||
|
import { getPlayerState } from '$lib/player.svelte';
|
||||||
|
|
||||||
// import { AudioLines } from 'lucide-svelte';
|
// import { AudioLines } from 'lucide-svelte';
|
||||||
import type { Song } from '$lib/song';
|
import type { Song } from '$lib/song';
|
||||||
|
import type { SubmitFunction } from '../../../routes/songs/[hash]/$types';
|
||||||
|
|
||||||
interface Props {
|
interface Props {
|
||||||
song: Song;
|
song: Song;
|
||||||
@@ -11,15 +13,30 @@
|
|||||||
|
|
||||||
let { song }: Props = $props();
|
let { song }: Props = $props();
|
||||||
|
|
||||||
async function playSong(e: MouseEvent) {
|
const player = getPlayerState();
|
||||||
e.preventDefault();
|
|
||||||
// TODO: Play song
|
const submitPlaySong: SubmitFunction = async () => {
|
||||||
}
|
return async ({ update, result }) => {
|
||||||
|
await update({
|
||||||
|
invalidateAll: false
|
||||||
|
});
|
||||||
|
|
||||||
|
if (result.type === 'success' && result.data) {
|
||||||
|
player.currentlyPlaying = result.data.track ?? null;
|
||||||
|
player.progress = result.data.position;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
};
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<form class="flex flex-col gap-3" method="POST" action="/songs/{song.hash}?/play" use:enhance>
|
<form
|
||||||
|
class="flex flex-col gap-3"
|
||||||
|
method="POST"
|
||||||
|
action="/songs/{song.hash}?/play"
|
||||||
|
use:enhance={submitPlaySong}
|
||||||
|
>
|
||||||
<div class="relative">
|
<div class="relative">
|
||||||
<button type="submit" class="relative overflow-hidden rounded-lg" oncontextmenu={playSong}>
|
<button type="submit" class="relative overflow-hidden rounded-lg">
|
||||||
<img
|
<img
|
||||||
class="aspect-square w-auto transition-all duration-150 hover:scale-105 hover:saturate-150"
|
class="aspect-square w-auto transition-all duration-150 hover:scale-105 hover:saturate-150"
|
||||||
src={getCoverUrl(song.hash)}
|
src={getCoverUrl(song.hash)}
|
||||||
|
|||||||
50
src/lib/player.svelte.ts
Normal file
50
src/lib/player.svelte.ts
Normal file
@@ -0,0 +1,50 @@
|
|||||||
|
import type { Track } from './proto/library';
|
||||||
|
import type { PlayerStatus } from './proto/player';
|
||||||
|
import { getContext, setContext } from 'svelte';
|
||||||
|
import { PlayerClient } from './proto/player.client';
|
||||||
|
import { protoTransport } from '../hooks.client';
|
||||||
|
|
||||||
|
class PlayerState {
|
||||||
|
volume = $state(0);
|
||||||
|
currentlyPlaying = $state<Track | null>(null);
|
||||||
|
progress = $state<bigint>(0n);
|
||||||
|
isPaused = $state(false);
|
||||||
|
#abortContoller: AbortController | null = null;
|
||||||
|
|
||||||
|
constructor() {
|
||||||
|
const client = new PlayerClient(protoTransport);
|
||||||
|
this.#abortContoller = new AbortController();
|
||||||
|
|
||||||
|
const stream = client.getStatus(
|
||||||
|
{},
|
||||||
|
{
|
||||||
|
abort: this.#abortContoller.signal
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
|
stream.responses.onMessage((status) => this.applyStatus(status));
|
||||||
|
}
|
||||||
|
|
||||||
|
applyStatus(status: PlayerStatus) {
|
||||||
|
this.volume = status.volume;
|
||||||
|
this.progress = status.progress;
|
||||||
|
this.currentlyPlaying = status.currentlyPlaying ?? null;
|
||||||
|
this.isPaused = status.isPaused;
|
||||||
|
}
|
||||||
|
|
||||||
|
abort() {
|
||||||
|
if (this.#abortContoller !== null) {
|
||||||
|
this.#abortContoller.abort();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const PLAYER_KEY = Symbol('GROOVE_PLAYER');
|
||||||
|
|
||||||
|
export function setPlayerState() {
|
||||||
|
return setContext(PLAYER_KEY, new PlayerState());
|
||||||
|
}
|
||||||
|
|
||||||
|
export function getPlayerState() {
|
||||||
|
return getContext<ReturnType<typeof setPlayerState>>(PLAYER_KEY);
|
||||||
|
}
|
||||||
17
src/lib/proto.ts
Normal file
17
src/lib/proto.ts
Normal file
@@ -0,0 +1,17 @@
|
|||||||
|
export function serializable<F, T extends object = object>(data: T): F {
|
||||||
|
const obj = {};
|
||||||
|
|
||||||
|
for (const key in data) {
|
||||||
|
const value = data[key];
|
||||||
|
|
||||||
|
if (typeof value === 'object') {
|
||||||
|
/// @ts-ignore
|
||||||
|
obj[key] = serializable(value);
|
||||||
|
} else {
|
||||||
|
/// @ts-ignore
|
||||||
|
obj[key] = value;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return obj as unknown as F;
|
||||||
|
}
|
||||||
@@ -41,6 +41,10 @@ export interface Track {
|
|||||||
* @generated from protobuf field: uint64 artist_id = 4;
|
* @generated from protobuf field: uint64 artist_id = 4;
|
||||||
*/
|
*/
|
||||||
artistId: bigint;
|
artistId: bigint;
|
||||||
|
/**
|
||||||
|
* @generated from protobuf field: uint64 duration = 5;
|
||||||
|
*/
|
||||||
|
duration: bigint;
|
||||||
}
|
}
|
||||||
// @generated message type with reflection information, may provide speed optimized methods
|
// @generated message type with reflection information, may provide speed optimized methods
|
||||||
class TrackList$Type extends MessageType<TrackList> {
|
class TrackList$Type extends MessageType<TrackList> {
|
||||||
@@ -122,6 +126,13 @@ class Track$Type extends MessageType<Track> {
|
|||||||
kind: 'scalar',
|
kind: 'scalar',
|
||||||
T: 4 /*ScalarType.UINT64*/,
|
T: 4 /*ScalarType.UINT64*/,
|
||||||
L: 0 /*LongType.BIGINT*/
|
L: 0 /*LongType.BIGINT*/
|
||||||
|
},
|
||||||
|
{
|
||||||
|
no: 5,
|
||||||
|
name: 'duration',
|
||||||
|
kind: 'scalar',
|
||||||
|
T: 4 /*ScalarType.UINT64*/,
|
||||||
|
L: 0 /*LongType.BIGINT*/
|
||||||
}
|
}
|
||||||
]);
|
]);
|
||||||
}
|
}
|
||||||
@@ -131,6 +142,7 @@ class Track$Type extends MessageType<Track> {
|
|||||||
message.name = '';
|
message.name = '';
|
||||||
message.artistName = '';
|
message.artistName = '';
|
||||||
message.artistId = 0n;
|
message.artistId = 0n;
|
||||||
|
message.duration = 0n;
|
||||||
if (value !== undefined) reflectionMergePartial<Track>(this, message, value);
|
if (value !== undefined) reflectionMergePartial<Track>(this, message, value);
|
||||||
return message;
|
return message;
|
||||||
}
|
}
|
||||||
@@ -157,6 +169,9 @@ class Track$Type extends MessageType<Track> {
|
|||||||
case /* uint64 artist_id */ 4:
|
case /* uint64 artist_id */ 4:
|
||||||
message.artistId = reader.uint64().toBigInt();
|
message.artistId = reader.uint64().toBigInt();
|
||||||
break;
|
break;
|
||||||
|
case /* uint64 duration */ 5:
|
||||||
|
message.duration = reader.uint64().toBigInt();
|
||||||
|
break;
|
||||||
default:
|
default:
|
||||||
let u = options.readUnknownField;
|
let u = options.readUnknownField;
|
||||||
if (u === 'throw')
|
if (u === 'throw')
|
||||||
@@ -190,6 +205,8 @@ class Track$Type extends MessageType<Track> {
|
|||||||
writer.tag(3, WireType.LengthDelimited).string(message.artistName);
|
writer.tag(3, WireType.LengthDelimited).string(message.artistName);
|
||||||
/* uint64 artist_id = 4; */
|
/* uint64 artist_id = 4; */
|
||||||
if (message.artistId !== 0n) writer.tag(4, WireType.Varint).uint64(message.artistId);
|
if (message.artistId !== 0n) writer.tag(4, WireType.Varint).uint64(message.artistId);
|
||||||
|
/* uint64 duration = 5; */
|
||||||
|
if (message.duration !== 0n) writer.tag(5, WireType.Varint).uint64(message.duration);
|
||||||
let u = options.writeUnknownFields;
|
let u = options.writeUnknownFields;
|
||||||
if (u !== false) (u == true ? UnknownFieldHandler.onWrite : u)(this.typeName, message, writer);
|
if (u !== false) (u == true ? UnknownFieldHandler.onWrite : u)(this.typeName, message, writer);
|
||||||
return writer;
|
return writer;
|
||||||
|
|||||||
@@ -4,6 +4,13 @@
|
|||||||
import type { RpcTransport } from '@protobuf-ts/runtime-rpc';
|
import type { RpcTransport } from '@protobuf-ts/runtime-rpc';
|
||||||
import type { ServiceInfo } from '@protobuf-ts/runtime-rpc';
|
import type { ServiceInfo } from '@protobuf-ts/runtime-rpc';
|
||||||
import { Player } from './player';
|
import { Player } from './player';
|
||||||
|
import type { SetVolumeResponse } from './player';
|
||||||
|
import type { SetVolumeRequest } from './player';
|
||||||
|
import type { SeekPositionResponse } from './player';
|
||||||
|
import type { SeekPositionRequest } from './player';
|
||||||
|
import type { PlayerStatus } from './player';
|
||||||
|
import type { ServerStreamingCall } from '@protobuf-ts/runtime-rpc';
|
||||||
|
import type { PauseState } from './player';
|
||||||
import type { Empty } from './google/protobuf/empty';
|
import type { Empty } from './google/protobuf/empty';
|
||||||
import { stackIntercept } from '@protobuf-ts/runtime-rpc';
|
import { stackIntercept } from '@protobuf-ts/runtime-rpc';
|
||||||
import type { PlayTrackResponse } from './player';
|
import type { PlayTrackResponse } from './player';
|
||||||
@@ -22,13 +29,35 @@ export interface IPlayerClient {
|
|||||||
options?: RpcOptions
|
options?: RpcOptions
|
||||||
): UnaryCall<PlayTrackRequest, PlayTrackResponse>;
|
): UnaryCall<PlayTrackRequest, PlayTrackResponse>;
|
||||||
/**
|
/**
|
||||||
* @generated from protobuf rpc: ResumeTrack(google.protobuf.Empty) returns (google.protobuf.Empty);
|
* @generated from protobuf rpc: ResumeTrack(google.protobuf.Empty) returns (player.PauseState);
|
||||||
*/
|
*/
|
||||||
resumeTrack(input: Empty, options?: RpcOptions): UnaryCall<Empty, Empty>;
|
resumeTrack(input: Empty, options?: RpcOptions): UnaryCall<Empty, PauseState>;
|
||||||
/**
|
/**
|
||||||
* @generated from protobuf rpc: PauseTrack(google.protobuf.Empty) returns (google.protobuf.Empty);
|
* @generated from protobuf rpc: PauseTrack(google.protobuf.Empty) returns (player.PauseState);
|
||||||
*/
|
*/
|
||||||
pauseTrack(input: Empty, options?: RpcOptions): UnaryCall<Empty, Empty>;
|
pauseTrack(input: Empty, options?: RpcOptions): UnaryCall<Empty, PauseState>;
|
||||||
|
/**
|
||||||
|
* @generated from protobuf rpc: TogglePause(google.protobuf.Empty) returns (player.PauseState);
|
||||||
|
*/
|
||||||
|
togglePause(input: Empty, options?: RpcOptions): UnaryCall<Empty, PauseState>;
|
||||||
|
/**
|
||||||
|
* @generated from protobuf rpc: GetStatus(google.protobuf.Empty) returns (stream player.PlayerStatus);
|
||||||
|
*/
|
||||||
|
getStatus(input: Empty, options?: RpcOptions): ServerStreamingCall<Empty, PlayerStatus>;
|
||||||
|
/**
|
||||||
|
* @generated from protobuf rpc: SeekPosition(player.SeekPositionRequest) returns (player.SeekPositionResponse);
|
||||||
|
*/
|
||||||
|
seekPosition(
|
||||||
|
input: SeekPositionRequest,
|
||||||
|
options?: RpcOptions
|
||||||
|
): UnaryCall<SeekPositionRequest, SeekPositionResponse>;
|
||||||
|
/**
|
||||||
|
* @generated from protobuf rpc: SetVolume(player.SetVolumeRequest) returns (player.SetVolumeResponse);
|
||||||
|
*/
|
||||||
|
setVolume(
|
||||||
|
input: SetVolumeRequest,
|
||||||
|
options?: RpcOptions
|
||||||
|
): UnaryCall<SetVolumeRequest, SetVolumeResponse>;
|
||||||
}
|
}
|
||||||
/**
|
/**
|
||||||
* @generated from protobuf service player.Player
|
* @generated from protobuf service player.Player
|
||||||
@@ -56,19 +85,75 @@ export class PlayerClient implements IPlayerClient, ServiceInfo {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
/**
|
/**
|
||||||
* @generated from protobuf rpc: ResumeTrack(google.protobuf.Empty) returns (google.protobuf.Empty);
|
* @generated from protobuf rpc: ResumeTrack(google.protobuf.Empty) returns (player.PauseState);
|
||||||
*/
|
*/
|
||||||
resumeTrack(input: Empty, options?: RpcOptions): UnaryCall<Empty, Empty> {
|
resumeTrack(input: Empty, options?: RpcOptions): UnaryCall<Empty, PauseState> {
|
||||||
const method = this.methods[1],
|
const method = this.methods[1],
|
||||||
opt = this._transport.mergeOptions(options);
|
opt = this._transport.mergeOptions(options);
|
||||||
return stackIntercept<Empty, Empty>('unary', this._transport, method, opt, input);
|
return stackIntercept<Empty, PauseState>('unary', this._transport, method, opt, input);
|
||||||
}
|
}
|
||||||
/**
|
/**
|
||||||
* @generated from protobuf rpc: PauseTrack(google.protobuf.Empty) returns (google.protobuf.Empty);
|
* @generated from protobuf rpc: PauseTrack(google.protobuf.Empty) returns (player.PauseState);
|
||||||
*/
|
*/
|
||||||
pauseTrack(input: Empty, options?: RpcOptions): UnaryCall<Empty, Empty> {
|
pauseTrack(input: Empty, options?: RpcOptions): UnaryCall<Empty, PauseState> {
|
||||||
const method = this.methods[2],
|
const method = this.methods[2],
|
||||||
opt = this._transport.mergeOptions(options);
|
opt = this._transport.mergeOptions(options);
|
||||||
return stackIntercept<Empty, Empty>('unary', this._transport, method, opt, input);
|
return stackIntercept<Empty, PauseState>('unary', this._transport, method, opt, input);
|
||||||
|
}
|
||||||
|
/**
|
||||||
|
* @generated from protobuf rpc: TogglePause(google.protobuf.Empty) returns (player.PauseState);
|
||||||
|
*/
|
||||||
|
togglePause(input: Empty, options?: RpcOptions): UnaryCall<Empty, PauseState> {
|
||||||
|
const method = this.methods[3],
|
||||||
|
opt = this._transport.mergeOptions(options);
|
||||||
|
return stackIntercept<Empty, PauseState>('unary', this._transport, method, opt, input);
|
||||||
|
}
|
||||||
|
/**
|
||||||
|
* @generated from protobuf rpc: GetStatus(google.protobuf.Empty) returns (stream player.PlayerStatus);
|
||||||
|
*/
|
||||||
|
getStatus(input: Empty, options?: RpcOptions): ServerStreamingCall<Empty, PlayerStatus> {
|
||||||
|
const method = this.methods[4],
|
||||||
|
opt = this._transport.mergeOptions(options);
|
||||||
|
return stackIntercept<Empty, PlayerStatus>(
|
||||||
|
'serverStreaming',
|
||||||
|
this._transport,
|
||||||
|
method,
|
||||||
|
opt,
|
||||||
|
input
|
||||||
|
);
|
||||||
|
}
|
||||||
|
/**
|
||||||
|
* @generated from protobuf rpc: SeekPosition(player.SeekPositionRequest) returns (player.SeekPositionResponse);
|
||||||
|
*/
|
||||||
|
seekPosition(
|
||||||
|
input: SeekPositionRequest,
|
||||||
|
options?: RpcOptions
|
||||||
|
): UnaryCall<SeekPositionRequest, SeekPositionResponse> {
|
||||||
|
const method = this.methods[5],
|
||||||
|
opt = this._transport.mergeOptions(options);
|
||||||
|
return stackIntercept<SeekPositionRequest, SeekPositionResponse>(
|
||||||
|
'unary',
|
||||||
|
this._transport,
|
||||||
|
method,
|
||||||
|
opt,
|
||||||
|
input
|
||||||
|
);
|
||||||
|
}
|
||||||
|
/**
|
||||||
|
* @generated from protobuf rpc: SetVolume(player.SetVolumeRequest) returns (player.SetVolumeResponse);
|
||||||
|
*/
|
||||||
|
setVolume(
|
||||||
|
input: SetVolumeRequest,
|
||||||
|
options?: RpcOptions
|
||||||
|
): UnaryCall<SetVolumeRequest, SetVolumeResponse> {
|
||||||
|
const method = this.methods[6],
|
||||||
|
opt = this._transport.mergeOptions(options);
|
||||||
|
return stackIntercept<SetVolumeRequest, SetVolumeResponse>(
|
||||||
|
'unary',
|
||||||
|
this._transport,
|
||||||
|
method,
|
||||||
|
opt,
|
||||||
|
input
|
||||||
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -12,6 +12,7 @@ import { UnknownFieldHandler } from '@protobuf-ts/runtime';
|
|||||||
import type { PartialMessage } from '@protobuf-ts/runtime';
|
import type { PartialMessage } from '@protobuf-ts/runtime';
|
||||||
import { reflectionMergePartial } from '@protobuf-ts/runtime';
|
import { reflectionMergePartial } from '@protobuf-ts/runtime';
|
||||||
import { MessageType } from '@protobuf-ts/runtime';
|
import { MessageType } from '@protobuf-ts/runtime';
|
||||||
|
import { Track } from './library';
|
||||||
/**
|
/**
|
||||||
* @generated from protobuf message player.PlayTrackRequest
|
* @generated from protobuf message player.PlayTrackRequest
|
||||||
*/
|
*/
|
||||||
@@ -24,7 +25,82 @@ export interface PlayTrackRequest {
|
|||||||
/**
|
/**
|
||||||
* @generated from protobuf message player.PlayTrackResponse
|
* @generated from protobuf message player.PlayTrackResponse
|
||||||
*/
|
*/
|
||||||
export interface PlayTrackResponse {}
|
export interface PlayTrackResponse {
|
||||||
|
/**
|
||||||
|
* @generated from protobuf field: library.Track track = 1;
|
||||||
|
*/
|
||||||
|
track?: Track;
|
||||||
|
/**
|
||||||
|
* @generated from protobuf field: uint64 position = 2;
|
||||||
|
*/
|
||||||
|
position: bigint;
|
||||||
|
}
|
||||||
|
/**
|
||||||
|
* @generated from protobuf message player.PlayerStatus
|
||||||
|
*/
|
||||||
|
export interface PlayerStatus {
|
||||||
|
/**
|
||||||
|
* @generated from protobuf field: optional library.Track currently_playing = 1;
|
||||||
|
*/
|
||||||
|
currentlyPlaying?: Track;
|
||||||
|
/**
|
||||||
|
* @generated from protobuf field: bool is_paused = 2;
|
||||||
|
*/
|
||||||
|
isPaused: boolean;
|
||||||
|
/**
|
||||||
|
* @generated from protobuf field: float volume = 3;
|
||||||
|
*/
|
||||||
|
volume: number;
|
||||||
|
/**
|
||||||
|
* @generated from protobuf field: uint64 progress = 4;
|
||||||
|
*/
|
||||||
|
progress: bigint;
|
||||||
|
}
|
||||||
|
/**
|
||||||
|
* @generated from protobuf message player.PauseState
|
||||||
|
*/
|
||||||
|
export interface PauseState {
|
||||||
|
/**
|
||||||
|
* @generated from protobuf field: bool is_paused = 1;
|
||||||
|
*/
|
||||||
|
isPaused: boolean;
|
||||||
|
}
|
||||||
|
/**
|
||||||
|
* @generated from protobuf message player.SeekPositionRequest
|
||||||
|
*/
|
||||||
|
export interface SeekPositionRequest {
|
||||||
|
/**
|
||||||
|
* @generated from protobuf field: uint64 position = 1;
|
||||||
|
*/
|
||||||
|
position: bigint;
|
||||||
|
}
|
||||||
|
/**
|
||||||
|
* @generated from protobuf message player.SeekPositionResponse
|
||||||
|
*/
|
||||||
|
export interface SeekPositionResponse {
|
||||||
|
/**
|
||||||
|
* @generated from protobuf field: uint64 position = 1;
|
||||||
|
*/
|
||||||
|
position: bigint;
|
||||||
|
}
|
||||||
|
/**
|
||||||
|
* @generated from protobuf message player.SetVolumeRequest
|
||||||
|
*/
|
||||||
|
export interface SetVolumeRequest {
|
||||||
|
/**
|
||||||
|
* @generated from protobuf field: float volume = 1;
|
||||||
|
*/
|
||||||
|
volume: number;
|
||||||
|
}
|
||||||
|
/**
|
||||||
|
* @generated from protobuf message player.SetVolumeResponse
|
||||||
|
*/
|
||||||
|
export interface SetVolumeResponse {
|
||||||
|
/**
|
||||||
|
* @generated from protobuf field: float volume = 1;
|
||||||
|
*/
|
||||||
|
volume: number;
|
||||||
|
}
|
||||||
// @generated message type with reflection information, may provide speed optimized methods
|
// @generated message type with reflection information, may provide speed optimized methods
|
||||||
class PlayTrackRequest$Type extends MessageType<PlayTrackRequest> {
|
class PlayTrackRequest$Type extends MessageType<PlayTrackRequest> {
|
||||||
constructor() {
|
constructor() {
|
||||||
@@ -90,10 +166,20 @@ export const PlayTrackRequest = new PlayTrackRequest$Type();
|
|||||||
// @generated message type with reflection information, may provide speed optimized methods
|
// @generated message type with reflection information, may provide speed optimized methods
|
||||||
class PlayTrackResponse$Type extends MessageType<PlayTrackResponse> {
|
class PlayTrackResponse$Type extends MessageType<PlayTrackResponse> {
|
||||||
constructor() {
|
constructor() {
|
||||||
super('player.PlayTrackResponse', []);
|
super('player.PlayTrackResponse', [
|
||||||
|
{ no: 1, name: 'track', kind: 'message', T: () => Track },
|
||||||
|
{
|
||||||
|
no: 2,
|
||||||
|
name: 'position',
|
||||||
|
kind: 'scalar',
|
||||||
|
T: 4 /*ScalarType.UINT64*/,
|
||||||
|
L: 0 /*LongType.BIGINT*/
|
||||||
|
}
|
||||||
|
]);
|
||||||
}
|
}
|
||||||
create(value?: PartialMessage<PlayTrackResponse>): PlayTrackResponse {
|
create(value?: PartialMessage<PlayTrackResponse>): PlayTrackResponse {
|
||||||
const message = globalThis.Object.create(this.messagePrototype!);
|
const message = globalThis.Object.create(this.messagePrototype!);
|
||||||
|
message.position = 0n;
|
||||||
if (value !== undefined) reflectionMergePartial<PlayTrackResponse>(this, message, value);
|
if (value !== undefined) reflectionMergePartial<PlayTrackResponse>(this, message, value);
|
||||||
return message;
|
return message;
|
||||||
}
|
}
|
||||||
@@ -103,13 +189,50 @@ class PlayTrackResponse$Type extends MessageType<PlayTrackResponse> {
|
|||||||
options: BinaryReadOptions,
|
options: BinaryReadOptions,
|
||||||
target?: PlayTrackResponse
|
target?: PlayTrackResponse
|
||||||
): PlayTrackResponse {
|
): PlayTrackResponse {
|
||||||
return target ?? this.create();
|
let message = target ?? this.create(),
|
||||||
|
end = reader.pos + length;
|
||||||
|
while (reader.pos < end) {
|
||||||
|
let [fieldNo, wireType] = reader.tag();
|
||||||
|
switch (fieldNo) {
|
||||||
|
case /* library.Track track */ 1:
|
||||||
|
message.track = Track.internalBinaryRead(reader, reader.uint32(), options, message.track);
|
||||||
|
break;
|
||||||
|
case /* uint64 position */ 2:
|
||||||
|
message.position = reader.uint64().toBigInt();
|
||||||
|
break;
|
||||||
|
default:
|
||||||
|
let u = options.readUnknownField;
|
||||||
|
if (u === 'throw')
|
||||||
|
throw new globalThis.Error(
|
||||||
|
`Unknown field ${fieldNo} (wire type ${wireType}) for ${this.typeName}`
|
||||||
|
);
|
||||||
|
let d = reader.skip(wireType);
|
||||||
|
if (u !== false)
|
||||||
|
(u === true ? UnknownFieldHandler.onRead : u)(
|
||||||
|
this.typeName,
|
||||||
|
message,
|
||||||
|
fieldNo,
|
||||||
|
wireType,
|
||||||
|
d
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return message;
|
||||||
}
|
}
|
||||||
internalBinaryWrite(
|
internalBinaryWrite(
|
||||||
message: PlayTrackResponse,
|
message: PlayTrackResponse,
|
||||||
writer: IBinaryWriter,
|
writer: IBinaryWriter,
|
||||||
options: BinaryWriteOptions
|
options: BinaryWriteOptions
|
||||||
): IBinaryWriter {
|
): IBinaryWriter {
|
||||||
|
/* library.Track track = 1; */
|
||||||
|
if (message.track)
|
||||||
|
Track.internalBinaryWrite(
|
||||||
|
message.track,
|
||||||
|
writer.tag(1, WireType.LengthDelimited).fork(),
|
||||||
|
options
|
||||||
|
).join();
|
||||||
|
/* uint64 position = 2; */
|
||||||
|
if (message.position !== 0n) writer.tag(2, WireType.Varint).uint64(message.position);
|
||||||
let u = options.writeUnknownFields;
|
let u = options.writeUnknownFields;
|
||||||
if (u !== false) (u == true ? UnknownFieldHandler.onWrite : u)(this.typeName, message, writer);
|
if (u !== false) (u == true ? UnknownFieldHandler.onWrite : u)(this.typeName, message, writer);
|
||||||
return writer;
|
return writer;
|
||||||
@@ -119,11 +242,435 @@ class PlayTrackResponse$Type extends MessageType<PlayTrackResponse> {
|
|||||||
* @generated MessageType for protobuf message player.PlayTrackResponse
|
* @generated MessageType for protobuf message player.PlayTrackResponse
|
||||||
*/
|
*/
|
||||||
export const PlayTrackResponse = new PlayTrackResponse$Type();
|
export const PlayTrackResponse = new PlayTrackResponse$Type();
|
||||||
|
// @generated message type with reflection information, may provide speed optimized methods
|
||||||
|
class PlayerStatus$Type extends MessageType<PlayerStatus> {
|
||||||
|
constructor() {
|
||||||
|
super('player.PlayerStatus', [
|
||||||
|
{ no: 1, name: 'currently_playing', kind: 'message', T: () => Track },
|
||||||
|
{ no: 2, name: 'is_paused', kind: 'scalar', T: 8 /*ScalarType.BOOL*/ },
|
||||||
|
{ no: 3, name: 'volume', kind: 'scalar', T: 2 /*ScalarType.FLOAT*/ },
|
||||||
|
{
|
||||||
|
no: 4,
|
||||||
|
name: 'progress',
|
||||||
|
kind: 'scalar',
|
||||||
|
T: 4 /*ScalarType.UINT64*/,
|
||||||
|
L: 0 /*LongType.BIGINT*/
|
||||||
|
}
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
create(value?: PartialMessage<PlayerStatus>): PlayerStatus {
|
||||||
|
const message = globalThis.Object.create(this.messagePrototype!);
|
||||||
|
message.isPaused = false;
|
||||||
|
message.volume = 0;
|
||||||
|
message.progress = 0n;
|
||||||
|
if (value !== undefined) reflectionMergePartial<PlayerStatus>(this, message, value);
|
||||||
|
return message;
|
||||||
|
}
|
||||||
|
internalBinaryRead(
|
||||||
|
reader: IBinaryReader,
|
||||||
|
length: number,
|
||||||
|
options: BinaryReadOptions,
|
||||||
|
target?: PlayerStatus
|
||||||
|
): PlayerStatus {
|
||||||
|
let message = target ?? this.create(),
|
||||||
|
end = reader.pos + length;
|
||||||
|
while (reader.pos < end) {
|
||||||
|
let [fieldNo, wireType] = reader.tag();
|
||||||
|
switch (fieldNo) {
|
||||||
|
case /* optional library.Track currently_playing */ 1:
|
||||||
|
message.currentlyPlaying = Track.internalBinaryRead(
|
||||||
|
reader,
|
||||||
|
reader.uint32(),
|
||||||
|
options,
|
||||||
|
message.currentlyPlaying
|
||||||
|
);
|
||||||
|
break;
|
||||||
|
case /* bool is_paused */ 2:
|
||||||
|
message.isPaused = reader.bool();
|
||||||
|
break;
|
||||||
|
case /* float volume */ 3:
|
||||||
|
message.volume = reader.float();
|
||||||
|
break;
|
||||||
|
case /* uint64 progress */ 4:
|
||||||
|
message.progress = reader.uint64().toBigInt();
|
||||||
|
break;
|
||||||
|
default:
|
||||||
|
let u = options.readUnknownField;
|
||||||
|
if (u === 'throw')
|
||||||
|
throw new globalThis.Error(
|
||||||
|
`Unknown field ${fieldNo} (wire type ${wireType}) for ${this.typeName}`
|
||||||
|
);
|
||||||
|
let d = reader.skip(wireType);
|
||||||
|
if (u !== false)
|
||||||
|
(u === true ? UnknownFieldHandler.onRead : u)(
|
||||||
|
this.typeName,
|
||||||
|
message,
|
||||||
|
fieldNo,
|
||||||
|
wireType,
|
||||||
|
d
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return message;
|
||||||
|
}
|
||||||
|
internalBinaryWrite(
|
||||||
|
message: PlayerStatus,
|
||||||
|
writer: IBinaryWriter,
|
||||||
|
options: BinaryWriteOptions
|
||||||
|
): IBinaryWriter {
|
||||||
|
/* optional library.Track currently_playing = 1; */
|
||||||
|
if (message.currentlyPlaying)
|
||||||
|
Track.internalBinaryWrite(
|
||||||
|
message.currentlyPlaying,
|
||||||
|
writer.tag(1, WireType.LengthDelimited).fork(),
|
||||||
|
options
|
||||||
|
).join();
|
||||||
|
/* bool is_paused = 2; */
|
||||||
|
if (message.isPaused !== false) writer.tag(2, WireType.Varint).bool(message.isPaused);
|
||||||
|
/* float volume = 3; */
|
||||||
|
if (message.volume !== 0) writer.tag(3, WireType.Bit32).float(message.volume);
|
||||||
|
/* uint64 progress = 4; */
|
||||||
|
if (message.progress !== 0n) writer.tag(4, WireType.Varint).uint64(message.progress);
|
||||||
|
let u = options.writeUnknownFields;
|
||||||
|
if (u !== false) (u == true ? UnknownFieldHandler.onWrite : u)(this.typeName, message, writer);
|
||||||
|
return writer;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
/**
|
||||||
|
* @generated MessageType for protobuf message player.PlayerStatus
|
||||||
|
*/
|
||||||
|
export const PlayerStatus = new PlayerStatus$Type();
|
||||||
|
// @generated message type with reflection information, may provide speed optimized methods
|
||||||
|
class PauseState$Type extends MessageType<PauseState> {
|
||||||
|
constructor() {
|
||||||
|
super('player.PauseState', [
|
||||||
|
{ no: 1, name: 'is_paused', kind: 'scalar', T: 8 /*ScalarType.BOOL*/ }
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
create(value?: PartialMessage<PauseState>): PauseState {
|
||||||
|
const message = globalThis.Object.create(this.messagePrototype!);
|
||||||
|
message.isPaused = false;
|
||||||
|
if (value !== undefined) reflectionMergePartial<PauseState>(this, message, value);
|
||||||
|
return message;
|
||||||
|
}
|
||||||
|
internalBinaryRead(
|
||||||
|
reader: IBinaryReader,
|
||||||
|
length: number,
|
||||||
|
options: BinaryReadOptions,
|
||||||
|
target?: PauseState
|
||||||
|
): PauseState {
|
||||||
|
let message = target ?? this.create(),
|
||||||
|
end = reader.pos + length;
|
||||||
|
while (reader.pos < end) {
|
||||||
|
let [fieldNo, wireType] = reader.tag();
|
||||||
|
switch (fieldNo) {
|
||||||
|
case /* bool is_paused */ 1:
|
||||||
|
message.isPaused = reader.bool();
|
||||||
|
break;
|
||||||
|
default:
|
||||||
|
let u = options.readUnknownField;
|
||||||
|
if (u === 'throw')
|
||||||
|
throw new globalThis.Error(
|
||||||
|
`Unknown field ${fieldNo} (wire type ${wireType}) for ${this.typeName}`
|
||||||
|
);
|
||||||
|
let d = reader.skip(wireType);
|
||||||
|
if (u !== false)
|
||||||
|
(u === true ? UnknownFieldHandler.onRead : u)(
|
||||||
|
this.typeName,
|
||||||
|
message,
|
||||||
|
fieldNo,
|
||||||
|
wireType,
|
||||||
|
d
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return message;
|
||||||
|
}
|
||||||
|
internalBinaryWrite(
|
||||||
|
message: PauseState,
|
||||||
|
writer: IBinaryWriter,
|
||||||
|
options: BinaryWriteOptions
|
||||||
|
): IBinaryWriter {
|
||||||
|
/* bool is_paused = 1; */
|
||||||
|
if (message.isPaused !== false) writer.tag(1, WireType.Varint).bool(message.isPaused);
|
||||||
|
let u = options.writeUnknownFields;
|
||||||
|
if (u !== false) (u == true ? UnknownFieldHandler.onWrite : u)(this.typeName, message, writer);
|
||||||
|
return writer;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
/**
|
||||||
|
* @generated MessageType for protobuf message player.PauseState
|
||||||
|
*/
|
||||||
|
export const PauseState = new PauseState$Type();
|
||||||
|
// @generated message type with reflection information, may provide speed optimized methods
|
||||||
|
class SeekPositionRequest$Type extends MessageType<SeekPositionRequest> {
|
||||||
|
constructor() {
|
||||||
|
super('player.SeekPositionRequest', [
|
||||||
|
{
|
||||||
|
no: 1,
|
||||||
|
name: 'position',
|
||||||
|
kind: 'scalar',
|
||||||
|
T: 4 /*ScalarType.UINT64*/,
|
||||||
|
L: 0 /*LongType.BIGINT*/
|
||||||
|
}
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
create(value?: PartialMessage<SeekPositionRequest>): SeekPositionRequest {
|
||||||
|
const message = globalThis.Object.create(this.messagePrototype!);
|
||||||
|
message.position = 0n;
|
||||||
|
if (value !== undefined) reflectionMergePartial<SeekPositionRequest>(this, message, value);
|
||||||
|
return message;
|
||||||
|
}
|
||||||
|
internalBinaryRead(
|
||||||
|
reader: IBinaryReader,
|
||||||
|
length: number,
|
||||||
|
options: BinaryReadOptions,
|
||||||
|
target?: SeekPositionRequest
|
||||||
|
): SeekPositionRequest {
|
||||||
|
let message = target ?? this.create(),
|
||||||
|
end = reader.pos + length;
|
||||||
|
while (reader.pos < end) {
|
||||||
|
let [fieldNo, wireType] = reader.tag();
|
||||||
|
switch (fieldNo) {
|
||||||
|
case /* uint64 position */ 1:
|
||||||
|
message.position = reader.uint64().toBigInt();
|
||||||
|
break;
|
||||||
|
default:
|
||||||
|
let u = options.readUnknownField;
|
||||||
|
if (u === 'throw')
|
||||||
|
throw new globalThis.Error(
|
||||||
|
`Unknown field ${fieldNo} (wire type ${wireType}) for ${this.typeName}`
|
||||||
|
);
|
||||||
|
let d = reader.skip(wireType);
|
||||||
|
if (u !== false)
|
||||||
|
(u === true ? UnknownFieldHandler.onRead : u)(
|
||||||
|
this.typeName,
|
||||||
|
message,
|
||||||
|
fieldNo,
|
||||||
|
wireType,
|
||||||
|
d
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return message;
|
||||||
|
}
|
||||||
|
internalBinaryWrite(
|
||||||
|
message: SeekPositionRequest,
|
||||||
|
writer: IBinaryWriter,
|
||||||
|
options: BinaryWriteOptions
|
||||||
|
): IBinaryWriter {
|
||||||
|
/* uint64 position = 1; */
|
||||||
|
if (message.position !== 0n) writer.tag(1, WireType.Varint).uint64(message.position);
|
||||||
|
let u = options.writeUnknownFields;
|
||||||
|
if (u !== false) (u == true ? UnknownFieldHandler.onWrite : u)(this.typeName, message, writer);
|
||||||
|
return writer;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
/**
|
||||||
|
* @generated MessageType for protobuf message player.SeekPositionRequest
|
||||||
|
*/
|
||||||
|
export const SeekPositionRequest = new SeekPositionRequest$Type();
|
||||||
|
// @generated message type with reflection information, may provide speed optimized methods
|
||||||
|
class SeekPositionResponse$Type extends MessageType<SeekPositionResponse> {
|
||||||
|
constructor() {
|
||||||
|
super('player.SeekPositionResponse', [
|
||||||
|
{
|
||||||
|
no: 1,
|
||||||
|
name: 'position',
|
||||||
|
kind: 'scalar',
|
||||||
|
T: 4 /*ScalarType.UINT64*/,
|
||||||
|
L: 0 /*LongType.BIGINT*/
|
||||||
|
}
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
create(value?: PartialMessage<SeekPositionResponse>): SeekPositionResponse {
|
||||||
|
const message = globalThis.Object.create(this.messagePrototype!);
|
||||||
|
message.position = 0n;
|
||||||
|
if (value !== undefined) reflectionMergePartial<SeekPositionResponse>(this, message, value);
|
||||||
|
return message;
|
||||||
|
}
|
||||||
|
internalBinaryRead(
|
||||||
|
reader: IBinaryReader,
|
||||||
|
length: number,
|
||||||
|
options: BinaryReadOptions,
|
||||||
|
target?: SeekPositionResponse
|
||||||
|
): SeekPositionResponse {
|
||||||
|
let message = target ?? this.create(),
|
||||||
|
end = reader.pos + length;
|
||||||
|
while (reader.pos < end) {
|
||||||
|
let [fieldNo, wireType] = reader.tag();
|
||||||
|
switch (fieldNo) {
|
||||||
|
case /* uint64 position */ 1:
|
||||||
|
message.position = reader.uint64().toBigInt();
|
||||||
|
break;
|
||||||
|
default:
|
||||||
|
let u = options.readUnknownField;
|
||||||
|
if (u === 'throw')
|
||||||
|
throw new globalThis.Error(
|
||||||
|
`Unknown field ${fieldNo} (wire type ${wireType}) for ${this.typeName}`
|
||||||
|
);
|
||||||
|
let d = reader.skip(wireType);
|
||||||
|
if (u !== false)
|
||||||
|
(u === true ? UnknownFieldHandler.onRead : u)(
|
||||||
|
this.typeName,
|
||||||
|
message,
|
||||||
|
fieldNo,
|
||||||
|
wireType,
|
||||||
|
d
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return message;
|
||||||
|
}
|
||||||
|
internalBinaryWrite(
|
||||||
|
message: SeekPositionResponse,
|
||||||
|
writer: IBinaryWriter,
|
||||||
|
options: BinaryWriteOptions
|
||||||
|
): IBinaryWriter {
|
||||||
|
/* uint64 position = 1; */
|
||||||
|
if (message.position !== 0n) writer.tag(1, WireType.Varint).uint64(message.position);
|
||||||
|
let u = options.writeUnknownFields;
|
||||||
|
if (u !== false) (u == true ? UnknownFieldHandler.onWrite : u)(this.typeName, message, writer);
|
||||||
|
return writer;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
/**
|
||||||
|
* @generated MessageType for protobuf message player.SeekPositionResponse
|
||||||
|
*/
|
||||||
|
export const SeekPositionResponse = new SeekPositionResponse$Type();
|
||||||
|
// @generated message type with reflection information, may provide speed optimized methods
|
||||||
|
class SetVolumeRequest$Type extends MessageType<SetVolumeRequest> {
|
||||||
|
constructor() {
|
||||||
|
super('player.SetVolumeRequest', [
|
||||||
|
{ no: 1, name: 'volume', kind: 'scalar', T: 2 /*ScalarType.FLOAT*/ }
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
create(value?: PartialMessage<SetVolumeRequest>): SetVolumeRequest {
|
||||||
|
const message = globalThis.Object.create(this.messagePrototype!);
|
||||||
|
message.volume = 0;
|
||||||
|
if (value !== undefined) reflectionMergePartial<SetVolumeRequest>(this, message, value);
|
||||||
|
return message;
|
||||||
|
}
|
||||||
|
internalBinaryRead(
|
||||||
|
reader: IBinaryReader,
|
||||||
|
length: number,
|
||||||
|
options: BinaryReadOptions,
|
||||||
|
target?: SetVolumeRequest
|
||||||
|
): SetVolumeRequest {
|
||||||
|
let message = target ?? this.create(),
|
||||||
|
end = reader.pos + length;
|
||||||
|
while (reader.pos < end) {
|
||||||
|
let [fieldNo, wireType] = reader.tag();
|
||||||
|
switch (fieldNo) {
|
||||||
|
case /* float volume */ 1:
|
||||||
|
message.volume = reader.float();
|
||||||
|
break;
|
||||||
|
default:
|
||||||
|
let u = options.readUnknownField;
|
||||||
|
if (u === 'throw')
|
||||||
|
throw new globalThis.Error(
|
||||||
|
`Unknown field ${fieldNo} (wire type ${wireType}) for ${this.typeName}`
|
||||||
|
);
|
||||||
|
let d = reader.skip(wireType);
|
||||||
|
if (u !== false)
|
||||||
|
(u === true ? UnknownFieldHandler.onRead : u)(
|
||||||
|
this.typeName,
|
||||||
|
message,
|
||||||
|
fieldNo,
|
||||||
|
wireType,
|
||||||
|
d
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return message;
|
||||||
|
}
|
||||||
|
internalBinaryWrite(
|
||||||
|
message: SetVolumeRequest,
|
||||||
|
writer: IBinaryWriter,
|
||||||
|
options: BinaryWriteOptions
|
||||||
|
): IBinaryWriter {
|
||||||
|
/* float volume = 1; */
|
||||||
|
if (message.volume !== 0) writer.tag(1, WireType.Bit32).float(message.volume);
|
||||||
|
let u = options.writeUnknownFields;
|
||||||
|
if (u !== false) (u == true ? UnknownFieldHandler.onWrite : u)(this.typeName, message, writer);
|
||||||
|
return writer;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
/**
|
||||||
|
* @generated MessageType for protobuf message player.SetVolumeRequest
|
||||||
|
*/
|
||||||
|
export const SetVolumeRequest = new SetVolumeRequest$Type();
|
||||||
|
// @generated message type with reflection information, may provide speed optimized methods
|
||||||
|
class SetVolumeResponse$Type extends MessageType<SetVolumeResponse> {
|
||||||
|
constructor() {
|
||||||
|
super('player.SetVolumeResponse', [
|
||||||
|
{ no: 1, name: 'volume', kind: 'scalar', T: 2 /*ScalarType.FLOAT*/ }
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
create(value?: PartialMessage<SetVolumeResponse>): SetVolumeResponse {
|
||||||
|
const message = globalThis.Object.create(this.messagePrototype!);
|
||||||
|
message.volume = 0;
|
||||||
|
if (value !== undefined) reflectionMergePartial<SetVolumeResponse>(this, message, value);
|
||||||
|
return message;
|
||||||
|
}
|
||||||
|
internalBinaryRead(
|
||||||
|
reader: IBinaryReader,
|
||||||
|
length: number,
|
||||||
|
options: BinaryReadOptions,
|
||||||
|
target?: SetVolumeResponse
|
||||||
|
): SetVolumeResponse {
|
||||||
|
let message = target ?? this.create(),
|
||||||
|
end = reader.pos + length;
|
||||||
|
while (reader.pos < end) {
|
||||||
|
let [fieldNo, wireType] = reader.tag();
|
||||||
|
switch (fieldNo) {
|
||||||
|
case /* float volume */ 1:
|
||||||
|
message.volume = reader.float();
|
||||||
|
break;
|
||||||
|
default:
|
||||||
|
let u = options.readUnknownField;
|
||||||
|
if (u === 'throw')
|
||||||
|
throw new globalThis.Error(
|
||||||
|
`Unknown field ${fieldNo} (wire type ${wireType}) for ${this.typeName}`
|
||||||
|
);
|
||||||
|
let d = reader.skip(wireType);
|
||||||
|
if (u !== false)
|
||||||
|
(u === true ? UnknownFieldHandler.onRead : u)(
|
||||||
|
this.typeName,
|
||||||
|
message,
|
||||||
|
fieldNo,
|
||||||
|
wireType,
|
||||||
|
d
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return message;
|
||||||
|
}
|
||||||
|
internalBinaryWrite(
|
||||||
|
message: SetVolumeResponse,
|
||||||
|
writer: IBinaryWriter,
|
||||||
|
options: BinaryWriteOptions
|
||||||
|
): IBinaryWriter {
|
||||||
|
/* float volume = 1; */
|
||||||
|
if (message.volume !== 0) writer.tag(1, WireType.Bit32).float(message.volume);
|
||||||
|
let u = options.writeUnknownFields;
|
||||||
|
if (u !== false) (u == true ? UnknownFieldHandler.onWrite : u)(this.typeName, message, writer);
|
||||||
|
return writer;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
/**
|
||||||
|
* @generated MessageType for protobuf message player.SetVolumeResponse
|
||||||
|
*/
|
||||||
|
export const SetVolumeResponse = new SetVolumeResponse$Type();
|
||||||
/**
|
/**
|
||||||
* @generated ServiceType for protobuf service player.Player
|
* @generated ServiceType for protobuf service player.Player
|
||||||
*/
|
*/
|
||||||
export const Player = new ServiceType('player.Player', [
|
export const Player = new ServiceType('player.Player', [
|
||||||
{ name: 'PlayTrack', options: {}, I: PlayTrackRequest, O: PlayTrackResponse },
|
{ name: 'PlayTrack', options: {}, I: PlayTrackRequest, O: PlayTrackResponse },
|
||||||
{ name: 'ResumeTrack', options: {}, I: Empty, O: Empty },
|
{ name: 'ResumeTrack', options: {}, I: Empty, O: PauseState },
|
||||||
{ name: 'PauseTrack', options: {}, I: Empty, O: Empty }
|
{ name: 'PauseTrack', options: {}, I: Empty, O: PauseState },
|
||||||
|
{ name: 'TogglePause', options: {}, I: Empty, O: PauseState },
|
||||||
|
{ name: 'GetStatus', serverStreaming: true, options: {}, I: Empty, O: PlayerStatus },
|
||||||
|
{ name: 'SeekPosition', options: {}, I: SeekPositionRequest, O: SeekPositionResponse },
|
||||||
|
{ name: 'SetVolume', options: {}, I: SetVolumeRequest, O: SetVolumeResponse }
|
||||||
]);
|
]);
|
||||||
|
|||||||
@@ -1,4 +0,0 @@
|
|||||||
import { writable } from 'svelte/store';
|
|
||||||
|
|
||||||
export const currentlyPlaying = writable<string | null>(null);
|
|
||||||
export const volume = writable<number>(1.0);
|
|
||||||
@@ -5,8 +5,17 @@
|
|||||||
import AppSidebar from '$lib/components/groove/AppSidebar.svelte';
|
import AppSidebar from '$lib/components/groove/AppSidebar.svelte';
|
||||||
import Footer from '$lib/components/groove/Footer.svelte';
|
import Footer from '$lib/components/groove/Footer.svelte';
|
||||||
import { ScrollArea } from '$lib/components/ui/scroll-area';
|
import { ScrollArea } from '$lib/components/ui/scroll-area';
|
||||||
|
import { getPlayerState, setPlayerState } from '$lib/player.svelte';
|
||||||
|
import { onMount } from 'svelte';
|
||||||
|
|
||||||
let { children } = $props();
|
let { children } = $props();
|
||||||
|
|
||||||
|
setPlayerState();
|
||||||
|
const player = getPlayerState();
|
||||||
|
|
||||||
|
onMount(() => {
|
||||||
|
return () => player.abort();
|
||||||
|
});
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<ModeWatcher />
|
<ModeWatcher />
|
||||||
|
|||||||
1
src/routes/+layout.ts
Normal file
1
src/routes/+layout.ts
Normal file
@@ -0,0 +1 @@
|
|||||||
|
export const ssr = false;
|
||||||
@@ -1,4 +1,5 @@
|
|||||||
import { PlayerClient } from '$lib/proto/player.client';
|
import { PlayerClient } from '$lib/proto/player.client';
|
||||||
|
import { fail } from '@sveltejs/kit';
|
||||||
import { protoTransport } from '../../hooks.server';
|
import { protoTransport } from '../../hooks.server';
|
||||||
import type { Actions } from './$types';
|
import type { Actions } from './$types';
|
||||||
|
|
||||||
@@ -6,11 +7,55 @@ export const actions = {
|
|||||||
resume: async () => {
|
resume: async () => {
|
||||||
const client = new PlayerClient(protoTransport);
|
const client = new PlayerClient(protoTransport);
|
||||||
|
|
||||||
await client.resumeTrack({});
|
const response = await client.resumeTrack({});
|
||||||
|
|
||||||
|
return {
|
||||||
|
isPaused: response.response.isPaused
|
||||||
|
};
|
||||||
},
|
},
|
||||||
pause: async () => {
|
pause: async () => {
|
||||||
const client = new PlayerClient(protoTransport);
|
const client = new PlayerClient(protoTransport);
|
||||||
|
|
||||||
await client.pauseTrack({});
|
const response = await client.pauseTrack({});
|
||||||
|
|
||||||
|
return {
|
||||||
|
isPaused: response.response.isPaused
|
||||||
|
};
|
||||||
|
},
|
||||||
|
seek: async ({ request }) => {
|
||||||
|
const formData = await request.formData();
|
||||||
|
|
||||||
|
const position = formData.get('position')?.toString();
|
||||||
|
|
||||||
|
if (!position) {
|
||||||
|
return fail(400);
|
||||||
|
}
|
||||||
|
|
||||||
|
const client = new PlayerClient(protoTransport);
|
||||||
|
|
||||||
|
const response = await client.seekPosition({ position: BigInt(position) });
|
||||||
|
|
||||||
|
return {
|
||||||
|
position: response.response.position
|
||||||
|
};
|
||||||
|
},
|
||||||
|
volume: async ({ request }) => {
|
||||||
|
const formData = await request.formData();
|
||||||
|
|
||||||
|
const volume = formData.get('volume')?.toString();
|
||||||
|
|
||||||
|
if (!volume) {
|
||||||
|
return fail(400);
|
||||||
|
}
|
||||||
|
|
||||||
|
const client = new PlayerClient(protoTransport);
|
||||||
|
|
||||||
|
const response = await client.setVolume({
|
||||||
|
volume: parseFloat(volume)
|
||||||
|
});
|
||||||
|
|
||||||
|
return {
|
||||||
|
volume: response.response.volume
|
||||||
|
};
|
||||||
}
|
}
|
||||||
} satisfies Actions;
|
} satisfies Actions;
|
||||||
|
|||||||
@@ -1,13 +1,22 @@
|
|||||||
import { PlayerClient } from '$lib/proto/player.client';
|
import { PlayerClient } from '$lib/proto/player.client';
|
||||||
|
import { fail } from '@sveltejs/kit';
|
||||||
import { protoTransport } from '../../../hooks.server';
|
import { protoTransport } from '../../../hooks.server';
|
||||||
import type { Actions } from './$types';
|
import type { Actions } from './$types';
|
||||||
|
import { serializable } from '$lib/proto';
|
||||||
|
import type { PlayTrackResponse } from '$lib/proto/player';
|
||||||
|
|
||||||
export const actions = {
|
export const actions = {
|
||||||
play: async ({ params }) => {
|
play: async ({ params }) => {
|
||||||
const client = new PlayerClient(protoTransport);
|
const client = new PlayerClient(protoTransport);
|
||||||
|
|
||||||
await client.playTrack({
|
const response = await client.playTrack({
|
||||||
hash: params.hash
|
hash: params.hash
|
||||||
});
|
});
|
||||||
|
|
||||||
|
if (response.response.track === undefined) {
|
||||||
|
return fail(500);
|
||||||
|
}
|
||||||
|
|
||||||
|
return serializable<PlayTrackResponse>(response.response);
|
||||||
}
|
}
|
||||||
} satisfies Actions;
|
} satisfies Actions;
|
||||||
|
|||||||
Reference in New Issue
Block a user