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:
parent
843c0a4588
commit
7f7789792b
@ -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()
|
||||
}
|
||||
}
|
||||
|
@ -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()
|
||||
}
|
||||
}
|
||||
|
@ -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) }
|
||||
}
|
||||
}
|
||||
|
@ -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()
|
||||
}
|
||||
|
@ -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 -> {}
|
||||
}
|
||||
|
@ -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,
|
||||
)
|
||||
},
|
||||
|
@ -44,6 +44,7 @@ fun DeleteLibraryMangaDialog(
|
||||
},
|
||||
confirmButton = {
|
||||
TextButton(
|
||||
enabled = list.any { it.isChecked },
|
||||
onClick = {
|
||||
onDismissRequest()
|
||||
onConfirm(
|
||||
|
@ -42,6 +42,7 @@ fun DownloadCustomAmountDialog(
|
||||
},
|
||||
confirmButton = {
|
||||
TextButton(
|
||||
enabled = amount != 0,
|
||||
onClick = {
|
||||
onDismissRequest()
|
||||
onConfirm(amount.coerceIn(0, maxAmount))
|
||||
|
@ -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)
|
||||
|
@ -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))
|
||||
}
|
||||
},
|
||||
|
@ -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
|
||||
}
|
||||
|
@ -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)) {
|
||||
|
@ -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 <--
|
||||
)
|
||||
|
@ -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)
|
||||
}
|
||||
|
||||
|
@ -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 -> {
|
||||
|
@ -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 -> {
|
||||
|
@ -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)
|
||||
}
|
||||
|
@ -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 <--
|
||||
)
|
||||
|
@ -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)
|
||||
}
|
||||
|
@ -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
|
||||
}
|
||||
}
|
||||
|
@ -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" -->
|
||||
|
Loading…
x
Reference in New Issue
Block a user