feat(ui): 歌曲列表改用封面图,替代统一的小图标

- 新增 SongArtwork 组件:内存字节 > 本地封面文件 > 图标兜底,圆角裁剪
- WebDAV 文件列表:前导图标换成封面(来自 songs.artwork_path)
- 播放列表页:批量查封面路径(getSongsByPaths),正在播放的歌优先用内存里的封面字节;
  切歌后重新拉一次,保证新补全的封面能显示
- 没有封面的歌仍回退为音符/播放图标,不影响列表结构
This commit is contained in:
2026-10-24 20:47:12 +08:00
parent 0a1ebfa5df
commit d07086aaa0
3 changed files with 124 additions and 9 deletions
+47 -5
View File
@@ -2,6 +2,8 @@
import 'package:flutter/material.dart';
import 'package:provider/provider.dart';
import '../services/audio_service.dart';
import '../database/song_database.dart';
import '../widgets/song_artwork.dart';
class PlaylistPage extends StatefulWidget {
const PlaylistPage({super.key});
@@ -17,6 +19,9 @@ class _PlaylistPageState extends State<PlaylistPage> {
int _scrollGeneration = 0;
bool _hasScrolledToCurrent = false;
/// path -> 封面文件路径(来自 songs 表)
Map<String, String> _artworkByPath = {};
// ⭐ 固定 item 高度(根据实际 ListTile 测量结果调整)
// 实际约 72-76px,取 74px 作为稳定值
static const double _itemExtent = 74.0;
@@ -28,13 +33,46 @@ class _PlaylistPageState extends State<PlaylistPage> {
@override
void initState() {
super.initState();
_loadArtworkPaths();
AudioService().addListener(_onAudioChanged);
WidgetsBinding.instance.addPostFrameCallback((_) {
_scrollToCurrentSong();
});
}
String? _lastArtworkSongId;
/// 切歌后可能有新歌补全了封面,重新拉一次路径表
void _onAudioChanged() {
final song = AudioService().currentSong;
if (song == null || song.id == _lastArtworkSongId) return;
_lastArtworkSongId = song.id;
_loadArtworkPaths();
}
/// 批量取封面文件路径,供列表显示
Future<void> _loadArtworkPaths() async {
try {
final queue = AudioService().queue;
if (queue.isEmpty) return;
final rows =
await SongDatabase().getSongsByPaths(queue.map((s) => s.id).toList());
final map = <String, String>{};
rows.forEach((path, row) {
final artwork = row['artwork_path'] as String?;
if (artwork != null && artwork.isNotEmpty) map[path] = artwork;
});
if (mounted && map.isNotEmpty) {
setState(() => _artworkByPath = map);
}
} catch (_) {
// 封面只是装饰,失败不影响列表
}
}
@override
void dispose() {
AudioService().removeListener(_onAudioChanged);
_scrollController.dispose();
super.dispose();
}
@@ -271,11 +309,15 @@ class _PlaylistPageState extends State<PlaylistPage> {
horizontal: 8,
vertical: 4,
),
leading: Icon(
isCurrent ? Icons.play_arrow : Icons.music_note,
color:
isCurrent ? const Color(0xFFB8D4D0) : Colors.grey[600],
size: 24,
leading: SongArtwork(
bytes: song.artwork,
filePath: _artworkByPath[song.id],
size: 48,
fallbackIcon:
isCurrent ? Icons.play_arrow : Icons.music_note,
fallbackColor: isCurrent
? const Color(0xFFB8D4D0)
: Colors.grey[600],
),
title: Text(
song.title,