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

* Enable `confirmButton` when appropriate

* Show error in dialog instead

* Follow M3 guidelines
This commit is contained in:
zbue 2023-01-15 07:24:57 +08:00 committed by GitHub
parent 62480f090b
commit 33a2219716
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
12 changed files with 68 additions and 47 deletions

View File

@ -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()
} }
} }

View File

@ -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()
} }
} }

View File

@ -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(
onCreate(name) enabled = name.isNotEmpty() && !nameAlreadyExists,
onDismissRequest() onClick = {
},) { onCreate(name)
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(
onRename(name) enabled = valueHasChanged && !nameAlreadyExists,
onDismissRequest() onClick = {
},) { onRename(name)
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,
) )
}, },

View File

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

View File

@ -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))

View File

@ -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)

View File

@ -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))
} }
}, },

View File

@ -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
} }

View File

@ -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)) {

View File

@ -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,
) )
} }

View File

@ -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)
} }

View File

@ -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" -->