feat(ui): 主页收藏/歌单左右滑动切换
- 媒体库下方按钮由三个减为两个(本地音乐 / 最近播放),对称排列 - 收藏区改为双页:我的收藏 / 歌单列表,左右滑动切换 - 标题行随页变化(图标 + 文案),右侧加两点指示器(当前页长条) - 歌单项左侧改为歌单图标(不再是歌曲封面),显示名称与歌曲数 - 点击歌单进入详情页,返回后自动刷新 - 手势挂在 CustomScrollView 外层,纵向滚动不受影响 真机验证:左右滑动切换正常,指示器跟随,歌单列表显示 TL / 1 首。
This commit is contained in:
+251
-142
@@ -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(),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user