diff --git a/app/src/main/java/com/example/research/ui/settings/components/DictionaryListItem.kt b/app/src/main/java/com/example/research/ui/settings/components/DictionaryListItem.kt index d06eae6..7270852 100644 --- a/app/src/main/java/com/example/research/ui/settings/components/DictionaryListItem.kt +++ b/app/src/main/java/com/example/research/ui/settings/components/DictionaryListItem.kt @@ -28,6 +28,7 @@ import androidx.compose.runtime.remember import androidx.compose.runtime.rememberCoroutineScope import androidx.compose.runtime.rememberUpdatedState import androidx.compose.runtime.setValue +import androidx.compose.runtime.snapshotFlow import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.draw.clipToBounds @@ -83,7 +84,7 @@ fun DictionaryListItem( onToggle: () -> Unit, onDelete: () -> Unit, modifier: Modifier = Modifier, - canDelete: Boolean = true + isDeleteBlocked: () -> Boolean = { false } ) { val haptic = LocalHapticFeedback.current val density = LocalDensity.current @@ -95,8 +96,9 @@ fun DictionaryListItem( val offsetAnim = remember { Animatable(0f) } var rawOffset by remember { mutableFloatStateOf(0f) } - val isDeleteRevealed by remember(canDelete) { - derivedStateOf { canDelete && rawOffset <= -swipeThresholdPx } + val isDeleteBlockedCurrent by rememberUpdatedState(isDeleteBlocked) + val isDeleteRevealed by remember { + derivedStateOf { !isDeleteBlockedCurrent() && rawOffset <= -swipeThresholdPx } } LaunchedEffect(isDeleteRevealed) { @@ -105,11 +107,14 @@ fun DictionaryListItem( } } - LaunchedEffect(canDelete) { - if (!canDelete) { - rawOffset = 0f - offsetAnim.snapTo(0f) - } + LaunchedEffect(Unit) { + snapshotFlow { isDeleteBlockedCurrent() } + .collect { blocked -> + if (blocked) { + rawOffset = 0f + scope.launch { offsetAnim.snapTo(0f) } + } + } } val currentOnDelete by rememberUpdatedState(onDelete) @@ -142,13 +147,14 @@ fun DictionaryListItem( .height(DictionaryItemHeight) .background(MaterialTheme.colorScheme.surfaceVariant) .graphicsLayer { translationX = offsetAnim.value } - .pointerInput(canDelete, maxSwipePx, swipeThresholdPx) { - if (!canDelete) return@pointerInput + .pointerInput(maxSwipePx, swipeThresholdPx) { detectHorizontalDragGestures( onDragStart = { + if (isDeleteBlockedCurrent()) return@detectHorizontalDragGestures haptic.performHapticFeedback(HapticFeedbackType.TextHandleMove) }, onDragEnd = { + if (isDeleteBlockedCurrent()) return@detectHorizontalDragGestures val target = if (rawOffset <= -swipeThresholdPx) -maxSwipePx else 0f rawOffset = target scope.launch { @@ -159,6 +165,7 @@ fun DictionaryListItem( } }, onDragCancel = { + if (isDeleteBlockedCurrent()) return@detectHorizontalDragGestures val target = if (rawOffset <= -swipeThresholdPx) -maxSwipePx else 0f rawOffset = target scope.launch { @@ -169,6 +176,7 @@ fun DictionaryListItem( } }, onHorizontalDrag = { _, dragAmount -> + if (isDeleteBlockedCurrent()) return@detectHorizontalDragGestures val newOffset = (rawOffset + dragAmount).coerceIn(-maxSwipePx, 0f) rawOffset = newOffset scope.launch { offsetAnim.snapTo(newOffset) } diff --git a/app/src/main/java/com/example/research/ui/settings/components/DictionaryManagement.kt b/app/src/main/java/com/example/research/ui/settings/components/DictionaryManagement.kt index 8f2feae..fed8e5a 100644 --- a/app/src/main/java/com/example/research/ui/settings/components/DictionaryManagement.kt +++ b/app/src/main/java/com/example/research/ui/settings/components/DictionaryManagement.kt @@ -13,6 +13,7 @@ import androidx.compose.runtime.derivedStateOf import androidx.compose.runtime.getValue import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.remember +import androidx.compose.runtime.rememberUpdatedState import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.hapticfeedback.HapticFeedbackType @@ -75,6 +76,13 @@ fun DictionaryManagement( derivedStateOf { importState is ImportState.Importing || importState is ImportState.Extracting } } val hasEnabledSources by remember(sources) { derivedStateOf { sources.any { it.isEnabled } } } + val isBlockingMutations = isInProgress + + // Invariant: the mutation-block flag is read only at state-read/gesture time (derivedStateOf, + // snapshotFlow, pointerInput callbacks), never at row composition time. A composition-time read + // (e.g. Modifier.alpha(if (isDeleteBlocked()) ...)) would invalidate every row on each flip. + val isMutationsBlockedState = rememberUpdatedState(isBlockingMutations) + val isDeleteBlocked = remember { { isMutationsBlockedState.value } } SectionCard( modifier = modifier @@ -125,13 +133,13 @@ fun DictionaryManagement( // Disable accessibility on background content during import to improve performance. // The progress dialog remains accessible for cancellation. - val backgroundModifier = if (isInProgress) { + val backgroundModifier = if (isBlockingMutations) { Modifier.semantics(mergeDescendants = true) { } } else { Modifier } - if (dictionaries.isEmpty() && !isInProgress) { + if (dictionaries.isEmpty() && !isBlockingMutations) { Column( modifier = backgroundModifier .fillMaxWidth() @@ -153,7 +161,7 @@ fun DictionaryManagement( ) } } else { - if (dictionaryStatus is DictionaryStatus.NeedsUpdate && !isInProgress && hasEnabledSources) { + if (dictionaryStatus is DictionaryStatus.NeedsUpdate && !isBlockingMutations && hasEnabledSources) { DictionaryUpdateCard( onStartDownload = onStartDownload, modifier = backgroundModifier @@ -167,10 +175,10 @@ fun DictionaryManagement( modifier = Modifier .fillMaxWidth() .then(backgroundModifier), - canDelete = !isInProgress + isDeleteBlocked = isDeleteBlocked ) - if (!isInProgress) { + if (!isBlockingMutations) { DictionaryActionButtons( onAddSource = { haptic.performHapticFeedback(HapticFeedbackType.TextHandleMove) @@ -248,7 +256,7 @@ private fun DictionaryListSection( onToggleDictionary: (String) -> Unit, onDeleteDictionary: (Dictionary) -> Unit, modifier: Modifier = Modifier, - canDelete: Boolean = true + isDeleteBlocked: () -> Boolean = { false } ) { Column( modifier = modifier, @@ -259,7 +267,7 @@ private fun DictionaryListSection( dictionary = dictionary, onToggle = { onToggleDictionary(dictionary.path) }, onDelete = { onDeleteDictionary(dictionary) }, - canDelete = canDelete + isDeleteBlocked = isDeleteBlocked ) } }