Skip to content

Commit 1bb96aa

Browse files
committed
Merge branch 'dev'
2 parents 971ca41 + 6a856b3 commit 1bb96aa

15 files changed

Lines changed: 1082 additions & 131 deletions

App.tsx

Lines changed: 57 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@ import { useArtists } from './hooks/useArtists';
99
import { useFileUpload } from './hooks/useFileUpload';
1010
import { useNetease } from './hooks/useNetease';
1111
import { usePageTitle } from './hooks/usePageTitle';
12+
import { useSharePanel } from './hooks/useSharePanel';
1213
import { getFontFamily } from './utils/fontUtils';
1314
import { ErrorService } from './utils/errorService';
1415
import ErrorBoundary from './components/ErrorBoundary';
@@ -19,13 +20,14 @@ import GlobalBackground from './components/GlobalBackground';
1920
import MiniPlayerBar from './components/MiniPlayerBar';
2021
import { getSongDetail, getSongUrl, getSongLyric, getAlbumCoverUrl } from './apis/netease';
2122

22-
type NavTab = 'songs' | 'artists' | 'netease' | 'together' | 'settings';
23+
type NavTab = 'songs' | 'artists' | 'netease' | 'together' | 'settings' | 'share';
2324

2425
const ArtistsView = lazy(() => import('./components/ArtistsView').then(m => ({ default: m.ArtistsView })));
2526
const NeteasePanel = lazy(() => import('./components/NeteasePanel').then(m => ({ default: m.NeteasePanel })));
2627
const SettingsPanel = lazy(() => import('./components/SettingsPanel'));
2728
const MusicPlayer = lazy(() => import('./components/MusicPlayer'));
2829
const TogetherListenPanel = lazy(() => import('./components/TogetherListenPanel'));
30+
const SharePanel = lazy(() => import('./components/SharePanel'));
2931

3032
const LoadingFallback = () => (
3133
<div className="flex items-center justify-center h-full">
@@ -61,6 +63,8 @@ const AppContent: React.FC = () => {
6163
activeTab
6264
});
6365

