feat(playlists): reorder tracks
This commit is contained in:
@@ -11,6 +11,7 @@ service Library {
|
|||||||
rpc DeletePlaylist(DeletePlaylistRequest) returns (google.protobuf.Empty);
|
rpc DeletePlaylist(DeletePlaylistRequest) returns (google.protobuf.Empty);
|
||||||
rpc AddTrackToPlaylist(AddTrackToPlaylistRequest) returns (google.protobuf.Empty);
|
rpc AddTrackToPlaylist(AddTrackToPlaylistRequest) returns (google.protobuf.Empty);
|
||||||
rpc RemoveTrackFromPlaylist(RemoveTrackFromPlaylistRequest) returns (google.protobuf.Empty);
|
rpc RemoveTrackFromPlaylist(RemoveTrackFromPlaylistRequest) returns (google.protobuf.Empty);
|
||||||
|
rpc SwapTracks(SwapTracksRequest) returns (TrackList);
|
||||||
}
|
}
|
||||||
|
|
||||||
message TrackList {
|
message TrackList {
|
||||||
@@ -56,3 +57,9 @@ message RemoveTrackFromPlaylistRequest {
|
|||||||
uint32 playlist_id = 1;
|
uint32 playlist_id = 1;
|
||||||
uint32 track_rank = 2;
|
uint32 track_rank = 2;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
message SwapTracksRequest {
|
||||||
|
uint32 playlist_id = 1;
|
||||||
|
uint32 a_rank = 2;
|
||||||
|
uint32 b_rank = 3;
|
||||||
|
}
|
||||||
|
|||||||
@@ -4,6 +4,7 @@
|
|||||||
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 { Library } from "./library";
|
import { Library } from "./library";
|
||||||
|
import type { SwapTracksRequest } from "./library";
|
||||||
import type { RemoveTrackFromPlaylistRequest } from "./library";
|
import type { RemoveTrackFromPlaylistRequest } from "./library";
|
||||||
import type { AddTrackToPlaylistRequest } from "./library";
|
import type { AddTrackToPlaylistRequest } from "./library";
|
||||||
import type { DeletePlaylistRequest } from "./library";
|
import type { DeletePlaylistRequest } from "./library";
|
||||||
@@ -43,6 +44,10 @@ export interface ILibraryClient {
|
|||||||
* @generated from protobuf rpc: RemoveTrackFromPlaylist(library.RemoveTrackFromPlaylistRequest) returns (google.protobuf.Empty);
|
* @generated from protobuf rpc: RemoveTrackFromPlaylist(library.RemoveTrackFromPlaylistRequest) returns (google.protobuf.Empty);
|
||||||
*/
|
*/
|
||||||
removeTrackFromPlaylist(input: RemoveTrackFromPlaylistRequest, options?: RpcOptions): UnaryCall<RemoveTrackFromPlaylistRequest, Empty>;
|
removeTrackFromPlaylist(input: RemoveTrackFromPlaylistRequest, options?: RpcOptions): UnaryCall<RemoveTrackFromPlaylistRequest, Empty>;
|
||||||
|
/**
|
||||||
|
* @generated from protobuf rpc: SwapTracks(library.SwapTracksRequest) returns (library.TrackList);
|
||||||
|
*/
|
||||||
|
swapTracks(input: SwapTracksRequest, options?: RpcOptions): UnaryCall<SwapTracksRequest, TrackList>;
|
||||||
}
|
}
|
||||||
/**
|
/**
|
||||||
* @generated from protobuf service library.Library
|
* @generated from protobuf service library.Library
|
||||||
@@ -95,4 +100,11 @@ export class LibraryClient implements ILibraryClient, ServiceInfo {
|
|||||||
const method = this.methods[5], opt = this._transport.mergeOptions(options);
|
const method = this.methods[5], opt = this._transport.mergeOptions(options);
|
||||||
return stackIntercept<RemoveTrackFromPlaylistRequest, Empty>("unary", this._transport, method, opt, input);
|
return stackIntercept<RemoveTrackFromPlaylistRequest, Empty>("unary", this._transport, method, opt, input);
|
||||||
}
|
}
|
||||||
|
/**
|
||||||
|
* @generated from protobuf rpc: SwapTracks(library.SwapTracksRequest) returns (library.TrackList);
|
||||||
|
*/
|
||||||
|
swapTracks(input: SwapTracksRequest, options?: RpcOptions): UnaryCall<SwapTracksRequest, TrackList> {
|
||||||
|
const method = this.methods[6], opt = this._transport.mergeOptions(options);
|
||||||
|
return stackIntercept<SwapTracksRequest, TrackList>("unary", this._transport, method, opt, input);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -125,6 +125,23 @@ export interface RemoveTrackFromPlaylistRequest {
|
|||||||
*/
|
*/
|
||||||
trackRank: number;
|
trackRank: number;
|
||||||
}
|
}
|
||||||
|
/**
|
||||||
|
* @generated from protobuf message library.SwapTracksRequest
|
||||||
|
*/
|
||||||
|
export interface SwapTracksRequest {
|
||||||
|
/**
|
||||||
|
* @generated from protobuf field: uint32 playlist_id = 1;
|
||||||
|
*/
|
||||||
|
playlistId: number;
|
||||||
|
/**
|
||||||
|
* @generated from protobuf field: uint32 a_rank = 2;
|
||||||
|
*/
|
||||||
|
aRank: number;
|
||||||
|
/**
|
||||||
|
* @generated from protobuf field: uint32 b_rank = 3;
|
||||||
|
*/
|
||||||
|
bRank: number;
|
||||||
|
}
|
||||||
// @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> {
|
||||||
constructor() {
|
constructor() {
|
||||||
@@ -611,6 +628,69 @@ class RemoveTrackFromPlaylistRequest$Type extends MessageType<RemoveTrackFromPla
|
|||||||
* @generated MessageType for protobuf message library.RemoveTrackFromPlaylistRequest
|
* @generated MessageType for protobuf message library.RemoveTrackFromPlaylistRequest
|
||||||
*/
|
*/
|
||||||
export const RemoveTrackFromPlaylistRequest = new RemoveTrackFromPlaylistRequest$Type();
|
export const RemoveTrackFromPlaylistRequest = new RemoveTrackFromPlaylistRequest$Type();
|
||||||
|
// @generated message type with reflection information, may provide speed optimized methods
|
||||||
|
class SwapTracksRequest$Type extends MessageType<SwapTracksRequest> {
|
||||||
|
constructor() {
|
||||||
|
super("library.SwapTracksRequest", [
|
||||||
|
{ no: 1, name: "playlist_id", kind: "scalar", T: 13 /*ScalarType.UINT32*/ },
|
||||||
|
{ no: 2, name: "a_rank", kind: "scalar", T: 13 /*ScalarType.UINT32*/ },
|
||||||
|
{ no: 3, name: "b_rank", kind: "scalar", T: 13 /*ScalarType.UINT32*/ }
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
create(value?: PartialMessage<SwapTracksRequest>): SwapTracksRequest {
|
||||||
|
const message = globalThis.Object.create((this.messagePrototype!));
|
||||||
|
message.playlistId = 0;
|
||||||
|
message.aRank = 0;
|
||||||
|
message.bRank = 0;
|
||||||
|
if (value !== undefined)
|
||||||
|
reflectionMergePartial<SwapTracksRequest>(this, message, value);
|
||||||
|
return message;
|
||||||
|
}
|
||||||
|
internalBinaryRead(reader: IBinaryReader, length: number, options: BinaryReadOptions, target?: SwapTracksRequest): SwapTracksRequest {
|
||||||
|
let message = target ?? this.create(), end = reader.pos + length;
|
||||||
|
while (reader.pos < end) {
|
||||||
|
let [fieldNo, wireType] = reader.tag();
|
||||||
|
switch (fieldNo) {
|
||||||
|
case /* uint32 playlist_id */ 1:
|
||||||
|
message.playlistId = reader.uint32();
|
||||||
|
break;
|
||||||
|
case /* uint32 a_rank */ 2:
|
||||||
|
message.aRank = reader.uint32();
|
||||||
|
break;
|
||||||
|
case /* uint32 b_rank */ 3:
|
||||||
|
message.bRank = reader.uint32();
|
||||||
|
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: SwapTracksRequest, writer: IBinaryWriter, options: BinaryWriteOptions): IBinaryWriter {
|
||||||
|
/* uint32 playlist_id = 1; */
|
||||||
|
if (message.playlistId !== 0)
|
||||||
|
writer.tag(1, WireType.Varint).uint32(message.playlistId);
|
||||||
|
/* uint32 a_rank = 2; */
|
||||||
|
if (message.aRank !== 0)
|
||||||
|
writer.tag(2, WireType.Varint).uint32(message.aRank);
|
||||||
|
/* uint32 b_rank = 3; */
|
||||||
|
if (message.bRank !== 0)
|
||||||
|
writer.tag(3, WireType.Varint).uint32(message.bRank);
|
||||||
|
let u = options.writeUnknownFields;
|
||||||
|
if (u !== false)
|
||||||
|
(u == true ? UnknownFieldHandler.onWrite : u)(this.typeName, message, writer);
|
||||||
|
return writer;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
/**
|
||||||
|
* @generated MessageType for protobuf message library.SwapTracksRequest
|
||||||
|
*/
|
||||||
|
export const SwapTracksRequest = new SwapTracksRequest$Type();
|
||||||
/**
|
/**
|
||||||
* @generated ServiceType for protobuf service library.Library
|
* @generated ServiceType for protobuf service library.Library
|
||||||
*/
|
*/
|
||||||
@@ -620,5 +700,6 @@ export const Library = new ServiceType("library.Library", [
|
|||||||
{ name: "CreatePlaylist", options: {}, I: CreatePlaylistRequest, O: CreatePlaylistResponse },
|
{ name: "CreatePlaylist", options: {}, I: CreatePlaylistRequest, O: CreatePlaylistResponse },
|
||||||
{ name: "DeletePlaylist", options: {}, I: DeletePlaylistRequest, O: Empty },
|
{ name: "DeletePlaylist", options: {}, I: DeletePlaylistRequest, O: Empty },
|
||||||
{ name: "AddTrackToPlaylist", options: {}, I: AddTrackToPlaylistRequest, O: Empty },
|
{ name: "AddTrackToPlaylist", options: {}, I: AddTrackToPlaylistRequest, O: Empty },
|
||||||
{ name: "RemoveTrackFromPlaylist", options: {}, I: RemoveTrackFromPlaylistRequest, O: Empty }
|
{ name: "RemoveTrackFromPlaylist", options: {}, I: RemoveTrackFromPlaylistRequest, O: Empty },
|
||||||
|
{ name: "SwapTracks", options: {}, I: SwapTracksRequest, O: TrackList }
|
||||||
]);
|
]);
|
||||||
|
|||||||
@@ -4,6 +4,8 @@ import { PlayerClient } from '$lib/proto/player.client';
|
|||||||
import { LibraryClient } from '$lib/proto/library.client';
|
import { LibraryClient } from '$lib/proto/library.client';
|
||||||
import { serializable } from '$lib/proto';
|
import { serializable } from '$lib/proto';
|
||||||
import type { PlayerStatus } from '$lib/proto/player';
|
import type { PlayerStatus } from '$lib/proto/player';
|
||||||
|
import { fail } from '@sveltejs/kit';
|
||||||
|
import type { TrackList } from '$lib/proto/library';
|
||||||
|
|
||||||
export const actions = {
|
export const actions = {
|
||||||
play: async ({ params }) => {
|
play: async ({ params }) => {
|
||||||
@@ -19,5 +21,26 @@ export const actions = {
|
|||||||
const client = new LibraryClient(protoTransport);
|
const client = new LibraryClient(protoTransport);
|
||||||
|
|
||||||
const _response = await client.deletePlaylist({ id: parseInt(params.id) });
|
const _response = await client.deletePlaylist({ id: parseInt(params.id) });
|
||||||
|
},
|
||||||
|
'swap-tracks': async ({ params, request }) => {
|
||||||
|
const formData = await request.formData();
|
||||||
|
|
||||||
|
const a = formData.get('a')?.toString();
|
||||||
|
const b = formData.get('b')?.toString();
|
||||||
|
|
||||||
|
if (!a || !b) {
|
||||||
|
return fail(400);
|
||||||
|
}
|
||||||
|
|
||||||
|
const client = new LibraryClient(protoTransport);
|
||||||
|
|
||||||
|
const response = await client.swapTracks({
|
||||||
|
playlistId: parseInt(params.id),
|
||||||
|
aRank: parseInt(a),
|
||||||
|
bRank: parseInt(b)
|
||||||
|
});
|
||||||
|
|
||||||
|
console.log(response.response.tracks);
|
||||||
|
return serializable<TrackList>(response.response);
|
||||||
}
|
}
|
||||||
} satisfies Actions;
|
} satisfies Actions;
|
||||||
|
|||||||
@@ -7,9 +7,10 @@
|
|||||||
import duration from 'dayjs/plugin/duration';
|
import duration from 'dayjs/plugin/duration';
|
||||||
import { getCoverUrl } from '$lib/covers';
|
import { getCoverUrl } from '$lib/covers';
|
||||||
import Play from 'virtual:icons/lucide/play';
|
import Play from 'virtual:icons/lucide/play';
|
||||||
import Shuffle from 'virtual:icons/lucide/shuffle';
|
|
||||||
import { getPlayerState } from '$lib/player.svelte';
|
import { getPlayerState } from '$lib/player.svelte';
|
||||||
import { Button } from '$lib/components/ui/button';
|
import { Button } from '$lib/components/ui/button';
|
||||||
|
import { deserialize } from '$app/forms';
|
||||||
|
import type { Track, TrackList } from '$lib/proto/library';
|
||||||
dayjs.extend(duration);
|
dayjs.extend(duration);
|
||||||
|
|
||||||
const library = getLibraryState();
|
const library = getLibraryState();
|
||||||
@@ -20,6 +21,48 @@
|
|||||||
let totalDuration = $derived<number>(
|
let totalDuration = $derived<number>(
|
||||||
playlist?.tracks.reduce((acc, track) => acc + Number(track.duration), 0) ?? 0
|
playlist?.tracks.reduce((acc, track) => acc + Number(track.duration), 0) ?? 0
|
||||||
);
|
);
|
||||||
|
|
||||||
|
function onDragStart(e: DragEvent) {
|
||||||
|
const element = e.target as HTMLElement;
|
||||||
|
|
||||||
|
e.dataTransfer?.setData('text/plain', element.getAttribute('data-playlist-index')!);
|
||||||
|
}
|
||||||
|
|
||||||
|
async function onDrop(e: DragEvent) {
|
||||||
|
const aIndex = e.dataTransfer?.getData('text/plain')!;
|
||||||
|
const bIndex = (e.currentTarget as HTMLElement).getAttribute('data-playlist-index')!;
|
||||||
|
|
||||||
|
const formData = new FormData();
|
||||||
|
|
||||||
|
formData.set('a', aIndex);
|
||||||
|
formData.set('b', bIndex);
|
||||||
|
|
||||||
|
const response = await fetch(`/playlists/${playlist!.id}?/swap-tracks`, {
|
||||||
|
method: 'POST',
|
||||||
|
headers: {
|
||||||
|
'x-sveltekit-action': 'true'
|
||||||
|
},
|
||||||
|
body: formData
|
||||||
|
});
|
||||||
|
|
||||||
|
if (!response.ok) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const result = deserialize(await response.text());
|
||||||
|
|
||||||
|
if (result.type === 'success' && result.data && 'tracks' in result.data) {
|
||||||
|
const _playlist = library.playlists.find((p) => p.id === playlist?.id);
|
||||||
|
|
||||||
|
if (playlist) {
|
||||||
|
playlist.tracks = (result.data as unknown as TrackList).tracks;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (_playlist) {
|
||||||
|
_playlist.tracks = (result.data as unknown as TrackList).tracks;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
{#if playlist}
|
{#if playlist}
|
||||||
@@ -40,7 +83,8 @@
|
|||||||
</div>
|
</div>
|
||||||
<div class="mx-4">
|
<div class="mx-4">
|
||||||
<div class="mb-2">
|
<div class="mb-2">
|
||||||
<Button class="size-12" onclick={() => player.playPlaylist(playlist.id, fetch)}><Play /></Button
|
<Button class="size-12" onclick={() => player.playPlaylist(playlist.id, fetch)}
|
||||||
|
><Play /></Button
|
||||||
>
|
>
|
||||||
</div>
|
</div>
|
||||||
<Table.Root>
|
<Table.Root>
|
||||||
@@ -54,9 +98,16 @@
|
|||||||
</Table.Row>
|
</Table.Row>
|
||||||
</Table.Header>
|
</Table.Header>
|
||||||
<Table.Body>
|
<Table.Body>
|
||||||
<!-- formaction="/tracks/{track.hash}?/play" -->
|
|
||||||
{#each playlist.tracks as track, i}
|
{#each playlist.tracks as track, i}
|
||||||
<Table.Row class="select-none" ondblclick={() => player.playTrack(track.hash, fetch)}>
|
<Table.Row
|
||||||
|
class="select-none"
|
||||||
|
ondblclick={() => player.playTrack(track.hash, fetch)}
|
||||||
|
data-playlist-index={i}
|
||||||
|
ondragstart={onDragStart}
|
||||||
|
ondrop={onDrop}
|
||||||
|
ondragover={(e) => e.preventDefault()}
|
||||||
|
draggable
|
||||||
|
>
|
||||||
<Table.Cell class="text-center">{i + 1}</Table.Cell>
|
<Table.Cell class="text-center">{i + 1}</Table.Cell>
|
||||||
<Table.Cell class="w-0">
|
<Table.Cell class="w-0">
|
||||||
<img class="overflow-hidden rounded-md" src={getCoverUrl(track.hash)} alt="" />
|
<img class="overflow-hidden rounded-md" src={getCoverUrl(track.hash)} alt="" />
|
||||||
|
|||||||
Reference in New Issue
Block a user