feat: subtitle styling
This commit is contained in:
@@ -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<SettingsScreen> {
|
||||
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'),
|
||||
|
||||
@@ -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<SubtitleStylingScreen> createState() => _SubtitleStylingScreenState();
|
||||
}
|
||||
|
||||
class _SubtitleStylingScreenState extends State<SubtitleStylingScreen> {
|
||||
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<void> _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<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,
|
||||
),
|
||||
);
|
||||
|
||||
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);
|
||||
});
|
||||
},
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -120,6 +120,13 @@ class VideoPlayerScreenState extends State<VideoPlayerScreen> {
|
||||
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(
|
||||
|
||||
@@ -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<void> setSubtitleFontSize(int size) async {
|
||||
await _prefs.setInt(_keySubtitleFontSize, size);
|
||||
}
|
||||
|
||||
int getSubtitleFontSize() {
|
||||
return _prefs.getInt(_keySubtitleFontSize) ?? 55;
|
||||
}
|
||||
|
||||
// Text Color (hex format #RRGGBB, default white)
|
||||
Future<void> setSubtitleTextColor(String color) async {
|
||||
await _prefs.setString(_keySubtitleTextColor, color);
|
||||
}
|
||||
|
||||
String getSubtitleTextColor() {
|
||||
return _prefs.getString(_keySubtitleTextColor) ?? '#FFFFFF';
|
||||
}
|
||||
|
||||
// Border Size (0-5, default 3)
|
||||
Future<void> setSubtitleBorderSize(int size) async {
|
||||
await _prefs.setInt(_keySubtitleBorderSize, size);
|
||||
}
|
||||
|
||||
int getSubtitleBorderSize() {
|
||||
return _prefs.getInt(_keySubtitleBorderSize) ?? 3;
|
||||
}
|
||||
|
||||
// Border Color (hex format #RRGGBB, default black)
|
||||
Future<void> setSubtitleBorderColor(String color) async {
|
||||
await _prefs.setString(_keySubtitleBorderColor, color);
|
||||
}
|
||||
|
||||
String getSubtitleBorderColor() {
|
||||
return _prefs.getString(_keySubtitleBorderColor) ?? '#000000';
|
||||
}
|
||||
|
||||
// Background Color (hex format #RRGGBB, default black)
|
||||
Future<void> 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<void> setSubtitleBackgroundOpacity(int opacity) async {
|
||||
await _prefs.setInt(_keySubtitleBackgroundOpacity, opacity);
|
||||
}
|
||||
|
||||
int getSubtitleBackgroundOpacity() {
|
||||
return _prefs.getInt(_keySubtitleBackgroundOpacity) ?? 0;
|
||||
}
|
||||
|
||||
// Keyboard Shortcuts (Legacy String-based)
|
||||
Map<String, String> 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),
|
||||
]);
|
||||
}
|
||||
|
||||
|
||||
+20
-4
@@ -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"
|
||||
|
||||
@@ -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:
|
||||
|
||||
Reference in New Issue
Block a user