From 7650a62572649dc5b960506a804750288436d0f0 Mon Sep 17 00:00:00 2001 From: Patrick Lang <72232737+patrickunterwegs@users.noreply.github.com> Date: Fri, 2 Oct 2026 21:45:23 +0300 Subject: [PATCH 1/4] Added priority slider for tasks. Made NO_STATUS an explicit value that maps to null --- .../composeResources/values/strings.xml | 6 + .../screens/core/domain/IcalEntry.kt | 5 +- .../components/StatusWithProgressIcon.kt | 10 +- .../details/presentation/DetailsAction.kt | 1 + .../details/presentation/DetailsScreenRoot.kt | 2 + .../details/presentation/DetailsViewModel.kt | 19 +++- .../JournalStatusPickerBottomSheet.kt | 29 ++++- .../TaskStatusProgressPickerBottomSheet.kt | 103 +++++++++++++++++- 8 files changed, 160 insertions(+), 15 deletions(-) diff --git a/shared/src/commonMain/composeResources/values/strings.xml b/shared/src/commonMain/composeResources/values/strings.xml index c485118f..875c8670 100644 --- a/shared/src/commonMain/composeResources/values/strings.xml +++ b/shared/src/commonMain/composeResources/values/strings.xml @@ -250,6 +250,12 @@ In process Completed + Priority + Not specified + Low + Medium + High + Not authorized. Please check your server, username and password. Welcome to %1$s diff --git a/shared/src/commonMain/kotlin/at/techbee/spectacled/screens/core/domain/IcalEntry.kt b/shared/src/commonMain/kotlin/at/techbee/spectacled/screens/core/domain/IcalEntry.kt index 49e8918f..e5f9f13e 100644 --- a/shared/src/commonMain/kotlin/at/techbee/spectacled/screens/core/domain/IcalEntry.kt +++ b/shared/src/commonMain/kotlin/at/techbee/spectacled/screens/core/domain/IcalEntry.kt @@ -22,6 +22,7 @@ import spectacled.shared.generated.resources.status_draft import spectacled.shared.generated.resources.status_final import spectacled.shared.generated.resources.status_in_process import spectacled.shared.generated.resources.status_needs_action +import spectacled.shared.generated.resources.status_no_status import kotlin.time.ExperimentalTime import kotlin.uuid.ExperimentalUuidApi import kotlin.uuid.Uuid @@ -277,9 +278,11 @@ data class IcalEntry( enum class Status( - val rfcName: String, + val rfcName: String?, val stringRes: StringResource ) { + NO_STATUS(null, Res.string.status_no_status), + FINAL("FINAL", Res.string.status_final), DRAFT("DRAFT", Res.string.status_draft), diff --git a/shared/src/commonMain/kotlin/at/techbee/spectacled/screens/core/presentation/components/StatusWithProgressIcon.kt b/shared/src/commonMain/kotlin/at/techbee/spectacled/screens/core/presentation/components/StatusWithProgressIcon.kt index 8b117455..6ced1bcb 100644 --- a/shared/src/commonMain/kotlin/at/techbee/spectacled/screens/core/presentation/components/StatusWithProgressIcon.kt +++ b/shared/src/commonMain/kotlin/at/techbee/spectacled/screens/core/presentation/components/StatusWithProgressIcon.kt @@ -8,6 +8,7 @@ import androidx.compose.material.icons.Icons import androidx.compose.material.icons.outlined.Check import androidx.compose.material.icons.outlined.Close import androidx.compose.material.icons.outlined.QuestionMark +import androidx.compose.material.icons.outlined.Reviews import androidx.compose.material3.CircularProgressIndicator import androidx.compose.material3.Icon import androidx.compose.material3.MaterialTheme @@ -31,6 +32,7 @@ fun StatusWithProgressIcon( ) { val text = when(status) { + Status.NO_STATUS -> "?" Status.NEEDS_ACTION -> "0" Status.IN_PROCESS -> (percent?:1L).toString() Status.COMPLETED -> "100" @@ -38,8 +40,9 @@ fun StatusWithProgressIcon( } val icon = when(status) { + Status.NO_STATUS -> Icons.Outlined.QuestionMark Status.FINAL -> Icons.Outlined.Check - Status.DRAFT -> Icons.Outlined.QuestionMark + Status.DRAFT -> Icons.Outlined.Reviews Status.CANCELLED -> Icons.Outlined.Close else -> null } @@ -48,7 +51,7 @@ fun StatusWithProgressIcon( Status.FINAL, Status.COMPLETED -> 1f Status.DRAFT -> 0.33f Status.IN_PROCESS -> (percent?:1L)/100f - Status.NEEDS_ACTION, Status.CANCELLED -> 0f + Status.NO_STATUS, Status.NEEDS_ACTION, Status.CANCELLED -> 0f } Box(contentAlignment = Alignment.Center) { @@ -85,6 +88,9 @@ fun StatusWithProgressIcon( private fun StatusWithProgress_Icon_Preview() { AppTheme(spectacledVariant = SpectacledVariant.JOURNALS) { Column(verticalArrangement = Arrangement.spacedBy(8.dp)) { + + StatusWithProgressIcon(status = Status.NO_STATUS) + StatusWithProgressIcon(status = Status.FINAL) StatusWithProgressIcon(status = Status.DRAFT) StatusWithProgressIcon(status = Status.CANCELLED) diff --git a/shared/src/commonMain/kotlin/at/techbee/spectacled/screens/details/presentation/DetailsAction.kt b/shared/src/commonMain/kotlin/at/techbee/spectacled/screens/details/presentation/DetailsAction.kt index 29b6dc5f..ee5ef312 100644 --- a/shared/src/commonMain/kotlin/at/techbee/spectacled/screens/details/presentation/DetailsAction.kt +++ b/shared/src/commonMain/kotlin/at/techbee/spectacled/screens/details/presentation/DetailsAction.kt @@ -17,6 +17,7 @@ sealed interface DetailsAction { data class OnUpdateDtStart(val icsDateTime: IcsDateTime?): DetailsAction data class OnUpdateDue(val icsDateTime: IcsDateTime?): DetailsAction data class OnUpdateProgress(val percent: Long): DetailsAction + data class OnUpdatePriority(val priority: Long?): DetailsAction data class OnUpdateSubtaskProgress(val percent: Long, val subtaskIcalEntryId: Long): DetailsAction data class OnAddSubtask(val summary: String): DetailsAction data class OnUpdateUrl(val url: Url?): DetailsAction diff --git a/shared/src/commonMain/kotlin/at/techbee/spectacled/screens/details/presentation/DetailsScreenRoot.kt b/shared/src/commonMain/kotlin/at/techbee/spectacled/screens/details/presentation/DetailsScreenRoot.kt index a51bc517..2ea5803b 100644 --- a/shared/src/commonMain/kotlin/at/techbee/spectacled/screens/details/presentation/DetailsScreenRoot.kt +++ b/shared/src/commonMain/kotlin/at/techbee/spectacled/screens/details/presentation/DetailsScreenRoot.kt @@ -236,9 +236,11 @@ fun DetailsScreenRoot( TaskStatusProgressPickerBottomSheet( status = detailsState.icalEntry.status, percentComplete = detailsState.icalEntry.percentComplete, + priority = detailsState.icalEntry.priority, sheetState = rememberBottomSheetState(initialValue = SheetValue.Expanded), onStatusUpdated = { detailsViewModel.onAction(DetailsAction.OnUpdateStatus(it)) }, onProgressUpdated = { detailsViewModel.onAction(DetailsAction.OnUpdateProgress(it)) }, + onPriorityUpdated = { detailsViewModel.onAction(DetailsAction.OnUpdatePriority(it)) }, onDismiss = { detailsViewModel.onAction(DetailsAction.OnShowSheetOrDialog(null)) } ) DetailsSheetOrDialog.ADD_SUBTASKS -> diff --git a/shared/src/commonMain/kotlin/at/techbee/spectacled/screens/details/presentation/DetailsViewModel.kt b/shared/src/commonMain/kotlin/at/techbee/spectacled/screens/details/presentation/DetailsViewModel.kt index 4aacad7f..574bfd2a 100644 --- a/shared/src/commonMain/kotlin/at/techbee/spectacled/screens/details/presentation/DetailsViewModel.kt +++ b/shared/src/commonMain/kotlin/at/techbee/spectacled/screens/details/presentation/DetailsViewModel.kt @@ -344,6 +344,7 @@ class DetailsViewModel( is DetailsAction.OnPin -> { onPinIcalEntry(action.pin) } is DetailsAction.OnUpdateStatus -> { onUpdateStatus(action.status) } is DetailsAction.OnUpdateProgress -> { onUpdateTaskProgress(action.percent) } + is DetailsAction.OnUpdatePriority -> { onUpdatePriority(action.priority) } is DetailsAction.OnUpdateSubtaskProgress -> { onUpdateSubtaskProgress(action.percent, action.subtaskIcalEntryId) } is DetailsAction.OnAddSubtask -> { insertSubtask(action.summary) } is DetailsAction.OnNavigateToIcalEntryId -> { _state.update { it.copy(navigateToIcalEntryId = action.id) } } @@ -462,7 +463,7 @@ class DetailsViewModel( 100L -> 99L else -> it.icalEntry.percentComplete } - Status.NEEDS_ACTION -> 0L + Status.NO_STATUS, Status.NEEDS_ACTION -> 0L else -> it.icalEntry.percentComplete } } else 0, @@ -473,6 +474,22 @@ class DetailsViewModel( } } + private fun onUpdatePriority(newPriority: Long?) { + + if(!state.value.allowEditing()) + return + + _state.update { + it.copy( + icalEntry = it.icalEntry.copy( + priority = newPriority, + lastModified = IcsDateTime.now(), + syncState = it.icalEntry.syncState.afterLocalEdit() + ) + ) + } + } + private fun onUpdateColor(newColor: Color?) { diff --git a/shared/src/commonMain/kotlin/at/techbee/spectacled/screens/details/presentation/components/JournalStatusPickerBottomSheet.kt b/shared/src/commonMain/kotlin/at/techbee/spectacled/screens/details/presentation/components/JournalStatusPickerBottomSheet.kt index d4178099..78265be8 100644 --- a/shared/src/commonMain/kotlin/at/techbee/spectacled/screens/details/presentation/components/JournalStatusPickerBottomSheet.kt +++ b/shared/src/commonMain/kotlin/at/techbee/spectacled/screens/details/presentation/components/JournalStatusPickerBottomSheet.kt @@ -24,7 +24,6 @@ import at.techbee.spectacled.theme.AppTheme import org.jetbrains.compose.resources.stringResource import spectacled.shared.generated.resources.Res import spectacled.shared.generated.resources.done -import spectacled.shared.generated.resources.status_no_status @OptIn(ExperimentalMaterial3Api::class) @Composable @@ -45,10 +44,6 @@ fun JournalStatusPickerBottomSheet( Text(stringResource(Res.string.done)) } }, - menuActionLeft = { TextButton(onClick = { - onStatusUpdated(null) - onDismiss() - }) { Text(stringResource(Res.string.status_no_status)) } } ) { Column( horizontalAlignment = Alignment.CenterHorizontally, @@ -58,6 +53,14 @@ fun JournalStatusPickerBottomSheet( FlowRow( horizontalArrangement = Arrangement.spacedBy(8.dp, Alignment.CenterHorizontally) ) { + + FilterChip( + leadingIcon = { Status.NO_STATUS.StatusIcon(null) }, + selected = status == null, + onClick = { onStatusUpdated(null) }, + label = { Text(stringResource(Status.NO_STATUS.stringRes)) } + ) + val statusSet = setOf(Status.DRAFT, Status.FINAL, Status.CANCELLED) statusSet.forEach { selectableStatus -> @@ -90,4 +93,20 @@ private fun JournalStatusPickerBottomSheet_Preview() { } } +@OptIn(ExperimentalMaterial3Api::class) +@Preview(showBackground = true) +@Composable +private fun JournalStatusPickerBottomSheet_NO_STATUS_Preview() { + AppTheme(spectacledVariant = SpectacledVariant.JOURNALS) { + Scaffold { + JournalStatusPickerBottomSheet( + status = null, + sheetState = rememberBottomSheetState(initialValue = SheetValue.Expanded), + onStatusUpdated = {}, + onDismiss = {} + ) + } + } +} + diff --git a/shared/src/commonMain/kotlin/at/techbee/spectacled/screens/details/presentation/components/TaskStatusProgressPickerBottomSheet.kt b/shared/src/commonMain/kotlin/at/techbee/spectacled/screens/details/presentation/components/TaskStatusProgressPickerBottomSheet.kt index 4e580e08..34030fd1 100644 --- a/shared/src/commonMain/kotlin/at/techbee/spectacled/screens/details/presentation/components/TaskStatusProgressPickerBottomSheet.kt +++ b/shared/src/commonMain/kotlin/at/techbee/spectacled/screens/details/presentation/components/TaskStatusProgressPickerBottomSheet.kt @@ -6,8 +6,11 @@ import androidx.compose.foundation.layout.FlowRow import androidx.compose.foundation.layout.Row import androidx.compose.foundation.layout.fillMaxWidth import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.layout.width +import androidx.compose.material3.AssistChip import androidx.compose.material3.ExperimentalMaterial3Api import androidx.compose.material3.FilterChip +import androidx.compose.material3.HorizontalDivider import androidx.compose.material3.Scaffold import androidx.compose.material3.SheetState import androidx.compose.material3.SheetValue @@ -18,9 +21,16 @@ import androidx.compose.material3.TextButton import androidx.compose.material3.TriStateCheckbox import androidx.compose.material3.rememberBottomSheetState import androidx.compose.runtime.Composable +import androidx.compose.runtime.LaunchedEffect +import androidx.compose.runtime.getValue +import androidx.compose.runtime.mutableFloatStateOf +import androidx.compose.runtime.remember +import androidx.compose.runtime.setValue import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.state.ToggleableState +import androidx.compose.ui.text.style.TextAlign +import androidx.compose.ui.text.style.TextOverflow import androidx.compose.ui.tooling.preview.Preview import androidx.compose.ui.unit.dp import at.techbee.spectacled.SpectacledVariant @@ -31,16 +41,23 @@ import org.jetbrains.compose.resources.stringResource import spectacled.shared.generated.resources.Res import spectacled.shared.generated.resources.done import spectacled.shared.generated.resources.percent_complete -import spectacled.shared.generated.resources.status_no_status +import spectacled.shared.generated.resources.priority +import spectacled.shared.generated.resources.priority_Medium +import spectacled.shared.generated.resources.priority_high +import spectacled.shared.generated.resources.priority_low +import spectacled.shared.generated.resources.priority_not_specified +import kotlin.math.roundToInt @OptIn(ExperimentalMaterial3Api::class) @Composable fun TaskStatusProgressPickerBottomSheet( status: Status?, percentComplete: Long?, + priority: Long?, sheetState: SheetState, onProgressUpdated: (Long) -> Unit, onStatusUpdated: (Status?) -> Unit, + onPriorityUpdated: (Long?) -> Unit, onDismiss: () -> Unit ) { @@ -53,11 +70,7 @@ fun TaskStatusProgressPickerBottomSheet( ) { Text(stringResource(Res.string.done)) } - }, - menuActionLeft = { TextButton(onClick = { - onStatusUpdated(null) - onDismiss() - }) { Text(stringResource(Res.string.status_no_status)) } } + } ) { Column( horizontalAlignment = Alignment.CenterHorizontally, @@ -93,6 +106,14 @@ fun TaskStatusProgressPickerBottomSheet( FlowRow( horizontalArrangement = Arrangement.spacedBy(8.dp, Alignment.CenterHorizontally) ) { + + FilterChip( + leadingIcon = { Status.NO_STATUS.StatusIcon(null) }, + selected = status == null, + onClick = { onStatusUpdated(null) }, + label = { Text(stringResource(Status.NO_STATUS.stringRes)) } + ) + val statusSet = setOf(Status.NEEDS_ACTION, Status.IN_PROCESS, Status.COMPLETED, Status.CANCELLED) statusSet.forEach { selectableStatus -> @@ -104,6 +125,72 @@ fun TaskStatusProgressPickerBottomSheet( ) } } + + HorizontalDivider(modifier = Modifier.padding(horizontal = 8.dp, vertical = 4.dp)) + + Row( + horizontalArrangement = Arrangement.spacedBy(16.dp), + verticalAlignment = Alignment.CenterVertically, + modifier = Modifier.fillMaxWidth().padding(horizontal = 8.dp) + ) { + + Text(stringResource(Res.string.priority)) + + val currentPriorityStep = when (priority) { + null, 0L -> 0f + in 6L..9L -> 1f + 5L -> 2f + in 1L..4L -> 3f + else -> 0f + } + + // Local continuous value for smooth dragging + var sliderValue by remember(priority) { mutableFloatStateOf(currentPriorityStep) } + + LaunchedEffect(priority) { + sliderValue = currentPriorityStep + } + + Slider( + value = sliderValue, + valueRange = 0f..3f, + steps = 2, + onValueChange = { sliderValue = it }, + onValueChangeFinished = { + onPriorityUpdated( + when (sliderValue.roundToInt()) { + 0 -> null + 1 -> 9L + 2 -> 5L + 3 -> 1L + else -> null + } + ) + + }, + thumb = { + AssistChip( + onClick = {}, + label = { + Text( + text = stringResource(when (sliderValue.roundToInt()) { + 0 -> Res.string.priority_not_specified + 1 -> Res.string.priority_low + 2 -> Res.string.priority_Medium + 3 -> Res.string.priority_high + else -> Res.string.priority_not_specified + }), + maxLines = 1, + overflow = TextOverflow.Ellipsis, + textAlign = TextAlign.Center, + modifier = Modifier.width(72.dp) + ) + } + ) + }, + modifier = Modifier.weight(1f) + ) + } } } } @@ -119,9 +206,11 @@ private fun TaskStatusProgressPickerBottomSheet_Preview() { TaskStatusProgressPickerBottomSheet( status = null, percentComplete = 0L, + priority = 5, sheetState = rememberBottomSheetState(initialValue = SheetValue.Expanded), onStatusUpdated = {}, onProgressUpdated = {}, + onPriorityUpdated = {}, onDismiss = {} ) } @@ -137,9 +226,11 @@ private fun TaskStatusProgressPickerBottomSheet_inprocess_Preview() { TaskStatusProgressPickerBottomSheet( status = Status.IN_PROCESS, percentComplete = 33, + priority = null, sheetState = rememberBottomSheetState(initialValue = SheetValue.Expanded), onStatusUpdated = {}, onProgressUpdated = {}, + onPriorityUpdated = {}, onDismiss = {} ) } From bcf194e092b149ebc0f3f70566d9b89f4109124d Mon Sep 17 00:00:00 2001 From: Patrick Lang <72232737+patrickunterwegs@users.noreply.github.com> Date: Fri, 2 Oct 2026 22:18:12 +0300 Subject: [PATCH 2/4] improved code with Claude suggestions --- .../composeResources/values/strings.xml | 2 +- .../screens/core/domain/IcalEntry.kt | 2 - .../components/StatusWithProgressIcon.kt | 41 ++++++++----------- .../details/presentation/DetailsViewModel.kt | 2 +- .../JournalStatusPickerBottomSheet.kt | 6 ++- .../TaskStatusProgressPickerBottomSheet.kt | 35 ++++++++-------- 6 files changed, 40 insertions(+), 48 deletions(-) diff --git a/shared/src/commonMain/composeResources/values/strings.xml b/shared/src/commonMain/composeResources/values/strings.xml index 875c8670..d8afb58b 100644 --- a/shared/src/commonMain/composeResources/values/strings.xml +++ b/shared/src/commonMain/composeResources/values/strings.xml @@ -253,7 +253,7 @@ Priority Not specified Low - Medium + Medium High Not authorized. Please check your server, username and password. diff --git a/shared/src/commonMain/kotlin/at/techbee/spectacled/screens/core/domain/IcalEntry.kt b/shared/src/commonMain/kotlin/at/techbee/spectacled/screens/core/domain/IcalEntry.kt index e5f9f13e..e9a58f98 100644 --- a/shared/src/commonMain/kotlin/at/techbee/spectacled/screens/core/domain/IcalEntry.kt +++ b/shared/src/commonMain/kotlin/at/techbee/spectacled/screens/core/domain/IcalEntry.kt @@ -22,7 +22,6 @@ import spectacled.shared.generated.resources.status_draft import spectacled.shared.generated.resources.status_final import spectacled.shared.generated.resources.status_in_process import spectacled.shared.generated.resources.status_needs_action -import spectacled.shared.generated.resources.status_no_status import kotlin.time.ExperimentalTime import kotlin.uuid.ExperimentalUuidApi import kotlin.uuid.Uuid @@ -281,7 +280,6 @@ enum class Status( val rfcName: String?, val stringRes: StringResource ) { - NO_STATUS(null, Res.string.status_no_status), FINAL("FINAL", Res.string.status_final), DRAFT("DRAFT", Res.string.status_draft), diff --git a/shared/src/commonMain/kotlin/at/techbee/spectacled/screens/core/presentation/components/StatusWithProgressIcon.kt b/shared/src/commonMain/kotlin/at/techbee/spectacled/screens/core/presentation/components/StatusWithProgressIcon.kt index 6ced1bcb..0fe6847f 100644 --- a/shared/src/commonMain/kotlin/at/techbee/spectacled/screens/core/presentation/components/StatusWithProgressIcon.kt +++ b/shared/src/commonMain/kotlin/at/techbee/spectacled/screens/core/presentation/components/StatusWithProgressIcon.kt @@ -5,10 +5,10 @@ import androidx.compose.foundation.layout.Box import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.size import androidx.compose.material.icons.Icons -import androidx.compose.material.icons.outlined.Check -import androidx.compose.material.icons.outlined.Close -import androidx.compose.material.icons.outlined.QuestionMark -import androidx.compose.material.icons.outlined.Reviews +import androidx.compose.material.icons.outlined.CheckCircle +import androidx.compose.material.icons.outlined.Circle +import androidx.compose.material.icons.outlined.RemoveCircle +import androidx.compose.material.icons.outlined.Unpublished import androidx.compose.material3.CircularProgressIndicator import androidx.compose.material3.Icon import androidx.compose.material3.MaterialTheme @@ -27,12 +27,11 @@ import at.techbee.spectacled.theme.AppTheme @Composable fun StatusWithProgressIcon( - status: Status, + status: Status?, percent: Long? = null, ) { val text = when(status) { - Status.NO_STATUS -> "?" Status.NEEDS_ACTION -> "0" Status.IN_PROCESS -> (percent?:1L).toString() Status.COMPLETED -> "100" @@ -40,18 +39,17 @@ fun StatusWithProgressIcon( } val icon = when(status) { - Status.NO_STATUS -> Icons.Outlined.QuestionMark - Status.FINAL -> Icons.Outlined.Check - Status.DRAFT -> Icons.Outlined.Reviews - Status.CANCELLED -> Icons.Outlined.Close - else -> null + Status.FINAL -> Icons.Outlined.CheckCircle + Status.DRAFT -> Icons.Outlined.Unpublished + Status.CANCELLED -> Icons.Outlined.RemoveCircle + else -> Icons.Outlined.Circle } val progress = when(status) { Status.FINAL, Status.COMPLETED -> 1f Status.DRAFT -> 0.33f Status.IN_PROCESS -> (percent?:1L)/100f - Status.NO_STATUS, Status.NEEDS_ACTION, Status.CANCELLED -> 0f + null, Status.NEEDS_ACTION, Status.CANCELLED -> 0f } Box(contentAlignment = Alignment.Center) { @@ -64,17 +62,12 @@ fun StatusWithProgressIcon( fontWeight = FontWeight.Bold, fontSize = 9.sp ) - } - - icon?.let { - Icon( - imageVector = it, - contentDescription = null, - tint = ProgressIndicatorDefaults.circularColor, - modifier = Modifier.size(14.dp) - ) - } - + } ?: Icon( + imageVector = icon, + contentDescription = null, + tint = if(status == Status.CANCELLED) MaterialTheme.colorScheme.error else ProgressIndicatorDefaults.circularColor, + modifier = Modifier.size(14.dp) + ) CircularProgressIndicator( progress = { progress }, @@ -89,7 +82,7 @@ private fun StatusWithProgress_Icon_Preview() { AppTheme(spectacledVariant = SpectacledVariant.JOURNALS) { Column(verticalArrangement = Arrangement.spacedBy(8.dp)) { - StatusWithProgressIcon(status = Status.NO_STATUS) + StatusWithProgressIcon(status = null) StatusWithProgressIcon(status = Status.FINAL) StatusWithProgressIcon(status = Status.DRAFT) diff --git a/shared/src/commonMain/kotlin/at/techbee/spectacled/screens/details/presentation/DetailsViewModel.kt b/shared/src/commonMain/kotlin/at/techbee/spectacled/screens/details/presentation/DetailsViewModel.kt index 574bfd2a..2a793865 100644 --- a/shared/src/commonMain/kotlin/at/techbee/spectacled/screens/details/presentation/DetailsViewModel.kt +++ b/shared/src/commonMain/kotlin/at/techbee/spectacled/screens/details/presentation/DetailsViewModel.kt @@ -463,7 +463,7 @@ class DetailsViewModel( 100L -> 99L else -> it.icalEntry.percentComplete } - Status.NO_STATUS, Status.NEEDS_ACTION -> 0L + Status.NEEDS_ACTION -> 0L else -> it.icalEntry.percentComplete } } else 0, diff --git a/shared/src/commonMain/kotlin/at/techbee/spectacled/screens/details/presentation/components/JournalStatusPickerBottomSheet.kt b/shared/src/commonMain/kotlin/at/techbee/spectacled/screens/details/presentation/components/JournalStatusPickerBottomSheet.kt index 78265be8..c1e731ba 100644 --- a/shared/src/commonMain/kotlin/at/techbee/spectacled/screens/details/presentation/components/JournalStatusPickerBottomSheet.kt +++ b/shared/src/commonMain/kotlin/at/techbee/spectacled/screens/details/presentation/components/JournalStatusPickerBottomSheet.kt @@ -20,10 +20,12 @@ import androidx.compose.ui.unit.dp import at.techbee.spectacled.SpectacledVariant import at.techbee.spectacled.screens.core.domain.Status import at.techbee.spectacled.screens.core.presentation.components.BottomSheetWithMenu +import at.techbee.spectacled.screens.core.presentation.components.StatusWithProgressIcon import at.techbee.spectacled.theme.AppTheme import org.jetbrains.compose.resources.stringResource import spectacled.shared.generated.resources.Res import spectacled.shared.generated.resources.done +import spectacled.shared.generated.resources.status_no_status @OptIn(ExperimentalMaterial3Api::class) @Composable @@ -55,10 +57,10 @@ fun JournalStatusPickerBottomSheet( ) { FilterChip( - leadingIcon = { Status.NO_STATUS.StatusIcon(null) }, + leadingIcon = { StatusWithProgressIcon(null, null) }, selected = status == null, onClick = { onStatusUpdated(null) }, - label = { Text(stringResource(Status.NO_STATUS.stringRes)) } + label = { Text(stringResource(Res.string.status_no_status)) } ) val statusSet = setOf(Status.DRAFT, Status.FINAL, Status.CANCELLED) diff --git a/shared/src/commonMain/kotlin/at/techbee/spectacled/screens/details/presentation/components/TaskStatusProgressPickerBottomSheet.kt b/shared/src/commonMain/kotlin/at/techbee/spectacled/screens/details/presentation/components/TaskStatusProgressPickerBottomSheet.kt index 34030fd1..1182275d 100644 --- a/shared/src/commonMain/kotlin/at/techbee/spectacled/screens/details/presentation/components/TaskStatusProgressPickerBottomSheet.kt +++ b/shared/src/commonMain/kotlin/at/techbee/spectacled/screens/details/presentation/components/TaskStatusProgressPickerBottomSheet.kt @@ -21,7 +21,6 @@ import androidx.compose.material3.TextButton import androidx.compose.material3.TriStateCheckbox import androidx.compose.material3.rememberBottomSheetState import androidx.compose.runtime.Composable -import androidx.compose.runtime.LaunchedEffect import androidx.compose.runtime.getValue import androidx.compose.runtime.mutableFloatStateOf import androidx.compose.runtime.remember @@ -36,16 +35,18 @@ import androidx.compose.ui.unit.dp import at.techbee.spectacled.SpectacledVariant import at.techbee.spectacled.screens.core.domain.Status import at.techbee.spectacled.screens.core.presentation.components.BottomSheetWithMenu +import at.techbee.spectacled.screens.core.presentation.components.StatusWithProgressIcon import at.techbee.spectacled.theme.AppTheme import org.jetbrains.compose.resources.stringResource import spectacled.shared.generated.resources.Res import spectacled.shared.generated.resources.done import spectacled.shared.generated.resources.percent_complete import spectacled.shared.generated.resources.priority -import spectacled.shared.generated.resources.priority_Medium import spectacled.shared.generated.resources.priority_high import spectacled.shared.generated.resources.priority_low +import spectacled.shared.generated.resources.priority_medium import spectacled.shared.generated.resources.priority_not_specified +import spectacled.shared.generated.resources.status_no_status import kotlin.math.roundToInt @OptIn(ExperimentalMaterial3Api::class) @@ -108,10 +109,10 @@ fun TaskStatusProgressPickerBottomSheet( ) { FilterChip( - leadingIcon = { Status.NO_STATUS.StatusIcon(null) }, + leadingIcon = { StatusWithProgressIcon(null, null) }, selected = status == null, onClick = { onStatusUpdated(null) }, - label = { Text(stringResource(Status.NO_STATUS.stringRes)) } + label = { Text(stringResource(Res.string.status_no_status)) } ) val statusSet = setOf(Status.NEEDS_ACTION, Status.IN_PROCESS, Status.COMPLETED, Status.CANCELLED) @@ -147,26 +148,24 @@ fun TaskStatusProgressPickerBottomSheet( // Local continuous value for smooth dragging var sliderValue by remember(priority) { mutableFloatStateOf(currentPriorityStep) } - LaunchedEffect(priority) { - sliderValue = currentPriorityStep - } - Slider( value = sliderValue, valueRange = 0f..3f, steps = 2, onValueChange = { sliderValue = it }, onValueChangeFinished = { - onPriorityUpdated( - when (sliderValue.roundToInt()) { - 0 -> null - 1 -> 9L - 2 -> 5L - 3 -> 1L - else -> null - } - ) + val newStep = sliderValue.roundToInt() + + if (newStep != currentPriorityStep.roundToInt()) + onPriorityUpdated( + when (newStep) { + 1 -> 9L + 2 -> 5L + 3 -> 1L + else -> null + } + ) }, thumb = { AssistChip( @@ -176,7 +175,7 @@ fun TaskStatusProgressPickerBottomSheet( text = stringResource(when (sliderValue.roundToInt()) { 0 -> Res.string.priority_not_specified 1 -> Res.string.priority_low - 2 -> Res.string.priority_Medium + 2 -> Res.string.priority_medium 3 -> Res.string.priority_high else -> Res.string.priority_not_specified }), From 279afb741e62c67f35894090c6bcdb757bed1841 Mon Sep 17 00:00:00 2001 From: Claude Date: Fri, 2 Oct 2026 19:21:23 +0000 Subject: [PATCH 3/4] Extract priority mapping into a tested Priority enum - Add core/domain/Priority with the RFC 5545 grouping (1-4 high, 5 medium, 6-9 low, everything else not specified) and the slider step mapping, so the composable no longer carries two hand-kept `when` tables. - Add PriorityTest covering every ICS value, undefined/out-of-range input, round trips and slider step order. - Replace the AssistChip slider thumb with a non-interactive Box so it is not announced as a button and cannot intercept presses meant for the Slider. - Make Status.rfcName non-null again now that NO_STATUS is gone. Co-Authored-By: Claude Opus 5.5 Claude-Session: https://claude.ai/code/session_01QFR1hUx3v8E39bLfsi7Ct3 --- .../screens/core/domain/IcalEntry.kt | 3 +- .../screens/core/domain/Priority.kt | 37 +++++++++ .../TaskStatusProgressPickerBottomSheet.kt | 77 ++++++++----------- .../screens/core/domain/PriorityTest.kt | 62 +++++++++++++++ 4 files changed, 132 insertions(+), 47 deletions(-) create mode 100644 shared/src/commonMain/kotlin/at/techbee/spectacled/screens/core/domain/Priority.kt create mode 100644 shared/src/commonTest/kotlin/at/techbee/spectacled/screens/core/domain/PriorityTest.kt diff --git a/shared/src/commonMain/kotlin/at/techbee/spectacled/screens/core/domain/IcalEntry.kt b/shared/src/commonMain/kotlin/at/techbee/spectacled/screens/core/domain/IcalEntry.kt index e9a58f98..49e8918f 100644 --- a/shared/src/commonMain/kotlin/at/techbee/spectacled/screens/core/domain/IcalEntry.kt +++ b/shared/src/commonMain/kotlin/at/techbee/spectacled/screens/core/domain/IcalEntry.kt @@ -277,10 +277,9 @@ data class IcalEntry( enum class Status( - val rfcName: String?, + val rfcName: String, val stringRes: StringResource ) { - FINAL("FINAL", Res.string.status_final), DRAFT("DRAFT", Res.string.status_draft), diff --git a/shared/src/commonMain/kotlin/at/techbee/spectacled/screens/core/domain/Priority.kt b/shared/src/commonMain/kotlin/at/techbee/spectacled/screens/core/domain/Priority.kt new file mode 100644 index 00000000..10de9774 --- /dev/null +++ b/shared/src/commonMain/kotlin/at/techbee/spectacled/screens/core/domain/Priority.kt @@ -0,0 +1,37 @@ +package at.techbee.spectacled.screens.core.domain + +import org.jetbrains.compose.resources.StringResource +import spectacled.shared.generated.resources.Res +import spectacled.shared.generated.resources.priority_high +import spectacled.shared.generated.resources.priority_low +import spectacled.shared.generated.resources.priority_medium +import spectacled.shared.generated.resources.priority_not_specified + +/** + * The coarse priority levels offered in the UI, ordered from lowest to highest so that + * [ordinal] can be used directly as a slider step. + * + * RFC 5545 (3.8.1.9) defines PRIORITY as 0 (undefined) or 1 (highest) to 9 (lowest) and groups + * 1-4 as high, 5 as medium and 6-9 as low. [icsValue] is the value written when a level is + * picked; [fromIcsValue] maps any stored value back to its level. + */ +enum class Priority( + val icsValue: Long?, + val stringRes: StringResource +) { + NOT_SPECIFIED(null, Res.string.priority_not_specified), + LOW(9L, Res.string.priority_low), + MEDIUM(5L, Res.string.priority_medium), + HIGH(1L, Res.string.priority_high); + + companion object { + fun fromIcsValue(icsValue: Long?): Priority = when (icsValue) { + in 1L..4L -> HIGH + 5L -> MEDIUM + in 6L..9L -> LOW + else -> NOT_SPECIFIED // null, 0 (undefined per RFC 5545) and out-of-range values + } + + fun fromSliderStep(step: Int): Priority = entries.getOrElse(step) { NOT_SPECIFIED } + } +} diff --git a/shared/src/commonMain/kotlin/at/techbee/spectacled/screens/details/presentation/components/TaskStatusProgressPickerBottomSheet.kt b/shared/src/commonMain/kotlin/at/techbee/spectacled/screens/details/presentation/components/TaskStatusProgressPickerBottomSheet.kt index 1182275d..be82fd84 100644 --- a/shared/src/commonMain/kotlin/at/techbee/spectacled/screens/details/presentation/components/TaskStatusProgressPickerBottomSheet.kt +++ b/shared/src/commonMain/kotlin/at/techbee/spectacled/screens/details/presentation/components/TaskStatusProgressPickerBottomSheet.kt @@ -1,16 +1,20 @@ package at.techbee.spectacled.screens.details.presentation.components +import androidx.compose.foundation.background +import androidx.compose.foundation.border import androidx.compose.foundation.layout.Arrangement +import androidx.compose.foundation.layout.Box import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.FlowRow import androidx.compose.foundation.layout.Row import androidx.compose.foundation.layout.fillMaxWidth import androidx.compose.foundation.layout.padding import androidx.compose.foundation.layout.width -import androidx.compose.material3.AssistChip +import androidx.compose.material3.AssistChipDefaults import androidx.compose.material3.ExperimentalMaterial3Api import androidx.compose.material3.FilterChip import androidx.compose.material3.HorizontalDivider +import androidx.compose.material3.MaterialTheme import androidx.compose.material3.Scaffold import androidx.compose.material3.SheetState import androidx.compose.material3.SheetValue @@ -33,6 +37,7 @@ import androidx.compose.ui.text.style.TextOverflow import androidx.compose.ui.tooling.preview.Preview import androidx.compose.ui.unit.dp import at.techbee.spectacled.SpectacledVariant +import at.techbee.spectacled.screens.core.domain.Priority import at.techbee.spectacled.screens.core.domain.Status import at.techbee.spectacled.screens.core.presentation.components.BottomSheetWithMenu import at.techbee.spectacled.screens.core.presentation.components.StatusWithProgressIcon @@ -42,10 +47,6 @@ import spectacled.shared.generated.resources.Res import spectacled.shared.generated.resources.done import spectacled.shared.generated.resources.percent_complete import spectacled.shared.generated.resources.priority -import spectacled.shared.generated.resources.priority_high -import spectacled.shared.generated.resources.priority_low -import spectacled.shared.generated.resources.priority_medium -import spectacled.shared.generated.resources.priority_not_specified import spectacled.shared.generated.resources.status_no_status import kotlin.math.roundToInt @@ -137,55 +138,41 @@ fun TaskStatusProgressPickerBottomSheet( Text(stringResource(Res.string.priority)) - val currentPriorityStep = when (priority) { - null, 0L -> 0f - in 6L..9L -> 1f - 5L -> 2f - in 1L..4L -> 3f - else -> 0f - } + val currentPriority = Priority.fromIcsValue(priority) // Local continuous value for smooth dragging - var sliderValue by remember(priority) { mutableFloatStateOf(currentPriorityStep) } + var sliderValue by remember(priority) { mutableFloatStateOf(currentPriority.ordinal.toFloat()) } Slider( value = sliderValue, - valueRange = 0f..3f, - steps = 2, + valueRange = 0f..(Priority.entries.size - 1).toFloat(), + steps = Priority.entries.size - 2, onValueChange = { sliderValue = it }, onValueChangeFinished = { - - val newStep = sliderValue.roundToInt() - - if (newStep != currentPriorityStep.roundToInt()) - onPriorityUpdated( - when (newStep) { - 1 -> 9L - 2 -> 5L - 3 -> 1L - else -> null - } - ) + val newPriority = Priority.fromSliderStep(sliderValue.roundToInt()) + if (newPriority != currentPriority) + onPriorityUpdated(newPriority.icsValue) }, thumb = { - AssistChip( - onClick = {}, - label = { - Text( - text = stringResource(when (sliderValue.roundToInt()) { - 0 -> Res.string.priority_not_specified - 1 -> Res.string.priority_low - 2 -> Res.string.priority_medium - 3 -> Res.string.priority_high - else -> Res.string.priority_not_specified - }), - maxLines = 1, - overflow = TextOverflow.Ellipsis, - textAlign = TextAlign.Center, - modifier = Modifier.width(72.dp) - ) - } - ) + // Plain Box rather than a chip/Surface so the thumb stays non-interactive and + // every press reaches the Slider's own drag handling. + Box( + contentAlignment = Alignment.Center, + modifier = Modifier + .background(MaterialTheme.colorScheme.surfaceContainerLow, AssistChipDefaults.shape) + .border(1.dp, MaterialTheme.colorScheme.outlineVariant, AssistChipDefaults.shape) + ) { + Text( + text = stringResource(Priority.fromSliderStep(sliderValue.roundToInt()).stringRes), + style = MaterialTheme.typography.labelLarge, + maxLines = 1, + overflow = TextOverflow.Ellipsis, + textAlign = TextAlign.Center, + modifier = Modifier + .padding(horizontal = 8.dp, vertical = 6.dp) + .width(72.dp) + ) + } }, modifier = Modifier.weight(1f) ) diff --git a/shared/src/commonTest/kotlin/at/techbee/spectacled/screens/core/domain/PriorityTest.kt b/shared/src/commonTest/kotlin/at/techbee/spectacled/screens/core/domain/PriorityTest.kt new file mode 100644 index 00000000..fcaee769 --- /dev/null +++ b/shared/src/commonTest/kotlin/at/techbee/spectacled/screens/core/domain/PriorityTest.kt @@ -0,0 +1,62 @@ +package at.techbee.spectacled.screens.core.domain + +import kotlin.test.Test +import kotlin.test.assertEquals + +class PriorityTest { + + @Test + fun fromIcsValue_followsRfc5545Grouping() { + val expected = mapOf( + 1L to Priority.HIGH, + 2L to Priority.HIGH, + 3L to Priority.HIGH, + 4L to Priority.HIGH, + 5L to Priority.MEDIUM, + 6L to Priority.LOW, + 7L to Priority.LOW, + 8L to Priority.LOW, + 9L to Priority.LOW + ) + expected.forEach { (icsValue, priority) -> + assertEquals(priority, Priority.fromIcsValue(icsValue), "fromIcsValue($icsValue)") + } + } + + @Test + fun fromIcsValue_undefinedAndOutOfRange_isNotSpecified() { + listOf(null, 0L, -1L, 10L, Long.MAX_VALUE).forEach { icsValue -> + assertEquals(Priority.NOT_SPECIFIED, Priority.fromIcsValue(icsValue), "fromIcsValue($icsValue)") + } + } + + @Test + fun icsValue_roundTripsToSameLevel() { + Priority.entries.forEach { priority -> + assertEquals(priority, Priority.fromIcsValue(priority.icsValue), "round trip for $priority") + } + } + + @Test + fun icsValue_perLevel() { + assertEquals(null, Priority.NOT_SPECIFIED.icsValue) + assertEquals(9L, Priority.LOW.icsValue) + assertEquals(5L, Priority.MEDIUM.icsValue) + assertEquals(1L, Priority.HIGH.icsValue) + } + + @Test + fun sliderSteps_orderedFromLowestToHighest() { + assertEquals( + listOf(Priority.NOT_SPECIFIED, Priority.LOW, Priority.MEDIUM, Priority.HIGH), + (0..3).map { Priority.fromSliderStep(it) } + ) + } + + @Test + fun fromSliderStep_outOfRange_isNotSpecified() { + listOf(-1, 4, Int.MAX_VALUE).forEach { step -> + assertEquals(Priority.NOT_SPECIFIED, Priority.fromSliderStep(step), "fromSliderStep($step)") + } + } +} From f38517917428e0b9478573875818681733f9ffec Mon Sep 17 00:00:00 2001 From: Patrick Lang <72232737+patrickunterwegs@users.noreply.github.com> Date: Fri, 2 Oct 2026 22:36:49 +0300 Subject: [PATCH 4/4] Show StatusWithProgressIcon consistently also in DetailsScreenRoot.kt --- .../screens/details/presentation/DetailsScreenRoot.kt | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/shared/src/commonMain/kotlin/at/techbee/spectacled/screens/details/presentation/DetailsScreenRoot.kt b/shared/src/commonMain/kotlin/at/techbee/spectacled/screens/details/presentation/DetailsScreenRoot.kt index 2ea5803b..83ad9fee 100644 --- a/shared/src/commonMain/kotlin/at/techbee/spectacled/screens/details/presentation/DetailsScreenRoot.kt +++ b/shared/src/commonMain/kotlin/at/techbee/spectacled/screens/details/presentation/DetailsScreenRoot.kt @@ -61,7 +61,6 @@ import at.techbee.spectacled.screens.Route.IcalEntryDetails import at.techbee.spectacled.screens.core.Platforms import at.techbee.spectacled.screens.core.domain.CalendarComponent import at.techbee.spectacled.screens.core.domain.IcalEntry -import at.techbee.spectacled.screens.core.domain.Status import at.techbee.spectacled.screens.core.domain.SyncState import at.techbee.spectacled.screens.core.getPlatform import at.techbee.spectacled.screens.core.presentation.components.BottomSheetWithMenu @@ -388,8 +387,7 @@ fun DetailsScreenRoot( enabled = detailsState.allowEditing() && !detailsState.isLoading ) { StatusWithProgressIcon( - status = detailsState.icalEntry.status - ?: if(detailsState.icalEntry.isTask()) Status.NEEDS_ACTION else Status.FINAL, + status = detailsState.icalEntry.status, percent = detailsState.icalEntry.percentComplete ) }