From 83c5fafe46d1728f081ba7a8d19ad65887c2b16b Mon Sep 17 00:00:00 2001 From: PartyDonut Date: Fri, 17 Oct 2025 13:05:51 +0200 Subject: [PATCH] feat: Enable player orientation for native player on phones --- .../fladder/api/PlayerSettingsHelper.g.kt | 43 +++++++++--- .../composables/dialogs/AudioSelection.kt | 4 +- .../composables/dialogs/SubtitlePicker.kt | 4 +- .../fladder/objects/VideoPlayerObject.kt | 17 +++-- .../nl/jknaapen/fladder/player/ExoPlayer.kt | 68 ++++++++++--------- .../fladder/utility/AllowedOrientations.kt | 60 ++++++++++++++++ .../utility/{capitalize.kt => Capitalize.kt} | 0 .../pigeon_player_settings_provider.dart | 11 +++ .../settings/player_settings_page.dart | 17 ++--- lib/src/player_settings_helper.g.dart | 32 +++++++-- pigeons/player_settings_pigeon.dart | 9 +++ 11 files changed, 197 insertions(+), 68 deletions(-) create mode 100644 android/app/src/main/kotlin/nl/jknaapen/fladder/utility/AllowedOrientations.kt rename android/app/src/main/kotlin/nl/jknaapen/fladder/utility/{capitalize.kt => Capitalize.kt} (100%) diff --git a/android/app/src/main/kotlin/nl/jknaapen/fladder/api/PlayerSettingsHelper.g.kt b/android/app/src/main/kotlin/nl/jknaapen/fladder/api/PlayerSettingsHelper.g.kt index e18e1ce..7cb8ef8 100644 --- a/android/app/src/main/kotlin/nl/jknaapen/fladder/api/PlayerSettingsHelper.g.kt +++ b/android/app/src/main/kotlin/nl/jknaapen/fladder/api/PlayerSettingsHelper.g.kt @@ -65,6 +65,19 @@ private object PlayerSettingsHelperPigeonUtils { } +enum class PlayerOrientations(val raw: Int) { + PORTRAIT_UP(0), + PORTRAIT_DOWN(1), + LAND_SCAPE_LEFT(2), + LAND_SCAPE_RIGHT(3); + + companion object { + fun ofRaw(raw: Int): PlayerOrientations? { + return values().firstOrNull { it.raw == raw } + } + } +} + enum class AutoNextType(val raw: Int) { OFF(0), STATIC(1), @@ -110,7 +123,8 @@ data class PlayerSettings ( val themeColor: Long? = null, val skipForward: Long, val skipBackward: Long, - val autoNextType: AutoNextType + val autoNextType: AutoNextType, + val acceptedOrientations: List ) { companion object { @@ -121,7 +135,8 @@ data class PlayerSettings ( val skipForward = pigeonVar_list[3] as Long val skipBackward = pigeonVar_list[4] as Long val autoNextType = pigeonVar_list[5] as AutoNextType - return PlayerSettings(enableTunneling, skipTypes, themeColor, skipForward, skipBackward, autoNextType) + val acceptedOrientations = pigeonVar_list[6] as List + return PlayerSettings(enableTunneling, skipTypes, themeColor, skipForward, skipBackward, autoNextType, acceptedOrientations) } } fun toList(): List { @@ -132,6 +147,7 @@ data class PlayerSettings ( skipForward, skipBackward, autoNextType, + acceptedOrientations, ) } override fun equals(other: Any?): Boolean { @@ -150,20 +166,25 @@ private open class PlayerSettingsHelperPigeonCodec : StandardMessageCodec() { return when (type) { 129.toByte() -> { return (readValue(buffer) as Long?)?.let { - AutoNextType.ofRaw(it.toInt()) + PlayerOrientations.ofRaw(it.toInt()) } } 130.toByte() -> { return (readValue(buffer) as Long?)?.let { - SegmentType.ofRaw(it.toInt()) + AutoNextType.ofRaw(it.toInt()) } } 131.toByte() -> { return (readValue(buffer) as Long?)?.let { - SegmentSkip.ofRaw(it.toInt()) + SegmentType.ofRaw(it.toInt()) } } 132.toByte() -> { + return (readValue(buffer) as Long?)?.let { + SegmentSkip.ofRaw(it.toInt()) + } + } + 133.toByte() -> { return (readValue(buffer) as? List)?.let { PlayerSettings.fromList(it) } @@ -173,20 +194,24 @@ private open class PlayerSettingsHelperPigeonCodec : StandardMessageCodec() { } override fun writeValue(stream: ByteArrayOutputStream, value: Any?) { when (value) { - is AutoNextType -> { + is PlayerOrientations -> { stream.write(129) writeValue(stream, value.raw) } - is SegmentType -> { + is AutoNextType -> { stream.write(130) writeValue(stream, value.raw) } - is SegmentSkip -> { + is SegmentType -> { stream.write(131) writeValue(stream, value.raw) } - is PlayerSettings -> { + is SegmentSkip -> { stream.write(132) + writeValue(stream, value.raw) + } + is PlayerSettings -> { + stream.write(133) writeValue(stream, value.toList()) } else -> super.writeValue(stream, value) diff --git a/android/app/src/main/kotlin/nl/jknaapen/fladder/composables/dialogs/AudioSelection.kt b/android/app/src/main/kotlin/nl/jknaapen/fladder/composables/dialogs/AudioSelection.kt index ec4f175..713c84e 100644 --- a/android/app/src/main/kotlin/nl/jknaapen/fladder/composables/dialogs/AudioSelection.kt +++ b/android/app/src/main/kotlin/nl/jknaapen/fladder/composables/dialogs/AudioSelection.kt @@ -28,8 +28,8 @@ fun AudioPicker( onDismissRequest: () -> Unit, ) { val selectedIndex by VideoPlayerObject.currentAudioTrackIndex.collectAsState() - val audioTracks by VideoPlayerObject.audioTracks.collectAsState(listOf()) - val internalAudioTracks by VideoPlayerObject.exoAudioTracks + val audioTracks by VideoPlayerObject.audioTracks.collectAsState(emptyList()) + val internalAudioTracks by VideoPlayerObject.exoAudioTracks.collectAsState(emptyList()) if (internalAudioTracks.isEmpty()) return diff --git a/android/app/src/main/kotlin/nl/jknaapen/fladder/composables/dialogs/SubtitlePicker.kt b/android/app/src/main/kotlin/nl/jknaapen/fladder/composables/dialogs/SubtitlePicker.kt index 2b64ad6..6b69fce 100644 --- a/android/app/src/main/kotlin/nl/jknaapen/fladder/composables/dialogs/SubtitlePicker.kt +++ b/android/app/src/main/kotlin/nl/jknaapen/fladder/composables/dialogs/SubtitlePicker.kt @@ -29,8 +29,8 @@ fun SubtitlePicker( onDismissRequest: () -> Unit, ) { val selectedIndex by VideoPlayerObject.currentSubtitleTrackIndex.collectAsState() - val subTitles by VideoPlayerObject.subtitleTracks.collectAsState(listOf()) - val internalSubTracks by VideoPlayerObject.exoSubTracks + val subTitles by VideoPlayerObject.subtitleTracks.collectAsState(emptyList()) + val internalSubTracks by VideoPlayerObject.exoSubTracks.collectAsState(emptyList()) if (internalSubTracks.isEmpty()) return diff --git a/android/app/src/main/kotlin/nl/jknaapen/fladder/objects/VideoPlayerObject.kt b/android/app/src/main/kotlin/nl/jknaapen/fladder/objects/VideoPlayerObject.kt index e5707fa..80a019b 100644 --- a/android/app/src/main/kotlin/nl/jknaapen/fladder/objects/VideoPlayerObject.kt +++ b/android/app/src/main/kotlin/nl/jknaapen/fladder/objects/VideoPlayerObject.kt @@ -5,8 +5,9 @@ import VideoPlayerControlsCallback import VideoPlayerListenerCallback import android.os.Build import androidx.annotation.RequiresApi -import androidx.compose.runtime.mutableStateOf +import kotlinx.coroutines.flow.Flow import kotlinx.coroutines.flow.MutableStateFlow +import kotlinx.coroutines.flow.asStateFlow import kotlinx.coroutines.flow.combine import kotlinx.coroutines.flow.map import nl.jknaapen.fladder.VideoPlayerActivity @@ -52,8 +53,8 @@ object VideoPlayerObject { val currentAudioTrackIndex = MutableStateFlow((implementation.playbackData.value?.defaultAudioTrack ?: -1).toInt()) - val exoAudioTracks = mutableStateOf>(listOf()) - val exoSubTracks = mutableStateOf>(listOf()) + val exoAudioTracks = MutableStateFlow>(emptyList()) + val exoSubTracks = MutableStateFlow>(emptyList()) fun setSubtitleTrackIndex(value: Int, init: Boolean = false) { currentSubtitleTrackIndex.value = value @@ -72,9 +73,15 @@ object VideoPlayerObject { val subtitleTracks = implementation.playbackData.map { it?.subtitleTracks ?: listOf() } val audioTracks = implementation.playbackData.map { it?.audioTracks ?: listOf() } - val hasSubtracks = subtitleTracks.map { it.isNotEmpty() && exoSubTracks.value.isNotEmpty() } - val hasAudioTracks = audioTracks.map { it.isNotEmpty() && exoAudioTracks.value.isNotEmpty() } + val hasSubtracks: Flow = + combine(subtitleTracks, exoSubTracks.asStateFlow()) { sub, exo -> + sub.isNotEmpty() && exo.isNotEmpty() + } + val hasAudioTracks: Flow = + combine(audioTracks, exoAudioTracks.asStateFlow()) { audio, exo -> + audio.isNotEmpty() && exo.isNotEmpty() + } fun setPlaybackState(state: PlaybackState) { _currentState.value = state diff --git a/android/app/src/main/kotlin/nl/jknaapen/fladder/player/ExoPlayer.kt b/android/app/src/main/kotlin/nl/jknaapen/fladder/player/ExoPlayer.kt index 47263df..e761f3a 100644 --- a/android/app/src/main/kotlin/nl/jknaapen/fladder/player/ExoPlayer.kt +++ b/android/app/src/main/kotlin/nl/jknaapen/fladder/player/ExoPlayer.kt @@ -42,6 +42,7 @@ import nl.jknaapen.fladder.composables.overlays.NextUpOverlay import nl.jknaapen.fladder.messengers.properlySetSubAndAudioTracks import nl.jknaapen.fladder.objects.PlayerSettingsObject import nl.jknaapen.fladder.objects.VideoPlayerObject +import nl.jknaapen.fladder.utility.AllowedOrientations import nl.jknaapen.fladder.utility.getAudioTracks import nl.jknaapen.fladder.utility.getSubtitleTracks import kotlin.time.Duration.Companion.seconds @@ -194,42 +195,45 @@ internal fun ExoPlayer( } } - - NextUpOverlay( - modifier = Modifier - .fillMaxSize() - ) { showControls -> - AndroidView( + AllowedOrientations( + PlayerSettingsObject.settings.value?.acceptedOrientations ?: emptyList() + ) { + NextUpOverlay( modifier = Modifier .fillMaxSize() - .background(color = Color.Black), - factory = { - PlayerView(it).apply { - player = exoPlayer - useController = false - layoutParams = ViewGroup.LayoutParams( - ViewGroup.LayoutParams.MATCH_PARENT, - ViewGroup.LayoutParams.MATCH_PARENT, - ) - keepScreenOn = true - subtitleView?.apply { - setStyle( - CaptionStyleCompat( - android.graphics.Color.WHITE, - android.graphics.Color.TRANSPARENT, - android.graphics.Color.TRANSPARENT, - CaptionStyleCompat.EDGE_TYPE_OUTLINE, - android.graphics.Color.BLACK, - null - ) + ) { showControls -> + AndroidView( + modifier = Modifier + .fillMaxSize() + .background(color = Color.Black), + factory = { + PlayerView(it).apply { + player = exoPlayer + useController = false + layoutParams = ViewGroup.LayoutParams( + ViewGroup.LayoutParams.MATCH_PARENT, + ViewGroup.LayoutParams.MATCH_PARENT, ) + keepScreenOn = true + subtitleView?.apply { + setStyle( + CaptionStyleCompat( + android.graphics.Color.WHITE, + android.graphics.Color.TRANSPARENT, + android.graphics.Color.TRANSPARENT, + CaptionStyleCompat.EDGE_TYPE_OUTLINE, + android.graphics.Color.BLACK, + null + ) + ) + } } + }, + ) + if (showControls) + CompositionLocalProvider(LocalPlayer provides exoPlayer) { + controls(exoPlayer) } - }, - ) - if (showControls) - CompositionLocalProvider(LocalPlayer provides exoPlayer) { - controls(exoPlayer) - } + } } } diff --git a/android/app/src/main/kotlin/nl/jknaapen/fladder/utility/AllowedOrientations.kt b/android/app/src/main/kotlin/nl/jknaapen/fladder/utility/AllowedOrientations.kt new file mode 100644 index 0000000..fb61ca1 --- /dev/null +++ b/android/app/src/main/kotlin/nl/jknaapen/fladder/utility/AllowedOrientations.kt @@ -0,0 +1,60 @@ +package nl.jknaapen.fladder.utility + +import PlayerOrientations +import android.content.pm.ActivityInfo +import androidx.activity.compose.LocalActivity +import androidx.compose.runtime.Composable +import androidx.compose.runtime.DisposableEffect + +@Composable +fun AllowedOrientations( + allowed: List, + content: @Composable () -> Unit +) { + val activity = LocalActivity.current + + DisposableEffect(allowed) { + val previousOrientation = activity?.requestedOrientation + + val newOrientation = allowed.toRequestedOrientation() + activity?.requestedOrientation = newOrientation + + onDispose { + previousOrientation?.let { activity.requestedOrientation = it } + } + } + + content() +} + +private fun List.toRequestedOrientation(): Int { + val hasPortraitUp = contains(PlayerOrientations.PORTRAIT_UP) + val hasPortraitDown = contains(PlayerOrientations.PORTRAIT_DOWN) + val hasLandscapeLeft = contains(PlayerOrientations.LAND_SCAPE_LEFT) + val hasLandscapeRight = contains(PlayerOrientations.LAND_SCAPE_RIGHT) + + return when { + hasPortraitUp && hasPortraitDown && !hasLandscapeLeft && !hasLandscapeRight -> + ActivityInfo.SCREEN_ORIENTATION_SENSOR_PORTRAIT + + hasLandscapeLeft && hasLandscapeRight && !hasPortraitUp && !hasPortraitDown -> + ActivityInfo.SCREEN_ORIENTATION_SENSOR_LANDSCAPE + + hasPortraitUp && !hasPortraitDown && !hasLandscapeLeft && !hasLandscapeRight -> + ActivityInfo.SCREEN_ORIENTATION_PORTRAIT + + hasPortraitDown && !hasPortraitUp && !hasLandscapeLeft && !hasLandscapeRight -> + ActivityInfo.SCREEN_ORIENTATION_REVERSE_PORTRAIT + + hasLandscapeLeft && !hasLandscapeRight && !hasPortraitUp && !hasPortraitDown -> + ActivityInfo.SCREEN_ORIENTATION_LANDSCAPE + + hasLandscapeRight && !hasLandscapeLeft && !hasPortraitUp && !hasPortraitDown -> + ActivityInfo.SCREEN_ORIENTATION_REVERSE_LANDSCAPE + + hasPortraitUp && hasLandscapeLeft && hasLandscapeRight && hasPortraitDown -> + ActivityInfo.SCREEN_ORIENTATION_FULL_SENSOR + + else -> ActivityInfo.SCREEN_ORIENTATION_UNSPECIFIED + } +} \ No newline at end of file diff --git a/android/app/src/main/kotlin/nl/jknaapen/fladder/utility/capitalize.kt b/android/app/src/main/kotlin/nl/jknaapen/fladder/utility/Capitalize.kt similarity index 100% rename from android/app/src/main/kotlin/nl/jknaapen/fladder/utility/capitalize.kt rename to android/app/src/main/kotlin/nl/jknaapen/fladder/utility/Capitalize.kt diff --git a/lib/providers/settings/pigeon_player_settings_provider.dart b/lib/providers/settings/pigeon_player_settings_provider.dart index a427f2d..cfc6446 100644 --- a/lib/providers/settings/pigeon_player_settings_provider.dart +++ b/lib/providers/settings/pigeon_player_settings_provider.dart @@ -1,6 +1,7 @@ import 'dart:io'; import 'package:flutter/foundation.dart'; +import 'package:flutter/services.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart'; @@ -50,6 +51,16 @@ final pigeonPlayerSettingsSyncProvider = Provider((ref) { }, skipBackward: (userData?.userSettings?.skipBackDuration ?? const Duration(seconds: 15)).inMilliseconds, skipForward: (userData?.userSettings?.skipForwardDuration ?? const Duration(seconds: 30)).inMilliseconds, + acceptedOrientations: (value.allowedOrientations?.toList() ?? DeviceOrientation.values) + .map( + (e) => switch (e) { + DeviceOrientation.portraitUp => pigeon.PlayerOrientations.portraitUp, + DeviceOrientation.portraitDown => pigeon.PlayerOrientations.portraitDown, + DeviceOrientation.landscapeLeft => pigeon.PlayerOrientations.landScapeLeft, + DeviceOrientation.landscapeRight => pigeon.PlayerOrientations.landScapeRight, + }, + ) + .toList(), ), ); } diff --git a/lib/screens/settings/player_settings_page.dart b/lib/screens/settings/player_settings_page.dart index d61d6ab..3535053 100644 --- a/lib/screens/settings/player_settings_page.dart +++ b/lib/screens/settings/player_settings_page.dart @@ -408,17 +408,12 @@ class _PlayerSettingsPageState extends ConsumerState { ), ], ), - if (videoSettings.wantedPlayer != PlayerOptions.nativePlayer) ...[ - if (!AdaptiveLayout.of(context).isDesktop && - !kIsWeb && - !ref.read(argumentsStateProvider).htpcMode && - videoSettings.wantedPlayer != PlayerOptions.nativePlayer) - SettingsListTile( - label: Text(context.localized.playerSettingsOrientationTitle), - subLabel: Text(context.localized.playerSettingsOrientationDesc), - onTap: () => showOrientationOptions(context, ref), - ), - ], + if (!AdaptiveLayout.of(context).isDesktop && !kIsWeb && !ref.read(argumentsStateProvider).htpcMode) + SettingsListTile( + label: Text(context.localized.playerSettingsOrientationTitle), + subLabel: Text(context.localized.playerSettingsOrientationDesc), + onTap: () => showOrientationOptions(context, ref), + ), ], ), ], diff --git a/lib/src/player_settings_helper.g.dart b/lib/src/player_settings_helper.g.dart index 0697221..3fac3d6 100644 --- a/lib/src/player_settings_helper.g.dart +++ b/lib/src/player_settings_helper.g.dart @@ -29,6 +29,13 @@ bool _deepEquals(Object? a, Object? b) { } +enum PlayerOrientations { + portraitUp, + portraitDown, + landScapeLeft, + landScapeRight, +} + enum AutoNextType { off, static, @@ -57,6 +64,7 @@ class PlayerSettings { required this.skipForward, required this.skipBackward, required this.autoNextType, + required this.acceptedOrientations, }); bool enableTunneling; @@ -71,6 +79,8 @@ class PlayerSettings { AutoNextType autoNextType; + List acceptedOrientations; + List _toList() { return [ enableTunneling, @@ -79,6 +89,7 @@ class PlayerSettings { skipForward, skipBackward, autoNextType, + acceptedOrientations, ]; } @@ -94,6 +105,7 @@ class PlayerSettings { skipForward: result[3]! as int, skipBackward: result[4]! as int, autoNextType: result[5]! as AutoNextType, + acceptedOrientations: (result[6] as List?)!.cast(), ); } @@ -123,17 +135,20 @@ class _PigeonCodec extends StandardMessageCodec { if (value is int) { buffer.putUint8(4); buffer.putInt64(value); - } else if (value is AutoNextType) { + } else if (value is PlayerOrientations) { buffer.putUint8(129); writeValue(buffer, value.index); - } else if (value is SegmentType) { + } else if (value is AutoNextType) { buffer.putUint8(130); writeValue(buffer, value.index); - } else if (value is SegmentSkip) { + } else if (value is SegmentType) { buffer.putUint8(131); writeValue(buffer, value.index); - } else if (value is PlayerSettings) { + } else if (value is SegmentSkip) { buffer.putUint8(132); + writeValue(buffer, value.index); + } else if (value is PlayerSettings) { + buffer.putUint8(133); writeValue(buffer, value.encode()); } else { super.writeValue(buffer, value); @@ -145,14 +160,17 @@ class _PigeonCodec extends StandardMessageCodec { switch (type) { case 129: final int? value = readValue(buffer) as int?; - return value == null ? null : AutoNextType.values[value]; + return value == null ? null : PlayerOrientations.values[value]; case 130: final int? value = readValue(buffer) as int?; - return value == null ? null : SegmentType.values[value]; + return value == null ? null : AutoNextType.values[value]; case 131: final int? value = readValue(buffer) as int?; - return value == null ? null : SegmentSkip.values[value]; + return value == null ? null : SegmentType.values[value]; case 132: + final int? value = readValue(buffer) as int?; + return value == null ? null : SegmentSkip.values[value]; + case 133: return PlayerSettings.decode(readValue(buffer)!); default: return super.readValueOfType(type, buffer); diff --git a/pigeons/player_settings_pigeon.dart b/pigeons/player_settings_pigeon.dart index d38fe90..a9d0de7 100644 --- a/pigeons/player_settings_pigeon.dart +++ b/pigeons/player_settings_pigeon.dart @@ -19,6 +19,7 @@ class PlayerSettings { final int skipForward; final int skipBackward; final AutoNextType autoNextType; + final List acceptedOrientations; const PlayerSettings({ required this.enableTunneling, @@ -27,9 +28,17 @@ class PlayerSettings { required this.skipForward, required this.skipBackward, required this.autoNextType, + required this.acceptedOrientations, }); } +enum PlayerOrientations { + portraitUp, + portraitDown, + landScapeLeft, + landScapeRight, +} + enum AutoNextType { off, static,