mirror of
https://github.com/atsunatsu/Look4Sat.git
synced 2026-10-02 19:17:21 +00:00
Compare commits
1
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
a8c48e33a0 |
No files matched your search
@@ -37,7 +37,6 @@ import kotlinx.coroutines.flow.StateFlow
|
||||
import kotlinx.coroutines.flow.update
|
||||
import kotlinx.coroutines.withContext
|
||||
import java.util.TimeZone
|
||||
import kotlin.time.Duration.Companion.milliseconds
|
||||
|
||||
class SatelliteRepo(
|
||||
private val dispatcher: CoroutineDispatcher,
|
||||
@@ -159,7 +158,7 @@ class SatelliteRepo(
|
||||
}
|
||||
}
|
||||
newPasses.sortBy { it.aosTime }
|
||||
delay(1000.milliseconds) // Simulate loading time for better UX
|
||||
delay(1000) // Simulate loading time for better UX
|
||||
_passes.update { newPasses }
|
||||
}
|
||||
_isCalculating.value = false
|
||||
@@ -177,7 +176,7 @@ class SatelliteRepo(
|
||||
val inRange = if (aosStartMinute <= aosEndMinute) {
|
||||
aosMinute in aosStartMinute..aosEndMinute
|
||||
} else {
|
||||
aosMinute !in (aosEndMinute + 1)..<aosStartMinute
|
||||
aosMinute >= aosStartMinute || aosMinute <= aosEndMinute
|
||||
}
|
||||
return if (invertAosTimeWindow) !inRange else inRange
|
||||
}
|
||||
|
||||
@@ -38,16 +38,16 @@ class SelectionRepo(
|
||||
) : ISelectionRepo {
|
||||
|
||||
private val currentItems = MutableStateFlow<List<SatItem>>(emptyList())
|
||||
private val currentModes = MutableStateFlow(settingsRepo.selectedSatModes.value)
|
||||
private val currentQuery = MutableStateFlow("")
|
||||
|
||||
// Resolve sat IDs once when modes change, then filter items reactively.
|
||||
// The HashSet gives O(1) catnum lookups instead of O(n) with a List.
|
||||
// Directly observe settingsRepo.selectedSatModes to ensure real-time sync across screens.
|
||||
private val itemsWithModes = settingsRepo.selectedSatModes.flatMapLatest { list: List<String> ->
|
||||
val catnumSet: Set<Int>? = if (list.isEmpty()) {
|
||||
private val itemsWithModes = currentModes.flatMapLatest { modes: List<String> ->
|
||||
val catnumSet: Set<Int>? = if (modes.isEmpty()) {
|
||||
null // null = no filtering
|
||||
} else {
|
||||
val ids = localSource.getIdsWithModes(list)
|
||||
val ids = localSource.getIdsWithModes(modes)
|
||||
if (ids.isEmpty()) null else ids.toHashSet()
|
||||
}
|
||||
currentItems.map { items ->
|
||||
@@ -65,7 +65,7 @@ class SelectionRepo(
|
||||
}
|
||||
}
|
||||
|
||||
override fun getCurrentModes() = settingsRepo.selectedSatModes.value
|
||||
override fun getCurrentModes() = currentModes.value
|
||||
|
||||
override fun getModesList() = Sources.satelliteModes
|
||||
|
||||
@@ -78,6 +78,7 @@ class SelectionRepo(
|
||||
}
|
||||
|
||||
override suspend fun setModes(modes: List<String>) {
|
||||
currentModes.value = modes
|
||||
settingsRepo.setSelectedSatModes(modes)
|
||||
}
|
||||
|
||||
|
||||
@@ -86,8 +86,6 @@ class SettingsRepo(
|
||||
private val keySstvMode = "sstvMode"
|
||||
private val keyLowElevation = "lowElevation"
|
||||
private val keyHighElevation = "highElevation"
|
||||
private val keyRadarCompassOffset = "radarCompassOffset"
|
||||
private val keyRadarCompassOffsetElev = "radarCompassOffsetElev"
|
||||
private val keyUseCustomTle = "useCustomTle"
|
||||
private val keyUseCustomTransceivers = "useCustomTransceivers"
|
||||
private val keyTleUrl = "tleUrl"
|
||||
@@ -334,8 +332,6 @@ class SettingsRepo(
|
||||
putString(keySstvMode, new.sstvMode)
|
||||
putLong(keyLowElevation, new.lowElevation.toRawBits())
|
||||
putLong(keyHighElevation, new.highElevation.toRawBits())
|
||||
putFloat(keyRadarCompassOffset, new.radarCompassOffset)
|
||||
putFloat(keyRadarCompassOffsetElev, new.radarCompassOffsetElev)
|
||||
}
|
||||
new
|
||||
}
|
||||
@@ -352,9 +348,7 @@ class SettingsRepo(
|
||||
shouldSeeWhatsNew = preferences.getBoolean(keyShouldSeeWhatsNew, true),
|
||||
sstvMode = preferences.getString(keySstvMode, null) ?: "Auto",
|
||||
lowElevation = Double.fromBits(preferences.getLong(keyLowElevation, 15.0.toRawBits())),
|
||||
highElevation = Double.fromBits(preferences.getLong(keyHighElevation, 45.0.toRawBits())),
|
||||
radarCompassOffset = preferences.getFloat(keyRadarCompassOffset, 0f),
|
||||
radarCompassOffsetElev = preferences.getFloat(keyRadarCompassOffsetElev, 0f)
|
||||
highElevation = Double.fromBits(preferences.getLong(keyHighElevation, 45.0.toRawBits()))
|
||||
)
|
||||
//endregion
|
||||
|
||||
|
||||
@@ -24,6 +24,7 @@ import kotlinx.coroutines.CoroutineDispatcher
|
||||
import kotlinx.coroutines.withContext
|
||||
import okhttp3.OkHttpClient
|
||||
import okhttp3.Request
|
||||
import java.io.ByteArrayInputStream
|
||||
import java.io.InputStream
|
||||
|
||||
class RemoteSource(
|
||||
@@ -45,14 +46,10 @@ class RemoteSource(
|
||||
override suspend fun getNetworkStream(url: String): InputStream? = withContext(dispatcher) {
|
||||
try {
|
||||
val networkRequest = Request.Builder().url(url).build()
|
||||
val response = httpClient.newCall(networkRequest).execute()
|
||||
if (!response.isSuccessful) {
|
||||
response.close()
|
||||
return@withContext null
|
||||
httpClient.newCall(networkRequest).execute().use { response ->
|
||||
if (!response.isSuccessful) return@withContext null
|
||||
ByteArrayInputStream(response.body.bytes())
|
||||
}
|
||||
// Return the body stream directly as the caller is responsible for closing it
|
||||
// That returns the connection to OkHttp's pool
|
||||
response.body.byteStream().buffered()
|
||||
} catch (exception: Exception) {
|
||||
println("RemoteSource network stream exception: $exception")
|
||||
null
|
||||
|
||||
@@ -61,9 +61,7 @@ data class OtherSettings(
|
||||
val shouldSeeWhatsNew: Boolean,
|
||||
val sstvMode: String = "Auto",
|
||||
val lowElevation: Double = 15.0,
|
||||
val highElevation: Double = 45.0,
|
||||
val radarCompassOffset: Float = 0f,
|
||||
val radarCompassOffsetElev: Float = 0f
|
||||
val highElevation: Double = 45.0
|
||||
)
|
||||
|
||||
data class DataSourcesSettings(
|
||||
|
||||
@@ -19,10 +19,10 @@ package com.rtbishop.look4sat.core.domain.source
|
||||
|
||||
object Sources {
|
||||
val satelliteDataUrls = mapOf(
|
||||
"CelesTrak" to "https://celestrak.org/NORAD/elements/gp.php?GROUP=active&FORMAT=csv",
|
||||
"AMSAT" to "https://amsat.org/tle/current/nasabare.txt",
|
||||
"McCants Classified" to "https://www.mmccants.org/tles/classfd.zip",
|
||||
"McCants Integrated" to "https://www.mmccants.org/tles/inttles.zip",
|
||||
"All" to "https://celestrak.org/NORAD/elements/gp.php?GROUP=active&FORMAT=csv",
|
||||
"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",
|
||||
"SatNOGS" to "https://db.satnogs.org/api/tle/?format=3le",
|
||||
"ARISS" to "https://live.ariss.org/iss.txt",
|
||||
|
||||
@@ -91,7 +91,7 @@ class DataParser(private val dispatcher: CoroutineDispatcher) {
|
||||
val line1 = tle[1]
|
||||
val line2 = tle[2]
|
||||
OrbitalData(
|
||||
name = tle[0].trim().removePrefix("0 "),
|
||||
name = tle[0].trim(),
|
||||
epoch = line1.substring(18, 32).toDouble(),
|
||||
meanmo = line2.substring(52, 63).toDouble(),
|
||||
eccn = line2.substring(26, 33).toDouble() / 1e7,
|
||||
|
||||
-122
@@ -1,122 +0,0 @@
|
||||
/*
|
||||
* 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.
|
||||
*/
|
||||
package com.rtbishop.look4sat.core.domain.utility
|
||||
|
||||
import com.rtbishop.look4sat.core.domain.model.SatRadio
|
||||
import com.rtbishop.look4sat.core.domain.predict.OrbitalPos
|
||||
import java.util.Locale
|
||||
|
||||
/**
|
||||
* Computes Doppler-corrected reciprocal frequencies for linear transponders.
|
||||
*
|
||||
* For a linear (passband) transponder, uplink and downlink frequencies are
|
||||
* related by a fixed passband offset. When the satellite moves, both are
|
||||
* Doppler-shifted. Given one, this computes the other:
|
||||
*
|
||||
* downlink → uplink: mapDownlinkToUplink (passband) → getUplinkFreq (Doppler)
|
||||
* uplink → downlink: mapUplinkToDownlink (passband) → getDownlinkFreq (Doppler)
|
||||
*/
|
||||
object DopplerFrequencyCalculator {
|
||||
|
||||
/**
|
||||
* Given a downlink frequency, compute the Doppler-corrected uplink frequency.
|
||||
* Returns null if the transponder is not a linear passband type.
|
||||
*/
|
||||
fun computeUplinkFromDownlink(
|
||||
downlinkHz: Long,
|
||||
transponder: SatRadio,
|
||||
orbitalPos: OrbitalPos
|
||||
): Long? {
|
||||
if (!isLinearTransponder(transponder)) return null
|
||||
val baseUplink = TransponderMapper.mapDownlinkToUplink(downlinkHz, transponder) ?: return null
|
||||
return orbitalPos.getUplinkFreq(baseUplink)
|
||||
}
|
||||
|
||||
/**
|
||||
* Given a downlink frequency, compute the Doppler-corrected uplink frequency
|
||||
* with an offset applied to the downlink (in Hz).
|
||||
* Returns null if the transponder is not a linear passband type.
|
||||
*
|
||||
* The user-entered downlink frequency already includes the offset, so subtract
|
||||
* it before mapping the downlink passband position back to the uplink.
|
||||
*/
|
||||
fun computeUplinkFromDownlinkWithOffset(
|
||||
downlinkHz: Long,
|
||||
transponder: SatRadio,
|
||||
orbitalPos: OrbitalPos,
|
||||
offsetHz: Long
|
||||
): Long? {
|
||||
if (!isLinearTransponder(transponder)) return null
|
||||
val baseUplink = TransponderMapper.mapDownlinkToUplink(downlinkHz - offsetHz, transponder) ?: return null
|
||||
return orbitalPos.getUplinkFreq(baseUplink)
|
||||
}
|
||||
|
||||
/**
|
||||
* Given an uplink frequency, compute the Doppler-corrected downlink frequency.
|
||||
* Returns null if the transponder is not a linear passband type.
|
||||
*/
|
||||
fun computeDownlinkFromUplink(
|
||||
uplinkHz: Long,
|
||||
transponder: SatRadio,
|
||||
orbitalPos: OrbitalPos
|
||||
): Long? {
|
||||
if (!isLinearTransponder(transponder)) return null
|
||||
val baseDownlink = TransponderMapper.mapUplinkToDownlink(uplinkHz, transponder) ?: return null
|
||||
return orbitalPos.getDownlinkFreq(baseDownlink)
|
||||
}
|
||||
|
||||
/**
|
||||
* Given an uplink frequency, compute the Doppler-corrected downlink frequency
|
||||
* with an offset applied to the downlink (in Hz).
|
||||
* Returns null if the transponder is not a linear passband type.
|
||||
*/
|
||||
fun computeDownlinkFromUplinkWithOffset(
|
||||
uplinkHz: Long,
|
||||
transponder: SatRadio,
|
||||
orbitalPos: OrbitalPos,
|
||||
offsetHz: Long
|
||||
): Long? {
|
||||
if (!isLinearTransponder(transponder)) return null
|
||||
val baseDownlink = TransponderMapper.mapUplinkToDownlink(uplinkHz, transponder) ?: return null
|
||||
return orbitalPos.getDownlinkFreq(baseDownlink + offsetHz)
|
||||
}
|
||||
|
||||
/** True if this transponder supports linear passband mapping. */
|
||||
fun isLinearTransponder(transponder: SatRadio): Boolean {
|
||||
val upLow = transponder.uplinkLow
|
||||
val upHigh = transponder.uplinkHigh
|
||||
val downLow = transponder.downlinkLow
|
||||
val downHigh = transponder.downlinkHigh
|
||||
return upLow != null && upHigh != null && downLow != null && downHigh != null
|
||||
&& upLow != upHigh && downLow != downHigh
|
||||
}
|
||||
|
||||
/**
|
||||
* True for the radio entry that should drive the standalone Calculator page.
|
||||
*
|
||||
* A frequency range alone is not enough: some non-user-facing or drifting data entries
|
||||
* can also have low/high frequencies. The calculator is meant for named linear
|
||||
* transponders, e.g. "Linear Transponder", "Linear Transp.", "SSB Transponder".
|
||||
*/
|
||||
fun isNamedLinearTransponder(transponder: SatRadio): Boolean {
|
||||
if (!isLinearTransponder(transponder)) return false
|
||||
|
||||
val info = transponder.info.lowercase(Locale.ENGLISH)
|
||||
val modes = listOfNotNull(transponder.downlinkMode, transponder.uplinkMode)
|
||||
.joinToString(separator = " ")
|
||||
.lowercase(Locale.ENGLISH)
|
||||
val hasLinearName = info.contains("linear")
|
||||
val hasTransponderName = info.contains("transponder") || info.contains("transp") ||
|
||||
info.contains("xponder") || info.contains("xpdr")
|
||||
val hasLinearMode = listOf("ssb", "usb", "lsb", "cw").any { modes.contains(it) }
|
||||
|
||||
return (hasLinearName && hasTransponderName) || (hasTransponderName && hasLinearMode)
|
||||
}
|
||||
}
|
||||
-190
@@ -1,190 +0,0 @@
|
||||
package com.rtbishop.look4sat.core.domain
|
||||
|
||||
import com.rtbishop.look4sat.core.domain.model.SatRadio
|
||||
import com.rtbishop.look4sat.core.domain.predict.OrbitalPos
|
||||
import com.rtbishop.look4sat.core.domain.utility.DopplerFrequencyCalculator
|
||||
import org.junit.Assert.assertEquals
|
||||
import org.junit.Assert.assertFalse
|
||||
import org.junit.Assert.assertNotNull
|
||||
import org.junit.Assert.assertNull
|
||||
import org.junit.Assert.assertTrue
|
||||
import org.junit.Test
|
||||
|
||||
class DopplerFrequencyCalculatorTest {
|
||||
|
||||
private fun linearTransponder(
|
||||
upLow: Long = 145_000_000L,
|
||||
upHigh: Long = 145_500_000L,
|
||||
downLow: Long = 435_000_000L,
|
||||
downHigh: Long? = 435_500_000L,
|
||||
inverted: Boolean = false,
|
||||
info: String = "Linear Transponder",
|
||||
downlinkMode: String? = "USB",
|
||||
uplinkMode: String? = "LSB"
|
||||
) = SatRadio(
|
||||
uuid = "linear", info = info, isAlive = true,
|
||||
downlinkLow = downLow, downlinkHigh = downHigh,
|
||||
downlinkMode = downlinkMode, uplinkLow = upLow, uplinkHigh = upHigh,
|
||||
uplinkMode = uplinkMode, isInverted = inverted, catnum = 12345
|
||||
)
|
||||
|
||||
private fun fmTransponder() = SatRadio(
|
||||
uuid = "fm", info = "FM Repeater", isAlive = true,
|
||||
downlinkLow = 435_600_000L, downlinkHigh = null,
|
||||
downlinkMode = "FM", uplinkLow = 145_900_000L, uplinkHigh = null,
|
||||
uplinkMode = "FM", isInverted = false, catnum = 99999
|
||||
)
|
||||
|
||||
private fun pos(distanceRateKmS: Double = 0.0) = OrbitalPos().apply {
|
||||
this.distanceRate = distanceRateKmS
|
||||
}
|
||||
|
||||
@Test
|
||||
fun isLinearTransponder_returnsTrueForLinear() {
|
||||
assertTrue(DopplerFrequencyCalculator.isLinearTransponder(linearTransponder()))
|
||||
}
|
||||
|
||||
@Test
|
||||
fun isLinearTransponder_returnsFalseForFM() {
|
||||
assertFalse(DopplerFrequencyCalculator.isLinearTransponder(fmTransponder()))
|
||||
}
|
||||
|
||||
@Test
|
||||
fun isLinearTransponder_returnsFalseForNullDownlinkHigh() {
|
||||
val xpdr = linearTransponder(downHigh = null)
|
||||
assertFalse(DopplerFrequencyCalculator.isLinearTransponder(xpdr))
|
||||
}
|
||||
|
||||
@Test
|
||||
fun isNamedLinearTransponder_returnsTrueForLinearTransponderName() {
|
||||
assertTrue(DopplerFrequencyCalculator.isNamedLinearTransponder(linearTransponder()))
|
||||
}
|
||||
|
||||
@Test
|
||||
fun isNamedLinearTransponder_returnsTrueForSsbTransponderName() {
|
||||
val xpdr = linearTransponder(info = "Mode V/U SSB Transponder", downlinkMode = "USB", uplinkMode = "LSB")
|
||||
assertTrue(DopplerFrequencyCalculator.isNamedLinearTransponder(xpdr))
|
||||
}
|
||||
|
||||
@Test
|
||||
fun isNamedLinearTransponder_returnsFalseForRangeEntryWithoutTransponderName() {
|
||||
val driftingRangeEntry = linearTransponder(info = "Upper side band (drifting)")
|
||||
assertFalse(DopplerFrequencyCalculator.isNamedLinearTransponder(driftingRangeEntry))
|
||||
}
|
||||
|
||||
@Test
|
||||
fun isNamedLinearTransponder_returnsFalseForFmRepeater() {
|
||||
assertFalse(DopplerFrequencyCalculator.isNamedLinearTransponder(fmTransponder()))
|
||||
}
|
||||
|
||||
@Test
|
||||
fun computeUplinkFromDownlink_linear_noDoppler() {
|
||||
val xpdr = linearTransponder()
|
||||
val orbitalPos = pos(0.0)
|
||||
val uplink = DopplerFrequencyCalculator.computeUplinkFromDownlink(435_200_000L, xpdr, orbitalPos)
|
||||
assertNotNull(uplink)
|
||||
assertEquals(145_200_000L, uplink)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun computeDownlinkFromUplink_linear_noDoppler() {
|
||||
val xpdr = linearTransponder()
|
||||
val orbitalPos = pos(0.0)
|
||||
val downlink = DopplerFrequencyCalculator.computeDownlinkFromUplink(145_200_000L, xpdr, orbitalPos)
|
||||
assertNotNull(downlink)
|
||||
assertEquals(435_200_000L, downlink)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun computeUplinkFromDownlink_withDoppler_positiveRangeRate() {
|
||||
// Satellite receding (positive range rate) → ground must transmit higher freq to compensate.
|
||||
val xpdr = linearTransponder()
|
||||
val orbitalPos = pos(7.0)
|
||||
val uplink = DopplerFrequencyCalculator.computeUplinkFromDownlink(435_200_000L, xpdr, orbitalPos)
|
||||
assertNotNull(uplink)
|
||||
assertTrue(uplink!! > 145_200_000L)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun computeUplinkFromDownlink_fmTransponder_returnsNull() {
|
||||
val orbitalPos = pos()
|
||||
val result = DopplerFrequencyCalculator.computeUplinkFromDownlink(435_600_000L, fmTransponder(), orbitalPos)
|
||||
assertNull(result)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun computeDownlinkFromUplink_fmTransponder_returnsNull() {
|
||||
val orbitalPos = pos()
|
||||
val result = DopplerFrequencyCalculator.computeDownlinkFromUplink(145_900_000L, fmTransponder(), orbitalPos)
|
||||
assertNull(result)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun computeDownlinkFromUplink_withPositiveOffset_addsOffsetToDownlink() {
|
||||
val xpdr = linearTransponder()
|
||||
val orbitalPos = pos(0.0)
|
||||
val downlink = DopplerFrequencyCalculator.computeDownlinkFromUplinkWithOffset(
|
||||
uplinkHz = 145_200_000L,
|
||||
transponder = xpdr,
|
||||
orbitalPos = orbitalPos,
|
||||
offsetHz = 2_500L
|
||||
)
|
||||
assertEquals(435_202_500L, downlink)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun computeUplinkFromDownlink_withPositiveOffset_subtractsOffsetBeforeMapping() {
|
||||
val xpdr = linearTransponder()
|
||||
val orbitalPos = pos(0.0)
|
||||
val uplink = DopplerFrequencyCalculator.computeUplinkFromDownlinkWithOffset(
|
||||
downlinkHz = 435_202_500L,
|
||||
transponder = xpdr,
|
||||
orbitalPos = orbitalPos,
|
||||
offsetHz = 2_500L
|
||||
)
|
||||
assertEquals(145_200_000L, uplink)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun computeOffsetRoundTrip_handlesNegativeOffset() {
|
||||
val xpdr = linearTransponder()
|
||||
val orbitalPos = pos(0.0)
|
||||
val downlink = DopplerFrequencyCalculator.computeDownlinkFromUplinkWithOffset(
|
||||
uplinkHz = 145_200_000L,
|
||||
transponder = xpdr,
|
||||
orbitalPos = orbitalPos,
|
||||
offsetHz = -2_500L
|
||||
)
|
||||
assertEquals(435_197_500L, downlink)
|
||||
|
||||
val uplink = DopplerFrequencyCalculator.computeUplinkFromDownlinkWithOffset(
|
||||
downlinkHz = downlink!!,
|
||||
transponder = xpdr,
|
||||
orbitalPos = orbitalPos,
|
||||
offsetHz = -2_500L
|
||||
)
|
||||
assertEquals(145_200_000L, uplink)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun computeUplinkFromDownlink_invertedTransponder() {
|
||||
val xpdr = linearTransponder(inverted = true, downHigh = 435_500_000L)
|
||||
val orbitalPos = pos(0.0)
|
||||
val uplink = DopplerFrequencyCalculator.computeUplinkFromDownlink(435_200_000L, xpdr, orbitalPos)
|
||||
assertNotNull(uplink)
|
||||
assertEquals(145_300_000L, uplink)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun computeUplinkFromDownlink_roundTrip() {
|
||||
val xpdr = linearTransponder()
|
||||
val orbitalPos = pos(3.5)
|
||||
val originalDownlink = 435_250_000L
|
||||
val uplink = DopplerFrequencyCalculator.computeUplinkFromDownlink(originalDownlink, xpdr, orbitalPos)
|
||||
assertNotNull(uplink)
|
||||
val roundTripDownlink = DopplerFrequencyCalculator.computeDownlinkFromUplink(uplink!!, xpdr, orbitalPos)
|
||||
assertNotNull(roundTripDownlink)
|
||||
val error = kotlin.math.abs(roundTripDownlink!! - originalDownlink)
|
||||
assertTrue("Round-trip error too large: $error", error < 10000)
|
||||
}
|
||||
}
|
||||
+3
-9
@@ -47,7 +47,6 @@ import androidx.compose.material3.MaterialTheme
|
||||
import androidx.compose.material3.ModalBottomSheet
|
||||
import androidx.compose.material3.Surface
|
||||
import androidx.compose.material3.Text
|
||||
import androidx.compose.material3.SheetValue
|
||||
import androidx.compose.material3.rememberModalBottomSheetState
|
||||
import androidx.compose.material3.adaptive.currentWindowAdaptiveInfo
|
||||
import androidx.compose.runtime.Composable
|
||||
@@ -309,8 +308,7 @@ fun SharedDialog(
|
||||
titleTextAlign: TextAlign = if (onCancel != null && onAccept != null) TextAlign.Center else TextAlign.Start,
|
||||
content: @Composable (padding: Dp) -> Unit
|
||||
) {
|
||||
val dismissible = onCancel != null
|
||||
DialogShell(onDismissRequest = onDismissRequest, dismissible = dismissible) { padding ->
|
||||
DialogShell(onDismissRequest = onDismissRequest) { padding ->
|
||||
Row(
|
||||
verticalAlignment = Alignment.CenterVertically,
|
||||
modifier = Modifier
|
||||
@@ -344,14 +342,10 @@ fun SharedDialog(
|
||||
@Composable
|
||||
private fun DialogShell(
|
||||
onDismissRequest: () -> Unit,
|
||||
dismissible: Boolean = true,
|
||||
content: @Composable (padding: Dp) -> Unit
|
||||
) {
|
||||
val padding = LocalSpacing.current.large
|
||||
val sheetState = rememberModalBottomSheetState(
|
||||
skipPartiallyExpanded = true,
|
||||
confirmValueChange = { if (dismissible) true else it != SheetValue.Hidden }
|
||||
)
|
||||
val sheetState = rememberModalBottomSheetState(skipPartiallyExpanded = true)
|
||||
val stopSheetFling = remember {
|
||||
object : NestedScrollConnection {
|
||||
override suspend fun onPostFling(consumed: Velocity, available: Velocity): Velocity {
|
||||
@@ -366,7 +360,7 @@ private fun DialogShell(
|
||||
}
|
||||
}
|
||||
ModalBottomSheet(
|
||||
onDismissRequest = if (dismissible) onDismissRequest else { {} },
|
||||
onDismissRequest = onDismissRequest,
|
||||
sheetState = sheetState,
|
||||
dragHandle = null,
|
||||
shape = RoundedCornerShape(topStart = 24.dp, topEnd = 24.dp),
|
||||
|
||||
@@ -1,9 +0,0 @@
|
||||
<vector xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
android:width="24dp"
|
||||
android:height="24dp"
|
||||
android:viewportWidth="15"
|
||||
android:viewportHeight="15">
|
||||
<path
|
||||
android:fillColor="@android:color/white"
|
||||
android:pathData="M13.91,6.75c-1.17,2.25 -4.3,5.31 -6.07,6.94c-0.19,0.172 -0.48,0.172 -0.67,0C5.39,12.06 2.26,9 1.09,6.75C-1.48,1.8 5,-1.5 7.5,3.45C10,-1.5 16.48,1.8 13.91,6.75z" />
|
||||
</vector>
|
||||
@@ -0,0 +1,9 @@
|
||||
<vector xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
android:width="24dp"
|
||||
android:height="24dp"
|
||||
android:viewportWidth="24"
|
||||
android:viewportHeight="24">
|
||||
<path
|
||||
android:fillColor="@android:color/white"
|
||||
android:pathData="M14,21c1.93,0 3.62,-1.17 4,-3l-1.75,-0.88C16,18.21 15.33,19 14,19l-4.9,0c0.83,-1 1.5,-2.34 1.5,-4c0,-0.35 -0.03,-0.69 -0.08,-1L14,14v-2l-4.18,0C9,10.42 8,9.6 8,8c0,-1.93 1.57,-3.5 3.5,-3.5c1.5,0 2.79,0.95 3.28,2.28L16.63,6c-0.8,-2.05 -2.79,-3.5 -5.13,-3.5C8.46,2.5 6,4.96 6,8c0,1.78 0.79,2.9 1.49,4L6,12v2l2.47,0c0.08,0.31 0.13,0.64 0.13,1c0,2.7 -2.6,4 -2.6,4v2H14z" />
|
||||
</vector>
|
||||
@@ -0,0 +1,9 @@
|
||||
<vector xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
android:width="24dp"
|
||||
android:height="24dp"
|
||||
android:viewportWidth="960"
|
||||
android:viewportHeight="960">
|
||||
<path
|
||||
android:fillColor="@android:color/white"
|
||||
android:pathData="M240,800L280,640L120,640L140,560L300,560L340,400L180,400L200,320L360,320L400,160L480,160L440,320L600,320L640,160L720,160L680,320L840,320L820,400L660,400L620,560L780,560L760,640L600,640L560,800L480,800L520,640L360,640L320,800L240,800ZM380,560L540,560L580,400L420,400L380,560Z" />
|
||||
</vector>
|
||||
@@ -155,11 +155,11 @@
|
||||
<string name="prefs_net_perm_error">Revisa los permisos de red</string>
|
||||
|
||||
<string name="prefs_other_title">Otros</string>
|
||||
<string name="prefs_other_switch_utc">Hora UTC</string>
|
||||
<string name="prefs_other_switch_utc">Mostrar hora UTC</string>
|
||||
<string name="prefs_other_switch_update">Actualización automática</string>
|
||||
<string name="prefs_other_switch_sweep">Animación radar</string>
|
||||
<string name="prefs_other_switch_sensors">Sensores</string>
|
||||
<string name="prefs_other_switch_night_mode">Filtro nocturno</string>
|
||||
<string name="prefs_other_switch_sweep">Animación de radar</string>
|
||||
<string name="prefs_other_switch_sensors">Sensores para radar</string>
|
||||
<string name="prefs_other_switch_night_mode">Filtro nocturno rojo</string>
|
||||
|
||||
<string name="prefs_highlight_title">Resaltado de elevación</string>
|
||||
<string name="prefs_highlight_low">Baja < %1$d°</string>
|
||||
|
||||
@@ -158,8 +158,8 @@
|
||||
<string name="prefs_other_switch_utc">Время в UTC</string>
|
||||
<string name="prefs_other_switch_update">Автообновление данных</string>
|
||||
<string name="prefs_other_switch_sweep">Анимация радара</string>
|
||||
<string name="prefs_other_switch_sensors">Сенсоры</string>
|
||||
<string name="prefs_other_switch_night_mode">Ночной фильтр</string>
|
||||
<string name="prefs_other_switch_sensors">Сенсоры для радара</string>
|
||||
<string name="prefs_other_switch_night_mode">Красный ночной фильтр</string>
|
||||
|
||||
<string name="prefs_highlight_title">Подсветка высоты</string>
|
||||
<string name="prefs_highlight_low">Низко < %1$d°</string>
|
||||
|
||||
@@ -155,11 +155,11 @@
|
||||
<string name="prefs_net_perm_error">Network අවසර පරික්ෂා කරන්න</string>
|
||||
|
||||
<string name="prefs_other_title">වෙනත්</string>
|
||||
<string name="prefs_other_switch_utc">UTC වේලාව</string>
|
||||
<string name="prefs_other_switch_utc">UTC වේලාව පෙන්වන්න</string>
|
||||
<string name="prefs_other_switch_update">දත්ත ස්වයං යාවත්කාලීන</string>
|
||||
<string name="prefs_other_switch_sweep">Radar sweep</string>
|
||||
<string name="prefs_other_switch_sensors">සංවේදක</string>
|
||||
<string name="prefs_other_switch_night_mode">රාත්රී පෙරහන</string>
|
||||
<string name="prefs_other_switch_sweep">Radar sweep සජීවිකරණය</string>
|
||||
<string name="prefs_other_switch_sensors">Radar සඳහා සංවේදක භාවිතා කරන්න</string>
|
||||
<string name="prefs_other_switch_night_mode">රතු රාත්රී පෙරහන</string>
|
||||
|
||||
<string name="prefs_highlight_title">උස් උද්දීපනය</string>
|
||||
<string name="prefs_highlight_low">අඩු < %1$d°</string>
|
||||
|
||||
@@ -187,11 +187,11 @@
|
||||
<string name="prefs_net_perm_error">Ağ izninizi kontrol edin</string>
|
||||
|
||||
<string name="prefs_other_title">Diğer</string>
|
||||
<string name="prefs_other_switch_utc">UTC saati</string>
|
||||
<string name="prefs_other_switch_utc">UTC saatini göster</string>
|
||||
<string name="prefs_other_switch_update">Veriyi otomatik güncelle</string>
|
||||
<string name="prefs_other_switch_sweep">Radar taraması</string>
|
||||
<string name="prefs_other_switch_sensors">Sensör</string>
|
||||
<string name="prefs_other_switch_night_mode">Gece filtresi</string>
|
||||
<string name="prefs_other_switch_sweep">Radar tarama animasyonu</string>
|
||||
<string name="prefs_other_switch_sensors">Radar için sensör kullan</string>
|
||||
<string name="prefs_other_switch_night_mode">Kırmızı gece filtresi</string>
|
||||
|
||||
<string name="prefs_highlight_title">Elevasyon vurgusu</string>
|
||||
<string name="prefs_highlight_low">Düşük < %1$d°</string>
|
||||
|
||||
@@ -158,8 +158,8 @@
|
||||
<string name="prefs_other_switch_utc">Час в UTC</string>
|
||||
<string name="prefs_other_switch_update">Автооновлення даних</string>
|
||||
<string name="prefs_other_switch_sweep">Анімація радара</string>
|
||||
<string name="prefs_other_switch_sensors">Сенсори</string>
|
||||
<string name="prefs_other_switch_night_mode">Нічний фільтр</string>
|
||||
<string name="prefs_other_switch_sensors">Сенсори для радара</string>
|
||||
<string name="prefs_other_switch_night_mode">Червоний нічний фільтр</string>
|
||||
|
||||
<string name="prefs_highlight_title">Підсвічування висоти</string>
|
||||
<string name="prefs_highlight_low">Низько < %1$d°</string>
|
||||
|
||||
@@ -60,9 +60,6 @@
|
||||
<string name="radar_string_no">否</string>
|
||||
<string name="radar_string_yes">是</string>
|
||||
<string name="radar_visible">日照中</string>
|
||||
<string name="radar_doppler_tx_hint">输入上行频率 (MHz)</string>
|
||||
<string name="radar_doppler_rx_hint">输入下行频率 (MHz)</string>
|
||||
<string name="radar_doppler_offset_hint">偏移 (kHz)</string>
|
||||
|
||||
<!-- Map screen -->
|
||||
<string name="map_prev">上一个</string>
|
||||
|
||||
@@ -55,16 +55,8 @@
|
||||
\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">
|
||||
* Added small tweaks to Sources and strings. Added official ISS source (#225)
|
||||
\n* Implemented AOS window and elevation highlight filters (#226)
|
||||
\n* Added cleartext traffic support for custom TLE URLs (#227)
|
||||
\n* Fixed manual OMM (.csv) data import, tweaked error messaging (#228)
|
||||
\n* Added support for Icom IC-705 CAT by ruilvo (#229)
|
||||
\n* Added configurable Radar offset to the sensors output by qwqtoday (#230)
|
||||
\n* Localized pass date and time formats for Chinese by atsunatsu (#231)
|
||||
\n* Added linear transponder Doppler calculator by atsunatsu (#232)
|
||||
\n* Added a few tweaks to SSTV sensitivity and reception, similar to Robot36
|
||||
\n* Replaced the types selection dialog with modes selection to avoid confusion
|
||||
* Added required tweaks to support Android 17 (API 37)
|
||||
\n\n* Added ACCESS_LOCAL_NETWORK permission check to support network data output
|
||||
</string>
|
||||
|
||||
<!-- Radar screen -->
|
||||
@@ -92,9 +84,6 @@
|
||||
<string name="radar_string_no">No</string>
|
||||
<string name="radar_string_yes">Yes</string>
|
||||
<string name="radar_visible">Visible</string>
|
||||
<string name="radar_doppler_tx_hint">Enter TX freq (MHz)</string>
|
||||
<string name="radar_doppler_rx_hint">Enter RX freq (MHz)</string>
|
||||
<string name="radar_doppler_offset_hint">Offset (kHz)</string>
|
||||
|
||||
<!-- Map screen -->
|
||||
<string name="map_prev">Prev</string>
|
||||
@@ -195,13 +184,11 @@
|
||||
<string name="prefs_net_perm_error">Check your network permission</string>
|
||||
|
||||
<string name="prefs_other_title">Other</string>
|
||||
<string name="prefs_other_switch_utc">UTC time</string>
|
||||
<string name="prefs_other_switch_utc">Show UTC time</string>
|
||||
<string name="prefs_other_switch_update">Auto-update data</string>
|
||||
<string name="prefs_other_switch_sweep">Radar sweep</string>
|
||||
<string name="prefs_other_switch_sensors">Use sensors</string>
|
||||
<string name="prefs_other_switch_night_mode">Night filter</string>
|
||||
<string name="prefs_other_compass_offset">Radar compass offset</string>
|
||||
<string name="prefs_other_compass_offset_elev">Radar compass offset (elev)</string>
|
||||
<string name="prefs_other_switch_sweep">Radar sweep animation</string>
|
||||
<string name="prefs_other_switch_sensors">Use sensors for radar</string>
|
||||
<string name="prefs_other_switch_night_mode">Red night filter</string>
|
||||
|
||||
<string name="prefs_highlight_title">Elevation highlight</string>
|
||||
<string name="prefs_highlight_low">Low < %1$d°</string>
|
||||
|
||||
@@ -1,8 +1,2 @@
|
||||
* Added ARISS source, tweaked SSTV sensitivity #225
|
||||
* Implemented AOS window and elev filter #226
|
||||
* Added cleartext traffic support for TLEs #227
|
||||
* Fixed OMM import, tweaked error messages #228
|
||||
* Added support for Icom CAT by ruilvo #229
|
||||
* Added tweakable Radar offset by qwqtoday #230
|
||||
* Localized date/time formats by atsunatsu #231
|
||||
* Added linear radio Doppler calc by atsunatsu #232
|
||||
* Added required tweaks to support Android 17 (API 37)
|
||||
* Added ACCESS_LOCAL_NETWORK permission check to support network data output
|
||||
+23
-31
@@ -34,9 +34,6 @@ import kotlinx.coroutines.delay
|
||||
import kotlinx.coroutines.flow.MutableStateFlow
|
||||
import kotlinx.coroutines.flow.StateFlow
|
||||
import kotlinx.coroutines.flow.collectLatest
|
||||
import kotlinx.coroutines.flow.combine
|
||||
import kotlinx.coroutines.flow.distinctUntilChanged
|
||||
import kotlinx.coroutines.flow.map
|
||||
import kotlinx.coroutines.flow.update
|
||||
import kotlinx.coroutines.isActive
|
||||
import kotlinx.coroutines.launch
|
||||
@@ -44,7 +41,6 @@ import java.text.SimpleDateFormat
|
||||
import java.util.Date
|
||||
import java.util.Locale
|
||||
import java.util.TimeZone
|
||||
import kotlin.time.Duration.Companion.milliseconds
|
||||
|
||||
class PassesViewModel(
|
||||
private val satelliteRepo: ISatelliteRepo,
|
||||
@@ -95,36 +91,32 @@ class PassesViewModel(
|
||||
_uiState.update { it.copy(modes = modes) }
|
||||
}
|
||||
}
|
||||
// Tick loop: restarts on passes change, UTC/DeepSpace changes. Grouping/sun-time
|
||||
// computations run once per restart, progress/countdown are calculated every second.
|
||||
// Main tick loop: reacts to new passes, then ticks every second
|
||||
viewModelScope.launch {
|
||||
combine(
|
||||
satelliteRepo.passes,
|
||||
settingsRepo.otherSettings.map { it.stateOfUtc }.distinctUntilChanged(),
|
||||
settingsRepo.passesSettings.map { it.showDeepSpace }.distinctUntilChanged()
|
||||
) { passes, isUtc, showDeepSpace -> Triple(passes, isUtc, showDeepSpace) }
|
||||
.collectLatest { (allPasses, isUtc, showDeepSpace) ->
|
||||
val filtered = if (showDeepSpace) allPasses
|
||||
else allPasses.filter { !it.isDeepSpace }
|
||||
// Expensive: recompute once per pass-list/UTC change, not every second
|
||||
val sunTimes = computeSunTimes(filtered, isUtc)
|
||||
val grouped = groupPasses(filtered, isUtc)
|
||||
_uiState.update { it.copy(sunTimes = sunTimes, groupedPasses = grouped) }
|
||||
while (isActive) {
|
||||
val timeNow = System.currentTimeMillis()
|
||||
val processed = computePassProgress(filtered, timeNow)
|
||||
val (nextPass, nextTime, isAos) = resolveNextPass(processed, timeNow)
|
||||
_uiState.update {
|
||||
it.copy(
|
||||
itemsList = processed,
|
||||
nextPass = nextPass,
|
||||
nextTime = nextTime,
|
||||
isNextTimeAos = isAos
|
||||
)
|
||||
}
|
||||
delay(1000.milliseconds)
|
||||
satelliteRepo.passes.collectLatest { allPasses ->
|
||||
while (isActive) {
|
||||
val timeNow = System.currentTimeMillis()
|
||||
val isUtc = _uiState.value.isUtc
|
||||
val showDeepSpace = _uiState.value.showDeepSpace
|
||||
val filtered = allPasses
|
||||
.let { if (showDeepSpace) it else it.filter { pass -> !pass.isDeepSpace } }
|
||||
val processed = computePassProgress(filtered, timeNow)
|
||||
val (nextPass, nextTime, isAos) = resolveNextPass(processed, timeNow)
|
||||
val sunTimes = computeSunTimes(processed, isUtc)
|
||||
val grouped = groupPasses(processed, isUtc)
|
||||
_uiState.update {
|
||||
it.copy(
|
||||
itemsList = processed,
|
||||
groupedPasses = grouped,
|
||||
sunTimes = sunTimes,
|
||||
nextPass = nextPass,
|
||||
nextTime = nextTime,
|
||||
isNextTimeAos = isAos
|
||||
)
|
||||
}
|
||||
delay(1000)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -45,7 +45,6 @@ import androidx.compose.material3.Text
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.LaunchedEffect
|
||||
import androidx.compose.runtime.getValue
|
||||
import androidx.compose.runtime.remember
|
||||
import androidx.compose.runtime.rememberCoroutineScope
|
||||
import androidx.compose.ui.Alignment
|
||||
import androidx.compose.ui.Modifier
|
||||
@@ -60,7 +59,6 @@ import androidx.lifecycle.compose.collectAsStateWithLifecycle
|
||||
import androidx.lifecycle.viewmodel.compose.viewModel
|
||||
import com.rtbishop.look4sat.core.domain.predict.OrbitalPos
|
||||
import com.rtbishop.look4sat.core.domain.repository.IContainerProvider
|
||||
import com.rtbishop.look4sat.core.domain.utility.DopplerFrequencyCalculator
|
||||
import com.rtbishop.look4sat.core.domain.utility.toDegrees
|
||||
import com.rtbishop.look4sat.core.presentation.EmptyListCard
|
||||
import com.rtbishop.look4sat.core.presentation.IconCard
|
||||
@@ -76,7 +74,6 @@ import kotlinx.coroutines.launch
|
||||
|
||||
private enum class RadarPage(val title: String) {
|
||||
Transceivers("Transceivers"),
|
||||
Calculator("Calculator"),
|
||||
Sstv("SSTV")
|
||||
}
|
||||
|
||||
@@ -153,31 +150,16 @@ private fun PagerCard(
|
||||
requestMicPermission: () -> Unit,
|
||||
modifier: Modifier = Modifier
|
||||
) {
|
||||
val hasCalculatorPage = remember(uiState.transceivers.transmitters) {
|
||||
uiState.transceivers.transmitters.any(DopplerFrequencyCalculator::isNamedLinearTransponder)
|
||||
}
|
||||
val pages = remember(hasCalculatorPage) {
|
||||
buildList {
|
||||
add(RadarPage.Transceivers)
|
||||
if (hasCalculatorPage) add(RadarPage.Calculator)
|
||||
add(RadarPage.Sstv)
|
||||
}
|
||||
}
|
||||
val pages = RadarPage.entries
|
||||
val pagerState = rememberPagerState(pageCount = { pages.size })
|
||||
val coroutineScope = rememberCoroutineScope()
|
||||
|
||||
LaunchedEffect(pages.size) {
|
||||
val lastPage = pages.lastIndex
|
||||
if (pagerState.currentPage > lastPage) pagerState.scrollToPage(lastPage)
|
||||
}
|
||||
|
||||
ElevatedCard(modifier = modifier) {
|
||||
Column(modifier = Modifier.fillMaxSize()) {
|
||||
val selectedTabIndex = pagerState.currentPage.coerceIn(0, pages.lastIndex)
|
||||
PrimaryTabRow(selectedTabIndex = selectedTabIndex) {
|
||||
PrimaryTabRow(selectedTabIndex = pagerState.currentPage) {
|
||||
pages.forEachIndexed { index, page ->
|
||||
Tab(
|
||||
selected = selectedTabIndex == index,
|
||||
selected = pagerState.currentPage == index,
|
||||
onClick = { coroutineScope.launch { pagerState.animateScrollToPage(index) } },
|
||||
text = { Text(text = page.title, maxLines = 1, overflow = TextOverflow.Ellipsis) }
|
||||
)
|
||||
@@ -194,12 +176,6 @@ private fun PagerCard(
|
||||
radioControl = uiState.radioControl,
|
||||
onAction = onAction
|
||||
)
|
||||
RadarPage.Calculator -> CalculatorPage(
|
||||
transceivers = uiState.transceivers.transmitters,
|
||||
selectedUuid = uiState.transceivers.selectedUuid,
|
||||
orbitalPos = uiState.orbitalPos,
|
||||
onAction = onAction
|
||||
)
|
||||
RadarPage.Sstv -> SstvPage(
|
||||
sstv = uiState.sstv,
|
||||
dopplerFrequency = uiState.transceivers.selectedFrequency?.let { formatFrequency(it) },
|
||||
@@ -249,7 +225,6 @@ private fun RadarCard(uiState: RadarState, modifier: Modifier = Modifier) {
|
||||
azimElev = uiState.orientationValues,
|
||||
shouldShowSweep = uiState.shouldShowSweep,
|
||||
shouldUseCompass = uiState.shouldUseCompass,
|
||||
shouldFlipRadar = uiState.shouldFlipRadar,
|
||||
modifier = Modifier.align(Alignment.Center),
|
||||
sunPosition = uiState.sunPosition,
|
||||
moonPosition = uiState.moonPosition,
|
||||
|
||||
@@ -58,7 +58,6 @@ data class RadarState(
|
||||
val satTrack: List<OrbitalPos> = emptyList(),
|
||||
val shouldShowSweep: Boolean = false,
|
||||
val shouldUseCompass: Boolean = false,
|
||||
val shouldFlipRadar: Boolean = false,
|
||||
val sunPosition: CelestialComputer.SunPosition? = null,
|
||||
val moonPosition: CelestialComputer.MoonPosition? = null,
|
||||
val transceivers: TransceiverSubState = TransceiverSubState(),
|
||||
|
||||
@@ -74,7 +74,6 @@ fun RadarViewCompose(
|
||||
azimElev: Pair<Float, Float>,
|
||||
shouldShowSweep: Boolean,
|
||||
shouldUseCompass: Boolean,
|
||||
shouldFlipRadar: Boolean,
|
||||
modifier: Modifier = Modifier,
|
||||
sunPosition: CelestialComputer.SunPosition? = null,
|
||||
moonPosition: CelestialComputer.MoonPosition? = null,
|
||||
@@ -126,9 +125,7 @@ fun RadarViewCompose(
|
||||
cachedSweepBrush = makeSweepBrush(center, primaryColor)
|
||||
cachedSweepColor = primaryColor
|
||||
}
|
||||
val baseRotation = if (shouldUseCompass) -azimElev.first else 0f
|
||||
val rotation = if (shouldFlipRadar) baseRotation + 180f else baseRotation
|
||||
rotate(rotation) {
|
||||
rotate(if (shouldUseCompass) -azimElev.first else 0f) {
|
||||
if (shouldShowSweep) cachedSweepBrush?.let { drawSweep(center, sweepDegrees, radius, it) }
|
||||
drawRadar(radius, radarColor)
|
||||
drawElevationLabels(radius, primaryColor, measurer)
|
||||
|
||||
@@ -67,12 +67,9 @@ class RadarViewModel(
|
||||
|
||||
private val stationPos = settingsRepo.stationPosition.value
|
||||
private val magDeclination = sensorsRepo.getMagDeclination(stationPos)
|
||||
private var compassOffset = settingsRepo.otherSettings.value.radarCompassOffset
|
||||
private var compassOffsetElev = settingsRepo.otherSettings.value.radarCompassOffsetElev
|
||||
private var transponders: List<SatRadio> = emptyList()
|
||||
private var sstvDecoder: SstvDecoder? = null
|
||||
private var sstvRecordingJob: Job? = null
|
||||
private var sensorCollectionJob: Job? = null
|
||||
|
||||
// Celestial positions change slowly, recompute at most once per minute
|
||||
private var lastCelestialUpdateMs = 0L
|
||||
@@ -91,49 +88,33 @@ class RadarViewModel(
|
||||
val uiState: StateFlow<RadarState> = _uiState
|
||||
|
||||
init {
|
||||
collectSettingsChanges() // also handles initial sensor subscription
|
||||
collectCompassSensor()
|
||||
collectSettingsChanges()
|
||||
collectPassAndStartTickLoop()
|
||||
collectRadioTrackingState()
|
||||
}
|
||||
|
||||
// Starts sensor collection if not already running.
|
||||
private fun startSensorCollection() {
|
||||
if (sensorCollectionJob?.isActive == true) return
|
||||
sensorCollectionJob = viewModelScope.launch {
|
||||
private fun collectCompassSensor() {
|
||||
if (!settingsRepo.otherSettings.value.stateOfSensors) return
|
||||
viewModelScope.launch {
|
||||
sensorsRepo.enableSensor()
|
||||
sensorsRepo.sensorData.collect { data ->
|
||||
val orientationValues =
|
||||
(data.first + magDeclination + compassOffset) to (data.second + compassOffsetElev)
|
||||
val orientationValues = (data.first + magDeclination) to data.second
|
||||
_uiState.update { it.copy(orientationValues = orientationValues) }
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Stops sensor collection and disables the hardware sensor.
|
||||
private fun stopSensorCollection() {
|
||||
sensorCollectionJob?.cancel()
|
||||
sensorCollectionJob = null
|
||||
sensorsRepo.disableSensor()
|
||||
}
|
||||
|
||||
private fun collectSettingsChanges() {
|
||||
viewModelScope.launch {
|
||||
settingsRepo.otherSettings.collect { settings ->
|
||||
compassOffset = settings.radarCompassOffset
|
||||
compassOffsetElev = settings.radarCompassOffsetElev
|
||||
settingsRepo.otherSettings.collectLatest { settings ->
|
||||
_uiState.update {
|
||||
it.copy(
|
||||
isUtc = settings.stateOfUtc,
|
||||
shouldShowSweep = settings.stateOfSweep,
|
||||
shouldUseCompass = settings.stateOfSensors,
|
||||
shouldFlipRadar = settings.radarCompassOffsetElev < 0f
|
||||
shouldUseCompass = settings.stateOfSensors
|
||||
)
|
||||
}
|
||||
// Reactively wire sensor hardware to the compass setting
|
||||
when {
|
||||
settings.stateOfSensors -> startSensorCollection()
|
||||
else -> stopSensorCollection()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -232,7 +213,7 @@ class RadarViewModel(
|
||||
}
|
||||
|
||||
override fun onCleared() {
|
||||
stopSensorCollection()
|
||||
sensorsRepo.disableSensor()
|
||||
}
|
||||
|
||||
fun onAction(action: RadarAction) {
|
||||
@@ -418,7 +399,7 @@ class RadarViewModel(
|
||||
// - 0.25: Aggressive (original default), amplifies noise; use only for clean inputs
|
||||
// - 0.35-0.40: RECOMMENDED for typical phone/mic inputs
|
||||
// - 0.50-0.60: Conservative, best for noisy environments
|
||||
private const val SSTV_TARGET_RMS = 0.45f // Changed from 0.25 to safer default
|
||||
private const val SSTV_TARGET_RMS = 0.40f // Changed from 0.25 to safer default
|
||||
|
||||
// Enable/disable RMS normalization entirely. Set false for direct receiver outputs.
|
||||
private const val SSTV_ENABLE_RMS_NORMALIZATION = true
|
||||
@@ -441,7 +422,7 @@ class RadarViewModel(
|
||||
// - Look4SatLimited: caps predicted lines to reduce visible vertical collapse.
|
||||
// - Robot36Compatible: unlimited predicted lines (legacy Robot36 behavior).
|
||||
// Ask users to compare both if they report line-skipping/compression artifacts.
|
||||
private val SSTV_LINE_RECOVERY_STRATEGY = LineRecoveryStrategy.Robot36Compatible
|
||||
private val SSTV_LINE_RECOVERY_STRATEGY = LineRecoveryStrategy.Look4SatLimited
|
||||
|
||||
// Debug: Return scope visualization in frames (increases per-frame memory usage).
|
||||
private const val SSTV_INCLUDE_SCOPE = false
|
||||
@@ -453,6 +434,7 @@ class RadarViewModel(
|
||||
218.1, 225.7, 233.6, 241.8, 250.3
|
||||
)
|
||||
|
||||
|
||||
fun factory(container: IMainContainer) = viewModelFactory {
|
||||
initializer {
|
||||
RadarViewModel(
|
||||
|
||||
+5
-180
@@ -39,19 +39,14 @@ import androidx.compose.foundation.lazy.LazyColumn
|
||||
import androidx.compose.foundation.lazy.itemsIndexed
|
||||
import androidx.compose.foundation.lazy.rememberLazyListState
|
||||
import androidx.compose.foundation.shape.CircleShape
|
||||
import androidx.compose.foundation.text.KeyboardOptions
|
||||
import androidx.compose.material3.FilterChip
|
||||
import androidx.compose.material3.HorizontalDivider
|
||||
import androidx.compose.material3.Icon
|
||||
import androidx.compose.material3.MaterialTheme
|
||||
import androidx.compose.material3.OutlinedTextField
|
||||
import androidx.compose.material3.Text
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.LaunchedEffect
|
||||
import androidx.compose.runtime.getValue
|
||||
import androidx.compose.runtime.mutableStateOf
|
||||
import androidx.compose.runtime.remember
|
||||
import androidx.compose.runtime.setValue
|
||||
import androidx.compose.ui.Alignment
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.draw.clip
|
||||
@@ -60,14 +55,11 @@ import androidx.compose.ui.graphics.Color
|
||||
import androidx.compose.ui.res.painterResource
|
||||
import androidx.compose.ui.res.stringResource
|
||||
import androidx.compose.ui.text.font.FontWeight
|
||||
import androidx.compose.ui.text.input.KeyboardType
|
||||
import androidx.compose.ui.text.style.TextAlign
|
||||
import androidx.compose.ui.text.style.TextOverflow
|
||||
import androidx.compose.ui.unit.dp
|
||||
import androidx.compose.ui.unit.sp
|
||||
import com.rtbishop.look4sat.core.domain.model.SatRadio
|
||||
import com.rtbishop.look4sat.core.domain.predict.OrbitalPos
|
||||
import com.rtbishop.look4sat.core.domain.utility.DopplerFrequencyCalculator
|
||||
import com.rtbishop.look4sat.core.presentation.CardButton
|
||||
import com.rtbishop.look4sat.core.presentation.R
|
||||
import com.rtbishop.look4sat.core.presentation.formatFrequency
|
||||
@@ -112,69 +104,6 @@ fun TransceiversPage(
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
fun CalculatorPage(
|
||||
transceivers: List<SatRadio>,
|
||||
selectedUuid: String?,
|
||||
orbitalPos: OrbitalPos?,
|
||||
onAction: (RadarAction) -> Unit,
|
||||
modifier: Modifier = Modifier
|
||||
) {
|
||||
val calculatorTransceivers = remember(transceivers) {
|
||||
transceivers.filter(DopplerFrequencyCalculator::isNamedLinearTransponder)
|
||||
}
|
||||
val selectedTransceiver = remember(calculatorTransceivers, selectedUuid) {
|
||||
calculatorTransceivers.firstOrNull { it.uuid == selectedUuid }
|
||||
?: calculatorTransceivers.firstOrNull()
|
||||
}
|
||||
|
||||
if (selectedTransceiver == null) {
|
||||
EmptyTransceiversContent(modifier)
|
||||
return
|
||||
}
|
||||
|
||||
LazyColumn(
|
||||
modifier = modifier
|
||||
.fillMaxSize()
|
||||
.padding(8.dp),
|
||||
verticalArrangement = Arrangement.spacedBy(10.dp)
|
||||
) {
|
||||
if (calculatorTransceivers.size > 1) {
|
||||
item {
|
||||
FlowRow(
|
||||
horizontalArrangement = Arrangement.spacedBy(6.dp),
|
||||
verticalArrangement = Arrangement.spacedBy(4.dp),
|
||||
modifier = Modifier.fillMaxWidth()
|
||||
) {
|
||||
calculatorTransceivers.forEach { radio ->
|
||||
FilterChip(
|
||||
selected = radio.uuid == selectedTransceiver.uuid,
|
||||
onClick = {
|
||||
if (radio.uuid != selectedUuid) onAction(RadarAction.SelectTransmitter(radio.uuid))
|
||||
},
|
||||
label = {
|
||||
Text(
|
||||
text = transceiverTitle(radio),
|
||||
maxLines = 1,
|
||||
overflow = TextOverflow.Ellipsis
|
||||
)
|
||||
}
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
item {
|
||||
DopplerCalculatorPanel(
|
||||
transponder = selectedTransceiver,
|
||||
orbitalPos = orbitalPos,
|
||||
modifier = Modifier.fillMaxWidth()
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun EmptyTransceiversContent(modifier: Modifier = Modifier) {
|
||||
Box(contentAlignment = Alignment.Center, modifier = modifier.fillMaxSize()) {
|
||||
@@ -243,8 +172,12 @@ private fun TransceiverItem(
|
||||
.rotate(if (isExpanded) 270f else 90f)
|
||||
)
|
||||
}
|
||||
// Title with mode
|
||||
val title = if (radio.isInverted) "INV: ${radio.info}" else radio.info
|
||||
val mode = "${radio.downlinkMode ?: "--"}/${radio.uplinkMode ?: "--"}"
|
||||
val fullTitle = "$title ($mode)"
|
||||
Text(
|
||||
text = transceiverTitle(radio),
|
||||
text = fullTitle,
|
||||
textAlign = TextAlign.Center,
|
||||
color = MaterialTheme.colorScheme.onSurface,
|
||||
maxLines = 1,
|
||||
@@ -504,108 +437,6 @@ private fun ExpandedRadioControl(
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun DopplerCalculatorPanel(
|
||||
transponder: SatRadio,
|
||||
orbitalPos: OrbitalPos?,
|
||||
modifier: Modifier = Modifier
|
||||
) {
|
||||
if (orbitalPos == null || !DopplerFrequencyCalculator.isLinearTransponder(transponder)) return
|
||||
|
||||
var txInputMHz by remember(transponder.uuid) { mutableStateOf("") }
|
||||
var rxInputMHz by remember(transponder.uuid) { mutableStateOf("") }
|
||||
var offsetKHz by remember(transponder.uuid) { mutableStateOf("") }
|
||||
var lastEditedBy by remember(transponder.uuid) { mutableStateOf(EditedField.TX) }
|
||||
|
||||
fun offsetHz(text: String): Long = text.toDoubleOrNull()?.let { (it * 1000).toLong() } ?: 0L
|
||||
fun formatHz(hz: Long): String = String.format(Locale.ENGLISH, "%.6f", hz / 1_000_000.0)
|
||||
fun mhzToHz(text: String): Long? = text.toDoubleOrNull()
|
||||
?.takeIf { it > 0.0 }
|
||||
?.let { (it * 1_000_000).toLong() }
|
||||
|
||||
fun calculateDownlink(txText: String, offsetText: String): String? {
|
||||
val txHz = mhzToHz(txText) ?: return null
|
||||
return DopplerFrequencyCalculator.computeDownlinkFromUplinkWithOffset(
|
||||
uplinkHz = txHz,
|
||||
transponder = transponder,
|
||||
orbitalPos = orbitalPos,
|
||||
offsetHz = offsetHz(offsetText)
|
||||
)?.let(::formatHz)
|
||||
}
|
||||
|
||||
fun calculateUplink(rxText: String, offsetText: String): String? {
|
||||
val rxHz = mhzToHz(rxText) ?: return null
|
||||
return DopplerFrequencyCalculator.computeUplinkFromDownlinkWithOffset(
|
||||
downlinkHz = rxHz,
|
||||
transponder = transponder,
|
||||
orbitalPos = orbitalPos,
|
||||
offsetHz = offsetHz(offsetText)
|
||||
)?.let(::formatHz)
|
||||
}
|
||||
|
||||
LaunchedEffect(orbitalPos, transponder.uuid) {
|
||||
if (lastEditedBy == EditedField.TX) {
|
||||
calculateDownlink(txInputMHz, offsetKHz)?.let { rxInputMHz = it }
|
||||
} else {
|
||||
calculateUplink(rxInputMHz, offsetKHz)?.let { txInputMHz = it }
|
||||
}
|
||||
}
|
||||
|
||||
Column(
|
||||
modifier = modifier,
|
||||
verticalArrangement = Arrangement.spacedBy(8.dp)
|
||||
) {
|
||||
OutlinedTextField(
|
||||
value = offsetKHz,
|
||||
onValueChange = { newValue ->
|
||||
offsetKHz = newValue
|
||||
if (lastEditedBy == EditedField.TX) {
|
||||
calculateDownlink(txInputMHz, newValue)?.let { rxInputMHz = it }
|
||||
} else {
|
||||
calculateUplink(rxInputMHz, newValue)?.let { txInputMHz = it }
|
||||
}
|
||||
},
|
||||
label = { Text(stringResource(R.string.radar_doppler_offset_hint)) },
|
||||
singleLine = true,
|
||||
keyboardOptions = KeyboardOptions(keyboardType = KeyboardType.Text),
|
||||
modifier = Modifier.fillMaxWidth()
|
||||
)
|
||||
|
||||
Row(
|
||||
horizontalArrangement = Arrangement.spacedBy(8.dp),
|
||||
modifier = Modifier.fillMaxWidth()
|
||||
) {
|
||||
OutlinedTextField(
|
||||
value = txInputMHz,
|
||||
onValueChange = { newValue ->
|
||||
txInputMHz = newValue
|
||||
lastEditedBy = EditedField.TX
|
||||
rxInputMHz = calculateDownlink(newValue, offsetKHz) ?: if (newValue.isEmpty()) "" else rxInputMHz
|
||||
},
|
||||
label = { Text(stringResource(R.string.radar_doppler_tx_hint)) },
|
||||
singleLine = true,
|
||||
keyboardOptions = KeyboardOptions(keyboardType = KeyboardType.Decimal),
|
||||
modifier = Modifier.weight(1f)
|
||||
)
|
||||
|
||||
OutlinedTextField(
|
||||
value = rxInputMHz,
|
||||
onValueChange = { newValue ->
|
||||
rxInputMHz = newValue
|
||||
lastEditedBy = EditedField.RX
|
||||
txInputMHz = calculateUplink(newValue, offsetKHz) ?: if (newValue.isEmpty()) "" else txInputMHz
|
||||
},
|
||||
label = { Text(stringResource(R.string.radar_doppler_rx_hint)) },
|
||||
singleLine = true,
|
||||
keyboardOptions = KeyboardOptions(keyboardType = KeyboardType.Decimal),
|
||||
modifier = Modifier.weight(1f)
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private enum class EditedField { TX, RX }
|
||||
|
||||
@Composable
|
||||
private fun FrequencyText(frequency: Long?, modifier: Modifier = Modifier) {
|
||||
val text = frequency?.let {
|
||||
@@ -621,11 +452,5 @@ private fun FrequencyText(frequency: Long?, modifier: Modifier = Modifier) {
|
||||
)
|
||||
}
|
||||
|
||||
private fun transceiverTitle(radio: SatRadio): String {
|
||||
val title = if (radio.isInverted) "INV: ${radio.info}" else radio.info
|
||||
val mode = "${radio.downlinkMode ?: "--"}/${radio.uplinkMode ?: "--"}"
|
||||
return "$title ($mode)"
|
||||
}
|
||||
|
||||
private val FREQ_ADJUSTMENTS =
|
||||
listOf(-10_000L to "-10k", -1_000L to "-1k", -100L to "-100", 100L to "+100", 1_000L to "+1k", 10_000L to "+10k")
|
||||
-5
@@ -59,11 +59,6 @@ class SatellitesViewModel(
|
||||
_uiState.update { it.copy(shouldSeeWarning = settings.shouldSeeWarning) }
|
||||
}
|
||||
}
|
||||
viewModelScope.launch {
|
||||
settingsRepo.selectedSatModes.collectLatest { modes ->
|
||||
_uiState.update { it.copy(currentModes = modes) }
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fun onAction(action: SatellitesAction) {
|
||||
|
||||
+23
-97
@@ -25,12 +25,10 @@ import androidx.compose.foundation.clickable
|
||||
import androidx.compose.foundation.layout.Arrangement
|
||||
import androidx.compose.foundation.layout.Column
|
||||
import androidx.compose.foundation.layout.Row
|
||||
import androidx.compose.foundation.layout.RowScope
|
||||
import androidx.compose.foundation.layout.Spacer
|
||||
import androidx.compose.foundation.layout.fillMaxHeight
|
||||
import androidx.compose.foundation.layout.fillMaxWidth
|
||||
import androidx.compose.foundation.layout.height
|
||||
import androidx.compose.foundation.layout.IntrinsicSize
|
||||
import androidx.compose.foundation.layout.padding
|
||||
import androidx.compose.foundation.layout.width
|
||||
import androidx.compose.foundation.lazy.grid.GridCells
|
||||
@@ -40,7 +38,6 @@ import androidx.compose.material3.ElevatedCard
|
||||
import androidx.compose.material3.Icon
|
||||
import androidx.compose.material3.LinearProgressIndicator
|
||||
import androidx.compose.material3.MaterialTheme
|
||||
import androidx.compose.material3.Slider
|
||||
import androidx.compose.material3.Switch
|
||||
import androidx.compose.material3.Text
|
||||
import androidx.compose.runtime.Composable
|
||||
@@ -222,7 +219,7 @@ private fun SettingsScreen(uiState: SettingsState, onAction: (SettingsAction) ->
|
||||
version = uiState.appVersionName,
|
||||
modifier = Modifier.weight(1f)
|
||||
)
|
||||
PrimaryIconCard(onClick = { safeOpenUri(donateUrl) }, resId = R.drawable.ic_like)
|
||||
PrimaryIconCard(onClick = { safeOpenUri(donateUrl) }, resId = R.drawable.ic_pound)
|
||||
}
|
||||
TopBar {
|
||||
Row(modifier = Modifier.weight(1f), horizontalArrangement = Arrangement.spacedBy(6.dp)) {
|
||||
@@ -244,7 +241,7 @@ private fun SettingsScreen(uiState: SettingsState, onAction: (SettingsAction) ->
|
||||
}
|
||||
} else {
|
||||
TopBar {
|
||||
PrimaryIconCard(onClick = { safeOpenUri(donateUrl) }, resId = R.drawable.ic_like)
|
||||
PrimaryIconCard(onClick = { safeOpenUri(donateUrl) }, resId = R.drawable.ic_pound)
|
||||
TopCard(
|
||||
onClick = { safeOpenUri(appUrl) },
|
||||
version = uiState.appVersionName,
|
||||
@@ -302,22 +299,8 @@ private fun SettingsScreen(uiState: SettingsState, onAction: (SettingsAction) ->
|
||||
onRadioControlClick = { dialogs.radioControl = true }
|
||||
)
|
||||
}
|
||||
item(span = { GridItemSpan(maxLineSpan) }) {
|
||||
if (isVerticalLayout) {
|
||||
Column(verticalArrangement = Arrangement.spacedBy(6.dp)) {
|
||||
OtherCard(uiState.otherSettings, onAction)
|
||||
CardCredits()
|
||||
}
|
||||
} else {
|
||||
Row(
|
||||
horizontalArrangement = Arrangement.spacedBy(6.dp),
|
||||
modifier = Modifier.fillMaxWidth().height(IntrinsicSize.Min)
|
||||
) {
|
||||
OtherCard(uiState.otherSettings, onAction, modifier = Modifier.weight(1f).fillMaxHeight())
|
||||
CardCredits(modifier = Modifier.weight(1f).fillMaxHeight())
|
||||
}
|
||||
}
|
||||
}
|
||||
item { OtherCard(uiState.otherSettings, onAction) }
|
||||
item { CardCredits() }
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -496,83 +479,40 @@ private fun OtherCardPreview() = MainTheme {
|
||||
shouldSeeWarning = false,
|
||||
shouldSeeWhatsNew = false
|
||||
)
|
||||
OtherCard(settings = values, onAction = {})
|
||||
OtherCard(settings = values) {}
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun OtherCard(settings: OtherSettings, onAction: (SettingsAction) -> Unit, modifier: Modifier = Modifier) {
|
||||
ElevatedCard(modifier = modifier.fillMaxWidth()) {
|
||||
private fun OtherCard(settings: OtherSettings, onAction: (SettingsAction) -> Unit) {
|
||||
ElevatedCard(
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.height(268.dp)
|
||||
) {
|
||||
Column(modifier = Modifier.padding(horizontal = 8.dp, vertical = 4.dp)) {
|
||||
Text(
|
||||
text = stringResource(id = R.string.prefs_other_title),
|
||||
color = MaterialTheme.colorScheme.primary
|
||||
)
|
||||
Spacer(modifier = Modifier.height(4.dp))
|
||||
// Display preferences: UTC clock + night filter
|
||||
Row(modifier = Modifier.fillMaxWidth(), horizontalArrangement = Arrangement.spacedBy(8.dp)) {
|
||||
SwitchTile(R.string.prefs_other_switch_utc, settings.stateOfUtc) {
|
||||
onAction(SettingsAction.ToggleUtc(it))
|
||||
}
|
||||
SwitchTile(R.string.prefs_other_switch_night_mode, settings.stateOfNightMode) {
|
||||
onAction(SettingsAction.ToggleNightMode(it))
|
||||
}
|
||||
SwitchRow(R.string.prefs_other_switch_utc, settings.stateOfUtc) {
|
||||
onAction(SettingsAction.ToggleUtc(it))
|
||||
}
|
||||
Spacer(modifier = Modifier.height(4.dp))
|
||||
// Radar behavior: sweep animation + sensor control
|
||||
Row(modifier = Modifier.fillMaxWidth(), horizontalArrangement = Arrangement.spacedBy(8.dp)) {
|
||||
SwitchTile(R.string.prefs_other_switch_sweep, settings.stateOfSweep) {
|
||||
onAction(SettingsAction.ToggleSweep(it))
|
||||
}
|
||||
SwitchTile(R.string.prefs_other_switch_sensors, settings.stateOfSensors) {
|
||||
onAction(SettingsAction.ToggleSensor(it))
|
||||
}
|
||||
}
|
||||
Spacer(modifier = Modifier.height(4.dp))
|
||||
// Data management (full width)
|
||||
SwitchRow(R.string.prefs_other_switch_update, settings.stateOfAutoUpdate) {
|
||||
onAction(SettingsAction.ToggleUpdate(it))
|
||||
}
|
||||
Spacer(modifier = Modifier.height(4.dp))
|
||||
// Compass calibration sliders at the bottom
|
||||
CompassOffsetRow(
|
||||
labelResId = R.string.prefs_other_compass_offset,
|
||||
value = settings.radarCompassOffset,
|
||||
range = -180f..180f
|
||||
) { onAction(SettingsAction.SetRadarCompassOffset(it)) }
|
||||
Spacer(modifier = Modifier.height(4.dp))
|
||||
CompassOffsetRow(
|
||||
labelResId = R.string.prefs_other_compass_offset_elev,
|
||||
value = settings.radarCompassOffsetElev,
|
||||
range = -90f..90f
|
||||
) { onAction(SettingsAction.SetRadarCompassOffsetElev(it)) }
|
||||
Spacer(modifier = Modifier.height(4.dp))
|
||||
SwitchRow(R.string.prefs_other_switch_sweep, settings.stateOfSweep) {
|
||||
onAction(SettingsAction.ToggleSweep(it))
|
||||
}
|
||||
SwitchRow(R.string.prefs_other_switch_sensors, settings.stateOfSensors) {
|
||||
onAction(SettingsAction.ToggleSensor(it))
|
||||
}
|
||||
SwitchRow(R.string.prefs_other_switch_night_mode, settings.stateOfNightMode) {
|
||||
onAction(SettingsAction.ToggleNightMode(it))
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun CompassOffsetRow(
|
||||
labelResId: Int,
|
||||
value: Float,
|
||||
range: ClosedFloatingPointRange<Float>,
|
||||
onValueChange: (Float) -> Unit
|
||||
) {
|
||||
Row(
|
||||
horizontalArrangement = Arrangement.SpaceBetween,
|
||||
verticalAlignment = Alignment.CenterVertically,
|
||||
modifier = Modifier.fillMaxWidth()
|
||||
) {
|
||||
Text(text = stringResource(id = labelResId))
|
||||
Text(text = "${value.toInt()}°")
|
||||
}
|
||||
Spacer(modifier = Modifier.height(4.dp))
|
||||
Slider(
|
||||
value = value,
|
||||
onValueChange = onValueChange,
|
||||
valueRange = range
|
||||
)
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun SwitchRow(labelResId: Int, checked: Boolean, onCheckedChange: (Boolean) -> Unit) {
|
||||
Row(
|
||||
@@ -585,21 +525,6 @@ private fun SwitchRow(labelResId: Int, checked: Boolean, onCheckedChange: (Boole
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun RowScope.SwitchTile(labelResId: Int, checked: Boolean, onCheckedChange: (Boolean) -> Unit) {
|
||||
Row(
|
||||
horizontalArrangement = Arrangement.SpaceBetween,
|
||||
verticalAlignment = Alignment.CenterVertically,
|
||||
modifier = Modifier.weight(1f)
|
||||
) {
|
||||
Text(
|
||||
text = stringResource(id = labelResId),
|
||||
modifier = Modifier.weight(1f)
|
||||
)
|
||||
Switch(checked = checked, onCheckedChange = onCheckedChange)
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun formatUpdateTime(updateTime: Long): String {
|
||||
val timePattern = stringResource(id = R.string.prefs_updated_time)
|
||||
@@ -634,6 +559,7 @@ private fun CardCredits(modifier: Modifier = Modifier) {
|
||||
ElevatedCard(
|
||||
modifier = modifier
|
||||
.fillMaxWidth()
|
||||
.height(268.dp)
|
||||
) {
|
||||
Column(
|
||||
verticalArrangement = Arrangement.SpaceBetween,
|
||||
|
||||
@@ -65,8 +65,6 @@ sealed interface SettingsAction {
|
||||
data class ToggleSensor(val value: Boolean) : SettingsAction
|
||||
data class ToggleLightTheme(val value: Boolean) : SettingsAction
|
||||
data class ToggleNightMode(val value: Boolean) : SettingsAction
|
||||
data class SetRadarCompassOffset(val value: Float) : SettingsAction
|
||||
data class SetRadarCompassOffsetElev(val value: Float) : SettingsAction
|
||||
|
||||
// Remote control
|
||||
data class UpdateRC(val settings: RCSettings) : SettingsAction
|
||||
|
||||
-2
@@ -123,8 +123,6 @@ class SettingsViewModel(
|
||||
is SettingsAction.ToggleSensor -> settingsRepo.updateOtherSettings { it.copy(stateOfSensors = action.value) }
|
||||
is SettingsAction.ToggleLightTheme -> settingsRepo.updateOtherSettings { it.copy(stateOfLightTheme = action.value) }
|
||||
is SettingsAction.ToggleNightMode -> settingsRepo.updateOtherSettings { it.copy(stateOfNightMode = action.value) }
|
||||
is SettingsAction.SetRadarCompassOffset -> settingsRepo.updateOtherSettings { it.copy(radarCompassOffset = action.value) }
|
||||
is SettingsAction.SetRadarCompassOffsetElev -> settingsRepo.updateOtherSettings { it.copy(radarCompassOffsetElev = action.value) }
|
||||
// Remote control & data sources
|
||||
is SettingsAction.UpdateRC -> settingsRepo.updateRCSettings(action.settings)
|
||||
is SettingsAction.UpdateRadioControl -> settingsRepo.updateRadioControlSettings(action.settings)
|
||||
|
||||
@@ -2,5 +2,4 @@
|
||||
org.gradle.caching=true
|
||||
org.gradle.jvmargs=-Xmx6g -XX:MaxMetaspaceSize=1g -XX:+HeapDumpOnOutOfMemoryError -Dfile.encoding=UTF-8
|
||||
org.gradle.parallel=true
|
||||
org.gradle.tooling.parallel=true
|
||||
org.gradle.workers.max=4
|
||||
@@ -1,8 +1,8 @@
|
||||
[versions]
|
||||
#noinspection UnusedVersionCatalogEntry
|
||||
appVersionCode = "444"
|
||||
appVersionCode = "443"
|
||||
#noinspection UnusedVersionCatalogEntry
|
||||
appVersionName = "4.4.4"
|
||||
appVersionName = "4.4.3"
|
||||
#noinspection UnusedVersionCatalogEntry
|
||||
compileSdk = "37"
|
||||
#noinspection UnusedVersionCatalogEntry
|
||||
@@ -23,7 +23,7 @@ compose-activity = "1.13.0"
|
||||
compose-lifecycle = "2.11.0"
|
||||
compose-navigation3 = "1.1.5"
|
||||
|
||||
google-ksp = "2.3.10"
|
||||
google-ksp = "2.3.8"
|
||||
|
||||
kotlin = "2.4.10"
|
||||
kotlin-coroutines = "1.11.0"
|
||||
|
||||
Reference in new issue
Block a user