mirror of
https://github.com/gabehf/Fladder.git
synced 2026-03-07 21:48:14 -08:00
258 lines
8.8 KiB
Dart
258 lines
8.8 KiB
Dart
import 'dart:convert';
|
|
import 'dart:math' as math;
|
|
|
|
import 'package:flutter/material.dart';
|
|
|
|
import 'package:collection/collection.dart';
|
|
import 'package:flutter_riverpod/flutter_riverpod.dart';
|
|
|
|
import 'package:fladder/providers/settings/subtitle_settings_provider.dart';
|
|
import 'package:fladder/providers/settings/video_player_settings_provider.dart';
|
|
import 'package:fladder/util/color_extensions.dart';
|
|
|
|
class SubtitleSettingsModel {
|
|
final double fontSize;
|
|
final FontWeight fontWeight;
|
|
final double verticalOffset;
|
|
final Color color;
|
|
final Color outlineColor;
|
|
final double outlineSize;
|
|
final Color backGroundColor;
|
|
final double shadow;
|
|
const SubtitleSettingsModel({
|
|
this.fontSize = 60,
|
|
this.fontWeight = FontWeight.normal,
|
|
this.verticalOffset = 0.10,
|
|
this.color = Colors.white,
|
|
this.outlineColor = const Color.fromRGBO(0, 0, 0, 0.85),
|
|
this.outlineSize = 4,
|
|
this.backGroundColor = const Color.fromARGB(0, 0, 0, 0),
|
|
this.shadow = 0.5,
|
|
});
|
|
|
|
SubtitleSettingsModel copyWith({
|
|
double? fontSize,
|
|
FontWeight? fontWeight,
|
|
double? verticalOffset,
|
|
Color? color,
|
|
Color? outlineColor,
|
|
double? outlineSize,
|
|
Color? backGroundColor,
|
|
double? shadow,
|
|
}) {
|
|
return SubtitleSettingsModel(
|
|
fontSize: fontSize ?? this.fontSize,
|
|
fontWeight: fontWeight ?? this.fontWeight,
|
|
verticalOffset: verticalOffset ?? this.verticalOffset,
|
|
color: color ?? this.color,
|
|
outlineColor: outlineColor ?? this.outlineColor,
|
|
outlineSize: outlineSize ?? this.outlineSize,
|
|
backGroundColor: backGroundColor ?? this.backGroundColor,
|
|
shadow: shadow ?? this.shadow,
|
|
);
|
|
}
|
|
|
|
TextStyle get backGroundStyle {
|
|
return style.copyWith(
|
|
shadows: (shadow > 0.01)
|
|
? [
|
|
Shadow(
|
|
blurRadius: 16,
|
|
color: Colors.black.withValues(alpha: shadow),
|
|
),
|
|
Shadow(
|
|
blurRadius: 8,
|
|
color: Colors.black.withValues(alpha: shadow),
|
|
),
|
|
]
|
|
: null,
|
|
foreground: Paint()
|
|
..style = PaintingStyle.stroke
|
|
..strokeWidth = outlineSize * (fontSize / 30)
|
|
..color = outlineColor
|
|
..strokeCap = StrokeCap.round
|
|
..strokeJoin = StrokeJoin.round,
|
|
);
|
|
}
|
|
|
|
TextStyle get style {
|
|
return TextStyle(
|
|
height: 1.4,
|
|
fontSize: fontSize,
|
|
fontWeight: fontWeight,
|
|
fontFamily: 'OpenSans',
|
|
letterSpacing: 0.0,
|
|
wordSpacing: 0.0,
|
|
color: color,
|
|
);
|
|
}
|
|
|
|
Map<String, dynamic> toMap() {
|
|
return <String, dynamic>{
|
|
'fontSize': fontSize,
|
|
'fontWeight': fontWeight.value,
|
|
'verticalOffset': verticalOffset,
|
|
'color': color.toMap,
|
|
'outlineColor': outlineColor.toMap,
|
|
'outlineSize': outlineSize,
|
|
'backGroundColor': backGroundColor.toMap,
|
|
'shadow': shadow,
|
|
};
|
|
}
|
|
|
|
String toJson() => json.encode(toMap());
|
|
|
|
factory SubtitleSettingsModel.fromJson(String source) => SubtitleSettingsModel.fromMap(json.decode(source));
|
|
|
|
factory SubtitleSettingsModel.fromMap(Map<String, dynamic> map) {
|
|
return const SubtitleSettingsModel().copyWith(
|
|
fontSize: map['fontSize'] as double?,
|
|
fontWeight: FontWeight.values.firstWhereOrNull((element) => element.index == map['fontWeight'] as int?),
|
|
verticalOffset: map['verticalOffset'] as double?,
|
|
color: colorFromJson(map['color']),
|
|
outlineColor: colorFromJson(map['outlineColor']),
|
|
outlineSize: map['outlineSize'] as double?,
|
|
backGroundColor: colorFromJson(map['backGroundColor']),
|
|
shadow: map['shadow'] as double?,
|
|
);
|
|
}
|
|
|
|
@override
|
|
String toString() {
|
|
return 'SubtitleSettingsModel(fontSize: $fontSize, fontWeight: $fontWeight, verticalOffset: $verticalOffset, color: $color, outlineColor: $outlineColor, outlineSize: $outlineSize, backGroundColor: $backGroundColor, shadow: $shadow)';
|
|
}
|
|
|
|
@override
|
|
bool operator ==(covariant SubtitleSettingsModel other) {
|
|
if (identical(this, other)) return true;
|
|
|
|
return other.fontSize == fontSize &&
|
|
other.fontWeight == fontWeight &&
|
|
other.verticalOffset == verticalOffset &&
|
|
other.color == color &&
|
|
other.outlineColor == outlineColor &&
|
|
other.outlineSize == outlineSize &&
|
|
other.backGroundColor == backGroundColor &&
|
|
other.shadow == shadow;
|
|
}
|
|
|
|
@override
|
|
int get hashCode {
|
|
return fontSize.hashCode ^
|
|
fontWeight.hashCode ^
|
|
verticalOffset.hashCode ^
|
|
color.hashCode ^
|
|
outlineColor.hashCode ^
|
|
outlineSize.hashCode ^
|
|
backGroundColor.hashCode ^
|
|
shadow.hashCode;
|
|
}
|
|
}
|
|
|
|
class SubtitleText extends ConsumerWidget {
|
|
final SubtitleSettingsModel subModel;
|
|
final EdgeInsets padding;
|
|
final String text;
|
|
final double offset;
|
|
const SubtitleText({
|
|
required this.subModel,
|
|
required this.padding,
|
|
required this.offset,
|
|
required this.text,
|
|
super.key,
|
|
});
|
|
|
|
// The reference width for calculating the visible text scale factor.
|
|
static const kTextScaleFactorReferenceWidth = 1920.0;
|
|
// The reference height for calculating the visible text scale factor.
|
|
static const kTextScaleFactorReferenceHeight = 1080.0;
|
|
|
|
@override
|
|
Widget build(BuildContext context, WidgetRef ref) {
|
|
final fillScreen = ref.watch(videoPlayerSettingsProvider.select((value) => value.fillScreen));
|
|
return Padding(
|
|
padding: (fillScreen ? EdgeInsets.zero : EdgeInsets.only(left: padding.left, right: padding.right))
|
|
.add(const EdgeInsets.all(16)),
|
|
child: LayoutBuilder(
|
|
builder: (context, constraints) {
|
|
final textScale = MediaQuery.textScalerOf(context)
|
|
.scale((ref.read(subtitleSettingsProvider.select((value) => value.fontSize)) *
|
|
math.sqrt(
|
|
((constraints.maxWidth * constraints.maxHeight) /
|
|
(kTextScaleFactorReferenceWidth * kTextScaleFactorReferenceHeight))
|
|
.clamp(0.0, 1.0),
|
|
)));
|
|
|
|
// Function to calculate the height of the text
|
|
double getTextHeight(BuildContext context, String text, TextStyle style) {
|
|
final TextPainter textPainter = TextPainter(
|
|
text: TextSpan(text: text, style: style),
|
|
textDirection: TextDirection.ltr,
|
|
textScaler: MediaQuery.textScalerOf(context),
|
|
)..layout(minWidth: 0, maxWidth: double.infinity);
|
|
|
|
return textPainter.height;
|
|
}
|
|
|
|
// Calculate the available height for the text alignment
|
|
double availableHeight = constraints.maxHeight;
|
|
|
|
// Calculate the desired position based on the percentage
|
|
double desiredPosition = availableHeight * offset;
|
|
|
|
// Get the height of the Text widget with the current font style
|
|
double textHeight = getTextHeight(context, text, subModel.style);
|
|
|
|
// Calculate the position to keep the text within visible bounds
|
|
double position = desiredPosition - textHeight / 2;
|
|
|
|
// Ensure the text doesn't go off-screen
|
|
position = position.clamp(0, availableHeight - textHeight);
|
|
|
|
return Visibility(
|
|
visible: text.isEmpty ? false : true,
|
|
child: IgnorePointer(
|
|
child: Stack(
|
|
alignment: Alignment.bottomCenter,
|
|
children: [
|
|
Positioned(
|
|
bottom: position,
|
|
child: Container(
|
|
constraints: BoxConstraints(maxWidth: constraints.maxWidth, maxHeight: constraints.maxHeight),
|
|
decoration: BoxDecoration(
|
|
color: subModel.backGroundColor,
|
|
borderRadius: BorderRadius.circular(16),
|
|
),
|
|
child: Padding(
|
|
padding: const EdgeInsets.symmetric(horizontal: 16),
|
|
child: Text(
|
|
text,
|
|
style: subModel.backGroundStyle.copyWith(fontSize: textScale),
|
|
textAlign: TextAlign.center,
|
|
),
|
|
),
|
|
),
|
|
),
|
|
Positioned(
|
|
bottom: position,
|
|
child: Container(
|
|
constraints: BoxConstraints(maxWidth: constraints.maxWidth, maxHeight: constraints.maxHeight),
|
|
child: Padding(
|
|
padding: const EdgeInsets.symmetric(horizontal: 16),
|
|
child: Text(
|
|
text,
|
|
style: subModel.style.copyWith(fontSize: textScale),
|
|
textAlign: TextAlign.center,
|
|
),
|
|
),
|
|
),
|
|
)
|
|
],
|
|
),
|
|
),
|
|
);
|
|
},
|
|
),
|
|
);
|
|
}
|
|
}
|