import 'dart:async'; import 'package:flutter/material.dart'; import '../../../core/infra/player_engine/player_engine.dart'; import '../../../shared/utils/format_utils.dart'; import '../../../shared/widgets/app_loading_ring.dart'; import '../services/network_speed_monitor.dart'; class BufferingOverlay extends StatefulWidget { final PlayerEngine player; final NetworkSpeedMonitor? speedMonitor; final ValueChanged? onBufferingChanged; final bool suppress; const BufferingOverlay({ super.key, required this.player, this.speedMonitor, this.onBufferingChanged, this.suppress = false, }); @override State createState() => _BufferingOverlayState(); } class _BufferingOverlayState extends State { bool _isBuffering = false; Timer? _debounceTimer; Duration _position = Duration.zero; Duration _buffer = Duration.zero; bool _playing = false; StreamSubscription? _bufferSub; StreamSubscription? _positionSub; StreamSubscription? _playingSub; @override void initState() { super.initState(); _readState(widget.player); _subscribe(widget.player); } @override void didUpdateWidget(covariant BufferingOverlay oldWidget) { super.didUpdateWidget(oldWidget); if (oldWidget.player != widget.player) { _cancelSubscriptions(); _setBuffering(false); _readState(widget.player); _subscribe(widget.player); } } @override void dispose() { _debounceTimer?.cancel(); _cancelSubscriptions(); super.dispose(); } void _readState(PlayerEngine player) { _position = player.state.position; _buffer = player.state.buffer; _playing = player.state.playing; } void _subscribe(PlayerEngine player) { _positionSub = player.stream.position.listen((p) { _position = p; _evaluate(); }); _bufferSub = player.stream.buffer.listen((b) { _buffer = b; _evaluate(); }); _playingSub = player.stream.playing.listen((playing) { _playing = playing; _evaluate(); }); } void _cancelSubscriptions() { unawaited(_positionSub?.cancel()); unawaited(_bufferSub?.cancel()); unawaited(_playingSub?.cancel()); _positionSub = null; _bufferSub = null; _playingSub = null; } void _setBuffering(bool value) { if (_isBuffering == value) return; setState(() => _isBuffering = value); widget.onBufferingChanged?.call(value); } void _evaluate() { final shouldBuffer = _playing && _buffer <= _position && _position > Duration.zero; if (shouldBuffer) { if (_isBuffering || _debounceTimer != null) return; _debounceTimer = Timer(const Duration(milliseconds: 200), () { _debounceTimer = null; if (!mounted) return; if (_playing && _buffer <= _position && _position > Duration.zero) { _setBuffering(true); } }); return; } _debounceTimer?.cancel(); _debounceTimer = null; _setBuffering(false); } @override Widget build(BuildContext context) { final monitor = widget.speedMonitor; return IgnorePointer( child: Opacity( opacity: widget.suppress ? 0.0 : 1.0, child: AnimatedOpacity( opacity: _isBuffering ? 1.0 : 0.0, duration: const Duration(milliseconds: 100), child: Center( child: Column( mainAxisSize: MainAxisSize.min, children: [ const AppLoadingRing(size: 40, color: Colors.white), if (monitor != null) ...[ const SizedBox(height: 12), ValueListenableBuilder( valueListenable: monitor.available, builder: (_, available, _) { if (!available) return const SizedBox.shrink(); return ValueListenableBuilder( valueListenable: monitor.speed, builder: (_, speed, _) { return Text( formatDownloadSpeed(speed), style: const TextStyle( color: Colors.white70, fontSize: 13, fontFeatures: [FontFeature.tabularFigures()], ), ); }, ); }, ), ], ], ), ), ), ), ); } }