From 3645b92a8209b6862f149e9774acbe700c5ce260 Mon Sep 17 00:00:00 2001 From: "Andres Gomez, Thomas (ITDV RL)" Date: Tue, 23 Sep 2025 14:16:31 +0200 Subject: [PATCH] Add a yeary stats screen --- .idea/deploymentTargetSelector.xml | 6 +- app/release/baselineProfiles/0/app-release.dm | Bin 4550 -> 4585 bytes app/release/baselineProfiles/1/app-release.dm | Bin 4502 -> 4537 bytes .../repository/event/EventRepository.kt | 25 +- .../destination/ReportDestination.kt | 19 ++ .../ui/navigation/home/HomeNavDisplay.kt | 2 + .../ui/page/event/edit/EventEditFactory.kt | 4 +- .../headache/ui/page/home/HomePage.kt | 31 ++- .../summary/monthly/MonthSummaryFactory.kt | 136 ++++------ .../page/summary/monthly/MonthSummaryPage.kt | 142 ++--------- .../summary/monthly/MonthSummaryViewModel.kt | 35 +-- .../summary/monthly/item/MonthSummaryBox.kt | 174 ------------- .../summary/monthly/item/MonthSummaryCell.kt | 9 - .../summary/monthly/item/MonthSummaryItem.kt | 4 +- .../summary/monthly/item/MonthSummaryTitle.kt | 4 +- .../ui/page/summary/report/ReportBox.kt | 229 +++++++++++++++++ .../ui/page/summary/report/ReportFactory.kt | 59 +++++ .../ui/page/summary/report/ReportPage.kt | 237 ++++++++++++++++++ .../ui/page/summary/report/ReportViewModel.kt | 28 +++ .../page/summary/yearly/YearSummaryFactory.kt | 8 +- .../page/summary/yearly/YearSummaryMonth.kt | 2 +- .../ui/page/summary/yearly/YearSummaryPage.kt | 34 +-- .../ui/theme/color/HeadacheColorPalette.kt | 14 +- .../headache/ui/theme/color/HeadacheColors.kt | 6 +- 24 files changed, 744 insertions(+), 464 deletions(-) create mode 100644 app/src/main/java/com/pixelized/headache/ui/navigation/destination/ReportDestination.kt delete mode 100644 app/src/main/java/com/pixelized/headache/ui/page/summary/monthly/item/MonthSummaryBox.kt delete mode 100644 app/src/main/java/com/pixelized/headache/ui/page/summary/monthly/item/MonthSummaryCell.kt create mode 100644 app/src/main/java/com/pixelized/headache/ui/page/summary/report/ReportBox.kt create mode 100644 app/src/main/java/com/pixelized/headache/ui/page/summary/report/ReportFactory.kt create mode 100644 app/src/main/java/com/pixelized/headache/ui/page/summary/report/ReportPage.kt create mode 100644 app/src/main/java/com/pixelized/headache/ui/page/summary/report/ReportViewModel.kt diff --git a/.idea/deploymentTargetSelector.xml b/.idea/deploymentTargetSelector.xml index c925b19..c9a3bbe 100644 --- a/.idea/deploymentTargetSelector.xml +++ b/.idea/deploymentTargetSelector.xml @@ -8,15 +8,15 @@ - - + + \ No newline at end of file diff --git a/app/release/baselineProfiles/0/app-release.dm b/app/release/baselineProfiles/0/app-release.dm index 6b396b61f9f22fa32abe0f308b7194810d7e8c8c..12141fa8edebe43e62c447f13ee20e87cb6899dd 100644 GIT binary patch literal 4585 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 diff --git a/app/release/baselineProfiles/1/app-release.dm b/app/release/baselineProfiles/1/app-release.dm index 2b85bed476ee9ad0307dca1020919e2582b2a6f0..9c9005bdcc923430abd7fd7ced9e3e2c31206c64 100644 GIT binary patch literal 4537 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 diff --git a/app/src/main/java/com/pixelized/headache/repository/event/EventRepository.kt b/app/src/main/java/com/pixelized/headache/repository/event/EventRepository.kt index 062f632..ed43d79 100644 --- a/app/src/main/java/com/pixelized/headache/repository/event/EventRepository.kt +++ b/app/src/main/java/com/pixelized/headache/repository/event/EventRepository.kt @@ -47,9 +47,9 @@ class EventRepository @Inject constructor( ) // Collectable data. Provide event data in a nested map struct of year, month, day. - private val eventMapFlow: StateFlow>>> = eventListFlow + private val eventMapFlow: StateFlow>>>> = eventListFlow .map { events -> - events.fold(initial = hashMapOf>>()) { acc, event -> + events.fold(initial = hashMapOf>>>()) { acc, event -> acc.also { val years = it.getOrPut(key = event.date.year) { hashMapOf( @@ -68,7 +68,8 @@ class EventRepository @Inject constructor( ) } val months = years.getOrPut(key = event.date.month) { hashMapOf() } - months[event.date.day] = event + val events = months.getOrPut(key = event.date.day) { mutableListOf() } + events.add(event) } } } @@ -78,6 +79,20 @@ class EventRepository @Inject constructor( initialValue = emptyMap(), ) + private val maxPillAmountPerMonth: StateFlow = eventMapFlow + .map { events -> + events.values.maxOf { yearEntry: Map>> -> + yearEntry.values.maxOf { monthEntry -> + monthEntry.values.sumOf { events -> events.sumOf { it.pills.size } } + } + } + } + .stateIn( + scope = scope, + started = SharingStarted.Lazily, + initialValue = 0, + ) + init { calendarIdRepository.calendarId .onEach { id -> @@ -92,7 +107,9 @@ class EventRepository @Inject constructor( fun eventsListFlow(): StateFlow> = eventListFlow - fun eventsMapFlow(): StateFlow>>> = eventMapFlow + fun eventsMapFlow(): StateFlow>>>> = eventMapFlow + + fun maxPillAmountPerMonthFlow(): StateFlow = maxPillAmountPerMonth fun event(id: Long): Event? = eventFlow.value.get(key = id) diff --git a/app/src/main/java/com/pixelized/headache/ui/navigation/destination/ReportDestination.kt b/app/src/main/java/com/pixelized/headache/ui/navigation/destination/ReportDestination.kt new file mode 100644 index 0000000..00a7503 --- /dev/null +++ b/app/src/main/java/com/pixelized/headache/ui/navigation/destination/ReportDestination.kt @@ -0,0 +1,19 @@ +package com.pixelized.headache.ui.navigation.destination + +import androidx.navigation3.runtime.EntryProviderBuilder +import androidx.navigation3.runtime.entry +import com.pixelized.headache.ui.navigation.home.HomeDestination +import com.pixelized.headache.ui.navigation.home.HomeNavigator +import com.pixelized.headache.ui.page.summary.report.ReportPage + +data object ReportDestination : HomeDestination + +fun EntryProviderBuilder<*>.reportDestinationEntry() { + entry { + ReportPage() + } +} + +fun HomeNavigator.navigateToReport() { + goTo(ReportDestination) +} \ No newline at end of file diff --git a/app/src/main/java/com/pixelized/headache/ui/navigation/home/HomeNavDisplay.kt b/app/src/main/java/com/pixelized/headache/ui/navigation/home/HomeNavDisplay.kt index ec6204b..cd8d92b 100644 --- a/app/src/main/java/com/pixelized/headache/ui/navigation/home/HomeNavDisplay.kt +++ b/app/src/main/java/com/pixelized/headache/ui/navigation/home/HomeNavDisplay.kt @@ -13,6 +13,7 @@ import com.pixelized.headache.ui.navigation.destination.calendarChooserDestinati import com.pixelized.headache.ui.navigation.destination.eventDestinationEntry import com.pixelized.headache.ui.navigation.destination.homeDestinationEntry import com.pixelized.headache.ui.navigation.destination.monthSummaryDestinationEntry +import com.pixelized.headache.ui.navigation.destination.reportDestinationEntry import com.pixelized.headache.ui.navigation.destination.yearSummaryDestinationEntry val LocalHomeNavigator = staticCompositionLocalOf { @@ -41,6 +42,7 @@ fun HomeNavDisplay( entryProvider = entryProvider { monthSummaryDestinationEntry() yearSummaryDestinationEntry() + reportDestinationEntry() } ) } diff --git a/app/src/main/java/com/pixelized/headache/ui/page/event/edit/EventEditFactory.kt b/app/src/main/java/com/pixelized/headache/ui/page/event/edit/EventEditFactory.kt index 6e90490..802e8a3 100644 --- a/app/src/main/java/com/pixelized/headache/ui/page/event/edit/EventEditFactory.kt +++ b/app/src/main/java/com/pixelized/headache/ui/page/event/edit/EventEditFactory.kt @@ -67,13 +67,13 @@ class EventEditFactory @Inject constructor() { EventPillEditUio( id = Event.Pill.Id.IBUPROFENE_400.value, color = HeadacheColorPalette.Pill.Ibuprofene400, - label = "Paracétamol 1000", + label = "Ibuprofène 400", amount = ibuprofeneAmount, ), EventPillEditUio( id = Event.Pill.Id.PARACETAMOL_1000.value, color = HeadacheColorPalette.Pill.Paracetamol1000, - label = "Ibuprofène 400", + label = "Paracétamol 1000", amount = paracetamolAmount, ), EventPillEditUio( diff --git a/app/src/main/java/com/pixelized/headache/ui/page/home/HomePage.kt b/app/src/main/java/com/pixelized/headache/ui/page/home/HomePage.kt index dbf6aa7..69e2379 100644 --- a/app/src/main/java/com/pixelized/headache/ui/page/home/HomePage.kt +++ b/app/src/main/java/com/pixelized/headache/ui/page/home/HomePage.kt @@ -21,26 +21,26 @@ import androidx.compose.material3.FloatingActionButton import androidx.compose.material3.Icon import androidx.compose.material3.MaterialTheme import androidx.compose.material3.NavigationBar -import androidx.compose.material3.NavigationBarDefaults import androidx.compose.material3.NavigationBarItem import androidx.compose.material3.Scaffold import androidx.compose.material3.Text -import androidx.compose.material3.TopAppBar -import androidx.compose.material3.TopAppBarDefaults import androidx.compose.runtime.Composable -import androidx.compose.runtime.IntState import androidx.compose.runtime.Stable -import androidx.compose.runtime.mutableIntStateOf +import androidx.compose.runtime.State +import androidx.compose.runtime.derivedStateOf import androidx.compose.runtime.remember import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.res.painterResource -import androidx.compose.ui.res.stringResource import androidx.compose.ui.unit.Dp import androidx.compose.ui.unit.dp import androidx.hilt.navigation.compose.hiltViewModel import com.pixelized.headache.R +import com.pixelized.headache.ui.navigation.destination.MonthSummaryDestination +import com.pixelized.headache.ui.navigation.destination.ReportDestination +import com.pixelized.headache.ui.navigation.destination.YearSummaryDestination import com.pixelized.headache.ui.navigation.destination.navigateToMonthSummary +import com.pixelized.headache.ui.navigation.destination.navigateToReport import com.pixelized.headache.ui.navigation.destination.navigateToYearSummary import com.pixelized.headache.ui.navigation.home.HomeNavDisplay import com.pixelized.headache.ui.navigation.home.HomeNavigator @@ -61,18 +61,25 @@ fun HomePage( navigator: HomeNavigator, editViewModel: EventEditBottomSheetViewModel = hiltViewModel(), ) { - val selectedItem = remember { mutableIntStateOf(0) } + val selectedItem = remember { + derivedStateOf { + when (navigator.backStack.last()) { + is YearSummaryDestination -> 0 + is ReportDestination -> 1 + is MonthSummaryDestination -> 2 + else -> -1 + } + } + } val items = rememberBottomBarItems( onYearlyFollowUp = { - selectedItem.intValue = 0 navigator.navigateToYearSummary() }, onMonthlyStatFollowUp = { - selectedItem.intValue = 1 - navigator.navigateToMonthSummary() + navigator.navigateToReport() }, onMonthlyListFollowUp = { - selectedItem.intValue = 2 + navigator.navigateToMonthSummary() }, ) @@ -123,7 +130,7 @@ private fun HomePageContent( modifier: Modifier = Modifier, navigator: HomeNavigator, items: List, - selectedItem: IntState, + selectedItem: State, onFabClick: () -> Unit, ) { Scaffold( diff --git a/app/src/main/java/com/pixelized/headache/ui/page/summary/monthly/MonthSummaryFactory.kt b/app/src/main/java/com/pixelized/headache/ui/page/summary/monthly/MonthSummaryFactory.kt index 2fd478a..b25e9ac 100644 --- a/app/src/main/java/com/pixelized/headache/ui/page/summary/monthly/MonthSummaryFactory.kt +++ b/app/src/main/java/com/pixelized/headache/ui/page/summary/monthly/MonthSummaryFactory.kt @@ -1,106 +1,78 @@ package com.pixelized.headache.ui.page.summary.monthly +import android.icu.text.Collator import android.icu.util.Calendar import androidx.compose.ui.graphics.Color +import androidx.compose.ui.text.intl.Locale import com.pixelized.headache.repository.event.Event -import com.pixelized.headache.ui.page.summary.monthly.item.MonthSummaryBoxUio -import com.pixelized.headache.ui.page.summary.monthly.item.MonthSummaryCell import com.pixelized.headache.ui.page.summary.monthly.item.MonthSummaryItemUio import com.pixelized.headache.ui.page.summary.monthly.item.MonthSummaryPillItemUio import com.pixelized.headache.ui.page.summary.monthly.item.MonthSummaryTitleUio -import com.pixelized.headache.utils.extention.event import javax.inject.Inject -import kotlin.math.max class MonthSummaryFactory @Inject constructor() { private val calendar = Calendar.getInstance() + private val locale = Locale.current.let { + java.util.Locale.forLanguageTag(it.toLanguageTag()) + } fun convertToItemUio( - events: Collection, - ): Map> { + events: Map>>>, + ): Map> { return events - .fold(hashMapOf>()) { acc, event -> - acc.also { it.getOrPut(event.date.copy(day = 1)) { mutableListOf() }.add(event) } - } - .map { entry -> - val pills = entry.value - .fold(hashMapOf()) { acc, event -> - event.pills.forEach { pill -> - val value = acc.getOrElse( - key = pill.id, - defaultValue = { - EventFolder( - label = pill.label, - amount = 0, - color = pill.color, + .flatMap { yearEntry -> + yearEntry.value.map { monthEntry -> + val pills = monthEntry.value.values + .fold( + initial = hashMapOf(), + ) { acc, events -> + events.map { event -> + event.pills.forEach { pill -> + val value = acc.getOrElse( + key = pill.id, + defaultValue = { + PillFolder(label = pill.label, color = pill.color) + }, ) - }, - ) - value.amount += 1 - acc[pill.id] = value + value.amount += pill.amount + acc[pill.id] = value + } + } + acc } - acc - } - .map { entry -> - MonthSummaryPillItemUio( - label = entry.value.label, - color = entry.value.color, - amount = entry.value.amount, - ) - } - .toList() + .map { entry -> + MonthSummaryPillItemUio( + label = entry.value.label, + color = entry.value.color, + amount = entry.value.amount, + ) + } + .toList() + .sortedWith(Comparator { s1, s2 -> + Collator.getInstance(locale).compare(s1.label, s2.label) + }) - MonthSummaryItemUio( - date = calendar.apply { event = entry.key }.time, - days = entry.value.size, - pills = pills, - ) - } - .sortedByDescending { it.date } - .groupByMonth() - } - - fun convertToBoxUio( - events: Collection, - ): Map> { - var maxPillAmount = 0 - return events - .fold(hashMapOf>()) { acc, event -> - acc.also { it.getOrPut(event.date.copy(day = 1)) { mutableListOf() }.add(event) } - } - .mapKeys { entry -> - maxPillAmount = max( - entry.value.sumOf { events -> events.pills.sumOf { pill -> pill.amount } }, - maxPillAmount, - ) - entry.key - } - .map { entry -> - val pillAmount = entry.value.sumOf { events -> - events.pills.sumOf { pill -> pill.amount } + MonthSummaryItemUio( + date = calendar.apply { + set(Calendar.YEAR, yearEntry.key) + set(Calendar.MONTH, monthEntry.key) + }.time, + days = monthEntry.value.values.sumOf { it.size }, + pills = pills, + ) } - val monthMaxDay = calendar.apply { - event = entry.key - add(Calendar.MONTH, 1) - add(Calendar.DAY_OF_YEAR, -1) - }.get(Calendar.DAY_OF_MONTH) - - MonthSummaryBoxUio( - date = calendar.apply { event = entry.key }.time, - headacheRatio = entry.value.size.toFloat() / monthMaxDay, - headacheAmount = entry.value.size, - headacheColor = Color.Companion.Red, - pillRatio = pillAmount.takeIf { it > 0 } - ?.let { it.toFloat() / maxPillAmount.toFloat() }, - pillAmount = pillAmount, - pillColor = Color.Companion.Blue, - ) } - .sortedByDescending { it.date } .groupByMonth() + .toSortedMap{s1, s2 -> + when{ + s1.date < s2.date -> 1 + s1.date > s2.date -> -1 + else -> 0 + } + } } - fun List.groupByMonth(): Map> { + fun List.groupByMonth(): Map> { return this.groupBy { MonthSummaryTitleUio( date = calendar.apply { @@ -112,9 +84,9 @@ class MonthSummaryFactory @Inject constructor() { } } - private class EventFolder( + private class PillFolder( val label: String, val color: Color, - var amount: Int, + var amount: Int = 0, ) } \ No newline at end of file diff --git a/app/src/main/java/com/pixelized/headache/ui/page/summary/monthly/MonthSummaryPage.kt b/app/src/main/java/com/pixelized/headache/ui/page/summary/monthly/MonthSummaryPage.kt index c2b4f9b..efa3bfb 100644 --- a/app/src/main/java/com/pixelized/headache/ui/page/summary/monthly/MonthSummaryPage.kt +++ b/app/src/main/java/com/pixelized/headache/ui/page/summary/monthly/MonthSummaryPage.kt @@ -1,23 +1,13 @@ package com.pixelized.headache.ui.page.summary.monthly -import androidx.compose.animation.AnimatedContent -import androidx.compose.animation.fadeIn -import androidx.compose.animation.fadeOut -import androidx.compose.animation.togetherWith import androidx.compose.foundation.layout.Arrangement import androidx.compose.foundation.layout.PaddingValues import androidx.compose.foundation.layout.WindowInsets import androidx.compose.foundation.layout.fillMaxSize -import androidx.compose.foundation.layout.fillMaxWidth import androidx.compose.foundation.layout.padding import androidx.compose.foundation.lazy.LazyColumn import androidx.compose.foundation.lazy.items -import androidx.compose.material.icons.Icons -import androidx.compose.material.icons.automirrored.filled.Article -import androidx.compose.material.icons.filled.BarChart import androidx.compose.material3.ExperimentalMaterial3Api -import androidx.compose.material3.Icon -import androidx.compose.material3.IconButton import androidx.compose.material3.NavigationBarDefaults import androidx.compose.material3.Scaffold import androidx.compose.material3.Text @@ -29,7 +19,6 @@ import androidx.compose.runtime.State import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.remember import androidx.compose.ui.Modifier -import androidx.compose.ui.graphics.Color import androidx.compose.ui.keepScreenOn import androidx.compose.ui.res.stringResource import androidx.compose.ui.tooling.preview.Preview @@ -40,9 +29,6 @@ import androidx.lifecycle.compose.collectAsStateWithLifecycle import com.pixelized.headache.R import com.pixelized.headache.ui.navigation.destination.navigateToEventPage import com.pixelized.headache.ui.navigation.main.LocalMainNavigator -import com.pixelized.headache.ui.page.summary.monthly.item.MonthSummaryBox -import com.pixelized.headache.ui.page.summary.monthly.item.MonthSummaryBoxUio -import com.pixelized.headache.ui.page.summary.monthly.item.MonthSummaryCell import com.pixelized.headache.ui.page.summary.monthly.item.MonthSummaryItem import com.pixelized.headache.ui.page.summary.monthly.item.MonthSummaryItemUio import com.pixelized.headache.ui.page.summary.monthly.item.MonthSummaryPillItemUio @@ -69,7 +55,6 @@ fun MonthSummaryPage( viewModel: MonthSummaryViewModel = hiltViewModel(), ) { val navigation = LocalMainNavigator.current - val boxMode = viewModel.boxMode.collectAsStateWithLifecycle() val events = viewModel.events.collectAsStateWithLifecycle() MonthSummaryContent( @@ -77,10 +62,6 @@ fun MonthSummaryPage( .keepScreenOn() .fillMaxSize(), events = events, - boxMode = boxMode, - onDisplay = { - viewModel.toggleDisplay() - }, onItem = { navigation.navigateToEventPage(date = it.date) }, @@ -93,10 +74,8 @@ private fun MonthSummaryContent( modifier: Modifier = Modifier, spacing: Dp = MonthSummaryPageDefault.spacing, listPadding: PaddingValues = MonthSummaryPageDefault.listPadding, - boxMode: State, - events: State>>, - onDisplay: () -> Unit, - onItem: (MonthSummaryCell) -> Unit, + events: State>>, + onItem: (MonthSummaryItemUio) -> Unit, ) { Scaffold( modifier = modifier, @@ -109,24 +88,6 @@ private fun MonthSummaryContent( title = { Text(text = stringResource(R.string.month_summary_title)) }, - actions = { - AnimatedContent( - targetState = boxMode.value, - transitionSpec = { fadeIn() togetherWith fadeOut() }, - ) { - IconButton( - onClick = onDisplay, - ) { - Icon( - imageVector = when (it) { - true -> Icons.AutoMirrored.Filled.Article - else -> Icons.Default.BarChart - }, - contentDescription = null, - ) - } - } - } ) }, content = { paddingValues -> @@ -140,24 +101,16 @@ private fun MonthSummaryContent( ) { events.value.forEach { entry -> item { - MonthSummaryCell( + MonthSummaryTitle( modifier = Modifier.padding(top = 16.dp), item = entry.key, - onItem = onItem, ) } items( items = entry.value, key = { item -> item.date }, - contentType = { item -> - when (item) { - is MonthSummaryBoxUio -> "MonthSummaryBoxUio" - is MonthSummaryItemUio -> "MonthSummaryItemUio" - is MonthSummaryTitleUio -> "MonthSummaryTitleUio" - } - }, ) { item -> - MonthSummaryCell( + MonthSummaryItem( item = item, onItem = onItem, ) @@ -168,79 +121,38 @@ private fun MonthSummaryContent( ) } -@Composable -private fun MonthSummaryCell( - modifier: Modifier = Modifier, - item: MonthSummaryCell, - onItem: (MonthSummaryCell) -> Unit, -) { - AnimatedContent( - modifier = Modifier - .fillMaxWidth() - .then(other = modifier), - targetState = item, - transitionSpec = { fadeIn() togetherWith fadeOut() }, - ) { item -> - when (item) { - is MonthSummaryTitleUio -> MonthSummaryTitle( - item = item, - ) - - is MonthSummaryBoxUio -> MonthSummaryBox( - item = item, - onItem = onItem, - ) - - is MonthSummaryItemUio -> MonthSummaryItem( - item = item, - onItem = onItem, - ) - } - } -} - @Composable @Preview private fun MonthSummaryPreview() { HeadacheTheme { - MonthSummaryContent( - boxMode = remember { mutableStateOf(false) }, - events = remember { - mutableStateOf( - mapOf( - MonthSummaryTitleUio( + val events = remember { + mutableStateOf( + mapOf( + MonthSummaryTitleUio( + date = Date(), + ) to listOf( + MonthSummaryItemUio( date = Date(), - ) to listOf( - MonthSummaryItemUio( - date = Date(), - days = 8, - pills = listOf( - MonthSummaryPillItemUio( - label = "Spifen 400", - amount = 4, - color = HeadacheColorPalette.Pill.Spifen400, - ), - MonthSummaryPillItemUio( - label = "Élétriptan 40", - amount = 2, - color = HeadacheColorPalette.Pill.Eletriptan40, - ), + days = 8, + pills = listOf( + MonthSummaryPillItemUio( + label = "Spifen 400", + amount = 4, + color = HeadacheColorPalette.Pill.Spifen400, + ), + MonthSummaryPillItemUio( + label = "Élétriptan 40", + amount = 2, + color = HeadacheColorPalette.Pill.Eletriptan40, ), ), - MonthSummaryBoxUio( - date = Date(), - headacheRatio = 8f / 30f, - headacheAmount = 8, - headacheColor = Color.Red, - pillRatio = 6f / 20f, - pillAmount = 6, - pillColor = Color.Blue, - ), ), - ) + ), ) - }, - onDisplay = { }, + ) + } + MonthSummaryContent( + events = events, onItem = { }, ) } diff --git a/app/src/main/java/com/pixelized/headache/ui/page/summary/monthly/MonthSummaryViewModel.kt b/app/src/main/java/com/pixelized/headache/ui/page/summary/monthly/MonthSummaryViewModel.kt index 06c3d71..6645d12 100644 --- a/app/src/main/java/com/pixelized/headache/ui/page/summary/monthly/MonthSummaryViewModel.kt +++ b/app/src/main/java/com/pixelized/headache/ui/page/summary/monthly/MonthSummaryViewModel.kt @@ -2,15 +2,11 @@ package com.pixelized.headache.ui.page.summary.monthly import androidx.lifecycle.ViewModel import androidx.lifecycle.viewModelScope -import com.pixelized.headache.repository.event.Event import com.pixelized.headache.repository.event.EventRepository -import com.pixelized.headache.ui.page.summary.monthly.item.MonthSummaryCell import dagger.hilt.android.lifecycle.HiltViewModel import kotlinx.coroutines.ExperimentalCoroutinesApi -import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.flow.SharingStarted -import kotlinx.coroutines.flow.StateFlow -import kotlinx.coroutines.flow.combine +import kotlinx.coroutines.flow.map import kotlinx.coroutines.flow.stateIn import javax.inject.Inject @@ -20,26 +16,13 @@ class MonthSummaryViewModel @Inject constructor( eventItemFactory: MonthSummaryFactory, ) : ViewModel() { - private val displayTypeFlow = MutableStateFlow(false) - val boxMode: StateFlow = displayTypeFlow - @OptIn(ExperimentalCoroutinesApi::class) - val events: StateFlow>> = combine( - eventRepository.eventsListFlow(), - displayTypeFlow, - transform = { events: Collection, display -> - when (display) { - true -> eventItemFactory.convertToBoxUio(events = events) - else -> eventItemFactory.convertToItemUio(events = events) - } - } - ).stateIn( - scope = viewModelScope, - started = SharingStarted.Lazily, - initialValue = emptyMap(), - ) - - fun toggleDisplay() { - displayTypeFlow.value = displayTypeFlow.value.not() - } + val events = eventRepository.eventsMapFlow() + .map { events -> + eventItemFactory.convertToItemUio(events = events) + }.stateIn( + scope = viewModelScope, + started = SharingStarted.Lazily, + initialValue = emptyMap(), + ) } \ No newline at end of file diff --git a/app/src/main/java/com/pixelized/headache/ui/page/summary/monthly/item/MonthSummaryBox.kt b/app/src/main/java/com/pixelized/headache/ui/page/summary/monthly/item/MonthSummaryBox.kt deleted file mode 100644 index 99f51ba..0000000 --- a/app/src/main/java/com/pixelized/headache/ui/page/summary/monthly/item/MonthSummaryBox.kt +++ /dev/null @@ -1,174 +0,0 @@ -package com.pixelized.headache.ui.page.summary.monthly.item - -import android.annotation.SuppressLint -import android.icu.text.SimpleDateFormat -import android.icu.util.Calendar -import androidx.compose.foundation.background -import androidx.compose.foundation.clickable -import androidx.compose.foundation.layout.Arrangement -import androidx.compose.foundation.layout.Box -import androidx.compose.foundation.layout.Column -import androidx.compose.foundation.layout.PaddingValues -import androidx.compose.foundation.layout.Row -import androidx.compose.foundation.layout.fillMaxWidth -import androidx.compose.foundation.layout.height -import androidx.compose.foundation.layout.padding -import androidx.compose.foundation.layout.width -import androidx.compose.material3.MaterialTheme -import androidx.compose.material3.Surface -import androidx.compose.material3.Text -import androidx.compose.runtime.Composable -import androidx.compose.runtime.Stable -import androidx.compose.ui.Alignment -import androidx.compose.ui.Modifier -import androidx.compose.ui.graphics.Color -import androidx.compose.ui.text.style.TextAlign -import androidx.compose.ui.tooling.preview.Preview -import androidx.compose.ui.tooling.preview.PreviewParameter -import androidx.compose.ui.tooling.preview.PreviewParameterProvider -import androidx.compose.ui.unit.Dp -import androidx.compose.ui.unit.DpSize -import androidx.compose.ui.unit.dp -import com.pixelized.headache.ui.theme.HeadacheTheme -import com.pixelized.headache.utils.extention.capitalize -import java.util.Date -import java.util.Locale - -@Stable -data object MonthSummaryBoxDefault { - @Stable - val padding: PaddingValues = PaddingValues(horizontal = 16.dp, vertical = 8.dp) - - @Stable - val spacing: DpSize = DpSize(width = 8.dp, height = 4.dp) - - @Stable - val labelWidth: Dp = 32.dp + 16.dp - - @Stable - val boxHeight: Dp = 16.dp - - @Stable - val boxPaddingValues: PaddingValues = PaddingValues(horizontal = 4.dp) - - @SuppressLint("ConstantLocale") - @Stable - val formatter = SimpleDateFormat("MMM", Locale.getDefault()) -} - -@Stable -data class MonthSummaryBoxUio( - override val date: Date, - val headacheRatio: Float, - val headacheAmount: Int, - val headacheColor: Color, - val pillRatio: Float?, - val pillAmount: Int, - val pillColor: Color, -) : MonthSummaryCell - -@Composable -fun MonthSummaryBox( - modifier: Modifier = Modifier, - padding: PaddingValues = MonthSummaryBoxDefault.padding, - spacing: DpSize = MonthSummaryBoxDefault.spacing, - boxPaddingValues: PaddingValues = MonthSummaryBoxDefault.boxPaddingValues, - labelWidth: Dp = MonthSummaryBoxDefault.labelWidth, - boxHeight: Dp = MonthSummaryBoxDefault.boxHeight, - formatter: SimpleDateFormat = MonthSummaryBoxDefault.formatter, - item: MonthSummaryBoxUio, - onItem: (MonthSummaryBoxUio) -> Unit, -) { - Row( - modifier = Modifier - .clickable { onItem(item) } - .padding(paddingValues = padding) - .then(other = modifier), - horizontalArrangement = Arrangement.spacedBy(space = spacing.width), - verticalAlignment = Alignment.CenterVertically, - ) { - Text( - modifier = Modifier.width(width = labelWidth), - style = MaterialTheme.typography.bodyMedium, - textAlign = TextAlign.End, - text = formatter.format(item.date).capitalize(), - ) - Column( - verticalArrangement = Arrangement.spacedBy(space = spacing.height), - ) { - Box( - modifier = Modifier - .background(color = item.headacheColor) - .height(height = boxHeight) - .fillMaxWidth(fraction = item.headacheRatio) - .padding(paddingValues = boxPaddingValues), - contentAlignment = Alignment.CenterStart, - ) { - Text( - style = MaterialTheme.typography.labelSmall, - color = Color.White, - text = "${item.headacheAmount}" - ) - } - item.pillRatio?.let { - Box( - modifier = Modifier - .background(color = item.pillColor) - .height(height = boxHeight) - .fillMaxWidth(fraction = it) - .padding(paddingValues = boxPaddingValues), - ) { - Text( - style = MaterialTheme.typography.labelSmall, - color = Color.White, - text = "${item.pillAmount}" - ) - } - } - } - } -} - -@Composable -@Preview -private fun MonthSummaryBoxPreview( - @PreviewParameter(BoxPreviewProvider::class) preview: MonthSummaryBoxUio, -) { - HeadacheTheme { - Surface { - MonthSummaryBox( - modifier = Modifier.fillMaxWidth(), - item = preview, - onItem = { }, - ) - } - } -} - -private class BoxPreviewProvider : PreviewParameterProvider { - val calendar = Calendar.getInstance().apply { - time = Date() - set(Calendar.DAY_OF_MONTH, 1) - } - override val values: Sequence - get() = sequenceOf( - MonthSummaryBoxUio( - date = calendar.apply { set(Calendar.MONTH, Calendar.DECEMBER) }.time, - headacheRatio = 0.2f, - headacheAmount = 1, - headacheColor = Color.Red, - pillRatio = 0.3f, - pillAmount = 1, - pillColor = Color.Blue, - ), - MonthSummaryBoxUio( - date = calendar.apply { set(Calendar.MONTH, Calendar.SEPTEMBER) }.time, - headacheRatio = 1f, - headacheAmount = 1, - headacheColor = Color.Red, - pillRatio = 0.3f, - pillAmount = 1, - pillColor = Color.Blue, - ), - ) -} \ No newline at end of file diff --git a/app/src/main/java/com/pixelized/headache/ui/page/summary/monthly/item/MonthSummaryCell.kt b/app/src/main/java/com/pixelized/headache/ui/page/summary/monthly/item/MonthSummaryCell.kt deleted file mode 100644 index 9d06e78..0000000 --- a/app/src/main/java/com/pixelized/headache/ui/page/summary/monthly/item/MonthSummaryCell.kt +++ /dev/null @@ -1,9 +0,0 @@ -package com.pixelized.headache.ui.page.summary.monthly.item - -import androidx.compose.runtime.Stable -import java.util.Date - -@Stable -sealed interface MonthSummaryCell { - val date: Date -} \ No newline at end of file diff --git a/app/src/main/java/com/pixelized/headache/ui/page/summary/monthly/item/MonthSummaryItem.kt b/app/src/main/java/com/pixelized/headache/ui/page/summary/monthly/item/MonthSummaryItem.kt index 4101862..7ee80f1 100644 --- a/app/src/main/java/com/pixelized/headache/ui/page/summary/monthly/item/MonthSummaryItem.kt +++ b/app/src/main/java/com/pixelized/headache/ui/page/summary/monthly/item/MonthSummaryItem.kt @@ -29,10 +29,10 @@ import java.util.Locale @Stable data class MonthSummaryItemUio( - override val date: Date, + val date: Date, val days: Int, val pills: List, -) : MonthSummaryCell +) @Stable object MonthSummaryItemDefault { diff --git a/app/src/main/java/com/pixelized/headache/ui/page/summary/monthly/item/MonthSummaryTitle.kt b/app/src/main/java/com/pixelized/headache/ui/page/summary/monthly/item/MonthSummaryTitle.kt index f898bd3..6c869e9 100644 --- a/app/src/main/java/com/pixelized/headache/ui/page/summary/monthly/item/MonthSummaryTitle.kt +++ b/app/src/main/java/com/pixelized/headache/ui/page/summary/monthly/item/MonthSummaryTitle.kt @@ -15,8 +15,8 @@ import java.util.Locale @Stable data class MonthSummaryTitleUio( - override val date: Date, -) : MonthSummaryCell + val date: Date, +) @Stable object MonthSummaryTitleDefault { diff --git a/app/src/main/java/com/pixelized/headache/ui/page/summary/report/ReportBox.kt b/app/src/main/java/com/pixelized/headache/ui/page/summary/report/ReportBox.kt new file mode 100644 index 0000000..936bac6 --- /dev/null +++ b/app/src/main/java/com/pixelized/headache/ui/page/summary/report/ReportBox.kt @@ -0,0 +1,229 @@ +package com.pixelized.headache.ui.page.summary.report + +import android.annotation.SuppressLint +import android.icu.text.DateFormat +import android.icu.text.SimpleDateFormat +import android.icu.util.Calendar +import androidx.compose.foundation.background +import androidx.compose.foundation.layout.Arrangement +import androidx.compose.foundation.layout.Box +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.Row +import androidx.compose.foundation.layout.height +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.layout.size +import androidx.compose.foundation.layout.width +import androidx.compose.foundation.shape.RoundedCornerShape +import androidx.compose.material3.MaterialTheme +import androidx.compose.material3.Surface +import androidx.compose.material3.Text +import androidx.compose.runtime.Composable +import androidx.compose.runtime.Stable +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.graphics.Color +import androidx.compose.ui.graphics.Shape +import androidx.compose.ui.text.style.TextOverflow +import androidx.compose.ui.tooling.preview.Preview +import androidx.compose.ui.tooling.preview.PreviewParameter +import androidx.compose.ui.tooling.preview.PreviewParameterProvider +import androidx.compose.ui.unit.Dp +import androidx.compose.ui.unit.DpSize +import androidx.compose.ui.unit.dp +import com.pixelized.headache.ui.theme.HeadacheTheme +import com.pixelized.headache.ui.theme.color.HeadacheColorPalette +import com.pixelized.headache.utils.extention.capitalize +import java.util.Date +import java.util.Locale + +@Stable +class ReportBoxUio( + val year: Int, + val months: List, +) { + @Stable + data class Month( + val date: Date, + val stats: List, + ) + + @Stable + data class Bar( + val color: Color, + val label: String, + val ratio: Float, + ) +} + +@Stable +object ReportBoxDefault { + + @Stable + val barSpace: Dp = 4.dp + + @Stable + val titleSpace: Dp = 8.dp + + @Stable + val barSize: DpSize = DpSize( + width = 28.dp, + height = 320.dp, + ) + + @Stable + val barShape: Shape = RoundedCornerShape( + topStart = barSize.width / 4, + topEnd = barSize.width / 4 + ) + + @SuppressLint("ConstantLocale") + @Stable + val formatter = SimpleDateFormat("MMM", Locale.getDefault()) +} + +@Composable +fun ReportBox( + modifier: Modifier = Modifier, + formatter: DateFormat = ReportBoxDefault.formatter, + barSize: DpSize = ReportBoxDefault.barSize, + barShape: Shape = ReportBoxDefault.barShape, + barSpace: Dp = ReportBoxDefault.barSpace, + titleSpace: Dp = ReportBoxDefault.titleSpace, + item: ReportBoxUio, +) { + Column( + modifier = modifier, + verticalArrangement = Arrangement.spacedBy(space = titleSpace), + ) { + Text( + modifier = Modifier.padding(top = 16.dp), + style = MaterialTheme.typography.displaySmall, + text = "${item.year}", + ) + Row( + horizontalArrangement = Arrangement.spacedBy(space = barSpace), + ) { + item.months.forEach { + Month( + formatter = formatter, + barSize = barSize, + barShape = barShape, + item = it, + ) + } + } + } +} + +@Composable +private fun Month( + modifier: Modifier = Modifier, + formatter: DateFormat = ReportBoxDefault.formatter, + barSize: DpSize = ReportBoxDefault.barSize, + barShape: Shape = ReportBoxDefault.barShape, + item: ReportBoxUio.Month, +) { + Column( + modifier = Modifier + .width(width = barSize.width) + .then(other = modifier), + horizontalAlignment = Alignment.CenterHorizontally, + ) { + Box( + modifier = Modifier.height(height = barSize.height), + contentAlignment = Alignment.BottomStart, + ) { + item.stats.forEachIndexed { index, stat -> + Box( + modifier = Modifier + .size( + width = barSize.width, + height = barSize.height * stat.ratio + ) + .background( + color = stat.color, + shape = barShape, + ), + contentAlignment = Alignment.TopCenter, + ) { + Text( + style = MaterialTheme.typography.labelSmall, + overflow = TextOverflow.Ellipsis, + maxLines = 1, + color = Color.White, + text = stat.label, + ) + } + } + } + Text( + style = MaterialTheme.typography.labelSmall, + overflow = TextOverflow.Ellipsis, + maxLines = 1, + text = formatter.format(item.date).capitalize(), + ) + } +} + +@Composable +@Preview +private fun ReportBoxPreview( + @PreviewParameter(ReportBoxPreviewProvider::class) preview: ReportBoxUio, +) { + HeadacheTheme { + Surface { + ReportBox( + item = preview, + ) + } + } +} + +object ReportBoxPreviewHelper { + fun month( + month: Int, + headache: Int = 0, + pills: Int = 0, + ) = ReportBoxUio.Month( + date = Calendar.getInstance().apply { + set(Calendar.MONTH, month) + }.time, + stats = listOf( + ReportBoxUio.Bar( + color = HeadacheColorPalette.Calendar.Headache, + label = "$headache", + ratio = (headache.toFloat() / 30).coerceIn(0f, 1f), + ), + ReportBoxUio.Bar( + color = HeadacheColorPalette.Calendar.Pill, + label = "$pills", + ratio = (pills.toFloat() / 24).coerceIn(0f, 1f), + ), + ).sortedByDescending { it.ratio }, + ) +} + +private class ReportBoxPreviewProvider : PreviewParameterProvider { + override val values: Sequence + get() = with(ReportBoxPreviewHelper) { + sequenceOf( + ReportBoxUio( + year = 2025, + months = listOf( + month(month = Calendar.JANUARY, headache = 6, pills = 10), + month(month = Calendar.FEBRUARY, headache = 15, pills = 24), + month(month = Calendar.MARCH, headache = 14, pills = 16), + month(month = Calendar.APRIL, headache = 16, pills = 18), + month(month = Calendar.MARCH, headache = 14, pills = 20), + month(month = Calendar.JUNE, headache = 12, pills = 13), + month(month = Calendar.JULY, headache = 7, pills = 3), + month(month = Calendar.AUGUST, headache = 8, pills = 5), + month(month = Calendar.SEPTEMBER, headache = 8, pills = 5), + month(month = Calendar.OCTOBER), + month(month = Calendar.NOVEMBER), + month(month = Calendar.DECEMBER), + ) + ) + ) + } +} \ No newline at end of file diff --git a/app/src/main/java/com/pixelized/headache/ui/page/summary/report/ReportFactory.kt b/app/src/main/java/com/pixelized/headache/ui/page/summary/report/ReportFactory.kt new file mode 100644 index 0000000..b1d05cf --- /dev/null +++ b/app/src/main/java/com/pixelized/headache/ui/page/summary/report/ReportFactory.kt @@ -0,0 +1,59 @@ +package com.pixelized.headache.ui.page.summary.report + +import android.icu.util.Calendar +import com.pixelized.headache.repository.event.Event +import com.pixelized.headache.ui.theme.color.HeadacheColorPalette +import com.pixelized.headache.utils.extention.event +import javax.inject.Inject + +class ReportFactory @Inject constructor() { + + fun convertToUio( + maxPillAmountPerMonth: Int, + events: Map>>>, + ): List { + return events + .map { yearEntry -> + ReportBoxUio( + year = yearEntry.key, + months = yearEntry.value.map { monthEntry -> + var headache = 0 + var pills = 0 + monthEntry.value.values.forEach { events -> + headache += events.size + pills += events.sumOf { it.pills.size } + } + val dayInMonth = Calendar.getInstance().let { + it.set(Calendar.YEAR, yearEntry.key) + it.set(Calendar.MONTH, monthEntry.key + 1) + it.set(Calendar.DAY_OF_MONTH, 1) + it.add(Calendar.DAY_OF_YEAR, -1) + it.get(Calendar.DAY_OF_MONTH) + } + ReportBoxUio.Month( + date = Calendar.getInstance().apply { + event = Event.Date( + day = 1, + month = monthEntry.key, + year = yearEntry.key + ) + }.time, + stats = listOf( + ReportBoxUio.Bar( + color = HeadacheColorPalette.Calendar.Headache, + label = "$headache", + ratio = headache.toFloat() / dayInMonth.toFloat(), + ), + ReportBoxUio.Bar( + color = HeadacheColorPalette.Calendar.Pill, + label = "$pills", + ratio = pills.toFloat() / maxPillAmountPerMonth.toFloat(), + ), + ).sortedByDescending { it.ratio }, + ) + } + ) + } + .sortedByDescending { it.year } + } +} \ No newline at end of file diff --git a/app/src/main/java/com/pixelized/headache/ui/page/summary/report/ReportPage.kt b/app/src/main/java/com/pixelized/headache/ui/page/summary/report/ReportPage.kt new file mode 100644 index 0000000..d90bbf7 --- /dev/null +++ b/app/src/main/java/com/pixelized/headache/ui/page/summary/report/ReportPage.kt @@ -0,0 +1,237 @@ +package com.pixelized.headache.ui.page.summary.report + + +import android.icu.util.Calendar +import androidx.compose.foundation.background +import androidx.compose.foundation.layout.Arrangement +import androidx.compose.foundation.layout.Box +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.PaddingValues +import androidx.compose.foundation.layout.Row +import androidx.compose.foundation.layout.WindowInsets +import androidx.compose.foundation.layout.fillMaxSize +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.layout.size +import androidx.compose.foundation.lazy.LazyColumn +import androidx.compose.foundation.lazy.items +import androidx.compose.material3.ExperimentalMaterial3Api +import androidx.compose.material3.MaterialTheme +import androidx.compose.material3.NavigationBarDefaults +import androidx.compose.material3.Scaffold +import androidx.compose.material3.Surface +import androidx.compose.material3.Text +import androidx.compose.material3.TopAppBar +import androidx.compose.material3.TopAppBarDefaults +import androidx.compose.runtime.Composable +import androidx.compose.runtime.Stable +import androidx.compose.runtime.State +import androidx.compose.runtime.mutableStateOf +import androidx.compose.runtime.remember +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.keepScreenOn +import androidx.compose.ui.platform.LocalDensity +import androidx.compose.ui.platform.LocalWindowInfo +import androidx.compose.ui.res.stringResource +import androidx.compose.ui.tooling.preview.Preview +import androidx.compose.ui.tooling.preview.PreviewParameter +import androidx.compose.ui.tooling.preview.PreviewParameterProvider +import androidx.compose.ui.unit.Dp +import androidx.compose.ui.unit.DpSize +import androidx.compose.ui.unit.dp +import androidx.hilt.navigation.compose.hiltViewModel +import androidx.lifecycle.compose.collectAsStateWithLifecycle +import com.pixelized.headache.R +import com.pixelized.headache.ui.theme.HeadacheTheme +import com.pixelized.headache.ui.theme.color.HeadacheColorPalette +import com.pixelized.headache.utils.extention.calculate + +@Stable +data object ReportPageDefault { + @Stable + val paddingValues = PaddingValues( + start = 16.dp, + end = 16.dp, + bottom = 16.dp + 16.dp + 56.dp, + ) + + @Stable + val contentSpace: Dp = 8.dp + + @Stable + val barSpace: Dp = ReportBoxDefault.barSpace +} + +@Composable +fun ReportPage( + viewModel: ReportViewModel = hiltViewModel(), +) { + val events = viewModel.events.collectAsStateWithLifecycle() + + ReportContent( + modifier = Modifier + .keepScreenOn() + .fillMaxSize(), + events = events, + ) +} + +@OptIn(ExperimentalMaterial3Api::class) +@Composable +private fun ReportContent( + modifier: Modifier = Modifier, + paddingValues: PaddingValues = ReportPageDefault.paddingValues, + contentSpace: Dp = ReportPageDefault.contentSpace, + barSpace: Dp = ReportPageDefault.barSpace, + barSize: DpSize = rememberBarSize(column = 12, paddingValues = paddingValues, space = barSpace), + events: State>, +) { + Scaffold( + modifier = modifier, + contentWindowInsets = remember { WindowInsets(0, 0, 0, 0) }, + topBar = { + TopAppBar( + colors = TopAppBarDefaults.topAppBarColors( + containerColor = NavigationBarDefaults.containerColor, + ), + title = { + Text(text = stringResource(R.string.year_summary_title)) + }, + actions = { + Column( + modifier = Modifier.padding(end = 16.dp) + ) { + Row( + horizontalArrangement = Arrangement.spacedBy(space = 4.dp), + verticalAlignment = Alignment.CenterVertically, + ) { + Box( + modifier = Modifier + .size(8.dp) + .background( + color = HeadacheColorPalette.Calendar.Headache, + ) + ) + Text( + style = MaterialTheme.typography.labelSmall, + text = "Jours de migraine", + ) + } + Row( + horizontalArrangement = Arrangement.spacedBy(space = 4.dp), + verticalAlignment = Alignment.CenterVertically, + ) { + Box( + modifier = Modifier + .size(8.dp) + .background( + color = HeadacheColorPalette.Calendar.Pill, + ) + ) + Text( + style = MaterialTheme.typography.labelSmall, + text = "Prise de cachet", + ) + } + } + } + ) + }, + content = { it -> + LazyColumn( + modifier = Modifier.padding(paddingValues = it), + contentPadding = paddingValues, + verticalArrangement = Arrangement.spacedBy(space = contentSpace), + ) { + items( + items = events.value + ) { item -> + ReportBox( + barSize = barSize, + barSpace = barSpace, + item = item, + ) + } + } + } + ) +} + +@Composable +private fun rememberBarSize( + column: Int, + paddingValues: PaddingValues, + space: Dp, +): DpSize { + val density = LocalDensity.current + val windowInfo = LocalWindowInfo.current + val screenWidth = remember(density, windowInfo) { + with(density) { windowInfo.containerSize.width.toDp() } + } + val (start, _, end, _) = paddingValues.calculate() + return remember { + DpSize( + width = (screenWidth - space * (column - 1) - start - end) / column, + height = ReportBoxDefault.barSize.height, + ) + } +} + +@Composable +@Preview +private fun ReportPreview( + @PreviewParameter(ReportPreviewProvider::class) preview: List, +) { + HeadacheTheme { + Surface { + ReportContent( + events = remember { mutableStateOf(preview) }, + ) + } + } +} + +private class ReportPreviewProvider : PreviewParameterProvider> { + + override val values: Sequence> + get() = with(ReportBoxPreviewHelper) { + sequenceOf( + listOf( + ReportBoxUio( + year = 2025, + months = listOf( + month(month = Calendar.JANUARY, headache = 6, pills = 10), + month(month = Calendar.FEBRUARY, headache = 15, pills = 24), + month(month = Calendar.MARCH, headache = 14, pills = 16), + month(month = Calendar.APRIL, headache = 16, pills = 18), + month(month = Calendar.MARCH, headache = 14, pills = 20), + month(month = Calendar.JUNE, headache = 12, pills = 13), + month(month = Calendar.JULY, headache = 7, pills = 3), + month(month = Calendar.AUGUST, headache = 8, pills = 5), + month(month = Calendar.SEPTEMBER, headache = 8, pills = 5), + month(month = Calendar.OCTOBER, headache = 0, pills = 0), + month(month = Calendar.NOVEMBER, headache = 0, pills = 0), + month(month = Calendar.DECEMBER, headache = 0, pills = 0), + ), + ), + ReportBoxUio( + year = 2024, + months = listOf( + month(month = Calendar.JANUARY, headache = 14), + month(month = Calendar.FEBRUARY, headache = 15), + month(month = Calendar.MARCH, headache = 15), + month(month = Calendar.APRIL, headache = 10), + month(month = Calendar.MARCH, headache = 7), + month(month = Calendar.JUNE, headache = 15), + month(month = Calendar.JULY, headache = 7), + month(month = Calendar.AUGUST, headache = 11, pills = 12), + month(month = Calendar.SEPTEMBER, headache = 12, pills = 15), + month(month = Calendar.OCTOBER, headache = 5, pills = 8), + month(month = Calendar.NOVEMBER, headache = 17, pills = 22), + month(month = Calendar.DECEMBER, headache = 12, pills = 17), + ), + ) + ) + ) + } +} \ No newline at end of file diff --git a/app/src/main/java/com/pixelized/headache/ui/page/summary/report/ReportViewModel.kt b/app/src/main/java/com/pixelized/headache/ui/page/summary/report/ReportViewModel.kt new file mode 100644 index 0000000..1ac2581 --- /dev/null +++ b/app/src/main/java/com/pixelized/headache/ui/page/summary/report/ReportViewModel.kt @@ -0,0 +1,28 @@ +package com.pixelized.headache.ui.page.summary.report + +import androidx.lifecycle.ViewModel +import androidx.lifecycle.viewModelScope +import com.pixelized.headache.repository.event.EventRepository +import dagger.hilt.android.lifecycle.HiltViewModel +import kotlinx.coroutines.flow.SharingStarted +import kotlinx.coroutines.flow.StateFlow +import kotlinx.coroutines.flow.combine +import kotlinx.coroutines.flow.stateIn +import javax.inject.Inject + +@HiltViewModel +class ReportViewModel @Inject constructor( + eventRepository: EventRepository, + reportFactory: ReportFactory, +) : ViewModel() { + + val events: StateFlow> = combine( + eventRepository.maxPillAmountPerMonthFlow(), + eventRepository.eventsMapFlow(), + reportFactory::convertToUio + ).stateIn( + scope = viewModelScope, + started = SharingStarted.Lazily, + initialValue = emptyList(), + ) +} \ No newline at end of file diff --git a/app/src/main/java/com/pixelized/headache/ui/page/summary/yearly/YearSummaryFactory.kt b/app/src/main/java/com/pixelized/headache/ui/page/summary/yearly/YearSummaryFactory.kt index 8b49002..27a35cf 100644 --- a/app/src/main/java/com/pixelized/headache/ui/page/summary/yearly/YearSummaryFactory.kt +++ b/app/src/main/java/com/pixelized/headache/ui/page/summary/yearly/YearSummaryFactory.kt @@ -8,7 +8,7 @@ import javax.inject.Inject class YearSummaryFactory @Inject constructor() { fun convertToUio( - events: Map>>, + events: Map>>>, ): List { val monthFirstDayCalendar = Calendar.getInstance().apply { firstDayOfWeek = Calendar.MONDAY @@ -49,7 +49,7 @@ class YearSummaryFactory @Inject constructor() { } val weeks = (1..monthLastDayCalendar.get(Calendar.DAY_OF_MONTH)) .fold(initial = initial) { accumulator, dayNumber -> - val event: Event? = monthEntry.value.get(dayNumber) + val event: List = monthEntry.value[dayNumber] ?: emptyList() val weekIndex = currentDayCalendar .apply { set(Calendar.YEAR, yearEntry.key) @@ -60,8 +60,8 @@ class YearSummaryFactory @Inject constructor() { .get(Calendar.WEEK_OF_MONTH) - 1 val day = DayUio( number = dayNumber, - headache = event != null, - pills = event?.pills?.map { it.color } ?: emptyList(), + headache = event.isNotEmpty(), + pills = event.flatMap { it.pills.map { pill -> pill.color } }, ) accumulator.also { acc -> acc[weekIndex] = acc.get(index = weekIndex).also { week -> diff --git a/app/src/main/java/com/pixelized/headache/ui/page/summary/yearly/YearSummaryMonth.kt b/app/src/main/java/com/pixelized/headache/ui/page/summary/yearly/YearSummaryMonth.kt index 7961ea3..edb5d37 100644 --- a/app/src/main/java/com/pixelized/headache/ui/page/summary/yearly/YearSummaryMonth.kt +++ b/app/src/main/java/com/pixelized/headache/ui/page/summary/yearly/YearSummaryMonth.kt @@ -142,7 +142,7 @@ fun YearSummaryMonth( width = localCellSize.width - 0f, height = localCellSize.height - (pillSize.height + space) * 2, ), - color = HeadacheColorPalette.Pill.Unknown, + color = HeadacheColorPalette.Calendar.Headache, ) } diff --git a/app/src/main/java/com/pixelized/headache/ui/page/summary/yearly/YearSummaryPage.kt b/app/src/main/java/com/pixelized/headache/ui/page/summary/yearly/YearSummaryPage.kt index 790dd46..216522e 100644 --- a/app/src/main/java/com/pixelized/headache/ui/page/summary/yearly/YearSummaryPage.kt +++ b/app/src/main/java/com/pixelized/headache/ui/page/summary/yearly/YearSummaryPage.kt @@ -83,23 +83,6 @@ fun YearSummaryPage( ) } -@Composable -private fun rememberDaySize( - column: Int, - paddingValues: PaddingValues = YearSummaryPageDefault.paddingValues, - space: Dp = YearSummaryPageDefault.space, -): Dp { - val density = LocalDensity.current - val windowInfo = LocalWindowInfo.current - val screenWidth = remember(density, windowInfo) { - with(density) { windowInfo.containerSize.width.toDp() } - } - val (start, _, end, _) = paddingValues.calculate() - return remember { - (screenWidth - space * (column - 1) - start - end) / (7 * column) - } -} - @OptIn(ExperimentalMaterial3Api::class) @Composable fun YearSummaryContent( @@ -169,6 +152,23 @@ fun YearSummaryContent( ) } +@Composable +private fun rememberDaySize( + column: Int, + paddingValues: PaddingValues = YearSummaryPageDefault.paddingValues, + space: Dp = YearSummaryPageDefault.space, +): Dp { + val density = LocalDensity.current + val windowInfo = LocalWindowInfo.current + val screenWidth = remember(density, windowInfo) { + with(density) { windowInfo.containerSize.width.toDp() } + } + val (start, _, end, _) = paddingValues.calculate() + return remember { + (screenWidth - space * (column - 1) - start - end) / (7 * column) + } +} + @Composable @Preview() private fun YearSummaryPreview( diff --git a/app/src/main/java/com/pixelized/headache/ui/theme/color/HeadacheColorPalette.kt b/app/src/main/java/com/pixelized/headache/ui/theme/color/HeadacheColorPalette.kt index 1dbb3b9..468c5f1 100644 --- a/app/src/main/java/com/pixelized/headache/ui/theme/color/HeadacheColorPalette.kt +++ b/app/src/main/java/com/pixelized/headache/ui/theme/color/HeadacheColorPalette.kt @@ -3,14 +3,6 @@ package com.pixelized.headache.ui.theme.color import androidx.compose.ui.graphics.Color import javax.annotation.concurrent.Immutable -val Purple80 = Color(0xFFD0BCFF) -val PurpleGrey80 = Color(0xFFCCC2DC) -val Pink80 = Color(0xFFEFB8C8) - -val Purple40 = Color(0xFF6650a4) -val PurpleGrey40 = Color(0xFF625b71) -val Pink40 = Color(0xFF7D5260) - @Immutable object HeadacheColorPalette { @@ -23,6 +15,12 @@ object HeadacheColorPalette { val Eletriptan40 = Additional.VeryLightPink } + @Immutable + object Calendar { + val Headache = Additional.LightRed + val Pill = Additional.DarkRed + } + @Immutable object Additional { val VeryDarkBlue: Color = Color(0xFF09179D) diff --git a/app/src/main/java/com/pixelized/headache/ui/theme/color/HeadacheColors.kt b/app/src/main/java/com/pixelized/headache/ui/theme/color/HeadacheColors.kt index 8ce3d84..602d222 100644 --- a/app/src/main/java/com/pixelized/headache/ui/theme/color/HeadacheColors.kt +++ b/app/src/main/java/com/pixelized/headache/ui/theme/color/HeadacheColors.kt @@ -40,7 +40,7 @@ data class HeadacheColors( fun headacheDarkColorScheme( base: ColorScheme = darkColorScheme(), calendar: HeadacheColors.Calendar = HeadacheColors.Calendar( - headache = HeadacheColorPalette.Additional.Red, + headache = HeadacheColorPalette.Calendar.Headache, onHeadache = Color.White, ), pill: HeadacheColors.Pill = HeadacheColors.Pill( @@ -61,7 +61,7 @@ fun headacheDarkColorScheme( fun headacheLightColorScheme( base: ColorScheme = lightColorScheme(), calendar: HeadacheColors.Calendar = HeadacheColors.Calendar( - headache = HeadacheColorPalette.Additional.Red, + headache = HeadacheColorPalette.Calendar.Headache, onHeadache = Color.White, ), pill: HeadacheColors.Pill = HeadacheColors.Pill( @@ -94,7 +94,7 @@ fun calculateElevatedColor( @ReadOnlyComposable @Composable -private fun calculateForegroundColor(color: Color, elevation: Dp): Color { +fun calculateForegroundColor(color: Color, elevation: Dp): Color { val alpha = ((4.5f * ln(elevation.value + 1)) + 2f) / 100f return color.copy(alpha = alpha) } \ No newline at end of file