Skip to content

Commit 4a6d61c

Browse files
authored
Save current queue to playlist feature (#1456)
1 parent 6a3b88a commit 4a6d61c

File tree

7 files changed

+148
-11
lines changed

7 files changed

+148
-11
lines changed

src/helpers/player_menu_items.ts

Lines changed: 14 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,7 @@ import {
1212
} from "@/plugins/api/interfaces";
1313
import { authManager } from "@/plugins/auth";
1414
import router from "@/plugins/router";
15+
import { eventbus } from "@/plugins/eventbus";
1516
import { store } from "@/plugins/store";
1617
import { $t } from "@/plugins/i18n";
1718

@@ -133,7 +134,7 @@ export const getPlayerMenuItems = (
133134
}
134135

135136
// add 'transfer queue' menu item
136-
if (playerQueue?.items) {
137+
if (playerQueue?.items && playerQueue.items > 0) {
137138
menuItems.push({
138139
label: "transfer_queue",
139140
icon: "mdi-swap-horizontal",
@@ -162,7 +163,7 @@ export const getPlayerMenuItems = (
162163
});
163164
}
164165
// add 'clear queue' menu item
165-
if (playerQueue?.items) {
166+
if (playerQueue?.items && playerQueue.items > 0) {
166167
menuItems.push({
167168
label: "queue_clear",
168169
labelArgs: [],
@@ -172,6 +173,17 @@ export const getPlayerMenuItems = (
172173
icon: "mdi-cancel",
173174
});
174175
}
176+
// add 'save queue as playlist' menu item
177+
if (playerQueue?.items && playerQueue.items > 0) {
178+
menuItems.push({
179+
label: "save_queue_as_playlist",
180+
labelArgs: [],
181+
action: () => {
182+
eventbus.emit("createPlaylist", { queueId: playerQueue!.queue_id });
183+
},
184+
icon: "mdi-playlist-plus",
185+
});
186+
}
175187

176188
// add 'select source' menu item
177189
const selectableSources = player.source_list.filter(
Lines changed: 110 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,110 @@
1+
<!--
2+
Global dialog to create a new playlist or save a queue as a playlist.
3+
Because this dialog can be called from various places throughout the app,
4+
we steer its visibility through the centralized eventbus.
5+
-->
6+
<template>
7+
<v-dialog
8+
v-model="showDialog"
9+
max-width="500"
10+
@update:model-value="
11+
(v) => {
12+
store.dialogActive = v;
13+
}
14+
"
15+
>
16+
<v-card>
17+
<v-card-title>
18+
{{ $t(queueId ? "save_queue_as_playlist" : "new_playlist") }}
19+
</v-card-title>
20+
<v-card-text>
21+
<v-text-field
22+
ref="nameInput"
23+
v-model="playlistName"
24+
:label="$t('new_playlist_name')"
25+
@keyup.enter="doSave"
26+
/>
27+
</v-card-text>
28+
<v-card-actions>
29+
<v-spacer />
30+
<v-btn variant="text" @click="showDialog = false">
31+
{{ $t("close") }}
32+
</v-btn>
33+
<v-btn
34+
variant="text"
35+
color="primary"
36+
:disabled="!playlistName"
37+
@click="doSave"
38+
>
39+
{{ $t("settings.save") }}
40+
</v-btn>
41+
</v-card-actions>
42+
</v-card>
43+
</v-dialog>
44+
</template>
45+
46+
<script setup lang="ts">
47+
import { nextTick, onBeforeUnmount, onMounted, ref, watch } from "vue";
48+
import { toast } from "vue-sonner";
49+
50+
import api from "@/plugins/api";
51+
import { type CreatePlaylistEvent, eventbus } from "@/plugins/eventbus";
52+
import { $t } from "@/plugins/i18n";
53+
import router from "@/plugins/router";
54+
import { store } from "@/plugins/store";
55+
56+
const showDialog = ref(false);
57+
const playlistName = ref("");
58+
const queueId = ref("");
59+
const providerId = ref("");
60+
const nameInput = ref();
61+
62+
watch(showDialog, (open) => {
63+
store.dialogActive = open;
64+
if (open) {
65+
nextTick(() => {
66+
nameInput.value?.focus();
67+
});
68+
}
69+
});
70+
71+
onMounted(() => {
72+
eventbus.on("createPlaylist", (evt: CreatePlaylistEvent) => {
73+
queueId.value = evt.queueId || "";
74+
providerId.value = evt.providerId || "";
75+
playlistName.value = "";
76+
showDialog.value = true;
77+
});
78+
});
79+
80+
onBeforeUnmount(() => {
81+
eventbus.off("createPlaylist");
82+
});
83+
84+
const doSave = async () => {
85+
if (!playlistName.value) return;
86+
showDialog.value = false;
87+
try {
88+
const playlist = queueId.value
89+
? await api.queueCommandSaveAsPlaylist(queueId.value, playlistName.value)
90+
: await api.createPlaylist(playlistName.value, providerId.value);
91+
toast.success($t("playlist_created"), {
92+
action: {
93+
label: $t("open_playlist"),
94+
onClick: () => {
95+
store.showFullscreenPlayer = false;
96+
router.push({
97+
name: "playlist",
98+
params: {
99+
itemId: playlist.item_id,
100+
provider: playlist.provider,
101+
},
102+
});
103+
},
104+
},
105+
});
106+
} catch (e) {
107+
toast.error(e as string);
108+
}
109+
};
110+
</script>

src/layouts/default/View.vue

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,7 @@
1616
<component :is="Component" />
1717
</router-view>
1818
<add-to-playlist-dialog />
19+
<create-playlist-dialog />
1920
<item-context-menu />
2021
</div>
2122
</SidebarInset>
@@ -28,6 +29,7 @@ import AppSidebar from "@/components/navigation/AppSidebar.vue";
2829
import { SidebarInset, SidebarProvider } from "@/components/ui/sidebar";
2930
import { store } from "@/plugins/store";
3031
import AddToPlaylistDialog from "./AddToPlaylistDialog.vue";
32+
import CreatePlaylistDialog from "./CreatePlaylistDialog.vue";
3133
import ItemContextMenu from "./ItemContextMenu.vue";
3234
</script>
3335

src/plugins/api/index.ts

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1228,6 +1228,16 @@ export class MusicAssistantApi {
12281228
auto_play: autoPlay,
12291229
});
12301230
}
1231+
public queueCommandSaveAsPlaylist(
1232+
queueId: string,
1233+
name: string,
1234+
): Promise<Playlist> {
1235+
// Save the current queue items as a new playlist.
1236+
return this.sendCommand("player_queues/save_as_playlist", {
1237+
queue_id: queueId,
1238+
name,
1239+
});
1240+
}
12311241

12321242
// Player related functions/commands
12331243

src/plugins/eventbus.ts

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -16,9 +16,15 @@ export type ContextMenuDialogEvent = {
1616
showPlayMenuHeader?: boolean;
1717
};
1818

19+
export type CreatePlaylistEvent = {
20+
queueId?: string;
21+
providerId?: string;
22+
};
23+
1924
export type Events = {
2025
contextmenu: ContextMenuDialogEvent;
2126
playlistdialog: PlaylistDialogEvent;
27+
createPlaylist: CreatePlaylistEvent;
2228
clearSelection: void;
2329
"homescreen-edit-toggle": void;
2430
"mobile-sidebar-open": void;

src/translations/en.json

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -819,6 +819,9 @@
819819
"update_metadata": "Update metadata",
820820
"cancel": "Cancel",
821821
"transfer_queue": "Transfer queue",
822+
"save_queue_as_playlist": "Save queue as playlist",
823+
"playlist_created": "Playlist created",
824+
"open_playlist": "Open playlist",
822825
"power_on_player": "Power on",
823826
"power_off_player": "Power off",
824827
"powered_off_players": "Unpowered players",

src/views/LibraryPlaylists.vue

Lines changed: 3 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -29,16 +29,15 @@ import {
2929
EventType,
3030
ProviderFeature,
3131
} from "@/plugins/api/interfaces";
32+
import { eventbus } from "@/plugins/eventbus";
3233
import { store } from "@/plugins/store";
3334
import { ListMusic } from "lucide-vue-next";
3435
import { onBeforeUnmount, onMounted, ref } from "vue";
35-
import { useI18n } from "vue-i18n";
3636
3737
defineOptions({
3838
name: "Playlists",
3939
});
4040
41-
const { t } = useI18n();
4241
const updateAvailable = ref(false);
4342
const total = ref(store.libraryPlaylistsCount);
4443
const extraMenuItems = ref<ToolBarMenuItem[]>([]);
@@ -129,12 +128,7 @@ onMounted(() => {
129128
onBeforeUnmount(unsub);
130129
});
131130
132-
const newPlaylist = async function (provId: string) {
133-
const name = prompt(t("new_playlist_name"));
134-
if (!name) return;
135-
await api
136-
.createPlaylist(name, provId)
137-
.then(() => location.reload())
138-
.catch((e) => alert(e));
131+
const newPlaylist = function (provId: string) {
132+
eventbus.emit("createPlaylist", { providerId: provId });
139133
};
140134
</script>

0 commit comments

Comments
 (0)