Add a FAB to create a task

Add bottom sheet to display the new task form
Set up the tasks list screen to display tasks
Implement a working migration from the old Donext database
This commit is contained in:
Geoffroy Bonneville
2025-09-11 20:11:20 -04:00
parent d6e05c17ba
commit d469fbc2ba
21 changed files with 498 additions and 136 deletions

13
.idea/deviceManager.xml generated Normal file
View File

@@ -0,0 +1,13 @@
<?xml version="1.0" encoding="UTF-8"?>
<project version="4">
<component name="DeviceTable">
<option name="columnSorters">
<list>
<ColumnSorterState>
<option name="column" value="Name" />
<option name="order" value="ASCENDING" />
</ColumnSorterState>
</list>
</option>
</component>
</project>

View File

@@ -4,5 +4,50 @@
<inspection_tool class="AutoCloseableResource" enabled="true" level="WARNING" enabled_by_default="true">
<option name="METHOD_MATCHER_CONFIG" value="java.util.Formatter,format,java.io.Writer,append,com.google.common.base.Preconditions,checkNotNull,org.hibernate.Session,close,java.io.PrintWriter,printf,java.io.PrintStream,printf,android.content.Context,obtainStyledAttributes" />
</inspection_tool>
<inspection_tool class="ComposePreviewDimensionRespectsLimit" enabled="true" level="WARNING" enabled_by_default="true">
<option name="composableFile" value="true" />
</inspection_tool>
<inspection_tool class="ComposePreviewMustBeTopLevelFunction" enabled="true" level="ERROR" enabled_by_default="true">
<option name="composableFile" value="true" />
</inspection_tool>
<inspection_tool class="ComposePreviewNeedsComposableAnnotation" enabled="true" level="ERROR" enabled_by_default="true">
<option name="composableFile" value="true" />
</inspection_tool>
<inspection_tool class="ComposePreviewNotSupportedInUnitTestFiles" enabled="true" level="ERROR" enabled_by_default="true">
<option name="composableFile" value="true" />
</inspection_tool>
<inspection_tool class="GlancePreviewDimensionRespectsLimit" enabled="true" level="WARNING" enabled_by_default="true">
<option name="composableFile" value="true" />
</inspection_tool>
<inspection_tool class="GlancePreviewMustBeTopLevelFunction" enabled="true" level="ERROR" enabled_by_default="true">
<option name="composableFile" value="true" />
</inspection_tool>
<inspection_tool class="GlancePreviewNeedsComposableAnnotation" enabled="true" level="ERROR" enabled_by_default="true">
<option name="composableFile" value="true" />
</inspection_tool>
<inspection_tool class="GlancePreviewNotSupportedInUnitTestFiles" enabled="true" level="ERROR" enabled_by_default="true">
<option name="composableFile" value="true" />
</inspection_tool>
<inspection_tool class="PreviewAnnotationInFunctionWithParameters" enabled="true" level="ERROR" enabled_by_default="true">
<option name="composableFile" value="true" />
</inspection_tool>
<inspection_tool class="PreviewApiLevelMustBeValid" enabled="true" level="ERROR" enabled_by_default="true">
<option name="composableFile" value="true" />
</inspection_tool>
<inspection_tool class="PreviewDeviceShouldUseNewSpec" enabled="true" level="WEAK WARNING" enabled_by_default="true">
<option name="composableFile" value="true" />
</inspection_tool>
<inspection_tool class="PreviewFontScaleMustBeGreaterThanZero" enabled="true" level="ERROR" enabled_by_default="true">
<option name="composableFile" value="true" />
</inspection_tool>
<inspection_tool class="PreviewMultipleParameterProviders" enabled="true" level="ERROR" enabled_by_default="true">
<option name="composableFile" value="true" />
</inspection_tool>
<inspection_tool class="PreviewParameterProviderOnFirstParameter" enabled="true" level="ERROR" enabled_by_default="true">
<option name="composableFile" value="true" />
</inspection_tool>
<inspection_tool class="PreviewPickerAnnotation" enabled="true" level="ERROR" enabled_by_default="true">
<option name="composableFile" value="true" />
</inspection_tool>
</profile>
</component>

View File

