import 'package:flutter/foundation.dart'; import 'package:flutter/material.dart'; import 'package:flutter/services.dart'; import 'package:provider/provider.dart'; import 'package:video_player/video_player.dart'; import '../providers/auth_provider.dart'; import '../providers/settings_provider.dart'; import '../services/chat_service.dart'; import '../widgets/web_stream_player.dart'; class PlayerPage extends StatefulWidget { final String title; final String playbackUrl; final String roomId; const PlayerPage({ super.key, required this.title, required this.playbackUrl, required this.roomId, }); @override _PlayerPageState createState() => _PlayerPageState(); } class _PlayerPageState extends State { VideoPlayerController? _controller; final ChatService _chatService = ChatService(); final TextEditingController _msgController = TextEditingController(); final List _messages = []; final List _danmakus = []; bool _isError = false; String? _errorMessage; bool _showDanmaku = true; bool _isRefreshing = false; bool _isFullscreen = false; int _playerVersion = 0; @override void initState() { super.initState(); if (!kIsWeb) { _initializePlayer(); } _initializeChat(); } Future _initializePlayer() async { _controller = VideoPlayerController.networkUrl( Uri.parse(widget.playbackUrl), ); try { await _controller!.initialize(); _controller!.play(); if (mounted) setState(() {}); } catch (e) { if (mounted) { setState(() { _isError = true; _errorMessage = e.toString(); _isRefreshing = false; }); } return; } if (mounted) { setState(() { _isError = false; _errorMessage = null; _isRefreshing = false; }); } } void _initializeChat() { final settings = context.read(); final auth = context.read(); // 使用真实用户名建立连接 final currentUsername = auth.username ?? "Guest_${widget.roomId}"; _chatService.connect(settings.baseUrl, widget.roomId, currentUsername); _chatService.messages.listen((msg) { if (mounted) { setState(() { _messages.insert(0, msg); if (!msg.isHistory && (msg.type == "chat" || msg.type == "danmaku")) { if (_showDanmaku) { _addDanmaku(msg.content); } } }); } }); } void _addDanmaku(String text) { final key = UniqueKey(); final top = 20.0 + (DateTime.now().millisecondsSinceEpoch % 6) * 30.0; final danmaku = _DanmakuItem( key: key, text: text, top: top, onFinished: () { if (mounted) setState(() => _danmakus.removeWhere((w) => w.key == key)); }, ); setState(() => _danmakus.add(danmaku)); } void _sendMsg() { if (_msgController.text.isNotEmpty) { final auth = context.read(); _chatService.sendMessage( _msgController.text, auth.username ?? "Anonymous", widget.roomId, ); _msgController.clear(); } } Future _refreshPlayer() async { if (_isRefreshing) { return; } setState(() { _isRefreshing = true; _isError = false; _errorMessage = null; _danmakus.clear(); _playerVersion++; }); if (kIsWeb) { await Future.delayed(const Duration(milliseconds: 150)); if (mounted) { setState(() => _isRefreshing = false); } return; } if (_controller != null) { await _controller!.dispose(); } _controller = null; if (mounted) { setState(() {}); } await _initializePlayer(); } Future _toggleFullscreen() async { final nextValue = !_isFullscreen; if (!kIsWeb) { await SystemChrome.setEnabledSystemUIMode( nextValue ? SystemUiMode.immersiveSticky : SystemUiMode.edgeToEdge, ); await SystemChrome.setPreferredOrientations( nextValue ? const [ DeviceOrientation.landscapeLeft, DeviceOrientation.landscapeRight, ] : DeviceOrientation.values, ); } if (mounted) { setState(() => _isFullscreen = nextValue); } } void _toggleDanmaku() { setState(() { _showDanmaku = !_showDanmaku; if (!_showDanmaku) { _danmakus.clear(); } }); } @override void dispose() { if (!kIsWeb && _isFullscreen) { SystemChrome.setEnabledSystemUIMode(SystemUiMode.edgeToEdge); SystemChrome.setPreferredOrientations(DeviceOrientation.values); } _controller?.dispose(); _chatService.dispose(); _msgController.dispose(); super.dispose(); } @override Widget build(BuildContext context) { bool isWide = MediaQuery.of(context).size.width > 900; return Scaffold( appBar: AppBar(title: Text(widget.title)), body: isWide ? _buildWideLayout() : _buildMobileLayout(), ); } // 宽屏布局:左右分栏 Widget _buildWideLayout() { return Row( crossAxisAlignment: CrossAxisAlignment.stretch, children: [ // 左侧视频区 (占比 75%) Expanded(flex: 3, child: _buildVideoPanel()), // 右侧聊天区 (占比 25%) Container( width: 350, decoration: BoxDecoration( border: Border( left: BorderSide(color: Theme.of(context).dividerColor), ), ), child: _buildChatSection(), ), ], ); } // 移动端布局:上下堆叠 Widget _buildMobileLayout() { return Column( children: [ SizedBox( height: 310, width: double.infinity, child: _buildVideoPanel(), ), Expanded(child: _buildChatSection()), ], ); } Widget _buildVideoPanel() { return Container( color: Colors.black, child: Column( children: [ Expanded(child: _buildVideoWithDanmaku()), _buildPlaybackControls(), ], ), ); } Widget _buildVideoWithDanmaku() { return Stack( children: [ Center( child: _isError ? Text( "Error: $_errorMessage", style: TextStyle(color: Colors.white), ) : kIsWeb ? WebStreamPlayer( key: ValueKey('web-player-$_playerVersion'), streamUrl: widget.playbackUrl, ) : _controller != null && _controller!.value.isInitialized ? AspectRatio( aspectRatio: _controller!.value.aspectRatio, child: VideoPlayer(_controller!), ) : CircularProgressIndicator(), ), if (_showDanmaku) ClipRect(child: Stack(children: _danmakus)), if (_isRefreshing) const Positioned( top: 16, right: 16, child: SizedBox( width: 24, height: 24, child: CircularProgressIndicator(strokeWidth: 2), ), ), ], ); } Widget _buildPlaybackControls() { return Container( width: double.infinity, padding: const EdgeInsets.symmetric(horizontal: 12, vertical: 10), decoration: BoxDecoration( color: Colors.black.withValues(alpha: 0.92), border: Border( top: BorderSide(color: Colors.white.withValues(alpha: 0.08)), ), ), child: Wrap( spacing: 10, runSpacing: 10, children: [ _buildControlButton( icon: Icons.refresh, label: "Refresh", onPressed: _refreshPlayer, ), _buildControlButton( icon: _showDanmaku ? Icons.subtitles : Icons.subtitles_off, label: _showDanmaku ? "Danmaku On" : "Danmaku Off", onPressed: _toggleDanmaku, ), _buildControlButton( icon: _isFullscreen ? Icons.fullscreen_exit : Icons.fullscreen, label: _isFullscreen ? "Exit Fullscreen" : "Fullscreen", onPressed: _toggleFullscreen, ), _buildControlButton( icon: Icons.high_quality, label: "Resolution", onPressed: () { ScaffoldMessenger.of(context).showSnackBar( const SnackBar( content: Text( "Resolution switching is planned for a later update.", ), ), ); }, ), ], ), ); } Widget _buildControlButton({ required IconData icon, required String label, required VoidCallback onPressed, }) { return FilledButton.tonalIcon( onPressed: onPressed, icon: Icon(icon, size: 18), label: Text(label), style: FilledButton.styleFrom( foregroundColor: Colors.white, backgroundColor: Colors.white.withValues(alpha: 0.12), ), ); } // 抽离聊天区域组件 Widget _buildChatSection() { return Column( children: [ Container( padding: EdgeInsets.symmetric(horizontal: 16, vertical: 12), color: Theme.of(context).colorScheme.surfaceContainerHighest, child: Row( children: [ Icon(Icons.chat_bubble_outline, size: 16), SizedBox(width: 8), Text("Live Chat", style: TextStyle(fontWeight: FontWeight.bold)), ], ), ), Expanded( child: ListView.builder( reverse: true, padding: EdgeInsets.all(8), itemCount: _messages.length, itemBuilder: (context, index) { final m = _messages[index]; return Padding( padding: const EdgeInsets.symmetric(vertical: 4.0), child: RichText( text: TextSpan( style: TextStyle( color: Theme.of(context).textTheme.bodyMedium?.color, ), children: [ TextSpan( text: "${m.username}: ", style: TextStyle( fontWeight: FontWeight.bold, color: m.type == "system" ? Colors.blue : Theme.of(context).colorScheme.primary, ), ), TextSpan(text: m.content), ], ), ), ); }, ), ), Divider(height: 1), Padding( padding: const EdgeInsets.all(8.0), child: Row( children: [ Expanded( child: TextField( controller: _msgController, decoration: InputDecoration( hintText: "Send a message...", border: OutlineInputBorder( borderRadius: BorderRadius.circular(20), ), contentPadding: EdgeInsets.symmetric( horizontal: 16, vertical: 8, ), ), onSubmitted: (_) => _sendMsg(), ), ), IconButton( icon: Icon( Icons.send, color: Theme.of(context).colorScheme.primary, ), onPressed: _sendMsg, ), ], ), ), ], ); } } class _DanmakuItem extends StatefulWidget { final String text; final double top; final VoidCallback onFinished; const _DanmakuItem({ super.key, required this.text, required this.top, required this.onFinished, }); @override __DanmakuItemState createState() => __DanmakuItemState(); } class __DanmakuItemState extends State<_DanmakuItem> with SingleTickerProviderStateMixin { late AnimationController _animationController; late Animation _animation; @override void initState() { super.initState(); _animationController = AnimationController( duration: const Duration(seconds: 10), vsync: this, ); // 使用相对位置:从右向左 _animation = Tween( begin: 1.0, end: -0.5, ).animate(_animationController); _animationController.forward().then((_) => widget.onFinished()); } @override void dispose() { _animationController.dispose(); super.dispose(); } @override Widget build(BuildContext context) { return AnimatedBuilder( animation: _animation, builder: (context, child) { return Positioned( top: widget.top, // left 使用 MediaQuery 获取屏幕宽度进行动态计算 left: MediaQuery.of(context).size.width * _animation.value, child: Text( widget.text, style: TextStyle( color: Colors.white, fontWeight: FontWeight.bold, fontSize: 18, shadows: [ Shadow( blurRadius: 4, color: Colors.black, offset: Offset(1, 1), ), ], ), ), ); }, ); } }