diff --git a/app/build.gradle b/app/build.gradle index cc54f640..9a276b18 100644 --- a/app/build.gradle +++ b/app/build.gradle @@ -89,6 +89,7 @@ dependencies { implementation "androidx.activity:activity-compose:$activity_compose_version" implementation "androidx.compose.animation:animation:$compose_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.runtime:runtime:$compose_version" implementation "androidx.compose.runtime:runtime-livedata:$compose_version" diff --git a/app/src/main/java/com/rtbishop/look4sat/presentation/MainComponents.kt b/app/src/main/java/com/rtbishop/look4sat/presentation/MainComponents.kt index aacb79bc..75df32a8 100644 --- a/app/src/main/java/com/rtbishop/look4sat/presentation/MainComponents.kt +++ b/app/src/main/java/com/rtbishop/look4sat/presentation/MainComponents.kt @@ -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 diff --git a/app/src/main/java/com/rtbishop/look4sat/presentation/entriesScreen/EntriesScreen.kt b/app/src/main/java/com/rtbishop/look4sat/presentation/entriesScreen/EntriesScreen.kt index 04803cff..16d0882b 100644 --- a/app/src/main/java/com/rtbishop/look4sat/presentation/entriesScreen/EntriesScreen.kt +++ b/app/src/main/java/com/rtbishop/look4sat/presentation/entriesScreen/EntriesScreen.kt @@ -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>, onSelected: (List, Boolean } } } - else -> { - Box(contentAlignment = Alignment.Center, modifier = Modifier.fillMaxSize()) { - CircularProgressIndicator(modifier = Modifier.size(80.dp)) - } - } + else -> CardLoadingIndicator() } } } diff --git a/app/src/main/java/com/rtbishop/look4sat/presentation/passesScreen/PassesScreen.kt b/app/src/main/java/com/rtbishop/look4sat/presentation/passesScreen/PassesScreen.kt index 44a8e56f..0f867ed2 100644 --- a/app/src/main/java/com/rtbishop/look4sat/presentation/passesScreen/PassesScreen.kt +++ b/app/src/main/java/com/rtbishop/look4sat/presentation/passesScreen/PassesScreen.kt @@ -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() + 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>) { +private fun PassesCard( + refreshState: PullRefreshState, isRefreshing: Boolean, passes: List +) { 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 + ) } } } diff --git a/app/src/main/java/com/rtbishop/look4sat/presentation/passesScreen/PassesViewModel.kt b/app/src/main/java/com/rtbishop/look4sat/presentation/passesScreen/PassesViewModel.kt index 1eb0a77d..acc5704d 100644 --- a/app/src/main/java/com/rtbishop/look4sat/presentation/passesScreen/PassesViewModel.kt +++ b/app/src/main/java/com/rtbishop/look4sat/presentation/passesScreen/PassesViewModel.kt @@ -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>>() + private val timerData = Triple("Next - Id:Null", "Name: Null", "00:00:00") + private val _timerText = MutableLiveData>() private var passesProcessing: Job? = null - private val _passes = MutableLiveData>>(DataState.Loading) - val passes: LiveData>> = _passes val entriesTotal: LiveData = repository.getEntriesTotal().asLiveData() + val passes: LiveData>> = _passes + val timerText: LiveData> = _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 + ) } } } diff --git a/app/src/main/res/drawable/ic_filter.xml b/app/src/main/res/drawable/ic_filter.xml new file mode 100644 index 00000000..92074d26 --- /dev/null +++ b/app/src/main/res/drawable/ic_filter.xml @@ -0,0 +1,10 @@ + + + diff --git a/app/src/main/res/drawable/ic_filter_new.xml b/app/src/main/res/drawable/ic_filter_new.xml new file mode 100644 index 00000000..38dfe83c --- /dev/null +++ b/app/src/main/res/drawable/ic_filter_new.xml @@ -0,0 +1,10 @@ + + + diff --git a/app/src/main/res/values-ru/strings.xml b/app/src/main/res/values-ru/strings.xml index a5508a0f..c61edfdd 100644 --- a/app/src/main/res/values-ru/strings.xml +++ b/app/src/main/res/values-ru/strings.xml @@ -80,7 +80,7 @@ Элевация: %.1f° %.1f° - КПС Высота: %d км - - - : - - + -- : -- : -- HH:mm:ss - EEE EEE - HH:mm:ss diff --git a/app/src/main/res/values-zh-rCN/strings.xml b/app/src/main/res/values-zh-rCN/strings.xml index 2a63f8e7..2291fc00 100644 --- a/app/src/main/res/values-zh-rCN/strings.xml +++ b/app/src/main/res/values-zh-rCN/strings.xml @@ -80,7 +80,7 @@ 仰角:%.1f° %.1f° - 出境 高度:%d km - " - - : - - " + -- : -- : -- HH:mm:ss - EEE EEE - HH:mm:ss diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index ff766e46..06803fb1 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -80,7 +80,7 @@ Elevation: %.1f° %.1f° - LOS Altitude: %d km - - - : - - + -- : -- : -- HH:mm:ss - EEE EEE - HH:mm:ss diff --git a/build.gradle b/build.gradle index bcb1dc7c..5d436708 100644 --- a/build.gradle +++ b/build.gradle @@ -16,8 +16,9 @@ buildscript { osmdroid_version = '6.1.14' json_version = '20220924' compose_version = '1.3.3' - compose_compiler_version = '1.4.2' + compose_compiler_version = '1.4.3' activity_compose_version = '1.6.1' + material_version = '1.3.1' material3_version = '1.0.1' leakcanary_version = '2.10' junit_version = '4.13.2'