diff --git a/app/src/main/java/com/mapgie/dash/data/preferences/SettingsRepository.kt b/app/src/main/java/com/mapgie/dash/data/preferences/SettingsRepository.kt index b6ad45e..c9c138c 100644 --- a/app/src/main/java/com/mapgie/dash/data/preferences/SettingsRepository.kt +++ b/app/src/main/java/com/mapgie/dash/data/preferences/SettingsRepository.kt @@ -25,6 +25,7 @@ data class AppSettings( // High-contrast (WCAG) variants of the built-in palettes val wcagMode: Boolean = false, val zenMode: Boolean = false, + val taskZenMode: Boolean = false, val showDueCountdown: Boolean = false, val deliveryMode: String = "NOTIFICATION", // Colour theme selection — name of AppTheme enum entry (SAGE, CORAL, TEAL, CUSTOM) @@ -71,6 +72,7 @@ class SettingsRepository @Inject constructor( val THEME_MODE = stringPreferencesKey("theme_mode") val WCAG_MODE = booleanPreferencesKey("wcag_mode") val ZEN_MODE = booleanPreferencesKey("zen_mode") + val TASK_ZEN_MODE = booleanPreferencesKey("task_zen_mode") val SHOW_DUE_COUNTDOWN = booleanPreferencesKey("show_due_countdown") val DELIVERY_MODE = stringPreferencesKey("delivery_mode") val APP_THEME = stringPreferencesKey("app_theme") @@ -116,6 +118,7 @@ class SettingsRepository @Inject constructor( ?: ThemeMode.SYSTEM, wcagMode = prefs[Keys.WCAG_MODE] ?: false, zenMode = prefs[Keys.ZEN_MODE] ?: false, + taskZenMode = prefs[Keys.TASK_ZEN_MODE] ?: false, showDueCountdown = prefs[Keys.SHOW_DUE_COUNTDOWN] ?: false, deliveryMode = prefs[Keys.DELIVERY_MODE] ?: "NOTIFICATION", // Normalise old per-mode entries to palette names for backward compatibility @@ -173,6 +176,10 @@ class SettingsRepository @Inject constructor( context.dataStore.edit { it[Keys.ZEN_MODE] = enabled } } + suspend fun setTaskZenMode(enabled: Boolean) { + context.dataStore.edit { it[Keys.TASK_ZEN_MODE] = enabled } + } + suspend fun setShowDueCountdown(enabled: Boolean) { context.dataStore.edit { it[Keys.SHOW_DUE_COUNTDOWN] = enabled } } diff --git a/app/src/main/java/com/mapgie/dash/ui/components/TaskCard.kt b/app/src/main/java/com/mapgie/dash/ui/components/TaskCard.kt index 41b081f..ce01a4a 100644 --- a/app/src/main/java/com/mapgie/dash/ui/components/TaskCard.kt +++ b/app/src/main/java/com/mapgie/dash/ui/components/TaskCard.kt @@ -27,6 +27,7 @@ import androidx.compose.runtime.remember import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.draw.clip +import androidx.compose.ui.graphics.Color import androidx.compose.ui.text.style.TextDecoration import androidx.compose.ui.text.style.TextOverflow import androidx.compose.ui.unit.dp @@ -49,7 +50,8 @@ fun TaskCard( onToggleDone: () -> Unit, showCategory: Boolean = true, modifier: Modifier = Modifier, - showOwner: Boolean = true + showOwner: Boolean = true, + zenMode: Boolean = false ) { val isDone = task.completedAt != null val priorityColor = when (task.priorityEnum()) { @@ -57,14 +59,16 @@ fun TaskCard( TaskPriority.NORMAL -> StatusFresh TaskPriority.LOWER -> MaterialTheme.colorScheme.outline } + val barColor = if (zenMode) Color.Transparent else priorityColor Card( modifier = modifier.fillMaxWidth(), colors = CardDefaults.cardColors( - containerColor = if (isDone) - MaterialTheme.colorScheme.surfaceVariant.copy(alpha = 0.5f) - else - MaterialTheme.colorScheme.surfaceVariant + containerColor = when { + zenMode -> MaterialTheme.colorScheme.surface + isDone -> MaterialTheme.colorScheme.surfaceVariant.copy(alpha = 0.5f) + else -> MaterialTheme.colorScheme.surfaceVariant + } ), ) { Row(modifier = Modifier.height(IntrinsicSize.Min)) { @@ -73,7 +77,7 @@ fun TaskCard( modifier = Modifier .width(4.dp) .fillMaxHeight() - .background(priorityColor) + .background(barColor) ) // Done checkbox Checkbox( @@ -124,12 +128,12 @@ fun TaskCard( horizontalArrangement = Arrangement.spacedBy(6.dp), verticalAlignment = Alignment.CenterVertically ) { - if (showCategory) { + if (showCategory && !zenMode) { task.category?.takeIf { it.isNotBlank() }?.let { cat -> CategoryBadge(cat) } } - DueBadge(task = task) + if (zenMode) ZenDueLabel(task = task) else DueBadge(task = task) } } // Owner badge @@ -181,6 +185,17 @@ private fun DueBadge(task: TaskDto) { ) } +/** Plain, uncoloured due date for zen mode, unlike DueBadge it never signals urgency. */ +@Composable +private fun ZenDueLabel(task: TaskDto) { + val date = task.dueDate?.let { runCatching { LocalDate.parse(it) }.getOrNull() } ?: return + Text( + text = date.format(DateTimeFormatter.ofPattern("MMM d")), + style = MaterialTheme.typography.labelSmall, + color = MaterialTheme.colorScheme.onSurfaceVariant + ) +} + @Composable private fun CategoryBadge(category: String) { Box( diff --git a/app/src/main/java/com/mapgie/dash/ui/screens/tasks/TaskListScreen.kt b/app/src/main/java/com/mapgie/dash/ui/screens/tasks/TaskListScreen.kt index 4e524cb..84e81d4 100644 --- a/app/src/main/java/com/mapgie/dash/ui/screens/tasks/TaskListScreen.kt +++ b/app/src/main/java/com/mapgie/dash/ui/screens/tasks/TaskListScreen.kt @@ -16,10 +16,13 @@ import androidx.compose.foundation.layout.size import androidx.compose.foundation.lazy.LazyColumn import androidx.compose.foundation.lazy.items import androidx.compose.material.icons.Icons +import androidx.compose.material.icons.filled.ArrowDownward +import androidx.compose.material.icons.filled.ArrowUpward import androidx.compose.material.icons.filled.ExpandLess import androidx.compose.material.icons.filled.ExpandMore import androidx.compose.material.icons.filled.Person import androidx.compose.material.icons.filled.Sort +import androidx.compose.material.icons.filled.Spa import androidx.compose.material3.ExperimentalMaterial3Api import androidx.compose.material3.FilterChip import androidx.compose.material3.FilterChipDefaults @@ -121,36 +124,64 @@ fun TaskListScreen( ) } Spacer(Modifier.weight(1f)) - IconButton( - onClick = { viewModel.setSort(uiState.sort.next()) }, - modifier = Modifier.size(48.dp) - ) { - Icon( - Icons.Filled.Sort, - contentDescription = "Sort: ${uiState.sort.label}", - tint = MaterialTheme.colorScheme.onSurfaceVariant - ) - } - if (uiState.ownerHandle.isNotBlank()) { + if (!uiState.zenMode) { IconButton( - onClick = { - viewModel.setOwnerFilter( - if (uiState.ownerFilter == OwnerFilter.MINE) OwnerFilter.ALL - else OwnerFilter.MINE + onClick = { viewModel.setSort(uiState.sort.next()) }, + modifier = Modifier.size(48.dp) + ) { + Icon( + Icons.Filled.Sort, + contentDescription = "Sort: ${uiState.sort.label}", + tint = MaterialTheme.colorScheme.onSurfaceVariant + ) + } + if (uiState.ownerHandle.isNotBlank()) { + IconButton( + onClick = { + viewModel.setOwnerFilter( + if (uiState.ownerFilter == OwnerFilter.MINE) OwnerFilter.ALL + else OwnerFilter.MINE + ) + }, + modifier = Modifier.size(48.dp) + ) { + Icon( + Icons.Filled.Person, + contentDescription = if (uiState.ownerFilter == OwnerFilter.MINE) + "Show all owners" else "Show my tasks", + tint = if (uiState.ownerFilter == OwnerFilter.MINE) + MaterialTheme.colorScheme.primary + else MaterialTheme.colorScheme.onSurfaceVariant ) - }, + } + } + } else { + IconButton( + onClick = { viewModel.setZenSort(!uiState.zenSortAscending) }, modifier = Modifier.size(48.dp) ) { Icon( - Icons.Filled.Person, - contentDescription = if (uiState.ownerFilter == OwnerFilter.MINE) - "Show all owners" else "Show my tasks", - tint = if (uiState.ownerFilter == OwnerFilter.MINE) - MaterialTheme.colorScheme.primary - else MaterialTheme.colorScheme.onSurfaceVariant + if (uiState.zenSortAscending) Icons.Filled.ArrowUpward else Icons.Filled.ArrowDownward, + contentDescription = if (uiState.zenSortAscending) + "Sorted: most urgent first" else "Sorted: least urgent first", + tint = MaterialTheme.colorScheme.onSurfaceVariant ) } } + IconButton( + onClick = { viewModel.setZenMode(!uiState.zenMode) }, + modifier = Modifier.size(48.dp) + ) { + Icon( + Icons.Filled.Spa, + contentDescription = if (uiState.zenMode) + "Exit zen mode" else "Enter zen mode", + tint = if (uiState.zenMode) + MaterialTheme.colorScheme.primary + else MaterialTheme.colorScheme.onSurfaceVariant, + modifier = Modifier.size(32.dp) + ) + } } val active = uiState.activeTasks @@ -170,7 +201,7 @@ fun TaskListScreen( verticalArrangement = Arrangement.spacedBy(6.dp), modifier = Modifier.fillMaxSize() ) { - if (uiState.groupByCategory && uiState.filter != TaskFilter.DONE) { + if (uiState.groupByCategory && !uiState.zenMode && uiState.filter != TaskFilter.DONE) { val grouped = active.groupBy { it.category?.takeIf(String::isNotBlank) ?: "Other" } grouped.forEach { (cat, tasks) -> stickyHeader { @@ -192,7 +223,8 @@ fun TaskListScreen( onToggleDone = { viewModel.markDone(task.id) }, onSwipeToggleDone = { viewModel.markDone(task.id) }, showCategory = false, - showOwner = (uiState.ownerFilter != OwnerFilter.MINE) + showOwner = (uiState.ownerFilter != OwnerFilter.MINE), + zenMode = uiState.zenMode ) } } @@ -205,7 +237,8 @@ fun TaskListScreen( onToggleDone = { viewModel.markDone(task.id) }, onSwipeToggleDone = { viewModel.markDone(task.id) }, showCategory = !uiState.groupByCategory, - showOwner = (uiState.ownerFilter != OwnerFilter.MINE) + showOwner = (uiState.ownerFilter != OwnerFilter.MINE), + zenMode = uiState.zenMode ) } } @@ -241,7 +274,8 @@ fun TaskListScreen( onToggleDone = { viewModel.markUndone(task.id) }, onSwipeToggleDone = { viewModel.markUndone(task.id) }, showCategory = !uiState.groupByCategory, - showOwner = (uiState.ownerFilter != OwnerFilter.MINE) + showOwner = (uiState.ownerFilter != OwnerFilter.MINE), + zenMode = uiState.zenMode ) } } @@ -294,7 +328,8 @@ private fun SwipeToCompleteCard( onToggleDone: () -> Unit, onSwipeToggleDone: () -> Unit, showCategory: Boolean = true, - showOwner: Boolean = true + showOwner: Boolean = true, + zenMode: Boolean = false ) { val isDone = task.completedAt != null val dismissState = rememberSwipeToDismissBoxState( @@ -335,6 +370,7 @@ private fun SwipeToCompleteCard( onToggleDone = onToggleDone, showCategory = showCategory, showOwner = showOwner, + zenMode = zenMode, modifier = Modifier .padding(horizontal = 12.dp) .semantics { role = Role.Button } diff --git a/app/src/main/java/com/mapgie/dash/ui/screens/tasks/TaskListViewModel.kt b/app/src/main/java/com/mapgie/dash/ui/screens/tasks/TaskListViewModel.kt index 7adc540..1b3d4f2 100644 --- a/app/src/main/java/com/mapgie/dash/ui/screens/tasks/TaskListViewModel.kt +++ b/app/src/main/java/com/mapgie/dash/ui/screens/tasks/TaskListViewModel.kt @@ -51,6 +51,8 @@ data class TaskUiState( val ownerHandle: String = "", val pinnedTaskId: String? = null, val hideThresholdDays: Int = -1, + val zenMode: Boolean = false, + val zenSortAscending: Boolean = true, ) { val displayed: List get() { @@ -66,6 +68,11 @@ data class TaskUiState( } matchesStatus && matchesOwner } + if (zenMode) { + // Zen sort: ascending = most urgent (overdue) first, matching the due sort order + val byUrgency = filtered.sortedWith(compareBy({ it.urgency().ordinal }, { it.dueDate ?: "" })) + return if (zenSortAscending) byUrgency else byUrgency.reversed() + } return when (sort) { TaskSort.PRIORITY -> filtered.sortedWith( compareBy( @@ -124,6 +131,7 @@ class TaskListViewModel @Inject constructor( ownerHandle = s.ownerHandle, groupByCategory = s.groupTasksByCategory, hideThresholdDays = s.taskHideThresholdDays, + zenMode = s.taskZenMode, ) } } @@ -290,4 +298,12 @@ class TaskListViewModel @Inject constructor( } fun setOwnerFilter(f: OwnerFilter) = _uiState.update { it.copy(ownerFilter = f) } fun clearError() = _uiState.update { it.copy(error = null) } + + fun setZenMode(enabled: Boolean) { + viewModelScope.launch { settingsRepository.setTaskZenMode(enabled) } + } + + fun setZenSort(ascending: Boolean) { + _uiState.update { it.copy(zenSortAscending = ascending) } + } } diff --git a/changelog/unreleased/task-zen-mode.json b/changelog/unreleased/task-zen-mode.json new file mode 100644 index 0000000..1e98d5e --- /dev/null +++ b/changelog/unreleased/task-zen-mode.json @@ -0,0 +1,4 @@ +{ + "bump": "minor", + "added": ["Zen mode for Tasks, matching the calmer, decluttered view already available for Chores"] +}