feat: currently playing + volume + seek position + toggle pause

This commit is contained in:
2024-11-24 23:35:14 +01:00
parent 7ae47d02f2
commit 2b471c6296
19 changed files with 1015 additions and 77 deletions

View File

@@ -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/

View File

@@ -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",

View File

@@ -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;
} }

View File

@@ -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;
} }

View File

@@ -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
View File

@@ -0,0 +1,5 @@
import { GrpcWebFetchTransport } from '@protobuf-ts/grpcweb-transport';
export const protoTransport = new GrpcWebFetchTransport({
baseUrl: 'http://[::1]:39993'
});

View File

@@ -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 />

View File

@@ -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 () => {
get value() { return async ({ update, result }) => {
return [$volume]; await update({
}, invalidateAll: false
set value(v: number[]) { });
$volume = v[0];
// TODO: Send message to player 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() {
return [player.volume];
},
set value(v: number[]) {
player.volume = v[0];
}
});
</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"
method="POST"
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> </button>
<span class="w-12 text-right text-sm">{dayjs.duration(0, 'seconds').format('mm:ss')}</span> </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} />

View File

@@ -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
View 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
View 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;
}

View File

@@ -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;

View File

@@ -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
);
} }
} }

View File

@@ -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 }
]); ]);

View File

@@ -1,4 +0,0 @@
import { writable } from 'svelte/store';
export const currentlyPlaying = writable<string | null>(null);
export const volume = writable<number>(1.0);

View File

@@ -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
View File

@@ -0,0 +1 @@
export const ssr = false;

View File

@@ -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;

View File

@@ -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;