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
21 changes: 21 additions & 0 deletions app/src/main/java/com/mapgie/dash/data/model/CategoryIcon.kt
Original file line number Diff line number Diff line change
Expand Up @@ -24,13 +24,24 @@ enum class CategoryIcon(val label: String) {
BRUSH("Cleaning"),
LEAF("Leaf"),
LAMP("Lamp"),
PAW_PRINT("Pets"),
BABY("Baby"),
DUMBBELL("Fitness"),
SHOPPING_CART("Shopping"),
WRENCH("Repairs"),
BOOK_OPEN("Study"),
GIFT("Gifts"),
BRIEFCASE("Work"),
WALLET("Money"),
GRADUATION_CAP("School"),
CIRCLE_ALERT("Other");

companion object {
/** The subset offered in the Settings › Categories icon picker, in picker order. */
val pickerSet: List<CategoryIcon> = listOf(
WASHING_MACHINE, BRUSH, HOME, DROPLET, SPROUT, UTENSILS, BATH, TREE_PINE, LEAF, LAMP,
CAR, ZAP, PILL, PRINTER, PLANE, SHIELD, CALENDAR,
PAW_PRINT, BABY, DUMBBELL, SHOPPING_CART, WRENCH, BOOK_OPEN, GIFT, BRIEFCASE, WALLET, GRADUATION_CAP,
)

fun fromName(name: String?): CategoryIcon? = name?.let { n -> entries.firstOrNull { it.name == n } }
Expand All @@ -56,6 +67,16 @@ enum class CategoryIcon(val label: String) {
has("insur") -> SHIELD
has("clean", "dust", "vacuum", "hoover", "mop") -> BRUSH
has("water", "filter", "softener", "boiler") -> DROPLET
has("pet", "dog", "cat", "animal", "vet") -> PAW_PRINT
has("baby", "infant", "nappy", "diaper", "pram", "stroller") -> BABY
has("gym", "fitness", "workout", "exercise") -> DUMBBELL
has("shop", "grocer", "market", "store") -> SHOPPING_CART
has("repair", "maintenance", "diy", "tool", "fix") -> WRENCH
has("study", "read", "book", "homework") -> BOOK_OPEN
has("gift", "present", "birthday", "christmas") -> GIFT
has("work", "office", "job", "meeting") -> BRIEFCASE
has("money", "bank", "budget", "wallet", "saving") -> WALLET
has("school", "class", "college", "uni", "course", "exam") -> GRADUATION_CAP
has("house", "home", "flat", "apartment") -> HOME
else -> PRINTER
}
Expand Down
28 changes: 24 additions & 4 deletions app/src/main/java/com/mapgie/dash/data/model/Swatch.kt
Original file line number Diff line number Diff line change
Expand Up @@ -12,9 +12,9 @@ data class SwatchTones(
)

/**
* The seven-colour palette the user picks from in Settings › Colours (severity
* tints) and Settings › Categories (a category's own colour). Plain Kotlin so
* the persisted name, the settings screens and the unit tests all share it.
* The palette the user picks from in Settings › Colours (severity tints) and
* Settings › Categories (a category's own colour). Plain Kotlin so the
* persisted name, the settings screens and the unit tests all share it.
*
* Each swatch carries a hand-tuned set of tones per brightness, matching the
* Cozy Cream / Zen Dark handoff. The dark text tones are the design's; the
Expand Down Expand Up @@ -60,6 +60,26 @@ enum class Swatch(
"Peach",
light = SwatchTones(spineArgb = 0xFFE0B28DL, textArgb = 0xFF8A562AL, tintArgb = 0xFFF6E8DCL),
dark = SwatchTones(spineArgb = 0xFFE0B28DL, textArgb = 0xFFE0B28DL, tintArgb = 0xFF4A3A2EL),
),
TEAL(
"Teal",
light = SwatchTones(spineArgb = 0xFF4F9D97L, textArgb = 0xFF2E6B66L, tintArgb = 0xFFDCEBE9L),
dark = SwatchTones(spineArgb = 0xFF6FB3ACL, textArgb = 0xFF8FCFC8L, tintArgb = 0xFF2B3D3AL),
),
CORAL(
"Coral",
light = SwatchTones(spineArgb = 0xFFDD7F63L, textArgb = 0xFF9E4830L, tintArgb = 0xFFF7E4DCL),
dark = SwatchTones(spineArgb = 0xFFE39178L, textArgb = 0xFFEAA791L, tintArgb = 0xFF48332BL),
),
SLATE(
"Slate",
light = SwatchTones(spineArgb = 0xFF7B8794L, textArgb = 0xFF4C5762L, tintArgb = 0xFFE4E7EAL),
dark = SwatchTones(spineArgb = 0xFF9AA6B2L, textArgb = 0xFFB4BEC8L, tintArgb = 0xFF373C42L),
),
CLAY(
"Clay",
light = SwatchTones(spineArgb = 0xFFB56A4EL, textArgb = 0xFF9A4F34L, tintArgb = 0xFFF1E1D8L),
dark = SwatchTones(spineArgb = 0xFFC98363L, textArgb = 0xFFDCA184L, tintArgb = 0xFF433026L),
);

fun tones(dark: Boolean): SwatchTones = if (dark) this.dark else light
Expand All @@ -68,7 +88,7 @@ enum class Swatch(
/** The six swatches offered for severity colours (Settings › Colours). */
val severityPalette: List<Swatch> = listOf(ROSE, GOLD, AMBER, SAGE, BLUE, LAVENDER)

/** All seven swatches, offered for category colours (Settings › Categories). */
/** Every swatch, offered for category and memo colours (Settings › Categories). */
val categoryPalette: List<Swatch> = entries.toList()

fun fromName(name: String?): Swatch? = name?.let { n -> entries.firstOrNull { it.name == n } }
Expand Down
80 changes: 50 additions & 30 deletions app/src/main/java/com/mapgie/dash/ui/components/TaskCard.kt
Original file line number Diff line number Diff line change
Expand Up @@ -18,20 +18,20 @@ import androidx.compose.material3.Icon
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.Text
import androidx.compose.runtime.Composable
import androidx.compose.runtime.remember
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.graphics.vector.ImageVector
import androidx.compose.ui.text.style.TextDecoration
import androidx.compose.ui.text.style.TextOverflow
import androidx.compose.ui.unit.dp
import com.mapgie.dash.data.model.Swatch
import com.mapgie.dash.data.model.TaskDto
import com.mapgie.dash.data.model.TaskPriority
import com.mapgie.dash.data.model.TaskUrgency
import com.mapgie.dash.data.model.priorityEnum
import com.mapgie.dash.data.model.urgency
import com.mapgie.dash.ui.components.core.DoneToggleChip
import com.mapgie.dash.ui.components.core.CardIconChip
import com.mapgie.dash.ui.components.core.MetaCaption
import com.mapgie.dash.ui.components.core.MetaLabel
import com.mapgie.dash.ui.components.core.OwnerAvatar
Expand All @@ -45,48 +45,62 @@ import com.mapgie.dash.ui.theme.badgeContainerColor
import com.mapgie.dash.ui.theme.barColor
import com.mapgie.dash.ui.theme.isDarkScheme
import com.mapgie.dash.ui.theme.mutedCardContainer
import com.mapgie.dash.ui.theme.spineColor
import com.mapgie.dash.ui.theme.statusTone
import com.mapgie.dash.ui.theme.textColor
import java.time.Instant
import com.mapgie.dash.ui.theme.tintColor
import java.time.LocalDate
import java.time.ZoneId
import java.time.format.DateTimeFormatter
import java.time.temporal.ChronoUnit

/**
* The revised (turn 5a) list card for a task: urgency spine, a circular
* done-toggle chip carrying the category's Lucide [icon] on the urgency tint,
* title with an uppercase "CATEGORY · HIGH" caption beneath, and a single
* right-hand row of owner avatar then due badge.
* The revised (turn 5a) list card for a task: urgency spine, a circular icon
* chip carrying the category's Lucide [icon] (a check once done) on the urgency
* tint, title with an uppercase "CATEGORY · HIGH" caption beneath, and a single
* right-hand row of owner avatar then due badge. Tapping the card opens the task;
* completion is a swipe-right or the overview's Mark done, so a tap never
* silently ticks the task off.
*
* The spine colour means urgency (the app-wide bar meaning, see `StatusTone.kt`);
* priority is carried by the caption text, never by colour alone.
* Colour follows Settings › Colours' two axes, exactly as the Chores card does:
* the spine and due badge take [spineSwatch] (the category colour, badge text
* neutral) or, when it is null, the urgency status tone; the round icon chip does
* the same with [iconSwatch]. Priority is always carried by the caption text, and
* the badge words restate the state, so colour is never the only signal.
*/
@Composable
fun TaskCard(
task: TaskDto,
onToggleDone: () -> Unit,
icon: ImageVector,
modifier: Modifier = Modifier,
showCategory: Boolean = true,
showOwner: Boolean = true,
zenMode: Boolean = false,
spineSwatch: Swatch? = null,
iconSwatch: Swatch? = null,
reminderCount: Int = 0,
isPinned: Boolean = false,
highlightQuery: String? = null
) {
val isDone = task.completedAt != null
val accents = LocalTypeAccents.current
val tone = task.statusTone()
val dark = isDarkScheme()
val barColor = if (zenMode) Color.Transparent else tone.barColor()
val barColor = when {
zenMode -> Color.Transparent
spineSwatch != null -> spineSwatch.spineColor()
else -> tone.barColor()
}

val chipContainer = when {
zenMode -> Color.Transparent
isDone -> MaterialTheme.colorScheme.surfaceContainerHigh
iconSwatch != null -> iconSwatch.tintColor()
else -> tone.badgeContainerColor() ?: accents.taskContainer
}
val chipContent = when {
zenMode || isDone -> MaterialTheme.colorScheme.onSurfaceVariant
iconSwatch != null -> iconSwatch.textColor()
tone == StatusTone.NEUTRAL || tone == StatusTone.NONE -> accents.onTaskContainer
else -> tone.textColor()
}
Expand Down Expand Up @@ -126,10 +140,8 @@ fun TaskCard(
horizontalArrangement = Arrangement.spacedBy(9.dp),
verticalAlignment = Alignment.CenterVertically
) {
DoneToggleChip(
isDone = isDone,
onToggle = onToggleDone,
icon = icon,
CardIconChip(
icon = if (isDone) LucideIcons.Check else icon,
containerColor = chipContainer,
contentColor = chipContent,
)
Expand Down Expand Up @@ -161,20 +173,20 @@ fun TaskCard(
modifier = Modifier.size(14.dp)
)
}
if (task.reminderAt != null && task.reminded != true && !isDone) {
val reminderInstant = remember(task.reminderAt) {
runCatching { Instant.parse(task.reminderAt) }.getOrNull()
}
val isReminderPast = reminderInstant != null && reminderInstant.isBefore(Instant.now())
if (reminderCount > 0 && !isDone) {
Icon(
imageVector = LucideIcons.Bell,
contentDescription = if (isReminderPast) "Reminder passed" else "Reminder set",
tint = if (isReminderPast)
MaterialTheme.colorScheme.onSurfaceVariant
else
MaterialTheme.colorScheme.primary,
contentDescription = if (reminderCount == 1) "1 reminder" else "$reminderCount reminders",
tint = MaterialTheme.colorScheme.primary,
modifier = Modifier.size(14.dp)
)
if (reminderCount > 1) {
Text(
text = reminderCount.toString(),
style = MaterialTheme.typography.labelSmall,
color = MaterialTheme.colorScheme.primary,
)
}
}
}
if (zenMode) {
Expand All @@ -199,7 +211,7 @@ fun TaskCard(
task.owner?.takeIf { showOwner && it.isNotBlank() }?.let { owner ->
OwnerAvatar(handle = owner)
}
if (!zenMode && !isDone) DueBadge(task = task)
if (!zenMode && !isDone) DueBadge(task = task, spineSwatch = spineSwatch)
}
}
}
Expand All @@ -211,28 +223,36 @@ fun TaskCard(
* anything further out, matching the handoff's right-hand cluster.
*/
@Composable
private fun DueBadge(task: TaskDto) {
private fun DueBadge(task: TaskDto, spineSwatch: Swatch? = null) {
val today = LocalDate.now(ZoneId.systemDefault())
val date = task.dueDate?.let { runCatching { LocalDate.parse(it) }.getOrNull() }
// When the spine follows the category colour, the badge follows it too (they
// are one axis); the badge text drops to neutral so the words still lead.
val containerOverride = spineSwatch?.tintColor()
val textOverride = if (spineSwatch != null) MaterialTheme.colorScheme.onSurfaceVariant else null
// "Eventually" carries no urgency but is still worth showing, so it never reads
// as a task with no due at all.
if (task.dueDate == null && task.duePeriod == "eventually") {
StatusBadge(text = "eventually", tone = StatusTone.NEUTRAL)
StatusBadge(text = "eventually", tone = StatusTone.NEUTRAL, containerOverride = containerOverride, textOverride = textOverride)
return
}
when (task.urgency()) {
TaskUrgency.OVERDUE -> {
val late = date?.let { ChronoUnit.DAYS.between(it, today) } ?: 1L
StatusBadge(text = "${late}d late", tone = StatusTone.CRITICAL)
StatusBadge(text = "${late}d late", tone = StatusTone.CRITICAL, containerOverride = containerOverride, textOverride = textOverride)
}
TaskUrgency.TODAY -> StatusBadge(text = "today", tone = StatusTone.ATTENTION)
TaskUrgency.TODAY -> StatusBadge(text = "today", tone = StatusTone.ATTENTION, containerOverride = containerOverride, textOverride = textOverride)
TaskUrgency.THIS_WEEK -> StatusBadge(
text = date?.format(DateTimeFormatter.ofPattern("EEE")) ?: "this week",
tone = StatusTone.NEUTRAL,
containerOverride = containerOverride,
textOverride = textOverride,
)
TaskUrgency.LATER -> StatusBadge(
text = date?.format(DateTimeFormatter.ofPattern("d MMM")) ?: "this month",
tone = StatusTone.NEUTRAL,
containerOverride = containerOverride,
textOverride = textOverride,
)
TaskUrgency.NONE -> Unit
}
Expand Down
Loading
Loading