From 75bc4cc2e5fc19a41cd2dd485fbc49fe0f8415db Mon Sep 17 00:00:00 2001 From: Sergei Melnikov Date: Mon, 10 Feb 2025 00:09:50 +0100 Subject: [PATCH 1/3] Implement insulin on board value --- app/build.gradle.kts | 2 + .../net/cacheux/nvp/app/MainActivity.kt | 5 + .../cacheux/nvp/app/MainScreenViewModel.kt | 14 +++ .../net/cacheux/nvp/app/SettingsViewModel.kt | 5 + .../DatastorePreferencesRepository.kt | 20 ++++ .../kotlin/net/cacheux/nvp/app/IoBUseCase.kt | 91 +++++++++++++++++++ .../cacheux/nvp/app/PreferencesRepository.kt | 8 ++ app/src/desktopMain/kotlin/main.kt | 6 +- .../cacheux/nvp/app/MainScreenViewModel.kt | 12 +++ .../repository/PreferencesRepositoryImpl.kt | 5 + gradle/libs.versions.toml | 3 + .../kotlin/net/cacheux/nvp/model/Dose.kt | 2 +- .../net/cacheux/nvp/model/InsulinUnit.kt | 16 ++++ .../kotlin/net/cacheux/nvp/model/IoB.kt | 9 ++ .../composeResources/values-fr/strings.xml | 8 ++ .../composeResources/values/strings.xml | 12 +++ .../net/cacheux/nvp/ui/InsulinOnBoard.kt | 29 ++++++ .../kotlin/net/cacheux/nvp/ui/MainScreen.kt | 40 +++++--- .../net/cacheux/nvp/ui/SettingsScreen.kt | 44 +++++++++ .../net/cacheux/nvp/ui/utils/UnitFormatter.kt | 9 ++ 20 files changed, 323 insertions(+), 17 deletions(-) create mode 100644 app/src/commonMain/kotlin/net/cacheux/nvp/app/IoBUseCase.kt create mode 100644 model/src/commonMain/kotlin/net/cacheux/nvp/model/InsulinUnit.kt create mode 100644 model/src/commonMain/kotlin/net/cacheux/nvp/model/IoB.kt create mode 100644 ui/src/commonMain/kotlin/net/cacheux/nvp/ui/InsulinOnBoard.kt create mode 100644 ui/src/commonMain/kotlin/net/cacheux/nvp/ui/utils/UnitFormatter.kt diff --git a/app/build.gradle.kts b/app/build.gradle.kts index 359783f..ed44039 100644 --- a/app/build.gradle.kts +++ b/app/build.gradle.kts @@ -32,6 +32,7 @@ kotlin { implementation(libs.androidx.datastore.core) implementation(libs.androidx.datastore.preferences) + implementation(libs.commons.math3) configurations["kspAndroid"].dependencies.add(project.dependencies.create(libs.hilt.android.compiler.get())) } @@ -74,6 +75,7 @@ kotlin { implementation(libs.androidx.room.common) implementation(libs.androidx.room.runtime) implementation(libs.androidx.sqlite.bundled) + implementation(libs.commons.math3) } val desktopMain by getting { diff --git a/app/src/androidMain/kotlin/net/cacheux/nvp/app/MainActivity.kt b/app/src/androidMain/kotlin/net/cacheux/nvp/app/MainActivity.kt index 59f79aa..347a330 100644 --- a/app/src/androidMain/kotlin/net/cacheux/nvp/app/MainActivity.kt +++ b/app/src/androidMain/kotlin/net/cacheux/nvp/app/MainActivity.kt @@ -57,11 +57,16 @@ class MainActivity : ComponentActivity() { groupDelay = settingsViewModel.groupDelay.asStateWrapper(), autoIgnoreEnabled = settingsViewModel.autoIgnoreEnabled.asStateWrapper(), autoIgnoreValue = settingsViewModel.autoIgnoreValue.asStateWrapper(), + groupIoB = settingsViewModel.groupIoB.asStateWrapper(), + insulinPeak = settingsViewModel.insulinPeak.asStateWrapper(), + delta = settingsViewModel.delta.asStateWrapper(), + insulinDuration = settingsViewModel.insulinDuration.asStateWrapper() ) ) } else { MainScreen( doseList = viewModel.doseList.collectAsState(listOf()).value.reversed(), + iob = viewModel.iob.collectAsState(null).value, message = viewModel.getReadMessage().collectAsState().value, loading = viewModel.isReading().collectAsState().value, diff --git a/app/src/androidMain/kotlin/net/cacheux/nvp/app/MainScreenViewModel.kt b/app/src/androidMain/kotlin/net/cacheux/nvp/app/MainScreenViewModel.kt index 2ed7d77..97b3d60 100644 --- a/app/src/androidMain/kotlin/net/cacheux/nvp/app/MainScreenViewModel.kt +++ b/app/src/androidMain/kotlin/net/cacheux/nvp/app/MainScreenViewModel.kt @@ -7,16 +7,20 @@ import dagger.hilt.android.qualifiers.ApplicationContext import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.ExperimentalCoroutinesApi +import kotlinx.coroutines.delay import kotlinx.coroutines.flow.Flow import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.flow.StateFlow import kotlinx.coroutines.flow.flatMapLatest +import kotlinx.coroutines.flow.flow import kotlinx.coroutines.launch import net.cacheux.nvp.app.utils.csvToDoseList import net.cacheux.nvp.logging.logDebug import net.cacheux.nvp.model.Dose import net.cacheux.nvp.model.DoseGroup +import net.cacheux.nvp.model.IoB import java.io.InputStream +import java.util.concurrent.TimeUnit import javax.inject.Inject @HiltViewModel @@ -33,6 +37,8 @@ class MainScreenViewModel @Inject constructor( preferencesRepository ) + private val iobUseCase: IoBUseCase = IoBUseCase(preferencesRepository) + fun getPenList() = storageRepository.getPenList() private val isReading = MutableStateFlow(false) @@ -64,6 +70,14 @@ class MainScreenViewModel @Inject constructor( storageRepository.getDoseList(it) } + val iob: Flow = iobUseCase.calculate(doseList, flow { + while(true) { + emit(System.currentTimeMillis()) + delay(TimeUnit.MINUTES.toMillis(1)) + } + }) + + val store = repository.getDataStore() fun loadCsvFile(input: InputStream) { diff --git a/app/src/androidMain/kotlin/net/cacheux/nvp/app/SettingsViewModel.kt b/app/src/androidMain/kotlin/net/cacheux/nvp/app/SettingsViewModel.kt index be2f7bb..41f60d0 100644 --- a/app/src/androidMain/kotlin/net/cacheux/nvp/app/SettingsViewModel.kt +++ b/app/src/androidMain/kotlin/net/cacheux/nvp/app/SettingsViewModel.kt @@ -12,4 +12,9 @@ class SettingsViewModel @Inject constructor( val groupDelay = preferencesRepository.groupDelay val autoIgnoreEnabled = preferencesRepository.autoIgnoreEnabled val autoIgnoreValue = preferencesRepository.autoIgnoreValue + + val groupIoB = preferencesRepository.groupIoB + val insulinPeak = preferencesRepository.insulinPeak + val delta = preferencesRepository.delta + val insulinDuration = preferencesRepository.insulinDuration } \ No newline at end of file diff --git a/app/src/androidMain/kotlin/net/cacheux/nvp/app/repository/DatastorePreferencesRepository.kt b/app/src/androidMain/kotlin/net/cacheux/nvp/app/repository/DatastorePreferencesRepository.kt index 765d61b..f1d8fbf 100644 --- a/app/src/androidMain/kotlin/net/cacheux/nvp/app/repository/DatastorePreferencesRepository.kt +++ b/app/src/androidMain/kotlin/net/cacheux/nvp/app/repository/DatastorePreferencesRepository.kt @@ -18,6 +18,10 @@ class DatastorePreferencesRepository(context: Context): PreferencesRepository { val GROUP_DELAY = intPreferencesKey("group_delay") val AUTO_IGNORE_ENABLED = booleanPreferencesKey("auto_ignore_enabled") val AUTO_IGNORE_VALUE = intPreferencesKey("auto_ignore_value") + val GROUP_IOB = booleanPreferencesKey("group_iob") + val IOB_INSULIN_PEAK = intPreferencesKey("iob_insulin_peak") + val IOB_DELTA = intPreferencesKey("iob_delta") + val IOB_INSULIN_DURATION = intPreferencesKey("iob_insulin_duration") } override val groupEnabled: StateFlowWrapper = @@ -39,4 +43,20 @@ class DatastorePreferencesRepository(context: Context): PreferencesRepository { PreferenceStateFlowWrapper( dataStore, PreferencesKeys.AUTO_IGNORE_VALUE, 2 ) + + override val groupIoB: StateFlowWrapper = PreferenceStateFlowWrapper( + dataStore, PreferencesKeys.GROUP_IOB, false + ) + + override val insulinPeak: StateFlowWrapper = PreferenceStateFlowWrapper( + dataStore, PreferencesKeys.IOB_INSULIN_PEAK, 75 + ) + + override val delta: StateFlowWrapper = PreferenceStateFlowWrapper( + dataStore, PreferencesKeys.IOB_DELTA, 15 + ) + + override val insulinDuration: StateFlowWrapper = PreferenceStateFlowWrapper( + dataStore, PreferencesKeys.IOB_INSULIN_DURATION, 5 + ) } diff --git a/app/src/commonMain/kotlin/net/cacheux/nvp/app/IoBUseCase.kt b/app/src/commonMain/kotlin/net/cacheux/nvp/app/IoBUseCase.kt new file mode 100644 index 0000000..8ace739 --- /dev/null +++ b/app/src/commonMain/kotlin/net/cacheux/nvp/app/IoBUseCase.kt @@ -0,0 +1,91 @@ +package net.cacheux.nvp.app + +import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.flow.Flow +import kotlinx.coroutines.flow.combine +import kotlinx.coroutines.flow.map +import net.cacheux.nvp.model.DoseGroup +import net.cacheux.nvp.model.InsulinUnit +import net.cacheux.nvp.model.IoB +import java.util.concurrent.TimeUnit +import org.apache.commons.math3.special.Gamma + +class IoBUseCase( + private val preferencesRepository: PreferencesRepository, + private val scope: CoroutineScope = CoroutineScope(Dispatchers.IO) +) { + // How fast insulin is absorbed (Novorapid: 1.5) + val absorptionRate: Double = 1.5 + + + fun calculate(doseGroups: Flow>, time: Flow): Flow { + return combine( + doseGroups, + time, + preferencesRepository.groupIoB.content, + preferencesRepository.insulinPeak.content.map { TimeUnit.MINUTES.toMillis(it.toLong()) }, + preferencesRepository.delta.content.map { TimeUnit.MINUTES.toMillis(it.toLong()) }, + preferencesRepository.insulinDuration.content.map { TimeUnit.HOURS.toMillis(it.toLong()) }, + ) { values -> + val doseGroups = values[0] as List + val time = values[1] as Long + val enabled = values[2] as Boolean + val insulinPeak = values[3] as Long + val delta = values[4] as Long + val insulinDuration = values[5] as Long + + if (!enabled) { + return@combine null + } + + val nextTime = time + delta + + val remaining = InsulinUnit( + doseGroups + .sortedByDescending { it.getTime() } + .takeWhile { time - it.getTime() <= insulinDuration } + .sumOf { + (it.getTotal() * this.fraction( + time, + it.getTime(), + insulinPeak + )).toInt() + } + ) + + val next = InsulinUnit( + doseGroups + .sortedByDescending { it.getTime() } + .takeWhile { nextTime - it.getTime() <= insulinDuration } + .sumOf { + (it.getTotal() * this.fraction( + nextTime, + it.getTime(), + insulinPeak + )).toInt() + } + ) + + IoB( + time = time, + remaining = remaining, + serial = doseGroups.lastOrNull()?.getSerial() ?: "", + current = remaining - next, + delta = delta + ) + } + } + + fun fraction( + time: Long, + doseTime: Long, + insulinPeak: Long, + ): Double { + val timeSinceBolus = time - doseTime + val x = absorptionRate * timeSinceBolus / insulinPeak + val r = Gamma.regularizedGammaQ(absorptionRate + 1, x) + + return r + } +} \ No newline at end of file diff --git a/app/src/commonMain/kotlin/net/cacheux/nvp/app/PreferencesRepository.kt b/app/src/commonMain/kotlin/net/cacheux/nvp/app/PreferencesRepository.kt index 8007e3f..35e9334 100644 --- a/app/src/commonMain/kotlin/net/cacheux/nvp/app/PreferencesRepository.kt +++ b/app/src/commonMain/kotlin/net/cacheux/nvp/app/PreferencesRepository.kt @@ -7,4 +7,12 @@ interface PreferencesRepository { val groupDelay: StateFlowWrapper val autoIgnoreEnabled: StateFlowWrapper val autoIgnoreValue: StateFlowWrapper + + val groupIoB: StateFlowWrapper + // Time to peak insulin activity (Novorapid: 75 minutes) + val insulinPeak: StateFlowWrapper + // Time period to calculate amount of current active insulin + val delta: StateFlowWrapper + // How old doses is calculated (Novorapid: 5 hours) + val insulinDuration: StateFlowWrapper } diff --git a/app/src/desktopMain/kotlin/main.kt b/app/src/desktopMain/kotlin/main.kt index ccd5d76..c941052 100644 --- a/app/src/desktopMain/kotlin/main.kt +++ b/app/src/desktopMain/kotlin/main.kt @@ -15,6 +15,7 @@ import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.flow.first import kotlinx.coroutines.launch import net.cacheux.nvp.app.DoseListUseCase +import net.cacheux.nvp.app.IoBUseCase import net.cacheux.nvp.app.MainScreenViewModel import net.cacheux.nvp.app.SettingsViewModel import net.cacheux.nvp.app.StorageRepository @@ -42,7 +43,8 @@ val preferencesRepository = PreferencesRepositoryImpl() val mainScreenViewModel = MainScreenViewModel( TestPenInfoRepository(), doseListUseCase = DoseListUseCase(storageRepository, preferencesRepository), - storageRepository = storageRepository + storageRepository = storageRepository, + iobUseCase = IoBUseCase(preferencesRepository) ) val settingsViewModel = SettingsViewModel(preferencesRepository) @@ -93,6 +95,8 @@ fun main() = application { } else { MainScreen( doseList = mainScreenViewModel.doseList.collectAsState(listOf()).value.reversed(), + iob = mainScreenViewModel.iob.collectAsState(null).value, + loadingFileAvailable = true, loading = mainScreenViewModel.isReading().collectAsState().value, diff --git a/app/src/desktopMain/kotlin/net/cacheux/nvp/app/MainScreenViewModel.kt b/app/src/desktopMain/kotlin/net/cacheux/nvp/app/MainScreenViewModel.kt index f629315..9323657 100644 --- a/app/src/desktopMain/kotlin/net/cacheux/nvp/app/MainScreenViewModel.kt +++ b/app/src/desktopMain/kotlin/net/cacheux/nvp/app/MainScreenViewModel.kt @@ -4,23 +4,28 @@ import io.github.vinceglb.filekit.core.PlatformFile import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.ExperimentalCoroutinesApi +import kotlinx.coroutines.delay import kotlinx.coroutines.flow.Flow import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.flow.StateFlow import kotlinx.coroutines.flow.flatMapLatest +import kotlinx.coroutines.flow.flow import kotlinx.coroutines.launch import net.cacheux.nvp.app.utils.csvToDoseList import net.cacheux.nvp.logging.logDebug import net.cacheux.nvp.model.Dose import net.cacheux.nvp.model.DoseGroup +import net.cacheux.nvp.model.IoB import net.cacheux.nvplib.NvpController import net.cacheux.nvplib.testing.TestingDataReader import java.nio.charset.Charset +import java.util.concurrent.TimeUnit class MainScreenViewModel( private val repository: PenInfoRepository, private val storageRepository: StorageRepository, private val doseListUseCase: DoseListUseCase, + private val iobUseCase: IoBUseCase, private val coroutineScope: CoroutineScope = CoroutineScope(Dispatchers.IO) ) { fun getPenList() = storageRepository.getPenList() @@ -54,6 +59,13 @@ class MainScreenViewModel( storageRepository.getDoseList(it) } + val iob: Flow = iobUseCase.calculate(doseList, flow { + while(true) { + emit(System.currentTimeMillis()) + delay(TimeUnit.MINUTES.toMillis(1)) + } + }) + val store = repository.getDataStore() fun loadCsvFile(file: PlatformFile) { diff --git a/app/src/desktopMain/kotlin/net/cacheux/nvp/app/repository/PreferencesRepositoryImpl.kt b/app/src/desktopMain/kotlin/net/cacheux/nvp/app/repository/PreferencesRepositoryImpl.kt index 4434b31..9c34f13 100644 --- a/app/src/desktopMain/kotlin/net/cacheux/nvp/app/repository/PreferencesRepositoryImpl.kt +++ b/app/src/desktopMain/kotlin/net/cacheux/nvp/app/repository/PreferencesRepositoryImpl.kt @@ -20,6 +20,11 @@ class PreferencesRepositoryImpl: PreferencesRepository { override val autoIgnoreEnabled = booleanStateFlowWrapper("autoIgnoreEnabled", "true") override val autoIgnoreValue = intStateFlowWrapper("autoIgnoreValue", "2") + override val groupIoB = booleanStateFlowWrapper("groupIoB", "false") + override val insulinPeak = intStateFlowWrapper("insulinPeak", "75") + override val delta = intStateFlowWrapper("delta", "15") + override val insulinDuration = intStateFlowWrapper("insulinDuration", "5") + private fun saveProperties() { propertiesFile.outputStream().use { properties.store(it, null) } } diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index 573ece9..b701550 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -12,6 +12,8 @@ compose-compiler = "1.5.14" room = "2.7.0-alpha11" sqlite = "2.5.0-SNAPSHOT" androidx-datastore = "1.1.1" +commons-math = "3.6.1" + [plugins] ksp = { id = "com.google.devtools.ksp", version = "1.9.24-1.0.20" } @@ -35,6 +37,7 @@ appcompat = { group = "androidx.appcompat", name = "appcompat", version = "1.7.0 androidx-activity-compose = { module = "androidx.activity:activity-compose", version = "1.9.3" } androidx-datastore-preferences = { module = "androidx.datastore:datastore-preferences", version = "androidx-datastore" } androidx-datastore-core = { module = "androidx.datastore:datastore-core", version.ref = "androidx-datastore" } +commons-math3 = { group = "org.apache.commons", name = "commons-math3", version.ref = "commons-math" } # Room androidx-room-compiler = { group = "androidx.room", name = "room-compiler", version.ref = "room" } androidx-room-common = { group = "androidx.room", name = "room-common", version.ref = "room" } diff --git a/model/src/commonMain/kotlin/net/cacheux/nvp/model/Dose.kt b/model/src/commonMain/kotlin/net/cacheux/nvp/model/Dose.kt index e4393ef..92af0f2 100644 --- a/model/src/commonMain/kotlin/net/cacheux/nvp/model/Dose.kt +++ b/model/src/commonMain/kotlin/net/cacheux/nvp/model/Dose.kt @@ -8,7 +8,7 @@ data class Dose( ): DatedItem { fun ignored() = Dose(time, value, true, serial) - fun displayedValue() = String.format("%.1f", value.toFloat() / 10) + fun displayedValue() = String.format("%.1f", InsulinUnit(value).toFloat()) /** * Compare with another without the ignored field diff --git a/model/src/commonMain/kotlin/net/cacheux/nvp/model/InsulinUnit.kt b/model/src/commonMain/kotlin/net/cacheux/nvp/model/InsulinUnit.kt new file mode 100644 index 0000000..5ddc3f3 --- /dev/null +++ b/model/src/commonMain/kotlin/net/cacheux/nvp/model/InsulinUnit.kt @@ -0,0 +1,16 @@ +package net.cacheux.nvp.model + +@JvmInline +value class InsulinUnit(private val v: Int) { + constructor(f: Float) : this((f * 10).toInt()) + constructor(f: Double) : this((f * 10).toInt()) + + fun toFloat() = v.toFloat() / 10 + fun toInt() = v + + operator fun minus(other: InsulinUnit): InsulinUnit { + return InsulinUnit(this.v - other.v) + } +} + + diff --git a/model/src/commonMain/kotlin/net/cacheux/nvp/model/IoB.kt b/model/src/commonMain/kotlin/net/cacheux/nvp/model/IoB.kt new file mode 100644 index 0000000..45f29d3 --- /dev/null +++ b/model/src/commonMain/kotlin/net/cacheux/nvp/model/IoB.kt @@ -0,0 +1,9 @@ +package net.cacheux.nvp.model + +data class IoB( + val time: Long, + val remaining: InsulinUnit, + val current: InsulinUnit, + val delta: Long, + val serial: String = "" +) \ No newline at end of file diff --git a/ui/src/commonMain/composeResources/values-fr/strings.xml b/ui/src/commonMain/composeResources/values-fr/strings.xml index e07931d..25da47d 100644 --- a/ui/src/commonMain/composeResources/values-fr/strings.xml +++ b/ui/src/commonMain/composeResources/values-fr/strings.xml @@ -10,6 +10,8 @@ Exporter au format CSV Importer un fichier CSV Bouton retour + Insuline à bord : + Active : OK Annuler @@ -24,4 +26,10 @@ Les valeurs en dessous de la valeur spécifiée ne seront pas comptées En dessous de unités + + Insuline à bord + Afficher l'insuline active + Période de pic d'insuline (min) + Période de calcul de l'insuline active (min) + Durée de l'insuline (heures) \ No newline at end of file diff --git a/ui/src/commonMain/composeResources/values/strings.xml b/ui/src/commonMain/composeResources/values/strings.xml index 42be821..588bce4 100644 --- a/ui/src/commonMain/composeResources/values/strings.xml +++ b/ui/src/commonMain/composeResources/values/strings.xml @@ -10,6 +10,8 @@ Export to CSV file Import from CSV file Back button + Insulin On Board: + Active: OK Cancel @@ -24,4 +26,14 @@ Values below specified limit doesn't count for total Below units + + Insulin on Board + Display active insulin + Insulin peak period + Period for calculating active insulin + Insulin duration period + + minutes + hours + \ No newline at end of file diff --git a/ui/src/commonMain/kotlin/net/cacheux/nvp/ui/InsulinOnBoard.kt b/ui/src/commonMain/kotlin/net/cacheux/nvp/ui/InsulinOnBoard.kt new file mode 100644 index 0000000..c90520f --- /dev/null +++ b/ui/src/commonMain/kotlin/net/cacheux/nvp/ui/InsulinOnBoard.kt @@ -0,0 +1,29 @@ +package net.cacheux.nvp.ui + +import androidx.compose.foundation.layout.Row +import androidx.compose.foundation.layout.Spacer +import androidx.compose.foundation.layout.width +import androidx.compose.material3.Text +import androidx.compose.runtime.Composable +import androidx.compose.ui.Modifier +import androidx.compose.ui.unit.dp +import net.cacheux.nvp.model.IoB +import net.cacheux.nvp.ui.ui.generated.resources.Res +import net.cacheux.nvp.ui.ui.generated.resources.active_insulin +import net.cacheux.nvp.ui.ui.generated.resources.insulin_on_board +import net.cacheux.nvp.ui.utils.formatUnit +import org.jetbrains.compose.resources.stringResource + + +@Composable +fun InsulinOnBoard( + iob: IoB? +) { + Row { + Text(stringResource(Res.string.insulin_on_board)) + Text(iob?.remaining.formatUnit()) + Spacer(modifier = Modifier.width(8.dp)) + Text(stringResource( Res.string.active_insulin)) + Text(iob?.current.formatUnit()) + } +} \ No newline at end of file diff --git a/ui/src/commonMain/kotlin/net/cacheux/nvp/ui/MainScreen.kt b/ui/src/commonMain/kotlin/net/cacheux/nvp/ui/MainScreen.kt index ec077cd..7ed94d5 100644 --- a/ui/src/commonMain/kotlin/net/cacheux/nvp/ui/MainScreen.kt +++ b/ui/src/commonMain/kotlin/net/cacheux/nvp/ui/MainScreen.kt @@ -40,6 +40,7 @@ import androidx.compose.ui.unit.sp import kotlinx.coroutines.launch import net.cacheux.nvp.model.Dose import net.cacheux.nvp.model.DoseGroup +import net.cacheux.nvp.model.IoB import net.cacheux.nvp.ui.ui.generated.resources.Res import net.cacheux.nvp.ui.ui.generated.resources.app_name import net.cacheux.nvp.ui.ui.generated.resources.open_drawer @@ -54,6 +55,7 @@ import java.util.GregorianCalendar @Composable fun MainScreen( doseList: List, + iob: IoB? = null, message: String? = null, loading: Boolean = false, @@ -127,23 +129,31 @@ fun MainScreen( sheetPeekHeight = 0.dp ) { Column { - DoseList( - doseList, - currentDoseGroup = currentDoseGroup.value, - onDoseClick = { - scope.launch { - if (currentDoseGroup.value == it) { - currentDoseGroup.value = null - scaffoldState.bottomSheetState.hide() - } else { - currentDoseGroup.value = null - scaffoldState.bottomSheetState.hide() - currentDoseGroup.value = it - scaffoldState.bottomSheetState.expand() + iob?.let { + InsulinOnBoard( + iob = iob, + ) + } + + Column { + DoseList( + doseList, + currentDoseGroup = currentDoseGroup.value, + onDoseClick = { + scope.launch { + if (currentDoseGroup.value == it) { + currentDoseGroup.value = null + scaffoldState.bottomSheetState.hide() + } else { + currentDoseGroup.value = null + scaffoldState.bottomSheetState.hide() + currentDoseGroup.value = it + scaffoldState.bottomSheetState.expand() + } } } - } - ) + ) + } } } } diff --git a/ui/src/commonMain/kotlin/net/cacheux/nvp/ui/SettingsScreen.kt b/ui/src/commonMain/kotlin/net/cacheux/nvp/ui/SettingsScreen.kt index 205e9f1..90752bd 100644 --- a/ui/src/commonMain/kotlin/net/cacheux/nvp/ui/SettingsScreen.kt +++ b/ui/src/commonMain/kotlin/net/cacheux/nvp/ui/SettingsScreen.kt @@ -24,6 +24,13 @@ import net.cacheux.nvp.ui.ui.generated.resources.group_delay import net.cacheux.nvp.ui.ui.generated.resources.group_delay_suffix import net.cacheux.nvp.ui.ui.generated.resources.group_doses import net.cacheux.nvp.ui.ui.generated.resources.group_doses_details +import net.cacheux.nvp.ui.ui.generated.resources.group_iob +import net.cacheux.nvp.ui.ui.generated.resources.group_iob_delta +import net.cacheux.nvp.ui.ui.generated.resources.group_iob_details +import net.cacheux.nvp.ui.ui.generated.resources.group_iob_insulin_duration +import net.cacheux.nvp.ui.ui.generated.resources.group_iob_insulin_peak +import net.cacheux.nvp.ui.ui.generated.resources.hours_suffix +import net.cacheux.nvp.ui.ui.generated.resources.minutes_suffix import net.cacheux.nvp.ui.ui.generated.resources.settings import org.jetbrains.compose.resources.stringResource import org.jetbrains.compose.ui.tooling.preview.Preview @@ -43,6 +50,12 @@ data class SettingsScreenParams( val groupDelay: StateWrapper = stateWrapper(60), val autoIgnoreEnabled: StateWrapper = stateWrapper(true), val autoIgnoreValue: StateWrapper = stateWrapper(2), + + val groupIoB: StateWrapper = stateWrapper(false), + val insulinPeak: StateWrapper = stateWrapper(75), + val delta: StateWrapper = stateWrapper(15), + val insulinDuration: StateWrapper = stateWrapper(5) + ) @Composable @@ -96,6 +109,37 @@ fun SettingsScreen( ) } } + + ExpandableSwitch( + label = stringResource(Res.string.group_iob), + subLabel = stringResource(Res.string.group_iob_details), + state = params.groupIoB, + testTag = "groupIobSwitch" + ) { + PrefDivider() + IntPreference( + label = stringResource(Res.string.group_iob_insulin_peak), + value = params.insulinPeak, + suffix = stringResource(Res.string.minutes_suffix), + testTag = "iobInsulinPeakPref" + ) + + PrefDivider() + IntPreference( + label = stringResource(Res.string.group_iob_delta), + value = params.delta, + suffix = stringResource(Res.string.minutes_suffix), + testTag = "iobDeltaPref" + ) + + PrefDivider() + IntPreference( + label = stringResource(Res.string.group_iob_insulin_duration), + value = params.insulinDuration, + suffix = stringResource(Res.string.hours_suffix), + testTag = "iobInsulinDurationPref" + ) + } } } diff --git a/ui/src/commonMain/kotlin/net/cacheux/nvp/ui/utils/UnitFormatter.kt b/ui/src/commonMain/kotlin/net/cacheux/nvp/ui/utils/UnitFormatter.kt new file mode 100644 index 0000000..f2c9381 --- /dev/null +++ b/ui/src/commonMain/kotlin/net/cacheux/nvp/ui/utils/UnitFormatter.kt @@ -0,0 +1,9 @@ +package net.cacheux.nvp.ui.utils + +import net.cacheux.nvp.model.InsulinUnit + +fun InsulinUnit?.formatUnit(): String { + return this?.let { + String.format("%.1f", this.toFloat()) + } ?: "0.0" +} \ No newline at end of file From 75c395d644539f4f5eb0c41788cbbd37e9c89e9f Mon Sep 17 00:00:00 2001 From: Sergei Melnikov Date: Mon, 10 Feb 2025 15:31:24 +0100 Subject: [PATCH 2/3] Fix calculating active insulin on mulitple doses --- .../kotlin/net/cacheux/nvp/app/IoBUseCase.kt | 20 +++++++++---------- 1 file changed, 9 insertions(+), 11 deletions(-) diff --git a/app/src/commonMain/kotlin/net/cacheux/nvp/app/IoBUseCase.kt b/app/src/commonMain/kotlin/net/cacheux/nvp/app/IoBUseCase.kt index 8ace739..c9d28e3 100644 --- a/app/src/commonMain/kotlin/net/cacheux/nvp/app/IoBUseCase.kt +++ b/app/src/commonMain/kotlin/net/cacheux/nvp/app/IoBUseCase.kt @@ -41,37 +41,35 @@ class IoBUseCase( val nextTime = time + delta - val remaining = InsulinUnit( + val remaining = doseGroups .sortedByDescending { it.getTime() } .takeWhile { time - it.getTime() <= insulinDuration } - .sumOf { + .map { (it.getTotal() * this.fraction( time, it.getTime(), insulinPeak )).toInt() } - ) - val next = InsulinUnit( + val next = doseGroups .sortedByDescending { it.getTime() } .takeWhile { nextTime - it.getTime() <= insulinDuration } - .sumOf { - (it.getTotal() * this.fraction( - nextTime, + .map { + (it.getTotal() * (this.fraction( + time, it.getTime(), insulinPeak - )).toInt() + ) - this.fraction(nextTime, it.getTime(), insulinPeak))).toInt() } - ) IoB( time = time, - remaining = remaining, + remaining = InsulinUnit(remaining.sum()), serial = doseGroups.lastOrNull()?.getSerial() ?: "", - current = remaining - next, + current = InsulinUnit(next.sum()), delta = delta ) } From bd1ff5a42c7e063b3292f0e3eebc1e7ea4ce985c Mon Sep 17 00:00:00 2001 From: Sergei Melnikov Date: Mon, 10 Feb 2025 15:41:00 +0100 Subject: [PATCH 3/3] update missed translations --- ui/src/commonMain/composeResources/values-fr/strings.xml | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/ui/src/commonMain/composeResources/values-fr/strings.xml b/ui/src/commonMain/composeResources/values-fr/strings.xml index 25da47d..8e3bbd6 100644 --- a/ui/src/commonMain/composeResources/values-fr/strings.xml +++ b/ui/src/commonMain/composeResources/values-fr/strings.xml @@ -32,4 +32,8 @@ Période de pic d'insuline (min) Période de calcul de l'insuline active (min) Durée de l'insuline (heures) + + minutes + heures + \ No newline at end of file