mirror of
https://github.com/gabehf/Fladder.git
synced 2026-03-07 13:38:13 -08:00
feat: Videoplayer remember subtitle and audio selection(#339)
This commit is contained in:
parent
93a38a0b6b
commit
b1491b0ada
10 changed files with 247 additions and 39 deletions
|
|
@ -55,6 +55,10 @@ flutter pub run build_runner build
|
||||||
```bash
|
```bash
|
||||||
flutter pub run build_runner watch
|
flutter pub run build_runner watch
|
||||||
```
|
```
|
||||||
|
Update localization definitions:
|
||||||
|
```bash
|
||||||
|
flutter gen-l10n
|
||||||
|
```
|
||||||
|
|
||||||
## 🌐 Using a demo Server
|
## 🌐 Using a demo Server
|
||||||
You can use a fake server from Jellyfin.
|
You can use a fake server from Jellyfin.
|
||||||
|
|
|
||||||
|
|
@ -1201,5 +1201,15 @@
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"maxConcurrentDownloadsTitle": "Max concurrent downloads",
|
"maxConcurrentDownloadsTitle": "Max concurrent downloads",
|
||||||
"maxConcurrentDownloadsDesc": "Sets the maximum number of downloads that can run at the same time. Set to 0 to disable the limit."
|
"maxConcurrentDownloadsDesc": "Sets the maximum number of downloads that can run at the same time. Set to 0 to disable the limit.",
|
||||||
|
"playbackTrackSelection": "Playback track selection",
|
||||||
|
"@playbackTrackSelection": {},
|
||||||
|
"rememberSubtitleSelections": "Set subtitle track based on previous item",
|
||||||
|
"@rememberSubtitleSelections": {},
|
||||||
|
"rememberAudioSelections": "Set audio track based on previous item",
|
||||||
|
"@rememberAudioSelections": {},
|
||||||
|
"rememberSubtitleSelectionsDesc": "Try to set the subtitle track to the closest match to the last video.",
|
||||||
|
"@rememberSubtitleSelectionsDesc": {},
|
||||||
|
"rememberAudioSelectionsDesc": "Try to set the audio track to the closest match to the last video.",
|
||||||
|
"@rememberAudioSelectionsDesc": {}
|
||||||
}
|
}
|
||||||
|
|
@ -34,6 +34,7 @@ class AccountModel with _$AccountModel {
|
||||||
@Default([]) List<LibraryFiltersModel> savedFilters,
|
@Default([]) List<LibraryFiltersModel> savedFilters,
|
||||||
@JsonKey(includeFromJson: false, includeToJson: false) UserPolicy? policy,
|
@JsonKey(includeFromJson: false, includeToJson: false) UserPolicy? policy,
|
||||||
@JsonKey(includeFromJson: false, includeToJson: false) ServerConfiguration? serverConfiguration,
|
@JsonKey(includeFromJson: false, includeToJson: false) ServerConfiguration? serverConfiguration,
|
||||||
|
@JsonKey(includeFromJson: false, includeToJson: false) UserConfiguration? userConfiguration,
|
||||||
}) = _AccountModel;
|
}) = _AccountModel;
|
||||||
|
|
||||||
factory AccountModel.fromJson(Map<String, dynamic> json) => _$AccountModelFromJson(json);
|
factory AccountModel.fromJson(Map<String, dynamic> json) => _$AccountModelFromJson(json);
|
||||||
|
|
|
||||||
|
|
@ -37,6 +37,9 @@ mixin _$AccountModel {
|
||||||
@JsonKey(includeFromJson: false, includeToJson: false)
|
@JsonKey(includeFromJson: false, includeToJson: false)
|
||||||
ServerConfiguration? get serverConfiguration =>
|
ServerConfiguration? get serverConfiguration =>
|
||||||
throw _privateConstructorUsedError;
|
throw _privateConstructorUsedError;
|
||||||
|
@JsonKey(includeFromJson: false, includeToJson: false)
|
||||||
|
UserConfiguration? get userConfiguration =>
|
||||||
|
throw _privateConstructorUsedError;
|
||||||
|
|
||||||
/// Serializes this AccountModel to a JSON map.
|
/// Serializes this AccountModel to a JSON map.
|
||||||
Map<String, dynamic> toJson() => throw _privateConstructorUsedError;
|
Map<String, dynamic> toJson() => throw _privateConstructorUsedError;
|
||||||
|
|
@ -68,7 +71,9 @@ abstract class $AccountModelCopyWith<$Res> {
|
||||||
List<LibraryFiltersModel> savedFilters,
|
List<LibraryFiltersModel> savedFilters,
|
||||||
@JsonKey(includeFromJson: false, includeToJson: false) UserPolicy? policy,
|
@JsonKey(includeFromJson: false, includeToJson: false) UserPolicy? policy,
|
||||||
@JsonKey(includeFromJson: false, includeToJson: false)
|
@JsonKey(includeFromJson: false, includeToJson: false)
|
||||||
ServerConfiguration? serverConfiguration});
|
ServerConfiguration? serverConfiguration,
|
||||||
|
@JsonKey(includeFromJson: false, includeToJson: false)
|
||||||
|
UserConfiguration? userConfiguration});
|
||||||
}
|
}
|
||||||
|
|
||||||
/// @nodoc
|
/// @nodoc
|
||||||
|
|
@ -99,6 +104,7 @@ class _$AccountModelCopyWithImpl<$Res, $Val extends AccountModel>
|
||||||
Object? savedFilters = null,
|
Object? savedFilters = null,
|
||||||
Object? policy = freezed,
|
Object? policy = freezed,
|
||||||
Object? serverConfiguration = freezed,
|
Object? serverConfiguration = freezed,
|
||||||
|
Object? userConfiguration = freezed,
|
||||||
}) {
|
}) {
|
||||||
return _then(_value.copyWith(
|
return _then(_value.copyWith(
|
||||||
name: null == name
|
name: null == name
|
||||||
|
|
@ -153,6 +159,10 @@ class _$AccountModelCopyWithImpl<$Res, $Val extends AccountModel>
|
||||||
? _value.serverConfiguration
|
? _value.serverConfiguration
|
||||||
: serverConfiguration // ignore: cast_nullable_to_non_nullable
|
: serverConfiguration // ignore: cast_nullable_to_non_nullable
|
||||||
as ServerConfiguration?,
|
as ServerConfiguration?,
|
||||||
|
userConfiguration: freezed == userConfiguration
|
||||||
|
? _value.userConfiguration
|
||||||
|
: userConfiguration // ignore: cast_nullable_to_non_nullable
|
||||||
|
as UserConfiguration?,
|
||||||
) as $Val);
|
) as $Val);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -179,7 +189,9 @@ abstract class _$$AccountModelImplCopyWith<$Res>
|
||||||
List<LibraryFiltersModel> savedFilters,
|
List<LibraryFiltersModel> savedFilters,
|
||||||
@JsonKey(includeFromJson: false, includeToJson: false) UserPolicy? policy,
|
@JsonKey(includeFromJson: false, includeToJson: false) UserPolicy? policy,
|
||||||
@JsonKey(includeFromJson: false, includeToJson: false)
|
@JsonKey(includeFromJson: false, includeToJson: false)
|
||||||
ServerConfiguration? serverConfiguration});
|
ServerConfiguration? serverConfiguration,
|
||||||
|
@JsonKey(includeFromJson: false, includeToJson: false)
|
||||||
|
UserConfiguration? userConfiguration});
|
||||||
}
|
}
|
||||||
|
|
||||||
/// @nodoc
|
/// @nodoc
|
||||||
|
|
@ -208,6 +220,7 @@ class __$$AccountModelImplCopyWithImpl<$Res>
|
||||||
Object? savedFilters = null,
|
Object? savedFilters = null,
|
||||||
Object? policy = freezed,
|
Object? policy = freezed,
|
||||||
Object? serverConfiguration = freezed,
|
Object? serverConfiguration = freezed,
|
||||||
|
Object? userConfiguration = freezed,
|
||||||
}) {
|
}) {
|
||||||
return _then(_$AccountModelImpl(
|
return _then(_$AccountModelImpl(
|
||||||
name: null == name
|
name: null == name
|
||||||
|
|
@ -262,6 +275,10 @@ class __$$AccountModelImplCopyWithImpl<$Res>
|
||||||
? _value.serverConfiguration
|
? _value.serverConfiguration
|
||||||
: serverConfiguration // ignore: cast_nullable_to_non_nullable
|
: serverConfiguration // ignore: cast_nullable_to_non_nullable
|
||||||
as ServerConfiguration?,
|
as ServerConfiguration?,
|
||||||
|
userConfiguration: freezed == userConfiguration
|
||||||
|
? _value.userConfiguration
|
||||||
|
: userConfiguration // ignore: cast_nullable_to_non_nullable
|
||||||
|
as UserConfiguration?,
|
||||||
));
|
));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -283,7 +300,9 @@ class _$AccountModelImpl extends _AccountModel with DiagnosticableTreeMixin {
|
||||||
final List<LibraryFiltersModel> savedFilters = const [],
|
final List<LibraryFiltersModel> savedFilters = const [],
|
||||||
@JsonKey(includeFromJson: false, includeToJson: false) this.policy,
|
@JsonKey(includeFromJson: false, includeToJson: false) this.policy,
|
||||||
@JsonKey(includeFromJson: false, includeToJson: false)
|
@JsonKey(includeFromJson: false, includeToJson: false)
|
||||||
this.serverConfiguration})
|
this.serverConfiguration,
|
||||||
|
@JsonKey(includeFromJson: false, includeToJson: false)
|
||||||
|
this.userConfiguration})
|
||||||
: _latestItemsExcludes = latestItemsExcludes,
|
: _latestItemsExcludes = latestItemsExcludes,
|
||||||
_searchQueryHistory = searchQueryHistory,
|
_searchQueryHistory = searchQueryHistory,
|
||||||
_savedFilters = savedFilters,
|
_savedFilters = savedFilters,
|
||||||
|
|
@ -346,10 +365,13 @@ class _$AccountModelImpl extends _AccountModel with DiagnosticableTreeMixin {
|
||||||
@override
|
@override
|
||||||
@JsonKey(includeFromJson: false, includeToJson: false)
|
@JsonKey(includeFromJson: false, includeToJson: false)
|
||||||
final ServerConfiguration? serverConfiguration;
|
final ServerConfiguration? serverConfiguration;
|
||||||
|
@override
|
||||||
|
@JsonKey(includeFromJson: false, includeToJson: false)
|
||||||
|
final UserConfiguration? userConfiguration;
|
||||||
|
|
||||||
@override
|
@override
|
||||||
String toString({DiagnosticLevel minLevel = DiagnosticLevel.info}) {
|
String toString({DiagnosticLevel minLevel = DiagnosticLevel.info}) {
|
||||||
return 'AccountModel(name: $name, id: $id, avatar: $avatar, lastUsed: $lastUsed, authMethod: $authMethod, localPin: $localPin, credentials: $credentials, latestItemsExcludes: $latestItemsExcludes, searchQueryHistory: $searchQueryHistory, quickConnectState: $quickConnectState, savedFilters: $savedFilters, policy: $policy, serverConfiguration: $serverConfiguration)';
|
return 'AccountModel(name: $name, id: $id, avatar: $avatar, lastUsed: $lastUsed, authMethod: $authMethod, localPin: $localPin, credentials: $credentials, latestItemsExcludes: $latestItemsExcludes, searchQueryHistory: $searchQueryHistory, quickConnectState: $quickConnectState, savedFilters: $savedFilters, policy: $policy, serverConfiguration: $serverConfiguration, userConfiguration: $userConfiguration)';
|
||||||
}
|
}
|
||||||
|
|
||||||
@override
|
@override
|
||||||
|
|
@ -369,7 +391,8 @@ class _$AccountModelImpl extends _AccountModel with DiagnosticableTreeMixin {
|
||||||
..add(DiagnosticsProperty('quickConnectState', quickConnectState))
|
..add(DiagnosticsProperty('quickConnectState', quickConnectState))
|
||||||
..add(DiagnosticsProperty('savedFilters', savedFilters))
|
..add(DiagnosticsProperty('savedFilters', savedFilters))
|
||||||
..add(DiagnosticsProperty('policy', policy))
|
..add(DiagnosticsProperty('policy', policy))
|
||||||
..add(DiagnosticsProperty('serverConfiguration', serverConfiguration));
|
..add(DiagnosticsProperty('serverConfiguration', serverConfiguration))
|
||||||
|
..add(DiagnosticsProperty('userConfiguration', userConfiguration));
|
||||||
}
|
}
|
||||||
|
|
||||||
@override
|
@override
|
||||||
|
|
@ -398,7 +421,9 @@ class _$AccountModelImpl extends _AccountModel with DiagnosticableTreeMixin {
|
||||||
.equals(other._savedFilters, _savedFilters) &&
|
.equals(other._savedFilters, _savedFilters) &&
|
||||||
(identical(other.policy, policy) || other.policy == policy) &&
|
(identical(other.policy, policy) || other.policy == policy) &&
|
||||||
(identical(other.serverConfiguration, serverConfiguration) ||
|
(identical(other.serverConfiguration, serverConfiguration) ||
|
||||||
other.serverConfiguration == serverConfiguration));
|
other.serverConfiguration == serverConfiguration) &&
|
||||||
|
(identical(other.userConfiguration, userConfiguration) ||
|
||||||
|
other.userConfiguration == userConfiguration));
|
||||||
}
|
}
|
||||||
|
|
||||||
@JsonKey(includeFromJson: false, includeToJson: false)
|
@JsonKey(includeFromJson: false, includeToJson: false)
|
||||||
|
|
@ -417,7 +442,8 @@ class _$AccountModelImpl extends _AccountModel with DiagnosticableTreeMixin {
|
||||||
quickConnectState,
|
quickConnectState,
|
||||||
const DeepCollectionEquality().hash(_savedFilters),
|
const DeepCollectionEquality().hash(_savedFilters),
|
||||||
policy,
|
policy,
|
||||||
serverConfiguration);
|
serverConfiguration,
|
||||||
|
userConfiguration);
|
||||||
|
|
||||||
/// Create a copy of AccountModel
|
/// Create a copy of AccountModel
|
||||||
/// with the given fields replaced by the non-null parameter values.
|
/// with the given fields replaced by the non-null parameter values.
|
||||||
|
|
@ -451,7 +477,9 @@ abstract class _AccountModel extends AccountModel {
|
||||||
@JsonKey(includeFromJson: false, includeToJson: false)
|
@JsonKey(includeFromJson: false, includeToJson: false)
|
||||||
final UserPolicy? policy,
|
final UserPolicy? policy,
|
||||||
@JsonKey(includeFromJson: false, includeToJson: false)
|
@JsonKey(includeFromJson: false, includeToJson: false)
|
||||||
final ServerConfiguration? serverConfiguration}) = _$AccountModelImpl;
|
final ServerConfiguration? serverConfiguration,
|
||||||
|
@JsonKey(includeFromJson: false, includeToJson: false)
|
||||||
|
final UserConfiguration? userConfiguration}) = _$AccountModelImpl;
|
||||||
const _AccountModel._() : super._();
|
const _AccountModel._() : super._();
|
||||||
|
|
||||||
factory _AccountModel.fromJson(Map<String, dynamic> json) =
|
factory _AccountModel.fromJson(Map<String, dynamic> json) =
|
||||||
|
|
@ -485,6 +513,9 @@ abstract class _AccountModel extends AccountModel {
|
||||||
@override
|
@override
|
||||||
@JsonKey(includeFromJson: false, includeToJson: false)
|
@JsonKey(includeFromJson: false, includeToJson: false)
|
||||||
ServerConfiguration? get serverConfiguration;
|
ServerConfiguration? get serverConfiguration;
|
||||||
|
@override
|
||||||
|
@JsonKey(includeFromJson: false, includeToJson: false)
|
||||||
|
UserConfiguration? get userConfiguration;
|
||||||
|
|
||||||
/// Create a copy of AccountModel
|
/// Create a copy of AccountModel
|
||||||
/// with the given fields replaced by the non-null parameter values.
|
/// with the given fields replaced by the non-null parameter values.
|
||||||
|
|
|
||||||
|
|
@ -175,6 +175,20 @@ class StreamModel {
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
class AudioAndSubStreamModel extends StreamModel {
|
||||||
|
final String language;
|
||||||
|
final String displayTitle;
|
||||||
|
AudioAndSubStreamModel({
|
||||||
|
required this.displayTitle,
|
||||||
|
required super.name,
|
||||||
|
required super.codec,
|
||||||
|
required super.isDefault,
|
||||||
|
required super.isExternal,
|
||||||
|
required super.index,
|
||||||
|
required this.language,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
class VersionStreamModel {
|
class VersionStreamModel {
|
||||||
final String name;
|
final String name;
|
||||||
final int index;
|
final int index;
|
||||||
|
|
@ -250,19 +264,17 @@ extension SortByExternalExtension<T extends StreamModel> on Iterable<T> {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
class AudioStreamModel extends StreamModel {
|
class AudioStreamModel extends AudioAndSubStreamModel {
|
||||||
final String displayTitle;
|
|
||||||
final String language;
|
|
||||||
final String channelLayout;
|
final String channelLayout;
|
||||||
|
|
||||||
AudioStreamModel({
|
AudioStreamModel({
|
||||||
required this.displayTitle,
|
required super.displayTitle,
|
||||||
required super.name,
|
required super.name,
|
||||||
required super.codec,
|
required super.codec,
|
||||||
required super.isDefault,
|
required super.isDefault,
|
||||||
required super.isExternal,
|
required super.isExternal,
|
||||||
required super.index,
|
required super.index,
|
||||||
required this.language,
|
required super.language,
|
||||||
required this.channelLayout,
|
required this.channelLayout,
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|
@ -292,8 +304,8 @@ class AudioStreamModel extends StreamModel {
|
||||||
|
|
||||||
AudioStreamModel.no({
|
AudioStreamModel.no({
|
||||||
super.name = 'Off',
|
super.name = 'Off',
|
||||||
this.displayTitle = 'Off',
|
super.displayTitle = 'Off',
|
||||||
this.language = '',
|
super.language = '',
|
||||||
super.codec = '',
|
super.codec = '',
|
||||||
this.channelLayout = '',
|
this.channelLayout = '',
|
||||||
super.isDefault = false,
|
super.isDefault = false,
|
||||||
|
|
@ -302,19 +314,17 @@ class AudioStreamModel extends StreamModel {
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
class SubStreamModel extends StreamModel {
|
class SubStreamModel extends AudioAndSubStreamModel {
|
||||||
String id;
|
String id;
|
||||||
String title;
|
String title;
|
||||||
String displayTitle;
|
|
||||||
String language;
|
|
||||||
String? url;
|
String? url;
|
||||||
bool supportsExternalStream;
|
bool supportsExternalStream;
|
||||||
SubStreamModel({
|
SubStreamModel({
|
||||||
required super.name,
|
required super.name,
|
||||||
required this.id,
|
required this.id,
|
||||||
required this.title,
|
required this.title,
|
||||||
required this.displayTitle,
|
required super.displayTitle,
|
||||||
required this.language,
|
required super.language,
|
||||||
this.url,
|
this.url,
|
||||||
required super.codec,
|
required super.codec,
|
||||||
required super.isDefault,
|
required super.isDefault,
|
||||||
|
|
@ -327,8 +337,8 @@ class SubStreamModel extends StreamModel {
|
||||||
super.name = 'Off',
|
super.name = 'Off',
|
||||||
this.id = 'Off',
|
this.id = 'Off',
|
||||||
this.title = 'Off',
|
this.title = 'Off',
|
||||||
this.displayTitle = 'Off',
|
super.displayTitle = 'Off',
|
||||||
this.language = '',
|
super.language = '',
|
||||||
this.url = '',
|
this.url = '',
|
||||||
super.codec = '',
|
super.codec = '',
|
||||||
super.isDefault = false,
|
super.isDefault = false,
|
||||||
|
|
|
||||||
|
|
@ -31,6 +31,7 @@ import 'package:fladder/util/bitrate_helper.dart';
|
||||||
import 'package:fladder/util/duration_extensions.dart';
|
import 'package:fladder/util/duration_extensions.dart';
|
||||||
import 'package:fladder/util/list_extensions.dart';
|
import 'package:fladder/util/list_extensions.dart';
|
||||||
import 'package:fladder/util/map_bool_helper.dart';
|
import 'package:fladder/util/map_bool_helper.dart';
|
||||||
|
import 'package:fladder/util/streams_selection.dart';
|
||||||
import 'package:fladder/wrappers/media_control_wrapper.dart';
|
import 'package:fladder/wrappers/media_control_wrapper.dart';
|
||||||
|
|
||||||
class Media {
|
class Media {
|
||||||
|
|
@ -196,13 +197,25 @@ class PlaybackModelHelper {
|
||||||
);
|
);
|
||||||
|
|
||||||
final streamModel = firstItemToPlay.streamModel;
|
final streamModel = firstItemToPlay.streamModel;
|
||||||
|
final audioStreamIndex = selectAudioStream(
|
||||||
|
ref.read(userProvider.select((value) => value?.userConfiguration?.rememberAudioSelections ?? true)),
|
||||||
|
oldModel?.mediaStreams?.currentAudioStream,
|
||||||
|
streamModel?.audioStreams,
|
||||||
|
streamModel?.defaultAudioStreamIndex
|
||||||
|
);
|
||||||
|
final subStreamIndex = selectSubStream(
|
||||||
|
ref.read(userProvider.select((value) => value?.userConfiguration?.rememberSubtitleSelections ?? true)),
|
||||||
|
oldModel?.mediaStreams?.currentSubStream,
|
||||||
|
streamModel?.subStreams,
|
||||||
|
streamModel?.defaultSubStreamIndex
|
||||||
|
);
|
||||||
|
|
||||||
final Response<PlaybackInfoResponse> response = await api.itemsItemIdPlaybackInfoPost(
|
final Response<PlaybackInfoResponse> response = await api.itemsItemIdPlaybackInfoPost(
|
||||||
itemId: firstItemToPlay.id,
|
itemId: firstItemToPlay.id,
|
||||||
body: PlaybackInfoDto(
|
body: PlaybackInfoDto(
|
||||||
startTimeTicks: startPosition?.toRuntimeTicks,
|
startTimeTicks: startPosition?.toRuntimeTicks,
|
||||||
audioStreamIndex: streamModel?.defaultAudioStreamIndex,
|
audioStreamIndex: audioStreamIndex,
|
||||||
subtitleStreamIndex: streamModel?.defaultSubStreamIndex,
|
subtitleStreamIndex: subStreamIndex,
|
||||||
enableTranscoding: true,
|
enableTranscoding: true,
|
||||||
autoOpenLiveStream: true,
|
autoOpenLiveStream: true,
|
||||||
deviceProfile: ref.read(videoProfileProvider),
|
deviceProfile: ref.read(videoProfileProvider),
|
||||||
|
|
@ -223,8 +236,8 @@ class PlaybackModelHelper {
|
||||||
if (mediaSource == null) return null;
|
if (mediaSource == null) return null;
|
||||||
|
|
||||||
final mediaStreamsWithUrls = MediaStreamsModel.fromMediaStreamsList(playbackInfo.mediaSources, ref).copyWith(
|
final mediaStreamsWithUrls = MediaStreamsModel.fromMediaStreamsList(playbackInfo.mediaSources, ref).copyWith(
|
||||||
defaultAudioStreamIndex: streamModel?.defaultAudioStreamIndex,
|
defaultAudioStreamIndex: audioStreamIndex,
|
||||||
defaultSubStreamIndex: streamModel?.defaultSubStreamIndex,
|
defaultSubStreamIndex: subStreamIndex,
|
||||||
);
|
);
|
||||||
|
|
||||||
final mediaSegments = await api.mediaSegmentsGet(id: item.id);
|
final mediaSegments = await api.mediaSegmentsGet(id: item.id);
|
||||||
|
|
@ -328,8 +341,18 @@ class PlaybackModelHelper {
|
||||||
|
|
||||||
final currentPosition = ref.read(mediaPlaybackProvider.select((value) => value.position));
|
final currentPosition = ref.read(mediaPlaybackProvider.select((value) => value.position));
|
||||||
|
|
||||||
final audioIndex = playbackModel.mediaStreams?.defaultAudioStreamIndex;
|
final audioIndex = selectAudioStream(
|
||||||
final subIndex = playbackModel.mediaStreams?.defaultSubStreamIndex;
|
ref.read(userProvider.select((value) => value?.userConfiguration?.rememberAudioSelections ?? true)),
|
||||||
|
playbackModel.mediaStreams?.currentAudioStream,
|
||||||
|
playbackModel.audioStreams,
|
||||||
|
playbackModel.mediaStreams?.defaultAudioStreamIndex
|
||||||
|
);
|
||||||
|
final subIndex = selectSubStream(
|
||||||
|
ref.read(userProvider.select((value) => value?.userConfiguration?.rememberSubtitleSelections ?? true)),
|
||||||
|
playbackModel.mediaStreams?.currentSubStream,
|
||||||
|
playbackModel.subStreams,
|
||||||
|
playbackModel.mediaStreams?.defaultSubStreamIndex
|
||||||
|
);
|
||||||
|
|
||||||
Response<PlaybackInfoResponse> response = await api.itemsItemIdPlaybackInfoPost(
|
Response<PlaybackInfoResponse> response = await api.itemsItemIdPlaybackInfoPost(
|
||||||
itemId: item.id,
|
itemId: item.id,
|
||||||
|
|
|
||||||
|
|
@ -902,4 +902,38 @@ class JellyService {
|
||||||
Future<Response<bool>> quickConnectEnabled() async => api.quickConnectEnabledGet();
|
Future<Response<bool>> quickConnectEnabled() async => api.quickConnectEnabledGet();
|
||||||
|
|
||||||
Future<Response<dynamic>> deleteItem(String itemId) => api.itemsItemIdDelete(itemId: itemId);
|
Future<Response<dynamic>> deleteItem(String itemId) => api.itemsItemIdDelete(itemId: itemId);
|
||||||
|
|
||||||
|
Future<UserConfiguration?> _updateUserConfiguration(UserConfiguration newUserConfiguration) async {
|
||||||
|
if (account?.id == null) return null;
|
||||||
|
|
||||||
|
final response = await api.usersConfigurationPost(
|
||||||
|
userId: account!.id,
|
||||||
|
body: newUserConfiguration,
|
||||||
|
);
|
||||||
|
|
||||||
|
if (response.isSuccessful) {
|
||||||
|
return newUserConfiguration;
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
Future<UserConfiguration?> updateRememberAudioSelections() {
|
||||||
|
final currentUserConfiguration = account?.userConfiguration;
|
||||||
|
if (currentUserConfiguration == null) return Future.value(null);
|
||||||
|
|
||||||
|
final updated = currentUserConfiguration.copyWith(
|
||||||
|
rememberAudioSelections: !(currentUserConfiguration.rememberAudioSelections ?? false),
|
||||||
|
);
|
||||||
|
return _updateUserConfiguration(updated);
|
||||||
|
}
|
||||||
|
|
||||||
|
Future<UserConfiguration?> updateRememberSubtitleSelections() {
|
||||||
|
final current = account?.userConfiguration;
|
||||||
|
if (current == null) return Future.value(null);
|
||||||
|
|
||||||
|
final updated = current.copyWith(
|
||||||
|
rememberSubtitleSelections: !(current.rememberSubtitleSelections ?? false),
|
||||||
|
);
|
||||||
|
return _updateUserConfiguration(updated);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -45,6 +45,7 @@ class User extends _$User {
|
||||||
name: response.body?.name ?? state?.name ?? "",
|
name: response.body?.name ?? state?.name ?? "",
|
||||||
policy: response.body?.policy,
|
policy: response.body?.policy,
|
||||||
serverConfiguration: systemConfiguration.body,
|
serverConfiguration: systemConfiguration.body,
|
||||||
|
userConfiguration: response.body?.configuration,
|
||||||
quickConnectState: quickConnectStatus.body ?? false,
|
quickConnectState: quickConnectStatus.body ?? false,
|
||||||
latestItemsExcludes: response.body?.configuration?.latestItemsExcludes ?? [],
|
latestItemsExcludes: response.body?.configuration?.latestItemsExcludes ?? [],
|
||||||
);
|
);
|
||||||
|
|
@ -53,6 +54,20 @@ class User extends _$User {
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
void setRememberAudioSelections() async {
|
||||||
|
final newUserConfiguration = await api.updateRememberAudioSelections();
|
||||||
|
if (newUserConfiguration != null) {
|
||||||
|
userState = state?.copyWith(userConfiguration: newUserConfiguration);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
void setRememberSubtitleSelections() async {
|
||||||
|
final newUserConfiguration = await api.updateRememberSubtitleSelections();
|
||||||
|
if (newUserConfiguration != null) {
|
||||||
|
userState = state?.copyWith(userConfiguration: newUserConfiguration);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
Future<Response> refreshMetaData(
|
Future<Response> refreshMetaData(
|
||||||
String itemId, {
|
String itemId, {
|
||||||
MetadataRefresh? metadataRefreshMode,
|
MetadataRefresh? metadataRefreshMode,
|
||||||
|
|
|
||||||
|
|
@ -10,6 +10,7 @@ import 'package:flutter_riverpod/flutter_riverpod.dart';
|
||||||
import 'package:fladder/models/items/media_segments_model.dart';
|
import 'package:fladder/models/items/media_segments_model.dart';
|
||||||
import 'package:fladder/models/settings/home_settings_model.dart';
|
import 'package:fladder/models/settings/home_settings_model.dart';
|
||||||
import 'package:fladder/models/settings/video_player_settings.dart';
|
import 'package:fladder/models/settings/video_player_settings.dart';
|
||||||
|
import 'package:fladder/providers/user_provider.dart';
|
||||||
import 'package:fladder/providers/connectivity_provider.dart';
|
import 'package:fladder/providers/connectivity_provider.dart';
|
||||||
import 'package:fladder/providers/settings/video_player_settings_provider.dart';
|
import 'package:fladder/providers/settings/video_player_settings_provider.dart';
|
||||||
import 'package:fladder/screens/settings/settings_list_tile.dart';
|
import 'package:fladder/screens/settings/settings_list_tile.dart';
|
||||||
|
|
@ -166,6 +167,29 @@ class _PlayerSettingsPageState extends ConsumerState<PlayerSettingsPage> {
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
|
SettingsLabelDivider(label: context.localized.playbackTrackSelection),
|
||||||
|
SettingsListTile(
|
||||||
|
label: Text(context.localized.rememberAudioSelections),
|
||||||
|
subLabel: Text(context.localized.rememberAudioSelectionsDesc),
|
||||||
|
onTap: () => ref.read(userProvider.notifier).setRememberAudioSelections(),
|
||||||
|
trailing: Switch(
|
||||||
|
value: ref.watch(userProvider.select(
|
||||||
|
(value) => value?.userConfiguration?.rememberAudioSelections ?? true,
|
||||||
|
)),
|
||||||
|
onChanged: (_) => ref.read(userProvider.notifier).setRememberAudioSelections(),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
SettingsListTile(
|
||||||
|
label: Text(context.localized.rememberSubtitleSelections),
|
||||||
|
subLabel: Text(context.localized.rememberSubtitleSelectionsDesc),
|
||||||
|
onTap: () => ref.read(userProvider.notifier).setRememberSubtitleSelections(),
|
||||||
|
trailing: Switch(
|
||||||
|
value: ref.watch(userProvider.select(
|
||||||
|
(value) => value?.userConfiguration?.rememberSubtitleSelections ?? true,
|
||||||
|
)),
|
||||||
|
onChanged: (_) => ref.read(userProvider.notifier).setRememberSubtitleSelections(),
|
||||||
|
),
|
||||||
|
),
|
||||||
const Divider(),
|
const Divider(),
|
||||||
SettingsLabelDivider(label: context.localized.advanced),
|
SettingsLabelDivider(label: context.localized.advanced),
|
||||||
if (PlayerOptions.available.length != 1)
|
if (PlayerOptions.available.length != 1)
|
||||||
|
|
@ -235,16 +259,16 @@ class _PlayerSettingsPageState extends ConsumerState<PlayerSettingsPage> {
|
||||||
label: Text(context.localized.settingsPlayerBufferSizeTitle),
|
label: Text(context.localized.settingsPlayerBufferSizeTitle),
|
||||||
subLabel: Text(context.localized.settingsPlayerBufferSizeDesc),
|
subLabel: Text(context.localized.settingsPlayerBufferSizeDesc),
|
||||||
trailing: SizedBox(
|
trailing: SizedBox(
|
||||||
width: 70,
|
width: 70,
|
||||||
child: IntInputField(
|
child: IntInputField(
|
||||||
suffix: 'MB',
|
suffix: 'MB',
|
||||||
controller: TextEditingController(text: videoSettings.bufferSize.toString()),
|
controller: TextEditingController(text: videoSettings.bufferSize.toString()),
|
||||||
onSubmitted: (value) {
|
onSubmitted: (value) {
|
||||||
if (value != null) {
|
if (value != null) {
|
||||||
provider.setBufferSize(value);
|
provider.setBufferSize(value);
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
)),
|
)),
|
||||||
),
|
),
|
||||||
SettingsListTile(
|
SettingsListTile(
|
||||||
label: Text(context.localized.settingsPlayerCustomSubtitlesTitle),
|
label: Text(context.localized.settingsPlayerCustomSubtitlesTitle),
|
||||||
|
|
|
||||||
56
lib/util/streams_selection.dart
Normal file
56
lib/util/streams_selection.dart
Normal file
|
|
@ -0,0 +1,56 @@
|
||||||
|
|
||||||
|
import 'package:fladder/models/items/media_streams_model.dart';
|
||||||
|
|
||||||
|
int? selectAudioStream(bool rememberAudioSelection, AudioAndSubStreamModel? previousStream, List<AudioAndSubStreamModel>? currentStream, int? defaultStream) {
|
||||||
|
if (!rememberAudioSelection){
|
||||||
|
return defaultStream;
|
||||||
|
}
|
||||||
|
return _selectStream(previousStream, currentStream, defaultStream);
|
||||||
|
}
|
||||||
|
|
||||||
|
int? selectSubStream(bool rememberSubSelection, AudioAndSubStreamModel? previousStream, List<AudioAndSubStreamModel>? currentStream, int? defaultStream) {
|
||||||
|
if (!rememberSubSelection){
|
||||||
|
return defaultStream;
|
||||||
|
}
|
||||||
|
return _selectStream(previousStream, currentStream, defaultStream);
|
||||||
|
}
|
||||||
|
|
||||||
|
int? _selectStream(AudioAndSubStreamModel? previousStream, List<AudioAndSubStreamModel>? currentStream, int? defaultStream) {
|
||||||
|
if (currentStream == null || previousStream == null){
|
||||||
|
return defaultStream;
|
||||||
|
}
|
||||||
|
|
||||||
|
int? bestStreamIndex;
|
||||||
|
int bestStreamScore = 0;
|
||||||
|
|
||||||
|
// Find the relative index of the previous stream
|
||||||
|
int prevRelIndex = 0;
|
||||||
|
for (var stream in currentStream) {
|
||||||
|
if (stream.index == previousStream.index) break;
|
||||||
|
prevRelIndex += 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
int newRelIndex = 0;
|
||||||
|
for (var stream in currentStream) {
|
||||||
|
int score = 0;
|
||||||
|
|
||||||
|
if (previousStream.codec == stream.codec) score += 1;
|
||||||
|
if (prevRelIndex == newRelIndex) score += 1;
|
||||||
|
if (previousStream.displayTitle == stream.displayTitle) {
|
||||||
|
score += 2;
|
||||||
|
}
|
||||||
|
if (previousStream.language != 'und' &&
|
||||||
|
previousStream.language == stream.language) {
|
||||||
|
score += 2;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (score > bestStreamScore && score >= 3) {
|
||||||
|
bestStreamScore = score;
|
||||||
|
bestStreamIndex = stream.index;
|
||||||
|
}
|
||||||
|
|
||||||
|
newRelIndex += 1;
|
||||||
|
}
|
||||||
|
return bestStreamIndex ?? defaultStream;
|
||||||
|
}
|
||||||
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue