diff --git a/src/app/music/page.tsx b/src/app/music/page.tsx index 27fd793..2a8758b 100644 --- a/src/app/music/page.tsx +++ b/src/app/music/page.tsx @@ -13,6 +13,7 @@ import { } from '@/lib/db.client'; import AddToPlaylistModal from '@/components/AddToPlaylistModal'; import Toast, { ToastProps } from '@/components/Toast'; +import LyricsPiPWindow from '@/components/LyricsPiPWindow'; interface Song { id: string; @@ -55,6 +56,16 @@ interface DbRecord { pic?: string; } +// 扩展 Window 接口以支持 Document PiP API +declare global { + interface Window { + documentPictureInPicture?: { + requestWindow: (options: { width: number; height: number }) => Promise; + window: Window | null; + }; + } +} + export default function MusicPage() { const router = useRouter(); const [currentSource, setCurrentSource] = useState<'netease' | 'qq' | 'kuwo'>('netease'); @@ -112,6 +123,11 @@ export default function MusicPage() { onCancel: () => {}, }); + // PiP 相关状态 + const [showPiPLyrics, setShowPiPLyrics] = useState(false); + const [pipOpacity, setPipOpacity] = useState(0.9); + const [pipMinimized, setPipMinimized] = useState(false); + const audioRef = useRef(null); const lyricsContainerRef = useRef(null); const lastSaveTimeRef = useRef(0); @@ -293,6 +309,36 @@ export default function MusicPage() { initializePlayState(); }, []); + // 恢复 PiP 偏好设置 + useEffect(() => { + const savedOpacity = localStorage.getItem('lyricsPiPOpacity'); + const savedMinimized = localStorage.getItem('lyricsPiPMinimized'); + if (savedOpacity) setPipOpacity(parseFloat(savedOpacity)); + if (savedMinimized) setPipMinimized(savedMinimized === 'true'); + }, []); + + // 监听来自 PiP 窗口的消息 + useEffect(() => { + const handleMessage = (event: MessageEvent) => { + switch (event.data.type) { + case 'PIP_OPACITY_CHANGE': + setPipOpacity(event.data.opacity); + localStorage.setItem('lyricsPiPOpacity', event.data.opacity.toString()); + break; + case 'PIP_MINIMIZED_CHANGE': + setPipMinimized(event.data.minimized); + localStorage.setItem('lyricsPiPMinimized', event.data.minimized.toString()); + break; + case 'PIP_CLOSE': + setShowPiPLyrics(false); + break; + } + }; + + window.addEventListener('message', handleMessage); + return () => window.removeEventListener('message', handleMessage); + }, []); + // 监听播放状态变化,自动保存 useEffect(() => { if (currentSong) { @@ -1098,6 +1144,29 @@ export default function MusicPage() { } }; + // PiP 窗口管理 + const togglePiPLyrics = () => { + if (!('documentPictureInPicture' in window)) { + setToast({ + message: '您的浏览器不支持画中画功能,请使用 Chrome 116+ 版本', + type: 'error', + }); + // 降级方案:打开全屏歌词 + setShowLyrics(true); + return; + } + + if (!currentSong) { + setToast({ + message: '请先播放歌曲', + type: 'info', + }); + return; + } + + setShowPiPLyrics(!showPiPLyrics); + }; + const progress = duration > 0 ? (currentTime / duration) * 100 : 0; const getQualityLabel = () => { @@ -1861,6 +1930,24 @@ export default function MusicPage() { + {/* PiP 歌词按钮 */} + {/* 添加到歌单按钮 */} + {/* 关闭按钮 */} + + + + + {/* 歌词内容 */} + {minimized ? ( + // 最小化模式:仅显示当前歌词 +
+ {lyrics.length > 0 && currentLyricIndex >= 0 + ? lyrics[currentLyricIndex]?.text || '♪' + : currentSong + ? '暂无歌词' + : '请播放歌曲'} +
+ ) : ( + // 完整模式:显示所有歌词 +
+ {lyrics.length > 0 ? ( + lyrics.map((line, index) => ( +
+ {line.text} +
+ )) + ) : ( +
+ {currentSong ? '暂无歌词' : '请播放歌曲'} +
+ )} +
+ )} + + ); +}; + +// 复制样式表到 PiP 窗口 +const copyStylesToPiPWindow = (pipWin: Window) => { + const styleSheets = Array.from(document.styleSheets); + styleSheets.forEach((styleSheet) => { + try { + const cssRules = Array.from(styleSheet.cssRules) + .map((rule) => rule.cssText) + .join(''); + const style = pipWin.document.createElement('style'); + style.textContent = cssRules; + pipWin.document.head.appendChild(style); + } catch (e) { + // 跨域样式表使用 link 标签 + if ((styleSheet as any).href) { + const link = pipWin.document.createElement('link'); + link.rel = 'stylesheet'; + link.href = (styleSheet as any).href; + pipWin.document.head.appendChild(link); + } + } + }); +}; + +// 主组件:管理 PiP 窗口 +export default function LyricsPiPWindow({ + currentSong, + lyrics, + currentLyricIndex, + isPlaying, + currentTime, + opacity, + minimized, + onOpacityChange, + onMinimizedChange, + onClose, +}: LyricsPiPWindowProps) { + const pipWindowRef = useRef(null); + const rootRef = useRef(null); + + // 渲染 PiP 内容 + const renderPiPContent = (pipWin: Window) => { + const container = pipWin.document.createElement('div'); + container.id = 'pip-lyrics-root'; + pipWin.document.body.appendChild(container); + + // 设置 body 样式 + pipWin.document.body.style.margin = '0'; + pipWin.document.body.style.padding = '0'; + pipWin.document.body.style.overflow = 'hidden'; + + // 使用 ReactDOM 渲染组件到 PiP 窗口 + const root = ReactDOM.createRoot(container); + rootRef.current = root; + + root.render( + { + window.postMessage({ type: 'PIP_OPACITY_CHANGE', opacity: newOpacity }, '*'); + }} + onMinimizedChange={(newMinimized) => { + window.postMessage({ type: 'PIP_MINIMIZED_CHANGE', minimized: newMinimized }, '*'); + }} + onClose={() => { + window.postMessage({ type: 'PIP_CLOSE' }, '*'); + }} + /> + ); + }; + + // 更新 PiP 内容 + useEffect(() => { + if (pipWindowRef.current && !pipWindowRef.current.closed && rootRef.current) { + rootRef.current.render( + { + window.postMessage({ type: 'PIP_OPACITY_CHANGE', opacity: newOpacity }, '*'); + }} + onMinimizedChange={(newMinimized) => { + window.postMessage({ type: 'PIP_MINIMIZED_CHANGE', minimized: newMinimized }, '*'); + }} + onClose={() => { + window.postMessage({ type: 'PIP_CLOSE' }, '*'); + }} + /> + ); + } + }, [currentSong, lyrics, currentLyricIndex, opacity, minimized]); + + // 打开 PiP 窗口 + useEffect(() => { + const openPiPWindow = async () => { + if (!('documentPictureInPicture' in window)) { + console.error('浏览器不支持 Document Picture-in-Picture API'); + return; + } + + try { + const pipWin = await (window as any).documentPictureInPicture.requestWindow({ + width: 400, + height: 300, + }); + + pipWindowRef.current = pipWin; + + // 复制样式表到 PiP 窗口 + copyStylesToPiPWindow(pipWin); + + // 监听窗口关闭 + pipWin.addEventListener('pagehide', () => { + if (rootRef.current) { + rootRef.current.unmount(); + rootRef.current = null; + } + pipWindowRef.current = null; + onClose(); + }); + + // 渲染内容 + renderPiPContent(pipWin); + } catch (error) { + console.error('打开画中画窗口失败:', error); + onClose(); + } + }; + + openPiPWindow(); + + // 清理函数 + return () => { + if (pipWindowRef.current && !pipWindowRef.current.closed) { + pipWindowRef.current.close(); + } + if (rootRef.current) { + rootRef.current.unmount(); + rootRef.current = null; + } + }; + }, []); // 只在组件挂载时执行一次 + + return null; // 此组件不渲染任何内容到主窗口 +}