up
This commit is contained in:
@@ -0,0 +1,202 @@
|
||||
<script setup lang="ts">
|
||||
import { NAlert, NButton, NInput, NSelect, NSpace, useDialog } from 'naive-ui'
|
||||
import { computed, onMounted, onUnmounted, ref, watch } from 'vue'
|
||||
import type { MusicPlaylist, OnlineIntent } from '../types/online'
|
||||
|
||||
const props = defineProps<{
|
||||
playlists: MusicPlaylist[]
|
||||
canEdit: boolean
|
||||
canUse: boolean
|
||||
isBusy: boolean
|
||||
}>()
|
||||
const emit = defineEmits<{ action: [intent: OnlineIntent] }>()
|
||||
const dialog = useDialog()
|
||||
const selectedId = ref<number | null>(null)
|
||||
const name = ref('')
|
||||
const newName = ref('')
|
||||
const order = ref<number[]>([])
|
||||
const original = ref<number[]>([])
|
||||
const selected = computed(() => props.playlists.find((p) => p.id === selectedId.value))
|
||||
const isDirty = computed(() => JSON.stringify(order.value) !== JSON.stringify(original.value))
|
||||
const hasConflict = computed(
|
||||
() =>
|
||||
selected.value &&
|
||||
JSON.stringify(selected.value.tracks.map((t) => t.id).sort((a, b) => a - b)) !==
|
||||
JSON.stringify([...original.value].sort((a, b) => a - b)),
|
||||
)
|
||||
const isDisabled = computed(() => !props.canEdit || props.isBusy)
|
||||
let timer: ReturnType<typeof setInterval> | undefined
|
||||
function reset() {
|
||||
original.value = selected.value?.tracks.map((t) => t.id) ?? []
|
||||
order.value = [...original.value]
|
||||
name.value = selected.value?.name ?? ''
|
||||
}
|
||||
watch(selectedId, reset)
|
||||
watch(
|
||||
() => props.playlists,
|
||||
(items, old) => {
|
||||
const added = items.find((p) => !old.some((o) => o.id === p.id))
|
||||
if (!isDirty.value && added) selectedId.value = added.id
|
||||
if (selectedId.value && !selected.value) selectedId.value = null
|
||||
const oldName = old.find((p) => p.id === selectedId.value)?.name
|
||||
if (name.value === oldName) name.value = selected.value?.name ?? ''
|
||||
const ids = selected.value?.tracks.map((t) => t.id) ?? []
|
||||
if (!isDirty.value || JSON.stringify(order.value) === JSON.stringify(ids)) {
|
||||
original.value = ids
|
||||
order.value = [...ids]
|
||||
}
|
||||
},
|
||||
)
|
||||
|
||||
function refresh() {
|
||||
if (props.canEdit && !props.isBusy) emit('action', { action: 'playlists' })
|
||||
}
|
||||
watch(
|
||||
() => props.canEdit,
|
||||
(value) => {
|
||||
if (value) refresh()
|
||||
},
|
||||
)
|
||||
onMounted(() => {
|
||||
refresh()
|
||||
timer = setInterval(refresh, 10000)
|
||||
})
|
||||
onUnmounted(() => clearInterval(timer))
|
||||
function move(index: number, offset: number) {
|
||||
const target = index + offset
|
||||
if (target < 0 || target >= order.value.length) return
|
||||
const [id] = order.value.splice(index, 1)
|
||||
if (id !== undefined) order.value.splice(target, 0, id)
|
||||
}
|
||||
function create() {
|
||||
emit('action', { action: 'playlist-create', name: newName.value })
|
||||
}
|
||||
function remove(trackId?: number) {
|
||||
if (!selected.value) return
|
||||
const playlistId = selected.value.id
|
||||
dialog.warning({
|
||||
title: trackId === undefined ? 'Удалить плейлист?' : 'Удалить трек?',
|
||||
content: 'Изменение сохранится на сервере. Используемые плейлисты и треки удалить нельзя.',
|
||||
positiveText: 'Удалить',
|
||||
negativeText: 'Отмена',
|
||||
onPositiveClick: () =>
|
||||
emit(
|
||||
'action',
|
||||
trackId === undefined
|
||||
? { action: 'playlist-delete', playlistId }
|
||||
: { action: 'playlist-track-delete', playlistId, trackId },
|
||||
),
|
||||
})
|
||||
}
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<n-space vertical data-testid="music-plan-editor">
|
||||
<p v-if="!canEdit">Для редактирования библиотеки подключитесь к серверу.</p>
|
||||
<template v-else>
|
||||
<n-button :disabled="isDisabled" @click="refresh">Обновить плейлисты</n-button>
|
||||
<label for="new-playlist-name">Новый плейлист</label>
|
||||
<n-input
|
||||
v-model:value="newName"
|
||||
:input-props="{ id: 'new-playlist-name' }"
|
||||
:disabled="isDisabled"
|
||||
placeholder="Название плейлиста"
|
||||
/>
|
||||
<n-button :disabled="isDisabled || isDirty || !newName.trim()" @click="create"
|
||||
>Создать плейлист</n-button
|
||||
>
|
||||
<n-select
|
||||
v-model:value="selectedId"
|
||||
:options="playlists.map((p) => ({ label: p.name, value: p.id }))"
|
||||
:disabled="isDisabled || isDirty"
|
||||
placeholder="Плейлист для редактирования"
|
||||
:input-props="{ 'aria-label': 'Плейлист для редактирования' }"
|
||||
/>
|
||||
<p v-if="!playlists.length">Плейлистов пока нет. Создайте первый.</p>
|
||||
<template v-if="selected">
|
||||
<label for="playlist-name">Название плейлиста</label>
|
||||
<n-input
|
||||
v-model:value="name"
|
||||
:input-props="{ id: 'playlist-name' }"
|
||||
:disabled="isDisabled"
|
||||
/>
|
||||
<n-space>
|
||||
<n-button
|
||||
:disabled="isDisabled || !name.trim() || name === selected.name"
|
||||
@click="emit('action', { action: 'playlist-rename', playlistId: selected.id, name })"
|
||||
>Сохранить название</n-button
|
||||
>
|
||||
<n-button :disabled="isDisabled || isDirty" @click="remove()">Удалить плейлист</n-button>
|
||||
<n-button
|
||||
:disabled="isDisabled || isDirty"
|
||||
@click="emit('action', { action: 'playlist-upload', playlistId: selected.id })"
|
||||
>Добавить MP3</n-button
|
||||
>
|
||||
<n-button
|
||||
:disabled="isDisabled || isDirty || !canUse || !selected.tracks.length"
|
||||
@click="emit('action', { action: 'playlist-use', playlistId: selected.id })"
|
||||
>Использовать в игре</n-button
|
||||
>
|
||||
</n-space>
|
||||
<p>Выбор в редакторе не меняет музыку игры. MP3 — до 64 МиБ.</p>
|
||||
<p v-if="!order.length">Треков пока нет.</p>
|
||||
<ol class="track-list">
|
||||
<li v-for="(id, index) in order" :key="id">
|
||||
<span>{{
|
||||
selected.tracks.find((t) => t.id === id)?.name ?? `Трек №${id} удалён на сервере`
|
||||
}}</span>
|
||||
<n-space>
|
||||
<n-button
|
||||
:disabled="isDisabled || index === 0"
|
||||
:aria-label="`Поднять трек ${index + 1}`"
|
||||
@click="move(index, -1)"
|
||||
>↑</n-button
|
||||
>
|
||||
<n-button
|
||||
:disabled="isDisabled || index === order.length - 1"
|
||||
:aria-label="`Опустить трек ${index + 1}`"
|
||||
@click="move(index, 1)"
|
||||
>↓</n-button
|
||||
>
|
||||
<n-button :disabled="isDisabled || isDirty" @click="remove(id)"
|
||||
>Удалить трек</n-button
|
||||
>
|
||||
</n-space>
|
||||
</li>
|
||||
</ol>
|
||||
<n-alert v-if="hasConflict" type="warning"
|
||||
>Состав треков изменился на сервере. Отмените черновик, чтобы принять актуальный
|
||||
список.</n-alert
|
||||
>
|
||||
<n-space v-if="isDirty || hasConflict">
|
||||
<n-button
|
||||
:disabled="isDisabled || hasConflict"
|
||||
@click="
|
||||
emit('action', {
|
||||
action: 'playlist-reorder',
|
||||
playlistId: selected.id,
|
||||
trackIds: [...order],
|
||||
})
|
||||
"
|
||||
>Сохранить порядок</n-button
|
||||
>
|
||||
<n-button :disabled="props.isBusy" @click="reset">Отменить</n-button>
|
||||
</n-space>
|
||||
</template>
|
||||
</template>
|
||||
</n-space>
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
.track-list {
|
||||
display: grid;
|
||||
gap: 12px;
|
||||
padding-left: 24px;
|
||||
}
|
||||
|
||||
.track-list li > span {
|
||||
display: block;
|
||||
margin-bottom: 8px;
|
||||
overflow-wrap: anywhere;
|
||||
}
|
||||
</style>
|
||||
Reference in New Issue
Block a user