From 5dc1d492952a17a82353608366a360764f7be833 Mon Sep 17 00:00:00 2001 From: Jean-Luc Makiola Date: Wed, 28 Oct 2026 14:28:45 +0100 Subject: [PATCH 1/2] Show the task description in reminder notifications (#94) (#108) --- .../agendula/data/reminders/TaskNotifier.kt | 14 +++++++++++- .../data/reminders/ExpandedTextTest.kt | 22 +++++++++++++++++++ 2 files changed, 35 insertions(+), 1 deletion(-) create mode 100644 app/src/test/java/de/jeanlucmakiola/agendula/data/reminders/ExpandedTextTest.kt diff --git a/app/src/main/java/de/jeanlucmakiola/agendula/data/reminders/TaskNotifier.kt b/app/src/main/java/de/jeanlucmakiola/agendula/data/reminders/TaskNotifier.kt index d7f087d..4adc2c5 100644 --- a/app/src/main/java/de/jeanlucmakiola/agendula/data/reminders/TaskNotifier.kt +++ b/app/src/main/java/de/jeanlucmakiola/agendula/data/reminders/TaskNotifier.kt @@ -18,6 +18,7 @@ import androidx.core.content.edit import de.jeanlucmakiola.agendula.MainActivity import de.jeanlucmakiola.agendula.R import de.jeanlucmakiola.agendula.data.prefs.Settings +import de.jeanlucmakiola.agendula.domain.readableDescription import de.jeanlucmakiola.agendula.data.prefs.is24Hour import de.jeanlucmakiola.agendula.data.di.ApplicationScope import de.jeanlucmakiola.agendula.data.tasks.ProviderResolver @@ -84,6 +85,7 @@ class TaskNotifier @Inject constructor( ), ) } + val description = task.description?.readableDescription()?.trim()?.takeIf { it.isNotBlank() } val tapIntent = PendingIntent.getActivity( context, task.taskId.toInt(), @@ -93,7 +95,10 @@ class TaskNotifier @Inject constructor( val notification = NotificationCompat.Builder(context, channelId) .setSmallIcon(R.drawable.ic_notification) .setContentTitle(title) - .apply { if (text != null) setContentText(text) } + .apply { (text ?: description)?.let(::setContentText) } + .apply { + expandedText(text, description)?.let { setStyle(NotificationCompat.BigTextStyle().bigText(it)) } + } .setCategory(NotificationCompat.CATEGORY_REMINDER) .setPriority(NotificationCompat.PRIORITY_HIGH) .setAutoCancel(true) @@ -240,3 +245,10 @@ private fun relativeDay(date: LocalDate, today: LocalDate, words: RelativeDays, else -> null } } + +/** The expanded body of a due reminder: the due line, then the task's description. */ +internal fun expandedText(dueLine: String?, description: String?): String? = when { + description == null -> null + dueLine == null -> description + else -> "$dueLine\n\n$description" +} diff --git a/app/src/test/java/de/jeanlucmakiola/agendula/data/reminders/ExpandedTextTest.kt b/app/src/test/java/de/jeanlucmakiola/agendula/data/reminders/ExpandedTextTest.kt new file mode 100644 index 0000000..4ca7d65 --- /dev/null +++ b/app/src/test/java/de/jeanlucmakiola/agendula/data/reminders/ExpandedTextTest.kt @@ -0,0 +1,22 @@ +package de.jeanlucmakiola.agendula.data.reminders + +import com.google.common.truth.Truth.assertThat +import org.junit.jupiter.api.Test + +class ExpandedTextTest { + + @Test + fun `no description means no expanded body`() { + assertThat(expandedText("Due today", null)).isNull() + } + + @Test + fun `description alone when there is no due line`() { + assertThat(expandedText(null, "Buy oat milk")).isEqualTo("Buy oat milk") + } + + @Test + fun `due line comes first, separated by a blank line`() { + assertThat(expandedText("Due today", "Buy oat milk")).isEqualTo("Due today\n\nBuy oat milk") + } +} From c1219a434d7ad1f7df7383078e540e4a197eb414 Mon Sep 17 00:00:00 2001 From: Jean-Luc Makiola Date: Wed, 28 Oct 2026 14:29:25 +0100 Subject: [PATCH 2/2] Pure black theme for AMOLED screens (#99) (#109) --- .../java/de/jeanlucmakiola/agendula/MainActivity.kt | 13 ++++++------- .../agendula/data/prefs/SettingsPrefs.kt | 13 ++++++++++++- .../agendula/ui/settings/AppearanceScreen.kt | 2 ++ .../de/jeanlucmakiola/agendula/ui/theme/Theme.kt | 2 ++ .../agendula/widget/TaskWidgetConfigActivity.kt | 13 ++++++------- .../de/jeanlucmakiola/agendula/widget/WidgetData.kt | 2 +- app/src/main/res/values/strings.xml | 2 ++ .../agendula/widget/WidgetDataTest.kt | 1 + 8 files changed, 32 insertions(+), 16 deletions(-) diff --git a/app/src/main/java/de/jeanlucmakiola/agendula/MainActivity.kt b/app/src/main/java/de/jeanlucmakiola/agendula/MainActivity.kt index 1ac4cd6..f6386e4 100644 --- a/app/src/main/java/de/jeanlucmakiola/agendula/MainActivity.kt +++ b/app/src/main/java/de/jeanlucmakiola/agendula/MainActivity.kt @@ -28,7 +28,6 @@ import de.jeanlucmakiola.agendula.data.di.LaunchHook import de.jeanlucmakiola.agendula.domain.SmartList import de.jeanlucmakiola.agendula.ui.imports.importIntentUri import de.jeanlucmakiola.agendula.ui.navigation.AppShortcuts -import de.jeanlucmakiola.agendula.data.prefs.ThemeMode import de.jeanlucmakiola.agendula.ui.RootScreen import de.jeanlucmakiola.agendula.ui.crash.CrashReportActivity import de.jeanlucmakiola.agendula.ui.navigation.NavRequest @@ -88,15 +87,15 @@ class MainActivity : ComponentActivity() { setContent { val settingsViewModel: SettingsViewModel = hiltViewModel() val ui by settingsViewModel.state.collectAsStateWithLifecycle() - val darkTheme = when (ui.settings.themeMode) { - ThemeMode.SYSTEM -> isSystemInDarkTheme() - ThemeMode.LIGHT -> false - ThemeMode.DARK -> true - } + val darkTheme = ui.settings.themeMode.isDark(isSystemInDarkTheme()) val context = LocalContext.current val use24Hour = ui.settings.timeFormat.is24Hour(DateFormat.is24HourFormat(context)) val firstDayOfWeek = ui.settings.weekStart ?: localeFirstDayOfWeek() - AgendulaTheme(darkTheme = darkTheme, dynamicColor = ui.settings.dynamicColor) { + AgendulaTheme( + darkTheme = darkTheme, + dynamicColor = ui.settings.dynamicColor, + pureBlack = ui.settings.themeMode.isPureBlack, + ) { CompositionLocalProvider( LocalUse24HourFormat provides use24Hour, LocalFirstDayOfWeek provides firstDayOfWeek, diff --git a/app/src/main/java/de/jeanlucmakiola/agendula/data/prefs/SettingsPrefs.kt b/app/src/main/java/de/jeanlucmakiola/agendula/data/prefs/SettingsPrefs.kt index 15d6244..73f09c2 100644 --- a/app/src/main/java/de/jeanlucmakiola/agendula/data/prefs/SettingsPrefs.kt +++ b/app/src/main/java/de/jeanlucmakiola/agendula/data/prefs/SettingsPrefs.kt @@ -31,7 +31,18 @@ import kotlinx.coroutines.flow.map import javax.inject.Inject import javax.inject.Singleton -enum class ThemeMode { SYSTEM, LIGHT, DARK } +/** [BLACK] is dark with pure-black surfaces, for AMOLED screens. */ +enum class ThemeMode { + SYSTEM, LIGHT, DARK, BLACK; + + fun isDark(systemDark: Boolean): Boolean = when (this) { + SYSTEM -> systemDark + LIGHT -> false + DARK, BLACK -> true + } + + val isPureBlack: Boolean get() = this == BLACK +} /** Clock convention: AUTO follows the device's 24-hour switch. */ enum class TimeFormatPref { AUTO, TWELVE_HOUR, TWENTY_FOUR_HOUR } diff --git a/app/src/main/java/de/jeanlucmakiola/agendula/ui/settings/AppearanceScreen.kt b/app/src/main/java/de/jeanlucmakiola/agendula/ui/settings/AppearanceScreen.kt index 7c1ce72..acaa3f9 100644 --- a/app/src/main/java/de/jeanlucmakiola/agendula/ui/settings/AppearanceScreen.kt +++ b/app/src/main/java/de/jeanlucmakiola/agendula/ui/settings/AppearanceScreen.kt @@ -140,6 +140,7 @@ internal fun AppearanceScreen( options = ThemeMode.entries, selected = state.settings.themeMode, label = { stringResource(themeLabel(it)) }, + summary = { if (it == ThemeMode.BLACK) stringResource(R.string.settings_theme_black_summary) else null }, onSelect = viewModel::setThemeMode, onDismiss = { showTheme = false }, ) @@ -167,4 +168,5 @@ private fun themeLabel(mode: ThemeMode): Int = when (mode) { ThemeMode.SYSTEM -> R.string.settings_theme_system ThemeMode.LIGHT -> R.string.settings_theme_light ThemeMode.DARK -> R.string.settings_theme_dark + ThemeMode.BLACK -> R.string.settings_theme_black } diff --git a/app/src/main/java/de/jeanlucmakiola/agendula/ui/theme/Theme.kt b/app/src/main/java/de/jeanlucmakiola/agendula/ui/theme/Theme.kt index 2bca6f8..6e02f5f 100644 --- a/app/src/main/java/de/jeanlucmakiola/agendula/ui/theme/Theme.kt +++ b/app/src/main/java/de/jeanlucmakiola/agendula/ui/theme/Theme.kt @@ -15,6 +15,7 @@ import de.jeanlucmakiola.floret.identity.FloretExpressiveTheme fun AgendulaTheme( darkTheme: Boolean = isSystemInDarkTheme(), dynamicColor: Boolean = true, + pureBlack: Boolean = false, content: @Composable () -> Unit, ) { FloretExpressiveTheme( @@ -22,6 +23,7 @@ fun AgendulaTheme( darkScheme = AgendulaDarkFallback, darkTheme = darkTheme, dynamicColor = dynamicColor, + pureBlack = pureBlack, typography = AgendulaTypography, content = content, ) diff --git a/app/src/main/java/de/jeanlucmakiola/agendula/widget/TaskWidgetConfigActivity.kt b/app/src/main/java/de/jeanlucmakiola/agendula/widget/TaskWidgetConfigActivity.kt index 7287add..504720c 100644 --- a/app/src/main/java/de/jeanlucmakiola/agendula/widget/TaskWidgetConfigActivity.kt +++ b/app/src/main/java/de/jeanlucmakiola/agendula/widget/TaskWidgetConfigActivity.kt @@ -39,7 +39,6 @@ import dagger.hilt.android.AndroidEntryPoint import de.jeanlucmakiola.agendula.R import de.jeanlucmakiola.agendula.data.prefs.Settings import de.jeanlucmakiola.agendula.data.prefs.SettingsPrefs -import de.jeanlucmakiola.agendula.data.prefs.ThemeMode import de.jeanlucmakiola.agendula.data.tasks.TasksRepository import de.jeanlucmakiola.agendula.domain.SavedFilter import de.jeanlucmakiola.agendula.domain.SmartList @@ -135,12 +134,12 @@ class TaskWidgetConfigActivity : ComponentActivity() { rowColor = parseWidgetRowColor(state[WIDGET_ROW_COLOR_KEY]) selectedListIds = parseWidgetListIds(state[WIDGET_LIST_IDS_KEY]) } - val dark = when (settings.themeMode) { - ThemeMode.SYSTEM -> isSystemInDarkTheme() - ThemeMode.LIGHT -> false - ThemeMode.DARK -> true - } - AgendulaTheme(darkTheme = dark, dynamicColor = settings.dynamicColor) { + val dark = settings.themeMode.isDark(isSystemInDarkTheme()) + AgendulaTheme( + darkTheme = dark, + dynamicColor = settings.dynamicColor, + pureBlack = settings.themeMode.isPureBlack, + ) { Surface(modifier = Modifier.fillMaxSize(), color = MaterialTheme.colorScheme.surface) {} WidgetFilterPicker( kind = kind, diff --git a/app/src/main/java/de/jeanlucmakiola/agendula/widget/WidgetData.kt b/app/src/main/java/de/jeanlucmakiola/agendula/widget/WidgetData.kt index e1c0c34..c5e626c 100644 --- a/app/src/main/java/de/jeanlucmakiola/agendula/widget/WidgetData.kt +++ b/app/src/main/java/de/jeanlucmakiola/agendula/widget/WidgetData.kt @@ -82,7 +82,7 @@ internal fun effectiveThemeMode(appThemeMode: ThemeMode, override: WidgetThemeMo override ?: when (appThemeMode) { ThemeMode.SYSTEM -> WidgetThemeMode.SYSTEM ThemeMode.LIGHT -> WidgetThemeMode.LIGHT - ThemeMode.DARK -> WidgetThemeMode.DARK + ThemeMode.DARK, ThemeMode.BLACK -> WidgetThemeMode.DARK } /** The smart lists a widget can show, in picker order. */ diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index 3a5dfb7..19045a3 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -448,6 +448,8 @@ Follow system Light Dark + Black + Dark with pure black backgrounds, for AMOLED screens Dynamic colour Use colours from your wallpaper Notifications diff --git a/app/src/test/java/de/jeanlucmakiola/agendula/widget/WidgetDataTest.kt b/app/src/test/java/de/jeanlucmakiola/agendula/widget/WidgetDataTest.kt index 1951ca0..a054a77 100644 --- a/app/src/test/java/de/jeanlucmakiola/agendula/widget/WidgetDataTest.kt +++ b/app/src/test/java/de/jeanlucmakiola/agendula/widget/WidgetDataTest.kt @@ -210,6 +210,7 @@ class WidgetDataTest { fun `no override resolves to whatever the app's own theme means`() { assertThat(readyWithTheme(ThemeMode.DARK).effectiveThemeMode(null)).isEqualTo(WidgetThemeMode.DARK) assertThat(readyWithTheme(ThemeMode.SYSTEM).effectiveThemeMode(null)).isEqualTo(WidgetThemeMode.SYSTEM) + assertThat(readyWithTheme(ThemeMode.BLACK).effectiveThemeMode(null)).isEqualTo(WidgetThemeMode.DARK) } @Test