Переглянути джерело

Merge branch 'backup/polishing' into polishing

Kristian Vos 4 роки тому
батько
коміт
f417d2cc25

+ 10 - 2
frontend/src/components/layout/MainHeader.vue

@@ -12,7 +12,9 @@
 		<span
 			class="nav-toggle"
 			:class="{ 'is-active': isMobile }"
+			tabindex="0"
 			@click="isMobile = !isMobile"
+			@keyup.enter="isMobile = !isMobile"
 		>
 			<span />
 			<span />
@@ -124,6 +126,7 @@ export default {
 	flex-shrink: 0;
 	background-color: $primary-color;
 	height: 64px;
+	overflow: hidden;
 	border-radius: 0% 0% 33% 33% / 0% 0% 7% 7%;
 
 	.nav-menu.is-active {
@@ -145,8 +148,13 @@ export default {
 	.nav-toggle {
 		height: 64px;
 
-		&.is-active span {
-			background-color: $dark-grey-2;
+		&:hover,
+		&:active {
+			background-color: darken($musare-blue, 10%);
+		}
+
+		span {
+			background-color: $white;
 		}
 	}
 

+ 12 - 140
frontend/src/components/modals/EditSong.vue

@@ -514,22 +514,8 @@
 				</button>
 			</div>
 		</modal>
-		<div
-			id="genre-helper-container"
-			:style="{
-				width: genreHelper.width + 'px',
-				height: genreHelper.height + 'px',
-				top: genreHelper.top + 'px',
-				left: genreHelper.left + 'px'
-			}"
-			v-if="genreHelper.shown"
-			@mousedown="onResizeGenreHelper"
-		>
-			<div
-				class="genre-helper-header"
-				@mousedown="onDragGenreHelper"
-			></div>
-			<div class="genre-helper-body">
+		<floating-box id="genreHelper" ref="genreHelper">
+			<template #body>
 				<span>Blues</span><span>Country</span><span>Disco</span
 				><span>Funk</span><span>Hip-Hop</span><span>Jazz</span
 				><span>Metal</span><span>Oldies</span><span>Other</span
@@ -542,8 +528,8 @@
 				><span>Drum & Bass</span><span>Club-House</span
 				><span>Indie</span><span>Heavy Metal</span
 				><span>Christian rock</span><span>Dubstep</span>
-			</div>
-		</div>
+			</template>
+		</floating-box>
 	</div>
 </template>
 
@@ -555,11 +541,13 @@ import io from "../../io";
 import keyboardShortcuts from "../../keyboardShortcuts";
 import validation from "../../validation";
 import Modal from "../Modal.vue";
+import FloatingBox from "../ui/FloatingBox.vue";
 
 export default {
-	components: { Modal },
+	components: { Modal, FloatingBox },
 	data() {
 		return {
+			focusedElementBefore: null,
 			discogsQuery: "",
 			youtubeVideoDuration: 0.0,
 			youtubeVideoCurrentTime: 0.0,
@@ -582,17 +570,6 @@ export default {
 			keydownGenreInputTimeout: 0,
 			artistAutosuggestItems: [],
 			genreAutosuggestItems: [],
-			genreHelper: {
-				width: 200,
-				height: 200,
-				top: 0,
-				left: 0,
-				shown: false,
-				pos1: 0,
-				pos2: 0,
-				pos3: 0,
-				pos4: 0
-			},
 			genres: [
 				"Blues",
 				"Country",
@@ -1071,79 +1048,11 @@ export default {
 			ctx.fillStyle = currentDurationColor;
 			ctx.fillRect(widthCurrentTime, 0, 1, 20);
 		},
-		onDragGenreHelper(e) {
-			const e1 = e || window.event;
-			e1.preventDefault();
-
-			this.genreHelper.pos3 = e1.clientX;
-			this.genreHelper.pos4 = e1.clientY;
-
-			document.onmousemove = e => {
-				const e2 = e || window.event;
-				e2.preventDefault();
-				// calculate the new cursor position:
-				this.genreHelper.pos1 = this.genreHelper.pos3 - e.clientX;
-				this.genreHelper.pos2 = this.genreHelper.pos4 - e.clientY;
-				this.genreHelper.pos3 = e.clientX;
-				this.genreHelper.pos4 = e.clientY;
-				// set the element's new position:
-				this.genreHelper.top -= this.genreHelper.pos2;
-				this.genreHelper.left -= this.genreHelper.pos1;
-			};
-
-			document.onmouseup = () => {
-				document.onmouseup = null;
-				document.onmousemove = null;
-
-				this.saveGenreHelper();
-			};
-		},
-		onResizeGenreHelper(e) {
-			if (e.target.id !== "genre-helper-container") return;
-
-			document.onmouseup = () => {
-				document.onmouseup = null;
-
-				const { height, width } = e.target.style;
-
-				this.genreHelper.height = Number(
-					height
-						.split("")
-						.splice(0, height.length - 2)
-						.join("")
-				);
-				this.genreHelper.width = Number(
-					width
-						.split("")
-						.splice(0, width.length - 2)
-						.join("")
-				);
-
-				this.saveGenreHelper();
-			};
-		},
 		toggleGenreHelper() {
-			this.genreHelper.shown = !this.genreHelper.shown;
-			this.saveGenreHelper();
+			this.$refs.genreHelper.toggleBox();
 		},
 		resetGenreHelper() {
-			this.genreHelper.top = 0;
-			this.genreHelper.left = 0;
-			this.genreHelper.width = 200;
-			this.genreHelper.height = 200;
-			this.saveGenreHelper();
-		},
-		saveGenreHelper() {
-			localStorage.setItem(
-				"genreHelper",
-				JSON.stringify({
-					height: this.genreHelper.height,
-					width: this.genreHelper.width,
-					top: this.genreHelper.top,
-					left: this.genreHelper.left,
-					shown: this.genreHelper.shown
-				})
-			);
+			this.$refs.genreHelper.resetBox();
 		},
 		...mapActions("admin/songs", [
 			"stopVideo",
@@ -1164,15 +1073,6 @@ export default {
 		//   this.editing.song.skipDuration
 		// );
 
-		if (localStorage.genreHelper) {
-			const genreHelper = JSON.parse(localStorage.getItem("genreHelper"));
-			this.genreHelper.height = genreHelper.height;
-			this.genreHelper.width = genreHelper.width;
-			this.genreHelper.top = genreHelper.top;
-			this.genreHelper.left = genreHelper.left;
-			this.genreHelper.shown = genreHelper.shown;
-		}
-
 		this.discogsQuery = this.editing.song.title;
 
 		lofig.get("cookie.secure").then(useHTTPS => {
@@ -1386,6 +1286,9 @@ export default {
 					sector: "admin",
 					modal: "editSong"
 				});
+				setTimeout(() => {
+					window.focusedElementBefore.focus();
+				}, 500);
 			}
 		});
 
@@ -1482,37 +1385,6 @@ export default {
 <style lang="scss">
 @import "../../styles/global.scss";
 
-#genre-helper-container {
-	background-color: white;
-	position: fixed;
-	z-index: 10000000;
-	resize: both;
-	overflow: auto;
-	border: 1px solid #d3d3d3;
-	min-height: 50px !important;
-	min-width: 50px !important;
-
-	.genre-helper-header {
-		cursor: move;
-		z-index: 100000001;
-		background-color: $musare-blue;
-		padding: 10px;
-		display: block;
-		height: 10px;
-		width: 100%;
-	}
-
-	.genre-helper-body {
-		display: flex;
-		flex-wrap: wrap;
-		justify-content: space-evenly;
-
-		span {
-			padding: 3px 6px;
-		}
-	}
-}
-
 .song-modal {
 	.modal-card-title {
 		text-align: center;

+ 165 - 0
frontend/src/components/ui/FloatingBox.vue

@@ -0,0 +1,165 @@
+<template>
+	<div
+		ref="box"
+		class="box"
+		:id="id"
+		v-if="shown"
+		:style="{
+			width: width + 'px',
+			height: height + 'px',
+			top: top + 'px',
+			left: left + 'px'
+		}"
+		@mousedown="onResizeBox"
+	>
+		<div class="box-header" @mousedown="onDragBox">
+			<slot name="header"></slot>
+		</div>
+		<div class="box-body">
+			<slot name="body"></slot>
+		</div>
+	</div>
+</template>
+
+<script>
+export default {
+	props: {
+		id: { type: String, default: null }
+	},
+	data() {
+		return {
+			width: 200,
+			height: 200,
+			top: 0,
+			left: 0,
+			shown: false,
+			pos1: 0,
+			pos2: 0,
+			pos3: 0,
+			pos4: 0
+		};
+	},
+	methods: {
+		onDragBox(e) {
+			const e1 = e || window.event;
+			e1.preventDefault();
+
+			this.pos3 = e1.clientX;
+			this.pos4 = e1.clientY;
+
+			document.onmousemove = e => {
+				const e2 = e || window.event;
+				e2.preventDefault();
+				// calculate the new cursor position:
+				this.pos1 = this.pos3 - e.clientX;
+				this.pos2 = this.pos4 - e.clientY;
+				this.pos3 = e.clientX;
+				this.pos4 = e.clientY;
+				// set the element's new position:
+				this.top -= this.pos2;
+				this.left -= this.pos1;
+			};
+
+			document.onmouseup = () => {
+				document.onmouseup = null;
+				document.onmousemove = null;
+
+				this.saveBox();
+			};
+		},
+		onResizeBox(e) {
+			if (e.target !== this.$refs.box) return;
+
+			document.onmouseup = () => {
+				document.onmouseup = null;
+
+				const { height, width } = e.target.style;
+
+				this.height = Number(
+					height
+						.split("")
+						.splice(0, height.length - 2)
+						.join("")
+				);
+				this.width = Number(
+					width
+						.split("")
+						.splice(0, width.length - 2)
+						.join("")
+				);
+
+				this.saveBox();
+			};
+		},
+		toggleBox() {
+			this.shown = !this.shown;
+			this.saveBox();
+		},
+		resetBox() {
+			this.top = 0;
+			this.left = 0;
+			this.width = 200;
+			this.height = 200;
+			this.saveBox();
+		},
+		saveBox() {
+			if (this.id === null) return;
+			localStorage.setItem(
+				`box:${this.id}`,
+				JSON.stringify({
+					height: this.height,
+					width: this.width,
+					top: this.top,
+					left: this.left,
+					shown: this.shown
+				})
+			);
+		}
+	},
+	mounted() {
+		if (this.id !== null && localStorage[`box:${this.id}`]) {
+			const json = JSON.parse(localStorage.getItem(`box:${this.id}`));
+			this.height = json.height;
+			this.width = json.width;
+			this.top = json.top;
+			this.left = json.left;
+			this.shown = json.shown;
+		}
+	}
+};
+</script>
+
+<style lang="scss" scoped>
+@import "../../styles/global.scss";
+
+.box {
+	background-color: white;
+	position: fixed;
+	z-index: 10000000;
+	resize: both;
+	overflow: auto;
+	border: 1px solid #d3d3d3;
+	min-height: 50px !important;
+	min-width: 50px !important;
+
+	.box-header {
+		cursor: move;
+		z-index: 100000001;
+		background-color: $musare-blue;
+		padding: 10px;
+		display: block;
+		height: 10px;
+		width: 100%;
+	}
+
+	.box-body {
+		display: flex;
+		flex-wrap: wrap;
+		justify-content: space-evenly;
+
+		span {
+			padding: 3px 6px;
+		}
+	}
+}
+</style>

+ 1 - 0
frontend/src/main.js

@@ -44,6 +44,7 @@ Vue.directive("scroll", {
 
 Vue.directive("focus", {
 	inserted(el) {
+		window.focusedElementBefore = document.activeElement;
 		el.focus();
 	}
 });

+ 13 - 2
frontend/src/mixins/ScrollAndFetchHandler.vue

@@ -5,7 +5,8 @@ export default {
 			position: 1,
 			maxPosition: 1,
 			gettingSet: false,
-			loadAllSongs: false
+			loadAllSongs: false,
+			interval: null
 		};
 	},
 	computed: {
@@ -29,8 +30,18 @@ export default {
 		},
 		loadAll() {
 			this.loadAllSongs = true;
-			this.getSet();
+			this.interval = setInterval(() => {
+				if (this.loadAllSongs && this.maxPosition > this.position)
+					this.getSet();
+				else {
+					clearInterval(this.interval);
+					this.loadAllSongs = false;
+				}
+			}, 500);
 		}
+	},
+	unmounted() {
+		clearInterval(this.interval);
 	}
 };
 </script>

+ 100 - 9
frontend/src/pages/Admin/tabs/QueueSongs.vue

@@ -20,6 +20,13 @@
 			>
 				Load all
 			</button>
+			<button
+				class="button is-primary"
+				@click="toggleKeyboardShortcutsHelper"
+				@dblclick="resetKeyboardShortcutsHelper"
+			>
+				Keyboard shortcuts helper
+			</button>
 			<br />
 			<br />
 			<table class="table is-striped">
@@ -103,6 +110,65 @@
 			</table>
 		</div>
 		<edit-song v-if="modals.editSong" />
+		<floating-box
+			id="keyboardShortcutsHelper"
+			ref="keyboardShortcutsHelper"
+		>
+			<template #body>
+				<div>
+					<div>
+						<span class="biggest"><b>Queue songs page</b></span>
+						<span
+							><b>Arrow keys up/down</b> - Moves between
+							songs</span
+						>
+						<span><b>E</b> - Edit selected song</span>
+						<span><b>A</b> - Add selected song</span>
+						<span><b>X</b> - Delete selected song</span>
+					</div>
+					<hr />
+					<div>
+						<span class="biggest"><b>Edit song modal</b></span>
+						<span class="bigger"><b>Navigation</b></span>
+						<span><b>Home</b> - Edit</span>
+						<span><b>End</b> - Edit</span>
+						<hr />
+						<span class="bigger"><b>Player controls</b></span>
+						<span><b>Numpad up/down</b> - Volume up/down 10%</span>
+						<span
+							><b>Ctrl + Numpad up/down</b> - Volume up/down
+							1%</span
+						>
+						<span><b>Numpad center</b> - Pause/resume</span>
+						<span><b>Ctrl + Numpad center</b> - Stop</span>
+						<span
+							><b>Numpad Right</b> - Skip to last 10 seconds</span
+						>
+						<hr />
+						<span class="bigger"><b>Form control</b></span>
+						<span
+							><b>Ctrl + D</b> - Executes purple button in that
+							input</span
+						>
+						<span
+							><b>Ctrl + Alt + D</b> - Fill in all Discogs
+							fields</span
+						>
+						<span
+							><b>Ctrl + R</b> - Executes red button in that
+							input</span
+						>
+						<span
+							><b>Ctrl + Alt + R</b> - Reset duration field</span
+						>
+						<hr />
+						<span class="bigger"><b>Modal control</b></span>
+						<span><b>Ctrl + S</b> - Save</span>
+						<span><b>Ctrl + X</b> - Exit</span>
+					</div>
+				</div>
+			</template>
+		</floating-box>
 	</div>
 </template>
 
@@ -115,12 +181,14 @@ import Toast from "toasters";
 import EditSong from "../../../components/modals/EditSong.vue";
 import UserIdToUsername from "../../../components/common/UserIdToUsername.vue";
 
+import FloatingBox from "../../../components/ui/FloatingBox.vue";
+
 import ScrollAndFetchHandler from "../../../mixins/ScrollAndFetchHandler.vue";
 
 import io from "../../../io";
 
 export default {
-	components: { EditSong, UserIdToUsername },
+	components: { EditSong, UserIdToUsername, FloatingBox },
 	mixins: [ScrollAndFetchHandler],
 	data() {
 		return {
@@ -165,6 +233,11 @@ export default {
 			});
 		},
 		remove(id) {
+			// eslint-disable-next-line
+			const dialogResult = window.confirm(
+				"Are you sure you want to delete this song?"
+			);
+			if (dialogResult !== true) return;
 			this.socket.emit("queueSongs.remove", id, res => {
 				if (res.status === "success")
 					new Toast({ content: res.message, timeout: 2000 });
@@ -193,6 +266,12 @@ export default {
 			if (event.srcElement.nextElementSibling)
 				event.srcElement.nextElementSibling.focus();
 		},
+		toggleKeyboardShortcutsHelper() {
+			this.$refs.keyboardShortcutsHelper.toggleBox();
+		},
+		resetKeyboardShortcutsHelper() {
+			this.$refs.keyboardShortcutsHelper.resetBox();
+		},
 		init() {
 			if (this.songs.length > 0)
 				this.position = Math.ceil(this.songs.length / 15) + 1;
@@ -201,14 +280,6 @@ export default {
 				this.maxPosition = Math.ceil(length / 15) + 1;
 
 				this.getSet();
-
-				setTimeout(() => {
-					if (
-						!this.loadAllSongs &&
-						this.maxPosition > this.position - 1
-					)
-						this.getSet();
-				}, 1000);
 			});
 
 			this.socket.emit("apis.joinAdminRoom", "queue", () => {});
@@ -294,6 +365,26 @@ td {
 	vertical-align: middle;
 }
 
+#keyboardShortcutsHelper {
+	.box-body {
+		b {
+			color: #000;
+		}
+
+		.biggest {
+			font-size: 18px;
+		}
+
+		.bigger {
+			font-size: 16px;
+		}
+
+		span {
+			display: block;
+		}
+	}
+}
+
 .is-primary:focus {
 	background-color: $primary-color !important;
 }

+ 208 - 16
frontend/src/pages/Admin/tabs/Songs.vue

@@ -20,7 +20,44 @@
 			>
 				Load all
 			</button>
+			<button
+				class="button is-primary"
+				@click="toggleKeyboardShortcutsHelper"
+				@dblclick="resetKeyboardShortcutsHelper"
+			>
+				Keyboard shortcuts helper
+			</button>
 			<br />
+			<div>
+				<input
+					type="text"
+					placeholder="Filter artist checkboxes"
+					v-model="artistFilterQuery"
+				/>
+				<label v-for="artist in filteredArtists" :key="artist">
+					<input
+						type="checkbox"
+						:checked="artistFilterSelected.indexOf(artist) !== -1"
+						@click="toggleArtistSelected(artist)"
+					/>
+					<span>{{ artist }}</span>
+				</label>
+			</div>
+			<div>
+				<input
+					type="text"
+					placeholder="Filter genre checkboxes"
+					v-model="genreFilterQuery"
+				/>
+				<label v-for="genre in filteredGenres" :key="genre">
+					<input
+						type="checkbox"
+						:checked="genreFilterSelected.indexOf(genre) !== -1"
+						@click="toggleGenreSelected(genre)"
+					/>
+					<span>{{ genre }}</span>
+				</label>
+			</div>
 			<br />
 			<table class="table is-striped">
 				<thead>
@@ -96,6 +133,65 @@
 			</table>
 		</div>
 		<edit-song v-if="modals.editSong" />
+		<floating-box
+			id="keyboardShortcutsHelper"
+			ref="keyboardShortcutsHelper"
+		>
+			<template #body>
+				<div>
+					<div>
+						<span class="biggest"><b>Songs page</b></span>
+						<span
+							><b>Arrow keys up/down</b> - Moves between
+							songs</span
+						>
+						<span><b>E</b> - Edit selected song</span>
+						<span><b>A</b> - Add selected song</span>
+						<span><b>X</b> - Delete selected song</span>
+					</div>
+					<hr />
+					<div>
+						<span class="biggest"><b>Edit song modal</b></span>
+						<span class="bigger"><b>Navigation</b></span>
+						<span><b>Home</b> - Edit</span>
+						<span><b>End</b> - Edit</span>
+						<hr />
+						<span class="bigger"><b>Player controls</b></span>
+						<span><b>Numpad up/down</b> - Volume up/down 10%</span>
+						<span
+							><b>Ctrl + Numpad up/down</b> - Volume up/down
+							1%</span
+						>
+						<span><b>Numpad center</b> - Pause/resume</span>
+						<span><b>Ctrl + Numpad center</b> - Stop</span>
+						<span
+							><b>Numpad Right</b> - Skip to last 10 seconds</span
+						>
+						<hr />
+						<span class="bigger"><b>Form control</b></span>
+						<span
+							><b>Ctrl + D</b> - Executes purple button in that
+							input</span
+						>
+						<span
+							><b>Ctrl + Alt + D</b> - Fill in all Discogs
+							fields</span
+						>
+						<span
+							><b>Ctrl + R</b> - Executes red button in that
+							input</span
+						>
+						<span
+							><b>Ctrl + Alt + R</b> - Reset duration field</span
+						>
+						<hr />
+						<span class="bigger"><b>Modal control</b></span>
+						<span><b>Ctrl + S</b> - Save</span>
+						<span><b>Ctrl + X</b> - Exit</span>
+					</div>
+				</div>
+			</template>
+		</floating-box>
 	</div>
 </template>
 
@@ -107,16 +203,22 @@ import Toast from "toasters";
 import EditSong from "../../../components/modals/EditSong.vue";
 import UserIdToUsername from "../../../components/common/UserIdToUsername.vue";
 
+import FloatingBox from "../../../components/ui/FloatingBox.vue";
+
 import ScrollAndFetchHandler from "../../../mixins/ScrollAndFetchHandler.vue";
 
 import io from "../../../io";
 
 export default {
 	mixins: [ScrollAndFetchHandler],
-	components: { EditSong, UserIdToUsername },
+	components: { EditSong, UserIdToUsername, FloatingBox },
 	data() {
 		return {
 			searchQuery: "",
+			artistFilterQuery: "",
+			artistFilterSelected: [],
+			genreFilterQuery: "",
+			genreFilterSelected: [],
 			editing: {
 				index: 0,
 				song: {}
@@ -129,9 +231,63 @@ export default {
 				song =>
 					JSON.stringify(Object.values(song)).indexOf(
 						this.searchQuery
-					) !== -1
+					) !== -1 &&
+					(this.artistFilterSelected.length === 0 ||
+						song.artists.some(
+							artist =>
+								this.artistFilterSelected.indexOf(artist) !== -1
+						)) &&
+					(this.genreFilterSelected.length === 0 ||
+						song.genres.some(
+							genre =>
+								this.genreFilterSelected.indexOf(genre) !== -1
+						))
 			);
 		},
+		artists() {
+			const artists = [];
+			this.songs.forEach(song => {
+				song.artists.forEach(artist => {
+					if (artists.indexOf(artist) === -1) artists.push(artist);
+				});
+			});
+			return artists.sort();
+		},
+		filteredArtists() {
+			return this.artists
+				.filter(
+					artist =>
+						this.artistFilterSelected.indexOf(artist) !== -1 ||
+						artist.indexOf(this.artistFilterQuery) !== -1
+				)
+				.sort(
+					(a, b) =>
+						(this.artistFilterSelected.indexOf(a) === -1 ? 1 : 0) -
+						(this.artistFilterSelected.indexOf(b) === -1 ? 1 : 0)
+				);
+		},
+		genres() {
+			const genres = [];
+			this.songs.forEach(song => {
+				song.genres.forEach(genre => {
+					if (genres.indexOf(genre) === -1) genres.push(genre);
+				});
+			});
+			return genres.sort();
+		},
+		filteredGenres() {
+			return this.genres
+				.filter(
+					genre =>
+						this.genreFilterSelected.indexOf(genre) !== -1 ||
+						genre.indexOf(this.genreFilterQuery) !== -1
+				)
+				.sort(
+					(a, b) =>
+						(this.genreFilterSelected.indexOf(a) === -1 ? 1 : 0) -
+						(this.genreFilterSelected.indexOf(b) === -1 ? 1 : 0)
+				);
+		},
 		...mapState("modals", {
 			modals: state => state.modals.admin
 		}),
@@ -151,6 +307,11 @@ export default {
 			this.openModal({ sector: "admin", modal: "editSong" });
 		},
 		remove(id) {
+			// eslint-disable-next-line
+			const dialogResult = window.confirm(
+				"Are you sure you want to delete this song?"
+			);
+			if (dialogResult !== true) return;
 			this.socket.emit("songs.remove", id, res => {
 				if (res.status === "success")
 					new Toast({ content: res.message, timeout: 4000 });
@@ -171,6 +332,30 @@ export default {
 				this.gettingSet = false;
 			});
 		},
+		toggleArtistSelected(artist) {
+			if (this.artistFilterSelected.indexOf(artist) === -1)
+				this.artistFilterSelected.push(artist);
+			else
+				this.artistFilterSelected.splice(
+					this.artistFilterSelected.indexOf(artist),
+					1
+				);
+		},
+		toggleGenreSelected(genre) {
+			if (this.genreFilterSelected.indexOf(genre) === -1)
+				this.genreFilterSelected.push(genre);
+			else
+				this.genreFilterSelected.splice(
+					this.genreFilterSelected.indexOf(genre),
+					1
+				);
+		},
+		toggleKeyboardShortcutsHelper() {
+			this.$refs.keyboardShortcutsHelper.toggleBox();
+		},
+		resetKeyboardShortcutsHelper() {
+			this.$refs.keyboardShortcutsHelper.resetBox();
+		},
 		init() {
 			if (this.songs.length > 0)
 				this.position = Math.ceil(this.songs.length / 15) + 1;
@@ -179,14 +364,6 @@ export default {
 				this.maxPosition = Math.ceil(length / 15) + 1;
 
 				this.getSet();
-
-				setTimeout(() => {
-					if (
-						!this.loadAllSongs &&
-						this.maxPosition > this.position - 1
-					)
-						this.getSet();
-				}, 1000);
 			});
 
 			this.socket.emit("apis.joinAdminRoom", "songs", () => {});
@@ -225,12 +402,7 @@ export default {
 		if (this.$route.query.songId) {
 			this.socket.emit("songs.getSong", this.$route.query.songId, res => {
 				if (res.status === "success") {
-					this.edit(res.data);
-
-					this.closeModal({
-						sector: "admin",
-						modal: "viewReport"
-					});
+					this.edit(res.data.song);
 				} else
 					new Toast({
 						content: "Song with that ID not found",
@@ -306,6 +478,26 @@ td {
 	vertical-align: middle;
 }
 
+#keyboardShortcutsHelper {
+	.box-body {
+		b {
+			color: #000;
+		}
+
+		.biggest {
+			font-size: 18px;
+		}
+
+		.bigger {
+			font-size: 16px;
+		}
+
+		span {
+			display: block;
+		}
+	}
+}
+
 .is-primary:focus {
 	background-color: $primary-color !important;
 }

+ 11 - 3
frontend/src/pages/Home/index.vue

@@ -5,7 +5,11 @@
 			<main-header />
 			<div class="group">
 				<div class="group-title">
-					<h1>Stations&nbsp;</h1>
+					<div>
+						<h1>
+							Stations
+						</h1>
+					</div>
 					<a
 						v-if="loggedIn"
 						href="#"
@@ -15,8 +19,7 @@
 								modal: 'createCommunityStation'
 							})
 						"
-					>
-						<i class="material-icons community-button"
+						><i class="material-icons community-button"
 							>add_circle_outline</i
 						>
 					</a>
@@ -601,8 +604,13 @@ html {
 		align-items: center;
 		justify-content: center;
 
+		h1 {
+			display: inline-block;
+		}
+
 		a {
 			display: flex;
+			margin-left: 8px;
 		}
 	}
 }

+ 30 - 1
frontend/src/pages/Station/StationHeader.vue

@@ -14,7 +14,12 @@
 				<h4>{{ station.displayName }}</h4>
 			</div>
 
-			<span class="nav-toggle" @click="controlBar = !controlBar">
+			<span
+				class="nav-toggle"
+				tab-index="0"
+				@click="controlBar = !controlBar"
+				@keyup.enter="isMobile = !isMobile"
+			>
 				<span />
 				<span />
 				<span />
@@ -220,6 +225,17 @@
 						>Display users in the station</span
 					>
 				</a>
+				<a
+					class="sidebar-item"
+					href="#"
+					@click="$parent.togglePlayerDebugBox()"
+					@dblclick="$parent.resetPlayerDebugBox()"
+				>
+					<span class="icon">
+						<i class="material-icons">bug_report</i>
+					</span>
+					<span class="icon-purpose">Toggle debug player box</span>
+				</a>
 			</div>
 		</div>
 	</div>
@@ -298,6 +314,7 @@ export default {
 .nav {
 	background-color: $primary-color;
 	line-height: 64px;
+	overflow: hidden;
 	border-radius: 0% 0% 33% 33% / 0% 0% 7% 7%;
 	transition: border-radius 0.1s 0s linear;
 
@@ -360,8 +377,20 @@ a.nav-item.is-tab:hover {
 	}
 }
 
+.nav {
+}
+
 .nav-toggle {
 	height: 64px;
+
+	span {
+		background-color: $white;
+	}
+
+	&:hover,
+	&:active {
+		background-color: darken($musare-blue, 10%);
+	}
 }
 
 @media screen and (max-width: 998px) {

+ 56 - 1
frontend/src/pages/Station/index.vue

@@ -428,6 +428,42 @@
 			</transition>
 		</div>
 
+		<floating-box id="playerDebugBox" ref="playerDebugBox">
+			<template #body>
+				<span><b>YouTube id</b>: {{ currentSong.songId }}</span>
+				<span><b>Duration</b>: {{ currentSong.duration }}</span>
+				<span
+					><b>Skip duration</b>: {{ currentSong.skipDuration }}</span
+				>
+				<span><b>Can autoplay</b>: {{ canAutoplay }}</span>
+				<span
+					><b>Attempts to play video</b>:
+					{{ attemptsToPlayVideo }}</span
+				>
+				<span
+					><b>Last time requested if can autoplay</b>:
+					{{ lastTimeRequestedIfCanAutoplay }}</span
+				>
+				<span><b>Loading</b>: {{ loading }}</span>
+				<span><b>Playback rate</b>: {{ playbackRate }}</span>
+				<span><b>Player ready</b>: {{ playerReady }}</span>
+				<span><b>Ready</b>: {{ ready }}</span>
+				<span><b>Seeking</b>: {{ seeking }}</span>
+				<span><b>System difference</b>: {{ systemDifference }}</span>
+				<span><b>Time before paused</b>: {{ timeBeforePause }}</span>
+				<span><b>Time elapsed</b>: {{ timeElapsed }}</span>
+				<span><b>Time paused</b>: {{ timePaused }}</span>
+				<span><b>Volume slider value</b>: {{ volumeSliderValue }}</span>
+				<span><b>Local paused</b>: {{ localPaused }}</span>
+				<span><b>No song</b>: {{ noSong }}</span>
+				<span
+					><b>Private playlist queue selected</b>:
+					{{ privatePlaylistQueueSelected }}</span
+				>
+				<span><b>Station paused</b>: {{ stationPaused }}</span>
+			</template>
+		</floating-box>
+
 		<Z404 v-if="!exists"></Z404>
 	</div>
 </template>
@@ -441,6 +477,8 @@ import StationHeader from "./StationHeader.vue";
 import UserIdToUsername from "../../components/common/UserIdToUsername.vue";
 import Z404 from "../404.vue";
 
+import FloatingBox from "../../components/ui/FloatingBox.vue";
+
 import io from "../../io";
 import keyboardShortcuts from "../../keyboardShortcuts";
 import utils from "../../../js/utils";
@@ -1011,6 +1049,12 @@ export default {
 				}
 			}
 		},
+		togglePlayerDebugBox() {
+			this.$refs.playerDebugBox.toggleBox();
+		},
+		resetPlayerDebugBox() {
+			this.$refs.playerDebugBox.resetBox();
+		},
 		join() {
 			this.socket.emit("stations.join", this.stationName, res => {
 				console.log(res.data);
@@ -1414,7 +1458,8 @@ export default {
 		SongsListSidebar: () => import("./SongsList.vue"),
 		UsersSidebar: () => import("./UsersList.vue"),
 		UserIdToUsername,
-		Z404
+		Z404,
+		FloatingBox
 	}
 };
 </script>
@@ -2002,4 +2047,14 @@ h6 {
 .experimental {
 	display: none !important;
 }
+
+#playerDebugBox {
+	.box-body {
+		flex-direction: column;
+
+		b {
+			color: #000;
+		}
+	}
+}
 </style>