Theme is now working properly

Fix overdue use case
Allow selecting today as due date
Dates are now real badges
Animate done tasks
This commit is contained in:
Geoffroy Bonneville
2025-09-17 15:14:33 -04:00
parent 926a9bf66b
commit 78ce584900
9 changed files with 139 additions and 80 deletions

View File

@@ -7,7 +7,7 @@
<activity <activity
android:name="com.wismna.geoffroy.donext.presentation.MainActivity" android:name="com.wismna.geoffroy.donext.presentation.MainActivity"
android:exported="true" android:exported="true"
android:label="@string/title_activity_main" android:label="DoNext"
android:theme="@style/Theme.DoNext"> android:theme="@style/Theme.DoNext">
<intent-filter> <intent-filter>
<action android:name="android.intent.action.MAIN" /> <action android:name="android.intent.action.MAIN" />

View File

@@ -3,13 +3,19 @@ package com.wismna.geoffroy.donext.domain.usecase
import com.wismna.geoffroy.donext.domain.model.TaskListWithOverdue import com.wismna.geoffroy.donext.domain.model.TaskListWithOverdue
import com.wismna.geoffroy.donext.domain.repository.TaskRepository import com.wismna.geoffroy.donext.domain.repository.TaskRepository
import kotlinx.coroutines.flow.Flow import kotlinx.coroutines.flow.Flow
import java.time.Instant import java.time.LocalDate
import java.time.ZoneOffset
import javax.inject.Inject import javax.inject.Inject
class GetTaskListsWithOverdueUseCase @Inject constructor( class GetTaskListsWithOverdueUseCase @Inject constructor(
private val taskRepository: TaskRepository private val taskRepository: TaskRepository
) { ) {
operator fun invoke(): Flow<List<TaskListWithOverdue>> { operator fun invoke(): Flow<List<TaskListWithOverdue>> {
return taskRepository.getTaskListsWithOverdue(Instant.parse("2025-09-15T12:00:00Z").toEpochMilli()) return taskRepository.getTaskListsWithOverdue(
LocalDate.now()
.atStartOfDay(ZoneOffset.UTC) // or system default
.toInstant()
.toEpochMilli()
)
} }
} }

View File

@@ -4,6 +4,7 @@ import android.os.Bundle
import androidx.activity.ComponentActivity import androidx.activity.ComponentActivity
import androidx.activity.compose.setContent import androidx.activity.compose.setContent
import androidx.activity.enableEdgeToEdge import androidx.activity.enableEdgeToEdge
import androidx.compose.foundation.isSystemInDarkTheme
import com.wismna.geoffroy.donext.presentation.screen.MainScreen import com.wismna.geoffroy.donext.presentation.screen.MainScreen
import com.wismna.geoffroy.donext.presentation.ui.theme.DoNextTheme import com.wismna.geoffroy.donext.presentation.ui.theme.DoNextTheme
import dagger.hilt.android.AndroidEntryPoint import dagger.hilt.android.AndroidEntryPoint
@@ -14,7 +15,7 @@ class MainActivity : ComponentActivity() {
super.onCreate(savedInstanceState) super.onCreate(savedInstanceState)
enableEdgeToEdge() enableEdgeToEdge()
setContent { setContent {
DoNextTheme { MainScreen() } DoNextTheme(darkTheme = isSystemInDarkTheme(), dynamicColor = false) { MainScreen() }
} }
} }
} }

View File

