Added DeepSpace passes filter, fixed refresh issue (#208)

This commit is contained in:
Arty Bishop authored and Arty Bishop committed 2026-04-18 15:56:28 +01:00
1 parent d2b3184084
commit 8265b73d75
10 files changed
+112 -68

No files matched your search

@@ -32,12 +32,15 @@ import androidx.compose.material3.Checkbox
import androidx.compose.material3.Icon
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.Slider
import androidx.compose.material3.Switch
import androidx.compose.material3.Text
import androidx.compose.runtime.Composable
import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableDoubleStateOf
import androidx.compose.runtime.mutableIntStateOf
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember
import androidx.compose.runtime.setValue
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.res.painterResource
@@ -65,17 +68,29 @@ private val hourSteps = listOf(1, 2, 4, 8, 12, 24, 48, 72, 96, 120, 144, 168, 19
@Preview
@Composable
private fun PassesDialogPreview() {
MainTheme { PassesDialog(24, 16.0, {}) { _, _ -> } }
MainTheme { PassesDialog(24, 16.0, true, {}) { _, _, _ -> } }
}
@Composable
internal fun PassesDialog(hours: Int, elevation: Double, cancel: () -> Unit, accept: (Int, Double) -> Unit) {
internal fun PassesDialog(
hours: Int,
elevation: Double,
showDeepSpace: Boolean,
cancel: () -> Unit,
accept: (Int, Double, Boolean) -> Unit
) {
val hoursIndex = remember { mutableIntStateOf(hourSteps.indexOfFirst { it >= hours }.coerceAtLeast(0)) }
val elevationValueNew = remember { mutableDoubleStateOf(elevation) }
var deepSpaceEnabled by remember { mutableStateOf(showDeepSpace) }
val onAccept = {
accept(hourSteps[hoursIndex.intValue], elevationValueNew.doubleValue).also { cancel() }
accept(hourSteps[hoursIndex.intValue], elevationValueNew.doubleValue, deepSpaceEnabled).also { cancel() }
}
SharedDialog(title = stringResource(R.string.pass_filter_title), onCancel = cancel, onAccept = onAccept) {
ToggleRow(
title = stringResource(R.string.pass_filter_deep_space),
checked = deepSpaceEnabled,
onCheckedChange = { deepSpaceEnabled = it }
)
SliderRow(
title = stringResource(R.string.pass_filter_elev),
value = elevationValueNew.doubleValue,
@@ -188,3 +203,20 @@ internal fun RadiosDialog(modes: List<String>, cancel: () -> Unit, accept: (List
}
}
}
@Composable
private fun ToggleRow(title: String, checked: Boolean, onCheckedChange: (Boolean) -> Unit) {
Row(
horizontalArrangement = Arrangement.SpaceBetween,
verticalAlignment = Alignment.CenterVertically,
modifier = Modifier.padding(horizontal = LocalSpacing.current.large)
) {
Text(
text = title,
fontSize = 16.sp,
modifier = Modifier.weight(1f),
color = MaterialTheme.colorScheme.onSurface
)
Switch(checked = checked, onCheckedChange = onCheckedChange)
}
}
@@ -100,9 +100,10 @@ private fun PassesScreen(
PassesDialog(
hours = uiState.hours,
elevation = uiState.elevation,
showDeepSpace = uiState.showDeepSpace,
cancel = { onAction(PassesAction.TogglePassesDialog) }
) { hours, elevation ->
onAction(PassesAction.FilterPasses(hours, elevation))
) { hours, elevation, showDeepSpace ->
onAction(PassesAction.FilterPasses(hours, elevation, showDeepSpace))
}
}
if (uiState.isRadiosDialogShown) {
@@ -29,6 +29,7 @@ data class PassesState(
val isNextTimeAos: Boolean = true,
val hours: Int = 24,
val elevation: Double = 16.0,
val showDeepSpace: Boolean = true,
val modes: List<String> = emptyList(),
val itemsList: List<OrbitalPass> = emptyList(),
val shouldSeeWhatsNew: Boolean = false
@@ -36,7 +37,7 @@ data class PassesState(
sealed interface PassesAction {
data object DismissWhatsNew : PassesAction
data class FilterPasses(val hoursAhead: Int, val minElevation: Double) : PassesAction
data class FilterPasses(val hoursAhead: Int, val minElevation: Double, val showDeepSpace: Boolean) : PassesAction
data class FilterRadios(val modes: List<String>) : PassesAction
data object RefreshPasses : PassesAction
data object TogglePassesDialog : PassesAction
@@ -34,7 +34,6 @@ import kotlinx.coroutines.delay
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.StateFlow
import kotlinx.coroutines.flow.collectLatest
import kotlinx.coroutines.flow.drop
import kotlinx.coroutines.flow.update
import kotlinx.coroutines.isActive
import kotlinx.coroutines.launch
@@ -51,6 +50,7 @@ class PassesViewModel(
nextPass = defaultPass,
hours = settingsRepo.passesSettings.value.hoursAhead,
elevation = settingsRepo.passesSettings.value.minElevation,
showDeepSpace = settingsRepo.passesSettings.value.showDeepSpace,
modes = settingsRepo.passesSettings.value.selectedModes,
shouldSeeWhatsNew = settingsRepo.otherSettings.value.shouldSeeWhatsNew
)
@@ -58,29 +58,20 @@ class PassesViewModel(
val uiState: StateFlow<PassesState> = _uiState
init {
// Show refreshing indicator whenever the selected satellites list changes
// Refresh indicator: mirrors the repo's isCalculating state
viewModelScope.launch {
settingsRepo.selectedIds.collectLatest { selectedIds ->
_uiState.update { it.copy(isRefreshing = true) }
if (selectedIds.isEmpty()) {
// No satellites selected — show indicator briefly, then stop
delay(1000)
_uiState.update { it.copy(isRefreshing = false) }
}
// For non-empty selections, the passes collector below will clear isRefreshing
}
}
// Stop refreshing whenever new passes arrive (from initial load, selection change, or filter)
viewModelScope.launch {
satelliteRepo.passes.drop(1).collect { _ ->
_uiState.update { it.copy(isRefreshing = false) }
satelliteRepo.isCalculating.collect { calculating ->
_uiState.update { it.copy(isRefreshing = calculating) }
}
}
// Local tick loop — computes pass progress and countdown timer every second
viewModelScope.launch {
while (isActive) {
val timeNow = System.currentTimeMillis()
val processed = computePassProgress(satelliteRepo.passes.value, timeNow)
val showDeepSpace = _uiState.value.showDeepSpace
val allPasses = satelliteRepo.passes.value
val filtered = if (showDeepSpace) allPasses else allPasses.filter { !it.isDeepSpace }
val processed = computePassProgress(filtered, timeNow)
val (nextPass, nextTime, isAos) = resolveNextPass(processed, timeNow)
_uiState.update {
it.copy(
@@ -108,11 +99,17 @@ class PassesViewModel(
fun onAction(action: PassesAction) {
when (action) {
PassesAction.DismissWhatsNew -> settingsRepo.setWhatsNewDismissed()
is PassesAction.FilterPasses -> applyFilter(action.hoursAhead, action.minElevation, _uiState.value.modes)
is PassesAction.FilterRadios -> applyFilter(_uiState.value.hours, _uiState.value.elevation, action.modes)
is PassesAction.FilterPasses ->
applyFilter(action.hoursAhead, action.minElevation, action.showDeepSpace, _uiState.value.modes)
is PassesAction.FilterRadios ->
applyFilter(_uiState.value.hours, _uiState.value.elevation, _uiState.value.showDeepSpace, action.modes)
PassesAction.RefreshPasses -> refreshPasses()
PassesAction.TogglePassesDialog -> _uiState.update { it.copy(isPassesDialogShown = !it.isPassesDialogShown) }
PassesAction.ToggleRadiosDialog -> _uiState.update { it.copy(isRadiosDialogShown = !it.isRadiosDialogShown) }
PassesAction.TogglePassesDialog ->
_uiState.update { it.copy(isPassesDialogShown = !it.isPassesDialogShown) }
PassesAction.ToggleRadiosDialog ->
_uiState.update { it.copy(isRadiosDialogShown = !it.isRadiosDialogShown) }
}
}
@@ -153,22 +150,24 @@ class PassesViewModel(
return Triple(defaultPass, "00:00:00", true)
}
private fun applyFilter(hoursAhead: Int, minElevation: Double, modes: List<String>) = viewModelScope.launch {
_uiState.update { it.copy(isRefreshing = true) }
settingsRepo.setPassesSettings(PassesSettings(hoursAhead, minElevation, modes))
_uiState.update { it.copy(hours = hoursAhead, elevation = minElevation, modes = modes) }
satelliteRepo.calculatePasses(System.currentTimeMillis(), hoursAhead, minElevation, modes)
_uiState.update { it.copy(isRefreshing = false) }
private fun applyFilter(
hoursAhead: Int,
minElevation: Double,
showDeepSpace: Boolean,
modes: List<String>
) = viewModelScope.launch {
settingsRepo.setPassesSettings(PassesSettings(showDeepSpace, hoursAhead, minElevation, modes))
_uiState.update {
it.copy(hours = hoursAhead, elevation = minElevation, showDeepSpace = showDeepSpace, modes = modes)
}
if (showDeepSpace) satelliteRepo.calculatePasses(System.currentTimeMillis(), hoursAhead, minElevation, modes)
}
private fun refreshPasses() = viewModelScope.launch {
_uiState.update { it.copy(isRefreshing = true) }
val (hoursAhead, minElevation, modes) = settingsRepo.passesSettings.value
val (_, hoursAhead, minElevation, modes) = settingsRepo.passesSettings.value
satelliteRepo.calculatePasses(System.currentTimeMillis(), hoursAhead, minElevation, modes)
_uiState.update { it.copy(isRefreshing = false) }
}
companion object {
val Factory: ViewModelProvider.Factory = viewModelFactory {
val applicationKey = ViewModelProvider.AndroidViewModelFactory.APPLICATION_KEY