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, 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 = val DebugLogging =
AppSwitchPreference<AppPreferences>( AppSwitchPreference<AppPreferences>(
title = R.string.verbose_logging, title = R.string.verbose_logging,
@ -851,7 +869,11 @@ val advancedPreferences =
), ),
ConditionalPreferences( ConditionalPreferences(
{ it.playbackPreferences.playerBackend == PlayerBackend.MPV }, { it.playbackPreferences.playerBackend == PlayerBackend.MPV },
listOf(AppPreference.MpvHardwareDecoding), listOf(
AppPreference.MpvHardwareDecoding,
AppPreference.MpvGpuNext,
AppPreference.MpvConfFile,
),
), ),
), ),
), ),

View file

@ -68,6 +68,7 @@ class AppPreferencesSerializer
.apply { .apply {
enableHardwareDecoding = enableHardwareDecoding =
AppPreference.MpvHardwareDecoding.defaultValue AppPreference.MpvHardwareDecoding.defaultValue
useGpuNext = AppPreference.MpvGpuNext.defaultValue
}.build() }.build()
}.build() }.build()
homePagePreferences = 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.AppPreferences
import com.github.damontecres.wholphin.preferences.updateAdvancedPreferences import com.github.damontecres.wholphin.preferences.updateAdvancedPreferences
import com.github.damontecres.wholphin.preferences.updateInterfacePreferences 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.updatePlaybackOverrides
import com.github.damontecres.wholphin.preferences.updateSubtitlePreferences import com.github.damontecres.wholphin.preferences.updateSubtitlePreferences
import com.github.damontecres.wholphin.ui.preferences.PreferencesViewModel 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 = val enableHardwareDecoding =
prefs?.mpvOptions?.enableHardwareDecoding prefs?.mpvOptions?.enableHardwareDecoding
?: AppPreference.MpvHardwareDecoding.defaultValue ?: AppPreference.MpvHardwareDecoding.defaultValue
MpvPlayer(context, enableHardwareDecoding) val useGpuNext =
prefs?.mpvOptions?.useGpuNext
?: AppPreference.MpvGpuNext.defaultValue
MpvPlayer(context, enableHardwareDecoding, useGpuNext)
.apply { .apply {
playWhenReady = true playWhenReady = true
} }

View file

