mirror of
https://github.com/JustinZeus/Wholphin.git
synced 2026-07-09 08:01:20 +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
|
<activity
|
||||||
android:name=".MainActivity"
|
android:name=".MainActivity"
|
||||||
android:exported="true"
|
android:exported="true"
|
||||||
android:launchMode="singleTask">
|
android:launchMode="singleTask"
|
||||||
|
android:configChanges="keyboard|keyboardHidden|navigation|orientation|screenSize|screenLayout">
|
||||||
<intent-filter>
|
<intent-filter>
|
||||||
<action android:name="android.intent.action.MAIN" />
|
<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 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 =
|
fun isMedia(event: KeyEvent): Boolean =
|
||||||
event.key == Key.MediaPlay ||
|
event.key == Key.MediaPlay ||
|
||||||
|
|
@ -31,7 +51,10 @@ fun isMedia(event: KeyEvent): Boolean =
|
||||||
event.key == Key.MediaRewind ||
|
event.key == Key.MediaRewind ||
|
||||||
event.key == Key.MediaSkipBackward ||
|
event.key == Key.MediaSkipBackward ||
|
||||||
event.key == Key.MediaNext ||
|
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 =
|
fun isBackwardButton(event: KeyEvent): Boolean =
|
||||||
event.key == Key.PageUp ||
|
event.key == Key.PageUp ||
|
||||||
|
|
|
||||||
|
|
@ -3,6 +3,8 @@ package com.github.damontecres.wholphin.ui.playback
|
||||||
import androidx.compose.ui.layout.ContentScale
|
import androidx.compose.ui.layout.ContentScale
|
||||||
import com.github.damontecres.wholphin.preferences.PrefContentScale
|
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 =
|
val playbackScaleOptions =
|
||||||
mapOf(
|
mapOf(
|
||||||
ContentScale.Fit to "Fit",
|
ContentScale.Fit to "Fit",
|
||||||
|
|
|
||||||
|
|
@ -32,7 +32,6 @@ import androidx.compose.runtime.LaunchedEffect
|
||||||
import androidx.compose.runtime.getValue
|
import androidx.compose.runtime.getValue
|
||||||
import androidx.compose.runtime.mutableFloatStateOf
|
import androidx.compose.runtime.mutableFloatStateOf
|
||||||
import androidx.compose.runtime.mutableLongStateOf
|
import androidx.compose.runtime.mutableLongStateOf
|
||||||
import androidx.compose.runtime.mutableStateOf
|
|
||||||
import androidx.compose.runtime.remember
|
import androidx.compose.runtime.remember
|
||||||
import androidx.compose.runtime.rememberCoroutineScope
|
import androidx.compose.runtime.rememberCoroutineScope
|
||||||
import androidx.compose.runtime.setValue
|
import androidx.compose.runtime.setValue
|
||||||
|
|
@ -60,9 +59,7 @@ import androidx.tv.material3.ListItem
|
||||||
import androidx.tv.material3.MaterialTheme
|
import androidx.tv.material3.MaterialTheme
|
||||||
import androidx.tv.material3.Text
|
import androidx.tv.material3.Text
|
||||||
import com.github.damontecres.wholphin.R
|
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.AppColors
|
||||||
import com.github.damontecres.wholphin.ui.indexOfFirstOrNull
|
|
||||||
import com.github.damontecres.wholphin.ui.seekBack
|
import com.github.damontecres.wholphin.ui.seekBack
|
||||||
import com.github.damontecres.wholphin.ui.seekForward
|
import com.github.damontecres.wholphin.ui.seekForward
|
||||||
import com.github.damontecres.wholphin.ui.stringRes
|
import com.github.damontecres.wholphin.ui.stringRes
|
||||||
|
|
@ -73,7 +70,6 @@ import kotlinx.coroutines.isActive
|
||||||
import kotlinx.coroutines.launch
|
import kotlinx.coroutines.launch
|
||||||
import org.jellyfin.sdk.model.api.MediaSegmentDto
|
import org.jellyfin.sdk.model.api.MediaSegmentDto
|
||||||
import org.jellyfin.sdk.model.extensions.ticks
|
import org.jellyfin.sdk.model.extensions.ticks
|
||||||
import timber.log.Timber
|
|
||||||
import kotlin.time.Duration
|
import kotlin.time.Duration
|
||||||
import kotlin.time.Duration.Companion.seconds
|
import kotlin.time.Duration.Companion.seconds
|
||||||
|
|
||||||
|
|
@ -110,22 +106,18 @@ sealed interface PlaybackAction {
|
||||||
@OptIn(UnstableApi::class)
|
@OptIn(UnstableApi::class)
|
||||||
@Composable
|
@Composable
|
||||||
fun PlaybackControls(
|
fun PlaybackControls(
|
||||||
subtitleStreams: List<SubtitleStream>,
|
|
||||||
playerControls: Player,
|
playerControls: Player,
|
||||||
controllerViewState: ControllerViewState,
|
controllerViewState: ControllerViewState,
|
||||||
onPlaybackActionClick: (PlaybackAction) -> Unit,
|
onPlaybackActionClick: (PlaybackAction) -> Unit,
|
||||||
showDebugInfo: Boolean,
|
onClickPlaybackDialogType: (PlaybackDialogType) -> Unit,
|
||||||
|
moreFocusRequester: FocusRequester,
|
||||||
|
captionFocusRequester: FocusRequester,
|
||||||
|
settingsFocusRequester: FocusRequester,
|
||||||
onSeekProgress: (Long) -> Unit,
|
onSeekProgress: (Long) -> Unit,
|
||||||
showPlay: Boolean,
|
showPlay: Boolean,
|
||||||
previousEnabled: Boolean,
|
previousEnabled: Boolean,
|
||||||
nextEnabled: Boolean,
|
nextEnabled: Boolean,
|
||||||
seekEnabled: Boolean,
|
seekEnabled: Boolean,
|
||||||
moreButtonOptions: MoreButtonOptions,
|
|
||||||
subtitleIndex: Int?,
|
|
||||||
audioIndex: Int?,
|
|
||||||
audioStreams: List<AudioStream>,
|
|
||||||
playbackSpeed: Float,
|
|
||||||
scale: ContentScale,
|
|
||||||
seekBarIntervals: Int,
|
seekBarIntervals: Int,
|
||||||
seekBack: Duration,
|
seekBack: Duration,
|
||||||
skipBackOnResume: Duration?,
|
skipBackOnResume: Duration?,
|
||||||
|
|
@ -144,12 +136,6 @@ fun PlaybackControls(
|
||||||
}
|
}
|
||||||
controllerViewState.pulseControls()
|
controllerViewState.pulseControls()
|
||||||
}
|
}
|
||||||
val onControllerInteractionForDialog = {
|
|
||||||
scope.launch(ExceptionHandler()) {
|
|
||||||
bringIntoViewRequester.bringIntoView()
|
|
||||||
}
|
|
||||||
controllerViewState.pulseControls(Long.MAX_VALUE)
|
|
||||||
}
|
|
||||||
LaunchedEffect(controllerViewState.controlsVisible) {
|
LaunchedEffect(controllerViewState.controlsVisible) {
|
||||||
if (controllerViewState.controlsVisible) {
|
if (controllerViewState.controlsVisible) {
|
||||||
initialFocusRequester.tryRequestFocus()
|
initialFocusRequester.tryRequestFocus()
|
||||||
|
|
@ -181,10 +167,9 @@ fun PlaybackControls(
|
||||||
.fillMaxWidth(),
|
.fillMaxWidth(),
|
||||||
) {
|
) {
|
||||||
LeftPlaybackButtons(
|
LeftPlaybackButtons(
|
||||||
|
moreFocusRequester = moreFocusRequester,
|
||||||
onControllerInteraction = onControllerInteraction,
|
onControllerInteraction = onControllerInteraction,
|
||||||
onPlaybackActionClick = onPlaybackActionClick,
|
onClickPlaybackDialogType = onClickPlaybackDialogType,
|
||||||
showDebugInfo = showDebugInfo,
|
|
||||||
moreButtonOptions = moreButtonOptions,
|
|
||||||
modifier = Modifier.align(Alignment.CenterStart),
|
modifier = Modifier.align(Alignment.CenterStart),
|
||||||
)
|
)
|
||||||
PlaybackButtons(
|
PlaybackButtons(
|
||||||
|
|
@ -219,15 +204,10 @@ fun PlaybackControls(
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
RightPlaybackButtons(
|
RightPlaybackButtons(
|
||||||
subtitleStreams = subtitleStreams,
|
captionFocusRequester = captionFocusRequester,
|
||||||
|
settingsFocusRequester = settingsFocusRequester,
|
||||||
onControllerInteraction = onControllerInteraction,
|
onControllerInteraction = onControllerInteraction,
|
||||||
onControllerInteractionForDialog = onControllerInteractionForDialog,
|
onClickPlaybackDialogType = onClickPlaybackDialogType,
|
||||||
onPlaybackActionClick = onPlaybackActionClick,
|
|
||||||
subtitleIndex = subtitleIndex,
|
|
||||||
audioStreams = audioStreams,
|
|
||||||
audioIndex = audioIndex,
|
|
||||||
playbackSpeed = playbackSpeed,
|
|
||||||
scale = scale,
|
|
||||||
modifier = Modifier,
|
modifier = Modifier,
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
@ -307,14 +287,11 @@ private val buttonSpacing = 4.dp
|
||||||
|
|
||||||
@Composable
|
@Composable
|
||||||
fun LeftPlaybackButtons(
|
fun LeftPlaybackButtons(
|
||||||
|
moreFocusRequester: FocusRequester,
|
||||||
onControllerInteraction: () -> Unit,
|
onControllerInteraction: () -> Unit,
|
||||||
onPlaybackActionClick: (PlaybackAction) -> Unit,
|
onClickPlaybackDialogType: (PlaybackDialogType) -> Unit,
|
||||||
showDebugInfo: Boolean,
|
|
||||||
moreButtonOptions: MoreButtonOptions,
|
|
||||||
modifier: Modifier = Modifier,
|
modifier: Modifier = Modifier,
|
||||||
) {
|
) {
|
||||||
var showMoreOptions by remember { mutableStateOf(false) }
|
|
||||||
val focusRequester = remember { FocusRequester() }
|
|
||||||
Row(
|
Row(
|
||||||
modifier = modifier.focusGroup(),
|
modifier = modifier.focusGroup(),
|
||||||
horizontalArrangement = Arrangement.spacedBy(buttonSpacing),
|
horizontalArrangement = Arrangement.spacedBy(buttonSpacing),
|
||||||
|
|
@ -324,58 +301,23 @@ fun LeftPlaybackButtons(
|
||||||
iconRes = R.drawable.baseline_more_vert_96,
|
iconRes = R.drawable.baseline_more_vert_96,
|
||||||
onClick = {
|
onClick = {
|
||||||
onControllerInteraction.invoke()
|
onControllerInteraction.invoke()
|
||||||
showMoreOptions = true
|
onClickPlaybackDialogType.invoke(PlaybackDialogType.MORE)
|
||||||
},
|
},
|
||||||
enabled = true,
|
enabled = true,
|
||||||
onControllerInteraction = onControllerInteraction,
|
onControllerInteraction = onControllerInteraction,
|
||||||
modifier = Modifier.focusRequester(focusRequester),
|
modifier = Modifier.focusRequester(moreFocusRequester),
|
||||||
)
|
|
||||||
}
|
|
||||||
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,
|
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private val speedOptions = listOf(".25", ".5", ".75", "1.0", "1.25", "1.5", "1.75", "2.0")
|
|
||||||
|
|
||||||
@Composable
|
@Composable
|
||||||
fun RightPlaybackButtons(
|
fun RightPlaybackButtons(
|
||||||
subtitleStreams: List<SubtitleStream>,
|
captionFocusRequester: FocusRequester,
|
||||||
|
settingsFocusRequester: FocusRequester,
|
||||||
onControllerInteraction: () -> Unit,
|
onControllerInteraction: () -> Unit,
|
||||||
onControllerInteractionForDialog: () -> Unit,
|
onClickPlaybackDialogType: (PlaybackDialogType) -> Unit,
|
||||||
onPlaybackActionClick: (PlaybackAction) -> Unit,
|
|
||||||
subtitleIndex: Int?,
|
|
||||||
audioStreams: List<AudioStream>,
|
|
||||||
audioIndex: Int?,
|
|
||||||
playbackSpeed: Float,
|
|
||||||
scale: ContentScale,
|
|
||||||
modifier: Modifier = Modifier,
|
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(
|
Row(
|
||||||
modifier = modifier.focusGroup(),
|
modifier = modifier.focusGroup(),
|
||||||
horizontalArrangement = Arrangement.spacedBy(buttonSpacing),
|
horizontalArrangement = Arrangement.spacedBy(buttonSpacing),
|
||||||
|
|
@ -385,8 +327,8 @@ fun RightPlaybackButtons(
|
||||||
enabled = true,
|
enabled = true,
|
||||||
iconRes = R.drawable.captions_svgrepo_com,
|
iconRes = R.drawable.captions_svgrepo_com,
|
||||||
onClick = {
|
onClick = {
|
||||||
onControllerInteractionForDialog.invoke()
|
onControllerInteraction.invoke()
|
||||||
showCaptionDialog = true
|
onClickPlaybackDialogType.invoke(PlaybackDialogType.CAPTIONS)
|
||||||
},
|
},
|
||||||
onControllerInteraction = onControllerInteraction,
|
onControllerInteraction = onControllerInteraction,
|
||||||
modifier = Modifier.focusRequester(captionFocusRequester),
|
modifier = Modifier.focusRequester(captionFocusRequester),
|
||||||
|
|
@ -395,131 +337,14 @@ fun RightPlaybackButtons(
|
||||||
PlaybackButton(
|
PlaybackButton(
|
||||||
iconRes = R.drawable.vector_settings,
|
iconRes = R.drawable.vector_settings,
|
||||||
onClick = {
|
onClick = {
|
||||||
onControllerInteractionForDialog.invoke()
|
onControllerInteraction.invoke()
|
||||||
showOptionsDialog = true
|
onClickPlaybackDialogType.invoke(PlaybackDialogType.SETTINGS)
|
||||||
},
|
},
|
||||||
enabled = true,
|
enabled = true,
|
||||||
onControllerInteraction = onControllerInteraction,
|
onControllerInteraction = onControllerInteraction,
|
||||||
modifier = Modifier.focusRequester(settingsFocusRequester),
|
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)
|
@OptIn(UnstableApi::class)
|
||||||
|
|
@ -629,7 +454,7 @@ fun PlaybackButton(
|
||||||
}
|
}
|
||||||
|
|
||||||
@Composable
|
@Composable
|
||||||
private fun BottomDialog(
|
fun BottomDialog(
|
||||||
choices: List<String>,
|
choices: List<String>,
|
||||||
onDismissRequest: () -> Unit,
|
onDismissRequest: () -> Unit,
|
||||||
onSelectChoice: (Int, String) -> 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 skipBackOnResume: Duration?,
|
||||||
private val oneClickPause: Boolean,
|
private val oneClickPause: Boolean,
|
||||||
private val onInteraction: () -> Unit,
|
private val onInteraction: () -> Unit,
|
||||||
|
private val onStop: () -> Unit,
|
||||||
|
private val onPlaybackDialogTypeClick: (PlaybackDialogType) -> Unit,
|
||||||
) {
|
) {
|
||||||
fun onKeyEvent(it: KeyEvent): Boolean {
|
fun onKeyEvent(it: KeyEvent): Boolean {
|
||||||
if (it.type == KeyEventType.KeyUp) onInteraction.invoke()
|
if (it.type == KeyEventType.KeyUp) onInteraction.invoke()
|
||||||
|
|
@ -34,12 +36,12 @@ class PlaybackKeyHandler(
|
||||||
result = false
|
result = false
|
||||||
} else if (it.type != KeyEventType.KeyUp) {
|
} else if (it.type != KeyEventType.KeyUp) {
|
||||||
result = false
|
result = false
|
||||||
} else if (isDirectionalDpad(it) || isEnterKey(it)) {
|
} else if (isDirectionalDpad(it) || isEnterKey(it) || isControllerMedia(it)) {
|
||||||
if (!controllerViewState.controlsVisible) {
|
if (!controllerViewState.controlsVisible) {
|
||||||
if (skipWithLeftRight && it.key == Key.DirectionLeft) {
|
if (skipWithLeftRight && isSkipBack(it)) {
|
||||||
updateSkipIndicator(-seekBack.inWholeMilliseconds)
|
updateSkipIndicator(-seekBack.inWholeMilliseconds)
|
||||||
player.seekBack(seekBack)
|
player.seekBack(seekBack)
|
||||||
} else if (skipWithLeftRight && it.key == Key.DirectionRight) {
|
} else if (skipWithLeftRight && isSkipForward(it)) {
|
||||||
player.seekForward(seekForward)
|
player.seekForward(seekForward)
|
||||||
updateSkipIndicator(seekForward.inWholeMilliseconds)
|
updateSkipIndicator(seekForward.inWholeMilliseconds)
|
||||||
} else if (oneClickPause && isEnterKey(it)) {
|
} else if (oneClickPause && isEnterKey(it)) {
|
||||||
|
|
@ -94,11 +96,16 @@ class PlaybackKeyHandler(
|
||||||
|
|
||||||
Key.MediaNext -> if (player.isCommandAvailable(Player.COMMAND_SEEK_TO_NEXT)) player.seekToNext()
|
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.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 -> result = false
|
||||||
}
|
}
|
||||||
} else if (it.key == Key.Enter && !controllerViewState.controlsVisible) {
|
} else if (isEnterKey(it) && !controllerViewState.controlsVisible) {
|
||||||
controllerViewState.showControls()
|
controllerViewState.showControls()
|
||||||
} else if (it.key == Key.Back && controllerViewState.controlsVisible) {
|
} else if (isBackKey(it) && controllerViewState.controlsVisible) {
|
||||||
// TODO change this to a BackHandler?
|
// TODO change this to a BackHandler?
|
||||||
controllerViewState.hideControls()
|
controllerViewState.hideControls()
|
||||||
} else {
|
} else {
|
||||||
|
|
|
||||||
|
|
@ -37,7 +37,6 @@ import androidx.compose.ui.focus.onFocusChanged
|
||||||
import androidx.compose.ui.input.key.KeyEventType
|
import androidx.compose.ui.input.key.KeyEventType
|
||||||
import androidx.compose.ui.input.key.onPreviewKeyEvent
|
import androidx.compose.ui.input.key.onPreviewKeyEvent
|
||||||
import androidx.compose.ui.input.key.type
|
import androidx.compose.ui.input.key.type
|
||||||
import androidx.compose.ui.layout.ContentScale
|
|
||||||
import androidx.compose.ui.layout.onGloballyPositioned
|
import androidx.compose.ui.layout.onGloballyPositioned
|
||||||
import androidx.compose.ui.platform.LocalDensity
|
import androidx.compose.ui.platform.LocalDensity
|
||||||
import androidx.compose.ui.res.stringResource
|
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.R
|
||||||
import com.github.damontecres.wholphin.data.model.BaseItem
|
import com.github.damontecres.wholphin.data.model.BaseItem
|
||||||
import com.github.damontecres.wholphin.data.model.Chapter
|
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.Playlist
|
||||||
import com.github.damontecres.wholphin.data.model.aspectRatioFloat
|
import com.github.damontecres.wholphin.data.model.aspectRatioFloat
|
||||||
import com.github.damontecres.wholphin.ui.AppColors
|
import com.github.damontecres.wholphin.ui.AppColors
|
||||||
|
|
@ -79,7 +77,6 @@ private val subtitleTextSize = 18.sp
|
||||||
@Composable
|
@Composable
|
||||||
fun PlaybackOverlay(
|
fun PlaybackOverlay(
|
||||||
item: BaseItem?,
|
item: BaseItem?,
|
||||||
subtitleStreams: List<SubtitleStream>,
|
|
||||||
chapters: List<Chapter>,
|
chapters: List<Chapter>,
|
||||||
playerControls: Player,
|
playerControls: Player,
|
||||||
controllerViewState: ControllerViewState,
|
controllerViewState: ControllerViewState,
|
||||||
|
|
@ -92,14 +89,10 @@ fun PlaybackOverlay(
|
||||||
skipBackOnResume: Duration?,
|
skipBackOnResume: Duration?,
|
||||||
seekForward: Duration,
|
seekForward: Duration,
|
||||||
onPlaybackActionClick: (PlaybackAction) -> Unit,
|
onPlaybackActionClick: (PlaybackAction) -> Unit,
|
||||||
|
onClickPlaybackDialogType: (PlaybackDialogType) -> Unit,
|
||||||
onSeekBarChange: (Long) -> Unit,
|
onSeekBarChange: (Long) -> Unit,
|
||||||
showDebugInfo: Boolean,
|
showDebugInfo: Boolean,
|
||||||
scale: ContentScale,
|
|
||||||
playbackSpeed: Float,
|
|
||||||
moreButtonOptions: MoreButtonOptions,
|
|
||||||
currentPlayback: CurrentPlayback?,
|
currentPlayback: CurrentPlayback?,
|
||||||
currentItemPlayback: ItemPlayback,
|
|
||||||
audioStreams: List<AudioStream>,
|
|
||||||
currentSegment: MediaSegmentDto?,
|
currentSegment: MediaSegmentDto?,
|
||||||
modifier: Modifier = Modifier,
|
modifier: Modifier = Modifier,
|
||||||
trickplayInfo: TrickplayInfo? = null,
|
trickplayInfo: TrickplayInfo? = null,
|
||||||
|
|
@ -151,7 +144,6 @@ fun PlaybackOverlay(
|
||||||
Controller(
|
Controller(
|
||||||
title = item?.title,
|
title = item?.title,
|
||||||
subtitle = item?.subtitleLong,
|
subtitle = item?.subtitleLong,
|
||||||
subtitleStreams = subtitleStreams,
|
|
||||||
playerControls = playerControls,
|
playerControls = playerControls,
|
||||||
controllerViewState = controllerViewState,
|
controllerViewState = controllerViewState,
|
||||||
showPlay = showPlay,
|
showPlay = showPlay,
|
||||||
|
|
@ -163,17 +155,11 @@ fun PlaybackOverlay(
|
||||||
skipBackOnResume = skipBackOnResume,
|
skipBackOnResume = skipBackOnResume,
|
||||||
seekForward = seekForward,
|
seekForward = seekForward,
|
||||||
onPlaybackActionClick = onPlaybackActionClick,
|
onPlaybackActionClick = onPlaybackActionClick,
|
||||||
|
onClickPlaybackDialogType = onClickPlaybackDialogType,
|
||||||
onSeekProgress = {
|
onSeekProgress = {
|
||||||
onSeekBarChange(it)
|
onSeekBarChange(it)
|
||||||
seekProgressMs = it
|
seekProgressMs = it
|
||||||
},
|
},
|
||||||
showDebugInfo = showDebugInfo,
|
|
||||||
scale = scale,
|
|
||||||
playbackSpeed = playbackSpeed,
|
|
||||||
moreButtonOptions = moreButtonOptions,
|
|
||||||
currentPlayback = currentPlayback,
|
|
||||||
currentItemPlayback = currentItemPlayback,
|
|
||||||
audioStreams = audioStreams,
|
|
||||||
seekBarInteractionSource = seekBarInteractionSource,
|
seekBarInteractionSource = seekBarInteractionSource,
|
||||||
nextState = nextState,
|
nextState = nextState,
|
||||||
onNextStateFocus = {
|
onNextStateFocus = {
|
||||||
|
|
@ -442,7 +428,6 @@ enum class OverlayViewState {
|
||||||
@Composable
|
@Composable
|
||||||
fun Controller(
|
fun Controller(
|
||||||
title: String?,
|
title: String?,
|
||||||
subtitleStreams: List<SubtitleStream>,
|
|
||||||
playerControls: Player,
|
playerControls: Player,
|
||||||
controllerViewState: ControllerViewState,
|
controllerViewState: ControllerViewState,
|
||||||
showClock: Boolean,
|
showClock: Boolean,
|
||||||
|
|
@ -454,14 +439,8 @@ fun Controller(
|
||||||
skipBackOnResume: Duration?,
|
skipBackOnResume: Duration?,
|
||||||
seekForward: Duration,
|
seekForward: Duration,
|
||||||
onPlaybackActionClick: (PlaybackAction) -> Unit,
|
onPlaybackActionClick: (PlaybackAction) -> Unit,
|
||||||
|
onClickPlaybackDialogType: (PlaybackDialogType) -> Unit,
|
||||||
onSeekProgress: (Long) -> Unit,
|
onSeekProgress: (Long) -> Unit,
|
||||||
showDebugInfo: Boolean,
|
|
||||||
scale: ContentScale,
|
|
||||||
playbackSpeed: Float,
|
|
||||||
moreButtonOptions: MoreButtonOptions,
|
|
||||||
currentPlayback: CurrentPlayback?,
|
|
||||||
currentItemPlayback: ItemPlayback,
|
|
||||||
audioStreams: List<AudioStream>,
|
|
||||||
nextState: OverlayViewState?,
|
nextState: OverlayViewState?,
|
||||||
currentSegment: MediaSegmentDto?,
|
currentSegment: MediaSegmentDto?,
|
||||||
modifier: Modifier = Modifier,
|
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(
|
PlaybackControls(
|
||||||
modifier = Modifier.fillMaxWidth(),
|
modifier = Modifier.fillMaxWidth(),
|
||||||
subtitleStreams = subtitleStreams,
|
|
||||||
playerControls = playerControls,
|
playerControls = playerControls,
|
||||||
onPlaybackActionClick = onPlaybackActionClick,
|
onPlaybackActionClick = onPlaybackActionClick,
|
||||||
controllerViewState = controllerViewState,
|
controllerViewState = controllerViewState,
|
||||||
showDebugInfo = showDebugInfo,
|
|
||||||
onSeekProgress = {
|
onSeekProgress = {
|
||||||
onSeekProgress(it)
|
onSeekProgress(it)
|
||||||
},
|
},
|
||||||
|
|
@ -535,17 +516,15 @@ fun Controller(
|
||||||
nextEnabled = nextEnabled,
|
nextEnabled = nextEnabled,
|
||||||
seekEnabled = seekEnabled,
|
seekEnabled = seekEnabled,
|
||||||
seekBarInteractionSource = seekBarInteractionSource,
|
seekBarInteractionSource = seekBarInteractionSource,
|
||||||
moreButtonOptions = moreButtonOptions,
|
|
||||||
subtitleIndex = currentItemPlayback.subtitleIndex,
|
|
||||||
audioIndex = currentItemPlayback.audioIndex,
|
|
||||||
audioStreams = audioStreams,
|
|
||||||
playbackSpeed = playbackSpeed,
|
|
||||||
scale = scale,
|
|
||||||
seekBarIntervals = 16,
|
seekBarIntervals = 16,
|
||||||
seekBack = seekBack,
|
seekBack = seekBack,
|
||||||
seekForward = seekForward,
|
seekForward = seekForward,
|
||||||
skipBackOnResume = skipBackOnResume,
|
skipBackOnResume = skipBackOnResume,
|
||||||
currentSegment = currentSegment,
|
currentSegment = currentSegment,
|
||||||
|
onClickPlaybackDialogType = onClickPlaybackDialogType,
|
||||||
|
moreFocusRequester = moreFocusRequester,
|
||||||
|
captionFocusRequester = captionFocusRequester,
|
||||||
|
settingsFocusRequester = settingsFocusRequester,
|
||||||
)
|
)
|
||||||
when (nextState) {
|
when (nextState) {
|
||||||
OverlayViewState.CHAPTERS ->
|
OverlayViewState.CHAPTERS ->
|
||||||
|
|
|
||||||
|
|
@ -142,6 +142,8 @@ fun PlaybackPage(
|
||||||
val subtitleSearch by viewModel.subtitleSearch.observeAsState(null)
|
val subtitleSearch by viewModel.subtitleSearch.observeAsState(null)
|
||||||
val subtitleSearchLanguage by viewModel.subtitleSearchLanguage.observeAsState(Locale.current.language)
|
val subtitleSearchLanguage by viewModel.subtitleSearchLanguage.observeAsState(Locale.current.language)
|
||||||
|
|
||||||
|
var playbackDialog by remember { mutableStateOf<PlaybackDialogType?>(null) }
|
||||||
|
|
||||||
OneTimeLaunchedEffect {
|
OneTimeLaunchedEffect {
|
||||||
if (prefs.playerBackend == PlayerBackend.MPV) {
|
if (prefs.playerBackend == PlayerBackend.MPV) {
|
||||||
scope.launch(Dispatchers.Main + ExceptionHandler()) {
|
scope.launch(Dispatchers.Main + ExceptionHandler()) {
|
||||||
|
|
@ -202,8 +204,58 @@ fun PlaybackPage(
|
||||||
skipBackOnResume = preferences.appPreferences.playbackPreferences.skipBackOnResume,
|
skipBackOnResume = preferences.appPreferences.playbackPreferences.skipBackOnResume,
|
||||||
onInteraction = viewModel::reportInteraction,
|
onInteraction = viewModel::reportInteraction,
|
||||||
oneClickPause = preferences.appPreferences.playbackPreferences.oneClickPause,
|
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 =
|
val showSegment =
|
||||||
!segmentCancelled && currentSegment != null &&
|
!segmentCancelled && currentSegment != null &&
|
||||||
nextUp == null && !controllerViewState.controlsVisible && skipIndicatorDuration == 0L
|
nextUp == null && !controllerViewState.controlsVisible && skipIndicatorDuration == 0L
|
||||||
|
|
@ -320,59 +372,11 @@ fun PlaybackPage(
|
||||||
seekForward = preferences.appPreferences.playbackPreferences.skipForwardMs.milliseconds,
|
seekForward = preferences.appPreferences.playbackPreferences.skipForwardMs.milliseconds,
|
||||||
seekBack = preferences.appPreferences.playbackPreferences.skipBackMs.milliseconds,
|
seekBack = preferences.appPreferences.playbackPreferences.skipBackMs.milliseconds,
|
||||||
skipBackOnResume = preferences.appPreferences.playbackPreferences.skipBackOnResume,
|
skipBackOnResume = preferences.appPreferences.playbackPreferences.skipBackOnResume,
|
||||||
onPlaybackActionClick = {
|
onPlaybackActionClick = onPlaybackActionClick,
|
||||||
when (it) {
|
onClickPlaybackDialogType = { playbackDialog = 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()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
},
|
|
||||||
onSeekBarChange = seekBarState::onValueChange,
|
onSeekBarChange = seekBarState::onValueChange,
|
||||||
showDebugInfo = showDebugInfo,
|
showDebugInfo = showDebugInfo,
|
||||||
scale = contentScale,
|
|
||||||
playbackSpeed = playbackSpeed,
|
|
||||||
moreButtonOptions = MoreButtonOptions(mapOf()),
|
|
||||||
currentPlayback = currentPlayback,
|
currentPlayback = currentPlayback,
|
||||||
currentItemPlayback = currentItemPlayback,
|
|
||||||
audioStreams = mediaInfo?.audioStreams ?: listOf(),
|
|
||||||
subtitleStreams = mediaInfo?.subtitleStreams ?: listOf(),
|
|
||||||
chapters = mediaInfo?.chapters ?: listOf(),
|
chapters = mediaInfo?.chapters ?: listOf(),
|
||||||
trickplayInfo = mediaInfo?.trickPlayInfo,
|
trickplayInfo = mediaInfo?.trickPlayInfo,
|
||||||
trickplayUrlFor = viewModel::getTrickplayUrl,
|
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