@@ -107,7 +107,6 @@ fun MainScreen(
onNavigate = { route -> onNavigate = { route ->
scope.launch { drawerState.close() } scope.launch { drawerState.close() }
navController.navigate(route) { navController.navigate(route) {
//launchSingleTop = true
restoreState = true restoreState = true
} }
} }

View File

@@ -16,6 +16,7 @@ import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.layout.size import androidx.compose.foundation.layout.size
import androidx.compose.foundation.shape.CircleShape import androidx.compose.foundation.shape.CircleShape
import androidx.compose.foundation.shape.RoundedCornerShape import androidx.compose.foundation.shape.RoundedCornerShape
import androidx.compose.material3.Badge
import androidx.compose.material3.Checkbox import androidx.compose.material3.Checkbox
import androidx.compose.material3.MaterialTheme import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.Text import androidx.compose.material3.Text
@@ -88,23 +89,19 @@ fun TaskItemScreen(
// Due date badge // Due date badge
viewModel.dueDateText?.let { dueMillis -> viewModel.dueDateText?.let { dueMillis ->
Box( Badge(
modifier = Modifier modifier = Modifier
.align( .align(
if (viewModel.description.isNullOrBlank()) Alignment.CenterEnd if (viewModel.description.isNullOrBlank()) Alignment.CenterEnd
else Alignment.TopEnd else Alignment.TopEnd
) ),
.background( containerColor = if (viewModel.isOverdue) MaterialTheme.colorScheme.error else MaterialTheme.colorScheme.primaryContainer
color = if (viewModel.isOverdue) MaterialTheme.colorScheme.error
else MaterialTheme.colorScheme.primary.copy(alpha = 0.1f),
shape = RoundedCornerShape(12.dp)
)
.padding(horizontal = 8.dp, vertical = 2.dp)
) { ) {
Text( Text(
modifier = Modifier.padding(start = 1.dp, end = 1.dp),
text = viewModel.dueDateText, text = viewModel.dueDateText,
style = MaterialTheme.typography.bodySmall, color = if (viewModel.isOverdue) Color.White else MaterialTheme.colorScheme.onPrimaryContainer,
color = if (viewModel.isOverdue) Color.White else MaterialTheme.colorScheme.primary style = MaterialTheme.typography.bodySmall
) )
} }
} }

View File

@@ -1,19 +1,20 @@
package com.wismna.geoffroy.donext.presentation.screen package com.wismna.geoffroy.donext.presentation.screen
import androidx.compose.foundation.layout.Spacer import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.PaddingValues
import androidx.compose.foundation.layout.fillMaxSize import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.layout.height import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.padding import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.lazy.LazyColumn import androidx.compose.foundation.lazy.LazyColumn
import androidx.compose.foundation.lazy.itemsIndexed import androidx.compose.foundation.lazy.items
import androidx.compose.material.icons.Icons import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.filled.Add import androidx.compose.material.icons.filled.Add
import androidx.compose.material3.ExtendedFloatingActionButton import androidx.compose.material3.ExtendedFloatingActionButton
import androidx.compose.material3.HorizontalDivider import androidx.compose.material3.HorizontalDivider
import androidx.compose.material3.Icon import androidx.compose.material3.Icon
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.Text import androidx.compose.material3.Text
import androidx.compose.runtime.Composable import androidx.compose.runtime.Composable
import androidx.compose.runtime.remember
import androidx.compose.ui.Modifier import androidx.compose.ui.Modifier
import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.dp
import androidx.hilt.lifecycle.viewmodel.compose.hiltViewModel import androidx.hilt.lifecycle.viewmodel.compose.hiltViewModel
@@ -29,36 +30,53 @@ fun TaskListScreen(
onTaskClick: (Task) -> Unit) { onTaskClick: (Task) -> Unit) {
val tasks = viewModel.tasks val tasks = viewModel.tasks
// Split tasks into active and done
val (active, done) = remember(tasks) {
tasks.partition { !it.isDone }
}
LazyColumn( LazyColumn(
modifier = modifier.fillMaxSize().padding() modifier = Modifier.fillMaxSize(),
contentPadding = PaddingValues(8.dp),
verticalArrangement = Arrangement.spacedBy(8.dp)
) { ) {
itemsIndexed(tasks, key = { id, task -> task.id!! }) { index, task -> // Active tasks section
if (index > 0) { items(
val prev = tasks[index - 1] items = active,
key = { it.id!! }
when { ) { task ->
// Divider between non-done and done tasks
!prev.isDone && task.isDone -> {
HorizontalDivider(
thickness = 1.dp,
color = MaterialTheme.colorScheme.onSurface.copy(alpha = 0.2f),
modifier = Modifier.padding(vertical = 8.dp)
)
}
// Extra spacing between different priorities (only if done status is same)
prev.priority != task.priority && prev.isDone == task.isDone -> {
Spacer(modifier = Modifier.height(20.dp))
}
}
}
TaskItemScreen( TaskItemScreen(
modifier = Modifier.animateItem(), modifier = Modifier.animateItem(),
viewModel = TaskItemViewModel(task), viewModel = TaskItemViewModel(task),
onClick = { onTaskClick(task) }, onClick = { onTaskClick(task) },
onToggleDone = { isChecked -> onToggleDone = { checked ->
viewModel.updateTaskDone(task.id!!, isChecked) viewModel.updateTaskDone(task.id!!, checked)
}
)
}
// Divider between active and done (optional)
if (done.isNotEmpty() && active.isNotEmpty()) {
item {
HorizontalDivider(
modifier = Modifier
.fillMaxWidth()
.padding(vertical = 4.dp)
)
}
}
// Done tasks section
items(
items = done,
key = { it.id!! }
) { task ->
TaskItemScreen(
modifier = Modifier.animateItem(),
viewModel = TaskItemViewModel(task),
onClick = { onTaskClick(task) },
onToggleDone = { checked ->
viewModel.updateTaskDone(task.id!!, checked)
} }
) )
} }

View File

@@ -143,8 +143,8 @@ fun TaskBottomSheet(
initialSelectedDateMillis = viewModel.dueDate, initialSelectedDateMillis = viewModel.dueDate,
selectableDates = object: SelectableDates { selectableDates = object: SelectableDates {
override fun isSelectableDate(utcTimeMillis: Long): Boolean { override fun isSelectableDate(utcTimeMillis: Long): Boolean {
val todayStartMillis = LocalDate.now() val todayStartMillis = LocalDate.now(ZoneOffset.UTC)
.atStartOfDay(ZoneId.systemDefault()) .atStartOfDay(ZoneOffset.UTC)
.toInstant() .toInstant()
.toEpochMilli() .toEpochMilli()
return utcTimeMillis >= todayStartMillis return utcTimeMillis >= todayStartMillis
@@ -173,6 +173,7 @@ fun TaskBottomSheet(
Row ( Row (
modifier = Modifier.fillMaxWidth(), modifier = Modifier.fillMaxWidth(),
horizontalArrangement = if (viewModel.isEditing()) Arrangement.SpaceBetween else Arrangement.End) { horizontalArrangement = if (viewModel.isEditing()) Arrangement.SpaceBetween else Arrangement.End) {
// --- Delete Button --- // --- Delete Button ---
if (viewModel.isEditing()) { if (viewModel.isEditing()) {
Button( Button(

View File

@@ -2,10 +2,26 @@ package com.wismna.geoffroy.donext.presentation.ui.theme
import androidx.compose.ui.graphics.Color import androidx.compose.ui.graphics.Color
val Purple80 = Color(0xFFD0BCFF) // Primary shades
val PurpleGrey80 = Color(0xFFCCC2DC) val Purple80 = Color(0xFFD0BCFF) // Light theme primary
val Pink80 = Color(0xFFEFB8C8) val Purple40 = Color(0xFF6650A4) // Dark theme primary
val Purple40 = Color(0xFF6650a4) val PurpleGrey80 = Color(0xFFCCC2DC)
val PurpleGrey40 = Color(0xFF625b71) val PurpleGrey40 = Color(0xFF625B71)
val Pink80 = Color(0xFFEFB8C8)
val Pink40 = Color(0xFF7D5260) val Pink40 = Color(0xFF7D5260)
// Container variants
val Purple80Container = Color(0xFFEADDFF)
val Purple40Container = Color(0xFF381E72)
val PurpleGrey80Container = Color(0xFFE8DEF8)
val PurpleGrey40Container = Color(0xFF4A4458)
val Pink80Container = Color(0xFFFFD8E4)
val Pink40Container = Color(0xFF633B48)
// Surface container
val LightSurfaceContainer = Color(0xFFF5F3FF)
val DarkSurfaceContainer = Color(0xFF1E1B2C)

View File

@@ -8,34 +8,12 @@ import androidx.compose.material3.dynamicDarkColorScheme
import androidx.compose.material3.dynamicLightColorScheme import androidx.compose.material3.dynamicLightColorScheme
import androidx.compose.material3.lightColorScheme import androidx.compose.material3.lightColorScheme
import androidx.compose.runtime.Composable import androidx.compose.runtime.Composable
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.platform.LocalContext import androidx.compose.ui.platform.LocalContext
private val DarkColorScheme = darkColorScheme(
primary = Purple80,
secondary = PurpleGrey80,
tertiary = Pink80
)
private val LightColorScheme = lightColorScheme(
primary = Purple40,
secondary = PurpleGrey40,
tertiary = Pink40
/* Other default colors to override
background = Color(0xFFFFFBFE),
surface = Color(0xFFFFFBFE),
onPrimary = Color.White,
onSecondary = Color.White,
onTertiary = Color.White,
onBackground = Color(0xFF1C1B1F),
onSurface = Color(0xFF1C1B1F),
*/
)
@Composable @Composable
fun DoNextTheme( fun DoNextTheme(
darkTheme: Boolean = isSystemInDarkTheme(), darkTheme: Boolean = isSystemInDarkTheme(),
// Dynamic color is available on Android 12+
dynamicColor: Boolean = true, dynamicColor: Boolean = true,
content: @Composable () -> Unit content: @Composable () -> Unit
) { ) {
@@ -45,8 +23,51 @@ fun DoNextTheme(
if (darkTheme) dynamicDarkColorScheme(context) else dynamicLightColorScheme(context) if (darkTheme) dynamicDarkColorScheme(context) else dynamicLightColorScheme(context)
} }
darkTheme -> DarkColorScheme darkTheme -> darkColorScheme(
else -> LightColorScheme primary = Purple40,
onPrimary = Color.White,
primaryContainer = Purple40Container,
onPrimaryContainer = Color.White,
secondary = PurpleGrey40,
onSecondary = Color.White,
secondaryContainer = PurpleGrey40Container,
onSecondaryContainer = Color.White,
tertiary = Pink40,
onTertiary = Color.White,
tertiaryContainer = Pink40Container,
onTertiaryContainer = Color.White,
background = Color(0xFF121212),
onBackground = Color.White,
surface = Color(0xFF121212),
onSurface = Color.White,
surfaceVariant = DarkSurfaceContainer,
onSurfaceVariant = Color.White,
error = Color(0xFFCF6679),
onError = Color.Black
)
else -> lightColorScheme(
primary = Purple80,
onPrimary = Color.Black,
primaryContainer = Purple80Container,
onPrimaryContainer = Color.Black,
secondary = PurpleGrey80,
onSecondary = Color.Black,
secondaryContainer = PurpleGrey80Container,
onSecondaryContainer = Color.Black,
tertiary = Pink80,
onTertiary = Color.Black,
tertiaryContainer = Pink80Container,
onTertiaryContainer = Color.Black,
background = Color(0xFFFFFBFE),
onBackground = Color.Black,
surface = Color(0xFFFFFBFE),
onSurface = Color.Black,
surfaceVariant = LightSurfaceContainer,
onSurfaceVariant = Color.Black,
error = Color(0xFFB00020),
onError = Color.White
)
} }
MaterialTheme( MaterialTheme(