@@ -1,12 +1,12 @@
apply plugin: 'com.android.application'
android {
compileSdk 35
compileSdk 36
defaultConfig {
applicationId "com.wismna.geoffroy.donext"
minSdkVersion 21
targetSdkVersion 35
targetSdkVersion 36
versionCode 33
versionName "1.12"
}

View File

@@ -14,8 +14,8 @@ android {
applicationId = "com.wismna.geoffroy.donext"
minSdk = 26
targetSdk = 36
versionCode = 1
versionName = "1.0"
versionCode = 34
versionName = "2.0"
testInstrumentationRunner = "androidx.test.runner.AndroidJUnitRunner"
}

View File

@@ -9,33 +9,39 @@ import java.time.Instant
fun TaskEntity.toDomain() = Task(
id = id,
name = name,
isDone = isDone,
taskListId = taskListId,
description = description,
cycles = cycles,
cycle = cycle,
isDone = isDone,
isDeleted = isDeleted,
updateDate = Instant.ofEpochMilli(updateDate)
dueDate = if (dueDate == null) null else Instant.ofEpochMilli(dueDate),
priority = priority,
order = order
)
fun Task.toEntity() = TaskEntity(
id = id,
id = id ?: 0,
name = name,
isDone = isDone,
taskListId = taskListId,
description = description,
cycles = cycles,
cycle = cycle,
priority = priority,
order = order,
isDone = isDone,
isDeleted = isDeleted,
updateDate = updateDate.toEpochMilli()
dueDate = dueDate?.toEpochMilli()
)
fun TaskListEntity.toDomain() = TaskList(
id = id,
name = name,
isDeleted = isDeleted
isDeleted = isDeleted,
order = order
)
fun TaskList.toEntity() = TaskListEntity(
id = id,
name = name,
isDeleted = isDeleted
isDeleted = isDeleted,
order = order
)

View File

@@ -9,14 +9,17 @@ data class TaskEntity(
@PrimaryKey(autoGenerate = true)
val id: Long = 0,
val name: String,
val description: String,
val cycles: Int = 0,
val description: String?,
val cycle: Int = 0,
val priority: Int,
@ColumnInfo(name = "display_order")
val order: Int,
@ColumnInfo(name = "done")
val isDone: Boolean = false,
@ColumnInfo(name = "deleted")
val isDeleted: Boolean = false,
@ColumnInfo(name = "task_list_id")
val taskListId: Long,
@ColumnInfo(name = "update_date")
val updateDate: Long = System.currentTimeMillis()
@ColumnInfo(name = "due_date")
val dueDate: Long? = null
)

View File

@@ -1,5 +1,6 @@
package com.wismna.geoffroy.donext.data.entities
import androidx.room.ColumnInfo
import androidx.room.Entity
import androidx.room.PrimaryKey
@@ -8,5 +9,8 @@ data class TaskListEntity(
@PrimaryKey(autoGenerate = true)
val id: Long = 0,
val name: String,
@ColumnInfo(name = "display_order")
val order: Int,
@ColumnInfo(name = "deleted")
val isDeleted: Boolean = false
)

View File

@@ -31,7 +31,89 @@ abstract class AppDatabase : RoomDatabase() {
val MIGRATION_6_7 = object : Migration(6, 7) {
override fun migrate(db: SupportSQLiteDatabase) {
// TODO: migrate from old Donext database (v6)
db.beginTransaction()
try {
// --- TASKS TABLE ---
// 1. Create the new tasks table with the updated schema
db.execSQL(
"""
CREATE TABLE tasks_new (
id INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL,
name TEXT NOT NULL,
description TEXT,
cycle INTEGER NOT NULL DEFAULT 0,
priority INTEGER NOT NULL,
display_order INTEGER NOT NULL,
done INTEGER NOT NULL DEFAULT 0,
deleted INTEGER NOT NULL DEFAULT 0,
task_list_id INTEGER NOT NULL,
due_date INTEGER
)
""".trimIndent()
)
// 2. Copy old data into the new table
// Map old column names to new ones
db.execSQL(
"""
INSERT INTO tasks_new (
id, name, description, cycle, priority, display_order,
done, deleted, task_list_id, due_date
)
SELECT
_id, -- old '_id' mapped to id
name,
description,
cycle,
priority,
displayorder, -- old 'displayorder' mapped to display_order
done,
deleted,
list, -- old 'list' mapped to task_list_id
duedate -- old column renamed to due_date
FROM tasks
""".trimIndent()
)
// 3. Drop the old table
db.execSQL("DROP TABLE tasks")
// 4. Rename the new table
db.execSQL("ALTER TABLE tasks_new RENAME TO tasks")
// --- TASK_LISTS TABLE ---
db.execSQL(
"""
CREATE TABLE task_lists_new (
id INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL,
name TEXT NOT NULL,
deleted INTEGER NOT NULL DEFAULT 0,
display_order INTEGER NOT NULL
)
""".trimIndent()
)
db.execSQL(
"""
INSERT INTO task_lists_new (
id, name, display_order, deleted
)
SELECT
_id, -- old '_id' mapped to id
name,
displayorder, -- old 'displayorder' mapped to display_order
1 - visible -- old 'visible' mapped to deleted
FROM tasklist
""".trimIndent()
)
db.execSQL("DROP TABLE tasklist")
db.execSQL("ALTER TABLE task_lists_new RENAME TO task_lists")
db.setTransactionSuccessful()
} finally {
db.endTransaction()
}
}
}
@@ -50,9 +132,9 @@ abstract class AppDatabase : RoomDatabase() {
// insert default lists
CoroutineScope(Dispatchers.IO).launch {
val dao = DB_INSTANCE?.taskListDao()
dao?.insertTaskList(TaskListEntity(name = "Work"))
dao?.insertTaskList(TaskListEntity(name = "Personal"))
dao?.insertTaskList(TaskListEntity(name = "Shopping"))
dao?.insertTaskList(TaskListEntity(name = "Work", order = 2))
dao?.insertTaskList(TaskListEntity(name = "Personal", order = 1))
dao?.insertTaskList(TaskListEntity(name = "Shopping", order = 3))
}
}
})

