Drops dead code across services, models, utils and widgets, including the connection auth service, which had no implementer, and the Live TV DVR provisioning models, which had no caller. Tests that only covered deleted behaviour are removed or trimmed. No behaviour change.
236 lines
6.5 KiB
Dart
236 lines
6.5 KiB
Dart
import 'dart:async';
|
|
|
|
import 'package:flutter/foundation.dart';
|
|
import 'package:flutter/material.dart'
|
|
show BuildContext, ListenableBuilder, MouseRegion, StatelessWidget, SystemMouseCursors, Widget;
|
|
|
|
/// Reasons that keep the video-player chrome visible and suppress auto-hide.
|
|
enum PlayerChromeHold { pip, contentStrip, promptInteraction, scrub }
|
|
|
|
/// Focus target to request after chrome has rebuilt visible controls.
|
|
enum PlayerChromeFocusTarget { playPause, timeline }
|
|
|
|
/// Owns video-player chrome visibility and auto-hide policy for one player route.
|
|
class PlayerChromeController extends ChangeNotifier implements ValueListenable<bool> {
|
|
PlayerChromeController({this._controlsVisible = true});
|
|
|
|
bool _controlsVisible;
|
|
bool _controlsPresented = true;
|
|
bool _contentStripVisible = false;
|
|
bool _playing = false;
|
|
bool _hasFirstFrame = true;
|
|
Duration _hideDelay = const Duration(seconds: 3);
|
|
Timer? _hideTimer;
|
|
PlayerChromeFocusTarget? _pendingFocusTarget;
|
|
final Set<PlayerChromeHold> _holds = <PlayerChromeHold>{};
|
|
final Stopwatch _pointerActivityStopwatch = Stopwatch()..start();
|
|
int _lastPointerActivityMs = -1000;
|
|
|
|
@override
|
|
bool get value => _controlsVisible;
|
|
|
|
bool get controlsVisible => _controlsVisible;
|
|
|
|
/// Whether controls may still be visibly rendered during their fade-out.
|
|
bool get controlsPresented => _controlsPresented;
|
|
bool get contentStripVisible => _contentStripVisible;
|
|
bool isHeld(PlayerChromeHold hold) => _holds.contains(hold);
|
|
PlayerChromeFocusTarget? get pendingFocusTarget => _pendingFocusTarget;
|
|
|
|
void configure({Duration? hideDelay, bool? hasFirstFrame}) {
|
|
var restartTimer = false;
|
|
if (hideDelay != null && hideDelay != _hideDelay) {
|
|
_hideDelay = hideDelay;
|
|
restartTimer = true;
|
|
}
|
|
if (hasFirstFrame != null && hasFirstFrame != _hasFirstFrame) {
|
|
_hasFirstFrame = hasFirstFrame;
|
|
restartTimer = true;
|
|
}
|
|
if (restartTimer) _startAutoHideForCurrentPlaybackState();
|
|
}
|
|
|
|
void setPlaying(bool playing) {
|
|
if (_playing == playing) return;
|
|
_playing = playing;
|
|
if (!_controlsVisible) return;
|
|
if (playing) {
|
|
startAutoHide();
|
|
} else {
|
|
startPausedAutoHide();
|
|
}
|
|
}
|
|
|
|
void setHasFirstFrame(bool hasFirstFrame) {
|
|
if (_hasFirstFrame == hasFirstFrame) return;
|
|
_hasFirstFrame = hasFirstFrame;
|
|
if (!_hasFirstFrame) {
|
|
cancelAutoHide();
|
|
return;
|
|
}
|
|
_startAutoHideForCurrentPlaybackState();
|
|
}
|
|
|
|
void setContentStripVisible(bool visible) {
|
|
if (_contentStripVisible == visible) return;
|
|
_contentStripVisible = visible;
|
|
if (visible) {
|
|
hold(PlayerChromeHold.contentStrip);
|
|
} else {
|
|
release(PlayerChromeHold.contentStrip);
|
|
}
|
|
}
|
|
|
|
void show({bool restartAutoHide = true, PlayerChromeFocusTarget? focusTarget}) {
|
|
_controlsPresented = true;
|
|
var shouldNotify = false;
|
|
if (focusTarget != null) {
|
|
_pendingFocusTarget = focusTarget;
|
|
shouldNotify = true;
|
|
}
|
|
if (!_controlsVisible) {
|
|
_controlsVisible = true;
|
|
shouldNotify = true;
|
|
}
|
|
if (shouldNotify) notifyListeners();
|
|
if (restartAutoHide) _startAutoHideForCurrentPlaybackState();
|
|
}
|
|
|
|
PlayerChromeFocusTarget? takeFocusTarget() {
|
|
final target = _pendingFocusTarget;
|
|
_pendingFocusTarget = null;
|
|
return target;
|
|
}
|
|
|
|
bool hide({bool ignoreHolds = false}) {
|
|
if (!_controlsVisible) return false;
|
|
if (!ignoreHolds && _holds.isNotEmpty) return false;
|
|
cancelAutoHide();
|
|
_controlsVisible = false;
|
|
if (_contentStripVisible) {
|
|
_contentStripVisible = false;
|
|
_holds.remove(PlayerChromeHold.contentStrip);
|
|
}
|
|
notifyListeners();
|
|
return true;
|
|
}
|
|
|
|
/// Called when the controls opacity animation reaches its hidden target.
|
|
void markControlsHidden() {
|
|
if (_controlsVisible) return;
|
|
_controlsPresented = false;
|
|
}
|
|
|
|
void toggle() {
|
|
if (_controlsVisible) {
|
|
hide();
|
|
} else {
|
|
show();
|
|
}
|
|
}
|
|
|
|
bool recordPointerActivity() {
|
|
final nowMs = _pointerActivityStopwatch.elapsedMilliseconds;
|
|
final shouldThrottle = _controlsVisible && nowMs - _lastPointerActivityMs < 120;
|
|
if (shouldThrottle) return false;
|
|
_lastPointerActivityMs = nowMs;
|
|
|
|
show(restartAutoHide: false);
|
|
_startAutoHideForCurrentPlaybackState();
|
|
return true;
|
|
}
|
|
|
|
void startAutoHide() {
|
|
_hideTimer?.cancel();
|
|
if (!_hasFirstFrame || _holds.isNotEmpty || !_playing) return;
|
|
_hideTimer = Timer(_hideDelay, () {
|
|
if (_playing && _hasFirstFrame) hide();
|
|
});
|
|
}
|
|
|
|
void startPausedAutoHide() {
|
|
_hideTimer?.cancel();
|
|
if (!_controlsVisible || !_hasFirstFrame || _holds.isNotEmpty) return;
|
|
_hideTimer = Timer(_hideDelay, hide);
|
|
}
|
|
|
|
void _startAutoHideForCurrentPlaybackState() {
|
|
if (!_controlsVisible) {
|
|
cancelAutoHide();
|
|
return;
|
|
}
|
|
if (_playing) {
|
|
startAutoHide();
|
|
} else {
|
|
startPausedAutoHide();
|
|
}
|
|
}
|
|
|
|
void restartAutoHideForCurrentPlaybackState() => _startAutoHideForCurrentPlaybackState();
|
|
|
|
void hideForPointerExit() {
|
|
if (_holds.contains(PlayerChromeHold.pip)) return;
|
|
hide(ignoreHolds: true);
|
|
}
|
|
|
|
void cancelAutoHide() {
|
|
_hideTimer?.cancel();
|
|
_hideTimer = null;
|
|
}
|
|
|
|
void hold(PlayerChromeHold hold) {
|
|
if (!_holds.add(hold)) return;
|
|
cancelAutoHide();
|
|
if (!_controlsVisible) {
|
|
_controlsVisible = true;
|
|
}
|
|
_controlsPresented = true;
|
|
notifyListeners();
|
|
}
|
|
|
|
void release(PlayerChromeHold hold, {bool notify = true, bool restartAutoHide = true}) {
|
|
if (!_holds.remove(hold)) return;
|
|
if (notify) notifyListeners();
|
|
if (restartAutoHide && _holds.isEmpty) _startAutoHideForCurrentPlaybackState();
|
|
}
|
|
|
|
@override
|
|
void dispose() {
|
|
cancelAutoHide();
|
|
super.dispose();
|
|
}
|
|
}
|
|
|
|
/// Defines the pointer boundary for all interactive video-player chrome.
|
|
class PlayerChromeInteractionRegion extends StatelessWidget {
|
|
final PlayerChromeController controller;
|
|
final bool hideOnExit;
|
|
final Widget child;
|
|
|
|
const PlayerChromeInteractionRegion({
|
|
super.key,
|
|
required this.controller,
|
|
required this.hideOnExit,
|
|
required this.child,
|
|
});
|
|
|
|
@override
|
|
Widget build(BuildContext context) {
|
|
return ListenableBuilder(
|
|
listenable: controller,
|
|
builder: (context, _) {
|
|
return MouseRegion(
|
|
cursor: controller.controlsVisible ? SystemMouseCursors.basic : SystemMouseCursors.none,
|
|
onHover: (_) => controller.recordPointerActivity(),
|
|
onExit: (_) {
|
|
if (!hideOnExit) return;
|
|
controller.cancelAutoHide();
|
|
controller.hideForPointerExit();
|
|
},
|
|
child: child,
|
|
);
|
|
},
|
|
);
|
|
}
|
|
}
|