feat(colors): optional raw calendar colours + readable title contrast
Add a "Soften calendar colours" setting (Settings → Design, default on). Turning it off paints calendar and event colours raw, exactly as the sync source (DAVx5/CalDAV) publishes them, instead of the theme-fitting pastels (#36). Event/calendar colours now flow through shared eventFill()/eventInk() helpers gated by a LocalSoftenColors composition local (widgets read the pref directly). Event titles pick black or white text by the fill's WCAG relative luminance, so a dark colour stays legible whether softened or raw (#21) — previously always near-black. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
13
CHANGELOG.md
13
CHANGELOG.md
@@ -8,6 +8,17 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
|
|||||||
## [Unreleased]
|
## [Unreleased]
|
||||||
|
|
||||||
### Added
|
### Added
|
||||||
|
- Show raw calendar colours. Calendula normally softens each calendar and event
|
||||||
|
colour toward a theme-fitting pastel so harsh sync colours read well on both
|
||||||
|
light and dark; a new **Soften calendar colours** setting (Settings → Design,
|
||||||
|
on by default) lets you turn that off and paint the exact colours your calendar
|
||||||
|
source publishes — matching DAVx5/CalDAV and other calendar apps. Thanks to
|
||||||
|
@leonp5 for the report ([#36]).
|
||||||
|
- Readable titles on dark event colours. An event bar's title now shows in white
|
||||||
|
on a dark colour and near-black on a light one, chosen automatically from the
|
||||||
|
colour's brightness, so a deep blue or purple event is legible at a glance in
|
||||||
|
the busy Week and Month views instead of dark-on-dark. This applies whether or
|
||||||
|
not colours are softened. Thanks to @ptab for the suggestion ([#21]).
|
||||||
- A custom snooze duration. The **Snooze duration** setting (Settings →
|
- A custom snooze duration. The **Snooze duration** setting (Settings →
|
||||||
Notifications) gains a **Custom…** option next to the minute presets: pick any
|
Notifications) gains a **Custom…** option next to the minute presets: pick any
|
||||||
amount and switch between minutes and hours, so a snoozed reminder comes back
|
amount and switch between minutes and hours, so a snoozed reminder comes back
|
||||||
@@ -971,10 +982,12 @@ automatically, with zero telemetry and no internet permission.
|
|||||||
[#25]: https://codeberg.org/jlmakiola/calendula/issues/25
|
[#25]: https://codeberg.org/jlmakiola/calendula/issues/25
|
||||||
[#27]: https://codeberg.org/jlmakiola/calendula/issues/27
|
[#27]: https://codeberg.org/jlmakiola/calendula/issues/27
|
||||||
[#29]: https://codeberg.org/jlmakiola/calendula/issues/29
|
[#29]: https://codeberg.org/jlmakiola/calendula/issues/29
|
||||||
|
[#21]: https://codeberg.org/jlmakiola/calendula/issues/21
|
||||||
[#30]: https://codeberg.org/jlmakiola/calendula/issues/30
|
[#30]: https://codeberg.org/jlmakiola/calendula/issues/30
|
||||||
[#32]: https://codeberg.org/jlmakiola/calendula/issues/32
|
[#32]: https://codeberg.org/jlmakiola/calendula/issues/32
|
||||||
[#33]: https://codeberg.org/jlmakiola/calendula/issues/33
|
[#33]: https://codeberg.org/jlmakiola/calendula/issues/33
|
||||||
[#34]: https://codeberg.org/jlmakiola/calendula/issues/34
|
[#34]: https://codeberg.org/jlmakiola/calendula/issues/34
|
||||||
|
[#36]: https://codeberg.org/jlmakiola/calendula/issues/36
|
||||||
[#37]: https://codeberg.org/jlmakiola/calendula/issues/37
|
[#37]: https://codeberg.org/jlmakiola/calendula/issues/37
|
||||||
[#39]: https://codeberg.org/jlmakiola/calendula/issues/39
|
[#39]: https://codeberg.org/jlmakiola/calendula/issues/39
|
||||||
[#35]: https://codeberg.org/jlmakiola/calendula/issues/35
|
[#35]: https://codeberg.org/jlmakiola/calendula/issues/35
|
||||||
|
|||||||
@@ -29,6 +29,7 @@ import de.jeanlucmakiola.calendula.domain.EventForm
|
|||||||
import de.jeanlucmakiola.calendula.domain.buildInsertEventForm
|
import de.jeanlucmakiola.calendula.domain.buildInsertEventForm
|
||||||
import de.jeanlucmakiola.calendula.ui.RootScreen
|
import de.jeanlucmakiola.calendula.ui.RootScreen
|
||||||
import de.jeanlucmakiola.calendula.ui.common.LocalShowHourLines
|
import de.jeanlucmakiola.calendula.ui.common.LocalShowHourLines
|
||||||
|
import de.jeanlucmakiola.calendula.ui.common.LocalSoftenColors
|
||||||
import de.jeanlucmakiola.calendula.ui.common.LocalUse24HourFormat
|
import de.jeanlucmakiola.calendula.ui.common.LocalUse24HourFormat
|
||||||
import de.jeanlucmakiola.calendula.ui.WidgetNavRequest
|
import de.jeanlucmakiola.calendula.ui.WidgetNavRequest
|
||||||
import de.jeanlucmakiola.calendula.ui.common.CalendarView
|
import de.jeanlucmakiola.calendula.ui.common.CalendarView
|
||||||
@@ -140,6 +141,7 @@ class MainActivity : AppCompatActivity() {
|
|||||||
CompositionLocalProvider(
|
CompositionLocalProvider(
|
||||||
LocalUse24HourFormat provides use24Hour,
|
LocalUse24HourFormat provides use24Hour,
|
||||||
LocalShowHourLines provides settings.showHourLines,
|
LocalShowHourLines provides settings.showHourLines,
|
||||||
|
LocalSoftenColors provides settings.softenColors,
|
||||||
) {
|
) {
|
||||||
RootScreen(
|
RootScreen(
|
||||||
modifier = Modifier.fillMaxSize(),
|
modifier = Modifier.fillMaxSize(),
|
||||||
|
|||||||
@@ -114,6 +114,21 @@ class SettingsPrefs @Inject constructor(
|
|||||||
store.edit { it[DYNAMIC_COLOR_KEY] = enabled }
|
store.edit { it[DYNAMIC_COLOR_KEY] = enabled }
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Whether raw provider colours are softened toward theme-fitting pastels
|
||||||
|
* before display (issue #36). Defaults to ON — the historical look, which
|
||||||
|
* caps harsh sync colours and pins brightness so entries read on both
|
||||||
|
* themes. Turning it off paints the calendar/event colour exactly as the
|
||||||
|
* sync source (DAVx5/CalDAV) publishes it.
|
||||||
|
*/
|
||||||
|
val softenCalendarColors: Flow<Boolean> = store.data.map { prefs ->
|
||||||
|
prefs[SOFTEN_COLORS_KEY] ?: true
|
||||||
|
}
|
||||||
|
|
||||||
|
suspend fun setSoftenCalendarColors(enabled: Boolean) {
|
||||||
|
store.edit { it[SOFTEN_COLORS_KEY] = enabled }
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Custom-font tokens per Material typeface role (issue #19). Stored as opaque
|
* Custom-font tokens per Material typeface role (issue #19). Stored as opaque
|
||||||
* strings — "system", "custom", or a bundled font's token — resolved to a
|
* strings — "system", "custom", or a bundled font's token — resolved to a
|
||||||
@@ -679,6 +694,7 @@ class SettingsPrefs @Inject constructor(
|
|||||||
companion object {
|
companion object {
|
||||||
internal val THEME_MODE_KEY = stringPreferencesKey("theme_mode")
|
internal val THEME_MODE_KEY = stringPreferencesKey("theme_mode")
|
||||||
internal val DYNAMIC_COLOR_KEY = booleanPreferencesKey("dynamic_color")
|
internal val DYNAMIC_COLOR_KEY = booleanPreferencesKey("dynamic_color")
|
||||||
|
internal val SOFTEN_COLORS_KEY = booleanPreferencesKey("soften_calendar_colors")
|
||||||
internal val BRAND_FONT_KEY = stringPreferencesKey("brand_font")
|
internal val BRAND_FONT_KEY = stringPreferencesKey("brand_font")
|
||||||
internal val PLAIN_FONT_KEY = stringPreferencesKey("plain_font")
|
internal val PLAIN_FONT_KEY = stringPreferencesKey("plain_font")
|
||||||
internal val BRAND_FONT_STAMP_KEY = intPreferencesKey("brand_font_stamp")
|
internal val BRAND_FONT_STAMP_KEY = intPreferencesKey("brand_font_stamp")
|
||||||
|
|||||||
@@ -59,6 +59,8 @@ import de.jeanlucmakiola.calendula.data.prefs.PastEventDisplay
|
|||||||
import de.jeanlucmakiola.calendula.domain.EventInstance
|
import de.jeanlucmakiola.calendula.domain.EventInstance
|
||||||
import de.jeanlucmakiola.calendula.domain.hasEnded
|
import de.jeanlucmakiola.calendula.domain.hasEnded
|
||||||
import de.jeanlucmakiola.calendula.ui.common.AgendaRangePicker
|
import de.jeanlucmakiola.calendula.ui.common.AgendaRangePicker
|
||||||
|
import de.jeanlucmakiola.calendula.ui.common.LocalSoftenColors
|
||||||
|
import de.jeanlucmakiola.calendula.ui.common.eventFill
|
||||||
import de.jeanlucmakiola.calendula.ui.common.agendaRangeLabel
|
import de.jeanlucmakiola.calendula.ui.common.agendaRangeLabel
|
||||||
import de.jeanlucmakiola.floret.identity.animateItemMotion
|
import de.jeanlucmakiola.floret.identity.animateItemMotion
|
||||||
import de.jeanlucmakiola.calendula.ui.common.CalendarDrawer
|
import de.jeanlucmakiola.calendula.ui.common.CalendarDrawer
|
||||||
@@ -72,7 +74,6 @@ import de.jeanlucmakiola.floret.components.GroupedRow
|
|||||||
import de.jeanlucmakiola.floret.components.Position
|
import de.jeanlucmakiola.floret.components.Position
|
||||||
import de.jeanlucmakiola.calendula.ui.common.ViewSwitcherPill
|
import de.jeanlucmakiola.calendula.ui.common.ViewSwitcherPill
|
||||||
import de.jeanlucmakiola.calendula.ui.common.next
|
import de.jeanlucmakiola.calendula.ui.common.next
|
||||||
import de.jeanlucmakiola.floret.components.pastelize
|
|
||||||
import de.jeanlucmakiola.floret.components.positionOf
|
import de.jeanlucmakiola.floret.components.positionOf
|
||||||
import de.jeanlucmakiola.calendula.ui.common.rememberCurrentMinute
|
import de.jeanlucmakiola.calendula.ui.common.rememberCurrentMinute
|
||||||
import de.jeanlucmakiola.calendula.ui.common.currentLocale
|
import de.jeanlucmakiola.calendula.ui.common.currentLocale
|
||||||
@@ -454,6 +455,7 @@ private fun AgendaEventRow(
|
|||||||
onClick: () -> Unit,
|
onClick: () -> Unit,
|
||||||
) {
|
) {
|
||||||
val dark = isSystemInDarkTheme()
|
val dark = isSystemInDarkTheme()
|
||||||
|
val soften = LocalSoftenColors.current
|
||||||
val title = event.title.ifBlank { stringResource(R.string.event_untitled) }
|
val title = event.title.ifBlank { stringResource(R.string.event_untitled) }
|
||||||
GroupedRow(
|
GroupedRow(
|
||||||
modifier = if (dimmed) modifier.alpha(EventDimAlpha) else modifier,
|
modifier = if (dimmed) modifier.alpha(EventDimAlpha) else modifier,
|
||||||
@@ -466,7 +468,7 @@ private fun AgendaEventRow(
|
|||||||
modifier = Modifier
|
modifier = Modifier
|
||||||
.size(width = 6.dp, height = 36.dp)
|
.size(width = 6.dp, height = 36.dp)
|
||||||
.clip(RoundedCornerShape(3.dp))
|
.clip(RoundedCornerShape(3.dp))
|
||||||
.background(pastelize(event.color, dark)),
|
.background(eventFill(event.color, dark, soften)),
|
||||||
)
|
)
|
||||||
},
|
},
|
||||||
onClick = onClick,
|
onClick = onClick,
|
||||||
|
|||||||
@@ -93,6 +93,8 @@ import de.jeanlucmakiola.calendula.data.prefs.SettingsPrefs
|
|||||||
import de.jeanlucmakiola.calendula.domain.CalendarSource
|
import de.jeanlucmakiola.calendula.domain.CalendarSource
|
||||||
import de.jeanlucmakiola.calendula.data.calendar.CalendarColorPalette
|
import de.jeanlucmakiola.calendula.data.calendar.CalendarColorPalette
|
||||||
import de.jeanlucmakiola.calendula.ui.common.CalendarColorChip
|
import de.jeanlucmakiola.calendula.ui.common.CalendarColorChip
|
||||||
|
import de.jeanlucmakiola.calendula.ui.common.LocalSoftenColors
|
||||||
|
import de.jeanlucmakiola.calendula.ui.common.eventFill
|
||||||
import de.jeanlucmakiola.calendula.ui.common.LeadingAvatar
|
import de.jeanlucmakiola.calendula.ui.common.LeadingAvatar
|
||||||
import de.jeanlucmakiola.calendula.ui.common.SourceLogo
|
import de.jeanlucmakiola.calendula.ui.common.SourceLogo
|
||||||
import de.jeanlucmakiola.calendula.ui.common.curatedSourcePackage
|
import de.jeanlucmakiola.calendula.ui.common.curatedSourcePackage
|
||||||
@@ -108,7 +110,6 @@ import de.jeanlucmakiola.calendula.ui.common.ColorSwatchRow
|
|||||||
import de.jeanlucmakiola.floret.components.GroupedRow
|
import de.jeanlucmakiola.floret.components.GroupedRow
|
||||||
import de.jeanlucmakiola.floret.components.InlineTextField
|
import de.jeanlucmakiola.floret.components.InlineTextField
|
||||||
import de.jeanlucmakiola.floret.components.Position
|
import de.jeanlucmakiola.floret.components.Position
|
||||||
import de.jeanlucmakiola.floret.components.pastelize
|
|
||||||
import java.time.LocalDate
|
import java.time.LocalDate
|
||||||
|
|
||||||
/** Sentinel [editorId] meaning "the editor is composing a new calendar". */
|
/** Sentinel [editorId] meaning "the editor is composing a new calendar". */
|
||||||
@@ -565,6 +566,7 @@ private fun CalendarEditor(
|
|||||||
var description by rememberSaveable(sessionKey) { mutableStateOf(initialDescription) }
|
var description by rememberSaveable(sessionKey) { mutableStateOf(initialDescription) }
|
||||||
var confirmDelete by remember { mutableStateOf(false) }
|
var confirmDelete by remember { mutableStateOf(false) }
|
||||||
val dark = isSystemInDarkTheme()
|
val dark = isSystemInDarkTheme()
|
||||||
|
val soften = LocalSoftenColors.current
|
||||||
|
|
||||||
Scaffold(
|
Scaffold(
|
||||||
modifier = Modifier
|
modifier = Modifier
|
||||||
@@ -624,7 +626,7 @@ private fun CalendarEditor(
|
|||||||
.padding(horizontal = 16.dp, vertical = 8.dp),
|
.padding(horizontal = 16.dp, vertical = 8.dp),
|
||||||
verticalArrangement = Arrangement.spacedBy(12.dp),
|
verticalArrangement = Arrangement.spacedBy(12.dp),
|
||||||
) {
|
) {
|
||||||
EditorCard(icon = Icons.Default.CalendarMonth, iconTint = pastelize(color, dark)) {
|
EditorCard(icon = Icons.Default.CalendarMonth, iconTint = eventFill(color, dark, soften)) {
|
||||||
InlineTextField(
|
InlineTextField(
|
||||||
value = name,
|
value = name,
|
||||||
onValueChange = { name = it },
|
onValueChange = { name = it },
|
||||||
|
|||||||
@@ -14,16 +14,17 @@ import androidx.compose.ui.Alignment
|
|||||||
import androidx.compose.ui.Modifier
|
import androidx.compose.ui.Modifier
|
||||||
import androidx.compose.ui.draw.clip
|
import androidx.compose.ui.draw.clip
|
||||||
import androidx.compose.ui.unit.dp
|
import androidx.compose.ui.unit.dp
|
||||||
import de.jeanlucmakiola.floret.components.pastelize
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Leading avatar for a calendar: a neutral chip holding a calendar glyph tinted
|
* Leading avatar for a calendar: a neutral chip holding a calendar glyph tinted
|
||||||
* in the calendar's (pastelised) colour. Shared by the calendar manager and the
|
* in the calendar's colour — softened to a pastel, or raw when the softener is
|
||||||
* visibility filter so they read identically.
|
* off (issue #36). Shared by the calendar manager and the visibility filter so
|
||||||
|
* they read identically.
|
||||||
*/
|
*/
|
||||||
@Composable
|
@Composable
|
||||||
fun CalendarColorChip(color: Int, modifier: Modifier = Modifier) {
|
fun CalendarColorChip(color: Int, modifier: Modifier = Modifier) {
|
||||||
val dark = isSystemInDarkTheme()
|
val dark = isSystemInDarkTheme()
|
||||||
|
val soften = LocalSoftenColors.current
|
||||||
Box(
|
Box(
|
||||||
modifier = modifier
|
modifier = modifier
|
||||||
.size(40.dp)
|
.size(40.dp)
|
||||||
@@ -34,7 +35,7 @@ fun CalendarColorChip(color: Int, modifier: Modifier = Modifier) {
|
|||||||
Icon(
|
Icon(
|
||||||
Icons.Filled.CalendarMonth,
|
Icons.Filled.CalendarMonth,
|
||||||
contentDescription = null,
|
contentDescription = null,
|
||||||
tint = pastelize(color, dark),
|
tint = eventFill(color, dark, soften),
|
||||||
modifier = Modifier.size(22.dp),
|
modifier = Modifier.size(22.dp),
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -18,15 +18,14 @@ import androidx.compose.runtime.Composable
|
|||||||
import androidx.compose.ui.Alignment
|
import androidx.compose.ui.Alignment
|
||||||
import androidx.compose.ui.Modifier
|
import androidx.compose.ui.Modifier
|
||||||
import androidx.compose.ui.draw.clip
|
import androidx.compose.ui.draw.clip
|
||||||
import androidx.compose.ui.graphics.Color
|
|
||||||
import androidx.compose.ui.unit.dp
|
import androidx.compose.ui.unit.dp
|
||||||
import de.jeanlucmakiola.floret.components.pastelize
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* A wrapping row of round colour swatches; the one matching [selected] is
|
* A wrapping row of round colour swatches; the one matching [selected] is
|
||||||
* ringed and checked. Shared by the calendar editor and the event-colour
|
* ringed and checked. Shared by the calendar editor and the event-colour
|
||||||
* picker so both pick a colour the same way. Swatches render through
|
* picker so both pick a colour the same way. Swatches render through
|
||||||
* [pastelize] — the softened colour the app actually paints, not the raw hue.
|
* [eventFill] — the colour the app actually paints (softened, or raw when the
|
||||||
|
* softener is off, issue #36), not necessarily the stored hue.
|
||||||
*/
|
*/
|
||||||
@OptIn(ExperimentalLayoutApi::class)
|
@OptIn(ExperimentalLayoutApi::class)
|
||||||
@Composable
|
@Composable
|
||||||
@@ -37,16 +36,18 @@ fun ColorSwatchRow(
|
|||||||
dark: Boolean,
|
dark: Boolean,
|
||||||
modifier: Modifier = Modifier,
|
modifier: Modifier = Modifier,
|
||||||
) {
|
) {
|
||||||
|
val soften = LocalSoftenColors.current
|
||||||
FlowRow(modifier = modifier, horizontalArrangement = Arrangement.spacedBy(12.dp)) {
|
FlowRow(modifier = modifier, horizontalArrangement = Arrangement.spacedBy(12.dp)) {
|
||||||
colors.forEach { argb ->
|
colors.forEach { argb ->
|
||||||
val isSelected = argb == selected
|
val isSelected = argb == selected
|
||||||
|
val fill = eventFill(argb, dark, soften)
|
||||||
Box(
|
Box(
|
||||||
contentAlignment = Alignment.Center,
|
contentAlignment = Alignment.Center,
|
||||||
modifier = Modifier
|
modifier = Modifier
|
||||||
.padding(vertical = 4.dp)
|
.padding(vertical = 4.dp)
|
||||||
.size(40.dp)
|
.size(40.dp)
|
||||||
.clip(CircleShape)
|
.clip(CircleShape)
|
||||||
.background(pastelize(argb, dark))
|
.background(fill)
|
||||||
.then(
|
.then(
|
||||||
if (isSelected) {
|
if (isSelected) {
|
||||||
Modifier.border(2.dp, MaterialTheme.colorScheme.onSurface, CircleShape)
|
Modifier.border(2.dp, MaterialTheme.colorScheme.onSurface, CircleShape)
|
||||||
@@ -60,7 +61,7 @@ fun ColorSwatchRow(
|
|||||||
Icon(
|
Icon(
|
||||||
Icons.Default.Check,
|
Icons.Default.Check,
|
||||||
contentDescription = null,
|
contentDescription = null,
|
||||||
tint = Color.Black.copy(alpha = 0.7f),
|
tint = eventInk(fill, alpha = 0.7f),
|
||||||
modifier = Modifier.size(20.dp),
|
modifier = Modifier.size(20.dp),
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,46 @@
|
|||||||
|
package de.jeanlucmakiola.calendula.ui.common
|
||||||
|
|
||||||
|
import androidx.compose.runtime.staticCompositionLocalOf
|
||||||
|
import androidx.compose.ui.graphics.Color
|
||||||
|
import androidx.compose.ui.graphics.luminance
|
||||||
|
import de.jeanlucmakiola.floret.components.pastelize
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Whether calendar/event colours are softened toward theme-fitting pastels
|
||||||
|
* before display (issue #36). Provided app-wide from the "soften colours"
|
||||||
|
* setting; the default `true` keeps the historical look. When off, the raw
|
||||||
|
* provider colour is painted verbatim — matching the sync source (DAVx5/CalDAV)
|
||||||
|
* and other calendar apps. Widgets live outside this composition and read the
|
||||||
|
* preference directly, then pass the flag to [eventFill] / [eventInk].
|
||||||
|
*/
|
||||||
|
val LocalSoftenColors = staticCompositionLocalOf { true }
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Display fill for an event chip/bar or a calendar tint: the [pastelize]d colour
|
||||||
|
* when [soften] is on, else the raw provider ARGB verbatim (forced opaque, since
|
||||||
|
* pastelize also returns an opaque colour).
|
||||||
|
*/
|
||||||
|
fun eventFill(rawArgb: Int, dark: Boolean, soften: Boolean): Color =
|
||||||
|
if (soften) pastelize(rawArgb, dark) else Color(rawArgb or 0xFF000000.toInt())
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Contrast ink (title text / glyph) for a filled chip painted with [eventFill]:
|
||||||
|
* white on a dark fill, near-black on a light one (issue #21). Applies to both
|
||||||
|
* softened and raw fills — a saturated hue (deep blue, purple, red) is
|
||||||
|
* perceptually dark even after softening pins its HSV value, so black text on it
|
||||||
|
* reads poorly. The choice is objective, not a tuned threshold: white wins only
|
||||||
|
* when it out-contrasts black against the fill, which (by the WCAG contrast
|
||||||
|
* ratio) is at relative luminance ≈ 0.18 — so mid and light colours keep
|
||||||
|
* near-black text. [alpha] carries the caller's soft emphasis.
|
||||||
|
*/
|
||||||
|
fun eventInk(fill: Color, alpha: Float = 0.8f): Color {
|
||||||
|
val useWhite = fill.luminance() < INK_LUMINANCE_CROSSOVER
|
||||||
|
return (if (useWhite) Color.White else Color.Black).copy(alpha = alpha)
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Relative-luminance crossover where white text starts to out-contrast black.
|
||||||
|
* Solving `contrast(white, L) = contrast(black, L)` on the WCAG ratio gives
|
||||||
|
* `L = sqrt(1.05 * 0.05) - 0.05 ≈ 0.179`.
|
||||||
|
*/
|
||||||
|
private const val INK_LUMINANCE_CROSSOVER = 0.179f
|
||||||
@@ -79,7 +79,9 @@ import de.jeanlucmakiola.calendula.ui.common.calendarSlideTransition
|
|||||||
import de.jeanlucmakiola.calendula.ui.common.rememberCalendarFadeSpec
|
import de.jeanlucmakiola.calendula.ui.common.rememberCalendarFadeSpec
|
||||||
import de.jeanlucmakiola.floret.identity.rememberReduceMotion
|
import de.jeanlucmakiola.floret.identity.rememberReduceMotion
|
||||||
import de.jeanlucmakiola.calendula.ui.common.next
|
import de.jeanlucmakiola.calendula.ui.common.next
|
||||||
import de.jeanlucmakiola.floret.components.pastelize
|
import de.jeanlucmakiola.calendula.ui.common.LocalSoftenColors
|
||||||
|
import de.jeanlucmakiola.calendula.ui.common.eventFill
|
||||||
|
import de.jeanlucmakiola.calendula.ui.common.eventInk
|
||||||
import de.jeanlucmakiola.calendula.ui.common.rememberCalendarSlideSpec
|
import de.jeanlucmakiola.calendula.ui.common.rememberCalendarSlideSpec
|
||||||
import de.jeanlucmakiola.calendula.ui.common.currentLocale
|
import de.jeanlucmakiola.calendula.ui.common.currentLocale
|
||||||
import de.jeanlucmakiola.calendula.ui.common.LocalUse24HourFormat
|
import de.jeanlucmakiola.calendula.ui.common.LocalUse24HourFormat
|
||||||
@@ -452,9 +454,11 @@ private fun AllDayBar(
|
|||||||
modifier: Modifier = Modifier,
|
modifier: Modifier = Modifier,
|
||||||
) {
|
) {
|
||||||
val title = event.title.ifBlank { stringResource(R.string.event_untitled) }
|
val title = event.title.ifBlank { stringResource(R.string.event_untitled) }
|
||||||
|
val soften = LocalSoftenColors.current
|
||||||
|
val fill = eventFill(event.color, dark, soften)
|
||||||
Box(
|
Box(
|
||||||
modifier = modifier
|
modifier = modifier
|
||||||
.background(pastelize(event.color, dark), RoundedCornerShape(4.dp))
|
.background(fill, RoundedCornerShape(4.dp))
|
||||||
.clickable(onClick = onClick)
|
.clickable(onClick = onClick)
|
||||||
.padding(horizontal = 6.dp, vertical = 2.dp)
|
.padding(horizontal = 6.dp, vertical = 2.dp)
|
||||||
.semantics { contentDescription = title },
|
.semantics { contentDescription = title },
|
||||||
@@ -465,7 +469,7 @@ private fun AllDayBar(
|
|||||||
style = MaterialTheme.typography.labelSmall,
|
style = MaterialTheme.typography.labelSmall,
|
||||||
maxLines = 1,
|
maxLines = 1,
|
||||||
overflow = TextOverflow.Ellipsis,
|
overflow = TextOverflow.Ellipsis,
|
||||||
color = Color.Black.copy(alpha = 0.8f),
|
color = eventInk(fill),
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -616,9 +620,11 @@ private fun EventBlock(
|
|||||||
val timeLabel = "${minToHm(block.startMin, use24Hour, locale)}–" +
|
val timeLabel = "${minToHm(block.startMin, use24Hour, locale)}–" +
|
||||||
minToHm(block.endMin, use24Hour, locale)
|
minToHm(block.endMin, use24Hour, locale)
|
||||||
val showTime = block.endMin - block.startMin >= 45
|
val showTime = block.endMin - block.startMin >= 45
|
||||||
|
val soften = LocalSoftenColors.current
|
||||||
|
val fill = eventFill(block.event.color, dark, soften)
|
||||||
Box(
|
Box(
|
||||||
modifier = modifier
|
modifier = modifier
|
||||||
.background(pastelize(block.event.color, dark), RoundedCornerShape(4.dp))
|
.background(fill, RoundedCornerShape(4.dp))
|
||||||
.clickable(onClick = onClick)
|
.clickable(onClick = onClick)
|
||||||
.padding(horizontal = 4.dp, vertical = 2.dp)
|
.padding(horizontal = 4.dp, vertical = 2.dp)
|
||||||
.semantics { contentDescription = "$title, $timeLabel" },
|
.semantics { contentDescription = "$title, $timeLabel" },
|
||||||
@@ -629,7 +635,7 @@ private fun EventBlock(
|
|||||||
style = MaterialTheme.typography.labelMedium,
|
style = MaterialTheme.typography.labelMedium,
|
||||||
maxLines = if (showTime) 1 else 2,
|
maxLines = if (showTime) 1 else 2,
|
||||||
overflow = TextOverflow.Ellipsis,
|
overflow = TextOverflow.Ellipsis,
|
||||||
color = Color.Black.copy(alpha = 0.85f),
|
color = eventInk(fill, alpha = 0.85f),
|
||||||
)
|
)
|
||||||
if (showTime) {
|
if (showTime) {
|
||||||
Text(
|
Text(
|
||||||
@@ -637,7 +643,7 @@ private fun EventBlock(
|
|||||||
style = MaterialTheme.typography.labelSmall,
|
style = MaterialTheme.typography.labelSmall,
|
||||||
maxLines = 1,
|
maxLines = 1,
|
||||||
overflow = TextOverflow.Ellipsis,
|
overflow = TextOverflow.Ellipsis,
|
||||||
color = Color.Black.copy(alpha = 0.6f),
|
color = eventInk(fill, alpha = 0.6f),
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -95,11 +95,12 @@ import de.jeanlucmakiola.calendula.domain.RecurringWriteScope
|
|||||||
import de.jeanlucmakiola.calendula.domain.Reminder
|
import de.jeanlucmakiola.calendula.domain.Reminder
|
||||||
import de.jeanlucmakiola.floret.identity.predictiveBack
|
import de.jeanlucmakiola.floret.identity.predictiveBack
|
||||||
import de.jeanlucmakiola.calendula.ui.common.CalendarFailure
|
import de.jeanlucmakiola.calendula.ui.common.CalendarFailure
|
||||||
|
import de.jeanlucmakiola.calendula.ui.common.LocalSoftenColors
|
||||||
|
import de.jeanlucmakiola.calendula.ui.common.eventFill
|
||||||
import de.jeanlucmakiola.floret.components.OptionCard
|
import de.jeanlucmakiola.floret.components.OptionCard
|
||||||
import de.jeanlucmakiola.calendula.ui.common.currentLocale
|
import de.jeanlucmakiola.calendula.ui.common.currentLocale
|
||||||
import de.jeanlucmakiola.calendula.ui.common.LocalUse24HourFormat
|
import de.jeanlucmakiola.calendula.ui.common.LocalUse24HourFormat
|
||||||
import de.jeanlucmakiola.calendula.ui.common.timeOfDayFormatter
|
import de.jeanlucmakiola.calendula.ui.common.timeOfDayFormatter
|
||||||
import de.jeanlucmakiola.floret.components.pastelize
|
|
||||||
import de.jeanlucmakiola.calendula.ui.common.recurrenceText
|
import de.jeanlucmakiola.calendula.ui.common.recurrenceText
|
||||||
import de.jeanlucmakiola.calendula.ui.common.reminderLeadTimeLabel
|
import de.jeanlucmakiola.calendula.ui.common.reminderLeadTimeLabel
|
||||||
import kotlinx.coroutines.launch
|
import kotlinx.coroutines.launch
|
||||||
@@ -381,7 +382,7 @@ private fun EventDetailContent(state: EventDetailUiState.Success, modifier: Modi
|
|||||||
val instance = detail.instance
|
val instance = detail.instance
|
||||||
val dark = isSystemInDarkTheme()
|
val dark = isSystemInDarkTheme()
|
||||||
val locale = currentDetailLocale()
|
val locale = currentDetailLocale()
|
||||||
val accent = pastelize(instance.color, dark)
|
val accent = eventFill(instance.color, dark, LocalSoftenColors.current)
|
||||||
|
|
||||||
Column(
|
Column(
|
||||||
modifier = modifier
|
modifier = modifier
|
||||||
|
|||||||
@@ -126,6 +126,8 @@ import de.jeanlucmakiola.calendula.data.calendar.CalendarColorPalette
|
|||||||
import de.jeanlucmakiola.calendula.ui.common.CalendarPickerGroups
|
import de.jeanlucmakiola.calendula.ui.common.CalendarPickerGroups
|
||||||
import de.jeanlucmakiola.calendula.ui.common.CalendarDatePickerDialog
|
import de.jeanlucmakiola.calendula.ui.common.CalendarDatePickerDialog
|
||||||
import de.jeanlucmakiola.calendula.ui.common.ColorSwatchRow
|
import de.jeanlucmakiola.calendula.ui.common.ColorSwatchRow
|
||||||
|
import de.jeanlucmakiola.calendula.ui.common.LocalSoftenColors
|
||||||
|
import de.jeanlucmakiola.calendula.ui.common.eventFill
|
||||||
import de.jeanlucmakiola.floret.components.DialogAmountField
|
import de.jeanlucmakiola.floret.components.DialogAmountField
|
||||||
import de.jeanlucmakiola.floret.components.DialogUnitDropdown
|
import de.jeanlucmakiola.floret.components.DialogUnitDropdown
|
||||||
import de.jeanlucmakiola.calendula.ui.common.eventFormFieldIcon
|
import de.jeanlucmakiola.calendula.ui.common.eventFormFieldIcon
|
||||||
@@ -146,7 +148,6 @@ import de.jeanlucmakiola.calendula.ui.common.LocalUse24HourFormat
|
|||||||
import de.jeanlucmakiola.calendula.ui.common.timeOfDayFormatter
|
import de.jeanlucmakiola.calendula.ui.common.timeOfDayFormatter
|
||||||
import de.jeanlucmakiola.calendula.ui.common.reminderLeadTimeLabel
|
import de.jeanlucmakiola.calendula.ui.common.reminderLeadTimeLabel
|
||||||
import de.jeanlucmakiola.calendula.ui.common.reminderUnitLabel
|
import de.jeanlucmakiola.calendula.ui.common.reminderUnitLabel
|
||||||
import de.jeanlucmakiola.floret.components.pastelize
|
|
||||||
import de.jeanlucmakiola.calendula.ui.common.recurrenceText
|
import de.jeanlucmakiola.calendula.ui.common.recurrenceText
|
||||||
import kotlinx.datetime.DayOfWeek
|
import kotlinx.datetime.DayOfWeek
|
||||||
import kotlinx.datetime.LocalDate
|
import kotlinx.datetime.LocalDate
|
||||||
@@ -541,7 +542,8 @@ private fun EventEditContent(
|
|||||||
val selectedCalendar = state.calendars.firstOrNull { it.id == form.calendarId }
|
val selectedCalendar = state.calendars.firstOrNull { it.id == form.calendarId }
|
||||||
// The accent ties the form to the detail screen's design language: the
|
// The accent ties the form to the detail screen's design language: the
|
||||||
// bar under the title takes the target calendar's colour.
|
// bar under the title takes the target calendar's colour.
|
||||||
val accent = selectedCalendar?.let { pastelize(it.color, dark) }
|
val soften = LocalSoftenColors.current
|
||||||
|
val accent = selectedCalendar?.let { eventFill(it.color, dark, soften) }
|
||||||
?: MaterialTheme.colorScheme.primary
|
?: MaterialTheme.colorScheme.primary
|
||||||
val gap = 12.dp
|
val gap = 12.dp
|
||||||
|
|
||||||
@@ -893,7 +895,7 @@ private fun EventEditContent(
|
|||||||
icon = Icons.Default.Palette,
|
icon = Icons.Default.Palette,
|
||||||
iconContentDescription = stringResource(R.string.event_edit_color),
|
iconContentDescription = stringResource(R.string.event_edit_color),
|
||||||
iconTint = if (colorSupported && swatch != null) {
|
iconTint = if (colorSupported && swatch != null) {
|
||||||
pastelize(swatch, dark)
|
eventFill(swatch, dark, soften)
|
||||||
} else {
|
} else {
|
||||||
MaterialTheme.colorScheme.onSurfaceVariant
|
MaterialTheme.colorScheme.onSurfaceVariant
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -71,6 +71,9 @@ import de.jeanlucmakiola.calendula.ui.common.CalendarView
|
|||||||
import de.jeanlucmakiola.calendula.ui.common.IMPLEMENTED_VIEWS
|
import de.jeanlucmakiola.calendula.ui.common.IMPLEMENTED_VIEWS
|
||||||
import de.jeanlucmakiola.calendula.ui.common.EventDimAlpha
|
import de.jeanlucmakiola.calendula.ui.common.EventDimAlpha
|
||||||
import de.jeanlucmakiola.calendula.ui.common.LocalDimCutoff
|
import de.jeanlucmakiola.calendula.ui.common.LocalDimCutoff
|
||||||
|
import de.jeanlucmakiola.calendula.ui.common.LocalSoftenColors
|
||||||
|
import de.jeanlucmakiola.calendula.ui.common.eventFill
|
||||||
|
import de.jeanlucmakiola.calendula.ui.common.eventInk
|
||||||
import de.jeanlucmakiola.calendula.ui.common.rememberCurrentMinute
|
import de.jeanlucmakiola.calendula.ui.common.rememberCurrentMinute
|
||||||
import de.jeanlucmakiola.calendula.ui.common.ViewSwitcherPill
|
import de.jeanlucmakiola.calendula.ui.common.ViewSwitcherPill
|
||||||
import de.jeanlucmakiola.calendula.ui.common.calendarSlideTransition
|
import de.jeanlucmakiola.calendula.ui.common.calendarSlideTransition
|
||||||
@@ -79,7 +82,6 @@ import de.jeanlucmakiola.floret.identity.rememberReduceMotion
|
|||||||
import de.jeanlucmakiola.calendula.ui.common.currentLocale
|
import de.jeanlucmakiola.calendula.ui.common.currentLocale
|
||||||
import de.jeanlucmakiola.calendula.ui.common.rememberCalendarSlideSpec
|
import de.jeanlucmakiola.calendula.ui.common.rememberCalendarSlideSpec
|
||||||
import de.jeanlucmakiola.calendula.ui.common.next
|
import de.jeanlucmakiola.calendula.ui.common.next
|
||||||
import de.jeanlucmakiola.floret.components.pastelize
|
|
||||||
import de.jeanlucmakiola.floret.time.isoWeekNumber
|
import de.jeanlucmakiola.floret.time.isoWeekNumber
|
||||||
import kotlinx.coroutines.launch
|
import kotlinx.coroutines.launch
|
||||||
import kotlinx.datetime.DayOfWeek
|
import kotlinx.datetime.DayOfWeek
|
||||||
@@ -630,7 +632,7 @@ private fun DayNumberCell(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/** A filled event pill/bar — pastelized fill, title clipped to one line. */
|
/** A filled event pill/bar — softened (or raw) fill, title clipped to one line. */
|
||||||
@Composable
|
@Composable
|
||||||
private fun MonthBar(
|
private fun MonthBar(
|
||||||
event: de.jeanlucmakiola.calendula.domain.EventInstance,
|
event: de.jeanlucmakiola.calendula.domain.EventInstance,
|
||||||
@@ -642,6 +644,8 @@ private fun MonthBar(
|
|||||||
val title = event.title.ifBlank { stringResource(R.string.event_untitled) }
|
val title = event.title.ifBlank { stringResource(R.string.event_untitled) }
|
||||||
val dimCutoff = LocalDimCutoff.current
|
val dimCutoff = LocalDimCutoff.current
|
||||||
val dimmed = dimCutoff != null && event.hasEnded(dimCutoff)
|
val dimmed = dimCutoff != null && event.hasEnded(dimCutoff)
|
||||||
|
val soften = LocalSoftenColors.current
|
||||||
|
val fill = eventFill(event.color, dark, soften)
|
||||||
val shape = RoundedCornerShape(
|
val shape = RoundedCornerShape(
|
||||||
topStart = if (continuesLeft) 0.dp else 4.dp,
|
topStart = if (continuesLeft) 0.dp else 4.dp,
|
||||||
bottomStart = if (continuesLeft) 0.dp else 4.dp,
|
bottomStart = if (continuesLeft) 0.dp else 4.dp,
|
||||||
@@ -650,7 +654,7 @@ private fun MonthBar(
|
|||||||
)
|
)
|
||||||
Box(
|
Box(
|
||||||
modifier = (if (dimmed) modifier.alpha(EventDimAlpha) else modifier)
|
modifier = (if (dimmed) modifier.alpha(EventDimAlpha) else modifier)
|
||||||
.background(pastelize(event.color, dark), shape)
|
.background(fill, shape)
|
||||||
.padding(horizontal = 4.dp)
|
.padding(horizontal = 4.dp)
|
||||||
.semantics { contentDescription = title },
|
.semantics { contentDescription = title },
|
||||||
contentAlignment = Alignment.CenterStart,
|
contentAlignment = Alignment.CenterStart,
|
||||||
@@ -660,7 +664,7 @@ private fun MonthBar(
|
|||||||
style = MaterialTheme.typography.labelSmall,
|
style = MaterialTheme.typography.labelSmall,
|
||||||
maxLines = 1,
|
maxLines = 1,
|
||||||
overflow = TextOverflow.Ellipsis,
|
overflow = TextOverflow.Ellipsis,
|
||||||
color = Color.Black.copy(alpha = 0.8f),
|
color = eventInk(fill),
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -673,6 +677,7 @@ private fun OverflowDots(
|
|||||||
dark: Boolean,
|
dark: Boolean,
|
||||||
modifier: Modifier = Modifier,
|
modifier: Modifier = Modifier,
|
||||||
) {
|
) {
|
||||||
|
val soften = LocalSoftenColors.current
|
||||||
Row(
|
Row(
|
||||||
modifier = modifier.height(EVENT_ROW_HEIGHT),
|
modifier = modifier.height(EVENT_ROW_HEIGHT),
|
||||||
horizontalArrangement = Arrangement.spacedBy(2.dp),
|
horizontalArrangement = Arrangement.spacedBy(2.dp),
|
||||||
@@ -682,7 +687,7 @@ private fun OverflowDots(
|
|||||||
Box(
|
Box(
|
||||||
modifier = Modifier
|
modifier = Modifier
|
||||||
.size(6.dp)
|
.size(6.dp)
|
||||||
.background(pastelize(argb, dark), CircleShape),
|
.background(eventFill(argb, dark, soften), CircleShape),
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
if (extra > 0) {
|
if (extra > 0) {
|
||||||
|
|||||||
@@ -55,9 +55,10 @@ import de.jeanlucmakiola.floret.components.GroupedRow
|
|||||||
import de.jeanlucmakiola.floret.components.InlineTextField
|
import de.jeanlucmakiola.floret.components.InlineTextField
|
||||||
import de.jeanlucmakiola.floret.components.Position
|
import de.jeanlucmakiola.floret.components.Position
|
||||||
import de.jeanlucmakiola.calendula.ui.common.currentLocale
|
import de.jeanlucmakiola.calendula.ui.common.currentLocale
|
||||||
|
import de.jeanlucmakiola.calendula.ui.common.LocalSoftenColors
|
||||||
|
import de.jeanlucmakiola.calendula.ui.common.eventFill
|
||||||
import de.jeanlucmakiola.calendula.ui.common.LocalUse24HourFormat
|
import de.jeanlucmakiola.calendula.ui.common.LocalUse24HourFormat
|
||||||
import de.jeanlucmakiola.calendula.ui.common.timeOfDayFormatter
|
import de.jeanlucmakiola.calendula.ui.common.timeOfDayFormatter
|
||||||
import de.jeanlucmakiola.floret.components.pastelize
|
|
||||||
import de.jeanlucmakiola.floret.components.positionOf
|
import de.jeanlucmakiola.floret.components.positionOf
|
||||||
import java.time.Instant as JavaInstant
|
import java.time.Instant as JavaInstant
|
||||||
import java.time.ZoneId
|
import java.time.ZoneId
|
||||||
@@ -192,6 +193,7 @@ private fun SearchResultRow(
|
|||||||
onClick: () -> Unit,
|
onClick: () -> Unit,
|
||||||
) {
|
) {
|
||||||
val dark = isSystemInDarkTheme()
|
val dark = isSystemInDarkTheme()
|
||||||
|
val soften = LocalSoftenColors.current
|
||||||
GroupedRow(
|
GroupedRow(
|
||||||
modifier = modifier,
|
modifier = modifier,
|
||||||
title = event.title,
|
title = event.title,
|
||||||
@@ -203,7 +205,7 @@ private fun SearchResultRow(
|
|||||||
modifier = Modifier
|
modifier = Modifier
|
||||||
.size(width = 6.dp, height = 36.dp)
|
.size(width = 6.dp, height = 36.dp)
|
||||||
.clip(RoundedCornerShape(3.dp))
|
.clip(RoundedCornerShape(3.dp))
|
||||||
.background(pastelize(event.color, dark)),
|
.background(eventFill(event.color, dark, soften)),
|
||||||
)
|
)
|
||||||
},
|
},
|
||||||
onClick = onClick,
|
onClick = onClick,
|
||||||
|
|||||||
@@ -517,7 +517,7 @@ private fun AppearanceScreen(
|
|||||||
} else {
|
} else {
|
||||||
stringResource(R.string.settings_dynamic_color_unavailable)
|
stringResource(R.string.settings_dynamic_color_unavailable)
|
||||||
},
|
},
|
||||||
position = Position.Bottom,
|
position = Position.Middle,
|
||||||
trailing = {
|
trailing = {
|
||||||
Switch(
|
Switch(
|
||||||
checked = state.dynamicColor,
|
checked = state.dynamicColor,
|
||||||
@@ -531,6 +531,18 @@ private fun AppearanceScreen(
|
|||||||
null
|
null
|
||||||
},
|
},
|
||||||
)
|
)
|
||||||
|
GroupedRow(
|
||||||
|
title = stringResource(R.string.settings_soften_colors),
|
||||||
|
summary = stringResource(R.string.settings_soften_colors_summary),
|
||||||
|
position = Position.Bottom,
|
||||||
|
trailing = {
|
||||||
|
Switch(
|
||||||
|
checked = state.softenColors,
|
||||||
|
onCheckedChange = viewModel::setSoftenColors,
|
||||||
|
)
|
||||||
|
},
|
||||||
|
onClick = { viewModel.setSoftenColors(!state.softenColors) },
|
||||||
|
)
|
||||||
|
|
||||||
Spacer(Modifier.height(16.dp))
|
Spacer(Modifier.height(16.dp))
|
||||||
|
|
||||||
|
|||||||
@@ -24,6 +24,8 @@ data class SettingsUiState(
|
|||||||
val themeMode: ThemeMode = ThemeMode.SYSTEM,
|
val themeMode: ThemeMode = ThemeMode.SYSTEM,
|
||||||
val dynamicColor: Boolean = true,
|
val dynamicColor: Boolean = true,
|
||||||
val dynamicColorAvailable: Boolean = true,
|
val dynamicColorAvailable: Boolean = true,
|
||||||
|
/** Whether raw provider colours are softened to theme-fitting pastels (#36). */
|
||||||
|
val softenColors: Boolean = true,
|
||||||
val weekStart: WeekStartPref = WeekStartPref.Auto,
|
val weekStart: WeekStartPref = WeekStartPref.Auto,
|
||||||
/** Clock convention for time labels (v2.11). AUTO follows the system setting. */
|
/** Clock convention for time labels (v2.11). AUTO follows the system setting. */
|
||||||
val timeFormat: TimeFormatPref = TimeFormatPref.AUTO,
|
val timeFormat: TimeFormatPref = TimeFormatPref.AUTO,
|
||||||
|
|||||||
@@ -123,14 +123,17 @@ class SettingsViewModel @Inject constructor(
|
|||||||
prefs.showHourLines,
|
prefs.showHourLines,
|
||||||
prefs.showWeekNumbers,
|
prefs.showWeekNumbers,
|
||||||
prefs.agendaShowToday,
|
prefs.agendaShowToday,
|
||||||
::Triple,
|
prefs.softenCalendarColors,
|
||||||
),
|
) { hourLines, weekNumbers, showToday, soften ->
|
||||||
|
DisplayToggles(hourLines, weekNumbers, showToday, soften)
|
||||||
|
},
|
||||||
) { view, screenRange, widgetRange, timeFormat, toggles ->
|
) { view, screenRange, widgetRange, timeFormat, toggles ->
|
||||||
ViewSettings(
|
ViewSettings(
|
||||||
view, screenRange, widgetRange, timeFormat,
|
view, screenRange, widgetRange, timeFormat,
|
||||||
showHourLines = toggles.first,
|
showHourLines = toggles.showHourLines,
|
||||||
showWeekNumbers = toggles.second,
|
showWeekNumbers = toggles.showWeekNumbers,
|
||||||
agendaShowToday = toggles.third,
|
agendaShowToday = toggles.agendaShowToday,
|
||||||
|
softenColors = toggles.softenColors,
|
||||||
)
|
)
|
||||||
},
|
},
|
||||||
combine(
|
combine(
|
||||||
@@ -155,6 +158,7 @@ class SettingsViewModel @Inject constructor(
|
|||||||
showHourLines = views.showHourLines,
|
showHourLines = views.showHourLines,
|
||||||
showWeekNumbers = views.showWeekNumbers,
|
showWeekNumbers = views.showWeekNumbers,
|
||||||
agendaShowToday = views.agendaShowToday,
|
agendaShowToday = views.agendaShowToday,
|
||||||
|
softenColors = views.softenColors,
|
||||||
agendaShowRangeBar = misc.showRangeBar,
|
agendaShowRangeBar = misc.showRangeBar,
|
||||||
autofocusEventTitle = misc.autofocusEventTitle,
|
autofocusEventTitle = misc.autofocusEventTitle,
|
||||||
pastEventDisplay = misc.pastEventDisplay,
|
pastEventDisplay = misc.pastEventDisplay,
|
||||||
@@ -229,6 +233,14 @@ class SettingsViewModel @Inject constructor(
|
|||||||
val showHourLines: Boolean,
|
val showHourLines: Boolean,
|
||||||
val showWeekNumbers: Boolean,
|
val showWeekNumbers: Boolean,
|
||||||
val agendaShowToday: Boolean,
|
val agendaShowToday: Boolean,
|
||||||
|
val softenColors: Boolean,
|
||||||
|
)
|
||||||
|
|
||||||
|
private data class DisplayToggles(
|
||||||
|
val showHourLines: Boolean,
|
||||||
|
val showWeekNumbers: Boolean,
|
||||||
|
val agendaShowToday: Boolean,
|
||||||
|
val softenColors: Boolean,
|
||||||
)
|
)
|
||||||
|
|
||||||
private data class MiscSettings(
|
private data class MiscSettings(
|
||||||
@@ -341,6 +353,19 @@ class SettingsViewModel @Inject constructor(
|
|||||||
viewModelScope.launch { prefs.setDynamicColor(enabled) }
|
viewModelScope.launch { prefs.setDynamicColor(enabled) }
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fun setSoftenColors(enabled: Boolean) {
|
||||||
|
viewModelScope.launch {
|
||||||
|
prefs.setSoftenCalendarColors(enabled)
|
||||||
|
// Both widgets paint event colours through the same softener, so a
|
||||||
|
// change has to redraw them too (they read the flag in their data
|
||||||
|
// preamble, like is24Hour).
|
||||||
|
widgetRefreshMutex.withLock {
|
||||||
|
AgendaWidget().updateAll(appContext)
|
||||||
|
MonthWidget().updateAll(appContext)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
fun setFont(role: FontRole, token: String) {
|
fun setFont(role: FontRole, token: String) {
|
||||||
viewModelScope.launch {
|
viewModelScope.launch {
|
||||||
when (role) {
|
when (role) {
|
||||||
|
|||||||
@@ -84,6 +84,9 @@ import de.jeanlucmakiola.calendula.ui.common.CalendarView
|
|||||||
import de.jeanlucmakiola.calendula.ui.common.IMPLEMENTED_VIEWS
|
import de.jeanlucmakiola.calendula.ui.common.IMPLEMENTED_VIEWS
|
||||||
import de.jeanlucmakiola.calendula.ui.common.EventDimAlpha
|
import de.jeanlucmakiola.calendula.ui.common.EventDimAlpha
|
||||||
import de.jeanlucmakiola.calendula.ui.common.LocalDimCutoff
|
import de.jeanlucmakiola.calendula.ui.common.LocalDimCutoff
|
||||||
|
import de.jeanlucmakiola.calendula.ui.common.LocalSoftenColors
|
||||||
|
import de.jeanlucmakiola.calendula.ui.common.eventFill
|
||||||
|
import de.jeanlucmakiola.calendula.ui.common.eventInk
|
||||||
import de.jeanlucmakiola.calendula.ui.common.NowLine
|
import de.jeanlucmakiola.calendula.ui.common.NowLine
|
||||||
import de.jeanlucmakiola.calendula.ui.common.rememberCurrentMinute
|
import de.jeanlucmakiola.calendula.ui.common.rememberCurrentMinute
|
||||||
import de.jeanlucmakiola.calendula.ui.common.ViewSwitcherPill
|
import de.jeanlucmakiola.calendula.ui.common.ViewSwitcherPill
|
||||||
@@ -98,7 +101,6 @@ import de.jeanlucmakiola.calendula.ui.common.formatMinuteOfDay
|
|||||||
import de.jeanlucmakiola.calendula.ui.common.hourSeparatorLines
|
import de.jeanlucmakiola.calendula.ui.common.hourSeparatorLines
|
||||||
import de.jeanlucmakiola.calendula.ui.common.rememberCalendarSlideSpec
|
import de.jeanlucmakiola.calendula.ui.common.rememberCalendarSlideSpec
|
||||||
import de.jeanlucmakiola.calendula.ui.common.next
|
import de.jeanlucmakiola.calendula.ui.common.next
|
||||||
import de.jeanlucmakiola.floret.components.pastelize
|
|
||||||
import de.jeanlucmakiola.floret.time.isoWeekNumber
|
import de.jeanlucmakiola.floret.time.isoWeekNumber
|
||||||
import kotlinx.coroutines.flow.first
|
import kotlinx.coroutines.flow.first
|
||||||
import kotlinx.coroutines.launch
|
import kotlinx.coroutines.launch
|
||||||
@@ -589,9 +591,11 @@ private fun AllDayBar(
|
|||||||
val title = event.title.ifBlank { stringResource(R.string.event_untitled) }
|
val title = event.title.ifBlank { stringResource(R.string.event_untitled) }
|
||||||
val dimCutoff = LocalDimCutoff.current
|
val dimCutoff = LocalDimCutoff.current
|
||||||
val dimmed = dimCutoff != null && event.hasEnded(dimCutoff)
|
val dimmed = dimCutoff != null && event.hasEnded(dimCutoff)
|
||||||
|
val soften = LocalSoftenColors.current
|
||||||
|
val fill = eventFill(event.color, dark, soften)
|
||||||
Box(
|
Box(
|
||||||
modifier = (if (dimmed) modifier.alpha(EventDimAlpha) else modifier)
|
modifier = (if (dimmed) modifier.alpha(EventDimAlpha) else modifier)
|
||||||
.background(pastelize(event.color, dark), RoundedCornerShape(4.dp))
|
.background(fill, RoundedCornerShape(4.dp))
|
||||||
.clickable(onClick = onClick)
|
.clickable(onClick = onClick)
|
||||||
.padding(horizontal = 6.dp, vertical = 2.dp)
|
.padding(horizontal = 6.dp, vertical = 2.dp)
|
||||||
.semantics { contentDescription = title },
|
.semantics { contentDescription = title },
|
||||||
@@ -602,7 +606,7 @@ private fun AllDayBar(
|
|||||||
style = MaterialTheme.typography.labelSmall,
|
style = MaterialTheme.typography.labelSmall,
|
||||||
maxLines = 1,
|
maxLines = 1,
|
||||||
overflow = TextOverflow.Ellipsis,
|
overflow = TextOverflow.Ellipsis,
|
||||||
color = Color.Black.copy(alpha = 0.8f),
|
color = eventInk(fill),
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -781,9 +785,11 @@ private fun EventBlock(
|
|||||||
val titleMaxLines = (contentHeight / titleLineHeight).toInt().coerceAtLeast(1)
|
val titleMaxLines = (contentHeight / titleLineHeight).toInt().coerceAtLeast(1)
|
||||||
val dimCutoff = LocalDimCutoff.current
|
val dimCutoff = LocalDimCutoff.current
|
||||||
val dimmed = dimCutoff != null && block.event.hasEnded(dimCutoff)
|
val dimmed = dimCutoff != null && block.event.hasEnded(dimCutoff)
|
||||||
|
val soften = LocalSoftenColors.current
|
||||||
|
val fill = eventFill(block.event.color, dark, soften)
|
||||||
Box(
|
Box(
|
||||||
modifier = (if (dimmed) modifier.alpha(EventDimAlpha) else modifier)
|
modifier = (if (dimmed) modifier.alpha(EventDimAlpha) else modifier)
|
||||||
.background(pastelize(block.event.color, dark), RoundedCornerShape(4.dp))
|
.background(fill, RoundedCornerShape(4.dp))
|
||||||
.clickable(onClick = onClick)
|
.clickable(onClick = onClick)
|
||||||
.padding(horizontal = 4.dp, vertical = 2.dp)
|
.padding(horizontal = 4.dp, vertical = 2.dp)
|
||||||
.semantics { contentDescription = "$title, $timeLabel" },
|
.semantics { contentDescription = "$title, $timeLabel" },
|
||||||
@@ -794,7 +800,7 @@ private fun EventBlock(
|
|||||||
style = MaterialTheme.typography.labelMedium,
|
style = MaterialTheme.typography.labelMedium,
|
||||||
maxLines = titleMaxLines,
|
maxLines = titleMaxLines,
|
||||||
overflow = TextOverflow.Ellipsis,
|
overflow = TextOverflow.Ellipsis,
|
||||||
color = Color.Black.copy(alpha = 0.85f),
|
color = eventInk(fill, alpha = 0.85f),
|
||||||
)
|
)
|
||||||
if (showTime) {
|
if (showTime) {
|
||||||
Text(
|
Text(
|
||||||
@@ -802,7 +808,7 @@ private fun EventBlock(
|
|||||||
style = MaterialTheme.typography.labelSmall,
|
style = MaterialTheme.typography.labelSmall,
|
||||||
maxLines = 1,
|
maxLines = 1,
|
||||||
overflow = TextOverflow.Ellipsis,
|
overflow = TextOverflow.Ellipsis,
|
||||||
color = Color.Black.copy(alpha = 0.6f),
|
color = eventInk(fill, alpha = 0.6f),
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -60,6 +60,8 @@ sealed interface AgendaWidgetData {
|
|||||||
val days: List<AgendaDay>,
|
val days: List<AgendaDay>,
|
||||||
/** Resolved clock convention for event time labels (the time-format pref). */
|
/** Resolved clock convention for event time labels (the time-format pref). */
|
||||||
val is24Hour: Boolean,
|
val is24Hour: Boolean,
|
||||||
|
/** Whether event colours are softened to pastels, or shown raw (#36). */
|
||||||
|
val soften: Boolean,
|
||||||
/** First day of the week, for the calendar-aligned "this week" range. */
|
/** First day of the week, for the calendar-aligned "this week" range. */
|
||||||
val weekStart: DayOfWeek,
|
val weekStart: DayOfWeek,
|
||||||
/** Saved range pref — the fallback when an instance has no Glance state yet. */
|
/** Saved range pref — the fallback when an instance has no Glance state yet. */
|
||||||
@@ -132,10 +134,12 @@ internal suspend fun Context.loadAgendaWidgetData(): AgendaWidgetData {
|
|||||||
val instances = ep.calendarRepository().instances(window).first()
|
val instances = ep.calendarRepository().instances(window).first()
|
||||||
val is24Hour = prefs.timeFormat.first()
|
val is24Hour = prefs.timeFormat.first()
|
||||||
.is24Hour(android.text.format.DateFormat.is24HourFormat(this))
|
.is24Hour(android.text.format.DateFormat.is24HourFormat(this))
|
||||||
|
val soften = prefs.softenCalendarColors.first()
|
||||||
return AgendaWidgetData.Ready(
|
return AgendaWidgetData.Ready(
|
||||||
today = anchor,
|
today = anchor,
|
||||||
days = groupAgendaDays(anchor, instances, zone),
|
days = groupAgendaDays(anchor, instances, zone),
|
||||||
is24Hour = is24Hour,
|
is24Hour = is24Hour,
|
||||||
|
soften = soften,
|
||||||
weekStart = weekStart,
|
weekStart = weekStart,
|
||||||
savedRange = savedRange,
|
savedRange = savedRange,
|
||||||
savedPastDisplay = savedPastDisplay,
|
savedPastDisplay = savedPastDisplay,
|
||||||
|
|||||||
@@ -56,7 +56,7 @@ import de.jeanlucmakiola.calendula.ui.common.CalendarView
|
|||||||
import de.jeanlucmakiola.calendula.ui.common.EventDimAlpha
|
import de.jeanlucmakiola.calendula.ui.common.EventDimAlpha
|
||||||
import de.jeanlucmakiola.calendula.ui.common.formatTimeOfDay
|
import de.jeanlucmakiola.calendula.ui.common.formatTimeOfDay
|
||||||
import de.jeanlucmakiola.calendula.ui.common.localizedDateFormatter
|
import de.jeanlucmakiola.calendula.ui.common.localizedDateFormatter
|
||||||
import de.jeanlucmakiola.floret.components.pastelize
|
import de.jeanlucmakiola.calendula.ui.common.eventFill
|
||||||
import de.jeanlucmakiola.calendula.widget.AgendaWidgetData
|
import de.jeanlucmakiola.calendula.widget.AgendaWidgetData
|
||||||
import de.jeanlucmakiola.calendula.widget.CalendulaGlanceTheme
|
import de.jeanlucmakiola.calendula.widget.CalendulaGlanceTheme
|
||||||
import de.jeanlucmakiola.calendula.widget.loadAgendaWidgetData
|
import de.jeanlucmakiola.calendula.widget.loadAgendaWidgetData
|
||||||
@@ -197,6 +197,7 @@ private fun AgendaWidgetBody(data: AgendaWidgetData, dark: Boolean) {
|
|||||||
is AgendaRow.Event -> EventRow(
|
is AgendaRow.Event -> EventRow(
|
||||||
event = row.event,
|
event = row.event,
|
||||||
dark = dark,
|
dark = dark,
|
||||||
|
soften = data.soften,
|
||||||
is24Hour = data.is24Hour,
|
is24Hour = data.is24Hour,
|
||||||
dimmed = pastDisplay == PastEventDisplay.DIM &&
|
dimmed = pastDisplay == PastEventDisplay.DIM &&
|
||||||
row.event.hasEnded(data.now),
|
row.event.hasEnded(data.now),
|
||||||
@@ -309,12 +310,18 @@ private fun PlaceholderRow(date: LocalDate) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
@Composable
|
@Composable
|
||||||
private fun EventRow(event: EventInstance, dark: Boolean, is24Hour: Boolean, dimmed: Boolean) {
|
private fun EventRow(
|
||||||
|
event: EventInstance,
|
||||||
|
dark: Boolean,
|
||||||
|
soften: Boolean,
|
||||||
|
is24Hour: Boolean,
|
||||||
|
dimmed: Boolean,
|
||||||
|
) {
|
||||||
val context = androidx.glance.LocalContext.current
|
val context = androidx.glance.LocalContext.current
|
||||||
val title = event.title.ifBlank { context.getString(R.string.event_untitled) }
|
val title = event.title.ifBlank { context.getString(R.string.event_untitled) }
|
||||||
// Glance has no generic alpha modifier, so dim by fading the colour stripe and
|
// Glance has no generic alpha modifier, so dim by fading the colour stripe and
|
||||||
// dropping both text lines to the lower-emphasis on-surface-variant tone.
|
// dropping both text lines to the lower-emphasis on-surface-variant tone.
|
||||||
val stripeColor = pastelize(event.color, dark).let {
|
val stripeColor = eventFill(event.color, dark, soften).let {
|
||||||
if (dimmed) it.copy(alpha = EventDimAlpha) else it
|
if (dimmed) it.copy(alpha = EventDimAlpha) else it
|
||||||
}
|
}
|
||||||
val titleColor = if (dimmed) GlanceTheme.colors.onSurfaceVariant else GlanceTheme.colors.onSurface
|
val titleColor = if (dimmed) GlanceTheme.colors.onSurfaceVariant else GlanceTheme.colors.onSurface
|
||||||
|
|||||||
@@ -3,7 +3,6 @@ package de.jeanlucmakiola.calendula.widget.month
|
|||||||
import android.content.Context
|
import android.content.Context
|
||||||
import android.content.res.Configuration
|
import android.content.res.Configuration
|
||||||
import androidx.compose.runtime.Composable
|
import androidx.compose.runtime.Composable
|
||||||
import androidx.compose.ui.graphics.Color
|
|
||||||
import androidx.compose.ui.unit.dp
|
import androidx.compose.ui.unit.dp
|
||||||
import androidx.compose.ui.unit.sp
|
import androidx.compose.ui.unit.sp
|
||||||
import androidx.datastore.preferences.core.intPreferencesKey
|
import androidx.datastore.preferences.core.intPreferencesKey
|
||||||
@@ -50,14 +49,17 @@ import de.jeanlucmakiola.calendula.MainActivity
|
|||||||
import de.jeanlucmakiola.calendula.R
|
import de.jeanlucmakiola.calendula.R
|
||||||
import de.jeanlucmakiola.calendula.domain.EventInstance
|
import de.jeanlucmakiola.calendula.domain.EventInstance
|
||||||
import de.jeanlucmakiola.calendula.ui.common.CalendarView
|
import de.jeanlucmakiola.calendula.ui.common.CalendarView
|
||||||
import de.jeanlucmakiola.floret.components.pastelize
|
|
||||||
import de.jeanlucmakiola.calendula.ui.month.MonthWeek
|
import de.jeanlucmakiola.calendula.ui.month.MonthWeek
|
||||||
import de.jeanlucmakiola.calendula.ui.month.layoutMonthWeeks
|
import de.jeanlucmakiola.calendula.ui.month.layoutMonthWeeks
|
||||||
|
import de.jeanlucmakiola.calendula.ui.common.eventFill
|
||||||
|
import de.jeanlucmakiola.calendula.ui.common.eventInk
|
||||||
import de.jeanlucmakiola.calendula.widget.CalendulaGlanceTheme
|
import de.jeanlucmakiola.calendula.widget.CalendulaGlanceTheme
|
||||||
import de.jeanlucmakiola.calendula.widget.MonthWidgetSource
|
import de.jeanlucmakiola.calendula.widget.MonthWidgetSource
|
||||||
import de.jeanlucmakiola.calendula.widget.loadMonthWidgetSource
|
import de.jeanlucmakiola.calendula.widget.loadMonthWidgetSource
|
||||||
import de.jeanlucmakiola.calendula.widget.systemZone
|
import de.jeanlucmakiola.calendula.widget.systemZone
|
||||||
import de.jeanlucmakiola.calendula.widget.today
|
import de.jeanlucmakiola.calendula.widget.today
|
||||||
|
import de.jeanlucmakiola.calendula.widget.widgetEntryPoint
|
||||||
|
import kotlinx.coroutines.flow.first
|
||||||
import androidx.compose.ui.unit.Dp
|
import androidx.compose.ui.unit.Dp
|
||||||
import kotlinx.datetime.DayOfWeek
|
import kotlinx.datetime.DayOfWeek
|
||||||
import kotlinx.datetime.LocalDate
|
import kotlinx.datetime.LocalDate
|
||||||
@@ -76,9 +78,6 @@ private val LANE_HEIGHT = 14.dp
|
|||||||
private val DAY_NUMBER_HEIGHT = 18.dp
|
private val DAY_NUMBER_HEIGHT = 18.dp
|
||||||
private val GRID_HPADDING = 8.dp
|
private val GRID_HPADDING = 8.dp
|
||||||
|
|
||||||
/** Dark ink that reads on the pastelized event fills, like the in-app MonthBar. */
|
|
||||||
private val EventInk = ColorProvider(Color(0xDE000000))
|
|
||||||
|
|
||||||
private fun currentMonthIndex(zone: TimeZone): Int {
|
private fun currentMonthIndex(zone: TimeZone): Int {
|
||||||
val t = today(zone)
|
val t = today(zone)
|
||||||
return t.year * 12 + t.month.ordinal
|
return t.year * 12 + t.month.ordinal
|
||||||
@@ -107,9 +106,12 @@ class MonthWidget : GlanceAppWidget() {
|
|||||||
val source = context.loadMonthWidgetSource()
|
val source = context.loadMonthWidgetSource()
|
||||||
val dark = (context.resources.configuration.uiMode and
|
val dark = (context.resources.configuration.uiMode and
|
||||||
Configuration.UI_MODE_NIGHT_MASK) == Configuration.UI_MODE_NIGHT_YES
|
Configuration.UI_MODE_NIGHT_MASK) == Configuration.UI_MODE_NIGHT_YES
|
||||||
|
// Read fresh (not through the cached source) so toggling the softener
|
||||||
|
// redraws with the new choice; it's one cheap DataStore read.
|
||||||
|
val soften = context.widgetEntryPoint().settingsPrefs().softenCalendarColors.first()
|
||||||
provideContent {
|
provideContent {
|
||||||
CalendulaGlanceTheme {
|
CalendulaGlanceTheme {
|
||||||
MonthWidgetBody(source = source, dark = dark)
|
MonthWidgetBody(source = source, dark = dark, soften = soften)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -140,7 +142,7 @@ class ResetMonthAction : ActionCallback {
|
|||||||
}
|
}
|
||||||
|
|
||||||
@Composable
|
@Composable
|
||||||
private fun MonthWidgetBody(source: MonthWidgetSource, dark: Boolean) {
|
private fun MonthWidgetBody(source: MonthWidgetSource, dark: Boolean, soften: Boolean) {
|
||||||
Column(
|
Column(
|
||||||
modifier = GlanceModifier
|
modifier = GlanceModifier
|
||||||
.fillMaxSize()
|
.fillMaxSize()
|
||||||
@@ -169,6 +171,7 @@ private fun MonthWidgetBody(source: MonthWidgetSource, dark: Boolean) {
|
|||||||
currentMonth = ym.month,
|
currentMonth = ym.month,
|
||||||
today = source.today,
|
today = source.today,
|
||||||
dark = dark,
|
dark = dark,
|
||||||
|
soften = soften,
|
||||||
colW = colW,
|
colW = colW,
|
||||||
modifier = GlanceModifier.defaultWeight(),
|
modifier = GlanceModifier.defaultWeight(),
|
||||||
)
|
)
|
||||||
@@ -278,6 +281,7 @@ private fun WeekRow(
|
|||||||
currentMonth: Month,
|
currentMonth: Month,
|
||||||
today: LocalDate,
|
today: LocalDate,
|
||||||
dark: Boolean,
|
dark: Boolean,
|
||||||
|
soften: Boolean,
|
||||||
colW: Dp,
|
colW: Dp,
|
||||||
modifier: GlanceModifier,
|
modifier: GlanceModifier,
|
||||||
) {
|
) {
|
||||||
@@ -297,7 +301,7 @@ private fun WeekRow(
|
|||||||
// One lane row per event row. A multi-day span is a single Box spanning
|
// One lane row per event row. A multi-day span is a single Box spanning
|
||||||
// its columns (colW * n) so it's connected with no seam and rounded ends.
|
// its columns (colW * n) so it's connected with no seam and rounded ends.
|
||||||
repeat(MAX_LANES) { lane ->
|
repeat(MAX_LANES) { lane ->
|
||||||
LaneRow(week = week, lane = lane, dark = dark, colW = colW)
|
LaneRow(week = week, lane = lane, dark = dark, soften = soften, colW = colW)
|
||||||
Spacer(GlanceModifier.height(1.dp))
|
Spacer(GlanceModifier.height(1.dp))
|
||||||
}
|
}
|
||||||
OverflowRow(week = week, colW = colW)
|
OverflowRow(week = week, colW = colW)
|
||||||
@@ -346,7 +350,7 @@ private fun DayNumber(date: LocalDate, isToday: Boolean, inMonth: Boolean, colW:
|
|||||||
}
|
}
|
||||||
|
|
||||||
@Composable
|
@Composable
|
||||||
private fun LaneRow(week: MonthWeek, lane: Int, dark: Boolean, colW: Dp) {
|
private fun LaneRow(week: MonthWeek, lane: Int, dark: Boolean, soften: Boolean, colW: Dp) {
|
||||||
val context = LocalContext.current
|
val context = LocalContext.current
|
||||||
Row(modifier = GlanceModifier.fillMaxWidth()) {
|
Row(modifier = GlanceModifier.fillMaxWidth()) {
|
||||||
var col = 0
|
var col = 0
|
||||||
@@ -354,12 +358,12 @@ private fun LaneRow(week: MonthWeek, lane: Int, dark: Boolean, colW: Dp) {
|
|||||||
val span = week.spans.firstOrNull { it.lane == lane && col in it.startCol..it.endCol }
|
val span = week.spans.firstOrNull { it.lane == lane && col in it.startCol..it.endCol }
|
||||||
if (span != null) {
|
if (span != null) {
|
||||||
val cols = span.endCol - col + 1
|
val cols = span.endCol - col + 1
|
||||||
SpanBar(event = span.event, dark = dark, width = colW * cols)
|
SpanBar(event = span.event, dark = dark, soften = soften, width = colW * cols)
|
||||||
col = span.endCol + 1
|
col = span.endCol + 1
|
||||||
} else {
|
} else {
|
||||||
val timed = timedEventAt(week, lane, col, week.days[col])
|
val timed = timedEventAt(week, lane, col, week.days[col])
|
||||||
if (timed != null) {
|
if (timed != null) {
|
||||||
SpanBar(event = timed, dark = dark, width = colW)
|
SpanBar(event = timed, dark = dark, soften = soften, width = colW)
|
||||||
} else {
|
} else {
|
||||||
// Empty lane cell: a tap opens that day, so blank space in a
|
// Empty lane cell: a tap opens that day, so blank space in a
|
||||||
// day column is a day-open target just like the number is.
|
// day column is a day-open target just like the number is.
|
||||||
@@ -378,8 +382,9 @@ private fun LaneRow(week: MonthWeek, lane: Int, dark: Boolean, colW: Dp) {
|
|||||||
|
|
||||||
/** A single connected, rounded event bar [width] wide with its clipped title. */
|
/** A single connected, rounded event bar [width] wide with its clipped title. */
|
||||||
@Composable
|
@Composable
|
||||||
private fun SpanBar(event: EventInstance, dark: Boolean, width: Dp) {
|
private fun SpanBar(event: EventInstance, dark: Boolean, soften: Boolean, width: Dp) {
|
||||||
val context = LocalContext.current
|
val context = LocalContext.current
|
||||||
|
val fill = eventFill(event.color, dark, soften)
|
||||||
Box(
|
Box(
|
||||||
modifier = GlanceModifier
|
modifier = GlanceModifier
|
||||||
.width(width)
|
.width(width)
|
||||||
@@ -402,13 +407,13 @@ private fun SpanBar(event: EventInstance, dark: Boolean, width: Dp) {
|
|||||||
modifier = GlanceModifier
|
modifier = GlanceModifier
|
||||||
.fillMaxSize()
|
.fillMaxSize()
|
||||||
.cornerRadius(4.dp)
|
.cornerRadius(4.dp)
|
||||||
.background(pastelize(event.color, dark)),
|
.background(fill),
|
||||||
contentAlignment = Alignment.CenterStart,
|
contentAlignment = Alignment.CenterStart,
|
||||||
) {
|
) {
|
||||||
Text(
|
Text(
|
||||||
text = event.title.ifBlank { context.getString(R.string.event_untitled) },
|
text = event.title.ifBlank { context.getString(R.string.event_untitled) },
|
||||||
maxLines = 1,
|
maxLines = 1,
|
||||||
style = TextStyle(color = EventInk, fontSize = 9.sp),
|
style = TextStyle(color = ColorProvider(eventInk(fill)), fontSize = 9.sp),
|
||||||
modifier = GlanceModifier.padding(horizontal = 3.dp),
|
modifier = GlanceModifier.padding(horizontal = 3.dp),
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -293,6 +293,8 @@
|
|||||||
<string name="settings_default_view">Default view</string>
|
<string name="settings_default_view">Default view</string>
|
||||||
<string name="settings_dynamic_color">Dynamic colour</string>
|
<string name="settings_dynamic_color">Dynamic colour</string>
|
||||||
<string name="settings_dynamic_color_unavailable">Requires Android 12 or newer</string>
|
<string name="settings_dynamic_color_unavailable">Requires Android 12 or newer</string>
|
||||||
|
<string name="settings_soften_colors">Soften calendar colours</string>
|
||||||
|
<string name="settings_soften_colors_summary">Tone calendar and event colours down to fit the theme. Turn off to show the raw colours from the calendar source.</string>
|
||||||
<string name="settings_font_headings">Headings font</string>
|
<string name="settings_font_headings">Headings font</string>
|
||||||
<string name="settings_font_body">Body font</string>
|
<string name="settings_font_body">Body font</string>
|
||||||
<string name="settings_font_system">System default</string>
|
<string name="settings_font_system">System default</string>
|
||||||
|
|||||||
Reference in New Issue
Block a user