feat(ui): 主页收藏/歌单左右滑动切换

- 媒体库下方按钮由三个减为两个(本地音乐 / 最近播放),对称排列
- 收藏区改为双页:我的收藏 / 歌单列表,左右滑动切换
- 标题行随页变化(图标 + 文案),右侧加两点指示器(当前页长条)
- 歌单项左侧改为歌单图标(不再是歌曲封面),显示名称与歌曲数
- 点击歌单进入详情页,返回后自动刷新
- 手势挂在 CustomScrollView 外层,纵向滚动不受影响

真机验证:左右滑动切换正常,指示器跟随,歌单列表显示 TL / 1 首。
This commit is contained in:
2026-10-31 23:55:02 +08:00
parent e89bd6381a
commit f793223311
+251 -142
View File
@@ -7,6 +7,9 @@ import 'webdav_setup_page.dart';
import 'settings_page.dart';
import 'playlists_page.dart';
import '../database/song_database.dart';
import '../models/playlist_summary.dart';
import '../repositories/playlist_repository.dart';
import 'playlist_detail_page.dart';
import '../widgets/song_artwork.dart';
import 'webdav_file_list_page.dart';
import '../constants/ui_constants.dart';
@@ -158,6 +161,10 @@ class _HomePageState extends BaseState<HomePage> {
List<SongItem> _favorites = [];
bool _isLoading = true;
/// 0 = 我的收藏,1 = 歌单列表(左右滑动切换)
int _homeTab = 0;
List<PlaylistSummary> _playlistSummaries = [];
// ========== 滚动控制 ==========
late final ScrollController _scrollController;
final GlobalKey _mediaLibraryKey = GlobalKey();
@@ -241,6 +248,7 @@ class _HomePageState extends BaseState<HomePage> {
try {
await WebDAVService.instance.loadCredentials();
await _loadFavoritesFromDb();
await _loadPlaylistSummaries();
} catch (e) {
_favorites = [];
} finally {
@@ -343,6 +351,223 @@ class _HomePageState extends BaseState<HomePage> {
}
}
Future<void> _loadPlaylistSummaries() async {
try {
final list = await PlaylistRepository().getSummaries();
if (!mounted) return;
setState(() => _playlistSummaries = list);
} catch (e) {
debugPrint('⚠️ [Home] 读取歌单失败: $e');
}
}
Widget _buildHomeTabIndicator() {
Widget seg(int index) {
final active = _homeTab == index;
return GestureDetector(
onTap: () => safeSetState(() => _homeTab = index),
behavior: HitTestBehavior.opaque,
child: Padding(
padding: const EdgeInsets.symmetric(vertical: 10, horizontal: 2),
child: AnimatedContainer(
duration: const Duration(milliseconds: 220),
curve: Curves.easeOutCubic,
width: active ? 26 : 7,
height: 7,
decoration: BoxDecoration(
color: active ? Colors.white : Colors.white24,
borderRadius: BorderRadius.circular(4),
),
),
),
);
}
return Row(
mainAxisSize: MainAxisSize.min,
children: [
seg(0),
const SizedBox(width: 6),
seg(1),
],
);
}
Widget _buildHomeSliver() {
if (_isLoading) {
return const SliverFillRemaining(
child: Center(
child: CircularProgressIndicator(color: Color(0xFFB8D4D0)),
),
);
}
return _homeTab == 0 ? _buildFavoritesSliver() : _buildPlaylistsSliver();
}
Widget _buildFavoritesSliver() {
if (_favorites.isEmpty) {
return SliverFillRemaining(
child: AnimatedOpacity(
opacity: 1.0 - _listReveal * 0.3,
duration: const Duration(milliseconds: 100),
child: Center(
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: [
Icon(Icons.favorite_border, size: 48, color: Colors.grey[600]),
const SizedBox(height: 16),
Text(
'还没有收藏歌曲\n在音乐库中点击 ♡ 添加',
textAlign: TextAlign.center,
style: TextStyle(
fontSize: 16,
color: Colors.grey[500],
height: 1.6,
),
),
],
),
),
),
);
}
return SliverList(
delegate: SliverChildBuilderDelegate(
(context, index) {
final song = _favorites[index];
return AnimatedOpacity(
opacity: 1.0,
duration: const Duration(milliseconds: 150),
child: Padding(
padding: const EdgeInsets.symmetric(vertical: 2),
child: ListTile(
contentPadding: EdgeInsets.zero,
visualDensity: VisualDensity.compact,
leading: SongArtwork(
filePath: song.artworkPath,
size: 44,
fallbackIcon: Icons.music_note,
),
title: Text(
song.displayTitle,
style: const TextStyle(
fontSize: 16,
fontWeight: FontWeight.w400,
color: Colors.white,
),
maxLines: 1,
overflow: TextOverflow.ellipsis,
),
subtitle: Text(
song.displaySubtitle,
style: TextStyle(fontSize: 13, color: Colors.grey[400]),
maxLines: 1,
overflow: TextOverflow.ellipsis,
),
trailing: IconButton(
icon: const Icon(
Icons.favorite,
color: Color(0xFFE05C5C),
size: 20,
),
onPressed: () async {
await SongDatabase().removeFavorite(song.path);
await _loadFavoritesFromDb();
if (mounted) safeSetState(() {});
},
),
onTap: () => _playFavorites(index),
),
),
);
},
childCount: _favorites.length,
),
);
}
Widget _buildPlaylistsSliver() {
if (_playlistSummaries.isEmpty) {
return SliverFillRemaining(
child: Center(
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: [
Icon(Icons.queue_music, size: 48, color: Colors.grey[600]),
const SizedBox(height: 16),
Text(
'还没有歌单\n在文件列表长按歌曲即可加入',
textAlign: TextAlign.center,
style: TextStyle(
fontSize: 16,
color: Colors.grey[500],
height: 1.6,
),
),
],
),
),
);
}
return SliverList(
delegate: SliverChildBuilderDelegate(
(context, index) {
final playlist = _playlistSummaries[index];
return Padding(
padding: const EdgeInsets.symmetric(vertical: 2),
child: ListTile(
contentPadding: EdgeInsets.zero,
visualDensity: VisualDensity.compact,
leading: Container(
width: 44,
height: 44,
decoration: BoxDecoration(
color: const Color(0xFF1E2A28),
borderRadius: BorderRadius.circular(10),
),
child: const Icon(
Icons.queue_music,
color: Color(0xFFB8D4D0),
size: 22,
),
),
title: Text(
playlist.name,
style: const TextStyle(
fontSize: 16,
fontWeight: FontWeight.w400,
color: Colors.white,
),
maxLines: 1,
overflow: TextOverflow.ellipsis,
),
subtitle: Text(
'${playlist.songCount} 首',
style: TextStyle(fontSize: 13, color: Colors.grey[400]),
),
onTap: () async {
await Navigator.push(
context,
MaterialPageRoute(
builder: (_) => PlaylistDetailPage(
playlistId: playlist.id,
playlistName: playlist.name,
),
),
);
await _loadPlaylistSummaries();
if (mounted) safeSetState(() {});
},
),
);
},
childCount: _playlistSummaries.length,
),
);
}
/// 收藏列表整队入队:点哪首就从哪首开始,队列是整个收藏列表
Future<void> _playFavorites(int index) async {
if (_favorites.isEmpty) return;
@@ -475,7 +700,16 @@ class _HomePageState extends BaseState<HomePage> {
),
),
Expanded(
child: CustomScrollView(
child: GestureDetector(
onHorizontalDragEnd: (details) {
final v = details.primaryVelocity ?? 0;
if (v < -200 && _homeTab == 0) {
safeSetState(() => _homeTab = 1);
} else if (v > 200 && _homeTab == 1) {
safeSetState(() => _homeTab = 0);
}
},
child: CustomScrollView(
controller: _scrollController,
physics: _mediaLibraryHeight > 0
? MagneticScrollPhysics(
@@ -662,42 +896,6 @@ class _HomePageState extends BaseState<HomePage> {
),
),
),
const SizedBox(width: 16),
Expanded(
child: _ClickableTile(
onTap: () async {
await Navigator.push(
context,
MaterialPageRoute(
builder: (_) => const PlaylistsPage(),
),
);
if (mounted) safeSetState(() {});
},
child: Padding(
padding: const EdgeInsets.symmetric(
vertical: 10),
child: Row(
mainAxisAlignment:
MainAxisAlignment.center,
children: [
Icon(Icons.playlist_play,
size: 24,
color: const Color(0xFFB8D4D0)),
const SizedBox(width: 8),
const Text(
'歌单列表',
style: TextStyle(
fontSize: 14,
fontWeight: FontWeight.w400,
color: Colors.white70,
),
),
],
),
),
),
),
],
),
const SizedBox(height: 32),
@@ -713,19 +911,26 @@ class _HomePageState extends BaseState<HomePage> {
height: 48,
color: const Color(0xFF0E1211),
padding: const EdgeInsets.symmetric(horizontal: 20),
child: const Row(
child: Row(
children: [
Icon(Icons.favorite,
color: Color(0xFFB8D4D0), size: 20),
SizedBox(width: 8),
Icon(
_homeTab == 0
? Icons.favorite
: Icons.queue_music,
color: const Color(0xFFB8D4D0),
size: 20,
),
const SizedBox(width: 8),
Text(
'我的收藏',
style: TextStyle(
_homeTab == 0 ? '我的收藏' : '歌单列表',
style: const TextStyle(
fontSize: 22,
fontWeight: FontWeight.w500,
color: Color(0xFFB8D4D0),
),
),
const Spacer(),
_buildHomeTabIndicator(),
],
),
),
@@ -737,110 +942,14 @@ class _HomePageState extends BaseState<HomePage> {
right: 20,
bottom: UIConstants.miniPlayerBottomSpace,
),
sliver: _isLoading
? const SliverFillRemaining(
child: Center(
child: CircularProgressIndicator(
color: Color(0xFFB8D4D0),
),
),
)
: _favorites.isEmpty
? SliverFillRemaining(
child: AnimatedOpacity(
opacity: 1.0 - _listReveal * 0.3,
duration: const Duration(milliseconds: 100),
child: Center(
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: [
Icon(
Icons.favorite_border,
size: 48,
color: Colors.grey[600],
),
const SizedBox(height: 16),
Text(
'还没有收藏歌曲\n在音乐库中点击 ♡ 添加',
textAlign: TextAlign.center,
style: TextStyle(
fontSize: 16,
color: Colors.grey[500],
height: 1.6,
),
),
],
),
),
),
)
: SliverList(
delegate: SliverChildBuilderDelegate(
(context, index) {
final song = _favorites[index];
// 收藏项不做滚动渐显:_listReveal 在首屏为 0,
// 会让条目完全透明(看起来"收藏了但不显示")。
return AnimatedOpacity(
opacity: 1.0,
duration: const Duration(milliseconds: 150),
child: Padding(
padding: const EdgeInsets.symmetric(
vertical: 2),
child: ListTile(
contentPadding: EdgeInsets.zero,
visualDensity: VisualDensity.compact,
leading: SongArtwork(
filePath: song.artworkPath,
size: 44,
fallbackIcon: Icons.music_note,
),
title: Text(
song.displayTitle,
style: const TextStyle(
fontSize: 16,
fontWeight: FontWeight.w400,
color: Colors.white,
),
maxLines: 1,
overflow: TextOverflow.ellipsis,
),
subtitle: Text(
song.displaySubtitle,
style: TextStyle(
fontSize: 13,
color: Colors.grey[400],
),
maxLines: 1,
overflow: TextOverflow.ellipsis,
),
trailing: IconButton(
icon: const Icon(
Icons.favorite,
color: Color(0xFFE05C5C),
size: 20,
),
onPressed: () async {
await SongDatabase()
.removeFavorite(song.path);
await _loadFavoritesFromDb();
if (mounted) safeSetState(() {});
},
),
onTap: () => _playFavorites(index),
),
),
);
},
childCount: _favorites.length,
),
),
sliver: _buildHomeSliver(),
),
],
),
),
),
],
),
);
}
}
}