This commit is contained in:
2026-10-07 01:56:54 +07:00
parent 71355777fd
commit affb19636a
3 changed files with 100 additions and 20 deletions
+1 -1
View File
@@ -5,7 +5,7 @@
<link rel="icon" href="/favicon.ico" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>Вечерний детектив</title>
<script type="module" crossorigin src="/assets/index-GBc-6ZcB.js"></script>
<script type="module" crossorigin src="/assets/index-VWZDnxHd.js"></script>
<link rel="modulepreload" crossorigin href="/assets/client-BPUTdqiR.js">
<link rel="modulepreload" crossorigin href="/assets/light-CAboqKml.js">
<link rel="modulepreload" crossorigin href="/assets/light-p8ayFy8X.js">
+57 -10
View File
@@ -103,22 +103,30 @@ function uploadTrack(options: { file: UploadFileInfo }) {
style="min-width: 220px"
@update:value="(value) => emit('setMusic', { playlistID: value || 0, trackIndex: 0, playback: 'MUSIC_PLAYBACK_STOPPED' })"
/>
<n-button
v-if="props.music?.playlist?.tracks?.[props.music.trackIndex || 0]?.ID"
size="small"
type="error"
ghost
@click="emit('deleteTrack', props.music.playlist.tracks[props.music.trackIndex || 0].ID!)"
>
Удалить трек
</n-button>
<n-select
v-if="props.music?.playlist?.tracks?.length"
:value="props.music.trackIndex"
:options="props.music.playlist.tracks.map((track, index) => ({ label: track.name || track.filename || `Трек ${index + 1}`, value: index }))"
style="min-width: 220px"
@update:value="(value) => emit('setMusic', { trackIndex: value })"
@update:value="(value) => emit('setMusic', { trackIndex: value, positionMs: 0 })"
/>
<div v-if="props.music?.playlist" class="track-list">
<p v-if="!props.music.playlist.tracks?.length" class="track-empty">В плейлисте пока нет треков.</p>
<div
v-for="(track, index) in props.music.playlist.tracks"
:key="track.ID"
class="track-row"
:class="{ 'track-row-active': index === (props.music.trackIndex ?? 0) }"
>
<n-button class="track-name" text @click="emit('setMusic', { trackIndex: index, positionMs: 0 })">
{{ track.name || track.filename || `Трек ${index + 1}` }}
</n-button>
<span class="track-filename">{{ track.filename }}</span>
<n-button size="small" type="error" ghost @click="emit('deleteTrack', track.ID!)">
Удалить
</n-button>
</div>
</div>
<n-button size="small" @click="emit('setMusic', { playback: 'MUSIC_PLAYBACK_PLAYING' })">Старт</n-button>
<n-button size="small" @click="emit('setMusic', { playback: 'MUSIC_PLAYBACK_PAUSED' })">Пауза</n-button>
<n-button size="small" @click="emit('setMusic', { playback: 'MUSIC_PLAYBACK_STOPPED', positionMs: 0 })">Стоп</n-button>
@@ -187,4 +195,43 @@ function uploadTrack(options: { file: UploadFileInfo }) {
color: #f2a2a2;
font-size: 13px;
}
.track-list {
flex-basis: 100%;
margin-top: 8px;
}
.track-empty {
color: #aaa;
margin: 0;
}
.track-row {
align-items: center;
border-top: 1px solid rgb(255 255 255 / 10%);
display: flex;
gap: 8px;
padding: 6px 0;
}
.track-row-active {
background: rgb(255 255 255 / 5%);
}
.track-name {
justify-content: flex-start;
min-width: 180px;
overflow: hidden;
text-align: left;
text-overflow: ellipsis;
white-space: nowrap;
}
.track-filename {
color: #aaa;
flex: 1;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
}
</style>
+42 -9
View File
@@ -74,6 +74,11 @@ const musicTrack = computed(() => {
const index = state.value?.music?.trackIndex || 0
return applicationFileURL(tracks[index]?.filename)
})
const musicSyncState = computed(() => [
state.value?.music?.playlist?.ID,
state.value?.music?.trackIndex,
state.value?.music?.playback,
] as const)
const screenBackground = computed(() => {
const image = state.value?.backgroundImage || state.value?.coverImage
return image ? { backgroundImage: `linear-gradient(rgb(0 0 0 / 55%), rgb(0 0 0 / 55%)), url("${image}")` } : {}
@@ -194,16 +199,33 @@ watch(state, async (value) => {
}
})
function waitForMusicMetadata(media: HTMLAudioElement): Promise<void> {
if (media.readyState >= HTMLMediaElement.HAVE_METADATA) return Promise.resolve()
return new Promise((resolve) => {
const finish = () => {
media.removeEventListener('loadedmetadata', finish)
media.removeEventListener('error', finish)
resolve()
}
media.addEventListener('loadedmetadata', finish, { once: true })
media.addEventListener('error', finish, { once: true })
})
}
watch(
() => state.value?.music,
async (value) => {
const media = musicAudioElement.value
if (!media || !value || !value.playlist?.tracks?.length) return
musicSyncState,
async (value, previous) => {
const music = state.value?.music
if (!music || !music.playlist?.tracks?.length) return
await nextTick()
media.volume = Math.max(0, Math.min(1, (value.volume ?? 100) / 100))
media.muted = !state.value?.soundEnabled
media.currentTime = (value.positionMs || 0) / 1000
if (value.playback === 'MUSIC_PLAYBACK_PLAYING') {
const media = musicAudioElement.value
if (!media) return
const trackChanged = !previous || value[0] !== previous[0] || value[1] !== previous[1]
const playbackChanged = !previous || value[2] !== previous[2]
if (!trackChanged && !playbackChanged) return
await waitForMusicMetadata(media)
media.currentTime = trackChanged ? 0 : (music.positionMs || 0) / 1000
if (music.playback === 'MUSIC_PLAYBACK_PLAYING') {
void media.play().catch(() => {
mediaError.value = 'Браузер заблокировал воспроизведение музыки. Нажмите «Подготовить экран».'
})
@@ -211,7 +233,18 @@ watch(
media.pause()
}
},
{ deep: true },
{ immediate: true },
)
watch(
() => [state.value?.music?.volume, state.value?.soundEnabled] as const,
([volume, soundEnabled]) => {
const media = musicAudioElement.value
if (!media) return
media.volume = Math.max(0, Math.min(1, (volume ?? 100) / 100))
media.muted = !soundEnabled
},
{ immediate: true },
)
async function advanceMusic() {