feat: show the proper message in the custom picker when all images are either uploaded or marked. (#6095)

* feat: show the message "Congratulations, all pictures in this album have been either uploaded or marked as not for upload." in the custom picker when all images are either uploaded or marked.

* refactor: fix indentation in the code

* refactor: replace LiveData with StateFlow

* fix: fixed the bug that was causing one ImageFragment testcase to fail.

* fix: fixed the Congratulation message being shown for a brief moment while switching off the switch

* refactor: move hardcoded string to strings.xml.

* docs: add comment to clarify visibility logic in ImageFragment

---------

Co-authored-by: Nicolas Raoul <nicolas.raoul@gmail.com>
This commit is contained in:
yuvraj-coder1 2025-01-18 19:34:28 +05:30 committed by GitHub
parent 23e1f01783
commit 1c6ebafb29
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
4 changed files with 81 additions and 3 deletions

View file

@ -24,6 +24,7 @@ import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.MainScope
import kotlinx.coroutines.cancel
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.launch
import java.util.TreeMap
import kotlin.collections.ArrayList
@ -103,6 +104,18 @@ class ImageAdapter(
*/
private var imagePositionAsPerIncreasingOrder = 0
/**
* Stores the number of images currently visible on the screen
*/
private val _currentImagesCount = MutableStateFlow(0)
val currentImagesCount = _currentImagesCount
/**
* Stores whether images are being loaded or not
*/
private val _isLoadingImages = MutableStateFlow(false)
val isLoadingImages = _isLoadingImages
/**
* Coroutine Dispatchers and Scope.
*/
@ -184,8 +197,12 @@ class ImageAdapter(
// If the position is not already visited, that means the position is new then
// finds the next actionable image position from all images
if (!alreadyAddedPositions.contains(position)) {
processThumbnailForActionedImage(holder, position, uploadingContributionList)
processThumbnailForActionedImage(
holder,
position,
uploadingContributionList
)
_isLoadingImages.value = false
// If the position is already visited, that means the image is already present
// inside map, so it will fetch the image from the map and load in the holder
} else {
@ -231,6 +248,7 @@ class ImageAdapter(
position: Int,
uploadingContributionList: List<Contribution>,
) {
_isLoadingImages.value = true
val next =
imageLoader.nextActionableImage(
allImages,
@ -252,6 +270,7 @@ class ImageAdapter(
actionableImagesMap[next] = allImages[next]
alreadyAddedPositions.add(imagePositionAsPerIncreasingOrder)
imagePositionAsPerIncreasingOrder++
_currentImagesCount.value = imagePositionAsPerIncreasingOrder
Glide
.with(holder.image)
.load(allImages[next].uri)
@ -267,6 +286,7 @@ class ImageAdapter(
reachedEndOfFolder = true
notifyItemRemoved(position)
}
_isLoadingImages.value = false
}
/**
@ -372,6 +392,7 @@ class ImageAdapter(
emptyMap: TreeMap<Int, Image>,
uploadedImages: List<Contribution> = ArrayList(),
) {
_isLoadingImages.value = true
allImages = fixedImages
val oldImageList: ArrayList<Image> = images
val newImageList: ArrayList<Image> = ArrayList(newImages)
@ -382,6 +403,7 @@ class ImageAdapter(
reachedEndOfFolder = false
selectedImages = ArrayList()
imagePositionAsPerIncreasingOrder = 0
_currentImagesCount.value = imagePositionAsPerIncreasingOrder
val diffResult =
DiffUtil.calculateDiff(
ImagesDiffCallback(oldImageList, newImageList),
@ -441,6 +463,7 @@ class ImageAdapter(
val entry = iterator.next()
if (entry.value == image) {
imagePositionAsPerIncreasingOrder -= 1
_currentImagesCount.value = imagePositionAsPerIncreasingOrder
iterator.remove()
alreadyAddedPositions.removeAt(alreadyAddedPositions.size - 1)
notifyItemRemoved(index)

View file

@ -12,8 +12,12 @@ import android.widget.ProgressBar
import android.widget.Switch
import androidx.appcompat.app.AlertDialog
import androidx.constraintlayout.widget.ConstraintLayout
import androidx.core.view.isVisible
import androidx.lifecycle.Lifecycle
import androidx.lifecycle.Observer
import androidx.lifecycle.ViewModelProvider
import androidx.lifecycle.lifecycleScope
import androidx.lifecycle.repeatOnLifecycle
import androidx.recyclerview.widget.GridLayoutManager
import androidx.recyclerview.widget.RecyclerView
import fr.free.nrw.commons.contributions.Contribution
@ -38,6 +42,10 @@ import fr.free.nrw.commons.theme.BaseActivity
import fr.free.nrw.commons.upload.FileProcessor
import fr.free.nrw.commons.upload.FileUtilsWrapper
import io.reactivex.schedulers.Schedulers
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.asStateFlow
import kotlinx.coroutines.flow.combine
import kotlinx.coroutines.launch
import java.util.TreeMap
import javax.inject.Inject
import kotlin.collections.ArrayList
@ -80,6 +88,12 @@ class ImageFragment :
*/
var allImages: ArrayList<Image> = ArrayList()
/**
* Keeps track of switch state
*/
private val _switchState = MutableStateFlow(false)
val switchState = _switchState.asStateFlow()
/**
* View model Factory.
*/
@ -214,7 +228,11 @@ class ImageFragment :
switch = binding?.switchWidget
switch?.visibility = View.VISIBLE
switch?.setOnCheckedChangeListener { _, isChecked -> onChangeSwitchState(isChecked) }
_switchState.value = switch?.isChecked ?: false
switch?.setOnCheckedChangeListener { _, isChecked ->
onChangeSwitchState(isChecked)
_switchState.value = isChecked
}
selectorRV = binding?.selectorRv
loader = binding?.loader
progressLayout = binding?.progressLayout
@ -234,6 +252,28 @@ class ImageFragment :
return binding?.root
}
/**
* onViewCreated
* Updates empty text view visibility based on image count, switch state, and loading status.
*/
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState)
viewLifecycleOwner.lifecycleScope.launch {
repeatOnLifecycle(Lifecycle.State.STARTED) {
combine(
imageAdapter.currentImagesCount,
switchState,
imageAdapter.isLoadingImages
) { imageCount, isChecked, isLoadingImages ->
Triple(imageCount, isChecked, isLoadingImages)
}.collect { (imageCount, isChecked, isLoadingImages) ->
binding?.allImagesUploadedOrMarked?.isVisible =
!isLoadingImages && !isChecked && imageCount == 0 && (switch?.isVisible == true)
}
}
}
}
private fun onChangeSwitchState(checked: Boolean) {
if (checked) {
showAlreadyActionedImages = true

View file

@ -49,6 +49,20 @@
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent" />
<TextView
android:id="@+id/all_images_uploaded_or_marked"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:visibility="gone"
android:textSize="16sp"
android:padding="@dimen/standard_gap"
android:textColor="@color/text_color_selector"
android:text="@string/congratulations_all_pictures_in_this_album_have_been_either_uploaded_or_marked_as_not_for_upload"
app:layout_constraintTop_toTopOf="parent"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
/>
<ProgressBar
android:id="@+id/loader"

View file

@ -867,5 +867,6 @@ Upload your first media by tapping on the add button.</string>
<string name="account_vanish_request_confirm"><![CDATA[Vanishing is a <b>last resort</b> and should <b>only be used when you wish to stop editing forever</b> and also to hide as many of your past associations as possible.<br/><br/>Account deletion on Wikimedia Commons is done by changing your account name to make it so others cannot recognize your contributions in a process called account vanishing. <b>Vanishing does not guarantee complete anonymity or remove contributions to the projects</b>.]]></string>
<string name="caption">Caption</string>
<string name="caption_copied_to_clipboard">Caption copied to clipboard</string>
<string name="congratulations_all_pictures_in_this_album_have_been_either_uploaded_or_marked_as_not_for_upload">Congratulations, all pictures in this album have been either uploaded or marked as not for upload.</string>
</resources>