feat: Implement next-up screen for native player (#533)

Co-authored-by: PartyDonut <PartyDonut@users.noreply.github.com>
This commit is contained in:
PartyDonut 2025-10-15 18:05:51 +02:00 committed by GitHub
parent 311b647286
commit 29b1c2e633
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
25 changed files with 782 additions and 203 deletions

View file

@ -30,6 +30,7 @@ import 'package:fladder/screens/details_screens/episode_detail_screen.dart';
import 'package:fladder/screens/details_screens/season_detail_screen.dart';
import 'package:fladder/screens/library_search/library_search_screen.dart';
import 'package:fladder/screens/photo_viewer/photo_viewer_screen.dart';
import 'package:fladder/src/video_player_helper.g.dart' show SimpleItemModel;
import 'package:fladder/util/localization_helper.dart';
import 'package:fladder/util/string_extensions.dart';
@ -233,6 +234,17 @@ class ItemBaseModel with ItemBaseModelMappable {
);
}
SimpleItemModel toSimpleItem(BuildContext? context) {
return SimpleItemModel(
id: id,
title: title,
subTitle: context != null ? label(context) : null,
overview: overview.summary,
logoUrl: images?.logo?.path,
primaryPoster: images?.primary?.path ?? getPosters?.primary?.path ?? "",
);
}
FladderItemType get type => switch (this) {
MovieModel _ => FladderItemType.movie,
SeriesModel _ => FladderItemType.series,

View file

@ -5,6 +5,7 @@ import 'package:flutter/foundation.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart';
import 'package:fladder/models/items/media_segments_model.dart';
import 'package:fladder/models/settings/video_player_settings.dart';
import 'package:fladder/providers/settings/client_settings_provider.dart';
import 'package:fladder/providers/settings/video_player_settings_provider.dart';
import 'package:fladder/providers/user_provider.dart';
@ -42,6 +43,11 @@ final pigeonPlayerSettingsSyncProvider = Provider<void>((ref) {
),
),
themeColor: color,
autoNextType: switch (value.nextVideoType) {
AutoNextType.off => pigeon.AutoNextType.off,
AutoNextType.static => pigeon.AutoNextType.static,
AutoNextType.smart => pigeon.AutoNextType.smart,
},
skipBackward: (userData?.userSettings?.skipBackDuration ?? const Duration(seconds: 15)).inMilliseconds,
skipForward: (userData?.userSettings?.skipForwardDuration ?? const Duration(seconds: 30)).inMilliseconds,
),

View file

