import 'dart:async'; import 'dart:math' as math; import 'package:flutter/foundation.dart'; import 'package:flutter/material.dart'; import '../../../core/contracts/playback.dart'; import '../../../core/infra/player_engine/player_engine.dart'; import '../../../providers/subtitle_settings_provider.dart'; class StyledSubtitleOverlay extends StatefulWidget { final PlayerEngine player; final SubtitleStyleSettings settings; final ValueListenable>? externalLines; final ValueListenable? externalActive; const StyledSubtitleOverlay({ super.key, required this.player, required this.settings, this.externalLines, this.externalActive, }); @override State createState() => _StyledSubtitleOverlayState(); } class _StyledSubtitleOverlayState extends State { static const _mpvReferenceHeight = 720.0; static const _baseEdgeInset = 24.0; List _subtitle = const []; StreamSubscription>? _subscription; @override void initState() { super.initState(); _bindPlayer(widget.player); widget.externalLines?.addListener(_onExternalChanged); widget.externalActive?.addListener(_onExternalChanged); } @override void didUpdateWidget(covariant StyledSubtitleOverlay oldWidget) { super.didUpdateWidget(oldWidget); if (oldWidget.player != widget.player) { unawaited(_subscription?.cancel()); _bindPlayer(widget.player); } if (oldWidget.externalLines != widget.externalLines) { oldWidget.externalLines?.removeListener(_onExternalChanged); widget.externalLines?.addListener(_onExternalChanged); } if (oldWidget.externalActive != widget.externalActive) { oldWidget.externalActive?.removeListener(_onExternalChanged); widget.externalActive?.addListener(_onExternalChanged); } } @override void dispose() { unawaited(_subscription?.cancel()); widget.externalLines?.removeListener(_onExternalChanged); widget.externalActive?.removeListener(_onExternalChanged); super.dispose(); } void _bindPlayer(PlayerEngine player) { _subtitle = player.state.subtitle; _subscription = player.stream.subtitle.listen((value) { if (mounted) setState(() => _subtitle = value); }); } void _onExternalChanged() { if (mounted) setState(() {}); } bool get _externalIsActive => widget.externalActive?.value != null; List get _activeLines => _externalIsActive ? (widget.externalLines?.value ?? const []) : _subtitle; @override Widget build(BuildContext context) { final primaryText = _joinLines(_activeLines); if (primaryText.isEmpty) { return const SizedBox.shrink(); } return IgnorePointer( child: LayoutBuilder( builder: (context, constraints) { final width = constraints.maxWidth; final height = constraints.maxHeight; if (!width.isFinite || !height.isFinite || width <= 0 || height <= 0) { return const SizedBox.shrink(); } final fontSize = _scaleFontSize(widget.settings.fontSize, height); final position = widget.settings.position.clamp(0, 150).toDouble(); final verticalProgress = math.min(position, 100.0) / 100.0; final extraLowering = math.max(0.0, position - 100.0) / 50.0; final bottomInset = _baseEdgeInset * (1.0 - extraLowering.clamp(0.0, 1.0)); final lineCount = math.max(1, primaryText.split('\n').length); final estimatedBlockHeight = math.min( height * 0.5, lineCount * fontSize * 1.32 + 12.0, ); final verticalRange = math.max( 0.0, height - _baseEdgeInset - bottomInset - estimatedBlockHeight, ); final top = _baseEdgeInset + verticalRange * verticalProgress; final horizontalInset = math.min(_baseEdgeInset, width * 0.06); return Stack( fit: StackFit.expand, children: [ Positioned( left: horizontalInset, right: horizontalInset, top: top, child: _buildLine( text: primaryText, fontSize: fontSize, bgOpacity: widget.settings.bgOpacity.clamp(0.0, 1.0), ), ), ], ); }, ), ); } String _joinLines(List lines) => lines .map((line) => line.trim()) .where((line) => line.isNotEmpty) .join('\n'); double _scaleFontSize(double settingsFontSize, double height) => (settingsFontSize * height / _mpvReferenceHeight).clamp(8.0, 200.0); Widget _buildLine({ required String text, required double fontSize, required double bgOpacity, }) { final child = Text( text, textAlign: TextAlign.center, softWrap: true, textScaler: TextScaler.noScaling, style: TextStyle( color: Colors.white, fontSize: fontSize, height: 1.32, letterSpacing: 0.0, fontWeight: FontWeight.w600, shadows: const [ Shadow(color: Colors.black, blurRadius: 2, offset: Offset(0, 1)), Shadow(color: Colors.black87, blurRadius: 4), ], ), ); if (bgOpacity > 0) { return DecoratedBox( decoration: BoxDecoration( color: Colors.black.withOpacity(bgOpacity), borderRadius: BorderRadius.circular(4), ), child: Padding( padding: const EdgeInsets.symmetric(horizontal: 8, vertical: 3), child: child, ), ); } return child; } }