Browse Source

refactor(EditPlaylist): Converted to composition API

Owen Diffey 2 years ago
parent
commit
88edd68afa
1 changed files with 340 additions and 364 deletions
  1. 340 364
      frontend/src/components/modals/EditPlaylist/index.vue

+ 340 - 364
frontend/src/components/modals/EditPlaylist/index.vue

@@ -1,3 +1,329 @@
+<script setup lang="ts">
+import { useStore } from "vuex";
+import {
+	defineAsyncComponent,
+	ref,
+	computed,
+	onMounted,
+	onBeforeUnmount
+} from "vue";
+import { Sortable } from "sortablejs-vue3";
+import Toast from "toasters";
+import { useModalState, useModalActions } from "@/vuex_helpers";
+import ws from "@/ws";
+import utils from "@/utils";
+
+const SongItem = defineAsyncComponent(
+	() => import("@/components/SongItem.vue")
+);
+const Settings = defineAsyncComponent(() => import("./Tabs/Settings.vue"));
+const AddSongs = defineAsyncComponent(() => import("./Tabs/AddSongs.vue"));
+const ImportPlaylists = defineAsyncComponent(
+	() => import("./Tabs/ImportPlaylists.vue")
+);
+
+const props = defineProps({
+	modalUuid: { type: String, default: "" }
+});
+
+const store = useStore();
+
+const station = computed(() => store.state.station);
+const loggedIn = computed(() => store.state.user.auth.loggedIn);
+const userId = computed(() => store.state.user.auth.userId);
+const userRole = computed(() => store.state.user.auth.role);
+
+const { socket } = store.state.websockets;
+
+const drag = ref(false);
+const apiDomain = ref("");
+const gettingSongs = ref(false);
+const tabs = ref([]);
+const songItems = ref([]);
+
+const playlistSongs = computed({
+	get: () => store.state.modals.editPlaylist[props.modalUuid].playlist.songs,
+	set: value => {
+		store.commit(
+			`modals/editPlaylist/${props.modalUuid}/updatePlaylistSongs`,
+			value
+		);
+	}
+});
+
+const modalState = useModalState("modals/editPlaylist/MODAL_UUID", {
+	modalUuid: props.modalUuid
+});
+const playlistId = computed(() => modalState.playlistId);
+const tab = computed(() => modalState.tab);
+const playlist = computed(() => modalState.playlist);
+
+const { setPlaylist, clearPlaylist, addSong, removeSong, repositionedSong } =
+	useModalActions(
+		"modals/editPlaylist/MODAL_UUID",
+		[
+			"setPlaylist",
+			"clearPlaylist",
+			"addSong",
+			"removeSong",
+			"repositionedSong"
+		],
+		{
+			modalUuid: props.modalUuid
+		}
+	);
+
+const closeCurrentModal = () =>
+	store.dispatch("modalVisibility/closeCurrentModal");
+const showTab = payload => {
+	tabs.value[`${payload}-tab`].scrollIntoView({ block: "nearest" });
+	store.dispatch(`modals/editPlaylist/${props.modalUuid}/showTab`, payload);
+};
+
+const isEditable = () =>
+	(playlist.value.type === "user" ||
+		playlist.value.type === "user-liked" ||
+		playlist.value.type === "user-disliked") &&
+	(userId.value === playlist.value.createdBy || userRole.value === "admin");
+
+const dragOptions = computed(() => ({
+	animation: 200,
+	group: "songs",
+	disabled: !isEditable(),
+	ghostClass: "draggable-list-ghost"
+}));
+
+const init = () => {
+	gettingSongs.value = true;
+	socket.dispatch("playlists.getPlaylist", playlistId.value, res => {
+		if (res.status === "success") {
+			setPlaylist(res.data.playlist);
+		} else new Toast(res.message);
+		gettingSongs.value = false;
+	});
+};
+
+const isAdmin = () => userRole.value === "admin";
+
+const isOwner = () =>
+	loggedIn.value && userId.value === playlist.value.createdBy;
+
+const repositionSong = ({ oldIndex, newIndex }) => {
+	if (oldIndex === newIndex) return; // we only need to update when song is moved
+	const song = playlistSongs.value[oldIndex];
+
+	socket.dispatch(
+		"playlists.repositionSong",
+		playlist.value._id,
+		{
+			...song,
+			oldIndex,
+			newIndex
+		},
+		res => {
+			if (res.status !== "success")
+				repositionedSong({
+					...song,
+					newIndex: oldIndex,
+					oldIndex: newIndex
+				});
+		}
+	);
+};
+
+const moveSongToTop = (song, index) => {
+	songItems.value[`song-item-${index}`].$refs.songActions.tippy.hide();
+
+	repositionSong({
+		oldIndex: index,
+		newIndex: 0
+	});
+};
+
+const moveSongToBottom = (song, index) => {
+	songItems.value[`song-item-${index}`].$refs.songActions.tippy.hide();
+
+	repositionSong({
+		oldIndex: index,
+		newIndex: playlistSongs.value.length
+	});
+};
+
+const totalLength = () => {
+	let length = 0;
+	playlist.value.songs.forEach(song => {
+		length += song.duration;
+	});
+	return utils.formatTimeLong(length);
+};
+
+// const shuffle = () => {
+// 	socket.dispatch("playlists.shuffle", playlist.value._id, res => {
+// 		new Toast(res.message);
+// 		if (res.status === "success") {
+// 			updatePlaylistSongs(
+// 				res.data.playlist.songs.sort((a, b) => a.position - b.position)
+// 			);
+// 		}
+// 	});
+// };
+
+const removeSongFromPlaylist = id =>
+	socket.dispatch(
+		"playlists.removeSongFromPlaylist",
+		id,
+		playlist.value._id,
+		res => {
+			new Toast(res.message);
+		}
+	);
+
+const removePlaylist = () => {
+	if (isOwner()) {
+		socket.dispatch("playlists.remove", playlist.value._id, res => {
+			new Toast(res.message);
+			if (res.status === "success") closeCurrentModal();
+		});
+	} else if (isAdmin()) {
+		socket.dispatch("playlists.removeAdmin", playlist.value._id, res => {
+			new Toast(res.message);
+			if (res.status === "success") closeCurrentModal();
+		});
+	}
+};
+
+const downloadPlaylist = async () => {
+	if (apiDomain.value === "")
+		apiDomain.value = await lofig.get("backend.apiDomain");
+
+	fetch(`${apiDomain.value}/export/playlist/${playlist.value._id}`, {
+		credentials: "include"
+	})
+		.then(res => res.blob())
+		.then(blob => {
+			const url = window.URL.createObjectURL(blob);
+
+			const a = document.createElement("a");
+			a.style.display = "none";
+			a.href = url;
+
+			a.download = `musare-playlist-${
+				playlist.value._id
+			}-${new Date().toISOString()}.json`;
+
+			document.body.appendChild(a);
+			a.click();
+			window.URL.revokeObjectURL(url);
+
+			new Toast("Successfully downloaded playlist.");
+		})
+		.catch(() => new Toast("Failed to export and download playlist."));
+};
+
+const addSongToQueue = youtubeId => {
+	socket.dispatch(
+		"stations.addToQueue",
+		station.value._id,
+		youtubeId,
+		data => {
+			if (data.status !== "success")
+				new Toast({
+					content: `Error: ${data.message}`,
+					timeout: 8000
+				});
+			else new Toast({ content: data.message, timeout: 4000 });
+		}
+	);
+};
+
+const clearAndRefillStationPlaylist = () => {
+	socket.dispatch(
+		"playlists.clearAndRefillStationPlaylist",
+		playlist.value._id,
+		data => {
+			if (data.status !== "success")
+				new Toast({
+					content: `Error: ${data.message}`,
+					timeout: 8000
+				});
+			else new Toast({ content: data.message, timeout: 4000 });
+		}
+	);
+};
+
+const clearAndRefillGenrePlaylist = () => {
+	socket.dispatch(
+		"playlists.clearAndRefillGenrePlaylist",
+		playlist.value._id,
+		data => {
+			if (data.status !== "success")
+				new Toast({
+					content: `Error: ${data.message}`,
+					timeout: 8000
+				});
+			else new Toast({ content: data.message, timeout: 4000 });
+		}
+	);
+};
+
+onMounted(() => {
+	ws.onConnect(init);
+
+	socket.on(
+		"event:playlist.song.added",
+		res => {
+			if (playlist.value._id === res.data.playlistId)
+				addSong(res.data.song);
+		},
+		{ modalUuid: props.modalUuid }
+	);
+
+	socket.on(
+		"event:playlist.song.removed",
+		res => {
+			if (playlist.value._id === res.data.playlistId) {
+				// remove song from array of playlists
+				removeSong(res.data.youtubeId);
+			}
+		},
+		{ modalUuid: props.modalUuid }
+	);
+
+	socket.on(
+		"event:playlist.displayName.updated",
+		res => {
+			if (playlist.value._id === res.data.playlistId) {
+				setPlaylist({
+					displayName: res.data.displayName,
+					...playlist.value
+				});
+			}
+		},
+		{ modalUuid: props.modalUuid }
+	);
+
+	socket.on(
+		"event:playlist.song.repositioned",
+		res => {
+			if (playlist.value._id === res.data.playlistId) {
+				const { song, playlistId } = res.data;
+
+				if (playlist.value._id === playlistId) {
+					repositionedSong(song);
+				}
+			}
+		},
+		{ modalUuid: props.modalUuid }
+	);
+});
+
+onBeforeUnmount(() => {
+	clearPlaylist();
+	// Delete the VueX module that was created for this modal, after all other cleanup tasks are performed
+	store.unregisterModule(["modals", "editPlaylist", props.modalUuid]);
+});
+</script>
+
 <template>
 	<modal
 		:title="
