Эх сурвалжийг харах

Worked on experimental ManageStation design (WIP)

Kristian Vos 4 жил өмнө
parent
commit
668df41ad0

+ 335 - 143
frontend/src/components/modals/ManageStation/Tabs/Playlists.vue

@@ -2,13 +2,6 @@
 	<div class="station-playlists">
 		<div class="tabs-container">
 			<div class="tab-selection">
-				<button
-					class="button is-default"
-					:class="{ selected: tab === 'current' }"
-					@click="showTab('current')"
-				>
-					Current
-				</button>
 				<button
 					class="button is-default"
 					:class="{ selected: tab === 'search' }"
@@ -24,65 +17,29 @@
 				>
 					My Playlists
 				</button>
-			</div>
-			<div class="tab" v-show="tab === 'current'">
-				<div v-if="currentPlaylists.length > 0">
-					<playlist-item
-						v-for="playlist in currentPlaylists"
-						:key="`key-${playlist._id}`"
-						:playlist="playlist"
-						:show-owner="true"
-					>
-						<div class="icons-group" slot="actions">
-							<confirm
-								v-if="isOwnerOrAdmin()"
-								@confirm="deselectPlaylist(playlist._id)"
-							>
-								<i
-									class="material-icons stop-icon"
-									content="Stop playing songs from this playlist"
-									v-tippy
-								>
-									stop
-								</i>
-							</confirm>
-							<confirm
-								v-if="isOwnerOrAdmin()"
-								@confirm="blacklistPlaylist(playlist._id)"
-							>
-								<i
-									class="material-icons stop-icon"
-									content="Blacklist Playlist"
-									v-tippy
-									>block</i
-								>
-							</confirm>
-							<i
-								v-if="playlist.createdBy === myUserId"
-								@click="showPlaylist(playlist._id)"
-								class="material-icons edit-icon"
-								content="Edit Playlist"
-								v-tippy
-								>edit</i
-							>
-							<i
-								v-if="
-									playlist.createdBy !== myUserId &&
-										(playlist.privacy === 'public' ||
-											isAdmin())
-								"
-								@click="showPlaylist(playlist._id)"
-								class="material-icons edit-icon"
-								content="View Playlist"
-								v-tippy
-								>visibility</i
-							>
-						</div>
-					</playlist-item>
-				</div>
-				<p v-else class="has-text-centered scrollable-list">
-					No playlists currently selected.
-				</p>
+				<button
+					class="button is-default"
+					:class="{ selected: tab === 'party' }"
+					v-if="station.type === 'community' && station.partyMode"
+					@click="showTab('party')"
+				>
+					Party
+				</button>
+				<button
+					class="button is-default"
+					:class="{ selected: tab === 'included' }"
+					v-if="!(station.type === 'community' && station.partyMode)"
+					@click="showTab('included')"
+				>
+					Included
+				</button>
+				<button
+					class="button is-default"
+					:class="{ selected: tab === 'excluded' }"
+					@click="showTab('excluded')"
+				>
+					Excluded
+				</button>
 			</div>
 			<div class="tab" v-show="tab === 'search'">
 				<label class="label"> Search for a public playlist </label>
@@ -121,12 +78,30 @@
 							>
 							<confirm
 								v-if="
-									(isOwnerOrAdmin() ||
-										(station.type === 'community' &&
-											station.partyMode)) &&
+									station.type === 'community' &&
+										station.partyMode &&
 										isSelected(playlist._id)
 								"
-								@confirm="deselectPlaylist(playlist._id)"
+								@confirm="deselectPartyPlaylist(playlist._id)"
+							>
+								<i
+									class="material-icons stop-icon"
+									content="Stop playing songs from this playlist"
+									v-tippy
+								>
+									stop
+								</i>
+							</confirm>
+							<confirm
+								v-if="
+									isOwnerOrAdmin() &&
+										!(
+											station.type === 'community' &&
+											station.partyMode
+										) &&
+										isIncluded(playlist._id)
+								"
+								@confirm="removeIncludedPlaylist(playlist._id)"
 							>
 								<i
 									class="material-icons stop-icon"
@@ -138,19 +113,30 @@
 							</confirm>
 							<i
 								v-if="
-									(isOwnerOrAdmin() ||
-										(station.type === 'community' &&
-											station.partyMode)) &&
+									station.type === 'community' &&
+										station.partyMode &&
 										!isSelected(playlist._id) &&
 										!isExcluded(playlist._id)
 								"
-								@click="selectPlaylist(playlist)"
+								@click="selectPartyPlaylist(playlist)"
 								class="material-icons play-icon"
-								:content="
-									station.partyMode
-										? 'Request songs from this playlist'
-										: 'Play songs from this playlist'
+								content="Request songs from this playlist"
+								v-tippy
+								>play_arrow</i
+							>
+							<i
+								v-if="
+									isOwnerOrAdmin() &&
+										!(
+											station.type === 'community' &&
+											station.partyMode
+										) &&
+										!isIncluded(playlist._id) &&
+										!isExcluded(playlist._id)
 								"
