Final satellite entries refactoring, hopefully

This commit is contained in:
Arty Bishop committed 2021-03-30 21:43:20 +01:00
1 parent 2044ea273b
commit 6b3a3b31d6
7 files changed
+160 -220

No files matched your search

@@ -37,8 +37,6 @@ class EntriesAdapter : RecyclerView.Adapter<EntriesAdapter.SatItemHolder>() {
}
}
private val listDiffer = AsyncListDiffer(this, diffCallback)
private val allItems = mutableListOf<SatItem>()
private var shouldSelectAll = true
private lateinit var entriesClickListener: EntriesClickListener
interface EntriesClickListener {
@@ -49,24 +47,7 @@ class EntriesAdapter : RecyclerView.Adapter<EntriesAdapter.SatItemHolder>() {
entriesClickListener = listener
}
fun submitAllItems(items: List<SatItem>) {
allItems.clear()
allItems.addAll(items)
listDiffer.submitList(items)
}
fun selectCurrentItems() {
val newList = mutableListOf<SatItem>()
listDiffer.currentList.forEach { item ->
item.isSelected = shouldSelectAll
newList.add(item)
}
submitCurrentItems(newList)
entriesClickListener.updateSelection(newList.map { it.catNum }, shouldSelectAll)
shouldSelectAll = !shouldSelectAll
}
private fun submitCurrentItems(items: List<SatItem>) {
fun submitItems(items: List<SatItem>) {
listDiffer.submitList(items)
}
@@ -19,7 +19,6 @@ package com.rtbishop.look4sat.ui.entriesScreen
import android.os.Bundle
import android.view.View
import android.widget.SearchView
import androidx.activity.result.contract.ActivityResultContracts
import androidx.fragment.app.Fragment
import androidx.fragment.app.viewModels
@@ -37,27 +36,24 @@ import com.rtbishop.look4sat.utility.getNavResult
import dagger.hilt.android.AndroidEntryPoint
@AndroidEntryPoint
class EntriesFragment : Fragment(R.layout.fragment_entries), SearchView.OnQueryTextListener {
class EntriesFragment : Fragment(R.layout.fragment_entries) {
private val viewModel: EntriesViewModel by viewModels()
private val filePicker =
registerForActivityResult(ActivityResultContracts.GetContent()) { uri ->
uri?.let { viewModel.importSatDataFromFile(uri) }
}
private var binding: FragmentEntriesBinding? = null
private var entriesAdapter: EntriesAdapter? = null
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState)
setupComponents(view)
setupObservers()
}
private fun setupComponents(view: View) {
entriesAdapter = EntriesAdapter().apply {
val entriesAdapter = EntriesAdapter().apply {
setEntriesClickListener(viewModel)
}
binding = FragmentEntriesBinding.bind(view).apply {
val binding = FragmentEntriesBinding.bind(view).apply {
entriesRecycler.apply {
setHasFixedSize(true)
adapter = entriesAdapter
@@ -68,82 +64,49 @@ class EntriesFragment : Fragment(R.layout.fragment_entries), SearchView.OnQueryT
importWeb.setOnClickListener { findNavController().navigate(R.id.nav_dialog_sources) }
importFile.setOnClickListener { filePicker.launch("*/*") }
selectMode.setOnClickListener { showModesDialog() }
selectAll.setOnClickListener { entriesAdapter?.selectCurrentItems() }
searchBar.setOnQueryTextListener(this@EntriesFragment)
selectAll.setOnClickListener { viewModel.selectCurrentItems() }
searchBar.setOnQueryTextListener(viewModel)
}
}
private fun setupObservers() {
viewModel.satData.observe(viewLifecycleOwner, { result ->
when (result) {
is Result.Success -> setLoaded(result.data)
is Result.InProgress -> setLoading()
is Result.Error -> setError()
}
viewModel.satData.observe(viewLifecycleOwner, { satData ->
handleSatData(satData, binding, entriesAdapter)
})
getNavResult<List<String>>(R.id.nav_entries, "sources") { result ->
result.map { TleSource(it) }.let { sources ->
if (sources.isNullOrEmpty()) {
viewModel.importSatDataFromSources()
} else {
viewModel.importSatDataFromSources(sources)
}
}
getNavResult<List<String>>(R.id.nav_entries, "sources") { navResult ->
handleNavResult(navResult)
}
}
private fun setLoaded(items: List<SatItem>) {
if (items.isEmpty()) {
binding?.apply {
entriesEmptyError.visibility = View.VISIBLE
entriesProgress.visibility = View.INVISIBLE
entriesRecycler.visibility = View.INVISIBLE
}
} else {
entriesAdapter?.submitAllItems(items)
binding?.apply {
entriesEmptyError.visibility = View.INVISIBLE
entriesProgress.visibility = View.INVISIBLE
entriesRecycler.visibility = View.VISIBLE
entriesRecycler.scrollToPosition(0)
}
}
}
private fun setLoading() {
binding?.apply {
entriesEmptyError.visibility = View.INVISIBLE
entriesProgress.visibility = View.VISIBLE
entriesRecycler.visibility = View.INVISIBLE
}
}
private fun setError() {
binding?.apply {
entriesEmptyError.visibility = View.INVISIBLE
entriesProgress.visibility = View.INVISIBLE
entriesRecycler.visibility = View.VISIBLE
}
val errorMsg = getString(R.string.entries_update_error)
Snackbar.make(requireView(), errorMsg, Snackbar.LENGTH_SHORT).show()
}
private fun showModesDialog() {
viewModel.createModesDialog(requireContext()).show()
}
override fun onQueryTextSubmit(query: String): Boolean {
return true
private fun handleSatData(
result: Result<List<SatItem>>,
binding: FragmentEntriesBinding,
entriesAdapter: EntriesAdapter
) {
when (result) {
is Result.Success -> {
entriesAdapter.submitItems(result.data)
binding.entriesProgress.visibility = View.INVISIBLE
binding.entriesRecycler.visibility = View.VISIBLE
binding.entriesRecycler.scrollToPosition(0)
}
is Result.InProgress -> {
binding.entriesProgress.visibility = View.VISIBLE
binding.entriesRecycler.visibility = View.INVISIBLE
}
is Result.Error -> {
binding.entriesProgress.visibility = View.INVISIBLE
binding.entriesRecycler.visibility = View.VISIBLE
val errorMsg = getString(R.string.entries_update_error)
Snackbar.make(requireView(), errorMsg, Snackbar.LENGTH_SHORT).show()
}
}
}
override fun onQueryTextChange(newText: String): Boolean {
viewModel.setNewQuery(newText)
return true
}
override fun onDestroyView() {
super.onDestroyView()
entriesAdapter = null
binding = null
private fun handleNavResult(result: List<String>) {
result.map { sourceUrl -> TleSource(sourceUrl) }.let { sources ->
viewModel.importSatDataFromSources(sources)
}
}
}
@@ -19,6 +19,7 @@ package com.rtbishop.look4sat.ui.entriesScreen
import android.content.Context
import android.net.Uri
import android.widget.SearchView
import androidx.appcompat.app.AlertDialog
import androidx.lifecycle.*
import com.google.android.material.dialog.MaterialAlertDialogBuilder
@@ -40,7 +41,7 @@ import kotlin.system.measureTimeMillis
class EntriesViewModel @Inject constructor(
private val prefsRepo: PrefsRepo,
private val satelliteRepo: SatelliteRepo
) : ViewModel(), EntriesAdapter.EntriesClickListener {
) : ViewModel(), EntriesAdapter.EntriesClickListener, SearchView.OnQueryTextListener {
private val transModes = MutableLiveData(prefsRepo.loadModesSelection())
private val currentQuery = MutableLiveData(String())
@@ -53,6 +54,7 @@ class EntriesViewModel @Inject constructor(
private val _satData = MediatorLiveData<Result<List<SatItem>>>().apply {
addSource(itemsWithQuery) { value -> this.value = value }
}
private var shouldSelectAll = true
val satData: LiveData<Result<List<SatItem>>> = _satData
fun importSatDataFromFile(uri: Uri) {
@@ -66,13 +68,15 @@ class EntriesViewModel @Inject constructor(
}
}
fun importSatDataFromSources(sources: List<TleSource> = prefsRepo.loadDefaultSources()) {
fun importSatDataFromSources(sources: List<TleSource>) {
viewModelScope.launch {
_satData.value = Result.InProgress
val satSources = if (sources.isNotEmpty()) sources
else prefsRepo.loadDefaultSources()
val updateMillis = measureTimeMillis {
try {
prefsRepo.saveTleSources(sources)
satelliteRepo.importSatDataFromWeb(sources)
prefsRepo.saveTleSources(satSources)
satelliteRepo.importSatDataFromWeb(satSources)
} catch (exception: Exception) {
_satData.value = Result.Error(exception)
}
@@ -81,6 +85,20 @@ class EntriesViewModel @Inject constructor(
}
}
fun selectCurrentItems() {
val newList = mutableListOf<SatItem>()
val currentValue = _satData.value
if (currentValue is Result.Success) {
currentValue.data.forEach { item ->
item.isSelected = shouldSelectAll
newList.add(item)
}
}
_satData.value = Result.Success(newList)
updateSelection(newList.map { it.catNum }, shouldSelectAll)
shouldSelectAll = !shouldSelectAll
}
fun createModesDialog(context: Context): AlertDialog {
val modes = arrayOf(
"AFSK", "AFSK S-Net", "AFSK SALSAT", "AHRPT", "AM", "APT", "BPSK", "BPSK PMT-A3",
@@ -101,7 +119,8 @@ class EntriesViewModel @Inject constructor(
}
}
setPositiveButton(context.getString(android.R.string.ok)) { _, _ ->
setNewModes(selectedModes)
transModes.value = selectedModes
prefsRepo.saveModesSelection(selectedModes)
}
setNeutralButton(context.getString(android.R.string.cancel)) { dialog, _ ->
dialog.dismiss()
@@ -110,13 +129,17 @@ class EntriesViewModel @Inject constructor(
return dialogBuilder.create()
}
fun setNewQuery(newQuery: String) {
currentQuery.value = newQuery
override fun onQueryTextSubmit(query: String): Boolean {
return true
}
private fun setNewModes(newModes: List<String>) {
transModes.value = newModes
prefsRepo.saveModesSelection(newModes)
override fun onQueryTextChange(newText: String): Boolean {
currentQuery.value = newText
return true
}
override fun updateSelection(catNums: List<Int>, isSelected: Boolean) {
viewModelScope.launch { satelliteRepo.updateEntriesSelection(catNums, isSelected) }
}
private fun filterByModes(items: List<SatItem>, modes: List<String>): List<SatItem> {
@@ -135,10 +158,4 @@ class EntriesViewModel @Inject constructor(
}
}
}
override fun updateSelection(catNums: List<Int>, isSelected: Boolean) {
viewModelScope.launch {
satelliteRepo.updateEntriesSelection(catNums, isSelected)
}
}
}
+87 -106
View File
@@ -1,120 +1,101 @@
<?xml version="1.0" encoding="utf-8"?>
<androidx.coordinatorlayout.widget.CoordinatorLayout xmlns:android="http://schemas.android.com/apk/res/android"
<androidx.constraintlayout.widget.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
android:layout_width="match_parent"
android:layout_height="match_parent">
<androidx.constraintlayout.widget.ConstraintLayout
android:layout_width="match_parent"
android:layout_height="match_parent">
<com.google.android.material.card.MaterialCardView
android:id="@+id/entries_bar"
style="@style/TopBar"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent" />
<com.google.android.material.card.MaterialCardView
android:id="@+id/entries_bar"
style="@style/TopBar"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent" />
<com.google.android.material.textview.MaterialTextView
android:id="@+id/import_msg"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginStart="24dp"
android:elevation="4dp"
android:text="@string/entries_update"
android:textSize="18sp"
app:layout_constraintBottom_toBottomOf="@+id/import_web"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="@+id/import_web" />
<com.google.android.material.textview.MaterialTextView
android:id="@+id/import_msg"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginStart="24dp"
android:elevation="4dp"
android:text="@string/entries_update"
android:textSize="18sp"
app:layout_constraintBottom_toBottomOf="@+id/import_web"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="@+id/import_web" />
<ImageButton
android:id="@+id/import_web"
android:layout_width="64dp"
android:layout_height="48dp"
android:backgroundTint="@color/greyDark"
android:contentDescription="@string/placeholder"
android:elevation="4dp"
android:src="@drawable/ic_update_web"
app:layout_constraintBottom_toBottomOf="@+id/import_file"
app:layout_constraintEnd_toStartOf="@+id/import_file"
app:layout_constraintTop_toTopOf="@+id/import_file" />
<ImageButton
android:id="@+id/import_web"
android:layout_width="64dp"
android:layout_height="48dp"
android:backgroundTint="@color/greyDark"
android:contentDescription="@string/placeholder"
android:elevation="4dp"
android:src="@drawable/ic_update_web"
app:layout_constraintBottom_toBottomOf="@+id/import_file"
app:layout_constraintEnd_toStartOf="@+id/import_file"
app:layout_constraintTop_toTopOf="@+id/import_file" />
<ImageButton
android:id="@+id/import_file"
android:layout_width="64dp"
android:layout_height="48dp"
android:layout_marginEnd="4dp"
android:backgroundTint="@color/greyDark"
android:contentDescription="@string/placeholder"
android:elevation="4dp"
android:src="@drawable/ic_update_file"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintTop_toTopOf="parent" />
<ImageButton
android:id="@+id/import_file"
android:layout_width="64dp"
android:layout_height="48dp"
android:layout_marginEnd="4dp"
android:backgroundTint="@color/greyDark"
android:contentDescription="@string/placeholder"
android:elevation="4dp"
android:src="@drawable/ic_update_file"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintTop_toTopOf="parent" />
<SearchView
android:id="@+id/search_bar"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_marginStart="8dp"
android:iconifiedByDefault="false"
android:queryHint="@string/entries_search_hint"
app:layout_constraintEnd_toStartOf="@+id/select_mode"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toBottomOf="@+id/entries_bar" />
<SearchView
android:id="@+id/search_bar"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_marginStart="8dp"
android:iconifiedByDefault="false"
android:queryHint="@string/entries_search_hint"
app:layout_constraintEnd_toStartOf="@+id/select_mode"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toBottomOf="@+id/entries_bar" />
<ImageButton
android:id="@+id/select_mode"
android:layout_width="64dp"
android:layout_height="48dp"
android:backgroundTint="@color/greySurface"
android:contentDescription="@string/placeholder"
android:src="@drawable/ic_satellite"
app:layout_constraintBottom_toBottomOf="@+id/select_all"
app:layout_constraintEnd_toStartOf="@+id/select_all"
app:layout_constraintTop_toTopOf="@+id/select_all" />
<ImageButton
android:id="@+id/select_mode"
android:layout_width="64dp"
android:layout_height="48dp"
android:backgroundTint="@color/greySurface"
android:contentDescription="@string/placeholder"
android:src="@drawable/ic_satellite"
app:layout_constraintBottom_toBottomOf="@+id/select_all"
app:layout_constraintEnd_toStartOf="@+id/select_all"
app:layout_constraintTop_toTopOf="@+id/select_all" />
<ImageButton
android:id="@+id/select_all"
android:layout_width="64dp"
android:layout_height="48dp"
android:backgroundTint="@color/greySurface"
android:contentDescription="@string/placeholder"
android:src="@drawable/ic_select_all"
app:layout_constraintEnd_toEndOf="@+id/import_file"
app:layout_constraintTop_toBottomOf="@+id/import_file" />
<ImageButton
android:id="@+id/select_all"
android:layout_width="64dp"
android:layout_height="48dp"
android:backgroundTint="@color/greySurface"
android:contentDescription="@string/placeholder"
android:src="@drawable/ic_select_all"
app:layout_constraintEnd_toEndOf="@+id/import_file"
app:layout_constraintTop_toBottomOf="@+id/import_file" />
<androidx.recyclerview.widget.RecyclerView
android:id="@+id/entries_recycler"
android:layout_width="0dp"
android:layout_height="0dp"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toBottomOf="@+id/search_bar" />
<androidx.recyclerview.widget.RecyclerView
android:id="@+id/entries_recycler"
android:layout_width="0dp"
android:layout_height="0dp"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toBottomOf="@+id/search_bar" />
<ProgressBar
android:id="@+id/entries_progress"
style="@android:style/Widget.DeviceDefault.Light.ProgressBar.Large"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toBottomOf="@+id/entries_bar" />
<com.google.android.material.textview.MaterialTextView
android:id="@+id/entries_empty_error"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="@string/entries_no_data"
android:textSize="20sp"
android:visibility="invisible"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toBottomOf="@+id/entries_bar" />
</androidx.constraintlayout.widget.ConstraintLayout>
</androidx.coordinatorlayout.widget.CoordinatorLayout>
<ProgressBar
android:id="@+id/entries_progress"
style="@android:style/Widget.DeviceDefault.Light.ProgressBar.Large"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toBottomOf="@+id/entries_bar" />
</androidx.constraintlayout.widget.ConstraintLayout>
+1 -2
View File
@@ -16,8 +16,7 @@
<string name="sources_https">Только протокол HTTPS</string>
<string name="entries_update">Обновить спутники:</string>
<string name="entries_search_hint">Поиск по имени / Id</string>
<string name="entries_no_data">Пожалуйста, обновите спутники</string>
<string name="entries_search_hint">Имя / Номер</string>
<string name="entries_update_error">Ошибка обновления спутников</string>
<string name="modes_title">Выберите модуляцию трансивера</string>
+1 -2
View File
@@ -19,8 +19,7 @@
<string name="sources_https">Protocol HTTPS only</string>
<string name="entries_update">TLE data update:</string>
<string name="entries_search_hint">Search by Name / Id</string>
<string name="entries_no_data">Please update TLE data</string>
<string name="entries_search_hint">Name / Id</string>
<string name="entries_update_error">Error importing satellite data</string>
<string name="modes_title">Choose transceiver mode</string>
+1 -1
View File
@@ -1,7 +1,7 @@
buildscript {
ext {
gradle_version = '4.1.3'
gradle_plugin_version = '1.4.31'
gradle_plugin_version = '1.4.32'
material_version = '1.3.0'
constraint_layout_version = '2.0.4'
lifecycle_version = '2.3.0'