@ -1,146 +1,156 @@
package com.github.damontecres.wholphin.ui.components package com.github.damontecres.wholphin.ui.components
import androidx.compose.foundation.background
import androidx.compose.foundation.interaction.MutableInteractionSource 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.Column
import androidx.compose.foundation.layout.PaddingValues import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.defaultMinSize 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.CircleShape
import androidx.compose.foundation.shape.RoundedCornerShape
import androidx.compose.foundation.text.BasicSecureTextField
import androidx.compose.foundation.text.BasicTextField import androidx.compose.foundation.text.BasicTextField
import androidx.compose.foundation.text.KeyboardActions
import androidx.compose.foundation.text.KeyboardOptions 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.Icons
import androidx.compose.material.icons.filled.Search import androidx.compose.material.icons.filled.Search
import androidx.compose.material3.ExperimentalMaterial3Api import androidx.compose.material3.ExperimentalMaterial3Api
import androidx.compose.material3.TextFieldDefaults import androidx.compose.material3.TextFieldDefaults
import androidx.compose.material3.TextFieldDefaults.Container
import androidx.compose.runtime.Composable import androidx.compose.runtime.Composable
import androidx.compose.runtime.CompositionLocalProvider import androidx.compose.runtime.CompositionLocalProvider
import androidx.compose.runtime.getValue
import androidx.compose.runtime.remember import androidx.compose.runtime.remember
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier import androidx.compose.ui.Modifier
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.graphics.SolidColor import androidx.compose.ui.graphics.SolidColor
import androidx.compose.ui.res.stringResource import androidx.compose.ui.res.stringResource
import androidx.compose.ui.text.input.ImeAction 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.compose.ui.unit.dp
import androidx.tv.material3.Icon import androidx.tv.material3.Icon
import androidx.tv.material3.LocalContentColor
import androidx.tv.material3.MaterialTheme import androidx.tv.material3.MaterialTheme
import com.github.damontecres.wholphin.R import com.github.damontecres.wholphin.R
import com.github.damontecres.wholphin.ui.PreviewTvSpec import com.github.damontecres.wholphin.ui.PreviewTvSpec
import com.github.damontecres.wholphin.ui.theme.WholphinTheme 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) @OptIn(ExperimentalMaterial3Api::class)
@Composable @Composable
fun EditTextBox( fun EditTextBox(
value: String, state: TextFieldState,
onValueChange: (String) -> Unit,
modifier: Modifier = Modifier, modifier: Modifier = Modifier,
keyboardOptions: KeyboardOptions = KeyboardOptions.Default, keyboardOptions: KeyboardOptions = KeyboardOptions.Default,
keyboardActions: KeyboardActions = KeyboardActions.Default, onKeyboardAction: KeyboardActionHandler? = null,
leadingIcon: @Composable (() -> Unit)? = null, leadingIcon: @Composable (() -> Unit)? = null,
enabled: Boolean = true, enabled: Boolean = true,
readOnly: Boolean = false, readOnly: Boolean = false,
height: Dp = 40.dp, maxLines: Int = 1,
isInputValid: (String) -> Boolean = { true }, isInputValid: (String) -> Boolean = { true },
supportingText: @Composable (() -> Unit)? = null, isPassword: Boolean = false,
placeholder: @Composable (() -> Unit)? = null,
interactionSource: MutableInteractionSource = remember { MutableInteractionSource() }, interactionSource: MutableInteractionSource = remember { MutableInteractionSource() },
) { ) {
// From ButtonDefaults val focused by interactionSource.collectIsFocusedAsState()
val colors = val colors =
TextFieldDefaults.colors( TextFieldDefaults.colors(
unfocusedTextColor = MaterialTheme.colorScheme.onSurface.copy(alpha = 0.8f), unfocusedTextColor = MaterialTheme.colorScheme.onSurfaceVariant,
focusedTextColor = MaterialTheme.colorScheme.inverseOnSurface, unfocusedContainerColor = MaterialTheme.colorScheme.surfaceVariant.copy(alpha = 0.6f),
focusedContainerColor = MaterialTheme.colorScheme.surfaceVariant.copy(alpha = 0.6f), focusedTextColor = MaterialTheme.colorScheme.onSurfaceVariant,
unfocusedContainerColor = MaterialTheme.colorScheme.surfaceVariant.copy(alpha = 0.8f), focusedContainerColor = MaterialTheme.colorScheme.surfaceVariant.copy(alpha = 0.8f),
disabledContainerColor = MaterialTheme.colorScheme.surfaceVariant.copy(alpha = 0.4f), disabledTextColor = MaterialTheme.colorScheme.onSurfaceVariant.copy(alpha = 0.25f),
errorContainerColor = MaterialTheme.colorScheme.errorContainer.copy(alpha = 0.8f), disabledContainerColor = MaterialTheme.colorScheme.surfaceVariant.copy(alpha = 0.25f),
focusedIndicatorColor = MaterialTheme.colorScheme.border, errorContainerColor = MaterialTheme.colorScheme.errorContainer.copy(alpha = 0.75f),
unfocusedLabelColor = Color.Unspecified, errorTextColor = MaterialTheme.colorScheme.onErrorContainer,
) )
CompositionLocalProvider(LocalTextSelectionColors provides colors.textSelectionColors) { val textColor =
BasicTextField( when {
value = value, !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 =
modifier modifier.defaultMinSize(
.defaultMinSize( minWidth = TextFieldDefaults.MinWidth,
minWidth = TextFieldDefaults.MinWidth, ),
minHeight = height,
).height(height),
onValueChange = onValueChange,
enabled = enabled, enabled = enabled,
readOnly = readOnly, readOnly = readOnly,
textStyle = MaterialTheme.typography.bodyLarge.merge(MaterialTheme.colorScheme.onPrimaryContainer),
cursorBrush = SolidColor(colors.cursorColor),
keyboardOptions = keyboardOptions, keyboardOptions = keyboardOptions,
keyboardActions = keyboardActions, onKeyboardAction = onKeyboardAction,
textStyle = MaterialTheme.typography.bodyLarge.merge(textColor),
interactionSource = interactionSource, interactionSource = interactionSource,
singleLine = true, cursorBrush = SolidColor(colors.cursorColor),
maxLines = 1, decorator = decorator,
minLines = 1, )
visualTransformation = } else {
if (keyboardOptions.keyboardType == KeyboardType.Password || BasicTextField(
keyboardOptions.keyboardType == KeyboardType.NumberPassword state = state,
) { modifier =
PasswordVisualTransformation() modifier.defaultMinSize(
} else { minWidth = TextFieldDefaults.MinWidth,
VisualTransformation.None ),
}, enabled = enabled,
decorationBox = readOnly = readOnly,
@Composable { innerTextField -> keyboardOptions = keyboardOptions,
// places leading icon, text field with label and placeholder, trailing icon onKeyboardAction = onKeyboardAction,
TextFieldDefaults.DecorationBox( textStyle = MaterialTheme.typography.bodyLarge.merge(textColor),
value = value, interactionSource = interactionSource,
visualTransformation = lineLimits = lineLimits,
if (keyboardOptions.keyboardType == KeyboardType.Password || cursorBrush = SolidColor(colors.cursorColor),
keyboardOptions.keyboardType == KeyboardType.NumberPassword decorator = decorator,
) {
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,
)
},
)
},
) )
} }
} }
@ -150,31 +160,24 @@ fun EditTextBox(
*/ */
@Composable @Composable
fun SearchEditTextBox( fun SearchEditTextBox(
value: String, state: TextFieldState,
onValueChange: (String) -> Unit,
onSearchClick: () -> Unit, onSearchClick: () -> Unit,
modifier: Modifier = Modifier, modifier: Modifier = Modifier,
enabled: Boolean = true, enabled: Boolean = true,
readOnly: Boolean = false, readOnly: Boolean = false,
height: Dp = 40.dp,
interactionSource: MutableInteractionSource = remember { MutableInteractionSource() }, interactionSource: MutableInteractionSource = remember { MutableInteractionSource() },
) { ) {
EditTextBox( EditTextBox(
value, state = state,
onValueChange, modifier = modifier,
modifier,
keyboardOptions = keyboardOptions =
KeyboardOptions( KeyboardOptions(
autoCorrectEnabled = false, autoCorrectEnabled = false,
imeAction = ImeAction.Search, imeAction = ImeAction.Search,
), ),
keyboardActions = onKeyboardAction = {
KeyboardActions( onSearchClick.invoke()
onSearch = { },
onSearchClick.invoke()
this.defaultKeyboardAction(ImeAction.Search)
},
),
leadingIcon = { leadingIcon = {
Icon( Icon(
imageVector = Icons.Default.Search, imageVector = Icons.Default.Search,
@ -184,7 +187,6 @@ fun SearchEditTextBox(
}, },
enabled = enabled, enabled = enabled,
readOnly = readOnly, readOnly = readOnly,
height = height,
interactionSource = interactionSource, interactionSource = interactionSource,
) )
} }
@ -193,16 +195,35 @@ fun SearchEditTextBox(
@Composable @Composable
private fun EditTextBoxPreview() { private fun EditTextBoxPreview() {
WholphinTheme { WholphinTheme {
Column { Column(verticalArrangement = Arrangement.spacedBy(8.dp)) {
EditTextBox( EditTextBox(
value = "string", state = rememberTextFieldState("string"),
onValueChange = {},
) )
SearchEditTextBox( SearchEditTextBox(
value = "search query", state = rememberTextFieldState("search query"),
onValueChange = {},
onSearchClick = { }, 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.LazyColumn
import androidx.compose.foundation.lazy.items import androidx.compose.foundation.lazy.items
import androidx.compose.foundation.shape.RoundedCornerShape import androidx.compose.foundation.shape.RoundedCornerShape
import androidx.compose.foundation.text.KeyboardActions
import androidx.compose.foundation.text.KeyboardOptions import androidx.compose.foundation.text.KeyboardOptions
import androidx.compose.foundation.text.input.rememberTextFieldState
import androidx.compose.material.icons.Icons import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.filled.Add import androidx.compose.material.icons.filled.Add
import androidx.compose.material3.CircularProgressIndicator import androidx.compose.material3.CircularProgressIndicator
@ -133,7 +133,7 @@ fun PlaylistList(
properties = DialogProperties(usePlatformDefaultWidth = false), properties = DialogProperties(usePlatformDefaultWidth = false),
elevation = 10.dp, elevation = 10.dp,
) { ) {
var playlistName by remember { mutableStateOf("") } val playlistName = rememberTextFieldState()
val focusRequester = remember { FocusRequester() } val focusRequester = remember { FocusRequester() }
LaunchedEffect(Unit) { focusRequester.tryRequestFocus() } LaunchedEffect(Unit) { focusRequester.tryRequestFocus() }
Column( Column(
@ -148,10 +148,7 @@ fun PlaylistList(
text = stringResource(R.string.name), text = stringResource(R.string.name),
) )
EditTextBox( EditTextBox(
value = playlistName, state = playlistName,
onValueChange = {
playlistName = it
},
keyboardOptions = keyboardOptions =
KeyboardOptions( KeyboardOptions(
capitalization = KeyboardCapitalization.Words, capitalization = KeyboardCapitalization.Words,
@ -159,13 +156,10 @@ fun PlaylistList(
keyboardType = KeyboardType.Text, keyboardType = KeyboardType.Text,
imeAction = ImeAction.Done, imeAction = ImeAction.Done,
), ),
keyboardActions = onKeyboardAction = {
KeyboardActions( showCreateDialog = false
onDone = { onCreatePlaylist.invoke(playlistName.text.toString())
showCreateDialog = false },
onCreatePlaylist.invoke(playlistName)
},
),
modifier = modifier =
Modifier Modifier
.focusRequester(focusRequester) .focusRequester(focusRequester)
@ -174,9 +168,9 @@ fun PlaylistList(
Button( Button(
onClick = { onClick = {
showCreateDialog = false showCreateDialog = false
onCreatePlaylist.invoke(playlistName) onCreatePlaylist.invoke(playlistName.text.toString())
}, },
enabled = playlistName.isNotNullOrBlank(), enabled = playlistName.text.isNotNullOrBlank(),
modifier = Modifier, modifier = Modifier,
) { ) {
Text(text = stringResource(R.string.submit)) 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.layout.padding
import androidx.compose.foundation.lazy.LazyColumn import androidx.compose.foundation.lazy.LazyColumn
import androidx.compose.foundation.lazy.LazyListScope import androidx.compose.foundation.lazy.LazyListScope
import androidx.compose.foundation.text.input.rememberTextFieldState
import androidx.compose.runtime.Composable import androidx.compose.runtime.Composable
import androidx.compose.runtime.LaunchedEffect import androidx.compose.runtime.LaunchedEffect
import androidx.compose.runtime.getValue import androidx.compose.runtime.getValue
@ -171,7 +172,7 @@ fun SearchPage(
val series by viewModel.series.observeAsState(SearchResult.NoQuery) val series by viewModel.series.observeAsState(SearchResult.NoQuery)
val episodes by viewModel.episodes.observeAsState(SearchResult.NoQuery) val episodes by viewModel.episodes.observeAsState(SearchResult.NoQuery)
var query by rememberSaveable { mutableStateOf("") } val query = rememberTextFieldState()
val focusRequester = remember { FocusRequester() } val focusRequester = remember { FocusRequester() }
var position by rememberPosition() var position by rememberPosition()
@ -179,7 +180,7 @@ fun SearchPage(
LaunchedEffect(query) { LaunchedEffect(query) {
delay(750L) delay(750L)
viewModel.search(query) viewModel.search(query.text.toString())
} }
LaunchedEffect(Unit) { LaunchedEffect(Unit) {
focusRequester.tryRequestFocus() focusRequester.tryRequestFocus()
@ -213,12 +214,9 @@ fun SearchPage(
focusManager.moveFocus(FocusDirection.Next) focusManager.moveFocus(FocusDirection.Next)
} }
SearchEditTextBox( SearchEditTextBox(
value = query, state = query,
onValueChange = {
query = it
},
onSearchClick = { onSearchClick = {
viewModel.search(query) viewModel.search(query.text.toString())
searchClicked = true searchClicked = true
}, },
modifier = modifier =

View file

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

View file

@ -1,16 +1,7 @@
package com.github.damontecres.wholphin.ui.preferences package com.github.damontecres.wholphin.ui.preferences
import androidx.compose.animation.AnimatedVisibility import androidx.compose.animation.AnimatedVisibility
import androidx.compose.foundation.background
import androidx.compose.foundation.interaction.MutableInteractionSource 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.foundation.text.KeyboardOptions
import androidx.compose.material.icons.Icons import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.filled.Done 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.remember
import androidx.compose.runtime.rememberCoroutineScope import androidx.compose.runtime.rememberCoroutineScope
import androidx.compose.runtime.setValue import androidx.compose.runtime.setValue
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier import androidx.compose.ui.Modifier
import androidx.compose.ui.graphics.Color import androidx.compose.ui.graphics.Color
import androidx.compose.ui.platform.LocalContext 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.res.stringResource
import androidx.compose.ui.text.TextStyle import androidx.compose.ui.text.TextStyle
import androidx.compose.ui.text.input.ImeAction 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.unit.dp
import androidx.compose.ui.window.Dialog
import androidx.tv.material3.Button
import androidx.tv.material3.MaterialTheme import androidx.tv.material3.MaterialTheme
import androidx.tv.material3.Text import androidx.tv.material3.Text
import androidx.tv.material3.surfaceColorAtElevation import androidx.tv.material3.surfaceColorAtElevation
import com.github.damontecres.wholphin.R
import com.github.damontecres.wholphin.preferences.AppChoicePreference import com.github.damontecres.wholphin.preferences.AppChoicePreference
import com.github.damontecres.wholphin.preferences.AppClickablePreference import com.github.damontecres.wholphin.preferences.AppClickablePreference
import com.github.damontecres.wholphin.preferences.AppDestinationPreference 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.DialogItem
import com.github.damontecres.wholphin.ui.components.DialogParams import com.github.damontecres.wholphin.ui.components.DialogParams
import com.github.damontecres.wholphin.ui.components.DialogPopup 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.ui.nav.Destination
import com.github.damontecres.wholphin.util.ExceptionHandler import com.github.damontecres.wholphin.util.ExceptionHandler
import kotlinx.coroutines.launch import kotlinx.coroutines.launch
import java.io.File
import java.util.SortedSet import java.util.SortedSet
@Suppress("UNCHECKED_CAST") @Suppress("UNCHECKED_CAST")
@ -82,6 +71,37 @@ fun <T> ComposablePreference(
} }
when (preference) { 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 -> is AppDestinationPreference ->
ClickPreference( ClickPreference(
title = title, title = title,
@ -244,73 +264,15 @@ fun <T> ComposablePreference(
} }
} }
AnimatedVisibility(showStringDialog != null) { AnimatedVisibility(showStringDialog != null) {
showStringDialog?.let { showStringDialog?.let { input ->
var mutableValue by remember { mutableStateOf(it.value ?: "") } StringInputDialog(
val onDone = { input = input,
it.onSubmit.invoke(mutableValue) onSave = {
} input.onSubmit.invoke(it)
Dialog( },
onDismissRequest = { showStringDialog = null }, onDismissRequest = { showStringDialog = null },
) { elevation = 3.dp,
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),
)
}
}
}
}
}
} }
} }
} }
@ -353,9 +315,11 @@ fun PreferenceSummary(
} }
} }
private data class StringInput( data class StringInput(
val title: String, val title: String,
val value: String?, val value: String?,
val keyboardOptions: KeyboardOptions, val keyboardOptions: KeyboardOptions,
val onSubmit: (String) -> Unit, 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.padding
import androidx.compose.foundation.layout.size import androidx.compose.foundation.layout.size
import androidx.compose.foundation.shape.RoundedCornerShape import androidx.compose.foundation.shape.RoundedCornerShape
import androidx.compose.foundation.text.KeyboardActions
import androidx.compose.foundation.text.KeyboardOptions import androidx.compose.foundation.text.KeyboardOptions
import androidx.compose.foundation.text.input.rememberTextFieldState
import androidx.compose.runtime.Composable import androidx.compose.runtime.Composable
import androidx.compose.runtime.LaunchedEffect import androidx.compose.runtime.LaunchedEffect
import androidx.compose.runtime.getValue 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.focus.focusRequester import androidx.compose.ui.focus.focusRequester
import androidx.compose.ui.res.stringResource 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.KeyboardCapitalization
import androidx.compose.ui.text.input.KeyboardType import androidx.compose.ui.text.input.KeyboardType
import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.dp
@ -170,9 +171,9 @@ fun SwitchServerContent(
viewModel.clearAddServerState() viewModel.clearAddServerState()
} }
val state by viewModel.addServerState.observeAsState(LoadingState.Pending) val state by viewModel.addServerState.observeAsState(LoadingState.Pending)
var url by remember { mutableStateOf("") } val url = rememberTextFieldState()
val submit = { val submit = {
viewModel.addServer(url) viewModel.addServer(url.text.toString())
} }
BasicDialog( BasicDialog(
onDismissRequest = { onDismissRequest = {
@ -196,21 +197,15 @@ fun SwitchServerContent(
text = stringResource(R.string.enter_server_url), text = stringResource(R.string.enter_server_url),
) )
EditTextBox( EditTextBox(
value = url, state = url,
onValueChange = {
url = it
viewModel.clearAddServerState()
},
keyboardOptions = keyboardOptions =
KeyboardOptions( KeyboardOptions(
capitalization = KeyboardCapitalization.None, capitalization = KeyboardCapitalization.None,
autoCorrectEnabled = false, autoCorrectEnabled = false,
keyboardType = KeyboardType.Uri, keyboardType = KeyboardType.Uri,
imeAction = ImeAction.Go,
), ),
keyboardActions = onKeyboardAction = { submit.invoke() },
KeyboardActions(
onDone = { submit.invoke() },
),
modifier = modifier =
Modifier Modifier
.focusRequester(focusRequester) .focusRequester(focusRequester)
@ -230,7 +225,7 @@ fun SwitchServerContent(
} }
Button( Button(
onClick = { submit.invoke() }, onClick = { submit.invoke() },
enabled = url.isNotNullOrBlank() && state == LoadingState.Pending, enabled = url.text.isNotNullOrBlank() && state == LoadingState.Pending,
modifier = Modifier, modifier = Modifier,
) { ) {
if (state == LoadingState.Loading) { 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.padding
import androidx.compose.foundation.layout.size import androidx.compose.foundation.layout.size
import androidx.compose.foundation.shape.RoundedCornerShape import androidx.compose.foundation.shape.RoundedCornerShape
import androidx.compose.foundation.text.KeyboardActions
import androidx.compose.foundation.text.KeyboardOptions import androidx.compose.foundation.text.KeyboardOptions
import androidx.compose.foundation.text.input.rememberTextFieldState
import androidx.compose.runtime.Composable import androidx.compose.runtime.Composable
import androidx.compose.runtime.LaunchedEffect import androidx.compose.runtime.LaunchedEffect
import androidx.compose.runtime.getValue import androidx.compose.runtime.getValue
@ -207,12 +207,17 @@ fun SwitchUserContent(
Text(text = "Use username/password") Text(text = "Use username/password")
} }
} else { } else {
var username by remember { mutableStateOf("") } val username = rememberTextFieldState()
var password by remember { mutableStateOf("") } val password = rememberTextFieldState()
val onSubmit = { val onSubmit = {
viewModel.login(server, username, password) viewModel.login(
server,
username.text.toString(),
password.text.toString(),
)
} }
val focusRequester = remember { FocusRequester() } val focusRequester = remember { FocusRequester() }
val passwordFocusRequester = remember { FocusRequester() }
LaunchedEffect(Unit) { focusRequester.tryRequestFocus() } LaunchedEffect(Unit) { focusRequester.tryRequestFocus() }
Text( Text(
text = "Enter username/password to login to ${server.name ?: server.url}", text = "Enter username/password to login to ${server.name ?: server.url}",
@ -229,11 +234,7 @@ fun SwitchUserContent(
modifier = Modifier.padding(end = 8.dp), modifier = Modifier.padding(end = 8.dp),
) )
EditTextBox( EditTextBox(
value = username, state = username,
onValueChange = {
username = it
viewModel.clearSwitchUserState()
},
keyboardOptions = keyboardOptions =
KeyboardOptions( KeyboardOptions(
capitalization = KeyboardCapitalization.None, capitalization = KeyboardCapitalization.None,
@ -241,6 +242,9 @@ fun SwitchUserContent(
keyboardType = KeyboardType.Text, keyboardType = KeyboardType.Text,
imeAction = ImeAction.Next, imeAction = ImeAction.Next,
), ),
onKeyboardAction = {
passwordFocusRequester.tryRequestFocus()
},
isInputValid = { userState !is LoadingState.Error }, isInputValid = { userState !is LoadingState.Error },
modifier = Modifier.focusRequester(focusRequester), modifier = Modifier.focusRequester(focusRequester),
) )
@ -253,29 +257,27 @@ fun SwitchUserContent(
text = "Password", text = "Password",
modifier = Modifier.padding(end = 8.dp), modifier = Modifier.padding(end = 8.dp),
) )
LaunchedEffect(password.text) {
viewModel.clearSwitchUserState()
}
EditTextBox( EditTextBox(
value = password, isPassword = true,
onValueChange = { state = password,
password = it
viewModel.clearSwitchUserState()
},
keyboardOptions = keyboardOptions =
KeyboardOptions( KeyboardOptions(
capitalization = KeyboardCapitalization.None, capitalization = KeyboardCapitalization.None,
autoCorrectEnabled = false, autoCorrectEnabled = false,
keyboardType = KeyboardType.Password, keyboardType = KeyboardType.Password,
imeAction = ImeAction.Go,
), ),
keyboardActions = onKeyboardAction = { onSubmit.invoke() },
KeyboardActions(
onDone = { onSubmit.invoke() },
),
isInputValid = { userState !is LoadingState.Error }, isInputValid = { userState !is LoadingState.Error },
modifier = Modifier, modifier = Modifier.focusRequester(passwordFocusRequester),
) )
} }
Button( Button(
onClick = { onSubmit.invoke() }, onClick = { onSubmit.invoke() },
enabled = username.isNotNullOrBlank() && password.isNotNullOrBlank(), enabled = username.text.isNotNullOrBlank() && password.text.isNotNullOrBlank(),
modifier = Modifier.align(Alignment.CenterHorizontally), modifier = Modifier.align(Alignment.CenterHorizontally),
) { ) {
Text("Login") Text("Login")

View file

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

View file

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

View file

@ -326,6 +326,9 @@
<string name="remove">Remove</string> <string name="remove">Remove</string>
<string name="dolby_vision">Dolby Vision</string> <string name="dolby_vision">Dolby Vision</string>
<string name="dolby_atmos">Dolby Atmos</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="subtitle_margin">Margin</string>
<string name="verbose_logging">Verbose logging</string> <string name="verbose_logging">Verbose logging</string>
<string name="image_cache_size">Image disk cache size (MB)</string> <string name="image_cache_size">Image disk cache size (MB)</string>