Implemented EclipsedIndicator and MapScroll on select

This commit is contained in:
Arty Bishop committed 2025-12-27 15:01:47 +00:00
1 parent fdbdc513f1
commit 01139f76e7
6 files changed
+134 -87

No files matched your search

@@ -20,6 +20,7 @@ import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.Text
import androidx.compose.runtime.Composable
import androidx.compose.runtime.DisposableEffect
import androidx.compose.runtime.LaunchedEffect
import androidx.compose.runtime.State
import androidx.compose.runtime.remember
import androidx.compose.ui.Alignment
@@ -82,19 +83,20 @@ fun NavGraphBuilder.mapDestination() {
composable(Screen.Map.route) {
val viewModel = viewModel(MapViewModel::class.java, factory = MapViewModel.Factory)
val uiState = viewModel.uiState.collectAsStateWithLifecycle()
MapScreen(uiState)
val mapView = rememberMapViewWithLifecycle()
MapScreen(uiState, mapView)
}
}
@Composable
private fun MapScreen(uiState: State<MapState>) {
private fun MapScreen(uiState: State<MapState>, mapView: MapView) {
val onItemClick = { item: OrbitalObject -> uiState.value.sendAction(MapAction.SelectItem(item)) }
val selectPrev = { uiState.value.sendAction(MapAction.SelectPrev) }
val selectNext = { uiState.value.sendAction(MapAction.SelectNext) }
val rotateMod = Modifier.rotate(180f)
val timeString = uiState.value.mapData?.aosTime ?: "00:00:00"
val isTimeAos = uiState.value.mapData?.isTimeAos ?: true
val osmInfo = "Wikimedia maps | Map data © OpenStreetMap contributors"
val osmInfo = "© OpenStreetMap contributors"
Column(modifier = Modifier.padding(6.dp), verticalArrangement = Arrangement.spacedBy(6.dp)) {
TimerRow {
CardIcon(onClick = selectPrev, iconId = R.drawable.ic_arrow, modifier = rotateMod)
@@ -104,7 +106,12 @@ private fun MapScreen(uiState: State<MapState>) {
NextPassRow(pass = uiState.value.orbitalPass)
ElevatedCard(modifier = Modifier.weight(1f)) {
Box(contentAlignment = Alignment.BottomCenter) {
MapView(isLightUi = uiState.value.isLightUi) { mapView ->
LaunchedEffect(uiState.value.track) {
val latitude = uiState.value.track?.get(0)?.get(0)?.latitude ?: 0.0
val longitude = uiState.value.track?.get(0)?.get(0)?.longitude ?: 0.0
mapView.controller.animateTo(GeoPoint(latitude, longitude))
}
AndroidView({ mapView }) { mapView ->
uiState.value.stationPosition?.let { setStationPosition(it, mapView) }
uiState.value.positions?.let { setPositions(it, mapView, onItemClick) }
uiState.value.track?.let { setSatelliteTrack(it, mapView) }
@@ -207,7 +214,6 @@ private fun getCustomTextIcon(textLabel: String, mapView: MapView): Drawable {
}
private fun setSatelliteTrack(satTrack: List<List<GeoPos>>, mapView: MapView) {
// val center = GeoPoint(satTrack[0][0].lat, satTrack[0][0].lon)
val trackOverlay = FolderOverlay()
try {
satTrack.forEach { track ->
@@ -222,7 +228,6 @@ private fun setSatelliteTrack(satTrack: List<List<GeoPos>>, mapView: MapView) {
} catch (exception: Exception) {
println(exception)
}
// mapView.controller.animateTo(center)
}
private fun setFootprint(orbitalPos: OrbitalPos, mapView: MapView) {
@@ -239,17 +244,7 @@ private fun setFootprint(orbitalPos: OrbitalPos, mapView: MapView) {
}
@Composable
private fun MapView(
modifier: Modifier = Modifier,
isLightUi: Boolean,
update: ((map: MapView) -> Unit)? = null
) {
val mapView = rememberMapViewWithLifecycle(isLightUi)
AndroidView({ mapView }, modifier) { update?.invoke(it) }
}
@Composable
private fun rememberMapViewWithLifecycle(isLightTheme: Boolean): MapView {
private fun rememberMapViewWithLifecycle(): MapView {
val tileSource = XYTileSource("tiles", 0, 6, 256, ".webp", emptyArray<String>())
val context = LocalContext.current
val mapView = remember { MapView(context) }.apply {
@@ -263,7 +258,7 @@ private fun rememberMapViewWithLifecycle(isLightTheme: Boolean): MapView {
zoomController.setVisibility(CustomZoomButtonsController.Visibility.NEVER)
overlayManager.tilesOverlay.loadingBackgroundColor = Color.TRANSPARENT
overlayManager.tilesOverlay.loadingLineColor = Color.TRANSPARENT
overlayManager.tilesOverlay.setColorFilter(getColorFilter(isLightTheme))
overlayManager.tilesOverlay.setColorFilter(getColorFilter())
setScrollableAreaLimitLatitude(maxLat, minLat, 0)
// add overlays: 0 - GSP, 1 - SatTrack, 2 - SatFootprint, 3 - SatIcons
overlays.addAll(Array(4) { FolderOverlay() })
@@ -290,13 +285,13 @@ private fun rememberMapViewLifecycleObserver(mapView: MapView) = remember(mapVie
}
@Composable
private fun getColorFilter(isLightTheme: Boolean): ColorMatrixColorFilter {
private fun getColorFilter(): ColorMatrixColorFilter {
val grayScale = ColorMatrix().apply { setSaturation(0f) }
val negative = ColorMatrix(
floatArrayOf(-1f, 0f, 0f, 0f, 260f, 0f, -1f, 0f, 0f, 260f, 0f, 0f, -1f, 0f, 260f, 0f, 0f, 0f, 1f, 0f)
)
negative.preConcat(grayScale)
return if (isLightTheme) ColorMatrixColorFilter(grayScale) else ColorMatrixColorFilter(negative)
return ColorMatrixColorFilter(negative)
}
private fun getMinZoom(screenHeight: Int): Double {
@@ -33,6 +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.compose.collectAsStateWithLifecycle
import androidx.lifecycle.viewmodel.compose.viewModel
import androidx.navigation.NavGraphBuilder
import androidx.navigation.compose.composable
@@ -60,7 +61,7 @@ fun NavGraphBuilder.passesDestination(navigateToRadar: (Int, Long) -> Unit) {
modelClass = PassesViewModel::class.java,
factory = PassesViewModel.Factory
)
val uiState = viewModel.uiState.value
val uiState = viewModel.uiState.collectAsStateWithLifecycle().value
PassesScreen(uiState, navigateToRadar)
}
}
@@ -17,8 +17,6 @@
*/
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
@@ -34,6 +32,9 @@ import com.rtbishop.look4sat.presentation.components.getDefaultPass
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.flow.update
import kotlinx.coroutines.isActive
import kotlinx.coroutines.launch
@@ -41,7 +42,7 @@ class PassesViewModel(
private val satelliteRepo: ISatelliteRepo, private val settingsRepo: ISettingsRepo
) : ViewModel() {
private val _uiState = mutableStateOf(
private val _uiState = MutableStateFlow(
PassesState(
isPassesDialogShown = false,
isRadiosDialogShown = false,
@@ -58,7 +59,7 @@ class PassesViewModel(
)
)
private var processing: Job? = null
val uiState: State<PassesState> = _uiState
val uiState: StateFlow<PassesState> = _uiState
init {
viewModelScope.launch {
@@ -69,7 +70,7 @@ class PassesViewModel(
val timeNow = System.currentTimeMillis()
val newPasses = satelliteRepo.processPasses(passes, timeNow)
setPassInfo(newPasses, timeNow)
_uiState.value = _uiState.value.copy(isRefreshing = false, itemsList = newPasses)
_uiState.update { it.copy(isRefreshing = false, itemsList = newPasses) }
delay(1000)
}
}
@@ -88,15 +89,15 @@ class PassesViewModel(
}
private fun applyFilter(hoursAhead: Int, minElevation: Double, modes: List<String>) = viewModelScope.launch {
_uiState.value = _uiState.value.copy(isRefreshing = true)
_uiState.update { it.copy(isRefreshing = true) }
processing?.cancelAndJoin()
settingsRepo.setPassesSettings(PassesSettings(hoursAhead, minElevation, modes))
_uiState.value = _uiState.value.copy(hours = hoursAhead, elevation = minElevation, modes = modes)
_uiState.update { it.copy(hours = hoursAhead, elevation = minElevation, modes = modes) }
satelliteRepo.calculatePasses(System.currentTimeMillis(), hoursAhead, minElevation, modes)
}
private fun refreshPasses() = viewModelScope.launch {
_uiState.value = _uiState.value.copy(isRefreshing = true)
_uiState.update { it.copy(isRefreshing = true) }
processing?.cancelAndJoin()
val (hoursAhead, minElevation, modes) = settingsRepo.passesSettings.value
satelliteRepo.calculatePasses(System.currentTimeMillis(), hoursAhead, minElevation, modes)
@@ -107,22 +108,22 @@ class PassesViewModel(
try {
val nextPass = passes.first { it.aosTime.minus(timeNow) > 0 }
val time = nextPass.aosTime.minus(timeNow).toTimerString()
_uiState.value = _uiState.value.copy(nextPass = nextPass, nextTime = time, isNextTimeAos = true)
_uiState.update { it.copy(nextPass = nextPass, nextTime = time, isNextTimeAos = true) }
} catch (exception: NoSuchElementException) {
val lastPass = passes.last()
val time = lastPass.losTime.minus(timeNow).toTimerString()
_uiState.value = _uiState.value.copy(nextPass = lastPass, nextTime = time, isNextTimeAos = false)
_uiState.update { it.copy(nextPass = lastPass, nextTime = time, isNextTimeAos = false) }
}
}
private fun toggleFilterDialog() {
val currentDialogState = _uiState.value.isPassesDialogShown
_uiState.value = _uiState.value.copy(isPassesDialogShown = currentDialogState.not())
_uiState.update { it.copy(isPassesDialogShown = currentDialogState.not()) }
}
private fun toggleRadiosDialog() {
val currentDialogState = _uiState.value.isRadiosDialogShown
_uiState.value = _uiState.value.copy(isRadiosDialogShown = currentDialogState.not())
_uiState.update { it.copy(isRadiosDialogShown = currentDialogState.not()) }
}
companion object {
@@ -15,10 +15,13 @@ import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.RowScope
import androidx.compose.foundation.layout.Spacer
import androidx.compose.foundation.layout.aspectRatio
import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.height
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.layout.size
import androidx.compose.foundation.lazy.LazyColumn
import androidx.compose.foundation.lazy.items
import androidx.compose.material3.ElevatedCard
@@ -101,7 +104,8 @@ private fun RadarScreen(uiState: RadarState, navigateBack: () -> Unit) {
}
Column(
verticalArrangement = Arrangement.SpaceBetween,
modifier = Modifier.fillMaxSize()
modifier = Modifier
.fillMaxSize()
.padding(horizontal = 6.dp, vertical = 4.dp)
) {
Row(
@@ -135,34 +139,35 @@ private fun RadarScreen(uiState: RadarState, navigateBack: () -> Unit) {
)
}
}
if (uiState.orbitalPos.eclipsed) {
val infiniteTransition = rememberInfiniteTransition()
val textAlpha = infiniteTransition.animateFloat(
initialValue = 1.0f,
targetValue = 0.0f,
animationSpec = infiniteRepeatable(
animation = tween(
durationMillis = 500,
delayMillis = 25,
easing = LinearEasing
),
repeatMode = RepeatMode.Reverse
))
Text(
text = "Eclipsed!",
fontSize = 18.sp,
fontWeight = FontWeight.Medium,
color = MaterialTheme.colorScheme.primary,
modifier = Modifier
.alpha(textAlpha.value)
.border(2.dp, MaterialTheme.colorScheme.primary, MaterialTheme.shapes.medium)
.padding(16.dp)
)
}
}
}
ElevatedCard(modifier = Modifier.fillMaxSize()) {
if (uiState.transmitters.isEmpty()) {
Column(
modifier = Modifier.fillMaxSize(),
horizontalAlignment = Alignment.CenterHorizontally,
verticalArrangement = Arrangement.Center
) {
Icon(
painter = painterResource(R.drawable.ic_satellite),
contentDescription = null,
modifier = Modifier.size(64.dp)
)
Spacer(modifier = Modifier.height(16.dp))
Text(
text = "This satellite doesn't have any known transcievers...",
textAlign = TextAlign.Center,
fontSize = 18.sp
)
}
} else {
Box(modifier = Modifier.fillMaxSize(), contentAlignment = Alignment.Center) {
TransmittersList(transmitters = uiState.transmitters)
if (uiState.orbitalPos?.eclipsed == true) {
EclipsedIndicator()
}
}
}
}
}
}
@@ -207,6 +212,42 @@ private fun TransmitterItemPreview() {
MainTheme { TransmitterItem(transmitter) }
}
@Composable
private fun EclipsedIndicator() {
val bgColor = MaterialTheme.colorScheme.primary
val bgShape = MaterialTheme.shapes.medium
val infiniteTransition = rememberInfiniteTransition()
val textAlpha = infiniteTransition.animateFloat(
initialValue = 1.0f,
targetValue = 0.0f,
animationSpec = infiniteRepeatable(
animation = tween(
durationMillis = 1000,
delayMillis = 25,
easing = LinearEasing
),
repeatMode = RepeatMode.Reverse
)
)
Box(
modifier = Modifier
.fillMaxSize()
.alpha(textAlpha.value)
.border(width = 2.dp, color = bgColor, shape = bgShape),
contentAlignment = Alignment.Center
) {
Text(
text = "Eclipsed!",
fontSize = 18.sp,
fontWeight = FontWeight.Medium,
color = MaterialTheme.colorScheme.background,
modifier = Modifier
.background(color = bgColor, shape = bgShape)
.padding(12.dp)
)
}
}
@Composable
private fun TransmitterItem(radio: SatRadio) {
Surface(color = MaterialTheme.colorScheme.background) {
@@ -224,7 +265,9 @@ private fun TransmitterItem(radio: SatRadio) {
Icon(
painter = painterResource(id = R.drawable.ic_arrow),
tint = Color.Green,
contentDescription = null, modifier = Modifier.rotate(90f).weight(0.15f)
contentDescription = null, modifier = Modifier
.rotate(90f)
.weight(0.15f)
)
Text(
text = if (radio.isInverted) "INVERTED: ${radio.info} " else "${radio.info} ",
@@ -239,7 +282,9 @@ private fun TransmitterItem(radio: SatRadio) {
Icon(
painter = painterResource(id = R.drawable.ic_arrow),
tint = Color.Red,
contentDescription = null, modifier = Modifier.rotate(-90f).weight(0.15f)
contentDescription = null, modifier = Modifier
.rotate(-90f)
.weight(0.15f)
)
}
FrequencyRow(satRadio = radio)
@@ -32,6 +32,7 @@ import androidx.compose.ui.text.style.TextAlign
import androidx.compose.ui.tooling.preview.Preview
import androidx.compose.ui.unit.dp
import androidx.compose.ui.unit.sp
import androidx.lifecycle.compose.collectAsStateWithLifecycle
import androidx.lifecycle.viewmodel.compose.viewModel
import androidx.navigation.NavGraphBuilder
import androidx.navigation.compose.composable
@@ -43,6 +44,7 @@ import com.rtbishop.look4sat.presentation.Screen
import com.rtbishop.look4sat.presentation.components.CardButton
import com.rtbishop.look4sat.presentation.components.gotoUrl
import com.rtbishop.look4sat.presentation.components.showToast
import org.osmdroid.library.BuildConfig
import java.text.SimpleDateFormat
import java.util.Date
import java.util.Locale
@@ -88,7 +90,7 @@ private fun SettingsScreen() {
}
// Position settings
val positionSettings = viewModel.positionSettings.value
val positionSettings = viewModel.positionSettings.collectAsStateWithLifecycle().value
val stationPos = positionSettings.stationPos
val setGpsPos = { locationRequest.launch(arrayOf(locationPermCoarse, locationPermFine)) }
val setGeoPos = { lat: Double, lon: Double -> viewModel.setGeoPosition(lat, lon) }
@@ -106,13 +108,13 @@ private fun SettingsScreen() {
}
// Data settings
val dataSettings = viewModel.dataSettings.value
val dataSettings = viewModel.dataSettings.collectAsStateWithLifecycle().value
val updateFromWeb: () -> Unit = { viewModel.updateFromWeb() }
val updateFromFile = { contentRequest.launch("*/*") }
val clearAllData: () -> Unit = { viewModel.clearAllData() }
// Other settings
val otherSettings = viewModel.otherSettings.value
val otherSettings = viewModel.otherSettings.collectAsStateWithLifecycle().value
val toggleUtc = { value: Boolean -> viewModel.toggleUtc(value) }
val toggleUpdate = { value: Boolean -> viewModel.toggleUpdate(value) }
val toggleSweep = { value: Boolean -> viewModel.toggleSweep(value) }
@@ -428,6 +430,7 @@ private fun OtherCard(
Text(text = stringResource(id = R.string.other_switch_sensors))
Switch(checked = settings.stateOfSensors, onCheckedChange = { toggleSensor(it) })
}
if (BuildConfig.DEBUG) {
Row(
horizontalArrangement = Arrangement.SpaceBetween,
verticalAlignment = Alignment.CenterVertically,
@@ -438,6 +441,7 @@ private fun OtherCard(
}
}
}
}
}
@Composable
@@ -17,8 +17,6 @@
*/
package com.rtbishop.look4sat.presentation.settings
import androidx.compose.runtime.State
import androidx.compose.runtime.mutableStateOf
import androidx.lifecycle.ViewModel
import androidx.lifecycle.ViewModelProvider
import androidx.lifecycle.viewModelScope
@@ -29,6 +27,9 @@ import com.rtbishop.look4sat.R
import com.rtbishop.look4sat.domain.model.OtherSettings
import com.rtbishop.look4sat.domain.repository.IDatabaseRepo
import com.rtbishop.look4sat.domain.repository.ISettingsRepo
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.StateFlow
import kotlinx.coroutines.flow.update
import kotlinx.coroutines.launch
class SettingsViewModel(
@@ -36,26 +37,26 @@ class SettingsViewModel(
) : ViewModel() {
private val defaultPosSettings = PositionSettings(false, settingsRepo.stationPosition.value, 0)
private val _positionSettings = mutableStateOf(defaultPosSettings)
val positionSettings: State<PositionSettings> = _positionSettings
private val _positionSettings = MutableStateFlow(defaultPosSettings)
val positionSettings: StateFlow<PositionSettings> = _positionSettings
private val defaultDataSettings = DataSettings(false, 0, 0, 0L)
private val _dataSettings = mutableStateOf(defaultDataSettings)
val dataSettings: State<DataSettings> = _dataSettings
private val _dataSettings = MutableStateFlow(defaultDataSettings)
val dataSettings: StateFlow<DataSettings> = _dataSettings
private val _otherSettings = mutableStateOf(settingsRepo.otherSettings.value)
val otherSettings: State<OtherSettings> = _otherSettings
private val _otherSettings = MutableStateFlow(settingsRepo.otherSettings.value)
val otherSettings: StateFlow<OtherSettings> = _otherSettings
init {
viewModelScope.launch {
settingsRepo.stationPosition.collect { geoPos ->
_positionSettings.value =
_positionSettings.value.copy(isUpdating = false, stationPos = geoPos)
_positionSettings.update { it.copy(isUpdating = false, stationPos = geoPos) }
}
}
viewModelScope.launch {
settingsRepo.databaseState.collect { state ->
_dataSettings.value = _dataSettings.value.copy(
_dataSettings.update {
it.copy(
isUpdating = false,
entriesTotal = state.numberOfSatellites,
radiosTotal = state.numberOfRadios,
@@ -63,62 +64,62 @@ class SettingsViewModel(
)
}
}
}
viewModelScope.launch {
settingsRepo.otherSettings.collect { _otherSettings.value = it }
settingsRepo.otherSettings.collect { settings -> _otherSettings.update { settings } }
}
}
fun setGpsPosition() {
if (settingsRepo.setStationPositionGps()) {
val messageResId = R.string.location_success
_positionSettings.value =
_positionSettings.value.copy(isUpdating = true, messageResId = messageResId)
_positionSettings.update { it.copy(isUpdating = true, messageResId = messageResId) }
} else {
val errorResId = R.string.location_gps_error
_positionSettings.value = _positionSettings.value.copy(messageResId = errorResId)
_positionSettings.update { it.copy(messageResId = errorResId) }
}
}
fun setGeoPosition(latitude: Double, longitude: Double) {
if (settingsRepo.setStationPositionGeo(latitude, longitude, 0.0)) {
val messageResId = R.string.location_success
_positionSettings.value = _positionSettings.value.copy(messageResId = messageResId)
_positionSettings.update { it.copy(messageResId = messageResId) }
} else {
val errorResId = R.string.location_manual_error
_positionSettings.value = _positionSettings.value.copy(messageResId = errorResId)
_positionSettings.update { it.copy(messageResId = errorResId) }
}
}
fun setQthPosition(locator: String) {
if (settingsRepo.setStationPositionQth(locator)) {
val messageResId = R.string.location_success
_positionSettings.value = _positionSettings.value.copy(messageResId = messageResId)
_positionSettings.update { it.copy(messageResId = messageResId) }
} else {
val errorResId = R.string.location_qth_error
_positionSettings.value = _positionSettings.value.copy(messageResId = errorResId)
_positionSettings.update { it.copy(messageResId = errorResId) }
}
}
fun dismissPosMessage() {
_positionSettings.value = _positionSettings.value.copy(messageResId = 0)
_positionSettings.update { it.copy(messageResId = 0) }
}
fun updateFromWeb() = viewModelScope.launch {
try {
_dataSettings.value = _dataSettings.value.copy(isUpdating = true)
_dataSettings.update { it.copy(isUpdating = true) }
databaseRepo.updateFromRemote()
} catch (exception: Exception) {
_dataSettings.value = _dataSettings.value.copy(isUpdating = false)
_dataSettings.update { it.copy(isUpdating = false) }
println(exception)
}
}
fun updateFromFile(uri: String) = viewModelScope.launch {
try {
_dataSettings.value = _dataSettings.value.copy(isUpdating = true)
_dataSettings.update { it.copy(isUpdating = true) }
databaseRepo.updateFromFile(uri)
} catch (exception: Exception) {
_dataSettings.value = _dataSettings.value.copy(isUpdating = false)
_dataSettings.update { it.copy(isUpdating = false) }
println(exception)
}
}