From ed6fa43d2070e8e323f171cb2cbab722d061e55b Mon Sep 17 00:00:00 2001 From: Tobias Zwick Date: Thu, 4 Apr 2024 18:45:45 +0200 Subject: [PATCH] add viewmodel for answers counter --- .../streetcomplete/screens/main/MainModule.kt | 3 + .../main/controls/AnswersCounterFragment.kt | 121 ++-------------- .../main/controls/AnswersCounterViewModel.kt | 135 ++++++++++++++++++ 3 files changed, 147 insertions(+), 112 deletions(-) create mode 100644 app/src/main/java/de/westnordost/streetcomplete/screens/main/controls/AnswersCounterViewModel.kt diff --git a/app/src/main/java/de/westnordost/streetcomplete/screens/main/MainModule.kt b/app/src/main/java/de/westnordost/streetcomplete/screens/main/MainModule.kt index 496e42b3b3..b4295e06c5 100644 --- a/app/src/main/java/de/westnordost/streetcomplete/screens/main/MainModule.kt +++ b/app/src/main/java/de/westnordost/streetcomplete/screens/main/MainModule.kt @@ -1,6 +1,8 @@ package de.westnordost.streetcomplete.screens.main import de.westnordost.streetcomplete.data.location.RecentLocationStore +import de.westnordost.streetcomplete.screens.main.controls.AnswersCounterViewModel +import de.westnordost.streetcomplete.screens.main.controls.AnswersCounterViewModelImpl import de.westnordost.streetcomplete.screens.main.controls.MainMenuButtonViewModel import de.westnordost.streetcomplete.screens.main.controls.MainMenuButtonViewModelImpl import de.westnordost.streetcomplete.screens.main.controls.MessagesButtonViewModel @@ -24,4 +26,5 @@ val mainModule = module { viewModel { OverlaysButtonViewModelImpl(get(), get(), get()) } viewModel { MessagesButtonViewModelImpl(get()) } viewModel { MainMenuButtonViewModelImpl(get(), get(), get(), get()) } + viewModel { AnswersCounterViewModelImpl(get(), get(), get(), get(), get()) } } diff --git a/app/src/main/java/de/westnordost/streetcomplete/screens/main/controls/AnswersCounterFragment.kt b/app/src/main/java/de/westnordost/streetcomplete/screens/main/controls/AnswersCounterFragment.kt index a3dcc3de59..93238da243 100644 --- a/app/src/main/java/de/westnordost/streetcomplete/screens/main/controls/AnswersCounterFragment.kt +++ b/app/src/main/java/de/westnordost/streetcomplete/screens/main/controls/AnswersCounterFragment.kt @@ -3,131 +3,28 @@ package de.westnordost.streetcomplete.screens.main.controls import android.os.Bundle import android.view.View import androidx.fragment.app.Fragment -import com.russhwolf.settings.ObservableSettings -import de.westnordost.streetcomplete.ApplicationConstants -import de.westnordost.streetcomplete.Prefs import de.westnordost.streetcomplete.R -import de.westnordost.streetcomplete.data.UnsyncedChangesCountSource -import de.westnordost.streetcomplete.data.download.DownloadProgressSource -import de.westnordost.streetcomplete.data.upload.UploadProgressSource -import de.westnordost.streetcomplete.data.user.statistics.StatisticsSource -import de.westnordost.streetcomplete.util.ktx.viewLifecycleScope -import kotlinx.coroutines.Dispatchers -import kotlinx.coroutines.launch -import kotlinx.coroutines.withContext -import org.koin.android.ext.android.inject +import de.westnordost.streetcomplete.util.ktx.observe +import org.koin.androidx.viewmodel.ext.android.viewModel /** Fragment that shows the "star" with the number of solved quests */ class AnswersCounterFragment : Fragment(R.layout.fragment_answers_counter) { - private val uploadProgressSource: UploadProgressSource by inject() - private val downloadProgressSource: DownloadProgressSource by inject() - - private val prefs: ObservableSettings by inject() - private val statisticsSource: StatisticsSource by inject() - private val unsyncedChangesCountSource: UnsyncedChangesCountSource by inject() - + private val viewModel by viewModel() private val answersCounterView get() = view as AnswersCounterView - private var showCurrentWeek: Boolean = false - - private val uploadProgressListener = object : UploadProgressSource.Listener { - override fun onStarted() { viewLifecycleScope.launch { updateProgress() } } - override fun onFinished() { viewLifecycleScope.launch { updateProgress() } } - } - - private val downloadProgressListener = object : DownloadProgressSource.Listener { - override fun onStarted() { viewLifecycleScope.launch { updateProgress() } } - override fun onFinished() { viewLifecycleScope.launch { updateProgress() } } - } - - private val unsyncedChangesCountListener = object : UnsyncedChangesCountSource.Listener { - override fun onIncreased() { viewLifecycleScope.launch { updateCount(true) } } - override fun onDecreased() { viewLifecycleScope.launch { updateCount(true) } } - } - - private val statisticsListener = object : StatisticsSource.Listener { - override fun onAddedOne(type: String) { - viewLifecycleScope.launch { addCount(+1, true) } - } - override fun onSubtractedOne(type: String) { - viewLifecycleScope.launch { addCount(-1, true) } - } - override fun onUpdatedAll() { - viewLifecycleScope.launch { updateCount(false) } - } - override fun onCleared() { - viewLifecycleScope.launch { updateCount(false) } - } - - override fun onUpdatedDaysActive() {} - } /* --------------------------------------- Lifecycle ---------------------------------------- */ override fun onViewCreated(view: View, savedInstanceState: Bundle?) { super.onViewCreated(view, savedInstanceState) - showCurrentWeek = savedInstanceState?.getBoolean(SHOW_CURRENT_WEEK, false) ?: false - answersCounterView.showLabel = showCurrentWeek + answersCounterView.setOnClickListener { viewModel.toggleShowingCurrentWeek() } - answersCounterView.setOnClickListener { - showCurrentWeek = !showCurrentWeek - viewLifecycleScope.launch { - updateCount(false) - answersCounterView.showLabel = showCurrentWeek - } + observe(viewModel.isUploadingOrDownloading) { answersCounterView.showProgress = it } + observe(viewModel.isShowingCurrentWeek) { answersCounterView.showLabel = it } + observe(viewModel.answersCount) { count -> + // only animate if count is positive, for positive feedback + answersCounterView.setUploadedCount(count, count > 0) } } - - override fun onStart() { - super.onStart() - - updateProgress() - uploadProgressSource.addListener(uploadProgressListener) - downloadProgressSource.addListener(downloadProgressListener) - // If autosync is on, the answers counter shows the uploaded + uploadable amount of quests. - if (isAutosync) unsyncedChangesCountSource.addListener(unsyncedChangesCountListener) - statisticsSource.addListener(statisticsListener) - - viewLifecycleScope.launch { updateCount(false) } - } - - override fun onStop() { - super.onStop() - uploadProgressSource.removeListener(uploadProgressListener) - downloadProgressSource.removeListener(downloadProgressListener) - statisticsSource.removeListener(statisticsListener) - unsyncedChangesCountSource.removeListener(unsyncedChangesCountListener) - } - - override fun onSaveInstanceState(outState: Bundle) { - super.onSaveInstanceState(outState) - outState.putBoolean(SHOW_CURRENT_WEEK, showCurrentWeek) - } - - private val isAutosync: Boolean get() = - Prefs.Autosync.valueOf(prefs.getStringOrNull(Prefs.AUTOSYNC) ?: ApplicationConstants.DEFAULT_AUTOSYNC) == Prefs.Autosync.ON - - private fun updateProgress() { - answersCounterView.showProgress = - uploadProgressSource.isUploadInProgress || downloadProgressSource.isDownloadInProgress - } - - private suspend fun updateCount(animated: Boolean) { - /* if autosync is on, show the uploaded count + the to-be-uploaded count (but only those - uploadables that will be part of the statistics, so no note stuff) */ - val editCount = withContext(Dispatchers.IO) { - if (showCurrentWeek) statisticsSource.getCurrentWeekEditCount() else statisticsSource.getEditCount() - } - val amount = editCount + if (isAutosync) unsyncedChangesCountSource.getSolvedCount() else 0 - answersCounterView.setUploadedCount(amount, animated) - } - - private fun addCount(diff: Int, animate: Boolean) { - answersCounterView.setUploadedCount(answersCounterView.uploadedCount + diff, animate) - } - - companion object { - private const val SHOW_CURRENT_WEEK = "showCurrentWeek" - } } diff --git a/app/src/main/java/de/westnordost/streetcomplete/screens/main/controls/AnswersCounterViewModel.kt b/app/src/main/java/de/westnordost/streetcomplete/screens/main/controls/AnswersCounterViewModel.kt new file mode 100644 index 0000000000..a1c19bf59b --- /dev/null +++ b/app/src/main/java/de/westnordost/streetcomplete/screens/main/controls/AnswersCounterViewModel.kt @@ -0,0 +1,135 @@ +package de.westnordost.streetcomplete.screens.main.controls + +import androidx.lifecycle.ViewModel +import androidx.lifecycle.viewModelScope +import com.russhwolf.settings.ObservableSettings +import de.westnordost.streetcomplete.ApplicationConstants +import de.westnordost.streetcomplete.Prefs +import de.westnordost.streetcomplete.data.UnsyncedChangesCountSource +import de.westnordost.streetcomplete.data.download.DownloadProgressSource +import de.westnordost.streetcomplete.data.upload.UploadProgressSource +import de.westnordost.streetcomplete.data.user.statistics.StatisticsSource +import de.westnordost.streetcomplete.util.ktx.launch +import kotlinx.coroutines.Dispatchers.IO +import kotlinx.coroutines.channels.awaitClose +import kotlinx.coroutines.flow.MutableStateFlow +import kotlinx.coroutines.flow.SharingStarted +import kotlinx.coroutines.flow.StateFlow +import kotlinx.coroutines.flow.callbackFlow +import kotlinx.coroutines.flow.combine +import kotlinx.coroutines.flow.stateIn +import kotlinx.coroutines.flow.update +import kotlinx.coroutines.plus + +abstract class AnswersCounterViewModel : ViewModel() { + abstract val isUploadingOrDownloading: StateFlow + + abstract val answersCount: StateFlow + + abstract val isShowingCurrentWeek: StateFlow + abstract fun toggleShowingCurrentWeek() +} + +class AnswersCounterViewModelImpl( + private val uploadProgressSource: UploadProgressSource, + private val downloadProgressSource: DownloadProgressSource, + private val prefs: ObservableSettings, + private val statisticsSource: StatisticsSource, + private val unsyncedChangesCountSource: UnsyncedChangesCountSource, +) : AnswersCounterViewModel() { + + override val isUploadingOrDownloading = MutableStateFlow( + uploadProgressSource.isUploadInProgress || downloadProgressSource.isDownloadInProgress + ) + + override val isShowingCurrentWeek = MutableStateFlow(false) + + private val editCount = MutableStateFlow(0) + private val editCountCurrentWeek = MutableStateFlow(0) + private val unsyncedEditsCount = MutableStateFlow(0) + private val isAutoSync = callbackFlow { + send(isAutoSync(prefs.getStringOrNull(Prefs.AUTOSYNC))) + val listener = prefs.addStringOrNullListener(Prefs.AUTOSYNC) { isAutoSync(it) } + awaitClose { listener.deactivate() } + } + + override val answersCount: StateFlow = combine( + editCount, editCountCurrentWeek, unsyncedEditsCount, isAutoSync, isShowingCurrentWeek + ) { editCount, editCountCurrentWeek, unsyncedEditsCount, isAutoSync, isShowingCurrentWeek -> + // when autosync is off, the unsynced edits are instead shown on the download button + val unsyncedEdits = if (isAutoSync) unsyncedEditsCount else 0 + val syncedEdits = if (isShowingCurrentWeek) editCountCurrentWeek else editCount + syncedEdits + unsyncedEdits + }.stateIn(viewModelScope + IO, SharingStarted.Lazily, 0) + + private val unsyncedChangesCountListener = object : UnsyncedChangesCountSource.Listener { + override fun onIncreased() { unsyncedEditsCount.update { it + 1 } } + override fun onDecreased() { unsyncedEditsCount.update { it - 1 } } + } + + private val statisticsListener = object : StatisticsSource.Listener { + override fun onAddedOne(type: String) { changeEditCount(+1) } + override fun onSubtractedOne(type: String) { changeEditCount(-1) } + override fun onUpdatedAll() { updateEditCount() } + override fun onCleared() { updateEditCount() } + override fun onUpdatedDaysActive() {} + } + + private val uploadProgressListener = object : UploadProgressSource.Listener { + override fun onStarted() { updateUploadOrDownloadInProgress() } + override fun onFinished() { updateUploadOrDownloadInProgress() } + } + + private val downloadProgressListener = object : DownloadProgressSource.Listener { + override fun onStarted() { updateUploadOrDownloadInProgress() } + override fun onFinished() { updateUploadOrDownloadInProgress() } + } + + init { + updateEditCount() + updateUnsyncedChangesCount() + + uploadProgressSource.addListener(uploadProgressListener) + downloadProgressSource.addListener(downloadProgressListener) + statisticsSource.addListener(statisticsListener) + unsyncedChangesCountSource.addListener(unsyncedChangesCountListener) + } + + override fun onCleared() { + uploadProgressSource.removeListener(uploadProgressListener) + downloadProgressSource.removeListener(downloadProgressListener) + statisticsSource.removeListener(statisticsListener) + unsyncedChangesCountSource.removeListener(unsyncedChangesCountListener) + } + + override fun toggleShowingCurrentWeek() { + isShowingCurrentWeek.update { !it } + } + + private fun updateUploadOrDownloadInProgress() { + isUploadingOrDownloading.value = + uploadProgressSource.isUploadInProgress || downloadProgressSource.isDownloadInProgress + } + + private fun updateEditCount() { + launch(IO) { + editCount.value = statisticsSource.getEditCount() + editCountCurrentWeek.value = statisticsSource.getCurrentWeekEditCount() + } + } + + private fun updateUnsyncedChangesCount() { + launch(IO) { + unsyncedEditsCount.value = unsyncedChangesCountSource.getSolvedCount() + } + } + + private fun changeEditCount(by: Int) { + editCount.update { it + by } + editCountCurrentWeek.update { it + by } + } + + private fun isAutoSync(pref: String?): Boolean = + Prefs.Autosync.valueOf(pref ?: ApplicationConstants.DEFAULT_AUTOSYNC) == Prefs.Autosync.ON + +}