diff --git a/.idea/deviceManager.xml b/.idea/deviceManager.xml
new file mode 100644
index 0000000..91f9558
--- /dev/null
+++ b/.idea/deviceManager.xml
@@ -0,0 +1,13 @@
+
+
+
+
+
+
\ No newline at end of file
diff --git a/.idea/inspectionProfiles/Project_Default.xml b/.idea/inspectionProfiles/Project_Default.xml
index 83915d7..2c8262f 100644
--- a/.idea/inspectionProfiles/Project_Default.xml
+++ b/.idea/inspectionProfiles/Project_Default.xml
@@ -4,5 +4,50 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/donext/build.gradle b/donext/build.gradle
index b1488fc..c86fc15 100644
--- a/donext/build.gradle
+++ b/donext/build.gradle
@@ -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"
}
diff --git a/donextv2/build.gradle.kts b/donextv2/build.gradle.kts
index 349224d..055710a 100644
--- a/donextv2/build.gradle.kts
+++ b/donextv2/build.gradle.kts
@@ -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"
}
diff --git a/donextv2/src/main/java/com/wismna/geoffroy/donext/data/Mappers.kt b/donextv2/src/main/java/com/wismna/geoffroy/donext/data/Mappers.kt
index 041a8b8..59736c7 100644
--- a/donextv2/src/main/java/com/wismna/geoffroy/donext/data/Mappers.kt
+++ b/donextv2/src/main/java/com/wismna/geoffroy/donext/data/Mappers.kt
@@ -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
)
diff --git a/donextv2/src/main/java/com/wismna/geoffroy/donext/data/entities/TaskEntity.kt b/donextv2/src/main/java/com/wismna/geoffroy/donext/data/entities/TaskEntity.kt
index 4eacfd8..3c6c366 100644
--- a/donextv2/src/main/java/com/wismna/geoffroy/donext/data/entities/TaskEntity.kt
+++ b/donextv2/src/main/java/com/wismna/geoffroy/donext/data/entities/TaskEntity.kt
@@ -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
)
diff --git a/donextv2/src/main/java/com/wismna/geoffroy/donext/data/entities/TaskListEntity.kt b/donextv2/src/main/java/com/wismna/geoffroy/donext/data/entities/TaskListEntity.kt
index 5d4bd43..3a4e599 100644
--- a/donextv2/src/main/java/com/wismna/geoffroy/donext/data/entities/TaskListEntity.kt
+++ b/donextv2/src/main/java/com/wismna/geoffroy/donext/data/entities/TaskListEntity.kt
@@ -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
)
diff --git a/donextv2/src/main/java/com/wismna/geoffroy/donext/data/local/Database.kt b/donextv2/src/main/java/com/wismna/geoffroy/donext/data/local/Database.kt
index 7a5b189..28242ff 100644
--- a/donextv2/src/main/java/com/wismna/geoffroy/donext/data/local/Database.kt
+++ b/donextv2/src/main/java/com/wismna/geoffroy/donext/data/local/Database.kt
@@ -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))
}
}
})
diff --git a/donextv2/src/main/java/com/wismna/geoffroy/donext/data/local/dao/TaskDao.kt b/donextv2/src/main/java/com/wismna/geoffroy/donext/data/local/dao/TaskDao.kt
index d33798c..dcf9957 100644
--- a/donextv2/src/main/java/com/wismna/geoffroy/donext/data/local/dao/TaskDao.kt
+++ b/donextv2/src/main/java/com/wismna/geoffroy/donext/data/local/dao/TaskDao.kt
@@ -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
+ @Query("SELECT * FROM tasks WHERE task_list_id = :listId ORDER BY display_order ASC")
+ fun getTasksForList(listId: Long): Flow>
@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)
diff --git a/donextv2/src/main/java/com/wismna/geoffroy/donext/data/local/dao/TaskListDao.kt b/donextv2/src/main/java/com/wismna/geoffroy/donext/data/local/dao/TaskListDao.kt
index ec5d7a1..affbde2 100644
--- a/donextv2/src/main/java/com/wismna/geoffroy/donext/data/local/dao/TaskListDao.kt
+++ b/donextv2/src/main/java/com/wismna/geoffroy/donext/data/local/dao/TaskListDao.kt
@@ -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>
@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)
}
\ No newline at end of file
diff --git a/donextv2/src/main/java/com/wismna/geoffroy/donext/data/local/repository/TaskRepositoryImpl.kt b/donextv2/src/main/java/com/wismna/geoffroy/donext/data/local/repository/TaskRepositoryImpl.kt
index d0244ad..f650190 100644
--- a/donextv2/src/main/java/com/wismna/geoffroy/donext/data/local/repository/TaskRepositoryImpl.kt
+++ b/donextv2/src/main/java/com/wismna/geoffroy/donext/data/local/repository/TaskRepositoryImpl.kt
@@ -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 {
- return taskDao.getTasksForList(listId).map { it.toDomain() }
+ override fun getTasksForList(listId: Long): Flow> {
+ 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) {
diff --git a/donextv2/src/main/java/com/wismna/geoffroy/donext/domain/model/Task.kt b/donextv2/src/main/java/com/wismna/geoffroy/donext/domain/model/Task.kt
index 3bc2ba3..e9f80ab 100644
--- a/donextv2/src/main/java/com/wismna/geoffroy/donext/domain/model/Task.kt
+++ b/donextv2/src/main/java/com/wismna/geoffroy/donext/domain/model/Task.kt
@@ -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
)
diff --git a/donextv2/src/main/java/com/wismna/geoffroy/donext/domain/model/TaskList.kt b/donextv2/src/main/java/com/wismna/geoffroy/donext/domain/model/TaskList.kt
index 33c39b0..dc88650 100644
--- a/donextv2/src/main/java/com/wismna/geoffroy/donext/domain/model/TaskList.kt
+++ b/donextv2/src/main/java/com/wismna/geoffroy/donext/domain/model/TaskList.kt
@@ -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
)
diff --git a/donextv2/src/main/java/com/wismna/geoffroy/donext/domain/repository/TaskRepository.kt b/donextv2/src/main/java/com/wismna/geoffroy/donext/domain/repository/TaskRepository.kt
index 1d2af95..854e07a 100644
--- a/donextv2/src/main/java/com/wismna/geoffroy/donext/domain/repository/TaskRepository.kt
+++ b/donextv2/src/main/java/com/wismna/geoffroy/donext/domain/repository/TaskRepository.kt
@@ -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
+ fun getTasksForList(listId: Long): Flow>
suspend fun insertTask(task: Task)
suspend fun updateTask(task: Task)
suspend fun deleteTask(taskId: Long, isDeleted: Boolean)
diff --git a/donextv2/src/main/java/com/wismna/geoffroy/donext/domain/usecase/AddTaskUseCase.kt b/donextv2/src/main/java/com/wismna/geoffroy/donext/domain/usecase/AddTaskUseCase.kt
new file mode 100644
index 0000000..3c6cb5b
--- /dev/null
+++ b/donextv2/src/main/java/com/wismna/geoffroy/donext/domain/usecase/AddTaskUseCase.kt
@@ -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
+ )
+ )
+ }
+}
\ No newline at end of file
diff --git a/donextv2/src/main/java/com/wismna/geoffroy/donext/domain/usecase/GetTasksForListUseCase.kt b/donextv2/src/main/java/com/wismna/geoffroy/donext/domain/usecase/GetTasksForListUseCase.kt
new file mode 100644
index 0000000..c37fa05
--- /dev/null
+++ b/donextv2/src/main/java/com/wismna/geoffroy/donext/domain/usecase/GetTasksForListUseCase.kt
@@ -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> = repository.getTasksForList(taskListId)
+}
\ No newline at end of file
diff --git a/donextv2/src/main/java/com/wismna/geoffroy/donext/presentation/MainActivity.kt b/donextv2/src/main/java/com/wismna/geoffroy/donext/presentation/MainActivity.kt
index 2ea1351..cced042 100644
--- a/donextv2/src/main/java/com/wismna/geoffroy/donext/presentation/MainActivity.kt
+++ b/donextv2/src/main/java/com/wismna/geoffroy/donext/presentation/MainActivity.kt
@@ -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
@@ -49,66 +31,4 @@ 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)
- }
- }
- }
-}
-
+}
\ No newline at end of file
diff --git a/donextv2/src/main/java/com/wismna/geoffroy/donext/presentation/screen/MainScreen.kt b/donextv2/src/main/java/com/wismna/geoffroy/donext/presentation/screen/MainScreen.kt
new file mode 100644
index 0000000..6fb1d1f
--- /dev/null
+++ b/donextv2/src/main/java/com/wismna/geoffroy/donext/presentation/screen/MainScreen.kt
@@ -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()
+ TaskListScreen(viewModel)
+ }
+ }
+ }
+}
+
+@Composable
+fun AddNewTaskButton(onClick: () -> Unit) {
+ ExtendedFloatingActionButton(
+ onClick = { onClick() },
+ icon = { Icon(Icons.Filled.Add, "Create a task.") },
+ text = { Text(text = "Create a task") },
+ )
+}
\ No newline at end of file
diff --git a/donextv2/src/main/java/com/wismna/geoffroy/donext/presentation/screen/TaskListScreen.kt b/donextv2/src/main/java/com/wismna/geoffroy/donext/presentation/screen/TaskListScreen.kt
index d16e625..c52ed97 100644
--- a/donextv2/src/main/java/com/wismna/geoffroy/donext/presentation/screen/TaskListScreen.kt
+++ b/donextv2/src/main/java/com/wismna/geoffroy/donext/presentation/screen/TaskListScreen.kt
@@ -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
- ) {
- Text("${taskList.name} Screen")
+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)
+ ) {
+ 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)
+ }
+ }
+ }
}
}
\ No newline at end of file
diff --git a/donextv2/src/main/java/com/wismna/geoffroy/donext/presentation/viewmodel/MainViewModel.kt b/donextv2/src/main/java/com/wismna/geoffroy/donext/presentation/viewmodel/MainViewModel.kt
index 0270dca..5f312f9 100644
--- a/donextv2/src/main/java/com/wismna/geoffroy/donext/presentation/viewmodel/MainViewModel.kt
+++ b/donextv2/src/main/java/com/wismna/geoffroy/donext/presentation/viewmodel/MainViewModel.kt
@@ -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>(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
+ }
}
\ No newline at end of file
diff --git a/donextv2/src/main/java/com/wismna/geoffroy/donext/presentation/viewmodel/TaskListViewModel.kt b/donextv2/src/main/java/com/wismna/geoffroy/donext/presentation/viewmodel/TaskListViewModel.kt
new file mode 100644
index 0000000..e6286b2
--- /dev/null
+++ b/donextv2/src/main/java/com/wismna/geoffroy/donext/presentation/viewmodel/TaskListViewModel.kt
@@ -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>(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)
+ }
+}
\ No newline at end of file