diff --git a/app/src/main/java/de/westnordost/streetcomplete/screens/about/AboutScreenModule.kt b/app/src/main/java/de/westnordost/streetcomplete/screens/about/AboutScreenModule.kt index d3a61a37cf..6fd2a0b9d5 100644 --- a/app/src/main/java/de/westnordost/streetcomplete/screens/about/AboutScreenModule.kt +++ b/app/src/main/java/de/westnordost/streetcomplete/screens/about/AboutScreenModule.kt @@ -6,4 +6,5 @@ import org.koin.dsl.module val aboutScreenModule = module { viewModel { LogsViewModelImpl(get()) } viewModel { CreditsViewModelImpl(get()) } + viewModel { ChangelogViewModelImpl(get()) } } diff --git a/app/src/main/java/de/westnordost/streetcomplete/screens/about/ChangelogFragment.kt b/app/src/main/java/de/westnordost/streetcomplete/screens/about/ChangelogFragment.kt index 06018eed06..511b808b8e 100644 --- a/app/src/main/java/de/westnordost/streetcomplete/screens/about/ChangelogFragment.kt +++ b/app/src/main/java/de/westnordost/streetcomplete/screens/about/ChangelogFragment.kt @@ -18,6 +18,7 @@ import de.westnordost.streetcomplete.screens.HasTitle import de.westnordost.streetcomplete.screens.TwoPaneDetailFragment import de.westnordost.streetcomplete.util.ktx.getRawTextFile import de.westnordost.streetcomplete.util.ktx.indicesOf +import de.westnordost.streetcomplete.util.ktx.observe import de.westnordost.streetcomplete.util.ktx.pxToDp import de.westnordost.streetcomplete.util.ktx.pxToSp import de.westnordost.streetcomplete.util.ktx.setHtmlBody @@ -28,21 +29,21 @@ import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.cancel import kotlinx.coroutines.launch import kotlinx.coroutines.withContext +import org.koin.androidx.viewmodel.ext.android.viewModel import kotlin.math.roundToInt /** Shows the full changelog */ class ChangelogFragment : TwoPaneDetailFragment(R.layout.fragment_changelog), HasTitle { private val binding by viewBinding(FragmentChangelogBinding::bind) + private val viewModel by viewModel() override val title: String get() = getString(R.string.about_title_changelog) override fun onViewCreated(view: View, savedInstanceState: Bundle?) { super.onViewCreated(view, savedInstanceState) - - viewLifecycleScope.launch { - val changelog = readChangelog(resources) - binding.webView.setHtmlBody(changelog) + observe(viewModel.changelog) { changelog -> + if (changelog != null) binding.webView.setHtmlBody(changelog) } } } diff --git a/app/src/main/java/de/westnordost/streetcomplete/screens/about/ChangelogViewModel.kt b/app/src/main/java/de/westnordost/streetcomplete/screens/about/ChangelogViewModel.kt new file mode 100644 index 0000000000..a18c8cc293 --- /dev/null +++ b/app/src/main/java/de/westnordost/streetcomplete/screens/about/ChangelogViewModel.kt @@ -0,0 +1,22 @@ +package de.westnordost.streetcomplete.screens.about + +import android.content.res.Resources +import androidx.lifecycle.ViewModel +import de.westnordost.streetcomplete.R +import de.westnordost.streetcomplete.util.ktx.getRawTextFile +import de.westnordost.streetcomplete.util.ktx.launch +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.flow.MutableStateFlow +import kotlinx.coroutines.flow.StateFlow + +abstract class ChangelogViewModel : ViewModel() { + abstract val changelog: StateFlow +} + +class ChangelogViewModelImpl(resources: Resources) : ChangelogViewModel() { + override val changelog = MutableStateFlow(null) + + init { + launch(Dispatchers.IO) { changelog.value = resources.getRawTextFile(R.raw.changelog) } + } +} diff --git a/app/src/main/java/de/westnordost/streetcomplete/screens/about/CreditsViewModel.kt b/app/src/main/java/de/westnordost/streetcomplete/screens/about/CreditsViewModel.kt index df564f0e3f..eb3296c5fc 100644 --- a/app/src/main/java/de/westnordost/streetcomplete/screens/about/CreditsViewModel.kt +++ b/app/src/main/java/de/westnordost/streetcomplete/screens/about/CreditsViewModel.kt @@ -1,6 +1,12 @@ package de.westnordost.streetcomplete.screens.about +import android.content.res.Resources import androidx.lifecycle.ViewModel +import de.westnordost.streetcomplete.R +import de.westnordost.streetcomplete.util.ktx.getYamlObject +import de.westnordost.streetcomplete.util.ktx.launch +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.flow.StateFlow import kotlinx.serialization.Serializable @@ -27,3 +33,60 @@ data class Contributor( ) { val githubLink: String get() = "https://github.com/$githubUsername" } + +class CreditsViewModelImpl(private val resources: Resources) : CreditsViewModel() { + override val credits = MutableStateFlow(null) + + init { + launch(Dispatchers.IO) { + val mainContributors = readMainContributors() + credits.value = Credits( + mainContributors = mainContributors, + codeContributors = readCodeContributors(mainContributors.map { it.githubUsername }), + projectsContributors = readProjectsContributors(), + artContributors = readArtContributors(), + translators = readTranslators() + ) + } + } + + private fun readMainContributors(): List = + resources.getYamlObject>(R.raw.credits_main) + + private fun readArtContributors(): List = + resources.getYamlObject>(R.raw.credits_art) + + private fun readProjectsContributors(): List = + resources.getYamlObject>(R.raw.credits_projects) + + private fun readCodeContributors(skipUsers: List): List { + return resources + .getYamlObject>(R.raw.credits_contributors) + .filter { it.githubUsername !in skipUsers && it.score >= 50 } + .sortedByDescending { it.score } + } + + private fun readTranslators(): Map> { + val translatorsByLanguage = + resources.getYamlObject>>(R.raw.credits_translators) + + // skip plain English. That's not a translation + translatorsByLanguage.remove("en") + + // skip those translators who contributed less than 2% of the translation + for (contributors in translatorsByLanguage.values) { + val totalTranslated = contributors.values.sum() + val removedAnyone = contributors.values.removeAll { 100 * it / totalTranslated < 2 } + if (removedAnyone) { + contributors["…"] = 1 + } + } + + return translatorsByLanguage.mapValues { (_, translators) -> + translators.entries.sortedByDescending { it.value }.map { it.key } + } + } +} + +private val Contributor.score: Int get() = + linesOfCodeChanged + linesOfInterfaceMarkupChanged / 5 + assetFilesChanged * 15 diff --git a/app/src/main/java/de/westnordost/streetcomplete/screens/about/CreditsViewModelImpl.kt b/app/src/main/java/de/westnordost/streetcomplete/screens/about/CreditsViewModelImpl.kt deleted file mode 100644 index d3dd44b4d3..0000000000 --- a/app/src/main/java/de/westnordost/streetcomplete/screens/about/CreditsViewModelImpl.kt +++ /dev/null @@ -1,65 +0,0 @@ -package de.westnordost.streetcomplete.screens.about - -import android.content.res.Resources -import de.westnordost.streetcomplete.R -import de.westnordost.streetcomplete.util.ktx.getYamlObject -import de.westnordost.streetcomplete.util.ktx.launch -import kotlinx.coroutines.Dispatchers.IO -import kotlinx.coroutines.flow.MutableStateFlow - -class CreditsViewModelImpl(private val resources: Resources) : CreditsViewModel() { - override val credits = MutableStateFlow(null) - - init { - launch(IO) { - val mainContributors = readMainContributors() - credits.value = Credits( - mainContributors = mainContributors, - codeContributors = readCodeContributors(mainContributors.map { it.githubUsername }), - projectsContributors = readProjectsContributors(), - artContributors = readArtContributors(), - translators = readTranslators() - ) - } - } - - private fun readMainContributors(): List = - resources.getYamlObject>(R.raw.credits_main) - - private fun readArtContributors(): List = - resources.getYamlObject>(R.raw.credits_art) - - private fun readProjectsContributors(): List = - resources.getYamlObject>(R.raw.credits_projects) - - private fun readCodeContributors(skipUsers: List): List { - return resources - .getYamlObject>(R.raw.credits_contributors) - .filter { it.githubUsername !in skipUsers && it.score >= 50 } - .sortedByDescending { it.score } - } - - private fun readTranslators(): Map> { - val translatorsByLanguage = - resources.getYamlObject>>(R.raw.credits_translators) - - // skip plain English. That's not a translation - translatorsByLanguage.remove("en") - - // skip those translators who contributed less than 2% of the translation - for (contributors in translatorsByLanguage.values) { - val totalTranslated = contributors.values.sum() - val removedAnyone = contributors.values.removeAll { 100 * it / totalTranslated < 2 } - if (removedAnyone) { - contributors["…"] = 1 - } - } - - return translatorsByLanguage.mapValues { (_, translators) -> - translators.entries.sortedByDescending { it.value }.map { it.key } - } - } -} - -private val Contributor.score: Int get() = - linesOfCodeChanged + linesOfInterfaceMarkupChanged / 5 + assetFilesChanged * 15 diff --git a/app/src/main/java/de/westnordost/streetcomplete/screens/about/LogsViewModel.kt b/app/src/main/java/de/westnordost/streetcomplete/screens/about/LogsViewModel.kt index 1f22b61951..a259b1f08e 100644 --- a/app/src/main/java/de/westnordost/streetcomplete/screens/about/LogsViewModel.kt +++ b/app/src/main/java/de/westnordost/streetcomplete/screens/about/LogsViewModel.kt @@ -1,9 +1,26 @@ package de.westnordost.streetcomplete.screens.about import androidx.lifecycle.ViewModel +import androidx.lifecycle.viewModelScope import de.westnordost.streetcomplete.data.logs.LogMessage +import de.westnordost.streetcomplete.data.logs.LogsController import de.westnordost.streetcomplete.data.logs.LogsFilters +import de.westnordost.streetcomplete.util.ktx.systemTimeNow +import de.westnordost.streetcomplete.util.ktx.toEpochMilli +import de.westnordost.streetcomplete.util.ktx.toLocalDate +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.ExperimentalCoroutinesApi +import kotlinx.coroutines.channels.awaitClose +import kotlinx.coroutines.flow.MutableStateFlow +import kotlinx.coroutines.flow.SharedFlow +import kotlinx.coroutines.flow.SharingStarted import kotlinx.coroutines.flow.StateFlow +import kotlinx.coroutines.flow.callbackFlow +import kotlinx.coroutines.flow.shareIn +import kotlinx.coroutines.flow.transformLatest +import kotlinx.coroutines.plus +import kotlinx.datetime.LocalDateTime +import kotlinx.datetime.LocalTime abstract class LogsViewModel : ViewModel() { abstract val filters: StateFlow @@ -11,3 +28,59 @@ abstract class LogsViewModel : ViewModel() { abstract fun setFilters(filters: LogsFilters) } + +class LogsViewModelImpl( + private val logsController: LogsController, +) : LogsViewModel() { + + override val filters = MutableStateFlow(LogsFilters( + timestampNewerThan = LocalDateTime(systemTimeNow().toLocalDate(), LocalTime(0, 0, 0)) + )) + + /** + * Produce a call back flow of all incoming logs matching the given [filters]. + */ + private fun getIncomingLogs(filters: LogsFilters) = callbackFlow { + // Listener that sends the messages matching the filters to the observer + val listener = object : LogsController.Listener { + override fun onAdded(message: LogMessage) { + if (filters.matches(message)) { + trySend(message) // Send it to the observer + } + } + } + logsController.addListener(listener) + awaitClose { logsController.removeListener(listener) } + } + + @OptIn(ExperimentalCoroutinesApi::class) + private val _logs: SharedFlow> = + filters.transformLatest { filters -> + val logs = logsController.getLogs(filters).toMutableList() + + emit(logs) + + getIncomingLogs(filters).collect { + logs.add(it) + emit(logs) + } + }.shareIn(viewModelScope + Dispatchers.IO, SharingStarted.Eagerly, 1) + + override val logs: StateFlow> = object : + StateFlow>, + SharedFlow> by _logs { + override val value: List get() = replayCache[0] + } + + override fun setFilters(filters: LogsFilters) { + this.filters.value = filters + } +} + +private fun LogsController.getLogs(filters: LogsFilters) = + getLogs( + levels = filters.levels, + messageContains = filters.messageContains, + newerThan = filters.timestampNewerThan?.toEpochMilli(), + olderThan = filters.timestampOlderThan?.toEpochMilli() + ) diff --git a/app/src/main/java/de/westnordost/streetcomplete/screens/about/LogsViewModelImpl.kt b/app/src/main/java/de/westnordost/streetcomplete/screens/about/LogsViewModelImpl.kt deleted file mode 100644 index b2fd4b4662..0000000000 --- a/app/src/main/java/de/westnordost/streetcomplete/screens/about/LogsViewModelImpl.kt +++ /dev/null @@ -1,78 +0,0 @@ -package de.westnordost.streetcomplete.screens.about - -import androidx.lifecycle.viewModelScope -import de.westnordost.streetcomplete.data.logs.LogMessage -import de.westnordost.streetcomplete.data.logs.LogsController -import de.westnordost.streetcomplete.data.logs.LogsFilters -import de.westnordost.streetcomplete.util.ktx.systemTimeNow -import de.westnordost.streetcomplete.util.ktx.toEpochMilli -import de.westnordost.streetcomplete.util.ktx.toLocalDate -import kotlinx.coroutines.Dispatchers -import kotlinx.coroutines.ExperimentalCoroutinesApi -import kotlinx.coroutines.channels.awaitClose -import kotlinx.coroutines.flow.MutableStateFlow -import kotlinx.coroutines.flow.SharedFlow -import kotlinx.coroutines.flow.SharingStarted -import kotlinx.coroutines.flow.StateFlow -import kotlinx.coroutines.flow.callbackFlow -import kotlinx.coroutines.flow.shareIn -import kotlinx.coroutines.flow.transformLatest -import kotlinx.coroutines.plus -import kotlinx.datetime.LocalDateTime -import kotlinx.datetime.LocalTime - -class LogsViewModelImpl( - private val logsController: LogsController, -) : LogsViewModel() { - - override val filters = MutableStateFlow(LogsFilters( - timestampNewerThan = LocalDateTime(systemTimeNow().toLocalDate(), LocalTime(0, 0, 0)) - )) - - /** - * Produce a call back flow of all incoming logs matching the given [filters]. - */ - private fun getIncomingLogs(filters: LogsFilters) = callbackFlow { - // Listener that sends the messages matching the filters to the observer - val listener = object : LogsController.Listener { - override fun onAdded(message: LogMessage) { - if (filters.matches(message)) { - trySend(message) // Send it to the observer - } - } - } - logsController.addListener(listener) - awaitClose { logsController.removeListener(listener) } - } - - @OptIn(ExperimentalCoroutinesApi::class) - private val _logs: SharedFlow> = - filters.transformLatest { filters -> - val logs = logsController.getLogs(filters).toMutableList() - - emit(logs) - - getIncomingLogs(filters).collect { - logs.add(it) - emit(logs) - } - }.shareIn(viewModelScope + Dispatchers.IO, SharingStarted.Eagerly, 1) - - override val logs: StateFlow> = object : - StateFlow>, - SharedFlow> by _logs { - override val value: List get() = replayCache[0] - } - - override fun setFilters(filters: LogsFilters) { - this.filters.value = filters - } -} - -private fun LogsController.getLogs(filters: LogsFilters) = - getLogs( - levels = filters.levels, - messageContains = filters.messageContains, - newerThan = filters.timestampNewerThan?.toEpochMilli(), - olderThan = filters.timestampOlderThan?.toEpochMilli() - ) diff --git a/app/src/main/java/de/westnordost/streetcomplete/screens/user/achievements/AchievementsViewModel.kt b/app/src/main/java/de/westnordost/streetcomplete/screens/user/achievements/AchievementsViewModel.kt index 3b62704b29..be8ede2697 100644 --- a/app/src/main/java/de/westnordost/streetcomplete/screens/user/achievements/AchievementsViewModel.kt +++ b/app/src/main/java/de/westnordost/streetcomplete/screens/user/achievements/AchievementsViewModel.kt @@ -2,9 +2,28 @@ package de.westnordost.streetcomplete.screens.user.achievements import androidx.lifecycle.ViewModel import de.westnordost.streetcomplete.data.user.achievements.Achievement +import de.westnordost.streetcomplete.data.user.achievements.AchievementsSource +import de.westnordost.streetcomplete.data.user.statistics.StatisticsSource +import de.westnordost.streetcomplete.util.ktx.launch +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.flow.StateFlow abstract class AchievementsViewModel : ViewModel() { abstract val isSynchronizingStatistics: StateFlow abstract val achievements: StateFlow>?> } + +class AchievementsViewModelImpl( + private val achievementsSource: AchievementsSource, + private val statisticsSource: StatisticsSource, +) : AchievementsViewModel() { + override val isSynchronizingStatistics = MutableStateFlow(statisticsSource.isSynchronizing) + override val achievements = MutableStateFlow>?>(null) + + init { + launch(Dispatchers.IO) { + achievements.value = achievementsSource.getAchievements() + } + } +} diff --git a/app/src/main/java/de/westnordost/streetcomplete/screens/user/achievements/AchievementsViewModelImpl.kt b/app/src/main/java/de/westnordost/streetcomplete/screens/user/achievements/AchievementsViewModelImpl.kt deleted file mode 100644 index 705f2e8813..0000000000 --- a/app/src/main/java/de/westnordost/streetcomplete/screens/user/achievements/AchievementsViewModelImpl.kt +++ /dev/null @@ -1,22 +0,0 @@ -package de.westnordost.streetcomplete.screens.user.achievements - -import de.westnordost.streetcomplete.data.user.achievements.Achievement -import de.westnordost.streetcomplete.data.user.achievements.AchievementsSource -import de.westnordost.streetcomplete.data.user.statistics.StatisticsSource -import de.westnordost.streetcomplete.util.ktx.launch -import kotlinx.coroutines.Dispatchers -import kotlinx.coroutines.flow.MutableStateFlow - -class AchievementsViewModelImpl( - private val achievementsSource: AchievementsSource, - private val statisticsSource: StatisticsSource, -) : AchievementsViewModel() { - override val isSynchronizingStatistics = MutableStateFlow(statisticsSource.isSynchronizing) - override val achievements = MutableStateFlow>?>(null) - - init { - launch(Dispatchers.IO) { - achievements.value = achievementsSource.getAchievements() - } - } -} diff --git a/app/src/main/java/de/westnordost/streetcomplete/screens/user/links/LinksViewModel.kt b/app/src/main/java/de/westnordost/streetcomplete/screens/user/links/LinksViewModel.kt index 25b97d1ca9..d41a52115f 100644 --- a/app/src/main/java/de/westnordost/streetcomplete/screens/user/links/LinksViewModel.kt +++ b/app/src/main/java/de/westnordost/streetcomplete/screens/user/links/LinksViewModel.kt @@ -1,10 +1,29 @@ package de.westnordost.streetcomplete.screens.user.links import androidx.lifecycle.ViewModel +import de.westnordost.streetcomplete.data.user.achievements.AchievementsSource import de.westnordost.streetcomplete.data.user.achievements.Link +import de.westnordost.streetcomplete.data.user.statistics.StatisticsSource +import de.westnordost.streetcomplete.util.ktx.launch +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.flow.StateFlow abstract class LinksViewModel : ViewModel() { abstract val isSynchronizingStatistics: StateFlow abstract val links: StateFlow?> } + +class LinksViewModelImpl( + private val achievementsSource: AchievementsSource, + private val statisticsSource: StatisticsSource, +) : LinksViewModel() { + override val isSynchronizingStatistics = MutableStateFlow(statisticsSource.isSynchronizing) + override val links = MutableStateFlow?>(null) + + init { + launch(Dispatchers.IO) { + links.value = achievementsSource.getLinks() + } + } +} diff --git a/app/src/main/java/de/westnordost/streetcomplete/screens/user/links/LinksViewModelImpl.kt b/app/src/main/java/de/westnordost/streetcomplete/screens/user/links/LinksViewModelImpl.kt deleted file mode 100644 index 65b83107a8..0000000000 --- a/app/src/main/java/de/westnordost/streetcomplete/screens/user/links/LinksViewModelImpl.kt +++ /dev/null @@ -1,22 +0,0 @@ -package de.westnordost.streetcomplete.screens.user.links - -import de.westnordost.streetcomplete.data.user.achievements.AchievementsSource -import de.westnordost.streetcomplete.data.user.achievements.Link -import de.westnordost.streetcomplete.data.user.statistics.StatisticsSource -import de.westnordost.streetcomplete.util.ktx.launch -import kotlinx.coroutines.Dispatchers.IO -import kotlinx.coroutines.flow.MutableStateFlow - -class LinksViewModelImpl( - private val achievementsSource: AchievementsSource, - private val statisticsSource: StatisticsSource, -) : LinksViewModel() { - override val isSynchronizingStatistics = MutableStateFlow(statisticsSource.isSynchronizing) - override val links = MutableStateFlow?>(null) - - init { - launch(IO) { - links.value = achievementsSource.getLinks() - } - } -} diff --git a/app/src/main/java/de/westnordost/streetcomplete/screens/user/login/LoginViewModel.kt b/app/src/main/java/de/westnordost/streetcomplete/screens/user/login/LoginViewModel.kt index b5c7139705..3c444ba212 100644 --- a/app/src/main/java/de/westnordost/streetcomplete/screens/user/login/LoginViewModel.kt +++ b/app/src/main/java/de/westnordost/streetcomplete/screens/user/login/LoginViewModel.kt @@ -1,7 +1,26 @@ package de.westnordost.streetcomplete.screens.user.login import androidx.lifecycle.ViewModel +import de.westnordost.streetcomplete.data.UnsyncedChangesCountSource +import de.westnordost.streetcomplete.data.user.OAUTH2_AUTHORIZATION_URL +import de.westnordost.streetcomplete.data.user.OAUTH2_CLIENT_ID +import de.westnordost.streetcomplete.data.user.OAUTH2_REDIRECT_URI +import de.westnordost.streetcomplete.data.user.OAUTH2_REQUESTED_SCOPES +import de.westnordost.streetcomplete.data.user.OAUTH2_REQUIRED_SCOPES +import de.westnordost.streetcomplete.data.user.OAUTH2_TOKEN_URL +import de.westnordost.streetcomplete.data.user.UserLoginStatusController +import de.westnordost.streetcomplete.data.user.UserUpdater +import de.westnordost.streetcomplete.data.user.oauth.OAuthAuthorizationParams +import de.westnordost.streetcomplete.data.user.oauth.OAuthException +import de.westnordost.streetcomplete.data.user.oauth.OAuthService +import de.westnordost.streetcomplete.data.user.oauth.extractAuthorizationCode +import de.westnordost.streetcomplete.util.ktx.launch +import de.westnordost.streetcomplete.util.logs.Log +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.flow.StateFlow +import kotlinx.coroutines.flow.update +import kotlinx.coroutines.withContext abstract class LoginViewModel : ViewModel() { abstract val unsyncedChangesCount: StateFlow @@ -35,3 +54,87 @@ enum class LoginError : LoginState { CommunicationError } data object LoggedIn : LoginState + +class LoginViewModelImpl( + private val unsyncedChangesCountSource: UnsyncedChangesCountSource, + private val userLoginStatusController: UserLoginStatusController, + private val oAuthService: OAuthService, + private val userUpdater: UserUpdater +) : LoginViewModel() { + override val loginState = MutableStateFlow(LoggedOut) + override val unsyncedChangesCount = MutableStateFlow(0) + + override val authorizationRequestUrl: String get() = oAuth.authorizationRequestUrl + + private val oAuth = OAuthAuthorizationParams( + OAUTH2_AUTHORIZATION_URL, + OAUTH2_TOKEN_URL, + OAUTH2_CLIENT_ID, + OAUTH2_REQUESTED_SCOPES, + OAUTH2_REDIRECT_URI + ) + + init { + launch(Dispatchers.IO) { + unsyncedChangesCount.update { unsyncedChangesCountSource.getCount() } + } + } + + override fun startLogin() { + loginState.compareAndSet(LoggedOut, RequestingAuthorization) + } + + override fun failAuthorization(url: String, errorCode: Int, description: String?) { + Log.e(TAG, "Error for URL " + url + if (description != null) ": $description" else "") + loginState.compareAndSet(RequestingAuthorization, LoginError.CommunicationError) + } + + override fun isAuthorizationResponseUrl(url: String): Boolean = + oAuth.itsForMe(url) + + override fun finishAuthorization(authorizationResponseUrl: String) { + launch { + val accessToken = retrieveAccessToken(authorizationResponseUrl) + if (accessToken != null) { + login(accessToken) + } + } + } + + private suspend fun retrieveAccessToken(authorizationResponseUrl: String): String? { + try { + loginState.value = RetrievingAccessToken + val authorizationCode = extractAuthorizationCode(authorizationResponseUrl) + val accessTokenResponse = withContext(Dispatchers.IO) { + oAuthService.retrieveAccessToken(oAuth, authorizationCode) + } + if (accessTokenResponse.grantedScopes?.containsAll(OAUTH2_REQUIRED_SCOPES) == false) { + loginState.value = LoginError.RequiredPermissionsNotGranted + return null + } + return accessTokenResponse.accessToken + } catch (e: Exception) { + if (e is OAuthException && e.error == "access_denied") { + loginState.value = LoginError.RequiredPermissionsNotGranted + } else { + Log.e(TAG, "Error during authorization", e) + loginState.value = LoginError.CommunicationError + } + return null + } + } + + private suspend fun login(accessToken: String) { + loginState.value = LoggedIn + userLoginStatusController.logIn(accessToken) + userUpdater.update() + } + + override fun resetLogin() { + if (loginState.value is LoginError) loginState.value = LoggedOut + } + + companion object { + private const val TAG = "Login" + } +} diff --git a/app/src/main/java/de/westnordost/streetcomplete/screens/user/login/LoginViewModelImpl.kt b/app/src/main/java/de/westnordost/streetcomplete/screens/user/login/LoginViewModelImpl.kt deleted file mode 100644 index 31c047991b..0000000000 --- a/app/src/main/java/de/westnordost/streetcomplete/screens/user/login/LoginViewModelImpl.kt +++ /dev/null @@ -1,105 +0,0 @@ -package de.westnordost.streetcomplete.screens.user.login - -import de.westnordost.streetcomplete.data.UnsyncedChangesCountSource -import de.westnordost.streetcomplete.data.user.OAUTH2_AUTHORIZATION_URL -import de.westnordost.streetcomplete.data.user.OAUTH2_CLIENT_ID -import de.westnordost.streetcomplete.data.user.OAUTH2_REDIRECT_URI -import de.westnordost.streetcomplete.data.user.OAUTH2_REQUESTED_SCOPES -import de.westnordost.streetcomplete.data.user.OAUTH2_REQUIRED_SCOPES -import de.westnordost.streetcomplete.data.user.OAUTH2_TOKEN_URL -import de.westnordost.streetcomplete.data.user.UserLoginStatusController -import de.westnordost.streetcomplete.data.user.UserUpdater -import de.westnordost.streetcomplete.data.user.oauth.OAuthAuthorizationParams -import de.westnordost.streetcomplete.data.user.oauth.OAuthException -import de.westnordost.streetcomplete.data.user.oauth.OAuthService -import de.westnordost.streetcomplete.data.user.oauth.extractAuthorizationCode -import de.westnordost.streetcomplete.util.ktx.launch -import de.westnordost.streetcomplete.util.logs.Log -import kotlinx.coroutines.Dispatchers.IO -import kotlinx.coroutines.flow.MutableStateFlow -import kotlinx.coroutines.flow.update -import kotlinx.coroutines.withContext - -class LoginViewModelImpl( - private val unsyncedChangesCountSource: UnsyncedChangesCountSource, - private val userLoginStatusController: UserLoginStatusController, - private val oAuthService: OAuthService, - private val userUpdater: UserUpdater -) : LoginViewModel() { - override val loginState = MutableStateFlow(LoggedOut) - override val unsyncedChangesCount = MutableStateFlow(0) - - override val authorizationRequestUrl: String get() = oAuth.authorizationRequestUrl - - private val oAuth = OAuthAuthorizationParams( - OAUTH2_AUTHORIZATION_URL, - OAUTH2_TOKEN_URL, - OAUTH2_CLIENT_ID, - OAUTH2_REQUESTED_SCOPES, - OAUTH2_REDIRECT_URI - ) - - init { - launch(IO) { - unsyncedChangesCount.update { unsyncedChangesCountSource.getCount() } - } - } - - override fun startLogin() { - loginState.compareAndSet(LoggedOut, RequestingAuthorization) - } - - override fun failAuthorization(url: String, errorCode: Int, description: String?) { - Log.e(TAG, "Error for URL " + url + if (description != null) ": $description" else "") - loginState.compareAndSet(RequestingAuthorization, LoginError.CommunicationError) - } - - override fun isAuthorizationResponseUrl(url: String): Boolean = - oAuth.itsForMe(url) - - override fun finishAuthorization(authorizationResponseUrl: String) { - launch { - val accessToken = retrieveAccessToken(authorizationResponseUrl) - if (accessToken != null) { - login(accessToken) - } - } - } - - private suspend fun retrieveAccessToken(authorizationResponseUrl: String): String? { - try { - loginState.value = RetrievingAccessToken - val authorizationCode = extractAuthorizationCode(authorizationResponseUrl) - val accessTokenResponse = withContext(IO) { - oAuthService.retrieveAccessToken(oAuth, authorizationCode) - } - if (accessTokenResponse.grantedScopes?.containsAll(OAUTH2_REQUIRED_SCOPES) == false) { - loginState.value = LoginError.RequiredPermissionsNotGranted - return null - } - return accessTokenResponse.accessToken - } catch (e: Exception) { - if (e is OAuthException && e.error == "access_denied") { - loginState.value = LoginError.RequiredPermissionsNotGranted - } else { - Log.e(TAG, "Error during authorization", e) - loginState.value = LoginError.CommunicationError - } - return null - } - } - - private suspend fun login(accessToken: String) { - loginState.value = LoggedIn - userLoginStatusController.logIn(accessToken) - userUpdater.update() - } - - override fun resetLogin() { - if (loginState.value is LoginError) loginState.value = LoggedOut - } - - companion object { - private const val TAG = "Login" - } -} diff --git a/app/src/main/java/de/westnordost/streetcomplete/screens/user/profile/ProfileViewModel.kt b/app/src/main/java/de/westnordost/streetcomplete/screens/user/profile/ProfileViewModel.kt index 2079c98df6..7e62faf60f 100644 --- a/app/src/main/java/de/westnordost/streetcomplete/screens/user/profile/ProfileViewModel.kt +++ b/app/src/main/java/de/westnordost/streetcomplete/screens/user/profile/ProfileViewModel.kt @@ -1,9 +1,24 @@ package de.westnordost.streetcomplete.screens.user.profile import androidx.lifecycle.ViewModel +import de.westnordost.streetcomplete.Prefs +import de.westnordost.streetcomplete.data.UnsyncedChangesCountSource +import de.westnordost.streetcomplete.data.user.UserDataSource +import de.westnordost.streetcomplete.data.user.UserLoginStatusController +import de.westnordost.streetcomplete.data.user.UserUpdater +import de.westnordost.streetcomplete.data.user.achievements.Achievement +import de.westnordost.streetcomplete.data.user.achievements.AchievementsSource import de.westnordost.streetcomplete.data.user.statistics.CountryStatistics +import de.westnordost.streetcomplete.data.user.statistics.StatisticsSource +import de.westnordost.streetcomplete.util.ktx.launch +import de.westnordost.streetcomplete.util.prefs.Preferences +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.flow.StateFlow +import kotlinx.coroutines.flow.update import kotlinx.datetime.LocalDate +import kotlinx.serialization.encodeToString +import kotlinx.serialization.json.Json import java.io.File abstract class ProfileViewModel : ViewModel() { @@ -35,3 +50,167 @@ abstract class ProfileViewModel : ViewModel() { } data class DatesActiveInRange(val datesActive: List, val range: Int) + +class ProfileViewModelImpl( + private val userDataSource: UserDataSource, + private val userLoginStatusController: UserLoginStatusController, + private val userUpdater: UserUpdater, + private val statisticsSource: StatisticsSource, + private val achievementsSource: AchievementsSource, + private val unsyncedChangesCountSource: UnsyncedChangesCountSource, + private val avatarsCacheDirectory: File, + private val prefs: Preferences +) : ProfileViewModel() { + + override val userName = MutableStateFlow(null) + override val userAvatarFile = MutableStateFlow(getUserAvatarFile()) + override val achievementLevels = MutableStateFlow(0) + override val unsyncedChangesCount = MutableStateFlow(0) + override val datesActive = MutableStateFlow(DatesActiveInRange(emptyList(), 0)) + override val daysActive = MutableStateFlow(0) + override val editCount = MutableStateFlow(0) + override val editCountCurrentWeek = MutableStateFlow(0) + override val rank = MutableStateFlow(-1) + override val rankCurrentWeek = MutableStateFlow(-1) + override val biggestSolvedCountCountryStatistics = MutableStateFlow(null) + override val biggestSolvedCountCurrentWeekCountryStatistics = MutableStateFlow(null) + + override var lastShownGlobalUserRank: Int? + set(value) { + if (value != null) { + prefs.putInt(Prefs.LAST_SHOWN_USER_GLOBAL_RANK, value) + } else { + prefs.remove(Prefs.LAST_SHOWN_USER_GLOBAL_RANK) + } + } + get() = prefs.getIntOrNull(Prefs.LAST_SHOWN_USER_GLOBAL_RANK) + + override var lastShownGlobalUserRankCurrentWeek: Int? + set(value) { + if (value != null) { + prefs.putInt(Prefs.LAST_SHOWN_USER_GLOBAL_RANK_CURRENT_WEEK, value) + } else { + prefs.remove(Prefs.LAST_SHOWN_USER_GLOBAL_RANK_CURRENT_WEEK) + } + } + get() = prefs.getIntOrNull(Prefs.LAST_SHOWN_USER_GLOBAL_RANK_CURRENT_WEEK) + + override var lastShownLocalUserRank: CountryStatistics? + set(value) { + prefs.putString(Prefs.LAST_SHOWN_USER_LOCAL_RANK, Json.encodeToString(value)) + } + get() = prefs.getStringOrNull(Prefs.LAST_SHOWN_USER_LOCAL_RANK)?.let { Json.decodeFromString(it) } + + override var lastShownLocalUserRankCurrentWeek: CountryStatistics? + set(value) { + prefs.putString(Prefs.LAST_SHOWN_USER_LOCAL_RANK_CURRENT_WEEK, Json.encodeToString(value)) + } + get() = prefs.getStringOrNull(Prefs.LAST_SHOWN_USER_LOCAL_RANK_CURRENT_WEEK)?.let { Json.decodeFromString(it) } + + override fun logOutUser() { + launch { userLoginStatusController.logOut() } + } + + private val unsyncedChangesCountListener = object : UnsyncedChangesCountSource.Listener { + override fun onIncreased() { unsyncedChangesCount.update { it + 1 } } + override fun onDecreased() { unsyncedChangesCount.update { it - 1 } } + } + private val statisticsListener = object : StatisticsSource.Listener { + override fun onAddedOne(type: String) { + editCount.update { it + 1 } + editCountCurrentWeek.update { it + 1 } + } + override fun onSubtractedOne(type: String) { + editCount.update { it - 1 } + editCountCurrentWeek.update { it - 1 } + } + override fun onUpdatedAll() { updateStatistics() } + override fun onCleared() { updateStatistics() } + override fun onUpdatedDaysActive() { updateDatesActive() } + } + private val achievementsListener = object : AchievementsSource.Listener { + override fun onAchievementUnlocked(achievement: Achievement, level: Int) { updateAchievementLevels() } + override fun onAllAchievementsUpdated() { updateAchievementLevels() } + } + private val userListener = object : UserDataSource.Listener { + override fun onUpdated() { + userName.value = userDataSource.userName + userAvatarFile.value = getUserAvatarFile() + } + } + private val userAvatarListener = object : UserUpdater.Listener { + override fun onUserAvatarUpdated() { + userAvatarFile.value = getUserAvatarFile() + } + } + + init { + userName.value = userDataSource.userName + updateAchievementLevels() + updateUnsyncedChangesCount() + updateStatistics() + + userDataSource.addListener(userListener) + userUpdater.addUserAvatarListener(userAvatarListener) + statisticsSource.addListener(statisticsListener) + unsyncedChangesCountSource.addListener(unsyncedChangesCountListener) + achievementsSource.addListener(achievementsListener) + } + + private fun updateStatistics() { + updateEditCounts() + updateRanks() + updateDatesActive() + } + + private fun updateRanks() { + launch(Dispatchers.IO) { + rank.value = statisticsSource.rank + rankCurrentWeek.value = statisticsSource.currentWeekRank + biggestSolvedCountCountryStatistics.value = + statisticsSource.getCountryStatisticsOfCountryWithBiggestSolvedCount() + biggestSolvedCountCurrentWeekCountryStatistics.value = + statisticsSource.getCurrentWeekCountryStatisticsOfCountryWithBiggestSolvedCount() + } + } + + private fun updateEditCounts() { + launch(Dispatchers.IO) { + editCount.update { statisticsSource.getEditCount() } + editCountCurrentWeek.update { statisticsSource.getCurrentWeekEditCount() } + } + } + + private fun updateAchievementLevels() { + launch(Dispatchers.IO) { + achievementLevels.value = achievementsSource.getAchievements().sumOf { it.second } + } + } + + private fun updateDatesActive() { + launch(Dispatchers.IO) { + daysActive.value = statisticsSource.daysActive + datesActive.value = DatesActiveInRange( + statisticsSource.getActiveDates(), + statisticsSource.activeDatesRange + ) + } + } + + private fun updateUnsyncedChangesCount() { + launch(Dispatchers.IO) { + unsyncedChangesCount.update { unsyncedChangesCountSource.getCount() } + } + } + + private fun getUserAvatarFile(): File = + File(avatarsCacheDirectory, userDataSource.userId.toString()) + + override fun onCleared() { + unsyncedChangesCountSource.removeListener(unsyncedChangesCountListener) + statisticsSource.removeListener(statisticsListener) + userDataSource.removeListener(userListener) + userUpdater.removeUserAvatarListener(userAvatarListener) + achievementsSource.removeListener(achievementsListener) + } +} diff --git a/app/src/main/java/de/westnordost/streetcomplete/screens/user/profile/ProfileViewModelImpl.kt b/app/src/main/java/de/westnordost/streetcomplete/screens/user/profile/ProfileViewModelImpl.kt deleted file mode 100644 index 3550d3dcdf..0000000000 --- a/app/src/main/java/de/westnordost/streetcomplete/screens/user/profile/ProfileViewModelImpl.kt +++ /dev/null @@ -1,183 +0,0 @@ -package de.westnordost.streetcomplete.screens.user.profile - -import de.westnordost.streetcomplete.Prefs -import de.westnordost.streetcomplete.data.UnsyncedChangesCountSource -import de.westnordost.streetcomplete.data.user.UserDataSource -import de.westnordost.streetcomplete.data.user.UserLoginStatusController -import de.westnordost.streetcomplete.data.user.UserUpdater -import de.westnordost.streetcomplete.data.user.achievements.Achievement -import de.westnordost.streetcomplete.data.user.achievements.AchievementsSource -import de.westnordost.streetcomplete.data.user.statistics.CountryStatistics -import de.westnordost.streetcomplete.data.user.statistics.StatisticsSource -import de.westnordost.streetcomplete.util.ktx.launch -import de.westnordost.streetcomplete.util.prefs.Preferences -import kotlinx.coroutines.Dispatchers.IO -import kotlinx.coroutines.flow.MutableStateFlow -import kotlinx.coroutines.flow.update -import kotlinx.serialization.encodeToString -import kotlinx.serialization.json.Json -import java.io.File - -class ProfileViewModelImpl( - private val userDataSource: UserDataSource, - private val userLoginStatusController: UserLoginStatusController, - private val userUpdater: UserUpdater, - private val statisticsSource: StatisticsSource, - private val achievementsSource: AchievementsSource, - private val unsyncedChangesCountSource: UnsyncedChangesCountSource, - private val avatarsCacheDirectory: File, - private val prefs: Preferences -) : ProfileViewModel() { - - override val userName = MutableStateFlow(null) - override val userAvatarFile = MutableStateFlow(getUserAvatarFile()) - override val achievementLevels = MutableStateFlow(0) - override val unsyncedChangesCount = MutableStateFlow(0) - override val datesActive = MutableStateFlow(DatesActiveInRange(emptyList(), 0)) - override val daysActive = MutableStateFlow(0) - override val editCount = MutableStateFlow(0) - override val editCountCurrentWeek = MutableStateFlow(0) - override val rank = MutableStateFlow(-1) - override val rankCurrentWeek = MutableStateFlow(-1) - override val biggestSolvedCountCountryStatistics = MutableStateFlow(null) - override val biggestSolvedCountCurrentWeekCountryStatistics = MutableStateFlow(null) - - override var lastShownGlobalUserRank: Int? - set(value) { - if (value != null) { - prefs.putInt(Prefs.LAST_SHOWN_USER_GLOBAL_RANK, value) - } else { - prefs.remove(Prefs.LAST_SHOWN_USER_GLOBAL_RANK) - } - } - get() = prefs.getIntOrNull(Prefs.LAST_SHOWN_USER_GLOBAL_RANK) - - override var lastShownGlobalUserRankCurrentWeek: Int? - set(value) { - if (value != null) { - prefs.putInt(Prefs.LAST_SHOWN_USER_GLOBAL_RANK_CURRENT_WEEK, value) - } else { - prefs.remove(Prefs.LAST_SHOWN_USER_GLOBAL_RANK_CURRENT_WEEK) - } - } - get() = prefs.getIntOrNull(Prefs.LAST_SHOWN_USER_GLOBAL_RANK_CURRENT_WEEK) - - override var lastShownLocalUserRank: CountryStatistics? - set(value) { - prefs.putString(Prefs.LAST_SHOWN_USER_LOCAL_RANK, Json.encodeToString(value)) - } - get() = prefs.getStringOrNull(Prefs.LAST_SHOWN_USER_LOCAL_RANK)?.let { Json.decodeFromString(it) } - - override var lastShownLocalUserRankCurrentWeek: CountryStatistics? - set(value) { - prefs.putString(Prefs.LAST_SHOWN_USER_LOCAL_RANK_CURRENT_WEEK, Json.encodeToString(value)) - } - get() = prefs.getStringOrNull(Prefs.LAST_SHOWN_USER_LOCAL_RANK_CURRENT_WEEK)?.let { Json.decodeFromString(it) } - - override fun logOutUser() { - launch { userLoginStatusController.logOut() } - } - - private val unsyncedChangesCountListener = object : UnsyncedChangesCountSource.Listener { - override fun onIncreased() { unsyncedChangesCount.update { it + 1 } } - override fun onDecreased() { unsyncedChangesCount.update { it - 1 } } - } - private val statisticsListener = object : StatisticsSource.Listener { - override fun onAddedOne(type: String) { - editCount.update { it + 1 } - editCountCurrentWeek.update { it + 1 } - } - override fun onSubtractedOne(type: String) { - editCount.update { it - 1 } - editCountCurrentWeek.update { it - 1 } - } - override fun onUpdatedAll() { updateStatistics() } - override fun onCleared() { updateStatistics() } - override fun onUpdatedDaysActive() { updateDatesActive() } - } - private val achievementsListener = object : AchievementsSource.Listener { - override fun onAchievementUnlocked(achievement: Achievement, level: Int) { updateAchievementLevels() } - override fun onAllAchievementsUpdated() { updateAchievementLevels() } - } - private val userListener = object : UserDataSource.Listener { - override fun onUpdated() { - userName.value = userDataSource.userName - userAvatarFile.value = getUserAvatarFile() - } - } - private val userAvatarListener = object : UserUpdater.Listener { - override fun onUserAvatarUpdated() { - userAvatarFile.value = getUserAvatarFile() - } - } - - init { - userName.value = userDataSource.userName - updateAchievementLevels() - updateUnsyncedChangesCount() - updateStatistics() - - userDataSource.addListener(userListener) - userUpdater.addUserAvatarListener(userAvatarListener) - statisticsSource.addListener(statisticsListener) - unsyncedChangesCountSource.addListener(unsyncedChangesCountListener) - achievementsSource.addListener(achievementsListener) - } - - private fun updateStatistics() { - updateEditCounts() - updateRanks() - updateDatesActive() - } - - private fun updateRanks() { - launch(IO) { - rank.value = statisticsSource.rank - rankCurrentWeek.value = statisticsSource.currentWeekRank - biggestSolvedCountCountryStatistics.value = - statisticsSource.getCountryStatisticsOfCountryWithBiggestSolvedCount() - biggestSolvedCountCurrentWeekCountryStatistics.value = - statisticsSource.getCurrentWeekCountryStatisticsOfCountryWithBiggestSolvedCount() - } - } - - private fun updateEditCounts() { - launch(IO) { - editCount.update { statisticsSource.getEditCount() } - editCountCurrentWeek.update { statisticsSource.getCurrentWeekEditCount() } - } - } - - private fun updateAchievementLevels() { - launch(IO) { - achievementLevels.value = achievementsSource.getAchievements().sumOf { it.second } - } - } - - private fun updateDatesActive() { - launch(IO) { - daysActive.value = statisticsSource.daysActive - datesActive.value = DatesActiveInRange( - statisticsSource.getActiveDates(), - statisticsSource.activeDatesRange - ) - } - } - - private fun updateUnsyncedChangesCount() { - launch(IO) { - unsyncedChangesCount.update { unsyncedChangesCountSource.getCount() } - } - } - - private fun getUserAvatarFile(): File = - File(avatarsCacheDirectory, userDataSource.userId.toString()) - - override fun onCleared() { - unsyncedChangesCountSource.removeListener(unsyncedChangesCountListener) - statisticsSource.removeListener(statisticsListener) - userDataSource.removeListener(userListener) - userUpdater.removeUserAvatarListener(userAvatarListener) - achievementsSource.removeListener(achievementsListener) - } -} diff --git a/app/src/main/java/de/westnordost/streetcomplete/screens/user/statistics/EditStatisticsViewModel.kt b/app/src/main/java/de/westnordost/streetcomplete/screens/user/statistics/EditStatisticsViewModel.kt index 73a0b6e41b..f98a09107e 100644 --- a/app/src/main/java/de/westnordost/streetcomplete/screens/user/statistics/EditStatisticsViewModel.kt +++ b/app/src/main/java/de/westnordost/streetcomplete/screens/user/statistics/EditStatisticsViewModel.kt @@ -1,8 +1,13 @@ package de.westnordost.streetcomplete.screens.user.statistics import androidx.lifecycle.ViewModel +import de.westnordost.streetcomplete.data.AllEditTypes import de.westnordost.streetcomplete.data.osm.edits.EditType import de.westnordost.streetcomplete.data.user.statistics.CountryStatistics +import de.westnordost.streetcomplete.data.user.statistics.StatisticsSource +import de.westnordost.streetcomplete.util.ktx.launch +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.flow.StateFlow abstract class EditStatisticsViewModel : ViewModel() { @@ -16,3 +21,39 @@ abstract class EditStatisticsViewModel : ViewModel() { } data class EditTypeObjStatistics(val type: EditType, val count: Int) + +class EditStatisticsViewModelImpl( + private val statisticsSource: StatisticsSource, + private val allEditTypes: AllEditTypes, +) : EditStatisticsViewModel() { + + override val hasEdits = MutableStateFlow(true) + override val isSynchronizingStatistics = MutableStateFlow(statisticsSource.isSynchronizing) + override val countryStatistics = MutableStateFlow?>(null) + override val editTypeStatistics = MutableStateFlow?>(null) + + // no updating of data implemented (because actually not needed. Not possible to add edits + // while in this screen) + + init { + launch(Dispatchers.IO) { hasEdits.value = statisticsSource.getEditCount() > 0 } + } + + override fun queryCountryStatistics() { + if (countryStatistics.value == null) { + launch(Dispatchers.IO) { countryStatistics.value = statisticsSource.getCountryStatistics() } + } + } + + override fun queryEditTypeStatistics() { + if (editTypeStatistics.value == null) { + launch(Dispatchers.IO) { editTypeStatistics.value = getEditTypeStatistics() } + } + } + + private fun getEditTypeStatistics(): Collection = + statisticsSource.getEditTypeStatistics().mapNotNull { + val editType = allEditTypes.getByName(it.type) ?: return@mapNotNull null + EditTypeObjStatistics(editType, it.count) + } +} diff --git a/app/src/main/java/de/westnordost/streetcomplete/screens/user/statistics/EditStatisticsViewModelImpl.kt b/app/src/main/java/de/westnordost/streetcomplete/screens/user/statistics/EditStatisticsViewModelImpl.kt deleted file mode 100644 index 7a204073be..0000000000 --- a/app/src/main/java/de/westnordost/streetcomplete/screens/user/statistics/EditStatisticsViewModelImpl.kt +++ /dev/null @@ -1,44 +0,0 @@ -package de.westnordost.streetcomplete.screens.user.statistics - -import de.westnordost.streetcomplete.data.AllEditTypes -import de.westnordost.streetcomplete.data.user.statistics.CountryStatistics -import de.westnordost.streetcomplete.data.user.statistics.StatisticsSource -import de.westnordost.streetcomplete.util.ktx.launch -import kotlinx.coroutines.Dispatchers.IO -import kotlinx.coroutines.flow.MutableStateFlow - -class EditStatisticsViewModelImpl( - private val statisticsSource: StatisticsSource, - private val allEditTypes: AllEditTypes, -) : EditStatisticsViewModel() { - - override val hasEdits = MutableStateFlow(true) - override val isSynchronizingStatistics = MutableStateFlow(statisticsSource.isSynchronizing) - override val countryStatistics = MutableStateFlow?>(null) - override val editTypeStatistics = MutableStateFlow?>(null) - - // no updating of data implemented (because actually not needed. Not possible to add edits - // while in this screen) - - init { - launch(IO) { hasEdits.value = statisticsSource.getEditCount() > 0 } - } - - override fun queryCountryStatistics() { - if (countryStatistics.value == null) { - launch(IO) { countryStatistics.value = statisticsSource.getCountryStatistics() } - } - } - - override fun queryEditTypeStatistics() { - if (editTypeStatistics.value == null) { - launch(IO) { editTypeStatistics.value = getEditTypeStatistics() } - } - } - - private fun getEditTypeStatistics(): Collection = - statisticsSource.getEditTypeStatistics().mapNotNull { - val editType = allEditTypes.getByName(it.type) ?: return@mapNotNull null - EditTypeObjStatistics(editType, it.count) - } -}