Keyboard in AddSubtract

This commit is contained in:
Sad Ellie 2023-09-16 18:46:48 +03:00
parent 532931914e
commit 7e6036f4ce
14 changed files with 584 additions and 207 deletions

View File

@ -21,8 +21,11 @@ package com.sadellie.unitto.core.ui
import android.content.ActivityNotFoundException
import android.content.Context
import android.content.Intent
import android.content.res.Configuration
import android.net.Uri
import android.widget.Toast
import androidx.compose.runtime.Composable
import androidx.compose.ui.platform.LocalConfiguration
import com.sadellie.unitto.core.base.R
/**
@ -35,3 +38,6 @@ fun openLink(mContext: Context, url: String) {
Toast.makeText(mContext, R.string.error_label, Toast.LENGTH_SHORT).show()
}
}
@Composable
fun isPortrait() = LocalConfiguration.current.orientation == Configuration.ORIENTATION_PORTRAIT

View File

@ -18,7 +18,6 @@
package com.sadellie.unitto.core.ui.common
import android.content.res.Configuration
import android.view.HapticFeedbackConstants
import androidx.compose.foundation.layout.PaddingValues
import androidx.compose.foundation.layout.fillMaxHeight
@ -29,20 +28,20 @@ import androidx.compose.runtime.rememberCoroutineScope
import androidx.compose.ui.Modifier
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.graphics.vector.ImageVector
import androidx.compose.ui.platform.LocalConfiguration
import androidx.compose.ui.platform.LocalView
import com.sadellie.unitto.core.ui.isPortrait
import kotlinx.coroutines.launch
@Composable
fun BasicKeyboardButton(
modifier: Modifier,
contentHeight: Float,
onClick: () -> Unit,
onLongClick: (() -> Unit)?,
containerColor: Color,
icon: ImageVector,
iconColor: Color,
allowVibration: Boolean,
contentHeight: Float,
) {
val view = LocalView.current
val coroutineScope = rememberCoroutineScope()
@ -53,7 +52,6 @@ fun BasicKeyboardButton(
}
}
}
UnittoButton(
modifier = modifier,
onClick = { onClick(); vibrate() },
@ -75,18 +73,19 @@ fun KeyboardButtonLight(
modifier: Modifier,
icon: ImageVector,
allowVibration: Boolean,
contentHeight: Float = if (isPortrait()) 0.51f else 0.7f,
onLongClick: (() -> Unit)? = null,
onClick: () -> Unit,
) {
BasicKeyboardButton(
modifier = modifier,
contentHeight = contentHeight,
onClick = onClick,
onLongClick = onLongClick,
containerColor = MaterialTheme.colorScheme.inverseOnSurface,
icon = icon,
iconColor = MaterialTheme.colorScheme.onSurfaceVariant,
allowVibration = allowVibration,
contentHeight = if (isPortrait()) 0.51f else 0.7f
)
}
@ -95,18 +94,19 @@ fun KeyboardButtonFilled(
modifier: Modifier,
icon: ImageVector,
allowVibration: Boolean,
contentHeight: Float = if (isPortrait()) 0.51f else 0.7f,
onLongClick: (() -> Unit)? = null,
onClick: () -> Unit,
) {
BasicKeyboardButton(
modifier = modifier,
contentHeight = contentHeight,
onClick = onClick,
onLongClick = onLongClick,
containerColor = MaterialTheme.colorScheme.primaryContainer,
icon = icon,
iconColor = MaterialTheme.colorScheme.onSecondaryContainer,
allowVibration = allowVibration,
contentHeight = if (isPortrait()) 0.51f else 0.7f
)
}
@ -115,21 +115,18 @@ fun KeyboardButtonAdditional(
modifier: Modifier,
icon: ImageVector,
allowVibration: Boolean,
contentHeight: Float = 0.8f,
onLongClick: (() -> Unit)? = null,
onClick: () -> Unit,
) {
BasicKeyboardButton(
modifier = modifier,
contentHeight = contentHeight,
onClick = onClick,
onLongClick = onLongClick,
containerColor = Color.Transparent,
icon = icon,
iconColor = MaterialTheme.colorScheme.onSurfaceVariant,
allowVibration = allowVibration,
contentHeight = if (isPortrait()) 0.8f else 0.8f
)
}
@Composable
private fun isPortrait() =
LocalConfiguration.current.orientation == Configuration.ORIENTATION_PORTRAIT

View File

@ -18,6 +18,11 @@
package com.sadellie.unitto.core.ui.common
import androidx.compose.animation.AnimatedVisibility
import androidx.compose.animation.fadeIn
import androidx.compose.animation.fadeOut
import androidx.compose.animation.slideInVertically
import androidx.compose.animation.slideOutVertically
import androidx.compose.foundation.layout.PaddingValues
import androidx.compose.foundation.layout.RowScope
import androidx.compose.material3.CenterAlignedTopAppBar
@ -51,18 +56,25 @@ fun UnittoScreenWithTopBar(
floatingActionButton: @Composable () -> Unit = {},
floatingActionButtonPosition: FabPosition = FabPosition.End,
scrollBehavior: TopAppBarScrollBehavior? = null,
showTopBar: Boolean = true,
content: @Composable (PaddingValues) -> Unit
) {
Scaffold(
modifier = modifier,
topBar = {
CenterAlignedTopAppBar(
title = title,
navigationIcon = navigationIcon,
actions = actions,
colors = colors,
scrollBehavior = scrollBehavior,
)
AnimatedVisibility(
visible = showTopBar,
enter = slideInVertically() + fadeIn(),
exit = slideOutVertically() + fadeOut()
) {
CenterAlignedTopAppBar(
title = title,
navigationIcon = navigationIcon,
actions = actions,
colors = colors,
scrollBehavior = scrollBehavior,
)
}
},
floatingActionButton = floatingActionButton,
floatingActionButtonPosition = floatingActionButtonPosition,

View File

@ -0,0 +1,56 @@
/*
* Unitto is a unit converter for Android
* Copyright (c) 2023 Elshan Agaev
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
package com.sadellie.unitto.core.ui.common.key.unittoicons
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.graphics.PathFillType.Companion.NonZero
import androidx.compose.ui.graphics.SolidColor
import androidx.compose.ui.graphics.StrokeCap.Companion.Butt
import androidx.compose.ui.graphics.StrokeJoin.Companion.Miter
import androidx.compose.ui.graphics.vector.ImageVector
import androidx.compose.ui.graphics.vector.ImageVector.Builder
import androidx.compose.ui.graphics.vector.path
import androidx.compose.ui.unit.dp
import com.sadellie.unitto.core.ui.common.key.UnittoIcons
val @Suppress("UnusedReceiverParameter") UnittoIcons.Check: ImageVector
get() {
if (_check != null) {
return _check!!
}
_check = Builder(name = "Check", defaultWidth = 150.0.dp, defaultHeight = 150.0.dp,
viewportWidth = 150.0f, viewportHeight = 150.0f).apply {
path(fill = SolidColor(Color(0xFF000000)), stroke = null, strokeLineWidth = 0.0f,
strokeLineCap = Butt, strokeLineJoin = Miter, strokeLineMiter = 4.0f,
pathFillType = NonZero) {
moveTo(59.625f, 112.156f)
lineTo(24.0f, 76.531f)
lineTo(32.906f, 67.625f)
lineTo(59.625f, 94.344f)
lineTo(116.969f, 37.0f)
lineTo(125.875f, 45.906f)
lineTo(59.625f, 112.156f)
close()
}
}
.build()
return _check!!
}
private var _check: ImageVector? = null

View File

@ -0,0 +1,65 @@
/*
* Unitto is a unit converter for Android
* Copyright (c) 2023 Elshan Agaev
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
package com.sadellie.unitto.core.ui.common.key.unittoicons
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.graphics.PathFillType.Companion.NonZero
import androidx.compose.ui.graphics.SolidColor
import androidx.compose.ui.graphics.StrokeCap.Companion.Butt
import androidx.compose.ui.graphics.StrokeJoin.Companion.Miter
import androidx.compose.ui.graphics.vector.ImageVector
import androidx.compose.ui.graphics.vector.ImageVector.Builder
import androidx.compose.ui.graphics.vector.path
import androidx.compose.ui.unit.dp
import com.sadellie.unitto.core.ui.common.key.UnittoIcons
val @Suppress("UnusedReceiverParameter") UnittoIcons.Tab: ImageVector
get() {
if (_tab != null) {
return _tab!!
}
_tab = Builder(name = "Tab", defaultWidth = 150.0.dp, defaultHeight = 150.0.dp,
viewportWidth = 150.0f, viewportHeight = 150.0f).apply {
path(fill = SolidColor(Color(0xFF000000)), stroke = null, strokeLineWidth = 0.0f,
strokeLineCap = Butt, strokeLineJoin = Miter, strokeLineMiter = 4.0f,
pathFillType = NonZero) {
moveTo(125.0f, 112.5f)
verticalLineTo(37.5f)
horizontalLineTo(137.5f)
verticalLineTo(112.5f)
horizontalLineTo(125.0f)
close()
moveTo(75.0f, 112.5f)
lineTo(66.094f, 103.75f)
lineTo(88.594f, 81.25f)
horizontalLineTo(12.5f)
verticalLineTo(68.75f)
horizontalLineTo(88.594f)
lineTo(66.25f, 46.25f)
lineTo(75.0f, 37.5f)
lineTo(112.5f, 75.0f)
lineTo(75.0f, 112.5f)
close()
}
}
.build()
return _tab!!
}
private var _tab: ImageVector? = null

View File

@ -126,6 +126,7 @@ data class UnitGroupsPreferences(
data class AddSubtractPreferences(
val separator: Int = Separator.SPACE,
val enableVibrations: Boolean = true,
)
data class AboutPreferences(
@ -232,7 +233,8 @@ class UserPreferencesRepository @Inject constructor(private val dataStore: DataS
val addSubtractPrefs: Flow<AddSubtractPreferences> = data
.map { preferences ->
AddSubtractPreferences(
separator = preferences[PrefsKeys.SEPARATOR] ?: Separator.SPACE
separator = preferences[PrefsKeys.SEPARATOR] ?: Separator.SPACE,
enableVibrations = preferences[PrefsKeys.ENABLE_VIBRATIONS] ?: true,
)
}

View File

@ -386,7 +386,7 @@ private fun PortraitKeyboard(
KeyboardButtonLight(mainButtonModifier, UnittoIcons.Key0, allowVibration) { addSymbol(Token.Digit._0) }
KeyboardButtonLight(mainButtonModifier, fractionalIcon, allowVibration) { addSymbol(Token.Digit.dot) }
}
KeyboardButtonLight(mainButtonModifier, UnittoIcons.Backspace, allowVibration, clearSymbols) { deleteSymbol() }
KeyboardButtonLight(mainButtonModifier, UnittoIcons.Backspace, allowVibration, onLongClick = clearSymbols) { deleteSymbol() }
KeyboardButtonFilled(mainButtonModifier, UnittoIcons.Equal, allowVibration) { evaluate() }
}
@ -538,7 +538,7 @@ private fun LandscapeKeyboard(
KeyboardButtonLight(buttonModifier, UnittoIcons.Key9, allowVibration) { addSymbol(Token.Digit._9) }
KeyboardButtonLight(buttonModifier, UnittoIcons.Key6, allowVibration) { addSymbol(Token.Digit._6) }
KeyboardButtonLight(buttonModifier, UnittoIcons.Key3, allowVibration) { addSymbol(Token.Digit._3) }
KeyboardButtonLight(buttonModifier, UnittoIcons.Backspace, allowVibration, clearSymbols) { deleteSymbol() }
KeyboardButtonLight(buttonModifier, UnittoIcons.Backspace, allowVibration, onLongClick = clearSymbols) { deleteSymbol() }
}
Column(Modifier.weight(1f)) {

View File

@ -109,7 +109,7 @@ internal fun DefaultKeyboard(
KeyboardButtonLight(bModifier, UnittoIcons.Key0, allowVibration) { addDigit(Token.Digit._0) }
KeyboardButtonLight(bModifier, fractionalIcon, allowVibration) { addDigit(Token.Digit.dot) }
}
KeyboardButtonLight(bModifier, UnittoIcons.Backspace, allowVibration, clearInput) { deleteDigit() }
KeyboardButtonLight(bModifier, UnittoIcons.Backspace, allowVibration, onLongClick = clearInput) { deleteDigit() }
KeyboardButtonFilled(bModifier, UnittoIcons.Plus, allowVibration) { addDigit(Token.Operator.plus) }
}
}
@ -159,7 +159,7 @@ internal fun NumberBaseKeyboard(
Row(cModifier, horizontalArrangement) {
KeyboardButtonLight(bModifier, UnittoIcons.Key0, allowVibration) { addDigit(Token.Digit._0) }
KeyboardButtonLight(
Modifier.fillMaxSize().weight(2f).padding(it.maxWidth * 0.015f, it.maxHeight * 0.008f), UnittoIcons.Backspace, allowVibration, clearInput) { deleteDigit() }
Modifier.fillMaxSize().weight(2f).padding(it.maxWidth * 0.015f, it.maxHeight * 0.008f), UnittoIcons.Backspace, allowVibration, onLongClick = clearInput) { deleteDigit() }
}
}
}

View File

@ -26,8 +26,11 @@ import androidx.compose.material3.Tab
import androidx.compose.material3.TabRow
import androidx.compose.material3.Text
import androidx.compose.runtime.Composable
import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember
import androidx.compose.runtime.rememberCoroutineScope
import androidx.compose.runtime.setValue
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.platform.LocalFocusManager
@ -60,6 +63,7 @@ internal fun DateCalculatorScreen(
val addSubtractLabel = "${stringResource(R.string.add)}/${stringResource(R.string.subtract)}"
val differenceLabel = stringResource(R.string.difference)
val focusManager = LocalFocusManager.current
var topBarShown by remember { mutableStateOf(true) }
val allTabs = remember { mutableListOf(addSubtractLabel, differenceLabel) }
val pagerState = rememberPagerState { allTabs.size }
@ -69,9 +73,8 @@ internal fun DateCalculatorScreen(
modifier = Modifier,
title = { Text(stringResource(R.string.date_calculator)) },
navigationIcon = { MenuButton(navigateToMenu) },
actions = {
SettingsButton(navigateToSettings)
},
actions = { SettingsButton(navigateToSettings) },
showTopBar = topBarShown,
) { paddingValues ->
Column(
modifier = Modifier.padding(paddingValues),
@ -97,9 +100,14 @@ internal fun DateCalculatorScreen(
verticalAlignment = Alignment.Top
) { page ->
when (page) {
0 -> AddSubtractPage()
1 -> DateDifferencePage().also {
0 -> AddSubtractPage(
toggleTopBar = { topBarShown = it }
)
1 -> {
focusManager.clearFocus(true)
topBarShown = true
DateDifferencePage()
}
}
}

View File

@ -24,11 +24,19 @@ import android.content.Context
import android.content.Intent
import android.provider.CalendarContract
import android.widget.Toast
import androidx.activity.compose.BackHandler
import androidx.compose.animation.AnimatedVisibility
import androidx.compose.animation.fadeIn
import androidx.compose.animation.fadeOut
import androidx.compose.animation.slideInVertically
import androidx.compose.animation.slideOutVertically
import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.ExperimentalLayoutApi
import androidx.compose.foundation.layout.FlowRow
import androidx.compose.foundation.layout.PaddingValues
import androidx.compose.foundation.layout.fillMaxHeight
import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.lazy.LazyColumn
@ -37,25 +45,34 @@ import androidx.compose.material.icons.filled.Event
import androidx.compose.material.icons.outlined.Add
import androidx.compose.material.icons.outlined.Remove
import androidx.compose.material3.FloatingActionButton
import androidx.compose.material3.HorizontalDivider
import androidx.compose.material3.Icon
import androidx.compose.material3.Scaffold
import androidx.compose.material3.SegmentedButton
import androidx.compose.material3.SegmentedButtonDefaults
import androidx.compose.material3.SingleChoiceSegmentedButtonRow
import androidx.compose.runtime.Composable
import androidx.compose.runtime.LaunchedEffect
import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember
import androidx.compose.runtime.setValue
import androidx.compose.ui.Modifier
import androidx.compose.ui.focus.onFocusEvent
import androidx.compose.ui.platform.LocalContext
import androidx.compose.ui.platform.LocalFocusManager
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.text.input.ImeAction
import androidx.compose.ui.text.input.TextFieldValue
import androidx.compose.ui.tooling.preview.Preview
import androidx.compose.ui.unit.dp
import androidx.hilt.navigation.compose.hiltViewModel
import androidx.lifecycle.compose.collectAsStateWithLifecycle
import com.sadellie.unitto.core.base.R
import com.sadellie.unitto.core.ui.common.textfield.addTokens
import com.sadellie.unitto.core.ui.common.textfield.deleteTokens
import com.sadellie.unitto.core.ui.isPortrait
import com.sadellie.unitto.feature.datecalculator.components.AddSubtractKeyboard
import com.sadellie.unitto.feature.datecalculator.components.DateTimeDialogs
import com.sadellie.unitto.feature.datecalculator.components.DateTimeSelectorBlock
import com.sadellie.unitto.feature.datecalculator.components.DialogState
@ -65,18 +82,20 @@ import java.time.ZonedDateTime
@Composable
internal fun AddSubtractPage(
viewModel: AddSubtractViewModel = hiltViewModel(),
toggleTopBar: (Boolean) -> Unit,
) {
val uiState = viewModel.uiState.collectAsStateWithLifecycle().value
AddSubtractView(
uiState = uiState,
toggleTopBar = toggleTopBar,
updateStart = viewModel::updateStart,
updateYears = viewModel::updateYears,
updateMonths = viewModel::updateMonths,
updateDays = viewModel::updateDays,
updateHours = viewModel::updateHours,
updateMinutes = viewModel::updateMinutes,
updateAddition = viewModel::updateAddition
updateAddition = viewModel::updateAddition,
)
}
@ -85,120 +104,207 @@ internal fun AddSubtractPage(
@Composable
private fun AddSubtractView(
uiState: AddSubtractState,
toggleTopBar: (Boolean) -> Unit,
updateStart: (ZonedDateTime) -> Unit,
updateYears: (String) -> Unit,
updateMonths: (String) -> Unit,
updateDays: (String) -> Unit,
updateHours: (String) -> Unit,
updateMinutes: (String) -> Unit,
updateYears: (TextFieldValue) -> Unit,
updateMonths: (TextFieldValue) -> Unit,
updateDays: (TextFieldValue) -> Unit,
updateHours: (TextFieldValue) -> Unit,
updateMinutes: (TextFieldValue) -> Unit,
updateAddition: (Boolean) -> Unit,
) {
var dialogState by remember { mutableStateOf(DialogState.NONE) }
val mContext = LocalContext.current
var addSymbol: ((TextFieldValue) -> Unit)? by remember { mutableStateOf(null) }
var focusedTextFieldValue: TextFieldValue? by remember { mutableStateOf(null) }
val showKeyboard = (addSymbol != null) and (focusedTextFieldValue != null)
val landscape = !isPortrait()
val focusManager = LocalFocusManager.current
Scaffold(
floatingActionButton = {
FloatingActionButton(onClick = { mContext.addEvent(uiState.start, uiState.result) }) {
Icon(
imageVector = Icons.Default.Event,
contentDescription = null,
)
LaunchedEffect(showKeyboard, landscape) {
toggleTopBar(showKeyboard and landscape)
}
BackHandler(showKeyboard) {
focusManager.clearFocus()
addSymbol = null
focusedTextFieldValue = null
}
Column(Modifier.fillMaxSize()) {
Scaffold(
modifier = Modifier
.fillMaxHeight()
.weight(1f),
floatingActionButton = {
FloatingActionButton(
onClick = {
mContext.addEvent(uiState.start, uiState.result)
}
) {
Icon(
imageVector = Icons.Default.Event,
contentDescription = null,
)
}
}
) {
LazyColumn(
modifier = Modifier.padding(horizontal = 16.dp),
verticalArrangement = Arrangement.spacedBy(16.dp),
contentPadding = PaddingValues(top = 16.dp)
) {
item("dates") {
FlowRow(
modifier = Modifier,
maxItemsInEachRow = 2,
verticalArrangement = Arrangement.spacedBy(16.dp),
horizontalArrangement = Arrangement.spacedBy(16.dp)
) {
DateTimeSelectorBlock(
modifier = Modifier
.weight(1f)
.fillMaxWidth(),
title = stringResource(R.string.date_difference_start),
dateTime = uiState.start,
onLongClick = { updateStart(ZonedDateTime.now()) },
onClick = { dialogState = DialogState.FROM },
onTimeClick = { dialogState = DialogState.FROM_TIME },
onDateClick = { dialogState = DialogState.FROM_DATE },
)
DateTimeSelectorBlock(
modifier = Modifier
.weight(1f)
.fillMaxWidth(),
title = stringResource(R.string.date_difference_end),
dateTime = uiState.result,
)
}
}
item("modes") {
SingleChoiceSegmentedButtonRow(
modifier = Modifier
.fillMaxWidth(),
) {
SegmentedButton(
selected = uiState.addition,
onClick = { updateAddition(true) },
shape = SegmentedButtonDefaults.itemShape(index = 0, count = 2),
icon = {}
) {
Icon(Icons.Outlined.Add, null)
}
SegmentedButton(
selected = !uiState.addition,
onClick = { updateAddition(false) },
shape = SegmentedButtonDefaults.itemShape(index = 1, count = 2),
icon = {}
) {
Icon(Icons.Outlined.Remove, null)
}
}
}
item("textFields") {
Column {
TimeUnitTextField(
modifier = Modifier.onFocusEvent {
if (it.hasFocus) {
addSymbol = updateYears
focusedTextFieldValue = uiState.years
}
},
value = uiState.years,
onValueChange = updateYears,
label = stringResource(R.string.date_difference_years),
formatterSymbols = uiState.formatterSymbols
)
TimeUnitTextField(
modifier = Modifier.onFocusEvent {
if (it.hasFocus) {
addSymbol = updateMonths
focusedTextFieldValue = uiState.months
}
},
value = uiState.months,
onValueChange = updateMonths,
label = stringResource(R.string.date_difference_months),
formatterSymbols = uiState.formatterSymbols
)
TimeUnitTextField(
modifier = Modifier.onFocusEvent {
if (it.hasFocus) {
addSymbol = updateDays
focusedTextFieldValue = uiState.days
}
},
value = uiState.days,
onValueChange = updateDays,
label = stringResource(R.string.date_difference_days),
formatterSymbols = uiState.formatterSymbols
)
TimeUnitTextField(
modifier = Modifier.onFocusEvent {
if (it.hasFocus) {
addSymbol = updateHours
focusedTextFieldValue = uiState.hours
}
},
value = uiState.hours,
onValueChange = updateHours,
label = stringResource(R.string.date_difference_hours),
formatterSymbols = uiState.formatterSymbols
)
TimeUnitTextField(
modifier = Modifier.onFocusEvent {
if (it.hasFocus) {
addSymbol = updateMinutes
focusedTextFieldValue = uiState.minutes
}
},
value = uiState.minutes,
onValueChange = updateMinutes,
label = stringResource(R.string.date_difference_minutes),
formatterSymbols = uiState.formatterSymbols
)
}
}
}
}
) {
LazyColumn(
modifier = Modifier.padding(start = 16.dp, end = 16.dp, top = 16.dp),
verticalArrangement = Arrangement.spacedBy(16.dp),
contentPadding = PaddingValues(bottom = 88.dp)
AnimatedVisibility(
visible = showKeyboard,
enter = slideInVertically { it / 2 } + fadeIn(),
exit = slideOutVertically { it / 2 } + fadeOut()
) {
item("dates") {
FlowRow(
modifier = Modifier,
maxItemsInEachRow = 2,
verticalArrangement = Arrangement.spacedBy(16.dp),
horizontalArrangement = Arrangement.spacedBy(16.dp)
) {
DateTimeSelectorBlock(
modifier = Modifier
.weight(1f)
.fillMaxWidth(),
title = stringResource(R.string.date_difference_start),
dateTime = uiState.start,
onLongClick = { updateStart(ZonedDateTime.now()) },
onClick = { dialogState = DialogState.FROM },
onTimeClick = { dialogState = DialogState.FROM_TIME },
onDateClick = { dialogState = DialogState.FROM_DATE },
)
DateTimeSelectorBlock(
modifier = Modifier
.weight(1f)
.fillMaxWidth(),
title = stringResource(R.string.date_difference_end),
dateTime = uiState.result,
)
}
}
item("modes") {
SingleChoiceSegmentedButtonRow(
modifier = Modifier
.fillMaxWidth(),
) {
SegmentedButton(
selected = uiState.addition,
onClick = { updateAddition(true) },
shape = SegmentedButtonDefaults.itemShape(index = 0, count = 2),
icon = {}
) {
Icon(Icons.Outlined.Add, null)
HorizontalDivider()
AddSubtractKeyboard(
modifier = Modifier
.weight(1f)
.fillMaxWidth()
.fillMaxHeight(if (isPortrait()) 0.4f else 0.6f)
.padding(2.dp, 4.dp),
addSymbol = {
val newValue = focusedTextFieldValue?.addTokens(it)
if (newValue != null) {
addSymbol?.invoke(newValue)
}
SegmentedButton(
selected = !uiState.addition,
onClick = { updateAddition(false) },
shape = SegmentedButtonDefaults.itemShape(index = 1, count = 2),
icon = {}
) {
Icon(Icons.Outlined.Remove, null)
},
deleteSymbol = {
val newValue = focusedTextFieldValue?.deleteTokens()
if (newValue != null) {
addSymbol?.invoke(newValue)
}
}
}
item("textFields") {
Column {
TimeUnitTextField(
value = uiState.years,
onValueChange = updateYears,
label = stringResource(R.string.date_difference_years),
formatterSymbols = uiState.formatterSymbols
)
TimeUnitTextField(
value = uiState.months,
onValueChange = updateMonths,
label = stringResource(R.string.date_difference_months),
formatterSymbols = uiState.formatterSymbols
)
TimeUnitTextField(
value = uiState.days,
onValueChange = updateDays,
label = stringResource(R.string.date_difference_days),
formatterSymbols = uiState.formatterSymbols
)
TimeUnitTextField(
value = uiState.hours,
onValueChange = updateHours,
label = stringResource(R.string.date_difference_hours),
formatterSymbols = uiState.formatterSymbols
)
TimeUnitTextField(
value = uiState.minutes,
onValueChange = updateMinutes,
label = stringResource(R.string.date_difference_minutes),
imeAction = ImeAction.Done,
formatterSymbols = uiState.formatterSymbols
)
}
}
},
onConfirm = {
focusManager.clearFocus()
addSymbol = null
focusedTextFieldValue = null
},
allowVibration = uiState.allowVibration,
imeAction = if (addSymbol == updateMinutes) ImeAction.Done else ImeAction.Next
)
}
}
@ -234,14 +340,15 @@ private fun Context.addEvent(start: ZonedDateTime, end: ZonedDateTime) {
fun AddSubtractViewPreview() {
AddSubtractView(
uiState = AddSubtractState(
years = "12"
years = TextFieldValue("12")
),
toggleTopBar = {},
updateStart = {},
updateYears = {},
updateMonths = {},
updateDays = {},
updateHours = {},
updateMinutes = {},
updateAddition = {}
updateAddition = {},
)
}

View File

@ -18,17 +18,19 @@
package com.sadellie.unitto.feature.datecalculator.addsubtract
import androidx.compose.ui.text.input.TextFieldValue
import com.sadellie.unitto.core.ui.common.textfield.FormatterSymbols
import java.time.ZonedDateTime
internal data class AddSubtractState(
val start: ZonedDateTime = ZonedDateTime.now(),
val result: ZonedDateTime = ZonedDateTime.now(),
val years: String = "",
val months: String = "",
val days: String = "",
val hours: String = "",
val minutes: String = "",
val years: TextFieldValue = TextFieldValue(),
val months: TextFieldValue = TextFieldValue(),
val days: TextFieldValue = TextFieldValue(),
val hours: TextFieldValue = TextFieldValue(),
val minutes: TextFieldValue = TextFieldValue(),
val addition: Boolean = true,
val formatterSymbols: FormatterSymbols = FormatterSymbols.Spaces,
val allowVibration: Boolean = false,
)

View File

@ -18,6 +18,8 @@
package com.sadellie.unitto.feature.datecalculator.addsubtract
import androidx.compose.ui.text.TextRange
import androidx.compose.ui.text.input.TextFieldValue
import androidx.lifecycle.ViewModel
import androidx.lifecycle.viewModelScope
import com.sadellie.unitto.core.ui.common.textfield.AllFormatterSymbols
@ -44,7 +46,8 @@ internal class AddSubtractViewModel @Inject constructor(
val uiState: StateFlow<AddSubtractState> = _uiState
.combine(userPreferencesRepository.addSubtractPrefs) { uiState, userPrefs ->
return@combine uiState.copy(
formatterSymbols = AllFormatterSymbols.getById(userPrefs.separator)
formatterSymbols = AllFormatterSymbols.getById(userPrefs.separator),
allowVibration = userPrefs.enableVibrations,
)
}
.onEach { updateResult() }
@ -52,78 +55,57 @@ internal class AddSubtractViewModel @Inject constructor(
fun updateStart(newValue: ZonedDateTime) = _uiState.update { it.copy(start = newValue) }
fun updateYears(newValue: String) = _uiState.update {
val years = when {
newValue.isEmpty() -> newValue
newValue.toLong() > 9_999L -> "9999"
else -> newValue
}
it.copy(years = years)
fun updateYears(value: TextFieldValue) = _uiState.update {
it.copy(years = checkWithMax(value, 9_999L))
}
fun updateMonths(newValue: String) = _uiState.update {
val months = when {
newValue.isEmpty() -> newValue
newValue.toLong() > 9_999L -> "9999"
else -> newValue
}
it.copy(months = months)
fun updateMonths(value: TextFieldValue) = _uiState.update {
it.copy(months = checkWithMax(value, 9_999L))
}
fun updateDays(newValue: String) = _uiState.update {
val days = when {
newValue.isEmpty() -> newValue
newValue.toLong() > 99_999L -> "99999"
else -> newValue
}
it.copy(days = days)
fun updateDays(value: TextFieldValue) = _uiState.update {
it.copy(days = checkWithMax(value, 99_999L))
}
fun updateHours(newValue: String) = _uiState.update {
val hours = when {
newValue.isEmpty() -> newValue
newValue.toLong() > 9_999_999L -> "9999999"
else -> newValue
}
it.copy(hours = hours)
fun updateHours(value: TextFieldValue) = _uiState.update {
it.copy(hours = checkWithMax(value, 9_999_999L))
}
fun updateMinutes(newValue: String) = _uiState.update {
val minutes = when {
newValue.isEmpty() -> newValue
newValue.toLong() > 99_999_999L -> "99999999"
else -> newValue
}
it.copy(minutes = minutes)
fun updateMinutes(value: TextFieldValue) = _uiState.update {
it.copy(minutes = checkWithMax(value, 99_999_999L))
}
// BCE is not handled properly because who gives a shit...
fun updateAddition(newValue: Boolean) = _uiState.update { it.copy(addition = newValue) }
fun updateAddition(newValue: Boolean) = _uiState.update {
it.copy(addition = newValue)
}
private fun updateResult() = viewModelScope.launch(Dispatchers.Default) {
// Gets canceled, works with latest _uiState only
_uiState.update { ui ->
val newResult = if (ui.addition) {
ui.start
.plusYears(ui.years.ifEmpty { "0" }.toLong())
.plusMonths(ui.months.ifEmpty { "0" }.toLong())
.plusDays(ui.days.ifEmpty { "0" }.toLong())
.plusHours(ui.hours.ifEmpty { "0" }.toLong())
.plusMinutes(ui.minutes.ifEmpty { "0" }.toLong())
.plusYears(ui.years.text.ifEmpty { "0" }.toLong())
.plusMonths(ui.months.text.ifEmpty { "0" }.toLong())
.plusDays(ui.days.text.ifEmpty { "0" }.toLong())
.plusHours(ui.hours.text.ifEmpty { "0" }.toLong())
.plusMinutes(ui.minutes.text.ifEmpty { "0" }.toLong())
} else {
ui.start
.minusYears(ui.years.ifEmpty { "0" }.toLong())
.minusMonths(ui.months.ifEmpty { "0" }.toLong())
.minusDays(ui.days.ifEmpty { "0" }.toLong())
.minusHours(ui.hours.ifEmpty { "0" }.toLong())
.minusMinutes(ui.minutes.ifEmpty { "0" }.toLong())
.minusYears(ui.years.text.ifEmpty { "0" }.toLong())
.minusMonths(ui.months.text.ifEmpty { "0" }.toLong())
.minusDays(ui.days.text.ifEmpty { "0" }.toLong())
.minusHours(ui.hours.text.ifEmpty { "0" }.toLong())
.minusMinutes(ui.minutes.text.ifEmpty { "0" }.toLong())
}
ui.copy(result = newResult)
}
}
private fun checkWithMax(value: TextFieldValue, maxValue: Long): TextFieldValue {
if (value.text.isEmpty()) return value
if (value.text.toLong() <= maxValue) return value
val maxValueText = maxValue.toString()
return TextFieldValue(maxValueText, TextRange(maxValueText.length))
}
}

View File

@ -0,0 +1,145 @@
/*
* Unitto is a unit converter for Android
* Copyright (c) 2023 Elshan Agaev
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
package com.sadellie.unitto.feature.datecalculator.components
import androidx.compose.animation.Crossfade
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.Spacer
import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.layout.padding
import androidx.compose.runtime.Composable
import androidx.compose.ui.Modifier
import androidx.compose.ui.focus.FocusDirection
import androidx.compose.ui.focus.FocusManager
import androidx.compose.ui.platform.LocalFocusManager
import androidx.compose.ui.text.input.ImeAction
import androidx.compose.ui.unit.dp
import com.sadellie.unitto.core.base.Token
import com.sadellie.unitto.core.ui.common.KeyboardButtonFilled
import com.sadellie.unitto.core.ui.common.KeyboardButtonLight
import com.sadellie.unitto.core.ui.common.key.UnittoIcons
import com.sadellie.unitto.core.ui.common.key.unittoicons.Backspace
import com.sadellie.unitto.core.ui.common.key.unittoicons.Check
import com.sadellie.unitto.core.ui.common.key.unittoicons.Key0
import com.sadellie.unitto.core.ui.common.key.unittoicons.Key1
import com.sadellie.unitto.core.ui.common.key.unittoicons.Key2
import com.sadellie.unitto.core.ui.common.key.unittoicons.Key3
import com.sadellie.unitto.core.ui.common.key.unittoicons.Key4
import com.sadellie.unitto.core.ui.common.key.unittoicons.Key5
import com.sadellie.unitto.core.ui.common.key.unittoicons.Key6
import com.sadellie.unitto.core.ui.common.key.unittoicons.Key7
import com.sadellie.unitto.core.ui.common.key.unittoicons.Key8
import com.sadellie.unitto.core.ui.common.key.unittoicons.Key9
import com.sadellie.unitto.core.ui.common.key.unittoicons.Tab
import com.sadellie.unitto.core.ui.isPortrait
@Composable
internal fun AddSubtractKeyboard(
modifier: Modifier,
addSymbol: (String) -> Unit,
deleteSymbol: () -> Unit,
onConfirm: () -> Unit,
allowVibration: Boolean,
imeAction: ImeAction,
focusManager: FocusManager = LocalFocusManager.current
) {
Row(modifier) {
val weightModifier = Modifier.weight(1f)
val mainButtonModifier = Modifier
.fillMaxSize()
.weight(1f)
.padding(4.dp)
val actionIconHeight = if (isPortrait()) 0.35f else 0.6f
fun keyboardAction() {
when (imeAction) {
ImeAction.Next -> focusManager.moveFocus(FocusDirection.Next)
else -> onConfirm()
}
}
Column(weightModifier) {
KeyboardButtonLight(mainButtonModifier, UnittoIcons.Key7, allowVibration) {
addSymbol(Token.Digit._7)
}
KeyboardButtonLight(mainButtonModifier, UnittoIcons.Key4, allowVibration) {
addSymbol(Token.Digit._4
)
}
KeyboardButtonLight(mainButtonModifier, UnittoIcons.Key1, allowVibration) {
addSymbol(Token.Digit._1)
}
Spacer(mainButtonModifier)
}
Column(weightModifier) {
KeyboardButtonLight(mainButtonModifier, UnittoIcons.Key8, allowVibration) {
addSymbol(Token.Digit._8)
}
KeyboardButtonLight(mainButtonModifier, UnittoIcons.Key5, allowVibration) {
addSymbol(Token.Digit._5)
}
KeyboardButtonLight(mainButtonModifier, UnittoIcons.Key2, allowVibration) {
addSymbol(Token.Digit._2)
}
KeyboardButtonLight(mainButtonModifier, UnittoIcons.Key0, allowVibration) {
addSymbol(Token.Digit._0)
}
}
Column(weightModifier) {
KeyboardButtonLight(mainButtonModifier, UnittoIcons.Key9, allowVibration) {
addSymbol(Token.Digit._9)
}
KeyboardButtonLight(mainButtonModifier, UnittoIcons.Key6, allowVibration) {
addSymbol(Token.Digit._6)
}
KeyboardButtonLight(mainButtonModifier, UnittoIcons.Key3, allowVibration) {
addSymbol(Token.Digit._3)
}
Spacer(mainButtonModifier)
}
Column(weightModifier) {
Crossfade(targetState = imeAction, modifier = mainButtonModifier) {
when (it) {
ImeAction.Next -> KeyboardButtonFilled(
Modifier.fillMaxSize(),
UnittoIcons.Tab,
allowVibration,
actionIconHeight
) { keyboardAction() }
else -> KeyboardButtonFilled(
Modifier.fillMaxSize(),
UnittoIcons.Check,
allowVibration,
actionIconHeight
) { keyboardAction() }
}
}
KeyboardButtonLight(
mainButtonModifier,
UnittoIcons.Backspace,
allowVibration,
actionIconHeight
) { deleteSymbol() }
}
}
}

View File

@ -22,7 +22,6 @@ import androidx.compose.animation.AnimatedVisibility
import androidx.compose.animation.scaleIn
import androidx.compose.animation.scaleOut
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.text.KeyboardOptions
import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.outlined.Clear
import androidx.compose.material3.Icon
@ -31,43 +30,39 @@ import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.OutlinedTextField
import androidx.compose.material3.Text
import androidx.compose.runtime.Composable
import androidx.compose.runtime.CompositionLocalProvider
import androidx.compose.ui.Modifier
import androidx.compose.ui.text.input.ImeAction
import androidx.compose.ui.text.input.KeyboardType
import androidx.compose.ui.platform.LocalTextInputService
import androidx.compose.ui.text.input.TextFieldValue
import com.sadellie.unitto.core.ui.common.textfield.ExpressionTransformer
import com.sadellie.unitto.core.ui.common.textfield.FormatterSymbols
@Composable
internal fun TimeUnitTextField(
value: String,
onValueChange: (String) -> Unit,
modifier: Modifier,
value: TextFieldValue,
onValueChange: (TextFieldValue) -> Unit,
label: String,
imeAction: ImeAction = ImeAction.Next,
formatterSymbols: FormatterSymbols
) {
) = CompositionLocalProvider(LocalTextInputService provides null) {
OutlinedTextField(
modifier = Modifier.fillMaxWidth(),
modifier = modifier.fillMaxWidth(),
value = value,
onValueChange = { newValue ->
onValueChange(newValue.filter { it.isDigit() })
onValueChange(newValue.copy(newValue.text.filter { it.isDigit() }))
},
label = { Text(label, color = MaterialTheme.colorScheme.onSurfaceVariant) },
trailingIcon = {
AnimatedVisibility(
visible = value.isNotBlank(),
visible = value.text.isNotBlank(),
enter = scaleIn(),
exit = scaleOut()
) {
IconButton(onClick = { onValueChange("") }) {
IconButton(onClick = { onValueChange(TextFieldValue()) }) {
Icon(Icons.Outlined.Clear, null)
}
}
},
keyboardOptions = KeyboardOptions(
autoCorrect = false,
keyboardType = KeyboardType.Decimal,
imeAction = imeAction
),
visualTransformation = ExpressionTransformer(formatterSymbols)
)
}