feat(Player, Playroom): add volume control functionality with persistent settings

This commit is contained in:
Astrian Zheng 2025-05-28 11:30:12 +10:00
parent 1cecf52dfd
commit b9a8ca7ae4
Signed by: Astrian
SSH Key Fingerprint: SHA256:rVnhx3DAKjujCwWE13aDl7uV6+9U1MvydLkNRXJrBiA
2 changed files with 160 additions and 6 deletions

View File

@ -236,6 +236,11 @@ watch(() => playQueueStore.list.length, async (newLength) => {
setTimeout(() => {
playQueueStore.preloadNext()
}, 2000)
//
if (player.value) {
initializeVolume()
}
})
//
@ -303,6 +308,48 @@ function getCurrentTrack() {
return currentTrack.value
}
//
function initializeVolume() {
if (player.value) {
const savedVolume = localStorage.getItem('audioVolume')
if (savedVolume) {
const volumeValue = parseFloat(savedVolume)
player.value.volume = volumeValue
console.log('[Player] 初始化音量:', volumeValue)
} else {
//
player.value.volume = 1
localStorage.setItem('audioVolume', '1')
}
}
}
//
function handleVolumeChange(event: Event) {
const target = event.target as HTMLAudioElement
if (target) {
// localStorage
localStorage.setItem('audioVolume', target.volume.toString())
console.log('[Player] 音量变化:', target.volume)
}
}
// localStorage
function syncVolumeFromStorage() {
if (player.value) {
const savedVolume = localStorage.getItem('audioVolume')
if (savedVolume) {
const volumeValue = parseFloat(savedVolume)
if (player.value.volume !== volumeValue) {
player.value.volume = volumeValue
}
}
}
}
// 使storage
setInterval(syncVolumeFromStorage, 100)
//
// onUnmounted(() => {
// playQueueStore.clearAllPreloadedAudio()
@ -312,16 +359,18 @@ function getCurrentTrack() {
<template>
<div>
<audio :src="currentAudioSrc" ref="playerRef" :autoplay="playQueueStore.isPlaying"
v-if="playQueueStore.list.length !== 0" @ended="() => {
v-if="playQueueStore.list.length !== 0" @volumechange="handleVolumeChange" @ended="() => {
if (playQueueStore.playMode.repeat === 'single') { playQueueStore.isPlaying = true }
else { playNext() }
}" @pause="playQueueStore.isPlaying = false" @play="playQueueStore.isPlaying = true" @playing="() => {
console.log('[Player] 音频开始播放事件')
playQueueStore.isBuffering = false
setMetadata()
initializeVolume()
}" @waiting="playQueueStore.isBuffering = true" @loadeddata="() => {
console.log('[Player] 音频数据加载完成')
playQueueStore.isBuffering = false
initializeVolume()
}" @canplay="() => {
console.log('[Player] 音频可以播放')
playQueueStore.isBuffering = false

View File

@ -47,11 +47,15 @@ const songInfo = useTemplateRef('songInfo')
const moreOptionsDialog = useTemplateRef('moreOptionsDialog')
const playButton = useTemplateRef('playButton')
const volumeSliderThumb = useTemplateRef('volumeSliderThumb')
const volumeSliderContainer = useTemplateRef('volumeSliderContainer')
const presentQueueListDialog = ref(false)
const presentLyrics = ref(false)
const showLyricsTooltip = ref(false)
const showMoreOptions = ref(false)
const presentVolumeControl = ref(false)
const volume = ref(1) // 0-1
import PlayQueueItem from '../components/PlayQueueItem.vue'
@ -66,6 +70,16 @@ onMounted(async () => {
playQueueStore.updatedCurrentTime = newTime
}
})
// DOM
await nextTick()
// localStorage
const savedVolume = localStorage.getItem('audioVolume')
if (savedVolume) {
volume.value = parseFloat(savedVolume)
}
thumbUpdate()
setupEntranceAnimations()
@ -93,6 +107,71 @@ function thumbUpdate() {
gsap.to(progressBarThumb.value, { x: newPosition, duration: 0.1 })
}
function volumeThumbUpdate() {
const containerWidth = volumeSliderContainer.value?.clientWidth || 0
const thumbWidth = volumeSliderThumb.value?.clientWidth || 0
const newPosition = (containerWidth - thumbWidth) * volume.value
gsap.to(volumeSliderThumb.value, { x: newPosition, duration: 0.1 })
}
function toggleVolumeControl() {
if (!presentVolumeControl.value) {
presentVolumeControl.value = true
nextTick(() => {
// Draggable
createVolumeDraggable()
volumeThumbUpdate()
})
} else {
presentVolumeControl.value = false
}
}
function createVolumeDraggable() {
if (!volumeSliderThumb.value || !volumeSliderContainer.value) {
console.warn('Volume slider elements not found')
return
}
//
const containerWidth = volumeSliderContainer.value.clientWidth
if (containerWidth === 0) {
console.warn('Volume slider container has no width')
return
}
// Draggable
Draggable.create(volumeSliderThumb.value, {
type: 'x',
bounds: volumeSliderContainer.value,
onDrag: function () {
const thumbPosition = this.x
const containerWidth = volumeSliderContainer.value?.clientWidth || 0
const thumbWidth = volumeSliderThumb.value?.clientWidth || 0
// 0-1
const newVolume = Math.max(0, Math.min(1, thumbPosition / (containerWidth - thumbWidth)))
volume.value = newVolume
updateAudioVolume()
// localStorage
localStorage.setItem('audioVolume', newVolume.toString())
},
onDragEnd: function () {
//
localStorage.setItem('audioVolume', volume.value.toString())
}
})
console.log('Volume draggable created successfully')
}
function updateAudioVolume() {
// 访
const audioElement = document.querySelector('audio')
if (audioElement) {
audioElement.volume = volume.value
}
}
function formatDetector() {
const format = playQueueStore.list[playQueueStore.currentIndex].song.sourceUrl?.split('.').pop()
if (format === 'mp3') { return 'MP3' }
@ -450,17 +529,26 @@ watch(() => playQueueStore.currentIndex, () => {
<!-- Control buttons -->
<div class="w-full flex justify-between items-center" ref="controlButtons">
<div class="flex-1 text-left flex gap-1">
<button
class="h-8 w-8 flex justify-center items-center rounded-full hover:bg-white/25 transition-all duration-200 hover:scale-110"
ref="volumeButton">
<button class="h-8 w-8 flex justify-center items-center rounded-full hover:bg-white/25 relative"
ref="volumeButton" @click="toggleVolumeControl">
<div class="w-6 h-6 relative">
<span class="text-black blur-md absolute top-0 left-0">
<span class="text-black blur-md absolute top-0 left-0 transition-all duration-200 hover:scale-110">
<SpeakerIcon :size="6" />
</span>
<span class="text-white absolute top-0 left-0">
<span class="text-white absolute top-0 left-0 transition-all duration-200 hover:scale-110">
<SpeakerIcon :size="6" />
</span>
</div>
<transition name="volume-control-fade">
<div v-if="presentVolumeControl" @click.stop
class="absolute bg-black/60 backdrop-blur-3xl rounded-md shadow-2xl border border-[#ffffff39] w-64 h-10 bottom-10 left-[-0.3rem] flex items-center justify-between px-4 z-50">
<div class="w-full py-[0.125rem] px-[0.25rem] bg-white/20 rounded-full" ref="volumeSliderContainer">
<div class="w-2 h-2 bg-white rounded-full shadow-md cursor-pointer" ref="volumeSliderThumb" />
</div>
<!-- <span class="text-white text-xs w-8 text-right flex-shrink-0">{{ Math.round(volume * 100) }}%</span> -->
</div>
</transition>
</button>
<button
class="text-white h-8 w-8 flex justify-center items-center rounded-full hover:bg-white/25 transition-all duration-200 hover:scale-110"
@ -681,4 +769,21 @@ watch(() => playQueueStore.currentIndex, () => {
opacity: 1;
transform: translateY(0) scale(1);
}
.volume-control-fade-enter-active,
.volume-control-fade-leave-active {
transition: opacity 0.2s cubic-bezier(.4, 0, .2, 1), transform 0.2s cubic-bezier(.4, 0, .2, 1);
}
.volume-control-fade-enter-from,
.volume-control-fade-leave-to {
opacity: 0;
transform: translateY(10px) scale(0.95);
}
.volume-control-fade-enter-to,
.volume-control-fade-leave-from {
opacity: 1;
transform: translateY(0) scale(1);
}
</style>