Selaa lähdekoodia

Merge branch 'polishing' of github.com:Musare/MusareNode into polishing

Jonathan 4 vuotta sitten
vanhempi
commit
2123f85fcf

+ 79 - 0
backend/logic/actions/playlists.js

@@ -105,6 +105,20 @@ CacheModule.runJob("SUB", {
 	}
 });
 
+CacheModule.runJob("SUB", {
+	channel: "playlist.updatePrivacy",
+	cb: res => {
+		IOModule.runJob("SOCKETS_FROM_USER", { userId: res.userId }, this).then(response => {
+			response.sockets.forEach(socket => {
+				socket.emit("event:playlist.updatePrivacy", {
+					playlistId: res.playlistId,
+					privacy: res.privacy
+				});
+			});
+		});
+	}
+});
+
 export default {
 	/**
 	 * Gets the first song from a private playlist
@@ -1175,5 +1189,70 @@ export default {
 				});
 			}
 		);
+	}),
+
+	/**
+	 * Updates the privacy of a private playlist
+	 *
+	 * @param {object} session - the session object automatically added by socket.io
+	 * @param {string} playlistId - the id of the playlist we are updating the displayName for
+	 * @param {Function} cb - gets called with the result
+	 */
+	updatePrivacy: isLoginRequired(async function updatePrivacy(session, playlistId, privacy, cb) {
+		const playlistModel = await DBModule.runJob(
+			"GET_MODEL",
+			{
+				modelName: "playlist"
+			},
+			this
+		);
+		async.waterfall(
+			[
+				next => {
+					playlistModel.updateOne(
+						{ _id: playlistId, createdBy: session.userId },
+						{ $set: { privacy } },
+						{ runValidators: true },
+						next
+					);
+				},
+
+				(res, next) => {
+					PlaylistsModule.runJob("UPDATE_PLAYLIST", { playlistId }, this)
+						.then(playlist => {
+							next(null, playlist);
+						})
+						.catch(next);
+				}
+			],
+			async err => {
+				if (err) {
+					err = await UtilsModule.runJob("GET_ERROR", { error: err }, this);
+					this.log(
+						"ERROR",
+						"PLAYLIST_UPDATE_PRIVACY",
+						`Updating privacy to "${privacy}" for private playlist "${playlistId}" failed for user "${session.userId}". "${err}"`
+					);
+					return cb({ status: "failure", message: err });
+				}
+				this.log(
+					"SUCCESS",
+					"PLAYLIST_UPDATE_PRIVACY",
+					`Successfully updated privacy to "${privacy}" for private playlist "${playlistId}" for user "${session.userId}".`
+				);
+				CacheModule.runJob("PUB", {
+					channel: "playlist.updatePrivacy",
+					value: {
+						playlistId,
+						privacy,
+						userId: session.userId
+					}
+				});
+				return cb({
+					status: "success",
+					message: "Playlist has been successfully updated"
+				});
+			}
+		);
 	})
 };

+ 34 - 0
backend/logic/actions/queueSongs.js

@@ -122,6 +122,40 @@ export default {
 		);
 	}),
 
+	/**
+	 * Gets a song from the Musare song id
+	 *
+	 * @param {object} session - the session object automatically added by socket.io
+	 * @param {string} songId - the Musare song id
+	 * @param {Function} cb
+	 */
+	getSongFromMusareId: isAdminRequired(async function getSong(session, songId, cb) {
+		const queueSongModel = await DBModule.runJob(
+			"GET_MODEL",
+			{
+				modelName: "queueSong"
+			},
+			this
+		);
+
+		async.waterfall(
+			[
+				next => {
+					queueSongModel.findOne({ _id: songId }, next);
+				}
+			],
+			async (err, song) => {
+				if (err) {
+					err = await UtilsModule.runJob("GET_ERROR", { error: err }, this);
+					this.log("ERROR", "QUEUE_SONGS_GET_SONG_FROM_MUSARE_ID", `Failed to get song ${songId}. "${err}"`);
+					return cb({ status: "failure", message: err });
+				}
+				this.log("SUCCESS", "QUEUE_SONGS_GET_SONG_FROM_MUSARE_ID", `Got song ${songId} successfully.`);
+				return cb({ status: "success", data: { song } });
+			}
+		);
+	}),
+
 	/**
 	 * Updates a queuesong
 	 *

+ 34 - 2
backend/logic/actions/songs.js

@@ -210,10 +210,10 @@ export default {
 	}),
 
 	/**
-	 * Gets a song
+	 * Gets a song from the YouTube song id
 	 *
 	 * @param {object} session - the session object automatically added by socket.io
-	 * @param {string} songId - the song id
+	 * @param {string} songId - the YouTube song id
 	 * @param {Function} cb
 	 */
 	getSong: isAdminRequired(function getSong(session, songId, cb) {
@@ -241,6 +241,38 @@ export default {
 		);
 	}),
 
