Replaced the types selection dialog with modes selection

This commit is contained in:
Arty Bishop committed 2026-07-31 16:04:39 +02:00
1 parent 2d3adfc6a6
commit f9806d7f7f
24 files changed
+393 -192

No files matched your search

@@ -45,7 +45,12 @@ interface Look4SatDao {
@Query("DELETE FROM entries") @Query("DELETE FROM entries")
suspend fun deleteEntries() suspend fun deleteEntries()
@Query("SELECT catnum FROM radios WHERE downlinkMode IN (:modes)") @Query(
"""
SELECT DISTINCT catnum FROM radios WHERE isAlive = 1
AND (downlinkMode IN (:modes) OR uplinkMode IN (:modes))
"""
)
suspend fun getIdsWithModes(modes: List<String>): List<Int> suspend fun getIdsWithModes(modes: List<String>): List<Int>
@Query("SELECT COUNT(*) FROM radios") @Query("SELECT COUNT(*) FROM radios")
@@ -47,7 +47,6 @@ class DatabaseRepo(
remoteSource.getFileStream(uri)?.let { stream -> remoteSource.getFileStream(uri)?.let { stream ->
val entries = parseSatelliteStream(uri, unwrapIfZipped(uri, stream)) val entries = parseSatelliteStream(uri, unwrapIfZipped(uri, stream))
localSource.insertEntries(entries) localSource.insertEntries(entries)
settingsRepo.setSatelliteTypeIds(customSourceType, entries.map { it.catnum })
importedCount = entries.size importedCount = entries.size
} }
setUpdateSuccessful(System.currentTimeMillis()) setUpdateSuccessful(System.currentTimeMillis())
@@ -78,12 +77,9 @@ class DatabaseRepo(
// launch all network requests concurrently // launch all network requests concurrently
val tleJobs = tleUrls.values.map { url -> async { url to remoteSource.getNetworkStream(url) } } val tleJobs = tleUrls.values.map { url -> async { url to remoteSource.getNetworkStream(url) } }
val radioJobs = radioUrls.values.map { url -> async { url to remoteSource.getNetworkStream(url) } } val radioJobs = radioUrls.values.map { url -> async { url to remoteSource.getNetworkStream(url) } }
// parse fetched data concurrently and associate with types // parse fetched data concurrently
val importedEntries = tleJobs.awaitAll().flatMap { (url, stream) -> val importedEntries = tleJobs.awaitAll().flatMap { (url, stream) ->
val type = tleUrls.entries.find { it.value == url }?.key ?: customSourceType stream?.let { parseSatelliteStream(url, unwrapIfZipped(url, it)) }.orEmpty()
stream?.let { parseSatelliteStream(url, unwrapIfZipped(url, it)) }.orEmpty().also { entries ->
settingsRepo.setSatelliteTypeIds(type, entries.map { it.catnum })
}
} }
val importedRadios = radioJobs.awaitAll().flatMap { (url, stream) -> val importedRadios = radioJobs.awaitAll().flatMap { (url, stream) ->
stream?.let { dataParser.parseJSONStream(unwrapIfZipped(url, it)) }.orEmpty() stream?.let { dataParser.parseJSONStream(unwrapIfZipped(url, it)) }.orEmpty()
@@ -73,7 +73,7 @@ class SatelliteRepo(
aosStartMinute = settings.aosStartMinute, aosStartMinute = settings.aosStartMinute,
aosEndMinute = settings.aosEndMinute, aosEndMinute = settings.aosEndMinute,
invertAosTimeWindow = settings.invertAosTimeWindow, invertAosTimeWindow = settings.invertAosTimeWindow,
modes = settings.selectedModes modes = settingsRepo.selectedSatModes.value
) )
} }
} }
@@ -38,16 +38,16 @@ class SelectionRepo(
) : ISelectionRepo { ) : ISelectionRepo {
private val currentItems = MutableStateFlow<List<SatItem>>(emptyList()) private val currentItems = MutableStateFlow<List<SatItem>>(emptyList())
private val currentTypes = MutableStateFlow(settingsRepo.selectedTypes.value) private val currentModes = MutableStateFlow(settingsRepo.selectedSatModes.value)
private val currentQuery = MutableStateFlow("") private val currentQuery = MutableStateFlow("")
// Resolve type IDs once when types change, then filter items reactively. // Resolve sat IDs once when modes change, then filter items reactively.
// The HashSet gives O(1) catnum lookups instead of O(n) with a List. // The HashSet gives O(1) catnum lookups instead of O(n) with a List.
private val itemsWithTypes = currentTypes.flatMapLatest { types: List<String> -> private val itemsWithModes = currentModes.flatMapLatest { modes: List<String> ->
val catnumSet: Set<Int>? = if (types.isEmpty()) { val catnumSet: Set<Int>? = if (modes.isEmpty()) {
null // null = no filtering null // null = no filtering
} else { } else {
val ids = settingsRepo.getSatelliteTypesIds(types) val ids = localSource.getIdsWithModes(modes)
if (ids.isEmpty()) null else ids.toHashSet() if (ids.isEmpty()) null else ids.toHashSet()
} }
currentItems.map { items -> currentItems.map { items ->
@@ -56,14 +56,18 @@ class SelectionRepo(
} }
private val itemsWithQuery = currentQuery.flatMapLatest { query -> private val itemsWithQuery = currentQuery.flatMapLatest { query ->
itemsWithTypes.map { items -> filterByQuery(items, query) } itemsWithModes.map { items ->
filterByQuery(items, query).sortedWith(
compareByDescending<SatItem> { it.isSelected }
.thenBy { it.name }
.thenBy { it.catnum }
)
}
} }
override fun getCurrentTypes() = currentTypes.value override fun getCurrentModes() = currentModes.value
override fun getTypesList() = Sources.satelliteDataUrls.keys.sorted().toMutableList().apply { override fun getModesList() = Sources.satelliteModes
remove("All")
}
override suspend fun getEntriesFlow() = withContext(dispatcher) { override suspend fun getEntriesFlow() = withContext(dispatcher) {
val selectedIds = settingsRepo.selectedIds.value.toHashSet() val selectedIds = settingsRepo.selectedIds.value.toHashSet()
@@ -73,9 +77,9 @@ class SelectionRepo(
return@withContext itemsWithQuery return@withContext itemsWithQuery
} }
override suspend fun setTypes(types: List<String>) { override suspend fun setModes(modes: List<String>) {
currentTypes.value = types currentModes.value = modes
settingsRepo.setSelectedTypes(types) settingsRepo.setSelectedSatModes(modes)
} }
override suspend fun setQuery(query: String) { override suspend fun setQuery(query: String) {
@@ -68,8 +68,7 @@ class SettingsRepo(
private val keyFrequencyPort = "frequencyPort" private val keyFrequencyPort = "frequencyPort"
private val keyFrequencyFormat = "frequencyFormat" private val keyFrequencyFormat = "frequencyFormat"
private val keySelectedIds = "selectedIds" private val keySelectedIds = "selectedIds"
private val keySelectedTypes = "selectedTypes" private val keySelectedSatModes = "selectedSatModes"
private val keySelectedModes = "selectedModes"
private val keyStateOfAutoUpdate = "stateOfAutoUpdate" private val keyStateOfAutoUpdate = "stateOfAutoUpdate"
private val keyStateOfSensors = "stateOfSensors" private val keyStateOfSensors = "stateOfSensors"
private val keyStateOfSweep = "stateOfSweep" private val keyStateOfSweep = "stateOfSweep"
@@ -95,9 +94,9 @@ class SettingsRepo(
//region # Satellites selection settings //region # Satellites selection settings
private val _satelliteSelection = MutableStateFlow(getSelectedIds()) private val _satelliteSelection = MutableStateFlow(getSelectedIds())
private val _typesSelection = MutableStateFlow(getSelectedTypes()) private val _satelliteModeSelection = MutableStateFlow(getSelectedSatModes())
override val selectedIds: StateFlow<List<Int>> = _satelliteSelection override val selectedIds: StateFlow<List<Int>> = _satelliteSelection
override val selectedTypes: StateFlow<List<String>> = _typesSelection override val selectedSatModes: StateFlow<List<String>> = _satelliteModeSelection
override fun setSelectedIds(ids: List<Int>) { override fun setSelectedIds(ids: List<Int>) {
val selectionString = ids.joinToString(separatorComma) val selectionString = ids.joinToString(separatorComma)
@@ -105,10 +104,10 @@ class SettingsRepo(
_satelliteSelection.value = ids _satelliteSelection.value = ids
} }
override fun setSelectedTypes(types: List<String>) { override fun setSelectedSatModes(modes: List<String>) {
val typesString = types.joinToString(separatorComma) val modesString = modes.joinToString(separatorComma)
preferences.edit { putString(keySelectedTypes, typesString) } preferences.edit { putString(keySelectedSatModes, modesString) }
_typesSelection.value = types _satelliteModeSelection.value = modes
} }
private fun getSelectedIds(): List<Int> { private fun getSelectedIds(): List<Int> {
@@ -117,10 +116,10 @@ class SettingsRepo(
return selectionString.split(separatorComma).map { it.toInt() } return selectionString.split(separatorComma).map { it.toInt() }
} }
private fun getSelectedTypes(): List<String> { private fun getSelectedSatModes(): List<String> {
val typesString = preferences.getString(keySelectedTypes, "Amateur") val modesString = preferences.getString(keySelectedSatModes, null)
if (typesString.isNullOrEmpty()) return emptyList() if (modesString.isNullOrEmpty()) return emptyList()
return typesString.split(separatorComma) return modesString.split(separatorComma).sorted()
} }
//endregion //endregion
@@ -135,7 +134,6 @@ class SettingsRepo(
putInt(keyFilterAosStartMinute, settings.aosStartMinute) putInt(keyFilterAosStartMinute, settings.aosStartMinute)
putInt(keyFilterAosEndMinute, settings.aosEndMinute) putInt(keyFilterAosEndMinute, settings.aosEndMinute)
putBoolean(keyFilterAosInvert, settings.invertAosTimeWindow) putBoolean(keyFilterAosInvert, settings.invertAosTimeWindow)
putString(keySelectedModes, settings.selectedModes.joinToString(separatorComma))
_passesSettings.value = settings _passesSettings.value = settings
} }
@@ -146,16 +144,13 @@ class SettingsRepo(
val aosStartMinute = preferences.getInt(keyFilterAosStartMinute, 0).coerceIn(0, 23 * 60 + 59) val aosStartMinute = preferences.getInt(keyFilterAosStartMinute, 0).coerceIn(0, 23 * 60 + 59)
val aosEndMinute = preferences.getInt(keyFilterAosEndMinute, 23 * 60 + 59).coerceIn(0, 23 * 60 + 59) val aosEndMinute = preferences.getInt(keyFilterAosEndMinute, 23 * 60 + 59).coerceIn(0, 23 * 60 + 59)
val invertAosTimeWindow = preferences.getBoolean(keyFilterAosInvert, false) val invertAosTimeWindow = preferences.getBoolean(keyFilterAosInvert, false)
val selectedModesString = preferences.getString(keySelectedModes, null)
val selectedModes = selectedModesString?.split(separatorComma)?.sorted() ?: emptyList()
return PassesSettings( return PassesSettings(
showDeepSpace, showDeepSpace,
hoursAhead, hoursAhead,
minElevation, minElevation,
aosStartMinute, aosStartMinute,
aosEndMinute, aosEndMinute,
invertAosTimeWindow, invertAosTimeWindow
selectedModes
) )
} }
//endregion //endregion
@@ -235,25 +230,6 @@ class SettingsRepo(
private val _databaseState = MutableStateFlow(getDatabaseState()) private val _databaseState = MutableStateFlow(getDatabaseState())
override val databaseState: StateFlow<DatabaseState> = _databaseState override val databaseState: StateFlow<DatabaseState> = _databaseState
override fun getSatelliteTypesIds(types: List<String>): List<Int> {
val idsSet = mutableSetOf<Int>()
types.forEach { type ->
val typeString = preferences.getString("type$type", null)
val typeIds = if (typeString.isNullOrBlank()) {
emptyList()
} else {
typeString.split(separatorComma).map { it.toInt() }
}
idsSet.addAll(typeIds)
}
return idsSet.toList()
}
override fun setSatelliteTypeIds(type: String, ids: List<Int>) {
if (type == "All") return
val typesString = ids.joinToString(separatorComma)
preferences.edit { putString("type$type", typesString) }
}
override fun updateDatabaseState(state: DatabaseState) = preferences.edit { override fun updateDatabaseState(state: DatabaseState) = preferences.edit {
putInt(keyNumberOfSatellites, state.numberOfSatellites) putInt(keyNumberOfSatellites, state.numberOfSatellites)
@@ -62,7 +62,6 @@ class DatabaseRepoTest {
assertEquals(1, localSource.insertedEntries.size) assertEquals(1, localSource.insertedEntries.size)
assertEquals(25544, localSource.insertedEntries.first().catnum) assertEquals(25544, localSource.insertedEntries.first().catnum)
assertEquals(listOf(25544), settingsRepo.satelliteTypeIdsByType["Other"])
assertTrue(settingsRepo.databaseState.value.numberOfSatellites > 0) assertTrue(settingsRepo.databaseState.value.numberOfSatellites > 0)
} }
@@ -102,7 +101,6 @@ class DatabaseRepoTest {
repository.updateFromRemote() repository.updateFromRemote()
assertTrue(localSource.insertedEntries.any { it.catnum == 25544 }) assertTrue(localSource.insertedEntries.any { it.catnum == 25544 })
assertEquals(listOf(25544), settingsRepo.satelliteTypeIdsByType["Other"])
} }
private fun validCsvStream(): InputStream = """ private fun validCsvStream(): InputStream = """
@@ -165,10 +163,10 @@ private class FakeSettingsRepo(dataSources: DataSourcesSettings = defaultDataSou
override val selectedIds: StateFlow<List<Int>> = MutableStateFlow(emptyList()) override val selectedIds: StateFlow<List<Int>> = MutableStateFlow(emptyList())
override val selectedTypes: StateFlow<List<String>> = MutableStateFlow(emptyList()) override val selectedSatModes: StateFlow<List<String>> = MutableStateFlow(emptyList())
override val passesSettings: StateFlow<PassesSettings> = MutableStateFlow( override val passesSettings: StateFlow<PassesSettings> = MutableStateFlow(
PassesSettings(hoursAhead = 24, minElevation = 0.0, selectedModes = emptyList()) PassesSettings(hoursAhead = 24, minElevation = 0.0)
) )
override val stationPosition: StateFlow<GeoPos> = MutableStateFlow(GeoPos(0.0, 0.0)) override val stationPosition: StateFlow<GeoPos> = MutableStateFlow(GeoPos(0.0, 0.0))
@@ -189,11 +187,9 @@ private class FakeSettingsRepo(dataSources: DataSourcesSettings = defaultDataSou
RadioControlSettings(false, RadioControlSettings.MODEL_YAESU_FT817, "", "", "", "", 9600) RadioControlSettings(false, RadioControlSettings.MODEL_YAESU_FT817, "", "", "", "", 9600)
) )
val satelliteTypeIdsByType = mutableMapOf<String, List<Int>>()
override fun setSelectedIds(ids: List<Int>) = Unit override fun setSelectedIds(ids: List<Int>) = Unit
override fun setSelectedTypes(types: List<String>) = Unit override fun setSelectedSatModes(modes: List<String>) = Unit
override fun setPassesSettings(settings: PassesSettings) = Unit override fun setPassesSettings(settings: PassesSettings) = Unit
@@ -203,11 +199,6 @@ private class FakeSettingsRepo(dataSources: DataSourcesSettings = defaultDataSou
override fun setStationPosition(locator: String): 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>) {
satelliteTypeIdsByType[type] = ids
}
override fun updateDatabaseState(state: DatabaseState) { override fun updateDatabaseState(state: DatabaseState) {
databaseState.value = state databaseState.value = state
@@ -0,0 +1,173 @@
/*
* Look4Sat. Amateur radio satellite tracker and pass predictor.
* Copyright (C) 2019-2026 Arty Bishop and contributors.
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
package com.rtbishop.look4sat.core.data.repository
import com.rtbishop.look4sat.core.domain.model.DataSourcesSettings
import com.rtbishop.look4sat.core.domain.model.DatabaseState
import com.rtbishop.look4sat.core.domain.model.OtherSettings
import com.rtbishop.look4sat.core.domain.model.PassesSettings
import com.rtbishop.look4sat.core.domain.model.RCSettings
import com.rtbishop.look4sat.core.domain.model.RadioControlSettings
import com.rtbishop.look4sat.core.domain.model.SatItem
import com.rtbishop.look4sat.core.domain.model.SatRadio
import com.rtbishop.look4sat.core.domain.predict.GeoPos
import com.rtbishop.look4sat.core.domain.predict.OrbitalObject
import com.rtbishop.look4sat.core.domain.repository.ISettingsRepo
import com.rtbishop.look4sat.core.domain.source.ILocalSource
import kotlinx.coroutines.ExperimentalCoroutinesApi
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.StateFlow
import kotlinx.coroutines.flow.first
import kotlinx.coroutines.test.StandardTestDispatcher
import kotlinx.coroutines.test.runTest
import org.junit.Assert.assertEquals
import org.junit.Test
@OptIn(ExperimentalCoroutinesApi::class)
class SelectionRepoTest {
private val dispatcher = StandardTestDispatcher()
@Test
fun `unknown mode values do not crash and do not filter out entries`() = runTest(dispatcher) {
val localSource = FakeLocalSource(
entries = listOf(
SatItem(25544, "ISS (ZARYA)", false),
SatItem(40967, "TIANGONG", false)
)
)
val settingsRepo = FakeSettingsRepo(selectedModes = listOf("REMOVED_MODE"))
val repository = SelectionRepo(dispatcher, localSource, settingsRepo)
val flow = repository.getEntriesFlow()
repository.setModes(listOf("REMOVED_MODE"))
val items = flow.first()
assertEquals(listOf(25544, 40967), items.map { it.catnum })
assertEquals(listOf("REMOVED_MODE"), repository.getCurrentModes())
}
@Test
fun `selected satellites are shown first`() = runTest(dispatcher) {
val localSource = FakeLocalSource(
entries = listOf(
SatItem(44444, "Zeta", false),
SatItem(25544, "Alpha", false),
SatItem(40967, "Beta", false)
)
)
val settingsRepo = FakeSettingsRepo(selectedModes = emptyList())
val repository = SelectionRepo(dispatcher, localSource, settingsRepo)
val flow = repository.getEntriesFlow()
repository.setSelection(listOf(40967), true)
val items = flow.first()
assertEquals(listOf(40967, 25544, 44444), items.map { it.catnum })
assertEquals(listOf(true, false, false), items.map { it.isSelected })
}
private class FakeLocalSource(
private val entries: List<SatItem>
) : ILocalSource {
override suspend fun getEntriesTotal(): Int = entries.size
override suspend fun getEntriesList(): List<SatItem> = entries
override suspend fun getEntriesWithIds(ids: List<Int>): List<OrbitalObject> = emptyList()
override suspend fun insertEntries(entries: List<com.rtbishop.look4sat.core.domain.predict.OrbitalData>) = Unit
override suspend fun deleteEntries() = Unit
override suspend fun getIdsWithModes(modes: List<String>): List<Int> = emptyList()
override suspend fun getRadiosTotal(): Int = 0
override suspend fun getRadiosWithId(id: Int): List<SatRadio> = emptyList()
override suspend fun insertRadios(radios: List<SatRadio>) = Unit
override suspend fun deleteRadios() = Unit
}
private class FakeSettingsRepo(
selectedModes: List<String>
) : ISettingsRepo {
override val appVersionName: String = "test"
override val selectedIds: StateFlow<List<Int>> = MutableStateFlow(emptyList())
override val selectedSatModes: MutableStateFlow<List<String>> = MutableStateFlow(selectedModes)
override val passesSettings: StateFlow<PassesSettings> = MutableStateFlow(
PassesSettings(hoursAhead = 24, minElevation = 0.0)
)
override val stationPosition: StateFlow<GeoPos> = MutableStateFlow(GeoPos(0.0, 0.0))
override val databaseState: MutableStateFlow<DatabaseState> = MutableStateFlow(DatabaseState(0, 0, 0L))
override val rcSettings: StateFlow<RCSettings> = MutableStateFlow(
RCSettings(false, "", "", "", false, "", "", "", false, "", "", "", false, "", "")
)
override val otherSettings: StateFlow<OtherSettings> = MutableStateFlow(
OtherSettings(false, false, false, false, false, false, false, false)
)
override val dataSourcesSettings: StateFlow<DataSourcesSettings> = MutableStateFlow(
DataSourcesSettings(false, false, "", "")
)
override val radioControlSettings: StateFlow<RadioControlSettings> = MutableStateFlow(
RadioControlSettings(false, RadioControlSettings.MODEL_YAESU_FT817, "", "", "", "", 9600)
)
override fun setSelectedIds(ids: List<Int>) = Unit
override fun setSelectedSatModes(modes: List<String>) {
selectedSatModes.value = modes
}
override fun setPassesSettings(settings: PassesSettings) = Unit
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 updateDatabaseState(state: DatabaseState) {
databaseState.value = state
}
override fun updateRCSettings(settings: RCSettings) = Unit
override fun updateOtherSettings(transform: (OtherSettings) -> OtherSettings) = Unit
override fun updateDataSourcesSettings(settings: DataSourcesSettings) = Unit
override fun updateRadioControlSettings(settings: RadioControlSettings) = Unit
}
}
@@ -29,8 +29,7 @@ data class PassesSettings(
val minElevation: Double, val minElevation: Double,
val aosStartMinute: Int = 0, val aosStartMinute: Int = 0,
val aosEndMinute: Int = 23 * 60 + 59, val aosEndMinute: Int = 23 * 60 + 59,
val invertAosTimeWindow: Boolean = false, val invertAosTimeWindow: Boolean = false
val selectedModes: List<String>
) )
data class RCSettings( data class RCSettings(
@@ -21,10 +21,10 @@ import com.rtbishop.look4sat.core.domain.model.SatItem
import kotlinx.coroutines.flow.Flow import kotlinx.coroutines.flow.Flow
interface ISelectionRepo { interface ISelectionRepo {
fun getCurrentTypes(): List<String> fun getCurrentModes(): List<String>
fun getTypesList(): List<String> fun getModesList(): List<String>
suspend fun getEntriesFlow(): Flow<List<SatItem>> suspend fun getEntriesFlow(): Flow<List<SatItem>>
suspend fun setTypes(types: List<String>) suspend fun setModes(modes: List<String>)
suspend fun setQuery(query: String) suspend fun setQuery(query: String)
suspend fun setSelection(selectAll: Boolean) suspend fun setSelection(selectAll: Boolean)
suspend fun setSelection(ids: List<Int>, isTicked: Boolean) suspend fun setSelection(ids: List<Int>, isTicked: Boolean)
@@ -32,9 +32,9 @@ interface ISettingsRepo {
//region # Satellites selection settings //region # Satellites selection settings
val selectedIds: StateFlow<List<Int>> val selectedIds: StateFlow<List<Int>>
val selectedTypes: StateFlow<List<String>> val selectedSatModes: StateFlow<List<String>>
fun setSelectedIds(ids: List<Int>) fun setSelectedIds(ids: List<Int>)
fun setSelectedTypes(types: List<String>) fun setSelectedSatModes(modes: List<String>)
//endregion //endregion
//region # Passes filter settings //region # Passes filter settings
@@ -51,8 +51,6 @@ interface ISettingsRepo {
//region # Database update settings //region # Database update settings
val databaseState: StateFlow<DatabaseState> val databaseState: StateFlow<DatabaseState>
fun getSatelliteTypesIds(types: List<String>): List<Int>
fun setSatelliteTypeIds(type: String, ids: List<Int>)
fun updateDatabaseState(state: DatabaseState) fun updateDatabaseState(state: DatabaseState)
//endregion //endregion
@@ -20,36 +20,24 @@ package com.rtbishop.look4sat.core.domain.source
object Sources { object Sources {
val satelliteDataUrls = mapOf( val satelliteDataUrls = mapOf(
"All" to "https://celestrak.org/NORAD/elements/gp.php?GROUP=active&FORMAT=csv", "All" to "https://celestrak.org/NORAD/elements/gp.php?GROUP=active&FORMAT=csv",
"Amateur" to "https://celestrak.org/NORAD/elements/gp.php?GROUP=amateur&FORMAT=csv",
"Brightest" to "https://celestrak.org/NORAD/elements/gp.php?GROUP=visual&FORMAT=csv",
"Cubesat" to "https://celestrak.org/NORAD/elements/gp.php?GROUP=cubesat&FORMAT=csv",
"Education" to "https://celestrak.org/NORAD/elements/gp.php?GROUP=education&FORMAT=csv",
"Engineer" to "https://celestrak.org/NORAD/elements/gp.php?GROUP=engineering&FORMAT=csv",
"Geostationary" to "https://celestrak.org/NORAD/elements/gp.php?GROUP=geo&FORMAT=csv",
"Globalstar" to "https://celestrak.org/NORAD/elements/gp.php?GROUP=globalstar&FORMAT=csv",
"GNSS" to "https://celestrak.org/NORAD/elements/gp.php?GROUP=gnss&FORMAT=csv",
"Intelsat" to "https://celestrak.org/NORAD/elements/gp.php?GROUP=intelsat&FORMAT=csv",
"Iridium" to "https://celestrak.org/NORAD/elements/gp.php?GROUP=iridium-NEXT&FORMAT=csv",
"Military" to "https://celestrak.org/NORAD/elements/gp.php?GROUP=military&FORMAT=csv",
"New" to "https://celestrak.org/NORAD/elements/gp.php?GROUP=last-30-days&FORMAT=csv",
"OneWeb" to "https://celestrak.org/NORAD/elements/gp.php?GROUP=oneweb&FORMAT=csv",
"Orbcomm" to "https://celestrak.org/NORAD/elements/gp.php?GROUP=orbcomm&FORMAT=csv",
"Resource" to "https://celestrak.org/NORAD/elements/gp.php?GROUP=resource&FORMAT=csv",
"SatNOGS" to "https://celestrak.org/NORAD/elements/gp.php?GROUP=satnogs&FORMAT=csv",
"Science" to "https://celestrak.org/NORAD/elements/gp.php?GROUP=science&FORMAT=csv",
"Spire" to "https://celestrak.org/NORAD/elements/gp.php?GROUP=spire&FORMAT=csv",
"Starlink" to "https://celestrak.org/NORAD/elements/gp.php?GROUP=starlink&FORMAT=csv",
"Swarm" to "https://celestrak.org/NORAD/elements/gp.php?GROUP=swarm&FORMAT=csv",
"Weather" to "https://celestrak.org/NORAD/elements/gp.php?GROUP=weather&FORMAT=csv",
"X-Comm" to "https://celestrak.org/NORAD/elements/gp.php?GROUP=x-comm&FORMAT=csv",
"Amsat" to "https://amsat.org/tle/current/nasabare.txt", "Amsat" to "https://amsat.org/tle/current/nasabare.txt",
"Classified" to "https://www.mmccants.org/tles/classfd.zip", "Classified" to "https://www.mmccants.org/tles/classfd.zip",
"McCants" to "https://www.mmccants.org/tles/inttles.zip", "McCants" to "https://www.mmccants.org/tles/inttles.zip",
"R4UAB" to "https://r4uab.ru/satonline.txt", "R4UAB" to "https://r4uab.ru/satonline.txt",
"SatNOGS" to "https://db.satnogs.org/api/tle/?format=3le",
"ARISS" to "https://live.ariss.org/iss.txt", "ARISS" to "https://live.ariss.org/iss.txt",
"Other" to "" // key for sats filter "Other" to "" // key for sats filter
) )
val transceiversDataUrls = mapOf( val transceiversDataUrls = mapOf(
"SatNOGS" to "https://db.satnogs.org/api/transmitters/?format=json&status=active" "SatNOGS" to "https://db.satnogs.org/api/transmitters/?format=json&status=active"
) )
val satelliteModes = listOf(
"4FSK", "64-QAM", "AFSK", "AFSK TUBiX10", "AHRPT", "AM", "APT", "ASK", "BPSK",
"BPSK PMT-A3", "CERTO", "CW", "DATV", "DBPSK", "DOKA", "DPSK", "DQPSK", "DSB", "DSTAR",
"DUV", "DVB-S2", "FFSK", "FM", "FMN", "FSK", "FSK AX.100 Mode 5", "FSK AX.100 Mode 6",
"FSK AX.25 G3RUH", "FT8", "GENESIS FSK", "GFSK", "GFSK Pkst", "GFSK Rktr", "GFSK/BPSK",
"GMSK", "GMSK USP", "HRPT", "LoRa", "LRPT", "LSB", "MFSK", "MSK", "MSK AX.100 Mode 5",
"MSK AX.100 Mode 6", "OFDM", "OQPSK", "PPM", "PSK", "PSK31", "PSK63", "QPSK", "QPSK31",
"QPSK63", "SIDLOC", "SQPSK", "SSDV", "SSTV", "UNKNOWN", "USB", "WSJT"
)
} }
@@ -13,12 +13,16 @@
<string name="nav_prefs">Ajustes</string> <string name="nav_prefs">Ajustes</string>
<!-- Satellites screen --> <!-- Satellites screen -->
<string name="sat_type_hint">Tipo: %s</string> <string name="sat_type_hint">Modos: %s</string>
<string name="sat_type_title">Seleccionar tipos</string> <string name="sat_type_title">Seleccionar modos</string>
<string name="sat_search_hint">Id - Nombre</string> <string name="sat_search_hint">Id - Nombre</string>
<string name="sat_search_clear">Vaciar</string> <string name="sat_search_clear">Vaciar</string>
<string name="sat_clear_all">Vaciar todo</string> <string name="sat_clear_all">Vaciar todo</string>
<string name="sat_select_all">Seleccionar todo</string> <string name="sat_select_all">Seleccionar todo</string>
<string name="sat_group_selected">Seleccionados</string>
<string name="sat_group_available">Disponibles</string>
<string name="sat_group_selected_count">Seleccionados (%1$d)</string>
<string name="sat_group_available_count">Disponibles (%1$d)</string>
<string name="sat_empty_list_message"> <string name="sat_empty_list_message">
Asegúrese de que su consulta de búsqueda sea correcta y que la base de datos esté actualizada</string> Asegúrese de que su consulta de búsqueda sea correcta y que la base de datos esté actualizada</string>
<string name="sat_warning_title">Alarma\!</string> <string name="sat_warning_title">Alarma\!</string>
@@ -26,7 +30,7 @@
Esta aplicación incluye más de 9000 satélites. Esta aplicación incluye más de 9000 satélites.
No tiene sentido rastrearlos todos a la vez. No tiene sentido rastrearlos todos a la vez.
\n\nIntenta siempre limitar la lista a los que te \n\nIntenta siempre limitar la lista a los que te
interesen mediante la búsqueda y el selector de tipos.</string> interesen mediante la búsqueda y el selector de modos.</string>
<!-- Passes screen --> <!-- Passes screen -->
<string name="pass_filter_title">Filtrar pases</string> <string name="pass_filter_title">Filtrar pases</string>
@@ -13,19 +13,23 @@
<string name="nav_prefs">Настройки</string> <string name="nav_prefs">Настройки</string>
<!-- Satellites screen --> <!-- Satellites screen -->
<string name="sat_type_hint">Тип: %s</string> <string name="sat_type_hint">Режимы: %s</string>
<string name="sat_type_title">Выберите типы</string> <string name="sat_type_title">Выберите режимы</string>
<string name="sat_search_hint">Id - Название</string> <string name="sat_search_hint">Id - Название</string>
<string name="sat_search_clear">Очистить</string> <string name="sat_search_clear">Очистить</string>
<string name="sat_clear_all">Очистить</string> <string name="sat_clear_all">Очистить</string>
<string name="sat_select_all">Выбрать</string> <string name="sat_select_all">Выбрать</string>
<string name="sat_group_selected">Выбранные</string>
<string name="sat_group_available">Доступные</string>
<string name="sat_group_selected_count">Выбранные (%1$d)</string>
<string name="sat_group_available_count">Доступные (%1$d)</string>
<string name="sat_empty_list_message">Убедитесь, что ваш поисковый запрос верен и база данных обновлена</string> <string name="sat_empty_list_message">Убедитесь, что ваш поисковый запрос верен и база данных обновлена</string>
<string name="sat_warning_title">Внимание\!</string> <string name="sat_warning_title">Внимание\!</string>
<string name="sat_warning_message"> <string name="sat_warning_message">
В этом приложении перечислено более 9000 спутников. В этом приложении перечислено более 9000 спутников.
Отслеживать их все одновременно бессмысленно. Отслеживать их все одновременно бессмысленно.
\n\nВсегда старайтесь сузить список до тех, \n\nВсегда старайтесь сузить список до тех,
которые вас интересуют, используя поиск и селектор типов.</string> которые вас интересуют, используя поиск и выбор режимов.</string>
<!-- Passes screen --> <!-- Passes screen -->
<string name="pass_filter_title">Фильтровать пролеты</string> <string name="pass_filter_title">Фильтровать пролеты</string>
@@ -13,19 +13,23 @@
<string name="nav_prefs">පසුතල</string> <string name="nav_prefs">පසුතල</string>
<!-- Satellites screen --> <!-- Satellites screen -->
<string name="sat_type_hint">වර්ගය: %s</string> <string name="sat_type_hint">මාදිලි: %s</string>
<string name="sat_type_title">ආකාර තෝරන්න</string> <string name="sat_type_title">මාදිලි තෝරන්න</string>
<string name="sat_search_hint">Id - නම</string> <string name="sat_search_hint">Id - නම</string>
<string name="sat_search_clear">පිරිසිදු ක°</string> <string name="sat_search_clear">පිරිසිදු ක°</string>
<string name="sat_clear_all">සියල්ල පිරිසිදු ක°</string> <string name="sat_clear_all">සියල්ල පිරිසිදු ක°</string>
<string name="sat_select_all">සියල්ල තෝරන්න</string> <string name="sat_select_all">සියල්ල තෝරන්න</string>
<string name="sat_group_selected">තෝරාගත්</string>
<string name="sat_group_available">තිබෙන</string>
<string name="sat_group_selected_count">තෝරාගත් (%1$d)</string>
<string name="sat_group_available_count">තිබෙන (%1$d)</string>
<string name="sat_empty_list_message"> <string name="sat_empty_list_message">
ඔබගේ සෙවුම් විමසුම නිවැරදි බවත් දත්ත සමුදාය යාවත්කාලීන කර ඇති බවත් සහතික කර ගන්න</string> ඔබගේ සෙවුම් විමසුම නිවැරදි බවත් දත්ත සමුදාය යාවත්කාලීන කර ඇති බවත් සහතික කර ගන්න</string>
<string name="sat_warning_title">අවවාදයයි\!</string> <string name="sat_warning_title">අවවාදයයි\!</string>
<string name="sat_warning_message"> <string name="sat_warning_message">
මෙම යෙදුමේ ලැයිස්තුගත කර ඇති චන්ද්‍රිකා 9000 කට වඩා තිබේ. මෙම යෙදුමේ ලැයිස්තුගත කර ඇති චන්ද්‍රිකා 9000 කට වඩා තිබේ.
ඒවා සියල්ලම එකවර නිරීක්ෂණය කිරීම තේරුමක් නැති දෙයක්. ඒවා සියල්ලම එකවර නිරීක්ෂණය කිරීම තේරුමක් නැති දෙයක්.
\n\nසෙවීම සහ වර්ග තේරීම හරහා ඔබ උනන්දුවක් දක්වන \n\nසෙවීම සහ මාදිලි තේරීම හරහා ඔබ උනන්දුවක් දක්වන
ඒවාට පමණක් ලැයිස්තුව පටු කිරීමට සැමවිටම උත්සාහ කරන්න.</string> ඒවාට පමණක් ලැයිස්තුව පටු කිරීමට සැමවිටම උත්සාහ කරන්න.</string>
<!-- Passes screen --> <!-- Passes screen -->
@@ -14,19 +14,23 @@
<string name="nav_prefs">Ayarlar</string> <string name="nav_prefs">Ayarlar</string>
<!-- Satellites screen --> <!-- Satellites screen -->
<string name="sat_type_hint">Uydu türü: %s</string> <string name="sat_type_hint">Modlar: %s</string>
<string name="sat_type_title">Türleri seçin</string> <string name="sat_type_title">Modları seçin</string>
<string name="sat_search_hint">Id - Ad</string> <string name="sat_search_hint">Id - Ad</string>
<string name="sat_search_clear">Temizle</string> <string name="sat_search_clear">Temizle</string>
<string name="sat_clear_all">Tümünü temizle</string> <string name="sat_clear_all">Tümünü temizle</string>
<string name="sat_select_all">Tümünü seç</string> <string name="sat_select_all">Tümünü seç</string>
<string name="sat_group_selected">Seçilenler</string>
<string name="sat_group_available">Kullanılabilir</string>
<string name="sat_group_selected_count">Seçilenler (%1$d)</string>
<string name="sat_group_available_count">Kullanılabilir (%1$d)</string>
<string name="sat_empty_list_message">"Arama sorgunuzun doğru olduğundan ve veritabanının güncellendiğinden emin olun"</string> <string name="sat_empty_list_message">"Arama sorgunuzun doğru olduğundan ve veritabanının güncellendiğinden emin olun"</string>
<string name="sat_warning_title">Uyarı\!</string> <string name="sat_warning_title">Uyarı\!</string>
<string name="sat_warning_message"> <string name="sat_warning_message">
Bu uygulamada 9000\'den fazla uydu listelenmiştir. Bu uygulamada 9000\'den fazla uydu listelenmiştir.
Hepsini aynı anda takip etmek anlamsızdır. Hepsini aynı anda takip etmek anlamsızdır.
\n\nListeyi yalnızca ilgilendiğiniz uydularla \n\nListeyi yalnızca ilgilendiğiniz uydularla
daraltmak için arama ve tür seçiciyi kullanın.</string> daraltmak için arama ve mod seçiciyi kullanın.</string>
<!-- Passes screen --> <!-- Passes screen -->
<string name="pass_filter_title">Geçişleri filtrele</string> <string name="pass_filter_title">Geçişleri filtrele</string>
@@ -13,12 +13,16 @@
<string name="nav_prefs">Налаштування</string> <string name="nav_prefs">Налаштування</string>
<!-- Satellites screen --> <!-- Satellites screen -->
<string name="sat_type_hint">Тип: %s</string> <string name="sat_type_hint">Режими: %s</string>
<string name="sat_type_title">Виберіть типи</string> <string name="sat_type_title">Виберіть режими</string>
<string name="sat_search_hint">Id - Назва</string> <string name="sat_search_hint">Id - Назва</string>
<string name="sat_search_clear">Очистити</string> <string name="sat_search_clear">Очистити</string>
<string name="sat_clear_all">Очистити всі</string> <string name="sat_clear_all">Очистити всі</string>
<string name="sat_select_all">Вибрати всі</string> <string name="sat_select_all">Вибрати всі</string>
<string name="sat_group_selected">Обрані</string>
<string name="sat_group_available">Доступні</string>
<string name="sat_group_selected_count">Обрані (%1$d)</string>
<string name="sat_group_available_count">Доступні (%1$d)</string>
<string name="sat_empty_list_message"> <string name="sat_empty_list_message">
Переконайтеся, що ваш пошуковий запит правильний, а база даних оновлена</string> Переконайтеся, що ваш пошуковий запит правильний, а база даних оновлена</string>
<string name="sat_warning_title">Увага\!</string> <string name="sat_warning_title">Увага\!</string>
@@ -26,7 +30,7 @@
У цьому додатку перелічено понад 9000 супутників. У цьому додатку перелічено понад 9000 супутників.
Немає сенсу відстежувати їх усі одночасно. Немає сенсу відстежувати їх усі одночасно.
\n\nЗавжди намагайтеся звузити список лише до тих, \n\nЗавжди намагайтеся звузити список лише до тих,
які вас цікавлять, за допомогою пошуку та селектора типів.</string> які вас цікавлять, за допомогою пошуку та вибору режимів.</string>
<!-- Passes screen --> <!-- Passes screen -->
<string name="pass_filter_title">Фільтр прольотів</string> <string name="pass_filter_title">Фільтр прольотів</string>
@@ -13,15 +13,19 @@
<string name="nav_prefs">设置</string> <string name="nav_prefs">设置</string>
<!-- Satellites screen --> <!-- Satellites screen -->
<string name="sat_type_hint">类型:%s</string> <string name="sat_type_hint">模式:%s</string>
<string name="sat_type_title">选择类型</string> <string name="sat_type_title">选择模式</string>
<string name="sat_search_hint">Id - 名称</string> <string name="sat_search_hint">Id - 名称</string>
<string name="sat_search_clear">清空</string> <string name="sat_search_clear">清空</string>
<string name="sat_clear_all">全部清空</string> <string name="sat_clear_all">全部清空</string>
<string name="sat_select_all">全选</string> <string name="sat_select_all">全选</string>
<string name="sat_group_selected">已选择</string>
<string name="sat_group_available">可用</string>
<string name="sat_group_selected_count">已选择(%1$d)</string>
<string name="sat_group_available_count">可用(%1$d)</string>
<string name="sat_empty_list_message">请确保您的搜索查询正确且数据库已更新</string> <string name="sat_empty_list_message">请确保您的搜索查询正确且数据库已更新</string>
<string name="sat_warning_title">警告\!</string> <string name="sat_warning_title">警告\!</string>
<string name="sat_warning_message">此应用收录了超过9000颗卫星信息\n同时追踪所有卫星并不现实\n建议始终通过搜索和分类筛选功能仅勾选您感兴趣的卫星</string> <string name="sat_warning_message">此应用收录了超过9000颗卫星信息\n同时追踪所有卫星并不现实\n建议始终通过搜索和模式筛选功能仅勾选您感兴趣的卫星</string>
<!-- Passes screen --> <!-- Passes screen -->
<string name="pass_filter_title">筛选过境</string> <string name="pass_filter_title">筛选过境</string>
@@ -14,19 +14,23 @@
<string name="nav_prefs">Settings</string> <string name="nav_prefs">Settings</string>
<!-- Satellites screen --> <!-- Satellites screen -->
<string name="sat_type_hint">Type: %s</string> <string name="sat_type_hint">Modes: %s</string>
<string name="sat_type_title">Select types</string> <string name="sat_type_title">Select modes</string>
<string name="sat_search_hint">Id - Name</string> <string name="sat_search_hint">Id - Name</string>
<string name="sat_search_clear">Clear</string> <string name="sat_search_clear">Clear</string>
<string name="sat_clear_all">Clear all</string> <string name="sat_clear_all">Clear all</string>
<string name="sat_select_all">Select all</string> <string name="sat_select_all">Select all</string>
<string name="sat_group_selected">Selected</string>
<string name="sat_group_available">Available</string>
<string name="sat_group_selected_count">Selected (%1$d)</string>
<string name="sat_group_available_count">Available (%1$d)</string>
<string name="sat_empty_list_message">"Make sure your search query is correct and the DB is updated"</string> <string name="sat_empty_list_message">"Make sure your search query is correct and the DB is updated"</string>
<string name="sat_warning_title">Warning\!</string> <string name="sat_warning_title">Warning\!</string>
<string name="sat_warning_message"> <string name="sat_warning_message">
There are over 9000 satellites listed in this app. There are over 9000 satellites listed in this app.
It makes no sense to track them all at the same time. It makes no sense to track them all at the same time.
\n\nAlways try to narrow down the list to only the ones \n\nAlways try to narrow down the list to only the ones
you\'re interested in via search and types selector.</string> you\'re interested in via search and the modes selector.</string>
<!-- Passes screen --> <!-- Passes screen -->
<string name="pass_filter_title">Filter passes</string> <string name="pass_filter_title">Filter passes</string>
@@ -62,16 +62,9 @@ import com.rtbishop.look4sat.core.presentation.SharedDialog
import com.rtbishop.look4sat.core.presentation.ElevationHighColor import com.rtbishop.look4sat.core.presentation.ElevationHighColor
import com.rtbishop.look4sat.core.presentation.ElevationLowColor import com.rtbishop.look4sat.core.presentation.ElevationLowColor
import com.rtbishop.look4sat.core.presentation.elevationColor import com.rtbishop.look4sat.core.presentation.elevationColor
import com.rtbishop.look4sat.core.domain.source.Sources
import kotlin.math.roundToInt import kotlin.math.roundToInt
private val allModes = listOf(
"AFSK", "AFSK S-Net", "AFSK SALSAT", "AHRPT", "AM", "APT", "BPSK", "BPSK PMT-A3",
"CERTO", "CW", "DQPSK", "DSTAR", "DUV", "FFSK", "FM", "FMN", "FSK", "FSK AX.100 Mode 5",
"FSK AX.100 Mode 6", "FSK AX.25 G3RUH", "GFSK", "GFSK Rktr", "GMSK", "HRPT", "LoRa",
"LRPT", "LSB", "MFSK", "MSK", "MSK AX.100 Mode 5", "MSK AX.100 Mode 6", "OFDM", "OQPSK",
"PSK", "PSK31", "PSK63", "QPSK", "QPSK31", "QPSK63", "SSTV", "USB", "WSJT"
)
private val hourSteps = listOf(1, 2, 4, 8, 12, 24, 48, 72, 96, 120, 144, 168, 192, 216, 240) private val hourSteps = listOf(1, 2, 4, 8, 12, 24, 48, 72, 96, 120, 144, 168, 192, 216, 240)
private const val dayMinutes = 24 * 60 private const val dayMinutes = 24 * 60
private const val minuteStep = 15 private const val minuteStep = 15
@@ -381,7 +374,7 @@ internal fun RadiosDialog(modes: List<String>, cancel: () -> Unit, accept: (List
horizontalArrangement = Arrangement.spacedBy(1.dp), horizontalArrangement = Arrangement.spacedBy(1.dp),
verticalArrangement = Arrangement.spacedBy(1.dp) verticalArrangement = Arrangement.spacedBy(1.dp)
) { ) {
itemsIndexed(allModes) { index, item -> itemsIndexed(Sources.satelliteModes) { index, item ->
Row( Row(
verticalAlignment = Alignment.CenterVertically, verticalAlignment = Alignment.CenterVertically,
modifier = Modifier modifier = Modifier
@@ -60,7 +60,7 @@ class PassesViewModel(
aosEndMinute = settingsRepo.passesSettings.value.aosEndMinute, aosEndMinute = settingsRepo.passesSettings.value.aosEndMinute,
invertAosTimeWindow = settingsRepo.passesSettings.value.invertAosTimeWindow, invertAosTimeWindow = settingsRepo.passesSettings.value.invertAosTimeWindow,
showDeepSpace = settingsRepo.passesSettings.value.showDeepSpace, showDeepSpace = settingsRepo.passesSettings.value.showDeepSpace,
modes = settingsRepo.passesSettings.value.selectedModes, modes = settingsRepo.selectedSatModes.value,
shouldSeeWhatsNew = settingsRepo.otherSettings.value.shouldSeeWhatsNew shouldSeeWhatsNew = settingsRepo.otherSettings.value.shouldSeeWhatsNew
) )
) )
@@ -86,6 +86,11 @@ class PassesViewModel(
} }
} }
} }
viewModelScope.launch {
settingsRepo.selectedSatModes.collectLatest { modes ->
_uiState.update { it.copy(modes = modes) }
}
}
// Main tick loop: reacts to new passes, then ticks every second // Main tick loop: reacts to new passes, then ticks every second
viewModelScope.launch { viewModelScope.launch {
satelliteRepo.passes.collectLatest { allPasses -> satelliteRepo.passes.collectLatest { allPasses ->
@@ -127,21 +132,9 @@ class PassesViewModel(
aosStartMinute = action.aosStartMinute, aosStartMinute = action.aosStartMinute,
aosEndMinute = action.aosEndMinute, aosEndMinute = action.aosEndMinute,
invertAosTimeWindow = action.invertAosTimeWindow, invertAosTimeWindow = action.invertAosTimeWindow,
showDeepSpace = action.showDeepSpace, showDeepSpace = action.showDeepSpace
modes = _uiState.value.modes
)
is PassesAction.FilterRadios ->
applyFilter(
hoursAhead = _uiState.value.hours,
minElevation = _uiState.value.elevation,
lowElevation = _uiState.value.lowElevation,
highElevation = _uiState.value.highElevation,
aosStartMinute = _uiState.value.aosStartMinute,
aosEndMinute = _uiState.value.aosEndMinute,
invertAosTimeWindow = _uiState.value.invertAosTimeWindow,
showDeepSpace = _uiState.value.showDeepSpace,
modes = action.modes
) )
is PassesAction.FilterRadios -> setModesFilter(action.modes)
PassesAction.RefreshPasses -> refreshPasses() PassesAction.RefreshPasses -> refreshPasses()
PassesAction.TogglePassesDialog -> PassesAction.TogglePassesDialog ->
_uiState.update { it.copy(isPassesDialogShown = !it.isPassesDialogShown) } _uiState.update { it.copy(isPassesDialogShown = !it.isPassesDialogShown) }
@@ -235,8 +228,7 @@ class PassesViewModel(
aosStartMinute: Int, aosStartMinute: Int,
aosEndMinute: Int, aosEndMinute: Int,
invertAosTimeWindow: Boolean, invertAosTimeWindow: Boolean,
showDeepSpace: Boolean, showDeepSpace: Boolean
modes: List<String>
) = viewModelScope.launch { ) = viewModelScope.launch {
settingsRepo.setPassesSettings( settingsRepo.setPassesSettings(
PassesSettings( PassesSettings(
@@ -245,8 +237,7 @@ class PassesViewModel(
minElevation, minElevation,
aosStartMinute, aosStartMinute,
aosEndMinute, aosEndMinute,
invertAosTimeWindow, invertAosTimeWindow
modes
) )
) )
settingsRepo.updateOtherSettings { it.copy(lowElevation = lowElevation, highElevation = highElevation) } settingsRepo.updateOtherSettings { it.copy(lowElevation = lowElevation, highElevation = highElevation) }
@@ -259,10 +250,10 @@ class PassesViewModel(
aosStartMinute = aosStartMinute, aosStartMinute = aosStartMinute,
aosEndMinute = aosEndMinute, aosEndMinute = aosEndMinute,
invertAosTimeWindow = invertAosTimeWindow, invertAosTimeWindow = invertAosTimeWindow,
showDeepSpace = showDeepSpace, showDeepSpace = showDeepSpace
modes = modes
) )
} }
val modes = settingsRepo.selectedSatModes.value
satelliteRepo.calculatePasses( satelliteRepo.calculatePasses(
time = System.currentTimeMillis(), time = System.currentTimeMillis(),
hoursAhead = hoursAhead, hoursAhead = hoursAhead,
@@ -274,6 +265,20 @@ class PassesViewModel(
) )
} }
private fun setModesFilter(modes: List<String>) = viewModelScope.launch {
settingsRepo.setSelectedSatModes(modes)
_uiState.update { it.copy(modes = modes) }
satelliteRepo.calculatePasses(
time = System.currentTimeMillis(),
hoursAhead = _uiState.value.hours,
minElevation = _uiState.value.elevation,
aosStartMinute = _uiState.value.aosStartMinute,
aosEndMinute = _uiState.value.aosEndMinute,
invertAosTimeWindow = _uiState.value.invertAosTimeWindow,
modes = modes
)
}
private fun refreshPasses() = viewModelScope.launch { private fun refreshPasses() = viewModelScope.launch {
val settings = settingsRepo.passesSettings.value val settings = settingsRepo.passesSettings.value
satelliteRepo.calculatePasses( satelliteRepo.calculatePasses(
@@ -283,7 +288,7 @@ class PassesViewModel(
aosStartMinute = settings.aosStartMinute, aosStartMinute = settings.aosStartMinute,
aosEndMinute = settings.aosEndMinute, aosEndMinute = settings.aosEndMinute,
invertAosTimeWindow = settings.invertAosTimeWindow, invertAosTimeWindow = settings.invertAosTimeWindow,
modes = settings.selectedModes modes = settingsRepo.selectedSatModes.value
) )
} }
@@ -45,18 +45,18 @@ import com.rtbishop.look4sat.core.presentation.SharedDialog
@Preview(showBackground = true) @Preview(showBackground = true)
@Composable @Composable
private fun MultiTypesDialogPreview() { private fun MultiModesDialogPreview() {
val types = listOf("Amateur", "Geostationary", "Military", "Weather") val modes = listOf("FM", "APT", "SSTV", "BPSK")
MainTheme { MultiTypesDialog(types, emptyList(), {}) {} } MainTheme { MultiModesDialog(modes, emptyList(), {}) {} }
} }
@Composable @Composable
internal fun MultiTypesDialog( internal fun MultiModesDialog(
allTypes: List<String>, types: List<String>, cancel: () -> Unit, accept: (List<String>) -> Unit allModes: List<String>, modes: List<String>, cancel: () -> Unit, accept: (List<String>) -> Unit
) { ) {
val selected = remember { mutableStateOf(types.toSet()) } val selected = remember { mutableStateOf(modes.toSet()) }
val toggle = { type: String -> val toggle = { mode: String ->
selected.value = if (type in selected.value) selected.value - type else selected.value + type selected.value = if (mode in selected.value) selected.value - mode else selected.value + mode
} }
val onAccept = { accept(selected.value.toList()) } val onAccept = { accept(selected.value.toList()) }
SharedDialog(title = stringResource(R.string.sat_type_title), onCancel = cancel, onAccept = onAccept) { SharedDialog(title = stringResource(R.string.sat_type_title), onCancel = cancel, onAccept = onAccept) {
@@ -68,7 +68,7 @@ internal fun MultiTypesDialog(
horizontalArrangement = Arrangement.spacedBy(1.dp), horizontalArrangement = Arrangement.spacedBy(1.dp),
verticalArrangement = Arrangement.spacedBy(1.dp) verticalArrangement = Arrangement.spacedBy(1.dp)
) { ) {
itemsIndexed(allTypes) { index, item -> itemsIndexed(allModes) { index, item ->
Row( Row(
verticalAlignment = Alignment.CenterVertically, verticalAlignment = Alignment.CenterVertically,
modifier = Modifier modifier = Modifier
@@ -87,11 +87,11 @@ private fun SatellitesScreen(
navigateUp: () -> Unit navigateUp: () -> Unit
) { ) {
if (uiState.isDialogShown) { if (uiState.isDialogShown) {
MultiTypesDialog( MultiModesDialog(
allTypes = uiState.typesList, allModes = uiState.modesList,
types = uiState.currentTypes, modes = uiState.currentModes,
cancel = { onAction(SatellitesAction.ToggleTypesDialog) }, cancel = { onAction(SatellitesAction.ToggleModesDialog) },
accept = { onAction(SatellitesAction.SelectTypes(it)) } accept = { onAction(SatellitesAction.SelectModes(it)) }
) )
} }
if (uiState.shouldSeeWarning) { if (uiState.shouldSeeWarning) {
@@ -123,9 +123,9 @@ private fun SatellitesScreen(
Column(modifier = Modifier.layoutPadding(), verticalArrangement = Arrangement.spacedBy(6.dp)) { Column(modifier = Modifier.layoutPadding(), verticalArrangement = Arrangement.spacedBy(6.dp)) {
if (isVerticalLayout()) { if (isVerticalLayout()) {
TopBar { TopBar {
TypeCard( ModeCard(
types = uiState.currentTypes, modes = uiState.currentModes,
onClick = { onAction(SatellitesAction.ToggleTypesDialog) }, onClick = { onAction(SatellitesAction.ToggleModesDialog) },
modifier = Modifier.weight(1f) modifier = Modifier.weight(1f)
) )
PrimaryIconCard( PrimaryIconCard(
@@ -157,9 +157,9 @@ private fun SatellitesScreen(
resId = R.drawable.ic_done, resId = R.drawable.ic_done,
modifier = Modifier.semantics { contentDescription = primCardCd } modifier = Modifier.semantics { contentDescription = primCardCd }
) )
TypeCard( ModeCard(
types = uiState.currentTypes, modes = uiState.currentModes,
onClick = { onAction(SatellitesAction.ToggleTypesDialog) }, onClick = { onAction(SatellitesAction.ToggleModesDialog) },
modifier = Modifier.weight(1f) modifier = Modifier.weight(1f)
) )
SearchBar( SearchBar(
@@ -241,8 +241,8 @@ private fun SearchBar(onQueryChange: (String) -> Unit, modifier: Modifier = Modi
} }
@Composable @Composable
private fun TypeCard(types: List<String>, onClick: () -> Unit, modifier: Modifier = Modifier) { private fun ModeCard(modes: List<String>, onClick: () -> Unit, modifier: Modifier = Modifier) {
val typesText = if (types.isEmpty()) "All" else types.joinToString(", ") val modesText = if (modes.isEmpty()) "All" else modes.joinToString(", ")
ElevatedCard(modifier = modifier) { ElevatedCard(modifier = modifier) {
Row( Row(
horizontalArrangement = Arrangement.spacedBy(12.dp), horizontalArrangement = Arrangement.spacedBy(12.dp),
@@ -252,12 +252,12 @@ private fun TypeCard(types: List<String>, onClick: () -> Unit, modifier: Modifie
.clickable { onClick() }) { .clickable { onClick() }) {
Spacer(Modifier) Spacer(Modifier)
Icon( Icon(
painter = painterResource(id = R.drawable.ic_tags), painter = painterResource(id = R.drawable.ic_radios),
contentDescription = null, contentDescription = null,
tint = MaterialTheme.colorScheme.primary tint = MaterialTheme.colorScheme.primary
) )
Text( Text(
text = stringResource(R.string.sat_type_hint, typesText), text = stringResource(R.string.sat_type_hint, modesText),
fontSize = 16.sp, fontSize = 16.sp,
fontWeight = FontWeight.Medium, fontWeight = FontWeight.Medium,
modifier = Modifier modifier = Modifier
@@ -323,9 +323,54 @@ private fun SatellitesPreview() {
@Composable @Composable
private fun SatellitesCard(items: List<SatItem>, onSelected: (Int, Boolean) -> Unit) { private fun SatellitesCard(items: List<SatItem>, onSelected: (Int, Boolean) -> Unit) {
val selectedItems = items.filter { it.isSelected }
val availableItems = items.filterNot { it.isSelected }
LazyVerticalGrid(columns = GridCells.Adaptive(320.dp)) { LazyVerticalGrid(columns = GridCells.Adaptive(320.dp)) {
items(items = items, key = { item -> item.catnum }) { entry -> stickyHeader(key = "sat_summary_header") {
Satellite(entry, onSelected, Modifier.animateItem()) SummaryHeader(
selectedText = stringResource(R.string.sat_group_selected_count, selectedItems.size),
availableText = stringResource(R.string.sat_group_available_count, availableItems.size)
)
}
if (selectedItems.isNotEmpty()) {
items(items = selectedItems, key = { item -> item.catnum }) { entry ->
Satellite(entry, onSelected, Modifier.animateItem())
}
}
if (availableItems.isNotEmpty()) {
items(items = availableItems, key = { item -> item.catnum }) { entry ->
Satellite(entry, onSelected, Modifier.animateItem())
}
} }
} }
} }
@Composable
private fun SummaryHeader(selectedText: String, availableText: String) {
Row(
verticalAlignment = Alignment.CenterVertically,
horizontalArrangement = Arrangement.SpaceBetween,
modifier = Modifier
.fillMaxWidth()
.background(MaterialTheme.colorScheme.surfaceContainerHighest)
.padding(horizontal = 12.dp, vertical = 4.dp)
) {
Text(
text = selectedText,
style = MaterialTheme.typography.titleSmall,
fontWeight = FontWeight.SemiBold,
color = MaterialTheme.colorScheme.onSurfaceVariant
)
Text(
text = availableText,
style = MaterialTheme.typography.titleSmall,
fontWeight = FontWeight.SemiBold,
color = MaterialTheme.colorScheme.onSurfaceVariant
)
}
}
@@ -24,8 +24,8 @@ data class SatellitesState(
val isLoading: Boolean = true, val isLoading: Boolean = true,
val shouldSeeWarning: Boolean = false, val shouldSeeWarning: Boolean = false,
val itemsList: List<SatItem> = emptyList(), val itemsList: List<SatItem> = emptyList(),
val currentTypes: List<String> = emptyList(), val currentModes: List<String> = emptyList(),
val typesList: List<String> = emptyList() val modesList: List<String> = emptyList()
) )
sealed interface SatellitesAction { sealed interface SatellitesAction {
@@ -34,7 +34,7 @@ sealed interface SatellitesAction {
data class SearchFor(val query: String) : SatellitesAction data class SearchFor(val query: String) : SatellitesAction
data object SelectAll : SatellitesAction data object SelectAll : SatellitesAction
data class SelectSingle(val id: Int, val isTicked: Boolean) : SatellitesAction data class SelectSingle(val id: Int, val isTicked: Boolean) : SatellitesAction
data class SelectTypes(val types: List<String>) : SatellitesAction data class SelectModes(val modes: List<String>) : SatellitesAction
data object ToggleTypesDialog : SatellitesAction data object ToggleModesDialog : SatellitesAction
data object UnselectAll : SatellitesAction data object UnselectAll : SatellitesAction
} }
@@ -35,13 +35,13 @@ class SatellitesViewModel(
private val settingsRepo: ISettingsRepo private val settingsRepo: ISettingsRepo
) : ViewModel() { ) : ViewModel() {
private val defaultTypes = selectionRepo.getCurrentTypes() private val defaultModes = selectionRepo.getCurrentModes()
private val _uiState = MutableStateFlow( private val _uiState = MutableStateFlow(
SatellitesState( SatellitesState(
isLoading = true, isLoading = true,
shouldSeeWarning = settingsRepo.otherSettings.value.shouldSeeWarning, shouldSeeWarning = settingsRepo.otherSettings.value.shouldSeeWarning,
currentTypes = defaultTypes, currentModes = defaultModes,
typesList = selectionRepo.getTypesList() modesList = selectionRepo.getModesList()
) )
) )
val uiState: StateFlow<SatellitesState> = _uiState val uiState: StateFlow<SatellitesState> = _uiState
@@ -49,7 +49,7 @@ class SatellitesViewModel(
init { init {
viewModelScope.launch { viewModelScope.launch {
selectionRepo.setQuery(String()) selectionRepo.setQuery(String())
selectionRepo.setTypes(defaultTypes) selectionRepo.setModes(defaultModes)
selectionRepo.getEntriesFlow().collectLatest { items -> selectionRepo.getEntriesFlow().collectLatest { items ->
_uiState.update { it.copy(isLoading = false, itemsList = items) } _uiState.update { it.copy(isLoading = false, itemsList = items) }
} }
@@ -68,8 +68,8 @@ class SatellitesViewModel(
is SatellitesAction.SearchFor -> searchFor(action.query) is SatellitesAction.SearchFor -> searchFor(action.query)
SatellitesAction.SelectAll -> selectAll(true) SatellitesAction.SelectAll -> selectAll(true)
is SatellitesAction.SelectSingle -> selectSingle(action.id, action.isTicked) is SatellitesAction.SelectSingle -> selectSingle(action.id, action.isTicked)
is SatellitesAction.SelectTypes -> selectTypes(action.types) is SatellitesAction.SelectModes -> selectModes(action.modes)
SatellitesAction.ToggleTypesDialog -> toggleTypesDialog() SatellitesAction.ToggleModesDialog -> toggleModesDialog()
SatellitesAction.UnselectAll -> selectAll(false) SatellitesAction.UnselectAll -> selectAll(false)
} }
} }
@@ -86,12 +86,12 @@ class SatellitesViewModel(
selectionRepo.setSelection(listOf(id), isTicked.not()) selectionRepo.setSelection(listOf(id), isTicked.not())
} }
private fun selectTypes(types: List<String>) = viewModelScope.launch { private fun selectModes(modes: List<String>) = viewModelScope.launch {
selectionRepo.setTypes(types) selectionRepo.setModes(modes)
_uiState.update { it.copy(currentTypes = types, isDialogShown = false) } _uiState.update { it.copy(currentModes = modes, isDialogShown = false) }
} }
private fun toggleTypesDialog() { private fun toggleModesDialog() {
_uiState.update { it.copy(isDialogShown = !it.isDialogShown) } _uiState.update { it.copy(isDialogShown = !it.isDialogShown) }
} }