diff --git a/backend/internal/api/auth.go b/backend/internal/api/auth.go index 5aeea03..5eabc06 100644 --- a/backend/internal/api/auth.go +++ b/backend/internal/api/auth.go @@ -91,6 +91,7 @@ func Login(c *gin.Context) { } c.JSON(http.StatusOK, gin.H{ - "token": token, + "token": token, + "username": user.Username, }) } diff --git a/frontend/lib/pages/login_page.dart b/frontend/lib/pages/login_page.dart index c83f62c..600707b 100644 --- a/frontend/lib/pages/login_page.dart +++ b/frontend/lib/pages/login_page.dart @@ -32,7 +32,7 @@ class _LoginPageState extends State { final response = await api.login(_usernameController.text, _passwordController.text); if (response.statusCode == 200) { final data = jsonDecode(response.body); - await auth.login(data['token']); + await auth.login(data['token'], data['username']); } else { final error = jsonDecode(response.body)['error'] ?? "Login Failed"; ScaffoldMessenger.of(context).showSnackBar(SnackBar(content: Text(error))); diff --git a/frontend/lib/pages/player_page.dart b/frontend/lib/pages/player_page.dart index 7cffa5c..e8c3f5b 100644 --- a/frontend/lib/pages/player_page.dart +++ b/frontend/lib/pages/player_page.dart @@ -2,6 +2,7 @@ import 'dart:async'; import 'package:flutter/material.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'; @@ -26,7 +27,7 @@ class _PlayerPageState extends State { final ChatService _chatService = ChatService(); final TextEditingController _msgController = TextEditingController(); final List _messages = []; - final List _danmakus = []; // 为简单起见,这里存储弹幕 Widget + final List _danmakus = []; bool _isError = false; String? _errorMessage; @@ -51,7 +52,11 @@ class _PlayerPageState extends State { void _initializeChat() { final settings = context.read(); - _chatService.connect(settings.baseUrl, widget.roomId, "User_${widget.roomId}"); // 暂定用户名 + final auth = context.read(); + + // 使用真实用户名建立连接 + final currentUsername = auth.username ?? "Guest_${widget.roomId}"; + _chatService.connect(settings.baseUrl, widget.roomId, currentUsername); _chatService.messages.listen((msg) { if (mounted) { @@ -67,7 +72,7 @@ class _PlayerPageState extends State { void _addDanmaku(String text) { final id = DateTime.now().millisecondsSinceEpoch; - final top = 20.0 + (id % 5) * 30.0; // 简单的多轨道显示 + final top = 20.0 + (id % 6) * 30.0; final danmaku = _DanmakuItem( key: ValueKey(id), @@ -83,7 +88,8 @@ class _PlayerPageState extends State { void _sendMsg() { if (_msgController.text.isNotEmpty) { - _chatService.sendMessage(_msgController.text, "Me", widget.roomId); + final auth = context.read(); + _chatService.sendMessage(_msgController.text, auth.username ?? "Anonymous", widget.roomId); _msgController.clear(); } } @@ -98,81 +104,139 @@ class _PlayerPageState extends State { @override Widget build(BuildContext context) { + bool isWide = MediaQuery.of(context).size.width > 900; + return Scaffold( appBar: AppBar(title: Text(widget.title)), - body: Column( - children: [ - // 视频播放器 + 弹幕层 - Container( + body: isWide ? _buildWideLayout() : _buildMobileLayout(), + ); + } + + // 宽屏布局:左右分栏 + Widget _buildWideLayout() { + return Row( + crossAxisAlignment: CrossAxisAlignment.stretch, + children: [ + // 左侧视频区 (占比 75%) + Expanded( + flex: 3, + child: Container( color: Colors.black, - width: double.infinity, - height: 250, - child: Stack( - children: [ - Center( - child: _isError - ? Text("Error: $_errorMessage", style: TextStyle(color: Colors.white)) - : _controller.value.isInitialized - ? AspectRatio(aspectRatio: _controller.value.aspectRatio, child: VideoPlayer(_controller)) - : CircularProgressIndicator(), - ), - // 弹幕层 - ..._danmakus, - ], - ), + child: _buildVideoWithDanmaku(), ), - // 评论区标题 - Container( - padding: EdgeInsets.symmetric(horizontal: 16, vertical: 8), - color: Theme.of(context).colorScheme.surfaceVariant, - child: Row( - children: [ - Icon(Icons.chat_bubble_outline, size: 16), - SizedBox(width: 8), - Text("Live Chat", style: TextStyle(fontWeight: FontWeight.bold)), - ], - ), + ), + // 右侧聊天区 (占比 25%) + Container( + width: 350, + decoration: BoxDecoration( + border: Border(left: BorderSide(color: Theme.of(context).dividerColor)), ), - // 消息列表 - Expanded( - child: ListView.builder( - reverse: true, - itemCount: _messages.length, - itemBuilder: (context, index) { - final m = _messages[index]; - return ListTile( - dense: true, - title: Text( - "${m.username}: ${m.content}", - style: TextStyle(color: m.type == "system" ? Colors.blue : null), - ), - ); - }, - ), + child: _buildChatSection(), + ), + ], + ); + } + + // 移动端布局:上下堆叠 + Widget _buildMobileLayout() { + return Column( + children: [ + // 上方视频区 + Container( + color: Colors.black, + width: double.infinity, + height: 250, + child: _buildVideoWithDanmaku(), + ), + // 下方聊天区 + Expanded(child: _buildChatSection()), + ], + ); + } + + // 抽离视频播放器与弹幕组件 + Widget _buildVideoWithDanmaku() { + return Stack( + children: [ + Center( + child: _isError + ? Text("Error: $_errorMessage", style: TextStyle(color: Colors.white)) + : _controller.value.isInitialized + ? AspectRatio( + aspectRatio: _controller.value.aspectRatio, + child: VideoPlayer(_controller), + ) + : CircularProgressIndicator(), + ), + // 弹幕层使用 ClipRect 裁剪,防止飘出视频区域 + ClipRect( + child: Stack(children: _danmakus), + ), + ], + ); + } + + // 抽离聊天区域组件 + Widget _buildChatSection() { + return Column( + children: [ + Container( + padding: EdgeInsets.symmetric(horizontal: 16, vertical: 12), + color: Theme.of(context).colorScheme.surfaceVariant, + child: Row( + children: [ + Icon(Icons.chat_bubble_outline, size: 16), + SizedBox(width: 8), + Text("Live Chat", style: TextStyle(fontWeight: FontWeight.bold)), + ], ), - // 输入框 - Padding( - padding: const EdgeInsets.all(8.0), - child: Row( - children: [ - Expanded( - child: TextField( - controller: _msgController, - decoration: InputDecoration( - hintText: "Say something...", - border: OutlineInputBorder(borderRadius: BorderRadius.circular(20)), - contentPadding: EdgeInsets.symmetric(horizontal: 16), - ), - onSubmitted: (_) => _sendMsg(), + ), + 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), + ], ), ), - SizedBox(width: 8), - IconButton(icon: Icon(Icons.send, color: Colors.blue), onPressed: _sendMsg), - ], - ), + ); + }, ), - ], - ), + ), + 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), + ], + ), + ), + ], ); } } @@ -190,13 +254,15 @@ class _DanmakuItem extends StatefulWidget { class __DanmakuItemState extends State<_DanmakuItem> with SingleTickerProviderStateMixin { late AnimationController _animationController; - late Animation _animation; + late Animation _animation; @override void initState() { super.initState(); - _animationController = AnimationController(duration: const Duration(seconds: 8), vsync: this); - _animation = Tween(begin: Offset(1.5, 0), end: Offset(-1.5, 0)).animate(_animationController); + _animationController = AnimationController(duration: const Duration(seconds: 10), vsync: this); + + // 使用相对位置:从右向左 + _animation = Tween(begin: 1.0, end: -0.5).animate(_animationController); _animationController.forward().then((_) => widget.onFinished()); } @@ -209,16 +275,24 @@ class __DanmakuItemState extends State<_DanmakuItem> with SingleTickerProviderSt @override Widget build(BuildContext context) { - return Positioned( - top: widget.top, - width: 300, - child: SlideTransition( - position: _animation, - child: Text( - widget.text, - style: TextStyle(color: Colors.white, fontWeight: FontWeight.bold, fontSize: 16, shadows: [Shadow(blurRadius: 2, color: Colors.black)]), - ), - ), + 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))], + ), + ), + ); + }, ); } } diff --git a/frontend/lib/providers/auth_provider.dart b/frontend/lib/providers/auth_provider.dart index 5bf4d57..5021d11 100644 --- a/frontend/lib/providers/auth_provider.dart +++ b/frontend/lib/providers/auth_provider.dart @@ -3,35 +3,42 @@ import 'package:shared_preferences/shared_preferences.dart'; class AuthProvider with ChangeNotifier { String? _token; + String? _username; // 新增用户名状态 bool _isAuthenticated = false; bool get isAuthenticated => _isAuthenticated; String? get token => _token; + String? get username => _username; AuthProvider() { - _loadToken(); + _loadAuth(); } - void _loadToken() async { + void _loadAuth() async { final prefs = await SharedPreferences.getInstance(); _token = prefs.getString('token'); + _username = prefs.getString('username'); _isAuthenticated = _token != null; notifyListeners(); } - Future login(String token) async { + Future login(String token, String username) async { _token = token; + _username = username; _isAuthenticated = true; final prefs = await SharedPreferences.getInstance(); await prefs.setString('token', token); + await prefs.setString('username', username); notifyListeners(); } Future logout() async { _token = null; + _username = null; _isAuthenticated = false; final prefs = await SharedPreferences.getInstance(); await prefs.remove('token'); + await prefs.remove('username'); notifyListeners(); } }