From 602b1553a2c099e6e95d3aa9fb9a14c44d036b4d Mon Sep 17 00:00:00 2001 From: Arty Bishop Date: Sat, 1 Aug 2026 14:57:03 +0200 Subject: [PATCH] v4.4.4 - Icom CAT, components, filters and sources tweaks --- .../core/data/repository/SatelliteRepo.kt | 5 +- .../core/data/repository/SelectionRepo.kt | 11 ++- .../look4sat/core/data/source/RemoteSource.kt | 11 ++- .../look4sat/core/domain/source/Sources.kt | 8 +- .../core/domain/utility/DataParser.kt | 2 +- .../look4sat/core/presentation/Components.kt | 12 ++- .../src/main/res/drawable/ic_like.xml | 9 ++ .../src/main/res/drawable/ic_pound.xml | 9 -- .../src/main/res/drawable/ic_tags.xml | 9 -- .../src/main/res/values-es/strings.xml | 8 +- .../src/main/res/values-ru/strings.xml | 4 +- .../src/main/res/values-si/strings.xml | 8 +- .../src/main/res/values-tr/strings.xml | 8 +- .../src/main/res/values-uk/strings.xml | 4 +- .../src/main/res/values/strings.xml | 20 +++-- .../android/en-US/changelogs/default.txt | 10 ++- .../feature/passes/PassesViewModel.kt | 54 +++++++----- .../look4sat/feature/radar/RadarViewModel.kt | 32 ++++--- .../feature/satellites/SatellitesViewModel.kt | 5 ++ .../feature/settings/SettingsScreen.kt | 87 ++++++++++++++----- gradle.properties | 4 +- gradle/libs.versions.toml | 6 +- 22 files changed, 202 insertions(+), 124 deletions(-) create mode 100644 core/presentation/src/main/res/drawable/ic_like.xml delete mode 100644 core/presentation/src/main/res/drawable/ic_pound.xml delete mode 100644 core/presentation/src/main/res/drawable/ic_tags.xml diff --git a/core/data/src/main/java/com/rtbishop/look4sat/core/data/repository/SatelliteRepo.kt b/core/data/src/main/java/com/rtbishop/look4sat/core/data/repository/SatelliteRepo.kt index 088ca67d..def16e02 100644 --- a/core/data/src/main/java/com/rtbishop/look4sat/core/data/repository/SatelliteRepo.kt +++ b/core/data/src/main/java/com/rtbishop/look4sat/core/data/repository/SatelliteRepo.kt @@ -37,6 +37,7 @@ 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, @@ -158,7 +159,7 @@ class SatelliteRepo( } } newPasses.sortBy { it.aosTime } - delay(1000) // Simulate loading time for better UX + delay(1000.milliseconds) // Simulate loading time for better UX _passes.update { newPasses } } _isCalculating.value = false @@ -176,7 +177,7 @@ class SatelliteRepo( val inRange = if (aosStartMinute <= aosEndMinute) { aosMinute in aosStartMinute..aosEndMinute } else { - aosMinute >= aosStartMinute || aosMinute <= aosEndMinute + aosMinute !in (aosEndMinute + 1)..>(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. - private val itemsWithModes = currentModes.flatMapLatest { modes: List -> - val catnumSet: Set? = if (modes.isEmpty()) { + // Directly observe settingsRepo.selectedSatModes to ensure real-time sync across screens. + private val itemsWithModes = settingsRepo.selectedSatModes.flatMapLatest { list: List -> + val catnumSet: Set? = if (list.isEmpty()) { null // null = no filtering } else { - val ids = localSource.getIdsWithModes(modes) + val ids = localSource.getIdsWithModes(list) if (ids.isEmpty()) null else ids.toHashSet() } 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 @@ -78,7 +78,6 @@ class SelectionRepo( } override suspend fun setModes(modes: List) { - currentModes.value = modes settingsRepo.setSelectedSatModes(modes) } diff --git a/core/data/src/main/java/com/rtbishop/look4sat/core/data/source/RemoteSource.kt b/core/data/src/main/java/com/rtbishop/look4sat/core/data/source/RemoteSource.kt index 300e930d..2678abbf 100644 --- a/core/data/src/main/java/com/rtbishop/look4sat/core/data/source/RemoteSource.kt +++ b/core/data/src/main/java/com/rtbishop/look4sat/core/data/source/RemoteSource.kt @@ -24,7 +24,6 @@ import kotlinx.coroutines.CoroutineDispatcher import kotlinx.coroutines.withContext import okhttp3.OkHttpClient import okhttp3.Request -import java.io.ByteArrayInputStream import java.io.InputStream class RemoteSource( @@ -46,10 +45,14 @@ class RemoteSource( override suspend fun getNetworkStream(url: String): InputStream? = withContext(dispatcher) { try { val networkRequest = Request.Builder().url(url).build() - httpClient.newCall(networkRequest).execute().use { response -> - if (!response.isSuccessful) return@withContext null - ByteArrayInputStream(response.body.bytes()) + val response = httpClient.newCall(networkRequest).execute() + if (!response.isSuccessful) { + 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) { println("RemoteSource network stream exception: $exception") null diff --git a/core/domain/src/main/java/com/rtbishop/look4sat/core/domain/source/Sources.kt b/core/domain/src/main/java/com/rtbishop/look4sat/core/domain/source/Sources.kt index 52dc76f1..77e150a4 100644 --- a/core/domain/src/main/java/com/rtbishop/look4sat/core/domain/source/Sources.kt +++ b/core/domain/src/main/java/com/rtbishop/look4sat/core/domain/source/Sources.kt @@ -19,10 +19,10 @@ package com.rtbishop.look4sat.core.domain.source object Sources { val satelliteDataUrls = mapOf( - "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", + "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", "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", diff --git a/core/domain/src/main/java/com/rtbishop/look4sat/core/domain/utility/DataParser.kt b/core/domain/src/main/java/com/rtbishop/look4sat/core/domain/utility/DataParser.kt index f82d2348..a2d52d3a 100644 --- a/core/domain/src/main/java/com/rtbishop/look4sat/core/domain/utility/DataParser.kt +++ b/core/domain/src/main/java/com/rtbishop/look4sat/core/domain/utility/DataParser.kt @@ -91,7 +91,7 @@ class DataParser(private val dispatcher: CoroutineDispatcher) { val line1 = tle[1] val line2 = tle[2] OrbitalData( - name = tle[0].trim(), + name = tle[0].trim().removePrefix("0 "), epoch = line1.substring(18, 32).toDouble(), meanmo = line2.substring(52, 63).toDouble(), eccn = line2.substring(26, 33).toDouble() / 1e7, diff --git a/core/presentation/src/main/java/com/rtbishop/look4sat/core/presentation/Components.kt b/core/presentation/src/main/java/com/rtbishop/look4sat/core/presentation/Components.kt index 04010d41..30c07d3e 100644 --- a/core/presentation/src/main/java/com/rtbishop/look4sat/core/presentation/Components.kt +++ b/core/presentation/src/main/java/com/rtbishop/look4sat/core/presentation/Components.kt @@ -47,6 +47,7 @@ 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 @@ -308,7 +309,8 @@ fun SharedDialog( titleTextAlign: TextAlign = if (onCancel != null && onAccept != null) TextAlign.Center else TextAlign.Start, content: @Composable (padding: Dp) -> Unit ) { - DialogShell(onDismissRequest = onDismissRequest) { padding -> + val dismissible = onCancel != null + DialogShell(onDismissRequest = onDismissRequest, dismissible = dismissible) { padding -> Row( verticalAlignment = Alignment.CenterVertically, modifier = Modifier @@ -342,10 +344,14 @@ 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) + val sheetState = rememberModalBottomSheetState( + skipPartiallyExpanded = true, + confirmValueChange = { if (dismissible) true else it != SheetValue.Hidden } + ) val stopSheetFling = remember { object : NestedScrollConnection { override suspend fun onPostFling(consumed: Velocity, available: Velocity): Velocity { @@ -360,7 +366,7 @@ private fun DialogShell( } } ModalBottomSheet( - onDismissRequest = onDismissRequest, + onDismissRequest = if (dismissible) onDismissRequest else { {} }, sheetState = sheetState, dragHandle = null, shape = RoundedCornerShape(topStart = 24.dp, topEnd = 24.dp), diff --git a/core/presentation/src/main/res/drawable/ic_like.xml b/core/presentation/src/main/res/drawable/ic_like.xml new file mode 100644 index 00000000..14595ca4 --- /dev/null +++ b/core/presentation/src/main/res/drawable/ic_like.xml @@ -0,0 +1,9 @@ + + + diff --git a/core/presentation/src/main/res/drawable/ic_pound.xml b/core/presentation/src/main/res/drawable/ic_pound.xml deleted file mode 100644 index ae8a8732..00000000 --- a/core/presentation/src/main/res/drawable/ic_pound.xml +++ /dev/null @@ -1,9 +0,0 @@ - - - diff --git a/core/presentation/src/main/res/drawable/ic_tags.xml b/core/presentation/src/main/res/drawable/ic_tags.xml deleted file mode 100644 index ad7640ae..00000000 --- a/core/presentation/src/main/res/drawable/ic_tags.xml +++ /dev/null @@ -1,9 +0,0 @@ - - - diff --git a/core/presentation/src/main/res/values-es/strings.xml b/core/presentation/src/main/res/values-es/strings.xml index f2f980ff..095ed335 100644 --- a/core/presentation/src/main/res/values-es/strings.xml +++ b/core/presentation/src/main/res/values-es/strings.xml @@ -155,11 +155,11 @@ Revisa los permisos de red Otros - Mostrar hora UTC + Hora UTC Actualización automática - Animación de radar - Sensores para radar - Filtro nocturno rojo + Animación radar + Sensores + Filtro nocturno Resaltado de elevación Baja < %1$d° diff --git a/core/presentation/src/main/res/values-ru/strings.xml b/core/presentation/src/main/res/values-ru/strings.xml index 3641e54e..4c73ec04 100644 --- a/core/presentation/src/main/res/values-ru/strings.xml +++ b/core/presentation/src/main/res/values-ru/strings.xml @@ -158,8 +158,8 @@ Время в UTC Автообновление данных Анимация радара - Сенсоры для радара - Красный ночной фильтр + Сенсоры + Ночной фильтр Подсветка высоты Низко < %1$d° diff --git a/core/presentation/src/main/res/values-si/strings.xml b/core/presentation/src/main/res/values-si/strings.xml index 11482985..32af6348 100644 --- a/core/presentation/src/main/res/values-si/strings.xml +++ b/core/presentation/src/main/res/values-si/strings.xml @@ -155,11 +155,11 @@ Network අවසර පරික්‍ෂා කරන්න වෙනත් - UTC වේලාව පෙන්වන්න + UTC වේලාව දත්ත ස්වයං යාවත්කාලීන - Radar sweep සජීවිකරණය - Radar සඳහා සංවේදක භාවිතා කරන්න - රතු රාත්‍රී පෙරහන + Radar sweep + සංවේදක + රාත්‍රී පෙරහන උස් උද්දීපනය අඩු < %1$d° diff --git a/core/presentation/src/main/res/values-tr/strings.xml b/core/presentation/src/main/res/values-tr/strings.xml index 72997597..1f96a4aa 100644 --- a/core/presentation/src/main/res/values-tr/strings.xml +++ b/core/presentation/src/main/res/values-tr/strings.xml @@ -187,11 +187,11 @@ Ağ izninizi kontrol edin Diğer - UTC saatini göster + UTC saati Veriyi otomatik güncelle - Radar tarama animasyonu - Radar için sensör kullan - Kırmızı gece filtresi + Radar taraması + Sensör + Gece filtresi Elevasyon vurgusu Düşük < %1$d° diff --git a/core/presentation/src/main/res/values-uk/strings.xml b/core/presentation/src/main/res/values-uk/strings.xml index bde698d7..8c1b66bf 100644 --- a/core/presentation/src/main/res/values-uk/strings.xml +++ b/core/presentation/src/main/res/values-uk/strings.xml @@ -158,8 +158,8 @@ Час в UTC Автооновлення даних Анімація радара - Сенсори для радара - Червоний нічний фільтр + Сенсори + Нічний фільтр Підсвічування висоти Низько < %1$d° diff --git a/core/presentation/src/main/res/values/strings.xml b/core/presentation/src/main/res/values/strings.xml index f88540cb..6cc651a1 100644 --- a/core/presentation/src/main/res/values/strings.xml +++ b/core/presentation/src/main/res/values/strings.xml @@ -55,8 +55,16 @@ \n\nPlease update the database at least weekly to get accurate predictions. What\'s new in Look4Sat - * Added required tweaks to support Android 17 (API 37) - \n\n* Added ACCESS_LOCAL_NETWORK permission check to support network data output + * 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 @@ -187,11 +195,11 @@ Check your network permission Other - Show UTC time + UTC time Auto-update data - Radar sweep animation - Use sensors for radar - Red night filter + Radar sweep + Use sensors + Night filter Radar compass offset Radar compass offset (elev) diff --git a/fastlane/metadata/android/en-US/changelogs/default.txt b/fastlane/metadata/android/en-US/changelogs/default.txt index 751fc60a..6997002f 100644 --- a/fastlane/metadata/android/en-US/changelogs/default.txt +++ b/fastlane/metadata/android/en-US/changelogs/default.txt @@ -1,2 +1,8 @@ -* Added required tweaks to support Android 17 (API 37) -* Added ACCESS_LOCAL_NETWORK permission check to support network data output +* 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 diff --git a/feature/passes/src/main/java/com/rtbishop/look4sat/feature/passes/PassesViewModel.kt b/feature/passes/src/main/java/com/rtbishop/look4sat/feature/passes/PassesViewModel.kt index 284e7456..39d2459e 100644 --- a/feature/passes/src/main/java/com/rtbishop/look4sat/feature/passes/PassesViewModel.kt +++ b/feature/passes/src/main/java/com/rtbishop/look4sat/feature/passes/PassesViewModel.kt @@ -34,6 +34,9 @@ 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 @@ -41,6 +44,7 @@ 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, @@ -91,32 +95,36 @@ class PassesViewModel( _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 { - 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 - ) + 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) } - delay(1000) } - } } } diff --git a/feature/radar/src/main/java/com/rtbishop/look4sat/feature/radar/RadarViewModel.kt b/feature/radar/src/main/java/com/rtbishop/look4sat/feature/radar/RadarViewModel.kt index 83d214fb..be89d4b6 100644 --- a/feature/radar/src/main/java/com/rtbishop/look4sat/feature/radar/RadarViewModel.kt +++ b/feature/radar/src/main/java/com/rtbishop/look4sat/feature/radar/RadarViewModel.kt @@ -72,6 +72,7 @@ class RadarViewModel( private var transponders: List = 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 @@ -90,15 +91,15 @@ class RadarViewModel( val uiState: StateFlow = _uiState init { - collectCompassSensor() - collectSettingsChanges() + collectSettingsChanges() // also handles initial sensor subscription collectPassAndStartTickLoop() collectRadioTrackingState() } - private fun collectCompassSensor() { - if (!settingsRepo.otherSettings.value.stateOfSensors) return - viewModelScope.launch { + // Starts sensor collection if not already running. + private fun startSensorCollection() { + if (sensorCollectionJob?.isActive == true) return + sensorCollectionJob = viewModelScope.launch { sensorsRepo.enableSensor() sensorsRepo.sensorData.collect { data -> 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() { viewModelScope.launch { - settingsRepo.otherSettings.collectLatest { settings -> + settingsRepo.otherSettings.collect { settings -> compassOffset = settings.radarCompassOffset compassOffsetElev = settings.radarCompassOffsetElev _uiState.update { @@ -121,6 +129,11 @@ class RadarViewModel( 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() { - sensorsRepo.disableSensor() + stopSensorCollection() } fun onAction(action: RadarAction) { @@ -405,7 +418,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.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. private const val SSTV_ENABLE_RMS_NORMALIZATION = true @@ -428,7 +441,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.Look4SatLimited + private val SSTV_LINE_RECOVERY_STRATEGY = LineRecoveryStrategy.Robot36Compatible // Debug: Return scope visualization in frames (increases per-frame memory usage). private const val SSTV_INCLUDE_SCOPE = false @@ -440,7 +453,6 @@ class RadarViewModel( 218.1, 225.7, 233.6, 241.8, 250.3 ) - fun factory(container: IMainContainer) = viewModelFactory { initializer { RadarViewModel( diff --git a/feature/satellites/src/main/java/com/rtbishop/look4sat/feature/satellites/SatellitesViewModel.kt b/feature/satellites/src/main/java/com/rtbishop/look4sat/feature/satellites/SatellitesViewModel.kt index 72bf17fa..84ad23c8 100644 --- a/feature/satellites/src/main/java/com/rtbishop/look4sat/feature/satellites/SatellitesViewModel.kt +++ b/feature/satellites/src/main/java/com/rtbishop/look4sat/feature/satellites/SatellitesViewModel.kt @@ -59,6 +59,11 @@ 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) { diff --git a/feature/settings/src/main/java/com/rtbishop/look4sat/feature/settings/SettingsScreen.kt b/feature/settings/src/main/java/com/rtbishop/look4sat/feature/settings/SettingsScreen.kt index a32393ac..53594bfb 100644 --- a/feature/settings/src/main/java/com/rtbishop/look4sat/feature/settings/SettingsScreen.kt +++ b/feature/settings/src/main/java/com/rtbishop/look4sat/feature/settings/SettingsScreen.kt @@ -25,10 +25,12 @@ 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 @@ -220,7 +222,7 @@ private fun SettingsScreen(uiState: SettingsState, onAction: (SettingsAction) -> version = uiState.appVersionName, modifier = Modifier.weight(1f) ) - PrimaryIconCard(onClick = { safeOpenUri(donateUrl) }, resId = R.drawable.ic_pound) + PrimaryIconCard(onClick = { safeOpenUri(donateUrl) }, resId = R.drawable.ic_like) } TopBar { Row(modifier = Modifier.weight(1f), horizontalArrangement = Arrangement.spacedBy(6.dp)) { @@ -242,7 +244,7 @@ private fun SettingsScreen(uiState: SettingsState, onAction: (SettingsAction) -> } } else { TopBar { - PrimaryIconCard(onClick = { safeOpenUri(donateUrl) }, resId = R.drawable.ic_pound) + PrimaryIconCard(onClick = { safeOpenUri(donateUrl) }, resId = R.drawable.ic_like) TopCard( onClick = { safeOpenUri(appUrl) }, version = uiState.appVersionName, @@ -300,8 +302,22 @@ private fun SettingsScreen(uiState: SettingsState, onAction: (SettingsAction) -> onRadioControlClick = { dialogs.radioControl = true } ) } - item { OtherCard(uiState.otherSettings, onAction) } - item { CardCredits() } + 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()) + } + } + } } } } @@ -480,46 +496,56 @@ private fun OtherCardPreview() = MainTheme { shouldSeeWarning = false, shouldSeeWhatsNew = false ) - OtherCard(settings = values) {} + OtherCard(settings = values, onAction = {}) } @Composable -private fun OtherCard(settings: OtherSettings, onAction: (SettingsAction) -> Unit) { - ElevatedCard( - modifier = Modifier - .fillMaxWidth() - .height(404.dp) - ) { +private fun OtherCard(settings: OtherSettings, onAction: (SettingsAction) -> Unit, modifier: Modifier = Modifier) { + ElevatedCard(modifier = modifier.fillMaxWidth()) { Column(modifier = Modifier.padding(horizontal = 8.dp, vertical = 4.dp)) { Text( text = stringResource(id = R.string.prefs_other_title), color = MaterialTheme.colorScheme.primary ) - SwitchRow(R.string.prefs_other_switch_utc, settings.stateOfUtc) { - onAction(SettingsAction.ToggleUtc(it)) + 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)) + } } + 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)) } - 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)) - } + 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)) } - SwitchRow(R.string.prefs_other_switch_night_mode, settings.stateOfNightMode) { - onAction(SettingsAction.ToggleNightMode(it)) - } + Spacer(modifier = Modifier.height(4.dp)) } } } @@ -539,6 +565,7 @@ private fun CompassOffsetRow( Text(text = stringResource(id = labelResId)) Text(text = "${value.toInt()}°") } + Spacer(modifier = Modifier.height(4.dp)) Slider( value = value, 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 private fun formatUpdateTime(updateTime: Long): String { val timePattern = stringResource(id = R.string.prefs_updated_time) @@ -592,7 +634,6 @@ private fun CardCredits(modifier: Modifier = Modifier) { ElevatedCard( modifier = modifier .fillMaxWidth() - .height(404.dp) ) { Column( verticalArrangement = Arrangement.SpaceBetween, diff --git a/gradle.properties b/gradle.properties index 8ecdada7..c72e9bb5 100644 --- a/gradle.properties +++ b/gradle.properties @@ -2,7 +2,5 @@ org.gradle.caching=true org.gradle.jvmargs=-Xmx6g -XX:MaxMetaspaceSize=1g -XX:+HeapDumpOnOutOfMemoryError -Dfile.encoding=UTF-8 org.gradle.parallel=true -org.gradle.workers.max=4 - -# Enabled parallel sync for Gradle 9.4+ org.gradle.tooling.parallel=true +org.gradle.workers.max=4 diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index 6f817fc0..cd816a80 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -1,8 +1,8 @@ [versions] #noinspection UnusedVersionCatalogEntry -appVersionCode = "443" +appVersionCode = "444" #noinspection UnusedVersionCatalogEntry -appVersionName = "4.4.3" +appVersionName = "4.4.4" #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.8" +google-ksp = "2.3.10" kotlin = "2.4.10" kotlin-coroutines = "1.11.0"