feature: Handle all media segments skipping (#167)

Co-authored-by: PartyDonut <PartyDonut@users.noreply.github.com>
This commit is contained in:
PartyDonut 2024-11-28 22:42:30 +01:00 committed by GitHub
parent 7877cae8ea
commit a06591084b
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
4 changed files with 54 additions and 66 deletions

View file

@ -1133,5 +1133,19 @@
"description": "To indicate a default value, default video player backend" "description": "To indicate a default value, default video player backend"
}, },
"noVideoPlayerOptions": "The selected backend has no options", "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"
} }

View file

@ -4,6 +4,7 @@ import 'package:collection/collection.dart';
import 'package:freezed_annotation/freezed_annotation.dart'; import 'package:freezed_annotation/freezed_annotation.dart';
import 'package:fladder/jellyfin/jellyfin_open_api.swagger.dart' as dto; 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.freezed.dart';
part 'media_segments_model.g.dart'; part 'media_segments_model.g.dart';
@ -18,6 +19,8 @@ class MediaSegmentsModel with _$MediaSegmentsModel {
factory MediaSegmentsModel.fromJson(Map<String, dynamic> json) => _$MediaSegmentsModelFromJson(json); factory MediaSegmentsModel.fromJson(Map<String, dynamic> 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 intro => segments.firstWhereOrNull((element) => element.type == MediaSegmentType.intro);
MediaSegment? get outro => segments.firstWhereOrNull((element) => element.type == MediaSegmentType.outro); MediaSegment? get outro => segments.firstWhereOrNull((element) => element.type == MediaSegmentType.outro);
} }
@ -35,6 +38,8 @@ class MediaSegment with _$MediaSegment {
factory MediaSegment.fromJson(Map<String, dynamic> json) => _$MediaSegmentFromJson(json); factory MediaSegment.fromJson(Map<String, dynamic> json) => _$MediaSegmentFromJson(json);
bool inRange(Duration position) => (position.compareTo(start) >= 0 && position.compareTo(end) <= 0); 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 { enum MediaSegmentType {
@ -45,6 +50,17 @@ enum MediaSegmentType {
outro, outro,
intro; 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) { Color get color => switch (this) {
MediaSegmentType.unknown => Colors.black, MediaSegmentType.unknown => Colors.black,
MediaSegmentType.commercial => Colors.purpleAccent, MediaSegmentType.commercial => Colors.purpleAccent,

View file

@ -60,47 +60,26 @@ class OpenQueueButton extends ConsumerWidget {
} }
} }
class IntroSkipButton extends ConsumerWidget { class SkipSegmentButton extends ConsumerWidget {
final String label;
final bool isOverlayVisible; final bool isOverlayVisible;
final Function()? skipIntro; final Function() pressedSkip;
const IntroSkipButton({this.skipIntro, required this.isOverlayVisible, super.key}); const SkipSegmentButton({required this.label, required this.isOverlayVisible, required this.pressedSkip, super.key});
@override @override
Widget build(BuildContext context, WidgetRef ref) { 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( return AnimatedOpacity(
opacity: 1, opacity: isOverlayVisible ? 0.85 : 0,
duration: const Duration(milliseconds: 250), duration: const Duration(milliseconds: 500),
child: ElevatedButton( child: ElevatedButton(
onPressed: () => skipOutro?.call(), onPressed: pressedSkip,
style: ElevatedButton.styleFrom(shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(5))), style: ElevatedButton.styleFrom(shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(5))),
child: const Padding( child: Padding(
padding: EdgeInsets.all(8), padding: const EdgeInsets.all(8),
child: Row( child: Row(
mainAxisSize: MainAxisSize.min, mainAxisSize: MainAxisSize.min,
children: [Text("(S)kip Credits"), Icon(Icons.skip_next_rounded)], children: [Text(label), const Icon(Icons.skip_next_rounded)],
), ),
), ),
), ),

View file

@ -54,8 +54,7 @@ class _DesktopControlsState extends ConsumerState<DesktopControls> {
bool _onKey(KeyEvent value) { bool _onKey(KeyEvent value) {
final mediaSegments = ref.read(playBackModel.select((value) => value?.mediaSegments)); final mediaSegments = ref.read(playBackModel.select((value) => value?.mediaSegments));
final position = ref.read(mediaPlaybackProvider).position; final position = ref.read(mediaPlaybackProvider).position;
bool showIntroSkipButton = mediaSegments?.intro?.inRange(position) ?? false; MediaSegment? segment = mediaSegments?.atPosition(position);
bool showOutroSkipButton = mediaSegments?.outro?.inRange(position) ?? false;
if (value is KeyRepeatEvent) { if (value is KeyRepeatEvent) {
if (value.logicalKey == LogicalKeyboardKey.arrowUp) { if (value.logicalKey == LogicalKeyboardKey.arrowUp) {
resetTimer(); resetTimer();
@ -70,10 +69,8 @@ class _DesktopControlsState extends ConsumerState<DesktopControls> {
} }
if (value is KeyDownEvent) { if (value is KeyDownEvent) {
if (value.logicalKey == LogicalKeyboardKey.keyS) { if (value.logicalKey == LogicalKeyboardKey.keyS) {
if (showIntroSkipButton) { if (segment != null) {
skipIntro(mediaSegments); skipToSegmentEnd(segment);
} else if (showOutroSkipButton) {
skipOutro(mediaSegments);
} }
return true; return true;
} }
@ -158,32 +155,22 @@ class _DesktopControlsState extends ConsumerState<DesktopControls> {
Consumer( Consumer(
builder: (context, ref, child) { builder: (context, ref, child) {
final position = ref.watch(mediaPlaybackProvider.select((value) => value.position)); final position = ref.watch(mediaPlaybackProvider.select((value) => value.position));
bool showIntroSkipButton = mediaSegments?.intro?.inRange(position) ?? false; MediaSegment? segment = mediaSegments?.atPosition(position);
bool showOutroSkipButton = mediaSegments?.outro?.inRange(position) ?? false; bool forceShow = segment?.forceShow(position) ?? false;
return Stack( return Stack(
children: [ children: [
if (showIntroSkipButton) if (segment != null)
Align( Align(
alignment: Alignment.centerRight, alignment: Alignment.centerRight,
child: Padding( child: Padding(
padding: const EdgeInsets.all(32), padding: const EdgeInsets.all(32),
child: IntroSkipButton( child: SkipSegmentButton(
isOverlayVisible: showOverlay, label: context.localized.skipButtonLabel(segment.type.label(context).toLowerCase()),
skipIntro: () => skipIntro(mediaSegments), 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<DesktopControls> {
); );
} }
void skipIntro(MediaSegmentsModel? mediaSegments) { void skipToSegmentEnd(MediaSegment? mediaSegments) {
resetTimer(); final end = mediaSegments?.end;
final end = mediaSegments?.intro?.end;
if (end != null) {
ref.read(videoPlayerProvider).seek(end);
}
}
void skipOutro(MediaSegmentsModel? mediaSegments) {
resetTimer();
final end = mediaSegments?.outro?.end;
if (end != null) { if (end != null) {
resetTimer();
ref.read(videoPlayerProvider).seek(end); ref.read(videoPlayerProvider).seek(end);
} }
} }