diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml index 9b391ce0..6d84299e 100644 --- a/app/src/main/AndroidManifest.xml +++ b/app/src/main/AndroidManifest.xml @@ -34,7 +34,8 @@ + android:launchMode="singleTask" + android:configChanges="keyboard|keyboardHidden|navigation|orientation|screenSize|screenLayout"> diff --git a/app/src/main/java/com/github/damontecres/wholphin/ui/playback/KeyIdentifier.kt b/app/src/main/java/com/github/damontecres/wholphin/ui/playback/KeyIdentifier.kt index e8480978..1915e02b 100644 --- a/app/src/main/java/com/github/damontecres/wholphin/ui/playback/KeyIdentifier.kt +++ b/app/src/main/java/com/github/damontecres/wholphin/ui/playback/KeyIdentifier.kt @@ -20,7 +20,27 @@ fun isDirectionalDpad(event: KeyEvent): Boolean = fun isDpad(event: KeyEvent): Boolean = event.key == Key.DirectionCenter || isDirectionalDpad(event) -fun isEnterKey(event: KeyEvent) = event.key == Key.DirectionCenter || event.key == Key.Enter || event.key == Key.NumPadEnter +fun isEnterKey(event: KeyEvent) = + event.key == Key.DirectionCenter || event.key == Key.Enter || event.key == Key.NumPadEnter || + event.key == Key.ButtonSelect || event.key == Key.ButtonA + +fun isBackKey(event: KeyEvent) = event.key == Key.Back || event.key == Key.ButtonB + +fun isControllerMedia(event: KeyEvent) = + event.key == Key.ButtonR1 || + event.key == Key.ButtonR2 || + event.key == Key.ButtonL1 || + event.key == Key.ButtonL2 + +fun isSkipBack(event: KeyEvent) = + event.key == Key.DirectionLeft || + event.key == Key.ButtonL1 || + event.key == Key.ButtonL2 + +fun isSkipForward(event: KeyEvent) = + event.key == Key.DirectionRight || + event.key == Key.ButtonR1 || + event.key == Key.ButtonR2 fun isMedia(event: KeyEvent): Boolean = event.key == Key.MediaPlay || @@ -31,7 +51,10 @@ fun isMedia(event: KeyEvent): Boolean = event.key == Key.MediaRewind || event.key == Key.MediaSkipBackward || event.key == Key.MediaNext || - event.key == Key.MediaPrevious + event.key == Key.MediaPrevious || + event.key == Key.Captions || + event.key == Key.MediaAudioTrack || + event.key == Key.MediaStop fun isBackwardButton(event: KeyEvent): Boolean = event.key == Key.PageUp || diff --git a/app/src/main/java/com/github/damontecres/wholphin/ui/playback/PlaybackConstants.kt b/app/src/main/java/com/github/damontecres/wholphin/ui/playback/PlaybackConstants.kt index 57491b5a..322827c1 100644 --- a/app/src/main/java/com/github/damontecres/wholphin/ui/playback/PlaybackConstants.kt +++ b/app/src/main/java/com/github/damontecres/wholphin/ui/playback/PlaybackConstants.kt @@ -3,6 +3,8 @@ package com.github.damontecres.wholphin.ui.playback import androidx.compose.ui.layout.ContentScale import com.github.damontecres.wholphin.preferences.PrefContentScale +val playbackSpeedOptions = listOf(".25", ".5", ".75", "1.0", "1.25", "1.5", "1.75", "2.0") + val playbackScaleOptions = mapOf( ContentScale.Fit to "Fit", diff --git a/app/src/main/java/com/github/damontecres/wholphin/ui/playback/PlaybackControls.kt b/app/src/main/java/com/github/damontecres/wholphin/ui/playback/PlaybackControls.kt index 04561939..2343a2e5 100644 --- a/app/src/main/java/com/github/damontecres/wholphin/ui/playback/PlaybackControls.kt +++ b/app/src/main/java/com/github/damontecres/wholphin/ui/playback/PlaybackControls.kt @@ -32,7 +32,6 @@ import androidx.compose.runtime.LaunchedEffect import androidx.compose.runtime.getValue import androidx.compose.runtime.mutableFloatStateOf import androidx.compose.runtime.mutableLongStateOf -import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.remember import androidx.compose.runtime.rememberCoroutineScope import androidx.compose.runtime.setValue @@ -60,9 +59,7 @@ import androidx.tv.material3.ListItem import androidx.tv.material3.MaterialTheme import androidx.tv.material3.Text import com.github.damontecres.wholphin.R -import com.github.damontecres.wholphin.data.model.TrackIndex import com.github.damontecres.wholphin.ui.AppColors -import com.github.damontecres.wholphin.ui.indexOfFirstOrNull import com.github.damontecres.wholphin.ui.seekBack import com.github.damontecres.wholphin.ui.seekForward import com.github.damontecres.wholphin.ui.stringRes @@ -73,7 +70,6 @@ import kotlinx.coroutines.isActive import kotlinx.coroutines.launch import org.jellyfin.sdk.model.api.MediaSegmentDto import org.jellyfin.sdk.model.extensions.ticks -import timber.log.Timber import kotlin.time.Duration import kotlin.time.Duration.Companion.seconds @@ -110,22 +106,18 @@ sealed interface PlaybackAction { @OptIn(UnstableApi::class) @Composable fun PlaybackControls( - subtitleStreams: List, playerControls: Player, controllerViewState: ControllerViewState, onPlaybackActionClick: (PlaybackAction) -> Unit, - showDebugInfo: Boolean, + onClickPlaybackDialogType: (PlaybackDialogType) -> Unit, + moreFocusRequester: FocusRequester, + captionFocusRequester: FocusRequester, + settingsFocusRequester: FocusRequester, onSeekProgress: (Long) -> Unit, showPlay: Boolean, previousEnabled: Boolean, nextEnabled: Boolean, seekEnabled: Boolean, - moreButtonOptions: MoreButtonOptions, - subtitleIndex: Int?, - audioIndex: Int?, - audioStreams: List, - playbackSpeed: Float, - scale: ContentScale, seekBarIntervals: Int, seekBack: Duration, skipBackOnResume: Duration?, @@ -144,12 +136,6 @@ fun PlaybackControls( } controllerViewState.pulseControls() } - val onControllerInteractionForDialog = { - scope.launch(ExceptionHandler()) { - bringIntoViewRequester.bringIntoView() - } - controllerViewState.pulseControls(Long.MAX_VALUE) - } LaunchedEffect(controllerViewState.controlsVisible) { if (controllerViewState.controlsVisible) { initialFocusRequester.tryRequestFocus() @@ -181,10 +167,9 @@ fun PlaybackControls( .fillMaxWidth(), ) { LeftPlaybackButtons( + moreFocusRequester = moreFocusRequester, onControllerInteraction = onControllerInteraction, - onPlaybackActionClick = onPlaybackActionClick, - showDebugInfo = showDebugInfo, - moreButtonOptions = moreButtonOptions, + onClickPlaybackDialogType = onClickPlaybackDialogType, modifier = Modifier.align(Alignment.CenterStart), ) PlaybackButtons( @@ -219,15 +204,10 @@ fun PlaybackControls( } } RightPlaybackButtons( - subtitleStreams = subtitleStreams, + captionFocusRequester = captionFocusRequester, + settingsFocusRequester = settingsFocusRequester, onControllerInteraction = onControllerInteraction, - onControllerInteractionForDialog = onControllerInteractionForDialog, - onPlaybackActionClick = onPlaybackActionClick, - subtitleIndex = subtitleIndex, - audioStreams = audioStreams, - audioIndex = audioIndex, - playbackSpeed = playbackSpeed, - scale = scale, + onClickPlaybackDialogType = onClickPlaybackDialogType, modifier = Modifier, ) } @@ -307,14 +287,11 @@ private val buttonSpacing = 4.dp @Composable fun LeftPlaybackButtons( + moreFocusRequester: FocusRequester, onControllerInteraction: () -> Unit, - onPlaybackActionClick: (PlaybackAction) -> Unit, - showDebugInfo: Boolean, - moreButtonOptions: MoreButtonOptions, + onClickPlaybackDialogType: (PlaybackDialogType) -> Unit, modifier: Modifier = Modifier, ) { - var showMoreOptions by remember { mutableStateOf(false) } - val focusRequester = remember { FocusRequester() } Row( modifier = modifier.focusGroup(), horizontalArrangement = Arrangement.spacedBy(buttonSpacing), @@ -324,58 +301,23 @@ fun LeftPlaybackButtons( iconRes = R.drawable.baseline_more_vert_96, onClick = { onControllerInteraction.invoke() - showMoreOptions = true + onClickPlaybackDialogType.invoke(PlaybackDialogType.MORE) }, enabled = true, onControllerInteraction = onControllerInteraction, - modifier = Modifier.focusRequester(focusRequester), - ) - } - if (showMoreOptions) { - val options = - buildList { - addAll(moreButtonOptions.options.keys) - add(stringResource(if (showDebugInfo) R.string.hide_debug_info else R.string.show_debug_info)) - } - BottomDialog( - choices = options, - onDismissRequest = { - showMoreOptions = false - focusRequester.tryRequestFocus() - }, - onSelectChoice = { index, choice -> - val action = moreButtonOptions.options[choice] ?: PlaybackAction.ShowDebug - onPlaybackActionClick.invoke(action) - }, - gravity = Gravity.START, + modifier = Modifier.focusRequester(moreFocusRequester), ) } } -private val speedOptions = listOf(".25", ".5", ".75", "1.0", "1.25", "1.5", "1.75", "2.0") - @Composable fun RightPlaybackButtons( - subtitleStreams: List, + captionFocusRequester: FocusRequester, + settingsFocusRequester: FocusRequester, onControllerInteraction: () -> Unit, - onControllerInteractionForDialog: () -> Unit, - onPlaybackActionClick: (PlaybackAction) -> Unit, - subtitleIndex: Int?, - audioStreams: List, - audioIndex: Int?, - playbackSpeed: Float, - scale: ContentScale, + onClickPlaybackDialogType: (PlaybackDialogType) -> Unit, modifier: Modifier = Modifier, ) { - var showCaptionDialog by remember { mutableStateOf(false) } - var showOptionsDialog by remember { mutableStateOf(false) } - var showAudioDialog by remember { mutableStateOf(false) } - var showSpeedDialog by remember { mutableStateOf(false) } - var showScaleDialog by remember { mutableStateOf(false) } - - val captionFocusRequester = remember { FocusRequester() } - val settingsFocusRequester = remember { FocusRequester() } - Row( modifier = modifier.focusGroup(), horizontalArrangement = Arrangement.spacedBy(buttonSpacing), @@ -385,8 +327,8 @@ fun RightPlaybackButtons( enabled = true, iconRes = R.drawable.captions_svgrepo_com, onClick = { - onControllerInteractionForDialog.invoke() - showCaptionDialog = true + onControllerInteraction.invoke() + onClickPlaybackDialogType.invoke(PlaybackDialogType.CAPTIONS) }, onControllerInteraction = onControllerInteraction, modifier = Modifier.focusRequester(captionFocusRequester), @@ -395,131 +337,14 @@ fun RightPlaybackButtons( PlaybackButton( iconRes = R.drawable.vector_settings, onClick = { - onControllerInteractionForDialog.invoke() - showOptionsDialog = true + onControllerInteraction.invoke() + onClickPlaybackDialogType.invoke(PlaybackDialogType.SETTINGS) }, enabled = true, onControllerInteraction = onControllerInteraction, modifier = Modifier.focusRequester(settingsFocusRequester), ) } - val scope = rememberCoroutineScope() - if (showCaptionDialog) { - val options = subtitleStreams.map { it.displayName } - Timber.v("subtitleIndex=$subtitleIndex, options=$options") - val currentChoice = - subtitleStreams.indexOfFirstOrNull { it.index == subtitleIndex } ?: subtitleStreams.size - BottomDialog( - choices = - options + - listOf( - stringResource(R.string.none), - stringResource(R.string.search_and_download), - ), - currentChoice = currentChoice, - onDismissRequest = { - onControllerInteraction.invoke() - showCaptionDialog = false - scope.launch { - // TODO this is hacky, but playback changes force refocus and this is a workaround - delay(250L) - captionFocusRequester.tryRequestFocus() - } - }, - onSelectChoice = { index, _ -> - if (index in subtitleStreams.indices) { - onPlaybackActionClick.invoke(PlaybackAction.ToggleCaptions(subtitleStreams[index].index)) - } else { - val idx = index - subtitleStreams.size - if (idx == 0) { - onPlaybackActionClick.invoke(PlaybackAction.ToggleCaptions(TrackIndex.DISABLED)) - } else { - onPlaybackActionClick.invoke(PlaybackAction.SearchCaptions) - } - } - }, - gravity = Gravity.END, - ) - } - if (showOptionsDialog) { - val options = - listOf( - stringResource(R.string.audio), - stringResource(R.string.playback_speed), - stringResource(R.string.video_scale), - ) - BottomDialog( - choices = options, - currentChoice = null, - onDismissRequest = { - onControllerInteraction.invoke() - showOptionsDialog = false - }, - onSelectChoice = { index, _ -> - onControllerInteractionForDialog.invoke() - when (index) { - 0 -> showAudioDialog = true - 1 -> showSpeedDialog = true - 2 -> showScaleDialog = true - } - }, - gravity = Gravity.END, - ) - } - if (showAudioDialog) { - BottomDialog( - choices = audioStreams.map { it.displayName }, - currentChoice = audioStreams.indexOfFirstOrNull { it.index == audioIndex }, - onDismissRequest = { - onControllerInteraction.invoke() - showAudioDialog = false - scope.launch { - delay(250L) - settingsFocusRequester.tryRequestFocus() - } - }, - onSelectChoice = { index, _ -> - onPlaybackActionClick.invoke(PlaybackAction.ToggleAudio(audioStreams[index].index)) - }, - gravity = Gravity.END, - ) - } - if (showSpeedDialog) { - BottomDialog( - choices = speedOptions, - currentChoice = speedOptions.indexOf(playbackSpeed.toString()), - onDismissRequest = { - onControllerInteraction.invoke() - showSpeedDialog = false - scope.launch { - delay(250L) - settingsFocusRequester.tryRequestFocus() - } - }, - onSelectChoice = { _, value -> - onPlaybackActionClick.invoke(PlaybackAction.PlaybackSpeed(value.toFloat())) - }, - gravity = Gravity.END, - ) - } - if (showScaleDialog) { - BottomDialog( - choices = playbackScaleOptions.values.toList(), - currentChoice = playbackScaleOptions.keys.toList().indexOf(scale), - onDismissRequest = { - onControllerInteraction.invoke() - showScaleDialog = false - scope.launch { - delay(250L) - settingsFocusRequester.tryRequestFocus() - } - }, - onSelectChoice = { index, _ -> - onPlaybackActionClick.invoke(PlaybackAction.Scale(playbackScaleOptions.keys.toList()[index])) - }, - gravity = Gravity.END, - ) - } } @OptIn(UnstableApi::class) @@ -629,7 +454,7 @@ fun PlaybackButton( } @Composable -private fun BottomDialog( +fun BottomDialog( choices: List, onDismissRequest: () -> Unit, onSelectChoice: (Int, String) -> Unit, diff --git a/app/src/main/java/com/github/damontecres/wholphin/ui/playback/PlaybackDialog.kt b/app/src/main/java/com/github/damontecres/wholphin/ui/playback/PlaybackDialog.kt new file mode 100644 index 00000000..acce5f94 --- /dev/null +++ b/app/src/main/java/com/github/damontecres/wholphin/ui/playback/PlaybackDialog.kt @@ -0,0 +1,174 @@ +package com.github.damontecres.wholphin.ui.playback + +import android.view.Gravity +import androidx.compose.runtime.Composable +import androidx.compose.ui.layout.ContentScale +import androidx.compose.ui.res.stringResource +import com.github.damontecres.wholphin.R +import com.github.damontecres.wholphin.data.model.TrackIndex +import com.github.damontecres.wholphin.ui.indexOfFirstOrNull +import timber.log.Timber + +enum class PlaybackDialogType { + MORE, + CAPTIONS, + SETTINGS, + AUDIO, + PLAYBACK_SPEED, + VIDEO_SCALE, +} + +data class PlaybackSettings( + val showDebugInfo: Boolean, + val audioIndex: Int?, + val audioStreams: List, + val subtitleIndex: Int?, + val subtitleStreams: List, + val playbackSpeed: Float, + val contentScale: ContentScale, +) + +@Composable +fun PlaybackDialog( + type: PlaybackDialogType, + settings: PlaybackSettings, + onDismissRequest: () -> Unit, + onControllerInteraction: () -> Unit, + onClickPlaybackDialogType: (PlaybackDialogType) -> Unit, + onPlaybackActionClick: (PlaybackAction) -> Unit, +) { + when (type) { + PlaybackDialogType.MORE -> { + val options = + buildList { + add(stringResource(if (settings.showDebugInfo) R.string.hide_debug_info else R.string.show_debug_info)) + } + BottomDialog( + choices = options, + onDismissRequest = { + onDismissRequest.invoke() +// focusRequester.tryRequestFocus() + }, + onSelectChoice = { index, choice -> + onPlaybackActionClick.invoke(PlaybackAction.ShowDebug) + }, + gravity = Gravity.START, + ) + } + + PlaybackDialogType.CAPTIONS -> { + val subtitleStreams = settings.subtitleStreams + val options = subtitleStreams.map { it.displayName } + Timber.v("subtitleIndex=${settings.subtitleIndex}, options=$options") + val currentChoice = + subtitleStreams.indexOfFirstOrNull { it.index == settings.subtitleIndex } ?: subtitleStreams.size + BottomDialog( + choices = + options + + listOf( + stringResource(R.string.none), + stringResource(R.string.search_and_download), + ), + currentChoice = currentChoice, + onDismissRequest = { + onControllerInteraction.invoke() + onDismissRequest.invoke() +// scope.launch { +// // TODO this is hacky, but playback changes force refocus and this is a workaround +// delay(250L) +// captionFocusRequester.tryRequestFocus() +// } + }, + onSelectChoice = { index, _ -> + if (index in subtitleStreams.indices) { + onPlaybackActionClick.invoke(PlaybackAction.ToggleCaptions(subtitleStreams[index].index)) + } else { + val idx = index - subtitleStreams.size + if (idx == 0) { + onPlaybackActionClick.invoke(PlaybackAction.ToggleCaptions(TrackIndex.DISABLED)) + } else { + onPlaybackActionClick.invoke(PlaybackAction.SearchCaptions) + } + } + }, + gravity = Gravity.END, + ) + } + + PlaybackDialogType.SETTINGS -> { + val options = + listOf( + stringResource(R.string.audio), + stringResource(R.string.playback_speed), + stringResource(R.string.video_scale), + ) + BottomDialog( + choices = options, + currentChoice = null, + onDismissRequest = onDismissRequest, + onSelectChoice = { index, _ -> + when (index) { + 0 -> onClickPlaybackDialogType(PlaybackDialogType.AUDIO) + 1 -> onClickPlaybackDialogType(PlaybackDialogType.PLAYBACK_SPEED) + 2 -> onClickPlaybackDialogType(PlaybackDialogType.VIDEO_SCALE) + } + }, + gravity = Gravity.END, + ) + } + + PlaybackDialogType.AUDIO -> + BottomDialog( + choices = settings.audioStreams.map { it.displayName }, + currentChoice = settings.audioStreams.indexOfFirstOrNull { it.index == settings.audioIndex }, + onDismissRequest = { + onControllerInteraction.invoke() + onDismissRequest.invoke() +// scope.launch { +// delay(250L) +// settingsFocusRequester.tryRequestFocus() +// } + }, + onSelectChoice = { index, _ -> + onPlaybackActionClick.invoke(PlaybackAction.ToggleAudio(settings.audioStreams[index].index)) + }, + gravity = Gravity.END, + ) + + PlaybackDialogType.PLAYBACK_SPEED -> + BottomDialog( + choices = playbackSpeedOptions, + currentChoice = playbackSpeedOptions.indexOf(settings.playbackSpeed.toString()), + onDismissRequest = { + onControllerInteraction.invoke() + onDismissRequest.invoke() +// scope.launch { +// delay(250L) +// settingsFocusRequester.tryRequestFocus() +// } + }, + onSelectChoice = { _, value -> + onPlaybackActionClick.invoke(PlaybackAction.PlaybackSpeed(value.toFloat())) + }, + gravity = Gravity.END, + ) + + PlaybackDialogType.VIDEO_SCALE -> + BottomDialog( + choices = playbackScaleOptions.values.toList(), + currentChoice = playbackScaleOptions.keys.toList().indexOf(settings.contentScale), + onDismissRequest = { + onControllerInteraction.invoke() + onDismissRequest.invoke() +// scope.launch { +// delay(250L) +// settingsFocusRequester.tryRequestFocus() +// } + }, + onSelectChoice = { index, _ -> + onPlaybackActionClick.invoke(PlaybackAction.Scale(playbackScaleOptions.keys.toList()[index])) + }, + gravity = Gravity.END, + ) + } +} diff --git a/app/src/main/java/com/github/damontecres/wholphin/ui/playback/PlaybackKeyHandler.kt b/app/src/main/java/com/github/damontecres/wholphin/ui/playback/PlaybackKeyHandler.kt index 65e1d57e..3715f458 100644 --- a/app/src/main/java/com/github/damontecres/wholphin/ui/playback/PlaybackKeyHandler.kt +++ b/app/src/main/java/com/github/damontecres/wholphin/ui/playback/PlaybackKeyHandler.kt @@ -25,6 +25,8 @@ class PlaybackKeyHandler( private val skipBackOnResume: Duration?, private val oneClickPause: Boolean, private val onInteraction: () -> Unit, + private val onStop: () -> Unit, + private val onPlaybackDialogTypeClick: (PlaybackDialogType) -> Unit, ) { fun onKeyEvent(it: KeyEvent): Boolean { if (it.type == KeyEventType.KeyUp) onInteraction.invoke() @@ -34,12 +36,12 @@ class PlaybackKeyHandler( result = false } else if (it.type != KeyEventType.KeyUp) { result = false - } else if (isDirectionalDpad(it) || isEnterKey(it)) { + } else if (isDirectionalDpad(it) || isEnterKey(it) || isControllerMedia(it)) { if (!controllerViewState.controlsVisible) { - if (skipWithLeftRight && it.key == Key.DirectionLeft) { + if (skipWithLeftRight && isSkipBack(it)) { updateSkipIndicator(-seekBack.inWholeMilliseconds) player.seekBack(seekBack) - } else if (skipWithLeftRight && it.key == Key.DirectionRight) { + } else if (skipWithLeftRight && isSkipForward(it)) { player.seekForward(seekForward) updateSkipIndicator(seekForward.inWholeMilliseconds) } else if (oneClickPause && isEnterKey(it)) { @@ -94,11 +96,16 @@ class PlaybackKeyHandler( Key.MediaNext -> if (player.isCommandAvailable(Player.COMMAND_SEEK_TO_NEXT)) player.seekToNext() Key.MediaPrevious -> if (player.isCommandAvailable(Player.COMMAND_SEEK_TO_PREVIOUS)) player.seekToPrevious() + + Key.Captions -> onPlaybackDialogTypeClick.invoke(PlaybackDialogType.CAPTIONS) + Key.MediaAudioTrack -> onPlaybackDialogTypeClick.invoke(PlaybackDialogType.AUDIO) + Key.MediaStop -> onStop.invoke() + else -> result = false } - } else if (it.key == Key.Enter && !controllerViewState.controlsVisible) { + } else if (isEnterKey(it) && !controllerViewState.controlsVisible) { controllerViewState.showControls() - } else if (it.key == Key.Back && controllerViewState.controlsVisible) { + } else if (isBackKey(it) && controllerViewState.controlsVisible) { // TODO change this to a BackHandler? controllerViewState.hideControls() } else { diff --git a/app/src/main/java/com/github/damontecres/wholphin/ui/playback/PlaybackOverlay.kt b/app/src/main/java/com/github/damontecres/wholphin/ui/playback/PlaybackOverlay.kt index 4c03905e..eea34233 100644 --- a/app/src/main/java/com/github/damontecres/wholphin/ui/playback/PlaybackOverlay.kt +++ b/app/src/main/java/com/github/damontecres/wholphin/ui/playback/PlaybackOverlay.kt @@ -37,7 +37,6 @@ import androidx.compose.ui.focus.onFocusChanged import androidx.compose.ui.input.key.KeyEventType import androidx.compose.ui.input.key.onPreviewKeyEvent import androidx.compose.ui.input.key.type -import androidx.compose.ui.layout.ContentScale import androidx.compose.ui.layout.onGloballyPositioned import androidx.compose.ui.platform.LocalDensity import androidx.compose.ui.res.stringResource @@ -50,7 +49,6 @@ import coil3.compose.AsyncImage import com.github.damontecres.wholphin.R import com.github.damontecres.wholphin.data.model.BaseItem import com.github.damontecres.wholphin.data.model.Chapter -import com.github.damontecres.wholphin.data.model.ItemPlayback import com.github.damontecres.wholphin.data.model.Playlist import com.github.damontecres.wholphin.data.model.aspectRatioFloat import com.github.damontecres.wholphin.ui.AppColors @@ -79,7 +77,6 @@ private val subtitleTextSize = 18.sp @Composable fun PlaybackOverlay( item: BaseItem?, - subtitleStreams: List, chapters: List, playerControls: Player, controllerViewState: ControllerViewState, @@ -92,14 +89,10 @@ fun PlaybackOverlay( skipBackOnResume: Duration?, seekForward: Duration, onPlaybackActionClick: (PlaybackAction) -> Unit, + onClickPlaybackDialogType: (PlaybackDialogType) -> Unit, onSeekBarChange: (Long) -> Unit, showDebugInfo: Boolean, - scale: ContentScale, - playbackSpeed: Float, - moreButtonOptions: MoreButtonOptions, currentPlayback: CurrentPlayback?, - currentItemPlayback: ItemPlayback, - audioStreams: List, currentSegment: MediaSegmentDto?, modifier: Modifier = Modifier, trickplayInfo: TrickplayInfo? = null, @@ -151,7 +144,6 @@ fun PlaybackOverlay( Controller( title = item?.title, subtitle = item?.subtitleLong, - subtitleStreams = subtitleStreams, playerControls = playerControls, controllerViewState = controllerViewState, showPlay = showPlay, @@ -163,17 +155,11 @@ fun PlaybackOverlay( skipBackOnResume = skipBackOnResume, seekForward = seekForward, onPlaybackActionClick = onPlaybackActionClick, + onClickPlaybackDialogType = onClickPlaybackDialogType, onSeekProgress = { onSeekBarChange(it) seekProgressMs = it }, - showDebugInfo = showDebugInfo, - scale = scale, - playbackSpeed = playbackSpeed, - moreButtonOptions = moreButtonOptions, - currentPlayback = currentPlayback, - currentItemPlayback = currentItemPlayback, - audioStreams = audioStreams, seekBarInteractionSource = seekBarInteractionSource, nextState = nextState, onNextStateFocus = { @@ -442,7 +428,6 @@ enum class OverlayViewState { @Composable fun Controller( title: String?, - subtitleStreams: List, playerControls: Player, controllerViewState: ControllerViewState, showClock: Boolean, @@ -454,14 +439,8 @@ fun Controller( skipBackOnResume: Duration?, seekForward: Duration, onPlaybackActionClick: (PlaybackAction) -> Unit, + onClickPlaybackDialogType: (PlaybackDialogType) -> Unit, onSeekProgress: (Long) -> Unit, - showDebugInfo: Boolean, - scale: ContentScale, - playbackSpeed: Float, - moreButtonOptions: MoreButtonOptions, - currentPlayback: CurrentPlayback?, - currentItemPlayback: ItemPlayback, - audioStreams: List, nextState: OverlayViewState?, currentSegment: MediaSegmentDto?, modifier: Modifier = Modifier, @@ -520,13 +499,15 @@ fun Controller( } } } + // TODO need to move these up a level? + val moreFocusRequester = remember { FocusRequester() } + val captionFocusRequester = remember { FocusRequester() } + val settingsFocusRequester = remember { FocusRequester() } PlaybackControls( modifier = Modifier.fillMaxWidth(), - subtitleStreams = subtitleStreams, playerControls = playerControls, onPlaybackActionClick = onPlaybackActionClick, controllerViewState = controllerViewState, - showDebugInfo = showDebugInfo, onSeekProgress = { onSeekProgress(it) }, @@ -535,17 +516,15 @@ fun Controller( nextEnabled = nextEnabled, seekEnabled = seekEnabled, seekBarInteractionSource = seekBarInteractionSource, - moreButtonOptions = moreButtonOptions, - subtitleIndex = currentItemPlayback.subtitleIndex, - audioIndex = currentItemPlayback.audioIndex, - audioStreams = audioStreams, - playbackSpeed = playbackSpeed, - scale = scale, seekBarIntervals = 16, seekBack = seekBack, seekForward = seekForward, skipBackOnResume = skipBackOnResume, currentSegment = currentSegment, + onClickPlaybackDialogType = onClickPlaybackDialogType, + moreFocusRequester = moreFocusRequester, + captionFocusRequester = captionFocusRequester, + settingsFocusRequester = settingsFocusRequester, ) when (nextState) { OverlayViewState.CHAPTERS -> diff --git a/app/src/main/java/com/github/damontecres/wholphin/ui/playback/PlaybackPage.kt b/app/src/main/java/com/github/damontecres/wholphin/ui/playback/PlaybackPage.kt index a611c9a5..6f895305 100644 --- a/app/src/main/java/com/github/damontecres/wholphin/ui/playback/PlaybackPage.kt +++ b/app/src/main/java/com/github/damontecres/wholphin/ui/playback/PlaybackPage.kt @@ -142,6 +142,8 @@ fun PlaybackPage( val subtitleSearch by viewModel.subtitleSearch.observeAsState(null) val subtitleSearchLanguage by viewModel.subtitleSearchLanguage.observeAsState(Locale.current.language) + var playbackDialog by remember { mutableStateOf(null) } + OneTimeLaunchedEffect { if (prefs.playerBackend == PlayerBackend.MPV) { scope.launch(Dispatchers.Main + ExceptionHandler()) { @@ -202,8 +204,58 @@ fun PlaybackPage( skipBackOnResume = preferences.appPreferences.playbackPreferences.skipBackOnResume, onInteraction = viewModel::reportInteraction, oneClickPause = preferences.appPreferences.playbackPreferences.oneClickPause, + onStop = { + player.stop() + viewModel.navigationManager.goBack() + }, + onPlaybackDialogTypeClick = { playbackDialog = it }, ) + val onPlaybackActionClick: (PlaybackAction) -> Unit = { + when (it) { + is PlaybackAction.PlaybackSpeed -> { + playbackSpeed = it.value + } + + is PlaybackAction.Scale -> { + contentScale = it.scale + } + + PlaybackAction.ShowDebug -> { + showDebugInfo = !showDebugInfo + } + + PlaybackAction.ShowPlaylist -> TODO() + PlaybackAction.ShowVideoFilterDialog -> TODO() + is PlaybackAction.ToggleAudio -> { + viewModel.changeAudioStream(it.index) + } + + is PlaybackAction.ToggleCaptions -> { + viewModel.changeSubtitleStream(it.index) + } + + PlaybackAction.SearchCaptions -> { + controllerViewState.hideControls() + viewModel.searchForSubtitles() + } + + PlaybackAction.Next -> { + // TODO focus is lost + viewModel.playNextUp() + } + + PlaybackAction.Previous -> { + val pos = player.currentPosition + if (pos < player.maxSeekToPreviousPosition && playlist.hasPrevious()) { + viewModel.playPrevious() + } else { + player.seekToPrevious() + } + } + } + } + val showSegment = !segmentCancelled && currentSegment != null && nextUp == null && !controllerViewState.controlsVisible && skipIndicatorDuration == 0L @@ -320,59 +372,11 @@ fun PlaybackPage( seekForward = preferences.appPreferences.playbackPreferences.skipForwardMs.milliseconds, seekBack = preferences.appPreferences.playbackPreferences.skipBackMs.milliseconds, skipBackOnResume = preferences.appPreferences.playbackPreferences.skipBackOnResume, - onPlaybackActionClick = { - when (it) { - is PlaybackAction.PlaybackSpeed -> { - playbackSpeed = it.value - } - - is PlaybackAction.Scale -> { - contentScale = it.scale - } - - PlaybackAction.ShowDebug -> { - showDebugInfo = !showDebugInfo - } - - PlaybackAction.ShowPlaylist -> TODO() - PlaybackAction.ShowVideoFilterDialog -> TODO() - is PlaybackAction.ToggleAudio -> { - viewModel.changeAudioStream(it.index) - } - - is PlaybackAction.ToggleCaptions -> { - viewModel.changeSubtitleStream(it.index) - } - - PlaybackAction.SearchCaptions -> { - controllerViewState.hideControls() - viewModel.searchForSubtitles() - } - - PlaybackAction.Next -> { - // TODO focus is lost - viewModel.playNextUp() - } - - PlaybackAction.Previous -> { - val pos = player.currentPosition - if (pos < player.maxSeekToPreviousPosition && playlist.hasPrevious()) { - viewModel.playPrevious() - } else { - player.seekToPrevious() - } - } - } - }, + onPlaybackActionClick = onPlaybackActionClick, + onClickPlaybackDialogType = { playbackDialog = it }, onSeekBarChange = seekBarState::onValueChange, showDebugInfo = showDebugInfo, - scale = contentScale, - playbackSpeed = playbackSpeed, - moreButtonOptions = MoreButtonOptions(mapOf()), currentPlayback = currentPlayback, - currentItemPlayback = currentItemPlayback, - audioStreams = mediaInfo?.audioStreams ?: listOf(), - subtitleStreams = mediaInfo?.subtitleStreams ?: listOf(), chapters = mediaInfo?.chapters ?: listOf(), trickplayInfo = mediaInfo?.trickPlayInfo, trickplayUrlFor = viewModel::getTrickplayUrl, @@ -524,6 +528,33 @@ fun PlaybackPage( ) } } + + playbackDialog?.let { type -> + PlaybackDialog( + type = type, + settings = + PlaybackSettings( + showDebugInfo = showDebugInfo, + audioIndex = currentItemPlayback?.audioIndex, + audioStreams = mediaInfo?.audioStreams.orEmpty(), + subtitleIndex = currentItemPlayback?.subtitleIndex, + subtitleStreams = mediaInfo?.subtitleStreams.orEmpty(), + playbackSpeed = playbackSpeed, + contentScale = contentScale, + ), + onDismissRequest = { + playbackDialog = null + if (controllerViewState.controlsVisible) { + controllerViewState.pulseControls() + } + }, + onControllerInteraction = { + controllerViewState.pulseControls(Long.MAX_VALUE) + }, + onClickPlaybackDialogType = { playbackDialog = it }, + onPlaybackActionClick = onPlaybackActionClick, + ) + } } } }