diff --git a/lib/l10n/app_en.arb b/lib/l10n/app_en.arb index 7c58111..092afe5 100644 --- a/lib/l10n/app_en.arb +++ b/lib/l10n/app_en.arb @@ -1133,5 +1133,19 @@ "description": "To indicate a default value, default video player backend" }, "noVideoPlayerOptions": "The selected backend has no options", - "mdkExperimental": "MDK is still in a experimental stage" + "mdkExperimental": "MDK is still in a experimental stage", + "skipButtonLabel": "(S)kip {segment}", + "@skipButtonLabel": { + "placeholders": { + "segment": { + "type": "String" + } + } + }, + "mediaSegmentUnknown": "Unknown", + "mediaSegmentCommercial": "Commercial", + "mediaSegmentPreview": "Preview", + "mediaSegmentRecap": "Recap", + "mediaSegmentOutro": "Outro", + "mediaSegmentIntro": "Intro" } \ No newline at end of file diff --git a/lib/models/items/media_segments_model.dart b/lib/models/items/media_segments_model.dart index f89388a..4f0c728 100644 --- a/lib/models/items/media_segments_model.dart +++ b/lib/models/items/media_segments_model.dart @@ -4,6 +4,7 @@ import 'package:collection/collection.dart'; import 'package:freezed_annotation/freezed_annotation.dart'; import 'package:fladder/jellyfin/jellyfin_open_api.swagger.dart' as dto; +import 'package:fladder/util/localization_helper.dart'; part 'media_segments_model.freezed.dart'; part 'media_segments_model.g.dart'; @@ -18,6 +19,8 @@ class MediaSegmentsModel with _$MediaSegmentsModel { factory MediaSegmentsModel.fromJson(Map json) => _$MediaSegmentsModelFromJson(json); + MediaSegment? atPosition(Duration position) => segments.firstWhereOrNull((element) => element.inRange(position)); + MediaSegment? get intro => segments.firstWhereOrNull((element) => element.type == MediaSegmentType.intro); MediaSegment? get outro => segments.firstWhereOrNull((element) => element.type == MediaSegmentType.outro); } @@ -35,6 +38,8 @@ class MediaSegment with _$MediaSegment { factory MediaSegment.fromJson(Map json) => _$MediaSegmentFromJson(json); bool inRange(Duration position) => (position.compareTo(start) >= 0 && position.compareTo(end) <= 0); + + bool forceShow(Duration position) => (position - start).inSeconds < (end - start).inSeconds * 0.20; } enum MediaSegmentType { @@ -45,6 +50,17 @@ enum MediaSegmentType { outro, intro; + String label(BuildContext context) { + return switch (this) { + MediaSegmentType.unknown => context.localized.mediaSegmentUnknown, + MediaSegmentType.commercial => context.localized.mediaSegmentCommercial, + MediaSegmentType.preview => context.localized.mediaSegmentPreview, + MediaSegmentType.recap => context.localized.mediaSegmentRecap, + MediaSegmentType.outro => context.localized.mediaSegmentOutro, + MediaSegmentType.intro => context.localized.mediaSegmentIntro, + }; + } + Color get color => switch (this) { MediaSegmentType.unknown => Colors.black, MediaSegmentType.commercial => Colors.purpleAccent, diff --git a/lib/screens/video_player/components/video_player_controls_extras.dart b/lib/screens/video_player/components/video_player_controls_extras.dart index 4d277c0..d31b45a 100644 --- a/lib/screens/video_player/components/video_player_controls_extras.dart +++ b/lib/screens/video_player/components/video_player_controls_extras.dart @@ -60,47 +60,26 @@ class OpenQueueButton extends ConsumerWidget { } } -class IntroSkipButton extends ConsumerWidget { +class SkipSegmentButton extends ConsumerWidget { + final String label; final bool isOverlayVisible; - final Function()? skipIntro; - const IntroSkipButton({this.skipIntro, required this.isOverlayVisible, super.key}); + final Function() pressedSkip; + const SkipSegmentButton({required this.label, required this.isOverlayVisible, required this.pressedSkip, super.key}); @override Widget build(BuildContext context, WidgetRef ref) { - return ElevatedButton( - onPressed: () => skipIntro?.call(), - style: ElevatedButton.styleFrom(shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(5))), - child: const Padding( - padding: EdgeInsets.all(8), - child: Row( - mainAxisSize: MainAxisSize.min, - children: [Text("(S)kip Intro"), Icon(Icons.skip_next_rounded)], - ), - ), - ); - } -} - -class OutroSkipButton extends ConsumerWidget { - final bool isOverlayVisible; - final Function()? skipOutro; - const OutroSkipButton({this.skipOutro, required this.isOverlayVisible, super.key}); - - @override - Widget build(BuildContext context, WidgetRef ref) { - // final semiHideSkip = skipCredits. return AnimatedOpacity( - opacity: 1, - duration: const Duration(milliseconds: 250), + opacity: isOverlayVisible ? 0.85 : 0, + duration: const Duration(milliseconds: 500), child: ElevatedButton( - onPressed: () => skipOutro?.call(), + onPressed: pressedSkip, style: ElevatedButton.styleFrom(shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(5))), - child: const Padding( - padding: EdgeInsets.all(8), + child: Padding( + padding: const EdgeInsets.all(8), child: Row( mainAxisSize: MainAxisSize.min, - children: [Text("(S)kip Credits"), Icon(Icons.skip_next_rounded)], + children: [Text(label), const Icon(Icons.skip_next_rounded)], ), ), ), diff --git a/lib/screens/video_player/video_player_controls.dart b/lib/screens/video_player/video_player_controls.dart index eb3ef20..0a2b2ab 100644 --- a/lib/screens/video_player/video_player_controls.dart +++ b/lib/screens/video_player/video_player_controls.dart @@ -54,8 +54,7 @@ class _DesktopControlsState extends ConsumerState { bool _onKey(KeyEvent value) { final mediaSegments = ref.read(playBackModel.select((value) => value?.mediaSegments)); final position = ref.read(mediaPlaybackProvider).position; - bool showIntroSkipButton = mediaSegments?.intro?.inRange(position) ?? false; - bool showOutroSkipButton = mediaSegments?.outro?.inRange(position) ?? false; + MediaSegment? segment = mediaSegments?.atPosition(position); if (value is KeyRepeatEvent) { if (value.logicalKey == LogicalKeyboardKey.arrowUp) { resetTimer(); @@ -70,10 +69,8 @@ class _DesktopControlsState extends ConsumerState { } if (value is KeyDownEvent) { if (value.logicalKey == LogicalKeyboardKey.keyS) { - if (showIntroSkipButton) { - skipIntro(mediaSegments); - } else if (showOutroSkipButton) { - skipOutro(mediaSegments); + if (segment != null) { + skipToSegmentEnd(segment); } return true; } @@ -158,32 +155,22 @@ class _DesktopControlsState extends ConsumerState { Consumer( builder: (context, ref, child) { final position = ref.watch(mediaPlaybackProvider.select((value) => value.position)); - bool showIntroSkipButton = mediaSegments?.intro?.inRange(position) ?? false; - bool showOutroSkipButton = mediaSegments?.outro?.inRange(position) ?? false; + MediaSegment? segment = mediaSegments?.atPosition(position); + bool forceShow = segment?.forceShow(position) ?? false; return Stack( children: [ - if (showIntroSkipButton) + if (segment != null) Align( alignment: Alignment.centerRight, child: Padding( padding: const EdgeInsets.all(32), - child: IntroSkipButton( - isOverlayVisible: showOverlay, - skipIntro: () => skipIntro(mediaSegments), + child: SkipSegmentButton( + label: context.localized.skipButtonLabel(segment.type.label(context).toLowerCase()), + isOverlayVisible: forceShow ? true : showOverlay, + pressedSkip: () => skipToSegmentEnd(segment), ), ), ), - if (showOutroSkipButton) - Align( - alignment: Alignment.centerRight, - child: Padding( - padding: const EdgeInsets.all(32), - child: OutroSkipButton( - isOverlayVisible: showOverlay, - skipOutro: () => skipOutro(mediaSegments), - ), - ), - ) ], ); }, @@ -580,18 +567,10 @@ class _DesktopControlsState extends ConsumerState { ); } - void skipIntro(MediaSegmentsModel? mediaSegments) { - resetTimer(); - final end = mediaSegments?.intro?.end; - if (end != null) { - ref.read(videoPlayerProvider).seek(end); - } - } - - void skipOutro(MediaSegmentsModel? mediaSegments) { - resetTimer(); - final end = mediaSegments?.outro?.end; + void skipToSegmentEnd(MediaSegment? mediaSegments) { + final end = mediaSegments?.end; if (end != null) { + resetTimer(); ref.read(videoPlayerProvider).seek(end); } }