diff --git a/core/data/src/main/java/com/rtbishop/look4sat/core/data/repository/SelectionRepo.kt b/core/data/src/main/java/com/rtbishop/look4sat/core/data/repository/SelectionRepo.kt index 0a2e7100..30c160b2 100644 --- a/core/data/src/main/java/com/rtbishop/look4sat/core/data/repository/SelectionRepo.kt +++ b/core/data/src/main/java/com/rtbishop/look4sat/core/data/repository/SelectionRepo.kt @@ -43,11 +43,13 @@ class SelectionRepo( // Resolve type IDs once when types change, then filter items reactively. // The HashSet gives O(1) catnum lookups instead of O(n) with a List. + // The three virtual types ("AMSAT Live FM", "AMSAT Live Linear", "Live SSTV") + // are resolved from live transponder data instead of SharedPreferences. private val itemsWithTypes = currentTypes.flatMapLatest { types: List -> val catnumSet: Set? = if (types.isEmpty()) { null // null = no filtering } else { - val ids = settingsRepo.getSatelliteTypesIds(types) + val ids = resolveTypeIds(types) if (ids.isEmpty()) null else ids.toHashSet() } currentItems.map { items -> @@ -61,8 +63,28 @@ class SelectionRepo( override fun getCurrentTypes() = currentTypes.value - override fun getTypesList() = Sources.satelliteDataUrls.keys.sorted().toMutableList().apply { - removeAt(0) + /** + * Resolves a list of type names to satellite catnums. The three virtual + * transponder/activity types are resolved from live data; all other types + * come from the per-type ID lists persisted by [ISettingsRepo]. + */ + private suspend fun resolveTypeIds(types: List): List { + val idsSet = mutableSetOf() + types.forEach { type -> + when (type) { + "AMSAT Live FM" -> idsSet.addAll(settingsRepo.getAmSatFmCatnums()) + "AMSAT Live Linear" -> idsSet.addAll(settingsRepo.getAmSatLinearCatnums()) + "Live SSTV" -> idsSet.addAll(localSource.getIdsWithModes(listOf("SSTV"))) + else -> idsSet.addAll(settingsRepo.getSatelliteTypesIds(listOf(type))) + } + } + return idsSet.toList() + } + + override fun getTypesList() = buildList { + // 三个转发器/活动虚拟类型排在最前. + addAll(listOf("AMSAT Live FM", "AMSAT Live Linear", "Live SSTV")) + addAll(Sources.satelliteDataUrls.keys.sorted().toMutableList().apply { removeAt(0) }) } override suspend fun getEntriesFlow() = withContext(dispatcher) { diff --git a/core/data/src/test/java/com/rtbishop/look4sat/core/data/repository/SelectionRepoSearchTest.kt b/core/data/src/test/java/com/rtbishop/look4sat/core/data/repository/SelectionRepoSearchTest.kt index 5af97f0d..0a9daedb 100644 --- a/core/data/src/test/java/com/rtbishop/look4sat/core/data/repository/SelectionRepoSearchTest.kt +++ b/core/data/src/test/java/com/rtbishop/look4sat/core/data/repository/SelectionRepoSearchTest.kt @@ -112,22 +112,86 @@ class SelectionRepoSearchTest { assertTrue(repo.getEntriesFlow().first().isEmpty()) } - private fun createRepo(items: List): ISelectionRepo { + @Test + fun `FM virtual type shows only AMSAT FM list satellites`() = runTest { + val repo = createRepo( + items = sampleItems, + amSatFm = setOf(25544, 39444) + ) + repo.setTypes(listOf("AMSAT Live FM")) + val results = repo.getEntriesFlow().first() + assertEquals(listOf(25544, 39444), results.map { it.catnum }) + } + + @Test + fun `SSTV virtual type shows only satellites with SSTV radios`() = runTest { + val repo = createRepo( + items = sampleItems, + sstvIds = listOf(43803) + ) + repo.setTypes(listOf("Live SSTV")) + val results = repo.getEntriesFlow().first() + assertEquals(listOf(43803), results.map { it.catnum }) + } + + @Test + fun `multiple virtual types union their satellites`() = runTest { + val repo = createRepo( + items = sampleItems, + amSatFm = setOf(25544), + amSatLinear = setOf(7530), + sstvIds = listOf(43803) + ) + repo.setTypes(listOf("AMSAT Live FM", "Live SSTV")) + val results = repo.getEntriesFlow().first() + assertEquals(setOf(25544, 43803), results.map { it.catnum }.toSet()) + } + + @Test + fun `clearing types restores full list`() = runTest { + val repo = createRepo(items = sampleItems, amSatFm = setOf(25544)) + repo.setTypes(listOf("AMSAT Live FM")) + assertTrue(repo.getEntriesFlow().first().isNotEmpty()) + repo.setTypes(emptyList()) + assertEquals(sampleItems.size, repo.getEntriesFlow().first().size) + } + + @Test + fun `types list has virtual transponder types first and keeps All`() { + val repo = createRepo(sampleItems) + val types = repo.getTypesList() + assertTrue(types.size >= 4) + assertEquals("AMSAT Live FM", types[0]) + assertEquals("AMSAT Live Linear", types[1]) + assertEquals("Live SSTV", types[2]) + // "All" must be kept in the list (first original type shown after virtual ones). + assertTrue("All" in types) + } + + private fun createRepo( + items: List, + amSatFm: Set = emptySet(), + amSatLinear: Set = emptySet(), + sstvIds: List = emptyList() + ): ISelectionRepo { return SelectionRepo( dispatcher = Dispatchers.Unconfined, - localSource = FakeLocalSourceForSearch(items), - settingsRepo = FakeSettingsRepoForSearch() + localSource = FakeLocalSourceForSearch(items, sstvIds), + settingsRepo = FakeSettingsRepoForSearch(amSatFm, amSatLinear) ) } } -private class FakeLocalSourceForSearch(private val items: List) : ILocalSource { +private class FakeLocalSourceForSearch( + private val items: List, + private val sstvIds: List = emptyList() +) : ILocalSource { override suspend fun getEntriesTotal(): Int = items.size override suspend fun getEntriesList(): List = items override suspend fun getEntriesWithIds(ids: List): List = emptyList() override suspend fun insertEntries(entries: List) = Unit override suspend fun deleteEntries() = Unit - override suspend fun getIdsWithModes(modes: List): List = emptyList() + override suspend fun getIdsWithModes(modes: List): List = sstvIds override suspend fun getRadiosTotal(): Int = 0 override suspend fun getRadiosWithId(id: Int): List = emptyList() override suspend fun insertRadios(radios: List, isCustom: Boolean) = Unit @@ -135,7 +199,10 @@ private class FakeLocalSourceForSearch(private val items: List) : ILoca override suspend fun deleteRadios() = Unit } -private class FakeSettingsRepoForSearch : ISettingsRepo { +private class FakeSettingsRepoForSearch( + private val amSatFm: Set = emptySet(), + private val amSatLinear: Set = emptySet() +) : ISettingsRepo { override val appVersionName: String = "test" override val selectedIds: StateFlow> = MutableStateFlow(emptyList()) override val selectedTypes: StateFlow> = MutableStateFlow(emptyList()) @@ -177,6 +244,9 @@ private class FakeSettingsRepoForSearch : ISettingsRepo { override fun updateOtherSettings(transform: (OtherSettings) -> OtherSettings) = Unit override fun updateDataSourcesSettings(settings: DataSourcesSettings) = Unit override fun updateDataSourcesStatus(status: Map) = Unit + override fun getAmSatFmCatnums(): Set = amSatFm + override fun getAmSatLinearCatnums(): Set = amSatLinear + override fun setAmSatCatnums(fmCatnums: Set, linearCatnums: Set) = Unit override fun updateRadioControlSettings(settings: RadioControlSettings) = Unit override fun getSatelliteOffset(catnum: Int): String = "" override fun setSatelliteOffset(catnum: Int, offset: String) = Unit diff --git a/feature/satellites/src/main/java/com/rtbishop/look4sat/feature/satellites/SatellitesScreen.kt b/feature/satellites/src/main/java/com/rtbishop/look4sat/feature/satellites/SatellitesScreen.kt index ec0270e3..6310ce27 100644 --- a/feature/satellites/src/main/java/com/rtbishop/look4sat/feature/satellites/SatellitesScreen.kt +++ b/feature/satellites/src/main/java/com/rtbishop/look4sat/feature/satellites/SatellitesScreen.kt @@ -177,6 +177,7 @@ private fun SatellitesScreen( ) } } + // 转发器/活动筛选已在"卫星类型"对话框前三个选项中体现(AMSAT Live FM/Linear, Live SSTV). ElevatedCard(modifier = Modifier.fillMaxSize()) { val emptyMessage = stringResource(R.string.sat_empty_list_message) when {