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