Просмотр исходного кода

Added PlayerDebug, split QueueList component, converted old system to ducks more.

KrisVos130 7 лет назад
Родитель
Сommit
1a0cc15cd4

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

@@ -147,6 +147,12 @@ cache.sub('station.resume', stationId => {
 
 cache.sub('station.queueUpdate', stationId => {
 	stations.getStation(stationId, (err, station) => {
+		station.queue = station.queue.map((song) => {
+			let username = utils.getUsernameFromUserId(song.requestedBy);
+			if (username === null) username = "Unknown";
+			song.requestedByUsername = username;
+			return song;
+		});
 		if (!err) utils.emitToRoom(`station.${stationId}`, "event:queue.update", station.queue);
 	});
 });
@@ -1063,6 +1069,16 @@ module.exports = {
 					if (canBe) return next(null, station);
 					return next('Insufficient permissions.');
 				});
+			},
+
+			(station, next) => {
+				station.queue = station.queue.map((song) => {
+					let username = utils.getUsernameFromUserId(song.requestedBy);
+					if (username === null) username = "Unknown";
+					song.requestedByUsername = username;
+					return song;
+				});
+				next(null, station);
 			}
 		], (err, station) => {
 			if (err) {

+ 14 - 0
backend/logic/utils.js

@@ -478,5 +478,19 @@ module.exports = {
 			if (err === true) return cb(true);
 			return cb(false);
 		});
+	},
+	getUsernameFromUserId: (userId) => {
+		async.waterfall([
+			(next) => {
+				if (!userId) return next(false);
+				db.models.user.findOne({_id: userId}, next);
+			}
+		], (err, user) => {
+			if (err === false) return null;
+			else if (err) {
+				return null;
+			}
+			return user.username;
+		});
 	}
 };

+ 4 - 2
frontend/app/js/components/AuthRoute.jsx

@@ -6,6 +6,7 @@ import { translate } from "react-i18next";
 import { initializeStation } from "actions/station";
 
 import { actionCreators as stationInfoActionCreators } from "ducks/stationInfo";
+import { actionCreators as stationCurrentSongActionCreators } from "ducks/stationCurrentSong";
 import { bindActionCreators } from "redux";
 
 import io from "io";
@@ -43,6 +44,7 @@ function clone(obj) {
 }),
 (dispatch) => ({
 	onJoinStation: bindActionCreators(stationInfoActionCreators.joinStation, dispatch),
+	onPauseTime: bindActionCreators(stationCurrentSongActionCreators.pauseTime, dispatch),
 }))
 
 @translate(["general"], { wait: true })
@@ -116,7 +118,6 @@ export default class AuthRoute extends Component {
 						type: res.data.type,
 						ownerId: res.data.owner,
 						paused: res.data.paused,
-						pausedAt: res.data.pausedAt,
 						// Mode
 						// Userlist
 							userList: [],
@@ -127,8 +128,9 @@ export default class AuthRoute extends Component {
 						songList: res.data.queue,
 						// locked: res.data.locked,
 						// partyMode: res.data.partyMode,
-						// privatePlaylist: res.data.privatePlaylist,
+						privatePlaylist: res.data.privatePlaylist,
 					});
