v4.4.4 - Icom CAT, components, filters and sources tweaks

This commit is contained in:
Arty Bishop committed 2026-08-04 20:51:22 +02:00
1 parent bd0881fb4b
commit 602b1553a2
22 files changed
+189 -111

No files matched your search

@@ -37,6 +37,7 @@ import kotlinx.coroutines.flow.StateFlow
import kotlinx.coroutines.flow.update import kotlinx.coroutines.flow.update
import kotlinx.coroutines.withContext import kotlinx.coroutines.withContext
import java.util.TimeZone import java.util.TimeZone
import kotlin.time.Duration.Companion.milliseconds
class SatelliteRepo( class SatelliteRepo(
private val dispatcher: CoroutineDispatcher, private val dispatcher: CoroutineDispatcher,
@@ -158,7 +159,7 @@ class SatelliteRepo(
} }
} }
newPasses.sortBy { it.aosTime } newPasses.sortBy { it.aosTime }
delay(1000) // Simulate loading time for better UX delay(1000.milliseconds) // Simulate loading time for better UX
_passes.update { newPasses } _passes.update { newPasses }
} }
_isCalculating.value = false _isCalculating.value = false
@@ -176,7 +177,7 @@ class SatelliteRepo(
val inRange = if (aosStartMinute <= aosEndMinute) { val inRange = if (aosStartMinute <= aosEndMinute) {
aosMinute in aosStartMinute..aosEndMinute aosMinute in aosStartMinute..aosEndMinute
} else { } else {
aosMinute >= aosStartMinute || aosMinute <= aosEndMinute aosMinute !in (aosEndMinute + 1)..<aosStartMinute
} }
return if (invertAosTimeWindow) !inRange else inRange return if (invertAosTimeWindow) !inRange else inRange
} }
@@ -38,16 +38,16 @@ class SelectionRepo(
) : ISelectionRepo { ) : ISelectionRepo {
private val currentItems = MutableStateFlow<List<SatItem>>(emptyList()) private val currentItems = MutableStateFlow<List<SatItem>>(emptyList())
private val currentModes = MutableStateFlow(settingsRepo.selectedSatModes.value)
private val currentQuery = MutableStateFlow("") private val currentQuery = MutableStateFlow("")
// Resolve sat IDs once when modes change, then filter items reactively. // 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. // The HashSet gives O(1) catnum lookups instead of O(n) with a List.
private val itemsWithModes = currentModes.flatMapLatest { modes: List<String> -> // Directly observe settingsRepo.selectedSatModes to ensure real-time sync across screens.
val catnumSet: Set<Int>? = if (modes.isEmpty()) { private val itemsWithModes = settingsRepo.selectedSatModes.flatMapLatest { list: List<String> ->
val catnumSet: Set<Int>? = if (list.isEmpty()) {
null // null = no filtering null // null = no filtering
} else { } else {
val ids = localSource.getIdsWithModes(modes) val ids = localSource.getIdsWithModes(list)
if (ids.isEmpty()) null else ids.toHashSet() if (ids.isEmpty()) null else ids.toHashSet()
} }
currentItems.map { items -> currentItems.map { items ->
@@ -65,7 +65,7 @@ class SelectionRepo(
} }
} }
override fun getCurrentModes() = currentModes.value override fun getCurrentModes() = settingsRepo.selectedSatModes.value
override fun getModesList() = Sources.satelliteModes override fun getModesList() = Sources.satelliteModes
@@ -78,7 +78,6 @@ class SelectionRepo(
} }
override suspend fun setModes(modes: List<String>) { override suspend fun setModes(modes: List<String>) {
currentModes.value = modes
settingsRepo.setSelectedSatModes(modes) settingsRepo.setSelectedSatModes(modes)
} }
@@ -24,7 +24,6 @@ import kotlinx.coroutines.CoroutineDispatcher
import kotlinx.coroutines.withContext import kotlinx.coroutines.withContext
import okhttp3.OkHttpClient import okhttp3.OkHttpClient
import okhttp3.Request import okhttp3.Request
import java.io.ByteArrayInputStream
import java.io.InputStream import java.io.InputStream
class RemoteSource( class RemoteSource(
@@ -46,10 +45,14 @@ class RemoteSource(
override suspend fun getNetworkStream(url: String): InputStream? = withContext(dispatcher) { override suspend fun getNetworkStream(url: String): InputStream? = withContext(dispatcher) {
try { try {
val networkRequest = Request.Builder().url(url).build() val networkRequest = Request.Builder().url(url).build()
httpClient.newCall(networkRequest).execute().use { response -> val response = httpClient.newCall(networkRequest).execute()
if (!response.isSuccessful) return@withContext null if (!response.isSuccessful) {
ByteArrayInputStream(response.body.bytes()) response.close()
return@withContext null
} }
// 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) { } catch (exception: Exception) {
println("RemoteSource network stream exception: $exception") println("RemoteSource network stream exception: $exception")
null null
@@ -19,10 +19,10 @@ package com.rtbishop.look4sat.core.domain.source
object Sources { object Sources {
val satelliteDataUrls = mapOf( val satelliteDataUrls = mapOf(
"All" to "https://celestrak.org/NORAD/elements/gp.php?GROUP=active&FORMAT=csv", "CelesTrak" to "https://celestrak.org/NORAD/elements/gp.php?GROUP=active&FORMAT=csv",
"Amsat" to "https://amsat.org/tle/current/nasabare.txt", "AMSAT" to "https://amsat.org/tle/current/nasabare.txt",
"Classified" to "https://www.mmccants.org/tles/classfd.zip", "McCants Classified" to "https://www.mmccants.org/tles/classfd.zip",
"McCants" to "https://www.mmccants.org/tles/inttles.zip", "McCants Integrated" to "https://www.mmccants.org/tles/inttles.zip",
"R4UAB" to "https://r4uab.ru/satonline.txt", "R4UAB" to "https://r4uab.ru/satonline.txt",
"SatNOGS" to "https://db.satnogs.org/api/tle/?format=3le", "SatNOGS" to "https://db.satnogs.org/api/tle/?format=3le",
"ARISS" to "https://live.ariss.org/iss.txt", "ARISS" to "https://live.ariss.org/iss.txt",
@@ -91,7 +91,7 @@ class DataParser(private val dispatcher: CoroutineDispatcher) {
val line1 = tle[1] val line1 = tle[1]
val line2 = tle[2] val line2 = tle[2]
OrbitalData( OrbitalData(
name = tle[0].trim(), name = tle[0].trim().removePrefix("0 "),
epoch = line1.substring(18, 32).toDouble(), epoch = line1.substring(18, 32).toDouble(),
meanmo = line2.substring(52, 63).toDouble(), meanmo = line2.substring(52, 63).toDouble(),
eccn = line2.substring(26, 33).toDouble() / 1e7, eccn = line2.substring(26, 33).toDouble() / 1e7,
@@ -47,6 +47,7 @@ import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.ModalBottomSheet import androidx.compose.material3.ModalBottomSheet
import androidx.compose.material3.Surface import androidx.compose.material3.Surface
import androidx.compose.material3.Text import androidx.compose.material3.Text
import androidx.compose.material3.SheetValue
import androidx.compose.material3.rememberModalBottomSheetState import androidx.compose.material3.rememberModalBottomSheetState
import androidx.compose.material3.adaptive.currentWindowAdaptiveInfo import androidx.compose.material3.adaptive.currentWindowAdaptiveInfo
import androidx.compose.runtime.Composable import androidx.compose.runtime.Composable
@@ -308,7 +309,8 @@ fun SharedDialog(
titleTextAlign: TextAlign = if (onCancel != null && onAccept != null) TextAlign.Center else TextAlign.Start, titleTextAlign: TextAlign = if (onCancel != null && onAccept != null) TextAlign.Center else TextAlign.Start,
content: @Composable (padding: Dp) -> Unit content: @Composable (padding: Dp) -> Unit
) { ) {
DialogShell(onDismissRequest = onDismissRequest) { padding -> val dismissible = onCancel != null
DialogShell(onDismissRequest = onDismissRequest, dismissible = dismissible) { padding ->
Row( Row(
verticalAlignment = Alignment.CenterVertically, verticalAlignment = Alignment.CenterVertically,
modifier = Modifier modifier = Modifier
@@ -342,10 +344,14 @@ fun SharedDialog(
@Composable @Composable
private fun DialogShell( private fun DialogShell(
onDismissRequest: () -> Unit, onDismissRequest: () -> Unit,
dismissible: Boolean = true,
content: @Composable (padding: Dp) -> Unit content: @Composable (padding: Dp) -> Unit
) { ) {
val padding = LocalSpacing.current.large val padding = LocalSpacing.current.large
val sheetState = rememberModalBottomSheetState(skipPartiallyExpanded = true) val sheetState = rememberModalBottomSheetState(
skipPartiallyExpanded = true,
confirmValueChange = { if (dismissible) true else it != SheetValue.Hidden }
)
val stopSheetFling = remember { val stopSheetFling = remember {
object : NestedScrollConnection { object : NestedScrollConnection {
override suspend fun onPostFling(consumed: Velocity, available: Velocity): Velocity { override suspend fun onPostFling(consumed: Velocity, available: Velocity): Velocity {
@@ -360,7 +366,7 @@ private fun DialogShell(
} }
} }
ModalBottomSheet( ModalBottomSheet(
onDismissRequest = onDismissRequest, onDismissRequest = if (dismissible) onDismissRequest else { {} },
sheetState = sheetState, sheetState = sheetState,
dragHandle = null, dragHandle = null,
shape = RoundedCornerShape(topStart = 24.dp, topEnd = 24.dp), shape = RoundedCornerShape(topStart = 24.dp, topEnd = 24.dp),
@@ -0,0 +1,9 @@
<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>
@@ -1,9 +0,0 @@
<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>
@@ -1,9 +0,0 @@
<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_net_perm_error">Revisa los permisos de red</string>
<string name="prefs_other_title">Otros</string> <string name="prefs_other_title">Otros</string>
<string name="prefs_other_switch_utc">Mostrar hora UTC</string> <string name="prefs_other_switch_utc">Hora UTC</string>
<string name="prefs_other_switch_update">Actualización automática</string> <string name="prefs_other_switch_update">Actualización automática</string>
<string name="prefs_other_switch_sweep">Animación de radar</string> <string name="prefs_other_switch_sweep">Animación radar</string>
<string name="prefs_other_switch_sensors">Sensores para radar</string> <string name="prefs_other_switch_sensors">Sensores</string>
<string name="prefs_other_switch_night_mode">Filtro nocturno rojo</string> <string name="prefs_other_switch_night_mode">Filtro nocturno</string>
<string name="prefs_highlight_title">Resaltado de elevación</string> <string name="prefs_highlight_title">Resaltado de elevación</string>
<string name="prefs_highlight_low">Baja &lt; %1$d°</string> <string name="prefs_highlight_low">Baja &lt; %1$d°</string>
@@ -158,8 +158,8 @@
<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_update">Автообновление данных</string>
<string name="prefs_other_switch_sweep">Анимация радара</string> <string name="prefs_other_switch_sweep">Анимация радара</string>
<string name="prefs_other_switch_sensors">Сенсоры для радара</string> <string name="prefs_other_switch_sensors">Сенсоры</string>
<string name="prefs_other_switch_night_mode">Красный ночной фильтр</string> <string name="prefs_other_switch_night_mode">Ночной фильтр</string>
<string name="prefs_highlight_title">Подсветка высоты</string> <string name="prefs_highlight_title">Подсветка высоты</string>
<string name="prefs_highlight_low">Низко &lt; %1$d°</string> <string name="prefs_highlight_low">Низко &lt; %1$d°</string>
@@ -155,11 +155,11 @@
<string name="prefs_net_perm_error">Network අවසර පරික්‍ෂා කරන්න</string> <string name="prefs_net_perm_error">Network අවසර පරික්‍ෂා කරන්න</string>
<string name="prefs_other_title">වෙනත්</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_update">දත්ත ස්වයං යාවත්කාලීන</string>
<string name="prefs_other_switch_sweep">Radar sweep සජීවිකරණය</string> <string name="prefs_other_switch_sweep">Radar sweep</string>
<string name="prefs_other_switch_sensors">Radar සඳහා සංවේදක භාවිතා කරන්න</string> <string name="prefs_other_switch_sensors">සංවේදක</string>
<string name="prefs_other_switch_night_mode">රතු රාත්‍රී පෙරහන</string> <string name="prefs_other_switch_night_mode">රාත්‍රී පෙරහන</string>
<string name="prefs_highlight_title">උස් උද්දීපනය</string> <string name="prefs_highlight_title">උස් උද්දීපනය</string>
<string name="prefs_highlight_low">අඩු &lt; %1$d°</string> <string name="prefs_highlight_low">අඩු &lt; %1$d°</string>
@@ -187,11 +187,11 @@
<string name="prefs_net_perm_error">Ağ izninizi kontrol edin</string> <string name="prefs_net_perm_error">Ağ izninizi kontrol edin</string>
<string name="prefs_other_title">Diğer</string> <string name="prefs_other_title">Diğer</string>
<string name="prefs_other_switch_utc">UTC saatini göster</string> <string name="prefs_other_switch_utc">UTC saati</string>
<string name="prefs_other_switch_update">Veriyi otomatik güncelle</string> <string name="prefs_other_switch_update">Veriyi otomatik güncelle</string>
<string name="prefs_other_switch_sweep">Radar tarama animasyonu</string> <string name="prefs_other_switch_sweep">Radar taraması</string>
<string name="prefs_other_switch_sensors">Radar için sensör kullan</string> <string name="prefs_other_switch_sensors">Sensör</string>
<string name="prefs_other_switch_night_mode">Kırmızı gece filtresi</string> <string name="prefs_other_switch_night_mode">Gece filtresi</string>
<string name="prefs_highlight_title">Elevasyon vurgusu</string> <string name="prefs_highlight_title">Elevasyon vurgusu</string>
<string name="prefs_highlight_low">Düşük &lt; %1$d°</string> <string name="prefs_highlight_low">Düşük &lt; %1$d°</string>
@@ -158,8 +158,8 @@
<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_update">Автооновлення даних</string>
<string name="prefs_other_switch_sweep">Анімація радара</string> <string name="prefs_other_switch_sweep">Анімація радара</string>
<string name="prefs_other_switch_sensors">Сенсори для радара</string> <string name="prefs_other_switch_sensors">Сенсори</string>
<string name="prefs_other_switch_night_mode">Червоний нічний фільтр</string> <string name="prefs_other_switch_night_mode">Нічний фільтр</string>
<string name="prefs_highlight_title">Підсвічування висоти</string> <string name="prefs_highlight_title">Підсвічування висоти</string>
<string name="prefs_highlight_low">Низько &lt; %1$d°</string> <string name="prefs_highlight_low">Низько &lt; %1$d°</string>
@@ -55,8 +55,16 @@
\n\nPlease update the database at least weekly to get accurate predictions.</string> \n\nPlease update the database at least weekly to get accurate predictions.</string>
<string name="pass_whatsnew_title" translatable="false">What\'s new in Look4Sat</string> <string name="pass_whatsnew_title" translatable="false">What\'s new in Look4Sat</string>
<string name="pass_whatsnew_message" translatable="false"> <string name="pass_whatsnew_message" translatable="false">
* Added required tweaks to support Android 17 (API 37) * Added small tweaks to Sources and strings. Added official ISS source (#225)
\n\n* Added ACCESS_LOCAL_NETWORK permission check to support network data output \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
</string> </string>
<!-- Radar screen --> <!-- Radar screen -->
@@ -187,11 +195,11 @@
<string name="prefs_net_perm_error">Check your network permission</string> <string name="prefs_net_perm_error">Check your network permission</string>
<string name="prefs_other_title">Other</string> <string name="prefs_other_title">Other</string>
<string name="prefs_other_switch_utc">Show UTC time</string> <string name="prefs_other_switch_utc">UTC time</string>
<string name="prefs_other_switch_update">Auto-update data</string> <string name="prefs_other_switch_update">Auto-update data</string>
<string name="prefs_other_switch_sweep">Radar sweep animation</string> <string name="prefs_other_switch_sweep">Radar sweep</string>
<string name="prefs_other_switch_sensors">Use sensors for radar</string> <string name="prefs_other_switch_sensors">Use sensors</string>
<string name="prefs_other_switch_night_mode">Red night filter</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">Radar compass offset</string>
<string name="prefs_other_compass_offset_elev">Radar compass offset (elev)</string> <string name="prefs_other_compass_offset_elev">Radar compass offset (elev)</string>
@@ -1,2 +1,8 @@
* Added required tweaks to support Android 17 (API 37) * Added ARISS source, tweaked SSTV sensitivity #225
* Added ACCESS_LOCAL_NETWORK permission check to support network data output * 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
@@ -34,6 +34,9 @@ import kotlinx.coroutines.delay
import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.StateFlow import kotlinx.coroutines.flow.StateFlow
import kotlinx.coroutines.flow.collectLatest 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.flow.update
import kotlinx.coroutines.isActive import kotlinx.coroutines.isActive
import kotlinx.coroutines.launch import kotlinx.coroutines.launch
@@ -41,6 +44,7 @@ import java.text.SimpleDateFormat
import java.util.Date import java.util.Date
import java.util.Locale import java.util.Locale
import java.util.TimeZone import java.util.TimeZone
import kotlin.time.Duration.Companion.milliseconds
class PassesViewModel( class PassesViewModel(
private val satelliteRepo: ISatelliteRepo, private val satelliteRepo: ISatelliteRepo,
@@ -91,30 +95,34 @@ class PassesViewModel(
_uiState.update { it.copy(modes = modes) } _uiState.update { it.copy(modes = modes) }
} }
} }
// Main tick loop: reacts to new passes, then ticks every second // Tick loop: restarts on passes change, UTC/DeepSpace changes. Grouping/sun-time
// computations run once per restart, progress/countdown are calculated every second.
viewModelScope.launch { viewModelScope.launch {
satelliteRepo.passes.collectLatest { allPasses -> 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) { while (isActive) {
val timeNow = System.currentTimeMillis() 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 processed = computePassProgress(filtered, timeNow)
val (nextPass, nextTime, isAos) = resolveNextPass(processed, timeNow) val (nextPass, nextTime, isAos) = resolveNextPass(processed, timeNow)
val sunTimes = computeSunTimes(processed, isUtc)
val grouped = groupPasses(processed, isUtc)
_uiState.update { _uiState.update {
it.copy( it.copy(
itemsList = processed, itemsList = processed,
groupedPasses = grouped,
sunTimes = sunTimes,
nextPass = nextPass, nextPass = nextPass,
nextTime = nextTime, nextTime = nextTime,
isNextTimeAos = isAos isNextTimeAos = isAos
) )
} }
delay(1000) delay(1000.milliseconds)
} }
} }
} }
@@ -72,6 +72,7 @@ class RadarViewModel(
private var transponders: List<SatRadio> = emptyList() private var transponders: List<SatRadio> = emptyList()
private var sstvDecoder: SstvDecoder? = null private var sstvDecoder: SstvDecoder? = null
private var sstvRecordingJob: Job? = null private var sstvRecordingJob: Job? = null
private var sensorCollectionJob: Job? = null
// Celestial positions change slowly, recompute at most once per minute // Celestial positions change slowly, recompute at most once per minute
private var lastCelestialUpdateMs = 0L private var lastCelestialUpdateMs = 0L
@@ -90,15 +91,15 @@ class RadarViewModel(
val uiState: StateFlow<RadarState> = _uiState val uiState: StateFlow<RadarState> = _uiState
init { init {
collectCompassSensor() collectSettingsChanges() // also handles initial sensor subscription
collectSettingsChanges()
collectPassAndStartTickLoop() collectPassAndStartTickLoop()
collectRadioTrackingState() collectRadioTrackingState()
} }
private fun collectCompassSensor() { // Starts sensor collection if not already running.
if (!settingsRepo.otherSettings.value.stateOfSensors) return private fun startSensorCollection() {
viewModelScope.launch { if (sensorCollectionJob?.isActive == true) return
sensorCollectionJob = viewModelScope.launch {
sensorsRepo.enableSensor() sensorsRepo.enableSensor()
sensorsRepo.sensorData.collect { data -> sensorsRepo.sensorData.collect { data ->
val orientationValues = val orientationValues =
@@ -108,9 +109,16 @@ class RadarViewModel(
} }
} }
// Stops sensor collection and disables the hardware sensor.
private fun stopSensorCollection() {
sensorCollectionJob?.cancel()
sensorCollectionJob = null
sensorsRepo.disableSensor()
}
private fun collectSettingsChanges() { private fun collectSettingsChanges() {
viewModelScope.launch { viewModelScope.launch {
settingsRepo.otherSettings.collectLatest { settings -> settingsRepo.otherSettings.collect { settings ->
compassOffset = settings.radarCompassOffset compassOffset = settings.radarCompassOffset
compassOffsetElev = settings.radarCompassOffsetElev compassOffsetElev = settings.radarCompassOffsetElev
_uiState.update { _uiState.update {
@@ -121,6 +129,11 @@ class RadarViewModel(
shouldFlipRadar = settings.radarCompassOffsetElev < 0f shouldFlipRadar = settings.radarCompassOffsetElev < 0f
) )
} }
// Reactively wire sensor hardware to the compass setting
when {
settings.stateOfSensors -> startSensorCollection()
else -> stopSensorCollection()
}
} }
} }
} }
@@ -219,7 +232,7 @@ class RadarViewModel(
} }
override fun onCleared() { override fun onCleared() {
sensorsRepo.disableSensor() stopSensorCollection()
} }
fun onAction(action: RadarAction) { fun onAction(action: RadarAction) {
@@ -405,7 +418,7 @@ class RadarViewModel(
// - 0.25: Aggressive (original default), amplifies noise; use only for clean inputs // - 0.25: Aggressive (original default), amplifies noise; use only for clean inputs
// - 0.35-0.40: RECOMMENDED for typical phone/mic inputs // - 0.35-0.40: RECOMMENDED for typical phone/mic inputs
// - 0.50-0.60: Conservative, best for noisy environments // - 0.50-0.60: Conservative, best for noisy environments
private const val SSTV_TARGET_RMS = 0.40f // Changed from 0.25 to safer default private const val SSTV_TARGET_RMS = 0.45f // Changed from 0.25 to safer default
// Enable/disable RMS normalization entirely. Set false for direct receiver outputs. // Enable/disable RMS normalization entirely. Set false for direct receiver outputs.
private const val SSTV_ENABLE_RMS_NORMALIZATION = true private const val SSTV_ENABLE_RMS_NORMALIZATION = true
@@ -428,7 +441,7 @@ class RadarViewModel(
// - Look4SatLimited: caps predicted lines to reduce visible vertical collapse. // - Look4SatLimited: caps predicted lines to reduce visible vertical collapse.
// - Robot36Compatible: unlimited predicted lines (legacy Robot36 behavior). // - Robot36Compatible: unlimited predicted lines (legacy Robot36 behavior).
// Ask users to compare both if they report line-skipping/compression artifacts. // Ask users to compare both if they report line-skipping/compression artifacts.
private val SSTV_LINE_RECOVERY_STRATEGY = LineRecoveryStrategy.Look4SatLimited private val SSTV_LINE_RECOVERY_STRATEGY = LineRecoveryStrategy.Robot36Compatible
// Debug: Return scope visualization in frames (increases per-frame memory usage). // Debug: Return scope visualization in frames (increases per-frame memory usage).
private const val SSTV_INCLUDE_SCOPE = false private const val SSTV_INCLUDE_SCOPE = false
@@ -440,7 +453,6 @@ class RadarViewModel(
218.1, 225.7, 233.6, 241.8, 250.3 218.1, 225.7, 233.6, 241.8, 250.3
) )
fun factory(container: IMainContainer) = viewModelFactory { fun factory(container: IMainContainer) = viewModelFactory {
initializer { initializer {
RadarViewModel( RadarViewModel(
@@ -59,6 +59,11 @@ class SatellitesViewModel(
_uiState.update { it.copy(shouldSeeWarning = settings.shouldSeeWarning) } _uiState.update { it.copy(shouldSeeWarning = settings.shouldSeeWarning) }
} }
} }
viewModelScope.launch {
settingsRepo.selectedSatModes.collectLatest { modes ->
_uiState.update { it.copy(currentModes = modes) }
}
}
} }
fun onAction(action: SatellitesAction) { fun onAction(action: SatellitesAction) {
@@ -25,10 +25,12 @@ import androidx.compose.foundation.clickable
import androidx.compose.foundation.layout.Arrangement import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.Row import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.RowScope
import androidx.compose.foundation.layout.Spacer import androidx.compose.foundation.layout.Spacer
import androidx.compose.foundation.layout.fillMaxHeight import androidx.compose.foundation.layout.fillMaxHeight
import androidx.compose.foundation.layout.fillMaxWidth import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.height import androidx.compose.foundation.layout.height
import androidx.compose.foundation.layout.IntrinsicSize
import androidx.compose.foundation.layout.padding import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.layout.width import androidx.compose.foundation.layout.width
import androidx.compose.foundation.lazy.grid.GridCells import androidx.compose.foundation.lazy.grid.GridCells
@@ -220,7 +222,7 @@ private fun SettingsScreen(uiState: SettingsState, onAction: (SettingsAction) ->
version = uiState.appVersionName, version = uiState.appVersionName,
modifier = Modifier.weight(1f) modifier = Modifier.weight(1f)
) )
PrimaryIconCard(onClick = { safeOpenUri(donateUrl) }, resId = R.drawable.ic_pound) PrimaryIconCard(onClick = { safeOpenUri(donateUrl) }, resId = R.drawable.ic_like)
} }
TopBar { TopBar {
Row(modifier = Modifier.weight(1f), horizontalArrangement = Arrangement.spacedBy(6.dp)) { Row(modifier = Modifier.weight(1f), horizontalArrangement = Arrangement.spacedBy(6.dp)) {
@@ -242,7 +244,7 @@ private fun SettingsScreen(uiState: SettingsState, onAction: (SettingsAction) ->
} }
} else { } else {
TopBar { TopBar {
PrimaryIconCard(onClick = { safeOpenUri(donateUrl) }, resId = R.drawable.ic_pound) PrimaryIconCard(onClick = { safeOpenUri(donateUrl) }, resId = R.drawable.ic_like)
TopCard( TopCard(
onClick = { safeOpenUri(appUrl) }, onClick = { safeOpenUri(appUrl) },
version = uiState.appVersionName, version = uiState.appVersionName,
@@ -300,8 +302,22 @@ private fun SettingsScreen(uiState: SettingsState, onAction: (SettingsAction) ->
onRadioControlClick = { dialogs.radioControl = true } onRadioControlClick = { dialogs.radioControl = true }
) )
} }
item { OtherCard(uiState.otherSettings, onAction) } item(span = { GridItemSpan(maxLineSpan) }) {
item { CardCredits() } 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())
}
}
}
} }
} }
} }
@@ -480,46 +496,56 @@ private fun OtherCardPreview() = MainTheme {
shouldSeeWarning = false, shouldSeeWarning = false,
shouldSeeWhatsNew = false shouldSeeWhatsNew = false
) )
OtherCard(settings = values) {} OtherCard(settings = values, onAction = {})
} }
@Composable @Composable
private fun OtherCard(settings: OtherSettings, onAction: (SettingsAction) -> Unit) { private fun OtherCard(settings: OtherSettings, onAction: (SettingsAction) -> Unit, modifier: Modifier = Modifier) {
ElevatedCard( ElevatedCard(modifier = modifier.fillMaxWidth()) {
modifier = Modifier
.fillMaxWidth()
.height(404.dp)
) {
Column(modifier = Modifier.padding(horizontal = 8.dp, vertical = 4.dp)) { Column(modifier = Modifier.padding(horizontal = 8.dp, vertical = 4.dp)) {
Text( Text(
text = stringResource(id = R.string.prefs_other_title), text = stringResource(id = R.string.prefs_other_title),
color = MaterialTheme.colorScheme.primary color = MaterialTheme.colorScheme.primary
) )
SwitchRow(R.string.prefs_other_switch_utc, settings.stateOfUtc) { 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)) onAction(SettingsAction.ToggleUtc(it))
} }
SwitchTile(R.string.prefs_other_switch_night_mode, settings.stateOfNightMode) {
onAction(SettingsAction.ToggleNightMode(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) { SwitchRow(R.string.prefs_other_switch_update, settings.stateOfAutoUpdate) {
onAction(SettingsAction.ToggleUpdate(it)) onAction(SettingsAction.ToggleUpdate(it))
} }
SwitchRow(R.string.prefs_other_switch_sweep, settings.stateOfSweep) { Spacer(modifier = Modifier.height(4.dp))
onAction(SettingsAction.ToggleSweep(it)) // Compass calibration sliders at the bottom
}
SwitchRow(R.string.prefs_other_switch_sensors, settings.stateOfSensors) {
onAction(SettingsAction.ToggleSensor(it))
}
CompassOffsetRow( CompassOffsetRow(
labelResId = R.string.prefs_other_compass_offset, labelResId = R.string.prefs_other_compass_offset,
value = settings.radarCompassOffset, value = settings.radarCompassOffset,
range = -180f..180f range = -180f..180f
) { onAction(SettingsAction.SetRadarCompassOffset(it)) } ) { onAction(SettingsAction.SetRadarCompassOffset(it)) }
Spacer(modifier = Modifier.height(4.dp))
CompassOffsetRow( CompassOffsetRow(
labelResId = R.string.prefs_other_compass_offset_elev, labelResId = R.string.prefs_other_compass_offset_elev,
value = settings.radarCompassOffsetElev, value = settings.radarCompassOffsetElev,
range = -90f..90f range = -90f..90f
) { onAction(SettingsAction.SetRadarCompassOffsetElev(it)) } ) { onAction(SettingsAction.SetRadarCompassOffsetElev(it)) }
SwitchRow(R.string.prefs_other_switch_night_mode, settings.stateOfNightMode) { Spacer(modifier = Modifier.height(4.dp))
onAction(SettingsAction.ToggleNightMode(it))
}
} }
} }
} }
@@ -539,6 +565,7 @@ private fun CompassOffsetRow(
Text(text = stringResource(id = labelResId)) Text(text = stringResource(id = labelResId))
Text(text = "${value.toInt()}°") Text(text = "${value.toInt()}°")
} }
Spacer(modifier = Modifier.height(4.dp))
Slider( Slider(
value = value, value = value,
onValueChange = onValueChange, onValueChange = onValueChange,
@@ -558,6 +585,21 @@ 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 @Composable
private fun formatUpdateTime(updateTime: Long): String { private fun formatUpdateTime(updateTime: Long): String {
val timePattern = stringResource(id = R.string.prefs_updated_time) val timePattern = stringResource(id = R.string.prefs_updated_time)
@@ -592,7 +634,6 @@ private fun CardCredits(modifier: Modifier = Modifier) {
ElevatedCard( ElevatedCard(
modifier = modifier modifier = modifier
.fillMaxWidth() .fillMaxWidth()
.height(404.dp)
) { ) {
Column( Column(
verticalArrangement = Arrangement.SpaceBetween, verticalArrangement = Arrangement.SpaceBetween,
+1 -3
View File
@@ -2,7 +2,5 @@
org.gradle.caching=true org.gradle.caching=true
org.gradle.jvmargs=-Xmx6g -XX:MaxMetaspaceSize=1g -XX:+HeapDumpOnOutOfMemoryError -Dfile.encoding=UTF-8 org.gradle.jvmargs=-Xmx6g -XX:MaxMetaspaceSize=1g -XX:+HeapDumpOnOutOfMemoryError -Dfile.encoding=UTF-8
org.gradle.parallel=true org.gradle.parallel=true
org.gradle.workers.max=4
# Enabled parallel sync for Gradle 9.4+
org.gradle.tooling.parallel=true org.gradle.tooling.parallel=true
org.gradle.workers.max=4
+3 -3
View File
@@ -1,8 +1,8 @@
[versions] [versions]
#noinspection UnusedVersionCatalogEntry #noinspection UnusedVersionCatalogEntry
appVersionCode = "443" appVersionCode = "444"
#noinspection UnusedVersionCatalogEntry #noinspection UnusedVersionCatalogEntry
appVersionName = "4.4.3" appVersionName = "4.4.4"
#noinspection UnusedVersionCatalogEntry #noinspection UnusedVersionCatalogEntry
compileSdk = "37" compileSdk = "37"
#noinspection UnusedVersionCatalogEntry #noinspection UnusedVersionCatalogEntry
@@ -23,7 +23,7 @@ compose-activity = "1.13.0"
compose-lifecycle = "2.11.0" compose-lifecycle = "2.11.0"
compose-navigation3 = "1.1.5" compose-navigation3 = "1.1.5"
google-ksp = "2.3.8" google-ksp = "2.3.10"
kotlin = "2.4.10" kotlin = "2.4.10"
kotlin-coroutines = "1.11.0" kotlin-coroutines = "1.11.0"