浏览代码

Worked more on experimental ManageStation design (WIP)

Kristian Vos 4 年之前
父节点
当前提交
7329a51d45

+ 1 - 0
frontend/src/components/PlaylistItem.vue

@@ -1,5 +1,6 @@
 <template>
 <template>
 	<div class="playlist-item universal-item">
 	<div class="playlist-item universal-item">
+		<slot name="left-icon" />
 		<div class="left-part">
 		<div class="left-part">
 			<p class="item-title">
 			<p class="item-title">
 				{{ playlist.displayName }}
 				{{ playlist.displayName }}

+ 0 - 161
frontend/src/components/modals/ManageStation/Tabs/Blacklist.vue

@@ -1,161 +0,0 @@
-<template>
-	<div class="station-blacklist">
-		<p class="has-text-centered">
-			Blacklist a playlist to prevent all of its songs playing in this
-			station.
-		</p>
-		<div class="tabs-container">
-			<!-- <div class="tab-selection">
-				<button
-					class="button is-default"
-					:class="{ selected: tab === 'playlists' }"
-					@click="showTab('playlists')"
-				>
-					Playlists
-				</button>
-				<button
-					class="button is-default"
-					:class="{ selected: tab === 'songs' }"
-					@click="showTab('songs')"
-				>
-					Songs
-				</button>
-			</div> -->
-			<div class="tab" v-show="tab === 'playlists'">
-				<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="deselectPlaylist(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 blacklisted.
-				</p>
-			</div>
-			<!-- <div class="tab" v-show="tab === 'songs'">
-				Blacklisting songs has yet to be added.
-			</div> -->
-		</div>
-	</div>
-</template>
-<script>
-import { mapActions, mapState, mapGetters } from "vuex";
-
-import Toast from "toasters";
-import PlaylistItem from "@/components/PlaylistItem.vue";
-import Confirm from "@/components/Confirm.vue";
-
-export default {
-	components: {
-		PlaylistItem,
-		Confirm
-	},
-	data() {
-		return {
-			tab: "playlists"
-		};
-	},
-	computed: {
-		...mapState({
-			userId: state => state.user.auth.userId
-		}),
-		...mapState("modals/manageStation", {
-			station: state => state.station,
-			originalStation: state => state.originalStation,
-			excludedPlaylists: state => state.excludedPlaylists
-		}),
-		...mapGetters({
-			socket: "websockets/getSocket"
-		})
-	},
-	methods: {
-		showTab(tab) {
-			this.tab = tab;
-		},
-		showPlaylist(playlistId) {
-			this.editPlaylist(playlistId);
-			this.openModal("editPlaylist");
-		},
-		deselectPlaylist(id) {
-			this.socket.dispatch(
-				"stations.removeExcludedPlaylist",
-				this.station._id,
-				id,
-				res => {
-					new Toast(res.message);
-				}
-			);
-		},
-		...mapActions("modalVisibility", ["openModal"]),
-		...mapActions("user/playlists", ["editPlaylist"])
-	}
-};
-</script>
-
-<style lang="scss" scoped>
-.station-blacklist {
-	.tabs-container {
-		margin-top: 10px;
-		.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) {
-				margin-bottom: 10px;
-			}
-		}
-	}
-}
-</style>

+ 208 - 39
frontend/src/components/modals/ManageStation/Tabs/Playlists.vue

@@ -20,7 +20,7 @@
 				<button
 				<button
 					class="button is-default"
 					class="button is-default"
 					:class="{ selected: tab === 'party' }"
 					:class="{ selected: tab === 'party' }"
-					v-if="station.type === 'community' && station.partyMode"
+					v-if="isPartyMode()"
 					@click="showTab('party')"
 					@click="showTab('party')"
 				>
 				>
 					Party
 					Party
@@ -28,7 +28,7 @@
 				<button
 				<button
 					class="button is-default"
 					class="button is-default"
 					:class="{ selected: tab === 'included' }"
 					:class="{ selected: tab === 'included' }"
-					v-if="!(station.type === 'community' && station.partyMode)"
+					v-if="isPlaylistMode()"
 					@click="showTab('included')"
 					@click="showTab('included')"
 				>
 				>
 					Included
 					Included
@@ -68,6 +68,54 @@
 						:playlist="playlist"
 						:playlist="playlist"
 						:show-owner="true"
 						:show-owner="true"
 					>
 					>
