diff --git a/core/ui/src/main/java/com/sadellie/unitto/core/ui/UIUtils.kt b/core/ui/src/main/java/com/sadellie/unitto/core/ui/UIUtils.kt index b055c7af..68b6f554 100644 --- a/core/ui/src/main/java/com/sadellie/unitto/core/ui/UIUtils.kt +++ b/core/ui/src/main/java/com/sadellie/unitto/core/ui/UIUtils.kt @@ -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 diff --git a/core/ui/src/main/java/com/sadellie/unitto/core/ui/common/KeyboardButton.kt b/core/ui/src/main/java/com/sadellie/unitto/core/ui/common/KeyboardButton.kt index 0f663262..c51763c9 100644 --- a/core/ui/src/main/java/com/sadellie/unitto/core/ui/common/KeyboardButton.kt +++ b/core/ui/src/main/java/com/sadellie/unitto/core/ui/common/KeyboardButton.kt @@ -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 diff --git a/core/ui/src/main/java/com/sadellie/unitto/core/ui/common/UnittoScreenWithTopBar.kt b/core/ui/src/main/java/com/sadellie/unitto/core/ui/common/UnittoScreenWithTopBar.kt index e87eedc2..8e508963 100644 --- a/core/ui/src/main/java/com/sadellie/unitto/core/ui/common/UnittoScreenWithTopBar.kt +++ b/core/ui/src/main/java/com/sadellie/unitto/core/ui/common/UnittoScreenWithTopBar.kt @@ -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, diff --git a/core/ui/src/main/java/com/sadellie/unitto/core/ui/common/key/unittoicons/Check.kt b/core/ui/src/main/java/com/sadellie/unitto/core/ui/common/key/unittoicons/Check.kt new file mode 100644 index 00000000..48f26960 --- /dev/null +++ b/core/ui/src/main/java/com/sadellie/unitto/core/ui/common/key/unittoicons/Check.kt @@ -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 . + */ + +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 diff --git a/core/ui/src/main/java/com/sadellie/unitto/core/ui/common/key/unittoicons/Tab.kt b/core/ui/src/main/java/com/sadellie/unitto/core/ui/common/key/unittoicons/Tab.kt new file mode 100644 index 00000000..08cc365c --- /dev/null +++ b/core/ui/src/main/java/com/sadellie/unitto/core/ui/common/key/unittoicons/Tab.kt @@ -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 . + */ + +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 diff --git a/data/userprefs/src/main/java/com/sadellie/unitto/data/userprefs/UserPreferences.kt b/data/userprefs/src/main/java/com/sadellie/unitto/data/userprefs/UserPreferences.kt index 7d70a9f6..c9439e0c 100644 --- a/data/userprefs/src/main/java/com/sadellie/unitto/data/userprefs/UserPreferences.kt +++ b/data/userprefs/src/main/java/com/sadellie/unitto/data/userprefs/UserPreferences.kt @@ -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 = data .map { preferences -> AddSubtractPreferences( - separator = preferences[PrefsKeys.SEPARATOR] ?: Separator.SPACE + separator = preferences[PrefsKeys.SEPARATOR] ?: Separator.SPACE, + enableVibrations = preferences[PrefsKeys.ENABLE_VIBRATIONS] ?: true, ) } diff --git a/feature/calculator/src/main/java/com/sadellie/unitto/feature/calculator/components/CalculatorKeyboard.kt b/feature/calculator/src/main/java/com/sadellie/unitto/feature/calculator/components/CalculatorKeyboard.kt index b879496b..9b73756c 100644 --- a/feature/calculator/src/main/java/com/sadellie/unitto/feature/calculator/components/CalculatorKeyboard.kt +++ b/feature/calculator/src/main/java/com/sadellie/unitto/feature/calculator/components/CalculatorKeyboard.kt @@ -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)) { diff --git a/feature/converter/src/main/java/com/sadellie/unitto/feature/converter/components/ConverterKeyboard.kt b/feature/converter/src/main/java/com/sadellie/unitto/feature/converter/components/ConverterKeyboard.kt index bf43af88..ded64c51 100644 --- a/feature/converter/src/main/java/com/sadellie/unitto/feature/converter/components/ConverterKeyboard.kt +++ b/feature/converter/src/main/java/com/sadellie/unitto/feature/converter/components/ConverterKeyboard.kt @@ -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() } } } } diff --git a/feature/datecalculator/src/main/java/com/sadellie/unitto/feature/datecalculator/DateCalculatorScreen.kt b/feature/datecalculator/src/main/java/com/sadellie/unitto/feature/datecalculator/DateCalculatorScreen.kt index d9337f73..542a7517 100644 --- a/feature/datecalculator/src/main/java/com/sadellie/unitto/feature/datecalculator/DateCalculatorScreen.kt +++ b/feature/datecalculator/src/main/java/com/sadellie/unitto/feature/datecalculator/DateCalculatorScreen.kt @@ -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() } } } diff --git a/feature/datecalculator/src/main/java/com/sadellie/unitto/feature/datecalculator/addsubtract/AddSubtractPage.kt b/feature/datecalculator/src/main/java/com/sadellie/unitto/feature/datecalculator/addsubtract/AddSubtractPage.kt index a379dc11..d2e5f3e1 100644 --- a/feature/datecalculator/src/main/java/com/sadellie/unitto/feature/datecalculator/addsubtract/AddSubtractPage.kt +++ b/feature/datecalculator/src/main/java/com/sadellie/unitto/feature/datecalculator/addsubtract/AddSubtractPage.kt @@ -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 = {}, ) } diff --git a/feature/datecalculator/src/main/java/com/sadellie/unitto/feature/datecalculator/addsubtract/AddSubtractUIState.kt b/feature/datecalculator/src/main/java/com/sadellie/unitto/feature/datecalculator/addsubtract/AddSubtractUIState.kt index 6afe641c..740830b2 100644 --- a/feature/datecalculator/src/main/java/com/sadellie/unitto/feature/datecalculator/addsubtract/AddSubtractUIState.kt +++ b/feature/datecalculator/src/main/java/com/sadellie/unitto/feature/datecalculator/addsubtract/AddSubtractUIState.kt @@ -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, ) diff --git a/feature/datecalculator/src/main/java/com/sadellie/unitto/feature/datecalculator/addsubtract/AddSubtractViewModel.kt b/feature/datecalculator/src/main/java/com/sadellie/unitto/feature/datecalculator/addsubtract/AddSubtractViewModel.kt index 710be166..2245f157 100644 --- a/feature/datecalculator/src/main/java/com/sadellie/unitto/feature/datecalculator/addsubtract/AddSubtractViewModel.kt +++ b/feature/datecalculator/src/main/java/com/sadellie/unitto/feature/datecalculator/addsubtract/AddSubtractViewModel.kt @@ -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 = _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)) + } } diff --git a/feature/datecalculator/src/main/java/com/sadellie/unitto/feature/datecalculator/components/Keyboard.kt b/feature/datecalculator/src/main/java/com/sadellie/unitto/feature/datecalculator/components/Keyboard.kt new file mode 100644 index 00000000..59d11355 --- /dev/null +++ b/feature/datecalculator/src/main/java/com/sadellie/unitto/feature/datecalculator/components/Keyboard.kt @@ -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 . + */ + +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() } + } + } +} diff --git a/feature/datecalculator/src/main/java/com/sadellie/unitto/feature/datecalculator/components/TimeUnitTextField.kt b/feature/datecalculator/src/main/java/com/sadellie/unitto/feature/datecalculator/components/TimeUnitTextField.kt index b6472698..8e63858e 100644 --- a/feature/datecalculator/src/main/java/com/sadellie/unitto/feature/datecalculator/components/TimeUnitTextField.kt +++ b/feature/datecalculator/src/main/java/com/sadellie/unitto/feature/datecalculator/components/TimeUnitTextField.kt @@ -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) ) }