跨平台音乐播放器 MVP,支持 Windows / Linux / macOS / iOS / Android 五大平台。用户可以播放本地音乐文件和在线音乐流,管理播放列表和收藏,并获得原生级别的系统媒体控制集成。
| 层级 | 技术选型 | 版本 |
|---|---|---|
| 框架 | Flutter | 3.41.5 |
| 语言 | Dart | 3.11.3 |
| 状态管理 | Riverpod 3.x | ^3.3.1 (NotifierProvider API) |
| 路由 | GoRouter | ^17.1.0 |
| 音频引擎 | just_audio + audio_service | ^0.10.5 / ^0.18.18 |
| 本地存储 | Hive | ^2.2.3 |
| 网络 | http | ^1.6.0 |
| UI 规范 | Material Design 3 | — |
- 36 个 Dart 源文件,共计 4,913 行代码
flutter analyze零错误零警告flutter build linux构建通过
┌─────────────────────────────────────────────────────────────┐
│ Flutter UI Layer │
│ Material 3 · 响应式布局 · 键盘快捷键 │
│ ┌──────────┬──────────┬───────────┬────────────┬─────────┐ │
│ │ HomePage │ Library │ SearchPage│ Playlist │ Player │ │
│ │ │ Page │ (多源切换) │ Page │ Page │ │
│ └──────────┴──────────┴───────────┴────────────┴─────────┘ │
│ │ MiniPlayer (全局常驻) │ │
├─────────────────────────────────────────────────────────────┤
│ State Layer (Riverpod) │
│ ┌───────────────┬────────────────┬──────────────────────┐ │
│ │ Player │ Library │ Online │ │
│ │ Providers │ Providers │ Providers │ │
│ │ (20+ 个) │ (3 个) │ (6 个, 含多源切换) │ │
│ └───────┬───────┴────────┬───────┴──────────┬───────────┘ │
│ │ │ ┌──────────────┤ │
│ │ │ │ MusicProvider│ │
│ │ │ │ Registry │ │
├──────────┼────────────────┼────┴──────────────┼──────────────┤
│ │ Data / Service Layer │ │
│ ┌───────▼───────┐ ┌──────▼──────┐ ┌─────────▼─────────┐ │
│ │ AudioHandler │ │ LocalMusic │ │ PlaylistService │ │
│ │ (just_audio │ │ Service │ │ (Hive CRUD) │ │
│ │ + audio_svc) │ │ (文件扫描) │ │ │ │
│ └───────┬───────┘ └─────────────┘ └───────────────────┘ │
│ │ │
│ ┌───────▼───────┐ ┌──────────────────────────────────┐ │
│ │ UrlResolver │ │ MusicProvider (abstract) │ │
│ │ (URL 解析) │ │ ┌─────┬────────┬──────┬───────┐ │ │
│ └───────────────┘ │ │Demo │Netease │ QQ │Spotify│ │ │
│ │ └─────┴────────┴──────┴───────┘ │ │
│ └──────────────────────────────────┘ │
│ │ │
│ ┌───────▼───────┐ ┌─────────────────────────┐ │
│ │ AuthService │ │ http client │ │
│ │ (Hive tokens) │ │ (Netease/QQ/Spotify) │ │
│ └───────────────┘ └─────────────────────────┘ │
├──────────────────────────────────────────────────────────────┤
│ Platform Layer │
│ ┌───────────────────────────────────────────────────────┐ │
│ │ Linux: GStreamer │ Android: ExoPlayer │ iOS: AVFound │ │
│ │ Windows: WinRT │ macOS: AVFoundation │ │
│ └───────────────────────────────────────────────────────┘ │
└──────────────────────────────────────────────────────────────┘
lib/
├── main.dart # 应用入口
├── core/
│ ├── auth/
│ │ ├── auth_service.dart # Hive 令牌存储 (多源认证)
│ │ └── auth_providers.dart # AuthService Riverpod Provider
│ ├── router/app_router.dart # GoRouter 路由配置
│ ├── theme/app_theme.dart # Material 3 主题
│ └── utils/format_duration.dart # 工具函数
├── features/
│ ├── home/ui/
│ │ ├── home_page.dart # 首页(快捷入口)
│ │ └── shell_page.dart # 响应式 Shell(导航栏/侧栏)
│ ├── library/
│ │ ├── data/local_music_service.dart # 本地音乐扫描
│ │ ├── providers/library_providers.dart # 本地歌曲 Provider
│ │ └── ui/library_page.dart # 本地音乐列表
│ ├── online/
│ │ ├── data/
│ │ │ ├── music_provider.dart # 抽象 MusicProvider 接口
│ │ │ ├── qq_music_api.dart # QQ 音乐直连 API 客户端 (685 行)
│ │ │ └── providers/
│ │ │ ├── demo_provider.dart # Demo 源 (SoundHelix)
│ │ │ ├── netease_provider.dart # 网易云音乐 API
│ │ │ ├── qq_provider.dart # QQ 音乐 (直连 u.y.qq.com)
│ │ │ └── spotify_provider.dart # Spotify Web API
│ │ ├── providers/
│ │ │ ├── music_provider_registry.dart # 多源注册中心
│ │ │ └── online_providers.dart # 搜索/热门/源切换 Provider
│ │ └── ui/search_page.dart # 搜索页面 (多源 UI)
│ ├── player/
│ │ ├── data/
│ │ │ ├── audio_handler.dart # just_audio ↔ audio_service 桥接
│ │ │ └── url_resolver.dart # 按需 URL 解析 (过期链接刷新)
│ │ ├── providers/player_providers.dart # 20+ 播放状态 Provider
│ │ └── ui/
│ │ ├── mini_player.dart # 底部迷你播放栏
│ │ ├── player_page.dart # 全屏播放页
│ │ └── queue_page.dart # 播放队列(可拖拽排序)
│ └── playlist/
│ ├── data/playlist_service.dart # Hive CRUD 服务
│ ├── providers/playlist_providers.dart # 播放列表 + 收藏 Provider
│ └── ui/
│ ├── playlist_page.dart # 播放列表管理
│ └── playlist_detail_page.dart # 播放列表详情
└── shared/
├── models/
│ ├── song.dart # Song 数据模型 (含 source/sourceId)
│ ├── playlist.dart # Playlist 数据模型
│ └── music_source.dart # MusicSource 枚举
└── widgets/
├── song_list_tile.dart # 通用歌曲列表项
├── source_icon.dart # 各源图标 & 标签
└── keyboard_shortcuts.dart # 桌面键盘快捷键
采用 Feature-First 分层架构,每个功能模块独立包含 data/、providers/、ui/ 三层:
| 层 | 职责 | 依赖方向 |
|---|---|---|
| UI | Widget 渲染、用户交互 | 依赖 Providers |
| Providers | 状态管理、业务逻辑编排 | 依赖 Data |
| Data | 数据获取(文件系统/网络/数据库) | 不依赖上层 |
shared/ 目录存放跨模块共享的数据模型和通用组件。
文件: lib/features/player/data/audio_handler.dart (339 行)
核心类 MusicPlayerHandler 继承 BaseAudioHandler,混入 SeekHandler 和 QueueHandler,实现 just_audio 与 audio_service 的双向桥接。
audio_service
┌───────────────────┐
│ PlaybackState │ ──→ 系统通知/锁屏控件
│ MediaItem │ ──→ 蓝牙/耳机按钮
│ Queue │
└───────┬───────────┘
│ 双向同步
┌───────────▼───────────┐
│ MusicPlayerHandler │
│ │
│ • play / pause │
│ • skipToNext/Prev │
│ • seek │
│ • loadPlaylist() │
│ • setRepeatMode() │
│ • setShuffleMode() │
└───────────┬───────────┘
│
┌───────▼───────┐
│ just_audio │
│ AudioPlayer │
└───────────────┘
关键实现细节:
-
状态同步:通过
Rx.combineLatest3组合playingStream、processingStateStream、playbackEventStream,实时生成PlaybackState广播给系统。 -
URI 路由:
_createAudioSource(MediaItem)根据 URI 前缀自动选择:/或file://→AudioSource.file()(本地文件)http:///https://→AudioSource.uri()(网络流)
-
队列管理:使用
AudioPlayer的原生队列 API(addAudioSource/removeAudioSourceAt/insertAudioSource),避免已废弃的ConcatenatingAudioSource手动管理。 -
时长修正:
_listenToDuration()监听实际解码时长,自动更新MediaItem.duration并同步回 queue。
文件: lib/features/player/providers/player_providers.dart (250 行)
提供 20+ 个 Riverpod Provider,分为三类:
| 类别 | Provider | 类型 | 用途 |
|---|---|---|---|
| 核心 | audioHandlerProvider |
Provider<MusicPlayerHandler> |
Handler 单例(main.dart 中 override) |
| 流式 | currentMediaItemProvider |
StreamProvider<MediaItem?> |
当前曲目 |
playbackStateProvider |
StreamProvider<PlaybackState> |
播放状态 | |
positionProvider |
StreamProvider<Duration> |
当前位置 | |
bufferedPositionProvider |
StreamProvider<Duration> |
缓冲位置 | |
durationProvider |
StreamProvider<Duration?> |
总时长 | |
queueProvider |
StreamProvider<List<MediaItem>> |
播放队列 | |
| 派生 | isPlayingProvider |
Provider<bool> |
是否正在播放 |
currentSongProvider |
Provider<Song?> |
当前 Song 模型 | |
progressProvider |
Provider<double> |
播放进度 0.0~1.0 | |
hasNextProvider |
Provider<bool> |
是否有下一曲 | |
hasPreviousProvider |
Provider<bool> |
是否有上一曲 | |
| 状态 | playModeProvider |
NotifierProvider<PlayModeNotifier, PlayMode> |
播放模式管理 |
播放模式状态机:
sequential ──→ repeatAll ──→ repeatOne ──→ shuffle ──→ sequential
│ │ │ │
└── LoopMode.off LoopMode.all LoopMode.one shuffle=true
文件: lib/features/library/data/local_music_service.dart (75 行)
- 递归扫描
~/Music目录 - 支持格式:
.mp3,.flac,.wav,.ogg,.m4a - 文件名解析策略:
- 匹配
Artist - Title.ext模式 → 自动提取艺术家和标题 - 不匹配 → 文件名作为标题,艺术家设为 "Unknown Artist"
- 匹配
- 生成唯一 ID:文件路径 hashCode 的十六进制表示
通过 LibrarySortNotifier + sortedLocalSongsProvider 实现三种排序:
- 按标题 (title)
- 按艺术家 (artist)
- 按专辑 (album)
在线音乐模块采用 策略模式 (Strategy Pattern),通过抽象接口 MusicProvider 统一各音乐平台的搜索、播放、详情等操作。新增音乐源只需实现接口并注册到 MusicProviderRegistry。
MusicProvider (abstract)
┌───────────────────────┐
│ + source: MusicSource │
│ + displayName: String │
│ + requiresAuth: bool │
│ + isAuthenticated │
│ + search(query) │
│ + resolvePlayUrl(song) │
│ + getSongDetail(id) │
│ + getHotSongs() │
│ + getRecommendations() │
└───────┬───────────────┘
┌─────────┬───┴────────┬───────────┐
▼ ▼ ▼ ▼
DemoProvider NeteaseProvider QQMusic Spotify
(内置) (localhost:3000) Provider Provider
(u.y.qq.com (api.spotify.com)
直连 API)
文件: lib/shared/models/music_source.dart
enum MusicSource { local, netease, qq, spotify, demo }
每个在线歌曲通过 Song.source 和 Song.sourceId 标记其来源平台和平台侧 ID,确保跨源歌曲不会混淆。
文件: lib/features/online/data/music_provider.dart
| 方法/属性 | 类型 | 说明 |
|---|---|---|
source |
MusicSource |
该 Provider 对应的音乐源 |
displayName |
String |
UI 显示名称 |
requiresAuth |
bool |
是否需要认证才能使用 |
isAuthenticated |
Future<bool> |
异步检查当前是否已认证 |
search(query, {limit, offset}) |
Future<List<Song>> |
搜索歌曲 |
resolvePlayUrl(song) |
Future<String?> |
获取可播放 URL(处理签名过期) |
getSongDetail(sourceId) |
Future<Song?> |
通过平台 ID 获取歌曲详情 |
getHotSongs({limit}) |
Future<List<Song>> |
获取热门/推荐歌曲 |
getRecommendations({limit}) |
Future<List<Song>> |
获取个性化推荐 |
文件: lib/features/online/data/providers/demo_provider.dart (160 行)
从原 OnlineMusicService 迁移而来,内置 8 首 SoundHelix 公共域示例音乐:
| sourceId | 曲名 | 艺术家 | 专辑 |
|---|---|---|---|
| demo-1 | Ambient Soundscape | SoundHelix | Demo Collection |
| demo-2 | Electronic Dreams | SoundHelix | Demo Collection |
| demo-3 | Classical Fusion | SoundHelix | Instrumental Vibes |
| demo-4 | Jazz Exploration | Melody Makers | Instrumental Vibes |
| demo-5 | Rock Anthem | Melody Makers | Power Tracks |
| demo-6 | Chill Lofi Beat | LoFi Studio | Relaxation |
| demo-7 | Synthwave Runner | Retro Synth | Neon Nights |
| demo-8 | Acoustic Morning | Nature Sounds | Relaxation |
- URL 永不过期(SoundHelix 静态资源),
resolvePlayUrl()直接返回song.uri - 搜索支持标题/艺术家/专辑大小写不敏感匹配,模拟 300ms 网络延迟
requiresAuth: false
文件: lib/features/online/data/providers/netease_provider.dart (224 行)
对接 NeteaseCloudMusicApi 开源项目(需本地部署,默认 localhost:3000)。
| API 端点 | 方法 | 说明 |
|---|---|---|
/search?keywords=... |
search() |
搜索歌曲,返回 result.songs[] |
/song/url?id=... |
resolvePlayUrl() |
获取播放 URL(临时签名,会过期) |
/song/detail?ids=... |
getSongDetail() |
歌曲详情(ar[]/al/dt 字段) |
/top/song?type=0 |
getHotSongs() |
新歌速递 |
- 两套独立 JSON 映射器(搜索结果 vs 详情的字段名不同:
artists/albumvsar/al) requiresAuth: false(公开 API 不强制登录)
文件:
lib/features/online/data/qq_music_api.dart(685 行) — 纯 API 客户端层lib/features/online/data/providers/qq_provider.dart(268 行) — MusicProvider 实现
直接对接 QQ 音乐内部 API (u.y.qq.com/cgi-bin/musicu.fcg),无需外部 Python/Node 服务器。这是 QQMusicApi 的纯 Dart 移植。
qq_music_api.dart — 纯 API 客户端
所有请求通过单一 POST 端点发送 JSON body,包含 comm(公共参数)和模块特定请求数据。
| 方法 | 功能 | 说明 |
|---|---|---|
search(keyword, {page, limit}) |
搜索歌曲 | 返回 {list, totalnum, curpage, curnum} |
querySong(mids) |
批量查询歌曲信息 | 通过 songmid 查询详情 |
getSongUrls(mids, {credential, quality}) |
获取播放 URL | 支持 4 种音质,需 vkey 签名 |
getTryUrl(mid) |
获取试听 URL | 免登录 30 秒试听 |
getLyric(mid) |
获取歌词 | 返回 Base64 编码的 LRC |
getLyricText(mid) |
获取歌词文本 | 解码后直接返回 UTF-8 |
getTopCategory() |
获取排行榜分类 | 全部榜单列表 |
getTopDetail(topId, {limit}) |
获取排行榜详情 | 指定榜单的歌曲列表 |
getHotkey() |
获取热搜关键词 | 用于搜索推荐 |
getAlbumCover(mid) |
获取专辑封面 | 从 CDN 获取 |
getAlbumDetail(mid) |
获取专辑详情 | 含歌曲列表 |
getAlbumSongs(mid, {limit}) |
获取专辑歌曲 | 分页获取 |
音质枚举 (QQMusicQuality):
| 枚举值 | 文件前缀 | 格式 | 说明 |
|---|---|---|---|
mp3_128 |
M500 | .mp3 | 128 kbps MP3 |
mp3_320 |
M800 | .mp3 | 320 kbps MP3(默认) |
flac |
F000 | .flac | 无损 FLAC |
ogg_192 |
O600 | .ogg | 192 kbps OGG |
认证 (QQMusicCredential):
Cookie 模式认证,通过 musicid(QQ 号或微信 uin)+ musickey(会话密钥)组合。
Q_H_L_前缀 → QQ 登录 (loginType=2)W_X_前缀 → 微信登录 (loginType=1)
认证为可选:搜索、排行榜等公共功能无需登录;高品质流媒体需要凭证。
qq_provider.dart — MusicProvider 集成
| MusicProvider 方法 | 实现 | 说明 |
|---|---|---|
search() |
QQMusicApi.search() |
搜索歌曲,映射为 Song 模型 |
resolvePlayUrl() |
getSongUrls() → fallback getTryUrl() |
优先完整链接,失败时回退试听 |
getSongDetail() |
QQMusicApi.querySong() |
通过 songmid 查询 |
getHotSongs() |
QQMusicApi.getTopDetail(26) |
热歌榜 (topId=26) |
getRecommendations() |
QQMusicApi.getTopDetail(62) |
飙升榜 (topId=62) |
额外方法:
-
getLyric(mid)— 获取歌词(供未来歌词页使用) -
getTopCategories()— 获取全部排行榜分类 -
专辑封面通过 albumMid 拼接 CDN URL:
https://y.gtimg.cn/music/photo_new/T002R300x300M000{albumMid}.jpg -
requiresAuth: false(基本功能免登录) -
凭证通过
AuthService读取:musickey→getToken(qq),musicid→getAuthData(qq, 'musicid')
文件: lib/features/online/data/providers/spotify_provider.dart (229 行)
对接 Spotify Web API(直接调用 api.spotify.com,需 OAuth2 token)。
| API 端点 | 方法 | 说明 |
|---|---|---|
/search?q=...&type=track |
search() |
搜索曲目 |
/tracks/{id} |
resolvePlayUrl() |
返回 preview_url(30 秒预览) |
/tracks/{id} |
getSongDetail() |
曲目详情 |
/recommendations?seed_tracks=... |
getRecommendations() |
个性化推荐 |
requiresAuth: true— 所有 API 调用需 Bearer token- MVP 阶段播放限制为 30 秒预览(完整播放需 Spotify SDK)
- 依赖
AuthService管理 token 存取
文件: lib/features/online/providers/music_provider_registry.dart
注册中心在 Riverpod Provider 中初始化,自动注册所有 4 个音乐源:
final musicProviderRegistryProvider = Provider<MusicProviderRegistry>((ref) {
final authService = ref.watch(authServiceProvider);
final registry = MusicProviderRegistry()
..register(DemoProvider())
..register(NeteaseProvider())
..register(QQMusicProvider(authService))
..register(SpotifyProvider(authService));
return registry;
});
扩展新音乐源只需:① 实现 MusicProvider 子类,② 在 MusicSource 枚举中添加值,③ 在此注册。
文件: lib/features/player/data/url_resolver.dart (33 行)
解决 QQ 音乐/网易云等平台 URL 临时签名过期问题:
播放请求 → UrlResolver.resolve(song)
├─ source == local → 直接返回文件路径
└─ source == online → 调用 provider.resolvePlayUrl()
→ 返回最新可用 URL
SearchPage 在播放前对所有歌曲调用 UrlResolver,确保传入 AudioHandler 的 URL 是有效的。
文件: lib/core/auth/auth_service.dart (60 行)
基于 Hive 的令牌持久化服务,支持多源独立认证:
| 方法 | 说明 |
|---|---|
saveToken(source, token) |
保存某源的访问令牌 |
getToken(source) |
获取令牌(同步,null 表示未登录) |
saveAuthData(source, data) |
保存额外认证数据(refresh token、cookie 等) |
getAuthData(source, key) |
获取指定认证数据字段 |
clearAuth(source) |
清除某源的全部认证信息(登出) |
hasToken(source) |
快速检查是否有令牌 |
存储 key 规则:{source.name} 为主 token,{source.name}_{key} 为附加数据。
文件: lib/features/online/providers/online_providers.dart (87 行)
| Provider | 类型 | 说明 |
|---|---|---|
activeSourceProvider |
NotifierProvider<..., MusicSource> |
当前选中的音乐源 Tab |
searchQueryProvider |
NotifierProvider<..., String> |
搜索输入文本 |
searchResultsProvider |
FutureProvider<List<Song>> |
搜索结果(watch source + query) |
hotSongsProvider |
FutureProvider<List<Song>> |
热门歌曲(无搜索时展示) |
activeSourceProviderInstance |
Provider<MusicProvider?> |
当前源对应的 Provider 实例 |
sourceNeedsLoginProvider |
FutureProvider<bool> |
是否需要显示登录卡片 |
搜索仅在 query >= 2 字符时触发,避免过于频繁的请求。切换音乐源自动刷新搜索结果和热门歌曲。
文件: lib/features/online/ui/search_page.dart (346 行)
┌──────────────────────────────────────┐
│ Search Online │ ← AppBar
├──────────────────────────────────────┤
│ [🎵 Demo] [☁ Netease] [♫ QQ] [🎧] │ ← ChoiceChip 源切换
├──────────────────────────────────────┤
│ 🔍 Search for songs, artists... ✕ │ ← TextField
├──────────────────────────────────────┤
│ │
│ 无搜索 → 显示 Hot Songs │
│ 有搜索 → 显示搜索结果 │
│ 需登录 → 显示 Login Required 卡片 │
│ │
└──────────────────────────────────────┘
- 源切换行使用
SingleChildScrollView水平滚动的ChoiceChip - 每个 Chip 显示
sourceIcon()+ providerdisplayName - 认证门控:对
requiresAuth的源(如 Spotify),通过sourceNeedsLoginProvider异步检查认证状态 - 播放前通过
UrlResolver解析所有歌曲 URL,过滤掉无法解析的歌曲
文件: lib/features/playlist/data/playlist_service.dart (120 行)
使用 Hive 作为轻量 NoSQL 存储:
| Hive Box | Key | Value | 用途 |
|---|---|---|---|
playlists |
playlist UUID | JSON string (Playlist) | 用户播放列表 |
favorites |
song ID | JSON string (Song) | 收藏歌曲 |
序列化方案:使用 dart:convert 的 jsonEncode/jsonDecode,配合 Song.toJson()/Song.fromJson() 和 Playlist.toJson()/Playlist.fromJson()。不使用 Hive TypeAdapter,避免了 hive_generator 的依赖冲突。
createPlaylist(name) → 生成 UUID v4 → 存入 Hive
deletePlaylist(id) → 从 Hive 删除
renamePlaylist(id, newName) → 读取 → 修改 → 写回
addSongToPlaylist(id, song) → 去重检查 → 追加 → 写回
removeSongFromPlaylist(id, songId) → 过滤 → 写回
收藏本质上是一个特殊的 "歌曲集合",以 song ID 为 key 独立存储在 favorites box 中:
toggleFavorite(song): 存在则删除,不存在则添加isFavorite(songId): O(1) 查询,利用 Hive box 的containsKey
┌─────────────────────────────────────────────┐
│ App │
│ ┌─────────────────────────────────────────┐│
│ │ StatefulShellRoute (IndexedStack) ││
│ │ ┌─────┬─────────┬────────┬──────────┐ ││
│ │ │Home │ Library │ Search │ Playlists│ ││
│ │ │ / │/library │/search │/playlists│ ││
│ │ └─────┴─────────┴────────┴──┬───────┘ ││
│ │ │ ││
│ │ /playlists/:id ││
│ └─────────────────────────────────────────┘│
│ │
│ /player ← 全屏播放页(滑入动画,独立导航栈) │
└─────────────────────────────────────────────┘
使用 StatefulShellRoute.indexedStack 确保各 Tab 页面状态独立保持(切换 Tab 不会销毁/重建)。
全屏播放页 /player 使用 parentNavigatorKey 指向根导航器,配合 CustomTransitionPage 实现从底部滑入的动画效果。
文件: lib/features/home/ui/shell_page.dart (104 行)
| 屏幕宽度 | 导航形式 | MiniPlayer 位置 |
|---|---|---|
| < 800px (手机/小平板) | 底部 NavigationBar |
NavigationBar 上方 |
| >= 800px (桌面/大平板) | 左侧 NavigationRail |
底部全宽 |
移动端布局: 桌面端布局:
┌──────────────────┐ ┌──┬────────────────┐
│ │ │ │ │
│ Content │ │N │ Content │
│ │ │a │ │
│ │ │v │ │
├──────────────────┤ │ │ │
│ MiniPlayer │ │R │ │
├──────────────────┤ │a │ │
│ NavigationBar │ │i │ │
│ 🏠 📁 🔍 📋 │ │l │ │
└──────────────────┘ ├──┴────────────────┤
│ MiniPlayer │
└───────────────────┘
┌──────────────────────────────────┐
│ ▼ 收起 🎵 队列 │ ← AppBar
│ │
│ ┌──────────────────┐ │
│ │ │ │
│ │ Album Cover │ │
│ │ 300 x 300 │ │
│ │ │ │
│ └──────────────────┘ │
│ │
│ 歌曲标题 (headlineSmall) │
│ 艺术家 (titleMedium) │
│ │
│ 0:45 ───●────────────── 3:21 │ ← ProgressBar (可拖拽)
│ │
│ ⏮ ▶ (FAB) ⏭ │ ← 主控制
│ │
│ 🔀 🔁 │ ← 模式切换
│ │
│ 🔉 ─────────────────── 🔊 │ ← 音量 (仅桌面端)
└──────────────────────────────────┘
桌面端独有功能:
- 音量滑块(
StreamBuilder实时监听player.volumeStream) - 仅在
Platform.isLinux || Platform.isMacOS || Platform.isWindows时显示
┌─────────────────────────────────┐
│ ═══════●═══════════════════════ │ ← 2px 进度条
│ 🎵 歌曲名 - 艺术家 ▶/⏸ │ ← 点击进入全屏
└─────────────────────────────────┘
- 仅当有曲目加载时显示(否则返回
SizedBox.shrink()) - 点击主区域通过
context.push('/player')打开全屏播放页
ReorderableListView.builder实现拖拽排序- 当前播放项高亮(
primaryContainer背景色) - AppBar "Clear Queue" 按钮清空队列
基于 ColorScheme.fromSeed(seedColor: Colors.deepPurple),自动派生完整的 Material 3 色彩系统:
| 属性 | 亮色模式 | 暗色模式 |
|---|---|---|
| 主色调 | Deep Purple 派生 | Deep Purple 派生 (darkMode) |
| Material 3 | 启用 | 启用 |
| AppBar | Surface 背景 | Surface 背景 |
| 导航栏 | 系统默认 M3 | 系统默认 M3 |
| 卡片 | 圆角 12px | 圆角 12px |
支持跟随系统自动切换亮色/暗色(ThemeMode.system)。
class Song {
final String id; // 唯一标识(本地:路径 hash;在线:{source}-{sourceId})
final String title; // 歌曲标题
final String? artist; // 艺术家
final String? album; // 专辑名
final Duration? duration; // 时长
final String? uri; // 播放地址(本地路径或 URL)
final String? artworkUrl; // 封面图地址
final bool isLocal; // 是否本地文件
final MusicSource source; // 来源平台 (默认: MusicSource.local)
final String? sourceId; // 平台侧原始 ID (用于 API 调用)
}
提供与 audio_service.MediaItem 的双向转换:
Song.fromMediaItem(MediaItem)— 从系统通知/播放控制反向构建(extras 中恢复 source/sourceId)song.toMediaItem()— 转为 MediaItem 用于播放引擎(source/sourceId 存入 extras)
toJson()/fromJson() 序列化中 source 使用 name 字符串存储,缺失时默认 MusicSource.local,确保向后兼容。
class Playlist {
final String id; // UUID v4
final String name; // 播放列表名称
final List<Song> songs; // 歌曲列表
final DateTime createdAt; // 创建时间
}
文件: lib/shared/widgets/keyboard_shortcuts.dart (85 行)
| 按键 | 功能 |
|---|---|
Space |
播放/暂停 |
← |
后退 5 秒 |
→ |
前进 5 秒 |
↑ |
音量 +0.1 |
↓ |
音量 -0.1 |
N |
下一曲 |
P |
上一曲 |
通过 Focus widget 的 onKeyEvent 实现,仅处理 KeyDownEvent 避免重复触发。
audio_service 在 Linux 上自动通过 D-Bus 实现 MPRIS 协议,无需额外代码即可支持:
- 系统媒体通知
- 桌面环境媒体控件(如 GNOME 顶栏)
- 蓝牙/耳机媒体按钮
main() async {
WidgetsFlutterBinding.ensureInitialized();
// 1. 初始化 Hive 文件存储
await Hive.initFlutter();
// 2. 打开播放列表和收藏的 Hive Box
await PlaylistService.init();
// 3. 初始化认证令牌存储
await AuthService.init();
// 4. 初始化 audio_service,注册 MusicPlayerHandler
final audioHandler = await AudioService.init(
builder: () => MusicPlayerHandler(),
config: AudioServiceConfig(...),
);
// 5. 启动应用,注入 audioHandler 到 Riverpod
runApp(
ProviderScope(
overrides: [audioHandlerProvider.overrideWithValue(audioHandler)],
child: const MusicPlayerApp(),
),
);
}
依赖注入关键点:
audioHandlerProvider在定义时抛出UnimplementedError,必须在ProviderScope.overrides中注入实际实例。AuthService.init()打开auth_tokensHive box,必须在runApp前完成。MusicProviderRegistry在首次访问时通过 Riverpod 自动初始化并注册所有 4 个音乐源。
# 核心
flutter_riverpod: ^3.3.1 # 状态管理
go_router: ^17.1.0 # 声明式路由
just_audio: ^0.10.5 # 跨平台音频播放
audio_service: ^0.18.18 # 后台播放 + 系统媒体控制
just_audio_background: ^0.0.1 # just_audio ↔ audio_service 桥接
# 数据
hive: ^2.2.3 # 轻量 NoSQL 存储
hive_flutter: ^1.1.0 # Hive Flutter 适配
uuid: ^4.5.3 # 播放列表 ID 生成
# 网络
http: ^1.6.0 # HTTP 客户端 (Netease/QQ/Spotify API)
# UI
audio_video_progress_bar: ^2.0.3 # 音频进度条
cached_network_image: ^3.4.1 # 网络图片缓存
# 工具
rxdart: ^0.28.0 # 响应式流组合
on_audio_query: ^2.9.0 # 音频元数据查询(预留)
path_provider: ^2.1.5 # 平台路径
| 优先级 | 功能 | 说明 |
|---|---|---|
| ✅ 已完成:Netease / QQ Music / Spotify + 可扩展架构 | ||
| ✅ 已完成:纯 Dart 移植 u.y.qq.com 协议,无需外部服务器 | ||
| P0 | 音频元数据读取 | 使用 on_audio_query 或 id3 包读取 MP3 标签和封面 |
| P0 | Netease 后端部署指南 | 提供 Docker 一键部署文档 |
| P0 | Spotify OAuth2 登录流程 | 实现 PKCE 流程,对接 Login 按钮 |
| P1 | 歌词显示 | LRC 格式逐行高亮同步 |
| P1 | 搜索历史 | Hive 持久化搜索记录 |
| 优先级 | 功能 | 说明 |
|---|---|---|
| P1 | 均衡器 | just_audio AndroidEqualizer / iOS 原生均衡器 |
| P1 | 睡眠定时器 | 定时暂停播放 |
| P2 | 跨设备同步 | 通过 Firebase/Supabase 同步播放列表 |
| P2 | 自定义主题色 | 用户可选 seed color,或从封面提取主色调 |
| 优先级 | 功能 | 说明 |
|---|---|---|
| P2 | 播客支持 | RSS 订阅、章节标记、播放速度 |
| P2 | CarPlay / Android Auto | 车载适配 |
| P3 | 社交功能 | 分享歌单、协作播放列表 |
| P3 | 本地网络播放 | DLNA/AirPlay 推送 |
- Flutter SDK >= 3.41.0
- Dart SDK >= 3.11.0
- Linux: GStreamer 开发库 (
libgstreamer1.0-dev) - macOS: Xcode >= 15
- Windows: Visual Studio 2022 with Desktop C++ workload
# 获取依赖
flutter pub get
# Linux 桌面运行
flutter run -d linux
# Android 运行
flutter run -d <device_id>
# 构建 Release
flutter build linux
flutter build apk
flutter build ios
flutter build macos
flutter build windows
将音乐文件放入 ~/Music 目录,支持格式:.mp3, .flac, .wav, .ogg, .m4a。
文件名建议使用 艺术家 - 标题.mp3 格式以获得最佳解析效果。
在 Search 页面选择 Demo Music 源,输入以下关键词试听示例音乐:
sound— 匹配 SoundHelix 系列jazz— Jazz Explorationchill— Chill Lofi Beatrelaxation— 匹配 Relaxation 专辑下的歌曲
- 部署 NeteaseCloudMusicApi:
docker run -p 3000:3000 binaryify/netease_cloud_music_api
- 在 Search 页面选择 Netease Cloud Music 源即可搜索
QQ 音乐使用直连 API,无需部署任何外部服务器。在 Search 页面选择 QQ Music 源即可搜索和试听。
- 基础功能(搜索、排行榜、试听)无需登录
- 高品质流媒体需要提供 QQ Music cookie 凭证(
musicid+musickey)
- 注册 Spotify Developer 应用获取 Client ID
- 完成 OAuth2 认证后,token 将自动保存
- 在 Search 页面选择 Spotify 源(MVP 阶段仅支持 30 秒预览)