Browse Source

Added tab system to EditPlaylist modal, fixed small bug, switched a lot of EditSong to VueX

Kristian Vos 3 years ago
parent
commit
14cf79d6cf

+ 2 - 1
backend/logic/actions/playlists.js

@@ -899,11 +899,12 @@ export default {
 					)
 						.then(response => {
 							const { song } = response;
-							const { _id, title, thumbnail, duration, status } = song;
+							const { _id, title, artists, thumbnail, duration, status } = song;
 							next(null, {
 								_id,
 								youtubeId,
 								title,
+								artists,
 								thumbnail,
 								duration,
 								status

+ 137 - 0
frontend/src/components/modals/EditPlaylist/Tabs/Settings.vue

@@ -0,0 +1,137 @@
+<template>
+	<div class="settings-tab section">
+		<div v-if="isEditable()">
+			<h4 class="section-title">Edit Details</h4>
+
+			<p class="section-description">
+				Change the display name and privacy of the playlist.
+			</p>
+
+			<hr class="section-horizontal-rule" />
+
+			<label class="label"> Change display name </label>
+
+			<div class="control is-grouped input-with-button">
+				<p class="control is-expanded">
+					<input
+						v-model="playlist.displayName"
+						class="input"
+						type="text"
+						placeholder="Playlist Display Name"
+						@keyup.enter="renamePlaylist()"
+					/>
+				</p>
+				<p class="control">
+					<a
+						class="button is-info"
+						@click.prevent="renamePlaylist()"
+						href="#"
+						>Rename</a
+					>
+				</p>
+			</div>
+		</div>
+
+		<div
+			v-if="
+				userId === playlist.createdBy ||
+					(playlist.type === 'genre' && isAdmin())
+			"
+		>
+			<label class="label"> Change privacy </label>
+			<div class="control is-grouped input-with-button">
+				<div class="control is-expanded select">
+					<select v-model="playlist.privacy">
+						<option value="private">Private</option>
+						<option value="public">Public</option>
+					</select>
+				</div>
+				<p class="control">
+					<a
+						class="button is-info"
+						@click.prevent="updatePrivacy()"
+						href="#"
+						>Update Privacy</a
+					>
+				</p>
+			</div>
+		</div>
+	</div>
+</template>
+
+<script>
+import { mapState, mapGetters /* , mapActions */ } from "vuex";
+import Toast from "toasters";
+
+import validation from "@/validation";
+
+export default {
+	data() {
+		return {};
+	},
+	computed: {
+		...mapState("modals/editPlaylist", {
+			playlist: state => state.playlist
+		}),
+		...mapGetters({
+			socket: "websockets/getSocket"
+		}),
+		...mapState({
+			userId: state => state.user.auth.userId,
+			userRole: state => state.user.auth.role
+		})
+	},
+	mounted() {},
+	methods: {
+		isEditable() {
+			return (
+				this.playlist.isUserModifiable &&
+				(this.userId === this.playlist.createdBy ||
+					this.userRole === "admin")
+			);
+		},
+		renamePlaylist() {
+			const { displayName } = this.playlist;
+			if (!validation.isLength(displayName, 2, 32))
+				return new Toast(
+					"Display name must have between 2 and 32 characters."
+				);
+			if (!validation.regex.ascii.test(displayName))
+				return new Toast(
+					"Invalid display name format. Only ASCII characters are allowed."
+				);
+
+			return this.socket.dispatch(
+				"playlists.updateDisplayName",
+				this.playlist._id,
+				this.playlist.displayName,
+				res => {
+					new Toast(res.message);
+				}
+			);
+		},
+		updatePrivacy() {
+			const { privacy } = this.playlist;
+			if (privacy === "public" || privacy === "private") {
+				this.socket.dispatch(
+					"playlists.updatePrivacy",
+					this.playlist._id,
+					privacy,
+					res => {
+						new Toast(res.message);
+					}
+				);
+			}
+		}
+		// 	...mapActions("modals/editSong", ["selectDiscogsInfo"])
+	}
+};
+</script>
+
+<style lang="scss" scoped>
+@media screen and (max-width: 1300px) {
+	.section {
+		max-width: 100% !important;
+	}
+}
+</style>

+ 239 - 0
frontend/src/components/modals/EditPlaylist/Tabs/Youtube.vue

@@ -0,0 +1,239 @@
+<template>
+	<div class="youtube-tab section">
+		<h4 class="section-title">Import from YouTube</h4>
+
+		<p class="section-description">
+			Import a playlist or song by searching or using a link from YouTube.
+		</p>
+
+		<hr class="section-horizontal-rule" />
+
+		<label class="label">
+			Search for a playlist from YouTube
+		</label>
+		<div class="control is-grouped input-with-button">
+			<p class="control is-expanded">
+				<input
+					class="input"
+					type="text"
+					placeholder="Enter YouTube Playlist URL here..."
+					v-model="search.playlist.query"
+					@keyup.enter="importPlaylist()"
+				/>
+			</p>
+			<p class="control has-addons">
+				<span class="select" id="playlist-import-type">
+					<select v-model="search.playlist.isImportingOnlyMusic">
+						<option :value="false">Import all</option>
+						<option :value="true">
+							Import only music
+						</option>
+					</select>
+				</span>
+				<a
+					class="button is-info"
+					@click.prevent="importPlaylist()"
+					href="#"
+					><i class="material-icons icon-with-button">publish</i
+					>Import</a
+				>
+			</p>
+		</div>
+
+		<label class="label">
+			Search for a song from YouTube
+		</label>
+		<div class="control is-grouped input-with-button">
+			<p class="control is-expanded">
+				<input
+					class="input"
+					type="text"
+					placeholder="Enter your YouTube query here..."
+					v-model="search.songs.query"
+					autofocus
+					@keyup.enter="searchForSongs()"
+				/>
+			</p>
+			<p class="control">
+				<a
+					class="button is-info"
+					@click.prevent="searchForSongs()"
+					href="#"
+					><i class="material-icons icon-with-button">search</i
+					>Search</a
+				>
+			</p>
+		</div>
+
+		<div v-if="search.songs.results.length > 0" id="song-query-results">
+			<search-query-item
+				v-for="(result, index) in search.songs.results"
+				:key="result.id"
+				:result="result"
+			>
+				<div slot="actions">
+					<transition name="search-query-actions" mode="out-in">
+						<a
+							class="button is-success"
+							v-if="result.isAddedToQueue"
+							href="#"
+							key="added-to-playlist"
+						>
+							<i class="material-icons icon-with-button">done</i>
+							Added to playlist
+						</a>
+						<a
+							class="button is-dark"
+							v-else
+							@click.prevent="addSongToPlaylist(result.id, index)"
+							href="#"
+							key="add-to-playlist"
+						>
+							<i class="material-icons icon-with-button">add</i>
+							Add to playlist
+						</a>
+					</transition>
+				</div>
+			</search-query-item>
+
+			<a
+				class="button is-primary load-more-button"
+				@click.prevent="loadMoreSongs()"
+				href="#"
+			>
+				Load more...
+			</a>
+		</div>
+	</div>
+</template>
+
+<script>
+import { mapState, mapGetters /* , mapActions */ } from "vuex";
+import Toast from "toasters";
+
+import SearchYoutube from "@/mixins/SearchYoutube.vue";
+
+import SearchQueryItem from "../../../SearchQueryItem.vue";
+
+export default {
+	components: { SearchQueryItem },
+	mixins: [SearchYoutube],
+	data() {
+		return {};
+	},
+	computed: {
+		...mapState("modals/editPlaylist", {
+			playlist: state => state.playlist
+		}),
+		...mapGetters({
+			socket: "websockets/getSocket"
+		})
+	},
+	watch: {
+		"search.songs.results": function checkIfSongInPlaylist(songs) {
+			songs.forEach((searchItem, index) =>
+				this.playlist.songs.find(song => {
+					if (song.youtubeId === searchItem.id)
+						this.search.songs.results[index].isAddedToQueue = true;
+
+					return song.youtubeId === searchItem.id;
+				})
+			);
+		},
+		"playlist.songs": function checkIfSongInPlaylist() {
+			this.search.songs.results.forEach((searchItem, index) =>
+				this.playlist.songs.find(song => {
+					this.search.songs.results[index].isAddedToQueue = false;
+					if (song.youtubeId === searchItem.id)
+						this.search.songs.results[index].isAddedToQueue = true;
+
+					return song.youtubeId === searchItem.id;
+				})
+			);
+		}
+	},
+	mounted() {},
+	methods: {
+		importPlaylist() {
+			let isImportingPlaylist = true;
+
+			// import query is blank
+			if (!this.search.playlist.query)
+				return new Toast("Please enter a YouTube playlist URL.");
+
+			const regex = new RegExp(`[\\?&]list=([^&#]*)`);
+			const splitQuery = regex.exec(this.search.playlist.query);
+
+			if (!splitQuery) {
+				return new Toast({
+					content: "Please enter a valid YouTube playlist URL.",
+					timeout: 4000
+				});
+			}
+
+			// don't give starting import message instantly in case of instant error
+			setTimeout(() => {
+				if (isImportingPlaylist) {
+					new Toast(
+						"Starting to import your playlist. This can take some time to do."
+					);
+				}
+			}, 750);
+
+			return this.socket.dispatch(
+				"playlists.addSetToPlaylist",
+				this.search.playlist.query,
+				this.playlist._id,
+				this.search.playlist.isImportingOnlyMusic,
+				res => {
+					new Toast({ content: res.message, timeout: 20000 });
+					if (res.status === "success") {
+						isImportingPlaylist = false;
+						if (this.search.playlist.isImportingOnlyMusic) {
+							new Toast({
+								content: `${res.data.stats.songsInPlaylistTotal} of the ${res.data.stats.videosInPlaylistTotal} videos in the playlist were songs.`,
+								timeout: 20000
+							});
+						}
+					}
+				}
+			);
+		}
+		// ...mapActions("modals/editSong", ["selectDiscogsInfo"])
+	}
+};
+</script>
+
+<style lang="scss" scoped>
+.youtube-tab {
+	#import-from-youtube-section {
+		#playlist-import-type select {
+			border-radius: 0;
+		}
+
+		#song-query-results {
+			padding: 10px;
+			margin-top: 10px;
+			border: 1px solid var(--light-grey-3);
+			border-radius: 3px;
+			max-width: 565px;
+
+			.search-query-item:not(:last-of-type) {
+				margin-bottom: 10px;
+			}
+		}
+
+		.load-more-button {
+			width: 100%;
+			margin-top: 10px;
+		}
+	}
+}
+
+@media screen and (max-width: 1300px) {
+	.youtube-tab #song-query-results,
+	.section {
+		max-width: 100% !important;
+	}
+}
+</style>

+ 155 - 374
frontend/src/components/modals/EditPlaylist.vue → frontend/src/components/modals/EditPlaylist/index.vue

@@ -19,204 +19,52 @@
 					<h5>Duration: {{ totalLength() }}</h5>
 				</div>
 
-				<div
-					id="playlist-settings-section"
-					v-if="
-						userId === playlist.createdBy ||
-							isEditable() ||
-							(playlist.type === 'genre' && isAdmin())
-					"
-					class="section"
-				>
-					<div v-if="isEditable()">
-						<h4 class="section-title">Edit Details</h4>
-
-						<p class="section-description">
-							Change the display name and privacy of the playlist.
-						</p>
-
-						<hr class="section-horizontal-rule" />
-
-						<label class="label"> Change display name </label>
-
-						<div class="control is-grouped input-with-button">
-							<p class="control is-expanded">
-								<input
-									v-model="playlist.displayName"
-									class="input"
-									type="text"
-									placeholder="Playlist Display Name"
-									@keyup.enter="renamePlaylist()"
-								/>
-							</p>
-							<p class="control">
-								<a
-									class="button is-info"
-									@click.prevent="renamePlaylist()"
-									href="#"
-									>Rename</a
-								>
-							</p>
-						</div>
+				<div id="tabs-container">
+					<div id="tab-selection">
+						<button
+							class="button is-default"
+							:class="{ selected: tab === 'settings' }"
+							ref="settings-tab"
+							@click="showTab('settings')"
+							v-if="
+								userId === playlist.createdBy ||
+									isEditable() ||
+									(playlist.type === 'genre' && isAdmin())
+							"
+						>
+							Settings
+						</button>
+						<button
+							class="button is-default"
+							:class="{ selected: tab === 'youtube' }"
+							ref="youtube-tab"
+							@click="showTab('youtube')"
+							v-if="isEditable()"
+						>
+							YouTube
+						</button>
 					</div>
-
-					<div
+					<settings
+						class="tab"
+						v-show="tab === 'settings'"
 						v-if="
 							userId === playlist.createdBy ||
+								isEditable() ||
 								(playlist.type === 'genre' && isAdmin())
 						"
-					>
-						<label class="label"> Change privacy </label>
-						<div class="control is-grouped input-with-button">
-							<div class="control is-expanded select">
-								<select v-model="playlist.privacy">
-									<option value="private">Private</option>
-									<option value="public">Public</option>
-								</select>
-							</div>
-							<p class="control">
-								<a
-									class="button is-info"
-									@click.prevent="updatePrivacy()"
-									href="#"
-									>Update Privacy</a
-								>
-							</p>
-						</div>
-					</div>
+					/>
+					<youtube
+						class="tab"
+						v-show="tab === 'youtube'"
+						v-if="isEditable()"
+					/>
 				</div>
 
+				<!--
+
 				<div
 					id="import-from-youtube-section"
-					class="section"
-					v-if="isEditable()"
-				>
-					<h4 class="section-title">Import from YouTube</h4>
-
-					<p class="section-description">
-						Import a playlist or song by searching or using a link
-						from YouTube.
-					</p>
-
-					<hr class="section-horizontal-rule" />
-
-					<label class="label">
-						Search for a playlist from YouTube
-					</label>
-					<div class="control is-grouped input-with-button">
-						<p class="control is-expanded">
-							<input
-								class="input"
-								type="text"
-								placeholder="Enter YouTube Playlist URL here..."
-								v-model="search.playlist.query"
-								@keyup.enter="importPlaylist()"
-							/>
-						</p>
-						<p class="control has-addons">
-							<span class="select" id="playlist-import-type">
-								<select
-									v-model="
-										search.playlist.isImportingOnlyMusic
-									"
-								>
-									<option :value="false">Import all</option>
-									<option :value="true">
-										Import only music
-									</option>
-								</select>
-							</span>
-							<a
-								class="button is-info"
-								@click.prevent="importPlaylist()"
-								href="#"
-								><i class="material-icons icon-with-button"
-									>publish</i
-								>Import</a
-							>
-						</p>
-					</div>
-
-					<label class="label">
-						Search for a song from YouTube
-					</label>
-					<div class="control is-grouped input-with-button">
-						<p class="control is-expanded">
-							<input
-								class="input"
-								type="text"
-								placeholder="Enter your YouTube query here..."
-								v-model="search.songs.query"
-								autofocus
-								@keyup.enter="searchForSongs()"
-							/>
-						</p>
-						<p class="control">
-							<a
-								class="button is-info"
-								@click.prevent="searchForSongs()"
-								href="#"
-								><i class="material-icons icon-with-button"
-									>search</i
-								>Search</a
-							>
-						</p>
-					</div>
-
-					<div
-						v-if="search.songs.results.length > 0"
-						id="song-query-results"
-					>
-						<search-query-item
-							v-for="(result, index) in search.songs.results"
-							:key="result.id"
-							:result="result"
-						>
-							<div slot="actions">
-								<transition
-									name="search-query-actions"
-									mode="out-in"
-								>
-									<a
-										class="button is-success"
-										v-if="result.isAddedToQueue"
-										href="#"
-										key="added-to-playlist"
-									>
-										<i
-											class="material-icons icon-with-button"
-											>done</i
-										>
-										Added to playlist
-									</a>
-									<a
-										class="button is-dark"
-										v-else
-										@click.prevent="
-											addSongToPlaylist(result.id, index)
-										"
-										href="#"
-										key="add-to-playlist"
-									>
-										<i
-											class="material-icons icon-with-button"
-											>add</i
-										>
-										Add to playlist
-									</a>
-								</transition>
-							</div>
-						</search-query-item>
-
-						<a
-							class="button is-primary load-more-button"
-							@click.prevent="loadMoreSongs()"
-							href="#"
-						>
-							Load more...
-						</a>
-					</div>
-				</div>
+					 -->
 			</div>
 
 			<div id="second-column">
@@ -235,8 +83,8 @@
 						<draggable
 							class="menu-list scrollable-list"
 							tag="ul"
-							v-if="playlist.songs.length > 0"
-							v-model="playlist.songs"
+							v-if="playlistSongs.length > 0"
+							v-model="playlistSongs"
 							v-bind="dragOptions"
 							@start="drag = true"
 							@end="drag = false"
@@ -249,7 +97,7 @@
 								"
 							>
 								<li
-									v-for="(song, index) in playlist.songs"
+									v-for="(song, index) in playlistSongs"
 									:key="`key-${song._id}`"
 								>
 									<song-item
@@ -300,7 +148,9 @@
 											<i
 												class="material-icons"
 												v-if="isEditable() && index > 0"
-												@click="moveSongToTop(index)"
+												@click="
+													moveSongToTop(song, index)
+												"
 												content="Move to top of Playlist"
 												v-tippy
 												>vertical_align_top</i
@@ -308,11 +158,16 @@
 											<i
 												v-if="
 													isEditable() &&
-														playlist.songs.length -
+														playlistSongs.length -
 															1 !==
 															index
 												"
-												@click="moveSongToBottom(index)"
+												@click="
+													moveSongToBottom(
+														song,
+														index
+													)
+												"
 												class="material-icons"
 												content="Move to bottom of Playlist"
 												v-tippy
@@ -386,25 +241,22 @@ import { mapState, mapGetters, mapActions } from "vuex";
 import draggable from "vuedraggable";
 import Toast from "toasters";
 
-import SearchYoutube from "@/mixins/SearchYoutube.vue";
-
-import validation from "@/validation";
 import Confirm from "@/components/Confirm.vue";
-import Modal from "../Modal.vue";
-import SearchQueryItem from "../SearchQueryItem.vue";
-import SongItem from "../SongItem.vue";
+import Modal from "../../Modal.vue";
+import SongItem from "../../SongItem.vue";
+
+import Settings from "./Tabs/Settings.vue";
+import Youtube from "./Tabs/Youtube.vue";
 
-import utils from "../../../js/utils";
+import utils from "../../../../js/utils";
 
 export default {
-	components: { Modal, draggable, Confirm, SearchQueryItem, SongItem },
-	mixins: [SearchYoutube],
+	components: { Modal, draggable, Confirm, SongItem, Settings, Youtube },
 	data() {
 		return {
 			utils,
 			drag: false,
-			apiDomain: "",
-			playlist: { songs: [] }
+			apiDomain: ""
 		};
 	},
 	computed: {
@@ -414,6 +266,21 @@ export default {
 		...mapState("user/playlists", {
 			editing: state => state.editing
 		}),
+		...mapState("modals/editPlaylist", {
+			tab: state => state.tab,
+			playlist: state => state.playlist
+		}),
+		playlistSongs: {
+			get() {
+				return this.$store.state.modals.editPlaylist.playlist.songs;
+			},
+			set(value) {
+				this.$store.commit(
+					"modals/editPlaylist/updatePlaylistSongs",
+					value
+				);
+			}
+		},
 		...mapState({
 			userId: state => state.user.auth.userId,
 			userRole: state => state.user.auth.role
@@ -430,24 +297,12 @@ export default {
 			socket: "websockets/getSocket"
 		})
 	},
-	watch: {
-		"search.songs.results": function checkIfSongInPlaylist(songs) {
-			songs.forEach((searchItem, index) =>
-				this.playlist.songs.find(song => {
-					if (song.youtubeId === searchItem.id)
-						this.search.songs.results[index].isAddedToQueue = true;
-
-					return song.youtubeId === searchItem.id;
-				})
-			);
-		}
-	},
 	mounted() {
 		this.socket.dispatch("playlists.getPlaylist", this.editing, res => {
 			if (res.status === "success") {
-				this.playlist = res.data.playlist;
-				this.playlist.songs.sort((a, b) => a.position - b.position);
-				this.playlist.oldId = res.data.playlist._id;
+				// this.playlist = res.data.playlist;
+				// this.playlist.songs.sort((a, b) => a.position - b.position);
+				this.setPlaylist(res.data.playlist);
 			} else new Toast(res.message);
 		});
 
@@ -455,7 +310,7 @@ export default {
 			"event:playlist.song.added",
 			res => {
 				if (this.playlist._id === res.data.playlistId)
-					this.playlist.songs.push(res.data.song);
+					this.addSong(res.data.song);
 			},
 			{ modal: "editPlaylist" }
 		);
@@ -465,19 +320,16 @@ export default {
 			res => {
 				if (this.playlist._id === res.data.playlistId) {
 					// remove song from array of playlists
-					this.playlist.songs.forEach((song, index) => {
-						if (song.youtubeId === res.data.youtubeId)
-							this.playlist.songs.splice(index, 1);
-					});
-
-					// if this song is in search results, mark it available to add to the playlist again
-					this.search.songs.results.forEach((searchItem, index) => {
-						if (res.data.youtubeId === searchItem.id) {
-							this.search.songs.results[
-								index
-							].isAddedToQueue = false;
-						}
-					});
+					this.removeSong(res.data.youtubeId);
+
+					// // if this song is in search results, mark it available to add to the playlist again
+					// this.search.songs.results.forEach((searchItem, index) => {
+					// 	if (res.data.youtubeId === searchItem.id) {
+					// 		this.search.songs.results[
+					// 			index
+					// 		].isAddedToQueue = false;
+					// 	}
+					// });
 				}
 			},
 			{ modal: "editPlaylist" }
@@ -486,8 +338,13 @@ export default {
 		this.socket.on(
 			"event:playlist.displayName.updated",
 			res => {
-				if (this.playlist._id === res.data.playlistId)
-					this.playlist.displayName = res.data.displayName;
+				if (this.playlist._id === res.data.playlistId) {
+					const playlist = {
+						displayName: res.data.displayName,
+						...this.playlist
+					};
+					this.setPlaylist(playlist);
+				}
 			},
 			{ modal: "editPlaylist" }
 		);
@@ -499,18 +356,7 @@ export default {
 					const { song, playlistId } = res.data;
 
 					if (this.playlist._id === playlistId) {
-						if (
-							this.playlist.songs[song.newIndex] &&
-							this.playlist.songs[song.newIndex].youtubeId ===
-								song.youtubeId
-						)
-							return;
-
-						this.playlist.songs.splice(
-							song.newIndex,
-							0,
-							this.playlist.songs.splice(song.oldIndex, 1)[0]
-						);
+						this.repositionedSong(song);
 					}
 				}
 			},
@@ -518,51 +364,6 @@ export default {
 		);
 	},
 	methods: {
-		importPlaylist() {
-			let isImportingPlaylist = true;
-
-			// import query is blank
-			if (!this.search.playlist.query)
-				return new Toast("Please enter a YouTube playlist URL.");
-
-			const regex = new RegExp(`[\\?&]list=([^&#]*)`);
-			const splitQuery = regex.exec(this.search.playlist.query);
-
-			if (!splitQuery) {
-				return new Toast({
-					content: "Please enter a valid YouTube playlist URL.",
-					timeout: 4000
-				});
-			}
-
-			// don't give starting import message instantly in case of instant error
-			setTimeout(() => {
-				if (isImportingPlaylist) {
-					new Toast(
-						"Starting to import your playlist. This can take some time to do."
-					);
-				}
-			}, 750);
-
-			return this.socket.dispatch(
-				"playlists.addSetToPlaylist",
-				this.search.playlist.query,
-				this.playlist._id,
-				this.search.playlist.isImportingOnlyMusic,
-				res => {
-					new Toast({ content: res.message, timeout: 20000 });
-					if (res.status === "success") {
-						isImportingPlaylist = false;
-						if (this.search.playlist.isImportingOnlyMusic) {
-							new Toast({
-								content: `${res.data.stats.songsInPlaylistTotal} of the ${res.data.stats.videosInPlaylistTotal} videos in the playlist were songs.`,
-								timeout: 20000
-							});
-						}
-					}
-				}
-			);
-		},
 		isEditable() {
 			return (
 				this.playlist.isUserModifiable &&
@@ -586,7 +387,7 @@ export default {
 				},
 				res => {
 					if (res.status !== "success")
-						this.repositionSongInList({
+						this.repositionedSong({
 							...moved.element,
 							newIndex: moved.oldIndex,
 							oldIndex: moved.newIndex
@@ -595,7 +396,7 @@ export default {
 			);
 		},
 		moveSongToTop(song, index) {
-			this.repositionSongInPlaylist({
+			this.repositionSong({
 				moved: {
 					element: song,
 					oldIndex: index,
@@ -604,11 +405,11 @@ export default {
 			});
 		},
 		moveSongToBottom(song, index) {
-			this.repositionSongInPlaylist({
+			this.repositionSong({
 				moved: {
 					element: song,
 					oldIndex: index,
-					newIndex: this.songsList.length
+					newIndex: this.playlistSongs.length
 				}
 			});
 		},
@@ -626,26 +427,15 @@ export default {
 				res => {
 					new Toast(res.message);
 					if (res.status === "success") {
-						this.playlist.songs = res.data.playlist.songs.sort(
-							(a, b) => a.position - b.position
+						this.updatePlaylistSongs(
+							res.data.playlist.songs.sort(
+								(a, b) => a.position - b.position
+							)
 						);
 					}
 				}
 			);
 		},
-		addSongToPlaylist(id, index) {
-			this.socket.dispatch(
-				"playlists.addSongToPlaylist",
-				false,
-				id,
-				this.playlist._id,
-				res => {
-					new Toast(res.message);
-					if (res.status === "success")
-						this.search.songs.results[index].isAddedToQueue = true;
-				}
-			);
-		},
 		removeSongFromPlaylist(id) {
 			if (this.playlist.displayName === "Liked Songs")
 				return this.socket.dispatch("songs.unlike", id, res => {
@@ -666,26 +456,6 @@ export default {
 				}
 			);
 		},
-		renamePlaylist() {
-			const { displayName } = this.playlist;
-			if (!validation.isLength(displayName, 2, 32))
-				return new Toast(
-					"Display name must have between 2 and 32 characters."
-				);
-			if (!validation.regex.ascii.test(displayName))
-				return new Toast(
-					"Invalid display name format. Only ASCII characters are allowed."
-				);
-
-			return this.socket.dispatch(
-				"playlists.updateDisplayName",
-				this.playlist._id,
-				this.playlist.displayName,
-				res => {
-					new Toast(res.message);
-				}
-			);
-		},
 		removePlaylist() {
 			this.socket.dispatch("playlists.remove", this.playlist._id, res => {
 				new Toast(res.message);
@@ -722,19 +492,6 @@ export default {
 					() => new Toast("Failed to export and download playlist.")
 				);
 		},
-		updatePrivacy() {
-			const { privacy } = this.playlist;
-			if (privacy === "public" || privacy === "private") {
-				this.socket.dispatch(
-					"playlists.updatePrivacy",
-					this.playlist._id,
-					privacy,
-					res => {
-						new Toast(res.message);
-					}
-				);
-			}
-		},
 		addSongToQueue(youtubeId) {
 			this.socket.dispatch(
 				"stations.addToQueue",
@@ -779,6 +536,18 @@ export default {
 				}
 			);
 		},
+		...mapActions({
+			showTab(dispatch, payload) {
+				this.$refs[`${payload}-tab`].scrollIntoView();
+				return dispatch("modals/editPlaylist/showTab", payload);
+			}
+		}),
+		...mapActions("modals/editPlaylist", [
+			"setPlaylist",
+			"addSong",
+			"removeSong",
+			"repositionedSong"
+		]),
 		...mapActions("modalVisibility", ["openModal", "closeModal"])
 	}
 };
@@ -831,13 +600,48 @@ export default {
 	.edit-playlist-modal .edit-playlist-modal-inner-container {
 		height: auto !important;
 
-		#import-from-youtube-section #song-query-results,
-		.section {
+		/deep/ .section {
 			max-width: 100% !important;
 		}
 	}
 }
 
+#tabs-container {
+	// padding: 16px;
+
+	#tab-selection {
+		display: flex;
+		overflow-x: auto;
+		margin: 24px 10px 0 10px;
+
+		.button {
+			border-radius: 5px 5px 0 0;
+			border: 0;
+			text-transform: uppercase;
+			font-size: 14px;
+			color: var(--dark-grey-3);
+			background-color: var(--light-grey-2);
+			flex-grow: 1;
+			height: 32px;
+
+			&:not(:first-of-type) {
+				margin-left: 5px;
+			}
+		}
+
+		.selected {
+			background-color: var(--primary-color) !important;
+			color: var(--white) !important;
+			font-weight: 600;
+		}
+	}
+	.tab {
+		border: 1px solid var(--light-grey-3);
+		// padding: 15px;
+		border-radius: 0 0 5px 5px;
+	}
+}
+
 .edit-playlist-modal {
 	.edit-playlist-modal-inner-container {
 		display: flex;
@@ -851,7 +655,7 @@ export default {
 				flex-basis: 100%;
 			}
 
-			.section {
+			/deep/ .section {
 				max-width: 100% !important;
 			}
 		}
@@ -863,7 +667,7 @@ export default {
 		justify-content: center;
 	}
 
-	.section {
+	/deep/ .section {
 		padding: 15px !important;
 		margin: 0 10px;
 		max-width: 600px;
@@ -887,7 +691,7 @@ export default {
 		overflow-y: auto;
 		flex-grow: 1;
 
-		.section {
+		/deep/ .section {
 			width: auto;
 		}
 
@@ -910,29 +714,6 @@ export default {
 				margin: 0;
 			}
 		}
-
-		#import-from-youtube-section {
-			#playlist-import-type select {
-				border-radius: 0;
-			}
-
-			#song-query-results {
-				padding: 10px;
-				margin-top: 10px;
-				border: 1px solid var(--light-grey-3);
-				border-radius: 3px;
-				max-width: 565px;
-
-				.search-query-item:not(:last-of-type) {
-					margin-bottom: 10px;
-				}
-			}
-
-			.load-more-button {
-				width: 100%;
-				margin-top: 10px;
-			}
-		}
 	}
 
 	#second-column {

+ 13 - 0
frontend/src/mixins/SearchYoutube.vue

@@ -77,6 +77,19 @@ export default {
 					} else if (res.status === "error") new Toast(res.message);
 				}
 			);
+		},
+		addSongToPlaylist(id, index) {
+			this.socket.dispatch(
+				"playlists.addSongToPlaylist",
+				false,
+				id,
+				this.playlist._id,
+				res => {
+					new Toast(res.message);
+					if (res.status === "success")
+						this.search.songs.results[index].isAddedToQueue = true;
+				}
+			);
 		}
 	}
 };

+ 1 - 1
frontend/src/pages/Admin/tabs/Playlists.vue

@@ -101,7 +101,7 @@ import utils from "../../../../js/utils";
 
 export default {
 	components: {
-		EditPlaylist: () => import("@/components/modals/EditPlaylist.vue"),
+		EditPlaylist: () => import("@/components/modals/EditPlaylist"),
 		UserIdToUsername,
 		Report: () => import("@/components/modals/Report.vue"),
 		EditSong: () => import("@/components/modals/EditSong")

+ 1 - 1
frontend/src/pages/Admin/tabs/Stations.vue

@@ -206,7 +206,7 @@ import ws from "@/ws";
 export default {
 	components: {
 		RequestSong: () => import("@/components/modals/RequestSong.vue"),
-		EditPlaylist: () => import("@/components/modals/EditPlaylist.vue"),
+		EditPlaylist: () => import("@/components/modals/EditPlaylist"),
 		CreatePlaylist: () => import("@/components/modals/CreatePlaylist.vue"),
 		ManageStationOwen: () =>
 			import("@/components/modals/ManageStationOwen/index.vue"),

+ 1 - 1
frontend/src/pages/Profile/index.vue

@@ -124,7 +124,7 @@ export default {
 		ProfilePicture,
 		RecentActivity,
 		Playlists,
-		EditPlaylist: () => import("@/components/modals/EditPlaylist.vue"),
+		EditPlaylist: () => import("@/components/modals/EditPlaylist"),
 		Report: () => import("@/components/modals/Report.vue"),
 		EditSong: () => import("@/components/modals/EditSong")
 	},

+ 1 - 1
frontend/src/pages/Station/index.vue

@@ -646,7 +646,7 @@ export default {
 		MainHeader,
 		MainFooter,
 		RequestSong: () => import("@/components/modals/RequestSong.vue"),
-		EditPlaylist: () => import("@/components/modals/EditPlaylist.vue"),
+		EditPlaylist: () => import("@/components/modals/EditPlaylist"),
 		CreatePlaylist: () => import("@/components/modals/CreatePlaylist.vue"),
 		ManageStationOwen: () =>
 			import("@/components/modals/ManageStationOwen/index.vue"),

+ 2 - 0
frontend/src/store/index.js

@@ -11,6 +11,7 @@ import station from "./modules/station";
 import admin from "./modules/admin";
 
 import editSongModal from "./modules/modals/editSong";
+import editPlaylistModal from "./modules/modals/editPlaylist";
 import manageStationModal from "./modules/modals/manageStation";
 import editUserModal from "./modules/modals/editUser";
 import viewPunishmentModal from "./modules/modals/viewPunishment";
@@ -31,6 +32,7 @@ export default new Vuex.Store({
 			namespaced: true,
 			modules: {
 				editSong: editSongModal,
+				editPlaylist: editPlaylistModal,
 				manageStation: manageStationModal,
 				editUser: editUserModal,
 				viewPunishment: viewPunishmentModal,

+ 56 - 0
frontend/src/store/modules/modals/editPlaylist.js

@@ -0,0 +1,56 @@
+/* eslint no-param-reassign: 0 */
+
+// import Vue from "vue";
+// import admin from "@/api/admin/index";
+
+export default {
+	namespaced: true,
+	state: {
+		tab: "settings",
+		playlist: { songs: [] }
+	},
+	getters: {},
+	actions: {
+		showTab: ({ commit }, tab) => commit("showTab", tab),
+		setPlaylist: ({ commit }, playlist) => commit("setPlaylist", playlist),
+		addSong: ({ commit }, song) => commit("addSong", song),
+		removeSong: ({ commit }, youtubeId) => commit("removeSong", youtubeId),
+		updatePlaylistSongs: ({ commit }, playlistSongs) =>
+			commit("updatePlaylistSongs", playlistSongs),
+		repositionedSong: ({ commit }, song) => commit("repositionedSong", song)
+	},
+	mutations: {
+		showTab(state, tab) {
+			state.tab = tab;
+		},
+		setPlaylist(state, playlist) {
+			state.playlist = { ...playlist };
+			state.playlist.songs.sort((a, b) => a.position - b.position);
+		},
+		addSong(state, song) {
+			state.playlist.songs.push(song);
+		},
+		removeSong(state, youtubeId) {
+			state.playlist.songs.forEach((song, index) => {
+				if (song.youtubeId === youtubeId)
+					state.playlist.songs.splice(index, 1);
+			});
+		},
+		updatePlaylistSongs(state, playlistSongs) {
+			state.playlist.songs = playlistSongs;
+		},
+		repositionedSong(state, song) {
+			if (
+				state.playlist.songs[song.newIndex] &&
+				state.playlist.songs[song.newIndex].youtubeId === song.youtubeId
+			)
+				return;
+
+			state.playlist.songs.splice(
+				song.newIndex,
+				0,
+				state.playlist.songs.splice(song.oldIndex, 1)[0]
+			);
+		}
+	}
+};