瀏覽代碼

refactor: Converted profile playlists page and sortablePlaylists to composition API and SortableJS

Owen Diffey 2 年之前
父節點
當前提交
c3dfbbc5e9

+ 0 - 1
frontend/src/composables/useDragBox.ts

@@ -1,4 +1,3 @@
-// WIP
 import { ref, onMounted, onUnmounted, nextTick } from "vue";
 
 export function useDragBox() {

+ 171 - 0
frontend/src/composables/useSortablePlaylists.ts

@@ -0,0 +1,171 @@
+import { ref, computed, onMounted } from "vue";
+import { useStore } from "vuex";
+import { Sortable } from "sortablejs-vue3";
+import Toast from "toasters";
+
+export function useSortablePlaylists() {
+    const orderOfPlaylists = ref([]);
+    const drag = ref(false);
+    const disabled = ref(false);
+
+    const store = useStore();
+
+    const playlists = computed({
+        get: () => {
+            return store.state.user.playlists.playlists;
+        },
+        set: (playlists) => {
+            store.commit("user/playlists/updatePlaylists", playlists);
+        }
+    });
+    const dragOptions = computed(() => ({
+        animation: 200,
+        group: "playlists",
+        disabled: disabled.value,
+        ghostClass: "draggable-list-ghost"
+    }));
+
+    const { socket } = store.state.websockets;
+
+    const setPlaylists = playlists => store.dispatch("user/playlists/setPlaylists", playlists);
+    const addPlaylist = playlist => store.dispatch("user/playlists/addPlaylist", playlist);
+    const removePlaylist = playlist => store.dispatch("user/playlists/removePlaylist", playlist);
+
+    const calculatePlaylistOrder = () => {
+        const calculatedOrder = [];
+        playlists.value.forEach(playlist =>
+            calculatedOrder.push(playlist._id)
+        );
+
+        return calculatedOrder;
+    };
+
+    const savePlaylistOrder = ({ oldIndex, newIndex }) => {
+        if (oldIndex === newIndex) return;
+		const oldPlaylists = playlists.value;
+
+		oldPlaylists.splice(
+			newIndex,
+			0,
+			oldPlaylists.splice(oldIndex, 1)[0]
+		);
+
+		setPlaylists(oldPlaylists).then(() => {
+			const recalculatedOrder = calculatePlaylistOrder();
+
+			socket.dispatch(
+				"users.updateOrderOfPlaylists",
+				recalculatedOrder,
+				res => {
+					if (res.status === "error") return new Toast(res.message);
+
+					orderOfPlaylists.value = calculatePlaylistOrder(); // new order in regards to the database
+					return new Toast(res.message);
+				}
+			);
+		});
+    };
+
+    onMounted(() => {
+		socket.on(
+			"event:playlist.created",
+			res => addPlaylist(res.data.playlist),
+			{ replaceable: true }
+		);
+
+		socket.on(
+			"event:playlist.deleted",
+			res => removePlaylist(res.data.playlistId),
+			{ replaceable: true }
+		);
+
+		socket.on(
+			"event:playlist.song.added",
+			res => {
+				playlists.value.forEach((playlist, index) => {
+					if (playlist._id === res.data.playlistId) {
+						playlists.value[index].songs.push(res.data.song);
+					}
+				});
+			},
+			{ replaceable: true }
+		);
+
+		socket.on(
+			"event:playlist.song.removed",
+			res => {
+				playlists.value.forEach((playlist, playlistIndex) => {
+					if (playlist._id === res.data.playlistId) {
+						playlists.value[playlistIndex].songs.forEach(
+							(song, songIndex) => {
+								if (song.youtubeId === res.data.youtubeId) {
+									playlists.value[playlistIndex].songs.splice(
+										songIndex,
+										1
+									);
+								}
+							}
+						);
+					}
+				});
+			},
+			{ replaceable: true }
+		);
+
+		socket.on(
+			"event:playlist.displayName.updated",
+			res => {
+				playlists.value.forEach((playlist, index) => {
+					if (playlist._id === res.data.playlistId) {
+						playlists.value[index].displayName =
+							res.data.displayName;
+					}
+				});
+			},
+			{ replaceable: true }
+		);
+
+		socket.on(
+			"event:playlist.privacy.updated",
+			res => {
+				playlists.value.forEach((playlist, index) => {
+					if (playlist._id === res.data.playlist._id) {
+						playlists.value[index].privacy =
+							res.data.playlist.privacy;
+					}
+				});
+			},
+			{ replaceable: true }
+		);
+
+		socket.on(
+			"event:user.orderOfPlaylists.updated",
+			res => {
+				const sortedPlaylists = [];
+
+				playlists.value.forEach(playlist => {
+					sortedPlaylists[res.data.order.indexOf(playlist._id)] =
+						playlist;
+				});
+
+				playlists.value = sortedPlaylists;
+				orderOfPlaylists.value = calculatePlaylistOrder();
+			},
+			{ replaceable: true }
+		);
+	});
+
+    return {
+		Sortable,
+        drag,
+		orderOfPlaylists,
+        disabled,
+        playlists,
+        dragOptions,
+		setPlaylists,
+        addPlaylist,
+        removePlaylist,
+		calculatePlaylistOrder,
+        savePlaylistOrder
+    };
+};

+ 72 - 71
frontend/src/pages/Profile/Tabs/Playlists.vue

@@ -1,3 +1,70 @@
+<script setup lang="ts">
+import {
+	defineAsyncComponent,
+	computed,
+	onMounted,
+	onBeforeUnmount
+} from "vue";
+import { useStore } from "vuex";
+import { useSortablePlaylists } from "@/composables/useSortablePlaylists";
+import ws from "@/ws";
+
+const PlaylistItem = defineAsyncComponent(
+	() => import("@/components/PlaylistItem.vue")
+);
+
+const props = defineProps({
+	userId: { type: String, default: "" },
+	username: { type: String, default: "" }
+});
+
+const store = useStore();
+
+const myUserId = computed(() => store.state.user.auth.userId);
+
+const { socket } = store.state.websockets;
+
+const {
+	Sortable,
+	drag,
+	orderOfPlaylists,
+	disabled,
+	playlists,
+	dragOptions,
+	setPlaylists,
+	calculatePlaylistOrder,
+	savePlaylistOrder
+} = useSortablePlaylists();
+
+const openModal = modal => store.dispatch("modalVisibility/openModal", modal);
+
+onMounted(() => {
+	ws.onConnect(() => {
+		if (myUserId.value !== props.userId)
+			socket.dispatch(
+				"apis.joinRoom",
+				`profile.${props.userId}.playlists`,
+				() => {}
+			);
+
+		socket.dispatch("playlists.indexForUser", props.userId, res => {
+			if (res.status === "success") setPlaylists(res.data.playlists);
+			orderOfPlaylists.value = calculatePlaylistOrder(); // order in regards to the database
+			disabled.value = myUserId.value !== props.userId;
+		});
+	});
+});
+
+onBeforeUnmount(() => {
+	if (myUserId.value !== props.userId)
+		socket.dispatch(
+			"apis.leaveRoom",
+			`profile.${props.userId}.playlists`,
+			() => {}
+		);
+});
+</script>
+
 <template>
 	<div class="content playlists-tab">
 		<div v-if="playlists.length > 0">
@@ -18,17 +85,17 @@
 
 			<hr class="section-horizontal-rule" />
 
-			<draggable
+			<Sortable
 				:component-data="{
 					name: !drag ? 'draggable-list-transition' : null
 				}"
 				v-if="playlists.length > 0"