@ -377,39 +377,42 @@ class _PlayerSettingsPageState extends ConsumerState<PlayerSettingsPage> {
"${context.localized.noVideoPlayerOptions}\n${context.localized.mdkExperimental}"),
},
),
if (videoSettings.wantedPlayer != PlayerOptions.nativePlayer) ...[
Column(
children: [
SettingsListTile(
label: Text(context.localized.settingsAutoNextTitle),
subLabel: Text(context.localized.settingsAutoNextDesc),
trailing: EnumBox(
current: ref.watch(
videoPlayerSettingsProvider.select(
(value) => value.nextVideoType.label(context),
),
Column(
children: [
SettingsListTile(
label: Text(context.localized.settingsAutoNextTitle),
subLabel: Text(context.localized.settingsAutoNextDesc),
trailing: EnumBox(
current: ref.watch(
videoPlayerSettingsProvider.select(
(value) => value.nextVideoType.label(context),
),
itemBuilder: (context) => AutoNextType.values
.map(
(entry) => ItemActionButton(
label: Text(entry.label(context)),
action: () => ref.read(videoPlayerSettingsProvider.notifier).state =
videoSettings.copyWith(nextVideoType: entry),
),
)
.toList(),
),
itemBuilder: (context) => AutoNextType.values
.map(
(entry) => ItemActionButton(
label: Text(entry.label(context)),
action: () => ref.read(videoPlayerSettingsProvider.notifier).state =
videoSettings.copyWith(nextVideoType: entry),
),
)
.toList(),
),
AnimatedFadeSize(
child: switch (ref.watch(videoPlayerSettingsProvider.select((value) => value.nextVideoType))) {
AutoNextType.smart => SettingsMessageBox(AutoNextType.smart.desc(context)),
AutoNextType.static => SettingsMessageBox(AutoNextType.static.desc(context)),
_ => const SizedBox.shrink(),
},
),
],
),
if (!AdaptiveLayout.of(context).isDesktop && !kIsWeb && !ref.read(argumentsStateProvider).htpcMode)
),
AnimatedFadeSize(
child: switch (ref.watch(videoPlayerSettingsProvider.select((value) => value.nextVideoType))) {
AutoNextType.smart => SettingsMessageBox(AutoNextType.smart.desc(context)),
AutoNextType.static => SettingsMessageBox(AutoNextType.static.desc(context)),
_ => const SizedBox.shrink(),
},
),
],
),
if (videoSettings.wantedPlayer != PlayerOptions.nativePlayer) ...[
if (!AdaptiveLayout.of(context).isDesktop &&
!kIsWeb &&
!ref.read(argumentsStateProvider).htpcMode &&
videoSettings.wantedPlayer != PlayerOptions.nativePlayer)
SettingsListTile(
label: Text(context.localized.playerSettingsOrientationTitle),
subLabel: Text(context.localized.playerSettingsOrientationDesc),

View file

@ -29,6 +29,12 @@ bool _deepEquals(Object? a, Object? b) {
}
enum AutoNextType {
off,
static,
smart,
}
enum SegmentType {
commercial,
preview,
@ -50,6 +56,7 @@ class PlayerSettings {
this.themeColor,
required this.skipForward,
required this.skipBackward,
required this.autoNextType,
});
bool enableTunneling;
@ -62,6 +69,8 @@ class PlayerSettings {
int skipBackward;
AutoNextType autoNextType;
List<Object?> _toList() {
return <Object?>[
enableTunneling,
@ -69,6 +78,7 @@ class PlayerSettings {
themeColor,
skipForward,
skipBackward,
autoNextType,
];
}
@ -83,6 +93,7 @@ class PlayerSettings {
themeColor: result[2] as int?,
skipForward: result[3]! as int,
skipBackward: result[4]! as int,
autoNextType: result[5]! as AutoNextType,
);
}
@ -112,14 +123,17 @@ class _PigeonCodec extends StandardMessageCodec {
if (value is int) {
buffer.putUint8(4);
buffer.putInt64(value);
} else if (value is SegmentType) {
} else if (value is AutoNextType) {
buffer.putUint8(129);
writeValue(buffer, value.index);
} else if (value is SegmentSkip) {
} else if (value is SegmentType) {
buffer.putUint8(130);
writeValue(buffer, value.index);
} else if (value is PlayerSettings) {
} else if (value is SegmentSkip) {
buffer.putUint8(131);
writeValue(buffer, value.index);
} else if (value is PlayerSettings) {
buffer.putUint8(132);
writeValue(buffer, value.encode());
} else {
super.writeValue(buffer, value);
@ -131,11 +145,14 @@ class _PigeonCodec extends StandardMessageCodec {
switch (type) {
case 129:
final int? value = readValue(buffer) as int?;
return value == null ? null : SegmentType.values[value];
return value == null ? null : AutoNextType.values[value];
case 130:
final int? value = readValue(buffer) as int?;
return value == null ? null : SegmentSkip.values[value];
return value == null ? null : SegmentType.values[value];
case 131:
final int? value = readValue(buffer) as int?;
return value == null ? null : SegmentSkip.values[value];
case 132:
return PlayerSettings.decode(readValue(buffer)!);
default:
return super.readValueOfType(type, buffer);

View file

@ -47,12 +47,75 @@ enum MediaSegmentType {
outro,
}
class PlayableData {
PlayableData({
class SimpleItemModel {
SimpleItemModel({
required this.id,
required this.title,
this.subTitle,
this.overview,
this.logoUrl,
required this.primaryPoster,
});
String id;
String title;
String? subTitle;
String? overview;
String? logoUrl;
String primaryPoster;
List<Object?> _toList() {
return <Object?>[
id,
title,
subTitle,
overview,
logoUrl,
primaryPoster,
];
}
Object encode() {
return _toList(); }
static SimpleItemModel decode(Object result) {
result as List<Object?>;
return SimpleItemModel(
id: result[0]! as String,
title: result[1]! as String,
subTitle: result[2] as String?,
overview: result[3] as String?,
logoUrl: result[4] as String?,
primaryPoster: result[5]! as String,
);
}
@override
// ignore: avoid_equals_and_hash_code_on_mutable_classes
bool operator ==(Object other) {
if (other is! SimpleItemModel || other.runtimeType != runtimeType) {
return false;
}
if (identical(this, other)) {
return true;
}
return _deepEquals(encode(), other.encode());
}
@override
// ignore: avoid_equals_and_hash_code_on_mutable_classes
int get hashCode => Object.hashAll(_toList())
;
}
class PlayableData {
PlayableData({
required this.currentItem,
required this.description,
required this.startPosition,
required this.defaultAudioTrack,
@ -67,13 +130,7 @@ class PlayableData {
required this.url,
});
String id;
String title;
String? subTitle;
String? logoUrl;
SimpleItemModel currentItem;
String description;
@ -93,18 +150,15 @@ class PlayableData {
List<MediaSegment> segments;
String? previousVideo;
SimpleItemModel? previousVideo;
String? nextVideo;
SimpleItemModel? nextVideo;
String url;
List<Object?> _toList() {
return <Object?>[
id,
title,
subTitle,
logoUrl,
currentItem,
description,
startPosition,
defaultAudioTrack,
@ -126,22 +180,19 @@ class PlayableData {
static PlayableData decode(Object result) {
result as List<Object?>;
return PlayableData(
id: result[0]! as String,
title: result[1]! as String,
subTitle: result[2] as String?,
logoUrl: result[3] as String?,
description: result[4]! as String,
startPosition: result[5]! as int,
defaultAudioTrack: result[6]! as int,
audioTracks: (result[7] as List<Object?>?)!.cast<AudioTrack>(),
defaultSubtrack: result[8]! as int,
subtitleTracks: (result[9] as List<Object?>?)!.cast<SubtitleTrack>(),
trickPlayModel: result[10] as TrickPlayModel?,
chapters: (result[11] as List<Object?>?)!.cast<Chapter>(),
segments: (result[12] as List<Object?>?)!.cast<MediaSegment>(),
previousVideo: result[13] as String?,
nextVideo: result[14] as String?,
url: result[15]! as String,
currentItem: result[0]! as SimpleItemModel,
description: result[1]! as String,
startPosition: result[2]! as int,
defaultAudioTrack: result[3]! as int,
audioTracks: (result[4] as List<Object?>?)!.cast<AudioTrack>(),
defaultSubtrack: result[5]! as int,
subtitleTracks: (result[6] as List<Object?>?)!.cast<SubtitleTrack>(),
trickPlayModel: result[7] as TrickPlayModel?,
chapters: (result[8] as List<Object?>?)!.cast<Chapter>(),
segments: (result[9] as List<Object?>?)!.cast<MediaSegment>(),
previousVideo: result[10] as SimpleItemModel?,
nextVideo: result[11] as SimpleItemModel?,
url: result[12]! as String,
);
}
@ -596,30 +647,33 @@ class _PigeonCodec extends StandardMessageCodec {
} else if (value is MediaSegmentType) {
buffer.putUint8(129);
writeValue(buffer, value.index);
} else if (value is PlayableData) {
} else if (value is SimpleItemModel) {
buffer.putUint8(130);
writeValue(buffer, value.encode());
} else if (value is MediaSegment) {
} else if (value is PlayableData) {
buffer.putUint8(131);
writeValue(buffer, value.encode());
} else if (value is AudioTrack) {
} else if (value is MediaSegment) {
buffer.putUint8(132);
writeValue(buffer, value.encode());
} else if (value is SubtitleTrack) {
} else if (value is AudioTrack) {
buffer.putUint8(133);
writeValue(buffer, value.encode());
} else if (value is Chapter) {
} else if (value is SubtitleTrack) {
buffer.putUint8(134);
writeValue(buffer, value.encode());
} else if (value is TrickPlayModel) {
} else if (value is Chapter) {
buffer.putUint8(135);
writeValue(buffer, value.encode());
} else if (value is StartResult) {
} else if (value is TrickPlayModel) {
buffer.putUint8(136);
writeValue(buffer, value.encode());
} else if (value is PlaybackState) {
} else if (value is StartResult) {
buffer.putUint8(137);
writeValue(buffer, value.encode());
} else if (value is PlaybackState) {
buffer.putUint8(138);
writeValue(buffer, value.encode());
} else {
super.writeValue(buffer, value);
}
@ -632,20 +686,22 @@ class _PigeonCodec extends StandardMessageCodec {
final int? value = readValue(buffer) as int?;
return value == null ? null : MediaSegmentType.values[value];
case 130:
return PlayableData.decode(readValue(buffer)!);
return SimpleItemModel.decode(readValue(buffer)!);
case 131:
return MediaSegment.decode(readValue(buffer)!);
return PlayableData.decode(readValue(buffer)!);
case 132:
return AudioTrack.decode(readValue(buffer)!);
return MediaSegment.decode(readValue(buffer)!);
case 133:
return SubtitleTrack.decode(readValue(buffer)!);
return AudioTrack.decode(readValue(buffer)!);
case 134:
return Chapter.decode(readValue(buffer)!);
return SubtitleTrack.decode(readValue(buffer)!);
case 135:
return TrickPlayModel.decode(readValue(buffer)!);
return Chapter.decode(readValue(buffer)!);
case 136:
return StartResult.decode(readValue(buffer)!);
return TrickPlayModel.decode(readValue(buffer)!);
case 137:
return StartResult.decode(readValue(buffer)!);
case 138:
return PlaybackState.decode(readValue(buffer)!);
default:
return super.readValueOfType(type, buffer);

View file

@ -100,15 +100,12 @@ class NativePlayer extends BasePlayer implements VideoPlayerListenerCallback {
Duration startPosition,
) async {
final playableData = PlayableData(
id: model.item.id,
title: model.item.title,
subTitle: context != null ? model.item.label(context) : "",
logoUrl: model.item.getPosters?.logo?.path,
currentItem: model.item.toSimpleItem(context),
startPosition: startPosition.inMilliseconds,
description: model.item.overview.summary,
defaultAudioTrack: model.mediaStreams?.defaultAudioStreamIndex ?? 1,
nextVideo: model.nextVideo?.name,
previousVideo: model.previousVideo?.name,
nextVideo: model.nextVideo?.toSimpleItem(context),
previousVideo: model.previousVideo?.toSimpleItem(context),
audioTracks: model.audioStreams
?.map(
(audio) => AudioTrack(