From b94faceb2f6498cf2e9769b4a38176799d222c14 Mon Sep 17 00:00:00 2001
From: Damontecres <154766448+damontecres@users.noreply.github.com>
Date: Wed, 29 Apr 2026 19:45:37 -0400
Subject: [PATCH] Choose backend to play media in context menu (#1325)
## Description
Adds a context menu option to choose the player backend or transcoding
to start playback. This allows for temporarily swapping to another
backend without having to updating the settings.
### Related issues
A couple comments have suggested this, but I don't think there's a
specific feature request
### Testing
Emulator
## Screenshots
## AI or LLM usage
None
---
.../wholphin/ui/components/ContextMenu.kt | 78 ++++++++++++++++---
.../wholphin/ui/nav/DestinationContent.kt | 22 +++++-
.../wholphin/ui/playback/PlaybackViewModel.kt | 5 +-
app/src/main/res/values/strings.xml | 2 +
4 files changed, 91 insertions(+), 16 deletions(-)
diff --git a/app/src/main/java/com/github/damontecres/wholphin/ui/components/ContextMenu.kt b/app/src/main/java/com/github/damontecres/wholphin/ui/components/ContextMenu.kt
index 22fd640f..e2111637 100644
--- a/app/src/main/java/com/github/damontecres/wholphin/ui/components/ContextMenu.kt
+++ b/app/src/main/java/com/github/damontecres/wholphin/ui/components/ContextMenu.kt
@@ -17,12 +17,14 @@ import androidx.compose.runtime.setValue
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.platform.LocalContext
import androidx.compose.ui.res.stringResource
+import androidx.compose.ui.unit.dp
import com.github.damontecres.wholphin.R
import com.github.damontecres.wholphin.data.ChosenStreams
import com.github.damontecres.wholphin.data.model.AudioItem
import com.github.damontecres.wholphin.data.model.BaseItem
import com.github.damontecres.wholphin.data.model.ItemPlayback
import com.github.damontecres.wholphin.data.model.Person
+import com.github.damontecres.wholphin.preferences.PlayerBackend
import com.github.damontecres.wholphin.ui.letNotEmpty
import com.github.damontecres.wholphin.ui.nav.Destination
import com.github.damontecres.wholphin.util.supportedPlayableTypes
@@ -185,6 +187,7 @@ fun ContextMenu(
val context = LocalContext.current
var chooseVersion by remember { mutableStateOf(null) }
var showDeleteDialog by remember { mutableStateOf(false) }
+ var showPlayWithDialog by remember { mutableStateOf(false) }
val dialogItems =
remember(context, item, chosenStreams) {
@@ -211,6 +214,7 @@ fun ContextMenu(
) { idx ->
val source = item.data.mediaSources!![idx]
actions.onChooseVersion.invoke(item, source)
+ onDismissRequest.invoke()
}
},
onChooseTracks = { type ->
@@ -239,6 +243,7 @@ fun ContextMenu(
itemPlayback = chosenStreams?.itemPlayback,
),
)
+ onDismissRequest.invoke()
},
preferredSubtitleLanguage = preferredSubtitleLanguage,
)
@@ -249,6 +254,7 @@ fun ContextMenu(
actions.onClearChosenStreams.invoke(chosenStreams)
},
onClickDelete = { showDeleteDialog = true },
+ onClickPlayWith = { showPlayWithDialog = true },
)
}
DialogPopup(
@@ -281,6 +287,31 @@ fun ContextMenu(
},
)
}
+ if (showPlayWithDialog) {
+ val dialogItems =
+ remember {
+ buildPlayWith(context) { transcode, backend ->
+ onDismissRequest.invoke()
+ actions.navigateTo(
+ Destination.Playback(
+ itemId = item.id,
+ positionMs = item.resumeMs,
+ forceTranscoding = transcode,
+ backend = backend,
+ ),
+ )
+ }
+ }
+ DialogPopup(
+ showDialog = true,
+ title = stringResource(R.string.play_with),
+ dialogItems = dialogItems,
+ onDismissRequest = { showPlayWithDialog = false },
+ dismissOnClick = true,
+ waitToLoad = false,
+ elevation = 3.dp,
+ )
+ }
}
private fun buildContextMenuItems(
@@ -302,6 +333,7 @@ private fun buildContextMenuItems(
onShowOverview: () -> Unit,
onClearChosenStreams: () -> Unit,
onClickDelete: () -> Unit,
+ onClickPlayWith: () -> Unit,
): List =
buildList {
if (showGoTo) {
@@ -316,7 +348,7 @@ private fun buildContextMenuItems(
)
}
if (item.type in supportedPlayableTypes) {
- if (item.playbackPosition >= Duration.ZERO) {
+ if (item.playbackPosition > Duration.ZERO) {
add(
DialogItem(
context.getString(R.string.resume),
@@ -566,18 +598,11 @@ private fun buildContextMenuItems(
if (item.type in supportedPlayableTypes) {
add(
DialogItem(
- context.getString(R.string.play_with_transcoding),
+ context.getString(R.string.play_with),
Icons.Default.PlayArrow,
- dismissOnClick = true,
- ) {
- actions.navigateTo(
- Destination.Playback(
- item.id,
- item.resumeMs ?: 0L,
- forceTranscoding = true,
- ),
- )
- },
+ dismissOnClick = false,
+ onClick = onClickPlayWith,
+ ),
)
}
if (item.data.mediaSources?.isNotEmpty() == true) {
@@ -593,6 +618,35 @@ private fun buildContextMenuItems(
}
}
+private fun buildPlayWith(
+ context: Context,
+ onClick: (Boolean, PlayerBackend?) -> Unit,
+) = buildList {
+ val entries =
+ PlayerBackend.entries
+ .filterNot { it == PlayerBackend.UNRECOGNIZED }
+ .zip(context.resources.getStringArray(R.array.player_backend_options))
+ .filterNot { it.first == PlayerBackend.PREFER_MPV }
+ entries.forEach { (backend, title) ->
+ add(
+ DialogItem(
+ title,
+ dismissOnClick = true,
+ ) {
+ onClick.invoke(false, backend)
+ },
+ )
+ }
+ add(
+ DialogItem(
+ context.getString(R.string.transcoding),
+ dismissOnClick = true,
+ ) {
+ onClick.invoke(true, null)
+ },
+ )
+}
+
@Composable
fun ContextMenu(
onDismissRequest: () -> Unit,
diff --git a/app/src/main/java/com/github/damontecres/wholphin/ui/nav/DestinationContent.kt b/app/src/main/java/com/github/damontecres/wholphin/ui/nav/DestinationContent.kt
index f6f3faa7..22dfc570 100644
--- a/app/src/main/java/com/github/damontecres/wholphin/ui/nav/DestinationContent.kt
+++ b/app/src/main/java/com/github/damontecres/wholphin/ui/nav/DestinationContent.kt
@@ -76,9 +76,25 @@ fun DestinationContent(
HomeSettingsPage(preferences, modifier)
}
- is Destination.PlaybackList,
- is Destination.Playback,
- -> {
+ is Destination.Playback -> {
+ val backend =
+ destination.backend ?: preferences.appPreferences.playbackPreferences.playerBackend
+ if (backend == PlayerBackend.EXTERNAL_PLAYER) {
+ PlayExternalPage(
+ preferences = preferences,
+ destination = destination,
+ modifier = modifier,
+ )
+ } else {
+ PlaybackPage(
+ preferences = preferences,
+ destination = destination,
+ modifier = modifier,
+ )
+ }
+ }
+
+ is Destination.PlaybackList -> {
if (preferences.appPreferences.playbackPreferences.playerBackend == PlayerBackend.EXTERNAL_PLAYER) {
PlayExternalPage(
preferences = preferences,
diff --git a/app/src/main/java/com/github/damontecres/wholphin/ui/playback/PlaybackViewModel.kt b/app/src/main/java/com/github/damontecres/wholphin/ui/playback/PlaybackViewModel.kt
index 680750c3..193bd08d 100644
--- a/app/src/main/java/com/github/damontecres/wholphin/ui/playback/PlaybackViewModel.kt
+++ b/app/src/main/java/com/github/damontecres/wholphin/ui/playback/PlaybackViewModel.kt
@@ -231,8 +231,11 @@ class PlaybackViewModel
) {
val softwareDecoding =
!preferences.appPreferences.playbackPreferences.mpvOptions.enableHardwareDecoding
+ val requestBackend =
+ (destination as? Destination.Playback)?.backend
+ ?: preferences.appPreferences.playbackPreferences.playerBackend
val playerBackend =
- when (preferences.appPreferences.playbackPreferences.playerBackend) {
+ when (requestBackend) {
PlayerBackend.UNRECOGNIZED,
PlayerBackend.EXO_PLAYER,
-> PlayerBackend.EXO_PLAYER
diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml
index 63a83aae..afa848d6 100644
--- a/app/src/main/res/values/strings.xml
+++ b/app/src/main/res/values/strings.xml
@@ -771,6 +771,8 @@
Show %1$s more
Skip behavior
For intros, credits, etc
+ Play with
+ Transcoding
- @string/ass_subtitle_mode_libass
- @string/ass_subtitle_mode_exoplayer