import 'dart:io'; // 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'; import '../services/favorite_events.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); FavoriteEvents().publish(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(_isCompact), 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, compact: _isCompact), if (!_isCompact) _buildLyricsPage(song), ], ), ), ); } /// 小窗/分屏:宽高比明显小于全屏(1368x3192 → 2.33;1368x2577 → 1.88) bool get _isCompact { final size = MediaQuery.of(context).size; if (size.width <= 0) return false; return size.height / size.width < 2.05; } /// 顶部指示器:当前页对应"长条",另外的页是小点。 /// 全屏三页(信息/主页/歌词),小窗两页(信息/主页)。 Widget _buildPageIndicator(bool compact) { 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), ), ), ), ); } final indices = compact ? const [0, 1] : const [0, 1, 2]; return Row( mainAxisSize: MainAxisSize.min, children: [ for (var i = 0; i < indices.length; i++) ...[ if (i > 0) const SizedBox(width: 6), segment(indices[i]), ], ], ); } Widget _buildMainPage(AudioService service, Song song, bool isPlaying, {required bool compact}) { final screen = MediaQuery.of(context).size; // 小窗:封面改为固定方形(约 30% 可用高度),给控件留出空间 final coverSide = compact ? (screen.height * 0.38).clamp(160.0, 380.0) : null; final artworkPath = song.artworkPath; final hasArtworkFile = artworkPath != null && artworkPath.isNotEmpty && File(artworkPath).existsSync(); final cover = ClipRRect( borderRadius: BorderRadius.circular(compact ? 18 : 24), child: song.artwork != null ? Image.memory( song.artwork!, fit: BoxFit.cover, gaplessPlayback: true, ) : hasArtworkFile ? Image.file( File(artworkPath), fit: BoxFit.cover, gaplessPlayback: true, ) : Container( color: const Color(0xFF2A3332), child: Icon( Icons.music_note, size: compact ? 48 : 72, color: Colors.white24, ), ), ); final padded = Padding( // 顶部留白:与 AppBar/指示器拉开距离,避免封面贴着顶部 padding: EdgeInsets.fromLTRB(24, compact ? 8 : 24, 24, 0), child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ // ---- 封面 ---- if (coverSide != null) Center( child: SizedBox( width: coverSide, height: coverSide, child: cover, ), ) else AspectRatio( aspectRatio: 1, child: cover, ), SizedBox(height: compact ? 12 : 28), // ---- 标题 / 艺术家(收藏心在标题右侧)---- Row( crossAxisAlignment: CrossAxisAlignment.center, children: [ Expanded( child: Text( song.title, style: TextStyle( fontSize: compact ? 20 : 24, fontWeight: FontWeight.w600, color: Colors.white, height: 1.25, ), maxLines: 1, overflow: TextOverflow.ellipsis, ), ), const SizedBox(width: 8), IconButton( onPressed: () => _toggleFavorite(song), tooltip: '收藏', padding: EdgeInsets.zero, constraints: const BoxConstraints(minWidth: 36, minHeight: 36), iconSize: 24, icon: Icon( _isFavorite ? Icons.favorite : Icons.favorite_border, color: _isFavorite ? const Color(0xFFE05C5C) : Colors.white54, size: 24, ), ), ], ), const SizedBox(height: 2), Text( song.artist, style: TextStyle( fontSize: compact ? 14 : 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), SizedBox(height: compact ? 10 : 18), // ---- 控制 ---- _buildControlButtons(service, song, isPlaying), // 底部多留 50px:控制行整体上移 SizedBox(height: compact ? 20 : 45), ], ), ); if (!compact) return padded; // 小窗/横屏:内容超高时可滚动;内容不足时撑满,避免底部大片空白 return LayoutBuilder( builder: (context, constraints) => SingleChildScrollView( child: ConstrainedBox( constraints: BoxConstraints(minHeight: constraints.maxHeight), child: IntrinsicHeight(child: padded), ), ), ); } /// 左页:文件信息 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: '播放队列', ), ], ); } }