diff --git a/core/ui/src/main/java/com/sadellie/unitto/core/ui/common/textfield/ClipboardManagerExt.kt b/core/ui/src/main/java/com/sadellie/unitto/core/ui/common/textfield/ClipboardManagerExt.kt index f3ef26bb..a606b219 100644 --- a/core/ui/src/main/java/com/sadellie/unitto/core/ui/common/textfield/ClipboardManagerExt.kt +++ b/core/ui/src/main/java/com/sadellie/unitto/core/ui/common/textfield/ClipboardManagerExt.kt @@ -50,3 +50,5 @@ internal fun ClipboardManager.copy(value: TextFieldValue) = this.setText( .text ) ) + +internal const val PLAIN_TEXT_LABEL = "plain text" diff --git a/core/ui/src/main/java/com/sadellie/unitto/core/ui/common/textfield/FixedInputTextFIeld.kt b/core/ui/src/main/java/com/sadellie/unitto/core/ui/common/textfield/FixedInputTextFIeld.kt index 94e79271..e7306ff8 100644 --- a/core/ui/src/main/java/com/sadellie/unitto/core/ui/common/textfield/FixedInputTextFIeld.kt +++ b/core/ui/src/main/java/com/sadellie/unitto/core/ui/common/textfield/FixedInputTextFIeld.kt @@ -23,7 +23,6 @@ import android.content.Context import androidx.compose.foundation.clickable import androidx.compose.foundation.horizontalScroll import androidx.compose.foundation.layout.fillMaxWidth -import androidx.compose.foundation.layout.padding import androidx.compose.foundation.rememberScrollState import androidx.compose.foundation.text.selection.SelectionContainer import androidx.compose.material3.Text @@ -36,7 +35,6 @@ import androidx.compose.ui.platform.LocalClipboardManager import androidx.compose.ui.platform.LocalContext import androidx.compose.ui.text.AnnotatedString import androidx.compose.ui.text.style.TextAlign -import androidx.compose.ui.unit.dp import com.sadellie.unitto.core.ui.theme.LocalNumberTypography @Composable @@ -45,7 +43,7 @@ fun FixedExpressionInputTextField( value: String, formatterSymbols: FormatterSymbols, textColor: Color, - onClick: (cleanValue: String) -> Unit + onClick: () -> Unit, ) { val clipboardManager = FormattedExpressionClipboardManager( formatterSymbols = formatterSymbols, @@ -55,13 +53,13 @@ fun FixedExpressionInputTextField( CompositionLocalProvider(LocalClipboardManager provides clipboardManager) { SelectionContainer( - modifier = modifier - .clickable { onClick(value) } - .fillMaxWidth() - .padding(horizontal = 8.dp) - .horizontalScroll(rememberScrollState(), reverseScrolling = true), + modifier = Modifier + .horizontalScroll(rememberScrollState()) // Must be first + .clickable(onClick = onClick) + .then(modifier) ) { Text( + modifier = Modifier.fillMaxWidth(), text = value.formatExpression(formatterSymbols), style = LocalNumberTypography.current.displaySmall .copy(color = textColor, textAlign = TextAlign.End), diff --git a/data/calculator/src/main/java/com/sadellie/unitto/data/calculator/CalculatorHistoryRepositoryImpl.kt b/data/calculator/src/main/java/com/sadellie/unitto/data/calculator/CalculatorHistoryRepositoryImpl.kt index cabc826f..7feb497a 100644 --- a/data/calculator/src/main/java/com/sadellie/unitto/data/calculator/CalculatorHistoryRepositoryImpl.kt +++ b/data/calculator/src/main/java/com/sadellie/unitto/data/calculator/CalculatorHistoryRepositoryImpl.kt @@ -53,6 +53,10 @@ class CalculatorHistoryRepositoryImpl @Inject constructor( ) } + override suspend fun delete(item: HistoryItem) { + calculatorHistoryDao.delete(item.id) + } + override suspend fun clear() { calculatorHistoryDao.clear() } diff --git a/data/database/src/main/java/com/sadellie/unitto/data/database/CalculatorHistoryDao.kt b/data/database/src/main/java/com/sadellie/unitto/data/database/CalculatorHistoryDao.kt index 6e83cc3e..3720083e 100644 --- a/data/database/src/main/java/com/sadellie/unitto/data/database/CalculatorHistoryDao.kt +++ b/data/database/src/main/java/com/sadellie/unitto/data/database/CalculatorHistoryDao.kt @@ -32,6 +32,9 @@ interface CalculatorHistoryDao { @Insert(onConflict = OnConflictStrategy.REPLACE) suspend fun insert(vararg historyEntity: CalculatorHistoryEntity) + @Query("DELETE FROM calculator_history WHERE entityId = :entityId") + suspend fun delete(entityId: Int) + @Query("DELETE FROM calculator_history") suspend fun clear() } diff --git a/data/model/src/main/java/com/sadellie/unitto/data/model/repository/CalculatorHistoryRepository.kt b/data/model/src/main/java/com/sadellie/unitto/data/model/repository/CalculatorHistoryRepository.kt index ce10de75..de010c19 100644 --- a/data/model/src/main/java/com/sadellie/unitto/data/model/repository/CalculatorHistoryRepository.kt +++ b/data/model/src/main/java/com/sadellie/unitto/data/model/repository/CalculatorHistoryRepository.kt @@ -26,7 +26,11 @@ interface CalculatorHistoryRepository { suspend fun add( expression: String, - result: String + result: String, + ) + + suspend fun delete( + item: HistoryItem, ) suspend fun clear() diff --git a/feature/calculator/src/main/java/com/sadellie/unitto/feature/calculator/CalculatorScreen.kt b/feature/calculator/src/main/java/com/sadellie/unitto/feature/calculator/CalculatorScreen.kt index a852154e..d986482e 100644 --- a/feature/calculator/src/main/java/com/sadellie/unitto/feature/calculator/CalculatorScreen.kt +++ b/feature/calculator/src/main/java/com/sadellie/unitto/feature/calculator/CalculatorScreen.kt @@ -101,7 +101,8 @@ internal fun CalculatorRoute( toggleCalculatorMode = viewModel::updateRadianMode, equal = viewModel::equal, clearHistory = viewModel::clearHistory, - addBracket = viewModel::addBracket + addBracket = viewModel::addBracket, + onDelete = viewModel::deleteHistoryItem, ) } @@ -117,7 +118,8 @@ internal fun CalculatorScreen( onCursorChange: (TextRange) -> Unit, toggleCalculatorMode: (Boolean) -> Unit, equal: () -> Unit, - clearHistory: () -> Unit + clearHistory: () -> Unit, + onDelete: (HistoryItem) -> Unit, ) { when (uiState) { is CalculatorUIState.Loading -> EmptyScreen() @@ -132,7 +134,8 @@ internal fun CalculatorScreen( toggleAngleMode = { toggleCalculatorMode(!uiState.radianMode) }, equal = equal, clearHistory = clearHistory, - addBracket = addBracket + addBracket = addBracket, + onDelete = onDelete, ) } } @@ -149,18 +152,27 @@ private fun Ready( onCursorChange: (TextRange) -> Unit, toggleAngleMode: () -> Unit, equal: () -> Unit, - clearHistory: () -> Unit + clearHistory: () -> Unit, + onDelete: (HistoryItem) -> Unit, ) { val focusManager = LocalFocusManager.current var showClearHistoryDialog by rememberSaveable { mutableStateOf(false) } - var showClearHistoryButton by rememberSaveable { mutableStateOf(false) } + val dragState = remember { + AnchoredDraggableState( + initialValue = DragState.CLOSED, + positionalThreshold = { 0f }, + velocityThreshold = { 0f }, + animationSpec = tween() + ) + } + val isOpen = dragState.currentValue == DragState.OPEN ScaffoldWithTopBar( title = { Text(stringResource(R.string.calculator_title)) }, navigationIcon = { MenuButton { navigateToMenu() } }, colors = TopAppBarDefaults.topAppBarColors(MaterialTheme.colorScheme.surfaceVariant), actions = { - Crossfade(showClearHistoryButton, label = "Clear button reveal") { + Crossfade(isOpen, label = "Clear button reveal") { if (it) { IconButton( onClick = { showClearHistoryDialog = true }, @@ -187,15 +199,6 @@ private fun Ready( val textBoxHeight = maxHeight * textBoxFill - val dragState = remember { - AnchoredDraggableState( - initialValue = DragState.CLOSED, - positionalThreshold = { 0f }, - velocityThreshold = { 0f }, - animationSpec = tween() - ) - } - var historyListHeight by remember { mutableStateOf(0.dp) } val keyboardHeight by remember(historyListHeight, textBoxHeight) { derivedStateOf { @@ -234,10 +237,6 @@ private fun Ready( focusManager.clearFocus() } - LaunchedEffect(dragState.currentValue) { - showClearHistoryButton = dragState.currentValue == DragState.OPEN - } - BackHandler(dragState.currentValue != DragState.CLOSED) { coroutineScope.launch { dragState.animateTo(DragState.CLOSED) @@ -251,7 +250,9 @@ private fun Ready( .height(historyListHeight), historyItems = uiState.history, formatterSymbols = uiState.formatterSymbols, - addTokens = addSymbol + addTokens = addSymbol, + onDelete = onDelete, + showDeleteButtons = isOpen ) TextBox( @@ -274,7 +275,14 @@ private fun Ready( CalculatorKeyboard( modifier = Modifier .semantics { testTag = "ready" } - .offset { IntOffset(x = 0, y = (historyListHeight + textBoxHeight).toPx().roundToInt()) } + .offset { + IntOffset( + x = 0, + y = (historyListHeight + textBoxHeight) + .toPx() + .roundToInt() + ) + } .height(keyboardHeight) .fillMaxWidth() .padding(horizontal = 8.dp, vertical = 4.dp), @@ -375,6 +383,7 @@ private fun PreviewCalculatorScreen() { toggleCalculatorMode = {}, equal = {}, clearHistory = {}, - addBracket = {} + addBracket = {}, + onDelete = {}, ) } \ No newline at end of file diff --git a/feature/calculator/src/main/java/com/sadellie/unitto/feature/calculator/CalculatorViewModel.kt b/feature/calculator/src/main/java/com/sadellie/unitto/feature/calculator/CalculatorViewModel.kt index 93ecc99b..bce8601c 100644 --- a/feature/calculator/src/main/java/com/sadellie/unitto/feature/calculator/CalculatorViewModel.kt +++ b/feature/calculator/src/main/java/com/sadellie/unitto/feature/calculator/CalculatorViewModel.kt @@ -32,6 +32,7 @@ import com.sadellie.unitto.core.ui.common.textfield.getTextField import com.sadellie.unitto.data.common.format import com.sadellie.unitto.data.common.isExpression import com.sadellie.unitto.data.common.stateIn +import com.sadellie.unitto.data.model.HistoryItem import com.sadellie.unitto.data.model.repository.CalculatorHistoryRepository import com.sadellie.unitto.data.model.repository.UserPreferencesRepository import dagger.hilt.android.lifecycle.HiltViewModel @@ -180,6 +181,10 @@ internal class CalculatorViewModel @Inject constructor( calculatorHistoryRepository.clear() } + fun deleteHistoryItem(item: HistoryItem) = viewModelScope.launch(Dispatchers.IO) { + calculatorHistoryRepository.delete(item) + } + fun equal() = viewModelScope.launch { val prefs = _prefs.value ?: return@launch if (_equalClicked.value) return@launch diff --git a/feature/calculator/src/main/java/com/sadellie/unitto/feature/calculator/components/HistoryList.kt b/feature/calculator/src/main/java/com/sadellie/unitto/feature/calculator/components/HistoryList.kt index c4269727..7ba3c09b 100644 --- a/feature/calculator/src/main/java/com/sadellie/unitto/feature/calculator/components/HistoryList.kt +++ b/feature/calculator/src/main/java/com/sadellie/unitto/feature/calculator/components/HistoryList.kt @@ -18,10 +18,15 @@ package com.sadellie.unitto.feature.calculator.components +import androidx.compose.animation.AnimatedVisibility +import androidx.compose.animation.Crossfade import androidx.compose.foundation.background import androidx.compose.foundation.layout.Arrangement import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.Row +import androidx.compose.foundation.layout.fillMaxHeight import androidx.compose.foundation.layout.fillMaxSize +import androidx.compose.foundation.layout.fillMaxWidth import androidx.compose.foundation.layout.height import androidx.compose.foundation.layout.wrapContentHeight import androidx.compose.foundation.lazy.LazyColumn @@ -29,7 +34,9 @@ import androidx.compose.foundation.lazy.items import androidx.compose.foundation.lazy.rememberLazyListState import androidx.compose.material.icons.Icons import androidx.compose.material.icons.filled.History +import androidx.compose.material.icons.outlined.Close import androidx.compose.material3.Icon +import androidx.compose.material3.IconButton import androidx.compose.material3.MaterialTheme import androidx.compose.material3.Text import androidx.compose.runtime.Composable @@ -53,18 +60,26 @@ internal fun HistoryList( historyItems: List, formatterSymbols: FormatterSymbols, addTokens: (String) -> Unit, + onDelete: (HistoryItem) -> Unit, + showDeleteButtons: Boolean, ) { - if (historyItems.isEmpty()) { - HistoryListPlaceholder( - modifier = modifier, - ) - } else { - HistoryListContent( - modifier = modifier, - historyItems = historyItems, - addTokens = addTokens, - formatterSymbols = formatterSymbols, - ) + Crossfade( + targetState = historyItems.isEmpty() + ) { emptyList -> + if (emptyList) { + HistoryListPlaceholder( + modifier = modifier, + ) + } else { + HistoryListContent( + modifier = modifier, + historyItems = historyItems, + formatterSymbols = formatterSymbols, + addTokens = addTokens, + onDelete = onDelete, + showDeleteButtons = showDeleteButtons, + ) + } } } @@ -92,19 +107,24 @@ private fun HistoryListPlaceholder( private fun HistoryListContent( modifier: Modifier, historyItems: List, - addTokens: (String) -> Unit, formatterSymbols: FormatterSymbols, + addTokens: (String) -> Unit, + onDelete: (HistoryItem) -> Unit, + showDeleteButtons: Boolean, ) { val state = rememberLazyListState() val focusManager = LocalFocusManager.current - // Very bad workaround for https://issuetracker.google.com/issues/295745063 - // Will remove once the fix is released + // Selection handles cause lag LaunchedEffect(state.isScrollInProgress) { focusManager.clearFocus(true) } - LaunchedEffect(historyItems) { state.scrollToItem(0) } + LaunchedEffect(historyItems) { + // Don't scroll when the UI is in state where user can delete an item. This fixes items + // placement animation + if (!showDeleteButtons) state.scrollToItem(0) + } LazyColumn( modifier = modifier, @@ -113,9 +133,12 @@ private fun HistoryListContent( ) { items(historyItems, { it.id }) { historyItem -> HistoryListItem( + modifier = Modifier.animateItemPlacement(), historyItem = historyItem, formatterSymbols = formatterSymbols, addTokens = addTokens, + onDelete = { onDelete(historyItem) }, + showDeleteButton = showDeleteButtons, ) } } @@ -127,24 +150,48 @@ private fun HistoryListItem( historyItem: HistoryItem, formatterSymbols: FormatterSymbols, addTokens: (String) -> Unit, + onDelete: () -> Unit, + showDeleteButton: Boolean, ) { - Column( + Row( modifier = modifier.height(HistoryItemHeight), - verticalArrangement = Arrangement.Center + verticalAlignment = Alignment.CenterVertically, ) { - FixedExpressionInputTextField( - value = historyItem.expression, - formatterSymbols = formatterSymbols, - textColor = MaterialTheme.colorScheme.onSurfaceVariant, - onClick = addTokens, - ) + AnimatedVisibility(visible = showDeleteButton) { + IconButton(onClick = onDelete) { + Icon( + modifier = Modifier, + imageVector = Icons.Outlined.Close, + contentDescription = stringResource(R.string.delete_label), + tint = MaterialTheme.colorScheme.onSurfaceVariant + ) + } + } + Column( + modifier = Modifier + .weight(1f) + .fillMaxHeight(), + verticalArrangement = Arrangement.Center, + horizontalAlignment = Alignment.End + ) { + FixedExpressionInputTextField( + modifier = Modifier + .fillMaxWidth(), + value = historyItem.expression, + formatterSymbols = formatterSymbols, + textColor = MaterialTheme.colorScheme.onSurfaceVariant, + onClick = { addTokens(historyItem.expression) }, + ) - FixedExpressionInputTextField( - value = historyItem.result, - formatterSymbols = formatterSymbols, - textColor = MaterialTheme.colorScheme.onSurfaceVariant.copy(alpha = 0.5f), - onClick = addTokens, - ) + FixedExpressionInputTextField( + modifier = Modifier + .fillMaxWidth(), + value = historyItem.result, + formatterSymbols = formatterSymbols, + textColor = MaterialTheme.colorScheme.onSurfaceVariant.copy(alpha = 0.5f), + onClick = { addTokens(historyItem.result) }, + ) + } } } @@ -179,6 +226,8 @@ private fun PreviewHistoryList() { .fillMaxSize(), historyItems = historyItems, formatterSymbols = FormatterSymbols.Spaces, - addTokens = {} + addTokens = {}, + onDelete = {}, + showDeleteButtons = true, ) }