feat: scrolling month calendar, green theme fixes, long-press create, event colors
- Month view rewritten as a continuous vertical scroll (iOS-style): no prev/next buttons, compact fixed-height week rows, month label on the 1st, top-bar title follows the visible month, Today scrolls back to today - Long-press a day opens the event editor pre-filled with that date - Theme: green container/tint colors so the FAB and accents are no longer the Material default purple - Event colors: parse #RGB shorthand, and fall back to a stable per-calendar palette colour instead of a constant blue when the server omits a colour - Top bar hides prev/next arrows in month view (scroll instead) Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
@@ -23,10 +23,26 @@ data class CalEvent(
|
||||
val calendarColor: String,
|
||||
val source: String,
|
||||
) {
|
||||
/** Per-event override colour, falling back to the calendar's colour. */
|
||||
val effectiveColor: String get() = color?.takeIf { it.isNotBlank() } ?: calendarColor
|
||||
/**
|
||||
* Per-event override colour, then the calendar's colour, then a stable
|
||||
* per-calendar palette colour (so events never collapse to one default).
|
||||
*/
|
||||
val effectiveColor: String
|
||||
get() = color?.takeIf { it.isNotBlank() }
|
||||
?: calendarColor.takeIf { it.isNotBlank() }
|
||||
?: fallbackColorFor("$source:$calendarId")
|
||||
|
||||
companion object {
|
||||
private val FALLBACK_PALETTE = listOf(
|
||||
"#34a853", "#4285f4", "#ea4335", "#fbbc05",
|
||||
"#46bdc6", "#9c27b0", "#ff7043", "#7090c0",
|
||||
)
|
||||
|
||||
private fun fallbackColorFor(key: String): String {
|
||||
val idx = (key.hashCode().and(Int.MAX_VALUE)) % FALLBACK_PALETTE.size
|
||||
return FALLBACK_PALETTE[idx]
|
||||
}
|
||||
|
||||
/** Parse one event object from the `/api/caldav/events` aggregate response. */
|
||||
fun fromJson(json: JSONObject): CalEvent? {
|
||||
val title = json.optString("title").takeIf { json.has("title") } ?: return null
|
||||
@@ -63,7 +79,7 @@ data class CalEvent(
|
||||
color = colorRaw.takeIf { it.isNotBlank() },
|
||||
calendarId = calendarId,
|
||||
calendarName = json.optString("calendar_name", ""),
|
||||
calendarColor = json.optString("calendarColor", "#4285f4"),
|
||||
calendarColor = json.optString("calendarColor", ""),
|
||||
source = json.optString("source", "local"),
|
||||
)
|
||||
}
|
||||
|
||||
@@ -25,9 +25,11 @@ import androidx.compose.material3.Scaffold
|
||||
import androidx.compose.material3.Text
|
||||
import androidx.compose.material3.TextButton
|
||||
import androidx.compose.material3.TopAppBar
|
||||
import androidx.compose.foundation.lazy.rememberLazyListState
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.collectAsState
|
||||
import androidx.compose.runtime.getValue
|
||||
import androidx.compose.runtime.mutableIntStateOf
|
||||
import androidx.compose.runtime.mutableStateOf
|
||||
import androidx.compose.runtime.remember
|
||||
import androidx.compose.runtime.setValue
|
||||
@@ -72,15 +74,19 @@ fun CalendarScreen(
|
||||
var editor by remember { mutableStateOf<EditorRequest?>(null) }
|
||||
var overlay by remember { mutableStateOf(Overlay.NONE) }
|
||||
|
||||
// Continuous month scrolling
|
||||
val monthListState = rememberLazyListState()
|
||||
var todaySignal by remember { mutableIntStateOf(0) }
|
||||
var visibleMonth by remember { mutableStateOf(state.currentDate) }
|
||||
val isMonth = state.viewType == CalViewType.MONTH
|
||||
val barTitle = if (isMonth) titleForView(CalViewType.MONTH, visibleMonth, lang)
|
||||
else titleForView(state.viewType, state.currentDate, lang)
|
||||
|
||||
Scaffold(
|
||||
topBar = {
|
||||
TopAppBar(
|
||||
title = {
|
||||
Text(
|
||||
titleForView(state.viewType, state.currentDate, lang),
|
||||
maxLines = 1,
|
||||
overflow = TextOverflow.Ellipsis,
|
||||
)
|
||||
Text(barTitle, maxLines = 1, overflow = TextOverflow.Ellipsis)
|
||||
},
|
||||
navigationIcon = {
|
||||
IconButton(onClick = { showMenu = true }) {
|
||||
@@ -88,14 +94,18 @@ fun CalendarScreen(
|
||||
}
|
||||
},
|
||||
actions = {
|
||||
IconButton(onClick = vm::navigatePrev) {
|
||||
Icon(Icons.Filled.ChevronLeft, contentDescription = null)
|
||||
if (!isMonth) {
|
||||
IconButton(onClick = vm::navigatePrev) {
|
||||
Icon(Icons.Filled.ChevronLeft, contentDescription = null)
|
||||
}
|
||||
}
|
||||
IconButton(onClick = vm::moveToToday) {
|
||||
IconButton(onClick = { if (isMonth) todaySignal++ else vm.moveToToday() }) {
|
||||
Icon(Icons.Filled.Today, contentDescription = tr("nav.today"))
|
||||
}
|
||||
IconButton(onClick = vm::navigateNext) {
|
||||
Icon(Icons.Filled.ChevronRight, contentDescription = null)
|
||||
if (!isMonth) {
|
||||
IconButton(onClick = vm::navigateNext) {
|
||||
Icon(Icons.Filled.ChevronRight, contentDescription = null)
|
||||
}
|
||||
}
|
||||
IconButton(onClick = { showFilter = true }) {
|
||||
Icon(Icons.Filled.FilterList, contentDescription = tr("filter.button"))
|
||||
@@ -121,7 +131,11 @@ fun CalendarScreen(
|
||||
)
|
||||
},
|
||||
floatingActionButton = {
|
||||
FloatingActionButton(onClick = { editor = EditorRequest(null, state.currentDate) }) {
|
||||
FloatingActionButton(
|
||||
onClick = { editor = EditorRequest(null, if (isMonth) visibleMonth else state.currentDate) },
|
||||
containerColor = MaterialTheme.colorScheme.primary,
|
||||
contentColor = MaterialTheme.colorScheme.onPrimary,
|
||||
) {
|
||||
Icon(Icons.Filled.Add, contentDescription = tr("cal.new_event"))
|
||||
}
|
||||
},
|
||||
@@ -137,9 +151,12 @@ fun CalendarScreen(
|
||||
CalendarBody(
|
||||
state = state,
|
||||
vm = vm,
|
||||
monthListState = monthListState,
|
||||
scrollToTodaySignal = todaySignal,
|
||||
onVisibleMonthChange = { visibleMonth = it },
|
||||
onEventClick = { detailEvent = it },
|
||||
onDayClick = { date -> vm.goToDate(date, CalViewType.DAY) },
|
||||
onEmptySlotClick = { date -> editor = EditorRequest(null, date) },
|
||||
onDayLongPress = { date -> editor = EditorRequest(null, date) },
|
||||
)
|
||||
}
|
||||
}
|
||||
@@ -215,12 +232,24 @@ fun CalendarScreen(
|
||||
private fun CalendarBody(
|
||||
state: CalendarUiState,
|
||||
vm: CalendarViewModel,
|
||||
monthListState: androidx.compose.foundation.lazy.LazyListState,
|
||||
scrollToTodaySignal: Int,
|
||||
onVisibleMonthChange: (LocalDate) -> Unit,
|
||||
onEventClick: (CalEvent) -> Unit,
|
||||
onDayClick: (LocalDate) -> Unit,
|
||||
onEmptySlotClick: (LocalDate) -> Unit,
|
||||
onDayLongPress: (LocalDate) -> Unit,
|
||||
) {
|
||||
when (state.viewType) {
|
||||
CalViewType.MONTH -> MonthView(state, vm, onDayClick, onEventClick)
|
||||
CalViewType.MONTH -> MonthView(
|
||||
state = state,
|
||||
vm = vm,
|
||||
listState = monthListState,
|
||||
scrollToTodaySignal = scrollToTodaySignal,
|
||||
onVisibleMonthChange = onVisibleMonthChange,
|
||||
onDayClick = onDayClick,
|
||||
onDayLongPress = onDayLongPress,
|
||||
onEventClick = onEventClick,
|
||||
)
|
||||
CalViewType.WEEK -> WeekView(state, vm, onEventClick)
|
||||
CalViewType.DAY -> DayView(state, vm, onEventClick)
|
||||
CalViewType.QUARTER -> QuarterView(state, onDayClick)
|
||||
|
||||
@@ -159,6 +159,21 @@ class CalendarViewModel @Inject constructor(
|
||||
}
|
||||
}
|
||||
|
||||
/** On-demand load for a month the user scrolled to in the continuous calendar. */
|
||||
fun ensureMonthLoaded(monthAnchor: LocalDate) {
|
||||
val first = monthAnchor.withDayOfMonth(1)
|
||||
val start = instant(first.minusMonths(1))
|
||||
val end = instant(first.plusMonths(2))
|
||||
if (isCached(start, end)) return
|
||||
viewModelScope.launch {
|
||||
_state.update { it.copy(isLoading = true) }
|
||||
runCatching { repository.fetchEvents(start, end) }
|
||||
.onSuccess { mergeIntoCache(it, start, end); refreshFromCache() }
|
||||
.onFailure { e -> _state.update { it.copy(error = e.message) } }
|
||||
_state.update { it.copy(isLoading = false) }
|
||||
}
|
||||
}
|
||||
|
||||
private fun prefetchBackground() {
|
||||
val months = settingsStore.cacheMonths
|
||||
val today = LocalDate.now().withDayOfMonth(1)
|
||||
|
||||
@@ -1,7 +1,9 @@
|
||||
package com.scarriffle.calendarr.ui.calendar
|
||||
|
||||
import androidx.compose.foundation.ExperimentalFoundationApi
|
||||
import androidx.compose.foundation.background
|
||||
import androidx.compose.foundation.clickable
|
||||
import androidx.compose.foundation.combinedClickable
|
||||
import androidx.compose.foundation.layout.Box
|
||||
import androidx.compose.foundation.layout.Column
|
||||
import androidx.compose.foundation.layout.Row
|
||||
@@ -10,15 +12,23 @@ import androidx.compose.foundation.layout.fillMaxWidth
|
||||
import androidx.compose.foundation.layout.height
|
||||
import androidx.compose.foundation.layout.padding
|
||||
import androidx.compose.foundation.layout.width
|
||||
import androidx.compose.foundation.lazy.LazyColumn
|
||||
import androidx.compose.foundation.lazy.LazyListState
|
||||
import androidx.compose.foundation.lazy.items
|
||||
import androidx.compose.foundation.shape.RoundedCornerShape
|
||||
import androidx.compose.material3.Divider
|
||||
import androidx.compose.material3.MaterialTheme
|
||||
import androidx.compose.material3.Surface
|
||||
import androidx.compose.material3.Text
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.LaunchedEffect
|
||||
import androidx.compose.runtime.remember
|
||||
import androidx.compose.runtime.snapshotFlow
|
||||
import androidx.compose.ui.Alignment
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.draw.clip
|
||||
import androidx.compose.ui.graphics.Color
|
||||
import androidx.compose.ui.text.font.FontWeight
|
||||
import androidx.compose.ui.text.style.TextAlign
|
||||
import androidx.compose.ui.text.style.TextOverflow
|
||||
import androidx.compose.ui.unit.dp
|
||||
import androidx.compose.ui.unit.sp
|
||||
@@ -28,111 +38,183 @@ import com.scarriffle.calendarr.ui.LocalLang
|
||||
import com.scarriffle.calendarr.util.colorFromHex
|
||||
import com.scarriffle.calendarr.util.contrastingTextColor
|
||||
import java.time.LocalDate
|
||||
import java.time.temporal.IsoFields
|
||||
import java.time.format.TextStyle
|
||||
import java.time.temporal.ChronoUnit
|
||||
|
||||
private const val MONTHS_BACK = 18L
|
||||
private const val MONTHS_AHEAD = 18L
|
||||
|
||||
/**
|
||||
* Continuous, vertically scrolling month calendar (matches the iOS app).
|
||||
* There are no prev/next buttons — the user scrolls through weeks and the
|
||||
* top-bar title follows the currently visible month.
|
||||
*/
|
||||
@Composable
|
||||
fun MonthView(
|
||||
state: CalendarUiState,
|
||||
vm: CalendarViewModel,
|
||||
listState: LazyListState,
|
||||
scrollToTodaySignal: Int,
|
||||
onVisibleMonthChange: (LocalDate) -> Unit,
|
||||
onDayClick: (LocalDate) -> Unit,
|
||||
onDayLongPress: (LocalDate) -> Unit,
|
||||
onEventClick: (CalEvent) -> Unit,
|
||||
) {
|
||||
val lang = LocalLang.current
|
||||
val mondayFirst = state.weekStartsOnMonday
|
||||
val today = LocalDate.now()
|
||||
val firstOfMonth = state.currentDate.withDayOfMonth(1)
|
||||
val firstVisible = startOfWeekFor(firstOfMonth, mondayFirst)
|
||||
val weeks = (0 until 6).map { w -> (0 until 7).map { d -> firstVisible.plusDays((w * 7 + d).toLong()) } }
|
||||
|
||||
val firstVisible = remember(mondayFirst) {
|
||||
startOfWeek(today.withDayOfMonth(1).minusMonths(MONTHS_BACK), mondayFirst)
|
||||
}
|
||||
val end = remember(mondayFirst) {
|
||||
startOfWeek(today.withDayOfMonth(1).plusMonths(MONTHS_AHEAD), mondayFirst)
|
||||
}
|
||||
val weekCount = remember(firstVisible, end) {
|
||||
(ChronoUnit.WEEKS.between(firstVisible, end).toInt() + 1).coerceAtLeast(1)
|
||||
}
|
||||
val todayIndex = remember(firstVisible) {
|
||||
ChronoUnit.WEEKS.between(firstVisible, startOfWeek(today, mondayFirst)).toInt()
|
||||
}
|
||||
|
||||
// Initial scroll to today's week.
|
||||
LaunchedEffect(Unit) {
|
||||
listState.scrollToItem((todayIndex - 1).coerceAtLeast(0))
|
||||
}
|
||||
// "Today" button.
|
||||
LaunchedEffect(scrollToTodaySignal) {
|
||||
if (scrollToTodaySignal > 0) listState.animateScrollToItem((todayIndex - 1).coerceAtLeast(0))
|
||||
}
|
||||
// Track visible month + trigger on-demand loads.
|
||||
LaunchedEffect(listState, weekCount) {
|
||||
snapshotFlow { listState.firstVisibleItemIndex }.collect { idx ->
|
||||
val weekStart = firstVisible.plusWeeks(idx.toLong())
|
||||
val month = weekStart.plusDays(3) // mid-week → representative month
|
||||
onVisibleMonthChange(month)
|
||||
vm.ensureMonthLoaded(month)
|
||||
}
|
||||
}
|
||||
|
||||
Column(Modifier.fillMaxSize()) {
|
||||
// Header: CW + weekdays
|
||||
Row(Modifier.fillMaxWidth().padding(vertical = 4.dp)) {
|
||||
Box(Modifier.width(28.dp))
|
||||
// Fixed weekday header
|
||||
Row(Modifier.fillMaxWidth().padding(vertical = 2.dp)) {
|
||||
weekdayLabels(mondayFirst, lang).forEach { label ->
|
||||
Text(
|
||||
label,
|
||||
modifier = Modifier.weight(1f),
|
||||
textAlign = androidx.compose.ui.text.style.TextAlign.Center,
|
||||
textAlign = TextAlign.Center,
|
||||
style = MaterialTheme.typography.labelSmall,
|
||||
color = MaterialTheme.colorScheme.onSurfaceVariant,
|
||||
)
|
||||
}
|
||||
}
|
||||
weeks.forEach { week ->
|
||||
Row(Modifier.fillMaxWidth().weight(1f)) {
|
||||
val cw = week.first().get(IsoFields.WEEK_OF_WEEK_BASED_YEAR)
|
||||
Box(Modifier.width(28.dp).fillMaxSize(), contentAlignment = Alignment.TopCenter) {
|
||||
Text(
|
||||
"$cw",
|
||||
style = MaterialTheme.typography.labelSmall,
|
||||
color = MaterialTheme.colorScheme.outline,
|
||||
fontSize = 9.sp,
|
||||
modifier = Modifier.padding(top = 4.dp),
|
||||
)
|
||||
}
|
||||
week.forEach { day ->
|
||||
DayCell(
|
||||
day = day,
|
||||
inMonth = day.month == firstOfMonth.month,
|
||||
isToday = day == today,
|
||||
events = vm.eventsOn(day, state.events),
|
||||
onClick = { onDayClick(day) },
|
||||
onEventClick = onEventClick,
|
||||
modifier = Modifier.weight(1f).fillMaxSize(),
|
||||
)
|
||||
}
|
||||
Divider(color = MaterialTheme.colorScheme.outline.copy(alpha = 0.4f))
|
||||
|
||||
LazyColumn(state = listState, modifier = Modifier.fillMaxSize()) {
|
||||
items(weekCount) { index ->
|
||||
val weekStart = firstVisible.plusWeeks(index.toLong())
|
||||
WeekRow(
|
||||
weekStart = weekStart,
|
||||
today = today,
|
||||
events = state.events,
|
||||
vm = vm,
|
||||
lang = lang,
|
||||
onDayClick = onDayClick,
|
||||
onDayLongPress = onDayLongPress,
|
||||
onEventClick = onEventClick,
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@OptIn(ExperimentalFoundationApi::class)
|
||||
@Composable
|
||||
private fun WeekRow(
|
||||
weekStart: LocalDate,
|
||||
today: LocalDate,
|
||||
events: List<CalEvent>,
|
||||
vm: CalendarViewModel,
|
||||
lang: String,
|
||||
onDayClick: (LocalDate) -> Unit,
|
||||
onDayLongPress: (LocalDate) -> Unit,
|
||||
onEventClick: (CalEvent) -> Unit,
|
||||
) {
|
||||
val days = (0 until 7).map { weekStart.plusDays(it.toLong()) }
|
||||
Row(
|
||||
Modifier
|
||||
.fillMaxWidth()
|
||||
.height(78.dp),
|
||||
) {
|
||||
days.forEach { day ->
|
||||
DayCell(
|
||||
day = day,
|
||||
isToday = day == today,
|
||||
events = vm.eventsOn(day, events),
|
||||
lang = lang,
|
||||
onClick = { onDayClick(day) },
|
||||
onLongClick = { onDayLongPress(day) },
|
||||
onEventClick = onEventClick,
|
||||
modifier = Modifier.weight(1f).fillMaxSize(),
|
||||
)
|
||||
}
|
||||
}
|
||||
Divider(color = MaterialTheme.colorScheme.outline.copy(alpha = 0.2f))
|
||||
}
|
||||
|
||||
@OptIn(ExperimentalFoundationApi::class)
|
||||
@Composable
|
||||
private fun DayCell(
|
||||
day: LocalDate,
|
||||
inMonth: Boolean,
|
||||
isToday: Boolean,
|
||||
events: List<CalEvent>,
|
||||
lang: String,
|
||||
onClick: () -> Unit,
|
||||
onLongClick: () -> Unit,
|
||||
onEventClick: (CalEvent) -> Unit,
|
||||
modifier: Modifier = Modifier,
|
||||
) {
|
||||
val todayColor = colorFromHex(LocalAppSettings.current.todayColor)
|
||||
val settings = LocalAppSettings.current
|
||||
val todayColor = colorFromHex(settings.todayColor)
|
||||
val monthLabelColor = colorFromHex(settings.monthLabelColor, MaterialTheme.colorScheme.onSurfaceVariant)
|
||||
val isFirst = day.dayOfMonth == 1
|
||||
|
||||
Column(
|
||||
modifier = modifier
|
||||
.padding(1.dp)
|
||||
.clickable(onClick = onClick),
|
||||
.combinedClickable(onClick = onClick, onLongClick = onLongClick)
|
||||
.padding(horizontal = 1.dp, vertical = 2.dp),
|
||||
horizontalAlignment = Alignment.CenterHorizontally,
|
||||
) {
|
||||
Box(contentAlignment = Alignment.Center, modifier = Modifier.padding(top = 2.dp)) {
|
||||
if (isToday) {
|
||||
Box(
|
||||
Modifier
|
||||
.height(22.dp).width(22.dp)
|
||||
.clip(RoundedCornerShape(11.dp))
|
||||
.background(todayColor),
|
||||
Row(verticalAlignment = Alignment.CenterVertically) {
|
||||
if (isFirst) {
|
||||
Text(
|
||||
day.month.getDisplayName(TextStyle.SHORT, com.scarriffle.calendarr.ui.L10n.locale(lang)),
|
||||
fontSize = 8.sp,
|
||||
color = monthLabelColor,
|
||||
modifier = Modifier.padding(end = 2.dp),
|
||||
)
|
||||
}
|
||||
Box(contentAlignment = Alignment.Center) {
|
||||
if (isToday) {
|
||||
Box(
|
||||
Modifier
|
||||
.height(18.dp)
|
||||
.width(18.dp)
|
||||
.clip(RoundedCornerShape(9.dp))
|
||||
.background(todayColor),
|
||||
)
|
||||
}
|
||||
Text(
|
||||
"${day.dayOfMonth}",
|
||||
fontSize = 11.sp,
|
||||
fontWeight = if (isToday) FontWeight.Bold else FontWeight.Normal,
|
||||
color = if (isToday) todayColor.contrastingTextColor() else MaterialTheme.colorScheme.onBackground,
|
||||
)
|
||||
}
|
||||
Text(
|
||||
"${day.dayOfMonth}",
|
||||
style = MaterialTheme.typography.labelMedium,
|
||||
fontWeight = if (isToday) FontWeight.Bold else FontWeight.Normal,
|
||||
color = when {
|
||||
isToday -> todayColor.contrastingTextColor()
|
||||
inMonth -> MaterialTheme.colorScheme.onBackground
|
||||
else -> MaterialTheme.colorScheme.outline
|
||||
},
|
||||
)
|
||||
}
|
||||
events.take(3).forEach { ev ->
|
||||
EventChip(ev, onClick = { onEventClick(ev) })
|
||||
}
|
||||
events.take(3).forEach { ev -> EventChip(ev) { onEventClick(ev) } }
|
||||
if (events.size > 3) {
|
||||
Text(
|
||||
"+${events.size - 3}",
|
||||
style = MaterialTheme.typography.labelSmall,
|
||||
fontSize = 8.sp,
|
||||
color = MaterialTheme.colorScheme.onSurfaceVariant,
|
||||
)
|
||||
Text("+${events.size - 3}", fontSize = 8.sp, color = MaterialTheme.colorScheme.onSurfaceVariant)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -140,26 +222,26 @@ private fun DayCell(
|
||||
@Composable
|
||||
private fun EventChip(event: CalEvent, onClick: () -> Unit) {
|
||||
val color = colorFromHex(event.effectiveColor)
|
||||
Box(
|
||||
Modifier
|
||||
Surface(
|
||||
color = color,
|
||||
shape = RoundedCornerShape(3.dp),
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.padding(horizontal = 1.dp, vertical = 1.dp)
|
||||
.clip(RoundedCornerShape(3.dp))
|
||||
.background(color)
|
||||
.clickable(onClick = onClick)
|
||||
.padding(horizontal = 3.dp, vertical = 1.dp),
|
||||
.padding(horizontal = 1.dp, vertical = 0.5.dp)
|
||||
.clickable(onClick = onClick),
|
||||
) {
|
||||
Text(
|
||||
event.title,
|
||||
maxLines = 1,
|
||||
overflow = TextOverflow.Ellipsis,
|
||||
fontSize = 9.sp,
|
||||
fontSize = 8.sp,
|
||||
color = color.contrastingTextColor(),
|
||||
modifier = Modifier.padding(horizontal = 3.dp, vertical = 1.dp),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
private fun startOfWeekFor(date: LocalDate, mondayFirst: Boolean): LocalDate {
|
||||
private fun startOfWeek(date: LocalDate, mondayFirst: Boolean): LocalDate {
|
||||
val dow = date.dayOfWeek.value
|
||||
val offset = if (mondayFirst) dow - 1 else dow % 7
|
||||
return date.minusDays(offset.toLong())
|
||||
|
||||
@@ -25,12 +25,22 @@ fun CalendarrTheme(
|
||||
) {
|
||||
val primary = BrandGreen
|
||||
|
||||
val container = Color(0xFF14532D)
|
||||
val onContainer = Color(0xFFB7F0C6)
|
||||
val colors = darkColorScheme(
|
||||
primary = primary,
|
||||
onPrimary = primary.contrastingTextColor(),
|
||||
primaryContainer = container,
|
||||
onPrimaryContainer = onContainer,
|
||||
secondary = primary,
|
||||
onSecondary = primary.contrastingTextColor(),
|
||||
secondaryContainer = container,
|
||||
onSecondaryContainer = onContainer,
|
||||
tertiary = primary,
|
||||
onTertiary = primary.contrastingTextColor(),
|
||||
tertiaryContainer = container,
|
||||
onTertiaryContainer = onContainer,
|
||||
surfaceTint = primary,
|
||||
background = Color(0xFF000000),
|
||||
onBackground = Color(0xFFF2F2F7),
|
||||
surface = Color(0xFF1C1C1E),
|
||||
|
||||
@@ -5,8 +5,15 @@ import androidx.compose.ui.graphics.Color
|
||||
/** Parse a "#RRGGBB" (or "RRGGBB") hex string into a Compose [Color]. */
|
||||
fun colorFromHex(hex: String?, fallback: Color = Color(0xFF4285F4)): Color {
|
||||
if (hex.isNullOrBlank()) return fallback
|
||||
val clean = hex.trim().removePrefix("#")
|
||||
val clean = hex.trim().removePrefix("#").filter { it.isLetterOrDigit() }
|
||||
return when (clean.length) {
|
||||
3 -> runCatching {
|
||||
// #RGB shorthand -> expand each nibble
|
||||
val r = clean[0].digitToInt(16) * 17
|
||||
val g = clean[1].digitToInt(16) * 17
|
||||
val b = clean[2].digitToInt(16) * 17
|
||||
Color(r / 255f, g / 255f, b / 255f, 1f)
|
||||
}.getOrDefault(fallback)
|
||||
6 -> runCatching {
|
||||
val v = clean.toLong(16)
|
||||
Color(
|
||||
|
||||
Reference in New Issue
Block a user