diff --git a/tasks-app-android/src/main/java/net/opatry/tasks/app/MainActivity.kt b/tasks-app-android/src/main/java/net/opatry/tasks/app/MainActivity.kt index 8275a1a5..a9f65b6c 100644 --- a/tasks-app-android/src/main/java/net/opatry/tasks/app/MainActivity.kt +++ b/tasks-app-android/src/main/java/net/opatry/tasks/app/MainActivity.kt @@ -32,10 +32,10 @@ import androidx.compose.runtime.LaunchedEffect import androidx.compose.runtime.getValue import androidx.compose.ui.Modifier import androidx.lifecycle.compose.collectAsStateWithLifecycle -import net.opatry.tasks.app.ui.TaskListsViewModel +import net.opatry.tasks.app.presentation.TaskListsViewModel +import net.opatry.tasks.app.presentation.UserState +import net.opatry.tasks.app.presentation.UserViewModel import net.opatry.tasks.app.ui.TasksApp -import net.opatry.tasks.app.ui.UserState -import net.opatry.tasks.app.ui.UserViewModel import net.opatry.tasks.app.ui.component.AuthorizeGoogleTasksButton import net.opatry.tasks.app.ui.component.LoadingPane import net.opatry.tasks.app.ui.screen.AboutApp diff --git a/tasks-app-android/src/test/java/net/opatry/tasks/app/di/AndroidDITest.kt b/tasks-app-android/src/test/java/net/opatry/tasks/app/di/AndroidDITest.kt index 73a82ad8..315c711c 100644 --- a/tasks-app-android/src/test/java/net/opatry/tasks/app/di/AndroidDITest.kt +++ b/tasks-app-android/src/test/java/net/opatry/tasks/app/di/AndroidDITest.kt @@ -29,8 +29,8 @@ import net.opatry.google.profile.UserInfoApi import net.opatry.google.tasks.TaskListsApi import net.opatry.google.tasks.TasksApi import net.opatry.tasks.CredentialsStorage -import net.opatry.tasks.app.ui.TaskListsViewModel -import net.opatry.tasks.app.ui.UserViewModel +import net.opatry.tasks.app.presentation.TaskListsViewModel +import net.opatry.tasks.app.presentation.UserViewModel import net.opatry.tasks.data.TaskDao import net.opatry.tasks.data.TaskListDao import net.opatry.tasks.data.TaskRepository diff --git a/tasks-app-desktop/src/main/kotlin/mainApp.kt b/tasks-app-desktop/src/main/kotlin/mainApp.kt index 28958c7c..42da9546 100644 --- a/tasks-app-desktop/src/main/kotlin/mainApp.kt +++ b/tasks-app-desktop/src/main/kotlin/mainApp.kt @@ -41,10 +41,10 @@ import net.opatry.tasks.app.di.loggingModule import net.opatry.tasks.app.di.networkModule import net.opatry.tasks.app.di.platformModule import net.opatry.tasks.app.di.tasksAppModule -import net.opatry.tasks.app.ui.TaskListsViewModel +import net.opatry.tasks.app.presentation.TaskListsViewModel +import net.opatry.tasks.app.presentation.UserState +import net.opatry.tasks.app.presentation.UserViewModel import net.opatry.tasks.app.ui.TasksApp -import net.opatry.tasks.app.ui.UserState -import net.opatry.tasks.app.ui.UserViewModel import net.opatry.tasks.app.ui.component.AuthorizeGoogleTasksButton import net.opatry.tasks.app.ui.component.LoadingPane import net.opatry.tasks.app.ui.screen.AboutApp diff --git a/tasks-app-desktop/src/test/kotlin/net/opatry/tasks/app/di/DesktopDITest.kt b/tasks-app-desktop/src/test/kotlin/net/opatry/tasks/app/di/DesktopDITest.kt index 62e9489b..4ff154e5 100644 --- a/tasks-app-desktop/src/test/kotlin/net/opatry/tasks/app/di/DesktopDITest.kt +++ b/tasks-app-desktop/src/test/kotlin/net/opatry/tasks/app/di/DesktopDITest.kt @@ -29,8 +29,8 @@ import net.opatry.google.profile.UserInfoApi import net.opatry.google.tasks.TaskListsApi import net.opatry.google.tasks.TasksApi import net.opatry.tasks.CredentialsStorage -import net.opatry.tasks.app.ui.TaskListsViewModel -import net.opatry.tasks.app.ui.UserViewModel +import net.opatry.tasks.app.presentation.TaskListsViewModel +import net.opatry.tasks.app.presentation.UserViewModel import net.opatry.tasks.data.TaskDao import net.opatry.tasks.data.TaskListDao import net.opatry.tasks.data.TaskRepository diff --git a/tasks-app-shared/src/androidMain/kotlin/net/opatry/tasks/app/ui/component/CompletedTaskRowPreview.kt b/tasks-app-shared/src/androidMain/kotlin/net/opatry/tasks/app/ui/component/CompletedTaskRowPreview.kt index 242abe0f..90397e3b 100644 --- a/tasks-app-shared/src/androidMain/kotlin/net/opatry/tasks/app/ui/component/CompletedTaskRowPreview.kt +++ b/tasks-app-shared/src/androidMain/kotlin/net/opatry/tasks/app/ui/component/CompletedTaskRowPreview.kt @@ -30,8 +30,8 @@ import kotlinx.datetime.Clock import kotlinx.datetime.LocalDate import kotlinx.datetime.TimeZone import kotlinx.datetime.todayIn -import net.opatry.tasks.app.ui.model.TaskId -import net.opatry.tasks.app.ui.model.TaskUIModel +import net.opatry.tasks.app.presentation.model.TaskId +import net.opatry.tasks.app.presentation.model.TaskUIModel import net.opatry.tasks.app.ui.tooling.TaskfolioThemedPreview private class CompletedTaskRowPreviewDataProvider : diff --git a/tasks-app-shared/src/androidMain/kotlin/net/opatry/tasks/app/ui/component/RemainingTaskRowPreview.kt b/tasks-app-shared/src/androidMain/kotlin/net/opatry/tasks/app/ui/component/RemainingTaskRowPreview.kt index 8850e79e..a301c176 100644 --- a/tasks-app-shared/src/androidMain/kotlin/net/opatry/tasks/app/ui/component/RemainingTaskRowPreview.kt +++ b/tasks-app-shared/src/androidMain/kotlin/net/opatry/tasks/app/ui/component/RemainingTaskRowPreview.kt @@ -32,8 +32,8 @@ import kotlinx.datetime.TimeZone import kotlinx.datetime.minus import kotlinx.datetime.plus import kotlinx.datetime.todayIn -import net.opatry.tasks.app.ui.model.TaskId -import net.opatry.tasks.app.ui.model.TaskUIModel +import net.opatry.tasks.app.presentation.model.TaskId +import net.opatry.tasks.app.presentation.model.TaskUIModel import net.opatry.tasks.app.ui.tooling.TaskfolioThemedPreview diff --git a/tasks-app-shared/src/androidMain/kotlin/net/opatry/tasks/app/ui/component/TaskListEditMenuPreview.kt b/tasks-app-shared/src/androidMain/kotlin/net/opatry/tasks/app/ui/component/TaskListEditMenuPreview.kt index 9d92fb3c..4258ba7c 100644 --- a/tasks-app-shared/src/androidMain/kotlin/net/opatry/tasks/app/ui/component/TaskListEditMenuPreview.kt +++ b/tasks-app-shared/src/androidMain/kotlin/net/opatry/tasks/app/ui/component/TaskListEditMenuPreview.kt @@ -35,8 +35,8 @@ import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.tooling.preview.Preview import androidx.compose.ui.unit.dp -import net.opatry.tasks.app.ui.model.TaskListId -import net.opatry.tasks.app.ui.model.TaskListUIModel +import net.opatry.tasks.app.presentation.model.TaskListId +import net.opatry.tasks.app.presentation.model.TaskListUIModel import net.opatry.tasks.app.ui.tooling.TaskfolioThemedPreview // FIXME When displayed with dark, the menu labels are invisible diff --git a/tasks-app-shared/src/androidMain/kotlin/net/opatry/tasks/app/ui/component/TaskListSortMenuPreview.kt b/tasks-app-shared/src/androidMain/kotlin/net/opatry/tasks/app/ui/component/TaskListSortMenuPreview.kt index 09b5badc..4e7bdefe 100644 --- a/tasks-app-shared/src/androidMain/kotlin/net/opatry/tasks/app/ui/component/TaskListSortMenuPreview.kt +++ b/tasks-app-shared/src/androidMain/kotlin/net/opatry/tasks/app/ui/component/TaskListSortMenuPreview.kt @@ -35,8 +35,8 @@ import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.tooling.preview.Preview import androidx.compose.ui.unit.dp -import net.opatry.tasks.app.ui.model.TaskListId -import net.opatry.tasks.app.ui.model.TaskListUIModel +import net.opatry.tasks.app.presentation.model.TaskListId +import net.opatry.tasks.app.presentation.model.TaskListUIModel import net.opatry.tasks.app.ui.tooling.TaskfolioThemedPreview import net.opatry.tasks.data.TaskListSorting diff --git a/tasks-app-shared/src/androidMain/kotlin/net/opatry/tasks/app/ui/component/TaskListTopAppBarPreview.kt b/tasks-app-shared/src/androidMain/kotlin/net/opatry/tasks/app/ui/component/TaskListTopAppBarPreview.kt index 05a6fb54..4ad1d508 100644 --- a/tasks-app-shared/src/androidMain/kotlin/net/opatry/tasks/app/ui/component/TaskListTopAppBarPreview.kt +++ b/tasks-app-shared/src/androidMain/kotlin/net/opatry/tasks/app/ui/component/TaskListTopAppBarPreview.kt @@ -26,8 +26,8 @@ import androidx.compose.runtime.Composable import androidx.compose.ui.tooling.preview.PreviewLightDark import androidx.compose.ui.tooling.preview.PreviewParameter import androidx.compose.ui.tooling.preview.PreviewParameterProvider -import net.opatry.tasks.app.ui.model.TaskListId -import net.opatry.tasks.app.ui.model.TaskListUIModel +import net.opatry.tasks.app.presentation.model.TaskListId +import net.opatry.tasks.app.presentation.model.TaskListUIModel import net.opatry.tasks.app.ui.tooling.TaskfolioThemedPreview import net.opatry.tasks.data.TaskListSorting diff --git a/tasks-app-shared/src/androidMain/kotlin/net/opatry/tasks/app/ui/component/TasksColumnPreview.kt b/tasks-app-shared/src/androidMain/kotlin/net/opatry/tasks/app/ui/component/TasksColumnPreview.kt new file mode 100644 index 00000000..252ecb22 --- /dev/null +++ b/tasks-app-shared/src/androidMain/kotlin/net/opatry/tasks/app/ui/component/TasksColumnPreview.kt @@ -0,0 +1,105 @@ +/* + * Copyright (c) 2025 Olivier Patry + * + * Permission is hereby granted, free of charge, to any person obtaining + * a copy of this software and associated documentation files (the "Software"), + * to deal in the Software without restriction, including without limitation + * the rights to use, copy, modify, merge, publish, distribute, sublicense, + * and/or sell copies of the Software, and to permit persons to whom the Software + * is furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, + * EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES + * OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. + * IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY + * CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, + * TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE + * OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + */ + +package net.opatry.tasks.app.ui.component + +import androidx.compose.runtime.Composable +import androidx.compose.ui.tooling.preview.PreviewLightDark +import androidx.compose.ui.tooling.preview.PreviewParameter +import androidx.compose.ui.tooling.preview.PreviewParameterProvider +import kotlinx.datetime.LocalDate +import net.opatry.tasks.app.presentation.model.DateRange +import net.opatry.tasks.app.presentation.model.TaskId +import net.opatry.tasks.app.presentation.model.TaskListId +import net.opatry.tasks.app.presentation.model.TaskListUIModel +import net.opatry.tasks.app.presentation.model.TaskUIModel +import net.opatry.tasks.app.ui.tooling.TaskfolioThemedPreview +import net.opatry.tasks.data.TaskListSorting + +private class TaskListPreviewParameterProvider : PreviewParameterProvider { + override val values = sequenceOf( + // fully empty state + TaskListUIModel( + id = TaskListId(0L), + title = "Whole new list", + remainingTasks = emptyMap(), + completedTasks = emptyList() + ), + // all done empty state + TaskListUIModel( + id = TaskListId(0L), + title = "All done", + remainingTasks = emptyMap(), + completedTasks = listOf( + TaskUIModel( + id = TaskId(1L), + title = "Task 1", + isCompleted = true, + ), + ) + ), + // remaining tasks sorted manually + TaskListUIModel( + id = TaskListId(0L), + title = "All remains manually ordered", + sorting = TaskListSorting.Manual, + remainingTasks = mapOf( + DateRange.None to listOf( + TaskUIModel( + id = TaskId(1L), + title = "Task 1", + isCompleted = false, + ), + ), + DateRange.Overdue(LocalDate.parse("2023-01-01"), 40) to listOf( + TaskUIModel( + id = TaskId(2L), + title = "Task 2", + isCompleted = false, + ), + TaskUIModel( + id = TaskId(3L), + title = "Task 3", + isCompleted = false, + indent = 1, + ), + ), + ), + completedTasks = emptyList() + ), + ) +} + +@PreviewLightDark +@Composable +private fun TasksColumnPreview( + @PreviewParameter(TaskListPreviewParameterProvider::class) + taskList: TaskListUIModel, +) { + TaskfolioThemedPreview { + TasksColumn( + taskLists = listOf(taskList), + taskList = taskList, + showCompletedDefaultValue = true + ) + } +} \ No newline at end of file diff --git a/tasks-app-shared/src/androidMain/kotlin/net/opatry/tasks/app/ui/component/profileIconPreview.kt b/tasks-app-shared/src/androidMain/kotlin/net/opatry/tasks/app/ui/component/profileIconPreview.kt index c911a6b8..fc8d4e6c 100644 --- a/tasks-app-shared/src/androidMain/kotlin/net/opatry/tasks/app/ui/component/profileIconPreview.kt +++ b/tasks-app-shared/src/androidMain/kotlin/net/opatry/tasks/app/ui/component/profileIconPreview.kt @@ -25,7 +25,7 @@ package net.opatry.tasks.app.ui.component import androidx.compose.foundation.layout.Column import androidx.compose.runtime.Composable import androidx.compose.ui.tooling.preview.PreviewLightDark -import net.opatry.tasks.app.ui.UserState +import net.opatry.tasks.app.presentation.UserState import net.opatry.tasks.app.ui.tooling.TaskfolioThemedPreview @PreviewLightDark diff --git a/tasks-app-shared/src/androidMain/kotlin/net/opatry/tasks/app/ui/screen/taskListsPanePreview.kt b/tasks-app-shared/src/androidMain/kotlin/net/opatry/tasks/app/ui/screen/taskListsPanePreview.kt index 986bcf31..a36d6675 100644 --- a/tasks-app-shared/src/androidMain/kotlin/net/opatry/tasks/app/ui/screen/taskListsPanePreview.kt +++ b/tasks-app-shared/src/androidMain/kotlin/net/opatry/tasks/app/ui/screen/taskListsPanePreview.kt @@ -29,8 +29,8 @@ import androidx.compose.runtime.Composable import androidx.compose.ui.Modifier import androidx.compose.ui.tooling.preview.PreviewLightDark import androidx.compose.ui.unit.dp -import net.opatry.tasks.app.ui.model.TaskListId -import net.opatry.tasks.app.ui.model.TaskListUIModel +import net.opatry.tasks.app.presentation.model.TaskListId +import net.opatry.tasks.app.presentation.model.TaskListUIModel import net.opatry.tasks.app.ui.tooling.TaskfolioThemedPreview diff --git a/tasks-app-shared/src/androidMain/kotlin/net/opatry/tasks/app/ui/screen/tasksPanePreview.kt b/tasks-app-shared/src/androidMain/kotlin/net/opatry/tasks/app/ui/screen/tasksPanePreview.kt index ef6f585b..1241eca8 100644 --- a/tasks-app-shared/src/androidMain/kotlin/net/opatry/tasks/app/ui/screen/tasksPanePreview.kt +++ b/tasks-app-shared/src/androidMain/kotlin/net/opatry/tasks/app/ui/screen/tasksPanePreview.kt @@ -38,8 +38,8 @@ import kotlinx.datetime.plus import kotlinx.datetime.todayIn import net.opatry.tasks.app.ui.component.CompletedTaskRow import net.opatry.tasks.app.ui.component.RemainingTaskRow -import net.opatry.tasks.app.ui.model.TaskId -import net.opatry.tasks.app.ui.model.TaskUIModel +import net.opatry.tasks.app.presentation.model.TaskId +import net.opatry.tasks.app.presentation.model.TaskUIModel import net.opatry.tasks.app.ui.tooling.TaskfolioThemedPreview diff --git a/tasks-app-shared/src/commonMain/kotlin/net/opatry/tasks/app/di/tasksAppModule.kt b/tasks-app-shared/src/commonMain/kotlin/net/opatry/tasks/app/di/tasksAppModule.kt index 07fa4ae9..6c0f2b9e 100644 --- a/tasks-app-shared/src/commonMain/kotlin/net/opatry/tasks/app/di/tasksAppModule.kt +++ b/tasks-app-shared/src/commonMain/kotlin/net/opatry/tasks/app/di/tasksAppModule.kt @@ -24,8 +24,8 @@ package net.opatry.tasks.app.di import net.opatry.google.profile.HttpUserInfoApi import net.opatry.google.profile.UserInfoApi -import net.opatry.tasks.app.ui.TaskListsViewModel -import net.opatry.tasks.app.ui.UserViewModel +import net.opatry.tasks.app.presentation.TaskListsViewModel +import net.opatry.tasks.app.presentation.UserViewModel import net.opatry.tasks.data.TaskRepository import org.koin.core.module.dsl.viewModel import org.koin.core.qualifier.named diff --git a/tasks-app-shared/src/commonMain/kotlin/net/opatry/tasks/app/ui/TaskListsViewModel.kt b/tasks-app-shared/src/commonMain/kotlin/net/opatry/tasks/app/presentation/TaskListsViewModel.kt similarity index 96% rename from tasks-app-shared/src/commonMain/kotlin/net/opatry/tasks/app/ui/TaskListsViewModel.kt rename to tasks-app-shared/src/commonMain/kotlin/net/opatry/tasks/app/presentation/TaskListsViewModel.kt index 7548e29f..14294062 100644 --- a/tasks-app-shared/src/commonMain/kotlin/net/opatry/tasks/app/ui/TaskListsViewModel.kt +++ b/tasks-app-shared/src/commonMain/kotlin/net/opatry/tasks/app/presentation/TaskListsViewModel.kt @@ -20,7 +20,7 @@ * OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. */ -package net.opatry.tasks.app.ui +package net.opatry.tasks.app.presentation import androidx.lifecycle.ViewModel import androidx.lifecycle.viewModelScope @@ -40,12 +40,13 @@ import kotlinx.datetime.TimeZone import kotlinx.datetime.atStartOfDayIn import kotlinx.datetime.toLocalDateTime import net.opatry.Logger -import net.opatry.tasks.app.ui.model.DateRange -import net.opatry.tasks.app.ui.model.TaskId -import net.opatry.tasks.app.ui.model.TaskListId -import net.opatry.tasks.app.ui.model.TaskListUIModel -import net.opatry.tasks.app.ui.model.TaskUIModel -import net.opatry.tasks.app.ui.model.compareTo +import net.opatry.tasks.app.presentation.model.DateRange +import net.opatry.tasks.app.presentation.model.TaskId +import net.opatry.tasks.app.presentation.model.TaskListId +import net.opatry.tasks.app.presentation.model.TaskListUIModel +import net.opatry.tasks.app.presentation.model.TaskUIModel +import net.opatry.tasks.app.presentation.model.compareTo +import net.opatry.tasks.app.ui.TaskEvent import net.opatry.tasks.data.TaskListSorting import net.opatry.tasks.data.TaskRepository import net.opatry.tasks.data.model.TaskDataModel diff --git a/tasks-app-shared/src/commonMain/kotlin/net/opatry/tasks/app/ui/UserViewModel.kt b/tasks-app-shared/src/commonMain/kotlin/net/opatry/tasks/app/presentation/UserViewModel.kt similarity index 99% rename from tasks-app-shared/src/commonMain/kotlin/net/opatry/tasks/app/ui/UserViewModel.kt rename to tasks-app-shared/src/commonMain/kotlin/net/opatry/tasks/app/presentation/UserViewModel.kt index 1f7392f7..29405f72 100644 --- a/tasks-app-shared/src/commonMain/kotlin/net/opatry/tasks/app/ui/UserViewModel.kt +++ b/tasks-app-shared/src/commonMain/kotlin/net/opatry/tasks/app/presentation/UserViewModel.kt @@ -20,7 +20,7 @@ * OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. */ -package net.opatry.tasks.app.ui +package net.opatry.tasks.app.presentation import androidx.lifecycle.ViewModel import androidx.lifecycle.viewModelScope diff --git a/tasks-app-shared/src/commonMain/kotlin/net/opatry/tasks/app/ui/model/TaskListUIModel.kt b/tasks-app-shared/src/commonMain/kotlin/net/opatry/tasks/app/presentation/model/TaskListUIModel.kt similarity index 97% rename from tasks-app-shared/src/commonMain/kotlin/net/opatry/tasks/app/ui/model/TaskListUIModel.kt rename to tasks-app-shared/src/commonMain/kotlin/net/opatry/tasks/app/presentation/model/TaskListUIModel.kt index 0f8101f9..97ff8d31 100644 --- a/tasks-app-shared/src/commonMain/kotlin/net/opatry/tasks/app/ui/model/TaskListUIModel.kt +++ b/tasks-app-shared/src/commonMain/kotlin/net/opatry/tasks/app/presentation/model/TaskListUIModel.kt @@ -20,7 +20,7 @@ * OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. */ -package net.opatry.tasks.app.ui.model +package net.opatry.tasks.app.presentation.model import net.opatry.tasks.data.TaskListSorting diff --git a/tasks-app-shared/src/commonMain/kotlin/net/opatry/tasks/app/ui/model/TaskUIModel.kt b/tasks-app-shared/src/commonMain/kotlin/net/opatry/tasks/app/presentation/model/TaskUIModel.kt similarity index 98% rename from tasks-app-shared/src/commonMain/kotlin/net/opatry/tasks/app/ui/model/TaskUIModel.kt rename to tasks-app-shared/src/commonMain/kotlin/net/opatry/tasks/app/presentation/model/TaskUIModel.kt index 7a8b6066..58e214a7 100644 --- a/tasks-app-shared/src/commonMain/kotlin/net/opatry/tasks/app/ui/model/TaskUIModel.kt +++ b/tasks-app-shared/src/commonMain/kotlin/net/opatry/tasks/app/presentation/model/TaskUIModel.kt @@ -20,7 +20,7 @@ * OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. */ -package net.opatry.tasks.app.ui.model +package net.opatry.tasks.app.presentation.model import kotlinx.datetime.Clock import kotlinx.datetime.LocalDate diff --git a/tasks-app-shared/src/commonMain/kotlin/net/opatry/tasks/app/ui/TasksApp.kt b/tasks-app-shared/src/commonMain/kotlin/net/opatry/tasks/app/ui/TasksApp.kt index f0ce1be1..bdbf27cf 100644 --- a/tasks-app-shared/src/commonMain/kotlin/net/opatry/tasks/app/ui/TasksApp.kt +++ b/tasks-app-shared/src/commonMain/kotlin/net/opatry/tasks/app/ui/TasksApp.kt @@ -56,6 +56,9 @@ import androidx.navigation.compose.NavHost import androidx.navigation.compose.composable import androidx.navigation.compose.currentBackStackEntryAsState import androidx.navigation.compose.rememberNavController +import net.opatry.tasks.app.presentation.TaskListsViewModel +import net.opatry.tasks.app.presentation.UserState +import net.opatry.tasks.app.presentation.UserViewModel import net.opatry.tasks.app.ui.component.EditTextDialog import net.opatry.tasks.app.ui.component.MissingScreen import net.opatry.tasks.app.ui.component.ProfileIcon diff --git a/tasks-app-shared/src/commonMain/kotlin/net/opatry/tasks/app/ui/component/CompletedTaskRow.kt b/tasks-app-shared/src/commonMain/kotlin/net/opatry/tasks/app/ui/component/CompletedTaskRow.kt index 06df5ad8..e5275b4c 100644 --- a/tasks-app-shared/src/commonMain/kotlin/net/opatry/tasks/app/ui/component/CompletedTaskRow.kt +++ b/tasks-app-shared/src/commonMain/kotlin/net/opatry/tasks/app/ui/component/CompletedTaskRow.kt @@ -48,18 +48,27 @@ import kotlinx.datetime.format.MonthNames import kotlinx.datetime.format.Padding import kotlinx.datetime.format.char import kotlinx.datetime.todayIn -import net.opatry.tasks.app.ui.model.TaskUIModel -import net.opatry.tasks.app.ui.screen.TaskListPaneTestTag.COMPLETED_TASK_COMPLETION_DATE -import net.opatry.tasks.app.ui.screen.TaskListPaneTestTag.COMPLETED_TASK_DELETE_ICON -import net.opatry.tasks.app.ui.screen.TaskListPaneTestTag.COMPLETED_TASK_ICON -import net.opatry.tasks.app.ui.screen.TaskListPaneTestTag.COMPLETED_TASK_NOTES -import net.opatry.tasks.app.ui.screen.TaskListPaneTestTag.COMPLETED_TASK_ROW +import net.opatry.tasks.app.presentation.model.TaskUIModel +import net.opatry.tasks.app.ui.component.CompletedTaskRowTestTag.COMPLETED_TASK_COMPLETION_DATE +import net.opatry.tasks.app.ui.component.CompletedTaskRowTestTag.COMPLETED_TASK_DELETE_ICON +import net.opatry.tasks.app.ui.component.CompletedTaskRowTestTag.COMPLETED_TASK_ICON +import net.opatry.tasks.app.ui.component.CompletedTaskRowTestTag.COMPLETED_TASK_NOTES +import net.opatry.tasks.app.ui.component.CompletedTaskRowTestTag.COMPLETED_TASK_ROW import net.opatry.tasks.resources.Res import net.opatry.tasks.resources.task_list_pane_completed_date_label import net.opatry.tasks.resources.task_list_pane_delete_task_icon_content_desc import org.jetbrains.annotations.VisibleForTesting import org.jetbrains.compose.resources.stringResource +@VisibleForTesting +internal object CompletedTaskRowTestTag { + const val COMPLETED_TASK_ROW = "COMPLETED_TASK_ROW" + const val COMPLETED_TASK_ICON = "COMPLETED_TASK_ICON" + const val COMPLETED_TASK_NOTES = "COMPLETED_TASK_NOTES" + const val COMPLETED_TASK_COMPLETION_DATE = "COMPLETED_TASK_COMPLETION_DATE" + const val COMPLETED_TASK_DELETE_ICON = "COMPLETED_TASK_DELETE_ICON" +} + private fun LocalDate.toLabel(): String { // TODO localize names & format return if (year == Clock.System.todayIn(TimeZone.currentSystemDefault()).year) { @@ -85,9 +94,8 @@ private fun LocalDate.toLabel(): String { } } -@VisibleForTesting @Composable -internal fun CompletedTaskRow( +fun CompletedTaskRow( task: TaskUIModel, onAction: (TaskAction) -> Unit, ) { diff --git a/tasks-app-shared/src/commonMain/kotlin/net/opatry/tasks/app/ui/component/RemainingTaskRow.kt b/tasks-app-shared/src/commonMain/kotlin/net/opatry/tasks/app/ui/component/RemainingTaskRow.kt index 88614370..69e49db4 100644 --- a/tasks-app-shared/src/commonMain/kotlin/net/opatry/tasks/app/ui/component/RemainingTaskRow.kt +++ b/tasks-app-shared/src/commonMain/kotlin/net/opatry/tasks/app/ui/component/RemainingTaskRow.kt @@ -53,14 +53,14 @@ import kotlinx.datetime.format.MonthNames import kotlinx.datetime.format.Padding import kotlinx.datetime.format.char import kotlinx.datetime.todayIn -import net.opatry.tasks.app.ui.model.DateRange -import net.opatry.tasks.app.ui.model.TaskListUIModel -import net.opatry.tasks.app.ui.model.TaskUIModel -import net.opatry.tasks.app.ui.screen.TaskListPaneTestTag.REMAINING_TASK_DUE_DATE_CHIP -import net.opatry.tasks.app.ui.screen.TaskListPaneTestTag.REMAINING_TASK_ICON -import net.opatry.tasks.app.ui.screen.TaskListPaneTestTag.REMAINING_TASK_MENU_ICON -import net.opatry.tasks.app.ui.screen.TaskListPaneTestTag.REMAINING_TASK_NOTES -import net.opatry.tasks.app.ui.screen.TaskListPaneTestTag.REMAINING_TASK_ROW +import net.opatry.tasks.app.presentation.model.DateRange +import net.opatry.tasks.app.presentation.model.TaskListUIModel +import net.opatry.tasks.app.presentation.model.TaskUIModel +import net.opatry.tasks.app.ui.component.RemainingTaskRowTestTag.REMAINING_TASK_DUE_DATE_CHIP +import net.opatry.tasks.app.ui.component.RemainingTaskRowTestTag.REMAINING_TASK_ICON +import net.opatry.tasks.app.ui.component.RemainingTaskRowTestTag.REMAINING_TASK_MENU_ICON +import net.opatry.tasks.app.ui.component.RemainingTaskRowTestTag.REMAINING_TASK_NOTES +import net.opatry.tasks.app.ui.component.RemainingTaskRowTestTag.REMAINING_TASK_ROW import net.opatry.tasks.resources.Res import net.opatry.tasks.resources.task_due_date_label_days_ago import net.opatry.tasks.resources.task_due_date_label_no_date @@ -75,6 +75,14 @@ import org.jetbrains.compose.resources.pluralStringResource import org.jetbrains.compose.resources.stringResource import kotlin.math.abs +@VisibleForTesting +internal object RemainingTaskRowTestTag { + const val REMAINING_TASK_ROW = "REMAINING_TASK_ROW" + const val REMAINING_TASK_ICON = "REMAINING_TASK_ICON" + const val REMAINING_TASK_NOTES = "REMAINING_TASK_NOTES" + const val REMAINING_TASK_DUE_DATE_CHIP = "REMAINING_TASK_DUE_DATE_CHIP" + const val REMAINING_TASK_MENU_ICON = "REMAINING_TASK_MENU_ICON" +} @VisibleForTesting @Composable @@ -132,9 +140,8 @@ internal fun DateRange.toLabel(sectionLabel: Boolean = false): String = when (th } } -@VisibleForTesting @Composable -internal fun RemainingTaskRow( +fun RemainingTaskRow( taskLists: List, task: TaskUIModel, showDate: Boolean = true, diff --git a/tasks-app-shared/src/commonMain/kotlin/net/opatry/tasks/app/ui/component/TaskListEditMenu.kt b/tasks-app-shared/src/commonMain/kotlin/net/opatry/tasks/app/ui/component/TaskListEditMenu.kt index d445cbff..c8e4a368 100644 --- a/tasks-app-shared/src/commonMain/kotlin/net/opatry/tasks/app/ui/component/TaskListEditMenu.kt +++ b/tasks-app-shared/src/commonMain/kotlin/net/opatry/tasks/app/ui/component/TaskListEditMenu.kt @@ -38,11 +38,11 @@ import androidx.compose.runtime.CompositionLocalProvider import androidx.compose.ui.Modifier import androidx.compose.ui.platform.testTag import androidx.compose.ui.unit.dp +import net.opatry.tasks.app.presentation.model.TaskListUIModel import net.opatry.tasks.app.ui.component.TaskListEditMenuTestTag.CLEAR_COMPLETED_TASKS import net.opatry.tasks.app.ui.component.TaskListEditMenuTestTag.DELETE import net.opatry.tasks.app.ui.component.TaskListEditMenuTestTag.EDIT_MENU import net.opatry.tasks.app.ui.component.TaskListEditMenuTestTag.RENAME -import net.opatry.tasks.app.ui.model.TaskListUIModel import net.opatry.tasks.resources.Res import net.opatry.tasks.resources.task_list_menu_clear_all_completed_tasks import net.opatry.tasks.resources.task_list_menu_default_list_cannot_be_deleted diff --git a/tasks-app-shared/src/commonMain/kotlin/net/opatry/tasks/app/ui/component/TaskListSortMenu.kt b/tasks-app-shared/src/commonMain/kotlin/net/opatry/tasks/app/ui/component/TaskListSortMenu.kt index b51279b8..7aefe5d1 100644 --- a/tasks-app-shared/src/commonMain/kotlin/net/opatry/tasks/app/ui/component/TaskListSortMenu.kt +++ b/tasks-app-shared/src/commonMain/kotlin/net/opatry/tasks/app/ui/component/TaskListSortMenu.kt @@ -31,11 +31,11 @@ import androidx.compose.material3.Text import androidx.compose.runtime.Composable import androidx.compose.ui.Modifier import androidx.compose.ui.platform.testTag +import net.opatry.tasks.app.presentation.model.TaskListUIModel import net.opatry.tasks.app.ui.component.TaskListSortMenuTestTag.SORT_DUE_DATE import net.opatry.tasks.app.ui.component.TaskListSortMenuTestTag.SORT_MANUAL import net.opatry.tasks.app.ui.component.TaskListSortMenuTestTag.SORT_MENU import net.opatry.tasks.app.ui.component.TaskListSortMenuTestTag.SORT_TITLE -import net.opatry.tasks.app.ui.model.TaskListUIModel import net.opatry.tasks.data.TaskListSorting import net.opatry.tasks.resources.Res import net.opatry.tasks.resources.task_list_menu_sort_by diff --git a/tasks-app-shared/src/commonMain/kotlin/net/opatry/tasks/app/ui/component/TaskListTopAppBar.kt b/tasks-app-shared/src/commonMain/kotlin/net/opatry/tasks/app/ui/component/TaskListTopAppBar.kt index ee0e3911..609aca4c 100644 --- a/tasks-app-shared/src/commonMain/kotlin/net/opatry/tasks/app/ui/component/TaskListTopAppBar.kt +++ b/tasks-app-shared/src/commonMain/kotlin/net/opatry/tasks/app/ui/component/TaskListTopAppBar.kt @@ -42,9 +42,9 @@ import androidx.compose.runtime.setValue import androidx.compose.ui.Modifier import androidx.compose.ui.platform.testTag import androidx.compose.ui.text.style.TextOverflow +import net.opatry.tasks.app.presentation.model.TaskListUIModel import net.opatry.tasks.app.ui.component.TaskListTopAppBarTestTag.MORE_MENU_ICON import net.opatry.tasks.app.ui.component.TaskListTopAppBarTestTag.SORT_MENU_ICON -import net.opatry.tasks.app.ui.model.TaskListUIModel import net.opatry.tasks.data.TaskListSorting @VisibleForTesting diff --git a/tasks-app-shared/src/commonMain/kotlin/net/opatry/tasks/app/ui/component/TaskMenu.kt b/tasks-app-shared/src/commonMain/kotlin/net/opatry/tasks/app/ui/component/TaskMenu.kt index 487a68c6..a9288119 100644 --- a/tasks-app-shared/src/commonMain/kotlin/net/opatry/tasks/app/ui/component/TaskMenu.kt +++ b/tasks-app-shared/src/commonMain/kotlin/net/opatry/tasks/app/ui/component/TaskMenu.kt @@ -26,8 +26,8 @@ import Check import CopyPlus import ListPlus import LucideIcons -import SquareStack import Trash2 +import androidx.annotation.VisibleForTesting import androidx.compose.material3.DropdownMenu import androidx.compose.material3.DropdownMenuItem import androidx.compose.material3.HorizontalDivider @@ -38,6 +38,8 @@ import androidx.compose.runtime.Composable import androidx.compose.runtime.CompositionLocalProvider import androidx.compose.ui.Modifier import androidx.compose.ui.platform.testTag +import net.opatry.tasks.app.presentation.model.TaskListUIModel +import net.opatry.tasks.app.presentation.model.TaskUIModel import net.opatry.tasks.app.ui.component.TaskMenuTestTag.ADD_SUBTASK import net.opatry.tasks.app.ui.component.TaskMenuTestTag.DELETE import net.opatry.tasks.app.ui.component.TaskMenuTestTag.INDENT @@ -46,8 +48,6 @@ import net.opatry.tasks.app.ui.component.TaskMenuTestTag.MOVE_TO_NEW_LIST import net.opatry.tasks.app.ui.component.TaskMenuTestTag.MOVE_TO_TOP import net.opatry.tasks.app.ui.component.TaskMenuTestTag.TASK_MENU import net.opatry.tasks.app.ui.component.TaskMenuTestTag.UNINDENT -import net.opatry.tasks.app.ui.model.TaskListUIModel -import net.opatry.tasks.app.ui.model.TaskUIModel import net.opatry.tasks.resources.Res import net.opatry.tasks.resources.task_menu_add_subtask import net.opatry.tasks.resources.task_menu_delete @@ -58,7 +58,8 @@ import net.opatry.tasks.resources.task_menu_new_list import net.opatry.tasks.resources.task_menu_unindent import org.jetbrains.compose.resources.stringResource -object TaskMenuTestTag { +@VisibleForTesting +internal object TaskMenuTestTag { const val TASK_MENU = "TASK_MENU" const val ADD_SUBTASK = "TASK_MENU_ADD_SUBTASK" const val MOVE_TO_TOP = "MOVE_TO_TOP" diff --git a/tasks-app-shared/src/commonMain/kotlin/net/opatry/tasks/app/ui/component/TasksColumn.kt b/tasks-app-shared/src/commonMain/kotlin/net/opatry/tasks/app/ui/component/TasksColumn.kt new file mode 100644 index 00000000..f616d8c1 --- /dev/null +++ b/tasks-app-shared/src/commonMain/kotlin/net/opatry/tasks/app/ui/component/TasksColumn.kt @@ -0,0 +1,228 @@ +/* + * Copyright (c) 2025 Olivier Patry + * + * Permission is hereby granted, free of charge, to any person obtaining + * a copy of this software and associated documentation files (the "Software"), + * to deal in the Software without restriction, including without limitation + * the rights to use, copy, modify, merge, publish, distribute, sublicense, + * and/or sell copies of the Software, and to permit persons to whom the Software + * is furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, + * EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES + * OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. + * IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY + * CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, + * TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE + * OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + */ + +package net.opatry.tasks.app.ui.component + +import CheckCheck +import ChevronDown +import ChevronRight +import CircleOff +import LucideIcons +import androidx.annotation.VisibleForTesting +import androidx.compose.foundation.background +import androidx.compose.foundation.clickable +import androidx.compose.foundation.layout.Arrangement +import androidx.compose.foundation.layout.Box +import androidx.compose.foundation.layout.PaddingValues +import androidx.compose.foundation.layout.fillMaxSize +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.Icon +import androidx.compose.material3.MaterialTheme +import androidx.compose.material3.Text +import androidx.compose.runtime.Composable +import androidx.compose.runtime.getValue +import androidx.compose.runtime.mutableStateOf +import androidx.compose.runtime.remember +import androidx.compose.runtime.setValue +import androidx.compose.ui.Modifier +import androidx.compose.ui.draw.clip +import androidx.compose.ui.platform.testTag +import androidx.compose.ui.unit.dp +import net.opatry.tasks.app.presentation.model.DateRange +import net.opatry.tasks.app.presentation.model.TaskListUIModel +import net.opatry.tasks.app.presentation.model.TaskUIModel +import net.opatry.tasks.app.ui.component.TasksColumnTestTag.ALL_COMPLETE_EMPTY_STATE +import net.opatry.tasks.app.ui.component.TasksColumnTestTag.COMPLETED_TASKS_TOGGLE +import net.opatry.tasks.app.ui.component.TasksColumnTestTag.COMPLETED_TASKS_TOGGLE_LABEL +import net.opatry.tasks.app.ui.component.TasksColumnTestTag.FULLY_EMPTY_STATE +import net.opatry.tasks.app.ui.component.TasksColumnTestTag.TASKS_COLUMN +import net.opatry.tasks.data.TaskListSorting +import net.opatry.tasks.resources.Res +import net.opatry.tasks.resources.task_list_pane_all_tasks_complete_desc +import net.opatry.tasks.resources.task_list_pane_all_tasks_complete_title +import net.opatry.tasks.resources.task_list_pane_completed_section_title_with_count +import net.opatry.tasks.resources.task_lists_screen_empty_list_desc +import net.opatry.tasks.resources.task_lists_screen_empty_list_title +import org.jetbrains.compose.resources.stringResource + +@VisibleForTesting +internal object TasksColumnTestTag { + const val FULLY_EMPTY_STATE = "FULLY_EMPTY_STATE" + const val ALL_COMPLETE_EMPTY_STATE = "ALL_COMPLETE_EMPTY_STATE" + const val TASKS_COLUMN = "TASKS_COLUMN" + const val COMPLETED_TASKS_TOGGLE = "COMPLETED_TASKS_TOGGLE" + const val COMPLETED_TASKS_TOGGLE_LABEL = "COMPLETED_TASKS_TOGGLE_LABEL" +} + +@Composable +fun TasksColumn( + taskLists: List, + taskList: TaskListUIModel, + onToggleCompletionState: (TaskUIModel) -> Unit = {}, + onEditTask: (TaskUIModel) -> Unit = {}, + onUpdateDueDate: (TaskUIModel) -> Unit = {}, + onNewSubTask: (TaskUIModel) -> Unit = {}, + onUnindent: (TaskUIModel) -> Unit = {}, + onIndent: (TaskUIModel) -> Unit = {}, + onMoveToTop: (TaskUIModel) -> Unit = {}, + onMoveToList: (TaskUIModel, TaskListUIModel) -> Unit = { _, _ -> }, + onMoveToNewList: (TaskUIModel) -> Unit = {}, + onDeleteTask: (TaskUIModel) -> Unit = {}, + showCompletedDefaultValue: Boolean = false, +) { + var showCompleted by remember(taskList.id) { mutableStateOf(showCompletedDefaultValue) } + + if (taskList.isEmpty) { + // TODO SVG undraw.co illustration `files/undraw_to_do_list_re_9nt7.svg` + EmptyState( + icon = LucideIcons.CircleOff, + title = stringResource(Res.string.task_lists_screen_empty_list_title), + description = stringResource(Res.string.task_lists_screen_empty_list_desc), + modifier = Modifier + .fillMaxSize() + .testTag(FULLY_EMPTY_STATE), + ) + } else { + LazyColumn( + modifier = Modifier.testTag(TASKS_COLUMN), + contentPadding = PaddingValues(vertical = 8.dp), + verticalArrangement = Arrangement.spacedBy(8.dp), + ) { + if (taskList.isEmptyRemainingTasksVisible) { + item(key = "all_tasks_complete") { + EmptyState( + icon = LucideIcons.CheckCheck, + title = stringResource(Res.string.task_list_pane_all_tasks_complete_title), + description = stringResource(Res.string.task_list_pane_all_tasks_complete_desc), + modifier = Modifier + .fillMaxWidth() + .padding(vertical = 24.dp) + .testTag(ALL_COMPLETE_EMPTY_STATE), + ) + } + } + + taskList.remainingTasks.forEach { (dateRange, tasks) -> + if (dateRange != null) { + stickyHeader(key = dateRange.key) { + Box( + Modifier + .fillMaxWidth() + .background(MaterialTheme.colorScheme.background) + .padding(horizontal = 12.dp, vertical = 8.dp) + ) { + Text( + dateRange.toLabel(sectionLabel = true), + style = MaterialTheme.typography.titleSmall, + color = dateRange.toColor(), + ) + } + } + } + items(tasks, key = { it.id.value }) { task -> + RemainingTaskRow( + taskLists, + task, + // TODO could come from the UI mapper/UI state + showDate = when { + taskList.sorting == TaskListSorting.Manual -> true + taskList.sorting == TaskListSorting.Title -> true + dateRange is DateRange.Overdue -> true + else -> false + } + ) { action -> + when (action) { + TaskAction.ToggleCompletion -> onToggleCompletionState(task) + TaskAction.Edit -> onEditTask(task) + TaskAction.UpdateDueDate -> onUpdateDueDate(task) + TaskAction.AddSubTask -> onNewSubTask(task) + TaskAction.Unindent -> onUnindent(task) + TaskAction.Indent -> onIndent(task) + TaskAction.MoveToTop -> onMoveToTop(task) + is TaskAction.MoveToList -> onMoveToList(task, action.targetParentList) + TaskAction.MoveToNewList -> onMoveToNewList(task) + TaskAction.Delete -> onDeleteTask(task) + } + } + } + } + + if (taskList.hasCompletedTasks) { + stickyHeader(key = "completed") { + Box( + Modifier + .clip(MaterialTheme.shapes.large) + .fillMaxWidth() + .clickable { showCompleted = !showCompleted } + .background(MaterialTheme.colorScheme.background) + .padding(horizontal = 12.dp, vertical = 8.dp) + .testTag(COMPLETED_TASKS_TOGGLE) + ) { + RowWithIcon( + icon = { + when { + showCompleted -> Icon(LucideIcons.ChevronDown, null) + else -> Icon(LucideIcons.ChevronRight, null) + } + } + ) { + Text( + stringResource(Res.string.task_list_pane_completed_section_title_with_count, taskList.completedTasks.size), + modifier = Modifier.testTag(COMPLETED_TASKS_TOGGLE_LABEL), + style = MaterialTheme.typography.titleSmall + ) + } + } + } + } + + if (showCompleted) { + items(taskList.completedTasks, key = { it.id.value }) { task -> + CompletedTaskRow( + task, + onAction = { action -> + when (action) { + TaskAction.ToggleCompletion -> onToggleCompletionState(task) + TaskAction.Edit -> onEditTask(task) + TaskAction.UpdateDueDate -> onUpdateDueDate(task) + TaskAction.Delete -> onDeleteTask(task) + else -> Unit + } + }, + ) + } + } + } + } +} + +private val DateRange.key: String + get() = when (this) { + is DateRange.Overdue -> "overdue${numberOfDays}" + is DateRange.Today -> "today" + is DateRange.Later -> "later${numberOfDays}" + DateRange.None -> "none" + } + diff --git a/tasks-app-shared/src/commonMain/kotlin/net/opatry/tasks/app/ui/component/profileIcon.kt b/tasks-app-shared/src/commonMain/kotlin/net/opatry/tasks/app/ui/component/profileIcon.kt index 6176d97d..f46edda9 100644 --- a/tasks-app-shared/src/commonMain/kotlin/net/opatry/tasks/app/ui/component/profileIcon.kt +++ b/tasks-app-shared/src/commonMain/kotlin/net/opatry/tasks/app/ui/component/profileIcon.kt @@ -56,8 +56,8 @@ import androidx.compose.ui.window.Popup import androidx.lifecycle.compose.collectAsStateWithLifecycle import coil3.compose.AsyncImage import net.opatry.google.auth.GoogleAuthenticator -import net.opatry.tasks.app.ui.UserState -import net.opatry.tasks.app.ui.UserViewModel +import net.opatry.tasks.app.presentation.UserState +import net.opatry.tasks.app.presentation.UserViewModel import net.opatry.tasks.resources.Res import net.opatry.tasks.resources.profile_popup_no_email import net.opatry.tasks.resources.profile_popup_sign_explanation diff --git a/tasks-app-shared/src/commonMain/kotlin/net/opatry/tasks/app/ui/screen/taskListScreen.kt b/tasks-app-shared/src/commonMain/kotlin/net/opatry/tasks/app/ui/screen/taskListScreen.kt index 274dba4a..afdd9799 100644 --- a/tasks-app-shared/src/commonMain/kotlin/net/opatry/tasks/app/ui/screen/taskListScreen.kt +++ b/tasks-app-shared/src/commonMain/kotlin/net/opatry/tasks/app/ui/screen/taskListScreen.kt @@ -42,8 +42,8 @@ import androidx.compose.runtime.rememberCoroutineScope import androidx.compose.runtime.setValue import androidx.lifecycle.compose.collectAsStateWithLifecycle import kotlinx.coroutines.launch +import net.opatry.tasks.app.presentation.TaskListsViewModel import net.opatry.tasks.app.ui.TaskEvent -import net.opatry.tasks.app.ui.TaskListsViewModel import net.opatry.tasks.app.ui.asLabel import net.opatry.tasks.app.ui.component.BrokenListIndentationEmptyState import net.opatry.tasks.app.ui.component.LoadingPane diff --git a/tasks-app-shared/src/commonMain/kotlin/net/opatry/tasks/app/ui/screen/taskListsPane.kt b/tasks-app-shared/src/commonMain/kotlin/net/opatry/tasks/app/ui/screen/taskListsPane.kt index e592c0e6..1a127cd9 100644 --- a/tasks-app-shared/src/commonMain/kotlin/net/opatry/tasks/app/ui/screen/taskListsPane.kt +++ b/tasks-app-shared/src/commonMain/kotlin/net/opatry/tasks/app/ui/screen/taskListsPane.kt @@ -24,6 +24,7 @@ package net.opatry.tasks.app.ui.screen import CircleFadingPlus import LucideIcons +import androidx.annotation.VisibleForTesting import androidx.compose.animation.AnimatedVisibility import androidx.compose.foundation.background import androidx.compose.foundation.layout.Arrangement @@ -48,8 +49,8 @@ import androidx.compose.ui.semantics.selected import androidx.compose.ui.semantics.semantics import androidx.compose.ui.text.style.TextOverflow import androidx.compose.ui.unit.dp +import net.opatry.tasks.app.presentation.model.TaskListUIModel import net.opatry.tasks.app.ui.component.RowWithIcon -import net.opatry.tasks.app.ui.model.TaskListUIModel import net.opatry.tasks.app.ui.screen.TaskListsPaneTestTag.NEW_TASK_LIST_BUTTON import net.opatry.tasks.app.ui.screen.TaskListsPaneTestTag.TASK_LIST_ROW import net.opatry.tasks.resources.Res @@ -57,7 +58,8 @@ import net.opatry.tasks.resources.task_lists_screen_add_task_list_cta import org.jetbrains.compose.resources.stringResource -object TaskListsPaneTestTag { +@VisibleForTesting +internal object TaskListsPaneTestTag { const val NEW_TASK_LIST_BUTTON = "NEW_TASK_LIST_BUTTON" const val TASK_LIST_ROW = "TASK_LIST_ROW" } diff --git a/tasks-app-shared/src/commonMain/kotlin/net/opatry/tasks/app/ui/screen/tasksPane.kt b/tasks-app-shared/src/commonMain/kotlin/net/opatry/tasks/app/ui/screen/tasksPane.kt index 2c0650d9..bc4035d0 100644 --- a/tasks-app-shared/src/commonMain/kotlin/net/opatry/tasks/app/ui/screen/tasksPane.kt +++ b/tasks-app-shared/src/commonMain/kotlin/net/opatry/tasks/app/ui/screen/tasksPane.kt @@ -23,30 +23,20 @@ package net.opatry.tasks.app.ui.screen import CalendarDays -import CheckCheck -import ChevronDown -import ChevronRight -import CircleOff import ListPlus import LucideIcons import NotepadText import Plus import androidx.compose.animation.AnimatedVisibility -import androidx.compose.foundation.background -import androidx.compose.foundation.clickable import androidx.compose.foundation.layout.Arrangement import androidx.compose.foundation.layout.Box import androidx.compose.foundation.layout.Column -import androidx.compose.foundation.layout.PaddingValues import androidx.compose.foundation.layout.Row -import androidx.compose.foundation.layout.fillMaxSize import androidx.compose.foundation.layout.fillMaxWidth import androidx.compose.foundation.layout.imePadding import androidx.compose.foundation.layout.navigationBarsPadding import androidx.compose.foundation.layout.padding import androidx.compose.foundation.layout.statusBarsPadding -import androidx.compose.foundation.lazy.LazyColumn -import androidx.compose.foundation.lazy.items import androidx.compose.foundation.rememberScrollState import androidx.compose.foundation.text.KeyboardOptions import androidx.compose.foundation.verticalScroll @@ -84,8 +74,6 @@ import androidx.compose.runtime.remember import androidx.compose.runtime.setValue import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier -import androidx.compose.ui.draw.clip -import androidx.compose.ui.platform.testTag import androidx.compose.ui.text.input.ImeAction import androidx.compose.ui.text.input.KeyboardType import androidx.compose.ui.unit.dp @@ -94,24 +82,15 @@ import kotlinx.datetime.Instant import kotlinx.datetime.TimeZone import kotlinx.datetime.atStartOfDayIn import kotlinx.datetime.toLocalDateTime -import net.opatry.tasks.app.ui.TaskListsViewModel -import net.opatry.tasks.app.ui.component.CompletedTaskRow +import net.opatry.tasks.app.presentation.TaskListsViewModel +import net.opatry.tasks.app.presentation.model.TaskListUIModel +import net.opatry.tasks.app.presentation.model.TaskUIModel import net.opatry.tasks.app.ui.component.EditTextDialog -import net.opatry.tasks.app.ui.component.EmptyState -import net.opatry.tasks.app.ui.component.RemainingTaskRow -import net.opatry.tasks.app.ui.component.RowWithIcon -import net.opatry.tasks.app.ui.component.TaskAction import net.opatry.tasks.app.ui.component.TaskListEditMenuAction import net.opatry.tasks.app.ui.component.TaskListTopAppBar +import net.opatry.tasks.app.ui.component.TasksColumn import net.opatry.tasks.app.ui.component.toColor import net.opatry.tasks.app.ui.component.toLabel -import net.opatry.tasks.app.ui.model.DateRange -import net.opatry.tasks.app.ui.model.TaskListUIModel -import net.opatry.tasks.app.ui.model.TaskUIModel -import net.opatry.tasks.app.ui.screen.TaskListPaneTestTag.ALL_COMPLETE_EMPTY_STATE -import net.opatry.tasks.app.ui.screen.TaskListPaneTestTag.COMPLETED_TASKS_TOGGLE -import net.opatry.tasks.app.ui.screen.TaskListPaneTestTag.COMPLETED_TASKS_TOGGLE_LABEL -import net.opatry.tasks.data.TaskListSorting import net.opatry.tasks.resources.Res import net.opatry.tasks.resources.dialog_cancel import net.opatry.tasks.resources.task_due_date_update_cta @@ -126,12 +105,9 @@ import net.opatry.tasks.resources.task_editor_sheet_title_field_empty_error import net.opatry.tasks.resources.task_editor_sheet_title_field_label import net.opatry.tasks.resources.task_editor_sheet_title_field_placeholder import net.opatry.tasks.resources.task_editor_sheet_validate -import net.opatry.tasks.resources.task_list_pane_all_tasks_complete_desc -import net.opatry.tasks.resources.task_list_pane_all_tasks_complete_title import net.opatry.tasks.resources.task_list_pane_clear_completed_confirm_dialog_confirm import net.opatry.tasks.resources.task_list_pane_clear_completed_confirm_dialog_message import net.opatry.tasks.resources.task_list_pane_clear_completed_confirm_dialog_title -import net.opatry.tasks.resources.task_list_pane_completed_section_title_with_count import net.opatry.tasks.resources.task_list_pane_delete_list_confirm_dialog_confirm import net.opatry.tasks.resources.task_list_pane_delete_list_confirm_dialog_message import net.opatry.tasks.resources.task_list_pane_delete_list_confirm_dialog_title @@ -140,28 +116,10 @@ import net.opatry.tasks.resources.task_list_pane_rename_dialog_title import net.opatry.tasks.resources.task_list_pane_task_deleted_snackbar import net.opatry.tasks.resources.task_list_pane_task_deleted_undo_snackbar import net.opatry.tasks.resources.task_list_pane_task_restored_snackbar -import net.opatry.tasks.resources.task_lists_screen_empty_list_desc -import net.opatry.tasks.resources.task_lists_screen_empty_list_title import net.opatry.tasks.resources.task_menu_move_to_new_list_create_task_list_dialog_confirm import net.opatry.tasks.resources.task_menu_move_to_new_list_create_task_list_dialog_title import org.jetbrains.compose.resources.stringResource -object TaskListPaneTestTag { - const val ALL_COMPLETE_EMPTY_STATE = "ALL_COMPLETE_EMPTY_STATE" - const val REMAINING_TASK_ROW = "REMAINING_TASK_ROW" - const val REMAINING_TASK_ICON = "REMAINING_TASK_ICON" - const val REMAINING_TASK_NOTES = "REMAINING_TASK_NOTES" - const val REMAINING_TASK_DUE_DATE_CHIP = "REMAINING_TASK_DUE_DATE_CHIP" - const val REMAINING_TASK_MENU_ICON = "REMAINING_TASK_MENU_ICON" - const val COMPLETED_TASK_ROW = "COMPLETED_TASK_ROW" - const val COMPLETED_TASK_ICON = "COMPLETED_TASK_ICON" - const val COMPLETED_TASK_NOTES = "COMPLETED_TASK_NOTES" - const val COMPLETED_TASK_COMPLETION_DATE = "COMPLETED_TASK_COMPLETION_DATE" - const val COMPLETED_TASK_DELETE_ICON = "COMPLETED_TASK_DELETE_ICON" - const val COMPLETED_TASKS_TOGGLE = "COMPLETED_TASKS_TOGGLE" - const val COMPLETED_TASKS_TOGGLE_LABEL = "COMPLETED_TASKS_TOGGLE_LABEL" -} - @OptIn(ExperimentalMaterial3Api::class) @Composable fun TaskListDetail( @@ -241,46 +199,36 @@ fun TaskListDetail( } ) { innerPadding -> Box(Modifier.padding(innerPadding)) { - if (taskList.isEmpty) { - // TODO SVG undraw.co illustration `files/undraw_to_do_list_re_9nt7.svg` - EmptyState( - icon = LucideIcons.CircleOff, - title = stringResource(Res.string.task_lists_screen_empty_list_title), - description = stringResource(Res.string.task_lists_screen_empty_list_desc), - modifier = Modifier.fillMaxSize(), - ) - } else { - TasksColumn( - taskLists, - taskList, - onToggleCompletionState = { viewModel.toggleTaskCompletionState(it.id) }, - onEditTask = { - taskOfInterest = it - showEditTaskSheet = true - }, - onUpdateDueDate = { - taskOfInterest = it - showDatePickerDialog = true - }, - onNewSubTask = { - taskOfInterest = it - showNewSubTaskSheet = true - }, - onUnindent = { viewModel.unindentTask(it.id) }, - onIndent = { viewModel.indentTask(it.id) }, - onMoveToTop = { viewModel.moveToTop(it.id) }, - onMoveToList = { task, taskList -> viewModel.moveToList(task.id, taskList.id) }, - onMoveToNewList = { - taskOfInterest = it - showNewTaskListAlert = true - }, - onDeleteTask = { - taskOfInterest = it - showUndoTaskDeletionSnackbar = true - viewModel.deleteTask(it.id) - }, - ) - } + TasksColumn( + taskLists, + taskList, + onToggleCompletionState = { viewModel.toggleTaskCompletionState(it.id) }, + onEditTask = { + taskOfInterest = it + showEditTaskSheet = true + }, + onUpdateDueDate = { + taskOfInterest = it + showDatePickerDialog = true + }, + onNewSubTask = { + taskOfInterest = it + showNewSubTaskSheet = true + }, + onUnindent = { viewModel.unindentTask(it.id) }, + onIndent = { viewModel.indentTask(it.id) }, + onMoveToTop = { viewModel.moveToTop(it.id) }, + onMoveToList = { task, taskList -> viewModel.moveToList(task.id, taskList.id) }, + onMoveToNewList = { + taskOfInterest = it + showNewTaskListAlert = true + }, + onDeleteTask = { + taskOfInterest = it + showUndoTaskDeletionSnackbar = true + viewModel.deleteTask(it.id) + }, + ) } } @@ -589,140 +537,3 @@ fun TaskListDetail( ) } } - -@Composable -fun TasksColumn( - taskLists: List, - taskList: TaskListUIModel, - onToggleCompletionState: (TaskUIModel) -> Unit = {}, - onEditTask: (TaskUIModel) -> Unit = {}, - onUpdateDueDate: (TaskUIModel) -> Unit = {}, - onNewSubTask: (TaskUIModel) -> Unit = {}, - onUnindent: (TaskUIModel) -> Unit = {}, - onIndent: (TaskUIModel) -> Unit = {}, - onMoveToTop: (TaskUIModel) -> Unit = {}, - onMoveToList: (TaskUIModel, TaskListUIModel) -> Unit = { _, _ -> }, - onMoveToNewList: (TaskUIModel) -> Unit = {}, - onDeleteTask: (TaskUIModel) -> Unit = {}, -) { - var showCompleted by remember(taskList.id) { mutableStateOf(false) } - - LazyColumn( - contentPadding = PaddingValues(vertical = 8.dp), - verticalArrangement = Arrangement.spacedBy(8.dp) - ) { - if (taskList.isEmptyRemainingTasksVisible) { - item(key = "all_tasks_complete") { - EmptyState( - icon = LucideIcons.CheckCheck, - title = stringResource(Res.string.task_list_pane_all_tasks_complete_title), - description = stringResource(Res.string.task_list_pane_all_tasks_complete_desc), - modifier = Modifier - .fillMaxWidth() - .padding(vertical = 24.dp) - .testTag(ALL_COMPLETE_EMPTY_STATE), - ) - } - } - - taskList.remainingTasks.forEach { (dateRange, tasks) -> - if (dateRange != null) { - stickyHeader(key = dateRange.key) { - Box( - Modifier - .fillMaxWidth() - .background(MaterialTheme.colorScheme.background) - .padding(horizontal = 12.dp, vertical = 8.dp) - ) { - Text( - dateRange.toLabel(sectionLabel = true), - style = MaterialTheme.typography.titleSmall, - color = dateRange.toColor(), - ) - } - } - } - items(tasks, key = { it.id.value }) { task -> - RemainingTaskRow( - taskLists, - task, - // TODO could come from the UI mapper/UI state - showDate = when { - taskList.sorting == TaskListSorting.Manual -> true - taskList.sorting == TaskListSorting.Title -> true - dateRange is DateRange.Overdue -> true - else -> false - } - ) { action -> - when (action) { - TaskAction.ToggleCompletion -> onToggleCompletionState(task) - TaskAction.Edit -> onEditTask(task) - TaskAction.UpdateDueDate -> onUpdateDueDate(task) - TaskAction.AddSubTask -> onNewSubTask(task) - TaskAction.Unindent -> onUnindent(task) - TaskAction.Indent -> onIndent(task) - TaskAction.MoveToTop -> onMoveToTop(task) - is TaskAction.MoveToList -> onMoveToList(task, action.targetParentList) - TaskAction.MoveToNewList -> onMoveToNewList(task) - TaskAction.Delete -> onDeleteTask(task) - } - } - } - } - - if (taskList.hasCompletedTasks) { - stickyHeader(key = "completed") { - Box( - Modifier - .clip(MaterialTheme.shapes.large) - .fillMaxWidth() - .clickable { showCompleted = !showCompleted } - .background(MaterialTheme.colorScheme.background) - .padding(horizontal = 12.dp, vertical = 8.dp) - .testTag(COMPLETED_TASKS_TOGGLE) - ) { - RowWithIcon( - icon = { - when { - showCompleted -> Icon(LucideIcons.ChevronDown, null) - else -> Icon(LucideIcons.ChevronRight, null) - } - } - ) { - Text( - stringResource(Res.string.task_list_pane_completed_section_title_with_count, taskList.completedTasks.size), - modifier = Modifier.testTag(COMPLETED_TASKS_TOGGLE_LABEL), - style = MaterialTheme.typography.titleSmall - ) - } - } - } - } - - if (showCompleted) { - items(taskList.completedTasks, key = { it.id.value }) { task -> - CompletedTaskRow( - task, - onAction = { action -> - when (action) { - TaskAction.ToggleCompletion -> onToggleCompletionState(task) - TaskAction.Edit -> onEditTask(task) - TaskAction.UpdateDueDate -> onUpdateDueDate(task) - TaskAction.Delete -> onDeleteTask(task) - else -> Unit - } - }, - ) - } - } - } -} - -private val DateRange.key: String - get() = when (this) { - is DateRange.Overdue -> "overdue${numberOfDays}" - is DateRange.Today -> "today" - is DateRange.Later -> "later${numberOfDays}" - DateRange.None -> "none" - } - diff --git a/tasks-app-shared/src/commonTest/kotlin/net/opatry/tasks/ui/TaskListsViewModelTest.kt b/tasks-app-shared/src/commonTest/kotlin/net/opatry/tasks/presentation/TaskListsViewModelTest.kt similarity index 98% rename from tasks-app-shared/src/commonTest/kotlin/net/opatry/tasks/ui/TaskListsViewModelTest.kt rename to tasks-app-shared/src/commonTest/kotlin/net/opatry/tasks/presentation/TaskListsViewModelTest.kt index d426eaef..8e0038c7 100644 --- a/tasks-app-shared/src/commonTest/kotlin/net/opatry/tasks/ui/TaskListsViewModelTest.kt +++ b/tasks-app-shared/src/commonTest/kotlin/net/opatry/tasks/presentation/TaskListsViewModelTest.kt @@ -20,7 +20,7 @@ * OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. */ -package net.opatry.tasks.ui +package net.opatry.tasks.presentation import kotlinx.coroutines.ExperimentalCoroutinesApi import kotlinx.coroutines.flow.MutableSharedFlow @@ -43,13 +43,13 @@ import kotlinx.datetime.plus import kotlinx.datetime.toInstant import kotlinx.datetime.toLocalDateTime import net.opatry.Logger +import net.opatry.tasks.app.presentation.TaskListsViewModel +import net.opatry.tasks.app.presentation.model.DateRange +import net.opatry.tasks.app.presentation.model.TaskId +import net.opatry.tasks.app.presentation.model.TaskListId +import net.opatry.tasks.app.presentation.model.TaskListUIModel +import net.opatry.tasks.app.presentation.model.TaskUIModel import net.opatry.tasks.app.ui.TaskEvent -import net.opatry.tasks.app.ui.TaskListsViewModel -import net.opatry.tasks.app.ui.model.DateRange -import net.opatry.tasks.app.ui.model.TaskId -import net.opatry.tasks.app.ui.model.TaskListId -import net.opatry.tasks.app.ui.model.TaskListUIModel -import net.opatry.tasks.app.ui.model.TaskUIModel import net.opatry.tasks.data.TaskListSorting import net.opatry.tasks.data.TaskRepository import net.opatry.tasks.data.model.TaskDataModel diff --git a/tasks-app-shared/src/commonTest/kotlin/net/opatry/tasks/ui/UserViewModelTest.kt b/tasks-app-shared/src/commonTest/kotlin/net/opatry/tasks/presentation/UserViewModelTest.kt similarity index 98% rename from tasks-app-shared/src/commonTest/kotlin/net/opatry/tasks/ui/UserViewModelTest.kt rename to tasks-app-shared/src/commonTest/kotlin/net/opatry/tasks/presentation/UserViewModelTest.kt index 0a58802b..cb2d40c5 100644 --- a/tasks-app-shared/src/commonTest/kotlin/net/opatry/tasks/ui/UserViewModelTest.kt +++ b/tasks-app-shared/src/commonTest/kotlin/net/opatry/tasks/presentation/UserViewModelTest.kt @@ -20,7 +20,7 @@ * OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. */ -package net.opatry.tasks.ui +package net.opatry.tasks.presentation import kotlinx.coroutines.ExperimentalCoroutinesApi import kotlinx.coroutines.test.advanceUntilIdle @@ -31,8 +31,8 @@ import net.opatry.google.profile.UserInfoApi import net.opatry.google.profile.model.UserInfo import net.opatry.tasks.CredentialsStorage import net.opatry.tasks.TokenCache -import net.opatry.tasks.app.ui.UserState -import net.opatry.tasks.app.ui.UserViewModel +import net.opatry.tasks.app.presentation.UserState +import net.opatry.tasks.app.presentation.UserViewModel import net.opatry.tasks.data.UserDao import net.opatry.tasks.data.entity.UserEntity import net.opatry.test.MainDispatcherRule diff --git a/tasks-app-shared/src/commonTest/kotlin/net/opatry/tasks/ui/TaskUIModelMapperTest.kt b/tasks-app-shared/src/commonTest/kotlin/net/opatry/tasks/presentation/model/TaskUIModelMapperTest.kt similarity index 99% rename from tasks-app-shared/src/commonTest/kotlin/net/opatry/tasks/ui/TaskUIModelMapperTest.kt rename to tasks-app-shared/src/commonTest/kotlin/net/opatry/tasks/presentation/model/TaskUIModelMapperTest.kt index fca36d28..b18d3b2a 100644 --- a/tasks-app-shared/src/commonTest/kotlin/net/opatry/tasks/ui/TaskUIModelMapperTest.kt +++ b/tasks-app-shared/src/commonTest/kotlin/net/opatry/tasks/presentation/model/TaskUIModelMapperTest.kt @@ -20,7 +20,7 @@ * OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. */ -package net.opatry.tasks.ui +package net.opatry.tasks.presentation.model import kotlinx.datetime.Clock import kotlinx.datetime.Instant @@ -28,7 +28,7 @@ import kotlinx.datetime.LocalDate import kotlinx.datetime.LocalDateTime import kotlinx.datetime.TimeZone import kotlinx.datetime.toInstant -import net.opatry.tasks.app.ui.asTaskUIModel +import net.opatry.tasks.app.presentation.asTaskUIModel import net.opatry.tasks.data.model.TaskDataModel import kotlin.test.Test import kotlin.test.assertEquals diff --git a/tasks-app-shared/src/commonTest/kotlin/net/opatry/tasks/ui/component/CompletedTaskRowTest.kt b/tasks-app-shared/src/commonTest/kotlin/net/opatry/tasks/ui/component/CompletedTaskRowTest.kt index bd67927e..7c9d776f 100644 --- a/tasks-app-shared/src/commonTest/kotlin/net/opatry/tasks/ui/component/CompletedTaskRowTest.kt +++ b/tasks-app-shared/src/commonTest/kotlin/net/opatry/tasks/ui/component/CompletedTaskRowTest.kt @@ -31,12 +31,12 @@ import androidx.compose.ui.test.performClick import androidx.compose.ui.test.runComposeUiTest import kotlinx.datetime.LocalDate import net.opatry.tasks.app.ui.component.CompletedTaskRow +import net.opatry.tasks.app.ui.component.CompletedTaskRowTestTag.COMPLETED_TASK_COMPLETION_DATE +import net.opatry.tasks.app.ui.component.CompletedTaskRowTestTag.COMPLETED_TASK_DELETE_ICON +import net.opatry.tasks.app.ui.component.CompletedTaskRowTestTag.COMPLETED_TASK_ICON +import net.opatry.tasks.app.ui.component.CompletedTaskRowTestTag.COMPLETED_TASK_NOTES +import net.opatry.tasks.app.ui.component.CompletedTaskRowTestTag.COMPLETED_TASK_ROW import net.opatry.tasks.app.ui.component.TaskAction -import net.opatry.tasks.app.ui.screen.TaskListPaneTestTag.COMPLETED_TASK_COMPLETION_DATE -import net.opatry.tasks.app.ui.screen.TaskListPaneTestTag.COMPLETED_TASK_DELETE_ICON -import net.opatry.tasks.app.ui.screen.TaskListPaneTestTag.COMPLETED_TASK_ICON -import net.opatry.tasks.app.ui.screen.TaskListPaneTestTag.COMPLETED_TASK_NOTES -import net.opatry.tasks.app.ui.screen.TaskListPaneTestTag.COMPLETED_TASK_ROW import net.opatry.tasks.resources.Res import net.opatry.tasks.resources.task_list_pane_completed_date_label import net.opatry.tasks.ui.screen.createTask diff --git a/tasks-app-shared/src/commonTest/kotlin/net/opatry/tasks/ui/component/DateRangeColorTest.kt b/tasks-app-shared/src/commonTest/kotlin/net/opatry/tasks/ui/component/DateRangeColorTest.kt index e0ea944d..17a0b8ef 100644 --- a/tasks-app-shared/src/commonTest/kotlin/net/opatry/tasks/ui/component/DateRangeColorTest.kt +++ b/tasks-app-shared/src/commonTest/kotlin/net/opatry/tasks/ui/component/DateRangeColorTest.kt @@ -30,7 +30,7 @@ import kotlinx.datetime.DateTimeUnit import kotlinx.datetime.minus import kotlinx.datetime.plus import net.opatry.tasks.app.ui.component.toColor -import net.opatry.tasks.app.ui.model.DateRange +import net.opatry.tasks.app.presentation.model.DateRange import net.opatry.tasks.ui.screen.today import kotlin.test.Test import kotlin.test.assertEquals diff --git a/tasks-app-shared/src/commonTest/kotlin/net/opatry/tasks/ui/component/DateRangeLabelTest.kt b/tasks-app-shared/src/commonTest/kotlin/net/opatry/tasks/ui/component/DateRangeLabelTest.kt index 5015aca5..ba5d04dd 100644 --- a/tasks-app-shared/src/commonTest/kotlin/net/opatry/tasks/ui/component/DateRangeLabelTest.kt +++ b/tasks-app-shared/src/commonTest/kotlin/net/opatry/tasks/ui/component/DateRangeLabelTest.kt @@ -28,7 +28,7 @@ import kotlinx.datetime.DateTimeUnit import kotlinx.datetime.minus import kotlinx.datetime.plus import net.opatry.tasks.app.ui.component.toLabel -import net.opatry.tasks.app.ui.model.DateRange +import net.opatry.tasks.app.presentation.model.DateRange import net.opatry.tasks.resources.Res import net.opatry.tasks.resources.task_due_date_label_days_ago import net.opatry.tasks.resources.task_due_date_label_today diff --git a/tasks-app-shared/src/commonTest/kotlin/net/opatry/tasks/ui/component/RemainingTaskRowTest.kt b/tasks-app-shared/src/commonTest/kotlin/net/opatry/tasks/ui/component/RemainingTaskRowTest.kt index 0d956159..87860ca2 100644 --- a/tasks-app-shared/src/commonTest/kotlin/net/opatry/tasks/ui/component/RemainingTaskRowTest.kt +++ b/tasks-app-shared/src/commonTest/kotlin/net/opatry/tasks/ui/component/RemainingTaskRowTest.kt @@ -39,6 +39,11 @@ import androidx.compose.ui.test.runComposeUiTest import kotlinx.datetime.DateTimeUnit import kotlinx.datetime.minus import net.opatry.tasks.app.ui.component.RemainingTaskRow +import net.opatry.tasks.app.ui.component.RemainingTaskRowTestTag.REMAINING_TASK_DUE_DATE_CHIP +import net.opatry.tasks.app.ui.component.RemainingTaskRowTestTag.REMAINING_TASK_ICON +import net.opatry.tasks.app.ui.component.RemainingTaskRowTestTag.REMAINING_TASK_MENU_ICON +import net.opatry.tasks.app.ui.component.RemainingTaskRowTestTag.REMAINING_TASK_NOTES +import net.opatry.tasks.app.ui.component.RemainingTaskRowTestTag.REMAINING_TASK_ROW import net.opatry.tasks.app.ui.component.TaskAction import net.opatry.tasks.app.ui.component.TaskMenuTestTag.ADD_SUBTASK import net.opatry.tasks.app.ui.component.TaskMenuTestTag.DELETE @@ -48,11 +53,6 @@ import net.opatry.tasks.app.ui.component.TaskMenuTestTag.MOVE_TO_NEW_LIST import net.opatry.tasks.app.ui.component.TaskMenuTestTag.MOVE_TO_TOP import net.opatry.tasks.app.ui.component.TaskMenuTestTag.TASK_MENU import net.opatry.tasks.app.ui.component.TaskMenuTestTag.UNINDENT -import net.opatry.tasks.app.ui.screen.TaskListPaneTestTag.REMAINING_TASK_DUE_DATE_CHIP -import net.opatry.tasks.app.ui.screen.TaskListPaneTestTag.REMAINING_TASK_ICON -import net.opatry.tasks.app.ui.screen.TaskListPaneTestTag.REMAINING_TASK_MENU_ICON -import net.opatry.tasks.app.ui.screen.TaskListPaneTestTag.REMAINING_TASK_NOTES -import net.opatry.tasks.app.ui.screen.TaskListPaneTestTag.REMAINING_TASK_ROW import net.opatry.tasks.resources.Res import net.opatry.tasks.resources.task_due_date_label_weeks_ago import net.opatry.tasks.ui.screen.createTask diff --git a/tasks-app-shared/src/commonTest/kotlin/net/opatry/tasks/ui/screen/TasksColumnTest.kt b/tasks-app-shared/src/commonTest/kotlin/net/opatry/tasks/ui/component/TasksColumnTest.kt similarity index 77% rename from tasks-app-shared/src/commonTest/kotlin/net/opatry/tasks/ui/screen/TasksColumnTest.kt rename to tasks-app-shared/src/commonTest/kotlin/net/opatry/tasks/ui/component/TasksColumnTest.kt index 4727f873..ce868dba 100644 --- a/tasks-app-shared/src/commonTest/kotlin/net/opatry/tasks/ui/screen/TasksColumnTest.kt +++ b/tasks-app-shared/src/commonTest/kotlin/net/opatry/tasks/ui/component/TasksColumnTest.kt @@ -20,7 +20,7 @@ * OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. */ -package net.opatry.tasks.ui.screen +package net.opatry.tasks.ui.component import androidx.compose.ui.test.ExperimentalTestApi import androidx.compose.ui.test.assertCountEquals @@ -30,14 +30,17 @@ import androidx.compose.ui.test.onAllNodesWithTag import androidx.compose.ui.test.onNodeWithTag import androidx.compose.ui.test.performClick import androidx.compose.ui.test.runComposeUiTest -import net.opatry.tasks.app.ui.screen.TaskListPaneTestTag.ALL_COMPLETE_EMPTY_STATE -import net.opatry.tasks.app.ui.screen.TaskListPaneTestTag.COMPLETED_TASKS_TOGGLE -import net.opatry.tasks.app.ui.screen.TaskListPaneTestTag.COMPLETED_TASKS_TOGGLE_LABEL -import net.opatry.tasks.app.ui.screen.TaskListPaneTestTag.COMPLETED_TASK_ROW -import net.opatry.tasks.app.ui.screen.TaskListPaneTestTag.REMAINING_TASK_ROW -import net.opatry.tasks.app.ui.screen.TasksColumn +import net.opatry.tasks.app.ui.component.CompletedTaskRowTestTag.COMPLETED_TASK_ROW +import net.opatry.tasks.app.ui.component.RemainingTaskRowTestTag.REMAINING_TASK_ROW +import net.opatry.tasks.app.ui.component.TasksColumn +import net.opatry.tasks.app.ui.component.TasksColumnTestTag.ALL_COMPLETE_EMPTY_STATE +import net.opatry.tasks.app.ui.component.TasksColumnTestTag.COMPLETED_TASKS_TOGGLE +import net.opatry.tasks.app.ui.component.TasksColumnTestTag.COMPLETED_TASKS_TOGGLE_LABEL +import net.opatry.tasks.app.ui.component.TasksColumnTestTag.FULLY_EMPTY_STATE +import net.opatry.tasks.app.ui.component.TasksColumnTestTag.TASKS_COLUMN import net.opatry.tasks.resources.Res import net.opatry.tasks.resources.task_list_pane_completed_section_title_with_count +import net.opatry.tasks.ui.screen.createTaskList import org.jetbrains.compose.resources.stringResource import kotlin.test.Test @@ -61,6 +64,23 @@ class TasksColumnTest { .assertIsDisplayed() } + @Test + fun TasksColumn_FullyEmptyState() = runComposeUiTest { + val taskList = createTaskList(remainingTaskCount = 0, completedTaskCount = 0) + setContent { + TasksColumn( + taskLists = listOf(taskList), + taskList = taskList, + ) + } + + onNodeWithTag(FULLY_EMPTY_STATE) + .assertIsDisplayed() + + onNodeWithTag(TASKS_COLUMN) + .assertDoesNotExist() + } + @Test fun TasksColumn_AllCompletedEmptyState() = runComposeUiTest { val taskList = createTaskList(remainingTaskCount = 0, completedTaskCount = 1) @@ -71,6 +91,9 @@ class TasksColumnTest { ) } + onNodeWithTag(FULLY_EMPTY_STATE) + .assertDoesNotExist() + onNodeWithTag(ALL_COMPLETE_EMPTY_STATE) .assertIsDisplayed() diff --git a/tasks-app-shared/src/commonTest/kotlin/net/opatry/tasks/ui/screen/TaskListFactory.kt b/tasks-app-shared/src/commonTest/kotlin/net/opatry/tasks/ui/screen/TaskListFactory.kt index 7aa0c60b..637e867d 100644 --- a/tasks-app-shared/src/commonTest/kotlin/net/opatry/tasks/ui/screen/TaskListFactory.kt +++ b/tasks-app-shared/src/commonTest/kotlin/net/opatry/tasks/ui/screen/TaskListFactory.kt @@ -26,10 +26,10 @@ import kotlinx.datetime.Clock import kotlinx.datetime.LocalDate import kotlinx.datetime.TimeZone import kotlinx.datetime.todayIn -import net.opatry.tasks.app.ui.model.TaskId -import net.opatry.tasks.app.ui.model.TaskListId -import net.opatry.tasks.app.ui.model.TaskListUIModel -import net.opatry.tasks.app.ui.model.TaskUIModel +import net.opatry.tasks.app.presentation.model.TaskId +import net.opatry.tasks.app.presentation.model.TaskListId +import net.opatry.tasks.app.presentation.model.TaskListUIModel +import net.opatry.tasks.app.presentation.model.TaskUIModel val today: LocalDate diff --git a/tasks-app-shared/src/commonTest/kotlin/net/opatry/tasks/ui/screen/TaskListsColumnTest.kt b/tasks-app-shared/src/commonTest/kotlin/net/opatry/tasks/ui/screen/TaskListsColumnTest.kt index b8a558af..1ae174a7 100644 --- a/tasks-app-shared/src/commonTest/kotlin/net/opatry/tasks/ui/screen/TaskListsColumnTest.kt +++ b/tasks-app-shared/src/commonTest/kotlin/net/opatry/tasks/ui/screen/TaskListsColumnTest.kt @@ -34,8 +34,8 @@ import androidx.compose.ui.test.onLast import androidx.compose.ui.test.onNodeWithTag import androidx.compose.ui.test.performClick import androidx.compose.ui.test.runComposeUiTest -import net.opatry.tasks.app.ui.model.TaskListId -import net.opatry.tasks.app.ui.model.TaskListUIModel +import net.opatry.tasks.app.presentation.model.TaskListId +import net.opatry.tasks.app.presentation.model.TaskListUIModel import net.opatry.tasks.app.ui.screen.TaskListsColumn import net.opatry.tasks.app.ui.screen.TaskListsPaneTestTag.NEW_TASK_LIST_BUTTON import net.opatry.tasks.app.ui.screen.TaskListsPaneTestTag.TASK_LIST_ROW