Browse Source

Merge branch 'polishing' into owen

Owen Diffey 3 years ago
parent
commit
8e89e922de

+ 19 - 5
frontend/src/App.vue

@@ -683,19 +683,27 @@ a {
 
 .tippy-box[data-theme~="addToPlaylist"] {
 	font-size: 15px;
-	padding: 5px;
+	padding: 0;
 	border: 1px solid var(--light-grey-3);
 	box-shadow: 0 14px 28px rgba(0, 0, 0, 0.25), 0 10px 10px rgba(0, 0, 0, 0.22);
 	background-color: var(--white);
 	color: var(--dark-grey);
-	width: 100%;
+	width: 350px;
+
+	.tippy-content {
+		padding: 0;
+	}
 
 	.nav-dropdown-items {
+		max-height: 220px;
+		overflow-y: auto;
+		padding: 10px 10px 0 10px;
+
 		.nav-item {
 			width: 100%;
 			justify-content: flex-start;
 			border: 0;
-			padding: 10px;
+			padding: 8px 4px;
 			font-size: 15.5px;
 			min-height: 36px;
 			background: var(--light-grey);
@@ -707,6 +715,7 @@ a {
 				flex-direction: row;
 				align-items: center;
 				overflow-wrap: anywhere;
+				margin: 0 !important;
 
 				p {
 					margin-left: 10px;
@@ -776,13 +785,18 @@ a {
 	.tippy-content > span {
 		display: flex;
 		flex-direction: column;
-		button {
-			width: 100%;
+
+		button.nav-item {
 			&:not(:last-of-type) {
 				margin-bottom: 10px;
 			}
 		}
 	}
+
+	#create-playlist {
+		margin: 10px 10px 10px 10px;
+		width: unset;
+	}
 }
 
 .select {

+ 0 - 18
frontend/src/components/AddToPlaylistDropdown.vue

@@ -177,21 +177,3 @@ export default {
 	}
 };
 </script>
-
-<style lang="scss" scoped>
-.nav-dropdown-items button .control {
-	margin-bottom: 0 !important;
-}
-
-#create-playlist {
-	margin-top: 10px;
-
-	i {
-		color: #fff;
-	}
-}
-
-.addToPlaylistDropdown {
-	display: flex;
-}
-</style>

+ 0 - 5
frontend/src/components/modals/EditPlaylist/Tabs/AddSongs.vue

@@ -215,13 +215,8 @@ export default {
 <style lang="scss" scoped>
 .youtube-tab {
 	.song-query-results {
-		padding: 10px;
 		margin-top: 10px;
-		border: 1px solid var(--light-grey-3);
-		border-radius: 3px;
 		max-width: 565px;
-		max-height: 500px;
-		overflow: auto;
 
 		.search-query-item:not(:last-of-type) {
 			margin-bottom: 10px;

+ 20 - 0
frontend/src/pages/Admin/index.vue

@@ -233,6 +233,26 @@ export default {
 };
 </script>
 
+<style lang="scss">
+.main-container .container {
+	.button-row {
+		display: flex;
+		flex-direction: row;
+		flex-wrap: wrap;
+		justify-content: center;
+		margin-bottom: 5px;
+
+		& > .button,
+		& > span {
+			margin: 5px 0;
+			&:not(:first-child) {
+				margin-left: 5px;
+			}
+		}
+	}
+}
+</style>
+
 <style lang="scss" scoped>
 .night-mode {
 	.tabs {

+ 244 - 28
frontend/src/pages/Admin/tabs/HiddenSongs.vue

@@ -2,35 +2,113 @@
 	<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>
-			<input
-				v-model="searchQuery"
-				type="text"
-				class="input"
-				placeholder="Search for Songs"
-			/>
-			<button
-				v-if="!loadAllSongs"
-				class="button is-primary"
-				@click="loadAll()"
-			>
-				Load all
-			</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>
-			<br />
 			<br />
 			<table class="table is-striped">
 				<thead>
@@ -211,18 +289,92 @@ export default {
 	mixins: [ScrollAndFetchHandler],
 	data() {
 		return {
-			searchQuery: ""
+			searchQuery: "",
+			artistFilterQuery: "",
+			artistFilterSelected: [],
+			genreFilterQuery: "",
+			genreFilterSelected: [],
+			searchBoxShown: true,
+			filterArtistBoxShown: false,
+			filterGenreBoxShown: false
 		};
 	},
 	computed: {
 		filteredSongs() {
 			return this.songs.filter(
 				song =>
-					JSON.stringify(Object.values(song)).indexOf(
-						this.searchQuery
-					) !== -1
+					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
 		}),
@@ -288,6 +440,33 @@ export default {
 			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();
 		},
@@ -326,6 +505,10 @@ export default {
 
 <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);
@@ -351,6 +534,39 @@ export default {
 	}
 }
 
+.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;
 

+ 5 - 4
frontend/src/pages/Admin/tabs/News.vue

@@ -2,6 +2,11 @@
 	<div>
 		<page-metadata title="Admin | News" />
 		<div class="container">
+			<div class="button-row">
+				<button class="is-primary button" @click="edit()">
+					Create News Item
+				</button>
+			</div>
 			<table class="table is-striped">
 				<thead>
 					<tr>
@@ -40,10 +45,6 @@
 					</tr>
 				</tbody>
 			</table>
-
-			<button class="is-primary button" @click="edit()">
-				Create News Item
-			</button>
 		</div>
 
 		<edit-news

+ 44 - 44
frontend/src/pages/Admin/tabs/Playlists.vue

@@ -2,50 +2,50 @@
 	<div>
 		<page-metadata title="Admin | Playlists" />
 		<div class="container">
-			<button
-				class="button is-primary"
-				@click="deleteOrphanedStationPlaylists()"
-			>
-				Delete orphaned station playlists
-			</button>
-			<button
-				class="button is-primary"
-				@click="deleteOrphanedGenrePlaylists()"
-			>
-				Delete orphaned genre playlists
-			</button>
-			<button
-				class="button is-primary"
-				@click="deleteOrphanedArtistPlaylists()"
-			>
-				Delete orphaned artist playlists
-			</button>
-			<button
-				class="button is-primary"
-				@click="requestOrphanedPlaylistSongs()"
-			>
-				Request orphaned playlist songs
-			</button>
-			<button
-				class="button is-primary"
-				@click="clearAndRefillAllStationPlaylists()"
-			>
-				Clear and refill all station playlists
-			</button>
-			<button
-				class="button is-primary"
-				@click="clearAndRefillAllGenrePlaylists()"
-			>
-				Clear and refill all genre playlists
-			</button>
-			<button
-				class="button is-primary"
-				@click="clearAndRefillAllArtistPlaylists()"
-			>
-				Clear and refill all artist playlists
-			</button>
-			<br />
-			<br />
+			<div class="button-row">
+				<button
+					class="button is-primary"
+					@click="deleteOrphanedStationPlaylists()"
+				>
+					Delete orphaned station playlists
+				</button>
+				<button
+					class="button is-primary"
+					@click="deleteOrphanedGenrePlaylists()"
+				>
+					Delete orphaned genre playlists
+				</button>
+				<button
+					class="button is-primary"
+					@click="deleteOrphanedArtistPlaylists()"
+				>
+					Delete orphaned artist playlists
+				</button>
+				<button
+					class="button is-primary"
+					@click="requestOrphanedPlaylistSongs()"
+				>
+					Request orphaned playlist songs
+				</button>
+				<button
+					class="button is-primary"
+					@click="clearAndRefillAllStationPlaylists()"
+				>
+					Clear and refill all station playlists
+				</button>
+				<button
+					class="button is-primary"
+					@click="clearAndRefillAllGenrePlaylists()"
+				>
+					Clear and refill all genre playlists
+				</button>
+				<button
+					class="button is-primary"
+					@click="clearAndRefillAllArtistPlaylists()"
+				>
+					Clear and refill all artist playlists
+				</button>
+			</div>
 			<table class="table is-striped">
 				<thead>
 					<tr>

+ 8 - 5
frontend/src/pages/Admin/tabs/Stations.vue

@@ -2,11 +2,14 @@
 	<div>
 		<page-metadata title="Admin | Stations" />
 		<div class="container">
-			<button class="button is-primary" @click="clearEveryStationQueue()">
-				Clear every station queue
-			</button>
-			<br />
-			<br />
+			<div class="button-row">
+				<button
+					class="button is-primary"
+					@click="clearEveryStationQueue()"
+				>
+					Clear every station queue
+				</button>
+			</div>
 			<table class="table is-striped">
 				<thead>
 					<tr>

+ 244 - 28
frontend/src/pages/Admin/tabs/UnverifiedSongs.vue

@@ -2,35 +2,113 @@
 	<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>
-			<input
-				v-model="searchQuery"
-				type="text"
-				class="input"
-				placeholder="Search for Songs"
-			/>
-			<button
-				v-if="!loadAllSongs"
-				class="button is-primary"
-				@click="loadAll()"
-			>
-				Load all
-			</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>
-			<br />
 			<br />
 			<table class="table is-striped">
 				<thead>
@@ -229,18 +307,92 @@ export default {
 	mixins: [ScrollAndFetchHandler],
 	data() {
 		return {
-			searchQuery: ""
+			searchQuery: "",
+			artistFilterQuery: "",
+			artistFilterSelected: [],
+			genreFilterQuery: "",
+			genreFilterSelected: [],
+			searchBoxShown: true,
+			filterArtistBoxShown: false,
+			filterGenreBoxShown: false
 		};
 	},
 	computed: {
 		filteredSongs() {
 			return this.songs.filter(
 				song =>
-					JSON.stringify(Object.values(song)).indexOf(
-						this.searchQuery
-					) !== -1
+					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
 		}),
@@ -311,6 +463,33 @@ export default {
 			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();
 		},
@@ -352,6 +531,10 @@ export default {
 
 <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);
@@ -377,6 +560,39 @@ export default {
 	}
 }
 
+.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;
 

+ 9 - 0
frontend/src/pages/Admin/tabs/Users.vue

@@ -210,6 +210,15 @@ body {
 	font-family: "Hind", sans-serif;
 }
 
+h2 {
+	font-size: 30px;
+	text-align: center;
+
+	@media only screen and (min-width: 700px) {
+		font-size: 35px;
+	}
+}
+
 .profile-picture {
 	max-width: 50px !important;
 	max-height: 50px !important;

+ 137 - 42
frontend/src/pages/Admin/tabs/VerifiedSongs.vue

@@ -2,54 +2,79 @@
 	<div>
 		<page-metadata title="Admin | Songs" />
 		<div class="container">
-			<p>
-				<span>Sets loaded: {{ setsLoaded }} / {{ maxSets }}</span>
-				<br />
-				<span>Loaded songs: {{ songs.length }}</span>
-			</p>
-			<input
-				v-model="searchQuery"
-				type="text"
-				class="input"
-				placeholder="Search for Songs"
-			/>
-			<button
-				v-if="!loadAllSongs"
-				class="button is-primary"
-				@click="loadAll()"
-			>
-				Load all
-			</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>
-			<button class="button is-primary" @click="openModal('importAlbum')">
-				Import album
-			</button>
-			<confirm placement="bottom" @confirm="updateAllSongs()">
+			<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-danger"
-					content="Update all songs"
-					v-tippy
+					class="button is-primary"
+					@click="openModal('requestSong')"
 				>
-					Update all songs
+					Request song
 				</button>
-			</confirm>
+				<button
+					class="button is-primary"
+					@click="openModal('importAlbum')"
+				>
+					Import album
+				</button>
+				<confirm placement="bottom" @confirm="updateAllSongs()">
+					<button class="button is-danger">Update all songs</button>
+				</confirm>
+			</div>
 			<br />
-			<div>
+			<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">
+				<label
+					v-for="artist in filteredArtists"
+					:key="artist"
+					v-show="filterArtistBoxShown"
+				>
 					<input
 						type="checkbox"
 						:checked="artistFilterSelected.indexOf(artist) !== -1"
@@ -58,13 +83,29 @@
 					<span>{{ artist }}</span>
 				</label>
 			</div>
-			<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">
+				<label
+					v-for="genre in filteredGenres"
+					:key="genre"
+					v-show="filterGenreBoxShown"
+				>
 					<input
 						type="checkbox"
 						:checked="genreFilterSelected.indexOf(genre) !== -1"
@@ -73,6 +114,11 @@
 					<span>{{ genre }}</span>
 				</label>
 			</div>
+			<p>
+				<span>Sets loaded: {{ setsLoaded }} / {{ maxSets }}</span>
+				<br />
+				<span>Loaded songs: {{ songs.length }}</span>
+			</p>
 			<br />
 			<table class="table is-striped">
 				<thead>
@@ -283,7 +329,10 @@ export default {
 			editing: {
 				index: 0,
 				song: {}
-			}
+			},
+			searchBoxShown: true,
+			filterArtistBoxShown: false,
+			filterGenreBoxShown: false
 		};
 	},
 	computed: {
@@ -490,6 +539,15 @@ export default {
 					1
 				);
 		},
+		toggleSearchBox() {
+			this.searchBoxShown = !this.searchBoxShown;
+		},
+		toggleFilterArtistsBox() {
+			this.filterArtistBoxShown = !this.filterArtistBoxShown;
+		},
+		toggleFilterGenresBox() {
+			this.filterGenreBoxShown = !this.filterGenreBoxShown;
+		},
 		toggleKeyboardShortcutsHelper() {
 			this.$refs.keyboardShortcutsHelper.toggleBox();
 		},
@@ -527,6 +585,10 @@ export default {
 
 <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);
@@ -556,6 +618,39 @@ body {
 	font-family: "Hind", sans-serif;
 }
 
+.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: 100px;