mirror of
https://github.com/rt-bishop/Look4Sat.git
synced 2026-10-02 03:15:37 +00:00
Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
782b438a07 | ||
|
|
fac319b215 | ||
|
|
b7ac9c913a | ||
|
|
de3d5a6d84 | ||
|
|
5e39c945b2 | ||
|
|
1d07f76ada |
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()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
+4
-3
@@ -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
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
+12
-3
@@ -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) {
|
||||||
|
|||||||
+19
-2
@@ -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() }
|
||||||
}
|
}
|
||||||
+33
@@ -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)))
|
||||||
|
}
|
||||||
|
}
|
||||||
+59
@@ -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 {
|
||||||
|
|||||||
+2
-2
@@ -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 < %1$d°</string>
|
<string name="prefs_highlight_low">Baja < %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">Низко < %1$d°</string>
|
<string name="prefs_highlight_low">Низко < %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">අඩු < %1$d°</string>
|
<string name="prefs_highlight_low">අඩු < %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 < %1$d°</string>
|
<string name="prefs_highlight_low">Düşük < %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">Низько < %1$d°</string>
|
<string name="prefs_highlight_low">Низько < %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">低 < %1$d°</string>
|
<string name="prefs_highlight_low">低 < %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 < %1$d°</string>
|
<string name="prefs_highlight_low">Low < %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
|
||||||
|
|||||||
+38
-25
@@ -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)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
+4
-1
@@ -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) }
|
||||||
|
|||||||
@@ -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
@@ -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
|
||||||
|
|||||||
Reference in new issue
Block a user