From 8e0bc75ad9362e326aa42d1ef48149c0bb0b7090 Mon Sep 17 00:00:00 2001 From: mackt <1033530438@qq.com> Date: Sun, 7 Apr 2024 23:04:46 +0800 Subject: [PATCH] update(AudioPlayer): Add playback feature. --- public/img/audio-player/collapse.svg | 4 + public/img/audio-player/expand.svg | 4 + public/img/audio-player/listLoop.svg | 3 + public/img/audio-player/listPlay.svg | 4 + public/img/audio-player/pause.svg | 6 +- public/img/audio-player/play.svg | 6 +- public/img/audio-player/random.svg | 4 - public/img/audio-player/randomPlay.svg | 4 + public/img/audio-player/scale.svg | 4 - public/img/audio-player/singleLoop.svg | 5 + src/app/mylist/page.tsx | 3 +- src/components/AudioPlayer/Player.tsx | 161 +++++++++++-------- src/components/AudioPlayer/PlayerControl.tsx | 114 +++++++++++++ src/components/Login/LoginForm.tsx | 4 +- src/components/Song/SongCard.tsx | 18 ++- src/components/Song/SongCardList.tsx | 37 ++++- src/store/audio.ts | 78 +++++++-- src/utils/timeFormat.ts | 16 ++ 18 files changed, 367 insertions(+), 108 deletions(-) create mode 100644 public/img/audio-player/collapse.svg create mode 100644 public/img/audio-player/expand.svg create mode 100644 public/img/audio-player/listLoop.svg create mode 100644 public/img/audio-player/listPlay.svg delete mode 100644 public/img/audio-player/random.svg create mode 100644 public/img/audio-player/randomPlay.svg delete mode 100644 public/img/audio-player/scale.svg create mode 100644 public/img/audio-player/singleLoop.svg create mode 100644 src/components/AudioPlayer/PlayerControl.tsx create mode 100644 src/utils/timeFormat.ts diff --git a/public/img/audio-player/collapse.svg b/public/img/audio-player/collapse.svg new file mode 100644 index 0000000..07ae0e4 --- /dev/null +++ b/public/img/audio-player/collapse.svg @@ -0,0 +1,4 @@ + + + + diff --git a/public/img/audio-player/expand.svg b/public/img/audio-player/expand.svg new file mode 100644 index 0000000..47fdd78 --- /dev/null +++ b/public/img/audio-player/expand.svg @@ -0,0 +1,4 @@ + + + + diff --git a/public/img/audio-player/listLoop.svg b/public/img/audio-player/listLoop.svg new file mode 100644 index 0000000..ffaad8c --- /dev/null +++ b/public/img/audio-player/listLoop.svg @@ -0,0 +1,3 @@ + + + diff --git a/public/img/audio-player/listPlay.svg b/public/img/audio-player/listPlay.svg new file mode 100644 index 0000000..78783c1 --- /dev/null +++ b/public/img/audio-player/listPlay.svg @@ -0,0 +1,4 @@ + + + + diff --git a/public/img/audio-player/pause.svg b/public/img/audio-player/pause.svg index 443d1c7..2fd9622 100644 --- a/public/img/audio-player/pause.svg +++ b/public/img/audio-player/pause.svg @@ -1,4 +1,4 @@ - - - + + + diff --git a/public/img/audio-player/play.svg b/public/img/audio-player/play.svg index 74ef673..300be2f 100644 --- a/public/img/audio-player/play.svg +++ b/public/img/audio-player/play.svg @@ -1,5 +1,3 @@ - - - - + + diff --git a/public/img/audio-player/random.svg b/public/img/audio-player/random.svg deleted file mode 100644 index 7ded78b..0000000 --- a/public/img/audio-player/random.svg +++ /dev/null @@ -1,4 +0,0 @@ - - - - diff --git a/public/img/audio-player/randomPlay.svg b/public/img/audio-player/randomPlay.svg new file mode 100644 index 0000000..43d47c4 --- /dev/null +++ b/public/img/audio-player/randomPlay.svg @@ -0,0 +1,4 @@ + + + + diff --git a/public/img/audio-player/scale.svg b/public/img/audio-player/scale.svg deleted file mode 100644 index 69ebc75..0000000 --- a/public/img/audio-player/scale.svg +++ /dev/null @@ -1,4 +0,0 @@ - - - - diff --git a/public/img/audio-player/singleLoop.svg b/public/img/audio-player/singleLoop.svg new file mode 100644 index 0000000..10cb8ec --- /dev/null +++ b/public/img/audio-player/singleLoop.svg @@ -0,0 +1,5 @@ + + + + + diff --git a/src/app/mylist/page.tsx b/src/app/mylist/page.tsx index b5d38c6..62cdbc8 100644 --- a/src/app/mylist/page.tsx +++ b/src/app/mylist/page.tsx @@ -36,6 +36,7 @@ export default function Journal() { // journal: apiGetSongCollect, // }; const result = await apiGetSongCollect({ userId: userInfo?.id, pageNum, pageSize }); + console.log(result); if (result.code === 200) setList(result.data.rows); }; @@ -74,7 +75,7 @@ export default function Journal() { 播放全部 {/* List */} - + ); diff --git a/src/components/AudioPlayer/Player.tsx b/src/components/AudioPlayer/Player.tsx index e8a33c5..5d884ca 100644 --- a/src/components/AudioPlayer/Player.tsx +++ b/src/components/AudioPlayer/Player.tsx @@ -1,84 +1,111 @@ 'use client'; -import { useState } from 'react'; +/** + * 待优化: + * 1. 拖动滚动条时音轨不要动,等拖动完毕后再动 + * 2. 切歌时,时间显示 + * 3. 预加载 + */ +import { useState, useRef, useEffect } from 'react'; -import Image from 'next/image'; import { useShallow } from 'zustand/react/shallow'; -import { AutoScrollContainer } from '@/components'; +import PlayerControl from './PlayerControl'; + import useAudioStore from '@/store/audio'; export default function AudioPlayer({ className }: { className?: string }) { - const { audioInfo, playList } = useAudioStore( + const audioRef = useRef(new Audio()); + const [trackProgress, setTrackProgress] = useState(0); // 音频进度 + const isReady = useRef(false); // 是否完成加载 + + const { duration } = audioRef.current; // 音频总时长; + + const { playStatus, setPlayStatus, audio, switchSongByDiff } = useAudioStore( useShallow((state) => ({ - audioInfo: state.audioInfo, + playStatus: state.playStatus, + setPlayStatus: state.setPlayStatus, + audio: state.audio, playList: state.playList, + switchSongByDiff: state.switchSongByDiff, })), ); - const [curTime, setCurTime] = useState('00:00'); - const [totalTime, setTotalTime] = useState('00:00'); + // 播放/暂停 + const handlePlay = () => { + setPlayStatus(!playStatus); + }; + + // 上一首 + const handlePlayPre = () => { + switchSongByDiff(-1); + }; + + // 下一首 + const handlePlayNext = () => { + switchSongByDiff(1); + }; + + // 调整进度 + const handleChangeProgress = (value: string) => { + audioRef.current.currentTime = Number(value); + setTrackProgress(audioRef.current.currentTime); + }; + + // 给 audio 绑定监听 + const handleTimeUpdate = (status: boolean) => { + status + ? audioRef.current.addEventListener('timeupdate', onTimeUpdate) + : audioRef.current.removeEventListener('timeupdate', onTimeUpdate); + }; + + // 监听进度 + const onTimeUpdate = () => { + if (audioRef.current.ended) handlePlayNext(); + setTrackProgress(audioRef.current.currentTime); + }; + + // 监听播放/暂停 + useEffect(() => { + playStatus ? audioRef.current.play() : audioRef.current.pause(); + }, [playStatus]); + + // 监听切换歌曲 + useEffect(() => { + console.log('触发'); + handleTimeUpdate(false); + audioRef.current.pause(); + if (!audio) return; + audioRef.current = new Audio(audio.src); + + if (isReady.current) { + audioRef.current.play(); + setPlayStatus(true); + handleTimeUpdate(true); + } else { + isReady.current = true; + } + }, [audio]); + + useEffect(() => { + return () => { + audioRef.current.pause(); + handleTimeUpdate(false); + }; + }, []); return ( -
- {/* cover */} -
- music -
- - {/* title & author */} -
- -
-

{'Ferrum Aeternumaaaaaaaaaaa'}

-

{'Ensiferum/mmmmsa'}

-
-
-
- - {/* progress bar */} -
- {/* bar */} -
- {/* black */} -
- {/* gery */} -
- {/* point */} -
-
- {/* time */} -

- {curTime} - {` / ${totalTime}`} -

-
- - {/* control */} -
- - {/* 上一首 */} - - {/* 播放/暂停 */} - - {/* 下一首 */} - -
+
+
); } diff --git a/src/components/AudioPlayer/PlayerControl.tsx b/src/components/AudioPlayer/PlayerControl.tsx new file mode 100644 index 0000000..6982f9a --- /dev/null +++ b/src/components/AudioPlayer/PlayerControl.tsx @@ -0,0 +1,114 @@ +'use client'; + +import Image from 'next/image'; + +import { secondToDate } from '@/utils/timeFormat'; + +import { AutoScrollContainer } from '@/components'; + +interface Props { + audio: SongInfo | null; + playStatus: boolean; + onPlay: () => void; + onPrev: () => void; + onNext: () => void; + onChangeProgress: (value: string) => void; + trackProgress: number; + duration: number; + className?: string; +} + +export default function AudioPlayer({ + audio, + playStatus, + onPlay, + onPrev, + onNext, + onChangeProgress, + trackProgress, + duration, + className, +}: Props) { + const currentPercentage = duration ? `${(trackProgress / duration) * 100}%` : '0%'; + const trackStyling: string = ` + -webkit-gradient(linear, 0% 0%, 100% 0%, color-stop(${currentPercentage}, rgb(24,24,24)), color-stop(${currentPercentage}, rgba(0,0,0,0.1))) + `; // 进度条样式 + + return ( +
+ {/* 专辑封面 */} +
+ {audio?.pic && ( + music + )} +
+ + {/* title & author */} +
+ +
+

{audio?.title}

+

{`${audio?.artist}/${audio?.album}`}

+
+
+
+ + {/* progress bar */} +
+ {/* bar */} +
+ onChangeProgress(e.target.value)} + style={{ background: trackStyling }} + /> +
+ {/* time */} +

+ {`${secondToDate(trackProgress)}`} + {` /${secondToDate(duration)}`} +

+
+ + {/* control */} +
+ + {/* 上一首 */} + + {/* 播放/暂停 */} + + {/* 下一首 */} + +
+
+ ); +} diff --git a/src/components/Login/LoginForm.tsx b/src/components/Login/LoginForm.tsx index c6ecad2..c850035 100644 --- a/src/components/Login/LoginForm.tsx +++ b/src/components/Login/LoginForm.tsx @@ -76,7 +76,7 @@ export default function LoginForm() {
void }) { return ( - onPlay(id)} > {/* left */}
@@ -36,6 +42,6 @@ export default function JournalItem({ id, title, pic, artist, haveCollect }: Son className="w-[24px] h-[24px] overflow-hidden" />
- +
); } diff --git a/src/components/Song/SongCardList.tsx b/src/components/Song/SongCardList.tsx index d3c1bad..9624dcd 100644 --- a/src/components/Song/SongCardList.tsx +++ b/src/components/Song/SongCardList.tsx @@ -1,13 +1,46 @@ /** * 歌曲卡片 */ +'use client'; + +import { useShallow } from 'zustand/react/shallow'; + import { SongCard } from '@/components'; +import useAudioStore from '@/store/audio'; + +interface Props { + listId: string; + songList: SongInfo[]; + className?: string; +} + +export default function JournalItem({ listId, songList, className }: Props) { + const { playStatus, setPlayStatus, setPlayList, audio, playList, playListId, setAudio } = useAudioStore( + useShallow((state) => ({ + playStatus: state.playStatus, + setPlayStatus: state.setPlayStatus, + setplayList: state.setPlayList, + audio: state.audio, + setAudio: state.setAudio, + playList: state.playList, + setPlayList: state.setPlayList, + playListId: state.playListId, + })), + ); + + const handlePlayList = (audioId: string) => { + // 正在播放当前歌单 + if (playListId !== listId) { + setPlayList({ id: listId, list: songList }); + } + // 正在播放其他歌单 + setAudio(audioId); + }; -export default function JournalItem({ songList, className }: { songList: SongInfo[]; className?: string }) { return (
{songList.map((song: SongInfo) => ( - + handlePlayList(audioId)} /> ))}
); diff --git a/src/store/audio.ts b/src/store/audio.ts index bbb922b..b4b5ded 100644 --- a/src/store/audio.ts +++ b/src/store/audio.ts @@ -6,43 +6,91 @@ import { apiGetSongInfo } from '@/services'; interface AuioState { /** 播放状态 */ - play: boolean; + playStatus: boolean; + /** 播放列表id */ + playListId: string; + /** 修改播放列表id */ + setPlayListId: (id: string) => void; /** 播放器显示状态 */ show: boolean; - /** 当前音频信息 */ - audioInfo: SongInfo | null; + /** 正在播放的音频信息 */ + audio: SongInfo | null; + /** 设置正在播放的音频 + * @param id 音频 id,必须在 playList 内 + */ + setAudio: (id: string | null) => void; /** 播放列表 */ - playList: Array; + playList: Array; + /** 播放/暂停 */ + setPlayStatus: (value: boolean) => void; /** 显示/隐藏播放器 */ setShow: (value: boolean) => void; + /** 设置播放列表 */ + setPlayList: (params: { id: string; list: SongInfo[] }) => void; /** 获取歌曲信息 */ - getSongInfo: (id: string) => Promise; + getSongInfoById: (id: string) => Promise; + /** 根据id切换歌曲 */ + switchSongById: (id: string) => Promise; /** 切换歌曲 */ - switchSong: (id: string) => Promise; + switchSongByDiff: (diff: number) => void; } const useAuioState = create()( devtools( persist( - (set) => { - const audioInfo = null; - const getSongInfo = async (id: string) => { + (set, get) => { + // 设置当前播放的音频 + const setAudio = (id: string | null) => { + const audio = get().playList.find((item) => item?.id === id) || null; + set({ audio }); + set({ playStatus: true }); + }; + + // 根据 id 获取歌曲信息 + const getSongInfoById = async (id: string) => { const res = await apiGetSongInfo(id); - if (res.code === 200) set(produce((state) => void (state.audioInfo = res.data))); + if (res.code === 200) set({ audio: res.data }); return res.code === 200; }; + // 设置播放列表 + const setPlayListId = (id: string) => { + set({ playListId: id }); + }; + + // 切歌 + const switchSongByDiff = (diff: number) => { + const playList = get().playList; + if (!playList) return; + const index = playList.findIndex((item) => item.id === get().audio?.id); + const nextIndex = index + diff; + if (nextIndex < 0) setAudio(playList[playList.length - 1].id); + if (nextIndex >= playList.length) setAudio(playList[0].id); + set({ audio: playList[nextIndex] }); + setAudio(playList[nextIndex].id); + return; + }; + return { - play: false, + playStatus: false, show: false, - audioInfo, + audio: null, + setAudio, playList: [], - switchSong: async (id: string) => { - const res = await getSongInfo(id); + switchSongById: async (id: string) => { + const res = await getSongInfoById(id); return res; }, + playListId: '', + setPlayListId, + setPlayStatus: (value) => set({ playStatus: value }), setShow: (value) => set({ show: value }), - getSongInfo, + setPlayList: (params) => { + set({ playListId: params.id }); + set({ playList: params.list }); + }, + getSongInfoById, + switchSongByDiff, }; }, { diff --git a/src/utils/timeFormat.ts b/src/utils/timeFormat.ts new file mode 100644 index 0000000..8104441 --- /dev/null +++ b/src/utils/timeFormat.ts @@ -0,0 +1,16 @@ +export const secondToDate: (v: number) => string = (second) => { + const hourNum = Math.floor(second / 3600); + const minuteNum = Math.floor((second / 60) % 60); + const secondNum = Math.floor(second % 60); + + const h = hourNum ? `${hourNum}:` : ''; + const m = minuteNum ? `${unitAddZero(minuteNum)}:` : `00:`; + const s = unitAddZero(secondNum); + + return `${h}${m}${s}`; +}; + +// 格式化个位数 +const unitAddZero: (v: number) => string = (v) => { + return v < 10 ? `0${v}` : `${v}`; +};