// lib/pages/player_page.dart import 'package:flutter/material.dart'; import 'package:provider/provider.dart'; import '../base/base_state.dart'; import '../database/song_database.dart'; import '../services/audio_service.dart'; class PlayerPage extends StatefulWidget { final VoidCallback? onClose; final VoidCallback? onOpenPlaylist; const PlayerPage({ super.key, this.onClose, this.onOpenPlaylist, }); @override State createState() => _PlayerPageState(); } class _PlayerPageState extends BaseState { double? _dragProgress; bool _isFavorite = false; String _album = ''; String _folder = ''; String _dbTitle = ''; String _dbArtist = ''; String? _metaSongId; /// 0 = 文件信息,1 = 播放主页,2 = 歌词 final PageController _pageController = PageController(initialPage: 1); int _pageIndex = 1; @override void dispose() { _pageController.dispose(); super.dispose(); } String _formatDuration(Duration d) { if (d.inMilliseconds < 0) return '00:00'; final minutes = d.inMinutes; final seconds = d.inSeconds % 60; return '${minutes.toString().padLeft(2, '0')}:${seconds.toString().padLeft(2, '0')}'; } void _closePage() { if (widget.onClose != null) { widget.onClose!(); } else { Navigator.pop(context); } } void _openPlaylist() { if (widget.onOpenPlaylist != null) { widget.onOpenPlaylist!(); } else { Navigator.pushNamed(context, '/playlist'); } } @override void initState() { super.initState(); _syncStateWithRetry(); } @override void didChangeDependencies() { super.didChangeDependencies(); WidgetsBinding.instance.addPostFrameCallback((_) { if (mounted) { context.read().syncPlayerStateNow(); } }); } void _syncStateWithRetry() { final service = context.read(); service.syncPlayerStateNow(); const delays = [200, 500, 800, 1200]; for (final delay in delays) { Future.delayed(Duration(milliseconds: delay), () { if (mounted) { service.syncPlayerStateNow(); } }); } } /// 元数据(专辑、收藏状态)只在切歌时查一次 Future _loadMeta(Song song) async { if (_metaSongId == song.id) return; _metaSongId = song.id; final db = SongDatabase(); String album = ''; String dbTitle = ''; String dbArtist = ''; bool favorite = false; try { final row = await db.getSongByPath(song.id); album = (row?['album'] as String?)?.trim() ?? ''; dbTitle = (row?['title'] as String?)?.trim() ?? ''; dbArtist = (row?['artist'] as String?)?.trim() ?? ''; favorite = await db.isFavorite(song.id); } catch (_) { // 元数据只是装饰,失败忽略 } // 上级目录名,作为没有专辑信息时的补充 String folder = ''; final segments = song.id.split('/').where((s) => s.isNotEmpty).toList(); if (segments.length >= 2) { try { folder = Uri.decodeComponent(segments[segments.length - 2]); } catch (_) { folder = segments[segments.length - 2]; } } if (!mounted) return; setState(() { _album = album; _folder = folder; _dbTitle = dbTitle; _dbArtist = dbArtist; _isFavorite = favorite; }); } Future _toggleFavorite(Song song) async { try { await SongDatabase().toggleFavorite(song.id); final favorite = await SongDatabase().isFavorite(song.id); if (!mounted) return; setState(() => _isFavorite = favorite); } catch (e) { if (mounted) { ScaffoldMessenger.of(context).showSnackBar( SnackBar(content: Text('收藏失败: $e')), ); } } } @override Widget build(BuildContext context) { final service = context.watch(); final song = service.currentSong; if (song == null) { return Scaffold( backgroundColor: const Color(0xFF0E1211), body: Center( child: Column( mainAxisAlignment: MainAxisAlignment.center, children: [ const Text( '没有正在播放的歌曲', style: TextStyle(color: Colors.grey), ), const SizedBox(height: 16), ElevatedButton( onPressed: _closePage, style: ElevatedButton.styleFrom( backgroundColor: const Color(0xFFB8D4D0), foregroundColor: Colors.black87, ), child: const Text('返回'), ), ], ), ), ); } _loadMeta(song); final isPlaying = service.isPlaying; return Scaffold( backgroundColor: const Color(0xFF0E1211), appBar: AppBar( backgroundColor: Colors.transparent, elevation: 0, leading: IconButton( icon: const Icon(Icons.keyboard_arrow_down, color: Colors.white70), onPressed: _closePage, ), centerTitle: true, title: _buildPageIndicator(), actions: [ PopupMenuButton( icon: const Icon(Icons.more_vert, color: Colors.white54), onSelected: (value) async { if (value == 'clear_cache') { await context.read().clearCurrentSongCache(); if (mounted) { ScaffoldMessenger.of(context).showSnackBar( const SnackBar( content: Text('缓存已清除,重新加载中...'), backgroundColor: Colors.orange, ), ); } } else if (value == 'mode') { context.read().togglePlayMode(); } else if (value == 'playlist') { _openPlaylist(); } }, itemBuilder: (context) => [ PopupMenuItem( value: 'mode', child: Row( children: [ Icon(service.playModeIcon, color: const Color(0xFFB8D4D0)), const SizedBox(width: 12), const Text('切换播放模式'), ], ), ), const PopupMenuItem( value: 'playlist', child: Row( children: [ Icon(Icons.queue_music, color: Color(0xFFB8D4D0)), SizedBox(width: 12), Text('播放队列'), ], ), ), const PopupMenuItem( value: 'clear_cache', child: Row( children: [ Icon(Icons.cleaning_services, color: Colors.redAccent), SizedBox(width: 12), Text('清除缓存'), ], ), ), ], ), ], ), body: SafeArea( top: false, child: PageView( controller: _pageController, onPageChanged: (index) => setState(() => _pageIndex = index), children: [ _buildInfoPage(song), _buildMainPage(service, song, isPlaying), _buildLyricsPage(song), ], ), ), ); } /// 顶部指示器:当前页对应"长条",另外两页是小点。 /// 主页=中间长条,信息页=左长条,歌词页=右长条。 Widget _buildPageIndicator() { Widget segment(int index) { final active = _pageIndex == index; return GestureDetector( onTap: () => _pageController.animateToPage( index, duration: const Duration(milliseconds: 280), curve: Curves.easeOutCubic, ), 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 ? 30 : 7, height: 7, decoration: BoxDecoration( color: active ? Colors.white : Colors.white24, borderRadius: BorderRadius.circular(4), ), ), ), ); } return Row( mainAxisSize: MainAxisSize.min, children: [ segment(0), const SizedBox(width: 6), segment(1), const SizedBox(width: 6), segment(2), ], ); } Widget _buildMainPage(AudioService service, Song song, bool isPlaying) { return Padding( // 顶部留 16:与 AppBar/指示器拉开距离,避免封面贴着顶部 padding: const EdgeInsets.fromLTRB(24, 24, 24, 0), child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ // ---- 封面 ---- AspectRatio( aspectRatio: 1, child: ClipRRect( borderRadius: BorderRadius.circular(24), child: song.artwork != null ? Image.memory( song.artwork!, fit: BoxFit.cover, gaplessPlayback: true, ) : Container( color: const Color(0xFF2A3332), child: const Icon( Icons.music_note, size: 72, color: Colors.white24, ), ), ), ), const SizedBox(height: 28), // ---- 标题 / 艺术家(收藏心在标题右侧)---- Row( crossAxisAlignment: CrossAxisAlignment.start, children: [ Expanded( child: Text( song.title, style: const TextStyle( fontSize: 24, fontWeight: FontWeight.w600, color: Colors.white, height: 1.25, ), maxLines: 2, overflow: TextOverflow.ellipsis, ), ), const SizedBox(width: 8), IconButton( onPressed: () => _toggleFavorite(song), tooltip: '收藏', icon: Icon( _isFavorite ? Icons.favorite : Icons.favorite_border, color: _isFavorite ? const Color(0xFFE05C5C) : Colors.white54, size: 26, ), ), ], ), const SizedBox(height: 10), Text( song.artist, style: TextStyle(fontSize: 16, color: Colors.grey[300]), maxLines: 1, overflow: TextOverflow.ellipsis, ), // ---- 信息区(专辑 / 目录)---- if (_album.isNotEmpty || _folder.isNotEmpty) ...[ const SizedBox(height: 10), if (_album.isNotEmpty) Text( _album, style: TextStyle( fontSize: 12, color: Colors.grey[500], height: 1.5, ), maxLines: 2, overflow: TextOverflow.ellipsis, ), if (_folder.isNotEmpty) Text( '($_folder)', style: TextStyle( fontSize: 12, color: Colors.grey[600], height: 1.5, ), maxLines: 1, overflow: TextOverflow.ellipsis, ), ], const Spacer(), // ---- 进度 ---- _buildProgressSection(service), const SizedBox(height: 18), // ---- 控制 ---- _buildControlButtons(service, song, isPlaying), // 底部多留 50px:控制行整体上移 const SizedBox(height: 45), ], ), ); } /// 左页:文件信息 Widget _buildInfoPage(Song song) { final rawName = song.id.split('/').last; String fileName = rawName; try { fileName = Uri.decodeComponent(rawName); } catch (_) {} final dot = fileName.lastIndexOf('.'); final format = dot > 0 ? fileName.substring(dot + 1).toUpperCase() : '未知'; String decodedPath = song.id; try { decodedPath = Uri.decodeComponent(song.id); } catch (_) {} final effectiveArtist = _dbArtist.isNotEmpty ? _dbArtist : (song.artist.isEmpty ? '未知' : song.artist); final effectiveTitle = _dbTitle.isNotEmpty ? _dbTitle : song.title; final rows = >[ MapEntry('标题', effectiveTitle), MapEntry('歌手', effectiveArtist), MapEntry('专辑', _album.isEmpty ? '未知' : _album), MapEntry('文件夹', _folder.isEmpty ? '根目录' : _folder), MapEntry('文件名', fileName), MapEntry('格式', format), MapEntry('路径', decodedPath), ]; return SingleChildScrollView( padding: const EdgeInsets.symmetric(horizontal: 24, vertical: 8), child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ const Text( '文件信息', style: TextStyle( fontSize: 18, fontWeight: FontWeight.w600, color: Colors.white, ), ), const SizedBox(height: 20), for (final row in rows) ...[ Text( row.key, style: TextStyle(fontSize: 12, color: Colors.grey[600]), ), const SizedBox(height: 4), Text( row.value, style: const TextStyle( fontSize: 14, color: Colors.white, height: 1.4, ), ), const SizedBox(height: 18), ], ], ), ); } /// 右页:歌词(占位) Widget _buildLyricsPage(Song song) { return Center( child: Column( mainAxisAlignment: MainAxisAlignment.center, children: [ Icon(Icons.lyrics_outlined, size: 44, color: Colors.grey[700]), const SizedBox(height: 14), Text( '暂无歌词', style: TextStyle(fontSize: 14, color: Colors.grey[600]), ), const SizedBox(height: 6), Text( song.title, style: TextStyle(fontSize: 12, color: Colors.grey[700]), maxLines: 1, overflow: TextOverflow.ellipsis, ), ], ), ); } Widget _buildProgressSection(AudioService service) { return ValueListenableBuilder( valueListenable: service.positionNotifier, builder: (context, position, _) { return ValueListenableBuilder( valueListenable: service.durationNotifier, builder: (context, duration, _) { if (duration.inMilliseconds == 0) { return const Padding( padding: EdgeInsets.symmetric(vertical: 8), child: LinearProgressIndicator( color: Color(0xFFB8D4D0), backgroundColor: Color(0xFF2A3332), ), ); } final progress = position.inMilliseconds / duration.inMilliseconds; final bufferProgress = (service .bufferedNotifier.value.inMilliseconds / duration.inMilliseconds) .clamp(0.0, 1.0); final displayProgress = _dragProgress ?? progress; return Row( children: [ Text( _formatDuration( _dragProgress != null ? Duration( milliseconds: (_dragProgress! * duration.inMilliseconds) .round(), ) : position, ), style: TextStyle(fontSize: 12, color: Colors.grey[500]), ), const SizedBox(width: 10), Expanded( child: SizedBox( height: 24, child: LayoutBuilder( builder: (context, constraints) { final width = constraints.maxWidth; return Stack( alignment: Alignment.centerLeft, children: [ // 背景轨道 Positioned( left: 0, right: 0, top: 10.5, child: Container( height: 3, decoration: BoxDecoration( color: Colors.grey[800], borderRadius: BorderRadius.circular(2), ), ), ), // 已缓冲 Positioned( left: 0, top: 10.5, child: Container( width: width * bufferProgress.clamp(0.0, 1.0), height: 3, decoration: BoxDecoration( color: const Color(0xFF4A7A7A), borderRadius: BorderRadius.circular(2), ), ), ), // 已播放 Positioned( left: 0, top: 10.5, child: Container( width: width * displayProgress.clamp(0.0, 1.0), height: 3, decoration: BoxDecoration( color: const Color(0xFFB8D4D0), borderRadius: BorderRadius.circular(2), ), ), ), Positioned.fill( child: SliderTheme( data: SliderTheme.of(context).copyWith( trackHeight: 0, activeTrackColor: Colors.transparent, inactiveTrackColor: Colors.transparent, thumbColor: Colors.transparent, overlayColor: Colors.transparent, thumbShape: const RoundSliderThumbShape( enabledThumbRadius: 0, ), ), child: Slider( value: displayProgress.clamp(0.0, 1.0), min: 0.0, max: 1.0, onChanged: (value) { setState(() => _dragProgress = value); }, onChangeEnd: (value) { final newPosition = Duration( milliseconds: (value * duration.inMilliseconds) .round(), ); service.seekTo(newPosition); setState(() => _dragProgress = null); }, ), ), ), ], ); }, ), ), ), const SizedBox(width: 10), Text( _formatDuration(duration), style: TextStyle(fontSize: 12, color: Colors.grey[500]), ), ], ); }, ); }, ); } Widget _buildControlButtons( AudioService service, Song song, bool isPlaying) { return Row( mainAxisAlignment: MainAxisAlignment.spaceBetween, children: [ IconButton( onPressed: service.hasQueue ? service.previous : null, icon: const Icon(Icons.skip_previous_rounded, size: 34), color: service.hasQueue ? Colors.white70 : Colors.grey[700], ), Container( width: 68, height: 68, decoration: const BoxDecoration( color: Color(0xFFB8D4D0), shape: BoxShape.circle, ), child: IconButton( icon: Icon( isPlaying ? Icons.pause_rounded : Icons.play_arrow_rounded, color: Colors.black87, size: 36, ), padding: EdgeInsets.zero, onPressed: service.togglePlay, ), ), IconButton( onPressed: service.hasQueue ? service.next : null, icon: const Icon(Icons.skip_next_rounded, size: 34), color: service.hasQueue ? Colors.white70 : Colors.grey[700], ), IconButton( onPressed: service.togglePlayMode, icon: Icon( service.playModeIcon, color: const Color(0xFFB8D4D0), size: 26, ), tooltip: '播放模式', ), IconButton( onPressed: _openPlaylist, icon: const Icon( Icons.queue_music_rounded, color: Color(0xFFB8D4D0), size: 26, ), tooltip: '播放队列', ), ], ); } }