Browse Source

Started on draggable queue

Owen Diffey 3 năm trước cách đây
mục cha
commit
d697e89ef9

+ 64 - 0
backend/logic/actions/stations.js

@@ -3074,6 +3074,70 @@ export default {
 		);
 	},
 
+	/**
+	 * Reposition station queue
+	 *
+	 * @param session
+	 * @param stationId - the station id
+	 * @param queue - queue data
+	 * @param cb
+	 */
+	repositionQueue: isOwnerRequired(async function repositionQueue(session, stationId, queue, cb) {
+		const stationModel = await DBModule.runJob("GET_MODEL", { modelName: "station" }, this);
+
+		async.waterfall(
+			[
+				next => {
+					if (!queue) return next("Invalid queue data.");
+					return StationsModule.runJob("GET_STATION", { stationId }, this)
+						.then(station => next(null, station))
+						.catch(next);
+				},
+
+				(station, next) => {
+					if (!station) return next("Station not found.");
+					return stationModel
+						.updateOne({ _id: stationId }, { $set: { queue } }, this)
+						.then(station => next(null, station))
+						.catch(next);
+				},
+
+				(res, next) => {
+					StationsModule.runJob("UPDATE_STATION", { stationId }, this)
+						.then(station => next(null, station))
+						.catch(next);
+				}
+			],
+			async err => {
+				if (err) {
+					err = await UtilsModule.runJob("GET_ERROR", { error: err }, this);
+					this.log(
+						"ERROR",
+						"STATIONS_REPOSITION_QUEUE",
+						`Repositioning station "${stationId}" queue failed. "${err}"`
+					);
+					return cb({ status: "failure", message: err });
+				}
+
+				this.log(
+					"SUCCESS",
+					"STATIONS_REPOSITION_QUEUE",
+					`Repositioned station "${stationId}" queue successfully.`
+				);
+
+				CacheModule.runJob("PUB", {
+					channel: "station.queueUpdate",
+					value: stationId
+				});
+
+				return cb({
+					status: "success",
+					message: "Successfully repositioned queue."
+				});
+			}
+		);
+	}),
+
 	/**
 	 * Selects a private playlist for a station
 	 *

+ 1 - 2
backend/logic/youtube.js

@@ -305,9 +305,8 @@ class _YouTubeModule extends CoreClass {
 						YouTubeModule.log("ERROR", "GET_PLAYLIST_PAGE", `${err.message}`);
 						if (err.message === "Request failed with status code 404") {
 							return reject(new Error("Playlist not found. Is the playlist public/unlisted?"));
-						} else {
-							return reject(new Error("An error has occured. Please try again later."));
 						}
+						return reject(new Error("An error has occured. Please try again later."));
 					});
 			});
 		});

+ 97 - 43
frontend/src/pages/Station/Sidebar/Queue.vue

@@ -1,54 +1,68 @@
 <template>
 	<div id="queue">
-		<div
+		<draggable
 			:class="{
 				'actionable-button-hidden': !actionableButtonVisible,
 				'scrollable-list': true
 			}"
+			v-if="queue.length > 0"
+			v-model="queue"
+			v-bind="dragOptions"
+			@start="drag = true"
+			@end="drag = false"
 		>
-			<song-item
-				v-for="(song, index) in songsList"
-				:key="index + song.songId"
-				:song="song"
-				:requested-by="
-					station.type === 'community' && station.partyMode === true
-				"
+			<transition-group
+				type="transition"
+				:name="!drag ? 'draggable-list-transition' : null"
 			>
-				<div
-					v-if="isAdminOnly() || isOwnerOnly()"
-					class="song-actions"
-					slot="actions"
+				<song-item
+					v-for="(song, index) in queue"
+					:key="index + song.songId"
+					:song="song"
+					:requested-by="
+						station.type === 'community' &&
+							station.partyMode === true
+					"
+					:class="{
+						'item-draggable': isAdminOnly() || isOwnerOnly()
+					}"
 				>
-					<i
-						v-if="isOwnerOnly() || isAdminOnly()"
-						class="material-icons delete-icon"
-						@click="removeFromQueue(song.songId)"
-						content="Remove Song from Queue"
-						v-tippy
-						>delete_forever</i
-					>
-					<i
-						class="material-icons"
-						v-if="index > 0"
-						@click="moveSongToTop(index)"
-						content="Move to top of Queue"
-						v-tippy
-						>vertical_align_top</i
+					<div
+						v-if="isAdminOnly() || isOwnerOnly()"
+						class="song-actions"
+						slot="actions"
 					>
-					<i
-						v-if="songsList.length - 1 !== index"
-						@click="moveSongToBottom(index)"
-						class="material-icons"
-						content="Move to bottom of Queue"
-						v-tippy
-						>vertical_align_bottom</i
-					>
-				</div>
-			</song-item>
-			<p class="nothing-here-text" v-if="songsList.length < 1">
-				There are no songs currently queued
-			</p>
-		</div>
+						<i
+							v-if="isOwnerOnly() || isAdminOnly()"
+							class="material-icons delete-icon"
+							@click="removeFromQueue(song.songId)"
+							content="Remove Song from Queue"
+							v-tippy
+							>delete_forever</i
+						>
+						<i
+							class="material-icons"
+							v-if="index > 0"
+							@click="moveSongToTop(index)"
+							content="Move to top of Queue"
+							v-tippy
+							>vertical_align_top</i
+						>
+						<i
+							v-if="queue.length - 1 !== index"
+							@click="moveSongToBottom(index)"
+							class="material-icons"
+							content="Move to bottom of Queue"
+							v-tippy
+							>vertical_align_bottom</i
+						>
+					</div>
+				</song-item>
+			</transition-group>
+		</draggable>
+		<p class="nothing-here-text" v-else>
+			There are no songs currently queued
+		</p>
 		<button
 			class="button is-primary tab-actionable-button"
 			v-if="
@@ -110,19 +124,37 @@
 
 <script>
 import { mapActions, mapState, mapGetters } from "vuex";
+import draggable from "vuedraggable";
 import Toast from "toasters";
 
 import SongItem from "@/components/SongItem.vue";
 
 export default {
-	components: { SongItem },
+	components: { draggable, SongItem },
 	data() {
 		return {
 			dismissedWarning: false,
-			actionableButtonVisible: false
+			actionableButtonVisible: false,
+			drag: false
 		};
 	},
 	computed: {
+		dragOptions() {
+			return {
+				animation: 200,
+				group: "queue",
+				disabled: !(this.isAdminOnly() || this.isOwnerOnly()),
+				ghostClass: "draggable-list-ghost"
+			};
+		},
+		queue: {
+			get() {
+				return this.songsList;
+			},
+			set(queue) {
+				this.updateQueuePositioning(queue);
+			}
+		},
 		...mapState({
 			loggedIn: state => state.user.auth.loggedIn,
 			userId: state => state.user.auth.userId,
@@ -168,6 +200,28 @@ export default {
 				}
 			);
 		},
+		updateQueuePositioning(queue) {
+			this.socket.dispatch(
+				"stations.repositionQueue",
+				this.station._id,
+				queue,
+				res => {
+					new Toast({ content: res.message, timeout: 4000 });
+				}
+			);
+		},
+		moveSongToTop(index) {
+			this.queue.splice(0, 0, this.queue.splice(index, 1)[0]);
+			this.updateQueuePositioning(this.queue);
+		},
+		moveSongToBottom(index) {
+			this.queue.splice(
+				this.queue.length,
+				0,
+				this.queue.splice(index, 1)[0]
+			);
+			this.updateQueuePositioning(this.queue);
+		},
 		...mapActions("modalVisibility", ["openModal"])
 	}
 };