From d416ba644dafb8462bf264c47caaf521b4290f94 Mon Sep 17 00:00:00 2001 From: Abinanthankv Date: Thu, 9 Jan 2025 23:03:36 +0530 Subject: [PATCH 1/2] Subtitle customization --- .../doc_viewer/container/video_viewer.dart | 81 ++++++++- .../screen/playback_settings_screen.dart | 159 +++++++++++++++++- lib/utils/load_language.dart | 9 +- pubspec.lock | 18 +- pubspec.yaml | 1 + 5 files changed, 255 insertions(+), 13 deletions(-) diff --git a/lib/features/doc_viewer/container/video_viewer.dart b/lib/features/doc_viewer/container/video_viewer.dart index cb22a72..35653e1 100644 --- a/lib/features/doc_viewer/container/video_viewer.dart +++ b/lib/features/doc_viewer/container/video_viewer.dart @@ -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 { 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 { if ((progress ?? []).isEmpty) { player.play(); - return; } final duration = Duration( @@ -203,12 +198,51 @@ class _VideoViewerState extends State { player.seek(duration); player.play(); + + addListenerForTrakt(); } List 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 { _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 { 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 { if (context.mounted) Navigator.of(context).pop(); }, controller: controller, - controls: MaterialVideoControls, + controls: MaterialVideoControls ), + ); } @@ -523,10 +584,12 @@ class _VideoViewerState extends State { languages.containsKey(title) ? languages[title]! : title, + ), selected: player.state.track.subtitle.id == currentItem.id, onTap: () { + player.setSubtitleTrack(currentItem); Navigator.pop(context); }, diff --git a/lib/features/settings/screen/playback_settings_screen.dart b/lib/features/settings/screen/playback_settings_screen.dart index d29d50e..f98e7df 100644 --- a/lib/features/settings/screen/playback_settings_screen.dart +++ b/lib/features/settings/screen/playback_settings_screen.dart @@ -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 { // 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 _availableLanguages = {}; + final List _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: [ + 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> get dropdown => _availableLanguages.entries.map((item) { @@ -64,6 +122,12 @@ class _PlaybackSettingsScreenState extends State { ? 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 { 'externalPlayer': _enableExternalPlayer, 'externalPlayerId': extranalId, 'disableSubtitle': _disabledSubtitle, + 'subtitleStyle': _subtitleStyle[0], + 'subtitleColor': colorToHex(_selectedSubtitleColor), + 'subtitleSize': _subtitleSize, }, }; @@ -134,6 +201,12 @@ class _PlaybackSettingsScreenState extends State { @override Widget build(BuildContext context) { + final dropdownstyle = _subtitleStyle.map((String value) { + return DropdownMenuItem( + value: value, + child: Text(value), + ); + }).toList(); if (_error != null) { return Scaffold( body: Center( @@ -178,6 +251,7 @@ class _PlaybackSettingsScreenState extends State { 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 { _debouncedSave(); }, ), - if (!_disabledSubtitle) + if (!_disabledSubtitle) ...[ ListTile( title: const Text('Default Subtitle Track'), trailing: DropdownButton( @@ -223,6 +297,87 @@ class _PlaybackSettingsScreenState extends State { }, ), ), + 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( + 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( diff --git a/lib/utils/load_language.dart b/lib/utils/load_language.dart index e850655..82ee437 100644 --- a/lib/utils/load_language.dart +++ b/lib/utils/load_language.dart @@ -24,6 +24,7 @@ Future> 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? 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 { diff --git a/pubspec.lock b/pubspec.lock index 33fd291..0462eb6 100644 --- a/pubspec.lock +++ b/pubspec.lock @@ -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" diff --git a/pubspec.yaml b/pubspec.yaml index 69d45cc..0a83a80 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -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: From 1872a0d0d806b8a477be3ec3d10b9c94a1ad91da Mon Sep 17 00:00:00 2001 From: Abinanthankv Date: Thu, 9 Jan 2025 23:27:06 +0530 Subject: [PATCH 2/2] Update video_viewer.dart --- .../doc_viewer/container/video_viewer.dart | 63 ++++--------------- 1 file changed, 11 insertions(+), 52 deletions(-) diff --git a/lib/features/doc_viewer/container/video_viewer.dart b/lib/features/doc_viewer/container/video_viewer.dart index 35653e1..b01cb10 100644 --- a/lib/features/doc_viewer/container/video_viewer.dart +++ b/lib/features/doc_viewer/container/video_viewer.dart @@ -21,7 +21,6 @@ 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; @@ -63,6 +62,11 @@ class _VideoViewerState extends State { 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; @@ -187,6 +191,7 @@ class _VideoViewerState extends State { if ((progress ?? []).isEmpty) { player.play(); + return; } final duration = Duration( @@ -198,51 +203,12 @@ class _VideoViewerState extends State { player.seek(duration); player.play(); - - addListenerForTrakt(); } List 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 @@ -419,7 +385,7 @@ class _VideoViewerState extends State { _streamListen.cancel(); _duration.cancel(); - if (traktIntegration && widget.meta is types.Meta) { + if (widget.meta is types.Meta && player.state.duration.inSeconds > 30) { TraktService.instance!.stopScrobbling( meta: widget.meta as types.Meta, progress: currentProgressInPercentage, @@ -455,11 +421,9 @@ class _VideoViewerState extends State { setState(() { isScaled = !isScaled; }); - - }, ); - String subtitleStyleName = config.subtitleStyle ?? 'Normal'; + String subtitleStyleName = config.subtitleStyle ?? 'Normal'; String subtitleStyleColor = config.subtitleColor ?? 'white'; double subtitleSize = config.subtitleSize ; Color hexToColor(String hexColor) { @@ -480,11 +444,9 @@ class _VideoViewerState extends State { fullscreen: mobile, normal: mobile, child: Video( - subtitleViewConfiguration: SubtitleViewConfiguration( + subtitleViewConfiguration: SubtitleViewConfiguration( style: TextStyle(color: hexToColor(subtitleStyleColor), - // style: TextStyle(color: fontSize: subtitleSize, - // fontSize: 60.0, fontStyle: currentFontStyle, fontWeight: FontWeight.bold), ), @@ -496,9 +458,8 @@ class _VideoViewerState extends State { if (context.mounted) Navigator.of(context).pop(); }, controller: controller, - controls: MaterialVideoControls + controls: MaterialVideoControls, ), - ); } @@ -584,12 +545,10 @@ class _VideoViewerState extends State { languages.containsKey(title) ? languages[title]! : title, - ), selected: player.state.track.subtitle.id == currentItem.id, onTap: () { - player.setSubtitleTrack(currentItem); Navigator.pop(context); }, @@ -661,4 +620,4 @@ int calculateSecondsFromProgress( final clampedProgress = progressPercentage.clamp(0.0, 100.0); final currentSeconds = (duration * (clampedProgress / 100)).round(); return currentSeconds; -} +} \ No newline at end of file