View File

@@ -6,11 +6,12 @@ import androidx.room.OnConflictStrategy
import androidx.room.Query
import androidx.room.Update
import com.wismna.geoffroy.donext.data.entities.TaskEntity
import kotlinx.coroutines.flow.Flow
@Dao
interface TaskDao {
@Query("SELECT * FROM tasks WHERE task_list_id = :listId")
suspend fun getTasksForList(listId: Long): List<TaskEntity>
@Query("SELECT * FROM tasks WHERE task_list_id = :listId ORDER BY display_order ASC")
fun getTasksForList(listId: Long): Flow<List<TaskEntity>>
@Insert(onConflict = OnConflictStrategy.REPLACE)
suspend fun insertTask(task: TaskEntity)
@@ -18,14 +19,14 @@ interface TaskDao {
@Update
suspend fun updateTask(task: TaskEntity)
@Query("UPDATE tasks SET done = :done, update_date = :updateDate WHERE id = :taskId")
suspend fun markTaskDone(taskId: Long, done: Boolean, updateDate: Long = System.currentTimeMillis())
@Query("UPDATE tasks SET done = :done WHERE id = :taskId")
suspend fun markTaskDone(taskId: Long, done: Boolean)
@Query("UPDATE tasks SET deleted = :deleted, update_date = :updateDate WHERE id = :taskId")
suspend fun markTaskDeleted(taskId: Long, deleted: Boolean, updateDate: Long = System.currentTimeMillis())
@Query("UPDATE tasks SET deleted = :deleted WHERE id = :taskId")
suspend fun markTaskDeleted(taskId: Long, deleted: Boolean)
@Query("UPDATE tasks SET cycles = cycles + 1, update_date = :updateDate WHERE id = :taskId")
suspend fun increaseCycle(taskId: Long, updateDate: Long = System.currentTimeMillis())
@Query("UPDATE tasks SET cycle = cycle + 1 WHERE id = :taskId")
suspend fun increaseCycle(taskId: Long)
@Query("UPDATE tasks SET deleted = :deleted WHERE id = :taskListId")
suspend fun deleteAllTasksFromList(taskListId: Long, deleted: Boolean)

View File

@@ -10,7 +10,7 @@ import kotlinx.coroutines.flow.Flow
@Dao
interface TaskListDao {
@Query("SELECT * FROM task_lists WHERE isDeleted = 0")
@Query("SELECT * FROM task_lists WHERE deleted = 0 ORDER BY display_order ASC")
fun getTaskLists(): Flow<List<TaskListEntity>>
@Insert(onConflict = OnConflictStrategy.REPLACE)
@@ -19,6 +19,6 @@ interface TaskListDao {
@Update
suspend fun updateTaskList(taskList: TaskListEntity)
@Query("UPDATE task_lists SET isDeleted = :isDeleted WHERE id = :listId")
@Query("UPDATE task_lists SET deleted = :isDeleted WHERE id = :listId")
suspend fun deleteTaskList(listId: Long, isDeleted: Boolean)
}

View File

@@ -9,15 +9,14 @@ import com.wismna.geoffroy.donext.domain.model.TaskList
import com.wismna.geoffroy.donext.domain.repository.TaskRepository
import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.map
import java.time.Instant
import javax.inject.Inject
class TaskRepositoryImpl @Inject constructor(
private val taskDao: TaskDao,
private val taskListDao: TaskListDao
): TaskRepository {
override suspend fun getTasksForList(listId: Long): List<Task> {
return taskDao.getTasksForList(listId).map { it.toDomain() }
override fun getTasksForList(listId: Long): Flow<List<Task>> {
return taskDao.getTasksForList(listId).map {entity -> entity.map { it.toDomain() }}
}
override suspend fun insertTask(task: Task) {
@@ -25,8 +24,7 @@ class TaskRepositoryImpl @Inject constructor(
}
override suspend fun updateTask(task: Task) {
val updated = task.copy(updateDate = Instant.now())
taskDao.updateTask(updated.toEntity())
taskDao.updateTask(task.toEntity())
}
override suspend fun deleteTask(taskId: Long, isDeleted: Boolean) {

View File

@@ -3,12 +3,14 @@ package com.wismna.geoffroy.donext.domain.model
import java.time.Instant
data class Task(
val id: Long,
val id: Long? = null,
val name: String,
val description: String,
val cycles: Int,
val description: String?,
val cycle: Int,
val priority: Int,
val order: Int,
val isDone: Boolean,
val isDeleted: Boolean,
val taskListId: Long,
val updateDate: Instant = Instant.now()
val dueDate: Instant? = null
)

View File

@@ -3,5 +3,6 @@ package com.wismna.geoffroy.donext.domain.model
data class TaskList(
val id: Long,
val name: String,
val isDeleted: Boolean
val isDeleted: Boolean,
val order: Int
)

View File

@@ -5,7 +5,7 @@ import com.wismna.geoffroy.donext.domain.model.TaskList
import kotlinx.coroutines.flow.Flow
interface TaskRepository {
suspend fun getTasksForList(listId: Long): List<Task>
fun getTasksForList(listId: Long): Flow<List<Task>>
suspend fun insertTask(task: Task)
suspend fun updateTask(task: Task)
suspend fun deleteTask(taskId: Long, isDeleted: Boolean)

View File

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

View File

@@ -0,0 +1,10 @@
package com.wismna.geoffroy.donext.domain.usecase
import com.wismna.geoffroy.donext.domain.model.Task
import com.wismna.geoffroy.donext.domain.repository.TaskRepository
import kotlinx.coroutines.flow.Flow
import javax.inject.Inject
class GetTasksForListUseCase @Inject constructor(private val repository: TaskRepository) {
operator fun invoke(taskListId: Long): Flow<List<Task>> = repository.getTasksForList(taskListId)
}

View File

@@ -4,30 +4,12 @@ import android.os.Bundle
import androidx.activity.ComponentActivity
import androidx.activity.compose.setContent
import androidx.activity.enableEdgeToEdge
import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.layout.padding
import androidx.compose.material3.CircularProgressIndicator
import androidx.compose.material3.ExperimentalMaterial3Api
import androidx.compose.material3.PrimaryTabRow
import androidx.compose.material3.Scaffold
import androidx.compose.material3.Tab
import androidx.compose.material3.Text
import androidx.compose.runtime.Composable
import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableIntStateOf
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.text.style.TextOverflow
import androidx.hilt.navigation.compose.hiltViewModel
import androidx.navigation.NavHostController
import androidx.navigation.compose.NavHost
import androidx.navigation.compose.composable
import androidx.navigation.compose.rememberNavController
import com.wismna.geoffroy.donext.domain.model.TaskList
import com.wismna.geoffroy.donext.presentation.screen.TaskListScreen
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
import dagger.hilt.android.AndroidEntryPoint
@@ -50,65 +32,3 @@ class MainActivity : ComponentActivity() {
}
}
}
@OptIn(ExperimentalMaterial3Api::class)
@Composable
fun MainScreen(viewModel: MainViewModel, modifier: Modifier = Modifier) {
val navController = rememberNavController()
if (viewModel.isLoading) {
// Show loading or empty state
Box(Modifier.fillMaxSize(), contentAlignment = Alignment.Center) {
CircularProgressIndicator()
}
} else {
val startDestination = viewModel.taskLists[0]
// TODO: get last opened tab from saved settings
var selectedDestination by rememberSaveable { mutableIntStateOf(0) }
Scaffold(modifier = modifier) { contentPadding ->
PrimaryTabRow(
selectedTabIndex = selectedDestination,
modifier = Modifier.padding(contentPadding)
) {
viewModel.taskLists.forEachIndexed { index, destination ->
Tab(
selected = selectedDestination == index,
onClick = {
navController.navigate(route = destination.name)
selectedDestination = index
},
text = {
Text(
text = destination.name,
maxLines = 2,
overflow = TextOverflow.Ellipsis
)
}
)
}
}
AppNavHost(navController, startDestination, viewModel)
}
}
}
@Composable
fun AppNavHost(
navController: NavHostController,
startDestination: TaskList,
viewModel: MainViewModel,
modifier: Modifier = Modifier
) {
NavHost(
navController,
startDestination = startDestination.name
) {
viewModel.taskLists.forEach { destination ->
composable(destination.name) {
TaskListScreen(destination, modifier)
}
}
}
}

View File

@@ -0,0 +1,165 @@
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.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.Tab
import androidx.compose.material3.Text
import androidx.compose.runtime.Composable
import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableIntStateOf
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember
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.text.style.TextOverflow
import androidx.compose.ui.unit.dp
import androidx.hilt.navigation.compose.hiltViewModel
import androidx.navigation.NavHostController
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.TaskList
import com.wismna.geoffroy.donext.presentation.viewmodel.MainViewModel
import com.wismna.geoffroy.donext.presentation.viewmodel.TaskListViewModel
@OptIn(ExperimentalMaterial3Api::class)
@Composable
fun MainScreen(
viewModel: MainViewModel,
modifier: Modifier = Modifier) {
val navController = rememberNavController()
var showBottomSheet by remember { mutableStateOf(false) }
if (viewModel.isLoading) {
// Show loading or empty state
Box(Modifier.fillMaxSize(), contentAlignment = Alignment.Center) {
CircularProgressIndicator()
}
} else {
val startDestination = viewModel.taskLists[0]
// TODO: get last opened tab from saved settings
var selectedDestination by rememberSaveable { mutableIntStateOf(0) }
if (showBottomSheet) {
ModalBottomSheet(onDismissRequest = { showBottomSheet = false }) {
Column(Modifier.padding(16.dp)) {
Text("New Task", style = MaterialTheme.typography.titleLarge)
Spacer(Modifier.height(8.dp))
OutlinedTextField(
value = viewModel.title,
singleLine = true,
onValueChange = { viewModel.onTitleChanged(it) },
label = { Text("Title") },
modifier = Modifier.fillMaxWidth(),
isError = !viewModel.isTitleValid && viewModel.title.isNotEmpty(),
)
Spacer(Modifier.height(8.dp))
OutlinedTextField(
value = viewModel.description,
onValueChange = { viewModel.onDescriptionChanged(it) },
label = { Text("Description") },
maxLines = 3,
modifier = Modifier.fillMaxWidth()
)
Spacer(Modifier.height(16.dp))
Button(
onClick = {
val currentListId = viewModel.taskLists[selectedDestination].id
viewModel.createTask(currentListId)
showBottomSheet = false
},
modifier = Modifier.align(Alignment.End)
) {
Text("Add")
}
}
}
}
Scaffold(
modifier = modifier,
floatingActionButton = {
AddNewTaskButton {
showBottomSheet = true
}
}, topBar = {
PrimaryTabRow(selectedTabIndex = selectedDestination) {
viewModel.taskLists.forEachIndexed { index, destination ->
Tab(
selected = selectedDestination == index,
onClick = {
navController.navigate(route = "taskList/${destination.id}")
selectedDestination = index
},
text = {
Text(
text = destination.name,
maxLines = 2,
overflow = TextOverflow.Ellipsis
)
}
)
}
}
}) { contentPadding ->
// NavHost will now automatically be below the tabs
Box(modifier = Modifier
.padding(contentPadding)
.fillMaxSize()
) {
AppNavHost(navController, startDestination, viewModel)
}
}
}
}
@Composable
fun AppNavHost(
navController: NavHostController,
startDestination: TaskList,
viewModel: MainViewModel
) {
NavHost(
navController,
startDestination = "taskList/${startDestination.id}"
) {
viewModel.taskLists.forEach { destination ->
composable(
route = "taskList/{taskListId}",
arguments = listOf(navArgument("taskListId") { type = NavType.LongType })) {
val viewModel: TaskListViewModel = hiltViewModel<TaskListViewModel>()
TaskListScreen(viewModel)
}
}
}
}
@Composable
fun AddNewTaskButton(onClick: () -> Unit) {
ExtendedFloatingActionButton(
onClick = { onClick() },
icon = { Icon(Icons.Filled.Add, "Create a task.") },
text = { Text(text = "Create a task") },
)
}

View File

@@ -1,19 +1,43 @@
package com.wismna.geoffroy.donext.presentation.screen
import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.lazy.LazyColumn
import androidx.compose.foundation.lazy.items
import androidx.compose.material3.Text
import androidx.compose.runtime.Composable
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import com.wismna.geoffroy.donext.domain.model.TaskList
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.text.TextStyle
import androidx.compose.ui.text.style.TextDecoration
import androidx.compose.ui.unit.dp
import androidx.compose.ui.unit.sp
import androidx.hilt.navigation.compose.hiltViewModel
import com.wismna.geoffroy.donext.presentation.viewmodel.TaskListViewModel
@Composable
fun TaskListScreen(taskList: TaskList, modifier: Modifier = Modifier) {
Box(
modifier = Modifier.fillMaxSize(),
contentAlignment = Alignment.Center
fun TaskListScreen(viewModel: TaskListViewModel = hiltViewModel()) {
val tasks = viewModel.tasks
LazyColumn(
verticalArrangement = Arrangement.spacedBy(4.dp)) {
items(tasks) { task ->
Row(
modifier = Modifier
.fillMaxWidth()
.padding(8.dp)
) {
Text("${taskList.name} Screen")
Column {
}
Column {
val textStyle = TextStyle(textDecoration = if (task.isDeleted) TextDecoration.LineThrough else TextDecoration.None)
Text(task.name, fontSize = 18.sp, color = Color.DarkGray, style = textStyle)
Text(task.description.orEmpty(), fontSize = 14.sp, color = Color.LightGray, style = textStyle, maxLines = 3)
}
}
}
}
}

View File

@@ -6,23 +6,34 @@ import androidx.compose.runtime.setValue
import androidx.lifecycle.ViewModel
import androidx.lifecycle.viewModelScope
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
getTaskLists: GetTaskListsUseCase,
private val addTask: AddTaskUseCase
) : ViewModel() {
var taskLists by mutableStateOf<List<TaskList>>(emptyList())
private set
var isLoading by mutableStateOf(true)
private set
// --- Form state ---
var title by mutableStateOf("")
private set
var description by mutableStateOf("")
private set
val isTitleValid: Boolean
get() = title.isNotBlank()
init {
getTaskLists()
.onEach { lists ->
@@ -31,4 +42,19 @@ class MainViewModel @Inject constructor(
}
.launchIn(viewModelScope)
}
fun createTask(taskListId: Long) {
if (!isTitleValid) return
viewModelScope.launch {
addTask(taskListId, title, description)
}
}
fun onTitleChanged(newTitle: String) {
title = newTitle
}
fun onDescriptionChanged(newDesc: String) {
description = newDesc
}
}

View File

@@ -0,0 +1,38 @@
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.SavedStateHandle
import androidx.lifecycle.ViewModel
import androidx.lifecycle.viewModelScope
import com.wismna.geoffroy.donext.domain.model.Task
import com.wismna.geoffroy.donext.domain.usecase.GetTasksForListUseCase
import dagger.hilt.android.lifecycle.HiltViewModel
import kotlinx.coroutines.flow.launchIn
import kotlinx.coroutines.flow.onEach
import javax.inject.Inject
@HiltViewModel
class TaskListViewModel @Inject constructor(
getTasks: GetTasksForListUseCase,
savedStateHandle: SavedStateHandle
) : ViewModel() {
var tasks by mutableStateOf<List<Task>>(emptyList())
private set
var isLoading by mutableStateOf(true)
private set
private val taskListId: Long = checkNotNull(savedStateHandle["taskListId"])
init {
getTasks(taskListId)
.onEach { list ->
tasks = list
isLoading = false
}
.launchIn(viewModelScope)
}
}