+						<i
+							class="material-icons"
+							slot="left-icon"
+							v-if="
+								isAllowedToParty() && isSelected(playlist._id)
+							"
+							content="This playlist is currently selected"
+							v-tippy
+						>
+							radio
+						</i>
+						<i
+							class="material-icons"
+							slot="left-icon"
+							v-else-if="
+								isOwnerOrAdmin() &&
+									isPlaylistMode() &&
+									isIncluded(playlist._id)
+							"
+							content="This playlist is currently included"
+							v-tippy
+						>
+							play_arrow
+						</i>
+						<i
+							class="material-icons excluded-icon"
+							slot="left-icon"
+							v-else-if="
+								isOwnerOrAdmin() && isExcluded(playlist._id)
+							"
+							content="This playlist is currently excluded"
+							v-tippy
+						>
+							block
+						</i>
+						<i
+							class="material-icons"
+							slot="left-icon"
+							v-else
+							:content="
+								isPartyMode()
+									? 'This playlist is currently not selected or excluded'
+									: 'This playlist is currently not included or excluded'
+							"
+							v-tippy
+						>
+							play_disabled
+						</i>
 						<div class="icons-group" slot="actions">
 						<div class="icons-group" slot="actions">
 							<i
 							<i
 								v-if="isExcluded(playlist._id)"
 								v-if="isExcluded(playlist._id)"
@@ -77,11 +125,7 @@
 								>play_disabled</i
 								>play_disabled</i
 							>
 							>
 							<confirm
 							<confirm
-								v-if="
-									station.type === 'community' &&
-										station.partyMode &&
-										isSelected(playlist._id)
-								"
+								v-if="isPartyMode() && isSelected(playlist._id)"
 								@confirm="deselectPartyPlaylist(playlist._id)"
 								@confirm="deselectPartyPlaylist(playlist._id)"
 							>
 							>
 								<i
 								<i
@@ -95,10 +139,7 @@
 							<confirm
 							<confirm
 								v-if="
 								v-if="
 									isOwnerOrAdmin() &&
 									isOwnerOrAdmin() &&
-										!(
-											station.type === 'community' &&
-											station.partyMode
-										) &&
+										isPlaylistMode() &&
 										isIncluded(playlist._id)
 										isIncluded(playlist._id)
 								"
 								"
 								@confirm="removeIncludedPlaylist(playlist._id)"
 								@confirm="removeIncludedPlaylist(playlist._id)"
@@ -113,8 +154,7 @@
 							</confirm>
 							</confirm>
 							<i
 							<i
 								v-if="
 								v-if="
-									station.type === 'community' &&
-										station.partyMode &&
+									isPartyMode() &&
 										!isSelected(playlist._id) &&
 										!isSelected(playlist._id) &&
 										!isExcluded(playlist._id)
 										!isExcluded(playlist._id)
 								"
 								"
@@ -127,10 +167,7 @@
 							<i
 							<i
 								v-if="
 								v-if="
 									isOwnerOrAdmin() &&
 									isOwnerOrAdmin() &&
-										!(
-											station.type === 'community' &&
-											station.partyMode
-										) &&
+										isPlaylistMode() &&
 										!isIncluded(playlist._id) &&
 										!isIncluded(playlist._id) &&
 										!isExcluded(playlist._id)
 										!isExcluded(playlist._id)
 								"
 								"
@@ -154,6 +191,20 @@
 									>block</i
 									>block</i
 								>
 								>
 							</confirm>
 							</confirm>
+							<confirm
+								v-if="
+									isOwnerOrAdmin() && isExcluded(playlist._id)
+								"
+								@confirm="removeExcludedPlaylist(playlist._id)"
+							>
+								<i
+									class="material-icons stop-icon"
+									content="Stop blacklisting songs from this playlist"
+									v-tippy
+								>
+									stop
+								</i>
+							</confirm>
 							<i
 							<i
 								v-if="playlist.createdBy === myUserId"
 								v-if="playlist.createdBy === myUserId"
 								@click="showPlaylist(playlist._id)"
 								@click="showPlaylist(playlist._id)"
@@ -216,20 +267,67 @@
 							:key="playlist._id"
 							:key="playlist._id"
 							:playlist="playlist"
 							:playlist="playlist"
 						>
 						>
+							<i
+								class="material-icons"
+								slot="left-icon"
+								v-if="
+									isAllowedToParty() &&
+										isSelected(playlist._id)
+								"
+								content="This playlist is currently selected"
+								v-tippy
+							>
+								radio
+							</i>
+							<i
+								class="material-icons"
+								slot="left-icon"
+								v-else-if="
+									isOwnerOrAdmin() &&
+										isPlaylistMode() &&
+										isIncluded(playlist._id)
+								"
+								content="This playlist is currently included"
+								v-tippy
+							>
+								play_arrow
+							</i>
+							<i
+								class="material-icons excluded-icon"
+								slot="left-icon"
+								v-else-if="
+									isOwnerOrAdmin() && isExcluded(playlist._id)
+								"
+								content="This playlist is currently excluded"
+								v-tippy
+							>
+								block
+							</i>
+							<i
+								class="material-icons"
+								slot="left-icon"
+								v-else
+								:content="
+									isPartyMode()
+										? 'This playlist is currently not selected or excluded'
+										: 'This playlist is currently not included or excluded'
+								"
+								v-tippy
+							>
+								play_disabled
+							</i>
 							<div slot="actions">
 							<div slot="actions">