@@ -23,7 +349,7 @@
 						<button
 							class="button is-default"
 							:class="{ selected: tab === 'settings' }"
-							ref="settings-tab"
+							:ref="el => (tabs['settings-tab'] = el)"
 							@click="showTab('settings')"
 							v-if="
 								userId === playlist.createdBy ||
@@ -36,7 +362,7 @@
 						<button
 							class="button is-default"
 							:class="{ selected: tab === 'add-songs' }"
-							ref="add-songs-tab"
+							:ref="el => (tabs['add-songs-tab'] = el)"
 							@click="showTab('add-songs')"
 							v-if="isEditable()"
 						>
@@ -47,7 +373,7 @@
 							:class="{
 								selected: tab === 'import-playlists'
 							}"
-							ref="import-playlists-tab"
+							:ref="el => (tabs['import-playlists-tab'] = el)"
 							@click="showTab('import-playlists')"
 							v-if="isEditable()"
 						>
@@ -92,17 +418,17 @@
 					</div>
 
 					<aside class="menu">
-						<draggable
+						<sortable
 							:component-data="{
 								name: !drag ? 'draggable-list-transition' : null
 							}"
 							v-if="playlistSongs.length > 0"
-							v-model="playlistSongs"
+							:list="playlistSongs"
 							item-key="_id"
