// 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? _metaSongId; 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 = ''; bool favorite = false; try { final row = await db.getSongByPath(song.id); album = (row?['album'] 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; _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, ), 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( child: Padding( padding: const EdgeInsets.symmetric(horizontal: 24), 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), // ---- 标题 / 艺术家 ---- Text( song.title, style: const TextStyle( fontSize: 24, fontWeight: FontWeight.w600, color: Colors.white, height: 1.25, ), maxLines: 2, overflow: TextOverflow.ellipsis, ), const SizedBox(height: 6), Text( song.artist, style: TextStyle(fontSize: 14, color: Colors.grey[400]), maxLines: 1, overflow: TextOverflow.ellipsis, ), // ---- 信息区(专辑 / 目录)---- if (_album.isNotEmpty || _folder.isNotEmpty) ...[ const SizedBox(height: 14), 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), const SizedBox(height: 20), ], ), ), ), ); } 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: Stack( alignment: Alignment.centerLeft, children: [ Container( height: 3, decoration: BoxDecoration( color: Colors.grey[800], borderRadius: BorderRadius.circular(2), ), ), FractionallySizedBox( alignment: Alignment.centerLeft, widthFactor: bufferProgress.clamp(0.0, 1.0), child: Container( height: 3, decoration: BoxDecoration( color: const Color(0xFF4A7A7A), borderRadius: BorderRadius.circular(2), ), ), ), FractionallySizedBox( alignment: Alignment.centerLeft, widthFactor: displayProgress.clamp(0.0, 1.0), child: Container( 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: const Color(0xFFB8D4D0), overlayColor: const Color(0x33B8D4D0), thumbShape: const RoundSliderThumbShape( enabledThumbRadius: 6, ), overlayShape: const RoundSliderOverlayShape( overlayRadius: 14, ), ), 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: () => _toggleFavorite(song), icon: Icon( _isFavorite ? Icons.favorite : Icons.favorite_border, color: _isFavorite ? const Color(0xFFE05C5C) : Colors.white54, size: 26, ), tooltip: '收藏', ), IconButton( onPressed: _openPlaylist, icon: const Icon( Icons.queue_music_rounded, color: Color(0xFFB8D4D0), size: 26, ), tooltip: '播放队列', ), ], ); } }