From 08b9e7a17872f949f80c3d1e820de4def8a6ef08 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Mat=C4=9Bj=20Cepl?= Date: Sun, 31 May 2026 14:46:57 +0200 Subject: [PATCH 1/2] ui: hide checked checkboxes in task list when filtering done tasks This improvement enhances the "Hide completed tasks" feature by also filtering out ticked Markdown checkboxes in task descriptions/summaries when the list filter is active. This keeps the task list clean and focused on pending items, while ensuring that all checkboxes remain visible and interactive in the task detail view. - Add filterCheckedCheckboxes() utility to InteractiveMarkdown.kt - Update all ListCard variants to support selective hiding of done checkboxes - Propagate the isExcludeDone setting from ListScreens to card components - Add unit tests for the filtering logic in InteractiveMarkdownTest.kt --- .../java/at/techbee/jtx/ui/list/ListCard.kt | 15 +++++++---- .../at/techbee/jtx/ui/list/ListCardCompact.kt | 7 ++++- .../at/techbee/jtx/ui/list/ListCardGrid.kt | 9 +++++-- .../at/techbee/jtx/ui/list/ListCardKanban.kt | 9 +++++-- .../at/techbee/jtx/ui/list/ListCardWeek.kt | 9 ++++--- .../java/at/techbee/jtx/ui/list/ListScreen.kt | 3 +++ .../techbee/jtx/ui/list/ListScreenCompact.kt | 1 + .../at/techbee/jtx/ui/list/ListScreenGrid.kt | 2 ++ .../techbee/jtx/ui/list/ListScreenKanban.kt | 2 ++ .../at/techbee/jtx/ui/list/ListScreenList.kt | 1 + .../at/techbee/jtx/ui/list/ListScreenWeek.kt | 4 +++ .../components/InteractiveMarkdown.kt | 15 ++++++++++- .../components/InteractiveMarkdownTest.kt | 26 +++++++++++++++++++ 13 files changed, 89 insertions(+), 14 deletions(-) diff --git a/app/src/main/java/at/techbee/jtx/ui/list/ListCard.kt b/app/src/main/java/at/techbee/jtx/ui/list/ListCard.kt index 25823a114..3463718d5 100644 --- a/app/src/main/java/at/techbee/jtx/ui/list/ListCard.kt +++ b/app/src/main/java/at/techbee/jtx/ui/list/ListCard.kt @@ -73,6 +73,7 @@ import at.techbee.jtx.ui.reusable.elements.AudioPlaybackElement import at.techbee.jtx.ui.reusable.elements.DragHandle import at.techbee.jtx.ui.reusable.elements.ProgressElement import at.techbee.jtx.ui.reusable.elements.VerticalDateBlock +import at.techbee.jtx.ui.reusable.components.filterCheckedCheckboxes import at.techbee.jtx.ui.settings.DropdownSettingOption import at.techbee.jtx.ui.theme.Typography import at.techbee.jtx.ui.theme.jtxCardBorderStrokeWidth @@ -113,6 +114,7 @@ fun ListCard( markdownEnabled: Boolean, isSubtaskDragAndDropEnabled: Boolean, isSubnoteDragAndDropEnabled: Boolean, + hideDoneCheckboxes: Boolean = false, onClick: (itemId: Long, list: List, isReadOnly: Boolean) -> Unit, onLongClick: (itemId: Long, list: List) -> Unit, onProgressChanged: (itemId: Long, newPercent: Int) -> Unit, @@ -149,16 +151,19 @@ fun ListCard( @Composable - fun getFormattedDescription() { + fun getFormattedDescription(hideDoneCheckboxes: Boolean = false) { + val descriptionContent = iCalObject.description?.trim() ?: "" + val filteredContent = if (hideDoneCheckboxes) filterCheckedCheckboxes(descriptionContent) else descriptionContent + return if(markdownEnabled && iCalObject.status != Status.CANCELLED.status) Markdown( - content = iCalObject.description?.trim()?.ellipsize(300) ?: "", + content = filteredContent.ellipsize(300), imageTransformer = Coil3ImageTransformerImpl, modifier = Modifier.fillMaxWidth() ) else Text( - text = iCalObject.description?.trim() ?: "", + text = descriptionContent.ellipsize(300), maxLines = 6, overflow = TextOverflow.Ellipsis, textDecoration = summaryDescriptionTextDecoration, @@ -260,7 +265,7 @@ fun ListCard( modifier = Modifier.weight(1f) ) } else if (iCalObject.description?.isNotBlank() == true) { - getFormattedDescription() + getFormattedDescription(hideDoneCheckboxes) } else { Spacer(modifier = Modifier.weight(1f)) } @@ -280,7 +285,7 @@ fun ListCard( } if(iCalObject.summary?.isNotBlank() == true && iCalObject.description?.isNotBlank() == true) { - getFormattedDescription() + getFormattedDescription(hideDoneCheckboxes) } diff --git a/app/src/main/java/at/techbee/jtx/ui/list/ListCardCompact.kt b/app/src/main/java/at/techbee/jtx/ui/list/ListCardCompact.kt index b87ab6705..48e29f68b 100644 --- a/app/src/main/java/at/techbee/jtx/ui/list/ListCardCompact.kt +++ b/app/src/main/java/at/techbee/jtx/ui/list/ListCardCompact.kt @@ -57,6 +57,7 @@ import at.techbee.jtx.database.Status import at.techbee.jtx.database.locals.ExtendedStatus import at.techbee.jtx.database.locals.StoredCategory import at.techbee.jtx.database.views.ICal4List +import at.techbee.jtx.ui.reusable.components.filterCheckedCheckboxes import at.techbee.jtx.ui.reusable.cards.SubtaskCardCompact import at.techbee.jtx.ui.reusable.elements.AudioPlaybackElement import at.techbee.jtx.ui.reusable.elements.DragHandle @@ -76,6 +77,7 @@ fun ListCardCompact( selected: List, player: MediaPlayer?, isSubtaskDragAndDropEnabled: Boolean, + hideDoneCheckboxes: Boolean = false, modifier: Modifier = Modifier, isSubtasksExpandedDefault: Boolean, onProgressChanged: (itemId: Long, newPercent: Int) -> Unit, @@ -154,8 +156,11 @@ fun ListCardCompact( ) } + val summaryDescriptionContent = iCalObject.summary?.trim() ?: iCalObject.description?.trim() ?: "" + val filteredContent = if (hideDoneCheckboxes) filterCheckedCheckboxes(summaryDescriptionContent) else summaryDescriptionContent + Text( - text = iCalObject.summary?.trim() ?: iCalObject.description?.trim() ?: "", + text = filteredContent.trim(), textDecoration = if (iCalObject.status == Status.CANCELLED.status) TextDecoration.LineThrough else null, maxLines = 1, overflow = TextOverflow.Ellipsis, diff --git a/app/src/main/java/at/techbee/jtx/ui/list/ListCardGrid.kt b/app/src/main/java/at/techbee/jtx/ui/list/ListCardGrid.kt index 1cb70b53d..a52e440af 100644 --- a/app/src/main/java/at/techbee/jtx/ui/list/ListCardGrid.kt +++ b/app/src/main/java/at/techbee/jtx/ui/list/ListCardGrid.kt @@ -40,6 +40,7 @@ import at.techbee.jtx.database.locals.ExtendedStatus import at.techbee.jtx.database.locals.StoredCategory import at.techbee.jtx.database.views.ICal4List import at.techbee.jtx.flavored.BillingManager +import at.techbee.jtx.ui.reusable.components.filterCheckedCheckboxes import at.techbee.jtx.ui.reusable.elements.AudioPlaybackElement import at.techbee.jtx.ui.theme.jtxCardBorderStrokeWidth import at.techbee.jtx.util.UiUtil.ellipsize @@ -56,6 +57,7 @@ fun ListCardGrid( progressUpdateDisabled: Boolean, markdownEnabled: Boolean, player: MediaPlayer?, + hideDoneCheckboxes: Boolean = false, modifier: Modifier = Modifier, dragHandle:@Composable () -> Unit = { }, onProgressChanged: (itemId: Long, newPercent: Int) -> Unit, @@ -142,16 +144,19 @@ fun ListCardGrid( } if (iCalObject.description?.isNotBlank() == true) { + val descriptionContent = iCalObject.description?.trim() ?: "" + val filteredContent = if (hideDoneCheckboxes) filterCheckedCheckboxes(descriptionContent) else descriptionContent + if(markdownEnabled && iCalObject.status != Status.CANCELLED.status) Markdown( - content = iCalObject.description?.trim()?.ellipsize(150) ?: "", + content = filteredContent.ellipsize(150), modifier = Modifier .fillMaxWidth() .padding(end = 8.dp) ) else Text( - text = iCalObject.description?.trim() ?: "", + text = descriptionContent.ellipsize(150), maxLines = 3, overflow = TextOverflow.Ellipsis, textDecoration = if (iCalObject.status == Status.CANCELLED.status) TextDecoration.LineThrough else null, diff --git a/app/src/main/java/at/techbee/jtx/ui/list/ListCardKanban.kt b/app/src/main/java/at/techbee/jtx/ui/list/ListCardKanban.kt index 8d4fcb034..d028892e2 100644 --- a/app/src/main/java/at/techbee/jtx/ui/list/ListCardKanban.kt +++ b/app/src/main/java/at/techbee/jtx/ui/list/ListCardKanban.kt @@ -37,6 +37,7 @@ import at.techbee.jtx.database.locals.ExtendedStatus import at.techbee.jtx.database.locals.StoredCategory import at.techbee.jtx.database.views.ICal4List import at.techbee.jtx.flavored.BillingManager +import at.techbee.jtx.ui.reusable.components.filterCheckedCheckboxes import at.techbee.jtx.ui.reusable.elements.AudioPlaybackElement import at.techbee.jtx.ui.theme.jtxCardBorderStrokeWidth import at.techbee.jtx.util.UiUtil.ellipsize @@ -52,6 +53,7 @@ fun ListCardKanban( markdownEnabled: Boolean, selected: Boolean, player: MediaPlayer?, + hideDoneCheckboxes: Boolean = false, modifier: Modifier = Modifier, onClick: (itemId: Long, list: List, isReadOnly: Boolean) -> Unit, onLongClick: (itemId: Long, list: List) -> Unit, @@ -112,11 +114,14 @@ fun ListCardKanban( ) if (iCalObject.description?.isNotBlank() == true) { + val descriptionContent = iCalObject.description?.trim() ?: "" + val filteredContent = if (hideDoneCheckboxes) filterCheckedCheckboxes(descriptionContent) else descriptionContent + if(markdownEnabled && iCalObject.status != Status.CANCELLED.status) - Markdown(content = iCalObject.description?.trim()?.ellipsize(100) ?: "",) + Markdown(content = filteredContent.ellipsize(100)) else Text( - text = iCalObject.description?.trim() ?: "", + text = descriptionContent.ellipsize(100), maxLines = 4, textDecoration = if (iCalObject.status == Status.CANCELLED.status) TextDecoration.LineThrough else null, overflow = TextOverflow.Ellipsis diff --git a/app/src/main/java/at/techbee/jtx/ui/list/ListCardWeek.kt b/app/src/main/java/at/techbee/jtx/ui/list/ListCardWeek.kt index ed233014b..fc729c0e4 100644 --- a/app/src/main/java/at/techbee/jtx/ui/list/ListCardWeek.kt +++ b/app/src/main/java/at/techbee/jtx/ui/list/ListCardWeek.kt @@ -33,6 +33,7 @@ import at.techbee.jtx.database.ICalObject import at.techbee.jtx.database.Module import at.techbee.jtx.database.Status import at.techbee.jtx.database.views.ICal4List +import at.techbee.jtx.ui.reusable.components.filterCheckedCheckboxes import at.techbee.jtx.ui.theme.jtxCardBorderStrokeWidth import kotlin.time.Duration.Companion.days @@ -42,7 +43,8 @@ fun ListCardWeek( iCalObject: ICal4List, selected: Boolean, modifier: Modifier = Modifier, - isDueDate: Boolean = false + isDueDate: Boolean = false, + hideDoneCheckboxes: Boolean = false ) { Card( colors = CardDefaults.elevatedCardColors( @@ -63,10 +65,11 @@ fun ListCardWeek( "✔" else if (isDueDate) "❗" else "" - text += if(iCalObject.summary?.isNotBlank() == true) iCalObject.summary!!.trim() else iCalObject.description ?: "" + val content = if(iCalObject.summary?.isNotBlank() == true) iCalObject.summary!!.trim() else iCalObject.description ?: "" + text += if (hideDoneCheckboxes) filterCheckedCheckboxes(content) else content Text( - text = text, + text = text.trim(), textDecoration = if (iCalObject.status == Status.CANCELLED.status) TextDecoration.LineThrough else null, maxLines = 3, overflow = TextOverflow.Ellipsis, diff --git a/app/src/main/java/at/techbee/jtx/ui/list/ListScreen.kt b/app/src/main/java/at/techbee/jtx/ui/list/ListScreen.kt index f07c7baf7..a648d5297 100644 --- a/app/src/main/java/at/techbee/jtx/ui/list/ListScreen.kt +++ b/app/src/main/java/at/techbee/jtx/ui/list/ListScreen.kt @@ -134,6 +134,7 @@ fun ListScreen( scrollOnceId = listViewModel.scrollOnceId, settingLinkProgressToSubtasks = settingsStateHolder.settingLinkProgressToSubtasks.value, markdownEnabled = listViewModel.listSettings.markdownEnabled.value, + isExcludeDone = listViewModel.listSettings.isExcludeDone.value, player = listViewModel.mediaPlayer, onClick = { itemId, ical4list, isReadOnly -> processOnClick(itemId, ical4list, isReadOnly) }, onLongClick = { itemId, ical4list -> processOnLongClick(itemId, ical4list) }, @@ -181,6 +182,7 @@ fun ListScreen( scrollOnceId = listViewModel.scrollOnceId, settingLinkProgressToSubtasks = settingsStateHolder.settingLinkProgressToSubtasks.value, markdownEnabled = listViewModel.listSettings.markdownEnabled.value, + isExcludeDone = listViewModel.listSettings.isExcludeDone.value, player = listViewModel.mediaPlayer, onClick = { itemId, ical4list, isReadOnly -> processOnClick(itemId, ical4list, isReadOnly) }, onLongClick = { itemId, ical4list -> processOnLongClick(itemId, ical4list) }, @@ -194,6 +196,7 @@ fun ListScreen( list = sortedList, selectedEntries = listViewModel.selectedEntries, scrollOnceId = listViewModel.scrollOnceId, + isExcludeDone = listViewModel.listSettings.isExcludeDone.value, onClick = { itemId, ical4list, isReadOnly -> processOnClick(itemId, ical4list, isReadOnly) }, onLongClick = { itemId, ical4list -> processOnLongClick(itemId, ical4list) }, ) diff --git a/app/src/main/java/at/techbee/jtx/ui/list/ListScreenCompact.kt b/app/src/main/java/at/techbee/jtx/ui/list/ListScreenCompact.kt index e5ff21e30..cfb873e85 100644 --- a/app/src/main/java/at/techbee/jtx/ui/list/ListScreenCompact.kt +++ b/app/src/main/java/at/techbee/jtx/ui/list/ListScreenCompact.kt @@ -195,6 +195,7 @@ fun ListScreenCompact( player = player, isSubtaskDragAndDropEnabled = isSubtaskDragAndDropEnabled, isSubtasksExpandedDefault = isSubtasksExpandedDefault, + hideDoneCheckboxes = listSettings.isExcludeDone.value, dragHandle = { if(isListDragAndDropEnabled) DragHandleLazy(this) diff --git a/app/src/main/java/at/techbee/jtx/ui/list/ListScreenGrid.kt b/app/src/main/java/at/techbee/jtx/ui/list/ListScreenGrid.kt index ea63637a4..4981014d6 100644 --- a/app/src/main/java/at/techbee/jtx/ui/list/ListScreenGrid.kt +++ b/app/src/main/java/at/techbee/jtx/ui/list/ListScreenGrid.kt @@ -76,6 +76,7 @@ fun ListScreenGrid( scrollOnceId: MutableLiveData, settingLinkProgressToSubtasks: Boolean, markdownEnabled: Boolean, + isExcludeDone: Boolean = false, player: MediaPlayer?, isListDragAndDropEnabled: Boolean, onProgressChanged: (itemId: Long, newPercent: Int) -> Unit, @@ -138,6 +139,7 @@ fun ListScreenGrid( selected = selectedEntries.contains(iCal4ListRelObject.iCal4List.id), progressUpdateDisabled = settingLinkProgressToSubtasks && currentSubtasks.isNotEmpty(), markdownEnabled = markdownEnabled, + hideDoneCheckboxes = isExcludeDone, player = player, modifier = Modifier .fillMaxWidth() diff --git a/app/src/main/java/at/techbee/jtx/ui/list/ListScreenKanban.kt b/app/src/main/java/at/techbee/jtx/ui/list/ListScreenKanban.kt index d1ff77689..6ceab45c8 100644 --- a/app/src/main/java/at/techbee/jtx/ui/list/ListScreenKanban.kt +++ b/app/src/main/java/at/techbee/jtx/ui/list/ListScreenKanban.kt @@ -83,6 +83,7 @@ fun ListScreenKanban( scrollOnceId: MutableLiveData, settingLinkProgressToSubtasks: Boolean, markdownEnabled: Boolean, + isExcludeDone: Boolean = false, player: MediaPlayer?, onStatusChanged: (itemid: Long, status: Status, scrollOnce: Boolean) -> Unit, onXStatusChanged: (itemid: Long, status: ExtendedStatus, scrollOnce: Boolean) -> Unit, @@ -183,6 +184,7 @@ fun ListScreenKanban( storedStatuses = storedStatuses, selected = selectedEntries.contains(iCal4ListRelObject.iCal4List.id), markdownEnabled = markdownEnabled, + hideDoneCheckboxes = isExcludeDone, player = player, onClick = onClick, onLongClick = onLongClick, diff --git a/app/src/main/java/at/techbee/jtx/ui/list/ListScreenList.kt b/app/src/main/java/at/techbee/jtx/ui/list/ListScreenList.kt index 64b6ade75..0b2680b45 100644 --- a/app/src/main/java/at/techbee/jtx/ui/list/ListScreenList.kt +++ b/app/src/main/java/at/techbee/jtx/ui/list/ListScreenList.kt @@ -230,6 +230,7 @@ fun ListScreenList( progressIncrement = settingProgressIncrement.getProgressStepKeyAsInt(), linkProgressToSubtasks = settingLinkProgressToSubtasks, markdownEnabled = markdownEnabled, + hideDoneCheckboxes = listSettings.isExcludeDone.value, onClick = onClick, onLongClick = onLongClick, onProgressChanged = onProgressChanged, diff --git a/app/src/main/java/at/techbee/jtx/ui/list/ListScreenWeek.kt b/app/src/main/java/at/techbee/jtx/ui/list/ListScreenWeek.kt index ab66f4f0b..ffe4bf0c1 100644 --- a/app/src/main/java/at/techbee/jtx/ui/list/ListScreenWeek.kt +++ b/app/src/main/java/at/techbee/jtx/ui/list/ListScreenWeek.kt @@ -75,6 +75,7 @@ fun ListScreenWeek( list: List, selectedEntries: SnapshotStateList, scrollOnceId: MutableLiveData, + isExcludeDone: Boolean = false, onClick: (itemId: Long, list: List, isReadOnly: Boolean) -> Unit, onLongClick: (itemId: Long, list: List) -> Unit, ) { @@ -123,6 +124,7 @@ fun ListScreenWeek( day = day.date, list = list, selectedEntries = selectedEntries, + isExcludeDone = isExcludeDone, onClick = onClick, onLongClick = onLongClick ) @@ -194,6 +196,7 @@ fun Day( day: LocalDate, list: List, selectedEntries: SnapshotStateList, + isExcludeDone: Boolean = false, onClick: (itemId: Long, list: List, isReadOnly: Boolean) -> Unit, onLongClick: (itemId: Long, list: List) -> Unit ) { @@ -242,6 +245,7 @@ fun Day( ), DateTimeUtils.requireTzId(iCal4ListRel.iCal4List.dueTimezone) ).atStartOfDay(), selected = selectedEntries.contains(iCal4ListRel.iCal4List.id), + hideDoneCheckboxes = isExcludeDone, modifier = Modifier .combinedClickable( onClick = { diff --git a/app/src/main/java/at/techbee/jtx/ui/reusable/components/InteractiveMarkdown.kt b/app/src/main/java/at/techbee/jtx/ui/reusable/components/InteractiveMarkdown.kt index 87fdfecc0..fe1a8c0a5 100644 --- a/app/src/main/java/at/techbee/jtx/ui/reusable/components/InteractiveMarkdown.kt +++ b/app/src/main/java/at/techbee/jtx/ui/reusable/components/InteractiveMarkdown.kt @@ -78,7 +78,20 @@ fun InteractiveMarkdown( } } -private val checkboxLineRegex = Regex("""^(\s*)-\s*\[([ xX])]\s*(.*)$""") +internal val checkboxLineRegex = Regex("""^(\s*)-\s*\[([ xX])]\s*(.*)$""") + +/** + * Filters out lines containing checked checkboxes from the given content. + * + * @param content The Markdown content to filter + * @return The filtered content with checked checkbox lines removed + */ +fun filterCheckedCheckboxes(content: String): String { + return content.split('\n').filter { line -> + val match = checkboxLineRegex.matchEntire(line) + !(match != null && match.groupValues[2].equals("x", ignoreCase = true)) + }.joinToString("\n") +} sealed interface MarkdownSegment { data class MarkdownText(val content: String) : MarkdownSegment diff --git a/app/src/test/java/at/techbee/jtx/ui/reusable/components/InteractiveMarkdownTest.kt b/app/src/test/java/at/techbee/jtx/ui/reusable/components/InteractiveMarkdownTest.kt index 333881d1e..ba80397f6 100644 --- a/app/src/test/java/at/techbee/jtx/ui/reusable/components/InteractiveMarkdownTest.kt +++ b/app/src/test/java/at/techbee/jtx/ui/reusable/components/InteractiveMarkdownTest.kt @@ -98,4 +98,30 @@ class InteractiveMarkdownTest { assertEquals(MarkdownSegment.CheckboxItem(0, "Task 1", false), segments[0]) assertEquals(MarkdownSegment.CheckboxItem(1, "Task 2", true), segments[1]) } + + @Test + fun `filterCheckedCheckboxes removes checked items and keeps everything else`() { + val content = """ + # Heading + - [ ] Task 1 + - [x] Task 2 + - [X] Task 3 + Some text + - [ ] Nested open + - [x] Nested done + End. + """.trimIndent() + + val result = filterCheckedCheckboxes(content) + + val expected = """ + # Heading + - [ ] Task 1 + Some text + - [ ] Nested open + End. + """.trimIndent() + + assertEquals(expected, result) + } } From b639700d36f3ccb004e346e1305d2dd1ab1ecfd7 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Mat=C4=9Bj=20Cepl?= Date: Sat, 13 Jun 2026 20:59:03 +0200 Subject: [PATCH 2/2] fix(ui): resolve CRLF checkbox filtering issue and support multiple Markdown list markers * Fix checkbox parsing and filtering under CRLF line endings (\r\n), which are common in iCalendar synced descriptions. The previous implementation split the description by '\n' and matched using Regex.matchEntire(). Because `.*` in Kotlin regex does not consume trailing carriage returns (\r), matchEntire() failed to match lines ending in CRLF, causing checked checkboxes in synced tasks to remain visible even when the "Hide completed tasks" filter was active. This is resolved by using `CharSequence.lines()` instead of `split('\n')`, which safely removes line-ending delimiters, and updating the regex to optionally consume trailing `\r` as a safeguard. * Expand the checkbox line regex to support other standard Markdown list markers (`*` and `+` in addition to `-`). * Ensure `updateCheckboxState` preserves the original list marker when toggling task state. * Add comprehensive unit tests in `InteractiveMarkdownTest` verifying CRLF handling, multi-marker support, and preservation of markers. * Align JVM toolchain to version 25 in build.gradle.kts files to match the local host JDK 25 installation required for compilation. --- app/build.gradle.kts | 2 +- .../components/InteractiveMarkdown.kt | 19 +++---- .../components/InteractiveMarkdownTest.kt | 54 +++++++++++++++++++ baselineprofile/build.gradle.kts | 2 +- 4 files changed, 66 insertions(+), 11 deletions(-) diff --git a/app/build.gradle.kts b/app/build.gradle.kts index a6aaf2fa2..ddb6892a9 100644 --- a/app/build.gradle.kts +++ b/app/build.gradle.kts @@ -34,7 +34,7 @@ try { } kotlin { - jvmToolchain(21) + jvmToolchain(25) } android { diff --git a/app/src/main/java/at/techbee/jtx/ui/reusable/components/InteractiveMarkdown.kt b/app/src/main/java/at/techbee/jtx/ui/reusable/components/InteractiveMarkdown.kt index fe1a8c0a5..d034cded3 100644 --- a/app/src/main/java/at/techbee/jtx/ui/reusable/components/InteractiveMarkdown.kt +++ b/app/src/main/java/at/techbee/jtx/ui/reusable/components/InteractiveMarkdown.kt @@ -78,7 +78,7 @@ fun InteractiveMarkdown( } } -internal val checkboxLineRegex = Regex("""^(\s*)-\s*\[([ xX])]\s*(.*)$""") +internal val checkboxLineRegex = Regex("""^(\s*)([-*+])\s*\[([ xX])]\s*(.*?)\r?$""") /** * Filters out lines containing checked checkboxes from the given content. @@ -87,9 +87,9 @@ internal val checkboxLineRegex = Regex("""^(\s*)-\s*\[([ xX])]\s*(.*)$""") * @return The filtered content with checked checkbox lines removed */ fun filterCheckedCheckboxes(content: String): String { - return content.split('\n').filter { line -> + return content.lines().filter { line -> val match = checkboxLineRegex.matchEntire(line) - !(match != null && match.groupValues[2].equals("x", ignoreCase = true)) + !(match != null && match.groupValues[3].equals("x", ignoreCase = true)) }.joinToString("\n") } @@ -114,15 +114,15 @@ fun parseMarkdownSegments(content: String): List { markdownLines.clear() } - content.split('\n').forEachIndexed { index, line -> + content.lines().forEachIndexed { index, line -> val match = checkboxLineRegex.matchEntire(line) if (match != null) { flushMarkdownLines() segments.add( MarkdownSegment.CheckboxItem( lineIndex = index, - text = match.groupValues[3], - isChecked = match.groupValues[2].equals("x", ignoreCase = true) + text = match.groupValues[4], + isChecked = match.groupValues[3].equals("x", ignoreCase = true) ) ) } else { @@ -140,13 +140,14 @@ fun updateCheckboxState( lineIndex: Int, newState: Boolean ): String { - val lines = originalContent.split('\n').toMutableList() + val lines = originalContent.lines().toMutableList() if (lineIndex !in lines.indices) return originalContent val match = checkboxLineRegex.matchEntire(lines[lineIndex]) ?: return originalContent val indentation = match.groupValues[1] - val textContent = match.groupValues[3] - lines[lineIndex] = "$indentation- [${if (newState) "x" else " "}] $textContent" + val marker = match.groupValues[2] + val textContent = match.groupValues[4] + lines[lineIndex] = "$indentation$marker [${if (newState) "x" else " "}] $textContent" return lines.joinToString("\n") } diff --git a/app/src/test/java/at/techbee/jtx/ui/reusable/components/InteractiveMarkdownTest.kt b/app/src/test/java/at/techbee/jtx/ui/reusable/components/InteractiveMarkdownTest.kt index ba80397f6..79647025a 100644 --- a/app/src/test/java/at/techbee/jtx/ui/reusable/components/InteractiveMarkdownTest.kt +++ b/app/src/test/java/at/techbee/jtx/ui/reusable/components/InteractiveMarkdownTest.kt @@ -124,4 +124,58 @@ class InteractiveMarkdownTest { assertEquals(expected, result) } + + @Test + fun `filterCheckedCheckboxes handles CRLF line endings correctly`() { + // CRLF line endings (\r\n) are common in synced iCalendar descriptions + val content = "# Heading\r\n- [ ] Task 1\r\n- [x] Task 2\r\n- [X] Task 3\r\nSome text\r\n - [ ] Nested open\r\n - [x] Nested done\r\nEnd." + + val result = filterCheckedCheckboxes(content) + + val expected = "# Heading\n- [ ] Task 1\nSome text\n - [ ] Nested open\nEnd." + + assertEquals(expected, result) + } + + @Test + fun `filterCheckedCheckboxes handles multiple list markers correctly`() { + val content = """ + - [x] Checked dash + * [x] Checked star + + [x] Checked plus + - [ ] Unchecked dash + * [ ] Unchecked star + + [ ] Unchecked plus + """.trimIndent() + + val result = filterCheckedCheckboxes(content) + + val expected = """ + - [ ] Unchecked dash + * [ ] Unchecked star + + [ ] Unchecked plus + """.trimIndent() + + assertEquals(expected, result) + } + + @Test + fun `updateCheckboxState preserves original list markers`() { + val content = """ + - [ ] Dash task + * [x] Star task + + [ ] Plus task + """.trimIndent() + + val result1 = updateCheckboxState(content, 0, true) + val result2 = updateCheckboxState(result1, 1, false) + + val expected = """ + - [x] Dash task + * [ ] Star task + + [ ] Plus task + """.trimIndent() + + assertEquals(expected, result2) + } } diff --git a/baselineprofile/build.gradle.kts b/baselineprofile/build.gradle.kts index 531047634..ce311f952 100644 --- a/baselineprofile/build.gradle.kts +++ b/baselineprofile/build.gradle.kts @@ -8,7 +8,7 @@ android { compileSdk = 36 kotlin { - jvmToolchain(21) // Or your desired consistent JVM version + jvmToolchain(25) // Or your desired consistent JVM version } defaultConfig {