Created SelectionRepository, cleaned up code

This commit is contained in:
Arty Bishop committed 2025-12-27 15:01:33 +00:00
1 parent 5d2a870319
commit 208c587ddf
12 files changed
+156 -130

No files matched your search

@@ -9,6 +9,7 @@ import androidx.room.Room
import com.rtbishop.look4sat.data.DataParser import com.rtbishop.look4sat.data.DataParser
import com.rtbishop.look4sat.data.DataRepository import com.rtbishop.look4sat.data.DataRepository
import com.rtbishop.look4sat.data.SatelliteRepository import com.rtbishop.look4sat.data.SatelliteRepository
import com.rtbishop.look4sat.data.SelectionRepository
import com.rtbishop.look4sat.domain.IDataRepository import com.rtbishop.look4sat.domain.IDataRepository
import com.rtbishop.look4sat.domain.ISatelliteRepository import com.rtbishop.look4sat.domain.ISatelliteRepository
import com.rtbishop.look4sat.domain.ISensorRepository import com.rtbishop.look4sat.domain.ISensorRepository
@@ -58,6 +59,10 @@ class MainContainer(private val context: Context) {
return DataRepository(parser, fileSource, entries, radios, remoteSource, scope, settings) return DataRepository(parser, fileSource, entries, radios, remoteSource, scope, settings)
} }
fun provideSelectionRepository(): SelectionRepository {
return SelectionRepository(Dispatchers.Default, dataRepository, settingsRepository)
}
private fun provideSensorRepository(): ISensorRepository { private fun provideSensorRepository(): ISensorRepository {
val manager = context.getSystemService(Context.SENSOR_SERVICE) as SensorManager val manager = context.getSystemService(Context.SENSOR_SERVICE) as SensorManager
val sensor = manager.getDefaultSensor(Sensor.TYPE_ROTATION_VECTOR) val sensor = manager.getDefaultSensor(Sensor.TYPE_ROTATION_VECTOR)
@@ -28,9 +28,9 @@ interface EntriesDao {
@Query("SELECT COUNT(*) FROM entries") @Query("SELECT COUNT(*) FROM entries")
fun getEntriesTotal(): Flow<Int> fun getEntriesTotal(): Flow<Int>
@Transaction // @Transaction
@Query("SELECT catnum, name FROM entries ORDER BY name ASC") @Query("SELECT catnum, name FROM entries ORDER BY name ASC")
suspend fun getEntriesWithModes(): List<SatItem> suspend fun getEntriesList(): List<SatItem>
@Transaction @Transaction
@Query("SELECT * FROM entries WHERE catnum IN (:selectedIds)") @Query("SELECT * FROM entries WHERE catnum IN (:selectedIds)")
@@ -29,8 +29,8 @@ class EntriesStorage(private val entriesDao: EntriesDao) : IEntriesStorage {
override fun getEntriesTotal() = entriesDao.getEntriesTotal() override fun getEntriesTotal() = entriesDao.getEntriesTotal()
override suspend fun getEntriesWithModes(): List<DomainItem> { override suspend fun getEntriesList(): List<DomainItem> {
return entriesDao.getEntriesWithModes().toDomainItems() return entriesDao.getEntriesList().toDomainItems()
} }
override suspend fun getEntriesWithIds(ids: List<Int>): List<Satellite> { override suspend fun getEntriesWithIds(ids: List<Int>): List<Satellite> {
@@ -50,7 +50,7 @@ class EntriesStorage(private val entriesDao: EntriesDao) : IEntriesStorage {
private fun DomainEntry.toFramework() = FrameworkEntry(this.data, this.comment) private fun DomainEntry.toFramework() = FrameworkEntry(this.data, this.comment)
private fun FrameworkItem.toDomain() = DomainItem(this.catnum, this.name, this.modes, false) private fun FrameworkItem.toDomain() = DomainItem(this.catnum, this.name, false)
private fun List<DomainEntry>.toFrameworkEntries() = this.map { entry -> entry.toFramework() } private fun List<DomainEntry>.toFrameworkEntries() = this.map { entry -> entry.toFramework() }
@@ -17,16 +17,14 @@
*/ */
package com.rtbishop.look4sat.framework.model package com.rtbishop.look4sat.framework.model
import androidx.room.Relation
data class SatItem( data class SatItem(
val catnum: Int, val catnum: Int,
val name: String, val name: String
@Relation( // @Relation(
parentColumn = "catnum", // parentColumn = "catnum",
entity = SatRadio::class, // entity = SatRadio::class,
entityColumn = "catnum", // entityColumn = "catnum",
projection = ["mode"] // projection = ["mode"]
) // )
val modes: List<String> // val modes: List<String>
) )
@@ -9,7 +9,6 @@ import androidx.compose.foundation.lazy.items
import androidx.compose.foundation.text.BasicTextField import androidx.compose.foundation.text.BasicTextField
import androidx.compose.material3.* import androidx.compose.material3.*
import androidx.compose.runtime.Composable import androidx.compose.runtime.Composable
import androidx.compose.runtime.collectAsState
import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.saveable.rememberSaveable import androidx.compose.runtime.saveable.rememberSaveable
import androidx.compose.ui.Alignment import androidx.compose.ui.Alignment
@@ -26,7 +25,6 @@ import androidx.compose.ui.unit.dp
import androidx.compose.ui.unit.sp import androidx.compose.ui.unit.sp
import androidx.lifecycle.viewmodel.compose.viewModel import androidx.lifecycle.viewmodel.compose.viewModel
import com.rtbishop.look4sat.R import com.rtbishop.look4sat.R
import com.rtbishop.look4sat.model.DataState
import com.rtbishop.look4sat.model.SatItem import com.rtbishop.look4sat.model.SatItem
import com.rtbishop.look4sat.presentation.CardIcon import com.rtbishop.look4sat.presentation.CardIcon
import com.rtbishop.look4sat.presentation.CardLoadingIndicator import com.rtbishop.look4sat.presentation.CardLoadingIndicator
@@ -37,25 +35,33 @@ import com.rtbishop.look4sat.presentation.onClick
@Composable @Composable
fun EntriesScreen(navToPasses: () -> Unit) { fun EntriesScreen(navToPasses: () -> Unit) {
val viewModel = viewModel(EntriesViewModel::class.java, factory = EntriesViewModel.Factory) val viewModel = viewModel(EntriesViewModel::class.java, factory = EntriesViewModel.Factory)
val state = viewModel.satData.collectAsState(initial = DataState.Loading) val uiState = viewModel.uiState.value
val showDialog = rememberSaveable { mutableStateOf(false) } val showDialog = rememberSaveable { mutableStateOf(false) }
val toggleDialog = { showDialog.value = showDialog.value.not() } val toggleDialog = { showDialog.value = showDialog.value.not() }
if (showDialog.value) { if (showDialog.value) {
TypesDialog(list = viewModel.satTypes, selected = viewModel.getSatType(), toggleDialog) { TypesDialog(list = uiState.typesList, selected = uiState.currentType, toggleDialog) {
viewModel.setSatType(it) viewModel.setType(it)
} }
} }
val unselectAll = { viewModel.selectCurrentItems(false) } val unselectAll = { viewModel.updateSelection(false) }
val selectAll = { viewModel.selectCurrentItems(true) } val selectAll = { viewModel.updateSelection(true) }
Column(modifier = Modifier.padding(6.dp), verticalArrangement = Arrangement.spacedBy(6.dp)) { Column(modifier = Modifier.padding(6.dp), verticalArrangement = Arrangement.spacedBy(6.dp)) {
TopBar(setQuery = { newQuery: String -> viewModel.setQuery(newQuery) }, saveSelection = { TopBar(setQuery = { newQuery: String -> viewModel.setQuery(newQuery) }, saveSelection = {
viewModel.saveSelection() viewModel.saveSelection()
navToPasses() navToPasses()
}) })
MiddleBar(viewModel.getSatType(), { toggleDialog() }, { unselectAll() }, { selectAll() }) MiddleBar(uiState.currentType, { toggleDialog() }, { unselectAll() }, { selectAll() })
EntriesCard(state = state.value) { list, value -> viewModel.updateSelection(list, value) } ElevatedCard(modifier = Modifier.fillMaxSize()) {
if (uiState.isLoading) {
CardLoadingIndicator()
} else {
EntriesCard(uiState.itemsList) { list, value ->
viewModel.updateSelection(list, value)
}
}
}
} }
} }
@@ -169,7 +175,7 @@ private fun EntryTypeCard(type: String, onClick: () -> Unit, modifier: Modifier
@Preview(showBackground = true) @Preview(showBackground = true)
@Composable @Composable
private fun EntryPreview() { private fun EntryPreview() {
val satItem = SatItem(45555, "SatName", emptyList(), true) val satItem = SatItem(45555, "Very long Satellite name", true)
MainTheme { Entry(item = satItem, onSelected = { _, _ -> run {} }, modifier = Modifier) } MainTheme { Entry(item = satItem, onSelected = { _, _ -> run {} }, modifier = Modifier) }
} }
@@ -206,23 +212,15 @@ private fun Entry(item: SatItem, onSelected: (List<Int>, Boolean) -> Unit, modif
@Preview(showBackground = true) @Preview(showBackground = true)
@Composable @Composable
private fun EntriesPreview() { private fun EntriesPreview() {
MainTheme { EntriesCard(DataState.Loading) { _, _ -> run {} } } MainTheme { EntriesCard(emptyList()) { _, _ -> run {} } }
} }
@OptIn(ExperimentalFoundationApi::class) @OptIn(ExperimentalFoundationApi::class)
@Composable @Composable
fun EntriesCard(state: DataState<List<SatItem>>, onSelected: (List<Int>, Boolean) -> Unit) { fun EntriesCard(items: List<SatItem>, onSelected: (List<Int>, Boolean) -> Unit) {
ElevatedCard(modifier = Modifier.fillMaxSize()) { LazyColumn {
when (state) { items(items = items, key = { item -> item.catnum }) { entry ->
is DataState.Success -> { Entry(entry, onSelected, Modifier.animateItemPlacement())
LazyColumn {
items(items = state.data, key = { item -> item.catnum }) { entry ->
Entry(entry, onSelected, Modifier.animateItemPlacement())
}
}
}
else -> CardLoadingIndicator()
} }
} }
} }
@@ -0,0 +1,10 @@
package com.rtbishop.look4sat.presentation.entries
import com.rtbishop.look4sat.model.SatItem
data class EntriesScreenState(
val isLoading: Boolean,
val itemsList: List<SatItem>,
val currentType: String,
val typesList: List<String>,
)
@@ -17,115 +17,59 @@
*/ */
package com.rtbishop.look4sat.presentation.entries package com.rtbishop.look4sat.presentation.entries
import androidx.compose.runtime.mutableStateOf
import androidx.lifecycle.ViewModel import androidx.lifecycle.ViewModel
import androidx.lifecycle.ViewModelProvider import androidx.lifecycle.ViewModelProvider
import androidx.lifecycle.viewModelScope import androidx.lifecycle.viewModelScope
import androidx.lifecycle.viewmodel.initializer import androidx.lifecycle.viewmodel.initializer
import androidx.lifecycle.viewmodel.viewModelFactory import androidx.lifecycle.viewmodel.viewModelFactory
import com.rtbishop.look4sat.MainApplication import com.rtbishop.look4sat.MainApplication
import com.rtbishop.look4sat.domain.IDataRepository import com.rtbishop.look4sat.data.SelectionRepository
import com.rtbishop.look4sat.domain.ISettingsRepository
import com.rtbishop.look4sat.model.DataState
import com.rtbishop.look4sat.model.SatItem
import kotlinx.coroutines.CoroutineDispatcher
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.ExperimentalCoroutinesApi
import kotlinx.coroutines.delay import kotlinx.coroutines.delay
import kotlinx.coroutines.flow.*
import kotlinx.coroutines.launch import kotlinx.coroutines.launch
import kotlinx.coroutines.withContext
import java.util.*
@OptIn(ExperimentalCoroutinesApi::class) class EntriesViewModel(private val selectionRepo: SelectionRepository) : ViewModel() {
class EntriesViewModel(
private val dataRepository: IDataRepository,
private val settingsRepository: ISettingsRepository,
private val dispatcher: CoroutineDispatcher = Dispatchers.Default
) : ViewModel() {
private val satType = MutableStateFlow("All") private val defaultUiState = EntriesScreenState(
private val currentQuery = MutableStateFlow(String()) isLoading = true,
private val itemsFromRepo = MutableStateFlow<List<SatItem>>(emptyList()) itemsList = emptyList(),
private val itemsWithType = satType.flatMapLatest { type -> currentType = selectionRepo.getCurrentType(),
itemsFromRepo.map { items -> filterByType(items, type) } typesList = selectionRepo.getTypesList()
} )
private val itemsWithQuery = currentQuery.flatMapLatest { query -> val uiState = mutableStateOf(defaultUiState)
itemsWithType.map { items -> filterByQuery(items, query) }
}
val satData = itemsWithQuery.map { items -> DataState.Success(items) }
val satTypes = dataRepository.getSatelliteTypes()
init { init {
viewModelScope.launch { viewModelScope.launch {
delay(250) delay(1000)
itemsFromRepo.value = dataRepository.getEntriesWithSelection() selectionRepo.getEntriesFlow().collect { items ->
uiState.value = uiState.value.copy(isLoading = false, itemsList = items)
}
} }
} }
fun getSatType() = satType.value fun setType(type: String) = viewModelScope.launch {
selectionRepo.setType(type)
fun setSatType(type: String) { uiState.value = uiState.value.copy(currentType = type)
satType.value = type
} }
fun selectCurrentItems(selectAll: Boolean) = viewModelScope.launch { fun setQuery(query: String) = viewModelScope.launch { selectionRepo.setQuery(query) }
updateEntriesSelection(itemsWithQuery.first().map { item -> item.catnum }, selectAll)
}
fun setQuery(query: String) { fun updateSelection(selectAll: Boolean) = viewModelScope.launch {
currentQuery.value = query selectionRepo.updateSelection(selectAll)
}
fun saveSelection() = viewModelScope.launch {
val newSelection = itemsFromRepo.value.filter { it.isSelected }.map { it.catnum }
settingsRepository.saveEntriesSelection(newSelection)
} }
fun updateSelection(catNums: List<Int>, isSelected: Boolean) = viewModelScope.launch { fun updateSelection(catNums: List<Int>, isSelected: Boolean) = viewModelScope.launch {
updateEntriesSelection(catNums, isSelected) selectionRepo.updateSelection(catNums, isSelected)
} }
private suspend fun updateEntriesSelection(catNums: List<Int>, isSelected: Boolean) { fun saveSelection() = viewModelScope.launch { selectionRepo.saveSelection() }
withContext(dispatcher) {
itemsFromRepo.value.let { itemsAll ->
val copiedList = itemsAll.map { item -> item.copy() }
catNums.forEach { catnum ->
copiedList.find { item -> item.catnum == catnum }?.isSelected = isSelected
}
itemsFromRepo.value = copiedList
}
}
}
private suspend fun filterByType(items: List<SatItem>, type: String): List<SatItem> {
return withContext(dispatcher) {
if (type == "All") return@withContext items
val catnums = settingsRepository.loadSatType(type)
if (catnums.isEmpty()) return@withContext items
return@withContext items.filter { item -> item.catnum in catnums }
}
}
private suspend fun filterByQuery(items: List<SatItem>, query: String): List<SatItem> {
return withContext(dispatcher) {
if (query.isBlank()) return@withContext items
return@withContext try {
items.filter { it.catnum == query.toInt() }
} catch (e: Exception) {
items.filter { item -> item.name.lowercase().contains(query.lowercase()) }
}
}
}
companion object { companion object {
val Factory: ViewModelProvider.Factory = viewModelFactory { val Factory: ViewModelProvider.Factory = viewModelFactory {
val applicationKey = ViewModelProvider.AndroidViewModelFactory.APPLICATION_KEY val applicationKey = ViewModelProvider.AndroidViewModelFactory.APPLICATION_KEY
initializer { initializer {
val container = (this[applicationKey] as MainApplication).container val container = (this[applicationKey] as MainApplication).container
EntriesViewModel( EntriesViewModel(container.provideSelectionRepository())
container.dataRepository,
container.settingsRepository
)
} }
} }
} }
@@ -55,15 +55,13 @@ class DataRepository(
override fun getSatelliteTypes() = remoteSource.satelliteSourcesMap.keys.sorted() override fun getSatelliteTypes() = remoteSource.satelliteSourcesMap.keys.sorted()
override suspend fun getEntriesWithModes() = entrySource.getEntriesWithModes() override suspend fun getEntriesList(): List<SatItem> {
val selectedIds = settingsRepository.satelliteSelection.value
return entrySource.getEntriesList().map { it.copy(isSelected = it.catnum in selectedIds) }
}
override suspend fun getEntriesWithIds(ids: List<Int>) = entrySource.getEntriesWithIds(ids) override suspend fun getEntriesWithIds(ids: List<Int>) = entrySource.getEntriesWithIds(ids)
override suspend fun getEntriesWithSelection(): List<SatItem> {
val selectedIds = settingsRepository.satelliteSelection.value
return getEntriesWithModes().onEach { it.isSelected = it.catnum in selectedIds }
}
override suspend fun getRadiosWithId(id: Int) = radioSource.getRadiosWithId(id) override suspend fun getRadiosWithId(id: Int) = radioSource.getRadiosWithId(id)
override fun updateFromFile(uri: String) { override fun updateFromFile(uri: String) {
@@ -93,6 +91,7 @@ class DataRepository(
settingsRepository.saveSatType(type, catnums) settingsRepository.saveSatType(type, catnums)
importedEntries.addAll(satellites) importedEntries.addAll(satellites)
} }
"McCants", "Classified" -> { "McCants", "Classified" -> {
// unzip and parse tle stream // unzip and parse tle stream
val unzipped = ZipInputStream(stream).apply { nextEntry } val unzipped = ZipInputStream(stream).apply { nextEntry }
@@ -101,6 +100,7 @@ class DataRepository(
settingsRepository.saveSatType(type, catnums) settingsRepository.saveSatType(type, catnums)
importedEntries.addAll(satellites) importedEntries.addAll(satellites)
} }
else -> { else -> {
// parse csv stream // parse csv stream
val parsed = dataParser.parseCSVStream(stream) val parsed = dataParser.parseCSVStream(stream)
@@ -26,7 +26,7 @@ interface IEntriesStorage {
fun getEntriesTotal(): Flow<Int> fun getEntriesTotal(): Flow<Int>
suspend fun getEntriesWithModes(): List<SatItem> suspend fun getEntriesList(): List<SatItem>
suspend fun getEntriesWithIds(ids: List<Int>): List<Satellite> suspend fun getEntriesWithIds(ids: List<Int>): List<Satellite>
@@ -0,0 +1,73 @@
package com.rtbishop.look4sat.data
import com.rtbishop.look4sat.domain.IDataRepository
import com.rtbishop.look4sat.domain.ISettingsRepository
import com.rtbishop.look4sat.model.SatItem
import kotlinx.coroutines.CoroutineDispatcher
import kotlinx.coroutines.ExperimentalCoroutinesApi
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.first
import kotlinx.coroutines.flow.flatMapLatest
import kotlinx.coroutines.flow.map
import kotlinx.coroutines.withContext
@OptIn(ExperimentalCoroutinesApi::class)
class SelectionRepository(
private val dispatcher: CoroutineDispatcher,
private val dataRepository: IDataRepository,
private val settingsRepository: ISettingsRepository
) {
private val currentItems = MutableStateFlow<List<SatItem>>(emptyList())
private val currentType = MutableStateFlow("All")
private val currentQuery = MutableStateFlow("")
private val itemsWithType = currentType.flatMapLatest { type ->
currentItems.map { items -> items.filterByType(type) }
}
private val itemsWithQuery = currentQuery.flatMapLatest { query ->
itemsWithType.map { items -> items.filterByQuery(query) }
}
fun getCurrentType() = currentType.value
fun getTypesList() = dataRepository.getSatelliteTypes()
suspend fun getEntriesFlow() = withContext(dispatcher) {
currentItems.value = dataRepository.getEntriesList()
return@withContext itemsWithQuery
}
suspend fun setType(type: String) = withContext(dispatcher) { currentType.value = type }
suspend fun setQuery(query: String) = withContext(dispatcher) { currentQuery.value = query }
suspend fun updateSelection(selectAll: Boolean) = withContext(dispatcher) {
updateSelection(itemsWithQuery.first().map { item -> item.catnum }, selectAll)
}
suspend fun updateSelection(catNums: List<Int>, isSelected: Boolean) = withContext(dispatcher) {
currentItems.value = currentItems.value.map { item ->
if (item.catnum in catNums) item.copy(isSelected = isSelected) else item
}
}
suspend fun saveSelection() = withContext(dispatcher) {
val currentSelection = currentItems.value.filter { it.isSelected }.map { it.catnum }
settingsRepository.saveEntriesSelection(currentSelection)
}
private suspend fun List<SatItem>.filterByType(type: String) = withContext(dispatcher) {
if (type == "All") return@withContext this@filterByType
val catnums = settingsRepository.loadSatType(type)
if (catnums.isEmpty()) return@withContext this@filterByType
return@withContext this@filterByType.filter { item -> item.catnum in catnums }
}
private suspend fun List<SatItem>.filterByQuery(query: String) = withContext(dispatcher) {
if (query.isBlank()) return@withContext this@filterByQuery
return@withContext try {
this@filterByQuery.filter { it.catnum == query.toInt() }
} catch (e: Exception) {
this@filterByQuery.filter { item -> item.name.lowercase().contains(query.lowercase()) }
}
}
}
@@ -33,12 +33,10 @@ interface IDataRepository {
fun getSatelliteTypes(): List<String> fun getSatelliteTypes(): List<String>
suspend fun getEntriesWithModes(): List<SatItem> suspend fun getEntriesList(): List<SatItem>
suspend fun getEntriesWithIds(ids: List<Int>): List<Satellite> suspend fun getEntriesWithIds(ids: List<Int>): List<Satellite>
suspend fun getEntriesWithSelection(): List<SatItem>
suspend fun getRadiosWithId(id: Int): List<SatRadio> suspend fun getRadiosWithId(id: Int): List<SatRadio>
fun updateFromFile(uri: String) fun updateFromFile(uri: String)
@@ -20,6 +20,6 @@ package com.rtbishop.look4sat.model
data class SatItem( data class SatItem(
val catnum: Int, val catnum: Int,
val name: String, val name: String,
val modes: List<String>, // val modes: List<String>,
var isSelected: Boolean val isSelected: Boolean
) )