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/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..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,20 @@ fun InteractiveMarkdown( } } -private 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. + * + * @param content The Markdown content to filter + * @return The filtered content with checked checkbox lines removed + */ +fun filterCheckedCheckboxes(content: String): String { + return content.lines().filter { line -> + val match = checkboxLineRegex.matchEntire(line) + !(match != null && match.groupValues[3].equals("x", ignoreCase = true)) + }.joinToString("\n") +} sealed interface MarkdownSegment { data class MarkdownText(val content: String) : MarkdownSegment @@ -101,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 { @@ -127,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 333881d1e..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 @@ -98,4 +98,84 @@ 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) + } + + @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 {