-							v-bind="dragOptions"
+							:options="dragOptions"
 							@start="drag = true"
 							@end="drag = false"
-							@change="repositionSong"
+							@update="repositionSong"
 						>
 							<template #item="{ element, index }">
 								<div class="menu-list scrollable-list">
@@ -111,7 +437,12 @@
 										:class="{
 											'item-draggable': isEditable()
 										}"
-										:ref="`song-item-${index}`"
+										:ref="
+											el =>
+												(songItems[
+													`song-item-${index}`
+												] = el)
+										"
 									>
 										<template #tippyActions>
 											<i
@@ -193,7 +524,7 @@
 									</song-item>
 								</div>
 							</template>
-						</draggable>
+						</sortable>
 						<p v-else-if="gettingSongs" class="nothing-here-text">
 							Loading songs...
 						</p>
@@ -246,361 +577,6 @@
 	</modal>
 </template>
 
-<script>
-import { mapState, mapGetters, mapActions } from "vuex";
-import draggable from "vuedraggable";
-import Toast from "toasters";
-
-import { mapModalState, mapModalActions } from "@/vuex_helpers";
-import ws from "@/ws";
-import SongItem from "../../SongItem.vue";
-
-import Settings from "./Tabs/Settings.vue";
-import AddSongs from "./Tabs/AddSongs.vue";
-import ImportPlaylists from "./Tabs/ImportPlaylists.vue";
-
-import utils from "@/utils";
-
-export default {
-	components: {
-		draggable,
-		SongItem,
-		Settings,
-		AddSongs,
-		ImportPlaylists
-	},
-	props: {
-		modalUuid: { type: String, default: "" }
-	},
-	data() {
-		return {
-			utils,
-			drag: false,
-			apiDomain: "",
-			gettingSongs: false
-		};
-	},
-	computed: {
-		...mapState("station", {
-			station: state => state.station
-		}),
-		...mapModalState("modals/editPlaylist/MODAL_UUID", {
-			playlistId: state => state.playlistId,
-			tab: state => state.tab,
-			playlist: state => state.playlist
-		}),
-		playlistSongs: {
-			get() {
-				return this.$store.state.modals.editPlaylist[this.modalUuid]
-					.playlist.songs;
-			},
-			set(value) {
-				this.$store.commit(
-					`modals/editPlaylist/${this.modalUuid}/updatePlaylistSongs`,
-					value
-				);
-			}
-		},
-		...mapState({
-			loggedIn: state => state.user.auth.loggedIn,
-			userId: state => state.user.auth.userId,
-			userRole: state => state.user.auth.role
-		}),
-		dragOptions() {
-			return {
-				animation: 200,
-				group: "songs",
-				disabled: !this.isEditable(),
-				ghostClass: "draggable-list-ghost"
-			};
-		},
-		...mapGetters({
-			socket: "websockets/getSocket"
-		})
-	},
-	mounted() {
-		ws.onConnect(this.init);
-
-		this.socket.on(
-			"event:playlist.song.added",
-			res => {
-				if (this.playlist._id === res.data.playlistId)
-					this.addSong(res.data.song);
-			},
-			{ modalUuid: this.modalUuid }
-		);
-
-		this.socket.on(
-			"event:playlist.song.removed",
-			res => {
-				if (this.playlist._id === res.data.playlistId) {
-					// remove song from array of playlists
-					this.removeSong(res.data.youtubeId);
-				}
-			},
-			{ modalUuid: this.modalUuid }
-		);
-
-		this.socket.on(
-			"event:playlist.displayName.updated",
-			res => {
-				if (this.playlist._id === res.data.playlistId) {
-					const playlist = {
-						displayName: res.data.displayName,
-						...this.playlist
-					};
-					this.setPlaylist(playlist);
-				}
-			},
-			{ modalUuid: this.modalUuid }
-		);
-
-		this.socket.on(
-			"event:playlist.song.repositioned",
-			res => {
-				if (this.playlist._id === res.data.playlistId) {
-					const { song, playlistId } = res.data;
-
-					if (this.playlist._id === playlistId) {
-						this.repositionedSong(song);
-					}
-				}
-			},
-			{ modalUuid: this.modalUuid }
-		);
-	},
-	beforeUnmount() {
-		this.clearPlaylist();
-		// Delete the VueX module that was created for this modal, after all other cleanup tasks are performed
-		this.$store.unregisterModule([
-			"modals",
-			"editPlaylist",
-			this.modalUuid
-		]);
-	},
-	methods: {
-		init() {
-			this.gettingSongs = true;
-			this.socket.dispatch(
-				"playlists.getPlaylist",
-				this.playlistId,
-				res => {
-					if (res.status === "success") {
-						this.setPlaylist(res.data.playlist);
-					} else new Toast(res.message);
-					this.gettingSongs = false;
-				}
-			);
-		},
-		isEditable() {
-			return (
-				(this.playlist.type === "user" ||
-					this.playlist.type === "user-liked" ||
-					this.playlist.type === "user-disliked") &&
-				(this.userId === this.playlist.createdBy ||
-					this.userRole === "admin")
-			);
-		},
-		isAdmin() {
-			return this.userRole === "admin";
-		},
-		isOwner() {
-			return this.loggedIn && this.userId === this.playlist.createdBy;
-		},
-		repositionSong({ moved }) {
-			if (!moved) return; // we only need to update when song is moved
-
-			this.socket.dispatch(
-				"playlists.repositionSong",
-				this.playlist._id,
-				{
-					...moved.element,
-					oldIndex: moved.oldIndex,
-					newIndex: moved.newIndex
-				},
-				res => {
-					if (res.status !== "success")
-						this.repositionedSong({
-							...moved.element,
-							newIndex: moved.oldIndex,
-							oldIndex: moved.newIndex
-						});
-				}
-			);
-		},
-		moveSongToTop(song, index) {
-			this.$refs[`song-item-${index}`].$refs.songActions.tippy.hide();
-
-			this.repositionSong({
-				moved: {
-					element: song,
-					oldIndex: index,
-					newIndex: 0
-				}
-			});
-		},
-		moveSongToBottom(song, index) {
-			this.$refs[`song-item-${index}`].$refs.songActions.tippy.hide();
-
-			this.repositionSong({
-				moved: {
-					element: song,
-					oldIndex: index,
-					newIndex: this.playlistSongs.length
-				}
-			});
-		},
-		totalLength() {
-			let length = 0;
-			this.playlist.songs.forEach(song => {
-				length += song.duration;
-			});
-			return this.utils.formatTimeLong(length);
-		},
-		shuffle() {
-			this.socket.dispatch(
-				"playlists.shuffle",
-				this.playlist._id,
-				res => {
-					new Toast(res.message);
-					if (res.status === "success") {
-						this.updatePlaylistSongs(
-							res.data.playlist.songs.sort(
-								(a, b) => a.position - b.position
-							)
-						);
-					}
-				}
-			);
-		},
-		removeSongFromPlaylist(id) {
-			return this.socket.dispatch(
-				"playlists.removeSongFromPlaylist",
-				id,
-				this.playlist._id,
-				res => {
-					new Toast(res.message);
-				}
-			);
-		},
-		removePlaylist() {
-			if (this.isOwner()) {
-				this.socket.dispatch(
-					"playlists.remove",
-					this.playlist._id,
-					res => {
-						new Toast(res.message);
-						if (res.status === "success")
-							this.closeModal("editPlaylist");
-					}
-				);
-			} else if (this.isAdmin()) {
-				this.socket.dispatch(
-					"playlists.removeAdmin",
-					this.playlist._id,
-					res => {
-						new Toast(res.message);
-						if (res.status === "success")
-							this.closeModal("editPlaylist");
-					}
-				);
-			}
-		},
-		async downloadPlaylist() {
-			if (this.apiDomain === "")
-				this.apiDomain = await lofig.get("backend.apiDomain");
-
-			fetch(`${this.apiDomain}/export/playlist/${this.playlist._id}`, {
-				credentials: "include"
-			})
-				.then(res => res.blob())
-				.then(blob => {
-					const url = window.URL.createObjectURL(blob);
-
-					const a = document.createElement("a");
-					a.style.display = "none";
-					a.href = url;
-
-					a.download = `musare-playlist-${
-						this.playlist._id
-					}-${new Date().toISOString()}.json`;
-
-					document.body.appendChild(a);
-					a.click();
-					window.URL.revokeObjectURL(url);
-
-					new Toast("Successfully downloaded playlist.");
-				})
-				.catch(
-					() => new Toast("Failed to export and download playlist.")
-				);
-		},
-		addSongToQueue(youtubeId) {
-			this.socket.dispatch(
-				"stations.addToQueue",
-				this.station._id,
-				youtubeId,
-				data => {
-					if (data.status !== "success")
-						new Toast({
-							content: `Error: ${data.message}`,
-							timeout: 8000
-						});
-					else new Toast({ content: data.message, timeout: 4000 });
-				}
-			);
-		},
-		clearAndRefillStationPlaylist() {
-			this.socket.dispatch(
-				"playlists.clearAndRefillStationPlaylist",
-				this.playlist._id,
-				data => {
-					if (data.status !== "success")
-						new Toast({
-							content: `Error: ${data.message}`,
-							timeout: 8000
-						});
-					else new Toast({ content: data.message, timeout: 4000 });
-				}
-			);
-		},
-		clearAndRefillGenrePlaylist() {
-			this.socket.dispatch(
-				"playlists.clearAndRefillGenrePlaylist",
-				this.playlist._id,
-				data => {
-					if (data.status !== "success")
-						new Toast({
-							content: `Error: ${data.message}`,
-							timeout: 8000
-						});
-					else new Toast({ content: data.message, timeout: 4000 });
-				}
-			);
-		},
-		...mapActions({
-			showTab(dispatch, payload) {
-				this.$refs[`${payload}-tab`].scrollIntoView({
-					block: "nearest"
-				});
-				return dispatch(
-					`modals/editPlaylist/${this.modalUuid}/showTab`,
-					payload
-				);
-			}
-		}),
-		...mapModalActions("modals/editPlaylist/MODAL_UUID", [
-			"setPlaylist",
-			"clearPlaylist",
-			"addSong",
-			"removeSong",
-			"repositionedSong"
-		]),
-		...mapActions("modalVisibility", ["openModal", "closeModal"])
-	}
-};
-</script>
-
 <style lang="less" scoped>
 .night-mode {
 	.label,