Subtitle customization

This commit is contained in:
Abinanthankv 2025-01-09 23:03:36 +05:30
parent 30de3443ed
commit d416ba644d
5 changed files with 255 additions and 13 deletions

View file

@ -21,6 +21,7 @@ import '../types/doc_source.dart';
import 'video_viewer/desktop_video_player.dart';
import 'video_viewer/mobile_video_player.dart';
class VideoViewer extends StatefulWidget {
final DocSource source;
final LibraryItem? meta;
@ -62,11 +63,6 @@ class _VideoViewerState extends State<VideoViewer> {
saveWatchHistory() {
final duration = player.state.duration.inSeconds;
if (duration < 30) {
return;
}
final position = player.state.position.inSeconds;
final progress = duration > 0 ? (position / duration * 100).round() : 0;
@ -191,7 +187,6 @@ class _VideoViewerState extends State<VideoViewer> {
if ((progress ?? []).isEmpty) {
player.play();
return;
}
final duration = Duration(
@ -203,12 +198,51 @@ class _VideoViewerState extends State<VideoViewer> {
player.seek(duration);
player.play();
addListenerForTrakt();
}
List<StreamSubscription> listener = [];
bool traktIntegration = false;
addListenerForTrakt() {
if (traktIntegration == true) {
return;
}
traktIntegration = true;
final streams = player.stream.playing.listen((item) {
if (item) {
TraktService.instance!.startScrobbling(
meta: widget.meta as types.Meta,
progress: currentProgressInPercentage,
);
} else {
TraktService.instance!.pauseScrobbling(
meta: widget.meta as types.Meta,
progress: currentProgressInPercentage,
);
}
});
final oneMore = player.stream.completed.listen((item) {
if (item && player.state.duration.inSeconds > 10) {
TraktService.instance!.stopScrobbling(
meta: widget.meta as types.Meta,
progress: currentProgressInPercentage,
);
}
});
listener.add(streams);
listener.add(oneMore);
}
PlaybackConfig config = getPlaybackConfig();
bool defaultConfigSelected = false;
@override
@ -385,7 +419,7 @@ class _VideoViewerState extends State<VideoViewer> {
_streamListen.cancel();
_duration.cancel();
if (widget.meta is types.Meta && player.state.duration.inSeconds > 30) {
if (traktIntegration && widget.meta is types.Meta) {
TraktService.instance!.stopScrobbling(
meta: widget.meta as types.Meta,
progress: currentProgressInPercentage,
@ -421,13 +455,39 @@ class _VideoViewerState extends State<VideoViewer> {
setState(() {
isScaled = !isScaled;
});
},
);
String subtitleStyleName = config.subtitleStyle ?? 'Normal';
String subtitleStyleColor = config.subtitleColor ?? 'white';
double subtitleSize = config.subtitleSize ;
Color hexToColor(String hexColor) {
final hexCode = hexColor.replaceAll('#', '');
return Color(int.parse('0x$hexCode'));
}
FontStyle getFontStyleFromString(String styleName) {
switch (styleName.toLowerCase()) {
case 'italic':
return FontStyle.italic;
case 'normal': // Explicitly handle 'normal' (good practice)
default: // Default case for any other string or null
return FontStyle.normal;
}
}
FontStyle currentFontStyle = getFontStyleFromString(subtitleStyleName);
return MaterialVideoControlsTheme(
fullscreen: mobile,
normal: mobile,
child: Video(
subtitleViewConfiguration: SubtitleViewConfiguration(
style: TextStyle(color: hexToColor(subtitleStyleColor),
// style: TextStyle(color:
fontSize: subtitleSize,
// fontSize: 60.0,
fontStyle: currentFontStyle,
fontWeight: FontWeight.bold),
),
fit: isScaled ? BoxFit.fitWidth : BoxFit.fitHeight,
pauseUponEnteringBackgroundMode: true,
key: key,
@ -436,8 +496,9 @@ class _VideoViewerState extends State<VideoViewer> {
if (context.mounted) Navigator.of(context).pop();
},
controller: controller,
controls: MaterialVideoControls,
controls: MaterialVideoControls
),
);
}
@ -523,10 +584,12 @@ class _VideoViewerState extends State<VideoViewer> {
languages.containsKey(title)
? languages[title]!
: title,
),
selected:
player.state.track.subtitle.id == currentItem.id,
onTap: () {
player.setSubtitleTrack(currentItem);
Navigator.pop(context);
},

View file

@ -6,6 +6,8 @@ import 'package:pocketbase/pocketbase.dart';
import '../../../engine/engine.dart';
import '../../../utils/load_language.dart';
import 'package:flex_color_picker/flex_color_picker.dart';
import 'package:flutter/services.dart';
class PlaybackSettingsScreen extends StatefulWidget {
const PlaybackSettingsScreen({super.key});
@ -21,13 +23,69 @@ class _PlaybackSettingsScreenState extends State<PlaybackSettingsScreen> {
// Playback settings
bool _autoPlay = true;
double _playbackSpeed = 1.0;
double _subtitleSize = 10.0;
String _defaultAudioTrack = 'eng';
String _defaultSubtitleTrack = 'eng';
bool _enableExternalPlayer = true;
String? _defaultPlayerId;
bool _disabledSubtitle = false;
Map<String, String> _availableLanguages = {};
final List<String> _subtitleStyle = [
'Normal',
'Italic',
];
String colorToHex(Color color) {
return '#${color.value.toRadixString(16).padLeft(8, '0').toUpperCase()}';
}
Color hexToColor(String hexColor) {
final hexCode = hexColor.replaceAll('#', '');
return Color(int.parse('0x$hexCode'));
}
Color _selectedSubtitleColor = Colors.yellow;
_showColorPickerDialog(BuildContext context) async {
Color? color = await showDialog(
context: context,
builder: (BuildContext context) {
return AlertDialog(
title: const Text('Pick a Subtitle Color'),
content: SingleChildScrollView(
child: ColorPicker(
color: _selectedSubtitleColor,
onColorChanged: (Color color) {
_selectedSubtitleColor = color;
},
// Remove pickerType
enableShadesSelection: true,
),
),
actions: <Widget>[
TextButton(
onPressed: () {
Navigator.of(context).pop();
},
child: const Text('Cancel'),
),
TextButton(
onPressed: () {
Navigator.of(context)
.pop(_selectedSubtitleColor); // Return the color
},
child: const Text('OK'),
),
],
);
},
);
if (color != null) {
setState(() {
_selectedSubtitleColor = color;
});
_debouncedSave(); // Debounced save after color change
}
}
List<DropdownMenuItem<String>> get dropdown =>
_availableLanguages.entries.map((item) {
@ -64,6 +122,12 @@ class _PlaybackSettingsScreenState extends State<PlaybackSettingsScreen> {
? playbackConfig.externalPlayerId![currentPlatform]
: null;
_disabledSubtitle = playbackConfig.disableSubtitle;
final subtitleStyle = playbackConfig.subtitleStyle; // Get saved style
_subtitleStyle.removeWhere((style) =>
style == subtitleStyle); // Remove saved style from dropdown options
_subtitleStyle.insert(0, subtitleStyle ?? "Normal");
_selectedSubtitleColor = hexToColor(playbackConfig.subtitleColor!);
_subtitleSize = playbackConfig.subtitleSize.toDouble();
}
@override
@ -103,6 +167,9 @@ class _PlaybackSettingsScreenState extends State<PlaybackSettingsScreen> {
'externalPlayer': _enableExternalPlayer,
'externalPlayerId': extranalId,
'disableSubtitle': _disabledSubtitle,
'subtitleStyle': _subtitleStyle[0],
'subtitleColor': colorToHex(_selectedSubtitleColor),
'subtitleSize': _subtitleSize,
},
};
@ -134,6 +201,12 @@ class _PlaybackSettingsScreenState extends State<PlaybackSettingsScreen> {
@override
Widget build(BuildContext context) {
final dropdownstyle = _subtitleStyle.map((String value) {
return DropdownMenuItem<String>(
value: value,
child: Text(value),
);
}).toList();
if (_error != null) {
return Scaffold(
body: Center(
@ -178,6 +251,7 @@ class _PlaybackSettingsScreenState extends State<PlaybackSettingsScreen> {
divisions: 18,
label: '${_playbackSpeed.toStringAsFixed(2)}x',
onChanged: (value) {
HapticFeedback.mediumImpact();
setState(() => _playbackSpeed =
double.parse(value.toStringAsFixed(2)));
_debouncedSave();
@ -209,7 +283,7 @@ class _PlaybackSettingsScreenState extends State<PlaybackSettingsScreen> {
_debouncedSave();
},
),
if (!_disabledSubtitle)
if (!_disabledSubtitle) ...[
ListTile(
title: const Text('Default Subtitle Track'),
trailing: DropdownButton<String>(
@ -223,6 +297,87 @@ class _PlaybackSettingsScreenState extends State<PlaybackSettingsScreen> {
},
),
),
Padding(
padding: const EdgeInsets.all(12.0),
child: Material( // Center the text
child: ConstrainedBox( // Prevent overflow
constraints: BoxConstraints(
maxWidth: MediaQuery.of(context).size.width * 0.8, // 80% of screen width
),
child: Text(
'Sample Text',
textAlign: TextAlign.center, // Center text within its box
style: TextStyle(
fontSize: _subtitleSize / 2,
color: _selectedSubtitleColor,
fontStyle: _subtitleStyle[0].toLowerCase() == 'italic'
? FontStyle.italic
: FontStyle.normal,
),
),
),
),
),
ListTile(
title: const Text('Subtitle Style'),
trailing: DropdownButton<String>(
value: _subtitleStyle[0],
items: dropdownstyle,
onChanged: (value) {
HapticFeedback.mediumImpact();
if (value != null) {
setState(() {
// <--- Crucial setState here
_subtitleStyle.remove(value);
_subtitleStyle.insert(0, value);
});
_debouncedSave();
}
},
),
),
ListTile(
title: const Text('Subtitle Color'),
trailing: GestureDetector(
// Use GestureDetector to make the color display tappable
onTap: () => _showColorPickerDialog(context),
child: Container(
width: 40,
height: 40,
decoration: BoxDecoration(
color: _selectedSubtitleColor,
borderRadius: BorderRadius.circular(5),
border: Border.all(color: Colors.grey),
),
),
),
),
ListTile(
title: const Text('Font Size'),
subtitle: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Slider(
value: _subtitleSize,
min: 20.0,
max: 60.0,
divisions: 18,
label: '${_subtitleSize.toStringAsFixed(2)}x',
onChanged: (value) {
HapticFeedback.mediumImpact();
setState(() =>
_subtitleSize = double.parse(value.toStringAsFixed(2)));
_debouncedSave();
},
),
Text('Current: ${_subtitleSize.toStringAsFixed(2)}x'),
],
),
),
],
const Divider(),
if (!isWeb)
SwitchListTile(

View file

@ -24,6 +24,7 @@ Future<Map<String, String>> loadLanguages(BuildContext context) async {
return availableLanguages;
}
PlaybackConfig getPlaybackConfig() {
final user = AppEngine.engine.pb.authStore.record;
if (user == null) {
@ -52,7 +53,10 @@ class PlaybackConfig {
@JsonKey(defaultValue: false)
final bool externalPlayer;
final Map<String, String?>? externalPlayerId;
final String? subtitleStyle;
final String? subtitleColor;
@JsonKey(defaultValue: 10.0)
final double subtitleSize;
PlaybackConfig({
required this.autoPlay,
required this.playbackSpeed,
@ -61,6 +65,9 @@ class PlaybackConfig {
required this.externalPlayer,
required this.disableSubtitle,
this.externalPlayerId,
this.subtitleStyle,
this.subtitleColor,
required this.subtitleSize,
});
String? get currentPlayerPackage {

View file

@ -542,6 +542,22 @@ packages:
url: "https://pub.dev"
source: hosted
version: "1.1.1"
flex_color_picker:
dependency: "direct main"
description:
name: flex_color_picker
sha256: c083b79f1c57eaeed9f464368be376951230b3cb1876323b784626152a86e480
url: "https://pub.dev"
source: hosted
version: "3.7.0"
flex_seed_scheme:
dependency: transitive
description:
name: flex_seed_scheme
sha256: d3ba3c5c92d2d79d45e94b4c6c71d01fac3c15017da1545880c53864da5dfeb0
url: "https://pub.dev"
source: hosted
version: "3.5.0"
flutter:
dependency: "direct main"
description: flutter
@ -1878,4 +1894,4 @@ packages:
version: "3.1.2"
sdks:
dart: ">=3.5.3 <4.0.0"
flutter: ">=3.24.0"
flutter: ">=3.27.0"

View file

@ -57,6 +57,7 @@ dependencies:
cast: ^2.1.0
permission_handler: ^11.3.1
android_intent_plus: ^5.2.1
flex_color_picker: ^3.7.0
dependency_overrides:
media_kit: