diff --git a/androidApp/src/screenshotTestDebug/reference/org/neotech/app/abysner/presentation/screens/SettingsScreenScreenshotTestKt/SettingsScreenScreenshotTest_58e6d892_0.png b/androidApp/src/screenshotTestDebug/reference/org/neotech/app/abysner/presentation/screens/SettingsScreenScreenshotTestKt/SettingsScreenScreenshotTest_58e6d892_0.png index 65431e5..08c5883 100644 --- a/androidApp/src/screenshotTestDebug/reference/org/neotech/app/abysner/presentation/screens/SettingsScreenScreenshotTestKt/SettingsScreenScreenshotTest_58e6d892_0.png +++ b/androidApp/src/screenshotTestDebug/reference/org/neotech/app/abysner/presentation/screens/SettingsScreenScreenshotTestKt/SettingsScreenScreenshotTest_58e6d892_0.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:4df88adf762077040cd065170400aae5eeb3c7099e109df0d4ee70345db0ba40 -size 34036 +oid sha256:59c3ad864d52bb662fb3b4c9a930542c8b867a4266156d05e81e4cb501da131d +size 44824 diff --git a/androidApp/src/screenshotTestDebug/reference/org/neotech/app/abysner/presentation/screens/ShareImageScreenshotTestKt/ShareImageCcrBailoutScreenshotTest_0.png b/androidApp/src/screenshotTestDebug/reference/org/neotech/app/abysner/presentation/screens/ShareImageScreenshotTestKt/ShareImageCcrBailoutScreenshotTest_0.png index 720c34e..22b0617 100644 --- a/androidApp/src/screenshotTestDebug/reference/org/neotech/app/abysner/presentation/screens/ShareImageScreenshotTestKt/ShareImageCcrBailoutScreenshotTest_0.png +++ b/androidApp/src/screenshotTestDebug/reference/org/neotech/app/abysner/presentation/screens/ShareImageScreenshotTestKt/ShareImageCcrBailoutScreenshotTest_0.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:0d59eb8a123a4e4964e8692db4c10bc5122e5a407b472dd0d33a6b6a2e4bfba1 -size 168419 +oid sha256:690f97b3419edd8e7a16bf59dba0c916e20250628a2f00549594137309b29e39 +size 168421 diff --git a/androidApp/src/screenshotTestDebug/reference/org/neotech/app/abysner/presentation/screens/ShareImageScreenshotTestKt/ShareImageExtremeScreenshotTest_0.png b/androidApp/src/screenshotTestDebug/reference/org/neotech/app/abysner/presentation/screens/ShareImageScreenshotTestKt/ShareImageExtremeScreenshotTest_0.png index 05bd2a1..498cb72 100644 --- a/androidApp/src/screenshotTestDebug/reference/org/neotech/app/abysner/presentation/screens/ShareImageScreenshotTestKt/ShareImageExtremeScreenshotTest_0.png +++ b/androidApp/src/screenshotTestDebug/reference/org/neotech/app/abysner/presentation/screens/ShareImageScreenshotTestKt/ShareImageExtremeScreenshotTest_0.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:940a28679db76f25ef485b48c852bf6c592474e43fc7e513ec43caf14dd9694f -size 168641 +oid sha256:aeca96503a20d77fc605f3204f283109bdb5a20458bdd1462f64db7863d908e3 +size 168652 diff --git a/androidApp/src/screenshotTestDebug/reference/org/neotech/app/abysner/presentation/screens/ShareImageScreenshotTestKt/ShareImageScreenshotTest_0.png b/androidApp/src/screenshotTestDebug/reference/org/neotech/app/abysner/presentation/screens/ShareImageScreenshotTestKt/ShareImageScreenshotTest_0.png index ea53627..7ffaf38 100644 --- a/androidApp/src/screenshotTestDebug/reference/org/neotech/app/abysner/presentation/screens/ShareImageScreenshotTestKt/ShareImageScreenshotTest_0.png +++ b/androidApp/src/screenshotTestDebug/reference/org/neotech/app/abysner/presentation/screens/ShareImageScreenshotTestKt/ShareImageScreenshotTest_0.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:c22fa89a4dd7943306ac2f6ef85369132f727247da7292ef115ae92beec87f12 -size 162232 +oid sha256:3be378abd28c22e3cf5a736190798fea281a9efbefb6c3b4b8ee11d658ac0349 +size 162225 diff --git a/androidApp/src/screenshotTestDebug/reference/org/neotech/app/abysner/presentation/screens/planner/PlannerScreenScreenshotTestKt/PlannerScreenFoldableScreenshotTest_2db23271_0.png b/androidApp/src/screenshotTestDebug/reference/org/neotech/app/abysner/presentation/screens/planner/PlannerScreenScreenshotTestKt/PlannerScreenFoldableScreenshotTest_2db23271_0.png index 33e0f6e..df66c9c 100644 --- a/androidApp/src/screenshotTestDebug/reference/org/neotech/app/abysner/presentation/screens/planner/PlannerScreenScreenshotTestKt/PlannerScreenFoldableScreenshotTest_2db23271_0.png +++ b/androidApp/src/screenshotTestDebug/reference/org/neotech/app/abysner/presentation/screens/planner/PlannerScreenScreenshotTestKt/PlannerScreenFoldableScreenshotTest_2db23271_0.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:88aead5d35fea0a4c28d6064745c3540499ff29d4755fce58fc0696e4cbb9d73 -size 68026 +oid sha256:39c54252d02c655a69d1d0017f5f070cc0660a6a299c513e5ab96cf886564cc4 +size 68217 diff --git a/androidApp/src/screenshotTestDebug/reference/org/neotech/app/abysner/presentation/screens/planner/PlannerScreenScreenshotTestKt/PlannerScreenScreenshotTest_d12982c9_0.png b/androidApp/src/screenshotTestDebug/reference/org/neotech/app/abysner/presentation/screens/planner/PlannerScreenScreenshotTestKt/PlannerScreenScreenshotTest_d12982c9_0.png index d8825e7..f34dfa7 100644 --- a/androidApp/src/screenshotTestDebug/reference/org/neotech/app/abysner/presentation/screens/planner/PlannerScreenScreenshotTestKt/PlannerScreenScreenshotTest_d12982c9_0.png +++ b/androidApp/src/screenshotTestDebug/reference/org/neotech/app/abysner/presentation/screens/planner/PlannerScreenScreenshotTestKt/PlannerScreenScreenshotTest_d12982c9_0.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:d684d1efca82a57ff44e3f46032cfe3c21800512a89695e19e594c8ff0a91903 -size 179226 +oid sha256:6af9cbf9017d3f0da415dab20ad13ae7909baf73484079403c8f2689f4988d0e +size 179431 diff --git a/androidApp/src/screenshotTestDebug/reference/org/neotech/app/abysner/presentation/screens/planner/PlannerScreenScreenshotTestKt/PlannerScreenWithWarningsScreenshotTest_d12982c9_0.png b/androidApp/src/screenshotTestDebug/reference/org/neotech/app/abysner/presentation/screens/planner/PlannerScreenScreenshotTestKt/PlannerScreenWithWarningsScreenshotTest_d12982c9_0.png index 461adbb..20159e5 100644 --- a/androidApp/src/screenshotTestDebug/reference/org/neotech/app/abysner/presentation/screens/planner/PlannerScreenScreenshotTestKt/PlannerScreenWithWarningsScreenshotTest_d12982c9_0.png +++ b/androidApp/src/screenshotTestDebug/reference/org/neotech/app/abysner/presentation/screens/planner/PlannerScreenScreenshotTestKt/PlannerScreenWithWarningsScreenshotTest_d12982c9_0.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:159334a6e064c9010b966d477c41629753bfc33666f44896ba7eb74b06ff187d -size 207614 +oid sha256:c706ce021a9b5c68b842a272522e144f4037128beee03f9aa2611db69befbcd5 +size 207860 diff --git a/androidApp/src/screenshotTestDebug/reference/org/neotech/app/abysner/presentation/screens/planner/decoplan/DecoPlanConfigurationSummeryDialogScreenshotTestKt/DecoPlanConfigurationSummeryDialogScreenshotTest_7a9301c3_0.png b/androidApp/src/screenshotTestDebug/reference/org/neotech/app/abysner/presentation/screens/planner/decoplan/DecoPlanConfigurationSummeryDialogScreenshotTestKt/DecoPlanConfigurationSummeryDialogScreenshotTest_7a9301c3_0.png index c42e3ab..46231cb 100644 --- a/androidApp/src/screenshotTestDebug/reference/org/neotech/app/abysner/presentation/screens/planner/decoplan/DecoPlanConfigurationSummeryDialogScreenshotTestKt/DecoPlanConfigurationSummeryDialogScreenshotTest_7a9301c3_0.png +++ b/androidApp/src/screenshotTestDebug/reference/org/neotech/app/abysner/presentation/screens/planner/decoplan/DecoPlanConfigurationSummeryDialogScreenshotTestKt/DecoPlanConfigurationSummeryDialogScreenshotTest_7a9301c3_0.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:3bdd9c11d467fd43a06f4c947e2e4e19bb38851478daa665b313d2c8a2c97586 -size 26721 +oid sha256:8563e1c14f3a4063be85476ef2038872a49f4138bb206bab4f9fd329b415d1de +size 26394 diff --git a/composeApp/src/commonMain/composeResources/values/strings.xml b/composeApp/src/commonMain/composeResources/values/strings.xml index b5b3786..d5acb4d 100644 --- a/composeApp/src/commonMain/composeResources/values/strings.xml +++ b/composeApp/src/commonMain/composeResources/values/strings.xml @@ -16,6 +16,11 @@ %1$d meters + + %1$d foot + %1$d feet + + %1$d minute %1$d minutes diff --git a/composeApp/src/commonMain/kotlin/org/neotech/app/abysner/presentation/component/GasPropertiesComponent.kt b/composeApp/src/commonMain/kotlin/org/neotech/app/abysner/presentation/component/GasPropertiesComponent.kt index 5aa5f64..017b8d7 100644 --- a/composeApp/src/commonMain/kotlin/org/neotech/app/abysner/presentation/component/GasPropertiesComponent.kt +++ b/composeApp/src/commonMain/kotlin/org/neotech/app/abysner/presentation/component/GasPropertiesComponent.kt @@ -31,6 +31,7 @@ import org.neotech.app.abysner.domain.core.physics.Pressure import org.neotech.app.abysner.domain.utilities.floorTolerant import org.neotech.app.abysner.domain.utilities.format import org.neotech.app.abysner.presentation.theme.AbysnerTheme +import org.neotech.app.abysner.presentation.utilities.depthUnitLabel import org.neotech.app.abysner.presentation.utilities.formatDisplayDepth import kotlin.math.round @@ -42,6 +43,7 @@ fun GasPropertiesComponent( maxPPO2Secondary: Double?, maxDensity: Double, environment: Environment, + unitSystem: UnitSystem, showTopRow: Boolean = true, onClickMix: (() -> Unit)? = null ) { @@ -91,8 +93,9 @@ fun GasPropertiesComponent( Row(modifier = Modifier.wrapContentHeight(), horizontalArrangement = Arrangement.spacedBy(12.dp)) { - val mod = gas?.oxygenModAmbientPressureWithTolerance(maxPPO2) - ?.formatModDepth(UnitSystem.METRIC, environment) + val mod = gas + ?.oxygenModAmbientPressureWithTolerance(maxPPO2) + ?.formatModDepth(unitSystem, environment) ?: EMPTY_PLACEHOLDER FlipCardComponent( @@ -121,7 +124,7 @@ fun GasPropertiesComponent( val modSecondary = gas ?.oxygenModAmbientPressureWithTolerance(maxPPO2Secondary) - ?.formatModDepth(UnitSystem.METRIC, environment) + ?.formatModDepth(unitSystem, environment) ?: EMPTY_PLACEHOLDER FlipCardComponent( @@ -149,7 +152,7 @@ fun GasPropertiesComponent( val densityMod = gas ?.densityModAmbientPressure(maxDensity) - ?.formatModDepthRounded(UnitSystem.METRIC, environment) + ?.formatModDepthRounded(unitSystem, environment) ?: EMPTY_PLACEHOLDER FlipCardComponent( @@ -220,6 +223,7 @@ private fun GasPropertiesComponentPreview() { maxPPO2Secondary = 1.6, maxDensity = 6.8, environment = Environment(Salinity.WATER_FRESH, ATMOSPHERIC_PRESSURE_AT_SEA_LEVEL), + unitSystem = UnitSystem.METRIC, showTopRow = true ) } diff --git a/composeApp/src/commonMain/kotlin/org/neotech/app/abysner/presentation/screens/DiveConfigurationScreen.kt b/composeApp/src/commonMain/kotlin/org/neotech/app/abysner/presentation/screens/DiveConfigurationScreen.kt index 73e990b..41afa49 100644 --- a/composeApp/src/commonMain/kotlin/org/neotech/app/abysner/presentation/screens/DiveConfigurationScreen.kt +++ b/composeApp/src/commonMain/kotlin/org/neotech/app/abysner/presentation/screens/DiveConfigurationScreen.kt @@ -48,7 +48,10 @@ import me.tatarka.inject.annotations.Inject import androidx.compose.ui.tooling.preview.Preview import org.neotech.app.abysner.domain.core.model.Configuration import org.neotech.app.abysner.domain.core.model.Salinity +import org.neotech.app.abysner.domain.core.model.UnitSystem +import org.neotech.app.abysner.domain.core.physics.METERS_PER_FOOT import org.neotech.app.abysner.domain.diveplanning.PlanningRepository +import org.neotech.app.abysner.domain.settings.SettingsRepository import org.neotech.app.abysner.domain.utilities.DecimalFormat import org.neotech.app.abysner.presentation.component.appendBold import org.neotech.app.abysner.presentation.component.preferences.CcrSetpointPreference @@ -60,6 +63,9 @@ import org.neotech.app.abysner.presentation.component.preferences.SingleChoicePr import org.neotech.app.abysner.presentation.component.preferences.SwitchPreference import org.neotech.app.abysner.presentation.component.textfield.SuffixVisualTransformation import org.neotech.app.abysner.presentation.theme.AbysnerTheme +import org.neotech.app.abysner.presentation.utilities.depthUnitLabel +import org.neotech.app.abysner.presentation.utilities.formatDepth +import org.neotech.app.abysner.presentation.utilities.rateUnitLabel import kotlin.math.abs import kotlin.math.roundToInt @@ -71,12 +77,15 @@ typealias DiveConfigurationScreen = @Composable (navController: NavHostControlle @Composable fun DiveConfigurationScreen( planningRepository: PlanningRepository, + settingsRepository: SettingsRepository, @Assisted navController: NavHostController = rememberNavController() ) { val configuration by planningRepository.configuration.collectAsState() + val settings by settingsRepository.settings.collectAsState() DiveConfigurationScreen( navController = navController, configuration = configuration, + unitSystem = settings.unitSystem, updateConfiguration = planningRepository::updateConfiguration, ) } @@ -86,6 +95,7 @@ fun DiveConfigurationScreen( fun DiveConfigurationScreen( navController: NavHostController = rememberNavController(), configuration: Configuration, + unitSystem: UnitSystem, updateConfiguration: ((Configuration) -> Configuration) -> Unit, ) { AbysnerTheme { @@ -173,39 +183,44 @@ fun DiveConfigurationScreen( NumberPreference( label = "Altitude", description = "The altitude of the water surface at which the dive is taking place, in most cases this will be 0 meter (sea level).", - initialValue = configuration.altitude.toInt(), - minValue = -450, - maxValue = 3000, - valueFormatter = { "$it m"}, - textFieldVisualTransformation = SuffixVisualTransformation(" m") + initialValue = unitSystem.metersToDisplayDepth(configuration.altitude).toInt(), + minValue = if (unitSystem == UnitSystem.IMPERIAL) { -1500 } else { -450 }, + maxValue = if (unitSystem == UnitSystem.IMPERIAL) { 10000 } else { 3000 }, + valueFormatter = { "$it ${unitSystem.depthUnitLabel}"}, + textFieldVisualTransformation = SuffixVisualTransformation(" ${unitSystem.depthUnitLabel}") ) { altitude -> - updateConfiguration { it.copy(altitude = altitude.toDouble()) } + val meters = unitSystem.displayDepthToMeters(altitude.toDouble()) + updateConfiguration { it.copy(altitude = meters) } } - SettingsSubTitle(subTitle = "Diver") + NumberPreference( label = "Ascent speed", description = "The speed at which the diver is planning to ascent to stops or the surface.", - initialValue = configuration.maxAscentRate.toInt(), + initialValue = unitSystem.metersToDisplayDepth(configuration.maxAscentRate).toInt(), minValue = 1, - maxValue = 18, - valueFormatter = { "$it m/min"}, - textFieldVisualTransformation = SuffixVisualTransformation(" m/min") + maxValue = if (unitSystem == UnitSystem.IMPERIAL) { 60 } else { 18 }, + valueFormatter = { "$it ${unitSystem.rateUnitLabel}"}, + textFieldVisualTransformation = SuffixVisualTransformation(" ${unitSystem.rateUnitLabel}") ) { ascentRate -> - updateConfiguration { it.copy(maxAscentRate = ascentRate.toDouble()) } + val meters = unitSystem.displayDepthToMeters(ascentRate.toDouble()) + updateConfiguration { it.copy(maxAscentRate = meters) } } + NumberPreference( label = "Descent speed", description = "The speed at which the diver is planning to descent to planned bottom sections.", - initialValue = configuration.maxDescentRate.toInt(), + initialValue = unitSystem.metersToDisplayDepth(configuration.maxDescentRate).toInt(), minValue = 1, - maxValue = 40, - valueFormatter = { "$it m/min"}, - textFieldVisualTransformation = SuffixVisualTransformation(" m/min") + maxValue = if (unitSystem == UnitSystem.IMPERIAL) { 130 } else { 40 }, + valueFormatter = { "$it ${unitSystem.rateUnitLabel}"}, + textFieldVisualTransformation = SuffixVisualTransformation(" ${unitSystem.rateUnitLabel}") ) { descentRate -> - updateConfiguration { it.copy(maxDescentRate = descentRate.toDouble()) } + val meters = unitSystem.displayDepthToMeters(descentRate.toDouble()) + updateConfiguration { it.copy(maxDescentRate = meters) } } + NumberPreference( label = "Gas usage", description = "The average amount of gas the diver is breathing per minute at 1 atmosphere during normal diving conditions. This is also known as SAC or RMV rate.", @@ -235,15 +250,26 @@ fun DiveConfigurationScreen( SingleChoicePreference( label = "Deco stop interval", description = "The interval at which to make deco stops.", - items = persistentListOf(3.0, 6.0, 9.0), - selectedItemIndex = when (configuration.decoStepSize) { - 3.0 -> 0 - 6.0 -> 1 - 9.0 -> 2 - else -> 1 + items = when (unitSystem) { + UnitSystem.METRIC -> persistentListOf(3.0, 6.0, 9.0) + UnitSystem.IMPERIAL -> persistentListOf(10.0 * METERS_PER_FOOT, 20.0 * METERS_PER_FOOT, 30.0 * METERS_PER_FOOT) + }, + selectedItemIndex = when (unitSystem) { + UnitSystem.METRIC -> when (configuration.decoStepSize) { + 3.0 -> 0 + 6.0 -> 1 + 9.0 -> 2 + else -> 0 + } + UnitSystem.IMPERIAL -> when ((configuration.decoStepSize / METERS_PER_FOOT).roundToInt()) { + 10 -> 0 + 20 -> 1 + 30 -> 2 + else -> 0 + } }, itemToStringMapper = { - "${it.roundToInt()} m" + it.formatDepth(unitSystem) } ) { decoStepSize -> updateConfiguration { it.copy(decoStepSize = decoStepSize) } @@ -252,15 +278,26 @@ fun DiveConfigurationScreen( SingleChoicePreference( label = "Last deco stop", description = "Depth at which the last deco stop will be made.", - items = persistentListOf(3.0, 6.0, 9.0), - selectedItemIndex = when (configuration.lastDecoStopDepth) { - 3.0 -> 0 - 6.0 -> 1 - 9.0 -> 2 - else -> 0 + items = when (unitSystem) { + UnitSystem.METRIC -> persistentListOf(3.0, 6.0, 9.0) + UnitSystem.IMPERIAL -> persistentListOf(10.0 * METERS_PER_FOOT, 20.0 * METERS_PER_FOOT, 30.0 * METERS_PER_FOOT) + }, + selectedItemIndex = when (unitSystem) { + UnitSystem.METRIC -> when (configuration.lastDecoStopDepth) { + 3.0 -> 0 + 6.0 -> 1 + 9.0 -> 2 + else -> 0 + } + UnitSystem.IMPERIAL -> when ((configuration.lastDecoStopDepth / METERS_PER_FOOT).roundToInt()) { + 10 -> 0 + 20 -> 1 + 30 -> 2 + else -> 0 + } }, itemToStringMapper = { - "${it.roundToInt()} m" + it.formatDepth(unitSystem) } ) { lastDecoStopDepth -> updateConfiguration { it.copy(lastDecoStopDepth = lastDecoStopDepth) } @@ -322,13 +359,14 @@ fun DiveConfigurationScreen( NumberPreference( label = "Deeper", description = "How much deeper the contingency plan should be, this is added to the deepest section of the planned dive.", - initialValue = configuration.contingencyDeeper.roundToInt(), + initialValue = unitSystem.metersToDisplayDepth(configuration.contingencyDeeper).toInt(), minValue = 0, - maxValue = 5, - valueFormatter = { "$it m"}, - textFieldVisualTransformation = SuffixVisualTransformation(" m") + maxValue = if (unitSystem == UnitSystem.IMPERIAL) { 15 } else { 5 }, + valueFormatter = { "$it ${unitSystem.depthUnitLabel}"}, + textFieldVisualTransformation = SuffixVisualTransformation(" ${unitSystem.depthUnitLabel}") ) { deeper -> - updateConfiguration { it.copy(contingencyDeeper = deeper.toDouble()) } + val meters = unitSystem.displayDepthToMeters(deeper.toDouble()) + updateConfiguration { it.copy(contingencyDeeper = meters) } } NumberPreference( @@ -400,7 +438,8 @@ fun DiveConfigurationScreen( fun DiveConfigurationScreenPreview() { DiveConfigurationScreen( configuration = Configuration(), - updateConfiguration = {} + updateConfiguration = {}, + unitSystem = UnitSystem.METRIC ) } diff --git a/composeApp/src/commonMain/kotlin/org/neotech/app/abysner/presentation/screens/SettingsScreen.kt b/composeApp/src/commonMain/kotlin/org/neotech/app/abysner/presentation/screens/SettingsScreen.kt index f6865fc..4a14613 100644 --- a/composeApp/src/commonMain/kotlin/org/neotech/app/abysner/presentation/screens/SettingsScreen.kt +++ b/composeApp/src/commonMain/kotlin/org/neotech/app/abysner/presentation/screens/SettingsScreen.kt @@ -37,8 +37,10 @@ import androidx.compose.ui.unit.dp import androidx.navigation.NavHostController import androidx.navigation.compose.currentBackStackEntryAsState import androidx.navigation.compose.rememberNavController +import kotlinx.collections.immutable.toImmutableList import me.tatarka.inject.annotations.Assisted import me.tatarka.inject.annotations.Inject +import org.neotech.app.abysner.domain.core.model.UnitSystem import org.neotech.app.abysner.domain.settings.SettingsRepository import org.neotech.app.abysner.domain.settings.model.SettingsModel import org.neotech.app.abysner.domain.settings.model.ThemeMode @@ -46,7 +48,6 @@ import org.neotech.app.abysner.presentation.component.preferences.SettingsSubTit import org.neotech.app.abysner.presentation.component.preferences.SingleChoicePreference import org.neotech.app.abysner.presentation.component.preferences.SwitchPreference import org.neotech.app.abysner.presentation.theme.AbysnerTheme -import kotlinx.collections.immutable.toImmutableList typealias SettingsScreen = @Composable (navController: NavHostController) -> Unit @@ -116,6 +117,24 @@ fun SettingsScreen( }, ) + SettingsSubTitle(subTitle = "Units") + + SingleChoicePreference( + label = "Unit system", + description = "Choose between metric (meters, bar, liters) and imperial (feet, psi, cubic feet).", + items = UnitSystem.entries.toImmutableList(), + selectedItemIndex = UnitSystem.entries.indexOf(settings.unitSystem), + itemToStringMapper = { + when (it) { + UnitSystem.METRIC -> "Metric" + UnitSystem.IMPERIAL -> "Imperial" + } + }, + onItemPicked = { picked -> + updateSettings { it.copy(unitSystem = picked) } + }, + ) + SettingsSubTitle(subTitle = "Deco plan") SwitchPreference( diff --git a/composeApp/src/commonMain/kotlin/org/neotech/app/abysner/presentation/screens/ShareImage.kt b/composeApp/src/commonMain/kotlin/org/neotech/app/abysner/presentation/screens/ShareImage.kt index 2590fa1..9fd366f 100644 --- a/composeApp/src/commonMain/kotlin/org/neotech/app/abysner/presentation/screens/ShareImage.kt +++ b/composeApp/src/commonMain/kotlin/org/neotech/app/abysner/presentation/screens/ShareImage.kt @@ -182,7 +182,7 @@ fun ShareImage( text = "Limits", style = MaterialTheme.typography.bodyLarge.copy(fontWeight = FontWeight.Bold) ) - GasLimitsTable(divePlanSet = divePlan) + GasLimitsTable(divePlanSet = divePlan, unitSystem = settingsModel.unitSystem) Text( modifier = Modifier.padding(top = 16.dp, bottom = 4.dp), diff --git a/composeApp/src/commonMain/kotlin/org/neotech/app/abysner/presentation/screens/planner/PlanScreen.kt b/composeApp/src/commonMain/kotlin/org/neotech/app/abysner/presentation/screens/planner/PlanScreen.kt index e275968..f02de93 100644 --- a/composeApp/src/commonMain/kotlin/org/neotech/app/abysner/presentation/screens/planner/PlanScreen.kt +++ b/composeApp/src/commonMain/kotlin/org/neotech/app/abysner/presentation/screens/planner/PlanScreen.kt @@ -267,6 +267,7 @@ fun PlannerScreen( diveMode = uiState.diveMode, cylinders = uiState.availableGas, segments = uiState.segments, + unitSystem = uiState.settingsModel.unitSystem, onDismiss = { cylinderSheet = null }, onAddCylinder = onAddCylinder, onUpdateCylinder = onUpdateCylinder, @@ -276,6 +277,7 @@ fun PlannerScreen( SegmentPickerBottomSheetHost( show = segmentSheet, configuration = uiState.configuration, + unitSystem = uiState.settingsModel.unitSystem, segments = uiState.segments, diveMode = uiState.diveMode, cylinders = uiState.availableGas.toImmutableList(), diff --git a/composeApp/src/commonMain/kotlin/org/neotech/app/abysner/presentation/screens/planner/PlanScreenContent.kt b/composeApp/src/commonMain/kotlin/org/neotech/app/abysner/presentation/screens/planner/PlanScreenContent.kt index daa93fb..4e5ed25 100644 --- a/composeApp/src/commonMain/kotlin/org/neotech/app/abysner/presentation/screens/planner/PlanScreenContent.kt +++ b/composeApp/src/commonMain/kotlin/org/neotech/app/abysner/presentation/screens/planner/PlanScreenContent.kt @@ -66,6 +66,7 @@ internal fun PlanScreenContent( ) SegmentsCardComponent( segments = uiState.segments, + unitSystem = uiState.settingsModel.unitSystem, addAllowed = uiState.availableGas.isNotEmpty(), onAddSegment = onAddSegment, onRemoveSegment = { index, _ -> onRemoveSegment(index) }, @@ -82,6 +83,7 @@ internal fun PlanScreenContent( isLoading = uiState.isCalculatingDivePlan, divePlanSet = uiState.selectedDivePlanSet.getOrNull(), planningException = uiState.selectedDivePlanSet.exceptionOrNull(), + unitSystem = uiState.settingsModel.unitSystem, ) } } diff --git a/composeApp/src/commonMain/kotlin/org/neotech/app/abysner/presentation/screens/planner/PlanScreenViewModel.kt b/composeApp/src/commonMain/kotlin/org/neotech/app/abysner/presentation/screens/planner/PlanScreenViewModel.kt index 1a3f078..32a1a5d 100644 --- a/composeApp/src/commonMain/kotlin/org/neotech/app/abysner/presentation/screens/planner/PlanScreenViewModel.kt +++ b/composeApp/src/commonMain/kotlin/org/neotech/app/abysner/presentation/screens/planner/PlanScreenViewModel.kt @@ -36,6 +36,7 @@ import org.neotech.app.abysner.domain.core.model.Configuration import org.neotech.app.abysner.domain.core.model.Cylinder import org.neotech.app.abysner.domain.core.model.DiveMode import org.neotech.app.abysner.domain.core.model.Gas +import org.neotech.app.abysner.domain.core.model.UnitSystem import org.neotech.app.abysner.domain.diveplanning.DivePlanner import org.neotech.app.abysner.domain.diveplanning.PlanningRepository import org.neotech.app.abysner.domain.diveplanning.model.DivePlanInputModel @@ -50,7 +51,6 @@ import org.neotech.app.abysner.domain.gasplanning.GasPlanner import org.neotech.app.abysner.domain.settings.SettingsRepository import org.neotech.app.abysner.domain.settings.model.SettingsModel import org.neotech.app.abysner.presentation.utilities.combine -import kotlin.math.roundToInt import kotlin.time.Duration import kotlin.time.measureTimedValue @@ -164,9 +164,10 @@ class PlanScreenViewModel( private val divePlanSet: StateFlow> = combine( planInput.map { it.model }.distinctUntilChanged(), planningRepository.configuration, - ) { model, config -> + settingsRepository.settings.map { it.unitSystem }.distinctUntilChanged(), + ) { model, config, unitSystem -> isCalculatingDivePlan.value = true - val result = measureTimedValue { calculateMultiDivePlan(model, config) } + val result = measureTimedValue { calculateMultiDivePlan(model, config, unitSystem) } .also { isCalculatingDivePlan.value = false } println("Duration: Calculating dive plan took ${result.duration}") result.value @@ -213,8 +214,9 @@ class PlanScreenViewModel( private fun calculateMultiDivePlan( model: MultiDivePlanInputModel, configuration: Configuration, + unitSystem: UnitSystem, ): Result = try { - val planner = DivePlanner(configuration) + val planner = DivePlanner(configuration, unitSystem) val gasPlanner = GasPlanner() val sets = model.dives.mapIndexed { index, diveInput -> @@ -282,9 +284,11 @@ class PlanScreenViewModel( plan to gasPlanner.calculateGasPlan(plan) } + val deeperDisplay = deeper?.let { unitSystem.metersToDisplayDepth(it).toInt() } + DivePlanSet( base = divePlan, - deeper = deeper?.roundToInt(), + deeper = deeperDisplay, longer = longer, bailout = diveInput.bailout, diveMode = diveInput.diveMode, diff --git a/composeApp/src/commonMain/kotlin/org/neotech/app/abysner/presentation/screens/planner/cylinders/CylinderPickerBottomSheet.kt b/composeApp/src/commonMain/kotlin/org/neotech/app/abysner/presentation/screens/planner/cylinders/CylinderPickerBottomSheet.kt index 61ccc42..9e3cbfb 100644 --- a/composeApp/src/commonMain/kotlin/org/neotech/app/abysner/presentation/screens/planner/cylinders/CylinderPickerBottomSheet.kt +++ b/composeApp/src/commonMain/kotlin/org/neotech/app/abysner/presentation/screens/planner/cylinders/CylinderPickerBottomSheet.kt @@ -49,6 +49,7 @@ import org.neotech.app.abysner.presentation.theme.AbysnerTheme import org.neotech.app.abysner.domain.core.model.Cylinder import org.neotech.app.abysner.domain.core.model.Environment import org.neotech.app.abysner.domain.core.model.Gas +import org.neotech.app.abysner.domain.core.model.UnitSystem import org.neotech.app.abysner.domain.diveplanning.model.DiveProfileSection import org.neotech.app.abysner.domain.diveplanning.model.PlannedCylinderModel import org.neotech.app.abysner.presentation.utilities.ModalTarget @@ -71,6 +72,7 @@ internal fun CylinderPickerBottomSheetHost( diveMode: DiveMode = DiveMode.OPEN_CIRCUIT, cylinders: List = emptyList(), segments: List = emptyList(), + unitSystem: UnitSystem, onDismiss: () -> Unit, onAddCylinder: (Cylinder) -> Unit, onUpdateCylinder: (Cylinder) -> Unit, @@ -96,6 +98,7 @@ internal fun CylinderPickerBottomSheetHost( environment = configuration.environment, maxPPO2 = configuration.maxPPO2, maxPPO2Secondary = configuration.maxPPO2Deco, + unitSystem = unitSystem, lockGas = lockGas, showBailoutToggle = showBailoutToggle, initialBailoutValue = plannedCylinder?.isAvailableForBailout ?: true, @@ -122,6 +125,7 @@ private fun CylinderPickerBottomSheet( environment: Environment, maxPPO2: Double, maxPPO2Secondary: Double, + unitSystem: UnitSystem = UnitSystem.METRIC, lockGas: Boolean = false, showBailoutToggle: Boolean = false, initialBailoutValue: Boolean = true, @@ -141,6 +145,7 @@ private fun CylinderPickerBottomSheet( environment = environment, maxPPO2 = maxPPO2, maxPPO2Secondary = maxPPO2Secondary, + unitSystem = unitSystem, lockGas = lockGas, showBailoutToggle = showBailoutToggle, initialBailoutValue = initialBailoutValue, @@ -160,6 +165,7 @@ private fun CylinderPickerBottomSheetContent( environment: Environment, maxPPO2: Double, maxPPO2Secondary: Double?, + unitSystem: UnitSystem = UnitSystem.METRIC, lockGas: Boolean = false, showBailoutToggle: Boolean = false, initialBailoutValue: Boolean = true, @@ -323,6 +329,7 @@ private fun CylinderPickerBottomSheetContent( maxPPO2Secondary = maxPPO2Secondary, maxDensity = Gas.MAX_GAS_DENSITY, environment = environment, + unitSystem = unitSystem, onClickMix = if (lockGas) { null } else { { showStandardGasPickerDialog.value = true } }, diff --git a/composeApp/src/commonMain/kotlin/org/neotech/app/abysner/presentation/screens/planner/decoplan/DecoPlanCard.kt b/composeApp/src/commonMain/kotlin/org/neotech/app/abysner/presentation/screens/planner/decoplan/DecoPlanCard.kt index 914bf50..e3362e4 100644 --- a/composeApp/src/commonMain/kotlin/org/neotech/app/abysner/presentation/screens/planner/decoplan/DecoPlanCard.kt +++ b/composeApp/src/commonMain/kotlin/org/neotech/app/abysner/presentation/screens/planner/decoplan/DecoPlanCard.kt @@ -62,9 +62,8 @@ import org.jetbrains.compose.resources.painterResource import org.neotech.app.abysner.domain.core.model.BreathingMode import org.neotech.app.abysner.domain.core.model.Cylinder import org.neotech.app.abysner.domain.core.model.DiveMode -import org.neotech.app.abysner.domain.core.model.Environment import org.neotech.app.abysner.domain.core.model.Gas -import org.neotech.app.abysner.domain.core.physics.metersToAmbientPressure +import org.neotech.app.abysner.domain.core.model.UnitSystem import org.neotech.app.abysner.domain.decompression.model.DiveSegment import org.neotech.app.abysner.domain.decompression.model.compactSimilarSegments import org.neotech.app.abysner.domain.diveplanning.DivePlanner @@ -88,8 +87,10 @@ import org.neotech.app.abysner.presentation.preview.PreviewData import org.neotech.app.abysner.presentation.theme.AbysnerTheme import org.neotech.app.abysner.presentation.theme.onWarning import org.neotech.app.abysner.presentation.theme.warning +import org.neotech.app.abysner.presentation.utilities.depthUnitLabel +import org.neotech.app.abysner.presentation.utilities.formatDepth +import org.neotech.app.abysner.presentation.utilities.formatDisplayDepth import kotlin.math.ceil -import kotlin.math.roundToInt @Composable fun DecoPlanCardComponent( @@ -122,7 +123,7 @@ fun DecoPlanCardComponent( append("Deco plan") withStyle(MaterialTheme.typography.titleSmall.toSpanStyle()) { if (divePlanSet?.isDeeper == true) { - append(" +${divePlanSet.deeper}m") + append(" +${divePlanSet.deeper}${settings.unitSystem.depthUnitLabel}") } if (divePlanSet?.isLonger == true) { append(" +${divePlanSet.longer}min") @@ -144,7 +145,10 @@ fun DecoPlanCardComponent( } if (showConfigurationInfo && divePlanSet != null) { - DecoPlanConfigurationSummeryDialog(configuration = divePlanSet.configuration) { + DecoPlanConfigurationSummeryDialog( + configuration = divePlanSet.configuration, + unitSystem = settings.unitSystem, + ) { showConfigurationInfo = false } } @@ -173,7 +177,7 @@ fun DecoPlanCardComponent( } else { val items = buildList { - add("Deeper\u202F+${divePlanSet.configuration.contingencyDeeper.roundToInt()}") + add("Deeper\u202F+${divePlanSet.configuration.contingencyDeeper.formatDepth(settings.unitSystem)}") add("Longer\u202F+${divePlanSet.configuration.contingencyLonger}") if (divePlanSet.isCcr) { add("Bail-out") } }.toImmutableList() @@ -231,7 +235,8 @@ fun DecoPlanCardComponent( DecoPlanExtraInfo( modifier = Modifier.fillMaxWidth().padding(horizontal = 16.dp), - divePlan = planToShow + divePlan = planToShow, + unitSystem = settings.unitSystem, ) } } @@ -302,7 +307,8 @@ fun DecoPlanOxygenToxicityDisplay( @Composable fun DecoPlanExtraInfo( modifier: Modifier = Modifier, - divePlan: DivePlan + divePlan: DivePlan, + unitSystem: UnitSystem, ) { FlowRow( modifier = modifier, @@ -311,7 +317,7 @@ fun DecoPlanExtraInfo( ) { InfoPill( label = "Depth", - value = "~${DecimalFormat.format(1, divePlan.averageDepth)} m", + value = "~${divePlan.averageDepth.formatDisplayDepth(unitSystem, decimals = 1)}", size = InfoPillSize.SMALL, ) if (divePlan.firstDeco != -1) { @@ -331,7 +337,7 @@ fun DecoPlanExtraInfo( if (divePlan.deepestCeiling > 0.0) { InfoPill( label = "Ceiling", - value = "${DecimalFormat.format(1, divePlan.deepestCeiling)} m", + value = divePlan.deepestCeiling.formatDisplayDepth(unitSystem, decimals = 1), size = InfoPillSize.SMALL, ) } @@ -376,7 +382,6 @@ fun DecoPlanTable( val segments = divePlan.segmentsCollapsed .toMutableList() .compactSimilarSegments(compactAscentsAndStops = settings.showBasicDecoTable) - val environment = divePlan.configuration.environment rowsIndexed(segments, key = { _, segment -> segment.start }) { index, diveSegment -> // For gas switch segments show the gas the diver is switching to, rather than the gas @@ -397,7 +402,7 @@ fun DecoPlanTable( runtime = diveSegment.end, gas = displayGas, isBailoutSwitch = isBailoutSwitch, - environment = environment, + unitSystem = settings.unitSystem, ) } } @@ -428,7 +433,7 @@ private fun RowScope.DecoPlanRow( runtime: Int, gas: Gas, isBailoutSwitch: Boolean = false, - environment: Environment, + unitSystem: UnitSystem, ) { val typeIcon = when (diveSegment.type) { DiveSegment.Type.DECO_STOP -> Res.drawable.ic_baseline_stop_square_24 @@ -440,7 +445,7 @@ private fun RowScope.DecoPlanRow( TextWithStartIcon( modifier = Modifier.weight(0.23f), - text = diveSegment.endDepth.toInt().toString(), + text = diveSegment.endDepth.formatDisplayDepth(unitSystem, includeUnit = false), icon = painterResource(resource = typeIcon) ) Text( @@ -457,12 +462,12 @@ private fun RowScope.DecoPlanRow( text = gas.toString(), ) - val endAmbientPressure = metersToAmbientPressure(diveSegment.endDepth, environment).value + val endAmbientPressure = diveSegment.endPressure val ppO2Text = when (val mode = diveSegment.breathingMode) { is BreathingMode.ClosedCircuit -> { val endMode = diveSegment.breathingModeAtEnd ?: mode - val startAmbientPressure = metersToAmbientPressure(diveSegment.startDepth, environment).value + val startAmbientPressure = diveSegment.startPressure val ppO2Start = mode.effectivePpO2(startAmbientPressure) val ppO2End = endMode.effectivePpO2(endAmbientPressure) diff --git a/composeApp/src/commonMain/kotlin/org/neotech/app/abysner/presentation/screens/planner/decoplan/DecoPlanConfigurationSummeryDialog.kt b/composeApp/src/commonMain/kotlin/org/neotech/app/abysner/presentation/screens/planner/decoplan/DecoPlanConfigurationSummeryDialog.kt index 25f5b6e..9010293 100644 --- a/composeApp/src/commonMain/kotlin/org/neotech/app/abysner/presentation/screens/planner/decoplan/DecoPlanConfigurationSummeryDialog.kt +++ b/composeApp/src/commonMain/kotlin/org/neotech/app/abysner/presentation/screens/planner/decoplan/DecoPlanConfigurationSummeryDialog.kt @@ -19,13 +19,17 @@ import androidx.compose.material3.Text import androidx.compose.material3.TextButton import androidx.compose.runtime.Composable import org.neotech.app.abysner.domain.core.model.Configuration +import org.neotech.app.abysner.domain.core.model.UnitSystem import org.neotech.app.abysner.domain.utilities.DecimalFormat import androidx.compose.ui.tooling.preview.Preview import org.neotech.app.abysner.presentation.theme.AbysnerTheme +import org.neotech.app.abysner.presentation.utilities.formatDepth +import org.neotech.app.abysner.presentation.utilities.rateUnitLabel @Composable fun DecoPlanConfigurationSummeryDialog( configuration: Configuration, + unitSystem: UnitSystem, onDismissRequest: () -> Unit ) { AlertDialog( @@ -51,15 +55,15 @@ fun DecoPlanConfigurationSummeryDialog( style = MaterialTheme.typography.bodySmall ) Text( - text = "Altitude pressure: ${DecimalFormat.format(0, configuration.environment.atmosphericPressure * 1000.0)} hPa (${DecimalFormat.format(0, configuration.altitude)} meters)", + text = "Altitude pressure: ${DecimalFormat.format(0, configuration.environment.atmosphericPressure * 1000.0)} hPa (${configuration.altitude.formatDepth(unitSystem)})", style = MaterialTheme.typography.bodySmall ) Text( - text = "Max ascent speed: ${configuration.maxAscentRate} m/min", + text = "Max ascent speed: ${configuration.maxAscentRate.formatDepth(unitSystem, includeUnit = false)} ${unitSystem.rateUnitLabel}", style = MaterialTheme.typography.bodySmall ) Text( - text = "Max descent speed: ${configuration.maxDescentRate} m/min", + text = "Max descent speed: ${configuration.maxDescentRate.formatDepth(unitSystem, includeUnit = false)} ${unitSystem.rateUnitLabel}", style = MaterialTheme.typography.bodySmall ) Text( @@ -71,7 +75,7 @@ fun DecoPlanConfigurationSummeryDialog( style = MaterialTheme.typography.bodySmall ) Text( - text = "Max END: ${configuration.maxEND}", + text = "Max END: ${configuration.maxEND.formatDepth(unitSystem)}", style = MaterialTheme.typography.bodySmall ) } @@ -85,6 +89,7 @@ fun DecoPlanConfigurationSummeryDialogPreview() { AbysnerTheme { DecoPlanConfigurationSummeryDialog( configuration = Configuration(), + unitSystem = UnitSystem.METRIC, onDismissRequest = {} ) } diff --git a/composeApp/src/commonMain/kotlin/org/neotech/app/abysner/presentation/screens/planner/gasplan/GasPlanCard.kt b/composeApp/src/commonMain/kotlin/org/neotech/app/abysner/presentation/screens/planner/gasplan/GasPlanCard.kt index d2d1e3b..149070f 100644 --- a/composeApp/src/commonMain/kotlin/org/neotech/app/abysner/presentation/screens/planner/gasplan/GasPlanCard.kt +++ b/composeApp/src/commonMain/kotlin/org/neotech/app/abysner/presentation/screens/planner/gasplan/GasPlanCard.kt @@ -12,7 +12,6 @@ package org.neotech.app.abysner.presentation.screens.planner.gasplan -import androidx.compose.ui.tooling.preview.Preview import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.fillMaxWidth import androidx.compose.foundation.layout.padding @@ -25,14 +24,16 @@ import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.remember import androidx.compose.runtime.setValue import androidx.compose.ui.Modifier +import androidx.compose.ui.text.SpanStyle import androidx.compose.ui.text.buildAnnotatedString import androidx.compose.ui.text.font.FontStyle import androidx.compose.ui.text.font.FontWeight -import androidx.compose.ui.text.SpanStyle import androidx.compose.ui.text.style.TextAlign import androidx.compose.ui.text.withStyle +import androidx.compose.ui.tooling.preview.Preview import androidx.compose.ui.unit.dp import org.neotech.app.abysner.domain.core.model.Gas +import org.neotech.app.abysner.domain.core.model.UnitSystem import org.neotech.app.abysner.domain.diveplanning.model.DivePlanSet import org.neotech.app.abysner.domain.gasplanning.model.CylinderGasRequirements import org.neotech.app.abysner.domain.utilities.DecimalFormat @@ -51,6 +52,8 @@ import org.neotech.app.abysner.presentation.screens.planner.decoplan.LoadingBoxW import org.neotech.app.abysner.presentation.theme.AbysnerTheme import org.neotech.app.abysner.presentation.theme.IconFont import org.neotech.app.abysner.presentation.theme.appendIcon +import org.neotech.app.abysner.presentation.utilities.depthUnitLabel +import org.neotech.app.abysner.presentation.utilities.formatDisplayDepth @Composable fun GasPlanCardComponent( @@ -58,6 +61,7 @@ fun GasPlanCardComponent( divePlanSet: DivePlanSet?, planningException: Throwable?, isLoading: Boolean, + unitSystem: UnitSystem, ) { val errorMessage: String? = planningException?.getUserReadableMessage() @@ -78,7 +82,7 @@ fun GasPlanCardComponent( append("Gas plan") withStyle(MaterialTheme.typography.titleSmall.toSpanStyle()) { if(divePlanSet?.isDeeper == true) { - append(" +${divePlanSet.deeper}m") + append(" +${divePlanSet.deeper}${unitSystem.depthUnitLabel}") } if(divePlanSet?.isLonger == true) { append(" +${divePlanSet.longer}min") @@ -174,7 +178,8 @@ fun GasPlanCardComponent( GasLimitsTable( modifier = Modifier.padding(horizontal = 16.dp) .padding(bottom = 16.dp), - divePlanSet + divePlanSet = divePlanSet, + unitSystem = unitSystem, ) val explanationText = if (divePlanSet.isCcr) { @@ -348,6 +353,7 @@ fun GasTotalsTable( fun GasLimitsTable( modifier: Modifier = Modifier, divePlanSet: DivePlanSet, + unitSystem: UnitSystem, ) { Table( modifier = modifier, @@ -363,7 +369,7 @@ fun GasLimitsTable( key = { it.gas }, ) { gasAtDepth -> Text(modifier = Modifier.weight(0.2f), text = gasAtDepth.gas.toString()) - Text(modifier = Modifier.weight(0.25f), text = "${gasAtDepth.depth.toInt()}m") + Text(modifier = Modifier.weight(0.25f), text = gasAtDepth.depth.formatDisplayDepth(unitSystem)) val alertSeverityDensity = when { gasAtDepth.density.greaterThanTolerant(Gas.MAX_GAS_DENSITY, DISPLAY_TOLERANCE) -> AlertSeverity.ERROR @@ -397,7 +403,8 @@ private fun GasPlanCardComponentPreview() { GasPlanCardComponent( divePlanSet = PreviewData.divePlan1, planningException = null, - isLoading = false + isLoading = false, + unitSystem = UnitSystem.METRIC, ) } } @@ -409,7 +416,8 @@ private fun GasPlanCardComponentWithWarningsPreview() { GasPlanCardComponent( divePlanSet = PreviewData.divePlan2, planningException = null, - isLoading = false + isLoading = false, + unitSystem = UnitSystem.METRIC, ) } } @@ -421,7 +429,8 @@ private fun GasPlanCardComponentEmptyPreview() { GasPlanCardComponent( divePlanSet = null, planningException = null, - isLoading = false + isLoading = false, + unitSystem = UnitSystem.METRIC, ) } } @@ -433,7 +442,8 @@ private fun GasPlanCardComponentCcrPreview() { GasPlanCardComponent( divePlanSet = PreviewData.divePlanCcr, planningException = null, - isLoading = false + isLoading = false, + unitSystem = UnitSystem.METRIC, ) } } @@ -445,7 +455,8 @@ private fun GasPlanCardComponentCcrBailoutPreview() { GasPlanCardComponent( divePlanSet = PreviewData.divePlanCcrBailout, planningException = null, - isLoading = false + isLoading = false, + unitSystem = UnitSystem.METRIC, ) } } diff --git a/composeApp/src/commonMain/kotlin/org/neotech/app/abysner/presentation/screens/planner/segments/SegmentPickerBottomSheet.kt b/composeApp/src/commonMain/kotlin/org/neotech/app/abysner/presentation/screens/planner/segments/SegmentPickerBottomSheet.kt index 706d890..0d3e3c4 100644 --- a/composeApp/src/commonMain/kotlin/org/neotech/app/abysner/presentation/screens/planner/segments/SegmentPickerBottomSheet.kt +++ b/composeApp/src/commonMain/kotlin/org/neotech/app/abysner/presentation/screens/planner/segments/SegmentPickerBottomSheet.kt @@ -14,6 +14,7 @@ package org.neotech.app.abysner.presentation.screens.planner.segments import abysner.composeapp.generated.resources.Res import abysner.composeapp.generated.resources.segment_picker_travel_time_hint +import abysner.composeapp.generated.resources.unit_foot import abysner.composeapp.generated.resources.unit_meter import abysner.composeapp.generated.resources.unit_minute import androidx.compose.foundation.layout.Arrangement @@ -56,7 +57,7 @@ import org.neotech.app.abysner.domain.core.model.Cylinder import org.neotech.app.abysner.domain.core.model.DiveMode import org.neotech.app.abysner.domain.core.model.Environment import org.neotech.app.abysner.domain.core.model.Gas -import org.neotech.app.abysner.domain.core.physics.metersToAmbientPressure +import org.neotech.app.abysner.domain.core.model.UnitSystem import org.neotech.app.abysner.domain.core.physics.partialPressure import org.neotech.app.abysner.domain.diveplanning.model.CylinderRole import org.neotech.app.abysner.domain.diveplanning.model.DiveProfileSection @@ -75,6 +76,7 @@ import org.neotech.app.abysner.presentation.component.textfield.OutlinedNumberIn import org.neotech.app.abysner.presentation.component.textfield.SuffixVisualTransformation import org.neotech.app.abysner.presentation.theme.AbysnerTheme import org.neotech.app.abysner.presentation.theme.bodyExtraLarge +import org.neotech.app.abysner.presentation.utilities.depthUnitLabel import org.neotech.app.abysner.presentation.utilities.ModalTarget @OptIn(ExperimentalMaterial3Api::class) @@ -82,6 +84,7 @@ import org.neotech.app.abysner.presentation.utilities.ModalTarget internal fun SegmentPickerBottomSheetHost( show: ModalTarget?, configuration: Configuration, + unitSystem: UnitSystem, segments: List, cylinders: ImmutableList, diveMode: DiveMode = DiveMode.OPEN_CIRCUIT, @@ -104,6 +107,7 @@ internal fun SegmentPickerBottomSheetHost( diveMode = diveMode, previousDepth = segments.getOrNull(previousIndex)?.depthInMeters ?: 0.0, configuration = configuration, + unitSystem = unitSystem, onAddOrUpdateDiveSegment = { if (editIndex != null) { onUpdateSegment(editIndex, it) @@ -129,6 +133,7 @@ private fun SegmentPickerBottomSheet( diveMode: DiveMode = DiveMode.OPEN_CIRCUIT, previousDepth: Double, configuration: Configuration, + unitSystem: UnitSystem = UnitSystem.METRIC, onAddOrUpdateDiveSegment: (gas: DiveProfileSection) -> Unit = {}, onDismiss: () -> Unit = {}, ) { @@ -148,6 +153,7 @@ private fun SegmentPickerBottomSheet( diveMode = diveMode, previousDepth = previousDepth, configuration = configuration, + unitSystem = unitSystem, onAddOrUpdateDiveSegment = onAddOrUpdateDiveSegment, onDismiss = onDismiss, ) @@ -167,6 +173,7 @@ private fun SegmentPickerBottomSheetContent( diveMode: DiveMode = DiveMode.OPEN_CIRCUIT, previousDepth: Double, configuration: Configuration, + unitSystem: UnitSystem = UnitSystem.METRIC, onAddOrUpdateDiveSegment: (gas: DiveProfileSection) -> Unit = {}, onDismiss: () -> Unit = {}, ) { @@ -195,7 +202,16 @@ private fun SegmentPickerBottomSheetContent( } var selectedCylinder: Cylinder by remember { mutableStateOf(initialCylinder ?: availableCylinders.first()) } - var depth by remember { mutableIntStateOf(initialValue?.depthInMeters?.roundToInt() ?: 10) } + var depthInDisplayUnit by remember { + mutableIntStateOf( + initialValue?.depthInMeters?.let { meters -> + unitSystem.metersToDisplayDepth(meters).roundToInt() + } ?: when (unitSystem) { + UnitSystem.METRIC -> 10 + UnitSystem.IMPERIAL -> 30 + } + ) + } var time by remember { mutableIntStateOf(initialValue?.duration ?: 15) } val errorMessageDepth = remember { mutableStateOf(null) } @@ -219,10 +235,11 @@ private fun SegmentPickerBottomSheetContent( primaryEnabled = isTimeValid.value && isDepthValid.value, onClose = dismiss, onPrimary = { + val depthMeters = unitSystem.displayDepthToMeters(depthInDisplayUnit.toDouble()) onAddOrUpdateDiveSegment( DiveProfileSection( duration = time, - depthInMeters = depth.toDouble(), + depthInMeters = depthMeters, cylinder = selectedCylinder ) ) @@ -237,7 +254,7 @@ private fun SegmentPickerBottomSheetContent( horizontalAlignment = Alignment.CenterHorizontally ) { - val ambientPressure = metersToAmbientPressure(depth.toDouble(), environment) + val ambientPressure = unitSystem.depthToAmbientPressure(depthInDisplayUnit.toDouble(), environment) if (!diveMode.isCcr) { GasPropertiesComponent( @@ -247,6 +264,7 @@ private fun SegmentPickerBottomSheetContent( maxPPO2 = maxPPO2, maxPPO2Secondary = null, environment = environment, + unitSystem = unitSystem, showTopRow = false, ) @@ -289,14 +307,14 @@ private fun SegmentPickerBottomSheetContent( .recordLayoutCoordinates("depth", textFieldPositions), label = "Depth", minValue = 1, - maxValue = 150, - visualTransformation = SuffixVisualTransformation(" m"), - initialValue = initialValue?.depthInMeters?.roundToInt() ?: 10, + maxValue = if (unitSystem == UnitSystem.IMPERIAL) { 500 } else { 150 }, + visualTransformation = SuffixVisualTransformation(" ${unitSystem.depthUnitLabel}"), + initialValue = depthInDisplayUnit, errorMessage = errorMessageDepth, isValid = isDepthValid, onNumberChanged = { if (it != null) { - depth = it + depthInDisplayUnit = it } }, supportingText = null, @@ -336,8 +354,7 @@ private fun SegmentPickerBottomSheetContent( } else if (anyErrorMessage == null) { val diluentGas = cylinders.ccrDiluentCylinder()?.cylinder?.gas if (diluentGas != null) { - val ambientPressure = metersToAmbientPressure(depth.toDouble(), environment).value - val diluentPpO2 = partialPressure(ambientPressure, diluentGas.oxygenFraction) + val diluentPpO2 = partialPressure(ambientPressure.value, diluentGas.oxygenFraction) if (diluentPpO2 > configuration.ccrHighSetpoint) { anyErrorMessage = "Warning: Diluent PPO2 exceeds setpoint at this depth!" } @@ -353,13 +370,18 @@ private fun SegmentPickerBottomSheetContent( } } - val distance = (previousDepth - depth) + val depthMetersForTravel = unitSystem.displayDepthToMeters(depthInDisplayUnit.toDouble()) + val distance = (previousDepth - depthMetersForTravel) val travelTime = configuration.travelTime(distance) val bottomTime = time - travelTime + val depthUnitPlural = when (unitSystem) { + UnitSystem.METRIC -> Res.plurals.unit_meter + UnitSystem.IMPERIAL -> Res.plurals.unit_foot + } val travelTimeHint = pluralsStringBuilder(Res.string.segment_picker_travel_time_hint) { pluralInt(Res.plurals.unit_minute, travelTime) - pluralInt(Res.plurals.unit_meter, depth) + pluralInt(depthUnitPlural, depthInDisplayUnit) pluralInt(Res.plurals.unit_minute, bottomTime) } diff --git a/composeApp/src/commonMain/kotlin/org/neotech/app/abysner/presentation/screens/planner/segments/SegmentsCard.kt b/composeApp/src/commonMain/kotlin/org/neotech/app/abysner/presentation/screens/planner/segments/SegmentsCard.kt index c8aaaea..89fa4aa 100644 --- a/composeApp/src/commonMain/kotlin/org/neotech/app/abysner/presentation/screens/planner/segments/SegmentsCard.kt +++ b/composeApp/src/commonMain/kotlin/org/neotech/app/abysner/presentation/screens/planner/segments/SegmentsCard.kt @@ -37,19 +37,21 @@ import androidx.compose.ui.Modifier import androidx.compose.ui.text.font.FontStyle import androidx.compose.ui.tooling.preview.Preview import androidx.compose.ui.unit.dp -import kotlin.math.roundToInt import org.jetbrains.compose.resources.painterResource import org.neotech.app.abysner.domain.core.model.Cylinder import org.neotech.app.abysner.domain.core.model.Gas +import org.neotech.app.abysner.domain.core.model.UnitSystem import org.neotech.app.abysner.domain.diveplanning.model.DiveProfileSection import org.neotech.app.abysner.presentation.component.IconAndTextButton import org.neotech.app.abysner.presentation.component.TextWithStartIcon import org.neotech.app.abysner.presentation.theme.AbysnerTheme +import org.neotech.app.abysner.presentation.utilities.formatDepth @Composable fun SegmentsCardComponent( modifier: Modifier = Modifier, segments: List, + unitSystem: UnitSystem, addAllowed: Boolean, onAddSegment: () -> Unit, onRemoveSegment: (index: Int, segment: DiveProfileSection) -> Unit, @@ -70,6 +72,7 @@ fun SegmentsCardComponent( onEditSegment(index, diveSegment) }, diveProfileSection = diveSegment, + unitSystem = unitSystem, onDelete = { onRemoveSegment(index, diveSegment) } @@ -113,6 +116,7 @@ fun SegmentsCardComponent( private fun SegmentListItemComponent( modifier: Modifier, diveProfileSection: DiveProfileSection = DiveProfileSection(10, 15.0, Cylinder(Gas.Air, 232, 12)), + unitSystem: UnitSystem = UnitSystem.METRIC, onDelete: (diveProfileSection: DiveProfileSection) -> Unit = {}, ) { Row( @@ -121,8 +125,7 @@ private fun SegmentListItemComponent( ) { TextWithStartIcon( modifier = Modifier.padding(start = 16.dp), - // TODO: Format depth to no decimals places specifically (round?) - text = "${diveProfileSection.depthInMeters.roundToInt()} m", + text = diveProfileSection.depthInMeters.formatDepth(unitSystem), icon = painterResource(resource = Res.drawable.ic_outline_vertical_align_bottom_24) ) TextWithStartIcon( @@ -158,6 +161,7 @@ private fun SegmentsCardComponentPreview() { DiveProfileSection(15, 15.0, cylinder), DiveProfileSection(30, 10.0, cylinder) ), + unitSystem = UnitSystem.METRIC, addAllowed = true, onAddSegment = {}, onRemoveSegment = { _, _ -> }, diff --git a/composeApp/src/commonMain/kotlin/org/neotech/app/abysner/presentation/utilities/UnitFormatter.kt b/composeApp/src/commonMain/kotlin/org/neotech/app/abysner/presentation/utilities/UnitFormatter.kt index e97245d..cdefedf 100644 --- a/composeApp/src/commonMain/kotlin/org/neotech/app/abysner/presentation/utilities/UnitFormatter.kt +++ b/composeApp/src/commonMain/kotlin/org/neotech/app/abysner/presentation/utilities/UnitFormatter.kt @@ -50,11 +50,10 @@ val UnitSystem.sacRateUnitLabel: String /** * Formats a depth value that is already in display-units (meters for metric, feet for imperial). - * Rounds to integer and optionally appends the unit suffix. */ -fun Double.formatDisplayDepth(unitSystem: UnitSystem, includeUnit: Boolean = true): String { - val value = roundToInt() - return if (includeUnit) { "$value ${unitSystem.depthUnitLabel}" } else { value.toString() } +fun Double.formatDisplayDepth(unitSystem: UnitSystem, decimals: Int = 0, includeUnit: Boolean = true): String { + val value = DecimalFormat.format(decimals, this) + return if (includeUnit) { "$value ${unitSystem.depthUnitLabel}" } else { value } } /** diff --git a/composeApp/src/commonTest/kotlin/org/neotech/app/abysner/presentation/utilities/UnitFormatterTest.kt b/composeApp/src/commonTest/kotlin/org/neotech/app/abysner/presentation/utilities/UnitFormatterTest.kt index 2370b62..b11add3 100644 --- a/composeApp/src/commonTest/kotlin/org/neotech/app/abysner/presentation/utilities/UnitFormatterTest.kt +++ b/composeApp/src/commonTest/kotlin/org/neotech/app/abysner/presentation/utilities/UnitFormatterTest.kt @@ -24,6 +24,12 @@ class UnitFormatterTest { assertEquals("100 ft", 100.0.formatDisplayDepth(UnitSystem.IMPERIAL)) } + @Test + fun formatDisplayDepth_respectsDecimalParameter() { + assertEquals("30.5 m", 30.48.formatDisplayDepth(UnitSystem.METRIC, decimals = 1)) + assertEquals("100.3 ft", 100.3.formatDisplayDepth(UnitSystem.IMPERIAL, decimals = 1)) + } + @Test fun formatDepth_convertsMetersToDisplayUnit() { assertEquals("30 m", 30.48.formatDepth(UnitSystem.METRIC)) diff --git a/domain/src/commonMain/kotlin/org/neotech/app/abysner/domain/diveplanning/DivePlanner.kt b/domain/src/commonMain/kotlin/org/neotech/app/abysner/domain/diveplanning/DivePlanner.kt index c7e2231..3f71587 100644 --- a/domain/src/commonMain/kotlin/org/neotech/app/abysner/domain/diveplanning/DivePlanner.kt +++ b/domain/src/commonMain/kotlin/org/neotech/app/abysner/domain/diveplanning/DivePlanner.kt @@ -73,7 +73,6 @@ class DivePlanner( cylinders: List, diveMode: DiveMode = DiveMode.OPEN_CIRCUIT, bailout: Boolean = false, - unitSystem: UnitSystem = UnitSystem.METRIC, ): DivePlan { require(!bailout || diveMode.isCcr) { diff --git a/domain/src/commonTest/kotlin/org/neotech/app/abysner/domain/diveplanning/DivePlannerTest.kt b/domain/src/commonTest/kotlin/org/neotech/app/abysner/domain/diveplanning/DivePlannerTest.kt index 987accb..4bdc4d4 100644 --- a/domain/src/commonTest/kotlin/org/neotech/app/abysner/domain/diveplanning/DivePlannerTest.kt +++ b/domain/src/commonTest/kotlin/org/neotech/app/abysner/domain/diveplanning/DivePlannerTest.kt @@ -538,7 +538,6 @@ class DivePlannerTest { val divePlan = divePlanner.addDive( plan = listOf(DiveProfileSection(duration = 25, 100.0 * METERS_PER_FOOT, bottomGas)), cylinders = listOf(bottomGas, Cylinder.aluminium80Cuft(Gas.Nitrox50)).assign(), - unitSystem = UnitSystem.IMPERIAL, ) val plan = divePlan.segmentsCollapsed