소스 검색

refactor: Merged admin songs pages into one admin/songs

Owen Diffey 3 년 전
부모
커밋
fd16bbdf69

+ 8 - 20
backend/logic/actions/songs.js

@@ -150,29 +150,21 @@ export default {
 	 * @param {object} session - the session object automatically added by the websocket
 	 * @param cb
 	 */
-	length: isAdminRequired(async function length(session, status, cb) {
+	length: isAdminRequired(async function length(session, cb) {
 		const songModel = await DBModule.runJob("GET_MODEL", { modelName: "song" }, this);
 		async.waterfall(
 			[
 				next => {
-					songModel.countDocuments({ status }, next);
+					songModel.countDocuments({}, next);
 				}
 			],
 			async (err, count) => {
 				if (err) {
 					err = await UtilsModule.runJob("GET_ERROR", { error: err }, this);
-					this.log(
-						"ERROR",
-						"SONGS_LENGTH",
-						`Failed to get length from songs that have the status ${status}. "${err}"`
-					);
+					this.log("ERROR", "SONGS_LENGTH", `Failed to get length from songs. "${err}"`);
 					return cb({ status: "error", message: err });
 				}
-				this.log(
-					"SUCCESS",
-					"SONGS_LENGTH",
-					`Got length from songs that have the status ${status} successfully.`
-				);
+				this.log("SUCCESS", "SONGS_LENGTH", `Got length from songs successfully.`);
 				return cb({ status: "success", message: "Successfully got length of songs.", data: { length: count } });
 			}
 		);
@@ -185,13 +177,13 @@ export default {
 	 * @param set - the set number to return
 	 * @param cb
 	 */
-	getSet: isAdminRequired(async function getSet(session, set, status, cb) {
+	getSet: isAdminRequired(async function getSet(session, set, cb) {
 		const songModel = await DBModule.runJob("GET_MODEL", { modelName: "song" }, this);
 		async.waterfall(
 			[
 				next => {
 					songModel
-						.find({ status })
+						.find({})
 						.skip(15 * (set - 1))
 						.limit(15)
 						.exec(next);
@@ -200,14 +192,10 @@ export default {
 			async (err, songs) => {
 				if (err) {
 					err = await UtilsModule.runJob("GET_ERROR", { error: err }, this);
-					this.log(
-						"ERROR",
-						"SONGS_GET_SET",
-						`Failed to get set from songs that have the status ${status}. "${err}"`
-					);
+					this.log("ERROR", "SONGS_GET_SET", `Failed to get set from songs. "${err}"`);
 					return cb({ status: "error", message: err });
 				}
-				this.log("SUCCESS", "SONGS_GET_SET", `Got set from songs that have the status ${status} successfully.`);
+				this.log("SUCCESS", "SONGS_GET_SET", `Got set from songs successfully.`);
 				return cb({ status: "success", message: "Successfully got set of songs.", data: { songs } });
 			}
 		);

+ 5 - 0
frontend/src/App.vue

@@ -1874,6 +1874,7 @@ html {
 }
 
 ::-webkit-scrollbar {
+	height: 10px;
 	width: 10px;
 }
 
@@ -1884,4 +1885,8 @@ html {
 ::-webkit-scrollbar-thumb {
 	background-color: var(--primary-color);
 }
+
+::-webkit-scrollbar-corner {
+	background-color: transparent;
+}
 </style>

+ 11 - 64
frontend/src/pages/Admin/index.vue

@@ -4,42 +4,13 @@
 		<div class="tabs is-centered">
 			<ul>
 				<li
-					:class="{ 'is-active': currentTab == 'hiddensongs' }"
-					ref="hiddensongs-tab"
-					@click="showTab('hiddensongs')"
+					:class="{ 'is-active': currentTab == 'songs' }"
+					ref="songs-tab"
+					@click="showTab('songs')"
 				>
-					<router-link
-						class="tab hiddensongs"
-						to="/admin/hiddensongs"
-					>
+					<router-link class="tab songs" to="/admin/songs">
 						<i class="material-icons">music_note</i>
-						<span>&nbsp;Hidden Songs</span>
-					</router-link>
-				</li>
-				<li
-					:class="{ 'is-active': currentTab == 'unverifiedsongs' }"
-					ref="unverifiedsongs-tab"
-					@click="showTab('unverifiedsongs')"
-				>
-					<router-link
-						class="tab unverifiedsongs"
-						to="/admin/unverifiedsongs"
-					>
-						<i class="material-icons">unpublished</i>
-						<span>&nbsp;Unverified Songs</span>
-					</router-link>
-				</li>
-				<li
-					:class="{ 'is-active': currentTab == 'verifiedsongs' }"
-					ref="verifiedsongs-tab"
-					@click="showTab('verifiedsongs')"
-				>
-					<router-link
-						class="tab verifiedsongs"
-						to="/admin/verifiedsongs"
-					>
-						<i class="material-icons">check_circle</i>
-						<span>&nbsp;Verified Songs</span>
+						<span>&nbsp;Songs</span>
 					</router-link>
 				</li>
 				<li
@@ -118,9 +89,7 @@
 			</ul>
 		</div>
 
-		<unverified-songs v-if="currentTab == 'unverifiedsongs'" />
-		<verified-songs v-if="currentTab == 'verifiedsongs'" />
-		<hidden-songs v-if="currentTab == 'hiddensongs'" />
+		<songs v-if="currentTab == 'songs'" />
 		<stations v-if="currentTab == 'stations'" />
 		<playlists v-if="currentTab == 'playlists'" />
 		<reports v-if="currentTab == 'reports'" />
@@ -140,15 +109,7 @@ import MainHeader from "@/components/layout/MainHeader.vue";
 export default {
 	components: {
 		MainHeader,
-		UnverifiedSongs: defineAsyncComponent(() =>
-			import("./tabs/UnverifiedSongs.vue")
-		),
-		VerifiedSongs: defineAsyncComponent(() =>
-			import("./tabs/VerifiedSongs.vue")
-		),
-		HiddenSongs: defineAsyncComponent(() =>
-			import("./tabs/HiddenSongs.vue")
-		),
+		Songs: defineAsyncComponent(() => import("./tabs/Songs.vue")),
 		Stations: defineAsyncComponent(() => import("./tabs/Stations.vue")),
 		Playlists: defineAsyncComponent(() => import("./tabs/Playlists.vue")),
 		Reports: defineAsyncComponent(() => import("./tabs/Reports.vue")),
@@ -181,14 +142,8 @@ export default {
 	methods: {
 		changeTab(path) {
 			switch (path) {
-				case "/admin/unverifiedsongs":
-					this.showTab("unverifiedsongs");
-					break;
-				case "/admin/verifiedsongs":
-					this.showTab("verifiedsongs");
-					break;
-				case "/admin/hiddensongs":
-					this.showTab("hiddensongs");
+				case "/admin/songs":
+					this.showTab("songs");
 					break;
 				case "/admin/stations":
 					this.showTab("stations");
@@ -215,7 +170,7 @@ export default {
 					if (localStorage.getItem("lastAdminPage")) {
 						this.showTab(localStorage.getItem("lastAdminPage"));
 					} else {
-						this.showTab("verifiedsongs");
+						this.showTab("songs");
 					}
 			}
 		},
@@ -306,18 +261,10 @@ export default {
 		border-bottom: 1px solid var(--light-grey-2);
 	}
 
-	.unverifiedsongs {
-		color: var(--teal);
-		border-color: var(--teal);
-	}
-	.verifiedsongs {
+	.songs {
 		color: var(--primary-color);
 		border-color: var(--primary-color);
 	}
-	.hiddensongs {
-		color: var(--grey);
-		border-color: var(--grey);
-	}
 	.stations {
 		color: var(--purple);
 		border-color: var(--purple);

+ 0 - 613
frontend/src/pages/Admin/tabs/HiddenSongs.vue

@@ -1,613 +0,0 @@
-<template>
-	<div>
-		<page-metadata title="Admin | Hidden songs" />
-		<div class="container">
-			<div class="button-row">
-				<button
-					v-if="!loadAllSongs"
-					class="button is-primary"
-					@click="loadAll()"
-				>
-					Load all sets
-				</button>
-				<button
-					class="button is-primary"
-					@click="toggleKeyboardShortcutsHelper"
-					@dblclick="resetKeyboardShortcutsHelper"
-				>
-					Keyboard shortcuts helper
-				</button>
-				<button
-					class="button is-primary"
-					@click="openModal('requestSong')"
-				>
-					Request song
-				</button>
-			</div>
-			<br />
-			<div class="box">
-				<p @click="toggleSearchBox()">
-					Search<i class="material-icons" v-show="searchBoxShown"
-						>expand_more</i
-					>
-					<i class="material-icons" v-show="!searchBoxShown"
-						>expand_less</i
-					>
-				</p>
-				<input
-					v-model="searchQuery"
-					type="text"
-					class="input"
-					placeholder="Search for Songs"
-					v-show="searchBoxShown"
-				/>
-			</div>
-			<div class="box">
-				<p @click="toggleFilterArtistsBox()">
-					Filter artists<i
-						class="material-icons"
-						v-show="filterArtistBoxShown"
-						>expand_more</i
-					>
-					<i class="material-icons" v-show="!filterArtistBoxShown"
-						>expand_less</i
-					>
-				</p>
-				<input
-					type="text"
-					class="input"
-					placeholder="Filter artist checkboxes"
-					v-model="artistFilterQuery"
-					v-show="filterArtistBoxShown"
-				/>
-				<label
-					v-for="artist in filteredArtists"
-					:key="artist"
-					v-show="filterArtistBoxShown"
-				>
-					<input
-						type="checkbox"
-						:checked="artistFilterSelected.indexOf(artist) !== -1"
-						@click="toggleArtistSelected(artist)"
-					/>
-					<span>{{ artist }}</span>
-				</label>
-			</div>
-			<div class="box">
-				<p @click="toggleFilterGenresBox()">
-					Filter genres<i
-						class="material-icons"
-						v-show="filterGenreBoxShown"
-						>expand_more</i
-					>
-					<i class="material-icons" v-show="!filterGenreBoxShown"
-						>expand_less</i
-					>
-				</p>
-				<input
-					type="text"
-					class="input"
-					placeholder="Filter genre checkboxes"
-					v-model="genreFilterQuery"
-					v-show="filterGenreBoxShown"
-				/>
-				<label
-					v-for="genre in filteredGenres"
-					:key="genre"
-					v-show="filterGenreBoxShown"
-				>
-					<input
-						type="checkbox"
-						:checked="genreFilterSelected.indexOf(genre) !== -1"
-						@click="toggleGenreSelected(genre)"
-					/>
-					<span>{{ genre }}</span>
-				</label>
-			</div>
-			<p>
-				<span>Sets loaded: {{ setsLoaded }} / {{ maxSets }}</span>
-				<br />
-				<span>Loaded songs: {{ songs.length }}</span>
-			</p>
-			<br />
-			<table class="table">
-				<thead>
-					<tr>
-						<td>Thumbnail</td>
-						<td>Title</td>
-						<td>Artists</td>
-						<td>Genres</td>
-						<td>ID / YouTube ID</td>
-						<td>Requested By</td>
-						<td>Options</td>
-					</tr>
-				</thead>
-				<tbody>
-					<tr
-						v-for="(song, index) in filteredSongs"
-						:key="song._id"
-						tabindex="0"
-						@keydown.up.prevent
-						@keydown.down.prevent
-						@keyup.up="selectPrevious($event)"
-						@keyup.down="selectNext($event)"
-						@keyup.e="edit(song, index)"
-						@keyup.a="add(song)"
-						@keyup.x="remove(song._id, index)"
-					>
-						<td>
-							<img
-								class="song-thumbnail"
-								:src="song.thumbnail"
-								onerror="this.src='/assets/notes-transparent.png'"
-							/>
-						</td>
-						<td>
-							<strong>{{ song.title }}</strong>
-						</td>
-						<td>{{ song.artists.join(", ") }}</td>
-						<td>{{ song.genres.join(", ") }}</td>
-						<td>
-							{{ song._id }}
-							<br />
-							<a
-								:href="
-									'https://www.youtube.com/watch?v=' +
-									`${song.youtubeId}`
-								"
-								target="_blank"
-							>
-								{{ song.youtubeId }}</a
-							>
-						</td>
-						<td>
-							<user-id-to-username
-								:user-id="song.requestedBy"
-								:link="true"
-							/>
-						</td>
-						<td class="optionsColumn">
-							<div>
-								<button
-									class="button is-primary"
-									@click="edit(song, index)"
-									content="Edit Song"
-									v-tippy
-								>
-									<i class="material-icons">edit</i>
-								</button>
-								<button
-									class="button is-success"
-									@click="unhide(song._id)"
-									content="Unhide Song"
-									v-tippy
-								>
-									<i class="material-icons">visibility</i>
-								</button>
-							</div>
-						</td>
-					</tr>
-				</tbody>
-			</table>
-		</div>
-		<import-album v-if="modals.importAlbum" />
-		<edit-song v-if="modals.editSong" song-type="songs" :key="song._id" />
-		<report v-if="modals.report" />
-		<request-song v-if="modals.requestSong" />
-		<floating-box
-			id="keyboardShortcutsHelper"
-			ref="keyboardShortcutsHelper"
-		>
-			<template #body>
-				<div>
-					<div>
-						<span class="biggest"><b>Hidden 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>
-
-<script>
-import { mapState, mapActions, mapGetters } from "vuex";
-import { defineAsyncComponent } from "vue";
-
-import Toast from "toasters";
-
-import UserIdToUsername from "@/components/UserIdToUsername.vue";
-import FloatingBox from "@/components/FloatingBox.vue";
-
-import ScrollAndFetchHandler from "@/mixins/ScrollAndFetchHandler.vue";
-
-import ws from "@/ws";
-
-export default {
-	components: {
-		EditSong: defineAsyncComponent(() =>
-			import("@/components/modals/EditSong")
-		),
-		Report: defineAsyncComponent(() =>
-			import("@/components/modals/Report.vue")
-		),
-		ImportAlbum: defineAsyncComponent(() =>
-			import("@/components/modals/ImportAlbum.vue")
-		),
-		RequestSong: defineAsyncComponent(() =>
-			import("@/components/modals/RequestSong.vue")
-		),
-		UserIdToUsername,
-		FloatingBox
-	},
-	mixins: [ScrollAndFetchHandler],
-	data() {
-		return {
-			searchQuery: "",
-			artistFilterQuery: "",
-			artistFilterSelected: [],
-			genreFilterQuery: "",
-			genreFilterSelected: [],
-			searchBoxShown: true,
-			filterArtistBoxShown: false,
-			filterGenreBoxShown: false
-		};
-	},
-	computed: {
-		filteredSongs() {
-			return this.songs.filter(
-				song =>
-					JSON.stringify(Object.values(song))
-						.toLowerCase()
-						.indexOf(this.searchQuery.toLowerCase()) !== -1 &&
-					(this.artistFilterSelected.length === 0 ||
-						song.artists.some(
-							artist =>
-								this.artistFilterSelected
-									.map(artistFilterSelected =>
-										artistFilterSelected.toLowerCase()
-									)
-									.indexOf(artist.toLowerCase()) !== -1
-						)) &&
-					(this.genreFilterSelected.length === 0 ||
-						song.genres.some(
-							genre =>
-								this.genreFilterSelected
-									.map(genreFilterSelected =>
-										genreFilterSelected.toLowerCase()
-									)
-									.indexOf(genre.toLowerCase()) !== -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
-							.toLowerCase()
-							.indexOf(this.artistFilterQuery.toLowerCase()) !==
-							-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
-							.toLowerCase()
-							.indexOf(this.genreFilterQuery.toLowerCase()) !== -1
-				)
-				.sort(
-					(a, b) =>
-						(this.genreFilterSelected.indexOf(a) === -1 ? 1 : 0) -
-						(this.genreFilterSelected.indexOf(b) === -1 ? 1 : 0)
-				);
-		},
-		...mapState("modalVisibility", {
-			modals: state => state.modals
-		}),
-		...mapState("admin/hiddenSongs", {
-			songs: state => state.songs
-		}),
-		...mapState("modals/editSong", {
-			song: state => state.song
-		}),
-		...mapGetters({
-			socket: "websockets/getSocket"
-		})
-	},
-	mounted() {
-		ws.onConnect(this.init);
-
-		this.socket.on("event:admin.song.updated", res => {
-			const { song } = res.data;
-			if (res.data.oldStatus && res.data.oldStatus === "hidden") {
-				this.removeSong(song._id);
-			} else {
-				this.addSong(song);
-				this.updateSong(song);
-			}
-		});
-	},
-	methods: {
-		edit(song) {
-			this.editSong(song);
-			this.openModal("editSong");
-		},
-		unhide(id) {
-			this.socket.dispatch("songs.unhide", id, res => {
-				new Toast(res.message);
-			});
-		},
-		getSet() {
-			if (this.isGettingSet) return;
-			if (this.position >= this.maxPosition) return;
-			this.isGettingSet = true;
-
-			this.socket.dispatch(
-				"songs.getSet",
-				this.position,
-				"hidden",
-				res => {
-					if (res.status === "success") {
-						res.data.songs.forEach(song => {
-							this.addSong(song);
-						});
-
-						this.position += 1;
-						this.isGettingSet = false;
-					}
-				}
-			);
-		},
-		selectPrevious(event) {
-			if (event.srcElement.previousElementSibling)
-				event.srcElement.previousElementSibling.focus();
-		},
-		selectNext(event) {
-			if (event.srcElement.nextElementSibling)
-				event.srcElement.nextElementSibling.focus();
-		},
-		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
-				);
-		},
-		toggleSearchBox() {
-			this.searchBoxShown = !this.searchBoxShown;
-		},
-		toggleFilterArtistsBox() {
-			this.filterArtistBoxShown = !this.filterArtistBoxShown;
-		},
-		toggleFilterGenresBox() {
-			this.filterGenreBoxShown = !this.filterGenreBoxShown;
-		},
-		toggleKeyboardShortcutsHelper() {
-			this.$refs.keyboardShortcutsHelper.toggleBox();
-		},
-		resetKeyboardShortcutsHelper() {
-			this.$refs.keyboardShortcutsHelper.resetBox();
-		},
-		init() {
-			this.position = 1;
-			this.maxPosition = 1;
-			this.resetSongs();
-
-			if (this.songs.length > 0)
-				this.position = Math.ceil(this.songs.length / 15) + 1;
-
-			this.socket.dispatch("songs.length", "hidden", res => {
-				if (res.status === "success") {
-					this.maxPosition = Math.ceil(res.data.length / 15) + 1;
-					return this.getSet();
-				}
-				return new Toast(`Error: ${res.mesage}`);
-			});
-
-			this.socket.dispatch("apis.joinAdminRoom", "hiddenSongs");
-		},
-		...mapActions("admin/hiddenSongs", [
-			"resetSongs",
-			"addSong",
-			"removeSong",
-			"updateSong"
-		]),
-		...mapActions("modals/editSong", ["editSong"]),
-		...mapActions("modalVisibility", ["openModal"])
-	}
-};
-</script>
-
-<style lang="scss" scoped>
-.night-mode {
-	.box {
-		background-color: var(--dark-grey-3) !important;
-	}
-
-	.table {
-		color: var(--light-grey-2);
-		background-color: var(--dark-grey-3);
-
-		thead tr {
-			background: var(--dark-grey-3);
-			td {
-				color: var(--white);
-			}
-		}
-
-		tbody tr:hover {
-			background-color: var(--dark-grey-4) !important;
-		}
-
-		tbody tr:nth-child(even) {
-			background-color: var(--dark-grey-2);
-		}
-
-		strong {
-			color: var(--light-grey-2);
-		}
-	}
-}
-
-.box {
-	background-color: var(--light-grey);
-	border-radius: 5px;
-	padding: 8px 16px;
-
-	p {
-		text-align: center;
-		font-size: 24px;
-		user-select: none;
-		cursor: pointer;
-		display: flex;
-		justify-content: center;
-		align-items: center;
-	}
-
-	input[type="text"] {
-		margin-top: 8px;
-		margin-bottom: 8px;
-	}
-
-	label {
-		margin-right: 8px;
-		display: inline-flex;
-		align-items: center;
-
-		input[type="checkbox"] {
-			margin-right: 2px;
-			height: 16px;
-			width: 16px;
-		}
-	}
-}
-
-.optionsColumn {
-	width: 140px;
-
-	div {
-		button {
-			width: 35px;
-
-			&:not(:last-child) {
-				margin-right: 5px;
-			}
-		}
-	}
-}
-
-.song-thumbnail {
-	display: block;
-	max-width: 50px;
-	margin: 0 auto;
-}
-
-td {
-	vertical-align: middle;
-}
-
-#keyboardShortcutsHelper {
-	.box-body {
-		.biggest {
-			font-size: 18px;
-		}
-
-		.bigger {
-			font-size: 16px;
-		}
-
-		span {
-			display: block;
-		}
-	}
-}
-
-.is-primary:focus {
-	background-color: var(--primary-color) !important;
-}
-</style>

+ 70 - 27
frontend/src/pages/Admin/tabs/VerifiedSongs.vue → frontend/src/pages/Admin/tabs/Songs.vue

@@ -185,7 +185,17 @@
 								>
 									<i class="material-icons">edit</i>
 								</button>
+								<button
+									v-if="song.status !== 'verified'"
+									class="button is-success"
+									@click="verify(song._id)"
+									content="Verify Song"
+									v-tippy
+								>
+									<i class="material-icons">check_circle</i>
+								</button>
 								<confirm
+									v-if="song.status === 'verified'"
 									placement="left"
 									@confirm="unverify(song._id)"
 								>
@@ -197,6 +207,30 @@
 										<i class="material-icons">cancel</i>
 									</button>
 								</confirm>
+								<confirm
+									v-if="song.status !== 'hidden'"
+									placement="left"
+									@confirm="hide(song._id)"
+								>
+									<button
+										class="button is-danger"
+										content="Hide Song"
+										v-tippy
+									>
+										<i class="material-icons"
+											>visibility_off</i
+										>
+									</button>
+								</confirm>
+								<button
+									v-if="song.status === 'hidden'"
+									class="button is-success"
+									@click="unhide(song._id)"
+									content="Unhide Song"
+									v-tippy
+								>
+									<i class="material-icons">visibility</i>
+								</button>
 							</div>
 						</td>
 					</tr>
@@ -414,7 +448,7 @@ export default {
 		...mapState("modalVisibility", {
 			modals: state => state.modals
 		}),
-		...mapState("admin/verifiedSongs", {
+		...mapState("admin/songs", {
 			songs: state => state.songs
 		}),
 		...mapState("modals/editSong", {
@@ -429,12 +463,9 @@ export default {
 
 		this.socket.on("event:admin.song.updated", res => {
 			const { song } = res.data;
-			if (res.data.oldStatus && res.data.oldStatus === "verified") {
-				this.removeSong(song._id);
-			} else {
+			if (this.songs.filter(s => s._id === song._id).length === 0)
 				this.addSong(song);
-				this.updateSong(song);
-			}
+			else this.updateSong(song);
 		});
 
 		if (this.$route.query.songId) {
@@ -449,7 +480,7 @@ export default {
 		}
 
 		keyboardShortcuts.registerShortcut(
-			"verifiedSongs.toggleKeyboardShortcutsHelper",
+			"songs.toggleKeyboardShortcutsHelper",
 			{
 				keyCode: 191, // '/' key
 				ctrl: true,
@@ -461,7 +492,7 @@ export default {
 		);
 
 		keyboardShortcuts.registerShortcut(
-			"verifiedSongs.resetKeyboardShortcutsHelper",
+			"songs.resetKeyboardShortcutsHelper",
 			{
 				keyCode: 191, // '/' key
 				ctrl: true,
@@ -475,8 +506,8 @@ export default {
 	},
 	beforeUnmount() {
 		const shortcutNames = [
-			"verifiedSongs.toggleKeyboardShortcutsHelper",
-			"verifiedSongs.resetKeyboardShortcutsHelper"
+			"songs.toggleKeyboardShortcutsHelper",
+			"songs.resetKeyboardShortcutsHelper"
 		];
 
 		shortcutNames.forEach(shortcutName => {
@@ -488,11 +519,26 @@ export default {
 			this.editSong(song);
 			this.openModal("editSong");
 		},
+		verify(id) {
+			this.socket.dispatch("songs.verify", id, res => {
+				new Toast(res.message);
+			});
+		},
 		unverify(id) {
 			this.socket.dispatch("songs.unverify", id, res => {
 				new Toast(res.message);
 			});
 		},
+		hide(id) {
+			this.socket.dispatch("songs.hide", id, res => {
+				new Toast(res.message);
+			});
+		},
+		unhide(id) {
+			this.socket.dispatch("songs.unhide", id, res => {
+				new Toast(res.message);
+			});
+		},
 		updateAllSongs() {
 			new Toast("Updating all songs, this could take a very long time.");
 			this.socket.dispatch("songs.updateAll", res => {
@@ -505,21 +551,16 @@ export default {
 			if (this.position >= this.maxPosition) return;
 			this.isGettingSet = true;
 
-			this.socket.dispatch(
-				"songs.getSet",
-				this.position,
-				"verified",
-				res => {
-					if (res.status === "success") {
-						res.data.songs.forEach(song => {
-							this.addSong(song);
-						});
-
-						this.position += 1;
-						this.isGettingSet = false;
-					}
+			this.socket.dispatch("songs.getSet", this.position, res => {
+				if (res.status === "success") {
+					res.data.songs.forEach(song => {
+						this.addSong(song);
+					});
+
+					this.position += 1;
+					this.isGettingSet = false;
 				}
-			);
+			});
 		},
 		toggleArtistSelected(artist) {
 			if (this.artistFilterSelected.indexOf(artist) === -1)
@@ -562,7 +603,7 @@ export default {
 			if (this.songs.length > 0)
 				this.position = Math.ceil(this.songs.length / 15) + 1;
 
-			this.socket.dispatch("songs.length", "verified", res => {
+			this.socket.dispatch("songs.length", res => {
 				if (res.status === "success") {
 					this.maxPosition = Math.ceil(res.data.length / 15) + 1;
 					this.getSet();
@@ -571,7 +612,7 @@ export default {
 
 			this.socket.dispatch("apis.joinAdminRoom", "songs", () => {});
 		},
-		...mapActions("admin/verifiedSongs", [
+		...mapActions("admin/songs", [
 			"resetSongs",
 			"addSong",
 			"removeSong",
@@ -653,7 +694,9 @@ export default {
 	div {
 		button {
 			width: 35px;
-
+		}
+		> button,
+		> span {
 			&:not(:last-child) {
 				margin-right: 5px;
 			}

+ 0 - 643
frontend/src/pages/Admin/tabs/UnverifiedSongs.vue

@@ -1,643 +0,0 @@
-<template>
-	<div>
-		<page-metadata title="Admin | Unverified songs" />
-		<div class="container">
-			<div class="button-row">
-				<button
-					v-if="!loadAllSongs"
-					class="button is-primary"
-					@click="loadAll()"
-				>
-					Load all sets
-				</button>
-				<button
-					class="button is-primary"
-					@click="toggleKeyboardShortcutsHelper"
-					@dblclick="resetKeyboardShortcutsHelper"
-				>
-					Keyboard shortcuts helper
-				</button>
-				<button
-					class="button is-primary"
-					@click="openModal('requestSong')"
-				>
-					Request song
-				</button>
-			</div>
-			<br />
-			<div class="box">
-				<p @click="toggleSearchBox()">
-					Search<i class="material-icons" v-show="searchBoxShown"
-						>expand_more</i
-					>
-					<i class="material-icons" v-show="!searchBoxShown"
-						>expand_less</i
-					>
-				</p>
-				<input
-					v-model="searchQuery"
-					type="text"
-					class="input"
-					placeholder="Search for Songs"
-					v-show="searchBoxShown"
-				/>
-			</div>
-			<div class="box">
-				<p @click="toggleFilterArtistsBox()">
-					Filter artists<i
-						class="material-icons"
-						v-show="filterArtistBoxShown"
-						>expand_more</i
-					>
-					<i class="material-icons" v-show="!filterArtistBoxShown"
-						>expand_less</i
-					>
-				</p>
-				<input
-					type="text"
-					class="input"
-					placeholder="Filter artist checkboxes"
-					v-model="artistFilterQuery"
-					v-show="filterArtistBoxShown"
-				/>
-				<label
-					v-for="artist in filteredArtists"
-					:key="artist"
-					v-show="filterArtistBoxShown"
-				>
-					<input
-						type="checkbox"
-						:checked="artistFilterSelected.indexOf(artist) !== -1"
-						@click="toggleArtistSelected(artist)"
-					/>
-					<span>{{ artist }}</span>
-				</label>
-			</div>
-			<div class="box">
-				<p @click="toggleFilterGenresBox()">
-					Filter genres<i
-						class="material-icons"
-						v-show="filterGenreBoxShown"
-						>expand_more</i
-					>
-					<i class="material-icons" v-show="!filterGenreBoxShown"
-						>expand_less</i
-					>
-				</p>
-				<input
-					type="text"
-					class="input"
-					placeholder="Filter genre checkboxes"
-					v-model="genreFilterQuery"
-					v-show="filterGenreBoxShown"
-				/>
-				<label
-					v-for="genre in filteredGenres"
-					:key="genre"
-					v-show="filterGenreBoxShown"
-				>
-					<input
-						type="checkbox"
-						:checked="genreFilterSelected.indexOf(genre) !== -1"
-						@click="toggleGenreSelected(genre)"
-					/>
-					<span>{{ genre }}</span>
-				</label>
-			</div>
-			<p>
-				<span>Sets loaded: {{ setsLoaded }} / {{ maxSets }}</span>
-				<br />
-				<span>Loaded songs: {{ songs.length }}</span>
-			</p>
-			<br />
-			<table class="table">
-				<thead>
-					<tr>
-						<td>Thumbnail</td>
-						<td>Title</td>
-						<td>Artists</td>
-						<td>Genres</td>
-						<td>ID / YouTube ID</td>
-						<td>Requested By</td>
-						<td>Options</td>
-					</tr>
-				</thead>
-				<tbody>
-					<tr
-						v-for="song in filteredSongs"
-						:key="song._id"
-						tabindex="0"
-						@keydown.up.prevent
-						@keydown.down.prevent
-						@keyup.up="selectPrevious($event)"
-						@keyup.down="selectNext($event)"
-						@keyup.e="edit(song, index)"
-						@keyup.a="add(song)"
-						@keyup.x="remove(song._id, index)"
-					>
-						<td>
-							<img
-								class="song-thumbnail"
-								:src="song.thumbnail"
-								onerror="this.src='/assets/notes-transparent.png'"
-							/>
-						</td>
-						<td>
-							<strong>{{ song.title }}</strong>
-						</td>
-						<td>{{ song.artists.join(", ") }}</td>
-						<td>{{ song.genres.join(", ") }}</td>
-						<td>
-							{{ song._id }}
-							<br />
-							<a
-								:href="
-									'https://www.youtube.com/watch?v=' +
-									`${song.youtubeId}`
-								"
-								target="_blank"
-							>
-								{{ song.youtubeId }}</a
-							>
-						</td>
-						<td>
-							<user-id-to-username
-								:user-id="song.requestedBy"
-								:link="true"
-							/>
-						</td>
-						<td class="optionsColumn">
-							<div>
-								<button
-									class="button is-primary"
-									@click="edit(song, index)"
-									content="Edit Song"
-									v-tippy
-								>
-									<i class="material-icons">edit</i>
-								</button>
-								<button
-									class="button is-success"
-									@click="verify(song._id)"
-									content="Verify Song"
-									v-tippy
-								>
-									<i class="material-icons">check_circle</i>
-								</button>
-								<confirm
-									placement="left"
-									@confirm="hide(song._id)"
-								>
-									<button
-										class="button is-danger"
-										content="Hide Song"
-										v-tippy
-									>
-										<i class="material-icons"
-											>visibility_off</i
-										>
-									</button>
-								</confirm>
-							</div>
-						</td>
-					</tr>
-				</tbody>
-			</table>
-		</div>
-		<import-album v-if="modals.importAlbum" />
-		<edit-song v-if="modals.editSong" song-type="songs" :key="song._id" />
-		<report v-if="modals.report" />
-		<request-song v-if="modals.requestSong" />
-		<floating-box
-			id="keyboardShortcutsHelper"
-			ref="keyboardShortcutsHelper"
-		>
-			<template #body>
-				<div>
-					<div>
-						<span class="biggest"
-							><b>Unverified 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>
-
-<script>
-import { mapState, mapActions, mapGetters } from "vuex";
-import { defineAsyncComponent } from "vue";
-
-import Toast from "toasters";
-
-import UserIdToUsername from "@/components/UserIdToUsername.vue";
-import FloatingBox from "@/components/FloatingBox.vue";
-import Confirm from "@/components/Confirm.vue";
-
-import ScrollAndFetchHandler from "@/mixins/ScrollAndFetchHandler.vue";
-
-import ws from "@/ws";
-
-export default {
-	components: {
-		EditSong: defineAsyncComponent(() =>
-			import("@/components/modals/EditSong")
-		),
-		Report: defineAsyncComponent(() =>
-			import("@/components/modals/Report.vue")
-		),
-		ImportAlbum: defineAsyncComponent(() =>
-			import("@/components/modals/ImportAlbum.vue")
-		),
-		RequestSong: defineAsyncComponent(() =>
-			import("@/components/modals/RequestSong.vue")
-		),
-		UserIdToUsername,
-		FloatingBox,
-		Confirm
-	},
-	mixins: [ScrollAndFetchHandler],
-	data() {
-		return {
-			searchQuery: "",
-			artistFilterQuery: "",
-			artistFilterSelected: [],
-			genreFilterQuery: "",
-			genreFilterSelected: [],
-			searchBoxShown: true,
-			filterArtistBoxShown: false,
-			filterGenreBoxShown: false
-		};
-	},
-	computed: {
-		filteredSongs() {
-			return this.songs.filter(
-				song =>
-					JSON.stringify(Object.values(song))
-						.toLowerCase()
-						.indexOf(this.searchQuery.toLowerCase()) !== -1 &&
-					(this.artistFilterSelected.length === 0 ||
-						song.artists.some(
-							artist =>
-								this.artistFilterSelected
-									.map(artistFilterSelected =>
-										artistFilterSelected.toLowerCase()
-									)
-									.indexOf(artist.toLowerCase()) !== -1
-						)) &&
-					(this.genreFilterSelected.length === 0 ||
-						song.genres.some(
-							genre =>
-								this.genreFilterSelected
-									.map(genreFilterSelected =>
-										genreFilterSelected.toLowerCase()
-									)
-									.indexOf(genre.toLowerCase()) !== -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
-							.toLowerCase()
-							.indexOf(this.artistFilterQuery.toLowerCase()) !==
-							-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
-							.toLowerCase()
-							.indexOf(this.genreFilterQuery.toLowerCase()) !== -1
-				)
-				.sort(
-					(a, b) =>
-						(this.genreFilterSelected.indexOf(a) === -1 ? 1 : 0) -
-						(this.genreFilterSelected.indexOf(b) === -1 ? 1 : 0)
-				);
-		},
-		...mapState("modalVisibility", {
-			modals: state => state.modals
-		}),
-		...mapState("admin/unverifiedSongs", {
-			songs: state => state.songs
-		}),
-		...mapState("modals/editSong", {
-			song: state => state.song
-		}),
-		...mapGetters({
-			socket: "websockets/getSocket"
-		})
-	},
-	mounted() {
-		ws.onConnect(this.init);
-
-		this.socket.on("event:admin.song.updated", res => {
-			const { song } = res.data;
-			if (res.data.oldStatus && res.data.oldStatus === "unverified") {
-				this.removeSong(song._id);
-			} else {
-				this.addSong(song);
-				this.updateSong(song);
-			}
-		});
-	},
-	methods: {
-		edit(song) {
-			this.editSong(song);
-			this.openModal("editSong");
-		},
-		verify(id) {
-			this.socket.dispatch("songs.verify", id, res => {
-				new Toast(res.message);
-			});
-		},
-		hide(id) {
-			this.socket.dispatch("songs.hide", id, res => {
-				new Toast(res.message);
-			});
-		},
-		getSet() {
-			if (this.isGettingSet) return;
-			if (this.position >= this.maxPosition) return;
-			this.isGettingSet = true;
-
-			this.socket.dispatch(
-				"songs.getSet",
-				this.position,
-				"unverified",
-				res => {
-					if (res.status === "success") {
-						res.data.songs.forEach(song => {
-							this.addSong(song);
-						});
-
-						this.position += 1;
-						this.isGettingSet = false;
-					}
-				}
-			);
-		},
-		selectPrevious(event) {
-			if (event.srcElement.previousElementSibling)
-				event.srcElement.previousElementSibling.focus();
-		},
-		selectNext(event) {
-			if (event.srcElement.nextElementSibling)
-				event.srcElement.nextElementSibling.focus();
-		},
-		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
-				);
-		},
-		toggleSearchBox() {
-			this.searchBoxShown = !this.searchBoxShown;
-		},
-		toggleFilterArtistsBox() {
-			this.filterArtistBoxShown = !this.filterArtistBoxShown;
-		},
-		toggleFilterGenresBox() {
-			this.filterGenreBoxShown = !this.filterGenreBoxShown;
-		},
-		toggleKeyboardShortcutsHelper() {
-			this.$refs.keyboardShortcutsHelper.toggleBox();
-		},
-		resetKeyboardShortcutsHelper() {
-			this.$refs.keyboardShortcutsHelper.resetBox();
-		},
-		init() {
-			this.position = 1;
-			this.maxPosition = 1;
-			this.resetSongs();
-
-			if (this.songs.length > 0)
-				this.position = Math.ceil(this.songs.length / 15) + 1;
-
-			this.socket.dispatch("songs.length", "unverified", res => {
-				if (res.status === "success") {
-					this.maxPosition = Math.ceil(res.data.length / 15) + 1;
-					this.getSet();
-				}
-			});
-
-			this.socket.dispatch(
-				"apis.joinAdminRoom",
-				"unverifiedSongs",
-				() => {}
-			);
-		},
-		...mapActions("admin/unverifiedSongs", [
-			"resetSongs",
-			"addSong",
-			"removeSong",
-			"updateSong"
-		]),
-		...mapActions("modals/editSong", ["editSong"]),
-		...mapActions("modalVisibility", ["openModal"])
-	}
-};
-</script>
-
-<style lang="scss" scoped>
-.night-mode {
-	.box {
-		background-color: var(--dark-grey-3) !important;
-	}
-
-	.table {
-		color: var(--light-grey-2);
-		background-color: var(--dark-grey-3);
-
-		thead tr {
-			background: var(--dark-grey-3);
-			td {
-				color: var(--white);
-			}
-		}
-
-		tbody tr:hover {
-			background-color: var(--dark-grey-4) !important;
-		}
-
-		tbody tr:nth-child(even) {
-			background-color: var(--dark-grey-2);
-		}
-
-		strong {
-			color: var(--light-grey-2);
-		}
-	}
-}
-
-.box {
-	background-color: var(--light-grey);
-	border-radius: 5px;
-	padding: 8px 16px;
-
-	p {
-		text-align: center;
-		font-size: 24px;
-		user-select: none;
-		cursor: pointer;
-		display: flex;
-		justify-content: center;
-		align-items: center;
-	}
-
-	input[type="text"] {
-		margin-top: 8px;
-		margin-bottom: 8px;
-	}
-
-	label {
-		margin-right: 8px;
-		display: inline-flex;
-		align-items: center;
-
-		input[type="checkbox"] {
-			margin-right: 2px;
-			height: 16px;
-			width: 16px;
-		}
-	}
-}
-
-.optionsColumn {
-	width: 140px;
-
-	div {
-		button {
-			width: 35px;
-
-			&:not(:last-child) {
-				margin-right: 5px;
-			}
-		}
-	}
-}
-
-.song-thumbnail {
-	display: block;
-	max-width: 50px;
-	margin: 0 auto;
-}
-
-td {
-	vertical-align: middle;
-
-	& > div {
-		display: inline-flex;
-	}
-}
-
-#keyboardShortcutsHelper {
-	.box-body {
-		.biggest {
-			font-size: 18px;
-		}
-
-		.bigger {
-			font-size: 16px;
-		}
-
-		span {
-			display: block;
-		}
-	}
-}
-
-.is-primary:focus {
-	background-color: var(--primary-color) !important;
-}
-</style>

+ 1 - 65
frontend/src/store/modules/admin.js

@@ -9,71 +9,7 @@ const actions = {};
 const mutations = {};
 
 const modules = {
-	hiddenSongs: {
-		namespaced: true,
-		state: {
-			songs: []
-		},
-		getters: {},
-		actions: {
-			resetSongs: ({ commit }) => commit("resetSongs"),
-			addSong: ({ commit }, song) => commit("addSong", song),
-			removeSong: ({ commit }, songId) => commit("removeSong", songId),
-			updateSong: ({ commit }, updatedSong) =>
-				commit("updateSong", updatedSong)
-		},
-		mutations: {
-			resetSongs(state) {
-				state.songs = [];
-			},
-			addSong(state, song) {
-				if (!state.songs.find(s => s._id === song._id))
-					state.songs.push(song);
-			},
-			removeSong(state, songId) {
-				state.songs = state.songs.filter(song => song._id !== songId);
-			},
-			updateSong(state, updatedSong) {
-				state.songs.forEach((song, index) => {
-					if (song._id === updatedSong._id)
-						state.songs[index] = updatedSong;
-				});
-			}
-		}
-	},
-	unverifiedSongs: {
-		namespaced: true,
-		state: {
-			songs: []
-		},
-		getters: {},
-		actions: {
-			resetSongs: ({ commit }) => commit("resetSongs"),
-			addSong: ({ commit }, song) => commit("addSong", song),
-			removeSong: ({ commit }, songId) => commit("removeSong", songId),
-			updateSong: ({ commit }, updatedSong) =>
-				commit("updateSong", updatedSong)
-		},
-		mutations: {
-			resetSongs(state) {
-				state.songs = [];
-			},
-			addSong(state, song) {
-				if (!state.songs.find(s => s._id === song._id))
-					state.songs.push(song);
-			},
-			removeSong(state, songId) {
-				state.songs = state.songs.filter(song => song._id !== songId);
-			},
-			updateSong(state, updatedSong) {
-				state.songs.forEach((song, index) => {
-					if (song._id === updatedSong._id)
-						state.songs[index] = updatedSong;
-				});
-			}
-		}
-	},
-	verifiedSongs: {
+	songs: {
 		namespaced: true,
 		state: {
 			songs: []