fix(playback): 播放页/迷你播放器立即显示已有封面,不再等元数据流程

原因有两层:
1) Song 模型没有封面路径,播放时只带 url/title/artist,
   播放页只能等元数据服务返回 artwork 字节后才显示;
2) 元数据回来后用新 Song 覆盖当前歌曲时丢了 artworkPath,
   即使带上了也会被冲掉。

- Song 增加 artworkPath 字段
- PlaybackController / 首页收藏 / playPlaylist 构造 Song 时带上
- 播放页:artwork 字节优先,其次直接读 artwork_path 文件
- 迷你播放器同样加 FileImage 兜底
- 元数据更新重建 Song 时保留 artworkPath

真机验证:点击播放后封面立即出现,不再先显示音符占位。
This commit is contained in:
2026-11-01 16:15:49 +08:00
parent a927e3dd44
commit 708ab91ccb
5 changed files with 48 additions and 11 deletions
+2
View File
@@ -98,6 +98,7 @@ class PlaybackController {
final dbSong = await _db.getSongByPath(songPath); final dbSong = await _db.getSongByPath(songPath);
final song = Song( final song = Song(
id: songPath, id: songPath,
artworkPath: dbSong?['artwork_path'] as String?,
title: dbSong?['title'] as String? ?? title: dbSong?['title'] as String? ??
songPath.split('/').last.replaceAll(RegExp(r'\.[^.]*$'), ''), songPath.split('/').last.replaceAll(RegExp(r'\.[^.]*$'), ''),
artist: dbSong?['artist'] as String? ?? '未知艺术家', artist: dbSong?['artist'] as String? ?? '未知艺术家',
@@ -117,6 +118,7 @@ class PlaybackController {
if (dbSong != null) { if (dbSong != null) {
queue.add(Song( queue.add(Song(
id: item.path, id: item.path,
artworkPath: dbSong['artwork_path'] as String?,
title: dbSong['title'] as String? ?? title: dbSong['title'] as String? ??
item.name.replaceAll(RegExp(r'\.[^.]*$'), ''), item.name.replaceAll(RegExp(r'\.[^.]*$'), ''),
artist: dbSong['artist'] as String? ?? '未知艺术家', artist: dbSong['artist'] as String? ?? '未知艺术家',
+2
View File
@@ -378,6 +378,7 @@ class _HomePageState extends BaseState<HomePage> {
id: song.path, id: song.path,
title: song.displayTitle, title: song.displayTitle,
artist: song.displaySubtitle, artist: song.displaySubtitle,
artworkPath: song.artworkPath,
url: url, url: url,
)); ));
} catch (e) { } catch (e) {
@@ -619,6 +620,7 @@ class _HomePageState extends BaseState<HomePage> {
id: item.path, id: item.path,
title: item.displayTitle, title: item.displayTitle,
artist: item.displaySubtitle, artist: item.displaySubtitle,
artworkPath: item.artworkPath,
url: WebDAVService.instance.getFileUrl(item.path), url: WebDAVService.instance.getFileUrl(item.path),
)) ))
.toList(); .toList();
+20 -8
View File
@@ -1,3 +1,4 @@
import 'dart:io';
// lib/pages/player_page.dart // lib/pages/player_page.dart
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:provider/provider.dart'; import 'package:provider/provider.dart';
@@ -322,6 +323,11 @@ class _PlayerPageState extends BaseState<PlayerPage> {
? (screen.height * 0.38).clamp(160.0, 380.0) ? (screen.height * 0.38).clamp(160.0, 380.0)
: null; : null;
final artworkPath = song.artworkPath;
final hasArtworkFile = artworkPath != null &&
artworkPath.isNotEmpty &&
File(artworkPath).existsSync();
final cover = ClipRRect( final cover = ClipRRect(
borderRadius: BorderRadius.circular(compact ? 18 : 24), borderRadius: BorderRadius.circular(compact ? 18 : 24),
child: song.artwork != null child: song.artwork != null
@@ -330,14 +336,20 @@ class _PlayerPageState extends BaseState<PlayerPage> {
fit: BoxFit.cover, fit: BoxFit.cover,
gaplessPlayback: true, gaplessPlayback: true,
) )
: Container( : hasArtworkFile
color: const Color(0xFF2A3332), ? Image.file(
child: Icon( File(artworkPath),
Icons.music_note, fit: BoxFit.cover,
size: compact ? 48 : 72, gaplessPlayback: true,
color: Colors.white24, )
), : Container(
), color: const Color(0xFF2A3332),
child: Icon(
Icons.music_note,
size: compact ? 48 : 72,
color: Colors.white24,
),
),
); );
final padded = Padding( final padded = Padding(
+8
View File
@@ -32,12 +32,17 @@ class Song {
final String? url; final String? url;
final Uint8List? artwork; final Uint8List? artwork;
/// 本地封面文件路径(songs.artwork_path)。
/// 命中过元数据的歌直接用它显示封面,不必等元数据流程回来。
final String? artworkPath;
Song({ Song({
required this.id, required this.id,
required this.title, required this.title,
required this.artist, required this.artist,
this.url, this.url,
this.artwork, this.artwork,
this.artworkPath,
}); });
Map<String, dynamic> toJson() => { Map<String, dynamic> toJson() => {
@@ -299,6 +304,7 @@ class AudioService extends ChangeNotifier {
final path = dbSong['remote_path'] as String; final path = dbSong['remote_path'] as String;
songs.add(Song( songs.add(Song(
id: path, id: path,
artworkPath: dbSong['artwork_path'] as String?,
title: dbSong['title'] as String? ?? '', title: dbSong['title'] as String? ?? '',
artist: dbSong['artist'] as String? ?? '未知艺术家', artist: dbSong['artist'] as String? ?? '未知艺术家',
url: WebDAVService.instance.getFileUrl(path), url: WebDAVService.instance.getFileUrl(path),
@@ -855,6 +861,8 @@ class AudioService extends ChangeNotifier {
artist: metadata.artist.isNotEmpty ? metadata.artist : song.artist, artist: metadata.artist.isNotEmpty ? metadata.artist : song.artist,
url: song.url, url: song.url,
artwork: metadata.artwork, artwork: metadata.artwork,
// 保留封面文件路径:元数据流程回来前,播放页靠它立即显示封面
artworkPath: song.artworkPath,
); );
_queue[_currentIndex] = updatedSong; _queue[_currentIndex] = updatedSong;
_currentSong = updatedSong; _currentSong = updatedSong;
+16 -3
View File
@@ -1,3 +1,4 @@
import 'dart:io';
// lib/widgets/global_mini_player.dart // lib/widgets/global_mini_player.dart
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:provider/provider.dart'; import 'package:provider/provider.dart';
@@ -54,7 +55,6 @@ class GlobalMiniPlayer extends StatelessWidget {
borderRadius: BorderRadius.circular(12), borderRadius: BorderRadius.circular(12),
child: Row( child: Row(
children: [ children: [
const SizedBox(width: 12),
Container( Container(
width: 44, width: 44,
height: 44, height: 44,
@@ -66,9 +66,22 @@ class GlobalMiniPlayer extends StatelessWidget {
image: MemoryImage(song.artwork!), image: MemoryImage(song.artwork!),
fit: BoxFit.cover, 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( ? const Icon(
Icons.music_note, Icons.music_note,
color: Colors.white38, color: Colors.white38,