Support custom mpv.conf (#327)

Part of #235 
Closes #305

Adds a preference for specifying a custom `mpv.conf`

Also adds a preference to toggle between `vo=gpu` and `vo=gpu-next`, see
https://github.com/mpv-player/mpv/wiki/GPU-Next-vs-GPU

There are some caveats:
* Multi-line text entry via a remote is rough
* [Track selection
options](https://mpv.io/manual/stable/#track-selection) may not work as
expected because Wholphin manages this itself
* Bad settings will crash the app hard

Dev note: This rewrites the text field inputs to use the [state based
implementations](https://developer.android.com/develop/ui/compose/text/migrate-state-based)
This commit is contained in:
damontecres 2025-12-04 13:21:40 -05:00 committed by GitHub
parent 45cb385b9b
commit a294661520
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
15 changed files with 408 additions and 269 deletions

View file

@ -701,6 +701,24 @@ sealed interface AppPreference<Pref, T> {
summary = R.string.disable_if_crash,
)
val MpvGpuNext =
AppSwitchPreference<AppPreferences>(
title = R.string.mpv_use_gpu_next,
defaultValue = true,
getter = { it.playbackPreferences.mpvOptions.useGpuNext },
setter = { prefs, value ->
prefs.updateMpvOptions { useGpuNext = value }
},
summaryOn = R.string.enabled,
summaryOff = R.string.disabled,
)
val MpvConfFile =
AppClickablePreference<AppPreferences>(
title = R.string.mpv_conf,
summary = null,
)
val DebugLogging =
AppSwitchPreference<AppPreferences>(
title = R.string.verbose_logging,
@ -851,7 +869,11 @@ val advancedPreferences =
),
ConditionalPreferences(
{ it.playbackPreferences.playerBackend == PlayerBackend.MPV },
listOf(AppPreference.MpvHardwareDecoding),
listOf(
AppPreference.MpvHardwareDecoding,
AppPreference.MpvGpuNext,
AppPreference.MpvConfFile,
),
),
),
),

View file

@ -68,6 +68,7 @@ class AppPreferencesSerializer
.apply {
enableHardwareDecoding =
AppPreference.MpvHardwareDecoding.defaultValue
useGpuNext = AppPreference.MpvGpuNext.defaultValue
}.build()
}.build()
homePagePreferences =

View file

@ -10,6 +10,7 @@ import com.github.damontecres.wholphin.preferences.AppPreference
import com.github.damontecres.wholphin.preferences.AppPreferences
import com.github.damontecres.wholphin.preferences.updateAdvancedPreferences
import com.github.damontecres.wholphin.preferences.updateInterfacePreferences
import com.github.damontecres.wholphin.preferences.updateMpvOptions
import com.github.damontecres.wholphin.preferences.updatePlaybackOverrides
import com.github.damontecres.wholphin.preferences.updateSubtitlePreferences
import com.github.damontecres.wholphin.ui.preferences.PreferencesViewModel
@ -144,4 +145,11 @@ suspend fun upgradeApp(
}
}
}
if (previous.isEqualOrBefore(Version.fromString("0.3.4-2-g0"))) {
appPreferences.updateData {
it.updateMpvOptions {
useGpuNext = AppPreference.MpvGpuNext.defaultValue
}
}
}
}

View file

@ -49,7 +49,10 @@ class PlayerFactory
val enableHardwareDecoding =
prefs?.mpvOptions?.enableHardwareDecoding
?: AppPreference.MpvHardwareDecoding.defaultValue
MpvPlayer(context, enableHardwareDecoding)
val useGpuNext =
prefs?.mpvOptions?.useGpuNext
?: AppPreference.MpvGpuNext.defaultValue
MpvPlayer(context, enableHardwareDecoding, useGpuNext)
.apply {
playWhenReady = true
}

View file