+								@click="includePlaylist(playlist)"
+								class="material-icons play-icon"
+								:content="'Play songs from this playlist'"
 								v-tippy
 								>play_arrow</i
 							>
@@ -241,37 +227,61 @@
 								<i
 									v-if="
 										station.type === 'community' &&
-											(isOwnerOrAdmin() ||
-												station.partyMode) &&
+											station.partyMode &&
 											!isSelected(playlist._id) &&
 											!isExcluded(playlist._id)
 									"
-									@click="selectPlaylist(playlist)"
+									@click="selectPartyPlaylist(playlist)"
 									class="material-icons play-icon"
-									:content="
-										station.partyMode
-											? 'Request songs from this playlist'
-											: 'Play songs from this playlist'
+									content="Request songs from this playlist"
+									v-tippy
+									>play_arrow</i
+								>
+								<i
+									v-if="
+										station.type === 'community' &&
+											isOwnerOrAdmin() &&
+											!station.partyMode &&
+											!isSelected(playlist._id) &&
+											!isExcluded(playlist._id)
 									"
+									@click="includePlaylist(playlist)"
+									class="material-icons play-icon"
+									content="Play songs from this playlist"
 									v-tippy
 									>play_arrow</i
 								>
 								<confirm
 									v-if="
 										station.type === 'community' &&
-											(isOwnerOrAdmin() ||
-												station.partyMode) &&
+											station.partyMode &&
 											isSelected(playlist._id)
 									"
-									@confirm="deselectPlaylist(playlist._id)"
+									@confirm="
+										deselectPartyPlaylist(playlist._id)
+									"
 								>
 									<i
 										class="material-icons stop-icon"
-										:content="
-											station.partyMode
-												? 'Stop requesting songs from this playlist'
-												: 'Stop playing songs from this playlist'
-										"
+										content="Stop requesting songs from this playlist"
+										v-tippy
+										>stop</i
+									>
+								</confirm>
+								<confirm
+									v-if="
+										station.type === 'community' &&
+											isOwnerOrAdmin() &&
+											!station.partyMode &&
+											isIncluded(playlist._id)
+									"
+									@confirm="
+										removeIncludedPlaylist(playlist._id)
+									"
+								>
+									<i
+										class="material-icons stop-icon"
+										content="Stop playing songs from this playlist"
 										v-tippy
 										>stop</i
 									>
@@ -305,6 +315,174 @@
 					You don't have any playlists!
 				</p>
 			</div>
+			<div
+				class="tab"
+				v-show="tab === 'party'"
+				v-if="station.type === 'community' && station.partyMode"
+			>
+				<div v-if="partyPlaylists.length > 0">
+					<playlist-item
+						v-for="playlist in partyPlaylists"
+						:key="`key-${playlist._id}`"
+						:playlist="playlist"
+						:show-owner="true"
+					>
+						<div class="icons-group" slot="actions">
+							<confirm
+								v-if="isOwnerOrAdmin()"
+								@confirm="deselectPartyPlaylist(playlist._id)"
+							>
+								<i
+									class="material-icons stop-icon"
+									content="Stop playing songs from this playlist"
+									v-tippy
+								>
+									stop
+								</i>
+							</confirm>
+							<confirm
+								v-if="isOwnerOrAdmin()"
+								@confirm="blacklistPlaylist(playlist._id)"
+							>
+								<i
+									class="material-icons stop-icon"
+									content="Blacklist Playlist"
+									v-tippy
+									>block</i
+								>
+							</confirm>
+							<i
+								v-if="playlist.createdBy === myUserId"
+								@click="showPlaylist(playlist._id)"
+								class="material-icons edit-icon"
+								content="Edit Playlist"
+								v-tippy
+								>edit</i
+							>
+							<i
+								v-if="
+									playlist.createdBy !== myUserId &&
+										(playlist.privacy === 'public' ||
+											isAdmin())
+								"
+								@click="showPlaylist(playlist._id)"
+								class="material-icons edit-icon"
+								content="View Playlist"
+								v-tippy
+								>visibility</i
+							>
+						</div>
+					</playlist-item>
+				</div>
+				<p v-else class="has-text-centered scrollable-list">
+					No playlists currently being played.
+				</p>
+			</div>
+			<div
+				class="tab"
+				v-show="tab === 'included'"
+				v-if="!(station.type === 'community' && station.partyMode)"
+			>
+				<div v-if="includedPlaylists.length > 0">
+					<playlist-item
+						v-for="playlist in includedPlaylists"
+						:key="`key-${playlist._id}`"
+						:playlist="playlist"
+						:show-owner="true"
+					>
+						<div class="icons-group" slot="actions">
+							<confirm
+								v-if="isOwnerOrAdmin()"
+								@confirm="removeIncludedPlaylist(playlist._id)"
+							>
+								<i
+									class="material-icons stop-icon"
+									content="Stop playing songs from this playlist"
+									v-tippy
+								>
+									stop
+								</i>
+							</confirm>
+							<confirm
+								v-if="isOwnerOrAdmin()"
+								@confirm="blacklistPlaylist(playlist._id)"
+							>
+								<i
+									class="material-icons stop-icon"
+									content="Blacklist Playlist"
+									v-tippy
+									>block</i
+								>
+							</confirm>
+							<i
+								v-if="playlist.createdBy === myUserId"
+								@click="showPlaylist(playlist._id)"
+								class="material-icons edit-icon"
+								content="Edit Playlist"
+								v-tippy
+								>edit</i
+							>
+							<i
+								v-if="
+									playlist.createdBy !== myUserId &&
+										(playlist.privacy === 'public' ||
+											isAdmin())
+								"
+								@click="showPlaylist(playlist._id)"
+								class="material-icons edit-icon"
+								content="View Playlist"
+								v-tippy
+								>visibility</i
+							>
+						</div>
+					</playlist-item>
+				</div>
+				<p v-else class="has-text-centered scrollable-list">
+					No playlists currently included.
+				</p>
+			</div>
+			<div class="tab" v-show="tab === 'excluded'">
+				<div v-if="excludedPlaylists.length > 0">
+					<playlist-item
+						:playlist="playlist"
+						v-for="playlist in excludedPlaylists"
+						:key="`key-${playlist._id}`"
+					>
+						<div class="icons-group" slot="actions">
+							<confirm
+								@confirm="removeExcludedPlaylist(playlist._id)"
+							>
+								<i
+									class="material-icons stop-icon"
+									content="Stop blacklisting songs from this playlist
+							"
+									v-tippy
+									>stop</i
+								>
+							</confirm>
+							<i
+								v-if="playlist.createdBy === userId"
+								@click="showPlaylist(playlist._id)"
+								class="material-icons edit-icon"
+								content="Edit Playlist"
+								v-tippy
+								>edit</i
+							>
+							<i
+								v-else
+								@click="showPlaylist(playlist._id)"
+								class="material-icons edit-icon"
+								content="View Playlist"
+								v-tippy
+								>visibility</i
+							>
+						</div>
+					</playlist-item>
+				</div>
+				<p v-else class="has-text-centered scrollable-list">
+					No playlists currently excluded.
+				</p>
+			</div>
 		</div>
 	</div>
 </template>
