mirror of
https://github.com/wismna/DoNext.git
synced 2025-10-03 07:30:13 -04:00
Removed order from tasks
Tasks are ordered by priority and display is a bit different for each Done tasks are shown at the bottom Add done checkbox Add priority segmented button to bottom sheet Use database converters instead of manual mapping Some refactoring
This commit is contained in:
@@ -1,9 +1,9 @@
|
|||||||
package com.wismna.geoffroy.donext.data
|
package com.wismna.geoffroy.donext.data
|
||||||
|
|
||||||
import androidx.room.TypeConverter
|
import androidx.room.TypeConverter
|
||||||
|
import com.wismna.geoffroy.donext.domain.model.Priority
|
||||||
import java.time.Instant
|
import java.time.Instant
|
||||||
|
|
||||||
|
|
||||||
class Converters {
|
class Converters {
|
||||||
@TypeConverter
|
@TypeConverter
|
||||||
fun fromTimestamp(value: Long?): Instant? {
|
fun fromTimestamp(value: Long?): Instant? {
|
||||||
@@ -14,4 +14,10 @@ class Converters {
|
|||||||
fun instantToTimestamp(instant: Instant?): Long? {
|
fun instantToTimestamp(instant: Instant?): Long? {
|
||||||
return instant?.toEpochMilli()
|
return instant?.toEpochMilli()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@TypeConverter
|
||||||
|
fun fromPriority(priority: Priority): Int = priority.value
|
||||||
|
|
||||||
|
@TypeConverter
|
||||||
|
fun toPriority(value: Int): Priority = Priority.fromValue(value)
|
||||||
}
|
}
|
@@ -4,19 +4,16 @@ import com.wismna.geoffroy.donext.data.entities.TaskEntity
|
|||||||
import com.wismna.geoffroy.donext.data.entities.TaskListEntity
|
import com.wismna.geoffroy.donext.data.entities.TaskListEntity
|
||||||
import com.wismna.geoffroy.donext.domain.model.Task
|
import com.wismna.geoffroy.donext.domain.model.Task
|
||||||
import com.wismna.geoffroy.donext.domain.model.TaskList
|
import com.wismna.geoffroy.donext.domain.model.TaskList
|
||||||
import java.time.Instant
|
|
||||||
|
|
||||||
fun TaskEntity.toDomain() = Task(
|
fun TaskEntity.toDomain() = Task(
|
||||||
id = id,
|
id = id,
|
||||||
name = name,
|
name = name,
|
||||||
taskListId = taskListId,
|
taskListId = taskListId,
|
||||||
description = description,
|
description = description,
|
||||||
cycle = cycle,
|
|
||||||
isDone = isDone,
|
isDone = isDone,
|
||||||
isDeleted = isDeleted,
|
isDeleted = isDeleted,
|
||||||
dueDate = if (dueDate == null) null else Instant.ofEpochMilli(dueDate),
|
dueDate = dueDate,
|
||||||
priority = priority,
|
priority = priority,
|
||||||
order = order
|
|
||||||
)
|
)
|
||||||
|
|
||||||
fun Task.toEntity() = TaskEntity(
|
fun Task.toEntity() = TaskEntity(
|
||||||
@@ -24,12 +21,10 @@ fun Task.toEntity() = TaskEntity(
|
|||||||
name = name,
|
name = name,
|
||||||
taskListId = taskListId,
|
taskListId = taskListId,
|
||||||
description = description,
|
description = description,
|
||||||
cycle = cycle,
|
|
||||||
priority = priority,
|
priority = priority,
|
||||||
order = order,
|
|
||||||
isDone = isDone,
|
isDone = isDone,
|
||||||
isDeleted = isDeleted,
|
isDeleted = isDeleted,
|
||||||
dueDate = dueDate?.toEpochMilli()
|
dueDate = dueDate
|
||||||
)
|
)
|
||||||
|
|
||||||
fun TaskListEntity.toDomain() = TaskList(
|
fun TaskListEntity.toDomain() = TaskList(
|
||||||
|
@@ -3,6 +3,8 @@ package com.wismna.geoffroy.donext.data.entities
|
|||||||
import androidx.room.ColumnInfo
|
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 java.time.Instant
|
||||||
|
|
||||||
@Entity(tableName = "tasks")
|
@Entity(tableName = "tasks")
|
||||||
data class TaskEntity(
|
data class TaskEntity(
|
||||||
@@ -10,10 +12,7 @@ data class TaskEntity(
|
|||||||
val id: Long = 0,
|
val id: Long = 0,
|
||||||
val name: String,
|
val name: String,
|
||||||
val description: String?,
|
val description: String?,
|
||||||
val cycle: Int = 0,
|
val priority: Priority,
|
||||||
val priority: Int,
|
|
||||||
@ColumnInfo(name = "display_order")
|
|
||||||
val order: Int,
|
|
||||||
@ColumnInfo(name = "done")
|
@ColumnInfo(name = "done")
|
||||||
val isDone: Boolean = false,
|
val isDone: Boolean = false,
|
||||||
@ColumnInfo(name = "deleted")
|
@ColumnInfo(name = "deleted")
|
||||||
@@ -21,5 +20,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: Long? = null
|
val dueDate: Instant? = null
|
||||||
)
|
)
|
||||||
|
@@ -41,9 +41,7 @@ abstract class AppDatabase : RoomDatabase() {
|
|||||||
id INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL,
|
id INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL,
|
||||||
name TEXT NOT NULL,
|
name TEXT NOT NULL,
|
||||||
description TEXT,
|
description TEXT,
|
||||||
cycle INTEGER NOT NULL DEFAULT 0,
|
|
||||||
priority INTEGER NOT NULL,
|
priority INTEGER NOT NULL,
|
||||||
display_order INTEGER NOT NULL,
|
|
||||||
done INTEGER NOT NULL DEFAULT 0,
|
done INTEGER NOT NULL DEFAULT 0,
|
||||||
deleted INTEGER NOT NULL DEFAULT 0,
|
deleted INTEGER NOT NULL DEFAULT 0,
|
||||||
task_list_id INTEGER NOT NULL,
|
task_list_id INTEGER NOT NULL,
|
||||||
@@ -57,16 +55,14 @@ abstract class AppDatabase : RoomDatabase() {
|
|||||||
db.execSQL(
|
db.execSQL(
|
||||||
"""
|
"""
|
||||||
INSERT INTO tasks_new (
|
INSERT INTO tasks_new (
|
||||||
id, name, description, cycle, priority, display_order,
|
id, name, description, priority,
|
||||||
done, deleted, task_list_id, due_date
|
done, deleted, task_list_id, due_date
|
||||||
)
|
)
|
||||||
SELECT
|
SELECT
|
||||||
_id, -- old '_id' mapped to id
|
_id, -- old '_id' mapped to id
|
||||||
name,
|
name,
|
||||||
description,
|
description,
|
||||||
cycle,
|
|
||||||
priority,
|
priority,
|
||||||
displayorder, -- old 'displayorder' mapped to display_order
|
|
||||||
done,
|
done,
|
||||||
deleted,
|
deleted,
|
||||||
list, -- old 'list' mapped to task_list_id
|
list, -- old 'list' mapped to task_list_id
|
||||||
|
@@ -10,7 +10,7 @@ import kotlinx.coroutines.flow.Flow
|
|||||||
|
|
||||||
@Dao
|
@Dao
|
||||||
interface TaskDao {
|
interface TaskDao {
|
||||||
@Query("SELECT * FROM tasks WHERE task_list_id = :listId ORDER BY display_order ASC")
|
@Query("SELECT * FROM tasks WHERE task_list_id = :listId ORDER BY done ASC, priority DESC")
|
||||||
fun getTasksForList(listId: Long): Flow<List<TaskEntity>>
|
fun getTasksForList(listId: Long): Flow<List<TaskEntity>>
|
||||||
|
|
||||||
@Insert(onConflict = OnConflictStrategy.REPLACE)
|
@Insert(onConflict = OnConflictStrategy.REPLACE)
|
||||||
@@ -25,9 +25,6 @@ interface TaskDao {
|
|||||||
@Query("UPDATE tasks SET deleted = :deleted WHERE id = :taskId")
|
@Query("UPDATE tasks SET deleted = :deleted WHERE id = :taskId")
|
||||||
suspend fun markTaskDeleted(taskId: Long, deleted: Boolean)
|
suspend fun markTaskDeleted(taskId: Long, deleted: Boolean)
|
||||||
|
|
||||||
@Query("UPDATE tasks SET cycle = cycle + 1 WHERE id = :taskId")
|
|
||||||
suspend fun increaseCycle(taskId: Long)
|
|
||||||
|
|
||||||
@Query("UPDATE tasks SET deleted = :deleted WHERE id = :taskListId")
|
@Query("UPDATE tasks SET deleted = :deleted WHERE id = :taskListId")
|
||||||
suspend fun deleteAllTasksFromList(taskListId: Long, deleted: Boolean)
|
suspend fun deleteAllTasksFromList(taskListId: Long, deleted: Boolean)
|
||||||
}
|
}
|
@@ -31,14 +31,10 @@ class TaskRepositoryImpl @Inject constructor(
|
|||||||
taskDao.markTaskDeleted(taskId, isDeleted)
|
taskDao.markTaskDeleted(taskId, isDeleted)
|
||||||
}
|
}
|
||||||
|
|
||||||
override suspend fun closeTask(taskId: Long, isDone: Boolean) {
|
override suspend fun toggleTaskDone(taskId: Long, isDone: Boolean) {
|
||||||
taskDao.markTaskDone(taskId, isDone)
|
taskDao.markTaskDone(taskId, isDone)
|
||||||
}
|
}
|
||||||
|
|
||||||
override suspend fun increaseTaskCycle(taskId: Long) {
|
|
||||||
taskDao.increaseCycle(taskId)
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun getTaskLists(): Flow<List<TaskList>> {
|
override fun getTaskLists(): Flow<List<TaskList>> {
|
||||||
return taskListDao.getTaskLists().map {entities -> entities.map { it.toDomain() }}
|
return taskListDao.getTaskLists().map {entities -> entities.map { it.toDomain() }}
|
||||||
}
|
}
|
||||||
|
@@ -0,0 +1,12 @@
|
|||||||
|
package com.wismna.geoffroy.donext.domain.model
|
||||||
|
|
||||||
|
enum class Priority(val value: Int, val label: String) {
|
||||||
|
LOW(0, "Low"),
|
||||||
|
NORMAL(1, "Normal"),
|
||||||
|
HIGH(2, "High");
|
||||||
|
|
||||||
|
companion object {
|
||||||
|
fun fromValue(value: Int): Priority =
|
||||||
|
Priority.entries.firstOrNull { it.value == value } ?: NORMAL
|
||||||
|
}
|
||||||
|
}
|
@@ -6,9 +6,7 @@ data class Task(
|
|||||||
val id: Long? = null,
|
val id: Long? = null,
|
||||||
val name: String,
|
val name: String,
|
||||||
val description: String?,
|
val description: String?,
|
||||||
val cycle: Int,
|
val priority: Priority,
|
||||||
val priority: Int,
|
|
||||||
val order: Int,
|
|
||||||
val isDone: Boolean,
|
val isDone: Boolean,
|
||||||
val isDeleted: Boolean,
|
val isDeleted: Boolean,
|
||||||
val taskListId: Long,
|
val taskListId: Long,
|
||||||
|
@@ -9,8 +9,7 @@ interface TaskRepository {
|
|||||||
suspend fun insertTask(task: Task)
|
suspend fun insertTask(task: Task)
|
||||||
suspend fun updateTask(task: Task)
|
suspend fun updateTask(task: Task)
|
||||||
suspend fun deleteTask(taskId: Long, isDeleted: Boolean)
|
suspend fun deleteTask(taskId: Long, isDeleted: Boolean)
|
||||||
suspend fun closeTask(taskId: Long, isDone: Boolean)
|
suspend fun toggleTaskDone(taskId: Long, isDone: Boolean)
|
||||||
suspend fun increaseTaskCycle(taskId: Long)
|
|
||||||
|
|
||||||
fun getTaskLists(): Flow<List<TaskList>>
|
fun getTaskLists(): Flow<List<TaskList>>
|
||||||
suspend fun insertTaskList(taskList: TaskList)
|
suspend fun insertTaskList(taskList: TaskList)
|
||||||
|
@@ -1,5 +1,6 @@
|
|||||||
package com.wismna.geoffroy.donext.domain.usecase
|
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.model.Task
|
||||||
import com.wismna.geoffroy.donext.domain.repository.TaskRepository
|
import com.wismna.geoffroy.donext.domain.repository.TaskRepository
|
||||||
import javax.inject.Inject
|
import javax.inject.Inject
|
||||||
@@ -7,17 +8,15 @@ 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?) {
|
suspend operator fun invoke(taskListId: Long, title: String, description: String?, priority: Priority) {
|
||||||
repository.insertTask(
|
repository.insertTask(
|
||||||
Task(
|
Task(
|
||||||
taskListId = taskListId,
|
taskListId = taskListId,
|
||||||
name = title,
|
name = title,
|
||||||
description = description ?: "",
|
description = description ?: "",
|
||||||
isDeleted = false,
|
isDeleted = false,
|
||||||
cycle = 0,
|
|
||||||
isDone = false,
|
isDone = false,
|
||||||
priority = 0,
|
priority = priority,
|
||||||
order = 0
|
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
@@ -0,0 +1,12 @@
|
|||||||
|
package com.wismna.geoffroy.donext.domain.usecase
|
||||||
|
|
||||||
|
import com.wismna.geoffroy.donext.domain.repository.TaskRepository
|
||||||
|
import javax.inject.Inject
|
||||||
|
|
||||||
|
class ToggleTaskDoneUseCase @Inject constructor(
|
||||||
|
private val repository: TaskRepository
|
||||||
|
) {
|
||||||
|
suspend operator fun invoke(taskId: Long, isDone: Boolean) {
|
||||||
|
repository.toggleTaskDone(taskId, isDone)
|
||||||
|
}
|
||||||
|
}
|
@@ -1,3 +1,5 @@
|
|||||||
|
@file:OptIn(ExperimentalMaterial3Api::class)
|
||||||
|
|
||||||
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
|
||||||
@@ -19,9 +21,13 @@ import androidx.compose.material3.ModalBottomSheet
|
|||||||
import androidx.compose.material3.OutlinedTextField
|
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.SegmentedButtonDefaults
|
||||||
|
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
|
||||||
@@ -30,6 +36,8 @@ 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.compose.ui.unit.dp
|
||||||
import androidx.hilt.navigation.compose.hiltViewModel
|
import androidx.hilt.navigation.compose.hiltViewModel
|
||||||
@@ -39,11 +47,11 @@ 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.TaskList
|
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.TaskListViewModel
|
||||||
|
|
||||||
@OptIn(ExperimentalMaterial3Api::class)
|
|
||||||
@Composable
|
@Composable
|
||||||
fun MainScreen(
|
fun MainScreen(
|
||||||
viewModel: MainViewModel,
|
viewModel: MainViewModel,
|
||||||
@@ -62,39 +70,7 @@ fun MainScreen(
|
|||||||
var selectedDestination by rememberSaveable { mutableIntStateOf(0) }
|
var selectedDestination by rememberSaveable { mutableIntStateOf(0) }
|
||||||
|
|
||||||
if (showBottomSheet) {
|
if (showBottomSheet) {
|
||||||
ModalBottomSheet(onDismissRequest = { showBottomSheet = false }) {
|
AddTaskBottomSheet(viewModel, selectedDestination, { 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(
|
Scaffold(
|
||||||
@@ -123,7 +99,6 @@ fun MainScreen(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}) { contentPadding ->
|
}) { contentPadding ->
|
||||||
// NavHost will now automatically be below the tabs
|
|
||||||
Box(modifier = Modifier
|
Box(modifier = Modifier
|
||||||
.padding(contentPadding)
|
.padding(contentPadding)
|
||||||
.fillMaxSize()
|
.fillMaxSize()
|
||||||
@@ -155,11 +130,100 @@ fun AppNavHost(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@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")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
@Composable
|
@Composable
|
||||||
fun AddNewTaskButton(onClick: () -> Unit) {
|
fun AddNewTaskButton(onClick: () -> Unit) {
|
||||||
ExtendedFloatingActionButton(
|
ExtendedFloatingActionButton(
|
||||||
onClick = { onClick() },
|
onClick = onClick,
|
||||||
icon = { Icon(Icons.Filled.Add, "Create a task.") },
|
icon = { Icon(Icons.Filled.Add, "Create a task.") },
|
||||||
text = { Text(text = "Create a task") },
|
text = { Text(text = "Create a task") },
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Composable
|
||||||
|
fun SingleChoiceSegmentedButton(
|
||||||
|
value: Priority,
|
||||||
|
onValueChange: (Priority) -> Unit) {
|
||||||
|
val options = listOf(Priority.LOW.label, Priority.NORMAL.label, Priority.HIGH.label)
|
||||||
|
|
||||||
|
SingleChoiceSegmentedButtonRow {
|
||||||
|
options.forEachIndexed { index, label ->
|
||||||
|
SegmentedButton(
|
||||||
|
shape = SegmentedButtonDefaults.itemShape(
|
||||||
|
index = index,
|
||||||
|
count = options.size
|
||||||
|
),
|
||||||
|
onClick = { onValueChange(Priority.fromValue(index)) },
|
||||||
|
selected = index == value.value,
|
||||||
|
label = { Text(label) }
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
@@ -1,21 +1,31 @@
|
|||||||
package com.wismna.geoffroy.donext.presentation.screen
|
package com.wismna.geoffroy.donext.presentation.screen
|
||||||
|
|
||||||
import androidx.compose.foundation.layout.Arrangement
|
|
||||||
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.fillMaxSize
|
||||||
import androidx.compose.foundation.layout.fillMaxWidth
|
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.foundation.layout.size
|
||||||
import androidx.compose.foundation.lazy.LazyColumn
|
import androidx.compose.foundation.lazy.LazyColumn
|
||||||
import androidx.compose.foundation.lazy.items
|
import androidx.compose.foundation.lazy.itemsIndexed
|
||||||
|
import androidx.compose.foundation.shape.CircleShape
|
||||||
|
import androidx.compose.material3.Checkbox
|
||||||
|
import androidx.compose.material3.HorizontalDivider
|
||||||
|
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.ui.Modifier
|
import androidx.compose.ui.Modifier
|
||||||
import androidx.compose.ui.graphics.Color
|
import androidx.compose.ui.draw.alpha
|
||||||
import androidx.compose.ui.text.TextStyle
|
import androidx.compose.ui.draw.clip
|
||||||
|
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.unit.dp
|
import androidx.compose.ui.unit.dp
|
||||||
import androidx.compose.ui.unit.sp
|
|
||||||
import androidx.hilt.navigation.compose.hiltViewModel
|
import androidx.hilt.navigation.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 com.wismna.geoffroy.donext.presentation.viewmodel.TaskListViewModel
|
||||||
|
|
||||||
@Composable
|
@Composable
|
||||||
@@ -23,21 +33,96 @@ fun TaskListScreen(viewModel: TaskListViewModel = hiltViewModel()) {
|
|||||||
val tasks = viewModel.tasks
|
val tasks = viewModel.tasks
|
||||||
|
|
||||||
LazyColumn(
|
LazyColumn(
|
||||||
verticalArrangement = Arrangement.spacedBy(4.dp)) {
|
modifier = Modifier.fillMaxSize()
|
||||||
items(tasks) { task ->
|
) {
|
||||||
Row(
|
itemsIndexed(tasks, key = { _, task -> task.id!! }) { index, task ->
|
||||||
modifier = Modifier
|
if (index > 0) {
|
||||||
.fillMaxWidth()
|
val prev = tasks[index - 1]
|
||||||
.padding(8.dp)
|
|
||||||
) {
|
when {
|
||||||
Column {
|
// Divider between non-done and done tasks
|
||||||
}
|
!prev.isDone && task.isDone -> {
|
||||||
Column {
|
HorizontalDivider(
|
||||||
val textStyle = TextStyle(textDecoration = if (task.isDeleted) TextDecoration.LineThrough else TextDecoration.None)
|
thickness = 1.dp,
|
||||||
Text(task.name, fontSize = 18.sp, color = Color.DarkGray, style = textStyle)
|
color = MaterialTheme.colorScheme.onSurface.copy(alpha = 0.2f),
|
||||||
Text(task.description.orEmpty(), fontSize = 14.sp, color = Color.LightGray, style = textStyle, maxLines = 3)
|
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))
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
TaskItem(
|
||||||
|
task = task,
|
||||||
|
onToggleDone = { isChecked ->
|
||||||
|
viewModel.updateTaskDone(task.id!!, isChecked)
|
||||||
|
}
|
||||||
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Composable
|
||||||
|
fun TaskItem(
|
||||||
|
task: Task,
|
||||||
|
onToggleDone: (Boolean) -> Unit
|
||||||
|
) {
|
||||||
|
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) {
|
||||||
|
Priority.HIGH -> MaterialTheme.colorScheme.onSurface
|
||||||
|
Priority.NORMAL -> MaterialTheme.colorScheme.onSurface
|
||||||
|
Priority.LOW -> MaterialTheme.colorScheme.onSurfaceVariant
|
||||||
|
},
|
||||||
|
textDecoration = if (task.isDone) TextDecoration.LineThrough else TextDecoration.None)
|
||||||
|
|
||||||
|
Row(
|
||||||
|
modifier = Modifier
|
||||||
|
.fillMaxWidth()
|
||||||
|
.padding(8.dp)
|
||||||
|
.alpha(if (task.isDone) 0.5f else 1f),
|
||||||
|
) {
|
||||||
|
/*Row(
|
||||||
|
modifier = Modifier
|
||||||
|
.fillMaxWidth()
|
||||||
|
.padding(8.dp),
|
||||||
|
verticalAlignment = Alignment.CenterVertically
|
||||||
|
) {*/
|
||||||
|
Checkbox(
|
||||||
|
checked = task.isDone,
|
||||||
|
onCheckedChange = onToggleDone,
|
||||||
|
modifier = Modifier
|
||||||
|
.size(40.dp) // Adjust size as needed
|
||||||
|
.clip(CircleShape)
|
||||||
|
)
|
||||||
|
|
||||||
|
Column(
|
||||||
|
modifier = Modifier.weight(1f)
|
||||||
|
) {
|
||||||
|
Text(
|
||||||
|
text = task.name,
|
||||||
|
style = baseStyle
|
||||||
|
)
|
||||||
|
|
||||||
|
if (!task.description.isNullOrBlank()) {
|
||||||
|
Text(
|
||||||
|
text = task.description,
|
||||||
|
style = baseStyle.copy(
|
||||||
|
fontSize = MaterialTheme.typography.bodyMedium.fontSize
|
||||||
|
),
|
||||||
|
maxLines = 3,
|
||||||
|
overflow = TextOverflow.Ellipsis
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// }
|
||||||
|
}
|
||||||
|
}
|
@@ -5,6 +5,7 @@ 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.AddTaskUseCase
|
||||||
import com.wismna.geoffroy.donext.domain.usecase.GetTaskListsUseCase
|
import com.wismna.geoffroy.donext.domain.usecase.GetTaskListsUseCase
|
||||||
@@ -30,6 +31,8 @@ class MainViewModel @Inject constructor(
|
|||||||
private set
|
private set
|
||||||
var description by mutableStateOf("")
|
var description by mutableStateOf("")
|
||||||
private set
|
private set
|
||||||
|
var priority by mutableStateOf(Priority.NORMAL)
|
||||||
|
private set
|
||||||
|
|
||||||
val isTitleValid: Boolean
|
val isTitleValid: Boolean
|
||||||
get() = title.isNotBlank()
|
get() = title.isNotBlank()
|
||||||
@@ -46,15 +49,23 @@ class MainViewModel @Inject constructor(
|
|||||||
fun createTask(taskListId: Long) {
|
fun createTask(taskListId: Long) {
|
||||||
if (!isTitleValid) return
|
if (!isTitleValid) return
|
||||||
viewModelScope.launch {
|
viewModelScope.launch {
|
||||||
addTask(taskListId, title, description)
|
addTask(taskListId, title, description, priority)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fun onTitleChanged(newTitle: String) {
|
fun onTitleChanged(newTitle: String) {
|
||||||
title = newTitle
|
title = newTitle
|
||||||
}
|
}
|
||||||
|
|
||||||
fun onDescriptionChanged(newDesc: String) {
|
fun onDescriptionChanged(newDesc: String) {
|
||||||
description = newDesc
|
description = newDesc
|
||||||
}
|
}
|
||||||
|
fun onPriorityChanged(newPriority: Priority) {
|
||||||
|
priority = newPriority
|
||||||
|
}
|
||||||
|
|
||||||
|
fun resetTaskForm() {
|
||||||
|
title = ""
|
||||||
|
description = ""
|
||||||
|
priority = Priority.NORMAL
|
||||||
|
}
|
||||||
}
|
}
|
@@ -7,15 +7,18 @@ import androidx.lifecycle.SavedStateHandle
|
|||||||
import androidx.lifecycle.ViewModel
|
import androidx.lifecycle.ViewModel
|
||||||
import androidx.lifecycle.viewModelScope
|
import androidx.lifecycle.viewModelScope
|
||||||
import com.wismna.geoffroy.donext.domain.model.Task
|
import com.wismna.geoffroy.donext.domain.model.Task
|
||||||
|
import com.wismna.geoffroy.donext.domain.usecase.ToggleTaskDoneUseCase
|
||||||
import com.wismna.geoffroy.donext.domain.usecase.GetTasksForListUseCase
|
import com.wismna.geoffroy.donext.domain.usecase.GetTasksForListUseCase
|
||||||
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 TaskListViewModel @Inject constructor(
|
class TaskListViewModel @Inject constructor(
|
||||||
getTasks: GetTasksForListUseCase,
|
getTasks: GetTasksForListUseCase,
|
||||||
|
private val toggleTaskDone: ToggleTaskDoneUseCase,
|
||||||
savedStateHandle: SavedStateHandle
|
savedStateHandle: SavedStateHandle
|
||||||
) : ViewModel() {
|
) : ViewModel() {
|
||||||
|
|
||||||
@@ -35,4 +38,10 @@ class TaskListViewModel @Inject constructor(
|
|||||||
}
|
}
|
||||||
.launchIn(viewModelScope)
|
.launchIn(viewModelScope)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fun updateTaskDone(taskId: Long, isDone: Boolean) {
|
||||||
|
viewModelScope.launch {
|
||||||
|
toggleTaskDone(taskId, isDone)
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
Reference in New Issue
Block a user