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

277 lines
8.8 KiB
Vue
Raw Normal View History

2022-04-15 14:24:30 +00:00
<template>
2024-04-04 22:20:42 +00:00
<form class="max-w-[540px]" @submit.prevent="submit" @keydown.esc="maybeClose">
<header class="gap-4">
<img alt="" :src="coverUrl" class="w-[84px] aspect-square object-cover object-center rounded-md">
<div class="flex-1 flex flex-col justify-center overflow-hidden">
<h1 :class="{ mixed: !editingOnlyOneSong }">{{ displayedTitle }}</h1>
<h2
data-testid="displayed-artist-name"
:class="{ mixed: !allSongsAreFromSameArtist && !formData.artist_name }"
>
{{ displayedArtistName }}
</h2>
<h2
data-testid="displayed-album-name"
:class="{ mixed: !allSongsAreInSameAlbum && !formData.album_name }"
>
{{ displayedAlbumName }}
</h2>
</div>
</header>
2024-04-04 22:20:42 +00:00
<Tabs class="mt-4">
<TabList>
<TabButton
id="editSongTabDetails"
2024-04-04 22:20:42 +00:00
:selected="currentTab === 'details'"
aria-controls="editSongPanelDetails"
2024-04-04 22:20:42 +00:00
@click="currentTab = 'details'"
>
Details
2024-04-04 22:20:42 +00:00
</TabButton>
<TabButton
v-if="editingOnlyOneSong"
id="editSongTabLyrics"
2024-04-04 22:20:42 +00:00
:selected="currentTab === 'lyrics'"
aria-controls="editSongPanelLyrics"
data-testid="edit-song-lyrics-tab"
2024-04-04 22:20:42 +00:00
@click="currentTab = 'lyrics'"
>
Lyrics
2024-04-04 22:20:42 +00:00
</TabButton>
</TabList>
2024-04-04 22:20:42 +00:00
<TabPanelContainer>
<TabPanel
v-show="currentTab === 'details'"
id="editSongPanelDetails"
aria-labelledby="editSongTabDetails"
2024-04-04 22:20:42 +00:00
class="space-y-5"
>
2024-04-04 22:20:42 +00:00
<FormRow v-if="editingOnlyOneSong">
<template #label>Title</template>
<TextInput v-model="formData.title" v-koel-focus data-testid="title-input" name="title" title="Title" />
</FormRow>
<FormRow :cols="2">
<FormRow>
<template #label>Artist</template>
<TextInput
v-model="formData.artist_name"
:placeholder="inputPlaceholder"
data-testid="artist-input"
name="artist"
2024-04-04 22:20:42 +00:00
/>
</FormRow>
<FormRow>
<template #label>Album Artist</template>
<TextInput
v-model="formData.album_artist_name"
:placeholder="inputPlaceholder"
data-testid="albumArtist-input"
name="album_artist"
2024-04-04 22:20:42 +00:00
/>
</FormRow>
</FormRow>
<FormRow>
<template #label>Album</template>
<TextInput
v-model="formData.album_name"
:placeholder="inputPlaceholder"
data-testid="album-input"
name="album"
/>
</FormRow>
2024-04-04 22:20:42 +00:00
<FormRow :cols="2">
<FormRow>
<template #label>Track</template>
<TextInput
v-model="formData.track"
:placeholder="inputPlaceholder"
data-testid="track-input"
min="1"
name="track"
type="number"
2024-04-04 22:20:42 +00:00
/>
</FormRow>
<FormRow>
<template #label>Disc</template>
<TextInput
v-model="formData.disc"
:placeholder="inputPlaceholder"
data-testid="disc-input"
min="1"
name="disc"
type="number"
2024-04-04 22:20:42 +00:00
/>
</FormRow>
</FormRow>
<FormRow :cols="2">
<FormRow>
<template #label>Genre</template>
<TextInput
v-model="formData.genre"
:placeholder="inputPlaceholder"
data-testid="genre-input"
name="genre"
list="genres"
2024-04-04 22:20:42 +00:00
/>
<datalist id="genres">
2022-12-02 16:17:37 +00:00
<option v-for="genre in genres" :key="genre" :value="genre" />
</datalist>
2024-04-04 22:20:42 +00:00
</FormRow>
<FormRow>
<template #label>Year</template>
<TextInput
v-model="formData.year"
:placeholder="inputPlaceholder"
data-testid="year-input"
name="year"
type="number"
2024-04-04 22:20:42 +00:00
/>
</FormRow>
</FormRow>
</TabPanel>
2022-04-15 14:24:30 +00:00
2024-04-04 22:20:42 +00:00
<TabPanel
v-if="editingOnlyOneSong"
v-show="currentTab === 'lyrics'"
id="editSongPanelLyrics"
aria-labelledby="editSongTabLyrics"
>
2024-04-04 22:20:42 +00:00
<FormRow>
<TextArea
2022-12-02 16:17:37 +00:00
v-model="formData.lyrics"
v-koel-focus
data-testid="lyrics-input"
name="lyrics"
title="Lyrics"
/>
2024-04-04 22:20:42 +00:00
</FormRow>
</TabPanel>
</TabPanelContainer>
</Tabs>
<footer>
<Btn type="submit">Update</Btn>
<Btn class="btn-cancel" white @click.prevent="maybeClose">Cancel</Btn>
</footer>
</form>
2022-04-15 14:24:30 +00:00
</template>
2022-04-15 17:00:08 +00:00
<script lang="ts" setup>
import { computed, reactive, ref } from 'vue'
2022-06-10 10:47:46 +00:00
import { isEqual } from 'lodash'
import { defaultCover, eventBus, logger, pluralize } from '@/utils'
2022-07-24 11:47:18 +00:00
import { songStore, SongUpdateData } from '@/stores'
import { useDialogBox, useMessageToaster, useModal, useOverlay } from '@/composables'
import { genres } from '@/config'
2022-04-15 17:00:08 +00:00
2024-04-04 22:20:42 +00:00
import Btn from '@/components/ui/form/Btn.vue'
import TextInput from '@/components/ui/form/TextInput.vue'
import TextArea from '@/components/ui/form/TextArea.vue'
import FormRow from '@/components/ui/form/FormRow.vue'
import Tabs from '@/components/ui/tabs/Tabs.vue'
import TabList from '@/components/ui/tabs/TabList.vue'
import TabButton from '@/components/ui/tabs/TabButton.vue'
import TabPanel from '@/components/ui/tabs/TabPanel.vue'
import TabPanelContainer from '@/components/ui/tabs/TabPanelContainer.vue'
const { showOverlay, hideOverlay } = useOverlay()
const { toastSuccess } = useMessageToaster()
const { showConfirmDialog, showErrorDialog } = useDialogBox()
const { getFromContext } = useModal()
const songs = getFromContext<Song[]>('songs')
const currentTab = ref(getFromContext<EditSongFormTabName>('initialTab'))
2022-07-20 08:00:02 +00:00
const editingOnlyOneSong = songs.length === 1
const inputPlaceholder = editingOnlyOneSong ? '' : 'Leave unchanged'
const allSongsShareSameValue = (key: keyof Song) => {
if (editingOnlyOneSong) return true
return new Set(songs.map(song => song[key])).size === 1
}
2022-04-15 14:24:30 +00:00
const allSongsAreFromSameArtist = allSongsShareSameValue('artist_name')
const allSongsAreInSameAlbum = allSongsShareSameValue('album_id')
const coverUrl = allSongsAreInSameAlbum ? (songs[0].album_cover || defaultCover) : defaultCover
2022-07-20 08:00:02 +00:00
2022-07-24 11:47:18 +00:00
const formData = reactive<SongUpdateData>({
title: allSongsShareSameValue('title') ? songs[0].title : '',
album_name: allSongsAreInSameAlbum ? songs[0].album_name : '',
artist_name: allSongsAreFromSameArtist ? songs[0].artist_name : '',
2022-06-10 10:47:46 +00:00
album_artist_name: '',
lyrics: editingOnlyOneSong ? songs[0].lyrics : '',
track: allSongsShareSameValue('track') && songs[0].track !== 0 ? songs[0].track : null,
disc: allSongsShareSameValue('disc') && songs[0].disc !== 0 ? songs[0].disc : null,
year: allSongsShareSameValue('year') ? songs[0].year : null,
genre: allSongsShareSameValue('genre') ? songs[0].genre : ''
2022-04-15 17:00:08 +00:00
})
2022-04-15 14:24:30 +00:00
// If the album artist(s) is the same as the artist(s), we set the form value as empty to not confuse the user
// and make it less error-prone.
if (allSongsAreInSameAlbum && allSongsAreFromSameArtist && songs[0].album_artist_id === songs[0].artist_id) {
formData.album_artist_name = ''
} else {
formData.album_artist_name = allSongsShareSameValue('album_artist_name') ? songs[0].album_artist_name : ''
}
2022-06-10 10:47:46 +00:00
if (!editingOnlyOneSong) {
delete formData.title
delete formData.lyrics
2022-06-10 10:47:46 +00:00
}
2022-04-15 17:00:08 +00:00
const initialFormData = Object.assign({}, formData)
2022-04-15 17:00:08 +00:00
const displayedTitle = computed(() => {
return editingOnlyOneSong ? formData.title : `${songs.length} songs selected`
2022-04-15 17:00:08 +00:00
})
const displayedArtistName = computed(() => {
return allSongsAreFromSameArtist || 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(() => {
return allSongsAreInSameAlbum || formData.album_name ? formData.album_name : 'Mixed Albums'
2022-04-15 17:00:08 +00:00
})
const emit = defineEmits<{ (e: 'close'): void }>()
2022-04-15 17:00:08 +00:00
const close = () => emit('close')
const maybeClose = async () => {
if (isEqual(formData, initialFormData)) {
2022-04-15 17:00:08 +00:00
close()
return
2022-04-15 14:24:30 +00:00
}
2022-04-15 17:00:08 +00:00
await showConfirmDialog('Discard all changes?') && close()
2022-04-15 17:00:08 +00:00
}
const submit = async () => {
showOverlay()
2022-04-15 17:00:08 +00:00
try {
await songStore.update(songs, formData)
toastSuccess(`Updated ${pluralize(songs, 'song')}.`)
eventBus.emit('SONGS_UPDATED')
2022-04-15 17:00:08 +00:00
close()
} catch (error) {
showErrorDialog('Something went wrong. Please try again.', 'Error')
logger.error(error)
2022-04-15 17:00:08 +00:00
} finally {
hideOverlay()
2022-04-15 17:00:08 +00:00
}
}
2022-04-15 14:24:30 +00:00
</script>
2024-04-04 20:13:35 +00:00
<style lang="postcss" scoped>
2024-04-04 22:20:42 +00:00
.mixed {
@apply opacity-50;
2022-04-15 14:24:30 +00:00
}
</style>