Add a yeary stats screen
This commit is contained in:
parent
6bb58e06c0
commit
3645b92a82
24 changed files with 744 additions and 464 deletions
6
.idea/deploymentTargetSelector.xml
generated
6
.idea/deploymentTargetSelector.xml
generated
|
|
@ -8,15 +8,15 @@
|
|||
<SelectionState runConfigName="EventFactoryText">
|
||||
<option name="selectionMode" value="DROPDOWN" />
|
||||
</SelectionState>
|
||||
<SelectionState runConfigName="text2Pills_3()">
|
||||
<option name="selectionMode" value="DROPDOWN" />
|
||||
</SelectionState>
|
||||
<SelectionState runConfigName="EventEditPreview">
|
||||
<option name="selectionMode" value="DROPDOWN" />
|
||||
</SelectionState>
|
||||
<SelectionState runConfigName="YearPreview">
|
||||
<option name="selectionMode" value="DROPDOWN" />
|
||||
</SelectionState>
|
||||
<SelectionState runConfigName="ReportBoxPreview">
|
||||
<option name="selectionMode" value="DROPDOWN" />
|
||||
</SelectionState>
|
||||
</selectionStates>
|
||||
</component>
|
||||
</project>
|
||||
Binary file not shown.
Binary file not shown.
|
|
@ -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<Map<Int, Map<Int, Map<Int, Event>>>> = eventListFlow
|
||||
private val eventMapFlow: StateFlow<Map<Int, Map<Int, Map<Int, List<Event>>>>> = eventListFlow
|
||||
.map { events ->
|
||||
events.fold(initial = hashMapOf<Int, HashMap<Int, HashMap<Int, Event>>>()) { acc, event ->
|
||||
events.fold(initial = hashMapOf<Int, HashMap<Int, HashMap<Int, MutableList<Event>>>>()) { 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<Int> = eventMapFlow
|
||||
.map { events ->
|
||||
events.values.maxOf { yearEntry: Map<Int, Map<Int, List<Event>>> ->
|
||||
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<Collection<Event>> = eventListFlow
|
||||
|
||||
fun eventsMapFlow(): StateFlow<Map<Int, Map<Int, Map<Int, Event>>>> = eventMapFlow
|
||||
fun eventsMapFlow(): StateFlow<Map<Int, Map<Int, Map<Int, List<Event>>>>> = eventMapFlow
|
||||
|
||||
fun maxPillAmountPerMonthFlow(): StateFlow<Int> = maxPillAmountPerMonth
|
||||
|
||||
fun event(id: Long): Event? = eventFlow.value.get(key = id)
|
||||
|
||||
|
|
|
|||
|
|
@ -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<ReportDestination> {
|
||||
ReportPage()
|
||||
}
|
||||
}
|
||||
|
||||
fun HomeNavigator.navigateToReport() {
|
||||
goTo(ReportDestination)
|
||||
}
|
||||
|
|
@ -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<HomeNavigator> {
|
||||
|
|
@ -41,6 +42,7 @@ fun HomeNavDisplay(
|
|||
entryProvider = entryProvider {
|
||||
monthSummaryDestinationEntry()
|
||||
yearSummaryDestinationEntry()
|
||||
reportDestinationEntry()
|
||||
}
|
||||
)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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(
|
||||
|
|
|
|||
|
|
@ -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<BottomBarItemUio>,
|
||||
selectedItem: IntState,
|
||||
selectedItem: State<Int>,
|
||||
onFabClick: () -> Unit,
|
||||
) {
|
||||
Scaffold(
|
||||
|
|
|
|||
|
|
@ -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<Event>,
|
||||
): Map<MonthSummaryCell, List<MonthSummaryCell>> {
|
||||
events: Map<Int, Map<Int, Map<Int, List<Event>>>>,
|
||||
): Map<MonthSummaryTitleUio, List<MonthSummaryItemUio>> {
|
||||
return events
|
||||
.fold(hashMapOf<Event.Date, MutableList<Event>>()) { acc, event ->
|
||||
acc.also { it.getOrPut(event.date.copy(day = 1)) { mutableListOf() }.add(event) }
|
||||
}
|
||||
.map { entry ->
|
||||
val pills = entry.value
|
||||
.fold(hashMapOf<Event.Pill.Id, EventFolder>()) { 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<Event.Pill.Id, PillFolder>(),
|
||||
) { 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<Event>,
|
||||
): Map<MonthSummaryCell, List<MonthSummaryCell>> {
|
||||
var maxPillAmount = 0
|
||||
return events
|
||||
.fold(hashMapOf<Event.Date, MutableList<Event>>()) { 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<MonthSummaryCell>.groupByMonth(): Map<MonthSummaryCell, List<MonthSummaryCell>> {
|
||||
fun List<MonthSummaryItemUio>.groupByMonth(): Map<MonthSummaryTitleUio, List<MonthSummaryItemUio>> {
|
||||
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,
|
||||
)
|
||||
}
|
||||
|
|
@ -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<Boolean>,
|
||||
events: State<Map<MonthSummaryCell, List<MonthSummaryCell>>>,
|
||||
onDisplay: () -> Unit,
|
||||
onItem: (MonthSummaryCell) -> Unit,
|
||||
events: State<Map<MonthSummaryTitleUio, List<MonthSummaryItemUio>>>,
|
||||
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 = { },
|
||||
)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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<Boolean> = displayTypeFlow
|
||||
|
||||
@OptIn(ExperimentalCoroutinesApi::class)
|
||||
val events: StateFlow<Map<MonthSummaryCell, List<MonthSummaryCell>>> = combine(
|
||||
eventRepository.eventsListFlow(),
|
||||
displayTypeFlow,
|
||||
transform = { events: Collection<Event>, 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(),
|
||||
)
|
||||
}
|
||||
|
|
@ -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<MonthSummaryBoxUio> {
|
||||
val calendar = Calendar.getInstance().apply {
|
||||
time = Date()
|
||||
set(Calendar.DAY_OF_MONTH, 1)
|
||||
}
|
||||
override val values: Sequence<MonthSummaryBoxUio>
|
||||
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,
|
||||
),
|
||||
)
|
||||
}
|
||||
|
|
@ -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
|
||||
}
|
||||
|
|
@ -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<MonthSummaryPillItemUio>,
|
||||
) : MonthSummaryCell
|
||||
)
|
||||
|
||||
@Stable
|
||||
object MonthSummaryItemDefault {
|
||||
|
|
|
|||
|
|
@ -15,8 +15,8 @@ import java.util.Locale
|
|||
|
||||
@Stable
|
||||
data class MonthSummaryTitleUio(
|
||||
override val date: Date,
|
||||
) : MonthSummaryCell
|
||||
val date: Date,
|
||||
)
|
||||
|
||||
@Stable
|
||||
object MonthSummaryTitleDefault {
|
||||
|
|
|
|||
|
|
@ -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<Month>,
|
||||
) {
|
||||
@Stable
|
||||
data class Month(
|
||||
val date: Date,
|
||||
val stats: List<Bar>,
|
||||
)
|
||||
|
||||
@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<ReportBoxUio> {
|
||||
override val values: Sequence<ReportBoxUio>
|
||||
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),
|
||||
)
|
||||
)
|
||||
)
|
||||
}
|
||||
}
|
||||
|
|
@ -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<Int, Map<Int, Map<Int, List<Event>>>>,
|
||||
): List<ReportBoxUio> {
|
||||
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 }
|
||||
}
|
||||
}
|
||||
|
|
@ -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<List<ReportBoxUio>>,
|
||||
) {
|
||||
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<ReportBoxUio>,
|
||||
) {
|
||||
HeadacheTheme {
|
||||
Surface {
|
||||
ReportContent(
|
||||
events = remember { mutableStateOf(preview) },
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private class ReportPreviewProvider : PreviewParameterProvider<List<ReportBoxUio>> {
|
||||
|
||||
override val values: Sequence<List<ReportBoxUio>>
|
||||
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),
|
||||
),
|
||||
)
|
||||
)
|
||||
)
|
||||
}
|
||||
}
|
||||
|
|
@ -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<List<ReportBoxUio>> = combine(
|
||||
eventRepository.maxPillAmountPerMonthFlow(),
|
||||
eventRepository.eventsMapFlow(),
|
||||
reportFactory::convertToUio
|
||||
).stateIn(
|
||||
scope = viewModelScope,
|
||||
started = SharingStarted.Lazily,
|
||||
initialValue = emptyList(),
|
||||
)
|
||||
}
|
||||
|
|
@ -8,7 +8,7 @@ import javax.inject.Inject
|
|||
class YearSummaryFactory @Inject constructor() {
|
||||
|
||||
fun convertToUio(
|
||||
events: Map<Int, Map<Int, Map<Int, Event>>>,
|
||||
events: Map<Int, Map<Int, Map<Int, List<Event>>>>,
|
||||
): List<YearUio> {
|
||||
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<Event> = 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 ->
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
)
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -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(
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
}
|
||||
Loading…
Add table
Add a link
Reference in a new issue