Skip to content

Commit e19eb9c

Browse files
committed
实现音量调节功能,添加音量控制相关逻辑和UI组件,并在songPlayer中应用持久化音量设置。更新backlog.md以反映音量调节功能的添加。
1 parent d8826c2 commit e19eb9c

3 files changed

Lines changed: 184 additions & 6 deletions

File tree

‎backlog.md‎

Lines changed: 1 addition & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,4 @@
1919

2020
加AI,预测分类
2121

22-
导入曲目的路径应该记住上一次的选择
23-
24-
需要加一个音量调节的功能
22+
导入曲目的路径应该记住上一次的选择

‎src/renderer/src/components/playerControls.vue‎

Lines changed: 158 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,8 @@ import pause from '@renderer/assets/pause.png?asset'
66
import fastForward from '@renderer/assets/fastForward.png?asset'
77
import nextSong from '@renderer/assets/nextSong.png?asset'
88
import more from '@renderer/assets/more.png?asset'
9+
import volumePng from '@renderer/assets/volume.png?asset'
10+
import volumeMutePng from '@renderer/assets/volumeMute.png?asset'
911
import { ref, onUnmounted, watch, useTemplateRef, onMounted, computed } from 'vue'
1012
import { v4 as uuidV4 } from 'uuid'
1113
import { useRuntimeStore } from '@renderer/stores/runtime'
@@ -33,7 +35,8 @@ const emits = defineEmits([
3335
'delSong',
3436
'moveToLikeLibrary',
3537
'moveToListLibrary',
36-
'exportTrack'
38+
'exportTrack',
39+
'setVolume'
3740
])
3841
3942
const setPlayingValue = (value: boolean) => {
@@ -129,6 +132,96 @@ const showInFileExplorer = () => {
129132
window.electron.ipcRenderer.send('show-item-in-folder', runtime.playingData.playingSong?.filePath)
130133
}
131134
135+
// ---------------- 音量控制 ----------------
136+
const VOLUME_KEY = 'frkb_volume'
137+
const volume = ref(1)
138+
const lastNonZeroVolume = ref(1)
139+
const sliderHovering = ref(false)
140+
const iconHovering = ref(false)
141+
let draggingVolume = false
142+
const showVolumeSlider = computed(
143+
() => iconHovering.value || sliderHovering.value || draggingVolume
144+
)
145+
146+
const volumeIcon = computed(() => {
147+
return volume.value <= 0 ? volumeMutePng : volumePng
148+
})
149+
150+
const emitSetVolume = (val: number) => {
151+
const v = Math.min(1, Math.max(0, val))
152+
volume.value = v
153+
if (v > 0) lastNonZeroVolume.value = v
154+
emits('setVolume', v)
155+
try {
156+
localStorage.setItem(VOLUME_KEY, String(v))
157+
} catch (_) {}
158+
}
159+
160+
let justDragged = false
161+
const toggleMute = () => {
162+
// 仅在拖动中或刚拖完的极短时间内抑制,其他情况立即切换
163+
if (draggingVolume || justDragged) return
164+
if (volume.value > 0) {
165+
lastNonZeroVolume.value = volume.value
166+
emitSetVolume(0)
167+
} else {
168+
const prev = Number.isFinite(lastNonZeroVolume.value as number)
169+
? (lastNonZeroVolume.value as number)
170+
: 0
171+
// 若历史非零音量过低(<10%),按 10% 恢复,避免“体感几乎为 0”
172+
const restore = prev > 0 ? Math.max(prev, 0.1) : 0.25
173+
emitSetVolume(restore)
174+
}
175+
}
176+
177+
const volumeBarRef = useTemplateRef<HTMLDivElement>('volumeBarRef')
178+
const updateVolumeFromClientY = (clientY: number) => {
179+
const el = volumeBarRef.value
180+
if (!el) return
181+
const rect = el.getBoundingClientRect()
182+
if (!rect || rect.height <= 0) return
183+
const ratio = 1 - (clientY - rect.top) / rect.height
184+
emitSetVolume(ratio)
185+
}
186+
187+
const handleBarMousedown = (e: MouseEvent) => {
188+
draggingVolume = true
189+
updateVolumeFromClientY(e.clientY)
190+
document.addEventListener('mousemove', handleBarMousemove)
191+
document.addEventListener('mouseup', handleBarMouseup)
192+
}
193+
const handleBarMousemove = (e: MouseEvent) => {
194+
if (!draggingVolume) return
195+
updateVolumeFromClientY(e.clientY)
196+
}
197+
const handleBarMouseup = () => {
198+
draggingVolume = false
199+
justDragged = true
200+
setTimeout(() => {
201+
justDragged = false
202+
}, 150)
203+
document.removeEventListener('mousemove', handleBarMousemove)
204+
document.removeEventListener('mouseup', handleBarMouseup)
205+
}
206+
207+
onUnmounted(() => {
208+
document.removeEventListener('mousemove', handleBarMousemove)
209+
document.removeEventListener('mouseup', handleBarMouseup)
210+
})
211+
212+
// 初始化从 localStorage 读取音量(默认 0.8)并应用
213+
onMounted(() => {
214+
try {
215+
const s = localStorage.getItem(VOLUME_KEY)
216+
let v = s !== null ? parseFloat(s) : NaN
217+
if (!(v >= 0 && v <= 1)) v = 0.8
218+
// 设置组件内部状态并通知父组件
219+
volume.value = v
220+
lastNonZeroVolume.value = v > 0 ? v : 0
221+
emits('setVolume', v)
222+
} catch (_) {}
223+
})
224+
132225
// ---------------- 系统媒体会话(Media Session API)集成 ----------------
133226
// 目标:启用系统的 上一首/下一首/播放/暂停 按钮,并同步元数据和播放状态
134227
let artworkUrl: string = ''
@@ -296,6 +389,27 @@ onUnmounted(() => {
296389
<img :src="nextSong" draggable="false" />
297390
</div>
298391
<bubbleBox :dom="nextSongRef || undefined" :title="t('player.next')" shortcut="S" />
392+
<!-- 音量按钮,与“下一首”同风格 -->
393+
<div
394+
class="buttonIcon volumeIcon"
395+
@mouseenter="iconHovering = true"
396+
@mouseleave="iconHovering = false"
397+
style="position: relative"
398+
>
399+
<img :src="volumeIcon" draggable="false" @click.stop="toggleMute" />
400+
<transition name="fade">
401+
<div
402+
v-if="showVolumeSlider"
403+
class="volumePopover unselectable"
404+
@mouseenter="sliderHovering = true"
405+
@mouseleave="sliderHovering = false"
406+
>
407+
<div class="volumeBar" ref="volumeBarRef" @mousedown="handleBarMousedown">
408+
<div class="volumeFill" :style="{ height: Math.round(volume * 100) + '%' }"></div>
409+
</div>
410+
</div>
411+
</transition>
412+
</div>
299413
<div class="buttonIcon" @click.stop="handelMoreClick()">
300414
<img :src="more" draggable="false" />
301415
</div>
@@ -393,10 +507,53 @@ onUnmounted(() => {
393507
filter: contrast(200%) drop-shadow(0px 0px 10px #fff);
394508
}
395509
}
510+
511+
// 仅移除音量图标的发光
512+
.volumeIcon:hover {
513+
filter: none;
514+
}
396515
}
397516
398517
img {
399518
width: 20px;
400519
height: 20px;
401520
}
521+
522+
.volumePopover {
523+
position: absolute;
524+
bottom: 38px; // 更贴近图标,轻微重叠,避免鼠标经过间隙
525+
left: 50%;
526+
transform: translateX(-50%);
527+
width: 32px;
528+
height: 120px;
529+
background-color: #202020;
530+
border: 1px solid #424242;
531+
border-radius: 6px;
532+
display: flex;
533+
align-items: center;
534+
justify-content: center;
535+
padding: 8px 0;
536+
z-index: 100;
537+
// 确保浮层不受任何父级滤镜影响
538+
filter: none;
539+
}
540+
541+
.volumeBar {
542+
position: relative;
543+
width: 6px;
544+
height: 100%;
545+
background: #2e2e2e;
546+
border-radius: 4px;
547+
cursor: pointer;
548+
filter: none;
549+
}
550+
551+
.volumeFill {
552+
position: absolute;
553+
left: 0;
554+
bottom: 0;
555+
width: 100%;
556+
background: #0078d4;
557+
border-radius: 4px;
558+
}
402559
</style>

‎src/renderer/src/pages/modules/songPlayer/songPlayer.vue‎

Lines changed: 25 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -331,6 +331,14 @@ onMounted(() => {
331331
}
332332
wavesurferInstance.value = createWaveSurferInstance(waveform.value)
333333
334+
// 应用持久化音量(默认 0.8)
335+
try {
336+
const s = localStorage.getItem('frkb_volume')
337+
let v = s !== null ? parseFloat(s) : NaN
338+
if (!(v >= 0 && v <= 1)) v = 0.8
339+
wavesurferInstance.value.setVolume?.(v)
340+
} catch (_) {}
341+
334342
attachEventListeners(wavesurferInstance.value)
335343
336344
window.electron.ipcRenderer.on(
@@ -344,7 +352,11 @@ onMounted(() => {
344352
// 验证此时UI显示的歌曲与返回的文件路径是否匹配
345353
if (filePath === runtime.playingData.playingSong?.filePath) {
346354
const name = (filePath?.match(/[^\\/]+$/) || [])[0] || 'unknown'
347-
handleLoadBlob(new Blob([audioData]), filePath, requestId || currentLoadRequestId.value)
355+
const ab = audioData.buffer.slice(
356+
audioData.byteOffset,
357+
audioData.byteOffset + audioData.byteLength
358+
) as ArrayBuffer
359+
handleLoadBlob(new Blob([ab]), filePath, requestId || currentLoadRequestId.value)
348360
} else {
349361
const name = (filePath?.match(/[^\\/]+$/) || [])[0] || 'unknown'
350362
const cur = runtime.playingData.playingSong?.filePath || 'N/A'
@@ -363,7 +375,11 @@ onMounted(() => {
363375
return
364376
}
365377
366-
const blob = new Blob([audioData])
378+
const ab = audioData.buffer.slice(
379+
audioData.byteOffset,
380+
audioData.byteOffset + audioData.byteLength
381+
) as ArrayBuffer
382+
const blob = new Blob([ab])
367383
preloadedBlob.value = blob
368384
369385
try {
@@ -979,6 +995,13 @@ watch(
979995
@moveToListLibrary="(song) => playerActions.moveToListLibrary(song)"
980996
@moveToLikeLibrary="(song) => playerActions.moveToLikeLibrary(song)"
981997
@exportTrack="playerActions.exportTrack"
998+
@setVolume="
999+
(v) => {
1000+
try {
1001+
wavesurferInstance?.setVolume?.(v)
1002+
} catch (_) {}
1003+
}
1004+
"
9821005
/>
9831006
</div>
9841007

0 commit comments

Comments
 (0)