mirror of
https://github.com/gabehf/Fladder.git
synced 2026-03-09 07:28:14 -07:00
feature: Handle all media segments skipping (#167)
Co-authored-by: PartyDonut <PartyDonut@users.noreply.github.com>
This commit is contained in:
parent
7877cae8ea
commit
a06591084b
4 changed files with 54 additions and 66 deletions
|
|
@ -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"
|
||||||
}
|
}
|
||||||
|
|
@ -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,
|
||||||
|
|
|
||||||
|
|
@ -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)],
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
|
|
|
||||||
|
|
@ -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);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue