mirror of
https://github.com/rt-bishop/Look4Sat.git
synced 2026-10-13 15:57:24 +00:00
Tweaked NextPassRow component to include AOS/LOS timer
This commit is contained in:
1 parent
fa7b8cefe7
commit
e335b43c77
8 files changed
+135
-153
No files matched your search
+81
-39
@@ -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)
|
||||
}
|
||||
|
||||
+24
-100
@@ -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(
|
||||
|
||||
+1
-1
@@ -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)
|
||||
|
||||
Reference in new issue
Block a user