mirror of
https://github.com/atsunatsu/Look4Sat.git
synced 2026-10-03 11:17:25 +00:00
Compare commits
6
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
a082abb72c | ||
|
|
a2e41a0c1a | ||
|
|
e1daf70f11 | ||
|
|
ca9f93c1ed | ||
|
|
42bd55e180 | ||
|
|
7401e1451b |
No files matched your search
@@ -158,6 +158,12 @@ class DatabaseRepo(
|
||||
val all = nameToCatnum.filter { (localName, _) ->
|
||||
dataParser.matchesAmSatName(localName, keys)
|
||||
}.values.toSet()
|
||||
// No local entry matches this AMSAT name (e.g. a brand-new
|
||||
// satellite not yet in the local TLE, like TEVEL2/RS95S):
|
||||
// skip it. Returning empty is correct — crashing here (via
|
||||
// all.first()) aborted the WHOLE list update and kept the
|
||||
// stale pre-whitelist FM list with all five ISS modules.
|
||||
if (all.isEmpty()) return emptySet()
|
||||
if (all.size <= 1) return all
|
||||
val preferred = all.intersect(activeCatnums)
|
||||
if (preferred.isNotEmpty()) return preferred
|
||||
@@ -166,12 +172,19 @@ class DatabaseRepo(
|
||||
// entries) instead of admitting every alias (DESTINY etc.).
|
||||
return setOf(all.minOrNull() ?: all.first())
|
||||
}
|
||||
// A failed FM/Linear page fetch yields an empty list here; keep the
|
||||
// previous lists so a single network hiccup cannot wipe the filters.
|
||||
// A failed FM/Linear page fetch yields an empty list here; do NOT
|
||||
// keep the stale list verbatim (it may predate the whitelist and
|
||||
// still contain ISS module aliases like DESTINY). Instead re-run
|
||||
// the multi-match disambiguation on the previous catnums via their
|
||||
// local names, so the whitelist / smallest-catnum rules clean it.
|
||||
fun cleanStaleList(previous: Set<Int>): Set<Int> =
|
||||
previous.mapNotNull { catnum ->
|
||||
nameToCatnum.entries.firstOrNull { it.value == catnum }?.key
|
||||
}.flatMap { resolvePerName(it) }.toSet()
|
||||
val fmCatnums = fmNames.flatMap { resolvePerName(it) }.toSet()
|
||||
.ifEmpty { settingsRepo.getAmSatFmCatnums() }
|
||||
.ifEmpty { cleanStaleList(settingsRepo.getAmSatFmCatnums()) }
|
||||
val linearCatnums = linearNames.flatMap { resolvePerName(it) }.toSet()
|
||||
.ifEmpty { settingsRepo.getAmSatLinearCatnums() }
|
||||
.ifEmpty { cleanStaleList(settingsRepo.getAmSatLinearCatnums()) }
|
||||
settingsRepo.setAmSatCatnums(fmCatnums, linearCatnums)
|
||||
println("AMSAT live lists updated: FM=${fmCatnums.size}, Linear=${linearCatnums.size}, Active=${activeCatnums.size}")
|
||||
}.onFailure {
|
||||
|
||||
+19
-11
@@ -25,6 +25,7 @@ import com.rtbishop.look4sat.core.domain.source.Sources
|
||||
import kotlinx.coroutines.CoroutineDispatcher
|
||||
import kotlinx.coroutines.ExperimentalCoroutinesApi
|
||||
import kotlinx.coroutines.flow.MutableStateFlow
|
||||
import kotlinx.coroutines.flow.combine
|
||||
import kotlinx.coroutines.flow.first
|
||||
import kotlinx.coroutines.flow.flatMapLatest
|
||||
import kotlinx.coroutines.flow.map
|
||||
@@ -45,17 +46,21 @@ class SelectionRepo(
|
||||
// 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<String> ->
|
||||
val catnumSet: Set<Int>? = if (types.isEmpty()) {
|
||||
null // null = no filtering
|
||||
} else {
|
||||
val ids = resolveTypeIds(types)
|
||||
if (ids.isEmpty()) emptySet() else ids.toHashSet()
|
||||
// The AMSAT list version counter re-triggers resolution when a background
|
||||
// data sync rewrites the FM/Linear lists, so the UI reflects the new list
|
||||
// without a restart or type toggle.
|
||||
private val itemsWithTypes = combine(currentTypes, settingsRepo.amSatListsVersion) { types, _ -> types }
|
||||
.flatMapLatest { types: List<String> ->
|
||||
val catnumSet: Set<Int>? = if (types.isEmpty()) {
|
||||
null // null = no filtering
|
||||
} else {
|
||||
val ids = resolveTypeIds(types)
|
||||
if (ids.isEmpty()) emptySet() else ids.toHashSet()
|
||||
}
|
||||
currentItems.map { items ->
|
||||
if (catnumSet == null) items else items.filter { it.catnum in catnumSet }
|
||||
}
|
||||
}
|
||||
currentItems.map { items ->
|
||||
if (catnumSet == null) items else items.filter { it.catnum in catnumSet }
|
||||
}
|
||||
}
|
||||
|
||||
private val itemsWithQuery = currentQuery.flatMapLatest { query ->
|
||||
itemsWithTypes.map { items -> filterByQuery(items, query) }
|
||||
@@ -97,7 +102,10 @@ class SelectionRepo(
|
||||
override fun getTypesList() = buildList {
|
||||
// 三个转发器/活动虚拟类型排在最前.
|
||||
addAll(Sources.virtualTypeNames)
|
||||
addAll(Sources.satelliteDataUrls.keys.sorted().toMutableList().apply { removeAt(0) })
|
||||
// 所有真实 TLE 源类型均可选(含 "All" = CelesTrak active);只排除
|
||||
// "Other"(空 URL 占位,无数据)。不要用 removeAt(0) —— 字母序第一个
|
||||
// 是 "All"/"ARISS" 这类真实源,删掉会让用户勾不到它们。
|
||||
addAll(Sources.satelliteDataUrls.keys.sorted().filterNot { it == "Other" })
|
||||
}
|
||||
|
||||
override suspend fun getEntriesFlow() = withContext(dispatcher) {
|
||||
|
||||
@@ -498,7 +498,9 @@ class SettingsRepo(
|
||||
}
|
||||
|
||||
override fun setSatelliteTypeIds(type: String, ids: List<Int>) {
|
||||
if (type == "All") return
|
||||
// "All" is a real TLE source type (CelesTrak active group) whose ids
|
||||
// must be persisted like any other type — skipping it made the "All"
|
||||
// filter resolve to an empty set and show nothing.
|
||||
val typesString = ids.joinToString(separatorComma)
|
||||
preferences.edit { putString("type$type", typesString) }
|
||||
}
|
||||
@@ -749,6 +751,9 @@ class SettingsRepo(
|
||||
//endregion
|
||||
|
||||
//region # AMSAT live-transponder lists
|
||||
private val _amSatListsVersion = MutableStateFlow(0)
|
||||
override val amSatListsVersion: StateFlow<Int> = _amSatListsVersion
|
||||
|
||||
override fun getAmSatFmCatnums(): Set<Int> {
|
||||
val raw = preferences.getString(keyAmSatFmCatnums, null) ?: return emptySet()
|
||||
return raw.split(separatorComma).mapNotNull { it.toIntOrNull() }.toSet()
|
||||
@@ -764,6 +769,7 @@ class SettingsRepo(
|
||||
putString(keyAmSatFmCatnums, fmCatnums.sorted().joinToString(separatorComma))
|
||||
putString(keyAmSatLinearCatnums, linearCatnums.sorted().joinToString(separatorComma))
|
||||
}
|
||||
_amSatListsVersion.update { it + 1 }
|
||||
}
|
||||
|
||||
override fun getAmSatActiveCatnums(): Set<Int> {
|
||||
|
||||
+72
-5
@@ -127,6 +127,29 @@ class DatabaseRepoTest {
|
||||
assertEquals(listOf(98248), settingsRepo.satelliteTypeIdsByType["SatNOGS"])
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `remote update persists All source type ids`() = runTest(dispatcher) {
|
||||
// Regression: "All" (CelesTrak active) is a real TLE source type whose
|
||||
// ids must be persisted on sync — the old `if (type == "All") return`
|
||||
// silently dropped them, making the "All" filter resolve to nothing.
|
||||
val allUrl = Sources.satelliteDataUrls.getValue("All")
|
||||
val localSource = FakeLocalSource()
|
||||
val remoteSource = FakeRemoteSource().apply {
|
||||
networkStreams[allUrl] = { validCsvStream() }
|
||||
}
|
||||
val settingsRepo = FakeSettingsRepo(
|
||||
dataSources = DataSourcesSettings(
|
||||
satelliteUrls = listOf(allUrl),
|
||||
transceiversUrls = emptyList()
|
||||
)
|
||||
)
|
||||
val repository = DatabaseRepo(dispatcher, dataParser, localSource, remoteSource, settingsRepo)
|
||||
|
||||
repository.updateFromRemote()
|
||||
|
||||
assertEquals(listOf(25544), settingsRepo.satelliteTypeIdsByType["All"])
|
||||
}
|
||||
|
||||
private fun validCsvStream(): InputStream = """
|
||||
OBJECT_NAME,OBJECT_ID,EPOCH,MEAN_MOTION,ECCENTRICITY,INCLINATION,RA_OF_ASC_NODE,ARG_OF_PERICENTER,MEAN_ANOMALY,EPHEMERIS_TYPE,CLASSIFICATION_TYPE,NORAD_CAT_ID,ELEMENT_SET_NO,REV_AT_EPOCH,BSTAR,MEAN_MOTION_DOT,MEAN_MOTION_DDOT
|
||||
ISS (ZARYA),1998-067A,2021-11-16T12:28:09.322176,15.48582035,.0004694,51.6447,309.4881,203.6966,299.8876,0,U,25544,999,31220,.31985E-4,.1288E-4,0
|
||||
@@ -195,26 +218,69 @@ class DatabaseRepoTest {
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `amsat fm list keeps previous list when fm page fetch fails`() = runTest(dispatcher) {
|
||||
fun `amsat fm list cleans stale ISS module aliases when fm page fetch fails`() = runTest(dispatcher) {
|
||||
val satnogsUrl = Sources.satelliteDataUrls.getValue("SatNOGS")
|
||||
val localSource = FakeLocalSource()
|
||||
val remoteSource = FakeRemoteSource().apply {
|
||||
networkStreams[satnogsUrl] = { jamxTleStream() }
|
||||
// FM page NOT registered -> 404 -> empty parse -> previous list retained.
|
||||
networkStreams[satnogsUrl] = { issModulesCsvStream() }
|
||||
// FM page NOT registered -> 404 -> empty parse.
|
||||
}
|
||||
val settingsRepo = FakeSettingsRepo(
|
||||
dataSources = DataSourcesSettings(
|
||||
satelliteUrls = listOf(satnogsUrl),
|
||||
transceiversUrls = emptyList()
|
||||
)
|
||||
).apply { amSatFm = setOf(25544, 7530) } // list from an earlier good sync
|
||||
).apply {
|
||||
// Pre-whitelist era stale list: ALL five ISS module entries.
|
||||
amSatFm = setOf(25544, 25575, 26400, 26700, 49044)
|
||||
}
|
||||
val repository = DatabaseRepo(dispatcher, dataParser, localSource, remoteSource, settingsRepo)
|
||||
|
||||
repository.updateFromRemote()
|
||||
|
||||
assertEquals(setOf(25544, 7530), settingsRepo.amSatFm)
|
||||
// The stale list is not kept verbatim: with the FM page down, the
|
||||
// previous catnums are re-disambiguated via local names, collapsing
|
||||
// the five ISS modules to the primary ZARYA (25544).
|
||||
assertEquals(setOf(25544), settingsRepo.amSatFm)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `amsat fm list survives unknown satellite names and still disambiguates ISS`() = runTest(dispatcher) {
|
||||
// Regression: AMSAT pages list satellites absent from the local TLE
|
||||
// (TEVEL2-1..9, RS95S). resolvePerName used to crash on the empty
|
||||
// match set (all.first()), aborting the WHOLE list update and keeping
|
||||
// the stale pre-whitelist FM list with all five ISS modules.
|
||||
val satnogsUrl = Sources.satelliteDataUrls.getValue("SatNOGS")
|
||||
val localSource = FakeLocalSource()
|
||||
val remoteSource = FakeRemoteSource().apply {
|
||||
networkStreams[satnogsUrl] = { issModulesCsvStream() }
|
||||
networkStreams[Sources.amSatLiveUrls.getValue("FM")] = { amsatFmPageWithUnknownStream() }
|
||||
}
|
||||
val settingsRepo = FakeSettingsRepo(
|
||||
dataSources = DataSourcesSettings(
|
||||
satelliteUrls = listOf(satnogsUrl),
|
||||
transceiversUrls = emptyList()
|
||||
)
|
||||
).apply { amSatActive = setOf(25544) }
|
||||
val repository = DatabaseRepo(dispatcher, dataParser, localSource, remoteSource, settingsRepo)
|
||||
|
||||
repository.updateFromRemote()
|
||||
|
||||
// No crash; ISS still collapses to the primary ZARYA (25544).
|
||||
assertEquals(setOf(25544), settingsRepo.amSatFm)
|
||||
}
|
||||
|
||||
private fun amsatFmPageWithUnknownStream(): InputStream = """
|
||||
<table>
|
||||
<thead><tr><th>Satellite</th><th>Uplink</th><th>Downlink</th><th>Comment</th></tr></thead>
|
||||
<tbody>
|
||||
<tr><td>ISS</td><td>145.990 MHz</td><td>437.800 MHz</td><td></td></tr>
|
||||
<tr><td>TEVEL2-1 thru TEVEL2-9</td><td>145.970 MHz</td><td>436.400 MHz</td><td></td></tr>
|
||||
<tr><td>RS95S(QMR-KWT-2)</td><td>145.920 MHz</td><td>436.950 MHz</td><td></td></tr>
|
||||
</tbody>
|
||||
</table>
|
||||
""".trimIndent().byteInputStream()
|
||||
|
||||
private fun issModulesCsvStream(): InputStream = """
|
||||
OBJECT_NAME,OBJECT_ID,EPOCH,MEAN_MOTION,ECCENTRICITY,INCLINATION,RA_OF_ASC_NODE,ARG_OF_PERICENTER,MEAN_ANOMALY,EPHEMERIS_TYPE,CLASSIFICATION_TYPE,NORAD_CAT_ID,ELEMENT_SET_NO,REV_AT_EPOCH,BSTAR,MEAN_MOTION_DOT,MEAN_MOTION_DDOT
|
||||
ISS (ZARYA),1998-067A,2021-11-16T12:28:09.322176,15.48582035,.0004694,51.6447,309.4881,203.6966,299.8876,0,U,25544,999,31220,.31985E-4,.1288E-4,0
|
||||
@@ -365,6 +431,7 @@ private class FakeSettingsRepo(dataSources: DataSourcesSettings = defaultDataSou
|
||||
var amSatFm: Set<Int> = emptySet()
|
||||
var amSatLinear: Set<Int> = emptySet()
|
||||
var amSatActive: Set<Int> = emptySet()
|
||||
override val amSatListsVersion: StateFlow<Int> = MutableStateFlow(0)
|
||||
|
||||
override fun getAmSatFmCatnums(): Set<Int> = amSatFm
|
||||
override fun getAmSatLinearCatnums(): Set<Int> = amSatLinear
|
||||
|
||||
+77
-5
@@ -217,6 +217,68 @@ class SelectionRepoSearchTest {
|
||||
assertTrue("All" in types)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `All type resolves to persisted CelesTrak active ids`() = runTest {
|
||||
// Regression: "All" is a real TLE source type (CelesTrak active). Its
|
||||
// ids are persisted on sync like any other type; selecting it must
|
||||
// show the synced satellites, NOT an empty list.
|
||||
val fake = FakeSettingsRepoForSearch()
|
||||
fake.setSatelliteTypeIds("All", sampleItems.map { it.catnum })
|
||||
val repo = SelectionRepo(
|
||||
dispatcher = Dispatchers.Unconfined,
|
||||
localSource = FakeLocalSourceForSearch(sampleItems),
|
||||
settingsRepo = fake
|
||||
)
|
||||
repo.setTypes(listOf("All"))
|
||||
val results = repo.getEntriesFlow().first()
|
||||
assertEquals(sampleItems.map { it.catnum }.toSet(), results.map { it.catnum }.toSet())
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `getTypesList excludes only the Other placeholder`() = runTest {
|
||||
val repo = createRepo(sampleItems)
|
||||
val types = repo.getTypesList()
|
||||
// All real TLE source types must be selectable, including "All"
|
||||
// (CelesTrak active) and "ARISS" (removed by the old removeAt(0)).
|
||||
assertTrue("All" in types)
|
||||
assertTrue("ARISS" in types)
|
||||
assertTrue("R4UAB" in types)
|
||||
assertTrue("SatNOGS" in types)
|
||||
assertTrue("Other" !in types)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `all types shows all items`() = runTest {
|
||||
val repo = createRepo(sampleItems)
|
||||
repo.setTypes(emptyList()) // "All" = no type filtering
|
||||
val results = repo.getEntriesFlow().first()
|
||||
assertEquals(sampleItems.size, results.size)
|
||||
assertEquals(sampleItems.map { it.catnum }.toSet(), results.map { it.catnum }.toSet())
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `amsat list version bump re-resolves FM filter after data sync`() = runTest {
|
||||
// ISS module aliases: dirty pre-sync FM list contains both ZARYA and DESTINY.
|
||||
val items = listOf(
|
||||
SatItem(25544, "ISS (ZARYA)"),
|
||||
SatItem(26700, "ISS (DESTINY)")
|
||||
)
|
||||
val fake = FakeSettingsRepoForSearch(amSatFm = setOf(25544, 26700))
|
||||
val repo = SelectionRepo(
|
||||
dispatcher = Dispatchers.Unconfined,
|
||||
localSource = FakeLocalSourceForSearch(items),
|
||||
settingsRepo = fake
|
||||
)
|
||||
repo.setTypes(listOf("AMSAT Live FM"))
|
||||
assertEquals(setOf(25544, 26700), repo.getEntriesFlow().first().map { it.catnum }.toSet())
|
||||
|
||||
// Simulate a background data sync: lists rewritten + version bumped.
|
||||
fake.setAmSatCatnums(fmCatnums = setOf(25544), linearCatnums = emptySet())
|
||||
|
||||
// The FM filter must reflect the new list WITHOUT a type toggle or restart.
|
||||
assertEquals(listOf(25544), repo.getEntriesFlow().first().map { it.catnum })
|
||||
}
|
||||
|
||||
private fun createRepo(
|
||||
items: List<SatItem>,
|
||||
amSatFm: Set<Int> = emptySet(),
|
||||
@@ -252,10 +314,11 @@ private class FakeLocalSourceForSearch(
|
||||
}
|
||||
|
||||
private class FakeSettingsRepoForSearch(
|
||||
private val amSatFm: Set<Int> = emptySet(),
|
||||
private val amSatLinear: Set<Int> = emptySet(),
|
||||
var amSatFm: Set<Int> = emptySet(),
|
||||
var amSatLinear: Set<Int> = emptySet(),
|
||||
private var amSatActive: Set<Int> = emptySet()
|
||||
) : ISettingsRepo {
|
||||
private val typeIds = mutableMapOf<String, List<Int>>()
|
||||
override val appVersionName: String = "test"
|
||||
override val selectedIds: StateFlow<List<Int>> = MutableStateFlow(emptyList())
|
||||
override val selectedTypes: StateFlow<List<String>> = MutableStateFlow(emptyList())
|
||||
@@ -290,8 +353,12 @@ private class FakeSettingsRepoForSearch(
|
||||
override fun setStationPosition(latitude: Double, longitude: Double, altitude: Double): Boolean = true
|
||||
override fun setStationPosition(): Boolean = true
|
||||
override fun setStationPosition(locator: String): Boolean = true
|
||||
override fun getSatelliteTypesIds(types: List<String>): List<Int> = emptyList()
|
||||
override fun setSatelliteTypeIds(type: String, ids: List<Int>) = Unit
|
||||
override fun getSatelliteTypesIds(types: List<String>): List<Int> =
|
||||
types.flatMap { typeIds[it].orEmpty() }.distinct()
|
||||
|
||||
override fun setSatelliteTypeIds(type: String, ids: List<Int>) {
|
||||
typeIds[type] = ids
|
||||
}
|
||||
override fun updateDatabaseState(state: DatabaseState) = Unit
|
||||
override fun updateRCSettings(settings: RCSettings) = Unit
|
||||
override fun updateOtherSettings(transform: (OtherSettings) -> OtherSettings) = Unit
|
||||
@@ -299,7 +366,12 @@ private class FakeSettingsRepoForSearch(
|
||||
override fun updateDataSourcesStatus(status: Map<String, Int>) = Unit
|
||||
override fun getAmSatFmCatnums(): Set<Int> = amSatFm
|
||||
override fun getAmSatLinearCatnums(): Set<Int> = amSatLinear
|
||||
override fun setAmSatCatnums(fmCatnums: Set<Int>, linearCatnums: Set<Int>) = Unit
|
||||
override val amSatListsVersion: StateFlow<Int> = MutableStateFlow(0)
|
||||
override fun setAmSatCatnums(fmCatnums: Set<Int>, linearCatnums: Set<Int>) {
|
||||
amSatFm = fmCatnums
|
||||
amSatLinear = linearCatnums
|
||||
(amSatListsVersion as MutableStateFlow<Int>).value++
|
||||
}
|
||||
override fun getAmSatActiveCatnums(): Set<Int> = amSatActive
|
||||
override fun setAmSatActiveCatnums(catnums: Set<Int>) { amSatActive = catnums }
|
||||
override fun updateRadioControlSettings(settings: RadioControlSettings) = Unit
|
||||
|
||||
+5
@@ -89,6 +89,11 @@ interface ISettingsRepo {
|
||||
* active-transponder table (whitelist for the virtual type filters). */
|
||||
fun getAmSatActiveCatnums(): Set<Int>
|
||||
fun setAmSatActiveCatnums(catnums: Set<Int>)
|
||||
/** Monotonic version counter, incremented every time the AMSAT live
|
||||
* FM/Linear lists are rewritten. Consumers that resolve the virtual
|
||||
* types (SelectionRepo) should re-resolve on change so a background
|
||||
* data sync is reflected in the UI without a restart. */
|
||||
val amSatListsVersion: StateFlow<Int>
|
||||
//endregion
|
||||
|
||||
//region # Radio control settings
|
||||
|
||||
+1
@@ -285,6 +285,7 @@ class LoTWGridSyncTest {
|
||||
override fun updateDataSourcesStatus(status: Map<String, Int>) = TODO()
|
||||
override fun getAmSatFmCatnums(): Set<Int> = emptySet()
|
||||
override fun getAmSatLinearCatnums(): Set<Int> = emptySet()
|
||||
override val amSatListsVersion: StateFlow<Int> = MutableStateFlow(0)
|
||||
override fun setAmSatCatnums(fmCatnums: Set<Int>, linearCatnums: Set<Int>) = Unit
|
||||
override fun getAmSatActiveCatnums(): Set<Int> = emptySet()
|
||||
override fun setAmSatActiveCatnums(catnums: Set<Int>) = Unit
|
||||
|
||||
@@ -112,6 +112,7 @@ class FakeSettingsRepo(
|
||||
|
||||
override fun getAmSatFmCatnums(): Set<Int> = amSatFm
|
||||
override fun getAmSatLinearCatnums(): Set<Int> = amSatLinear
|
||||
override val amSatListsVersion: StateFlow<Int> = MutableStateFlow(0)
|
||||
override fun setAmSatCatnums(fmCatnums: Set<Int>, linearCatnums: Set<Int>) = TODO()
|
||||
override fun getAmSatActiveCatnums(): Set<Int> = emptySet()
|
||||
override fun setAmSatActiveCatnums(catnums: Set<Int>) = TODO()
|
||||
|
||||
@@ -1,8 +1,8 @@
|
||||
[versions]
|
||||
#noinspection UnusedVersionCatalogEntry
|
||||
appVersionCode = "501"
|
||||
appVersionCode = "504"
|
||||
#noinspection UnusedVersionCatalogEntry
|
||||
appVersionName = "4.4.7-ba7opf.15.4"
|
||||
appVersionName = "4.4.7-ba7opf.15.7"
|
||||
#noinspection UnusedVersionCatalogEntry
|
||||
compileSdk = "37"
|
||||
#noinspection UnusedVersionCatalogEntry
|
||||
|
||||
Reference in new issue
Block a user