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">
|
<SelectionState runConfigName="EventFactoryText">
|
||||||
<option name="selectionMode" value="DROPDOWN" />
|
<option name="selectionMode" value="DROPDOWN" />
|
||||||
</SelectionState>
|
</SelectionState>
|
||||||
<SelectionState runConfigName="text2Pills_3()">
|
|
||||||
<option name="selectionMode" value="DROPDOWN" />
|
|
||||||
</SelectionState>
|
|
||||||
<SelectionState runConfigName="EventEditPreview">
|
<SelectionState runConfigName="EventEditPreview">
|
||||||
<option name="selectionMode" value="DROPDOWN" />
|
<option name="selectionMode" value="DROPDOWN" />
|
||||||
</SelectionState>
|
</SelectionState>
|
||||||
<SelectionState runConfigName="YearPreview">
|
<SelectionState runConfigName="YearPreview">
|
||||||
<option name="selectionMode" value="DROPDOWN" />
|
<option name="selectionMode" value="DROPDOWN" />
|
||||||
</SelectionState>
|
</SelectionState>
|
||||||
|
<SelectionState runConfigName="ReportBoxPreview">
|
||||||
|
<option name="selectionMode" value="DROPDOWN" />
|
||||||
|
</SelectionState>
|
||||||
</selectionStates>
|
</selectionStates>
|
||||||
</component>
|
</component>
|
||||||
</project>
|
</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.
|
// 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 ->
|
.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 {
|
acc.also {
|
||||||
val years = it.getOrPut(key = event.date.year) {
|
val years = it.getOrPut(key = event.date.year) {
|
||||||
hashMapOf(
|
hashMapOf(
|
||||||
|
@ -68,7 +68,8 @@ class EventRepository @Inject constructor(
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
val months = years.getOrPut(key = event.date.month) { hashMapOf() }
|
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(),
|
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 {
|
init {
|
||||||
calendarIdRepository.calendarId
|
calendarIdRepository.calendarId
|
||||||
.onEach { id ->
|
.onEach { id ->
|
||||||
|
@ -92,7 +107,9 @@ class EventRepository @Inject constructor(
|
||||||
|
|
||||||
fun eventsListFlow(): StateFlow<Collection<Event>> = eventListFlow
|
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)
|
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.eventDestinationEntry
|
||||||
import com.pixelized.headache.ui.navigation.destination.homeDestinationEntry
|
import com.pixelized.headache.ui.navigation.destination.homeDestinationEntry
|
||||||
import com.pixelized.headache.ui.navigation.destination.monthSummaryDestinationEntry
|
import com.pixelized.headache.ui.navigation.destination.monthSummaryDestinationEntry
|
||||||
|
import com.pixelized.headache.ui.navigation.destination.reportDestinationEntry
|
||||||
import com.pixelized.headache.ui.navigation.destination.yearSummaryDestinationEntry
|
import com.pixelized.headache.ui.navigation.destination.yearSummaryDestinationEntry
|
||||||
|
|
||||||
val LocalHomeNavigator = staticCompositionLocalOf<HomeNavigator> {
|
val LocalHomeNavigator = staticCompositionLocalOf<HomeNavigator> {
|
||||||
|
@ -41,6 +42,7 @@ fun HomeNavDisplay(
|
||||||
entryProvider = entryProvider {
|
entryProvider = entryProvider {
|
||||||
monthSummaryDestinationEntry()
|
monthSummaryDestinationEntry()
|
||||||
yearSummaryDestinationEntry()
|
yearSummaryDestinationEntry()
|
||||||
|
reportDestinationEntry()
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
|
@ -67,13 +67,13 @@ class EventEditFactory @Inject constructor() {
|
||||||
EventPillEditUio(
|
EventPillEditUio(
|
||||||
id = Event.Pill.Id.IBUPROFENE_400.value,
|
id = Event.Pill.Id.IBUPROFENE_400.value,
|
||||||
color = HeadacheColorPalette.Pill.Ibuprofene400,
|
color = HeadacheColorPalette.Pill.Ibuprofene400,
|
||||||
label = "Paracétamol 1000",
|
label = "Ibuprofène 400",
|
||||||
amount = ibuprofeneAmount,
|
amount = ibuprofeneAmount,
|
||||||
),
|
),
|
||||||
EventPillEditUio(
|
EventPillEditUio(
|
||||||
id = Event.Pill.Id.PARACETAMOL_1000.value,
|
id = Event.Pill.Id.PARACETAMOL_1000.value,
|
||||||
color = HeadacheColorPalette.Pill.Paracetamol1000,
|
color = HeadacheColorPalette.Pill.Paracetamol1000,
|
||||||
label = "Ibuprofène 400",
|
label = "Paracétamol 1000",
|
||||||
amount = paracetamolAmount,
|
amount = paracetamolAmount,
|
||||||
),
|
),
|
||||||
EventPillEditUio(
|
EventPillEditUio(
|
||||||
|
|
|
@ -21,26 +21,26 @@ import androidx.compose.material3.FloatingActionButton
|
||||||
import androidx.compose.material3.Icon
|
import androidx.compose.material3.Icon
|
||||||
import androidx.compose.material3.MaterialTheme
|
import androidx.compose.material3.MaterialTheme
|
||||||
import androidx.compose.material3.NavigationBar
|
import androidx.compose.material3.NavigationBar
|
||||||
import androidx.compose.material3.NavigationBarDefaults
|
|
||||||
import androidx.compose.material3.NavigationBarItem
|
import androidx.compose.material3.NavigationBarItem
|
||||||
import androidx.compose.material3.Scaffold
|
import androidx.compose.material3.Scaffold
|
||||||
import androidx.compose.material3.Text
|
import androidx.compose.material3.Text
|
||||||
import androidx.compose.material3.TopAppBar
|
|
||||||
import androidx.compose.material3.TopAppBarDefaults
|
|
||||||
import androidx.compose.runtime.Composable
|
import androidx.compose.runtime.Composable
|
||||||
import androidx.compose.runtime.IntState
|
|
||||||
import androidx.compose.runtime.Stable
|
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.runtime.remember
|
||||||
import androidx.compose.ui.Alignment
|
import androidx.compose.ui.Alignment
|
||||||
import androidx.compose.ui.Modifier
|
import androidx.compose.ui.Modifier
|
||||||
import androidx.compose.ui.res.painterResource
|
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.compose.ui.unit.dp
|
import androidx.compose.ui.unit.dp
|
||||||
import androidx.hilt.navigation.compose.hiltViewModel
|
import androidx.hilt.navigation.compose.hiltViewModel
|
||||||
import com.pixelized.headache.R
|
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.navigateToMonthSummary
|
||||||
|
import com.pixelized.headache.ui.navigation.destination.navigateToReport
|
||||||
import com.pixelized.headache.ui.navigation.destination.navigateToYearSummary
|
import com.pixelized.headache.ui.navigation.destination.navigateToYearSummary
|
||||||
import com.pixelized.headache.ui.navigation.home.HomeNavDisplay
|
import com.pixelized.headache.ui.navigation.home.HomeNavDisplay
|
||||||
import com.pixelized.headache.ui.navigation.home.HomeNavigator
|
import com.pixelized.headache.ui.navigation.home.HomeNavigator
|
||||||
|
@ -61,18 +61,25 @@ fun HomePage(
|
||||||
navigator: HomeNavigator,
|
navigator: HomeNavigator,
|
||||||
editViewModel: EventEditBottomSheetViewModel = hiltViewModel(),
|
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(
|
val items = rememberBottomBarItems(
|
||||||
onYearlyFollowUp = {
|
onYearlyFollowUp = {
|
||||||
selectedItem.intValue = 0
|
|
||||||
navigator.navigateToYearSummary()
|
navigator.navigateToYearSummary()
|
||||||
},
|
},
|
||||||
onMonthlyStatFollowUp = {
|
onMonthlyStatFollowUp = {
|
||||||
selectedItem.intValue = 1
|
navigator.navigateToReport()
|
||||||
navigator.navigateToMonthSummary()
|
|
||||||
},
|
},
|
||||||
onMonthlyListFollowUp = {
|
onMonthlyListFollowUp = {
|
||||||
selectedItem.intValue = 2
|
navigator.navigateToMonthSummary()
|
||||||
},
|
},
|
||||||
)
|
)
|
||||||
|
|
||||||
|
@ -123,7 +130,7 @@ private fun HomePageContent(
|
||||||
modifier: Modifier = Modifier,
|
modifier: Modifier = Modifier,
|
||||||
navigator: HomeNavigator,
|
navigator: HomeNavigator,
|
||||||
items: List<BottomBarItemUio>,
|
items: List<BottomBarItemUio>,
|
||||||
selectedItem: IntState,
|
selectedItem: State<Int>,
|
||||||
onFabClick: () -> Unit,
|
onFabClick: () -> Unit,
|
||||||
) {
|
) {
|
||||||
Scaffold(
|
Scaffold(
|
||||||
|
|
|
@ -1,106 +1,78 @@
|
||||||
package com.pixelized.headache.ui.page.summary.monthly
|
package com.pixelized.headache.ui.page.summary.monthly
|
||||||
|
|
||||||
|
import android.icu.text.Collator
|
||||||
import android.icu.util.Calendar
|
import android.icu.util.Calendar
|
||||||
import androidx.compose.ui.graphics.Color
|
import androidx.compose.ui.graphics.Color
|
||||||
|
import androidx.compose.ui.text.intl.Locale
|
||||||
import com.pixelized.headache.repository.event.Event
|
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.MonthSummaryItemUio
|
||||||
import com.pixelized.headache.ui.page.summary.monthly.item.MonthSummaryPillItemUio
|
import com.pixelized.headache.ui.page.summary.monthly.item.MonthSummaryPillItemUio
|
||||||
import com.pixelized.headache.ui.page.summary.monthly.item.MonthSummaryTitleUio
|
import com.pixelized.headache.ui.page.summary.monthly.item.MonthSummaryTitleUio
|
||||||
import com.pixelized.headache.utils.extention.event
|
|
||||||
import javax.inject.Inject
|
import javax.inject.Inject
|
||||||
import kotlin.math.max
|
|
||||||
|
|
||||||
class MonthSummaryFactory @Inject constructor() {
|
class MonthSummaryFactory @Inject constructor() {
|
||||||
private val calendar = Calendar.getInstance()
|
private val calendar = Calendar.getInstance()
|
||||||
|
private val locale = Locale.current.let {
|
||||||
|
java.util.Locale.forLanguageTag(it.toLanguageTag())
|
||||||
|
}
|
||||||
|
|
||||||
fun convertToItemUio(
|
fun convertToItemUio(
|
||||||
events: Collection<Event>,
|
events: Map<Int, Map<Int, Map<Int, List<Event>>>>,
|
||||||
): Map<MonthSummaryCell, List<MonthSummaryCell>> {
|
): Map<MonthSummaryTitleUio, List<MonthSummaryItemUio>> {
|
||||||
return events
|
return events
|
||||||
.fold(hashMapOf<Event.Date, MutableList<Event>>()) { acc, event ->
|
.flatMap { yearEntry ->
|
||||||
acc.also { it.getOrPut(event.date.copy(day = 1)) { mutableListOf() }.add(event) }
|
yearEntry.value.map { monthEntry ->
|
||||||
}
|
val pills = monthEntry.value.values
|
||||||
.map { entry ->
|
.fold(
|
||||||
val pills = entry.value
|
initial = hashMapOf<Event.Pill.Id, PillFolder>(),
|
||||||
.fold(hashMapOf<Event.Pill.Id, EventFolder>()) { acc, event ->
|
) { acc, events ->
|
||||||
event.pills.forEach { pill ->
|
events.map { event ->
|
||||||
val value = acc.getOrElse(
|
event.pills.forEach { pill ->
|
||||||
key = pill.id,
|
val value = acc.getOrElse(
|
||||||
defaultValue = {
|
key = pill.id,
|
||||||
EventFolder(
|
defaultValue = {
|
||||||
label = pill.label,
|
PillFolder(label = pill.label, color = pill.color)
|
||||||
amount = 0,
|
},
|
||||||
color = pill.color,
|
|
||||||
)
|
)
|
||||||
},
|
value.amount += pill.amount
|
||||||
)
|
acc[pill.id] = value
|
||||||
value.amount += 1
|
}
|
||||||
acc[pill.id] = value
|
}
|
||||||
|
acc
|
||||||
}
|
}
|
||||||
acc
|
.map { entry ->
|
||||||
}
|
MonthSummaryPillItemUio(
|
||||||
.map { entry ->
|
label = entry.value.label,
|
||||||
MonthSummaryPillItemUio(
|
color = entry.value.color,
|
||||||
label = entry.value.label,
|
amount = entry.value.amount,
|
||||||
color = entry.value.color,
|
)
|
||||||
amount = entry.value.amount,
|
}
|
||||||
)
|
.toList()
|
||||||
}
|
.sortedWith(Comparator { s1, s2 ->
|
||||||
.toList()
|
Collator.getInstance(locale).compare(s1.label, s2.label)
|
||||||
|
})
|
||||||
|
|
||||||
MonthSummaryItemUio(
|
MonthSummaryItemUio(
|
||||||
date = calendar.apply { event = entry.key }.time,
|
date = calendar.apply {
|
||||||
days = entry.value.size,
|
set(Calendar.YEAR, yearEntry.key)
|
||||||
pills = pills,
|
set(Calendar.MONTH, monthEntry.key)
|
||||||
)
|
}.time,
|
||||||
}
|
days = monthEntry.value.values.sumOf { it.size },
|
||||||
.sortedByDescending { it.date }
|
pills = pills,
|
||||||
.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 }
|
|
||||||
}
|
}
|
||||||
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()
|
.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 {
|
return this.groupBy {
|
||||||
MonthSummaryTitleUio(
|
MonthSummaryTitleUio(
|
||||||
date = calendar.apply {
|
date = calendar.apply {
|
||||||
|
@ -112,9 +84,9 @@ class MonthSummaryFactory @Inject constructor() {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private class EventFolder(
|
private class PillFolder(
|
||||||
val label: String,
|
val label: String,
|
||||||
val color: Color,
|
val color: Color,
|
||||||
var amount: Int,
|
var amount: Int = 0,
|
||||||
)
|
)
|
||||||
}
|
}
|
|
@ -1,23 +1,13 @@
|
||||||
package com.pixelized.headache.ui.page.summary.monthly
|
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.Arrangement
|
||||||
import androidx.compose.foundation.layout.PaddingValues
|
import androidx.compose.foundation.layout.PaddingValues
|
||||||
import androidx.compose.foundation.layout.WindowInsets
|
import androidx.compose.foundation.layout.WindowInsets
|
||||||
import androidx.compose.foundation.layout.fillMaxSize
|
import androidx.compose.foundation.layout.fillMaxSize
|
||||||
import androidx.compose.foundation.layout.fillMaxWidth
|
|
||||||
import androidx.compose.foundation.layout.padding
|
import androidx.compose.foundation.layout.padding
|
||||||
import androidx.compose.foundation.lazy.LazyColumn
|
import androidx.compose.foundation.lazy.LazyColumn
|
||||||
import androidx.compose.foundation.lazy.items
|
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.ExperimentalMaterial3Api
|
||||||
import androidx.compose.material3.Icon
|
|
||||||
import androidx.compose.material3.IconButton
|
|
||||||
import androidx.compose.material3.NavigationBarDefaults
|
import androidx.compose.material3.NavigationBarDefaults
|
||||||
import androidx.compose.material3.Scaffold
|
import androidx.compose.material3.Scaffold
|
||||||
import androidx.compose.material3.Text
|
import androidx.compose.material3.Text
|
||||||
|
@ -29,7 +19,6 @@ import androidx.compose.runtime.State
|
||||||
import androidx.compose.runtime.mutableStateOf
|
import androidx.compose.runtime.mutableStateOf
|
||||||
import androidx.compose.runtime.remember
|
import androidx.compose.runtime.remember
|
||||||
import androidx.compose.ui.Modifier
|
import androidx.compose.ui.Modifier
|
||||||
import androidx.compose.ui.graphics.Color
|
|
||||||
import androidx.compose.ui.keepScreenOn
|
import androidx.compose.ui.keepScreenOn
|
||||||
import androidx.compose.ui.res.stringResource
|
import androidx.compose.ui.res.stringResource
|
||||||
import androidx.compose.ui.tooling.preview.Preview
|
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.R
|
||||||
import com.pixelized.headache.ui.navigation.destination.navigateToEventPage
|
import com.pixelized.headache.ui.navigation.destination.navigateToEventPage
|
||||||
import com.pixelized.headache.ui.navigation.main.LocalMainNavigator
|
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.MonthSummaryItem
|
||||||
import com.pixelized.headache.ui.page.summary.monthly.item.MonthSummaryItemUio
|
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.MonthSummaryPillItemUio
|
||||||
|
@ -69,7 +55,6 @@ fun MonthSummaryPage(
|
||||||
viewModel: MonthSummaryViewModel = hiltViewModel(),
|
viewModel: MonthSummaryViewModel = hiltViewModel(),
|
||||||
) {
|
) {
|
||||||
val navigation = LocalMainNavigator.current
|
val navigation = LocalMainNavigator.current
|
||||||
val boxMode = viewModel.boxMode.collectAsStateWithLifecycle()
|
|
||||||
val events = viewModel.events.collectAsStateWithLifecycle()
|
val events = viewModel.events.collectAsStateWithLifecycle()
|
||||||
|
|
||||||
MonthSummaryContent(
|
MonthSummaryContent(
|
||||||
|
@ -77,10 +62,6 @@ fun MonthSummaryPage(
|
||||||
.keepScreenOn()
|
.keepScreenOn()
|
||||||
.fillMaxSize(),
|
.fillMaxSize(),
|
||||||
events = events,
|
events = events,
|
||||||
boxMode = boxMode,
|
|
||||||
onDisplay = {
|
|
||||||
viewModel.toggleDisplay()
|
|
||||||
},
|
|
||||||
onItem = {
|
onItem = {
|
||||||
navigation.navigateToEventPage(date = it.date)
|
navigation.navigateToEventPage(date = it.date)
|
||||||
},
|
},
|
||||||
|
@ -93,10 +74,8 @@ private fun MonthSummaryContent(
|
||||||
modifier: Modifier = Modifier,
|
modifier: Modifier = Modifier,
|
||||||
spacing: Dp = MonthSummaryPageDefault.spacing,
|
spacing: Dp = MonthSummaryPageDefault.spacing,
|
||||||
listPadding: PaddingValues = MonthSummaryPageDefault.listPadding,
|
listPadding: PaddingValues = MonthSummaryPageDefault.listPadding,
|
||||||
boxMode: State<Boolean>,
|
events: State<Map<MonthSummaryTitleUio, List<MonthSummaryItemUio>>>,
|
||||||
events: State<Map<MonthSummaryCell, List<MonthSummaryCell>>>,
|
onItem: (MonthSummaryItemUio) -> Unit,
|
||||||
onDisplay: () -> Unit,
|
|
||||||
onItem: (MonthSummaryCell) -> Unit,
|
|
||||||
) {
|
) {
|
||||||
Scaffold(
|
Scaffold(
|
||||||
modifier = modifier,
|
modifier = modifier,
|
||||||
|
@ -109,24 +88,6 @@ private fun MonthSummaryContent(
|
||||||
title = {
|
title = {
|
||||||
Text(text = stringResource(R.string.month_summary_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 ->
|
content = { paddingValues ->
|
||||||
|
@ -140,24 +101,16 @@ private fun MonthSummaryContent(
|
||||||
) {
|
) {
|
||||||
events.value.forEach { entry ->
|
events.value.forEach { entry ->
|
||||||
item {
|
item {
|
||||||
MonthSummaryCell(
|
MonthSummaryTitle(
|
||||||
modifier = Modifier.padding(top = 16.dp),
|
modifier = Modifier.padding(top = 16.dp),
|
||||||
item = entry.key,
|
item = entry.key,
|
||||||
onItem = onItem,
|
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
items(
|
items(
|
||||||
items = entry.value,
|
items = entry.value,
|
||||||
key = { item -> item.date },
|
key = { item -> item.date },
|
||||||
contentType = { item ->
|
|
||||||
when (item) {
|
|
||||||
is MonthSummaryBoxUio -> "MonthSummaryBoxUio"
|
|
||||||
is MonthSummaryItemUio -> "MonthSummaryItemUio"
|
|
||||||
is MonthSummaryTitleUio -> "MonthSummaryTitleUio"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
) { item ->
|
) { item ->
|
||||||
MonthSummaryCell(
|
MonthSummaryItem(
|
||||||
item = item,
|
item = item,
|
||||||
onItem = onItem,
|
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
|
@Composable
|
||||||
@Preview
|
@Preview
|
||||||
private fun MonthSummaryPreview() {
|
private fun MonthSummaryPreview() {
|
||||||
HeadacheTheme {
|
HeadacheTheme {
|
||||||
MonthSummaryContent(
|
val events = remember {
|
||||||
boxMode = remember { mutableStateOf(false) },
|
mutableStateOf(
|
||||||
events = remember {
|
mapOf(
|
||||||
mutableStateOf(
|
MonthSummaryTitleUio(
|
||||||
mapOf(
|
date = Date(),
|
||||||
MonthSummaryTitleUio(
|
) to listOf(
|
||||||
|
MonthSummaryItemUio(
|
||||||
date = Date(),
|
date = Date(),
|
||||||
) to listOf(
|
days = 8,
|
||||||
MonthSummaryItemUio(
|
pills = listOf(
|
||||||
date = Date(),
|
MonthSummaryPillItemUio(
|
||||||
days = 8,
|
label = "Spifen 400",
|
||||||
pills = listOf(
|
amount = 4,
|
||||||
MonthSummaryPillItemUio(
|
color = HeadacheColorPalette.Pill.Spifen400,
|
||||||
label = "Spifen 400",
|
),
|
||||||
amount = 4,
|
MonthSummaryPillItemUio(
|
||||||
color = HeadacheColorPalette.Pill.Spifen400,
|
label = "Élétriptan 40",
|
||||||
),
|
amount = 2,
|
||||||
MonthSummaryPillItemUio(
|
color = HeadacheColorPalette.Pill.Eletriptan40,
|
||||||
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 = { },
|
onItem = { },
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
|
@ -2,15 +2,11 @@ package com.pixelized.headache.ui.page.summary.monthly
|
||||||
|
|
||||||
import androidx.lifecycle.ViewModel
|
import androidx.lifecycle.ViewModel
|
||||||
import androidx.lifecycle.viewModelScope
|
import androidx.lifecycle.viewModelScope
|
||||||
import com.pixelized.headache.repository.event.Event
|
|
||||||
import com.pixelized.headache.repository.event.EventRepository
|
import com.pixelized.headache.repository.event.EventRepository
|
||||||
import com.pixelized.headache.ui.page.summary.monthly.item.MonthSummaryCell
|
|
||||||
import dagger.hilt.android.lifecycle.HiltViewModel
|
import dagger.hilt.android.lifecycle.HiltViewModel
|
||||||
import kotlinx.coroutines.ExperimentalCoroutinesApi
|
import kotlinx.coroutines.ExperimentalCoroutinesApi
|
||||||
import kotlinx.coroutines.flow.MutableStateFlow
|
|
||||||
import kotlinx.coroutines.flow.SharingStarted
|
import kotlinx.coroutines.flow.SharingStarted
|
||||||
import kotlinx.coroutines.flow.StateFlow
|
import kotlinx.coroutines.flow.map
|
||||||
import kotlinx.coroutines.flow.combine
|
|
||||||
import kotlinx.coroutines.flow.stateIn
|
import kotlinx.coroutines.flow.stateIn
|
||||||
import javax.inject.Inject
|
import javax.inject.Inject
|
||||||
|
|
||||||
|
@ -20,26 +16,13 @@ class MonthSummaryViewModel @Inject constructor(
|
||||||
eventItemFactory: MonthSummaryFactory,
|
eventItemFactory: MonthSummaryFactory,
|
||||||
) : ViewModel() {
|
) : ViewModel() {
|
||||||
|
|
||||||
private val displayTypeFlow = MutableStateFlow(false)
|
|
||||||
val boxMode: StateFlow<Boolean> = displayTypeFlow
|
|
||||||
|
|
||||||
@OptIn(ExperimentalCoroutinesApi::class)
|
@OptIn(ExperimentalCoroutinesApi::class)
|
||||||
val events: StateFlow<Map<MonthSummaryCell, List<MonthSummaryCell>>> = combine(
|
val events = eventRepository.eventsMapFlow()
|
||||||
eventRepository.eventsListFlow(),
|
.map { events ->
|
||||||
displayTypeFlow,
|
eventItemFactory.convertToItemUio(events = events)
|
||||||
transform = { events: Collection<Event>, display ->
|
}.stateIn(
|
||||||
when (display) {
|
scope = viewModelScope,
|
||||||
true -> eventItemFactory.convertToBoxUio(events = events)
|
started = SharingStarted.Lazily,
|
||||||
else -> eventItemFactory.convertToItemUio(events = events)
|
initialValue = emptyMap(),
|
||||||
}
|
)
|
||||||
}
|
|
||||||
).stateIn(
|
|
||||||
scope = viewModelScope,
|
|
||||||
started = SharingStarted.Lazily,
|
|
||||||
initialValue = emptyMap(),
|
|
||||||
)
|
|
||||||
|
|
||||||
fun toggleDisplay() {
|
|
||||||
displayTypeFlow.value = displayTypeFlow.value.not()
|
|
||||||
}
|
|
||||||
}
|
}
|
|
@ -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
|
@Stable
|
||||||
data class MonthSummaryItemUio(
|
data class MonthSummaryItemUio(
|
||||||
override val date: Date,
|
val date: Date,
|
||||||
val days: Int,
|
val days: Int,
|
||||||
val pills: List<MonthSummaryPillItemUio>,
|
val pills: List<MonthSummaryPillItemUio>,
|
||||||
) : MonthSummaryCell
|
)
|
||||||
|
|
||||||
@Stable
|
@Stable
|
||||||
object MonthSummaryItemDefault {
|
object MonthSummaryItemDefault {
|
||||||
|
|
|
@ -15,8 +15,8 @@ import java.util.Locale
|
||||||
|
|
||||||
@Stable
|
@Stable
|
||||||
data class MonthSummaryTitleUio(
|
data class MonthSummaryTitleUio(
|
||||||
override val date: Date,
|
val date: Date,
|
||||||
) : MonthSummaryCell
|
)
|
||||||
|
|
||||||
@Stable
|
@Stable
|
||||||
object MonthSummaryTitleDefault {
|
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() {
|
class YearSummaryFactory @Inject constructor() {
|
||||||
|
|
||||||
fun convertToUio(
|
fun convertToUio(
|
||||||
events: Map<Int, Map<Int, Map<Int, Event>>>,
|
events: Map<Int, Map<Int, Map<Int, List<Event>>>>,
|
||||||
): List<YearUio> {
|
): List<YearUio> {
|
||||||
val monthFirstDayCalendar = Calendar.getInstance().apply {
|
val monthFirstDayCalendar = Calendar.getInstance().apply {
|
||||||
firstDayOfWeek = Calendar.MONDAY
|
firstDayOfWeek = Calendar.MONDAY
|
||||||
|
@ -49,7 +49,7 @@ class YearSummaryFactory @Inject constructor() {
|
||||||
}
|
}
|
||||||
val weeks = (1..monthLastDayCalendar.get(Calendar.DAY_OF_MONTH))
|
val weeks = (1..monthLastDayCalendar.get(Calendar.DAY_OF_MONTH))
|
||||||
.fold(initial = initial) { accumulator, dayNumber ->
|
.fold(initial = initial) { accumulator, dayNumber ->
|
||||||
val event: Event? = monthEntry.value.get(dayNumber)
|
val event: List<Event> = monthEntry.value[dayNumber] ?: emptyList()
|
||||||
val weekIndex = currentDayCalendar
|
val weekIndex = currentDayCalendar
|
||||||
.apply {
|
.apply {
|
||||||
set(Calendar.YEAR, yearEntry.key)
|
set(Calendar.YEAR, yearEntry.key)
|
||||||
|
@ -60,8 +60,8 @@ class YearSummaryFactory @Inject constructor() {
|
||||||
.get(Calendar.WEEK_OF_MONTH) - 1
|
.get(Calendar.WEEK_OF_MONTH) - 1
|
||||||
val day = DayUio(
|
val day = DayUio(
|
||||||
number = dayNumber,
|
number = dayNumber,
|
||||||
headache = event != null,
|
headache = event.isNotEmpty(),
|
||||||
pills = event?.pills?.map { it.color } ?: emptyList(),
|
pills = event.flatMap { it.pills.map { pill -> pill.color } },
|
||||||
)
|
)
|
||||||
accumulator.also { acc ->
|
accumulator.also { acc ->
|
||||||
acc[weekIndex] = acc.get(index = weekIndex).also { week ->
|
acc[weekIndex] = acc.get(index = weekIndex).also { week ->
|
||||||
|
|
|
@ -142,7 +142,7 @@ fun YearSummaryMonth(
|
||||||
width = localCellSize.width - 0f,
|
width = localCellSize.width - 0f,
|
||||||
height = localCellSize.height - (pillSize.height + space) * 2,
|
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)
|
@OptIn(ExperimentalMaterial3Api::class)
|
||||||
@Composable
|
@Composable
|
||||||
fun YearSummaryContent(
|
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
|
@Composable
|
||||||
@Preview()
|
@Preview()
|
||||||
private fun YearSummaryPreview(
|
private fun YearSummaryPreview(
|
||||||
|
|
|
@ -3,14 +3,6 @@ package com.pixelized.headache.ui.theme.color
|
||||||
import androidx.compose.ui.graphics.Color
|
import androidx.compose.ui.graphics.Color
|
||||||
import javax.annotation.concurrent.Immutable
|
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
|
@Immutable
|
||||||
object HeadacheColorPalette {
|
object HeadacheColorPalette {
|
||||||
|
|
||||||
|
@ -23,6 +15,12 @@ object HeadacheColorPalette {
|
||||||
val Eletriptan40 = Additional.VeryLightPink
|
val Eletriptan40 = Additional.VeryLightPink
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Immutable
|
||||||
|
object Calendar {
|
||||||
|
val Headache = Additional.LightRed
|
||||||
|
val Pill = Additional.DarkRed
|
||||||
|
}
|
||||||
|
|
||||||
@Immutable
|
@Immutable
|
||||||
object Additional {
|
object Additional {
|
||||||
val VeryDarkBlue: Color = Color(0xFF09179D)
|
val VeryDarkBlue: Color = Color(0xFF09179D)
|
||||||
|
|
|
@ -40,7 +40,7 @@ data class HeadacheColors(
|
||||||
fun headacheDarkColorScheme(
|
fun headacheDarkColorScheme(
|
||||||
base: ColorScheme = darkColorScheme(),
|
base: ColorScheme = darkColorScheme(),
|
||||||
calendar: HeadacheColors.Calendar = HeadacheColors.Calendar(
|
calendar: HeadacheColors.Calendar = HeadacheColors.Calendar(
|
||||||
headache = HeadacheColorPalette.Additional.Red,
|
headache = HeadacheColorPalette.Calendar.Headache,
|
||||||
onHeadache = Color.White,
|
onHeadache = Color.White,
|
||||||
),
|
),
|
||||||
pill: HeadacheColors.Pill = HeadacheColors.Pill(
|
pill: HeadacheColors.Pill = HeadacheColors.Pill(
|
||||||
|
@ -61,7 +61,7 @@ fun headacheDarkColorScheme(
|
||||||
fun headacheLightColorScheme(
|
fun headacheLightColorScheme(
|
||||||
base: ColorScheme = lightColorScheme(),
|
base: ColorScheme = lightColorScheme(),
|
||||||
calendar: HeadacheColors.Calendar = HeadacheColors.Calendar(
|
calendar: HeadacheColors.Calendar = HeadacheColors.Calendar(
|
||||||
headache = HeadacheColorPalette.Additional.Red,
|
headache = HeadacheColorPalette.Calendar.Headache,
|
||||||
onHeadache = Color.White,
|
onHeadache = Color.White,
|
||||||
),
|
),
|
||||||
pill: HeadacheColors.Pill = HeadacheColors.Pill(
|
pill: HeadacheColors.Pill = HeadacheColors.Pill(
|
||||||
|
@ -94,7 +94,7 @@ fun calculateElevatedColor(
|
||||||
|
|
||||||
@ReadOnlyComposable
|
@ReadOnlyComposable
|
||||||
@Composable
|
@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
|
val alpha = ((4.5f * ln(elevation.value + 1)) + 2f) / 100f
|
||||||
return color.copy(alpha = alpha)
|
return color.copy(alpha = alpha)
|
||||||
}
|
}
|
Loading…
Add table
Add a link
Reference in a new issue