+					this.props.onPauseTime(res.data.pausedAt);
 				} else {
 					this.setState({
 						noStation: true,

+ 36 - 1
frontend/app/js/ducks/stationCurrentSong.js

@@ -1,4 +1,21 @@
 import { Map, List } from "immutable";
+import store from "../index.js";
+
+function calculateTimeElapsed() {
+	const state = store.getState();
+	const
+		paused		=	state.station.info.get("paused"),
+		songId		=	state.station.currentSong.get("songId"),
+		pausedAt	=	state.station.currentSong.getIn(["timings", "pausedAt"]),
+		startedAt	=	state.station.currentSong.getIn(["timings", "startedAt"]),
+		timePaused	=	state.station.currentSong.getIn(["timings", "timePaused"]);
+
+	if (songId !== "") {
+		let timePausedNow = (paused) ? Date.now() - pausedAt : 0;
+		//TODO Fix this function. It's accurate, but sometimes if paused at the wrong moment (e.g. 10 seconds to the milisecond) the display can flicker between 9 and 10 seconds every .5s.
+		return (Date.now() - startedAt - timePaused - timePausedNow) / 1000;
+	} else return 0;
+}
 
 const NEXT_SONG = "STATION_CURRENT_SONG::NEXT_SONG";
 const LIKE_UPDATE = "STATION_CURRENT_SONG::LIKE_UPDATE";
@@ -7,6 +24,7 @@ const LIKED_UPDATE = "STATION_CURRENT_SONG::LIKED_UPDATE";
 const DISLIKED_UPDATE = "STATION_CURRENT_SONG::DISLIKED_UPDATE";
 const PAUSE_TIME = "STATION_CURRENT_SONG::PAUSE_TIME";
 const RESUME_TIME = "STATION_CURRENT_SONG::RESUME_TIME";
+const TIME_ELAPSED_UPDATE = "STATION_CURRENT_SONG::TIME_ELAPSED_UPDATE";
 
 function nextSong(song) {
 	return {
@@ -52,11 +70,18 @@ function pauseTime(pausedAt) {
 
 function resumeTime(timePaused) {
 	return {
-		type: PAUSE_TIME,
+		type: RESUME_TIME,
 		timePaused,
 	}
 }
 
+function timeElapsedUpdate() {
+	return {
+		type: TIME_ELAPSED_UPDATE,
+		timeElapsed: calculateTimeElapsed(),
+	}
+}
+
 
 
 const initialState = Map({
@@ -71,6 +96,8 @@ const initialState = Map({
 	}),
 	"title": "",
 	"artists": [],
+	"thumbnail": "",
+	"playlists": List([]),
 	"ratings": Map({
 		"enabled": false,
 		"likes": 0,
@@ -86,6 +113,7 @@ function reducer(state = initialState, action) {
 		const { song } = action;
 		if (song === null) return initialState;
 		//TODO Handle no song event / Song being null event (so no song)
+		const previousState = state;
 		state = initialState;
 
 		return state.merge({
@@ -94,11 +122,13 @@ function reducer(state = initialState, action) {
 				duration: song.timings.duration,
 				skipDuration: song.timings.skipDuration,
 				timeElapsed: 0,
+				pausedAt: previousState.getIn(["timings", "pausedAt"]),
 				timePaused: song.timings.timePaused,
 				startedAt: song.timings.startedAt,
 			}),
 			title: song.title,
 			artists: List(song.artists),
+			thumbnail: song.thumbnail,
 			ratings: Map({
 				enabled: !(song.ratings.likes === -1 && song.ratings.dislikes === -1),
 				likes: song.ratings.likes,
@@ -131,6 +161,9 @@ function reducer(state = initialState, action) {
 		const { timePaused } = action;
 		state = state.setIn(["timings", "timePaused"], timePaused);
 		return state;
+	case TIME_ELAPSED_UPDATE:
+		const { timeElapsed } = action;
+		return state.setIn(["timings", "timeElapsed"], timeElapsed);
 	}
 	return state;
 }
@@ -143,6 +176,7 @@ const actionCreators = {
 	dislikedUpdate,
 	pauseTime,
 	resumeTime,
+	timeElapsedUpdate,
 };
 
 const actionTypes = {
@@ -153,6 +187,7 @@ const actionTypes = {
 	DISLIKED_UPDATE,
 	PAUSE_TIME,
 	RESUME_TIME,
+	TIME_ELAPSED_UPDATE,
 };
 
 export {

+ 60 - 58
frontend/app/js/ducks/stationInfo.js

@@ -1,7 +1,7 @@
 import { Map, List } from "immutable";
 
-const JOIN_STATION = "STATION_INFO::JOIN_STATION";
-const LEAVE_STATION = "STATION_INFO::LEAVE_STATION";
+const JOIN = "STATION_INFO::JOIN";
+const LEAVE = "STATION_INFO::LEAVE";
 const USER_LIST_UPDATE = "STATION_INFO::USER_LIST_UPDATE";
 const USER_COUNT_UPDATE = "STATION_INFO::USER_COUNT_UPDATE";
 const NAME_UPDATE = "STATION_INFO::NAME_UPDATE";
@@ -10,17 +10,19 @@ const DESCRIPTION_UPDATE = "STATION_INFO::DESCRIPTION_UPDATE";
 const MODE_UPDATE = "STATION_INFO::MODE_UPDATE";
 const QUEUE_INDEX = "STATION_INFO::QUEUE_INDEX";
 const QUEUE_UPDATE = "STATION_INFO::QUEUE_UPDATE";
+const PAUSE = "STATION_INFO::PAUSE";
+const RESUME = "STATION_INFO::RESUME";
 
 function joinStation(station) {
 	return {
-		type: JOIN_STATION,
+		type: JOIN,
 		station,
 	}
 }
 
 function leaveStation() {
 	return {
-		type: LEAVE_STATION,
+		type: LEAVE,
 	}
 }
 
@@ -80,6 +82,18 @@ function queueUpdate(songList) {
 	}
 }
 
+function pause() {
+	return {
+		type: PAUSE,
+	}
+}
+
+function resume() {
+	return {
+		type: RESUME,
+	}
+}
+
 
 
 const initialState = Map({
@@ -95,6 +109,7 @@ const initialState = Map({
 	"userList": List([]),
 	"userCount": 0,
 	"songList": List([]),
+	"privatePlaylist": "",
 });
 
 function reducer(state = initialState, action) {
@@ -116,23 +131,12 @@ function reducer(state = initialState, action) {
 	}
 
 	switch (action.type) {
-	case JOIN_STATION:
+	case JOIN:
 		const { stationId, privacy, type, ownerId, paused } = action.station;
 		name = action.station.name;
 		displayName = action.station.displayName;
 		description = action.station.description;
 		userCount = action.station.userCount;
-		mode = (getModeTemp(action.station.partyMode, action.station.locked));
-
-		userList = List([]);
-		action.station.userList.forEach((user) => {
-			userList.push(user);
-		});
-
-		songList = List([]);
-		action.station.songList.forEach((song) => {
-			songList.push(song);
-		});
 
 		return state.merge({
 			stationId,
@@ -143,60 +147,54 @@ function reducer(state = initialState, action) {
 			type,
 			ownerId,
 			paused,
-			mode,
-			userList,
+			mode: (getModeTemp(action.station.partyMode, action.station.locked)),
+			userList: List(action.station.userList),
 			userCount,
-			songList,
+			songList: List(action.station.songList),
+			privatePlaylist: action.station.privatePlaylist,
 		});
-	case LEAVE_STATION:
+	case LEAVE:
 		return initialState;
 	case USER_LIST_UPDATE:
-		userList = List([]);
-		action.userList.forEach((user) => {
-			userList.push(user);
+		return state.merge({
+			userList: List(action.userList),
 		});
-
-		state.set("userList", userList);
-		return state;
 	case USER_COUNT_UPDATE:
-		userCount = action.userCount;
-
-		state.set("userCount", userCount);
-		return state;
+		return state.merge({
+			userCount: action.userCount,
+		});
 	case NAME_UPDATE:
-		name = action.name;
-
-		state.set("name", name);
-		return state;
+		return state.merge({
+			name: action.name,
+		});
 	case DISPLAY_NAME_UPDATE:
-		displayName = action.displayName;
-
-		state.set("displayName", displayName);
-		return state;
+		return state.merge({
+			displayName: action.displayName,
+		});
 	case DESCRIPTION_UPDATE:
-		description = action.description;
-
-		state.set("description", description);
-		return state;
+		return state.merge({
+			description: action.description,
+		});
 	case MODE_UPDATE:
-		mode = action.mode;
-
-		state.set("mode", mode);
-		return state;
+		return state.merge({
+			mode: action.mode,
+		});
 	case QUEUE_INDEX:
-		songList = List([]);
-		action.songList.forEach((song) => {
-			songList.push(song);
+		return state.merge({
+			songList: List(action.songList),
 		});
-
-		return state;
 	case QUEUE_UPDATE:
-		songList = List([]);
-		action.songList.forEach((song) => {
-			songList.push(song);
+		return state.merge({
+			songList: List(action.songList),
+		});
+	case PAUSE:
+		return state.merge({
+			paused: true,
+		});
+	case RESUME:
+		return state.merge({
+			paused: false,
 		});
-
-		return state;
 	}
 	return state;
 }
@@ -212,11 +210,13 @@ const actionCreators = {
 	modeUpdate,
 	queueIndex,
 	queueUpdate,
+	pause,
+	resume,
 };
 
 const actionTypes = {
-	JOIN_STATION,
-	LEAVE_STATION,
+	JOIN_STATION: JOIN,
+	LEAVE_STATION: LEAVE,
 	USER_LIST_UPDATE,
 	USER_COUNT_UPDATE,
 	NAME_UPDATE,
@@ -225,6 +225,8 @@ const actionTypes = {
 	MODE_UPDATE,
 	QUEUE_INDEX,
 	QUEUE_UPDATE,
+	PAUSE,
+	RESUME,
 };
 
 export {

+ 2 - 0
frontend/app/js/index.js

@@ -33,3 +33,5 @@ ReactDOM.render(
 	</I18nextProvider>,
 	document.getElementById("root")
 );
+
+export default store;

+ 1 - 1
frontend/app/js/views/Station/Player.jsx

@@ -16,7 +16,7 @@ let getPlayerCallbacks = [];
 	startedAt: state.station.currentSong.getIn(["timings", "startedAt"]),
 	timePaused: state.station.currentSong.getIn(["timings", "timePaused"]),
 	skipDuration: state.station.currentSong.getIn(["timings", "skipDuration"]),
-	pausedAt: state.station.currentSong.getIn(["timings", "pausedAt"]),
+	pausedAt: state.station.info.get("pausedAt"),
 	exists: state.station.currentSong.get("songId") !== "",
 	paused: state.station.info.get("paused"),
 }))

+ 64 - 0
frontend/app/js/views/Station/PlayerDebug.jsx

@@ -0,0 +1,64 @@
+import React, { Component } from "react";
+import PropTypes from "prop-types";
+
+import { connect } from "react-redux";
+
+@connect(state => ({
+	volume: state.volume.get("loudness"),
+	muted: state.volume.get("muted"),
+	songId: state.station.currentSong.get("songId"),
+	duration: state.station.currentSong.getIn(["timings", "duration"]),
+	skipDuration: state.station.currentSong.getIn(["timings", "skipDuration"]),
+	timeElapsed: state.station.currentSong.getIn(["timings", "timeElapsed"]),
+	timePaused: state.station.currentSong.getIn(["timings", "timePaused"]),
+	pausedAt: state.station.currentSong.getIn(["timings", "pausedAt"]),
+	startedAt: state.station.currentSong.getIn(["timings", "startedAt"]),
+	exists: state.station.currentSong.get("songId") !== "",
+	paused: state.station.info.get("paused"),
+	mode: state.station.info.get("mode"),
+}))
+export default class PlayerDebug extends Component {
+	static propTypes = {
+	};
+
+	static defaultProps = {
+	};
+
+	constructor(props) {
+		super(props);
+	}
+
+	/*getTimeElapsed = () => {
+		if (this.props.exists) {
+			// TODO Replace with Date.currently
+			let timePausedNow = 0;
+			if (this.props.paused) timePausedNow = Date.now() - this.props.pausedAt;
+			return Date.now() - this.props.startedAt - this.props.timePaused - timePausedNow;
+		} else return 0;
+	};*/
+
+	render() {
+		return (
+			<div style={{border: "1px solid black"}}>
+				<h3>Volume</h3>
+				<b>Loudness: </b> { this.props.volume } <br/>
+				<b>Muted: </b> { this.props.muted.toString() } <br/>
+				<hr/>
+				<h3>Station info</h3>
+				<b>Paused: </b> { this.props.paused.toString() } <br/>
+				<b>Mode: </b> { this.props.mode } <br/>
+				<hr/>
+				<h3>Current song</h3>
+				<b>Song id: </b> { this.props.songId } <br/>
+				<b>Duration: </b> { this.props.duration } <br/>
+				<b>Skip duration: </b> { this.props.skipDuration } <br/>
+				<b>Time elapsed: </b> { this.props.timeElapsed } <br/>
+				<b>Time paused: </b> { this.props.timePaused } <br/>
+				<b>Paused at: </b> { this.props.pausedAt } <br/>
+				<b>Started at: </b> { this.props.startedAt } <br/>
+				<b>Exists: </b> { this.props.exists.toString() } <br/>
+				<hr/>
+			</div>
+		);
+	}
+}

+ 1 - 1
frontend/app/js/views/Station/Views/Overlays.jsx

@@ -7,7 +7,7 @@ import Settings from "./Settings";
 import Playlists from "./Playlists";
 import EditPlaylist from "./EditPlaylist";
 import SearchYouTube from "./SearchYouTube";
-import QueueList from "./QueueList";
+import QueueList from "./QueueList/index.jsx";
 
 @connect(state => ({
 	overlay1: state.stationOverlay.get("overlay1"),

+ 0 - 240
frontend/app/js/views/Station/Views/QueueList.jsx

@@ -1,240 +0,0 @@
-import React, { Component } from "react";
-import PropTypes from "prop-types";
-
-import CustomInput from "components/CustomInput.jsx";
-import CustomErrors from "components/CustomMessages.jsx";
-
-import { connect } from "react-redux";
-
-import { closeOverlay1, openOverlay2, closeOverlay2 } from "actions/stationOverlay";
-import { selectPlaylist, deselectPlaylists } from "actions/playlistQueue";
-
-import io from "io";
-
-@connect(state => ({
-	user: {
-		loggedIn: state.user.get("loggedIn"),
-		userId: state.user.get("userId"),
-		role: state.user.get("role"),
-	},
-	stationId: state.station.get("id"),
-	stationOwner: state.station.get("ownerId"),
-	songTitle: state.songPlayer.get("title"),
-	songArtists: state.songPlayer.get("artists"),
-	songDuration: state.songPlayer.get("duration"),
-	songThumbnail: state.songPlayer.get("thumbnail"),
-	simpleSong: state.songPlayer.get("simple"),
-	songExists: state.songPlayer.get("exists"),
-	playlistSelectedId: state.playlistQueue.get("playlistSelected"),
-}))
-export default class Settings extends Component {
-	constructor(props) {
-		super(props);
-
-		this.state = {
-			queue: [],
-			userIdMap: {},
-			userIdMapLoading: [],
-			playlists: [],
-		};
-
-		io.getSocket((socket) => {
-			socket.emit('stations.getQueue', this.props.stationId, data => {
-				if (data.status === 'success') {
-					this.setState({
-						queue: data.queue,
-					});
-					data.queue.forEach((song) => {
-						this.checkUserId(song.requestedBy);
-					});
-				}
-			});
-
-			socket.on('event:queue.update', queue => {
-				this.setState({
-					queue,
-				});
-				queue.forEach((song) => {
-					this.checkUserId(song.requestedBy);
-				});
-			});
-
-			socket.emit('playlists.indexForUser', res => {
-				if (res.status === 'success') this.setState({
-					playlists: res.data,
-				});
-			});
-
-			socket.on('event:playlist.create', () => {
-				socket.emit('playlists.indexForUser', res => {
-					if (res.status === 'success') this.setState({
-						playlists: res.data,
-					});
-				});
-			});
-			socket.on('event:playlist.delete', () => {
-				socket.emit('playlists.indexForUser', res => {
-					if (res.status === 'success') this.setState({
-						playlists: res.data,
-					});
-				});
-			});
-		});
-	}
-
-	isOwner = () => {
-		if (this.props.loggedIn) {
-			if (this.props.user.role === "admin") return true;
-			if (this.props.user.userId === this.props.stationOwner) return true;
-		}
-
-		return false;
-	};
-
-	deleteSong = (songId) => {
-		io.getSocket((socket) => {
-			socket.emit("stations.removeFromQueue", this.props.stationId, songId, (data) => {
-				if (data.status === "success") this.messages.clearAddSuccess("Successfully removed song.");
-				else this.messages.clearAddError("Failed to remove song.", data.message);
-			});
-		});
-	};
-
-	checkUserId = (userId) => {
-		if (!this.state.userIdMap[`Z${ userId }`] && !this.state.userIdMapLoading[`Z${ userId }`]) {
-			this.setState({
-				userIdMapLoading: this.state.userIdMapLoading.concat([`Z${ userId }`]),
-			});
-			io.getSocket((socket) => {
-				socket.emit("users.getUsernameFromId", userId, (data) => {
-					if (data.status === "success") {
-						this.setState({
-							userIdMap: {
-								[`Z${ userId }`]: data.data,
-							},
-						});
-					}
-				});
-			});
-		}
-	};
-
-	addSongToQueueCallback = (songId) => {
-		io.getSocket((socket) => {
-			// Add song to queue
-			socket.emit("stations.addToQueue", this.props.stationId, songId, res => {
-				if (res.status === "success") {
-					this.messages.clearAddSuccess("Successfully added song.");
-				} else {
-					this.messages.addError(res.message);
-				}
-				this.props.dispatch(closeOverlay2());
-			});
-		});
-	};
-
-	addSongToQueue = () => {
-		this.props.dispatch(openOverlay2("searchYouTube", null, this.addSongToQueueCallback));
-	};
-
-	getPlaylistAction = (playlistId) => {
-		if (playlistId === this.props.playlistSelectedId) {
-			return <span>SELECTED</span>;
-		} else return <span onClick={ () => { this.selectPlaylist(playlistId); } }>SELECT</span>;
-	}
-
-	selectPlaylist = (playlistId) => {
-		this.props.dispatch(selectPlaylist(playlistId));
-	}
-
-	deselectAll = () => {
-		this.props.dispatch(deselectPlaylists());
-	}
-
-	close = () => {
-		this.props.dispatch(closeOverlay1());
-	};
-
-	render() {
-		console.log(this.isOwner());
-
-		return (
-			<div className="overlay">
-				<button onClick={ this.close } className="back"><i className="material-icons">arrow_back</i></button>
-				<div className="content">
-					<h1>Queue</h1>
-					<CustomErrors onRef={ ref => (this.messages = ref) } />
-
-					{
-						(this.state.queue)
-						? (
-							<ul>
-								{
-									(this.props.songExists)
-									? (
-										<li>
-											<div className="left">
-												<img src={ this.props.songThumbnail } onError={function(e) {e.target.src="/assets/images/notes.png"}}/>
-											</div>
-											<div className="right">
-												<span className="duration">{ this.props.songDuration }</span>
-												<p className="title">{ this.props.songTitle }</p>
-												<span className="title-artists-spacing"/>
-												<p className="artists">{ this.props.songTitle }</p>
-											</div>
-										</li>
-									) : null
-								}
-								{
-									this.state.queue.map((song) => {
-										return (
-											<li key={ song.songId }>
-												<div className="left">
-													<img src={ song.thumbnail }/>
-												</div>
-												<div className="right">
-													<span className="duration">{ song.duration }</span>
-													<p className="title">{ song.title }</p>
-													<span className="title-artists-spacing"/>
-													<p className="artists">{ song.title }</p>
-													<span>
-														<span>Requested by: </span>
-														<a href={ `/u/${ this.state.userIdMap[`Z${ song.requestedBy }`] }` }>{ this.state.userIdMap[`Z${ song.requestedBy }`] }</a>
-													</span>
-													<i onClick={ () => { this.deleteSong(song.songId) } }>Delete</i>
-												</div>
-											</li>
-										);
-									})
-								}
-							</ul>
-						)
-						: null
-					}
-
-					<button onClick={ this.addSongToQueue }>Add song to queue</button>
-
-					<hr/>
-
-					<ul>
-						{
-							this.state.playlists.map((playlist) => {
-								return (
-									<li key={ playlist._id }>
-										{ playlist.displayName } - { this.getPlaylistAction(playlist._id) }
-									</li>
-								)
-							})
-						}
-					</ul>
-
-					{
-						(this.props.playlistSelectedId)
-							? <button onClick={ this.deselectAll }>Deselect all playlists</button>
-						: null
-					}
-				</div>
-			</div>
-		);
-	}
-}

+ 35 - 0
frontend/app/js/views/Station/Views/QueueList/PlaylistItem.jsx

@@ -0,0 +1,35 @@
+import React, { Component } from "react";
+import PropTypes from "prop-types";
+
+import { connect } from "react-redux";
+
+@connect(state => ({
+	station: {
+		playlistSelectedId: state.station.info.get("playlistSelected"),
+	},
+}))
+export default class PlaylistItem extends Component {
+	constructor(props) {
+		super(props);
+	}
+
+	selectPlaylist = (playlistId) => {
+		this.props.dispatch(selectPlaylist(playlistId));
+	}
+
+	getPlaylistAction = (playlistId) => {
+		if (playlistId === this.props.station.playlistSelectedId) {
+			return <span>SELECTED</span>;
+		} else return <span onClick={ () => { this.selectPlaylist(playlistId); } }>SELECT</span>;
+	}
+
+	render() {
+		const { playlist } = this.props;
+
+		return (
+			<li key={ playlist._id }>
+				{ playlist.displayName } - { this.getPlaylistAction(playlist._id) }
+			</li>
+		);
+	}
+}

+ 31 - 0
frontend/app/js/views/Station/Views/QueueList/PlaylistList.jsx

@@ -0,0 +1,31 @@
+import React, { Component } from "react";
+import PropTypes from "prop-types";
+
+import { connect } from "react-redux";
+
+import PlaylistItem from "./PlaylistItem.jsx";
+
+@connect(state => ({
+	station: {
+		playlists: state.station.info.get("playlists"),
+	},
+}))
+export default class PlaylistList extends Component {
+	constructor(props) {
+		super(props);
+	}
+
+	render() {
+		const { playlists } = this.props;
+
+		return (
+			<ul>
+				{
+					playlists.map((playlist) => {
+						return <PlaylistItem playlist={ playlist }/>;
+					})
+				}
+			</ul>
+		);
+	}
+}

+ 67 - 0
frontend/app/js/views/Station/Views/QueueList/SongItem.jsx

@@ -0,0 +1,67 @@
+import React, { Component } from "react";
+import PropTypes from "prop-types";
+
+import { connect } from "react-redux";
+
+import io from "io";
+
+@connect(state => ({
+	station: {
+		stationId: state.station.info.get("id"),
+	},
+}))
+export default class SongItem extends Component {
+	constructor(props) {
+		super(props);
+	}
+
+	deleteSong = (songId) => {
+		io.getSocket((socket) => {
+			socket.emit("stations.removeFromQueue", this.props.station.stationId, songId, (data) => {
+				if (data.status === "success") this.messages.clearAddSuccess("Successfully removed song.");
+				else this.messages.clearAddError("Failed to remove song.", data.message);
+			});
+		});
+	};
+
+	isOwner = () => {
+		if (this.props.loggedIn) {
+			if (this.props.user.role === "admin") return true;
+			if (this.props.user.userId === this.props.station.owner) return true;
+		}
+
+		return false;
+	};
+
+	render() {
+		const { song } = this.props;
+		const showRequestedBy = (song.requestedByUsername && song.requestedByUsername !== "Unknown");
+		const showDelete = (this.isOwner());
+
+		return (
+			<li>
+				<div className="left">
+					<img src={ song.thumbnail } onError={function(e) {e.target.src="/assets/images/notes.png"}}/>
+				</div>
+				<div className="right">
+					<span className="duration">{ song.duration }</span>
+					<p className="title">{ song.title }</p>
+					<span className="title-artists-spacing"/>
+					<p className="artists">{ song.artists.toJS().join(", ") }</p>
+					{
+						(showRequestedBy) ?
+						<span>
+							<span>Requested by: </span>
+							<a href={`/u/${ song.requestedByUsername }`}>{ song.requestedByUsername }</a>
+						</span> : null
+					}
+					{
+						(showDelete)
+						? <i onClick={ () => { this.deleteSong(song.songId) } }>Delete</i>
+						: null
+					}
+				</div>
+			</li>
+		);
+	}
+}

+ 31 - 0
frontend/app/js/views/Station/Views/QueueList/SongList.jsx

@@ -0,0 +1,31 @@
+import React, { Component } from "react";
+import PropTypes from "prop-types";
+
+import { connect } from "react-redux";
+
+import SongItem from "./SongItem.jsx";
+
+@connect(state => ({
+	station: {
+		songList: state.station.info.get("songList"),
+	},
+}))
+export default class SongList extends Component {
+	constructor(props) {
+		super(props);
+	}
+
+	render() {
+		const { songList } = this.props.station;
+
+		return (
+			<ul>
+				{
+					songList.map((song) => {
+						return <SongItem song={ song }/>;
+					})
+				}
+			</ul>
+		);
+	}
+}

+ 116 - 0
frontend/app/js/views/Station/Views/QueueList/index.jsx

@@ -0,0 +1,116 @@
+import React, { Component } from "react";
+import PropTypes from "prop-types";
+
+import CustomInput from "components/CustomInput.jsx";
+import CustomErrors from "components/CustomMessages.jsx";
+
+import SongList from "./SongList.jsx";
+import PlaylistList from "./PlaylistList.jsx";
+
+import { connect } from "react-redux";
+
+import { closeOverlay1, openOverlay2, closeOverlay2 } from "actions/stationOverlay";
+import { selectPlaylist, deselectPlaylists } from "actions/playlistQueue";
+
+import io from "io";
+
+@connect(state => ({
+	user: {
+		loggedIn: state.session.get("loggedIn"),
+		userId: state.session.get("userId"),
+		role: state.session.get("role"),
+	},
+	station: {
+		stationId: state.station.info.get("id"),
+		owner: state.station.info.get("ownerId"),
+		playlistSelectedId: state.station.info.get("playlistSelected"),
+		songList: state.station.info.get("songList"),
+	},
+	song: {
+		exists: state.station.currentSong.get("songId") !== "",
+		title: state.station.currentSong.get("title"),
+		artists: state.station.currentSong.get("artists"),
+		duration: state.station.currentSong.getIn(["timings", "duration"]),
+		thumbnail: state.station.currentSong.get("thumbnail"),
+	},
+}))
+export default class QueueList extends Component {
+	constructor(props) {
+		super(props);
+
+		this.state = {
+			playlists: [],
+		};
+
+		io.getSocket((socket) => {
+			socket.emit('playlists.indexForUser', res => {
+				if (res.status === 'success') this.setState({
+					playlists: res.data,
+				});
+			});
+
+			socket.on('event:playlist.create', () => {
+				socket.emit('playlists.indexForUser', res => {
+					if (res.status === 'success') this.setState({
+						playlists: res.data,
+					});
+				});
+			});
+			socket.on('event:playlist.delete', () => {
+				socket.emit('playlists.indexForUser', res => {
+					if (res.status === 'success') this.setState({
+						playlists: res.data,
+					});
+				});
+			});
+		});
+	}
+
+	addSongToQueueCallback = (songId) => {
+		io.getSocket((socket) => {
+			// Add song to queue
+			socket.emit("stations.addToQueue", this.props.stationId, songId, res => {
+				if (res.status === "success") {
+					this.messages.clearAddSuccess("Successfully added song.");
+				} else {
+					this.messages.addError(res.message);
+				}
+				this.props.dispatch(closeOverlay2());
+			});
+		});
+	};
+
+	addSongToQueue = () => {
+		this.props.dispatch(openOverlay2("searchYouTube", null, this.addSongToQueueCallback));
+	};
+
+	deselectAll = () => {
+		this.props.dispatch(deselectPlaylists());
+	}
+
+	close = () => {
+		this.props.dispatch(closeOverlay1());
+	};
+
+	render() {
+		return (
+			<div className="overlay">
+				<button onClick={ this.close } className="back"><i className="material-icons">arrow_back</i></button>
+				<div className="content">
+					<h1>Queue</h1>
+					<CustomErrors onRef={ ref => (this.messages = ref) } />
+
+					<SongList/>
+					<button onClick={ this.addSongToQueue }>Add song to queue</button>
+					<hr/>
+					<PlaylistList/>
+					{
+						(this.props.station.playlistSelectedId)
+							? <button onClick={ this.deselectAll }>Deselect all playlists</button>
+							: null
+					}
+				</div>
+			</div>
+		);
+	}
+}

+ 101 - 67
frontend/app/js/views/Station/index.jsx

@@ -1,10 +1,12 @@
 import React, { Component } from "react";
 import { NavLink } from "react-router-dom";
+import { Map, List } from "immutable";
 
 import PropTypes from "prop-types";
 import { translate, Trans } from "react-i18next";
 
 import Player from "./Player";
+import PlayerDebug from "./PlayerDebug";
 import Seekerbar from "./Seekerbar";
 import VolumeSlider from "./VolumeSlider";
 import Ratings from "./Ratings";
@@ -12,6 +14,7 @@ import Time from "./Time";
 import Overlays from "./Views/Overlays";
 
 import { actionCreators as stationCurrentSongActionCreators } from "ducks/stationCurrentSong";
+import { actionCreators as stationInfoActionCreators } from "ducks/stationInfo";
 import { bindActionCreators } from "redux";
 
 //import { changeVolume } from "actions/volume";
@@ -45,8 +48,8 @@ import config from "config";
 		stationId: state.station.info.get("stationId"),
 		name: state.station.info.get("name"),
 		displayName: state.station.info.get("displayName"),
+		type: state.station.info.get("type"),
 		paused: state.station.info.get("paused"),
-		pausedAt: state.station.info.get("pausedAt"),
 		ownerId: state.station.info.get("ownerId"),
 	},/*
 	selectedPlaylistObject: {
@@ -62,6 +65,11 @@ import config from "config";
 	onDislikedUpdate: bindActionCreators(stationCurrentSongActionCreators.dislikedUpdate, dispatch),
 	onPauseTime: bindActionCreators(stationCurrentSongActionCreators.pauseTime, dispatch),
 	onResumeTime: bindActionCreators(stationCurrentSongActionCreators.resumeTime, dispatch),
+	onPause: bindActionCreators(stationInfoActionCreators.pause, dispatch),
+	onResume: bindActionCreators(stationInfoActionCreators.resume, dispatch),
+	onQueueIndex: bindActionCreators(stationInfoActionCreators.queueIndex, dispatch),
+	onQueueUpdate: bindActionCreators(stationInfoActionCreators.queueUpdate, dispatch),
+	onTimeElapsedUpdate: bindActionCreators(stationCurrentSongActionCreators.timeElapsedUpdate, dispatch),
 	openOverlay1: bindActionCreators(openOverlay1, dispatch),
 }))
 
@@ -78,6 +86,11 @@ export default class Station extends Component {
 	constructor(props) {
 		super();
 
+		this.state = {
+			timeElapsedInterval: setInterval(() => {
+				props.onTimeElapsedUpdate();
+			}, 500),
+		};
 		/*this.state = {
 			mode: this.getModeTemp(props.partyEnabled, props.queueLocked),
 		};*/
@@ -93,11 +106,11 @@ export default class Station extends Component {
 								skipDuration: res.data.currentSong.skipDuration,
 								// timeElapsed?
 								timePaused: res.data.timePaused,
-								// pausedAt?
 								startedAt: res.data.startedAt,
 							},
 							title: res.data.currentSong.title,
 							artists: res.data.currentSong.artists,
+							thumbnail: res.data.currentSong.thumbnail,
 							ratings: {
 								enabled: !(res.data.currentSong.likes === -1 && res.data.currentSong.dislikes === -1),
 								likes: res.data.currentSong.likes,
@@ -110,74 +123,89 @@ export default class Station extends Component {
 						// TODO This will probably need to be handled
 						this.props.onNextSong(null);
 					}
-				}
 
-				socket.on("event:songs.next", data => {
-					//this.addTopToQueue();
-					if (data.currentSong) {
-						let song = {
-							songId: data.currentSong.songId,
-							timings: {
-								duration: data.currentSong.duration,
-								skipDuration: data.currentSong.skipDuration,
-								// timeElapsed?
-								timePaused: data.timePaused,
-								// pausedAt?
-								startedAt: data.startedAt,
-							},
-							title: data.currentSong.title,
-							artists: data.currentSong.artists,
-							ratings: {
-								enabled: !(data.currentSong.likes === -1 && data.currentSong.dislikes === -1),
-								likes: data.currentSong.likes,
-								dislikes: data.currentSong.dislikes,
-							},
-						};
-						this.props.onNextSong(song);
-						this.fetchOwnRatings();
-					} else {
-						this.props.onNextSong(null);
-					}
-				});
+					socket.on("event:songs.next", data => {
+						//this.addTopToQueue();
+						if (data.currentSong) {
+							let song = {
+								songId: data.currentSong.songId,
+								timings: {
+									duration: data.currentSong.duration,
+									skipDuration: data.currentSong.skipDuration,
+									// timeElapsed?
+									timePaused: data.timePaused,
+									// pausedAt?
+									startedAt: data.startedAt,
+								},
+								title: data.currentSong.title,
+								artists: data.currentSong.artists,
+								thumbnail: data.currentSong.thumbnail,
+								ratings: {
+									enabled: !(data.currentSong.likes === -1 && data.currentSong.dislikes === -1),
+									likes: data.currentSong.likes,
+									dislikes: data.currentSong.dislikes,
+								},
+							};
+							this.props.onNextSong(song);
+							this.fetchOwnRatings();
+						} else {
+							this.props.onNextSong(null);
+						}
+					});
+					socket.on("event:stations.pause", pausedAt => {
+						// TODO Dispatch to station info
+						this.props.onPause();
+						this.props.onPauseTime(pausedAt);
+					});
+					socket.on("event:stations.resume", data => {
+						// TODO Dispatch to station info
+						this.props.onResume();
+						this.props.onResumeTime(data.timePaused);
+					});
+					socket.on("event:song.like", data => {
+						if (data.songId === this.props.song.songId) {
+							this.props.onLikeUpdate(data.likes);
+							this.props.onDislikeUpdate(data.dislikes);
+						}
+					});
+					socket.on("event:song.dislike", data => {
+						if (data.songId === this.props.song.songId) {
+							this.props.onLikeUpdate(data.likes);
+							this.props.onDislikeUpdate(data.dislikes);
+						}
+					});
+					socket.on("event:song.unlike", data => {
+						if (data.songId === this.props.song.songId) {
+							this.props.onLikeUpdate(data.likes);
+							this.props.onDislikeUpdate(data.dislikes);
+						}
+					});
+					socket.on("event:song.undislike", data => {
+						if (data.songId === this.props.song.songId) {
+							this.props.onLikeUpdate(data.likes);
+							this.props.onDislikeUpdate(data.dislikes);
+						}
+					});
+					socket.on("event:song.newRatings", data => {
+						if (data.songId === this.props.song.songId) {
+							this.props.onLikedUpdate(data.liked);
+							this.props.onDislikedUpdate(data.disliked);
+						}
+					});
 
-				socket.on("event:stations.pause", pausedAt => {
-					// TODO Dispatch to station info
-					this.props.onPauseTime(pausedAt);
-				});
-				socket.on("event:stations.resume", data => {
-					// TODO Dispatch to station info
-					this.props.onResumeTime(data.timePaused);
-				});
-				socket.on("event:song.like", data => {
-					if (data.songId === this.props.song.songId) {
-						this.props.onLikeUpdate(data.likes);
-						this.props.onDislikeUpdate(data.dislikes);
-					}
-				});
-				socket.on("event:song.dislike", data => {
-					if (data.songId === this.props.song.songId) {
-						this.props.onLikeUpdate(data.likes);
-						this.props.onDislikeUpdate(data.dislikes);
+					if (this.props.station.type === "community") {
+						socket.emit("stations.getQueue", this.props.station.stationId, data => {
+							if (data.status === "success") {
+								this.props.onQueueIndex(data.queue);
+							}
+							//TODO Handle error
+						});
+
+						socket.on("event:queue.update", queue => {
+							this.props.onQueueUpdate(queue);
+						});
 					}
-				});
-				socket.on("event:song.unlike", data => {
-					if (data.songId === this.props.song.songId) {
-						this.props.onLikeUpdate(data.likes);
-						this.props.onDislikeUpdate(data.dislikes);
-					}
-				});
-				socket.on("event:song.undislike", data => {
-					if (data.songId === this.props.song.songId) {
-						this.props.onLikeUpdate(data.likes);
-						this.props.onDislikeUpdate(data.dislikes);
-					}
-				});
-				socket.on("event:song.newRatings", data => {
-					if (data.songId === this.props.song.songId) {
-						this.props.onLikedUpdate(data.liked);
-						this.props.onDislikedUpdate(data.disliked);
-					}
-				});
+				}
 			});
 		});
 
@@ -188,6 +216,10 @@ export default class Station extends Component {
 		}, 1000);*/
 	}
 
+	componentWillUnmount() {
+		clearInterval(this.state.timeElapsedInterval);
+	}
+
 	/*isInQueue = (songId, cb) => {
 		io.getSocket((socket) => {
 			socket.emit('stations.getQueue', this.props.stationId, data => {
@@ -368,6 +400,8 @@ export default class Station extends Component {
 
 				<h1 onClick={ this.addSongTemp }>{ this.props.station.displayName }</h1>
 
+				<PlayerDebug />
+
 				<div className={(!this.props.song.exists) ? "player-container hidden" : "player-container"}>
 					<div className="iframe-container">
 						<Player onRef={ ref => (this.player = ref) }/>