Browse Source

Worked more on station pages. They are more functional now, still very broken.

KrisVos130 7 years ago
parent
commit
6e626b43eb

+ 24 - 0
frontend/app/js/actions/volume.js

@@ -0,0 +1,24 @@
+export const INITIALIZE = "INITIALIZE";
+export const CHANGE_VOLUME = "CHANGE_VOLUME";
+export const CHANGE_VOLUME_MUTED = "CHANGE_VOLUME_MUTED";
+
+export function initialize(volume) {
+	return {
+		type: INITIALIZE,
+		volume,
+	};
+}
+
+export function changeVolume(volume) {
+	return {
+		type: CHANGE_VOLUME,
+		volume,
+	};
+}
+
+export function changeVolumeMuted(muted) {
+	return {
+		type: CHANGE_VOLUME_MUTED,
+		muted,
+	};
+}

+ 12 - 5
frontend/app/js/app.jsx

@@ -5,6 +5,7 @@ import { connect } from "react-redux";
 import { translate } from "react-i18next";
 
 import { ban, authenticate } from "actions/auth";
+import { initialize as initializeVolume } from "actions/volume";
 import Navbar from "components/Global/Navbar";
 
 import config from "config";
@@ -46,6 +47,12 @@ class App extends Component { // eslint-disable-line react/no-multi-comp
 			// TODO
 			localStorage.removeItem("github_redirect");
 		}
