diff --git a/.github/nightly-notes/2026-10-27-feat-tilt-fullscreen-rotation-locked.md b/.github/nightly-notes/2026-10-27-feat-tilt-fullscreen-rotation-locked.md new file mode 100644 index 0000000000..df4d9003f8 --- /dev/null +++ b/.github/nightly-notes/2026-10-27-feat-tilt-fullscreen-rotation-locked.md @@ -0,0 +1,2 @@ +## New +- Tilt for fullscreen setting: turn the phone sideways to enter fullscreen even with rotation locked, off by default (#1219) diff --git a/app/src/main/java/io/github/aedev/flow/data/local/PlayerPreferences.kt b/app/src/main/java/io/github/aedev/flow/data/local/PlayerPreferences.kt index bb07c06cab..e0621b2a0e 100644 --- a/app/src/main/java/io/github/aedev/flow/data/local/PlayerPreferences.kt +++ b/app/src/main/java/io/github/aedev/flow/data/local/PlayerPreferences.kt @@ -77,6 +77,7 @@ class PlayerPreferences( val QUEUE_AUTOPLAY_ENABLED = booleanPreferencesKey("queue_autoplay_enabled") val START_VIDEOS_PAUSED = booleanPreferencesKey("start_videos_paused") val OPEN_VIDEOS_FULLSCREEN = booleanPreferencesKey("open_videos_fullscreen") + val TILT_FULLSCREEN_ROTATION_LOCKED = booleanPreferencesKey("tilt_fullscreen_rotation_locked") val RESUME_FROM_HEADPHONES = booleanPreferencesKey("resume_from_headphones") val MUSIC_ENDLESS_RADIO_ENABLED = booleanPreferencesKey("music_endless_radio_enabled") val AUTOPLAY_COUNTDOWN_SECONDS = intPreferencesKey("autoplay_countdown_seconds") @@ -1641,6 +1642,17 @@ class PlayerPreferences( } } + /** Turning the phone sideways enters fullscreen even while the system rotation lock is on. */ + val tiltFullscreenRotationLocked: Flow = + context.playerPreferencesDataStore.data + .map { preferences -> preferences[Keys.TILT_FULLSCREEN_ROTATION_LOCKED] ?: false } + + suspend fun setTiltFullscreenRotationLocked(enabled: Boolean) { + context.playerPreferencesDataStore.edit { preferences -> + preferences[Keys.TILT_FULLSCREEN_ROTATION_LOCKED] = enabled + } + } + /** A Play button on headphones may start the last songs after Flow was closed. */ val resumeFromHeadphones: Flow = context.playerPreferencesDataStore.data diff --git a/app/src/main/java/io/github/aedev/flow/ui/screens/player/VideoPlayerHost.kt b/app/src/main/java/io/github/aedev/flow/ui/screens/player/VideoPlayerHost.kt index 2b4a99aaad..9dee47040f 100644 --- a/app/src/main/java/io/github/aedev/flow/ui/screens/player/VideoPlayerHost.kt +++ b/app/src/main/java/io/github/aedev/flow/ui/screens/player/VideoPlayerHost.kt @@ -367,6 +367,8 @@ fun VideoPlayerHost( isFullscreen = screenState.isFullscreen, videoAspectRatio = effectiveVideoAspectRatio, isPortraitFullscreen = screenState.isFullscreenPortrait, + ignoreRotationLock = + prefs.tiltFullscreenRotationLocked && !isLargeWindow && !context.orientationRequestIgnored(config), onEnterFullscreen = { screenState.isFullscreen = true }, onExitFullscreen = { screenState.isFullscreen = false diff --git a/app/src/main/java/io/github/aedev/flow/ui/screens/player/effects/PlayerWindowEffects.kt b/app/src/main/java/io/github/aedev/flow/ui/screens/player/effects/PlayerWindowEffects.kt index 85cb508ea1..3c763c9ba9 100644 --- a/app/src/main/java/io/github/aedev/flow/ui/screens/player/effects/PlayerWindowEffects.kt +++ b/app/src/main/java/io/github/aedev/flow/ui/screens/player/effects/PlayerWindowEffects.kt @@ -188,6 +188,7 @@ internal fun OrientationListenerEffect( isFullscreen: Boolean, videoAspectRatio: Float = 16f / 9f, isPortraitFullscreen: Boolean = false, + ignoreRotationLock: Boolean = false, onEnterFullscreen: () -> Unit, onExitFullscreen: () -> Unit, ) { @@ -197,6 +198,7 @@ internal fun OrientationListenerEffect( val currentIsFullscreen by rememberUpdatedState(isFullscreen) val currentAspectRatio by rememberUpdatedState(videoAspectRatio) val currentIsPortraitFullscreen by rememberUpdatedState(isPortraitFullscreen) + val currentIgnoreRotationLock by rememberUpdatedState(ignoreRotationLock) val currentEnter by rememberUpdatedState(onEnterFullscreen) val currentExit by rememberUpdatedState(onExitFullscreen) @@ -206,14 +208,15 @@ internal fun OrientationListenerEffect( override fun onOrientationChanged(orientation: Int) { if (orientation == ORIENTATION_UNKNOWN) return - val autoRotateOn = + val autoRotateOn = { try { Settings.System.getInt(context.contentResolver, Settings.System.ACCELEROMETER_ROTATION) == 1 } catch (e: Exception) { true } + } - if (!autoRotateOn) return + if (!followsTilt(currentIgnoreRotationLock, autoRotateOn)) return val newOrientation = heldOrientation(orientation, physicalOrientation) @@ -233,18 +236,18 @@ internal fun OrientationListenerEffect( LaunchedEffect(physicalOrientation, isExpanded) { delay(150) + if (physicalOrientation != 1 || !isExpanded) return@LaunchedEffect + if (currentIsPortraitFullscreen || currentAspectRatio < 1f) return@LaunchedEffect + if (!currentIsFullscreen) currentEnter() + } - if (physicalOrientation == -1) return@LaunchedEffect - - val isVerticalVideo = currentAspectRatio < 1f - - if (currentIsPortraitFullscreen) return@LaunchedEffect - - if (physicalOrientation == 1 && isExpanded && !currentIsFullscreen && !isVerticalVideo) { - currentEnter() - } else if (physicalOrientation == 0 && currentIsFullscreen && !isVerticalVideo) { - currentExit() - } + // Exit answers only the phone turning upright, not the sheet expanding: "Open videos in + // fullscreen" enters as the sheet settles, and a re-run on that flip left it again 150 ms later. + LaunchedEffect(physicalOrientation) { + delay(150) + if (physicalOrientation != 0) return@LaunchedEffect + if (currentIsPortraitFullscreen || currentAspectRatio < 1f) return@LaunchedEffect + if (currentIsFullscreen) currentExit() } } @@ -262,3 +265,13 @@ internal fun heldOrientation( in 0..30, in 330..359 -> 0 else -> previous } + +/** + * Whether a tilt may move the player in or out of fullscreen. Fullscreen asks for + * SENSOR_LANDSCAPE, which turns the window even under the system rotation lock, so the lock only + * matters when the viewer has not asked to tilt through it (#1219). + */ +internal fun followsTilt( + ignoreRotationLock: Boolean, + autoRotateOn: () -> Boolean, +): Boolean = ignoreRotationLock || autoRotateOn() diff --git a/app/src/main/java/io/github/aedev/flow/ui/screens/player/state/VideoPlayerPreferencesState.kt b/app/src/main/java/io/github/aedev/flow/ui/screens/player/state/VideoPlayerPreferencesState.kt index 4aa6cffa53..47a4f3b8d7 100644 --- a/app/src/main/java/io/github/aedev/flow/ui/screens/player/state/VideoPlayerPreferencesState.kt +++ b/app/src/main/java/io/github/aedev/flow/ui/screens/player/state/VideoPlayerPreferencesState.kt @@ -54,6 +54,7 @@ internal class VideoPlayerPreferencesState( val relatedCardStyle: PlayerRelatedCardStyle, val feedColumns: HomeFeedColumns, val deArrowEnabled: Boolean, + val tiltFullscreenRotationLocked: Boolean, ) @Composable @@ -91,6 +92,7 @@ internal fun rememberVideoPlayerPreferences(context: Context): VideoPlayerPrefer val relatedCardStyle by playerPreferences.playerRelatedCardStyle.collectAsState(initial = PlayerRelatedCardStyle.FULL_WIDTH) val feedColumns by playerPreferences.homeFeedColumns.collectAsState(initial = HomeFeedColumns.AUTO) val deArrowEnabled by playerPreferences.deArrowEnabled.collectAsState(initial = false) + val tiltFullscreenRotationLocked by playerPreferences.tiltFullscreenRotationLocked.collectAsState(initial = false) return VideoPlayerPreferencesState( preferences = playerPreferences, @@ -124,5 +126,6 @@ internal fun rememberVideoPlayerPreferences(context: Context): VideoPlayerPrefer relatedCardStyle = relatedCardStyle, feedColumns = feedColumns, deArrowEnabled = deArrowEnabled, + tiltFullscreenRotationLocked = tiltFullscreenRotationLocked, ) } diff --git a/app/src/main/java/io/github/aedev/flow/ui/screens/settings/index/PlaybackGesturesIndex.kt b/app/src/main/java/io/github/aedev/flow/ui/screens/settings/index/PlaybackGesturesIndex.kt new file mode 100644 index 0000000000..9a4f6582bd --- /dev/null +++ b/app/src/main/java/io/github/aedev/flow/ui/screens/settings/index/PlaybackGesturesIndex.kt @@ -0,0 +1,115 @@ +package io.github.aedev.flow.ui.screens.settings.index + +import io.github.aedev.flow.R +import io.github.aedev.flow.ui.components.settings.SettingEntry +import io.github.aedev.flow.ui.components.settings.SettingsDestination + +internal object PlaybackGesturesIndex { + private val page = SettingsDestination.PLAYBACK + + val doubleTapSeek = + SettingEntry( + key = "playback.double_tap_seek", + title = R.string.player_settings_double_tap_seek, + keywords = R.string.settings_keywords_gestures, + section = R.string.player_appearance_gestures_header, + destination = page, + ) + val seekZoneWidth = + SettingEntry( + key = "playback.seek_zone_width", + title = R.string.player_settings_seek_zone_width, + keywords = R.string.settings_keywords_gestures, + section = R.string.player_appearance_gestures_header, + destination = page, + ) + val brightnessGesture = + SettingEntry( + key = "playback.brightness_gesture", + title = R.string.player_appearance_brightness_gesture_title, + summary = R.string.player_appearance_brightness_gesture_subtitle, + keywords = R.string.settings_keywords_gestures, + section = R.string.player_appearance_gestures_header, + destination = page, + ) + val rememberBrightness = + SettingEntry( + key = "playback.remember_brightness", + title = R.string.player_appearance_remember_brightness_title, + summary = R.string.player_appearance_remember_brightness_subtitle, + section = R.string.player_appearance_gestures_header, + destination = page, + ) + val volumeGesture = + SettingEntry( + key = "playback.volume_gesture", + title = R.string.player_appearance_volume_gesture_title, + summary = R.string.player_appearance_volume_gesture_subtitle, + keywords = R.string.settings_keywords_gestures, + section = R.string.player_appearance_gestures_header, + destination = page, + ) + val volumeBoost = + SettingEntry( + key = "playback.volume_boost", + title = R.string.player_appearance_volume_boost_title, + summary = R.string.player_appearance_volume_boost_subtitle, + section = R.string.player_appearance_gestures_header, + destination = page, + ) + val speedSwipe = + SettingEntry( + key = "playback.speed_swipe", + title = R.string.player_settings_speed_swipe, + keywords = R.string.settings_keywords_gestures, + section = R.string.player_appearance_gestures_header, + destination = page, + ) + val speedSwipeStep = + SettingEntry( + key = "playback.speed_swipe_step", + title = R.string.player_settings_speed_swipe_step, + section = R.string.player_appearance_gestures_header, + destination = page, + ) + val seekGesture = + SettingEntry( + key = "playback.seek_gesture", + title = R.string.player_appearance_seek_gesture_title, + summary = R.string.player_appearance_seek_gesture_subtitle, + keywords = R.string.settings_keywords_gestures, + section = R.string.player_appearance_gestures_header, + destination = page, + ) + val haptics = + SettingEntry( + key = "playback.haptics", + title = R.string.player_appearance_haptics_title, + summary = R.string.player_appearance_haptics_subtitle, + section = R.string.player_appearance_gestures_header, + destination = page, + ) + val controlsWhileLoading = + SettingEntry( + key = "playback.controls_while_loading", + title = R.string.player_appearance_controls_while_loading_title, + summary = R.string.player_appearance_controls_while_loading_subtitle, + section = R.string.player_appearance_gestures_header, + destination = page, + ) + + val all = + listOf( + doubleTapSeek, + seekZoneWidth, + brightnessGesture, + rememberBrightness, + volumeGesture, + volumeBoost, + speedSwipe, + speedSwipeStep, + seekGesture, + haptics, + controlsWhileLoading, + ) +} diff --git a/app/src/main/java/io/github/aedev/flow/ui/screens/settings/index/PlaybackIndex.kt b/app/src/main/java/io/github/aedev/flow/ui/screens/settings/index/PlaybackIndex.kt index f6617b34e1..440d38ca1c 100644 --- a/app/src/main/java/io/github/aedev/flow/ui/screens/settings/index/PlaybackIndex.kt +++ b/app/src/main/java/io/github/aedev/flow/ui/screens/settings/index/PlaybackIndex.kt @@ -1,6 +1,7 @@ package io.github.aedev.flow.ui.screens.settings.index import io.github.aedev.flow.R +import io.github.aedev.flow.ui.components.settings.SettingAvailability import io.github.aedev.flow.ui.components.settings.SettingEntry import io.github.aedev.flow.ui.components.settings.SettingsDestination @@ -32,6 +33,16 @@ internal object PlaybackIndex { section = R.string.playback_header, destination = page, ) + val tiltFullscreen = + SettingEntry( + key = "playback.tilt_fullscreen", + title = R.string.player_settings_tilt_fullscreen, + summary = R.string.player_settings_tilt_fullscreen_subtitle, + keywords = R.string.settings_keywords_tilt_fullscreen, + section = R.string.playback_header, + destination = page, + availability = SettingAvailability.NotOnTv, + ) val autoplay = SettingEntry( key = "playback.autoplay", @@ -138,48 +149,6 @@ internal object PlaybackIndex { section = R.string.settings_section_speed, destination = page, ) - val doubleTapSeek = - SettingEntry( - key = "playback.double_tap_seek", - title = R.string.player_settings_double_tap_seek, - keywords = R.string.settings_keywords_gestures, - section = R.string.player_appearance_gestures_header, - destination = page, - ) - val seekZoneWidth = - SettingEntry( - key = "playback.seek_zone_width", - title = R.string.player_settings_seek_zone_width, - keywords = R.string.settings_keywords_gestures, - section = R.string.player_appearance_gestures_header, - destination = page, - ) - val brightnessGesture = - SettingEntry( - key = "playback.brightness_gesture", - title = R.string.player_appearance_brightness_gesture_title, - summary = R.string.player_appearance_brightness_gesture_subtitle, - keywords = R.string.settings_keywords_gestures, - section = R.string.player_appearance_gestures_header, - destination = page, - ) - val rememberBrightness = - SettingEntry( - key = "playback.remember_brightness", - title = R.string.player_appearance_remember_brightness_title, - summary = R.string.player_appearance_remember_brightness_subtitle, - section = R.string.player_appearance_gestures_header, - destination = page, - ) - val volumeGesture = - SettingEntry( - key = "playback.volume_gesture", - title = R.string.player_appearance_volume_gesture_title, - summary = R.string.player_appearance_volume_gesture_subtitle, - keywords = R.string.settings_keywords_gestures, - section = R.string.player_appearance_gestures_header, - destination = page, - ) val normalizeVolume = SettingEntry( key = "playback.normalize_volume", @@ -188,54 +157,6 @@ internal object PlaybackIndex { section = R.string.eq_settings_section, destination = page, ) - val volumeBoost = - SettingEntry( - key = "playback.volume_boost", - title = R.string.player_appearance_volume_boost_title, - summary = R.string.player_appearance_volume_boost_subtitle, - section = R.string.player_appearance_gestures_header, - destination = page, - ) - val speedSwipe = - SettingEntry( - key = "playback.speed_swipe", - title = R.string.player_settings_speed_swipe, - keywords = R.string.settings_keywords_gestures, - section = R.string.player_appearance_gestures_header, - destination = page, - ) - val speedSwipeStep = - SettingEntry( - key = "playback.speed_swipe_step", - title = R.string.player_settings_speed_swipe_step, - section = R.string.player_appearance_gestures_header, - destination = page, - ) - val seekGesture = - SettingEntry( - key = "playback.seek_gesture", - title = R.string.player_appearance_seek_gesture_title, - summary = R.string.player_appearance_seek_gesture_subtitle, - keywords = R.string.settings_keywords_gestures, - section = R.string.player_appearance_gestures_header, - destination = page, - ) - val haptics = - SettingEntry( - key = "playback.haptics", - title = R.string.player_appearance_haptics_title, - summary = R.string.player_appearance_haptics_subtitle, - section = R.string.player_appearance_gestures_header, - destination = page, - ) - val controlsWhileLoading = - SettingEntry( - key = "playback.controls_while_loading", - title = R.string.player_appearance_controls_while_loading_title, - summary = R.string.player_appearance_controls_while_loading_subtitle, - section = R.string.player_appearance_gestures_header, - destination = page, - ) val castButton = SettingEntry( key = "playback.cast_button", @@ -531,6 +452,7 @@ internal object PlaybackIndex { backgroundPlay, startPaused, openFullscreen, + tiltFullscreen, autoplay, queueAutoplay, autoplayCountdown, @@ -545,52 +467,44 @@ internal object PlaybackIndex { speedPresets, speedSlider, longPressSpeed, - doubleTapSeek, - seekZoneWidth, - brightnessGesture, - rememberBrightness, - volumeGesture, - volumeBoost, - speedSwipe, - speedSwipeStep, - seekGesture, - haptics, - controlsWhileLoading, - castButton, - captionsButton, - pipButton, - autoplayButton, - sleepTimerButton, - lockButton, - speedIndicator, - commentsButton, - sponsorBlockButton, - nextPrevButtons, - autoPip, - continueWatchingMiniPlayer, - restoreMusicMiniPlayer, - openMusicPlayerOnPlay, - audioLanguage, - subtitleLanguage, - autoCaptions, - relatedVideos, - relatedCardStyle, - titleLines, - comments, - commentsPreview, - liveChat, - disableShortsPlayer, - shortsPlayerPrompt, - shortsPlaybackMode, - shortsBackgroundPlay, - shortsPip, - shortsContinueIntoFeed, - endlessRadio, - pauseMusicWhenMuted, - musicResume, - resumeFromHeadphones, - musicSideSwipes, - musicVideoSwitch, - lyricsProviders, - ) + ) + + PlaybackGesturesIndex.all + + listOf( + castButton, + captionsButton, + pipButton, + autoplayButton, + sleepTimerButton, + lockButton, + speedIndicator, + commentsButton, + sponsorBlockButton, + nextPrevButtons, + autoPip, + continueWatchingMiniPlayer, + restoreMusicMiniPlayer, + openMusicPlayerOnPlay, + audioLanguage, + subtitleLanguage, + autoCaptions, + relatedVideos, + relatedCardStyle, + titleLines, + comments, + commentsPreview, + liveChat, + disableShortsPlayer, + shortsPlayerPrompt, + shortsPlaybackMode, + shortsBackgroundPlay, + shortsPip, + shortsContinueIntoFeed, + endlessRadio, + pauseMusicWhenMuted, + musicResume, + resumeFromHeadphones, + musicSideSwipes, + musicVideoSwitch, + lyricsProviders, + ) } diff --git a/app/src/main/java/io/github/aedev/flow/ui/screens/settings/playback/PlaybackSections.kt b/app/src/main/java/io/github/aedev/flow/ui/screens/settings/playback/PlaybackSections.kt index 8deba60771..906e2ce170 100644 --- a/app/src/main/java/io/github/aedev/flow/ui/screens/settings/playback/PlaybackSections.kt +++ b/app/src/main/java/io/github/aedev/flow/ui/screens/settings/playback/PlaybackSections.kt @@ -22,6 +22,7 @@ import io.github.aedev.flow.ui.components.shared.FlowNavRow import io.github.aedev.flow.ui.components.shared.FlowSwitchRow import io.github.aedev.flow.ui.components.shared.FlowToggleOption import io.github.aedev.flow.ui.screens.settings.index.DestinationIndex +import io.github.aedev.flow.ui.screens.settings.index.PlaybackGesturesIndex import io.github.aedev.flow.ui.screens.settings.index.PlaybackIndex private const val UNLIMITED_TITLE_LINES = 0 @@ -49,6 +50,7 @@ internal fun SettingsListScope.playbackSections( } switch(PlaybackIndex.startPaused, viewModel.startPaused, viewModel::setStartPaused) switch(PlaybackIndex.openFullscreen, viewModel.openFullscreen, viewModel::setOpenFullscreen) + switch(PlaybackIndex.tiltFullscreen, viewModel.tiltFullscreen, viewModel::setTiltFullscreen) switch(PlaybackIndex.autoplay, viewModel.autoplay, viewModel::setAutoplay, enabled = !state.loopAll) switch(PlaybackIndex.queueAutoplay, viewModel.queueAutoplay, viewModel::setQueueAutoplay, enabled = !state.loopAll) choice(PlaybackIndex.autoplayCountdown, onClick = { openDialog(PlaybackDialog.AUTOPLAY_COUNTDOWN) }) { @@ -82,7 +84,7 @@ internal fun SettingsListScope.playbackSections( } } group(key = "playback.gestures", header = R.string.player_appearance_gestures_header) { - choice(PlaybackIndex.doubleTapSeek, onClick = { openDialog(PlaybackDialog.DOUBLE_TAP_SEEK) }) { + choice(PlaybackGesturesIndex.doubleTapSeek, onClick = { openDialog(PlaybackDialog.DOUBLE_TAP_SEEK) }) { val seconds by viewModel.doubleTapSeek.collectAsStateWithLifecycle() if (seconds <= 0) { stringResource(R.string.player_settings_double_tap_seek_off) @@ -91,37 +93,37 @@ internal fun SettingsListScope.playbackSections( } } if (state.doubleTapSeek) { - choice(PlaybackIndex.seekZoneWidth, onClick = { openDialog(PlaybackDialog.SEEK_ZONE_WIDTH) }) { + choice(PlaybackGesturesIndex.seekZoneWidth, onClick = { openDialog(PlaybackDialog.SEEK_ZONE_WIDTH) }) { val zone by viewModel.seekZone.collectAsStateWithLifecycle() stringResource(zone.labelRes()) } } switch( - PlaybackIndex.brightnessGesture, + PlaybackGesturesIndex.brightnessGesture, viewModel.brightnessGesture, viewModel::setBrightnessGesture, enabled = state.speedSwipeSide != SpeedSwipeSide.LEFT, ) - switch(PlaybackIndex.rememberBrightness, viewModel.rememberBrightness, viewModel::setRememberBrightness) + switch(PlaybackGesturesIndex.rememberBrightness, viewModel.rememberBrightness, viewModel::setRememberBrightness) switch( - PlaybackIndex.volumeGesture, + PlaybackGesturesIndex.volumeGesture, viewModel.volumeGesture, viewModel::setVolumeGesture, enabled = state.speedSwipeSide != SpeedSwipeSide.RIGHT, ) - switch(PlaybackIndex.volumeBoost, viewModel.volumeBoost, viewModel::setVolumeBoost) - choice(PlaybackIndex.speedSwipe, onClick = { openDialog(PlaybackDialog.SPEED_SWIPE_SIDE) }) { + switch(PlaybackGesturesIndex.volumeBoost, viewModel.volumeBoost, viewModel::setVolumeBoost) + choice(PlaybackGesturesIndex.speedSwipe, onClick = { openDialog(PlaybackDialog.SPEED_SWIPE_SIDE) }) { speedSwipeSideLabel(state.speedSwipeSide) } if (state.speedSwipeSide != SpeedSwipeSide.OFF || state.musicSideSwipes) { - choice(PlaybackIndex.speedSwipeStep, onClick = { openDialog(PlaybackDialog.SPEED_SWIPE_STEP) }) { + choice(PlaybackGesturesIndex.speedSwipeStep, onClick = { openDialog(PlaybackDialog.SPEED_SWIPE_STEP) }) { val step by viewModel.speedSwipeStep.collectAsStateWithLifecycle() speedSwipeStepLabel(step) } } - switch(PlaybackIndex.seekGesture, viewModel.seekGesture, viewModel::setSeekGesture) - switch(PlaybackIndex.haptics, viewModel.haptics, viewModel::setHaptics) - switch(PlaybackIndex.controlsWhileLoading, viewModel.controlsWhileLoading, viewModel::setControlsWhileLoading) + switch(PlaybackGesturesIndex.seekGesture, viewModel.seekGesture, viewModel::setSeekGesture) + switch(PlaybackGesturesIndex.haptics, viewModel.haptics, viewModel::setHaptics) + switch(PlaybackGesturesIndex.controlsWhileLoading, viewModel.controlsWhileLoading, viewModel::setControlsWhileLoading) } group(key = "playback.buttons", header = R.string.settings_section_player_buttons) { switch(PlaybackIndex.castButton, viewModel.castButton, viewModel::setCastButton) diff --git a/app/src/main/java/io/github/aedev/flow/ui/screens/settings/playback/PlaybackSettingsViewModel.kt b/app/src/main/java/io/github/aedev/flow/ui/screens/settings/playback/PlaybackSettingsViewModel.kt index e9d01d8266..a2d9d4f3ce 100644 --- a/app/src/main/java/io/github/aedev/flow/ui/screens/settings/playback/PlaybackSettingsViewModel.kt +++ b/app/src/main/java/io/github/aedev/flow/ui/screens/settings/playback/PlaybackSettingsViewModel.kt @@ -38,6 +38,7 @@ class PlaybackSettingsViewModel val autoplay = preferences.autoplayEnabled.asState(true) val startPaused = preferences.startVideosPaused.asState(false) val openFullscreen = preferences.openVideosFullscreen.asState(false) + val tiltFullscreen = preferences.tiltFullscreenRotationLocked.asState(false) val queueAutoplay = preferences.queueAutoplayEnabled.asState(true) val autoplayCountdown = preferences.autoplayCountdownSeconds.asState(0) val loopAll = preferences.videoLoopEnabled.asState(false) @@ -126,6 +127,8 @@ class PlaybackSettingsViewModel fun setOpenFullscreen(value: Boolean) = write { preferences.setOpenVideosFullscreen(value) } + fun setTiltFullscreen(value: Boolean) = write { preferences.setTiltFullscreenRotationLocked(value) } + fun setAutoplay(value: Boolean) = write { preferences.setAutoplayEnabled(value && !loopAll.value) } fun setQueueAutoplay(value: Boolean) = write { preferences.setQueueAutoplayEnabled(value && !loopAll.value) } diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index 5ef019727c..44170c21aa 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -1298,6 +1298,8 @@ Open a video without playing it until you press play Open videos in fullscreen A video you open starts in fullscreen + Tilt for fullscreen + Turn sideways for fullscreen, even with rotation locked Autoplay related videos Play a suggested video when nothing is queued Autoplay queued videos @@ -3720,6 +3722,7 @@ subtitles, cc, closed captions skip, previous video, next video, double tap swipe, double tap, touch + rotate, rotation lock, auto-rotate, landscape, sideways e.g. 1.35 Add preset Minimum buffer diff --git a/app/src/test/java/io/github/aedev/flow/ui/screens/player/effects/FollowsTiltTest.kt b/app/src/test/java/io/github/aedev/flow/ui/screens/player/effects/FollowsTiltTest.kt new file mode 100644 index 0000000000..6dbf716bab --- /dev/null +++ b/app/src/test/java/io/github/aedev/flow/ui/screens/player/effects/FollowsTiltTest.kt @@ -0,0 +1,28 @@ +package io.github.aedev.flow.ui.screens.player.effects + +import com.google.common.truth.Truth.assertThat +import org.junit.Test + +class FollowsTiltTest { + @Test + fun `auto-rotate on follows the tilt`() { + assertThat(followsTilt(ignoreRotationLock = false, autoRotateOn = { true })).isTrue() + } + + @Test + fun `rotation lock stops the tilt when the switch is off`() { + assertThat(followsTilt(ignoreRotationLock = false, autoRotateOn = { false })).isFalse() + } + + @Test + fun `switch on follows the tilt through the rotation lock`() { + assertThat(followsTilt(ignoreRotationLock = true, autoRotateOn = { false })).isTrue() + } + + @Test + fun `switch on never reads the system setting`() { + var reads = 0 + followsTilt(ignoreRotationLock = true, autoRotateOn = { reads++ == 0 }) + assertThat(reads).isEqualTo(0) + } +}