66+
const sharePanel = useSharePanel();
67+
6468
const loadMusicFromUrl = useCallback(async (item: PlaylistItem, index: number) => {
6569
setErrorMessage(null);
6670
playlist.setCurrentIndex(index);
@@ -72,6 +76,7 @@ const AppContent: React.FC = () => {
7276
album: item.album,
7377
coverUrl: item.coverUrl,
7478
lyrics: item.lyrics,
79+
translatedLyrics: item.translatedLyrics,
7580
neteaseId: item.neteaseId,
7681
artistIds: item.artistIds,
7782
file: item.file
@@ -279,18 +284,11 @@ const AppContent: React.FC = () => {
279284
const audio = player.audioRef.current;
280285
if (!audio) return;
281286

282-
const handleEnded = () => {
283-
console.log('===== Ended event triggered =====');
284-
console.log('Current playback mode:', player.playbackMode);
285-
console.log('Current track:', player.track?.metadata.title);
286-
console.log('Is playing netease:', !!player.track?.neteaseId);
287-
287+
const handleEnded = () => {
288288
if (player.playbackMode === 'single') {
289-
console.log('Single mode: restarting current track');
290289
audio.currentTime = 0;
291290
audio.play().catch(() => {});
292291
} else {
293-
console.log('List/shuffle mode: calling handleNext');
294292
handleNext();
295293
}
296294
};
@@ -310,6 +308,26 @@ const AppContent: React.FC = () => {
310308
}, 300);
311309
}, []);
312310

311+
const handleShareClick = useCallback(() => {
312+
if (!player.track) return;
313+
314+
if (player.track.neteaseId) {
315+
sharePanel.updateConfig('enableNeteaseMusicId', true);
316+
sharePanel.updateConfig('neteaseMusicId', player.track.neteaseId.toString());
317+
sharePanel.updateConfig('enableTrackIndex', false);
318+
sharePanel.updateConfig('playlistOrigin', '');
319+
} else {
320+
sharePanel.updateConfig('enableTrackIndex', true);
321+
sharePanel.updateConfig('trackIndex', playlist.currentIndex.toString());
322+
sharePanel.updateConfig('enableNeteaseMusicId', false);
323+
sharePanel.updateConfig('playlistOrigin', settings.customSourceUrl || defaultSourceUrl);
324+
}
325+
326+
sharePanel.updateConfig('seekTo', '');
327+
setShowFullPlayer(false);
328+
setActiveTab('share');
329+
}, [player.track, playlist.currentIndex, sharePanel, settings.customSourceUrl]);
330+
313331
const handleSeek = useCallback((time: number) => {
314332
if (player.audioRef.current && time >= 0) {
315333
player.audioRef.current.currentTime = time;
@@ -397,6 +415,34 @@ const AppContent: React.FC = () => {
397415
</div>
398416
</Suspense>
399417
);
418+
case 'share':
419+
return (
420+
<Suspense fallback={<LoadingFallback />}>
421+
<div className="h-full flex flex-col">
422+
<div className="p-6 border-b border-white/[0.05]">
423+
<h2 className="text-2xl font-bold text-white drop-shadow-md">分享</h2>
424+
<p className="text-sm text-white/40 mt-1 drop-shadow-sm">生成分享链接</p>
425+
</div>
426+
<div className="flex-1 overflow-y-auto playlist-scrollbar p-4">
427+
<SharePanel
428+
isOpen={true}
429+
onClose={() => {}}
430+
config={sharePanel.config}
431+
updateConfig={sharePanel.updateConfig}
432+
shareUrl={sharePanel.shareUrl}
433+
resetConfig={sharePanel.resetConfig}
434+
onReadCurrentTime={() => sharePanel.readCurrentTime(player.currentTime)}
435+
onReadCurrentUrl={() => sharePanel.readCurrentUrl(settings.customSourceUrl || defaultSourceUrl)}
436+
onReadCurrentTrack={handleShareClick}
437+
onCopy={sharePanel.copyToClipboard}
438+
onValidate={sharePanel.validateConfig}
439+
currentTime={player.currentTime}
440+
isMobile={false}
441+
/>
442+
</div>
443+
</div>
444+
</Suspense>
445+
);
400446
case 'songs':
401447
default:
402448
return (
@@ -419,7 +465,7 @@ const AppContent: React.FC = () => {
419465
/>
420466
);
421467
}
422-
}, [activeTab, selectedArtist, playlist, player, settings, artistsByLetter, pinyinLoadError, loadMusicFromUrl, loadNeteaseMusic, triggerFileUpload, triggerFolderUpload]);
468+
}, [activeTab, selectedArtist, playlist, player, settings, artistsByLetter, pinyinLoadError, loadMusicFromUrl, loadNeteaseMusic, triggerFileUpload, triggerFolderUpload, sharePanel]);
423469

424470
return (
425471
<div className="h-screen w-full overflow-hidden" style={{ fontFamily: getFontFamily(settings.selectedFont) }}>
@@ -558,6 +604,7 @@ const AppContent: React.FC = () => {
558604
formatTime={player.formatTime}
559605
onArtistClick={handleArtistClick}
560606
isTogetherListenConnected={isTogetherListenConnected}
607+
onShareClick={handleShareClick}
561608
/>
562609
</Suspense>
563610
</div>

README.md

Lines changed: 33 additions & 63 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,19 @@
11
# Serene Player - 宁静播放器
22

33
<div align="center">
4-
<img width="1200" height="475" alt="GHBanner" src="./屏幕截图 2026-01-02 003355.png" />
4+
5+
<details>
6+
<summary>应用截图</summary>
7+
8+
<div style="display: flex; overflow-x: auto; justify-content: center; align-items: center; gap: 10px; padding: 10px 0;">
9+
<img height="285" alt="移动端竖屏" src="https://raw.githubusercontent.com/Domdkw/warehouse/2e8b8d64d27e0fc242c5500b2e3f63789aa84407/Serene-Player/%E5%B1%8F%E5%B9%95%E6%88%AA%E5%9B%BE_2-5-2026_103338_localhost.jpeg" />
10+
<img height="285" alt="桌面端横屏1" src="https://raw.githubusercontent.com/Domdkw/warehouse/2e8b8d64d27e0fc242c5500b2e3f63789aa84407/Serene-Player/%E5%B1%8F%E5%B9%95%E6%88%AA%E5%9B%BE_2-5-2026_10380_localhost.jpeg">
11+
<img height="285" alt="桌面端横屏2" src="https://raw.githubusercontent.com/Domdkw/warehouse/2e8b8d64d27e0fc242c5500b2e3f63789aa84407/Serene-Player/%E5%B1%8F%E5%B9%95%E6%88%AA%E5%9B%BE_2-5-2026_103450_localhost.jpeg" />
12+
13+
</div>
14+
15+
</details>
16+
517
</div>
618

719
基于 React 和 TypeScript 构建的现代化优雅音乐播放器。由 Gemini 3 Flash + TRAE + Hand 开发
@@ -15,28 +27,14 @@
1527

1628
## 功能特性
1729

18-
- 🎵 美观简约的用户界面,支持 3D 封面效果
19-
- 🎨 流畅的动画过渡效果
20-
- 🎧 完整播放控制(播放/暂停、切歌、音量调节、进度拖拽)
21-
- 📋 播放列表管理,支持文件夹嵌套
22-
- 🔗 支持外链歌单导入
23-
- 🔄 多种播放模式(单曲、列表、重复、随机)
24-
- 📊 实时进度追踪
25-
- 🎼 歌词自动滚动显示,支持逐字高亮和逐行高亮
26-
- 📝 歌词翻译显示(可开关)
27-
- 🎨 音频可视化频谱效果
28-
- 🎭 全局动态背景效果
29-
- 👤 艺术家视图,按首字母排序
30-
- 🔤 拼音首字母排序支持(中文艺术家)
31-
- 📱 移动端适配,支持手势切换页面
32-
- ⚙️ 可自定义设置(分块加载、字体、字体粗细、字间距、行高)
33-
- 🪞 镜像源支持,加速音乐文件加载
34-
- 💾 本地存储记住用户设置
35-
- 🔍 本地音乐搜索功能,支持按歌曲名和艺术家搜索
36-
- 🎵 网易云音乐集成,支持在线搜索和播放
37-
- ❤️ 收藏功能,保存喜欢的歌曲
38-
- 📄 网易云歌曲详情和封面显示
39-
- 🎤 网易云歌词自动获取和显示
30+
- 完整播放控制(播放/暂停、切歌、进度拖拽)
31+
- 多种播放模式(单曲、列表、重复、随机)
32+
- 歌词翻译显示(可开关)
33+
- 移动端适配,支持手势切换页面
34+
- 本地音乐搜索功能,支持按歌曲名和艺术家搜索
35+
- 网易云音乐集成,支持在线搜索和播放
36+
- 当前播放曲目下载功能
37+
- 媒体会话 API 支持,实现系统媒体控制
4038

4139
## 本地运行
4240

@@ -105,7 +103,7 @@ npm run build
105103

106104
### 添加音乐方法
107105
1. 手动上传,临时
108-
2. 自定义音乐源,参考 [useMusicSource](./hooks/useMusicSource.md)
106+
2. 自定义音乐源,参考 [queryParams](./hooks/QueryParams.md)
109107
3. - [Fork](https://github.com/Domdkw/Serene-Player/fork)
110108
- 将音乐文件添加到 `public/music/` 目录
111109
- 更新 `public/discList.json` 配置播放列表
@@ -118,7 +116,7 @@ npm run build
118116

119117
#### 歌词显示
120118

121-
- 网易云歌曲会自动获取歌词
119+
- 网易云歌曲会自动获取歌词,翻译
122120
- 歌词支持自动滚动和高亮显示
123121
- 可在设置中开启或关闭歌词翻译
124122

@@ -147,51 +145,22 @@ pip install -r requirements.txt
147145
python main.py
148146
```
149147

150-
## 技术栈
151-
152-
- React 19
153-
- TypeScript 5.8
154-
- Vite 6
155-
- Lucide React Icons
156-
- HTML5 Audio API
157-
- fetch-in-chunks(分块加载)
158-
- Tailwind CSS
159-
- jsmediatags(元数据提取)
160-
161148
## 移动端特性
162149

163150
- 左右滑动手势切换页面(播放器/歌词/列表)
164151
- 优化的触控体验
165152
- 响应式布局适配
166153
- 底部迷你播放栏
167154

168-
## 自定义设置
169-
170-
点击设置按钮可调整:
171-
172-
- **分块拉取片数**:1/4/8/16(数值越大加载越快)
173-
- **字体选择**:多种中文字体可选
174-
- **歌词字体粗细**:细体/常规/粗体
175-
- **歌词字间距**:可调节
176-
- **歌词行高**:可调节
177-
- **翻译显示**:开启/关闭歌词翻译
178-
179-
## 浏览器支持
180-
181-
- Chrome 90+
182-
- Firefox 88+
183-
- Safari 14+
184-
- Edge 90+
185-
186155
## 开发计划
187156

188-
- [ ] 支持更多音频格式(FLAC、WAV、OGG)
157+
- [ ] 移动端显示作曲家信息
189158
- [x] 支持网易云音乐播放 灵感源于[maomao1996/Vue-mmPlayer](https://github.com/maomao1996/Vue-mmPlayer)和issue [#1]
190-
- [ ] 歌单分享功能
159+
- [x] 歌单分享功能
191160
- [ ] 深色/浅色主题切换
192-
- [ ] 音乐均衡器调节
193-
- [ ] 播放历史记录
194-
- [ ] 歌曲评分功能
161+
- [ ] 支持更多音乐平台
162+
- [x] 历史记录
163+
- [ ] 添加更多发现插件
195164
- [ ] 播放列表导出/导入
196165

197166
## 贡献指南
@@ -203,8 +172,9 @@ python main.py
203172

204173
本项目基于 MIT 许可证开源 - 查看 [LICENSE](LICENSE) 文件了解详情
205174

206-
## 致谢
175+
## 使用的库
207176

208-
- [jsmediatags](https://github.com/aadsm/jsmediatags) 提供元数据提取功能
209-
- [pinyin-pro](https://github.com/zh-lx/pinyin-pro) 提供拼音支持
210-
- [fetch-in-chunks](https://github.com/AnthumChris/fetch-in-chunks) 提供分块加载支持
177+
- [jsmediatags](https://github.com/aadsm/jsmediatags) 元数据提取功能
178+
- [pinyin-pro](https://github.com/zh-lx/pinyin-pro) 拼音支持
179+
- [fetch-in-chunks](https://github.com/AnthumChris/fetch-in-chunks) 分块加载支持
180+
- [Peerjs](https://github.com/peerjs/peerjs) P2P 播放支持

components/MusicPlayer.tsx

Lines changed: 16 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,8 @@ import {
99
Download,
1010
FileText,
1111
MoreHorizontal,
12-
Users
12+
Users,
13+
Share2
1314
} from 'lucide-react';
1415
import { Track, LyricLine as ParsedLyric } from '../types';
1516
import { getFontFamily } from '../utils/fontUtils';
@@ -46,6 +47,7 @@ interface MusicPlayerProps {
4647
formatTime: (time: number) => string;
4748
onArtistClick?: (artistName: string) => void;
4849
isTogetherListenConnected?: boolean;
50+
onShareClick?: () => void;
4951
}
5052

5153
// 流光加载条组件
@@ -77,6 +79,7 @@ const MusicPlayer: React.FC<MusicPlayerProps> = ({
7779
formatTime,
7880
onArtistClick,
7981
isTogetherListenConnected = false,
82+
onShareClick,
8083
}) => {
8184
const coverRef = useRef<HTMLDivElement>(null);
8285

@@ -338,6 +341,18 @@ const MusicPlayer: React.FC<MusicPlayerProps> = ({
338341
<Languages size={16} />
339342
</button>
340343
</div>
344+
{/* 分享按钮 */}
345+
{onShareClick && (
346+
<div className="relative group">
347+
<button
348+
onClick={onShareClick}
349+
className="flex items-center justify-center w-9 h-9 rounded-full text-white/70 hover:bg-white/20 hover:text-white transition-all"
350+
title="分享"
351+
>
352+
<Share2 size={16} />
353+
</button>
354+
</div>
355+
)}
341356
</div>
342357
{/* Lyric Tags - AR & AL */}
343358
{(track.metadata.lyricArtist || track.metadata.lyricAlbum || track.sourceType === 'streaming') && (

0 commit comments

Comments
 (0)