فهرست منبع

Merge pull request #15 from Musare/staging

Beta Release Day 8
Jonathan 8 سال پیش
والد
کامیت
2f68a28ceb

+ 108 - 12
backend/logic/actions/playlists.js

@@ -29,6 +29,22 @@ cache.sub('playlist.delete', res => {
 	});
 });
 
+cache.sub('playlist.moveSongToTop', res => {
+	utils.socketsFromUser(res.userId, (sockets) => {
+		sockets.forEach((socket) => {
+			socket.emit('event:playlist.moveSongToTop', {playlistId: res.playlistId, songId: res.songId});
+		});
+	});
+});
+
+cache.sub('playlist.moveSongToBottom', res => {
+	utils.socketsFromUser(res.userId, (sockets) => {
+		sockets.forEach((socket) => {
+			socket.emit('event:playlist.moveSongToBottom', {playlistId: res.playlistId, songId: res.songId});
+		});
+	});
+});
+
 cache.sub('playlist.addSong', res => {
 	utils.socketsFromUser(res.userId, (sockets) => {
 		sockets.forEach((socket) => {
@@ -57,7 +73,7 @@ let lib = {
 
 	indexForUser: hooks.loginRequired((session, cb, userId) => {
 		db.models.playlist.find({ createdBy: userId }, (err, playlists) => {
-			if (err) return cb({ status: 'failure', message: 'Something went wrong when getting the playlists.'});;
+			if (err) return cb({ status: 'failure', message: 'Something went wrong when getting the playlists'});;
 			cb({
 				status: 'success',
 				data: playlists
@@ -91,7 +107,7 @@ let lib = {
 
 	getPlaylist: hooks.loginRequired((session, id, cb, userId) => {
 		playlists.getPlaylist(id, (err, playlist) => {
-			if (err || playlist.createdBy !== userId) return cb({status: 'success', message: 'Playlist not found.'});
+			if (err || playlist.createdBy !== userId) return cb({status: 'success', message: 'Playlist not found'});
 			if (err == null) return cb({
 				status: 'success',
 				data: playlist
@@ -111,10 +127,11 @@ let lib = {
 	}),
 
 	addSongToPlaylist: hooks.loginRequired((session, songId, playlistId, cb, userId) => {
+		console.log(songId);
 		async.waterfall([
 			(next) => {
 				playlists.getPlaylist(playlistId, (err, playlist) => {
-					if (err || !playlist || playlist.createdBy !== userId) return next('Something went wrong when trying to get the playlist.');
+					if (err || !playlist || playlist.createdBy !== userId) return next('Something went wrong when trying to get the playlist');
 
 					let found = false;
 					playlist.songs.forEach((song) => {
@@ -142,7 +159,7 @@ let lib = {
 				});
 			},
 			(newSong, next) => {
-				db.models.playlist.update({_id: playlistId}, {$push: {songs: newSong}}, (err) => {
+				db.models.playlist.update({ _id: playlistId }, { $push: { songs: newSong } }, (err) => {
 					if (err) {
 						console.error(err);
 						return next('Failed to add song to playlist');
@@ -163,7 +180,7 @@ let lib = {
 		});
 	}),
 	
-	/*addSetToPlaylist: hooks.loginRequired((session, url, playlistId, cb, userId) => {
+	addSetToPlaylist: hooks.loginRequired((session, url, playlistId, cb, userId) => {
 		async.waterfall([
 			(next) => {
 				utils.getPlaylistFromYouTube(url, songs => {
@@ -171,14 +188,20 @@ let lib = {
 				});
 			},
 			(songs, next) => {
+				let processed = 0;
+				function checkDone() {
+					if (processed === songs.length) next();
+				}
 				for (let s = 0; s < songs.length; s++) {
-					lib.addSongToPlaylist(session, songs[s].contentDetails.videoId, playlistId, (res) => {})();
+					lib.addSongToPlaylist(session, songs[s].contentDetails.videoId, playlistId, () => {
+						processed++;
+						checkDone();
+					});
 				}
-				next(null);
 			},
 			(next) => {
 				playlists.getPlaylist(playlistId, (err, playlist) => {
-					if (err || !playlist || playlist.createdBy !== userId) return next('Something went wrong while trying to get the playlist.');
+					if (err || !playlist || playlist.createdBy !== userId) return next('Something went wrong while trying to get the playlist');
 
 					next(null, playlist);
 				});
@@ -186,14 +209,14 @@ let lib = {
 		],
 		(err, playlist) => {
 			if (err) return cb({ status: 'failure', message: err });
-			else if (playlist.songs) return cb({ status: 'success', message: 'Playlist has been successfully added', data: playlist.songs });
+			else if (playlist.songs) return cb({ status: 'success', message: 'Playlist has been successfully imported.', data: playlist.songs });
 		});
-	}),*/
+	}),
 
 
 	removeSongFromPlaylist: hooks.loginRequired((session, songId, playlistId, cb, userId) => {
 		playlists.getPlaylist(playlistId, (err, playlist) => {
-			if (err || !playlist || playlist.createdBy !== userId) return cb({ status: 'failure', message: 'Something went wrong when getting the playlist.'});
+			if (err || !playlist || playlist.createdBy !== userId) return cb({ status: 'failure', message: 'Something went wrong when getting the playlist'});
 
 			for (let z = 0; z < playlist.songs.length; z++) {
 				if (playlist.songs[z]._id == songId) playlist.songs.shift(playlist.songs[z]);
@@ -221,7 +244,80 @@ let lib = {
 				return cb({ status: 'success', message: 'Playlist has been successfully updated' });
 			})
 		});
-	}),/*
+	}),
+
+	moveSongToTop: hooks.loginRequired((session, playlistId, songId, cb, userId) => {
+		playlists.getPlaylist(playlistId, (err, playlist) => {
+			if (err || !playlist || playlist.createdBy !== userId) return cb({ status: 'failure', message: 'Something went wrong when getting the playlist'});
+			let found = false;
+			let foundSong;
+			playlist.songs.forEach((song) => {
+				if (song._id === songId) {
+					foundSong = song;
+					found = true;
+				}
+			});
+
+			if (found) {
+				db.models.playlist.update({_id: playlistId}, {$pull: {songs: {_id: songId}}}, (err) => {
+					console.log(err);
+					if (err) return cb({status: 'failure', message: 'Something went wrong when moving the song'});
+					db.models.playlist.update({_id: playlistId}, {
+						$push: {
+							songs: {
+								$each: [foundSong],
+								$position: 0
+							}
+						}
+					}, (err) => {
+						console.log(err);
+						if (err) return cb({status: 'failure', message: 'Something went wrong when moving the song'});
+						playlists.updatePlaylist(playlistId, (err) => {
+							if (err) return cb({ status: 'failure', message: err});
+							cache.pub('playlist.moveSongToTop', {playlistId, songId, userId: userId});
+							return cb({ status: 'success', message: 'Playlist has been successfully updated' });
+						})
+					});
+				});
+			} else {
+				return cb({status: 'failure', message: 'Song not found.'});
+			}
+		});
+	}),
+
+	moveSongToBottom: hooks.loginRequired((session, playlistId, songId, cb, userId) => {
+		playlists.getPlaylist(playlistId, (err, playlist) => {
+			if (err || !playlist || playlist.createdBy !== userId) return cb({ status: 'failure', message: 'Something went wrong when getting the playlist'});
+			let found = false;
+			let foundSong;
+			playlist.songs.forEach((song) => {
+				if (song._id === songId) {
+					foundSong = song;
+					found = true;
+				}
+			});
+
+			if (found) {
+				db.models.playlist.update({_id: playlistId}, {$pull: {songs: {_id: songId}}}, (err) => {
+					console.log(err);
+					if (err) return cb({status: 'failure', message: 'Something went wrong when moving the song'});
+					db.models.playlist.update({_id: playlistId}, {
+						$push: { songs: foundSong }
+					}, (err) => {
+						console.log(err);
+						if (err) return cb({status: 'failure', message: 'Something went wrong when moving the song'});
+						playlists.updatePlaylist(playlistId, (err) => {
+							if (err) return cb({ status: 'failure', message: err});
+							cache.pub('playlist.moveSongToBottom', {playlistId, songId, userId: userId});
+							return cb({ status: 'success', message: 'Playlist has been successfully updated' });
+						})
+					});
+				});
+			} else return cb({status: 'failure', message: 'Song not found'});
+		});
+	}),
+
+	/*
 
 	promoteSong: hooks.loginRequired((session, playlistId, fromIndex, cb, userId) => {
 		db.models.playlist.findOne({ _id: playlistId }, (err, playlist) => {

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

@@ -10,9 +10,7 @@ const request = require('request');
 const hooks = require('./hooks');
 
 cache.sub('queue.newSong', songId => {
-	console.log(123321);
 	db.models.queueSong.findOne({_id: songId}, (err, song) => {
-		console.log(err, song);
 		utils.emitToRoom('admin.queue', 'event:admin.queueSong.added', song);
 	});
 });

+ 36 - 6
backend/logic/actions/reports.js

@@ -3,25 +3,44 @@
 const async = require('async');
 
 const db = require('../db');
+const cache = require('../cache');
+const utils = require('../utils');
 const hooks = require('./hooks');
 const songs = require('../songs');
 
+cache.sub('report.resolve', reportId => {
+	utils.emitToRoom('admin.reports', 'event:admin.report.resolved', reportId);
+});
+
+cache.sub('report.create', report => {
+	utils.emitToRoom('admin.reports', 'event:admin.report.created', report);
+});
+
 module.exports = {
 
 	index: hooks.adminRequired((session, cb) => {
 		db.models.report.find({ resolved: false }).sort({ released: 'desc' }).exec((err, reports) => {
-			if (err) console.error(err);
+			if (err) {
+				console.error(err);
+				cb({ 'status': 'failure', 'message': 'Something went wrong'});
+			}
 			cb({ status: 'success', data: reports });
 		});
 	}),
 
 	resolve: hooks.adminRequired((session, _id, cb) => {
 		db.models.report.findOne({ _id }).sort({ released: 'desc' }).exec((err, report) => {
-			if (err) console.error(err);
+			if (err) {
+				console.error(err);
+				cb({ 'status': 'failure', 'message': 'Something went wrong'});
+			}
 			report.resolved = true;
 			report.save(err => {
 				if (err) console.error(err);
-				else cb({ status: 'success', message: 'Successfully resolved Report' });
+				else {
+					cache.pub('report.resolve', _id);
+					cb({ status: 'success', message: 'Successfully resolved Report' });
+				}
 			});
 		});
 	}),
@@ -32,7 +51,7 @@ module.exports = {
 			(next) => {
 				songs.getSong(data.songId, (err, song) => {
 					if (err) return next(err);
-					if (!song) return next('Song does not exist in our Database.');
+					if (!song) return next('Song does not exist in our Database');
 					next();
 				});
 			},
@@ -92,6 +111,14 @@ module.exports = {
 
 				next();
 			},
+			
+			(next) => {
+				for (let r = 0; r < data.issues.length; r++) {
+					if (data.issues[r].reasons.length === 0) data.issues.splice(r, 1);
+				}
+
+				next();
+			},
 
 			(next) => {
 				data.createdBy = userId;
@@ -99,9 +126,12 @@ module.exports = {
 				db.models.report.create(data, next);
 			}
 
-		], err => {
+		], (err, report) => {
 			if (err) return cb({ 'status': 'failure', 'message': 'Something went wrong'});
-			return cb({ 'status': 'success', 'message': 'Successfully created report' });
+			else {
+				cache.pub('report.create', report);
+				return cb({ 'status': 'success', 'message': 'Successfully created report' });
+			}
 		});
 	})
 

+ 16 - 8
backend/logic/actions/stations.js

@@ -126,6 +126,13 @@ module.exports = {
 		});
 	},
 
+	find: (session, stationId, cb) => {
+		stations.getStation(stationId, (err, station) => {
+			if (err) cb({ status: 'error', message: err });
+			else if (station) cb({ status: 'success', data: station });
+		});
+	},
+
 	getPlaylist: (session, stationId, cb) => {
 		let playlist = [];
 
@@ -242,9 +249,7 @@ module.exports = {
 				if (err) return cb({ status: 'failure', message: 'Something went wrong when saving the station.' });
 				stations.updateStation(stationId, (err, station) => {
 					cache.pub('station.voteSkipSong', stationId);
-					if (station.currentSong && station.currentSong.skipVotes.length >= 1) {
-						stations.skipStation(stationId)();
-					}
+					if (station.currentSong && station.currentSong.skipVotes.length >= 3) stations.skipStation(stationId)();
 					cb({ status: 'success', message: 'Successfully voted to skip the song.' });
 				})
 			});
@@ -404,6 +409,7 @@ module.exports = {
 
 	create: hooks.loginRequired((session, data, cb) => {
 		data._id = data._id.toLowerCase();
+		let blacklist = ["country", "edm", "musare", "hip-hop", "rap", "top-hits", "todays-hits", "old-school", "christmas", "about", "support", "staff", "help", "news", "terms", "privacy", "profile", "c", "community", "tos", "login", "register", "p", "official", "o", "trap", "faq", "team", "donate", "buy", "shop", "forums", "explore", "settings", "admin"];
 		async.waterfall([
 
 			(next) => {
@@ -418,7 +424,7 @@ module.exports = {
 				if (station) return next({ 'status': 'failure', 'message': 'A station with that name or display name already exists' });
 				const { _id, displayName, description, genres, playlist, type, blacklistedGenres } = data;
 				cache.hget('sessions', session.sessionId, (err, session) => {
-					if (type == 'official') {
+					if (type === 'official') {
 						db.models.user.findOne({_id: session.userId}, (err, user) => {
 							if (err) return next({ 'status': 'failure', 'message': 'Something went wrong when getting your user info.' });
 							if (!user) return next({ 'status': 'failure', 'message': 'User not found.' });
@@ -435,7 +441,8 @@ module.exports = {
 								currentSong: stations.defaultSong
 							}, next);
 						});
-					} else if (type == 'community') {
+					} else if (type === 'community') {
+						if (blacklist.indexOf(_id) !== -1) return next({ 'status': 'failure', 'message': 'That id is blacklisted. Please use a different id.' });
 						db.models.station.create({
 							_id,
 							displayName,
@@ -453,10 +460,11 @@ module.exports = {
 		], (err, station) => {
 			if (err) {
 				console.error(err);
-				return cb({ 'status': 'failure', 'message': 'Something went wrong.'});
+				return cb({ 'status': 'failure', 'message': err.message});
+			} else {
+				cache.pub('station.create', data._id);
+				cb({ 'status': 'success', 'message': 'Successfully created station' });
 			}
-			cache.pub('station.create', data._id);
-			cb({ 'status': 'success', 'message': 'Successfully created station.' });
 		});
 	}),
 

+ 1 - 0
backend/logic/io.js

@@ -90,6 +90,7 @@ module.exports = {
 					if (err && err !== true) socket.emit('ready', false);
 					else if (session && session.userId) {
 						db.models.user.findOne({ _id: session.userId }, (err, user) => {
+							if (err || !user) return socket.emit('ready', false);
 							let role = '';
 							let username = '';
 							let userId = '';

+ 5 - 8
backend/logic/stations.js

@@ -146,7 +146,7 @@ module.exports = {
 					station = cache.schemas.station(station);
 					cache.hset('stations', stationId, station);
 					next(true, station);
-				} else next('Station not found.');
+				} else next('Station not found');
 			},
 
 		], (err, station) => {
@@ -235,9 +235,7 @@ module.exports = {
 																		thumbnail: song.thumbnail
 																	};
 																	station.playlist = newPlaylist;
-																} else {
-																	$set.currentSong = _this.defaultSong;
-																}
+																} else $set.currentSong = _this.defaultSong;
 																$set.startedAt = Date.now();
 																$set.timePaused = 0;
 																next(null, $set);
@@ -391,11 +389,10 @@ module.exports = {
 
 	defaultSong: {
 		_id: '60ItHLz5WEA',
-		title: 'Faded',
-		artists: ['Alan Walker'],
+		title: 'Faded - Alan Walker',
 		duration: 212,
-		skipDuration: 0,
-		thumbnail: 'https://i.scdn.co/image/2ddde58427f632037093857ebb71a67ddbdec34b'
+		likes: -1,
+		dislikes: -1
 	}
 
 };

+ 81 - 62
backend/logic/utils.js

@@ -93,6 +93,10 @@ function convertTime (duration) {
 	return (hours < 10 ? ("0" + hours + ":") : (hours + ":")) + (minutes < 10 ? ("0" + minutes + ":") : (minutes + ":")) + (seconds < 10 ? ("0" + seconds) : seconds);
 }
 
+let youtubeRequestCallbacks = [];
+let youtubeRequestsPending = 0;
+let youtubeRequestsActive = false;
+
 module.exports = {
 	htmlEntities: str => String(str).replace(/&/g, '&amp;').replace(/</g, '&lt;').replace(/>/g, '&gt;').replace(/"/g, '&quot;'),
 	generateRandomString: function(len) {
@@ -179,9 +183,7 @@ module.exports = {
 		let socket = this.socketFromSession(socketId);
 		let rooms = socket.rooms;
 		for (let room in rooms) {
-			if (room.indexOf('song.') !== -1) {
-				socket.leave(rooms);
-			}
+			if (room.indexOf('song.') !== -1) socket.leave(rooms);
 		}
 		socket.join(room);
 	},
@@ -190,9 +192,7 @@ module.exports = {
 			let socket = sockets[id];
 			let rooms = socket.rooms;
 			for (let room in rooms) {
-				if (room.indexOf('song.') !== -1) {
-					socket.leave(room);
-				}
+				if (room.indexOf('song.') !== -1) socket.leave(room);
 			}
 			socket.join(room);
 		}
@@ -202,9 +202,7 @@ module.exports = {
 			let socket = sockets[id];
 			let rooms = socket.rooms;
 			for (let room in rooms) {
-				if (room.indexOf('song.') !== -1) {
-					socket.leave(room);
-				}
+				if (room.indexOf('song.') !== -1) socket.leave(room);
 			}
 		}
 	},
@@ -226,55 +224,66 @@ module.exports = {
 		let roomSockets = [];
 		for (let id in sockets) {
 			let socket = sockets[id];
-			if (socket.rooms[room]) {
-				roomSockets.push(socket);
-			}
+			if (socket.rooms[room]) roomSockets.push(socket);
 		}
 		return roomSockets;
 	},
 	getSongFromYouTube: (songId, cb) => {
-		const youtubeParams = [
-			'part=snippet,contentDetails,statistics,status',
-			`id=${encodeURIComponent(songId)}`,
-			`key=${config.get('apis.youtube.key')}`
-		].join('&');
 
-		request(`https://www.googleapis.com/youtube/v3/videos?${youtubeParams}`, (err, res, body) => {
+		youtubeRequestCallbacks.push({cb: (test) => {
+			youtubeRequestsActive = true;
+			const youtubeParams = [
+				'part=snippet,contentDetails,statistics,status',
+				`id=${encodeURIComponent(songId)}`,
+				`key=${config.get('apis.youtube.key')}`
+			].join('&');
 
-			if (err) {
-				console.error(err);
-				return next('Failed to find song from YouTube');
-			}
+			request(`https://www.googleapis.com/youtube/v3/videos?${youtubeParams}`, (err, res, body) => {
 
-			body = JSON.parse(body);
+				youtubeRequestCallbacks.splice(0, 1);
+				if (youtubeRequestCallbacks.length > 0) {
+					youtubeRequestCallbacks[0].cb(youtubeRequestCallbacks[0].songId);
+				} else youtubeRequestsActive = false;
 
-			//TODO Clean up duration converter
-			let dur = body.items[0].contentDetails.duration;
-			dur = dur.replace('PT', '');
-			let duration = 0;
-			dur = dur.replace(/([\d]*)H/, (v, v2) => {
-				v2 = Number(v2);
-				duration = (v2 * 60 * 60);
-				return '';
-			});
-			dur = dur.replace(/([\d]*)M/, (v, v2) => {
-				v2 = Number(v2);
-				duration += (v2 * 60);
-				return '';
-			});
-			dur = dur.replace(/([\d]*)S/, (v, v2) => {
-				v2 = Number(v2);
-				duration += v2;
-				return '';
+				if (err) {
+					console.error(err);
+					return null;
+				}
+
+				body = JSON.parse(body);
+
+				//TODO Clean up duration converter
+				let dur = body.items[0].contentDetails.duration;
+				dur = dur.replace('PT', '');
+				let duration = 0;
+				dur = dur.replace(/([\d]*)H/, (v, v2) => {
+					v2 = Number(v2);
+					duration = (v2 * 60 * 60);
+					return '';
+				});
+				dur = dur.replace(/([\d]*)M/, (v, v2) => {
+					v2 = Number(v2);
+					duration += (v2 * 60);
+					return '';
+				});
+				dur = dur.replace(/([\d]*)S/, (v, v2) => {
+					v2 = Number(v2);
+					duration += v2;
+					return '';
+				});
+
+				let song = {
+					_id: body.items[0].id,
+					title: body.items[0].snippet.title,
+					duration
+				};
+				cb(song);
 			});
+		}, songId});
 
-			let song = {
-				_id: body.items[0].id,
-				title: body.items[0].snippet.title,
-				duration
-			};
-			cb(song);
-		});
+		if (!youtubeRequestsActive) {
+			youtubeRequestCallbacks[0].cb(youtubeRequestCallbacks[0].songId);
+		}
 	},
 	getPlaylistFromYouTube: (url, cb) => {
 		
@@ -282,22 +291,32 @@ module.exports = {
 		var regex = new RegExp('[\\?&]' + name + '=([^&#]*)');
 		let playlistId = regex.exec(url)[1];
 
-		const youtubeParams = [
-			'part=contentDetails',
-			`playlistId=${encodeURIComponent(playlistId)}`,
-			`maxResults=50`,
-			`key=${config.get('apis.youtube.key')}`
-		].join('&');
+		function getPage(pageToken, songs) {
+			let nextPageToken = (pageToken) ? `pageToken=${pageToken}` : '';
+			const youtubeParams = [
+				'part=contentDetails',
+				`playlistId=${encodeURIComponent(playlistId)}`,
+				`maxResults=5`,
+				`key=${config.get('apis.youtube.key')}`,
+				nextPageToken
+			].join('&');
 
-		request(`https://www.googleapis.com/youtube/v3/playlistItems?${youtubeParams}`, (err, res, body) => {
-			if (err) {
-				console.error(err);
-				return next('Failed to find playlist from YouTube');
-			}
+			request(`https://www.googleapis.com/youtube/v3/playlistItems?${youtubeParams}`, (err, res, body) => {
+				if (err) {
+					console.error(err);
+					return next('Failed to find playlist from YouTube');
+				}
 
-			body = JSON.parse(body);
-			cb(body.items);
-		});
+				body = JSON.parse(body);
+				songs = songs.concat(body.items);
+				if (body.nextPageToken) getPage(body.nextPageToken, songs);
+				else {
+					console.log(songs);
+					cb(songs);
+				}
+			});
+		}
+		getPage(null, []);
 	},
 	getSongFromSpotify: (song, cb) => {
 		const spotifyParams = [

+ 70 - 0
frontend/App.vue

@@ -147,4 +147,74 @@
 		border-radius: 5px;
 		z-index: 10000000;
 	}
+
+	.tooltip {
+		position: relative;
+		
+		&:after {
+			position: absolute;
+			min-width: 80px;
+			margin-left: -75%;
+			text-align: center;
+			padding: 7.5px 6px;
+			border-radius: 2px;
+			background-color: #323232;
+			font-size: .9em;
+			color: #fff;
+			content: attr(data-tooltip);
+			opacity: 0;
+			transition: all .2s ease-in-out .1s;
+			visibility: hidden;
+		}
+		
+		&:hover:after {
+			opacity: 1;
+			visibility: visible;
+		}
+	}
+
+	.tooltip-top {
+		&:after {
+			bottom: 150%;
+		}
+
+		&:hover {
+			&:after { bottom: 120%; }
+		}
+	}
+
+
+	.tooltip-bottom {
+		&:after {
+			top: 155%;
+		}
+
+		&:hover {
+			&:after { top: 125%; }
+		}
+	}
+
+	.tooltip-left {
+		&:after {
+			bottom: -10px;
+			right: 130%;
+			min-width: 100px;
+		}
+
+		&:hover {
+			&:after { right: 110%; }
+		}
+	}
+
+	.tooltip-right {
+		&:after {
+			bottom: -10px;
+			left: 190%;
+			min-width: 100px;
+		}
+
+		&:hover {
+			&:after { left: 200%; }
+		}
+	}
 </style>

BIN
frontend/build/assets/notes-transparent.png


BIN
frontend/build/assets/notes.png


+ 0 - 0
frontend/build/assets/discord.svg → frontend/build/assets/social/discord.svg


+ 47 - 0
frontend/build/assets/social/github.svg

@@ -0,0 +1,47 @@
+<svg width="245" height="240" xmlns="http://www.w3.org/2000/svg">
+ <metadata id="metadata3362">image/svg+xml</metadata>
+
+ <g>
+  <title>background</title>
+  <rect fill="none" id="canvas_background" height="402" width="582" y="-1" x="-1"/>
+ </g>
+ <g>
+  <title>Layer 1</title>
+  <g id="svg_86">
+   <metadata fill="#4a4a4a" transform="matrix(1.1862952709197998,0,0,1.1862952709197998,27.944357648753794,19.726211432238415) " id="svg_85">image/svg+xml</metadata>
+   <defs fill="#4a4a4a">
+    <clipPath fill="#4a4a4a" clipPathUnits="userSpaceOnUse" id="svg_83">
+     <path fill="#4a4a4a" d="m0,551.986l530.973,0l0,-551.986l-530.973,0l0,551.986z" id="svg_84"/>
+    </clipPath>
+   </defs>
+   <g transform="matrix(1.1862952709197998,0,0,1.1862952709197998,27.944357648753794,19.726211432238415) " id="svg_66">
+    <title fill="#4a4a4a">Layer 1</title>
+    <g transform="matrix(1.3333333,0,0,-1.3333333,0,735.98133) " id="svg_81">
+     <path fill="#4a4a4a" fill-rule="evenodd" d="m60.38802,551.98602c-33.347,0 -60.388,-27.035 -60.388,-60.388c0,-26.68 17.303,-49.316 41.297,-57.301c3.018,-0.559 4.126,1.31 4.126,2.905c0,1.439 -0.056,6.197 -0.082,11.243c-16.8,-3.653 -20.345,7.125 -20.345,7.125c-2.747,6.979 -6.705,8.836 -6.705,8.836c-5.479,3.748 0.413,3.671 0.413,3.671c6.064,-0.426 9.257,-6.224 9.257,-6.224c5.386,-9.231 14.127,-6.562 17.573,-5.019c0.543,3.902 2.107,6.567 3.834,8.075c-13.413,1.526 -27.513,6.705 -27.513,29.844c0,6.592 2.359,11.98 6.222,16.209c-0.627,1.521 -2.694,7.663 0.586,15.981c0,0 5.071,1.622 16.61,-6.191c4.817,1.338 9.983,2.009 15.115,2.033c5.132,-0.024 10.302,-0.695 15.128,-2.033c11.526,7.813 16.59,6.191 16.59,6.191c3.287,-8.318 1.22,-14.46 0.593,-15.981c3.872,-4.229 6.214,-9.617 6.214,-16.209c0,-23.195 -14.127,-28.301 -27.574,-29.796c2.166,-1.874 4.096,-5.549 4.096,-11.183c0,-8.08 -0.069,-14.583 -0.069,-16.572c0,-1.608 1.086,-3.49 4.147,-2.898c23.982,7.994 41.263,30.622 41.263,57.294c0,33.353 -27.037,60.388 -60.388,60.388" id="svg_82"/>
+    </g>
+    <g transform="matrix(1.3333333,0,0,-1.3333333,0,735.98133) " id="svg_79">
+     <path fill="#4a4a4a" fill-rule="nonzero" d="m22.87242,465.28284c-0.133,-0.301 -0.605,-0.391 -1.035,-0.185c-0.439,0.198 -0.684,0.607 -0.542,0.908c0.13,0.308 0.602,0.394 1.04,0.188c0.438,-0.197 0.688,-0.61 0.537,-0.911" id="svg_80"/>
+    </g>
+    <g transform="matrix(1.3333333,0,0,-1.3333333,0,735.98133) " id="svg_77">
+     <path fill="#4a4a4a" fill-rule="nonzero" d="m25.31871,462.55432c-0.288,-0.267 -0.852,-0.143 -1.233,0.279c-0.396,0.421 -0.469,0.985 -0.177,1.255c0.297,0.267 0.843,0.142 1.238,-0.279c0.396,-0.426 0.473,-0.984 0.172,-1.255" id="svg_78"/>
+    </g>
+    <g transform="matrix(1.3333333,0,0,-1.3333333,0,735.98133) " id="svg_75">
+     <path fill="#4a4a4a" fill-rule="nonzero" d="m27.69963,459.07684c-0.37,-0.258 -0.976,-0.017 -1.35,0.52c-0.37,0.538 -0.37,1.182 0.009,1.44c0.374,0.258 0.971,0.025 1.35,-0.507c0.369,-0.546 0.369,-1.19 -0.009,-1.453" id="svg_76"/>
+    </g>
+    <g transform="matrix(1.3333333,0,0,-1.3333333,0,735.98133) " id="svg_73">
+     <path fill="#4a4a4a" fill-rule="nonzero" d="m30.96132,455.71643c-0.331,-0.365 -1.036,-0.267 -1.552,0.232c-0.528,0.486 -0.675,1.177 -0.344,1.542c0.336,0.366 1.045,0.263 1.565,-0.231c0.524,-0.486 0.683,-1.182 0.331,-1.543" id="svg_74"/>
+    </g>
+    <g transform="matrix(1.3333333,0,0,-1.3333333,0,735.98133) " id="svg_71">
+     <path fill="#4a4a4a" fill-rule="nonzero" d="m35.46132,453.76535c-0.147,-0.473 -0.825,-0.687 -1.509,-0.486c-0.683,0.207 -1.13,0.76 -0.992,1.238c0.142,0.476 0.824,0.7 1.513,0.485c0.682,-0.206 1.13,-0.756 0.988,-1.237" id="svg_72"/>
+    </g>
+    <g transform="matrix(1.3333333,0,0,-1.3333333,0,735.98133) " id="svg_69">
+     <path fill="#4a4a4a" fill-rule="nonzero" d="m40.40373,453.40393c0.017,-0.498 -0.563,-0.911 -1.281,-0.92c-0.722,-0.016 -1.307,0.387 -1.315,0.877c0,0.503 0.568,0.911 1.289,0.924c0.718,0.014 1.307,-0.387 1.307,-0.881" id="svg_70"/>
+    </g>
+    <g transform="matrix(1.3333333,0,0,-1.3333333,0,735.98133) " id="svg_67">
+     <path fill="#4a4a4a" fill-rule="nonzero" d="m45.00234,454.18625c0.086,-0.485 -0.413,-0.984 -1.126,-1.117c-0.701,-0.129 -1.35,0.172 -1.439,0.653c-0.087,0.498 0.42,0.997 1.121,1.126c0.714,0.124 1.353,-0.168 1.444,-0.662" id="svg_68"/>
+    </g>
+   </g>
+  </g>
+  <g id="svg_22"/>
+ </g>
+</svg>

+ 44 - 14
frontend/components/Admin/Reports.vue

@@ -8,7 +8,7 @@
 						<td>Created By</td>
 						<td>Created At</td>
 						<td>Description</td>
-						<td>Issues</td>
+						<td>Options</td>
 					</tr>
 				</thead>
 				<tbody>
@@ -23,40 +23,70 @@
 							<span>{{ report.createdAt }}</span>
 						</td>
 						<td>
-							<span>{{ report.issues }}</span>
+							<span>{{ report.description }}</span>
 						</td>
 						<td>
-							<a class='button is-primary' @click='resolve()'>Resolve</a>
+							<a class='button is-warning' @click='toggleModal(report)'>Issues</a>
+							<a class='button is-primary' @click='resolve(report._id)'>Resolve</a>
 						</td>
 					</tr>
 				</tbody>
 			</table>
 		</div>
 	</div>
+
+	<issues-modal v-if='isModalActive'></issues-modal>
 </template>
 
 <script>
 	import { Toast } from 'vue-roaster';
+	import io from '../../io';
+
+	import IssuesModal from '../Modals/IssuesModal.vue';
 
 	export default {
 		data() {
 			return {
-				reports: []
+				reports: [],
+				isModalActive: false
+			}
+		},
+		methods: {
+			init: function() {
+				this.socket.emit('apis.joinAdminRoom', 'reports', data => {});
+			},
+			toggleModal: function (report) {
+				this.isModalActive = !this.isModalActive;
+				if (this.isModalActive) this.editing = report;
+			},
+			resolve: function (reportId) {
+				this.socket.emit('reports.resolve', reportId, res => {
+					Toast.methods.addToast(res.message, 3000);
+				});
 			}
 		},
-		methods: {},
 		ready: function () {
 			let _this = this;
-			let socketInterval = setInterval(() => {
-				if (!!_this.$parent.$parent.socket) {
-					_this.socket = _this.$parent.$parent.socket;
-					_this.socket.emit('reports.index', res => {
-						_this.reports = res.data;
+			io.getSocket((socket) => {
+				_this.socket = socket;
+				if (_this.socket.connected) _this.init();
+				_this.socket.emit('reports.index', res => {
+					_this.reports = res.data;
+				});
+				_this.socket.on('event:admin.report.resolved', reportId => {
+					_this.reports = _this.reports.filter(report => {
+						return report._id !== reportId;
 					});
-					clearInterval(socketInterval);
-				}
-			}, 100);
-		}
+				});
+				_this.socket.on('event:admin.report.created', report => {
+					_this.reports.push(report);
+				});
+				io.onConnect(() => {
+					_this.init();
+				});
+			});
+		},
+		components: { IssuesModal }
 	}
 </script>
 

+ 4 - 6
frontend/components/Admin/Stations.vue

@@ -114,7 +114,7 @@
 					genres,
 					blacklistedGenres,
 				}, result => {
-					// Toast
+					Toast.methods.addToast(result.message, 3000);
 				});
 			},
 			removeStation: function (index) {
@@ -138,7 +138,7 @@
 				else Toast.methods.addToast('Genre cannot be empty', 3000);
 			},
 			removeBlacklistedGenre: function (index) { this.newStation.blacklistedGenres.splice(index, 1); },
-			init: function() {
+			init: function () {
 				let _this = this;
 				_this.socket.emit('stations.index', data => {
 					_this.stations = data.stations;
@@ -150,14 +150,12 @@
 			let _this = this;
 			io.getSocket((socket) => {
 				_this.socket = socket;
-				if (_this.socket.connected) {
-					_this.init();
-				}
+				if (_this.socket.connected) _this.init();
 				_this.socket.on('event:admin.station.added', station => {
 					_this.stations.push(station);
 				});
 				_this.socket.on('event:admin.station.removed', stationId => {
-					_this.stations = _this.stations.filter(function(station) {
+					_this.stations = _this.stations.filter(station => {
 						return station._id !== stationId;
 					});
 				});

+ 3 - 3
frontend/components/MainFooter.vue

@@ -7,16 +7,16 @@
 				</p>
 				<p>
 					<a class='icon' href='https://github.com/Musare/MusareNode' title='GitHub Repository'>
-						<i class='fa fa-github'></i>
+						<img src='/assets/social/github.svg'/>
 					</a>
-					<a class='icon' href='https://twitter.com/MusareMusic' title='Twitter Account'>
+					<a class='icon' href='https://twitter.com/MusareApp' title='Twitter Account'>
 						<i class='fa fa-twitter'></i>
 					</a>
 					<a class='icon' href='https://www.facebook.com/MusareMusic/' title='Facebook Page'>
 						<i class='fa fa-facebook'></i>
 					</a>
 					<a class='icon' href='https://discord.gg/Y5NxYGP' title='Discord Server'>
-						<img src='/assets/discord.svg'/>
+						<img src='/assets/social/discord.svg'/>
 					</a>
 				</p>
 			</div>

+ 2 - 2
frontend/components/MainHeader.vue

@@ -14,7 +14,7 @@
 
 		<div class="nav-right nav-menu" :class="{ 'is-active': isMobile }">
 			<a class="nav-item is-tab admin" href="#" v-link="{ path: '/admin' }" v-if="$parent.$parent.role === 'admin'">
-				Admin
+				<strong>Admin</strong>
 			</a>
 			<a class="nav-item is-tab" href="#">
 				About
@@ -100,7 +100,7 @@
 			}
 		}
 		.admin {
-			color: $purple;
+			color: #424242;
 		}
 	}
 	.grouped {

+ 42 - 1
frontend/components/Modals/AddSongToQueue.vue

@@ -7,6 +7,18 @@
 				<button class="delete" @click="$parent.toggleModal('addSongToQueue')" ></button>
 			</header>
 			<section class="modal-card-body">
+				<aside class='menu' v-if='$parent.$parent.loggedIn'>
+					<ul class='menu-list'>
+						<li v-for='playlist in playlists' track-by='$index'>
+							<a :href='' target='_blank' @click='$parent.editPlaylist(playlist._id)'>{{ playlist.displayName }}</a>
+							<div class='controls'>
+								<a href='#' @click='selectPlaylist(playlist._id)' v-if="!isPlaylistSelected(playlist._id)"><i class='material-icons'>panorama_fish_eye</i></a>
+								<a href='#' @click='unSelectPlaylist()' v-if="isPlaylistSelected(playlist._id)"><i class='material-icons'>lens</i></a>
+							</div>
+						</li>
+					</ul>
+					<br />
+				</aside>
 				<div class="control is-grouped">
 					<p class="control is-expanded">
 						<input class="input" type="text" placeholder="YouTube Query" v-model="querySearch">
@@ -45,10 +57,30 @@
 		data() {
 			return {
 				querySearch: '',
-				queryResults: []
+				queryResults: [],
+				playlists: [],
+				privatePlaylistQueueSelected: null
 			}
 		},
 		methods: {
+			isPlaylistSelected: function(playlistId) {
+				console.log(this.privatePlaylistQueueSelected === playlistId);
+				return this.privatePlaylistQueueSelected === playlistId;
+			},
+			selectPlaylist: function (playlistId) {
+				let _this = this;
+				if (_this.$parent.type === 'community') {
+					_this.privatePlaylistQueueSelected = playlistId;
+					_this.$parent.privatePlaylistQueueSelected = playlistId;
+				}
+			},
+			unSelectPlaylist: function () {
+				let _this = this;
+				if (_this.$parent.type === 'community') {
+					_this.privatePlaylistQueueSelected = null;
+					_this.$parent.privatePlaylistQueueSelected = null;
+				}
+			},
 			addSongToQueue: function (songId) {
 				let _this = this;
 				if (_this.$parent.type === 'community') {
@@ -77,6 +109,11 @@
 					query.pop();
 					query = query.join('');
 				}
+				if (query.indexOf('&list=') !== -1) {
+					query = query.split('&list=');
+					query.pop();
+					query = query.join('');
+				}
 				_this.socket.emit('apis.searchYoutube', query, results => {
 					results = results.data;
 					_this.queryResults = [];
@@ -95,6 +132,10 @@
 			let _this = this;
 			io.getSocket((socket) => {
 				_this.socket = socket;
+				_this.socket.emit('playlists.indexForUser', res => {
+					if (res.status === 'success') _this.playlists = res.data;
+				});
+				_this.privatePlaylistQueueSelected = _this.$parent.privatePlaylistQueueSelected;
 			});
 		},
 		events: {

+ 41 - 0
frontend/components/Modals/IssuesModal.vue

@@ -0,0 +1,41 @@
+<template>
+	<div class='modal is-active'>
+		<div class='modal-background'></div>
+		<div class='modal-card'>
+			<header class='modal-card-head'>
+				<p class='modal-card-title'>Report Issues</p>
+				<button class='delete' @click='$parent.toggleModal()'></button>
+			</header>
+			<section class='modal-card-body'>
+
+				<table class='table is-narrow'>
+					<thead>
+						<tr>
+							<td>Issue</td>
+							<td>Reasons</td>
+						</tr>
+					</thead>
+					<tbody>
+						<tr v-for='(index, issue) in $parent.editing.issues' track-by='$index'>
+							<td>
+								<span>{{ issue.name }}</span>
+							</td>
+							<td>
+								<span>{{ issue.reasons }}</span>
+							</td>
+						</tr>
+					</tbody>
+				</table>
+
+			</section>
+			<footer class='modal-card-foot'>
+				<a class='button is-primary' @click='$parent.resolve($parent.editing._id)'>
+					<span>Resolve</span>
+				</a>
+				<a class='button is-danger' @click='$parent.toggleModal()'>
+					<span>Cancel</span>
+				</a>
+			</footer>
+		</div>
+	</div>
+</template>

+ 5 - 1
frontend/components/Modals/Login.vue

@@ -21,7 +21,9 @@
 			<footer class='modal-card-foot'>
 				<a class='button is-primary' @click='submitModal("login")'>Submit</a>
 				<a class='button is-github' :href='$parent.serverDomain + "/auth/github/authorize"'>
-					<i class='fa fa-github' aria-hidden='true'></i>
+					<div class='icon'>
+						<img class='invert' src='/assets/social/github.svg'/>
+					</div>
 					&nbsp;&nbsp;Login with GitHub
 				</a>
 			</footer>
@@ -53,4 +55,6 @@
 		background-color: #333 !important;
 		color: #fff !important;
 	}
+
+	.invert { filter: brightness(5); }
 </style>

+ 50 - 18
frontend/components/Modals/Playlists/Edit.vue

@@ -49,14 +49,14 @@
 						</tr>
 					</tbody>
 				</table>
-				<!--div class='control is-grouped'>
+				<div class='control is-grouped'>
 					<p class='control is-expanded'>
 						<input class='input' type='text' placeholder='YouTube Playlist URL' v-model='importQuery'>
 					</p>
 					<p class='control'>
 						<a class='button is-info' @click='importPlaylist()'>Import</a>
 					</p>
-				</div-->
+				</div>
 				<h5>Edit playlist details:</h5>
 				<div class='control is-grouped'>
 					<p class='control is-expanded'>
@@ -82,13 +82,26 @@
 		data() {
 			return {
 				playlist: {},
-				songQueryResults: []
+				songQueryResults: [],
+				songQuery: '',
+				importQuery: ''
 			}
 		},
 		methods: {
 			searchForSongs: function () {
 				let _this = this;
-				_this.socket.emit('apis.searchYoutube', _this.songQuery, res => {
+				let query = _this.songQuery;
+				if (query.indexOf('&index=') !== -1) {
+					query = query.split('&index=');
+					query.pop();
+					query = query.join('');
+				}
+				if (query.indexOf('&list=') !== -1) {
+					query = query.split('&list=');
+					query.pop();
+					query = query.join('');
+				}
+				_this.socket.emit('apis.searchYoutube', query, res => {
 					if (res.status == 'success') {
 						_this.songQueryResults = [];
 						for (let i = 0; i < res.data.items.length; i++) {
@@ -99,40 +112,39 @@
 								thumbnail: res.data.items[i].snippet.thumbnails.default.url
 							});
 						}
-					} else if (res.status == 'error') Toast.methods.addToast(res.message, 3000);
+						Toast.methods.addToast(res.message, 3000);
+					} else if (res.status === 'error') Toast.methods.addToast(res.message, 3000);
 				});
 			},
 			addSongToPlaylist: function (id) {
 				let _this = this;
 				_this.socket.emit('playlists.addSongToPlaylist', id, _this.playlist._id, res => {
-					if (res.status == 'success') {
-						Toast.methods.addToast(res.message, 3000);
-					}
+					Toast.methods.addToast(res.message, 4000);
 				});
 			},
-			/*importPlaylist: function () {
+			importPlaylist: function () {
 				let _this = this;
+				Toast.methods.addToast('Starting to import your playlist. This can take some time to do.', 4000);
 				this.socket.emit('playlists.addSetToPlaylist', _this.importQuery, _this.playlist._id, res => {
-					if (res.status == 'success') _this.playlist.songs = res.data;
+					if (res.status === 'success') _this.playlist.songs = res.data;
+					Toast.methods.addToast(res.message, 4000);
 				});
-			},*/
+			},
 			removeSongFromPlaylist: function (id) {
 				let _this = this;
 				this.socket.emit('playlists.removeSongFromPlaylist', id, _this.playlist._id, res => {
-					if (res.status == 'success') {
-						Toast.methods.addToast(res.message, 3000);
-					}
+					Toast.methods.addToast(res.message, 4000);
 				});
 			},
 			renamePlaylist: function () {
 				this.socket.emit('playlists.updateDisplayName', this.playlist._id, this.playlist.displayName, res => {
-					if (res.status == 'success') Toast.methods.addToast(res.message, 3000);
+					Toast.methods.addToast(res.message, 4000);
 				});
 			},
 			removePlaylist: function () {
 				let _this = this;
 				_this.socket.emit('playlists.remove', _this.playlist._id, res => {
-					if (res.status == 'success') {
+					if (res.status === 'success') {
 						Toast.methods.addToast(res.message, 3000);
 						_this.$parent.toggleModal('editPlaylist');
 					}
@@ -141,13 +153,13 @@
 			promoteSong: function (fromIndex) {
 				let _this = this;
 				_this.socket.emit('playlists.promoteSong', _this.playlist._id, fromIndex, res => {
-					if (res.status == 'success') _this.$set('playlist.songs', res.data); // bug: v-for is not updating
+					if (res.status === 'success') _this.$set('playlist.songs', res.data); // bug: v-for is not updating
 				});
 			},
 			demoteSong: function (fromIndex) {
 				let _this = this;
 				_this.socket.emit('playlists.demoteSong', _this.playlist._id, fromIndex, res => {
-					if (res.status == 'success') _this.$set('playlist.songs', res.data); // bug: v-for is not updating
+					if (res.status === 'success') _this.$set('playlist.songs', res.data); // bug: v-for is not updating
 				});
 			}*/
 		},
@@ -171,6 +183,26 @@
 				_this.socket.on('event:playlist.updateDisplayName', (data) => {
 					if (_this.playlist._id === data.playlistId) _this.playlist.displayName = data.displayName;
 				});
+				_this.socket.on('event:playlist.moveSongToBottom', (data) => {
+					if (_this.playlist._id === data.playlistId) {
+						let songIndex;
+						_this.playlist.songs.forEach((song, index) => {
+							if (song._id === data.songId) songIndex = index;
+						});
+						let song = _this.playlist.songs.splice(songIndex, 1)[0];
+						_this.playlist.songs.push(song);
+					}
+				});
+				_this.socket.on('event:playlist.moveSongToTop', (data) => {
+					if (_this.playlist._id === data.playlistId) {
+						let songIndex;
+						_this.playlist.songs.forEach((song, index) => {
+							if (song._id === data.songId) songIndex = index;
+						});
+						let song = _this.playlist.songs.splice(songIndex, 1)[0];
+						_this.playlist.songs.unshift(song);
+					}
+				});
 			});
 		},
 		events: {

+ 5 - 1
frontend/components/Modals/Register.vue

@@ -26,7 +26,9 @@
 			<footer class='modal-card-foot'>
 				<a class='button is-primary' @click='submitModal()'>Submit</a>
 				<a class='button is-github' :href='$parent.serverDomain + "/auth/github/authorize"'>
-					<i class='fa fa-github' aria-hidden='true'></i>
+					<div class='icon'>
+						<img class='invert' src='/assets/social/github.svg'/>
+					</div>
 					&nbsp;&nbsp;Register with GitHub
 				</a>
 			</footer>
@@ -74,4 +76,6 @@
 		background-color: #333 !important;
 		color: #fff !important;
 	}
+
+	.invert { filter: brightness(5); }
 </style>

+ 7 - 6
frontend/components/Modals/Report.vue

@@ -8,7 +8,7 @@
 			</header>
 			<section class='modal-card-body'>
 				<div class='columns song-types'>
-					<div class='column song-type' v-if='$parent.previousSong !== null && $parent.previousSong.likes !== -1 && $parent.previousSong.dislikes !== -1'>
+					<div class='column song-type' v-if='$parent.previousSong !== null'>
 						<div class='card is-fullwidth' :class="{ 'is-highlight-active': isPreviousSongActive }" @click="highlight('previousSong')">
 							<header class='card-header'>
 								<p class='card-header-title'>
@@ -35,7 +35,7 @@
 							</div>
 						</div>
 					</div>
-					<div class='column song-type' v-if='$parent.currentSong !== null && $parent.currentSong.likes !== -1 && $parent.currentSong.dislikes !== -1'>
+					<div class='column song-type' v-if='$parent.currentSong !== {}'>
 						<div class='card is-fullwidth'  :class="{ 'is-highlight-active': isCurrentSongActive }" @click="highlight('currentSong')">
 							<header class='card-header'>
 								<p class='card-header-title'>
@@ -63,8 +63,7 @@
 						</div>
 					</div>
 				</div>
-				<h4 v-if='($parent.currentSong === null || ($parent.currentSong.likes === -1 && $parent.currentSong.dislikes === -1)) && ($parent.previousSong === null || ($parent.previousSong.likes === -1 || $parent.previousSong.dislikes === -1))'>There are currently no songs to report.</h4>
-				<div class='edit-report-wrapper' v-else>
+				<div class='edit-report-wrapper'>
 					<div class='columns is-multiline'>
 						<div class='column is-half' v-for='issue in issues'>
 							<label class='label'>{{ issue.name }}</label>
@@ -84,7 +83,7 @@
 				</div>
 			</section>
 			<footer class='modal-card-foot'>
-				<a class='button is-success' @click='create()' v-if='!(($parent.currentSong === null || ($parent.currentSong.likes === -1 && $parent.currentSong.dislikes === -1)) && ($parent.previousSong === null || ($parent.previousSong.likes === -1 || $parent.previousSong.dislikes === -1)))'>
+				<a class='button is-success' @click='create()'>
 					<i class='material-icons save-changes'>done</i>
 					<span>&nbsp;Create</span>
 				</a>
@@ -163,8 +162,10 @@
 		},
 		methods: {
 			create: function () {
-				this.socket.emit('reports.create', this.report, res => {
+				let _this = this;
+				_this.socket.emit('reports.create', _this.report, res => {
 					Toast.methods.addToast(res.message, 4000);
+					if (res.status == 'success') _this.$parent.modals.report = !_this.$parent.modals.report;
 				});
 			},
 			updateCharactersRemaining: function () {

+ 0 - 11
frontend/components/Sidebars/Playlist.vue

@@ -147,16 +147,5 @@
 		&:active, &:focus { border: 0; }
 	}
 
-	.menu { padding: 0 20px; }
-
-	.menu-list li a:hover { color: #000 !important; }
-
-	.menu-list li {
-		display: flex;
-		justify-content: space-between;
-	}
-
-	.icons-group { display: flex; }
-
 	.none-found { text-align: center; }
 </style>

+ 4 - 5
frontend/components/Station/CommunityHeader.vue

@@ -9,7 +9,7 @@
 					<i class='material-icons'>settings</i>
 				</span>
 			</a>
-			<a v-if='$parent.$parent.loggedIn' class='nav-item' href='#' @click='$parent.modals.report = !$parent.modals.report'>
+			<a v-if='$parent.$parent.loggedIn && !$parent.noSong && !$parent.simpleSong' class='nav-item' href='#' @click='$parent.modals.report = !$parent.modals.report'>
 				<span class='icon'>
 					<i class='material-icons'>report</i>
 				</span>
@@ -19,7 +19,7 @@
 					<i class='material-icons'>skip_next</i>
 				</span>
 			</a>
-			<a v-if='!isOwner() && $parent.$parent.loggedIn && $parent.currentSong' class='nav-item' href='#' @click='$parent.voteSkipStation()'>
+			<a v-if='!isOwner() && $parent.$parent.loggedIn && !$parent.noSong' class='nav-item' href='#' @click='$parent.voteSkipStation()'>
 				<span class='icon'>
 					<i class='material-icons'>skip_next</i>
 				</span>
@@ -63,7 +63,7 @@
 					<i class='material-icons'>people</i>
 				</span>
 			</a>-->
-			<a class='nav-item' href='#' @click='$parent.sidebars.playlist = !$parent.sidebars.playlist'>
+			<a class='nav-item' href='#' @click='$parent.sidebars.playlist = !$parent.sidebars.playlist' v-if='$parent.$parent.loggedIn'>
 				<span class='icon'>
 					<i class='material-icons'>library_music</i>
 				</span>
@@ -82,7 +82,7 @@
 		},
 		methods: {
 			isOwner: function () {
-				return this.$parent.$parent.role === 'admin' || this.$parent.$parent.userId === this.$parent.station.owner
+				return this.$parent.$parent.loggedIn && (this.$parent.$parent.role === 'admin' || this.$parent.$parent.userId === this.$parent.station.owner);
 			}
 		}
 	}
@@ -132,7 +132,6 @@
 	.nav-center {
 		display: flex;
     	align-items: center;
-		text-transform: uppercase;
 		color: $blue;
 		font-size: 22px;
 	}

+ 10 - 6
frontend/components/Station/OfficialHeader.vue

@@ -9,12 +9,12 @@
 					<i class='material-icons'>settings</i>
 				</span>
 			</a>
-			<a class='nav-item' href='#' @click='$parent.toggleModal("addSongToQueue")' v-if='$parent.type === "official"'>
+			<a class='nav-item' href='#' @click='$parent.toggleModal("addSongToQueue")' v-if='$parent.type === "official" && $parent.$parent.loggedIn'>
 				<span class='icon'>
 					<i class='material-icons'>queue_music</i>
 				</span>
 			</a>
-			<a v-if='$parent.$parent.loggedIn' class='nav-item' href='#' @click='$parent.modals.report = !$parent.modals.report'>
+			<a v-if='$parent.$parent.loggedIn && !$parent.noSong && !$parent.simpleSong' class='nav-item' href='#' @click='$parent.modals.report = !$parent.modals.report'>
 				<span class='icon'>
 					<i class='material-icons'>report</i>
 				</span>
@@ -24,7 +24,7 @@
 					<i class='material-icons'>skip_next</i>
 				</span>
 			</a>
-			<a v-if='!isOwner() && $parent.$parent.loggedIn && $parent.currentSong' class='nav-item' href='#' @click='$parent.voteSkipStation()'>
+			<a v-if='!isOwner() && $parent.$parent.loggedIn && !$parent.noSong' class='nav-item' href='#' @click='$parent.voteSkipStation()'>
 				<span class='icon'>
 					<i class='material-icons'>skip_next</i>
 				</span>
@@ -43,7 +43,7 @@
 		</div>
 
 		<div class='nav-center stationDisplayName'>
-			{{$parent.station.displayName}}
+			{{ $parent.station.displayName }}
 		</div>
 
 		<span class="nav-toggle" :class="{ 'is-active': isMobile }" @click="isMobile = !isMobile">
@@ -87,7 +87,7 @@
 		},
 		methods: {
 			isOwner: function () {
-				return this.$parent.$parent.role === 'admin' || this.$parent.$parent.userId === this.$parent.station.owner
+				return this.$parent.$parent.loggedIn && this.$parent.$parent.role === 'admin';
 			}
 		}
 	}
@@ -118,6 +118,11 @@
 		}
 	}
 
+	.skip-votes {
+		position: relative;
+		left: 11px;
+	}
+
 	.nav-toggle {
 		height: 64px;
 	}
@@ -132,7 +137,6 @@
 	.nav-center {
 		display: flex;
     	align-items: center;
-		text-transform: uppercase;
 		color: $blue;
 		font-size: 22px;
 	}

+ 38 - 14
frontend/components/Station/Station.vue

@@ -40,25 +40,27 @@
 						<h4 class="thin" style="margin-left: 0">{{currentSong.artists}}</h4>
 						<div class="columns is-mobile">
 							<form style="margin-top: 12px; margin-bottom: 0;" action="#" class="column is-7-desktop is-4-mobile">
-								<p style="margin-top: 0; position: relative;">
+								<p style="margin-top: 0; position: relative; display: flex;">
+									<i class="material-icons">volume_down</i>
 									<input type="range" id="volumeSlider" min="0" max="100" class="active" v-on:change="changeVolume()" v-on:input="changeVolume()">
+									<i class="material-icons">volume_up</i>
 								</p>
 							</form>
 							<div class="column is-8-mobile is-5-desktop" style="float: right;">
 								<ul id="ratings" v-if="currentSong.likes !== -1 && currentSong.dislikes !== -1">
 									<li id="like" class="right" @click="toggleLike()">
 										<span class="flow-text">{{currentSong.likes}} </span>
-										<i id="thumbs_up" class="material-icons grey-text" v-bind:class="{liked: liked}">thumb_up</i>
+										<i id="thumbs_up" class="material-icons grey-text" v-bind:class="{ liked: liked }">thumb_up</i>
 									</li>
 									<li style="margin-right: 10px;" id="dislike" class="right" @click="toggleDislike()">
 										<span class="flow-text">{{currentSong.dislikes}} </span>
-										<i id="thumbs_down" class="material-icons grey-text" v-bind:class="{disliked: disliked}">thumb_down</i>
+										<i id="thumbs_down" class="material-icons grey-text" v-bind:class="{ disliked: disliked }">thumb_down</i>
 									</li>
 								</ul>
 							</div>
 						</div>
 					</div>
-					<div class="column is-4-desktop is-12-mobile" v-if="!simpleSong">
+					<div class="column is-4-desktop" v-if="!simpleSong">
 						<img class="image" id="song-thumbnail" style="margin-top: 10px !important" :src="currentSong.thumbnail" alt="Song Thumbnail" onerror="this.src='/assets/notes-transparent.png'" />
 					</div>
 				</div>
@@ -114,7 +116,8 @@
 				queue: [],
 				timeBeforePause: 0,
 				station: {},
-				skipVotes: 0
+				skipVotes: 0,
+				privatePlaylistQueueSelected: null
 			}
 		},
 		methods: {
@@ -318,8 +321,6 @@
 								if (data.status === 'success') _this.queue = data.queue;
 							});
 						}
-					} else {
-						//TODO Handle error
 					}
 				});
 			}
@@ -331,16 +332,21 @@
 
 			io.getSocket((socket) => {
 				_this.socket = socket;
-				io.removeAllListeners();
 
-				if (_this.socket.connected) {
-					_this.joinStation();
-				}
+				io.removeAllListeners();
 
+				if (_this.socket.connected) _this.joinStation();
 				io.onConnect(() => {
 					_this.joinStation();
 				});
 
+				_this.socket.emit('stations.find', _this.stationId, res => {
+					if (res.status === 'error') {
+						_this.$router.go('/404');
+						Toast.methods.addToast(res.message, 3000);
+					}
+				});
+
 				_this.socket.on('event:songs.next', data => {
 					_this.previousSong = (_this.currentSong._id) ? _this.currentSong : null;
 					_this.currentSong = (data.currentSong) ? data.currentSong : {};
@@ -350,9 +356,7 @@
 					if (data.currentSong) {
 						_this.noSong = false;
 						_this.simpleSong = (data.currentSong.likes === -1 && data.currentSong.dislikes === -1);
-						if (_this.simpleSong) {
-							_this.currentSong.skipDuration = 0;
-						}
+						if (_this.simpleSong) _this.currentSong.skipDuration = 0;
 						if (!_this.playerReady) _this.youtubeReady();
 						else _this.playVideo();
 						_this.socket.emit('songs.getOwnSongRatings', data.currentSong._id, (data) => {
@@ -461,6 +465,11 @@
 		text-align: center;
 	}
 
+	#volumeSlider {
+		padding: 0 15px;
+    	background: transparent;
+	}
+
 	.stationDisplayName {
 		color: white !important;
 	}
@@ -742,4 +751,19 @@
 	.btn-search {
 		font-size: 14px;
 	}
+
+	.menu { padding: 0 10px; }
+
+	.menu-list li a:hover { color: #000 !important; }
+
+	.menu-list li {
+		display: flex;
+		justify-content: space-between;
+	}
+
+	.menu-list a {
+		padding: 0 10px !important;
+	}
+
+	.icons-group { display: flex; }
 </style>

+ 6 - 1
frontend/components/pages/Admin.vue

@@ -61,4 +61,9 @@
 	}
 </script>
 
-<style lang='scss' scoped></style>
+<style lang='scss' scoped>
+	.is-active a {
+		color: #03a9f4 !important;
+		border-color: #03a9f4 !important;
+	}
+</style>

+ 7 - 9
frontend/components/pages/Home.vue

@@ -80,14 +80,12 @@
 			auth.getStatus((authenticated, role, username, userId) => {
 				io.getSocket((socket) => {
 					_this.socket = socket;
-					if (_this.socket.connected) {
-						_this.init();
-					}
+					if (_this.socket.connected) _this.init();
 					io.onConnect(() => {
 						_this.init();
 					});
 					_this.socket.on('event:stations.created', station => {
-						if (!station.currentSong) station.currentSong = {thumbnail: '/assets/notes-transparent.png'};
+						if (!station.currentSong) station.currentSong = { thumbnail: '/assets/notes-transparent.png' };
 						if (station.privacy !== 'public') {
 							station.class = {'station-red': true}
 						} else if (station.type === 'community') {
@@ -107,16 +105,16 @@
 			init: function() {
 				let _this = this;
 				auth.getStatus((authenticated, role, username, userId) => {
-					_this.socket.emit("stations.index", data => {
+					_this.socket.emit('stations.index', data => {
 						_this.stations.community = [];
 						_this.stations.official = [];
-						if (data.status === "success")  data.stations.forEach(station => {
-							if (!station.currentSong) station.currentSong = {thumbnail: '/assets/notes-transparent.png'};
+						if (data.status === "success") data.stations.forEach(station => {
+							if (!station.currentSong) station.currentSong = { thumbnail: '/assets/notes-transparent.png' };
 							if (station.privacy !== 'public') {
-								station.class = {'station-red': true}
+								station.class = { 'station-red': true }
 							} else if (station.type === 'community') {
 								if (station.owner === userId) {
-									station.class = {'station-blue': true}
+									station.class = { 'station-blue': true }
 								}
 							}
 							if (station.type == 'official') _this.stations.official.push(station);

+ 1 - 0
frontend/package.json

@@ -28,6 +28,7 @@
     "vue-loader": "^8.5.2",
     "vue-style-loader": "^1.0.0",
     "whatwg-fetch": "^0.11.1",
+		"webpack": "^1.14.0",
     "webpack-dev-server": "^1.15.1"
   },
   "dependencies": {

+ 2 - 0
frontend/webpack.config.js

@@ -1,3 +1,5 @@
+const webpack = require('webpack');
+
 module.exports = {
 	entry: './main.js',
 	output: {