Enable `confirmButton` only when needed to respond to user input (#8848)

* Enable `confirmButton` when appropriate

* Show error in dialog instead

* Follow M3 guidelines
pull/8917/head
zbue 2 years ago committed by GitHub
parent 62480f090b
commit 33a2219716
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23

@ -1,7 +1,6 @@
package eu.kanade.domain.category.interactor package eu.kanade.domain.category.interactor
import eu.kanade.domain.category.model.Category import eu.kanade.domain.category.model.Category
import eu.kanade.domain.category.model.anyWithName
import eu.kanade.domain.category.repository.CategoryRepository import eu.kanade.domain.category.repository.CategoryRepository
import eu.kanade.domain.library.service.LibraryPreferences import eu.kanade.domain.library.service.LibraryPreferences
import eu.kanade.tachiyomi.util.lang.withNonCancellableContext import eu.kanade.tachiyomi.util.lang.withNonCancellableContext
@ -23,10 +22,6 @@ class CreateCategoryWithName(
suspend fun await(name: String): Result = withNonCancellableContext { suspend fun await(name: String): Result = withNonCancellableContext {
val categories = categoryRepository.getAll() val categories = categoryRepository.getAll()
if (categories.anyWithName(name)) {
return@withNonCancellableContext Result.NameAlreadyExistsError
}
val nextOrder = categories.maxOfOrNull { it.order }?.plus(1) ?: 0 val nextOrder = categories.maxOfOrNull { it.order }?.plus(1) ?: 0
val newCategory = Category( val newCategory = Category(
id = 0, id = 0,
@ -46,7 +41,6 @@ class CreateCategoryWithName(
sealed class Result { sealed class Result {
object Success : Result() object Success : Result()
object NameAlreadyExistsError : Result()
data class InternalError(val error: Throwable) : Result() data class InternalError(val error: Throwable) : Result()
} }
} }

@ -2,7 +2,6 @@ package eu.kanade.domain.category.interactor
import eu.kanade.domain.category.model.Category import eu.kanade.domain.category.model.Category
import eu.kanade.domain.category.model.CategoryUpdate import eu.kanade.domain.category.model.CategoryUpdate
import eu.kanade.domain.category.model.anyWithName
import eu.kanade.domain.category.repository.CategoryRepository import eu.kanade.domain.category.repository.CategoryRepository
import eu.kanade.tachiyomi.util.lang.withNonCancellableContext import eu.kanade.tachiyomi.util.lang.withNonCancellableContext
import eu.kanade.tachiyomi.util.system.logcat import eu.kanade.tachiyomi.util.system.logcat
@ -13,11 +12,6 @@ class RenameCategory(
) { ) {
suspend fun await(categoryId: Long, name: String) = withNonCancellableContext { suspend fun await(categoryId: Long, name: String) = withNonCancellableContext {
val categories = categoryRepository.getAll()
if (categories.anyWithName(name)) {
return@withNonCancellableContext Result.NameAlreadyExistsError
}
val update = CategoryUpdate( val update = CategoryUpdate(
id = categoryId, id = categoryId,
name = name, name = name,
@ -36,7 +30,6 @@ class RenameCategory(
sealed class Result { sealed class Result {
object Success : Result() object Success : Result()
object NameAlreadyExistsError : Result()
data class InternalError(val error: Throwable) : Result() data class InternalError(val error: Throwable) : Result()
} }
} }

@ -15,6 +15,7 @@ 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 eu.kanade.domain.category.model.Category import eu.kanade.domain.category.model.Category
import eu.kanade.domain.category.model.anyWithName
import eu.kanade.tachiyomi.R import eu.kanade.tachiyomi.R
import kotlinx.coroutines.delay import kotlinx.coroutines.delay
import kotlin.time.Duration.Companion.seconds import kotlin.time.Duration.Companion.seconds
@ -23,17 +24,23 @@ import kotlin.time.Duration.Companion.seconds
fun CategoryCreateDialog( fun CategoryCreateDialog(
onDismissRequest: () -> Unit, onDismissRequest: () -> Unit,
onCreate: (String) -> Unit, onCreate: (String) -> Unit,
categories: List<Category>,
) { ) {
var name by remember { mutableStateOf("") } var name by remember { mutableStateOf("") }
val focusRequester = remember { FocusRequester() } val focusRequester = remember { FocusRequester() }
val nameAlreadyExists = remember(name) { categories.anyWithName(name) }
AlertDialog( AlertDialog(
onDismissRequest = onDismissRequest, onDismissRequest = onDismissRequest,
confirmButton = { confirmButton = {
TextButton(onClick = { TextButton(
enabled = name.isNotEmpty() && !nameAlreadyExists,
onClick = {
onCreate(name) onCreate(name)
onDismissRequest() onDismissRequest()
},) { },
) {
Text(text = stringResource(R.string.action_add)) Text(text = stringResource(R.string.action_add))
} }
}, },
@ -47,13 +54,15 @@ fun CategoryCreateDialog(
}, },
text = { text = {
OutlinedTextField( OutlinedTextField(
modifier = Modifier modifier = Modifier.focusRequester(focusRequester),
.focusRequester(focusRequester),
value = name, value = name,
onValueChange = { name = it }, onValueChange = { name = it },
label = { label = { Text(text = stringResource(R.string.name)) },
Text(text = stringResource(R.string.name)) supportingText = {
val msgRes = if (name.isNotEmpty() && nameAlreadyExists) R.string.error_category_exists else R.string.information_required_plain
Text(text = stringResource(msgRes))
}, },
isError = name.isNotEmpty() && nameAlreadyExists,
singleLine = true, singleLine = true,
) )
}, },
@ -70,18 +79,25 @@ fun CategoryCreateDialog(
fun CategoryRenameDialog( fun CategoryRenameDialog(
onDismissRequest: () -> Unit, onDismissRequest: () -> Unit,
onRename: (String) -> Unit, onRename: (String) -> Unit,
categories: List<Category>,
category: Category, category: Category,
) { ) {
var name by remember { mutableStateOf(category.name) } var name by remember { mutableStateOf(category.name) }
var valueHasChanged by remember { mutableStateOf(false) }
val focusRequester = remember { FocusRequester() } val focusRequester = remember { FocusRequester() }
val nameAlreadyExists = remember(name) { categories.anyWithName(name) }
AlertDialog( AlertDialog(
onDismissRequest = onDismissRequest, onDismissRequest = onDismissRequest,
confirmButton = { confirmButton = {
TextButton(onClick = { TextButton(
enabled = valueHasChanged && !nameAlreadyExists,
onClick = {
onRename(name) onRename(name)
onDismissRequest() onDismissRequest()
},) { },
) {
Text(text = stringResource(android.R.string.ok)) Text(text = stringResource(android.R.string.ok))
} }
}, },
@ -95,13 +111,18 @@ fun CategoryRenameDialog(
}, },
text = { text = {
OutlinedTextField( OutlinedTextField(
modifier = Modifier modifier = Modifier.focusRequester(focusRequester),
.focusRequester(focusRequester),
value = name, value = name,
onValueChange = { name = it }, onValueChange = {
label = { valueHasChanged = name != it
Text(text = stringResource(R.string.name)) name = it
},
label = { Text(text = stringResource(R.string.name)) },
supportingText = {
val msgRes = if (valueHasChanged && nameAlreadyExists) R.string.error_category_exists else R.string.information_required_plain
Text(text = stringResource(msgRes))
}, },
isError = valueHasChanged && nameAlreadyExists,
singleLine = true, singleLine = true,
) )
}, },

@ -44,6 +44,7 @@ fun DeleteLibraryMangaDialog(
}, },
confirmButton = { confirmButton = {
TextButton( TextButton(
enabled = list.any { it.isChecked },
onClick = { onClick = {
onDismissRequest() onDismissRequest()
onConfirm( onConfirm(

@ -42,6 +42,7 @@ fun DownloadCustomAmountDialog(
}, },
confirmButton = { confirmButton = {
TextButton( TextButton(
enabled = amount != 0,
onClick = { onClick = {
onDismissRequest() onDismissRequest()
onConfirm(amount.coerceIn(0, maxAmount)) onConfirm(amount.coerceIn(0, maxAmount))

@ -275,10 +275,6 @@ object SettingsAdvancedScreen : SearchableSettings {
pref = userAgentPref, pref = userAgentPref,
title = stringResource(R.string.pref_user_agent_string), title = stringResource(R.string.pref_user_agent_string),
onValueChanged = { onValueChanged = {
if (it.isBlank()) {
context.toast(R.string.error_user_agent_string_blank)
return@EditTextPreference false
}
try { try {
// OkHttp checks for valid values internally // OkHttp checks for valid values internally
Headers.Builder().add("User-Agent", it) Headers.Builder().add("User-Agent", it)

@ -315,7 +315,10 @@ object SettingsLibraryScreen : SearchableSettings {
} }
}, },
confirmButton = { confirmButton = {
TextButton(onClick = { onValueChanged(portraitValue, landscapeValue) }) { TextButton(
enabled = portraitValue != initialPortrait || landscapeValue != initialLandscape,
onClick = { onValueChanged(portraitValue, landscapeValue) },
) {
Text(text = stringResource(android.R.string.ok)) Text(text = stringResource(android.R.string.ok))
} }
}, },

@ -222,7 +222,7 @@ object SettingsTrackingScreen : SearchableSettings {
label = { Text(text = stringResource(uNameStringRes)) }, label = { Text(text = stringResource(uNameStringRes)) },
keyboardOptions = KeyboardOptions(imeAction = ImeAction.Next), keyboardOptions = KeyboardOptions(imeAction = ImeAction.Next),
singleLine = true, singleLine = true,
isError = inputError && username.text.isEmpty(), isError = inputError && !processing,
) )
var hidePassword by remember { mutableStateOf(true) } var hidePassword by remember { mutableStateOf(true) }
@ -253,21 +253,16 @@ object SettingsTrackingScreen : SearchableSettings {
imeAction = ImeAction.Done, imeAction = ImeAction.Done,
), ),
singleLine = true, singleLine = true,
isError = inputError && password.text.isEmpty(), isError = inputError && !processing,
) )
} }
}, },
confirmButton = { confirmButton = {
Button( Button(
modifier = Modifier.fillMaxWidth(), modifier = Modifier.fillMaxWidth(),
enabled = !processing, enabled = !processing && username.text.isNotBlank() && password.text.isNotBlank(),
onClick = { onClick = {
if (username.text.isEmpty() || password.text.isEmpty()) {
inputError = true
return@Button
}
scope.launchIO { scope.launchIO {
inputError = false
processing = true processing = true
val result = checkLogin( val result = checkLogin(
context = context, context = context,
@ -275,6 +270,7 @@ object SettingsTrackingScreen : SearchableSettings {
username = username.text, username = username.text,
password = password.text, password = password.text,
) )
inputError = !result
if (result) onDismissRequest() if (result) onDismissRequest()
processing = false processing = false
} }

@ -1,7 +1,12 @@
package eu.kanade.presentation.more.settings.widget package eu.kanade.presentation.more.settings.widget
import androidx.compose.foundation.layout.fillMaxWidth import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.filled.Cancel
import androidx.compose.material.icons.filled.Error
import androidx.compose.material3.AlertDialog import androidx.compose.material3.AlertDialog
import androidx.compose.material3.Icon
import androidx.compose.material3.IconButton
import androidx.compose.material3.OutlinedTextField import androidx.compose.material3.OutlinedTextField
import androidx.compose.material3.Text import androidx.compose.material3.Text
import androidx.compose.material3.TextButton import androidx.compose.material3.TextButton
@ -50,6 +55,16 @@ fun EditTextPreferenceWidget(
OutlinedTextField( OutlinedTextField(
value = textFieldValue, value = textFieldValue,
onValueChange = { textFieldValue = it }, onValueChange = { textFieldValue = it },
trailingIcon = {
if (textFieldValue.text.isBlank()) {
Icon(imageVector = Icons.Filled.Error, contentDescription = null)
} else {
IconButton(onClick = { textFieldValue = TextFieldValue("") }) {
Icon(imageVector = Icons.Filled.Cancel, contentDescription = null)
}
}
},
isError = textFieldValue.text.isBlank(),
singleLine = true, singleLine = true,
modifier = Modifier.fillMaxWidth(), modifier = Modifier.fillMaxWidth(),
) )
@ -59,6 +74,7 @@ fun EditTextPreferenceWidget(
), ),
confirmButton = { confirmButton = {
TextButton( TextButton(
enabled = textFieldValue.text != value && textFieldValue.text.isNotBlank(),
onClick = { onClick = {
scope.launch { scope.launch {
if (onConfirm(textFieldValue.text)) { if (onConfirm(textFieldValue.text)) {

@ -52,13 +52,15 @@ class CategoryScreen : Screen {
CategoryDialog.Create -> { CategoryDialog.Create -> {
CategoryCreateDialog( CategoryCreateDialog(
onDismissRequest = screenModel::dismissDialog, onDismissRequest = screenModel::dismissDialog,
onCreate = { screenModel.createCategory(it) }, onCreate = screenModel::createCategory,
categories = successState.categories,
) )
} }
is CategoryDialog.Rename -> { is CategoryDialog.Rename -> {
CategoryRenameDialog( CategoryRenameDialog(
onDismissRequest = screenModel::dismissDialog, onDismissRequest = screenModel::dismissDialog,
onRename = { screenModel.renameCategory(dialog.category, it) }, onRename = { screenModel.renameCategory(dialog.category, it) },
categories = successState.categories,
category = dialog.category, category = dialog.category,
) )
} }

@ -47,7 +47,6 @@ class CategoryScreenModel(
coroutineScope.launch { coroutineScope.launch {
when (createCategoryWithName.await(name)) { when (createCategoryWithName.await(name)) {
is CreateCategoryWithName.Result.InternalError -> _events.send(CategoryEvent.InternalError) is CreateCategoryWithName.Result.InternalError -> _events.send(CategoryEvent.InternalError)
CreateCategoryWithName.Result.NameAlreadyExistsError -> _events.send(CategoryEvent.CategoryWithNameAlreadyExists)
else -> {} else -> {}
} }
} }
@ -84,7 +83,6 @@ class CategoryScreenModel(
coroutineScope.launch { coroutineScope.launch {
when (renameCategory.await(category, name)) { when (renameCategory.await(category, name)) {
is RenameCategory.Result.InternalError -> _events.send(CategoryEvent.InternalError) is RenameCategory.Result.InternalError -> _events.send(CategoryEvent.InternalError)
RenameCategory.Result.NameAlreadyExistsError -> _events.send(CategoryEvent.CategoryWithNameAlreadyExists)
else -> {} else -> {}
} }
} }
@ -117,7 +115,6 @@ sealed class CategoryDialog {
sealed class CategoryEvent { sealed class CategoryEvent {
sealed class LocalizedMessage(@StringRes val stringRes: Int) : CategoryEvent() sealed class LocalizedMessage(@StringRes val stringRes: Int) : CategoryEvent()
object CategoryWithNameAlreadyExists : LocalizedMessage(R.string.error_category_exists)
object InternalError : LocalizedMessage(R.string.internal_error) object InternalError : LocalizedMessage(R.string.internal_error)
} }

@ -882,6 +882,7 @@
<string name="information_empty_category">You have no categories. Tap the plus button to create one for organizing your library.</string> <string name="information_empty_category">You have no categories. Tap the plus button to create one for organizing your library.</string>
<string name="information_empty_category_dialog">You don\'t have any categories yet.</string> <string name="information_empty_category_dialog">You don\'t have any categories yet.</string>
<string name="information_cloudflare_bypass_failure">Failed to bypass Cloudflare</string> <string name="information_cloudflare_bypass_failure">Failed to bypass Cloudflare</string>
<string name="information_required_plain">*required</string>
<!-- Do not translate "WebView" --> <!-- Do not translate "WebView" -->
<string name="information_webview_required">WebView is required for Tachiyomi</string> <string name="information_webview_required">WebView is required for Tachiyomi</string>
<!-- Do not translate "WebView" --> <!-- Do not translate "WebView" -->

Loading…
Cancel
Save