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