-								<i
+								<!-- <i
 									v-if="isExcluded(playlist._id)"
 									v-if="isExcluded(playlist._id)"
 									class="material-icons stop-icon"
 									class="material-icons stop-icon"
 									content="This playlist is blacklisted in this station"
 									content="This playlist is blacklisted in this station"
 									v-tippy
 									v-tippy
 									>play_disabled</i
 									>play_disabled</i
-								>
+								> -->
 								<i
 								<i
 									v-if="
 									v-if="
-										station.type === 'community' &&
-											station.partyMode &&
-											!isSelected(playlist._id) &&
-											!isExcluded(playlist._id)
+										isPartyMode() &&
+											!isSelected(playlist._id)
 									"
 									"
 									@click="selectPartyPlaylist(playlist)"
 									@click="selectPartyPlaylist(playlist)"
 									class="material-icons play-icon"
 									class="material-icons play-icon"
@@ -239,11 +337,9 @@
 								>
 								>
 								<i
 								<i
 									v-if="
 									v-if="
-										station.type === 'community' &&
+										isPlaylistMode() &&
 											isOwnerOrAdmin() &&
 											isOwnerOrAdmin() &&
-											!station.partyMode &&
-											!isSelected(playlist._id) &&
-											!isExcluded(playlist._id)
+											!isSelected(playlist._id)
 									"
 									"
 									@click="includePlaylist(playlist)"
 									@click="includePlaylist(playlist)"
 									class="material-icons play-icon"
 									class="material-icons play-icon"
@@ -253,8 +349,7 @@
 								>
 								>
 								<confirm
 								<confirm
 									v-if="
 									v-if="
-										station.type === 'community' &&
-											station.partyMode &&
+										isPartyMode() &&
 											isSelected(playlist._id)
 											isSelected(playlist._id)
 									"
 									"
 									@confirm="
 									@confirm="
@@ -270,9 +365,8 @@
 								</confirm>
 								</confirm>
 								<confirm
 								<confirm
 									v-if="
 									v-if="
-										station.type === 'community' &&
+										isPlaylistMode() &&
 											isOwnerOrAdmin() &&
 											isOwnerOrAdmin() &&
-											!station.partyMode &&
 											isIncluded(playlist._id)
 											isIncluded(playlist._id)
 									"
 									"
 									@confirm="
 									@confirm="
@@ -300,6 +394,23 @@
 										>block</i
 										>block</i
 									>
 									>
 								</confirm>
 								</confirm>
+								<confirm
+									v-if="
+										isOwnerOrAdmin() &&
+											isExcluded(playlist._id)
+									"
+									@confirm="
+										removeExcludedPlaylist(playlist._id)
+									"
+								>
+									<i
+										class="material-icons stop-icon"
+										content="Stop blacklisting songs from this playlist"
+										v-tippy
+									>
+										stop
+									</i>
+								</confirm>
 								<i
 								<i
 									@click="showPlaylist(playlist._id)"
 									@click="showPlaylist(playlist._id)"
 									class="material-icons edit-icon"
 									class="material-icons edit-icon"
@@ -315,11 +426,7 @@
 					You don't have any playlists!
 					You don't have any playlists!
 				</p>
 				</p>
 			</div>
 			</div>
-			<div
-				class="tab"
-				v-show="tab === 'party'"
-				v-if="station.type === 'community' && station.partyMode"
-			>
+			<div class="tab" v-show="tab === 'party'" v-if="isPartyMode()">
 				<div v-if="partyPlaylists.length > 0">
 				<div v-if="partyPlaylists.length > 0">
 					<playlist-item
 					<playlist-item
 						v-for="playlist in partyPlaylists"
 						v-for="playlist in partyPlaylists"
@@ -327,6 +434,14 @@
 						:playlist="playlist"
 						:playlist="playlist"
 						:show-owner="true"
 						:show-owner="true"
 					>
 					>
+						<i
+							class="material-icons"
+							slot="left-icon"
+							content="This playlist is currently selected"
+							v-tippy
+						>
+							radio
+						</i>
 						<div class="icons-group" slot="actions">
 						<div class="icons-group" slot="actions">
 							<confirm
 							<confirm
 								v-if="isOwnerOrAdmin()"
 								v-if="isOwnerOrAdmin()"
@@ -381,7 +496,7 @@
 			<div
 			<div
 				class="tab"
 				class="tab"
 				v-show="tab === 'included'"
 				v-show="tab === 'included'"
-				v-if="!(station.type === 'community' && station.partyMode)"
+				v-if="isPlaylistMode()"
 			>
 			>
 				<div v-if="includedPlaylists.length > 0">
 				<div v-if="includedPlaylists.length > 0">
 					<playlist-item
 					<playlist-item
@@ -390,6 +505,14 @@
 						:playlist="playlist"
 						:playlist="playlist"
 						:show-owner="true"
 						:show-owner="true"
 					>
 					>
