diff --git a/.gitignore b/.gitignore index 55b5c2f..4eaf175 100644 --- a/.gitignore +++ b/.gitignore @@ -35,5 +35,3 @@ app/google-services.json .DS_Store app/.DS_Store -.DS_Store -app/.DS_Store diff --git a/app/build.gradle.kts b/app/build.gradle.kts index dd9314a..82f4c07 100644 --- a/app/build.gradle.kts +++ b/app/build.gradle.kts @@ -115,6 +115,8 @@ android { compileOptions { sourceCompatibility = JavaVersion.VERSION_11 targetCompatibility = JavaVersion.VERSION_11 + + isCoreLibraryDesugaringEnabled = true } buildFeatures { compose = true @@ -191,6 +193,8 @@ dependencies { // Kotlin reflection library implementation(libs.kotlin.reflect) + + coreLibraryDesugaring(libs.desugar.jdk.libs) } project(":").tasks.named("buildAppDebug") { diff --git a/app/src/main/java/com/flowintent/workspace/ui/TaskListScreen.kt b/app/src/main/java/com/flowintent/workspace/ui/TaskListScreen.kt index 4eaea90..d98a7f6 100644 --- a/app/src/main/java/com/flowintent/workspace/ui/TaskListScreen.kt +++ b/app/src/main/java/com/flowintent/workspace/ui/TaskListScreen.kt @@ -17,7 +17,6 @@ import androidx.compose.material.icons.filled.Category import androidx.compose.material.icons.filled.DashboardCustomize import androidx.compose.material.icons.filled.Doorbell import androidx.compose.material3.Card -import androidx.compose.material3.Checkbox import androidx.compose.material3.Icon import androidx.compose.material3.IconButton import androidx.compose.material3.Text @@ -118,7 +117,6 @@ private fun ListCardContent(viewModel: TaskViewModel = hiltViewModel()) { val list = remember(taskList) { taskList.toMutableStateList() } var draggingItem by remember { mutableStateOf(null) } var itemHeight by remember { mutableStateOf(50.dp) } - val isSelectionMode = viewModel.isSelectionMode.collectAsState() LazyColumn( modifier = Modifier.fillMaxSize(), @@ -126,7 +124,6 @@ private fun ListCardContent(viewModel: TaskViewModel = hiltViewModel()) { ) { itemsIndexed(list, key = { index: Int, task: Task -> task.uid }) { index, task -> val isDragging = draggingItem?.index == index - val isSelected = viewModel.selectedTasks[task.uid] ?: false Box( modifier = Modifier .graphicsLayer { @@ -180,23 +177,17 @@ private fun ListCardContent(viewModel: TaskViewModel = hiltViewModel()) { modifier = Modifier.padding(start = 12.dp, top = 12.dp, end = 12.dp), verticalAlignment = Alignment.CenterVertically ) { - if (isSelectionMode.value) { - Checkbox( - checked = isSelected, - onCheckedChange = { viewModel.toggleSelection(task.uid) }, - modifier = Modifier.padding(start = 8.dp) - ) - } SwipeableCard( task = task, onDelete = { viewModel.deleteTask(task) }, onEdit = { viewModel.setUpdateTaskId(task.uid) }, onHeightChange = { itemHeight = it }, ) { + val isExpanded = viewModel.expandedMap[task.uid] ?: false Column( modifier = Modifier .fillMaxWidth() - .padding(12.dp) + .padding(horizontal = 12.dp) ) { Text( text = task.title, @@ -204,13 +195,15 @@ private fun ListCardContent(viewModel: TaskViewModel = hiltViewModel()) { fontFamily = FontFamily.SansSerif, fontWeight = FontWeight.Bold ) - Text( - text = task.content.asString(), - modifier = Modifier.padding(top = 12.dp), - fontSize = 16.sp, - fontFamily = FontFamily.SansSerif, - fontWeight = FontWeight.Bold - ) + if (isExpanded) { + Text( + text = task.content.asString(), + modifier = Modifier.padding(top = 8.dp), + fontSize = 16.sp, + fontFamily = FontFamily.SansSerif, + fontWeight = FontWeight.Bold + ) + } } } } diff --git a/app/src/main/java/com/flowintent/workspace/ui/button/CustomRadioButton.kt b/app/src/main/java/com/flowintent/workspace/ui/button/CustomRadioButton.kt new file mode 100644 index 0000000..113611c --- /dev/null +++ b/app/src/main/java/com/flowintent/workspace/ui/button/CustomRadioButton.kt @@ -0,0 +1,53 @@ +package com.flowintent.workspace.ui.button + +import androidx.compose.foundation.background +import androidx.compose.foundation.border +import androidx.compose.foundation.clickable +import androidx.compose.foundation.layout.Box +import androidx.compose.foundation.layout.size +import androidx.compose.foundation.shape.CircleShape +import androidx.compose.material.icons.Icons +import androidx.compose.material.icons.filled.Check +import androidx.compose.material3.Icon +import androidx.compose.material3.MaterialTheme +import androidx.compose.runtime.Composable +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.draw.clip +import androidx.compose.ui.graphics.Color +import androidx.compose.ui.unit.dp + +@Composable +fun CustomRadioButton( + selected: Boolean, + onClick: () -> Unit, + modifier: Modifier = Modifier +) { + val selectedColor = Color(0xFF42A5F5) + + Box( + modifier = modifier + .size(20.dp) + .clip(CircleShape) + .background( + if (selected) selectedColor else Color.Transparent + ) + .border( + width = 2.dp, + color = if (selected) selectedColor + else MaterialTheme.colorScheme.onSurface.copy(alpha = 0.4f), + shape = CircleShape + ) + .clickable { onClick() }, + contentAlignment = Alignment.Center + ) { + if (selected) { + Icon( + imageVector = Icons.Default.Check, + contentDescription = "Selected", + tint = Color.White, + modifier = Modifier.size(12.dp) + ) + } + } +} diff --git a/app/src/main/java/com/flowintent/workspace/ui/dialog/OpenTaskDialog.kt b/app/src/main/java/com/flowintent/workspace/ui/dialog/OpenTaskDialog.kt index 074d389..9ef21ff 100644 --- a/app/src/main/java/com/flowintent/workspace/ui/dialog/OpenTaskDialog.kt +++ b/app/src/main/java/com/flowintent/workspace/ui/dialog/OpenTaskDialog.kt @@ -64,7 +64,8 @@ fun OpenTaskDialog( taskType = TaskType.LOCAL_TASKS, cardColor = colorPicker.next().toArgbCompat(), iconColor = 0xFFFFFFFF.toInt(), - textColor = 0xFF000000.toInt() + textColor = 0xFF000000.toInt(), + dueDate = System.currentTimeMillis() ) ) } else { diff --git a/app/src/main/java/com/flowintent/workspace/ui/swipe/SwipeableCard.kt b/app/src/main/java/com/flowintent/workspace/ui/swipe/SwipeableCard.kt index c6d43c2..80fa771 100644 --- a/app/src/main/java/com/flowintent/workspace/ui/swipe/SwipeableCard.kt +++ b/app/src/main/java/com/flowintent/workspace/ui/swipe/SwipeableCard.kt @@ -54,7 +54,7 @@ fun SwipeableCard( val maxSwipe = with(LocalDensity.current) { 200.dp.toPx() } val cardHeight by animateDpAsState( - targetValue = if (isExpanded) 100.dp else 50.dp, + targetValue = if (isExpanded) 80.dp else 50.dp, animationSpec = tween(300), label = "cardHeight" ) diff --git a/app/src/main/java/com/flowintent/workspace/ui/swipe/SwipeableCardContent.kt b/app/src/main/java/com/flowintent/workspace/ui/swipe/SwipeableCardContent.kt index 3c5a731..2ee0283 100644 --- a/app/src/main/java/com/flowintent/workspace/ui/swipe/SwipeableCardContent.kt +++ b/app/src/main/java/com/flowintent/workspace/ui/swipe/SwipeableCardContent.kt @@ -1,6 +1,7 @@ package com.flowintent.workspace.ui.swipe import androidx.compose.foundation.layout.Box +import androidx.compose.foundation.layout.Row import androidx.compose.foundation.layout.fillMaxHeight import androidx.compose.foundation.layout.fillMaxSize import androidx.compose.foundation.layout.padding @@ -8,20 +9,29 @@ import androidx.compose.foundation.layout.width import androidx.compose.foundation.shape.RoundedCornerShape import androidx.compose.material3.Card import androidx.compose.material3.CardDefaults +import androidx.compose.material3.MaterialTheme import androidx.compose.material3.Text import androidx.compose.runtime.Composable +import androidx.compose.runtime.collectAsState import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.graphics.Color import androidx.compose.ui.text.style.TextAlign import androidx.compose.ui.unit.dp +import androidx.hilt.lifecycle.viewmodel.compose.hiltViewModel import com.flowintent.core.db.Task +import com.flowintent.workspace.ui.button.CustomRadioButton +import com.flowintent.workspace.ui.vm.TaskViewModel +import com.flowintent.workspace.util.getRelativeDayLabel @Composable fun SwipeableCardContent( task: Task, - content: @Composable () -> Unit + content: @Composable () -> Unit, + viewModel: TaskViewModel = hiltViewModel() ) { + val isSelected = viewModel.selectedTasks[task.uid] ?: false + Box(modifier = Modifier.fillMaxSize()) { // UID bar Card( @@ -45,7 +55,22 @@ fun SwipeableCardContent( .padding(start = 48.dp), contentAlignment = Alignment.CenterStart ) { - content() + Row(verticalAlignment = Alignment.CenterVertically) { + CustomRadioButton( + selected = isSelected, + onClick = { viewModel.toggleSelection(task.uid) }, + modifier = Modifier.padding(start = 8.dp) + ) + content() + } + Text( + text = getRelativeDayLabel(task.dueDate), + modifier = Modifier + .align(Alignment.BottomEnd) + .padding(8.dp), + style = MaterialTheme.typography.labelSmall, + color = MaterialTheme.colorScheme.onSurfaceVariant + ) } } } diff --git a/app/src/main/java/com/flowintent/workspace/util/DateHelper.kt b/app/src/main/java/com/flowintent/workspace/util/DateHelper.kt new file mode 100644 index 0000000..0c6c533 --- /dev/null +++ b/app/src/main/java/com/flowintent/workspace/util/DateHelper.kt @@ -0,0 +1,24 @@ +package com.flowintent.workspace.util + +import java.text.SimpleDateFormat +import java.util.Calendar +import java.util.Locale + +fun getRelativeDayLabel(dueDateMillis: Long): String { + val now = Calendar.getInstance() + val due = Calendar.getInstance().apply { timeInMillis = dueDateMillis } + + val today = now.get(Calendar.DAY_OF_YEAR) + val dueDay = due.get(Calendar.DAY_OF_YEAR) + val diff = dueDay - today + + return when (diff) { + 0 -> "Today" + 1 -> "Tomorrow" + -1 -> "Yesterday" + else -> { + val format = SimpleDateFormat("dd MMM", Locale.getDefault()) + format.format(due.time) + } + } +} diff --git a/core/src/main/java/com/flowintent/core/db/Task.kt b/core/src/main/java/com/flowintent/core/db/Task.kt index 106bc62..c1fe073 100644 --- a/core/src/main/java/com/flowintent/core/db/Task.kt +++ b/core/src/main/java/com/flowintent/core/db/Task.kt @@ -12,5 +12,6 @@ data class Task( @ColumnInfo("task_type") var taskType: TaskType = TaskType.LOCAL_TASKS, @ColumnInfo("card_color") var cardColor: Int, @ColumnInfo("icon_color") var iconColor: Int, - @ColumnInfo("text_color") var textColor: Int = -1 + @ColumnInfo("text_color") var textColor: Int = -1, + @ColumnInfo("due_date") var dueDate: Long ) diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index 69abfee..ee9aa9d 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -4,6 +4,7 @@ activityCompose = "1.11.0" agp = "8.11.2" coreSplashscreen = "1.0.1" datastorePreferences = "1.1.7" +desugar_jdk_libs = "2.1.5" firebaseBom = "34.3.0" gson = "2.13.2" hilt = "2.57.1" @@ -59,6 +60,7 @@ androidx-ui-test-manifest = { module = "androidx.compose.ui:ui-test-manifest", v androidx-ui-tooling-preview = { module = "androidx.compose.ui:ui-tooling-preview" } androidx-ui = { module = "androidx.compose.ui:ui" } androidx-ui-tooling = { module = "androidx.compose.ui:ui-tooling" } +desugar_jdk_libs = { module = "com.android.tools:desugar_jdk_libs", version.ref = "desugar_jdk_libs" } firebase-analytics = { module = "com.google.firebase:firebase-analytics" } firebase-auth = { module = "com.google.firebase:firebase-auth" } firebase-bom = { module = "com.google.firebase:firebase-bom", version.ref = "firebaseBom" } diff --git a/test/src/test/java/com/flowintent/test/TaskRepositoryTest.kt b/test/src/test/java/com/flowintent/test/TaskRepositoryTest.kt index d66cfbd..29d4b95 100644 --- a/test/src/test/java/com/flowintent/test/TaskRepositoryTest.kt +++ b/test/src/test/java/com/flowintent/test/TaskRepositoryTest.kt @@ -42,7 +42,8 @@ class TaskRepositoryTest { taskType = TaskType.LOCAL_TASKS, cardColor = 0xFF6200EE.toInt(), iconColor = 0xFFFFFFFF.toInt(), - textColor = 0xFF000000.toInt() + textColor = 0xFF000000.toInt(), + dueDate = System.currentTimeMillis() ), Task( uid = 2, @@ -51,7 +52,8 @@ class TaskRepositoryTest { taskType = TaskType.LOCAL_TASKS, cardColor = 0xFF03DAC5.toInt(), iconColor = 0xFFFFFFFF.toInt(), - textColor = 0xFF000000.toInt() + textColor = 0xFF000000.toInt(), + dueDate = System.currentTimeMillis() ) ) `when`(toDoDao.getAllTasks()).thenReturn(flowOf(expectedTasks)) @@ -73,7 +75,8 @@ class TaskRepositoryTest { taskType = TaskType.LOCAL_TASKS, cardColor = 0xFF6200EE.toInt(), iconColor = 0xFFFFFFFF.toInt(), - textColor = 0xFF000000.toInt() + textColor = 0xFF000000.toInt(), + dueDate = System.currentTimeMillis() ) // Act: Call the repository method @@ -94,7 +97,8 @@ class TaskRepositoryTest { taskType = TaskType.LOCAL_TASKS, cardColor = 0xFF6200EE.toInt(), iconColor = 0xFFFFFFFF.toInt(), - textColor = 0xFF000000.toInt() + textColor = 0xFF000000.toInt(), + dueDate = System.currentTimeMillis() ) `when`(toDoDao.findByTaskName(taskName)).thenReturn(expectedTask) @@ -115,7 +119,8 @@ class TaskRepositoryTest { taskType = TaskType.LOCAL_TASKS, cardColor = 0xFF6200EE.toInt(), iconColor = 0xFFFFFFFF.toInt(), - textColor = 0xFF000000.toInt() + textColor = 0xFF000000.toInt(), + dueDate = System.currentTimeMillis() ) val expectedRowsAffected = 1 `when`(toDoDao.delete(task)).thenReturn(expectedRowsAffected)