Improve settings and playback controls
This commit is contained in:
@@ -1,7 +1,9 @@
|
||||
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';
|
||||
@@ -13,11 +15,11 @@ class PlayerPage extends StatefulWidget {
|
||||
final String roomId;
|
||||
|
||||
const PlayerPage({
|
||||
Key? key,
|
||||
super.key,
|
||||
required this.title,
|
||||
required this.playbackUrl,
|
||||
required this.roomId,
|
||||
}) : super(key: key);
|
||||
});
|
||||
|
||||
@override
|
||||
_PlayerPageState createState() => _PlayerPageState();
|
||||
@@ -32,6 +34,10 @@ class _PlayerPageState extends State<PlayerPage> {
|
||||
|
||||
bool _isError = false;
|
||||
String? _errorMessage;
|
||||
bool _showDanmaku = true;
|
||||
bool _isRefreshing = false;
|
||||
bool _isFullscreen = false;
|
||||
int _playerVersion = 0;
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
@@ -42,7 +48,7 @@ class _PlayerPageState extends State<PlayerPage> {
|
||||
_initializeChat();
|
||||
}
|
||||
|
||||
void _initializePlayer() async {
|
||||
Future<void> _initializePlayer() async {
|
||||
_controller = VideoPlayerController.networkUrl(
|
||||
Uri.parse(widget.playbackUrl),
|
||||
);
|
||||
@@ -51,11 +57,21 @@ class _PlayerPageState extends State<PlayerPage> {
|
||||
_controller!.play();
|
||||
if (mounted) setState(() {});
|
||||
} catch (e) {
|
||||
if (mounted)
|
||||
if (mounted) {
|
||||
setState(() {
|
||||
_isError = true;
|
||||
_errorMessage = e.toString();
|
||||
_isRefreshing = false;
|
||||
});
|
||||
}
|
||||
return;
|
||||
}
|
||||
if (mounted) {
|
||||
setState(() {
|
||||
_isError = false;
|
||||
_errorMessage = null;
|
||||
_isRefreshing = false;
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
@@ -72,7 +88,9 @@ class _PlayerPageState extends State<PlayerPage> {
|
||||
setState(() {
|
||||
_messages.insert(0, msg);
|
||||
if (!msg.isHistory && (msg.type == "chat" || msg.type == "danmaku")) {
|
||||
_addDanmaku(msg.content);
|
||||
if (_showDanmaku) {
|
||||
_addDanmaku(msg.content);
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
@@ -107,8 +125,73 @@ class _PlayerPageState extends State<PlayerPage> {
|
||||
}
|
||||
}
|
||||
|
||||
Future<void> _refreshPlayer() async {
|
||||
if (_isRefreshing) {
|
||||
return;
|
||||
}
|
||||
|
||||
setState(() {
|
||||
_isRefreshing = true;
|
||||
_isError = false;
|
||||
_errorMessage = null;
|
||||
_danmakus.clear();
|
||||
_playerVersion++;
|
||||
});
|
||||
|
||||
if (kIsWeb) {
|
||||
await Future<void>.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<void> _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();
|
||||
@@ -131,13 +214,7 @@ class _PlayerPageState extends State<PlayerPage> {
|
||||
crossAxisAlignment: CrossAxisAlignment.stretch,
|
||||
children: [
|
||||
// 左侧视频区 (占比 75%)
|
||||
Expanded(
|
||||
flex: 3,
|
||||
child: Container(
|
||||
color: Colors.black,
|
||||
child: _buildVideoWithDanmaku(),
|
||||
),
|
||||
),
|
||||
Expanded(flex: 3, child: _buildVideoPanel()),
|
||||
// 右侧聊天区 (占比 25%)
|
||||
Container(
|
||||
width: 350,
|
||||
@@ -156,20 +233,28 @@ class _PlayerPageState extends State<PlayerPage> {
|
||||
Widget _buildMobileLayout() {
|
||||
return Column(
|
||||
children: [
|
||||
// 上方视频区
|
||||
Container(
|
||||
color: Colors.black,
|
||||
SizedBox(
|
||||
height: 310,
|
||||
width: double.infinity,
|
||||
height: 250,
|
||||
child: _buildVideoWithDanmaku(),
|
||||
child: _buildVideoPanel(),
|
||||
),
|
||||
// 下方聊天区
|
||||
Expanded(child: _buildChatSection()),
|
||||
],
|
||||
);
|
||||
}
|
||||
|
||||
// 抽离视频播放器与弹幕组件
|
||||
Widget _buildVideoPanel() {
|
||||
return Container(
|
||||
color: Colors.black,
|
||||
child: Column(
|
||||
children: [
|
||||
Expanded(child: _buildVideoWithDanmaku()),
|
||||
_buildPlaybackControls(),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildVideoWithDanmaku() {
|
||||
return Stack(
|
||||
children: [
|
||||
@@ -180,7 +265,10 @@ class _PlayerPageState extends State<PlayerPage> {
|
||||
style: TextStyle(color: Colors.white),
|
||||
)
|
||||
: kIsWeb
|
||||
? WebStreamPlayer(streamUrl: widget.playbackUrl)
|
||||
? WebStreamPlayer(
|
||||
key: ValueKey('web-player-$_playerVersion'),
|
||||
streamUrl: widget.playbackUrl,
|
||||
)
|
||||
: _controller != null && _controller!.value.isInitialized
|
||||
? AspectRatio(
|
||||
aspectRatio: _controller!.value.aspectRatio,
|
||||
@@ -188,19 +276,91 @@ class _PlayerPageState extends State<PlayerPage> {
|
||||
)
|
||||
: CircularProgressIndicator(),
|
||||
),
|
||||
// 弹幕层使用 ClipRect 裁剪,防止飘出视频区域
|
||||
ClipRect(child: Stack(children: _danmakus)),
|
||||
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.surfaceVariant,
|
||||
color: Theme.of(context).colorScheme.surfaceContainerHighest,
|
||||
child: Row(
|
||||
children: [
|
||||
Icon(Icons.chat_bubble_outline, size: 16),
|
||||
@@ -283,11 +443,11 @@ class _DanmakuItem extends StatefulWidget {
|
||||
final VoidCallback onFinished;
|
||||
|
||||
const _DanmakuItem({
|
||||
Key? key,
|
||||
super.key,
|
||||
required this.text,
|
||||
required this.top,
|
||||
required this.onFinished,
|
||||
}) : super(key: key);
|
||||
});
|
||||
|
||||
@override
|
||||
__DanmakuItemState createState() => __DanmakuItemState();
|
||||
|
||||
Reference in New Issue
Block a user