Files
plezy/lib/screens/settings/subtitle_styling_screen.dart
T

314 lines
12 KiB
Dart

import 'package:flutter/material.dart';
import 'package:plezy/widgets/app_icon.dart';
import 'package:material_symbols_icons/symbols.dart';
import 'package:flex_color_picker/flex_color_picker.dart';
import '../../focus/focusable_button.dart';
import '../../focus/input_mode_tracker.dart';
import '../../i18n/strings.g.dart';
import '../../services/settings_service.dart';
import '../../widgets/focused_scroll_scaffold.dart';
import '../../widgets/settings_section.dart';
import '../../widgets/tv_color_picker.dart';
import 'settings_utils.dart';
class SubtitleStylingScreen extends StatefulWidget {
const SubtitleStylingScreen({super.key});
@override
State<SubtitleStylingScreen> createState() => _SubtitleStylingScreenState();
}
class _SubtitleStylingScreenState extends State<SubtitleStylingScreen> {
late SettingsService _settingsService;
bool _isLoading = true;
int _fontSize = 55;
String _textColor = '#FFFFFF';
int _borderSize = 3;
String _borderColor = '#000000';
String _backgroundColor = '#000000';
int _backgroundOpacity = 0;
int _subtitlePosition = 100;
@override
void initState() {
super.initState();
_loadSettings();
}
Future<void> _loadSettings() async {
_settingsService = await SettingsService.getInstance();
if (!mounted) return;
setState(() {
_fontSize = _settingsService.getSubtitleFontSize();
_textColor = _settingsService.getSubtitleTextColor();
_borderSize = _settingsService.getSubtitleBorderSize();
_borderColor = _settingsService.getSubtitleBorderColor();
_backgroundColor = _settingsService.getSubtitleBackgroundColor();
_backgroundOpacity = _settingsService.getSubtitleBackgroundOpacity();
_subtitlePosition = _settingsService.getSubtitlePosition();
_isLoading = false;
});
}
Color _hexToColor(String hexString) {
final buffer = StringBuffer();
if (hexString.length == 7) buffer.write('ff');
buffer.write(hexString.replaceFirst('#', ''));
return Color(int.parse(buffer.toString(), radix: 16));
}
String _colorToHex(Color color) {
return '#${((color.r * 255.0).round() & 0xff).toRadixString(16).padLeft(2, '0')}${((color.g * 255.0).round() & 0xff).toRadixString(16).padLeft(2, '0')}${((color.b * 255.0).round() & 0xff).toRadixString(16).padLeft(2, '0')}'
.toUpperCase();
}
Future<void> _showColorPicker(String title, String currentColor, Function(String) onColorSelected) async {
Color initialColor = _hexToColor(currentColor);
final Color selectedColor = await showColorPickerDialog(
context,
initialColor,
title: Text(title),
barrierColor: Colors.black54,
width: 40,
height: 40,
spacing: 0,
runSpacing: 0,
borderRadius: 4,
wheelDiameter: 165,
enableOpacity: false,
showColorCode: true,
colorCodeHasColor: true,
pickersEnabled: const <ColorPickerType, bool>{
ColorPickerType.both: false,
ColorPickerType.primary: true,
ColorPickerType.accent: false,
ColorPickerType.wheel: true,
ColorPickerType.custom: false,
},
actionButtons: const ColorPickerActionButtons(okButton: true, closeButton: true, dialogActionButtons: false),
);
final hexColor = _colorToHex(selectedColor);
onColorSelected(hexColor);
}
void _showTvColorPicker(String title, String currentColor, Function(String) onColorSelected) {
Color pickerColor = _hexToColor(currentColor);
final saveFocusNode = FocusNode();
showDialog(
context: context,
builder: (BuildContext dialogContext) {
return StatefulBuilder(
builder: (context, setDialogState) {
return AlertDialog(
title: Text(title),
content: TvColorPicker(
initialColor: pickerColor,
onColorChanged: (color) => setDialogState(() => pickerColor = color),
onConfirm: () => saveFocusNode.requestFocus(),
),
actions: [
FocusableButton(
onPressed: () => Navigator.pop(dialogContext),
child: TextButton(onPressed: () => Navigator.pop(dialogContext), child: Text(t.common.cancel)),
),
FocusableButton(
focusNode: saveFocusNode,
onPressed: () {
onColorSelected(_colorToHex(pickerColor));
Navigator.pop(dialogContext);
},
child: TextButton(
onPressed: () {
onColorSelected(_colorToHex(pickerColor));
Navigator.pop(dialogContext);
},
child: Text(t.common.save),
),
),
],
);
},
);
},
).then((_) => saveFocusNode.dispose());
}
void _openColorPicker(String title, String currentColor, Function(String) onColorSelected) {
if (InputModeTracker.isKeyboardMode(context)) {
_showTvColorPicker(title, currentColor, onColorSelected);
} else {
_showColorPicker(title, currentColor, onColorSelected);
}
}
String _formatPosition(int value) {
if (value == 0) return 'Top';
if (value == 100) return 'Bottom';
return '$value%';
}
@override
Widget build(BuildContext context) {
if (_isLoading) {
return FocusedScrollScaffold(
title: Text(t.screens.subtitleStyling),
slivers: [const SliverFillRemaining(child: Center(child: CircularProgressIndicator()))],
);
}
return FocusedScrollScaffold(
title: Text(t.screens.subtitleStyling),
slivers: [
SliverList(
delegate: SliverChildListDelegate([
// --- Text ---
SettingsSectionHeader(t.subtitlingStyling.text),
ListTile(
leading: const AppIcon(Symbols.format_size_rounded, fill: 1),
title: Text(t.subtitlingStyling.fontSize),
subtitle: Text('$_fontSize'),
trailing: const AppIcon(Symbols.chevron_right_rounded, fill: 1),
onTap: () => showNumericInputDialog(
context: context,
title: t.subtitlingStyling.fontSize,
labelText: t.subtitlingStyling.fontSize,
suffixText: '',
min: 10,
max: 80,
currentValue: _fontSize,
onSave: (value) async {
setState(() => _fontSize = value);
await _settingsService.setSubtitleFontSize(value);
},
),
),
ListTile(
leading: Container(
width: 24,
height: 24,
decoration: BoxDecoration(
color: _hexToColor(_textColor),
border: const Border.fromBorderSide(BorderSide(color: Colors.grey)),
borderRadius: const BorderRadius.all(Radius.circular(4)),
),
),
title: Text(t.subtitlingStyling.textColor),
subtitle: Text(_textColor),
trailing: const AppIcon(Symbols.chevron_right_rounded, fill: 1),
onTap: () => _openColorPicker(t.subtitlingStyling.textColor, _textColor, (color) {
setState(() => _textColor = color);
_settingsService.setSubtitleTextColor(color);
}),
),
ListTile(
leading: const AppIcon(Symbols.vertical_align_bottom_rounded, fill: 1),
title: Text(t.subtitlingStyling.position),
subtitle: Text(_formatPosition(_subtitlePosition)),
trailing: const AppIcon(Symbols.chevron_right_rounded, fill: 1),
onTap: () => showNumericInputDialog(
context: context,
title: t.subtitlingStyling.position,
labelText: t.subtitlingStyling.position,
suffixText: '%',
min: 0,
max: 100,
currentValue: _subtitlePosition,
onSave: (value) async {
setState(() => _subtitlePosition = value);
await _settingsService.setSubtitlePosition(value);
},
),
),
// --- Border ---
SettingsSectionHeader(t.subtitlingStyling.border),
ListTile(
leading: const AppIcon(Symbols.border_style_rounded, fill: 1),
title: Text(t.subtitlingStyling.borderSize),
subtitle: Text('$_borderSize'),
trailing: const AppIcon(Symbols.chevron_right_rounded, fill: 1),
onTap: () => showNumericInputDialog(
context: context,
title: t.subtitlingStyling.borderSize,
labelText: t.subtitlingStyling.borderSize,
suffixText: '',
min: 0,
max: 5,
currentValue: _borderSize,
onSave: (value) async {
setState(() => _borderSize = value);
await _settingsService.setSubtitleBorderSize(value);
},
),
),
ListTile(
leading: Container(
width: 24,
height: 24,
decoration: BoxDecoration(
color: _hexToColor(_borderColor),
border: const Border.fromBorderSide(BorderSide(color: Colors.grey)),
borderRadius: const BorderRadius.all(Radius.circular(4)),
),
),
title: Text(t.subtitlingStyling.borderColor),
subtitle: Text(_borderColor),
trailing: const AppIcon(Symbols.chevron_right_rounded, fill: 1),
onTap: () => _openColorPicker(t.subtitlingStyling.borderColor, _borderColor, (color) {
setState(() => _borderColor = color);
_settingsService.setSubtitleBorderColor(color);
}),
),
// --- Background ---
SettingsSectionHeader(t.subtitlingStyling.background),
ListTile(
leading: const AppIcon(Symbols.opacity_rounded, fill: 1),
title: Text(t.subtitlingStyling.backgroundOpacity),
subtitle: Text('$_backgroundOpacity%'),
trailing: const AppIcon(Symbols.chevron_right_rounded, fill: 1),
onTap: () => showNumericInputDialog(
context: context,
title: t.subtitlingStyling.backgroundOpacity,
labelText: t.subtitlingStyling.backgroundOpacity,
suffixText: '%',
min: 0,
max: 100,
currentValue: _backgroundOpacity,
onSave: (value) async {
setState(() => _backgroundOpacity = value);
await _settingsService.setSubtitleBackgroundOpacity(value);
},
),
),
ListTile(
leading: Container(
width: 24,
height: 24,
decoration: BoxDecoration(
color: _hexToColor(_backgroundColor),
border: const Border.fromBorderSide(BorderSide(color: Colors.grey)),
borderRadius: const BorderRadius.all(Radius.circular(4)),
),
),
title: Text(t.subtitlingStyling.backgroundColor),
subtitle: Text(_backgroundColor),
trailing: const AppIcon(Symbols.chevron_right_rounded, fill: 1),
onTap: () => _openColorPicker(t.subtitlingStyling.backgroundColor, _backgroundColor, (color) {
setState(() => _backgroundColor = color);
_settingsService.setSubtitleBackgroundColor(color);
}),
),
const SizedBox(height: 24),
]),
),
],
);
}
}