+	/**
+	 * Gets a song from the Musare song id
+	 *
+	 * @param {object} session - the session object automatically added by socket.io
+	 * @param {string} songId - the Musare song id
+	 * @param {Function} cb
+	 */
+	getSongFromMusareId: isAdminRequired(function getSong(session, songId, cb) {
+		async.waterfall(
+			[
+				next => {
+					SongsModule.runJob("GET_SONG", { id: songId }, this)
+						.then(response => {
+							next(null, response.song);
+						})
+						.catch(err => {
+							next(err);
+						});
+				}
+			],
+			async (err, song) => {
+				if (err) {
+					err = await UtilsModule.runJob("GET_ERROR", { error: err }, this);
+					this.log("ERROR", "SONGS_GET_SONG_FROM_MUSARE_ID", `Failed to get song ${songId}. "${err}"`);
+					return cb({ status: "failure", message: err });
+				}
+				this.log("SUCCESS", "SONGS_GET_SONG_FROM_MUSARE_ID", `Got song ${songId} successfully.`);
+				return cb({ status: "success", data: { song } });
+			}
+		);
+	}),
+
 	/**
 	 * Obtains basic metadata of a song in order to format an activity
 	 *

+ 2 - 1
backend/logic/db/schemas/playlist.js

@@ -3,5 +3,6 @@ export default {
 	isUserModifiable: { type: Boolean, default: true, required: true },
 	songs: { type: Array },
 	createdBy: { type: String, required: true },
-	createdAt: { type: Date, default: Date.now, required: true }
+	createdAt: { type: Date, default: Date.now, required: true },
+	privacy: { type: String, enum: ["public", "private"], default: "private" }
 };

+ 1 - 0
backend/logic/db/schemas/station.js

@@ -7,6 +7,7 @@ export default {
 	description: { type: String, minlength: 2, maxlength: 128, required: true },
 	paused: { type: Boolean, default: false, required: true },
 	currentSong: {
+		_id: { type: String },
 		songId: { type: String },
 		title: { type: String },
 		artists: [{ type: String }],

+ 2 - 0
backend/logic/stations.js

@@ -610,6 +610,7 @@ class _StationsModule extends CoreClass {
 							const { song } = response;
 							if (song) {
 								const newSong = {
+									_id: song._id,
 									songId: song.songId,
 									title: song.title,
 									artists: song.artists,
@@ -851,6 +852,7 @@ class _StationsModule extends CoreClass {
 							};
 						} else {
 							$set.currentSong = {
+								_id: song._id,
 								songId: song.songId,
 								title: song.title,
 								artists: song.artists,

+ 10 - 2
frontend/src/App.vue

@@ -84,6 +84,11 @@ export default {
 
 		if (nightmode) this.enableNightMode();
 		else this.disableNightMode();
+
+		const autoSkipDisliked =
+			true || JSON.parse(localStorage.getItem("autoSkipDisliked"));
+
+		this.changeAutoSkipDisliked(autoSkipDisliked);
 	},
 	mounted() {
 		document.onkeydown = ev => {
@@ -173,7 +178,10 @@ export default {
 				.classList.remove("night-mode");
 		},
 		...mapActions("modals", ["closeCurrentModal"]),
-		...mapActions("user/preferences", ["changeNightmode"])
+		...mapActions("user/preferences", [
+			"changeNightmode",
+			"changeAutoSkipDisliked"
+		])
 	}
 };
 </script>
@@ -504,7 +512,7 @@ button.delete:focus {
 		height: 36px;
 		border-radius: 3px 0 0 3px;
 		border-right: 0;
-		border-colour: $light-grey-2;
+		border-color: $light-grey-2;
 	}
 
 	.button {

+ 33 - 0
frontend/src/components/modals/EditPlaylist.vue

@@ -152,6 +152,19 @@
 					>
 				</p>
 			</div>
+			<div class="control is-grouped">
+				<div class="control 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="updatePrivacy()" href="#"
+						>Update Privacy</a
+					>
+				</p>
+			</div>
 		</div>
 		<div slot="footer">
 			<a class="button is-danger" @click="removePlaylist()" href="#"
@@ -387,6 +400,19 @@ export default {
 				}
 			);
 		},
+		updatePrivacy() {
+			const { privacy } = this.playlist;
+			if (privacy === "public" || privacy === "private") {
+				this.socket.emit(
+					"playlists.updatePrivacy",
+					this.playlist._id,
+					privacy,
+					res => {
+						new Toast({ content: res.message, timeout: 4000 });
+					}
+				);
+			}
+		},
 		...mapActions("modals", ["closeModal"])
 	}
 };
@@ -429,4 +455,11 @@ li a {
 h5 {
 	padding: 20px 0;
 }
+
+.control.select {
+	flex-grow: 1;
+	select {
+		width: 100%;
+	}
+}
 </style>

+ 221 - 197
frontend/src/components/modals/EditSong.vue

@@ -5,7 +5,7 @@
 				<div class="left-section">
 					<div class="top-section">
 						<div class="player-section">
-							<div id="player"></div>
+							<div id="editSongPlayer"></div>
 							<canvas
 								id="durationCanvas"
 								height="20"
@@ -74,11 +74,12 @@
 						</div>
 						<img
 							class="thumbnail-preview"
-							:src="editing.song.thumbnail"
+							:src="song.thumbnail"
 							onerror="this.src='/assets/notes-transparent.png'"
+							v-if="songDataLoaded"
 						/>
 					</div>
-					<div class="edit-section">
+					<div class="edit-section" v-if="songDataLoaded">
 						<div class="control is-grouped">
 							<div class="title-container">
 								<label class="label">Title</label>
@@ -87,7 +88,7 @@
 										class="input"
 										type="text"
 										id="title-input"
-										v-model="editing.song.title"
+										v-model="song.title"
 										@keyup.ctrl.alt.d="
 											getAlbumData('title')
 										"
@@ -106,7 +107,7 @@
 									<input
 										class="input"
 										type="text"
-										v-model.number="editing.song.duration"
+										v-model.number="song.duration"
 									/>
 									<button
 										class="button duration-fill-button"
@@ -122,9 +123,7 @@
 									<input
 										class="input"
 										type="text"
-										v-model.number="
-											editing.song.skipDuration
-										"
+										v-model.number="song.skipDuration"
 									/>
 								</p>
 							</div>
@@ -136,7 +135,7 @@
 									<input
 										class="input"
 										type="text"
-										v-model="editing.song.thumbnail"
+										v-model="song.thumbnail"
 										@keyup.ctrl.alt.d="
 											getAlbumData('albumArt')
 										"
@@ -203,8 +202,7 @@
 								<div class="list-container">
 									<div
 										class="list-item"
-										v-for="(artist, index) in editing.song
-											.artists"
+										v-for="(artist, index) in song.artists"
 										:key="index"
 									>
 										<div
@@ -276,8 +274,7 @@
 								<div class="list-container">
 									<div
 										class="list-item"
-										v-for="(genre, index) in editing.song
-											.genres"
+										v-for="(genre, index) in song.genres"
 										:key="index"
 									>
 										<div
@@ -296,41 +293,33 @@
 									<input
 										class="input"
 										type="text"
-										v-model="editing.song.songId"
+										v-model="song.songId"
 									/>
 								</p>
 							</div>
 						</div>
 					</div>
 				</div>
-				<div class="right-section">
+				<div class="right-section" v-if="songDataLoaded">
 					<div class="api-section">
-						<div
-							class="selected-discogs-info"
-							v-if="!editing.song.discogs"
-						>
+						<div class="selected-discogs-info" v-if="!song.discogs">
 							<p class="selected-discogs-info-none">None</p>
 						</div>
-						<div
-							class="selected-discogs-info"
-							v-if="editing.song.discogs"
-						>
+						<div class="selected-discogs-info" v-if="song.discogs">
 							<div class="top-container">
-								<img
-									:src="editing.song.discogs.album.albumArt"
-								/>
+								<img :src="song.discogs.album.albumArt" />
 								<div class="right-container">
 									<p class="album-title">
-										{{ editing.song.discogs.album.title }}
+										{{ song.discogs.album.title }}
 									</p>
 									<div class="bottom-row">
 										<p class="type-year">
 											<span>{{
-												editing.song.discogs.album.type
+												song.discogs.album.type
 											}}</span>
 											<span>{{
-												editing.song.discogs.album.year
+												song.discogs.album.year
 											}}</span>
 										</p>
 									</div>
@@ -340,34 +329,24 @@
 								<p class="bottom-container-field">
 									Artists:
 									<span>{{
-										editing.song.discogs.album.artists.join(
-											", "
-										)
+										song.discogs.album.artists.join(", ")
 									}}</span>
 								</p>
 								<p class="bottom-container-field">
 									Genres:
 									<span>{{
-										editing.song.discogs.album.genres.join(
-											", "
-										)
+										song.discogs.album.genres.join(", ")
 									}}</span>
 								</p>
 								<p class="bottom-container-field">
 									Data quality:
-									<span>{{
-										editing.song.discogs.dataQuality
-									}}</span>
+									<span>{{ song.discogs.dataQuality }}</span>
 								</p>
 								<p class="bottom-container-field">
 									Track:
 									<span
-										>{{
-											editing.song.discogs.track.position
-										}}.
-										{{
-											editing.song.discogs.track.title
-										}}</span
+										>{{ song.discogs.track.position }}.
+										{{ song.discogs.track.title }}</span
 									>
 								</p>
 							</div>
@@ -492,23 +471,17 @@
 				</div>
 			</div>
 			<div slot="footer" class="footer-buttons">
-				<button
-					class="button is-success"
-					@click="save(editing.song, false)"
-				>
+				<button class="button is-success" @click="save(song, false)">
 					<i class="material-icons save-changes">done</i>
 					<span>&nbsp;Save</span>
 				</button>
-				<button
-					class="button is-success"
-					@click="save(editing.song, true)"
-				>
+				<button class="button is-success" @click="save(song, true)">
 					<i class="material-icons save-changes">done</i>
 					<span>&nbsp;Save and close</span>
 				</button>
 				<button
 					class="button is-danger"
-					@click="closeModal({ sector: 'admin', modal: 'editSong' })"
+					@click="closeModal({ sector: sector, modal: 'editSong' })"
 				>
 					<span>&nbsp;Close</span>
 				</button>
@@ -545,8 +518,14 @@ import FloatingBox from "../ui/FloatingBox.vue";
 
 export default {
 	components: { Modal, FloatingBox },
+	props: {
+		songId: { type: String, default: null },
+		songType: { type: String, default: null },
+		sector: { type: String, default: "admin" }
+	},
 	data() {
 		return {
+			songDataLoaded: false,
 			focusedElementBefore: null,
 			discogsQuery: "",
 			youtubeVideoDuration: 0.0,
@@ -605,10 +584,9 @@ export default {
 		};
 	},
 	computed: {
-		...mapState("admin/songs", {
+		...mapState("editSongModal", {
 			video: state => state.video,
-			editing: state => state.editing,
-			songs: state => state.songs
+			song: state => state.song
 		}),
 		...mapState("modals", {
 			modals: state => state.modals.admin
@@ -616,10 +594,10 @@ export default {
 	},
 	watch: {
 		/* eslint-disable */
