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 {
implementation("androidx.lifecycle:lifecycle-runtime-ktx:2.9.3")
implementation("androidx.activity:activity-compose:1.10.1")
implementation(platform("androidx.compose:compose-bom:2025.08.01"))
implementation("androidx.activity:activity-compose:1.11.0")
implementation(platform("androidx.compose:compose-bom:2025.09.00"))
implementation("androidx.compose.ui:ui")
implementation("androidx.compose.ui:ui-graphics")
implementation("androidx.compose.ui:ui-tooling-preview")
implementation("androidx.compose.material3:material3")
implementation("androidx.navigation:navigation-compose:2.9.4")
implementation("androidx.hilt:hilt-navigation-compose:1.2.0")
androidTestImplementation(platform("androidx.compose:compose-bom:2025.08.01"))
implementation("androidx.hilt:hilt-navigation-compose:1.3.0")
androidTestImplementation(platform("androidx.compose:compose-bom:2025.09.00"))
androidTestImplementation("androidx.compose.ui:ui-test-junit4")
debugImplementation("androidx.compose.ui:ui-tooling")
debugImplementation("androidx.compose.ui:ui-test-manifest")

View File

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

View File

@@ -4,7 +4,6 @@ import androidx.room.ColumnInfo
import androidx.room.Entity
import androidx.room.PrimaryKey
import com.wismna.geoffroy.donext.domain.model.Priority
import java.time.Instant
@Entity(tableName = "tasks")
data class TaskEntity(
@@ -20,5 +19,5 @@ data class TaskEntity(
@ColumnInfo(name = "task_list_id")
val taskListId: Long,
@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
import java.time.Instant
data class Task(
val id: Long? = null,
val name: String,
@@ -10,5 +8,5 @@ data class Task(
val isDone: Boolean,
val isDeleted: Boolean,
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(
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(
Task(
taskListId = taskListId,
@@ -17,6 +17,7 @@ class AddTaskUseCase @Inject constructor(
isDeleted = false,
isDone = false,
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.material3.Scaffold
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.ui.theme.DoNextTheme
import com.wismna.geoffroy.donext.presentation.viewmodel.MainViewModel

View File

@@ -3,22 +3,16 @@
package com.wismna.geoffroy.donext.presentation.screen
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.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.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.ExperimentalMaterial3Api
import androidx.compose.material3.ExtendedFloatingActionButton
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.Scaffold
import androidx.compose.material3.SegmentedButton
@@ -27,7 +21,6 @@ import androidx.compose.material3.SingleChoiceSegmentedButtonRow
import androidx.compose.material3.Tab
import androidx.compose.material3.Text
import androidx.compose.runtime.Composable
import androidx.compose.runtime.LaunchedEffect
import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableIntStateOf
import androidx.compose.runtime.mutableStateOf
@@ -36,21 +29,16 @@ import androidx.compose.runtime.saveable.rememberSaveable
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.text.style.TextOverflow
import androidx.compose.ui.unit.dp
import androidx.hilt.navigation.compose.hiltViewModel
import androidx.navigation.NavHostController
import androidx.hilt.lifecycle.viewmodel.compose.hiltViewModel
import androidx.navigation.NavType
import androidx.navigation.compose.NavHost
import androidx.navigation.compose.composable
import androidx.navigation.compose.rememberNavController
import androidx.navigation.navArgument
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.TaskListViewModel
import com.wismna.geoffroy.donext.presentation.viewmodel.TaskViewModel
@Composable
fun MainScreen(
@@ -65,18 +53,21 @@ fun MainScreen(
CircularProgressIndicator()
}
} else {
val taskViewModel: TaskViewModel = hiltViewModel()
val startDestination = viewModel.taskLists[0]
// TODO: get last opened tab from saved settings
var selectedDestination by rememberSaveable { mutableIntStateOf(0) }
if (showBottomSheet) {
AddTaskBottomSheet(viewModel, selectedDestination, { showBottomSheet = false })
TaskBottomSheet(taskViewModel, { showBottomSheet = false })
}
Scaffold(
modifier = modifier,
floatingActionButton = {
AddNewTaskButton {
val currentListId = viewModel.taskLists[selectedDestination].id
taskViewModel.startNewTask(currentListId)
showBottomSheet = true
}
}, topBar = {
@@ -89,11 +80,19 @@ fun MainScreen(
selectedDestination = index
},
text = {
/*BadgedBox(
badge = {
if (overdueCount > 0) {
Badge { Text(overdueCount.toString()) }
}
}
) {*/
Text(
text = destination.name,
maxLines = 2,
overflow = TextOverflow.Ellipsis
)
//}
}
)
}
@@ -102,18 +101,6 @@ fun MainScreen(
Box(modifier = Modifier
.padding(contentPadding)
.fillMaxSize()
) {
AppNavHost(navController, startDestination, viewModel)
}
}
}
}
@Composable
fun AppNavHost(
navController: NavHostController,
startDestination: TaskList,
viewModel: MainViewModel
) {
NavHost(
navController,
@@ -122,77 +109,18 @@ fun AppNavHost(
viewModel.taskLists.forEach { destination ->
composable(
route = "taskList/{taskListId}",
arguments = listOf(navArgument("taskListId") { type = NavType.LongType })) {
val viewModel: TaskListViewModel = hiltViewModel<TaskListViewModel>()
TaskListScreen(viewModel)
}
}
}
}
@Composable
fun AddTaskBottomSheet(
viewModel: MainViewModel,
selectedListIndex: Int,
onDismiss: () -> Unit
arguments = listOf(navArgument("taskListId") {
type = NavType.LongType
})
) {
val titleFocusRequester = remember { FocusRequester() }
LaunchedEffect(Unit) {
titleFocusRequester.requestFocus()
TaskListScreen(
onTaskClick = { task ->
taskViewModel.startEditTask(task)
showBottomSheet = true
})
}
}
}
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")
}
}
}

View File

@@ -1,5 +1,6 @@
package com.wismna.geoffroy.donext.presentation.screen
import androidx.compose.foundation.clickable
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.Spacer
@@ -16,6 +17,7 @@ import androidx.compose.material3.HorizontalDivider
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.Text
import androidx.compose.runtime.Composable
import androidx.compose.runtime.remember
import androidx.compose.ui.Modifier
import androidx.compose.ui.draw.alpha
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.TextOverflow
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.Task
import com.wismna.geoffroy.donext.presentation.viewmodel.TaskListViewModel
import java.time.Instant
import java.time.LocalDate
import java.time.ZoneOffset
@Composable
fun TaskListScreen(viewModel: TaskListViewModel = hiltViewModel()) {
fun TaskListScreen(viewModel: TaskListViewModel = hiltViewModel(), onTaskClick: (Task) -> Unit) {
val tasks = viewModel.tasks
LazyColumn(
@@ -58,6 +63,7 @@ fun TaskListScreen(viewModel: TaskListViewModel = hiltViewModel()) {
TaskItem(
task = task,
onClick = { onTaskClick(task) },
onToggleDone = { isChecked ->
viewModel.updateTaskDone(task.id!!, isChecked)
}
@@ -69,15 +75,26 @@ fun TaskListScreen(viewModel: TaskListViewModel = hiltViewModel()) {
@Composable
fun TaskItem(
task: Task,
onClick: () -> 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(
fontWeight = when (task.priority) {
Priority.HIGH -> FontWeight.Bold
Priority.NORMAL -> 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.NORMAL -> MaterialTheme.colorScheme.onSurface
Priority.LOW -> MaterialTheme.colorScheme.onSurfaceVariant
@@ -87,15 +104,10 @@ fun TaskItem(
Row(
modifier = Modifier
.fillMaxWidth()
.clickable { onClick() }
.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(
checked = task.isDone,
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.lifecycle.ViewModel
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.usecase.AddTaskUseCase
import com.wismna.geoffroy.donext.domain.usecase.GetTaskListsUseCase
import dagger.hilt.android.lifecycle.HiltViewModel
import kotlinx.coroutines.flow.launchIn
import kotlinx.coroutines.flow.onEach
import kotlinx.coroutines.launch
import javax.inject.Inject
@HiltViewModel
class MainViewModel @Inject constructor(
getTaskLists: GetTaskListsUseCase,
private val addTask: AddTaskUseCase
getTaskLists: GetTaskListsUseCase
) : ViewModel() {
var taskLists by mutableStateOf<List<TaskList>>(emptyList())
@@ -26,17 +22,6 @@ class MainViewModel @Inject constructor(
var isLoading by mutableStateOf(true)
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 {
getTaskLists()
.onEach { lists ->
@@ -45,27 +30,4 @@ class MainViewModel @Inject constructor(
}
.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
}
}