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
+72
View File
@@ -0,0 +1,72 @@
// lib/widgets/song_artwork.dart
import 'dart:io';
import 'dart:typed_data';
import 'package:flutter/material.dart';
/// 歌曲封面:优先进内存字节,其次本地封面文件,最后回退图标。
///
/// 列表页一次会构建几十行,这里用 `existsSync` 判断一次即可,
/// 图片本身由 Flutter 按需解码。
class SongArtwork extends StatelessWidget {
final Uint8List? bytes;
final String? filePath;
final double size;
final IconData fallbackIcon;
final Color? fallbackColor;
const SongArtwork({
super.key,
this.bytes,
this.filePath,
this.size = 44,
this.fallbackIcon = Icons.music_note,
this.fallbackColor,
});
@override
Widget build(BuildContext context) {
Widget child;
if (bytes != null && bytes!.isNotEmpty) {
child = Image.memory(
bytes!,
width: size,
height: size,
fit: BoxFit.cover,
gaplessPlayback: true,
errorBuilder: (_, __, ___) => _fallback(),
);
} else if (filePath != null &&
filePath!.isNotEmpty &&
File(filePath!).existsSync()) {
child = Image.file(
File(filePath!),
width: size,
height: size,
fit: BoxFit.cover,
gaplessPlayback: true,
errorBuilder: (_, __, ___) => _fallback(),
);
} else {
child = _fallback();
}
return ClipRRect(
borderRadius: BorderRadius.circular(6),
child: SizedBox(width: size, height: size, child: child),
);
}
Widget _fallback() {
return Container(
width: size,
height: size,
color: const Color(0xFF232928),
child: Icon(
fallbackIcon,
size: size * 0.55,
color: fallbackColor ?? const Color(0xFFB8D4D0),
),
);
}
}