From 608580fc7e25ad8cbfccd4e3b30b344de0128647 Mon Sep 17 00:00:00 2001 From: Guido Schmit Date: Sun, 31 May 2026 13:27:03 +0200 Subject: [PATCH] fix: event colors/null parsing, edit+copy, settings chips, scroll perf, splash MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - CalEvent parsing: handle Android's JSONObject.optString JSON-null quirk that returned the string "null" — this made every event blue (color "null" → fallback) and showed "null" for empty location/description. Now null-safe. - Event detail: copy-to-calendar action; calendar row uses a calendar icon (was identical to the notes icon); empty location/notes rows hidden - Event editor: copy mode (prefill from an existing event, save as new) - Settings: hour height, text/line contrast are now button chips (like the cache range); color picker wraps, shows the current colour and a check mark - Month scroll perf: precompute a day→events index once per change and only reload when the visible month actually changes (was filtering every cell every frame → laggy) - Splash: dark window/splash background + inset launcher icon (was an ugly white box on startup) Co-Authored-By: Claude Opus 4.8 --- .../calendarr/domain/model/CalEvent.kt | 32 +++++--- .../java/com/scarriffle/calendarr/ui/L10n.kt | 14 ++++ .../calendarr/ui/calendar/CalendarScreen.kt | 6 +- .../calendarr/ui/calendar/MonthView.kt | 48 ++++++++--- .../calendarr/ui/event/EventDetailSheet.kt | 16 +++- .../calendarr/ui/event/EventEditorSheet.kt | 27 +++--- .../calendarr/ui/settings/SettingsScreen.kt | 82 ++++++++++++------- app/src/main/res/drawable/splash_icon.xml | 5 ++ app/src/main/res/values-v31/themes.xml | 10 +++ app/src/main/res/values/themes.xml | 7 +- 10 files changed, 181 insertions(+), 66 deletions(-) create mode 100644 app/src/main/res/drawable/splash_icon.xml create mode 100644 app/src/main/res/values-v31/themes.xml diff --git a/app/src/main/java/com/scarriffle/calendarr/domain/model/CalEvent.kt b/app/src/main/java/com/scarriffle/calendarr/domain/model/CalEvent.kt index e4eaf34..5a3e0b2 100644 --- a/app/src/main/java/com/scarriffle/calendarr/domain/model/CalEvent.kt +++ b/app/src/main/java/com/scarriffle/calendarr/domain/model/CalEvent.kt @@ -43,11 +43,22 @@ data class CalEvent( return FALLBACK_PALETTE[idx] } + /** + * Read a string field, treating JSON null correctly. Android's + * [JSONObject.optString] returns the literal string "null" for a JSON + * null value, which previously made every event blue (color "null" → + * unparseable → fallback) and showed "null" for empty location/notes. + */ + private fun JSONObject.strOrNull(key: String): String? { + if (!has(key) || isNull(key)) return null + return optString(key, "").takeIf { it.isNotBlank() && it != "null" } + } + /** 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 - val startStr = json.optString("start").takeIf { json.has("start") } ?: return null - val endStr = json.optString("end").takeIf { json.has("end") } ?: return null + val title = json.strOrNull("title") ?: return null + val startStr = json.strOrNull("start") ?: return null + val endStr = json.strOrNull("end") ?: return null // id may be a String (local UUID) or an Int (CalDAV numeric) val id: String = when (val raw = json.opt("id")) { @@ -66,21 +77,20 @@ data class CalEvent( else -> raw.toString() } - val colorRaw = json.optString("color", "") return CalEvent( id = id, - url = json.optString("url", ""), + url = json.strOrNull("url") ?: "", title = title, startDate = start, endDate = end, isAllDay = isAllDay, - location = json.optString("location", ""), - notes = json.optString("description", ""), - color = colorRaw.takeIf { it.isNotBlank() }, + location = json.strOrNull("location") ?: "", + notes = json.strOrNull("description") ?: "", + color = json.strOrNull("color"), calendarId = calendarId, - calendarName = json.optString("calendar_name", ""), - calendarColor = json.optString("calendarColor", ""), - source = json.optString("source", "local"), + calendarName = json.strOrNull("calendar_name") ?: "", + calendarColor = json.strOrNull("calendarColor") ?: "", + source = json.strOrNull("source") ?: "local", ) } } diff --git a/app/src/main/java/com/scarriffle/calendarr/ui/L10n.kt b/app/src/main/java/com/scarriffle/calendarr/ui/L10n.kt index 5170248..7ff2d54 100644 --- a/app/src/main/java/com/scarriffle/calendarr/ui/L10n.kt +++ b/app/src/main/java/com/scarriffle/calendarr/ui/L10n.kt @@ -53,10 +53,16 @@ object L10n { "settings.color.accent" to "Akzentfarbe", "settings.color.today" to "Heutige-Tag-Farbe", "settings.color.divider" to "Monatswechsel-Linie", "settings.color.label" to "Monatskürzel", "settings.textcontrast" to "Schriftkontrast", "settings.linecontrast" to "Linienkontrast", + "settings.contrast.dark" to "Dunkel", "settings.contrast.medium" to "Mittel", + "settings.contrast.bright" to "Hell", "settings.contrast.max" to "Maximum", + "settings.linecontrast.barely" to "Kaum", "settings.linecontrast.subtle" to "Subtil", + "settings.linecontrast.normal" to "Normal", "settings.linecontrast.strong" to "Stark", "settings.calview" to "Kalenderansicht", "settings.defaultview" to "Standardansicht", "settings.firstweekday" to "Erster Wochentag", "settings.monday" to "Montag", "settings.sunday" to "Sonntag", "settings.dimpast" to "Vergangene Termine ausgrauen", "settings.hourheight" to "Stundenhöhe", + "settings.hourheight.compact" to "Kompakt", "settings.hourheight.normal" to "Normal", + "settings.hourheight.comfort" to "Komfort", "settings.hourheight.large" to "Gross", "common.cancel" to "Abbrechen", "common.close" to "Schliessen", "common.ok" to "OK", "common.error" to "Fehler", "common.delete" to "Löschen", "common.save" to "Sichern", "common.retry" to "Erneut versuchen", @@ -88,6 +94,7 @@ object L10n { "event.calendar_picker" to "Kalender", "event.color_section" to "Farbe", "event.color" to "Terminfarbe", "event.reset_color" to "Zurücksetzen", "event.edit_title" to "Termin bearbeiten", "event.new_title" to "Neuer Termin", + "event.copy_title" to "Termin kopieren", "event.copy_to" to "In Kalender kopieren", "event.save" to "Sichern", "event.add" to "Hinzufügen", "event.delete_confirm" to "Diesen Termin löschen?", "accounts.title" to "Konten", "accounts.loading" to "Lade Konten…", @@ -146,10 +153,16 @@ object L10n { "settings.color.accent" to "Accent color", "settings.color.today" to "Today color", "settings.color.divider" to "Month divider line", "settings.color.label" to "Month abbreviation", "settings.textcontrast" to "Text contrast", "settings.linecontrast" to "Line contrast", + "settings.contrast.dark" to "Dark", "settings.contrast.medium" to "Medium", + "settings.contrast.bright" to "Bright", "settings.contrast.max" to "Maximum", + "settings.linecontrast.barely" to "Barely", "settings.linecontrast.subtle" to "Subtle", + "settings.linecontrast.normal" to "Normal", "settings.linecontrast.strong" to "Strong", "settings.calview" to "Calendar view", "settings.defaultview" to "Default view", "settings.firstweekday" to "First day of week", "settings.monday" to "Monday", "settings.sunday" to "Sunday", "settings.dimpast" to "Dim past events", "settings.hourheight" to "Hour height", + "settings.hourheight.compact" to "Compact", "settings.hourheight.normal" to "Normal", + "settings.hourheight.comfort" to "Comfort", "settings.hourheight.large" to "Large", "common.cancel" to "Cancel", "common.close" to "Close", "common.ok" to "OK", "common.error" to "Error", "common.delete" to "Delete", "common.save" to "Save", "common.retry" to "Retry", @@ -181,6 +194,7 @@ object L10n { "event.calendar_picker" to "Calendar", "event.color_section" to "Color", "event.color" to "Event color", "event.reset_color" to "Reset", "event.edit_title" to "Edit event", "event.new_title" to "New event", + "event.copy_title" to "Copy event", "event.copy_to" to "Copy to calendar", "event.save" to "Save", "event.add" to "Add", "event.delete_confirm" to "Delete this event?", "accounts.title" to "Accounts", "accounts.loading" to "Loading accounts…", diff --git a/app/src/main/java/com/scarriffle/calendarr/ui/calendar/CalendarScreen.kt b/app/src/main/java/com/scarriffle/calendarr/ui/calendar/CalendarScreen.kt index ded34dd..df89cab 100644 --- a/app/src/main/java/com/scarriffle/calendarr/ui/calendar/CalendarScreen.kt +++ b/app/src/main/java/com/scarriffle/calendarr/ui/calendar/CalendarScreen.kt @@ -53,7 +53,7 @@ import java.time.LocalDate private enum class Overlay { NONE, PROFILE, SETTINGS, ACCOUNTS } -data class EditorRequest(val existing: CalEvent?, val date: LocalDate) +data class EditorRequest(val existing: CalEvent?, val date: LocalDate, val prefill: CalEvent? = null) @OptIn(ExperimentalMaterial3Api::class) @Composable @@ -197,6 +197,10 @@ fun CalendarScreen( vm.deleteEvent(ev) {} detailEvent = null }, + onCopy = { + detailEvent = null + editor = EditorRequest(existing = null, date = localDate(ev.startDate), prefill = ev) + }, ) } diff --git a/app/src/main/java/com/scarriffle/calendarr/ui/calendar/MonthView.kt b/app/src/main/java/com/scarriffle/calendarr/ui/calendar/MonthView.kt index 04a8a5a..9c7db02 100644 --- a/app/src/main/java/com/scarriffle/calendarr/ui/calendar/MonthView.kt +++ b/app/src/main/java/com/scarriffle/calendarr/ui/calendar/MonthView.kt @@ -40,6 +40,8 @@ import com.scarriffle.calendarr.util.contrastingTextColor import java.time.LocalDate import java.time.format.TextStyle import java.time.temporal.ChronoUnit +import kotlinx.coroutines.flow.distinctUntilChanged +import kotlinx.coroutines.flow.map private const val MONTHS_BACK = 18L private const val MONTHS_AHEAD = 18L @@ -85,16 +87,21 @@ fun MonthView( LaunchedEffect(scrollToTodaySignal) { if (scrollToTodaySignal > 0) listState.animateScrollToItem((todayIndex - 1).coerceAtLeast(0)) } - // Track visible month + trigger on-demand loads. + // Track visible month + trigger on-demand loads (only when the month changes). 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) - } + snapshotFlow { listState.firstVisibleItemIndex } + .map { firstVisible.plusWeeks(it.toLong()).plusDays(3).withDayOfMonth(1) } + .distinctUntilChanged() + .collect { month -> + onVisibleMonthChange(month) + vm.ensureMonthLoaded(month) + } } + // Precompute a day → events index once per event-list change (avoids + // filtering the whole list for every cell on every recomposition → smooth scroll). + val eventsByDay = remember(state.events) { buildEventsByDay(state.events) } + Column(Modifier.fillMaxSize()) { // Fixed weekday header Row(Modifier.fillMaxWidth().padding(vertical = 2.dp)) { @@ -116,8 +123,7 @@ fun MonthView( WeekRow( weekStart = weekStart, today = today, - events = state.events, - vm = vm, + eventsByDay = eventsByDay, lang = lang, onDayClick = onDayClick, onDayLongPress = onDayLongPress, @@ -133,8 +139,7 @@ fun MonthView( private fun WeekRow( weekStart: LocalDate, today: LocalDate, - events: List, - vm: CalendarViewModel, + eventsByDay: Map>, lang: String, onDayClick: (LocalDate) -> Unit, onDayLongPress: (LocalDate) -> Unit, @@ -150,7 +155,7 @@ private fun WeekRow( DayCell( day = day, isToday = day == today, - events = vm.eventsOn(day, events), + events = eventsByDay[day] ?: emptyList(), lang = lang, onClick = { onDayClick(day) }, onLongClick = { onDayLongPress(day) }, @@ -246,3 +251,22 @@ private fun startOfWeek(date: LocalDate, mondayFirst: Boolean): LocalDate { val offset = if (mondayFirst) dow - 1 else dow % 7 return date.minusDays(offset.toLong()) } + +/** Index events by each local day they overlap, for O(1) lookup while scrolling. */ +private fun buildEventsByDay(events: List): Map> { + val map = HashMap>() + for (ev in events) { + val first = localDate(ev.startDate) + // end is exclusive; the last covered day is the instant just before it + val last = localDate(ev.endDate.minusSeconds(1)).coerceAtLeast(first) + var d = first + var guard = 0 + while (!d.isAfter(last) && guard < 400) { + map.getOrPut(d) { mutableListOf() }.add(ev) + d = d.plusDays(1) + guard++ + } + } + map.values.forEach { list -> list.sortBy { it.startDate } } + return map +} diff --git a/app/src/main/java/com/scarriffle/calendarr/ui/event/EventDetailSheet.kt b/app/src/main/java/com/scarriffle/calendarr/ui/event/EventDetailSheet.kt index 0d9532f..a54512d 100644 --- a/app/src/main/java/com/scarriffle/calendarr/ui/event/EventDetailSheet.kt +++ b/app/src/main/java/com/scarriffle/calendarr/ui/event/EventDetailSheet.kt @@ -5,12 +5,16 @@ import androidx.compose.foundation.layout.Arrangement import androidx.compose.foundation.layout.Box import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.Row +import androidx.compose.foundation.layout.ExperimentalLayoutApi +import androidx.compose.foundation.layout.FlowRow import androidx.compose.foundation.layout.Spacer import androidx.compose.foundation.layout.fillMaxWidth import androidx.compose.foundation.layout.padding import androidx.compose.foundation.layout.size import androidx.compose.foundation.shape.CircleShape import androidx.compose.material.icons.Icons +import androidx.compose.material.icons.filled.CalendarMonth +import androidx.compose.material.icons.filled.ContentCopy import androidx.compose.material.icons.filled.Delete import androidx.compose.material.icons.filled.Edit import androidx.compose.material.icons.filled.LocationOn @@ -41,13 +45,14 @@ import com.scarriffle.calendarr.ui.calendar.eventDateRange import com.scarriffle.calendarr.ui.tr import com.scarriffle.calendarr.util.colorFromHex -@OptIn(ExperimentalMaterial3Api::class) +@OptIn(ExperimentalMaterial3Api::class, ExperimentalLayoutApi::class) @Composable fun EventDetailSheet( event: CalEvent, onDismiss: () -> Unit, onEdit: () -> Unit, onDelete: () -> Unit, + onCopy: () -> Unit, ) { val lang = LocalLang.current var confirmDelete by remember { mutableStateOf(false) } @@ -68,7 +73,7 @@ fun EventDetailSheet( Spacer(Modifier.size(16.dp)) DetailRow(Icons.Filled.Schedule, eventDateRange(event, lang)) if (event.calendarName.isNotBlank()) { - DetailRow(Icons.Filled.Notes, event.calendarName) + DetailRow(Icons.Filled.CalendarMonth, event.calendarName) } if (event.location.isNotBlank()) { DetailRow(Icons.Filled.LocationOn, event.location) @@ -77,7 +82,7 @@ fun EventDetailSheet( DetailRow(Icons.Filled.Notes, event.notes) } Spacer(Modifier.size(20.dp)) - Row(horizontalArrangement = Arrangement.spacedBy(12.dp)) { + FlowRow(horizontalArrangement = Arrangement.spacedBy(12.dp)) { if (canEdit) { OutlinedButton(onClick = onEdit) { Icon(Icons.Filled.Edit, contentDescription = null) @@ -85,6 +90,11 @@ fun EventDetailSheet( Text(tr("event.edit_title")) } } + OutlinedButton(onClick = onCopy) { + Icon(Icons.Filled.ContentCopy, contentDescription = null) + Spacer(Modifier.size(6.dp)) + Text(tr("event.copy_title")) + } if (canDelete) { OutlinedButton(onClick = { confirmDelete = true }) { Icon(Icons.Filled.Delete, contentDescription = null, tint = MaterialTheme.colorScheme.error) diff --git a/app/src/main/java/com/scarriffle/calendarr/ui/event/EventEditorSheet.kt b/app/src/main/java/com/scarriffle/calendarr/ui/event/EventEditorSheet.kt index dee00e3..a510d32 100644 --- a/app/src/main/java/com/scarriffle/calendarr/ui/event/EventEditorSheet.kt +++ b/app/src/main/java/com/scarriffle/calendarr/ui/event/EventEditorSheet.kt @@ -75,16 +75,19 @@ fun EventEditorSheet( val context = LocalContext.current val lang = LocalLang.current val existing = request.existing + // Fields are prefilled from the event being edited OR copied. + val template = existing ?: request.prefill + val isCopy = existing == null && request.prefill != null val sheetState = rememberModalBottomSheetState(skipPartiallyExpanded = true) - var title by remember { mutableStateOf(existing?.title ?: "") } - var allDay by remember { mutableStateOf(existing?.isAllDay ?: false) } - var location by remember { mutableStateOf(existing?.location ?: "") } - var description by remember { mutableStateOf(existing?.notes ?: "") } - var color by remember { mutableStateOf(existing?.color) } + var title by remember { mutableStateOf(template?.title ?: "") } + var allDay by remember { mutableStateOf(template?.isAllDay ?: false) } + var location by remember { mutableStateOf(template?.location ?: "") } + var description by remember { mutableStateOf(template?.notes ?: "") } + var color by remember { mutableStateOf(template?.color) } - val initialStart = existing?.startDate - val initialEnd = existing?.endDate + val initialStart = template?.startDate + val initialEnd = template?.endDate var startDate by remember { mutableStateOf(initialStart?.let { LocalDate.ofInstant(it, zone) } ?: request.date) } @@ -95,7 +98,7 @@ fun EventEditorSheet( mutableStateOf( initialEnd?.let { val d = LocalDate.ofInstant(it, zone) - if (existing?.isAllDay == true) d.minusDays(1) else d + if (template?.isAllDay == true) d.minusDays(1) else d } ?: request.date ) } @@ -103,7 +106,7 @@ fun EventEditorSheet( mutableStateOf(initialEnd?.let { LocalTime.ofInstant(it, zone).withSecond(0).withNano(0) } ?: LocalTime.of(10, 0)) } - val preselected = existing?.let { ev -> + val preselected = template?.let { ev -> val id = calendarKey(ev.source, ev.calendarId).substringAfter(":").toIntOrNull() writableCalendars.firstOrNull { it.source == ev.source && it.numericId == id } } @@ -139,7 +142,11 @@ fun EventEditorSheet( .padding(bottom = 32.dp), ) { Text( - if (existing == null) tr("event.new_title") else tr("event.edit_title"), + when { + existing != null -> tr("event.edit_title") + isCopy -> tr("event.copy_title") + else -> tr("event.new_title") + }, style = MaterialTheme.typography.titleLarge, fontWeight = FontWeight.SemiBold, modifier = Modifier.padding(vertical = 8.dp), diff --git a/app/src/main/java/com/scarriffle/calendarr/ui/settings/SettingsScreen.kt b/app/src/main/java/com/scarriffle/calendarr/ui/settings/SettingsScreen.kt index 6478841..46f4dc6 100644 --- a/app/src/main/java/com/scarriffle/calendarr/ui/settings/SettingsScreen.kt +++ b/app/src/main/java/com/scarriffle/calendarr/ui/settings/SettingsScreen.kt @@ -7,6 +7,8 @@ import androidx.compose.foundation.horizontalScroll import androidx.compose.foundation.layout.Arrangement import androidx.compose.foundation.layout.Box import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.ExperimentalLayoutApi +import androidx.compose.foundation.layout.FlowRow import androidx.compose.foundation.layout.Row import androidx.compose.foundation.layout.Spacer import androidx.compose.foundation.layout.fillMaxSize @@ -17,6 +19,7 @@ import androidx.compose.foundation.rememberScrollState import androidx.compose.foundation.shape.CircleShape import androidx.compose.foundation.verticalScroll import androidx.compose.material.icons.Icons +import androidx.compose.material.icons.filled.Check import androidx.compose.material.icons.filled.Close import androidx.compose.material3.Divider import androidx.compose.material3.ExperimentalMaterial3Api @@ -25,7 +28,6 @@ import androidx.compose.material3.Icon import androidx.compose.material3.IconButton import androidx.compose.material3.MaterialTheme import androidx.compose.material3.Scaffold -import androidx.compose.material3.Slider import androidx.compose.material3.Surface import androidx.compose.material3.Switch import androidx.compose.material3.Text @@ -38,7 +40,6 @@ import androidx.compose.runtime.setValue 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.unit.dp import androidx.hilt.navigation.compose.hiltViewModel @@ -47,6 +48,7 @@ import com.scarriffle.calendarr.domain.model.CalViewType import com.scarriffle.calendarr.ui.LocalAppSettings import com.scarriffle.calendarr.ui.tr import com.scarriffle.calendarr.util.colorFromHex +import com.scarriffle.calendarr.util.contrastingTextColor private val PALETTE = listOf( "#4285f4", "#ea4335", "#34a853", "#fbbc05", "#46bdc6", "#9c27b0", "#ff7043", "#7090c0", @@ -120,19 +122,37 @@ fun SettingsScreen( Divider(Modifier.padding(vertical = 16.dp)) Section(tr("settings.hourheight")) - Text("${settings.hourHeight} dp", style = MaterialTheme.typography.bodySmall, color = MaterialTheme.colorScheme.onSurfaceVariant) - Slider( - value = settings.hourHeight.toFloat(), - onValueChange = { settings = settings.copy(hourHeight = it.toInt()) }, - onValueChangeFinished = { update(settings) }, - valueRange = 28f..100f, + ChipRow( + options = listOf( + "28" to tr("settings.hourheight.compact"), + "44" to tr("settings.hourheight.normal"), + "60" to tr("settings.hourheight.comfort"), + "80" to tr("settings.hourheight.large"), + ), + selected = settings.hourHeight.toString(), + onSelect = { update(settings.copy(hourHeight = it.toInt())) }, ) - Spacer(Modifier.size(8.dp)) + Spacer(Modifier.size(16.dp)) Section(tr("settings.textcontrast")) - ContrastStepper(settings.textContrast) { update(settings.copy(textContrast = it)) } + ChipRow( + options = listOf( + "1" to tr("settings.contrast.dark"), "2" to tr("settings.contrast.medium"), + "3" to tr("settings.contrast.bright"), "4" to tr("settings.contrast.max"), + ), + selected = settings.textContrast.toString(), + onSelect = { update(settings.copy(textContrast = it.toInt())) }, + ) + Spacer(Modifier.size(16.dp)) Section(tr("settings.linecontrast")) - ContrastStepper(settings.lineContrast) { update(settings.copy(lineContrast = it)) } + ChipRow( + options = listOf( + "1" to tr("settings.linecontrast.barely"), "2" to tr("settings.linecontrast.subtle"), + "3" to tr("settings.linecontrast.normal"), "4" to tr("settings.linecontrast.strong"), + ), + selected = settings.lineContrast.toString(), + onSelect = { update(settings.copy(lineContrast = it.toInt())) }, + ) Divider(Modifier.padding(vertical = 16.dp)) Section(tr("settings.cache.title")) @@ -162,29 +182,35 @@ private fun ChipRow(options: List>, selected: String, onSel } } +@OptIn(ExperimentalLayoutApi::class) @Composable private fun ColorChooser(label: String, current: String, onPick: (String) -> Unit) { - Column(Modifier.padding(vertical = 6.dp)) { - Text(label, style = MaterialTheme.typography.bodyMedium, color = MaterialTheme.colorScheme.onSurfaceVariant) - Row(Modifier.padding(top = 4.dp), horizontalArrangement = Arrangement.spacedBy(10.dp)) { - PALETTE.forEach { hex -> + // Show the user's current colour even if it isn't one of the presets. + val swatches = remember(current) { + (if (PALETTE.any { it.equals(current, ignoreCase = true) }) PALETTE else listOf(current) + PALETTE) + } + Column(Modifier.fillMaxWidth().padding(vertical = 8.dp)) { + Row(verticalAlignment = Alignment.CenterVertically) { + Box(Modifier.size(18.dp).clip(CircleShape).background(colorFromHex(current))) + Spacer(Modifier.size(8.dp)) + Text(label, style = MaterialTheme.typography.bodyMedium) + } + FlowRow( + Modifier.fillMaxWidth().padding(top = 8.dp), + horizontalArrangement = Arrangement.spacedBy(12.dp), + verticalArrangement = Arrangement.spacedBy(8.dp), + ) { + swatches.forEach { hex -> val selected = hex.equals(current, ignoreCase = true) Box( - Modifier.size(28.dp).clip(CircleShape).background(colorFromHex(hex)) - .then(if (selected) Modifier.border(2.dp, Color.White, CircleShape) else Modifier) + Modifier.size(34.dp).clip(CircleShape).background(colorFromHex(hex)) + .then(if (selected) Modifier.border(3.dp, MaterialTheme.colorScheme.onBackground, CircleShape) else Modifier) .clickable { onPick(hex) }, - ) + contentAlignment = Alignment.Center, + ) { + if (selected) Icon(Icons.Filled.Check, contentDescription = null, tint = colorFromHex(hex).contrastingTextColor(), modifier = Modifier.size(18.dp)) + } } } } } - -@Composable -private fun ContrastStepper(value: Int, onChange: (Int) -> Unit) { - Slider( - value = value.toFloat(), - onValueChange = { onChange(it.toInt().coerceIn(1, 4)) }, - valueRange = 1f..4f, - steps = 2, - ) -} diff --git a/app/src/main/res/drawable/splash_icon.xml b/app/src/main/res/drawable/splash_icon.xml new file mode 100644 index 0000000..a72e494 --- /dev/null +++ b/app/src/main/res/drawable/splash_icon.xml @@ -0,0 +1,5 @@ + + + diff --git a/app/src/main/res/values-v31/themes.xml b/app/src/main/res/values-v31/themes.xml new file mode 100644 index 0000000..70e8a9d --- /dev/null +++ b/app/src/main/res/values-v31/themes.xml @@ -0,0 +1,10 @@ + + + diff --git a/app/src/main/res/values/themes.xml b/app/src/main/res/values/themes.xml index a564b56..7b56210 100644 --- a/app/src/main/res/values/themes.xml +++ b/app/src/main/res/values/themes.xml @@ -1,5 +1,10 @@ + #0B1220 +