Initial commit
This commit is contained in:
@@ -0,0 +1,702 @@
|
||||
import 'dart:async';
|
||||
import 'dart:io';
|
||||
|
||||
import 'package:flutter/foundation.dart';
|
||||
import 'package:flutter/gestures.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter/services.dart';
|
||||
import 'package:window_manager/window_manager.dart';
|
||||
|
||||
import '../../../core/infra/player_engine/player_engine.dart';
|
||||
import '../gestures/gesture_action.dart';
|
||||
import '../gestures/gesture_bindings.dart';
|
||||
import 'player_controls_chrome.dart';
|
||||
import 'player_hold_speed_prompt.dart';
|
||||
|
||||
class PlayerVideoWidget extends StatefulWidget {
|
||||
final PlayerEngine player;
|
||||
final ValueListenable<BoxFit> fit;
|
||||
final List<Widget> topBar;
|
||||
final Widget bottomBar;
|
||||
final Widget? centerBar;
|
||||
final List<Widget> overlays;
|
||||
final VoidCallback? onControlsAreaTap;
|
||||
final bool isFullscreen;
|
||||
final Future<void> Function() onToggleFullscreen;
|
||||
final bool isPip;
|
||||
final Future<void> Function()? onTogglePip;
|
||||
final double topBarLeftInset;
|
||||
final double topBarTopInset;
|
||||
final bool lockControls;
|
||||
final bool keyboardEnabled;
|
||||
final int seekForwardSeconds;
|
||||
final int seekBackwardSeconds;
|
||||
final bool volumeBoost;
|
||||
|
||||
final GestureBindings gestureBindings;
|
||||
|
||||
final double currentRate;
|
||||
final ValueChanged<double>? onSetRate;
|
||||
final VoidCallback? onPrevItem;
|
||||
final VoidCallback? onNextItem;
|
||||
final VoidCallback? onExit;
|
||||
|
||||
|
||||
final void Function(Offset globalPosition)? onSecondaryContextMenu;
|
||||
|
||||
|
||||
final Widget Function(VoidCallback toggleControls, VoidCallback showControls)?
|
||||
mobileGestureBuilder;
|
||||
|
||||
|
||||
final Widget? gestureFeedback;
|
||||
|
||||
|
||||
final bool suppressControlsChrome;
|
||||
|
||||
|
||||
final Widget? leftBar;
|
||||
|
||||
|
||||
final Widget? rightBar;
|
||||
|
||||
const PlayerVideoWidget({
|
||||
super.key,
|
||||
required this.player,
|
||||
required this.fit,
|
||||
required this.topBar,
|
||||
required this.bottomBar,
|
||||
this.centerBar,
|
||||
this.overlays = const <Widget>[],
|
||||
this.onControlsAreaTap,
|
||||
required this.isFullscreen,
|
||||
required this.onToggleFullscreen,
|
||||
this.isPip = false,
|
||||
this.onTogglePip,
|
||||
this.topBarLeftInset = 8,
|
||||
this.topBarTopInset = 8,
|
||||
this.lockControls = false,
|
||||
this.keyboardEnabled = true,
|
||||
this.seekForwardSeconds = 15,
|
||||
this.seekBackwardSeconds = 15,
|
||||
this.volumeBoost = false,
|
||||
this.gestureBindings = GestureBindings.defaults,
|
||||
this.currentRate = 1.0,
|
||||
this.onSetRate,
|
||||
this.onPrevItem,
|
||||
this.onNextItem,
|
||||
this.onExit,
|
||||
this.onSecondaryContextMenu,
|
||||
this.mobileGestureBuilder,
|
||||
this.gestureFeedback,
|
||||
this.suppressControlsChrome = false,
|
||||
this.leftBar,
|
||||
this.rightBar,
|
||||
});
|
||||
|
||||
@override
|
||||
State<PlayerVideoWidget> createState() => _PlayerVideoWidgetState();
|
||||
}
|
||||
|
||||
class _PlayerVideoWidgetState extends State<PlayerVideoWidget> {
|
||||
static const double _volumeStep = 5;
|
||||
static const double _pipResizeEdgeSize = 10;
|
||||
static const List<ResizeEdge> _pipResizeEdges = <ResizeEdge>[
|
||||
ResizeEdge.topLeft,
|
||||
ResizeEdge.top,
|
||||
ResizeEdge.topRight,
|
||||
ResizeEdge.left,
|
||||
ResizeEdge.right,
|
||||
ResizeEdge.bottomLeft,
|
||||
ResizeEdge.bottom,
|
||||
ResizeEdge.bottomRight,
|
||||
];
|
||||
|
||||
bool _controlsVisible = true;
|
||||
bool _cursorVisible = true;
|
||||
bool _hoveringControls = false;
|
||||
Timer? _controlsHideTimer;
|
||||
Timer? _cursorHideTimer;
|
||||
StreamSubscription<bool>? _playingSub;
|
||||
Offset? _lastMousePosition;
|
||||
|
||||
bool _holdingLeft = false;
|
||||
bool _holdingRight = false;
|
||||
double? _savedRateBeforeHold;
|
||||
|
||||
bool _holdSpeedPromptVisible = false;
|
||||
bool _holdSpeedPromptIsLeft = false;
|
||||
double _holdSpeedPromptRate = 1.0;
|
||||
|
||||
Timer? _pendingSeekLeft;
|
||||
Timer? _pendingSeekRight;
|
||||
static const _arrowHoldThreshold = Duration(milliseconds: 200);
|
||||
|
||||
|
||||
static const _topHoverTrigger = 120.0;
|
||||
static const _bottomHoverTrigger = 180.0;
|
||||
|
||||
Timer? _secondaryTapTimer;
|
||||
Offset? _secondaryTapDownPos;
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
_playingSub = widget.player.stream.playing.listen((playing) {
|
||||
if (!mounted) return;
|
||||
if (playing) {
|
||||
_scheduleHide();
|
||||
_scheduleHideCursor();
|
||||
} else {
|
||||
_showControls(lock: true);
|
||||
_showCursor(lock: true);
|
||||
}
|
||||
});
|
||||
if (widget.player.state.playing) {
|
||||
_scheduleHide();
|
||||
_scheduleHideCursor();
|
||||
}
|
||||
}
|
||||
|
||||
@override
|
||||
void didUpdateWidget(covariant PlayerVideoWidget oldWidget) {
|
||||
super.didUpdateWidget(oldWidget);
|
||||
if (oldWidget.player != widget.player) {
|
||||
unawaited(_playingSub?.cancel());
|
||||
_playingSub = widget.player.stream.playing.listen((playing) {
|
||||
if (!mounted) return;
|
||||
if (playing) {
|
||||
_scheduleHide();
|
||||
_scheduleHideCursor();
|
||||
} else {
|
||||
_showControls(lock: true);
|
||||
_showCursor(lock: true);
|
||||
}
|
||||
});
|
||||
}
|
||||
if (oldWidget.lockControls != widget.lockControls) {
|
||||
if (widget.lockControls) {
|
||||
_controlsHideTimer?.cancel();
|
||||
_cursorHideTimer?.cancel();
|
||||
_showControls(lock: true);
|
||||
_showCursor(lock: true);
|
||||
} else if (widget.player.state.playing) {
|
||||
_scheduleHide();
|
||||
_scheduleHideCursor();
|
||||
}
|
||||
}
|
||||
if (oldWidget.keyboardEnabled && !widget.keyboardEnabled) {
|
||||
_cancelPendingArrowSeeks();
|
||||
_restoreHoldRateIfNeeded();
|
||||
}
|
||||
}
|
||||
|
||||
@override
|
||||
void dispose() {
|
||||
_controlsHideTimer?.cancel();
|
||||
_cursorHideTimer?.cancel();
|
||||
_secondaryTapTimer?.cancel();
|
||||
_cancelPendingArrowSeeks();
|
||||
_restoreHoldRateIfNeeded();
|
||||
unawaited(_playingSub?.cancel());
|
||||
super.dispose();
|
||||
}
|
||||
|
||||
void _cancelPendingArrowSeeks() {
|
||||
_pendingSeekLeft?.cancel();
|
||||
_pendingSeekLeft = null;
|
||||
_pendingSeekRight?.cancel();
|
||||
_pendingSeekRight = null;
|
||||
}
|
||||
|
||||
void _showControls({bool lock = false}) {
|
||||
_controlsHideTimer?.cancel();
|
||||
if (!_controlsVisible && mounted) {
|
||||
setState(() => _controlsVisible = true);
|
||||
}
|
||||
if (!lock && widget.player.state.playing) {
|
||||
_scheduleHide();
|
||||
}
|
||||
}
|
||||
|
||||
void _scheduleHide() {
|
||||
_controlsHideTimer?.cancel();
|
||||
if (_hoveringControls || widget.lockControls) return;
|
||||
_controlsHideTimer = Timer(const Duration(seconds: 3), () {
|
||||
if (mounted &&
|
||||
widget.player.state.playing &&
|
||||
!_hoveringControls &&
|
||||
!widget.lockControls) {
|
||||
setState(() => _controlsVisible = false);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
void _showCursor({bool lock = false}) {
|
||||
_cursorHideTimer?.cancel();
|
||||
if (!_cursorVisible && mounted) {
|
||||
setState(() => _cursorVisible = true);
|
||||
}
|
||||
if (!lock && widget.player.state.playing) {
|
||||
_scheduleHideCursor();
|
||||
}
|
||||
}
|
||||
|
||||
void _scheduleHideCursor() {
|
||||
_cursorHideTimer?.cancel();
|
||||
if (_hoveringControls || widget.lockControls) return;
|
||||
_cursorHideTimer = Timer(const Duration(seconds: 3), () {
|
||||
if (mounted &&
|
||||
widget.player.state.playing &&
|
||||
!_hoveringControls &&
|
||||
!widget.lockControls) {
|
||||
setState(() => _cursorVisible = false);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
bool _inEdgeZone(double localY, double viewHeight) =>
|
||||
localY <= _topHoverTrigger || localY >= viewHeight - _bottomHoverTrigger;
|
||||
|
||||
Future<void> _togglePlayPause() async {
|
||||
if (widget.player.state.playing) {
|
||||
await widget.player.pause();
|
||||
} else {
|
||||
await widget.player.play();
|
||||
}
|
||||
}
|
||||
|
||||
KeyEventResult _handleKeyEvent(FocusNode node, KeyEvent event) {
|
||||
if (!widget.keyboardEnabled) return KeyEventResult.ignored;
|
||||
final key = event.logicalKey;
|
||||
|
||||
if (key == LogicalKeyboardKey.arrowLeft ||
|
||||
key == LogicalKeyboardKey.arrowRight) {
|
||||
return _handleArrowKey(event, key == LogicalKeyboardKey.arrowLeft);
|
||||
}
|
||||
|
||||
final isRepeat = event is KeyRepeatEvent;
|
||||
if (event is! KeyDownEvent && !isRepeat) return KeyEventResult.ignored;
|
||||
if (key == LogicalKeyboardKey.space) {
|
||||
if (isRepeat) return KeyEventResult.handled;
|
||||
unawaited(_togglePlayPause());
|
||||
_showControls();
|
||||
return KeyEventResult.handled;
|
||||
}
|
||||
if (key == LogicalKeyboardKey.arrowUp) {
|
||||
if (Platform.isAndroid) return KeyEventResult.ignored;
|
||||
final maxVol = widget.volumeBoost ? 150.0 : 100.0;
|
||||
final next = (widget.player.state.volume + _volumeStep).clamp(
|
||||
0.0,
|
||||
maxVol,
|
||||
);
|
||||
unawaited(widget.player.setVolume(next));
|
||||
_showControls();
|
||||
return KeyEventResult.handled;
|
||||
}
|
||||
if (key == LogicalKeyboardKey.arrowDown) {
|
||||
if (Platform.isAndroid) return KeyEventResult.ignored;
|
||||
final maxVol = widget.volumeBoost ? 150.0 : 100.0;
|
||||
final next = (widget.player.state.volume - _volumeStep).clamp(
|
||||
0.0,
|
||||
maxVol,
|
||||
);
|
||||
unawaited(widget.player.setVolume(next));
|
||||
_showControls();
|
||||
return KeyEventResult.handled;
|
||||
}
|
||||
if (key == LogicalKeyboardKey.keyF ||
|
||||
(key == LogicalKeyboardKey.escape && widget.isFullscreen)) {
|
||||
if (isRepeat) return KeyEventResult.handled;
|
||||
unawaited(widget.onToggleFullscreen());
|
||||
_showControls();
|
||||
return KeyEventResult.handled;
|
||||
}
|
||||
if (key == LogicalKeyboardKey.keyP) {
|
||||
if (isRepeat) return KeyEventResult.handled;
|
||||
final togglePip = widget.onTogglePip;
|
||||
if (togglePip != null) {
|
||||
unawaited(togglePip());
|
||||
_showControls();
|
||||
return KeyEventResult.handled;
|
||||
}
|
||||
}
|
||||
if (key == LogicalKeyboardKey.escape && widget.isPip) {
|
||||
if (isRepeat) return KeyEventResult.handled;
|
||||
final togglePip = widget.onTogglePip;
|
||||
if (togglePip != null) {
|
||||
unawaited(togglePip());
|
||||
_showControls();
|
||||
return KeyEventResult.handled;
|
||||
}
|
||||
}
|
||||
return KeyEventResult.ignored;
|
||||
}
|
||||
|
||||
KeyEventResult _handleArrowKey(KeyEvent event, bool isLeft) {
|
||||
final seekSeconds = isLeft
|
||||
? -widget.seekBackwardSeconds
|
||||
: widget.seekForwardSeconds;
|
||||
|
||||
if (event is KeyDownEvent) {
|
||||
if (isLeft) {
|
||||
_pendingSeekLeft?.cancel();
|
||||
_pendingSeekLeft = Timer(_arrowHoldThreshold, () {
|
||||
_pendingSeekLeft = null;
|
||||
if (!mounted) return;
|
||||
_enterHoldSpeed(isLeft: true);
|
||||
});
|
||||
} else {
|
||||
_pendingSeekRight?.cancel();
|
||||
_pendingSeekRight = Timer(_arrowHoldThreshold, () {
|
||||
_pendingSeekRight = null;
|
||||
if (!mounted) return;
|
||||
_enterHoldSpeed(isLeft: false);
|
||||
});
|
||||
}
|
||||
return KeyEventResult.handled;
|
||||
}
|
||||
if (event is KeyRepeatEvent) {
|
||||
return KeyEventResult.handled;
|
||||
}
|
||||
if (event is KeyUpEvent) {
|
||||
final pending = isLeft ? _pendingSeekLeft : _pendingSeekRight;
|
||||
if (pending != null) {
|
||||
pending.cancel();
|
||||
if (isLeft) {
|
||||
_pendingSeekLeft = null;
|
||||
} else {
|
||||
_pendingSeekRight = null;
|
||||
}
|
||||
_seekBy(seekSeconds);
|
||||
_showControls();
|
||||
}
|
||||
final wasHolding = isLeft ? _holdingLeft : _holdingRight;
|
||||
if (wasHolding) {
|
||||
if (isLeft) {
|
||||
_holdingLeft = false;
|
||||
} else {
|
||||
_holdingRight = false;
|
||||
}
|
||||
if (!_holdingLeft && !_holdingRight) {
|
||||
_hideHoldSpeedPrompt();
|
||||
final restore = _savedRateBeforeHold ?? widget.currentRate;
|
||||
_savedRateBeforeHold = null;
|
||||
widget.onSetRate?.call(restore);
|
||||
}
|
||||
}
|
||||
return KeyEventResult.handled;
|
||||
}
|
||||
return KeyEventResult.ignored;
|
||||
}
|
||||
|
||||
void _enterHoldSpeed({required bool isLeft}) {
|
||||
final holding = isLeft ? _holdingLeft : _holdingRight;
|
||||
if (holding) return;
|
||||
|
||||
final holdSpeed = isLeft
|
||||
? widget.gestureBindings.keyboardHoldLeftSpeed
|
||||
: widget.gestureBindings.keyboardHoldRightSpeed;
|
||||
_savedRateBeforeHold ??= widget.currentRate;
|
||||
widget.onSetRate?.call(holdSpeed);
|
||||
if (isLeft) {
|
||||
_holdingLeft = true;
|
||||
} else {
|
||||
_holdingRight = true;
|
||||
}
|
||||
_showHoldSpeedPrompt(isLeft: isLeft, rate: holdSpeed);
|
||||
}
|
||||
|
||||
void _showHoldSpeedPrompt({required bool isLeft, required double rate}) {
|
||||
if (!mounted) return;
|
||||
setState(() {
|
||||
_holdSpeedPromptVisible = true;
|
||||
_holdSpeedPromptIsLeft = isLeft;
|
||||
_holdSpeedPromptRate = rate;
|
||||
});
|
||||
}
|
||||
|
||||
void _hideHoldSpeedPrompt() {
|
||||
if (!_holdSpeedPromptVisible) return;
|
||||
if (!mounted) {
|
||||
_holdSpeedPromptVisible = false;
|
||||
return;
|
||||
}
|
||||
setState(() => _holdSpeedPromptVisible = false);
|
||||
}
|
||||
|
||||
void _restoreHoldRateIfNeeded() {
|
||||
if (!_holdingLeft && !_holdingRight) return;
|
||||
_holdingLeft = false;
|
||||
_holdingRight = false;
|
||||
_hideHoldSpeedPrompt();
|
||||
final restore = _savedRateBeforeHold;
|
||||
_savedRateBeforeHold = null;
|
||||
if (restore != null) widget.onSetRate?.call(restore);
|
||||
}
|
||||
|
||||
void _seekBy(int seconds) {
|
||||
final target = widget.player.state.position + Duration(seconds: seconds);
|
||||
unawaited(
|
||||
widget.player.seek(target < Duration.zero ? Duration.zero : target),
|
||||
);
|
||||
}
|
||||
|
||||
void _toggleControlsVisibility() {
|
||||
if (_controlsVisible) {
|
||||
setState(() => _controlsVisible = false);
|
||||
_controlsHideTimer?.cancel();
|
||||
} else {
|
||||
_showControls();
|
||||
_showCursor();
|
||||
}
|
||||
}
|
||||
|
||||
void _handleSecondaryTap() {
|
||||
final contextMenu = widget.onSecondaryContextMenu;
|
||||
if (contextMenu != null) {
|
||||
contextMenu(_secondaryTapDownPos ?? Offset.zero);
|
||||
return;
|
||||
}
|
||||
if (widget.gestureBindings.mouseRightDoubleClickAction ==
|
||||
GestureAction.none) {
|
||||
_dispatchGesture(GestureKind.rightClick);
|
||||
return;
|
||||
}
|
||||
final pending = _secondaryTapTimer;
|
||||
if (pending != null && pending.isActive) {
|
||||
pending.cancel();
|
||||
_secondaryTapTimer = null;
|
||||
_dispatchGesture(GestureKind.rightDoubleClick);
|
||||
return;
|
||||
}
|
||||
_secondaryTapTimer = Timer(kDoubleTapTimeout, () {
|
||||
_secondaryTapTimer = null;
|
||||
if (!mounted) return;
|
||||
_dispatchGesture(GestureKind.rightClick);
|
||||
});
|
||||
}
|
||||
|
||||
void _dispatchGesture(GestureKind kind) {
|
||||
final action = widget.gestureBindings.actionFor(kind);
|
||||
switch (action) {
|
||||
case GestureAction.none:
|
||||
return;
|
||||
case GestureAction.playPause:
|
||||
unawaited(_togglePlayPause());
|
||||
break;
|
||||
case GestureAction.toggleControls:
|
||||
_toggleControlsVisibility();
|
||||
return;
|
||||
case GestureAction.fullscreen:
|
||||
unawaited(widget.onToggleFullscreen());
|
||||
break;
|
||||
case GestureAction.seekBackward:
|
||||
_seekBy(-widget.seekBackwardSeconds);
|
||||
break;
|
||||
case GestureAction.seekForward:
|
||||
_seekBy(widget.seekForwardSeconds);
|
||||
break;
|
||||
case GestureAction.previousItem:
|
||||
widget.onPrevItem?.call();
|
||||
break;
|
||||
case GestureAction.nextItem:
|
||||
widget.onNextItem?.call();
|
||||
break;
|
||||
case GestureAction.exit:
|
||||
widget.onExit?.call();
|
||||
break;
|
||||
}
|
||||
_showControls();
|
||||
_showCursor();
|
||||
}
|
||||
|
||||
Widget _withPipResizeAffordance(Widget child) {
|
||||
if (!widget.isPip) return child;
|
||||
return DragToResizeArea(
|
||||
resizeEdgeSize: _pipResizeEdgeSize,
|
||||
resizeEdgeColor: Colors.transparent,
|
||||
enableResizeEdges: _pipResizeEdges,
|
||||
child: child,
|
||||
);
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final isWindowed = !widget.isFullscreen && !widget.isPip;
|
||||
|
||||
return Focus(
|
||||
autofocus: true,
|
||||
onKeyEvent: _handleKeyEvent,
|
||||
child: Stack(
|
||||
fit: StackFit.expand,
|
||||
children: [
|
||||
Positioned.fill(
|
||||
child: LayoutBuilder(
|
||||
builder: (context, constraints) {
|
||||
final viewHeight = constraints.maxHeight;
|
||||
|
||||
if (widget.mobileGestureBuilder != null) {
|
||||
return _withPipResizeAffordance(
|
||||
Stack(
|
||||
fit: StackFit.expand,
|
||||
children: [
|
||||
PlayerVideoSurface(
|
||||
player: widget.player,
|
||||
fit: widget.fit,
|
||||
),
|
||||
widget.mobileGestureBuilder!(
|
||||
_toggleControlsVisibility,
|
||||
_showControls,
|
||||
),
|
||||
...widget.overlays,
|
||||
if (widget.gestureFeedback != null)
|
||||
widget.gestureFeedback!,
|
||||
HoldSpeedPrompt(
|
||||
visible: _holdSpeedPromptVisible,
|
||||
isLeft: _holdSpeedPromptIsLeft,
|
||||
rate: _holdSpeedPromptRate,
|
||||
),
|
||||
ControlsChrome(
|
||||
visible:
|
||||
_controlsVisible &&
|
||||
!widget.suppressControlsChrome,
|
||||
topBar: widget.topBar,
|
||||
topBarLeftInset: widget.topBarLeftInset,
|
||||
topBarTopInset: widget.topBarTopInset,
|
||||
bottomBar: widget.bottomBar,
|
||||
centerBar: widget.centerBar,
|
||||
leftBar: widget.leftBar,
|
||||
rightBar: widget.rightBar,
|
||||
onControlsAreaTap: widget.onControlsAreaTap,
|
||||
isWindowed: isWindowed,
|
||||
isPip: widget.isPip,
|
||||
onControlsHoverChanged: (hovering) {
|
||||
_hoveringControls = hovering;
|
||||
if (!hovering && widget.player.state.playing) {
|
||||
_scheduleHide();
|
||||
}
|
||||
},
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
} else {
|
||||
return MouseRegion(
|
||||
cursor: _cursorVisible
|
||||
? SystemMouseCursors.basic
|
||||
: SystemMouseCursors.none,
|
||||
onEnter: (event) {
|
||||
_lastMousePosition = event.position;
|
||||
_showCursor();
|
||||
if (widget.isPip ||
|
||||
_inEdgeZone(event.localPosition.dy, viewHeight)) {
|
||||
_showControls();
|
||||
}
|
||||
},
|
||||
onExit: (_) {
|
||||
_lastMousePosition = null;
|
||||
},
|
||||
onHover: (event) {
|
||||
final pos = event.position;
|
||||
final last = _lastMousePosition;
|
||||
_lastMousePosition = pos;
|
||||
if (last != null &&
|
||||
_cursorVisible &&
|
||||
_controlsVisible &&
|
||||
(pos - last).distanceSquared < 9) {
|
||||
return;
|
||||
}
|
||||
_showCursor();
|
||||
if (widget.isPip ||
|
||||
_inEdgeZone(event.localPosition.dy, viewHeight)) {
|
||||
_showControls();
|
||||
}
|
||||
},
|
||||
child: GestureDetector(
|
||||
behavior: HitTestBehavior.opaque,
|
||||
onTap: widget.lockControls
|
||||
? null
|
||||
: () => _dispatchGesture(GestureKind.leftClick),
|
||||
onDoubleTap:
|
||||
widget.lockControls ||
|
||||
widget
|
||||
.gestureBindings
|
||||
.mouseLeftDoubleClickAction ==
|
||||
GestureAction.none
|
||||
? null
|
||||
: () => _dispatchGesture(GestureKind.leftDoubleClick),
|
||||
onSecondaryTap: widget.lockControls
|
||||
? null
|
||||
: _handleSecondaryTap,
|
||||
onSecondaryTapDown: widget.lockControls
|
||||
? null
|
||||
: (d) => _secondaryTapDownPos = d.globalPosition,
|
||||
child: _withPipResizeAffordance(
|
||||
Stack(
|
||||
fit: StackFit.expand,
|
||||
children: [
|
||||
PlayerVideoSurface(
|
||||
player: widget.player,
|
||||
fit: widget.fit,
|
||||
),
|
||||
...widget.overlays,
|
||||
HoldSpeedPrompt(
|
||||
visible: _holdSpeedPromptVisible,
|
||||
isLeft: _holdSpeedPromptIsLeft,
|
||||
rate: _holdSpeedPromptRate,
|
||||
),
|
||||
ControlsChrome(
|
||||
visible:
|
||||
_controlsVisible &&
|
||||
!widget.suppressControlsChrome,
|
||||
topBar: widget.topBar,
|
||||
topBarLeftInset: widget.topBarLeftInset,
|
||||
topBarTopInset: widget.topBarTopInset,
|
||||
bottomBar: widget.bottomBar,
|
||||
centerBar: widget.centerBar,
|
||||
leftBar: widget.leftBar,
|
||||
rightBar: widget.rightBar,
|
||||
onControlsAreaTap: widget.onControlsAreaTap,
|
||||
isWindowed: isWindowed,
|
||||
isPip: widget.isPip,
|
||||
onControlsHoverChanged: (hovering) {
|
||||
_hoveringControls = hovering;
|
||||
if (!hovering && widget.player.state.playing) {
|
||||
_scheduleHide();
|
||||
}
|
||||
},
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
},
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
class PlayerVideoSurface extends StatelessWidget {
|
||||
final PlayerEngine player;
|
||||
final ValueListenable<BoxFit> fit;
|
||||
|
||||
const PlayerVideoSurface({
|
||||
super.key,
|
||||
required this.player,
|
||||
required this.fit,
|
||||
});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final child = player.buildVideoView(context, fit);
|
||||
if (player.usesPlatformSurface) return child;
|
||||
return ColoredBox(color: Colors.black, child: child);
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user