-		"editing.song.duration": function() {
+		"song.duration": function () {
 			this.drawCanvas();
 		},
-		"editing.song.skipDuration": function() {
+		"song.skipDuration": function () {
 			this.drawCanvas();
 		}
 		/* eslint-enable */
@@ -628,123 +606,169 @@ export default {
 		// if (this.modals.editSong = false) this.video.player.stopVideo();
 
 		// this.loadVideoById(
-		//   this.editing.song.songId,
-		//   this.editing.song.skipDuration
+		//   this.song.songId,
+		//   this.song.skipDuration
 		// );
 
-		this.discogsQuery = this.editing.song.title;
-
 		lofig.get("cookie.secure").then(useHTTPS => {
 			this.useHTTPS = useHTTPS;
 		});
 
 		io.getSocket(socket => {
 			this.socket = socket;
-		});
 
-		this.interval = setInterval(() => {
-			if (
-				this.editing.song.duration !== -1 &&
-				this.video.paused === false &&
-				this.playerReady &&
-				this.video.player.getCurrentTime() -
-					this.editing.song.skipDuration >
-					this.editing.song.duration
-			) {
-				this.video.paused = false;
-				this.video.player.stopVideo();
-				this.drawCanvas();
-			}
-			if (this.playerReady) {
-				this.youtubeVideoCurrentTime = this.video.player
-					.getCurrentTime()
-					.toFixed(3);
-			}
+			this.socket.emit(
+				`${this.songType}.getSongFromMusareId`,
+				this.songId,
+				res => {
+					if (res.status === "success") {
+						const { song } = res.data;
+						// this.song = { ...song };
+						// if (this.song.discogs === undefined)
+						// 	this.song.discogs = null;
+						this.editSong(song);
+
+						this.songDataLoaded = true;
+
+						// this.edit(res.data.song);
+
+						this.discogsQuery = this.song.title;
+
+						this.interval = setInterval(() => {
+							if (
+								this.song.duration !== -1 &&
+								this.video.paused === false &&
+								this.playerReady &&
+								this.video.player.getCurrentTime() -
+									this.song.skipDuration >
+									this.song.duration
+							) {
+								this.video.paused = false;
+								this.video.player.stopVideo();
+								this.drawCanvas();
+							}
+							if (this.playerReady) {
+								this.youtubeVideoCurrentTime = this.video.player
+									.getCurrentTime()
+									.toFixed(3);
+							}
 
-			if (this.video.paused === false) this.drawCanvas();
-		}, 200);
-
-		this.video.player = new window.YT.Player("player", {
-			height: 298,
-			width: 530,
-			videoId: this.editing.song.songId,
-			host: "https://www.youtube-nocookie.com",
-			playerVars: {
-				controls: 0,
-				iv_load_policy: 3,
-				rel: 0,
-				showinfo: 0,
-				autoplay: 1
-			},
-			startSeconds: this.editing.song.skipDuration,
-			events: {
-				onReady: () => {
-					let volume = parseInt(localStorage.getItem("volume"));
-					volume = typeof volume === "number" ? volume : 20;
-					console.log(`Seekto: ${this.editing.song.skipDuration}`);
-					this.video.player.seekTo(this.editing.song.skipDuration);
-					this.video.player.setVolume(volume);
-					if (volume > 0) this.video.player.unMute();
-					this.youtubeVideoDuration = this.video.player
-						.getDuration()
-						.toFixed(3);
-					this.youtubeVideoNote = "(~)";
-					this.playerReady = true;
-
-					this.drawCanvas();
-				},
-				onStateChange: event => {
-					this.drawCanvas();
-
-					if (event.data === 1) {
-						if (!this.video.autoPlayed) {
-							this.video.autoPlayed = true;
-							return this.video.player.stopVideo();
-						}
-
-						this.video.paused = false;
-						let youtubeDuration = this.video.player.getDuration();
-						this.youtubeVideoDuration = youtubeDuration.toFixed(3);
-						this.youtubeVideoNote = "";
-
-						if (this.editing.song.duration === -1)
-							this.editing.song.duration = youtubeDuration;
-
-						youtubeDuration -= this.editing.song.skipDuration;
-						if (this.editing.song.duration > youtubeDuration + 1) {
-							this.video.player.stopVideo();
-							this.video.paused = true;
-							return new Toast({
-								content:
-									"Video can't play. Specified duration is bigger than the YouTube song duration.",
-								timeout: 4000
-							});
-						}
-						if (this.editing.song.duration <= 0) {
-							this.video.player.stopVideo();
-							this.video.paused = true;
-							return new Toast({
-								content:
-									"Video can't play. Specified duration has to be more than 0 seconds.",
-								timeout: 4000
-							});
-						}
-
-						if (
-							this.video.player.getCurrentTime() <
-							this.editing.song.skipDuration
-						) {
-							return this.video.player.seekTo(
-								this.editing.song.skipDuration
-							);
-						}
-					} else if (event.data === 2) {
-						this.video.paused = true;
+							if (this.video.paused === false) this.drawCanvas();
+						}, 200);
+
+						this.video.player = new window.YT.Player(
+							"editSongPlayer",
+							{
+								height: 298,
+								width: 530,
+								videoId: this.song.songId,
+								host: "https://www.youtube-nocookie.com",
+								playerVars: {
+									controls: 0,
+									iv_load_policy: 3,
+									rel: 0,
+									showinfo: 0,
+									autoplay: 1
+								},
+								startSeconds: this.song.skipDuration,
+								events: {
+									onReady: () => {
+										let volume = parseInt(
+											localStorage.getItem("volume")
+										);
+										volume =
+											typeof volume === "number"
+												? volume
+												: 20;
+										console.log(
+											`Seekto: ${this.song.skipDuration}`
+										);
+										this.video.player.seekTo(
+											this.song.skipDuration
+										);
+										this.video.player.setVolume(volume);
+										if (volume > 0)
+											this.video.player.unMute();
+										this.youtubeVideoDuration = this.video.player
+											.getDuration()
+											.toFixed(3);
+										this.youtubeVideoNote = "(~)";
+										this.playerReady = true;
+
+										this.drawCanvas();
+									},
+									onStateChange: event => {
+										this.drawCanvas();
+
+										if (event.data === 1) {
+											if (!this.video.autoPlayed) {
+												this.video.autoPlayed = true;
+												return this.video.player.stopVideo();
+											}
+
+											this.video.paused = false;
+											let youtubeDuration = this.video.player.getDuration();
+											this.youtubeVideoDuration = youtubeDuration.toFixed(
+												3
+											);
+											this.youtubeVideoNote = "";
+
+											if (this.song.duration === -1)
+												this.song.duration = youtubeDuration;
+
+											youtubeDuration -= this.song
+												.skipDuration;
+											if (
+												this.song.duration >
+												youtubeDuration + 1
+											) {
+												this.video.player.stopVideo();
+												this.video.paused = true;
+												return new Toast({
+													content:
+														"Video can't play. Specified duration is bigger than the YouTube song duration.",
+													timeout: 4000
+												});
+											}
+											if (this.song.duration <= 0) {
+												this.video.player.stopVideo();
+												this.video.paused = true;
+												return new Toast({
+													content:
+														"Video can't play. Specified duration has to be more than 0 seconds.",
+													timeout: 4000
+												});
+											}
+
+											if (
+												this.video.player.getCurrentTime() <
+												this.song.skipDuration
+											) {
+												return this.video.player.seekTo(
+													this.song.skipDuration
+												);
+											}
+										} else if (event.data === 2) {
+											this.video.paused = true;
+										}
+
+										return false;
+									}
+								}
+							}
+						);
+					} else {
+						new Toast({
+							content: "Song with that ID not found",
+							timeout: 3000
+						});
+						this.closeModal({
+							sector: this.sector,
+							modal: "editSong"
+						});
 					}
-
-					return false;
 				}
-			}
+			);
 		});
 
 		let volume = parseFloat(localStorage.getItem("volume"));
@@ -834,7 +858,7 @@ export default {
 			ctrl: true,
 			preventDefault: true,
 			handler: () => {
-				this.save(this.editing.song, false);
+				this.save(this.song, false);
 			}
 		});
 
@@ -844,7 +868,7 @@ export default {
 			preventDefault: true,
 			handler: () => {
 				this.closeModal({
-					sector: "admin",
+					sector: this.sector,
 					modal: "editSong"
 				});
 				setTimeout(() => {
@@ -1042,25 +1066,25 @@ export default {
 			}
 
 			return this.socket.emit(
-				`${this.editing.type}.update`,
+				`${this.songType}.update`,
 				song._id,
 				song,
 				res => {
 					new Toast({ content: res.message, timeout: 4000 });
 					if (res.status === "success") {
-						this.songs.forEach(originalSong => {
-							const updatedSong = song;
-							if (originalSong._id === updatedSong._id) {
-								Object.keys(originalSong).forEach(n => {
-									updatedSong[n] = originalSong[n];
-									return originalSong[n];
-								});
-							}
-						});
+						// this.songs.forEach(originalSong => {
+						// 	const updatedSong = song;
+						// 	if (originalSong._id === updatedSong._id) {
+						// 		Object.keys(originalSong).forEach(n => {
+						// 			updatedSong[n] = originalSong[n];
+						// 			return originalSong[n];
+						// 		});
+						// 	}
+						// });
 					}
 					if (close)
 						this.closeModal({
-							sector: "admin",
+							sector: this.sector,
 							modal: "editSong"
 						});
 				}
@@ -1099,33 +1123,33 @@ export default {
 			}
 		},
 		fillDuration() {
-			this.editing.song.duration =
-				this.youtubeVideoDuration - this.editing.song.skipDuration;
+			this.song.duration =
+				this.youtubeVideoDuration - this.song.skipDuration;
 		},
 		getAlbumData(type) {
-			if (!this.editing.song.discogs) return;
+			if (!this.song.discogs) return;
 			if (type === "title")
 				this.updateSongField({
 					field: "title",
-					value: this.editing.song.discogs.track.title
+					value: this.song.discogs.track.title
 				});
 			if (type === "albumArt")
 				this.updateSongField({
 					field: "thumbnail",
-					value: this.editing.song.discogs.album.albumArt
+					value: this.song.discogs.album.albumArt
 				});
 			if (type === "genres")
 				this.updateSongField({
 					field: "genres",
 					value: JSON.parse(
-						JSON.stringify(this.editing.song.discogs.album.genres)
+						JSON.stringify(this.song.discogs.album.genres)
 					)
 				});
 			if (type === "artists")
 				this.updateSongField({
 					field: "artists",
 					value: JSON.parse(
-						JSON.stringify(this.editing.song.discogs.album.artists)
+						JSON.stringify(this.song.discogs.album.artists)
 					)
 				});
 		},
@@ -1264,9 +1288,7 @@ export default {
 				case "skipToLast10Secs":
 					if (this.video.paused) this.pauseVideo(false);
 					this.video.player.seekTo(
-						this.editing.song.duration -
-							10 +
-							this.editing.song.skipDuration
+						this.song.duration - 10 + this.song.skipDuration
 					);
 					break;
 			}
@@ -1283,13 +1305,13 @@ export default {
 					.getElementById("new-genre")
 					.value.toLowerCase()
 					.trim();
-				if (this.editing.song.genres.indexOf(genre) !== -1)
+				if (this.song.genres.indexOf(genre) !== -1)
 					return new Toast({
 						content: "Genre already exists",
 						timeout: 3000
 					});
 				if (genre) {
-					this.editing.song.genres.push(genre);
+					this.song.genres.push(genre);
 					document.getElementById("new-genre").value = "";
 					return false;
 				}
@@ -1301,13 +1323,13 @@ export default {
 			}
 			if (type === "artists") {
 				const artist = document.getElementById("new-artist").value;
-				if (this.editing.song.artists.indexOf(artist) !== -1)
+				if (this.song.artists.indexOf(artist) !== -1)
 					return new Toast({
 						content: "Artist already exists",
 						timeout: 3000
 					});
 				if (document.getElementById("new-artist").value !== "") {
-					this.editing.song.artists.push(artist);
+					this.song.artists.push(artist);
 					document.getElementById("new-artist").value = "";
 					return false;
 				}
@@ -1320,9 +1342,8 @@ export default {
 			return false;
 		},
 		removeTag(type, index) {
-			if (type === "genres") this.editing.song.genres.splice(index, 1);
-			else if (type === "artists")
-				this.editing.song.artists.splice(index, 1);
+			if (type === "genres") this.song.genres.splice(index, 1);
+			else if (type === "artists") this.song.artists.splice(index, 1);
 		},
 		drawCanvas() {
 			const canvasElement = document.getElementById("durationCanvas");
@@ -1330,13 +1351,16 @@ export default {
 
 			const videoDuration = Number(this.youtubeVideoDuration);
 
-			const skipDuration = Number(this.editing.song.skipDuration);
-			const duration = Number(this.editing.song.duration);
+			const skipDuration = Number(this.song.skipDuration);
+			const duration = Number(this.song.duration);
 			const afterDuration = videoDuration - (skipDuration + duration);
 
 			const width = 530;
 
-			const currentTime = this.video.player.getCurrentTime();
+			const currentTime =
+				this.video.player && this.video.player.getCurrentTime
+					? this.video.player.getCurrentTime()
+					: 0;
 
 			const widthSkipDuration = (skipDuration / videoDuration) * width;
 			const widthDuration = (duration / videoDuration) * width;
@@ -1370,7 +1394,7 @@ export default {
 		resetGenreHelper() {
 			this.$refs.genreHelper.resetBox();
 		},
-		...mapActions("admin/songs", [
+		...mapActions("editSongModal", [
 			"stopVideo",
 			"loadVideoById",
 			"pauseVideo",

+ 14 - 1
frontend/src/components/ui/PlaylistItem.vue

@@ -1,7 +1,15 @@
 <template>
 	<div class="playlist">
 		<div class="left-part">
-			<p class="top-text">{{ playlist.displayName }}</p>
+			<p class="top-text">
+				{{ playlist.displayName }}
+				<i
+					v-if="playlist.privacy === 'private'"
+					class="privateIcon material-icons"
+					title="This playlist is not visible to other users."
+					>lock</i
+				>
+			</p>
 			<p class="bottom-text">
 				{{ totalLength(playlist) }} •
 				{{ playlist.songs.length }}
@@ -59,6 +67,11 @@ export default {
 		font-size: 20px;
 		line-height: 23px;
 		margin-bottom: 0;
+
+		.privateIcon {
+			color: $dark-pink;
+			font-size: 18px;
+		}
 	}
 
 	.bottom-text {

+ 15 - 8
frontend/src/pages/Admin/tabs/QueueSongs.vue

@@ -109,7 +109,11 @@
 				</tbody>
 			</table>
 		</div>
-		<edit-song v-if="modals.editSong" />
+		<edit-song
+			v-if="modals.editSong"
+			:song-id="editingSongId"
+			song-type="queueSongs"
+		/>
 		<floating-box
 			id="keyboardShortcutsHelper"
 			ref="keyboardShortcutsHelper"
@@ -192,6 +196,7 @@ export default {
 	mixins: [ScrollAndFetchHandler],
 	data() {
 		return {
+			editingSongId: "",
 			searchQuery: "",
 			songs: []
 		};
@@ -245,13 +250,14 @@ export default {
 		});
 	},
 	methods: {
-		edit(song, index) {
-			const newSong = {};
-			Object.keys(song).forEach(n => {
-				newSong[n] = song[n];
-			});
+		edit(song) {
+			// const newSong = {};
+			// Object.keys(song).forEach(n => {
+			// 	newSong[n] = song[n];
+			// });
 
-			this.editSong({ index, song: newSong, type: "queueSongs" });
+			// this.editSong({ index, song: newSong, type: "queueSongs" });
+			this.editingSongId = song._id;
 			this.openModal({ sector: "admin", modal: "editSong" });
 		},
 		add(song) {
@@ -311,7 +317,8 @@ export default {
 
 			this.socket.emit("apis.joinAdminRoom", "queue", () => {});
 		},
-		...mapActions("admin/songs", ["stopVideo", "editSong"]),
+		// ...mapActions("admin/songs", ["editSong"]),
+		...mapActions("editSongModal", ["stopVideo"]),
 		...mapActions("modals", ["openModal"])
 	}
 };

+ 11 - 4
frontend/src/pages/Admin/tabs/Songs.vue

@@ -132,7 +132,11 @@
 				</tbody>
 			</table>
 		</div>
-		<edit-song v-if="modals.editSong" />
+		<edit-song
+			v-if="modals.editSong"
+			:song-id="editingSongId"
+			song-type="songs"
+		/>
 		<floating-box
 			id="keyboardShortcutsHelper"
 			ref="keyboardShortcutsHelper"
@@ -214,6 +218,7 @@ export default {
 	mixins: [ScrollAndFetchHandler],
 	data() {
 		return {
+			editingSongId: "",
 			searchQuery: "",
 			artistFilterQuery: "",
 			artistFilterSelected: [],
@@ -337,7 +342,8 @@ export default {
 	},
 	methods: {
 		edit(song) {
-			this.editSong({ song, type: "songs" });
+			// this.editSong({ song, type: "songs" });
+			this.editingSongId = song._id;
 			this.openModal({ sector: "admin", modal: "editSong" });
 		},
 		remove(id) {
@@ -403,12 +409,13 @@ export default {
 			this.socket.emit("apis.joinAdminRoom", "songs", () => {});
 		},
 		...mapActions("admin/songs", [
-			"stopVideo",
-			"editSong",
+			// "stopVideo",
+			// "editSong",
 			"addSong",
 			"removeSong",
 			"updateSong"
 		]),
+		...mapActions("editSongModal", ["stopVideo"]),
 		...mapActions("modals", ["openModal", "closeModal"])
 	}
 };

+ 5 - 1
frontend/src/pages/Admin/tabs/Users.vue

@@ -34,7 +34,11 @@
 						<td v-else>Not Linked</td>
 						<td v-if="user.hasPassword">Yes</td>
 						<td v-else>Not Linked</td>
-						<td>{{ user.username }}</td>
+						<td>
+							<a :href="'/u/' + user.username" target="_blank">{{
+								user.username
+							}}</a>
+						</td>
 						<td>{{ user.role }}</td>
 						<td>{{ user.email.address }}</td>
 						<td>{{ user.email.verified }}</td>

+ 58 - 29
frontend/src/pages/Profile.vue

@@ -76,7 +76,6 @@
 					<button
 						:class="{ active: activeTab === 'playlists' }"
 						@click="switchTab('playlists')"
-						v-if="user._id === userId"
 					>
 						Playlists
 					</button>
@@ -131,35 +130,49 @@
 					class="content playlists-tab"
 					v-if="activeTab === 'playlists'"
 				>
-					<div
-						class="item playlist"
-						v-for="playlist in playlists"
-						:key="playlist._id"
-					>
-						<playlist-item :playlist="playlist">
-							<div slot="actions">
-								<button
-									class="button is-primary"
-									@click="editPlaylistClick(playlist._id)"
-								>
-									<i class="material-icons icon-with-button"
-										>create</i
-									>Edit
-								</button>
-							</div>
-						</playlist-item>
+					<div v-if="playlists.length > 0">
+						<div
+							class="item playlist"
+							v-for="playlist in playlists"
+							:key="playlist._id"
+						>
+							<playlist-item
+								v-if="
+									playlist.privacy === 'public' ||
+										(playlist.privacy === 'private' &&
+											playlist.createdBy === userId)
+								"
+								:playlist="playlist"
+							>
+								<div v-if="user._id === userId" slot="actions">
+									<button
+										class="button is-primary"
+										@click="editPlaylistClick(playlist._id)"
+									>
+										<i
+											class="material-icons icon-with-button"
+											>create</i
+										>Edit
+									</button>
+								</div>
+							</playlist-item>
+						</div>
+						<button
+							v-if="user._id === userId"
+							class="button is-primary"
+							@click="
+								openModal({
+									sector: 'station',
+									modal: 'createPlaylist'
+								})
+							"
+						>
+							Create new playlist
+						</button>
+					</div>
+					<div v-else>
+						<h2>No playlists here.</h2>
 					</div>
-					<button
-						class="button is-primary"
-						@click="
-							openModal({
-								sector: 'station',
-								modal: 'createPlaylist'
-							})
-						"
-					>
-						Create new playlist
-					</button>
 				</div>
 			</div>
 		</div>
@@ -350,6 +363,22 @@ export default {
 									);
 								}
 							);
+
+							this.socket.on(
+								"event:playlist.updatePrivacy",
+								data => {
+									this.playlists.forEach(
+										(playlist, index) => {
+											if (
+												playlist._id === data.playlistId
+											) {
+												this.playlists[index].privacy =
+													data.privacy;
+											}
+										}
+									);
+								}
+							);
 						}
 					}
 				}

+ 29 - 3
frontend/src/pages/Settings/tabs/Preferences.vue

@@ -7,6 +7,17 @@
 				<p>Use nightmode</p>
 			</label>
 		</p>
+		<p class="control is-expanded checkbox-control">
+			<input
+				type="checkbox"
+				id="autoSkipDisliked"
+				v-model="localAutoSkipDisliked"
+			/>
+			<label for="autoSkipDisliked">
+				<span></span>
+				<p>Automatically vote to skip disliked songs</p>
+			</label>
+		</p>
 		<button class="button is-primary save-changes" @click="saveChanges()">
 			Save changes
 		</button>
@@ -19,25 +30,40 @@ import { mapState, mapActions } from "vuex";
 export default {
 	data() {
 		return {
-			localNightmode: false
+			localNightmode: false,
+			localAutoSkipDisliked: false
 		};
 	},
 	computed: mapState({
-		nightmode: state => state.user.preferences.nightmode
+		nightmode: state => state.user.preferences.nightmode,
+		autoSkipDisliked: state => state.user.preferences.autoSkipDisliked
 	}),
 	mounted() {
 		this.localNightmode = this.nightmode;
+		this.localAutoSkipDisliked = this.autoSkipDisliked;
 	},
 	methods: {
 		saveChanges() {
 			if (this.localNightmode !== this.nightmode)
 				this.changeNightmodeLocal();
+			if (this.localAutoSkipDisliked !== this.autoSkipDisliked)
+				this.changeAutoSkipDislikedLocal();
 		},
 		changeNightmodeLocal() {
 			localStorage.setItem("nightmode", this.localNightmode);
 			this.changeNightmode(this.localNightmode);
 		},
-		...mapActions("user/preferences", ["changeNightmode"])
+		changeAutoSkipDislikedLocal() {
+			localStorage.setItem(
+				"autoSkipDisliked",
+				this.localAutoSkipDisliked
+			);
+			this.changeAutoSkipDisliked(this.localAutoSkipDisliked);
+		},
+		...mapActions("user/preferences", [
+			"changeNightmode",
+			"changeAutoSkipDisliked"
+		])
 	}
 };
 </script>

+ 52 - 4
frontend/src/pages/Station/Report.vue

@@ -2,7 +2,10 @@
 	<modal title="Report">
 		<div slot="body">
 			<div class="columns song-types">
-				<div v-if="previousSong !== null" class="column song-type">
+				<div
+					v-if="previousSong !== null && localQueueSong === null"
+					class="column song-type"
+				>
 					<div
 						class="card is-fullwidth"
 						:class="{ 'is-highlight-active': isPreviousSongActive }"
@@ -43,7 +46,10 @@
 						/>
 					</div>
 				</div>
-				<div v-if="currentSong !== {}" class="column song-type">
+				<div
+					v-if="currentSong !== {} && localQueueSong === null"
+					class="column song-type"
+				>
 					<div
 						class="card is-fullwidth"
 						:class="{ 'is-highlight-active': isCurrentSongActive }"
@@ -84,6 +90,38 @@
 						/>
 					</div>
 				</div>
+				<div v-if="localQueueSong !== null" class="column song-type">
+					<div class="card is-fullwidth">
+						<header class="card-header">
+							<p class="card-header-title">Queue Song</p>
+						</header>
+						<div class="card-content">
+							<article class="media">
+								<figure class="media-left">
+									<p class="image is-64x64">
+										<img
+											:src="localQueueSong.thumbnail"
+											onerror='this.src="/assets/notes-transparent.png"'
+										/>
+									</p>
+								</figure>
+								<div class="media-content">
+									<div class="content">
+										<p>
+											<strong>{{
+												localQueueSong.title
+											}}</strong>
+											<br />
+											<small>{{
+												localQueueSong.artists
+											}}</small>
+										</p>
+									</div>
+								</div>
+							</article>
+						</div>
+					</div>
+				</div>
 			</div>
 			<div class="edit-report-wrapper">
 				<div class="columns is-multiline">
@@ -156,6 +194,7 @@ export default {
 		return {
 			isPreviousSongActive: false,
 			isCurrentSongActive: true,
+			localQueueSong: null,
 			report: {
 				resolved: false,
 				songId: "",
@@ -174,7 +213,8 @@ export default {
 					reasons: [
 						"Doesn't exist",
 						"It's private",
-						"It's not available in my country"
+						"It's not available in my country",
+						"Unofficial"
 					]
 				},
 				{
@@ -207,7 +247,8 @@ export default {
 		},
 		...mapState({
 			currentSong: state => state.station.currentSong,
-			previousSong: state => state.station.previousSong
+			previousSong: state => state.station.previousSong,
+			queueSong: state => state.station.reportQueueSong
 		})
 	},
 	mounted() {
@@ -216,6 +257,12 @@ export default {
 		});
 
 		this.report.songId = this.currentSong.songId;
+
+		if (this.queueSong !== null) {
+			this.localQueueSong = this.queueSong;
+			this.report.songId = this.queueSong.songId;
+			this.updateReportQueueSong(null);
+		}
 	},
 	methods: {
 		create() {
@@ -252,6 +299,7 @@ export default {
 				}
 			}
 		},
+		...mapActions("station", ["updateReportQueueSong"]),
 		...mapActions("modals", ["closeModal"])
 	}
 };

+ 8 - 0
frontend/src/pages/Station/components/CurrentlyPlaying.vue

@@ -71,6 +71,14 @@
 				>
 					<div class="icon"></div>
 				</a>
+				<button
+					class="button is-primary"
+					id="editsong-icon"
+					v-if="$parent.isAdminOnly() && !currentSong.simpleSong"
+					@click="$parent.editSong(currentSong)"
+				>
+					<i class="material-icons icon-with-button">edit</i>Edit
+				</button>
 			</div>
 		</div>
 	</div>

+ 8 - 0
frontend/src/pages/Station/components/Sidebar/MyPlaylists.vue

@@ -117,6 +117,14 @@ export default {
 					}
 				});
 			});
+
+			this.socket.on("event:playlist.updatePrivacy", data => {
+				this.playlists.forEach((playlist, index) => {
+					if (playlist._id === data.playlistId) {
+						this.playlists[index].privacy = data.privacy;
+					}
+				});
+			});
 		});
 	},
 	methods: {

+ 79 - 14
frontend/src/pages/Station/components/Sidebar/Queue/QueueItem.vue

@@ -48,21 +48,48 @@
 			<p id="song-duration">
 				{{ utils.formatTime(song.duration) }}
 			</p>
-			<i
-				v-if="
-					station.type === 'community' &&
-						($parent.isOwnerOnly() || $parent.isAdminOnly())
-				"
-				class="material-icons"
-				id="remove-queue-item"
-				@click="$parent.removeFromQueue(song.songId)"
-				>delete_forever</i
-			>
+			<div id="queue-item-buttons">
+				<i
+					v-if="
+						$parent.loggedIn &&
+							!song.simpleSong &&
+							song.likes !== -1 &&
+							song.dislikes !== -1
+					"
+					class="material-icons"
+					id="report-queue-item"
+					@click="reportQueueSong(song)"
+					>flag</i
+				>
+				<i
+					v-if="
+						$parent.isAdminOnly() &&
+							!song.simpleSong &&
+							song.likes !== -1 &&
+							song.dislikes !== -1
+					"
+					class="material-icons"
+					id="edit-queue-item"
+					@click="$parent.$parent.$parent.editSong(song)"
+					>edit</i
+				>
+				<i
+					v-if="
+						station.type === 'community' &&
+							($parent.isOwnerOnly() || $parent.isAdminOnly())
+					"
+					class="material-icons"
+					id="remove-queue-item"
+					@click="$parent.removeFromQueue(song.songId)"
+					>delete_forever</i
+				>
+			</div>
 		</div>
 	</div>
 </template>
 
 <script>
+import { mapActions } from "vuex";
 import { formatDistance, parseISO } from "date-fns";
 
 import UserIdToUsername from "../../../../../components/common/UserIdToUsername.vue";
@@ -88,6 +115,12 @@ export default {
 		};
 	},
 	methods: {
+		reportQueueSong(song) {
+			this.updateReportQueueSong(song);
+			this.openModal({ sector: "station", modal: "report" });
+		},
+		...mapActions("station", ["updateReportQueueSong"]),
+		...mapActions("modals", ["openModal"]),
 		formatDistance,
 		parseISO
 	}
@@ -111,17 +144,27 @@ export default {
 		display: flex;
 		align-items: center;
 	}
+	#duration-and-actions {
+		margin-left: 5px;
+	}
+	#queue-item-buttons {
+		display: flex;
+		flex-direction: column;
+		margin-left: 10px;
+	}
 
 	#thumbnail {
-		width: 60px;
-		height: 60px;
+		width: 65px;
+		height: 65px;
+		margin: -7.5px;
+		border-radius: 3px 0 0 3px;
 	}
 
 	#song-info {
 		display: flex;
 		flex-direction: column;
 		justify-content: center;
-		margin-left: 25px;
+		margin-left: 20px;
 
 		*:not(i) {
 			margin: 0;
@@ -146,9 +189,31 @@ export default {
 		font-size: 20px;
 	}
 
+	#report-queue-item {
+		cursor: pointer;
+		color: $yellow;
+		&:hover,
+		&:focus {
+			color: darken($yellow, 5%);
+		}
+	}
+
+	#edit-queue-item {
+		cursor: pointer;
+		color: $musare-blue;
+		&:hover,
+		&:focus {
+			color: darken($musare-blue, 5%);
+		}
+	}
+
 	#remove-queue-item {
 		cursor: pointer;
-		margin-left: 10px;
+		color: $red;
+		&:hover,
+		&:focus {
+			color: darken($red, 5%);
+		}
 	}
 }
 </style>

+ 45 - 7
frontend/src/pages/Station/index.vue

@@ -90,7 +90,10 @@
 
 				<div class="player-container quadrant" v-show="!noSong">
 					<div id="video-container">
-						<div id="player" style="width: 100%; height: 100%" />
+						<div
+							id="stationPlayer"
+							style="width: 100%; height: 100%"
+						/>
 						<div class="player-cannot-autoplay" v-if="!canAutoplay">
 							<p>
 								Please click anywhere on the screen for the
@@ -299,6 +302,13 @@
 
 		<main-footer v-if="exists" />
 
+		<edit-song
+			v-if="modals.editSong"
+			:song-id="editingSongId"
+			song-type="songs"
+			sector="station"
+		/>
+
 		<floating-box id="player-debug-box" ref="playerDebugBox">
 			<template #body>
 				<span><b>YouTube id</b>: {{ currentSong.songId }}</span>
@@ -380,7 +390,8 @@ export default {
 		FloatingBox,
 		CurrentlyPlaying,
 		StationSidebar,
-		AddToPlaylistDropdown
+		AddToPlaylistDropdown,
+		EditSong: () => import("../../components/modals/EditSong.vue")
 	},
 	data() {
 		return {
@@ -406,7 +417,8 @@ export default {
 			seeking: false,
 			playbackRate: 1,
 			volumeSliderValue: 0,
-			showPlaylistDropdown: false
+			showPlaylistDropdown: false,
+			editingSongId: ""
 		};
 	},
 	computed: {
@@ -484,8 +496,17 @@ export default {
 							if (this.currentSong.songId === song.songId) {
 								this.liked = song.liked;
 								this.disliked = song.disliked;
-								if (song.disliked === true)
+								if (
+									this.autoSkipDisliked &&
+									song.disliked === true
+								) {
 									this.voteSkipStation();
+									new Toast({
+										content:
+											"Automatically voted to skip disliked song.",
+										timeout: 4000
+									});
+								}
 							}
 						}
 					);
@@ -705,7 +726,7 @@ export default {
 		},
 		youtubeReady() {
 			if (!this.player) {
-				this.player = new window.YT.Player("player", {
+				this.player = new window.YT.Player("stationPlayer", {
 					height: 270,
 					width: 480,
 					videoId: this.currentSong.songId,
@@ -740,7 +761,19 @@ export default {
 						},
 						onError: err => {
 							console.log("iframe error", err);
-							if (this.loggedIn) this.voteSkipStation();
+							if (this.loggedIn) {
+								new Toast({
+									content:
+										"Error with YouTube Embed, voted to skip the current song.",
+									timeout: 8000
+								});
+								this.voteSkipStation();
+							} else {
+								new Toast({
+									content: "Error with YouTube Embed",
+									timeout: 8000
+								});
+							}
 						},
 						onStateChange: event => {
 							if (
@@ -1429,6 +1462,10 @@ export default {
 				}
 			);
 		},
+		editSong(song) {
+			this.editingSongId = song._id;
+			this.openModal({ sector: "station", modal: "editSong" });
+		},
 		...mapActions("modals", ["openModal"]),
 		...mapActions("station", [
 			"joinStation",
@@ -1442,7 +1479,8 @@ export default {
 			"updateNoSong",
 			"editStation",
 			"updateIfStationIsFavorited"
-		])
+		]),
+		...mapActions("editSongModal", ["stopVideo"])
 	}
 };
 </script>

+ 3 - 1
frontend/src/store/index.js

@@ -6,6 +6,7 @@ import settings from "./modules/settings";
 import modals from "./modules/modals";
 import station from "./modules/station";
 import admin from "./modules/admin";
+import editSongModal from "./modules/editSongModal";
 
 Vue.use(Vuex);
 
@@ -15,7 +16,8 @@ export default new Vuex.Store({
 		settings,
 		modals,
 		station,
-		admin
+		admin,
+		editSongModal
 	},
 	strict: false
 });

+ 1 - 61
frontend/src/store/modules/admin.js

@@ -12,70 +12,16 @@ const modules = {
 	songs: {
 		namespaced: true,
 		state: {
-			video: {
-				player: null,
-				paused: true,
-				playerReady: false,
-				autoPlayed: false,
-				currentTime: 0
-			},
-			editing: {},
 			songs: []
 		},
 		getters: {},
 		actions: {
-			editSong: ({ commit }, song) => commit("editSong", song),
-			stopVideo: ({ commit }) => commit("stopVideo"),
-			loadVideoById: ({ commit }, id, skipDuration) =>
-				commit("loadVideoById", id, skipDuration),
-			pauseVideo: ({ commit }, status) => commit("pauseVideo", status),
-			getCurrentTime: ({ commit, state }, fixedVal) => {
-				return new Promise(resolve => {
-					commit("getCurrentTime", fixedVal);
-					resolve(state.video.currentTime);
-				});
-			},
 			addSong: ({ commit }, song) => commit("addSong", song),
 			removeSong: ({ commit }, songId) => commit("removeSong", songId),
 			updateSong: ({ commit }, updatedSong) =>
-				commit("updateSong", updatedSong),
-			updateSongField: ({ commit }, data) =>
-				commit("updateSongField", data),
-			selectDiscogsInfo: ({ commit }, discogsInfo) =>
-				commit("selectDiscogsInfo", discogsInfo)
+				commit("updateSong", updatedSong)
 		},
 		mutations: {
-			editSong(state, song) {
-				if (song.song.discogs === undefined) song.song.discogs = null;
-				state.editing = { ...song };
-			},
-			stopVideo(state) {
-				state.video.player.stopVideo();
-			},
-			loadVideoById(state, id, skipDuration) {
-				state.video.player.loadVideoById(id, skipDuration);
-			},
-			pauseVideo(state, status) {
-				if (status) state.video.player.pauseVideo();
-				else state.video.player.playVideo();
-				state.video.paused = status;
-			},
-			getCurrentTime(state, fixedVal) {
-				if (!state.playerReady) state.video.currentTime = 0;
-				else {
-					Promise.resolve(state.video.player.getCurrentTime()).then(
-						time => {
-							if (fixedVal)
-								Promise.resolve(time.toFixed(fixedVal)).then(
-									fixedTime => {
-										state.video.currentTime = fixedTime;
-									}
-								);
-							else state.video.currentTime = time;
-						}
-					);
-				}
-			},
 			addSong(state, song) {
 				state.songs.push(song);
 			},
@@ -89,12 +35,6 @@ const modules = {
 					if (song._id === updatedSong._id)
 						Vue.set(state.songs, index, updatedSong);
 				});
-			},
-			updateSongField(state, data) {
-				state.editing.song[data.field] = data.value;
-			},
-			selectDiscogsInfo(state, discogsInfo) {
-				state.editing.song.discogs = discogsInfo;
 			}
 		}
 	},

+ 74 - 0
frontend/src/store/modules/editSongModal.js

@@ -0,0 +1,74 @@
+/* eslint no-param-reassign: 0 */
+
+// import Vue from "vue";
+// import admin from "../../api/admin/index";
+
+export default {
+	namespaced: true,
+	state: {
+		video: {
+			player: null,
+			paused: true,
+			playerReady: false,
+			autoPlayed: false,
+			currentTime: 0
+		},
+		song: {}
+	},
+	getters: {},
+	actions: {
+		editSong: ({ commit }, song) => commit("editSong", song),
+		stopVideo: ({ commit }) => commit("stopVideo"),
+		loadVideoById: ({ commit }, id, skipDuration) =>
+			commit("loadVideoById", id, skipDuration),
+		pauseVideo: ({ commit }, status) => commit("pauseVideo", status),
+		getCurrentTime: ({ commit, state }, fixedVal) => {
+			return new Promise(resolve => {
+				commit("getCurrentTime", fixedVal);
+				resolve(state.video.currentTime);
+			});
+		},
+		updateSongField: ({ commit }, data) => commit("updateSongField", data),
+		selectDiscogsInfo: ({ commit }, discogsInfo) =>
+			commit("selectDiscogsInfo", discogsInfo)
+	},
+	mutations: {
+		editSong(state, song) {
+			if (song.discogs === undefined) song.discogs = null;
+			state.song = { ...song };
+		},
+		stopVideo(state) {
+			state.video.player.stopVideo();
+		},
+		loadVideoById(state, id, skipDuration) {
+			state.video.player.loadVideoById(id, skipDuration);
+		},
+		pauseVideo(state, status) {
+			if (status) state.video.player.pauseVideo();
+			else state.video.player.playVideo();
+			state.video.paused = status;
+		},
+		getCurrentTime(state, fixedVal) {
+			if (!state.playerReady) state.video.currentTime = 0;
+			else {
+				Promise.resolve(state.video.player.getCurrentTime()).then(
+					time => {
+						if (fixedVal)
+							Promise.resolve(time.toFixed(fixedVal)).then(
+								fixedTime => {
+									state.video.currentTime = fixedTime;
+								}
+							);
+						else state.video.currentTime = time;
+					}
+				);
+			}
+		},
+		updateSongField(state, data) {
+			state.song[data.field] = data.value;
+		},
+		selectDiscogsInfo(state, discogsInfo) {
+			state.song.discogs = discogsInfo;
+		}
+	}
+};

+ 8 - 1
frontend/src/store/modules/station.js

@@ -11,7 +11,8 @@ const state = {
 	songsList: [],
 	stationPaused: true,
 	localPaused: false,
-	noSong: true
+	noSong: true,
+	reportQueueSong: null
 };
 
 const getters = {};
@@ -35,6 +36,9 @@ const actions = {
 	updatePreviousSong: ({ commit }, previousSong) => {
 		commit("updatePreviousSong", previousSong);
 	},
+	updateReportQueueSong: ({ commit }, reportQueueSong) => {
+		commit("updateReportQueueSong", reportQueueSong);
+	},
 	updateSongsList: ({ commit }, songsList) => {
 		commit("updateSongsList", songsList);
 	},
@@ -81,6 +85,9 @@ const mutations = {
 	updatePreviousSong(state, previousSong) {
 		state.previousSong = previousSong;
 	},
+	updateReportQueueSong(state, reportQueueSong) {
+		state.reportQueueSong = reportQueueSong;
+	},
 	updateSongsList(state, songsList) {
 		state.songsList = songsList;
 	},

+ 8 - 1
frontend/src/store/modules/user.js

@@ -217,16 +217,23 @@ const modules = {
 	preferences: {
 		namespaced: true,
 		state: {
-			nightmode: true
+			nightmode: true,
+			autoSkipDisliked: true
 		},
 		actions: {
 			changeNightmode: ({ commit }, nightmode) => {
 				commit("changeNightmode", nightmode);
+			},
+			changeAutoSkipDisliked: ({ commit }, autoSkipDisliked) => {
+				commit("changeAutoSkipDisliked", autoSkipDisliked);
 			}
 		},
 		mutations: {
 			changeNightmode(state, nightmode) {
 				state.nightmode = nightmode;
+			},
+			changeAutoSkipDisliked(state, autoSkipDisliked) {
+				state.autoSkipDisliked = autoSkipDisliked;
 			}
 		}
 	}