diff --git a/app/src/main/java/com/github/damontecres/wholphin/preferences/AppPreference.kt b/app/src/main/java/com/github/damontecres/wholphin/preferences/AppPreference.kt index 7b069dd9..96c69bb5 100644 --- a/app/src/main/java/com/github/damontecres/wholphin/preferences/AppPreference.kt +++ b/app/src/main/java/com/github/damontecres/wholphin/preferences/AppPreference.kt @@ -701,6 +701,24 @@ sealed interface AppPreference { summary = R.string.disable_if_crash, ) + val MpvGpuNext = + AppSwitchPreference( + 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( + title = R.string.mpv_conf, + summary = null, + ) + val DebugLogging = AppSwitchPreference( 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, + ), ), ), ), diff --git a/app/src/main/java/com/github/damontecres/wholphin/preferences/AppPreferencesSerializer.kt b/app/src/main/java/com/github/damontecres/wholphin/preferences/AppPreferencesSerializer.kt index 2a582049..1ad40c0e 100644 --- a/app/src/main/java/com/github/damontecres/wholphin/preferences/AppPreferencesSerializer.kt +++ b/app/src/main/java/com/github/damontecres/wholphin/preferences/AppPreferencesSerializer.kt @@ -68,6 +68,7 @@ class AppPreferencesSerializer .apply { enableHardwareDecoding = AppPreference.MpvHardwareDecoding.defaultValue + useGpuNext = AppPreference.MpvGpuNext.defaultValue }.build() }.build() homePagePreferences = diff --git a/app/src/main/java/com/github/damontecres/wholphin/services/AppUpgradeHandler.kt b/app/src/main/java/com/github/damontecres/wholphin/services/AppUpgradeHandler.kt index 492ac67b..89e9bce2 100644 --- a/app/src/main/java/com/github/damontecres/wholphin/services/AppUpgradeHandler.kt +++ b/app/src/main/java/com/github/damontecres/wholphin/services/AppUpgradeHandler.kt @@ -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 + } + } + } } diff --git a/app/src/main/java/com/github/damontecres/wholphin/services/PlayerFactory.kt b/app/src/main/java/com/github/damontecres/wholphin/services/PlayerFactory.kt index 57dc781b..c59ba81d 100644 --- a/app/src/main/java/com/github/damontecres/wholphin/services/PlayerFactory.kt +++ b/app/src/main/java/com/github/damontecres/wholphin/services/PlayerFactory.kt @@ -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 } diff --git a/app/src/main/java/com/github/damontecres/wholphin/ui/components/EditTextBox.kt b/app/src/main/java/com/github/damontecres/wholphin/ui/components/EditTextBox.kt index 63d98a4f..67faa648 100644 --- a/app/src/main/java/com/github/damontecres/wholphin/ui/components/EditTextBox.kt +++ b/app/src/main/java/com/github/damontecres/wholphin/ui/components/EditTextBox.kt @@ -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, + ) } } } diff --git a/app/src/main/java/com/github/damontecres/wholphin/ui/detail/PlaylistList.kt b/app/src/main/java/com/github/damontecres/wholphin/ui/detail/PlaylistList.kt index 12ad6ac3..8bea4a89 100644 --- a/app/src/main/java/com/github/damontecres/wholphin/ui/detail/PlaylistList.kt +++ b/app/src/main/java/com/github/damontecres/wholphin/ui/detail/PlaylistList.kt @@ -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)) diff --git a/app/src/main/java/com/github/damontecres/wholphin/ui/main/SearchPage.kt b/app/src/main/java/com/github/damontecres/wholphin/ui/main/SearchPage.kt index b4ee7b1b..7e258048 100644 --- a/app/src/main/java/com/github/damontecres/wholphin/ui/main/SearchPage.kt +++ b/app/src/main/java/com/github/damontecres/wholphin/ui/main/SearchPage.kt @@ -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 = diff --git a/app/src/main/java/com/github/damontecres/wholphin/ui/playback/DownloadSubtitlesDialog.kt b/app/src/main/java/com/github/damontecres/wholphin/ui/playback/DownloadSubtitlesDialog.kt index 557dbcce..0f156b58 100644 --- a/app/src/main/java/com/github/damontecres/wholphin/ui/playback/DownloadSubtitlesDialog.kt +++ b/app/src/main/java/com/github/damontecres/wholphin/ui/playback/DownloadSubtitlesDialog.kt @@ -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, diff --git a/app/src/main/java/com/github/damontecres/wholphin/ui/preferences/ComposablePreference.kt b/app/src/main/java/com/github/damontecres/wholphin/ui/preferences/ComposablePreference.kt index d9ccbc9f..5a4d4177 100644 --- a/app/src/main/java/com/github/damontecres/wholphin/ui/preferences/ComposablePreference.kt +++ b/app/src/main/java/com/github/damontecres/wholphin/ui/preferences/ComposablePreference.kt @@ -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 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 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, ) diff --git a/app/src/main/java/com/github/damontecres/wholphin/ui/preferences/StringInputDialog.kt b/app/src/main/java/com/github/damontecres/wholphin/ui/preferences/StringInputDialog.kt new file mode 100644 index 00000000..a5e0468f --- /dev/null +++ b/app/src/main/java/com/github/damontecres/wholphin/ui/preferences/StringInputDialog.kt @@ -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, + ) + } +} diff --git a/app/src/main/java/com/github/damontecres/wholphin/ui/setup/SwitchServerContent.kt b/app/src/main/java/com/github/damontecres/wholphin/ui/setup/SwitchServerContent.kt index 40066d95..ed5fb658 100644 --- a/app/src/main/java/com/github/damontecres/wholphin/ui/setup/SwitchServerContent.kt +++ b/app/src/main/java/com/github/damontecres/wholphin/ui/setup/SwitchServerContent.kt @@ -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) { diff --git a/app/src/main/java/com/github/damontecres/wholphin/ui/setup/SwitchUserContent.kt b/app/src/main/java/com/github/damontecres/wholphin/ui/setup/SwitchUserContent.kt index a33479cb..f57e4256 100644 --- a/app/src/main/java/com/github/damontecres/wholphin/ui/setup/SwitchUserContent.kt +++ b/app/src/main/java/com/github/damontecres/wholphin/ui/setup/SwitchUserContent.kt @@ -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") diff --git a/app/src/main/java/com/github/damontecres/wholphin/util/mpv/MpvPlayer.kt b/app/src/main/java/com/github/damontecres/wholphin/util/mpv/MpvPlayer.kt index 1ba202eb..d1bfd492 100644 --- a/app/src/main/java/com/github/damontecres/wholphin/util/mpv/MpvPlayer.kt +++ b/app/src/main/java/com/github/damontecres/wholphin/util/mpv/MpvPlayer.kt @@ -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") } diff --git a/app/src/main/proto/WholphinDataStore.proto b/app/src/main/proto/WholphinDataStore.proto index ad99df9f..7003217b 100644 --- a/app/src/main/proto/WholphinDataStore.proto +++ b/app/src/main/proto/WholphinDataStore.proto @@ -36,6 +36,7 @@ enum PlayerBackend{ message MpvOptions{ bool enable_hardware_decoding = 1; + bool use_gpu_next = 2; } message PlaybackOverrides{ diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index 37a6a444..a6698533 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -326,6 +326,9 @@ Remove Dolby Vision Dolby Atmos + MPV: Use gpu-next + Edit mpv.conf + Discard changes? Margin Verbose logging Image disk cache size (MB)