Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand Down Expand Up @@ -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")
Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -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 }
}
Expand Down
31 changes: 23 additions & 8 deletions app/src/main/java/com/mapgie/dash/ui/components/TaskCard.kt
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -49,22 +50,25 @@ 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()) {
TaskPriority.HIGHER -> StatusAging
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)) {
Expand All @@ -73,7 +77,7 @@ fun TaskCard(
modifier = Modifier
.width(4.dp)
.fillMaxHeight()
.background(priorityColor)
.background(barColor)
)
// Done checkbox
Checkbox(
Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -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(
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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
Expand All @@ -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 {
Expand All @@ -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
)
}
}
Expand All @@ -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
)
}
}
Expand Down Expand Up @@ -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
)
}
}
Expand Down Expand Up @@ -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(
Expand Down Expand Up @@ -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 }
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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<TaskDto>
get() {
Expand All @@ -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(
Expand Down Expand Up @@ -124,6 +131,7 @@ class TaskListViewModel @Inject constructor(
ownerHandle = s.ownerHandle,
groupByCategory = s.groupTasksByCategory,
hideThresholdDays = s.taskHideThresholdDays,
zenMode = s.taskZenMode,
)
}
}
Expand Down Expand Up @@ -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) }
}
}
4 changes: 4 additions & 0 deletions changelog/unreleased/task-zen-mode.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
{
"bump": "minor",
"added": ["Zen mode for Tasks, matching the calmer, decluttered view already available for Chores"]
}
Loading