Loading app/src/main/java/foundation/e/apps/feature/auth/login/LoginViewModel.kt +28 −29 Original line number Diff line number Diff line Loading @@ -73,39 +73,31 @@ class LoginViewModel @Inject constructor( email: String, oauthToken: String, ) { viewModelScope.launch { submitLogin(R.string.sign_in_microg_login_failed) { launchLoginSubmission(R.string.sign_in_microg_login_failed) { loginWorkflowCoordinator.submitMicrogLogin(email, oauthToken) } } } private fun initiateAnonymousLogin() { viewModelScope.launch { submitLogin(R.string.anonymous_login_failed_desc) { launchLoginSubmission(R.string.anonymous_login_failed_desc) { loginWorkflowCoordinator.submitAnonymousLogin() } } } private fun initiateGoogleLogin( email: String, oauthToken: String, ) { viewModelScope.launch { submitLogin(R.string.sign_in_failed_desc) { launchLoginSubmission(R.string.sign_in_failed_desc) { loginWorkflowCoordinator.submitGoogleLogin(email, oauthToken) } } } private fun initiateNoGoogleLogin() { viewModelScope.launch { submitLogin(R.string.something_went_wrong) { launchLoginSubmission(R.string.something_went_wrong) { loginWorkflowCoordinator.submitNoGoogleLogin() } } } private fun logout() { viewModelScope.launch { Loading @@ -114,17 +106,20 @@ class LoginViewModel @Inject constructor( } } private suspend fun submitLogin( private fun launchLoginSubmission( @StringRes fallbackMessageRes: Int, action: suspend () -> LoginWorkflowResult, ) { beginSubmission() if (!beginSubmission()) return viewModelScope.launch { try { handleLoginResult(action(), fallbackMessageRes) } finally { finishSubmission() } } } private fun handleLoginResult( result: LoginWorkflowResult, Loading Loading @@ -154,14 +149,18 @@ class LoginViewModel @Inject constructor( } } private fun beginSubmission() { _uiState.update { currentState -> currentState.copy( private fun beginSubmission(): Boolean { val currentState = _uiState.value if (currentState.isSubmitting) { return false } _uiState.value = currentState.copy( isSubmitting = true, errorMessage = null, navigationTarget = null, ) } return true } private fun finishSubmission() { Loading app/src/main/java/foundation/e/apps/ui/application/ApplicationViewModel.kt +15 −6 Original line number Diff line number Diff line Loading @@ -112,7 +112,7 @@ class ApplicationViewModel @Inject constructor( applicationLiveData.postValue(result) updateShareVisibilityState(app.shareUri.toString()) updateAppContentRatingState(packageName, app.contentRating) updateAppContentRatingState(packageName, app.contentRating, source) } catch (e: InternalException.AppNotFound) { _errorMessageLiveData.postValue(R.string.app_not_found) scheduleAutoRedirect() Loading @@ -124,16 +124,25 @@ class ApplicationViewModel @Inject constructor( private suspend fun updateAppContentRatingState( packageName: String, contentRating: ContentRating contentRating: ContentRating, source: Source, ) { // Initially update the state without ID to show the UI immediately _appContentRatingState.update { contentRating } val ratingWithId = playStoreRepository.getContentRatingWithId(packageName, contentRating) if (source != Source.PLAY_STORE) { return } runCatching { playStoreRepository.getContentRatingWithId(packageName, contentRating) }.onSuccess { ratingWithId -> // Later, update with a new rating; no visual change in the UI val updatedContentRating = contentRating.copy(id = ratingWithId.id) _appContentRatingState.update { updatedContentRating } }.onFailure { throwable -> Timber.w(throwable, "Failed to enrich content rating for %s", packageName) } } private fun updateShareVisibilityState(shareUri: String) { Loading app/src/main/java/foundation/e/apps/ui/application/dialog/PendingOwnUpdateViewModel.kt 0 → 100644 +26 −0 Original line number Diff line number Diff line /* * Copyright (C) 2026 e Foundation * * This program is free software: you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation, either version 3 of the License, or * (at your option) any later version. * * This program is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with this program. If not, see <https://www.gnu.org/licenses/>. * */ package foundation.e.apps.ui.application.dialog import androidx.lifecycle.ViewModel import foundation.e.apps.data.application.data.Application class PendingOwnUpdateViewModel : ViewModel() { var pendingApplication: Application? = null } app/src/main/java/foundation/e/apps/ui/applicationlist/ApplicationListFragment.kt +5 −6 Original line number Diff line number Diff line Loading @@ -50,6 +50,7 @@ import foundation.e.apps.ui.AppInfoFetchViewModel import foundation.e.apps.ui.AppProgressViewModel import foundation.e.apps.ui.PrivacyInfoViewModel import foundation.e.apps.ui.application.dialog.OwnUpdateWarningDialogFragment import foundation.e.apps.ui.application.dialog.PendingOwnUpdateViewModel import foundation.e.apps.ui.application.dialog.ownUpdateWarningDialogAction import foundation.e.apps.ui.application.subFrags.ApplicationDialogFragment import foundation.e.apps.ui.parentFragment.LegacyLoadFailureDialogs Loading Loading @@ -79,6 +80,7 @@ class ApplicationListFragment : private val sessionViewModel: SessionViewModel by activityViewModels() private val mainActivityViewModel: MainActivityViewModel by activityViewModels() private val appProgressViewModel: AppProgressViewModel by viewModels() private val pendingOwnUpdateViewModel: PendingOwnUpdateViewModel by viewModels() private val loadFailureDialogs by lazy { LegacyLoadFailureDialogs(this, sessionViewModel, loginViewModel) } Loading @@ -87,8 +89,6 @@ class ApplicationListFragment : private val binding get() = _binding!! private lateinit var listAdapter: ApplicationListRVAdapter private var pendingSelfUpdateApplication: Application? = null companion object { private const val OWN_UPDATE_DIALOG_TAG = "application_list_own_update_dialog" } Loading Loading @@ -168,7 +168,6 @@ class ApplicationListFragment : override fun onDestroyView() { super.onDestroyView() loadFailureDialogs.dismissActiveDialog() pendingSelfUpdateApplication = null _binding?.recyclerView?.adapter = null _binding = null } Loading Loading @@ -227,8 +226,8 @@ class ApplicationListFragment : OwnUpdateWarningDialogFragment.REQUEST_KEY, viewLifecycleOwner, ) { _, result -> val applicationToInstall = pendingSelfUpdateApplication pendingSelfUpdateApplication = null val applicationToInstall = pendingOwnUpdateViewModel.pendingApplication pendingOwnUpdateViewModel.pendingApplication = null if (result.ownUpdateWarningDialogAction() == OwnUpdateWarningDialogFragment.ACTION_CONFIRM Loading @@ -239,7 +238,7 @@ class ApplicationListFragment : } private fun showOwnUpdateWarningDialog(application: Application) { pendingSelfUpdateApplication = application pendingOwnUpdateViewModel.pendingApplication = application if (childFragmentManager.findFragmentByTag(OWN_UPDATE_DIALOG_TAG) != null) { return } Loading app/src/main/java/foundation/e/apps/ui/search/SearchFragment.kt +5 −6 Original line number Diff line number Diff line Loading @@ -54,6 +54,7 @@ import foundation.e.apps.ui.AppInfoFetchViewModel import foundation.e.apps.ui.AppProgressViewModel import foundation.e.apps.ui.PrivacyInfoViewModel import foundation.e.apps.ui.application.dialog.OwnUpdateWarningDialogFragment import foundation.e.apps.ui.application.dialog.PendingOwnUpdateViewModel import foundation.e.apps.ui.application.dialog.ownUpdateWarningDialogAction import foundation.e.apps.ui.application.subFrags.ApplicationDialogFragment import foundation.e.apps.ui.applicationlist.ApplicationListRVAdapter Loading @@ -75,6 +76,7 @@ class SearchFragment : Fragment(R.layout.fragment_search), ApplicationInstaller, private val appInfoFetchViewModel: AppInfoFetchViewModel by viewModels() val mainActivityViewModel: MainActivityViewModel by activityViewModels() private val appProgressViewModel: AppProgressViewModel by viewModels() private val pendingOwnUpdateViewModel: PendingOwnUpdateViewModel by viewModels() private var lastSearch = "" Loading @@ -88,8 +90,6 @@ class SearchFragment : Fragment(R.layout.fragment_search), ApplicationInstaller, lateinit var filterChipOpenSource: Chip lateinit var filterChipPWA: Chip private var pendingSelfUpdateApplication: Application? = null override fun onViewCreated(view: View, savedInstanceState: Bundle?) { super.onViewCreated(view, savedInstanceState) _binding = FragmentSearchBinding.bind(view) Loading Loading @@ -411,7 +411,6 @@ class SearchFragment : Fragment(R.layout.fragment_search), ApplicationInstaller, override fun onDestroyView() { super.onDestroyView() pendingSelfUpdateApplication = null _binding = null searchView = null shimmerLayout = null Loading Loading @@ -479,8 +478,8 @@ class SearchFragment : Fragment(R.layout.fragment_search), ApplicationInstaller, OwnUpdateWarningDialogFragment.REQUEST_KEY, viewLifecycleOwner, ) { _, result -> val applicationToInstall = pendingSelfUpdateApplication pendingSelfUpdateApplication = null val applicationToInstall = pendingOwnUpdateViewModel.pendingApplication pendingOwnUpdateViewModel.pendingApplication = null if (result.ownUpdateWarningDialogAction() == OwnUpdateWarningDialogFragment.ACTION_CONFIRM Loading @@ -491,7 +490,7 @@ class SearchFragment : Fragment(R.layout.fragment_search), ApplicationInstaller, } private fun showOwnUpdateWarningDialog(application: Application) { pendingSelfUpdateApplication = application pendingOwnUpdateViewModel.pendingApplication = application if (childFragmentManager.findFragmentByTag(OWN_UPDATE_DIALOG_TAG) != null) { return } Loading Loading
app/src/main/java/foundation/e/apps/feature/auth/login/LoginViewModel.kt +28 −29 Original line number Diff line number Diff line Loading @@ -73,39 +73,31 @@ class LoginViewModel @Inject constructor( email: String, oauthToken: String, ) { viewModelScope.launch { submitLogin(R.string.sign_in_microg_login_failed) { launchLoginSubmission(R.string.sign_in_microg_login_failed) { loginWorkflowCoordinator.submitMicrogLogin(email, oauthToken) } } } private fun initiateAnonymousLogin() { viewModelScope.launch { submitLogin(R.string.anonymous_login_failed_desc) { launchLoginSubmission(R.string.anonymous_login_failed_desc) { loginWorkflowCoordinator.submitAnonymousLogin() } } } private fun initiateGoogleLogin( email: String, oauthToken: String, ) { viewModelScope.launch { submitLogin(R.string.sign_in_failed_desc) { launchLoginSubmission(R.string.sign_in_failed_desc) { loginWorkflowCoordinator.submitGoogleLogin(email, oauthToken) } } } private fun initiateNoGoogleLogin() { viewModelScope.launch { submitLogin(R.string.something_went_wrong) { launchLoginSubmission(R.string.something_went_wrong) { loginWorkflowCoordinator.submitNoGoogleLogin() } } } private fun logout() { viewModelScope.launch { Loading @@ -114,17 +106,20 @@ class LoginViewModel @Inject constructor( } } private suspend fun submitLogin( private fun launchLoginSubmission( @StringRes fallbackMessageRes: Int, action: suspend () -> LoginWorkflowResult, ) { beginSubmission() if (!beginSubmission()) return viewModelScope.launch { try { handleLoginResult(action(), fallbackMessageRes) } finally { finishSubmission() } } } private fun handleLoginResult( result: LoginWorkflowResult, Loading Loading @@ -154,14 +149,18 @@ class LoginViewModel @Inject constructor( } } private fun beginSubmission() { _uiState.update { currentState -> currentState.copy( private fun beginSubmission(): Boolean { val currentState = _uiState.value if (currentState.isSubmitting) { return false } _uiState.value = currentState.copy( isSubmitting = true, errorMessage = null, navigationTarget = null, ) } return true } private fun finishSubmission() { Loading
app/src/main/java/foundation/e/apps/ui/application/ApplicationViewModel.kt +15 −6 Original line number Diff line number Diff line Loading @@ -112,7 +112,7 @@ class ApplicationViewModel @Inject constructor( applicationLiveData.postValue(result) updateShareVisibilityState(app.shareUri.toString()) updateAppContentRatingState(packageName, app.contentRating) updateAppContentRatingState(packageName, app.contentRating, source) } catch (e: InternalException.AppNotFound) { _errorMessageLiveData.postValue(R.string.app_not_found) scheduleAutoRedirect() Loading @@ -124,16 +124,25 @@ class ApplicationViewModel @Inject constructor( private suspend fun updateAppContentRatingState( packageName: String, contentRating: ContentRating contentRating: ContentRating, source: Source, ) { // Initially update the state without ID to show the UI immediately _appContentRatingState.update { contentRating } val ratingWithId = playStoreRepository.getContentRatingWithId(packageName, contentRating) if (source != Source.PLAY_STORE) { return } runCatching { playStoreRepository.getContentRatingWithId(packageName, contentRating) }.onSuccess { ratingWithId -> // Later, update with a new rating; no visual change in the UI val updatedContentRating = contentRating.copy(id = ratingWithId.id) _appContentRatingState.update { updatedContentRating } }.onFailure { throwable -> Timber.w(throwable, "Failed to enrich content rating for %s", packageName) } } private fun updateShareVisibilityState(shareUri: String) { Loading
app/src/main/java/foundation/e/apps/ui/application/dialog/PendingOwnUpdateViewModel.kt 0 → 100644 +26 −0 Original line number Diff line number Diff line /* * Copyright (C) 2026 e Foundation * * This program is free software: you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation, either version 3 of the License, or * (at your option) any later version. * * This program is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with this program. If not, see <https://www.gnu.org/licenses/>. * */ package foundation.e.apps.ui.application.dialog import androidx.lifecycle.ViewModel import foundation.e.apps.data.application.data.Application class PendingOwnUpdateViewModel : ViewModel() { var pendingApplication: Application? = null }
app/src/main/java/foundation/e/apps/ui/applicationlist/ApplicationListFragment.kt +5 −6 Original line number Diff line number Diff line Loading @@ -50,6 +50,7 @@ import foundation.e.apps.ui.AppInfoFetchViewModel import foundation.e.apps.ui.AppProgressViewModel import foundation.e.apps.ui.PrivacyInfoViewModel import foundation.e.apps.ui.application.dialog.OwnUpdateWarningDialogFragment import foundation.e.apps.ui.application.dialog.PendingOwnUpdateViewModel import foundation.e.apps.ui.application.dialog.ownUpdateWarningDialogAction import foundation.e.apps.ui.application.subFrags.ApplicationDialogFragment import foundation.e.apps.ui.parentFragment.LegacyLoadFailureDialogs Loading Loading @@ -79,6 +80,7 @@ class ApplicationListFragment : private val sessionViewModel: SessionViewModel by activityViewModels() private val mainActivityViewModel: MainActivityViewModel by activityViewModels() private val appProgressViewModel: AppProgressViewModel by viewModels() private val pendingOwnUpdateViewModel: PendingOwnUpdateViewModel by viewModels() private val loadFailureDialogs by lazy { LegacyLoadFailureDialogs(this, sessionViewModel, loginViewModel) } Loading @@ -87,8 +89,6 @@ class ApplicationListFragment : private val binding get() = _binding!! private lateinit var listAdapter: ApplicationListRVAdapter private var pendingSelfUpdateApplication: Application? = null companion object { private const val OWN_UPDATE_DIALOG_TAG = "application_list_own_update_dialog" } Loading Loading @@ -168,7 +168,6 @@ class ApplicationListFragment : override fun onDestroyView() { super.onDestroyView() loadFailureDialogs.dismissActiveDialog() pendingSelfUpdateApplication = null _binding?.recyclerView?.adapter = null _binding = null } Loading Loading @@ -227,8 +226,8 @@ class ApplicationListFragment : OwnUpdateWarningDialogFragment.REQUEST_KEY, viewLifecycleOwner, ) { _, result -> val applicationToInstall = pendingSelfUpdateApplication pendingSelfUpdateApplication = null val applicationToInstall = pendingOwnUpdateViewModel.pendingApplication pendingOwnUpdateViewModel.pendingApplication = null if (result.ownUpdateWarningDialogAction() == OwnUpdateWarningDialogFragment.ACTION_CONFIRM Loading @@ -239,7 +238,7 @@ class ApplicationListFragment : } private fun showOwnUpdateWarningDialog(application: Application) { pendingSelfUpdateApplication = application pendingOwnUpdateViewModel.pendingApplication = application if (childFragmentManager.findFragmentByTag(OWN_UPDATE_DIALOG_TAG) != null) { return } Loading
app/src/main/java/foundation/e/apps/ui/search/SearchFragment.kt +5 −6 Original line number Diff line number Diff line Loading @@ -54,6 +54,7 @@ import foundation.e.apps.ui.AppInfoFetchViewModel import foundation.e.apps.ui.AppProgressViewModel import foundation.e.apps.ui.PrivacyInfoViewModel import foundation.e.apps.ui.application.dialog.OwnUpdateWarningDialogFragment import foundation.e.apps.ui.application.dialog.PendingOwnUpdateViewModel import foundation.e.apps.ui.application.dialog.ownUpdateWarningDialogAction import foundation.e.apps.ui.application.subFrags.ApplicationDialogFragment import foundation.e.apps.ui.applicationlist.ApplicationListRVAdapter Loading @@ -75,6 +76,7 @@ class SearchFragment : Fragment(R.layout.fragment_search), ApplicationInstaller, private val appInfoFetchViewModel: AppInfoFetchViewModel by viewModels() val mainActivityViewModel: MainActivityViewModel by activityViewModels() private val appProgressViewModel: AppProgressViewModel by viewModels() private val pendingOwnUpdateViewModel: PendingOwnUpdateViewModel by viewModels() private var lastSearch = "" Loading @@ -88,8 +90,6 @@ class SearchFragment : Fragment(R.layout.fragment_search), ApplicationInstaller, lateinit var filterChipOpenSource: Chip lateinit var filterChipPWA: Chip private var pendingSelfUpdateApplication: Application? = null override fun onViewCreated(view: View, savedInstanceState: Bundle?) { super.onViewCreated(view, savedInstanceState) _binding = FragmentSearchBinding.bind(view) Loading Loading @@ -411,7 +411,6 @@ class SearchFragment : Fragment(R.layout.fragment_search), ApplicationInstaller, override fun onDestroyView() { super.onDestroyView() pendingSelfUpdateApplication = null _binding = null searchView = null shimmerLayout = null Loading Loading @@ -479,8 +478,8 @@ class SearchFragment : Fragment(R.layout.fragment_search), ApplicationInstaller, OwnUpdateWarningDialogFragment.REQUEST_KEY, viewLifecycleOwner, ) { _, result -> val applicationToInstall = pendingSelfUpdateApplication pendingSelfUpdateApplication = null val applicationToInstall = pendingOwnUpdateViewModel.pendingApplication pendingOwnUpdateViewModel.pendingApplication = null if (result.ownUpdateWarningDialogAction() == OwnUpdateWarningDialogFragment.ACTION_CONFIRM Loading @@ -491,7 +490,7 @@ class SearchFragment : Fragment(R.layout.fragment_search), ApplicationInstaller, } private fun showOwnUpdateWarningDialog(application: Application) { pendingSelfUpdateApplication = application pendingOwnUpdateViewModel.pendingApplication = application if (childFragmentManager.findFragmentByTag(OWN_UPDATE_DIALOG_TAG) != null) { return } Loading