Files
sm-emby-share/lib/features/player/widgets/styled_subtitle_overlay.dart
T
2026-07-14 11:11:36 +08:00

196 lines
5.7 KiB
Dart

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<List<String>>? externalLines;
final ValueListenable<EmbySubtitleTrack?>? externalActive;
const StyledSubtitleOverlay({
super.key,
required this.player,
required this.settings,
this.externalLines,
this.externalActive,
});
@override
State<StyledSubtitleOverlay> createState() => _StyledSubtitleOverlayState();
}
class _StyledSubtitleOverlayState extends State<StyledSubtitleOverlay> {
static const _mpvReferenceHeight = 720.0;
static const _baseEdgeInset = 24.0;
List<String> _subtitle = const [];
StreamSubscription<List<String>>? _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<String> 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<String> 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;
}
}