mirror of
https://github.com/commons-app/apps-android-commons.git
synced 2025-10-26 20:33:53 +01:00
[GSoC] Full Screen Mode (#5032)
* Gesture detection implemented * Left and right swipe * Selection implemented * onDown implemented * onDown implemented * FS mode implemented * OnSwipe doc * Scope cancel * Added label in Manifest
This commit is contained in:
parent
a6c51a75a8
commit
52912087d6
14 changed files with 579 additions and 115 deletions
|
|
@ -46,7 +46,10 @@
|
||||||
android:finishOnTaskLaunch="true" />
|
android:finishOnTaskLaunch="true" />
|
||||||
|
|
||||||
<activity
|
<activity
|
||||||
android:name=".media.ZoomableActivity" />
|
android:name=".media.ZoomableActivity"
|
||||||
|
android:label="Zoomable Activity"
|
||||||
|
android:configChanges="screenSize|keyboard|orientation"
|
||||||
|
android:parentActivityName=".customselector.ui.selector.CustomSelectorActivity" />
|
||||||
|
|
||||||
<activity android:name=".auth.LoginActivity">
|
<activity android:name=".auth.LoginActivity">
|
||||||
<intent-filter>
|
<intent-filter>
|
||||||
|
|
|
||||||
|
|
@ -66,4 +66,18 @@ abstract class UploadedStatusDao {
|
||||||
suspend fun getUploadedFromImageSHA1(imageSHA1: String):UploadedStatus? {
|
suspend fun getUploadedFromImageSHA1(imageSHA1: String):UploadedStatus? {
|
||||||
return getFromImageSHA1(imageSHA1)
|
return getFromImageSHA1(imageSHA1)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Check whether the imageSHA1 is present in database
|
||||||
|
*/
|
||||||
|
@Query("SELECT COUNT() FROM uploaded_table WHERE imageSHA1 = (:imageSHA1) AND imageResult = (:imageResult) ")
|
||||||
|
abstract suspend fun findByImageSHA1(imageSHA1 : String, imageResult: Boolean): Int
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Check whether the modifiedImageSHA1 is present in database
|
||||||
|
*/
|
||||||
|
@Query("SELECT COUNT() FROM uploaded_table WHERE modifiedImageSHA1 = (:modifiedImageSHA1) AND modifiedImageResult = (:modifiedImageResult) ")
|
||||||
|
abstract suspend fun findByModifiedImageSHA1(modifiedImageSHA1 : String,
|
||||||
|
modifiedImageResult: Boolean): Int
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
@ -0,0 +1,10 @@
|
||||||
|
package fr.free.nrw.commons.customselector.helper
|
||||||
|
|
||||||
|
object CustomSelectorConstants {
|
||||||
|
|
||||||
|
const val TOTAL_IMAGES = "total_images"
|
||||||
|
const val TOTAL_SELECTED_IMAGES = "total_selected_images"
|
||||||
|
const val PRESENT_POSITION = "present_position"
|
||||||
|
const val NEW_SELECTED_IMAGES = "new_selected_images"
|
||||||
|
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,72 @@
|
||||||
|
package fr.free.nrw.commons.customselector.helper
|
||||||
|
|
||||||
|
import android.content.Context
|
||||||
|
import android.view.GestureDetector
|
||||||
|
import android.view.MotionEvent
|
||||||
|
import android.view.View
|
||||||
|
import kotlin.math.abs
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Class for detecting swipe gestures
|
||||||
|
*/
|
||||||
|
open class OnSwipeTouchListener(context: Context?) : View.OnTouchListener {
|
||||||
|
|
||||||
|
private val gestureDetector: GestureDetector
|
||||||
|
|
||||||
|
override fun onTouch(view: View?, motionEvent: MotionEvent?): Boolean {
|
||||||
|
return gestureDetector.onTouchEvent(motionEvent)
|
||||||
|
}
|
||||||
|
|
||||||
|
private inner class GestureListener : GestureDetector.SimpleOnGestureListener() {
|
||||||
|
|
||||||
|
private val SWIPE_THRESHOLD = 100
|
||||||
|
private val SWIPE_VELOCITY_THRESHOLD = 100
|
||||||
|
|
||||||
|
override fun onDown(e: MotionEvent?): Boolean {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onFling(
|
||||||
|
event1: MotionEvent,
|
||||||
|
event2: MotionEvent,
|
||||||
|
velocityX: Float,
|
||||||
|
velocityY: Float
|
||||||
|
): Boolean {
|
||||||
|
try {
|
||||||
|
val diffY: Float = event2.y - event1.y
|
||||||
|
val diffX: Float = event2.x - event1.x
|
||||||
|
if (abs(diffX) > abs(diffY)) {
|
||||||
|
if (abs(diffX) > SWIPE_THRESHOLD && abs(velocityX) >
|
||||||
|
SWIPE_VELOCITY_THRESHOLD) {
|
||||||
|
if (diffX > 0) {
|
||||||
|
onSwipeRight()
|
||||||
|
} else {
|
||||||
|
onSwipeLeft()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
if (abs(diffY) > SWIPE_THRESHOLD && abs(velocityY) >
|
||||||
|
SWIPE_VELOCITY_THRESHOLD) {
|
||||||
|
if (diffY > 0) {
|
||||||
|
onSwipeDown()
|
||||||
|
} else {
|
||||||
|
onSwipeUp()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} catch (exception: Exception) {
|
||||||
|
exception.printStackTrace()
|
||||||
|
}
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
open fun onSwipeRight() {}
|
||||||
|
open fun onSwipeLeft() {}
|
||||||
|
open fun onSwipeUp() {}
|
||||||
|
open fun onSwipeDown() {}
|
||||||
|
|
||||||
|
init {
|
||||||
|
gestureDetector = GestureDetector(context, GestureListener())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -19,5 +19,9 @@ interface ImageSelectListener {
|
||||||
* onLongPress
|
* onLongPress
|
||||||
* @param imageUri : uri of image
|
* @param imageUri : uri of image
|
||||||
*/
|
*/
|
||||||
fun onLongPress(imageUri: Uri)
|
fun onLongPress(
|
||||||
|
position: Int,
|
||||||
|
images: ArrayList<Image>,
|
||||||
|
selectedImages: ArrayList<Image>
|
||||||
|
)
|
||||||
}
|
}
|
||||||
|
|
@ -0,0 +1,7 @@
|
||||||
|
package fr.free.nrw.commons.customselector.listeners
|
||||||
|
|
||||||
|
import fr.free.nrw.commons.customselector.model.Image
|
||||||
|
|
||||||
|
interface PassDataListener {
|
||||||
|
fun passSelectedImages(selectedImages: ArrayList<Image>)
|
||||||
|
}
|
||||||
|
|
@ -228,7 +228,7 @@ class ImageAdapter(
|
||||||
|
|
||||||
// launch media preview on long click.
|
// launch media preview on long click.
|
||||||
holder.itemView.setOnLongClickListener {
|
holder.itemView.setOnLongClickListener {
|
||||||
imageSelectListener.onLongPress(image.uri)
|
imageSelectListener.onLongPress(position, images, selectedImages)
|
||||||
true
|
true
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -317,6 +317,13 @@ class ImageAdapter(
|
||||||
diffResult.dispatchUpdatesTo(this)
|
diffResult.dispatchUpdatesTo(this)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Set new selected images
|
||||||
|
*/
|
||||||
|
fun setSelectedImages(newSelectedImages: ArrayList<Image>){
|
||||||
|
selectedImages = ArrayList(newSelectedImages)
|
||||||
|
imageSelectListener.onSelectedImagesChanged(selectedImages, 0)
|
||||||
|
}
|
||||||
/**
|
/**
|
||||||
* Refresh the data in the adapter
|
* Refresh the data in the adapter
|
||||||
*/
|
*/
|
||||||
|
|
|
||||||
|
|
@ -4,7 +4,6 @@ import android.app.Activity
|
||||||
import android.app.Dialog
|
import android.app.Dialog
|
||||||
import android.content.Intent
|
import android.content.Intent
|
||||||
import android.content.SharedPreferences
|
import android.content.SharedPreferences
|
||||||
import android.net.Uri
|
|
||||||
import android.os.Bundle
|
import android.os.Bundle
|
||||||
import android.view.View
|
import android.view.View
|
||||||
import android.view.Window
|
import android.view.Window
|
||||||
|
|
@ -16,6 +15,7 @@ import androidx.lifecycle.ViewModelProvider
|
||||||
import fr.free.nrw.commons.R
|
import fr.free.nrw.commons.R
|
||||||
import fr.free.nrw.commons.customselector.database.NotForUploadStatus
|
import fr.free.nrw.commons.customselector.database.NotForUploadStatus
|
||||||
import fr.free.nrw.commons.customselector.database.NotForUploadStatusDao
|
import fr.free.nrw.commons.customselector.database.NotForUploadStatusDao
|
||||||
|
import fr.free.nrw.commons.customselector.helper.CustomSelectorConstants
|
||||||
import fr.free.nrw.commons.customselector.listeners.FolderClickListener
|
import fr.free.nrw.commons.customselector.listeners.FolderClickListener
|
||||||
import fr.free.nrw.commons.customselector.listeners.ImageSelectListener
|
import fr.free.nrw.commons.customselector.listeners.ImageSelectListener
|
||||||
import fr.free.nrw.commons.customselector.model.Image
|
import fr.free.nrw.commons.customselector.model.Image
|
||||||
|
|
@ -112,6 +112,18 @@ class CustomSelectorActivity: BaseActivity(), FolderClickListener, ImageSelectLi
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
override fun onActivityResult(requestCode: Int, resultCode: Int, data: Intent?) {
|
||||||
|
super.onActivityResult(requestCode, resultCode, data)
|
||||||
|
if (requestCode == 101) {
|
||||||
|
if (resultCode == Activity.RESULT_OK) {
|
||||||
|
val selectedImages: ArrayList<Image> =
|
||||||
|
data!!
|
||||||
|
.getParcelableArrayListExtra(CustomSelectorConstants.NEW_SELECTED_IMAGES)!!
|
||||||
|
imageFragment!!.passSelectedImages(selectedImages)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Show Custom Selector Welcome Dialog.
|
* Show Custom Selector Welcome Dialog.
|
||||||
*/
|
*/
|
||||||
|
|
@ -305,9 +317,19 @@ class CustomSelectorActivity: BaseActivity(), FolderClickListener, ImageSelectLi
|
||||||
* onLongPress
|
* onLongPress
|
||||||
* @param imageUri : uri of image
|
* @param imageUri : uri of image
|
||||||
*/
|
*/
|
||||||
override fun onLongPress(imageUri: Uri) {
|
override fun onLongPress(
|
||||||
val intent = Intent(this, ZoomableActivity::class.java).setData(imageUri);
|
position: Int,
|
||||||
startActivity(intent)
|
images: ArrayList<Image>,
|
||||||
|
selectedImages: ArrayList<Image>
|
||||||
|
) {
|
||||||
|
val intent = Intent(this, ZoomableActivity::class.java)
|
||||||
|
intent.putExtra(CustomSelectorConstants.PRESENT_POSITION, position);
|
||||||
|
intent.putParcelableArrayListExtra(CustomSelectorConstants.TOTAL_IMAGES, images)
|
||||||
|
intent.putParcelableArrayListExtra(
|
||||||
|
CustomSelectorConstants.TOTAL_SELECTED_IMAGES,
|
||||||
|
selectedImages
|
||||||
|
)
|
||||||
|
startActivityForResult(intent, 101)
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|
|
||||||
|
|
@ -19,6 +19,7 @@ import fr.free.nrw.commons.R
|
||||||
import fr.free.nrw.commons.customselector.database.NotForUploadStatusDao
|
import fr.free.nrw.commons.customselector.database.NotForUploadStatusDao
|
||||||
import fr.free.nrw.commons.customselector.database.UploadedStatusDao
|
import fr.free.nrw.commons.customselector.database.UploadedStatusDao
|
||||||
import fr.free.nrw.commons.customselector.helper.ImageHelper
|
import fr.free.nrw.commons.customselector.helper.ImageHelper
|
||||||
|
import fr.free.nrw.commons.customselector.listeners.PassDataListener
|
||||||
import fr.free.nrw.commons.customselector.helper.ImageHelper.CUSTOM_SELECTOR_PREFERENCE_KEY
|
import fr.free.nrw.commons.customselector.helper.ImageHelper.CUSTOM_SELECTOR_PREFERENCE_KEY
|
||||||
import fr.free.nrw.commons.customselector.helper.ImageHelper.SWITCH_STATE_PREFERENCE_KEY
|
import fr.free.nrw.commons.customselector.helper.ImageHelper.SWITCH_STATE_PREFERENCE_KEY
|
||||||
import fr.free.nrw.commons.customselector.listeners.ImageSelectListener
|
import fr.free.nrw.commons.customselector.listeners.ImageSelectListener
|
||||||
|
|
@ -42,7 +43,7 @@ import kotlin.collections.ArrayList
|
||||||
/**
|
/**
|
||||||
* Custom Selector Image Fragment.
|
* Custom Selector Image Fragment.
|
||||||
*/
|
*/
|
||||||
class ImageFragment: CommonsDaggerSupportFragment(), RefreshUIListener {
|
class ImageFragment: CommonsDaggerSupportFragment(), RefreshUIListener, PassDataListener {
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Current bucketId.
|
* Current bucketId.
|
||||||
|
|
@ -293,6 +294,7 @@ class ImageFragment: CommonsDaggerSupportFragment(), RefreshUIListener {
|
||||||
* notifyDataSetChanged, rebuild the holder views to account for deleted images.
|
* notifyDataSetChanged, rebuild the holder views to account for deleted images.
|
||||||
*/
|
*/
|
||||||
override fun onResume() {
|
override fun onResume() {
|
||||||
|
Log.d("haha", "onResume: ")
|
||||||
imageAdapter.notifyDataSetChanged()
|
imageAdapter.notifyDataSetChanged()
|
||||||
super.onResume()
|
super.onResume()
|
||||||
}
|
}
|
||||||
|
|
@ -327,4 +329,8 @@ class ImageFragment: CommonsDaggerSupportFragment(), RefreshUIListener {
|
||||||
override fun refresh() {
|
override fun refresh() {
|
||||||
imageAdapter.refresh(filteredImages, allImages)
|
imageAdapter.refresh(filteredImages, allImages)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
override fun passSelectedImages(selectedImages: ArrayList<Image>){
|
||||||
|
imageAdapter.setSelectedImages(selectedImages)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -13,6 +13,7 @@ import fr.free.nrw.commons.customselector.ui.selector.CustomSelectorActivity;
|
||||||
import fr.free.nrw.commons.description.DescriptionEditActivity;
|
import fr.free.nrw.commons.description.DescriptionEditActivity;
|
||||||
import fr.free.nrw.commons.explore.depictions.WikidataItemDetailsActivity;
|
import fr.free.nrw.commons.explore.depictions.WikidataItemDetailsActivity;
|
||||||
import fr.free.nrw.commons.explore.SearchActivity;
|
import fr.free.nrw.commons.explore.SearchActivity;
|
||||||
|
import fr.free.nrw.commons.media.ZoomableActivity;
|
||||||
import fr.free.nrw.commons.notification.NotificationActivity;
|
import fr.free.nrw.commons.notification.NotificationActivity;
|
||||||
import fr.free.nrw.commons.profile.ProfileActivity;
|
import fr.free.nrw.commons.profile.ProfileActivity;
|
||||||
import fr.free.nrw.commons.review.ReviewActivity;
|
import fr.free.nrw.commons.review.ReviewActivity;
|
||||||
|
|
@ -75,4 +76,7 @@ public abstract class ActivityBuilderModule {
|
||||||
|
|
||||||
@ContributesAndroidInjector
|
@ContributesAndroidInjector
|
||||||
abstract DescriptionEditActivity bindDescriptionEditActivity();
|
abstract DescriptionEditActivity bindDescriptionEditActivity();
|
||||||
|
|
||||||
|
@ContributesAndroidInjector
|
||||||
|
abstract ZoomableActivity bindZoomableActivity();
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,92 +0,0 @@
|
||||||
package fr.free.nrw.commons.media;
|
|
||||||
|
|
||||||
import android.graphics.drawable.Animatable;
|
|
||||||
import android.net.Uri;
|
|
||||||
import android.os.Bundle;
|
|
||||||
import android.view.View;
|
|
||||||
import android.widget.ProgressBar;
|
|
||||||
import androidx.annotation.Nullable;
|
|
||||||
import androidx.appcompat.app.AppCompatActivity;
|
|
||||||
import butterknife.BindView;
|
|
||||||
import butterknife.ButterKnife;
|
|
||||||
import com.facebook.drawee.backends.pipeline.Fresco;
|
|
||||||
import com.facebook.drawee.controller.BaseControllerListener;
|
|
||||||
import com.facebook.drawee.controller.ControllerListener;
|
|
||||||
import com.facebook.drawee.drawable.ProgressBarDrawable;
|
|
||||||
import com.facebook.drawee.drawable.ScalingUtils;
|
|
||||||
import com.facebook.drawee.generic.GenericDraweeHierarchy;
|
|
||||||
import com.facebook.drawee.generic.GenericDraweeHierarchyBuilder;
|
|
||||||
import com.facebook.drawee.interfaces.DraweeController;
|
|
||||||
import com.facebook.imagepipeline.image.ImageInfo;
|
|
||||||
import fr.free.nrw.commons.R;
|
|
||||||
import fr.free.nrw.commons.media.zoomControllers.zoomable.DoubleTapGestureListener;
|
|
||||||
import fr.free.nrw.commons.media.zoomControllers.zoomable.ZoomableDraweeView;
|
|
||||||
import timber.log.Timber;
|
|
||||||
|
|
||||||
|
|
||||||
public class ZoomableActivity extends AppCompatActivity {
|
|
||||||
private Uri imageUri;
|
|
||||||
|
|
||||||
@BindView(R.id.zoomable)
|
|
||||||
ZoomableDraweeView photo;
|
|
||||||
@BindView(R.id.zoom_progress_bar)
|
|
||||||
ProgressBar spinner;
|
|
||||||
|
|
||||||
@Override
|
|
||||||
protected void onCreate(Bundle savedInstanceState) {
|
|
||||||
super.onCreate(savedInstanceState);
|
|
||||||
|
|
||||||
imageUri = getIntent().getData();
|
|
||||||
if (null == imageUri) {
|
|
||||||
throw new IllegalArgumentException("No data to display");
|
|
||||||
}
|
|
||||||
Timber.d("URl = " + imageUri);
|
|
||||||
|
|
||||||
setContentView(R.layout.activity_zoomable);
|
|
||||||
ButterKnife.bind(this);
|
|
||||||
init();
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Two types of loading indicators have been added to the zoom activity:
|
|
||||||
* 1. An Indeterminate spinner for showing the time lapsed between dispatch of the image request
|
|
||||||
* and starting to receiving the image.
|
|
||||||
* 2. ProgressBarDrawable that reflects how much image has been downloaded
|
|
||||||
*/
|
|
||||||
private final ControllerListener loadingListener = new BaseControllerListener<ImageInfo>() {
|
|
||||||
@Override
|
|
||||||
public void onSubmit(String id, Object callerContext) {
|
|
||||||
// Sometimes the spinner doesn't appear when rapidly switching between images, this fixes that
|
|
||||||
spinner.setVisibility(View.VISIBLE);
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public void onIntermediateImageSet(String id, @Nullable ImageInfo imageInfo) {
|
|
||||||
spinner.setVisibility(View.GONE);
|
|
||||||
}
|
|
||||||
@Override
|
|
||||||
public void onFinalImageSet(String id, @Nullable ImageInfo imageInfo, @Nullable Animatable animatable) {
|
|
||||||
spinner.setVisibility(View.GONE);
|
|
||||||
}
|
|
||||||
};
|
|
||||||
private void init() {
|
|
||||||
if( imageUri != null ) {
|
|
||||||
GenericDraweeHierarchy hierarchy = GenericDraweeHierarchyBuilder.newInstance(getResources())
|
|
||||||
.setActualImageScaleType(ScalingUtils.ScaleType.FIT_CENTER)
|
|
||||||
.setProgressBarImage(new ProgressBarDrawable())
|
|
||||||
.setProgressBarImageScaleType(ScalingUtils.ScaleType.FIT_CENTER)
|
|
||||||
.build();
|
|
||||||
photo.setHierarchy(hierarchy);
|
|
||||||
photo.setAllowTouchInterceptionWhileZoomed(true);
|
|
||||||
photo.setIsLongpressEnabled(false);
|
|
||||||
photo.setTapListener(new DoubleTapGestureListener(photo));
|
|
||||||
DraweeController controller = Fresco.newDraweeControllerBuilder()
|
|
||||||
.setUri(imageUri)
|
|
||||||
.setControllerListener(loadingListener)
|
|
||||||
.build();
|
|
||||||
photo.setController(controller);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
}
|
|
||||||
387
app/src/main/java/fr/free/nrw/commons/media/ZoomableActivity.kt
Normal file
387
app/src/main/java/fr/free/nrw/commons/media/ZoomableActivity.kt
Normal file
|
|
@ -0,0 +1,387 @@
|
||||||
|
package fr.free.nrw.commons.media
|
||||||
|
|
||||||
|
import android.app.Activity
|
||||||
|
import android.content.Intent
|
||||||
|
import android.graphics.drawable.Animatable
|
||||||
|
import android.net.Uri
|
||||||
|
import android.os.Bundle
|
||||||
|
import android.view.View
|
||||||
|
import android.widget.ProgressBar
|
||||||
|
import android.widget.TextView
|
||||||
|
import android.widget.Toast
|
||||||
|
import butterknife.BindView
|
||||||
|
import butterknife.ButterKnife
|
||||||
|
import com.facebook.drawee.backends.pipeline.Fresco
|
||||||
|
import com.facebook.drawee.controller.BaseControllerListener
|
||||||
|
import com.facebook.drawee.controller.ControllerListener
|
||||||
|
import com.facebook.drawee.drawable.ProgressBarDrawable
|
||||||
|
import com.facebook.drawee.drawable.ScalingUtils
|
||||||
|
import com.facebook.drawee.generic.GenericDraweeHierarchyBuilder
|
||||||
|
import com.facebook.drawee.interfaces.DraweeController
|
||||||
|
import com.facebook.imagepipeline.image.ImageInfo
|
||||||
|
import fr.free.nrw.commons.R
|
||||||
|
import fr.free.nrw.commons.customselector.database.NotForUploadStatus
|
||||||
|
import fr.free.nrw.commons.customselector.database.NotForUploadStatusDao
|
||||||
|
import fr.free.nrw.commons.customselector.database.UploadedStatusDao
|
||||||
|
import fr.free.nrw.commons.customselector.helper.CustomSelectorConstants
|
||||||
|
import fr.free.nrw.commons.customselector.helper.OnSwipeTouchListener
|
||||||
|
import fr.free.nrw.commons.customselector.model.Image
|
||||||
|
import fr.free.nrw.commons.media.zoomControllers.zoomable.DoubleTapGestureListener
|
||||||
|
import fr.free.nrw.commons.media.zoomControllers.zoomable.ZoomableDraweeView
|
||||||
|
import fr.free.nrw.commons.theme.BaseActivity
|
||||||
|
import fr.free.nrw.commons.upload.FileProcessor
|
||||||
|
import fr.free.nrw.commons.upload.FileUtilsWrapper
|
||||||
|
import fr.free.nrw.commons.utils.CustomSelectorUtils
|
||||||
|
import kotlinx.coroutines.*
|
||||||
|
import timber.log.Timber
|
||||||
|
import javax.inject.Inject
|
||||||
|
|
||||||
|
class ZoomableActivity : BaseActivity() {
|
||||||
|
|
||||||
|
private lateinit var imageUri: Uri
|
||||||
|
|
||||||
|
@JvmField
|
||||||
|
@BindView(R.id.zoomable)
|
||||||
|
var photo: ZoomableDraweeView? = null
|
||||||
|
|
||||||
|
@JvmField
|
||||||
|
@BindView(R.id.zoom_progress_bar)
|
||||||
|
var spinner: ProgressBar? = null
|
||||||
|
|
||||||
|
@JvmField
|
||||||
|
@BindView(R.id.selection_count)
|
||||||
|
var selectedCount: TextView? = null
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Total images present in folder
|
||||||
|
*/
|
||||||
|
private var images: ArrayList<Image>? = null
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Total selected images present in folder
|
||||||
|
*/
|
||||||
|
private var selectedImages: ArrayList<Image>? = null
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Present position of the image
|
||||||
|
*/
|
||||||
|
private var position = 0
|
||||||
|
|
||||||
|
/**
|
||||||
|
* FileUtilsWrapper class to get imageSHA1 from uri
|
||||||
|
*/
|
||||||
|
@Inject
|
||||||
|
lateinit var fileUtilsWrapper: FileUtilsWrapper
|
||||||
|
|
||||||
|
/**
|
||||||
|
* FileProcessor to pre-process the file.
|
||||||
|
*/
|
||||||
|
@Inject
|
||||||
|
lateinit var fileProcessor: FileProcessor
|
||||||
|
|
||||||
|
/**
|
||||||
|
* NotForUploadStatus Dao class for database operations
|
||||||
|
*/
|
||||||
|
@Inject
|
||||||
|
lateinit var notForUploadStatusDao: NotForUploadStatusDao
|
||||||
|
|
||||||
|
/**
|
||||||
|
* UploadedStatus Dao class for database operations
|
||||||
|
*/
|
||||||
|
@Inject
|
||||||
|
lateinit var uploadedStatusDao: UploadedStatusDao
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Coroutine Dispatchers and Scope.
|
||||||
|
*/
|
||||||
|
private var defaultDispatcher : CoroutineDispatcher = Dispatchers.Default
|
||||||
|
private var ioDispatcher : CoroutineDispatcher = Dispatchers.IO
|
||||||
|
private val scope : CoroutineScope = MainScope()
|
||||||
|
|
||||||
|
override fun onCreate(savedInstanceState: Bundle?) {
|
||||||
|
super.onCreate(savedInstanceState)
|
||||||
|
images = intent.getParcelableArrayListExtra(
|
||||||
|
CustomSelectorConstants.TOTAL_IMAGES
|
||||||
|
)
|
||||||
|
selectedImages = intent.getParcelableArrayListExtra(
|
||||||
|
CustomSelectorConstants.TOTAL_SELECTED_IMAGES
|
||||||
|
)
|
||||||
|
position = intent.getIntExtra(CustomSelectorConstants.PRESENT_POSITION, 0)
|
||||||
|
imageUri = if (images.isNullOrEmpty()) {
|
||||||
|
intent.data as Uri
|
||||||
|
} else {
|
||||||
|
images!![position].uri
|
||||||
|
}
|
||||||
|
Timber.d("URl = $imageUri")
|
||||||
|
setContentView(R.layout.activity_zoomable)
|
||||||
|
ButterKnife.bind(this)
|
||||||
|
init(imageUri)
|
||||||
|
onSwap()
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Handle swap gestures. Ex. onSwipeLeft, onSwipeRight, onSwipeUp, onSwipeDown
|
||||||
|
*/
|
||||||
|
private fun onSwap() {
|
||||||
|
if (!images.isNullOrEmpty()) {
|
||||||
|
photo!!.setOnTouchListener(object : OnSwipeTouchListener(this) {
|
||||||
|
override fun onSwipeLeft() {
|
||||||
|
super.onSwipeLeft()
|
||||||
|
if (position < images!!.size - 1) {
|
||||||
|
position++
|
||||||
|
init(images!![position].uri)
|
||||||
|
} else {
|
||||||
|
Toast.makeText(
|
||||||
|
this@ZoomableActivity,
|
||||||
|
"No more images found",
|
||||||
|
Toast.LENGTH_SHORT
|
||||||
|
).show()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onSwipeRight() {
|
||||||
|
super.onSwipeRight()
|
||||||
|
if (position > 0) {
|
||||||
|
position--
|
||||||
|
init(images!![position].uri)
|
||||||
|
} else {
|
||||||
|
Toast.makeText(
|
||||||
|
this@ZoomableActivity,
|
||||||
|
"No more images found",
|
||||||
|
Toast.LENGTH_SHORT
|
||||||
|
).show()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onSwipeUp() {
|
||||||
|
super.onSwipeUp()
|
||||||
|
scope.launch {
|
||||||
|
val imageSHA1 = CustomSelectorUtils.getImageSHA1(
|
||||||
|
images!![position].uri,
|
||||||
|
ioDispatcher,
|
||||||
|
fileUtilsWrapper,
|
||||||
|
contentResolver
|
||||||
|
)
|
||||||
|
var isNonActionable = notForUploadStatusDao.find(imageSHA1)
|
||||||
|
if (isNonActionable > 0) {
|
||||||
|
Toast.makeText(
|
||||||
|
this@ZoomableActivity,
|
||||||
|
"Can't select this image for upload", Toast.LENGTH_SHORT
|
||||||
|
).show()
|
||||||
|
} else {
|
||||||
|
isNonActionable =
|
||||||
|
uploadedStatusDao.findByImageSHA1(imageSHA1, true)
|
||||||
|
if (isNonActionable > 0) {
|
||||||
|
Toast.makeText(
|
||||||
|
this@ZoomableActivity,
|
||||||
|
"Can't select this image for upload", Toast.LENGTH_SHORT
|
||||||
|
).show()
|
||||||
|
} else {
|
||||||
|
val imageModifiedSHA1 = CustomSelectorUtils.generateModifiedSHA1(
|
||||||
|
images!![position],
|
||||||
|
defaultDispatcher,
|
||||||
|
this@ZoomableActivity,
|
||||||
|
fileProcessor,
|
||||||
|
fileUtilsWrapper
|
||||||
|
)
|
||||||
|
isNonActionable = uploadedStatusDao.findByModifiedImageSHA1(
|
||||||
|
imageModifiedSHA1,
|
||||||
|
true
|
||||||
|
)
|
||||||
|
if (isNonActionable > 0) {
|
||||||
|
Toast.makeText(
|
||||||
|
this@ZoomableActivity,
|
||||||
|
"Can't select this image for upload",
|
||||||
|
Toast.LENGTH_SHORT
|
||||||
|
).show()
|
||||||
|
} else {
|
||||||
|
if (!selectedImages!!.contains(images!![position])) {
|
||||||
|
selectedImages!!.add(images!![position])
|
||||||
|
}
|
||||||
|
position = getNextActionableImage(position + 1)
|
||||||
|
init(images!![position].uri)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onSwipeDown() {
|
||||||
|
super.onSwipeDown()
|
||||||
|
scope.launch {
|
||||||
|
insertInNotForUpload(images!![position])
|
||||||
|
if (position < images!!.size - 1) {
|
||||||
|
position++
|
||||||
|
init(images!![position].uri)
|
||||||
|
} else {
|
||||||
|
Toast.makeText(
|
||||||
|
this@ZoomableActivity,
|
||||||
|
"No more images found",
|
||||||
|
Toast.LENGTH_SHORT
|
||||||
|
).show()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Gets next actionable image
|
||||||
|
*/
|
||||||
|
private suspend fun getNextActionableImage(index: Int): Int {
|
||||||
|
var nextPosition = position
|
||||||
|
for(i in index until images!!.size){
|
||||||
|
nextPosition = i
|
||||||
|
val imageSHA1 = CustomSelectorUtils.getImageSHA1(
|
||||||
|
images!![i].uri,
|
||||||
|
ioDispatcher,
|
||||||
|
fileUtilsWrapper,
|
||||||
|
contentResolver
|
||||||
|
)
|
||||||
|
var isNonActionable = notForUploadStatusDao.find(imageSHA1)
|
||||||
|
if (isNonActionable <= 0) {
|
||||||
|
isNonActionable = uploadedStatusDao.findByImageSHA1(imageSHA1, true)
|
||||||
|
if (isNonActionable <= 0) {
|
||||||
|
val imageModifiedSHA1 = CustomSelectorUtils.generateModifiedSHA1(
|
||||||
|
images!![i],
|
||||||
|
defaultDispatcher,
|
||||||
|
this@ZoomableActivity,
|
||||||
|
fileProcessor,
|
||||||
|
fileUtilsWrapper
|
||||||
|
)
|
||||||
|
isNonActionable = uploadedStatusDao.findByModifiedImageSHA1(
|
||||||
|
imageModifiedSHA1,
|
||||||
|
true
|
||||||
|
)
|
||||||
|
if (isNonActionable <= 0) {
|
||||||
|
return i
|
||||||
|
} else {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return nextPosition
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Unselect item UI
|
||||||
|
*/
|
||||||
|
private fun itemUnselected() {
|
||||||
|
selectedCount!!.visibility = View.INVISIBLE
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Select item UI
|
||||||
|
*/
|
||||||
|
private fun itemSelected(i: Int) {
|
||||||
|
selectedCount!!.visibility = View.VISIBLE
|
||||||
|
selectedCount!!.text = i.toString()
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get index from list
|
||||||
|
*/
|
||||||
|
private fun getIndex(list: ArrayList<Image>?, image: Image): Int {
|
||||||
|
return list!!.indexOf(image)
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Two types of loading indicators have been added to the zoom activity:
|
||||||
|
* 1. An Indeterminate spinner for showing the time lapsed between dispatch of the image request
|
||||||
|
* and starting to receiving the image.
|
||||||
|
* 2. ProgressBarDrawable that reflects how much image has been downloaded
|
||||||
|
*/
|
||||||
|
private val loadingListener: ControllerListener<ImageInfo?> =
|
||||||
|
object : BaseControllerListener<ImageInfo?>() {
|
||||||
|
override fun onSubmit(id: String, callerContext: Any) {
|
||||||
|
// Sometimes the spinner doesn't appear when rapidly switching between images, this fixes that
|
||||||
|
spinner!!.visibility = View.VISIBLE
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onIntermediateImageSet(id: String, imageInfo: ImageInfo?) {
|
||||||
|
spinner!!.visibility = View.GONE
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onFinalImageSet(
|
||||||
|
id: String,
|
||||||
|
imageInfo: ImageInfo?,
|
||||||
|
animatable: Animatable?
|
||||||
|
) {
|
||||||
|
spinner!!.visibility = View.GONE
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun init(imageUri: Uri?) {
|
||||||
|
if (imageUri != null) {
|
||||||
|
val hierarchy = GenericDraweeHierarchyBuilder.newInstance(resources)
|
||||||
|
.setActualImageScaleType(ScalingUtils.ScaleType.FIT_CENTER)
|
||||||
|
.setProgressBarImage(ProgressBarDrawable())
|
||||||
|
.setProgressBarImageScaleType(ScalingUtils.ScaleType.FIT_CENTER)
|
||||||
|
.build()
|
||||||
|
photo!!.hierarchy = hierarchy
|
||||||
|
photo!!.setAllowTouchInterceptionWhileZoomed(true)
|
||||||
|
photo!!.setIsLongpressEnabled(false)
|
||||||
|
photo!!.setTapListener(DoubleTapGestureListener(photo))
|
||||||
|
val controller: DraweeController = Fresco.newDraweeControllerBuilder()
|
||||||
|
.setUri(imageUri)
|
||||||
|
.setControllerListener(loadingListener)
|
||||||
|
.build()
|
||||||
|
photo!!.controller = controller
|
||||||
|
|
||||||
|
if (!images.isNullOrEmpty()) {
|
||||||
|
val selectedIndex = getIndex(selectedImages, images!![position])
|
||||||
|
val isSelected = selectedIndex != -1
|
||||||
|
if (isSelected) {
|
||||||
|
itemSelected(selectedIndex + 1)
|
||||||
|
} else {
|
||||||
|
itemUnselected()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Inserts an image in Not For Upload Database
|
||||||
|
*/
|
||||||
|
private suspend fun insertInNotForUpload(it: Image) {
|
||||||
|
val imageSHA1 = CustomSelectorUtils.getImageSHA1(
|
||||||
|
it.uri,
|
||||||
|
ioDispatcher,
|
||||||
|
fileUtilsWrapper,
|
||||||
|
contentResolver
|
||||||
|
)
|
||||||
|
notForUploadStatusDao.insert(
|
||||||
|
NotForUploadStatus(
|
||||||
|
imageSHA1,
|
||||||
|
true
|
||||||
|
)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Send selected images in fragment
|
||||||
|
*/
|
||||||
|
override fun onBackPressed() {
|
||||||
|
if (!images.isNullOrEmpty()) {
|
||||||
|
val returnIntent = Intent()
|
||||||
|
returnIntent.putParcelableArrayListExtra(
|
||||||
|
CustomSelectorConstants.NEW_SELECTED_IMAGES,
|
||||||
|
selectedImages
|
||||||
|
)
|
||||||
|
setResult(Activity.RESULT_OK, returnIntent)
|
||||||
|
finish()
|
||||||
|
}
|
||||||
|
super.onBackPressed()
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onDestroy() {
|
||||||
|
scope.cancel()
|
||||||
|
super.onDestroy()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -5,6 +5,7 @@ import android.content.Context
|
||||||
import android.net.Uri
|
import android.net.Uri
|
||||||
import androidx.exifinterface.media.ExifInterface
|
import androidx.exifinterface.media.ExifInterface
|
||||||
import fr.free.nrw.commons.customselector.model.Image
|
import fr.free.nrw.commons.customselector.model.Image
|
||||||
|
import fr.free.nrw.commons.filepicker.PickedFiles
|
||||||
import fr.free.nrw.commons.customselector.ui.selector.ImageLoader
|
import fr.free.nrw.commons.customselector.ui.selector.ImageLoader
|
||||||
import fr.free.nrw.commons.filepicker.PickedFiles
|
import fr.free.nrw.commons.filepicker.PickedFiles
|
||||||
import fr.free.nrw.commons.media.MediaClient
|
import fr.free.nrw.commons.media.MediaClient
|
||||||
|
|
@ -26,17 +27,18 @@ class CustomSelectorUtils {
|
||||||
/**
|
/**
|
||||||
* Get image sha1 from uri, used to retrieve the original image sha1.
|
* Get image sha1 from uri, used to retrieve the original image sha1.
|
||||||
*/
|
*/
|
||||||
suspend fun getImageSHA1(uri: Uri,
|
suspend fun getImageSHA1(
|
||||||
ioDispatcher : CoroutineDispatcher,
|
uri: Uri,
|
||||||
fileUtilsWrapper: FileUtilsWrapper,
|
ioDispatcher: CoroutineDispatcher,
|
||||||
contentResolver: ContentResolver
|
fileUtilsWrapper: FileUtilsWrapper,
|
||||||
|
contentResolver: ContentResolver
|
||||||
): String {
|
): String {
|
||||||
return withContext(ioDispatcher) {
|
return withContext(ioDispatcher) {
|
||||||
|
|
||||||
try {
|
try {
|
||||||
val result = fileUtilsWrapper.getSHA1(contentResolver.openInputStream(uri))
|
val result = fileUtilsWrapper.getSHA1(contentResolver.openInputStream(uri))
|
||||||
result
|
result
|
||||||
} catch (e: FileNotFoundException){
|
} catch (e: FileNotFoundException) {
|
||||||
e.printStackTrace()
|
e.printStackTrace()
|
||||||
""
|
""
|
||||||
}
|
}
|
||||||
|
|
@ -44,16 +46,15 @@ class CustomSelectorUtils {
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Generate Modified SHA1 using present Exif settings.
|
* Generates modified SHA1 of an image
|
||||||
*
|
|
||||||
* @return modified sha1
|
|
||||||
*/
|
*/
|
||||||
suspend fun generateModifiedSHA1(image: Image,
|
suspend fun generateModifiedSHA1(
|
||||||
defaultDispatcher : CoroutineDispatcher,
|
image: Image,
|
||||||
context: Context,
|
defaultDispatcher: CoroutineDispatcher,
|
||||||
fileProcessor: FileProcessor,
|
context: Context,
|
||||||
fileUtilsWrapper: FileUtilsWrapper
|
fileProcessor: FileProcessor,
|
||||||
) : String {
|
fileUtilsWrapper: FileUtilsWrapper
|
||||||
|
): String {
|
||||||
return withContext(defaultDispatcher) {
|
return withContext(defaultDispatcher) {
|
||||||
val uploadableFile = PickedFiles.pickedExistingPicture(context, image.uri)
|
val uploadableFile = PickedFiles.pickedExistingPicture(context, image.uri)
|
||||||
val exifInterface: ExifInterface? = try {
|
val exifInterface: ExifInterface? = try {
|
||||||
|
|
@ -64,7 +65,9 @@ class CustomSelectorUtils {
|
||||||
}
|
}
|
||||||
fileProcessor.redactExifTags(exifInterface, fileProcessor.getExifTagsToRedact())
|
fileProcessor.redactExifTags(exifInterface, fileProcessor.getExifTagsToRedact())
|
||||||
val sha1 =
|
val sha1 =
|
||||||
fileUtilsWrapper.getSHA1(fileUtilsWrapper.getFileInputStream(uploadableFile.filePath))
|
fileUtilsWrapper.getSHA1(
|
||||||
|
fileUtilsWrapper.getFileInputStream(uploadableFile.filePath)
|
||||||
|
)
|
||||||
uploadableFile.file.delete()
|
uploadableFile.file.delete()
|
||||||
sha1
|
sha1
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -23,4 +23,21 @@
|
||||||
app:layout_constraintTop_toTopOf="parent"
|
app:layout_constraintTop_toTopOf="parent"
|
||||||
/>
|
/>
|
||||||
|
|
||||||
|
<TextView
|
||||||
|
android:id="@+id/selection_count"
|
||||||
|
android:layout_width="@dimen/dimen_20"
|
||||||
|
android:layout_height="@dimen/dimen_20"
|
||||||
|
app:layout_constraintDimensionRatio="H,1:1"
|
||||||
|
android:textSize="11sp"
|
||||||
|
android:textStyle="bold"
|
||||||
|
android:textColor="@color/black"
|
||||||
|
android:layout_margin="@dimen/dimen_6"
|
||||||
|
android:gravity="center|center_vertical"
|
||||||
|
style="@style/TextAppearance.AppCompat.Small"
|
||||||
|
android:text="12"
|
||||||
|
android:visibility="gone"
|
||||||
|
android:background="@drawable/circle_shape"
|
||||||
|
app:layout_constraintStart_toStartOf="parent"
|
||||||
|
app:layout_constraintTop_toTopOf="parent"/>
|
||||||
|
|
||||||
</androidx.constraintlayout.widget.ConstraintLayout>
|
</androidx.constraintlayout.widget.ConstraintLayout>
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue