Delete calculator history item by one

closes #58
This commit is contained in:
Sad Ellie 2024-02-04 23:48:22 +03:00
parent 9a865b4049
commit 402f48ee1f
8 changed files with 135 additions and 61 deletions

View File

@ -50,3 +50,5 @@ internal fun ClipboardManager.copy(value: TextFieldValue) = this.setText(
.text .text
) )
) )
internal const val PLAIN_TEXT_LABEL = "plain text"

View File

@ -23,7 +23,6 @@ import android.content.Context
import androidx.compose.foundation.clickable import androidx.compose.foundation.clickable
import androidx.compose.foundation.horizontalScroll import androidx.compose.foundation.horizontalScroll
import androidx.compose.foundation.layout.fillMaxWidth import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.rememberScrollState import androidx.compose.foundation.rememberScrollState
import androidx.compose.foundation.text.selection.SelectionContainer import androidx.compose.foundation.text.selection.SelectionContainer
import androidx.compose.material3.Text 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.platform.LocalContext
import androidx.compose.ui.text.AnnotatedString import androidx.compose.ui.text.AnnotatedString
import androidx.compose.ui.text.style.TextAlign import androidx.compose.ui.text.style.TextAlign
import androidx.compose.ui.unit.dp
import com.sadellie.unitto.core.ui.theme.LocalNumberTypography import com.sadellie.unitto.core.ui.theme.LocalNumberTypography
@Composable @Composable
@ -45,7 +43,7 @@ fun FixedExpressionInputTextField(
value: String, value: String,
formatterSymbols: FormatterSymbols, formatterSymbols: FormatterSymbols,
textColor: Color, textColor: Color,
onClick: (cleanValue: String) -> Unit onClick: () -> Unit,
) { ) {
val clipboardManager = FormattedExpressionClipboardManager( val clipboardManager = FormattedExpressionClipboardManager(
formatterSymbols = formatterSymbols, formatterSymbols = formatterSymbols,
@ -55,13 +53,13 @@ fun FixedExpressionInputTextField(
CompositionLocalProvider(LocalClipboardManager provides clipboardManager) { CompositionLocalProvider(LocalClipboardManager provides clipboardManager) {
SelectionContainer( SelectionContainer(
modifier = modifier modifier = Modifier
.clickable { onClick(value) } .horizontalScroll(rememberScrollState()) // Must be first
.fillMaxWidth() .clickable(onClick = onClick)
.padding(horizontal = 8.dp) .then(modifier)
.horizontalScroll(rememberScrollState(), reverseScrolling = true),
) { ) {
Text( Text(
modifier = Modifier.fillMaxWidth(),
text = value.formatExpression(formatterSymbols), text = value.formatExpression(formatterSymbols),
style = LocalNumberTypography.current.displaySmall style = LocalNumberTypography.current.displaySmall
.copy(color = textColor, textAlign = TextAlign.End), .copy(color = textColor, textAlign = TextAlign.End),

View File

@ -53,6 +53,10 @@ class CalculatorHistoryRepositoryImpl @Inject constructor(
) )
} }
override suspend fun delete(item: HistoryItem) {
calculatorHistoryDao.delete(item.id)
}
override suspend fun clear() { override suspend fun clear() {
calculatorHistoryDao.clear() calculatorHistoryDao.clear()
} }

View File

@ -32,6 +32,9 @@ interface CalculatorHistoryDao {
@Insert(onConflict = OnConflictStrategy.REPLACE) @Insert(onConflict = OnConflictStrategy.REPLACE)
suspend fun insert(vararg historyEntity: CalculatorHistoryEntity) suspend fun insert(vararg historyEntity: CalculatorHistoryEntity)
@Query("DELETE FROM calculator_history WHERE entityId = :entityId")
suspend fun delete(entityId: Int)
@Query("DELETE FROM calculator_history") @Query("DELETE FROM calculator_history")
suspend fun clear() suspend fun clear()
} }

View File

@ -26,7 +26,11 @@ interface CalculatorHistoryRepository {
suspend fun add( suspend fun add(
expression: String, expression: String,
result: String result: String,
)
suspend fun delete(
item: HistoryItem,
) )
suspend fun clear() suspend fun clear()

View File

@ -101,7 +101,8 @@ internal fun CalculatorRoute(
toggleCalculatorMode = viewModel::updateRadianMode, toggleCalculatorMode = viewModel::updateRadianMode,
equal = viewModel::equal, equal = viewModel::equal,
clearHistory = viewModel::clearHistory, clearHistory = viewModel::clearHistory,
addBracket = viewModel::addBracket addBracket = viewModel::addBracket,
onDelete = viewModel::deleteHistoryItem,
) )
} }
@ -117,7 +118,8 @@ internal fun CalculatorScreen(
onCursorChange: (TextRange) -> Unit, onCursorChange: (TextRange) -> Unit,
toggleCalculatorMode: (Boolean) -> Unit, toggleCalculatorMode: (Boolean) -> Unit,
equal: () -> Unit, equal: () -> Unit,
clearHistory: () -> Unit clearHistory: () -> Unit,
onDelete: (HistoryItem) -> Unit,
) { ) {
when (uiState) { when (uiState) {
is CalculatorUIState.Loading -> EmptyScreen() is CalculatorUIState.Loading -> EmptyScreen()
@ -132,7 +134,8 @@ internal fun CalculatorScreen(
toggleAngleMode = { toggleCalculatorMode(!uiState.radianMode) }, toggleAngleMode = { toggleCalculatorMode(!uiState.radianMode) },
equal = equal, equal = equal,
clearHistory = clearHistory, clearHistory = clearHistory,
addBracket = addBracket addBracket = addBracket,
onDelete = onDelete,
) )
} }
} }
@ -149,18 +152,27 @@ private fun Ready(
onCursorChange: (TextRange) -> Unit, onCursorChange: (TextRange) -> Unit,
toggleAngleMode: () -> Unit, toggleAngleMode: () -> Unit,
equal: () -> Unit, equal: () -> Unit,
clearHistory: () -> Unit clearHistory: () -> Unit,
onDelete: (HistoryItem) -> Unit,
) { ) {
val focusManager = LocalFocusManager.current val focusManager = LocalFocusManager.current
var showClearHistoryDialog by rememberSaveable { mutableStateOf(false) } 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( ScaffoldWithTopBar(
title = { Text(stringResource(R.string.calculator_title)) }, title = { Text(stringResource(R.string.calculator_title)) },
navigationIcon = { MenuButton { navigateToMenu() } }, navigationIcon = { MenuButton { navigateToMenu() } },
colors = TopAppBarDefaults.topAppBarColors(MaterialTheme.colorScheme.surfaceVariant), colors = TopAppBarDefaults.topAppBarColors(MaterialTheme.colorScheme.surfaceVariant),
actions = { actions = {
Crossfade(showClearHistoryButton, label = "Clear button reveal") { Crossfade(isOpen, label = "Clear button reveal") {
if (it) { if (it) {
IconButton( IconButton(
onClick = { showClearHistoryDialog = true }, onClick = { showClearHistoryDialog = true },
@ -187,15 +199,6 @@ private fun Ready(
val textBoxHeight = maxHeight * textBoxFill val textBoxHeight = maxHeight * textBoxFill
val dragState = remember {
AnchoredDraggableState(
initialValue = DragState.CLOSED,
positionalThreshold = { 0f },
velocityThreshold = { 0f },
animationSpec = tween()
)
}
var historyListHeight by remember { mutableStateOf(0.dp) } var historyListHeight by remember { mutableStateOf(0.dp) }
val keyboardHeight by remember(historyListHeight, textBoxHeight) { val keyboardHeight by remember(historyListHeight, textBoxHeight) {
derivedStateOf { derivedStateOf {
@ -234,10 +237,6 @@ private fun Ready(
focusManager.clearFocus() focusManager.clearFocus()
} }
LaunchedEffect(dragState.currentValue) {
showClearHistoryButton = dragState.currentValue == DragState.OPEN
}
BackHandler(dragState.currentValue != DragState.CLOSED) { BackHandler(dragState.currentValue != DragState.CLOSED) {
coroutineScope.launch { coroutineScope.launch {
dragState.animateTo(DragState.CLOSED) dragState.animateTo(DragState.CLOSED)
@ -251,7 +250,9 @@ private fun Ready(
.height(historyListHeight), .height(historyListHeight),
historyItems = uiState.history, historyItems = uiState.history,
formatterSymbols = uiState.formatterSymbols, formatterSymbols = uiState.formatterSymbols,
addTokens = addSymbol addTokens = addSymbol,
onDelete = onDelete,
showDeleteButtons = isOpen
) )
TextBox( TextBox(
@ -274,7 +275,14 @@ private fun Ready(
CalculatorKeyboard( CalculatorKeyboard(
modifier = Modifier modifier = Modifier
.semantics { testTag = "ready" } .semantics { testTag = "ready" }
.offset { IntOffset(x = 0, y = (historyListHeight + textBoxHeight).toPx().roundToInt()) } .offset {
IntOffset(
x = 0,
y = (historyListHeight + textBoxHeight)
.toPx()
.roundToInt()
)
}
.height(keyboardHeight) .height(keyboardHeight)
.fillMaxWidth() .fillMaxWidth()
.padding(horizontal = 8.dp, vertical = 4.dp), .padding(horizontal = 8.dp, vertical = 4.dp),
@ -375,6 +383,7 @@ private fun PreviewCalculatorScreen() {
toggleCalculatorMode = {}, toggleCalculatorMode = {},
equal = {}, equal = {},
clearHistory = {}, clearHistory = {},
addBracket = {} addBracket = {},
onDelete = {},
) )
} }

View File

@ -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.format
import com.sadellie.unitto.data.common.isExpression import com.sadellie.unitto.data.common.isExpression
import com.sadellie.unitto.data.common.stateIn 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.CalculatorHistoryRepository
import com.sadellie.unitto.data.model.repository.UserPreferencesRepository import com.sadellie.unitto.data.model.repository.UserPreferencesRepository
import dagger.hilt.android.lifecycle.HiltViewModel import dagger.hilt.android.lifecycle.HiltViewModel
@ -180,6 +181,10 @@ internal class CalculatorViewModel @Inject constructor(
calculatorHistoryRepository.clear() calculatorHistoryRepository.clear()
} }
fun deleteHistoryItem(item: HistoryItem) = viewModelScope.launch(Dispatchers.IO) {
calculatorHistoryRepository.delete(item)
}
fun equal() = viewModelScope.launch { fun equal() = viewModelScope.launch {
val prefs = _prefs.value ?: return@launch val prefs = _prefs.value ?: return@launch
if (_equalClicked.value) return@launch if (_equalClicked.value) return@launch

View File

@ -18,10 +18,15 @@
package com.sadellie.unitto.feature.calculator.components 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.background
import androidx.compose.foundation.layout.Arrangement import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Column 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.fillMaxSize
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.height import androidx.compose.foundation.layout.height
import androidx.compose.foundation.layout.wrapContentHeight import androidx.compose.foundation.layout.wrapContentHeight
import androidx.compose.foundation.lazy.LazyColumn 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.foundation.lazy.rememberLazyListState
import androidx.compose.material.icons.Icons import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.filled.History import androidx.compose.material.icons.filled.History
import androidx.compose.material.icons.outlined.Close
import androidx.compose.material3.Icon import androidx.compose.material3.Icon
import androidx.compose.material3.IconButton
import androidx.compose.material3.MaterialTheme import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.Text import androidx.compose.material3.Text
import androidx.compose.runtime.Composable import androidx.compose.runtime.Composable
@ -53,18 +60,26 @@ internal fun HistoryList(
historyItems: List<HistoryItem>, historyItems: List<HistoryItem>,
formatterSymbols: FormatterSymbols, formatterSymbols: FormatterSymbols,
addTokens: (String) -> Unit, addTokens: (String) -> Unit,
onDelete: (HistoryItem) -> Unit,
showDeleteButtons: Boolean,
) { ) {
if (historyItems.isEmpty()) { Crossfade(
HistoryListPlaceholder( targetState = historyItems.isEmpty()
modifier = modifier, ) { emptyList ->
) if (emptyList) {
} else { HistoryListPlaceholder(
HistoryListContent( modifier = modifier,
modifier = modifier, )
historyItems = historyItems, } else {
addTokens = addTokens, HistoryListContent(
formatterSymbols = formatterSymbols, modifier = modifier,
) historyItems = historyItems,
formatterSymbols = formatterSymbols,
addTokens = addTokens,
onDelete = onDelete,
showDeleteButtons = showDeleteButtons,
)
}
} }
} }
@ -92,19 +107,24 @@ private fun HistoryListPlaceholder(
private fun HistoryListContent( private fun HistoryListContent(
modifier: Modifier, modifier: Modifier,
historyItems: List<HistoryItem>, historyItems: List<HistoryItem>,
addTokens: (String) -> Unit,
formatterSymbols: FormatterSymbols, formatterSymbols: FormatterSymbols,
addTokens: (String) -> Unit,
onDelete: (HistoryItem) -> Unit,
showDeleteButtons: Boolean,
) { ) {
val state = rememberLazyListState() val state = rememberLazyListState()
val focusManager = LocalFocusManager.current val focusManager = LocalFocusManager.current
// Very bad workaround for https://issuetracker.google.com/issues/295745063 // Selection handles cause lag
// Will remove once the fix is released
LaunchedEffect(state.isScrollInProgress) { LaunchedEffect(state.isScrollInProgress) {
focusManager.clearFocus(true) 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( LazyColumn(
modifier = modifier, modifier = modifier,
@ -113,9 +133,12 @@ private fun HistoryListContent(
) { ) {
items(historyItems, { it.id }) { historyItem -> items(historyItems, { it.id }) { historyItem ->
HistoryListItem( HistoryListItem(
modifier = Modifier.animateItemPlacement(),
historyItem = historyItem, historyItem = historyItem,
formatterSymbols = formatterSymbols, formatterSymbols = formatterSymbols,
addTokens = addTokens, addTokens = addTokens,
onDelete = { onDelete(historyItem) },
showDeleteButton = showDeleteButtons,
) )
} }
} }
@ -127,24 +150,48 @@ private fun HistoryListItem(
historyItem: HistoryItem, historyItem: HistoryItem,
formatterSymbols: FormatterSymbols, formatterSymbols: FormatterSymbols,
addTokens: (String) -> Unit, addTokens: (String) -> Unit,
onDelete: () -> Unit,
showDeleteButton: Boolean,
) { ) {
Column( Row(
modifier = modifier.height(HistoryItemHeight), modifier = modifier.height(HistoryItemHeight),
verticalArrangement = Arrangement.Center verticalAlignment = Alignment.CenterVertically,
) { ) {
FixedExpressionInputTextField( AnimatedVisibility(visible = showDeleteButton) {
value = historyItem.expression, IconButton(onClick = onDelete) {
formatterSymbols = formatterSymbols, Icon(
textColor = MaterialTheme.colorScheme.onSurfaceVariant, modifier = Modifier,
onClick = addTokens, 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( FixedExpressionInputTextField(
value = historyItem.result, modifier = Modifier
formatterSymbols = formatterSymbols, .fillMaxWidth(),
textColor = MaterialTheme.colorScheme.onSurfaceVariant.copy(alpha = 0.5f), value = historyItem.result,
onClick = addTokens, formatterSymbols = formatterSymbols,
) textColor = MaterialTheme.colorScheme.onSurfaceVariant.copy(alpha = 0.5f),
onClick = { addTokens(historyItem.result) },
)
}
} }
} }
@ -179,6 +226,8 @@ private fun PreviewHistoryList() {
.fillMaxSize(), .fillMaxSize(),
historyItems = historyItems, historyItems = historyItems,
formatterSymbols = FormatterSymbols.Spaces, formatterSymbols = FormatterSymbols.Spaces,
addTokens = {} addTokens = {},
onDelete = {},
showDeleteButtons = true,
) )
} }