diff --git a/.prettierignore b/.prettierignore index ab78a95..45c1750 100644 --- a/.prettierignore +++ b/.prettierignore @@ -2,3 +2,5 @@ package-lock.json pnpm-lock.yaml yarn.lock +src/lib/proto/ +src/lib/components/ui/ diff --git a/package.json b/package.json index f97d1d4..5968557 100644 --- a/package.json +++ b/package.json @@ -8,7 +8,8 @@ "preview": "vite preview", "check": "svelte-kit sync && svelte-check --tsconfig ./tsconfig.json", "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": { "@sveltejs/adapter-auto": "^3.0.0", diff --git a/protos/library.proto b/protos/library.proto index 3787455..ee0ed1d 100644 --- a/protos/library.proto +++ b/protos/library.proto @@ -17,4 +17,5 @@ message Track { string name = 2; string artist_name = 3; uint64 artist_id = 4; + uint64 duration = 5; } diff --git a/protos/player.proto b/protos/player.proto index 837622d..087e54c 100644 --- a/protos/player.proto +++ b/protos/player.proto @@ -1,13 +1,18 @@ syntax = "proto3"; import 'google/protobuf/empty.proto'; +import 'library.proto'; package player; service Player { rpc PlayTrack(PlayTrackRequest) returns (PlayTrackResponse); - rpc ResumeTrack(google.protobuf.Empty) returns (google.protobuf.Empty); - rpc PauseTrack(google.protobuf.Empty) returns (google.protobuf.Empty); + rpc ResumeTrack(google.protobuf.Empty) returns (PauseState); + 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 { @@ -15,4 +20,33 @@ message PlayTrackRequest { } 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; } diff --git a/src/app.css b/src/app.css index 1f1ad5f..81f02c7 100644 --- a/src/app.css +++ b/src/app.css @@ -26,10 +26,10 @@ --sidebar-background: 0 0% 98%; --sidebar-foreground: 240 5.3% 26.1%; - --sidebar-primary: 240 5.9% 10%; - --sidebar-primary-foreground: 0 0% 98%; - --sidebar-accent: 240 4.8% 95.9%; - --sidebar-accent-foreground: 240 5.9% 10%; + --sidebar-primary: 217.2 91.2% 59.8%; + --sidebar-primary-foreground: 222.2 47.4% 11.2%; + --sidebar-accent: 217.2 91.2% 59.8%; + --sidebar-accent-foreground: 222.2 47.4% 11.2%; --sidebar-border: 220 13% 91%; --sidebar-ring: 217.2 91.2% 59.8%; @@ -56,12 +56,12 @@ --input: 217.2 32.6% 17.5%; --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-primary: 224.3 76.3% 48%; - --sidebar-primary-foreground: 0 0% 100%; - --sidebar-accent: 240 3.7% 15.9%; - --sidebar-accent-foreground: 240 4.8% 95.9%; + --sidebar-primary: 217.2 91.2% 59.8%; + --sidebar-primary-foreground: 222.2 47.4% 11.2%; + --sidebar-accent: 217.2 91.2% 59.8%; + --sidebar-accent-foreground: 222.2 47.4% 11.2%; --sidebar-border: 240 3.7% 15.9%; --sidebar-ring: 217.2 91.2% 59.8%; } diff --git a/src/hooks.client.ts b/src/hooks.client.ts new file mode 100644 index 0000000..ebfd068 --- /dev/null +++ b/src/hooks.client.ts @@ -0,0 +1,5 @@ +import { GrpcWebFetchTransport } from '@protobuf-ts/grpcweb-transport'; + +export const protoTransport = new GrpcWebFetchTransport({ + baseUrl: 'http://[::1]:39993' +}); diff --git a/src/lib/components/groove/AppSidebar.svelte b/src/lib/components/groove/AppSidebar.svelte index 09c5f7f..8a79142 100644 --- a/src/lib/components/groove/AppSidebar.svelte +++ b/src/lib/components/groove/AppSidebar.svelte @@ -9,6 +9,9 @@ + {#snippet tooltipContent()} + Home + {/snippet} {#snippet child({ props })} @@ -25,6 +28,9 @@ + {#snippet tooltipContent()} + Songs + {/snippet} {#snippet child({ props })} @@ -35,6 +41,9 @@ + {#snippet tooltipContent()} + Albums + {/snippet} {#snippet child({ props })} @@ -45,6 +54,9 @@ + {#snippet tooltipContent()} + Playlists + {/snippet} {#snippet child({ props })} @@ -60,6 +72,9 @@ + {#snippet tooltipContent()} + Settings + {/snippet} {#snippet child({ props })} diff --git a/src/lib/components/groove/Footer.svelte b/src/lib/components/groove/Footer.svelte index 718cacf..e25376f 100644 --- a/src/lib/components/groove/Footer.svelte +++ b/src/lib/components/groove/Footer.svelte @@ -16,65 +16,117 @@ import { Slider } from '$lib/components/ui/slider'; import * as Popover from '$lib/components/ui/popover'; 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 { getCoverUrl } from '$lib/covers'; + import type { SubmitFunction } from '../../../routes/player/$types'; 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) { - if (!$currentlyPlaying) { + let mouse = $state({ + offsetX: 0, + offsetY: 0 + }); + + let progressBar = $state(null); + let volumeForm = $state(null); + + const seekProgressValue: SubmitFunction = ({ formData, cancel }) => { + if (!player.currentlyPlaying || !progressBar) { + cancel(); return; } - const target = e.currentTarget! as HTMLElement; - const _targetProgress = Math.max(0.0, Math.min(1.0, e.offsetX / target.offsetWidth)); - } */ + const targetProgress = Math.round( + 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) { 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() { - return [$volume]; + return [player.volume]; }, set value(v: number[]) { - $volume = v[0]; - // TODO: Send message to player + player.volume = v[0]; } - }; + });
- {dayjs.duration(0, 'seconds').format('mm:ss')} - - {dayjs.duration(0, 'seconds').format('mm:ss')} + {dayjs.duration(Number(player.progress), 'milliseconds').format('mm:ss')} +
+ +
+ {dayjs.duration(trackDuration, 'milliseconds').format('mm:ss')}
+ + diff --git a/src/lib/components/groove/SongListing.svelte b/src/lib/components/groove/SongListing.svelte index de31017..00b5a9e 100644 --- a/src/lib/components/groove/SongListing.svelte +++ b/src/lib/components/groove/SongListing.svelte @@ -1,9 +1,11 @@ -
+
-