mirror of
https://github.com/rt-bishop/Look4Sat.git
synced 2026-10-02 03:15:37 +00:00
Switched to MVI architecture for PassesScreen
This commit is contained in:
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()
|
||||
}
|
||||
Reference in new issue
Block a user