This commit is contained in:
2026-10-19 01:26:01 +07:00
parent 8cce8f7807
commit 426acbc389
2 changed files with 130 additions and 117 deletions
+124 -113
View File
@@ -49,9 +49,7 @@ const statusLabels: Record<Playback, string> = {
function isCurrent(index: number) {
return !!props.channel.source && props.channel.trackIndex === index
}
function canPlay(index: number) {
return isCurrent(index) && props.outputOpen && !props.isBusy
}
const canPlay = computed(() => !!props.channel.source && props.outputOpen && !props.isBusy)
function edit() {
if (props.isBusy || !props.canEdit) return
selectedId.value = props.channel.playlistId
@@ -230,7 +228,7 @@ function remove(trackId?: number) {
</template>
</template>
</n-space>
<n-space v-else vertical data-testid="music-plan-view">
<div v-else class="music-plan-view" data-testid="music-plan-view">
<n-select
:value="channel.playlistId"
:options="localPlaylists.map((p) => ({ label: p.name, value: p.id }))"
@@ -250,6 +248,116 @@ function remove(trackId?: number) {
@update:value="emit('command', { action: 'loop', channel: 'music', enabled: $event })"
/>
</label>
<p v-if="channel.source" class="current-track" data-testid="current-music-track">{{ channel.source.name }}</p>
<div class="track-actions">
<n-space align="center">
<n-button
square
:disabled="!canPlay"
:aria-label="control.label"
:title="control.label"
@click="emit('command', { action: control.action, channel: 'music' })"
>
<svg
width="18"
height="18"
viewBox="0 0 24 24"
fill="currentColor"
aria-hidden="true"
>
<path
v-if="control.action === 'pause'"
d="M6 4h4v16H6zM14 4h4v16h-4z"
/>
<path v-else d="M7 4v16l13-8z" />
</svg>
</n-button>
<n-button
square
:disabled="!canPlay"
aria-label="Стоп"
title="Стоп"
@click="emit('command', { action: 'stop', channel: 'music' })"
>
<svg
width="18"
height="18"
viewBox="0 0 24 24"
fill="currentColor"
aria-hidden="true"
>
<path d="M5 5h14v14H5z" />
</svg>
</n-button>
<n-button
square
:disabled="!canPlay"
aria-label="Сначала"
title="Сначала"
@click="emit('command', { action: 'replay', channel: 'music' })"
>
<svg
width="18"
height="18"
viewBox="0 0 24 24"
fill="none"
stroke="currentColor"
stroke-width="2"
aria-hidden="true"
>
<path d="M20 11a8 8 0 1 0-2 6M20 4v7h-7" />
</svg>
</n-button>
<div class="track-volume">
<label for="volume-music" title="Громкость, %">
<svg
width="18"
height="18"
viewBox="0 0 24 24"
fill="none"
stroke="currentColor"
stroke-width="2"
aria-hidden="true"
>
<path d="M11 5 6 9H3v6h3l5 4zM15 8a6 6 0 0 1 0 8M18 5a10 10 0 0 1 0 14" />
</svg>
</label>
<n-slider
class="track-volume-input"
:value="channel.volume"
id="volume-music"
role="group"
aria-label="Громкость, %"
:step="1"
:format-tooltip="(value) => `${value}%`"
:min="0"
:max="100"
:disabled="!channel.source || isBusy"
@update:value="
emit('command', { action: 'volume', channel: 'music', volume: $event ?? 0 })
"
/>
</div>
</n-space>
</div>
<div class="track-progress">
<n-progress
class="track-progress-bar"
type="line"
:percentage="progress"
:show-indicator="false"
aria-label="Прогресс воспроизведения"
/>
<span class="track-time" role="status">
{{ statusLabels[channel.status] }} ·
{{ formatTimer(Math.floor(channel.position) * 1000) }} /
{{ channel.duration === null ? '—' : formatTimer(channel.duration * 1000) }}
</span>
</div>
<n-alert v-if="channel.error" type="error">{{
channel.error
}}</n-alert>
<p v-if="!localPlaylists.length">Плейлистов в игре пока нет.</p>
<p v-else-if="!activePlaylist">Выберите плейлист.</p>
<p v-else-if="!activePlaylist.tracks.length">Треков пока нет.</p>
@@ -273,122 +381,20 @@ function remove(trackId?: number) {
>{{ isCurrent(index) ? 'Выбран' : 'Выбрать' }}</n-button
>
</div>
<div class="track-actions">
<n-space align="center">
<n-button
square
:disabled="!canPlay(index)"
:aria-label="isCurrent(index) ? control.label : 'Воспроизвести'"
:title="isCurrent(index) ? control.label : 'Воспроизвести'"
@click="emit('command', { action: control.action, channel: 'music' })"
>
<svg
width="18"
height="18"
viewBox="0 0 24 24"
fill="currentColor"
aria-hidden="true"
>
<path
v-if="isCurrent(index) && control.action === 'pause'"
d="M6 4h4v16H6zM14 4h4v16h-4z"
/>
<path v-else d="M7 4v16l13-8z" />
</svg>
</n-button>
<n-button
square
:disabled="!canPlay(index)"
aria-label="Стоп"
title="Стоп"
@click="emit('command', { action: 'stop', channel: 'music' })"
>
<svg
width="18"
height="18"
viewBox="0 0 24 24"
fill="currentColor"
aria-hidden="true"
>
<path d="M5 5h14v14H5z" />
</svg>
</n-button>
<n-button
square
:disabled="!canPlay(index)"
aria-label="Сначала"
title="Сначала"
@click="emit('command', { action: 'replay', channel: 'music' })"
>
<svg
width="18"
height="18"
viewBox="0 0 24 24"
fill="none"
stroke="currentColor"
stroke-width="2"
aria-hidden="true"
>
<path d="M20 11a8 8 0 1 0-2 6M20 4v7h-7" />
</svg>
</n-button>
<div class="track-volume">
<label :for="`volume-music-${track.id}`" title="Громкость, %">
<svg
width="18"
height="18"
viewBox="0 0 24 24"
fill="none"
stroke="currentColor"
stroke-width="2"
aria-hidden="true"
>
<path d="M11 5 6 9H3v6h3l5 4zM15 8a6 6 0 0 1 0 8M18 5a10 10 0 0 1 0 14" />
</svg>
</label>
<n-slider
class="track-volume-input"
:value="channel.volume"
:id="`volume-music-${track.id}`"
role="group"
aria-label="Громкость, %"
:step="1"
:format-tooltip="(value) => `${value}%`"
:min="0"
:max="100"
:disabled="!isCurrent(index) || isBusy"
@update:value="
emit('command', { action: 'volume', channel: 'music', volume: $event ?? 0 })
"
/>
</div>
</n-space>
</div>
<div class="track-progress">
<n-progress
class="track-progress-bar"
type="line"
:percentage="isCurrent(index) ? progress : 0"
:show-indicator="false"
aria-label="Прогресс воспроизведения"
/>
<span v-if="isCurrent(index)" class="track-time" role="status">
{{ statusLabels[channel.status] }} ·
{{ formatTimer(Math.floor(channel.position) * 1000) }} /
{{ channel.duration === null ? '—' : formatTimer(channel.duration * 1000) }}
</span>
</div>
<n-alert v-if="isCurrent(index) && channel.error" type="error">{{
channel.error
}}</n-alert>
</div>
</li>
</ul>
</n-space>
</div>
</n-card>
</template>
<style scoped>
.music-plan-view {
display: flex;
flex-direction: column;
gap: 12px;
}
.track-list {
display: grid;
gap: 12px;
@@ -440,6 +446,11 @@ function remove(trackId?: number) {
grid-template-columns: minmax(0, 1fr) auto;
}
.current-track {
margin: 0;
overflow-wrap: anywhere;
}
.music-loop,
.track-volume {
align-items: center;