@@ -325,7 +503,7 @@ export default {
 	mixins: [SortablePlaylists],
 	data() {
 		return {
-			tab: "current",
+			tab: "included",
 			search: {
 				query: "",
 				searchedQuery: "",
@@ -337,12 +515,6 @@ export default {
 		};
 	},
 	computed: {
-		currentPlaylists() {
-			if (this.station.type === "community" && this.station.partyMode) {
-				return this.partyPlaylists;
-			}
-			return this.includedPlaylists;
-		},
 		resultsLeftCount() {
 			return this.search.count - this.search.results.length;
 		},
@@ -367,6 +539,9 @@ export default {
 		})
 	},
 	mounted() {
+		if (this.station.type === "community" && this.station.partyMode)
+			this.showTab("search");
+
 		this.socket.dispatch("playlists.indexMyPlaylists", true, res => {
 			if (res.status === "success") this.setPlaylists(res.data.playlists);
 			this.orderOfPlaylists = this.calculatePlaylistOrder(); // order in regards to the database
@@ -411,68 +586,85 @@ export default {
 			this.editPlaylist(playlistId);
 			this.openModal("editPlaylist");
 		},
-		selectPlaylist(playlist) {
-			if (this.station.type === "community" && this.station.partyMode) {
-				if (!this.isSelected(playlist.id)) {
-					this.partyPlaylists.push(playlist);
-					this.addPartyPlaylistSongToQueue();
-					new Toast(
-						"Successfully selected playlist to auto request songs."
-					);
+		selectPartyPlaylist(playlist) {
+			if (!this.isSelected(playlist.id)) {
+				this.partyPlaylists.push(playlist);
+				this.addPartyPlaylistSongToQueue();
+				new Toast(
+					"Successfully selected playlist to auto request songs."
+				);
+			} else {
+				new Toast("Error: Playlist already selected.");
+			}
+		},
+		includePlaylist(playlist) {
+			this.socket.dispatch(
+				"stations.includePlaylist",
+				this.station._id,
+				playlist._id,
+				res => {
+					new Toast(res.message);
+				}
+			);
+		},
+		deselectPartyPlaylist(id) {
+			return new Promise(resolve => {
+				let selected = false;
+				this.partyPlaylists.forEach((playlist, index) => {
+					if (playlist._id === id) {
+						selected = true;
+						this.partyPlaylists.splice(index, 1);
+					}
+				});
+				if (selected) {
+					new Toast("Successfully deselected playlist.");
+					resolve();
 				} else {
-					new Toast("Error: Playlist already selected.");
+					new Toast("Playlist not selected.");
+					resolve();
 				}
-			} else {
+			});
+		},
+		removeIncludedPlaylist(id) {
+			return new Promise(resolve => {
 				this.socket.dispatch(
-					"stations.includePlaylist",
+					"stations.removeIncludedPlaylist",
 					this.station._id,
-					playlist._id,
+					id,
 					res => {
 						new Toast(res.message);
+						resolve();
 					}
 				);
-			}
+			});
 		},
-		deselectPlaylist(id) {
+		removeExcludedPlaylist(id) {
 			return new Promise(resolve => {
-				if (
-					this.station.type === "community" &&
-					this.station.partyMode
-				) {
-					let selected = false;
-					this.currentPlaylists.forEach((playlist, index) => {
-						if (playlist._id === id) {
-							selected = true;
-							this.partyPlaylists.splice(index, 1);
-						}
-					});
-					if (selected) {
-						new Toast("Successfully deselected playlist.");
-						resolve();
-					} else {
-						new Toast("Playlist not selected.");
+				this.socket.dispatch(
+					"stations.removeExcludedPlaylist",
+					this.station._id,
+					id,
+					res => {
+						new Toast(res.message);
 						resolve();
 					}
-				} else {
-					this.socket.dispatch(
-						"stations.removeIncludedPlaylist",
-						this.station._id,
-						id,
-						res => {
-							new Toast(res.message);
-							resolve();
-						}
-					);
-				}
+				);
 			});
 		},
 		isSelected(id) {
 			let selected = false;
-			this.currentPlaylists.forEach(playlist => {
+			this.partyPlaylists.forEach(playlist => {
 				if (playlist._id === id) selected = true;
 			});
 			return selected;
 		},
+		isIncluded(id) {
+			let included = false;
+			this.includedPlaylists.forEach(playlist => {
+				if (playlist._id === id) included = true;
+			});
+			return included;
+		},
 		isExcluded(id) {
 			let selected = false;
 			this.excludedPlaylists.forEach(playlist => {
@@ -523,7 +715,7 @@ export default {
 			});
 		},
 		async blacklistPlaylist(id) {
-			if (this.isSelected(id)) await this.deselectPlaylist(id);
+			if (this.isIncluded(id)) await this.removeIncludedPlaylist(id);
 
 			this.socket.dispatch(
 				"stations.excludePlaylist",

+ 0 - 245
frontend/src/components/modals/ManageStation/Tabs/Search.vue

@@ -1,245 +0,0 @@
-<template>
-	<div class="search">
-		<div class="musare-search">
-			<label class="label"> Search for a song on Musare </label>
-			<div class="control is-grouped input-with-button">
-				<p class="control is-expanded">
-					<input
-						class="input"
-						type="text"
-						placeholder="Enter your song query here..."
-						v-model="musareSearch.query"
-						@keyup.enter="searchForMusareSongs(1)"
-					/>
-				</p>
-				<p class="control">
-					<a class="button is-info" @click="searchForMusareSongs(1)"
-						><i class="material-icons icon-with-button">search</i
-						>Search</a
-					>
-				</p>
-			</div>
-			<div v-if="musareSearch.results.length > 0">
-				<song-item
-					v-for="song in musareSearch.results"
-					:key="song._id"
-					:song="song"
-				>
-					<div class="song-actions" slot="actions">
-						<i
-							class="material-icons add-to-queue-icon"
-							v-if="station.partyMode && !station.locked"
-							@click="addSongToQueue(song.youtubeId)"
-							content="Add Song to Queue"
-							v-tippy
-							>queue</i
-						>
-					</div>
-				</song-item>
-				<button
-					v-if="resultsLeftCount > 0"
-					class="button is-primary load-more-button"
-					@click="searchForMusareSongs(musareSearch.page + 1)"
-				>
-					Load {{ nextPageResultsCount }} more results
-				</button>
-			</div>
-		</div>
-		<div class="youtube-search">
-			<label class="label"> Search for a song on 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()"
-						><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"
-								key="added-to-queue"
-							>
-								<i class="material-icons icon-with-button"
-									>done</i
-								>
-								Added to queue
-							</a>
-							<a
-								class="button is-dark"
-								v-else
-								@click.prevent="
-									addSongToQueue(result.id, index)
-								"
-								key="add-to-queue"
-							>
-								<i class="material-icons icon-with-button"
-									>add</i
-								>
-								Add to queue
-							</a>
-						</transition>
-					</div>
-				</search-query-item>
-
-				<a
-					class="button is-primary load-more-button"
-					@click.prevent="loadMoreSongs()"
-				>
-					Load more...
-				</a>
-			</div>
-		</div>
-	</div>
-</template>
-
-<script>
-import { mapState, mapGetters } from "vuex";
-
-import Toast from "toasters";
-import SearchYoutube from "@/mixins/SearchYoutube.vue";
-
-import SongItem from "@/components/SongItem.vue";
-import SearchQueryItem from "../../../SearchQueryItem.vue";
-
-export default {
-	components: {
-		SongItem,
-		SearchQueryItem
-	},
-	mixins: [SearchYoutube],
-	data() {
-		return {
-			musareSearch: {
-				query: "",
-				searchedQuery: "",
-				page: 0,
-				count: 0,
-				resultsLeft: 0,
-				results: []
-			}
-		};
-	},
-	computed: {
-		resultsLeftCount() {
-			return this.musareSearch.count - this.musareSearch.results.length;
-		},
-		nextPageResultsCount() {
-			return Math.min(this.musareSearch.pageSize, this.resultsLeftCount);
-		},
-		...mapState("modals/manageStation", {
-			station: state => state.station,
-			originalStation: state => state.originalStation
-		}),
-		...mapGetters({
-			socket: "websockets/getSocket"
-		})
-	},
-	methods: {
-		addSongToQueue(youtubeId, index) {
-			if (this.station.type === "community") {
-				this.socket.dispatch(
-					"stations.addToQueue",
-					this.station._id,
-					youtubeId,
-					res => {
-						if (res.status !== "success")
-							new Toast(`Error: ${res.message}`);
-						else {
-							if (index)
-								this.search.songs.results[
-									index
-								].isAddedToQueue = true;
-
-							new Toast(res.message);
-						}
-					}
-				);
-			} else {
-				this.socket.dispatch("songs.request", youtubeId, res => {
-					if (res.status !== "success")
-						new Toast(`Error: ${res.message}`);
-					else {
-						this.search.songs.results[index].isAddedToQueue = true;
-
-						new Toast(res.message);
-					}
-				});
-			}
-		},
-		searchForMusareSongs(page) {
-			if (
-				this.musareSearch.page >= page ||
-				this.musareSearch.searchedQuery !== this.musareSearch.query
-			) {
-				this.musareSearch.results = [];
-				this.musareSearch.page = 0;
-				this.musareSearch.count = 0;
-				this.musareSearch.resultsLeft = 0;
-				this.musareSearch.pageSize = 0;
-			}
-
-			this.musareSearch.searchedQuery = this.musareSearch.query;
-			this.socket.dispatch(
-				"songs.searchOfficial",
-				this.musareSearch.query,
-				page,
-				res => {
-					const { data } = res;
-					const { count, pageSize, songs } = data;
-					if (res.status === "success") {
-						this.musareSearch.results = [
-							...this.musareSearch.results,
-							...songs
-						];
-						this.musareSearch.page = page;
-						this.musareSearch.count = count;
-						this.musareSearch.resultsLeft =
-							count - this.musareSearch.results.length;
-						this.musareSearch.pageSize = pageSize;
-					} else if (res.status === "error") {
-						this.musareSearch.results = [];
-						this.musareSearch.page = 0;
-						this.musareSearch.count = 0;
-						this.musareSearch.resultsLeft = 0;
-						this.musareSearch.pageSize = 0;
-						new Toast(res.message);
-					}
-				}
-			);
-		}
-	}
-};
-</script>
-
-<style lang="scss">
-.search {
-	.musare-search,
-	.universal-item:not(:last-of-type) {
-		margin-bottom: 10px;
-	}
-	.load-more-button {
-		width: 100%;
-		margin-top: 10px;
-	}
-}
-</style>

+ 425 - 0
frontend/src/components/modals/ManageStation/Tabs/Songs.vue

@@ -0,0 +1,425 @@
+<template>
+	<div class="songs">
+		<div class="tabs-container">
+			<div class="tab-selection">
+				<button
+					class="button is-default"
+					:class="{ selected: tab === 'search' }"
+					v-if="
+						station.type === 'community' &&
+							station.partyMode &&
+							(isOwnerOrAdmin() || !station.locked)
+					"
+					@click="showTab('search')"
+				>
+					Search
+				</button>
+				<button
+					class="button is-default"
+					:class="{ selected: tab === 'included' }"
+					v-if="
+						isOwnerOrAdmin() &&
+							!(station.type === 'community' && station.partyMode)
+					"
+					@click="showTab('included')"
+				>
+					Included
+				</button>
+				<button
+					class="button is-default"
+					:class="{ selected: tab === 'excluded' }"
+					v-if="isOwnerOrAdmin()"
+					@click="showTab('excluded')"
+				>
+					Excluded
+				</button>
+			</div>
+			<div
+				class="tab"
+				v-show="tab === 'search'"
+				v-if="
+					station.type === 'community' &&
+						station.partyMode &&
+						(isOwnerOrAdmin() || !station.locked)
+				"
+			>
+				<div class="musare-songs">
+					<label class="label"> Search for a song on Musare </label>
+					<div class="control is-grouped input-with-button">
+						<p class="control is-expanded">
+							<input
+								class="input"
+								type="text"
+								placeholder="Enter your song query here..."
+								v-model="musareSearch.query"
+								@keyup.enter="searchForMusareSongs(1)"
+							/>
+						</p>
+						<p class="control">
+							<a
+								class="button is-info"
+								@click="searchForMusareSongs(1)"
+								><i class="material-icons icon-with-button"
+									>search</i
+								>Search</a
+							>
+						</p>
+					</div>
+					<div v-if="musareSearch.results.length > 0">
+						<song-item
+							v-for="song in musareSearch.results"
+							:key="song._id"
+							:song="song"
+						>
+							<div class="song-actions" slot="actions">
+								<i
+									class="material-icons add-to-queue-icon"
+									v-if="station.partyMode && !station.locked"
+									@click="addSongToQueue(song.youtubeId)"
+									content="Add Song to Queue"
+									v-tippy
+									>queue</i
+								>
+							</div>
+						</song-item>
+						<button
+							v-if="resultsLeftCount > 0"
+							class="button is-primary load-more-button"
+							@click="searchForMusareSongs(musareSearch.page + 1)"
+						>
+							Load {{ nextPageResultsCount }} more results
+						</button>
+					</div>
+				</div>
+				<div class="youtube-search">
+					<label class="label"> Search for a song on 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()"
+								><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"
+										key="added-to-queue"
+									>
+										<i
+											class="material-icons icon-with-button"
+											>done</i
+										>
+										Added to queue
+									</a>
+									<a
+										class="button is-dark"
+										v-else
+										@click.prevent="
+											addSongToQueue(result.id, index)
+										"
+										key="add-to-queue"
+									>
+										<i
+											class="material-icons icon-with-button"
+											>add</i
+										>
+										Add to queue
+									</a>
+								</transition>
+							</div>
+						</search-query-item>
+
+						<a
+							class="button is-primary load-more-button"
+							@click.prevent="loadMoreSongs()"
+						>
+							Load more...
+						</a>
+					</div>
+				</div>
+			</div>
+			<div
+				class="tab"
+				v-show="tab === 'included'"
+				v-if="
+					isOwnerOrAdmin() &&
+						!(station.type === 'community' && station.partyMode)
+				"
+			>
+				<div v-if="includedSongs.length > 0">
+					<song-item
+						v-for="song in includedSongs"
+						:key="song._id"
+						:song="song"
+					>
+					</song-item>
+				</div>
+				<p v-else class="has-text-centered scrollable-list">
+					No songs currently included. To include songs, include a
+					playlist.
+				</p>
+			</div>
+			<div
+				class="tab"
+				v-show="tab === 'excluded'"
+				v-if="isOwnerOrAdmin()"
+			>
+				<div v-if="excludedSongs.length > 0">
+					<song-item
+						v-for="song in excludedSongs"
+						:key="song._id"
+						:song="song"
+					>
+					</song-item>
+				</div>
+				<p v-else class="has-text-centered scrollable-list">
+					No songs currently excluded. To excluded songs, exclude a
+					playlist.
+				</p>
+			</div>
+		</div>
+	</div>
+</template>
+
+<script>
+import { mapState, mapGetters } from "vuex";
+
+import Toast from "toasters";
+import SearchYoutube from "@/mixins/SearchYoutube.vue";
+
+import SongItem from "@/components/SongItem.vue";
+import SearchQueryItem from "../../../SearchQueryItem.vue";
+
+export default {
+	components: {
+		SongItem,
+		SearchQueryItem
+	},
+	mixins: [SearchYoutube],
+	data() {
+		return {
+			tab: "search",
+			musareSearch: {
+				query: "",
+				searchedQuery: "",
+				page: 0,
+				count: 0,
+				resultsLeft: 0,
+				results: []
+			}
+		};
+	},
+	computed: {
+		resultsLeftCount() {
+			return this.musareSearch.count - this.musareSearch.results.length;
+		},
+		nextPageResultsCount() {
+			return Math.min(this.musareSearch.pageSize, this.resultsLeftCount);
+		},
+		includedSongs() {
+			return this.includedPlaylists
+				.map(playlist => playlist.songs)
+				.flat()
+				.filter((song, index, self) => self.indexOf(song) === index)
+				.filter(song => this.excludedSongIds.indexOf(song._id) === -1);
+		},
+		excludedSongs() {
+			return this.excludedPlaylists
+				.map(playlist => playlist.songs)
+				.flat()
+				.filter((song, index, self) => self.indexOf(song) === index);
+		},
+		excludedSongIds() {
+			return this.excludedSongs.map(excludedSong => excludedSong._id);
+		},
+		...mapState({
+			loggedIn: state => state.user.auth.loggedIn,
+			userId: state => state.user.auth.userId,
+			role: state => state.user.auth.role
+		}),
+		...mapState("modals/manageStation", {
+			station: state => state.station,
+			originalStation: state => state.originalStation,
+			excludedPlaylists: state => state.excludedPlaylists,
+			includedPlaylists: state => state.includedPlaylists
+		}),
+		...mapGetters({
+			socket: "websockets/getSocket"
+		})
+	},
+	mounted() {
+		if (
+			this.isOwnerOrAdmin() &&
+			!(this.station.type === "community" && this.station.partyMode)
+		)
+			this.showTab("included");
+	},
+	methods: {
+		showTab(tab) {
+			this.tab = tab;
+		},
+		isOwner() {
+			return this.loggedIn && this.userId === this.station.owner;
+		},
+		isAdmin() {
+			return this.loggedIn && this.role === "admin";
+		},
+		isOwnerOrAdmin() {
+			return this.isOwner() || this.isAdmin();
+		},
+		addSongToQueue(youtubeId, index) {
+			if (this.station.type === "community") {
+				this.socket.dispatch(
+					"stations.addToQueue",
+					this.station._id,
+					youtubeId,
+					res => {
+						if (res.status !== "success")
+							new Toast(`Error: ${res.message}`);
+						else {
+							if (index)
+								this.search.songs.results[
+									index
+								].isAddedToQueue = true;
+
+							new Toast(res.message);
+						}
+					}
+				);
+			} else {
+				this.socket.dispatch("songs.request", youtubeId, res => {
+					if (res.status !== "success")
+						new Toast(`Error: ${res.message}`);
+					else {
+						this.search.songs.results[index].isAddedToQueue = true;
+
+						new Toast(res.message);
+					}
+				});
+			}
+		},
+		searchForMusareSongs(page) {
+			if (
+				this.musareSearch.page >= page ||
+				this.musareSearch.searchedQuery !== this.musareSearch.query
+			) {
+				this.musareSearch.results = [];
+				this.musareSearch.page = 0;
+				this.musareSearch.count = 0;
+				this.musareSearch.resultsLeft = 0;
+				this.musareSearch.pageSize = 0;
+			}
+
+			this.musareSearch.searchedQuery = this.musareSearch.query;
+			this.socket.dispatch(
+				"songs.searchOfficial",
+				this.musareSearch.query,
+				page,
+				res => {
+					const { data } = res;
+					const { count, pageSize, songs } = data;
+					if (res.status === "success") {
+						this.musareSearch.results = [
+							...this.musareSearch.results,
+							...songs
+						];
+						this.musareSearch.page = page;
+						this.musareSearch.count = count;
+						this.musareSearch.resultsLeft =
+							count - this.musareSearch.results.length;
+						this.musareSearch.pageSize = pageSize;
+					} else if (res.status === "error") {
+						this.musareSearch.results = [];
+						this.musareSearch.page = 0;
+						this.musareSearch.count = 0;
+						this.musareSearch.resultsLeft = 0;
+						this.musareSearch.pageSize = 0;
+						new Toast(res.message);
+					}
+				}
+			);
+		}
+	}
+};
+</script>
+
+<style lang="scss" scoped>
+.songs {
+	.tabs-container {
+		.tab-selection {
+			display: flex;
+			overflow-x: auto;
+			.button {
+				border-radius: 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 {
+			padding: 15px 0;
+			border-radius: 0;
+			.playlist-item:not(:last-of-type),
+			.item.item-draggable:not(:last-of-type) {
+				margin-bottom: 10px;
+			}
+			.load-more-button {
+				width: 100%;
+				margin-top: 10px;
+			}
+		}
+	}
+
+	.musare-songs,
+	.universal-item:not(:last-of-type) {
+		margin-bottom: 10px;
+	}
+	.load-more-button {
+		width: 100%;
+		margin-top: 10px;
+	}
+}
+</style>

+ 127 - 60
frontend/src/components/modals/ManageStation/index.vue

@@ -40,25 +40,17 @@
 							</button>
 							<button
 								v-if="
-									loggedIn &&
+									(loggedIn &&
 										station.type === 'community' &&
 										station.partyMode &&
-										((station.locked && isOwnerOrAdmin()) ||
-											!station.locked)
+										!station.locked) ||
+										isOwnerOrAdmin()
 								"
 								class="button is-default"
-								:class="{ selected: tab === 'search' }"
-								@click="showTab('search')"
-							>
-								Search
-							</button>
-							<button
-								v-if="isOwnerOrAdmin()"
-								class="button is-default"
-								:class="{ selected: tab === 'blacklist' }"
-								@click="showTab('blacklist')"
+								:class="{ selected: tab === 'songs' }"
+								@click="showTab('songs')"
 							>
-								Blacklist
+								Songs
 							</button>
 						</div>
 						<settings
@@ -78,58 +70,86 @@
 							class="tab"
 							v-show="tab === 'playlists'"
 						/>
-						<search
+						<songs
 							v-if="
-								loggedIn &&
+								(loggedIn &&
 									station.type === 'community' &&
 									station.partyMode &&
-									((station.locked && isOwnerOrAdmin()) ||
-										!station.locked)
+									!station.locked) ||
+									isOwnerOrAdmin()
 							"
 							class="tab"
-							v-show="tab === 'search'"
-						/>
-						<blacklist
-							v-if="isOwnerOrAdmin()"
-							class="tab"
-							v-show="tab === 'blacklist'"
+							v-show="tab === 'songs'"
 						/>
 					</div>
 				</div>
 				<div class="right-section">
 					<div class="section">
+						<div id="about-station-container">
+							<div id="station-info">
+								<div id="station-name">
+									<h1>{{ station.displayName }}</h1>
+									<i
+										class="material-icons stationMode"
+										:content="
+											station.partyMode
+												? 'Station in Party mode'
+												: 'Station in Playlist mode'
+										"
+										v-tippy
+										>{{
+											station.partyMode
+												? "emoji_people"
+												: "playlist_play"
+										}}</i
+									>
+								</div>
+								<p>{{ station.description }}</p>
+							</div>
+
+							<div id="admin-buttons" v-if="isOwnerOrAdmin()">
+								<!-- (Admin) Pause/Resume Button -->
+								<button
+									class="button is-danger"
+									v-if="stationPaused"
+									@click="resumeStation()"
+								>
+									<i class="material-icons icon-with-button"
+										>play_arrow</i
+									>
+									<span class="optional-desktop-only-text">
+										Resume Station
+									</span>
+								</button>
+								<button
+									class="button is-danger"
+									@click="pauseStation()"
+									v-else
+								>
+									<i class="material-icons icon-with-button"
+										>pause</i
+									>
+									<span class="optional-desktop-only-text">
+										Pause Station
+									</span>
+								</button>
+
+								<!-- (Admin) Skip Button -->
+								<button
+									class="button is-danger"
+									@click="skipStation()"
+								>
+									<i class="material-icons icon-with-button"
+										>skip_next</i
+									>
+									<span class="optional-desktop-only-text">
+										Force Skip
+									</span>
+								</button>
+							</div>
+						</div>
 						<div class="queue-title">
 							<h4 class="section-title">Queue</h4>
-							<i
-								v-if="isOwnerOrAdmin() && stationPaused"
-								@click="resumeStation()"
-								class="material-icons resume-station"
-								content="Resume Station"
-								v-tippy
-							>
-								play_arrow
-							</i>
-							<i
-								v-if="isOwnerOrAdmin() && !stationPaused"
-								@click="pauseStation()"
-								class="material-icons pause-station"
-								content="Pause Station"
-								v-tippy
-							>
-								pause
-							</i>
-							<confirm
-								v-if="isOwnerOrAdmin()"
-								@confirm="skipStation()"
-							>
-								<i
-									class="material-icons skip-station"
-									content="Force Skip Station"
-									v-tippy
-								>
-									skip_next
-								</i>
-							</confirm>
 						</div>
 						<hr class="section-horizontal-rule" />
 						<song-item
@@ -202,8 +222,7 @@ import Modal from "../../Modal.vue";
 
 import Settings from "./Tabs/Settings.vue";
 import Playlists from "./Tabs/Playlists.vue";
-import Search from "./Tabs/Search.vue";
-import Blacklist from "./Tabs/Blacklist.vue";
+import Songs from "./Tabs/Songs.vue";
 
 export default {
 	components: {
@@ -213,8 +232,7 @@ export default {
 		SongItem,
 		Settings,
 		Playlists,
-		Search,
-		Blacklist
+		Songs
 	},
 	props: {
 		stationId: { type: String, default: "" },
@@ -247,7 +265,7 @@ export default {
 				this.editStation(station);
 
 				if (!this.isOwnerOrAdmin() && this.station.partyMode)
-					this.showTab("search");
+					this.showTab("songs");
 
 				const currentSong = res.data.station.currentSong
 					? res.data.station.currentSong
@@ -577,7 +595,6 @@ export default {
 .manage-station-modal.modal .modal-card-body .custom-modal-body {
 	display: flex;
 	flex-wrap: wrap;
-	height: 100%;
 
 	.section {
 		display: flex;
@@ -633,6 +650,56 @@ export default {
 		overflow-y: auto;
 		flex-grow: 1;
 		.section {
+			#about-station-container {
+				padding: 20px;
+				display: flex;
+				flex-direction: column;
+				flex-grow: unset;
+				border-radius: 5px;
+				margin: 0 0 20px 0;
+				background-color: var(--white);
+				border: 1px solid var(--light-grey-3);
+
+				#station-info {
+					#station-name {
+						flex-direction: row !important;
+						display: flex;
+						flex-direction: row;
+						max-width: 100%;
+
+						h1 {
+							margin: 0;
+							font-size: 36px;
+							line-height: 0.8;
+						}
+
+						i {
+							margin-left: 10px;
+							font-size: 30px;
+							color: var(--yellow);
+							&.stationMode {
+								padding-left: 10px;
+								margin-left: auto;
+								color: var(--primary-color);
+							}
+						}
+					}
+
+					p {
+						max-width: 700px;
+						margin-bottom: 10px;
+					}
+				}
+
+				#admin-buttons {
+					display: flex;
+
+					.button {
+						margin: 3px;
+					}
+				}
+			}
+
 			.queue-title {
 				display: flex;
 				line-height: 30px;