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
<img width="1280" height="720" alt="play_with Large"
src="https://github.com/user-attachments/assets/2a75756f-5d7f-45f6-8ea6-a406f3d7f3e1"
/>


## AI or LLM usage
None
This commit is contained in:
Damontecres 2026-04-29 19:45:37 -04:00 committed by GitHub
parent c718d903a4
commit b94faceb2f
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
4 changed files with 91 additions and 16 deletions

View file

@ -17,12 +17,14 @@ import androidx.compose.runtime.setValue
import androidx.compose.ui.graphics.Color import androidx.compose.ui.graphics.Color
import androidx.compose.ui.platform.LocalContext import androidx.compose.ui.platform.LocalContext
import androidx.compose.ui.res.stringResource import androidx.compose.ui.res.stringResource
import androidx.compose.ui.unit.dp
import com.github.damontecres.wholphin.R import com.github.damontecres.wholphin.R
import com.github.damontecres.wholphin.data.ChosenStreams import com.github.damontecres.wholphin.data.ChosenStreams
import com.github.damontecres.wholphin.data.model.AudioItem import com.github.damontecres.wholphin.data.model.AudioItem
import com.github.damontecres.wholphin.data.model.BaseItem import com.github.damontecres.wholphin.data.model.BaseItem
import com.github.damontecres.wholphin.data.model.ItemPlayback import com.github.damontecres.wholphin.data.model.ItemPlayback
import com.github.damontecres.wholphin.data.model.Person 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.letNotEmpty
import com.github.damontecres.wholphin.ui.nav.Destination import com.github.damontecres.wholphin.ui.nav.Destination
import com.github.damontecres.wholphin.util.supportedPlayableTypes import com.github.damontecres.wholphin.util.supportedPlayableTypes
@ -185,6 +187,7 @@ fun ContextMenu(
val context = LocalContext.current val context = LocalContext.current
var chooseVersion by remember { mutableStateOf<DialogParams?>(null) } var chooseVersion by remember { mutableStateOf<DialogParams?>(null) }
var showDeleteDialog by remember { mutableStateOf(false) } var showDeleteDialog by remember { mutableStateOf(false) }
var showPlayWithDialog by remember { mutableStateOf(false) }
val dialogItems = val dialogItems =
remember(context, item, chosenStreams) { remember(context, item, chosenStreams) {
@ -211,6 +214,7 @@ fun ContextMenu(
) { idx -> ) { idx ->
val source = item.data.mediaSources!![idx] val source = item.data.mediaSources!![idx]
actions.onChooseVersion.invoke(item, source) actions.onChooseVersion.invoke(item, source)
onDismissRequest.invoke()
} }
}, },
onChooseTracks = { type -> onChooseTracks = { type ->
@ -239,6 +243,7 @@ fun ContextMenu(
itemPlayback = chosenStreams?.itemPlayback, itemPlayback = chosenStreams?.itemPlayback,
), ),
) )
onDismissRequest.invoke()
}, },
preferredSubtitleLanguage = preferredSubtitleLanguage, preferredSubtitleLanguage = preferredSubtitleLanguage,
) )
@ -249,6 +254,7 @@ fun ContextMenu(
actions.onClearChosenStreams.invoke(chosenStreams) actions.onClearChosenStreams.invoke(chosenStreams)
}, },
onClickDelete = { showDeleteDialog = true }, onClickDelete = { showDeleteDialog = true },
onClickPlayWith = { showPlayWithDialog = true },
) )
} }
DialogPopup( 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( private fun buildContextMenuItems(
@ -302,6 +333,7 @@ private fun buildContextMenuItems(
onShowOverview: () -> Unit, onShowOverview: () -> Unit,
onClearChosenStreams: () -> Unit, onClearChosenStreams: () -> Unit,
onClickDelete: () -> Unit, onClickDelete: () -> Unit,
onClickPlayWith: () -> Unit,
): List<DialogItem> = ): List<DialogItem> =
buildList { buildList {
if (showGoTo) { if (showGoTo) {
@ -316,7 +348,7 @@ private fun buildContextMenuItems(
) )
} }
if (item.type in supportedPlayableTypes) { if (item.type in supportedPlayableTypes) {
if (item.playbackPosition >= Duration.ZERO) { if (item.playbackPosition > Duration.ZERO) {
add( add(
DialogItem( DialogItem(
context.getString(R.string.resume), context.getString(R.string.resume),
@ -566,19 +598,12 @@ private fun buildContextMenuItems(
if (item.type in supportedPlayableTypes) { if (item.type in supportedPlayableTypes) {
add( add(
DialogItem( DialogItem(
context.getString(R.string.play_with_transcoding), context.getString(R.string.play_with),
Icons.Default.PlayArrow, Icons.Default.PlayArrow,
dismissOnClick = true, dismissOnClick = false,
) { onClick = onClickPlayWith,
actions.navigateTo(
Destination.Playback(
item.id,
item.resumeMs ?: 0L,
forceTranscoding = true,
), ),
) )
},
)
} }
if (item.data.mediaSources?.isNotEmpty() == true) { if (item.data.mediaSources?.isNotEmpty() == true) {
add( add(
@ -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 @Composable
fun ContextMenu( fun ContextMenu(
onDismissRequest: () -> Unit, onDismissRequest: () -> Unit,

View file

@ -76,9 +76,25 @@ fun DestinationContent(
HomeSettingsPage(preferences, modifier) 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) { if (preferences.appPreferences.playbackPreferences.playerBackend == PlayerBackend.EXTERNAL_PLAYER) {
PlayExternalPage( PlayExternalPage(
preferences = preferences, preferences = preferences,

View file

@ -231,8 +231,11 @@ class PlaybackViewModel
) { ) {
val softwareDecoding = val softwareDecoding =
!preferences.appPreferences.playbackPreferences.mpvOptions.enableHardwareDecoding !preferences.appPreferences.playbackPreferences.mpvOptions.enableHardwareDecoding
val requestBackend =
(destination as? Destination.Playback)?.backend
?: preferences.appPreferences.playbackPreferences.playerBackend
val playerBackend = val playerBackend =
when (preferences.appPreferences.playbackPreferences.playerBackend) { when (requestBackend) {
PlayerBackend.UNRECOGNIZED, PlayerBackend.UNRECOGNIZED,
PlayerBackend.EXO_PLAYER, PlayerBackend.EXO_PLAYER,
-> PlayerBackend.EXO_PLAYER -> PlayerBackend.EXO_PLAYER

View file

@ -771,6 +771,8 @@
<string name="show_more">Show %1$s more</string> <string name="show_more">Show %1$s more</string>
<string name="skip_behavior">Skip behavior</string> <string name="skip_behavior">Skip behavior</string>
<string name="skip_behavior_summary">For intros, credits, etc</string> <string name="skip_behavior_summary">For intros, credits, etc</string>
<string name="play_with">Play with</string>
<string name="transcoding">Transcoding</string>
<array name="ass_subtitle_modes"> <array name="ass_subtitle_modes">
<item>@string/ass_subtitle_mode_libass</item> <item>@string/ass_subtitle_mode_libass</item>
<item>@string/ass_subtitle_mode_exoplayer</item> <item>@string/ass_subtitle_mode_exoplayer</item>