mirror of
https://github.com/f-droid/fdroidclient.git
synced 2026-02-19 15:48:19 -05:00
Break up RepoDetailsState into repoFlow and archiveStateFlow
Before this commit, the UI would not update, because the `repo` in the `state` was never updated. This was mostly noticeable when toggling mirrors on and off. So the UI needs to use repoFlow directly (to listen to RepoManager changes). But then there is no need for RepoDetailsState anymore.
This commit is contained in:
@@ -13,6 +13,7 @@ import androidx.lifecycle.ViewModelProvider
|
||||
import androidx.lifecycle.viewmodel.MutableCreationExtras
|
||||
import com.google.android.material.dialog.MaterialAlertDialogBuilder
|
||||
import com.google.android.material.textfield.TextInputLayout
|
||||
import org.fdroid.database.Repository
|
||||
import org.fdroid.download.Mirror
|
||||
import org.fdroid.fdroid.FDroidApp
|
||||
import org.fdroid.fdroid.R
|
||||
@@ -36,15 +37,7 @@ class RepoDetailsActivity : AppCompatActivity() {
|
||||
private lateinit var viewModel: RepoDetailsViewModel
|
||||
|
||||
// Only call this once in onCreate()
|
||||
private fun initViewModel() {
|
||||
val repoId = intent.getLongExtra(ARG_REPO_ID, 0)
|
||||
val repo = FDroidApp.getRepoManager(this).getRepository(repoId)
|
||||
if (repo == null) {
|
||||
// repo must have been deleted just now (maybe slow UI?)
|
||||
finish()
|
||||
return
|
||||
}
|
||||
|
||||
private fun initViewModel(repo: Repository) {
|
||||
val factory = RepoDetailsViewModel.Factory
|
||||
val extras = MutableCreationExtras().apply {
|
||||
set(RepoDetailsViewModel.APP_KEY, application)
|
||||
@@ -60,15 +53,34 @@ class RepoDetailsActivity : AppCompatActivity() {
|
||||
(application as FDroidApp).setSecureWindow(this)
|
||||
(application as FDroidApp).applyPureBlackBackgroundInDarkTheme(this)
|
||||
|
||||
initViewModel()
|
||||
val repoId = intent.getLongExtra(ARG_REPO_ID, 0)
|
||||
val repo = FDroidApp.getRepoManager(this).getRepository(repoId)
|
||||
if (repo == null) {
|
||||
// repo must have been deleted just now (maybe slow UI?)
|
||||
finish()
|
||||
return
|
||||
}
|
||||
|
||||
initViewModel(repo)
|
||||
|
||||
// Needs an observer because it is lazy
|
||||
viewModel.repoLiveData.observe(this, {})
|
||||
|
||||
setContent {
|
||||
val state by viewModel.state.collectAsState()
|
||||
val repo by viewModel.repoFlow.collectAsState(repo)
|
||||
val archiveState by viewModel.archiveStateFlow.collectAsState(ArchiveState.UNKNOWN)
|
||||
val numberOfApps by viewModel.numberAppsFlow.collectAsState(0)
|
||||
|
||||
val r = repo
|
||||
if (r == null) {
|
||||
finish()
|
||||
return@setContent
|
||||
}
|
||||
|
||||
FDroidContent {
|
||||
RepoDetailsScreen(
|
||||
state = state,
|
||||
repo = r,
|
||||
archiveState = archiveState,
|
||||
numberOfApps = numberOfApps,
|
||||
// app bar
|
||||
onBackClicked = { onBackPressedDispatcher.onBackPressed() },
|
||||
@@ -92,10 +104,10 @@ class RepoDetailsActivity : AppCompatActivity() {
|
||||
}
|
||||
|
||||
private fun onShareClicked() {
|
||||
val uri = viewModel.state.value.repo.getShareUri()
|
||||
val repo = viewModel.repoLiveData.value ?: return
|
||||
val intent = Intent(Intent.ACTION_SEND).apply {
|
||||
type = "text/plain"
|
||||
putExtra(Intent.EXTRA_TEXT, uri)
|
||||
putExtra(Intent.EXTRA_TEXT, repo.shareUri)
|
||||
}
|
||||
startActivity(
|
||||
Intent.createChooser(intent, getResources().getString(R.string.share_repository))
|
||||
@@ -138,7 +150,7 @@ class RepoDetailsActivity : AppCompatActivity() {
|
||||
}
|
||||
|
||||
private fun onShowAppsClicked() {
|
||||
val repo = viewModel.state.value.repo
|
||||
val repo = viewModel.repoLiveData.value ?: return
|
||||
if (!repo.enabled) {
|
||||
return
|
||||
}
|
||||
@@ -149,7 +161,7 @@ class RepoDetailsActivity : AppCompatActivity() {
|
||||
}
|
||||
|
||||
private fun onEditCredentialsClicked() {
|
||||
val repo = viewModel.state.value.repo
|
||||
val repo = viewModel.repoLiveData.value ?: return
|
||||
|
||||
val view = layoutInflater.inflate(R.layout.login, null, false)
|
||||
val usernameInput = view.findViewById<TextInputLayout>(R.id.edit_name)
|
||||
@@ -178,8 +190,8 @@ class RepoDetailsActivity : AppCompatActivity() {
|
||||
}
|
||||
|
||||
private fun onShareMirror(mirror: Mirror) {
|
||||
val fingerprint = viewModel.state.value.repo.fingerprint
|
||||
val uri = mirror.getFDroidLinkUrl(fingerprint)
|
||||
val repo = viewModel.repoLiveData.value ?: return
|
||||
val uri = mirror.getFDroidLinkUrl(repo.fingerprint)
|
||||
val intent = Intent(Intent.ACTION_SEND).apply {
|
||||
type = "text/plain"
|
||||
putExtra(Intent.EXTRA_TEXT, uri)
|
||||
|
||||
@@ -63,7 +63,8 @@ import org.fdroid.fdroid.compose.FDroidSwitchRow
|
||||
|
||||
@Composable
|
||||
fun RepoDetailsScreen(
|
||||
state: RepoDetailsState,
|
||||
repo: Repository,
|
||||
archiveState: ArchiveState,
|
||||
numberOfApps: Int,
|
||||
// app bar functions
|
||||
onBackClicked: () -> Unit,
|
||||
@@ -80,9 +81,9 @@ fun RepoDetailsScreen(
|
||||
onShareMirror: (Mirror) -> Unit,
|
||||
onDeleteMirror: (Mirror) -> Unit,
|
||||
) {
|
||||
val officialMirrors = state.repo.getAllOfficialMirrors()
|
||||
val userMirrors = state.repo.getAllUserMirrors()
|
||||
val disabledMirrors = state.repo.disabledMirrors.toHashSet()
|
||||
val officialMirrors = repo.getAllOfficialMirrors()
|
||||
val userMirrors = repo.getAllUserMirrors()
|
||||
val disabledMirrors = repo.disabledMirrors.toHashSet()
|
||||
|
||||
Scaffold(topBar = {
|
||||
TopAppBar(elevation = 4.dp,
|
||||
@@ -125,17 +126,17 @@ fun RepoDetailsScreen(
|
||||
) {
|
||||
Spacer(modifier = Modifier) // spacedBy will provide the padding
|
||||
GeneralInfoCard(
|
||||
state.repo,
|
||||
state.archiveState,
|
||||
repo,
|
||||
archiveState,
|
||||
numberOfApps,
|
||||
onShowAppsClicked,
|
||||
onToggleArchiveClicked,
|
||||
)
|
||||
BasicAuthCard(state.repo, onEditCredentialsClicked)
|
||||
if (state.repo.certificate.isEmpty()) {
|
||||
BasicAuthCard(repo, onEditCredentialsClicked)
|
||||
if (repo.certificate.isEmpty()) {
|
||||
UnsignedCard()
|
||||
} else {
|
||||
FingerprintCard(state.repo)
|
||||
FingerprintExpandable(repo)
|
||||
}
|
||||
// The repo's address is currently also an official mirror.
|
||||
// So if there is only one mirror, this is the address => don't show this section.
|
||||
@@ -423,7 +424,8 @@ fun RepoDetailsScreenPreview() {
|
||||
val repo = FDroidApp.createSwapRepo("https://example.org/fdroid/repo", "foo bar")
|
||||
FDroidContent {
|
||||
RepoDetailsScreen(
|
||||
RepoDetailsState(repo, ArchiveState.ENABLED),
|
||||
repo,
|
||||
ArchiveState.ENABLED,
|
||||
numberOfApps = 42,
|
||||
{}, {}, {}, {}, {}, // app bar
|
||||
{}, {}, {}, // other buttons
|
||||
|
||||
@@ -17,8 +17,6 @@ import info.guardianproject.netcipher.NetCipher
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.flow.Flow
|
||||
import kotlinx.coroutines.flow.MutableStateFlow
|
||||
import kotlinx.coroutines.flow.asStateFlow
|
||||
import kotlinx.coroutines.flow.combine
|
||||
import kotlinx.coroutines.flow.distinctUntilChanged
|
||||
import kotlinx.coroutines.flow.flowOn
|
||||
import kotlinx.coroutines.flow.map
|
||||
@@ -32,11 +30,6 @@ import org.fdroid.fdroid.data.DBHelper
|
||||
import org.fdroid.fdroid.generateQrBitmapKt
|
||||
import org.fdroid.fdroid.work.RepoUpdateWorker
|
||||
|
||||
data class RepoDetailsState(
|
||||
val repo: Repository,
|
||||
val archiveState: ArchiveState,
|
||||
)
|
||||
|
||||
enum class ArchiveState {
|
||||
ENABLED,
|
||||
DISABLED,
|
||||
@@ -45,10 +38,12 @@ enum class ArchiveState {
|
||||
|
||||
class RepoDetailsViewModel(
|
||||
app: Application,
|
||||
initialRepo: Repository,
|
||||
private val initialRepo: Repository,
|
||||
) : AndroidViewModel(app) {
|
||||
|
||||
companion object {
|
||||
private const val TAG = "RepoDetailsViewModel"
|
||||
|
||||
// TODO: Use androidx.lifecycle.ViewModelProvider.AndroidViewModelFactory.Companion.APPLICATION_KEY
|
||||
// That seems to require setting up dependency injection.
|
||||
val APP_KEY = object : CreationExtras.Key<Application> {}
|
||||
@@ -62,19 +57,15 @@ class RepoDetailsViewModel(
|
||||
}
|
||||
}
|
||||
|
||||
private val repoId = initialRepo.repoId
|
||||
|
||||
private val repoManager = FDroidApp.getRepoManager(app)
|
||||
private val repositoryDao = DBHelper.getDb(app).getRepositoryDao()
|
||||
private val appDao = DBHelper.getDb(app).getAppDao()
|
||||
|
||||
private val _state = MutableStateFlow(
|
||||
RepoDetailsState(initialRepo, initialRepo.archiveState())
|
||||
)
|
||||
val state = _state.asStateFlow()
|
||||
val liveData = _state.asLiveData()
|
||||
|
||||
val repoFlow = combine(_state, repoManager.repositoriesState) { s, reposState ->
|
||||
reposState.find { repo -> repo.repoId == s.repo.repoId }
|
||||
}.distinctUntilChanged()
|
||||
val repoFlow: Flow<Repository?> = repoManager.repositoriesState.map { reposState ->
|
||||
reposState.find { repo -> repo.repoId == repoId }
|
||||
}
|
||||
val repoLiveData = repoFlow.asLiveData()
|
||||
|
||||
val numberAppsFlow: Flow<Int> = repoFlow.map { repo ->
|
||||
@@ -82,23 +73,24 @@ class RepoDetailsViewModel(
|
||||
appDao.getNumberOfAppsInRepository(repo.repoId)
|
||||
} else 0
|
||||
}.flowOn(Dispatchers.IO).distinctUntilChanged()
|
||||
val numberOfAppsLiveData = numberAppsFlow.asLiveData()
|
||||
|
||||
val archiveStateFlow = MutableStateFlow(initialRepo.archiveState())
|
||||
|
||||
val qrCodeLiveData = MutableLiveData<Bitmap?>(null)
|
||||
|
||||
fun setArchiveRepoEnabled(enabled: Boolean) {
|
||||
val repo = _state.value.repo
|
||||
_state.value = _state.value.copy(archiveState = ArchiveState.UNKNOWN)
|
||||
viewModelScope.launch(Dispatchers.IO) {
|
||||
val repo = repoLiveData.value ?: return@launch
|
||||
archiveStateFlow.emit(ArchiveState.UNKNOWN)
|
||||
try {
|
||||
val repoId = repoManager.setArchiveRepoEnabled(repo, enabled, NetCipher.getProxy())
|
||||
_state.value = _state.value.copy(archiveState = enabled.toArchiveState())
|
||||
archiveStateFlow.emit(enabled.toArchiveState())
|
||||
if (enabled && repoId != null) withContext(Dispatchers.Main) {
|
||||
RepoUpdateWorker.updateNow(getApplication(), repoId)
|
||||
}
|
||||
} catch (e: Exception) {
|
||||
Log.e(this.javaClass.simpleName, "Error toggling archive repo: ", e)
|
||||
_state.value = _state.value.copy(archiveState = repo.archiveState())
|
||||
Log.e(TAG, "Error toggling archive repo: ", e)
|
||||
archiveStateFlow.emit(repo.archiveState())
|
||||
withContext(Dispatchers.Main) {
|
||||
Toast.makeText(getApplication(), R.string.repo_archive_failed, LENGTH_SHORT)
|
||||
.show()
|
||||
@@ -108,35 +100,24 @@ class RepoDetailsViewModel(
|
||||
}
|
||||
|
||||
fun deleteRepository() {
|
||||
val repoId = _state.value.repo.repoId
|
||||
viewModelScope.launch(Dispatchers.IO) {
|
||||
repoManager.deleteRepository(repoId)
|
||||
}
|
||||
}
|
||||
|
||||
fun updateUsernameAndPassword(username: String, password: String) {
|
||||
val repoId = _state.value.repo.repoId
|
||||
viewModelScope.launch(Dispatchers.IO) {
|
||||
repositoryDao.updateUsernameAndPassword(repoId, username, password)
|
||||
}
|
||||
}
|
||||
|
||||
fun updateDisabledMirrors(toDisable: List<String>) {
|
||||
val repoId = _state.value.repo.repoId
|
||||
viewModelScope.launch(Dispatchers.IO) {
|
||||
repositoryDao.updateDisabledMirrors(repoId, toDisable)
|
||||
}
|
||||
}
|
||||
|
||||
fun setMirrorEnabled(mirror: Mirror, enabled: Boolean) {
|
||||
val repoId = _state.value.repo.repoId
|
||||
viewModelScope.launch(Dispatchers.IO) {
|
||||
repoManager.setMirrorEnabled(repoId, mirror, enabled)
|
||||
}
|
||||
}
|
||||
|
||||
fun deleteUserMirror(mirror: Mirror) {
|
||||
val repoId = _state.value.repo.repoId
|
||||
viewModelScope.launch(Dispatchers.IO) {
|
||||
repoManager.deleteUserMirror(repoId, mirror)
|
||||
}
|
||||
@@ -159,13 +140,13 @@ class RepoDetailsViewModel(
|
||||
|
||||
// TODO: initialise this once on ViewModel creation, and don't take an Activity, do fixed size
|
||||
fun generateQrCode(activity: AppCompatActivity) {
|
||||
val repo = _state.value.repo
|
||||
if (repo.address.startsWith("content://") || repo.address.startsWith("file://")) {
|
||||
// no need to show a QR Code, it is not shareable
|
||||
qrCodeLiveData.value = null
|
||||
return
|
||||
}
|
||||
viewModelScope.launch(Dispatchers.Default) {
|
||||
val repo = repoLiveData.value ?: return@launch
|
||||
if (repo.address.startsWith("content://") || repo.address.startsWith("file://")) {
|
||||
// no need to show a QR Code, it is not shareable
|
||||
qrCodeLiveData.value = null
|
||||
return@launch
|
||||
}
|
||||
val bitmap = generateQrBitmapKt(activity, repo.shareUri)
|
||||
withContext(Dispatchers.Main) {
|
||||
qrCodeLiveData.value = bitmap
|
||||
|
||||
Reference in New Issue
Block a user