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 uses: actions/setup-java@v5
with: with:
distribution: 'temurin' distribution: 'temurin'
java-version: '21' java-version: '17'
- name: Setup Gradle - name: Setup Gradle
uses: gradle/actions/setup-gradle@v5 uses: gradle/actions/setup-gradle@v5
@@ -67,7 +67,7 @@ jobs:
serviceAccountJsonPlainText: ${{secrets.SERVICE_ACCOUNT_JSON}} serviceAccountJsonPlainText: ${{secrets.SERVICE_ACCOUNT_JSON}}
packageName: com.rtbishop.look4sat packageName: com.rtbishop.look4sat
releaseFiles: app/build/outputs/apk/release/look4sat.aab releaseFiles: app/build/outputs/apk/release/look4sat.aab
track: beta track: production
whatsNewDirectory: fastlane/metadata/android/en-US/whatsnew whatsNewDirectory: fastlane/metadata/android/en-US/whatsnew
- name: Create Release - name: Create Release
+3 -3
View File
@@ -4,7 +4,7 @@ plugins {
} }
kotlin { kotlin {
jvmToolchain(21) jvmToolchain(17)
} }
android { android {
@@ -13,8 +13,8 @@ android {
defaultConfig { defaultConfig {
applicationId = "com.rtbishop.look4sat" applicationId = "com.rtbishop.look4sat"
minSdk = 24 minSdk = 24
versionCode = 400 versionCode = 410
versionName = "4.0.0" versionName = "4.1.0"
} }
buildFeatures { buildFeatures {
compose = true compose = true
@@ -17,6 +17,9 @@
*/ */
package com.rtbishop.look4sat.presentation 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.Icon
import androidx.compose.material3.MaterialTheme import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.Text import androidx.compose.material3.Text
@@ -76,7 +79,12 @@ fun MainScreen(navController: NavHostController = rememberNavController()) {
else -> NavigationSuiteType.ShortNavigationBarMedium 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() } satellitesDestination { navController.navigateUp() }
passesDestination { catNum: Int, aosTime: Long -> passesDestination { catNum: Int, aosTime: Long ->
val radarRoute = "${Screen.Radar.route}?catNum=${catNum}&aosTime=${aosTime}" 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.Arrangement
import androidx.compose.foundation.layout.Box import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.PaddingValues
import androidx.compose.foundation.layout.Row import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.RowScope import androidx.compose.foundation.layout.RowScope
import androidx.compose.foundation.layout.Spacer import androidx.compose.foundation.layout.Spacer
@@ -210,8 +211,9 @@ fun CardButton(onClick: () -> Unit, text: String, modifier: Modifier = Modifier)
onClick = { onClick() }, colors = ButtonDefaults.buttonColors( onClick = { onClick() }, colors = ButtonDefaults.buttonColors(
containerColor = MaterialTheme.colorScheme.surfaceVariant, containerColor = MaterialTheme.colorScheme.surfaceVariant,
contentColor = MaterialTheme.colorScheme.onSurfaceVariant contentColor = MaterialTheme.colorScheme.onSurfaceVariant
), shape = MaterialTheme.shapes.small, modifier = modifier ), shape = MaterialTheme.shapes.small, modifier = modifier,
) { Text(text = text, fontSize = 17.sp) } contentPadding = PaddingValues(horizontal = 8.dp, vertical = 8.dp)
) { Text(text = text, fontSize = 16.sp, textAlign = TextAlign.Center) }
} }
@Composable @Composable
@@ -253,7 +255,7 @@ fun EmptyListCard(message: String) {
modifier = Modifier.padding(32.dp) modifier = Modifier.padding(32.dp)
) { ) {
Text(text = """¯\_(ツ)_/¯""", fontSize = 32.sp) 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) Text(text = message, fontSize = 18.sp, textAlign = TextAlign.Center)
} }
} }
@@ -278,7 +280,7 @@ fun SharedDialog(
) { ) {
Text( Text(
text = title, text = title,
fontSize = 18.sp, fontSize = 16.sp,
fontWeight = FontWeight.Medium, fontWeight = FontWeight.Medium,
color = MaterialTheme.colorScheme.primary, color = MaterialTheme.colorScheme.primary,
modifier = Modifier.padding(start = padding, top = padding, end = padding) modifier = Modifier.padding(start = padding, top = padding, end = padding)
@@ -297,7 +299,7 @@ fun SharedDialog(
@Composable @Composable
fun InfoDialog(title: String, text: String, onDismiss: () -> Unit) { fun InfoDialog(title: String, text: String, onDismiss: () -> Unit) {
val padding = LocalSpacing.current.large val padding = LocalSpacing.current.large
Dialog(onDismissRequest = onDismiss) { Dialog(onDismissRequest = {}) {
ElevatedCard { ElevatedCard {
Column( Column(
horizontalAlignment = Alignment.CenterHorizontally, horizontalAlignment = Alignment.CenterHorizontally,
@@ -110,12 +110,12 @@ private fun PassesScreen(uiState: PassesState, navigateToRadar: (Int, Long) -> U
uiState.takeAction(PassesAction.FilterRadios(modes)) uiState.takeAction(PassesAction.FilterRadios(modes))
} }
} }
if (uiState.shouldSeeWelcome) { if (uiState.shouldSeeWhatsNew) {
InfoDialog( InfoDialog(
title = stringResource(R.string.pass_welcome_title), title = stringResource(R.string.pass_whatsnew_title),
text = stringResource(R.string.pass_welcome_message) text = stringResource(R.string.pass_whatsnew_message)
) { ) {
uiState.takeAction(PassesAction.DismissWelcome) uiState.takeAction(PassesAction.DismissWhatsNew)
} }
} }
val gridState = rememberLazyGridState() val gridState = rememberLazyGridState()
@@ -31,12 +31,12 @@ data class PassesState(
val elevation: Double, val elevation: Double,
val modes: List<String>, val modes: List<String>,
val itemsList: List<OrbitalPass>, val itemsList: List<OrbitalPass>,
val shouldSeeWelcome: Boolean, val shouldSeeWhatsNew: Boolean,
val takeAction: (PassesAction) -> Unit val takeAction: (PassesAction) -> Unit
) )
sealed class PassesAction { sealed class PassesAction {
data object DismissWelcome : PassesAction() data object DismissWhatsNew : PassesAction()
data class FilterPasses(val hoursAhead: Int, val minElevation: Double) : PassesAction() data class FilterPasses(val hoursAhead: Int, val minElevation: Double) : PassesAction()
data class FilterRadios(val modes: List<String>) : PassesAction() data class FilterRadios(val modes: List<String>) : PassesAction()
data object RefreshPasses : PassesAction() data object RefreshPasses : PassesAction()
@@ -40,7 +40,8 @@ import kotlinx.coroutines.isActive
import kotlinx.coroutines.launch import kotlinx.coroutines.launch
class PassesViewModel( class PassesViewModel(
private val satelliteRepo: ISatelliteRepo, private val settingsRepo: ISettingsRepo private val satelliteRepo: ISatelliteRepo,
private val settingsRepo: ISettingsRepo
) : ViewModel() { ) : ViewModel() {
private val _uiState = MutableStateFlow( private val _uiState = MutableStateFlow(
@@ -56,7 +57,7 @@ class PassesViewModel(
elevation = settingsRepo.passesSettings.value.minElevation, elevation = settingsRepo.passesSettings.value.minElevation,
modes = settingsRepo.passesSettings.value.selectedModes, modes = settingsRepo.passesSettings.value.selectedModes,
itemsList = emptyList(), itemsList = emptyList(),
shouldSeeWelcome = settingsRepo.otherSettings.value.shouldSeeWelcome, shouldSeeWhatsNew = settingsRepo.otherSettings.value.shouldSeeWhatsNew,
takeAction = ::handleAction takeAction = ::handleAction
) )
) )
@@ -81,14 +82,14 @@ class PassesViewModel(
} }
viewModelScope.launch { viewModelScope.launch {
settingsRepo.otherSettings.collectLatest { settings -> settingsRepo.otherSettings.collectLatest { settings ->
_uiState.update { it.copy(shouldSeeWelcome = settings.shouldSeeWelcome) } _uiState.update { it.copy(shouldSeeWhatsNew = settings.shouldSeeWhatsNew) }
} }
} }
} }
private fun handleAction(action: PassesAction) { private fun handleAction(action: PassesAction) {
when (action) { when (action) {
PassesAction.DismissWelcome -> settingsRepo.setWelcomeDismissed() PassesAction.DismissWhatsNew -> settingsRepo.setWhatsNewDismissed()
is PassesAction.FilterPasses -> applyFilter(action.hoursAhead, action.minElevation, uiState.value.modes) is PassesAction.FilterPasses -> applyFilter(action.hoursAhead, action.minElevation, uiState.value.modes)
is PassesAction.FilterRadios -> applyFilter(uiState.value.hours, uiState.value.elevation, action.modes) is PassesAction.FilterRadios -> applyFilter(uiState.value.hours, uiState.value.elevation, action.modes)
PassesAction.RefreshPasses -> refreshPasses() PassesAction.RefreshPasses -> refreshPasses()
@@ -25,6 +25,7 @@ import androidx.compose.animation.core.rememberInfiniteTransition
import androidx.compose.animation.core.tween import androidx.compose.animation.core.tween
import androidx.compose.foundation.background import androidx.compose.foundation.background
import androidx.compose.foundation.border import androidx.compose.foundation.border
import androidx.compose.foundation.clickable
import androidx.compose.foundation.layout.Arrangement import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Box import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.Column
@@ -45,6 +46,7 @@ import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier import androidx.compose.ui.Modifier
import androidx.compose.ui.draw.alpha import androidx.compose.ui.draw.alpha
import androidx.compose.ui.draw.rotate import androidx.compose.ui.draw.rotate
import androidx.compose.ui.keepScreenOn
import androidx.compose.ui.res.painterResource import androidx.compose.ui.res.painterResource
import androidx.compose.ui.res.stringResource import androidx.compose.ui.res.stringResource
import androidx.compose.ui.semantics.contentDescription 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.domain.utility.toDegrees
import com.rtbishop.look4sat.presentation.MainTheme import com.rtbishop.look4sat.presentation.MainTheme
import com.rtbishop.look4sat.presentation.Screen 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.IconCard
import com.rtbishop.look4sat.presentation.common.NextPassRow import com.rtbishop.look4sat.presentation.common.NextPassRow
import com.rtbishop.look4sat.presentation.common.TimerRow import com.rtbishop.look4sat.presentation.common.TimerRow
@@ -96,7 +99,7 @@ private fun RadarScreen(uiState: RadarState, navigateUp: () -> Unit) {
} }
val upcomingPass = uiState.currentPass ?: getDefaultPass() val upcomingPass = uiState.currentPass ?: getDefaultPass()
if (upcomingPass.losTime < System.currentTimeMillis()) navigateUp() 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()) { if (isVerticalLayout()) {
TopBar { TopBar {
IconCard(action = navigateUp, resId = R.drawable.ic_back) IconCard(action = navigateUp, resId = R.drawable.ic_back)
@@ -115,89 +118,18 @@ private fun RadarScreen(uiState: RadarState, navigateUp: () -> Unit) {
if(isVerticalLayout()) { if(isVerticalLayout()) {
ElevatedCard(modifier = Modifier.weight(1f)) { ElevatedCard(modifier = Modifier.weight(1f)) {
Box(contentAlignment = Alignment.Center) { Box(contentAlignment = Alignment.Center) {
RadarViewCompose( if (uiState.orbitalPos == null) {
item = uiState.orbitalPos, ElevatedCard(modifier = Modifier.fillMaxSize()) {
items = uiState.satTrack, EmptyListCard(message = "")
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
)
} }
} }
} uiState.orbitalPos?.let { position ->
}
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) {
RadarViewCompose( RadarViewCompose(
item = uiState.orbitalPos, item = position,
items = uiState.satTrack, items = uiState.satTrack,
azimElev = uiState.orientationValues, azimElev = uiState.orientationValues,
shouldShowSweep = uiState.shouldShowSweep, shouldShowSweep = uiState.shouldShowSweep,
shouldUseCompass = false, shouldUseCompass = uiState.shouldUseCompass,
modifier = Modifier.align(Alignment.Center) modifier = Modifier.align(Alignment.Center)
) )
Column( Column(
@@ -211,12 +143,12 @@ private fun RadarScreen(uiState: RadarState, navigateUp: () -> Unit) {
modifier = Modifier.fillMaxWidth() modifier = Modifier.fillMaxWidth()
) { ) {
RadarTextTop( RadarTextTop(
uiState.orbitalPos?.azimuth ?: 0.0, position.azimuth,
stringResource(R.string.radar_az_text), stringResource(R.string.radar_az_text),
true true
) )
RadarTextTop( RadarTextTop(
uiState.orbitalPos?.elevation ?: 0.0, position.elevation,
stringResource(R.string.radar_el_text), stringResource(R.string.radar_el_text),
false false
) )
@@ -226,12 +158,12 @@ private fun RadarScreen(uiState: RadarState, navigateUp: () -> Unit) {
modifier = Modifier.fillMaxWidth() modifier = Modifier.fillMaxWidth()
) { ) {
RadarTextBottom( RadarTextBottom(
uiState.orbitalPos?.altitude ?: 0.0, position.altitude,
stringResource(R.string.radar_alt_text), stringResource(R.string.radar_alt_text),
true true
) )
RadarTextBottom( RadarTextBottom(
uiState.orbitalPos?.distance ?: 0.0, position.distance,
stringResource(R.string.radar_dist_text), stringResource(R.string.radar_dist_text),
false 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)) { ElevatedCard(modifier = Modifier.weight(1f)) {
if (uiState.transmitters.isEmpty()) { if (uiState.transmitters.isEmpty()) {
Box(contentAlignment = Alignment.Center, modifier = Modifier.fillMaxSize()) { Box(contentAlignment = Alignment.Center, modifier = Modifier.fillMaxSize()) {
@@ -248,7 +277,11 @@ private fun RadarScreen(uiState: RadarState, navigateUp: () -> Unit) {
modifier = Modifier.padding(32.dp) modifier = Modifier.padding(32.dp)
) { ) {
Text(text = """¯\_(ツ)_/¯""", fontSize = 32.sp) 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(
text = stringResource(R.string.radar_no_data), text = stringResource(R.string.radar_no_data),
fontSize = 18.sp, fontSize = 18.sp,
@@ -258,7 +291,15 @@ private fun RadarScreen(uiState: RadarState, navigateUp: () -> Unit) {
} }
} else { } else {
Box(modifier = Modifier.fillMaxSize(), contentAlignment = Alignment.Center) { 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) { if (uiState.orbitalPos?.eclipsed == true) {
EclipsedIndicator() EclipsedIndicator()
} }
@@ -291,10 +332,18 @@ private fun RadarTextBottom(value: Double, text: String, isLeft: Boolean) {
} }
@Composable @Composable
private fun TransmittersList(transmitters: List<SatRadio>) { private fun TransmittersList(
transmitters: List<SatRadio>,
selectedUuid: String?,
onSelect: (String) -> Unit
) {
LazyColumn(modifier = Modifier.fillMaxSize()) { LazyColumn(modifier = Modifier.fillMaxSize()) {
items(items = transmitters, key = { item -> item.uuid }) { radio -> 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, "", "Extremely powerful transmitter", true, 10000000000L, 10000000000L,
null, 10000000000L, 10000000000L, "FSK AX.100 Mode 5", true, 0 null, 10000000000L, 10000000000L, "FSK AX.100 Mode 5", true, 0
) )
MainTheme { TransmitterItem(transmitter) } MainTheme { TransmitterItem(transmitter, isClickable = true, isSelected = true, onClick = {}) }
} }
@Composable @Composable
@@ -346,36 +395,46 @@ private fun EclipsedIndicator() {
} }
@Composable @Composable
private fun TransmitterItem(radio: SatRadio) { private fun TransmitterItem(
// LaunchedEffect(radio) { radio: SatRadio,
// BluetoothCIV.updateOnce(radio) isClickable: Boolean,
// } isSelected: Boolean,
onClick: () -> Unit
) {
val title = if (radio.isInverted) "INVERTED: ${radio.info}" else radio.info val title = if (radio.isInverted) "INVERTED: ${radio.info}" else radio.info
val fullTitle = "$title - (${radio.downlinkMode ?: "--"}/${radio.uplinkMode ?: "--"})" val fullTitle = "$title - (${radio.downlinkMode ?: "--"}/${radio.uplinkMode ?: "--"})"
Surface(color = MaterialTheme.colorScheme.background, Surface(
// modifier = Modifier.clickable(onClick = { color = MaterialTheme.colorScheme.background,
// Log.d("BluetoothCivManager", radio.toString()) modifier = Modifier
// if(radio.uuid == BluetoothCIV.selected) BluetoothCIV.selected = "NONE" else .fillMaxWidth()
// { .then(if (isClickable) Modifier.clickable { onClick() } else Modifier)
// BluetoothCIV.connect(radio)
// BluetoothCIV.updateOnce(radio)
// }
// })
) { ) {
Surface(modifier = Modifier.padding(bottom = 2.dp)) { Surface(modifier = Modifier.padding(bottom = 2.dp)) {
Column( Column(
horizontalAlignment = Alignment.CenterHorizontally,
verticalArrangement = Arrangement.spacedBy(6.dp), verticalArrangement = Arrangement.spacedBy(6.dp),
modifier = Modifier modifier = Modifier
.background(MaterialTheme.colorScheme.surface) .background(MaterialTheme.colorScheme.surface)
.padding(horizontal = 8.dp, vertical = 6.dp) .padding(horizontal = 8.dp, vertical = 6.dp)
) { ) {
Text( Box {
text = fullTitle, Text(
textAlign = TextAlign.Center, text = fullTitle,
modifier = Modifier textAlign = TextAlign.Center,
.fillMaxWidth() modifier = Modifier
.infiniteMarquee() .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 = true)
FrequencyRow(radio = radio, isDownlink = false) FrequencyRow(radio = radio, isDownlink = false)
} }
@@ -31,9 +31,12 @@ data class RadarState(
val shouldShowSweep: Boolean, val shouldShowSweep: Boolean,
val shouldUseCompass: Boolean, val shouldUseCompass: Boolean,
val transmitters: List<SatRadio>, val transmitters: List<SatRadio>,
val selectedTransmitterUuid: String?,
val selectedFrequency: Long?,
val sendAction: (RadarAction) -> Unit val sendAction: (RadarAction) -> Unit
) )
sealed class RadarAction { sealed class RadarAction {
data class AddToCalendar(val name: String, val aosTime: Long, val losTime: Long) : 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 package com.rtbishop.look4sat.presentation.radar
import android.media.SoundPool
import androidx.compose.animation.core.animateFloat import androidx.compose.animation.core.animateFloat
import androidx.compose.animation.core.infiniteRepeatable import androidx.compose.animation.core.infiniteRepeatable
import androidx.compose.animation.core.rememberInfiniteTransition import androidx.compose.animation.core.rememberInfiniteTransition
@@ -27,7 +26,6 @@ import androidx.compose.foundation.layout.aspectRatio
import androidx.compose.material3.MaterialTheme import androidx.compose.material3.MaterialTheme
import androidx.compose.runtime.Composable import androidx.compose.runtime.Composable
import androidx.compose.runtime.mutableFloatStateOf import androidx.compose.runtime.mutableFloatStateOf
import androidx.compose.runtime.mutableIntStateOf
import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember import androidx.compose.runtime.remember
import androidx.compose.ui.Modifier 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.Stroke
import androidx.compose.ui.graphics.drawscope.rotate import androidx.compose.ui.graphics.drawscope.rotate
import androidx.compose.ui.graphics.drawscope.translate 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.TextMeasurer
import androidx.compose.ui.text.TextStyle import androidx.compose.ui.text.TextStyle
import androidx.compose.ui.text.drawText import androidx.compose.ui.text.drawText
@@ -59,15 +55,15 @@ import kotlin.math.sin
@Composable @Composable
fun RadarViewCompose( fun RadarViewCompose(
item: OrbitalPos?, item: OrbitalPos,
items: List<OrbitalPos>, items: List<OrbitalPos>,
azimElev: Pair<Float, Float>, azimElev: Pair<Float, Float>,
shouldShowSweep: Boolean, shouldShowSweep: Boolean,
shouldUseCompass: Boolean, shouldUseCompass: Boolean,
modifier: Modifier = Modifier modifier: Modifier = Modifier
) { ) {
val context = LocalContext.current // val context = LocalContext.current
val view = LocalView.current // val view = LocalView.current
val radarColor = MaterialTheme.colorScheme.secondary val radarColor = MaterialTheme.colorScheme.secondary
val trackColor = MaterialTheme.colorScheme.primary val trackColor = MaterialTheme.colorScheme.primary
val aimColor = MaterialTheme.colorScheme.error val aimColor = MaterialTheme.colorScheme.error
@@ -75,15 +71,15 @@ fun RadarViewCompose(
val animTransition = rememberInfiniteTransition(label = "animScale") val animTransition = rememberInfiniteTransition(label = "animScale")
val animSpec = infiniteRepeatable<Float>(tween(1000)) val animSpec = infiniteRepeatable<Float>(tween(1000))
val animScale = animTransition.animateFloat(16f, 64f, animSpec, label = "animScale") val animScale = animTransition.animateFloat(16f, 64f, animSpec, label = "animScale")
val aimThreshold = 0.05f // val aimThreshold = 0.05f
val measurer = rememberTextMeasurer() val measurer = rememberTextMeasurer()
val sweepDegrees = remember { mutableFloatStateOf(0f) } val sweepDegrees = remember { mutableFloatStateOf(0f) }
val trackCreated = remember { mutableStateOf(false) } val trackCreated = remember { mutableStateOf(false) }
val trackPath = remember { mutableStateOf(Path()) } val trackPath = remember { mutableStateOf(Path()) }
val trackEffect = remember { mutableStateOf(PathEffect.cornerPathEffect(0f)) } val trackEffect = remember { mutableStateOf(PathEffect.cornerPathEffect(0f)) }
val soundPool = remember { mutableStateOf<SoundPool?>(null) } // val soundPool = remember { mutableStateOf<SoundPool?>(null) }
val beepSoundId = remember { mutableIntStateOf(0) } // val beepSoundId = remember { mutableIntStateOf(0) }
val aimTargetDifference = remember { mutableFloatStateOf(0f) } // val aimTargetDifference = remember { mutableFloatStateOf(0f) }
// LaunchedEffect(item.azimuth, item.elevation, azimElev.first, azimElev.second) { // LaunchedEffect(item.azimuth, item.elevation, azimElev.first, azimElev.second) {
// val aimAzimuthRadians = azimElev.first.toDouble().toRadians() // val aimAzimuthRadians = azimElev.first.toDouble().toRadians()
@@ -95,13 +91,10 @@ fun RadarViewCompose(
// val satX = sph2CartX(item.azimuth, item.elevation, radius) // val satX = sph2CartX(item.azimuth, item.elevation, radius)
// val satY = sph2CartY(item.azimuth, item.elevation, radius) // val satY = sph2CartY(item.azimuth, item.elevation, radius)
// aimTargetDifference.floatValue = sqrt((satX - aimX).pow(2) + (satY - aimY).pow(2)) // aimTargetDifference.floatValue = sqrt((satX - aimX).pow(2) + (satY - aimY).pow(2))
//
//
// val minPlaybackRate = 0.5f // val minPlaybackRate = 0.5f
// val maxPlaybackRate = 2.0f // val maxPlaybackRate = 2.0f
// val playbackRate = // val playbackRate =
// maxPlaybackRate - (aimTargetDifference.floatValue / (maxPlaybackRate - minPlaybackRate)) // maxPlaybackRate - (aimTargetDifference.floatValue / (maxPlaybackRate - minPlaybackRate))
//
// soundPool.value?.setRate(beepSoundId.intValue, playbackRate) // soundPool.value?.setRate(beepSoundId.intValue, playbackRate)
// } // }
// //
@@ -127,20 +120,16 @@ fun RadarViewCompose(
// .setUsage(AudioAttributes.USAGE_ASSISTANCE_SONIFICATION) // .setUsage(AudioAttributes.USAGE_ASSISTANCE_SONIFICATION)
// .setContentType(AudioAttributes.CONTENT_TYPE_SONIFICATION) // .setContentType(AudioAttributes.CONTENT_TYPE_SONIFICATION)
// .build() // .build()
//
// soundPool.value = SoundPool.Builder() // soundPool.value = SoundPool.Builder()
// .setMaxStreams(1) // .setMaxStreams(1)
// .setAudioAttributes(audioAttributes) // .setAudioAttributes(audioAttributes)
// .build() // .build()
//
// beepSoundId.intValue = soundPool.value?.load(context, R.raw.beep, 1) ?: 0 // beepSoundId.intValue = soundPool.value?.load(context, R.raw.beep, 1) ?: 0
//
// soundPool.value?.setOnLoadCompleteListener { soundPool, _, status -> // soundPool.value?.setOnLoadCompleteListener { soundPool, _, status ->
// if (status == 0) { // if (status == 0) {
// soundPool.play(beepSoundId.intValue, 0.5f, 0.5f, 0, -1, 1f) // soundPool.play(beepSoundId.intValue, 0.5f, 0.5f, 0, -1, 1f)
// } // }
// } // }
//
// onDispose { // onDispose {
// soundPool.value?.release() // soundPool.value?.release()
// } // }
@@ -161,7 +150,7 @@ fun RadarViewCompose(
drawInfo(radius, trackColor, measurer, 3) drawInfo(radius, trackColor, measurer, 3)
translate(center.x, center.y) { translate(center.x, center.y) {
drawTrack(trackPath.value, trackEffect.value, aimColor, trackColor) drawTrack(trackPath.value, trackEffect.value, aimColor, trackColor)
if (item != null && item.elevation > 0) { if (item.elevation > 0) {
drawPosition(item, radius, animScale.value, trackColor) drawPosition(item, radius, animScale.value, trackColor)
} }
if (shouldUseCompass) { if (shouldUseCompass) {
@@ -25,7 +25,10 @@ import androidx.lifecycle.viewModelScope
import androidx.lifecycle.viewmodel.initializer import androidx.lifecycle.viewmodel.initializer
import androidx.lifecycle.viewmodel.viewModelFactory import androidx.lifecycle.viewmodel.viewModelFactory
import com.rtbishop.look4sat.MainApplication 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.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.data.framework.NetworkReporter
import com.rtbishop.look4sat.domain.model.SatRadio import com.rtbishop.look4sat.domain.model.SatRadio
import com.rtbishop.look4sat.domain.predict.OrbitalObject import com.rtbishop.look4sat.domain.predict.OrbitalObject
@@ -65,6 +68,8 @@ class RadarViewModel(
satTrack = emptyList(), satTrack = emptyList(),
shouldShowSweep = settingsRepo.otherSettings.value.stateOfSweep, shouldShowSweep = settingsRepo.otherSettings.value.stateOfSweep,
shouldUseCompass = settingsRepo.otherSettings.value.stateOfSensors, shouldUseCompass = settingsRepo.otherSettings.value.stateOfSensors,
selectedTransmitterUuid = null,
selectedFrequency = null,
transmitters = emptyList(), transmitters = emptyList(),
sendAction = ::handleAction, sendAction = ::handleAction,
) )
@@ -109,9 +114,9 @@ class RadarViewModel(
_uiState.update { it.copy(currentTime = time, isCurrentTimeAos = false) } _uiState.update { it.copy(currentTime = time, isCurrentTimeAos = false) }
} }
} }
processRadios(transmitters, satPass.orbitalObject, timeNow)
sendPassData(satPass, pos, satPass.orbitalObject) sendPassData(satPass, pos, satPass.orbitalObject)
sendPassDataBT(pos) sendPassDataBT(pos)
processRadios(transmitters, satPass.orbitalObject, timeNow)
delay(1000) delay(1000)
} }
} }
@@ -126,6 +131,7 @@ class RadarViewModel(
private fun handleAction(action: RadarAction) { private fun handleAction(action: RadarAction) {
when (action) { when (action) {
is RadarAction.AddToCalendar -> addToCalendar(action.name, action.aosTime, action.losTime) 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) { if (settingsRepo.rcSettings.value.rotatorState) {
val server = settingsRepo.rcSettings.value.rotatorAddress val server = settingsRepo.rcSettings.value.rotatorAddress
val port = settingsRepo.rcSettings.value.rotatorPort.toInt() val port = settingsRepo.rcSettings.value.rotatorPort.toInt()
val format = settingsRepo.rcSettings.value.rotatorFormat
val azimuth = orbitalPos.azimuth.toDegrees().round(2) val azimuth = orbitalPos.azimuth.toDegrees().round(2)
val elevation = orbitalPos.elevation.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) { private fun sendPassDataBT(orbitalPos: OrbitalPos) {
viewModelScope.launch { viewModelScope.launch {
if (settingsRepo.rcSettings.value.bluetoothState) { if (settingsRepo.rcSettings.value.bluetoothRotatorState) {
val btDevice = settingsRepo.rcSettings.value.bluetoothAddress val btRotatorDevice = settingsRepo.rcSettings.value.bluetoothRotatorAddress
if (bluetoothReporter.isConnected()) { if (bluetoothReporter.isConnected(BtService.ROTATOR)) {
val format = settingsRepo.rcSettings.value.bluetoothFormat val format = settingsRepo.rcSettings.value.bluetoothRotatorFormat
val azimuth = orbitalPos.azimuth.toDegrees().round(0).toInt() val azimuth = orbitalPos.azimuth.toDegrees().round(0)
val elevation = orbitalPos.elevation.toDegrees().round(0).toInt() val elevation = orbitalPos.elevation.toDegrees().round(0)
bluetoothReporter.reportRotation(format, azimuth, elevation) bluetoothReporter.reportRotation(format, azimuth, elevation, WithoutExtParams(0))
} else if (!bluetoothReporter.isConnecting()) { } else if (!bluetoothReporter.isConnecting(BtService.ROTATOR)) {
// Log.i("BTReporter", "BTReporter: Attempting to connect...") // Log.i("BTReporter", "BTReporter (rotator): Attempting to connect...")
bluetoothReporter.connectBTDevice(btDevice) 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) { private suspend fun processRadios(radios: List<SatRadio>, orbitalObject: OrbitalObject, time: Long) {
val transmitters = satelliteRepo.getRadios(orbitalObject, stationPos, radios, time) 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 { companion object {
@@ -27,6 +27,20 @@ import androidx.compose.ui.tooling.preview.Preview
import com.rtbishop.look4sat.R import com.rtbishop.look4sat.R
import com.rtbishop.look4sat.presentation.MainTheme import com.rtbishop.look4sat.presentation.MainTheme
import com.rtbishop.look4sat.presentation.common.SharedDialog 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) @Preview(showBackground = true)
@Composable @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 titleText = stringResource(id = R.string.prefs_station_title)
val onAccept = { saveValues(latValue.value, lonValue.value, save).also { dismiss() } } val onAccept = { saveValues(latValue.value, lonValue.value, save).also { dismiss() } }
SharedDialog(title = titleText, onCancel = dismiss, onAccept = onAccept) { SharedDialog(title = titleText, onCancel = dismiss, onAccept = onAccept) {
Text(text = stringResource(id = R.string.prefs_station_lat_text)) OutlinedTextField(
OutlinedTextField(value = latValue.value, onValueChange = { latValue.value = it }) value = latValue.value,
Text(text = stringResource(id = R.string.prefs_station_lon_text)) onValueChange = { latValue.value = it },
OutlinedTextField(value = lonValue.value, onValueChange = { lonValue.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 locator = rememberSaveable { mutableStateOf(qthLocator) }
val onAccept = { save(locator.value).also { dismiss() } } val onAccept = { save(locator.value).also { dismiss() } }
SharedDialog(title = stringResource(R.string.prefs_locator_title), onCancel = dismiss, onAccept = onAccept) { SharedDialog(title = stringResource(R.string.prefs_locator_title), onCancel = dismiss, onAccept = onAccept) {
Text(text = stringResource(id = R.string.prefs_locator_text)) OutlinedTextField(
OutlinedTextField(value = locator.value, onValueChange = { locator.value = it }) 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.padding
import androidx.compose.foundation.layout.width import androidx.compose.foundation.layout.width
import androidx.compose.foundation.lazy.grid.GridCells import androidx.compose.foundation.lazy.grid.GridCells
import androidx.compose.foundation.lazy.grid.GridItemSpan
import androidx.compose.foundation.lazy.grid.LazyVerticalGrid import androidx.compose.foundation.lazy.grid.LazyVerticalGrid
import androidx.compose.material3.ElevatedCard import androidx.compose.material3.ElevatedCard
import androidx.compose.material3.Icon import androidx.compose.material3.Icon
import androidx.compose.material3.LinearProgressIndicator import androidx.compose.material3.LinearProgressIndicator
import androidx.compose.material3.MaterialTheme import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.OutlinedTextField
import androidx.compose.material3.Switch import androidx.compose.material3.Switch
import androidx.compose.material3.Text import androidx.compose.material3.Text
import androidx.compose.runtime.Composable import androidx.compose.runtime.Composable
@@ -60,7 +60,6 @@ import androidx.navigation.NavGraphBuilder
import androidx.navigation.compose.composable import androidx.navigation.compose.composable
import com.rtbishop.look4sat.R import com.rtbishop.look4sat.R
import com.rtbishop.look4sat.domain.model.OtherSettings 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.domain.predict.GeoPos
import com.rtbishop.look4sat.presentation.MainTheme import com.rtbishop.look4sat.presentation.MainTheme
import com.rtbishop.look4sat.presentation.Screen import com.rtbishop.look4sat.presentation.Screen
@@ -87,19 +86,6 @@ fun NavGraphBuilder.settingsDestination() {
@Composable @Composable
private fun SettingsScreen(uiState: SettingsState) { 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 locationContract = ActivityResultContracts.RequestMultiplePermissions()
val locationError = stringResource(R.string.prefs_loc_gps_error) val locationError = stringResource(R.string.prefs_loc_gps_error)
val locationPermCoarse = Manifest.permission.ACCESS_COARSE_LOCATION val locationPermCoarse = Manifest.permission.ACCESS_COARSE_LOCATION
@@ -112,8 +98,11 @@ private fun SettingsScreen(uiState: SettingsState) {
} }
} }
val contentContract = ActivityResultContracts.GetContent() val contentContract = ActivityResultContracts.GetContent()
val contentRequest = rememberLauncherForActivityResult(contentContract) { uri -> val contentRequestForTle = rememberLauncherForActivityResult(contentContract) { uri ->
uri?.let { uiState.sendAction(SettingsAction.UpdateFromFile(uri.toString())) } uri?.let { uiState.sendAction(SettingsAction.UpdateTLEFromFile(uri.toString())) }
}
val contentRequestForTransceivers = rememberLauncherForActivityResult(contentContract) { uri ->
uri?.let { uiState.sendAction(SettingsAction.UpdateTransceiversFromFile(uri.toString())) }
} }
// Position settings // Position settings
@@ -137,10 +126,39 @@ private fun SettingsScreen(uiState: SettingsState) {
LocatorDialog(posSettings.stationPos.qthLocator, showLocDialog, setQthPos) 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 // Data settings
val dataSettings = uiState.dataSettings val dataSettings = uiState.dataSettings
val updateFromWeb: () -> Unit = { uiState.sendAction(SettingsAction.UpdateFromWeb) } val updateFromWeb: () -> Unit = { uiState.sendAction(SettingsAction.UpdateFromWeb) }
val updateFromFile = { contentRequest.launch("*/*") }
val clearAllData: () -> Unit = { uiState.sendAction(SettingsAction.ClearAllData) } val clearAllData: () -> Unit = { uiState.sendAction(SettingsAction.ClearAllData) }
// RC settings // RC settings
@@ -148,12 +166,84 @@ private fun SettingsScreen(uiState: SettingsState) {
val setRotatorState = { value: Boolean -> uiState.sendRCAction(RCAction.SetRotatorState(value)) } val setRotatorState = { value: Boolean -> uiState.sendRCAction(RCAction.SetRotatorState(value)) }
val setRotatorAddress = { value: String -> uiState.sendRCAction(RCAction.SetRotatorAddress(value)) } val setRotatorAddress = { value: String -> uiState.sendRCAction(RCAction.SetRotatorAddress(value)) }
val setRotatorPort = { value: String -> uiState.sendRCAction(RCAction.SetRotatorPort(value)) } val setRotatorPort = { value: String -> uiState.sendRCAction(RCAction.SetRotatorPort(value)) }
val setBluetoothState = { value: Boolean -> val setRotatorFormat = { value: String -> uiState.sendRCAction(RCAction.SetRotatorFormat(value)) }
bluetoothRequest.launch(bluetoothPerm) val setFrequencyState = { value: Boolean -> uiState.sendRCAction(RCAction.SetFrequencyState(value)) }
uiState.sendRCAction(RCAction.SetBluetoothState(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 // Other settings
val otherSettings = uiState.otherSettings val otherSettings = uiState.otherSettings
@@ -209,9 +299,8 @@ private fun SettingsScreen(uiState: SettingsState) {
item { item {
LocationCard(posSettings, setGpsPos, showPosDialog, showLocDialog, dismissPos, uiState.sendSystemAction) LocationCard(posSettings, setGpsPos, showPosDialog, showLocDialog, dismissPos, uiState.sendSystemAction)
} }
item { DataCard(dataSettings, updateFromWeb, updateFromFile, clearAllData) } item { DataCard(dataSettings, updateFromWeb, clearAllData, showDataSourcesDialog) }
item { NetworkOutputCard(rcSettings, setRotatorState, setRotatorAddress, setRotatorPort) } item(span = { GridItemSpan(maxLineSpan) }) { OutputCard({ showNetworkDialog() }, { showBluetoothDialog() }) }
item { BluetoothOutputCard(rcSettings, setBluetoothState, setBluetoothAddress, setBluetoothFormat) }
item { OtherCard(otherSettings, toggleUtc, toggleUpdate, toggleSweep, toggleSensor) } item { OtherCard(otherSettings, toggleUtc, toggleUpdate, toggleSweep, toggleSensor) }
item { CardCredits() } item { CardCredits() }
} }
@@ -290,15 +379,15 @@ private fun LocationCard(
@Composable @Composable
private fun DataCardPreview() = MainTheme { private fun DataCardPreview() = MainTheme {
val settings = DataSettings(true, 5000, 2500, 0L) val settings = DataSettings(true, 5000, 2500, 0L)
DataCard(settings = settings, {}, {}) {} DataCard(settings = settings, {}, {}, {})
} }
@Composable @Composable
private fun DataCard( private fun DataCard(
settings: DataSettings, settings: DataSettings,
updateFromWeb: () -> Unit, updateFromWeb: () -> Unit,
updateFromFile: () -> Unit, clearAllData: () -> Unit,
clearAllData: () -> Unit showDataSourcesDialog: () -> Unit
) { ) {
ElevatedCard(modifier = Modifier.fillMaxWidth()) { ElevatedCard(modifier = Modifier.fillMaxWidth()) {
Column( Column(
@@ -320,21 +409,19 @@ private fun DataCard(
Text(text = stringResource(R.string.prefs_data_radios, settings.radiosTotal)) Text(text = stringResource(R.string.prefs_data_radios, settings.radiosTotal))
} }
Spacer(modifier = Modifier.height(1.dp)) Spacer(modifier = Modifier.height(1.dp))
Row(horizontalArrangement = Arrangement.SpaceEvenly) { Row(horizontalArrangement = Arrangement.spacedBy(6.dp)) {
CardButton( CardButton(
onClick = { updateFromWeb() }, onClick = updateFromWeb,
text = stringResource(id = R.string.prefs_data_update), text = stringResource(id = R.string.prefs_data_update),
modifier = Modifier.weight(1f) modifier = Modifier.weight(1f)
) )
Spacer(modifier = Modifier.width(6.dp))
CardButton( CardButton(
onClick = { updateFromFile() }, onClick = showDataSourcesDialog,
text = stringResource(id = R.string.prefs_data_import), text = stringResource(id = R.string.prefs_data_import),
modifier = Modifier.weight(1f) modifier = Modifier.weight(1f)
) )
Spacer(modifier = Modifier.width(6.dp))
CardButton( CardButton(
onClick = { clearAllData() }, onClick = clearAllData,
text = stringResource(id = R.string.prefs_data_clear), text = stringResource(id = R.string.prefs_data_clear),
modifier = Modifier.weight(1f) modifier = Modifier.weight(1f)
) )
@@ -345,126 +432,32 @@ private fun DataCard(
@Preview(showBackground = true) @Preview(showBackground = true)
@Composable @Composable
private fun NetworkOutputCardPreview() = MainTheme { private fun OutputCardPreview() = MainTheme { OutputCard({}, {}) }
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)
}
@Composable @Composable
private fun NetworkOutputCard( private fun OutputCard(
settings: RCSettings, onNetworkClick: () -> Unit,
setRotatorState: (Boolean) -> Unit = {}, onBluetoothClick: () -> Unit
setRotatorAddress: (String) -> Unit = {},
setRotatorPort: (String) -> Unit = {}
) { ) {
ElevatedCard(modifier = Modifier.fillMaxWidth()) { ElevatedCard(modifier = Modifier.fillMaxWidth()) {
Column(modifier = Modifier.padding(horizontal = 8.dp, vertical = 4.dp)) { Column(modifier = Modifier.padding(horizontal = 8.dp, vertical = 4.dp)) {
Text( Text(
text = stringResource(id = R.string.prefs_net_title), text = stringResource(id = R.string.prefs_data_output_title),
color = MaterialTheme.colorScheme.primary color = MaterialTheme.colorScheme.primary
) )
Row( Spacer(modifier = Modifier.height(6.dp))
horizontalArrangement = Arrangement.SpaceBetween, Row(horizontalArrangement = Arrangement.spacedBy(6.dp)) {
verticalAlignment = Alignment.CenterVertically, CardButton(
modifier = Modifier.fillMaxWidth() onClick = onNetworkClick,
) { text = stringResource(id = R.string.prefs_net_output),
Text(text = stringResource(R.string.prefs_net_switch)) modifier = Modifier.weight(1f)
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
) )
OutlinedTextField( CardButton(
value = settings.rotatorPort, onClick = onBluetoothClick,
onValueChange = { setRotatorPort(it) }, text = stringResource(id = R.string.prefs_bt_output),
label = { Text(text = stringResource(R.string.prefs_net_port_hint)) }, modifier = Modifier.weight(1f)
singleLine = true,
modifier = Modifier.weight(1f),
enabled = settings.rotatorState
) )
} }
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, stateOfUtc = false,
stateOfLightTheme = false, stateOfLightTheme = false,
shouldSeeWarning = false, shouldSeeWarning = false,
shouldSeeWelcome = false shouldSeeWhatsNew = false
) )
OtherCard(settings = values, {}, {}, {}, {}) 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.OtherSettings
import com.rtbishop.look4sat.domain.model.RCSettings 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.GeoPos
import com.rtbishop.look4sat.domain.predict.OrbitalPass import com.rtbishop.look4sat.domain.predict.OrbitalPass
@@ -39,9 +40,11 @@ data class SettingsState(
val dataSettings: DataSettings, val dataSettings: DataSettings,
val otherSettings: OtherSettings, val otherSettings: OtherSettings,
val rcSettings: RCSettings, val rcSettings: RCSettings,
val dataSourcesSettings: DataSourcesSettings,
val sendAction: (SettingsAction) -> Unit, val sendAction: (SettingsAction) -> Unit,
val sendRCAction: (RCAction) -> Unit, val sendRCAction: (RCAction) -> Unit,
val sendSystemAction: (SystemAction) -> Unit val sendSystemAction: (SystemAction) -> Unit,
val sendDataSourcesAction: (DataSourcesAction) -> Unit
) )
sealed class SettingsAction { sealed class SettingsAction {
@@ -50,7 +53,8 @@ sealed class SettingsAction {
data class SetQthPosition(val locator: String) : SettingsAction() data class SetQthPosition(val locator: String) : SettingsAction()
data object DismissPosMessages : SettingsAction() data object DismissPosMessages : SettingsAction()
data object UpdateFromWeb : 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 object ClearAllData : SettingsAction()
data class ToggleUtc(val value: Boolean) : SettingsAction() data class ToggleUtc(val value: Boolean) : SettingsAction()
data class ToggleUpdate(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 SetRotatorState(val value: Boolean) : RCAction()
data class SetRotatorAddress(val value: String) : RCAction() data class SetRotatorAddress(val value: String) : RCAction()
data class SetRotatorPort(val value: String) : RCAction() data class SetRotatorPort(val value: String) : RCAction()
data class SetBluetoothState(val value: Boolean) : RCAction() data class SetRotatorFormat(val value: String) : RCAction()
data class SetBluetoothFormat(val value: String) : RCAction() data class SetFrequencyState(val value: Boolean) : RCAction()
data class SetBluetoothName(val value: String) : RCAction() data class SetFrequencyAddress(val value: String) : RCAction()
data class SetBluetoothAddress(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 androidx.lifecycle.viewmodel.viewModelFactory
import com.rtbishop.look4sat.MainApplication import com.rtbishop.look4sat.MainApplication
import com.rtbishop.look4sat.R import com.rtbishop.look4sat.R
import com.rtbishop.look4sat.domain.model.DataSourcesSettings
import com.rtbishop.look4sat.domain.predict.OrbitalPass import com.rtbishop.look4sat.domain.predict.OrbitalPass
import com.rtbishop.look4sat.domain.repository.IDatabaseRepo import com.rtbishop.look4sat.domain.repository.IDatabaseRepo
import com.rtbishop.look4sat.domain.repository.ISatelliteRepo import com.rtbishop.look4sat.domain.repository.ISatelliteRepo
@@ -45,25 +46,32 @@ class SettingsViewModel(
private val databaseRepo: IDatabaseRepo, private val databaseRepo: IDatabaseRepo,
private val satelliteRepo: ISatelliteRepo, private val satelliteRepo: ISatelliteRepo,
private val settingsRepo: ISettingsRepo, private val settingsRepo: ISettingsRepo,
private val showToast: IShowToast, private val showToast: IShowToast
appVersionName: String
) : ViewModel() { ) : ViewModel() {
private val defaultPosSettings = PositionSettings(false, settingsRepo.stationPosition.value, 0) private val defaultPosSettings = PositionSettings(false, settingsRepo.stationPosition.value, 0)
private val defaultDataSettings = DataSettings(false, 0, 0, 0L) 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( private val _uiState = MutableStateFlow(
SettingsState( SettingsState(
nextTime = "00:00:00", nextTime = "00:00:00",
isNextTimeAos = true, isNextTimeAos = true,
nextPass = getDefaultPass(), nextPass = getDefaultPass(),
appVersionName = appVersionName, appVersionName = settingsRepo.appVersionName,
positionSettings = defaultPosSettings, positionSettings = defaultPosSettings,
dataSettings = defaultDataSettings, dataSettings = defaultDataSettings,
otherSettings = settingsRepo.otherSettings.value, otherSettings = settingsRepo.otherSettings.value,
rcSettings = settingsRepo.rcSettings.value, rcSettings = settingsRepo.rcSettings.value,
dataSourcesSettings = defaultDataSourcesSettings,
sendAction = ::handleAction, sendAction = ::handleAction,
sendRCAction = ::handleAction, sendRCAction = ::handleAction,
sendSystemAction = ::handleAction sendSystemAction = ::handleAction,
sendDataSourcesAction = ::handleAction
) )
) )
private var processing: Job? = null private var processing: Job? = null
@@ -113,6 +121,11 @@ class SettingsViewModel(
_uiState.update { it.copy(otherSettings = settings) } _uiState.update { it.copy(otherSettings = settings) }
} }
} }
viewModelScope.launch {
settingsRepo.dataSourcesSettings.collect { settings ->
_uiState.update { it.copy(dataSourcesSettings = settings) }
}
}
} }
private fun handleAction(action: SettingsAction) { private fun handleAction(action: SettingsAction) {
@@ -122,7 +135,8 @@ class SettingsViewModel(
is SettingsAction.SetQthPosition -> setQthPosition(action.locator) is SettingsAction.SetQthPosition -> setQthPosition(action.locator)
SettingsAction.DismissPosMessages -> dismissPosMessage() SettingsAction.DismissPosMessages -> dismissPosMessage()
SettingsAction.UpdateFromWeb -> updateFromWeb() SettingsAction.UpdateFromWeb -> updateFromWeb()
is SettingsAction.UpdateFromFile -> updateFromFile(action.uri) is SettingsAction.UpdateTLEFromFile -> updateTLEFromFile(action.uri)
is SettingsAction.UpdateTransceiversFromFile -> updateTransceiversFromFile(action.uri)
SettingsAction.ClearAllData -> clearAllData() SettingsAction.ClearAllData -> clearAllData()
is SettingsAction.ToggleUtc -> settingsRepo.setStateOfUtc(action.value) is SettingsAction.ToggleUtc -> settingsRepo.setStateOfUtc(action.value)
is SettingsAction.ToggleUpdate -> settingsRepo.setStateOfAutoUpdate(action.value) is SettingsAction.ToggleUpdate -> settingsRepo.setStateOfAutoUpdate(action.value)
@@ -137,10 +151,18 @@ class SettingsViewModel(
is RCAction.SetRotatorState -> settingsRepo.setRotatorState(action.value) is RCAction.SetRotatorState -> settingsRepo.setRotatorState(action.value)
is RCAction.SetRotatorAddress -> settingsRepo.setRotatorAddress(action.value) is RCAction.SetRotatorAddress -> settingsRepo.setRotatorAddress(action.value)
is RCAction.SetRotatorPort -> settingsRepo.setRotatorPort(action.value) is RCAction.SetRotatorPort -> settingsRepo.setRotatorPort(action.value)
is RCAction.SetBluetoothState -> settingsRepo.setBluetoothState(action.value) is RCAction.SetRotatorFormat -> settingsRepo.setRotatorFormat(action.value)
is RCAction.SetBluetoothAddress -> settingsRepo.setBluetoothAddress(action.value) is RCAction.SetFrequencyState -> settingsRepo.setFrequencyState(action.value)
is RCAction.SetBluetoothFormat -> settingsRepo.setBluetoothFormat(action.value) is RCAction.SetFrequencyAddress -> settingsRepo.setFrequencyAddress(action.value)
is RCAction.SetBluetoothName -> settingsRepo.setBluetoothName(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() { private fun setGpsPosition() {
if (settingsRepo.setStationPosition()) { if (settingsRepo.setStationPosition()) {
val messageResId = R.string.prefs_loc_success 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 { try {
val newDataSettings = _uiState.value.dataSettings.copy(isUpdating = true) val newDataSettings = _uiState.value.dataSettings.copy(isUpdating = true)
_uiState.update { it.copy(dataSettings = newDataSettings) } _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) { } catch (exception: Exception) {
val newDataSettings = _uiState.value.dataSettings.copy(isUpdating = false) val newDataSettings = _uiState.value.dataSettings.copy(isUpdating = false)
_uiState.update { it.copy(dataSettings = newDataSettings) } _uiState.update { it.copy(dataSettings = newDataSettings) }
@@ -253,8 +296,7 @@ class SettingsViewModel(
container.databaseRepo, container.databaseRepo,
container.satelliteRepo, container.satelliteRepo,
container.settingsRepo, container.settingsRepo,
container.provideShowToast(), container.provideShowToast()
container.provideAppVersionName()
) )
} }
} }
+22 -6
View File
@@ -108,15 +108,31 @@
<string name="prefs_data_clear_success">Datos eliminados con éxito</string> <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_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_title">Salida de datos de red</string>
<string name="prefs_net_switch">Habilitar salida</string> <string name="prefs_net_rotator_switch">Enable rotation output</string>
<string name="prefs_net_ip_hint">Dirección IP</string> <string name="prefs_net_rotator_ip_hint">Dirección IP</string>
<string name="prefs_net_port_hint">Puerto</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_title">Salida por Bluetooth</string>
<string name="prefs_bt_switch">Habilitar salida</string> <string name="prefs_bt_rotator_switch">Enable rotation output</string>
<string name="prefs_bt_device_hint">Id dispositivo</string> <string name="prefs_bt_rotator_device_hint">Id dispositivo</string>
<string name="prefs_bt_output_hint">Formato de datos</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_bt_perm_error">Revisa los permisos de Bluetooth</string>
<string name="prefs_other_title">Otros ajustes</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_qth_error">Неправильный локатор QTH</string>
<string name="prefs_loc_success">Позиция успешно обновлена</string> <string name="prefs_loc_success">Позиция успешно обновлена</string>
<string name="prefs_station_title">Настройки местоположения</string> <string name="prefs_station_title">Настройки местоположения</string>
<string name="prefs_station_lat_text">Установите широту наземной станции</string> <string name="prefs_station_lat_text">Введите широту наземной станции</string>
<string name="prefs_station_lon_text">Установите долготу наземной станции</string> <string name="prefs_station_lon_text">Введите долготу наземной станции</string>
<string name="prefs_locator_title">Настройки локатора QTH</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_title">Данные спутников</string>
<string name="prefs_data_entries">Спутников: %s</string> <string name="prefs_data_entries">Спутников: %s</string>
@@ -107,15 +107,31 @@
<string name="prefs_data_clear_success">Очистка прошла успешно</string> <string name="prefs_data_clear_success">Очистка прошла успешно</string>
<string name="prefs_data_update_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_title">Вывод сетевых данных</string>
<string name="prefs_net_switch">Включить вывод</string> <string name="prefs_net_rotator_switch">Включить вывод ротатора</string>
<string name="prefs_net_ip_hint">IP адрес</string> <string name="prefs_net_rotator_ip_hint">IP адрес</string>
<string name="prefs_net_port_hint">Порт</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_title">Вывод данных Bluetooth</string>
<string name="prefs_bt_switch">Включить вывод</string> <string name="prefs_bt_rotator_switch">Включить вывод ротатора</string>
<string name="prefs_bt_device_hint">Id устройства</string> <string name="prefs_bt_rotator_device_hint">Id устройства</string>
<string name="prefs_bt_output_hint">Формат данных</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_bt_perm_error">Нет разрешения использовать bluetooth</string>
<string name="prefs_other_title">Другие настройки</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_clear_success">දත්ත ඉවත් කිරීම සාර්ථකයි</string>
<string name="prefs_data_update_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_title">ජාල ප්‍රතිදානය</string>
<string name="prefs_net_switch">ප්‍රතිදානය සක්‍රීය කරන්න</string> <string name="prefs_net_rotator_switch">Enable rotation output</string>
<string name="prefs_net_ip_hint">IP ලිපිනය</string> <string name="prefs_net_rotator_ip_hint">IP ලිපිනය</string>
<string name="prefs_net_port_hint">Port</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_title">Bluetooth දත්ත ප්‍රතිදානය</string>
<string name="prefs_bt_switch">ප්‍රතිදානය සක්‍රීය කරන්න</string> <string name="prefs_bt_rotator_switch">Enable rotation output</string>
<string name="prefs_bt_device_hint">උපාංග id</string> <string name="prefs_bt_rotator_device_hint">උපාංග id</string>
<string name="prefs_bt_output_hint">දත්ත අකාරය</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_bt_perm_error">Bluetooth අවසර පරික්‍ෂා ක°</string>
<string name="prefs_other_title">වෙනත් සැකසුම්</string> <string name="prefs_other_title">වෙනත් සැකසුම්</string>
+28 -12
View File
@@ -17,7 +17,7 @@
<string name="sat_type_title">Виберіть тип супутника</string> <string name="sat_type_title">Виберіть тип супутника</string>
<string name="sat_search_hint">Id - Назва</string> <string name="sat_search_hint">Id - Назва</string>
<string name="sat_search_clear">Очистити</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_select_all">Вибрати всі</string>
<string name="sat_empty_list_message"> <string name="sat_empty_list_message">
Переконайтеся, що ваш пошуковий запит правильний, а база даних оновлена</string> Переконайтеся, що ваш пошуковий запит правильний, а база даних оновлена</string>
@@ -87,15 +87,15 @@
<string name="prefs_loc_title">Місцезнаходження</string> <string name="prefs_loc_title">Місцезнаходження</string>
<string name="prefs_loc_gps_title">GPS</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_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_title">QTH</string>
<string name="prefs_loc_qth_error">Некоректний локатор QTH</string> <string name="prefs_loc_qth_error">Некоректний локатор QTH</string>
<string name="prefs_loc_success">Позицію успішно оновлено</string> <string name="prefs_loc_success">Позицію успішно оновлено</string>
<string name="prefs_station_title">Налаштування місцезнаходження станції</string> <string name="prefs_station_title">Налаштування місцезнаходження станції</string>
<string name="prefs_station_lat_text">Задайте широту станції</string> <string name="prefs_station_lat_text">Широта місцезнаходження станції</string>
<string name="prefs_station_lon_text">Задайте довготу станції</string> <string name="prefs_station_lon_text">Довгота місцезнаходження станції</string>
<string name="prefs_locator_title">Налаштування QTH локатора</string> <string name="prefs_locator_title">Налаштування QTH локатора</string>
<string name="prefs_locator_text">Задати позицію за QTH локатором</string> <string name="prefs_locator_text">Задати позицію за QTH локатором</string>
@@ -108,15 +108,31 @@
<string name="prefs_data_clear_success">Дані успішно очищено</string> <string name="prefs_data_clear_success">Дані успішно очищено</string>
<string name="prefs_data_update_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_title">Вивід мережевих даних</string>
<string name="prefs_net_switch">Увімкнути вихід</string> <string name="prefs_net_rotator_switch">Enable rotation output</string>
<string name="prefs_net_ip_hint">IP адреса</string> <string name="prefs_net_rotator_ip_hint">IP адреса</string>
<string name="prefs_net_port_hint">Порт</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_title">Вивід даних Bluetooth</string>
<string name="prefs_bt_switch">Увімкнути вихід</string> <string name="prefs_bt_rotator_switch">Enable rotation output</string>
<string name="prefs_bt_device_hint">Id пристрою</string> <string name="prefs_bt_rotator_device_hint">Id пристрою</string>
<string name="prefs_bt_output_hint">Формат даних</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_bt_perm_error">Перевірте дозвіл Bluetooth</string>
<string name="prefs_other_title">Інші налаштування</string> <string name="prefs_other_title">Інші налаштування</string>
@@ -126,6 +142,6 @@
<string name="prefs_other_switch_sensors">Використовувати сенсори пристрою</string> <string name="prefs_other_switch_sensors">Використовувати сенсори пристрою</string>
<string name="prefs_outro_title">Я хотів би подякувати:</string> <string name="prefs_outro_title">Я хотів би подякувати:</string>
<string name="prefs_outro_license">Це ПЗ поставляється без жодних гарантій.</string> <string name="prefs_outro_license">Ця програма поставляється без жодних гарантій</string>
</resources> </resources>
+23 -7
View File
@@ -80,7 +80,7 @@
<string name="prefs_loc_title">站位</string> <string name="prefs_loc_title">站位</string>
<string name="prefs_loc_gps_title">GPS 定位</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_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_title">QTH 网格</string>
<string name="prefs_loc_qth_error">无效的 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_clear_success">数据清空成功</string>
<string name="prefs_data_update_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_title">网络数据输出</string>
<string name="prefs_net_switch">启用输出</string> <string name="prefs_net_rotator_switch">启用方位俯仰角输出</string>
<string name="prefs_net_ip_hint">IP地址</string> <string name="prefs_net_rotator_ip_hint">IP地址</string>
<string name="prefs_net_port_hint">端口</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_title">蓝牙数据输出</string>
<string name="prefs_bt_switch">启用输出</string> <string name="prefs_bt_rotator_switch">启用方位俯仰角输出</string>
<string name="prefs_bt_device_hint">蓝牙设备 ID</string> <string name="prefs_bt_rotator_device_hint">蓝牙设备 ID</string>
<string name="prefs_bt_output_hint">数据格式</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_bt_perm_error">请检查蓝牙权限</string>
<string name="prefs_other_title">其他设置</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_search_clear">Clear</string>
<string name="sat_clear_all">Clear all</string> <string name="sat_clear_all">Clear all</string>
<string name="sat_select_all">Select 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_title">Warning\!</string>
<string name="sat_warning_message"> <string name="sat_warning_message">
There are over 9000 satellites listed in this app. There are over 9000 satellites listed in this app.
@@ -46,6 +46,11 @@
<string name="pass_welcome_message"> <string name="pass_welcome_message">
Make sure to set your position via GPS, Lat/Lon or QTH in Settings. 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> \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 --> <!-- Radar screen -->
<string name="radar_back">Back</string> <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_clear_success">Data was cleared successfully</string>
<string name="prefs_data_update_success">Update completed 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_title">Network data output</string>
<string name="prefs_net_switch">Enable output</string> <string name="prefs_net_rotator_switch">Enable rotation output</string>
<string name="prefs_net_ip_hint">IP address</string> <string name="prefs_net_rotator_ip_hint">IP address</string>
<string name="prefs_net_port_hint">Port</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_title">Bluetooth data output</string>
<string name="prefs_bt_switch">Enable output</string> <string name="prefs_bt_rotator_switch">Enable rotation output</string>
<string name="prefs_bt_device_hint">Device id</string> <string name="prefs_bt_rotator_device_hint">Device id</string>
<string name="prefs_bt_output_hint">Data format</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_bt_perm_error">Check your bluetooth permission</string>
<string name="prefs_other_title">Other settings</string> <string name="prefs_other_title">Other settings</string>
+1 -1
View File
@@ -4,7 +4,7 @@ plugins {
} }
kotlin { kotlin {
jvmToolchain(21) jvmToolchain(17)
} }
android { android {
@@ -18,6 +18,7 @@
package com.rtbishop.look4sat.data.framework package com.rtbishop.look4sat.data.framework
import android.bluetooth.BluetoothManager import android.bluetooth.BluetoothManager
import android.bluetooth.BluetoothSocket
import android.util.Log import android.util.Log
import java.io.OutputStream import java.io.OutputStream
import java.util.* import java.util.*
@@ -25,69 +26,110 @@ import kotlin.math.abs
import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Job import kotlinx.coroutines.Job
import kotlinx.coroutines.launch 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( class BluetoothReporter(
private val bluetoothManager: BluetoothManager, private val bluetoothManager: BluetoothManager,
private val reporterScope: CoroutineScope private val reporterScope: CoroutineScope
) { ) : IReporterRepo<WithoutExtParams> {
private val tag = "BTReporter" private val tag = "BTReporter"
private val sppid: UUID = UUID.fromString("00001101-0000-1000-8000-00805f9b34fb") 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) { fun connect(service: BtService, deviceId: String) {
if (!connected) { serviceToDevice[service] = deviceId
rotationConnectionJob = reporterScope.launch { val connection = deviceConnections.getOrPut(deviceId) {
try { DeviceConnection()
bluetoothManager.adapter.getRemoteDevice(deviceId)?.let { device -> }
device.createInsecureRfcommSocketToServiceRecord(sppid)?.let { socket -> if (connection.connected || connection.connecting) return
connecting = true connection.connectionJob = reporterScope.launch {
socket.connect() try {
outputStream = socket.outputStream connection.connecting = true
connected = true val device = bluetoothManager.adapter.getRemoteDevice(deviceId)
connecting = false val socket = device.createInsecureRfcommSocketToServiceRecord(sppid)
Log.i(tag, "$tag: Connected!") socket.connect()
} connection.socket = socket
} connection.outputStream = socket.outputStream
} catch (e: SecurityException) { connection.connected = true
Log.e(tag, "$tag: ${e.message}") Log.i(tag, "$tag: Connected to $deviceId")
} catch (e: Exception) { } catch (e: Exception) {
Log.e(tag, "$tag: ${e.message}") Log.e(tag, "$tag: ${e.message}")
} connection.connected = false
} finally {
connection.connecting = false
} }
} }
} }
fun reportRotation(format: String, azimuth: Int, elevation: Int) { private suspend fun write(service: BtService, buffer: String) {
if (connected) { val deviceId = serviceToDevice[service] ?: return
rotationReportingJob = reporterScope.launch { val connection = deviceConnections[deviceId] ?: return
val newElevation = if (elevation > 0) elevation else 0 if (!connection.connected) return
try { try {
val azimuthString = intToStringWithLeadingZeroes(azimuth) writeMutex.withLock {
val elevationString = intToStringWithLeadingZeroes(newElevation) connection.outputStream?.write(buffer.toByteArray())
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
}
} }
} 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 java.nio.channels.SocketChannel
import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Job import kotlinx.coroutines.Job
import kotlinx.coroutines.cancelAndJoin import kotlinx.coroutines.sync.Mutex
import kotlinx.coroutines.sync.withLock
import kotlinx.coroutines.launch 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 enum class NetworkService { ROTATOR, FREQUENCY }
private var rotationReporting: Job? = null
// private var frequencySocketChannel: SocketChannel? = null data class SocketConnection(
// private var frequencyReporting: Job? = null var socket: SocketChannel? = null,
// var connected: Boolean = false,
// fun reportFrequency(server: String, port: Int, frequency: Long) { var connecting: Boolean = false,
// frequencyReporting = reporterScope.launch { var connectionJob: Job? = null
// 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()
// }
// }
// }
fun reportRotation(server: String, port: Int, azimuth: Double, elevation: Double) { class NetworkReporter(private val reporterScope: CoroutineScope) : IReporterRepo<ExtendedParams> {
rotationReporting = reporterScope.launch { private val serviceToAddress = mutableMapOf<NetworkService, String>()
val newElevation = if (elevation > 0.0) elevation else 0.0 private val connections = mutableMapOf<String, SocketConnection>()
runCatching { private val writeMutex = Mutex()
if (rotationSocketChannel == null) {
rotationSocketChannel = SocketChannel.open(InetSocketAddress(server, port)) fun isConnected(service: NetworkService): Boolean {
} else { val addr = serviceToAddress[service] ?: return false
val buffer = ByteBuffer.wrap("\\P $azimuth $newElevation\n".toByteArray()) return connections[addr]?.connected == true
rotationSocketChannel?.write(buffer) }
}
}.onFailure { error -> private fun getOrCreateConnection(addr: String): SocketConnection {
println(error.localizedMessage) return connections.getOrPut(addr) { SocketConnection() }
rotationSocketChannel = null }
rotationReporting?.cancelAndJoin()
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 satelliteRepo = provideSatelliteRepo()
val databaseRepo = provideDatabaseRepo() val databaseRepo = provideDatabaseRepo()
fun provideAppVersionName(): String {
return context.packageManager.getPackageInfo(context.packageName, 0).versionName ?: "4.0.0"
}
fun provideAddToCalendar(): IAddToCalendar = AddToCalendar(context) fun provideAddToCalendar(): IAddToCalendar = AddToCalendar(context)
fun provideShowToast(): IShowToast = ShowToast(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 manager = context.getSystemService(Context.LOCATION_SERVICE) as LocationManager
val appPrefsFileName = "${context.packageName}_preferences" val appPrefsFileName = "${context.packageName}_preferences"
val appPreferences = context.getSharedPreferences(appPrefsFileName, Context.MODE_PRIVATE) 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 private val settingsRepo: ISettingsRepo
) : IDatabaseRepo { ) : 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) } val importedSatellites = remoteSource.getFileStream(uri)?.let { dataParser.parseTLEStream(it) }
importedSatellites?.let { localSource.insertEntries(it) } importedSatellites?.let { localSource.insertEntries(it) }
setUpdateSuccessful(System.currentTimeMillis()) 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) { override suspend fun updateFromRemote() = withContext(dispatcher) {
val importedEntries = mutableListOf<OrbitalData>() val importedEntries = mutableListOf<OrbitalData>()
val importedRadios = mutableListOf<SatRadio>() val importedRadios = mutableListOf<SatRadio>()
// fetch val tleUrls = Sources.satelliteDataUrls.toMutableMap().apply {
val jobsMap = Sources.satelliteDataUrls.mapValues { async { remoteSource.getNetworkStream(it.value) } } if (settingsRepo.dataSourcesSettings.value.useCustomTLE) {
val jobRadios = async { remoteSource.getNetworkStream(Sources.RADIO_DATA_URL) } 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 // parse
jobsMap.mapValues { job -> job.value.await() }.forEach { entry -> jobsMap.mapValues { job -> job.value.await() }.forEach { entry ->
entry.value?.let { stream -> entry.value?.let { stream ->
when (val type = entry.key) { when (val type = entry.key) {
"Amsat", "R4UAB" -> { "Amsat", "R4UAB", "Other" -> {
// parse tle stream // parse tle stream
val satellites = dataParser.parseTLEStream(stream) val satellites = dataParser.parseTLEStream(stream)
val catnums = satellites.map { it.catnum } 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 // insert
localSource.insertEntries(importedEntries) localSource.insertEntries(importedEntries)
localSource.insertRadios(importedRadios) 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.OtherSettings
import com.rtbishop.look4sat.domain.model.PassesSettings import com.rtbishop.look4sat.domain.model.PassesSettings
import com.rtbishop.look4sat.domain.model.RCSettings 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.GeoPos
import com.rtbishop.look4sat.domain.repository.ISettingsRepo import com.rtbishop.look4sat.domain.repository.ISettingsRepo
import com.rtbishop.look4sat.domain.utility.positionToQth import com.rtbishop.look4sat.domain.utility.positionToQth
@@ -38,13 +39,17 @@ import kotlinx.coroutines.flow.update
class SettingsRepo( class SettingsRepo(
private val locationManager: LocationManager, private val locationManager: LocationManager,
private val preferences: SharedPreferences private val preferences: SharedPreferences,
override val appVersionName: String
) : ISettingsRepo, LocationListenerCompat { ) : ISettingsRepo, LocationListenerCompat {
private val keyBluetoothAddress = "bluetoothAddress" private val keyBluetoothRotatorAddress = "bluetoothAddress"
private val keyBluetoothName = "bluetoothName" private val keyBluetoothRotatorName = "bluetoothName"
private val keyBluetoothFormat = "bluetoothFormat" private val keyBluetoothRotatorFormat = "bluetoothFormat"
private val keyBluetoothState = "bluetoothState" private val keyBluetoothRotatorState = "bluetoothState"
private val keyBluetoothFrequencyState = "bluetoothFrequencyState"
private val keyBluetoothFrequencyAddress = "bluetoothFrequencyAddress"
private val keyBluetoothFrequencyFormat = "bluetoothFrequencyFormat"
private val keyFilterHoursAhead = "filterHoursAhead" private val keyFilterHoursAhead = "filterHoursAhead"
private val keyFilterMinElevation = "filterMinElevation" private val keyFilterMinElevation = "filterMinElevation"
private val keyNumberOfRadios = "numberOfRadios" private val keyNumberOfRadios = "numberOfRadios"
@@ -52,6 +57,11 @@ class SettingsRepo(
private val keyRotatorAddress = "rotatorAddress" private val keyRotatorAddress = "rotatorAddress"
private val keyRotatorPort = "rotatorPort" private val keyRotatorPort = "rotatorPort"
private val keyRotatorState = "rotatorState" 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 keySelectedIds = "selectedIds"
private val keySelectedTypes = "selectedTypes" private val keySelectedTypes = "selectedTypes"
private val keySelectedModes = "selectedModes" private val keySelectedModes = "selectedModes"
@@ -67,7 +77,11 @@ class SettingsRepo(
private val keyStationTimestamp = "stationTimestamp" private val keyStationTimestamp = "stationTimestamp"
private val keyUpdateTimestamp = "updateTimestamp" private val keyUpdateTimestamp = "updateTimestamp"
private val keyShouldSeeWarning = "shouldSeeWarning" 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 = "," private val separatorComma = ","
//region # Satellites selection settings //region # Satellites selection settings
@@ -235,24 +249,39 @@ class SettingsRepo(
private val _rcSettings = MutableStateFlow(getRCSettings()) private val _rcSettings = MutableStateFlow(getRCSettings())
override val rcSettings: StateFlow<RCSettings> = _rcSettings override val rcSettings: StateFlow<RCSettings> = _rcSettings
override fun setBluetoothAddress(value: String) { override fun setBluetoothRotatorAddress(value: String) {
preferences.edit { putString(keyBluetoothAddress, value) } preferences.edit { putString(keyBluetoothRotatorAddress, value) }
_rcSettings.update { it.copy(bluetoothAddress = value) } _rcSettings.update { it.copy(bluetoothRotatorAddress = value) }
} }
override fun setBluetoothFormat(value: String) { override fun setBluetoothRotatorFormat(value: String) {
preferences.edit { putString(keyBluetoothFormat, value) } preferences.edit { putString(keyBluetoothRotatorFormat, value) }
_rcSettings.update { it.copy(bluetoothFormat = value) } _rcSettings.update { it.copy(bluetoothRotatorFormat = value) }
} }
override fun setBluetoothName(value: String) { override fun setBluetoothRotatorName(value: String) {
preferences.edit { putString(keyBluetoothName, value) } preferences.edit { putString(keyBluetoothRotatorName, value) }
_rcSettings.update { it.copy(bluetoothName = value) } _rcSettings.update { it.copy(bluetoothRotatorName = value) }
} }
override fun setBluetoothState(value: Boolean) { override fun setBluetoothRotatorState(value: Boolean) {
preferences.edit { putBoolean(keyBluetoothState, value) } preferences.edit { putBoolean(keyBluetoothRotatorState, value) }
_rcSettings.update { it.copy(bluetoothState = 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) { override fun setRotatorAddress(value: String) {
@@ -270,14 +299,47 @@ class SettingsRepo(
_rcSettings.update { it.copy(rotatorState = value) } _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( private fun getRCSettings(): RCSettings = RCSettings(
rotatorState = preferences.getBoolean(keyRotatorState, false), rotatorState = preferences.getBoolean(keyRotatorState, false),
rotatorAddress = preferences.getString(keyRotatorAddress, null) ?: "127.0.0.1", rotatorAddress = preferences.getString(keyRotatorAddress, null) ?: "127.0.0.1",
rotatorPort = preferences.getString(keyRotatorPort, null) ?: "4533", rotatorPort = preferences.getString(keyRotatorPort, null) ?: "4533",
bluetoothState = preferences.getBoolean(keyBluetoothState, false), rotatorFormat = preferences.getString(keyRotatorFormat, null) ?: $$"P $AZ $EL",
bluetoothFormat = preferences.getString(keyBluetoothFormat, null) ?: $$"W$AZ $EL", frequencyState = preferences.getBoolean(keyFrequencyState, false),
bluetoothName = preferences.getString(keyBluetoothName, null) ?: "Default", frequencyAddress = preferences.getString(keyFrequencyAddress, null) ?: "127.0.0.1",
bluetoothAddress = preferences.getString(keyBluetoothAddress, null) ?: "00:0C:BF:13:80:5D" 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 //endregion
@@ -315,9 +377,9 @@ class SettingsRepo(
_otherSettings.update { it.copy(shouldSeeWarning = false) } _otherSettings.update { it.copy(shouldSeeWarning = false) }
} }
override fun setWelcomeDismissed() { override fun setWhatsNewDismissed() {
preferences.edit { putBoolean(keyShouldSeeWelcome, false) } preferences.edit { putBoolean(keyShouldSeeWhatsNew, false) }
_otherSettings.update { it.copy(shouldSeeWelcome = false) } _otherSettings.update { it.copy(shouldSeeWhatsNew = false) }
} }
private fun getOtherSettings(): OtherSettings = OtherSettings( private fun getOtherSettings(): OtherSettings = OtherSettings(
@@ -327,7 +389,39 @@ class SettingsRepo(
stateOfUtc = preferences.getBoolean(keyStateOfUtc, false), stateOfUtc = preferences.getBoolean(keyStateOfUtc, false),
stateOfLightTheme = preferences.getBoolean(keyStateOfLightTheme, false), stateOfLightTheme = preferences.getBoolean(keyStateOfLightTheme, false),
shouldSeeWarning = preferences.getBoolean(keyShouldSeeWarning, true), 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 //endregion
} }
+1 -1
View File
@@ -3,7 +3,7 @@ plugins {
} }
kotlin { kotlin {
jvmToolchain(21) jvmToolchain(17)
} }
dependencies { dependencies {
@@ -33,10 +33,18 @@ data class RCSettings(
val rotatorState: Boolean, val rotatorState: Boolean,
val rotatorAddress: String, val rotatorAddress: String,
val rotatorPort: String, val rotatorPort: String,
val bluetoothState: Boolean, val rotatorFormat: String,
val bluetoothFormat: String, val frequencyState: Boolean,
val bluetoothName: String, val frequencyAddress: String,
val bluetoothAddress: 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( data class OtherSettings(
@@ -46,5 +54,12 @@ data class OtherSettings(
val stateOfUtc: Boolean, val stateOfUtc: Boolean,
val stateOfLightTheme: Boolean, val stateOfLightTheme: Boolean,
val shouldSeeWarning: 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 package com.rtbishop.look4sat.domain.repository
interface IDatabaseRepo { interface IDatabaseRepo {
suspend fun updateFromFile(uri: String) suspend fun updateTLEFromFile(uri: String)
suspend fun updateTransceiversFromFile(uri: String)
suspend fun updateFromRemote() suspend fun updateFromRemote()
suspend fun clearAllData() 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.OtherSettings
import com.rtbishop.look4sat.domain.model.PassesSettings import com.rtbishop.look4sat.domain.model.PassesSettings
import com.rtbishop.look4sat.domain.model.RCSettings 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.GeoPos
import kotlinx.coroutines.flow.StateFlow import kotlinx.coroutines.flow.StateFlow
interface ISettingsRepo { interface ISettingsRepo {
val appVersionName: String
//region # Satellites selection settings //region # Satellites selection settings
val selectedIds: StateFlow<List<Int>> val selectedIds: StateFlow<List<Int>>
val selectedTypes: StateFlow<List<String>> val selectedTypes: StateFlow<List<String>>
@@ -54,13 +57,21 @@ interface ISettingsRepo {
//region # RC settings //region # RC settings
val rcSettings: StateFlow<RCSettings> val rcSettings: StateFlow<RCSettings>
fun setBluetoothAddress(value: String) fun setBluetoothRotatorAddress(value: String)
fun setBluetoothFormat(value: String) fun setBluetoothRotatorFormat(value: String)
fun setBluetoothName(value: String) fun setBluetoothRotatorName(value: String)
fun setBluetoothState(value: Boolean) fun setBluetoothRotatorState(value: Boolean)
fun setBluetoothFrequencyAddress(value: String)
fun setBluetoothFrequencyFormat(value: String)
fun setBluetoothFrequencyState(value: Boolean)
fun setRotatorAddress(value: String) fun setRotatorAddress(value: String)
fun setRotatorPort(value: String) fun setRotatorPort(value: String)
fun setRotatorState(value: Boolean) 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 //endregion
//region # Other settings //region # Other settings
@@ -71,6 +82,14 @@ interface ISettingsRepo {
fun setStateOfUtc(value: Boolean) fun setStateOfUtc(value: Boolean)
fun setStateOfLightTheme(value: Boolean) fun setStateOfLightTheme(value: Boolean)
fun setWarningDismissed() 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 //endregion
} }
@@ -23,7 +23,6 @@ object Sources {
"All" to "https://celestrak.org/NORAD/elements/gp.php?GROUP=active&FORMAT=csv", "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", "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", "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", "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", "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", "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", "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", "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", "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", "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", "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", "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", "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", "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", "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", "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", "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", "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", "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 What's New dialog to announce changes and features
Added AOS/LOS info to the countdown component Added remote frequency control feature, by londre7 #195
Added various minor UI fixes for readability Updated the Chinese translation, by wty2019wty #196
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
@@ -1,9 +1,3 @@
Added support for landscape layout and tablets Added What's New dialog to announce changes and features
Added AOS/LOS info to the countdown component Added remote frequency control feature, by londre7 #195
Added various minor UI fixes for readability Updated the Chinese translation, by wty2019wty #196
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
+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] [versions]
android-gradle-plugin = "9.0.0" android-gradle-plugin = "9.0.1"
google-ksp = "2.3.2" google-ksp = "2.3.5"
kotlin = "2.3.0" kotlin = "2.3.10"
androidx-core-ktx = "1.17.0" androidx-core-ktx = "1.17.0"
androidx-core-splashscreen = "1.2.0" androidx-core-splashscreen = "1.2.0"
androidx-room = "2.8.4" androidx-room = "2.8.4"
compose-bom = "2026.01.01" compose-bom = "2026.02.00"
compose-activity = "1.12.3" compose-activity = "1.12.4"
compose-lifecycle = "2.10.0" compose-lifecycle = "2.10.0"
compose-navigation = "2.9.7" 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 = { group = "androidx.compose.material3", name = "material3" }
compose-material3-navigation = { group = "androidx.compose.material3", name = "material3-adaptive-navigation-suite" } compose-material3-navigation = { group = "androidx.compose.material3", name = "material3-adaptive-navigation-suite" }
compose-runtime = { group = "androidx.compose.runtime", name = "runtime" } 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-activity = { module = "androidx.activity:activity-compose", version.ref = "compose-activity" }
compose-lifecycle = { module = "androidx.lifecycle:lifecycle-runtime-compose", version.ref = "compose-lifecycle" } compose-lifecycle = { module = "androidx.lifecycle:lifecycle-runtime-compose", version.ref = "compose-lifecycle" }
-3
View File
@@ -5,9 +5,6 @@ pluginManagement {
gradlePluginPortal() gradlePluginPortal()
} }
} }
plugins {
id("org.gradle.toolchains.foojay-resolver-convention") version "1.0.0"
}
dependencyResolutionManagement { dependencyResolutionManagement {
repositoriesMode.set(RepositoriesMode.FAIL_ON_PROJECT_REPOS) repositoriesMode.set(RepositoriesMode.FAIL_ON_PROJECT_REPOS)
repositories { repositories {