diff --git a/feature/radar/src/main/java/com/rtbishop/look4sat/feature/radar/RadarScreen.kt b/feature/radar/src/main/java/com/rtbishop/look4sat/feature/radar/RadarScreen.kt index de2a401c..c0511126 100644 --- a/feature/radar/src/main/java/com/rtbishop/look4sat/feature/radar/RadarScreen.kt +++ b/feature/radar/src/main/java/com/rtbishop/look4sat/feature/radar/RadarScreen.kt @@ -37,9 +37,9 @@ import androidx.compose.foundation.layout.size import androidx.compose.foundation.lazy.LazyColumn import androidx.compose.foundation.lazy.items import androidx.compose.material3.ElevatedCard +import androidx.compose.material3.HorizontalDivider import androidx.compose.material3.Icon import androidx.compose.material3.MaterialTheme -import androidx.compose.material3.Surface import androidx.compose.material3.Text import androidx.compose.runtime.Composable import androidx.compose.runtime.getValue @@ -89,19 +89,23 @@ fun NavGraphBuilder.radarDestination( composable(radarRoute, radarArgs) { val viewModel = viewModel(RadarViewModel::class.java, factory = RadarViewModel.Factory) val uiState by viewModel.uiState.collectAsStateWithLifecycle() - RadarScreen(uiState, navigateUp, navigateToRadioControl) + RadarScreen(uiState, viewModel::onAction, navigateUp, navigateToRadioControl) } } @Composable private fun RadarScreen( uiState: RadarState, + onAction: (RadarAction) -> Unit, navigateUp: () -> Unit, navigateToRadioControl: (Int, Long) -> Unit ) { + val upcomingPass = uiState.currentPass ?: getDefaultPass() + if (upcomingPass.losTime < System.currentTimeMillis()) navigateUp() + val addToCalendar: () -> Unit = { uiState.currentPass?.let { pass -> - uiState.sendAction(RadarAction.AddToCalendar(pass.name, pass.aosTime, pass.losTime)) + onAction(RadarAction.AddToCalendar(pass.name, pass.aosTime, pass.losTime)) } } val openRadioControl: () -> Unit = { @@ -109,8 +113,6 @@ private fun RadarScreen( navigateToRadioControl(pass.catNum, pass.aosTime) } } - val upcomingPass = uiState.currentPass ?: getDefaultPass() - if (upcomingPass.losTime < System.currentTimeMillis()) navigateUp() Column( modifier = Modifier .layoutPadding() @@ -120,28 +122,26 @@ private fun RadarScreen( val isVertical = isVerticalLayout() if (isVertical) { TopBar { - IconCard(action = navigateUp, resId = R.drawable.ic_back) + IconCard(action = addToCalendar, resId = R.drawable.ic_calendar) TimerRow(timeString = uiState.currentTime, isTimeAos = uiState.isCurrentTimeAos) IconCard(action = openRadioControl, resId = R.drawable.ic_radios) - IconCard(action = addToCalendar, resId = R.drawable.ic_calendar) } TopBar { NextPassRow(pass = upcomingPass, isUtc = uiState.isUtc) } } else { TopBar { - IconCard(action = navigateUp, resId = R.drawable.ic_back) + IconCard(action = addToCalendar, resId = R.drawable.ic_calendar) TimerRow(timeString = uiState.currentTime, isTimeAos = uiState.isCurrentTimeAos) NextPassRow(pass = upcomingPass, modifier = Modifier.weight(1f), isUtc = uiState.isUtc) IconCard(action = openRadioControl, resId = R.drawable.ic_radios) - IconCard(action = addToCalendar, resId = R.drawable.ic_calendar) } } if (isVertical) { RadarCard(uiState, Modifier.weight(1f)) - TransmittersCard(uiState, Modifier.weight(1f)) + TransmittersCard(uiState.transmitters, uiState.selectedTransmitterUuid, onAction, Modifier.weight(1f)) } else { Row(horizontalArrangement = Arrangement.spacedBy(6.dp)) { RadarCard(uiState, Modifier.weight(1f)) - TransmittersCard(uiState, Modifier.weight(1f)) + TransmittersCard(uiState.transmitters, uiState.selectedTransmitterUuid, onAction, Modifier.weight(1f)) } } } @@ -255,17 +255,22 @@ private fun RadarLabel( } @Composable -private fun TransmittersCard(uiState: RadarState, modifier: Modifier = Modifier) { +private fun TransmittersCard( + transmitters: List, + selectedUuid: String?, + onAction: (RadarAction) -> Unit, + modifier: Modifier = Modifier +) { ElevatedCard(modifier = modifier) { - if (uiState.transmitters.isEmpty()) { + if (transmitters.isEmpty()) { EmptyTransmittersContent() } else { TransmittersList( - transmitters = uiState.transmitters, - selectedUuid = uiState.selectedTransmitterUuid, + transmitters = transmitters, + selectedUuid = selectedUuid, onSelect = { uuid -> - if (uiState.selectedTransmitterUuid != null) { - uiState.sendAction(RadarAction.SelectTransmitter(uuid)) + if (selectedUuid != null) { + onAction(RadarAction.SelectTransmitter(uuid)) } } ) @@ -324,7 +329,6 @@ private fun TransmitterItemPreview() { MainTheme { TransmitterItem(transmitter, isClickable = true, isSelected = true, onClick = {}) } } - @Composable private fun TransmitterItem( radio: SatRadio, @@ -334,42 +338,42 @@ private fun TransmitterItem( ) { val title = if (radio.isInverted) "INVERTED: ${radio.info}" else radio.info val fullTitle = "$title - (${radio.downlinkMode ?: "--"}/${radio.uplinkMode ?: "--"})" - Surface( - color = MaterialTheme.colorScheme.background, + Column( modifier = Modifier .fillMaxWidth() .then(if (isClickable) Modifier.clickable { onClick() } else Modifier) ) { - Surface(modifier = Modifier.padding(bottom = 2.dp)) { - Column( - horizontalAlignment = Alignment.CenterHorizontally, - verticalArrangement = Arrangement.spacedBy(6.dp), - modifier = Modifier - .background(MaterialTheme.colorScheme.surface) - .padding(horizontal = 8.dp, vertical = 6.dp) - ) { - Box { - Text( - text = fullTitle, - textAlign = TextAlign.Center, - modifier = Modifier - .fillMaxWidth() - .padding(horizontal = 6.dp) - .infiniteMarquee() + Column( + horizontalAlignment = Alignment.CenterHorizontally, + verticalArrangement = Arrangement.spacedBy(6.dp), + modifier = Modifier + .fillMaxWidth() + .background(MaterialTheme.colorScheme.surface) + .padding(horizontal = 8.dp, vertical = 6.dp) + ) { + Box { + Text( + text = fullTitle, + textAlign = TextAlign.Center, + color = MaterialTheme.colorScheme.onSurface, + modifier = Modifier + .fillMaxWidth() + .padding(horizontal = 6.dp) + .infiniteMarquee() + ) + if (isSelected) { + Icon( + painter = painterResource(id = R.drawable.ic_radios), + contentDescription = null, + tint = MaterialTheme.colorScheme.primary, + modifier = Modifier.background(color = MaterialTheme.colorScheme.surface) ) - if (isSelected) { - Icon( - painter = painterResource(id = R.drawable.ic_radios), - contentDescription = null, - tint = MaterialTheme.colorScheme.primary, - modifier = Modifier.background(color = MaterialTheme.colorScheme.surface) - ) - } } - FrequencyRow(radio = radio, isDownlink = true) - FrequencyRow(radio = radio, isDownlink = false) } + FrequencyRow(radio = radio, isDownlink = true) + FrequencyRow(radio = radio, isDownlink = false) } + HorizontalDivider(thickness = 2.dp, color = MaterialTheme.colorScheme.background) } } diff --git a/feature/radar/src/main/java/com/rtbishop/look4sat/feature/radar/RadarState.kt b/feature/radar/src/main/java/com/rtbishop/look4sat/feature/radar/RadarState.kt index bf2b4e12..a23ae4c1 100644 --- a/feature/radar/src/main/java/com/rtbishop/look4sat/feature/radar/RadarState.kt +++ b/feature/radar/src/main/java/com/rtbishop/look4sat/feature/radar/RadarState.kt @@ -22,19 +22,18 @@ import com.rtbishop.look4sat.core.domain.predict.OrbitalPass import com.rtbishop.look4sat.core.domain.predict.OrbitalPos data class RadarState( - val currentPass: OrbitalPass?, - val currentTime: String, - val isCurrentTimeAos: Boolean, - val isUtc: Boolean, - val orientationValues: Pair, - val orbitalPos: OrbitalPos?, - val satTrack: List, - val shouldShowSweep: Boolean, - val shouldUseCompass: Boolean, - val transmitters: List, - val selectedTransmitterUuid: String?, - val selectedFrequency: Long?, - val sendAction: (RadarAction) -> Unit + val currentPass: OrbitalPass? = null, + val currentTime: String = "00:00:00", + val isCurrentTimeAos: Boolean = true, + val isUtc: Boolean = false, + val orientationValues: Pair = 0f to 0f, + val orbitalPos: OrbitalPos? = null, + val satTrack: List = emptyList(), + val shouldShowSweep: Boolean = false, + val shouldUseCompass: Boolean = false, + val transmitters: List = emptyList(), + val selectedTransmitterUuid: String? = null, + val selectedFrequency: Long? = null ) sealed class RadarAction { diff --git a/feature/radar/src/main/java/com/rtbishop/look4sat/feature/radar/RadarViewModel.kt b/feature/radar/src/main/java/com/rtbishop/look4sat/feature/radar/RadarViewModel.kt index ebc60317..d11e0379 100644 --- a/feature/radar/src/main/java/com/rtbishop/look4sat/feature/radar/RadarViewModel.kt +++ b/feature/radar/src/main/java/com/rtbishop/look4sat/feature/radar/RadarViewModel.kt @@ -54,25 +54,16 @@ class RadarViewModel( private val addToCalendar: IAddToCalendar ) : ViewModel() { - private val _uiState = MutableStateFlow( - RadarState( - currentPass = null, - currentTime = "00:00:00", - isCurrentTimeAos = true, - isUtc = settingsRepo.otherSettings.value.stateOfUtc, - orientationValues = sensorsRepo.orientation.value, - orbitalPos = null, - satTrack = emptyList(), - shouldShowSweep = settingsRepo.otherSettings.value.stateOfSweep, - shouldUseCompass = settingsRepo.otherSettings.value.stateOfSensors, - selectedTransmitterUuid = null, - selectedFrequency = null, - transmitters = emptyList(), - sendAction = ::handleAction, - ) - ) private val stationPos = settingsRepo.stationPosition.value private val magDeclination = sensorsRepo.getMagDeclination(stationPos) + private val _uiState = MutableStateFlow( + RadarState( + isUtc = settingsRepo.otherSettings.value.stateOfUtc, + orientationValues = sensorsRepo.orientation.value, + shouldShowSweep = settingsRepo.otherSettings.value.stateOfSweep, + shouldUseCompass = settingsRepo.otherSettings.value.stateOfSensors + ) + ) val uiState: StateFlow = _uiState init { @@ -81,7 +72,7 @@ class RadarViewModel( viewModelScope.launch { sensorsRepo.enableSensor() sensorsRepo.orientation.collect { data -> - val orientationValues = Pair(data.first + magDeclination, data.second) + val orientationValues = (data.first + magDeclination) to data.second _uiState.update { it.copy(orientationValues = orientationValues) } } } @@ -92,41 +83,29 @@ class RadarViewModel( _uiState.update { it.copy(isUtc = settings.stateOfUtc) } } } - // Resolve which pass we're tracking + // Resolve which pass we're tracking and start the tick loop viewModelScope.launch { val catNum = savedStateHandle.get("catNum") ?: 0 val aosTime = savedStateHandle.get("aosTime") ?: 0L val passes = satelliteRepo.passes.value - val pass = passes.find { pass -> pass.catNum == catNum && pass.aosTime == aosTime } - val currentPass = pass ?: passes.firstOrNull() + val currentPass = passes.find { it.catNum == catNum && it.aosTime == aosTime } + ?: passes.firstOrNull() currentPass?.let { satPass -> _uiState.update { it.copy(currentPass = satPass) } val transmitters = satelliteRepo.getRadiosWithId(satPass.catNum) - // Compute track once (it doesn't change) + // Compute track once (it doesn't change for a given pass) if (!satPass.isDeepSpace) { val track = satelliteRepo.getTrack( satPass.orbitalObject, stationPos, satPass.aosTime, satPass.losTime ) _uiState.update { it.copy(satTrack = track) } } - // Local tick loop — computes position only while the radar screen is alive + // Tick loop — position, timer, and radio updates every second while (isActive) { val timeNow = System.currentTimeMillis() val pos = satelliteRepo.getPosition(satPass.orbitalObject, stationPos, timeNow) - when { - satPass.isDeepSpace -> { - val time = 0L.toTimerString() - _uiState.update { it.copy(currentTime = time, isCurrentTimeAos = false, orbitalPos = pos) } - } - satPass.aosTime > timeNow -> { - val time = satPass.aosTime.minus(timeNow).toTimerString() - _uiState.update { it.copy(currentTime = time, isCurrentTimeAos = true, orbitalPos = pos) } - } - else -> { - val time = satPass.losTime.minus(timeNow).toTimerString() - _uiState.update { it.copy(currentTime = time, isCurrentTimeAos = false, orbitalPos = pos) } - } - } + val (time, isAos) = computeTimer(satPass.isDeepSpace, satPass.aosTime, satPass.losTime, timeNow) + _uiState.update { it.copy(currentTime = time, isCurrentTimeAos = isAos, orbitalPos = pos) } processRadios(transmitters, satPass.orbitalObject, timeNow) sendPassData(pos) delay(1000) @@ -140,10 +119,18 @@ class RadarViewModel( super.onCleared() } - private fun handleAction(action: RadarAction) { + fun onAction(action: RadarAction) { when (action) { is RadarAction.AddToCalendar -> addToCalendar(action.name, action.aosTime, action.losTime) - is RadarAction.SelectTransmitter -> { _uiState.update { it.copy(selectedTransmitterUuid = action.uuid) } } + is RadarAction.SelectTransmitter -> _uiState.update { it.copy(selectedTransmitterUuid = action.uuid) } + } + } + + private fun computeTimer(isDeepSpace: Boolean, aosTime: Long, losTime: Long, timeNow: Long): Pair { + return when { + isDeepSpace -> 0L.toTimerString() to false + aosTime > timeNow -> (aosTime - timeNow).toTimerString() to true + else -> (losTime - timeNow).toTimerString() to false } } @@ -183,8 +170,10 @@ class RadarViewModel( private suspend fun processRadios(radios: List, orbitalObject: OrbitalObject, time: Long) { val transmitters = satelliteRepo.getRadios(orbitalObject, stationPos, radios, time) + val isFreqEnabled = + settingsRepo.rcSettings.value.frequencyState || settingsRepo.rcSettings.value.bluetoothFrequencyState _uiState.update { state -> - if (!settingsRepo.rcSettings.value.frequencyState && !settingsRepo.rcSettings.value.bluetoothFrequencyState) { + if (!isFreqEnabled) { return@update state.copy( transmitters = transmitters, selectedTransmitterUuid = null, @@ -197,18 +186,12 @@ class RadarViewModel( val low = radio.downlinkLow val high = radio.downlinkHigh when { - low != null && high != null -> - (low + high) / 2 - low != null -> - low + low != null && high != null -> (low + high) / 2 + low != null -> low else -> null } } - state.copy( - transmitters = transmitters, - selectedTransmitterUuid = selectedUuid, - selectedFrequency = freq - ) + state.copy(transmitters = transmitters, selectedTransmitterUuid = selectedUuid, selectedFrequency = freq) } } diff --git a/feature/radiocontrol/src/main/java/com/rtbishop/look4sat/feature/radiocontrol/RadioControlScreen.kt b/feature/radiocontrol/src/main/java/com/rtbishop/look4sat/feature/radiocontrol/RadioControlScreen.kt index 77af477f..36e541a9 100644 --- a/feature/radiocontrol/src/main/java/com/rtbishop/look4sat/feature/radiocontrol/RadioControlScreen.kt +++ b/feature/radiocontrol/src/main/java/com/rtbishop/look4sat/feature/radiocontrol/RadioControlScreen.kt @@ -26,14 +26,12 @@ import androidx.compose.foundation.layout.ExperimentalLayoutApi import androidx.compose.foundation.layout.FlowRow import androidx.compose.foundation.layout.Row import androidx.compose.foundation.layout.Spacer -import androidx.compose.foundation.layout.fillMaxSize import androidx.compose.foundation.layout.fillMaxWidth import androidx.compose.foundation.layout.height import androidx.compose.foundation.layout.padding import androidx.compose.foundation.layout.size import androidx.compose.foundation.layout.width import androidx.compose.foundation.lazy.LazyColumn -import androidx.compose.foundation.lazy.items import androidx.compose.foundation.shape.CircleShape import androidx.compose.material3.ElevatedCard import androidx.compose.material3.FilterChip @@ -59,11 +57,15 @@ import androidx.navigation.navArgument import com.rtbishop.look4sat.core.domain.model.SatRadio import com.rtbishop.look4sat.core.presentation.CardButton import com.rtbishop.look4sat.core.presentation.IconCard +import com.rtbishop.look4sat.core.presentation.NextPassRow import com.rtbishop.look4sat.core.presentation.R import com.rtbishop.look4sat.core.presentation.Screen import com.rtbishop.look4sat.core.presentation.TimerRow import com.rtbishop.look4sat.core.presentation.TopBar +import com.rtbishop.look4sat.core.presentation.getDefaultPass +import com.rtbishop.look4sat.core.presentation.isVerticalLayout import com.rtbishop.look4sat.core.presentation.layoutPadding +import java.util.Locale fun NavGraphBuilder.radioControlDestination(navigateUp: () -> Unit) { val route = "${Screen.RadioControl.route}?catNum={catNum}&aosTime={aosTime}" @@ -86,16 +88,21 @@ private fun RadioControlScreen(uiState: RadioControlState, navigateUp: () -> Uni .keepScreenOn(), verticalArrangement = Arrangement.spacedBy(6.dp) ) { - TopBar { - IconCard(action = navigateUp, resId = R.drawable.ic_back) - TimerRow(timeString = uiState.currentTime, isTimeAos = uiState.isCurrentTimeAos) - uiState.currentPass?.let { pass -> - Text( - text = pass.name, - fontSize = 16.sp, - fontWeight = FontWeight.Medium, - modifier = Modifier.padding(horizontal = 8.dp) - ) + val currentPass = uiState.currentPass ?: getDefaultPass() + val isVertical = isVerticalLayout() + if (isVertical) { + TopBar { + IconCard(action = navigateUp, resId = R.drawable.ic_back) + TimerRow(timeString = uiState.currentTime, isTimeAos = uiState.isCurrentTimeAos) + IconCard(action = navigateUp, resId = R.drawable.ic_back) + } + TopBar { NextPassRow(pass = currentPass) } + } else { + TopBar { + IconCard(action = navigateUp, resId = R.drawable.ic_back) + TimerRow(timeString = uiState.currentTime, isTimeAos = uiState.isCurrentTimeAos) + NextPassRow(pass = currentPass, modifier = Modifier.weight(1f)) + IconCard(action = navigateUp, resId = R.drawable.ic_back) } } @@ -265,7 +272,7 @@ private fun CtcssSelector(uiState: RadioControlState) { FilterChip( selected = uiState.ctcssTone == tone, onClick = { uiState.sendAction(RadioControlAction.SetCtcssTone(tone)) }, - label = { Text(String.format("%.1f", tone)) } + label = { Text(String.format(Locale.ENGLISH, "%.1f", tone)) } ) } } diff --git a/feature/radiocontrol/src/main/java/com/rtbishop/look4sat/feature/radiocontrol/RadioControlViewModel.kt b/feature/radiocontrol/src/main/java/com/rtbishop/look4sat/feature/radiocontrol/RadioControlViewModel.kt index 926719dc..8c62b5dd 100644 --- a/feature/radiocontrol/src/main/java/com/rtbishop/look4sat/feature/radiocontrol/RadioControlViewModel.kt +++ b/feature/radiocontrol/src/main/java/com/rtbishop/look4sat/feature/radiocontrol/RadioControlViewModel.kt @@ -30,11 +30,9 @@ import com.rtbishop.look4sat.core.domain.repository.IContainerProvider import com.rtbishop.look4sat.core.domain.repository.IRadioTrackingService import com.rtbishop.look4sat.core.domain.repository.ISatelliteRepo import com.rtbishop.look4sat.core.domain.repository.ISettingsRepo -import com.rtbishop.look4sat.core.domain.repository.RadioTrackingState import com.rtbishop.look4sat.core.domain.utility.toTimerString import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.flow.StateFlow -import kotlinx.coroutines.flow.combine import kotlinx.coroutines.flow.update import kotlinx.coroutines.launch import java.util.Locale @@ -108,8 +106,8 @@ class RadioControlViewModel( } val isAos = pass != null && !pass.isDeepSpace && pass.aosTime > timeNow - _uiState.update { - it.copy( + _uiState.update { state -> + state.copy( currentTime = timeStr, isCurrentTimeAos = isAos, azimuth = String.format(Locale.ENGLISH, "%.1f", svc.azimuth),