2022-04-15 14:24:30 +00:00
|
|
|
<template>
|
2022-04-21 09:38:24 +00:00
|
|
|
<div class="edit-song" data-testid="edit-song-form" tabindex="0" @keydown.esc="maybeClose">
|
2022-04-15 17:00:08 +00:00
|
|
|
<SoundBar v-if="loading"/>
|
2022-04-15 14:24:30 +00:00
|
|
|
<form v-else @submit.prevent="submit">
|
|
|
|
<header>
|
2022-07-05 22:28:26 +00:00
|
|
|
<span class="cover" :style="{ backgroundImage: `url(${coverUrl})` }"/>
|
2022-04-21 09:38:24 +00:00
|
|
|
<div class="meta">
|
2022-04-15 14:24:30 +00:00
|
|
|
<h1 :class="{ mixed: !editingOnlyOneSong }">{{ displayedTitle }}</h1>
|
2022-06-10 10:47:46 +00:00
|
|
|
<h2 :class="{ mixed: !allSongsAreFromSameArtist && !formData.artist_name }">{{ displayedArtistName }}</h2>
|
|
|
|
<h2 :class="{ mixed: !allSongsAreInSameAlbum && !formData.album_name }">{{ displayedAlbumName }}</h2>
|
2022-04-21 09:38:24 +00:00
|
|
|
</div>
|
2022-04-15 14:24:30 +00:00
|
|
|
</header>
|
|
|
|
|
|
|
|
<div class="tabs">
|
|
|
|
<div class="clear" role="tablist">
|
|
|
|
<button
|
2022-05-06 10:28:02 +00:00
|
|
|
id="editSongTabDetails"
|
2022-04-15 14:24:30 +00:00
|
|
|
:aria-selected="currentView === 'details'"
|
|
|
|
aria-controls="editSongPanelDetails"
|
|
|
|
role="tab"
|
2022-06-10 10:47:46 +00:00
|
|
|
type="button"
|
2022-05-06 10:28:02 +00:00
|
|
|
@click.prevent="currentView = 'details'"
|
2022-04-15 14:24:30 +00:00
|
|
|
>
|
|
|
|
Details
|
|
|
|
</button>
|
|
|
|
<button
|
|
|
|
v-if="editingOnlyOneSong"
|
2022-05-06 10:28:02 +00:00
|
|
|
id="editSongTabLyrics"
|
2022-04-15 14:24:30 +00:00
|
|
|
:aria-selected="currentView === 'lyrics'"
|
|
|
|
aria-controls="editSongPanelLyrics"
|
|
|
|
data-testid="edit-song-lyrics-tab"
|
2022-05-06 10:28:02 +00:00
|
|
|
role="tab"
|
2022-06-10 10:47:46 +00:00
|
|
|
type="button"
|
2022-05-06 10:28:02 +00:00
|
|
|
@click.prevent="currentView = 'lyrics'"
|
2022-04-15 14:24:30 +00:00
|
|
|
>
|
|
|
|
Lyrics
|
|
|
|
</button>
|
|
|
|
</div>
|
|
|
|
|
|
|
|
<div class="panes">
|
|
|
|
<div
|
2022-04-21 09:38:24 +00:00
|
|
|
v-show="currentView === 'details'"
|
2022-04-15 14:24:30 +00:00
|
|
|
id="editSongPanelDetails"
|
2022-04-21 09:38:24 +00:00
|
|
|
aria-labelledby="editSongTabDetails"
|
2022-04-15 14:24:30 +00:00
|
|
|
role="tabpanel"
|
|
|
|
tabindex="0"
|
|
|
|
>
|
2022-05-06 10:28:02 +00:00
|
|
|
<div v-if="editingOnlyOneSong" class="form-row">
|
2022-04-15 14:24:30 +00:00
|
|
|
<label>Title</label>
|
2022-04-21 09:38:24 +00:00
|
|
|
<input v-model="formData.title" v-koel-focus name="title" title="Title" type="text">
|
2022-04-15 14:24:30 +00:00
|
|
|
</div>
|
|
|
|
|
|
|
|
<div class="form-row">
|
|
|
|
<label>Artist</label>
|
2022-04-27 22:23:05 +00:00
|
|
|
<input
|
2022-06-10 10:47:46 +00:00
|
|
|
v-model="formData.artist_name"
|
|
|
|
:placeholder="inputPlaceholder"
|
2022-04-27 22:23:05 +00:00
|
|
|
name="artist"
|
|
|
|
type="text"
|
|
|
|
>
|
2022-04-15 14:24:30 +00:00
|
|
|
</div>
|
|
|
|
|
|
|
|
<div class="form-row">
|
|
|
|
<label>Album</label>
|
2022-04-27 22:23:05 +00:00
|
|
|
<input
|
2022-06-10 10:47:46 +00:00
|
|
|
v-model="formData.album_name"
|
|
|
|
:placeholder="inputPlaceholder"
|
2022-04-27 22:23:05 +00:00
|
|
|
name="album"
|
|
|
|
type="text"
|
|
|
|
>
|
2022-04-15 14:24:30 +00:00
|
|
|
</div>
|
|
|
|
|
|
|
|
<div class="form-row">
|
2022-06-10 10:47:46 +00:00
|
|
|
<label>Album Artist</label>
|
|
|
|
<input
|
|
|
|
v-model="formData.album_artist_name"
|
|
|
|
name="album_artist"
|
|
|
|
:placeholder="inputPlaceholder"
|
|
|
|
type="text"
|
|
|
|
>
|
2022-04-15 14:24:30 +00:00
|
|
|
</div>
|
|
|
|
|
2022-06-10 10:47:46 +00:00
|
|
|
<div class="form-row">
|
|
|
|
<div class="cols">
|
|
|
|
<div>
|
|
|
|
<label>Track</label>
|
|
|
|
<input
|
|
|
|
v-model="formData.track"
|
|
|
|
name="track"
|
|
|
|
pattern="\d*"
|
|
|
|
title="Empty or a number"
|
|
|
|
type="text"
|
|
|
|
:placeholder="inputPlaceholder"
|
|
|
|
>
|
|
|
|
</div>
|
|
|
|
<div>
|
|
|
|
<label>Disc</label>
|
|
|
|
<input
|
|
|
|
v-model="formData.disc"
|
|
|
|
name="disc"
|
|
|
|
pattern="\d*"
|
|
|
|
title="Empty or a number"
|
|
|
|
type="text"
|
|
|
|
:placeholder="inputPlaceholder"
|
|
|
|
>
|
|
|
|
</div>
|
|
|
|
</div>
|
2022-04-15 14:24:30 +00:00
|
|
|
</div>
|
|
|
|
</div>
|
|
|
|
|
|
|
|
<div
|
2022-04-21 09:38:24 +00:00
|
|
|
v-if="editingOnlyOneSong"
|
|
|
|
v-show="currentView === 'lyrics'"
|
2022-04-15 14:24:30 +00:00
|
|
|
id="editSongPanelLyrics"
|
2022-04-21 09:38:24 +00:00
|
|
|
aria-labelledby="editSongTabLyrics"
|
2022-04-15 14:24:30 +00:00
|
|
|
role="tabpanel"
|
|
|
|
tabindex="0"
|
|
|
|
>
|
|
|
|
<div class="form-row">
|
2022-04-21 09:38:24 +00:00
|
|
|
<textarea v-model="formData.lyrics" v-koel-focus name="lyrics" title="Lyrics"></textarea>
|
2022-04-15 14:24:30 +00:00
|
|
|
</div>
|
|
|
|
</div>
|
|
|
|
</div>
|
|
|
|
</div>
|
|
|
|
|
|
|
|
<footer>
|
2022-04-15 17:00:08 +00:00
|
|
|
<Btn type="submit">Update</Btn>
|
2022-04-21 09:38:24 +00:00
|
|
|
<Btn class="btn-cancel" white @click.prevent="maybeClose">Cancel</Btn>
|
2022-04-15 14:24:30 +00:00
|
|
|
</footer>
|
|
|
|
</form>
|
|
|
|
</div>
|
|
|
|
</template>
|
|
|
|
|
2022-04-15 17:00:08 +00:00
|
|
|
<script lang="ts" setup>
|
2022-07-20 08:00:02 +00:00
|
|
|
import { computed, onMounted, reactive, ref } from 'vue'
|
2022-06-10 10:47:46 +00:00
|
|
|
import { isEqual } from 'lodash'
|
2022-07-20 08:00:02 +00:00
|
|
|
import { alerts, defaultCover, pluralize, requireInjection } from '@/utils'
|
2022-06-10 10:47:46 +00:00
|
|
|
import { songStore } from '@/stores'
|
2022-07-20 08:00:02 +00:00
|
|
|
import { EditSongFormInitialTabKey, SongsKey } from '@/symbols'
|
2022-04-15 17:00:08 +00:00
|
|
|
|
2022-07-07 18:05:46 +00:00
|
|
|
import Btn from '@/components/ui/Btn.vue'
|
|
|
|
import SoundBar from '@/components/ui/SoundBar.vue'
|
|
|
|
|
2022-06-10 10:47:46 +00:00
|
|
|
type EditFormData = Pick<Song, 'title' | 'album_name' | 'artist_name' | 'album_artist_name' | 'lyrics' | 'track' | 'disc'>
|
|
|
|
|
2022-07-20 08:00:02 +00:00
|
|
|
const [initialTab] = requireInjection(EditSongFormInitialTabKey)
|
|
|
|
const [songs] = requireInjection(SongsKey)
|
|
|
|
|
2022-07-10 15:17:48 +00:00
|
|
|
const currentView = ref<EditSongFormTabName>('details')
|
2022-06-10 10:47:46 +00:00
|
|
|
const loading = ref(false)
|
2022-04-15 14:24:30 +00:00
|
|
|
|
2022-07-20 08:00:02 +00:00
|
|
|
const mutatedSongs = computed(() => songs.value)
|
|
|
|
|
2022-04-15 17:00:08 +00:00
|
|
|
/**
|
|
|
|
* In order not to mess up the original songs, we manually assign and manipulate their attributes.
|
|
|
|
*/
|
|
|
|
const formData = reactive<EditFormData>({
|
|
|
|
title: '',
|
2022-06-10 10:47:46 +00:00
|
|
|
album_name: '',
|
|
|
|
artist_name: '',
|
|
|
|
album_artist_name: '',
|
2022-04-15 17:00:08 +00:00
|
|
|
lyrics: '',
|
|
|
|
track: null,
|
2022-06-10 10:47:46 +00:00
|
|
|
disc: null
|
2022-04-15 17:00:08 +00:00
|
|
|
})
|
2022-04-15 14:24:30 +00:00
|
|
|
|
2022-04-30 08:16:28 +00:00
|
|
|
const initialFormData = {}
|
2022-04-15 17:00:08 +00:00
|
|
|
|
|
|
|
const editingOnlyOneSong = computed(() => mutatedSongs.value.length === 1)
|
2022-06-10 10:47:46 +00:00
|
|
|
const inputPlaceholder = computed(() => editingOnlyOneSong.value ? '' : 'Leave unchanged')
|
2022-04-15 17:00:08 +00:00
|
|
|
|
2022-06-10 10:47:46 +00:00
|
|
|
const allSongsAreFromSameArtist = computed(() => allSongsShareSameValue('artist_name'))
|
|
|
|
const allSongsAreInSameAlbum = computed(() => allSongsShareSameValue('album_name'))
|
|
|
|
|
2022-07-05 22:28:26 +00:00
|
|
|
const coverUrl = computed(() => allSongsAreInSameAlbum.value
|
|
|
|
? mutatedSongs.value[0].album_cover || defaultCover
|
|
|
|
: defaultCover
|
|
|
|
)
|
2022-06-10 10:47:46 +00:00
|
|
|
|
|
|
|
const allSongsShareSameValue = (key: keyof EditFormData) => {
|
|
|
|
if (editingOnlyOneSong.value) return true
|
|
|
|
return new Set(mutatedSongs.value.map(song => song[key])).size === 1
|
|
|
|
}
|
2022-04-15 17:00:08 +00:00
|
|
|
|
|
|
|
const displayedTitle = computed(() => {
|
|
|
|
return editingOnlyOneSong.value ? formData.title : `${mutatedSongs.value.length} songs selected`
|
|
|
|
})
|
|
|
|
|
|
|
|
const displayedArtistName = computed(() => {
|
2022-06-10 10:47:46 +00:00
|
|
|
return allSongsAreFromSameArtist.value || formData.artist_name ? formData.artist_name : 'Mixed Artists'
|
2022-04-15 17:00:08 +00:00
|
|
|
})
|
2022-04-15 14:24:30 +00:00
|
|
|
|
2022-04-15 17:00:08 +00:00
|
|
|
const displayedAlbumName = computed(() => {
|
2022-06-10 10:47:46 +00:00
|
|
|
return allSongsAreInSameAlbum.value || formData.album_name ? formData.album_name : 'Mixed Albums'
|
2022-04-15 17:00:08 +00:00
|
|
|
})
|
|
|
|
|
2022-04-21 09:38:24 +00:00
|
|
|
const isPristine = computed(() => isEqual(formData, initialFormData))
|
2022-04-15 17:00:08 +00:00
|
|
|
|
|
|
|
const open = async () => {
|
2022-04-21 09:38:24 +00:00
|
|
|
currentView.value = initialTab.value
|
2022-04-15 17:00:08 +00:00
|
|
|
const firstSong = mutatedSongs.value[0]
|
2022-04-15 14:24:30 +00:00
|
|
|
|
2022-06-10 10:47:46 +00:00
|
|
|
formData.title = allSongsShareSameValue('title') ? firstSong.title : ''
|
|
|
|
formData.album_name = allSongsShareSameValue('album_name') ? firstSong.album_name : ''
|
|
|
|
formData.artist_name = allSongsShareSameValue('artist_name') ? firstSong.artist_name : ''
|
2022-07-04 13:24:02 +00:00
|
|
|
|
|
|
|
// If the album artist is the same as the artist, we set the form value as empty to not confuse the user
|
|
|
|
// and make it less error-prone.
|
|
|
|
if (editingOnlyOneSong.value && firstSong.album_artist_id === firstSong.artist_id) {
|
|
|
|
formData.album_artist_name = ''
|
|
|
|
} else {
|
|
|
|
formData.album_artist_name = allSongsShareSameValue('album_artist_name') ? firstSong.album_artist_name : ''
|
|
|
|
}
|
|
|
|
|
2022-06-10 10:47:46 +00:00
|
|
|
formData.lyrics = editingOnlyOneSong.value ? firstSong.lyrics : ''
|
|
|
|
formData.track = allSongsShareSameValue('track') ? firstSong.track : null
|
|
|
|
formData.disc = allSongsShareSameValue('disc') ? firstSong.disc : null
|
2022-04-21 09:38:24 +00:00
|
|
|
|
2022-04-30 08:16:28 +00:00
|
|
|
Object.assign(initialFormData, formData)
|
2022-04-15 17:00:08 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
const emit = defineEmits(['close'])
|
|
|
|
const close = () => emit('close')
|
|
|
|
|
|
|
|
const maybeClose = () => {
|
|
|
|
if (isPristine.value) {
|
|
|
|
close()
|
|
|
|
return
|
2022-04-15 14:24:30 +00:00
|
|
|
}
|
2022-04-15 17:00:08 +00:00
|
|
|
|
|
|
|
alerts.confirm('Discard all changes?', close)
|
|
|
|
}
|
|
|
|
|
|
|
|
const submit = async () => {
|
|
|
|
loading.value = true
|
|
|
|
|
|
|
|
try {
|
|
|
|
await songStore.update(mutatedSongs.value, formData)
|
2022-04-24 17:58:12 +00:00
|
|
|
alerts.success(`Updated ${pluralize(mutatedSongs.value.length, 'song')}.`)
|
2022-04-15 17:00:08 +00:00
|
|
|
close()
|
|
|
|
} finally {
|
|
|
|
loading.value = false
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2022-07-10 15:17:48 +00:00
|
|
|
onMounted(async () => await open())
|
2022-04-15 14:24:30 +00:00
|
|
|
</script>
|
|
|
|
|
|
|
|
<style lang="scss" scoped>
|
|
|
|
form {
|
2022-06-10 10:47:46 +00:00
|
|
|
max-width: 540px;
|
|
|
|
|
2022-04-15 14:24:30 +00:00
|
|
|
.tabs {
|
|
|
|
padding: 0;
|
|
|
|
}
|
|
|
|
|
|
|
|
> header {
|
2022-07-05 22:28:26 +00:00
|
|
|
gap: 1.2rem;
|
|
|
|
|
|
|
|
.cover {
|
|
|
|
flex: 0 0 84px;
|
|
|
|
height: 84px;
|
|
|
|
background-size: cover;
|
|
|
|
border-radius: 5px;
|
2022-04-15 14:24:30 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
.meta {
|
|
|
|
flex: 1;
|
|
|
|
|
|
|
|
.mixed {
|
|
|
|
opacity: .5;
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
2022-06-10 10:47:46 +00:00
|
|
|
|
|
|
|
.form-row .cols {
|
|
|
|
display: flex;
|
|
|
|
place-content: space-between;
|
|
|
|
gap: 1rem;
|
|
|
|
|
|
|
|
> div {
|
|
|
|
flex: 1;
|
|
|
|
}
|
|
|
|
}
|
2022-04-15 14:24:30 +00:00
|
|
|
}
|
|
|
|
</style>
|