Switched to MVI architecture for PassesScreen

This commit is contained in:
Arty Bishop committed 2025-12-27 15:01:45 +00:00
1 parent b9ade5f243
commit a088a7a0a2
20 files changed
+203 -236

No files matched your search

@@ -25,6 +25,7 @@ import com.rtbishop.look4sat.presentation.entries.EntriesViewModel
import com.rtbishop.look4sat.presentation.info.InfoScreen
import com.rtbishop.look4sat.presentation.map.MapScreen
import com.rtbishop.look4sat.presentation.passes.PassesScreen
import com.rtbishop.look4sat.presentation.passes.PassesViewModel
import com.rtbishop.look4sat.presentation.radar.RadarScreen
import com.rtbishop.look4sat.presentation.settings.SettingsScreen
@@ -73,10 +74,11 @@ private fun MainNavGraph(navController: NavHostController) {
EntriesScreen(viewModel.uiState.value, navToPasses)
}
composable(Screen.Passes.route) {
val viewModel = viewModel(PassesViewModel::class.java, factory = PassesViewModel.Factory)
val navToRadar = { catNum: Int, aosTime: Long ->
navController.navigate("${Screen.Radar.route}?catNum=${catNum}&aosTime=${aosTime}")
}
PassesScreen(navToRadar)
PassesScreen(viewModel.uiState.value, navToRadar)
}
composable(radarRoute, radarArgs) { RadarScreen() }
composable(Screen.Map.route) { MapScreen() }
@@ -38,7 +38,7 @@ fun TypesDialog(items: List<String>, selected: String, dismiss: () -> Unit, sele
dismiss()
}
Dialog(onDismissRequest = { dismiss() }) {
ElevatedCard(modifier = Modifier.fillMaxHeight(0.75f)) {
ElevatedCard(modifier = Modifier.fillMaxHeight(0.80f)) {
LazyVerticalGrid(
columns = GridCells.Adaptive(160.dp),
modifier = Modifier.background(MaterialTheme.colorScheme.background),
@@ -47,7 +47,8 @@ fun TypesDialog(items: List<String>, selected: String, dismiss: () -> Unit, sele
) {
itemsIndexed(items) { index, item ->
Surface {
Row(verticalAlignment = Alignment.CenterVertically,
Row(
verticalAlignment = Alignment.CenterVertically,
modifier = Modifier
.background(MaterialTheme.colorScheme.surface)
.clickable { clickAction(item) }) {
@@ -64,7 +65,7 @@ fun TypesDialog(items: List<String>, selected: String, dismiss: () -> Unit, sele
maxLines = 1,
overflow = TextOverflow.Ellipsis
)
RadioButton(selected = item == selected, onClick = { clickAction(item) })
RadioButton(selected = item == selected, onClick = null, modifier = Modifier.padding(8.dp))
}
}
}
@@ -46,18 +46,17 @@ import com.rtbishop.look4sat.presentation.components.CardLoadingIndicator
@Composable
fun EntriesScreen(uiState: EntriesState, navToPasses: () -> Unit) {
val showDialog = rememberSaveable { mutableStateOf(false) }
val toggleDialog = { showDialog.value = showDialog.value.not() }
if (showDialog.value) {
val toggleDialog = { uiState.takeAction(EntriesAction.ToggleTypesDialog) }
if (uiState.isDialogShown) {
TypesDialog(items = uiState.typesList, selected = uiState.currentType, toggleDialog) {
uiState.takeAction(EntriesAction.SelectType(it))
}
}
val unselectAll = { uiState.takeAction(EntriesAction.UnselectAll) }
val selectAll = { uiState.takeAction(EntriesAction.SelectAll) }
Column(modifier = Modifier.padding(6.dp), verticalArrangement = Arrangement.spacedBy(6.dp)) {
TopBar(setQuery = { newQuery: String -> uiState.takeAction(EntriesAction.SearchFor(newQuery)) },
TopBar(
setQuery = { newQuery: String -> uiState.takeAction(EntriesAction.SearchFor(newQuery)) },
saveSelection = {
uiState.takeAction(EntriesAction.SaveSelection)
navToPasses()
@@ -3,6 +3,7 @@ package com.rtbishop.look4sat.presentation.entries
import com.rtbishop.look4sat.domain.model.SatItem
data class EntriesState(
val isDialogShown: Boolean,
val isLoading: Boolean,
val itemsList: List<SatItem>,
val currentType: String,
@@ -16,5 +17,6 @@ sealed class EntriesAction {
data object SelectAll : EntriesAction()
data class SelectSingle(val id: Int, val isTicked: Boolean) : EntriesAction()
data class SelectType(val type: String) : EntriesAction()
data object ToggleTypesDialog : EntriesAction()
data object UnselectAll : EntriesAction()
}
@@ -17,6 +17,7 @@
*/
package com.rtbishop.look4sat.presentation.entries
import androidx.compose.runtime.State
import androidx.compose.runtime.mutableStateOf
import androidx.lifecycle.ViewModel
import androidx.lifecycle.ViewModelProvider
@@ -30,20 +31,23 @@ import kotlinx.coroutines.launch
class EntriesViewModel(private val selectionRepo: ISelectionRepo) : ViewModel() {
private val defaultUiState = EntriesState(
isLoading = true,
itemsList = emptyList(),
currentType = selectionRepo.getCurrentType(),
typesList = selectionRepo.getTypesList(),
takeAction = ::handleAction
private val _uiState = mutableStateOf(
EntriesState(
isDialogShown = false,
isLoading = true,
itemsList = emptyList(),
currentType = selectionRepo.getCurrentType(),
typesList = selectionRepo.getTypesList(),
takeAction = ::handleAction
)
)
val uiState = mutableStateOf(defaultUiState)
val uiState: State<EntriesState> = _uiState
init {
viewModelScope.launch {
delay(1000)
selectionRepo.getEntriesFlow().collect { items ->
uiState.value = uiState.value.copy(isLoading = false, itemsList = items)
_uiState.value = _uiState.value.copy(isLoading = false, itemsList = items)
}
}
}
@@ -55,6 +59,7 @@ class EntriesViewModel(private val selectionRepo: ISelectionRepo) : ViewModel()
EntriesAction.SelectAll -> selectAll(true)
is EntriesAction.SelectSingle -> selectSingle(action.id, action.isTicked)
is EntriesAction.SelectType -> selectType(action.type)
EntriesAction.ToggleTypesDialog -> toggleTypesDialog()
EntriesAction.UnselectAll -> selectAll(false)
}
}
@@ -73,7 +78,12 @@ class EntriesViewModel(private val selectionRepo: ISelectionRepo) : ViewModel()
private fun selectType(type: String) = viewModelScope.launch {
selectionRepo.setType(type)
uiState.value = uiState.value.copy(currentType = type)
_uiState.value = _uiState.value.copy(currentType = type)
}
private fun toggleTypesDialog() {
val currentDialogState = _uiState.value.isDialogShown
_uiState.value = _uiState.value.copy(isDialogShown = currentDialogState.not())
}
companion object {
@@ -39,7 +39,8 @@ import androidx.compose.material3.Surface
import androidx.compose.material3.Text
import androidx.compose.runtime.Composable
import androidx.compose.runtime.mutableIntStateOf
import androidx.compose.runtime.saveable.rememberSaveable
import androidx.compose.runtime.mutableStateListOf
import androidx.compose.runtime.remember
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.res.painterResource
@@ -54,27 +55,43 @@ import com.rtbishop.look4sat.R
import com.rtbishop.look4sat.presentation.MainTheme
import com.rtbishop.look4sat.presentation.components.CardButton
private val allModes = listOf(
"AFSK", "AFSK S-Net", "AFSK SALSAT", "AHRPT", "AM", "APT", "BPSK", "BPSK PMT-A3",
"CERTO", "CW", "DQPSK", "DSTAR", "DUV", "FFSK", "FM", "FMN", "FSK", "FSK AX.100 Mode 5",
"FSK AX.100 Mode 6", "FSK AX.25 G3RUH", "GFSK", "GFSK Rktr", "GMSK", "HRPT", "LoRa",
"LRPT", "LSB", "MFSK", "MSK", "MSK AX.100 Mode 5", "MSK AX.100 Mode 6", "OFDM", "OQPSK",
"PSK", "PSK31", "PSK63", "QPSK", "QPSK31", "QPSK63", "SSTV", "USB", "WSJT"
)
@Preview(showBackground = true)
@Composable
private fun FilterDialogPreview() {
MainTheme { FilterDialog(24, 16.0, {}) { _, _ -> } }
MainTheme { FilterDialog(24, 16.0, emptyList(), {}) { _, _, _ -> } }
}
@Composable
fun FilterDialog(hours: Int, elevation: Double, dismiss: () -> Unit, save: (Int, Double) -> Unit) {
val hoursValue = rememberSaveable { mutableIntStateOf(hours) }
val elevationValue = rememberSaveable { mutableIntStateOf(elevation.toInt()) }
fun FilterDialog(
hours: Int,
elev: Double,
modes: List<String>,
dismiss: () -> Unit,
save: (Int, Double, List<String>) -> Unit
) {
val maxWidthModifier = Modifier.fillMaxWidth(1f)
val hoursValue = remember { mutableIntStateOf(hours) }
val elevationValue = remember { mutableIntStateOf(elev.toInt()) }
val selected = remember { mutableStateListOf<String>().apply { addAll(modes) } }
val select = { mode: String -> if (selected.contains(mode)) selected.remove(mode) else selected.add(mode) }
Dialog(onDismissRequest = { dismiss() }) {
ElevatedCard {
ElevatedCard(modifier = Modifier.fillMaxHeight(0.80f)) {
Column(
horizontalAlignment = Alignment.CenterHorizontally,
verticalArrangement = Arrangement.spacedBy(6.dp),
modifier = maxWidthModifier.padding(12.dp)
) {
Text(text = "Filter passes", fontSize = 20.sp, color = MaterialTheme.colorScheme.primary)
Text(text = "Filter passes", fontSize = 18.sp, color = MaterialTheme.colorScheme.primary)
Row(horizontalArrangement = Arrangement.spacedBy(4.dp), verticalAlignment = Alignment.Bottom) {
Text(text = "Show passes within", fontSize = 18.sp, modifier = Modifier.weight(1f))
Text(text = "Show passes within", fontSize = 16.sp, modifier = Modifier.weight(1f))
Icon(
painter = painterResource(id = R.drawable.ic_time),
contentDescription = null,
@@ -85,7 +102,7 @@ fun FilterDialog(hours: Int, elevation: Double, dismiss: () -> Unit, save: (Int,
)
Text(
text = "${hoursValue.intValue}h",
fontSize = 20.sp,
fontSize = 18.sp,
fontWeight = FontWeight.Medium,
color = MaterialTheme.colorScheme.primary
)
@@ -97,7 +114,7 @@ fun FilterDialog(hours: Int, elevation: Double, dismiss: () -> Unit, save: (Int,
colors = SliderDefaults.colors(inactiveTrackColor = MaterialTheme.colorScheme.onSurfaceVariant)
)
Row(horizontalArrangement = Arrangement.spacedBy(4.dp), verticalAlignment = Alignment.Bottom) {
Text(text = "Show passes above", fontSize = 18.sp, modifier = Modifier.weight(1f))
Text(text = "Show passes above", fontSize = 16.sp, modifier = Modifier.weight(1f))
Icon(
painter = painterResource(id = R.drawable.ic_elevation),
contentDescription = null,
@@ -108,7 +125,7 @@ fun FilterDialog(hours: Int, elevation: Double, dismiss: () -> Unit, save: (Int,
)
Text(
text = "${elevationValue.intValue}°",
fontSize = 20.sp,
fontSize = 18.sp,
fontWeight = FontWeight.Medium,
color = MaterialTheme.colorScheme.primary
)
@@ -119,11 +136,47 @@ fun FilterDialog(hours: Int, elevation: Double, dismiss: () -> Unit, save: (Int,
valueRange = 0f..90f,
colors = SliderDefaults.colors(inactiveTrackColor = MaterialTheme.colorScheme.onSurfaceVariant)
)
LazyVerticalGrid(
columns = GridCells.Adaptive(160.dp),
modifier = Modifier
.background(MaterialTheme.colorScheme.background)
.weight(1f),
horizontalArrangement = Arrangement.spacedBy(1.dp),
verticalArrangement = Arrangement.spacedBy(1.dp)
) {
itemsIndexed(allModes) { index, item ->
Surface {
Row(verticalAlignment = Alignment.CenterVertically,
modifier = Modifier
.background(MaterialTheme.colorScheme.surface)
.clickable { select(item) }) {
Text(
text = "$index).",
modifier = Modifier.padding(start = 8.dp, end = 6.dp),
fontWeight = FontWeight.Normal,
color = MaterialTheme.colorScheme.secondary
)
Text(
text = item,
modifier = Modifier.weight(1f),
fontWeight = FontWeight.Medium,
maxLines = 1,
overflow = TextOverflow.Ellipsis
)
Checkbox(
checked = selected.contains(item),
onCheckedChange = null,
modifier = Modifier.padding(8.dp)
)
}
}
}
}
Row(horizontalArrangement = Arrangement.SpaceBetween, modifier = Modifier.fillMaxWidth()) {
CardButton(onClick = { dismiss() }, text = stringResource(id = R.string.btn_cancel))
CardButton(
onClick = {
save(hoursValue.intValue, elevationValue.intValue.toDouble())
save(hoursValue.intValue, elevationValue.intValue.toDouble(), selected)
dismiss()
}, text = stringResource(id = R.string.btn_accept)
)
@@ -132,54 +185,3 @@ fun FilterDialog(hours: Int, elevation: Double, dismiss: () -> Unit, save: (Int,
}
}
}
@Preview(showBackground = true)
@Composable
private fun ModesDialogPreview() {
val allModes = listOf(
"AFSK", "AFSK S-Net", "AFSK SALSAT", "AHRPT", "AM", "APT", "BPSK", "BPSK PMT-A3",
"CERTO", "CW", "DQPSK", "DSTAR", "DUV", "FFSK", "FM", "FMN", "FSK", "FSK AX.100 Mode 5",
"FSK AX.100 Mode 6", "FSK AX.25 G3RUH", "GFSK", "GFSK Rktr", "GMSK", "HRPT", "LoRa",
"LRPT", "LSB", "MFSK", "MSK", "MSK AX.100 Mode 5", "MSK AX.100 Mode 6", "OFDM", "OQPSK",
"PSK", "PSK31", "PSK63", "QPSK", "QPSK31", "QPSK63", "SSTV", "USB", "WSJT"
)
MainTheme { ModesDialog(allModes, listOf("AFSK", "AFSK S-Net"), {}) {} }
}
@Composable
fun ModesDialog(items: List<String>, selected: List<String>, dismiss: () -> Unit, select: (String) -> Unit) {
Dialog(onDismissRequest = { dismiss() }) {
ElevatedCard(modifier = Modifier.fillMaxHeight(0.75f)) {
LazyVerticalGrid(
columns = GridCells.Adaptive(160.dp),
modifier = Modifier.background(MaterialTheme.colorScheme.background),
horizontalArrangement = Arrangement.spacedBy(1.dp),
verticalArrangement = Arrangement.spacedBy(1.dp)
) {
itemsIndexed(items) { index, item ->
Surface {
Row(verticalAlignment = Alignment.CenterVertically,
modifier = Modifier
.background(MaterialTheme.colorScheme.surface)
.clickable { select(item) }) {
Text(
text = "$index).",
modifier = Modifier.padding(start = 12.dp, end = 6.dp),
fontWeight = FontWeight.Normal,
color = MaterialTheme.colorScheme.secondary
)
Text(
text = item,
modifier = Modifier.weight(1f),
fontWeight = FontWeight.Medium,
maxLines = 1,
overflow = TextOverflow.Ellipsis
)
Checkbox(checked = selected.contains(item), onCheckedChange = { select(item) })
}
}
}
}
}
}
}
@@ -23,9 +23,6 @@ import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.Surface
import androidx.compose.material3.Text
import androidx.compose.runtime.Composable
import androidx.compose.runtime.collectAsState
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.saveable.rememberSaveable
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.res.painterResource
@@ -36,9 +33,7 @@ import androidx.compose.ui.text.style.TextOverflow
import androidx.compose.ui.tooling.preview.Preview
import androidx.compose.ui.unit.dp
import androidx.compose.ui.unit.sp
import androidx.lifecycle.viewmodel.compose.viewModel
import com.rtbishop.look4sat.R
import com.rtbishop.look4sat.domain.model.DataState
import com.rtbishop.look4sat.domain.predict.NearEarthSatellite
import com.rtbishop.look4sat.domain.predict.OrbitalData
import com.rtbishop.look4sat.domain.predict.SatPass
@@ -53,27 +48,16 @@ import java.util.*
private val sdf = SimpleDateFormat("HH:mm:ss", Locale.ENGLISH)
@Composable
fun PassesScreen(navToRadar: (Int, Long) -> Unit) {
val viewModel = viewModel(PassesViewModel::class.java, factory = PassesViewModel.Factory)
val state = viewModel.passes.collectAsState(initial = null)
val timerText = viewModel.timerText.collectAsState(initial = null)
val isRefreshing = state.value is DataState.Loading
val refreshState = rememberPullRefreshState(refreshing = isRefreshing,
onRefresh = { viewModel.calculatePasses() })
val value = state.value
var passes = emptyList<SatPass>()
if (value is DataState.Success) passes = value.data
val showDialog = rememberSaveable { mutableStateOf(false) }
val toggleDialog = { showDialog.value = showDialog.value.not() }
if (showDialog.value) {
val (hoursAhead, minElevation) = viewModel.getFilterSettings()
FilterDialog(hoursAhead, minElevation, { toggleDialog() }) { newHours, newElevation ->
viewModel.calculatePasses(System.currentTimeMillis(), newHours, newElevation)
fun PassesScreen(uiState: PassesState, navToRadar: (Int, Long) -> Unit) {
val refreshState = rememberPullRefreshState(refreshing = uiState.isRefreshing, onRefresh = {
uiState.takeAction(PassesAction.RefreshPasses)
})
val toggleDialog = { uiState.takeAction(PassesAction.ToggleFilterDialog) }
if (uiState.isDialogShown) {
FilterDialog(uiState.hours, uiState.elevation, uiState.modes, toggleDialog) { hours, elevation, modes ->
uiState.takeAction(PassesAction.ApplyFilter(hours, elevation, modes))
}
}
Column(modifier = Modifier.padding(6.dp), verticalArrangement = Arrangement.spacedBy(6.dp)) {
ElevatedCard(modifier = Modifier.height(52.dp)) {
Row(
@@ -88,11 +72,11 @@ fun PassesScreen(navToRadar: (Int, Long) -> Unit) {
)
}
Column(verticalArrangement = Arrangement.SpaceEvenly, modifier = Modifier.weight(1f)) {
Text(text = timerText.value?.first ?: "Null")
Text(text = timerText.value?.second ?: "Null")
Text(text = "Next - Id:${uiState.nextId}", maxLines = 1, overflow = TextOverflow.Ellipsis)
Text(text = uiState.nextName, maxLines = 1, overflow = TextOverflow.Ellipsis)
}
Text(
text = timerText.value?.third ?: "00:00:00",
text = uiState.nextTime,
fontSize = 36.sp,
fontWeight = FontWeight.Bold,
color = MaterialTheme.colorScheme.primary,
@@ -103,7 +87,7 @@ fun PassesScreen(navToRadar: (Int, Long) -> Unit) {
)
}
}
PassesCard(refreshState, isRefreshing, passes, navToRadar)
PassesCard(refreshState, uiState.isRefreshing, uiState.itemsList, navToRadar)
}
}
@@ -0,0 +1,23 @@
package com.rtbishop.look4sat.presentation.passes
import com.rtbishop.look4sat.domain.predict.SatPass
data class PassesState(
val isDialogShown: Boolean,
val isRefreshing: Boolean,
val isUtc: Boolean,
val nextId: Int,
val nextName: String,
val nextTime: String,
val hours: Int,
val elevation: Double,
val modes: List<String>,
val itemsList: List<SatPass>,
val takeAction: (PassesAction) -> Unit
)
sealed class PassesAction {
data class ApplyFilter(val hoursAhead: Int, val minElevation: Double, val modes: List<String>) : PassesAction()
data object RefreshPasses : PassesAction()
data object ToggleFilterDialog : PassesAction()
}
@@ -17,13 +17,14 @@
*/
package com.rtbishop.look4sat.presentation.passes
import androidx.compose.runtime.State
import androidx.compose.runtime.mutableStateOf
import androidx.lifecycle.ViewModel
import androidx.lifecycle.ViewModelProvider
import androidx.lifecycle.viewModelScope
import androidx.lifecycle.viewmodel.initializer
import androidx.lifecycle.viewmodel.viewModelFactory
import com.rtbishop.look4sat.MainApplication
import com.rtbishop.look4sat.domain.model.DataState
import com.rtbishop.look4sat.domain.model.PassesSettings
import com.rtbishop.look4sat.domain.predict.SatPass
import com.rtbishop.look4sat.domain.repository.ISatelliteRepo
@@ -32,8 +33,6 @@ import com.rtbishop.look4sat.domain.utility.toTimerString
import kotlinx.coroutines.Job
import kotlinx.coroutines.cancelAndJoin
import kotlinx.coroutines.delay
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.StateFlow
import kotlinx.coroutines.isActive
import kotlinx.coroutines.launch
@@ -41,46 +40,35 @@ class PassesViewModel(
private val satelliteRepo: ISatelliteRepo, private val settingsRepo: ISettingsRepo
) : ViewModel() {
private val _passes = MutableStateFlow<DataState<List<SatPass>>>(DataState.Loading)
private val timerData = Triple("Next - Id:Null", "Name: Null", "00:00:00")
private val _timerText = MutableStateFlow(timerData)
private var passesProcessing: Job? = null
val passes: StateFlow<DataState<List<SatPass>>> = _passes
val timerText: StateFlow<Triple<String, String, String>?> = _timerText
fun getFilterSettings() = settingsRepo.passesSettings.value
// fun shouldUseUTC() = settingsRepo.otherSettings.value.utcState
private val _uiState = mutableStateOf(
PassesState(
isDialogShown = false,
isRefreshing = true,
isUtc = settingsRepo.otherSettings.value.stateOfUtc,
nextId = 0,
nextName = "...",
nextTime = "00:00:00",
hours = settingsRepo.passesSettings.value.hoursAhead,
elevation = settingsRepo.passesSettings.value.minElevation,
modes = settingsRepo.passesSettings.value.selectedModes,
itemsList = emptyList(),
takeAction = ::handleAction
)
)
private var processing: Job? = null
val uiState: State<PassesState> = _uiState
init {
viewModelScope.launch { satelliteRepo.initRepository() }
viewModelScope.launch {
satelliteRepo.passes.collect { passes ->
passesProcessing?.cancelAndJoin()
passesProcessing = viewModelScope.launch {
processing?.cancelAndJoin()
processing = viewModelScope.launch {
while (isActive) {
val timeNow = System.currentTimeMillis()
val newPasses = satelliteRepo.processPasses(passes, timeNow)
if (newPasses.isNotEmpty()) {
try {
val nextPass = newPasses.first { it.aosTime.minus(timeNow) > 0 }
val catNum = nextPass.catNum
val name = nextPass.name
val millisBeforeStart = nextPass.aosTime.minus(timeNow)
val timerString = millisBeforeStart.toTimerString()
_timerText.emit(Triple("Next - Id:$catNum", name, timerString))
} catch (exception: NoSuchElementException) {
val lastPass = newPasses.last()
val catNum = lastPass.catNum
val name = lastPass.name
val millisBeforeEnd = lastPass.losTime.minus(timeNow)
val timerString = millisBeforeEnd.toTimerString()
_timerText.emit(Triple("Next - Id:$catNum", name, timerString))
}
} else _timerText.emit(timerData)
_passes.emit(DataState.Success(newPasses))
setPassInfo(newPasses, timeNow)
_uiState.value = _uiState.value.copy(isRefreshing = false, itemsList = newPasses)
delay(1000)
}
}
@@ -88,18 +76,45 @@ class PassesViewModel(
}
}
fun calculatePasses() = viewModelScope.launch {
_passes.emit(DataState.Loading)
passesProcessing?.cancelAndJoin()
private fun handleAction(action: PassesAction) {
when (action) {
is PassesAction.ApplyFilter -> applyFilter(action.hoursAhead, action.minElevation, action.modes)
PassesAction.RefreshPasses -> refreshPasses()
PassesAction.ToggleFilterDialog -> toggleFilterDialog()
}
}
private fun applyFilter(hoursAhead: Int, minElevation: Double, modes: List<String>) = viewModelScope.launch {
_uiState.value = _uiState.value.copy(isRefreshing = true)
processing?.cancelAndJoin()
settingsRepo.setPassesSettings(PassesSettings(hoursAhead, minElevation, modes))
_uiState.value = _uiState.value.copy(hours = hoursAhead, elevation = minElevation, modes = modes)
satelliteRepo.calculatePasses(System.currentTimeMillis(), hoursAhead, minElevation)
}
private fun refreshPasses() = viewModelScope.launch {
_uiState.value = _uiState.value.copy(isRefreshing = true)
processing?.cancelAndJoin()
val (hoursAhead, minElevation) = settingsRepo.passesSettings.value
satelliteRepo.calculatePasses(System.currentTimeMillis(), hoursAhead, minElevation)
}
fun calculatePasses(timeRef: Long, hoursAhead: Int, minElev: Double) = viewModelScope.launch {
_passes.emit(DataState.Loading)
passesProcessing?.cancelAndJoin()
settingsRepo.updatePassesSettings(PassesSettings(hoursAhead, minElev))
satelliteRepo.calculatePasses(timeRef, hoursAhead, minElev)
private fun setPassInfo(passes: List<SatPass>, timeNow: Long) {
if (passes.isEmpty()) return
try {
val nextPass = passes.first { it.aosTime.minus(timeNow) > 0 }
val time = nextPass.aosTime.minus(timeNow).toTimerString()
_uiState.value = _uiState.value.copy(nextId = nextPass.catNum, nextName = nextPass.name, nextTime = time)
} catch (exception: NoSuchElementException) {
val lastPass = passes.last()
val time = lastPass.losTime.minus(timeNow).toTimerString()
_uiState.value = _uiState.value.copy(nextId = lastPass.catNum, nextName = lastPass.name, nextTime = time)
}
}
private fun toggleFilterDialog() {
val currentDialogState = _uiState.value.isDialogShown
_uiState.value = _uiState.value.copy(isDialogShown = currentDialogState.not())
}
companion object {
@@ -148,7 +148,7 @@ class RadarViewModel(
private fun processRadios(radios: List<SatRadio>, satellite: Satellite, time: Long) {
viewModelScope.launch {
delay(125)
val list = satelliteRepo.processRadios(satellite, stationPos, radios, time)
val list = satelliteRepo.getRadios(satellite, stationPos, radios, time)
transmitters.value = list
}
}
@@ -59,9 +59,7 @@ class SatelliteRepo(
return withContext(dispatcher) { sat.getPosition(pos, time) }
}
override suspend fun getTrack(
sat: Satellite, pos: GeoPos, start: Long, end: Long
): List<SatPos> {
override suspend fun getTrack(sat: Satellite, pos: GeoPos, start: Long, end: Long): List<SatPos> {
return withContext(dispatcher) {
val positions = mutableListOf<SatPos>()
var currentTime = start
@@ -73,9 +71,7 @@ class SatelliteRepo(
}
}
override suspend fun processRadios(
sat: Satellite, pos: GeoPos, radios: List<SatRadio>, time: Long
): List<SatRadio> {
override suspend fun getRadios(sat: Satellite, pos: GeoPos, radios: List<SatRadio>, time: Long): List<SatRadio> {
return withContext(dispatcher) {
val satPos = sat.getPosition(pos, time)
val copiedList = radios.map { it.copy() }
@@ -82,26 +82,19 @@ class SettingsRepo(private val manager: LocationManager, private val preferences
private val _passesSettings = MutableStateFlow(getPassesSettings())
override val passesSettings: StateFlow<PassesSettings> = _passesSettings
override fun getSelectedModes(): List<String> {
val selectedModesString = preferences.getString(keySelectedModes, null)
return selectedModesString?.split(separatorComma)?.sorted() ?: emptyList()
}
override fun setSelectedModes(modes: List<String>) {
val selectedModes = modes.joinToString(separatorComma)
preferences.edit { putString(keySelectedModes, selectedModes) }
}
override fun updatePassesSettings(settings: PassesSettings) = preferences.edit {
putInt(keyFilterHoursAhead, settings.filterHoursAhead)
putLong(keyFilterMinElevation, settings.filterMinElevation.toRawBits())
override fun setPassesSettings(settings: PassesSettings) = preferences.edit {
putInt(keyFilterHoursAhead, settings.hoursAhead)
putLong(keyFilterMinElevation, settings.minElevation.toRawBits())
putString(keySelectedModes, settings.selectedModes.joinToString(separatorComma))
_passesSettings.value = settings
}
private fun getPassesSettings(): PassesSettings {
val hoursAhead = preferences.getInt(keyFilterHoursAhead, 24)
val minElevation = Double.fromBits(preferences.getLong(keyFilterMinElevation, 16.0.toRawBits()))
return PassesSettings(hoursAhead, minElevation)
val selectedModesString = preferences.getString(keySelectedModes, null)
val selectedModes = selectedModesString?.split(separatorComma)?.sorted() ?: emptyList()
return PassesSettings(hoursAhead, minElevation, selectedModes)
}
//endregion
@@ -1,24 +0,0 @@
/*
* Look4Sat. Amateur radio satellite tracker and pass predictor.
* Copyright (C) 2019-2022 Arty Bishop (bishop.arty@gmail.com)
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
package com.rtbishop.look4sat.domain.model
sealed class DataState<out T> {
data class Success<out T>(val data: T) : DataState<T>()
data class Exception(val message: String?) : DataState<Nothing>()
data object Loading : DataState<Nothing>()
}
@@ -2,7 +2,7 @@ package com.rtbishop.look4sat.domain.model
data class DatabaseState(val numberOfRadios: Int, val numberOfSatellites: Int, val updateTimestamp: Long)
data class PassesSettings(val filterHoursAhead: Int, val filterMinElevation: Double)
data class PassesSettings(val hoursAhead: Int, val minElevation: Double, val selectedModes: List<String>)
data class OtherSettings(
val stateOfAutoUpdate: Boolean,
@@ -18,10 +18,7 @@
package com.rtbishop.look4sat.domain.repository
interface IDatabaseRepo {
suspend fun updateFromFile(uri: String)
suspend fun updateFromRemote()
suspend fun clearAllData()
}
@@ -8,24 +8,13 @@ import com.rtbishop.look4sat.domain.predict.Satellite
import kotlinx.coroutines.flow.StateFlow
interface ISatelliteRepo {
val passes: StateFlow<List<SatPass>>
val satellites: StateFlow<List<Satellite>>
suspend fun getRadiosWithId(id: Int): List<SatRadio>
suspend fun initRepository()
suspend fun getPosition(sat: Satellite, pos: GeoPos, time: Long): SatPos
suspend fun getTrack(sat: Satellite, pos: GeoPos, start: Long, end: Long): List<SatPos>
suspend fun processRadios(
sat: Satellite, pos: GeoPos, radios: List<SatRadio>, time: Long
): List<SatRadio>
suspend fun getRadios(sat: Satellite, pos: GeoPos, radios: List<SatRadio>, time: Long): List<SatRadio>
suspend fun processPasses(passList: List<SatPass>, time: Long): List<SatPass>
suspend fun calculatePasses(time: Long, hoursAhead: Int = 8, minElevation: Double = 16.0)
}
@@ -4,20 +4,12 @@ import com.rtbishop.look4sat.domain.model.SatItem
import kotlinx.coroutines.flow.Flow
interface ISelectionRepo {
fun getCurrentType(): String
fun getTypesList(): List<String>
suspend fun getEntriesFlow(): Flow<List<SatItem>>
suspend fun setType(type: String)
suspend fun setQuery(query: String)
suspend fun setSelection(selectAll: Boolean)
suspend fun setSelection(ids: List<Int>, isTicked: Boolean)
suspend fun saveSelection()
}
@@ -63,9 +63,7 @@ interface ISettingsRepo {
//region # Passes filter settings
val passesSettings: StateFlow<PassesSettings>
fun getSelectedModes(): List<String>
fun setSelectedModes(modes: List<String>)
fun updatePassesSettings(settings: PassesSettings)
fun setPassesSettings(settings: PassesSettings)
//endregion
//region # Station position settings
@@ -23,22 +23,13 @@ import com.rtbishop.look4sat.domain.model.SatRadio
import com.rtbishop.look4sat.domain.predict.Satellite
interface ILocalSource {
suspend fun getEntriesTotal(): Int
suspend fun getEntriesList(): List<SatItem>
suspend fun getEntriesWithIds(ids: List<Int>): List<Satellite>
suspend fun insertEntries(entries: List<SatEntry>)
suspend fun deleteEntries()
suspend fun getRadiosTotal(): Int
suspend fun getRadiosWithId(id: Int): List<SatRadio>
suspend fun insertRadios(radios: List<SatRadio>)
suspend fun deleteRadios()
}
@@ -3,10 +3,7 @@ package com.rtbishop.look4sat.domain.source
import kotlinx.coroutines.flow.StateFlow
interface ISensorSource {
val orientation: StateFlow<Pair<Float, Float>>
fun enableSensor()
fun disableSensor()
}