diff --git a/lib/screens/settings_screen.dart b/lib/screens/settings_screen.dart index c8f41d38..9e885e35 100644 --- a/lib/screens/settings_screen.dart +++ b/lib/screens/settings_screen.dart @@ -11,6 +11,7 @@ import '../widgets/desktop_app_bar.dart'; import '../widgets/hotkey_recorder_widget.dart'; import 'about_screen.dart'; import 'logs_screen.dart'; +import 'subtitle_styling_screen.dart'; class SettingsScreen extends StatefulWidget { const SettingsScreen({super.key}); @@ -207,6 +208,20 @@ class _SettingsScreenState extends State { trailing: const Icon(Icons.chevron_right), onTap: () => _showBufferSizeDialog(), ), + ListTile( + leading: const Icon(Icons.subtitles), + title: const Text('Subtitle Styling'), + subtitle: const Text('Customize subtitle appearance'), + trailing: const Icon(Icons.chevron_right), + onTap: () { + Navigator.push( + context, + MaterialPageRoute( + builder: (context) => const SubtitleStylingScreen(), + ), + ); + }, + ), ListTile( leading: const Icon(Icons.replay_10), title: const Text('Small Skip Duration'), diff --git a/lib/screens/subtitle_styling_screen.dart b/lib/screens/subtitle_styling_screen.dart new file mode 100644 index 00000000..c4cbeb58 --- /dev/null +++ b/lib/screens/subtitle_styling_screen.dart @@ -0,0 +1,328 @@ +import 'package:flutter/material.dart'; +import 'package:flex_color_picker/flex_color_picker.dart'; +import '../services/settings_service.dart'; +import '../widgets/desktop_app_bar.dart'; + +class SubtitleStylingScreen extends StatefulWidget { + const SubtitleStylingScreen({super.key}); + + @override + State createState() => _SubtitleStylingScreenState(); +} + +class _SubtitleStylingScreenState extends State { + static const EdgeInsets _sliderPadding = EdgeInsets.fromLTRB(16, 16, 16, 8); + + late SettingsService _settingsService; + bool _isLoading = true; + + int _fontSize = 55; + String _textColor = '#FFFFFF'; + int _borderSize = 3; + String _borderColor = '#000000'; + String _backgroundColor = '#000000'; + int _backgroundOpacity = 0; + + @override + void initState() { + super.initState(); + _loadSettings(); + } + + Future _loadSettings() async { + _settingsService = await SettingsService.getInstance(); + + setState(() { + _fontSize = _settingsService.getSubtitleFontSize(); + _textColor = _settingsService.getSubtitleTextColor(); + _borderSize = _settingsService.getSubtitleBorderSize(); + _borderColor = _settingsService.getSubtitleBorderColor(); + _backgroundColor = _settingsService.getSubtitleBackgroundColor(); + _backgroundOpacity = _settingsService.getSubtitleBackgroundOpacity(); + _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.red.toRadixString(16).padLeft(2, '0')}${color.green.toRadixString(16).padLeft(2, '0')}${color.blue.toRadixString(16).padLeft(2, '0')}'.toUpperCase(); + } + + Future _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.both: false, + ColorPickerType.primary: true, + ColorPickerType.accent: false, + ColorPickerType.wheel: true, + ColorPickerType.custom: false, + }, + actionButtons: const ColorPickerActionButtons( + okButton: true, + closeButton: true, + dialogActionButtons: false, + ), + ); + + if (selectedColor != null) { + final hexColor = _colorToHex(selectedColor); + onColorSelected(hexColor); + } + } + + @override + Widget build(BuildContext context) { + if (_isLoading) { + return const Scaffold(body: Center(child: CircularProgressIndicator())); + } + + return Scaffold( + body: CustomScrollView( + slivers: [ + const CustomAppBar(title: Text('Subtitle Styling'), pinned: true), + SliverPadding( + padding: const EdgeInsets.all(16), + sliver: SliverList( + delegate: SliverChildListDelegate([ + _buildStylingCard(), + const SizedBox(height: 24), + ]), + ), + ), + ], + ), + ); + } + + Widget _buildStylingCard() { + return Card( + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Padding( + padding: const EdgeInsets.all(16), + child: Text( + 'Styling Options', + style: Theme.of(context).textTheme.titleMedium?.copyWith(fontWeight: FontWeight.bold), + ), + ), + // Font Size Slider + Padding( + padding: _sliderPadding, + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + const Text('Font Size'), + Text('$_fontSize'), + ], + ), + const SizedBox(height: 8), + Row( + children: [ + const Text('30', style: TextStyle(fontSize: 12, color: Colors.grey)), + Expanded( + child: Slider( + value: _fontSize.toDouble(), + min: 30, + max: 80, + divisions: 50, + label: _fontSize.toString(), + onChanged: (value) { + setState(() { + _fontSize = value.toInt(); + }); + }, + onChangeEnd: (value) { + _settingsService.setSubtitleFontSize(_fontSize); + }, + ), + ), + const Text('80', style: TextStyle(fontSize: 12, color: Colors.grey)), + ], + ), + ], + ), + ), + const Divider(), + // Text Color + ListTile( + leading: Container( + width: 40, + height: 40, + decoration: BoxDecoration( + color: _hexToColor(_textColor), + border: Border.all(color: Colors.grey), + borderRadius: BorderRadius.circular(4), + ), + ), + title: const Text('Text Color'), + subtitle: Text(_textColor), + trailing: const Icon(Icons.chevron_right), + onTap: () { + _showColorPicker('Text Color', _textColor, (color) { + setState(() { + _textColor = color; + }); + _settingsService.setSubtitleTextColor(color); + }); + }, + ), + const Divider(), + // Border Size Slider + Padding( + padding: _sliderPadding, + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + const Text('Border Size'), + Text('$_borderSize'), + ], + ), + const SizedBox(height: 8), + Row( + children: [ + const Text('0', style: TextStyle(fontSize: 12, color: Colors.grey)), + Expanded( + child: Slider( + value: _borderSize.toDouble(), + min: 0, + max: 5, + divisions: 5, + label: _borderSize.toString(), + onChanged: (value) { + setState(() { + _borderSize = value.toInt(); + }); + }, + onChangeEnd: (value) { + _settingsService.setSubtitleBorderSize(_borderSize); + }, + ), + ), + const Text('5', style: TextStyle(fontSize: 12, color: Colors.grey)), + ], + ), + ], + ), + ), + const Divider(), + // Border Color + ListTile( + leading: Container( + width: 40, + height: 40, + decoration: BoxDecoration( + color: _hexToColor(_borderColor), + border: Border.all(color: Colors.grey), + borderRadius: BorderRadius.circular(4), + ), + ), + title: const Text('Border Color'), + subtitle: Text(_borderColor), + trailing: const Icon(Icons.chevron_right), + onTap: () { + _showColorPicker('Border Color', _borderColor, (color) { + setState(() { + _borderColor = color; + }); + _settingsService.setSubtitleBorderColor(color); + }); + }, + ), + const Divider(), + // Background Opacity Slider + Padding( + padding: _sliderPadding, + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + const Text('Background Opacity'), + Text('$_backgroundOpacity%'), + ], + ), + const SizedBox(height: 8), + Row( + children: [ + const Text('0%', style: TextStyle(fontSize: 12, color: Colors.grey)), + Expanded( + child: Slider( + value: _backgroundOpacity.toDouble(), + min: 0, + max: 100, + divisions: 20, + label: '$_backgroundOpacity%', + onChanged: (value) { + setState(() { + _backgroundOpacity = value.toInt(); + }); + }, + onChangeEnd: (value) { + _settingsService.setSubtitleBackgroundOpacity(_backgroundOpacity); + }, + ), + ), + const Text('100%', style: TextStyle(fontSize: 12, color: Colors.grey)), + ], + ), + ], + ), + ), + const Divider(), + // Background Color + ListTile( + leading: Container( + width: 40, + height: 40, + decoration: BoxDecoration( + color: _hexToColor(_backgroundColor), + border: Border.all(color: Colors.grey), + borderRadius: BorderRadius.circular(4), + ), + ), + title: const Text('Background Color'), + subtitle: Text(_backgroundColor), + trailing: const Icon(Icons.chevron_right), + onTap: () { + _showColorPicker('Background Color', _backgroundColor, (color) { + setState(() { + _backgroundColor = color; + }); + _settingsService.setSubtitleBackgroundColor(color); + }); + }, + ), + ], + ), + ); + } +} diff --git a/lib/screens/video_player_screen.dart b/lib/screens/video_player_screen.dart index 1b965b13..260505c2 100644 --- a/lib/screens/video_player_screen.dart +++ b/lib/screens/video_player_screen.dart @@ -120,6 +120,13 @@ class VideoPlayerScreenState extends State { libassAndroidFontName: 'Droid Sans Fallback', bufferSize: bufferSizeBytes, logLevel: debugLoggingEnabled ? MPVLogLevel.debug : MPVLogLevel.error, + mpvConfiguration: { + 'sub-font-size': settingsService.getSubtitleFontSize().toString(), + 'sub-color': settingsService.getSubtitleTextColor(), + 'sub-border-size': settingsService.getSubtitleBorderSize().toString(), + 'sub-border-color': settingsService.getSubtitleBorderColor(), + 'sub-back-color': '#${(settingsService.getSubtitleBackgroundOpacity() * 255 / 100).toInt().toRadixString(16).padLeft(2, '0').toUpperCase()}${settingsService.getSubtitleBackgroundColor().replaceFirst('#', '')}', + } ), ); controller = VideoController( diff --git a/lib/services/settings_service.dart b/lib/services/settings_service.dart index 79e2932a..aefd06c4 100644 --- a/lib/services/settings_service.dart +++ b/lib/services/settings_service.dart @@ -31,6 +31,12 @@ class SettingsService { static const String _keySubtitleSyncOffset = 'subtitle_sync_offset'; static const String _keyVolume = 'volume'; static const String _keyRotationLocked = 'rotation_locked'; + static const String _keySubtitleFontSize = 'subtitle_font_size'; + static const String _keySubtitleTextColor = 'subtitle_text_color'; + static const String _keySubtitleBorderSize = 'subtitle_border_size'; + static const String _keySubtitleBorderColor = 'subtitle_border_color'; + static const String _keySubtitleBackgroundColor = 'subtitle_background_color'; + static const String _keySubtitleBackgroundOpacity = 'subtitle_background_opacity'; static SettingsService? _instance; late SharedPreferences _prefs; @@ -220,6 +226,62 @@ class SettingsService { return _prefs.getBool(_keyRotationLocked) ?? true; // Default: locked (landscape only) } + // Subtitle Styling Settings + + // Font Size (30-80, default 55) + Future setSubtitleFontSize(int size) async { + await _prefs.setInt(_keySubtitleFontSize, size); + } + + int getSubtitleFontSize() { + return _prefs.getInt(_keySubtitleFontSize) ?? 55; + } + + // Text Color (hex format #RRGGBB, default white) + Future setSubtitleTextColor(String color) async { + await _prefs.setString(_keySubtitleTextColor, color); + } + + String getSubtitleTextColor() { + return _prefs.getString(_keySubtitleTextColor) ?? '#FFFFFF'; + } + + // Border Size (0-5, default 3) + Future setSubtitleBorderSize(int size) async { + await _prefs.setInt(_keySubtitleBorderSize, size); + } + + int getSubtitleBorderSize() { + return _prefs.getInt(_keySubtitleBorderSize) ?? 3; + } + + // Border Color (hex format #RRGGBB, default black) + Future setSubtitleBorderColor(String color) async { + await _prefs.setString(_keySubtitleBorderColor, color); + } + + String getSubtitleBorderColor() { + return _prefs.getString(_keySubtitleBorderColor) ?? '#000000'; + } + + // Background Color (hex format #RRGGBB, default black) + Future setSubtitleBackgroundColor(String color) async { + await _prefs.setString(_keySubtitleBackgroundColor, color); + } + + String getSubtitleBackgroundColor() { + return _prefs.getString(_keySubtitleBackgroundColor) ?? '#000000'; + } + + // Background Opacity (0-100, default 0 for transparent) + Future setSubtitleBackgroundOpacity(int opacity) async { + await _prefs.setInt(_keySubtitleBackgroundOpacity, opacity); + } + + int getSubtitleBackgroundOpacity() { + return _prefs.getInt(_keySubtitleBackgroundOpacity) ?? 0; + } + // Keyboard Shortcuts (Legacy String-based) Map getDefaultKeyboardShortcuts() { return { @@ -737,6 +799,12 @@ class SettingsService { _prefs.remove(_keyAudioSyncOffset), _prefs.remove(_keySubtitleSyncOffset), _prefs.remove(_keyVolume), + _prefs.remove(_keySubtitleFontSize), + _prefs.remove(_keySubtitleTextColor), + _prefs.remove(_keySubtitleBorderSize), + _prefs.remove(_keySubtitleBorderColor), + _prefs.remove(_keySubtitleBackgroundColor), + _prefs.remove(_keySubtitleBackgroundOpacity), ]); } diff --git a/pubspec.lock b/pubspec.lock index 27652cc9..8c3f79fc 100644 --- a/pubspec.lock +++ b/pubspec.lock @@ -249,6 +249,22 @@ packages: url: "https://pub.dev" source: hosted version: "1.1.1" + flex_color_picker: + dependency: "direct main" + description: + name: flex_color_picker + sha256: f5b0b53d4ae0d59b1e28dfc21d5398e5028cf8e764518e491a52fd050aa23881 + url: "https://pub.dev" + source: hosted + version: "3.7.2" + flex_seed_scheme: + dependency: transitive + description: + name: flex_seed_scheme + sha256: "828291a5a4d4283590541519d8b57821946660ac61d2e07d955f81cfcab22e5d" + url: "https://pub.dev" + source: hosted + version: "3.6.1" flutter: dependency: "direct main" description: flutter @@ -266,10 +282,10 @@ packages: dependency: "direct dev" description: name: flutter_launcher_icons - sha256: "526faf84284b86a4cb36d20a5e45147747b7563d921373d4ee0559c54fcdbcea" + sha256: "10f13781741a2e3972126fae08393d3c4e01fa4cd7473326b94b72cf594195e7" url: "https://pub.dev" source: hosted - version: "0.13.1" + version: "0.14.4" flutter_lints: dependency: "direct dev" description: @@ -476,8 +492,8 @@ packages: dependency: "direct main" description: path: media_kit - ref: HEAD - resolved-ref: "5d002a6bd9c4b4247f83f1c0719f8144daf87f0f" + ref: custom-mpv-config + resolved-ref: fd7bd79a9ba691306038c61791ea5754a5287d8a url: "https://github.com/edde746/media-kit" source: git version: "1.2.1" diff --git a/pubspec.yaml b/pubspec.yaml index c4760733..1925f9af 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -24,11 +24,13 @@ dependencies: package_info_plus: ^9.0.0 provider: ^6.1.2 hotkey_manager: ^0.2.3 + flex_color_picker: ^3.6.0 dependency_overrides: media_kit: git: url: https://github.com/edde746/media-kit + ref: custom-mpv-config path: media_kit dev_dependencies: