Add event add and edit
This commit is contained in:
parent
2c5d9b6df1
commit
62c3639a9e
44 changed files with 1897 additions and 473 deletions
3
.idea/deploymentTargetSelector.xml
generated
3
.idea/deploymentTargetSelector.xml
generated
|
|
@ -14,6 +14,9 @@
|
|||
<SelectionState runConfigName="text2Pills_3()">
|
||||
<option name="selectionMode" value="DROPDOWN" />
|
||||
</SelectionState>
|
||||
<SelectionState runConfigName="EventEditPreview">
|
||||
<option name="selectionMode" value="DROPDOWN" />
|
||||
</SelectionState>
|
||||
</selectionStates>
|
||||
</component>
|
||||
</project>
|
||||
1
.idea/misc.xml
generated
1
.idea/misc.xml
generated
|
|
@ -1,4 +1,3 @@
|
|||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<project version="4">
|
||||
<component name="DeviceStreaming">
|
||||
<option name="deviceSelectionList">
|
||||
|
|
|
|||
|
|
@ -51,7 +51,7 @@ android {
|
|||
isMinifyEnabled = true
|
||||
signingConfig = signingConfigs.getByName("pixelized")
|
||||
defaultConfig {
|
||||
versionCode = 1 // getGitBuildNumber()
|
||||
versionCode = getGitBuildNumber()
|
||||
}
|
||||
proguardFiles(
|
||||
getDefaultProguardFile("proguard-android-optimize.txt"),
|
||||
|
|
@ -65,6 +65,7 @@ android {
|
|||
}
|
||||
kotlinOptions {
|
||||
jvmTarget = "11"
|
||||
freeCompilerArgs = listOf("-XXLanguage:+PropertyParamAnnotationDefaultTargetMode")
|
||||
}
|
||||
buildFeatures {
|
||||
compose = true
|
||||
|
|
@ -116,7 +117,7 @@ private fun getGitBuildNumber(
|
|||
standardOutput = stdout
|
||||
}
|
||||
stdout.toString(charset).trim().toInt()
|
||||
} catch (e: Exception) {
|
||||
0
|
||||
} catch (_: Exception) {
|
||||
1
|
||||
}
|
||||
}
|
||||
Binary file not shown.
Binary file not shown.
|
|
@ -1,5 +1,7 @@
|
|||
package com.pixelized.headache.repository.event
|
||||
|
||||
import androidx.compose.ui.graphics.Color
|
||||
|
||||
data class Event(
|
||||
val id: Long,
|
||||
val title: String,
|
||||
|
|
@ -25,74 +27,21 @@ data class Event(
|
|||
}
|
||||
}
|
||||
|
||||
sealed class Pill(
|
||||
data class Pill(
|
||||
val id: Id,
|
||||
val label: String,
|
||||
val amount: Int,
|
||||
val description: String,
|
||||
val color: Color,
|
||||
val isValid: Boolean,
|
||||
val isMisspelled: Boolean,
|
||||
) {
|
||||
class Unknown(
|
||||
amount: Int,
|
||||
description: String,
|
||||
) : Pill(
|
||||
label = "?",
|
||||
amount = amount,
|
||||
description = description,
|
||||
isValid = false,
|
||||
isMisspelled = false,
|
||||
)
|
||||
|
||||
class Ibuprofene400(
|
||||
amount: Int,
|
||||
description: String,
|
||||
isValid: Boolean,
|
||||
isMisspelled: Boolean,
|
||||
) : Pill(
|
||||
label = "Ibuprofène 400",
|
||||
amount = amount,
|
||||
description = description,
|
||||
isValid = isValid,
|
||||
isMisspelled = isMisspelled,
|
||||
)
|
||||
|
||||
class Paracetamol1000(
|
||||
amount: Int,
|
||||
description: String,
|
||||
isValid: Boolean,
|
||||
isMisspelled: Boolean,
|
||||
) : Pill(
|
||||
label = "Paracétamol 1000",
|
||||
amount = amount,
|
||||
description = description,
|
||||
isValid = isValid,
|
||||
isMisspelled = isMisspelled,
|
||||
)
|
||||
|
||||
class Spifen400(
|
||||
amount: Int,
|
||||
description: String,
|
||||
isValid: Boolean,
|
||||
isMisspelled: Boolean,
|
||||
) : Pill(
|
||||
label = "Spifen 400",
|
||||
amount = amount,
|
||||
description = description,
|
||||
isValid = isValid,
|
||||
isMisspelled = isMisspelled,
|
||||
)
|
||||
|
||||
class Eletriptan40(
|
||||
amount: Int,
|
||||
description: String,
|
||||
isValid: Boolean,
|
||||
isMisspelled: Boolean,
|
||||
) : Pill(
|
||||
label = "Élétriptan 40",
|
||||
amount = amount,
|
||||
description = description,
|
||||
isValid = isValid,
|
||||
isMisspelled = isMisspelled,
|
||||
)
|
||||
enum class Id(val value: Long) {
|
||||
UNKNOWN(-1L),
|
||||
IBUPROFENE_400(0L),
|
||||
PARACETAMOL_1000(1L),
|
||||
SPIFEN_400(2L),
|
||||
ELETRIPTAN_40(3L),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,158 +0,0 @@
|
|||
package com.pixelized.headache.repository.event
|
||||
|
||||
|
||||
import android.icu.util.Calendar
|
||||
import com.pixelized.headache.utils.extention.event
|
||||
import java.util.Date
|
||||
import java.util.regex.Pattern
|
||||
import javax.inject.Inject
|
||||
|
||||
class EventFactory @Inject constructor() {
|
||||
private val amountSplitRegex = Pattern.compile("""\s[xX]\s*""")
|
||||
private val calendar = Calendar.getInstance()
|
||||
|
||||
fun parseEvent(
|
||||
start: Long,
|
||||
id: Long,
|
||||
title: String,
|
||||
description: String,
|
||||
): Event {
|
||||
val date = calendar.apply { time = Date(start) }.event
|
||||
val pills = description
|
||||
.takeIf { it.isNotBlank() }
|
||||
?.split("\n")
|
||||
?.mapNotNull {
|
||||
when {
|
||||
it.isBlank() -> null
|
||||
else -> parsePill(it)
|
||||
}
|
||||
}
|
||||
?: listOf()
|
||||
|
||||
return Event(
|
||||
id = id,
|
||||
title = title,
|
||||
description = description,
|
||||
date = date,
|
||||
pills = pills,
|
||||
isValid = pills.all { it.isValid },
|
||||
isMisspelled = pills.any { it.isMisspelled },
|
||||
)
|
||||
}
|
||||
|
||||
@Throws(Exception::class)
|
||||
fun parsePill(pill: String): Event.Pill {
|
||||
val split = pill.split(regex = amountSplitRegex)
|
||||
val label = split.getOrNull(0) ?: error("missing label")
|
||||
val amount = split.getOrNull(1)?.trim()?.toIntOrNull() ?: 1
|
||||
|
||||
return when {
|
||||
label.contains(SPIFEN_400, ignoreCase = true) -> Event.Pill.Spifen400(
|
||||
amount = amount,
|
||||
description = pill,
|
||||
isValid = true,
|
||||
isMisspelled = false,
|
||||
)
|
||||
|
||||
label.contains(ELETRIPTAN_40, ignoreCase = true) -> Event.Pill.Eletriptan40(
|
||||
amount = amount,
|
||||
description = pill,
|
||||
isValid = true,
|
||||
isMisspelled = false,
|
||||
)
|
||||
|
||||
label.contains(IBUPROFENE_400, ignoreCase = true) -> Event.Pill.Ibuprofene400(
|
||||
amount = amount,
|
||||
description = pill,
|
||||
isValid = true,
|
||||
isMisspelled = false,
|
||||
)
|
||||
|
||||
label.contains(PARACETAMOL_1000, ignoreCase = true) -> Event.Pill.Paracetamol1000(
|
||||
amount = amount,
|
||||
description = pill,
|
||||
isValid = true,
|
||||
isMisspelled = false,
|
||||
)
|
||||
|
||||
SPIFEN_400_LIST.any { label.contains(it, ignoreCase = true) } -> Event.Pill.Spifen400(
|
||||
amount = amount,
|
||||
description = pill,
|
||||
isValid = true,
|
||||
isMisspelled = true,
|
||||
)
|
||||
|
||||
ELETRIPTAN_40_LIST.any {
|
||||
label.contains(
|
||||
it,
|
||||
ignoreCase = true
|
||||
)
|
||||
} -> Event.Pill.Eletriptan40(
|
||||
amount = amount,
|
||||
description = pill,
|
||||
isValid = true,
|
||||
isMisspelled = true,
|
||||
)
|
||||
|
||||
IBUPROFENE_400_LIST.any {
|
||||
label.contains(
|
||||
it,
|
||||
ignoreCase = true
|
||||
)
|
||||
} -> Event.Pill.Ibuprofene400(
|
||||
amount = amount,
|
||||
description = pill,
|
||||
isValid = true,
|
||||
isMisspelled = true,
|
||||
)
|
||||
|
||||
PARACETAMOL_1000_LIST.any {
|
||||
label.contains(
|
||||
it,
|
||||
ignoreCase = true
|
||||
)
|
||||
} -> Event.Pill.Paracetamol1000(
|
||||
amount = amount,
|
||||
description = pill,
|
||||
isValid = true,
|
||||
isMisspelled = true,
|
||||
)
|
||||
|
||||
else -> Event.Pill.Unknown(
|
||||
amount = amount,
|
||||
description = pill,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
companion object {
|
||||
private const val PARACETAMOL_1000 = "Paracétamol 1000"
|
||||
private val PARACETAMOL_1000_LIST = listOf<String>(
|
||||
"Doliprane 1000",
|
||||
)
|
||||
|
||||
private const val IBUPROFENE_400 = "Ibuprofène 400"
|
||||
private val IBUPROFENE_400_LIST = listOf(
|
||||
"Ibuprofene 400",
|
||||
"Ibuprofen 400",
|
||||
"Ibuprofen",
|
||||
)
|
||||
|
||||
private const val SPIFEN_400 = "Spifen 400"
|
||||
private val SPIFEN_400_LIST = listOf<String>(
|
||||
"Spifen",
|
||||
)
|
||||
|
||||
private const val ELETRIPTAN_40 = "Élétriptan 40"
|
||||
private val ELETRIPTAN_40_LIST = listOf(
|
||||
"Eletriptan 40",
|
||||
"Életriptan 40",
|
||||
"Élitriptan 40",
|
||||
"Elitriptan 40",
|
||||
"Eletripan 40",
|
||||
"Elétripan 40",
|
||||
"Elitripan 40",
|
||||
"Elitripan",
|
||||
)
|
||||
}
|
||||
}
|
||||
|
|
@ -1,17 +1,27 @@
|
|||
package com.pixelized.headache.repository.event
|
||||
|
||||
import android.content.ContentUris
|
||||
import android.content.ContentValues
|
||||
import android.content.Context
|
||||
import android.icu.util.Calendar
|
||||
import android.icu.util.TimeZone
|
||||
import android.net.Uri
|
||||
import android.provider.CalendarContract
|
||||
import com.pixelized.headache.repository.calendar.GoogleCalendarIdRepository
|
||||
import com.pixelized.headache.repository.event.factory.EventFactory
|
||||
import com.pixelized.headache.utils.extention.event
|
||||
import dagger.hilt.android.qualifiers.ApplicationContext
|
||||
import kotlinx.coroutines.CoroutineScope
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.Job
|
||||
import kotlinx.coroutines.flow.MutableStateFlow
|
||||
import kotlinx.coroutines.flow.SharingStarted
|
||||
import kotlinx.coroutines.flow.StateFlow
|
||||
import kotlinx.coroutines.launch
|
||||
import kotlinx.coroutines.flow.launchIn
|
||||
import kotlinx.coroutines.flow.map
|
||||
import kotlinx.coroutines.flow.onEach
|
||||
import kotlinx.coroutines.flow.stateIn
|
||||
import kotlinx.coroutines.withContext
|
||||
import javax.inject.Inject
|
||||
import javax.inject.Singleton
|
||||
|
||||
|
|
@ -21,41 +31,120 @@ class EventRepository @Inject constructor(
|
|||
private val calendarIdRepository: GoogleCalendarIdRepository,
|
||||
private val factory: EventFactory,
|
||||
) {
|
||||
private val timeZone = TimeZone.getTimeZone("UTC")
|
||||
private val scope = CoroutineScope(Dispatchers.IO + Job())
|
||||
private val eventFlow = hashMapOf<Long, MutableStateFlow<List<Event>>>()
|
||||
private val eventFlow = MutableStateFlow(mapOf<Long, Event>())
|
||||
|
||||
private val eventListFlow: StateFlow<Collection<Event>> = eventFlow
|
||||
.map { events -> events.values.sortedBy { it.date } }
|
||||
.stateIn(
|
||||
scope = scope,
|
||||
started = SharingStarted.Lazily,
|
||||
initialValue = emptyList(),
|
||||
)
|
||||
|
||||
init {
|
||||
scope.launch {
|
||||
calendarIdRepository.calendarId.collect { id ->
|
||||
calendarIdRepository.calendarId
|
||||
.onEach { id ->
|
||||
fetchEvents(
|
||||
calendarId = id,
|
||||
startDate = Event.Date(day = 1, month = Calendar.JANUARY, year = 2023),
|
||||
endDate = Calendar.getInstance().apply { add(Calendar.DAY_OF_MONTH, 1) }.event,
|
||||
refresh = true,
|
||||
)
|
||||
}
|
||||
.launchIn(scope = scope)
|
||||
}
|
||||
|
||||
fun eventsFlow(): StateFlow<Map<Long, Event>> = eventFlow
|
||||
|
||||
fun eventsListFlow(): StateFlow<Collection<Event>> = eventListFlow
|
||||
|
||||
fun event(id: Long): Event? = eventFlow.value.get(key = id)
|
||||
|
||||
fun createEvent(
|
||||
calendarId: Long = calendarIdRepository.calendarId.value,
|
||||
title: String,
|
||||
description: String,
|
||||
year: Int,
|
||||
month: Int,
|
||||
day: Int,
|
||||
): Boolean {
|
||||
val (startMillis, endMillis) = Calendar.getInstance(timeZone).apply {
|
||||
set(year, month, day, 0, 0, 0)
|
||||
set(Calendar.MILLISECOND, 0)
|
||||
}.let {
|
||||
val startMillis = it.timeInMillis
|
||||
it.add(Calendar.DAY_OF_MONTH, 1)
|
||||
val endMillis = it.timeInMillis
|
||||
startMillis to endMillis
|
||||
}
|
||||
|
||||
val values = ContentValues().apply {
|
||||
put(CalendarContract.Events.CALENDAR_ID, calendarId)
|
||||
put(CalendarContract.Events.TITLE, title)
|
||||
put(CalendarContract.Events.DESCRIPTION, description)
|
||||
put(CalendarContract.Events.DTSTART, startMillis)
|
||||
put(CalendarContract.Events.DTEND, endMillis)
|
||||
put(CalendarContract.Events.AVAILABILITY, CalendarContract.Events.AVAILABILITY_FREE)
|
||||
put(CalendarContract.Events.ALL_DAY, 1)
|
||||
put(CalendarContract.Events.EVENT_TIMEZONE, "UTC")
|
||||
}
|
||||
|
||||
return context.contentResolver.insert(
|
||||
CalendarContract.Events.CONTENT_URI,
|
||||
values
|
||||
) != null
|
||||
}
|
||||
|
||||
fun eventFlow(
|
||||
calendarId: Long,
|
||||
): StateFlow<List<Event>> {
|
||||
return eventFlow.getOrPut(calendarId) { MutableStateFlow(emptyList()) }
|
||||
fun updateEvent(
|
||||
id: Long,
|
||||
title: String? = null,
|
||||
description: String? = null,
|
||||
year: Int,
|
||||
month: Int,
|
||||
day: Int,
|
||||
): Boolean {
|
||||
val (startMillis, endMillis) = Calendar.getInstance(timeZone).apply {
|
||||
set(year, month, day, 0, 0, 0)
|
||||
set(Calendar.MILLISECOND, 0)
|
||||
}.let {
|
||||
val startMillis = it.timeInMillis
|
||||
it.add(Calendar.DAY_OF_MONTH, 1)
|
||||
val endMillis = it.timeInMillis
|
||||
startMillis to endMillis
|
||||
}
|
||||
|
||||
val values = ContentValues().apply {
|
||||
title?.let { put(CalendarContract.Events.TITLE, it) }
|
||||
description?.let { put(CalendarContract.Events.DESCRIPTION, it) }
|
||||
put(CalendarContract.Events.DTSTART, startMillis)
|
||||
put(CalendarContract.Events.DTEND, endMillis)
|
||||
}
|
||||
|
||||
val updateUri: Uri = ContentUris.withAppendedId(CalendarContract.Events.CONTENT_URI, id)
|
||||
val rows: Int = context.contentResolver.update(updateUri, values, null, null)
|
||||
|
||||
return rows > 0
|
||||
}
|
||||
|
||||
fun fetchEvents(
|
||||
calendarId: Long,
|
||||
fun deleteEvent(
|
||||
eventId: Long,
|
||||
): Boolean {
|
||||
val deleteUri = ContentUris.withAppendedId(CalendarContract.Events.CONTENT_URI, eventId)
|
||||
val success = context.contentResolver.delete(deleteUri, null, null) > 0
|
||||
if (success) {
|
||||
eventFlow.value = eventFlow.value.toMutableMap().also {
|
||||
it.remove(eventId)
|
||||
}
|
||||
}
|
||||
return success
|
||||
}
|
||||
|
||||
suspend fun fetchEvents(
|
||||
calendarId: Long = calendarIdRepository.calendarId.value,
|
||||
startDate: Event.Date,
|
||||
endDate: Event.Date,
|
||||
refresh: Boolean,
|
||||
): List<Event> {
|
||||
val flow = eventFlow.getOrPut(calendarId) { MutableStateFlow(emptyList()) }
|
||||
if (flow.value.isNotEmpty() && refresh.not()) {
|
||||
return flow.value
|
||||
}
|
||||
|
||||
val events = mutableListOf<Event>()
|
||||
|
||||
) = withContext(Dispatchers.IO) {
|
||||
val startCalendar = Calendar.getInstance().apply { this.event = startDate }
|
||||
val endCalendar = Calendar.getInstance().apply { this.event = endDate }
|
||||
|
||||
|
|
@ -84,25 +173,23 @@ class EventRepository @Inject constructor(
|
|||
"${CalendarContract.Events.DTSTART} ASC"
|
||||
)
|
||||
|
||||
cursor?.use {
|
||||
val idIdx = it.getColumnIndex(CalendarContract.Events._ID)
|
||||
val titleIdx = it.getColumnIndex(CalendarContract.Events.TITLE)
|
||||
val descIdx = it.getColumnIndex(CalendarContract.Events.DESCRIPTION)
|
||||
val startIdx = it.getColumnIndex(CalendarContract.Events.DTSTART)
|
||||
eventFlow.value = eventFlow.value.toMutableMap().also { events ->
|
||||
cursor?.use {
|
||||
val idIdx = it.getColumnIndex(CalendarContract.Events._ID)
|
||||
val titleIdx = it.getColumnIndex(CalendarContract.Events.TITLE)
|
||||
val descIdx = it.getColumnIndex(CalendarContract.Events.DESCRIPTION)
|
||||
val startIdx = it.getColumnIndex(CalendarContract.Events.DTSTART)
|
||||
|
||||
while (it.moveToNext()) {
|
||||
val event = factory.parseEvent(
|
||||
id = it.getLong(idIdx),
|
||||
title = it.getString(titleIdx) ?: "",
|
||||
description = it.getString(descIdx) ?: "",
|
||||
start = it.getLong(startIdx),
|
||||
)
|
||||
events.add(event)
|
||||
while (it.moveToNext()) {
|
||||
val event = factory.parseEvent(
|
||||
id = it.getLong(idIdx),
|
||||
title = it.getString(titleIdx) ?: "",
|
||||
description = it.getString(descIdx) ?: "",
|
||||
start = it.getLong(startIdx),
|
||||
)
|
||||
events[event.id] = event
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
flow.value = events
|
||||
|
||||
return events
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,42 @@
|
|||
package com.pixelized.headache.repository.event.factory
|
||||
|
||||
import android.icu.util.Calendar
|
||||
import com.pixelized.headache.repository.event.Event
|
||||
import com.pixelized.headache.utils.extention.event
|
||||
import java.util.Date
|
||||
import javax.inject.Inject
|
||||
|
||||
class EventFactory @Inject constructor(
|
||||
private val pillFactory: PillFactory,
|
||||
) {
|
||||
private val calendar = Calendar.getInstance()
|
||||
|
||||
fun parseEvent(
|
||||
start: Long,
|
||||
id: Long,
|
||||
title: String,
|
||||
description: String,
|
||||
): Event {
|
||||
val date = calendar.apply { time = Date(start) }.event
|
||||
val pills = description
|
||||
.takeIf { it.isNotBlank() }
|
||||
?.split("\n")
|
||||
?.mapNotNull {
|
||||
when {
|
||||
it.isBlank() -> null
|
||||
else -> pillFactory.parsePill(pill = it)
|
||||
}
|
||||
}
|
||||
?: listOf()
|
||||
|
||||
return Event(
|
||||
id = id,
|
||||
title = title,
|
||||
description = description,
|
||||
date = date,
|
||||
pills = pills,
|
||||
isValid = pills.all { it.isValid },
|
||||
isMisspelled = pills.any { it.isMisspelled },
|
||||
)
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,158 @@
|
|||
package com.pixelized.headache.repository.event.factory
|
||||
|
||||
import com.pixelized.headache.repository.event.Event
|
||||
import com.pixelized.headache.ui.theme.color.HeadacheColorPalette
|
||||
import java.util.regex.Pattern
|
||||
import javax.inject.Inject
|
||||
|
||||
class PillFactory @Inject constructor() {
|
||||
private val amountSplitRegex = Pattern.compile("""\s[xX]\s*""")
|
||||
|
||||
@Throws(Exception::class)
|
||||
fun parsePill(pill: String): Event.Pill {
|
||||
val split = pill.split(regex = amountSplitRegex)
|
||||
val label = split.getOrNull(0) ?: error("missing label")
|
||||
val amount = split.getOrNull(1)?.trim()?.toIntOrNull() ?: 1
|
||||
|
||||
return when {
|
||||
label == SPIFEN_400_LABEL -> {
|
||||
Event.Pill(
|
||||
id = Event.Pill.Id.SPIFEN_400,
|
||||
label = SPIFEN_400_LABEL,
|
||||
color = HeadacheColorPalette.Pill.Spifen400,
|
||||
amount = amount,
|
||||
description = pill,
|
||||
isValid = true,
|
||||
isMisspelled = false,
|
||||
)
|
||||
}
|
||||
|
||||
label.contains(ELETRIPTAN_40_LABEL, ignoreCase = true) -> {
|
||||
Event.Pill(
|
||||
id = Event.Pill.Id.ELETRIPTAN_40,
|
||||
label = ELETRIPTAN_40_LABEL,
|
||||
color = HeadacheColorPalette.Pill.Eletriptan40,
|
||||
amount = amount,
|
||||
description = pill,
|
||||
isValid = true,
|
||||
isMisspelled = false,
|
||||
)
|
||||
}
|
||||
|
||||
label.contains(IBUPROFENE_400_LABEL, ignoreCase = true) -> {
|
||||
Event.Pill(
|
||||
id = Event.Pill.Id.IBUPROFENE_400,
|
||||
label = IBUPROFENE_400_LABEL,
|
||||
color = HeadacheColorPalette.Pill.Ibuprofene400,
|
||||
amount = amount,
|
||||
description = pill,
|
||||
isValid = true,
|
||||
isMisspelled = false,
|
||||
)
|
||||
}
|
||||
|
||||
label.contains(PARACETAMOL_1000_LABEL, ignoreCase = true) -> {
|
||||
Event.Pill(
|
||||
id = Event.Pill.Id.PARACETAMOL_1000,
|
||||
label = PARACETAMOL_1000_LABEL,
|
||||
color = HeadacheColorPalette.Pill.Paracetamol1000,
|
||||
amount = amount,
|
||||
description = pill,
|
||||
isValid = true,
|
||||
isMisspelled = false,
|
||||
)
|
||||
}
|
||||
|
||||
SPIFEN_400_LIST.any { label.contains(it, ignoreCase = true) } -> {
|
||||
Event.Pill(
|
||||
id = Event.Pill.Id.SPIFEN_400,
|
||||
label = SPIFEN_400_LABEL,
|
||||
color = HeadacheColorPalette.Pill.Spifen400,
|
||||
amount = amount,
|
||||
description = pill,
|
||||
isValid = true,
|
||||
isMisspelled = true,
|
||||
)
|
||||
}
|
||||
|
||||
ELETRIPTAN_40_LIST.any { label.contains(it, ignoreCase = true) } -> {
|
||||
Event.Pill(
|
||||
id = Event.Pill.Id.ELETRIPTAN_40,
|
||||
label = ELETRIPTAN_40_LABEL,
|
||||
color = HeadacheColorPalette.Pill.Eletriptan40,
|
||||
amount = amount,
|
||||
description = pill,
|
||||
isValid = true,
|
||||
isMisspelled = true,
|
||||
)
|
||||
}
|
||||
|
||||
IBUPROFENE_400_LIST.any { label.contains(it, ignoreCase = true) } -> {
|
||||
Event.Pill(
|
||||
id = Event.Pill.Id.IBUPROFENE_400,
|
||||
label = IBUPROFENE_400_LABEL,
|
||||
color = HeadacheColorPalette.Pill.Ibuprofene400,
|
||||
amount = amount,
|
||||
description = pill,
|
||||
isValid = true,
|
||||
isMisspelled = true,
|
||||
)
|
||||
}
|
||||
|
||||
PARACETAMOL_1000_LIST.any { label.contains(it, ignoreCase = true) } -> {
|
||||
Event.Pill(
|
||||
id = Event.Pill.Id.PARACETAMOL_1000,
|
||||
label = PARACETAMOL_1000_LABEL,
|
||||
color = HeadacheColorPalette.Pill.Paracetamol1000,
|
||||
amount = amount,
|
||||
description = pill,
|
||||
isValid = true,
|
||||
isMisspelled = true,
|
||||
)
|
||||
}
|
||||
|
||||
else -> {
|
||||
Event.Pill(
|
||||
id = Event.Pill.Id.UNKNOWN,
|
||||
label = "?",
|
||||
color = HeadacheColorPalette.Pill.Unknown,
|
||||
amount = amount,
|
||||
description = pill,
|
||||
isValid = false,
|
||||
isMisspelled = false,
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
companion object {
|
||||
private const val PARACETAMOL_1000_LABEL = "Paracétamol 1000"
|
||||
private val PARACETAMOL_1000_LIST = listOf<String>(
|
||||
"Doliprane 1000",
|
||||
)
|
||||
|
||||
private const val IBUPROFENE_400_LABEL = "Ibuprofène 400"
|
||||
private val IBUPROFENE_400_LIST = listOf(
|
||||
"Ibuprofene 400",
|
||||
"Ibuprofen 400",
|
||||
"Ibuprofen",
|
||||
)
|
||||
|
||||
private const val SPIFEN_400_LABEL = "Spifen 400"
|
||||
private val SPIFEN_400_LIST = listOf<String>(
|
||||
"Spifen",
|
||||
)
|
||||
|
||||
private const val ELETRIPTAN_40_LABEL = "Élétriptan 40"
|
||||
private val ELETRIPTAN_40_LIST = listOf(
|
||||
"Eletriptan 40",
|
||||
"Életriptan 40",
|
||||
"Élitriptan 40",
|
||||
"Elitriptan 40",
|
||||
"Eletripan 40",
|
||||
"Elétripan 40",
|
||||
"Elitripan 40",
|
||||
"Elitripan",
|
||||
)
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,42 @@
|
|||
package com.pixelized.headache.ui.common.error
|
||||
|
||||
import android.content.Context
|
||||
import androidx.annotation.StringRes
|
||||
import androidx.compose.material3.SnackbarHostState
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.LaunchedEffect
|
||||
import androidx.compose.runtime.Stable
|
||||
import androidx.compose.ui.platform.LocalContext
|
||||
import com.pixelized.headache.ui.page.LocalSnack
|
||||
import kotlinx.coroutines.flow.Flow
|
||||
|
||||
@Stable
|
||||
sealed interface ErrorToast
|
||||
|
||||
@Stable
|
||||
data class StringErrorMessage(
|
||||
val message: String,
|
||||
) : ErrorToast
|
||||
|
||||
@Stable
|
||||
data class ResourceErrorMessage(
|
||||
@StringRes val message: Int,
|
||||
) : ErrorToast
|
||||
|
||||
@Composable
|
||||
fun HandleErrorMessage(
|
||||
context: Context = LocalContext.current,
|
||||
snack: SnackbarHostState = LocalSnack.current,
|
||||
errors: Flow<ErrorToast>,
|
||||
) {
|
||||
LaunchedEffect(Unit) {
|
||||
errors.collect { message ->
|
||||
snack.showSnackbar(
|
||||
message = when (message) {
|
||||
is StringErrorMessage -> message.message
|
||||
is ResourceErrorMessage -> context.getString(message.message)
|
||||
},
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -3,6 +3,7 @@ package com.pixelized.headache.ui.navigation
|
|||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.CompositionLocalProvider
|
||||
import androidx.compose.runtime.staticCompositionLocalOf
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.lifecycle.viewmodel.navigation3.rememberViewModelStoreNavEntryDecorator
|
||||
import androidx.navigation3.runtime.entryProvider
|
||||
import androidx.navigation3.runtime.rememberSavedStateNavEntryDecorator
|
||||
|
|
@ -19,12 +20,14 @@ val LocalNavigator = staticCompositionLocalOf<Navigator> {
|
|||
|
||||
@Composable
|
||||
fun MainNavDisplay(
|
||||
modifier: Modifier = Modifier,
|
||||
navigator: Navigator,
|
||||
) {
|
||||
CompositionLocalProvider(
|
||||
LocalNavigator provides navigator,
|
||||
) {
|
||||
NavDisplay(
|
||||
modifier = modifier,
|
||||
backStack = navigator.backStack,
|
||||
entryDecorators = listOf(
|
||||
rememberSceneSetupNavEntryDecorator(),
|
||||
|
|
|
|||
|
|
@ -6,8 +6,8 @@ import androidx.navigation3.runtime.EntryProviderBuilder
|
|||
import androidx.navigation3.runtime.entry
|
||||
import com.pixelized.headache.repository.event.Event
|
||||
import com.pixelized.headache.ui.navigation.Navigator
|
||||
import com.pixelized.headache.ui.page.event.EventPage
|
||||
import com.pixelized.headache.ui.page.event.EventViewModel
|
||||
import com.pixelized.headache.ui.page.event.list.EventPage
|
||||
import com.pixelized.headache.ui.page.event.list.EventViewModel
|
||||
import com.pixelized.headache.utils.extention.event
|
||||
import java.util.Date
|
||||
|
||||
|
|
|
|||
|
|
@ -1,14 +1,62 @@
|
|||
package com.pixelized.headache.ui.page
|
||||
|
||||
import androidx.compose.foundation.layout.WindowInsets
|
||||
import androidx.compose.foundation.layout.padding
|
||||
import androidx.compose.material3.MaterialTheme
|
||||
import androidx.compose.material3.Scaffold
|
||||
import androidx.compose.material3.Snackbar
|
||||
import androidx.compose.material3.SnackbarHost
|
||||
import androidx.compose.material3.SnackbarHostState
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.CompositionLocalProvider
|
||||
import androidx.compose.runtime.remember
|
||||
import androidx.compose.runtime.staticCompositionLocalOf
|
||||
import androidx.compose.ui.Modifier
|
||||
import com.pixelized.headache.ui.navigation.MainNavDisplay
|
||||
import com.pixelized.headache.ui.navigation.Navigator
|
||||
|
||||
val LocalSnack = staticCompositionLocalOf<SnackbarHostState> {
|
||||
error("Local SnackHost no yet ready")
|
||||
}
|
||||
|
||||
val LocalErrorSnack = staticCompositionLocalOf<SnackbarHostState> {
|
||||
error("Local SnackHost for Error no yet ready")
|
||||
}
|
||||
|
||||
@Composable
|
||||
fun MainPage(
|
||||
navigator: Navigator,
|
||||
snack: SnackbarHostState = remember { SnackbarHostState() },
|
||||
errorSnack: SnackbarHostState = remember { SnackbarHostState() },
|
||||
) {
|
||||
MainNavDisplay(
|
||||
navigator = navigator,
|
||||
)
|
||||
CompositionLocalProvider(
|
||||
LocalSnack provides snack,
|
||||
LocalErrorSnack provides errorSnack,
|
||||
) {
|
||||
Scaffold(
|
||||
contentWindowInsets = remember { WindowInsets(0, 0, 0, 0) },
|
||||
snackbarHost = {
|
||||
SnackbarHost(
|
||||
hostState = snack,
|
||||
)
|
||||
SnackbarHost(
|
||||
hostState = errorSnack,
|
||||
snackbar = {
|
||||
Snackbar(
|
||||
containerColor = MaterialTheme.colorScheme.error,
|
||||
contentColor = MaterialTheme.colorScheme.onError,
|
||||
actionContentColor = MaterialTheme.colorScheme.onError,
|
||||
snackbarData = it,
|
||||
)
|
||||
}
|
||||
)
|
||||
},
|
||||
content = { padding ->
|
||||
MainNavDisplay(
|
||||
modifier = Modifier.padding(paddingValues = padding),
|
||||
navigator = navigator,
|
||||
)
|
||||
}
|
||||
)
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,243 @@
|
|||
package com.pixelized.headache.ui.page.event.edit
|
||||
|
||||
import android.annotation.SuppressLint
|
||||
import android.icu.text.DateFormat
|
||||
import android.icu.text.SimpleDateFormat
|
||||
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.padding
|
||||
import androidx.compose.foundation.lazy.LazyColumn
|
||||
import androidx.compose.foundation.lazy.items
|
||||
import androidx.compose.material3.Button
|
||||
import androidx.compose.material3.ExperimentalMaterial3Api
|
||||
import androidx.compose.material3.HorizontalDivider
|
||||
import androidx.compose.material3.MaterialTheme
|
||||
import androidx.compose.material3.ModalBottomSheet
|
||||
import androidx.compose.material3.OutlinedButton
|
||||
import androidx.compose.material3.SheetState
|
||||
import androidx.compose.material3.Snackbar
|
||||
import androidx.compose.material3.SnackbarHost
|
||||
import androidx.compose.material3.SnackbarHostState
|
||||
import androidx.compose.material3.Surface
|
||||
import androidx.compose.material3.Text
|
||||
import androidx.compose.material3.rememberModalBottomSheetState
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.Stable
|
||||
import androidx.compose.runtime.mutableStateOf
|
||||
import androidx.compose.runtime.remember
|
||||
import androidx.compose.runtime.rememberCoroutineScope
|
||||
import androidx.compose.ui.Alignment
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.res.stringResource
|
||||
import androidx.compose.ui.tooling.preview.Preview
|
||||
import androidx.compose.ui.tooling.preview.PreviewParameter
|
||||
import androidx.compose.ui.tooling.preview.PreviewParameterProvider
|
||||
import androidx.compose.ui.unit.DpSize
|
||||
import androidx.compose.ui.unit.dp
|
||||
import androidx.hilt.navigation.compose.hiltViewModel
|
||||
import com.pixelized.headache.R
|
||||
import com.pixelized.headache.ui.common.error.HandleErrorMessage
|
||||
import com.pixelized.headache.ui.theme.HeadacheTheme
|
||||
import com.pixelized.headache.utils.extention.capitalize
|
||||
import kotlinx.coroutines.launch
|
||||
import java.util.Date
|
||||
import java.util.Locale
|
||||
|
||||
@Stable
|
||||
data class EventEditBottomSheetUio(
|
||||
val id: Long?,
|
||||
val date: Date,
|
||||
val pills: List<EventPillEditUio>,
|
||||
)
|
||||
|
||||
@Stable
|
||||
data object EventEditDefault {
|
||||
|
||||
@Stable
|
||||
val padding: PaddingValues = PaddingValues(horizontal = 16.dp, vertical = 16.dp)
|
||||
|
||||
@Stable
|
||||
val spacing: DpSize = DpSize(width = 8.dp, height = 16.dp)
|
||||
|
||||
@SuppressLint("ConstantLocale")
|
||||
@Stable
|
||||
val formatter = SimpleDateFormat("EEEE dd MMMM yyyy", Locale.getDefault())
|
||||
}
|
||||
|
||||
@OptIn(ExperimentalMaterial3Api::class)
|
||||
@Composable
|
||||
fun EventEditBottomSheet(
|
||||
viewModel: EventEditBottomSheetViewModel = hiltViewModel(),
|
||||
snack: SnackbarHostState = remember { SnackbarHostState() },
|
||||
sheetState: SheetState = rememberModalBottomSheetState(
|
||||
skipPartiallyExpanded = true,
|
||||
),
|
||||
onDismissRequest: () -> Unit,
|
||||
) {
|
||||
viewModel.data.value?.let { item ->
|
||||
val scope = rememberCoroutineScope()
|
||||
|
||||
ModalBottomSheet(
|
||||
sheetState = sheetState,
|
||||
onDismissRequest = onDismissRequest,
|
||||
) {
|
||||
Box(
|
||||
contentAlignment = Alignment.BottomCenter,
|
||||
) {
|
||||
EventEditContent(
|
||||
item = item,
|
||||
onAddPill = viewModel::onAddPill,
|
||||
onRemovePill = viewModel::onRemovePill,
|
||||
onDelete = { scope.launch { viewModel.delete() } },
|
||||
onConfirm = { scope.launch { viewModel.confirm() } },
|
||||
)
|
||||
SnackbarHost(
|
||||
hostState = snack,
|
||||
snackbar = {
|
||||
Snackbar(
|
||||
containerColor = MaterialTheme.colorScheme.error,
|
||||
contentColor = MaterialTheme.colorScheme.onError,
|
||||
actionContentColor = MaterialTheme.colorScheme.onError,
|
||||
snackbarData = it,
|
||||
)
|
||||
}
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
HandleErrorMessage(
|
||||
snack = snack,
|
||||
errors = viewModel.errors
|
||||
)
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun EventEditContent(
|
||||
modifier: Modifier = Modifier,
|
||||
padding: PaddingValues = EventEditDefault.padding,
|
||||
spacing: DpSize = EventEditDefault.spacing,
|
||||
formatter: DateFormat = EventEditDefault.formatter,
|
||||
item: EventEditBottomSheetUio,
|
||||
onAddPill: (EventPillEditUio) -> Unit,
|
||||
onRemovePill: (EventPillEditUio) -> Unit,
|
||||
onDelete: () -> Unit,
|
||||
onConfirm: () -> Unit,
|
||||
) {
|
||||
Column(
|
||||
modifier = Modifier
|
||||
.padding(paddingValues = padding)
|
||||
.then(other = modifier),
|
||||
verticalArrangement = Arrangement.spacedBy(space = spacing.height),
|
||||
) {
|
||||
Text(
|
||||
style = MaterialTheme.typography.titleLarge,
|
||||
text = formatter.format(item.date).capitalize()
|
||||
)
|
||||
|
||||
HorizontalDivider()
|
||||
|
||||
LazyColumn(
|
||||
modifier = Modifier.weight(weight = 1f, fill = false),
|
||||
) {
|
||||
items(
|
||||
items = item.pills
|
||||
) {
|
||||
EventPillEdit(
|
||||
item = it,
|
||||
onAdd = onAddPill,
|
||||
onRemove = onRemovePill,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
HorizontalDivider()
|
||||
|
||||
Row(
|
||||
horizontalArrangement = Arrangement.spacedBy(space = spacing.width),
|
||||
) {
|
||||
if (item.id != null) {
|
||||
OutlinedButton(
|
||||
modifier = Modifier.weight(1f),
|
||||
onClick = onDelete,
|
||||
) {
|
||||
Text(
|
||||
text = stringResource(R.string.action_delete)
|
||||
)
|
||||
}
|
||||
}
|
||||
Button(
|
||||
modifier = Modifier.weight(2f),
|
||||
onClick = onConfirm,
|
||||
) {
|
||||
Text(
|
||||
text = stringResource(R.string.action_confirm)
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
@Preview
|
||||
private fun EventEditPreview(
|
||||
@PreviewParameter(EventEditPreviewPreviewProvider::class) preview: EventEditBottomSheetUio,
|
||||
) {
|
||||
HeadacheTheme {
|
||||
Surface {
|
||||
val item = remember { mutableStateOf(preview) }
|
||||
EventEditContent(
|
||||
item = item.value,
|
||||
onAddPill = { pill ->
|
||||
item.value = item.value.let { event ->
|
||||
val pills = event.pills.toMutableList()
|
||||
val pillIndex = pills.indexOfFirst { it.id == pill.id }
|
||||
pills[pillIndex] =
|
||||
pills[pillIndex].let { pill -> pill.copy(amount = pill.amount + 1) }
|
||||
event.copy(pills = pills)
|
||||
}
|
||||
},
|
||||
onRemovePill = { pill ->
|
||||
item.value = item.value.let { event ->
|
||||
val pills = event.pills.toMutableList()
|
||||
val pillIndex = pills.indexOfFirst { it.id == pill.id }
|
||||
pills[pillIndex] =
|
||||
pills[pillIndex].let { pill -> pill.copy(amount = pill.amount - 1) }
|
||||
event.copy(pills = pills)
|
||||
}
|
||||
},
|
||||
onDelete = { },
|
||||
onConfirm = { },
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private class EventEditPreviewPreviewProvider : PreviewParameterProvider<EventEditBottomSheetUio> {
|
||||
override val values: Sequence<EventEditBottomSheetUio>
|
||||
get() = sequenceOf(
|
||||
EventEditBottomSheetUio(
|
||||
id = null,
|
||||
date = Date(),
|
||||
pills = listOf(
|
||||
EventPillEditUio(id = 0, label = "Paracétamol 1000", 0),
|
||||
EventPillEditUio(id = 1, label = "Ibuprofène 400", 0),
|
||||
EventPillEditUio(id = 2, label = "Spifen 400", 0),
|
||||
EventPillEditUio(id = 3, label = "Élétriptan 40", 0),
|
||||
)
|
||||
),
|
||||
EventEditBottomSheetUio(
|
||||
id = 1,
|
||||
date = Date(),
|
||||
pills = listOf(
|
||||
EventPillEditUio(id = 0, label = "Paracétamol 1000", 0),
|
||||
EventPillEditUio(id = 1, label = "Ibuprofène 400", 0),
|
||||
EventPillEditUio(id = 2, label = "Spifen 400", 2),
|
||||
EventPillEditUio(id = 3, label = "Élétriptan 40", 1),
|
||||
)
|
||||
),
|
||||
)
|
||||
}
|
||||
|
|
@ -0,0 +1,126 @@
|
|||
package com.pixelized.headache.ui.page.event.edit
|
||||
|
||||
import android.icu.util.Calendar
|
||||
import androidx.compose.runtime.State
|
||||
import androidx.compose.runtime.mutableStateOf
|
||||
import androidx.lifecycle.ViewModel
|
||||
import com.pixelized.headache.R
|
||||
import com.pixelized.headache.repository.event.EventRepository
|
||||
import com.pixelized.headache.ui.common.error.ErrorToast
|
||||
import com.pixelized.headache.ui.common.error.ResourceErrorMessage
|
||||
import com.pixelized.headache.utils.extention.event
|
||||
import dagger.hilt.android.lifecycle.HiltViewModel
|
||||
import kotlinx.coroutines.flow.MutableSharedFlow
|
||||
import kotlinx.coroutines.flow.SharedFlow
|
||||
import javax.inject.Inject
|
||||
import kotlin.math.max
|
||||
|
||||
@HiltViewModel
|
||||
class EventEditBottomSheetViewModel @Inject constructor(
|
||||
private val repository: EventRepository,
|
||||
private val factory: EventEditFactory,
|
||||
) : ViewModel() {
|
||||
|
||||
private val _data = mutableStateOf<EventEditBottomSheetUio?>(null)
|
||||
val data: State<EventEditBottomSheetUio?> = _data
|
||||
|
||||
private val _errors = MutableSharedFlow<ErrorToast>()
|
||||
val errors: SharedFlow<ErrorToast> = _errors
|
||||
|
||||
fun show(
|
||||
eventId: Long? = null,
|
||||
) {
|
||||
val event = eventId?.let { id -> repository.event(id = id) }
|
||||
_data.value = factory.convertToUio(event = event)
|
||||
}
|
||||
|
||||
fun dismiss() {
|
||||
_data.value = null
|
||||
}
|
||||
|
||||
fun onAddPill(pill: EventPillEditUio) {
|
||||
updatePill(
|
||||
pill = pill,
|
||||
delta = 1,
|
||||
)
|
||||
}
|
||||
|
||||
fun onRemovePill(pill: EventPillEditUio) {
|
||||
updatePill(
|
||||
pill = pill,
|
||||
delta = -1,
|
||||
)
|
||||
}
|
||||
|
||||
fun updatePill(pill: EventPillEditUio, delta: Int) {
|
||||
_data.value = _data.value?.let { data ->
|
||||
val index = data.pills.indexOfFirst {
|
||||
it.id == pill.id
|
||||
}
|
||||
val newPill = data.pills[index].copy(
|
||||
amount = max(data.pills[index].amount + delta, 0)
|
||||
)
|
||||
data.copy(
|
||||
pills = data.pills.toMutableList().also {
|
||||
it[index] = newPill
|
||||
}
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
suspend fun delete() {
|
||||
val event = _data.value ?: return
|
||||
val eventId = event.id ?: return
|
||||
|
||||
val success = repository.deleteEvent(eventId = eventId)
|
||||
|
||||
if (success) {
|
||||
_data.value = null
|
||||
} else {
|
||||
val message = ResourceErrorMessage(message = R.string.error_edit_calendar)
|
||||
_errors.emit(message)
|
||||
}
|
||||
}
|
||||
|
||||
suspend fun confirm() {
|
||||
val event = _data.value ?: return
|
||||
val date = Calendar.getInstance().apply { time = event.date }.event
|
||||
val description = factory.convertToDescription(pills = event.pills)
|
||||
|
||||
val success = if (event.id == null) {
|
||||
repository.createEvent(
|
||||
title = "Céphalée",
|
||||
description = description,
|
||||
year = date.year,
|
||||
month = date.month,
|
||||
day = date.day,
|
||||
)
|
||||
} else {
|
||||
repository.updateEvent(
|
||||
id = event.id,
|
||||
title = "Céphalée",
|
||||
description = description,
|
||||
year = date.year,
|
||||
month = date.month,
|
||||
day = date.day,
|
||||
)
|
||||
}
|
||||
|
||||
if (success) {
|
||||
repository.fetchEvents(
|
||||
startDate = Calendar.getInstance().apply {
|
||||
time = event.date
|
||||
add(Calendar.DAY_OF_MONTH, -1)
|
||||
}.event,
|
||||
endDate = Calendar.getInstance().apply {
|
||||
time = event.date
|
||||
add(Calendar.DAY_OF_MONTH, 1)
|
||||
}.event,
|
||||
)
|
||||
_data.value = null
|
||||
} else {
|
||||
val message = ResourceErrorMessage(message = R.string.error_edit_calendar)
|
||||
_errors.emit(message)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,100 @@
|
|||
package com.pixelized.headache.ui.page.event.edit
|
||||
|
||||
import android.icu.util.Calendar
|
||||
import com.pixelized.headache.repository.event.Event
|
||||
import com.pixelized.headache.utils.extention.event
|
||||
import java.util.Date
|
||||
import javax.inject.Inject
|
||||
|
||||
class EventEditFactory @Inject constructor() {
|
||||
|
||||
fun convertToUio(
|
||||
event: Event?,
|
||||
): EventEditBottomSheetUio {
|
||||
return when (event) {
|
||||
null -> EventEditBottomSheetUio(
|
||||
id = null,
|
||||
date = Date(),
|
||||
pills = listOf(
|
||||
EventPillEditUio(
|
||||
id = Event.Pill.Id.IBUPROFENE_400.value,
|
||||
label = "Paracétamol 1000",
|
||||
amount = 0,
|
||||
),
|
||||
EventPillEditUio(
|
||||
id = Event.Pill.Id.PARACETAMOL_1000.value,
|
||||
label = "Ibuprofène 400",
|
||||
amount = 0,
|
||||
),
|
||||
EventPillEditUio(
|
||||
id = Event.Pill.Id.SPIFEN_400.value,
|
||||
label = "Spifen 400",
|
||||
amount = 0,
|
||||
),
|
||||
EventPillEditUio(
|
||||
id = Event.Pill.Id.ELETRIPTAN_40.value,
|
||||
label = "Élétriptan 40",
|
||||
amount = 0,
|
||||
),
|
||||
),
|
||||
)
|
||||
|
||||
else -> {
|
||||
val date = Calendar.getInstance().apply { this.event = event.date }.time
|
||||
|
||||
val ibuprofeneAmount = event.pills.sumOf {
|
||||
if (it.id == Event.Pill.Id.IBUPROFENE_400) it.amount else 0
|
||||
}
|
||||
val paracetamolAmount = event.pills.sumOf {
|
||||
if (it.id == Event.Pill.Id.PARACETAMOL_1000) it.amount else 0
|
||||
}
|
||||
val spifenAmount = event.pills.sumOf {
|
||||
if (it.id == Event.Pill.Id.SPIFEN_400) it.amount else 0
|
||||
}
|
||||
val eletriptanAmount = event.pills.sumOf {
|
||||
if (it.id == Event.Pill.Id.ELETRIPTAN_40) it.amount else 0
|
||||
}
|
||||
|
||||
EventEditBottomSheetUio(
|
||||
id = event.id,
|
||||
date = date,
|
||||
pills = listOf(
|
||||
EventPillEditUio(
|
||||
id = Event.Pill.Id.IBUPROFENE_400.value,
|
||||
label = "Paracétamol 1000",
|
||||
amount = ibuprofeneAmount,
|
||||
),
|
||||
EventPillEditUio(
|
||||
id = Event.Pill.Id.PARACETAMOL_1000.value,
|
||||
label = "Ibuprofène 400",
|
||||
amount = paracetamolAmount,
|
||||
),
|
||||
EventPillEditUio(
|
||||
id = Event.Pill.Id.SPIFEN_400.value,
|
||||
label = "Spifen 400",
|
||||
amount = spifenAmount,
|
||||
),
|
||||
EventPillEditUio(
|
||||
id = Event.Pill.Id.ELETRIPTAN_40.value,
|
||||
label = "Élétriptan 40",
|
||||
amount = eletriptanAmount,
|
||||
),
|
||||
),
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fun convertToDescription(
|
||||
pills: List<EventPillEditUio>,
|
||||
): String {
|
||||
return pills
|
||||
.mapNotNull { it.takeIf { it.amount > 0 } }
|
||||
.joinToString(separator = "\n") {
|
||||
when (it.amount) {
|
||||
1 -> it.label
|
||||
else -> "${it.label} x${it.amount}"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,159 @@
|
|||
package com.pixelized.headache.ui.page.event.edit
|
||||
|
||||
import androidx.compose.animation.AnimatedContent
|
||||
import androidx.compose.animation.SizeTransform
|
||||
import androidx.compose.animation.fadeIn
|
||||
import androidx.compose.animation.fadeOut
|
||||
import androidx.compose.animation.slideInVertically
|
||||
import androidx.compose.animation.slideOutVertically
|
||||
import androidx.compose.animation.togetherWith
|
||||
import androidx.compose.foundation.layout.Arrangement
|
||||
import androidx.compose.foundation.layout.PaddingValues
|
||||
import androidx.compose.foundation.layout.Row
|
||||
import androidx.compose.foundation.layout.padding
|
||||
import androidx.compose.foundation.layout.widthIn
|
||||
import androidx.compose.foundation.shape.RoundedCornerShape
|
||||
import androidx.compose.material.icons.Icons
|
||||
import androidx.compose.material.icons.filled.Add
|
||||
import androidx.compose.material.icons.filled.Remove
|
||||
import androidx.compose.material3.Icon
|
||||
import androidx.compose.material3.IconButton
|
||||
import androidx.compose.material3.IconButtonDefaults
|
||||
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.Shape
|
||||
import androidx.compose.ui.text.style.TextAlign
|
||||
import androidx.compose.ui.tooling.preview.Preview
|
||||
import androidx.compose.ui.unit.Dp
|
||||
import androidx.compose.ui.unit.dp
|
||||
import com.pixelized.headache.ui.theme.HeadacheTheme
|
||||
|
||||
@Stable
|
||||
data class EventPillEditUio(
|
||||
val id: Long,
|
||||
val label: String,
|
||||
val amount: Int,
|
||||
)
|
||||
|
||||
@Stable
|
||||
data object EventPillEditDefault {
|
||||
@Stable
|
||||
val padding: PaddingValues = PaddingValues(horizontal = 0.dp, vertical = 0.dp)
|
||||
|
||||
@Stable
|
||||
val spacing: Dp = 4.dp
|
||||
|
||||
@Stable
|
||||
val minAmountWidth: Dp = 32.dp
|
||||
|
||||
@Stable
|
||||
val buttonOutShape: Shape = RoundedCornerShape(
|
||||
topStart = 32.dp,
|
||||
bottomStart = 32.dp,
|
||||
topEnd = 4.dp,
|
||||
bottomEnd = 4.dp
|
||||
)
|
||||
|
||||
@Stable
|
||||
val buttonInShape: Shape = RoundedCornerShape(
|
||||
topEnd = 32.dp,
|
||||
bottomEnd = 32.dp,
|
||||
topStart = 4.dp,
|
||||
bottomStart = 4.dp
|
||||
)
|
||||
}
|
||||
|
||||
@Composable
|
||||
fun EventPillEdit(
|
||||
modifier: Modifier = Modifier,
|
||||
padding: PaddingValues = EventPillEditDefault.padding,
|
||||
spacing: Dp = EventPillEditDefault.spacing,
|
||||
minAmountWidth: Dp = EventPillEditDefault.minAmountWidth,
|
||||
buttonOutShape: Shape = EventPillEditDefault.buttonOutShape,
|
||||
buttonInShape: Shape = EventPillEditDefault.buttonInShape,
|
||||
item: EventPillEditUio,
|
||||
onAdd: (EventPillEditUio) -> Unit,
|
||||
onRemove: (EventPillEditUio) -> Unit,
|
||||
) {
|
||||
Row(
|
||||
modifier = Modifier
|
||||
.padding(paddingValues = padding)
|
||||
.then(other = modifier),
|
||||
verticalAlignment = Alignment.CenterVertically,
|
||||
horizontalArrangement = Arrangement.spacedBy(space = spacing),
|
||||
) {
|
||||
Text(
|
||||
modifier = Modifier.weight(weight = 1f),
|
||||
style = MaterialTheme.typography.titleMedium,
|
||||
text = item.label
|
||||
)
|
||||
|
||||
IconButton(
|
||||
onClick = { onRemove(item) },
|
||||
colors = IconButtonDefaults.iconButtonColors(
|
||||
contentColor = MaterialTheme.colorScheme.onPrimary,
|
||||
containerColor = MaterialTheme.colorScheme.primary,
|
||||
),
|
||||
shape = buttonOutShape,
|
||||
) {
|
||||
Icon(
|
||||
imageVector = Icons.Default.Remove,
|
||||
contentDescription = null,
|
||||
)
|
||||
}
|
||||
|
||||
AnimatedContent(
|
||||
targetState = item.amount,
|
||||
transitionSpec = {
|
||||
val direction = if (targetState < initialState) -1 else 1
|
||||
val enter = fadeIn() + slideInVertically { -direction * it }
|
||||
val exit = fadeOut() + slideOutVertically { direction * it }
|
||||
enter togetherWith exit using SizeTransform(clip = false)
|
||||
},
|
||||
) {
|
||||
Text(
|
||||
modifier = Modifier.widthIn(min = minAmountWidth),
|
||||
style = MaterialTheme.typography.titleMedium,
|
||||
textAlign = TextAlign.Center,
|
||||
text = "$it",
|
||||
)
|
||||
}
|
||||
|
||||
IconButton(
|
||||
onClick = { onAdd(item) },
|
||||
colors = IconButtonDefaults.iconButtonColors(
|
||||
contentColor = MaterialTheme.colorScheme.onPrimary,
|
||||
containerColor = MaterialTheme.colorScheme.primary,
|
||||
),
|
||||
shape = buttonInShape,
|
||||
) {
|
||||
Icon(
|
||||
imageVector = Icons.Default.Add,
|
||||
contentDescription = null,
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
@Preview
|
||||
private fun EventPillEditPreview() {
|
||||
HeadacheTheme {
|
||||
Surface {
|
||||
EventPillEdit(
|
||||
item = EventPillEditUio(
|
||||
id = 0,
|
||||
label = "Élétriptan 40",
|
||||
amount = 2,
|
||||
),
|
||||
onAdd = { },
|
||||
onRemove = { },
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -1,14 +1,16 @@
|
|||
package com.pixelized.headache.ui.page.event
|
||||
package com.pixelized.headache.ui.page.event.list
|
||||
|
||||
import android.annotation.SuppressLint
|
||||
import android.icu.text.SimpleDateFormat
|
||||
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.IntrinsicSize
|
||||
import androidx.compose.foundation.layout.PaddingValues
|
||||
import androidx.compose.foundation.layout.fillMaxHeight
|
||||
import androidx.compose.foundation.layout.fillMaxWidth
|
||||
import androidx.compose.foundation.layout.height
|
||||
import androidx.compose.foundation.layout.heightIn
|
||||
import androidx.compose.foundation.layout.padding
|
||||
|
|
@ -27,6 +29,7 @@ import androidx.compose.ui.tooling.preview.PreviewParameterProvider
|
|||
import androidx.compose.ui.unit.Dp
|
||||
import androidx.compose.ui.unit.dp
|
||||
import com.pixelized.headache.ui.theme.HeadacheTheme
|
||||
import com.pixelized.headache.ui.theme.color.HeadacheColorPalette
|
||||
import java.util.Date
|
||||
import java.util.Locale
|
||||
|
||||
|
|
@ -72,9 +75,12 @@ fun EventItem(
|
|||
colorBoxPadding: PaddingValues = EventItemDefault.colorBoxPadding,
|
||||
formatter: SimpleDateFormat = EventItemDefault.formatter,
|
||||
item: EventItemUio,
|
||||
onItem: (EventItemUio) -> Unit,
|
||||
) {
|
||||
Box(
|
||||
modifier = Modifier
|
||||
.clickable { onItem(item) }
|
||||
.fillMaxWidth()
|
||||
.heightIn(min = minHeigh)
|
||||
.height(IntrinsicSize.Min)
|
||||
.then(other = modifier),
|
||||
|
|
@ -125,6 +131,7 @@ private fun EventItemPreview(
|
|||
Surface {
|
||||
EventItem(
|
||||
item = preview,
|
||||
onItem = { },
|
||||
)
|
||||
}
|
||||
}
|
||||
|
|
@ -141,6 +148,7 @@ private class EventPreviewProvider() : PreviewParameterProvider<EventItemUio> {
|
|||
label = "Spifen 400",
|
||||
amount = 1,
|
||||
description = "Spifen 400",
|
||||
color = HeadacheColorPalette.Additional.Pink,
|
||||
isValid = true,
|
||||
isMisspelled = false,
|
||||
),
|
||||
|
|
@ -148,6 +156,7 @@ private class EventPreviewProvider() : PreviewParameterProvider<EventItemUio> {
|
|||
label = "Élétriptan 40",
|
||||
amount = 1,
|
||||
description = "Élétriptan 40",
|
||||
color = HeadacheColorPalette.Additional.Purple,
|
||||
isValid = true,
|
||||
isMisspelled = false,
|
||||
),
|
||||
|
|
@ -170,6 +179,7 @@ private class EventPreviewProvider() : PreviewParameterProvider<EventItemUio> {
|
|||
label = "Spifen 400",
|
||||
amount = 1,
|
||||
description = "Spifen 400",
|
||||
color = HeadacheColorPalette.Additional.Yellow,
|
||||
isValid = true,
|
||||
isMisspelled = false,
|
||||
),
|
||||
|
|
@ -177,6 +187,7 @@ private class EventPreviewProvider() : PreviewParameterProvider<EventItemUio> {
|
|||
label = "Élétriptan 40",
|
||||
amount = 1,
|
||||
description = "Élitriptan 40",
|
||||
color = HeadacheColorPalette.Additional.Purple,
|
||||
isValid = true,
|
||||
isMisspelled = true,
|
||||
),
|
||||
|
|
@ -192,6 +203,7 @@ private class EventPreviewProvider() : PreviewParameterProvider<EventItemUio> {
|
|||
label = "Spifen 400",
|
||||
amount = 1,
|
||||
description = "Spifen 400",
|
||||
color = HeadacheColorPalette.Additional.Yellow,
|
||||
isValid = true,
|
||||
isMisspelled = false,
|
||||
),
|
||||
|
|
@ -199,6 +211,7 @@ private class EventPreviewProvider() : PreviewParameterProvider<EventItemUio> {
|
|||
label = "?",
|
||||
amount = 1,
|
||||
description = "Élitriptan 40",
|
||||
color = HeadacheColorPalette.Additional.Purple,
|
||||
isValid = false,
|
||||
isMisspelled = false,
|
||||
),
|
||||
|
|
@ -1,4 +1,4 @@
|
|||
package com.pixelized.headache.ui.page.event
|
||||
package com.pixelized.headache.ui.page.event.list
|
||||
|
||||
import android.icu.util.Calendar
|
||||
import com.pixelized.headache.repository.event.Event
|
||||
|
|
@ -10,7 +10,7 @@ class EventItemFactory @Inject constructor() {
|
|||
private val calendar = Calendar.getInstance()
|
||||
|
||||
fun convertToUio(
|
||||
events: List<Event>,
|
||||
events: Collection<Event>,
|
||||
): List<EventItemUio> {
|
||||
return events
|
||||
.sortedByDescending { it.date }
|
||||
|
|
@ -38,6 +38,7 @@ class EventItemFactory @Inject constructor() {
|
|||
label = pill.label,
|
||||
amount = pill.amount,
|
||||
description = pill.description,
|
||||
color = pill.color,
|
||||
isValid = pill.isValid,
|
||||
isMisspelled = pill.isMisspelled,
|
||||
)
|
||||
|
|
@ -1,6 +1,10 @@
|
|||
package com.pixelized.headache.ui.page.event
|
||||
package com.pixelized.headache.ui.page.event.list
|
||||
|
||||
import androidx.compose.animation.AnimatedContent
|
||||
import androidx.compose.animation.animateColorAsState
|
||||
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.fillMaxSize
|
||||
|
|
@ -8,40 +12,56 @@ 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.foundation.shape.CircleShape
|
||||
import androidx.compose.material.icons.Icons
|
||||
import androidx.compose.material.icons.automirrored.filled.ArrowBack
|
||||
import androidx.compose.material.icons.filled.Add
|
||||
import androidx.compose.material3.ExperimentalMaterial3Api
|
||||
import androidx.compose.material3.FloatingActionButton
|
||||
import androidx.compose.material3.Icon
|
||||
import androidx.compose.material3.IconButton
|
||||
import androidx.compose.material3.MaterialTheme
|
||||
import androidx.compose.material3.Scaffold
|
||||
import androidx.compose.material3.Surface
|
||||
import androidx.compose.material3.Text
|
||||
import androidx.compose.material3.TopAppBar
|
||||
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.Modifier
|
||||
import androidx.compose.ui.res.painterResource
|
||||
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.dp
|
||||
import androidx.hilt.navigation.compose.hiltViewModel
|
||||
import androidx.lifecycle.compose.collectAsStateWithLifecycle
|
||||
import com.pixelized.headache.R
|
||||
import com.pixelized.headache.ui.navigation.LocalNavigator
|
||||
import com.pixelized.headache.ui.page.event.edit.EventEditBottomSheet
|
||||
import com.pixelized.headache.ui.page.event.edit.EventEditBottomSheetViewModel
|
||||
import com.pixelized.headache.ui.theme.HeadacheTheme
|
||||
import com.pixelized.headache.ui.theme.color.HeadacheColorPalette
|
||||
import java.util.Date
|
||||
|
||||
@Stable
|
||||
data object EventPageDefault {
|
||||
@Stable
|
||||
val padding: PaddingValues = PaddingValues(vertical = 8.dp)
|
||||
val listPadding: PaddingValues = PaddingValues(bottom = 56.dp + 16.dp * 2)
|
||||
|
||||
@Stable
|
||||
val spacing: Dp = 0.dp
|
||||
}
|
||||
|
||||
@OptIn(ExperimentalMaterial3Api::class)
|
||||
@Composable
|
||||
fun EventPage(
|
||||
viewModel: EventViewModel = hiltViewModel(),
|
||||
editViewModel: EventEditBottomSheetViewModel = hiltViewModel(),
|
||||
) {
|
||||
val navigation = LocalNavigator.current
|
||||
val events = viewModel.events.collectAsStateWithLifecycle()
|
||||
|
|
@ -56,6 +76,13 @@ fun EventPage(
|
|||
onBack = { navigation.popBackstack() },
|
||||
onMisspelledFilter = viewModel::toggleMisspelledFilter,
|
||||
onInvalidFilter = viewModel::toggleErrorFilter,
|
||||
onAddEvent = { editViewModel.show() },
|
||||
onEvent = { editViewModel.show(eventId = it.id) }
|
||||
)
|
||||
|
||||
EventEditBottomSheet(
|
||||
viewModel = editViewModel,
|
||||
onDismissRequest = { editViewModel.dismiss() },
|
||||
)
|
||||
}
|
||||
|
||||
|
|
@ -63,7 +90,7 @@ fun EventPage(
|
|||
@OptIn(ExperimentalMaterial3Api::class)
|
||||
private fun EventContent(
|
||||
modifier: Modifier = Modifier,
|
||||
padding: PaddingValues = EventPageDefault.padding,
|
||||
listPadding: PaddingValues = EventPageDefault.listPadding,
|
||||
spacing: Dp = EventPageDefault.spacing,
|
||||
invalidFilter: State<Boolean>,
|
||||
misspelledFilter: State<Boolean>,
|
||||
|
|
@ -71,6 +98,8 @@ private fun EventContent(
|
|||
onBack: () -> Unit,
|
||||
onInvalidFilter: () -> Unit,
|
||||
onMisspelledFilter: () -> Unit,
|
||||
onAddEvent: () -> Unit,
|
||||
onEvent: (EventItemUio) -> Unit,
|
||||
) {
|
||||
Scaffold(
|
||||
modifier = modifier,
|
||||
|
|
@ -126,28 +155,104 @@ private fun EventContent(
|
|||
}
|
||||
)
|
||||
},
|
||||
floatingActionButton = {
|
||||
FloatingActionButton(
|
||||
containerColor = MaterialTheme.colorScheme.primary,
|
||||
shape = CircleShape,
|
||||
onClick = onAddEvent,
|
||||
) {
|
||||
Icon(
|
||||
imageVector = Icons.Default.Add,
|
||||
contentDescription = null,
|
||||
)
|
||||
}
|
||||
},
|
||||
content = { paddingValues ->
|
||||
LazyColumn(
|
||||
modifier = Modifier
|
||||
.padding(paddingValues = paddingValues)
|
||||
.fillMaxSize(),
|
||||
verticalArrangement = Arrangement.spacedBy(space = spacing),
|
||||
contentPadding = padding,
|
||||
reverseLayout = true,
|
||||
contentPadding = listPadding,
|
||||
) {
|
||||
items(
|
||||
items = events.value,
|
||||
key = { it.id },
|
||||
contentType = { "EventItem" },
|
||||
) { item ->
|
||||
EventItem(
|
||||
modifier = Modifier
|
||||
.animateItem()
|
||||
.fillMaxWidth(),
|
||||
item = item,
|
||||
)
|
||||
AnimatedContent(
|
||||
modifier = Modifier.animateItem(),
|
||||
targetState = item,
|
||||
transitionSpec = { fadeIn() togetherWith fadeOut() },
|
||||
) { animatedItem ->
|
||||
EventItem(
|
||||
modifier = Modifier.fillMaxWidth(),
|
||||
item = animatedItem,
|
||||
onItem = onEvent,
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
)
|
||||
}
|
||||
|
||||
@Composable
|
||||
@Preview
|
||||
private fun EventContentPreview(
|
||||
@PreviewParameter(EventContentPreviewProvider::class) preview: List<EventItemUio>,
|
||||
) {
|
||||
HeadacheTheme {
|
||||
Surface {
|
||||
EventContent(
|
||||
invalidFilter = remember { mutableStateOf(false) },
|
||||
misspelledFilter = remember { mutableStateOf(false) },
|
||||
events = remember { mutableStateOf(preview) },
|
||||
onBack = { },
|
||||
onInvalidFilter = { },
|
||||
onMisspelledFilter = { },
|
||||
onAddEvent = { },
|
||||
onEvent = { },
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private class EventContentPreviewProvider : PreviewParameterProvider<List<EventItemUio>> {
|
||||
override val values: Sequence<List<EventItemUio>>
|
||||
get() = sequenceOf(
|
||||
listOf(
|
||||
EventItemUio(
|
||||
id = 0,
|
||||
date = Date(),
|
||||
pills = emptyList(),
|
||||
isValid = true,
|
||||
isMisspelled = false,
|
||||
),
|
||||
EventItemUio(
|
||||
id = 1,
|
||||
date = Date(),
|
||||
pills = listOf(
|
||||
PillUio(
|
||||
label = "Spifen 400",
|
||||
amount = 1,
|
||||
description = "Spifen 400",
|
||||
color = HeadacheColorPalette.Additional.Yellow,
|
||||
isValid = true,
|
||||
isMisspelled = false,
|
||||
),
|
||||
PillUio(
|
||||
label = "Élétriptan 40",
|
||||
amount = 1,
|
||||
description = "Élitriptan 40",
|
||||
color = HeadacheColorPalette.Additional.Purple,
|
||||
isValid = true,
|
||||
isMisspelled = true,
|
||||
),
|
||||
),
|
||||
isValid = true,
|
||||
isMisspelled = false,
|
||||
),
|
||||
),
|
||||
)
|
||||
}
|
||||
|
|
@ -1,4 +1,4 @@
|
|||
package com.pixelized.headache.ui.page.event
|
||||
package com.pixelized.headache.ui.page.event.list
|
||||
|
||||
import androidx.lifecycle.ViewModel
|
||||
import androidx.lifecycle.viewModelScope
|
||||
|
|
@ -16,14 +16,11 @@ import kotlinx.coroutines.flow.MutableStateFlow
|
|||
import kotlinx.coroutines.flow.SharingStarted
|
||||
import kotlinx.coroutines.flow.StateFlow
|
||||
import kotlinx.coroutines.flow.combine
|
||||
import kotlinx.coroutines.flow.flatMapLatest
|
||||
import kotlinx.coroutines.flow.flowOf
|
||||
import kotlinx.coroutines.flow.mapLatest
|
||||
import kotlinx.coroutines.flow.stateIn
|
||||
|
||||
@HiltViewModel(assistedFactory = EventViewModel.Factory::class)
|
||||
class EventViewModel @AssistedInject constructor(
|
||||
googleCalendarIdRepository: GoogleCalendarIdRepository,
|
||||
eventRepository: EventRepository,
|
||||
eventItemFactory: EventItemFactory,
|
||||
@Assisted val argument: EventDestination,
|
||||
|
|
@ -36,43 +33,36 @@ class EventViewModel @AssistedInject constructor(
|
|||
val invalidFilter: StateFlow<Boolean> = invalidFilterFlow
|
||||
|
||||
@OptIn(ExperimentalCoroutinesApi::class)
|
||||
val events: StateFlow<List<EventItemUio>> = googleCalendarIdRepository.calendarId
|
||||
.flatMapLatest { id: Long? ->
|
||||
when (id) {
|
||||
null -> flowOf(emptyList())
|
||||
else -> {
|
||||
combine(
|
||||
when (argument.date) {
|
||||
null -> eventRepository.eventFlow(calendarId = id)
|
||||
else -> eventRepository.eventFlow(calendarId = id)
|
||||
.mapLatest { events ->
|
||||
events.filter { event ->
|
||||
event.date isSameMonth argument.date
|
||||
}
|
||||
}
|
||||
},
|
||||
misspelledFilterFlow,
|
||||
invalidFilterFlow,
|
||||
) { events, misspelled, invalid ->
|
||||
if (!misspelled && !invalid) {
|
||||
events
|
||||
} else {
|
||||
events.filter {
|
||||
misspelled && it.isMisspelled || invalid && it.isValid.not()
|
||||
}
|
||||
}
|
||||
val events: StateFlow<List<EventItemUio>> = combine(
|
||||
when (argument.date) {
|
||||
null -> eventRepository.eventsListFlow()
|
||||
else -> eventRepository.eventsListFlow()
|
||||
.mapLatest { events ->
|
||||
events.filter { event ->
|
||||
event.date isSameMonth argument.date
|
||||
}
|
||||
}
|
||||
},
|
||||
misspelledFilterFlow,
|
||||
invalidFilterFlow,
|
||||
transform = { events, misspelled, invalid ->
|
||||
if (!misspelled && !invalid) {
|
||||
events
|
||||
} else {
|
||||
events.filter {
|
||||
misspelled && it.isMisspelled || invalid && it.isValid.not()
|
||||
}
|
||||
}
|
||||
}
|
||||
.mapLatest { events: List<Event> ->
|
||||
eventItemFactory.convertToUio(events = events)
|
||||
}
|
||||
.stateIn(
|
||||
scope = viewModelScope,
|
||||
started = SharingStarted.Lazily,
|
||||
initialValue = emptyList(),
|
||||
).mapLatest { events: Collection<Event> ->
|
||||
eventItemFactory.convertToUio(
|
||||
events = events,
|
||||
)
|
||||
}.stateIn(
|
||||
scope = viewModelScope,
|
||||
started = SharingStarted.Lazily,
|
||||
initialValue = emptyList(),
|
||||
)
|
||||
|
||||
fun toggleMisspelledFilter() {
|
||||
misspelledFilterFlow.value = !misspelledFilterFlow.value
|
||||
|
|
@ -1,13 +1,18 @@
|
|||
package com.pixelized.headache.ui.page.event
|
||||
package com.pixelized.headache.ui.page.event.list
|
||||
|
||||
import androidx.compose.foundation.background
|
||||
import androidx.compose.foundation.layout.Arrangement
|
||||
import androidx.compose.foundation.layout.Box
|
||||
import androidx.compose.foundation.layout.Row
|
||||
import androidx.compose.foundation.layout.size
|
||||
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.font.FontStyle
|
||||
import androidx.compose.ui.tooling.preview.Preview
|
||||
import androidx.compose.ui.tooling.preview.PreviewParameter
|
||||
|
|
@ -15,12 +20,14 @@ import androidx.compose.ui.tooling.preview.PreviewParameterProvider
|
|||
import androidx.compose.ui.unit.Dp
|
||||
import androidx.compose.ui.unit.dp
|
||||
import com.pixelized.headache.ui.theme.HeadacheTheme
|
||||
import com.pixelized.headache.ui.theme.color.HeadacheColorPalette
|
||||
|
||||
@Stable
|
||||
data class PillUio(
|
||||
val label: String,
|
||||
val amount: Int,
|
||||
val description: String,
|
||||
val color: Color,
|
||||
val isValid: Boolean,
|
||||
val isMisspelled: Boolean,
|
||||
)
|
||||
|
|
@ -44,7 +51,18 @@ fun PillItem(
|
|||
Text(
|
||||
modifier = Modifier.alignByBaseline(),
|
||||
style = MaterialTheme.typography.bodyMedium,
|
||||
text = "- ${item.label}: ${item.amount}",
|
||||
text = "-",
|
||||
)
|
||||
Box(
|
||||
modifier = Modifier
|
||||
.align(alignment = Alignment.CenterVertically)
|
||||
.size(12.dp)
|
||||
.background(color = item.color)
|
||||
)
|
||||
Text(
|
||||
modifier = Modifier.alignByBaseline(),
|
||||
style = MaterialTheme.typography.bodyMedium,
|
||||
text = "${item.label}: ${item.amount}",
|
||||
)
|
||||
Text(
|
||||
modifier = Modifier.alignByBaseline(),
|
||||
|
|
@ -80,6 +98,7 @@ private class PillPreviewProvider : PreviewParameterProvider<PillUio> {
|
|||
label = "Élétriptan 40",
|
||||
amount = 2,
|
||||
description = "Élétriptan 400 x2",
|
||||
color = HeadacheColorPalette.Additional.Purple,
|
||||
isValid = true,
|
||||
isMisspelled = false,
|
||||
),
|
||||
|
|
@ -87,6 +106,7 @@ private class PillPreviewProvider : PreviewParameterProvider<PillUio> {
|
|||
label = "Élétriptan 400",
|
||||
amount = 2,
|
||||
description = "Élitriptan 400 x2",
|
||||
color = HeadacheColorPalette.Additional.Purple,
|
||||
isValid = true,
|
||||
isMisspelled = true,
|
||||
),
|
||||
|
|
@ -94,6 +114,7 @@ private class PillPreviewProvider : PreviewParameterProvider<PillUio> {
|
|||
label = "?",
|
||||
amount = 2,
|
||||
description = "Ilitriptan 400 x2",
|
||||
color = HeadacheColorPalette.Additional.Purple,
|
||||
isValid = false,
|
||||
isMisspelled = false,
|
||||
),
|
||||
|
|
@ -1,17 +1,25 @@
|
|||
package com.pixelized.headache.ui.page.summary
|
||||
|
||||
import androidx.compose.animation.animateColorAsState
|
||||
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.fillMaxSize
|
||||
import androidx.compose.foundation.layout.fillMaxWidth
|
||||
import androidx.compose.foundation.layout.padding
|
||||
import androidx.compose.foundation.layout.systemBarsPadding
|
||||
import androidx.compose.foundation.lazy.LazyColumn
|
||||
import androidx.compose.foundation.lazy.items
|
||||
import androidx.compose.foundation.shape.CircleShape
|
||||
import androidx.compose.material.icons.Icons
|
||||
import androidx.compose.material.icons.automirrored.filled.ArrowBack
|
||||
import androidx.compose.material.icons.automirrored.filled.Article
|
||||
import androidx.compose.material.icons.filled.Add
|
||||
import androidx.compose.material.icons.filled.BarChart
|
||||
import androidx.compose.material3.ExperimentalMaterial3Api
|
||||
import androidx.compose.material3.FloatingActionButton
|
||||
import androidx.compose.material3.Icon
|
||||
import androidx.compose.material3.IconButton
|
||||
import androidx.compose.material3.MaterialTheme
|
||||
|
|
@ -21,8 +29,12 @@ import androidx.compose.material3.TopAppBar
|
|||
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.Modifier
|
||||
import androidx.compose.ui.graphics.Color
|
||||
import androidx.compose.ui.res.stringResource
|
||||
import androidx.compose.ui.tooling.preview.Preview
|
||||
import androidx.compose.ui.unit.Dp
|
||||
import androidx.compose.ui.unit.dp
|
||||
import androidx.hilt.navigation.compose.hiltViewModel
|
||||
|
|
@ -30,32 +42,42 @@ import androidx.lifecycle.compose.collectAsStateWithLifecycle
|
|||
import com.pixelized.headache.R
|
||||
import com.pixelized.headache.ui.navigation.LocalNavigator
|
||||
import com.pixelized.headache.ui.navigation.destination.navigateToEventPage
|
||||
import com.pixelized.headache.ui.page.event.edit.EventEditBottomSheet
|
||||
import com.pixelized.headache.ui.page.event.edit.EventEditBottomSheetViewModel
|
||||
import com.pixelized.headache.ui.page.summary.item.MonthSummaryBox
|
||||
import com.pixelized.headache.ui.page.summary.item.MonthSummaryBoxUio
|
||||
import com.pixelized.headache.ui.page.summary.item.MonthSummaryCell
|
||||
import com.pixelized.headache.ui.page.summary.item.MonthSummaryItem
|
||||
import com.pixelized.headache.ui.page.summary.item.MonthSummaryItemUio
|
||||
import com.pixelized.headache.ui.page.summary.item.MonthSummaryTitle
|
||||
import com.pixelized.headache.ui.page.summary.item.MonthSummaryTitleUio
|
||||
import com.pixelized.headache.ui.theme.HeadacheTheme
|
||||
import com.pixelized.headache.ui.theme.color.HeadacheColorPalette
|
||||
import java.util.Date
|
||||
|
||||
@Stable
|
||||
data object MonthSummaryPageDefault {
|
||||
@Stable
|
||||
val padding: PaddingValues = PaddingValues(vertical = 8.dp)
|
||||
val listPadding: PaddingValues = PaddingValues(bottom = 56.dp + 16.dp * 2)
|
||||
|
||||
@Stable
|
||||
val spacing: Dp = 0.dp
|
||||
}
|
||||
|
||||
@Stable
|
||||
sealed interface MonthSummaryItem {
|
||||
val date: Date
|
||||
}
|
||||
|
||||
@OptIn(ExperimentalMaterial3Api::class)
|
||||
@Composable
|
||||
fun MonthSummaryPage(
|
||||
viewModel: MonthSummaryViewModel = hiltViewModel(),
|
||||
editViewModel: EventEditBottomSheetViewModel = hiltViewModel(),
|
||||
) {
|
||||
val navigation = LocalNavigator.current
|
||||
val boxMode = viewModel.boxMode.collectAsStateWithLifecycle()
|
||||
val events = viewModel.events.collectAsStateWithLifecycle()
|
||||
|
||||
MonthSummaryContent(
|
||||
modifier = Modifier.fillMaxSize(),
|
||||
modifier = Modifier
|
||||
.systemBarsPadding()
|
||||
.fillMaxSize(),
|
||||
events = events,
|
||||
boxMode = boxMode,
|
||||
onBack = {
|
||||
|
|
@ -66,7 +88,15 @@ fun MonthSummaryPage(
|
|||
},
|
||||
onItem = {
|
||||
navigation.navigateToEventPage(date = it.date)
|
||||
}
|
||||
},
|
||||
onAddEvent = {
|
||||
editViewModel.show()
|
||||
},
|
||||
)
|
||||
|
||||
EventEditBottomSheet(
|
||||
viewModel = editViewModel,
|
||||
onDismissRequest = { editViewModel.dismiss() },
|
||||
)
|
||||
}
|
||||
|
||||
|
|
@ -75,12 +105,13 @@ fun MonthSummaryPage(
|
|||
private fun MonthSummaryContent(
|
||||
modifier: Modifier = Modifier,
|
||||
spacing: Dp = MonthSummaryPageDefault.spacing,
|
||||
padding: PaddingValues = MonthSummaryPageDefault.padding,
|
||||
listPadding: PaddingValues = MonthSummaryPageDefault.listPadding,
|
||||
boxMode: State<Boolean>,
|
||||
events: State<List<MonthSummaryItem>>,
|
||||
events: State<Map<MonthSummaryCell, List<MonthSummaryCell>>>,
|
||||
onBack: () -> Unit,
|
||||
onDisplay: () -> Unit,
|
||||
onItem: (MonthSummaryItem) -> Unit,
|
||||
onItem: (MonthSummaryCell) -> Unit,
|
||||
onAddEvent: () -> Unit,
|
||||
) {
|
||||
Scaffold(
|
||||
modifier = modifier,
|
||||
|
|
@ -100,56 +131,65 @@ private fun MonthSummaryContent(
|
|||
Text(text = stringResource(R.string.month_summary_title))
|
||||
},
|
||||
actions = {
|
||||
val color = animateColorAsState(
|
||||
when (boxMode.value) {
|
||||
true -> MaterialTheme.colorScheme.error
|
||||
else -> MaterialTheme.colorScheme.onSurface
|
||||
}
|
||||
)
|
||||
IconButton(
|
||||
onClick = onDisplay,
|
||||
AnimatedContent(
|
||||
targetState = boxMode.value,
|
||||
transitionSpec = { fadeIn() togetherWith fadeOut() },
|
||||
) {
|
||||
Icon(
|
||||
imageVector = Icons.Default.BarChart,
|
||||
tint = color.value,
|
||||
contentDescription = null,
|
||||
)
|
||||
IconButton(
|
||||
onClick = onDisplay,
|
||||
) {
|
||||
Icon(
|
||||
imageVector = when (it) {
|
||||
true -> Icons.AutoMirrored.Filled.Article
|
||||
else -> Icons.Default.BarChart
|
||||
},
|
||||
contentDescription = null,
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
)
|
||||
},
|
||||
floatingActionButton = {
|
||||
FloatingActionButton(
|
||||
containerColor = MaterialTheme.colorScheme.primary,
|
||||
shape = CircleShape,
|
||||
onClick = onAddEvent,
|
||||
) {
|
||||
Icon(
|
||||
imageVector = Icons.Default.Add,
|
||||
contentDescription = null,
|
||||
)
|
||||
}
|
||||
},
|
||||
content = { paddingValues ->
|
||||
LazyColumn(
|
||||
modifier = Modifier
|
||||
.padding(paddingValues = paddingValues)
|
||||
.fillMaxSize(),
|
||||
contentPadding = padding,
|
||||
contentPadding = listPadding,
|
||||
verticalArrangement = Arrangement.spacedBy(space = spacing),
|
||||
reverseLayout = true,
|
||||
reverseLayout = false,
|
||||
) {
|
||||
items(
|
||||
items = events.value,
|
||||
key = { it.date },
|
||||
contentType = {
|
||||
when (it) {
|
||||
is MonthSummaryBoxUio -> "MonthSummaryBoxUio"
|
||||
is MonthSummaryItemUio -> "MonthSummaryItemUio"
|
||||
}
|
||||
},
|
||||
) { item ->
|
||||
when (item) {
|
||||
is MonthSummaryBoxUio -> MonthSummaryBox(
|
||||
modifier = Modifier
|
||||
.animateItem()
|
||||
.fillMaxWidth(),
|
||||
item = item,
|
||||
events.value.forEach { entry ->
|
||||
stickyHeader {
|
||||
MonthSummaryCell(
|
||||
item = entry.key,
|
||||
onItem = onItem,
|
||||
)
|
||||
|
||||
is MonthSummaryItemUio -> MonthSummaryItem(
|
||||
modifier = Modifier
|
||||
.animateItem()
|
||||
.fillMaxWidth(),
|
||||
}
|
||||
items(
|
||||
items = entry.value,
|
||||
key = { it.date },
|
||||
contentType = {
|
||||
when (it) {
|
||||
is MonthSummaryBoxUio -> "MonthSummaryBoxUio"
|
||||
is MonthSummaryItemUio -> "MonthSummaryItemUio"
|
||||
is MonthSummaryTitleUio -> "MonthSummaryTitleUio"
|
||||
}
|
||||
},
|
||||
) { item ->
|
||||
MonthSummaryCell(
|
||||
item = item,
|
||||
onItem = onItem,
|
||||
)
|
||||
|
|
@ -158,4 +198,81 @@ private fun MonthSummaryContent(
|
|||
}
|
||||
}
|
||||
)
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun MonthSummaryCell(
|
||||
item: MonthSummaryCell,
|
||||
onItem: (MonthSummaryCell) -> Unit,
|
||||
) {
|
||||
AnimatedContent(
|
||||
modifier = Modifier.fillMaxWidth(),
|
||||
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(
|
||||
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,
|
||||
),
|
||||
),
|
||||
),
|
||||
MonthSummaryBoxUio(
|
||||
date = Date(),
|
||||
headacheRatio = 8f / 30f,
|
||||
headacheAmount = 8,
|
||||
headacheColor = Color.Red,
|
||||
pillRatio = 6f / 20f,
|
||||
pillAmount = 6,
|
||||
pillColor = Color.Blue,
|
||||
),
|
||||
),
|
||||
)
|
||||
)
|
||||
},
|
||||
onBack = { },
|
||||
onDisplay = { },
|
||||
onItem = { },
|
||||
onAddEvent = { },
|
||||
)
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,88 @@
|
|||
package com.pixelized.headache.ui.page.summary
|
||||
|
||||
import androidx.compose.foundation.background
|
||||
import androidx.compose.foundation.layout.Arrangement
|
||||
import androidx.compose.foundation.layout.Box
|
||||
import androidx.compose.foundation.layout.PaddingValues
|
||||
import androidx.compose.foundation.layout.Row
|
||||
import androidx.compose.foundation.layout.padding
|
||||
import androidx.compose.foundation.layout.size
|
||||
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.tooling.preview.Preview
|
||||
import androidx.compose.ui.unit.Dp
|
||||
import androidx.compose.ui.unit.dp
|
||||
import com.pixelized.headache.ui.theme.HeadacheTheme
|
||||
import com.pixelized.headache.ui.theme.color.HeadacheColorPalette
|
||||
|
||||
@Stable
|
||||
data class MonthSummaryPillItemUio(
|
||||
val label: String,
|
||||
val amount: Int,
|
||||
val color: Color,
|
||||
)
|
||||
|
||||
@Stable
|
||||
data object MonthSummaryPillItemDefault {
|
||||
@Stable
|
||||
val padding: PaddingValues = PaddingValues(0.dp)
|
||||
|
||||
@Stable
|
||||
val colorBoxSize: Dp = 8.dp
|
||||
|
||||
@Stable
|
||||
val spacing: Dp = 4.dp
|
||||
}
|
||||
|
||||
@Composable
|
||||
fun MonthSummaryPillItem(
|
||||
modifier: Modifier = Modifier,
|
||||
padding: PaddingValues = MonthSummaryPillItemDefault.padding,
|
||||
spacing: Dp = MonthSummaryPillItemDefault.spacing,
|
||||
colorBoxSize: Dp = MonthSummaryPillItemDefault.colorBoxSize,
|
||||
item: MonthSummaryPillItemUio,
|
||||
) {
|
||||
Row(
|
||||
modifier = Modifier
|
||||
.padding(paddingValues = padding)
|
||||
.then(other = modifier),
|
||||
horizontalArrangement = Arrangement.spacedBy(space = spacing),
|
||||
) {
|
||||
Text(
|
||||
style = MaterialTheme.typography.bodyMedium,
|
||||
text = "-",
|
||||
)
|
||||
Box(
|
||||
modifier = Modifier
|
||||
.align(alignment = Alignment.CenterVertically)
|
||||
.size(size = colorBoxSize)
|
||||
.background(color = item.color)
|
||||
)
|
||||
Text(
|
||||
style = MaterialTheme.typography.bodyMedium,
|
||||
text = "${item.label} : ${item.amount}",
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
@Preview
|
||||
private fun MonthSummaryPillItemPreview() {
|
||||
HeadacheTheme {
|
||||
Surface {
|
||||
MonthSummaryPillItem(
|
||||
item = MonthSummaryPillItemUio(
|
||||
label = "Élétriptan 40",
|
||||
amount = 1,
|
||||
color = HeadacheColorPalette.Pill.Eletriptan40,
|
||||
)
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -2,23 +2,22 @@ package com.pixelized.headache.ui.page.summary
|
|||
|
||||
import androidx.lifecycle.ViewModel
|
||||
import androidx.lifecycle.viewModelScope
|
||||
import com.pixelized.headache.repository.calendar.GoogleCalendarIdRepository
|
||||
import com.pixelized.headache.repository.event.Event
|
||||
import com.pixelized.headache.repository.event.EventRepository
|
||||
import com.pixelized.headache.ui.page.summary.item.MonthSummaryCell
|
||||
import com.pixelized.headache.ui.page.summary.item.MonthSummaryFactory
|
||||
import com.pixelized.headache.ui.page.summary.item.MonthSummaryTitleUio
|
||||
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.flatMapLatest
|
||||
import kotlinx.coroutines.flow.flowOf
|
||||
import kotlinx.coroutines.flow.stateIn
|
||||
import javax.inject.Inject
|
||||
|
||||
@HiltViewModel
|
||||
class MonthSummaryViewModel @Inject constructor(
|
||||
googleCalendarIdRepository: GoogleCalendarIdRepository,
|
||||
eventRepository: EventRepository,
|
||||
eventItemFactory: MonthSummaryFactory,
|
||||
) : ViewModel() {
|
||||
|
|
@ -27,23 +26,20 @@ class MonthSummaryViewModel @Inject constructor(
|
|||
val boxMode: StateFlow<Boolean> = displayTypeFlow
|
||||
|
||||
@OptIn(ExperimentalCoroutinesApi::class)
|
||||
val events: StateFlow<List<MonthSummaryItem>> = googleCalendarIdRepository.calendarId
|
||||
.flatMapLatest { id: Long? ->
|
||||
when (id) {
|
||||
null -> flowOf(emptyList())
|
||||
else -> eventRepository.eventFlow(calendarId = id)
|
||||
}
|
||||
}.combine(displayTypeFlow) { events: List<Event>, display ->
|
||||
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 = emptyList(),
|
||||
)
|
||||
).stateIn(
|
||||
scope = viewModelScope,
|
||||
started = SharingStarted.Lazily,
|
||||
initialValue = emptyMap(),
|
||||
)
|
||||
|
||||
fun toggleDisplay() {
|
||||
displayTypeFlow.value = displayTypeFlow.value.not()
|
||||
|
|
|
|||
|
|
@ -1,4 +1,4 @@
|
|||
package com.pixelized.headache.ui.page.summary
|
||||
package com.pixelized.headache.ui.page.summary.item
|
||||
|
||||
import android.annotation.SuppressLint
|
||||
import android.icu.text.SimpleDateFormat
|
||||
|
|
@ -30,6 +30,7 @@ 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
|
||||
|
||||
|
|
@ -42,7 +43,7 @@ data object MonthSummaryBoxDefault {
|
|||
val spacing: DpSize = DpSize(width = 8.dp, height = 4.dp)
|
||||
|
||||
@Stable
|
||||
val labelWidth: Dp = 64.dp + 16.dp
|
||||
val labelWidth: Dp = 32.dp + 16.dp
|
||||
|
||||
@Stable
|
||||
val boxHeight: Dp = 16.dp
|
||||
|
|
@ -52,7 +53,7 @@ data object MonthSummaryBoxDefault {
|
|||
|
||||
@SuppressLint("ConstantLocale")
|
||||
@Stable
|
||||
val formatter = SimpleDateFormat("MMM yyyy", Locale.getDefault())
|
||||
val formatter = SimpleDateFormat("MMM", Locale.getDefault())
|
||||
}
|
||||
|
||||
@Stable
|
||||
|
|
@ -64,7 +65,7 @@ data class MonthSummaryBoxUio(
|
|||
val pillRatio: Float?,
|
||||
val pillAmount: Int,
|
||||
val pillColor: Color,
|
||||
) : MonthSummaryItem
|
||||
) : MonthSummaryCell
|
||||
|
||||
@Composable
|
||||
fun MonthSummaryBox(
|
||||
|
|
@ -90,7 +91,7 @@ fun MonthSummaryBox(
|
|||
modifier = Modifier.width(width = labelWidth),
|
||||
style = MaterialTheme.typography.bodyMedium,
|
||||
textAlign = TextAlign.End,
|
||||
text = formatter.format(item.date),
|
||||
text = formatter.format(item.date).capitalize(),
|
||||
)
|
||||
Column(
|
||||
verticalArrangement = Arrangement.spacedBy(space = spacing.height),
|
||||
|
|
@ -0,0 +1,9 @@
|
|||
package com.pixelized.headache.ui.page.summary.item
|
||||
|
||||
import androidx.compose.runtime.Stable
|
||||
import java.util.Date
|
||||
|
||||
@Stable
|
||||
sealed interface MonthSummaryCell {
|
||||
val date: Date
|
||||
}
|
||||
|
|
@ -1,34 +1,50 @@
|
|||
package com.pixelized.headache.ui.page.summary
|
||||
package com.pixelized.headache.ui.page.summary.item
|
||||
|
||||
import android.icu.util.Calendar
|
||||
import androidx.compose.ui.graphics.Color
|
||||
import com.pixelized.headache.repository.event.Event
|
||||
import com.pixelized.headache.ui.page.summary.MonthSummaryPillItemUio
|
||||
import com.pixelized.headache.utils.extention.event
|
||||
import javax.inject.Inject
|
||||
import kotlin.math.max
|
||||
import kotlin.reflect.KClass
|
||||
|
||||
class MonthSummaryFactory @Inject constructor() {
|
||||
private val calendar = Calendar.getInstance()
|
||||
|
||||
fun convertToItemUio(
|
||||
events: List<Event>,
|
||||
): List<MonthSummaryItemUio> {
|
||||
events: Collection<Event>,
|
||||
): Map<MonthSummaryCell, List<MonthSummaryCell>> {
|
||||
return events
|
||||
.fold(hashMapOf<Event.Date, MutableList<Event>>()) { acc, event ->
|
||||
acc.also { it.getOrPut(event.date.copy(day = 1)) { mutableListOf() }.add(event) }
|
||||
}
|
||||
.mapKeys { entry ->
|
||||
.map { entry ->
|
||||
val pills = entry.value
|
||||
.fold(hashMapOf<String, Int>()) { acc, event ->
|
||||
.fold(hashMapOf<Event.Pill.Id, EventFolder>()) { acc, event ->
|
||||
event.pills.forEach { pill ->
|
||||
acc[pill.label] = acc.getOrDefault(pill.label, 0) + pill.amount
|
||||
val value = acc.getOrElse(
|
||||
key = pill.id,
|
||||
defaultValue = {
|
||||
EventFolder(
|
||||
label = pill.label,
|
||||
amount = 0,
|
||||
color = pill.color,
|
||||
)
|
||||
},
|
||||
)
|
||||
value.amount += 1
|
||||
acc[pill.id] = value
|
||||
}
|
||||
acc
|
||||
}
|
||||
.mapKeys {
|
||||
it.key to it.value
|
||||
.map { entry ->
|
||||
MonthSummaryPillItemUio(
|
||||
label = entry.value.label,
|
||||
color = entry.value.color,
|
||||
amount = entry.value.amount,
|
||||
)
|
||||
}
|
||||
.keys
|
||||
.toList()
|
||||
|
||||
MonthSummaryItemUio(
|
||||
|
|
@ -37,14 +53,13 @@ class MonthSummaryFactory @Inject constructor() {
|
|||
pills = pills,
|
||||
)
|
||||
}
|
||||
.keys
|
||||
.toList()
|
||||
.sortedByDescending { it.date }
|
||||
.groupByMonth()
|
||||
}
|
||||
|
||||
fun convertToBoxUio(
|
||||
events: List<Event>,
|
||||
): List<MonthSummaryBoxUio> {
|
||||
events: Collection<Event>,
|
||||
): Map<MonthSummaryCell, List<MonthSummaryCell>> {
|
||||
var maxPillAmount = 0
|
||||
return events
|
||||
.fold(hashMapOf<Event.Date, MutableList<Event>>()) { acc, event ->
|
||||
|
|
@ -57,9 +72,10 @@ class MonthSummaryFactory @Inject constructor() {
|
|||
)
|
||||
entry.key
|
||||
}
|
||||
.mapKeys { entry ->
|
||||
val pillAmount =
|
||||
entry.value.sumOf { events -> events.pills.sumOf { pill -> pill.amount } }
|
||||
.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)
|
||||
|
|
@ -77,8 +93,25 @@ class MonthSummaryFactory @Inject constructor() {
|
|||
pillColor = Color.Blue,
|
||||
)
|
||||
}
|
||||
.keys
|
||||
.toList()
|
||||
.sortedByDescending { it.date }
|
||||
.groupByMonth()
|
||||
}
|
||||
|
||||
fun List<MonthSummaryCell>.groupByMonth(): Map<MonthSummaryCell, List<MonthSummaryCell>> {
|
||||
return this.groupBy {
|
||||
MonthSummaryTitleUio(
|
||||
date = calendar.apply {
|
||||
time = it.date
|
||||
set(Calendar.DAY_OF_YEAR, 1)
|
||||
set(Calendar.MILLISECONDS_IN_DAY, 0)
|
||||
}.time,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
private class EventFolder(
|
||||
val label: String,
|
||||
val color: Color,
|
||||
var amount: Int,
|
||||
)
|
||||
}
|
||||
|
|
@ -1,4 +1,4 @@
|
|||
package com.pixelized.headache.ui.page.summary
|
||||
package com.pixelized.headache.ui.page.summary.item
|
||||
|
||||
import android.annotation.SuppressLint
|
||||
import android.icu.text.SimpleDateFormat
|
||||
|
|
@ -20,7 +20,11 @@ import androidx.compose.ui.tooling.preview.PreviewParameter
|
|||
import androidx.compose.ui.tooling.preview.PreviewParameterProvider
|
||||
import androidx.compose.ui.unit.DpSize
|
||||
import androidx.compose.ui.unit.dp
|
||||
import com.pixelized.headache.ui.page.summary.MonthSummaryPillItem
|
||||
import com.pixelized.headache.ui.page.summary.MonthSummaryPillItemUio
|
||||
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
|
||||
|
||||
|
|
@ -28,8 +32,8 @@ import java.util.Locale
|
|||
data class MonthSummaryItemUio(
|
||||
override val date: Date,
|
||||
val days: Int,
|
||||
val pills: List<Pair<String, Int>>,
|
||||
) : MonthSummaryItem
|
||||
val pills: List<MonthSummaryPillItemUio>,
|
||||
) : MonthSummaryCell
|
||||
|
||||
@Stable
|
||||
object MonthSummaryItemDefault {
|
||||
|
|
@ -42,7 +46,7 @@ object MonthSummaryItemDefault {
|
|||
|
||||
@SuppressLint("ConstantLocale")
|
||||
@Stable
|
||||
val formatter = SimpleDateFormat("MMMM yyyy", Locale.getDefault())
|
||||
val formatter = SimpleDateFormat("MMMM", Locale.getDefault())
|
||||
}
|
||||
|
||||
@Composable
|
||||
|
|
@ -83,20 +87,14 @@ fun MonthSummaryItem(
|
|||
}
|
||||
Column {
|
||||
item.pills.forEach {
|
||||
Text(
|
||||
style = MaterialTheme.typography.bodyMedium,
|
||||
text = "- ${it.first} : ${it.second}",
|
||||
MonthSummaryPillItem(
|
||||
item = it,
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
@Stable
|
||||
fun String.capitalize() =
|
||||
replaceFirstChar { if (it.isLowerCase()) it.titlecase(Locale.getDefault()) else it.toString() }
|
||||
|
||||
@Composable
|
||||
@Preview
|
||||
private fun MonthSummaryItemPreview(
|
||||
|
|
@ -120,10 +118,26 @@ private class PreviewProvider() : PreviewParameterProvider<MonthSummaryItemUio>
|
|||
date = Date(),
|
||||
days = 11,
|
||||
pills = listOf(
|
||||
"Paracétamol 1000" to 2,
|
||||
"Ibuprofen 400" to 4,
|
||||
"Spifen 400" to 6,
|
||||
"Eletripan 40" to 0,
|
||||
MonthSummaryPillItemUio(
|
||||
label = "Paracétamol 1000",
|
||||
amount = 2,
|
||||
color = HeadacheColorPalette.Pill.Paracetamol1000,
|
||||
),
|
||||
MonthSummaryPillItemUio(
|
||||
label = "Ibuprofen 400",
|
||||
amount = 4,
|
||||
color = HeadacheColorPalette.Pill.Ibuprofene400,
|
||||
),
|
||||
MonthSummaryPillItemUio(
|
||||
label = "Spifen 400",
|
||||
amount = 6,
|
||||
color = HeadacheColorPalette.Pill.Spifen400,
|
||||
),
|
||||
MonthSummaryPillItemUio(
|
||||
label = "Eletripan 40",
|
||||
amount = 0,
|
||||
color = HeadacheColorPalette.Pill.Eletriptan40,
|
||||
),
|
||||
),
|
||||
)
|
||||
)
|
||||
|
|
@ -0,0 +1,71 @@
|
|||
package com.pixelized.headache.ui.page.summary.item
|
||||
|
||||
import android.annotation.SuppressLint
|
||||
import android.icu.text.SimpleDateFormat
|
||||
import androidx.compose.foundation.background
|
||||
import androidx.compose.foundation.layout.Box
|
||||
import androidx.compose.foundation.layout.PaddingValues
|
||||
import androidx.compose.foundation.layout.padding
|
||||
import androidx.compose.material3.MaterialTheme
|
||||
import androidx.compose.material3.Text
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.Stable
|
||||
import androidx.compose.runtime.remember
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.graphics.Brush
|
||||
import androidx.compose.ui.unit.dp
|
||||
import java.util.Date
|
||||
import java.util.Locale
|
||||
|
||||
@Stable
|
||||
data class MonthSummaryTitleUio(
|
||||
override val date: Date,
|
||||
) : MonthSummaryCell
|
||||
|
||||
@Stable
|
||||
object MonthSummaryTitleDefault {
|
||||
|
||||
@Stable
|
||||
val padding: PaddingValues = PaddingValues(horizontal = 16.dp, vertical = 8.dp)
|
||||
|
||||
@SuppressLint("ConstantLocale")
|
||||
@Stable
|
||||
val formatter = SimpleDateFormat("yyyy", Locale.getDefault())
|
||||
}
|
||||
|
||||
val backgroundBrush: Brush
|
||||
@Composable
|
||||
@Stable
|
||||
get() {
|
||||
val colorScheme = MaterialTheme.colorScheme
|
||||
return remember(colorScheme) {
|
||||
Brush.verticalGradient(
|
||||
listOf(
|
||||
colorScheme.surface,
|
||||
colorScheme.surface,
|
||||
colorScheme.surface,
|
||||
colorScheme.surface.copy(alpha = 0f),
|
||||
)
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
fun MonthSummaryTitle(
|
||||
modifier: Modifier = Modifier,
|
||||
padding: PaddingValues = MonthSummaryTitleDefault.padding,
|
||||
formatter: SimpleDateFormat = MonthSummaryTitleDefault.formatter,
|
||||
item: MonthSummaryTitleUio,
|
||||
) {
|
||||
Box(
|
||||
modifier = Modifier
|
||||
.background(brush = backgroundBrush)
|
||||
.then(other = modifier),
|
||||
) {
|
||||
Text(
|
||||
modifier = Modifier.padding(paddingValues = padding),
|
||||
style = MaterialTheme.typography.titleLarge,
|
||||
text = formatter.format(item.date)
|
||||
)
|
||||
}
|
||||
}
|
||||
|
|
@ -1,11 +0,0 @@
|
|||
package com.pixelized.headache.ui.theme
|
||||
|
||||
import androidx.compose.ui.graphics.Color
|
||||
|
||||
val Purple80 = Color(0xFFD0BCFF)
|
||||
val PurpleGrey80 = Color(0xFFCCC2DC)
|
||||
val Pink80 = Color(0xFFEFB8C8)
|
||||
|
||||
val Purple40 = Color(0xFF6650a4)
|
||||
val PurpleGrey40 = Color(0xFF625b71)
|
||||
val Pink40 = Color(0xFF7D5260)
|
||||
|
|
@ -1,57 +1,42 @@
|
|||
package com.pixelized.headache.ui.theme
|
||||
|
||||
import android.app.Activity
|
||||
import android.os.Build
|
||||
import androidx.compose.foundation.isSystemInDarkTheme
|
||||
import androidx.compose.material3.MaterialTheme
|
||||
import androidx.compose.material3.darkColorScheme
|
||||
import androidx.compose.material3.dynamicDarkColorScheme
|
||||
import androidx.compose.material3.dynamicLightColorScheme
|
||||
import androidx.compose.material3.lightColorScheme
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.ui.platform.LocalContext
|
||||
import androidx.compose.runtime.ReadOnlyComposable
|
||||
import androidx.compose.runtime.Stable
|
||||
import androidx.compose.runtime.compositionLocalOf
|
||||
import com.pixelized.headache.ui.theme.color.HeadacheColors
|
||||
import com.pixelized.headache.ui.theme.color.headacheDarkColorScheme
|
||||
import com.pixelized.headache.ui.theme.color.headacheLightColorScheme
|
||||
|
||||
private val DarkColorScheme = darkColorScheme(
|
||||
primary = Purple80,
|
||||
secondary = PurpleGrey80,
|
||||
tertiary = Pink80
|
||||
)
|
||||
val LocalLwaTheme = compositionLocalOf<HeadacheTheme> {
|
||||
error("Local Snack Controller is not yet ready")
|
||||
}
|
||||
|
||||
private val LightColorScheme = lightColorScheme(
|
||||
primary = Purple40,
|
||||
secondary = PurpleGrey40,
|
||||
tertiary = Pink40
|
||||
val MaterialTheme.headache: HeadacheTheme
|
||||
@Stable
|
||||
@Composable
|
||||
@ReadOnlyComposable
|
||||
get() = LocalLwaTheme.current
|
||||
|
||||
/* Other default colors to override
|
||||
background = Color(0xFFFFFBFE),
|
||||
surface = Color(0xFFFFFBFE),
|
||||
onPrimary = Color.White,
|
||||
onSecondary = Color.White,
|
||||
onTertiary = Color.White,
|
||||
onBackground = Color(0xFF1C1B1F),
|
||||
onSurface = Color(0xFF1C1B1F),
|
||||
*/
|
||||
@Stable
|
||||
data class HeadacheTheme(
|
||||
val colorScheme: HeadacheColors,
|
||||
)
|
||||
|
||||
@Composable
|
||||
fun HeadacheTheme(
|
||||
darkTheme: Boolean = isSystemInDarkTheme(),
|
||||
// Dynamic color is available on Android 12+
|
||||
dynamicColor: Boolean = true,
|
||||
content: @Composable () -> Unit
|
||||
content: @Composable () -> Unit,
|
||||
) {
|
||||
val colorScheme = when {
|
||||
dynamicColor && Build.VERSION.SDK_INT >= Build.VERSION_CODES.S -> {
|
||||
val context = LocalContext.current
|
||||
if (darkTheme) dynamicDarkColorScheme(context) else dynamicLightColorScheme(context)
|
||||
}
|
||||
|
||||
darkTheme -> DarkColorScheme
|
||||
else -> LightColorScheme
|
||||
darkTheme -> headacheDarkColorScheme()
|
||||
else -> headacheLightColorScheme()
|
||||
}
|
||||
|
||||
MaterialTheme(
|
||||
colorScheme = colorScheme,
|
||||
colorScheme = colorScheme.base,
|
||||
typography = Typography,
|
||||
content = content
|
||||
)
|
||||
|
|
|
|||
|
|
@ -0,0 +1,60 @@
|
|||
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 {
|
||||
|
||||
@Immutable
|
||||
object Pill {
|
||||
val Unknown = Additional.Red
|
||||
val Ibuprofene400 = Additional.Blue
|
||||
val Paracetamol1000 = Additional.Green
|
||||
val Spifen400 = Additional.Yellow
|
||||
val Eletriptan40 = Additional.Pink
|
||||
}
|
||||
|
||||
@Immutable
|
||||
object Additional {
|
||||
val VeryDarkBlue: Color = Color(0xFF09179D)
|
||||
val DarkBlue: Color = Color(0xFF1A2BDB)
|
||||
val MediumBlue: Color = Color(0xFF1523A2)
|
||||
val Blue: Color = Color(0xFF2970F2)
|
||||
val LightBlue: Color = Color(0xFF1A91DB)
|
||||
val VeryLightBlue: Color = Color(0xFF1EDDEF)
|
||||
val VeryDarkPurple: Color = Color(0xFF5F0E9E)
|
||||
val DarkPurple: Color = Color(0xFF8330DB)
|
||||
val Purple: Color = Color(0xFF9B54C3)
|
||||
val LightPurple: Color = Color(0xFFBC52D9)
|
||||
val VeryLightPurple: Color = Color(0xFFC856D1)
|
||||
val VeryDarkGreen: Color = Color(0xFF16544A)
|
||||
val DarkGreen: Color = Color(0xFF207A6B)
|
||||
val Green: Color = Color(0xFF269482)
|
||||
val LightGreen: Color = Color(0xFF2AA18D)
|
||||
val VeryLightGreen: Color = Color(0xFF3AE0C5)
|
||||
val VeryDarkRed: Color = Color(0xFF631221)
|
||||
val DarkRed: Color = Color(0xFFA21D36)
|
||||
val Red: Color = Color(0xFFC92443)
|
||||
val LightRed: Color = Color(0xFFE32849)
|
||||
val VeryLightRed: Color = Color(0xFFF02B4F)
|
||||
val VeryDarkPink: Color = Color(0xFF960064)
|
||||
val DarkPink: Color = Color(0xFFBD007E)
|
||||
val Pink: Color = Color(0xFFD6008F)
|
||||
val LightPink: Color = Color(0xFFE35BB5)
|
||||
val VeryLightPink: Color = Color(0xFFFF66CC)
|
||||
val VeryDarkYellow: Color = Color(0xFFB76036)
|
||||
val DarkYellow: Color = Color(0xFFD48341)
|
||||
val Yellow: Color = Color(0xFFF3A850)
|
||||
val LightYellow: Color = Color(0xFFF5BF63)
|
||||
val VeryLightYellow: Color = Color(0xFFF9D679)
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,58 @@
|
|||
package com.pixelized.headache.ui.theme.color
|
||||
|
||||
import androidx.compose.material3.ColorScheme
|
||||
import androidx.compose.material3.MaterialTheme
|
||||
import androidx.compose.material3.darkColorScheme
|
||||
import androidx.compose.material3.lightColorScheme
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.ReadOnlyComposable
|
||||
import androidx.compose.runtime.Stable
|
||||
import androidx.compose.ui.graphics.Color
|
||||
import androidx.compose.ui.graphics.compositeOver
|
||||
import androidx.compose.ui.unit.Dp
|
||||
import androidx.compose.ui.unit.dp
|
||||
import kotlin.math.ln
|
||||
|
||||
@Stable
|
||||
data class HeadacheColors(
|
||||
val base: ColorScheme,
|
||||
)
|
||||
|
||||
@Composable
|
||||
@Stable
|
||||
fun headacheDarkColorScheme(
|
||||
base: ColorScheme = darkColorScheme(),
|
||||
) = HeadacheColors(
|
||||
base = base,
|
||||
)
|
||||
|
||||
@Composable
|
||||
@Stable
|
||||
fun headacheLightColorScheme(
|
||||
base: ColorScheme = lightColorScheme(),
|
||||
) = HeadacheColors(
|
||||
base = base,
|
||||
)
|
||||
|
||||
|
||||
@ReadOnlyComposable
|
||||
@Composable
|
||||
fun calculateElevatedColor(
|
||||
color: Color,
|
||||
onColor: Color = MaterialTheme.colorScheme.surface,
|
||||
elevation: Dp,
|
||||
): Color {
|
||||
return if (elevation > 0.dp) {
|
||||
val foregroundColor = calculateForegroundColor(onColor, elevation)
|
||||
foregroundColor.compositeOver(color)
|
||||
} else {
|
||||
color
|
||||
}
|
||||
}
|
||||
|
||||
@ReadOnlyComposable
|
||||
@Composable
|
||||
private fun calculateForegroundColor(color: Color, elevation: Dp): Color {
|
||||
val alpha = ((4.5f * ln(elevation.value + 1)) + 2f) / 100f
|
||||
return color.copy(alpha = alpha)
|
||||
}
|
||||
|
|
@ -0,0 +1,24 @@
|
|||
package com.pixelized.headache.utils.extention
|
||||
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.layout.boundsInWindow
|
||||
import androidx.compose.ui.layout.onGloballyPositioned
|
||||
|
||||
/**
|
||||
* is an extension function for Modifier.
|
||||
* It allows you to attach visibility detection logic to any composable element
|
||||
*/
|
||||
fun Modifier.isElementVisible(onVisibilityChanged: (Boolean) -> Unit): Modifier {
|
||||
var isVisible = false
|
||||
return this.onGloballyPositioned { layoutCoordinates ->
|
||||
val localIsVisible = layoutCoordinates.parentLayoutCoordinates?.let {
|
||||
val parentBounds = it.boundsInWindow()
|
||||
val childBounds = layoutCoordinates.boundsInWindow()
|
||||
parentBounds.overlaps(childBounds)
|
||||
} ?: false
|
||||
if (isVisible != localIsVisible) {
|
||||
isVisible = localIsVisible
|
||||
onVisibilityChanged(localIsVisible)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,10 @@
|
|||
package com.pixelized.headache.utils.extention
|
||||
|
||||
import androidx.compose.ui.Modifier
|
||||
|
||||
fun Modifier.thenIf(condition: Boolean, block: Modifier.() -> Modifier): Modifier {
|
||||
return when (condition) {
|
||||
true -> block(this)
|
||||
else -> this
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,14 @@
|
|||
package com.pixelized.headache.utils.extention
|
||||
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.Stable
|
||||
import java.util.Locale
|
||||
|
||||
@Composable
|
||||
@Stable
|
||||
fun String.capitalize() = replaceFirstChar {
|
||||
when {
|
||||
it.isLowerCase() -> it.titlecase(Locale.getDefault())
|
||||
else -> it.toString()
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,13 @@
|
|||
package com.pixelized.headache.utils
|
||||
|
||||
import androidx.compose.foundation.text.KeyboardActionScope
|
||||
import androidx.compose.foundation.text.KeyboardActions
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.remember
|
||||
|
||||
@Composable
|
||||
fun rememberKeyboardActions(
|
||||
onAny: KeyboardActionScope.() -> Unit,
|
||||
): KeyboardActions {
|
||||
return remember { KeyboardActions(onAny = onAny) }
|
||||
}
|
||||
|
|
@ -1,7 +1,12 @@
|
|||
<resources>
|
||||
<string name="app_name">Céphalée</string>
|
||||
|
||||
<string name="action_confirm">Confirmer</string>
|
||||
<string name="action_delete">Supprimer</string>
|
||||
|
||||
<string name="error_edit_calendar">L\'édition du calendrier a échouée</string>
|
||||
|
||||
<string name="calendar_chooser_title">Choix du calendrier</string>
|
||||
<string name="event_title">Évennement migraineux</string>
|
||||
<string name="month_summary_title">Suivit des migraines</string>
|
||||
<string name="month_summary_title">Suivi des migraines</string>
|
||||
</resources>
|
||||
|
|
@ -1,6 +1,11 @@
|
|||
<resources>
|
||||
<string name="app_name">Headache</string>
|
||||
|
||||
<string name="action_confirm">Confirm</string>
|
||||
<string name="action_delete">Delete</string>
|
||||
|
||||
<string name="error_edit_calendar">Calendar edit failed.</string>
|
||||
|
||||
<string name="calendar_chooser_title">Choose your calendar</string>
|
||||
<string name="event_title">Headache event</string>
|
||||
<string name="month_summary_title">Headache summary</string>
|
||||
|
|
|
|||
|
|
@ -1,10 +1,10 @@
|
|||
package com.pixelized.headache
|
||||
|
||||
import com.pixelized.headache.repository.event.EventFactory
|
||||
import com.pixelized.headache.repository.event.factory.PillFactory
|
||||
import org.junit.Test
|
||||
|
||||
class EventFactoryText {
|
||||
private val factory = EventFactory()
|
||||
class EventFactoryTest {
|
||||
private val factory = PillFactory()
|
||||
|
||||
@Test
|
||||
fun testPill() {
|
||||
Loading…
Add table
Add a link
Reference in a new issue