From 8ab234dc7b1ccfd631c37628026bf1be29be8fe5 Mon Sep 17 00:00:00 2001 From: Dima Date: Wed, 31 Jul 2024 21:57:36 +0900 Subject: [PATCH] Fix for Cancel button doesn't work in item edit mode.Closes #116. --- .../itemdetails/ItemDetailsEditScreen.kt | 44 +++++++++----- .../screens/itemdetails/ItemDetailsScreen.kt | 1 + .../screens/itemdetails/ItemDetailsTopBar.kt | 51 +++++++++++----- .../itemdetails/ItemDetailsViewModel.kt | 59 +++++++++++++++---- .../itemdetails/data/ItemDetailField.kt | 5 ++ 5 files changed, 119 insertions(+), 41 deletions(-) diff --git a/app/src/main/java/org/zotero/android/screens/itemdetails/ItemDetailsEditScreen.kt b/app/src/main/java/org/zotero/android/screens/itemdetails/ItemDetailsEditScreen.kt index 7847bde9..1620e266 100644 --- a/app/src/main/java/org/zotero/android/screens/itemdetails/ItemDetailsEditScreen.kt +++ b/app/src/main/java/org/zotero/android/screens/itemdetails/ItemDetailsEditScreen.kt @@ -21,6 +21,7 @@ import androidx.compose.runtime.remember import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.focus.FocusDirection +import androidx.compose.ui.focus.onFocusChanged import androidx.compose.ui.graphics.Color import androidx.compose.ui.platform.LocalFocusManager import androidx.compose.ui.res.stringResource @@ -30,7 +31,6 @@ import androidx.compose.ui.unit.dp import org.zotero.android.architecture.ui.CustomLayoutSize import org.zotero.android.database.objects.FieldKeys import org.zotero.android.screens.itemdetails.data.ItemDetailCreator -import org.zotero.android.screens.itemdetails.data.ItemDetailField import org.zotero.android.uicomponents.Strings import org.zotero.android.uicomponents.foundation.safeClickable import org.zotero.android.uicomponents.misc.CustomDivider @@ -90,7 +90,12 @@ internal fun ItemDetailsEditScreen( CustomDivider() } - ListOfEditFieldRows(viewState, layoutType, viewModel::setFieldValue) + ListOfEditFieldRows( + viewState = viewState, + layoutType = layoutType, + onValueChange = viewModel::onFieldValueTextChange, + onFocusChanges = viewModel::onFieldFocusFieldChange + ) DatesRows( dateAdded = viewState.data.dateAdded, dateModified = viewState.data.dateModified, @@ -105,13 +110,6 @@ internal fun ItemDetailsEditScreen( } } } - notesTagsAndAttachmentsBlock( - viewState = viewState, - viewModel = viewModel, - layoutType = layoutType, - onNoteClicked = { viewModel.openNoteEditor(it) }, - onAddNote = { viewModel.onAddNote() }, - ) } } @@ -184,20 +182,26 @@ private fun ListOfEditFieldRows( viewState: ItemDetailsViewState, layoutType: CustomLayoutSize.LayoutType, onValueChange: (String, String) -> Unit, + onFocusChanges: (String) -> Unit, ) { for (fieldId in viewState.data.fieldIds) { val field = viewState.data.fields.get(fieldId) ?: continue val title = field.name - val value = field.additionalInfo?.get(ItemDetailField.AdditionalInfoKey.formattedDate) - ?: field.value + val value = if (field.key == viewState.fieldFocusKey) { + viewState.fieldFocusText + } else { + field.valueOrAdditionalInfo + } FieldEditableRow( key = field.key, + fieldId = fieldId, detailTitle = title, detailValue = value, layoutType = layoutType, textColor = CustomTheme.colors.primaryContent, onValueChange = onValueChange, - isMultilineAllowed = field.key == FieldKeys.Item.extra + isMultilineAllowed = field.key == FieldKeys.Item.extra, + onFocusChanges = onFocusChanges ) } } @@ -205,12 +209,14 @@ private fun ListOfEditFieldRows( @Composable private fun FieldEditableRow( key: String, + fieldId: String, detailTitle: String, detailValue: String, layoutType: CustomLayoutSize.LayoutType, textColor: Color = CustomTheme.colors.primaryContent, isMultilineAllowed: Boolean = false, onValueChange: (String, String) -> Unit, + onFocusChanges: (String) -> Unit, ) { Column { Spacer(modifier = Modifier.height(8.dp)) @@ -233,7 +239,12 @@ private fun FieldEditableRow( if (isMultilineAllowed) { CustomTextField( modifier = Modifier - .fillMaxSize(), + .fillMaxSize() + .onFocusChanged { + if (it.hasFocus) { + onFocusChanges(fieldId) + } + }, value = detailValue, hint = "", textColor = textColor, @@ -248,7 +259,12 @@ private fun FieldEditableRow( } CustomTextField( modifier = Modifier - .fillMaxSize(), + .fillMaxSize() + .onFocusChanged { + if (it.hasFocus) { + onFocusChanges(fieldId) + } + }, value = detailValue, hint = "", textColor = textColor, diff --git a/app/src/main/java/org/zotero/android/screens/itemdetails/ItemDetailsScreen.kt b/app/src/main/java/org/zotero/android/screens/itemdetails/ItemDetailsScreen.kt index 4f23e168..9d06c0e2 100644 --- a/app/src/main/java/org/zotero/android/screens/itemdetails/ItemDetailsScreen.kt +++ b/app/src/main/java/org/zotero/android/screens/itemdetails/ItemDetailsScreen.kt @@ -111,6 +111,7 @@ internal fun ItemDetailsScreen( CustomScaffold( topBar = { ItemDetailsTopBar( + type = viewState.type, onViewOrEditClicked = viewModel::onSaveOrEditClicked, onCancelOrBackClicked = viewModel::onCancelOrBackClicked, isEditing = viewState.isEditing diff --git a/app/src/main/java/org/zotero/android/screens/itemdetails/ItemDetailsTopBar.kt b/app/src/main/java/org/zotero/android/screens/itemdetails/ItemDetailsTopBar.kt index a476f10c..078f14da 100644 --- a/app/src/main/java/org/zotero/android/screens/itemdetails/ItemDetailsTopBar.kt +++ b/app/src/main/java/org/zotero/android/screens/itemdetails/ItemDetailsTopBar.kt @@ -3,26 +3,32 @@ package org.zotero.android.screens.itemdetails import androidx.compose.runtime.Composable import androidx.compose.ui.res.stringResource import org.zotero.android.architecture.ui.CustomLayoutSize +import org.zotero.android.screens.itemdetails.data.DetailType import org.zotero.android.uicomponents.Strings import org.zotero.android.uicomponents.topbar.NewCustomTopBar import org.zotero.android.uicomponents.topbar.NewHeadingTextButton @Composable internal fun ItemDetailsTopBar( + type: DetailType, onViewOrEditClicked: () -> Unit, onCancelOrBackClicked: () -> Unit, isEditing: Boolean, ) { NewCustomTopBar( leftContainerContent = listOf { - ItemDetailsTopBarEditing(onCancelOrBackClicked, isEditing) + ItemDetailsTopBarEditing( + type = type, + onCancelOrBackClicked = onCancelOrBackClicked, + isEditing = isEditing + ) }, rightContainerContent = listOf { NewHeadingTextButton( isEnabled = true, onClick = onViewOrEditClicked, text = if (isEditing) { - stringResource(Strings.save) + stringResource(Strings.done) } else { stringResource(Strings.edit) } @@ -32,19 +38,34 @@ internal fun ItemDetailsTopBar( } @Composable -private fun ItemDetailsTopBarEditing(onCancelOrBackClicked: () -> Unit, isEditing: Boolean) { - NewHeadingTextButton( - isEnabled = true, - onClick = onCancelOrBackClicked, - text = if (isEditing) { - stringResource(Strings.cancel) - } else { - val layoutType = CustomLayoutSize.calculateLayoutType() - if (layoutType.isTablet()) { - stringResource(Strings.back) - } else { - stringResource(Strings.collections_all_items) +private fun ItemDetailsTopBarEditing( + type: DetailType, + onCancelOrBackClicked: () -> Unit, + isEditing: Boolean +) { + if (isEditing) { + when (type) { + is DetailType.creation, is DetailType.duplication -> { + NewHeadingTextButton( + onClick = onCancelOrBackClicked, + text = stringResource(Strings.cancel) + ) + } + + else -> { + //no-op } } - ) + } else { + val layoutType = CustomLayoutSize.calculateLayoutType() + val text = if (layoutType.isTablet()) { + stringResource(Strings.back) + } else { + stringResource(Strings.collections_all_items) + } + NewHeadingTextButton( + onClick = onCancelOrBackClicked, + text = text + ) + } } \ No newline at end of file diff --git a/app/src/main/java/org/zotero/android/screens/itemdetails/ItemDetailsViewModel.kt b/app/src/main/java/org/zotero/android/screens/itemdetails/ItemDetailsViewModel.kt index 3f73e28d..3cada7a6 100644 --- a/app/src/main/java/org/zotero/android/screens/itemdetails/ItemDetailsViewModel.kt +++ b/app/src/main/java/org/zotero/android/screens/itemdetails/ItemDetailsViewModel.kt @@ -11,7 +11,10 @@ import io.realm.ObjectChangeSet import io.realm.RealmObjectChangeListener import kotlinx.coroutines.CoroutineDispatcher import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.flow.MutableStateFlow +import kotlinx.coroutines.flow.debounce import kotlinx.coroutines.flow.launchIn +import kotlinx.coroutines.flow.map import kotlinx.coroutines.flow.onEach import kotlinx.coroutines.launch import org.greenrobot.eventbus.EventBus @@ -204,6 +207,7 @@ class ItemDetailsViewModel @Inject constructor( fun init() = initOnce { EventBus.getDefault().register(this) setupFileObservers() + setupOnFieldValueTextChangeFlow() val args = ScreenArguments.itemDetailsArgs @@ -298,7 +302,7 @@ class ItemDetailsViewModel @Inject constructor( fun onSaveOrEditClicked() { if (viewState.isEditing) { - saveChanges() + endEditing() } else { val updatedStateData = viewState.data.deepCopy() val updatedData = viewState.data.deepCopy( @@ -372,7 +376,6 @@ class ItemDetailsViewModel @Inject constructor( defaults = this.defaults ) } - else -> {} } } catch (e: Exception) { Timber.e(e, "can't load initial data ") @@ -381,7 +384,6 @@ class ItemDetailsViewModel @Inject constructor( } return } - data!! val request = CreateItemFromDetailDbRequest( key = key, libraryId = libraryId, @@ -620,7 +622,41 @@ class ItemDetailsViewModel @Inject constructor( } } - fun setFieldValue(id: String, value: String) { + private var onFieldValueTextChangeFlow = MutableStateFlow?>(null) + + private fun setupOnFieldValueTextChangeFlow() { + onFieldValueTextChangeFlow + .debounce(500) + .map { data -> + if (data != null) { + setFieldValue(data.first, data.second) + } + } + .launchIn(viewModelScope) + } + + fun onFieldFocusFieldChange(fieldId: String) { + val changeFlowValue = onFieldValueTextChangeFlow.value + if (changeFlowValue != null) { + setFieldValue(changeFlowValue.first, changeFlowValue.second) + } + val field = viewState.data.fields[fieldId] ?: return + updateState { + copy( + fieldFocusKey = fieldId, + fieldFocusText = field.valueOrAdditionalInfo + ) + } + } + + fun onFieldValueTextChange(id: String, value: String) { + updateState { + copy(fieldFocusText = value) + } + onFieldValueTextChangeFlow.tryEmit(id to value) + } + + private fun setFieldValue(id: String, value: String) { val field = viewState.data.fields[id] if (field == null) { return @@ -764,13 +800,13 @@ class ItemDetailsViewModel @Inject constructor( return field } - fun saveChanges() { + private fun endEditing() { if (viewState.snapshot == viewState.data) { return } try { coroutineScope.launch { - endEditing() + endEditingAsync() } } catch (error: Exception) { Timber.e(error, "ItemDetailStore: can't store changes") @@ -780,9 +816,7 @@ class ItemDetailsViewModel @Inject constructor( } } - private fun endEditing() { -// updateDateFieldIfNeeded() -// updateAccessedFieldIfNeeded() + private fun endEditingAsync() { viewModelScope.launch { val updatedFields: MutableMap = mutableMapOf() val updatedFieldsMap = viewState.data.fields.toMutableMap() @@ -940,8 +974,7 @@ class ItemDetailsViewModel @Inject constructor( private fun cancelChanges() { viewModelScope.launch { - val type = viewState.type!! - when (type) { + when (val type = viewState.type) { is DetailType.duplication -> { perform( dbWrapper = dbWrapperMain, @@ -1986,7 +2019,7 @@ data class ItemDetailsViewState( val key: String = "", val library: Library? = null, val userId: Long = 0, - val type: DetailType? = null, + val type: DetailType = DetailType.preview(""), val preScrolledChildKey: String? = null, val isEditing: Boolean = false, var error: ItemDetailError? = null, @@ -2001,6 +2034,8 @@ data class ItemDetailsViewState( var isLoadingData: Boolean = false, var backgroundProcessedItems: Set = emptySet(), val longPressOptionsHolder: LongPressOptionsHolder? = null, + val fieldFocusKey: String? = null, + val fieldFocusText: String = "", ) : ViewState sealed class ItemDetailsViewEffect : ViewEffect { diff --git a/app/src/main/java/org/zotero/android/screens/itemdetails/data/ItemDetailField.kt b/app/src/main/java/org/zotero/android/screens/itemdetails/data/ItemDetailField.kt index 9d91cbde..c562a138 100644 --- a/app/src/main/java/org/zotero/android/screens/itemdetails/data/ItemDetailField.kt +++ b/app/src/main/java/org/zotero/android/screens/itemdetails/data/ItemDetailField.kt @@ -56,4 +56,9 @@ class ItemDetailField( ) } + val valueOrAdditionalInfo: String get() { + return additionalInfo?.get(AdditionalInfoKey.formattedDate) + ?: value + } + } \ No newline at end of file