diff --git a/dist/index.html b/dist/index.html
index 17caf80..9b83d86 100644
--- a/dist/index.html
+++ b/dist/index.html
@@ -5,7 +5,7 @@
Вечерний детектив
-
+
diff --git a/src/components/GameScreensPanel.vue b/src/components/GameScreensPanel.vue
index 0687665..de6e96e 100644
--- a/src/components/GameScreensPanel.vue
+++ b/src/components/GameScreensPanel.vue
@@ -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' })"
/>
-
- Удалить трек
-
emit('setMusic', { trackIndex: value })"
+ @update:value="(value) => emit('setMusic', { trackIndex: value, positionMs: 0 })"
/>
+
+
В плейлисте пока нет треков.
+
+
+ {{ track.name || track.filename || `Трек ${index + 1}` }}
+
+ {{ track.filename }}
+
+ Удалить
+
+
+
Старт
Пауза
Стоп
@@ -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;
+}
diff --git a/src/components/ScreenPage.vue b/src/components/ScreenPage.vue
index b83be0a..c48ef6a 100644
--- a/src/components/ScreenPage.vue
+++ b/src/components/ScreenPage.vue
@@ -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 {
+ 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() {