Skip to content

Commit

Permalink
Provide background+text color combinations for calendar screen
Browse files Browse the repository at this point in the history
  • Loading branch information
carstenhag committed Feb 14, 2025
1 parent 6e83be8 commit c9c61e4
Show file tree
Hide file tree
Showing 7 changed files with 112 additions and 57 deletions.

This file was deleted.

65 changes: 35 additions & 30 deletions app/src/main/java/com/mensinator/app/calendar/CalendarScreen.kt
Original file line number Diff line number Diff line change
Expand Up @@ -14,11 +14,11 @@ import androidx.compose.ui.draw.clip
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.platform.LocalContext
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.text.TextStyle
import androidx.compose.ui.text.font.FontWeight
import androidx.compose.ui.text.style.TextAlign
import androidx.compose.ui.unit.dp
import androidx.compose.ui.unit.sp
import androidx.lifecycle.compose.collectAsStateWithLifecycle
import com.kizitonwose.calendar.compose.VerticalCalendar
import com.kizitonwose.calendar.compose.rememberCalendarState
import com.kizitonwose.calendar.core.CalendarDay
Expand All @@ -30,7 +30,10 @@ import com.mensinator.app.business.IPeriodDatabaseHelper
import com.mensinator.app.calendar.CalendarViewModel.UiAction
import com.mensinator.app.data.ColorSource
import com.mensinator.app.extensions.stringRes
import com.mensinator.app.settings.ColorSetting
import com.mensinator.app.ui.navigation.displayCutoutExcludingStatusBarsPadding
import com.mensinator.app.ui.theme.Black
import com.mensinator.app.ui.theme.DarkGrey
import com.mensinator.app.ui.theme.isDarkMode
import kotlinx.collections.immutable.*
import org.koin.androidx.compose.koinViewModel
Expand All @@ -48,7 +51,12 @@ fun CalendarScreen(
modifier: Modifier,
viewModel: CalendarViewModel = koinViewModel()
) {
val state = viewModel.viewState.collectAsState()
val state = viewModel.viewState.collectAsStateWithLifecycle()

val isDarkMode = isDarkMode()
LaunchedEffect(isDarkMode) {
viewModel.updateDarkModeStatus(isDarkMode)
}

val context = LocalContext.current
val notificationScheduler: INotificationScheduler = koinInject()
Expand Down Expand Up @@ -297,7 +305,7 @@ fun Day(
val state = viewState.value

val colorMap = ColorSource.getColorMap(isDarkMode())
val calendarColors = state.calendarColors
val calendarColorMap = state.calendarColorMap
val dbHelper: IPeriodDatabaseHelper = koinInject()

if (day.position != DayPosition.MonthDate) {
Expand All @@ -308,22 +316,22 @@ fun Day(
return
}

val backgroundColor = when {
day.date in state.selectedDays -> colorMap[calendarColors?.selectedColorName]
?: colorMap["LightGray"]
day.date in state.periodDates.keys -> colorMap[calendarColors?.periodColorName]
?: colorMap["Red"]
state.periodPredictionDate?.isEqual(day.date) == true -> colorMap[calendarColors?.nextPeriodColorName]
?: colorMap["Yellow"]
day.date in state.ovulationDates -> colorMap[calendarColors?.ovulationColorName]
?: colorMap["Blue"]
state.ovulationPredictionDate?.isEqual(day.date) == true -> colorMap[calendarColors?.nextOvulationColorName]
?: colorMap["Magenta"]
val fallbackColors = if (isDarkMode()) {
ColorCombination(DarkGrey, Color.White)
} else {
ColorCombination(Color.Transparent, Black)
}
val dayColors = when {
day.date in state.selectedDays -> calendarColorMap[ColorSetting.SELECTION]
day.date in state.periodDates.keys -> calendarColorMap[ColorSetting.PERIOD]
state.periodPredictionDate?.isEqual(day.date) == true -> calendarColorMap[ColorSetting.EXPECTED_PERIOD]
day.date in state.ovulationDates -> calendarColorMap[ColorSetting.OVULATION]
state.ovulationPredictionDate?.isEqual(day.date) == true -> calendarColorMap[ColorSetting.EXPECTED_OVULATION]
else -> null
} ?: Color.Transparent
} ?: fallbackColors

val border = if (day.date.isEqual(LocalDate.now())) {
BorderStroke(1.dp, Color.DarkGray)
BorderStroke(1.5.dp, Color.DarkGray)
} else null

val fontStyleType = when {
Expand All @@ -335,11 +343,12 @@ fun Day(
val isSelected = day.date in state.selectedDays
val hasSymptomDate = day.date in state.symptomDates

val shape = MaterialTheme.shapes.small
Surface(
modifier = Modifier
.aspectRatio(1f) // This ensures the cells remain square.
.padding(2.dp)
.clip(MaterialTheme.shapes.small)
.clip(shape)
.clickable {
val newSelectedDates = if (isSelected) {
state.selectedDays - day.date
Expand All @@ -348,15 +357,15 @@ fun Day(
}.toPersistentSet()
onAction(UiAction.SelectDays(newSelectedDates))
},
shape = MaterialTheme.shapes.small,
color = backgroundColor,
shape = shape,
color = dayColors.backgroundColor,
border = border
) {
Box(contentAlignment = Alignment.Center) {
Text(
text = day.date.dayOfMonth.toString(),
fontWeight = fontStyleType,
color = if (isSelected) MaterialTheme.colorScheme.onPrimary else MaterialTheme.colorScheme.onSurface
color = dayColors.textColor
)

// Add symptom circles
Expand All @@ -383,19 +392,15 @@ fun Day(
if (state.showCycleNumbers) {
val cycleNumber = calculateCycleNumber(day.date, dbHelper)
if (cycleNumber > 0) {
Box(
modifier = Modifier
.size(18.dp)
.background(Color.Transparent)
.align(Alignment.TopStart)
Surface(
shape = shape,
color = Color.Transparent,
modifier = Modifier.align(Alignment.TopStart)
) {
Text(
text = cycleNumber.toString(),
style = TextStyle(
fontSize = 8.sp,
textAlign = TextAlign.Left
),
modifier = Modifier.padding(4.dp)
fontSize = 8.sp,
modifier = Modifier.padding(horizontal = 4.dp)
)
}
}
Expand Down
42 changes: 32 additions & 10 deletions app/src/main/java/com/mensinator/app/calendar/CalendarViewModel.kt
Original file line number Diff line number Diff line change
Expand Up @@ -8,11 +8,16 @@ import com.mensinator.app.business.IOvulationPrediction
import com.mensinator.app.business.IPeriodDatabaseHelper
import com.mensinator.app.business.IPeriodPrediction
import com.mensinator.app.business.PeriodId
import com.mensinator.app.data.ColorSource
import com.mensinator.app.data.Symptom
import com.mensinator.app.data.isActive
import com.mensinator.app.extensions.pickBestContrastTextColorForThisBackground
import com.mensinator.app.settings.ColorSetting
import com.mensinator.app.settings.IntSetting
import com.mensinator.app.settings.StringSetting
import com.mensinator.app.ui.ResourceMapper
import com.mensinator.app.ui.theme.Black
import com.mensinator.app.ui.theme.DarkGrey
import kotlinx.collections.immutable.*
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.flow.MutableStateFlow
Expand All @@ -29,6 +34,7 @@ class CalendarViewModel(
private val periodPrediction: IPeriodPrediction,
private val ovulationPrediction: IOvulationPrediction,
) : ViewModel() {

private val _viewState = MutableStateFlow(
ViewState()
)
Expand Down Expand Up @@ -65,12 +71,20 @@ class CalendarViewModel(
activeSymptoms = dbHelper.getAllSymptoms().filter { it.isActive }
.toPersistentSet(),
periodMessageText = periodMessageText,
calendarColors = getCalendarColors()
)
}
}
}

fun updateDarkModeStatus(isDarkMode: Boolean) {
_viewState.update {
it.copy(
isDarkMode = isDarkMode,
calendarColorMap = getCalendarColorMap(isDarkMode)
)
}
}

fun onAction(uiAction: UiAction): Unit = when (uiAction) {
is UiAction.UpdateFocusedYearMonth -> {
_viewState.update {
Expand Down Expand Up @@ -121,17 +135,25 @@ class CalendarViewModel(
}
}

private fun getCalendarColors(): CalendarColors {
return CalendarColors(
periodColorName = dbHelper.getSettingByKey("period_color")?.value,
selectedColorName = dbHelper.getSettingByKey("selection_color")?.value,
nextPeriodColorName = dbHelper.getSettingByKey("expected_period_color")?.value,
ovulationColorName = dbHelper.getSettingByKey("ovulation_color")?.value,
nextOvulationColorName = dbHelper.getSettingByKey("expected_ovulation_color")?.value,
)
private fun getCalendarColorMap(isDarkMode: Boolean): Map<ColorSetting, ColorCombination> {
return ColorSetting.entries.associate {
val backgroundColor = ColorSource.getColor(
isDarkMode,
dbHelper.getSettingByKey(it.settingDbKey)?.value ?: "LightGray"
)
val textColor = backgroundColor.pickBestContrastTextColorForThisBackground(
isDarkMode,
DarkGrey,
Black
)

it to ColorCombination(backgroundColor, textColor)
}
}

data class ViewState(
val isDarkMode: Boolean = false,

val showCycleNumbers: Boolean = false,
val focusedYearMonth: YearMonth = YearMonth.now(),
val periodPredictionDate: LocalDate? = null,
Expand All @@ -144,7 +166,7 @@ class CalendarViewModel(
val periodMessageText: String? = null,
val selectedDays: PersistentSet<LocalDate> = persistentSetOf(),
val activeSymptomIdsForLatestSelectedDay: PersistentSet<Int> = persistentSetOf(),
val calendarColors: CalendarColors? = null
val calendarColorMap: Map<ColorSetting, ColorCombination> = mapOf()
)

sealed class UiAction {
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
package com.mensinator.app.calendar

import androidx.compose.ui.graphics.Color

data class ColorCombination(
val backgroundColor: Color,
val textColor: Color,
)
32 changes: 32 additions & 0 deletions app/src/main/java/com/mensinator/app/extensions/ColorExtensions.kt
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
package com.mensinator.app.extensions

import androidx.compose.ui.graphics.Color
import androidx.compose.ui.graphics.luminance

// Pick the text color with the best contrast against this background color.
fun Color.pickBestContrastTextColorForThisBackground(
isDarkMode: Boolean,
textColor1: Color,
textColor2: Color
): Color {
fun calculateContrastRatio(color1: Color, color2: Color): Double {
val lum1 = color1.luminance()
val lum2 = color2.luminance()

val lighter = maxOf(lum1, lum2)
val darker = minOf(lum1, lum2)

return (lighter + 0.05) / (darker + 0.05) // +0.05 to avoid null division
}

val safeBackground = when {
Color.Transparent == this && isDarkMode -> Color.Black
Color.Transparent == this -> Color.White
else -> this
}

val contrast1 = calculateContrastRatio(safeBackground, textColor1)
val contrast2 = calculateContrastRatio(safeBackground, textColor2)

return if (contrast1 >= contrast2) textColor1 else textColor2
}
Original file line number Diff line number Diff line change
Expand Up @@ -7,9 +7,9 @@ import android.util.Log
import android.widget.Toast
import androidx.compose.ui.graphics.Color
import androidx.lifecycle.ViewModel
import com.mensinator.app.R
import com.mensinator.app.business.IExportImport
import com.mensinator.app.business.IPeriodDatabaseHelper
import com.mensinator.app.R
import com.mensinator.app.data.ColorSource
import com.mensinator.app.settings.ColorSetting.*
import kotlinx.coroutines.flow.MutableStateFlow
Expand All @@ -30,7 +30,6 @@ class SettingsViewModel(
periodColor = Color.Yellow,
selectionColor = Color.Yellow,
expectedPeriodColor = Color.Yellow,
periodSelectionColor = Color.Yellow,
ovulationColor = Color.Yellow,
expectedOvulationColor = Color.Yellow,
openColorPickerForSetting = null,
Expand Down Expand Up @@ -64,7 +63,6 @@ class SettingsViewModel(
val periodColor: Color,
val selectionColor: Color,
val expectedPeriodColor: Color,
val periodSelectionColor: Color,
val ovulationColor: Color,
val expectedOvulationColor: Color,
val openColorPickerForSetting: ColorSetting? = null,
Expand Down Expand Up @@ -106,7 +104,6 @@ class SettingsViewModel(
periodColor = getColor(isDarkMode, PERIOD.settingDbKey),
selectionColor = getColor(isDarkMode, SELECTION.settingDbKey),
expectedPeriodColor = getColor(isDarkMode, EXPECTED_PERIOD.settingDbKey),
periodSelectionColor = getColor(isDarkMode, PERIOD_SELECTION.settingDbKey),
ovulationColor = getColor(isDarkMode, OVULATION.settingDbKey),
expectedOvulationColor = getColor(isDarkMode, EXPECTED_OVULATION.settingDbKey),

Expand Down Expand Up @@ -238,10 +235,9 @@ class SettingsViewModel(
}

enum class ColorSetting(val stringResId: Int, val settingDbKey: String) {
PERIOD(R.string.period_color, "period_color"),
SELECTION(R.string.selection_color, "selection_color"),
PERIOD(R.string.period_color, "period_color"),
EXPECTED_PERIOD(R.string.expected_period_color, "expected_period_color"),
PERIOD_SELECTION(R.string.period_selection_color, "period_selection_color"),
OVULATION(R.string.ovulation_color, "ovulation_color"),
EXPECTED_OVULATION(R.string.expected_ovulation_color, "expected_ovulation_color"),
}
Expand Down
5 changes: 3 additions & 2 deletions app/src/main/java/com/mensinator/app/ui/theme/Color.kt
Original file line number Diff line number Diff line change
Expand Up @@ -17,5 +17,6 @@ val PurpleGrey40 = Color(0xFF625B71)
val Pink40 = Color(0xFF7D5260)


val Black = Color(0xFF000000)
val White = Color(0xFFC0C0C0)
val Black = Color.Black
val DarkGrey = Color(29, 27, 32)
val White = Color.White

0 comments on commit c9c61e4

Please sign in to comment.