Skip to content

Commit

Permalink
add viewmodel for answers counter
Browse files Browse the repository at this point in the history
  • Loading branch information
westnordost committed Apr 4, 2024
1 parent 86d2b31 commit ed6fa43
Show file tree
Hide file tree
Showing 3 changed files with 147 additions and 112 deletions.
Original file line number Diff line number Diff line change
@@ -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
Expand All @@ -24,4 +26,5 @@ val mainModule = module {
viewModel<OverlaysButtonViewModel> { OverlaysButtonViewModelImpl(get(), get(), get()) }
viewModel<MessagesButtonViewModel> { MessagesButtonViewModelImpl(get()) }
viewModel<MainMenuButtonViewModel> { MainMenuButtonViewModelImpl(get(), get(), get(), get()) }
viewModel<AnswersCounterViewModel> { AnswersCounterViewModelImpl(get(), get(), get(), get(), get()) }
}
Original file line number Diff line number Diff line change
Expand Up @@ -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<AnswersCounterViewModel>()
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"
}
}
Original file line number Diff line number Diff line change
@@ -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<Boolean>

abstract val answersCount: StateFlow<Int>

abstract val isShowingCurrentWeek: StateFlow<Boolean>
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<Int>(0)
private val editCountCurrentWeek = MutableStateFlow<Int>(0)
private val unsyncedEditsCount = MutableStateFlow<Int>(0)
private val isAutoSync = callbackFlow<Boolean> {
send(isAutoSync(prefs.getStringOrNull(Prefs.AUTOSYNC)))
val listener = prefs.addStringOrNullListener(Prefs.AUTOSYNC) { isAutoSync(it) }
awaitClose { listener.deactivate() }
}

override val answersCount: StateFlow<Int> = 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

Check failure on line 134 in app/src/main/java/de/westnordost/streetcomplete/screens/main/controls/AnswersCounterViewModel.kt

View workflow job for this annotation

GitHub Actions / Kotlin

Unexpected blank line(s) before "}"
}

0 comments on commit ed6fa43

Please sign in to comment.