mirror of
https://github.com/atsunatsu/Look4Sat.git
synced 2026-10-04 11:37:22 +00:00
Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
c1277763de |
No files matched your search
@@ -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()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
+2
-19
@@ -114,29 +114,12 @@ 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 tokens = query.split(' ')
|
val lowerQuery = query.lowercase()
|
||||||
.map { normalizeForSearch(it) }
|
return items.filter { it.name.lowercase().contains(lowerQuery) }
|
||||||
.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() }
|
|
||||||
}
|
}
|
||||||
-59
@@ -84,65 +84,6 @@ 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 {
|
||||||
|
|||||||
Reference in new issue
Block a user