Added compose pull to refresh for passes list

This commit is contained in:
Arty Bishop committed 2025-12-27 15:01:32 +00:00
1 parent 1ea4da3baf
commit a08cfc7fb8
11 files changed
+157 -44

No files matched your search

@@ -11,17 +11,16 @@ import androidx.compose.foundation.clickable
import androidx.compose.foundation.interaction.Interaction
import androidx.compose.foundation.interaction.MutableInteractionSource
import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.layout.size
import androidx.compose.foundation.shape.CircleShape
import androidx.compose.material3.ButtonDefaults
import androidx.compose.material3.ElevatedButton
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.Text
import androidx.compose.material3.*
import androidx.compose.runtime.Composable
import androidx.compose.runtime.LaunchedEffect
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.composed
import androidx.compose.ui.draw.scale
@@ -40,6 +39,13 @@ fun CardButton(onClick: () -> Unit, text: String, modifier: Modifier = Modifier)
) { Text(text = text, fontSize = 17.sp) }
}
@Composable
fun CardLoadingIndicator() {
Box(contentAlignment = Alignment.Center, modifier = Modifier.fillMaxSize()) {
CircularProgressIndicator(modifier = Modifier.size(80.dp))
}
}
@Composable
fun RadarPing() {
val pingColor = MaterialTheme.colorScheme.primary
@@ -29,6 +29,7 @@ import androidx.hilt.navigation.compose.hiltViewModel
import com.rtbishop.look4sat.R
import com.rtbishop.look4sat.domain.model.DataState
import com.rtbishop.look4sat.domain.model.SatItem
import com.rtbishop.look4sat.presentation.CardLoadingIndicator
import com.rtbishop.look4sat.presentation.MainTheme
import com.rtbishop.look4sat.presentation.onClick
import com.rtbishop.look4sat.presentation.passesScreen.PassesViewModel
@@ -229,11 +230,7 @@ fun EntriesCard(state: DataState<List<SatItem>>, onSelected: (List<Int>, Boolean
}
}
}
else -> {
Box(contentAlignment = Alignment.Center, modifier = Modifier.fillMaxSize()) {
CircularProgressIndicator(modifier = Modifier.size(80.dp))
}
}
else -> CardLoadingIndicator()
}
}
}
@@ -1,3 +1,5 @@
@file:OptIn(ExperimentalMaterialApi::class)
package com.rtbishop.look4sat.presentation.passesScreen
import androidx.compose.foundation.ExperimentalFoundationApi
@@ -5,6 +7,11 @@ import androidx.compose.foundation.background
import androidx.compose.foundation.layout.*
import androidx.compose.foundation.lazy.LazyColumn
import androidx.compose.foundation.lazy.items
import androidx.compose.material.ExperimentalMaterialApi
import androidx.compose.material.pullrefresh.PullRefreshIndicator
import androidx.compose.material.pullrefresh.PullRefreshState
import androidx.compose.material.pullrefresh.pullRefresh
import androidx.compose.material.pullrefresh.rememberPullRefreshState
import androidx.compose.material3.*
import androidx.compose.runtime.Composable
import androidx.compose.runtime.livedata.observeAsState
@@ -16,7 +23,7 @@ import androidx.compose.ui.text.font.FontWeight
import androidx.compose.ui.text.style.TextAlign
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.*
import androidx.hilt.navigation.compose.hiltViewModel
import com.rtbishop.look4sat.R
import com.rtbishop.look4sat.domain.model.DataState
@@ -29,11 +36,52 @@ import java.util.*
private val sdf = SimpleDateFormat("HH:mm:ss", Locale.ENGLISH)
@OptIn(ExperimentalMaterialApi::class)
@Composable
fun PassesScreen(viewModel: PassesViewModel = hiltViewModel()) {
val state = viewModel.passes.observeAsState(DataState.Loading)
val state = viewModel.passes.observeAsState()
val timerText = viewModel.timerText.observeAsState()
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
Column(modifier = Modifier.padding(6.dp), verticalArrangement = Arrangement.spacedBy(6.dp)) {
PassesCard(state = state.value)
ElevatedCard(modifier = Modifier.height(52.dp)) {
Row(
verticalAlignment = Alignment.CenterVertically,
horizontalArrangement = Arrangement.SpaceBetween,
modifier = Modifier.fillMaxSize()
) {
IconButton(onClick = {}) {
Icon(
painter = painterResource(id = R.drawable.ic_filter),
contentDescription = null
)
}
Column(
verticalArrangement = Arrangement.SpaceEvenly, modifier = Modifier.weight(1f)
) {
Text(text = timerText.value?.first ?: "Null")
Text(text = timerText.value?.second ?: "Null")
}
Text(
text = timerText.value?.third ?: "00:00:00",
fontSize = 36.sp,
fontWeight = FontWeight.Bold,
color = MaterialTheme.colorScheme.primary,
textAlign = TextAlign.End,
modifier = Modifier
.weight(1f)
.padding(bottom = 2.dp, end = 8.dp)
)
}
}
PassesCard(refreshState, isRefreshing, passes)
}
}
@@ -51,7 +99,7 @@ private fun PassPreview() {
@Composable
private fun Pass(pass: SatPass, modifier: Modifier = Modifier) {
Surface(color = MaterialTheme.colorScheme.background, modifier = modifier) {
Surface(modifier = Modifier.padding(bottom = 1.dp)) {
Surface(modifier = Modifier.padding(bottom = 2.dp)) {
Column(
verticalArrangement = Arrangement.spacedBy(2.dp),
modifier = Modifier
@@ -60,8 +108,8 @@ private fun Pass(pass: SatPass, modifier: Modifier = Modifier) {
) {
Row(verticalAlignment = Alignment.CenterVertically) {
Text(
text = "Id:${pass.catNum} - ",
modifier = Modifier.width(90.dp),
text = "Id:${pass.catNum} - ",
modifier = Modifier.width(82.dp),
textAlign = TextAlign.End,
color = MaterialTheme.colorScheme.primary
)
@@ -91,13 +139,20 @@ private fun Pass(pass: SatPass, modifier: Modifier = Modifier) {
horizontalArrangement = Arrangement.SpaceBetween,
modifier = Modifier.fillMaxWidth()
) {
Text(text = stringResource(id = R.string.pass_aosAz, pass.aosAzimuth))
Text(
text = stringResource(id = R.string.pass_aosAz, pass.aosAzimuth),
fontSize = 15.sp
)
Text(
text = stringResource(id = R.string.pass_altitude, pass.altitude),
textAlign = TextAlign.Center,
fontSize = 15.sp,
modifier = Modifier.weight(1f)
)
Text(text = stringResource(id = R.string.pass_losAz, pass.losAzimuth))
Text(
text = stringResource(id = R.string.pass_losAz, pass.losAzimuth),
fontSize = 15.sp
)
}
Row(
verticalAlignment = Alignment.CenterVertically,
@@ -105,14 +160,22 @@ private fun Pass(pass: SatPass, modifier: Modifier = Modifier) {
modifier = Modifier.fillMaxWidth()
) {
val deepTime = stringResource(id = R.string.pass_placeholder)
Text(text = if (pass.isDeepSpace) deepTime else sdf.format(Date(pass.aosTime)))
Text(
text = if (pass.isDeepSpace) deepTime else sdf.format(Date(pass.aosTime)),
fontSize = 15.sp
)
LinearProgressIndicator(
progress = pass.progress,
modifier = modifier.weight(1f),
progress = if (pass.isDeepSpace) 1f else pass.progress,
modifier = modifier
.fillMaxWidth(0.75f)
.padding(top = 3.dp),
color = MaterialTheme.colorScheme.primary,
trackColor = MaterialTheme.colorScheme.inverseSurface
)
Text(text = if (pass.isDeepSpace) deepTime else sdf.format(Date(pass.losTime)))
Text(
text = if (pass.isDeepSpace) deepTime else sdf.format(Date(pass.losTime)),
fontSize = 15.sp
)
}
}
}
@@ -121,23 +184,22 @@ private fun Pass(pass: SatPass, modifier: Modifier = Modifier) {
@OptIn(ExperimentalFoundationApi::class)
@Composable
private fun PassesCard(state: DataState<List<SatPass>>) {
private fun PassesCard(
refreshState: PullRefreshState, isRefreshing: Boolean, passes: List<SatPass>
) {
ElevatedCard(modifier = Modifier.fillMaxSize()) {
when (state) {
is DataState.Success -> {
LazyColumn {
items(
items = state.data,
key = { item -> item.catNum + item.aosTime }) { pass ->
Pass(pass, Modifier.animateItemPlacement())
}
}
}
else -> {
Box(contentAlignment = Alignment.Center, modifier = Modifier.fillMaxSize()) {
CircularProgressIndicator(modifier = Modifier.size(80.dp))
Box(Modifier.pullRefresh(refreshState)) {
LazyColumn(modifier = Modifier.fillMaxSize()) {
items(items = passes, key = { item -> item.catNum + item.aosTime }) { pass ->
Pass(pass, Modifier.animateItemPlacement())
}
}
PullRefreshIndicator(
refreshing = isRefreshing,
state = refreshState,
modifier = Modifier.align(Alignment.TopCenter),
backgroundColor = MaterialTheme.colorScheme.primary
)
}
}
}
@@ -23,6 +23,7 @@ import com.rtbishop.look4sat.domain.ISatelliteManager
import com.rtbishop.look4sat.domain.ISettingsManager
import com.rtbishop.look4sat.domain.model.DataState
import com.rtbishop.look4sat.domain.predict.SatPass
import com.rtbishop.look4sat.utility.toTimerString
import dagger.hilt.android.lifecycle.HiltViewModel
import kotlinx.coroutines.*
import javax.inject.Inject
@@ -34,10 +35,13 @@ class PassesViewModel @Inject constructor(
private val settings: ISettingsManager
) : ViewModel() {
private val _passes = MutableLiveData<DataState<List<SatPass>>>()
private val timerData = Triple("Next - Id:Null", "Name: Null", "00:00:00")
private val _timerText = MutableLiveData<Triple<String, String, String>>()
private var passesProcessing: Job? = null
private val _passes = MutableLiveData<DataState<List<SatPass>>>(DataState.Loading)
val passes: LiveData<DataState<List<SatPass>>> = _passes
val entriesTotal: LiveData<Int> = repository.getEntriesTotal().asLiveData()
val passes: LiveData<DataState<List<SatPass>>> = _passes
val timerText: LiveData<Triple<String, String, String>> = _timerText
init {
viewModelScope.launch {
@@ -45,8 +49,27 @@ class PassesViewModel @Inject constructor(
passesProcessing?.cancelAndJoin()
passesProcessing = viewModelScope.launch {
while (isActive) {
val time = System.currentTimeMillis()
val newPasses = satelliteManager.processPasses(passes, time)
val timeNow = System.currentTimeMillis()
val newPasses = satelliteManager.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.postValue(Triple("Next - Id:$catNum", name, timerString))
} catch (e: NoSuchElementException) {
val lastPass = newPasses.last()
val catNum = lastPass.catNum
val name = lastPass.name
val millisBeforeEnd = lastPass.losTime.minus(timeNow)
val timerString = millisBeforeEnd.toTimerString()
_timerText.postValue(Triple("Next - Id:$catNum", name, timerString))
}
} else _timerText.postValue(timerData)
_passes.postValue(DataState.Success(newPasses))
delay(1000)
}
@@ -71,6 +94,7 @@ class PassesViewModel @Inject constructor(
) {
viewModelScope.launch {
_passes.postValue(DataState.Loading)
// _timerText.postValue(timerDefaultText)
passesProcessing?.cancelAndJoin()
selection?.let { items -> settings.saveEntriesSelection(items) }
settings.setHoursAhead(hoursAhead)
@@ -78,7 +102,9 @@ class PassesViewModel @Inject constructor(
val stationPos = settings.loadStationPosition()
val selectedIds = settings.loadEntriesSelection()
val satellites = repository.getEntriesWithIds(selectedIds)
satelliteManager.calculatePasses(satellites, stationPos, timeRef, hoursAhead, minElevation)
satelliteManager.calculatePasses(
satellites, stationPos, timeRef, hoursAhead, minElevation
)
}
}
}