|
|
|
@ -1,7 +1,9 @@ |
|
|
|
import 'package:flutter/material.dart'; |
|
|
|
import 'package:flutter_screenutil/flutter_screenutil.dart'; |
|
|
|
import 'package:get/get.dart'; |
|
|
|
import '../../agent/controllers/agent_controller.dart'; |
|
|
|
import '../../agent/views/circular_progress_button.dart'; |
|
|
|
import '../../../core/utils/logger.dart'; |
|
|
|
|
|
|
|
class MusicPlaylistView extends StatelessWidget { |
|
|
|
const MusicPlaylistView({Key? key}) : super(key: key); |
|
|
|
@ -18,7 +20,6 @@ class MusicPlaylistView extends StatelessWidget { |
|
|
|
icon: const Icon(Icons.arrow_back_ios, size: 20), // iOS风格箭头 |
|
|
|
onPressed: () => Navigator.pop(context), |
|
|
|
), |
|
|
|
//title: const Text('音乐播放列表'), |
|
|
|
backgroundColor: isDarkMode ? Colors.grey[900] : Colors.white, |
|
|
|
foregroundColor: isDarkMode ? Colors.white : Colors.black, |
|
|
|
elevation: 0, |
|
|
|
@ -45,32 +46,177 @@ class MusicPlaylistView extends StatelessWidget { |
|
|
|
], |
|
|
|
), |
|
|
|
) |
|
|
|
: ListView.builder( |
|
|
|
padding: const EdgeInsets.all(16), |
|
|
|
itemCount: playlist.length, |
|
|
|
itemBuilder: (context, index) { |
|
|
|
final item = playlist[index]; |
|
|
|
final id = item["id"]?.toString() ?? ""; |
|
|
|
final name = item["name"] ?? "未知歌曲"; |
|
|
|
final url = item["url"] ?? ""; |
|
|
|
final singer = item["singer"] ?? item["sgener"] ?? "未知歌手"; |
|
|
|
final image = item["image"] ?? ""; |
|
|
|
|
|
|
|
return _buildMusicItem( |
|
|
|
context, |
|
|
|
controller, |
|
|
|
id, |
|
|
|
name, |
|
|
|
url, |
|
|
|
singer, |
|
|
|
image, |
|
|
|
isDarkMode, |
|
|
|
); |
|
|
|
}, |
|
|
|
: Column( |
|
|
|
children: [ |
|
|
|
// 上部:当前播放音乐详情 - 进一步优化高度占比 |
|
|
|
SizedBox( |
|
|
|
height: MediaQuery.of(context).size.height * |
|
|
|
0.35, // 从35%减少到32%,为进度条优化腾出空间 |
|
|
|
child: _buildCurrentPlayingSection( |
|
|
|
controller, playlist, isDarkMode), |
|
|
|
), |
|
|
|
// 中部:播放全部按钮 |
|
|
|
_buildPlayAllSection(controller, playlist, isDarkMode), |
|
|
|
// 下部:歌单列表 - 使用剩余空间 |
|
|
|
Expanded( |
|
|
|
child: |
|
|
|
_buildPlaylistSection(controller, playlist, isDarkMode), |
|
|
|
), |
|
|
|
], |
|
|
|
), |
|
|
|
); |
|
|
|
} |
|
|
|
|
|
|
|
// 构建当前播放音乐详情部分 |
|
|
|
Widget _buildCurrentPlayingSection(AgentController controller, |
|
|
|
List<Map<String, dynamic>> playlist, bool isDarkMode) { |
|
|
|
return Obx(() { |
|
|
|
// 查找当前播放的歌曲 - 修复自动切换时不更新的问题 |
|
|
|
Map<String, dynamic>? currentSong; |
|
|
|
if (controller.currentId.value != 0) { |
|
|
|
try { |
|
|
|
currentSong = playlist.firstWhere( |
|
|
|
(song) => |
|
|
|
int.tryParse(song['id']?.toString() ?? '0') == |
|
|
|
controller.currentId.value, |
|
|
|
); |
|
|
|
} catch (e) { |
|
|
|
// 如果没找到,使用第一首歌曲 |
|
|
|
currentSong = playlist.isNotEmpty ? playlist.first : null; |
|
|
|
} |
|
|
|
} else { |
|
|
|
currentSong = playlist.isNotEmpty ? playlist.first : null; |
|
|
|
} |
|
|
|
|
|
|
|
if (currentSong == null) { |
|
|
|
return Container( |
|
|
|
padding: const EdgeInsets.all(16), // 减少padding |
|
|
|
child: Center( |
|
|
|
child: Column( |
|
|
|
mainAxisAlignment: MainAxisAlignment.center, |
|
|
|
children: [ |
|
|
|
Icon( |
|
|
|
Icons.music_note, |
|
|
|
size: 60, // 减小图标尺寸 |
|
|
|
color: Colors.grey[400], |
|
|
|
), |
|
|
|
const SizedBox(height: 12), // 减少间距 |
|
|
|
Text( |
|
|
|
'暂无播放音乐', |
|
|
|
style: TextStyle( |
|
|
|
fontSize: 16, // 减小字体 |
|
|
|
color: isDarkMode ? Colors.grey[400] : Colors.grey[600], |
|
|
|
), |
|
|
|
), |
|
|
|
], |
|
|
|
), |
|
|
|
), |
|
|
|
); |
|
|
|
} |
|
|
|
|
|
|
|
final title = currentSong['name'] ?? currentSong['title'] ?? '未知歌曲'; |
|
|
|
final artist = currentSong['singer'] ?? |
|
|
|
currentSong['sgener'] ?? |
|
|
|
currentSong['artist'] ?? |
|
|
|
'未知歌手'; |
|
|
|
final coverUrl = currentSong['image'] ?? currentSong['coverUrl'] ?? ''; |
|
|
|
|
|
|
|
return SingleChildScrollView( |
|
|
|
// 添加滚动支持 |
|
|
|
child: Container( |
|
|
|
padding: const EdgeInsets.symmetric( |
|
|
|
horizontal: 20, vertical: 12), // 优化padding:水平增加,垂直减少 |
|
|
|
child: Column( |
|
|
|
mainAxisAlignment: MainAxisAlignment.center, |
|
|
|
children: [ |
|
|
|
// 封面图片 - 进一步优化尺寸 |
|
|
|
_buildCoverImage(coverUrl, isDarkMode, |
|
|
|
size: 110), // 从120减小到110,为进度条优化腾出空间 |
|
|
|
const SizedBox(height: 12), // 减少间距从16到14 |
|
|
|
// 歌曲信息 |
|
|
|
_buildSongInfo(title, artist, isDarkMode), |
|
|
|
const SizedBox(height: 16), // 增加歌曲信息和进度条之间的间距,提升视觉层次 |
|
|
|
// 进度条 - 现在是水平布局,需要更多关注 |
|
|
|
_buildProgressBar(controller, isDarkMode), |
|
|
|
const SizedBox(height: 15), // 增加进度条和控制按钮之间的间距 |
|
|
|
// 播放控制按钮 |
|
|
|
_buildPlayControls(controller, playlist, isDarkMode), |
|
|
|
], |
|
|
|
), |
|
|
|
), |
|
|
|
); |
|
|
|
}); |
|
|
|
} |
|
|
|
|
|
|
|
// 构建播放全部按钮部分 |
|
|
|
Widget _buildPlayAllSection(AgentController controller, |
|
|
|
List<Map<String, dynamic>> playlist, bool isDarkMode) { |
|
|
|
return Container( |
|
|
|
padding: const EdgeInsets.symmetric( |
|
|
|
horizontal: 20, vertical: 12), // 优化padding,与音乐详情区域保持一致 |
|
|
|
child: SizedBox( |
|
|
|
width: double.infinity, |
|
|
|
height: 48, // 从50减小到48,更紧凑 |
|
|
|
child: ElevatedButton.icon( |
|
|
|
onPressed: () async { |
|
|
|
if (playlist.isNotEmpty) { |
|
|
|
await controller.playPlaylist(playlist); |
|
|
|
} |
|
|
|
}, |
|
|
|
icon: const Icon(Icons.play_arrow, |
|
|
|
color: Colors.white, size: 20), // 调整图标大小 |
|
|
|
label: const Text( |
|
|
|
'播放全部', |
|
|
|
style: TextStyle( |
|
|
|
fontSize: 15, // 从16减小到15 |
|
|
|
fontWeight: FontWeight.w600, |
|
|
|
color: Colors.white, |
|
|
|
), |
|
|
|
), |
|
|
|
style: ElevatedButton.styleFrom( |
|
|
|
backgroundColor: Theme.of(Get.context!).primaryColor, |
|
|
|
shape: RoundedRectangleBorder( |
|
|
|
borderRadius: BorderRadius.circular(14), // 从25减小到14,更协调 |
|
|
|
), |
|
|
|
elevation: 3, // 从2增加到3,增强视觉层次 |
|
|
|
shadowColor: Theme.of(Get.context!) |
|
|
|
.primaryColor |
|
|
|
.withValues(alpha: 0.4), // 添加阴影颜色 |
|
|
|
), |
|
|
|
), |
|
|
|
), |
|
|
|
); |
|
|
|
} |
|
|
|
|
|
|
|
// 构建歌单列表部分 |
|
|
|
Widget _buildPlaylistSection(AgentController controller, |
|
|
|
List<Map<String, dynamic>> playlist, bool isDarkMode) { |
|
|
|
return ListView.builder( |
|
|
|
padding: const EdgeInsets.symmetric( |
|
|
|
horizontal: 20, vertical: 8), // 优化padding,与其他区域保持一致 |
|
|
|
itemCount: playlist.length, |
|
|
|
itemBuilder: (context, index) { |
|
|
|
final item = playlist[index]; |
|
|
|
final id = item["id"]?.toString() ?? ""; |
|
|
|
final name = item["name"] ?? "未知歌曲"; |
|
|
|
final url = item["url"] ?? ""; |
|
|
|
final singer = item["singer"] ?? item["sgener"] ?? "未知歌手"; |
|
|
|
final image = item["image"] ?? ""; |
|
|
|
|
|
|
|
return _buildMusicItem( |
|
|
|
context, |
|
|
|
controller, |
|
|
|
id, |
|
|
|
name, |
|
|
|
url, |
|
|
|
singer, |
|
|
|
image, |
|
|
|
isDarkMode, |
|
|
|
); |
|
|
|
}, |
|
|
|
); |
|
|
|
} |
|
|
|
|
|
|
|
Widget _buildMusicItem( |
|
|
|
BuildContext context, |
|
|
|
AgentController controller, |
|
|
|
@ -89,123 +235,399 @@ class MusicPlaylistView extends StatelessWidget { |
|
|
|
'coverUrl': image, |
|
|
|
}; |
|
|
|
|
|
|
|
return GestureDetector( |
|
|
|
onTap: () { |
|
|
|
// 点击音乐卡片直接播放对应歌曲 |
|
|
|
_playSelectedSong(controller, song, Get.arguments ?? []); |
|
|
|
}, |
|
|
|
child: Container( |
|
|
|
margin: const EdgeInsets.only(bottom: 10), // 从12减小到10,更紧凑 |
|
|
|
padding: const EdgeInsets.all(14), // 从12增加到14,增加内部空间 |
|
|
|
decoration: BoxDecoration( |
|
|
|
color: isDarkMode ? Colors.grey[800] : Colors.white, |
|
|
|
borderRadius: BorderRadius.circular(14), // 从12增加到14,更现代的圆角 |
|
|
|
boxShadow: [ |
|
|
|
BoxShadow( |
|
|
|
color: Colors.black |
|
|
|
.withValues(alpha: isDarkMode ? 0.25 : 0.05), // 增强阴影效果 |
|
|
|
blurRadius: 4, // 从3增加到4 |
|
|
|
spreadRadius: 0, // 从1减小到0,更自然的阴影 |
|
|
|
offset: const Offset(0, 2), // 从(0,1)调整到(0,2),更明显的深度感 |
|
|
|
), |
|
|
|
], |
|
|
|
), |
|
|
|
child: Row( |
|
|
|
children: [ |
|
|
|
// 封面图片 - 优化设计 |
|
|
|
Container( |
|
|
|
width: 52, // 从50增加到52,稍微大一点 |
|
|
|
height: 52, |
|
|
|
decoration: BoxDecoration( |
|
|
|
borderRadius: BorderRadius.circular(10), // 从8增加到10,更现代的圆角 |
|
|
|
color: isDarkMode |
|
|
|
? Colors.grey[700] |
|
|
|
: Colors.grey[200], // 根据主题调整背景色 |
|
|
|
boxShadow: [ |
|
|
|
BoxShadow( |
|
|
|
color: |
|
|
|
Colors.black.withValues(alpha: isDarkMode ? 0.2 : 0.08), |
|
|
|
blurRadius: 2, |
|
|
|
offset: const Offset(0, 1), |
|
|
|
), |
|
|
|
], |
|
|
|
), |
|
|
|
child: image.isNotEmpty |
|
|
|
? ClipRRect( |
|
|
|
borderRadius: BorderRadius.circular(10), |
|
|
|
child: Image.network( |
|
|
|
image, |
|
|
|
fit: BoxFit.cover, |
|
|
|
errorBuilder: (context, error, stackTrace) { |
|
|
|
return Icon( |
|
|
|
Icons.music_note, |
|
|
|
color: isDarkMode |
|
|
|
? Colors.grey[500] |
|
|
|
: Colors.grey[600], |
|
|
|
size: 24, // 指定图标大小 |
|
|
|
); |
|
|
|
}, |
|
|
|
), |
|
|
|
) |
|
|
|
: Icon( |
|
|
|
Icons.music_note, |
|
|
|
color: isDarkMode ? Colors.grey[500] : Colors.grey[600], |
|
|
|
size: 24, // 指定图标大小 |
|
|
|
), |
|
|
|
), |
|
|
|
const SizedBox(width: 14), // 从12增加到14,增加间距 |
|
|
|
// 歌曲信息 - 优化文本样式 |
|
|
|
Expanded( |
|
|
|
child: Column( |
|
|
|
crossAxisAlignment: CrossAxisAlignment.start, |
|
|
|
mainAxisAlignment: MainAxisAlignment.center, // 垂直居中对齐 |
|
|
|
children: [ |
|
|
|
Text( |
|
|
|
name, |
|
|
|
style: TextStyle( |
|
|
|
fontSize: 15.5, // 从16减小到15.5,更协调 |
|
|
|
fontWeight: FontWeight.w600, // 从w500增加到w600,更突出 |
|
|
|
color: isDarkMode ? Colors.white : Colors.black87, |
|
|
|
height: 1.2, // 添加行高控制 |
|
|
|
), |
|
|
|
maxLines: 1, |
|
|
|
overflow: TextOverflow.ellipsis, |
|
|
|
), |
|
|
|
const SizedBox(height: 3), // 从4减小到3,更紧凑 |
|
|
|
Text( |
|
|
|
singer, |
|
|
|
style: TextStyle( |
|
|
|
fontSize: 13, // 从14减小到13 |
|
|
|
color: isDarkMode ? Colors.grey[400] : Colors.grey[600], |
|
|
|
height: 1.1, // 添加行高控制 |
|
|
|
), |
|
|
|
maxLines: 1, |
|
|
|
overflow: TextOverflow.ellipsis, |
|
|
|
), |
|
|
|
], |
|
|
|
), |
|
|
|
), |
|
|
|
const SizedBox(width: 14), // 从12增加到14,与封面图片间距保持一致 |
|
|
|
// 播放按钮 |
|
|
|
Obx(() { |
|
|
|
// 修复类型比较问题:将id转换为int进行比较 |
|
|
|
final currentIdInt = int.tryParse(id) ?? 0; |
|
|
|
final isThisPlaying = controller.isPlaying.value && |
|
|
|
controller.currentId.value == currentIdInt; |
|
|
|
|
|
|
|
// 只有当前播放的歌曲才显示进度 |
|
|
|
final currentProgress = |
|
|
|
isThisPlaying ? controller.progress.value : 0.0; |
|
|
|
|
|
|
|
return CircularProgressButton( |
|
|
|
isPlaying: isThisPlaying, |
|
|
|
progress: currentProgress, |
|
|
|
primaryColor: Theme.of(context).primaryColor, |
|
|
|
backgroundColor: Colors.grey, |
|
|
|
size: 40.0, |
|
|
|
strokeWidth: 2.5, |
|
|
|
onPlayPause: () async { |
|
|
|
try { |
|
|
|
Logger.d('MusicPlaylistView', |
|
|
|
'播放按钮被点击,当前播放状态: ${controller.isPlaying.value}'); |
|
|
|
Logger.d('MusicPlaylistView', |
|
|
|
'当前歌曲ID: ${controller.currentId.value}, 点击歌曲ID: $currentIdInt'); |
|
|
|
|
|
|
|
if (controller.isPlaying.value) { |
|
|
|
if (controller.currentId.value == currentIdInt) { |
|
|
|
Logger.d('MusicPlaylistView', '暂停当前播放的歌曲'); |
|
|
|
await controller.musicPause(); |
|
|
|
} else { |
|
|
|
Logger.d('MusicPlaylistView', '播放新歌曲'); |
|
|
|
await controller.musicPlay(song); |
|
|
|
} |
|
|
|
} else { |
|
|
|
Logger.d('MusicPlaylistView', '开始播放歌曲'); |
|
|
|
await controller.musicPlay(song); |
|
|
|
} |
|
|
|
} catch (e) { |
|
|
|
Logger.e('MusicPlaylistView', '播放音乐时发生错误: $e'); |
|
|
|
} |
|
|
|
}, |
|
|
|
onSeek: isThisPlaying |
|
|
|
? (seekProgress) { |
|
|
|
// 只有当前播放的歌曲才允许跳转 |
|
|
|
final targetPosition = |
|
|
|
(controller.duration.value * seekProgress).round(); |
|
|
|
controller.seekToPosition(targetPosition); |
|
|
|
} |
|
|
|
: null, |
|
|
|
); |
|
|
|
}), |
|
|
|
], |
|
|
|
), |
|
|
|
), |
|
|
|
); |
|
|
|
} |
|
|
|
|
|
|
|
// 构建封面图片 |
|
|
|
Widget _buildCoverImage(String coverUrl, bool isDarkMode, |
|
|
|
{double size = 200}) { |
|
|
|
return Container( |
|
|
|
margin: const EdgeInsets.only(bottom: 12), |
|
|
|
padding: const EdgeInsets.all(12), |
|
|
|
width: size, |
|
|
|
height: size, |
|
|
|
decoration: BoxDecoration( |
|
|
|
color: isDarkMode ? Colors.grey[800] : Colors.white, |
|
|
|
borderRadius: BorderRadius.circular(12), |
|
|
|
borderRadius: BorderRadius.circular(16), |
|
|
|
boxShadow: [ |
|
|
|
BoxShadow( |
|
|
|
color: Colors.black.withValues(alpha: isDarkMode ? 0.2 : 0.03), |
|
|
|
blurRadius: 3, |
|
|
|
spreadRadius: 1, |
|
|
|
offset: const Offset(0, 1), |
|
|
|
color: Colors.black.withValues(alpha: isDarkMode ? 0.3 : 0.1), |
|
|
|
blurRadius: 15, |
|
|
|
spreadRadius: 2, |
|
|
|
offset: const Offset(0, 6), |
|
|
|
), |
|
|
|
], |
|
|
|
), |
|
|
|
child: Row( |
|
|
|
child: ClipRRect( |
|
|
|
borderRadius: BorderRadius.circular(16), |
|
|
|
child: coverUrl.isNotEmpty |
|
|
|
? Image.network( |
|
|
|
coverUrl, |
|
|
|
fit: BoxFit.cover, |
|
|
|
errorBuilder: (context, error, stackTrace) { |
|
|
|
return _buildDefaultCover(isDarkMode); |
|
|
|
}, |
|
|
|
) |
|
|
|
: _buildDefaultCover(isDarkMode), |
|
|
|
), |
|
|
|
); |
|
|
|
} |
|
|
|
|
|
|
|
// 构建默认封面 |
|
|
|
Widget _buildDefaultCover(bool isDarkMode) { |
|
|
|
return Container( |
|
|
|
color: isDarkMode ? Colors.grey[800] : Colors.grey[300], |
|
|
|
child: Icon( |
|
|
|
Icons.music_note, |
|
|
|
size: 60, // 从20改为60,更合适的大小 |
|
|
|
color: isDarkMode ? Colors.grey[600] : Colors.grey[500], |
|
|
|
), |
|
|
|
); |
|
|
|
} |
|
|
|
|
|
|
|
// 构建歌曲信息 |
|
|
|
Widget _buildSongInfo(String title, String artist, bool isDarkMode) { |
|
|
|
return Column( |
|
|
|
children: [ |
|
|
|
Text( |
|
|
|
title, |
|
|
|
style: TextStyle( |
|
|
|
fontSize: 18, // 从20减小到18 |
|
|
|
fontWeight: FontWeight.bold, |
|
|
|
color: isDarkMode ? Colors.white : Colors.black87, |
|
|
|
), |
|
|
|
textAlign: TextAlign.center, |
|
|
|
maxLines: 2, |
|
|
|
overflow: TextOverflow.ellipsis, |
|
|
|
), |
|
|
|
const SizedBox(height: 6), // 从8减小到6 |
|
|
|
Text( |
|
|
|
artist, |
|
|
|
style: TextStyle( |
|
|
|
fontSize: 14, // 从16减小到14 |
|
|
|
color: isDarkMode ? Colors.grey[400] : Colors.grey[600], |
|
|
|
), |
|
|
|
textAlign: TextAlign.center, |
|
|
|
maxLines: 1, |
|
|
|
overflow: TextOverflow.ellipsis, |
|
|
|
), |
|
|
|
], |
|
|
|
); |
|
|
|
} |
|
|
|
|
|
|
|
// 构建进度条 - 优化布局:将进度条和时间显示合并到同一水平线 |
|
|
|
Widget _buildProgressBar(AgentController controller, bool isDarkMode) { |
|
|
|
return Obx(() { |
|
|
|
return Row( |
|
|
|
children: [ |
|
|
|
// 封面图片 |
|
|
|
Container( |
|
|
|
width: 50, |
|
|
|
height: 50, |
|
|
|
decoration: BoxDecoration( |
|
|
|
borderRadius: BorderRadius.circular(8), |
|
|
|
color: Colors.grey[300], |
|
|
|
// 左侧:当前播放时间 |
|
|
|
SizedBox( |
|
|
|
width: 35.w, // 固定宽度确保对齐,足够显示 "99:59" 格式 |
|
|
|
|
|
|
|
child: Text( |
|
|
|
_formatDuration(controller.currentPosition.value), |
|
|
|
style: TextStyle( |
|
|
|
fontSize: 12, |
|
|
|
color: isDarkMode ? Colors.grey[400] : Colors.grey[600], |
|
|
|
fontFeatures: const [FontFeature.tabularFigures()], // 使用等宽数字 |
|
|
|
), |
|
|
|
textAlign: TextAlign.left, |
|
|
|
), |
|
|
|
child: image.isNotEmpty |
|
|
|
? ClipRRect( |
|
|
|
borderRadius: BorderRadius.circular(8), |
|
|
|
child: Image.network( |
|
|
|
image, |
|
|
|
fit: BoxFit.cover, |
|
|
|
errorBuilder: (context, error, stackTrace) { |
|
|
|
return Icon( |
|
|
|
Icons.music_note, |
|
|
|
color: Colors.grey[600], |
|
|
|
); |
|
|
|
}, |
|
|
|
), |
|
|
|
) |
|
|
|
: Icon( |
|
|
|
Icons.music_note, |
|
|
|
color: Colors.grey[600], |
|
|
|
), |
|
|
|
), |
|
|
|
const SizedBox(width: 12), |
|
|
|
// 歌曲信息 |
|
|
|
SizedBox(width: 6.w), // 左侧时间和进度条之间的间距 |
|
|
|
// 中间:可交互的进度条 |
|
|
|
Expanded( |
|
|
|
child: Column( |
|
|
|
crossAxisAlignment: CrossAxisAlignment.start, |
|
|
|
children: [ |
|
|
|
Text( |
|
|
|
name, |
|
|
|
style: TextStyle( |
|
|
|
fontSize: 16, |
|
|
|
fontWeight: FontWeight.w500, |
|
|
|
color: isDarkMode ? Colors.white : Colors.black87, |
|
|
|
), |
|
|
|
maxLines: 1, |
|
|
|
overflow: TextOverflow.ellipsis, |
|
|
|
), |
|
|
|
const SizedBox(height: 4), |
|
|
|
Text( |
|
|
|
singer, |
|
|
|
style: TextStyle( |
|
|
|
fontSize: 14, |
|
|
|
color: isDarkMode ? Colors.grey[400] : Colors.grey[600], |
|
|
|
), |
|
|
|
maxLines: 1, |
|
|
|
overflow: TextOverflow.ellipsis, |
|
|
|
child: SliderTheme( |
|
|
|
data: SliderTheme.of(Get.context!).copyWith( |
|
|
|
activeTrackColor: Theme.of(Get.context!).primaryColor, |
|
|
|
inactiveTrackColor: |
|
|
|
isDarkMode ? Colors.grey[700] : Colors.grey[300], |
|
|
|
thumbColor: Theme.of(Get.context!).primaryColor, |
|
|
|
overlayColor: |
|
|
|
Theme.of(Get.context!).primaryColor.withValues(alpha: 0.2), |
|
|
|
trackHeight: 4.h, |
|
|
|
thumbShape: const RoundSliderThumbShape(enabledThumbRadius: 0), |
|
|
|
overlayShape: const RoundSliderOverlayShape(overlayRadius: 0), |
|
|
|
), |
|
|
|
child: Slider( |
|
|
|
value: controller.progress.value.clamp(0.0, 1.0), |
|
|
|
onChanged: (value) { |
|
|
|
final targetPosition = |
|
|
|
(controller.duration.value * value).round(); |
|
|
|
controller.seekToPosition(targetPosition); |
|
|
|
}, |
|
|
|
), |
|
|
|
), |
|
|
|
), |
|
|
|
SizedBox(width: 6.w), // 进度条和右侧时间之间的间距 |
|
|
|
// 右侧:音乐总时长 |
|
|
|
SizedBox( |
|
|
|
width: 35.w, // 固定宽度确保对齐,足够显示 "99:59" 格式 |
|
|
|
child: Text( |
|
|
|
_formatDuration(controller.duration.value), |
|
|
|
style: TextStyle( |
|
|
|
fontSize: 12, |
|
|
|
color: isDarkMode ? Colors.grey[400] : Colors.grey[600], |
|
|
|
fontFeatures: const [FontFeature.tabularFigures()], // 使用等宽数字 |
|
|
|
), |
|
|
|
textAlign: TextAlign.right, |
|
|
|
), |
|
|
|
), |
|
|
|
], |
|
|
|
); |
|
|
|
}); |
|
|
|
} |
|
|
|
|
|
|
|
// 格式化时间显示 |
|
|
|
String _formatDuration(int milliseconds) { |
|
|
|
final duration = Duration(milliseconds: milliseconds); |
|
|
|
final minutes = duration.inMinutes; |
|
|
|
final seconds = duration.inSeconds % 60; |
|
|
|
return '${minutes.toString().padLeft(2, '0')}:${seconds.toString().padLeft(2, '0')}'; |
|
|
|
} |
|
|
|
|
|
|
|
// 构建播放控制按钮 |
|
|
|
Widget _buildPlayControls(AgentController controller, |
|
|
|
List<Map<String, dynamic>> playlist, bool isDarkMode) { |
|
|
|
return Obx(() { |
|
|
|
return Row( |
|
|
|
mainAxisAlignment: MainAxisAlignment.center, |
|
|
|
children: [ |
|
|
|
// 上一曲按钮 |
|
|
|
IconButton( |
|
|
|
onPressed: () => _playPrevious(controller, playlist), |
|
|
|
icon: Icon( |
|
|
|
Icons.skip_previous, |
|
|
|
size: 32, // 从36减小到32 |
|
|
|
color: isDarkMode ? Colors.white70 : Colors.black54, |
|
|
|
), |
|
|
|
), |
|
|
|
const SizedBox(width: 16), // 从20减小到16 |
|
|
|
// 播放/暂停按钮 |
|
|
|
Container( |
|
|
|
width: 56, // 从64减小到56 |
|
|
|
height: 56, |
|
|
|
decoration: BoxDecoration( |
|
|
|
color: Theme.of(Get.context!).primaryColor, |
|
|
|
shape: BoxShape.circle, |
|
|
|
boxShadow: [ |
|
|
|
BoxShadow( |
|
|
|
color: Theme.of(Get.context!) |
|
|
|
.primaryColor |
|
|
|
.withValues(alpha: 0.3), |
|
|
|
blurRadius: 10, // 从12减小到10 |
|
|
|
spreadRadius: 1, // 从2减小到1 |
|
|
|
offset: const Offset(0, 3), // 从4减小到3 |
|
|
|
), |
|
|
|
], |
|
|
|
), |
|
|
|
child: IconButton( |
|
|
|
onPressed: () => _togglePlayPause(controller), |
|
|
|
icon: Icon( |
|
|
|
controller.isPlaying.value ? Icons.pause : Icons.play_arrow, |
|
|
|
size: 28, // 从32减小到28 |
|
|
|
color: Colors.white, |
|
|
|
), |
|
|
|
), |
|
|
|
), |
|
|
|
const SizedBox(width: 16), // 从20减小到16 |
|
|
|
// 下一曲按钮 |
|
|
|
IconButton( |
|
|
|
onPressed: () => _playNext(controller, playlist), |
|
|
|
icon: Icon( |
|
|
|
Icons.skip_next, |
|
|
|
size: 32, // 从36减小到32 |
|
|
|
color: isDarkMode ? Colors.white70 : Colors.black54, |
|
|
|
), |
|
|
|
), |
|
|
|
const SizedBox(width: 12), |
|
|
|
// 播放按钮 |
|
|
|
Obx(() { |
|
|
|
final isThisPlaying = |
|
|
|
controller.isPlaying.value && controller.currentId.value == id; |
|
|
|
|
|
|
|
// 只有当前播放的歌曲才显示进度 |
|
|
|
final currentProgress = |
|
|
|
isThisPlaying ? controller.progress.value : 0.0; |
|
|
|
|
|
|
|
return CircularProgressButton( |
|
|
|
isPlaying: isThisPlaying, |
|
|
|
progress: currentProgress, |
|
|
|
primaryColor: Theme.of(context).primaryColor, |
|
|
|
backgroundColor: Colors.grey, |
|
|
|
size: 40.0, |
|
|
|
strokeWidth: 2.5, |
|
|
|
onPlayPause: () async { |
|
|
|
try { |
|
|
|
if (controller.isPlaying.value) { |
|
|
|
if (controller.currentId.value == id) { |
|
|
|
await controller.musicPause(); |
|
|
|
} else { |
|
|
|
await controller.musicPlay(song); |
|
|
|
} |
|
|
|
} else { |
|
|
|
controller.musicPlay(song); |
|
|
|
} |
|
|
|
} catch (e) { |
|
|
|
// 静默处理错误 |
|
|
|
} |
|
|
|
}, |
|
|
|
onSeek: isThisPlaying |
|
|
|
? (seekProgress) { |
|
|
|
// 只有当前播放的歌曲才允许跳转 |
|
|
|
final targetPosition = |
|
|
|
(controller.duration.value * seekProgress).round(); |
|
|
|
controller.seekToPosition(targetPosition); |
|
|
|
} |
|
|
|
: null, |
|
|
|
); |
|
|
|
}), |
|
|
|
], |
|
|
|
), |
|
|
|
); |
|
|
|
); |
|
|
|
}); |
|
|
|
} |
|
|
|
|
|
|
|
// 播放/暂停切换 |
|
|
|
Future<void> _togglePlayPause(AgentController controller) async { |
|
|
|
if (controller.isPlaying.value) { |
|
|
|
await controller.musicPause(); |
|
|
|
} else { |
|
|
|
await controller.musicResume(); |
|
|
|
} |
|
|
|
} |
|
|
|
|
|
|
|
// 播放上一曲 |
|
|
|
void _playPrevious( |
|
|
|
AgentController controller, List<Map<String, dynamic>> playlist) { |
|
|
|
// 确保播放列表上下文是最新的 |
|
|
|
controller.updateCurrentPlaylistContext( |
|
|
|
playlist, controller.currentId.value); |
|
|
|
controller.playPrevious(); |
|
|
|
} |
|
|
|
|
|
|
|
// 播放下一曲 |
|
|
|
void _playNext( |
|
|
|
AgentController controller, List<Map<String, dynamic>> playlist) { |
|
|
|
// 确保播放列表上下文是最新的 |
|
|
|
controller.updateCurrentPlaylistContext( |
|
|
|
playlist, controller.currentId.value); |
|
|
|
controller.playNext(); |
|
|
|
} |
|
|
|
|
|
|
|
// 播放选中的歌曲 |
|
|
|
void _playSelectedSong(AgentController controller, Map<String, dynamic> song, |
|
|
|
List<Map<String, dynamic>> playlist) { |
|
|
|
// 更新播放列表上下文 |
|
|
|
final songId = int.tryParse(song['id']?.toString() ?? '0') ?? 0; |
|
|
|
controller.updateCurrentPlaylistContext(playlist, songId); |
|
|
|
|
|
|
|
// 播放选中的歌曲 |
|
|
|
controller.musicPlay(song); |
|
|
|
} |
|
|
|
} |
|
|
|
|