feat(ui): 文件列表收藏红心 + 首页收藏绑定 favorites 表
交互调整(按讨论): - WebDAV 文件列表:整行点击 = 播放(原来的行为),右侧播放按钮改成收藏红心 已收藏实心红,未收藏空心;点击即写/删 favorites 表 - 首页"我的收藏"改为读 favorites 表 + songs 元数据; 从文件列表返回时自动刷新 - 收藏项右侧红心改为真正删除(原来只是内存 removeAt) 修复: - 收藏列表项透明度绑定 _listReveal,首屏为 0 导致条目完全透明 (收藏成功但看不到)。收藏项改为固定 opacity 1.0。 验证(真机): 列表点红心 -> favorites 表 +1 -> 返回首页 -> 收藏区出现该条目
This commit is contained in:
+40
-10
@@ -5,6 +5,7 @@ import '../services/webdav_service.dart';
|
|||||||
import '../widgets/magnetic_scroll_physics.dart';
|
import '../widgets/magnetic_scroll_physics.dart';
|
||||||
import 'webdav_setup_page.dart';
|
import 'webdav_setup_page.dart';
|
||||||
import 'settings_page.dart';
|
import 'settings_page.dart';
|
||||||
|
import '../database/song_database.dart';
|
||||||
import 'webdav_file_list_page.dart';
|
import 'webdav_file_list_page.dart';
|
||||||
import '../constants/ui_constants.dart';
|
import '../constants/ui_constants.dart';
|
||||||
import '../base/base_state.dart';
|
import '../base/base_state.dart';
|
||||||
@@ -236,7 +237,7 @@ class _HomePageState extends BaseState<HomePage> {
|
|||||||
safeSetState(() => _isLoading = true);
|
safeSetState(() => _isLoading = true);
|
||||||
try {
|
try {
|
||||||
await WebDAVService.instance.loadCredentials();
|
await WebDAVService.instance.loadCredentials();
|
||||||
_favorites = [];
|
await _loadFavoritesFromDb();
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
_favorites = [];
|
_favorites = [];
|
||||||
} finally {
|
} 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 {
|
void _playSong(SongItem song) async {
|
||||||
try {
|
try {
|
||||||
// 先校验登录,再开始播放。
|
// 先校验登录,再开始播放。
|
||||||
@@ -416,6 +445,7 @@ class _HomePageState extends BaseState<HomePage> {
|
|||||||
builder: (_) => const WebDAVFileListPage(),
|
builder: (_) => const WebDAVFileListPage(),
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
|
await _loadFavoritesFromDb();
|
||||||
_resetSnapState();
|
_resetSnapState();
|
||||||
safeSetState(() {});
|
safeSetState(() {});
|
||||||
} else {
|
} else {
|
||||||
@@ -674,12 +704,11 @@ class _HomePageState extends BaseState<HomePage> {
|
|||||||
delegate: SliverChildBuilderDelegate(
|
delegate: SliverChildBuilderDelegate(
|
||||||
(context, index) {
|
(context, index) {
|
||||||
final song = _favorites[index];
|
final song = _favorites[index];
|
||||||
final itemProgress =
|
|
||||||
(_listReveal * 2 - index / 5)
|
|
||||||
.clamp(0.0, 1.0);
|
|
||||||
|
|
||||||
|
// 收藏项不做滚动渐显:_listReveal 在首屏为 0,
|
||||||
|
// 会让条目完全透明(看起来"收藏了但不显示")。
|
||||||
return AnimatedOpacity(
|
return AnimatedOpacity(
|
||||||
opacity: itemProgress,
|
opacity: 1.0,
|
||||||
duration: const Duration(milliseconds: 150),
|
duration: const Duration(milliseconds: 150),
|
||||||
child: Padding(
|
child: Padding(
|
||||||
padding: const EdgeInsets.symmetric(
|
padding: const EdgeInsets.symmetric(
|
||||||
@@ -713,13 +742,14 @@ class _HomePageState extends BaseState<HomePage> {
|
|||||||
trailing: IconButton(
|
trailing: IconButton(
|
||||||
icon: const Icon(
|
icon: const Icon(
|
||||||
Icons.favorite,
|
Icons.favorite,
|
||||||
color: Color(0xFFB8D4D0),
|
color: Color(0xFFE05C5C),
|
||||||
size: 20,
|
size: 20,
|
||||||
),
|
),
|
||||||
onPressed: () {
|
onPressed: () async {
|
||||||
safeSetState(() {
|
await SongDatabase()
|
||||||
_favorites.removeAt(index);
|
.removeFavorite(song.path);
|
||||||
});
|
await _loadFavoritesFromDb();
|
||||||
|
if (mounted) safeSetState(() {});
|
||||||
},
|
},
|
||||||
),
|
),
|
||||||
onTap: () => _playSong(song),
|
onTap: () => _playSong(song),
|
||||||
|
|||||||
@@ -27,6 +27,9 @@ class _WebDAVFileListPageState extends BaseState<WebDAVFileListPage> {
|
|||||||
Map<String, Map<String, dynamic>> _metaByPath = {};
|
Map<String, Map<String, dynamic>> _metaByPath = {};
|
||||||
String? _lastSyncedSongPath;
|
String? _lastSyncedSongPath;
|
||||||
|
|
||||||
|
/// 已收藏的歌曲路径
|
||||||
|
Set<String> _favoritePaths = {};
|
||||||
|
|
||||||
// 安全解码
|
// 安全解码
|
||||||
String _safeDecode(String input) {
|
String _safeDecode(String input) {
|
||||||
try {
|
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');
|
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) {
|
if (mounted) {
|
||||||
safeSetState(() {
|
safeSetState(() {
|
||||||
_items = items;
|
_items = items;
|
||||||
_metaByPath = meta;
|
_metaByPath = meta;
|
||||||
|
_favoritePaths = favorites;
|
||||||
_isLoading = false;
|
_isLoading = false;
|
||||||
});
|
});
|
||||||
// 如果当前正在播的歌就在这个目录里,补一次同步
|
// 如果当前正在播的歌就在这个目录里,补一次同步
|
||||||
@@ -382,8 +421,16 @@ class _WebDAVFileListPageState extends BaseState<WebDAVFileListPage> {
|
|||||||
),
|
),
|
||||||
),
|
),
|
||||||
trailing: IconButton(
|
trailing: IconButton(
|
||||||
icon: const Icon(Icons.play_arrow, color: Color(0xFFB8D4D0)),
|
tooltip: '收藏',
|
||||||
onPressed: () => _playSong(item),
|
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),
|
onTap: () => _playSong(item),
|
||||||
);
|
);
|
||||||
|
|||||||
Reference in New Issue
Block a user