Compare commits
2
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
0d30c0e0a4 | ||
|
|
d9dc358883 |
No files matched your search
@@ -7,6 +7,11 @@ All notable changes to this project are documented here. The format follows
|
|||||||
|
|
||||||
## [Unreleased]
|
## [Unreleased]
|
||||||
|
|
||||||
|
### Added
|
||||||
|
- A "Description in reminders" setting under Notifications sets how much of a
|
||||||
|
task's description a reminder shows: none, a short preview of up to three
|
||||||
|
lines (the new default), or all of it as before (#156).
|
||||||
|
|
||||||
## [1.3.1] - 2026-11-09
|
## [1.3.1] - 2026-11-09
|
||||||
|
|
||||||
### Changed
|
### Changed
|
||||||
|
|||||||
@@ -54,6 +54,9 @@ fun TimeFormatPref.is24Hour(systemIs24Hour: Boolean): Boolean = when (this) {
|
|||||||
TimeFormatPref.TWENTY_FOUR_HOUR -> true
|
TimeFormatPref.TWENTY_FOUR_HOUR -> true
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/** How much of a task's description a due reminder shows when expanded. */
|
||||||
|
enum class ReminderDescription { HIDDEN, SHORT, FULL }
|
||||||
|
|
||||||
const val DEFAULT_SNOOZE_MINUTES = 10
|
const val DEFAULT_SNOOZE_MINUTES = 10
|
||||||
/** Done starts folded away, to keep the open tasks in focus. */ val DEFAULT_COLLAPSED_SECTIONS: Set<TaskSection> = setOf(TaskSection.COMPLETED)
|
/** Done starts folded away, to keep the open tasks in focus. */ val DEFAULT_COLLAPSED_SECTIONS: Set<TaskSection> = setOf(TaskSection.COMPLETED)
|
||||||
|
|
||||||
@@ -103,6 +106,7 @@ data class Settings(
|
|||||||
val sortOrder: TaskSortOrder = TaskSortOrder.DUE,
|
val sortOrder: TaskSortOrder = TaskSortOrder.DUE,
|
||||||
/** How long a reminder's "Snooze" action puts it off. */
|
/** How long a reminder's "Snooze" action puts it off. */
|
||||||
val snoozeMinutes: Int = DEFAULT_SNOOZE_MINUTES,
|
val snoozeMinutes: Int = DEFAULT_SNOOZE_MINUTES,
|
||||||
|
val reminderDescription: ReminderDescription = ReminderDescription.SHORT,
|
||||||
val timeFormat: TimeFormatPref = TimeFormatPref.AUTO,
|
val timeFormat: TimeFormatPref = TimeFormatPref.AUTO,
|
||||||
/** The first day of the week; `null` follows the locale. */
|
/** The first day of the week; `null` follows the locale. */
|
||||||
val weekStart: DayOfWeek? = null,
|
val weekStart: DayOfWeek? = null,
|
||||||
@@ -171,6 +175,9 @@ class SettingsPrefs @Inject constructor(
|
|||||||
sortOrder = p[SORT_ORDER]?.let { runCatching { TaskSortOrder.valueOf(it) }.getOrNull() }
|
sortOrder = p[SORT_ORDER]?.let { runCatching { TaskSortOrder.valueOf(it) }.getOrNull() }
|
||||||
?: TaskSortOrder.DUE,
|
?: TaskSortOrder.DUE,
|
||||||
snoozeMinutes = p[SNOOZE_MINUTES]?.takeIf { it > 0 } ?: DEFAULT_SNOOZE_MINUTES,
|
snoozeMinutes = p[SNOOZE_MINUTES]?.takeIf { it > 0 } ?: DEFAULT_SNOOZE_MINUTES,
|
||||||
|
reminderDescription = p[REMINDER_DESCRIPTION]
|
||||||
|
?.let { runCatching { ReminderDescription.valueOf(it) }.getOrNull() }
|
||||||
|
?: ReminderDescription.SHORT,
|
||||||
timeFormat = p[TIME_FORMAT]?.let { runCatching { TimeFormatPref.valueOf(it) }.getOrNull() }
|
timeFormat = p[TIME_FORMAT]?.let { runCatching { TimeFormatPref.valueOf(it) }.getOrNull() }
|
||||||
?: TimeFormatPref.AUTO,
|
?: TimeFormatPref.AUTO,
|
||||||
autofocusTitle = p[AUTOFOCUS_TITLE] ?: true,
|
autofocusTitle = p[AUTOFOCUS_TITLE] ?: true,
|
||||||
@@ -199,6 +206,9 @@ class SettingsPrefs @Inject constructor(
|
|||||||
|
|
||||||
suspend fun setSnoozeMinutes(minutes: Int) = dataStore.edit { it[SNOOZE_MINUTES] = minutes.coerceAtLeast(1) }
|
suspend fun setSnoozeMinutes(minutes: Int) = dataStore.edit { it[SNOOZE_MINUTES] = minutes.coerceAtLeast(1) }
|
||||||
|
|
||||||
|
suspend fun setReminderDescription(pref: ReminderDescription) =
|
||||||
|
dataStore.edit { it[REMINDER_DESCRIPTION] = pref.name }
|
||||||
|
|
||||||
suspend fun setSyncIntervalMinutes(minutes: Int) = dataStore.edit { it[SYNC_INTERVAL] = minutes }
|
suspend fun setSyncIntervalMinutes(minutes: Int) = dataStore.edit { it[SYNC_INTERVAL] = minutes }
|
||||||
|
|
||||||
suspend fun setPushEnabled(enabled: Boolean) = dataStore.edit { it[PUSH_ENABLED] = enabled }
|
suspend fun setPushEnabled(enabled: Boolean) = dataStore.edit { it[PUSH_ENABLED] = enabled }
|
||||||
@@ -345,6 +355,7 @@ class SettingsPrefs @Inject constructor(
|
|||||||
val DEFAULT_EDIT_FIELDS = stringSetPreferencesKey("default_edit_fields")
|
val DEFAULT_EDIT_FIELDS = stringSetPreferencesKey("default_edit_fields")
|
||||||
val SORT_ORDER = stringPreferencesKey("sort_order")
|
val SORT_ORDER = stringPreferencesKey("sort_order")
|
||||||
val SNOOZE_MINUTES = intPreferencesKey("snooze_minutes")
|
val SNOOZE_MINUTES = intPreferencesKey("snooze_minutes")
|
||||||
|
val REMINDER_DESCRIPTION = stringPreferencesKey("reminder_description")
|
||||||
val TIME_FORMAT = stringPreferencesKey("time_format")
|
val TIME_FORMAT = stringPreferencesKey("time_format")
|
||||||
val WEEK_START = stringPreferencesKey("week_start")
|
val WEEK_START = stringPreferencesKey("week_start")
|
||||||
val AUTOFOCUS_TITLE = booleanPreferencesKey("autofocus_title")
|
val AUTOFOCUS_TITLE = booleanPreferencesKey("autofocus_title")
|
||||||
|
|||||||
@@ -18,6 +18,7 @@ import androidx.core.content.ContextCompat
|
|||||||
import androidx.core.content.edit
|
import androidx.core.content.edit
|
||||||
import de.jeanlucmakiola.agendula.MainActivity
|
import de.jeanlucmakiola.agendula.MainActivity
|
||||||
import de.jeanlucmakiola.agendula.R
|
import de.jeanlucmakiola.agendula.R
|
||||||
|
import de.jeanlucmakiola.agendula.data.prefs.ReminderDescription
|
||||||
import de.jeanlucmakiola.agendula.data.prefs.Settings
|
import de.jeanlucmakiola.agendula.data.prefs.Settings
|
||||||
import de.jeanlucmakiola.agendula.domain.readableDescription
|
import de.jeanlucmakiola.agendula.domain.readableDescription
|
||||||
import de.jeanlucmakiola.agendula.data.prefs.is24Hour
|
import de.jeanlucmakiola.agendula.data.prefs.is24Hour
|
||||||
@@ -86,7 +87,11 @@ class TaskNotifier @Inject constructor(
|
|||||||
),
|
),
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
val description = task.description?.readableDescription()?.trim()?.takeIf { it.isNotBlank() }
|
val description = reminderBody(
|
||||||
|
task.description?.readableDescription(),
|
||||||
|
settings.reminderDescription,
|
||||||
|
lineChars = (NOTIFICATION_LINE_CHARS / context.resources.configuration.fontScale).toInt(),
|
||||||
|
)
|
||||||
val tapIntent = PendingIntent.getActivity(
|
val tapIntent = PendingIntent.getActivity(
|
||||||
context,
|
context,
|
||||||
task.taskId.toInt(),
|
task.taskId.toInt(),
|
||||||
@@ -277,3 +282,96 @@ internal fun expandedText(dueLine: String?, description: String?): String? = whe
|
|||||||
dueLine == null -> description
|
dueLine == null -> description
|
||||||
else -> "$dueLine\n\n$description"
|
else -> "$dueLine\n\n$description"
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/** Rough characters per line of an expanded notification on a phone, at the default font size. */
|
||||||
|
private const val NOTIFICATION_LINE_CHARS = 40
|
||||||
|
|
||||||
|
/** The description a reminder shows, as [mode] asks; null when there is none to show. */
|
||||||
|
internal fun reminderBody(
|
||||||
|
description: String?,
|
||||||
|
mode: ReminderDescription,
|
||||||
|
lineChars: Int = NOTIFICATION_LINE_CHARS,
|
||||||
|
): String? {
|
||||||
|
val text = description?.trim()?.takeIf { it.isNotBlank() } ?: return null
|
||||||
|
return when (mode) {
|
||||||
|
ReminderDescription.HIDDEN -> null
|
||||||
|
ReminderDescription.SHORT -> shortDescription(text, lineChars = lineChars)
|
||||||
|
ReminderDescription.FULL -> text
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* [description] cut to about [maxLines] lines of an expanded notification,
|
||||||
|
* ending in an ellipsis when anything was left out. Blank lines are dropped,
|
||||||
|
* and a long line counts as the lines it wraps to.
|
||||||
|
*/
|
||||||
|
internal fun shortDescription(
|
||||||
|
description: String,
|
||||||
|
maxLines: Int = 3,
|
||||||
|
lineChars: Int = NOTIFICATION_LINE_CHARS,
|
||||||
|
): String {
|
||||||
|
val perLine = lineChars.coerceAtLeast(2)
|
||||||
|
val lines = description.lines().map { it.trimEnd() }.filter { it.isNotBlank() }
|
||||||
|
val kept = mutableListOf<String>()
|
||||||
|
var rowsLeft = maxLines
|
||||||
|
for ((index, line) in lines.withIndex()) {
|
||||||
|
val width = displayWidth(line)
|
||||||
|
val rows = (width + perLine - 1) / perLine
|
||||||
|
val last = index == lines.lastIndex
|
||||||
|
// The ellipsis after a line that ends the budget needs a cell of its own.
|
||||||
|
val fits = rows < rowsLeft || (rows == rowsLeft && (last || width < rows * perLine))
|
||||||
|
if (fits) {
|
||||||
|
kept += line
|
||||||
|
rowsLeft -= rows
|
||||||
|
if (rowsLeft == 0 && !last) return ellipsize(kept)
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
if (rowsLeft > 0) kept += cutAtWord(line, rowsLeft * perLine - 1)
|
||||||
|
return ellipsize(kept)
|
||||||
|
}
|
||||||
|
return kept.joinToString("\n")
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun ellipsize(lines: List<String>): String =
|
||||||
|
lines.joinToString("\n").trimEnd { it.isWhitespace() || it in ".,;:" } + "\u2026"
|
||||||
|
|
||||||
|
/** [line] cut to at most [max] cells, at a word break when there is one, never inside a character. */
|
||||||
|
private fun cutAtWord(line: String, max: Int): String {
|
||||||
|
var end = 0
|
||||||
|
var width = 0
|
||||||
|
while (end < line.length) {
|
||||||
|
val codePoint = line.codePointAt(end)
|
||||||
|
width += cellsOf(codePoint)
|
||||||
|
if (width > max) break
|
||||||
|
end += Character.charCount(codePoint)
|
||||||
|
}
|
||||||
|
if (end >= line.length || line[end].isWhitespace()) return line.substring(0, end)
|
||||||
|
val cut = line.substring(0, end)
|
||||||
|
val space = cut.indexOfLast { it.isWhitespace() }
|
||||||
|
return if (space > end / 2) cut.substring(0, space) else cut
|
||||||
|
}
|
||||||
|
|
||||||
|
/** How many cells [text] takes, wide characters (CJK, emoji) counting two. */
|
||||||
|
private fun displayWidth(text: String): Int {
|
||||||
|
var width = 0
|
||||||
|
var i = 0
|
||||||
|
while (i < text.length) {
|
||||||
|
val codePoint = text.codePointAt(i)
|
||||||
|
width += cellsOf(codePoint)
|
||||||
|
i += Character.charCount(codePoint)
|
||||||
|
}
|
||||||
|
return width
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun cellsOf(codePoint: Int): Int = when {
|
||||||
|
Character.isSupplementaryCodePoint(codePoint) -> 2
|
||||||
|
Character.UnicodeScript.of(codePoint) in WIDE_SCRIPTS -> 2
|
||||||
|
else -> 1
|
||||||
|
}
|
||||||
|
|
||||||
|
private val WIDE_SCRIPTS = setOf(
|
||||||
|
Character.UnicodeScript.HAN,
|
||||||
|
Character.UnicodeScript.HIRAGANA,
|
||||||
|
Character.UnicodeScript.KATAKANA,
|
||||||
|
Character.UnicodeScript.HANGUL,
|
||||||
|
)
|
||||||
@@ -39,6 +39,7 @@ import androidx.core.app.NotificationManagerCompat
|
|||||||
import androidx.core.content.ContextCompat
|
import androidx.core.content.ContextCompat
|
||||||
import androidx.core.net.toUri
|
import androidx.core.net.toUri
|
||||||
import de.jeanlucmakiola.agendula.R
|
import de.jeanlucmakiola.agendula.R
|
||||||
|
import de.jeanlucmakiola.agendula.data.prefs.ReminderDescription
|
||||||
import de.jeanlucmakiola.agendula.data.prefs.SNOOZE_PRESETS
|
import de.jeanlucmakiola.agendula.data.prefs.SNOOZE_PRESETS
|
||||||
import de.jeanlucmakiola.agendula.data.reminders.TaskNotifier
|
import de.jeanlucmakiola.agendula.data.reminders.TaskNotifier
|
||||||
import de.jeanlucmakiola.agendula.domain.TaskReminderOrder
|
import de.jeanlucmakiola.agendula.domain.TaskReminderOrder
|
||||||
@@ -113,6 +114,7 @@ internal fun NotificationsScreen(
|
|||||||
var showOffset by remember { mutableStateOf(false) }
|
var showOffset by remember { mutableStateOf(false) }
|
||||||
var showAllDay by remember { mutableStateOf(false) }
|
var showAllDay by remember { mutableStateOf(false) }
|
||||||
var showSnooze by remember { mutableStateOf(false) }
|
var showSnooze by remember { mutableStateOf(false) }
|
||||||
|
var showDescription by remember { mutableStateOf(false) }
|
||||||
var showAllDayTime by remember { mutableStateOf(false) }
|
var showAllDayTime by remember { mutableStateOf(false) }
|
||||||
var perListExpanded by remember { mutableStateOf(false) }
|
var perListExpanded by remember { mutableStateOf(false) }
|
||||||
var expandedLists by remember { mutableStateOf(emptySet<Long>()) }
|
var expandedLists by remember { mutableStateOf(emptySet<Long>()) }
|
||||||
@@ -178,9 +180,15 @@ internal fun NotificationsScreen(
|
|||||||
R.string.settings_allday_reminder_time_summary,
|
R.string.settings_allday_reminder_time_summary,
|
||||||
formatMinuteOfDay(state.settings.allDayReminderMinuteOfDay, LocalUse24HourFormat.current),
|
formatMinuteOfDay(state.settings.allDayReminderMinuteOfDay, LocalUse24HourFormat.current),
|
||||||
),
|
),
|
||||||
position = Position.Bottom,
|
position = Position.Middle,
|
||||||
onClick = { showAllDayTime = true },
|
onClick = { showAllDayTime = true },
|
||||||
)
|
)
|
||||||
|
GroupedRow(
|
||||||
|
title = stringResource(R.string.settings_reminder_description),
|
||||||
|
summary = reminderDescriptionLabel(state.settings.reminderDescription),
|
||||||
|
position = Position.Bottom,
|
||||||
|
onClick = { showDescription = true },
|
||||||
|
)
|
||||||
|
|
||||||
Spacer(Modifier.height(24.dp))
|
Spacer(Modifier.height(24.dp))
|
||||||
val batteryExempt = rememberBatteryOptimizationExempt(context)
|
val batteryExempt = rememberBatteryOptimizationExempt(context)
|
||||||
@@ -318,6 +326,17 @@ internal fun NotificationsScreen(
|
|||||||
onDismiss = { showSnooze = false },
|
onDismiss = { showSnooze = false },
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
if (showDescription) {
|
||||||
|
OptionPicker(
|
||||||
|
title = stringResource(R.string.settings_reminder_description),
|
||||||
|
options = ReminderDescription.entries,
|
||||||
|
selected = state.settings.reminderDescription,
|
||||||
|
label = { reminderDescriptionLabel(it) },
|
||||||
|
summary = { reminderDescriptionSummary(it) },
|
||||||
|
onSelect = viewModel::setReminderDescription,
|
||||||
|
onDismiss = { showDescription = false },
|
||||||
|
)
|
||||||
|
}
|
||||||
if (showAllDayTime) {
|
if (showAllDayTime) {
|
||||||
val minute = state.settings.allDayReminderMinuteOfDay
|
val minute = state.settings.allDayReminderMinuteOfDay
|
||||||
TimePickerAlert(
|
TimePickerAlert(
|
||||||
@@ -354,6 +373,24 @@ private fun snoozeLabel(minutes: Int): String =
|
|||||||
if (minutes % 60 == 0) pluralStringResource(R.plurals.duration_hours, minutes / 60, minutes / 60)
|
if (minutes % 60 == 0) pluralStringResource(R.plurals.duration_hours, minutes / 60, minutes / 60)
|
||||||
else pluralStringResource(R.plurals.duration_minutes, minutes, minutes)
|
else pluralStringResource(R.plurals.duration_minutes, minutes, minutes)
|
||||||
|
|
||||||
|
@Composable
|
||||||
|
private fun reminderDescriptionLabel(pref: ReminderDescription): String = stringResource(
|
||||||
|
when (pref) {
|
||||||
|
ReminderDescription.HIDDEN -> R.string.settings_reminder_description_hidden
|
||||||
|
ReminderDescription.SHORT -> R.string.settings_reminder_description_short
|
||||||
|
ReminderDescription.FULL -> R.string.settings_reminder_description_full
|
||||||
|
},
|
||||||
|
)
|
||||||
|
|
||||||
|
@Composable
|
||||||
|
private fun reminderDescriptionSummary(pref: ReminderDescription): String = stringResource(
|
||||||
|
when (pref) {
|
||||||
|
ReminderDescription.HIDDEN -> R.string.settings_reminder_description_hidden_summary
|
||||||
|
ReminderDescription.SHORT -> R.string.settings_reminder_description_short_summary
|
||||||
|
ReminderDescription.FULL -> R.string.settings_reminder_description_full_summary
|
||||||
|
},
|
||||||
|
)
|
||||||
|
|
||||||
/** Which list's override picker is open, and for which kind of task. */
|
/** Which list's override picker is open, and for which kind of task. */
|
||||||
private data class OverrideTarget(val listId: Long, val allDay: Boolean)
|
private data class OverrideTarget(val listId: Long, val allDay: Boolean)
|
||||||
|
|
||||||
|
|||||||
@@ -6,6 +6,7 @@ import dagger.hilt.android.lifecycle.HiltViewModel
|
|||||||
import de.jeanlucmakiola.agendula.data.appname.LauncherName
|
import de.jeanlucmakiola.agendula.data.appname.LauncherName
|
||||||
import de.jeanlucmakiola.agendula.data.appname.LauncherNameManager
|
import de.jeanlucmakiola.agendula.data.appname.LauncherNameManager
|
||||||
import de.jeanlucmakiola.agendula.data.di.IoDispatcher
|
import de.jeanlucmakiola.agendula.data.di.IoDispatcher
|
||||||
|
import de.jeanlucmakiola.agendula.data.prefs.ReminderDescription
|
||||||
import de.jeanlucmakiola.agendula.data.prefs.Settings
|
import de.jeanlucmakiola.agendula.data.prefs.Settings
|
||||||
import de.jeanlucmakiola.agendula.data.prefs.SettingsPrefs
|
import de.jeanlucmakiola.agendula.data.prefs.SettingsPrefs
|
||||||
import de.jeanlucmakiola.agendula.data.prefs.ThemeMode
|
import de.jeanlucmakiola.agendula.data.prefs.ThemeMode
|
||||||
@@ -259,6 +260,7 @@ class SettingsViewModel @Inject constructor(
|
|||||||
fun setTimeFormat(pref: TimeFormatPref) = viewModelScope.launch { prefs.setTimeFormat(pref) }
|
fun setTimeFormat(pref: TimeFormatPref) = viewModelScope.launch { prefs.setTimeFormat(pref) }
|
||||||
fun setWeekStart(day: DayOfWeek?) = viewModelScope.launch { prefs.setWeekStart(day) }
|
fun setWeekStart(day: DayOfWeek?) = viewModelScope.launch { prefs.setWeekStart(day) }
|
||||||
fun setSnoozeMinutes(minutes: Int) = viewModelScope.launch { prefs.setSnoozeMinutes(minutes) }
|
fun setSnoozeMinutes(minutes: Int) = viewModelScope.launch { prefs.setSnoozeMinutes(minutes) }
|
||||||
|
fun setReminderDescription(pref: ReminderDescription) = viewModelScope.launch { prefs.setReminderDescription(pref) }
|
||||||
fun setShowAddSubtaskRow(show: Boolean) = viewModelScope.launch { prefs.setShowAddSubtaskRow(show) }
|
fun setShowAddSubtaskRow(show: Boolean) = viewModelScope.launch { prefs.setShowAddSubtaskRow(show) }
|
||||||
fun setBottomAddBar(enabled: Boolean) = viewModelScope.launch { prefs.setBottomAddBar(enabled) }
|
fun setBottomAddBar(enabled: Boolean) = viewModelScope.launch { prefs.setBottomAddBar(enabled) }
|
||||||
fun setDefaultStartToday(enabled: Boolean) = viewModelScope.launch { prefs.setDefaultStartToday(enabled) }
|
fun setDefaultStartToday(enabled: Boolean) = viewModelScope.launch { prefs.setDefaultStartToday(enabled) }
|
||||||
|
|||||||
@@ -470,6 +470,13 @@
|
|||||||
<string name="settings_exact_alarms_blocked">Blocked — tap to allow exact reminders</string>
|
<string name="settings_exact_alarms_blocked">Blocked — tap to allow exact reminders</string>
|
||||||
<string name="settings_reliable_delivery">Reliable delivery</string>
|
<string name="settings_reliable_delivery">Reliable delivery</string>
|
||||||
<string name="settings_snooze_duration">Snooze duration</string>
|
<string name="settings_snooze_duration">Snooze duration</string>
|
||||||
|
<string name="settings_reminder_description">Description in reminders</string>
|
||||||
|
<string name="settings_reminder_description_hidden">Hidden</string>
|
||||||
|
<string name="settings_reminder_description_short">Short</string>
|
||||||
|
<string name="settings_reminder_description_full">Full</string>
|
||||||
|
<string name="settings_reminder_description_hidden_summary">Only the title and due time</string>
|
||||||
|
<string name="settings_reminder_description_short_summary">A short preview of the description</string>
|
||||||
|
<string name="settings_reminder_description_full_summary">The whole description</string>
|
||||||
<string name="settings_reliable_delivery_hint">Android may delay reminders to save battery. Exempt Agendula so they arrive on time.</string>
|
<string name="settings_reliable_delivery_hint">Android may delay reminders to save battery. Exempt Agendula so they arrive on time.</string>
|
||||||
<string name="settings_reliable_delivery_exempt">Exempt from battery optimisation — reminders arrive on time.</string>
|
<string name="settings_reliable_delivery_exempt">Exempt from battery optimisation — reminders arrive on time.</string>
|
||||||
<string name="settings_default_list">Default list</string>
|
<string name="settings_default_list">Default list</string>
|
||||||
|
|||||||
@@ -0,0 +1,99 @@
|
|||||||
|
package de.jeanlucmakiola.agendula.data.reminders
|
||||||
|
|
||||||
|
import com.google.common.truth.Truth.assertThat
|
||||||
|
import de.jeanlucmakiola.agendula.data.prefs.ReminderDescription
|
||||||
|
import org.junit.jupiter.api.Test
|
||||||
|
|
||||||
|
class ShortDescriptionTest {
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun `short text is left alone`() {
|
||||||
|
assertThat(shortDescription("Buy oat milk")).isEqualTo("Buy oat milk")
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun `three short lines fit`() {
|
||||||
|
assertThat(shortDescription("one\ntwo\nthree")).isEqualTo("one\ntwo\nthree")
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun `lines past the third are cut with an ellipsis`() {
|
||||||
|
assertThat(shortDescription("one\ntwo\nthree\nfour\nfive")).isEqualTo("one\ntwo\nthree…")
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun `blank lines take no room`() {
|
||||||
|
assertThat(shortDescription("one\n\n\ntwo\n \nthree")).isEqualTo("one\ntwo\nthree")
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun `a long paragraph is cut at a word within the line budget`() {
|
||||||
|
val text = List(40) { "word" }.joinToString(" ")
|
||||||
|
val short = shortDescription(text, maxLines = 3, lineChars = 40)
|
||||||
|
assertThat(short).endsWith("word…")
|
||||||
|
assertThat(short.length).isAtMost(3 * 40)
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun `a wrapping line counts as the lines it takes`() {
|
||||||
|
val long = "a".repeat(70)
|
||||||
|
val short = shortDescription("$long\nsecond\nthird", maxLines = 3, lineChars = 40)
|
||||||
|
assertThat(short).isEqualTo("$long\nsecond…")
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun `a word with no break is cut hard`() {
|
||||||
|
val short = shortDescription("x".repeat(200), maxLines = 3, lineChars = 40)
|
||||||
|
assertThat(short).isEqualTo("x".repeat(119) + "…")
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun `trailing punctuation gives way to the ellipsis`() {
|
||||||
|
assertThat(shortDescription("one.\ntwo,\nthree.\nfour")).isEqualTo("one.\ntwo,\nthree…")
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun `a line that fills the budget exactly leaves room for the ellipsis`() {
|
||||||
|
val full = "a".repeat(120)
|
||||||
|
val short = shortDescription("$full\nmore", maxLines = 3, lineChars = 40)
|
||||||
|
assertThat(short).isEqualTo("a".repeat(119) + "…")
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun `a cut never splits an emoji`() {
|
||||||
|
val short = shortDescription("x".repeat(118) + "😀" + "y".repeat(10), maxLines = 3, lineChars = 40)
|
||||||
|
assertThat(short).isEqualTo("x".repeat(118) + "…")
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun `a word that ends right at the cut is kept`() {
|
||||||
|
val short = shortDescription("aaaa bbbb cccc", maxLines = 1, lineChars = 10)
|
||||||
|
assertThat(short).isEqualTo("aaaa bbbb…")
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun `tabs count as word breaks`() {
|
||||||
|
val short = shortDescription("Meeting\tagenda\tnotes", maxLines = 1, lineChars = 12)
|
||||||
|
assertThat(short).isEqualTo("Meeting…")
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun `no space is left before the ellipsis`() {
|
||||||
|
assertThat(shortDescription("see list :\nmore", maxLines = 1, lineChars = 40)).isEqualTo("see list…")
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun `wide characters take two cells`() {
|
||||||
|
val short = shortDescription("中".repeat(30), maxLines = 1, lineChars = 20)
|
||||||
|
assertThat(short).isEqualTo("中".repeat(9) + "…")
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun `the setting picks what the reminder shows`() {
|
||||||
|
val long = List(60) { "word" }.joinToString(" ")
|
||||||
|
assertThat(reminderBody(long, ReminderDescription.HIDDEN)).isNull()
|
||||||
|
assertThat(reminderBody(long, ReminderDescription.FULL)).isEqualTo(long)
|
||||||
|
assertThat(reminderBody(long, ReminderDescription.SHORT)).isEqualTo(shortDescription(long))
|
||||||
|
assertThat(reminderBody(" ", ReminderDescription.FULL)).isNull()
|
||||||
|
}
|
||||||
|
}
|
||||||
Reference in new issue
Block a user