+						<i
+							class="material-icons"
+							slot="left-icon"
+							content="This playlist is currently included"
+							v-tippy
+						>
+							play_arrow
+						</i>
 						<div class="icons-group" slot="actions">
 						<div class="icons-group" slot="actions">
 							<confirm
 							<confirm
 								v-if="isOwnerOrAdmin()"
 								v-if="isOwnerOrAdmin()"
@@ -441,13 +564,25 @@
 					No playlists currently included.
 					No playlists currently included.
 				</p>
 				</p>
 			</div>
 			</div>
-			<div class="tab" v-show="tab === 'excluded'">
+			<div
+				class="tab"
+				v-show="tab === 'excluded'"
+				v-if="isOwnerOrAdmin()"
+			>
 				<div v-if="excludedPlaylists.length > 0">
 				<div v-if="excludedPlaylists.length > 0">
 					<playlist-item
 					<playlist-item
 						:playlist="playlist"
 						:playlist="playlist"
 						v-for="playlist in excludedPlaylists"
 						v-for="playlist in excludedPlaylists"
 						:key="`key-${playlist._id}`"
 						:key="`key-${playlist._id}`"
 					>
 					>
+						<i
+							class="material-icons excluded-icon"
+							slot="left-icon"
+							content="This playlist is currently excluded"
+							v-tippy
+						>
+							block
+						</i>
 						<div class="icons-group" slot="actions">
 						<div class="icons-group" slot="actions">
 							<confirm
 							<confirm
 								@confirm="removeExcludedPlaylist(playlist._id)"
 								@confirm="removeExcludedPlaylist(playlist._id)"
