Tweaked NextPassRow component to include AOS/LOS timer

This commit is contained in:
Arty Bishop committed 2026-10-11 12:51:22 +01:00
1 parent fa7b8cefe7
commit e335b43c77
8 files changed
+135 -153

No files matched your search

@@ -58,6 +58,7 @@ import androidx.compose.material3.rememberModalBottomSheetState
import androidx.compose.material3.adaptive.currentWindowAdaptiveInfoV2
import androidx.compose.runtime.Composable
import androidx.compose.runtime.compositionLocalOf
import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember
import androidx.compose.runtime.saveable.rememberSaveable
@@ -90,19 +91,21 @@ import androidx.compose.ui.text.style.TextDecoration
import androidx.compose.ui.text.style.TextOverflow
import androidx.compose.ui.platform.LocalDensity
import androidx.compose.ui.platform.LocalWindowInfo
import androidx.compose.ui.text.style.LineHeightStyle
import androidx.compose.ui.tooling.preview.Preview
import androidx.compose.ui.unit.Dp
import androidx.compose.ui.unit.TextUnit
import androidx.compose.ui.unit.Velocity
import androidx.compose.ui.unit.dp
import androidx.compose.ui.unit.sp
import androidx.lifecycle.compose.collectAsStateWithLifecycle
import com.rtbishop.look4sat.core.domain.predict.NearEarthObject
import com.rtbishop.look4sat.core.domain.predict.OrbitalData
import com.rtbishop.look4sat.core.domain.predict.OrbitalPass
import java.text.SimpleDateFormat
import java.util.Date
import com.rtbishop.look4sat.core.domain.utility.toTimerString
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.StateFlow
import java.util.Locale
import java.util.TimeZone
@Composable
@Preview(showBackground = true)
@@ -153,27 +156,21 @@ fun RowScope.TimerRow(timeString: String, isTimeAos: Boolean) {
@Composable
@Preview(showBackground = true)
private fun NextPassRowPreview() = MainTheme {
TopBar { NextPassRow(pass = getDefaultPass()) }
TopBar { NextPassRow(pass = getDefaultPass(), timeNow = MutableStateFlow(System.currentTimeMillis())) }
}
@Composable
fun RowScope.NextPassRow(pass: OrbitalPass, modifier: Modifier = Modifier, isUtc: Boolean = false) {
val timeZone = remember(isUtc) {
if (isUtc) TimeZone.getTimeZone("UTC") else TimeZone.getDefault()
fun RowScope.NextPassRow(pass: OrbitalPass, timeNow: StateFlow<Long>, modifier: Modifier = Modifier) {
val aosLosFormat = stringResource(id = R.string.pass_aosLos)
val aosLosStr = remember(pass.aosAzimuth, pass.losAzimuth, aosLosFormat) {
String.format(displayLocale(), aosLosFormat, pass.aosAzimuth.toInt() % 360, pass.losAzimuth.toInt() % 360)
}
val sdfTime = remember(isUtc) {
SimpleDateFormat("HH:mm:ss", displayLocale()).also { it.timeZone = timeZone }
}
ElevatedCard(
modifier = modifier
.height(48.dp)
.weight(1f)
) {
ElevatedCard(modifier = modifier.height(48.dp).weight(1f)) {
Column(
verticalArrangement = Arrangement.spacedBy((-2).dp),
modifier = Modifier
.fillMaxSize()
.padding(start = 6.dp, top = 1.dp, end = 6.dp, bottom = 0.dp)
.padding(start = 6.dp, top = 2.dp, end = 6.dp, bottom = 0.dp)
) {
Row(verticalAlignment = Alignment.CenterVertically) {
Text(
@@ -188,41 +185,43 @@ fun RowScope.NextPassRow(pass: OrbitalPass, modifier: Modifier = Modifier, isUtc
.infiniteMarquee(),
fontWeight = FontWeight.Medium,
maxLines = 1,
overflow = TextOverflow.Ellipsis
)
val elevColor = elevationColor(pass.maxElevation)
Icon(
painter = painterResource(R.drawable.ic_elevation),
contentDescription = null,
tint = elevColor,
modifier = Modifier.size(16.dp)
)
Spacer(modifier = Modifier.width(4.dp))
Text(
text = "${pass.maxElevation}°",
color = elevColor
overflow = TextOverflow.Ellipsis,
color = MaterialTheme.colorScheme.onSurface
)
PassTimerChip(pass = pass, timeNow = timeNow)
}
Row(
verticalAlignment = Alignment.CenterVertically,
horizontalArrangement = Arrangement.SpaceBetween,
modifier = Modifier.fillMaxWidth()
) {
Text(
text = sdfTime.format(Date(pass.aosTime)),
text = aosLosStr,
fontSize = 15.sp,
maxLines = 1,
overflow = TextOverflow.Ellipsis,
color = MaterialTheme.colorScheme.onSurface,
modifier = Modifier.weight(1f)
)
Row(
modifier = Modifier.weight(1f),
horizontalArrangement = Arrangement.Center,
verticalAlignment = Alignment.CenterVertically
) {
Text(text = "${pass.altitude} km", fontSize = 15.sp)
Row(verticalAlignment = Alignment.CenterVertically) {
val elevColor = elevationColor(pass.maxElevation)
Icon(
painter = painterResource(id = R.drawable.ic_elevation),
contentDescription = null,
tint = elevColor,
modifier = Modifier.size(16.dp)
)
Spacer(modifier = Modifier.width(4.dp))
Text(
text = "${pass.maxElevation}°",
color = elevColor
)
}
Text(
text = stringResource(R.string.pass_aosLos, pass.aosAzimuth.toInt(), pass.losAzimuth.toInt()),
text = "${pass.altitude} km",
fontSize = 15.sp,
maxLines = 1,
overflow = TextOverflow.Ellipsis,
color = MaterialTheme.colorScheme.onSurface,
textAlign = TextAlign.End,
modifier = Modifier.weight(1f)
)
@@ -231,7 +230,50 @@ fun RowScope.NextPassRow(pass: OrbitalPass, modifier: Modifier = Modifier, isUtc
}
}
private fun displayLocale(): Locale {
/**
* Countdown to AOS while the pass is pending, then to LOS while it is active.
* Gray denotes a pending pass, primary an active one. Collects the clock itself so a
* tick recomposes only this chip.
*/
@Composable
fun PassTimerChip(pass: OrbitalPass, timeNow: StateFlow<Long>) {
val now by timeNow.collectAsStateWithLifecycle()
// A DeepSpace object is permanently in view, so it gets a neutral label instead of a countdown
val isActive = !pass.isDeepSpace && now >= pass.aosTime
val text = when {
pass.isDeepSpace -> stringResource(R.string.pass_deep_space)
isActive -> stringResource(R.string.pass_timer_los, (pass.losTime - now).toTimerString())
else -> stringResource(R.string.pass_timer_aos, (pass.aosTime - now).toTimerString())
}
Surface(
shape = MaterialTheme.shapes.small,
color = if (isActive) MaterialTheme.colorScheme.primary else MaterialTheme.colorScheme.surfaceVariant,
contentColor = if (isActive) {
MaterialTheme.colorScheme.onPrimary
} else {
MaterialTheme.colorScheme.onSurfaceVariant
}
) {
Text(
text = text,
fontSize = 14.sp,
fontWeight = FontWeight.Medium,
style = LocalTextStyle.current.copy(
// Tabular figures stop the chip reflowing on every tick, while the trimmed
// line height keeps it short without shrinking the timer text
fontFeatureSettings = "tnum",
lineHeight = 16.sp,
lineHeightStyle = LineHeightStyle(
alignment = LineHeightStyle.Alignment.Center,
trim = LineHeightStyle.Trim.Both
)
),
modifier = Modifier.padding(horizontal = 6.dp, vertical = 2.dp)
)
}
}
fun displayLocale(): Locale {
val locale = Locale.getDefault()
return if (locale.language == Locale.CHINESE.language) locale else Locale.ENGLISH
}
@@ -46,9 +46,8 @@
<string name="pass_elevation">Elevation: %.1f°</string>
<string name="pass_deep_space" translatable="false">DeepSpace</string>
<string name="pass_altitude">Altitude: %d km</string>
<string name="pass_aosLos" translatable="false">%03d° - %03d°</string>
<string name="pass_aosLos" translatable="false">%03d°/%03d°</string>
<string name="pass_azimuth" translatable="false">%03d°</string>
<string name="pass_duration_placeholder" translatable="false">--m --s</string>
<string name="pass_timer_aos" translatable="false">AOS %s</string>
<string name="pass_timer_los" translatable="false">LOS %s</string>
<string name="pass_empty_list_message">
@@ -70,6 +70,7 @@ import com.rtbishop.look4sat.core.presentation.TimerRow
import com.rtbishop.look4sat.core.presentation.TopBar
import com.rtbishop.look4sat.core.presentation.isVerticalLayout
import com.rtbishop.look4sat.core.presentation.layoutPadding
import kotlinx.coroutines.flow.StateFlow
import org.osmdroid.events.DelayedMapListener
import org.osmdroid.events.MapListener
import org.osmdroid.events.ScrollEvent
@@ -130,7 +131,7 @@ private val darkTileFilter by lazy { createColorFilter() }
/**
* Recolor the shared overlay paints for the current theme. The light OSM tiles are white, so the
* dark-theme amber/grey overlays would vanish; light theme swaps them for dark, high-contrast colors.
* dark-theme amber/gray overlays would vanish; light theme swaps them for dark, high-contrast colors.
* Must run before the set*() overlay builders on each frame, as several icons cache the paint color.
*/
private fun applyMapColors(isLightUi: Boolean) {
@@ -177,11 +178,16 @@ fun MapDestination() {
viewModel.onAction(MapAction.SetVisible(false))
}
}
MapScreen(uiState, viewModel::onAction, mapView)
MapScreen(uiState, viewModel.timeNow, viewModel::onAction, mapView)
}
@Composable
private fun MapScreen(uiState: MapState, onAction: (MapAction) -> Unit, mapView: MapView) {
private fun MapScreen(
uiState: MapState,
timeNow: StateFlow<Long>,
onAction: (MapAction) -> Unit,
mapView: MapView
) {
val rotateMod = Modifier.rotate(180f)
val timeString = uiState.mapData?.aosTime ?: "00:00:00"
val isTimeAos = uiState.mapData?.isTimeAos ?: true
@@ -198,12 +204,12 @@ private fun MapScreen(uiState: MapState, onAction: (MapAction) -> Unit, mapView:
TimerRow(timeString = timeString, isTimeAos = isTimeAos)
IconCard(action = { onAction(MapAction.SelectNext) }, resId = R.drawable.ic_arrow)
}
TopBar { NextPassRow(pass = uiState.orbitalPass, isUtc = uiState.isUtc) }
TopBar { NextPassRow(pass = uiState.orbitalPass, timeNow = timeNow) }
} else {
TopBar {
IconCard(action = { onAction(MapAction.SelectPrev) }, resId = R.drawable.ic_arrow, modifier = rotateMod)
TimerRow(timeString = timeString, isTimeAos = isTimeAos)
NextPassRow(pass = uiState.orbitalPass, modifier = Modifier.weight(1f), isUtc = uiState.isUtc)
NextPassRow(pass = uiState.orbitalPass, timeNow = timeNow, modifier = Modifier.weight(1f))
IconCard(action = { onAction(MapAction.SelectNext) }, resId = R.drawable.ic_arrow)
}
}
@@ -408,7 +414,7 @@ private fun setPositions(
if (isLonVisible) visibleSats.add(satellite to geoPos)
}
} else {
for (entry in posMap) visibleSats.add(entry.key to entry.value)
for ((key, value) in posMap) visibleSats.add(key to value)
}
val showLabels = visibleSats.size <= LABEL_LIMIT
@@ -507,7 +513,7 @@ private fun setFootprint(rangeCircle: List<GeoPos>, mapView: MapView) {
var pts = footprintPoints
if (pts == null || pts.size != rangeCircle.size) {
pts = ArrayList(rangeCircle.size)
for (gp in rangeCircle) pts.add(GeoPoint(gp.latitude, gp.longitude))
for ((latitude, longitude) in rangeCircle) pts.add(GeoPoint(latitude, longitude))
footprintPoints = pts
} else {
for (i in rangeCircle.indices) {
@@ -77,6 +77,9 @@ class MapViewModel(
private val isScreenVisible = MutableStateFlow(true)
val uiState: StateFlow<MapState> = _uiState
private val _timeNow = MutableStateFlow(System.currentTimeMillis())
val timeNow: StateFlow<Long> = _timeNow
init {
viewModelScope.launch {
settingsRepo.otherSettings.collectLatest { settings ->
@@ -148,7 +151,9 @@ class MapViewModel(
while (isActive) {
// Suspends while the map is off-screen instead of predicting into the void
isScreenVisible.first { it }
dateNow.time = System.currentTimeMillis()
val timeNow = System.currentTimeMillis()
_timeNow.value = timeNow
dateNow.time = timeNow
updateMapState(orbitalObject, allSatellites, stationPos, dateNow)
delay(effectiveRate.milliseconds)
}
@@ -35,9 +35,7 @@ import androidx.compose.material3.ElevatedCard
import androidx.compose.material3.HorizontalDivider
import androidx.compose.material3.Icon
import androidx.compose.material3.LinearProgressIndicator
import androidx.compose.material3.LocalTextStyle
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.Surface
import androidx.compose.material3.Text
import androidx.compose.material3.pulltorefresh.PullToRefreshBox
import androidx.compose.material3.pulltorefresh.PullToRefreshDefaults
@@ -51,7 +49,6 @@ import androidx.compose.ui.platform.LocalContext
import androidx.compose.ui.res.painterResource
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.text.font.FontWeight
import androidx.compose.ui.text.style.LineHeightStyle
import androidx.compose.ui.text.style.TextAlign
import androidx.compose.ui.text.style.TextOverflow
import androidx.compose.ui.tooling.preview.Preview
@@ -64,16 +61,17 @@ import com.rtbishop.look4sat.core.domain.predict.NearEarthObject
import com.rtbishop.look4sat.core.domain.predict.OrbitalData
import com.rtbishop.look4sat.core.domain.predict.OrbitalPass
import com.rtbishop.look4sat.core.domain.repository.IContainerProvider
import com.rtbishop.look4sat.core.domain.utility.toTimerString
import com.rtbishop.look4sat.core.presentation.EmptyListCard
import com.rtbishop.look4sat.core.presentation.IconCard
import com.rtbishop.look4sat.core.presentation.MainTheme
import com.rtbishop.look4sat.core.presentation.NextPassRow
import com.rtbishop.look4sat.core.presentation.PassTimerChip
import com.rtbishop.look4sat.core.presentation.R
import com.rtbishop.look4sat.core.presentation.ScreenColumn
import com.rtbishop.look4sat.core.presentation.SearchBar
import com.rtbishop.look4sat.core.presentation.TopBar
import com.rtbishop.look4sat.core.presentation.WhatsNewDialog
import com.rtbishop.look4sat.core.presentation.displayLocale
import com.rtbishop.look4sat.core.presentation.elevationColor
import com.rtbishop.look4sat.core.presentation.infiniteMarquee
import com.rtbishop.look4sat.core.presentation.isVerticalLayout
@@ -81,7 +79,6 @@ import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.StateFlow
import java.text.SimpleDateFormat
import java.util.Date
import java.util.Locale
import java.util.TimeZone
@Composable
@@ -153,7 +150,7 @@ private fun PassesScreen(
)
},
bottomInfo = {
NextPassRow(pass = uiState.nextPass, isUtc = uiState.isUtc)
NextPassRow(pass = uiState.nextPass, timeNow = timeNow)
},
endAction = {
IconCard(action = { onAction(PassesAction.TogglePassesDialog) }, resId = R.drawable.ic_filter)
@@ -274,11 +271,6 @@ private fun StickyDateHeader(label: String, sunriseTime: String, sunsetTime: Str
}
}
internal fun displayLocale(): Locale {
val locale = Locale.getDefault()
return if (locale.language == Locale.CHINESE.language) locale else Locale.ENGLISH
}
@Preview(showBackground = true)
@Composable
private fun DeepSpacePassPreview() {
@@ -306,49 +298,6 @@ private fun ActivePassPreview() {
MainTheme { PassItem(pass = pass, { _, _ -> }, MutableStateFlow(aosTime + 280000L)) }
}
/**
* Countdown to AOS while the pass is pending, then to LOS while it is active.
* Gray denotes a pending pass, primary an active one. Collects the clock itself so a
* tick recomposes only this chip.
*/
@Composable
private fun PassTimerChip(pass: OrbitalPass, timeNow: StateFlow<Long>) {
val now by timeNow.collectAsStateWithLifecycle()
// A DeepSpace object is permanently in view, so it gets a neutral label instead of a countdown
val isActive = !pass.isDeepSpace && now >= pass.aosTime
val text = when {
pass.isDeepSpace -> stringResource(R.string.pass_deep_space)
isActive -> stringResource(R.string.pass_timer_los, (pass.losTime - now).toTimerString())
else -> stringResource(R.string.pass_timer_aos, (pass.aosTime - now).toTimerString())
}
Surface(
shape = MaterialTheme.shapes.small,
color = if (isActive) MaterialTheme.colorScheme.primary else MaterialTheme.colorScheme.surfaceVariant,
contentColor = if (isActive) {
MaterialTheme.colorScheme.onPrimary
} else {
MaterialTheme.colorScheme.onSurfaceVariant
}
) {
Text(
text = text,
fontSize = 14.sp,
fontWeight = FontWeight.Medium,
style = LocalTextStyle.current.copy(
// Tabular figures stop the chip reflowing on every tick, while the trimmed
// line height keeps it short without shrinking the timer text
fontFeatureSettings = "tnum",
lineHeight = 16.sp,
lineHeightStyle = LineHeightStyle(
alignment = LineHeightStyle.Alignment.Center,
trim = LineHeightStyle.Trim.Both
)
),
modifier = Modifier.padding(horizontal = 6.dp, vertical = 2.dp)
)
}
}
/** Elapsed fraction of the pass. Collects the clock itself to keep ticks off the text rows. */
@Composable
private fun PassProgressBar(pass: OrbitalPass, timeNow: StateFlow<Long>, modifier: Modifier = Modifier) {
@@ -369,7 +318,6 @@ private fun PassItem(
isVerticalLayout: Boolean = true,
isUtc: Boolean = false
) {
val passSatId = stringResource(id = R.string.pass_satId, pass.catNum)
val horizontalPadding = if (isVerticalLayout) 6.dp else 12.dp
val timeZone = remember(isUtc) {
if (isUtc) TimeZone.getTimeZone("UTC") else TimeZone.getDefault()
@@ -377,26 +325,22 @@ private fun PassItem(
val sdfTime = remember(isUtc) {
SimpleDateFormat("HH:mm:ss", displayLocale()).also { it.timeZone = timeZone }
}
val azimuthFormat = stringResource(id = R.string.pass_azimuth)
val aosLosFormat = stringResource(id = R.string.pass_aosLos)
val aosTimeStr = remember(pass.aosTime, isUtc) { sdfTime.format(Date(pass.aosTime)) }
val losTimeStr = remember(pass.losTime, isUtc) { sdfTime.format(Date(pass.losTime)) }
val aosAzStr = remember(pass.aosAzimuth, azimuthFormat) {
String.format(displayLocale(), azimuthFormat, pass.aosAzimuth.toInt() % 360)
}
val losAzStr = remember(pass.losAzimuth, azimuthFormat) {
String.format(displayLocale(), azimuthFormat, pass.losAzimuth.toInt() % 360)
}
val durationStr = remember(pass.aosTime, pass.losTime) {
val seconds = (pass.losTime - pass.aosTime) / 1000
"${seconds / 60}m ${seconds % 60}s"
val aosLosStr = remember(pass.aosAzimuth, pass.losAzimuth, aosLosFormat) {
String.format(displayLocale(), aosLosFormat, pass.aosAzimuth.toInt() % 360, pass.losAzimuth.toInt() % 360)
}
// do not delete
// val durationStr = remember(pass.aosTime, pass.losTime) {
// val seconds = (pass.losTime - pass.aosTime) / 1000
// "${seconds / 60}m ${seconds % 60}s"
// }
// do not delete
// val progressTint = MaterialTheme.colorScheme.secondary.copy(alpha = 0.08f)
// val progressFraction = if (pass.isDeepSpace) 0f else pass.progress.coerceIn(0f, 1f)
Column(
modifier = modifier.clickable { navigateToRadar(pass.catNum, pass.aosTime) }
) {
Column(modifier = modifier.clickable { navigateToRadar(pass.catNum, pass.aosTime) }) {
Column(
verticalArrangement = Arrangement.spacedBy(4.dp),
modifier = Modifier
@@ -408,7 +352,7 @@ private fun PassItem(
) {
Row(verticalAlignment = Alignment.CenterVertically) {
Text(
text = "$passSatId - ",
text = "${stringResource(R.string.pass_satId, pass.catNum)} - ",
color = MaterialTheme.colorScheme.primary
)
Text(
@@ -429,45 +373,25 @@ private fun PassItem(
modifier = Modifier.fillMaxWidth()
) {
Text(
text = if (pass.isDeepSpace) {
stringResource(R.string.pass_duration_placeholder)
} else {
durationStr
},
text = aosLosStr,
fontSize = 15.sp,
maxLines = 1,
overflow = TextOverflow.Ellipsis,
color = MaterialTheme.colorScheme.onSurface,
modifier = Modifier.weight(1f)
)
// AOS azimuth, peak elevation and LOS azimuth read as the arc of the pass
Row(
horizontalArrangement = Arrangement.spacedBy(10.dp),
verticalAlignment = Alignment.CenterVertically
) {
Text(
text = aosAzStr,
fontSize = 15.sp,
color = MaterialTheme.colorScheme.onSurface
Row(verticalAlignment = Alignment.CenterVertically) {
val elevColor = elevationColor(pass.maxElevation)
Icon(
painter = painterResource(id = R.drawable.ic_elevation),
contentDescription = null,
tint = elevColor,
modifier = Modifier.size(16.dp)
)
Row(verticalAlignment = Alignment.CenterVertically) {
val elevColor = elevationColor(pass.maxElevation)
Icon(
painter = painterResource(id = R.drawable.ic_elevation),
contentDescription = null,
tint = elevColor,
modifier = Modifier.size(16.dp)
)
Spacer(modifier = Modifier.width(4.dp))
Text(
text = "${pass.maxElevation}°",
color = elevColor
)
}
Spacer(modifier = Modifier.width(4.dp))
Text(
text = losAzStr,
fontSize = 15.sp,
color = MaterialTheme.colorScheme.onSurface
text = "${pass.maxElevation}°",
color = elevColor
)
}
Text(
@@ -135,7 +135,7 @@ class PassesViewModel(
)
val sunTimes = computeSunTimes(filtered, isUtc)
var live: List<OrbitalPass>? = null
while (isActive) {
while (this@launch.isActive) {
val timeNow = System.currentTimeMillis()
_timeNow.value = timeNow
val current = filtered.filter { it.isDeepSpace || timeNow < it.losTime }
@@ -75,6 +75,7 @@ import com.rtbishop.look4sat.core.presentation.formatFrequency
import com.rtbishop.look4sat.core.presentation.getDefaultPass
import com.rtbishop.look4sat.core.presentation.isVerticalLayout
import com.rtbishop.look4sat.core.presentation.layoutPadding
import kotlinx.coroutines.flow.StateFlow
import kotlinx.coroutines.launch
private enum class RadarPage(val title: String) {
@@ -99,7 +100,7 @@ fun RadarDestination(navigateUp: () -> Unit) {
val permissionLauncher = rememberLauncherForActivityResult(
ActivityResultContracts.RequestPermission()
) { granted -> viewModel.onAction(RadarAction.SstvPermissionResult(granted)) }
RadarScreen(uiState, viewModel::onAction, navigateUp, requestMicPermission = {
RadarScreen(uiState, viewModel.timeNow, viewModel::onAction, navigateUp, requestMicPermission = {
permissionLauncher.launch(Manifest.permission.RECORD_AUDIO)
})
}
@@ -107,6 +108,7 @@ fun RadarDestination(navigateUp: () -> Unit) {
@Composable
private fun RadarScreen(
uiState: RadarState,
timeNow: StateFlow<Long>,
onAction: (RadarAction) -> Unit,
navigateUp: () -> Unit,
requestMicPermission: () -> Unit
@@ -129,12 +131,12 @@ private fun RadarScreen(
TimerRow(timeString = uiState.currentTime, isTimeAos = uiState.isTimeAos)
IconCard(action = addToCalendar, resId = R.drawable.ic_calendar)
}
TopBar { NextPassRow(pass = upcomingPass, isUtc = uiState.isUtc) }
TopBar { NextPassRow(pass = upcomingPass, timeNow = timeNow) }
} else {
TopBar {
IconCard(action = navigateUp, resId = R.drawable.ic_back)
TimerRow(timeString = uiState.currentTime, isTimeAos = uiState.isTimeAos)
NextPassRow(pass = upcomingPass, modifier = Modifier.weight(1f), isUtc = uiState.isUtc)
NextPassRow(pass = upcomingPass, timeNow = timeNow, modifier = Modifier.weight(1f))
IconCard(action = addToCalendar, resId = R.drawable.ic_calendar)
}
}
@@ -91,6 +91,9 @@ class RadarViewModel(
)
val uiState: StateFlow<RadarState> = _uiState
private val _timeNow = MutableStateFlow(System.currentTimeMillis())
val timeNow: StateFlow<Long> = _timeNow
init {
collectSettingsChanges() // also handles initial sensor subscription
collectStationPositionChanges()
@@ -198,6 +201,7 @@ class RadarViewModel(
private suspend fun tickPass(pass: OrbitalPass, allRadios: List<SatRadio>) {
val timeNow = System.currentTimeMillis()
_timeNow.value = timeNow
val pos = satelliteRepo.getPosition(pass.orbitalObject, stationPos, timeNow)
// Recompute celestial positions at most once per minute (they move very slowly)