Compare commits

...

5 Commits

Author SHA1 Message Date
Geoffroy Bonneville
83f441a618 Little padding on priority text 2025-09-12 22:38:51 -04:00
Geoffroy Bonneville
80f86ebdee Lower alpha on low priority tasks 2025-09-12 22:34:44 -04:00
Geoffroy Bonneville
e250ac91d0 Simplify due date data type
Due date displays proper date
Overdue tasks display as red
2025-09-12 22:32:39 -04:00
Geoffroy Bonneville
cc25aa4b05 Display the due date in the bottom sheet 2025-09-12 16:09:29 -04:00
Geoffroy Bonneville
7939257cd6 Tapping on a task allows edition
Refactoring of the bottom sheet to allow edition
Create a Task ViewModel
2025-09-12 14:12:55 -04:00
13 changed files with 376 additions and 183 deletions

View File

@@ -47,15 +47,15 @@ android {
dependencies { dependencies {
implementation("androidx.lifecycle:lifecycle-runtime-ktx:2.9.3") implementation("androidx.lifecycle:lifecycle-runtime-ktx:2.9.3")
implementation("androidx.activity:activity-compose:1.10.1") implementation("androidx.activity:activity-compose:1.11.0")
implementation(platform("androidx.compose:compose-bom:2025.08.01")) implementation(platform("androidx.compose:compose-bom:2025.09.00"))
implementation("androidx.compose.ui:ui") implementation("androidx.compose.ui:ui")
implementation("androidx.compose.ui:ui-graphics") implementation("androidx.compose.ui:ui-graphics")
implementation("androidx.compose.ui:ui-tooling-preview") implementation("androidx.compose.ui:ui-tooling-preview")
implementation("androidx.compose.material3:material3") implementation("androidx.compose.material3:material3")
implementation("androidx.navigation:navigation-compose:2.9.4") implementation("androidx.navigation:navigation-compose:2.9.4")
implementation("androidx.hilt:hilt-navigation-compose:1.2.0") implementation("androidx.hilt:hilt-navigation-compose:1.3.0")
androidTestImplementation(platform("androidx.compose:compose-bom:2025.08.01")) androidTestImplementation(platform("androidx.compose:compose-bom:2025.09.00"))
androidTestImplementation("androidx.compose.ui:ui-test-junit4") androidTestImplementation("androidx.compose.ui:ui-test-junit4")
debugImplementation("androidx.compose.ui:ui-tooling") debugImplementation("androidx.compose.ui:ui-tooling")
debugImplementation("androidx.compose.ui:ui-test-manifest") debugImplementation("androidx.compose.ui:ui-test-manifest")

View File

@@ -2,19 +2,8 @@ package com.wismna.geoffroy.donext.data
import androidx.room.TypeConverter import androidx.room.TypeConverter
import com.wismna.geoffroy.donext.domain.model.Priority import com.wismna.geoffroy.donext.domain.model.Priority
import java.time.Instant
class Converters { class Converters {
@TypeConverter
fun fromTimestamp(value: Long?): Instant? {
return value?.let { Instant.ofEpochMilli(it) }
}
@TypeConverter
fun instantToTimestamp(instant: Instant?): Long? {
return instant?.toEpochMilli()
}
@TypeConverter @TypeConverter
fun fromPriority(priority: Priority): Int = priority.value fun fromPriority(priority: Priority): Int = priority.value

View File

@@ -4,7 +4,6 @@ import androidx.room.ColumnInfo
import androidx.room.Entity import androidx.room.Entity
import androidx.room.PrimaryKey import androidx.room.PrimaryKey
import com.wismna.geoffroy.donext.domain.model.Priority import com.wismna.geoffroy.donext.domain.model.Priority
import java.time.Instant
@Entity(tableName = "tasks") @Entity(tableName = "tasks")
data class TaskEntity( data class TaskEntity(
@@ -20,5 +19,5 @@ data class TaskEntity(
@ColumnInfo(name = "task_list_id") @ColumnInfo(name = "task_list_id")
val taskListId: Long, val taskListId: Long,
@ColumnInfo(name = "due_date") @ColumnInfo(name = "due_date")
val dueDate: Instant? = null val dueDate: Long? = null
) )

View File

@@ -1,7 +1,5 @@
package com.wismna.geoffroy.donext.domain.model package com.wismna.geoffroy.donext.domain.model
import java.time.Instant
data class Task( data class Task(
val id: Long? = null, val id: Long? = null,
val name: String, val name: String,
@@ -10,5 +8,5 @@ data class Task(
val isDone: Boolean, val isDone: Boolean,
val isDeleted: Boolean, val isDeleted: Boolean,
val taskListId: Long, val taskListId: Long,
val dueDate: Instant? = null val dueDate: Long? = null
) )

View File

@@ -8,7 +8,7 @@ import javax.inject.Inject
class AddTaskUseCase @Inject constructor( class AddTaskUseCase @Inject constructor(
private val repository: TaskRepository private val repository: TaskRepository
) { ) {
suspend operator fun invoke(taskListId: Long, title: String, description: String?, priority: Priority) { suspend operator fun invoke(taskListId: Long, title: String, description: String?, priority: Priority, dueDate: Long?) {
repository.insertTask( repository.insertTask(
Task( Task(
taskListId = taskListId, taskListId = taskListId,
@@ -17,6 +17,7 @@ class AddTaskUseCase @Inject constructor(
isDeleted = false, isDeleted = false,
isDone = false, isDone = false,
priority = priority, priority = priority,
dueDate = dueDate
) )
) )
} }

View File

@@ -0,0 +1,12 @@
package com.wismna.geoffroy.donext.domain.usecase
import com.wismna.geoffroy.donext.domain.repository.TaskRepository
import javax.inject.Inject
class DeleteTaskUseCase @Inject constructor(
private val repository: TaskRepository
) {
suspend operator fun invoke(taskId: Long) {
repository.deleteTask(taskId, true)
}
}

View File

@@ -0,0 +1,25 @@
package com.wismna.geoffroy.donext.domain.usecase
import com.wismna.geoffroy.donext.domain.model.Priority
import com.wismna.geoffroy.donext.domain.model.Task
import com.wismna.geoffroy.donext.domain.repository.TaskRepository
import javax.inject.Inject
class UpdateTaskUseCase @Inject constructor(
private val repository: TaskRepository
) {
suspend operator fun invoke(taskId: Long, taskListId: Long, title: String, description: String?, priority: Priority, dueDate: Long?) {
repository.updateTask(
Task(
id = taskId,
taskListId = taskListId,
name = title,
description = description ?: "",
isDeleted = false,
isDone = false,
priority = priority,
dueDate = dueDate
)
)
}
}

View File

@@ -8,7 +8,7 @@ import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.layout.padding import androidx.compose.foundation.layout.padding
import androidx.compose.material3.Scaffold import androidx.compose.material3.Scaffold
import androidx.compose.ui.Modifier import androidx.compose.ui.Modifier
import androidx.hilt.navigation.compose.hiltViewModel import androidx.hilt.lifecycle.viewmodel.compose.hiltViewModel
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 com.wismna.geoffroy.donext.presentation.viewmodel.MainViewModel import com.wismna.geoffroy.donext.presentation.viewmodel.MainViewModel

View File

@@ -3,22 +3,16 @@
package com.wismna.geoffroy.donext.presentation.screen package com.wismna.geoffroy.donext.presentation.screen
import androidx.compose.foundation.layout.Box import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.Spacer
import androidx.compose.foundation.layout.fillMaxSize import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.height
import androidx.compose.foundation.layout.padding import androidx.compose.foundation.layout.padding
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.Button import androidx.compose.material3.Badge
import androidx.compose.material3.BadgedBox
import androidx.compose.material3.CircularProgressIndicator import androidx.compose.material3.CircularProgressIndicator
import androidx.compose.material3.ExperimentalMaterial3Api import androidx.compose.material3.ExperimentalMaterial3Api
import androidx.compose.material3.ExtendedFloatingActionButton import androidx.compose.material3.ExtendedFloatingActionButton
import androidx.compose.material3.Icon import androidx.compose.material3.Icon
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.ModalBottomSheet
import androidx.compose.material3.OutlinedTextField
import androidx.compose.material3.PrimaryTabRow import androidx.compose.material3.PrimaryTabRow
import androidx.compose.material3.Scaffold import androidx.compose.material3.Scaffold
import androidx.compose.material3.SegmentedButton import androidx.compose.material3.SegmentedButton
@@ -27,7 +21,6 @@ import androidx.compose.material3.SingleChoiceSegmentedButtonRow
import androidx.compose.material3.Tab import androidx.compose.material3.Tab
import androidx.compose.material3.Text import androidx.compose.material3.Text
import androidx.compose.runtime.Composable import androidx.compose.runtime.Composable
import androidx.compose.runtime.LaunchedEffect
import androidx.compose.runtime.getValue import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableIntStateOf import androidx.compose.runtime.mutableIntStateOf
import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.mutableStateOf
@@ -36,21 +29,16 @@ import androidx.compose.runtime.saveable.rememberSaveable
import androidx.compose.runtime.setValue import androidx.compose.runtime.setValue
import androidx.compose.ui.Alignment import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier import androidx.compose.ui.Modifier
import androidx.compose.ui.focus.FocusRequester
import androidx.compose.ui.focus.focusRequester
import androidx.compose.ui.text.style.TextOverflow import androidx.compose.ui.text.style.TextOverflow
import androidx.compose.ui.unit.dp import androidx.hilt.lifecycle.viewmodel.compose.hiltViewModel
import androidx.hilt.navigation.compose.hiltViewModel
import androidx.navigation.NavHostController
import androidx.navigation.NavType import androidx.navigation.NavType
import androidx.navigation.compose.NavHost import androidx.navigation.compose.NavHost
import androidx.navigation.compose.composable import androidx.navigation.compose.composable
import androidx.navigation.compose.rememberNavController import androidx.navigation.compose.rememberNavController
import androidx.navigation.navArgument import androidx.navigation.navArgument
import com.wismna.geoffroy.donext.domain.model.Priority import com.wismna.geoffroy.donext.domain.model.Priority
import com.wismna.geoffroy.donext.domain.model.TaskList
import com.wismna.geoffroy.donext.presentation.viewmodel.MainViewModel import com.wismna.geoffroy.donext.presentation.viewmodel.MainViewModel
import com.wismna.geoffroy.donext.presentation.viewmodel.TaskListViewModel import com.wismna.geoffroy.donext.presentation.viewmodel.TaskViewModel
@Composable @Composable
fun MainScreen( fun MainScreen(
@@ -65,18 +53,21 @@ fun MainScreen(
CircularProgressIndicator() CircularProgressIndicator()
} }
} else { } else {
val taskViewModel: TaskViewModel = hiltViewModel()
val startDestination = viewModel.taskLists[0] val startDestination = viewModel.taskLists[0]
// TODO: get last opened tab from saved settings // TODO: get last opened tab from saved settings
var selectedDestination by rememberSaveable { mutableIntStateOf(0) } var selectedDestination by rememberSaveable { mutableIntStateOf(0) }
if (showBottomSheet) { if (showBottomSheet) {
AddTaskBottomSheet(viewModel, selectedDestination, { showBottomSheet = false }) TaskBottomSheet(taskViewModel, { showBottomSheet = false })
} }
Scaffold( Scaffold(
modifier = modifier, modifier = modifier,
floatingActionButton = { floatingActionButton = {
AddNewTaskButton { AddNewTaskButton {
val currentListId = viewModel.taskLists[selectedDestination].id
taskViewModel.startNewTask(currentListId)
showBottomSheet = true showBottomSheet = true
} }
}, topBar = { }, topBar = {
@@ -89,11 +80,19 @@ fun MainScreen(
selectedDestination = index selectedDestination = index
}, },
text = { text = {
/*BadgedBox(
badge = {
if (overdueCount > 0) {
Badge { Text(overdueCount.toString()) }
}
}
) {*/
Text( Text(
text = destination.name, text = destination.name,
maxLines = 2, maxLines = 2,
overflow = TextOverflow.Ellipsis overflow = TextOverflow.Ellipsis
) )
//}
} }
) )
} }
@@ -103,18 +102,6 @@ fun MainScreen(
.padding(contentPadding) .padding(contentPadding)
.fillMaxSize() .fillMaxSize()
) { ) {
AppNavHost(navController, startDestination, viewModel)
}
}
}
}
@Composable
fun AppNavHost(
navController: NavHostController,
startDestination: TaskList,
viewModel: MainViewModel
) {
NavHost( NavHost(
navController, navController,
startDestination = "taskList/${startDestination.id}" startDestination = "taskList/${startDestination.id}"
@@ -122,77 +109,18 @@ fun AppNavHost(
viewModel.taskLists.forEach { destination -> viewModel.taskLists.forEach { destination ->
composable( composable(
route = "taskList/{taskListId}", route = "taskList/{taskListId}",
arguments = listOf(navArgument("taskListId") { type = NavType.LongType })) { arguments = listOf(navArgument("taskListId") {
val viewModel: TaskListViewModel = hiltViewModel<TaskListViewModel>() type = NavType.LongType
TaskListScreen(viewModel) })
}
}
}
}
@Composable
fun AddTaskBottomSheet(
viewModel: MainViewModel,
selectedListIndex: Int,
onDismiss: () -> Unit
) {
val titleFocusRequester = remember { FocusRequester() }
LaunchedEffect(Unit) {
titleFocusRequester.requestFocus()
}
ModalBottomSheet(onDismissRequest = onDismiss) {
Column(Modifier.padding(16.dp)) {
Text(
"New Task",
style = MaterialTheme.typography.titleLarge
)
Spacer(Modifier.height(8.dp))
// --- Title ---
OutlinedTextField(
value = viewModel.title,
singleLine = true,
onValueChange = { viewModel.onTitleChanged(it) },
label = { Text("Title") },
modifier = Modifier
.fillMaxWidth()
.focusRequester(titleFocusRequester),
isError = !viewModel.isTitleValid && viewModel.title.isNotEmpty(),
)
Spacer(Modifier.height(8.dp))
// --- Description ---
OutlinedTextField(
value = viewModel.description,
onValueChange = { viewModel.onDescriptionChanged(it) },
label = { Text("Description") },
maxLines = 3,
modifier = Modifier.fillMaxWidth()
)
Spacer(Modifier.height(8.dp))
// --- Priority ---
Text("Priority", style = MaterialTheme.typography.labelLarge)
Spacer(Modifier.height(4.dp))
SingleChoiceSegmentedButton(
value = viewModel.priority,
onValueChange = { viewModel.onPriorityChanged(it) }
)
Spacer(Modifier.height(16.dp))
// --- Add Button ---
Button(
onClick = {
val currentListId = viewModel.taskLists[selectedListIndex].id
viewModel.createTask(currentListId)
onDismiss()
viewModel.resetTaskForm()
},
modifier = Modifier.align(Alignment.End)
) { ) {
Text("Add") TaskListScreen(
onTaskClick = { task ->
taskViewModel.startEditTask(task)
showBottomSheet = true
})
}
}
}
} }
} }
} }

View File

@@ -1,5 +1,6 @@
package com.wismna.geoffroy.donext.presentation.screen package com.wismna.geoffroy.donext.presentation.screen
import androidx.compose.foundation.clickable
import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.Row import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.Spacer import androidx.compose.foundation.layout.Spacer
@@ -16,6 +17,7 @@ import androidx.compose.material3.HorizontalDivider
import androidx.compose.material3.MaterialTheme 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.draw.alpha import androidx.compose.ui.draw.alpha
import androidx.compose.ui.draw.clip import androidx.compose.ui.draw.clip
@@ -23,13 +25,16 @@ import androidx.compose.ui.text.font.FontWeight
import androidx.compose.ui.text.style.TextDecoration import androidx.compose.ui.text.style.TextDecoration
import androidx.compose.ui.text.style.TextOverflow import androidx.compose.ui.text.style.TextOverflow
import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.dp
import androidx.hilt.navigation.compose.hiltViewModel import androidx.hilt.lifecycle.viewmodel.compose.hiltViewModel
import com.wismna.geoffroy.donext.domain.model.Priority import com.wismna.geoffroy.donext.domain.model.Priority
import com.wismna.geoffroy.donext.domain.model.Task import com.wismna.geoffroy.donext.domain.model.Task
import com.wismna.geoffroy.donext.presentation.viewmodel.TaskListViewModel import com.wismna.geoffroy.donext.presentation.viewmodel.TaskListViewModel
import java.time.Instant
import java.time.LocalDate
import java.time.ZoneOffset
@Composable @Composable
fun TaskListScreen(viewModel: TaskListViewModel = hiltViewModel()) { fun TaskListScreen(viewModel: TaskListViewModel = hiltViewModel(), onTaskClick: (Task) -> Unit) {
val tasks = viewModel.tasks val tasks = viewModel.tasks
LazyColumn( LazyColumn(
@@ -58,6 +63,7 @@ fun TaskListScreen(viewModel: TaskListViewModel = hiltViewModel()) {
TaskItem( TaskItem(
task = task, task = task,
onClick = { onTaskClick(task) },
onToggleDone = { isChecked -> onToggleDone = { isChecked ->
viewModel.updateTaskDone(task.id!!, isChecked) viewModel.updateTaskDone(task.id!!, isChecked)
} }
@@ -69,15 +75,26 @@ fun TaskListScreen(viewModel: TaskListViewModel = hiltViewModel()) {
@Composable @Composable
fun TaskItem( fun TaskItem(
task: Task, task: Task,
onClick: () -> Unit,
onToggleDone: (Boolean) -> Unit onToggleDone: (Boolean) -> Unit
) { ) {
val today = remember {
LocalDate.now(ZoneOffset.UTC)
}
val isOverdue = task.dueDate?.let { millis ->
val dueDate = Instant.ofEpochMilli(millis)
.atZone(ZoneOffset.UTC)
.toLocalDate()
dueDate.isBefore(today)
} ?: false
val baseStyle = MaterialTheme.typography.bodyLarge.copy( val baseStyle = MaterialTheme.typography.bodyLarge.copy(
fontWeight = when (task.priority) { fontWeight = when (task.priority) {
Priority.HIGH -> FontWeight.Bold Priority.HIGH -> FontWeight.Bold
Priority.NORMAL -> FontWeight.Normal Priority.NORMAL -> FontWeight.Normal
Priority.LOW -> FontWeight.Normal Priority.LOW -> FontWeight.Normal
}, },
color = when (task.priority) { color = if (isOverdue && !task.isDone) MaterialTheme.colorScheme.error else when (task.priority) {
Priority.HIGH -> MaterialTheme.colorScheme.onSurface Priority.HIGH -> MaterialTheme.colorScheme.onSurface
Priority.NORMAL -> MaterialTheme.colorScheme.onSurface Priority.NORMAL -> MaterialTheme.colorScheme.onSurface
Priority.LOW -> MaterialTheme.colorScheme.onSurfaceVariant Priority.LOW -> MaterialTheme.colorScheme.onSurfaceVariant
@@ -87,15 +104,10 @@ fun TaskItem(
Row( Row(
modifier = Modifier modifier = Modifier
.fillMaxWidth() .fillMaxWidth()
.clickable { onClick() }
.padding(8.dp) .padding(8.dp)
.alpha(if (task.isDone) 0.5f else 1f), .alpha(if (task.isDone || task.priority == Priority.LOW) 0.5f else 1f),
) { ) {
/*Row(
modifier = Modifier
.fillMaxWidth()
.padding(8.dp),
verticalAlignment = Alignment.CenterVertically
) {*/
Checkbox( Checkbox(
checked = task.isDone, checked = task.isDone,
onCheckedChange = onToggleDone, onCheckedChange = onToggleDone,

View File

@@ -0,0 +1,175 @@
package com.wismna.geoffroy.donext.presentation.screen
import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.Spacer
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.height
import androidx.compose.foundation.layout.padding
import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.filled.Clear
import androidx.compose.material.icons.filled.DateRange
import androidx.compose.material3.Button
import androidx.compose.material3.ButtonDefaults
import androidx.compose.material3.DatePicker
import androidx.compose.material3.DatePickerDialog
import androidx.compose.material3.ExperimentalMaterial3Api
import androidx.compose.material3.Icon
import androidx.compose.material3.IconButton
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.ModalBottomSheet
import androidx.compose.material3.OutlinedTextField
import androidx.compose.material3.Text
import androidx.compose.material3.TextButton
import androidx.compose.material3.rememberDatePickerState
import androidx.compose.runtime.Composable
import androidx.compose.runtime.LaunchedEffect
import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember
import androidx.compose.runtime.setValue
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.focus.FocusRequester
import androidx.compose.ui.focus.focusRequester
import androidx.compose.ui.unit.dp
import com.wismna.geoffroy.donext.presentation.viewmodel.TaskViewModel
import java.time.Instant
import java.time.ZoneOffset
import java.time.format.DateTimeFormatter
import java.time.format.FormatStyle
@OptIn(ExperimentalMaterial3Api::class)
@Composable
fun TaskBottomSheet(
viewModel: TaskViewModel,
onDismiss: () -> Unit
) {
val titleFocusRequester = remember { FocusRequester() }
LaunchedEffect(Unit) {
titleFocusRequester.requestFocus()
}
ModalBottomSheet(onDismissRequest = onDismiss) {
Column(Modifier.padding(16.dp)) {
Text(
"New Task",
style = MaterialTheme.typography.titleLarge
)
Spacer(Modifier.height(8.dp))
// --- Title ---
OutlinedTextField(
value = viewModel.title,
singleLine = true,
onValueChange = { viewModel.onTitleChanged(it) },
label = { Text("Title") },
modifier = Modifier
.fillMaxWidth()
.focusRequester(titleFocusRequester),
isError = viewModel.title.isEmpty(),
)
Spacer(Modifier.height(12.dp))
// --- Description ---
OutlinedTextField(
value = viewModel.description,
onValueChange = { viewModel.onDescriptionChanged(it) },
label = { Text("Description") },
maxLines = 3,
modifier = Modifier.fillMaxWidth()
)
Spacer(Modifier.height(12.dp))
// --- Priority ---
Row (
modifier = Modifier.fillMaxWidth().padding(start = 17.dp),
horizontalArrangement = Arrangement.SpaceBetween,
verticalAlignment = Alignment.CenterVertically) {
Text("Priority", style = MaterialTheme.typography.labelLarge)
SingleChoiceSegmentedButton(
value = viewModel.priority,
onValueChange = { viewModel.onPriorityChanged(it) }
)
}
Spacer(Modifier.height(12.dp))
// --- Due Date ---
var showDatePicker by remember { mutableStateOf(false) }
val formattedDate = viewModel.dueDate?.let {
Instant.ofEpochMilli(it)
.atZone(ZoneOffset.UTC)
.toLocalDate()
.format(DateTimeFormatter.ofLocalizedDate(FormatStyle.MEDIUM))
} ?: ""
OutlinedTextField(
value = formattedDate,
onValueChange = {},
readOnly = true,
label = { Text("Due Date") },
trailingIcon = {
Row {
if (viewModel.dueDate != null) {
IconButton(onClick = { viewModel.onDueDateChanged(null) }) {
Icon(Icons.Default.Clear, contentDescription = "Clear due date")
}
}
IconButton(onClick = { showDatePicker = true }) {
Icon(Icons.Default.DateRange, contentDescription = "Pick due date")
}
}
},
modifier = Modifier.fillMaxWidth()
)
if (showDatePicker) {
val datePickerState = rememberDatePickerState(initialSelectedDateMillis = viewModel.dueDate)
DatePickerDialog(
onDismissRequest = { showDatePicker = false },
confirmButton = {
TextButton(onClick = {
datePickerState.selectedDateMillis?.let { viewModel.onDueDateChanged(it) }
showDatePicker = false
}) { Text("OK") }
},
dismissButton = {
TextButton(onClick = { showDatePicker = false }) { Text("Cancel") }
}
) {
DatePicker(state = datePickerState)
}
}
Spacer(Modifier.height(16.dp))
Row (
modifier = Modifier.fillMaxWidth(),
horizontalArrangement = if (viewModel.isEditing()) Arrangement.SpaceBetween else Arrangement.End) {
// --- Delete Button ---
if (viewModel.isEditing()) {
Button(
onClick = { viewModel.delete(); onDismiss() },
colors = ButtonDefaults.textButtonColors(
contentColor = MaterialTheme.colorScheme.error
)
) { Text("Delete") }
}
// --- Save Button ---
Button(
onClick = {
viewModel.save()
onDismiss()
},
enabled = viewModel.title.isNotBlank(),
//modifier = Modifier.align(Alignment.End)
) {
Text(if (viewModel.isEditing()) "Save" else "Create")
}
}
}
}
}

View File

@@ -5,20 +5,16 @@ import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.setValue import androidx.compose.runtime.setValue
import androidx.lifecycle.ViewModel import androidx.lifecycle.ViewModel
import androidx.lifecycle.viewModelScope import androidx.lifecycle.viewModelScope
import com.wismna.geoffroy.donext.domain.model.Priority
import com.wismna.geoffroy.donext.domain.model.TaskList import com.wismna.geoffroy.donext.domain.model.TaskList
import com.wismna.geoffroy.donext.domain.usecase.AddTaskUseCase
import com.wismna.geoffroy.donext.domain.usecase.GetTaskListsUseCase import com.wismna.geoffroy.donext.domain.usecase.GetTaskListsUseCase
import dagger.hilt.android.lifecycle.HiltViewModel import dagger.hilt.android.lifecycle.HiltViewModel
import kotlinx.coroutines.flow.launchIn import kotlinx.coroutines.flow.launchIn
import kotlinx.coroutines.flow.onEach import kotlinx.coroutines.flow.onEach
import kotlinx.coroutines.launch
import javax.inject.Inject import javax.inject.Inject
@HiltViewModel @HiltViewModel
class MainViewModel @Inject constructor( class MainViewModel @Inject constructor(
getTaskLists: GetTaskListsUseCase, getTaskLists: GetTaskListsUseCase
private val addTask: AddTaskUseCase
) : ViewModel() { ) : ViewModel() {
var taskLists by mutableStateOf<List<TaskList>>(emptyList()) var taskLists by mutableStateOf<List<TaskList>>(emptyList())
@@ -26,17 +22,6 @@ class MainViewModel @Inject constructor(
var isLoading by mutableStateOf(true) var isLoading by mutableStateOf(true)
private set private set
// --- Form state ---
var title by mutableStateOf("")
private set
var description by mutableStateOf("")
private set
var priority by mutableStateOf(Priority.NORMAL)
private set
val isTitleValid: Boolean
get() = title.isNotBlank()
init { init {
getTaskLists() getTaskLists()
.onEach { lists -> .onEach { lists ->
@@ -45,27 +30,4 @@ class MainViewModel @Inject constructor(
} }
.launchIn(viewModelScope) .launchIn(viewModelScope)
} }
fun createTask(taskListId: Long) {
if (!isTitleValid) return
viewModelScope.launch {
addTask(taskListId, title, description, priority)
}
}
fun onTitleChanged(newTitle: String) {
title = newTitle
}
fun onDescriptionChanged(newDesc: String) {
description = newDesc
}
fun onPriorityChanged(newPriority: Priority) {
priority = newPriority
}
fun resetTaskForm() {
title = ""
description = ""
priority = Priority.NORMAL
}
} }

View File

@@ -0,0 +1,92 @@
package com.wismna.geoffroy.donext.presentation.viewmodel
import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.setValue
import androidx.lifecycle.ViewModel
import androidx.lifecycle.viewModelScope
import com.wismna.geoffroy.donext.domain.model.Priority
import com.wismna.geoffroy.donext.domain.model.Task
import com.wismna.geoffroy.donext.domain.usecase.AddTaskUseCase
import com.wismna.geoffroy.donext.domain.usecase.DeleteTaskUseCase
import com.wismna.geoffroy.donext.domain.usecase.UpdateTaskUseCase
import dagger.hilt.android.lifecycle.HiltViewModel
import kotlinx.coroutines.launch
import javax.inject.Inject
@HiltViewModel
class TaskViewModel @Inject constructor(
private val createTaskUseCase: AddTaskUseCase,
private val updateTaskUseCase: UpdateTaskUseCase,
private val deleteTaskUseCase: DeleteTaskUseCase
) : ViewModel() {
var title by mutableStateOf("")
private set
var description by mutableStateOf("")
private set
var priority by mutableStateOf(Priority.NORMAL)
private set
var dueDate by mutableStateOf<Long?>(null)
private set
private var editingTaskId: Long? = null
private var taskListId: Long? = null
fun isEditing(): Boolean = editingTaskId != null
fun startNewTask(selectedListId: Long) {
editingTaskId = null
taskListId = selectedListId
title = ""
description = ""
priority = Priority.NORMAL
dueDate = null
}
fun startEditTask(task: Task) {
editingTaskId = task.id
taskListId = task.taskListId
title = task.name
description = task.description ?: ""
priority = task.priority
dueDate = task.dueDate
}
fun onTitleChanged(value: String) { title = value }
fun onDescriptionChanged(value: String) { description = value }
fun onPriorityChanged(value: Priority) { priority = value }
fun onDueDateChanged(value: Long?) { dueDate = value }
fun save(onDone: (() -> Unit)? = null) {
if (title.isBlank()) return
viewModelScope.launch {
if (isEditing()) {
updateTaskUseCase(editingTaskId!!, taskListId!!, title, description, priority, dueDate)
} else {
createTaskUseCase(taskListId!!, title, description, priority, dueDate)
}
// reset state after save
reset()
onDone?.invoke()
}
}
fun delete() {
editingTaskId?.let { id ->
viewModelScope.launch {
deleteTaskUseCase(id)
reset()
}
}
}
fun reset() {
editingTaskId = null
taskListId = null
title = ""
description = ""
priority = Priority.NORMAL
}
}