@ -1,146 +1,156 @@
package com.github.damontecres.wholphin.ui.components
import androidx.compose.foundation.background
import androidx.compose.foundation.interaction.MutableInteractionSource
import androidx.compose.foundation.interaction.collectIsFocusedAsState
import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.PaddingValues
import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.defaultMinSize
import androidx.compose.foundation.layout.height
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.layout.width
import androidx.compose.foundation.shape.CircleShape
import androidx.compose.foundation.shape.RoundedCornerShape
import androidx.compose.foundation.text.BasicSecureTextField
import androidx.compose.foundation.text.BasicTextField
import androidx.compose.foundation.text.KeyboardActions
import androidx.compose.foundation.text.KeyboardOptions
import androidx.compose.foundation.text.selection.LocalTextSelectionColors
import androidx.compose.foundation.text.input.KeyboardActionHandler
import androidx.compose.foundation.text.input.TextFieldDecorator
import androidx.compose.foundation.text.input.TextFieldLineLimits
import androidx.compose.foundation.text.input.TextFieldState
import androidx.compose.foundation.text.input.rememberTextFieldState
import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.filled.Search
import androidx.compose.material3.ExperimentalMaterial3Api
import androidx.compose.material3.TextFieldDefaults
import androidx.compose.material3.TextFieldDefaults.Container
import androidx.compose.runtime.Composable
import androidx.compose.runtime.CompositionLocalProvider
import androidx.compose.runtime.getValue
import androidx.compose.runtime.remember
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.graphics.SolidColor
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.text.input.ImeAction
import androidx.compose.ui.text.input.KeyboardType
import androidx.compose.ui.text.input.PasswordVisualTransformation
import androidx.compose.ui.text.input.VisualTransformation
import androidx.compose.ui.unit.Dp
import androidx.compose.ui.unit.dp
import androidx.tv.material3.Icon
import androidx.tv.material3.LocalContentColor
import androidx.tv.material3.MaterialTheme
import com.github.damontecres.wholphin.R
import com.github.damontecres.wholphin.ui.PreviewTvSpec
import com.github.damontecres.wholphin.ui.theme.WholphinTheme
import com.google.protobuf.value
/**
* A modified [BasicTextField] that looks & fits better with TV material controls
*/
@OptIn(ExperimentalMaterial3Api::class)
@Composable
fun EditTextBox(
value: String,
onValueChange: (String) -> Unit,
state: TextFieldState,
modifier: Modifier = Modifier,
keyboardOptions: KeyboardOptions = KeyboardOptions.Default,
keyboardActions: KeyboardActions = KeyboardActions.Default,
onKeyboardAction: KeyboardActionHandler? = null,
leadingIcon: @Composable (() -> Unit)? = null,
enabled: Boolean = true,
readOnly: Boolean = false,
height: Dp = 40.dp,
maxLines: Int = 1,
isInputValid: (String) -> Boolean = { true },
supportingText: @Composable (() -> Unit)? = null,
placeholder: @Composable (() -> Unit)? = null,
isPassword: Boolean = false,
interactionSource: MutableInteractionSource = remember { MutableInteractionSource() },
) {
// From ButtonDefaults
val focused by interactionSource.collectIsFocusedAsState()
val colors =
TextFieldDefaults.colors(
unfocusedTextColor = MaterialTheme.colorScheme.onSurface.copy(alpha = 0.8f),
focusedTextColor = MaterialTheme.colorScheme.inverseOnSurface,
focusedContainerColor = MaterialTheme.colorScheme.surfaceVariant.copy(alpha = 0.6f),
unfocusedContainerColor = MaterialTheme.colorScheme.surfaceVariant.copy(alpha = 0.8f),
disabledContainerColor = MaterialTheme.colorScheme.surfaceVariant.copy(alpha = 0.4f),
errorContainerColor = MaterialTheme.colorScheme.errorContainer.copy(alpha = 0.8f),
focusedIndicatorColor = MaterialTheme.colorScheme.border,
unfocusedLabelColor = Color.Unspecified,
unfocusedTextColor = MaterialTheme.colorScheme.onSurfaceVariant,
unfocusedContainerColor = MaterialTheme.colorScheme.surfaceVariant.copy(alpha = 0.6f),
focusedTextColor = MaterialTheme.colorScheme.onSurfaceVariant,
focusedContainerColor = MaterialTheme.colorScheme.surfaceVariant.copy(alpha = 0.8f),
disabledTextColor = MaterialTheme.colorScheme.onSurfaceVariant.copy(alpha = 0.25f),
disabledContainerColor = MaterialTheme.colorScheme.surfaceVariant.copy(alpha = 0.25f),
errorContainerColor = MaterialTheme.colorScheme.errorContainer.copy(alpha = 0.75f),
errorTextColor = MaterialTheme.colorScheme.onErrorContainer,
)
CompositionLocalProvider(LocalTextSelectionColors provides colors.textSelectionColors) {
BasicTextField(
value = value,
val textColor =
when {
!isInputValid(state.text.toString()) -> colors.errorTextColor
focused -> colors.focusedTextColor
!enabled -> colors.disabledTextColor
!focused -> colors.unfocusedTextColor
else -> colors.unfocusedTextColor
}
val lineLimits =
remember(maxLines) {
if (maxLines == 1) {
TextFieldLineLimits.SingleLine
} else {
TextFieldLineLimits.MultiLine(
maxLines,
maxLines,
)
}
}
val decorator =
remember {
TextFieldDecorator { innerTextField: @Composable () -> Unit ->
val containerColor =
when {
!isInputValid(state.text.toString()) -> colors.errorContainerColor
focused -> colors.focusedContainerColor
!enabled -> colors.disabledContainerColor
!focused -> colors.unfocusedContainerColor
else -> colors.unfocusedContainerColor
}
Row(
horizontalArrangement = Arrangement.spacedBy(8.dp),
verticalAlignment = Alignment.CenterVertically,
modifier =
Modifier
.background(
containerColor,
shape = if (maxLines > 1) RoundedCornerShape(8.dp) else CircleShape,
).padding(8.dp),
) {
CompositionLocalProvider(
androidx.compose.material3.LocalContentColor provides MaterialTheme.colorScheme.onSurfaceVariant,
) {
CompositionLocalProvider(LocalContentColor provides MaterialTheme.colorScheme.onSurfaceVariant) {
leadingIcon?.invoke()
innerTextField.invoke()
}
}
}
}
}
if (isPassword) {
BasicSecureTextField(
state = state,
modifier =
modifier
.defaultMinSize(
minWidth = TextFieldDefaults.MinWidth,
minHeight = height,
).height(height),
onValueChange = onValueChange,
modifier.defaultMinSize(
minWidth = TextFieldDefaults.MinWidth,
),
enabled = enabled,
readOnly = readOnly,
textStyle = MaterialTheme.typography.bodyLarge.merge(MaterialTheme.colorScheme.onPrimaryContainer),
cursorBrush = SolidColor(colors.cursorColor),
keyboardOptions = keyboardOptions,
keyboardActions = keyboardActions,
onKeyboardAction = onKeyboardAction,
textStyle = MaterialTheme.typography.bodyLarge.merge(textColor),
interactionSource = interactionSource,
singleLine = true,
maxLines = 1,
minLines = 1,
visualTransformation =
if (keyboardOptions.keyboardType == KeyboardType.Password ||
keyboardOptions.keyboardType == KeyboardType.NumberPassword
) {
PasswordVisualTransformation()
} else {
VisualTransformation.None
},
decorationBox =
@Composable { innerTextField ->
// places leading icon, text field with label and placeholder, trailing icon
TextFieldDefaults.DecorationBox(
value = value,
visualTransformation =
if (keyboardOptions.keyboardType == KeyboardType.Password ||
keyboardOptions.keyboardType == KeyboardType.NumberPassword
) {
PasswordVisualTransformation()
} else {
VisualTransformation.None
},
innerTextField = innerTextField,
placeholder = placeholder,
label = null,
leadingIcon = leadingIcon,
trailingIcon = null,
prefix = null,
suffix = null,
supportingText = supportingText,
shape = CircleShape,
singleLine = true,
enabled = enabled,
isError = false,
interactionSource = interactionSource,
colors = colors,
contentPadding =
PaddingValues(
horizontal = 8.dp,
vertical = 10.dp,
),
container = {
Container(
enabled = enabled,
isError = !isInputValid.invoke(value),
interactionSource = interactionSource,
modifier = Modifier,
colors = colors,
shape = CircleShape,
focusedIndicatorLineThickness = 4.dp,
unfocusedIndicatorLineThickness = 0.dp,
)
},
)
},
cursorBrush = SolidColor(colors.cursorColor),
decorator = decorator,
)
} else {
BasicTextField(
state = state,
modifier =
modifier.defaultMinSize(
minWidth = TextFieldDefaults.MinWidth,
),
enabled = enabled,
readOnly = readOnly,
keyboardOptions = keyboardOptions,
onKeyboardAction = onKeyboardAction,
textStyle = MaterialTheme.typography.bodyLarge.merge(textColor),
interactionSource = interactionSource,
lineLimits = lineLimits,
cursorBrush = SolidColor(colors.cursorColor),
decorator = decorator,
)
}
}
@ -150,31 +160,24 @@ fun EditTextBox(
*/
@Composable
fun SearchEditTextBox(
value: String,
onValueChange: (String) -> Unit,
state: TextFieldState,
onSearchClick: () -> Unit,
modifier: Modifier = Modifier,
enabled: Boolean = true,
readOnly: Boolean = false,
height: Dp = 40.dp,
interactionSource: MutableInteractionSource = remember { MutableInteractionSource() },
) {
EditTextBox(
value,
onValueChange,
modifier,
state = state,
modifier = modifier,
keyboardOptions =
KeyboardOptions(
autoCorrectEnabled = false,
imeAction = ImeAction.Search,
),
keyboardActions =
KeyboardActions(
onSearch = {
onSearchClick.invoke()
this.defaultKeyboardAction(ImeAction.Search)
},
),
onKeyboardAction = {
onSearchClick.invoke()
},
leadingIcon = {
Icon(
imageVector = Icons.Default.Search,
@ -184,7 +187,6 @@ fun SearchEditTextBox(
},
enabled = enabled,
readOnly = readOnly,
height = height,
interactionSource = interactionSource,
)
}
@ -193,16 +195,35 @@ fun SearchEditTextBox(
@Composable
private fun EditTextBoxPreview() {
WholphinTheme {
Column {
Column(verticalArrangement = Arrangement.spacedBy(8.dp)) {
EditTextBox(
value = "string",
onValueChange = {},
state = rememberTextFieldState("string"),
)
SearchEditTextBox(
value = "search query",
onValueChange = {},
state = rememberTextFieldState("search query"),
onSearchClick = { },
)
EditTextBox(
state = rememberTextFieldState("string\nstring\nstring"),
maxLines = 5,
// height = 88.dp,
)
EditTextBox(
state = rememberTextFieldState("search"),
leadingIcon = {
Icon(
imageVector = Icons.Default.Search,
contentDescription = stringResource(R.string.search),
tint = MaterialTheme.colorScheme.onPrimaryContainer,
)
},
isInputValid = { true },
modifier = Modifier.width(280.dp),
)
EditTextBox(
state = rememberTextFieldState("password"),
isPassword = true,
)
}
}
}

View file

@ -9,8 +9,8 @@ import androidx.compose.foundation.layout.size
import androidx.compose.foundation.lazy.LazyColumn
import androidx.compose.foundation.lazy.items
import androidx.compose.foundation.shape.RoundedCornerShape
import androidx.compose.foundation.text.KeyboardActions
import androidx.compose.foundation.text.KeyboardOptions
import androidx.compose.foundation.text.input.rememberTextFieldState
import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.filled.Add
import androidx.compose.material3.CircularProgressIndicator
@ -133,7 +133,7 @@ fun PlaylistList(
properties = DialogProperties(usePlatformDefaultWidth = false),
elevation = 10.dp,
) {
var playlistName by remember { mutableStateOf("") }
val playlistName = rememberTextFieldState()
val focusRequester = remember { FocusRequester() }
LaunchedEffect(Unit) { focusRequester.tryRequestFocus() }
Column(
@ -148,10 +148,7 @@ fun PlaylistList(
text = stringResource(R.string.name),
)
EditTextBox(
value = playlistName,
onValueChange = {
playlistName = it
},
state = playlistName,
keyboardOptions =
KeyboardOptions(
capitalization = KeyboardCapitalization.Words,
@ -159,13 +156,10 @@ fun PlaylistList(
keyboardType = KeyboardType.Text,
imeAction = ImeAction.Done,
),
keyboardActions =
KeyboardActions(
onDone = {
showCreateDialog = false
onCreatePlaylist.invoke(playlistName)
},
),
onKeyboardAction = {
showCreateDialog = false
onCreatePlaylist.invoke(playlistName.text.toString())
},
modifier =
Modifier
.focusRequester(focusRequester)
@ -174,9 +168,9 @@ fun PlaylistList(
Button(
onClick = {
showCreateDialog = false
onCreatePlaylist.invoke(playlistName)
onCreatePlaylist.invoke(playlistName.text.toString())
},
enabled = playlistName.isNotNullOrBlank(),
enabled = playlistName.text.isNotNullOrBlank(),
modifier = Modifier,
) {
Text(text = stringResource(R.string.submit))

View file

@ -12,6 +12,7 @@ import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.lazy.LazyColumn
import androidx.compose.foundation.lazy.LazyListScope
import androidx.compose.foundation.text.input.rememberTextFieldState
import androidx.compose.runtime.Composable
import androidx.compose.runtime.LaunchedEffect
import androidx.compose.runtime.getValue
@ -171,7 +172,7 @@ fun SearchPage(
val series by viewModel.series.observeAsState(SearchResult.NoQuery)
val episodes by viewModel.episodes.observeAsState(SearchResult.NoQuery)
var query by rememberSaveable { mutableStateOf("") }
val query = rememberTextFieldState()
val focusRequester = remember { FocusRequester() }
var position by rememberPosition()
@ -179,7 +180,7 @@ fun SearchPage(
LaunchedEffect(query) {
delay(750L)
viewModel.search(query)
viewModel.search(query.text.toString())
}
LaunchedEffect(Unit) {
focusRequester.tryRequestFocus()
@ -213,12 +214,9 @@ fun SearchPage(
focusManager.moveFocus(FocusDirection.Next)
}
SearchEditTextBox(
value = query,
onValueChange = {
query = it
},
state = query,
onSearchClick = {
viewModel.search(query)
viewModel.search(query.text.toString())
searchClicked = true
},
modifier =

View file

@ -11,17 +11,14 @@ import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.lazy.LazyColumn
import androidx.compose.foundation.lazy.itemsIndexed
import androidx.compose.foundation.shape.RoundedCornerShape
import androidx.compose.foundation.text.KeyboardActions
import androidx.compose.foundation.text.KeyboardOptions
import androidx.compose.foundation.text.input.rememberTextFieldState
import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.filled.Star
import androidx.compose.material3.HorizontalDivider
import androidx.compose.runtime.Composable
import androidx.compose.runtime.LaunchedEffect
import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember
import androidx.compose.runtime.setValue
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.draw.drawBehind
@ -103,7 +100,7 @@ fun DownloadSubtitlesContent(
style = MaterialTheme.typography.titleLarge,
color = MaterialTheme.colorScheme.onSurface,
)
var lang by remember { mutableStateOf(language) }
val lang = rememberTextFieldState(language)
Row(
horizontalArrangement = Arrangement.spacedBy(8.dp),
verticalAlignment = Alignment.CenterVertically,
@ -114,14 +111,10 @@ fun DownloadSubtitlesContent(
color = MaterialTheme.colorScheme.onSurface,
)
EditTextBox(
value = lang,
onValueChange = { lang = it },
keyboardActions =
KeyboardActions(
onSearch = {
onSearch(lang)
},
),
state = lang,
onKeyboardAction = {
onSearch(lang.text.toString())
},
keyboardOptions =
KeyboardOptions(
imeAction = ImeAction.Search,

View file

@ -1,16 +1,7 @@
package com.github.damontecres.wholphin.ui.preferences
import androidx.compose.animation.AnimatedVisibility
import androidx.compose.foundation.background
import androidx.compose.foundation.interaction.MutableInteractionSource
import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.shape.RoundedCornerShape
import androidx.compose.foundation.text.KeyboardActions
import androidx.compose.foundation.text.KeyboardOptions
import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.filled.Done
@ -22,7 +13,6 @@ import androidx.compose.runtime.mutableStateSetOf
import androidx.compose.runtime.remember
import androidx.compose.runtime.rememberCoroutineScope
import androidx.compose.runtime.setValue
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.platform.LocalContext
@ -30,13 +20,12 @@ import androidx.compose.ui.res.stringArrayResource
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.text.TextStyle
import androidx.compose.ui.text.input.ImeAction
import androidx.compose.ui.text.input.KeyboardCapitalization
import androidx.compose.ui.text.input.KeyboardType
import androidx.compose.ui.unit.dp
import androidx.compose.ui.window.Dialog
import androidx.tv.material3.Button
import androidx.tv.material3.MaterialTheme
import androidx.tv.material3.Text
import androidx.tv.material3.surfaceColorAtElevation
import com.github.damontecres.wholphin.R
import com.github.damontecres.wholphin.preferences.AppChoicePreference
import com.github.damontecres.wholphin.preferences.AppClickablePreference
import com.github.damontecres.wholphin.preferences.AppDestinationPreference
@ -48,10 +37,10 @@ import com.github.damontecres.wholphin.preferences.AppSwitchPreference
import com.github.damontecres.wholphin.ui.components.DialogItem
import com.github.damontecres.wholphin.ui.components.DialogParams
import com.github.damontecres.wholphin.ui.components.DialogPopup
import com.github.damontecres.wholphin.ui.components.EditTextBox
import com.github.damontecres.wholphin.ui.nav.Destination
import com.github.damontecres.wholphin.util.ExceptionHandler
import kotlinx.coroutines.launch
import java.io.File
import java.util.SortedSet
@Suppress("UNCHECKED_CAST")
@ -82,6 +71,37 @@ fun <T> ComposablePreference(
}
when (preference) {
AppPreference.MpvConfFile -> {
ClickPreference(
title = title,
onClick = {
val confFile = File(context.filesDir, "mpv.conf")
val contents = if (confFile.exists()) confFile.readText() else ""
showStringDialog =
StringInput(
title = title,
value = contents,
keyboardOptions =
KeyboardOptions(
capitalization = KeyboardCapitalization.None,
autoCorrectEnabled = false,
keyboardType = KeyboardType.Text,
imeAction = ImeAction.None,
),
onSubmit = {
confFile.writeText(it)
showStringDialog = null
},
maxLines = 10,
confirmDiscard = true,
)
},
summary = preference.summary(context, value),
interactionSource = interactionSource,
modifier = modifier,
)
}
is AppDestinationPreference ->
ClickPreference(
title = title,
@ -244,73 +264,15 @@ fun <T> ComposablePreference(
}
}
AnimatedVisibility(showStringDialog != null) {
showStringDialog?.let {
var mutableValue by remember { mutableStateOf(it.value ?: "") }
val onDone = {
it.onSubmit.invoke(mutableValue)
}
Dialog(
showStringDialog?.let { input ->
StringInputDialog(
input = input,
onSave = {
input.onSubmit.invoke(it)
},
onDismissRequest = { showStringDialog = null },
) {
Box(
contentAlignment = Alignment.Center,
modifier =
Modifier
.padding(16.dp)
.background(
color = dialogBackgroundColor,
shape = RoundedCornerShape(8.dp),
),
) {
Column(
verticalArrangement = Arrangement.spacedBy(8.dp),
modifier =
Modifier
.padding(16.dp),
) {
Text(
text = it.title,
style = MaterialTheme.typography.titleLarge,
color = MaterialTheme.colorScheme.onSecondaryContainer,
modifier = Modifier,
)
EditTextBox(
value = mutableValue,
onValueChange = { mutableValue = it },
keyboardOptions = it.keyboardOptions.copy(imeAction = ImeAction.Done),
keyboardActions =
KeyboardActions(
onDone = { onDone.invoke() },
),
leadingIcon = null,
isInputValid = { true },
modifier = Modifier.fillMaxWidth(),
)
Row(
horizontalArrangement = Arrangement.SpaceAround,
modifier = Modifier.fillMaxWidth(),
) {
Button(
onClick = { showStringDialog = null },
modifier = Modifier,
) {
Text(
text = stringResource(R.string.cancel),
)
}
Button(
onClick = onDone,
modifier = Modifier,
) {
Text(
text = stringResource(R.string.save),
)
}
}
}
}
}
elevation = 3.dp,
)
}
}
}
@ -353,9 +315,11 @@ fun PreferenceSummary(
}
}
private data class StringInput(
data class StringInput(
val title: String,
val value: String?,
val keyboardOptions: KeyboardOptions,
val onSubmit: (String) -> Unit,
val maxLines: Int = 1,
val confirmDiscard: Boolean = false,
)

View file

@ -0,0 +1,133 @@
package com.github.damontecres.wholphin.ui.preferences
import androidx.compose.foundation.background
import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.layout.width
import androidx.compose.foundation.shape.RoundedCornerShape
import androidx.compose.foundation.text.input.rememberTextFieldState
import androidx.compose.runtime.Composable
import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember
import androidx.compose.runtime.setValue
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.unit.Dp
import androidx.compose.ui.unit.dp
import androidx.compose.ui.window.Dialog
import androidx.compose.ui.window.DialogProperties
import androidx.tv.material3.Button
import androidx.tv.material3.MaterialTheme
import androidx.tv.material3.Text
import androidx.tv.material3.surfaceColorAtElevation
import com.github.damontecres.wholphin.R
import com.github.damontecres.wholphin.ui.components.ConfirmDialog
import com.github.damontecres.wholphin.ui.components.EditTextBox
@Composable
fun StringInputDialog(
input: StringInput,
onSave: (String) -> Unit,
onDismissRequest: () -> Unit,
elevation: Dp = 3.dp,
) {
val mutableValue = rememberTextFieldState(input.value ?: "")
// var mutableValue by remember { mutableStateOf(input.value ?: "") }
val onDone = {
onSave.invoke(mutableValue.text.toString())
}
var showConfirm by remember { mutableStateOf(false) }
Dialog(
properties =
DialogProperties(
usePlatformDefaultWidth = false,
),
onDismissRequest = {
if (input.confirmDiscard && mutableValue.text.toString() != input.value) {
showConfirm = true
} else {
onDismissRequest.invoke()
}
},
) {
Box(
contentAlignment = Alignment.Center,
modifier =
Modifier
.padding(16.dp)
.width(512.dp)
.background(
color = MaterialTheme.colorScheme.surfaceColorAtElevation(elevation),
shape = RoundedCornerShape(8.dp),
),
) {
Column(
verticalArrangement = Arrangement.spacedBy(8.dp),
modifier =
Modifier
.padding(16.dp),
) {
Text(
text = input.title,
style = MaterialTheme.typography.titleLarge,
color = MaterialTheme.colorScheme.onSecondaryContainer,
modifier = Modifier,
)
EditTextBox(
state = mutableValue,
keyboardOptions = input.keyboardOptions,
onKeyboardAction = {
onDone.invoke()
},
leadingIcon = null,
isInputValid = { true },
maxLines = input.maxLines,
modifier = Modifier.fillMaxWidth(),
)
Row(
horizontalArrangement = Arrangement.SpaceAround,
modifier = Modifier.fillMaxWidth(),
) {
Button(
onClick = onDismissRequest,
modifier = Modifier,
) {
Text(
text = stringResource(R.string.cancel),
)
}
Button(
onClick = onDone,
modifier = Modifier,
) {
Text(
text = stringResource(R.string.save),
)
}
}
}
}
}
if (showConfirm) {
ConfirmDialog(
title = stringResource(R.string.discard_change),
body = null,
onCancel = {
showConfirm = false
},
onConfirm = {
showConfirm = false
onDismissRequest.invoke()
},
elevation = elevation * 2,
)
}
}

View file

@ -9,8 +9,8 @@ import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.layout.size
import androidx.compose.foundation.shape.RoundedCornerShape
import androidx.compose.foundation.text.KeyboardActions
import androidx.compose.foundation.text.KeyboardOptions
import androidx.compose.foundation.text.input.rememberTextFieldState
import androidx.compose.runtime.Composable
import androidx.compose.runtime.LaunchedEffect
import androidx.compose.runtime.getValue
@ -23,6 +23,7 @@ import androidx.compose.ui.Modifier
import androidx.compose.ui.focus.FocusRequester
import androidx.compose.ui.focus.focusRequester
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.text.input.ImeAction
import androidx.compose.ui.text.input.KeyboardCapitalization
import androidx.compose.ui.text.input.KeyboardType
import androidx.compose.ui.unit.dp
@ -170,9 +171,9 @@ fun SwitchServerContent(
viewModel.clearAddServerState()
}
val state by viewModel.addServerState.observeAsState(LoadingState.Pending)
var url by remember { mutableStateOf("") }
val url = rememberTextFieldState()
val submit = {
viewModel.addServer(url)
viewModel.addServer(url.text.toString())
}
BasicDialog(
onDismissRequest = {
@ -196,21 +197,15 @@ fun SwitchServerContent(
text = stringResource(R.string.enter_server_url),
)
EditTextBox(
value = url,
onValueChange = {
url = it
viewModel.clearAddServerState()
},
state = url,
keyboardOptions =
KeyboardOptions(
capitalization = KeyboardCapitalization.None,
autoCorrectEnabled = false,
keyboardType = KeyboardType.Uri,
imeAction = ImeAction.Go,
),
keyboardActions =
KeyboardActions(
onDone = { submit.invoke() },
),
onKeyboardAction = { submit.invoke() },
modifier =
Modifier
.focusRequester(focusRequester)
@ -230,7 +225,7 @@ fun SwitchServerContent(
}
Button(
onClick = { submit.invoke() },
enabled = url.isNotNullOrBlank() && state == LoadingState.Pending,
enabled = url.text.isNotNullOrBlank() && state == LoadingState.Pending,
modifier = Modifier,
) {
if (state == LoadingState.Loading) {

View file

@ -12,8 +12,8 @@ import androidx.compose.foundation.layout.height
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.layout.size
import androidx.compose.foundation.shape.RoundedCornerShape
import androidx.compose.foundation.text.KeyboardActions
import androidx.compose.foundation.text.KeyboardOptions
import androidx.compose.foundation.text.input.rememberTextFieldState
import androidx.compose.runtime.Composable
import androidx.compose.runtime.LaunchedEffect
import androidx.compose.runtime.getValue
@ -207,12 +207,17 @@ fun SwitchUserContent(
Text(text = "Use username/password")
}
} else {
var username by remember { mutableStateOf("") }
var password by remember { mutableStateOf("") }
val username = rememberTextFieldState()
val password = rememberTextFieldState()
val onSubmit = {
viewModel.login(server, username, password)
viewModel.login(
server,
username.text.toString(),
password.text.toString(),
)
}
val focusRequester = remember { FocusRequester() }
val passwordFocusRequester = remember { FocusRequester() }
LaunchedEffect(Unit) { focusRequester.tryRequestFocus() }
Text(
text = "Enter username/password to login to ${server.name ?: server.url}",
@ -229,11 +234,7 @@ fun SwitchUserContent(
modifier = Modifier.padding(end = 8.dp),
)
EditTextBox(
value = username,
onValueChange = {
username = it
viewModel.clearSwitchUserState()
},
state = username,
keyboardOptions =
KeyboardOptions(
capitalization = KeyboardCapitalization.None,
@ -241,6 +242,9 @@ fun SwitchUserContent(
keyboardType = KeyboardType.Text,
imeAction = ImeAction.Next,
),
onKeyboardAction = {
passwordFocusRequester.tryRequestFocus()
},
isInputValid = { userState !is LoadingState.Error },
modifier = Modifier.focusRequester(focusRequester),
)
@ -253,29 +257,27 @@ fun SwitchUserContent(
text = "Password",
modifier = Modifier.padding(end = 8.dp),
)
LaunchedEffect(password.text) {
viewModel.clearSwitchUserState()
}
EditTextBox(
value = password,
onValueChange = {
password = it
viewModel.clearSwitchUserState()
},
isPassword = true,
state = password,
keyboardOptions =
KeyboardOptions(
capitalization = KeyboardCapitalization.None,
autoCorrectEnabled = false,
keyboardType = KeyboardType.Password,
imeAction = ImeAction.Go,
),
keyboardActions =
KeyboardActions(
onDone = { onSubmit.invoke() },
),
onKeyboardAction = { onSubmit.invoke() },
isInputValid = { userState !is LoadingState.Error },
modifier = Modifier,
modifier = Modifier.focusRequester(passwordFocusRequester),
)
}
Button(
onClick = { onSubmit.invoke() },
enabled = username.isNotNullOrBlank() && password.isNotNullOrBlank(),
enabled = username.text.isNotNullOrBlank() && password.text.isNotNullOrBlank(),
modifier = Modifier.align(Alignment.CenterHorizontally),
) {
Text("Login")

View file

@ -57,6 +57,7 @@ import kotlin.time.Duration.Companion.seconds
class MpvPlayer(
private val context: Context,
enableHardwareDecoding: Boolean,
useGpuNext: Boolean,
) : BasePlayer(),
MPVLib.EventObserver,
TrackSelector.InvalidationListener {
@ -100,7 +101,7 @@ class MpvPlayer(
if (enableHardwareDecoding) {
MPVLib.setOptionString("hwdec", "mediacodec,mediacodec-copy")
MPVLib.setOptionString("vo", "gpu")
MPVLib.setOptionString("vo", if (useGpuNext) "gpu-next" else "gpu")
} else {
MPVLib.setOptionString("hwdec", "no")
}

View file

@ -36,6 +36,7 @@ enum PlayerBackend{
message MpvOptions{
bool enable_hardware_decoding = 1;
bool use_gpu_next = 2;
}
message PlaybackOverrides{

View file

@ -326,6 +326,9 @@
<string name="remove">Remove</string>
<string name="dolby_vision">Dolby Vision</string>
<string name="dolby_atmos">Dolby Atmos</string>
<string name="mpv_use_gpu_next">MPV: Use gpu-next</string>
<string name="mpv_conf">Edit mpv.conf</string>
<string name="discard_change">Discard changes?</string>
<string name="subtitle_margin">Margin</string>
<string name="verbose_logging">Verbose logging</string>
<string name="image_cache_size">Image disk cache size (MB)</string>