diff --git a/feature/passes/src/main/java/com/rtbishop/look4sat/feature/passes/PassesScreen.kt b/feature/passes/src/main/java/com/rtbishop/look4sat/feature/passes/PassesScreen.kt index 1c909f89..fe5eafb0 100644 --- a/feature/passes/src/main/java/com/rtbishop/look4sat/feature/passes/PassesScreen.kt +++ b/feature/passes/src/main/java/com/rtbishop/look4sat/feature/passes/PassesScreen.kt @@ -29,11 +29,8 @@ import androidx.compose.foundation.layout.padding import androidx.compose.foundation.layout.size import androidx.compose.foundation.layout.width import androidx.compose.foundation.lazy.grid.GridCells -import androidx.compose.foundation.lazy.grid.GridItemSpan -import androidx.compose.foundation.lazy.grid.LazyGridState import androidx.compose.foundation.lazy.grid.LazyVerticalGrid import androidx.compose.foundation.lazy.grid.items -import androidx.compose.foundation.lazy.grid.rememberLazyGridState import androidx.compose.material3.ElevatedCard import androidx.compose.material3.ExperimentalMaterial3Api import androidx.compose.material3.HorizontalDivider @@ -45,8 +42,6 @@ import androidx.compose.material3.pulltorefresh.PullToRefreshBox import androidx.compose.material3.pulltorefresh.PullToRefreshDefaults import androidx.compose.material3.pulltorefresh.rememberPullToRefreshState import androidx.compose.runtime.Composable -import androidx.compose.runtime.derivedStateOf -import androidx.compose.runtime.getValue import androidx.compose.runtime.remember import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier @@ -125,7 +120,6 @@ private fun PassesScreen( onAction(PassesAction.DismissWhatsNew) } } - val gridState = rememberLazyGridState() ScreenColumn( topBar = { isVerticalLayout -> TopBar( @@ -151,7 +145,6 @@ private fun PassesScreen( passes = uiState.itemsList, navigateToRadar = navigateToRadar, refreshPasses = { onAction(PassesAction.RefreshPasses) }, - gridState = gridState, sunTimes = uiState.sunTimes ) } @@ -165,7 +158,6 @@ private fun PassesList( passes: List, navigateToRadar: (Int, Long) -> Unit, refreshPasses: () -> Unit, - gridState: LazyGridState, sunTimes: Map> ) { val isVerticalLayout = isVerticalLayout() @@ -175,31 +167,15 @@ private fun PassesList( SimpleDateFormat("EEE, dd MMM yyyy", Locale.ENGLISH).also { it.timeZone = timeZone } } - // Group passes by calendar day so we know where headers go + // Deep Space first, then date-grouped timed passes val groupedPasses = remember(passes, isUtc) { - passes.filter { !it.isDeepSpace }.groupBy { sdfDate.format(Date(it.aosTime)) } + - if (passes.any { it.isDeepSpace }) mapOf("Deep Space" to passes.filter { it.isDeepSpace }) else emptyMap() - } - - // Derive the sticky header label from the first visible item index - val stickyHeader by remember(gridState, groupedPasses) { - derivedStateOf { - val firstVisible = gridState.firstVisibleItemIndex - var itemIndex = 0 - var header = groupedPasses.keys.firstOrNull() ?: "" - for ((key, items) in groupedPasses) { - header = key - itemIndex += 1 + items.size - if (itemIndex > firstVisible) break - } - header - } - } - - // The sticky overlay should only show when the first in-list header has scrolled out of view. - // firstVisibleItemIndex == 0 means the first header is still visible — hide the overlay. - val showStickyOverlay by remember(gridState) { - derivedStateOf { gridState.firstVisibleItemIndex > 0 || gridState.firstVisibleItemScrollOffset > 0 } + val ordered = LinkedHashMap>() + val deepSpace = passes.filter { it.isDeepSpace } + if (deepSpace.isNotEmpty()) ordered["Deep Space"] = deepSpace + passes.filter { !it.isDeepSpace } + .groupByTo(LinkedHashMap()) { sdfDate.format(Date(it.aosTime)) } + .forEach { (k, v) -> ordered[k] = v } + ordered } ElevatedCard(modifier = Modifier.fillMaxSize()) { @@ -220,32 +196,20 @@ private fun PassesList( if (passes.isEmpty()) { EmptyListCard(message = stringResource(R.string.pass_empty_list_message)) } else { - Column { - // Sticky header overlay — only visible once the in-list header scrolls away - if (showStickyOverlay) { - val (rise, set) = sunTimes[stickyHeader] ?: ("--:--" to "--:--") - StickyDateHeader(label = stickyHeader, sunriseTime = rise, sunsetTime = set) - } - LazyVerticalGrid( - state = gridState, - columns = GridCells.Adaptive(320.dp), - modifier = Modifier.fillMaxSize() - ) { - for ((dateLabel, dayPasses) in groupedPasses) { - // In-list section header (scrolls away, drives the sticky overlay) - item(span = { GridItemSpan(maxLineSpan) }) { - val (rise, set) = sunTimes[dateLabel] ?: ("--:--" to "--:--") - StickyDateHeader(label = dateLabel, sunriseTime = rise, sunsetTime = set) - } - items(items = dayPasses, key = { item -> item.catNum + item.aosTime }) { pass -> - PassItem( - pass = pass, - navigateToRadar = navigateToRadar, - modifier = Modifier.animateItem(), - isVerticalLayout = isVerticalLayout, - isUtc = isUtc - ) - } + LazyVerticalGrid(columns = GridCells.Adaptive(320.dp), modifier = Modifier.fillMaxSize()) { + for ((dateLabel, dayPasses) in groupedPasses) { + stickyHeader(key = "header_$dateLabel") { + val (rise, set) = sunTimes[dateLabel] ?: ("--:--" to "--:--") + StickyDateHeader(label = dateLabel, sunriseTime = rise, sunsetTime = set) + } + items(items = dayPasses, key = { item -> item.catNum + item.aosTime }) { pass -> + PassItem( + pass = pass, + navigateToRadar = navigateToRadar, + modifier = Modifier.animateItem(), + isVerticalLayout = isVerticalLayout, + isUtc = isUtc + ) } } } diff --git a/feature/passes/src/main/java/com/rtbishop/look4sat/feature/passes/PassesViewModel.kt b/feature/passes/src/main/java/com/rtbishop/look4sat/feature/passes/PassesViewModel.kt index 99c2a65a..d1fb9ac0 100644 --- a/feature/passes/src/main/java/com/rtbishop/look4sat/feature/passes/PassesViewModel.kt +++ b/feature/passes/src/main/java/com/rtbishop/look4sat/feature/passes/PassesViewModel.kt @@ -124,13 +124,20 @@ class PassesViewModel( } } - /** Computes sunrise/sunset strings for each unique calendar day in the pass list. */ + // Computes sunrise/sunset strings for each unique calendar day in the pass list, plus today for Deep Space private fun computeSunTimes(passes: List, isUtc: Boolean): Map> { val stationPos = settingsRepo.stationPosition.value val tz = if (isUtc) TimeZone.getTimeZone("UTC") else TimeZone.getDefault() val sdfDate = SimpleDateFormat("EEE, dd MMM yyyy", Locale.ENGLISH).also { it.timeZone = tz } val sdfTime = SimpleDateFormat("HH:mm", Locale.ENGLISH).also { it.timeZone = tz } val result = LinkedHashMap>() + // Deep Space group always shows today's sun times + if (passes.any { it.isDeepSpace }) { + val riseSet = CelestialComputer.findSunRiseSet(stationPos, System.currentTimeMillis()) + val rise = if (riseSet.riseTimeMillis > 0) sdfTime.format(Date(riseSet.riseTimeMillis)) else "--:--" + val set = if (riseSet.setTimeMillis > 0) sdfTime.format(Date(riseSet.setTimeMillis)) else "--:--" + result["Deep Space"] = rise to set + } for (pass in passes) { if (pass.isDeepSpace) continue val label = sdfDate.format(Date(pass.aosTime))