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
+155 -42

No files matched your search

+1
View File
@@ -89,6 +89,7 @@ dependencies {
implementation "androidx.activity:activity-compose:$activity_compose_version" implementation "androidx.activity:activity-compose:$activity_compose_version"
implementation "androidx.compose.animation:animation:$compose_version" implementation "androidx.compose.animation:animation:$compose_version"
implementation "androidx.compose.compiler:compiler:$compose_compiler_version" implementation "androidx.compose.compiler:compiler:$compose_compiler_version"
implementation "androidx.compose.material:material:$material_version"
implementation "androidx.compose.material3:material3:$material3_version" implementation "androidx.compose.material3:material3:$material3_version"
implementation "androidx.compose.runtime:runtime:$compose_version" implementation "androidx.compose.runtime:runtime:$compose_version"
implementation "androidx.compose.runtime:runtime-livedata:$compose_version" implementation "androidx.compose.runtime:runtime-livedata:$compose_version"
@@ -11,17 +11,16 @@ import androidx.compose.foundation.clickable
import androidx.compose.foundation.interaction.Interaction import androidx.compose.foundation.interaction.Interaction
import androidx.compose.foundation.interaction.MutableInteractionSource import androidx.compose.foundation.interaction.MutableInteractionSource
import androidx.compose.foundation.layout.Box import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.layout.padding import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.layout.size import androidx.compose.foundation.layout.size
import androidx.compose.foundation.shape.CircleShape import androidx.compose.foundation.shape.CircleShape
import androidx.compose.material3.ButtonDefaults import androidx.compose.material3.*
import androidx.compose.material3.ElevatedButton
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.Text
import androidx.compose.runtime.Composable import androidx.compose.runtime.Composable
import androidx.compose.runtime.LaunchedEffect import androidx.compose.runtime.LaunchedEffect
import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember import androidx.compose.runtime.remember
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier import androidx.compose.ui.Modifier
import androidx.compose.ui.composed import androidx.compose.ui.composed
import androidx.compose.ui.draw.scale 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) } ) { Text(text = text, fontSize = 17.sp) }
} }
@Composable
fun CardLoadingIndicator() {
Box(contentAlignment = Alignment.Center, modifier = Modifier.fillMaxSize()) {
CircularProgressIndicator(modifier = Modifier.size(80.dp))
}
}
@Composable @Composable
fun RadarPing() { fun RadarPing() {
val pingColor = MaterialTheme.colorScheme.primary val pingColor = MaterialTheme.colorScheme.primary
@@ -29,6 +29,7 @@ import androidx.hilt.navigation.compose.hiltViewModel
import com.rtbishop.look4sat.R import com.rtbishop.look4sat.R
import com.rtbishop.look4sat.domain.model.DataState import com.rtbishop.look4sat.domain.model.DataState
import com.rtbishop.look4sat.domain.model.SatItem import com.rtbishop.look4sat.domain.model.SatItem
import com.rtbishop.look4sat.presentation.CardLoadingIndicator
import com.rtbishop.look4sat.presentation.MainTheme import com.rtbishop.look4sat.presentation.MainTheme
import com.rtbishop.look4sat.presentation.onClick import com.rtbishop.look4sat.presentation.onClick
import com.rtbishop.look4sat.presentation.passesScreen.PassesViewModel import com.rtbishop.look4sat.presentation.passesScreen.PassesViewModel
@@ -229,11 +230,7 @@ fun EntriesCard(state: DataState<List<SatItem>>, onSelected: (List<Int>, Boolean
} }
} }
} }
else -> { else -> CardLoadingIndicator()
Box(contentAlignment = Alignment.Center, modifier = Modifier.fillMaxSize()) {
CircularProgressIndicator(modifier = Modifier.size(80.dp))
}
}
} }
} }
} }
@@ -1,3 +1,5 @@
@file:OptIn(ExperimentalMaterialApi::class)
package com.rtbishop.look4sat.presentation.passesScreen package com.rtbishop.look4sat.presentation.passesScreen
import androidx.compose.foundation.ExperimentalFoundationApi import androidx.compose.foundation.ExperimentalFoundationApi
@@ -5,6 +7,11 @@ import androidx.compose.foundation.background
import androidx.compose.foundation.layout.* import androidx.compose.foundation.layout.*
import androidx.compose.foundation.lazy.LazyColumn import androidx.compose.foundation.lazy.LazyColumn
import androidx.compose.foundation.lazy.items 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.material3.*
import androidx.compose.runtime.Composable import androidx.compose.runtime.Composable
import androidx.compose.runtime.livedata.observeAsState 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.TextAlign
import androidx.compose.ui.text.style.TextOverflow import androidx.compose.ui.text.style.TextOverflow
import androidx.compose.ui.tooling.preview.Preview 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 androidx.hilt.navigation.compose.hiltViewModel
import com.rtbishop.look4sat.R import com.rtbishop.look4sat.R
import com.rtbishop.look4sat.domain.model.DataState import com.rtbishop.look4sat.domain.model.DataState
@@ -29,11 +36,52 @@ import java.util.*
private val sdf = SimpleDateFormat("HH:mm:ss", Locale.ENGLISH) private val sdf = SimpleDateFormat("HH:mm:ss", Locale.ENGLISH)
@OptIn(ExperimentalMaterialApi::class)
@Composable @Composable
fun PassesScreen(viewModel: PassesViewModel = hiltViewModel()) { 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)) { 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 @Composable
private fun Pass(pass: SatPass, modifier: Modifier = Modifier) { private fun Pass(pass: SatPass, modifier: Modifier = Modifier) {
Surface(color = MaterialTheme.colorScheme.background, modifier = modifier) { Surface(color = MaterialTheme.colorScheme.background, modifier = modifier) {
Surface(modifier = Modifier.padding(bottom = 1.dp)) { Surface(modifier = Modifier.padding(bottom = 2.dp)) {
Column( Column(
verticalArrangement = Arrangement.spacedBy(2.dp), verticalArrangement = Arrangement.spacedBy(2.dp),
modifier = Modifier modifier = Modifier
@@ -61,7 +109,7 @@ private fun Pass(pass: SatPass, modifier: Modifier = Modifier) {
Row(verticalAlignment = Alignment.CenterVertically) { Row(verticalAlignment = Alignment.CenterVertically) {
Text( Text(
text = "Id:${pass.catNum} - ", text = "Id:${pass.catNum} - ",
modifier = Modifier.width(90.dp), modifier = Modifier.width(82.dp),
textAlign = TextAlign.End, textAlign = TextAlign.End,
color = MaterialTheme.colorScheme.primary color = MaterialTheme.colorScheme.primary
) )
@@ -91,13 +139,20 @@ private fun Pass(pass: SatPass, modifier: Modifier = Modifier) {
horizontalArrangement = Arrangement.SpaceBetween, horizontalArrangement = Arrangement.SpaceBetween,
modifier = Modifier.fillMaxWidth() 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(
text = stringResource(id = R.string.pass_altitude, pass.altitude), text = stringResource(id = R.string.pass_altitude, pass.altitude),
textAlign = TextAlign.Center, textAlign = TextAlign.Center,
fontSize = 15.sp,
modifier = Modifier.weight(1f) 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( Row(
verticalAlignment = Alignment.CenterVertically, verticalAlignment = Alignment.CenterVertically,
@@ -105,14 +160,22 @@ private fun Pass(pass: SatPass, modifier: Modifier = Modifier) {
modifier = Modifier.fillMaxWidth() modifier = Modifier.fillMaxWidth()
) { ) {
val deepTime = stringResource(id = R.string.pass_placeholder) 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( LinearProgressIndicator(
progress = pass.progress, progress = if (pass.isDeepSpace) 1f else pass.progress,
modifier = modifier.weight(1f), modifier = modifier
.fillMaxWidth(0.75f)
.padding(top = 3.dp),
color = MaterialTheme.colorScheme.primary, color = MaterialTheme.colorScheme.primary,
trackColor = MaterialTheme.colorScheme.inverseSurface 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) @OptIn(ExperimentalFoundationApi::class)
@Composable @Composable
private fun PassesCard(state: DataState<List<SatPass>>) { private fun PassesCard(
refreshState: PullRefreshState, isRefreshing: Boolean, passes: List<SatPass>
) {
ElevatedCard(modifier = Modifier.fillMaxSize()) { ElevatedCard(modifier = Modifier.fillMaxSize()) {
when (state) { Box(Modifier.pullRefresh(refreshState)) {
is DataState.Success -> { LazyColumn(modifier = Modifier.fillMaxSize()) {
LazyColumn { items(items = passes, key = { item -> item.catNum + item.aosTime }) { pass ->
items(
items = state.data,
key = { item -> item.catNum + item.aosTime }) { pass ->
Pass(pass, Modifier.animateItemPlacement()) Pass(pass, Modifier.animateItemPlacement())
} }
} }
} PullRefreshIndicator(
else -> { refreshing = isRefreshing,
Box(contentAlignment = Alignment.Center, modifier = Modifier.fillMaxSize()) { state = refreshState,
CircularProgressIndicator(modifier = Modifier.size(80.dp)) 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.ISettingsManager
import com.rtbishop.look4sat.domain.model.DataState import com.rtbishop.look4sat.domain.model.DataState
import com.rtbishop.look4sat.domain.predict.SatPass import com.rtbishop.look4sat.domain.predict.SatPass
import com.rtbishop.look4sat.utility.toTimerString
import dagger.hilt.android.lifecycle.HiltViewModel import dagger.hilt.android.lifecycle.HiltViewModel
import kotlinx.coroutines.* import kotlinx.coroutines.*
import javax.inject.Inject import javax.inject.Inject
@@ -34,10 +35,13 @@ class PassesViewModel @Inject constructor(
private val settings: ISettingsManager private val settings: ISettingsManager
) : ViewModel() { ) : 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 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 entriesTotal: LiveData<Int> = repository.getEntriesTotal().asLiveData()
val passes: LiveData<DataState<List<SatPass>>> = _passes
val timerText: LiveData<Triple<String, String, String>> = _timerText
init { init {
viewModelScope.launch { viewModelScope.launch {
@@ -45,8 +49,27 @@ class PassesViewModel @Inject constructor(
passesProcessing?.cancelAndJoin() passesProcessing?.cancelAndJoin()
passesProcessing = viewModelScope.launch { passesProcessing = viewModelScope.launch {
while (isActive) { while (isActive) {
val time = System.currentTimeMillis() val timeNow = System.currentTimeMillis()
val newPasses = satelliteManager.processPasses(passes, time) 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)) _passes.postValue(DataState.Success(newPasses))
delay(1000) delay(1000)
} }
@@ -71,6 +94,7 @@ class PassesViewModel @Inject constructor(
) { ) {
viewModelScope.launch { viewModelScope.launch {
_passes.postValue(DataState.Loading) _passes.postValue(DataState.Loading)
// _timerText.postValue(timerDefaultText)
passesProcessing?.cancelAndJoin() passesProcessing?.cancelAndJoin()
selection?.let { items -> settings.saveEntriesSelection(items) } selection?.let { items -> settings.saveEntriesSelection(items) }
settings.setHoursAhead(hoursAhead) settings.setHoursAhead(hoursAhead)
@@ -78,7 +102,9 @@ class PassesViewModel @Inject constructor(
val stationPos = settings.loadStationPosition() val stationPos = settings.loadStationPosition()
val selectedIds = settings.loadEntriesSelection() val selectedIds = settings.loadEntriesSelection()
val satellites = repository.getEntriesWithIds(selectedIds) val satellites = repository.getEntriesWithIds(selectedIds)
satelliteManager.calculatePasses(satellites, stationPos, timeRef, hoursAhead, minElevation) satelliteManager.calculatePasses(
satellites, stationPos, timeRef, hoursAhead, minElevation
)
} }
} }
} }
+10
View File
@@ -0,0 +1,10 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="24dp"
android:height="24dp"
android:tint="#000000"
android:viewportWidth="24"
android:viewportHeight="24">
<path
android:fillColor="@android:color/white"
android:pathData="M10,18h4v-2h-4v2zM3,6v2h18L21,6L3,6zM6,13h12v-2L6,11v2z" />
</vector>
@@ -0,0 +1,10 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="24dp"
android:height="24dp"
android:tint="#000000"
android:viewportWidth="24"
android:viewportHeight="24">
<path
android:fillColor="@android:color/white"
android:pathData="M4.25,5.61C6.27,8.2 10,13 10,13v6c0,0.55 0.45,1 1,1h2c0.55,0 1,-0.45 1,-1v-6c0,0 3.72,-4.8 5.74,-7.39C20.25,4.95 19.78,4 18.95,4H5.04C4.21,4 3.74,4.95 4.25,5.61z" />
</vector>
+1 -1
View File
@@ -80,7 +80,7 @@
<string name="pass_elevation">Элевация: %.1f°</string> <string name="pass_elevation">Элевация: %.1f°</string>
<string name="pass_losAz">%.1f° - КПС</string> <string name="pass_losAz">%.1f° - КПС</string>
<string name="pass_altitude">Высота: %d км</string> <string name="pass_altitude">Высота: %d км</string>
<string name="pass_placeholder"> - - : - - </string> <string name="pass_placeholder"> -- : -- : -- </string>
<string name="pass_startTime">HH:mm:ss - EEE</string> <string name="pass_startTime">HH:mm:ss - EEE</string>
<string name="pass_endTime">EEE - HH:mm:ss</string> <string name="pass_endTime">EEE - HH:mm:ss</string>
+1 -1
View File
@@ -80,7 +80,7 @@
<string name="pass_elevation">仰角:%.1f°</string> <string name="pass_elevation">仰角:%.1f°</string>
<string name="pass_losAz">%.1f° - 出境</string> <string name="pass_losAz">%.1f° - 出境</string>
<string name="pass_altitude">高度:%d km</string> <string name="pass_altitude">高度:%d km</string>
<string name="pass_placeholder">" - - : - - "</string> <string name="pass_placeholder"> -- : -- : -- </string>
<string name="pass_startTime">HH:mm:ss - EEE</string> <string name="pass_startTime">HH:mm:ss - EEE</string>
<string name="pass_endTime">EEE - HH:mm:ss</string> <string name="pass_endTime">EEE - HH:mm:ss</string>
+1 -1
View File
@@ -80,7 +80,7 @@
<string name="pass_elevation">Elevation: %.1f°</string> <string name="pass_elevation">Elevation: %.1f°</string>
<string name="pass_losAz">%.1f° - LOS</string> <string name="pass_losAz">%.1f° - LOS</string>
<string name="pass_altitude">Altitude: %d km</string> <string name="pass_altitude">Altitude: %d km</string>
<string name="pass_placeholder"> - - : - - </string> <string name="pass_placeholder"> -- : -- : -- </string>
<string name="pass_startTime">HH:mm:ss - EEE</string> <string name="pass_startTime">HH:mm:ss - EEE</string>
<string name="pass_endTime">EEE - HH:mm:ss</string> <string name="pass_endTime">EEE - HH:mm:ss</string>
+2 -1
View File
@@ -16,8 +16,9 @@ buildscript {
osmdroid_version = '6.1.14' osmdroid_version = '6.1.14'
json_version = '20220924' json_version = '20220924'
compose_version = '1.3.3' compose_version = '1.3.3'
compose_compiler_version = '1.4.2' compose_compiler_version = '1.4.3'
activity_compose_version = '1.6.1' activity_compose_version = '1.6.1'
material_version = '1.3.1'
material3_version = '1.0.1' material3_version = '1.0.1'
leakcanary_version = '2.10' leakcanary_version = '2.10'
junit_version = '4.13.2' junit_version = '4.13.2'