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
)
}