Просмотр исходного кода

feat: add dedicated create artist modal

Kristian Vos 5 дней назад
Родитель
Сommit
7d980dc571

+ 1 - 0
frontend/src/components/ModalManager.vue

@@ -16,6 +16,7 @@ const modalComponents = shallowRef(
 		createStation: "CreateStation.vue",
 		editNews: "EditNews.vue",
 		editArtist: "EditArtist.vue",
+		createArtist: "CreateArtist.vue",
 		editAlbum: "EditAlbum.vue",
 		manageStation: "ManageStation/index.vue",
 		importArtistMB: "ImportArtistMB.vue",

+ 357 - 0
frontend/src/components/modals/CreateArtist.vue

@@ -0,0 +1,357 @@
+<script setup lang="ts">
+import Toast from "toasters";
+import { defineAsyncComponent, ref, computed } from "vue";
+import { useRouter } from "vue-router";
+import { useWebsocketsStore } from "@/stores/websockets";
+import { useModalsStore } from "@/stores/modals";
+import { MusicBrainzArtistTemp } from "@/types/artist";
+
+const Modal = defineAsyncComponent(() => import("@/components/Modal.vue"));
+// const SaveButton = defineAsyncComponent(
+// 	() => import("@/components/SaveButton.vue")
+// );
+
+defineProps({
+	modalUuid: { type: String, required: true }
+});
+
+const { socket } = useWebsocketsStore();
+const { closeCurrentModal } = useModalsStore();
+const router = useRouter();
+
+const musicbrainzArtistSearchQuery = ref("");
+const musicbrainzArtists = ref<MusicBrainzArtistTemp[]>([]);
+const selectedMusicbrainzArtistId = ref("");
+
+type GetMusicbrainzArtistResponse = {
+	data: MusicBrainzArtistTemp;
+};
+
+type CreateArtistResponse = {
+	data: {
+		artistId: string;
+	};
+	message: string;
+	status: "success" | "error";
+};
+
+const createArtist = async () => {
+	const musicbrainzIdentifier = selectedMusicbrainzArtistId.value;
+
+	const musicbrainzArtistRes = (await socket.dispatchAsync(
+		"artists.getMusicbrainzArtist",
+		musicbrainzIdentifier
+	)) as { res: GetMusicbrainzArtistResponse };
+	const { data: musicbrainzData } = musicbrainzArtistRes.res;
+	if (!musicbrainzData) {
+		new Toast("Failed to get MusicBrainz artist data.");
+		return null;
+	}
+
+	const { aliases, id } = musicbrainzData;
+	let { name } = musicbrainzData;
+	aliases.forEach(alias => {
+		if (alias.locale === "en" && alias.primary === true) name = alias.name;
+	});
+
+	const data = {
+		name,
+		musicbrainzIdentifier: id,
+		musicbrainzData
+	};
+
+	const createArtistRes = (await socket.dispatchAsync(
+		"artists.create",
+		data
+	)) as { res: CreateArtistResponse };
+	if (createArtistRes.res.status !== "success") {
+		new Toast("Failed to create artist.");
+		new Toast(createArtistRes.res.message);
+		return null;
+	}
+
+	const { artistId } = createArtistRes.res.data;
+
+	return artistId;
+};
+
+const alternativeNameMap = computed(() => {
+	const map = {};
+	musicbrainzArtists.value.forEach(musicbrainzArtist => {
+		const { aliases, id } = musicbrainzArtist;
+		let name;
+		if (!aliases) return;
+		aliases.forEach(alias => {
+			if (alias.locale === "en" && alias.primary === true)
+				name = alias.name;
+		});
+		if (musicbrainzArtist.name === name) return;
+		if (name) map[id] = name;
+	});
+	return map;
+});
+
+const createArtistAndEdit = async () => {
+	const artistId = await createArtist();
+	if (!artistId) return;
+	closeCurrentModal();
+	router.push({
+		path: "/admin/artists",
+		query: {
+			artistId
+		}
+	});
+};
+
+const createArtistAndClose = async () => {
+	const artistId = await createArtist();
+	if (!artistId) return;
+	closeCurrentModal();
+};
+
+const searchMusicbrainzArtists = () => {
+	socket.dispatch(
+		"artists.searchMusicbrainzArtists",
+		musicbrainzArtistSearchQuery.value,
+		res => {
+			// TODO handle error
+			const { data } = res;
+			const { musicbrainzArtists: _musicbrainzArtists } = data;
+			musicbrainzArtists.value = _musicbrainzArtists;
+		}
+	);
+};
+
+const selectMusicbrainzArtist = musicbrainzArtistId => {
+	selectedMusicbrainzArtistId.value = musicbrainzArtistId;
+};
+</script>
+
+<template>
+	<modal
+		class="create-artist-modal"
+		title="Create Artist"
+		size="slim"
+		:split="false"
+	>
+		<template #body>
+			<div class="flex flex-column w-full gap-4">
+				<div class="flex flex-column gap-4 w-full">
+					<div class="control is-grouped w-full">
+						<div
+							class="musicbrainz-artist-search-query-container w-full"
+						>
+							<label class="label"
+								>MusicBrainz artist search query</label
+							>
+							<p class="control has-addons">
+								<input
+									class="input"
+									type="text"
+									v-model="musicbrainzArtistSearchQuery"
+									placeholder="Enter artist name..."
+									@keyup.enter="searchMusicbrainzArtists()"
+								/>
+							</p>
+						</div>
+					</div>
+					<button
+						class="button is-primary"
+						@click="searchMusicbrainzArtists()"
+					>
+						Search MusicBrainz artists
+					</button>
+				</div>
+				<div class="musicbrainz-artists">
+					<div
+						class="musicbrainz-artist"
+						:class="
+							musicbrainzArtist.id === selectedMusicbrainzArtistId
+								? 'active-musicbrainz-artist'
+								: ''
+						"
+						v-for="musicbrainzArtist in musicbrainzArtists"
+						:key="musicbrainzArtist.id"
+						@click="selectMusicbrainzArtist(musicbrainzArtist.id)"
+					>
+						<p class="musicbrainz-artist-title">
+							<a
+								:href="`https://musicbrainz.org/artist/${musicbrainzArtist.id}`"
+								target="_blank"
+							>
+								{{ musicbrainzArtist.name }}
+							</a>
+							<span class="musicbrainz-artist-disambiguation">
+								{{ musicbrainzArtist.disambiguation }}
+							</span>
+							<span>
+								{{ musicbrainzArtist.type }}
+							</span>
+						</p>
+						<p
+							v-if="alternativeNameMap[musicbrainzArtist.id]"
+							class="musicbrainz-artist-alternative-title"
+						>
+							<span class="material-icons">list_alt</span>
+							<span>
+								{{ alternativeNameMap[musicbrainzArtist.id] }}
+							</span>
+						</p>
+						<p
+							v-if="musicbrainzArtist.area"
+							class="musicbrainz-artist-area"
+						>
+							<span class="material-icons">public</span>
+							<span>
+								{{ musicbrainzArtist.area.name }}
+							</span>
+							<span v-if="musicbrainzArtist.area.disambigutation">
+								({{ musicbrainzArtist.area.disambigutation }})
+							</span>
+							<span v-if="musicbrainzArtist.country">
+								({{ musicbrainzArtist.country }})
+							</span>
+						</p>
+						<p v-else-if="musicbrainzArtist.country">
+							<span class="material-icons">public</span>
+							<span>
+								{{ musicbrainzArtist.country }}
+							</span>
+						</p>
+						<p
+							v-if="
+								musicbrainzArtist['life-span'].begin ||
+								musicbrainzArtist['life-span'].ended
+							"
+							class="musicbrainz-artist-life-span"
+						>
+							<span class="material-icons">schedule</span>
+							<span>{{
+								musicbrainzArtist["life-span"].begin ??
+								"unknown"
+							}}</span
+							>-<span>{{
+								musicbrainzArtist["life-span"].ended ?? "now"
+							}}</span>
+						</p>
+						<p class="musicbrainz-artist-score">
+							<span class="material-icons">percent</span>
+							<span> Score: {{ musicbrainzArtist.score }} </span>
+						</p>
+
+						<div
+							class="musicbrainz-artist-tags"
+							v-if="
+								musicbrainzArtist.tags &&
+								musicbrainzArtist.tags.length
+							"
+						>
+							<span class="material-icons">label</span>
+							<span
+								v-for="tag in musicbrainzArtist.tags"
+								:key="tag.name"
+								class="musicbrainz-artist-tag"
+							>
+								{{ tag.name }} ({{ tag.count }})
+							</span>
+						</div>
+					</div>
+				</div>
+			</div>
+		</template>
+		<template #footer>
+			<div>
+				<button
+					v-if="selectedMusicbrainzArtistId"
+					class="button is-primary"
+					@click="createArtistAndEdit()"
+				>
+					<span>Create and edit</span>
+				</button>
+				<button
+					v-if="selectedMusicbrainzArtistId"
+					class="button is-primary"
+					@click="createArtistAndClose()"
+				>
+					<span>Create and close</span>
+				</button>
+			</div>
+		</template>
+	</modal>
+</template>
+
+<style lang="less" scoped>
+.flex {
+	display: flex;
+}
+
+.flex-column {
+	flex-direction: column;
+}
+
+.gap-4 {
+	gap: 1rem;
+}
+
+.w-full {
+	width: 100%;
+}
+
+.musicbrainz-artists {
+	display: flex;
+	flex-direction: column;
+	gap: 8px;
+
+	.musicbrainz-artist {
+		padding: 8px;
+		border-radius: 8px;
+		background-color: var(--dark-grey);
+		display: flex;
+		flex-direction: column;
+		gap: 4px;
+
+		&.active-musicbrainz-artist {
+			outline: 1px solid var(--white);
+		}
+
+		.musicbrainz-artist-title {
+			display: flex;
+			gap: 8px;
+			line-height: 20px;
+
+			a {
+				font-size: 20px;
+			}
+
+			.musicbrainz-artist-disambiguation {
+				flex: 1;
+			}
+		}
+
+		.musicbrainz-artist-alternative-title,
+		.musicbrainz-artist-area,
+		.musicbrainz-artist-life-span,
+		.musicbrainz-artist-score {
+			display: flex;
+			flex-direction: row;
+			gap: 4px;
+			line-height: 24px;
+		}
+
+		.musicbrainz-artist-tags {
+			display: flex;
+			flex-wrap: wrap;
+			gap: 4px;
+			line-height: 24px;
+
+			.musicbrainz-artist-tag {
+				background-color: var(--dark-grey-2);
+				padding: 4px 8px;
+				border-radius: 16px;
+				line-height: 16px;
+				font-size: 16px;
+			}
+		}
+	}
+}
+</style>

+ 1 - 2
frontend/src/pages/Admin/Artists.vue

@@ -137,8 +137,7 @@ const remove = (id: string) => {
 					class="is-primary button"
 					@click="
 						openModal({
-							modal: 'editArtist',
-							props: { createArtist: true }
+							modal: 'createArtist'
 						})
 					"
 				>