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:
parent
62480f090b
commit
33a2219716
@ -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…
Reference in New Issue
Block a user