fix(tvos): hide unsupported playback options
This commit is contained in:
@@ -51,9 +51,9 @@ class _PlaybackSettingsScreenState extends State<PlaybackSettingsScreen> {
|
||||
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(),
|
||||
|
||||
@@ -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;
|
||||
|
||||
|
||||
@@ -156,6 +156,19 @@ class _AutoPipPref extends Pref<bool> {
|
||||
Future<void> writeTo(BaseSharedPreferencesService svc, bool value) => svc.writeBool(key, value);
|
||||
}
|
||||
|
||||
class _UseExternalPlayerPref extends Pref<bool> {
|
||||
const _UseExternalPlayerPref() : super('use_external_player');
|
||||
|
||||
@override
|
||||
bool readFrom(BaseSharedPreferencesService svc) {
|
||||
if (!PlatformDetector.supportsExternalPlayers()) return false;
|
||||
return svc.prefs.getBool(key) ?? false;
|
||||
}
|
||||
|
||||
@override
|
||||
Future<void> 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<VisualEffectsSetting>(
|
||||
'visual_effects',
|
||||
|
||||
@@ -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) {
|
||||
|
||||
@@ -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<bool?> 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) {
|
||||
|
||||
@@ -403,7 +403,8 @@ class MediaContextMenuState extends State<MediaContextMenu> {
|
||||
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<MediaContextMenu> {
|
||||
|
||||
/// Handle play in external player action
|
||||
Future<void> _handlePlayExternal(BuildContext context) async {
|
||||
if (!PlatformDetector.supportsExternalPlayers()) return;
|
||||
|
||||
final item = _mediaItem!;
|
||||
|
||||
// Check if the item is downloaded and use local file path if available
|
||||
|
||||
@@ -145,7 +145,7 @@ extension _PlexVideoControlsVisibilityMethods on _PlexVideoControlsState {
|
||||
}
|
||||
|
||||
Future<void> _checkPipSupport() async {
|
||||
if (!Platform.isAndroid && !Platform.isIOS && !Platform.isMacOS) {
|
||||
if (!PlatformDetector.supportsPictureInPicture()) {
|
||||
return;
|
||||
}
|
||||
|
||||
|
||||
@@ -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();
|
||||
|
||||
Reference in New Issue
Block a user