ソースを参照

Added Redux devtools action filter, added stationPlaylists duck, split the queue and playlist into different components, made playlists overlay functional.

KrisVos130 7 年 前
コミット
6d0a2ae82a

+ 0 - 15
frontend/app/js/ducks/stationInfo.js

@@ -12,7 +12,6 @@ const QUEUE_INDEX = "STATION_INFO::QUEUE_INDEX";
 const QUEUE_UPDATE = "STATION_INFO::QUEUE_UPDATE";
 const PAUSE = "STATION_INFO::PAUSE";
 const RESUME = "STATION_INFO::RESUME";
-const PLAYLISTS_UPDATE = "STATION_INFO::PLAYLISTS_UPDATE";
 
 function joinStation(station) {
 	return {
@@ -95,13 +94,6 @@ function resume() {
 	}
 }
 
-function playlistsUpdate(playlists) {
-	return {
-		type: PLAYLISTS_UPDATE,
-		playlists,
-	}
-}
-
 
 
 const initialState = Map({
@@ -118,7 +110,6 @@ const initialState = Map({
 	"userCount": 0,
 	"songList": List([]),
 	"privatePlaylist": "",
-	"playlists": List([]),
 });
 
 function reducer(state = initialState, action) {
@@ -204,10 +195,6 @@ function reducer(state = initialState, action) {
 		return state.merge({
 			paused: false,
 		});
-	case PLAYLISTS_UPDATE:
-		return state.merge({
-			playlists: action.playlists,
-		});
 	}
 	return state;
 }
@@ -225,7 +212,6 @@ const actionCreators = {
 	queueUpdate,
 	pause,
 	resume,
-	playlistsUpdate,
 };
 
 const actionTypes = {
@@ -241,7 +227,6 @@ const actionTypes = {
 	QUEUE_UPDATE,
 	PAUSE,
 	RESUME,
-	PLAYLISTS_UPDATE,
 };
 
 export {

+ 175 - 0
frontend/app/js/ducks/stationPlaylists.js

@@ -0,0 +1,175 @@
+import { Map, List, fromJS } from "immutable";
+
+const UPDATE = "STATION_PLAYLISTS::UPDATE";
+const ADD_SONG = "STATION_PLAYLISTS::ADD_SONG";
+const UPDATE_DISPLAY_NAME = "STATION_PLAYLISTS::UPDATE_DISPLAY_NAME";
+const MOVE_SONG_TO_BOTTOM = "STATION_PLAYLISTS::MOVE_SONG_TO_BOTTOM";
+const MOVE_SONG_TO_TOP = "STATION_PLAYLISTS::MOVE_SONG_TO_TOP";
+const REMOVE_SONG = "STATION_PLAYLISTS::REMOVE_SONG";
+
+function update(playlists) {
+	return {
+		type: UPDATE,
+		playlists,
+	}
+}
+
+function addSong(playlistId, song) {
+	return {
+		type: ADD_SONG,
+		playlistId,
+		song,
+	}
+}
+
+function updateDisplayName(playlistId, displayName) {
+	return {
+		type: UPDATE_DISPLAY_NAME,
+		playlistId,
+		displayName,
+	}
+}
+
+function moveSongToBottom(playlistId, songId) {
+	return {
+		type: MOVE_SONG_TO_BOTTOM,
+		playlistId,
+		songId,
+	}
+}
+
+function moveSongToTop(playlistId, songId) {
+	return {
+		type: MOVE_SONG_TO_TOP,
+		playlistId,
+		songId,
+	}
+}
+
+function removeSong(playlistId, songId) {
+	return {
+		type: REMOVE_SONG,
+		playlistId,
+		songId,
+	}
+}
+
+
+
+
+
+const initialState = List([]);
+
+function reducer(state = initialState, action) {
+	let playlistId, songId;
+
+	function updatePlaylist(playlistId, updater) {
+		return state.update(
+			state.findIndex(function(playlist) {
+				console.log(55544, playlist);
+				return playlist.get("playlistId") === playlistId;
+			}),
+			updater
+		);
+	}
+
+	function updatePlaylistSongs(playlistId, updater) {
+		return updatePlaylist(playlistId, function (playlist) {
+			return playlist.update("songs", function (songs) {
+				return songs.update(updater);
+			});
+		})
+	}
+
+	switch (action.type) {
+	case UPDATE:
+		let { playlists } = action;
+
+		playlists = playlists.map((playlist) => {
+			return {
+				playlistId: playlist._id,
+				createdAt: playlist.createdAt,
+				createdBy: playlist.createdBy,
+				displayName: playlist.displayName,
+				songs: playlist.songs,
+			};
+		});
+
+		return fromJS(playlists);
+	case ADD_SONG:
+		const { song } = action;
+		playlistId = action.playlistId;
+
+		return updatePlaylistSongs(playlistId, function (songs) {
+			return songs.push(fromJS(song));
+		});
+	case UPDATE_DISPLAY_NAME:
+		const { displayName } = action;
+		playlistId = action.playlistId;
+
+		return updatePlaylist(playlistId, function (playlist) {
+			return playlist.set("displayName", displayName);
+		});
+	case MOVE_SONG_TO_BOTTOM:
+		playlistId = action.playlistId;
+		songId = action.songId;
+
+		return updatePlaylistSongs(playlistId, function (songs) {
+			let songIndex = songs.findIndex(function (song) {
+				return song.get("songId") === songId;
+			});
+			let song = songs.get(songIndex);
+			songs = songs.delete(songIndex);
+			songs = songs.push(song);
+			return songs;
+		});
+	case MOVE_SONG_TO_TOP:
+		playlistId = action.playlistId;
+		songId = action.songId;
+
+		return updatePlaylistSongs(playlistId, function (songs) {
+			let songIndex = songs.findIndex(function (song) {
+				return song.get("songId") === songId;
+			});
+			let song = songs.get(songIndex);
+			songs = songs.delete(songIndex);
+			songs = songs.unshift(song);
+			return songs;
+		});
+	case REMOVE_SONG:
+		playlistId = action.playlistId;
+		songId = action.songId;
+
+		return updatePlaylistSongs(playlistId, function (songs) {
+			let songIndex = songs.findIndex(function (song) {
+				return song.get("songId") === songId;
+			});
+			return songs.delete(songIndex);
+		});
+	}
+	return state;
+}
+
+const actionCreators = {
+	update,
+	addSong,
+	updateDisplayName,
+	moveSongToBottom,
+	moveSongToTop,
+	removeSong,
+};
+
+const actionTypes = {
+	ADD_SONG,
+	UPDATE_DISPLAY_NAME,
+	MOVE_SONG_TO_BOTTOM,
+	MOVE_SONG_TO_TOP,
+	REMOVE_SONG,
+};
+
+export {
+	actionCreators,
+	actionTypes,
+};
+
+export default reducer;

+ 3 - 1
frontend/app/js/index.js

@@ -20,7 +20,9 @@ let store = null;
 const middleware = applyMiddleware(thunk);
 store = createStore(
 	rootReducer,
-	window.__REDUX_DEVTOOLS_EXTENSION__ && window.__REDUX_DEVTOOLS_EXTENSION__() //middleware //TODO See what the middleware does
+	window.__REDUX_DEVTOOLS_EXTENSION__ && window.__REDUX_DEVTOOLS_EXTENSION__({
+		actionsBlacklist: ["STATION_CURRENT_SONG::TIME_ELAPSED_UPDATE"],
+	}) //middleware
 );
 
 ReactDOM.render(

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

@@ -10,6 +10,7 @@ import session from "../ducks/session";
 import volume from "../ducks/volume";
 import stationCurrentSong from "../ducks/stationCurrentSong";
 import stationInfo from "../ducks/stationInfo";
+import stationPlaylists from "../ducks/stationPlaylists";
 
 export default combineReducers({
 	volume,
@@ -23,5 +24,6 @@ export default combineReducers({
 	station: combineReducers({
 		info: stationInfo,
 		currentSong: stationCurrentSong,
+		playlists: stationPlaylists,
 	}),
 });

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

@@ -4,10 +4,10 @@ import PropTypes from "prop-types";
 import { connect } from "react-redux";
 
 import Settings from "./Settings";
-import Playlists from "./Playlists";
-import EditPlaylist from "./EditPlaylist";
+import Playlists from "./Playlists/index.jsx";
+import EditPlaylist from "./Playlists/EditPlaylist";
 import SearchYouTube from "./SearchYouTube";
-import QueueList from "./QueueList/index.jsx";
+import QueueList from "./Queue/index.jsx";
 
 @connect(state => ({
 	overlay1: state.stationOverlay.get("overlay1"),

+ 0 - 156
frontend/app/js/views/Station/Views/Playlists.jsx

@@ -1,156 +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 } from "actions/stationOverlay";
-
-import io from "io";
-
-@connect(state => ({
-	user: {
-		userId: state.user.get("userId"),
-		role: state.user.get("role"),
-	},
-	loggedIn: state.user.get("loggedIn"),
-	stationId: state.station.get("id"),
-	stationOwner: state.station.get("ownerId"),
-	partyEnabled: state.station.get("partyMode"),
-	queueLocked: state.station.get("locked"),
-	privatePlaylist: state.station.get("privatePlaylist"),
-}))
-export default class Playlists extends Component {
-	constructor(props) {
-		super(props);
-
-		CustomInput.initialize(this);
-
-		this.state = {
-			playlists: [],
-			mode: this.getModeTemp(props.partyEnabled, props.queueLocked),
-		};
-
-		io.getSocket((socket) => {
-			socket.emit('playlists.indexForUser', res => {
-				if (res.status === 'success') this.setState({
-					playlists: res.data,
-				});
-			});
-		});
-	}
-
-	close = () => {
-		this.props.dispatch(closeOverlay1());
-	};
-
-	createPlaylist = () => {
-		this.messages.clearErrorSuccess();
-		if (CustomInput.hasInvalidInput(this.input, ["description"])) {
-			this.messages.clearAddError(this.props.t("general:someFieldsAreIncorrectError"));
-		} else {
-			io.getSocket(socket => {
-				socket.emit('playlists.create', { displayName: this.input.description.getValue(), songs: [] }, res => {
-					if (res.status === "success") {
-						this.props.dispatch(openOverlay2("editPlaylist", { playlistId: res.playlistId }));
-					} else {
-						this.messages.addError(res.message);
-					}
-				});
-			});
-		}
-	};
-
-	getModeTemp = (partyEnabled, queueLocked) => {
-		// If party enabled
-		// If queue locked
-		// Mode is DJ
-		// If queue not locked
-		// Mode party
-		// If party not enabled
-		// Mode is normal
-
-		if (partyEnabled) {
-			if (queueLocked) return "dj";
-			else return "party";
-		} else return "normal";
-	};
-
-	getPlaylistAction = (playlistId) => {
-		if (this.state.mode === "normal") {
-			if (this.isOwner()) {
-				const play = <span onClick={ () => { this.playPlaylist(playlistId) } }>PLAY!</span>;
-				//const stop = <span onClick={ () => { this.stopPlaylist(playlistId) } }>STOP!</span>;
-				const stop = null; // There's currently no backend functionality to stop a playlist from playing
-
-				if (this.props.privatePlaylist === playlistId) return stop;
-				else return play;
-			}
-		}
-
-		return null;
-	};
-
-	isOwner = () => {
-		if (this.props.loggedIn) {
-			if (this.props.user.userId === this.props.stationOwner) return true;
-		}
-
-		return false;
-	};
-
-	playPlaylist = (playlistId) => {
-		this.messages.clearErrorSuccess();
-		io.getSocket((socket) => {
-			socket.emit("stations.selectPrivatePlaylist", this.props.stationId, playlistId, (res) => {
-				if (res.status === "success") {
-					this.message.addSuccess("Successfully selected private playlist.");
-				} else {
-					this.messages.addError(res.message);
-				}
-			});
-		});
-	};
-
-	/*stopPlaylist = (playlistId) => {
-		this.messages.clearErrorSuccess();
-		io.getSocket((socket) => {
-			socket.emit("stations.selectPrivatePlaylist", this.props.stationId, playlistId, (res) => {
-				if (res.status === "success") {
-					this.message.addSuccess("Successfully selected private playlist.");
-				} else {
-					this.messages.addError(res.message);
-				}
-			});
-		});
-	};*/
-
-	render() {
-		return (
-			<div className="overlay">
-				<button onClick={ this.close }>Back</button>
-				<h1>Playlists</h1>
-				<CustomErrors onRef={ ref => (this.messages = ref) } />
-
-				<CustomInput key="description" type="playlistDescription" name="description" label="Description" placeholder="Description" original={ this.props.description } onRef={ ref => (this.input.description = ref) } />
-				<button onClick={ this.createPlaylist }>Create playlist</button>
-
-				<h2>Playlists</h2>
-				<ul>
-					{
-						this.state.playlists.map((playlist) => {
-							return (
-								<li key={ playlist._id }>
-									{ playlist.displayName } - <span onClick={ () => { this.props.dispatch(openOverlay2("editPlaylist", { playlistId: playlist._id })) } }>Edit</span>
-									{ this.getPlaylistAction(playlist._id) }
-								</li>
-							)
-						})
-					}
-				</ul>
-			</div>
-		);
-	}
-}

+ 21 - 118
frontend/app/js/views/Station/Views/EditPlaylist.jsx → frontend/app/js/views/Station/Views/Playlists/EditPlaylist.jsx

@@ -11,111 +11,13 @@ import { closeOverlay2, openOverlay3, closeOverlay3 } from "actions/stationOverl
 import io from "io";
 
 @connect(state => ({
-	stationId: state.station.get("id"),
+	playlists: state.station.playlists,
 }))
 export default class EditPlaylist extends Component {
 	constructor(props) {
 		super(props);
 
 		CustomInput.initialize(this);
-
-		this.state = {
-			gotPlaylist: false,
-			playlist: {},
-		};
-
-		io.getSocket((socket) => {
-			socket.emit('playlists.getPlaylist', this.props.playlistId, res => {
-				if (res.status === 'success') {
-					this.input.displayName.setValue(res.data.displayName, true);
-					this.setState({
-						gotPlaylist: true,
-						playlist: res.data,
-					});
-				}
-			});
-
-			socket.on('event:playlist.addSong', data => {
-				if (this.props.playlistId === data.playlistId) {
-					let songs = this.state.playlist.songs;
-					songs.push(data.song);
-					this.setState({
-						playlist: {
-							...this.state.playlist,
-							songs,
-						},
-					});
-				}
-			});
-
-			socket.on('event:playlist.updateDisplayName', data => {
-				if (this.props.playlistId === data.playlistId) {
-					this.setState({
-						playlist: {
-							...this.state.playlist,
-							displayName: data.displayName,
-						},
-					});
-				}
-			});
-
-			socket.on('event:playlist.moveSongToBottom', data => {
-				if (this.props.playlistId === data.playlistId) {
-					let songs = this.state.playlist.songs;
-					let songIndex;
-					songs.forEach((song, index) => {
-						if (song.songId === data.songId) songIndex = index;
-					});
-					let song = songs.splice(songIndex, 1)[0];
-					songs.push(song);
-
-					this.setState({
-						playlist: {
-							...this.state.playlist,
-							songs,
-						},
-					});
-				}
-			});
-
-			socket.on('event:playlist.moveSongToTop', (data) => {
-				if (this.props.playlistId === data.playlistId) {
-					let songs = this.state.playlist.songs;
-					let songIndex;
-					songs.forEach((song, index) => {
-						if (song.songId === data.songId) songIndex = index;
-					});
-					let song = songs.splice(songIndex, 1)[0];
-					songs.unshift(song);
-
-					this.setState({
-						playlist: {
-							...this.state.playlist,
-							songs,
-						},
-					});
-				}
-			});
-
-			socket.on('event:playlist.removeSong', data => {
-				if (this.props.playlistId === data.playlistId) {
-					//TODO Somehow make this sync, so when 2 songs get removed at the same ms it removes both not just one
-					let songs = this.state.playlist.songs;
-					songs.forEach((song, index) => {
-						if (song.songId === data.songId) songs.splice(index, 1);
-					});
-
-					this.setState({
-						playlist: {
-							...this.state.playlist,
-							songs,
-						},
-					});
-				}
-			});
-		});
-
-		console.log("edit Playlist", props);
 	}
 
 	addSongToPlaylistCallback = (songId) => {
@@ -211,33 +113,34 @@ export default class EditPlaylist extends Component {
 	};
 
 	render() {
+		const { playlistId } = this.props;
+		const playlist = this.props.playlists.find((playlist) => {
+			return playlist.get("playlistId") === playlistId;
+		});
+
 		return (
 			<div className="overlay">
 				<button onClick={ this.close }>Back</button>
 				<h1>Edit Playlist</h1>
-				<CustomInput type="playlistDescription" name="displayName" label="Display name" placeholder="Display name" onRef={ ref => (this.input.displayName = ref) } />
+				<CustomInput type="playlistDescription" name="displayName" label="Display name" placeholder="Display name" onRef={ ref => (this.input.displayName = ref) } original={ playlist.get("displayName") }/>
 				<button onClick={ this.changeDisplayName }>Change displayname</button>
 
 
 				{
-					(this.state.gotPlaylist)
-					? (
-						<ul>
-							{
-								this.state.playlist.songs.map((song) => {
-									return (
-										<li key={ song.songId }>
-											<p>{ song.title }</p>
-											<span onClick={ () => { this.deleteSong(song.songId) }}>DELETE</span><br/>
-											<span onClick={ () => { this.promoteSong(song.songId) }}>UP</span><br/>
-											<span onClick={ () => { this.demoteSong(song.songId) }}>DOWN</span>
-										</li>
-									);
-								})
-							}
-						</ul>
-					)
-					: null
+					<ul>
+						{
+							playlist.get("songs").map((song) => {
+								return (
+									<li key={ song.get("songId") }>
+										<p>{ song.get("title") }</p>
+										<span onClick={ () => { this.deleteSong(song.get("songId")) }}>DELETE</span><br/>
+										<span onClick={ () => { this.promoteSong(song.get("songId")) }}>UP</span><br/>
+										<span onClick={ () => { this.demoteSong(song.get("songId")) }}>DOWN</span>
+									</li>
+								);
+							})
+						}
+					</ul>
 				}
 
 

+ 67 - 0
frontend/app/js/views/Station/Views/Playlists/PlaylistItem.jsx

@@ -0,0 +1,67 @@
+import React, { Component } from "react";
+import PropTypes from "prop-types";
+
+import { connect } from "react-redux";
+
+import { openOverlay2 } from "actions/stationOverlay";
+
+import io from "io";
+
+@connect(state => ({
+	station: {
+		stationId: state.station.info.get("stationId"),
+		mode: state.station.info.get("mode"),
+	},
+}))
+export default class PlaylistItem extends Component {
+	constructor(props) {
+		super(props);
+	}
+
+	isOwner = () => {
+		if (this.props.loggedIn) {
+			if (this.props.user.userId === this.props.stationOwner) return true;
+		}
+
+		return false;
+	};
+
+	playPlaylist = (playlistId) => {
+		this.messages.clearErrorSuccess();
+		io.getSocket((socket) => {
+			socket.emit("stations.selectPrivatePlaylist", this.props.station.stationId, playlistId, (res) => {
+				if (res.status === "success") {
+					this.message.addSuccess("Successfully selected private playlist.");
+				} else {
+					this.messages.addError(res.message);
+				}
+			});
+		});
+	};
+
+	getPlaylistAction = (playlistId) => {
+		if (this.props.station.mode === "normal") {
+			if (this.isOwner()) {
+				const play = <span onClick={ () => { this.playPlaylist(playlistId) } }>PLAY!</span>;
+				//const stop = <span onClick={ () => { this.stopPlaylist(playlistId) } }>STOP!</span>;
+				const stop = null; // There's currently no backend functionality to stop a playlist from playing
+
+				if (this.props.privatePlaylist === playlistId) return stop;
+				else return play;
+			}
+		}
+
+		return null;
+	};
+
+	render() {
+		const { playlist } = this.props;
+
+		return (
+			<li>
+				{ playlist.get("displayName") } - <span onClick={ () => { this.props.dispatch(openOverlay2("editPlaylist", { playlistId: playlist.get("playlistId") })) } }>Edit</span>
+				{ this.getPlaylistAction(playlist.get("playlistId")) }
+			</li>
+		);
+	}
+}

+ 29 - 0
frontend/app/js/views/Station/Views/Playlists/PlaylistList.jsx

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

+ 74 - 0
frontend/app/js/views/Station/Views/Playlists/index.jsx

@@ -0,0 +1,74 @@
+import React, { Component } from "react";
+import PropTypes from "prop-types";
+
+import CustomInput from "components/CustomInput.jsx";
+import CustomErrors from "components/CustomMessages.jsx";
+
+import PlaylistList from "./PlaylistList.jsx";
+
+import { connect } from "react-redux";
+
+import { closeOverlay1 } from "actions/stationOverlay";
+
+import io from "io";
+
+@connect(state => ({
+
+}))
+export default class Playlists extends Component {
+	constructor(props) {
+		super(props);
+
+		CustomInput.initialize(this);
+	}
+
+	close = () => {
+		this.props.dispatch(closeOverlay1());
+	};
+
+	createPlaylist = () => {
+		this.messages.clearErrorSuccess();
+		if (CustomInput.hasInvalidInput(this.input, ["description"])) {
+			this.messages.clearAddError(this.props.t("general:someFieldsAreIncorrectError"));
+		} else {
+			io.getSocket(socket => {
+				socket.emit('playlists.create', { displayName: this.input.description.getValue(), songs: [] }, res => {
+					if (res.status === "success") {
+						this.props.dispatch(openOverlay2("editPlaylist", { playlistId: res.playlistId }));
+					} else {
+						this.messages.addError(res.message);
+					}
+				});
+			});
+		}
+	};
+
+	/*stopPlaylist = (playlistId) => {
+		this.messages.clearErrorSuccess();
+		io.getSocket((socket) => {
+			socket.emit("stations.selectPrivatePlaylist", this.props.stationId, playlistId, (res) => {
+				if (res.status === "success") {
+					this.message.addSuccess("Successfully selected private playlist.");
+				} else {
+					this.messages.addError(res.message);
+				}
+			});
+		});
+	};*/
+
+	render() {
+		return (
+			<div className="overlay">
+				<button onClick={ this.close }>Back</button>
+				<h1>Playlists</h1>
+				<CustomErrors onRef={ ref => (this.messages = ref) } />
+
+				<CustomInput key="description" type="playlistDescription" name="description" label="Description" placeholder="Description" original={ this.props.description } onRef={ ref => (this.input.description = ref) } />
+				<button onClick={ this.createPlaylist }>Create playlist</button>
+
+				<h2>Playlists</h2>
+				<PlaylistList/>
+			</div>
+		);
+	}
+}

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


+ 2 - 2
frontend/app/js/views/Station/Views/QueueList/PlaylistList.jsx → frontend/app/js/views/Station/Views/Queue/PlaylistList.jsx

@@ -7,7 +7,7 @@ import PlaylistItem from "./PlaylistItem.jsx";
 
 @connect(state => ({
 	station: {
-		playlists: state.station.info.get("playlists"),
+		playlists: state.station.playlists,
 	},
 }))
 export default class PlaylistList extends Component {
@@ -16,7 +16,7 @@ export default class PlaylistList extends Component {
 	}
 
 	render() {
-		const { playlists } = this.props.station;
+		const { playlists } = this.props;
 
 		return (
 			<ul>

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


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


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


+ 22 - 1
frontend/app/js/views/Station/index.jsx

@@ -15,6 +15,7 @@ import Overlays from "./Views/Overlays";
 
 import { actionCreators as stationCurrentSongActionCreators } from "ducks/stationCurrentSong";
 import { actionCreators as stationInfoActionCreators } from "ducks/stationInfo";
+import { actionCreators as stationPlaylistsActionCreators } from "ducks/stationPlaylists";
 import { bindActionCreators } from "redux";
 
 //import { changeVolume } from "actions/volume";
@@ -70,7 +71,12 @@ import config from "config";
 	onQueueIndex: bindActionCreators(stationInfoActionCreators.queueIndex, dispatch),
 	onQueueUpdate: bindActionCreators(stationInfoActionCreators.queueUpdate, dispatch),
 	onTimeElapsedUpdate: bindActionCreators(stationCurrentSongActionCreators.timeElapsedUpdate, dispatch),
-	onPlaylistsUpdate: bindActionCreators(stationInfoActionCreators.playlistsUpdate, dispatch),
+	onPlaylistsUpdate: bindActionCreators(stationPlaylistsActionCreators.update, dispatch),
+	onPlaylistsAddSong: bindActionCreators(stationPlaylistsActionCreators.addSong, dispatch),
+	onPlaylistsUpdateDisplayName: bindActionCreators(stationPlaylistsActionCreators.updateDisplayName, dispatch),
+	onPlaylistsMoveSongToBottom: bindActionCreators(stationPlaylistsActionCreators.moveSongToBottom, dispatch),
+	onPlaylistsMoveSongToTop: bindActionCreators(stationPlaylistsActionCreators.moveSongToTop, dispatch),
+	onPlaylistsRemoveSong: bindActionCreators(stationPlaylistsActionCreators.removeSong, dispatch),
 	openOverlay1: bindActionCreators(openOverlay1, dispatch),
 }))
 
@@ -207,6 +213,21 @@ export default class Station extends Component {
 							if (res.status === "success") this.props.onPlaylistsUpdate(res.data);
 						});
 					});
+					socket.on("event:playlist.addSong", data => {
+						this.props.onPlaylistsAddSong(data.playlistId, data.song);
+					});
+					socket.on("event:playlist.updateDisplayName", data => {
+						this.props.onPlaylistsUpdateDisplayName(data.playlistId, data.displayName);
+					});
+					socket.on("event:playlist.moveSongToBottom", data => {
+						this.props.onPlaylistsMoveSongToBottom(data.playlistId, data.songId);
+					});
+					socket.on("event:playlist.moveSongToTop", data => {
+						this.props.onPlaylistsMoveSongToTop(data.playlistId, data.songId);
+					});
+					socket.on("event:playlist.removeSong", data => {
+						this.props.onPlaylistsRemoveSong(data.playlistId, data.songId);
+					});
 
 					if (this.props.station.type === "community") {
 						socket.emit("stations.getQueue", this.props.station.stationId, data => {

+ 142 - 80
frontend/package-lock.json

@@ -5,9 +5,9 @@
   "requires": true,
   "dependencies": {
     "abbrev": {
-      "version": "1.1.0",
-      "resolved": "https://registry.npmjs.org/abbrev/-/abbrev-1.1.0.tgz",
-      "integrity": "sha1-0FVMIlZjbi9W58LlrRg/hZQo2B8=",
+      "version": "1.1.1",
+      "resolved": "https://registry.npmjs.org/abbrev/-/abbrev-1.1.1.tgz",
+      "integrity": "sha512-nne9/IiQ/hzIhY6pdDnbBtz7DjPTKrY00P/zvPSm5pOFkl6xuGrGnXn/VtTNNfNtAfZ9/1RtehkszU9qcTii0Q==",
       "dev": true
     },
     "accepts": {
@@ -134,9 +134,9 @@
       }
     },
     "aproba": {
-      "version": "1.1.2",
-      "resolved": "https://registry.npmjs.org/aproba/-/aproba-1.1.2.tgz",
-      "integrity": "sha512-ZpYajIfO0j2cOFTO955KUMIKNmj6zhX8kVztMAxFsDaMwz+9Z9SV0uou2pC9HJqcfpffOsjnbrDMvkNy+9RXPw==",
+      "version": "1.2.0",
+      "resolved": "https://registry.npmjs.org/aproba/-/aproba-1.2.0.tgz",
+      "integrity": "sha512-Y9J6ZjXtoYh8RnXVCMOU/ttDmk1aBjunq9vO0ta5x85WDQiQfUF9sIPBITdbiiIVcBo03Hi3jMxigBtsddlXRw==",
       "dev": true
     },
     "are-we-there-yet": {
@@ -1649,9 +1649,9 @@
       "dev": true
     },
     "caseless": {
-      "version": "0.12.0",
-      "resolved": "https://registry.npmjs.org/caseless/-/caseless-0.12.0.tgz",
-      "integrity": "sha1-G2gcIf+EAzyCZUMJBolCDRhxUdw=",
+      "version": "0.11.0",
+      "resolved": "https://registry.npmjs.org/caseless/-/caseless-0.11.0.tgz",
+      "integrity": "sha1-cVuW6phBWTzDMGeSP17GDr2k99c=",
       "dev": true
     },
     "center-align": {
@@ -3102,9 +3102,9 @@
       }
     },
     "extsprintf": {
-      "version": "1.0.2",
-      "resolved": "https://registry.npmjs.org/extsprintf/-/extsprintf-1.0.2.tgz",
-      "integrity": "sha1-4QgOBljjALBilJkMxw4VAiNf1VA=",
+      "version": "1.3.0",
+      "resolved": "https://registry.npmjs.org/extsprintf/-/extsprintf-1.3.0.tgz",
+      "integrity": "sha1-lpGEQOMEGnpBT4xS48V06zw+HgU=",
       "dev": true
     },
     "fast-deep-equal": {
@@ -3348,7 +3348,7 @@
       "integrity": "sha1-LANAXHU4w51+s3sxcCLjJfsBi/c=",
       "dev": true,
       "requires": {
-        "aproba": "1.1.2",
+        "aproba": "1.2.0",
         "console-control-strings": "1.1.0",
         "has-unicode": "2.0.1",
         "object-assign": "4.1.1",
@@ -3552,20 +3552,24 @@
       "integrity": "sha1-/Xqtcmvxpf0W38KbL3pmAdJxOcQ=",
       "dev": true
     },
-    "har-schema": {
-      "version": "1.0.5",
-      "resolved": "https://registry.npmjs.org/har-schema/-/har-schema-1.0.5.tgz",
-      "integrity": "sha1-0mMTX0MwfALGAq/I/pWXDAFRNp4=",
-      "dev": true
-    },
     "har-validator": {
-      "version": "4.2.1",
-      "resolved": "https://registry.npmjs.org/har-validator/-/har-validator-4.2.1.tgz",
-      "integrity": "sha1-M0gdDxu/9gDdID11gSpqX7oALio=",
+      "version": "2.0.6",
+      "resolved": "https://registry.npmjs.org/har-validator/-/har-validator-2.0.6.tgz",
+      "integrity": "sha1-zcvAgYgmWtEZtqWnyKtw7s+10n0=",
       "dev": true,
       "requires": {
-        "ajv": "4.11.8",
-        "har-schema": "1.0.5"
+        "chalk": "1.1.3",
+        "commander": "2.12.2",
+        "is-my-json-valid": "2.16.0",
+        "pinkie-promise": "2.0.1"
+      },
+      "dependencies": {
+        "commander": {
+          "version": "2.12.2",
+          "resolved": "https://registry.npmjs.org/commander/-/commander-2.12.2.tgz",
+          "integrity": "sha512-BFnaq5ZOGcDN7FlrtBT4xxkgIToalIIxwjxLWVJ8bGTpe1LroqMiqQXdA7ygc7CRvaYS+9zfPGFnJqFSayx+AA==",
+          "dev": true
+        }
       }
     },
     "has": {
@@ -3870,7 +3874,7 @@
       "dev": true,
       "requires": {
         "assert-plus": "0.2.0",
-        "jsprim": "1.4.0",
+        "jsprim": "1.4.1",
         "sshpk": "1.13.1"
       }
     },
@@ -4364,15 +4368,15 @@
       "dev": true
     },
     "jsprim": {
-      "version": "1.4.0",
-      "resolved": "https://registry.npmjs.org/jsprim/-/jsprim-1.4.0.tgz",
-      "integrity": "sha1-o7h+QCmNjDgFUtjMdiigu5WiKRg=",
+      "version": "1.4.1",
+      "resolved": "https://registry.npmjs.org/jsprim/-/jsprim-1.4.1.tgz",
+      "integrity": "sha1-MT5mvB5cwG5Di8G3SZwuXFastqI=",
       "dev": true,
       "requires": {
         "assert-plus": "1.0.0",
-        "extsprintf": "1.0.2",
+        "extsprintf": "1.3.0",
         "json-schema": "0.2.3",
-        "verror": "1.3.6"
+        "verror": "1.10.0"
       },
       "dependencies": {
         "assert-plus": {
@@ -4526,6 +4530,12 @@
       "integrity": "sha1-h79AKSuM+D5OjOGjrkIJ4gBxZ1o=",
       "dev": true
     },
+    "lodash.mergewith": {
+      "version": "4.6.0",
+      "resolved": "https://registry.npmjs.org/lodash.mergewith/-/lodash.mergewith-4.6.0.tgz",
+      "integrity": "sha1-FQzwoWeR9ZA7iJHqsVRgknS96lU=",
+      "dev": true
+    },
     "loglevel": {
       "version": "1.4.1",
       "resolved": "https://registry.npmjs.org/loglevel/-/loglevel-1.4.1.tgz",
@@ -4818,9 +4828,9 @@
       "dev": true
     },
     "nan": {
-      "version": "2.6.2",
-      "resolved": "https://registry.npmjs.org/nan/-/nan-2.6.2.tgz",
-      "integrity": "sha1-5P805slf37WuzAjeZZb0NgWn20U=",
+      "version": "2.8.0",
+      "resolved": "https://registry.npmjs.org/nan/-/nan-2.8.0.tgz",
+      "integrity": "sha1-7XFfP+neArV6XmJS2QqWZ14fCFo=",
       "dev": true
     },
     "natural-compare": {
@@ -4882,7 +4892,7 @@
         "nopt": "3.0.6",
         "npmlog": "4.1.2",
         "osenv": "0.1.4",
-        "request": "2.81.0",
+        "request": "2.79.0",
         "rimraf": "2.5.4",
         "semver": "5.3.0",
         "tar": "2.2.1",
@@ -4935,9 +4945,9 @@
       }
     },
     "node-sass": {
-      "version": "3.13.1",
-      "resolved": "https://registry.npmjs.org/node-sass/-/node-sass-3.13.1.tgz",
-      "integrity": "sha1-ckD7v/I5YwS0IjUn7TAgWJwAT8I=",
+      "version": "4.7.2",
+      "resolved": "https://registry.npmjs.org/node-sass/-/node-sass-4.7.2.tgz",
+      "integrity": "sha512-CaV+wLqZ7//Jdom5aUFCpGNoECd7BbNhjuwdsX/LkXBrHl8eb1Wjw4HvWqcFvhr5KuNgAk8i/myf/MQ1YYeroA==",
       "dev": true,
       "requires": {
         "async-foreach": "0.1.3",
@@ -4949,13 +4959,16 @@
         "in-publish": "2.0.0",
         "lodash.assign": "4.2.0",
         "lodash.clonedeep": "4.5.0",
+        "lodash.mergewith": "4.6.0",
         "meow": "3.7.0",
         "mkdirp": "0.5.1",
-        "nan": "2.6.2",
+        "nan": "2.8.0",
         "node-gyp": "3.6.2",
         "npmlog": "4.1.2",
-        "request": "2.81.0",
-        "sass-graph": "2.2.4"
+        "request": "2.79.0",
+        "sass-graph": "2.2.4",
+        "stdout-stream": "1.4.0",
+        "true-case-path": "1.0.2"
       }
     },
     "nopt": {
@@ -4964,7 +4977,7 @@
       "integrity": "sha1-xkZdvwirzU2zWTF/eaxopkayj/k=",
       "dev": true,
       "requires": {
-        "abbrev": "1.1.0"
+        "abbrev": "1.1.1"
       }
     },
     "normalize-package-data": {
@@ -5367,12 +5380,6 @@
         "sha.js": "2.4.8"
       }
     },
-    "performance-now": {
-      "version": "0.2.0",
-      "resolved": "https://registry.npmjs.org/performance-now/-/performance-now-0.2.0.tgz",
-      "integrity": "sha1-M+8wxcd9TqIcWlOGnZG1bY8lVeU=",
-      "dev": true
-    },
     "pify": {
       "version": "2.3.0",
       "resolved": "https://registry.npmjs.org/pify/-/pify-2.3.0.tgz",
@@ -6080,6 +6087,15 @@
         "react-pure-render": "1.0.2"
       }
     },
+    "redux-devtools-filter-actions": {
+      "version": "1.2.2",
+      "resolved": "https://registry.npmjs.org/redux-devtools-filter-actions/-/redux-devtools-filter-actions-1.2.2.tgz",
+      "integrity": "sha1-Doo9lWd+bpZqCdShTNIfWM7rxow=",
+      "dev": true,
+      "requires": {
+        "lodash": "4.17.4"
+      }
+    },
     "redux-devtools-instrument": {
       "version": "1.8.2",
       "resolved": "https://registry.npmjs.org/redux-devtools-instrument/-/redux-devtools-instrument-1.8.2.tgz",
@@ -6255,19 +6271,19 @@
       }
     },
     "request": {
-      "version": "2.81.0",
-      "resolved": "https://registry.npmjs.org/request/-/request-2.81.0.tgz",
-      "integrity": "sha1-xpKJRqDgbF+Nb4qTM0af/aRimKA=",
+      "version": "2.79.0",
+      "resolved": "https://registry.npmjs.org/request/-/request-2.79.0.tgz",
+      "integrity": "sha1-Tf5b9r6LjNw3/Pk+BLZVd3InEN4=",
       "dev": true,
       "requires": {
         "aws-sign2": "0.6.0",
         "aws4": "1.6.0",
-        "caseless": "0.12.0",
+        "caseless": "0.11.0",
         "combined-stream": "1.0.5",
         "extend": "3.0.1",
         "forever-agent": "0.6.1",
         "form-data": "2.1.4",
-        "har-validator": "4.2.1",
+        "har-validator": "2.0.6",
         "hawk": "3.1.3",
         "http-signature": "1.1.1",
         "is-typedarray": "1.0.0",
@@ -6275,13 +6291,19 @@
         "json-stringify-safe": "5.0.1",
         "mime-types": "2.1.15",
         "oauth-sign": "0.8.2",
-        "performance-now": "0.2.0",
-        "qs": "6.4.0",
-        "safe-buffer": "5.1.1",
+        "qs": "6.3.2",
         "stringstream": "0.0.5",
-        "tough-cookie": "2.3.2",
-        "tunnel-agent": "0.6.0",
+        "tough-cookie": "2.3.3",
+        "tunnel-agent": "0.4.3",
         "uuid": "3.1.0"
+      },
+      "dependencies": {
+        "qs": {
+          "version": "6.3.2",
+          "resolved": "https://registry.npmjs.org/qs/-/qs-6.3.2.tgz",
+          "integrity": "sha1-51vV9uJoEioqDgvaYwslUMFmUCw=",
+          "dev": true
+        }
       }
     },
     "require-directory": {
@@ -6921,6 +6943,15 @@
       "integrity": "sha1-+vUbnrdKrvOzrPStX2Gr8ky3uT4=",
       "dev": true
     },
+    "stdout-stream": {
+      "version": "1.4.0",
+      "resolved": "https://registry.npmjs.org/stdout-stream/-/stdout-stream-1.4.0.tgz",
+      "integrity": "sha1-osfIWH5U2UJ+qe2zrD8s1SLfN4s=",
+      "dev": true,
+      "requires": {
+        "readable-stream": "2.3.3"
+      }
+    },
     "stream-browserify": {
       "version": "2.0.1",
       "resolved": "https://registry.npmjs.org/stream-browserify/-/stream-browserify-2.0.1.tgz",
@@ -6944,15 +6975,6 @@
         "xtend": "4.0.1"
       }
     },
-    "string_decoder": {
-      "version": "1.0.3",
-      "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.0.3.tgz",
-      "integrity": "sha512-4AH6Z5fzNNBcH+6XDMfA/BTt87skxqJlO0lAh3Dker5zThcAxG6mKz+iGu308UKoPPQ8Dcqx/4JhujzltRa+hQ==",
-      "dev": true,
-      "requires": {
-        "safe-buffer": "5.1.1"
-      }
-    },
     "string-width": {
       "version": "1.0.2",
       "resolved": "https://registry.npmjs.org/string-width/-/string-width-1.0.2.tgz",
@@ -6964,6 +6986,15 @@
         "strip-ansi": "3.0.1"
       }
     },
+    "string_decoder": {
+      "version": "1.0.3",
+      "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.0.3.tgz",
+      "integrity": "sha512-4AH6Z5fzNNBcH+6XDMfA/BTt87skxqJlO0lAh3Dker5zThcAxG6mKz+iGu308UKoPPQ8Dcqx/4JhujzltRa+hQ==",
+      "dev": true,
+      "requires": {
+        "safe-buffer": "5.1.1"
+      }
+    },
     "stringformat": {
       "version": "0.0.5",
       "resolved": "https://registry.npmjs.org/stringformat/-/stringformat-0.0.5.tgz",
@@ -7158,9 +7189,9 @@
       "dev": true
     },
     "tough-cookie": {
-      "version": "2.3.2",
-      "resolved": "https://registry.npmjs.org/tough-cookie/-/tough-cookie-2.3.2.tgz",
-      "integrity": "sha1-8IH3bkyFcg5sN6X6ztc3FQ2EByo=",
+      "version": "2.3.3",
+      "resolved": "https://registry.npmjs.org/tough-cookie/-/tough-cookie-2.3.3.tgz",
+      "integrity": "sha1-C2GKVWW23qkL80JdBNVe3EdadWE=",
       "dev": true,
       "requires": {
         "punycode": "1.4.1"
@@ -7178,6 +7209,30 @@
       "integrity": "sha1-yy4SAwZ+DI3h9hQJS5/kVwTqYAM=",
       "dev": true
     },
+    "true-case-path": {
+      "version": "1.0.2",
+      "resolved": "https://registry.npmjs.org/true-case-path/-/true-case-path-1.0.2.tgz",
+      "integrity": "sha1-fskRMJJHZsf1c74wIMNPj9/QDWI=",
+      "dev": true,
+      "requires": {
+        "glob": "6.0.4"
+      },
+      "dependencies": {
+        "glob": {
+          "version": "6.0.4",
+          "resolved": "https://registry.npmjs.org/glob/-/glob-6.0.4.tgz",
+          "integrity": "sha1-DwiGD2oVUSey+t1PnOJLGqtuTSI=",
+          "dev": true,
+          "requires": {
+            "inflight": "1.0.6",
+            "inherits": "2.0.3",
+            "minimatch": "3.0.4",
+            "once": "1.4.0",
+            "path-is-absolute": "1.0.1"
+          }
+        }
+      }
+    },
     "tryit": {
       "version": "1.0.3",
       "resolved": "https://registry.npmjs.org/tryit/-/tryit-1.0.3.tgz",
@@ -7191,13 +7246,10 @@
       "dev": true
     },
     "tunnel-agent": {
-      "version": "0.6.0",
-      "resolved": "https://registry.npmjs.org/tunnel-agent/-/tunnel-agent-0.6.0.tgz",
-      "integrity": "sha1-J6XeoGs2sEoKmWZ3SykIaPD8QP0=",
-      "dev": true,
-      "requires": {
-        "safe-buffer": "5.1.1"
-      }
+      "version": "0.4.3",
+      "resolved": "https://registry.npmjs.org/tunnel-agent/-/tunnel-agent-0.4.3.tgz",
+      "integrity": "sha1-Y3PbdpCf5XDgjXNYM2Xtgop07us=",
+      "dev": true
     },
     "tweetnacl": {
       "version": "0.14.5",
@@ -7410,12 +7462,22 @@
       "dev": true
     },
     "verror": {
-      "version": "1.3.6",
-      "resolved": "https://registry.npmjs.org/verror/-/verror-1.3.6.tgz",
-      "integrity": "sha1-z/XfEpRtKX0rqu+qJoniW+AcAFw=",
+      "version": "1.10.0",
+      "resolved": "https://registry.npmjs.org/verror/-/verror-1.10.0.tgz",
+      "integrity": "sha1-OhBcoXBTr1XW4nDB+CiGguGNpAA=",
       "dev": true,
       "requires": {
-        "extsprintf": "1.0.2"
+        "assert-plus": "1.0.0",
+        "core-util-is": "1.0.2",
+        "extsprintf": "1.3.0"
+      },
+      "dependencies": {
+        "assert-plus": {
+          "version": "1.0.0",
+          "resolved": "https://registry.npmjs.org/assert-plus/-/assert-plus-1.0.0.tgz",
+          "integrity": "sha1-8S4PPF13sLHN2RRpQuTpbB5N1SU=",
+          "dev": true
+        }
       }
     },
     "vm-browserify": {

+ 1 - 0
frontend/package.json

@@ -41,6 +41,7 @@
     "react-trigger-change": "^1.0.2",
     "redux-devtools": "^3.3.1",
     "redux-devtools-dock-monitor": "^1.1.1",
+    "redux-devtools-filter-actions": "^1.2.2",
     "redux-devtools-log-monitor": "^1.1.1",
     "redux-logger": "^2.7.4",
     "sass-loader": "^4.0.2",