Compare commits

..
15 Commits
Author SHA1 Message Date
Arty Bishop de99e09f5e v4.1.0 - Remote freq control for Hamlib/SDR++, by londre7 2026-02-22 13:51:58 +00:00
Arty Bishop e717d75324 Added a few visual fixes to extended data output feature 2026-02-21 12:31:59 +00:00
wty2019wty a545863473 Updated the Chinese translation for freq output (#196) 2026-02-21 09:35:22 +00:00
londre7 85ffe94266 Implemented extended data output (#195) 2026-02-20 18:42:55 +00:00
Arty Bishop 7adbf951bc v4.0.4 - R4UAB source priority, Chinese translation fixes 2026-02-14 14:12:36 +00:00
Mubi-Baihua 78dc217642 Revise the Chinese translation. (#194) 2026-02-14 12:22:25 +00:00
Arty Bishop 64137473b3 v4.0.3 - KeepScreenOn for RadarScreen, translation fixes 2026-02-10 15:25:28 +00:00
Kaitlyn Kenwell c08effb445 Add setting to keep the screen on in radar view (#193) 2026-02-09 19:31:55 +00:00
troilus 65410defe2 Translate custom data source strings to Chinese 2026-02-08 11:09:03 +00:00
Arty Bishop ed71c8148b v4.0.2 - Custom data import by londre7, minor UI tweaks 2026-02-07 17:22:42 +00:00
Arty Bishop 13a79af27e Added minor tweaks to import components and strings 2026-02-07 13:41:12 +00:00
londre7 f6564c3c71 Added custom data sources import, by londre7 (#189) 2026-02-07 13:03:12 +00:00
Arty Bishop 8e42f81ea3 Added tweaks to components, updated dependencies 2026-02-07 12:30:46 +00:00
Eugene ada5b48ba5 minor updates for uk localization 2026-02-02 20:28:48 +00:00
Arty Bishop a6a59877a8 v4.0.1 - Hotfix for RadarScreen track projection display 2026-01-31 13:55:44 +00:00
38 changed files with 1490 additions and 543 deletions

No files matched your search

+2 -2
View File
@@ -22,7 +22,7 @@ jobs:
uses: actions/setup-java@v5
with:
distribution: 'temurin'
java-version: '21'
java-version: '17'
- name: Setup Gradle
uses: gradle/actions/setup-gradle@v5
@@ -67,7 +67,7 @@ jobs:
serviceAccountJsonPlainText: ${{secrets.SERVICE_ACCOUNT_JSON}}
packageName: com.rtbishop.look4sat
releaseFiles: app/build/outputs/apk/release/look4sat.aab
track: beta
track: production
whatsNewDirectory: fastlane/metadata/android/en-US/whatsnew
- name: Create Release
+3 -3
View File
@@ -4,7 +4,7 @@ plugins {
}
kotlin {
jvmToolchain(21)
jvmToolchain(17)
}
android {
@@ -13,8 +13,8 @@ android {
defaultConfig {
applicationId = "com.rtbishop.look4sat"
minSdk = 24
versionCode = 400
versionName = "4.0.0"
versionCode = 410
versionName = "4.1.0"
}
buildFeatures {
compose = true
@@ -17,6 +17,9 @@
*/
package com.rtbishop.look4sat.presentation
import androidx.compose.animation.core.tween
import androidx.compose.animation.fadeIn
import androidx.compose.animation.fadeOut
import androidx.compose.material3.Icon
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.Text
@@ -76,7 +79,12 @@ fun MainScreen(navController: NavHostController = rememberNavController()) {
else -> NavigationSuiteType.ShortNavigationBarMedium
}
) {
NavHost(navController = navController, startDestination = startDestination) {
NavHost(
navController = navController,
startDestination = startDestination,
enterTransition = { fadeIn(animationSpec = tween(350)) },
exitTransition = { fadeOut(animationSpec = tween(350)) }
) {
satellitesDestination { navController.navigateUp() }
passesDestination { catNum: Int, aosTime: Long ->
val radarRoute = "${Screen.Radar.route}?catNum=${catNum}&aosTime=${aosTime}"
@@ -23,6 +23,7 @@ import androidx.compose.foundation.clickable
import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.PaddingValues
import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.RowScope
import androidx.compose.foundation.layout.Spacer
@@ -210,8 +211,9 @@ fun CardButton(onClick: () -> Unit, text: String, modifier: Modifier = Modifier)
onClick = { onClick() }, colors = ButtonDefaults.buttonColors(
containerColor = MaterialTheme.colorScheme.surfaceVariant,
contentColor = MaterialTheme.colorScheme.onSurfaceVariant
), shape = MaterialTheme.shapes.small, modifier = modifier
) { Text(text = text, fontSize = 17.sp) }
), shape = MaterialTheme.shapes.small, modifier = modifier,
contentPadding = PaddingValues(horizontal = 8.dp, vertical = 8.dp)
) { Text(text = text, fontSize = 16.sp, textAlign = TextAlign.Center) }
}
@Composable
@@ -253,7 +255,7 @@ fun EmptyListCard(message: String) {
modifier = Modifier.padding(32.dp)
) {
Text(text = """¯\_(ツ)_/¯""", fontSize = 32.sp)
Text(text = stringResource(R.string.empty_list_message), fontSize = 21.sp)
Text(text = stringResource(R.string.empty_list_message), fontSize = 21.sp, textAlign = TextAlign.Center)
Text(text = message, fontSize = 18.sp, textAlign = TextAlign.Center)
}
}
@@ -278,7 +280,7 @@ fun SharedDialog(
) {
Text(
text = title,
fontSize = 18.sp,
fontSize = 16.sp,
fontWeight = FontWeight.Medium,
color = MaterialTheme.colorScheme.primary,
modifier = Modifier.padding(start = padding, top = padding, end = padding)
@@ -297,7 +299,7 @@ fun SharedDialog(
@Composable
fun InfoDialog(title: String, text: String, onDismiss: () -> Unit) {
val padding = LocalSpacing.current.large
Dialog(onDismissRequest = onDismiss) {
Dialog(onDismissRequest = {}) {
ElevatedCard {
Column(
horizontalAlignment = Alignment.CenterHorizontally,
@@ -110,12 +110,12 @@ private fun PassesScreen(uiState: PassesState, navigateToRadar: (Int, Long) -> U
uiState.takeAction(PassesAction.FilterRadios(modes))
}
}
if (uiState.shouldSeeWelcome) {
if (uiState.shouldSeeWhatsNew) {
InfoDialog(
title = stringResource(R.string.pass_welcome_title),
text = stringResource(R.string.pass_welcome_message)
title = stringResource(R.string.pass_whatsnew_title),
text = stringResource(R.string.pass_whatsnew_message)
) {
uiState.takeAction(PassesAction.DismissWelcome)
uiState.takeAction(PassesAction.DismissWhatsNew)
}
}
val gridState = rememberLazyGridState()
@@ -31,12 +31,12 @@ data class PassesState(
val elevation: Double,
val modes: List<String>,
val itemsList: List<OrbitalPass>,
val shouldSeeWelcome: Boolean,
val shouldSeeWhatsNew: Boolean,
val takeAction: (PassesAction) -> Unit
)
sealed class PassesAction {
data object DismissWelcome : PassesAction()
data object DismissWhatsNew : PassesAction()
data class FilterPasses(val hoursAhead: Int, val minElevation: Double) : PassesAction()
data class FilterRadios(val modes: List<String>) : PassesAction()
data object RefreshPasses : PassesAction()
@@ -40,7 +40,8 @@ import kotlinx.coroutines.isActive
import kotlinx.coroutines.launch
class PassesViewModel(
private val satelliteRepo: ISatelliteRepo, private val settingsRepo: ISettingsRepo
private val satelliteRepo: ISatelliteRepo,
private val settingsRepo: ISettingsRepo
) : ViewModel() {
private val _uiState = MutableStateFlow(
@@ -56,7 +57,7 @@ class PassesViewModel(
elevation = settingsRepo.passesSettings.value.minElevation,
modes = settingsRepo.passesSettings.value.selectedModes,
itemsList = emptyList(),
shouldSeeWelcome = settingsRepo.otherSettings.value.shouldSeeWelcome,
shouldSeeWhatsNew = settingsRepo.otherSettings.value.shouldSeeWhatsNew,
takeAction = ::handleAction
)
)
@@ -81,14 +82,14 @@ class PassesViewModel(
}
viewModelScope.launch {
settingsRepo.otherSettings.collectLatest { settings ->
_uiState.update { it.copy(shouldSeeWelcome = settings.shouldSeeWelcome) }
_uiState.update { it.copy(shouldSeeWhatsNew = settings.shouldSeeWhatsNew) }
}
}
}
private fun handleAction(action: PassesAction) {
when (action) {
PassesAction.DismissWelcome -> settingsRepo.setWelcomeDismissed()
PassesAction.DismissWhatsNew -> settingsRepo.setWhatsNewDismissed()
is PassesAction.FilterPasses -> applyFilter(action.hoursAhead, action.minElevation, uiState.value.modes)
is PassesAction.FilterRadios -> applyFilter(uiState.value.hours, uiState.value.elevation, action.modes)
PassesAction.RefreshPasses -> refreshPasses()
@@ -25,6 +25,7 @@ import androidx.compose.animation.core.rememberInfiniteTransition
import androidx.compose.animation.core.tween
import androidx.compose.foundation.background
import androidx.compose.foundation.border
import androidx.compose.foundation.clickable
import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.Column
@@ -45,6 +46,7 @@ import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.draw.alpha
import androidx.compose.ui.draw.rotate
import androidx.compose.ui.keepScreenOn
import androidx.compose.ui.res.painterResource
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.semantics.contentDescription
@@ -64,6 +66,7 @@ import com.rtbishop.look4sat.domain.model.SatRadio
import com.rtbishop.look4sat.domain.utility.toDegrees
import com.rtbishop.look4sat.presentation.MainTheme
import com.rtbishop.look4sat.presentation.Screen
import com.rtbishop.look4sat.presentation.common.EmptyListCard
import com.rtbishop.look4sat.presentation.common.IconCard
import com.rtbishop.look4sat.presentation.common.NextPassRow
import com.rtbishop.look4sat.presentation.common.TimerRow
@@ -96,7 +99,7 @@ private fun RadarScreen(uiState: RadarState, navigateUp: () -> Unit) {
}
val upcomingPass = uiState.currentPass ?: getDefaultPass()
if (upcomingPass.losTime < System.currentTimeMillis()) navigateUp()
Column(modifier = Modifier.layoutPadding(), verticalArrangement = Arrangement.spacedBy(6.dp)) {
Column(modifier = Modifier.layoutPadding().keepScreenOn(), verticalArrangement = Arrangement.spacedBy(6.dp)) {
if (isVerticalLayout()) {
TopBar {
IconCard(action = navigateUp, resId = R.drawable.ic_back)
@@ -115,89 +118,18 @@ private fun RadarScreen(uiState: RadarState, navigateUp: () -> Unit) {
if(isVerticalLayout()) {
ElevatedCard(modifier = Modifier.weight(1f)) {
Box(contentAlignment = Alignment.Center) {
RadarViewCompose(
item = uiState.orbitalPos,
items = uiState.satTrack,
azimElev = uiState.orientationValues,
shouldShowSweep = uiState.shouldShowSweep,
shouldUseCompass = uiState.shouldUseCompass,
modifier = Modifier.align(Alignment.Center)
)
Column(
verticalArrangement = Arrangement.SpaceBetween,
modifier = Modifier
.fillMaxSize()
.padding(horizontal = 6.dp, vertical = 4.dp)
) {
Row(
horizontalArrangement = Arrangement.SpaceBetween,
modifier = Modifier.fillMaxWidth()
) {
RadarTextTop(
uiState.orbitalPos?.azimuth ?: 0.0,
stringResource(R.string.radar_az_text),
true
)
RadarTextTop(
uiState.orbitalPos?.elevation ?: 0.0,
stringResource(R.string.radar_el_text),
false
)
}
Row(
horizontalArrangement = Arrangement.SpaceBetween,
modifier = Modifier.fillMaxWidth()
) {
RadarTextBottom(
uiState.orbitalPos?.altitude ?: 0.0,
stringResource(R.string.radar_alt_text),
true
)
RadarTextBottom(
uiState.orbitalPos?.distance ?: 0.0,
stringResource(R.string.radar_dist_text),
false
)
if (uiState.orbitalPos == null) {
ElevatedCard(modifier = Modifier.fillMaxSize()) {
EmptyListCard(message = "")
}
}
}
}
ElevatedCard(modifier = Modifier.weight(1f)) {
if (uiState.transmitters.isEmpty()) {
Box(contentAlignment = Alignment.Center, modifier = Modifier.fillMaxSize()) {
Column(
horizontalAlignment = Alignment.CenterHorizontally,
verticalArrangement = Arrangement.spacedBy(24.dp),
modifier = Modifier.padding(32.dp)
) {
Text(text = """¯\_(ツ)_/¯""", fontSize = 32.sp)
Text(text = stringResource(R.string.empty_list_message), fontSize = 21.sp)
Text(
text = stringResource(R.string.radar_no_data),
fontSize = 18.sp,
textAlign = TextAlign.Center
)
}
}
} else {
Box(modifier = Modifier.fillMaxSize(), contentAlignment = Alignment.Center) {
TransmittersList(transmitters = uiState.transmitters)
if (uiState.orbitalPos?.eclipsed == true) {
EclipsedIndicator()
}
}
}
}
} else {
Row(horizontalArrangement = Arrangement.spacedBy(6.dp)) {
ElevatedCard(modifier = Modifier.weight(1f)) {
Box(contentAlignment = Alignment.Center) {
uiState.orbitalPos?.let { position ->
RadarViewCompose(
item = uiState.orbitalPos,
item = position,
items = uiState.satTrack,
azimElev = uiState.orientationValues,
shouldShowSweep = uiState.shouldShowSweep,
shouldUseCompass = false,
shouldUseCompass = uiState.shouldUseCompass,
modifier = Modifier.align(Alignment.Center)
)
Column(
@@ -211,12 +143,12 @@ private fun RadarScreen(uiState: RadarState, navigateUp: () -> Unit) {
modifier = Modifier.fillMaxWidth()
) {
RadarTextTop(
uiState.orbitalPos?.azimuth ?: 0.0,
position.azimuth,
stringResource(R.string.radar_az_text),
true
)
RadarTextTop(
uiState.orbitalPos?.elevation ?: 0.0,
position.elevation,
stringResource(R.string.radar_el_text),
false
)
@@ -226,12 +158,12 @@ private fun RadarScreen(uiState: RadarState, navigateUp: () -> Unit) {
modifier = Modifier.fillMaxWidth()
) {
RadarTextBottom(
uiState.orbitalPos?.altitude ?: 0.0,
position.altitude,
stringResource(R.string.radar_alt_text),
true
)
RadarTextBottom(
uiState.orbitalPos?.distance ?: 0.0,
position.distance,
stringResource(R.string.radar_dist_text),
false
)
@@ -239,6 +171,103 @@ private fun RadarScreen(uiState: RadarState, navigateUp: () -> Unit) {
}
}
}
}
ElevatedCard(modifier = Modifier.weight(1f)) {
if (uiState.transmitters.isEmpty()) {
Box(contentAlignment = Alignment.Center, modifier = Modifier.fillMaxSize()) {
Column(
horizontalAlignment = Alignment.CenterHorizontally,
verticalArrangement = Arrangement.spacedBy(24.dp),
modifier = Modifier.padding(32.dp)
) {
Text(text = """¯\_(ツ)_/¯""", fontSize = 32.sp)
Text(
text = stringResource(R.string.empty_list_message),
fontSize = 21.sp,
textAlign = TextAlign.Center
)
Text(
text = stringResource(R.string.radar_no_data),
fontSize = 18.sp,
textAlign = TextAlign.Center
)
}
}
} else {
Box(modifier = Modifier.fillMaxSize(), contentAlignment = Alignment.Center) {
TransmittersList(
transmitters = uiState.transmitters,
selectedUuid = uiState.selectedTransmitterUuid,
onSelect = { uuid ->
if (uiState.selectedTransmitterUuid != null) {
uiState.sendAction(RadarAction.SelectTransmitter(uuid))
}
}
)
if (uiState.orbitalPos?.eclipsed == true) {
EclipsedIndicator()
}
}
}
}
} else {
Row(horizontalArrangement = Arrangement.spacedBy(6.dp)) {
ElevatedCard(modifier = Modifier.weight(1f)) {
Box(contentAlignment = Alignment.Center) {
if (uiState.orbitalPos == null) {
ElevatedCard(modifier = Modifier.fillMaxSize()) {
EmptyListCard(message = "")
}
}
uiState.orbitalPos?.let { position ->
RadarViewCompose(
item = position,
items = uiState.satTrack,
azimElev = uiState.orientationValues,
shouldShowSweep = uiState.shouldShowSweep,
shouldUseCompass = false,
modifier = Modifier.align(Alignment.Center)
)
Column(
verticalArrangement = Arrangement.SpaceBetween,
modifier = Modifier
.fillMaxSize()
.padding(horizontal = 6.dp, vertical = 4.dp)
) {
Row(
horizontalArrangement = Arrangement.SpaceBetween,
modifier = Modifier.fillMaxWidth()
) {
RadarTextTop(
position.azimuth,
stringResource(R.string.radar_az_text),
true
)
RadarTextTop(
position.elevation,
stringResource(R.string.radar_el_text),
false
)
}
Row(
horizontalArrangement = Arrangement.SpaceBetween,
modifier = Modifier.fillMaxWidth()
) {
RadarTextBottom(
position.altitude,
stringResource(R.string.radar_alt_text),
true
)
RadarTextBottom(
position.distance,
stringResource(R.string.radar_dist_text),
false
)
}
}
}
}
}
ElevatedCard(modifier = Modifier.weight(1f)) {
if (uiState.transmitters.isEmpty()) {
Box(contentAlignment = Alignment.Center, modifier = Modifier.fillMaxSize()) {
@@ -248,7 +277,11 @@ private fun RadarScreen(uiState: RadarState, navigateUp: () -> Unit) {
modifier = Modifier.padding(32.dp)
) {
Text(text = """¯\_(ツ)_/¯""", fontSize = 32.sp)
Text(text = stringResource(R.string.empty_list_message), fontSize = 21.sp)
Text(
text = stringResource(R.string.empty_list_message),
fontSize = 21.sp,
textAlign = TextAlign.Center
)
Text(
text = stringResource(R.string.radar_no_data),
fontSize = 18.sp,
@@ -258,7 +291,15 @@ private fun RadarScreen(uiState: RadarState, navigateUp: () -> Unit) {
}
} else {
Box(modifier = Modifier.fillMaxSize(), contentAlignment = Alignment.Center) {
TransmittersList(transmitters = uiState.transmitters)
TransmittersList(
transmitters = uiState.transmitters,
selectedUuid = uiState.selectedTransmitterUuid,
onSelect = { uuid ->
if (uiState.selectedTransmitterUuid != null) {
uiState.sendAction(RadarAction.SelectTransmitter(uuid))
}
}
)
if (uiState.orbitalPos?.eclipsed == true) {
EclipsedIndicator()
}
@@ -291,10 +332,18 @@ private fun RadarTextBottom(value: Double, text: String, isLeft: Boolean) {
}
@Composable
private fun TransmittersList(transmitters: List<SatRadio>) {
private fun TransmittersList(
transmitters: List<SatRadio>,
selectedUuid: String?,
onSelect: (String) -> Unit
) {
LazyColumn(modifier = Modifier.fillMaxSize()) {
items(items = transmitters, key = { item -> item.uuid }) { radio ->
TransmitterItem(radio)
TransmitterItem(
radio,
(selectedUuid != null),
(radio.uuid == selectedUuid)
) { onSelect(radio.uuid) }
}
}
}
@@ -306,7 +355,7 @@ private fun TransmitterItemPreview() {
"", "Extremely powerful transmitter", true, 10000000000L, 10000000000L,
null, 10000000000L, 10000000000L, "FSK AX.100 Mode 5", true, 0
)
MainTheme { TransmitterItem(transmitter) }
MainTheme { TransmitterItem(transmitter, isClickable = true, isSelected = true, onClick = {}) }
}
@Composable
@@ -346,36 +395,46 @@ private fun EclipsedIndicator() {
}
@Composable
private fun TransmitterItem(radio: SatRadio) {
// LaunchedEffect(radio) {
// BluetoothCIV.updateOnce(radio)
// }
private fun TransmitterItem(
radio: SatRadio,
isClickable: Boolean,
isSelected: Boolean,
onClick: () -> Unit
) {
val title = if (radio.isInverted) "INVERTED: ${radio.info}" else radio.info
val fullTitle = "$title - (${radio.downlinkMode ?: "--"}/${radio.uplinkMode ?: "--"})"
Surface(color = MaterialTheme.colorScheme.background,
// modifier = Modifier.clickable(onClick = {
// Log.d("BluetoothCivManager", radio.toString())
// if(radio.uuid == BluetoothCIV.selected) BluetoothCIV.selected = "NONE" else
// {
// BluetoothCIV.connect(radio)
// BluetoothCIV.updateOnce(radio)
// }
// })
Surface(
color = MaterialTheme.colorScheme.background,
modifier = Modifier
.fillMaxWidth()
.then(if (isClickable) Modifier.clickable { onClick() } else Modifier)
) {
Surface(modifier = Modifier.padding(bottom = 2.dp)) {
Column(
horizontalAlignment = Alignment.CenterHorizontally,
verticalArrangement = Arrangement.spacedBy(6.dp),
modifier = Modifier
.background(MaterialTheme.colorScheme.surface)
.padding(horizontal = 8.dp, vertical = 6.dp)
) {
Text(
text = fullTitle,
textAlign = TextAlign.Center,
modifier = Modifier
.fillMaxWidth()
.infiniteMarquee()
)
Box {
Text(
text = fullTitle,
textAlign = TextAlign.Center,
modifier = Modifier
.fillMaxWidth()
.padding(horizontal = 6.dp)
.infiniteMarquee()
)
if (isSelected) {
Icon(
painter = painterResource(id = R.drawable.ic_radios),
contentDescription = null,
tint = MaterialTheme.colorScheme.primary,
modifier = Modifier.background(color = MaterialTheme.colorScheme.surface)
)
}
}
FrequencyRow(radio = radio, isDownlink = true)
FrequencyRow(radio = radio, isDownlink = false)
}
@@ -31,9 +31,12 @@ data class RadarState(
val shouldShowSweep: Boolean,
val shouldUseCompass: Boolean,
val transmitters: List<SatRadio>,
val selectedTransmitterUuid: String?,
val selectedFrequency: Long?,
val sendAction: (RadarAction) -> Unit
)
sealed class RadarAction {
data class AddToCalendar(val name: String, val aosTime: Long, val losTime: Long) : RadarAction()
data class SelectTransmitter(val uuid: String) : RadarAction()
}
@@ -17,7 +17,6 @@
*/
package com.rtbishop.look4sat.presentation.radar
import android.media.SoundPool
import androidx.compose.animation.core.animateFloat
import androidx.compose.animation.core.infiniteRepeatable
import androidx.compose.animation.core.rememberInfiniteTransition
@@ -27,7 +26,6 @@ import androidx.compose.foundation.layout.aspectRatio
import androidx.compose.material3.MaterialTheme
import androidx.compose.runtime.Composable
import androidx.compose.runtime.mutableFloatStateOf
import androidx.compose.runtime.mutableIntStateOf
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember
import androidx.compose.ui.Modifier
@@ -44,8 +42,6 @@ import androidx.compose.ui.graphics.drawscope.Fill
import androidx.compose.ui.graphics.drawscope.Stroke
import androidx.compose.ui.graphics.drawscope.rotate
import androidx.compose.ui.graphics.drawscope.translate
import androidx.compose.ui.platform.LocalContext
import androidx.compose.ui.platform.LocalView
import androidx.compose.ui.text.TextMeasurer
import androidx.compose.ui.text.TextStyle
import androidx.compose.ui.text.drawText
@@ -59,15 +55,15 @@ import kotlin.math.sin
@Composable
fun RadarViewCompose(
item: OrbitalPos?,
item: OrbitalPos,
items: List<OrbitalPos>,
azimElev: Pair<Float, Float>,
shouldShowSweep: Boolean,
shouldUseCompass: Boolean,
modifier: Modifier = Modifier
) {
val context = LocalContext.current
val view = LocalView.current
// val context = LocalContext.current
// val view = LocalView.current
val radarColor = MaterialTheme.colorScheme.secondary
val trackColor = MaterialTheme.colorScheme.primary
val aimColor = MaterialTheme.colorScheme.error
@@ -75,15 +71,15 @@ fun RadarViewCompose(
val animTransition = rememberInfiniteTransition(label = "animScale")
val animSpec = infiniteRepeatable<Float>(tween(1000))
val animScale = animTransition.animateFloat(16f, 64f, animSpec, label = "animScale")
val aimThreshold = 0.05f
// val aimThreshold = 0.05f
val measurer = rememberTextMeasurer()
val sweepDegrees = remember { mutableFloatStateOf(0f) }
val trackCreated = remember { mutableStateOf(false) }
val trackPath = remember { mutableStateOf(Path()) }
val trackEffect = remember { mutableStateOf(PathEffect.cornerPathEffect(0f)) }
val soundPool = remember { mutableStateOf<SoundPool?>(null) }
val beepSoundId = remember { mutableIntStateOf(0) }
val aimTargetDifference = remember { mutableFloatStateOf(0f) }
// val soundPool = remember { mutableStateOf<SoundPool?>(null) }
// val beepSoundId = remember { mutableIntStateOf(0) }
// val aimTargetDifference = remember { mutableFloatStateOf(0f) }
// LaunchedEffect(item.azimuth, item.elevation, azimElev.first, azimElev.second) {
// val aimAzimuthRadians = azimElev.first.toDouble().toRadians()
@@ -95,13 +91,10 @@ fun RadarViewCompose(
// val satX = sph2CartX(item.azimuth, item.elevation, radius)
// val satY = sph2CartY(item.azimuth, item.elevation, radius)
// aimTargetDifference.floatValue = sqrt((satX - aimX).pow(2) + (satY - aimY).pow(2))
//
//
// val minPlaybackRate = 0.5f
// val maxPlaybackRate = 2.0f
// val playbackRate =
// maxPlaybackRate - (aimTargetDifference.floatValue / (maxPlaybackRate - minPlaybackRate))
//
// soundPool.value?.setRate(beepSoundId.intValue, playbackRate)
// }
//
@@ -127,20 +120,16 @@ fun RadarViewCompose(
// .setUsage(AudioAttributes.USAGE_ASSISTANCE_SONIFICATION)
// .setContentType(AudioAttributes.CONTENT_TYPE_SONIFICATION)
// .build()
//
// soundPool.value = SoundPool.Builder()
// .setMaxStreams(1)
// .setAudioAttributes(audioAttributes)
// .build()
//
// beepSoundId.intValue = soundPool.value?.load(context, R.raw.beep, 1) ?: 0
//
// soundPool.value?.setOnLoadCompleteListener { soundPool, _, status ->
// if (status == 0) {
// soundPool.play(beepSoundId.intValue, 0.5f, 0.5f, 0, -1, 1f)
// }
// }
//
// onDispose {
// soundPool.value?.release()
// }
@@ -161,7 +150,7 @@ fun RadarViewCompose(
drawInfo(radius, trackColor, measurer, 3)
translate(center.x, center.y) {
drawTrack(trackPath.value, trackEffect.value, aimColor, trackColor)
if (item != null && item.elevation > 0) {
if (item.elevation > 0) {
drawPosition(item, radius, animScale.value, trackColor)
}
if (shouldUseCompass) {
@@ -25,7 +25,10 @@ import androidx.lifecycle.viewModelScope
import androidx.lifecycle.viewmodel.initializer
import androidx.lifecycle.viewmodel.viewModelFactory
import com.rtbishop.look4sat.MainApplication
import com.rtbishop.look4sat.data.framework.WithoutExtParams
import com.rtbishop.look4sat.data.framework.BluetoothReporter
import com.rtbishop.look4sat.data.framework.BtService
import com.rtbishop.look4sat.data.framework.ExtendedParams
import com.rtbishop.look4sat.data.framework.NetworkReporter
import com.rtbishop.look4sat.domain.model.SatRadio
import com.rtbishop.look4sat.domain.predict.OrbitalObject
@@ -65,6 +68,8 @@ class RadarViewModel(
satTrack = emptyList(),
shouldShowSweep = settingsRepo.otherSettings.value.stateOfSweep,
shouldUseCompass = settingsRepo.otherSettings.value.stateOfSensors,
selectedTransmitterUuid = null,
selectedFrequency = null,
transmitters = emptyList(),
sendAction = ::handleAction,
)
@@ -109,9 +114,9 @@ class RadarViewModel(
_uiState.update { it.copy(currentTime = time, isCurrentTimeAos = false) }
}
}
processRadios(transmitters, satPass.orbitalObject, timeNow)
sendPassData(satPass, pos, satPass.orbitalObject)
sendPassDataBT(pos)
processRadios(transmitters, satPass.orbitalObject, timeNow)
delay(1000)
}
}
@@ -126,6 +131,7 @@ class RadarViewModel(
private fun handleAction(action: RadarAction) {
when (action) {
is RadarAction.AddToCalendar -> addToCalendar(action.name, action.aosTime, action.losTime)
is RadarAction.SelectTransmitter -> { _uiState.update { it.copy(selectedTransmitterUuid = action.uuid) } }
}
}
@@ -141,25 +147,47 @@ class RadarViewModel(
if (settingsRepo.rcSettings.value.rotatorState) {
val server = settingsRepo.rcSettings.value.rotatorAddress
val port = settingsRepo.rcSettings.value.rotatorPort.toInt()
val format = settingsRepo.rcSettings.value.rotatorFormat
val azimuth = orbitalPos.azimuth.toDegrees().round(2)
val elevation = orbitalPos.elevation.toDegrees().round(2)
networkReporter.reportRotation(server, port, azimuth, elevation)
networkReporter.reportRotation(format, azimuth, elevation, ExtendedParams(server, port))
}
if (settingsRepo.rcSettings.value.frequencyState) {
_uiState.value.selectedFrequency?.let { freq ->
val server = settingsRepo.rcSettings.value.frequencyAddress
val port = settingsRepo.rcSettings.value.frequencyPort.toInt()
val format = settingsRepo.rcSettings.value.frequencyFormat
networkReporter.reportFrequency(format,freq, ExtendedParams(server, port))
}
}
}
}
private fun sendPassDataBT(orbitalPos: OrbitalPos) {
viewModelScope.launch {
if (settingsRepo.rcSettings.value.bluetoothState) {
val btDevice = settingsRepo.rcSettings.value.bluetoothAddress
if (bluetoothReporter.isConnected()) {
val format = settingsRepo.rcSettings.value.bluetoothFormat
val azimuth = orbitalPos.azimuth.toDegrees().round(0).toInt()
val elevation = orbitalPos.elevation.toDegrees().round(0).toInt()
bluetoothReporter.reportRotation(format, azimuth, elevation)
} else if (!bluetoothReporter.isConnecting()) {
// Log.i("BTReporter", "BTReporter: Attempting to connect...")
bluetoothReporter.connectBTDevice(btDevice)
if (settingsRepo.rcSettings.value.bluetoothRotatorState) {
val btRotatorDevice = settingsRepo.rcSettings.value.bluetoothRotatorAddress
if (bluetoothReporter.isConnected(BtService.ROTATOR)) {
val format = settingsRepo.rcSettings.value.bluetoothRotatorFormat
val azimuth = orbitalPos.azimuth.toDegrees().round(0)
val elevation = orbitalPos.elevation.toDegrees().round(0)
bluetoothReporter.reportRotation(format, azimuth, elevation, WithoutExtParams(0))
} else if (!bluetoothReporter.isConnecting(BtService.ROTATOR)) {
// Log.i("BTReporter", "BTReporter (rotator): Attempting to connect...")
bluetoothReporter.connect(BtService.ROTATOR,btRotatorDevice)
}
}
if (settingsRepo.rcSettings.value.bluetoothFrequencyState) {
val btFrequencyDevice = settingsRepo.rcSettings.value.bluetoothFrequencyAddress
if (bluetoothReporter.isConnected(BtService.FREQUENCY)) {
val format = settingsRepo.rcSettings.value.bluetoothFrequencyFormat
val frequency = _uiState.value.selectedFrequency
frequency?.let { freq ->
bluetoothReporter.reportFrequency(format, freq, WithoutExtParams(0))
}
} else if (!bluetoothReporter.isConnecting(BtService.FREQUENCY)) {
// Log.i("BTReporter", "BTReporter (frequency): Attempting to connect...")
bluetoothReporter.connect(BtService.FREQUENCY, btFrequencyDevice)
}
}
}
@@ -167,7 +195,33 @@ class RadarViewModel(
private suspend fun processRadios(radios: List<SatRadio>, orbitalObject: OrbitalObject, time: Long) {
val transmitters = satelliteRepo.getRadios(orbitalObject, stationPos, radios, time)
_uiState.update { it.copy(transmitters = transmitters) }
_uiState.update { state ->
if (!settingsRepo.rcSettings.value.frequencyState && !settingsRepo.rcSettings.value.bluetoothFrequencyState) {
return@update state.copy(
transmitters = transmitters,
selectedTransmitterUuid = null,
selectedFrequency = null
)
}
val selectedUuid = state.selectedTransmitterUuid ?: transmitters.firstOrNull()?.uuid
val selectedRadio = transmitters.firstOrNull { it.uuid == selectedUuid }
val freq = selectedRadio?.let { radio ->
val low = radio.downlinkLow
val high = radio.downlinkHigh
when {
low != null && high != null ->
(low + high) / 2
low != null ->
low
else -> null
}
}
state.copy(
transmitters = transmitters,
selectedTransmitterUuid = selectedUuid,
selectedFrequency = freq
)
}
}
companion object {
@@ -27,6 +27,20 @@ import androidx.compose.ui.tooling.preview.Preview
import com.rtbishop.look4sat.R
import com.rtbishop.look4sat.presentation.MainTheme
import com.rtbishop.look4sat.presentation.common.SharedDialog
import com.rtbishop.look4sat.presentation.common.CardButton
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.Spacer
import androidx.compose.foundation.layout.height
import androidx.compose.foundation.layout.padding
import androidx.compose.material3.Switch
import androidx.compose.ui.unit.dp
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import com.rtbishop.look4sat.presentation.LocalSpacing
import com.rtbishop.look4sat.domain.model.RCSettings
@Preview(showBackground = true)
@Composable
@@ -41,10 +55,16 @@ fun PositionDialog(lat: Double, lon: Double, dismiss: () -> Unit, save: (Double,
val titleText = stringResource(id = R.string.prefs_station_title)
val onAccept = { saveValues(latValue.value, lonValue.value, save).also { dismiss() } }
SharedDialog(title = titleText, onCancel = dismiss, onAccept = onAccept) {
Text(text = stringResource(id = R.string.prefs_station_lat_text))
OutlinedTextField(value = latValue.value, onValueChange = { latValue.value = it })
Text(text = stringResource(id = R.string.prefs_station_lon_text))
OutlinedTextField(value = lonValue.value, onValueChange = { lonValue.value = it })
OutlinedTextField(
value = latValue.value,
onValueChange = { latValue.value = it },
label = { Text(text = stringResource(id = R.string.prefs_station_lat_text)) }
)
OutlinedTextField(
value = lonValue.value,
onValueChange = { lonValue.value = it },
label = { Text(text = stringResource(id = R.string.prefs_station_lon_text)) }
)
}
}
@@ -67,7 +87,398 @@ fun LocatorDialog(qthLocator: String, dismiss: () -> Unit, save: (String) -> Uni
val locator = rememberSaveable { mutableStateOf(qthLocator) }
val onAccept = { save(locator.value).also { dismiss() } }
SharedDialog(title = stringResource(R.string.prefs_locator_title), onCancel = dismiss, onAccept = onAccept) {
Text(text = stringResource(id = R.string.prefs_locator_text))
OutlinedTextField(value = locator.value, onValueChange = { locator.value = it })
OutlinedTextField(
value = locator.value,
onValueChange = { locator.value = it },
label = { Text(text = stringResource(id = R.string.prefs_locator_text)) }
)
}
}
@Preview(showBackground = true)
@Composable
private fun TransceiversDialogPreview() {
MainTheme {
DataSourcesDialog(
useCustomTle = true,
useCustomTransceivers = true,
tleUrl = "https://example.com/tle.txt",
transceiversUrl = "https://example.com/tx.json",
onImportTle = {},
onImportTransceivers = {},
onDismiss = {},
onSave = { _, _, _, _ -> }
)
}
}
@Composable
fun DataSourcesDialog(
useCustomTle: Boolean,
useCustomTransceivers: Boolean,
tleUrl: String,
transceiversUrl: String,
onImportTle: () -> Unit,
onImportTransceivers: () -> Unit,
onDismiss: () -> Unit,
onSave: (Boolean, Boolean, String, String) -> Unit
) {
val padding = LocalSpacing.current.large
val isEnabledCustomTle = rememberSaveable { mutableStateOf(useCustomTle) }
val isEnabledCustomTransceivers = rememberSaveable { mutableStateOf(useCustomTransceivers) }
val urlTle = rememberSaveable { mutableStateOf(tleUrl) }
val urlTransceivers = rememberSaveable { mutableStateOf(transceiversUrl) }
val onAccept = {
onSave(isEnabledCustomTle.value, isEnabledCustomTransceivers.value, urlTle.value, urlTransceivers.value)
onDismiss()
}
val onCancel = { onDismiss() }
SharedDialog(
title = stringResource(id = R.string.prefs_data_sources_title),
onCancel = onCancel,
onAccept = onAccept,
) {
Column(modifier = Modifier.padding(horizontal = padding)) {
Row(horizontalArrangement = Arrangement.spacedBy(6.dp)) {
CardButton(
onClick = {
onImportTle()
onDismiss()
},
text = "TLE (3LE)\nR4UAB (.txt)",
modifier = Modifier.weight(1f)
)
CardButton(
onClick = {
onImportTransceivers()
onDismiss()
},
text = "Transceivers\nSatNOGS (.json)",
modifier = Modifier.weight(1f)
)
}
Spacer(modifier = Modifier.height(6.dp))
Row(
horizontalArrangement = Arrangement.SpaceBetween,
verticalAlignment = Alignment.CenterVertically,
modifier = Modifier.fillMaxWidth()
) {
Text(text = stringResource(id = R.string.prefs_data_sources_tle_switch))
Switch(
checked = isEnabledCustomTle.value,
onCheckedChange = { isEnabledCustomTle.value = it }
)
}
OutlinedTextField(
value = urlTle.value,
onValueChange = { urlTle.value = it },
label = { Text(text = stringResource(id = R.string.prefs_data_sources_url_title)) },
singleLine = true,
modifier = Modifier.fillMaxWidth(),
enabled = isEnabledCustomTle.value,
)
Spacer(modifier = Modifier.height(6.dp))
Row(
horizontalArrangement = Arrangement.SpaceBetween,
verticalAlignment = Alignment.CenterVertically,
modifier = Modifier.fillMaxWidth()
) {
Text(text = stringResource(id = R.string.prefs_data_sources_transceivers_switch))
Switch(
checked = isEnabledCustomTransceivers.value,
onCheckedChange = { isEnabledCustomTransceivers.value = it }
)
}
OutlinedTextField(
value = urlTransceivers.value,
onValueChange = { urlTransceivers.value = it },
label = { Text(text = stringResource(id = R.string.prefs_data_sources_url_title)) },
singleLine = true,
modifier = Modifier.fillMaxWidth(),
enabled = isEnabledCustomTransceivers.value,
)
}
}
}
@Preview(showBackground = true)
@Composable
fun PreviewNetworkOutputDialog() {
MainTheme {
NetworkOutputDialog(
initialSettings = RCSettings(
rotatorState = false,
rotatorAddress = "127.0.0.1",
rotatorPort = "4533",
rotatorFormat = $$"P $AZ $EL",
frequencyState = false,
frequencyAddress = "127.0.0.1",
frequencyPort = "4532",
frequencyFormat = $$"set_freq $FREQ",
bluetoothRotatorState = false,
bluetoothRotatorFormat = $$"W$AZ $EL",
bluetoothRotatorName = "Default",
bluetoothRotatorAddress = "00:0C:BF:13:80:5D",
bluetoothFrequencyState = false,
bluetoothFrequencyAddress = "00:0C:BF:13:80:5D",
bluetoothFrequencyFormat = $$"FA$FREQ"
),
onDismiss = {},
onSave = { _, _, _, _, _, _, _, _ -> }
)
}
}
@Composable
fun NetworkOutputDialog(
initialSettings: RCSettings,
onDismiss: () -> Unit,
onSave: (
Boolean, String, String, String,
Boolean, String, String, String
) -> Unit
) {
val padding = LocalSpacing.current.large
val rotatorState = rememberSaveable { mutableStateOf(initialSettings.rotatorState) }
val rotatorAddress = rememberSaveable { mutableStateOf(initialSettings.rotatorAddress) }
val rotatorPort = rememberSaveable { mutableStateOf(initialSettings.rotatorPort) }
val rotatorFormat = rememberSaveable { mutableStateOf(initialSettings.rotatorFormat) }
val frequencyState = rememberSaveable { mutableStateOf(initialSettings.frequencyState) }
val frequencyAddress = rememberSaveable { mutableStateOf(initialSettings.frequencyAddress) }
val frequencyPort = rememberSaveable { mutableStateOf(initialSettings.frequencyPort) }
val frequencyFormat = rememberSaveable { mutableStateOf(initialSettings.frequencyFormat) }
val onAccept = {
onSave(
rotatorState.value,
rotatorAddress.value,
rotatorPort.value,
rotatorFormat.value,
frequencyState.value,
frequencyAddress.value,
frequencyPort.value,
frequencyFormat.value
)
onDismiss()
}
SharedDialog(
title = stringResource(R.string.prefs_net_title),
onCancel = onDismiss,
onAccept = onAccept
) {
Column(modifier = Modifier.padding(horizontal = padding)) {
Row(
horizontalArrangement = Arrangement.SpaceBetween,
verticalAlignment = Alignment.CenterVertically,
modifier = Modifier.fillMaxWidth()
) {
Text(stringResource(R.string.prefs_net_rotator_switch))
Switch(
checked = rotatorState.value,
onCheckedChange = { rotatorState.value = it }
)
}
Row(
horizontalArrangement = Arrangement.spacedBy(6.dp),
modifier = Modifier.fillMaxWidth()
) {
OutlinedTextField(
value = rotatorAddress.value,
onValueChange = { rotatorAddress.value = it },
singleLine = true,
label = { Text(stringResource(R.string.prefs_net_rotator_ip_hint)) },
modifier = Modifier.weight(1.5f),
enabled = rotatorState.value
)
OutlinedTextField(
value = rotatorPort.value,
onValueChange = { rotatorPort.value = it },
singleLine = true,
label = { Text(stringResource(R.string.prefs_net_rotator_port_hint)) },
modifier = Modifier.weight(1f),
enabled = rotatorState.value
)
}
Spacer(modifier = Modifier.height(6.dp))
OutlinedTextField(
value = rotatorFormat.value,
onValueChange = { rotatorFormat.value = it },
singleLine = true,
label = { Text(stringResource(R.string.prefs_net_rotator_format_hint)) },
modifier = Modifier.fillMaxWidth(),
enabled = rotatorState.value
)
Spacer(modifier = Modifier.height(6.dp))
Row(
horizontalArrangement = Arrangement.SpaceBetween,
verticalAlignment = Alignment.CenterVertically,
modifier = Modifier.fillMaxWidth()
) {
Text(stringResource(R.string.prefs_net_frequency_switch))
Switch(
checked = frequencyState.value,
onCheckedChange = { frequencyState.value = it }
)
}
Row(
horizontalArrangement = Arrangement.spacedBy(6.dp),
modifier = Modifier.fillMaxWidth()
) {
OutlinedTextField(
value = frequencyAddress.value,
onValueChange = { frequencyAddress.value = it },
singleLine = true,
label = { Text(stringResource(R.string.prefs_net_frequency_ip_hint)) },
modifier = Modifier.weight(1.5f),
enabled = frequencyState.value
)
OutlinedTextField(
value = frequencyPort.value,
onValueChange = { frequencyPort.value = it },
singleLine = true,
label = { Text(stringResource(R.string.prefs_net_frequency_port_hint)) },
modifier = Modifier.weight(1f),
enabled = frequencyState.value
)
}
Spacer(modifier = Modifier.height(6.dp))
OutlinedTextField(
value = frequencyFormat.value,
onValueChange = { frequencyFormat.value = it },
maxLines = 2,
label = { Text(stringResource(R.string.prefs_net_frequency_format_hint)) },
modifier = Modifier.fillMaxWidth(),
enabled = frequencyState.value
)
}
}
}
@Preview(showBackground = true)
@Composable
fun PreviewBluetoothOutputDialog() {
MainTheme {
BluetoothOutputDialog(
initialSettings = RCSettings(
rotatorState = false,
rotatorAddress = "127.0.0.1",
rotatorPort = "4533",
rotatorFormat = $$"P $AZ $EL",
frequencyState = false,
frequencyAddress = "127.0.0.1",
frequencyPort = "4532",
frequencyFormat = $$"set_freq $FREQ",
bluetoothRotatorState = false,
bluetoothRotatorFormat = $$"W$AZ $EL",
bluetoothRotatorName = "Default",
bluetoothRotatorAddress = "00:0C:BF:13:80:5D",
bluetoothFrequencyState = false,
bluetoothFrequencyAddress = "00:0C:BF:13:80:5D",
bluetoothFrequencyFormat = $$"FA$FREQ"
),
onDismiss = {},
onSave = { _, _, _, _, _, _ -> }
)
}
}
@Composable
fun BluetoothOutputDialog(
initialSettings: RCSettings,
onDismiss: () -> Unit,
onSave: (
Boolean, String, String,
Boolean, String, String
) -> Unit
) {
val padding = LocalSpacing.current.large
val rotatorState = rememberSaveable { mutableStateOf(initialSettings.bluetoothRotatorState) }
val rotatorAddress = rememberSaveable { mutableStateOf(initialSettings.bluetoothRotatorAddress) }
val rotatorFormat = rememberSaveable { mutableStateOf(initialSettings.bluetoothRotatorFormat) }
val frequencyState = rememberSaveable { mutableStateOf(initialSettings.bluetoothFrequencyState) }
val frequencyAddress = rememberSaveable { mutableStateOf(initialSettings.bluetoothFrequencyAddress) }
val frequencyFormat = rememberSaveable { mutableStateOf(initialSettings.bluetoothFrequencyFormat) }
val onAccept = {
onSave(
rotatorState.value,
rotatorAddress.value,
rotatorFormat.value,
frequencyState.value,
frequencyAddress.value,
frequencyFormat.value
)
onDismiss()
}
SharedDialog(
title = stringResource(R.string.prefs_bt_title),
onCancel = onDismiss,
onAccept = onAccept
) {
Column(modifier = Modifier.padding(horizontal = padding)) {
Row(
horizontalArrangement = Arrangement.SpaceBetween,
verticalAlignment = Alignment.CenterVertically,
modifier = Modifier.fillMaxWidth()
) {
Text(stringResource(R.string.prefs_bt_rotator_switch))
Switch(
checked = rotatorState.value,
onCheckedChange = { rotatorState.value = it }
)
}
Row(
horizontalArrangement = Arrangement.spacedBy(6.dp),
modifier = Modifier.fillMaxWidth()
) {
OutlinedTextField(
value = rotatorAddress.value,
onValueChange = { rotatorAddress.value = it },
singleLine = true,
label = { Text(stringResource(R.string.prefs_bt_rotator_device_hint)) },
modifier = Modifier.weight(1.5f),
enabled = rotatorState.value
)
OutlinedTextField(
value = rotatorFormat.value,
onValueChange = { rotatorFormat.value = it },
singleLine = true,
label = { Text(stringResource(R.string.prefs_bt_rotator_output_hint)) },
modifier = Modifier.weight(1f),
enabled = rotatorState.value
)
}
Spacer(modifier = Modifier.height(6.dp))
Row(
horizontalArrangement = Arrangement.SpaceBetween,
verticalAlignment = Alignment.CenterVertically,
modifier = Modifier.fillMaxWidth()
) {
Text(stringResource(R.string.prefs_bt_frequency_switch))
Switch(
checked = frequencyState.value,
onCheckedChange = { frequencyState.value = it }
)
}
Row(
horizontalArrangement = Arrangement.spacedBy(6.dp),
modifier = Modifier.fillMaxWidth()
) {
OutlinedTextField(
value = frequencyAddress.value,
onValueChange = { frequencyAddress.value = it },
singleLine = true,
label = { Text(stringResource(R.string.prefs_bt_frequency_device_hint)) },
modifier = Modifier.weight(1.5f),
enabled = frequencyState.value
)
OutlinedTextField(
value = frequencyFormat.value,
onValueChange = { frequencyFormat.value = it },
singleLine = true,
label = { Text(stringResource(R.string.prefs_bt_frequency_output_hint)) },
modifier = Modifier.weight(1f),
enabled = frequencyState.value
)
}
}
}
}
@@ -32,12 +32,12 @@ import androidx.compose.foundation.layout.height
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.layout.width
import androidx.compose.foundation.lazy.grid.GridCells
import androidx.compose.foundation.lazy.grid.GridItemSpan
import androidx.compose.foundation.lazy.grid.LazyVerticalGrid
import androidx.compose.material3.ElevatedCard
import androidx.compose.material3.Icon
import androidx.compose.material3.LinearProgressIndicator
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.OutlinedTextField
import androidx.compose.material3.Switch
import androidx.compose.material3.Text
import androidx.compose.runtime.Composable
@@ -60,7 +60,6 @@ import androidx.navigation.NavGraphBuilder
import androidx.navigation.compose.composable
import com.rtbishop.look4sat.R
import com.rtbishop.look4sat.domain.model.OtherSettings
import com.rtbishop.look4sat.domain.model.RCSettings
import com.rtbishop.look4sat.domain.predict.GeoPos
import com.rtbishop.look4sat.presentation.MainTheme
import com.rtbishop.look4sat.presentation.Screen
@@ -87,19 +86,6 @@ fun NavGraphBuilder.settingsDestination() {
@Composable
private fun SettingsScreen(uiState: SettingsState) {
// Permissions setup
val bluetoothContract = ActivityResultContracts.RequestPermission()
val bluetoothError = stringResource(R.string.prefs_bt_perm_error)
val bluetoothPerm = when {
Build.VERSION.SDK_INT < Build.VERSION_CODES.S -> Manifest.permission.BLUETOOTH
else -> Manifest.permission.BLUETOOTH_CONNECT
}
val bluetoothRequest = rememberLauncherForActivityResult(bluetoothContract) { isGranted ->
if (!isGranted) {
uiState.sendRCAction(RCAction.SetBluetoothState(false))
uiState.sendSystemAction(SystemAction.ShowToast(bluetoothError))
}
}
val locationContract = ActivityResultContracts.RequestMultiplePermissions()
val locationError = stringResource(R.string.prefs_loc_gps_error)
val locationPermCoarse = Manifest.permission.ACCESS_COARSE_LOCATION
@@ -112,8 +98,11 @@ private fun SettingsScreen(uiState: SettingsState) {
}
}
val contentContract = ActivityResultContracts.GetContent()
val contentRequest = rememberLauncherForActivityResult(contentContract) { uri ->
uri?.let { uiState.sendAction(SettingsAction.UpdateFromFile(uri.toString())) }
val contentRequestForTle = rememberLauncherForActivityResult(contentContract) { uri ->
uri?.let { uiState.sendAction(SettingsAction.UpdateTLEFromFile(uri.toString())) }
}
val contentRequestForTransceivers = rememberLauncherForActivityResult(contentContract) { uri ->
uri?.let { uiState.sendAction(SettingsAction.UpdateTransceiversFromFile(uri.toString())) }
}
// Position settings
@@ -137,10 +126,39 @@ private fun SettingsScreen(uiState: SettingsState) {
LocatorDialog(posSettings.stationPos.qthLocator, showLocDialog, setQthPos)
}
// Data sources dialog
val dataSourcesDialogState = rememberSaveable { mutableStateOf(false) }
val showDataSourcesDialog = { dataSourcesDialogState.value = true }
val dismissDataSourcesDialog = { dataSourcesDialogState.value = false }
if (dataSourcesDialogState.value) {
DataSourcesDialog(
useCustomTle = uiState.dataSourcesSettings.useCustomTLE,
useCustomTransceivers = uiState.dataSourcesSettings.useCustomTransceivers,
tleUrl = uiState.dataSourcesSettings.tleUrl,
transceiversUrl = uiState.dataSourcesSettings.transceiversUrl,
onImportTle = { contentRequestForTle.launch("*/*") },
onImportTransceivers = { contentRequestForTransceivers.launch("*/*") },
onDismiss = dismissDataSourcesDialog,
onSave = {
useCustomTle, useCustomTransceivers, tleUrl, transceiversUrl ->
if (!useCustomTle || tleUrl.isNotBlank()) {
uiState.sendDataSourcesAction(DataSourcesAction.SetUseCustomTle(useCustomTle))
uiState.sendDataSourcesAction(DataSourcesAction.SetTleUrl(tleUrl))
}
if (!useCustomTransceivers || transceiversUrl.isNotBlank()) {
uiState.sendDataSourcesAction(DataSourcesAction.SetUseCustomTransceivers(useCustomTransceivers))
uiState.sendDataSourcesAction(DataSourcesAction.SetTransceiversUrl(transceiversUrl))
}
if (useCustomTle || useCustomTransceivers) {
uiState.sendAction(SettingsAction.UpdateFromWeb)
}
}
)
}
// Data settings
val dataSettings = uiState.dataSettings
val updateFromWeb: () -> Unit = { uiState.sendAction(SettingsAction.UpdateFromWeb) }
val updateFromFile = { contentRequest.launch("*/*") }
val clearAllData: () -> Unit = { uiState.sendAction(SettingsAction.ClearAllData) }
// RC settings
@@ -148,12 +166,84 @@ private fun SettingsScreen(uiState: SettingsState) {
val setRotatorState = { value: Boolean -> uiState.sendRCAction(RCAction.SetRotatorState(value)) }
val setRotatorAddress = { value: String -> uiState.sendRCAction(RCAction.SetRotatorAddress(value)) }
val setRotatorPort = { value: String -> uiState.sendRCAction(RCAction.SetRotatorPort(value)) }
val setBluetoothState = { value: Boolean ->
bluetoothRequest.launch(bluetoothPerm)
uiState.sendRCAction(RCAction.SetBluetoothState(value))
val setRotatorFormat = { value: String -> uiState.sendRCAction(RCAction.SetRotatorFormat(value)) }
val setFrequencyState = { value: Boolean -> uiState.sendRCAction(RCAction.SetFrequencyState(value)) }
val setFrequencyAddress = { value: String -> uiState.sendRCAction(RCAction.SetFrequencyAddress(value)) }
val setFrequencyPort = { value: String -> uiState.sendRCAction(RCAction.SetFrequencyPort(value)) }
val setFrequencyFormat = { value: String -> uiState.sendRCAction(RCAction.SetFrequencyFormat(value)) }
val setBluetoothRotatorState = { value: Boolean -> uiState.sendRCAction(RCAction.SetBluetoothRotatorState(value)) }
val setBluetoothRotatorAddress = { value: String -> uiState.sendRCAction(RCAction.SetBluetoothRotatorAddress(value)) }
val setBluetoothRotatorFormat = { value: String -> uiState.sendRCAction(RCAction.SetBluetoothRotatorFormat(value)) }
val setBluetoothFrequencyState = { value: Boolean -> uiState.sendRCAction(RCAction.SetBluetoothFrequencyState(value)) }
val setBluetoothFrequencyAddress = { value: String -> uiState.sendRCAction(RCAction.SetBluetoothFrequencyAddress(value)) }
val setBluetoothFrequencyFormat = { value: String -> uiState.sendRCAction(RCAction.SetBluetoothFrequencyFormat(value)) }
// Network data output
val networkDialogState = rememberSaveable { mutableStateOf(false) }
val showNetworkDialog = { networkDialogState.value = true }
val dismissNetworkDialog = { networkDialogState.value = false }
if (networkDialogState.value) {
NetworkOutputDialog(
initialSettings = rcSettings,
onDismiss = dismissNetworkDialog,
onSave = { rotatorState,
rotatorAddress,
rotatorPort,
rotatorFormat,
frequencyState,
frequencyAddress,
frequencyPort,
frequencyFormat ->
setRotatorState(rotatorState)
setRotatorAddress(rotatorAddress)
setRotatorPort(rotatorPort)
setRotatorFormat(rotatorFormat)
setFrequencyState(frequencyState)
setFrequencyAddress(frequencyAddress)
setFrequencyPort(frequencyPort)
setFrequencyFormat(frequencyFormat)
}
)
}
// Bluetooth data output
val bluetoothDialogState = rememberSaveable { mutableStateOf(false) }
val bluetoothContract = ActivityResultContracts.RequestPermission()
val bluetoothError = stringResource(R.string.prefs_bt_perm_error)
val bluetoothPerm = when {
Build.VERSION.SDK_INT < Build.VERSION_CODES.S -> Manifest.permission.BLUETOOTH
else -> Manifest.permission.BLUETOOTH_CONNECT
}
val bluetoothRequest = rememberLauncherForActivityResult(bluetoothContract) { isGranted ->
if (!isGranted)
{
uiState.sendRCAction(RCAction.SetBluetoothRotatorState(false))
uiState.sendRCAction(RCAction.SetBluetoothFrequencyState(false))
uiState.sendSystemAction(SystemAction.ShowToast(bluetoothError))
}
else { bluetoothDialogState.value = true }
}
val showBluetoothDialog = { bluetoothRequest.launch(bluetoothPerm) }
val dismissBluetoothDialog = { bluetoothDialogState.value = false }
if (bluetoothDialogState.value) {
BluetoothOutputDialog(
initialSettings = rcSettings,
onDismiss = dismissBluetoothDialog,
onSave = { rotatorState,
rotatorAddress,
rotatorFormat,
frequencyState,
frequencyAddress,
frequencyFormat ->
setBluetoothRotatorState(rotatorState)
setBluetoothRotatorAddress(rotatorAddress)
setBluetoothRotatorFormat(rotatorFormat)
setBluetoothFrequencyState(frequencyState)
setBluetoothFrequencyAddress(frequencyAddress)
setBluetoothFrequencyFormat(frequencyFormat)
}
)
}
val setBluetoothAddress = { value: String -> uiState.sendRCAction(RCAction.SetBluetoothAddress(value)) }
val setBluetoothFormat = { value: String -> uiState.sendRCAction(RCAction.SetBluetoothFormat(value)) }
// Other settings
val otherSettings = uiState.otherSettings
@@ -209,9 +299,8 @@ private fun SettingsScreen(uiState: SettingsState) {
item {
LocationCard(posSettings, setGpsPos, showPosDialog, showLocDialog, dismissPos, uiState.sendSystemAction)
}
item { DataCard(dataSettings, updateFromWeb, updateFromFile, clearAllData) }
item { NetworkOutputCard(rcSettings, setRotatorState, setRotatorAddress, setRotatorPort) }
item { BluetoothOutputCard(rcSettings, setBluetoothState, setBluetoothAddress, setBluetoothFormat) }
item { DataCard(dataSettings, updateFromWeb, clearAllData, showDataSourcesDialog) }
item(span = { GridItemSpan(maxLineSpan) }) { OutputCard({ showNetworkDialog() }, { showBluetoothDialog() }) }
item { OtherCard(otherSettings, toggleUtc, toggleUpdate, toggleSweep, toggleSensor) }
item { CardCredits() }
}
@@ -290,15 +379,15 @@ private fun LocationCard(
@Composable
private fun DataCardPreview() = MainTheme {
val settings = DataSettings(true, 5000, 2500, 0L)
DataCard(settings = settings, {}, {}) {}
DataCard(settings = settings, {}, {}, {})
}
@Composable
private fun DataCard(
settings: DataSettings,
updateFromWeb: () -> Unit,
updateFromFile: () -> Unit,
clearAllData: () -> Unit
clearAllData: () -> Unit,
showDataSourcesDialog: () -> Unit
) {
ElevatedCard(modifier = Modifier.fillMaxWidth()) {
Column(
@@ -320,21 +409,19 @@ private fun DataCard(
Text(text = stringResource(R.string.prefs_data_radios, settings.radiosTotal))
}
Spacer(modifier = Modifier.height(1.dp))
Row(horizontalArrangement = Arrangement.SpaceEvenly) {
Row(horizontalArrangement = Arrangement.spacedBy(6.dp)) {
CardButton(
onClick = { updateFromWeb() },
onClick = updateFromWeb,
text = stringResource(id = R.string.prefs_data_update),
modifier = Modifier.weight(1f)
)
Spacer(modifier = Modifier.width(6.dp))
CardButton(
onClick = { updateFromFile() },
onClick = showDataSourcesDialog,
text = stringResource(id = R.string.prefs_data_import),
modifier = Modifier.weight(1f)
)
Spacer(modifier = Modifier.width(6.dp))
CardButton(
onClick = { clearAllData() },
onClick = clearAllData,
text = stringResource(id = R.string.prefs_data_clear),
modifier = Modifier.weight(1f)
)
@@ -345,126 +432,32 @@ private fun DataCard(
@Preview(showBackground = true)
@Composable
private fun NetworkOutputCardPreview() = MainTheme {
val settings = RCSettings(
rotatorState = false,
rotatorAddress = "127.0.0.1",
rotatorPort = "4533",
bluetoothState = false,
bluetoothFormat = $$"W$AZ $EL",
bluetoothName = "Name",
bluetoothAddress = "00:0C:BF:13:80:5D"
)
NetworkOutputCard(settings)
}
private fun OutputCardPreview() = MainTheme { OutputCard({}, {}) }
@Composable
private fun NetworkOutputCard(
settings: RCSettings,
setRotatorState: (Boolean) -> Unit = {},
setRotatorAddress: (String) -> Unit = {},
setRotatorPort: (String) -> Unit = {}
private fun OutputCard(
onNetworkClick: () -> Unit,
onBluetoothClick: () -> Unit
) {
ElevatedCard(modifier = Modifier.fillMaxWidth()) {
Column(modifier = Modifier.padding(horizontal = 8.dp, vertical = 4.dp)) {
Text(
text = stringResource(id = R.string.prefs_net_title),
text = stringResource(id = R.string.prefs_data_output_title),
color = MaterialTheme.colorScheme.primary
)
Row(
horizontalArrangement = Arrangement.SpaceBetween,
verticalAlignment = Alignment.CenterVertically,
modifier = Modifier.fillMaxWidth()
) {
Text(text = stringResource(R.string.prefs_net_switch))
Switch(checked = settings.rotatorState, onCheckedChange = { setRotatorState(it) })
}
Row(
horizontalArrangement = Arrangement.spacedBy(8.dp),
verticalAlignment = Alignment.CenterVertically,
modifier = Modifier.fillMaxWidth()
) {
OutlinedTextField(
value = settings.rotatorAddress,
singleLine = true,
label = { Text(text = stringResource(R.string.prefs_net_ip_hint)) },
onValueChange = { setRotatorAddress(it) },
modifier = Modifier.weight(1.5f),
enabled = settings.rotatorState
Spacer(modifier = Modifier.height(6.dp))
Row(horizontalArrangement = Arrangement.spacedBy(6.dp)) {
CardButton(
onClick = onNetworkClick,
text = stringResource(id = R.string.prefs_net_output),
modifier = Modifier.weight(1f)
)
OutlinedTextField(
value = settings.rotatorPort,
onValueChange = { setRotatorPort(it) },
label = { Text(text = stringResource(R.string.prefs_net_port_hint)) },
singleLine = true,
modifier = Modifier.weight(1f),
enabled = settings.rotatorState
CardButton(
onClick = onBluetoothClick,
text = stringResource(id = R.string.prefs_bt_output),
modifier = Modifier.weight(1f)
)
}
Spacer(modifier = Modifier.height(4.dp))
}
}
}
@Preview(showBackground = true)
@Composable
private fun BluetoothOutputCardPreview() = MainTheme {
val settings = RCSettings(
rotatorState = false,
rotatorAddress = "127.0.0.1",
rotatorPort = "4533",
bluetoothState = false,
bluetoothFormat = $$"W$AZ $EL",
bluetoothName = "Name",
bluetoothAddress = "00:0C:BF:13:80:5D"
)
BluetoothOutputCard(settings)
}
@Composable
private fun BluetoothOutputCard(
settings: RCSettings,
setBluetoothState: (Boolean) -> Unit = {},
setBluetoothAddress: (String) -> Unit = {},
setBluetoothFormat: (String) -> Unit = {}
) {
ElevatedCard(modifier = Modifier.fillMaxWidth()) {
Column(modifier = Modifier.padding(horizontal = 8.dp, vertical = 4.dp)) {
Text(
text = stringResource(id = R.string.prefs_bt_title),
color = MaterialTheme.colorScheme.primary
)
Row(
horizontalArrangement = Arrangement.SpaceBetween,
verticalAlignment = Alignment.CenterVertically,
modifier = Modifier.fillMaxWidth()
) {
Text(text = stringResource(R.string.prefs_bt_switch))
Switch(checked = settings.bluetoothState, onCheckedChange = { setBluetoothState(it) })
}
Row(
horizontalArrangement = Arrangement.spacedBy(8.dp),
verticalAlignment = Alignment.CenterVertically,
modifier = Modifier.fillMaxWidth()
) {
OutlinedTextField(
value = settings.bluetoothAddress,
singleLine = true,
label = { Text(text = stringResource(R.string.prefs_bt_device_hint)) },
onValueChange = { setBluetoothAddress(it) },
modifier = Modifier.weight(1.5f),
enabled = settings.bluetoothState
)
OutlinedTextField(
value = settings.bluetoothFormat,
onValueChange = { setBluetoothFormat(it) },
label = { Text(text = stringResource(R.string.prefs_bt_output_hint)) },
singleLine = true,
modifier = Modifier.weight(1f),
enabled = settings.bluetoothState
)
}
Spacer(modifier = Modifier.height(4.dp))
}
}
}
@@ -479,7 +472,7 @@ private fun OtherCardPreview() = MainTheme {
stateOfUtc = false,
stateOfLightTheme = false,
shouldSeeWarning = false,
shouldSeeWelcome = false
shouldSeeWhatsNew = false
)
OtherCard(settings = values, {}, {}, {}, {})
}
@@ -19,6 +19,7 @@ package com.rtbishop.look4sat.presentation.settings
import com.rtbishop.look4sat.domain.model.OtherSettings
import com.rtbishop.look4sat.domain.model.RCSettings
import com.rtbishop.look4sat.domain.model.DataSourcesSettings
import com.rtbishop.look4sat.domain.predict.GeoPos
import com.rtbishop.look4sat.domain.predict.OrbitalPass
@@ -39,9 +40,11 @@ data class SettingsState(
val dataSettings: DataSettings,
val otherSettings: OtherSettings,
val rcSettings: RCSettings,
val dataSourcesSettings: DataSourcesSettings,
val sendAction: (SettingsAction) -> Unit,
val sendRCAction: (RCAction) -> Unit,
val sendSystemAction: (SystemAction) -> Unit
val sendSystemAction: (SystemAction) -> Unit,
val sendDataSourcesAction: (DataSourcesAction) -> Unit
)
sealed class SettingsAction {
@@ -50,7 +53,8 @@ sealed class SettingsAction {
data class SetQthPosition(val locator: String) : SettingsAction()
data object DismissPosMessages : SettingsAction()
data object UpdateFromWeb : SettingsAction()
data class UpdateFromFile(val uri: String) : SettingsAction()
data class UpdateTLEFromFile(val uri: String) : SettingsAction()
data class UpdateTransceiversFromFile(val uri: String) : SettingsAction()
data object ClearAllData : SettingsAction()
data class ToggleUtc(val value: Boolean) : SettingsAction()
data class ToggleUpdate(val value: Boolean) : SettingsAction()
@@ -67,8 +71,23 @@ sealed class RCAction {
data class SetRotatorState(val value: Boolean) : RCAction()
data class SetRotatorAddress(val value: String) : RCAction()
data class SetRotatorPort(val value: String) : RCAction()
data class SetBluetoothState(val value: Boolean) : RCAction()
data class SetBluetoothFormat(val value: String) : RCAction()
data class SetBluetoothName(val value: String) : RCAction()
data class SetBluetoothAddress(val value: String) : RCAction()
data class SetRotatorFormat(val value: String) : RCAction()
data class SetFrequencyState(val value: Boolean) : RCAction()
data class SetFrequencyAddress(val value: String) : RCAction()
data class SetFrequencyPort(val value: String) : RCAction()
data class SetFrequencyFormat(val value: String) : RCAction()
data class SetBluetoothRotatorState(val value: Boolean) : RCAction()
data class SetBluetoothRotatorFormat(val value: String) : RCAction()
data class SetBluetoothRotatorName(val value: String) : RCAction()
data class SetBluetoothRotatorAddress(val value: String) : RCAction()
data class SetBluetoothFrequencyState(val value: Boolean) : RCAction()
data class SetBluetoothFrequencyFormat(val value: String) : RCAction()
data class SetBluetoothFrequencyAddress(val value: String) : RCAction()
}
sealed class DataSourcesAction {
data class SetUseCustomTle(val value: Boolean) : DataSourcesAction()
data class SetUseCustomTransceivers(val value: Boolean) : DataSourcesAction()
data class SetTleUrl(val value: String) : DataSourcesAction()
data class SetTransceiversUrl(val value: String) : DataSourcesAction()
}
@@ -24,6 +24,7 @@ import androidx.lifecycle.viewmodel.initializer
import androidx.lifecycle.viewmodel.viewModelFactory
import com.rtbishop.look4sat.MainApplication
import com.rtbishop.look4sat.R
import com.rtbishop.look4sat.domain.model.DataSourcesSettings
import com.rtbishop.look4sat.domain.predict.OrbitalPass
import com.rtbishop.look4sat.domain.repository.IDatabaseRepo
import com.rtbishop.look4sat.domain.repository.ISatelliteRepo
@@ -45,25 +46,32 @@ class SettingsViewModel(
private val databaseRepo: IDatabaseRepo,
private val satelliteRepo: ISatelliteRepo,
private val settingsRepo: ISettingsRepo,
private val showToast: IShowToast,
appVersionName: String
private val showToast: IShowToast
) : ViewModel() {
private val defaultPosSettings = PositionSettings(false, settingsRepo.stationPosition.value, 0)
private val defaultDataSettings = DataSettings(false, 0, 0, 0L)
private val defaultDataSourcesSettings = DataSourcesSettings(
useCustomTLE = false,
useCustomTransceivers = false,
tleUrl = settingsRepo.dataSourcesSettings.value.tleUrl,
transceiversUrl = settingsRepo.dataSourcesSettings.value.transceiversUrl
)
private val _uiState = MutableStateFlow(
SettingsState(
nextTime = "00:00:00",
isNextTimeAos = true,
nextPass = getDefaultPass(),
appVersionName = appVersionName,
appVersionName = settingsRepo.appVersionName,
positionSettings = defaultPosSettings,
dataSettings = defaultDataSettings,
otherSettings = settingsRepo.otherSettings.value,
rcSettings = settingsRepo.rcSettings.value,
dataSourcesSettings = defaultDataSourcesSettings,
sendAction = ::handleAction,
sendRCAction = ::handleAction,
sendSystemAction = ::handleAction
sendSystemAction = ::handleAction,
sendDataSourcesAction = ::handleAction
)
)
private var processing: Job? = null
@@ -113,6 +121,11 @@ class SettingsViewModel(
_uiState.update { it.copy(otherSettings = settings) }
}
}
viewModelScope.launch {
settingsRepo.dataSourcesSettings.collect { settings ->
_uiState.update { it.copy(dataSourcesSettings = settings) }
}
}
}
private fun handleAction(action: SettingsAction) {
@@ -122,7 +135,8 @@ class SettingsViewModel(
is SettingsAction.SetQthPosition -> setQthPosition(action.locator)
SettingsAction.DismissPosMessages -> dismissPosMessage()
SettingsAction.UpdateFromWeb -> updateFromWeb()
is SettingsAction.UpdateFromFile -> updateFromFile(action.uri)
is SettingsAction.UpdateTLEFromFile -> updateTLEFromFile(action.uri)
is SettingsAction.UpdateTransceiversFromFile -> updateTransceiversFromFile(action.uri)
SettingsAction.ClearAllData -> clearAllData()
is SettingsAction.ToggleUtc -> settingsRepo.setStateOfUtc(action.value)
is SettingsAction.ToggleUpdate -> settingsRepo.setStateOfAutoUpdate(action.value)
@@ -137,10 +151,18 @@ class SettingsViewModel(
is RCAction.SetRotatorState -> settingsRepo.setRotatorState(action.value)
is RCAction.SetRotatorAddress -> settingsRepo.setRotatorAddress(action.value)
is RCAction.SetRotatorPort -> settingsRepo.setRotatorPort(action.value)
is RCAction.SetBluetoothState -> settingsRepo.setBluetoothState(action.value)
is RCAction.SetBluetoothAddress -> settingsRepo.setBluetoothAddress(action.value)
is RCAction.SetBluetoothFormat -> settingsRepo.setBluetoothFormat(action.value)
is RCAction.SetBluetoothName -> settingsRepo.setBluetoothName(action.value)
is RCAction.SetRotatorFormat -> settingsRepo.setRotatorFormat(action.value)
is RCAction.SetFrequencyState -> settingsRepo.setFrequencyState(action.value)
is RCAction.SetFrequencyAddress -> settingsRepo.setFrequencyAddress(action.value)
is RCAction.SetFrequencyPort -> settingsRepo.setFrequencyPort(action.value)
is RCAction.SetFrequencyFormat -> settingsRepo.setFrequencyFormat(action.value)
is RCAction.SetBluetoothRotatorState -> settingsRepo.setBluetoothRotatorState(action.value)
is RCAction.SetBluetoothRotatorAddress -> settingsRepo.setBluetoothRotatorAddress(action.value)
is RCAction.SetBluetoothRotatorFormat -> settingsRepo.setBluetoothRotatorFormat(action.value)
is RCAction.SetBluetoothRotatorName -> settingsRepo.setBluetoothRotatorName(action.value)
is RCAction.SetBluetoothFrequencyState -> settingsRepo.setBluetoothFrequencyState(action.value)
is RCAction.SetBluetoothFrequencyFormat -> settingsRepo.setBluetoothFrequencyFormat(action.value)
is RCAction.SetBluetoothFrequencyAddress -> settingsRepo.setBluetoothFrequencyAddress(action.value)
}
}
@@ -150,6 +172,15 @@ class SettingsViewModel(
}
}
private fun handleAction(action: DataSourcesAction) {
when (action) {
is DataSourcesAction.SetUseCustomTle -> settingsRepo.setUseCustomTle(action.value)
is DataSourcesAction.SetUseCustomTransceivers -> settingsRepo.setUseCustomTransceivers(action.value)
is DataSourcesAction.SetTleUrl -> settingsRepo.setTleUrl(action.value)
is DataSourcesAction.SetTransceiversUrl -> settingsRepo.setTransceiversUrl(action.value)
}
}
private fun setGpsPosition() {
if (settingsRepo.setStationPosition()) {
val messageResId = R.string.prefs_loc_success
@@ -217,11 +248,23 @@ class SettingsViewModel(
}
}
private fun updateFromFile(uri: String) = viewModelScope.launch {
private fun updateTLEFromFile(uri: String) = viewModelScope.launch {
try {
val newDataSettings = _uiState.value.dataSettings.copy(isUpdating = true)
_uiState.update { it.copy(dataSettings = newDataSettings) }
databaseRepo.updateFromFile(uri)
databaseRepo.updateTLEFromFile(uri)
} catch (exception: Exception) {
val newDataSettings = _uiState.value.dataSettings.copy(isUpdating = false)
_uiState.update { it.copy(dataSettings = newDataSettings) }
println(exception)
}
}
private fun updateTransceiversFromFile(uri: String) = viewModelScope.launch {
try {
val newDataSettings = _uiState.value.dataSettings.copy(isUpdating = true)
_uiState.update { it.copy(dataSettings = newDataSettings) }
databaseRepo.updateTransceiversFromFile(uri)
} catch (exception: Exception) {
val newDataSettings = _uiState.value.dataSettings.copy(isUpdating = false)
_uiState.update { it.copy(dataSettings = newDataSettings) }
@@ -253,8 +296,7 @@ class SettingsViewModel(
container.databaseRepo,
container.satelliteRepo,
container.settingsRepo,
container.provideShowToast(),
container.provideAppVersionName()
container.provideShowToast()
)
}
}
+22 -6
View File
@@ -108,15 +108,31 @@
<string name="prefs_data_clear_success">Datos eliminados con éxito</string>
<string name="prefs_data_update_success">Actualización completada con éxito</string>
<string name="prefs_data_sources_title">Custom data sources</string>
<string name="prefs_data_sources_tle_switch">Custom TLE URL</string>
<string name="prefs_data_sources_transceivers_switch">Custom transceivers URL</string>
<string name="prefs_data_output_title">Data output</string>
<string name="prefs_net_output">Network</string>
<string name="prefs_bt_output">Bluetooth</string>
<string name="prefs_net_title">Salida de datos de red</string>
<string name="prefs_net_switch">Habilitar salida</string>
<string name="prefs_net_ip_hint">Dirección IP</string>
<string name="prefs_net_port_hint">Puerto</string>
<string name="prefs_net_rotator_switch">Enable rotation output</string>
<string name="prefs_net_rotator_ip_hint">Dirección IP</string>
<string name="prefs_net_rotator_port_hint">Puerto</string>
<string name="prefs_net_rotator_format_hint">Formato de datos</string>
<string name="prefs_net_frequency_switch">Enable frequency output</string>
<string name="prefs_net_frequency_ip_hint">Dirección IP</string>
<string name="prefs_net_frequency_port_hint">Puerto</string>
<string name="prefs_net_frequency_format_hint">Formato de datos</string>
<string name="prefs_bt_title">Salida por Bluetooth</string>
<string name="prefs_bt_switch">Habilitar salida</string>
<string name="prefs_bt_device_hint">Id dispositivo</string>
<string name="prefs_bt_output_hint">Formato de datos</string>
<string name="prefs_bt_rotator_switch">Enable rotation output</string>
<string name="prefs_bt_rotator_device_hint">Id dispositivo</string>
<string name="prefs_bt_rotator_output_hint">Formato de datos</string>
<string name="prefs_bt_frequency_switch">Enable frequency output</string>
<string name="prefs_bt_frequency_device_hint">Id dispositivo</string>
<string name="prefs_bt_frequency_output_hint">Formato de datos</string>
<string name="prefs_bt_perm_error">Revisa los permisos de Bluetooth</string>
<string name="prefs_other_title">Otros ajustes</string>
+25 -9
View File
@@ -93,10 +93,10 @@
<string name="prefs_loc_qth_error">Неправильный локатор QTH</string>
<string name="prefs_loc_success">Позиция успешно обновлена</string>
<string name="prefs_station_title">Настройки местоположения</string>
<string name="prefs_station_lat_text">Установите широту наземной станции</string>
<string name="prefs_station_lon_text">Установите долготу наземной станции</string>
<string name="prefs_station_lat_text">Введите широту наземной станции</string>
<string name="prefs_station_lon_text">Введите долготу наземной станции</string>
<string name="prefs_locator_title">Настройки локатора QTH</string>
<string name="prefs_locator_text">Установить позицию используя локатор</string>
<string name="prefs_locator_text">Введите позицию через локатор</string>
<string name="prefs_data_title">Данные спутников</string>
<string name="prefs_data_entries">Спутников: %s</string>
@@ -107,15 +107,31 @@
<string name="prefs_data_clear_success">Очистка прошла успешно</string>
<string name="prefs_data_update_success">Обновление прошло успешно</string>
<string name="prefs_data_sources_title">Свои источники данных</string>
<string name="prefs_data_sources_tle_switch">Custom TLE URL</string>
<string name="prefs_data_sources_transceivers_switch">Custom Transceivers URL</string>
<string name="prefs_data_output_title">Вывод данных</string>
<string name="prefs_net_output">Сеть</string>
<string name="prefs_bt_output">Bluetooth</string>
<string name="prefs_net_title">Вывод сетевых данных</string>
<string name="prefs_net_switch">Включить вывод</string>
<string name="prefs_net_ip_hint">IP адрес</string>
<string name="prefs_net_port_hint">Порт</string>
<string name="prefs_net_rotator_switch">Включить вывод ротатора</string>
<string name="prefs_net_rotator_ip_hint">IP адрес</string>
<string name="prefs_net_rotator_port_hint">Порт</string>
<string name="prefs_net_rotator_format_hint">Формат</string>
<string name="prefs_net_frequency_switch">Включить вывод частоты</string>
<string name="prefs_net_frequency_ip_hint">IP адрес</string>
<string name="prefs_net_frequency_port_hint">Порт</string>
<string name="prefs_net_frequency_format_hint">Формат</string>
<string name="prefs_bt_title">Вывод данных Bluetooth</string>
<string name="prefs_bt_switch">Включить вывод</string>
<string name="prefs_bt_device_hint">Id устройства</string>
<string name="prefs_bt_output_hint">Формат данных</string>
<string name="prefs_bt_rotator_switch">Включить вывод ротатора</string>
<string name="prefs_bt_rotator_device_hint">Id устройства</string>
<string name="prefs_bt_rotator_output_hint">Формат</string>
<string name="prefs_bt_frequency_switch">Включить вывод частоты</string>
<string name="prefs_bt_frequency_device_hint">Id устройства</string>
<string name="prefs_bt_frequency_output_hint">Формат</string>
<string name="prefs_bt_perm_error">Нет разрешения использовать bluetooth</string>
<string name="prefs_other_title">Другие настройки</string>
+22 -6
View File
@@ -108,15 +108,31 @@
<string name="prefs_data_clear_success">දත්ත ඉවත් කිරීම සාර්ථකයි</string>
<string name="prefs_data_update_success">යාවත්කාලීනය සම්පූර්ණ කිරීම සාර්ථකයි</string>
<string name="prefs_data_sources_title">Custom data sources</string>
<string name="prefs_data_sources_tle_switch">Custom TLE URL</string>
<string name="prefs_data_sources_transceivers_switch">Custom transceivers URL</string>
<string name="prefs_data_output_title">Data output</string>
<string name="prefs_net_output">Network</string>
<string name="prefs_bt_output">Bluetooth</string>
<string name="prefs_net_title">ජාල ප්‍රතිදානය</string>
<string name="prefs_net_switch">ප්‍රතිදානය සක්‍රීය කරන්න</string>
<string name="prefs_net_ip_hint">IP ලිපිනය</string>
<string name="prefs_net_port_hint">Port</string>
<string name="prefs_net_rotator_switch">Enable rotation output</string>
<string name="prefs_net_rotator_ip_hint">IP ලිපිනය</string>
<string name="prefs_net_rotator_port_hint">Port</string>
<string name="prefs_net_rotator_format_hint">දත්ත අකාරය</string>
<string name="prefs_net_frequency_switch">Enable frequency output</string>
<string name="prefs_net_frequency_ip_hint">IP ලිපිනය</string>
<string name="prefs_net_frequency_port_hint">Port</string>
<string name="prefs_net_frequency_format_hint">දත්ත අකාරය</string>
<string name="prefs_bt_title">Bluetooth දත්ත ප්‍රතිදානය</string>
<string name="prefs_bt_switch">ප්‍රතිදානය සක්‍රීය කරන්න</string>
<string name="prefs_bt_device_hint">උපාංග id</string>
<string name="prefs_bt_output_hint">දත්ත අකාරය</string>
<string name="prefs_bt_rotator_switch">Enable rotation output</string>
<string name="prefs_bt_rotator_device_hint">උපාංග id</string>
<string name="prefs_bt_rotator_output_hint">දත්ත අකාරය</string>
<string name="prefs_bt_frequency_switch">Enable frequency output</string>
<string name="prefs_bt_frequency_device_hint">උපාංග id</string>
<string name="prefs_bt_frequency_output_hint">දත්ත අකාරය</string>
<string name="prefs_bt_perm_error">Bluetooth අවසර පරික්‍ෂා ක°</string>
<string name="prefs_other_title">වෙනත් සැකසුම්</string>
+28 -12
View File
@@ -17,7 +17,7 @@
<string name="sat_type_title">Виберіть тип супутника</string>
<string name="sat_search_hint">Id - Назва</string>
<string name="sat_search_clear">Очистити</string>
<string name="sat_clear_all">Очистити</string>
<string name="sat_clear_all">Очистити всі</string>
<string name="sat_select_all">Вибрати всі</string>
<string name="sat_empty_list_message">
Переконайтеся, що ваш пошуковий запит правильний, а база даних оновлена</string>
@@ -87,15 +87,15 @@
<string name="prefs_loc_title">Місцезнаходження</string>
<string name="prefs_loc_gps_title">GPS</string>
<string name="prefs_loc_gps_error">Перевірте дозволи на визначення місця</string>
<string name="prefs_loc_gps_error">Перевірте дозвіл на використання геолокацї</string>
<string name="prefs_loc_input_title">Ввести</string>
<string name="prefs_loc_input_error">Введені некоректні дані</string>
<string name="prefs_loc_input_error">Введено некоректну локацію</string>
<string name="prefs_loc_qth_title">QTH</string>
<string name="prefs_loc_qth_error">Некоректний локатор QTH</string>
<string name="prefs_loc_success">Позицію успішно оновлено</string>
<string name="prefs_station_title">Налаштування місцезнаходження станції</string>
<string name="prefs_station_lat_text">Задайте широту станції</string>
<string name="prefs_station_lon_text">Задайте довготу станції</string>
<string name="prefs_station_lat_text">Широта місцезнаходження станції</string>
<string name="prefs_station_lon_text">Довгота місцезнаходження станції</string>
<string name="prefs_locator_title">Налаштування QTH локатора</string>
<string name="prefs_locator_text">Задати позицію за QTH локатором</string>
@@ -108,15 +108,31 @@
<string name="prefs_data_clear_success">Дані успішно очищено</string>
<string name="prefs_data_update_success">Оновлення успішне</string>
<string name="prefs_data_sources_title">Custom data sources</string>
<string name="prefs_data_sources_tle_switch">Custom TLE URL</string>
<string name="prefs_data_sources_transceivers_switch">Custom transceivers URL</string>
<string name="prefs_data_output_title">Data output</string>
<string name="prefs_net_output">Network</string>
<string name="prefs_bt_output">Bluetooth</string>
<string name="prefs_net_title">Вивід мережевих даних</string>
<string name="prefs_net_switch">Увімкнути вихід</string>
<string name="prefs_net_ip_hint">IP адреса</string>
<string name="prefs_net_port_hint">Порт</string>
<string name="prefs_net_rotator_switch">Enable rotation output</string>
<string name="prefs_net_rotator_ip_hint">IP адреса</string>
<string name="prefs_net_rotator_port_hint">Порт</string>
<string name="prefs_net_rotator_format_hint">Формат даних</string>
<string name="prefs_net_frequency_switch">Enable frequency output</string>
<string name="prefs_net_frequency_ip_hint">IP адреса</string>
<string name="prefs_net_frequency_port_hint">Порт</string>
<string name="prefs_net_frequency_format_hint">Формат даних</string>
<string name="prefs_bt_title">Вивід даних Bluetooth</string>
<string name="prefs_bt_switch">Увімкнути вихід</string>
<string name="prefs_bt_device_hint">Id пристрою</string>
<string name="prefs_bt_output_hint">Формат даних</string>
<string name="prefs_bt_rotator_switch">Enable rotation output</string>
<string name="prefs_bt_rotator_device_hint">Id пристрою</string>
<string name="prefs_bt_rotator_output_hint">Формат даних</string>
<string name="prefs_bt_frequency_switch">Enable frequency output</string>
<string name="prefs_bt_frequency_device_hint">Id пристрою</string>
<string name="prefs_bt_frequency_output_hint">Формат даних</string>
<string name="prefs_bt_perm_error">Перевірте дозвіл Bluetooth</string>
<string name="prefs_other_title">Інші налаштування</string>
@@ -126,6 +142,6 @@
<string name="prefs_other_switch_sensors">Використовувати сенсори пристрою</string>
<string name="prefs_outro_title">Я хотів би подякувати:</string>
<string name="prefs_outro_license">Це ПЗ поставляється без жодних гарантій.</string>
<string name="prefs_outro_license">Ця програма поставляється без жодних гарантій</string>
</resources>
+23 -7
View File
@@ -80,7 +80,7 @@
<string name="prefs_loc_title">站位</string>
<string name="prefs_loc_gps_title">GPS 定位</string>
<string name="prefs_loc_gps_error">检查您的位置权限</string>
<string name="prefs_loc_input_title">进入</string>
<string name="prefs_loc_input_title">输入位置</string>
<string name="prefs_loc_input_error">输入的位置无效</string>
<string name="prefs_loc_qth_title">QTH 网格</string>
<string name="prefs_loc_qth_error">无效的 QTH 网格</string>
@@ -100,15 +100,31 @@
<string name="prefs_data_clear_success">数据清空成功</string>
<string name="prefs_data_update_success">更新成功</string>
<string name="prefs_data_sources_title">自定义数据源</string>
<string name="prefs_data_sources_tle_switch">自定义TLE URL</string>
<string name="prefs_data_sources_transceivers_switch">自定义收发器URL</string>
<string name="prefs_data_output_title">数据输出</string>
<string name="prefs_net_output">网络</string>
<string name="prefs_bt_output">蓝牙</string>
<string name="prefs_net_title">网络数据输出</string>
<string name="prefs_net_switch">启用输出</string>
<string name="prefs_net_ip_hint">IP地址</string>
<string name="prefs_net_port_hint">端口</string>
<string name="prefs_net_rotator_switch">启用方位俯仰角输出</string>
<string name="prefs_net_rotator_ip_hint">IP地址</string>
<string name="prefs_net_rotator_port_hint">端口</string>
<string name="prefs_net_rotator_format_hint">数据格式</string>
<string name="prefs_net_frequency_switch">启用多普勒频率输出</string>
<string name="prefs_net_frequency_ip_hint">IP地址</string>
<string name="prefs_net_frequency_port_hint">端口</string>
<string name="prefs_net_frequency_format_hint">数据格式</string>
<string name="prefs_bt_title">蓝牙数据输出</string>
<string name="prefs_bt_switch">启用输出</string>
<string name="prefs_bt_device_hint">蓝牙设备 ID</string>
<string name="prefs_bt_output_hint">数据格式</string>
<string name="prefs_bt_rotator_switch">启用方位俯仰角输出</string>
<string name="prefs_bt_rotator_device_hint">蓝牙设备 ID</string>
<string name="prefs_bt_rotator_output_hint">数据格式</string>
<string name="prefs_bt_frequency_switch">启用多普勒频率输出</string>
<string name="prefs_bt_frequency_device_hint">蓝牙设备 ID</string>
<string name="prefs_bt_frequency_output_hint">数据格式</string>
<string name="prefs_bt_perm_error">请检查蓝牙权限</string>
<string name="prefs_other_title">其他设置</string>
+29 -7
View File
@@ -20,7 +20,7 @@
<string name="sat_search_clear">Clear</string>
<string name="sat_clear_all">Clear all</string>
<string name="sat_select_all">Select all</string>
<string name="sat_empty_list_message">" Make sure your search query is correct and the DB is updated"</string>
<string name="sat_empty_list_message">"Make sure your search query is correct and the DB is updated"</string>
<string name="sat_warning_title">Warning\!</string>
<string name="sat_warning_message">
There are over 9000 satellites listed in this app.
@@ -46,6 +46,11 @@
<string name="pass_welcome_message">
Make sure to set your position via GPS, Lat/Lon or QTH in Settings.
\n\nPlease update the database at least weekly to get accurate predictions.</string>
<string name="pass_whatsnew_title" translatable="false">What\'s new in Look4Sat</string>
<string name="pass_whatsnew_message" translatable="false">
• Londre7 added a remote frequency control feature (Hamlib/SDR++) by sending compatible messages via Network/BT.
Enable it in Settings and tap the transceiver (Radar Screen).
</string>
<!-- Radar screen -->
<string name="radar_back">Back</string>
@@ -131,15 +136,32 @@
<string name="prefs_data_clear_success">Data was cleared successfully</string>
<string name="prefs_data_update_success">Update completed successfully</string>
<string name="prefs_data_sources_title">Custom data sources</string>
<string name="prefs_data_sources_tle_switch">Custom TLE URL</string>
<string name="prefs_data_sources_transceivers_switch">Custom transceivers URL</string>
<string name="prefs_data_sources_url_title" translatable="false">URL (HTTPS)</string>
<string name="prefs_data_output_title">Data output</string>
<string name="prefs_net_output">Network</string>
<string name="prefs_bt_output">Bluetooth</string>
<string name="prefs_net_title">Network data output</string>
<string name="prefs_net_switch">Enable output</string>
<string name="prefs_net_ip_hint">IP address</string>
<string name="prefs_net_port_hint">Port</string>
<string name="prefs_net_rotator_switch">Enable rotation output</string>
<string name="prefs_net_rotator_ip_hint">IP address</string>
<string name="prefs_net_rotator_port_hint">Port</string>
<string name="prefs_net_rotator_format_hint">Format</string>
<string name="prefs_net_frequency_switch">Enable frequency output</string>
<string name="prefs_net_frequency_ip_hint">IP address</string>
<string name="prefs_net_frequency_port_hint">Port</string>
<string name="prefs_net_frequency_format_hint">Format</string>
<string name="prefs_bt_title">Bluetooth data output</string>
<string name="prefs_bt_switch">Enable output</string>
<string name="prefs_bt_device_hint">Device id</string>
<string name="prefs_bt_output_hint">Data format</string>
<string name="prefs_bt_rotator_switch">Enable rotation output</string>
<string name="prefs_bt_rotator_device_hint">Device id</string>
<string name="prefs_bt_rotator_output_hint">Data format</string>
<string name="prefs_bt_frequency_switch">Enable frequency output</string>
<string name="prefs_bt_frequency_device_hint">Device id</string>
<string name="prefs_bt_frequency_output_hint">Data format</string>
<string name="prefs_bt_perm_error">Check your bluetooth permission</string>
<string name="prefs_other_title">Other settings</string>
+1 -1
View File
@@ -4,7 +4,7 @@ plugins {
}
kotlin {
jvmToolchain(21)
jvmToolchain(17)
}
android {
@@ -18,6 +18,7 @@
package com.rtbishop.look4sat.data.framework
import android.bluetooth.BluetoothManager
import android.bluetooth.BluetoothSocket
import android.util.Log
import java.io.OutputStream
import java.util.*
@@ -25,69 +26,110 @@ import kotlin.math.abs
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Job
import kotlinx.coroutines.launch
import kotlinx.coroutines.sync.Mutex
import kotlinx.coroutines.sync.withLock
import com.rtbishop.look4sat.domain.repository.IReporterParams
import com.rtbishop.look4sat.domain.repository.IReporterRepo
data class WithoutExtParams(
val dummy: Int
) : IReporterParams
enum class BtService { ROTATOR, FREQUENCY }
data class DeviceConnection(
var socket: BluetoothSocket? = null,
var outputStream: OutputStream? = null,
var connected: Boolean = false,
var connecting: Boolean = false,
var connectionJob: Job? = null
)
class BluetoothReporter(
private val bluetoothManager: BluetoothManager,
private val reporterScope: CoroutineScope
) {
) : IReporterRepo<WithoutExtParams> {
private val tag = "BTReporter"
private val sppid: UUID = UUID.fromString("00001101-0000-1000-8000-00805f9b34fb")
private lateinit var outputStream: OutputStream
private var rotationConnectionJob: Job? = null
private var rotationReportingJob: Job? = null
private var connected = false
private var connecting = false
fun isConnected(): Boolean = connected
private val serviceToDevice = mutableMapOf<BtService, String>()
private val deviceConnections = mutableMapOf<String, DeviceConnection>()
private val writeMutex = Mutex()
fun isConnecting(): Boolean = connecting
fun isConnected(service: BtService): Boolean {
val deviceId = serviceToDevice[service] ?: return false
return deviceConnections[deviceId]?.connected == true
}
fun isConnecting(service: BtService): Boolean {
val deviceId = serviceToDevice[service] ?: return false
return deviceConnections[deviceId]?.connecting == true
}
fun connectBTDevice(deviceId: String) {
if (!connected) {
rotationConnectionJob = reporterScope.launch {
try {
bluetoothManager.adapter.getRemoteDevice(deviceId)?.let { device ->
device.createInsecureRfcommSocketToServiceRecord(sppid)?.let { socket ->
connecting = true
socket.connect()
outputStream = socket.outputStream
connected = true
connecting = false
Log.i(tag, "$tag: Connected!")
}
}
} catch (e: SecurityException) {
Log.e(tag, "$tag: ${e.message}")
} catch (e: Exception) {
Log.e(tag, "$tag: ${e.message}")
}
fun connect(service: BtService, deviceId: String) {
serviceToDevice[service] = deviceId
val connection = deviceConnections.getOrPut(deviceId) {
DeviceConnection()
}
if (connection.connected || connection.connecting) return
connection.connectionJob = reporterScope.launch {
try {
connection.connecting = true
val device = bluetoothManager.adapter.getRemoteDevice(deviceId)
val socket = device.createInsecureRfcommSocketToServiceRecord(sppid)
socket.connect()
connection.socket = socket
connection.outputStream = socket.outputStream
connection.connected = true
Log.i(tag, "$tag: Connected to $deviceId")
} catch (e: Exception) {
Log.e(tag, "$tag: ${e.message}")
connection.connected = false
} finally {
connection.connecting = false
}
}
}
fun reportRotation(format: String, azimuth: Int, elevation: Int) {
if (connected) {
rotationReportingJob = reporterScope.launch {
val newElevation = if (elevation > 0) elevation else 0
try {
val azimuthString = intToStringWithLeadingZeroes(azimuth)
val elevationString = intToStringWithLeadingZeroes(newElevation)
val crChar = '\r'
val nlChar = '\n'
val tbChar = '\t'
var buffer = format.replace("\$AZ", azimuthString)
buffer = buffer.replace("\$EL", elevationString)
buffer = buffer.replace("\\r", crChar.toString())
buffer = buffer.replace("\\n", nlChar.toString())
buffer = buffer.replace("\\t", tbChar.toString())
Log.i(tag, "$tag: Sending $buffer")
if (connected) outputStream.write(buffer.toByteArray())
} catch (e: Exception) {
Log.e(tag, "$tag: ${e.message}")
connected = false
}
private suspend fun write(service: BtService, buffer: String) {
val deviceId = serviceToDevice[service] ?: return
val connection = deviceConnections[deviceId] ?: return
if (!connection.connected) return
try {
writeMutex.withLock {
connection.outputStream?.write(buffer.toByteArray())
}
} catch (e: Exception) {
Log.e(tag, "$tag: Write failed ${e.message}")
connection.connected = false
}
}
override fun reportRotation(format: String, azimuth: Double, elevation: Double, params: WithoutExtParams) {
reporterScope.launch {
if (!isConnected(BtService.ROTATOR)) return@launch
val newElevation = if (elevation > 0.0) elevation else 0.0
val azimuthString = intToStringWithLeadingZeroes(azimuth.toInt())
val elevationString = intToStringWithLeadingZeroes(newElevation.toInt())
var buffer = format
.replace("\$AZ", azimuthString)
.replace("\$EL", elevationString)
.replace("\\r", "\r")
.replace("\\n", "\n")
.replace("\\t", "\t")
write(BtService.ROTATOR, buffer)
}
}
override fun reportFrequency(format: String, frequency: Long, params: WithoutExtParams) {
reporterScope.launch {
if (!isConnected(BtService.FREQUENCY)) return@launch
var buffer = format
.replace("\$FREQ", frequency.toString())
.replace("\\r", "\r")
.replace("\\n", "\n")
.replace("\\t", "\t")
write(BtService.FREQUENCY, buffer)
}
}
@@ -22,49 +22,94 @@ import java.nio.ByteBuffer
import java.nio.channels.SocketChannel
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Job
import kotlinx.coroutines.cancelAndJoin
import kotlinx.coroutines.sync.Mutex
import kotlinx.coroutines.sync.withLock
import kotlinx.coroutines.launch
import com.rtbishop.look4sat.domain.repository.IReporterParams
import com.rtbishop.look4sat.domain.repository.IReporterRepo
class NetworkReporter(private val reporterScope: CoroutineScope) {
data class ExtendedParams(
val server: String,
val port: Int
) : IReporterParams
private var rotationSocketChannel: SocketChannel? = null
private var rotationReporting: Job? = null
enum class NetworkService { ROTATOR, FREQUENCY }
// private var frequencySocketChannel: SocketChannel? = null
// private var frequencyReporting: Job? = null
//
// fun reportFrequency(server: String, port: Int, frequency: Long) {
// frequencyReporting = reporterScope.launch {
// runCatching {
// if (frequencySocketChannel == null) {
// frequencySocketChannel = SocketChannel.open(InetSocketAddress(server, port))
// } else {
// val buffer = ByteBuffer.wrap("\\set_freq $frequency\n".toByteArray())
// frequencySocketChannel?.write(buffer)
// }
// }.onFailure { error ->
// println(error.localizedMessage)
// frequencySocketChannel = null
// frequencyReporting?.cancelAndJoin()
// }
// }
// }
data class SocketConnection(
var socket: SocketChannel? = null,
var connected: Boolean = false,
var connecting: Boolean = false,
var connectionJob: Job? = null
)
fun reportRotation(server: String, port: Int, azimuth: Double, elevation: Double) {
rotationReporting = reporterScope.launch {
val newElevation = if (elevation > 0.0) elevation else 0.0
runCatching {
if (rotationSocketChannel == null) {
rotationSocketChannel = SocketChannel.open(InetSocketAddress(server, port))
} else {
val buffer = ByteBuffer.wrap("\\P $azimuth $newElevation\n".toByteArray())
rotationSocketChannel?.write(buffer)
}
}.onFailure { error ->
println(error.localizedMessage)
rotationSocketChannel = null
rotationReporting?.cancelAndJoin()
class NetworkReporter(private val reporterScope: CoroutineScope) : IReporterRepo<ExtendedParams> {
private val serviceToAddress = mutableMapOf<NetworkService, String>()
private val connections = mutableMapOf<String, SocketConnection>()
private val writeMutex = Mutex()
fun isConnected(service: NetworkService): Boolean {
val addr = serviceToAddress[service] ?: return false
return connections[addr]?.connected == true
}
private fun getOrCreateConnection(addr: String): SocketConnection {
return connections.getOrPut(addr) { SocketConnection() }
}
fun connect(service: NetworkService, server: String, port: Int) {
val addr = "$server:$port"
serviceToAddress[service] = addr
val connection = getOrCreateConnection(addr)
if (connection.connected || connection.connecting) return
connection.connectionJob = reporterScope.launch {
try {
connection.connecting = true
val socket = SocketChannel.open(InetSocketAddress(server, port))
connection.socket = socket
connection.connected = true
println("NetworkReporter: $service connected to $addr")
} catch (e: Exception) {
println("NetworkReporter connect error: ${e.message}")
connection.connected = false
} finally {
connection.connecting = false
}
}
}
private suspend fun write(service: NetworkService, command: String) {
val addr = serviceToAddress[service] ?: return
val connection = connections[addr] ?: return
if (!connection.connected) return
try {
writeMutex.withLock {
val buffer = ByteBuffer.wrap("\\$command\n".toByteArray())
connection.socket?.write(buffer)
}
} catch (e: Exception) {
println("NetworkReporter write error: ${e.message}")
connection.connected = false
}
}
override fun reportRotation(format: String, azimuth: Double, elevation: Double, params: ExtendedParams) {
reporterScope.launch {
connect(NetworkService.ROTATOR, params.server, params.port)
if (!isConnected(NetworkService.ROTATOR)) return@launch
val el = if (elevation > 0.0) elevation else 0.0
val command = format
.replace("\$AZ", azimuth.toString())
.replace("\$EL", el.toString())
write(NetworkService.ROTATOR, command)
}
}
override fun reportFrequency(format: String, frequency: Long, params: ExtendedParams) {
reporterScope.launch {
connect(NetworkService.FREQUENCY, params.server, params.port)
if (!isConnected(NetworkService.FREQUENCY)) return@launch
val command = format.replace("\$FREQ", frequency.toString())
write(NetworkService.FREQUENCY, command)
}
}
}
@@ -61,10 +61,6 @@ class MainContainer(private val context: Context) {
val satelliteRepo = provideSatelliteRepo()
val databaseRepo = provideDatabaseRepo()
fun provideAppVersionName(): String {
return context.packageManager.getPackageInfo(context.packageName, 0).versionName ?: "4.0.0"
}
fun provideAddToCalendar(): IAddToCalendar = AddToCalendar(context)
fun provideShowToast(): IShowToast = ShowToast(context)
@@ -111,6 +107,7 @@ class MainContainer(private val context: Context) {
val manager = context.getSystemService(Context.LOCATION_SERVICE) as LocationManager
val appPrefsFileName = "${context.packageName}_preferences"
val appPreferences = context.getSharedPreferences(appPrefsFileName, Context.MODE_PRIVATE)
return SettingsRepo(manager, appPreferences)
val appVersionName = context.packageManager.getPackageInfo(context.packageName, 0).versionName ?: "4.0.4"
return SettingsRepo(manager, appPreferences, appVersionName)
}
}
@@ -39,23 +39,46 @@ class DatabaseRepo(
private val settingsRepo: ISettingsRepo
) : IDatabaseRepo {
override suspend fun updateFromFile(uri: String) = withContext(dispatcher) {
override suspend fun updateTLEFromFile(uri: String) = withContext(dispatcher) {
val importedSatellites = remoteSource.getFileStream(uri)?.let { dataParser.parseTLEStream(it) }
importedSatellites?.let { localSource.insertEntries(it) }
setUpdateSuccessful(System.currentTimeMillis())
}
override suspend fun updateTransceiversFromFile(uri: String) = withContext(dispatcher) {
val radios = remoteSource.getFileStream(uri)?.let { dataParser.parseJSONStream(it) }
radios?.let {
if(it.isNotEmpty()) {
localSource.deleteRadios()
localSource.insertRadios(it)
}
}
setUpdateSuccessful(System.currentTimeMillis())
}
override suspend fun updateFromRemote() = withContext(dispatcher) {
val importedEntries = mutableListOf<OrbitalData>()
val importedRadios = mutableListOf<SatRadio>()
// fetch
val jobsMap = Sources.satelliteDataUrls.mapValues { async { remoteSource.getNetworkStream(it.value) } }
val jobRadios = async { remoteSource.getNetworkStream(Sources.RADIO_DATA_URL) }
val tleUrls = Sources.satelliteDataUrls.toMutableMap().apply {
if (settingsRepo.dataSourcesSettings.value.useCustomTLE) {
this["Other"] = settingsRepo.dataSourcesSettings.value.tleUrl
}
}
val jobsMap = tleUrls
.filter { (_, url) -> url.isNotEmpty() }
.mapValues { (_, url) -> async { remoteSource.getNetworkStream(url) } }
val radioUrls = buildList {
add(Sources.RADIO_DATA_URL)
if (settingsRepo.dataSourcesSettings.value.useCustomTransceivers) {
add(settingsRepo.dataSourcesSettings.value.transceiversUrl)
}
}
val jobRadios = radioUrls.associateWith { url -> async { remoteSource.getNetworkStream(url) } }
// parse
jobsMap.mapValues { job -> job.value.await() }.forEach { entry ->
entry.value?.let { stream ->
when (val type = entry.key) {
"Amsat", "R4UAB" -> {
"Amsat", "R4UAB", "Other" -> {
// parse tle stream
val satellites = dataParser.parseTLEStream(stream)
val catnums = satellites.map { it.catnum }
@@ -82,7 +105,11 @@ class DatabaseRepo(
}
}
}
jobRadios.await()?.let { importedRadios.addAll(dataParser.parseJSONStream(it)) }
jobRadios.values.forEach { job ->
job.await()?.let {
importedRadios.addAll(dataParser.parseJSONStream(it))
}
}
// insert
localSource.insertEntries(importedEntries)
localSource.insertRadios(importedRadios)
@@ -27,6 +27,7 @@ import com.rtbishop.look4sat.domain.model.DatabaseState
import com.rtbishop.look4sat.domain.model.OtherSettings
import com.rtbishop.look4sat.domain.model.PassesSettings
import com.rtbishop.look4sat.domain.model.RCSettings
import com.rtbishop.look4sat.domain.model.DataSourcesSettings
import com.rtbishop.look4sat.domain.predict.GeoPos
import com.rtbishop.look4sat.domain.repository.ISettingsRepo
import com.rtbishop.look4sat.domain.utility.positionToQth
@@ -38,13 +39,17 @@ import kotlinx.coroutines.flow.update
class SettingsRepo(
private val locationManager: LocationManager,
private val preferences: SharedPreferences
private val preferences: SharedPreferences,
override val appVersionName: String
) : ISettingsRepo, LocationListenerCompat {
private val keyBluetoothAddress = "bluetoothAddress"
private val keyBluetoothName = "bluetoothName"
private val keyBluetoothFormat = "bluetoothFormat"
private val keyBluetoothState = "bluetoothState"
private val keyBluetoothRotatorAddress = "bluetoothAddress"
private val keyBluetoothRotatorName = "bluetoothName"
private val keyBluetoothRotatorFormat = "bluetoothFormat"
private val keyBluetoothRotatorState = "bluetoothState"
private val keyBluetoothFrequencyState = "bluetoothFrequencyState"
private val keyBluetoothFrequencyAddress = "bluetoothFrequencyAddress"
private val keyBluetoothFrequencyFormat = "bluetoothFrequencyFormat"
private val keyFilterHoursAhead = "filterHoursAhead"
private val keyFilterMinElevation = "filterMinElevation"
private val keyNumberOfRadios = "numberOfRadios"
@@ -52,6 +57,11 @@ class SettingsRepo(
private val keyRotatorAddress = "rotatorAddress"
private val keyRotatorPort = "rotatorPort"
private val keyRotatorState = "rotatorState"
private val keyRotatorFormat = "rotatorFormat"
private val keyFrequencyState = "frequencyState"
private val keyFrequencyAddress = "frequencyAddress"
private val keyFrequencyPort = "frequencyPort"
private val keyFrequencyFormat = "frequencyFormat"
private val keySelectedIds = "selectedIds"
private val keySelectedTypes = "selectedTypes"
private val keySelectedModes = "selectedModes"
@@ -67,7 +77,11 @@ class SettingsRepo(
private val keyStationTimestamp = "stationTimestamp"
private val keyUpdateTimestamp = "updateTimestamp"
private val keyShouldSeeWarning = "shouldSeeWarning"
private val keyShouldSeeWelcome = "shouldSeeWelcome"
private val keyShouldSeeWhatsNew = "shouldSeeWhatsNew_v$appVersionName"
private val keyUseCustomTle = "useCustomTle"
private val keyUseCustomTransceivers = "useCustomTransceivers"
private val keyTleUrl = "tleUrl"
private val keyTransceiversUrl = "transceiversUrl"
private val separatorComma = ","
//region # Satellites selection settings
@@ -235,24 +249,39 @@ class SettingsRepo(
private val _rcSettings = MutableStateFlow(getRCSettings())
override val rcSettings: StateFlow<RCSettings> = _rcSettings
override fun setBluetoothAddress(value: String) {
preferences.edit { putString(keyBluetoothAddress, value) }
_rcSettings.update { it.copy(bluetoothAddress = value) }
override fun setBluetoothRotatorAddress(value: String) {
preferences.edit { putString(keyBluetoothRotatorAddress, value) }
_rcSettings.update { it.copy(bluetoothRotatorAddress = value) }
}
override fun setBluetoothFormat(value: String) {
preferences.edit { putString(keyBluetoothFormat, value) }
_rcSettings.update { it.copy(bluetoothFormat = value) }
override fun setBluetoothRotatorFormat(value: String) {
preferences.edit { putString(keyBluetoothRotatorFormat, value) }
_rcSettings.update { it.copy(bluetoothRotatorFormat = value) }
}
override fun setBluetoothName(value: String) {
preferences.edit { putString(keyBluetoothName, value) }
_rcSettings.update { it.copy(bluetoothName = value) }
override fun setBluetoothRotatorName(value: String) {
preferences.edit { putString(keyBluetoothRotatorName, value) }
_rcSettings.update { it.copy(bluetoothRotatorName = value) }
}
override fun setBluetoothState(value: Boolean) {
preferences.edit { putBoolean(keyBluetoothState, value) }
_rcSettings.update { it.copy(bluetoothState = value) }
override fun setBluetoothRotatorState(value: Boolean) {
preferences.edit { putBoolean(keyBluetoothRotatorState, value) }
_rcSettings.update { it.copy(bluetoothRotatorState = value) }
}
override fun setBluetoothFrequencyState(value: Boolean) {
preferences.edit { putBoolean(keyBluetoothFrequencyState, value) }
_rcSettings.update { it.copy(bluetoothFrequencyState = value) }
}
override fun setBluetoothFrequencyAddress(value: String) {
preferences.edit { putString(keyBluetoothFrequencyAddress, value) }
_rcSettings.update { it.copy(bluetoothFrequencyAddress = value) }
}
override fun setBluetoothFrequencyFormat(value: String) {
preferences.edit { putString(keyBluetoothFrequencyFormat, value) }
_rcSettings.update { it.copy(bluetoothFrequencyFormat = value) }
}
override fun setRotatorAddress(value: String) {
@@ -270,14 +299,47 @@ class SettingsRepo(
_rcSettings.update { it.copy(rotatorState = value) }
}
override fun setRotatorFormat(value: String) {
preferences.edit { putString(keyRotatorFormat, value) }
_rcSettings.update { it.copy(rotatorFormat = value) }
}
override fun setFrequencyState(value: Boolean) {
preferences.edit { putBoolean(keyFrequencyState, value) }
_rcSettings.update { it.copy(frequencyState = value) }
}
override fun setFrequencyAddress(value: String) {
preferences.edit { putString(keyFrequencyAddress, value) }
_rcSettings.update { it.copy(frequencyAddress = value) }
}
override fun setFrequencyPort(value: String) {
preferences.edit { putString(keyFrequencyPort, value) }
_rcSettings.update { it.copy(frequencyPort = value) }
}
override fun setFrequencyFormat(value: String) {
preferences.edit { putString(keyFrequencyFormat, value) }
_rcSettings.update { it.copy(frequencyFormat = value) }
}
private fun getRCSettings(): RCSettings = RCSettings(
rotatorState = preferences.getBoolean(keyRotatorState, false),
rotatorAddress = preferences.getString(keyRotatorAddress, null) ?: "127.0.0.1",
rotatorPort = preferences.getString(keyRotatorPort, null) ?: "4533",
bluetoothState = preferences.getBoolean(keyBluetoothState, false),
bluetoothFormat = preferences.getString(keyBluetoothFormat, null) ?: $$"W$AZ $EL",
bluetoothName = preferences.getString(keyBluetoothName, null) ?: "Default",
bluetoothAddress = preferences.getString(keyBluetoothAddress, null) ?: "00:0C:BF:13:80:5D"
rotatorFormat = preferences.getString(keyRotatorFormat, null) ?: $$"P $AZ $EL",
frequencyState = preferences.getBoolean(keyFrequencyState, false),
frequencyAddress = preferences.getString(keyFrequencyAddress, null) ?: "127.0.0.1",
frequencyPort = preferences.getString(keyFrequencyPort, null) ?: "4532",
frequencyFormat = preferences.getString(keyFrequencyFormat, null) ?: $$"set_freq $FREQ",
bluetoothRotatorState = preferences.getBoolean(keyBluetoothRotatorState, false),
bluetoothRotatorFormat = preferences.getString(keyBluetoothRotatorFormat, null) ?: $$"W$AZ $EL",
bluetoothRotatorName = preferences.getString(keyBluetoothRotatorName, null) ?: "Default",
bluetoothRotatorAddress = preferences.getString(keyBluetoothRotatorAddress, null) ?: "00:0C:BF:13:80:5D",
bluetoothFrequencyState = preferences.getBoolean(keyBluetoothFrequencyState, false),
bluetoothFrequencyAddress = preferences.getString(keyBluetoothFrequencyAddress, null) ?: "00:0C:BF:13:80:5D",
bluetoothFrequencyFormat = preferences.getString(keyBluetoothFrequencyFormat, null) ?: $$"FA$FREQ"
)
//endregion
@@ -315,9 +377,9 @@ class SettingsRepo(
_otherSettings.update { it.copy(shouldSeeWarning = false) }
}
override fun setWelcomeDismissed() {
preferences.edit { putBoolean(keyShouldSeeWelcome, false) }
_otherSettings.update { it.copy(shouldSeeWelcome = false) }
override fun setWhatsNewDismissed() {
preferences.edit { putBoolean(keyShouldSeeWhatsNew, false) }
_otherSettings.update { it.copy(shouldSeeWhatsNew = false) }
}
private fun getOtherSettings(): OtherSettings = OtherSettings(
@@ -327,7 +389,39 @@ class SettingsRepo(
stateOfUtc = preferences.getBoolean(keyStateOfUtc, false),
stateOfLightTheme = preferences.getBoolean(keyStateOfLightTheme, false),
shouldSeeWarning = preferences.getBoolean(keyShouldSeeWarning, true),
shouldSeeWelcome = preferences.getBoolean(keyShouldSeeWelcome, true)
shouldSeeWhatsNew = preferences.getBoolean(keyShouldSeeWhatsNew, true)
)
//endregion
//region # Data sources settings
private val _dataSourcesSettings = MutableStateFlow(getDataSourcesSettings())
override val dataSourcesSettings: StateFlow<DataSourcesSettings> = _dataSourcesSettings
override fun setUseCustomTle(value: Boolean) {
preferences.edit { putBoolean(keyUseCustomTle, value) }
_dataSourcesSettings.update { it.copy(useCustomTLE = value) }
}
override fun setUseCustomTransceivers(value: Boolean) {
preferences.edit { putBoolean(keyUseCustomTransceivers, value) }
_dataSourcesSettings.update { it.copy(useCustomTransceivers = value) }
}
override fun setTleUrl(value: String) {
preferences.edit { putString(keyTleUrl, value) }
_dataSourcesSettings.update { it.copy(tleUrl = value) }
}
override fun setTransceiversUrl(value: String) {
preferences.edit { putString(keyTransceiversUrl, value) }
_dataSourcesSettings.update { it.copy(transceiversUrl = value) }
}
private fun getDataSourcesSettings(): DataSourcesSettings = DataSourcesSettings(
useCustomTLE = preferences.getBoolean(keyUseCustomTle, false),
useCustomTransceivers = preferences.getBoolean(keyUseCustomTransceivers, false),
tleUrl = preferences.getString(keyTleUrl, "https://example.com/tle.txt") ?: "",
transceiversUrl = preferences.getString(keyTransceiversUrl, "https://example.com/radio.json") ?: ""
)
//endregion
}
+1 -1
View File
@@ -3,7 +3,7 @@ plugins {
}
kotlin {
jvmToolchain(21)
jvmToolchain(17)
}
dependencies {
@@ -33,10 +33,18 @@ data class RCSettings(
val rotatorState: Boolean,
val rotatorAddress: String,
val rotatorPort: String,
val bluetoothState: Boolean,
val bluetoothFormat: String,
val bluetoothName: String,
val bluetoothAddress: String,
val rotatorFormat: String,
val frequencyState: Boolean,
val frequencyAddress: String,
val frequencyPort: String,
val frequencyFormat: String,
val bluetoothRotatorState: Boolean,
val bluetoothRotatorFormat: String,
val bluetoothRotatorName: String,
val bluetoothRotatorAddress: String,
val bluetoothFrequencyState: Boolean,
val bluetoothFrequencyFormat: String,
val bluetoothFrequencyAddress: String
)
data class OtherSettings(
@@ -46,5 +54,12 @@ data class OtherSettings(
val stateOfUtc: Boolean,
val stateOfLightTheme: Boolean,
val shouldSeeWarning: Boolean,
val shouldSeeWelcome: Boolean
val shouldSeeWhatsNew: Boolean
)
data class DataSourcesSettings(
val useCustomTLE: Boolean,
val useCustomTransceivers: Boolean,
val tleUrl: String,
val transceiversUrl: String
)
@@ -18,7 +18,8 @@
package com.rtbishop.look4sat.domain.repository
interface IDatabaseRepo {
suspend fun updateFromFile(uri: String)
suspend fun updateTLEFromFile(uri: String)
suspend fun updateTransceiversFromFile(uri: String)
suspend fun updateFromRemote()
suspend fun clearAllData()
}
@@ -0,0 +1,25 @@
/*
* Look4Sat. Amateur radio satellite tracker and pass predictor.
* Copyright (C) 2019-2026 Arty Bishop and contributors.
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
package com.rtbishop.look4sat.domain.repository
interface IReporterParams
interface IReporterRepo<T : IReporterParams> {
fun reportRotation(format: String, azimuth: Double, elevation: Double, params: T)
fun reportFrequency(format: String, frequency: Long, params: T)
}
@@ -21,11 +21,14 @@ import com.rtbishop.look4sat.domain.model.DatabaseState
import com.rtbishop.look4sat.domain.model.OtherSettings
import com.rtbishop.look4sat.domain.model.PassesSettings
import com.rtbishop.look4sat.domain.model.RCSettings
import com.rtbishop.look4sat.domain.model.DataSourcesSettings
import com.rtbishop.look4sat.domain.predict.GeoPos
import kotlinx.coroutines.flow.StateFlow
interface ISettingsRepo {
val appVersionName: String
//region # Satellites selection settings
val selectedIds: StateFlow<List<Int>>
val selectedTypes: StateFlow<List<String>>
@@ -54,13 +57,21 @@ interface ISettingsRepo {
//region # RC settings
val rcSettings: StateFlow<RCSettings>
fun setBluetoothAddress(value: String)
fun setBluetoothFormat(value: String)
fun setBluetoothName(value: String)
fun setBluetoothState(value: Boolean)
fun setBluetoothRotatorAddress(value: String)
fun setBluetoothRotatorFormat(value: String)
fun setBluetoothRotatorName(value: String)
fun setBluetoothRotatorState(value: Boolean)
fun setBluetoothFrequencyAddress(value: String)
fun setBluetoothFrequencyFormat(value: String)
fun setBluetoothFrequencyState(value: Boolean)
fun setRotatorAddress(value: String)
fun setRotatorPort(value: String)
fun setRotatorState(value: Boolean)
fun setRotatorFormat(value: String)
fun setFrequencyAddress(value: String)
fun setFrequencyPort(value: String)
fun setFrequencyState(value: Boolean)
fun setFrequencyFormat(value: String)
//endregion
//region # Other settings
@@ -71,6 +82,14 @@ interface ISettingsRepo {
fun setStateOfUtc(value: Boolean)
fun setStateOfLightTheme(value: Boolean)
fun setWarningDismissed()
fun setWelcomeDismissed()
fun setWhatsNewDismissed()
//endregion
//region # Transceivers settings
val dataSourcesSettings: StateFlow<DataSourcesSettings>
fun setUseCustomTle(value: Boolean)
fun setUseCustomTransceivers(value: Boolean)
fun setTleUrl(value: String)
fun setTransceiversUrl(value: String)
//endregion
}
@@ -23,7 +23,6 @@ object Sources {
"All" to "https://celestrak.org/NORAD/elements/gp.php?GROUP=active&FORMAT=csv",
"Amateur" to "https://celestrak.org/NORAD/elements/gp.php?GROUP=amateur&FORMAT=csv",
"Brightest" to "https://celestrak.org/NORAD/elements/gp.php?GROUP=visual&FORMAT=csv",
"Classified" to "https://www.mmccants.org/tles/classfd.zip",
"Cubesat" to "https://celestrak.org/NORAD/elements/gp.php?GROUP=cubesat&FORMAT=csv",
"Education" to "https://celestrak.org/NORAD/elements/gp.php?GROUP=education&FORMAT=csv",
"Engineer" to "https://celestrak.org/NORAD/elements/gp.php?GROUP=engineering&FORMAT=csv",
@@ -32,12 +31,10 @@ object Sources {
"GNSS" to "https://celestrak.org/NORAD/elements/gp.php?GROUP=gnss&FORMAT=csv",
"Intelsat" to "https://celestrak.org/NORAD/elements/gp.php?GROUP=intelsat&FORMAT=csv",
"Iridium" to "https://celestrak.org/NORAD/elements/gp.php?GROUP=iridium-NEXT&FORMAT=csv",
"McCants" to "https://www.mmccants.org/tles/inttles.zip",
"Military" to "https://celestrak.org/NORAD/elements/gp.php?GROUP=military&FORMAT=csv",
"New" to "https://celestrak.org/NORAD/elements/gp.php?GROUP=last-30-days&FORMAT=csv",
"OneWeb" to "https://celestrak.org/NORAD/elements/gp.php?GROUP=oneweb&FORMAT=csv",
"Orbcomm" to "https://celestrak.org/NORAD/elements/gp.php?GROUP=orbcomm&FORMAT=csv",
"R4UAB" to "https://r4uab.ru/satonline.txt",
"Resource" to "https://celestrak.org/NORAD/elements/gp.php?GROUP=resource&FORMAT=csv",
"SatNOGS" to "https://celestrak.org/NORAD/elements/gp.php?GROUP=satnogs&FORMAT=csv",
"Science" to "https://celestrak.org/NORAD/elements/gp.php?GROUP=science&FORMAT=csv",
@@ -46,6 +43,10 @@ object Sources {
"Swarm" to "https://celestrak.org/NORAD/elements/gp.php?GROUP=swarm&FORMAT=csv",
"Weather" to "https://celestrak.org/NORAD/elements/gp.php?GROUP=weather&FORMAT=csv",
"X-Comm" to "https://celestrak.org/NORAD/elements/gp.php?GROUP=x-comm&FORMAT=csv",
"Amsat" to "https://amsat.org/tle/current/nasabare.txt"
"Amsat" to "https://amsat.org/tle/current/nasabare.txt",
"Classified" to "https://www.mmccants.org/tles/classfd.zip",
"McCants" to "https://www.mmccants.org/tles/inttles.zip",
"R4UAB" to "https://r4uab.ru/satonline.txt",
"Other" to "" // key for sats filter
)
}
@@ -1,9 +1,3 @@
Added support for landscape layout and tablets
Added AOS/LOS info to the countdown component
Added various minor UI fixes for readability
Cleaned up the translations and strings.xml
Moved Modes filter dialog to Passes screen
Rewrote all the screens with Jetpack Compose
Switched to using Bottom Navigation component
Switched to using unified dialogs and top bars
Updated GitHub Actions workflow deps and scripts
Added What's New dialog to announce changes and features
Added remote frequency control feature, by londre7 #195
Updated the Chinese translation, by wty2019wty #196
@@ -1,9 +1,3 @@
Added support for landscape layout and tablets
Added AOS/LOS info to the countdown component
Added various minor UI fixes for readability
Cleaned up the translations and strings.xml
Moved Modes filter dialog to Passes screen
Rewrote all the screens with Jetpack Compose
Switched to using Bottom Navigation component
Switched to using unified dialogs and top bars
Updated GitHub Actions workflow deps and scripts
Added What's New dialog to announce changes and features
Added remote frequency control feature, by londre7 #195
Updated the Chinese translation, by wty2019wty #196
+13
View File
@@ -0,0 +1,13 @@
#This file is generated by updateDaemonJvm
toolchainUrl.FREE_BSD.AARCH64=https\://api.foojay.io/disco/v3.0/ids/29ee363f71d060405f729a8f1b7f7aef/redirect
toolchainUrl.FREE_BSD.X86_64=https\://api.foojay.io/disco/v3.0/ids/67a0fee3c4236b6397dcbe8575ca2011/redirect
toolchainUrl.LINUX.AARCH64=https\://api.foojay.io/disco/v3.0/ids/29ee363f71d060405f729a8f1b7f7aef/redirect
toolchainUrl.LINUX.X86_64=https\://api.foojay.io/disco/v3.0/ids/ecd23fd7707c683afbcd6052998cb6a9/redirect
toolchainUrl.MAC_OS.AARCH64=https\://api.foojay.io/disco/v3.0/ids/10fc3bf1ee0001078a473afe6e43cfdb/redirect
toolchainUrl.MAC_OS.X86_64=https\://api.foojay.io/disco/v3.0/ids/658299a896470fbb3103ba3a430ee227/redirect
toolchainUrl.UNIX.AARCH64=https\://api.foojay.io/disco/v3.0/ids/29ee363f71d060405f729a8f1b7f7aef/redirect
toolchainUrl.UNIX.X86_64=https\://api.foojay.io/disco/v3.0/ids/67a0fee3c4236b6397dcbe8575ca2011/redirect
toolchainUrl.WINDOWS.AARCH64=https\://api.foojay.io/disco/v3.0/ids/23adb857f3cb3cbe28750bc7faa7abc0/redirect
toolchainUrl.WINDOWS.X86_64=https\://api.foojay.io/disco/v3.0/ids/932015f6361ccaead0c6d9b8717ed96e/redirect
toolchainVendor=JETBRAINS
toolchainVersion=21
+6 -6
View File
@@ -1,14 +1,14 @@
[versions]
android-gradle-plugin = "9.0.0"
google-ksp = "2.3.2"
kotlin = "2.3.0"
android-gradle-plugin = "9.0.1"
google-ksp = "2.3.5"
kotlin = "2.3.10"
androidx-core-ktx = "1.17.0"
androidx-core-splashscreen = "1.2.0"
androidx-room = "2.8.4"
compose-bom = "2026.01.01"
compose-activity = "1.12.3"
compose-bom = "2026.02.00"
compose-activity = "1.12.4"
compose-lifecycle = "2.10.0"
compose-navigation = "2.9.7"
@@ -34,7 +34,7 @@ compose-animation = { group = "androidx.compose.animation", name = "animation" }
compose-material3 = { group = "androidx.compose.material3", name = "material3" }
compose-material3-navigation = { group = "androidx.compose.material3", name = "material3-adaptive-navigation-suite" }
compose-runtime = { group = "androidx.compose.runtime", name = "runtime" }
compose-tooling = { group = "androidx.compose.ui", name = "ui-tooling" }
compose-tooling = { group = "androidx.compose.ui", name = "ui-tooling-preview" }
compose-activity = { module = "androidx.activity:activity-compose", version.ref = "compose-activity" }
compose-lifecycle = { module = "androidx.lifecycle:lifecycle-runtime-compose", version.ref = "compose-lifecycle" }
-3
View File
@@ -5,9 +5,6 @@ pluginManagement {
gradlePluginPortal()
}
}
plugins {
id("org.gradle.toolchains.foojay-resolver-convention") version "1.0.0"
}
dependencyResolutionManagement {
repositoriesMode.set(RepositoriesMode.FAIL_ON_PROJECT_REPOS)
repositories {