generated from VLADIMIR/template_frontend
up
This commit is contained in:
Vendored
+1
-1
@@ -5,7 +5,7 @@
|
|||||||
<link rel="icon" href="/favicon.ico" />
|
<link rel="icon" href="/favicon.ico" />
|
||||||
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||||
<title>Вечерний детектив</title>
|
<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/client-BPUTdqiR.js">
|
||||||
<link rel="modulepreload" crossorigin href="/assets/light-CAboqKml.js">
|
<link rel="modulepreload" crossorigin href="/assets/light-CAboqKml.js">
|
||||||
<link rel="modulepreload" crossorigin href="/assets/light-p8ayFy8X.js">
|
<link rel="modulepreload" crossorigin href="/assets/light-p8ayFy8X.js">
|
||||||
|
|||||||
@@ -103,22 +103,30 @@ function uploadTrack(options: { file: UploadFileInfo }) {
|
|||||||
style="min-width: 220px"
|
style="min-width: 220px"
|
||||||
@update:value="(value) => emit('setMusic', { playlistID: value || 0, trackIndex: 0, playback: 'MUSIC_PLAYBACK_STOPPED' })"
|
@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
|
<n-select
|
||||||
v-if="props.music?.playlist?.tracks?.length"
|
v-if="props.music?.playlist?.tracks?.length"
|
||||||
:value="props.music.trackIndex"
|
:value="props.music.trackIndex"
|
||||||
:options="props.music.playlist.tracks.map((track, index) => ({ label: track.name || track.filename || `Трек ${index + 1}`, value: index }))"
|
:options="props.music.playlist.tracks.map((track, index) => ({ label: track.name || track.filename || `Трек ${index + 1}`, value: index }))"
|
||||||
style="min-width: 220px"
|
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_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_PAUSED' })">Пауза</n-button>
|
||||||
<n-button size="small" @click="emit('setMusic', { playback: 'MUSIC_PLAYBACK_STOPPED', positionMs: 0 })">Стоп</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;
|
color: #f2a2a2;
|
||||||
font-size: 13px;
|
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>
|
</style>
|
||||||
|
|||||||
@@ -74,6 +74,11 @@ const musicTrack = computed(() => {
|
|||||||
const index = state.value?.music?.trackIndex || 0
|
const index = state.value?.music?.trackIndex || 0
|
||||||
return applicationFileURL(tracks[index]?.filename)
|
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 screenBackground = computed(() => {
|
||||||
const image = state.value?.backgroundImage || state.value?.coverImage
|
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}")` } : {}
|
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(
|
watch(
|
||||||
() => state.value?.music,
|
musicSyncState,
|
||||||
async (value) => {
|
async (value, previous) => {
|
||||||
const media = musicAudioElement.value
|
const music = state.value?.music
|
||||||
if (!media || !value || !value.playlist?.tracks?.length) return
|
if (!music || !music.playlist?.tracks?.length) return
|
||||||
await nextTick()
|
await nextTick()
|
||||||
media.volume = Math.max(0, Math.min(1, (value.volume ?? 100) / 100))
|
const media = musicAudioElement.value
|
||||||
media.muted = !state.value?.soundEnabled
|
if (!media) return
|
||||||
media.currentTime = (value.positionMs || 0) / 1000
|
const trackChanged = !previous || value[0] !== previous[0] || value[1] !== previous[1]
|
||||||
if (value.playback === 'MUSIC_PLAYBACK_PLAYING') {
|
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(() => {
|
void media.play().catch(() => {
|
||||||
mediaError.value = 'Браузер заблокировал воспроизведение музыки. Нажмите «Подготовить экран».'
|
mediaError.value = 'Браузер заблокировал воспроизведение музыки. Нажмите «Подготовить экран».'
|
||||||
})
|
})
|
||||||
@@ -211,7 +233,18 @@ watch(
|
|||||||
media.pause()
|
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() {
|
async function advanceMusic() {
|
||||||
|
|||||||
Reference in New Issue
Block a user