@@ -574,7 +709,11 @@ export default {
 			this.tab = tab;
 			this.tab = tab;
 		},
 		},
 		isOwner() {
 		isOwner() {
-			return this.loggedIn && this.userId === this.station.owner;
+			return (
+				this.loggedIn &&
+				this.station &&
+				this.userId === this.station.owner
+			);
 		},
 		},
 		isAdmin() {
 		isAdmin() {
 			return this.loggedIn && this.role === "admin";
 			return this.loggedIn && this.role === "admin";
@@ -582,6 +721,24 @@ export default {
 		isOwnerOrAdmin() {
 		isOwnerOrAdmin() {
 			return this.isOwner() || this.isAdmin();
 			return this.isOwner() || this.isAdmin();
 		},
 		},
+		isPartyMode() {
+			return (
+				this.station &&
+				this.station.type === "community" &&
+				this.station.partyMode
+			);
+		},
+		isAllowedToParty() {
+			return (
+				this.station &&
+				this.isPartyMode() &&
+				(!this.station.locked || this.isOwnerOrAdmin()) &&
+				this.loggedIn
+			);
+		},
+		isPlaylistMode() {
+			return this.station && !this.isPartyMode();
+		},
 		showPlaylist(playlistId) {
 		showPlaylist(playlistId) {
 			this.editPlaylist(playlistId);
 			this.editPlaylist(playlistId);
 			this.openModal("editPlaylist");
 			this.openModal("editPlaylist");
@@ -773,6 +930,18 @@ export default {
 </script>
 </script>
 
 
 <style lang="scss" scoped>
 <style lang="scss" scoped>
+.excluded-icon {
+	color: var(--red);
+}
+
+.included-icon {
+	color: var(--green);
+}
+
+.selected-icon {
+	color: var(--purple);
+}
+
 .station-playlists {
 .station-playlists {
 	.tabs-container {
 	.tabs-container {
 		.tab-selection {
 		.tab-selection {

+ 46 - 15
frontend/src/components/modals/ManageStation/Tabs/Songs.vue

@@ -16,14 +16,14 @@
 				</button>
 				</button>
 				<button
 				<button
 					class="button is-default"
 					class="button is-default"
-					:class="{ selected: tab === 'included' }"
+					:class="{ selected: tab === 'stationPlaylist' }"
 					v-if="
 					v-if="
 						isOwnerOrAdmin() &&
 						isOwnerOrAdmin() &&
 							!(station.type === 'community' && station.partyMode)
 							!(station.type === 'community' && station.partyMode)
 					"
 					"
-					@click="showTab('included')"
+					@click="showTab('stationPlaylist')"
 				>
 				>
-					Included
+					Station playlist
 				</button>
 				</button>
 				<button
 				<button
 					class="button is-default"
 					class="button is-default"
@@ -169,15 +169,19 @@
 			</div>
 			</div>
 			<div
 			<div
 				class="tab"
 				class="tab"
-				v-show="tab === 'included'"
+				v-show="tab === 'stationPlaylist'"
 				v-if="
 				v-if="
 					isOwnerOrAdmin() &&
 					isOwnerOrAdmin() &&
 						!(station.type === 'community' && station.partyMode)
 						!(station.type === 'community' && station.partyMode)
 				"
 				"
 			>
 			>
-				<div v-if="includedSongs.length > 0">
+				<div v-if="stationPlaylist.songs.length > 0">
+					<div id="playlist-info-section" class="section">
+						<h5>Song Count: {{ stationPlaylist.songs.length }}</h5>
+						<h5>Duration: {{ totalLength(stationPlaylist) }}</h5>
+					</div>
 					<song-item
 					<song-item
-						v-for="song in includedSongs"
+						v-for="song in stationPlaylist.songs"
 						:key="song._id"
 						:key="song._id"
 						:song="song"
 						:song="song"
 					>
 					>
@@ -194,6 +198,9 @@
 				v-if="isOwnerOrAdmin()"
 				v-if="isOwnerOrAdmin()"
 			>
 			>
 				<div v-if="excludedSongs.length > 0">
 				<div v-if="excludedSongs.length > 0">
+					<div id="playlist-info-section" class="section">
+						<h5>Song Count: {{ excludedSongs.length }}</h5>
+					</div>
 					<song-item
 					<song-item
 						v-for="song in excludedSongs"
 						v-for="song in excludedSongs"
 						:key="song._id"
 						:key="song._id"
@@ -219,6 +226,8 @@ import SearchYoutube from "@/mixins/SearchYoutube.vue";
 import SongItem from "@/components/SongItem.vue";
 import SongItem from "@/components/SongItem.vue";
 import SearchQueryItem from "../../../SearchQueryItem.vue";
 import SearchQueryItem from "../../../SearchQueryItem.vue";
 
 
+import utils from "../../../../../js/utils";
+
 export default {
 export default {
 	components: {
 	components: {
 		SongItem,
 		SongItem,
@@ -227,6 +236,7 @@ export default {
 	mixins: [SearchYoutube],
 	mixins: [SearchYoutube],
 	data() {
 	data() {
 		return {
 		return {
+			utils,
 			tab: "search",
 			tab: "search",
 			musareSearch: {
 			musareSearch: {
 				query: "",
 				query: "",
@@ -245,13 +255,6 @@ export default {
 		nextPageResultsCount() {
 		nextPageResultsCount() {
 			return Math.min(this.musareSearch.pageSize, this.resultsLeftCount);
 			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() {
 		excludedSongs() {
 			return this.excludedPlaylists
 			return this.excludedPlaylists
 				.map(playlist => playlist.songs)
 				.map(playlist => playlist.songs)
@@ -270,7 +273,7 @@ export default {
 			station: state => state.station,
 			station: state => state.station,
 			originalStation: state => state.originalStation,
 			originalStation: state => state.originalStation,
 			excludedPlaylists: state => state.excludedPlaylists,
 			excludedPlaylists: state => state.excludedPlaylists,
-			includedPlaylists: state => state.includedPlaylists
+			stationPlaylist: state => state.stationPlaylist
 		}),
 		}),
 		...mapGetters({
 		...mapGetters({
 			socket: "websockets/getSocket"
 			socket: "websockets/getSocket"
@@ -281,7 +284,7 @@ export default {
 			this.isOwnerOrAdmin() &&
 			this.isOwnerOrAdmin() &&
 			!(this.station.type === "community" && this.station.partyMode)
 			!(this.station.type === "community" && this.station.partyMode)
 		)
 		)
-			this.showTab("included");
+			this.showTab("stationPlaylist");
 	},
 	},
 	methods: {
 	methods: {
 		showTab(tab) {
 		showTab(tab) {
@@ -296,6 +299,13 @@ export default {
 		isOwnerOrAdmin() {
 		isOwnerOrAdmin() {
 			return this.isOwner() || this.isAdmin();
 			return this.isOwner() || this.isAdmin();
 		},
 		},
+		totalLength(playlist) {
+			let length = 0;
+			playlist.songs.forEach(song => {
+				length += song.duration;
+			});
+			return this.utils.formatTimeLong(length);
+		},
 		addSongToQueue(youtubeId, index) {
 		addSongToQueue(youtubeId, index) {
 			if (this.station.type === "community") {
 			if (this.station.type === "community") {
 				this.socket.dispatch(
 				this.socket.dispatch(
@@ -421,5 +431,26 @@ export default {
 		width: 100%;
 		width: 100%;
 		margin-top: 10px;
 		margin-top: 10px;
 	}
 	}
+
+	#playlist-info-section {
+		border: 1px solid var(--light-grey-3);
+		border-radius: 3px;
+		padding: 15px !important;
+		margin-bottom: 16px;
+
+		h3 {
+			font-weight: 600;
+			font-size: 30px;
+		}
+
+		h5 {
+			font-size: 18px;
+		}
+
+		h3,
+		h5 {
+			margin: 0;
+		}
+	}
 }
 }
 </style>
 </style>

+ 258 - 170
frontend/src/components/modals/ManageStation/index.vue

@@ -12,83 +12,19 @@
 		<template #body>
 		<template #body>
 			<div class="custom-modal-body" v-if="station && station._id">
 			<div class="custom-modal-body" v-if="station && station._id">
 				<div class="left-section">
 				<div class="left-section">
-					<div class="section tabs-container">
-						<div class="tab-selection">
-							<button
-								v-if="isOwnerOrAdmin()"
-								class="button is-default"
-								:class="{ selected: tab === 'settings' }"
-								@click="showTab('settings')"
-							>
-								Settings
-							</button>
-							<button
-								v-if="
-									isOwnerOrAdmin() ||
-										(loggedIn &&
-											station.type === 'community' &&
-											station.partyMode &&
-											((station.locked &&
-												isOwnerOrAdmin()) ||
-												!station.locked))
-								"
-								class="button is-default"
-								:class="{ selected: tab === 'playlists' }"
-								@click="showTab('playlists')"
-							>
-								Playlists
-							</button>
-							<button
-								v-if="
-									(loggedIn &&
-										station.type === 'community' &&
-										station.partyMode &&
-										!station.locked) ||
-										isOwnerOrAdmin()
-								"
-								class="button is-default"
-								:class="{ selected: tab === 'songs' }"
-								@click="showTab('songs')"
-							>
-								Songs
-							</button>
-						</div>
-						<settings
-							v-if="isOwnerOrAdmin()"
-							class="tab"
-							v-show="tab === 'settings'"
-						/>
-						<playlists
-							v-if="
-								isOwnerOrAdmin() ||
-									(loggedIn &&
-										station.type === 'community' &&
-										station.partyMode &&
-										((station.locked && isOwnerOrAdmin()) ||
-											!station.locked))
-							"
-							class="tab"
-							v-show="tab === 'playlists'"
-						/>
-						<songs
-							v-if="
-								(loggedIn &&
-									station.type === 'community' &&
-									station.partyMode &&
-									!station.locked) ||
-									isOwnerOrAdmin()
-							"
-							class="tab"
-							v-show="tab === 'songs'"
-						/>
-					</div>
-				</div>
-				<div class="right-section">
 					<div class="section">
 					<div class="section">
 						<div id="about-station-container">
 						<div id="about-station-container">
 							<div id="station-info">
 							<div id="station-info">
 								<div id="station-name">
 								<div id="station-name">
 									<h1>{{ station.displayName }}</h1>
 									<h1>{{ station.displayName }}</h1>
+									<i
+										v-if="station.type === 'official'"
+										class="material-icons verified-station"
+										content="Verified Station"
+										v-tippy
+									>
+										check_circle
+									</i>
 									<i
 									<i
 										class="material-icons stationMode"
 										class="material-icons stationMode"
 										:content="
 										:content="
@@ -146,8 +82,76 @@
 										Force Skip
 										Force Skip
 									</span>
 									</span>
 								</button>
 								</button>
+
+								<!-- Station Settings Button -->
+								<!-- <button
+									class="button is-primary"
+									@click="openModal('manageStation')"
+								>
+									<i class="material-icons icon-with-button"
+										>settings</i
+									>
+									<span class="optional-desktop-only-text">
+										Manage Station
+									</span>
+								</button> -->
+								<router-link
+									v-if="sector !== 'station' && station.name"
+									:to="{
+										name: 'station',
+										params: { id: station.name }
+									}"
+									class="button is-primary"
+								>
+									Go To Station
+								</router-link>
 							</div>
 							</div>
 						</div>
 						</div>
+						<div class="tab-selection">
+							<button
+								v-if="isOwnerOrAdmin()"
+								class="button is-default"
+								:class="{ selected: tab === 'settings' }"
+								@click="showTab('settings')"
+							>
+								Settings
+							</button>
+							<button
+								v-if="isAllowedToParty() || isOwnerOrAdmin()"
+								class="button is-default"
+								:class="{ selected: tab === 'playlists' }"
+								@click="showTab('playlists')"
+							>
+								Playlists
+							</button>
+							<button
+								v-if="isAllowedToParty() || isOwnerOrAdmin()"
+								class="button is-default"
+								:class="{ selected: tab === 'songs' }"
+								@click="showTab('songs')"
+							>
+								Songs
+							</button>
+						</div>
+						<settings
+							v-if="isOwnerOrAdmin()"
+							class="tab"
+							v-show="tab === 'settings'"
+						/>
+						<playlists
+							v-if="isAllowedToParty() || isOwnerOrAdmin()"
+							class="tab"
+							v-show="tab === 'playlists'"
+						/>
+						<songs
+							v-if="isAllowedToParty() || isOwnerOrAdmin()"
+							class="tab"
+							v-show="tab === 'songs'"
+						/>
+					</div>
+				</div>
+				<div class="right-section">
+					<div class="section">
 						<div class="queue-title">
 						<div class="queue-title">
 							<h4 class="section-title">Queue</h4>
 							<h4 class="section-title">Queue</h4>
 						</div>
 						</div>
@@ -168,7 +172,7 @@
 			</div>
 			</div>
 		</template>
 		</template>
 		<template #footer>
 		<template #footer>
-			<router-link
+			<!-- <router-link
 				v-if="sector !== 'station' && station.name"
 				v-if="sector !== 'station' && station.name"
 				:to="{
 				:to="{
 					name: 'station',
 					name: 'station',
@@ -177,14 +181,7 @@
 				class="button is-primary"
 				class="button is-primary"
 			>
 			>
 				Go To Station
 				Go To Station
-			</router-link>
-			<a
-				class="button is-default"
-				v-if="isOwnerOrAdmin() && !station.partyMode"
-				@click="stationPlaylist()"
-			>
-				View Station Playlist
-			</a>
+			</router-link> -->
 			<button
 			<button
 				class="button is-primary tab-actionable-button"
 				class="button is-primary tab-actionable-button"
 				v-if="loggedIn && station.type === 'official'"
 				v-if="loggedIn && station.type === 'official'"
@@ -249,6 +246,7 @@ export default {
 			station: state => state.station,
 			station: state => state.station,
 			originalStation: state => state.originalStation,
 			originalStation: state => state.originalStation,
 			songsList: state => state.songsList,
 			songsList: state => state.songsList,
+			stationPlaylist: state => state.stationPlaylist,
 			includedPlaylists: state => state.includedPlaylists,
 			includedPlaylists: state => state.includedPlaylists,
 			excludedPlaylists: state => state.excludedPlaylists,
 			excludedPlaylists: state => state.excludedPlaylists,
 			stationPaused: state => state.stationPaused,
 			stationPaused: state => state.stationPaused,
@@ -293,6 +291,19 @@ export default {
 					}
 					}
 				);
 				);
 
 
+				if (this.isOwnerOrAdmin()) {
+					this.socket.dispatch(
+						"playlists.getPlaylistForStation",
+						this.station._id,
+						true,
+						res => {
+							if (res.status === "success") {
+								this.updateStationPlaylist(res.data.playlist);
+							}
+						}
+					);
+				}
+
 				this.socket.dispatch(
 				this.socket.dispatch(
 					"stations.getQueue",
 					"stations.getQueue",
 					this.stationId,
 					this.stationId,
@@ -462,6 +473,73 @@ export default {
 			},
 			},
 			{ modal: "manageStation" }
 			{ modal: "manageStation" }
 		);
 		);
+
+		if (this.isOwnerOrAdmin()) {
+			this.socket.on(
+				"event:playlist.song.added",
+				res => {
+					if (this.stationPlaylist._id === res.data.playlistId)
+						this.stationPlaylist.songs.push(res.data.song);
+				},
+				{
+					modal: "manageStation"
+				}
+			);
+
+			this.socket.on(
+				"event:playlist.song.removed",
+				res => {
+					if (this.stationPlaylist._id === res.data.playlistId) {
+						// remove song from array of playlists
+						this.stationPlaylist.songs.forEach((song, index) => {
+							if (song.youtubeId === res.data.youtubeId)
+								this.stationPlaylist.songs.splice(index, 1);
+						});
+					}
+				},
+				{
+					modal: "manageStation"
+				}
+			);
+
+			this.socket.on(
+				"event:playlist.songs.repositioned",
+				res => {
+					if (this.stationPlaylist._id === res.data.playlistId) {
+						// for each song that has a new position
+						res.data.songsBeingChanged.forEach(changedSong => {
+							this.stationPlaylist.songs.forEach(
+								(song, index) => {
+									// find song locally
+									if (
+										song.youtubeId === changedSong.youtubeId
+									) {
+										// change song position attribute
+										this.stationPlaylist.songs[
+											index
+										].position = changedSong.position;
+
+										// reposition in array if needed
+										if (index !== changedSong.position - 1)
+											this.stationPlaylist.songs.splice(
+												changedSong.position - 1,
+												0,
+												this.stationPlaylist.songs.splice(
+													index,
+													1
+												)[0]
+											);
+									}
+								}
+							);
+						});
+					}
+				},
+				{
+					modal: "manageStation"
+				}
+			);
+		}
 	},
 	},
 	beforeDestroy() {
 	beforeDestroy() {
 		this.socket.dispatch(
 		this.socket.dispatch(
@@ -476,7 +554,11 @@ export default {
 	},
 	},
 	methods: {
 	methods: {
 		isOwner() {
 		isOwner() {
-			return this.loggedIn && this.userId === this.station.owner;
+			return (
+				this.loggedIn &&
+				this.station &&
+				this.userId === this.station.owner
+			);
 		},
 		},
 		isAdmin() {
 		isAdmin() {
 			return this.loggedIn && this.role === "admin";
 			return this.loggedIn && this.role === "admin";
@@ -484,6 +566,24 @@ export default {
 		isOwnerOrAdmin() {
 		isOwnerOrAdmin() {
 			return this.isOwner() || this.isAdmin();
 			return this.isOwner() || this.isAdmin();
 		},
 		},
+		isPartyMode() {
+			return (
+				this.station &&
+				this.station.type === "community" &&
+				this.station.partyMode
+			);
+		},
+		isAllowedToParty() {
+			return (
+				this.station &&
+				this.isPartyMode() &&
+				(!this.station.locked || this.isOwnerOrAdmin()) &&
+				this.loggedIn
+			);
+		},
+		isPlaylistMode() {
+			return this.station && !this.isPartyMode();
+		},
 		removeStation() {
 		removeStation() {
 			this.socket.dispatch("stations.remove", this.station._id, res => {
 			this.socket.dispatch("stations.remove", this.station._id, res => {
 				new Toast(res.message);
 				new Toast(res.message);
@@ -534,21 +634,6 @@ export default {
 				}
 				}
 			);
 			);
 		},
 		},
-		stationPlaylist() {
-			this.socket.dispatch(
-				"playlists.getPlaylistForStation",
-				this.station._id,
-				false,
-				res => {
-					if (res.status === "success") {
-						this.editPlaylist(res.data.playlist._id);
-						this.openModal("editPlaylist");
-					} else {
-						new Toast(res.message);
-					}
-				}
-			);
-		},
 		...mapActions("modals/manageStation", [
 		...mapActions("modals/manageStation", [
 			"showTab",
 			"showTab",
 			"editStation",
 			"editStation",
@@ -556,6 +641,7 @@ export default {
 			"setExcludedPlaylists",
 			"setExcludedPlaylists",
 			"clearStation",
 			"clearStation",
 			"updateSongsList",
 			"updateSongsList",
+			"updateStationPlaylist",
 			"repositionSongInList",
 			"repositionSongInList",
 			"updateStationPaused",
 			"updateStationPaused",
 			"updateCurrentSong"
 			"updateCurrentSong"
@@ -611,38 +697,90 @@ export default {
 		overflow-y: auto;
 		overflow-y: auto;
 		flex-grow: 1;
 		flex-grow: 1;
 
 
-		.tabs-container {
-			.tab-selection {
+		#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);
+						}
+					}
+
+					.verified-station {
+						color: var(--primary-color);
+					}
+				}
+
+				p {
+					max-width: 700px;
+					margin-bottom: 10px;
+				}
+			}
+
+			#admin-buttons {
 				display: flex;
 				display: flex;
-				overflow-x: auto;
 
 
 				.button {
 				.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;
-					}
+					margin: 3px;
 				}
 				}
+			}
+		}
 
 
-				.selected {
-					background-color: var(--primary-color) !important;
-					color: var(--white) !important;
-					font-weight: 600;
+		.tab-selection {
+			display: flex;
+			overflow-x: auto;
+
+			.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;
 				}
 				}
 			}
 			}
-			.tab {
-				border: 1px solid var(--light-grey-3);
-				padding: 15px;
-				border-radius: 0 0 5px 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;
+		}
 	}
 	}
 	.right-section {
 	.right-section {
 		flex-basis: 50%;
 		flex-basis: 50%;
@@ -650,56 +788,6 @@ export default {
 		overflow-y: auto;
 		overflow-y: auto;
 		flex-grow: 1;
 		flex-grow: 1;
 		.section {
 		.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 {
 			.queue-title {
 				display: flex;
 				display: flex;
 				line-height: 30px;
 				line-height: 30px;

+ 6 - 0
frontend/src/store/modules/modals/manageStation.js

@@ -6,6 +6,7 @@ export default {
 		tab: "settings",
 		tab: "settings",
 		originalStation: {},
 		originalStation: {},
 		station: {},
 		station: {},
+		stationPlaylist: { songs: [] },
 		includedPlaylists: [],
 		includedPlaylists: [],
 		excludedPlaylists: [],
 		excludedPlaylists: [],
 		songsList: [],
 		songsList: [],
@@ -23,6 +24,8 @@ export default {
 		clearStation: ({ commit }) => commit("clearStation"),
 		clearStation: ({ commit }) => commit("clearStation"),
 		updateSongsList: ({ commit }, songsList) =>
 		updateSongsList: ({ commit }, songsList) =>
 			commit("updateSongsList", songsList),
 			commit("updateSongsList", songsList),
+		updateStationPlaylist: ({ commit }, stationPlaylist) =>
+			commit("updateStationPlaylist", stationPlaylist),
 		repositionSongInList: ({ commit }, song) =>
 		repositionSongInList: ({ commit }, song) =>
 			commit("repositionSongInList", song),
 			commit("repositionSongInList", song),
 		updateStationPaused: ({ commit }, stationPaused) =>
 		updateStationPaused: ({ commit }, stationPaused) =>
@@ -55,6 +58,9 @@ export default {
 		updateSongsList(state, songsList) {
 		updateSongsList(state, songsList) {
 			state.songsList = songsList;
 			state.songsList = songsList;
 		},
 		},
+		updateStationPlaylist(state, stationPlaylist) {
+			state.stationPlaylist = stationPlaylist;
+		},
 		repositionSongInList(state, song) {
 		repositionSongInList(state, song) {
 			if (
 			if (
 				state.songsList[song.newIndex] &&
 				state.songsList[song.newIndex] &&