二次开发源码提交
This commit is contained in:
@@ -0,0 +1,764 @@
|
||||
import 'dart:async';
|
||||
import 'dart:io';
|
||||
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter/services.dart';
|
||||
import 'package:media_kit/media_kit.dart';
|
||||
import 'package:media_kit_video/media_kit_video.dart';
|
||||
import 'package:window_manager/window_manager.dart';
|
||||
import '../../../../core/utils/video_fullscreen.dart';
|
||||
|
||||
/// 自定义视频控制栏叠加层(Bilibili 风格)
|
||||
class VideoControlsOverlay extends StatefulWidget {
|
||||
final VideoState state;
|
||||
final String title;
|
||||
|
||||
const VideoControlsOverlay({super.key, required this.state, required this.title});
|
||||
|
||||
@override
|
||||
State<VideoControlsOverlay> createState() => _VideoControlsOverlayState();
|
||||
}
|
||||
|
||||
class _VideoControlsOverlayState extends State<VideoControlsOverlay> with WindowListener {
|
||||
Player get player => widget.state.widget.controller.player;
|
||||
|
||||
bool _controlsVisible = true;
|
||||
Timer? _hideTimer;
|
||||
bool _isLongPressing = false;
|
||||
double _rateBeforeLongPress = 1.0;
|
||||
|
||||
// 进度条拖拽状态
|
||||
bool _isSeeking = false;
|
||||
Duration _seekPosition = Duration.zero;
|
||||
|
||||
// 音量控制
|
||||
bool _volumeSliderVisible = false;
|
||||
double _volumeBeforeMute = 100.0;
|
||||
|
||||
// 自管全屏状态
|
||||
bool _isFullscreen = false;
|
||||
|
||||
// 键盘焦点
|
||||
final FocusNode _focusNode = FocusNode();
|
||||
|
||||
// 桌面端鼠标悬停
|
||||
Timer? _mouseHoverTimer;
|
||||
|
||||
bool get _isDesktop => Platform.isWindows || Platform.isLinux || Platform.isMacOS;
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
_startHideTimer();
|
||||
if (_isDesktop) {
|
||||
windowManager.addListener(this);
|
||||
}
|
||||
WidgetsBinding.instance.addPostFrameCallback((_) {
|
||||
if (mounted) _focusNode.requestFocus();
|
||||
});
|
||||
}
|
||||
|
||||
@override
|
||||
void dispose() {
|
||||
_hideTimer?.cancel();
|
||||
_mouseHoverTimer?.cancel();
|
||||
_focusNode.dispose();
|
||||
if (_isDesktop) {
|
||||
windowManager.removeListener(this);
|
||||
}
|
||||
if (_isFullscreen) {
|
||||
_doExitFullscreen();
|
||||
}
|
||||
super.dispose();
|
||||
}
|
||||
|
||||
// ========== WindowListener(桌面端) ==========
|
||||
|
||||
@override
|
||||
void onWindowEnterFullScreen() {
|
||||
if (!_isFullscreen && mounted) {
|
||||
setState(() => _isFullscreen = true);
|
||||
}
|
||||
}
|
||||
|
||||
@override
|
||||
void onWindowLeaveFullScreen() {
|
||||
if (_isFullscreen && mounted) {
|
||||
setState(() => _isFullscreen = false);
|
||||
videoFullscreenNotifier.value = false;
|
||||
}
|
||||
}
|
||||
|
||||
// ========== 全屏控制 ==========
|
||||
|
||||
Future<void> _toggleFullscreen() async {
|
||||
if (_isFullscreen) {
|
||||
await _doExitFullscreen();
|
||||
} else {
|
||||
await _doEnterFullscreen();
|
||||
}
|
||||
if (mounted) setState(() {});
|
||||
}
|
||||
|
||||
Future<void> _doEnterFullscreen() async {
|
||||
_isFullscreen = true;
|
||||
if (_isDesktop) {
|
||||
videoFullscreenNotifier.value = true;
|
||||
await windowManager.setFullScreen(true);
|
||||
} else {
|
||||
await Future.wait([
|
||||
SystemChrome.setEnabledSystemUIMode(SystemUiMode.immersiveSticky, overlays: []),
|
||||
SystemChrome.setPreferredOrientations([
|
||||
DeviceOrientation.landscapeLeft,
|
||||
DeviceOrientation.landscapeRight,
|
||||
]),
|
||||
]);
|
||||
}
|
||||
}
|
||||
|
||||
Future<void> _doExitFullscreen() async {
|
||||
_isFullscreen = false;
|
||||
if (_isDesktop) {
|
||||
await windowManager.setFullScreen(false);
|
||||
videoFullscreenNotifier.value = false;
|
||||
} else {
|
||||
await Future.wait([
|
||||
SystemChrome.setEnabledSystemUIMode(SystemUiMode.manual, overlays: SystemUiOverlay.values),
|
||||
SystemChrome.setPreferredOrientations([
|
||||
DeviceOrientation.portraitUp,
|
||||
DeviceOrientation.portraitDown,
|
||||
]),
|
||||
]);
|
||||
}
|
||||
}
|
||||
|
||||
void _onBack() {
|
||||
if (_isFullscreen) {
|
||||
_doExitFullscreen().then((_) {
|
||||
if (mounted) setState(() {});
|
||||
});
|
||||
} else {
|
||||
Navigator.of(context).pop();
|
||||
}
|
||||
}
|
||||
|
||||
// ========== 控制栏显隐 ==========
|
||||
|
||||
void _startHideTimer() {
|
||||
_hideTimer?.cancel();
|
||||
_hideTimer = Timer(const Duration(seconds: 3), () {
|
||||
if (mounted && !_isLongPressing) {
|
||||
_hideControls();
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
void _hideControls() {
|
||||
setState(() {
|
||||
_controlsVisible = false;
|
||||
_volumeSliderVisible = false;
|
||||
});
|
||||
}
|
||||
|
||||
void _showControls() {
|
||||
setState(() {
|
||||
_controlsVisible = true;
|
||||
});
|
||||
_startHideTimer();
|
||||
}
|
||||
|
||||
void _toggleControls() {
|
||||
if (_controlsVisible) {
|
||||
_hideControls();
|
||||
} else {
|
||||
_showControls();
|
||||
}
|
||||
}
|
||||
|
||||
void _onLongPressStart() {
|
||||
_hideTimer?.cancel();
|
||||
_rateBeforeLongPress = player.state.rate;
|
||||
player.setRate(2.0);
|
||||
setState(() {
|
||||
_isLongPressing = true;
|
||||
_controlsVisible = false;
|
||||
_volumeSliderVisible = false;
|
||||
});
|
||||
}
|
||||
|
||||
void _onLongPressEnd() {
|
||||
player.setRate(_rateBeforeLongPress);
|
||||
setState(() {
|
||||
_isLongPressing = false;
|
||||
_controlsVisible = true;
|
||||
});
|
||||
_startHideTimer();
|
||||
}
|
||||
|
||||
// ========== 桌面端鼠标悬停 ==========
|
||||
|
||||
void _onMouseHover(PointerHoverEvent event) {
|
||||
if (!_controlsVisible) {
|
||||
setState(() => _controlsVisible = true);
|
||||
}
|
||||
_hideTimer?.cancel();
|
||||
_mouseHoverTimer?.cancel();
|
||||
_mouseHoverTimer = Timer(const Duration(seconds: 1), () {
|
||||
if (mounted && !_isLongPressing && !_volumeSliderVisible && !_isSeeking) {
|
||||
_hideControls();
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
void _onMouseExit(PointerExitEvent event) {
|
||||
_mouseHoverTimer?.cancel();
|
||||
_startHideTimer();
|
||||
}
|
||||
|
||||
// ========== 键盘快捷键 ==========
|
||||
|
||||
void _onKeyEvent(KeyEvent event) {
|
||||
if (event is! KeyDownEvent) return;
|
||||
final key = event.logicalKey;
|
||||
if (key == LogicalKeyboardKey.space) {
|
||||
player.playOrPause();
|
||||
} else if (key == LogicalKeyboardKey.keyM) {
|
||||
_toggleMute();
|
||||
} else if (key == LogicalKeyboardKey.enter || key == LogicalKeyboardKey.numpadEnter) {
|
||||
_toggleFullscreen();
|
||||
} else if (key == LogicalKeyboardKey.escape) {
|
||||
if (_isFullscreen) _onBack();
|
||||
} else if (key == LogicalKeyboardKey.arrowLeft) {
|
||||
final pos = player.state.position - const Duration(seconds: 5);
|
||||
player.seek(pos < Duration.zero ? Duration.zero : pos);
|
||||
} else if (key == LogicalKeyboardKey.arrowRight) {
|
||||
final pos = player.state.position + const Duration(seconds: 5);
|
||||
final dur = player.state.duration;
|
||||
player.seek(pos > dur ? dur : pos);
|
||||
} else if (key == LogicalKeyboardKey.arrowUp) {
|
||||
final vol = (player.state.volume + 5).clamp(0.0, 100.0);
|
||||
player.setVolume(vol);
|
||||
} else if (key == LogicalKeyboardKey.arrowDown) {
|
||||
final vol = (player.state.volume - 5).clamp(0.0, 100.0);
|
||||
player.setVolume(vol);
|
||||
}
|
||||
}
|
||||
|
||||
void _toggleMute() {
|
||||
if (player.state.volume <= 0) {
|
||||
player.setVolume(_volumeBeforeMute);
|
||||
} else {
|
||||
_volumeBeforeMute = player.state.volume;
|
||||
player.setVolume(0);
|
||||
}
|
||||
}
|
||||
|
||||
// ========== 音量图标点击 ==========
|
||||
|
||||
void _onVolumeIconTap(double currentVolume) {
|
||||
if (_volumeSliderVisible) {
|
||||
// 滑条已展开,再次点击小喇叭 → 静音
|
||||
if (currentVolume <= 0) {
|
||||
player.setVolume(_volumeBeforeMute);
|
||||
} else {
|
||||
_volumeBeforeMute = currentVolume;
|
||||
player.setVolume(0);
|
||||
}
|
||||
} else {
|
||||
// 滑条未展开,展开滑条
|
||||
_hideTimer?.cancel();
|
||||
setState(() => _volumeSliderVisible = true);
|
||||
}
|
||||
}
|
||||
|
||||
// ========== 倍速菜单 ==========
|
||||
|
||||
void _showSpeedMenu() {
|
||||
_hideTimer?.cancel();
|
||||
final rates = [0.5, 0.75, 1.0, 1.25, 1.5, 2.0, 2.5, 3.0];
|
||||
final currentRate = player.state.rate;
|
||||
|
||||
showModalBottomSheet(
|
||||
context: context,
|
||||
backgroundColor: Colors.black87,
|
||||
shape: const RoundedRectangleBorder(
|
||||
borderRadius: BorderRadius.vertical(top: Radius.circular(12)),
|
||||
),
|
||||
builder: (ctx) => SafeArea(
|
||||
child: Column(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
Padding(
|
||||
padding: const EdgeInsets.all(12),
|
||||
child: Text('倍速播放',
|
||||
style: Theme.of(context).textTheme.titleSmall?.copyWith(
|
||||
color: Colors.white70,
|
||||
)),
|
||||
),
|
||||
const Divider(height: 1, color: Colors.white24),
|
||||
Wrap(
|
||||
children: rates.map((rate) {
|
||||
final selected = (rate - currentRate).abs() < 0.01;
|
||||
return SizedBox(
|
||||
width: (MediaQuery.of(context).size.width - 32) / 4,
|
||||
child: ListTile(
|
||||
title: Text(
|
||||
'${rate}x',
|
||||
textAlign: TextAlign.center,
|
||||
style: TextStyle(
|
||||
color: selected ? const Color(0xFFE94560) : Colors.white,
|
||||
fontWeight: selected ? FontWeight.bold : FontWeight.normal,
|
||||
fontSize: 14,
|
||||
),
|
||||
),
|
||||
onTap: () {
|
||||
player.setRate(rate);
|
||||
Navigator.of(ctx).pop();
|
||||
_startHideTimer();
|
||||
},
|
||||
),
|
||||
);
|
||||
}).toList(),
|
||||
),
|
||||
const SizedBox(height: 8),
|
||||
],
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
// ========== 工具方法 ==========
|
||||
|
||||
String _formatDuration(Duration d) {
|
||||
final h = d.inHours;
|
||||
final m = d.inMinutes.remainder(60).toString().padLeft(2, '0');
|
||||
final s = d.inSeconds.remainder(60).toString().padLeft(2, '0');
|
||||
return h > 0 ? '$h:$m:$s' : '$m:$s';
|
||||
}
|
||||
|
||||
// ========== 构建 UI ==========
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
Widget overlay = GestureDetector(
|
||||
behavior: HitTestBehavior.translucent,
|
||||
onTap: _toggleControls,
|
||||
onDoubleTap: () => player.playOrPause(),
|
||||
onLongPressStart: (_) => _onLongPressStart(),
|
||||
onLongPressEnd: (_) => _onLongPressEnd(),
|
||||
child: Stack(
|
||||
fit: StackFit.expand,
|
||||
children: [
|
||||
// 长按2倍速提示
|
||||
if (_isLongPressing)
|
||||
Center(
|
||||
child: Container(
|
||||
padding: const EdgeInsets.symmetric(horizontal: 20, vertical: 10),
|
||||
decoration: BoxDecoration(
|
||||
color: Colors.black54,
|
||||
borderRadius: BorderRadius.circular(8),
|
||||
),
|
||||
child: Row(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
const Icon(Icons.fast_forward, color: Colors.white, size: 20),
|
||||
const SizedBox(width: 8),
|
||||
Text(
|
||||
'2.0x 快进中',
|
||||
style: TextStyle(
|
||||
color: Colors.white,
|
||||
fontSize: 16,
|
||||
fontWeight: FontWeight.w600,
|
||||
shadows: [Shadow(color: Colors.black.withValues(alpha: 0.5), blurRadius: 4)],
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
|
||||
// Buffering 指示器
|
||||
StreamBuilder<bool>(
|
||||
stream: player.stream.buffering,
|
||||
builder: (context, snapshot) {
|
||||
final buffering = snapshot.data ?? false;
|
||||
if (!buffering) return const SizedBox.shrink();
|
||||
return const Center(
|
||||
child: CircularProgressIndicator(color: Colors.white70),
|
||||
);
|
||||
},
|
||||
),
|
||||
|
||||
// 控制栏
|
||||
AnimatedOpacity(
|
||||
opacity: _controlsVisible ? 1.0 : 0.0,
|
||||
duration: const Duration(milliseconds: 250),
|
||||
child: IgnorePointer(
|
||||
ignoring: !_controlsVisible,
|
||||
child: Column(
|
||||
children: [
|
||||
_buildTopBar(),
|
||||
const Spacer(),
|
||||
_buildBottomBar(),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
|
||||
// 桌面端快捷键提示
|
||||
if (_isDesktop)
|
||||
AnimatedOpacity(
|
||||
opacity: _controlsVisible ? 0.4 : 0.0,
|
||||
duration: const Duration(milliseconds: 250),
|
||||
child: IgnorePointer(
|
||||
ignoring: !_controlsVisible,
|
||||
child: _buildShortcutsHint(),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
|
||||
// 桌面端:鼠标悬停 + 键盘
|
||||
if (_isDesktop) {
|
||||
overlay = MouseRegion(
|
||||
onHover: _onMouseHover,
|
||||
onExit: _onMouseExit,
|
||||
child: KeyboardListener(
|
||||
focusNode: _focusNode,
|
||||
onKeyEvent: _onKeyEvent,
|
||||
child: overlay,
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
return PopScope(
|
||||
canPop: !_isFullscreen,
|
||||
onPopInvokedWithResult: (didPop, _) {
|
||||
if (!didPop) _onBack();
|
||||
},
|
||||
child: overlay,
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildTopBar() {
|
||||
return Container(
|
||||
decoration: const BoxDecoration(
|
||||
gradient: LinearGradient(
|
||||
begin: Alignment.topCenter,
|
||||
end: Alignment.bottomCenter,
|
||||
colors: [Colors.black54, Colors.transparent],
|
||||
),
|
||||
),
|
||||
padding: EdgeInsets.only(
|
||||
top: MediaQuery.of(context).padding.top + 4,
|
||||
left: 4,
|
||||
right: 4,
|
||||
bottom: 8,
|
||||
),
|
||||
child: Row(
|
||||
children: [
|
||||
IconButton(
|
||||
icon: const Icon(Icons.arrow_back, color: Colors.white),
|
||||
onPressed: _onBack,
|
||||
),
|
||||
Expanded(
|
||||
child: Text(
|
||||
widget.title,
|
||||
maxLines: 1,
|
||||
overflow: TextOverflow.ellipsis,
|
||||
style: const TextStyle(
|
||||
color: Colors.white,
|
||||
fontSize: 14,
|
||||
fontWeight: FontWeight.w500,
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildBottomBar() {
|
||||
return Container(
|
||||
decoration: const BoxDecoration(
|
||||
gradient: LinearGradient(
|
||||
begin: Alignment.bottomCenter,
|
||||
end: Alignment.topCenter,
|
||||
colors: [Colors.black54, Colors.transparent],
|
||||
),
|
||||
),
|
||||
padding: const EdgeInsets.only(left: 8, right: 8, bottom: 4),
|
||||
child: Column(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
_buildSeekBar(),
|
||||
Padding(
|
||||
padding: EdgeInsets.only(
|
||||
bottom: MediaQuery.of(context).padding.bottom,
|
||||
),
|
||||
child: _buildControlsRow(),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildControlsRow() {
|
||||
return Row(
|
||||
children: [
|
||||
// 播放/暂停
|
||||
StreamBuilder<bool>(
|
||||
stream: player.stream.playing,
|
||||
builder: (context, snapshot) {
|
||||
final playing = snapshot.data ?? false;
|
||||
return IconButton(
|
||||
icon: Icon(playing ? Icons.pause : Icons.play_arrow, color: Colors.white),
|
||||
onPressed: () => player.playOrPause(),
|
||||
);
|
||||
},
|
||||
),
|
||||
// 音量控制
|
||||
_buildVolumeControl(),
|
||||
// 时间
|
||||
StreamBuilder<Duration>(
|
||||
stream: player.stream.position,
|
||||
builder: (context, posSnapshot) {
|
||||
return StreamBuilder<Duration>(
|
||||
stream: player.stream.duration,
|
||||
builder: (context, durSnapshot) {
|
||||
final pos = posSnapshot.data ?? Duration.zero;
|
||||
final dur = durSnapshot.data ?? Duration.zero;
|
||||
return Padding(
|
||||
padding: const EdgeInsets.only(left: 8),
|
||||
child: Text(
|
||||
'${_formatDuration(pos)} / ${_formatDuration(dur)}',
|
||||
style: const TextStyle(color: Colors.white70, fontSize: 12),
|
||||
),
|
||||
);
|
||||
},
|
||||
);
|
||||
},
|
||||
),
|
||||
const Spacer(),
|
||||
// 倍速按钮
|
||||
StreamBuilder<double>(
|
||||
stream: player.stream.rate,
|
||||
builder: (context, snapshot) {
|
||||
final rate = snapshot.data ?? 1.0;
|
||||
return GestureDetector(
|
||||
onTap: _showSpeedMenu,
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.symmetric(horizontal: 10, vertical: 6),
|
||||
child: Text(
|
||||
'${rate}x',
|
||||
style: const TextStyle(
|
||||
color: Colors.white,
|
||||
fontSize: 13,
|
||||
fontWeight: FontWeight.w600,
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
},
|
||||
),
|
||||
// 全屏按钮
|
||||
IconButton(
|
||||
icon: Icon(
|
||||
_isFullscreen ? Icons.fullscreen_exit : Icons.fullscreen,
|
||||
color: Colors.white,
|
||||
),
|
||||
onPressed: _toggleFullscreen,
|
||||
),
|
||||
],
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildVolumeControl() {
|
||||
return StreamBuilder<double>(
|
||||
stream: player.stream.volume,
|
||||
builder: (context, snapshot) {
|
||||
final volume = snapshot.data ?? 100.0;
|
||||
final isMuted = volume <= 0;
|
||||
|
||||
IconData volumeIcon;
|
||||
if (isMuted) {
|
||||
volumeIcon = Icons.volume_off;
|
||||
} else if (volume < 50) {
|
||||
volumeIcon = Icons.volume_down;
|
||||
} else {
|
||||
volumeIcon = Icons.volume_up;
|
||||
}
|
||||
|
||||
return Row(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
GestureDetector(
|
||||
onTap: () => _onVolumeIconTap(volume),
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.all(8),
|
||||
child: Icon(volumeIcon, color: Colors.white, size: 22),
|
||||
),
|
||||
),
|
||||
AnimatedSize(
|
||||
duration: const Duration(milliseconds: 200),
|
||||
curve: Curves.easeInOut,
|
||||
alignment: Alignment.centerLeft,
|
||||
child: _volumeSliderVisible
|
||||
? SizedBox(
|
||||
width: 100,
|
||||
child: SliderTheme(
|
||||
data: SliderThemeData(
|
||||
trackHeight: 2,
|
||||
thumbShape: const RoundSliderThumbShape(enabledThumbRadius: 5),
|
||||
overlayShape: const RoundSliderOverlayShape(overlayRadius: 10),
|
||||
activeTrackColor: const Color(0xFFE94560),
|
||||
inactiveTrackColor: Colors.white24,
|
||||
thumbColor: const Color(0xFFE94560),
|
||||
),
|
||||
child: Slider(
|
||||
value: volume.clamp(0.0, 100.0),
|
||||
min: 0,
|
||||
max: 100,
|
||||
onChanged: (v) {
|
||||
_hideTimer?.cancel();
|
||||
_mouseHoverTimer?.cancel();
|
||||
player.setVolume(v);
|
||||
},
|
||||
onChangeEnd: (_) {
|
||||
_startHideTimer();
|
||||
if (_isDesktop) _startMouseHoverTimer();
|
||||
},
|
||||
),
|
||||
),
|
||||
)
|
||||
: const SizedBox(width: 0),
|
||||
),
|
||||
],
|
||||
);
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
void _startMouseHoverTimer() {
|
||||
_mouseHoverTimer?.cancel();
|
||||
_mouseHoverTimer = Timer(const Duration(seconds: 1), () {
|
||||
if (mounted && !_isLongPressing && !_volumeSliderVisible && !_isSeeking) {
|
||||
_hideControls();
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
Widget _buildSeekBar() {
|
||||
return StreamBuilder<Duration>(
|
||||
stream: player.stream.position,
|
||||
builder: (context, posSnapshot) {
|
||||
return StreamBuilder<Duration>(
|
||||
stream: player.stream.duration,
|
||||
builder: (context, durSnapshot) {
|
||||
final position = posSnapshot.data ?? Duration.zero;
|
||||
final duration = durSnapshot.data ?? Duration.zero;
|
||||
|
||||
final displayPos = _isSeeking ? _seekPosition : position;
|
||||
final value = duration.inMilliseconds > 0
|
||||
? displayPos.inMilliseconds / duration.inMilliseconds
|
||||
: 0.0;
|
||||
|
||||
return SizedBox(
|
||||
height: 20,
|
||||
child: SliderTheme(
|
||||
data: SliderThemeData(
|
||||
trackHeight: 2,
|
||||
thumbShape: const RoundSliderThumbShape(enabledThumbRadius: 6),
|
||||
overlayShape: const RoundSliderOverlayShape(overlayRadius: 12),
|
||||
activeTrackColor: const Color(0xFFE94560),
|
||||
inactiveTrackColor: Colors.white24,
|
||||
thumbColor: const Color(0xFFE94560),
|
||||
),
|
||||
child: Slider(
|
||||
value: value.clamp(0.0, 1.0),
|
||||
onChanged: (v) {
|
||||
_hideTimer?.cancel();
|
||||
_mouseHoverTimer?.cancel();
|
||||
setState(() {
|
||||
_isSeeking = true;
|
||||
_seekPosition = Duration(
|
||||
milliseconds: (duration.inMilliseconds * v).round(),
|
||||
);
|
||||
});
|
||||
},
|
||||
onChangeEnd: (v) {
|
||||
final seekTo = Duration(
|
||||
milliseconds: (duration.inMilliseconds * v).round(),
|
||||
);
|
||||
player.seek(seekTo);
|
||||
setState(() => _isSeeking = false);
|
||||
_startHideTimer();
|
||||
if (_isDesktop) _startMouseHoverTimer();
|
||||
},
|
||||
),
|
||||
),
|
||||
);
|
||||
},
|
||||
);
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildShortcutsHint() {
|
||||
const shortcuts = [
|
||||
('Space', '暂停/播放'),
|
||||
('M', '静音'),
|
||||
('Enter', '全屏'),
|
||||
('Esc', '退出全屏'),
|
||||
('←→', '快退/快进 5s'),
|
||||
('↑↓', '音量 ±5'),
|
||||
];
|
||||
return Align(
|
||||
alignment: Alignment.centerRight,
|
||||
child: Padding(
|
||||
padding: EdgeInsets.only(
|
||||
right: 12,
|
||||
top: MediaQuery.of(context).padding.top + 48,
|
||||
),
|
||||
child: Container(
|
||||
padding: const EdgeInsets.symmetric(horizontal: 10, vertical: 8),
|
||||
decoration: BoxDecoration(
|
||||
color: Colors.black38,
|
||||
borderRadius: BorderRadius.circular(6),
|
||||
),
|
||||
child: Column(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: shortcuts.map((s) {
|
||||
return Padding(
|
||||
padding: const EdgeInsets.symmetric(vertical: 1.5),
|
||||
child: Row(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
SizedBox(
|
||||
width: 42,
|
||||
child: Text(
|
||||
s.$1,
|
||||
style: const TextStyle(
|
||||
color: Colors.white,
|
||||
fontSize: 10,
|
||||
fontWeight: FontWeight.w600,
|
||||
fontFamily: 'monospace',
|
||||
),
|
||||
),
|
||||
),
|
||||
Text(
|
||||
s.$2,
|
||||
style: const TextStyle(color: Colors.white70, fontSize: 10),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}).toList(),
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user