+
+		let volume = parseFloat(localStorage.getItem("volume"));
+		volume = (typeof volume === "number" && !isNaN(volume)) ? volume : 20;
+		console.log("VOLUME", volume);
+		localStorage.setItem("volume", volume);
+		dispatch(initializeVolume(volume));
 	}
 
 	render() {
@@ -151,12 +158,12 @@ class App extends Component { // eslint-disable-line react/no-multi-comp
 						auth="ignored"
 					/>
 					<AuthRoute
-						path="/c/:station"
+						path="/community/:name"
 						component={ asyncComponent({
 							resolve: () => System.import("views/Station"),
-							name: "Station"
+							name: "Station",
 						})}
-						auth="ignored"
+						auth="station"
 						title="TODO"
 					/>
 					<AuthRoute
@@ -164,7 +171,7 @@ class App extends Component { // eslint-disable-line react/no-multi-comp
 						path="/"
 						component={ asyncComponent({
 							resolve: () => System.import("views/Home"),
-							name: "Home"
+							name: "Home",
 						})}
 						auth="ignored"
 						title={ t("pages:homepage") }
@@ -173,7 +180,7 @@ class App extends Component { // eslint-disable-line react/no-multi-comp
 						path="*"
 						component={ asyncComponent({
 							resolve: () => System.import("views/Errors/Error404"),
-							name: "Error404"
+							name: "Error404",
 						})}
 						auth="ignored"
 						title={ t("pages:error404") }

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

@@ -22,6 +22,10 @@ const PropsRoute = ({ component, ...rest }) => {
 };
 // Above two functions are from https://github.com/ReactTraining/react-router/issues/4105#issuecomment-289195202
 
+function clone(obj) {
+	return Object.assign({}, obj);
+}
+
 @connect(state => ({
 	loggedIn: state.user.get("loggedIn"),
 	role: state.user.get("role"),
@@ -65,14 +69,16 @@ export default class AuthRoute extends Component {
 			receivedStationData: false,
 		};
 		const { auth } = props;
+		let getStationData = false;
 
 		if (auth === "ignored") state.continue = true;
 		else if (auth === "station") {
 			state.waitingFor = "station";
-			this.getStationData();
+			getStationData = true;
 		} else state.waitingFor = "auth";
-
 		this.state = state;
+
+		if (getStationData) this.getStationData();
 	}
 
 	componentWillUpdate(nextProps) {
@@ -98,11 +104,12 @@ export default class AuthRoute extends Component {
 			return <PropsRoute props={ this.props } component={ this.props.component }/>
 		} else if (waitingFor === "station" && receivedStationData) {
 			if (stationData) {
-				const props = JSON.parse(JSON.stringify(this.props));
+				const props = clone(this.props);
 				// TODO Replace the above hack with a proper Object.clone
 				props.stationName = stationName;
 				props.stationData = stationData;
-				return <Route props={ props } component={ this.props.component } />;
+				window.props = props; //TODO Replace
+				return <Route component={ this.props.component } />;
 			}
 			return <Redirect to={ "/" } />;
 		} else if (waitingFor === "auth" && authProcessed) {

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

@@ -20,7 +20,7 @@ let store = null;
 const middleware = applyMiddleware(thunk);
 store = createStore(
 	rootReducer,
-	middleware
+	window.__REDUX_DEVTOOLS_EXTENSION__ && window.__REDUX_DEVTOOLS_EXTENSION__() //middleware //TODO See what the middleware does
 );
 
 ReactDOM.render(

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

@@ -1,6 +1,8 @@
 import { combineReducers } from "redux";
 import user from "reducers/user";
+import volume from "reducers/volume";
 
 export default combineReducers({
 	user,
+	volume,
 });

+ 35 - 0
frontend/app/js/reducers/volume.js

@@ -0,0 +1,35 @@
+import { Map } from "immutable";
+
+import {
+	INITIALIZE,
+	CHANGE_VOLUME,
+	CHANGE_VOLUME_MUTED,
+} from "actions/volume";
+
+const initialState = Map({
+	volume: 0,
+	muted: false, //TODO Store muted and initialize it
+});
+
+const actionsMap = {
+	[INITIALIZE]: (state, action) => {
+		return state.merge({
+			volume: action.volume,
+		});
+	},
+	[CHANGE_VOLUME]: (state, action) => {
+		return state.merge({
+			volume: action.volume,
+		});
+	},
+	[CHANGE_VOLUME_MUTED]: (state, action) => {
+		return state.merge({
+			muted: action.muted,
+		});
+	},
+};
+
+export default function reducer(state = initialState, action = {}) {
+	const fn = actionsMap[action.type];
+	return fn ? fn(state, action) : state;
+}

+ 75 - 44
frontend/app/js/views/Station/Player.jsx

@@ -2,8 +2,14 @@ import React, { Component } from "react";
 import PropTypes from "prop-types";
 const i18next = require("i18next");
 
+import { connect } from "react-redux";
+
 const t = i18next.t;
+let getPlayerCallbacks = [];
 
+@connect(state => ({
+	volume: state.volume.get("volume"),
+}))
 export default class Player extends Component {
 	static propTypes = {
 		onRef: PropTypes.func,
@@ -24,20 +30,28 @@ export default class Player extends Component {
 				paused: true,
 				pausedAt: null, // Find better spot for this one
 			},
-			volume: 0,
 		};
 	}
 
 	componentDidMount() {
 		this.props.onRef(this);
+		this.setState({
+			seekerbar: this.seekerbar,
+		});
 		this.initializePlayer();
 	}
 	componentWillUnmount() {
 		this.props.onRef(null);
 	}
 
-	playSong(songId, skipDuration, timePaused, startedAt) {
-		if (this.state.player.ready) {
+	clearSong() {
+		this.getPlayer((player) => {
+			player.loadVideoById("");
+		});
+	}
+
+	playSong(songId, skipDuration, timePaused, startedAt, cb) {
+		this.getPlayer((player) => {
 			let pausedAt = (this.state.player.paused) ? Date.now() : null;
 			this.setState({
 				song: {
@@ -53,8 +67,9 @@ export default class Player extends Component {
 				},
 			});
 
-			this.player.loadVideoById(songId, this.getProperVideoTime());
-		} else return; // CALLBACK
+			player.loadVideoById(songId, this.getProperVideoTime());
+			cb();
+		});
 	}
 
 	getProperVideoTime = () => {
@@ -73,33 +88,30 @@ export default class Player extends Component {
 	};
 
 	pause() {
-		if (this.state.player.paused) return;
-		this.setState({
-			player: {
-				...this.state.player,
-				paused: true,
-				pausedAt: Date.now(),
-			},
+		this.getPlayer((player) => {
+			if (this.state.player.paused) return;
+			this.setState({
+				player: {
+					...this.state.player,
+					paused: true,
+					pausedAt: Date.now(),
+				},
+			});
+			player.pauseVideo();
 		});
-		this.player.pauseVideo();
 	}
 
 	resume() {
-		if (!this.state.player.paused) return;
-		this.setState({
-			player: {
-				...this.state.player,
-				paused: false,
-			},
+		this.getPlayer((player) => {
+			if (!this.state.player.paused) return;
+			this.setState({
+				player: {
+					...this.state.player,
+					paused: false,
+				},
+			});
+			player.playVideo();
 		});
-		this.player.playVideo();
-	}
-
-	initializePlayerVolume() {
-		let volume = parseInt(localStorage.getItem("volume"));
-		volume = (typeof volume === "number") ? volume : 20;
-		this.player.setVolume(this.state.volume);
-		if (this.state.volume > 0) this.player.unMute();
 	}
 
 	initializePlayer = () => {
@@ -127,33 +139,52 @@ export default class Player extends Component {
 						},
 					});
 
-					this.initializePlayerVolume();
+					getPlayerCallbacks.forEach((cb) => {
+						cb(this.player);
+					});
+
+					this.player.setVolume(this.props.volume);
 				},
 				"onError": function(err) {
 					console.log("iframe error", err);
 					// VOTE TO SKIP SONG
 				},
 				"onStateChange": (event) => {
-					if (event.data === YT.PlayerState.PLAYING) {
-						if (this.state.player.loading) this.setState({
-							player: {
-								...this.state.player,
-								loading: false,
-							},
-						});
-						if (this.state.player.paused) this.player.pauseVideo();
-						if (this.state.player.paused || this.state.player.loading) this.player.seekTo(this.getProperVideoTime(), true);
-					}
-
-					if (event.data === YT.PlayerState.PAUSED) {
-						if (!this.state.player.paused) {
-							this.player.seekTo(this.getProperVideoTime(), true);
-							this.player.playVideo();
+					this.getPlayer((player) => {
+						if (event.data === YT.PlayerState.PLAYING) {
+							if (this.state.player.loading) this.setState({
+								player: {
+									...this.state.player,
+									loading: false,
+								},
+							});
+							if (this.state.player.paused) player.pauseVideo();
+							if (this.state.player.paused || this.state.player.loading) player.seekTo(this.getProperVideoTime(), true);
+						}
+
+						if (event.data === YT.PlayerState.PAUSED) {
+							if (!this.state.player.paused) {
+								player.seekTo(this.getProperVideoTime(), true);
+								player.playVideo();
+							}
 						}
-					}
+					});
 				},
 			},
 		});
+	};
+
+	getPlayer(cb) {
+		if (!this.state.player.ready) getPlayerCallbacks.push(cb);
+		else cb(this.player);
+	};
+
+	componentWillUpdate(nextProps) {
+		if (nextProps.volume !== this.props.volume) {
+			this.getPlayer((player) => {
+				player.setVolume(nextProps.volume);
+			});
+		}
 	}
 
 	render() {

+ 63 - 0
frontend/app/js/views/Station/Seekerbar.jsx

@@ -0,0 +1,63 @@
+import React, { Component } from "react";
+import PropTypes from "prop-types";
+
+export default class Seekerbar extends Component {
+	static propTypes = {
+		onRef: PropTypes.func,
+	};
+
+	static defaultProps = {
+		onRef: () => {},
+	};
+
+	constructor(props) {
+		super(props);
+
+		this.state = {
+			timeTotal: 0,
+			timeElapsed: 0,
+			timeElapsedGuess: 0,
+			percentage: 0,
+		};
+
+		setInterval(() => {
+			let timeElapsedGuess = this.state.timeElapsedGuess;
+			timeElapsedGuess += 15;
+
+			if (timeElapsedGuess <= this.state.timeElapsed) {
+				timeElapsedGuess = this.state.timeElapsed;
+			}
+
+			this.setState({
+				percentage: (timeElapsedGuess / this.state.timeTotal) * 100,
+				timeElapsedGuess,
+			});
+		}, 50);
+	}
+
+	componentDidMount() {
+		this.props.onRef(this);
+	}
+	componentWillUnmount() {
+		this.props.onRef(null);
+	}
+
+	setTime = (timeTotal) => {
+		this.setState({
+			timeTotal: timeTotal * 1000,
+			timeElapsed: 0,
+		});
+	};
+
+	setTimeElapsed = (time) => {
+		this.setState({
+			timeElapsed: time * 1000,
+		});
+	};
+
+	render() {
+		return (
+			<span style={{"width": this.state.percentage + "%", "background-color": "blue", "height": "100%", "display": "inline-block"}}/>
+		);
+	}
+}

+ 48 - 0
frontend/app/js/views/Station/Time.jsx

@@ -0,0 +1,48 @@
+import React, { Component } from "react";
+import PropTypes from "prop-types";
+
+const formatTime = (duration) => {
+	let d = moment.duration(duration, "seconds");
+	if (duration < 0) return "0:00";
+	return ((d.hours() > 0) ? (d.hours() < 10 ? ("0" + d.hours() + ":") : (d.hours() + ":")) : "") + (d.minutes() + ":") + (d.seconds() < 10 ? ("0" + d.seconds()) : d.seconds());
+};
+
+export default class Time extends Component {
+	static propTypes = {
+		onRef: PropTypes.func,
+	};
+
+	static defaultProps = {
+		onRef: () => {},
+	};
+
+	constructor(props) {
+		super(props);
+
+		this.state = {
+			time: 0,
+		};
+	}
+
+	componentDidMount() {
+		this.props.onRef(this);
+	}
+	componentWillUnmount() {
+		this.props.onRef(null);
+	}
+
+	formatTime = formatTime;
+	static formatTime = formatTime;
+
+	setTime = (time) => {
+		this.setState({
+			time,
+		});
+	};
+
+	render() {
+		return (
+			<span>{ this.formatTime(this.state.time) }</span>
+		);
+	}
+}

+ 46 - 0
frontend/app/js/views/Station/VolumeSlider.jsx

@@ -0,0 +1,46 @@
+import React, { Component } from "react";
+import PropTypes from "prop-types";
+
+import { connect } from "react-redux";
+
+import { changeVolume, changeVolumeMuted } from "actions/volume";
+
+@connect(state => ({
+	volume: state.volume.get("volume"),
+	muted: state.volume.get("muted"),
+}))
+export default class VolumeSlider extends Component {
+	constructor(props) {
+		super(props);
+	}
+
+	changeVolumeHandler = (e) => {
+		let volume = e.target.value / 100;
+		this.props.dispatch(changeVolume(volume));
+	};
+
+	muteVolume = () => {
+		this.props.dispatch(changeVolumeMuted(true));
+	};
+
+	unmuteVolume = () => {
+		this.props.dispatch(changeVolumeMuted(false));
+	};
+
+	render() {
+		return (
+			<div className="volume-container">
+				<h2>{ this.props.volume }. Muted: { (this.props.muted) ? "true" : "false" }</h2>
+				{
+					(this.props.muted) ? ([
+						<span key="unmuteButton" onClick={ this.unmuteVolume }>UNMUTE</span>,
+						<input key="disabledVolumeInput" type="range" min="0" max="10000" value="0" disabled/>,
+					]) : ([
+						<span key="muteButton" onClick={ this.muteVolume }>MUTE</span>,
+						<input key="volumeInput" type="range" min="0" max="10000" onChange={ this.changeVolumeHandler }/>, //Add default value
+					])
+				}
+			</div>
+		);
+	}
+}

+ 138 - 4
frontend/app/js/views/Station/index.jsx

@@ -5,18 +5,28 @@ import PropTypes from "prop-types";
 import { translate, Trans } from "react-i18next";
 
 import Player from "./Player";
+import Time from "./Time";
+import Seekerbar from "./Seekerbar";
+import VolumeSlider from "./VolumeSlider";
+
+import { changeVolume } from "actions/volume";
 
 import { connect } from "react-redux";
 
 import io from "io";
 import config from "config";
 
+const Aux = (props) => {
+	return props.children;
+};
+
 @connect(state => ({
 	user: {
 		userId: state.user.get("userId"),
 		role: state.user.get("role"),
 	},
 	loggedIn: state.user.get("loggedIn"),
+	volume: state.volume.get("volume"),
 }))
 
 @translate(["station"], { wait: true })
@@ -32,15 +42,102 @@ export default class Station extends Component {
 	constructor() {
 		super();
 
-		this.state = {
+		let temp = window.props;
+		let stationName = temp.stationName;
 
+		this.state = {
+			station: temp.stationData,
+			currentSongExists: false,
 		};
 
 		io.getSocket(socket => {
-
+			socket.emit("stations.join", stationName, res => {
+				console.log(res);
+				if (res.status === 'success') {
+					this.setState({
+						station: { //TODO Refactor this to be better optimized
+							_id: res.data._id,
+							name: stationName,
+							displayName: res.data.displayName,
+							description: res.data.description,
+							privacy: res.data.privacy,
+							locked: res.data.locked,
+							partyMode: res.data.partyMode,
+							owner: res.data.owner,
+							privatePlaylist: res.data.privatePlaylist,
+							type: res.data.type,
+							paused: res.data.paused,
+						},
+						currentSong: (res.data.currentSong) ? res.data.currentSong : {},
+						currentSongExists: !!res.data.currentSong,
+					});
+
+					if (res.data.paused) this.player.pause(); //TODO Add async getPlayer here
+					else this.player.resume();
+
+					if (res.data.currentSong) {
+						res.data.currentSong.startedAt = res.data.startedAt;
+						res.data.currentSong.timePaused = res.data.timePaused;
+					}
+					this.changeSong(res.data.currentSong);
+				}
+			});
 		});
+
+		setInterval(() => {
+			if (this.state.currentSongExists) {
+				this.time.setTime(this.player.getTimeElapsed() / 1000);
+				this.seekerbar.setTimeElapsed(this.player.getTimeElapsed() / 1000);
+			}
+		}, 1000);
 	}
 
+	changeSong = (newSongObject) => {
+		let currentSongExists = !!newSongObject;
+		let state = {
+			currentSongExists,
+			currentSong: newSongObject,
+		};
+
+		if (currentSongExists) {
+			state.timeTotal = Time.formatTime(newSongObject.duration);
+			state.simpleSong = (newSongObject.likes === -1 && newSongObject.dislikes === -1);
+			if (state.simpleSong) {
+				state.currentSong.skipDuration = 0;
+				newSongObject.skipDuration = 0;// Do this better
+			}
+
+			this.seekerbar.setTime(newSongObject.duration);
+
+			this.player.playSong(newSongObject.songId, newSongObject.skipDuration, newSongObject.timePaused, newSongObject.startedAt, () => {
+				this.seekerbar.setTimeElapsed(this.player.getTimeElapsed() / 1000);
+			});
+		} else {
+			this.player.clearSong();
+		}
+
+		this.setState(state, () => {
+			this.getOwnRatings();
+		});
+	};
+
+	getOwnRatings = () => {
+		io.getSocket((socket) => {
+			if (!this.state.currentSongExists) return;
+			socket.emit('songs.getOwnSongRatings', this.state.currentSong.songId, (data) => {
+				if (this.state.currentSong.songId === data.songId) {
+					this.setState({
+						currentSong: {
+							...this.state.currentSong,
+							liked: data.liked,
+							disliked: data.disliked,
+						},
+					});
+				}
+			});
+		});
+	};
+
 	isOwner = (ownerId) => {
 		if (this.props.loggedIn) {
 			if (this.props.user.role === "admin") return true;
@@ -54,6 +151,18 @@ export default class Station extends Component {
 		this.player.playSong("jbZXYhjh3ms", 0, 0, Date.now());
 	};
 
+	addSongTemp = () => {
+		io.getSocket(socket => {
+			socket.emit('stations.addToQueue', this.state.station._id, '60ItHLz5WEA', data => {
+				console.log("ATQ Res", data);
+			});
+		});
+	};
+
+	changeVolume = () => {
+		this.props.dispatch(changeVolume(32))
+	};
+
 	render() {
 		const { t } = this.props;
 
@@ -61,13 +170,38 @@ export default class Station extends Component {
 
 		return (
 			<main id="station">
-				<h1>{ t("home:title") }</h1>
+				<h1>{ this.state.station.displayName }</h1>
+
+
+				<button onClick={ this.changeVolume }>Change volume</button>
 
 				<button onClick={ this.changeId }>Change ID</button>
 				<button onClick={ () => { this.player.pause() } }>Pause</button>
 				<button onClick={ () => { this.player.resume() } }>Resume</button>
 
-				<Player onRef={ ref => (this.player = ref) }/>
+				<div className={(!this.state.currentSongExists) ? "hidden" : ""}>
+					<Player onRef={ ref => (this.player = ref) }/>
+				</div>
+
+				{ (this.state.currentSongExists) ? (
+				[
+					<span key="title">{ this.state.currentSong.title }</span>,
+					<br key="br1"/>,
+					<span key="artists">{ this.state.currentSong.artists.join(", ") }</span>,
+					<span key="time">
+						<Time onRef={ ref => (this.time = ref) }/> - { Time.formatTime(this.state.currentSong.duration) }
+					</span>,
+					<div key="seekerbar" className="seekerbar-container" style={{"width": "100%", "background-color": "yellow", "height": "20px", "display": "block"}}>
+						<Seekerbar onRef={ ref => (this.seekerbar = ref) }/>
+					</div>,
+				]) : (
+					<h1>No song playing</h1>
+				) }
+
+
+				<VolumeSlider key="volumeSlider"/>,
+
+				<button onClick={ this.addSongTemp }>Add song to queue TEMP</button>
 			</main>
 		);
 	}

+ 4 - 0
frontend/app/styles/main.scss

@@ -44,6 +44,10 @@ h1 {
 	margin-bottom: 16px;
 }
 
+.hidden {
+	display: none;
+}
+
 main {
 	margin-left: auto;
 	margin-right: auto;