Compare commits
4 Commits
26d815f2e3
...
master
| Author | SHA1 | Date | |
|---|---|---|---|
| 301bc9f51a | |||
| 4cb4daaa80 | |||
| 4ffad5c19b | |||
| 2411c5e01c |
40
README.md
40
README.md
@@ -1,38 +1,6 @@
|
||||
# sv
|
||||
# groove-web
|
||||
|
||||
Everything you need to build a Svelte project, powered by [`sv`](https://github.com/sveltejs/cli).
|
||||
An audio player for local files. Uses [SvelteKit](https://svelte.dev/) and [shadcn-svelte](https://www.shadcn-svelte.com/).
|
||||
|
||||
## Creating a project
|
||||
|
||||
If you're seeing this, you've probably already done this step. Congrats!
|
||||
|
||||
```bash
|
||||
# create a new project in the current directory
|
||||
npx sv create
|
||||
|
||||
# create a new project in my-app
|
||||
npx sv create my-app
|
||||
```
|
||||
|
||||
## Developing
|
||||
|
||||
Once you've created a project and installed dependencies with `npm install` (or `pnpm install` or `yarn`), start a development server:
|
||||
|
||||
```bash
|
||||
npm run dev
|
||||
|
||||
# or start the server and open the app in a new browser tab
|
||||
npm run dev -- --open
|
||||
```
|
||||
|
||||
## Building
|
||||
|
||||
To create a production version of your app:
|
||||
|
||||
```bash
|
||||
npm run build
|
||||
```
|
||||
|
||||
You can preview the production build with `npm run preview`.
|
||||
|
||||
> To deploy your app, you may need to install an [adapter](https://svelte.dev/docs/kit/adapters) for your target environment.
|
||||
> [!NOTE]
|
||||
> This is merely the frontend, for the backend see [groove](https://github.com/4-0-9/groove).
|
||||
|
||||
@@ -78,4 +78,5 @@ message TracksRequest {
|
||||
|
||||
message PlayPlaylistRequest {
|
||||
uint32 id = 1;
|
||||
optional uint32 starting_rank = 2;
|
||||
}
|
||||
|
||||
@@ -10,8 +10,10 @@
|
||||
import { cn } from '$lib/utils';
|
||||
import CreatePlaylistDialog from './CreatePlaylistDialog.svelte';
|
||||
import { getLibraryState } from '$lib/library.svelte';
|
||||
import { getPlayerState } from '$lib/player.svelte';
|
||||
|
||||
const library = getLibraryState();
|
||||
const player = getPlayerState();
|
||||
</script>
|
||||
|
||||
<Sidebar.Root collapsible="icon">
|
||||
@@ -92,6 +94,7 @@
|
||||
class="cursor-pointer"
|
||||
isActive={$page.url.pathname === playlistLocation}
|
||||
href={playlistLocation}
|
||||
ondblclick={() => player.playPlaylist({ id: playlist.id }, fetch)}
|
||||
>
|
||||
{playlist.name}
|
||||
</Sidebar.MenuSubButton>
|
||||
|
||||
@@ -54,7 +54,7 @@
|
||||
size="icon"
|
||||
onclick={(e) => {
|
||||
e.stopPropagation();
|
||||
player.playPlaylist(id, fetch);
|
||||
player.playPlaylist({ id }, fetch);
|
||||
}}
|
||||
>
|
||||
<Play />
|
||||
|
||||
@@ -52,13 +52,21 @@ class PlayerState {
|
||||
}
|
||||
}
|
||||
|
||||
async playPlaylist(id: number, fetch: typeof globalThis.fetch) {
|
||||
async playPlaylist(opts: { id: number; startRank?: number }, fetch: typeof globalThis.fetch) {
|
||||
const { id, startRank } = opts;
|
||||
|
||||
const formData = new FormData();
|
||||
|
||||
if (startRank) {
|
||||
formData.set('start', startRank.toString());
|
||||
}
|
||||
|
||||
const response = await fetch(`/playlists/${id}?/play`, {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'x-sveltekit-action': 'true'
|
||||
},
|
||||
body: new FormData()
|
||||
body: formData
|
||||
});
|
||||
|
||||
if (!response.ok) {
|
||||
|
||||
@@ -153,6 +153,10 @@ export interface PlayPlaylistRequest {
|
||||
* @generated from protobuf field: uint32 id = 1;
|
||||
*/
|
||||
id: number;
|
||||
/**
|
||||
* @generated from protobuf field: optional uint32 starting_rank = 2;
|
||||
*/
|
||||
startingRank?: number;
|
||||
}
|
||||
// @generated message type with reflection information, may provide speed optimized methods
|
||||
class TrackRequest$Type extends MessageType<TrackRequest> {
|
||||
@@ -768,7 +772,8 @@ export const TracksRequest = new TracksRequest$Type();
|
||||
class PlayPlaylistRequest$Type extends MessageType<PlayPlaylistRequest> {
|
||||
constructor() {
|
||||
super("player.PlayPlaylistRequest", [
|
||||
{ no: 1, name: "id", kind: "scalar", T: 13 /*ScalarType.UINT32*/ }
|
||||
{ no: 1, name: "id", kind: "scalar", T: 13 /*ScalarType.UINT32*/ },
|
||||
{ no: 2, name: "starting_rank", kind: "scalar", opt: true, T: 13 /*ScalarType.UINT32*/ }
|
||||
]);
|
||||
}
|
||||
create(value?: PartialMessage<PlayPlaylistRequest>): PlayPlaylistRequest {
|
||||
@@ -786,6 +791,9 @@ class PlayPlaylistRequest$Type extends MessageType<PlayPlaylistRequest> {
|
||||
case /* uint32 id */ 1:
|
||||
message.id = reader.uint32();
|
||||
break;
|
||||
case /* optional uint32 starting_rank */ 2:
|
||||
message.startingRank = reader.uint32();
|
||||
break;
|
||||
default:
|
||||
let u = options.readUnknownField;
|
||||
if (u === "throw")
|
||||
@@ -801,6 +809,9 @@ class PlayPlaylistRequest$Type extends MessageType<PlayPlaylistRequest> {
|
||||
/* uint32 id = 1; */
|
||||
if (message.id !== 0)
|
||||
writer.tag(1, WireType.Varint).uint32(message.id);
|
||||
/* optional uint32 starting_rank = 2; */
|
||||
if (message.startingRank !== undefined)
|
||||
writer.tag(2, WireType.Varint).uint32(message.startingRank);
|
||||
let u = options.writeUnknownFields;
|
||||
if (u !== false)
|
||||
(u == true ? UnknownFieldHandler.onWrite : u)(this.typeName, message, writer);
|
||||
|
||||
@@ -50,7 +50,7 @@
|
||||
</div>
|
||||
</header>
|
||||
<main class="grow overflow-hidden">
|
||||
<ScrollArea class="flex h-full flex-col overflow-auto">
|
||||
<ScrollArea class="h-full overflow-auto">
|
||||
<div class="m-4 flex flex-col">
|
||||
<!-- <Sidebar.Trigger /> -->
|
||||
{@render children()}
|
||||
|
||||
@@ -8,11 +8,16 @@ import { fail } from '@sveltejs/kit';
|
||||
import type { TrackList } from '$lib/proto/library';
|
||||
|
||||
export const actions = {
|
||||
play: async ({ params }) => {
|
||||
play: async ({ params, request }) => {
|
||||
const formData = await request.formData();
|
||||
|
||||
const start = formData.get('start')?.toString();
|
||||
|
||||
const client = new PlayerClient(protoTransport);
|
||||
|
||||
const response = await client.playPlaylist({
|
||||
id: parseInt(params.id)
|
||||
id: parseInt(params.id),
|
||||
startingRank: start ? Number(start) : undefined
|
||||
});
|
||||
|
||||
return serializable<PlayerStatus>(response.response);
|
||||
@@ -40,7 +45,6 @@ export const actions = {
|
||||
bRank: parseInt(b)
|
||||
});
|
||||
|
||||
console.log(response.response.tracks);
|
||||
return serializable<TrackList>(response.response);
|
||||
}
|
||||
} satisfies Actions;
|
||||
|
||||
@@ -8,6 +8,7 @@
|
||||
import { getCoverUrl } from '$lib/covers';
|
||||
import Play from 'virtual:icons/lucide/play';
|
||||
import Trash2 from 'virtual:icons/lucide/trash-2';
|
||||
import AudioLines from 'virtual:icons/lucide/audio-lines';
|
||||
import { getPlayerState } from '$lib/player.svelte';
|
||||
import { Button } from '$lib/components/ui/button';
|
||||
import * as ContextMenu from '$lib/components/ui/context-menu';
|
||||
@@ -58,7 +59,7 @@
|
||||
</div>
|
||||
<div class="mx-4">
|
||||
<div class="mb-2">
|
||||
<Button class="size-12" onclick={() => player.playPlaylist(playlist.id, fetch)}
|
||||
<Button class="size-12" onclick={() => player.playPlaylist({ id: playlist.id }, fetch)}
|
||||
><Play /></Button
|
||||
>
|
||||
</div>
|
||||
@@ -80,7 +81,7 @@
|
||||
<Table.Row
|
||||
{...props}
|
||||
class="select-none"
|
||||
ondblclick={() => player.playTrack(track.hash, fetch)}
|
||||
ondblclick={() => player.playPlaylist({ id: playlist.id, startRank: i }, fetch)}
|
||||
data-playlist-index={i}
|
||||
ondragstart={onDragStart}
|
||||
ondrop={onDrop}
|
||||
@@ -88,14 +89,23 @@
|
||||
draggable
|
||||
>
|
||||
<Table.Cell class="text-center">{i + 1}</Table.Cell>
|
||||
<Table.Cell class="w-0">
|
||||
<Table.Cell class="relative w-0">
|
||||
<div
|
||||
class="absolute bottom-0 left-0 right-0 top-0 z-10 hidden items-center justify-center"
|
||||
class:!flex={player.currentlyPlaying?.hash === track.hash}
|
||||
>
|
||||
<AudioLines class="size-6 animate-pulse text-primary" />
|
||||
</div>
|
||||
<img
|
||||
class="overflow-hidden rounded-md"
|
||||
class:brightness-[40%]={player.currentlyPlaying?.hash === track.hash}
|
||||
src={getCoverUrl(track.hash)}
|
||||
alt=""
|
||||
/>
|
||||
</Table.Cell>
|
||||
<Table.Cell class="text-nowrap">{track.name}</Table.Cell>
|
||||
<Table.Cell class="text-nowrap">
|
||||
{track.name}
|
||||
</Table.Cell>
|
||||
<Table.Cell class="w-fit text-nowrap">{track.artistName}</Table.Cell>
|
||||
<Table.Cell class="text-right"
|
||||
>{dayjs
|
||||
|
||||
Reference in New Issue
Block a user