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

* Enable `confirmButton` when appropriate

* Show error in dialog instead

* Follow M3 guidelines

(cherry picked from commit 33a221971692c1662dc883a7bac9fdcc7b843d35)

# Conflicts:
#	app/src/main/java/eu/kanade/domain/category/interactor/CreateCategoryWithName.kt
#	app/src/main/java/eu/kanade/presentation/category/components/CategoryDialogs.kt
#	app/src/main/java/eu/kanade/tachiyomi/ui/category/CategoryScreen.kt
This commit is contained in:
zbue 2023-01-15 07:24:57 +08:00 committed by Jobobby04
parent 843c0a4588
commit 7f7789792b
21 changed files with 76 additions and 69 deletions

View File

@ -1,7 +1,6 @@
package eu.kanade.domain.category.interactor
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.library.service.LibraryPreferences
import eu.kanade.tachiyomi.util.lang.withNonCancellableContext
@ -23,10 +22,6 @@ class CreateCategoryWithName(
suspend fun await(name: String): Result = withNonCancellableContext {
val categories = categoryRepository.getAll()
if (categories.anyWithName(name)) {
return@withNonCancellableContext Result.NameAlreadyExistsError
}
val nextOrder = categories.maxOfOrNull { it.order }?.plus(1) ?: 0
val newCategory = Category(
id = 0,
@ -49,7 +44,6 @@ class CreateCategoryWithName(
data class Success(val category: Category) : Result()
// SY <--
object NameAlreadyExistsError : 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.CategoryUpdate
import eu.kanade.domain.category.model.anyWithName
import eu.kanade.domain.category.repository.CategoryRepository
import eu.kanade.tachiyomi.util.lang.withNonCancellableContext
import eu.kanade.tachiyomi.util.system.logcat
@ -13,11 +12,6 @@ class RenameCategory(
) {
suspend fun await(categoryId: Long, name: String) = withNonCancellableContext {
val categories = categoryRepository.getAll()
if (categories.anyWithName(name)) {
return@withNonCancellableContext Result.NameAlreadyExistsError
}
val update = CategoryUpdate(
id = categoryId,
name = name,
@ -36,7 +30,6 @@ class RenameCategory(
sealed class Result {
object Success : Result()
object NameAlreadyExistsError : Result()
data class InternalError(val error: Throwable) : Result()
}
}

View File

@ -6,11 +6,6 @@ import eu.kanade.tachiyomi.util.preference.plusAssign
class CreateSourceCategory(private val preferences: SourcePreferences) {
fun await(category: String): Result {
// Do not allow duplicate categories.
if (categoryExists(category)) {
return Result.CategoryExists
}
if (category.contains("|")) {
return Result.InvalidName
}
@ -22,15 +17,7 @@ class CreateSourceCategory(private val preferences: SourcePreferences) {
}
sealed class Result {
object CategoryExists : Result()
object InvalidName : Result()
object Success : Result()
}
/**
* Returns true if a repo with the given name already exists.
*/
private fun categoryExists(name: String): Boolean {
return preferences.sourcesTabCategories().get().any { it.equals(name, true) }
}
}

View File

@ -6,11 +6,6 @@ import eu.kanade.tachiyomi.util.preference.plusAssign
class CreateSourceRepo(private val preferences: UnsortedPreferences) {
fun await(name: String): Result {
// Do not allow duplicate repos.
if (repoExists(name)) {
return Result.RepoExists
}
// Do not allow invalid formats
if (!name.matches(repoRegex)) {
return Result.InvalidName
@ -22,7 +17,6 @@ class CreateSourceRepo(private val preferences: UnsortedPreferences) {
}
sealed class Result {
object RepoExists : Result()
object InvalidName : Result()
object Success : Result()
}

View File

@ -10,7 +10,6 @@ class RenameSourceCategory(
fun await(categoryOld: String, categoryNew: String): CreateSourceCategory.Result {
when (val result = createSourceCategory.await(categoryNew)) {
CreateSourceCategory.Result.CategoryExists -> return result
CreateSourceCategory.Result.InvalidName -> return result
CreateSourceCategory.Result.Success -> {}
}

View File

@ -24,20 +24,27 @@ fun CategoryCreateDialog(
onDismissRequest: () -> Unit,
onCreate: (String) -> Unit,
// SY -->
categories: List<String>,
title: String,
extraMessage: String? = null,
alreadyExistsError: Int = R.string.error_category_exists,
// SY <--
) {
var name by remember { mutableStateOf("") }
val focusRequester = remember { FocusRequester() }
val nameAlreadyExists = remember(name) { categories.contains(name) }
AlertDialog(
onDismissRequest = onDismissRequest,
confirmButton = {
TextButton(onClick = {
onCreate(name)
onDismissRequest()
},) {
TextButton(
enabled = name.isNotEmpty() && !nameAlreadyExists,
onClick = {
onCreate(name)
onDismissRequest()
},
) {
Text(text = stringResource(R.string.action_add))
}
},
@ -64,6 +71,11 @@ fun CategoryCreateDialog(
label = {
Text(text = stringResource(R.string.name))
},
supportingText = {
val msgRes = if (name.isNotEmpty() && nameAlreadyExists) alreadyExistsError else R.string.information_required_plain
Text(text = stringResource(msgRes))
},
isError = name.isNotEmpty() && nameAlreadyExists,
singleLine = true,
)
// SY -->
@ -83,18 +95,27 @@ fun CategoryCreateDialog(
fun CategoryRenameDialog(
onDismissRequest: () -> Unit,
onRename: (String) -> Unit,
// SY -->
categories: List<String>,
category: String,
alreadyExistsError: Int = R.string.error_category_exists,
// SY <--
) {
var name by remember { mutableStateOf(category) }
var valueHasChanged by remember { mutableStateOf(false) }
val focusRequester = remember { FocusRequester() }
val nameAlreadyExists = remember(name) { categories.contains(name) }
AlertDialog(
onDismissRequest = onDismissRequest,
confirmButton = {
TextButton(onClick = {
onRename(name)
onDismissRequest()
},) {
TextButton(
enabled = valueHasChanged && !nameAlreadyExists,
onClick = {
onRename(name)
onDismissRequest()
},
) {
Text(text = stringResource(android.R.string.ok))
}
},
@ -108,13 +129,18 @@ fun CategoryRenameDialog(
},
text = {
OutlinedTextField(
modifier = Modifier
.focusRequester(focusRequester),
modifier = Modifier.focusRequester(focusRequester),
value = name,
onValueChange = { name = it },
label = {
Text(text = stringResource(R.string.name))
onValueChange = {
valueHasChanged = name != it
name = it
},
label = { Text(text = stringResource(R.string.name)) },
supportingText = {
val msgRes = if (valueHasChanged && nameAlreadyExists) alreadyExistsError else R.string.information_required_plain
Text(text = stringResource(msgRes))
},
isError = valueHasChanged && nameAlreadyExists,
singleLine = true,
)
},

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -1,7 +1,12 @@
package eu.kanade.presentation.more.settings.widget
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.Icon
import androidx.compose.material3.IconButton
import androidx.compose.material3.OutlinedTextField
import androidx.compose.material3.Text
import androidx.compose.material3.TextButton
@ -50,6 +55,16 @@ fun EditTextPreferenceWidget(
OutlinedTextField(
value = textFieldValue,
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,
modifier = Modifier.fillMaxWidth(),
)
@ -59,6 +74,7 @@ fun EditTextPreferenceWidget(
),
confirmButton = {
TextButton(
enabled = textFieldValue.text != value && textFieldValue.text.isNotBlank(),
onClick = {
scope.launch {
if (onConfirm(textFieldValue.text)) {

View File

@ -6,6 +6,7 @@ import androidx.compose.runtime.collectAsState
import androidx.compose.runtime.getValue
import androidx.compose.ui.platform.LocalContext
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.util.fastMap
import cafe.adriel.voyager.core.model.rememberScreenModel
import cafe.adriel.voyager.core.screen.Screen
import cafe.adriel.voyager.core.screen.uniqueScreenKey
@ -54,8 +55,9 @@ class CategoryScreen : Screen {
CategoryDialog.Create -> {
CategoryCreateDialog(
onDismissRequest = screenModel::dismissDialog,
onCreate = { screenModel.createCategory(it) },
onCreate = screenModel::createCategory,
// SY -->
categories = successState.categories.fastMap { it.name },
title = stringResource(R.string.action_add_category),
// SY <--
)
@ -65,6 +67,7 @@ class CategoryScreen : Screen {
onDismissRequest = screenModel::dismissDialog,
onRename = { screenModel.renameCategory(dialog.category, it) },
// SY -->
categories = successState.categories.fastMap { it.name },
category = dialog.category.name,
// SY <--
)

View File

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

View File

@ -49,8 +49,10 @@ class SortTagScreen : Screen {
CategoryCreateDialog(
onDismissRequest = screenModel::dismissDialog,
onCreate = { screenModel.createTag(it) },
categories = successState.tags,
title = stringResource(R.string.add_tag),
extraMessage = stringResource(R.string.action_add_tags_message),
alreadyExistsError = R.string.error_tag_exists,
)
}
is SortTagDialog.Delete -> {

View File

@ -48,8 +48,10 @@ class RepoScreen : Screen {
CategoryCreateDialog(
onDismissRequest = screenModel::dismissDialog,
onCreate = { screenModel.createRepo(it) },
categories = successState.repos,
title = stringResource(R.string.action_add_repo),
extraMessage = stringResource(R.string.action_add_repo_message),
alreadyExistsError = R.string.error_repo_exists,
)
}
is RepoDialog.Delete -> {

View File

@ -46,7 +46,6 @@ class RepoScreenModel(
fun createRepo(name: String) {
coroutineScope.launchIO {
when (createSourceRepo.await(name)) {
is CreateSourceRepo.Result.RepoExists -> _events.send(RepoEvent.RepoExists)
is CreateSourceRepo.Result.InvalidName -> _events.send(RepoEvent.InvalidName)
else -> {}
}
@ -85,7 +84,6 @@ class RepoScreenModel(
sealed class RepoEvent {
sealed class LocalizedMessage(@StringRes val stringRes: Int) : RepoEvent()
object RepoExists : LocalizedMessage(R.string.error_repo_exists)
object InvalidName : LocalizedMessage(R.string.invalid_repo_name)
object InternalError : LocalizedMessage(R.string.internal_error)
}

View File

@ -51,6 +51,7 @@ class SourceCategoryScreen : Screen {
onDismissRequest = screenModel::dismissDialog,
onCreate = { screenModel.createCategory(it) },
// SY -->
categories = successState.categories,
title = stringResource(R.string.action_add_category),
// SY <--
)
@ -60,6 +61,7 @@ class SourceCategoryScreen : Screen {
onDismissRequest = screenModel::dismissDialog,
onRename = { screenModel.renameCategory(dialog.category, it) },
// SY -->
categories = successState.categories,
category = dialog.category,
// SY <--
)

View File

@ -48,7 +48,6 @@ class SourceCategoryScreenModel(
fun createCategory(name: String) {
coroutineScope.launchIO {
when (createSourceCategory.await(name)) {
is CreateSourceCategory.Result.CategoryExists -> _events.send(SourceCategoryEvent.CategoryExists)
is CreateSourceCategory.Result.InvalidName -> _events.send(SourceCategoryEvent.InvalidName)
else -> {}
}
@ -75,7 +74,6 @@ class SourceCategoryScreenModel(
fun renameCategory(categoryOld: String, categoryNew: String) {
coroutineScope.launchIO {
when (renameSourceCategory.await(categoryOld, categoryNew)) {
is CreateSourceCategory.Result.CategoryExists -> _events.send(SourceCategoryEvent.CategoryExists)
is CreateSourceCategory.Result.InvalidName -> _events.send(SourceCategoryEvent.InvalidName)
else -> {}
}
@ -103,7 +101,6 @@ class SourceCategoryScreenModel(
sealed class SourceCategoryEvent {
sealed class LocalizedMessage(@StringRes val stringRes: Int) : SourceCategoryEvent()
object CategoryExists : LocalizedMessage(R.string.error_category_exists)
object InvalidName : LocalizedMessage(R.string.invalid_category_name)
object InternalError : LocalizedMessage(R.string.internal_error)
}

View File

@ -206,7 +206,6 @@ class FavoritesSyncHelper(val context: Context) {
val local = localCategories.getOrElse(index) {
when (val createCategoryWithNameResult = createCategoryWithName.await(remote)) {
is CreateCategoryWithName.Result.InternalError -> throw createCategoryWithNameResult.error
CreateCategoryWithName.Result.NameAlreadyExistsError -> throw IllegalStateException("Category $remote already exists")
is CreateCategoryWithName.Result.Success -> createCategoryWithNameResult.category
}
}

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_dialog">You don\'t have any categories yet.</string>
<string name="information_cloudflare_bypass_failure">Failed to bypass Cloudflare</string>
<string name="information_required_plain">*required</string>
<!-- Do not translate "WebView" -->
<string name="information_webview_required">WebView is required for Tachiyomi</string>
<!-- Do not translate "WebView" -->