diff --git a/core/ui/src/main/java/app/myzel394/numberhub/core/ui/common/KeypadFlow.kt b/core/ui/src/main/java/app/myzel394/numberhub/core/ui/common/KeypadFlow.kt index 8f749381..fa28aa9e 100644 --- a/core/ui/src/main/java/app/myzel394/numberhub/core/ui/common/KeypadFlow.kt +++ b/core/ui/src/main/java/app/myzel394/numberhub/core/ui/common/KeypadFlow.kt @@ -25,7 +25,6 @@ import androidx.compose.foundation.layout.FlowRowScope import androidx.compose.foundation.layout.fillMaxHeight import androidx.compose.foundation.layout.fillMaxWidth import androidx.compose.runtime.Composable -import androidx.compose.runtime.remember import androidx.compose.ui.Modifier /** @@ -51,8 +50,8 @@ fun KeypadFlow( @IntRange(0, 100) verticalPadding: Int = 10, content: @Composable FlowRowScope.(width: Float, height: Float) -> Unit, ) { - val height: Float = remember { (1f - verticalPadding / 100f) / rows } - val width: Float = remember { (1f - horizontalPadding / 100f) / columns } + val height: Float = (1f - verticalPadding / 100f) / rows + val width: Float = (1f - horizontalPadding / 100f) / columns FlowRow( modifier = modifier, diff --git a/data/converter/src/main/java/app/myzel394/numberhub/data/converter/UnitsRepositoryImpl.kt b/data/converter/src/main/java/app/myzel394/numberhub/data/converter/UnitsRepositoryImpl.kt index 4931cb0a..37c69af6 100644 --- a/data/converter/src/main/java/app/myzel394/numberhub/data/converter/UnitsRepositoryImpl.kt +++ b/data/converter/src/main/java/app/myzel394/numberhub/data/converter/UnitsRepositoryImpl.kt @@ -102,17 +102,232 @@ class UnitsRepositoryImpl @Inject constructor( suspend fun getPairId(id: String): String = withContext(Dispatchers.IO) { val basedUnitPair = getUnitStats(id).pairedUnitId - if (basedUnitPair != null) return@withContext basedUnitPair + if (basedUnitPair != null) { + return@withContext basedUnitPair + } val inMemoryUnit = inMemory.first { it.id == id } val collection = inMemory.filter { it.group == inMemoryUnit.group } - val pair = collection - .map { getById(it.id) to getUnitStats(it.id) } - .sortedByDescending { it.second.frequency } - .firstOrNull { it.second.isFavorite }?.first ?: collection.first() + return@withContext when (inMemoryUnit.id) { + // === === === Length === === === + UnitID.nanometer -> UnitID.micrometer + UnitID.micrometer -> UnitID.millimeter + UnitID.millimeter -> UnitID.centimeter - return@withContext pair.id + UnitID.centimeter -> UnitID.inch + UnitID.inch -> UnitID.centimeter + UnitID.decimeter -> UnitID.centimeter + UnitID.foot -> UnitID.meter + UnitID.yard -> UnitID.meter + UnitID.meter -> UnitID.foot + + UnitID.kilometer -> UnitID.mile + UnitID.mile -> UnitID.kilometer + UnitID.nautical_mile -> UnitID.kilometer + + UnitID.mercury_equatorial_radius -> UnitID.kilometer + UnitID.mars_equatorial_radius -> UnitID.kilometer + UnitID.venus_equatorial_radius -> UnitID.kilometer + UnitID.earth_equatorial_radius -> UnitID.kilometer + UnitID.neptune_equatorial_radius -> UnitID.kilometer + UnitID.uranus_equatorial_radius -> UnitID.kilometer + UnitID.saturn_equatorial_radius -> UnitID.kilometer + UnitID.jupiter_equatorial_radius -> UnitID.kilometer + UnitID.sun_equatorial_radius -> UnitID.kilometer + + UnitID.light_year -> UnitID.kilometer + + UnitID.parsec -> UnitID.light_year + UnitID.kiloparsec -> UnitID.parsec + UnitID.megaparsec -> UnitID.kiloparsec + + + // === === === Mass === === === + UnitID.electron_mass_rest -> UnitID.atomic_mass_unit + UnitID.atomic_mass_unit -> UnitID.electron_mass_rest + + UnitID.microgram -> UnitID.milligram + UnitID.milligram -> UnitID.gram + UnitID.grain -> UnitID.gram + UnitID.carat -> UnitID.gram + + UnitID.gram -> UnitID.carat + UnitID.ounce -> UnitID.gram + UnitID.pound -> UnitID.kilogram + UnitID.kilogram -> UnitID.pound + + UnitID.metric_ton -> UnitID.kilogram + UnitID.imperial_ton -> UnitID.pound + + UnitID.mercury_mass -> UnitID.kilogram + UnitID.mars_mass -> UnitID.kilogram + UnitID.venus_mass -> UnitID.kilogram + UnitID.earth_mass -> UnitID.kilogram + UnitID.uranus_mass -> UnitID.kilogram + UnitID.neptune_mass -> UnitID.kilogram + UnitID.saturn_mass -> UnitID.kilogram + UnitID.jupiter_mass -> UnitID.kilogram + UnitID.sun_mass -> UnitID.kilogram + + + // === === === Speed === === === + UnitID.millimeter_per_hour -> UnitID.millimeter_per_second + UnitID.millimeter_per_second -> UnitID.millimeter_per_hour + UnitID.millimeter_per_minute -> UnitID.millimeter_per_second + UnitID.centimeter_per_hour -> UnitID.centimeter_per_second + UnitID.centimeter_per_second -> UnitID.centimeter_per_hour + UnitID.centimeter_per_minute -> UnitID.centimeter_per_second + UnitID.meter_per_hour -> UnitID.meter_per_second + UnitID.meter_per_second -> UnitID.meter_per_hour + UnitID.meter_per_minute -> UnitID.meter_per_second + + UnitID.kilometer_per_hour -> UnitID.mile_per_hour + UnitID.kilometer_per_second -> UnitID.mile_per_second + UnitID.kilometer_per_minute -> UnitID.kilometer_per_second + UnitID.mile_per_hour -> UnitID.kilometer_per_hour + UnitID.mile_per_second -> UnitID.kilometer_per_second + UnitID.mile_per_minute -> UnitID.mile_per_second + + UnitID.foot_per_hour -> UnitID.foot_per_second + UnitID.foot_per_second -> UnitID.foot_per_hour + UnitID.foot_per_minute -> UnitID.foot_per_second + UnitID.yard_per_hour -> UnitID.yard_per_second + UnitID.yard_per_second -> UnitID.yard_per_hour + UnitID.yard_per_minute -> UnitID.yard_per_second + + UnitID.knot -> UnitID.kilometer_per_hour + UnitID.mach -> UnitID.kilometer_per_hour + UnitID.velocity_of_light_in_vacuum -> UnitID.kilometer_per_hour + UnitID.earths_orbital_speed -> UnitID.kilometer_per_hour + + UnitID.cosmic_velocity_first -> UnitID.kilometer_per_hour + UnitID.cosmic_velocity_second -> UnitID.kilometer_per_hour + UnitID.cosmic_velocity_third -> UnitID.kilometer_per_hour + + + // === === === Temperature === === === + UnitID.celsius -> UnitID.fahrenheit + UnitID.fahrenheit -> UnitID.celsius + UnitID.kelvin -> UnitID.celsius + + + // === === === Area === === === + UnitID.square_micrometer -> UnitID.square_millimeter + UnitID.square_millimeter -> UnitID.square_centimeter + UnitID.square_centimeter -> UnitID.square_meter + UnitID.square_decimeter -> UnitID.square_meter + UnitID.square_meter -> UnitID.square_kilometer + + UnitID.square_kilometer -> UnitID.square_meter + + UnitID.square_inch -> UnitID.square_foot + UnitID.square_foot -> UnitID.square_inch + UnitID.square_yard -> UnitID.square_meter + UnitID.square_mile -> UnitID.square_kilometer + + UnitID.acre -> UnitID.square_meter + UnitID.hectare -> UnitID.square_meter + UnitID.cent -> UnitID.square_meter + + + // === === === Time === === === + UnitID.attosecond -> UnitID.nanosecond + UnitID.nanosecond -> UnitID.microsecond + UnitID.microsecond -> UnitID.millisecond + UnitID.millisecond -> UnitID.second + + UnitID.jiffy -> UnitID.millisecond + + UnitID.second -> UnitID.millisecond + UnitID.minute -> UnitID.second + UnitID.hour -> UnitID.minute + UnitID.day -> UnitID.hour + UnitID.week -> UnitID.day + + + // === === === Data === === === + // TODO: Add tibibyte, exibyte + UnitID.bit -> UnitID.byte + UnitID.byte -> UnitID.kilobyte + UnitID.kilobyte -> UnitID.megabyte + UnitID.megabyte -> UnitID.gigabyte + UnitID.gigabyte -> UnitID.terabyte + UnitID.terabyte -> UnitID.petabyte + UnitID.petabyte -> UnitID.exabyte + + UnitID.kilobit -> UnitID.kilobyte + UnitID.megabit -> UnitID.megabyte + UnitID.gigabit -> UnitID.gigabyte + UnitID.terabit -> UnitID.terabyte + UnitID.petabit -> UnitID.petabyte + UnitID.exabit -> UnitID.exabyte + + UnitID.kibibit -> UnitID.kilobyte + UnitID.mebibit -> UnitID.megabyte + UnitID.gibibit -> UnitID.gigabyte + + UnitID.kibibyte -> UnitID.kilobyte + UnitID.mebibyte -> UnitID.megabyte + UnitID.gibibyte -> UnitID.gigabyte + + + // === === === Acceleration === === === + UnitID.millimeter_per_square_second -> UnitID.centimeter_per_square_second + UnitID.centimeter_per_square_second -> UnitID.meter_per_square_second + UnitID.decimeter_per_square_second -> UnitID.meter_per_square_second + UnitID.meter_per_square_second -> UnitID.kilometer_per_square_second + + UnitID.mercury_surface_gravity -> UnitID.meter_per_square_second + UnitID.mars_surface_gravity -> UnitID.meter_per_square_second + UnitID.venus_surface_gravity -> UnitID.meter_per_square_second + UnitID.uranus_surface_gravity -> UnitID.meter_per_square_second + UnitID.earth_surface_gravity -> UnitID.meter_per_square_second + UnitID.saturn_surface_gravity -> UnitID.meter_per_square_second + UnitID.neptune_surface_gravity -> UnitID.meter_per_square_second + UnitID.jupiter_surface_gravity -> UnitID.meter_per_square_second + UnitID.sun_surface_gravity -> UnitID.meter_per_square_second + + + // === === === Power === === === + UnitID.attowatt -> UnitID.watt + UnitID.watt -> UnitID.kilowatt + UnitID.kilowatt -> UnitID.watt + UnitID.megawatt -> UnitID.kilowatt + + + // === === === Angle === === === + UnitID.degree -> UnitID.radian + UnitID.radian -> UnitID.degree + + + // === === === Data Transfer === === === + UnitID.bit_per_second -> UnitID.byte_per_second + UnitID.kilobit_per_second -> UnitID.kilobyte_per_second + UnitID.megabit_per_second -> UnitID.megabyte_per_second + UnitID.gigabit_per_second -> UnitID.gigabyte_per_second + UnitID.terabit_per_second -> UnitID.terabyte_per_second + UnitID.petabit_per_second -> UnitID.petabyte_per_second + UnitID.exabit_per_second -> UnitID.exabyte_per_second + + UnitID.byte_per_second -> UnitID.kilobyte_per_second + UnitID.kilobyte_per_second -> UnitID.megabyte_per_second + UnitID.megabyte_per_second -> UnitID.gigabyte_per_second + UnitID.gigabyte_per_second -> UnitID.megabyte_per_second + UnitID.terabyte_per_second -> UnitID.gigabyte_per_second + UnitID.petabyte_per_second -> UnitID.terabyte_per_second + + + // === === === Fuel === === === + UnitID.kilometer_per_liter -> UnitID.mile_us_per_liter + UnitID.mile_us_per_liter -> UnitID.kilometer_per_liter + + else -> + (collection + .map { getById(it.id) to getUnitStats(it.id) } + .sortedByDescending { it.second.frequency } + .firstOrNull { it.second.isFavorite }?.first ?: collection.first()).id + } } suspend fun incrementCounter(id: String) = withContext(Dispatchers.IO) { diff --git a/data/model/src/main/java/app/myzel394/numberhub/data/model/converter/unit/BasicUnit.kt b/data/model/src/main/java/app/myzel394/numberhub/data/model/converter/unit/BasicUnit.kt index 1b697d5f..14298017 100644 --- a/data/model/src/main/java/app/myzel394/numberhub/data/model/converter/unit/BasicUnit.kt +++ b/data/model/src/main/java/app/myzel394/numberhub/data/model/converter/unit/BasicUnit.kt @@ -37,6 +37,16 @@ sealed interface BasicUnit { interface NumberBase : BasicUnit { fun convert(unitTo: NumberBase, value: String): String + + companion object { + val Hexadecimal = NumberBaseUnit( + "hexadecimal", + BigDecimal(16), + UnitGroup.NUMBER_BASE, + 0, + 0, + ) + } } interface Default : BasicUnit { diff --git a/data/userprefs/src/main/java/app/myzel394/numberhub/data/userprefs/PreferenceExt.kt b/data/userprefs/src/main/java/app/myzel394/numberhub/data/userprefs/PreferenceExt.kt index 8fc3e046..f353ddb9 100644 --- a/data/userprefs/src/main/java/app/myzel394/numberhub/data/userprefs/PreferenceExt.kt +++ b/data/userprefs/src/main/java/app/myzel394/numberhub/data/userprefs/PreferenceExt.kt @@ -120,7 +120,7 @@ internal fun Preferences.getUnitConverterFormatTime(): Boolean { internal fun Preferences.getUnitConverterSorting(): UnitsListSorting { return this[PrefsKeys.UNIT_CONVERTER_SORTING] - ?.let { UnitsListSorting.valueOf(it) } ?: UnitsListSorting.USAGE + ?.let { UnitsListSorting.valueOf(it) } ?: UnitsListSorting.SCALE_ASC } internal fun Preferences.getShownUnitGroups(): List { diff --git a/feature/converter/src/main/java/app/myzel394/numberhub/feature/converter/ConverterScreen.kt b/feature/converter/src/main/java/app/myzel394/numberhub/feature/converter/ConverterScreen.kt index c6be06a3..499a7cd3 100644 --- a/feature/converter/src/main/java/app/myzel394/numberhub/feature/converter/ConverterScreen.kt +++ b/feature/converter/src/main/java/app/myzel394/numberhub/feature/converter/ConverterScreen.kt @@ -22,6 +22,7 @@ import androidx.compose.animation.AnimatedContent import androidx.compose.animation.AnimatedVisibility import androidx.compose.animation.SizeTransform import androidx.compose.animation.core.FastOutSlowInEasing +import androidx.compose.animation.core.LinearEasing import androidx.compose.animation.core.animateFloatAsState import androidx.compose.animation.core.tween import androidx.compose.animation.expandHorizontally @@ -31,6 +32,11 @@ import androidx.compose.animation.fadeOut import androidx.compose.animation.shrinkVertically import androidx.compose.animation.togetherWith import androidx.compose.foundation.background +import androidx.compose.foundation.gestures.AnchoredDraggableState +import androidx.compose.foundation.gestures.DraggableAnchors +import androidx.compose.foundation.gestures.Orientation +import androidx.compose.foundation.gestures.anchoredDraggable +import androidx.compose.foundation.horizontalScroll import androidx.compose.foundation.layout.Arrangement import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.PaddingValues @@ -40,6 +46,7 @@ import androidx.compose.foundation.layout.fillMaxSize import androidx.compose.foundation.layout.fillMaxWidth import androidx.compose.foundation.layout.height import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.rememberScrollState import androidx.compose.foundation.shape.RoundedCornerShape import androidx.compose.material.icons.Icons import androidx.compose.material.icons.outlined.SwapHoriz @@ -64,6 +71,7 @@ import androidx.compose.ui.draw.rotate import androidx.compose.ui.focus.onFocusEvent import androidx.compose.ui.graphics.Color import androidx.compose.ui.platform.LocalContext +import androidx.compose.ui.platform.LocalDensity import androidx.compose.ui.res.stringResource import androidx.compose.ui.text.input.TextFieldValue import androidx.compose.ui.text.style.TextAlign @@ -94,11 +102,14 @@ import app.myzel394.numberhub.data.common.isExpression import app.myzel394.numberhub.data.converter.ConverterResult import app.myzel394.numberhub.data.converter.UnitID import app.myzel394.numberhub.data.model.converter.UnitGroup +import app.myzel394.numberhub.feature.converter.components.BaseCalculationSummary import app.myzel394.numberhub.feature.converter.components.DefaultKeyboard import app.myzel394.numberhub.feature.converter.components.NumberBaseKeyboard import app.myzel394.numberhub.feature.converter.components.UnitSelectionButton +import app.myzel394.numberhub.feature.converter.components.ValueOneSummary import java.math.BigDecimal import java.util.Locale +import kotlin.math.absoluteValue @Composable internal fun ConverterRoute( @@ -203,10 +214,30 @@ private fun NumberBase( convert() } + val density = LocalDensity.current + val dragState = remember { + AnchoredDraggableState( + initialValue = DragState.CLOSED, + anchors = DraggableAnchors { + DragState.CLOSED at 0f + DragState.OPEN at with(density) { -60.dp.toPx() } + }, + positionalThreshold = { 0f }, + velocityThreshold = { 0f }, + animationSpec = tween(easing = LinearEasing, durationMillis = 50), + ) + } + PortraitLandscape( modifier = modifier.fillMaxSize(), content1 = { contentModifier -> - ColumnWithConstraints(modifier = contentModifier) { + ColumnWithConstraints( + modifier = contentModifier + .anchoredDraggable( + state = dragState, + orientation = Orientation.Vertical, + ), + ) { val textFieldModifier = Modifier.weight(2f) NumberBaseTextField( @@ -224,6 +255,22 @@ private fun NumberBase( ) AnimatedUnitShortName(stringResource(uiState.unitTo.shortName)) + if (uiState.result is ConverterResult.NumberBase) { + BaseCalculationSummary( + modifier = Modifier + .fillMaxWidth() + .horizontalScroll(rememberScrollState()) + .then(with(density) { Modifier.height(dragState.offset.absoluteValue.toDp()) }), + basis = uiState.unitTo, + result = uiState.result, + onResultChange = { newValue -> + val valueConverted = uiState.unitTo.convert(uiState.unitFrom, newValue) + + updateInput1(TextFieldValue(valueConverted)) + }, + ) + } + Spacer(modifier = Modifier.height(it.maxHeight * 0.03f)) UnitSelectionButtons( @@ -250,6 +297,7 @@ private fun NumberBase( addDigit = { updateInput1(uiState.input.addTokens(it)) }, deleteDigit = { updateInput1(uiState.input.deleteTokens()) }, clearInput = { updateInput1(TextFieldValue()) }, + basis = uiState.unitFrom, ) }, ) @@ -284,6 +332,20 @@ private fun Default( } var focusedOnInput1 by rememberSaveable { mutableStateOf(true) } + val density = LocalDensity.current + val dragState = remember { + AnchoredDraggableState( + initialValue = DragState.CLOSED, + anchors = DraggableAnchors { + DragState.CLOSED at 0f + DragState.OPEN at with(density) { -60.dp.toPx() } + }, + positionalThreshold = { 0f }, + velocityThreshold = { 0f }, + animationSpec = tween(easing = LinearEasing, durationMillis = 50), + ) + } + LaunchedEffect(connection) { if ((connection == ConnectionState.Available) and (uiState.result is ConverterResult.Error)) { val unitFrom = uiState.unitFrom @@ -304,7 +366,13 @@ private fun Default( PortraitLandscape( modifier = modifier.fillMaxSize(), content1 = { contentModifier -> - ColumnWithConstraints(modifier = contentModifier) { boxWithConstraintsScope -> + ColumnWithConstraints( + modifier = contentModifier + .anchoredDraggable( + state = dragState, + orientation = Orientation.Vertical, + ), + ) { boxWithConstraintsScope -> val textFieldModifier = Modifier .fillMaxWidth() .weight(2f) @@ -425,6 +493,18 @@ private fun Default( ), ) + if (uiState.result is ConverterResult.Default && uiState.unitTo.factor >= BigDecimal.ZERO) { + ValueOneSummary( + modifier = with(density) { + Modifier + .fillMaxWidth() + .height(dragState.offset.absoluteValue.toDp()) + .horizontalScroll(rememberScrollState()) + }, + uiState = uiState, + ) + } + Spacer(modifier = Modifier.height(boxWithConstraintsScope.maxHeight * 0.03f)) UnitSelectionButtons( diff --git a/feature/converter/src/main/java/app/myzel394/numberhub/feature/converter/DragState.kt b/feature/converter/src/main/java/app/myzel394/numberhub/feature/converter/DragState.kt new file mode 100644 index 00000000..50e2cb4c --- /dev/null +++ b/feature/converter/src/main/java/app/myzel394/numberhub/feature/converter/DragState.kt @@ -0,0 +1,3 @@ +package app.myzel394.numberhub.feature.converter + +internal enum class DragState { CLOSED, OPEN } diff --git a/feature/converter/src/main/java/app/myzel394/numberhub/feature/converter/UnitFromSelectorScreen.kt b/feature/converter/src/main/java/app/myzel394/numberhub/feature/converter/UnitFromSelectorScreen.kt index 90be2e13..e2990f6d 100644 --- a/feature/converter/src/main/java/app/myzel394/numberhub/feature/converter/UnitFromSelectorScreen.kt +++ b/feature/converter/src/main/java/app/myzel394/numberhub/feature/converter/UnitFromSelectorScreen.kt @@ -156,7 +156,7 @@ private fun UnitFromSelectorScreenPreview() { selectedUnitGroup = UnitGroup.SPEED, shownUnitGroups = UnitGroup.entries, showFavoritesOnly = false, - sorting = UnitsListSorting.USAGE, + sorting = UnitsListSorting.SCALE_ASC, ), onQueryChange = {}, toggleFavoritesOnly = {}, diff --git a/feature/converter/src/main/java/app/myzel394/numberhub/feature/converter/UnitToSelectorScreen.kt b/feature/converter/src/main/java/app/myzel394/numberhub/feature/converter/UnitToSelectorScreen.kt index 1acfcba4..8ed18192 100644 --- a/feature/converter/src/main/java/app/myzel394/numberhub/feature/converter/UnitToSelectorScreen.kt +++ b/feature/converter/src/main/java/app/myzel394/numberhub/feature/converter/UnitToSelectorScreen.kt @@ -155,7 +155,7 @@ private fun UnitToSelectorPreview() { query = TextFieldValue("test"), units = units, showFavoritesOnly = false, - sorting = UnitsListSorting.USAGE, + sorting = UnitsListSorting.SCALE_ASC, input = "100", scale = 3, outputFormat = OutputFormat.PLAIN, diff --git a/feature/converter/src/main/java/app/myzel394/numberhub/feature/converter/components/BaseCalculationSummary.kt b/feature/converter/src/main/java/app/myzel394/numberhub/feature/converter/components/BaseCalculationSummary.kt new file mode 100644 index 00000000..e4fa3632 --- /dev/null +++ b/feature/converter/src/main/java/app/myzel394/numberhub/feature/converter/components/BaseCalculationSummary.kt @@ -0,0 +1,86 @@ +package app.myzel394.numberhub.feature.converter.components + +import androidx.compose.foundation.clickable +import androidx.compose.foundation.layout.Arrangement +import androidx.compose.foundation.layout.Row +import androidx.compose.foundation.layout.offset +import androidx.compose.foundation.layout.padding +import androidx.compose.material3.MaterialTheme +import androidx.compose.material3.Text +import androidx.compose.runtime.Composable +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.draw.clip +import androidx.compose.ui.unit.dp +import app.myzel394.numberhub.data.converter.ConverterResult +import app.myzel394.numberhub.data.model.converter.unit.BasicUnit + +@Composable +internal fun BaseCalculationSummary( + modifier: Modifier = Modifier, + basis: BasicUnit.NumberBase, + result: ConverterResult.NumberBase, + onResultChange: (String) -> Unit, +) { + val fontStyle = MaterialTheme.typography.headlineSmall + + Row( + modifier, + horizontalArrangement = Arrangement.End, + verticalAlignment = Alignment.CenterVertically, + ) { + for (index in 0.. - val bModifier = Modifier.fillMaxWidth(width).fillMaxHeight(height) + val bModifier = Modifier + .fillMaxWidth(width) + .fillMaxHeight(height) if (acButton) { - KeyboardButtonTertiary(bModifier, IconPack.Clear, stringResource(R.string.delete_label), contentHeight) { clearInput() } - KeyboardButtonFilled(bModifier, IconPack.Brackets, stringResource(R.string.keyboard_brackets), contentHeight) { addBracket() } + KeyboardButtonTertiary( + bModifier, + IconPack.Clear, + stringResource(R.string.delete_label), + contentHeight, + ) { clearInput() } + KeyboardButtonFilled( + bModifier, + IconPack.Brackets, + stringResource(R.string.keyboard_brackets), + contentHeight, + ) { addBracket() } } else { - KeyboardButtonFilled(bModifier, IconPack.LeftBracket, stringResource(R.string.keyboard_left_bracket), contentHeight) { addDigit(Token.Operator.leftBracket) } - KeyboardButtonFilled(bModifier, IconPack.RightBracket, stringResource(R.string.keyboard_right_bracket), contentHeight) { addDigit(Token.Operator.rightBracket) } + KeyboardButtonFilled( + bModifier, + IconPack.LeftBracket, + stringResource(R.string.keyboard_left_bracket), + contentHeight, + ) { addDigit(Token.Operator.leftBracket) } + KeyboardButtonFilled( + bModifier, + IconPack.RightBracket, + stringResource(R.string.keyboard_right_bracket), + contentHeight, + ) { addDigit(Token.Operator.rightBracket) } } - KeyboardButtonFilled(bModifier, IconPack.Power, stringResource(R.string.keyboard_power), contentHeight) { addDigit(Token.Operator.power) } - KeyboardButtonFilled(bModifier, IconPack.Root, stringResource(R.string.keyboard_root), contentHeight) { addDigit(Token.Operator.sqrt) } + KeyboardButtonFilled( + bModifier, + IconPack.Power, + stringResource(R.string.keyboard_power), + contentHeight, + ) { addDigit(Token.Operator.power) } + KeyboardButtonFilled( + bModifier, + IconPack.Root, + stringResource(R.string.keyboard_root), + contentHeight, + ) { addDigit(Token.Operator.sqrt) } - KeyboardButtonLight(bModifier, IconPack.Key7, Token.Digit._7, contentHeight) { addDigit(Token.Digit._7) } - KeyboardButtonLight(bModifier, IconPack.Key8, Token.Digit._8, contentHeight) { addDigit(Token.Digit._8) } - KeyboardButtonLight(bModifier, IconPack.Key9, Token.Digit._9, contentHeight) { addDigit(Token.Digit._9) } - KeyboardButtonFilled(bModifier, IconPack.Divide, stringResource(R.string.keyboard_divide), contentHeight) { addDigit(Token.Operator.divide) } + KeyboardButtonLight( + bModifier, + IconPack.Key7, + Token.Digit._7, + contentHeight, + ) { addDigit(Token.Digit._7) } + KeyboardButtonLight( + bModifier, + IconPack.Key8, + Token.Digit._8, + contentHeight, + ) { addDigit(Token.Digit._8) } + KeyboardButtonLight( + bModifier, + IconPack.Key9, + Token.Digit._9, + contentHeight, + ) { addDigit(Token.Digit._9) } + KeyboardButtonFilled( + bModifier, + IconPack.Divide, + stringResource(R.string.keyboard_divide), + contentHeight, + ) { addDigit(Token.Operator.divide) } - KeyboardButtonLight(bModifier, IconPack.Key4, Token.Digit._4, contentHeight) { addDigit(Token.Digit._4) } - KeyboardButtonLight(bModifier, IconPack.Key5, Token.Digit._5, contentHeight) { addDigit(Token.Digit._5) } - KeyboardButtonLight(bModifier, IconPack.Key6, Token.Digit._6, contentHeight) { addDigit(Token.Digit._6) } - KeyboardButtonFilled(bModifier, IconPack.Multiply, stringResource(R.string.keyboard_multiply), contentHeight) { addDigit(Token.Operator.multiply) } + KeyboardButtonLight( + bModifier, + IconPack.Key4, + Token.Digit._4, + contentHeight, + ) { addDigit(Token.Digit._4) } + KeyboardButtonLight( + bModifier, + IconPack.Key5, + Token.Digit._5, + contentHeight, + ) { addDigit(Token.Digit._5) } + KeyboardButtonLight( + bModifier, + IconPack.Key6, + Token.Digit._6, + contentHeight, + ) { addDigit(Token.Digit._6) } + KeyboardButtonFilled( + bModifier, + IconPack.Multiply, + stringResource(R.string.keyboard_multiply), + contentHeight, + ) { addDigit(Token.Operator.multiply) } - KeyboardButtonLight(bModifier, IconPack.Key1, Token.Digit._1, contentHeight) { addDigit(Token.Digit._1) } - KeyboardButtonLight(bModifier, IconPack.Key2, Token.Digit._2, contentHeight) { addDigit(Token.Digit._2) } - KeyboardButtonLight(bModifier, IconPack.Key3, Token.Digit._3, contentHeight) { addDigit(Token.Digit._3) } - KeyboardButtonFilled(bModifier, IconPack.Minus, stringResource(R.string.keyboard_minus), contentHeight) { addDigit(Token.Operator.minus) } + KeyboardButtonLight( + bModifier, + IconPack.Key1, + Token.Digit._1, + contentHeight, + ) { addDigit(Token.Digit._1) } + KeyboardButtonLight( + bModifier, + IconPack.Key2, + Token.Digit._2, + contentHeight, + ) { addDigit(Token.Digit._2) } + KeyboardButtonLight( + bModifier, + IconPack.Key3, + Token.Digit._3, + contentHeight, + ) { addDigit(Token.Digit._3) } + KeyboardButtonFilled( + bModifier, + IconPack.Minus, + stringResource(R.string.keyboard_minus), + contentHeight, + ) { addDigit(Token.Operator.minus) } if (middleZero) { - KeyboardButtonLight(bModifier, fractionalIcon, stringResource(fractionalIconDescription), contentHeight) { addDigit(Token.Digit.dot) } - KeyboardButtonLight(bModifier, IconPack.Key0, Token.Digit._0, contentHeight) { addDigit(Token.Digit._0) } + KeyboardButtonLight( + bModifier, + fractionalIcon, + stringResource(fractionalIconDescription), + contentHeight, + ) { addDigit(Token.Digit.dot) } + KeyboardButtonLight( + bModifier, + IconPack.Key0, + Token.Digit._0, + contentHeight, + ) { addDigit(Token.Digit._0) } } else { - KeyboardButtonLight(bModifier, IconPack.Key0, Token.Digit._0, contentHeight) { addDigit(Token.Digit._0) } - KeyboardButtonLight(bModifier, fractionalIcon, stringResource(fractionalIconDescription), contentHeight) { addDigit(Token.Digit.dot) } + KeyboardButtonLight( + bModifier, + IconPack.Key0, + Token.Digit._0, + contentHeight, + ) { addDigit(Token.Digit._0) } + KeyboardButtonLight( + bModifier, + fractionalIcon, + stringResource(fractionalIconDescription), + contentHeight, + ) { addDigit(Token.Digit.dot) } } - KeyboardButtonLight(bModifier, IconPack.Backspace, stringResource(R.string.delete_label), contentHeight, onLongClick = clearInput) { deleteDigit() } - KeyboardButtonFilled(bModifier, IconPack.Plus, stringResource(R.string.keyboard_plus), contentHeight) { addDigit(Token.Operator.plus) } + KeyboardButtonLight( + bModifier, + IconPack.Backspace, + stringResource(R.string.delete_label), + contentHeight, + onLongClick = clearInput, + ) { deleteDigit() } + KeyboardButtonFilled( + bModifier, + IconPack.Plus, + stringResource(R.string.keyboard_plus), + contentHeight, + ) { addDigit(Token.Operator.plus) } } } +val AVAILABLE_NUMBERS = mapOf( + Token.Digit._0 to IconPack.Key0, + Token.Digit._1 to IconPack.Key1, + Token.Digit._2 to IconPack.Key2, + Token.Digit._3 to IconPack.Key3, + Token.Digit._4 to IconPack.Key4, + Token.Digit._5 to IconPack.Key5, + Token.Digit._6 to IconPack.Key6, + Token.Digit._7 to IconPack.Key7, + Token.Digit._8 to IconPack.Key8, + Token.Digit._9 to IconPack.Key9, + Token.Letter._A to IconPack.KeyA, + Token.Letter._B to IconPack.KeyB, + Token.Letter._C to IconPack.KeyC, + Token.Letter._D to IconPack.KeyD, + Token.Letter._E to IconPack.KeyE, + Token.Letter._F to IconPack.KeyF, +) + @Composable internal fun NumberBaseKeyboard( modifier: Modifier, addDigit: (String) -> Unit, clearInput: () -> Unit, deleteDigit: () -> Unit, + basis: BasicUnit.NumberBase, ) { val contentHeight: Float = if (LocalWindowSize.current.heightSizeClass < WindowHeightSizeClass.Medium) KeyboardButtonToken.CONTENT_HEIGHT_SHORT else KeyboardButtonToken.CONTENT_HEIGHT_TALL + val isUsingColumn = + (LocalWindowSize.current.widthSizeClass > WindowWidthSizeClass.Expanded) or (LocalWindowSize.current.heightSizeClass > WindowHeightSizeClass.Compact) - KeypadFlow( - modifier = modifier, - rows = 6, - columns = 3, - ) { width, height -> - val bModifier = Modifier.fillMaxWidth(width).fillMaxHeight(height) - val wideButtonModifier = Modifier.fillMaxHeight(height).fillMaxWidth(width * 2) + var direction by remember { + mutableStateOf(AnimatedContentTransitionScope.SlideDirection.Right) + } - KeyboardButtonFilled(bModifier, IconPack.KeyA, Token.Letter._A, contentHeight) { addDigit(Token.Letter._A) } - KeyboardButtonFilled(bModifier, IconPack.KeyB, Token.Letter._B, contentHeight) { addDigit(Token.Letter._B) } - KeyboardButtonFilled(bModifier, IconPack.KeyC, Token.Letter._C, contentHeight) { addDigit(Token.Letter._C) } + LaunchedEffect(isUsingColumn) { + direction = if (isUsingColumn) { + AnimatedContentTransitionScope.SlideDirection.Right + } else { + AnimatedContentTransitionScope.SlideDirection.Up + } + } - KeyboardButtonFilled(bModifier, IconPack.KeyD, Token.Letter._D, contentHeight) { addDigit(Token.Letter._D) } - KeyboardButtonFilled(bModifier, IconPack.KeyE, Token.Letter._E, contentHeight) { addDigit(Token.Letter._E) } - KeyboardButtonFilled(bModifier, IconPack.KeyF, Token.Letter._F, contentHeight) { addDigit(Token.Letter._F) } + LaunchedEffect(basis) { + direction = when (direction) { + AnimatedContentTransitionScope.SlideDirection.Up -> AnimatedContentTransitionScope.SlideDirection.Down + AnimatedContentTransitionScope.SlideDirection.Down -> AnimatedContentTransitionScope.SlideDirection.Up + AnimatedContentTransitionScope.SlideDirection.Left -> AnimatedContentTransitionScope.SlideDirection.Right + AnimatedContentTransitionScope.SlideDirection.Right -> AnimatedContentTransitionScope.SlideDirection.Left + else -> direction + } + } - KeyboardButtonLight(bModifier, IconPack.Key7, Token.Digit._7, contentHeight) { addDigit(Token.Digit._7) } - KeyboardButtonLight(bModifier, IconPack.Key8, Token.Digit._8, contentHeight) { addDigit(Token.Digit._8) } - KeyboardButtonLight(bModifier, IconPack.Key9, Token.Digit._9, contentHeight) { addDigit(Token.Digit._9) } + ColumnWithConstraints(modifier) { constraints -> + AnimatedContent( + transitionSpec = { + slideIntoContainer(animationSpec = tween(600), towards = direction).togetherWith( + slideOutOfContainer(animationSpec = tween(600), towards = direction), + ) + }, + targetState = basis.factor.toInt(), + label = "ConverterKeyboard-FlowRow", + ) { amount -> + val columns: Int = when { + amount == 5 -> 2 + amount == 3 -> 2 + amount == 7 -> 2 + amount == 10 -> 3 + amount < 10 -> if (amount.and(1) == 0) 2 else if (amount % 3 == 0) 3 else amount % 3 + else -> 3 + } + val rows = when { + amount == 5 -> 3 + amount == 3 -> 2 + amount == 7 -> 4 + amount == 10 -> 4 + amount < 10 -> ceil(amount.toDouble() / columns.toDouble()).toInt() + 1 + amount == 11 -> 5 + amount == 12 -> 5 + amount == 13 -> 5 + else -> 6 + } + val horizontalSpacing = 8.dp + val verticalSpacing = 12.dp + val height: Float = (1f - (verticalSpacing * (rows - 1) / constraints.maxHeight)) / rows - KeyboardButtonLight(bModifier, IconPack.Key4, Token.Digit._4, contentHeight) { addDigit(Token.Digit._4) } - KeyboardButtonLight(bModifier, IconPack.Key5, Token.Digit._5, contentHeight) { addDigit(Token.Digit._5) } - KeyboardButtonLight(bModifier, IconPack.Key6, Token.Digit._6, contentHeight) { addDigit(Token.Digit._6) } + FlowRow( + modifier = modifier, + maxItemsInEachRow = columns, + horizontalArrangement = Arrangement.spacedBy(horizontalSpacing), + verticalArrangement = Arrangement.spacedBy(verticalSpacing), + ) { + val bModifier = Modifier + .fillMaxHeight(height) + .fillMaxWidth() - KeyboardButtonLight(bModifier, IconPack.Key1, Token.Digit._1, contentHeight) { addDigit(Token.Digit._1) } - KeyboardButtonLight(bModifier, IconPack.Key2, Token.Digit._2, contentHeight) { addDigit(Token.Digit._2) } - KeyboardButtonLight(bModifier, IconPack.Key3, Token.Digit._3, contentHeight) { addDigit(Token.Digit._3) } + when { + amount in arrayOf(3, 5, 7) -> { + for (int in createSortedArray(1.. { + for (int in createSortedArray(0.. { + for (int in createSortedArray(1..9, columns)) { + val key = AVAILABLE_NUMBERS.keys.elementAt(int) + val icon = AVAILABLE_NUMBERS[key]!! + KeyboardButtonLight( + bModifier.weight(1f), + icon, + key, + contentHeight, + ) { addDigit(key) } + } + + KeyboardButtonLight( + bModifier.weight(1f), + IconPack.Key0, + Token.Digit._0, + contentHeight, + ) { addDigit(Token.Digit._0) } + KeyboardButtonTertiary( + bModifier.weight(2f), + IconPack.Backspace, + stringResource(R.string.delete_label), + contentHeight, + clearInput, + ) { deleteDigit() } + } + + else -> { + val rowsAmount = ceil((amount - 10) / 3f).toInt() + + for (row in (rowsAmount - 1) downTo 0) { + Row( + Modifier.fillMaxWidth(), + horizontalArrangement = Arrangement.spacedBy(horizontalSpacing), + ) { + val rowStart = 10 + (row) * columns + val rowEnd = (rowStart + 3).coerceAtMost(amount.coerceAtMost(16)) + + for (index in createSortedArray(rowStart.. { + val result = mutableListOf() + val rows = ceil(range.count().toDouble() / columns.toDouble()).toInt() + for (row in rows downTo 0) { + for (column in 0 until columns) { + val index = row * columns + column + range.first + if (index <= range.last) { + result.add(index) + } + } + } + + return result +} diff --git a/feature/settings/src/main/java/app/myzel394/numberhub/feature/settings/converter/ConverterSettingsScreen.kt b/feature/settings/src/main/java/app/myzel394/numberhub/feature/settings/converter/ConverterSettingsScreen.kt index 2b82e9d6..38085237 100644 --- a/feature/settings/src/main/java/app/myzel394/numberhub/feature/settings/converter/ConverterSettingsScreen.kt +++ b/feature/settings/src/main/java/app/myzel394/numberhub/feature/settings/converter/ConverterSettingsScreen.kt @@ -139,7 +139,7 @@ private fun PreviewConverterSettingsScreen() { precision = 3, outputFormat = OutputFormat.PLAIN, unitConverterFormatTime = false, - unitConverterSorting = UnitsListSorting.USAGE, + unitConverterSorting = UnitsListSorting.SCALE_ASC, shownUnitGroups = UnitGroup.entries, unitConverterFavoritesOnly = false, enableToolsExperiment = false, diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index 42a81fa5..9c5cc044 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -5,25 +5,25 @@ versionName = "NumberHub" androidxBrowserBrowser = "1.8.0" androidGradlePlugin = "8.3.2" androidxActivityActivityCompose = "1.9.0" -androidxAppCompatAppCompat = "1.6.1" -androidxCompose = "1.6.7" +androidxAppCompatAppCompat = "1.7.0" +androidxCompose = "1.6.8" androidxComposeCompiler = "1.5.9" androidxComposeMaterial3 = "1.2.1" androidxCoreCoreKts = "1.13.1" -androidxGlanceGlance = "1.0.0" +androidxGlanceGlance = "1.1.0" androidxDatastoreDatastorePreferences = "1.1.1" -androidxEspresso = "3.5.1" +androidxEspresso = "3.6.1" androidxHiltHiltNavigationCompose = "1.2.0" androidxMacroBenchmark = "1.2.4" -androidxLifecycleLifecycleRuntimeCompose = "2.7.0" +androidxLifecycleLifecycleRuntimeCompose = "2.8.3" androidxNavigationNavigationCompose = "2.7.7" androidxProfileinstallerProfileinstaller = "1.3.1" androidxRoom = "2.6.1" -androidxTest = "1.5.0" -androidxTestExtJunitKtx = "1.1.5" -androidxTestRunner = "1.5.2" +androidxTest = "1.6.1" +androidxTestExtJunitKtx = "1.2.1" +androidxTestRunner = "1.6.1" androidxUiAutomator = "2.3.0" -androidxWindowWindow = "1.2.0" +androidxWindowWindow = "1.3.0" androidxWorkWorkRuntimeKtx = "2.9.0" comAndroidToolsDesugarJdkLibs = "2.0.4" comGithubSadellieThemmo = "1.3.0"