mirror of
https://github.com/atsunatsu/Look4Sat.git
synced 2026-10-02 03:15:37 +00:00
Implemented EclipsedIndicator and MapScroll on select
This commit is contained in:
1 parent
fdbdc513f1
commit
01139f76e7
6 files changed
+147
-100
No files matched your search
@@ -20,6 +20,7 @@ import androidx.compose.material3.MaterialTheme
|
|||||||
import androidx.compose.material3.Text
|
import androidx.compose.material3.Text
|
||||||
import androidx.compose.runtime.Composable
|
import androidx.compose.runtime.Composable
|
||||||
import androidx.compose.runtime.DisposableEffect
|
import androidx.compose.runtime.DisposableEffect
|
||||||
|
import androidx.compose.runtime.LaunchedEffect
|
||||||
import androidx.compose.runtime.State
|
import androidx.compose.runtime.State
|
||||||
import androidx.compose.runtime.remember
|
import androidx.compose.runtime.remember
|
||||||
import androidx.compose.ui.Alignment
|
import androidx.compose.ui.Alignment
|
||||||
@@ -82,19 +83,20 @@ fun NavGraphBuilder.mapDestination() {
|
|||||||
composable(Screen.Map.route) {
|
composable(Screen.Map.route) {
|
||||||
val viewModel = viewModel(MapViewModel::class.java, factory = MapViewModel.Factory)
|
val viewModel = viewModel(MapViewModel::class.java, factory = MapViewModel.Factory)
|
||||||
val uiState = viewModel.uiState.collectAsStateWithLifecycle()
|
val uiState = viewModel.uiState.collectAsStateWithLifecycle()
|
||||||
MapScreen(uiState)
|
val mapView = rememberMapViewWithLifecycle()
|
||||||
|
MapScreen(uiState, mapView)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@Composable
|
@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 onItemClick = { item: OrbitalObject -> uiState.value.sendAction(MapAction.SelectItem(item)) }
|
||||||
val selectPrev = { uiState.value.sendAction(MapAction.SelectPrev) }
|
val selectPrev = { uiState.value.sendAction(MapAction.SelectPrev) }
|
||||||
val selectNext = { uiState.value.sendAction(MapAction.SelectNext) }
|
val selectNext = { uiState.value.sendAction(MapAction.SelectNext) }
|
||||||
val rotateMod = Modifier.rotate(180f)
|
val rotateMod = Modifier.rotate(180f)
|
||||||
val timeString = uiState.value.mapData?.aosTime ?: "00:00:00"
|
val timeString = uiState.value.mapData?.aosTime ?: "00:00:00"
|
||||||
val isTimeAos = uiState.value.mapData?.isTimeAos ?: true
|
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)) {
|
Column(modifier = Modifier.padding(6.dp), verticalArrangement = Arrangement.spacedBy(6.dp)) {
|
||||||
TimerRow {
|
TimerRow {
|
||||||
CardIcon(onClick = selectPrev, iconId = R.drawable.ic_arrow, modifier = rotateMod)
|
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)
|
NextPassRow(pass = uiState.value.orbitalPass)
|
||||||
ElevatedCard(modifier = Modifier.weight(1f)) {
|
ElevatedCard(modifier = Modifier.weight(1f)) {
|
||||||
Box(contentAlignment = Alignment.BottomCenter) {
|
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.stationPosition?.let { setStationPosition(it, mapView) }
|
||||||
uiState.value.positions?.let { setPositions(it, mapView, onItemClick) }
|
uiState.value.positions?.let { setPositions(it, mapView, onItemClick) }
|
||||||
uiState.value.track?.let { setSatelliteTrack(it, mapView) }
|
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) {
|
private fun setSatelliteTrack(satTrack: List<List<GeoPos>>, mapView: MapView) {
|
||||||
// val center = GeoPoint(satTrack[0][0].lat, satTrack[0][0].lon)
|
|
||||||
val trackOverlay = FolderOverlay()
|
val trackOverlay = FolderOverlay()
|
||||||
try {
|
try {
|
||||||
satTrack.forEach { track ->
|
satTrack.forEach { track ->
|
||||||
@@ -222,7 +228,6 @@ private fun setSatelliteTrack(satTrack: List<List<GeoPos>>, mapView: MapView) {
|
|||||||
} catch (exception: Exception) {
|
} catch (exception: Exception) {
|
||||||
println(exception)
|
println(exception)
|
||||||
}
|
}
|
||||||
// mapView.controller.animateTo(center)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun setFootprint(orbitalPos: OrbitalPos, mapView: MapView) {
|
private fun setFootprint(orbitalPos: OrbitalPos, mapView: MapView) {
|
||||||
@@ -239,17 +244,7 @@ private fun setFootprint(orbitalPos: OrbitalPos, mapView: MapView) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
@Composable
|
@Composable
|
||||||
private fun MapView(
|
private fun rememberMapViewWithLifecycle(): 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 {
|
|
||||||
val tileSource = XYTileSource("tiles", 0, 6, 256, ".webp", emptyArray<String>())
|
val tileSource = XYTileSource("tiles", 0, 6, 256, ".webp", emptyArray<String>())
|
||||||
val context = LocalContext.current
|
val context = LocalContext.current
|
||||||
val mapView = remember { MapView(context) }.apply {
|
val mapView = remember { MapView(context) }.apply {
|
||||||
@@ -263,7 +258,7 @@ private fun rememberMapViewWithLifecycle(isLightTheme: Boolean): MapView {
|
|||||||
zoomController.setVisibility(CustomZoomButtonsController.Visibility.NEVER)
|
zoomController.setVisibility(CustomZoomButtonsController.Visibility.NEVER)
|
||||||
overlayManager.tilesOverlay.loadingBackgroundColor = Color.TRANSPARENT
|
overlayManager.tilesOverlay.loadingBackgroundColor = Color.TRANSPARENT
|
||||||
overlayManager.tilesOverlay.loadingLineColor = Color.TRANSPARENT
|
overlayManager.tilesOverlay.loadingLineColor = Color.TRANSPARENT
|
||||||
overlayManager.tilesOverlay.setColorFilter(getColorFilter(isLightTheme))
|
overlayManager.tilesOverlay.setColorFilter(getColorFilter())
|
||||||
setScrollableAreaLimitLatitude(maxLat, minLat, 0)
|
setScrollableAreaLimitLatitude(maxLat, minLat, 0)
|
||||||
// add overlays: 0 - GSP, 1 - SatTrack, 2 - SatFootprint, 3 - SatIcons
|
// add overlays: 0 - GSP, 1 - SatTrack, 2 - SatFootprint, 3 - SatIcons
|
||||||
overlays.addAll(Array(4) { FolderOverlay() })
|
overlays.addAll(Array(4) { FolderOverlay() })
|
||||||
@@ -290,13 +285,13 @@ private fun rememberMapViewLifecycleObserver(mapView: MapView) = remember(mapVie
|
|||||||
}
|
}
|
||||||
|
|
||||||
@Composable
|
@Composable
|
||||||
private fun getColorFilter(isLightTheme: Boolean): ColorMatrixColorFilter {
|
private fun getColorFilter(): ColorMatrixColorFilter {
|
||||||
val grayScale = ColorMatrix().apply { setSaturation(0f) }
|
val grayScale = ColorMatrix().apply { setSaturation(0f) }
|
||||||
val negative = ColorMatrix(
|
val negative = ColorMatrix(
|
||||||
floatArrayOf(-1f, 0f, 0f, 0f, 260f, 0f, -1f, 0f, 0f, 260f, 0f, 0f, -1f, 0f, 260f, 0f, 0f, 0f, 1f, 0f)
|
floatArrayOf(-1f, 0f, 0f, 0f, 260f, 0f, -1f, 0f, 0f, 260f, 0f, 0f, -1f, 0f, 260f, 0f, 0f, 0f, 1f, 0f)
|
||||||
)
|
)
|
||||||
negative.preConcat(grayScale)
|
negative.preConcat(grayScale)
|
||||||
return if (isLightTheme) ColorMatrixColorFilter(grayScale) else ColorMatrixColorFilter(negative)
|
return ColorMatrixColorFilter(negative)
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun getMinZoom(screenHeight: Int): Double {
|
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.tooling.preview.Preview
|
||||||
import androidx.compose.ui.unit.dp
|
import androidx.compose.ui.unit.dp
|
||||||
import androidx.compose.ui.unit.sp
|
import androidx.compose.ui.unit.sp
|
||||||
|
import androidx.lifecycle.compose.collectAsStateWithLifecycle
|
||||||
import androidx.lifecycle.viewmodel.compose.viewModel
|
import androidx.lifecycle.viewmodel.compose.viewModel
|
||||||
import androidx.navigation.NavGraphBuilder
|
import androidx.navigation.NavGraphBuilder
|
||||||
import androidx.navigation.compose.composable
|
import androidx.navigation.compose.composable
|
||||||
@@ -60,7 +61,7 @@ fun NavGraphBuilder.passesDestination(navigateToRadar: (Int, Long) -> Unit) {
|
|||||||
modelClass = PassesViewModel::class.java,
|
modelClass = PassesViewModel::class.java,
|
||||||
factory = PassesViewModel.Factory
|
factory = PassesViewModel.Factory
|
||||||
)
|
)
|
||||||
val uiState = viewModel.uiState.value
|
val uiState = viewModel.uiState.collectAsStateWithLifecycle().value
|
||||||
PassesScreen(uiState, navigateToRadar)
|
PassesScreen(uiState, navigateToRadar)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -17,8 +17,6 @@
|
|||||||
*/
|
*/
|
||||||
package com.rtbishop.look4sat.presentation.passes
|
package com.rtbishop.look4sat.presentation.passes
|
||||||
|
|
||||||
import androidx.compose.runtime.State
|
|
||||||
import androidx.compose.runtime.mutableStateOf
|
|
||||||
import androidx.lifecycle.ViewModel
|
import androidx.lifecycle.ViewModel
|
||||||
import androidx.lifecycle.ViewModelProvider
|
import androidx.lifecycle.ViewModelProvider
|
||||||
import androidx.lifecycle.viewModelScope
|
import androidx.lifecycle.viewModelScope
|
||||||
@@ -34,6 +32,9 @@ import com.rtbishop.look4sat.presentation.components.getDefaultPass
|
|||||||
import kotlinx.coroutines.Job
|
import kotlinx.coroutines.Job
|
||||||
import kotlinx.coroutines.cancelAndJoin
|
import kotlinx.coroutines.cancelAndJoin
|
||||||
import kotlinx.coroutines.delay
|
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.isActive
|
||||||
import kotlinx.coroutines.launch
|
import kotlinx.coroutines.launch
|
||||||
|
|
||||||
@@ -41,7 +42,7 @@ class PassesViewModel(
|
|||||||
private val satelliteRepo: ISatelliteRepo, private val settingsRepo: ISettingsRepo
|
private val satelliteRepo: ISatelliteRepo, private val settingsRepo: ISettingsRepo
|
||||||
) : ViewModel() {
|
) : ViewModel() {
|
||||||
|
|
||||||
private val _uiState = mutableStateOf(
|
private val _uiState = MutableStateFlow(
|
||||||
PassesState(
|
PassesState(
|
||||||
isPassesDialogShown = false,
|
isPassesDialogShown = false,
|
||||||
isRadiosDialogShown = false,
|
isRadiosDialogShown = false,
|
||||||
@@ -58,7 +59,7 @@ class PassesViewModel(
|
|||||||
)
|
)
|
||||||
)
|
)
|
||||||
private var processing: Job? = null
|
private var processing: Job? = null
|
||||||
val uiState: State<PassesState> = _uiState
|
val uiState: StateFlow<PassesState> = _uiState
|
||||||
|
|
||||||
init {
|
init {
|
||||||
viewModelScope.launch {
|
viewModelScope.launch {
|
||||||
@@ -69,7 +70,7 @@ class PassesViewModel(
|
|||||||
val timeNow = System.currentTimeMillis()
|
val timeNow = System.currentTimeMillis()
|
||||||
val newPasses = satelliteRepo.processPasses(passes, timeNow)
|
val newPasses = satelliteRepo.processPasses(passes, timeNow)
|
||||||
setPassInfo(newPasses, timeNow)
|
setPassInfo(newPasses, timeNow)
|
||||||
_uiState.value = _uiState.value.copy(isRefreshing = false, itemsList = newPasses)
|
_uiState.update { it.copy(isRefreshing = false, itemsList = newPasses) }
|
||||||
delay(1000)
|
delay(1000)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -88,15 +89,15 @@ class PassesViewModel(
|
|||||||
}
|
}
|
||||||
|
|
||||||
private fun applyFilter(hoursAhead: Int, minElevation: Double, modes: List<String>) = viewModelScope.launch {
|
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()
|
processing?.cancelAndJoin()
|
||||||
settingsRepo.setPassesSettings(PassesSettings(hoursAhead, minElevation, modes))
|
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)
|
satelliteRepo.calculatePasses(System.currentTimeMillis(), hoursAhead, minElevation, modes)
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun refreshPasses() = viewModelScope.launch {
|
private fun refreshPasses() = viewModelScope.launch {
|
||||||
_uiState.value = _uiState.value.copy(isRefreshing = true)
|
_uiState.update { it.copy(isRefreshing = true) }
|
||||||
processing?.cancelAndJoin()
|
processing?.cancelAndJoin()
|
||||||
val (hoursAhead, minElevation, modes) = settingsRepo.passesSettings.value
|
val (hoursAhead, minElevation, modes) = settingsRepo.passesSettings.value
|
||||||
satelliteRepo.calculatePasses(System.currentTimeMillis(), hoursAhead, minElevation, modes)
|
satelliteRepo.calculatePasses(System.currentTimeMillis(), hoursAhead, minElevation, modes)
|
||||||
@@ -107,22 +108,22 @@ class PassesViewModel(
|
|||||||
try {
|
try {
|
||||||
val nextPass = passes.first { it.aosTime.minus(timeNow) > 0 }
|
val nextPass = passes.first { it.aosTime.minus(timeNow) > 0 }
|
||||||
val time = nextPass.aosTime.minus(timeNow).toTimerString()
|
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) {
|
} catch (exception: NoSuchElementException) {
|
||||||
val lastPass = passes.last()
|
val lastPass = passes.last()
|
||||||
val time = lastPass.losTime.minus(timeNow).toTimerString()
|
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() {
|
private fun toggleFilterDialog() {
|
||||||
val currentDialogState = _uiState.value.isPassesDialogShown
|
val currentDialogState = _uiState.value.isPassesDialogShown
|
||||||
_uiState.value = _uiState.value.copy(isPassesDialogShown = currentDialogState.not())
|
_uiState.update { it.copy(isPassesDialogShown = currentDialogState.not()) }
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun toggleRadiosDialog() {
|
private fun toggleRadiosDialog() {
|
||||||
val currentDialogState = _uiState.value.isRadiosDialogShown
|
val currentDialogState = _uiState.value.isRadiosDialogShown
|
||||||
_uiState.value = _uiState.value.copy(isRadiosDialogShown = currentDialogState.not())
|
_uiState.update { it.copy(isRadiosDialogShown = currentDialogState.not()) }
|
||||||
}
|
}
|
||||||
|
|
||||||
companion object {
|
companion object {
|
||||||
|
|||||||
@@ -15,10 +15,13 @@ import androidx.compose.foundation.layout.Box
|
|||||||
import androidx.compose.foundation.layout.Column
|
import androidx.compose.foundation.layout.Column
|
||||||
import androidx.compose.foundation.layout.Row
|
import androidx.compose.foundation.layout.Row
|
||||||
import androidx.compose.foundation.layout.RowScope
|
import androidx.compose.foundation.layout.RowScope
|
||||||
|
import androidx.compose.foundation.layout.Spacer
|
||||||
import androidx.compose.foundation.layout.aspectRatio
|
import androidx.compose.foundation.layout.aspectRatio
|
||||||
import androidx.compose.foundation.layout.fillMaxSize
|
import androidx.compose.foundation.layout.fillMaxSize
|
||||||
import androidx.compose.foundation.layout.fillMaxWidth
|
import androidx.compose.foundation.layout.fillMaxWidth
|
||||||
|
import androidx.compose.foundation.layout.height
|
||||||
import androidx.compose.foundation.layout.padding
|
import androidx.compose.foundation.layout.padding
|
||||||
|
import androidx.compose.foundation.layout.size
|
||||||
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.material3.ElevatedCard
|
import androidx.compose.material3.ElevatedCard
|
||||||
@@ -101,7 +104,8 @@ private fun RadarScreen(uiState: RadarState, navigateBack: () -> Unit) {
|
|||||||
}
|
}
|
||||||
Column(
|
Column(
|
||||||
verticalArrangement = Arrangement.SpaceBetween,
|
verticalArrangement = Arrangement.SpaceBetween,
|
||||||
modifier = Modifier.fillMaxSize()
|
modifier = Modifier
|
||||||
|
.fillMaxSize()
|
||||||
.padding(horizontal = 6.dp, vertical = 4.dp)
|
.padding(horizontal = 6.dp, vertical = 4.dp)
|
||||||
) {
|
) {
|
||||||
Row(
|
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()) {
|
ElevatedCard(modifier = Modifier.fillMaxSize()) {
|
||||||
TransmittersList(transmitters = uiState.transmitters)
|
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) }
|
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
|
@Composable
|
||||||
private fun TransmitterItem(radio: SatRadio) {
|
private fun TransmitterItem(radio: SatRadio) {
|
||||||
Surface(color = MaterialTheme.colorScheme.background) {
|
Surface(color = MaterialTheme.colorScheme.background) {
|
||||||
@@ -224,7 +265,9 @@ private fun TransmitterItem(radio: SatRadio) {
|
|||||||
Icon(
|
Icon(
|
||||||
painter = painterResource(id = R.drawable.ic_arrow),
|
painter = painterResource(id = R.drawable.ic_arrow),
|
||||||
tint = Color.Green,
|
tint = Color.Green,
|
||||||
contentDescription = null, modifier = Modifier.rotate(90f).weight(0.15f)
|
contentDescription = null, modifier = Modifier
|
||||||
|
.rotate(90f)
|
||||||
|
.weight(0.15f)
|
||||||
)
|
)
|
||||||
Text(
|
Text(
|
||||||
text = if (radio.isInverted) "INVERTED: ${radio.info} " else "${radio.info} ",
|
text = if (radio.isInverted) "INVERTED: ${radio.info} " else "${radio.info} ",
|
||||||
@@ -239,7 +282,9 @@ private fun TransmitterItem(radio: SatRadio) {
|
|||||||
Icon(
|
Icon(
|
||||||
painter = painterResource(id = R.drawable.ic_arrow),
|
painter = painterResource(id = R.drawable.ic_arrow),
|
||||||
tint = Color.Red,
|
tint = Color.Red,
|
||||||
contentDescription = null, modifier = Modifier.rotate(-90f).weight(0.15f)
|
contentDescription = null, modifier = Modifier
|
||||||
|
.rotate(-90f)
|
||||||
|
.weight(0.15f)
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
FrequencyRow(satRadio = radio)
|
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.tooling.preview.Preview
|
||||||
import androidx.compose.ui.unit.dp
|
import androidx.compose.ui.unit.dp
|
||||||
import androidx.compose.ui.unit.sp
|
import androidx.compose.ui.unit.sp
|
||||||
|
import androidx.lifecycle.compose.collectAsStateWithLifecycle
|
||||||
import androidx.lifecycle.viewmodel.compose.viewModel
|
import androidx.lifecycle.viewmodel.compose.viewModel
|
||||||
import androidx.navigation.NavGraphBuilder
|
import androidx.navigation.NavGraphBuilder
|
||||||
import androidx.navigation.compose.composable
|
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.CardButton
|
||||||
import com.rtbishop.look4sat.presentation.components.gotoUrl
|
import com.rtbishop.look4sat.presentation.components.gotoUrl
|
||||||
import com.rtbishop.look4sat.presentation.components.showToast
|
import com.rtbishop.look4sat.presentation.components.showToast
|
||||||
|
import org.osmdroid.library.BuildConfig
|
||||||
import java.text.SimpleDateFormat
|
import java.text.SimpleDateFormat
|
||||||
import java.util.Date
|
import java.util.Date
|
||||||
import java.util.Locale
|
import java.util.Locale
|
||||||
@@ -88,7 +90,7 @@ private fun SettingsScreen() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Position settings
|
// Position settings
|
||||||
val positionSettings = viewModel.positionSettings.value
|
val positionSettings = viewModel.positionSettings.collectAsStateWithLifecycle().value
|
||||||
val stationPos = positionSettings.stationPos
|
val stationPos = positionSettings.stationPos
|
||||||
val setGpsPos = { locationRequest.launch(arrayOf(locationPermCoarse, locationPermFine)) }
|
val setGpsPos = { locationRequest.launch(arrayOf(locationPermCoarse, locationPermFine)) }
|
||||||
val setGeoPos = { lat: Double, lon: Double -> viewModel.setGeoPosition(lat, lon) }
|
val setGeoPos = { lat: Double, lon: Double -> viewModel.setGeoPosition(lat, lon) }
|
||||||
@@ -106,13 +108,13 @@ private fun SettingsScreen() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Data settings
|
// Data settings
|
||||||
val dataSettings = viewModel.dataSettings.value
|
val dataSettings = viewModel.dataSettings.collectAsStateWithLifecycle().value
|
||||||
val updateFromWeb: () -> Unit = { viewModel.updateFromWeb() }
|
val updateFromWeb: () -> Unit = { viewModel.updateFromWeb() }
|
||||||
val updateFromFile = { contentRequest.launch("*/*") }
|
val updateFromFile = { contentRequest.launch("*/*") }
|
||||||
val clearAllData: () -> Unit = { viewModel.clearAllData() }
|
val clearAllData: () -> Unit = { viewModel.clearAllData() }
|
||||||
|
|
||||||
// Other settings
|
// Other settings
|
||||||
val otherSettings = viewModel.otherSettings.value
|
val otherSettings = viewModel.otherSettings.collectAsStateWithLifecycle().value
|
||||||
val toggleUtc = { value: Boolean -> viewModel.toggleUtc(value) }
|
val toggleUtc = { value: Boolean -> viewModel.toggleUtc(value) }
|
||||||
val toggleUpdate = { value: Boolean -> viewModel.toggleUpdate(value) }
|
val toggleUpdate = { value: Boolean -> viewModel.toggleUpdate(value) }
|
||||||
val toggleSweep = { value: Boolean -> viewModel.toggleSweep(value) }
|
val toggleSweep = { value: Boolean -> viewModel.toggleSweep(value) }
|
||||||
@@ -428,13 +430,15 @@ private fun OtherCard(
|
|||||||
Text(text = stringResource(id = R.string.other_switch_sensors))
|
Text(text = stringResource(id = R.string.other_switch_sensors))
|
||||||
Switch(checked = settings.stateOfSensors, onCheckedChange = { toggleSensor(it) })
|
Switch(checked = settings.stateOfSensors, onCheckedChange = { toggleSensor(it) })
|
||||||
}
|
}
|
||||||
Row(
|
if (BuildConfig.DEBUG) {
|
||||||
horizontalArrangement = Arrangement.SpaceBetween,
|
Row(
|
||||||
verticalAlignment = Alignment.CenterVertically,
|
horizontalArrangement = Arrangement.SpaceBetween,
|
||||||
modifier = Modifier.fillMaxWidth()
|
verticalAlignment = Alignment.CenterVertically,
|
||||||
) {
|
modifier = Modifier.fillMaxWidth()
|
||||||
Text(text = stringResource(id = R.string.other_switch_light_theme))
|
) {
|
||||||
Switch(checked = settings.stateOfLightTheme, onCheckedChange = { toggleLightTheme(it) })
|
Text(text = stringResource(id = R.string.other_switch_light_theme))
|
||||||
|
Switch(checked = settings.stateOfLightTheme, onCheckedChange = { toggleLightTheme(it) })
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
+30
-29
@@ -17,8 +17,6 @@
|
|||||||
*/
|
*/
|
||||||
package com.rtbishop.look4sat.presentation.settings
|
package com.rtbishop.look4sat.presentation.settings
|
||||||
|
|
||||||
import androidx.compose.runtime.State
|
|
||||||
import androidx.compose.runtime.mutableStateOf
|
|
||||||
import androidx.lifecycle.ViewModel
|
import androidx.lifecycle.ViewModel
|
||||||
import androidx.lifecycle.ViewModelProvider
|
import androidx.lifecycle.ViewModelProvider
|
||||||
import androidx.lifecycle.viewModelScope
|
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.model.OtherSettings
|
||||||
import com.rtbishop.look4sat.domain.repository.IDatabaseRepo
|
import com.rtbishop.look4sat.domain.repository.IDatabaseRepo
|
||||||
import com.rtbishop.look4sat.domain.repository.ISettingsRepo
|
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
|
import kotlinx.coroutines.launch
|
||||||
|
|
||||||
class SettingsViewModel(
|
class SettingsViewModel(
|
||||||
@@ -36,89 +37,89 @@ class SettingsViewModel(
|
|||||||
) : ViewModel() {
|
) : ViewModel() {
|
||||||
|
|
||||||
private val defaultPosSettings = PositionSettings(false, settingsRepo.stationPosition.value, 0)
|
private val defaultPosSettings = PositionSettings(false, settingsRepo.stationPosition.value, 0)
|
||||||
private val _positionSettings = mutableStateOf(defaultPosSettings)
|
private val _positionSettings = MutableStateFlow(defaultPosSettings)
|
||||||
val positionSettings: State<PositionSettings> = _positionSettings
|
val positionSettings: StateFlow<PositionSettings> = _positionSettings
|
||||||
|
|
||||||
private val defaultDataSettings = DataSettings(false, 0, 0, 0L)
|
private val defaultDataSettings = DataSettings(false, 0, 0, 0L)
|
||||||
private val _dataSettings = mutableStateOf(defaultDataSettings)
|
private val _dataSettings = MutableStateFlow(defaultDataSettings)
|
||||||
val dataSettings: State<DataSettings> = _dataSettings
|
val dataSettings: StateFlow<DataSettings> = _dataSettings
|
||||||
|
|
||||||
private val _otherSettings = mutableStateOf(settingsRepo.otherSettings.value)
|
private val _otherSettings = MutableStateFlow(settingsRepo.otherSettings.value)
|
||||||
val otherSettings: State<OtherSettings> = _otherSettings
|
val otherSettings: StateFlow<OtherSettings> = _otherSettings
|
||||||
|
|
||||||
init {
|
init {
|
||||||
viewModelScope.launch {
|
viewModelScope.launch {
|
||||||
settingsRepo.stationPosition.collect { geoPos ->
|
settingsRepo.stationPosition.collect { geoPos ->
|
||||||
_positionSettings.value =
|
_positionSettings.update { it.copy(isUpdating = false, stationPos = geoPos) }
|
||||||
_positionSettings.value.copy(isUpdating = false, stationPos = geoPos)
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
viewModelScope.launch {
|
viewModelScope.launch {
|
||||||
settingsRepo.databaseState.collect { state ->
|
settingsRepo.databaseState.collect { state ->
|
||||||
_dataSettings.value = _dataSettings.value.copy(
|
_dataSettings.update {
|
||||||
isUpdating = false,
|
it.copy(
|
||||||
entriesTotal = state.numberOfSatellites,
|
isUpdating = false,
|
||||||
radiosTotal = state.numberOfRadios,
|
entriesTotal = state.numberOfSatellites,
|
||||||
timestamp = state.updateTimestamp
|
radiosTotal = state.numberOfRadios,
|
||||||
)
|
timestamp = state.updateTimestamp
|
||||||
|
)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
viewModelScope.launch {
|
viewModelScope.launch {
|
||||||
settingsRepo.otherSettings.collect { _otherSettings.value = it }
|
settingsRepo.otherSettings.collect { settings -> _otherSettings.update { settings } }
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fun setGpsPosition() {
|
fun setGpsPosition() {
|
||||||
if (settingsRepo.setStationPositionGps()) {
|
if (settingsRepo.setStationPositionGps()) {
|
||||||
val messageResId = R.string.location_success
|
val messageResId = R.string.location_success
|
||||||
_positionSettings.value =
|
_positionSettings.update { it.copy(isUpdating = true, messageResId = messageResId) }
|
||||||
_positionSettings.value.copy(isUpdating = true, messageResId = messageResId)
|
|
||||||
} else {
|
} else {
|
||||||
val errorResId = R.string.location_gps_error
|
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) {
|
fun setGeoPosition(latitude: Double, longitude: Double) {
|
||||||
if (settingsRepo.setStationPositionGeo(latitude, longitude, 0.0)) {
|
if (settingsRepo.setStationPositionGeo(latitude, longitude, 0.0)) {
|
||||||
val messageResId = R.string.location_success
|
val messageResId = R.string.location_success
|
||||||
_positionSettings.value = _positionSettings.value.copy(messageResId = messageResId)
|
_positionSettings.update { it.copy(messageResId = messageResId) }
|
||||||
} else {
|
} else {
|
||||||
val errorResId = R.string.location_manual_error
|
val errorResId = R.string.location_manual_error
|
||||||
_positionSettings.value = _positionSettings.value.copy(messageResId = errorResId)
|
_positionSettings.update { it.copy(messageResId = errorResId) }
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fun setQthPosition(locator: String) {
|
fun setQthPosition(locator: String) {
|
||||||
if (settingsRepo.setStationPositionQth(locator)) {
|
if (settingsRepo.setStationPositionQth(locator)) {
|
||||||
val messageResId = R.string.location_success
|
val messageResId = R.string.location_success
|
||||||
_positionSettings.value = _positionSettings.value.copy(messageResId = messageResId)
|
_positionSettings.update { it.copy(messageResId = messageResId) }
|
||||||
} else {
|
} else {
|
||||||
val errorResId = R.string.location_qth_error
|
val errorResId = R.string.location_qth_error
|
||||||
_positionSettings.value = _positionSettings.value.copy(messageResId = errorResId)
|
_positionSettings.update { it.copy(messageResId = errorResId) }
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fun dismissPosMessage() {
|
fun dismissPosMessage() {
|
||||||
_positionSettings.value = _positionSettings.value.copy(messageResId = 0)
|
_positionSettings.update { it.copy(messageResId = 0) }
|
||||||
}
|
}
|
||||||
|
|
||||||
fun updateFromWeb() = viewModelScope.launch {
|
fun updateFromWeb() = viewModelScope.launch {
|
||||||
try {
|
try {
|
||||||
_dataSettings.value = _dataSettings.value.copy(isUpdating = true)
|
_dataSettings.update { it.copy(isUpdating = true) }
|
||||||
databaseRepo.updateFromRemote()
|
databaseRepo.updateFromRemote()
|
||||||
} catch (exception: Exception) {
|
} catch (exception: Exception) {
|
||||||
_dataSettings.value = _dataSettings.value.copy(isUpdating = false)
|
_dataSettings.update { it.copy(isUpdating = false) }
|
||||||
println(exception)
|
println(exception)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fun updateFromFile(uri: String) = viewModelScope.launch {
|
fun updateFromFile(uri: String) = viewModelScope.launch {
|
||||||
try {
|
try {
|
||||||
_dataSettings.value = _dataSettings.value.copy(isUpdating = true)
|
_dataSettings.update { it.copy(isUpdating = true) }
|
||||||
databaseRepo.updateFromFile(uri)
|
databaseRepo.updateFromFile(uri)
|
||||||
} catch (exception: Exception) {
|
} catch (exception: Exception) {
|
||||||
_dataSettings.value = _dataSettings.value.copy(isUpdating = false)
|
_dataSettings.update { it.copy(isUpdating = false) }
|
||||||
println(exception)
|
println(exception)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in new issue
Block a user