feature: Add actions for media segments (#236)

Co-authored-by: PartyDonut <PartyDonut@users.noreply.github.com>
This commit is contained in:
PartyDonut 2025-02-23 16:56:49 +01:00 committed by GitHub
parent f0414439f3
commit e4b8a050c7
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
9 changed files with 155 additions and 23 deletions

View file

@ -1179,5 +1179,9 @@
"qualityOptionsTitle": "Quality options",
"qualityOptionsOriginal": "Original",
"qualityOptionsAuto": "Auto",
"version": "Version"
"version": "Version",
"mediaSegmentActions": "Media segment actions",
"segmentActionNone": "None",
"segmentActionAskToSkip": "Ask to skip",
"segmentActionSkip": "Skip"
}

View file

@ -42,6 +42,28 @@ class MediaSegment with _$MediaSegment {
bool forceShow(Duration position) => (position - start).inSeconds < (end - start).inSeconds * 0.20;
}
const Map<MediaSegmentType, SegmentSkip> defaultSegmentSkipValues = {
MediaSegmentType.commercial: SegmentSkip.askToSkip,
MediaSegmentType.preview: SegmentSkip.askToSkip,
MediaSegmentType.recap: SegmentSkip.askToSkip,
MediaSegmentType.outro: SegmentSkip.askToSkip,
MediaSegmentType.intro: SegmentSkip.askToSkip,
};
enum SegmentSkip {
none,
askToSkip,
skip;
const SegmentSkip();
String label(BuildContext context) => switch (this) {
SegmentSkip.none => context.localized.segmentActionNone,
SegmentSkip.askToSkip => context.localized.segmentActionAskToSkip,
SegmentSkip.skip => context.localized.segmentActionSkip,
};
}
enum MediaSegmentType {
unknown,
commercial,

View file

@ -5,6 +5,7 @@ import 'package:flutter/widgets.dart';
import 'package:freezed_annotation/freezed_annotation.dart';
import 'package:fladder/models/items/media_segments_model.dart';
import 'package:fladder/util/bitrate_helper.dart';
import 'package:fladder/util/localization_helper.dart';
@ -28,6 +29,7 @@ class VideoPlayerSettingsModel with _$VideoPlayerSettingsModel {
@Default(Bitrate.original) Bitrate maxHomeBitrate,
@Default(Bitrate.original) Bitrate maxInternetBitrate,
String? audioDevice,
@Default(defaultSegmentSkipValues) Map<MediaSegmentType, SegmentSkip> segmentSkipSettings,
}) = _VideoPlayerSettingsModel;
double get volume => switch (defaultTargetPlatform) {

View file

@ -34,6 +34,8 @@ mixin _$VideoPlayerSettingsModel {
Bitrate get maxHomeBitrate => throw _privateConstructorUsedError;
Bitrate get maxInternetBitrate => throw _privateConstructorUsedError;
String? get audioDevice => throw _privateConstructorUsedError;
Map<MediaSegmentType, SegmentSkip> get segmentSkipSettings =>
throw _privateConstructorUsedError;
/// Serializes this VideoPlayerSettingsModel to a JSON map.
Map<String, dynamic> toJson() => throw _privateConstructorUsedError;
@ -63,7 +65,8 @@ abstract class $VideoPlayerSettingsModelCopyWith<$Res> {
AutoNextType nextVideoType,
Bitrate maxHomeBitrate,
Bitrate maxInternetBitrate,
String? audioDevice});
String? audioDevice,
Map<MediaSegmentType, SegmentSkip> segmentSkipSettings});
}
/// @nodoc
@ -94,6 +97,7 @@ class _$VideoPlayerSettingsModelCopyWithImpl<$Res,
Object? maxHomeBitrate = null,
Object? maxInternetBitrate = null,
Object? audioDevice = freezed,
Object? segmentSkipSettings = null,
}) {
return _then(_value.copyWith(
screenBrightness: freezed == screenBrightness
@ -144,6 +148,10 @@ class _$VideoPlayerSettingsModelCopyWithImpl<$Res,
? _value.audioDevice
: audioDevice // ignore: cast_nullable_to_non_nullable
as String?,
segmentSkipSettings: null == segmentSkipSettings
? _value.segmentSkipSettings
: segmentSkipSettings // ignore: cast_nullable_to_non_nullable
as Map<MediaSegmentType, SegmentSkip>,
) as $Val);
}
}
@ -169,7 +177,8 @@ abstract class _$$VideoPlayerSettingsModelImplCopyWith<$Res>
AutoNextType nextVideoType,
Bitrate maxHomeBitrate,
Bitrate maxInternetBitrate,
String? audioDevice});
String? audioDevice,
Map<MediaSegmentType, SegmentSkip> segmentSkipSettings});
}
/// @nodoc
@ -199,6 +208,7 @@ class __$$VideoPlayerSettingsModelImplCopyWithImpl<$Res>
Object? maxHomeBitrate = null,
Object? maxInternetBitrate = null,
Object? audioDevice = freezed,
Object? segmentSkipSettings = null,
}) {
return _then(_$VideoPlayerSettingsModelImpl(
screenBrightness: freezed == screenBrightness
@ -249,6 +259,10 @@ class __$$VideoPlayerSettingsModelImplCopyWithImpl<$Res>
? _value.audioDevice
: audioDevice // ignore: cast_nullable_to_non_nullable
as String?,
segmentSkipSettings: null == segmentSkipSettings
? _value._segmentSkipSettings
: segmentSkipSettings // ignore: cast_nullable_to_non_nullable
as Map<MediaSegmentType, SegmentSkip>,
));
}
}
@ -269,8 +283,11 @@ class _$VideoPlayerSettingsModelImpl extends _VideoPlayerSettingsModel
this.nextVideoType = AutoNextType.smart,
this.maxHomeBitrate = Bitrate.original,
this.maxInternetBitrate = Bitrate.original,
this.audioDevice})
this.audioDevice,
final Map<MediaSegmentType, SegmentSkip> segmentSkipSettings =
defaultSegmentSkipValues})
: _allowedOrientations = allowedOrientations,
_segmentSkipSettings = segmentSkipSettings,
super._();
factory _$VideoPlayerSettingsModelImpl.fromJson(Map<String, dynamic> json) =>
@ -317,10 +334,19 @@ class _$VideoPlayerSettingsModelImpl extends _VideoPlayerSettingsModel
final Bitrate maxInternetBitrate;
@override
final String? audioDevice;
final Map<MediaSegmentType, SegmentSkip> _segmentSkipSettings;
@override
@JsonKey()
Map<MediaSegmentType, SegmentSkip> get segmentSkipSettings {
if (_segmentSkipSettings is EqualUnmodifiableMapView)
return _segmentSkipSettings;
// ignore: implicit_dynamic_type
return EqualUnmodifiableMapView(_segmentSkipSettings);
}
@override
String toString({DiagnosticLevel minLevel = DiagnosticLevel.info}) {
return 'VideoPlayerSettingsModel(screenBrightness: $screenBrightness, videoFit: $videoFit, fillScreen: $fillScreen, hardwareAccel: $hardwareAccel, useLibass: $useLibass, playerOptions: $playerOptions, internalVolume: $internalVolume, allowedOrientations: $allowedOrientations, nextVideoType: $nextVideoType, maxHomeBitrate: $maxHomeBitrate, maxInternetBitrate: $maxInternetBitrate, audioDevice: $audioDevice)';
return 'VideoPlayerSettingsModel(screenBrightness: $screenBrightness, videoFit: $videoFit, fillScreen: $fillScreen, hardwareAccel: $hardwareAccel, useLibass: $useLibass, playerOptions: $playerOptions, internalVolume: $internalVolume, allowedOrientations: $allowedOrientations, nextVideoType: $nextVideoType, maxHomeBitrate: $maxHomeBitrate, maxInternetBitrate: $maxInternetBitrate, audioDevice: $audioDevice, segmentSkipSettings: $segmentSkipSettings)';
}
@override
@ -339,7 +365,8 @@ class _$VideoPlayerSettingsModelImpl extends _VideoPlayerSettingsModel
..add(DiagnosticsProperty('nextVideoType', nextVideoType))
..add(DiagnosticsProperty('maxHomeBitrate', maxHomeBitrate))
..add(DiagnosticsProperty('maxInternetBitrate', maxInternetBitrate))
..add(DiagnosticsProperty('audioDevice', audioDevice));
..add(DiagnosticsProperty('audioDevice', audioDevice))
..add(DiagnosticsProperty('segmentSkipSettings', segmentSkipSettings));
}
/// Create a copy of VideoPlayerSettingsModel
@ -361,18 +388,20 @@ class _$VideoPlayerSettingsModelImpl extends _VideoPlayerSettingsModel
abstract class _VideoPlayerSettingsModel extends VideoPlayerSettingsModel {
factory _VideoPlayerSettingsModel(
{final double? screenBrightness,
final BoxFit videoFit,
final bool fillScreen,
final bool hardwareAccel,
final bool useLibass,
final PlayerOptions? playerOptions,
final double internalVolume,
final Set<DeviceOrientation>? allowedOrientations,
final AutoNextType nextVideoType,
final Bitrate maxHomeBitrate,
final Bitrate maxInternetBitrate,
final String? audioDevice}) = _$VideoPlayerSettingsModelImpl;
{final double? screenBrightness,
final BoxFit videoFit,
final bool fillScreen,
final bool hardwareAccel,
final bool useLibass,
final PlayerOptions? playerOptions,
final double internalVolume,
final Set<DeviceOrientation>? allowedOrientations,
final AutoNextType nextVideoType,
final Bitrate maxHomeBitrate,
final Bitrate maxInternetBitrate,
final String? audioDevice,
final Map<MediaSegmentType, SegmentSkip> segmentSkipSettings}) =
_$VideoPlayerSettingsModelImpl;
_VideoPlayerSettingsModel._() : super._();
factory _VideoPlayerSettingsModel.fromJson(Map<String, dynamic> json) =
@ -402,6 +431,8 @@ abstract class _VideoPlayerSettingsModel extends VideoPlayerSettingsModel {
Bitrate get maxInternetBitrate;
@override
String? get audioDevice;
@override
Map<MediaSegmentType, SegmentSkip> get segmentSkipSettings;
/// Create a copy of VideoPlayerSettingsModel
/// with the given fields replaced by the non-null parameter values.

View file

@ -31,6 +31,12 @@ _$VideoPlayerSettingsModelImpl _$$VideoPlayerSettingsModelImplFromJson(
$enumDecodeNullable(_$BitrateEnumMap, json['maxInternetBitrate']) ??
Bitrate.original,
audioDevice: json['audioDevice'] as String?,
segmentSkipSettings:
(json['segmentSkipSettings'] as Map<String, dynamic>?)?.map(
(k, e) => MapEntry($enumDecode(_$MediaSegmentTypeEnumMap, k),
$enumDecode(_$SegmentSkipEnumMap, e)),
) ??
defaultSegmentSkipValues,
);
Map<String, dynamic> _$$VideoPlayerSettingsModelImplToJson(
@ -50,6 +56,8 @@ Map<String, dynamic> _$$VideoPlayerSettingsModelImplToJson(
'maxHomeBitrate': _$BitrateEnumMap[instance.maxHomeBitrate]!,
'maxInternetBitrate': _$BitrateEnumMap[instance.maxInternetBitrate]!,
'audioDevice': instance.audioDevice,
'segmentSkipSettings': instance.segmentSkipSettings.map((k, e) =>
MapEntry(_$MediaSegmentTypeEnumMap[k]!, _$SegmentSkipEnumMap[e]!)),
};
const _$BoxFitEnumMap = {
@ -95,6 +103,21 @@ const _$BitrateEnumMap = {
Bitrate.b4Mbps: 'b4Mbps',
Bitrate.b3Mbps: 'b3Mbps',
Bitrate.b1_5Mbps: 'b1_5Mbps',
Bitrate.b420Kbps: 'b420Kbps',
Bitrate.b720Kbps: 'b720Kbps',
Bitrate.b420Kbps: 'b420Kbps',
};
const _$SegmentSkipEnumMap = {
SegmentSkip.none: 'none',
SegmentSkip.askToSkip: 'askToSkip',
SegmentSkip.skip: 'skip',
};
const _$MediaSegmentTypeEnumMap = {
MediaSegmentType.unknown: 'unknown',
MediaSegmentType.commercial: 'commercial',
MediaSegmentType.preview: 'preview',
MediaSegmentType.recap: 'recap',
MediaSegmentType.outro: 'outro',
MediaSegmentType.intro: 'intro',
};

View file

@ -6,7 +6,7 @@ part of 'movies_details_provider.dart';
// RiverpodGenerator
// **************************************************************************
String _$movieDetailsHash() => r'da07dcdb6e1955119df64f8a6a5634216435982c';
String _$movieDetailsHash() => r'872ea61464ef8493c7e6c559c526377f1c8f6a6d';
/// Copied from Dart SDK
class _SystemHash {

View file

@ -4,8 +4,10 @@ import 'package:flutter/foundation.dart';
import 'package:flutter/material.dart';
import 'package:auto_route/auto_route.dart';
import 'package:collection/collection.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart';
import 'package:fladder/models/items/media_segments_model.dart';
import 'package:fladder/models/settings/home_settings_model.dart';
import 'package:fladder/models/settings/video_player_settings.dart';
import 'package:fladder/providers/connectivity_provider.dart';
@ -130,6 +132,40 @@ class _PlayerSettingsPageState extends ConsumerState<PlayerSettingsPage> {
),
),
const Divider(),
SettingsLabelDivider(label: context.localized.mediaSegmentActions),
...videoSettings.segmentSkipSettings.entries.sorted((a, b) => b.key.index.compareTo(a.key.index)).map(
(entry) => Padding(
padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 8),
child: Row(
children: [
Expanded(
child: Text(
entry.key.label(context),
style: Theme.of(context).textTheme.titleLarge,
),
),
EnumBox(
current: entry.value.label(context),
itemBuilder: (context) => SegmentSkip.values
.map(
(value) => PopupMenuItem(
value: value,
child: Text(value.label(context)),
onTap: () {
final newEntries = videoSettings.segmentSkipSettings.map(
(key, currentValue) => MapEntry(key, key == entry.key ? value : currentValue));
ref.read(videoPlayerSettingsProvider.notifier).state =
ref.read(videoPlayerSettingsProvider).copyWith(segmentSkipSettings: newEntries);
},
),
)
.toList(),
)
],
),
),
),
const Divider(),
SettingsLabelDivider(label: context.localized.advanced),
if (PlayerOptions.available.length != 1)
SettingsListTile(

View file

@ -65,16 +65,22 @@ class OpenQueueButton extends ConsumerWidget {
class SkipSegmentButton extends ConsumerWidget {
final MediaSegment? segment;
final SegmentSkip? skipType;
final bool isOverlayVisible;
final Function() pressedSkip;
const SkipSegmentButton(
{required this.segment, required this.isOverlayVisible, required this.pressedSkip, super.key});
const SkipSegmentButton({
required this.segment,
this.skipType,
required this.isOverlayVisible,
required this.pressedSkip,
super.key,
});
@override
Widget build(BuildContext context, WidgetRef ref) {
return AnimatedFadeSize(
child: segment != null
child: segment != null && skipType != SegmentSkip.none
? AnimatedOpacity(
opacity: isOverlayVisible ? 1 : 0.15,
duration: const Duration(milliseconds: 500),

View file

@ -166,6 +166,13 @@ class _DesktopControlsState extends ConsumerState<DesktopControls> {
final position = ref.watch(mediaPlaybackProvider.select((value) => value.position));
MediaSegment? segment = mediaSegments?.atPosition(position);
bool forceShow = segment?.forceShow(position) ?? false;
final segmentSkipType = ref
.watch(videoPlayerSettingsProvider.select((value) => value.segmentSkipSettings[segment?.type]));
final autoSkip =
forceShow == true && segmentSkipType == SegmentSkip.skip && player.lastState?.buffering == false;
if (autoSkip) {
skipToSegmentEnd(segment);
}
return Stack(
children: [
Align(
@ -174,6 +181,7 @@ class _DesktopControlsState extends ConsumerState<DesktopControls> {
padding: const EdgeInsets.all(32),
child: SkipSegmentButton(
segment: segment,
skipType: segmentSkipType,
isOverlayVisible: forceShow ? true : showOverlay,
pressedSkip: () => skipToSegmentEnd(segment),
),