diff --git a/lib/main.dart b/lib/main.dart index 0a561ec..1301c0d 100644 --- a/lib/main.dart +++ b/lib/main.dart @@ -1,23 +1,24 @@ +// lib/main.dart import 'package:flutter/material.dart'; import 'package:provider/provider.dart'; import 'package:media_kit/media_kit.dart'; import 'services/audio_service.dart'; import 'services/playback_service.dart'; import 'pages/home_page.dart'; +import 'widgets/global_mini_player.dart'; + +// ✅ 全局导航键 +final GlobalKey navigatorKey = GlobalKey(); void main() { WidgetsFlutterBinding.ensureInitialized(); - // 捕获初始化阶段的所有异常,避免引擎崩溃 try { - // 必须先初始化 media_kit MediaKit.ensureInitialized(); - // 然后初始化播放服务 PlaybackService().init(); } catch (e, stack) { print('❌ 初始化失败: $e'); print(stack); - // 显示错误页面,而不是白屏 runApp( MaterialApp( home: Scaffold( @@ -47,7 +48,6 @@ void main() { return; } - // 运行时错误捕获 FlutterError.onError = (details) { print('❌ Flutter Error: ${details.exception}'); print('Stack: ${details.stack}'); @@ -68,6 +68,7 @@ class QTPlayerApp extends StatelessWidget { Widget build(BuildContext context) { return MaterialApp( title: '清听', + navigatorKey: navigatorKey, // ✅ 设置导航键 theme: ThemeData( brightness: Brightness.dark, primaryColor: const Color(0xFF7C9A9E), @@ -80,6 +81,25 @@ class QTPlayerApp extends StatelessWidget { useMaterial3: true, ), home: const HomePage(), + builder: (context, child) { + return Overlay( + initialEntries: [ + OverlayEntry( + builder: (context) => Stack( + children: [ + Positioned.fill(child: child!), + const Positioned( + left: 0, + right: 0, + bottom: 0, + child: GlobalMiniPlayer(), + ), + ], + ), + ), + ], + ); + }, ); } } diff --git a/lib/pages/home_page.dart b/lib/pages/home_page.dart index 459ffac..4b45b56 100644 --- a/lib/pages/home_page.dart +++ b/lib/pages/home_page.dart @@ -688,9 +688,6 @@ class _HomePageState extends State { ], ), ), - - // ---- 底部 MiniPlayer ---- - if (showMiniBar) const MiniPlayerBar(), ], ), ); diff --git a/lib/pages/playlist_page.dart b/lib/pages/playlist_page.dart new file mode 100644 index 0000000..1754009 --- /dev/null +++ b/lib/pages/playlist_page.dart @@ -0,0 +1,151 @@ +// lib/pages/playlist_page.dart +import 'package:flutter/material.dart'; +import 'package:provider/provider.dart'; +import '../services/audio_service.dart'; + +class PlaylistPage extends StatelessWidget { + const PlaylistPage({super.key}); + + @override + Widget build(BuildContext context) { + final service = context.watch(); + final queue = service.queue; + final currentIndex = service.currentIndex; + + return Scaffold( + backgroundColor: const Color(0xFF0E1211), + appBar: AppBar( + title: const Text( + '播放列表', + style: TextStyle( + color: Colors.white, + fontSize: 18, + fontWeight: FontWeight.w500, + ), + ), + backgroundColor: Colors.transparent, + elevation: 0, + foregroundColor: Colors.white, + leading: IconButton( + icon: const Icon(Icons.arrow_back_ios_new), + onPressed: () => Navigator.pop(context), + ), + actions: [ + IconButton( + icon: const Icon(Icons.clear_all, color: Colors.white54), + onPressed: () { + showDialog( + context: context, + builder: (context) => AlertDialog( + backgroundColor: const Color(0xFF1A1F1E), + title: const Text( + '清空播放列表', + style: TextStyle(color: Colors.white), + ), + content: const Text( + '确定要清空当前播放列表吗?', + style: TextStyle(color: Colors.grey), + ), + actions: [ + TextButton( + onPressed: () => Navigator.pop(context), + child: const Text('取消'), + ), + TextButton( + onPressed: () { + service.clearQueue(); + Navigator.pop(context); + Navigator.pop(context); + }, + child: const Text( + '清空', + style: TextStyle(color: Colors.red), + ), + ), + ], + ), + ); + }, + ), + ], + ), + body: queue.isEmpty + ? const Center( + child: Column( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + Icon(Icons.playlist_play, size: 48, color: Colors.grey), + SizedBox(height: 16), + Text( + '播放列表为空', + style: TextStyle(color: Colors.grey), + ), + ], + ), + ) + : ListView.builder( + padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 8), + itemCount: queue.length, + itemBuilder: (context, index) { + final song = queue[index]; + final isCurrent = index == currentIndex; + + return ListTile( + contentPadding: const EdgeInsets.symmetric( + horizontal: 8, + vertical: 4, + ), + leading: Icon( + isCurrent ? Icons.play_arrow : Icons.music_note, + color: + isCurrent ? const Color(0xFFB8D4D0) : Colors.grey[600], + size: 24, + ), + title: Text( + song.title, + style: TextStyle( + fontSize: 16, + fontWeight: isCurrent ? FontWeight.w600 : FontWeight.w400, + color: isCurrent ? Colors.white : Colors.grey[300], + ), + maxLines: 1, + overflow: TextOverflow.ellipsis, + ), + subtitle: Text( + song.artist, + style: TextStyle( + fontSize: 13, + color: isCurrent ? Colors.grey[400] : Colors.grey[600], + ), + maxLines: 1, + overflow: TextOverflow.ellipsis, + ), + trailing: isCurrent + ? Container( + padding: const EdgeInsets.symmetric( + horizontal: 8, + vertical: 2, + ), + decoration: BoxDecoration( + color: const Color(0xFFB8D4D0).withOpacity(0.2), + borderRadius: BorderRadius.circular(4), + ), + child: const Text( + '正在播放', + style: TextStyle( + fontSize: 10, + color: Color(0xFFB8D4D0), + ), + ), + ) + : null, + onTap: () { + service.setQueue(queue, startIndex: index); + Navigator.pop(context); + }, + ); + }, + ), + ); + } +} diff --git a/lib/pages/webdav_file_list_page.dart b/lib/pages/webdav_file_list_page.dart index d93fc31..d01dc02 100644 --- a/lib/pages/webdav_file_list_page.dart +++ b/lib/pages/webdav_file_list_page.dart @@ -1,11 +1,10 @@ // lib/pages/webdav_file_list_page.dart +import 'dart:convert'; import 'package:flutter/material.dart'; import 'package:provider/provider.dart'; import '../services/webdav_service.dart'; import '../services/playback_service.dart'; import '../services/audio_service.dart'; -import 'dart:async'; -import 'dart:convert'; class WebDAVFileListPage extends StatefulWidget { final String currentPath; @@ -27,7 +26,7 @@ class _WebDAVFileListPageState extends State { try { return Uri.decodeComponent(input); } catch (_) { - return input; // 解码失败时返回原始字符串 + return input; } } @@ -38,6 +37,9 @@ class _WebDAVFileListPageState extends State { _loadDirectory(); } + // ============================================================ + // 加载目录 + // ============================================================ Future _loadDirectory() async { setState(() { _isLoading = true; @@ -59,6 +61,9 @@ class _WebDAVFileListPageState extends State { } } + // ============================================================ + // 进入子目录 + // ============================================================ void _enterDirectory(WebDAVItem dir) { Navigator.push( context, @@ -68,19 +73,19 @@ class _WebDAVFileListPageState extends State { ); } + // ============================================================ + // ⭐ 核心:播放歌曲 + 自动构建队列 + // ============================================================ void _playSong(WebDAVItem file) async { try { - final url = WebDAVService.instance.getFileUrl(file.path); + // 1. 获取当前目录所有音乐文件(过滤掉目录) + final musicFiles = _items.where((item) => !item.isDirectory).toList(); - // ✅ 动态获取认证头(从 SharedPreferences 读取) - final headers = await WebDAVService.instance.getAuthHeaders(); - - if (headers.isEmpty) { - print('⚠️ 未获取到认证头,请检查 WebDAV 登录状态'); + if (musicFiles.isEmpty) { if (mounted) { ScaffoldMessenger.of(context).showSnackBar( const SnackBar( - content: Text('请先登录 WebDAV'), + content: Text('当前目录没有音乐文件'), backgroundColor: Colors.orange, ), ); @@ -88,18 +93,57 @@ class _WebDAVFileListPageState extends State { return; } - print('🎵 [文件列表] 播放 URL: $url'); - print('📋 [文件列表] 认证头已设置: ${headers.keys}'); + // 2. 获取认证头 + final headers = await WebDAVService.instance.getAuthHeaders(); - final song = Song( - id: file.path, - title: file.name.replaceAll(RegExp(r'\.[^.]*$'), ''), - artist: '未知艺术家', - url: url, - ); - context.read().playSong(song); + // 3. 构建播放队列(所有音乐文件) + final queue = musicFiles.map((item) { + final itemUrl = WebDAVService.instance.getFileUrl(item.path); + return Song( + id: item.path, + title: item.name.replaceAll(RegExp(r'\.[^.]*$'), ''), + artist: '未知艺术家', + url: itemUrl, + ); + }).toList(); - await PlaybackService().play(url, headers: headers); + // 4. 找到当前点击歌曲在队列中的位置 + final startIndex = queue.indexWhere((s) => s.id == file.path); + if (startIndex == -1) { + // 极端情况:队列构建有问题,退化为单曲播放 + final url = WebDAVService.instance.getFileUrl(file.path); + final song = Song( + id: file.path, + title: file.name.replaceAll(RegExp(r'\.[^.]*$'), ''), + artist: '未知艺术家', + url: url, + ); + context.read().setQueue([song], startIndex: 0); + await PlaybackService().play(url, headers: headers); + return; + } + + // 5. 设置队列并播放 + final audioService = context.read(); + audioService.setQueue(queue, startIndex: startIndex); + + // 6. 播放(AudioService 内部已经调用了 PlaybackService,但为了确保认证头传递) + // 这里再显式调用一下,确保认证头正确 + final targetUrl = queue[startIndex].url!; + await PlaybackService().play(targetUrl, headers: headers); + + // 7. 更新 AudioService 的播放状态(确保 UI 同步) + // setQueue 已经调用了 _playCurrent(),所以这里不需要重复调用 + + if (mounted) { + ScaffoldMessenger.of(context).showSnackBar( + SnackBar( + content: Text('正在播放: ${file.name}'), + backgroundColor: const Color(0xFF4CAF50), + duration: const Duration(seconds: 1), + ), + ); + } } catch (e) { if (mounted) { ScaffoldMessenger.of(context).showSnackBar( @@ -112,6 +156,67 @@ class _WebDAVFileListPageState extends State { } } + // ============================================================ + // 全部播放(从第一首开始) + // ============================================================ + void _playAll() async { + try { + final musicFiles = _items.where((item) => !item.isDirectory).toList(); + + if (musicFiles.isEmpty) { + if (mounted) { + ScaffoldMessenger.of(context).showSnackBar( + const SnackBar( + content: Text('当前目录没有音乐文件'), + backgroundColor: Colors.orange, + ), + ); + } + return; + } + + final headers = await WebDAVService.instance.getAuthHeaders(); + + final queue = musicFiles.map((item) { + final itemUrl = WebDAVService.instance.getFileUrl(item.path); + return Song( + id: item.path, + title: item.name.replaceAll(RegExp(r'\.[^.]*$'), ''), + artist: '未知艺术家', + url: itemUrl, + ); + }).toList(); + + final audioService = context.read(); + audioService.setQueue(queue, startIndex: 0); + + final targetUrl = queue[0].url!; + await PlaybackService().play(targetUrl, headers: headers); + + if (mounted) { + ScaffoldMessenger.of(context).showSnackBar( + SnackBar( + content: Text('开始播放全部 (${queue.length}首)'), + backgroundColor: const Color(0xFF4CAF50), + duration: const Duration(seconds: 1), + ), + ); + } + } catch (e) { + if (mounted) { + ScaffoldMessenger.of(context).showSnackBar( + SnackBar( + content: Text('播放失败: $e'), + backgroundColor: Colors.red, + ), + ); + } + } + } + + // ============================================================ + // 显示路径 + // ============================================================ String _getDisplayPath() { if (_currentPath == '/' || _currentPath.isEmpty) return '根目录'; @@ -123,6 +228,9 @@ class _WebDAVFileListPageState extends State { return '... / ${parts.sublist(parts.length - 2).join(' / ')}'; } + // ============================================================ + // Build + // ============================================================ @override Widget build(BuildContext context) { return Scaffold( @@ -140,6 +248,12 @@ class _WebDAVFileListPageState extends State { onPressed: () => Navigator.pop(context), ), actions: [ + // 全部播放按钮 + IconButton( + icon: const Icon(Icons.playlist_play), + onPressed: _playAll, + tooltip: '全部播放', + ), IconButton( icon: const Icon(Icons.refresh), onPressed: _loadDirectory, @@ -211,6 +325,9 @@ class _WebDAVFileListPageState extends State { ); } + // ============================================================ + // 构建列表项 + // ============================================================ Widget _buildListItem(WebDAVItem item) { final displayName = _safeDecode(item.name); diff --git a/lib/services/audio_service.dart b/lib/services/audio_service.dart index 6f2453a..06cc89b 100644 --- a/lib/services/audio_service.dart +++ b/lib/services/audio_service.dart @@ -1,8 +1,21 @@ // lib/services/audio_service.dart import 'dart:async'; import 'package:flutter/material.dart'; +import 'package:media_kit/media_kit.dart'; import 'playback_service.dart'; +// ============================================================ +// 播放模式 +// ============================================================ +enum PlayMode { + sequential, // 顺序循环 + repeatOne, // 单曲循环 + shuffle, // 随机播放 +} + +// ============================================================ +// 歌曲模型 +// ============================================================ class Song { final String id; final String title; @@ -17,23 +30,127 @@ class Song { }); } +// ============================================================ +// AudioService - 播放状态管理与 UI 数据源 +// ============================================================ class AudioService extends ChangeNotifier { + // ---------- 单例 ---------- + static final AudioService _instance = AudioService._internal(); + factory AudioService() => _instance; + AudioService._internal(); + + // ---------- 播放状态 ---------- Song? _currentSong; bool _isPlaying = false; Duration _position = Duration.zero; Duration _duration = Duration.zero; - Duration _bufferedPosition = Duration.zero; // ✅ 新增:缓冲位置 + Duration _bufferedPosition = Duration.zero; + // ---------- 播放模式 ---------- + PlayMode _playMode = PlayMode.sequential; + + // ---------- 播放队列 ---------- + List _queue = []; + int _currentIndex = -1; + + // ---------- 随机播放相关 ---------- + List _shuffledIndices = []; + int _shuffledIndex = -1; + + // ---------- 监听控制 ---------- bool _listening = false; final List _subscriptions = []; + // ---------- Getter ---------- Song? get currentSong => _currentSong; bool get isPlaying => _isPlaying; Duration get position => _position; Duration get duration => _duration; - Duration get bufferedPosition => _bufferedPosition; // ✅ 新增 getter + Duration get bufferedPosition => _bufferedPosition; + PlayMode get playMode => _playMode; + List get queue => List.unmodifiable(_queue); + int get currentIndex => _currentIndex; + bool get hasQueue => _queue.isNotEmpty; + // ---------- 播放模式图标 ---------- + IconData get playModeIcon { + switch (_playMode) { + case PlayMode.sequential: + return Icons.repeat; + case PlayMode.repeatOne: + return Icons.repeat_one; + case PlayMode.shuffle: + return Icons.shuffle; + } + } + + // ---------- 播放模式切换 ---------- + void togglePlayMode() { + switch (_playMode) { + case PlayMode.sequential: + _playMode = PlayMode.repeatOne; + break; + case PlayMode.repeatOne: + _playMode = PlayMode.shuffle; + break; + case PlayMode.shuffle: + _playMode = PlayMode.sequential; + break; + } + notifyListeners(); + } + + // ---------- 设置播放队列 ---------- + void setQueue(List queue, {int startIndex = 0}) { + if (queue.isEmpty) { + _clearQueue(); + return; + } + + _queue = List.from(queue); + _currentIndex = startIndex.clamp(0, _queue.length - 1); + + // 初始化随机播放索引 + _shuffledIndices = List.generate(_queue.length, (i) => i); + _shuffledIndices.shuffle(); + _shuffledIndex = _shuffledIndices.indexOf(_currentIndex); + if (_shuffledIndex == -1) { + _shuffledIndex = 0; + _currentIndex = _shuffledIndices[0]; + } + + // 播放当前歌曲 + _playCurrent(); + } + + // ---------- 清空队列 ---------- + void _clearQueue() { + _queue.clear(); + _currentIndex = -1; + _shuffledIndices.clear(); + _shuffledIndex = -1; + stopPlay(); + } + + // ---------- 播放指定歌曲(外部入口) ---------- Future playSong(Song song) async { + // 如果当前队列不包含这首歌,替换队列 + if (_queue.isEmpty || _queue[_currentIndex].id != song.id) { + setQueue([song], startIndex: 0); + } else { + // 如果已经在队列中,直接播放 + _playCurrent(); + } + } + + // ---------- 播放当前索引歌曲 ---------- + void _playCurrent() { + if (_currentIndex < 0 || _currentIndex >= _queue.length) { + stopPlay(); + return; + } + + final song = _queue[_currentIndex]; _currentSong = song; _position = Duration.zero; _duration = Duration.zero; @@ -46,9 +163,59 @@ class AudioService extends ChangeNotifier { return; } - await PlaybackService().play(song.url!); + // 由 PlaybackService 实际播放,状态由流更新 + PlaybackService().play(song.url!); } + // ---------- 播放下一首 ---------- + void next() { + if (_queue.isEmpty) return; + + // 随机模式 + if (_playMode == PlayMode.shuffle) { + if (_shuffledIndices.isEmpty) return; + final nextIdx = (_shuffledIndex + 1) % _shuffledIndices.length; + _shuffledIndex = nextIdx; + _currentIndex = _shuffledIndices[nextIdx]; + _playCurrent(); + return; + } + + // 顺序/单曲模式 + final nextIdx = (_currentIndex + 1) % _queue.length; + _currentIndex = nextIdx; + _playCurrent(); + } + + // ---------- 播放上一首 ---------- + void previous() { + if (_queue.isEmpty) return; + + // 随机模式 + if (_playMode == PlayMode.shuffle) { + if (_shuffledIndices.isEmpty) return; + final prevIdx = (_shuffledIndex - 1) % _shuffledIndices.length; + if (prevIdx < 0) { + _shuffledIndex = _shuffledIndices.length - 1; + } else { + _shuffledIndex = prevIdx; + } + _currentIndex = _shuffledIndices[_shuffledIndex]; + _playCurrent(); + return; + } + + // 顺序/单曲模式 + final prevIdx = (_currentIndex - 1) % _queue.length; + if (prevIdx < 0) { + _currentIndex = _queue.length - 1; + } else { + _currentIndex = prevIdx; + } + _playCurrent(); + } + + // ---------- 播放/暂停切换 ---------- void togglePlay() { if (_currentSong == null) return; @@ -57,8 +224,10 @@ class AudioService extends ChangeNotifier { } else { PlaybackService().resume(); } + // 状态由 player.stream.playing 更新 } + // ---------- 停止播放 ---------- void stopPlay() { _currentSong = null; _isPlaying = false; @@ -69,12 +238,37 @@ class AudioService extends ChangeNotifier { notifyListeners(); } + // ---------- Seek ---------- void seekTo(Duration position) { PlaybackService().seek(position); _position = position; notifyListeners(); } + // ---------- 处理播放结束 ---------- + void _onPlaybackCompleted() { + if (_queue.isEmpty) return; + + // 单曲循环模式 + if (_playMode == PlayMode.repeatOne) { + _playCurrent(); + return; + } + + // 其他模式:播放下一首 + next(); + } + + void clearQueue() { + _queue.clear(); + _currentIndex = -1; + _shuffledIndices.clear(); + _shuffledIndex = -1; + stopPlay(); + notifyListeners(); + } + + // ---------- 监听 media_kit 状态 ---------- void _startListening() { if (_listening) return; _listening = true; @@ -108,7 +302,6 @@ class AudioService extends ChangeNotifier { }), ); - // ✅ 新增:监听缓冲位置 _subscriptions.add( player.stream.buffer.listen((buffer) { if (_bufferedPosition != buffer) { @@ -117,6 +310,13 @@ class AudioService extends ChangeNotifier { } }), ); + + // ✅ 播放结束监听 + _subscriptions.add( + player.stream.completed.listen((_) { + _onPlaybackCompleted(); + }), + ); } void _stopListening() { @@ -126,4 +326,11 @@ class AudioService extends ChangeNotifier { } _subscriptions.clear(); } + + // ---------- 资源释放 ---------- + void dispose() { + _stopListening(); + PlaybackService().dispose(); + super.dispose(); + } } diff --git a/lib/services/playback_service.dart b/lib/services/playback_service.dart index d47b8eb..803c6cf 100644 --- a/lib/services/playback_service.dart +++ b/lib/services/playback_service.dart @@ -1,57 +1,91 @@ // lib/services/playback_service.dart import 'package:media_kit/media_kit.dart'; +// ============================================================ +// PlaybackService - 播放引擎封装 +// 职责:管理 Player 生命周期,不涉及业务逻辑 +// ============================================================ class PlaybackService { - static final PlaybackService _instance = PlaybackService._(); + static final PlaybackService _instance = PlaybackService._internal(); factory PlaybackService() => _instance; - PlaybackService._(); + PlaybackService._internal(); - late final Player _player; + Player? _player; bool _initialized = false; - // 获取 Player 实例(供 AudioService 直接监听) + // ---------- Getter ---------- Player get player { if (!_initialized) init(); - return _player; + return _player!; } + bool get isInitialized => _initialized; + + // ---------- 初始化 ---------- void init() { if (_initialized) return; _player = Player(); _initialized = true; + + // 可以在这里配置初始参数 + // _player.setVolume(1.0); } + // ---------- 播放 ---------- Future play(String url, {Map? headers}) async { if (!_initialized) init(); - print('🎵 播放 URL: $url'); - print('📋 认证头: ${headers?.keys}'); - final media = headers != null && headers.isNotEmpty + + final media = (headers != null && headers.isNotEmpty) ? Media(url, httpHeaders: headers) : Media(url); - await _player.open(media); - await _player.play(); + + await _player!.open(media); + await _player!.play(); } + // ---------- 控制 ---------- void pause() { - if (_initialized) _player.pause(); + if (_initialized && _player != null) { + _player!.pause(); + } } void resume() { - if (_initialized) _player.play(); + if (_initialized && _player != null) { + _player!.play(); + } } void stop() { - if (_initialized) _player.stop(); + if (_initialized && _player != null) { + _player!.stop(); + } } void seek(Duration position) { - if (_initialized) _player.seek(position); + if (_initialized && _player != null) { + _player!.seek(position); + } } + void setVolume(double volume) { + if (_initialized && _player != null) { + _player!.setVolume(volume); + } + } + + // ---------- 释放 ---------- void dispose() { - if (_initialized) { - _player.dispose(); + if (_initialized && _player != null) { + _player!.dispose(); + _player = null; _initialized = false; } } + + // ---------- 重新初始化(用于 Hot Restart 场景) ---------- + void reinitialize() { + dispose(); + init(); + } } diff --git a/lib/widgets/global_mini_player.dart b/lib/widgets/global_mini_player.dart new file mode 100644 index 0000000..f633caa --- /dev/null +++ b/lib/widgets/global_mini_player.dart @@ -0,0 +1,134 @@ +// lib/widgets/global_mini_player.dart +import 'package:flutter/material.dart'; +import 'package:provider/provider.dart'; +import '../services/audio_service.dart'; +import '../pages/player_page.dart'; +import '../pages/playlist_page.dart'; +import '../main.dart'; // ✅ 导入 navigatorKey + +class GlobalMiniPlayer extends StatelessWidget { + const GlobalMiniPlayer({super.key}); + + @override + Widget build(BuildContext context) { + final service = context.watch(); + final song = service.currentSong; + + final bottomPadding = MediaQuery.of(context).padding.bottom; + + return GestureDetector( + onTap: () { + if (song != null) { + // ✅ 使用 navigatorKey 跳转 + navigatorKey.currentState?.push( + MaterialPageRoute( + builder: (_) => const PlayerPage(), + ), + ); + } + }, + child: Container( + height: 56 + bottomPadding, + decoration: BoxDecoration( + color: const Color(0xFF1A1F1E), + boxShadow: [ + BoxShadow( + color: Colors.black.withOpacity(0.4), + blurRadius: 12, + offset: const Offset(0, -4), + ), + ], + ), + child: SafeArea( + top: false, + bottom: true, + child: Padding( + padding: const EdgeInsets.only(bottom: 0), + child: Row( + children: [ + const SizedBox(width: 12), + Container( + width: 40, + height: 40, + decoration: BoxDecoration( + color: song != null + ? const Color(0xFF2A3332) + : Colors.grey[800], + borderRadius: BorderRadius.circular(4), + ), + child: Icon( + song != null ? Icons.music_note : Icons.music_off, + color: song != null ? Colors.white38 : Colors.grey[600], + size: 20, + ), + ), + const SizedBox(width: 12), + Expanded( + child: Column( + mainAxisAlignment: MainAxisAlignment.center, + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text( + song != null ? song.title : '清听', + style: const TextStyle( + fontSize: 14, + fontWeight: FontWeight.w500, + color: Colors.white, + decoration: TextDecoration.none, + ), + maxLines: 1, + overflow: TextOverflow.ellipsis, + ), + Text( + song != null ? song.artist : '未播放', + style: TextStyle( + fontSize: 12, + color: song != null + ? Colors.grey[400] + : Colors.grey[600], + decoration: TextDecoration.none, + ), + maxLines: 1, + overflow: TextOverflow.ellipsis, + ), + ], + ), + ), + IconButton( + icon: Icon( + song != null && service.isPlaying + ? Icons.pause + : Icons.play_arrow, + color: song != null ? Colors.white : Colors.grey[600], + size: 24, + ), + onPressed: () { + if (song != null) { + service.togglePlay(); + } + }, + ), + IconButton( + icon: Icon( + Icons.playlist_play_outlined, + color: Colors.grey[500], + size: 24, + ), + onPressed: () { + // ✅ 使用 navigatorKey 跳转 + navigatorKey.currentState?.push( + MaterialPageRoute( + builder: (_) => const PlaylistPage(), + ), + ); + }, + ), + const SizedBox(width: 4), + ], + ), + ), + ), + ), + ); + } +}