-				v-model="playlists"
+				:list="playlists"
 				item-key="_id"
-				v-bind="dragOptions"
+				:options="dragOptions"
 				@start="drag = true"
 				@end="drag = false"
-				@change="savePlaylistOrder"
+				@update="savePlaylistOrder"
 			>
 				<template #item="{ element }">
 					<playlist-item
@@ -73,7 +140,7 @@
 						</template>
 					</playlist-item>
 				</template>
-			</draggable>
+			</Sortable>
 
 			<button
 				v-if="myUserId === userId"
@@ -89,69 +156,3 @@
 		</div>
 	</div>
 </template>
-
-<script>
-import { mapActions, mapGetters } from "vuex";
-
-import PlaylistItem from "@/components/PlaylistItem.vue";
-import SortablePlaylists from "@/mixins/SortablePlaylists.vue";
-import ws from "@/ws";
-
-export default {
-	components: {
-		PlaylistItem
-	},
-	mixins: [SortablePlaylists],
-	props: {
-		userId: {
-			type: String,
-			default: ""
-		},
-		username: {
-			type: String,
-			default: ""
-		}
-	},
-	computed: {
-		...mapGetters({
-			socket: "websockets/getSocket"
-		})
-	},
-	mounted() {
-		if (
-			this.$route.query.tab === "recent-activity" ||
-			this.$route.query.tab === "playlists"
-		)
-			this.tab = this.$route.query.tab;
-
-		if (this.myUserId !== this.userId) {
-			ws.onConnect(() =>
-				this.socket.dispatch(
-					"apis.joinRoom",
-					`profile.${this.userId}.playlists`,
-					() => {}
-				)
-			);
-		}
-
-		ws.onConnect(() =>
-			this.socket.dispatch("playlists.indexForUser", this.userId, res => {
-				if (res.status === "success")
-					this.setPlaylists(res.data.playlists);
-				this.orderOfPlaylists = this.calculatePlaylistOrder(); // order in regards to the database
-			})
-		);
-	},
-	beforeUnmount() {
-		this.socket.dispatch(
-			"apis.leaveRoom",
-			`profile.${this.userId}.playlists`,
-			() => {}
-		);
-	},
-	methods: {
-		...mapActions("modalVisibility", ["openModal"]),
-		...mapActions("user/playlists", ["setPlaylists"])
-	}
-};
-</script>

