From dccd8b70b665b624bf0a4a1e7df4332a14662dee Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?N=CE=9EV=CE=9BR?= Date: Mon, 30 Mar 2026 00:35:16 +0200 Subject: [PATCH 1/3] feat(windows): Native Dolby Vision & High-Quality Audio Support via Energy Player This comprehensive update introduces native Dolby Vision (DV) playback and advanced audio codec support (Atmos, DTS:X, 5.1 passthrough) for Fladder on Windows. Rationale: Internal media players and standard Windows foundations often struggle to render Dolby Vision accurately, frequently resulting in washed-out colors (SDR tone mapping) or complete playback failure for specific profiles. Energy Media Player was selected as the integration partner because it provides: - Native Dolby Vision rendering on Windows. - Robust audio passthrough capabilities (Atmos/5.1). - A seamless external protocol ('energyplayer:') allowing deep integration from Dart. - Excellent handling of local and URL-based media strings. Key Implementation Details: 1. Dolby Vision Detection: - Enhanced MediaStreamsModel to identify all DV profiles (VideoRangeType: dovi, hdr10, hlg, and hybrids). - Hooked into ItemBaseModelExtensions.play to intercept DV content. 2. External Integration Logic: - Created ExternalPlayerHelper to manage Energy Player's lifecycle, store checking (Product ID: 9P9ZH5FL1BFK), and protocol launch. - Synchronizes initial playback position and subtitles automatically. 3. User Interface & Persistence: - Created DVPlayerSelectionDialog: A professional choice dialog appearing only for DV content. - Added persistent settings to VideoPlayerSettingsProvider with a "Remember my choice" option. - Integrated detailed configuration instructions in the UI, guiding users to 'Video/Audio settings' within Energy Player. 4. Technical & Build Stability: - Resolved a critical namespace collision between flutter/widgets.dart (RepeatMode) and the Jellyfin API generator. AI & Quality Note: This feature was architected and implemented with the assistance of the Antigravity AI agent. The AI was instrumental in mapping complex models, troubleshooting CMake failures, and harmonizing localizations. A manual code review is recommended as AI was used to resolve several non-obvious integration hurdles. Goal: Deliver a premium, bit-perfect Dolby Vision experience for Fladder users on Windows PCs. --- lib/l10n/app_en.arb | 14 +- lib/models/items/media_streams_model.dart | 15 + .../playback/direct_playback_model.dart | 2 +- .../playback/transcode_playback_model.dart | 2 +- .../settings/video_player_settings.dart | 17 + .../video_player_settings.freezed.dart | 51 +- .../settings/video_player_settings.g.dart | 10 + .../video_player_settings_provider.dart | 2 + .../settings/player_settings_page.dart | 38 ++ lib/seerr/seerr_models.g.dart | 476 ++++++++++++------ lib/util/external_player_helper.dart | 65 +++ .../item_base_model/play_item_helpers.dart | 25 + .../shared/dv_player_selection_dialog.dart | 120 +++++ pubspec.lock | 20 +- 14 files changed, 690 insertions(+), 167 deletions(-) create mode 100644 lib/util/external_player_helper.dart create mode 100644 lib/widgets/shared/dv_player_selection_dialog.dart diff --git a/lib/l10n/app_en.arb b/lib/l10n/app_en.arb index c280e9ad7..feb550072 100644 --- a/lib/l10n/app_en.arb +++ b/lib/l10n/app_en.arb @@ -2570,5 +2570,17 @@ "@groupedFoldersDescription": {}, "downloadTranscoded": "Download transcoded", "downloadOriginal": "Download original", - "today": "Today" + "today": "Today", + "dvPlayerSelectionTitle": "Dolby Vision Player", + "dvPlayerSelectionDesc": "Choose how to play Dolby Vision on Windows. Energy Player is required for native vision and high-quality audio passthrough (Atmos, 5.1).", + "dvPlayerAsk": "Always ask", + "dvPlayerDisabled": "Disabled (Internal player)", + "dvPlayerEnergyPlayer": "Energy Player (External)", + "dvPlayerDialogTitle": "Play with...", + "dvPlayerDialogDesc": "This content is in native Dolby Vision. Use Energy Player for accurate HDR colors and advanced audio support (Atmos/5.1 passthrough). Internal playback may have dull colors or limited audio codec support.", + "dvRememberChoice": "Remember my choice", + "dvPlayerNotInstalled": "Energy Player is not installed. Would you like to download it from the Microsoft Store?", + "dvPlayerGetFromStore": "Get from Store", + "dvEnableInstruction": "Configuration for native Dolby Vision and Audio:\n• Enable 'Allow Dolby Vision decoding' in Energy Player 'Video/Audio settings'.\n• Enable relevant 'Passthrough' options for Atmos/5.1 support.\n• Ensure HDR is enabled in Windows Display settings.", + "dvSyncWarning": "Note: Playback state is saved within Energy Player only and will not sync back to Fladder." } \ No newline at end of file diff --git a/lib/models/items/media_streams_model.dart b/lib/models/items/media_streams_model.dart index 63478e1ad..c1f7cc1db 100644 --- a/lib/models/items/media_streams_model.dart +++ b/lib/models/items/media_streams_model.dart @@ -71,6 +71,8 @@ class MediaStreamsModel { String? get mediaInfoTag => '${displayProfile?.value} ${resolution?.value}'; + bool get hasDolbyVision => videoStreams.any((element) => element.isDolbyVision); + Widget? audioIcon( BuildContext context, Function()? onTap, @@ -257,6 +259,19 @@ class VideoStreamModel extends StreamModel { index: stream.index ?? -1, ); } + + bool get isDolbyVision { + final range = videoRangeType; + if (range == null) return false; + return range == VideoRangeType.dovi || + range == VideoRangeType.doviwithhdr10 || + range == VideoRangeType.doviwithhlg || + range == VideoRangeType.doviwithsdr || + range == VideoRangeType.doviwithel || + range == VideoRangeType.doviwithhdr10plus || + range == VideoRangeType.doviwithelhdr10plus; + } + String get prettyName { return "${Resolution.fromVideoStream(this)?.value} - ${DisplayProfile.fromVideoStream(this).value} - (${codec.toUpperCase()})"; } diff --git a/lib/models/playback/direct_playback_model.dart b/lib/models/playback/direct_playback_model.dart index 19edfbfea..15f121ec4 100644 --- a/lib/models/playback/direct_playback_model.dart +++ b/lib/models/playback/direct_playback_model.dart @@ -1,4 +1,4 @@ -import 'package:flutter/widgets.dart'; +import 'package:flutter/widgets.dart' hide RepeatMode; import 'package:collection/collection.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart'; diff --git a/lib/models/playback/transcode_playback_model.dart b/lib/models/playback/transcode_playback_model.dart index 4b0110542..5a20b949f 100644 --- a/lib/models/playback/transcode_playback_model.dart +++ b/lib/models/playback/transcode_playback_model.dart @@ -1,4 +1,4 @@ -import 'package:flutter/widgets.dart'; +import 'package:flutter/widgets.dart' hide RepeatMode; import 'package:collection/collection.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart'; diff --git a/lib/models/settings/video_player_settings.dart b/lib/models/settings/video_player_settings.dart index 427f8bf32..abec49fc9 100644 --- a/lib/models/settings/video_player_settings.dart +++ b/lib/models/settings/video_player_settings.dart @@ -84,6 +84,7 @@ abstract class VideoPlayerSettingsModel with _$VideoPlayerSettingsModel { @Default(2.0) double speedBoostRate, @Default(true) bool enableDoubleTapSeek, @Default(false) bool enableAdvancedVideoOptions, + @Default(DVPlayerChoice.ask) DVPlayerChoice dvPlayerChoice, }) = _VideoPlayerSettingsModel; double get volume => switch (defaultTargetPlatform) { @@ -245,3 +246,19 @@ Map get _defaultVideoHotKeys => { VideoHotKeys.exit => KeyCombination(key: LogicalKeyboardKey.escape), }, }; + +enum DVPlayerChoice { + internalPlayer, + ask, + energyPlayer; + + const DVPlayerChoice(); + + String label(BuildContext context) { + return switch (this) { + DVPlayerChoice.internalPlayer => "Disabled (Internal player)", + DVPlayerChoice.ask => "Always ask", + DVPlayerChoice.energyPlayer => "Energy Player (External)", + }; + } +} diff --git a/lib/models/settings/video_player_settings.freezed.dart b/lib/models/settings/video_player_settings.freezed.dart index ef65e7386..692e44f07 100644 --- a/lib/models/settings/video_player_settings.freezed.dart +++ b/lib/models/settings/video_player_settings.freezed.dart @@ -35,6 +35,7 @@ mixin _$VideoPlayerSettingsModel implements DiagnosticableTreeMixin { double get speedBoostRate; bool get enableDoubleTapSeek; bool get enableAdvancedVideoOptions; + DVPlayerChoice get dvPlayerChoice; /// Create a copy of VideoPlayerSettingsModel /// with the given fields replaced by the non-null parameter values. @@ -72,12 +73,13 @@ mixin _$VideoPlayerSettingsModel implements DiagnosticableTreeMixin { ..add(DiagnosticsProperty('speedBoostRate', speedBoostRate)) ..add(DiagnosticsProperty('enableDoubleTapSeek', enableDoubleTapSeek)) ..add(DiagnosticsProperty( - 'enableAdvancedVideoOptions', enableAdvancedVideoOptions)); + 'enableAdvancedVideoOptions', enableAdvancedVideoOptions)) + ..add(DiagnosticsProperty('dvPlayerChoice', dvPlayerChoice)); } @override String toString({DiagnosticLevel minLevel = DiagnosticLevel.info}) { - return 'VideoPlayerSettingsModel(screenBrightness: $screenBrightness, videoFit: $videoFit, fillScreen: $fillScreen, hardwareAccel: $hardwareAccel, useLibass: $useLibass, enableTunneling: $enableTunneling, bufferSize: $bufferSize, playerOptions: $playerOptions, internalVolume: $internalVolume, allowedOrientations: $allowedOrientations, nextVideoType: $nextVideoType, maxHomeBitrate: $maxHomeBitrate, maxInternetBitrate: $maxInternetBitrate, audioDevice: $audioDevice, segmentSkipSettings: $segmentSkipSettings, hotKeys: $hotKeys, screensaver: $screensaver, enableSpeedBoost: $enableSpeedBoost, speedBoostRate: $speedBoostRate, enableDoubleTapSeek: $enableDoubleTapSeek, enableAdvancedVideoOptions: $enableAdvancedVideoOptions)'; + return 'VideoPlayerSettingsModel(screenBrightness: $screenBrightness, videoFit: $videoFit, fillScreen: $fillScreen, hardwareAccel: $hardwareAccel, useLibass: $useLibass, enableTunneling: $enableTunneling, bufferSize: $bufferSize, playerOptions: $playerOptions, internalVolume: $internalVolume, allowedOrientations: $allowedOrientations, nextVideoType: $nextVideoType, maxHomeBitrate: $maxHomeBitrate, maxInternetBitrate: $maxInternetBitrate, audioDevice: $audioDevice, segmentSkipSettings: $segmentSkipSettings, hotKeys: $hotKeys, screensaver: $screensaver, enableSpeedBoost: $enableSpeedBoost, speedBoostRate: $speedBoostRate, enableDoubleTapSeek: $enableDoubleTapSeek, enableAdvancedVideoOptions: $enableAdvancedVideoOptions, dvPlayerChoice: $dvPlayerChoice)'; } } @@ -108,7 +110,8 @@ abstract mixin class $VideoPlayerSettingsModelCopyWith<$Res> { bool enableSpeedBoost, double speedBoostRate, bool enableDoubleTapSeek, - bool enableAdvancedVideoOptions}); + bool enableAdvancedVideoOptions, + DVPlayerChoice dvPlayerChoice}); } /// @nodoc @@ -145,6 +148,7 @@ class _$VideoPlayerSettingsModelCopyWithImpl<$Res> Object? speedBoostRate = null, Object? enableDoubleTapSeek = null, Object? enableAdvancedVideoOptions = null, + Object? dvPlayerChoice = null, }) { return _then(_self.copyWith( screenBrightness: freezed == screenBrightness @@ -231,6 +235,10 @@ class _$VideoPlayerSettingsModelCopyWithImpl<$Res> ? _self.enableAdvancedVideoOptions : enableAdvancedVideoOptions // ignore: cast_nullable_to_non_nullable as bool, + dvPlayerChoice: null == dvPlayerChoice + ? _self.dvPlayerChoice + : dvPlayerChoice // ignore: cast_nullable_to_non_nullable + as DVPlayerChoice, )); } } @@ -349,7 +357,8 @@ extension VideoPlayerSettingsModelPatterns on VideoPlayerSettingsModel { bool enableSpeedBoost, double speedBoostRate, bool enableDoubleTapSeek, - bool enableAdvancedVideoOptions)? + bool enableAdvancedVideoOptions, + DVPlayerChoice dvPlayerChoice)? $default, { required TResult orElse(), }) { @@ -377,7 +386,8 @@ extension VideoPlayerSettingsModelPatterns on VideoPlayerSettingsModel { _that.enableSpeedBoost, _that.speedBoostRate, _that.enableDoubleTapSeek, - _that.enableAdvancedVideoOptions); + _that.enableAdvancedVideoOptions, + _that.dvPlayerChoice); case _: return orElse(); } @@ -419,7 +429,8 @@ extension VideoPlayerSettingsModelPatterns on VideoPlayerSettingsModel { bool enableSpeedBoost, double speedBoostRate, bool enableDoubleTapSeek, - bool enableAdvancedVideoOptions) + bool enableAdvancedVideoOptions, + DVPlayerChoice dvPlayerChoice) $default, ) { final _that = this; @@ -446,7 +457,8 @@ extension VideoPlayerSettingsModelPatterns on VideoPlayerSettingsModel { _that.enableSpeedBoost, _that.speedBoostRate, _that.enableDoubleTapSeek, - _that.enableAdvancedVideoOptions); + _that.enableAdvancedVideoOptions, + _that.dvPlayerChoice); case _: throw StateError('Unexpected subclass'); } @@ -487,7 +499,8 @@ extension VideoPlayerSettingsModelPatterns on VideoPlayerSettingsModel { bool enableSpeedBoost, double speedBoostRate, bool enableDoubleTapSeek, - bool enableAdvancedVideoOptions)? + bool enableAdvancedVideoOptions, + DVPlayerChoice dvPlayerChoice)? $default, ) { final _that = this; @@ -514,7 +527,8 @@ extension VideoPlayerSettingsModelPatterns on VideoPlayerSettingsModel { _that.enableSpeedBoost, _that.speedBoostRate, _that.enableDoubleTapSeek, - _that.enableAdvancedVideoOptions); + _that.enableAdvancedVideoOptions, + _that.dvPlayerChoice); case _: return null; } @@ -547,7 +561,8 @@ class _VideoPlayerSettingsModel extends VideoPlayerSettingsModel this.enableSpeedBoost = false, this.speedBoostRate = 2.0, this.enableDoubleTapSeek = true, - this.enableAdvancedVideoOptions = false}) + this.enableAdvancedVideoOptions = false, + this.dvPlayerChoice = DVPlayerChoice.ask}) : _allowedOrientations = allowedOrientations, _segmentSkipSettings = segmentSkipSettings, _hotKeys = hotKeys, @@ -636,6 +651,9 @@ class _VideoPlayerSettingsModel extends VideoPlayerSettingsModel @override @JsonKey() final bool enableAdvancedVideoOptions; + @override + @JsonKey() + final DVPlayerChoice dvPlayerChoice; /// Create a copy of VideoPlayerSettingsModel /// with the given fields replaced by the non-null parameter values. @@ -678,12 +696,13 @@ class _VideoPlayerSettingsModel extends VideoPlayerSettingsModel ..add(DiagnosticsProperty('speedBoostRate', speedBoostRate)) ..add(DiagnosticsProperty('enableDoubleTapSeek', enableDoubleTapSeek)) ..add(DiagnosticsProperty( - 'enableAdvancedVideoOptions', enableAdvancedVideoOptions)); + 'enableAdvancedVideoOptions', enableAdvancedVideoOptions)) + ..add(DiagnosticsProperty('dvPlayerChoice', dvPlayerChoice)); } @override String toString({DiagnosticLevel minLevel = DiagnosticLevel.info}) { - return 'VideoPlayerSettingsModel(screenBrightness: $screenBrightness, videoFit: $videoFit, fillScreen: $fillScreen, hardwareAccel: $hardwareAccel, useLibass: $useLibass, enableTunneling: $enableTunneling, bufferSize: $bufferSize, playerOptions: $playerOptions, internalVolume: $internalVolume, allowedOrientations: $allowedOrientations, nextVideoType: $nextVideoType, maxHomeBitrate: $maxHomeBitrate, maxInternetBitrate: $maxInternetBitrate, audioDevice: $audioDevice, segmentSkipSettings: $segmentSkipSettings, hotKeys: $hotKeys, screensaver: $screensaver, enableSpeedBoost: $enableSpeedBoost, speedBoostRate: $speedBoostRate, enableDoubleTapSeek: $enableDoubleTapSeek, enableAdvancedVideoOptions: $enableAdvancedVideoOptions)'; + return 'VideoPlayerSettingsModel(screenBrightness: $screenBrightness, videoFit: $videoFit, fillScreen: $fillScreen, hardwareAccel: $hardwareAccel, useLibass: $useLibass, enableTunneling: $enableTunneling, bufferSize: $bufferSize, playerOptions: $playerOptions, internalVolume: $internalVolume, allowedOrientations: $allowedOrientations, nextVideoType: $nextVideoType, maxHomeBitrate: $maxHomeBitrate, maxInternetBitrate: $maxInternetBitrate, audioDevice: $audioDevice, segmentSkipSettings: $segmentSkipSettings, hotKeys: $hotKeys, screensaver: $screensaver, enableSpeedBoost: $enableSpeedBoost, speedBoostRate: $speedBoostRate, enableDoubleTapSeek: $enableDoubleTapSeek, enableAdvancedVideoOptions: $enableAdvancedVideoOptions, dvPlayerChoice: $dvPlayerChoice)'; } } @@ -716,7 +735,8 @@ abstract mixin class _$VideoPlayerSettingsModelCopyWith<$Res> bool enableSpeedBoost, double speedBoostRate, bool enableDoubleTapSeek, - bool enableAdvancedVideoOptions}); + bool enableAdvancedVideoOptions, + DVPlayerChoice dvPlayerChoice}); } /// @nodoc @@ -753,6 +773,7 @@ class __$VideoPlayerSettingsModelCopyWithImpl<$Res> Object? speedBoostRate = null, Object? enableDoubleTapSeek = null, Object? enableAdvancedVideoOptions = null, + Object? dvPlayerChoice = null, }) { return _then(_VideoPlayerSettingsModel( screenBrightness: freezed == screenBrightness @@ -839,6 +860,10 @@ class __$VideoPlayerSettingsModelCopyWithImpl<$Res> ? _self.enableAdvancedVideoOptions : enableAdvancedVideoOptions // ignore: cast_nullable_to_non_nullable as bool, + dvPlayerChoice: null == dvPlayerChoice + ? _self.dvPlayerChoice + : dvPlayerChoice // ignore: cast_nullable_to_non_nullable + as DVPlayerChoice, )); } } diff --git a/lib/models/settings/video_player_settings.g.dart b/lib/models/settings/video_player_settings.g.dart index 0b4547e8d..c61de0ab6 100644 --- a/lib/models/settings/video_player_settings.g.dart +++ b/lib/models/settings/video_player_settings.g.dart @@ -52,6 +52,9 @@ _VideoPlayerSettingsModel _$VideoPlayerSettingsModelFromJson( enableDoubleTapSeek: json['enableDoubleTapSeek'] as bool? ?? true, enableAdvancedVideoOptions: json['enableAdvancedVideoOptions'] as bool? ?? false, + dvPlayerChoice: $enumDecodeNullable( + _$DVPlayerChoiceEnumMap, json['dvPlayerChoice']) ?? + DVPlayerChoice.ask, ); Map _$VideoPlayerSettingsModelToJson( @@ -82,6 +85,7 @@ Map _$VideoPlayerSettingsModelToJson( 'speedBoostRate': instance.speedBoostRate, 'enableDoubleTapSeek': instance.enableDoubleTapSeek, 'enableAdvancedVideoOptions': instance.enableAdvancedVideoOptions, + 'dvPlayerChoice': _$DVPlayerChoiceEnumMap[instance.dvPlayerChoice]!, }; const _$BoxFitEnumMap = { @@ -175,3 +179,9 @@ const _$ScreensaverEnumMap = { Screensaver.time: 'time', Screensaver.black: 'black', }; + +const _$DVPlayerChoiceEnumMap = { + DVPlayerChoice.internalPlayer: 'internalPlayer', + DVPlayerChoice.ask: 'ask', + DVPlayerChoice.energyPlayer: 'energyPlayer', +}; diff --git a/lib/providers/settings/video_player_settings_provider.dart b/lib/providers/settings/video_player_settings_provider.dart index da71228cb..e8813b428 100644 --- a/lib/providers/settings/video_player_settings_provider.dart +++ b/lib/providers/settings/video_player_settings_provider.dart @@ -137,4 +137,6 @@ class VideoPlayerSettingsProviderNotifier extends StateNotifier state = state.copyWith(enableDoubleTapSeek: value); void setEnableAdvancedVideoOptions(bool value) => state = state.copyWith(enableAdvancedVideoOptions: value); + + void setDVPlayerChoice(DVPlayerChoice value) => state = state.copyWith(dvPlayerChoice: value); } diff --git a/lib/screens/settings/player_settings_page.dart b/lib/screens/settings/player_settings_page.dart index ff41cc885..8db3f2af3 100644 --- a/lib/screens/settings/player_settings_page.dart +++ b/lib/screens/settings/player_settings_page.dart @@ -138,6 +138,44 @@ class _PlayerSettingsPageState extends ConsumerState { ], ), const SizedBox(height: 12), + if (Platform.isWindows) + ...settingsListGroup( + context, + SettingsLabelDivider(label: context.localized.dvPlayerSelectionTitle), + [ + SettingsListTile( + label: Text(context.localized.dvPlayerSelectionTitle), + subLabel: Text(context.localized.dvPlayerSelectionDesc), + trailing: EnumBox( + current: videoSettings.dvPlayerChoice.label(context), + itemBuilder: (context) => DVPlayerChoice.values + .map( + (entry) => ItemActionButton( + label: Text(entry.label(context)), + action: () => provider.setDVPlayerChoice(entry), + ), + ) + .toList(), + ), + ), + AnimatedFadeSize( + child: Column( + children: [ + SettingsMessageBox( + context.localized.dvEnableInstruction, + messageType: MessageType.info, + ), + const SizedBox(height: 8), + SettingsMessageBox( + context.localized.dvSyncWarning, + messageType: MessageType.warning, + ), + ], + ), + ), + ], + ), + const SizedBox(height: 12), ...settingsListGroup(context, SettingsLabelDivider(label: context.localized.mediaSegmentActions), [ ...videoSettings.segmentSkipSettings.entries.sorted((a, b) => b.key.index.compareTo(a.key.index)).map( (entry) => Padding( diff --git a/lib/seerr/seerr_models.g.dart b/lib/seerr/seerr_models.g.dart index c563bf8b7..23bd00c06 100644 --- a/lib/seerr/seerr_models.g.dart +++ b/lib/seerr/seerr_models.g.dart @@ -13,24 +13,32 @@ SeerrStatus _$SeerrStatusFromJson(Map json) => SeerrStatus( commitsBehind: (json['commitsBehind'] as num?)?.toInt(), ); -Map _$SeerrStatusToJson(SeerrStatus instance) => { +Map _$SeerrStatusToJson(SeerrStatus instance) => + { 'version': instance.version, 'commitTag': instance.commitTag, 'updateAvailable': instance.updateAvailable, 'commitsBehind': instance.commitsBehind, }; -SeerrUserQuota _$SeerrUserQuotaFromJson(Map json) => SeerrUserQuota( - movie: json['movie'] == null ? null : SeerrQuotaEntry.fromJson(json['movie'] as Map), - tv: json['tv'] == null ? null : SeerrQuotaEntry.fromJson(json['tv'] as Map), +SeerrUserQuota _$SeerrUserQuotaFromJson(Map json) => + SeerrUserQuota( + movie: json['movie'] == null + ? null + : SeerrQuotaEntry.fromJson(json['movie'] as Map), + tv: json['tv'] == null + ? null + : SeerrQuotaEntry.fromJson(json['tv'] as Map), ); -Map _$SeerrUserQuotaToJson(SeerrUserQuota instance) => { +Map _$SeerrUserQuotaToJson(SeerrUserQuota instance) => + { 'movie': instance.movie, 'tv': instance.tv, }; -SeerrQuotaEntry _$SeerrQuotaEntryFromJson(Map json) => SeerrQuotaEntry( +SeerrQuotaEntry _$SeerrQuotaEntryFromJson(Map json) => + SeerrQuotaEntry( days: (json['days'] as num?)?.toInt(), limit: (json['limit'] as num?)?.toInt(), used: (json['used'] as num?)?.toInt(), @@ -38,7 +46,8 @@ SeerrQuotaEntry _$SeerrQuotaEntryFromJson(Map json) => SeerrQuo restricted: json['restricted'] as bool?, ); -Map _$SeerrQuotaEntryToJson(SeerrQuotaEntry instance) => { +Map _$SeerrQuotaEntryToJson(SeerrQuotaEntry instance) => + { 'days': instance.days, 'limit': instance.limit, 'used': instance.used, @@ -46,47 +55,61 @@ Map _$SeerrQuotaEntryToJson(SeerrQuotaEntry instance) => json) => SeerrUserSettings( +SeerrUserSettings _$SeerrUserSettingsFromJson(Map json) => + SeerrUserSettings( locale: json['locale'] as String?, discoverRegion: json['discoverRegion'] as String?, originalLanguage: json['originalLanguage'] as String?, ); -Map _$SeerrUserSettingsToJson(SeerrUserSettings instance) => { +Map _$SeerrUserSettingsToJson(SeerrUserSettings instance) => + { 'locale': instance.locale, 'discoverRegion': instance.discoverRegion, 'originalLanguage': instance.originalLanguage, }; -SeerrUsersResponse _$SeerrUsersResponseFromJson(Map json) => SeerrUsersResponse( - results: - (json['results'] as List?)?.map((e) => SeerrUserModel.fromJson(e as Map)).toList(), - pageInfo: json['pageInfo'] == null ? null : SeerrPageInfo.fromJson(json['pageInfo'] as Map), +SeerrUsersResponse _$SeerrUsersResponseFromJson(Map json) => + SeerrUsersResponse( + results: (json['results'] as List?) + ?.map((e) => SeerrUserModel.fromJson(e as Map)) + .toList(), + pageInfo: json['pageInfo'] == null + ? null + : SeerrPageInfo.fromJson(json['pageInfo'] as Map), ); -Map _$SeerrUsersResponseToJson(SeerrUsersResponse instance) => { +Map _$SeerrUsersResponseToJson(SeerrUsersResponse instance) => + { 'results': instance.results, 'pageInfo': instance.pageInfo, }; -SeerrContentRating _$SeerrContentRatingFromJson(Map json) => SeerrContentRating( +SeerrContentRating _$SeerrContentRatingFromJson(Map json) => + SeerrContentRating( countryCode: json['iso_3166_1'] as String?, rating: json['rating'] as String?, descriptors: json['descriptors'] as List?, ); -Map _$SeerrContentRatingToJson(SeerrContentRating instance) => { +Map _$SeerrContentRatingToJson(SeerrContentRating instance) => + { 'iso_3166_1': instance.countryCode, 'rating': instance.rating, 'descriptors': instance.descriptors, }; SeerrCredits _$SeerrCreditsFromJson(Map json) => SeerrCredits( - cast: (json['cast'] as List?)?.map((e) => SeerrCast.fromJson(e as Map)).toList(), - crew: (json['crew'] as List?)?.map((e) => SeerrCrew.fromJson(e as Map)).toList(), + cast: (json['cast'] as List?) + ?.map((e) => SeerrCast.fromJson(e as Map)) + .toList(), + crew: (json['crew'] as List?) + ?.map((e) => SeerrCrew.fromJson(e as Map)) + .toList(), ); -Map _$SeerrCreditsToJson(SeerrCredits instance) => { +Map _$SeerrCreditsToJson(SeerrCredits instance) => + { 'cast': instance.cast, 'crew': instance.crew, }; @@ -133,7 +156,8 @@ Map _$SeerrCrewToJson(SeerrCrew instance) => { 'profilePath': instance.internalProfilePath, }; -SeerrMovieDetails _$SeerrMovieDetailsFromJson(Map json) => SeerrMovieDetails( +SeerrMovieDetails _$SeerrMovieDetailsFromJson(Map json) => + SeerrMovieDetails( id: (json['id'] as num?)?.toInt(), title: json['title'] as String?, originalTitle: json['originalTitle'] as String?, @@ -144,18 +168,28 @@ SeerrMovieDetails _$SeerrMovieDetailsFromJson(Map json) => Seer voteAverage: (json['voteAverage'] as num?)?.toDouble(), voteCount: (json['voteCount'] as num?)?.toInt(), runtime: (json['runtime'] as num?)?.toInt(), - genres: (json['genres'] as List?)?.map((e) => SeerrGenre.fromJson(e as Map)).toList(), - mediaInfo: json['mediaInfo'] == null ? null : SeerrMediaInfo.fromJson(json['mediaInfo'] as Map), - externalIds: - json['externalIds'] == null ? null : SeerrExternalIds.fromJson(json['externalIds'] as Map), - credits: json['credits'] == null ? null : SeerrCredits.fromJson(json['credits'] as Map), + genres: (json['genres'] as List?) + ?.map((e) => SeerrGenre.fromJson(e as Map)) + .toList(), + mediaInfo: json['mediaInfo'] == null + ? null + : SeerrMediaInfo.fromJson(json['mediaInfo'] as Map), + externalIds: json['externalIds'] == null + ? null + : SeerrExternalIds.fromJson( + json['externalIds'] as Map), + credits: json['credits'] == null + ? null + : SeerrCredits.fromJson(json['credits'] as Map), mediaId: _readJellyfinMediaId(json, 'mediaId') as String?, - contentRatings: (_readContentRatings(json, 'contentRatings') as List?) + contentRatings: (_readContentRatings(json, 'contentRatings') + as List?) ?.map((e) => SeerrContentRating.fromJson(e as Map)) .toList(), ); -Map _$SeerrMovieDetailsToJson(SeerrMovieDetails instance) => { +Map _$SeerrMovieDetailsToJson(SeerrMovieDetails instance) => + { 'id': instance.id, 'title': instance.title, 'originalTitle': instance.originalTitle, @@ -174,7 +208,8 @@ Map _$SeerrMovieDetailsToJson(SeerrMovieDetails instance) => json) => SeerrTvDetails( +SeerrTvDetails _$SeerrTvDetailsFromJson(Map json) => + SeerrTvDetails( id: (json['id'] as num?)?.toInt(), name: json['name'] as String?, originalName: json['originalName'] as String?, @@ -187,22 +222,34 @@ SeerrTvDetails _$SeerrTvDetailsFromJson(Map json) => SeerrTvDet voteCount: (json['voteCount'] as num?)?.toInt(), numberOfSeasons: (json['numberOfSeasons'] as num?)?.toInt(), numberOfEpisodes: (json['numberOfEpisodes'] as num?)?.toInt(), - genres: (json['genres'] as List?)?.map((e) => SeerrGenre.fromJson(e as Map)).toList(), - seasons: - (json['seasons'] as List?)?.map((e) => SeerrSeason.fromJson(e as Map)).toList(), - mediaInfo: json['mediaInfo'] == null ? null : SeerrMediaInfo.fromJson(json['mediaInfo'] as Map), - externalIds: - json['externalIds'] == null ? null : SeerrExternalIds.fromJson(json['externalIds'] as Map), - keywords: - (json['keywords'] as List?)?.map((e) => SeerrKeyword.fromJson(e as Map)).toList(), - credits: json['credits'] == null ? null : SeerrCredits.fromJson(json['credits'] as Map), + genres: (json['genres'] as List?) + ?.map((e) => SeerrGenre.fromJson(e as Map)) + .toList(), + seasons: (json['seasons'] as List?) + ?.map((e) => SeerrSeason.fromJson(e as Map)) + .toList(), + mediaInfo: json['mediaInfo'] == null + ? null + : SeerrMediaInfo.fromJson(json['mediaInfo'] as Map), + externalIds: json['externalIds'] == null + ? null + : SeerrExternalIds.fromJson( + json['externalIds'] as Map), + keywords: (json['keywords'] as List?) + ?.map((e) => SeerrKeyword.fromJson(e as Map)) + .toList(), + credits: json['credits'] == null + ? null + : SeerrCredits.fromJson(json['credits'] as Map), mediaId: _readJellyfinMediaId(json, 'mediaId') as String?, - contentRatings: (_readContentRatings(json, 'contentRatings') as List?) + contentRatings: (_readContentRatings(json, 'contentRatings') + as List?) ?.map((e) => SeerrContentRating.fromJson(e as Map)) .toList(), ); -Map _$SeerrTvDetailsToJson(SeerrTvDetails instance) => { +Map _$SeerrTvDetailsToJson(SeerrTvDetails instance) => + { 'id': instance.id, 'name': instance.name, 'originalName': instance.originalName, @@ -230,7 +277,8 @@ SeerrGenre _$SeerrGenreFromJson(Map json) => SeerrGenre( name: json['name'] as String?, ); -Map _$SeerrGenreToJson(SeerrGenre instance) => { +Map _$SeerrGenreToJson(SeerrGenre instance) => + { 'id': instance.id, 'name': instance.name, }; @@ -240,7 +288,8 @@ SeerrKeyword _$SeerrKeywordFromJson(Map json) => SeerrKeyword( name: json['name'] as String?, ); -Map _$SeerrKeywordToJson(SeerrKeyword instance) => { +Map _$SeerrKeywordToJson(SeerrKeyword instance) => + { 'id': instance.id, 'name': instance.name, }; @@ -255,7 +304,8 @@ SeerrSeason _$SeerrSeasonFromJson(Map json) => SeerrSeason( mediaId: _readJellyfinMediaId(json, 'mediaId') as String?, ); -Map _$SeerrSeasonToJson(SeerrSeason instance) => { +Map _$SeerrSeasonToJson(SeerrSeason instance) => + { 'id': instance.id, 'name': instance.name, 'overview': instance.overview, @@ -265,17 +315,20 @@ Map _$SeerrSeasonToJson(SeerrSeason instance) => json) => SeerrSeasonDetails( +SeerrSeasonDetails _$SeerrSeasonDetailsFromJson(Map json) => + SeerrSeasonDetails( id: (json['id'] as num?)?.toInt(), name: json['name'] as String?, overview: json['overview'] as String?, seasonNumber: (json['seasonNumber'] as num?)?.toInt(), internalPosterPath: json['posterPath'] as String?, - episodes: - (json['episodes'] as List?)?.map((e) => SeerrEpisode.fromJson(e as Map)).toList(), + episodes: (json['episodes'] as List?) + ?.map((e) => SeerrEpisode.fromJson(e as Map)) + .toList(), ); -Map _$SeerrSeasonDetailsToJson(SeerrSeasonDetails instance) => { +Map _$SeerrSeasonDetailsToJson(SeerrSeasonDetails instance) => + { 'id': instance.id, 'name': instance.name, 'overview': instance.overview, @@ -296,7 +349,8 @@ SeerrEpisode _$SeerrEpisodeFromJson(Map json) => SeerrEpisode( voteCount: (json['voteCount'] as num?)?.toInt(), ); -Map _$SeerrEpisodeToJson(SeerrEpisode instance) => { +Map _$SeerrEpisodeToJson(SeerrEpisode instance) => + { 'id': instance.id, 'name': instance.name, 'overview': instance.overview, @@ -308,7 +362,8 @@ Map _$SeerrEpisodeToJson(SeerrEpisode instance) => json) => +SeerrDownloadStatusEpisode _$SeerrDownloadStatusEpisodeFromJson( + Map json) => SeerrDownloadStatusEpisode( seriesId: (json['seriesId'] as num?)?.toInt(), tvdbId: (json['tvdbId'] as num?)?.toInt(), @@ -328,7 +383,9 @@ SeerrDownloadStatusEpisode _$SeerrDownloadStatusEpisodeFromJson(Map _$SeerrDownloadStatusEpisodeToJson(SeerrDownloadStatusEpisode instance) => { +Map _$SeerrDownloadStatusEpisodeToJson( + SeerrDownloadStatusEpisode instance) => + { 'seriesId': instance.seriesId, 'tvdbId': instance.tvdbId, 'episodeFileId': instance.episodeFileId, @@ -347,7 +404,8 @@ Map _$SeerrDownloadStatusEpisodeToJson(SeerrDownloadStatusEpiso 'id': instance.id, }; -SeerrDownloadStatus _$SeerrDownloadStatusFromJson(Map json) => SeerrDownloadStatus( +SeerrDownloadStatus _$SeerrDownloadStatusFromJson(Map json) => + SeerrDownloadStatus( externalId: (json['externalId'] as num?)?.toInt(), estimatedCompletionTime: json['estimatedCompletionTime'] as String?, mediaType: json['mediaType'] as String?, @@ -356,12 +414,16 @@ SeerrDownloadStatus _$SeerrDownloadStatusFromJson(Map json) => status: json['status'] as String?, timeLeft: json['timeLeft'] as String?, title: json['title'] as String?, - episode: - json['episode'] == null ? null : SeerrDownloadStatusEpisode.fromJson(json['episode'] as Map), + episode: json['episode'] == null + ? null + : SeerrDownloadStatusEpisode.fromJson( + json['episode'] as Map), downloadId: json['downloadId'] as String?, ); -Map _$SeerrDownloadStatusToJson(SeerrDownloadStatus instance) => { +Map _$SeerrDownloadStatusToJson( + SeerrDownloadStatus instance) => + { 'externalId': instance.externalId, 'estimatedCompletionTime': instance.estimatedCompletionTime, 'mediaType': instance.mediaType, @@ -374,7 +436,9 @@ Map _$SeerrDownloadStatusToJson(SeerrDownloadStatus instance) = 'downloadId': instance.downloadId, }; -SeerrMediaInfoSeason _$SeerrMediaInfoSeasonFromJson(Map json) => SeerrMediaInfoSeason( +SeerrMediaInfoSeason _$SeerrMediaInfoSeasonFromJson( + Map json) => + SeerrMediaInfoSeason( id: (json['id'] as num?)?.toInt(), seasonNumber: (json['seasonNumber'] as num?)?.toInt(), status: (json['status'] as num?)?.toInt(), @@ -382,7 +446,9 @@ SeerrMediaInfoSeason _$SeerrMediaInfoSeasonFromJson(Map json) = updatedAt: json['updatedAt'] as String?, ); -Map _$SeerrMediaInfoSeasonToJson(SeerrMediaInfoSeason instance) => { +Map _$SeerrMediaInfoSeasonToJson( + SeerrMediaInfoSeason instance) => + { 'id': instance.id, 'seasonNumber': instance.seasonNumber, 'status': instance.status, @@ -390,31 +456,42 @@ Map _$SeerrMediaInfoSeasonToJson(SeerrMediaInfoSeason instance) 'updatedAt': instance.updatedAt, }; -SeerrExternalIds _$SeerrExternalIdsFromJson(Map json) => SeerrExternalIds( +SeerrExternalIds _$SeerrExternalIdsFromJson(Map json) => + SeerrExternalIds( imdbId: json['imdbId'] as String?, facebookId: json['facebookId'] as String?, instagramId: json['instagramId'] as String?, twitterId: json['twitterId'] as String?, ); -Map _$SeerrExternalIdsToJson(SeerrExternalIds instance) => { +Map _$SeerrExternalIdsToJson(SeerrExternalIds instance) => + { 'imdbId': instance.imdbId, 'facebookId': instance.facebookId, 'instagramId': instance.instagramId, 'twitterId': instance.twitterId, }; -SeerrRatingsResponse _$SeerrRatingsResponseFromJson(Map json) => SeerrRatingsResponse( - rt: json['rt'] == null ? null : SeerrRtRating.fromJson(json['rt'] as Map), - imdb: json['imdb'] == null ? null : SeerrImdbRating.fromJson(json['imdb'] as Map), +SeerrRatingsResponse _$SeerrRatingsResponseFromJson( + Map json) => + SeerrRatingsResponse( + rt: json['rt'] == null + ? null + : SeerrRtRating.fromJson(json['rt'] as Map), + imdb: json['imdb'] == null + ? null + : SeerrImdbRating.fromJson(json['imdb'] as Map), ); -Map _$SeerrRatingsResponseToJson(SeerrRatingsResponse instance) => { +Map _$SeerrRatingsResponseToJson( + SeerrRatingsResponse instance) => + { 'rt': instance.rt, 'imdb': instance.imdb, }; -SeerrRtRating _$SeerrRtRatingFromJson(Map json) => SeerrRtRating( +SeerrRtRating _$SeerrRtRatingFromJson(Map json) => + SeerrRtRating( title: json['title'] as String?, year: (json['year'] as num?)?.toInt(), criticsScore: (json['criticsScore'] as num?)?.toInt(), @@ -424,7 +501,8 @@ SeerrRtRating _$SeerrRtRatingFromJson(Map json) => SeerrRtRatin url: json['url'] as String?, ); -Map _$SeerrRtRatingToJson(SeerrRtRating instance) => { +Map _$SeerrRtRatingToJson(SeerrRtRating instance) => + { 'title': instance.title, 'year': instance.year, 'criticsScore': instance.criticsScore, @@ -434,40 +512,58 @@ Map _$SeerrRtRatingToJson(SeerrRtRating instance) => json) => SeerrImdbRating( +SeerrImdbRating _$SeerrImdbRatingFromJson(Map json) => + SeerrImdbRating( title: json['title'] as String?, url: json['url'] as String?, criticsScore: (json['criticsScore'] as num?)?.toDouble(), ); -Map _$SeerrImdbRatingToJson(SeerrImdbRating instance) => { +Map _$SeerrImdbRatingToJson(SeerrImdbRating instance) => + { 'title': instance.title, 'url': instance.url, 'criticsScore': instance.criticsScore, }; -SeerrRequestsResponse _$SeerrRequestsResponseFromJson(Map json) => SeerrRequestsResponse( +SeerrRequestsResponse _$SeerrRequestsResponseFromJson( + Map json) => + SeerrRequestsResponse( results: (json['results'] as List?) ?.map((e) => SeerrMediaRequest.fromJson(e as Map)) .toList(), - pageInfo: json['pageInfo'] == null ? null : SeerrPageInfo.fromJson(json['pageInfo'] as Map), + pageInfo: json['pageInfo'] == null + ? null + : SeerrPageInfo.fromJson(json['pageInfo'] as Map), ); -Map _$SeerrRequestsResponseToJson(SeerrRequestsResponse instance) => { +Map _$SeerrRequestsResponseToJson( + SeerrRequestsResponse instance) => + { 'results': instance.results, 'pageInfo': instance.pageInfo, }; -SeerrMediaRequest _$SeerrMediaRequestFromJson(Map json) => SeerrMediaRequest( +SeerrMediaRequest _$SeerrMediaRequestFromJson(Map json) => + SeerrMediaRequest( id: (json['id'] as num?)?.toInt(), status: (json['status'] as num?)?.toInt(), - media: json['media'] == null ? null : SeerrMedia.fromJson(json['media'] as Map), - createdAt: json['createdAt'] == null ? null : DateTime.parse(json['createdAt'] as String), - updatedAt: json['updatedAt'] == null ? null : DateTime.parse(json['updatedAt'] as String), - requestedBy: - json['requestedBy'] == null ? null : SeerrUserModel.fromJson(json['requestedBy'] as Map), - modifiedBy: - json['modifiedBy'] == null ? null : SeerrUserModel.fromJson(json['modifiedBy'] as Map), + media: json['media'] == null + ? null + : SeerrMedia.fromJson(json['media'] as Map), + createdAt: json['createdAt'] == null + ? null + : DateTime.parse(json['createdAt'] as String), + updatedAt: json['updatedAt'] == null + ? null + : DateTime.parse(json['updatedAt'] as String), + requestedBy: json['requestedBy'] == null + ? null + : SeerrUserModel.fromJson( + json['requestedBy'] as Map), + modifiedBy: json['modifiedBy'] == null + ? null + : SeerrUserModel.fromJson(json['modifiedBy'] as Map), is4k: json['is4k'] as bool?, seasons: _parseRequestSeasons(json['seasons'] as List?), serverId: (json['serverId'] as num?)?.toInt(), @@ -475,7 +571,8 @@ SeerrMediaRequest _$SeerrMediaRequestFromJson(Map json) => Seer rootFolder: json['rootFolder'] as String?, ); -Map _$SeerrMediaRequestToJson(SeerrMediaRequest instance) => { +Map _$SeerrMediaRequestToJson(SeerrMediaRequest instance) => + { 'id': instance.id, 'status': instance.status, 'media': instance.media, @@ -490,33 +587,43 @@ Map _$SeerrMediaRequestToJson(SeerrMediaRequest instance) => json) => SeerrPageInfo( +SeerrPageInfo _$SeerrPageInfoFromJson(Map json) => + SeerrPageInfo( pages: (json['pages'] as num?)?.toInt(), pageSize: (json['pageSize'] as num?)?.toInt(), results: (json['results'] as num?)?.toInt(), page: (json['page'] as num?)?.toInt(), ); -Map _$SeerrPageInfoToJson(SeerrPageInfo instance) => { +Map _$SeerrPageInfoToJson(SeerrPageInfo instance) => + { 'pages': instance.pages, 'pageSize': instance.pageSize, 'results': instance.results, 'page': instance.page, }; -SeerrCreateRequestBody _$SeerrCreateRequestBodyFromJson(Map json) => SeerrCreateRequestBody( +SeerrCreateRequestBody _$SeerrCreateRequestBodyFromJson( + Map json) => + SeerrCreateRequestBody( mediaType: json['mediaType'] as String?, mediaId: (json['mediaId'] as num?)?.toInt(), is4k: json['is4k'] as bool?, - seasons: (json['seasons'] as List?)?.map((e) => (e as num).toInt()).toList(), + seasons: (json['seasons'] as List?) + ?.map((e) => (e as num).toInt()) + .toList(), serverId: (json['serverId'] as num?)?.toInt(), profileId: (json['profileId'] as num?)?.toInt(), rootFolder: json['rootFolder'] as String?, - tags: (json['tags'] as List?)?.map((e) => (e as num).toInt()).toList(), + tags: (json['tags'] as List?) + ?.map((e) => (e as num).toInt()) + .toList(), userId: (json['userId'] as num?)?.toInt(), ); -Map _$SeerrCreateRequestBodyToJson(SeerrCreateRequestBody instance) => { +Map _$SeerrCreateRequestBodyToJson( + SeerrCreateRequestBody instance) => + { 'mediaType': instance.mediaType, 'mediaId': instance.mediaId, if (instance.is4k case final value?) 'is4k': value, @@ -539,7 +646,8 @@ SeerrMedia _$SeerrMediaFromJson(Map json) => SeerrMedia( .toList(), ); -Map _$SeerrMediaToJson(SeerrMedia instance) => { +Map _$SeerrMediaToJson(SeerrMedia instance) => + { 'id': instance.id, 'tmdbId': instance.tmdbId, 'tvdbId': instance.tvdbId, @@ -548,19 +656,27 @@ Map _$SeerrMediaToJson(SeerrMedia instance) => json) => SeerrMediaResponse( - results: (json['results'] as List?)?.map((e) => SeerrMedia.fromJson(e as Map)).toList(), - pageInfo: json['pageInfo'] == null ? null : SeerrPageInfo.fromJson(json['pageInfo'] as Map), +SeerrMediaResponse _$SeerrMediaResponseFromJson(Map json) => + SeerrMediaResponse( + results: (json['results'] as List?) + ?.map((e) => SeerrMedia.fromJson(e as Map)) + .toList(), + pageInfo: json['pageInfo'] == null + ? null + : SeerrPageInfo.fromJson(json['pageInfo'] as Map), ); -Map _$SeerrMediaResponseToJson(SeerrMediaResponse instance) => { +Map _$SeerrMediaResponseToJson(SeerrMediaResponse instance) => + { 'results': instance.results, 'pageInfo': instance.pageInfo, }; -SeerrDiscoverItem _$SeerrDiscoverItemFromJson(Map json) => SeerrDiscoverItem( +SeerrDiscoverItem _$SeerrDiscoverItemFromJson(Map json) => + SeerrDiscoverItem( id: (json['id'] as num?)?.toInt(), - mediaType: $enumDecodeNullable(_$SeerrMediaTypeEnumMap, json['mediaType']), + mediaType: + $enumDecodeNullable(_$SeerrMediaTypeEnumMap, json['mediaType']), title: json['title'] as String?, name: json['name'] as String?, originalTitle: json['originalTitle'] as String?, @@ -570,11 +686,14 @@ SeerrDiscoverItem _$SeerrDiscoverItemFromJson(Map json) => Seer internalBackdropPath: json['backdropPath'] as String?, releaseDate: json['releaseDate'] as String?, firstAirDate: json['firstAirDate'] as String?, - mediaInfo: json['mediaInfo'] == null ? null : SeerrMediaInfo.fromJson(json['mediaInfo'] as Map), + mediaInfo: json['mediaInfo'] == null + ? null + : SeerrMediaInfo.fromJson(json['mediaInfo'] as Map), mediaId: _readJellyfinMediaId(json, 'mediaId') as String?, ); -Map _$SeerrDiscoverItemToJson(SeerrDiscoverItem instance) => { +Map _$SeerrDiscoverItemToJson(SeerrDiscoverItem instance) => + { 'id': instance.id, 'mediaType': _$SeerrMediaTypeEnumMap[instance.mediaType], 'title': instance.title, @@ -596,7 +715,9 @@ const _$SeerrMediaTypeEnumMap = { SeerrMediaType.person: 'person', }; -SeerrDiscoverResponse _$SeerrDiscoverResponseFromJson(Map json) => SeerrDiscoverResponse( +SeerrDiscoverResponse _$SeerrDiscoverResponseFromJson( + Map json) => + SeerrDiscoverResponse( results: (json['results'] as List?) ?.map((e) => SeerrDiscoverItem.fromJson(e as Map)) .toList(), @@ -605,82 +726,107 @@ SeerrDiscoverResponse _$SeerrDiscoverResponseFromJson(Map json) totalResults: (json['totalResults'] as num?)?.toInt(), ); -Map _$SeerrDiscoverResponseToJson(SeerrDiscoverResponse instance) => { +Map _$SeerrDiscoverResponseToJson( + SeerrDiscoverResponse instance) => + { 'results': instance.results, 'page': instance.page, 'totalPages': instance.totalPages, 'totalResults': instance.totalResults, }; -SeerrGenreResponse _$SeerrGenreResponseFromJson(Map json) => SeerrGenreResponse( - genres: (json['genres'] as List?)?.map((e) => SeerrGenre.fromJson(e as Map)).toList(), +SeerrGenreResponse _$SeerrGenreResponseFromJson(Map json) => + SeerrGenreResponse( + genres: (json['genres'] as List?) + ?.map((e) => SeerrGenre.fromJson(e as Map)) + .toList(), ); -Map _$SeerrGenreResponseToJson(SeerrGenreResponse instance) => { +Map _$SeerrGenreResponseToJson(SeerrGenreResponse instance) => + { 'genres': instance.genres, }; -SeerrWatchProvider _$SeerrWatchProviderFromJson(Map json) => SeerrWatchProvider( +SeerrWatchProvider _$SeerrWatchProviderFromJson(Map json) => + SeerrWatchProvider( providerId: (json['id'] as num?)?.toInt(), providerName: json['name'] as String?, internalLogoPath: json['logoPath'] as String?, displayPriority: (json['displayPriority'] as num?)?.toInt(), ); -Map _$SeerrWatchProviderToJson(SeerrWatchProvider instance) => { +Map _$SeerrWatchProviderToJson(SeerrWatchProvider instance) => + { 'id': instance.providerId, 'name': instance.providerName, 'logoPath': instance.internalLogoPath, 'displayPriority': instance.displayPriority, }; -SeerrWatchProviderRegion _$SeerrWatchProviderRegionFromJson(Map json) => SeerrWatchProviderRegion( +SeerrWatchProviderRegion _$SeerrWatchProviderRegionFromJson( + Map json) => + SeerrWatchProviderRegion( iso31661: json['iso_3166_1'] as String?, englishName: json['english_name'] as String?, nativeName: json['native_name'] as String?, ); -Map _$SeerrWatchProviderRegionToJson(SeerrWatchProviderRegion instance) => { +Map _$SeerrWatchProviderRegionToJson( + SeerrWatchProviderRegion instance) => + { 'iso_3166_1': instance.iso31661, 'english_name': instance.englishName, 'native_name': instance.nativeName, }; -SeerrCertification _$SeerrCertificationFromJson(Map json) => SeerrCertification( +SeerrCertification _$SeerrCertificationFromJson(Map json) => + SeerrCertification( certification: json['certification'] as String?, meaning: json['meaning'] as String?, order: (json['order'] as num?)?.toInt(), ); -Map _$SeerrCertificationToJson(SeerrCertification instance) => { +Map _$SeerrCertificationToJson(SeerrCertification instance) => + { 'certification': instance.certification, 'meaning': instance.meaning, 'order': instance.order, }; -SeerrCertificationsResponse _$SeerrCertificationsResponseFromJson(Map json) => +SeerrCertificationsResponse _$SeerrCertificationsResponseFromJson( + Map json) => SeerrCertificationsResponse( certifications: (json['certifications'] as Map?)?.map( (k, e) => MapEntry( - k, (e as List).map((e) => SeerrCertification.fromJson(e as Map)).toList()), + k, + (e as List) + .map((e) => + SeerrCertification.fromJson(e as Map)) + .toList()), ), ); -Map _$SeerrCertificationsResponseToJson(SeerrCertificationsResponse instance) => { +Map _$SeerrCertificationsResponseToJson( + SeerrCertificationsResponse instance) => + { 'certifications': instance.certifications, }; -SeerrAuthLocalBody _$SeerrAuthLocalBodyFromJson(Map json) => SeerrAuthLocalBody( +SeerrAuthLocalBody _$SeerrAuthLocalBodyFromJson(Map json) => + SeerrAuthLocalBody( email: json['email'] as String, password: json['password'] as String, ); -Map _$SeerrAuthLocalBodyToJson(SeerrAuthLocalBody instance) => { +Map _$SeerrAuthLocalBodyToJson(SeerrAuthLocalBody instance) => + { 'email': instance.email, 'password': instance.password, }; -SeerrAuthJellyfinBody _$SeerrAuthJellyfinBodyFromJson(Map json) => SeerrAuthJellyfinBody( +SeerrAuthJellyfinBody _$SeerrAuthJellyfinBodyFromJson( + Map json) => + SeerrAuthJellyfinBody( username: json['username'] as String, password: json['password'] as String, customHeaders: (json['customHeaders'] as Map?)?.map( @@ -689,14 +835,17 @@ SeerrAuthJellyfinBody _$SeerrAuthJellyfinBodyFromJson(Map json) hostname: json['hostname'] as String?, ); -Map _$SeerrAuthJellyfinBodyToJson(SeerrAuthJellyfinBody instance) => { +Map _$SeerrAuthJellyfinBodyToJson( + SeerrAuthJellyfinBody instance) => + { 'username': instance.username, 'password': instance.password, if (instance.customHeaders case final value?) 'customHeaders': value, if (instance.hostname case final value?) 'hostname': value, }; -_SeerrUserModel _$SeerrUserModelFromJson(Map json) => _SeerrUserModel( +_SeerrUserModel _$SeerrUserModelFromJson(Map json) => + _SeerrUserModel( id: (json['id'] as num?)?.toInt(), email: json['email'] as String?, username: json['username'] as String?, @@ -705,14 +854,18 @@ _SeerrUserModel _$SeerrUserModelFromJson(Map json) => _SeerrUse plexUsername: json['plexUsername'] as String?, permissions: (json['permissions'] as num?)?.toInt(), avatar: json['avatar'] as String?, - settings: json['settings'] == null ? null : SeerrUserSettings.fromJson(json['settings'] as Map), + settings: json['settings'] == null + ? null + : SeerrUserSettings.fromJson( + json['settings'] as Map), movieQuotaLimit: (json['movieQuotaLimit'] as num?)?.toInt(), movieQuotaDays: (json['movieQuotaDays'] as num?)?.toInt(), tvQuotaLimit: (json['tvQuotaLimit'] as num?)?.toInt(), tvQuotaDays: (json['tvQuotaDays'] as num?)?.toInt(), ); -Map _$SeerrUserModelToJson(_SeerrUserModel instance) => { +Map _$SeerrUserModelToJson(_SeerrUserModel instance) => + { 'id': instance.id, 'email': instance.email, 'username': instance.username, @@ -728,7 +881,8 @@ Map _$SeerrUserModelToJson(_SeerrUserModel instance) => json) => _SeerrSonarrServer( +_SeerrSonarrServer _$SeerrSonarrServerFromJson(Map json) => + _SeerrSonarrServer( id: (json['id'] as num?)?.toInt(), name: json['name'] as String?, hostname: json['hostname'] as String?, @@ -738,10 +892,12 @@ _SeerrSonarrServer _$SeerrSonarrServerFromJson(Map json) => _Se baseUrl: json['baseUrl'] as String?, activeProfileId: (json['activeProfileId'] as num?)?.toInt(), activeProfileName: json['activeProfileName'] as String?, - activeLanguageProfileId: (json['activeLanguageProfileId'] as num?)?.toInt(), + activeLanguageProfileId: + (json['activeLanguageProfileId'] as num?)?.toInt(), activeDirectory: json['activeDirectory'] as String?, activeAnimeProfileId: (json['activeAnimeProfileId'] as num?)?.toInt(), - activeAnimeLanguageProfileId: (json['activeAnimeLanguageProfileId'] as num?)?.toInt(), + activeAnimeLanguageProfileId: + (json['activeAnimeLanguageProfileId'] as num?)?.toInt(), activeAnimeProfileName: json['activeAnimeProfileName'] as String?, activeAnimeDirectory: json['activeAnimeDirectory'] as String?, is4k: json['is4k'] as bool?, @@ -752,14 +908,19 @@ _SeerrSonarrServer _$SeerrSonarrServerFromJson(Map json) => _Se profiles: (json['profiles'] as List?) ?.map((e) => SeerrServiceProfile.fromJson(e as Map)) .toList(), - tags: (json['tags'] as List?)?.map((e) => SeerrServiceTag.fromJson(e as Map)).toList(), + tags: (json['tags'] as List?) + ?.map((e) => SeerrServiceTag.fromJson(e as Map)) + .toList(), rootFolders: (json['rootFolders'] as List?) ?.map((e) => SeerrRootFolder.fromJson(e as Map)) .toList(), - activeTags: (json['activeTags'] as List?)?.map((e) => (e as num).toInt()).toList(), + activeTags: (json['activeTags'] as List?) + ?.map((e) => (e as num).toInt()) + .toList(), ); -Map _$SeerrSonarrServerToJson(_SeerrSonarrServer instance) => { +Map _$SeerrSonarrServerToJson(_SeerrSonarrServer instance) => + { 'id': instance.id, 'name': instance.name, 'hostname': instance.hostname, @@ -786,25 +947,34 @@ Map _$SeerrSonarrServerToJson(_SeerrSonarrServer instance) => < 'activeTags': instance.activeTags, }; -_SeerrSonarrServerResponse _$SeerrSonarrServerResponseFromJson(Map json) => _SeerrSonarrServerResponse( - server: json['server'] == null ? null : SeerrSonarrServer.fromJson(json['server'] as Map), +_SeerrSonarrServerResponse _$SeerrSonarrServerResponseFromJson( + Map json) => + _SeerrSonarrServerResponse( + server: json['server'] == null + ? null + : SeerrSonarrServer.fromJson(json['server'] as Map), profiles: (json['profiles'] as List?) ?.map((e) => SeerrServiceProfile.fromJson(e as Map)) .toList(), rootFolders: (json['rootFolders'] as List?) ?.map((e) => SeerrRootFolder.fromJson(e as Map)) .toList(), - tags: (json['tags'] as List?)?.map((e) => SeerrServiceTag.fromJson(e as Map)).toList(), + tags: (json['tags'] as List?) + ?.map((e) => SeerrServiceTag.fromJson(e as Map)) + .toList(), ); -Map _$SeerrSonarrServerResponseToJson(_SeerrSonarrServerResponse instance) => { +Map _$SeerrSonarrServerResponseToJson( + _SeerrSonarrServerResponse instance) => + { 'server': instance.server, 'profiles': instance.profiles, 'rootFolders': instance.rootFolders, 'tags': instance.tags, }; -_SeerrRadarrServer _$SeerrRadarrServerFromJson(Map json) => _SeerrRadarrServer( +_SeerrRadarrServer _$SeerrRadarrServerFromJson(Map json) => + _SeerrRadarrServer( id: (json['id'] as num?)?.toInt(), name: json['name'] as String?, hostname: json['hostname'] as String?, @@ -814,10 +984,12 @@ _SeerrRadarrServer _$SeerrRadarrServerFromJson(Map json) => _Se baseUrl: json['baseUrl'] as String?, activeProfileId: (json['activeProfileId'] as num?)?.toInt(), activeProfileName: json['activeProfileName'] as String?, - activeLanguageProfileId: (json['activeLanguageProfileId'] as num?)?.toInt(), + activeLanguageProfileId: + (json['activeLanguageProfileId'] as num?)?.toInt(), activeDirectory: json['activeDirectory'] as String?, activeAnimeProfileId: (json['activeAnimeProfileId'] as num?)?.toInt(), - activeAnimeLanguageProfileId: (json['activeAnimeLanguageProfileId'] as num?)?.toInt(), + activeAnimeLanguageProfileId: + (json['activeAnimeLanguageProfileId'] as num?)?.toInt(), activeAnimeProfileName: json['activeAnimeProfileName'] as String?, activeAnimeDirectory: json['activeAnimeDirectory'] as String?, is4k: json['is4k'] as bool?, @@ -828,14 +1000,19 @@ _SeerrRadarrServer _$SeerrRadarrServerFromJson(Map json) => _Se profiles: (json['profiles'] as List?) ?.map((e) => SeerrServiceProfile.fromJson(e as Map)) .toList(), - tags: (json['tags'] as List?)?.map((e) => SeerrServiceTag.fromJson(e as Map)).toList(), + tags: (json['tags'] as List?) + ?.map((e) => SeerrServiceTag.fromJson(e as Map)) + .toList(), rootFolders: (json['rootFolders'] as List?) ?.map((e) => SeerrRootFolder.fromJson(e as Map)) .toList(), - activeTags: (json['activeTags'] as List?)?.map((e) => (e as num).toInt()).toList(), + activeTags: (json['activeTags'] as List?) + ?.map((e) => (e as num).toInt()) + .toList(), ); -Map _$SeerrRadarrServerToJson(_SeerrRadarrServer instance) => { +Map _$SeerrRadarrServerToJson(_SeerrRadarrServer instance) => + { 'id': instance.id, 'name': instance.name, 'hostname': instance.hostname, @@ -862,57 +1039,73 @@ Map _$SeerrRadarrServerToJson(_SeerrRadarrServer instance) => < 'activeTags': instance.activeTags, }; -_SeerrRadarrServerResponse _$SeerrRadarrServerResponseFromJson(Map json) => _SeerrRadarrServerResponse( - server: json['server'] == null ? null : SeerrRadarrServer.fromJson(json['server'] as Map), +_SeerrRadarrServerResponse _$SeerrRadarrServerResponseFromJson( + Map json) => + _SeerrRadarrServerResponse( + server: json['server'] == null + ? null + : SeerrRadarrServer.fromJson(json['server'] as Map), profiles: (json['profiles'] as List?) ?.map((e) => SeerrServiceProfile.fromJson(e as Map)) .toList(), rootFolders: (json['rootFolders'] as List?) ?.map((e) => SeerrRootFolder.fromJson(e as Map)) .toList(), - tags: (json['tags'] as List?)?.map((e) => SeerrServiceTag.fromJson(e as Map)).toList(), + tags: (json['tags'] as List?) + ?.map((e) => SeerrServiceTag.fromJson(e as Map)) + .toList(), ); -Map _$SeerrRadarrServerResponseToJson(_SeerrRadarrServerResponse instance) => { +Map _$SeerrRadarrServerResponseToJson( + _SeerrRadarrServerResponse instance) => + { 'server': instance.server, 'profiles': instance.profiles, 'rootFolders': instance.rootFolders, 'tags': instance.tags, }; -_SeerrServiceProfile _$SeerrServiceProfileFromJson(Map json) => _SeerrServiceProfile( +_SeerrServiceProfile _$SeerrServiceProfileFromJson(Map json) => + _SeerrServiceProfile( id: (json['id'] as num?)?.toInt(), name: json['name'] as String?, ); -Map _$SeerrServiceProfileToJson(_SeerrServiceProfile instance) => { +Map _$SeerrServiceProfileToJson( + _SeerrServiceProfile instance) => + { 'id': instance.id, 'name': instance.name, }; -_SeerrServiceTag _$SeerrServiceTagFromJson(Map json) => _SeerrServiceTag( +_SeerrServiceTag _$SeerrServiceTagFromJson(Map json) => + _SeerrServiceTag( id: (json['id'] as num?)?.toInt(), label: json['label'] as String?, ); -Map _$SeerrServiceTagToJson(_SeerrServiceTag instance) => { +Map _$SeerrServiceTagToJson(_SeerrServiceTag instance) => + { 'id': instance.id, 'label': instance.label, }; -_SeerrRootFolder _$SeerrRootFolderFromJson(Map json) => _SeerrRootFolder( +_SeerrRootFolder _$SeerrRootFolderFromJson(Map json) => + _SeerrRootFolder( id: (json['id'] as num?)?.toInt(), freeSpace: (json['freeSpace'] as num?)?.toInt(), path: json['path'] as String?, ); -Map _$SeerrRootFolderToJson(_SeerrRootFolder instance) => { +Map _$SeerrRootFolderToJson(_SeerrRootFolder instance) => + { 'id': instance.id, 'freeSpace': instance.freeSpace, 'path': instance.path, }; -_SeerrMediaInfo _$SeerrMediaInfoFromJson(Map json) => _SeerrMediaInfo( +_SeerrMediaInfo _$SeerrMediaInfoFromJson(Map json) => + _SeerrMediaInfo( id: (json['id'] as num?)?.toInt(), tmdbId: (json['tmdbId'] as num?)?.toInt(), tvdbId: (json['tvdbId'] as num?)?.toInt(), @@ -934,7 +1127,8 @@ _SeerrMediaInfo _$SeerrMediaInfoFromJson(Map json) => _SeerrMed .toList(), ); -Map _$SeerrMediaInfoToJson(_SeerrMediaInfo instance) => { +Map _$SeerrMediaInfoToJson(_SeerrMediaInfo instance) => + { 'id': instance.id, 'tmdbId': instance.tmdbId, 'tvdbId': instance.tvdbId, diff --git a/lib/util/external_player_helper.dart b/lib/util/external_player_helper.dart new file mode 100644 index 000000000..14ba1cde1 --- /dev/null +++ b/lib/util/external_player_helper.dart @@ -0,0 +1,65 @@ +import 'package:flutter_riverpod/flutter_riverpod.dart'; +import 'package:url_launcher/url_launcher.dart'; +import 'dart:io'; + +import 'package:fladder/models/item_base_model.dart'; +import 'package:fladder/providers/user_provider.dart'; + +class ExternalPlayerHelper { + static const String energyProtocol = 'energyplayer:launch=media'; + static const String energyStoreUrl = 'ms-windows-store://pdp/?productid=9P9ZH5FL1BFK'; + + static bool canShowEnergyPlayer(ItemBaseModel? item) { + if (!Platform.isWindows) return false; + if (item == null) return false; + return item.streamModel?.hasDolbyVision ?? false; + } + + static Future isEnergyPlayerInstalled() async { + if (!Platform.isWindows) return false; + final uri = Uri.parse('energyplayer:'); + return await canLaunchUrl(uri); + } + + static Future openStore() async { + final uri = Uri.parse(energyStoreUrl); + if (await canLaunchUrl(uri)) { + await launchUrl(uri); + } else { + // Fallback to web link if store protocol fails + await launchUrl( + Uri.parse('https://apps.microsoft.com/detail/9P9ZH5FL1BFK')); + } + } + + static Future launchEnergyPlayer(WidgetRef ref, ItemBaseModel item) async { + final user = ref.read(userProvider); + if (user == null) return; + + final serverUrl = user.credentials.url; + final apiKey = user.credentials.token; + + if (serverUrl.isEmpty || apiKey.isEmpty) return; + + final videoId = item.id; + final streamUrl = "$serverUrl/Videos/$videoId/stream?static=true&api_key=$apiKey"; + + // Subtitles + final currentSub = item.streamModel?.currentSubStream; + final subsPath = (currentSub != null && currentSub.url != null) ? currentSub.url! : ""; + + // Start position (Energy Player uses seconds) + final startTime = (item.userData.playbackPositionTicks) ~/ 10000000; + + final energyUrl = Uri.encodeFull( + 'energyplayer:launch=media&path="$streamUrl"&subsPath="$subsPath"&startTime=$startTime', + ); + + final uri = Uri.parse(energyUrl); + if (await canLaunchUrl(uri)) { + await launchUrl(uri); + } else { + await openStore(); + } + } +} diff --git a/lib/util/item_base_model/play_item_helpers.dart b/lib/util/item_base_model/play_item_helpers.dart index 63989110e..2b325f2f4 100644 --- a/lib/util/item_base_model/play_item_helpers.dart +++ b/lib/util/item_base_model/play_item_helpers.dart @@ -31,7 +31,11 @@ import 'package:fladder/util/fladder_image.dart'; import 'package:fladder/util/list_extensions.dart'; import 'package:fladder/util/localization_helper.dart'; import 'package:fladder/util/refresh_state.dart'; +import 'package:fladder/util/external_player_helper.dart'; import 'package:fladder/widgets/full_screen_helpers/full_screen_wrapper.dart'; +import 'package:fladder/widgets/shared/dv_player_selection_dialog.dart'; +import 'package:fladder/providers/settings/video_player_settings_provider.dart'; +import 'package:fladder/models/settings/video_player_settings.dart'; extension BookBaseModelExtension on BookModel? { Future play( @@ -198,6 +202,27 @@ extension ItemBaseModelExtensions on ItemBaseModel? { }) async { if (itemModel == null) return; + // DV selection on Windows logic + if (ExternalPlayerHelper.canShowEnergyPlayer(itemModel)) { + final settings = ref.read(videoPlayerSettingsProvider); + DVPlayerChoice choice = settings.dvPlayerChoice; + + if (choice == DVPlayerChoice.ask) { + final result = await showDialog( + context: context, + builder: (context) => const DVPlayerSelectionDialog(), + ); + + if (result == null) return; + choice = result; + } + + if (choice == DVPlayerChoice.energyPlayer) { + await ExternalPlayerHelper.launchEnergyPlayer(ref, itemModel); + return; + } + } + await ref.read(videoPlayerProvider.notifier).init(); final op = CancelableOperation.fromFuture(ref.read(playbackModelHelper).createPlaybackModel( diff --git a/lib/widgets/shared/dv_player_selection_dialog.dart b/lib/widgets/shared/dv_player_selection_dialog.dart new file mode 100644 index 000000000..0ccad5d33 --- /dev/null +++ b/lib/widgets/shared/dv_player_selection_dialog.dart @@ -0,0 +1,120 @@ +import 'package:flutter/material.dart'; +import 'package:flutter_riverpod/flutter_riverpod.dart'; + +import 'package:fladder/models/settings/video_player_settings.dart'; +import 'package:fladder/providers/settings/video_player_settings_provider.dart'; +import 'package:fladder/util/external_player_helper.dart'; +import 'package:fladder/util/localization_helper.dart'; +import 'package:fladder/screens/settings/widgets/settings_message_box.dart'; + +class DVPlayerSelectionDialog extends ConsumerStatefulWidget { + const DVPlayerSelectionDialog({super.key}); + + @override + ConsumerState createState() => _DVPlayerSelectionDialogState(); +} + +class _DVPlayerSelectionDialogState extends ConsumerState { + bool _remember = false; + bool? _isInstalled; + + @override + void initState() { + super.initState(); + _checkInstallation(); + } + + Future _checkInstallation() async { + final installed = await ExternalPlayerHelper.isEnergyPlayerInstalled(); + if (mounted) { + setState(() => _isInstalled = installed); + } + } + + @override + Widget build(BuildContext context) { + return AlertDialog( + title: Text(context.localized.dvPlayerDialogTitle), + content: SingleChildScrollView( + child: Column( + mainAxisSize: MainAxisSize.min, + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text(context.localized.dvPlayerDialogDesc), + const SizedBox(height: 12), + Text( + context.localized.dvEnableInstruction, + style: Theme.of(context).textTheme.bodySmall?.copyWith( + color: Theme.of(context).colorScheme.primary, + ), + ), + const SizedBox(height: 12), + SettingsMessageBox( + context.localized.dvSyncWarning, + messageType: MessageType.warning, + ), + if (_isInstalled == false) ...[ + const SizedBox(height: 16), + Container( + padding: const EdgeInsets.all(12), + width: double.infinity, + decoration: BoxDecoration( + color: Theme.of(context).colorScheme.errorContainer, + borderRadius: BorderRadius.circular(8), + ), + child: Column( + children: [ + Text( + context.localized.dvPlayerNotInstalled, + textAlign: TextAlign.center, + style: TextStyle(color: Theme.of(context).colorScheme.onErrorContainer), + ), + const SizedBox(height: 8), + TextButton.icon( + onPressed: () => ExternalPlayerHelper.openStore(), + icon: const Icon(Icons.download), + label: Text(context.localized.dvPlayerGetFromStore), + style: TextButton.styleFrom(foregroundColor: Theme.of(context).colorScheme.error), + ), + ], + ), + ), + ], + const SizedBox(height: 16), + CheckboxListTile( + value: _remember, + onChanged: (val) => setState(() => _remember = val ?? false), + title: Text(context.localized.dvRememberChoice), + controlAffinity: ListTileControlAffinity.leading, + contentPadding: EdgeInsets.zero, + ), + ], + ), + ), + actions: [ + TextButton( + onPressed: () => Navigator.of(context).pop(), + child: Text(context.localized.cancel), + ), + TextButton( + onPressed: () { + if (_remember) { + ref.read(videoPlayerSettingsProvider.notifier).setDVPlayerChoice(DVPlayerChoice.internalPlayer); + } + Navigator.of(context).pop(DVPlayerChoice.internalPlayer); + }, + child: Text(context.localized.dvPlayerDisabled), + ), + FilledButton( + onPressed: () { + if (_remember) { + ref.read(videoPlayerSettingsProvider.notifier).setDVPlayerChoice(DVPlayerChoice.energyPlayer); + } + Navigator.of(context).pop(DVPlayerChoice.energyPlayer); + }, + child: Text(context.localized.dvPlayerEnergyPlayer), + ), + ], + ); + } +} diff --git a/pubspec.lock b/pubspec.lock index 7385c79a7..0c95ae8d6 100644 --- a/pubspec.lock +++ b/pubspec.lock @@ -245,10 +245,10 @@ packages: dependency: transitive description: name: characters - sha256: f71061c654a3380576a52b451dd5532377954cf9dbd272a78fc8479606670803 + sha256: faf38497bda5ead2a8c7615f4f7939df04333478bf32e4173fcb06d428b5716b url: "https://pub.dev" source: hosted - version: "1.4.0" + version: "1.4.1" charcode: dependency: transitive description: @@ -1257,18 +1257,18 @@ packages: dependency: transitive description: name: matcher - sha256: dc58c723c3c24bf8d3e2d3ad3f2f9d7bd9cf43ec6feaa64181775e60190153f2 + sha256: dc0b7dc7651697ea4ff3e69ef44b0407ea32c487a39fff6a4004fa585e901861 url: "https://pub.dev" source: hosted - version: "0.12.17" + version: "0.12.19" material_color_utilities: dependency: transitive description: name: material_color_utilities - sha256: f7142bb1154231d7ea5f96bc7bde4bda2a0945d2806bb11670e30b850d56bdec + sha256: "9c337007e82b1889149c82ed242ed1cb24a66044e30979c44912381e9be4c48b" url: "https://pub.dev" source: hosted - version: "0.11.1" + version: "0.13.0" media_kit: dependency: "direct main" description: @@ -1345,10 +1345,10 @@ packages: dependency: transitive description: name: meta - sha256: e3641ec5d63ebf0d9b41bd43201a66e3fc79a65db5f61fc181f04cd27aab950c + sha256: "23f08335362185a5ea2ad3a4e597f1375e78bce8a040df5c600c8d3552ef2394" url: "https://pub.dev" source: hosted - version: "1.16.0" + version: "1.17.0" mime: dependency: transitive description: @@ -2142,10 +2142,10 @@ packages: dependency: transitive description: name: test_api - sha256: "522f00f556e73044315fa4585ec3270f1808a4b186c936e612cab0b565ff1e00" + sha256: "8161c84903fd860b26bfdefb7963b3f0b68fee7adea0f59ef805ecca346f0c7a" url: "https://pub.dev" source: hosted - version: "0.7.6" + version: "0.7.10" timezone: dependency: transitive description: From 62aa600ff0c86ec401061f323662a0ba766c0376 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?N=CE=9EV=CE=9BR?= Date: Mon, 30 Mar 2026 01:13:43 +0200 Subject: [PATCH 2/3] fix(windows): Resolve Energy Player launch failures on remote devices This commit hardens the external player integration by resolving critical URL formatting and encoding issues that caused playback failures on remote devices (laptops). - Normalized server URLs: stripped trailing slashes to prevent invalid double-slash paths (e.g., //Videos). - Enhanced URI encoding: switched to Uri.encodeComponent to accurately handle all special characters and server address formats. - Removed diagnostic tools: stripped debug prints and clipboard functionality for the final build. --- lib/util/external_player_helper.dart | 27 +++++++++++++++++++-------- 1 file changed, 19 insertions(+), 8 deletions(-) diff --git a/lib/util/external_player_helper.dart b/lib/util/external_player_helper.dart index 14ba1cde1..3904808fc 100644 --- a/lib/util/external_player_helper.dart +++ b/lib/util/external_player_helper.dart @@ -7,7 +7,8 @@ import 'package:fladder/providers/user_provider.dart'; class ExternalPlayerHelper { static const String energyProtocol = 'energyplayer:launch=media'; - static const String energyStoreUrl = 'ms-windows-store://pdp/?productid=9P9ZH5FL1BFK'; + static const String energyStoreUrl = + 'ms-windows-store://pdp/?productid=9P9ZH5FL1BFK'; static bool canShowEnergyPlayer(ItemBaseModel? item) { if (!Platform.isWindows) return false; @@ -32,28 +33,38 @@ class ExternalPlayerHelper { } } - static Future launchEnergyPlayer(WidgetRef ref, ItemBaseModel item) async { + static Future launchEnergyPlayer( + WidgetRef ref, ItemBaseModel item) async { final user = ref.read(userProvider); if (user == null) return; - final serverUrl = user.credentials.url; + var serverUrl = user.credentials.url; final apiKey = user.credentials.token; if (serverUrl.isEmpty || apiKey.isEmpty) return; + // Normalize serverUrl to avoid double slashes + if (serverUrl.endsWith('/')) { + serverUrl = serverUrl.substring(0, serverUrl.length - 1); + } + final videoId = item.id; - final streamUrl = "$serverUrl/Videos/$videoId/stream?static=true&api_key=$apiKey"; + final streamUrl = + "$serverUrl/Videos/$videoId/stream?static=true&api_key=$apiKey"; // Subtitles final currentSub = item.streamModel?.currentSubStream; - final subsPath = (currentSub != null && currentSub.url != null) ? currentSub.url! : ""; + final subsPath = + (currentSub != null && currentSub.url != null) ? currentSub.url! : ""; // Start position (Energy Player uses seconds) final startTime = (item.userData.playbackPositionTicks) ~/ 10000000; - final energyUrl = Uri.encodeFull( - 'energyplayer:launch=media&path="$streamUrl"&subsPath="$subsPath"&startTime=$startTime', - ); + // Use proper encoding for individual parameters (no manual quotes needed) + final energyUrl = 'energyplayer:launch=media' + '&path=${Uri.encodeComponent(streamUrl)}' + '&subsPath=${Uri.encodeComponent(subsPath)}' + '&startTime=$startTime'; final uri = Uri.parse(energyUrl); if (await canLaunchUrl(uri)) { From 9c890e411c2bc2fafa61b696fa732010b2c670e6 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?N=CE=9EV=CE=9BR?= <63956787+NEVARLeVrai@users.noreply.github.com> Date: Mon, 30 Mar 2026 08:46:36 +0200 Subject: [PATCH 3/3] fix string in video_player_settings.dart Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> --- lib/models/settings/video_player_settings.dart | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/lib/models/settings/video_player_settings.dart b/lib/models/settings/video_player_settings.dart index abec49fc9..cb8c213b8 100644 --- a/lib/models/settings/video_player_settings.dart +++ b/lib/models/settings/video_player_settings.dart @@ -256,9 +256,9 @@ enum DVPlayerChoice { String label(BuildContext context) { return switch (this) { - DVPlayerChoice.internalPlayer => "Disabled (Internal player)", - DVPlayerChoice.ask => "Always ask", - DVPlayerChoice.energyPlayer => "Energy Player (External)", + DVPlayerChoice.internalPlayer => context.localized.dvPlayerDisabled, + DVPlayerChoice.ask => context.localized.dvPlayerAsk, + DVPlayerChoice.energyPlayer => context.localized.dvPlayerEnergyPlayer, }; } }