mirror of
https://github.com/JustinZeus/Wholphin.git
synced 2026-07-08 15:50:16 +02:00
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:
parent
45cb385b9b
commit
a294661520
15 changed files with 408 additions and 269 deletions
|
|
@ -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,
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
|
|
|
|||
|
|
@ -68,6 +68,7 @@ class AppPreferencesSerializer
|
|||
.apply {
|
||||
enableHardwareDecoding =
|
||||
AppPreference.MpvHardwareDecoding.defaultValue
|
||||
useGpuNext = AppPreference.MpvGpuNext.defaultValue
|
||||
}.build()
|
||||
}.build()
|
||||
homePagePreferences =
|
||||
|
|
|
|||
|
|
@ -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
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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
|
||||
.defaultMinSize(
|
||||
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,
|
||||
),
|
||||
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()
|
||||
cursorBrush = SolidColor(colors.cursorColor),
|
||||
decorator = decorator,
|
||||
)
|
||||
} 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,
|
||||
BasicTextField(
|
||||
state = state,
|
||||
modifier =
|
||||
modifier.defaultMinSize(
|
||||
minWidth = TextFieldDefaults.MinWidth,
|
||||
),
|
||||
container = {
|
||||
Container(
|
||||
enabled = enabled,
|
||||
isError = !isInputValid.invoke(value),
|
||||
readOnly = readOnly,
|
||||
keyboardOptions = keyboardOptions,
|
||||
onKeyboardAction = onKeyboardAction,
|
||||
textStyle = MaterialTheme.typography.bodyLarge.merge(textColor),
|
||||
interactionSource = interactionSource,
|
||||
modifier = Modifier,
|
||||
colors = colors,
|
||||
shape = CircleShape,
|
||||
focusedIndicatorLineThickness = 4.dp,
|
||||
unfocusedIndicatorLineThickness = 0.dp,
|
||||
)
|
||||
},
|
||||
)
|
||||
},
|
||||
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 = {
|
||||
onKeyboardAction = {
|
||||
onSearchClick.invoke()
|
||||
this.defaultKeyboardAction(ImeAction.Search)
|
||||
},
|
||||
),
|
||||
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,
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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 = {
|
||||
onKeyboardAction = {
|
||||
showCreateDialog = false
|
||||
onCreatePlaylist.invoke(playlistName)
|
||||
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))
|
||||
|
|
|
|||
|
|
@ -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 =
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
elevation = 3.dp,
|
||||
)
|
||||
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 value: String?,
|
||||
val keyboardOptions: KeyboardOptions,
|
||||
val onSubmit: (String) -> Unit,
|
||||
val maxLines: Int = 1,
|
||||
val confirmDiscard: Boolean = false,
|
||||
)
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
|
@ -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) {
|
||||
|
|
|
|||
|
|
@ -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),
|
||||
)
|
||||
EditTextBox(
|
||||
value = password,
|
||||
onValueChange = {
|
||||
password = it
|
||||
LaunchedEffect(password.text) {
|
||||
viewModel.clearSwitchUserState()
|
||||
},
|
||||
}
|
||||
EditTextBox(
|
||||
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")
|
||||
|
|
|
|||
|
|
@ -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")
|
||||
}
|
||||
|
|
|
|||
|
|
@ -36,6 +36,7 @@ enum PlayerBackend{
|
|||
|
||||
message MpvOptions{
|
||||
bool enable_hardware_decoding = 1;
|
||||
bool use_gpu_next = 2;
|
||||
}
|
||||
|
||||
message PlaybackOverrides{
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue