diff --git a/app/src/main/java/com/mensinator/app/App.kt b/app/src/main/java/com/mensinator/app/App.kt index 028c1cb..a1e7bd4 100644 --- a/app/src/main/java/com/mensinator/app/App.kt +++ b/app/src/main/java/com/mensinator/app/App.kt @@ -2,6 +2,7 @@ package com.mensinator.app import android.app.Application import com.mensinator.app.settings.SettingsViewModel +import com.mensinator.app.statistics.StatisticsViewModel import org.koin.android.ext.koin.androidContext import org.koin.android.ext.koin.androidLogger import org.koin.core.context.startKoin @@ -22,6 +23,7 @@ class App : Application() { singleOf(::NotificationScheduler) { bind() } viewModel { SettingsViewModel(get(), get(), get()) } + viewModel { StatisticsViewModel(get(), get(), get(), get(), get()) } } override fun onCreate() { diff --git a/app/src/main/java/com/mensinator/app/CalculationsHelper.kt b/app/src/main/java/com/mensinator/app/CalculationsHelper.kt index 75e0033..deb3f69 100644 --- a/app/src/main/java/com/mensinator/app/CalculationsHelper.kt +++ b/app/src/main/java/com/mensinator/app/CalculationsHelper.kt @@ -1,15 +1,18 @@ package com.mensinator.app import android.util.Log +import com.mensinator.app.extensions.roundToTwoDecimalPoints import java.time.LocalDate -import kotlin.math.round class CalculationsHelper( private val dbHelper: IPeriodDatabaseHelper, ) : ICalculationsHelper { - private val periodHistory = dbHelper.getSettingByKey("period_history")?.value?.toIntOrNull() ?: 5 - private val ovulationHistory = dbHelper.getSettingByKey("ovulation_history")?.value?.toIntOrNull() ?: 5 - private val lutealCalculation = dbHelper.getSettingByKey("luteal_period_calculation")?.value?.toIntOrNull() ?: 0 + private val periodHistory + get() = dbHelper.getSettingByKey("period_history")?.value?.toIntOrNull() ?: 5 + private val ovulationHistory + get() = dbHelper.getSettingByKey("ovulation_history")?.value?.toIntOrNull() ?: 5 + private val lutealCalculation + get() = dbHelper.getSettingByKey("luteal_period_calculation")?.value?.toIntOrNull() ?: 0 override fun calculateNextPeriod(): LocalDate { val expectedPeriodDate: LocalDate @@ -119,7 +122,7 @@ class CalculationsHelper( if (growthRate.isEmpty()) { return 0.0 } - return growthRate.average().roundTo2Decimals() + return growthRate.average().roundToTwoDecimalPoints() } } @@ -221,8 +224,4 @@ class CalculationsHelper( } } - - private fun Double.roundTo2Decimals(): Double { - return (round(this * 100) / 100) - } } \ No newline at end of file diff --git a/app/src/main/java/com/mensinator/app/StatisticsScreen.kt b/app/src/main/java/com/mensinator/app/StatisticsScreen.kt deleted file mode 100644 index 506b314..0000000 --- a/app/src/main/java/com/mensinator/app/StatisticsScreen.kt +++ /dev/null @@ -1,113 +0,0 @@ -package com.mensinator.app - -import androidx.compose.foundation.layout.* -import androidx.compose.foundation.rememberScrollState -import androidx.compose.foundation.verticalScroll -import androidx.compose.material3.Text -import androidx.compose.runtime.Composable -import androidx.compose.ui.Alignment -import androidx.compose.ui.Modifier -import androidx.compose.ui.res.stringResource -import androidx.compose.ui.tooling.preview.Preview -import androidx.compose.ui.unit.dp -import androidx.compose.ui.unit.sp -import com.mensinator.app.navigation.displayCutoutExcludingStatusBarsPadding -import org.koin.compose.koinInject -import java.time.LocalDate - -@Composable -fun StatisticsScreen() { - val dbHelper: IPeriodDatabaseHelper = koinInject() - val calcHelper: ICalculationsHelper = koinInject() - val ovulationPrediction: IOvulationPrediction = koinInject() - val periodPrediction: IPeriodPrediction = koinInject() - - val averageCycleLength = calcHelper.averageCycleLength() - val periodCount = dbHelper.getPeriodCount() - val ovulationCount = dbHelper.getOvulationCount() - val averagePeriodLength = calcHelper.averagePeriodLength() - val avgLutealLength = calcHelper.averageLutealLength() - val follicleGrowthDays = calcHelper.averageFollicalGrowthInDays() - val ovulationPredictionDate = ovulationPrediction.getPredictedOvulationDate() - val periodPredictionDate = periodPrediction.getPredictedPeriodDate() - - Column( - modifier = Modifier - .fillMaxSize() - .verticalScroll(rememberScrollState()) - .displayCutoutExcludingStatusBarsPadding() - .padding(16.dp) - ) { - RowOfText( - stringResource(id = R.string.period_count), - periodCount.toString() - ) - - RowOfText( - stringResource(id = R.string.average_cycle_length), - (Math.round(averageCycleLength * 10) / 10.0).toString() + " " + stringResource(id = R.string.days) - ) - - RowOfText( - stringResource(id = R.string.average_period_length), - (Math.round(averagePeriodLength * 10) / 10.0).toString() + " " + stringResource(id = R.string.days) - ) - - RowOfText( - if (periodPredictionDate < LocalDate.now()) { - stringResource(id = R.string.next_period_start_past) - } else { - stringResource(id = R.string.next_period_start_future) - }, - periodPredictionDate.toString() - ) - - RowOfText( - stringResource(id = R.string.ovulation_count), - ovulationCount.toString() - ) - - RowOfText( - stringResource(id = R.string.average_ovulation_day), - follicleGrowthDays.toString() - ) - - RowOfText( - stringResource(id = R.string.next_predicted_ovulation), - //nextPredictedOvulation - ovulationPredictionDate.toString() - ) - - RowOfText( - stringResource(id = R.string.average_luteal_length), - (Math.round(avgLutealLength * 10) / 10.0).toString() + " " + stringResource(id = R.string.days) - ) - } -} - -@Composable -fun RowOfText(stringOne: String, stringTwo: String) { - Row( - modifier = Modifier - .fillMaxWidth() - .padding(vertical = 16.dp), - verticalAlignment = Alignment.CenterVertically, - horizontalArrangement = Arrangement.Start - ) { - Text( - text = stringOne, - fontSize = 18.sp - ) - Spacer(modifier = Modifier.weight(1f)) - Text( - text = stringTwo, - fontSize = 17.sp, - ) - } -} - -@Preview(showBackground = true) -@Composable -private fun RowOfTextPreview() { - RowOfText("firstString", "secondstring") -} \ No newline at end of file diff --git a/app/src/main/java/com/mensinator/app/extensions/DoubleExtensions.kt b/app/src/main/java/com/mensinator/app/extensions/DoubleExtensions.kt new file mode 100644 index 0000000..2e2f213 --- /dev/null +++ b/app/src/main/java/com/mensinator/app/extensions/DoubleExtensions.kt @@ -0,0 +1,13 @@ +package com.mensinator.app.extensions + +import java.util.Locale +import kotlin.math.round + +fun Double.formatToOneDecimalPoint(): String { + if (this.isNaN()) return "-" + return String.format(Locale.getDefault(), "%.1f", this) +} + +fun Double.roundToTwoDecimalPoints(): Double { + return (round(this * 100) / 100) +} \ No newline at end of file diff --git a/app/src/main/java/com/mensinator/app/navigation/MensinatorApp.kt b/app/src/main/java/com/mensinator/app/navigation/MensinatorApp.kt index 6767669..8567d91 100644 --- a/app/src/main/java/com/mensinator/app/navigation/MensinatorApp.kt +++ b/app/src/main/java/com/mensinator/app/navigation/MensinatorApp.kt @@ -28,6 +28,7 @@ import androidx.navigation.compose.rememberNavController import com.mensinator.app.* import com.mensinator.app.R import com.mensinator.app.settings.SettingsScreen +import com.mensinator.app.statistics.StatisticsScreen import org.koin.compose.koinInject enum class Screen(@StringRes val titleRes: Int) { diff --git a/app/src/main/java/com/mensinator/app/statistics/StatisticsScreen.kt b/app/src/main/java/com/mensinator/app/statistics/StatisticsScreen.kt new file mode 100644 index 0000000..7f7e22a --- /dev/null +++ b/app/src/main/java/com/mensinator/app/statistics/StatisticsScreen.kt @@ -0,0 +1,144 @@ +package com.mensinator.app.statistics + +import androidx.compose.foundation.layout.* +import androidx.compose.foundation.rememberScrollState +import androidx.compose.foundation.verticalScroll +import androidx.compose.material3.MaterialTheme +import androidx.compose.material3.Text +import androidx.compose.runtime.Composable +import androidx.compose.runtime.LaunchedEffect +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.res.stringResource +import androidx.compose.ui.text.style.TextAlign +import androidx.compose.ui.tooling.preview.Preview +import androidx.compose.ui.unit.dp +import androidx.lifecycle.compose.collectAsStateWithLifecycle +import com.mensinator.app.R +import com.mensinator.app.navigation.displayCutoutExcludingStatusBarsPadding +import com.mensinator.app.ui.theme.MensinatorTheme +import org.koin.androidx.compose.koinViewModel + +@Composable +fun StatisticsScreen( + modifier: Modifier = Modifier, + viewModel: StatisticsViewModel = koinViewModel(), +) { + val state = viewModel.viewState.collectAsStateWithLifecycle().value + + LaunchedEffect(Unit) { + viewModel.refreshData() + } + + StatisticsScreenContent(modifier, state) +} + +@Composable +private fun StatisticsScreenContent( + modifier: Modifier = Modifier, + state: StatisticsViewModel.ViewState +) { + Column( + modifier = modifier + .fillMaxSize() + .verticalScroll(rememberScrollState()) + .displayCutoutExcludingStatusBarsPadding() + .padding(horizontal = 16.dp) + ) { + RowOfText( + stringResource(id = R.string.period_count), + state.trackedPeriods + ) + RowOfText( + stringResource(id = R.string.average_cycle_length), + state.averageCycleLength + ) + RowOfText( + stringResource(id = R.string.average_period_length), + state.averagePeriodLength + ) + RowOfText( + stringResource(id = R.string.next_period_start_future), + state.periodPredictionDate + ) + RowOfText( + stringResource(id = R.string.ovulation_count), + state.ovulationCount + ) + RowOfText( + stringResource(id = R.string.average_ovulation_day), + state.follicleGrowthDays + ) + RowOfText( + stringResource(id = R.string.next_predicted_ovulation), + state.ovulationPredictionDate + ) + RowOfText( + stringResource(id = R.string.average_luteal_length), + state.averageLutealLength + ) + } +} + +@Composable +fun RowOfText(stringOne: String, stringTwo: String?) { + Row( + modifier = Modifier + .fillMaxWidth() + .padding(vertical = 16.dp), + verticalAlignment = Alignment.CenterVertically, + horizontalArrangement = Arrangement.Absolute.SpaceAround + ) { + Text( + text = stringOne, + modifier = Modifier + .weight(0.7f) + .padding(end = 8.dp), + ) + stringTwo?.let { + Text( + text = it, + modifier = Modifier.alignByBaseline().widthIn(max = 200.dp), + style = MaterialTheme.typography.titleMedium, + textAlign = TextAlign.End + ) + } + } +} + +@Preview(showBackground = true) +@Composable +private fun RowOfTextPreview() { + RowOfText("firstString", "secondstring") +} + +@Preview(showBackground = true) +@Composable +private fun RowOfTextLongPreview() { + RowOfText("Very long first string, we could even use lorem ipsum here", "secondstring") +} + +@Preview(showBackground = true) +@Composable +private fun RowOfTextLongSecondPreview() { + RowOfText("Short Text", "first string, we could even use lorem ipsum here") +} + +@Preview(showBackground = true) +@Composable +private fun StatisticsScreenPreview() { + MensinatorTheme { + StatisticsScreenContent( + state = StatisticsViewModel.ViewState( + trackedPeriods = "3", + averageCycleLength = "28.5 days", + averagePeriodLength = "5.0 days", + periodPredictionDate = "28 Feb 2024", + ovulationCount = "4", + ovulationPredictionDate = "20 Mar 2024", + follicleGrowthDays = "14.0", + averageLutealLength = "15.0 days" + ) + ) + } +} \ No newline at end of file diff --git a/app/src/main/java/com/mensinator/app/statistics/StatisticsViewModel.kt b/app/src/main/java/com/mensinator/app/statistics/StatisticsViewModel.kt new file mode 100644 index 0000000..6696884 --- /dev/null +++ b/app/src/main/java/com/mensinator/app/statistics/StatisticsViewModel.kt @@ -0,0 +1,60 @@ +package com.mensinator.app.statistics + +import android.annotation.SuppressLint +import android.content.Context +import androidx.lifecycle.ViewModel +import com.mensinator.app.* +import com.mensinator.app.extensions.formatToOneDecimalPoint +import kotlinx.coroutines.flow.MutableStateFlow +import kotlinx.coroutines.flow.StateFlow +import kotlinx.coroutines.flow.asStateFlow +import kotlinx.coroutines.flow.update +import java.time.format.DateTimeFormatter +import java.time.format.FormatStyle + +class StatisticsViewModel( + @SuppressLint("StaticFieldLeak") private val appContext: Context, + private val periodDatabaseHelper: IPeriodDatabaseHelper, + private val calcHelper: ICalculationsHelper, + private val ovulationPrediction: IOvulationPrediction, + private val periodPrediction: IPeriodPrediction, +) : ViewModel() { + + private val dateFormatter = DateTimeFormatter.ofLocalizedDate(FormatStyle.MEDIUM) + + private val _viewState = MutableStateFlow( + ViewState() + ) + val viewState: StateFlow = _viewState.asStateFlow() + + data class ViewState( + val trackedPeriods: String? = null, + val averageCycleLength: String? = null, + val averagePeriodLength: String? = null, + val averageLutealLength: String? = null, + val follicleGrowthDays: String? = null, + val ovulationPredictionDate: String? = null, + val periodPredictionDate: String? = null, + val ovulationCount: String? = null, + ) + + fun refreshData() { + _viewState.update { + it.copy( + trackedPeriods = periodDatabaseHelper.getPeriodCount().toString(), + averageCycleLength = formatDays(calcHelper.averageCycleLength().formatToOneDecimalPoint()), + averagePeriodLength = formatDays(calcHelper.averagePeriodLength().formatToOneDecimalPoint()), + averageLutealLength =formatDays(calcHelper.averageLutealLength().formatToOneDecimalPoint()), + follicleGrowthDays = calcHelper.averageFollicalGrowthInDays().formatToOneDecimalPoint(), + ovulationPredictionDate = ovulationPrediction.getPredictedOvulationDate().format(dateFormatter), + periodPredictionDate = periodPrediction.getPredictedPeriodDate().format(dateFormatter), + ovulationCount = periodDatabaseHelper.getOvulationCount().toString() + ) + } + } + + private fun formatDays(text: String): String { + val days = appContext.getString(R.string.days) + return "$text $days" + } +} diff --git a/app/src/main/java/com/mensinator/app/ui/theme/Theme.kt b/app/src/main/java/com/mensinator/app/ui/theme/Theme.kt index afae6d5..b0e4a04 100644 --- a/app/src/main/java/com/mensinator/app/ui/theme/Theme.kt +++ b/app/src/main/java/com/mensinator/app/ui/theme/Theme.kt @@ -54,7 +54,6 @@ fun MensinatorTheme( MaterialTheme( colorScheme = colorScheme, - typography = Typography, shapes = Shapes, content = content ) diff --git a/app/src/main/java/com/mensinator/app/ui/theme/Type.kt b/app/src/main/java/com/mensinator/app/ui/theme/Type.kt deleted file mode 100644 index 351f9b6..0000000 --- a/app/src/main/java/com/mensinator/app/ui/theme/Type.kt +++ /dev/null @@ -1,34 +0,0 @@ -package com.mensinator.app.ui.theme - -import androidx.compose.material3.Typography -import androidx.compose.ui.text.TextStyle -import androidx.compose.ui.text.font.FontFamily -import androidx.compose.ui.text.font.FontWeight -import androidx.compose.ui.unit.sp - -// Set of Material typography styles to start with -val Typography = Typography( - bodyLarge = TextStyle( - fontFamily = FontFamily.Default, - fontWeight = FontWeight.Normal, - fontSize = 16.sp, - lineHeight = 24.sp, - letterSpacing = 0.5.sp - ) - /* Other default text styles to override - titleLarge = TextStyle( - fontFamily = FontFamily.Default, - fontWeight = FontWeight.Normal, - fontSize = 22.sp, - lineHeight = 28.sp, - letterSpacing = 0.sp - ), - labelSmall = TextStyle( - fontFamily = FontFamily.Default, - fontWeight = FontWeight.Medium, - fontSize = 11.sp, - lineHeight = 16.sp, - letterSpacing = 0.5.sp - ) - */ -) \ No newline at end of file