Tweaked passes list to show DeepSpace ones at the top

This commit is contained in:
Arty Bishop committed 2026-05-04 15:36:42 +01:00
1 parent dde22faf46
commit 8cc4ef5610
2 files changed
+30 -59

No files matched your search

@@ -29,11 +29,8 @@ import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.layout.size import androidx.compose.foundation.layout.size
import androidx.compose.foundation.layout.width import androidx.compose.foundation.layout.width
import androidx.compose.foundation.lazy.grid.GridCells 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.LazyVerticalGrid
import androidx.compose.foundation.lazy.grid.items import androidx.compose.foundation.lazy.grid.items
import androidx.compose.foundation.lazy.grid.rememberLazyGridState
import androidx.compose.material3.ElevatedCard import androidx.compose.material3.ElevatedCard
import androidx.compose.material3.ExperimentalMaterial3Api import androidx.compose.material3.ExperimentalMaterial3Api
import androidx.compose.material3.HorizontalDivider 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.PullToRefreshDefaults
import androidx.compose.material3.pulltorefresh.rememberPullToRefreshState import androidx.compose.material3.pulltorefresh.rememberPullToRefreshState
import androidx.compose.runtime.Composable import androidx.compose.runtime.Composable
import androidx.compose.runtime.derivedStateOf
import androidx.compose.runtime.getValue
import androidx.compose.runtime.remember import androidx.compose.runtime.remember
import androidx.compose.ui.Alignment import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier import androidx.compose.ui.Modifier
@@ -125,7 +120,6 @@ private fun PassesScreen(
onAction(PassesAction.DismissWhatsNew) onAction(PassesAction.DismissWhatsNew)
} }
} }
val gridState = rememberLazyGridState()
ScreenColumn( ScreenColumn(
topBar = { isVerticalLayout -> topBar = { isVerticalLayout ->
TopBar( TopBar(
@@ -151,7 +145,6 @@ private fun PassesScreen(
passes = uiState.itemsList, passes = uiState.itemsList,
navigateToRadar = navigateToRadar, navigateToRadar = navigateToRadar,
refreshPasses = { onAction(PassesAction.RefreshPasses) }, refreshPasses = { onAction(PassesAction.RefreshPasses) },
gridState = gridState,
sunTimes = uiState.sunTimes sunTimes = uiState.sunTimes
) )
} }
@@ -165,7 +158,6 @@ private fun PassesList(
passes: List<OrbitalPass>, passes: List<OrbitalPass>,
navigateToRadar: (Int, Long) -> Unit, navigateToRadar: (Int, Long) -> Unit,
refreshPasses: () -> Unit, refreshPasses: () -> Unit,
gridState: LazyGridState,
sunTimes: Map<String, Pair<String, String>> sunTimes: Map<String, Pair<String, String>>
) { ) {
val isVerticalLayout = isVerticalLayout() val isVerticalLayout = isVerticalLayout()
@@ -175,31 +167,15 @@ private fun PassesList(
SimpleDateFormat("EEE, dd MMM yyyy", Locale.ENGLISH).also { it.timeZone = timeZone } 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) { val groupedPasses = remember(passes, isUtc) {
passes.filter { !it.isDeepSpace }.groupBy { sdfDate.format(Date(it.aosTime)) } + val ordered = LinkedHashMap<String, List<OrbitalPass>>()
if (passes.any { it.isDeepSpace }) mapOf("Deep Space" to passes.filter { it.isDeepSpace }) else emptyMap() val deepSpace = passes.filter { it.isDeepSpace }
} if (deepSpace.isNotEmpty()) ordered["Deep Space"] = deepSpace
passes.filter { !it.isDeepSpace }
// Derive the sticky header label from the first visible item index .groupByTo(LinkedHashMap()) { sdfDate.format(Date(it.aosTime)) }
val stickyHeader by remember(gridState, groupedPasses) { .forEach { (k, v) -> ordered[k] = v }
derivedStateOf { ordered
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 }
} }
ElevatedCard(modifier = Modifier.fillMaxSize()) { ElevatedCard(modifier = Modifier.fillMaxSize()) {
@@ -220,32 +196,20 @@ private fun PassesList(
if (passes.isEmpty()) { if (passes.isEmpty()) {
EmptyListCard(message = stringResource(R.string.pass_empty_list_message)) EmptyListCard(message = stringResource(R.string.pass_empty_list_message))
} else { } else {
Column { LazyVerticalGrid(columns = GridCells.Adaptive(320.dp), modifier = Modifier.fillMaxSize()) {
// Sticky header overlay — only visible once the in-list header scrolls away for ((dateLabel, dayPasses) in groupedPasses) {
if (showStickyOverlay) { stickyHeader(key = "header_$dateLabel") {
val (rise, set) = sunTimes[stickyHeader] ?: ("--:--" to "--:--") val (rise, set) = sunTimes[dateLabel] ?: ("--:--" to "--:--")
StickyDateHeader(label = stickyHeader, sunriseTime = rise, sunsetTime = set) StickyDateHeader(label = dateLabel, sunriseTime = rise, sunsetTime = set)
} }
LazyVerticalGrid( items(items = dayPasses, key = { item -> item.catNum + item.aosTime }) { pass ->
state = gridState, PassItem(
columns = GridCells.Adaptive(320.dp), pass = pass,
modifier = Modifier.fillMaxSize() navigateToRadar = navigateToRadar,
) { modifier = Modifier.animateItem(),
for ((dateLabel, dayPasses) in groupedPasses) { isVerticalLayout = isVerticalLayout,
// In-list section header (scrolls away, drives the sticky overlay) isUtc = isUtc
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
)
}
} }
} }
} }
@@ -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<OrbitalPass>, isUtc: Boolean): Map<String, Pair<String, String>> { private fun computeSunTimes(passes: List<OrbitalPass>, isUtc: Boolean): Map<String, Pair<String, String>> {
val stationPos = settingsRepo.stationPosition.value val stationPos = settingsRepo.stationPosition.value
val tz = if (isUtc) TimeZone.getTimeZone("UTC") else TimeZone.getDefault() val tz = if (isUtc) TimeZone.getTimeZone("UTC") else TimeZone.getDefault()
val sdfDate = SimpleDateFormat("EEE, dd MMM yyyy", Locale.ENGLISH).also { it.timeZone = tz } 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 sdfTime = SimpleDateFormat("HH:mm", Locale.ENGLISH).also { it.timeZone = tz }
val result = LinkedHashMap<String, Pair<String, String>>() val result = LinkedHashMap<String, Pair<String, String>>()
// 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) { for (pass in passes) {
if (pass.isDeepSpace) continue if (pass.isDeepSpace) continue
val label = sdfDate.format(Date(pass.aosTime)) val label = sdfDate.format(Date(pass.aosTime))