diff --git a/lib/screens/settings/playback_settings_screen.dart b/lib/screens/settings/playback_settings_screen.dart index d32bbd30..cf3049c9 100644 --- a/lib/screens/settings/playback_settings_screen.dart +++ b/lib/screens/settings/playback_settings_screen.dart @@ -51,9 +51,9 @@ class _PlaybackSettingsScreenState extends State { children: [ SettingsSectionHeader(t.settings.player), if (Platform.isAndroid) _playerBackendSelector(), - _externalPlayerTile(), + if (PlatformDetector.supportsExternalPlayers()) _externalPlayerTile(), _hardwareDecodingTile(), - if ((Platform.isAndroid && !PlatformDetector.isTV()) || Platform.isIOS || Platform.isMacOS) _autoPipTile(), + if (PlatformDetector.supportsPictureInPicture()) _autoPipTile(), if (Platform.isAndroid) _matchContentFrameRateTile(), if (Platform.isWindows) _matchRefreshRateTile(), if (Platform.isWindows) _matchDynamicRangeTile(), diff --git a/lib/services/external_player_service.dart b/lib/services/external_player_service.dart index 805b647c..963bb10e 100644 --- a/lib/services/external_player_service.dart +++ b/lib/services/external_player_service.dart @@ -10,6 +10,7 @@ import '../media/media_server_client.dart'; import '../media/watch_progress.dart'; import '../models/external_player_models.dart'; import '../utils/app_logger.dart'; +import '../utils/platform_detector.dart'; import '../utils/snackbar_helper.dart'; import '../utils/watch_state_notifier.dart'; import '../i18n/strings.g.dart'; @@ -68,6 +69,8 @@ class ExternalPlayerService { String? mediaSourceId, String? videoUrl, }) async { + if (!PlatformDetector.supportsExternalPlayers()) return false; + try { String resolvedUrl; diff --git a/lib/services/settings_service.dart b/lib/services/settings_service.dart index d939f9a2..b547e154 100644 --- a/lib/services/settings_service.dart +++ b/lib/services/settings_service.dart @@ -156,6 +156,19 @@ class _AutoPipPref extends Pref { Future writeTo(BaseSharedPreferencesService svc, bool value) => svc.writeBool(key, value); } +class _UseExternalPlayerPref extends Pref { + const _UseExternalPlayerPref() : super('use_external_player'); + + @override + bool readFrom(BaseSharedPreferencesService svc) { + if (!PlatformDetector.supportsExternalPlayers()) return false; + return svc.prefs.getBool(key) ?? false; + } + + @override + Future writeTo(BaseSharedPreferencesService svc, bool value) => svc.writeBool(key, value); +} + String? _trimEmptyAsNull(String? v) { final t = v?.trim(); return (t == null || t.isEmpty) ? null : t; @@ -372,7 +385,7 @@ class SettingsService extends BaseSharedPreferencesService { static const showNavBarLabels = BoolPref('show_nav_bar_labels', defaultValue: true); static const globalShaderPreset = StringPref('global_shader_preset', defaultValue: 'none'); static const requireProfileSelectionOnOpen = BoolPref('require_profile_selection_on_open'); - static const useExternalPlayer = BoolPref('use_external_player'); + static const useExternalPlayer = _UseExternalPlayerPref(); static const forceTvMode = BoolPref('force_tv_mode'); static const visualEffects = EnumPref( 'visual_effects', diff --git a/lib/utils/platform_detector.dart b/lib/utils/platform_detector.dart index 7d63955e..7d5547d1 100644 --- a/lib/utils/platform_detector.dart +++ b/lib/utils/platform_detector.dart @@ -205,6 +205,15 @@ class PlatformDetector { return Platform.isWindows || Platform.isMacOS || Platform.isLinux; } + static bool supportsExternalPlayers() { + if (TvDetectionService._tvosBuild || isAppleTV()) return false; + return Platform.isAndroid || Platform.isIOS || Platform.isMacOS || Platform.isLinux || Platform.isWindows; + } + + static bool supportsPictureInPicture() { + return !TvDetectionService._tvosBuild && !isTV() && (Platform.isAndroid || Platform.isIOS || Platform.isMacOS); + } + /// Detects if the device is likely a tablet based on screen size /// Uses diagonal screen size to determine if device is a tablet static bool isTablet(BuildContext context) { diff --git a/lib/utils/video_player_navigation.dart b/lib/utils/video_player_navigation.dart index 0b525c04..bd2897a4 100644 --- a/lib/utils/video_player_navigation.dart +++ b/lib/utils/video_player_navigation.dart @@ -16,6 +16,7 @@ import '../services/external_player_service.dart'; import '../services/offline_watch_sync_service.dart'; import '../services/settings_service.dart'; import 'app_logger.dart'; +import 'platform_detector.dart'; const String kVideoPlayerRouteName = '/video_player'; @@ -185,7 +186,7 @@ Future navigateToVideoPlayer( // Check if external player is enabled try { final settingsService = await SettingsService.getInstance(); - if (settingsService.read(SettingsService.useExternalPlayer)) { + if (PlatformDetector.supportsExternalPlayers() && settingsService.read(SettingsService.useExternalPlayer)) { bool launched = false; if (isOffline) { diff --git a/lib/widgets/media_context_menu.dart b/lib/widgets/media_context_menu.dart index 912dbdc9..974c7db9 100644 --- a/lib/widgets/media_context_menu.dart +++ b/lib/widgets/media_context_menu.dart @@ -403,7 +403,8 @@ class MediaContextMenuState extends State { menuActions.add(_MenuAction(value: 'fileinfo', icon: Symbols.info_rounded, label: t.mediaMenu.fileInfo)); } - if (mediaKind == MediaKind.episode || mediaKind == MediaKind.movie) { + if (PlatformDetector.supportsExternalPlayers() && + (mediaKind == MediaKind.episode || mediaKind == MediaKind.movie)) { menuActions.add( _MenuAction( value: 'play_external', @@ -1226,6 +1227,8 @@ class MediaContextMenuState extends State { /// Handle play in external player action Future _handlePlayExternal(BuildContext context) async { + if (!PlatformDetector.supportsExternalPlayers()) return; + final item = _mediaItem!; // Check if the item is downloaded and use local file path if available diff --git a/lib/widgets/video_controls/parts/visibility.dart b/lib/widgets/video_controls/parts/visibility.dart index d58c968d..407ca502 100644 --- a/lib/widgets/video_controls/parts/visibility.dart +++ b/lib/widgets/video_controls/parts/visibility.dart @@ -145,7 +145,7 @@ extension _PlexVideoControlsVisibilityMethods on _PlexVideoControlsState { } Future _checkPipSupport() async { - if (!Platform.isAndroid && !Platform.isIOS && !Platform.isMacOS) { + if (!PlatformDetector.supportsPictureInPicture()) { return; } diff --git a/test/services/settings_service_test.dart b/test/services/settings_service_test.dart index df00f4af..2829c2a8 100644 --- a/test/services/settings_service_test.dart +++ b/test/services/settings_service_test.dart @@ -3,6 +3,7 @@ import 'package:flutter_test/flutter_test.dart'; import 'package:plezy/models/hotkey_model.dart'; import 'package:plezy/services/settings_service.dart'; import 'package:plezy/services/trackers/tracker_constants.dart'; +import 'package:plezy/utils/platform_detector.dart'; import '../test_helpers/prefs.dart'; @@ -12,6 +13,10 @@ void main() { SettingsService.resetForTesting(); }); + tearDown(() { + TvDetectionService.debugSetAppleTVOverride(null); + }); + group('SettingsService.parseMpvConfigText', () { test('parses plain key=value lines', () { final out = SettingsService.parseMpvConfigText('hwdec=auto\nvolume=100'); @@ -82,6 +87,26 @@ void main() { }); }); + group('SettingsService platform gates', () { + test('forces external player off on Apple TV even when stored enabled', () async { + final settings = await SettingsService.getInstance(); + await settings.write(SettingsService.useExternalPlayer, true); + + TvDetectionService.debugSetAppleTVOverride(true); + + expect(settings.read(SettingsService.useExternalPlayer), isFalse); + }); + + test('forces auto PiP off on Apple TV even when stored enabled', () async { + final settings = await SettingsService.getInstance(); + await settings.write(SettingsService.autoPip, true); + + TvDetectionService.debugSetAppleTVOverride(true); + + expect(settings.read(SettingsService.autoPip), isFalse); + }); + }); + group('SettingsService companion remote prefs', () { test('last manual host address trims whitespace and drops blanks', () async { final settings = await SettingsService.getInstance();