koel/resources/assets/js/components/song/SongEditForm.vue

282 lines
8.2 KiB
Vue
Raw Normal View History

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-04-21 09:38:24 +00:00
<img :src="coverUrl" alt="Album's cover" height="96" width="96">
<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
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"
@click.prevent="currentView = 'details'"
2022-04-15 14:24:30 +00:00
>
Details
</button>
<button
v-if="editingOnlyOneSong"
id="editSongTabLyrics"
2022-04-15 14:24:30 +00:00
:aria-selected="currentView === 'lyrics'"
aria-controls="editSongPanelLyrics"
data-testid="edit-song-lyrics-tab"
role="tab"
2022-06-10 10:47:46 +00:00
type="button"
@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"
>
<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-06-10 10:47:46 +00:00
import { computed, defineAsyncComponent, reactive, ref, toRefs } from 'vue'
import { isEqual } from 'lodash'
2022-04-15 14:24:30 +00:00
2022-06-10 10:47:46 +00:00
import { alerts, arrayify, defaultCover, pluralize } from '@/utils'
import { songStore } from '@/stores'
2022-04-15 17:00:08 +00:00
2022-06-10 10:47:46 +00:00
type EditFormData = Pick<Song, 'title' | 'album_name' | 'artist_name' | 'album_artist_name' | 'lyrics' | 'track' | 'disc'>
type TabName = 'details' | 'lyrics'
2022-04-15 14:24:30 +00:00
2022-04-21 18:39:18 +00:00
const Btn = defineAsyncComponent(() => import('@/components/ui/Btn.vue'))
2022-04-23 21:36:19 +00:00
const SoundBar = defineAsyncComponent(() => import('@/components/ui/SoundBar.vue'))
2022-04-15 14:24:30 +00:00
2022-04-21 09:38:24 +00:00
const props = withDefaults(defineProps<{ songs: Song[], initialTab: TabName }>(), { initialTab: 'details' })
2022-04-15 17:00:08 +00:00
const { songs, initialTab } = toRefs(props)
2022-04-15 14:24:30 +00:00
2022-04-15 17:00:08 +00:00
const mutatedSongs = ref<Song[]>([])
2022-04-21 09:38:24 +00:00
const currentView = ref<TabName>()
2022-06-10 10:47:46 +00:00
const loading = ref(false)
2022-04-15 14:24:30 +00:00
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'))
const coverUrl = computed(() => allSongsAreInSameAlbum.value ? mutatedSongs.value[0].album_cover : defaultCover)
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-20 09:37:22 +00:00
mutatedSongs.value = arrayify(songs.value)
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 : ''
// 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)
alerts.success(`Updated ${pluralize(mutatedSongs.value.length, 'song')}.`)
2022-04-15 17:00:08 +00:00
close()
} finally {
loading.value = false
}
}
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 {
img {
flex: 0 0 96px;
}
.meta {
flex: 1;
padding-left: 1rem;
.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>