mirror of
https://github.com/JustinZeus/Wholphin.git
synced 2026-07-08 15:50:16 +02:00
Handle more remote & controller buttons during playback (#269)
## Details Adds support for a few more media remote buttons Adds support for common controller buttons ### Dev notes Lifts the playback dialogs to the top level allowing them to be called from anywhere during playback ## Issues Closes #260
This commit is contained in:
parent
d3a3a2093a
commit
1f1d3443e3
8 changed files with 328 additions and 286 deletions
|
|
@ -34,7 +34,8 @@
|
|||
<activity
|
||||
android:name=".MainActivity"
|
||||
android:exported="true"
|
||||
android:launchMode="singleTask">
|
||||
android:launchMode="singleTask"
|
||||
android:configChanges="keyboard|keyboardHidden|navigation|orientation|screenSize|screenLayout">
|
||||
<intent-filter>
|
||||
<action android:name="android.intent.action.MAIN" />
|
||||
|
||||
|
|
|
|||
|
|
@ -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 ||
|
||||
|
|
|
|||
|
|
@ -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",
|
||||
|
|
|
|||
|
|
@ -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<SubtitleStream>,
|
||||
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<AudioStream>,
|
||||
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<SubtitleStream>,
|
||||
captionFocusRequester: FocusRequester,
|
||||
settingsFocusRequester: FocusRequester,
|
||||
onControllerInteraction: () -> Unit,
|
||||
onControllerInteractionForDialog: () -> Unit,
|
||||
onPlaybackActionClick: (PlaybackAction) -> Unit,
|
||||
subtitleIndex: Int?,
|
||||
audioStreams: List<AudioStream>,
|
||||
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<String>,
|
||||
onDismissRequest: () -> Unit,
|
||||
onSelectChoice: (Int, String) -> Unit,
|
||||
|
|
|
|||
|
|
@ -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<AudioStream>,
|
||||
val subtitleIndex: Int?,
|
||||
val subtitleStreams: List<SubtitleStream>,
|
||||
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,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
|
@ -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 {
|
||||
|
|
|
|||
|
|
@ -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<SubtitleStream>,
|
||||
chapters: List<Chapter>,
|
||||
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<AudioStream>,
|
||||
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<SubtitleStream>,
|
||||
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<AudioStream>,
|
||||
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 ->
|
||||
|
|
|
|||
|
|
@ -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<PlaybackDialogType?>(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,
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue