From 708ab91ccbf8ec5b4cfeeedd284c2ce805097dd7 Mon Sep 17 00:00:00 2001 From: lxh2875931338 Date: Sun, 1 Nov 2026 16:15:49 +0800 Subject: [PATCH] =?UTF-8?q?fix(playback):=20=E6=92=AD=E6=94=BE=E9=A1=B5/?= =?UTF-8?q?=E8=BF=B7=E4=BD=A0=E6=92=AD=E6=94=BE=E5=99=A8=E7=AB=8B=E5=8D=B3?= =?UTF-8?q?=E6=98=BE=E7=A4=BA=E5=B7=B2=E6=9C=89=E5=B0=81=E9=9D=A2=EF=BC=8C?= =?UTF-8?q?=E4=B8=8D=E5=86=8D=E7=AD=89=E5=85=83=E6=95=B0=E6=8D=AE=E6=B5=81?= =?UTF-8?q?=E7=A8=8B?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 原因有两层: 1) Song 模型没有封面路径,播放时只带 url/title/artist, 播放页只能等元数据服务返回 artwork 字节后才显示; 2) 元数据回来后用新 Song 覆盖当前歌曲时丢了 artworkPath, 即使带上了也会被冲掉。 - Song 增加 artworkPath 字段 - PlaybackController / 首页收藏 / playPlaylist 构造 Song 时带上 - 播放页:artwork 字节优先,其次直接读 artwork_path 文件 - 迷你播放器同样加 FileImage 兜底 - 元数据更新重建 Song 时保留 artworkPath 真机验证:点击播放后封面立即出现,不再先显示音符占位。 --- lib/controllers/playback_controller.dart | 2 ++ lib/pages/home_page.dart | 2 ++ lib/pages/player_page.dart | 28 +++++++++++++++++------- lib/services/audio_service.dart | 8 +++++++ lib/widgets/global_mini_player.dart | 19 +++++++++++++--- 5 files changed, 48 insertions(+), 11 deletions(-) diff --git a/lib/controllers/playback_controller.dart b/lib/controllers/playback_controller.dart index 2f4d8e3..1605de0 100644 --- a/lib/controllers/playback_controller.dart +++ b/lib/controllers/playback_controller.dart @@ -98,6 +98,7 @@ class PlaybackController { final dbSong = await _db.getSongByPath(songPath); final song = Song( id: songPath, + artworkPath: dbSong?['artwork_path'] as String?, title: dbSong?['title'] as String? ?? songPath.split('/').last.replaceAll(RegExp(r'\.[^.]*$'), ''), artist: dbSong?['artist'] as String? ?? '未知艺术家', @@ -117,6 +118,7 @@ class PlaybackController { if (dbSong != null) { queue.add(Song( id: item.path, + artworkPath: dbSong['artwork_path'] as String?, title: dbSong['title'] as String? ?? item.name.replaceAll(RegExp(r'\.[^.]*$'), ''), artist: dbSong['artist'] as String? ?? '未知艺术家', diff --git a/lib/pages/home_page.dart b/lib/pages/home_page.dart index 89c9ce8..c8ac778 100644 --- a/lib/pages/home_page.dart +++ b/lib/pages/home_page.dart @@ -378,6 +378,7 @@ class _HomePageState extends BaseState { id: song.path, title: song.displayTitle, artist: song.displaySubtitle, + artworkPath: song.artworkPath, url: url, )); } catch (e) { @@ -619,6 +620,7 @@ class _HomePageState extends BaseState { id: item.path, title: item.displayTitle, artist: item.displaySubtitle, + artworkPath: item.artworkPath, url: WebDAVService.instance.getFileUrl(item.path), )) .toList(); diff --git a/lib/pages/player_page.dart b/lib/pages/player_page.dart index 5ad2cb4..d6bf40f 100644 --- a/lib/pages/player_page.dart +++ b/lib/pages/player_page.dart @@ -1,3 +1,4 @@ +import 'dart:io'; // lib/pages/player_page.dart import 'package:flutter/material.dart'; import 'package:provider/provider.dart'; @@ -322,6 +323,11 @@ class _PlayerPageState extends BaseState { ? (screen.height * 0.38).clamp(160.0, 380.0) : null; + final artworkPath = song.artworkPath; + final hasArtworkFile = artworkPath != null && + artworkPath.isNotEmpty && + File(artworkPath).existsSync(); + final cover = ClipRRect( borderRadius: BorderRadius.circular(compact ? 18 : 24), child: song.artwork != null @@ -330,14 +336,20 @@ class _PlayerPageState extends BaseState { fit: BoxFit.cover, gaplessPlayback: true, ) - : Container( - color: const Color(0xFF2A3332), - child: Icon( - Icons.music_note, - size: compact ? 48 : 72, - color: Colors.white24, - ), - ), + : hasArtworkFile + ? Image.file( + File(artworkPath), + fit: BoxFit.cover, + gaplessPlayback: true, + ) + : Container( + color: const Color(0xFF2A3332), + child: Icon( + Icons.music_note, + size: compact ? 48 : 72, + color: Colors.white24, + ), + ), ); final padded = Padding( diff --git a/lib/services/audio_service.dart b/lib/services/audio_service.dart index fe92963..e591648 100644 --- a/lib/services/audio_service.dart +++ b/lib/services/audio_service.dart @@ -32,12 +32,17 @@ class Song { final String? url; final Uint8List? artwork; + /// 本地封面文件路径(songs.artwork_path)。 + /// 命中过元数据的歌直接用它显示封面,不必等元数据流程回来。 + final String? artworkPath; + Song({ required this.id, required this.title, required this.artist, this.url, this.artwork, + this.artworkPath, }); Map toJson() => { @@ -299,6 +304,7 @@ class AudioService extends ChangeNotifier { final path = dbSong['remote_path'] as String; songs.add(Song( id: path, + artworkPath: dbSong['artwork_path'] as String?, title: dbSong['title'] as String? ?? '', artist: dbSong['artist'] as String? ?? '未知艺术家', url: WebDAVService.instance.getFileUrl(path), @@ -855,6 +861,8 @@ class AudioService extends ChangeNotifier { artist: metadata.artist.isNotEmpty ? metadata.artist : song.artist, url: song.url, artwork: metadata.artwork, + // 保留封面文件路径:元数据流程回来前,播放页靠它立即显示封面 + artworkPath: song.artworkPath, ); _queue[_currentIndex] = updatedSong; _currentSong = updatedSong; diff --git a/lib/widgets/global_mini_player.dart b/lib/widgets/global_mini_player.dart index df7c722..ef4180c 100644 --- a/lib/widgets/global_mini_player.dart +++ b/lib/widgets/global_mini_player.dart @@ -1,3 +1,4 @@ +import 'dart:io'; // lib/widgets/global_mini_player.dart import 'package:flutter/material.dart'; import 'package:provider/provider.dart'; @@ -54,7 +55,6 @@ class GlobalMiniPlayer extends StatelessWidget { borderRadius: BorderRadius.circular(12), child: Row( children: [ - const SizedBox(width: 12), Container( width: 44, height: 44, @@ -66,9 +66,22 @@ class GlobalMiniPlayer extends StatelessWidget { image: MemoryImage(song.artwork!), fit: BoxFit.cover, ) - : null, + : (song.artworkPath != null && + song.artworkPath!.isNotEmpty && + File(song.artworkPath!) + .existsSync()) + ? DecorationImage( + image: FileImage( + File(song.artworkPath!)), + fit: BoxFit.cover, + ) + : null, ), - child: song.artwork == null + child: (song.artwork == null && + (song.artworkPath == null || + song.artworkPath!.isEmpty || + !File(song.artworkPath!) + .existsSync())) ? const Icon( Icons.music_note, color: Colors.white38,