feat(ui): 文件列表收藏红心 + 首页收藏绑定 favorites 表

交互调整(按讨论):
- WebDAV 文件列表:整行点击 = 播放(原来的行为),右侧播放按钮改成收藏红心
  已收藏实心红,未收藏空心;点击即写/删 favorites 表
- 首页"我的收藏"改为读 favorites 表 + songs 元数据;
  从文件列表返回时自动刷新
- 收藏项右侧红心改为真正删除(原来只是内存 removeAt)

修复:
- 收藏列表项透明度绑定 _listReveal,首屏为 0 导致条目完全透明
  (收藏成功但看不到)。收藏项改为固定 opacity 1.0。

验证(真机):
  列表点红心 -> favorites 表 +1 -> 返回首页 -> 收藏区出现该条目
This commit is contained in:
2026-10-24 21:38:36 +08:00
parent 4a8b1f28db
commit c1e720311e
2 changed files with 89 additions and 12 deletions
+40 -10
View File
@@ -5,6 +5,7 @@ import '../services/webdav_service.dart';
import '../widgets/magnetic_scroll_physics.dart';
import 'webdav_setup_page.dart';
import 'settings_page.dart';
import '../database/song_database.dart';
import 'webdav_file_list_page.dart';
import '../constants/ui_constants.dart';
import '../base/base_state.dart';
@@ -236,7 +237,7 @@ class _HomePageState extends BaseState<HomePage> {
safeSetState(() => _isLoading = true);
try {
await WebDAVService.instance.loadCredentials();
_favorites = [];
await _loadFavoritesFromDb();
} catch (e) {
_favorites = [];
} finally {
@@ -244,6 +245,34 @@ class _HomePageState extends BaseState<HomePage> {
}
}
/// 收藏列表直接绑定 favorites 表 + songs 元数据
Future<void> _loadFavoritesFromDb() async {
final db = SongDatabase();
final favRows = await db.getFavorites();
final paths = favRows
.map((row) => row['song_id'] as String?)
.whereType<String>()
.toList();
final meta = await db.getSongsByPaths(paths);
final items = <SongItem>[];
for (final path in paths) {
final row = meta[path];
final title = (row?['title'] as String?)?.trim() ?? '';
final artist = (row?['artist'] as String?)?.trim() ?? '';
final hasTitle = title.isNotEmpty;
items.add(SongItem(
path: path,
fileName: path.split('/').last,
title: hasTitle ? title : null,
artist: artist.isNotEmpty ? artist : null,
sourceTag: '收藏',
metadataState: hasTitle ? 'success' : 'unknown',
));
}
_favorites = items;
}
void _playSong(SongItem song) async {
try {
// 先校验登录,再开始播放。
@@ -416,6 +445,7 @@ class _HomePageState extends BaseState<HomePage> {
builder: (_) => const WebDAVFileListPage(),
),
);
await _loadFavoritesFromDb();
_resetSnapState();
safeSetState(() {});
} else {
@@ -674,12 +704,11 @@ class _HomePageState extends BaseState<HomePage> {
delegate: SliverChildBuilderDelegate(
(context, index) {
final song = _favorites[index];
final itemProgress =
(_listReveal * 2 - index / 5)
.clamp(0.0, 1.0);
// 收藏项不做滚动渐显:_listReveal 在首屏为 0,
// 会让条目完全透明(看起来"收藏了但不显示")。
return AnimatedOpacity(
opacity: itemProgress,
opacity: 1.0,
duration: const Duration(milliseconds: 150),
child: Padding(
padding: const EdgeInsets.symmetric(
@@ -713,13 +742,14 @@ class _HomePageState extends BaseState<HomePage> {
trailing: IconButton(
icon: const Icon(
Icons.favorite,
color: Color(0xFFB8D4D0),
color: Color(0xFFE05C5C),
size: 20,
),
onPressed: () {
safeSetState(() {
_favorites.removeAt(index);
});
onPressed: () async {
await SongDatabase()
.removeFavorite(song.path);
await _loadFavoritesFromDb();
if (mounted) safeSetState(() {});
},
),
onTap: () => _playSong(song),
+49 -2
View File
@@ -27,6 +27,9 @@ class _WebDAVFileListPageState extends BaseState<WebDAVFileListPage> {
Map<String, Map<String, dynamic>> _metaByPath = {};
String? _lastSyncedSongPath;
/// 已收藏的歌曲路径
Set<String> _favoritePaths = {};
// 安全解码
String _safeDecode(String input) {
try {
@@ -69,6 +72,30 @@ class _WebDAVFileListPageState extends BaseState<WebDAVFileListPage> {
});
}
/// 收藏 / 取消收藏:整行点击是播放,右侧红心专门做收藏
Future<void> _toggleFavorite(String path) async {
final wasFavorite = _favoritePaths.contains(path);
try {
await SongDatabase().toggleFavorite(path);
if (!mounted) return;
safeSetState(() {
final next = {..._favoritePaths};
if (wasFavorite) {
next.remove(path);
} else {
next.add(path);
}
_favoritePaths = next;
});
} catch (e) {
if (mounted) {
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(content: Text('收藏失败: $e')),
);
}
}
}
// ============================================================
// 加载目录
// ============================================================
@@ -94,10 +121,22 @@ class _WebDAVFileListPageState extends BaseState<WebDAVFileListPage> {
debugPrint('⚠️ [WebDAVList] 批量读取元数据失败: $e');
}
var favorites = <String>{};
try {
final favRows = await SongDatabase().getFavorites();
favorites = favRows
.map((row) => row['song_id'] as String?)
.whereType<String>()
.toSet();
} catch (e) {
debugPrint('⚠️ [WebDAVList] 读取收藏失败: $e');
}
if (mounted) {
safeSetState(() {
_items = items;
_metaByPath = meta;
_favoritePaths = favorites;
_isLoading = false;
});
// 如果当前正在播的歌就在这个目录里,补一次同步
@@ -382,8 +421,16 @@ class _WebDAVFileListPageState extends BaseState<WebDAVFileListPage> {
),
),
trailing: IconButton(
icon: const Icon(Icons.play_arrow, color: Color(0xFFB8D4D0)),
onPressed: () => _playSong(item),
tooltip: '收藏',
icon: Icon(
_favoritePaths.contains(item.path)
? Icons.favorite
: Icons.favorite_border,
color: _favoritePaths.contains(item.path)
? const Color(0xFFE05C5C)
: Colors.grey,
),
onPressed: () => _toggleFavorite(item.path),
),
onTap: () => _playSong(item),
);