+ 0 - 320
frontend/src/pages/Station/Sidebar/Playlists.vue

@@ -1,320 +0,0 @@
-<template>
-	<div id="my-playlists">
-		<div class="menu-list scrollable-list" v-if="playlists.length > 0">
-			<draggable
-				:component-data="{
-					name: !drag ? 'draggable-list-transition' : null
-				}"
-				v-model="playlists"
-				item-key="_id"
-				v-bind="dragOptions"
-				@start="drag = true"
-				@end="drag = false"
-				@change="savePlaylistOrder"
-			>
-				<template #item="{ element }">
-					<playlist-item :playlist="element" class="item-draggable">
-						<template #actions>
-							<i
-								v-if="isBlacklisted(element._id)"
-								class="material-icons stop-icon"
-								content="This playlist is blacklisted in this station"
-								v-tippy="{ theme: 'info' }"
-								>play_disabled</i
-							>
-							<i
-								v-if="
-									station.type === 'community' &&
-									isOwnerOrAdmin() &&
-									!isSelected(element._id) &&
-									!isBlacklisted(element._id)
-								"
-								@click="selectPlaylist(element)"
-								class="material-icons play-icon"
-								content="Request songs from this playlist"
-								v-tippy
-								>play_arrow</i
-							>
-							<quick-confirm
-								v-if="
-									station.type === 'community' &&
-									isOwnerOrAdmin() &&
-									isSelected(element._id)
-								"
-								@confirm="deselectPlaylist(element._id)"
-							>
-								<i
-									class="material-icons stop-icon"
-									content="Stop requesting songs from this playlist"
-									v-tippy
-									>stop</i
-								>
-							</quick-confirm>
-							<quick-confirm
-								v-if="
-									station.type === 'community' &&
-									isOwnerOrAdmin() &&
-									!isBlacklisted(element._id)
-								"
-								@confirm="blacklistPlaylist(element._id)"
-							>
-								<i
-									class="material-icons stop-icon"
-									content="Blacklist Playlist"
-									v-tippy
-									>block</i
-								>
-							</quick-confirm>
-							<i
-								@click="
-									openModal({
-										modal: 'editPlaylist',
-										data: { playlistId: element._id }
-									})
-								"
-								class="material-icons edit-icon"
-								content="Edit Playlist"
-								v-tippy
-								>edit</i
-							>
-						</template>
-					</playlist-item>
-				</template>
-			</draggable>
-		</div>
-
-		<p v-else class="nothing-here-text scrollable-list">
-			No Playlists found
-		</p>
-		<button
-			class="button create-playlist tab-actionable-button"
-			@click="openModal('createPlaylist')"
-		>
-			<i class="material-icons icon-with-button">create</i>
-			<span> Create Playlist </span>
-		</button>
-	</div>
-</template>
-
-<script>
-import { mapState, mapActions, mapGetters } from "vuex";
-import Toast from "toasters";
-import ws from "@/ws";
-
-import PlaylistItem from "@/components/PlaylistItem.vue";
-import SortablePlaylists from "@/mixins/SortablePlaylists.vue";
-
-export default {
-	components: { PlaylistItem },
-	mixins: [SortablePlaylists],
-	computed: {
-		currentPlaylists() {
-			if (this.station.type === "community") return this.autoRequest;
-
-			return this.autofill;
-		},
-		...mapState({
-			role: state => state.user.auth.role,
-			userId: state => state.user.auth.userId,
-			loggedIn: state => state.user.auth.loggedIn
-		}),
-		...mapState("station", {
-			autoRequest: state => state.autoRequest,
-			autofill: state => state.autofill,
-			blacklist: state => state.blacklist,
-			songsList: state => state.songsList
-		}),
-		...mapGetters({
-			socket: "websockets/getSocket"
-		})
-	},
-	mounted() {
-		ws.onConnect(this.init);
-
-		this.socket.on("event:station.autofillPlaylist", res => {
-			const { playlist } = res.data;
-			const playlistIndex = this.autofill
-				.map(autofillPlaylist => autofillPlaylist._id)
-				.indexOf(playlist._id);
-			if (playlistIndex === -1) this.autofill.push(playlist);
-		});
-
-		this.socket.on("event:station.blacklistedPlaylist", res => {
-			const { playlist } = res.data;
-			const playlistIndex = this.blacklist
-				.map(blacklistedPlaylist => blacklistedPlaylist._id)
-				.indexOf(playlist._id);
-			if (playlistIndex === -1) this.blacklist.push(playlist);
-		});
-
-		this.socket.on("event:station.removedAutofillPlaylist", res => {
-			const { playlistId } = res.data;
-			const playlistIndex = this.autofill
-				.map(playlist => playlist._id)
-				.indexOf(playlistId);
-			if (playlistIndex >= 0) this.autofill.splice(playlistIndex, 1);
-		});
-
-		this.socket.on("event:station.removedBlacklistedPlaylist", res => {
-			const { playlistId } = res.data;
-			const playlistIndex = this.blacklist
-				.map(playlist => playlist._id)
-				.indexOf(playlistId);
-			if (playlistIndex >= 0) this.blacklist.splice(playlistIndex, 1);
-		});
-	},
-	methods: {
-		init() {
-			/** Get playlists for user */
-			this.socket.dispatch("playlists.indexMyPlaylists", res => {
-				if (res.status === "success")
-					this.setPlaylists(res.data.playlists);
-				this.orderOfPlaylists = this.calculatePlaylistOrder(); // order in regards to the database
-			});
-		},
-		isOwner() {
-			return this.loggedIn && this.userId === this.station.owner;
-		},
-		isAdmin() {
-			return this.loggedIn && this.role === "admin";
-		},
-		isOwnerOrAdmin() {
-			return this.isOwner() || this.isAdmin();
-		},
-		selectPlaylist(playlist) {
-			if (this.station.type === "community") {
-				if (!this.isSelected(playlist.id)) {
-					this.autoRequest.push(playlist);
-					new Toast(
-						"Successfully selected playlist to auto request songs."
-					);
-				} else {
-					new Toast("Error: Playlist already selected.");
-				}
-			} else {
-				this.socket.dispatch(
-					"stations.autofillPlaylist",
-					this.station._id,
-					playlist._id,
-					res => {
-						new Toast(res.message);
-					}
-				);
-			}
-		},
-		deselectPlaylist(id) {
-			return new Promise(resolve => {
-				if (this.station.type === "community") {
-					let selected = false;
-					this.currentPlaylists.forEach((playlist, index) => {
-						if (playlist._id === id) {
-							selected = true;
-							this.autoRequest.splice(index, 1);
-						}
-					});
-					if (selected) {
-						new Toast("Successfully deselected playlist.");
-						resolve();
-					} else {
-						new Toast("Playlist not selected.");
-						resolve();
-					}
-				} else {
-					this.socket.dispatch(
-						"stations.removeAutofillPlaylist",
-						this.station._id,
-						id,
-						res => {
-							new Toast(res.message);
-							resolve();
-						}
-					);
-				}
-			});
-		},
-		isSelected(id) {
-			let selected = false;
-			this.currentPlaylists.forEach(playlist => {
-				if (playlist._id === id) selected = true;
-			});
-			return selected;
-		},
-		isBlacklisted(id) {
-			let selected = false;
-			this.blacklist.forEach(playlist => {
-				if (playlist._id === id) selected = true;
-			});
-			return selected;
-		},
-		async blacklistPlaylist(id) {
-			if (this.isSelected(id)) await this.deselectPlaylist(id);
-
-			this.socket.dispatch(
-				"stations.blacklistPlaylist",
-				this.station._id,
-				id,
-				res => {
-					new Toast(res.message);
-				}
-			);
-		},
-		...mapActions("modalVisibility", ["openModal"]),
-		...mapActions("user/playlists", ["setPlaylists"])
-	}
-};
-</script>
-
-<style lang="less" scoped>
-#my-playlists {
-	background-color: var(--white);
-	margin-bottom: 20px;
-	border-radius: 0 0 @border-radius @border-radius;
-	max-height: 100%;
-}
-
-.night-mode {
-	#my-playlists {
-		background-color: var(--dark-grey-3) !important;
-		border: 0 !important;
-	}
-
-	.draggable-list-ghost {
-		filter: brightness(95%);
-	}
-}
-
-.nothing-here-text {
-	margin-bottom: 10px;
-}
-
-.icons-group {
-	.edit-icon {
-		color: var(--primary-color);
-	}
-}
-
-.menu-list .playlist-item:not(:last-of-type) {
-	margin-bottom: 10px;
-}
-
-.create-playlist {
-	width: 100%;
-	height: 40px;
-	border-radius: @border-radius;
-	border: 0;
-
-	&:active,
-	&:focus {
-		border: 0;
-	}
-}
-
-.draggable-list-transition-move {
-	transition: transform 0.5s;
-}
-
-.draggable-list-ghost {
-	opacity: 0.5;
-	filter: brightness(95%);
-}
-</style>