Compare commits

...
9 Commits
27 changed files with 500 additions and 179 deletions

No files matched your search

+2 -2
View File
@@ -22,7 +22,7 @@ jobs:
uses: actions/setup-java@v5 uses: actions/setup-java@v5
with: with:
distribution: 'temurin' distribution: 'temurin'
java-version: '21' java-version: '17'
- name: Setup Gradle - name: Setup Gradle
uses: gradle/actions/setup-gradle@v5 uses: gradle/actions/setup-gradle@v5
@@ -67,7 +67,7 @@ jobs:
serviceAccountJsonPlainText: ${{secrets.SERVICE_ACCOUNT_JSON}} serviceAccountJsonPlainText: ${{secrets.SERVICE_ACCOUNT_JSON}}
packageName: com.rtbishop.look4sat packageName: com.rtbishop.look4sat
releaseFiles: app/build/outputs/apk/release/look4sat.aab releaseFiles: app/build/outputs/apk/release/look4sat.aab
track: beta track: production
whatsNewDirectory: fastlane/metadata/android/en-US/whatsnew whatsNewDirectory: fastlane/metadata/android/en-US/whatsnew
- name: Create Release - name: Create Release
+3 -3
View File
@@ -4,7 +4,7 @@ plugins {
} }
kotlin { kotlin {
jvmToolchain(21) jvmToolchain(17)
} }
android { android {
@@ -13,8 +13,8 @@ android {
defaultConfig { defaultConfig {
applicationId = "com.rtbishop.look4sat" applicationId = "com.rtbishop.look4sat"
minSdk = 24 minSdk = 24
versionCode = 400 versionCode = 403
versionName = "4.0.0" versionName = "4.0.3"
} }
buildFeatures { buildFeatures {
compose = true compose = true
@@ -17,6 +17,9 @@
*/ */
package com.rtbishop.look4sat.presentation package com.rtbishop.look4sat.presentation
import androidx.compose.animation.core.tween
import androidx.compose.animation.fadeIn
import androidx.compose.animation.fadeOut
import androidx.compose.material3.Icon import androidx.compose.material3.Icon
import androidx.compose.material3.MaterialTheme import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.Text import androidx.compose.material3.Text
@@ -76,7 +79,12 @@ fun MainScreen(navController: NavHostController = rememberNavController()) {
else -> NavigationSuiteType.ShortNavigationBarMedium else -> NavigationSuiteType.ShortNavigationBarMedium
} }
) { ) {
NavHost(navController = navController, startDestination = startDestination) { NavHost(
navController = navController,
startDestination = startDestination,
enterTransition = { fadeIn(animationSpec = tween(350)) },
exitTransition = { fadeOut(animationSpec = tween(350)) }
) {
satellitesDestination { navController.navigateUp() } satellitesDestination { navController.navigateUp() }
passesDestination { catNum: Int, aosTime: Long -> passesDestination { catNum: Int, aosTime: Long ->
val radarRoute = "${Screen.Radar.route}?catNum=${catNum}&aosTime=${aosTime}" val radarRoute = "${Screen.Radar.route}?catNum=${catNum}&aosTime=${aosTime}"
@@ -23,6 +23,7 @@ import androidx.compose.foundation.clickable
import androidx.compose.foundation.layout.Arrangement import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Box import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.PaddingValues
import androidx.compose.foundation.layout.Row import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.RowScope import androidx.compose.foundation.layout.RowScope
import androidx.compose.foundation.layout.Spacer import androidx.compose.foundation.layout.Spacer
@@ -210,8 +211,9 @@ fun CardButton(onClick: () -> Unit, text: String, modifier: Modifier = Modifier)
onClick = { onClick() }, colors = ButtonDefaults.buttonColors( onClick = { onClick() }, colors = ButtonDefaults.buttonColors(
containerColor = MaterialTheme.colorScheme.surfaceVariant, containerColor = MaterialTheme.colorScheme.surfaceVariant,
contentColor = MaterialTheme.colorScheme.onSurfaceVariant contentColor = MaterialTheme.colorScheme.onSurfaceVariant
), shape = MaterialTheme.shapes.small, modifier = modifier ), shape = MaterialTheme.shapes.small, modifier = modifier,
) { Text(text = text, fontSize = 17.sp) } contentPadding = PaddingValues(horizontal = 8.dp, vertical = 8.dp)
) { Text(text = text, fontSize = 16.sp, textAlign = TextAlign.Center) }
} }
@Composable @Composable
@@ -253,7 +255,7 @@ fun EmptyListCard(message: String) {
modifier = Modifier.padding(32.dp) modifier = Modifier.padding(32.dp)
) { ) {
Text(text = """¯\_(ツ)_/¯""", fontSize = 32.sp) Text(text = """¯\_(ツ)_/¯""", fontSize = 32.sp)
Text(text = stringResource(R.string.empty_list_message), fontSize = 21.sp) Text(text = stringResource(R.string.empty_list_message), fontSize = 21.sp, textAlign = TextAlign.Center)
Text(text = message, fontSize = 18.sp, textAlign = TextAlign.Center) Text(text = message, fontSize = 18.sp, textAlign = TextAlign.Center)
} }
} }
@@ -278,7 +280,7 @@ fun SharedDialog(
) { ) {
Text( Text(
text = title, text = title,
fontSize = 18.sp, fontSize = 16.sp,
fontWeight = FontWeight.Medium, fontWeight = FontWeight.Medium,
color = MaterialTheme.colorScheme.primary, color = MaterialTheme.colorScheme.primary,
modifier = Modifier.padding(start = padding, top = padding, end = padding) modifier = Modifier.padding(start = padding, top = padding, end = padding)
@@ -45,6 +45,7 @@ import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier import androidx.compose.ui.Modifier
import androidx.compose.ui.draw.alpha import androidx.compose.ui.draw.alpha
import androidx.compose.ui.draw.rotate import androidx.compose.ui.draw.rotate
import androidx.compose.ui.keepScreenOn
import androidx.compose.ui.res.painterResource import androidx.compose.ui.res.painterResource
import androidx.compose.ui.res.stringResource import androidx.compose.ui.res.stringResource
import androidx.compose.ui.semantics.contentDescription import androidx.compose.ui.semantics.contentDescription
@@ -64,6 +65,7 @@ import com.rtbishop.look4sat.domain.model.SatRadio
import com.rtbishop.look4sat.domain.utility.toDegrees import com.rtbishop.look4sat.domain.utility.toDegrees
import com.rtbishop.look4sat.presentation.MainTheme import com.rtbishop.look4sat.presentation.MainTheme
import com.rtbishop.look4sat.presentation.Screen import com.rtbishop.look4sat.presentation.Screen
import com.rtbishop.look4sat.presentation.common.EmptyListCard
import com.rtbishop.look4sat.presentation.common.IconCard import com.rtbishop.look4sat.presentation.common.IconCard
import com.rtbishop.look4sat.presentation.common.NextPassRow import com.rtbishop.look4sat.presentation.common.NextPassRow
import com.rtbishop.look4sat.presentation.common.TimerRow import com.rtbishop.look4sat.presentation.common.TimerRow
@@ -96,7 +98,7 @@ private fun RadarScreen(uiState: RadarState, navigateUp: () -> Unit) {
} }
val upcomingPass = uiState.currentPass ?: getDefaultPass() val upcomingPass = uiState.currentPass ?: getDefaultPass()
if (upcomingPass.losTime < System.currentTimeMillis()) navigateUp() if (upcomingPass.losTime < System.currentTimeMillis()) navigateUp()
Column(modifier = Modifier.layoutPadding(), verticalArrangement = Arrangement.spacedBy(6.dp)) { Column(modifier = Modifier.layoutPadding().keepScreenOn(), verticalArrangement = Arrangement.spacedBy(6.dp)) {
if (isVerticalLayout()) { if (isVerticalLayout()) {
TopBar { TopBar {
IconCard(action = navigateUp, resId = R.drawable.ic_back) IconCard(action = navigateUp, resId = R.drawable.ic_back)
@@ -115,49 +117,56 @@ private fun RadarScreen(uiState: RadarState, navigateUp: () -> Unit) {
if(isVerticalLayout()) { if(isVerticalLayout()) {
ElevatedCard(modifier = Modifier.weight(1f)) { ElevatedCard(modifier = Modifier.weight(1f)) {
Box(contentAlignment = Alignment.Center) { Box(contentAlignment = Alignment.Center) {
RadarViewCompose( if (uiState.orbitalPos == null) {
item = uiState.orbitalPos, ElevatedCard(modifier = Modifier.fillMaxSize()) {
items = uiState.satTrack, EmptyListCard(message = "")
azimElev = uiState.orientationValues,
shouldShowSweep = uiState.shouldShowSweep,
shouldUseCompass = uiState.shouldUseCompass,
modifier = Modifier.align(Alignment.Center)
)
Column(
verticalArrangement = Arrangement.SpaceBetween,
modifier = Modifier
.fillMaxSize()
.padding(horizontal = 6.dp, vertical = 4.dp)
) {
Row(
horizontalArrangement = Arrangement.SpaceBetween,
modifier = Modifier.fillMaxWidth()
) {
RadarTextTop(
uiState.orbitalPos?.azimuth ?: 0.0,
stringResource(R.string.radar_az_text),
true
)
RadarTextTop(
uiState.orbitalPos?.elevation ?: 0.0,
stringResource(R.string.radar_el_text),
false
)
} }
Row( }
horizontalArrangement = Arrangement.SpaceBetween, uiState.orbitalPos?.let { position ->
modifier = Modifier.fillMaxWidth() RadarViewCompose(
item = position,
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)
) { ) {
RadarTextBottom( Row(
uiState.orbitalPos?.altitude ?: 0.0, horizontalArrangement = Arrangement.SpaceBetween,
stringResource(R.string.radar_alt_text), modifier = Modifier.fillMaxWidth()
true ) {
) RadarTextTop(
RadarTextBottom( position.azimuth,
uiState.orbitalPos?.distance ?: 0.0, stringResource(R.string.radar_az_text),
stringResource(R.string.radar_dist_text), true
false )
) 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
)
}
} }
} }
} }
@@ -171,7 +180,11 @@ private fun RadarScreen(uiState: RadarState, navigateUp: () -> Unit) {
modifier = Modifier.padding(32.dp) modifier = Modifier.padding(32.dp)
) { ) {
Text(text = """¯\_(ツ)_/¯""", fontSize = 32.sp) Text(text = """¯\_(ツ)_/¯""", fontSize = 32.sp)
Text(text = stringResource(R.string.empty_list_message), fontSize = 21.sp) Text(
text = stringResource(R.string.empty_list_message),
fontSize = 21.sp,
textAlign = TextAlign.Center
)
Text( Text(
text = stringResource(R.string.radar_no_data), text = stringResource(R.string.radar_no_data),
fontSize = 18.sp, fontSize = 18.sp,
@@ -192,49 +205,56 @@ private fun RadarScreen(uiState: RadarState, navigateUp: () -> Unit) {
Row(horizontalArrangement = Arrangement.spacedBy(6.dp)) { Row(horizontalArrangement = Arrangement.spacedBy(6.dp)) {
ElevatedCard(modifier = Modifier.weight(1f)) { ElevatedCard(modifier = Modifier.weight(1f)) {
Box(contentAlignment = Alignment.Center) { Box(contentAlignment = Alignment.Center) {
RadarViewCompose( if (uiState.orbitalPos == null) {
item = uiState.orbitalPos, ElevatedCard(modifier = Modifier.fillMaxSize()) {
items = uiState.satTrack, EmptyListCard(message = "")
azimElev = uiState.orientationValues,
shouldShowSweep = uiState.shouldShowSweep,
shouldUseCompass = 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(
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, uiState.orbitalPos?.let { position ->
modifier = Modifier.fillMaxWidth() 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)
) { ) {
RadarTextBottom( Row(
uiState.orbitalPos?.altitude ?: 0.0, horizontalArrangement = Arrangement.SpaceBetween,
stringResource(R.string.radar_alt_text), modifier = Modifier.fillMaxWidth()
true ) {
) RadarTextTop(
RadarTextBottom( position.azimuth,
uiState.orbitalPos?.distance ?: 0.0, stringResource(R.string.radar_az_text),
stringResource(R.string.radar_dist_text), true
false )
) 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
)
}
} }
} }
} }
@@ -248,7 +268,11 @@ private fun RadarScreen(uiState: RadarState, navigateUp: () -> Unit) {
modifier = Modifier.padding(32.dp) modifier = Modifier.padding(32.dp)
) { ) {
Text(text = """¯\_(ツ)_/¯""", fontSize = 32.sp) Text(text = """¯\_(ツ)_/¯""", fontSize = 32.sp)
Text(text = stringResource(R.string.empty_list_message), fontSize = 21.sp) Text(
text = stringResource(R.string.empty_list_message),
fontSize = 21.sp,
textAlign = TextAlign.Center
)
Text( Text(
text = stringResource(R.string.radar_no_data), text = stringResource(R.string.radar_no_data),
fontSize = 18.sp, fontSize = 18.sp,
@@ -17,7 +17,6 @@
*/ */
package com.rtbishop.look4sat.presentation.radar package com.rtbishop.look4sat.presentation.radar
import android.media.SoundPool
import androidx.compose.animation.core.animateFloat import androidx.compose.animation.core.animateFloat
import androidx.compose.animation.core.infiniteRepeatable import androidx.compose.animation.core.infiniteRepeatable
import androidx.compose.animation.core.rememberInfiniteTransition import androidx.compose.animation.core.rememberInfiniteTransition
@@ -27,7 +26,6 @@ import androidx.compose.foundation.layout.aspectRatio
import androidx.compose.material3.MaterialTheme import androidx.compose.material3.MaterialTheme
import androidx.compose.runtime.Composable import androidx.compose.runtime.Composable
import androidx.compose.runtime.mutableFloatStateOf import androidx.compose.runtime.mutableFloatStateOf
import androidx.compose.runtime.mutableIntStateOf
import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember import androidx.compose.runtime.remember
import androidx.compose.ui.Modifier import androidx.compose.ui.Modifier
@@ -44,8 +42,6 @@ import androidx.compose.ui.graphics.drawscope.Fill
import androidx.compose.ui.graphics.drawscope.Stroke import androidx.compose.ui.graphics.drawscope.Stroke
import androidx.compose.ui.graphics.drawscope.rotate import androidx.compose.ui.graphics.drawscope.rotate
import androidx.compose.ui.graphics.drawscope.translate import androidx.compose.ui.graphics.drawscope.translate
import androidx.compose.ui.platform.LocalContext
import androidx.compose.ui.platform.LocalView
import androidx.compose.ui.text.TextMeasurer import androidx.compose.ui.text.TextMeasurer
import androidx.compose.ui.text.TextStyle import androidx.compose.ui.text.TextStyle
import androidx.compose.ui.text.drawText import androidx.compose.ui.text.drawText
@@ -59,15 +55,15 @@ import kotlin.math.sin
@Composable @Composable
fun RadarViewCompose( fun RadarViewCompose(
item: OrbitalPos?, item: OrbitalPos,
items: List<OrbitalPos>, items: List<OrbitalPos>,
azimElev: Pair<Float, Float>, azimElev: Pair<Float, Float>,
shouldShowSweep: Boolean, shouldShowSweep: Boolean,
shouldUseCompass: Boolean, shouldUseCompass: Boolean,
modifier: Modifier = Modifier modifier: Modifier = Modifier
) { ) {
val context = LocalContext.current // val context = LocalContext.current
val view = LocalView.current // val view = LocalView.current
val radarColor = MaterialTheme.colorScheme.secondary val radarColor = MaterialTheme.colorScheme.secondary
val trackColor = MaterialTheme.colorScheme.primary val trackColor = MaterialTheme.colorScheme.primary
val aimColor = MaterialTheme.colorScheme.error val aimColor = MaterialTheme.colorScheme.error
@@ -75,15 +71,15 @@ fun RadarViewCompose(
val animTransition = rememberInfiniteTransition(label = "animScale") val animTransition = rememberInfiniteTransition(label = "animScale")
val animSpec = infiniteRepeatable<Float>(tween(1000)) val animSpec = infiniteRepeatable<Float>(tween(1000))
val animScale = animTransition.animateFloat(16f, 64f, animSpec, label = "animScale") val animScale = animTransition.animateFloat(16f, 64f, animSpec, label = "animScale")
val aimThreshold = 0.05f // val aimThreshold = 0.05f
val measurer = rememberTextMeasurer() val measurer = rememberTextMeasurer()
val sweepDegrees = remember { mutableFloatStateOf(0f) } val sweepDegrees = remember { mutableFloatStateOf(0f) }
val trackCreated = remember { mutableStateOf(false) } val trackCreated = remember { mutableStateOf(false) }
val trackPath = remember { mutableStateOf(Path()) } val trackPath = remember { mutableStateOf(Path()) }
val trackEffect = remember { mutableStateOf(PathEffect.cornerPathEffect(0f)) } val trackEffect = remember { mutableStateOf(PathEffect.cornerPathEffect(0f)) }
val soundPool = remember { mutableStateOf<SoundPool?>(null) } // val soundPool = remember { mutableStateOf<SoundPool?>(null) }
val beepSoundId = remember { mutableIntStateOf(0) } // val beepSoundId = remember { mutableIntStateOf(0) }
val aimTargetDifference = remember { mutableFloatStateOf(0f) } // val aimTargetDifference = remember { mutableFloatStateOf(0f) }
// LaunchedEffect(item.azimuth, item.elevation, azimElev.first, azimElev.second) { // LaunchedEffect(item.azimuth, item.elevation, azimElev.first, azimElev.second) {
// val aimAzimuthRadians = azimElev.first.toDouble().toRadians() // val aimAzimuthRadians = azimElev.first.toDouble().toRadians()
@@ -95,13 +91,10 @@ fun RadarViewCompose(
// val satX = sph2CartX(item.azimuth, item.elevation, radius) // val satX = sph2CartX(item.azimuth, item.elevation, radius)
// val satY = sph2CartY(item.azimuth, item.elevation, radius) // val satY = sph2CartY(item.azimuth, item.elevation, radius)
// aimTargetDifference.floatValue = sqrt((satX - aimX).pow(2) + (satY - aimY).pow(2)) // aimTargetDifference.floatValue = sqrt((satX - aimX).pow(2) + (satY - aimY).pow(2))
//
//
// val minPlaybackRate = 0.5f // val minPlaybackRate = 0.5f
// val maxPlaybackRate = 2.0f // val maxPlaybackRate = 2.0f
// val playbackRate = // val playbackRate =
// maxPlaybackRate - (aimTargetDifference.floatValue / (maxPlaybackRate - minPlaybackRate)) // maxPlaybackRate - (aimTargetDifference.floatValue / (maxPlaybackRate - minPlaybackRate))
//
// soundPool.value?.setRate(beepSoundId.intValue, playbackRate) // soundPool.value?.setRate(beepSoundId.intValue, playbackRate)
// } // }
// //
@@ -127,20 +120,16 @@ fun RadarViewCompose(
// .setUsage(AudioAttributes.USAGE_ASSISTANCE_SONIFICATION) // .setUsage(AudioAttributes.USAGE_ASSISTANCE_SONIFICATION)
// .setContentType(AudioAttributes.CONTENT_TYPE_SONIFICATION) // .setContentType(AudioAttributes.CONTENT_TYPE_SONIFICATION)
// .build() // .build()
//
// soundPool.value = SoundPool.Builder() // soundPool.value = SoundPool.Builder()
// .setMaxStreams(1) // .setMaxStreams(1)
// .setAudioAttributes(audioAttributes) // .setAudioAttributes(audioAttributes)
// .build() // .build()
//
// beepSoundId.intValue = soundPool.value?.load(context, R.raw.beep, 1) ?: 0 // beepSoundId.intValue = soundPool.value?.load(context, R.raw.beep, 1) ?: 0
//
// soundPool.value?.setOnLoadCompleteListener { soundPool, _, status -> // soundPool.value?.setOnLoadCompleteListener { soundPool, _, status ->
// if (status == 0) { // if (status == 0) {
// soundPool.play(beepSoundId.intValue, 0.5f, 0.5f, 0, -1, 1f) // soundPool.play(beepSoundId.intValue, 0.5f, 0.5f, 0, -1, 1f)
// } // }
// } // }
//
// onDispose { // onDispose {
// soundPool.value?.release() // soundPool.value?.release()
// } // }
@@ -161,7 +150,7 @@ fun RadarViewCompose(
drawInfo(radius, trackColor, measurer, 3) drawInfo(radius, trackColor, measurer, 3)
translate(center.x, center.y) { translate(center.x, center.y) {
drawTrack(trackPath.value, trackEffect.value, aimColor, trackColor) drawTrack(trackPath.value, trackEffect.value, aimColor, trackColor)
if (item != null && item.elevation > 0) { if (item.elevation > 0) {
drawPosition(item, radius, animScale.value, trackColor) drawPosition(item, radius, animScale.value, trackColor)
} }
if (shouldUseCompass) { if (shouldUseCompass) {
@@ -27,6 +27,19 @@ import androidx.compose.ui.tooling.preview.Preview
import com.rtbishop.look4sat.R import com.rtbishop.look4sat.R
import com.rtbishop.look4sat.presentation.MainTheme import com.rtbishop.look4sat.presentation.MainTheme
import com.rtbishop.look4sat.presentation.common.SharedDialog import com.rtbishop.look4sat.presentation.common.SharedDialog
import com.rtbishop.look4sat.presentation.common.CardButton
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.Spacer
import androidx.compose.foundation.layout.height
import androidx.compose.foundation.layout.padding
import androidx.compose.material3.Switch
import androidx.compose.ui.unit.dp
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import com.rtbishop.look4sat.presentation.LocalSpacing
@Preview(showBackground = true) @Preview(showBackground = true)
@Composable @Composable
@@ -41,10 +54,16 @@ fun PositionDialog(lat: Double, lon: Double, dismiss: () -> Unit, save: (Double,
val titleText = stringResource(id = R.string.prefs_station_title) val titleText = stringResource(id = R.string.prefs_station_title)
val onAccept = { saveValues(latValue.value, lonValue.value, save).also { dismiss() } } val onAccept = { saveValues(latValue.value, lonValue.value, save).also { dismiss() } }
SharedDialog(title = titleText, onCancel = dismiss, onAccept = onAccept) { SharedDialog(title = titleText, onCancel = dismiss, onAccept = onAccept) {
Text(text = stringResource(id = R.string.prefs_station_lat_text)) OutlinedTextField(
OutlinedTextField(value = latValue.value, onValueChange = { latValue.value = it }) value = latValue.value,
Text(text = stringResource(id = R.string.prefs_station_lon_text)) onValueChange = { latValue.value = it },
OutlinedTextField(value = lonValue.value, onValueChange = { lonValue.value = it }) label = { Text(text = stringResource(id = R.string.prefs_station_lat_text)) }
)
OutlinedTextField(
value = lonValue.value,
onValueChange = { lonValue.value = it },
label = { Text(text = stringResource(id = R.string.prefs_station_lon_text)) }
)
} }
} }
@@ -67,7 +86,116 @@ fun LocatorDialog(qthLocator: String, dismiss: () -> Unit, save: (String) -> Uni
val locator = rememberSaveable { mutableStateOf(qthLocator) } val locator = rememberSaveable { mutableStateOf(qthLocator) }
val onAccept = { save(locator.value).also { dismiss() } } val onAccept = { save(locator.value).also { dismiss() } }
SharedDialog(title = stringResource(R.string.prefs_locator_title), onCancel = dismiss, onAccept = onAccept) { SharedDialog(title = stringResource(R.string.prefs_locator_title), onCancel = dismiss, onAccept = onAccept) {
Text(text = stringResource(id = R.string.prefs_locator_text)) OutlinedTextField(
OutlinedTextField(value = locator.value, onValueChange = { locator.value = it }) value = locator.value,
onValueChange = { locator.value = it },
label = { Text(text = stringResource(id = R.string.prefs_locator_text)) }
)
}
}
@Preview(showBackground = true)
@Composable
private fun TransceiversDialogPreview() {
MainTheme {
DataSourcesDialog(
useCustomTle = true,
useCustomTransceivers = true,
tleUrl = "https://example.com/tle.txt",
transceiversUrl = "https://example.com/tx.json",
onImportTle = {},
onImportTransceivers = {},
onDismiss = {},
onSave = { _, _, _, _ -> }
)
}
}
@Composable
fun DataSourcesDialog(
useCustomTle: Boolean,
useCustomTransceivers: Boolean,
tleUrl: String,
transceiversUrl: String,
onImportTle: () -> Unit,
onImportTransceivers: () -> Unit,
onDismiss: () -> Unit,
onSave: (Boolean, Boolean, String, String) -> Unit
) {
val padding = LocalSpacing.current.large
val isEnabledCustomTle = rememberSaveable { mutableStateOf(useCustomTle) }
val isEnabledCustomTransceivers = rememberSaveable { mutableStateOf(useCustomTransceivers) }
val urlTle = rememberSaveable { mutableStateOf(tleUrl) }
val urlTransceivers = rememberSaveable { mutableStateOf(transceiversUrl) }
val onAccept = {
onSave(isEnabledCustomTle.value, isEnabledCustomTransceivers.value, urlTle.value, urlTransceivers.value)
onDismiss()
}
val onCancel = { onDismiss() }
SharedDialog(
title = stringResource(id = R.string.prefs_data_sources_title),
onCancel = onCancel,
onAccept = onAccept,
) {
Column(modifier = Modifier.padding(horizontal = padding)) {
Row(horizontalArrangement = Arrangement.spacedBy(6.dp)) {
CardButton(
onClick = {
onImportTle()
onDismiss()
},
text = "TLE (3LE)\nR4UAB (.txt)",
modifier = Modifier.weight(1f)
)
CardButton(
onClick = {
onImportTransceivers()
onDismiss()
},
text = "Transceivers\nSatNOGS (.json)",
modifier = Modifier.weight(1f)
)
}
Spacer(modifier = Modifier.height(6.dp))
Row(
horizontalArrangement = Arrangement.SpaceBetween,
verticalAlignment = Alignment.CenterVertically,
modifier = Modifier.fillMaxWidth()
) {
Text(text = stringResource(id = R.string.prefs_data_sources_tle_switch))
Switch(
checked = isEnabledCustomTle.value,
onCheckedChange = { isEnabledCustomTle.value = it }
)
}
OutlinedTextField(
value = urlTle.value,
onValueChange = { urlTle.value = it },
label = { Text(text = stringResource(id = R.string.prefs_data_sources_url_title)) },
singleLine = true,
modifier = Modifier.fillMaxWidth(),
enabled = isEnabledCustomTle.value,
)
Spacer(modifier = Modifier.height(6.dp))
Row(
horizontalArrangement = Arrangement.SpaceBetween,
verticalAlignment = Alignment.CenterVertically,
modifier = Modifier.fillMaxWidth()
) {
Text(text = stringResource(id = R.string.prefs_data_sources_transceivers_switch))
Switch(
checked = isEnabledCustomTransceivers.value,
onCheckedChange = { isEnabledCustomTransceivers.value = it }
)
}
OutlinedTextField(
value = urlTransceivers.value,
onValueChange = { urlTransceivers.value = it },
label = { Text(text = stringResource(id = R.string.prefs_data_sources_url_title)) },
singleLine = true,
modifier = Modifier.fillMaxWidth(),
enabled = isEnabledCustomTransceivers.value,
)
}
} }
} }
@@ -112,8 +112,11 @@ private fun SettingsScreen(uiState: SettingsState) {
} }
} }
val contentContract = ActivityResultContracts.GetContent() val contentContract = ActivityResultContracts.GetContent()
val contentRequest = rememberLauncherForActivityResult(contentContract) { uri -> val contentRequestForTle = rememberLauncherForActivityResult(contentContract) { uri ->
uri?.let { uiState.sendAction(SettingsAction.UpdateFromFile(uri.toString())) } uri?.let { uiState.sendAction(SettingsAction.UpdateTLEFromFile(uri.toString())) }
}
val contentRequestForTransceivers = rememberLauncherForActivityResult(contentContract) { uri ->
uri?.let { uiState.sendAction(SettingsAction.UpdateTransceiversFromFile(uri.toString())) }
} }
// Position settings // Position settings
@@ -137,10 +140,39 @@ private fun SettingsScreen(uiState: SettingsState) {
LocatorDialog(posSettings.stationPos.qthLocator, showLocDialog, setQthPos) LocatorDialog(posSettings.stationPos.qthLocator, showLocDialog, setQthPos)
} }
// Data sources dialog
val dataSourcesDialogState = rememberSaveable { mutableStateOf(false) }
val showDataSourcesDialog = { dataSourcesDialogState.value = true }
val dismissDataSourcesDialog = { dataSourcesDialogState.value = false }
if (dataSourcesDialogState.value) {
DataSourcesDialog(
useCustomTle = uiState.dataSourcesSettings.useCustomTLE,
useCustomTransceivers = uiState.dataSourcesSettings.useCustomTransceivers,
tleUrl = uiState.dataSourcesSettings.tleUrl,
transceiversUrl = uiState.dataSourcesSettings.transceiversUrl,
onImportTle = { contentRequestForTle.launch("*/*") },
onImportTransceivers = { contentRequestForTransceivers.launch("*/*") },
onDismiss = dismissDataSourcesDialog,
onSave = {
useCustomTle, useCustomTransceivers, tleUrl, transceiversUrl ->
if (!useCustomTle || tleUrl.isNotBlank()) {
uiState.sendDataSourcesAction(DataSourcesAction.SetUseCustomTle(useCustomTle))
uiState.sendDataSourcesAction(DataSourcesAction.SetTleUrl(tleUrl))
}
if (!useCustomTransceivers || transceiversUrl.isNotBlank()) {
uiState.sendDataSourcesAction(DataSourcesAction.SetUseCustomTransceivers(useCustomTransceivers))
uiState.sendDataSourcesAction(DataSourcesAction.SetTransceiversUrl(transceiversUrl))
}
if (useCustomTle || useCustomTransceivers) {
uiState.sendAction(SettingsAction.UpdateFromWeb)
}
}
)
}
// Data settings // Data settings
val dataSettings = uiState.dataSettings val dataSettings = uiState.dataSettings
val updateFromWeb: () -> Unit = { uiState.sendAction(SettingsAction.UpdateFromWeb) } val updateFromWeb: () -> Unit = { uiState.sendAction(SettingsAction.UpdateFromWeb) }
val updateFromFile = { contentRequest.launch("*/*") }
val clearAllData: () -> Unit = { uiState.sendAction(SettingsAction.ClearAllData) } val clearAllData: () -> Unit = { uiState.sendAction(SettingsAction.ClearAllData) }
// RC settings // RC settings
@@ -209,7 +241,7 @@ private fun SettingsScreen(uiState: SettingsState) {
item { item {
LocationCard(posSettings, setGpsPos, showPosDialog, showLocDialog, dismissPos, uiState.sendSystemAction) LocationCard(posSettings, setGpsPos, showPosDialog, showLocDialog, dismissPos, uiState.sendSystemAction)
} }
item { DataCard(dataSettings, updateFromWeb, updateFromFile, clearAllData) } item { DataCard(dataSettings, updateFromWeb, clearAllData, showDataSourcesDialog) }
item { NetworkOutputCard(rcSettings, setRotatorState, setRotatorAddress, setRotatorPort) } item { NetworkOutputCard(rcSettings, setRotatorState, setRotatorAddress, setRotatorPort) }
item { BluetoothOutputCard(rcSettings, setBluetoothState, setBluetoothAddress, setBluetoothFormat) } item { BluetoothOutputCard(rcSettings, setBluetoothState, setBluetoothAddress, setBluetoothFormat) }
item { OtherCard(otherSettings, toggleUtc, toggleUpdate, toggleSweep, toggleSensor) } item { OtherCard(otherSettings, toggleUtc, toggleUpdate, toggleSweep, toggleSensor) }
@@ -290,15 +322,15 @@ private fun LocationCard(
@Composable @Composable
private fun DataCardPreview() = MainTheme { private fun DataCardPreview() = MainTheme {
val settings = DataSettings(true, 5000, 2500, 0L) val settings = DataSettings(true, 5000, 2500, 0L)
DataCard(settings = settings, {}, {}) {} DataCard(settings = settings, {}, {}, {})
} }
@Composable @Composable
private fun DataCard( private fun DataCard(
settings: DataSettings, settings: DataSettings,
updateFromWeb: () -> Unit, updateFromWeb: () -> Unit,
updateFromFile: () -> Unit, clearAllData: () -> Unit,
clearAllData: () -> Unit showDataSourcesDialog: () -> Unit
) { ) {
ElevatedCard(modifier = Modifier.fillMaxWidth()) { ElevatedCard(modifier = Modifier.fillMaxWidth()) {
Column( Column(
@@ -320,21 +352,19 @@ private fun DataCard(
Text(text = stringResource(R.string.prefs_data_radios, settings.radiosTotal)) Text(text = stringResource(R.string.prefs_data_radios, settings.radiosTotal))
} }
Spacer(modifier = Modifier.height(1.dp)) Spacer(modifier = Modifier.height(1.dp))
Row(horizontalArrangement = Arrangement.SpaceEvenly) { Row(horizontalArrangement = Arrangement.spacedBy(6.dp)) {
CardButton( CardButton(
onClick = { updateFromWeb() }, onClick = updateFromWeb,
text = stringResource(id = R.string.prefs_data_update), text = stringResource(id = R.string.prefs_data_update),
modifier = Modifier.weight(1f) modifier = Modifier.weight(1f)
) )
Spacer(modifier = Modifier.width(6.dp))
CardButton( CardButton(
onClick = { updateFromFile() }, onClick = showDataSourcesDialog,
text = stringResource(id = R.string.prefs_data_import), text = stringResource(id = R.string.prefs_data_import),
modifier = Modifier.weight(1f) modifier = Modifier.weight(1f)
) )
Spacer(modifier = Modifier.width(6.dp))
CardButton( CardButton(
onClick = { clearAllData() }, onClick = clearAllData,
text = stringResource(id = R.string.prefs_data_clear), text = stringResource(id = R.string.prefs_data_clear),
modifier = Modifier.weight(1f) modifier = Modifier.weight(1f)
) )
@@ -19,6 +19,7 @@ package com.rtbishop.look4sat.presentation.settings
import com.rtbishop.look4sat.domain.model.OtherSettings import com.rtbishop.look4sat.domain.model.OtherSettings
import com.rtbishop.look4sat.domain.model.RCSettings import com.rtbishop.look4sat.domain.model.RCSettings
import com.rtbishop.look4sat.domain.model.DataSourcesSettings
import com.rtbishop.look4sat.domain.predict.GeoPos import com.rtbishop.look4sat.domain.predict.GeoPos
import com.rtbishop.look4sat.domain.predict.OrbitalPass import com.rtbishop.look4sat.domain.predict.OrbitalPass
@@ -39,9 +40,11 @@ data class SettingsState(
val dataSettings: DataSettings, val dataSettings: DataSettings,
val otherSettings: OtherSettings, val otherSettings: OtherSettings,
val rcSettings: RCSettings, val rcSettings: RCSettings,
val dataSourcesSettings: DataSourcesSettings,
val sendAction: (SettingsAction) -> Unit, val sendAction: (SettingsAction) -> Unit,
val sendRCAction: (RCAction) -> Unit, val sendRCAction: (RCAction) -> Unit,
val sendSystemAction: (SystemAction) -> Unit val sendSystemAction: (SystemAction) -> Unit,
val sendDataSourcesAction: (DataSourcesAction) -> Unit
) )
sealed class SettingsAction { sealed class SettingsAction {
@@ -50,7 +53,8 @@ sealed class SettingsAction {
data class SetQthPosition(val locator: String) : SettingsAction() data class SetQthPosition(val locator: String) : SettingsAction()
data object DismissPosMessages : SettingsAction() data object DismissPosMessages : SettingsAction()
data object UpdateFromWeb : SettingsAction() data object UpdateFromWeb : SettingsAction()
data class UpdateFromFile(val uri: String) : SettingsAction() data class UpdateTLEFromFile(val uri: String) : SettingsAction()
data class UpdateTransceiversFromFile(val uri: String) : SettingsAction()
data object ClearAllData : SettingsAction() data object ClearAllData : SettingsAction()
data class ToggleUtc(val value: Boolean) : SettingsAction() data class ToggleUtc(val value: Boolean) : SettingsAction()
data class ToggleUpdate(val value: Boolean) : SettingsAction() data class ToggleUpdate(val value: Boolean) : SettingsAction()
@@ -72,3 +76,10 @@ sealed class RCAction {
data class SetBluetoothName(val value: String) : RCAction() data class SetBluetoothName(val value: String) : RCAction()
data class SetBluetoothAddress(val value: String) : RCAction() data class SetBluetoothAddress(val value: String) : RCAction()
} }
sealed class DataSourcesAction {
data class SetUseCustomTle(val value: Boolean) : DataSourcesAction()
data class SetUseCustomTransceivers(val value: Boolean) : DataSourcesAction()
data class SetTleUrl(val value: String) : DataSourcesAction()
data class SetTransceiversUrl(val value: String) : DataSourcesAction()
}
@@ -24,6 +24,7 @@ import androidx.lifecycle.viewmodel.initializer
import androidx.lifecycle.viewmodel.viewModelFactory import androidx.lifecycle.viewmodel.viewModelFactory
import com.rtbishop.look4sat.MainApplication import com.rtbishop.look4sat.MainApplication
import com.rtbishop.look4sat.R import com.rtbishop.look4sat.R
import com.rtbishop.look4sat.domain.model.DataSourcesSettings
import com.rtbishop.look4sat.domain.predict.OrbitalPass import com.rtbishop.look4sat.domain.predict.OrbitalPass
import com.rtbishop.look4sat.domain.repository.IDatabaseRepo import com.rtbishop.look4sat.domain.repository.IDatabaseRepo
import com.rtbishop.look4sat.domain.repository.ISatelliteRepo import com.rtbishop.look4sat.domain.repository.ISatelliteRepo
@@ -51,6 +52,12 @@ class SettingsViewModel(
private val defaultPosSettings = PositionSettings(false, settingsRepo.stationPosition.value, 0) private val defaultPosSettings = PositionSettings(false, settingsRepo.stationPosition.value, 0)
private val defaultDataSettings = DataSettings(false, 0, 0, 0L) private val defaultDataSettings = DataSettings(false, 0, 0, 0L)
private val defaultDataSourcesSettings = DataSourcesSettings(
useCustomTLE = false,
useCustomTransceivers = false,
tleUrl = settingsRepo.dataSourcesSettings.value.tleUrl,
transceiversUrl = settingsRepo.dataSourcesSettings.value.transceiversUrl
)
private val _uiState = MutableStateFlow( private val _uiState = MutableStateFlow(
SettingsState( SettingsState(
nextTime = "00:00:00", nextTime = "00:00:00",
@@ -61,9 +68,11 @@ class SettingsViewModel(
dataSettings = defaultDataSettings, dataSettings = defaultDataSettings,
otherSettings = settingsRepo.otherSettings.value, otherSettings = settingsRepo.otherSettings.value,
rcSettings = settingsRepo.rcSettings.value, rcSettings = settingsRepo.rcSettings.value,
dataSourcesSettings = defaultDataSourcesSettings,
sendAction = ::handleAction, sendAction = ::handleAction,
sendRCAction = ::handleAction, sendRCAction = ::handleAction,
sendSystemAction = ::handleAction sendSystemAction = ::handleAction,
sendDataSourcesAction = ::handleAction
) )
) )
private var processing: Job? = null private var processing: Job? = null
@@ -113,6 +122,11 @@ class SettingsViewModel(
_uiState.update { it.copy(otherSettings = settings) } _uiState.update { it.copy(otherSettings = settings) }
} }
} }
viewModelScope.launch {
settingsRepo.dataSourcesSettings.collect { settings ->
_uiState.update { it.copy(dataSourcesSettings = settings) }
}
}
} }
private fun handleAction(action: SettingsAction) { private fun handleAction(action: SettingsAction) {
@@ -122,7 +136,8 @@ class SettingsViewModel(
is SettingsAction.SetQthPosition -> setQthPosition(action.locator) is SettingsAction.SetQthPosition -> setQthPosition(action.locator)
SettingsAction.DismissPosMessages -> dismissPosMessage() SettingsAction.DismissPosMessages -> dismissPosMessage()
SettingsAction.UpdateFromWeb -> updateFromWeb() SettingsAction.UpdateFromWeb -> updateFromWeb()
is SettingsAction.UpdateFromFile -> updateFromFile(action.uri) is SettingsAction.UpdateTLEFromFile -> updateTLEFromFile(action.uri)
is SettingsAction.UpdateTransceiversFromFile -> updateTransceiversFromFile(action.uri)
SettingsAction.ClearAllData -> clearAllData() SettingsAction.ClearAllData -> clearAllData()
is SettingsAction.ToggleUtc -> settingsRepo.setStateOfUtc(action.value) is SettingsAction.ToggleUtc -> settingsRepo.setStateOfUtc(action.value)
is SettingsAction.ToggleUpdate -> settingsRepo.setStateOfAutoUpdate(action.value) is SettingsAction.ToggleUpdate -> settingsRepo.setStateOfAutoUpdate(action.value)
@@ -150,6 +165,15 @@ class SettingsViewModel(
} }
} }
private fun handleAction(action: DataSourcesAction) {
when (action) {
is DataSourcesAction.SetUseCustomTle -> settingsRepo.setUseCustomTle(action.value)
is DataSourcesAction.SetUseCustomTransceivers -> settingsRepo.setUseCustomTransceivers(action.value)
is DataSourcesAction.SetTleUrl -> settingsRepo.setTleUrl(action.value)
is DataSourcesAction.SetTransceiversUrl -> settingsRepo.setTransceiversUrl(action.value)
}
}
private fun setGpsPosition() { private fun setGpsPosition() {
if (settingsRepo.setStationPosition()) { if (settingsRepo.setStationPosition()) {
val messageResId = R.string.prefs_loc_success val messageResId = R.string.prefs_loc_success
@@ -217,11 +241,23 @@ class SettingsViewModel(
} }
} }
private fun updateFromFile(uri: String) = viewModelScope.launch { private fun updateTLEFromFile(uri: String) = viewModelScope.launch {
try { try {
val newDataSettings = _uiState.value.dataSettings.copy(isUpdating = true) val newDataSettings = _uiState.value.dataSettings.copy(isUpdating = true)
_uiState.update { it.copy(dataSettings = newDataSettings) } _uiState.update { it.copy(dataSettings = newDataSettings) }
databaseRepo.updateFromFile(uri) databaseRepo.updateTLEFromFile(uri)
} catch (exception: Exception) {
val newDataSettings = _uiState.value.dataSettings.copy(isUpdating = false)
_uiState.update { it.copy(dataSettings = newDataSettings) }
println(exception)
}
}
private fun updateTransceiversFromFile(uri: String) = viewModelScope.launch {
try {
val newDataSettings = _uiState.value.dataSettings.copy(isUpdating = true)
_uiState.update { it.copy(dataSettings = newDataSettings) }
databaseRepo.updateTransceiversFromFile(uri)
} catch (exception: Exception) { } catch (exception: Exception) {
val newDataSettings = _uiState.value.dataSettings.copy(isUpdating = false) val newDataSettings = _uiState.value.dataSettings.copy(isUpdating = false)
_uiState.update { it.copy(dataSettings = newDataSettings) } _uiState.update { it.copy(dataSettings = newDataSettings) }
+4
View File
@@ -108,6 +108,10 @@
<string name="prefs_data_clear_success">Datos eliminados con éxito</string> <string name="prefs_data_clear_success">Datos eliminados con éxito</string>
<string name="prefs_data_update_success">Actualización completada con éxito</string> <string name="prefs_data_update_success">Actualización completada con éxito</string>
<string name="prefs_data_sources_title">Custom data sources</string>
<string name="prefs_data_sources_tle_switch">Custom TLE URL</string>
<string name="prefs_data_sources_transceivers_switch">Custom transceivers URL</string>
<string name="prefs_net_title">Salida de datos de red</string> <string name="prefs_net_title">Salida de datos de red</string>
<string name="prefs_net_switch">Habilitar salida</string> <string name="prefs_net_switch">Habilitar salida</string>
<string name="prefs_net_ip_hint">Dirección IP</string> <string name="prefs_net_ip_hint">Dirección IP</string>
+7 -3
View File
@@ -93,10 +93,10 @@
<string name="prefs_loc_qth_error">Неправильный локатор QTH</string> <string name="prefs_loc_qth_error">Неправильный локатор QTH</string>
<string name="prefs_loc_success">Позиция успешно обновлена</string> <string name="prefs_loc_success">Позиция успешно обновлена</string>
<string name="prefs_station_title">Настройки местоположения</string> <string name="prefs_station_title">Настройки местоположения</string>
<string name="prefs_station_lat_text">Установите широту наземной станции</string> <string name="prefs_station_lat_text">Введите широту наземной станции</string>
<string name="prefs_station_lon_text">Установите долготу наземной станции</string> <string name="prefs_station_lon_text">Введите долготу наземной станции</string>
<string name="prefs_locator_title">Настройки локатора QTH</string> <string name="prefs_locator_title">Настройки локатора QTH</string>
<string name="prefs_locator_text">Установить позицию используя локатор</string> <string name="prefs_locator_text">Введите позицию через локатор</string>
<string name="prefs_data_title">Данные спутников</string> <string name="prefs_data_title">Данные спутников</string>
<string name="prefs_data_entries">Спутников: %s</string> <string name="prefs_data_entries">Спутников: %s</string>
@@ -107,6 +107,10 @@
<string name="prefs_data_clear_success">Очистка прошла успешно</string> <string name="prefs_data_clear_success">Очистка прошла успешно</string>
<string name="prefs_data_update_success">Обновление прошло успешно</string> <string name="prefs_data_update_success">Обновление прошло успешно</string>
<string name="prefs_data_sources_title">Свои источники данных</string>
<string name="prefs_data_sources_tle_switch">Custom TLE URL</string>
<string name="prefs_data_sources_transceivers_switch">Custom Transceivers URL</string>
<string name="prefs_net_title">Вывод сетевых данных</string> <string name="prefs_net_title">Вывод сетевых данных</string>
<string name="prefs_net_switch">Включить вывод</string> <string name="prefs_net_switch">Включить вывод</string>
<string name="prefs_net_ip_hint">IP адрес</string> <string name="prefs_net_ip_hint">IP адрес</string>
+4
View File
@@ -108,6 +108,10 @@
<string name="prefs_data_clear_success">දත්ත ඉවත් කිරීම සාර්ථකයි</string> <string name="prefs_data_clear_success">දත්ත ඉවත් කිරීම සාර්ථකයි</string>
<string name="prefs_data_update_success">යාවත්කාලීනය සම්පූර්ණ කිරීම සාර්ථකයි</string> <string name="prefs_data_update_success">යාවත්කාලීනය සම්පූර්ණ කිරීම සාර්ථකයි</string>
<string name="prefs_data_sources_title">Custom data sources</string>
<string name="prefs_data_sources_tle_switch">Custom TLE URL</string>
<string name="prefs_data_sources_transceivers_switch">Custom transceivers URL</string>
<string name="prefs_net_title">ජාල ප්‍රතිදානය</string> <string name="prefs_net_title">ජාල ප්‍රතිදානය</string>
<string name="prefs_net_switch">ප්‍රතිදානය සක්‍රීය කරන්න</string> <string name="prefs_net_switch">ප්‍රතිදානය සක්‍රීය කරන්න</string>
<string name="prefs_net_ip_hint">IP ලිපිනය</string> <string name="prefs_net_ip_hint">IP ලිපිනය</string>
+10 -6
View File
@@ -17,7 +17,7 @@
<string name="sat_type_title">Виберіть тип супутника</string> <string name="sat_type_title">Виберіть тип супутника</string>
<string name="sat_search_hint">Id - Назва</string> <string name="sat_search_hint">Id - Назва</string>
<string name="sat_search_clear">Очистити</string> <string name="sat_search_clear">Очистити</string>
<string name="sat_clear_all">Очистити</string> <string name="sat_clear_all">Очистити всі</string>
<string name="sat_select_all">Вибрати всі</string> <string name="sat_select_all">Вибрати всі</string>
<string name="sat_empty_list_message"> <string name="sat_empty_list_message">
Переконайтеся, що ваш пошуковий запит правильний, а база даних оновлена</string> Переконайтеся, що ваш пошуковий запит правильний, а база даних оновлена</string>
@@ -87,15 +87,15 @@
<string name="prefs_loc_title">Місцезнаходження</string> <string name="prefs_loc_title">Місцезнаходження</string>
<string name="prefs_loc_gps_title">GPS</string> <string name="prefs_loc_gps_title">GPS</string>
<string name="prefs_loc_gps_error">Перевірте дозволи на визначення місця</string> <string name="prefs_loc_gps_error">Перевірте дозвіл на використання геолокацї</string>
<string name="prefs_loc_input_title">Ввести</string> <string name="prefs_loc_input_title">Ввести</string>
<string name="prefs_loc_input_error">Введені некоректні дані</string> <string name="prefs_loc_input_error">Введено некоректну локацію</string>
<string name="prefs_loc_qth_title">QTH</string> <string name="prefs_loc_qth_title">QTH</string>
<string name="prefs_loc_qth_error">Некоректний локатор QTH</string> <string name="prefs_loc_qth_error">Некоректний локатор QTH</string>
<string name="prefs_loc_success">Позицію успішно оновлено</string> <string name="prefs_loc_success">Позицію успішно оновлено</string>
<string name="prefs_station_title">Налаштування місцезнаходження станції</string> <string name="prefs_station_title">Налаштування місцезнаходження станції</string>
<string name="prefs_station_lat_text">Задайте широту станції</string> <string name="prefs_station_lat_text">Широта місцезнаходження станції</string>
<string name="prefs_station_lon_text">Задайте довготу станції</string> <string name="prefs_station_lon_text">Довгота місцезнаходження станції</string>
<string name="prefs_locator_title">Налаштування QTH локатора</string> <string name="prefs_locator_title">Налаштування QTH локатора</string>
<string name="prefs_locator_text">Задати позицію за QTH локатором</string> <string name="prefs_locator_text">Задати позицію за QTH локатором</string>
@@ -108,6 +108,10 @@
<string name="prefs_data_clear_success">Дані успішно очищено</string> <string name="prefs_data_clear_success">Дані успішно очищено</string>
<string name="prefs_data_update_success">Оновлення успішне</string> <string name="prefs_data_update_success">Оновлення успішне</string>
<string name="prefs_data_sources_title">Custom data sources</string>
<string name="prefs_data_sources_tle_switch">Custom TLE URL</string>
<string name="prefs_data_sources_transceivers_switch">Custom transceivers URL</string>
<string name="prefs_net_title">Вивід мережевих даних</string> <string name="prefs_net_title">Вивід мережевих даних</string>
<string name="prefs_net_switch">Увімкнути вихід</string> <string name="prefs_net_switch">Увімкнути вихід</string>
<string name="prefs_net_ip_hint">IP адреса</string> <string name="prefs_net_ip_hint">IP адреса</string>
@@ -126,6 +130,6 @@
<string name="prefs_other_switch_sensors">Використовувати сенсори пристрою</string> <string name="prefs_other_switch_sensors">Використовувати сенсори пристрою</string>
<string name="prefs_outro_title">Я хотів би подякувати:</string> <string name="prefs_outro_title">Я хотів би подякувати:</string>
<string name="prefs_outro_license">Це ПЗ поставляється без жодних гарантій.</string> <string name="prefs_outro_license">Ця програма поставляється без жодних гарантій</string>
</resources> </resources>
+4
View File
@@ -100,6 +100,10 @@
<string name="prefs_data_clear_success">数据清空成功</string> <string name="prefs_data_clear_success">数据清空成功</string>
<string name="prefs_data_update_success">更新成功</string> <string name="prefs_data_update_success">更新成功</string>
<string name="prefs_data_sources_title">自定义数据源</string>
<string name="prefs_data_sources_tle_switch">自定义TLE URL</string>
<string name="prefs_data_sources_transceivers_switch">自定义收发器URL</string>
<string name="prefs_net_title">网络数据输出</string> <string name="prefs_net_title">网络数据输出</string>
<string name="prefs_net_switch">启用输出</string> <string name="prefs_net_switch">启用输出</string>
<string name="prefs_net_ip_hint">IP地址</string> <string name="prefs_net_ip_hint">IP地址</string>
+6 -1
View File
@@ -20,7 +20,7 @@
<string name="sat_search_clear">Clear</string> <string name="sat_search_clear">Clear</string>
<string name="sat_clear_all">Clear all</string> <string name="sat_clear_all">Clear all</string>
<string name="sat_select_all">Select all</string> <string name="sat_select_all">Select all</string>
<string name="sat_empty_list_message">" Make sure your search query is correct and the DB is updated"</string> <string name="sat_empty_list_message">"Make sure your search query is correct and the DB is updated"</string>
<string name="sat_warning_title">Warning\!</string> <string name="sat_warning_title">Warning\!</string>
<string name="sat_warning_message"> <string name="sat_warning_message">
There are over 9000 satellites listed in this app. There are over 9000 satellites listed in this app.
@@ -131,6 +131,11 @@
<string name="prefs_data_clear_success">Data was cleared successfully</string> <string name="prefs_data_clear_success">Data was cleared successfully</string>
<string name="prefs_data_update_success">Update completed successfully</string> <string name="prefs_data_update_success">Update completed successfully</string>
<string name="prefs_data_sources_title">Custom data sources</string>
<string name="prefs_data_sources_tle_switch">Custom TLE URL</string>
<string name="prefs_data_sources_transceivers_switch">Custom transceivers URL</string>
<string name="prefs_data_sources_url_title" translatable="false">URL (HTTPS)</string>
<string name="prefs_net_title">Network data output</string> <string name="prefs_net_title">Network data output</string>
<string name="prefs_net_switch">Enable output</string> <string name="prefs_net_switch">Enable output</string>
<string name="prefs_net_ip_hint">IP address</string> <string name="prefs_net_ip_hint">IP address</string>
+1 -1
View File
@@ -4,7 +4,7 @@ plugins {
} }
kotlin { kotlin {
jvmToolchain(21) jvmToolchain(17)
} }
android { android {
@@ -39,23 +39,46 @@ class DatabaseRepo(
private val settingsRepo: ISettingsRepo private val settingsRepo: ISettingsRepo
) : IDatabaseRepo { ) : IDatabaseRepo {
override suspend fun updateFromFile(uri: String) = withContext(dispatcher) { override suspend fun updateTLEFromFile(uri: String) = withContext(dispatcher) {
val importedSatellites = remoteSource.getFileStream(uri)?.let { dataParser.parseTLEStream(it) } val importedSatellites = remoteSource.getFileStream(uri)?.let { dataParser.parseTLEStream(it) }
importedSatellites?.let { localSource.insertEntries(it) } importedSatellites?.let { localSource.insertEntries(it) }
setUpdateSuccessful(System.currentTimeMillis()) setUpdateSuccessful(System.currentTimeMillis())
} }
override suspend fun updateTransceiversFromFile(uri: String) = withContext(dispatcher) {
val radios = remoteSource.getFileStream(uri)?.let { dataParser.parseJSONStream(it) }
radios?.let {
if(it.isNotEmpty()) {
localSource.deleteRadios()
localSource.insertRadios(it)
}
}
setUpdateSuccessful(System.currentTimeMillis())
}
override suspend fun updateFromRemote() = withContext(dispatcher) { override suspend fun updateFromRemote() = withContext(dispatcher) {
val importedEntries = mutableListOf<OrbitalData>() val importedEntries = mutableListOf<OrbitalData>()
val importedRadios = mutableListOf<SatRadio>() val importedRadios = mutableListOf<SatRadio>()
// fetch val tleUrls = Sources.satelliteDataUrls.toMutableMap().apply {
val jobsMap = Sources.satelliteDataUrls.mapValues { async { remoteSource.getNetworkStream(it.value) } } if (settingsRepo.dataSourcesSettings.value.useCustomTLE) {
val jobRadios = async { remoteSource.getNetworkStream(Sources.RADIO_DATA_URL) } this["Other"] = settingsRepo.dataSourcesSettings.value.tleUrl
}
}
val jobsMap = tleUrls
.filter { (_, url) -> url.isNotEmpty() }
.mapValues { (_, url) -> async { remoteSource.getNetworkStream(url) } }
val radioUrls = buildList {
add(Sources.RADIO_DATA_URL)
if (settingsRepo.dataSourcesSettings.value.useCustomTransceivers) {
add(settingsRepo.dataSourcesSettings.value.transceiversUrl)
}
}
val jobRadios = radioUrls.associateWith { url -> async { remoteSource.getNetworkStream(url) } }
// parse // parse
jobsMap.mapValues { job -> job.value.await() }.forEach { entry -> jobsMap.mapValues { job -> job.value.await() }.forEach { entry ->
entry.value?.let { stream -> entry.value?.let { stream ->
when (val type = entry.key) { when (val type = entry.key) {
"Amsat", "R4UAB" -> { "Amsat", "R4UAB", "Other" -> {
// parse tle stream // parse tle stream
val satellites = dataParser.parseTLEStream(stream) val satellites = dataParser.parseTLEStream(stream)
val catnums = satellites.map { it.catnum } val catnums = satellites.map { it.catnum }
@@ -82,7 +105,11 @@ class DatabaseRepo(
} }
} }
} }
jobRadios.await()?.let { importedRadios.addAll(dataParser.parseJSONStream(it)) } jobRadios.values.forEach { job ->
job.await()?.let {
importedRadios.addAll(dataParser.parseJSONStream(it))
}
}
// insert // insert
localSource.insertEntries(importedEntries) localSource.insertEntries(importedEntries)
localSource.insertRadios(importedRadios) localSource.insertRadios(importedRadios)
@@ -27,6 +27,7 @@ import com.rtbishop.look4sat.domain.model.DatabaseState
import com.rtbishop.look4sat.domain.model.OtherSettings import com.rtbishop.look4sat.domain.model.OtherSettings
import com.rtbishop.look4sat.domain.model.PassesSettings import com.rtbishop.look4sat.domain.model.PassesSettings
import com.rtbishop.look4sat.domain.model.RCSettings import com.rtbishop.look4sat.domain.model.RCSettings
import com.rtbishop.look4sat.domain.model.DataSourcesSettings
import com.rtbishop.look4sat.domain.predict.GeoPos import com.rtbishop.look4sat.domain.predict.GeoPos
import com.rtbishop.look4sat.domain.repository.ISettingsRepo import com.rtbishop.look4sat.domain.repository.ISettingsRepo
import com.rtbishop.look4sat.domain.utility.positionToQth import com.rtbishop.look4sat.domain.utility.positionToQth
@@ -68,6 +69,10 @@ class SettingsRepo(
private val keyUpdateTimestamp = "updateTimestamp" private val keyUpdateTimestamp = "updateTimestamp"
private val keyShouldSeeWarning = "shouldSeeWarning" private val keyShouldSeeWarning = "shouldSeeWarning"
private val keyShouldSeeWelcome = "shouldSeeWelcome" private val keyShouldSeeWelcome = "shouldSeeWelcome"
private val keyUseCustomTle = "useCustomTle"
private val keyUseCustomTransceivers = "useCustomTransceivers"
private val keyTleUrl = "tleUrl"
private val keyTransceiversUrl = "transceiversUrl"
private val separatorComma = "," private val separatorComma = ","
//region # Satellites selection settings //region # Satellites selection settings
@@ -330,4 +335,36 @@ class SettingsRepo(
shouldSeeWelcome = preferences.getBoolean(keyShouldSeeWelcome, true) shouldSeeWelcome = preferences.getBoolean(keyShouldSeeWelcome, true)
) )
//endregion //endregion
//region # Data sources settings
private val _dataSourcesSettings = MutableStateFlow(getDataSourcesSettings())
override val dataSourcesSettings: StateFlow<DataSourcesSettings> = _dataSourcesSettings
override fun setUseCustomTle(value: Boolean) {
preferences.edit { putBoolean(keyUseCustomTle, value) }
_dataSourcesSettings.update { it.copy(useCustomTLE = value) }
}
override fun setUseCustomTransceivers(value: Boolean) {
preferences.edit { putBoolean(keyUseCustomTransceivers, value) }
_dataSourcesSettings.update { it.copy(useCustomTransceivers = value) }
}
override fun setTleUrl(value: String) {
preferences.edit { putString(keyTleUrl, value) }
_dataSourcesSettings.update { it.copy(tleUrl = value) }
}
override fun setTransceiversUrl(value: String) {
preferences.edit { putString(keyTransceiversUrl, value) }
_dataSourcesSettings.update { it.copy(transceiversUrl = value) }
}
private fun getDataSourcesSettings(): DataSourcesSettings = DataSourcesSettings(
useCustomTLE = preferences.getBoolean(keyUseCustomTle, false),
useCustomTransceivers = preferences.getBoolean(keyUseCustomTransceivers, false),
tleUrl = preferences.getString(keyTleUrl, "https://example.com/tle.txt") ?: "",
transceiversUrl = preferences.getString(keyTransceiversUrl, "https://example.com/radio.json") ?: ""
)
//endregion
} }
+1 -1
View File
@@ -3,7 +3,7 @@ plugins {
} }
kotlin { kotlin {
jvmToolchain(21) jvmToolchain(17)
} }
dependencies { dependencies {
@@ -48,3 +48,10 @@ data class OtherSettings(
val shouldSeeWarning: Boolean, val shouldSeeWarning: Boolean,
val shouldSeeWelcome: Boolean val shouldSeeWelcome: Boolean
) )
data class DataSourcesSettings(
val useCustomTLE: Boolean,
val useCustomTransceivers: Boolean,
val tleUrl: String,
val transceiversUrl: String
)
@@ -18,7 +18,8 @@
package com.rtbishop.look4sat.domain.repository package com.rtbishop.look4sat.domain.repository
interface IDatabaseRepo { interface IDatabaseRepo {
suspend fun updateFromFile(uri: String) suspend fun updateTLEFromFile(uri: String)
suspend fun updateTransceiversFromFile(uri: String)
suspend fun updateFromRemote() suspend fun updateFromRemote()
suspend fun clearAllData() suspend fun clearAllData()
} }
@@ -21,6 +21,7 @@ import com.rtbishop.look4sat.domain.model.DatabaseState
import com.rtbishop.look4sat.domain.model.OtherSettings import com.rtbishop.look4sat.domain.model.OtherSettings
import com.rtbishop.look4sat.domain.model.PassesSettings import com.rtbishop.look4sat.domain.model.PassesSettings
import com.rtbishop.look4sat.domain.model.RCSettings import com.rtbishop.look4sat.domain.model.RCSettings
import com.rtbishop.look4sat.domain.model.DataSourcesSettings
import com.rtbishop.look4sat.domain.predict.GeoPos import com.rtbishop.look4sat.domain.predict.GeoPos
import kotlinx.coroutines.flow.StateFlow import kotlinx.coroutines.flow.StateFlow
@@ -73,4 +74,12 @@ interface ISettingsRepo {
fun setWarningDismissed() fun setWarningDismissed()
fun setWelcomeDismissed() fun setWelcomeDismissed()
//endregion //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
} }
@@ -46,6 +46,7 @@ object Sources {
"Swarm" to "https://celestrak.org/NORAD/elements/gp.php?GROUP=swarm&FORMAT=csv", "Swarm" to "https://celestrak.org/NORAD/elements/gp.php?GROUP=swarm&FORMAT=csv",
"Weather" to "https://celestrak.org/NORAD/elements/gp.php?GROUP=weather&FORMAT=csv", "Weather" to "https://celestrak.org/NORAD/elements/gp.php?GROUP=weather&FORMAT=csv",
"X-Comm" to "https://celestrak.org/NORAD/elements/gp.php?GROUP=x-comm&FORMAT=csv", "X-Comm" to "https://celestrak.org/NORAD/elements/gp.php?GROUP=x-comm&FORMAT=csv",
"Amsat" to "https://amsat.org/tle/current/nasabare.txt" "Amsat" to "https://amsat.org/tle/current/nasabare.txt",
"Other" to "" // key for sats filter
) )
} }
@@ -1,9 +1,2 @@
Added support for landscape layout and tablets Added custom data source translations, by troilus #191
Added AOS/LOS info to the countdown component Added keepScreenOn for RadarScreen, by Kaitlyn Kenwell #193
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
@@ -1,9 +1,2 @@
Added support for landscape layout and tablets Added custom data source translations, by troilus #191
Added AOS/LOS info to the countdown component Added keepScreenOn for RadarScreen, by Kaitlyn Kenwell #193
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
+2 -2
View File
@@ -1,7 +1,7 @@
[versions] [versions]
android-gradle-plugin = "9.0.0" android-gradle-plugin = "9.0.0"
google-ksp = "2.3.2" google-ksp = "2.3.2"
kotlin = "2.3.0" kotlin = "2.3.10"
androidx-core-ktx = "1.17.0" androidx-core-ktx = "1.17.0"
androidx-core-splashscreen = "1.2.0" androidx-core-splashscreen = "1.2.0"
@@ -34,7 +34,7 @@ compose-animation = { group = "androidx.compose.animation", name = "animation" }
compose-material3 = { group = "androidx.compose.material3", name = "material3" } compose-material3 = { group = "androidx.compose.material3", name = "material3" }
compose-material3-navigation = { group = "androidx.compose.material3", name = "material3-adaptive-navigation-suite" } compose-material3-navigation = { group = "androidx.compose.material3", name = "material3-adaptive-navigation-suite" }
compose-runtime = { group = "androidx.compose.runtime", name = "runtime" } compose-runtime = { group = "androidx.compose.runtime", name = "runtime" }
compose-tooling = { group = "androidx.compose.ui", name = "ui-tooling" } compose-tooling = { group = "androidx.compose.ui", name = "ui-tooling-preview" }
compose-activity = { module = "androidx.activity:activity-compose", version.ref = "compose-activity" } compose-activity = { module = "androidx.activity:activity-compose", version.ref = "compose-activity" }
compose-lifecycle = { module = "androidx.lifecycle:lifecycle-runtime-compose", version.ref = "compose-lifecycle" } compose-lifecycle = { module = "androidx.lifecycle:lifecycle-runtime-compose", version.ref = "compose-lifecycle" }