From d8569c7aedb6dc95d19a01ea5ae18cb015cd3fa6 Mon Sep 17 00:00:00 2001 From: Trey T Date: Sat, 18 Apr 2026 17:50:27 -0500 Subject: [PATCH] Audit: PullToRefreshBox on remaining list screens (iOS parity) HomeScreen + AllTasksScreen + TasksScreen now support pull-to-refresh. forceRefresh=true per CLAUDE.md mutation pattern. Co-Authored-By: Claude Opus 4.7 (1M context) --- .../tt/honeyDue/ui/screens/AllTasksScreen.kt | 108 ++++++++++-------- .../com/tt/honeyDue/ui/screens/HomeScreen.kt | 25 +++- .../com/tt/honeyDue/ui/screens/TasksScreen.kt | 20 +++- 3 files changed, 105 insertions(+), 48 deletions(-) diff --git a/composeApp/src/commonMain/kotlin/com/tt/honeyDue/ui/screens/AllTasksScreen.kt b/composeApp/src/commonMain/kotlin/com/tt/honeyDue/ui/screens/AllTasksScreen.kt index ba67472..3d0137c 100644 --- a/composeApp/src/commonMain/kotlin/com/tt/honeyDue/ui/screens/AllTasksScreen.kt +++ b/composeApp/src/commonMain/kotlin/com/tt/honeyDue/ui/screens/AllTasksScreen.kt @@ -7,6 +7,7 @@ import androidx.compose.foundation.lazy.items import androidx.compose.material.icons.Icons import androidx.compose.material.icons.filled.* import androidx.compose.material3.* +import androidx.compose.material3.pulltorefresh.PullToRefreshBox import androidx.compose.runtime.* import androidx.compose.ui.Modifier import androidx.compose.ui.text.font.FontWeight @@ -49,6 +50,7 @@ fun AllTasksScreen( var showCompleteDialog by remember { mutableStateOf(false) } var showNewTaskDialog by remember { mutableStateOf(false) } var selectedTask by remember { mutableStateOf(null) } + var isRefreshing by remember { mutableStateOf(false) } // Track which column to scroll to (from push notification navigation) var scrollToColumnIndex by remember { mutableStateOf(null) } @@ -58,6 +60,13 @@ fun AllTasksScreen( residenceViewModel.loadMyResidences() } + // Reset pull-to-refresh state once tasks finish loading + LaunchedEffect(tasksState) { + if (tasksState !is ApiResult.Loading) { + isRefreshing = false + } + } + // When tasks load and we have a pending navigation, find the column containing the task LaunchedEffect(navigateToTaskId, tasksState) { if (navigateToTaskId != null && tasksState is ApiResult.Success) { @@ -206,62 +215,71 @@ fun AllTasksScreen( } } } else { - DynamicTaskKanbanView( - columns = taskData.columns, - onCompleteTask = { task -> - if (onNavigateToCompleteTask != null) { - // Use full-screen navigation - val residenceName = (myResidencesState as? ApiResult.Success) - ?.data?.residences?.find { it.id == task.residenceId }?.name ?: "" - onNavigateToCompleteTask(task, residenceName) - } else { - // Fall back to dialog - selectedTask = task - showCompleteDialog = true - } + PullToRefreshBox( + isRefreshing = isRefreshing, + onRefresh = { + isRefreshing = true + viewModel.loadTasks(forceRefresh = true) }, - onEditTask = { task -> - onNavigateToEditTask(task) - }, - onCancelTask = { task -> + modifier = Modifier.fillMaxSize() + ) { + DynamicTaskKanbanView( + columns = taskData.columns, + onCompleteTask = { task -> + if (onNavigateToCompleteTask != null) { + // Use full-screen navigation + val residenceName = (myResidencesState as? ApiResult.Success) + ?.data?.residences?.find { it.id == task.residenceId }?.name ?: "" + onNavigateToCompleteTask(task, residenceName) + } else { + // Fall back to dialog + selectedTask = task + showCompleteDialog = true + } + }, + onEditTask = { task -> + onNavigateToEditTask(task) + }, + onCancelTask = { task -> // viewModel.cancelTask(task.id) { _ -> // viewModel.loadTasks() // } - }, - onUncancelTask = { task -> + }, + onUncancelTask = { task -> // viewModel.uncancelTask(task.id) { _ -> // viewModel.loadTasks() // } - }, - onMarkInProgress = { task -> - viewModel.markInProgress(task.id) { success -> - if (success) { - viewModel.loadTasks() + }, + onMarkInProgress = { task -> + viewModel.markInProgress(task.id) { success -> + if (success) { + viewModel.loadTasks() + } } - } - }, - onArchiveTask = { task -> - viewModel.archiveTask(task.id) { success -> - if (success) { - viewModel.loadTasks() + }, + onArchiveTask = { task -> + viewModel.archiveTask(task.id) { success -> + if (success) { + viewModel.loadTasks() + } } - } - }, - onUnarchiveTask = { task -> - viewModel.unarchiveTask(task.id) { success -> - if (success) { - viewModel.loadTasks() + }, + onUnarchiveTask = { task -> + viewModel.unarchiveTask(task.id) { success -> + if (success) { + viewModel.loadTasks() + } } + }, + modifier = Modifier, + bottomPadding = bottomNavBarPadding, + scrollToColumnIndex = scrollToColumnIndex, + onScrollComplete = { + scrollToColumnIndex = null + onClearNavigateToTask() } - }, - modifier = Modifier, - bottomPadding = bottomNavBarPadding, - scrollToColumnIndex = scrollToColumnIndex, - onScrollComplete = { - scrollToColumnIndex = null - onClearNavigateToTask() - } - ) + ) + } } } } diff --git a/composeApp/src/commonMain/kotlin/com/tt/honeyDue/ui/screens/HomeScreen.kt b/composeApp/src/commonMain/kotlin/com/tt/honeyDue/ui/screens/HomeScreen.kt index 7c0e402..de27b33 100644 --- a/composeApp/src/commonMain/kotlin/com/tt/honeyDue/ui/screens/HomeScreen.kt +++ b/composeApp/src/commonMain/kotlin/com/tt/honeyDue/ui/screens/HomeScreen.kt @@ -2,9 +2,12 @@ package com.tt.honeyDue.ui.screens import androidx.compose.foundation.clickable import androidx.compose.foundation.layout.* +import androidx.compose.foundation.rememberScrollState +import androidx.compose.foundation.verticalScroll import androidx.compose.material.icons.Icons import androidx.compose.material.icons.filled.* import androidx.compose.material3.* +import androidx.compose.material3.pulltorefresh.PullToRefreshBox import androidx.compose.runtime.* import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier @@ -32,6 +35,7 @@ fun HomeScreen( ) { val summaryState by viewModel.myResidencesState.collectAsStateWithLifecycle() val totalSummary by DataManager.totalSummary.collectAsStateWithLifecycle() + var isRefreshing by remember { mutableStateOf(false) } LaunchedEffect(Unit) { viewModel.loadMyResidences() @@ -39,6 +43,13 @@ fun HomeScreen( taskViewModel.loadTasks() } + // Reset refresh state once the underlying load completes + LaunchedEffect(summaryState) { + if (summaryState !is ApiResult.Loading) { + isRefreshing = false + } + } + // Handle errors for loading summary summaryState.HandleErrors( onRetry = { viewModel.loadMyResidences() }, @@ -65,10 +76,21 @@ fun HomeScreen( } ) { paddingValues -> WarmGradientBackground { - Column( + PullToRefreshBox( + isRefreshing = isRefreshing, + onRefresh = { + isRefreshing = true + viewModel.loadMyResidences(forceRefresh = true) + taskViewModel.loadTasks(forceRefresh = true) + }, modifier = Modifier .fillMaxSize() .padding(paddingValues) + ) { + Column( + modifier = Modifier + .fillMaxSize() + .verticalScroll(rememberScrollState()) .padding(horizontal = OrganicSpacing.comfortable, vertical = OrganicSpacing.cozy), verticalArrangement = Arrangement.spacedBy(OrganicSpacing.generous) ) { @@ -199,6 +221,7 @@ fun HomeScreen( onClick = onNavigateToTasks ) } + } } } } diff --git a/composeApp/src/commonMain/kotlin/com/tt/honeyDue/ui/screens/TasksScreen.kt b/composeApp/src/commonMain/kotlin/com/tt/honeyDue/ui/screens/TasksScreen.kt index 4a0983a..1e0acb0 100644 --- a/composeApp/src/commonMain/kotlin/com/tt/honeyDue/ui/screens/TasksScreen.kt +++ b/composeApp/src/commonMain/kotlin/com/tt/honeyDue/ui/screens/TasksScreen.kt @@ -8,6 +8,7 @@ import androidx.compose.material.icons.Icons import androidx.compose.material.icons.filled.* import androidx.compose.material.icons.automirrored.filled.ArrowBack import androidx.compose.material3.* +import androidx.compose.material3.pulltorefresh.PullToRefreshBox import androidx.compose.runtime.* import androidx.compose.ui.Modifier import androidx.compose.ui.unit.dp @@ -43,6 +44,7 @@ fun TasksScreen( var selectedTask by remember { mutableStateOf(null) } var showErrorDialog by remember { mutableStateOf(false) } var errorMessage by remember { mutableStateOf("") } + var isRefreshing by remember { mutableStateOf(false) } // Show error dialog when tasks fail to load LaunchedEffect(tasksState) { @@ -50,6 +52,9 @@ fun TasksScreen( errorMessage = com.tt.honeyDue.util.ErrorMessageParser.parse((tasksState as ApiResult.Error).message) showErrorDialog = true } + if (tasksState !is ApiResult.Loading) { + isRefreshing = false + } } LaunchedEffect(Unit) { @@ -138,12 +143,22 @@ fun TasksScreen( } } } else { + PullToRefreshBox( + isRefreshing = isRefreshing, + onRefresh = { + isRefreshing = true + viewModel.loadTasks(forceRefresh = true) + }, + modifier = Modifier + .fillMaxSize() + .padding(paddingValues) + ) { LazyColumn( modifier = Modifier .fillMaxSize(), contentPadding = PaddingValues( - top = paddingValues.calculateTopPadding() + OrganicSpacing.cozy, - bottom = paddingValues.calculateBottomPadding() + OrganicSpacing.cozy, + top = OrganicSpacing.cozy, + bottom = OrganicSpacing.cozy, start = OrganicSpacing.cozy, end = OrganicSpacing.cozy ), @@ -261,6 +276,7 @@ fun TasksScreen( } } } + } } }