Compare commits

...
6 Commits
Author SHA1 Message Date
Arty Bishop 782b438a07 v4.4.8 - Fuzzy satellites search, light theme, various fixes 2026-09-27 14:23:02 +01:00
Michael S fac319b215 Fixed IC-705 frequency read in dual-radio mode (#260) 2026-09-27 10:45:21 +01:00
Ondrej KolonicnyandClaude Opus 4.8 b7ac9c913a Added light theme option suitable for e-ink devices (#256)
Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-09-19 11:35:42 +01:00
atsunatsuandatsunatsu de3d5a6d84 Added fuzzy satellite name search ignoring separators (#255)
Co-authored-by: atsunatsu <atsunatsu@users.noreply.github.com>
2026-09-19 11:13:39 +01:00
atsunatsuandatsunatsu 5e39c945b2 Applied user elevation color thresholds to the radar page (#253)
Co-authored-by: atsunatsu <atsunatsu@users.noreply.github.com>
2026-09-19 11:10:50 +01:00
wty2019wtyanddependabot[bot] 1d07f76ada Fixed the AOS window filter so it follows the UTC setting (#252)
Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-09-19 11:03:05 +01:00
23 changed files with 384 additions and 179 deletions

No files matched your search

@@ -27,6 +27,8 @@ import android.view.View
import androidx.activity.ComponentActivity import androidx.activity.ComponentActivity
import androidx.activity.compose.setContent import androidx.activity.compose.setContent
import androidx.activity.enableEdgeToEdge import androidx.activity.enableEdgeToEdge
import androidx.compose.runtime.collectAsState
import androidx.compose.runtime.getValue
import androidx.core.splashscreen.SplashScreen.Companion.installSplashScreen import androidx.core.splashscreen.SplashScreen.Companion.installSplashScreen
import androidx.lifecycle.lifecycleScope import androidx.lifecycle.lifecycleScope
import com.rtbishop.look4sat.core.domain.repository.IContainerProvider import com.rtbishop.look4sat.core.domain.repository.IContainerProvider
@@ -48,8 +50,10 @@ class MainActivity : ComponentActivity() {
enableEdgeToEdge() enableEdgeToEdge()
super.onCreate(savedInstanceState) super.onCreate(savedInstanceState)
observeNightFilterState() observeNightFilterState()
val settingsRepo = (applicationContext as IContainerProvider).getMainContainer().settingsRepo
setContent { setContent {
MainTheme(isDarkTheme = true) { NavRoot() } val otherSettings by settingsRepo.otherSettings.collectAsState()
MainTheme(isDarkTheme = !otherSettings.stateOfLightTheme) { NavRoot() }
} }
} }
@@ -99,29 +99,43 @@ fun NavRoot(deeplink: String? = null) {
// Reverse: outgoing slides out to the right, incoming drifts in from the left // Reverse: outgoing slides out to the right, incoming drifts in from the left
val popTransition = slideInHorizontally(tween(300)) { -it / 3 } togetherWith val popTransition = slideInHorizontally(tween(300)) { -it / 3 } togetherWith
slideOutHorizontally(tween(300)) { it } slideOutHorizontally(tween(300)) { it }
NavDisplay( // Elevation color thresholds must be provided at the root level so that BOTH
modifier = Modifier.fillMaxSize(), // the tab content (MainScreen) and the root-level RadarDestination see the
backStack = rootBackStack, // user's custom thresholds. RadarDestination is a sibling entry of MainScreen
onBack = navigateBack, // in this NavDisplay, so a provider inside MainScreen never reaches it.
transitionSpec = { pushTransition }, val context = LocalContext.current
popTransitionSpec = { popTransition }, val container = (context.applicationContext as IContainerProvider).getMainContainer()
predictivePopTransitionSpec = { popTransition }, val otherSettings by container.settingsRepo.otherSettings.collectAsStateWithLifecycle()
entryDecorators = listOf( CompositionLocalProvider(
rememberSaveableStateHolderNavEntryDecorator(), LocalElevationThresholds provides ElevationThresholds(
rememberViewModelStoreNavEntryDecorator() low = otherSettings.lowElevation,
), high = otherSettings.highElevation
entryProvider = entryProvider { )
entry<Screen.Passes> { MainScreen(navigateToRadar = navigateToRadar) } ) {
entry<RadarDestination> { NavDisplay(
Surface( modifier = Modifier.fillMaxSize(),
modifier = Modifier.fillMaxSize(), backStack = rootBackStack,
color = MaterialTheme.colorScheme.background onBack = navigateBack,
) { transitionSpec = { pushTransition },
RadarDestination(navigateUp = navigateBack) popTransitionSpec = { popTransition },
predictivePopTransitionSpec = { popTransition },
entryDecorators = listOf(
rememberSaveableStateHolderNavEntryDecorator(),
rememberViewModelStoreNavEntryDecorator()
),
entryProvider = entryProvider {
entry<Screen.Passes> { MainScreen(navigateToRadar = navigateToRadar) }
entry<RadarDestination> {
Surface(
modifier = Modifier.fillMaxSize(),
color = MaterialTheme.colorScheme.background
) {
RadarDestination(navigateUp = navigateBack)
}
} }
} }
} )
) }
} }
@Composable @Composable
@@ -129,7 +143,6 @@ private fun MainScreen(navigateToRadar: () -> Unit = {}) {
val context = LocalContext.current val context = LocalContext.current
val container = (context.applicationContext as IContainerProvider).getMainContainer() val container = (context.applicationContext as IContainerProvider).getMainContainer()
val trackingState by container.radioTrackingService.state.collectAsStateWithLifecycle() val trackingState by container.radioTrackingService.state.collectAsStateWithLifecycle()
val otherSettings by container.settingsRepo.otherSettings.collectAsStateWithLifecycle()
val backStack = rememberNavBackStack(Screen.Passes) val backStack = rememberNavBackStack(Screen.Passes)
val currentKey = backStack.lastOrNull() val currentKey = backStack.lastOrNull()
@@ -139,88 +152,81 @@ private fun MainScreen(navigateToRadar: () -> Unit = {}) {
val navItems = val navItems =
listOf(Screen.Satellites, Screen.Passes, Screen.Status, Screen.Map, Screen.Settings) listOf(Screen.Satellites, Screen.Passes, Screen.Status, Screen.Map, Screen.Settings)
CompositionLocalProvider( NavigationSuiteScaffold(
LocalElevationThresholds provides ElevationThresholds( navigationSuiteItems = {
low = otherSettings.lowElevation, navItems.forEach { screen ->
high = otherSettings.highElevation val isSelected = when (currentKey) {
) is Screen.Satellites -> screen is Screen.Satellites
) { is Screen.Passes -> screen is Screen.Passes
NavigationSuiteScaffold( is Screen.Status -> screen is Screen.Status
navigationSuiteItems = { is Screen.Map -> screen is Screen.Map
navItems.forEach { screen -> is Screen.Settings -> screen is Screen.Settings
val isSelected = when (currentKey) { else -> false
is Screen.Satellites -> screen is Screen.Satellites
is Screen.Passes -> screen is Screen.Passes
is Screen.Status -> screen is Screen.Status
is Screen.Map -> screen is Screen.Map
is Screen.Settings -> screen is Screen.Settings
else -> false
}
item(
icon = {
Icon(
painter = painterResource(screen.iconResId),
contentDescription = stringResource(screen.titleResId)
)
},
label = { Text(stringResource(screen.titleResId)) },
selected = isSelected,
onClick = {
if (isSelected) return@item
while (backStack.size > 1) backStack.removeAt(backStack.size - 1)
if (screen !is Screen.Passes) backStack.add(screen)
}
)
} }
}, item(
navigationSuiteColors = NavigationSuiteDefaults.colors( icon = {
navigationRailContainerColor = MaterialTheme.colorScheme.surfaceContainer Icon(
), painter = painterResource(screen.iconResId),
layoutType = when { contentDescription = stringResource(screen.titleResId)
!hasEnoughHeight() && hasEnoughWidth() -> NavigationSuiteType.NavigationRail )
!hasEnoughWidth() -> NavigationSuiteType.ShortNavigationBarCompact },
else -> NavigationSuiteType.ShortNavigationBarMedium label = { Text(stringResource(screen.titleResId)) },
} selected = isSelected,
) { onClick = {
Column { if (isSelected) return@item
NavDisplay( while (backStack.size > 1) backStack.removeAt(backStack.size - 1)
backStack = backStack, if (screen !is Screen.Passes) backStack.add(screen)
modifier = Modifier.weight(1f),
onBack = navigateBack,
transitionSpec = { fadeTransition },
popTransitionSpec = { fadeTransition },
predictivePopTransitionSpec = { fadeTransition },
entryDecorators = listOf(
rememberSaveableStateHolderNavEntryDecorator(),
rememberViewModelStoreNavEntryDecorator()
),
entryProvider = entryProvider {
entry<Screen.Satellites> {
SatellitesDestination(navigateUp = navigateBack)
}
entry<Screen.Passes> {
PassesDestination { catNum, aosTime ->
container.satelliteRepo.selectPass(catNum, aosTime)
navigateToRadar()
}
}
entry<Screen.Status> { SatStatusDestination() }
entry<Screen.Map> { MapDestination() }
entry<Screen.Settings> { SettingsDestination() }
} }
) )
if (trackingState.isActive) { }
TrackingBanner( },
state = trackingState, navigationSuiteColors = NavigationSuiteDefaults.colors(
onClick = { navigationRailContainerColor = MaterialTheme.colorScheme.surfaceContainer
val pass = trackingState.currentPass ),
if (pass != null) { layoutType = when {
container.satelliteRepo.selectPass(pass.catNum, pass.aosTime) !hasEnoughHeight() && hasEnoughWidth() -> NavigationSuiteType.NavigationRail
navigateToRadar() !hasEnoughWidth() -> NavigationSuiteType.ShortNavigationBarCompact
} else -> NavigationSuiteType.ShortNavigationBarMedium
}
) {
Column {
NavDisplay(
backStack = backStack,
modifier = Modifier.weight(1f),
onBack = navigateBack,
transitionSpec = { fadeTransition },
popTransitionSpec = { fadeTransition },
predictivePopTransitionSpec = { fadeTransition },
entryDecorators = listOf(
rememberSaveableStateHolderNavEntryDecorator(),
rememberViewModelStoreNavEntryDecorator()
),
entryProvider = entryProvider {
entry<Screen.Satellites> {
SatellitesDestination(navigateUp = navigateBack)
}
entry<Screen.Passes> {
PassesDestination { catNum, aosTime ->
container.satelliteRepo.selectPass(catNum, aosTime)
navigateToRadar()
} }
) }
entry<Screen.Status> { SatStatusDestination() }
entry<Screen.Map> { MapDestination() }
entry<Screen.Settings> { SettingsDestination() }
} }
)
if (trackingState.isActive) {
TrackingBanner(
state = trackingState,
onClick = {
val pass = trackingState.currentPass
if (pass != null) {
container.satelliteRepo.selectPass(pass.catNum, pass.aosTime)
navigateToRadar()
}
}
)
} }
} }
} }
@@ -330,12 +330,13 @@ object IcomCivProtocol {
/** /**
* Parse frequency + mode from a CMD_READ_FREQ reply payload. * Parse frequency + mode from a CMD_READ_FREQ reply payload.
* Payload layout after stripping command byte: [5 freq bytes] [mode byte] [filter byte] * Payload layout after stripping command byte: [5 freq bytes]
* The IC-705 does not include a mode byte; if one is present it is parsed, otherwise mode is empty.
*/ */
fun parseFreqModePayload(payload: ByteArray): Pair<Long, String>? { fun parseFreqModePayload(payload: ByteArray): Pair<Long, String>? {
if (payload.size < 6) return null if (payload.size < 5) return null
val freqHz = decodeFrequencyBcd(payload.copyOfRange(0, 5)) val freqHz = decodeFrequencyBcd(payload.copyOfRange(0, 5))
val mode = BYTE_TO_MODE[payload[5]] ?: return null val mode = if (payload.size > 5) BYTE_TO_MODE[payload[5]] ?: return null else ""
return freqHz to mode return freqHz to mode
} }
@@ -35,6 +35,8 @@ 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.combine 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.withContext import kotlinx.coroutines.withContext
import java.util.TimeZone import java.util.TimeZone
@@ -67,8 +69,9 @@ class SatelliteRepo(
override suspend fun initRepository() = withContext(dispatcher) { override suspend fun initRepository() = withContext(dispatcher) {
combine( combine(
settingsRepo.selectedIds, settingsRepo.selectedIds,
settingsRepo.stationPosition settingsRepo.stationPosition,
) { selectedIds, _ -> selectedIds } settingsRepo.otherSettings.map { it.stateOfUtc }.distinctUntilChanged()
) { selectedIds, _, _ -> selectedIds }
.collect { selectedIds -> .collect { selectedIds ->
_satellites.update { localStorage.getEntriesWithIds(selectedIds) } _satellites.update { localStorage.getEntriesWithIds(selectedIds) }
val settings = settingsRepo.passesSettings.value val settings = settingsRepo.passesSettings.value
@@ -176,7 +179,13 @@ class SatelliteRepo(
aosEndMinute: Int, aosEndMinute: Int,
invertAosTimeWindow: Boolean invertAosTimeWindow: Boolean
): Boolean { ): Boolean {
val offsetMillis = TimeZone.getDefault().getOffset(aosTime).toLong() // Must match the timezone used to display pass times (UTC toggle vs device local)
val tz = if (settingsRepo.otherSettings.value.stateOfUtc) {
TimeZone.getTimeZone("UTC")
} else {
TimeZone.getDefault()
}
val offsetMillis = tz.getOffset(aosTime).toLong()
val localMillis = Math.floorMod(aosTime + offsetMillis, 24L * 60L * 60L * 1000L) val localMillis = Math.floorMod(aosTime + offsetMillis, 24L * 60L * 60L * 1000L)
val aosMinute = (localMillis / 60_000L).toInt() val aosMinute = (localMillis / 60_000L).toInt()
val inRange = if (aosStartMinute <= aosEndMinute) { val inRange = if (aosStartMinute <= aosEndMinute) {
@@ -114,12 +114,29 @@ class SelectionRepo(
/** /**
* Filters items by query. Uses toIntOrNull() instead of exception-based flow, * Filters items by query. Uses toIntOrNull() instead of exception-based flow,
* and lowercases the query once up front instead of per-item. * and lowercases the query once up front instead of per-item.
*
* Fuzzy search: the query is split into space-separated tokens and every
* token must appear in the satellite name after both sides are normalized
* (lowercased, non-alphanumeric separators such as dashes, spaces, brackets
* and dots stripped). This makes "ao7" match "AO-7 (AMSAT-OSCAR 7)" and
* "iss zarya" match "ISS (ZARYA)" — exact continuous-substring matching
* previously failed whenever the name contained a separator the query lacked.
*/ */
private fun filterByQuery(items: List<SatItem>, query: String): List<SatItem> { private fun filterByQuery(items: List<SatItem>, query: String): List<SatItem> {
if (query.isBlank()) return items if (query.isBlank()) return items
val catnum = query.toIntOrNull() val catnum = query.toIntOrNull()
if (catnum != null) return items.filter { it.catnum == catnum } if (catnum != null) return items.filter { it.catnum == catnum }
val lowerQuery = query.lowercase() val tokens = query.split(' ')
return items.filter { it.name.lowercase().contains(lowerQuery) } .map { normalizeForSearch(it) }
.filter { it.isNotEmpty() }
if (tokens.isEmpty()) return items
return items.filter { item ->
val normalizedName = normalizeForSearch(item.name)
tokens.all { normalizedName.contains(it) }
}
} }
/** Lowercases and strips all non-alphanumeric chars for fuzzy matching. */
private fun normalizeForSearch(text: String): String =
text.lowercase().filter { it.isLetterOrDigit() }
} }
@@ -0,0 +1,33 @@
package com.rtbishop.look4sat.core.data.framework
import org.junit.Assert.assertEquals
import org.junit.Assert.assertNotNull
import org.junit.Assert.assertNull
import org.junit.Test
class IcomCivProtocolTest {
private fun bytes(vararg values: Int) = ByteArray(values.size) { values[it].toByte() }
@Test
fun parseFreqModePayload_ic705ReadFreqReply() {
// Reply captured from an IC-705 to CMD 0x03: frequency only, no mode byte
val reply = bytes(0xFE, 0xFE, 0xE0, 0xA4, 0x03, 0x60, 0x74, 0x95, 0x45, 0x01, 0xFD)
val response = IcomCivProtocol.parseResponse(reply, IcomCivProtocol.CMD_READ_FREQ)
assertNotNull(response)
assertEquals(145957460L to "", IcomCivProtocol.parseFreqModePayload(response!!.payload))
}
@Test
fun parseFreqModePayload_withModeByte() {
assertEquals(
435611000L to "USB",
IcomCivProtocol.parseFreqModePayload(bytes(0x00, 0x10, 0x61, 0x35, 0x04, 0x01, 0x01))
)
}
@Test
fun parseFreqModePayload_tooShort() {
assertNull(IcomCivProtocol.parseFreqModePayload(bytes(0x60, 0x74, 0x95, 0x45)))
}
}
@@ -84,6 +84,65 @@ class SelectionRepoTest {
assertEquals(listOf(true, false, false), items.map { it.isSelected }) assertEquals(listOf(true, false, false), items.map { it.isSelected })
} }
@Test
fun `query without separators matches name with dashes spaces brackets`() = runTest(dispatcher) {
val repository = repoWithSearchEntries()
val flow = repository.getEntriesFlow()
repository.setQuery("ao7")
val items = flow.first()
// "ao7" is a substring of the normalized "AO-73 (FUNcube-1)" too, so a
// fuzzy search legitimately returns both AO-7 (first) and AO-73. The key
// guarantee is that AO-7 — unreachable before because of dashes/brackets
// — is now found.
assertEquals(7530, items.first().catnum)
assertEquals(listOf(7530, 39444), items.map { it.catnum })
}
@Test
fun `query without separators matches name with only dashes`() = runTest(dispatcher) {
val repository = repoWithSearchEntries()
val flow = repository.getEntriesFlow()
repository.setQuery("fo29")
assertEquals(listOf(99999), flow.first().map { it.catnum })
}
@Test
fun `space-separated tokens all must match in any order`() = runTest(dispatcher) {
val repository = repoWithSearchEntries()
val flow = repository.getEntriesFlow()
repository.setQuery("zarya iss")
assertEquals(listOf(25544), flow.first().map { it.catnum })
}
@Test
fun `partial token query matches substring`() = runTest(dispatcher) {
val repository = repoWithSearchEntries()
val flow = repository.getEntriesFlow()
repository.setQuery("funcube")
assertEquals(listOf(39444), flow.first().map { it.catnum })
}
@Test
fun `unmatched query returns nothing`() = runTest(dispatcher) {
val repository = repoWithSearchEntries()
val flow = repository.getEntriesFlow()
repository.setQuery("zzzznomatch")
assertEquals(emptyList<Int>(), flow.first().map { it.catnum })
}
private fun repoWithSearchEntries(): SelectionRepo {
val localSource = FakeLocalSource(
entries = listOf(
SatItem(25544, "ISS (ZARYA)", false),
SatItem(7530, "AO-7 (AMSAT-OSCAR 7)", false),
SatItem(39444, "AO-73 (FUNcube-1)", false),
SatItem(43803, "JO-97 (BIRDS-3)", false),
SatItem(99999, "FO-29", false)
)
)
return SelectionRepo(dispatcher, localSource, FakeSettingsRepo(selectedModes = emptyList()))
}
private class FakeLocalSource( private class FakeLocalSource(
private val entries: List<SatItem> private val entries: List<SatItem>
) : ILocalSource { ) : ILocalSource {
@@ -61,8 +61,8 @@ fun MainTheme(isDarkTheme: Boolean = isSystemInDarkTheme(), content: @Composable
SideEffect { SideEffect {
val window = (view.context as ComponentActivity).window val window = (view.context as ComponentActivity).window
val insetsController = WindowCompat.getInsetsController(window, view) val insetsController = WindowCompat.getInsetsController(window, view)
insetsController.isAppearanceLightStatusBars = false insetsController.isAppearanceLightStatusBars = !isDarkTheme
insetsController.isAppearanceLightNavigationBars = false insetsController.isAppearanceLightNavigationBars = !isDarkTheme
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) { if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) {
window.isNavigationBarContrastEnforced = false window.isNavigationBarContrastEnforced = false
} }
@@ -161,10 +161,13 @@
<string name="prefs_other_title">Otros</string> <string name="prefs_other_title">Otros</string>
<string name="prefs_other_switch_utc">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">Auto-actualizar</string>
<string name="prefs_other_switch_sweep">Animación radar</string> <string name="prefs_other_switch_sweep">Animación radar</string>
<string name="prefs_other_switch_sensors">Sensores</string> <string name="prefs_other_switch_sensors">Sensores</string>
<string name="prefs_other_switch_night_mode">Filtro nocturno</string> <string name="prefs_other_switch_night_mode">Filtro nocturno</string>
<string name="prefs_other_switch_light_theme">Tema claro</string>
<string name="prefs_other_compass_offset_az">Desfase de brújula (ácimut)</string>
<string name="prefs_other_compass_offset_elev">Desfase de brújula (elevación)</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>
@@ -161,10 +161,13 @@
<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">Анимация радара</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_other_switch_light_theme">Светлая тема</string>
<string name="prefs_other_compass_offset_az">Смещение компаса (азимут)</string>
<string name="prefs_other_compass_offset_elev">Смещение компаса (элевация)</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>
@@ -161,10 +161,13 @@
<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">රේඩාර් පරිලෝකනය</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_other_switch_light_theme">ආලෝක තේමාව</string>
<string name="prefs_other_compass_offset_az">මාලිමා අපගමනය (දිගංශය)</string>
<string name="prefs_other_compass_offset_elev">මාලිමා අපගමනය (උත්තෝලනය)</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>
@@ -193,10 +193,13 @@
<string name="prefs_other_title">Diğer</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 saati</string>
<string name="prefs_other_switch_update">Veriyi otomatik güncelle</string> <string name="prefs_other_switch_update">Otomatik güncelle</string>
<string name="prefs_other_switch_sweep">Radar taraması</string> <string name="prefs_other_switch_sweep">Radar taraması</string>
<string name="prefs_other_switch_sensors">Sensör</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_night_mode">Gece filtresi</string>
<string name="prefs_other_switch_light_theme">Açık tema</string>
<string name="prefs_other_compass_offset_az">Pusula sapması (azimut)</string>
<string name="prefs_other_compass_offset_elev">Pusula sapması (yükseklik açısı)</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>
@@ -161,10 +161,13 @@
<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">Анімація радара</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_other_switch_light_theme">Світла тема</string>
<string name="prefs_other_compass_offset_az">Зміщення компаса (азимут)</string>
<string name="prefs_other_compass_offset_elev">Зміщення компаса (піднесення)</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>
@@ -219,12 +219,13 @@
<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">雷达扫描动画</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_other_compass_offset">雷达罗盘水平偏移量</string> <string name="prefs_other_switch_light_theme">浅色主题</string>
<string name="prefs_other_compass_offset_elev">雷达罗盘俯仰偏移量</string> <string name="prefs_other_compass_offset_az">罗盘偏移量(方位角)</string>
<string name="prefs_other_compass_offset_elev">罗盘偏移量(仰角)</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>
@@ -56,14 +56,12 @@
\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">
* Tweaked sunrise/sunset calculations, added tests by atsunatsu (#241) * Fixed AOS window filter to follow the UTC setting by wty2019wty (#252)
\n\n* Overhauled sources import, added HTTP status by wty2019wty (#242) \n\n* Applied elevation color thresholds to the Radar by atsunatsu (#253)
\nSources now have priority - the top ones define satellite names and transceiver data. \n\n* Added fuzzy satellite search ignoring separators by atsunatsu (#255)
\nYou can enable/disable sources and restore defaults via the Satellite data import dialog. \n\n* Added light theme suitable for e-ink devices by Ondrej Kolonicny (#256)
\n\n* Localized AMSAT status screen for Chinese by atsunatsu (#245) \n\n* Fixed IC-705 frequency read in dual-radio mode by Michael S (#260)
\n\n* Extracted hardcoded UI strings to resources by TianhengZhuang (#247) \n\n* Fixed day/night map overlay calculation and display issue by rt-bishop
\n\n* Added AMSAT satellite status reports submission by atsunatsu (#248)
\n\n* Slightly optimized map performance to reduce ANRs and memory consumption. Zoom in to see satellite labels.
</string> </string>
<!-- AMSAT Status screen --> <!-- AMSAT Status screen -->
@@ -262,12 +260,13 @@
<string name="prefs_other_title">Other</string> <string name="prefs_other_title">Other</string>
<string name="prefs_other_switch_utc">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</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">Use sensors</string> <string name="prefs_other_switch_sensors">Sensors</string>
<string name="prefs_other_switch_night_mode">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_switch_light_theme">Light theme</string>
<string name="prefs_other_compass_offset_elev">Radar compass offset (elev)</string> <string name="prefs_other_compass_offset_az">Compass offset (azimuth)</string>
<string name="prefs_other_compass_offset_elev">Compass offset (elevation)</string>
<string name="prefs_highlight_title">Elevation highlight</string> <string name="prefs_highlight_title">Elevation highlight</string>
<string name="prefs_highlight_low">Low &lt; %1$d°</string> <string name="prefs_highlight_low">Low &lt; %1$d°</string>
@@ -1,5 +1,6 @@
* Tweaked sun calculations, added tests by atsunatsu (#241) * Fixed AOS window filter to follow the UTC setting by wty2019wty (#252)
* Overhauled sources import, added HTTP status by wty2019wty (#242) * Applied elevation color thresholds to the Radar by atsunatsu (#253)
* Localized AMSAT status screen for Chinese by atsunatsu (#245) * Added fuzzy satellite search ignoring separators by atsunatsu (#255)
* Extracted hardcoded UI strings to resources by TianhengZhuang (#247) * Added light theme suitable for e-ink devices by Ondrej Kolonicny (#256)
* Added AMSAT satellite status reports submission by atsunatsu (#248) * Fixed IC-705 frequency read in dual-radio mode by Michael S (#260)
* Fixed day/night map overlay calculation and display issue by rt-bishop
@@ -116,8 +116,9 @@ class MapNightOverlay : Overlay() {
else -> { else -> {
// Terminator crosses this column — find the crossing pixel by binary search // Terminator crosses this column — find the crossing pixel by binary search
val crossY = findCrossingY(proj, x, 0, h - 1, sinSunLat, cosSunLat, cosLonDiff) val isNightAtTop = dotTop < 0
if (dotTop < 0) { val crossY = findCrossingY(proj, x, 0, h - 1, sinSunLat, cosSunLat, cosLonDiff, isNightAtTop)
if (isNightAtTop) {
// Night at top, day at bottom // Night at top, day at bottom
rect.set(x.toFloat(), 0f, (x + stepPx).toFloat(), crossY.toFloat()) rect.set(x.toFloat(), 0f, (x + stepPx).toFloat(), crossY.toFloat())
canvas.drawRect(rect, nightPaint) canvas.drawRect(rect, nightPaint)
@@ -134,7 +135,11 @@ class MapNightOverlay : Overlay() {
/** /**
* Binary-search for the pixel row where the day/night boundary crosses column [x]. * Binary-search for the pixel row where the day/night boundary crosses column [x].
* [yTop] is in day, [yBot] is in night (or vice versa). *
* [isNightAtTop] states which side [yTop] is on. Both polarities occur: the pole nearest
* the sub-solar point is lit, so the terminator runs day-over-night while the sub-solar
* latitude is positive and night-over-day once it turns negative after the September
* equinox. Anchoring lo to the top pixel's side keeps the search valid either way.
*/ */
private fun findCrossingY( private fun findCrossingY(
proj: org.osmdroid.views.Projection, proj: org.osmdroid.views.Projection,
@@ -143,7 +148,8 @@ class MapNightOverlay : Overlay() {
yBot: Int, yBot: Int,
sinSunLat: Double, sinSunLat: Double,
cosSunLat: Double, cosSunLat: Double,
cosLonDiff: Double cosLonDiff: Double,
isNightAtTop: Boolean
): Int { ): Int {
var lo = yTop var lo = yTop
var hi = yBot var hi = yBot
@@ -152,7 +158,8 @@ class MapNightOverlay : Overlay() {
val geo = proj.fromPixels(x, mid, reusableGeoPoint) ?: return mid val geo = proj.fromPixels(x, mid, reusableGeoPoint) ?: return mid
val latRad = Math.toRadians(geo.latitude) val latRad = Math.toRadians(geo.latitude)
val dot = sin(latRad) * sinSunLat + cos(latRad) * cosSunLat * cosLonDiff val dot = sin(latRad) * sinSunLat + cos(latRad) * cosSunLat * cosLonDiff
if (dot < 0) hi = mid else lo = mid // Keep lo on the top pixel's side of the terminator and hi on the opposite side
if ((dot < 0) == isNightAtTop) lo = mid else hi = mid
} }
return (lo + hi) / 2 return (lo + hi) / 2
} }
@@ -122,6 +122,39 @@ private val moonIconPaint = Paint(Paint.ANTI_ALIAS_FLAG).apply {
android.graphics.PorterDuffColorFilter("#E0E0E0".toColorInt(), android.graphics.PorterDuff.Mode.SRC_IN) android.graphics.PorterDuffColorFilter("#E0E0E0".toColorInt(), android.graphics.PorterDuff.Mode.SRC_IN)
} }
/** Accent used for the station marker; recolored per theme so it stays visible on a light map. */
private var mapAccentColor = "#FFE082".toColorInt()
/** The dark-map filter (grayscale + invert) is only applied when NOT in light theme; cached to avoid rebuilds. */
private val darkTileFilter by lazy { createColorFilter() }
/**
* Recolor the shared overlay paints for the current theme. The light OSM tiles are white, so the
* dark-theme amber/grey overlays would vanish; light theme swaps them for dark, high-contrast colors.
* Must run before the set*() overlay builders on each frame, as several icons cache the paint color.
*/
private fun applyMapColors(isLightUi: Boolean) {
if (isLightUi) {
mapAccentColor = "#715C0C".toColorInt() // dark amber (matches app lightScheme primary)
footprintPaint.color = mapAccentColor
textPaint.color = "#1E1B13".toColorInt()
textPaint.setShadowLayer(3f, 3f, 3f, Color.WHITE)
sunIconPaint.colorFilter =
android.graphics.PorterDuffColorFilter(mapAccentColor, android.graphics.PorterDuff.Mode.SRC_IN)
moonIconPaint.colorFilter =
android.graphics.PorterDuffColorFilter("#4C4639".toColorInt(), android.graphics.PorterDuff.Mode.SRC_IN)
} else {
mapAccentColor = "#FFE082".toColorInt()
footprintPaint.color = mapAccentColor
textPaint.color = mapAccentColor
textPaint.setShadowLayer(3f, 3f, 3f, Color.BLACK)
sunIconPaint.colorFilter =
android.graphics.PorterDuffColorFilter(mapAccentColor, android.graphics.PorterDuff.Mode.SRC_IN)
moonIconPaint.colorFilter =
android.graphics.PorterDuffColorFilter("#E0E0E0".toColorInt(), android.graphics.PorterDuff.Mode.SRC_IN)
}
}
@Composable @Composable
fun MapDestination() { fun MapDestination() {
val context = LocalContext.current val context = LocalContext.current
@@ -177,6 +210,8 @@ private fun MapScreen(uiState: MapState, onAction: (MapAction) -> Unit, mapView:
ElevatedCard(modifier = Modifier.weight(1f)) { ElevatedCard(modifier = Modifier.weight(1f)) {
Box(contentAlignment = Alignment.BottomCenter) { Box(contentAlignment = Alignment.BottomCenter) {
AndroidView({ mapView }) { view -> AndroidView({ mapView }) { view ->
applyMapColors(uiState.isLightUi)
view.overlayManager.tilesOverlay.setColorFilter(if (uiState.isLightUi) null else darkTileFilter)
uiState.stationPosition?.let { setStationPosition(it, view) } uiState.stationPosition?.let { setStationPosition(it, view) }
uiState.track?.let { setSatelliteTrack(it, view) } uiState.track?.let { setSatelliteTrack(it, view) }
uiState.footprint?.let { setFootprint(it, view) } uiState.footprint?.let { setFootprint(it, view) }
@@ -288,7 +323,9 @@ private fun setStationPosition(stationPos: GeoPos, mapView: MapView) {
mapView.overlays[OVERLAY_STATION] = Marker(mapView).apply { mapView.overlays[OVERLAY_STATION] = Marker(mapView).apply {
setInfoWindow(null) setInfoWindow(null)
setAnchor(Marker.ANCHOR_CENTER, Marker.ANCHOR_BOTTOM) setAnchor(Marker.ANCHOR_CENTER, Marker.ANCHOR_BOTTOM)
icon = ContextCompat.getDrawable(mapView.context, R.drawable.ic_position) icon = ContextCompat.getDrawable(mapView.context, R.drawable.ic_position)?.apply {
setTint(mapAccentColor)
}
position = GeoPoint(stationPos.latitude, stationPos.longitude) position = GeoPoint(stationPos.latitude, stationPos.longitude)
} }
} }
@@ -585,7 +622,7 @@ private fun rememberMapViewWithLifecycle(): MapView {
zoomController.setVisibility(CustomZoomButtonsController.Visibility.NEVER) zoomController.setVisibility(CustomZoomButtonsController.Visibility.NEVER)
overlayManager.tilesOverlay.loadingBackgroundColor = Color.TRANSPARENT overlayManager.tilesOverlay.loadingBackgroundColor = Color.TRANSPARENT
overlayManager.tilesOverlay.loadingLineColor = Color.TRANSPARENT overlayManager.tilesOverlay.loadingLineColor = Color.TRANSPARENT
overlayManager.tilesOverlay.setColorFilter(createColorFilter()) // Tile color filter (dark inversion vs light) is applied per-theme in the AndroidView update block.
setScrollableAreaLimitLatitude(maxLat, minLat, 0) setScrollableAreaLimitLatitude(maxLat, minLat, 0)
overlays.addAll(Array(OVERLAY_COUNT) { FolderOverlay() }) overlays.addAll(Array(OVERLAY_COUNT) { FolderOverlay() })
} }
@@ -80,7 +80,7 @@ class MapViewModel(
init { init {
viewModelScope.launch { viewModelScope.launch {
settingsRepo.otherSettings.collectLatest { settings -> settingsRepo.otherSettings.collectLatest { settings ->
_uiState.update { it.copy(isUtc = settings.stateOfUtc) } _uiState.update { it.copy(isUtc = settings.stateOfUtc, isLightUi = settings.stateOfLightTheme) }
} }
} }
val (selectedCatNum, _) = satelliteRepo.selectedPass.value val (selectedCatNum, _) = satelliteRepo.selectedPass.value
@@ -495,17 +495,31 @@ private fun OtherCard(settings: OtherSettings, onAction: (SettingsAction) -> Uni
color = MaterialTheme.colorScheme.primary color = MaterialTheme.colorScheme.primary
) )
Spacer(modifier = Modifier.height(4.dp)) Spacer(modifier = Modifier.height(4.dp))
// Display preferences: UTC clock + night filter // Data preferences: auto-update + UTC clock
Row(modifier = Modifier.fillMaxWidth(), horizontalArrangement = Arrangement.spacedBy(8.dp)) { Row(modifier = Modifier.fillMaxWidth(), horizontalArrangement = Arrangement.spacedBy(8.dp)) {
SwitchTile(R.string.prefs_other_switch_update, settings.stateOfAutoUpdate) {
onAction(SettingsAction.ToggleUpdate(it))
}
SwitchTile(R.string.prefs_other_switch_utc, settings.stateOfUtc) { 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) { }
Spacer(modifier = Modifier.height(4.dp))
// Appearance: light theme + night filter
Row(modifier = Modifier.fillMaxWidth(), horizontalArrangement = Arrangement.spacedBy(8.dp)) {
SwitchTile(R.string.prefs_other_switch_light_theme, settings.stateOfLightTheme) {
onAction(SettingsAction.ToggleLightTheme(it))
}
SwitchTile(
R.string.prefs_other_switch_night_mode,
settings.stateOfNightMode,
enabled = !settings.stateOfLightTheme
) {
onAction(SettingsAction.ToggleNightMode(it)) onAction(SettingsAction.ToggleNightMode(it))
} }
} }
Spacer(modifier = Modifier.height(4.dp)) Spacer(modifier = Modifier.height(4.dp))
// Radar behavior: sweep animation + sensor control // Radar behavior: sweep animation + sensors, followed by the compass offset sliders
Row(modifier = Modifier.fillMaxWidth(), horizontalArrangement = Arrangement.spacedBy(8.dp)) { Row(modifier = Modifier.fillMaxWidth(), horizontalArrangement = Arrangement.spacedBy(8.dp)) {
SwitchTile(R.string.prefs_other_switch_sweep, settings.stateOfSweep) { SwitchTile(R.string.prefs_other_switch_sweep, settings.stateOfSweep) {
onAction(SettingsAction.ToggleSweep(it)) onAction(SettingsAction.ToggleSweep(it))
@@ -515,14 +529,9 @@ private fun OtherCard(settings: OtherSettings, onAction: (SettingsAction) -> Uni
} }
} }
Spacer(modifier = Modifier.height(4.dp)) 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 // Compass calibration sliders at the bottom
CompassOffsetRow( CompassOffsetRow(
labelResId = R.string.prefs_other_compass_offset, labelResId = R.string.prefs_other_compass_offset_az,
value = settings.radarCompassOffset, value = settings.radarCompassOffset,
range = -180f..180f range = -180f..180f
) { onAction(SettingsAction.SetRadarCompassOffset(it)) } ) { onAction(SettingsAction.SetRadarCompassOffset(it)) }
@@ -549,7 +558,14 @@ private fun CompassOffsetRow(
verticalAlignment = Alignment.CenterVertically, verticalAlignment = Alignment.CenterVertically,
modifier = Modifier.fillMaxWidth() modifier = Modifier.fillMaxWidth()
) { ) {
Text(text = stringResource(id = labelResId)) Text(
text = stringResource(id = labelResId),
modifier = Modifier
.weight(1f)
.padding(end = 4.dp)
.infiniteMarquee(),
maxLines = 1
)
Text(text = "${value.toInt()}°") Text(text = "${value.toInt()}°")
} }
Spacer(modifier = Modifier.height(4.dp)) Spacer(modifier = Modifier.height(4.dp))
@@ -561,19 +577,12 @@ private fun CompassOffsetRow(
} }
@Composable @Composable
private fun SwitchRow(labelResId: Int, checked: Boolean, onCheckedChange: (Boolean) -> Unit) { private fun RowScope.SwitchTile(
Row( labelResId: Int,
horizontalArrangement = Arrangement.SpaceBetween, checked: Boolean,
verticalAlignment = Alignment.CenterVertically, enabled: Boolean = true,
modifier = Modifier.fillMaxWidth() onCheckedChange: (Boolean) -> Unit
) { ) {
Text(text = stringResource(id = labelResId))
Switch(checked = checked, onCheckedChange = onCheckedChange)
}
}
@Composable
private fun RowScope.SwitchTile(labelResId: Int, checked: Boolean, onCheckedChange: (Boolean) -> Unit) {
Row( Row(
horizontalArrangement = Arrangement.SpaceBetween, horizontalArrangement = Arrangement.SpaceBetween,
verticalAlignment = Alignment.CenterVertically, verticalAlignment = Alignment.CenterVertically,
@@ -581,9 +590,13 @@ private fun RowScope.SwitchTile(labelResId: Int, checked: Boolean, onCheckedChan
) { ) {
Text( Text(
text = stringResource(id = labelResId), text = stringResource(id = labelResId),
modifier = Modifier.weight(1f) modifier = Modifier
.weight(1f)
.padding(end = 4.dp)
.infiniteMarquee(),
maxLines = 1
) )
Switch(checked = checked, onCheckedChange = onCheckedChange) Switch(checked = checked, enabled = enabled, onCheckedChange = onCheckedChange)
} }
} }
@@ -127,7 +127,10 @@ class SettingsViewModel(
is SettingsAction.ToggleUpdate -> settingsRepo.updateOtherSettings { it.copy(stateOfAutoUpdate = action.value) } is SettingsAction.ToggleUpdate -> settingsRepo.updateOtherSettings { it.copy(stateOfAutoUpdate = action.value) }
is SettingsAction.ToggleSweep -> settingsRepo.updateOtherSettings { it.copy(stateOfSweep = action.value) } is SettingsAction.ToggleSweep -> settingsRepo.updateOtherSettings { it.copy(stateOfSweep = action.value) }
is SettingsAction.ToggleSensor -> settingsRepo.updateOtherSettings { it.copy(stateOfSensors = action.value) } is SettingsAction.ToggleSensor -> settingsRepo.updateOtherSettings { it.copy(stateOfSensors = action.value) }
is SettingsAction.ToggleLightTheme -> settingsRepo.updateOtherSettings { it.copy(stateOfLightTheme = action.value) } is SettingsAction.ToggleLightTheme -> settingsRepo.updateOtherSettings {
// Night filter (red-only overlay) is incompatible with the light theme, so turn it off.
it.copy(stateOfLightTheme = action.value, stateOfNightMode = if (action.value) false else it.stateOfNightMode)
}
is SettingsAction.ToggleNightMode -> settingsRepo.updateOtherSettings { it.copy(stateOfNightMode = action.value) } is SettingsAction.ToggleNightMode -> settingsRepo.updateOtherSettings { it.copy(stateOfNightMode = action.value) }
is SettingsAction.SetRadarCompassOffset -> settingsRepo.updateOtherSettings { it.copy(radarCompassOffset = action.value) } is SettingsAction.SetRadarCompassOffset -> settingsRepo.updateOtherSettings { it.copy(radarCompassOffset = action.value) }
is SettingsAction.SetRadarCompassOffsetElev -> settingsRepo.updateOtherSettings { it.copy(radarCompassOffsetElev = action.value) } is SettingsAction.SetRadarCompassOffsetElev -> settingsRepo.updateOtherSettings { it.copy(radarCompassOffsetElev = action.value) }
+5 -5
View File
@@ -1,8 +1,8 @@
[versions] [versions]
#noinspection UnusedVersionCatalogEntry #noinspection UnusedVersionCatalogEntry
appVersionCode = "447" appVersionCode = "448"
#noinspection UnusedVersionCatalogEntry #noinspection UnusedVersionCatalogEntry
appVersionName = "4.4.7" appVersionName = "4.4.8"
#noinspection UnusedVersionCatalogEntry #noinspection UnusedVersionCatalogEntry
compileSdk = "37" compileSdk = "37"
#noinspection UnusedVersionCatalogEntry #noinspection UnusedVersionCatalogEntry
@@ -12,16 +12,16 @@ jdkVersion = "21"
#noinspection UnusedVersionCatalogEntry #noinspection UnusedVersionCatalogEntry
packageName = "com.rtbishop.look4sat" packageName = "com.rtbishop.look4sat"
android-gradle-plugin = "9.4.0" android-gradle-plugin = "9.4.1"
androidx-core-ktx = "1.19.0" androidx-core-ktx = "1.19.1"
androidx-core-splashscreen = "1.2.0" androidx-core-splashscreen = "1.2.0"
androidx-room = "2.8.5" androidx-room = "2.8.5"
compose-bom = "2026.09.00" compose-bom = "2026.09.00"
compose-activity = "1.13.0" compose-activity = "1.13.0"
compose-lifecycle = "2.11.0" compose-lifecycle = "2.11.0"
compose-navigation3 = "1.1.7" compose-navigation3 = "1.2.0"
google-ksp = "2.3.11" google-ksp = "2.3.11"
+2 -2
View File
@@ -1,7 +1,7 @@
#Sat Sep 12 11:29:59 BST 2026 #Sun Sep 27 11:54:01 BST 2026
distributionBase=GRADLE_USER_HOME distributionBase=GRADLE_USER_HOME
distributionPath=wrapper/dists distributionPath=wrapper/dists
distributionUrl=https\://services.gradle.org/distributions/gradle-9.7.1-bin.zip distributionUrl=https\://services.gradle.org/distributions/gradle-9.8.0-bin.zip
networkTimeout=10000 networkTimeout=10000
validateDistributionUrl=true validateDistributionUrl=true
zipStoreBase=GRADLE_USER_HOME zipStoreBase=GRADLE_USER_HOME