mirror of
https://github.com/commons-app/apps-android-commons.git
synced 2025-10-26 20:33:53 +01:00
[WIP]Lossless Transformations(Rotate feature) (#5252)
* UI setup for the crop feature almost setup * basic setup of rotate feature done * Added basic changes for editing feature * Getting data back from edit activity * Getting data back from edit activity * Updated contentUri * Finally the rotated image is getting uploaded * Minor Improvements for better testing * Fixed thumbnail preview * Fixed loss of exif data * Copy exif data * Save exif data * Added java docs * Minor fix * Added Javadoc * Refactoring * Formatting fixes * Minor Formatting Fix * Fix unit test * Add test coverage * Formatting fixes * Formatting Fixes --------- Co-authored-by: Priyank Shankar <priyankshankar@changejar.in>
This commit is contained in:
parent
6b8954b4a9
commit
2ddb6b2e5e
17 changed files with 570 additions and 28 deletions
|
|
@ -140,6 +140,8 @@ dependencies {
|
||||||
implementation "androidx.preference:preference:$PREFERENCE_VERSION"
|
implementation "androidx.preference:preference:$PREFERENCE_VERSION"
|
||||||
// Kotlin
|
// Kotlin
|
||||||
implementation "androidx.preference:preference-ktx:$PREFERENCE_VERSION"
|
implementation "androidx.preference:preference-ktx:$PREFERENCE_VERSION"
|
||||||
|
//Android Media
|
||||||
|
implementation 'com.github.juanitobananas:AndroidMediaUtil:v1.0-1'
|
||||||
|
|
||||||
implementation "androidx.multidex:multidex:$MULTIDEX_VERSION"
|
implementation "androidx.multidex:multidex:$MULTIDEX_VERSION"
|
||||||
|
|
||||||
|
|
@ -236,8 +238,8 @@ android {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
debug {
|
debug {
|
||||||
minifyEnabled false
|
|
||||||
testCoverageEnabled true
|
testCoverageEnabled true
|
||||||
|
minifyEnabled false
|
||||||
proguardFiles getDefaultProguardFile('proguard-android.txt'), 'proguard-rules.txt'
|
proguardFiles getDefaultProguardFile('proguard-android.txt'), 'proguard-rules.txt'
|
||||||
testProguardFile 'test-proguard-rules.txt'
|
testProguardFile 'test-proguard-rules.txt'
|
||||||
versionNameSuffix "-debug-" + getBranchName()
|
versionNameSuffix "-debug-" + getBranchName()
|
||||||
|
|
|
||||||
|
|
@ -48,9 +48,14 @@
|
||||||
tools:ignore="GoogleAppIndexingWarning">
|
tools:ignore="GoogleAppIndexingWarning">
|
||||||
|
|
||||||
<activity
|
<activity
|
||||||
|
android:theme="@style/EditActivityTheme"
|
||||||
android:name=".description.DescriptionEditActivity"
|
android:name=".description.DescriptionEditActivity"
|
||||||
android:exported="true" />
|
android:exported="true" />
|
||||||
|
|
||||||
|
<activity
|
||||||
|
android:name=".edit.EditActivity"
|
||||||
|
android:exported="false" />
|
||||||
|
|
||||||
<activity android:name="org.acra.dialog.CrashReportDialog"
|
<activity android:name="org.acra.dialog.CrashReportDialog"
|
||||||
android:process=":acra"
|
android:process=":acra"
|
||||||
android:launchMode="singleInstance"
|
android:launchMode="singleInstance"
|
||||||
|
|
|
||||||
249
app/src/main/java/fr/free/nrw/commons/edit/EditActivity.kt
Normal file
249
app/src/main/java/fr/free/nrw/commons/edit/EditActivity.kt
Normal file
|
|
@ -0,0 +1,249 @@
|
||||||
|
package fr.free.nrw.commons.edit
|
||||||
|
|
||||||
|
import android.animation.Animator
|
||||||
|
import android.animation.Animator.AnimatorListener
|
||||||
|
import android.animation.ValueAnimator
|
||||||
|
import android.content.Intent
|
||||||
|
import android.graphics.BitmapFactory
|
||||||
|
import android.graphics.Matrix
|
||||||
|
import android.graphics.drawable.BitmapDrawable
|
||||||
|
import android.media.ExifInterface
|
||||||
|
import android.os.Bundle
|
||||||
|
import android.util.Log
|
||||||
|
import android.view.animation.AccelerateDecelerateInterpolator
|
||||||
|
import android.widget.ImageView
|
||||||
|
import android.widget.Toast
|
||||||
|
import androidx.appcompat.app.AppCompatActivity
|
||||||
|
import androidx.core.graphics.rotationMatrix
|
||||||
|
import androidx.core.graphics.scaleMatrix
|
||||||
|
import androidx.core.net.toUri
|
||||||
|
import androidx.lifecycle.ViewModelProvider
|
||||||
|
import fr.free.nrw.commons.R
|
||||||
|
import kotlinx.android.synthetic.main.activity_edit.btn_save
|
||||||
|
import kotlinx.android.synthetic.main.activity_edit.iv
|
||||||
|
import kotlinx.android.synthetic.main.activity_edit.rotate_btn
|
||||||
|
import timber.log.Timber
|
||||||
|
import java.io.File
|
||||||
|
|
||||||
|
/**
|
||||||
|
* An activity class for editing and rotating images using LLJTran with EXIF attribute preservation.
|
||||||
|
*
|
||||||
|
* This activity allows loads an image, allows users to rotate it by 90-degree increments, and
|
||||||
|
* save the edited image while preserving its EXIF attributes. The class includes methods
|
||||||
|
* for initializing the UI, animating image rotations, copying EXIF data, and handling
|
||||||
|
* the image-saving process.
|
||||||
|
*/
|
||||||
|
class EditActivity : AppCompatActivity() {
|
||||||
|
private var imageUri = ""
|
||||||
|
private lateinit var vm: EditViewModel
|
||||||
|
private val sourceExifAttributeList = mutableListOf<Pair<String, String?>>()
|
||||||
|
|
||||||
|
override fun onCreate(savedInstanceState: Bundle?) {
|
||||||
|
super.onCreate(savedInstanceState)
|
||||||
|
setContentView(R.layout.activity_edit)
|
||||||
|
supportActionBar?.title = ""
|
||||||
|
val intent = intent
|
||||||
|
imageUri = intent.getStringExtra("image") ?: ""
|
||||||
|
vm = ViewModelProvider(this).get(EditViewModel::class.java)
|
||||||
|
val sourceExif = imageUri.toUri().path?.let { ExifInterface(it) }
|
||||||
|
val exifTags = arrayOf(
|
||||||
|
ExifInterface.TAG_APERTURE,
|
||||||
|
ExifInterface.TAG_DATETIME,
|
||||||
|
ExifInterface.TAG_EXPOSURE_TIME,
|
||||||
|
ExifInterface.TAG_FLASH,
|
||||||
|
ExifInterface.TAG_FOCAL_LENGTH,
|
||||||
|
ExifInterface.TAG_GPS_ALTITUDE,
|
||||||
|
ExifInterface.TAG_GPS_ALTITUDE_REF,
|
||||||
|
ExifInterface.TAG_GPS_DATESTAMP,
|
||||||
|
ExifInterface.TAG_GPS_LATITUDE,
|
||||||
|
ExifInterface.TAG_GPS_LATITUDE_REF,
|
||||||
|
ExifInterface.TAG_GPS_LONGITUDE,
|
||||||
|
ExifInterface.TAG_GPS_LONGITUDE_REF,
|
||||||
|
ExifInterface.TAG_GPS_PROCESSING_METHOD,
|
||||||
|
ExifInterface.TAG_GPS_TIMESTAMP,
|
||||||
|
ExifInterface.TAG_IMAGE_LENGTH,
|
||||||
|
ExifInterface.TAG_IMAGE_WIDTH,
|
||||||
|
ExifInterface.TAG_ISO,
|
||||||
|
ExifInterface.TAG_MAKE,
|
||||||
|
ExifInterface.TAG_MODEL,
|
||||||
|
ExifInterface.TAG_ORIENTATION,
|
||||||
|
ExifInterface.TAG_WHITE_BALANCE,
|
||||||
|
ExifInterface.WHITEBALANCE_AUTO,
|
||||||
|
ExifInterface.WHITEBALANCE_MANUAL
|
||||||
|
)
|
||||||
|
for (tag in exifTags) {
|
||||||
|
val attribute = sourceExif?.getAttribute(tag.toString())
|
||||||
|
sourceExifAttributeList.add(Pair(tag.toString(), attribute))
|
||||||
|
}
|
||||||
|
|
||||||
|
init()
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Initializes the ImageView and associated UI elements.
|
||||||
|
*
|
||||||
|
* This function sets up the ImageView for displaying an image, adjusts its view bounds,
|
||||||
|
* and scales the initial image to fit within the ImageView. It also sets click listeners
|
||||||
|
* for the "Rotate" and "Save" buttons.
|
||||||
|
*/
|
||||||
|
private fun init() {
|
||||||
|
iv.adjustViewBounds = true
|
||||||
|
iv.scaleType = ImageView.ScaleType.MATRIX
|
||||||
|
iv.post(Runnable {
|
||||||
|
val bitmap = BitmapFactory.decodeFile(imageUri)
|
||||||
|
iv.setImageBitmap(bitmap)
|
||||||
|
if (bitmap.width > 0) {
|
||||||
|
val scale =
|
||||||
|
iv.measuredWidth.toFloat() / (iv.drawable as BitmapDrawable).bitmap.width.toFloat()
|
||||||
|
iv.layoutParams.height =
|
||||||
|
(scale * (iv.drawable as BitmapDrawable).bitmap.height).toInt()
|
||||||
|
iv.imageMatrix = scaleMatrix(scale, scale)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
rotate_btn.setOnClickListener {
|
||||||
|
animateImageHeight()
|
||||||
|
}
|
||||||
|
btn_save.setOnClickListener {
|
||||||
|
getRotatedImage()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
var imageRotation = 0
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Animates the height, rotation, and scale of an ImageView to provide a smooth
|
||||||
|
* transition effect when rotating an image by 90 degrees.
|
||||||
|
*
|
||||||
|
* This function calculates the new height, rotation, and scale for the ImageView
|
||||||
|
* based on the current image rotation angle and animates the changes using a
|
||||||
|
* ValueAnimator. It also disables a rotate button during the animation to prevent
|
||||||
|
* further rotation actions.
|
||||||
|
*/
|
||||||
|
private fun animateImageHeight() {
|
||||||
|
val drawableWidth: Float = iv.getDrawable().getIntrinsicWidth().toFloat()
|
||||||
|
val drawableHeight: Float = iv.getDrawable().getIntrinsicHeight().toFloat()
|
||||||
|
val viewWidth: Float = iv.getMeasuredWidth().toFloat()
|
||||||
|
val viewHeight: Float = iv.getMeasuredHeight().toFloat()
|
||||||
|
val rotation = imageRotation % 360
|
||||||
|
val newRotation = rotation + 90
|
||||||
|
|
||||||
|
val newViewHeight: Int
|
||||||
|
val imageScale: Float
|
||||||
|
val newImageScale: Float
|
||||||
|
|
||||||
|
Timber.d("Rotation $rotation")
|
||||||
|
Timber.d("new Rotation $newRotation")
|
||||||
|
|
||||||
|
|
||||||
|
if (rotation == 0 || rotation == 180) {
|
||||||
|
imageScale = viewWidth / drawableWidth
|
||||||
|
newImageScale = viewWidth / drawableHeight
|
||||||
|
newViewHeight = (drawableWidth * newImageScale).toInt()
|
||||||
|
} else if (rotation == 90 || rotation == 270) {
|
||||||
|
imageScale = viewWidth / drawableHeight
|
||||||
|
newImageScale = viewWidth / drawableWidth
|
||||||
|
newViewHeight = (drawableHeight * newImageScale).toInt()
|
||||||
|
} else {
|
||||||
|
throw UnsupportedOperationException("rotation can 0, 90, 180 or 270. \${rotation} is unsupported")
|
||||||
|
}
|
||||||
|
|
||||||
|
val animator = ValueAnimator.ofFloat(0f, 1f).setDuration(1000L)
|
||||||
|
|
||||||
|
animator.interpolator = AccelerateDecelerateInterpolator()
|
||||||
|
|
||||||
|
animator.addListener(object : AnimatorListener {
|
||||||
|
override fun onAnimationStart(animation: Animator) {
|
||||||
|
rotate_btn.setEnabled(false)
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onAnimationEnd(animation: Animator) {
|
||||||
|
imageRotation = newRotation % 360
|
||||||
|
rotate_btn.setEnabled(true)
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onAnimationCancel(animation: Animator) {
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onAnimationRepeat(animation: Animator) {
|
||||||
|
}
|
||||||
|
|
||||||
|
})
|
||||||
|
|
||||||
|
animator.addUpdateListener { animation ->
|
||||||
|
val animVal = animation.animatedValue as Float
|
||||||
|
val complementaryAnimVal = 1 - animVal
|
||||||
|
val animatedHeight =
|
||||||
|
(complementaryAnimVal * viewHeight + animVal * newViewHeight).toInt()
|
||||||
|
val animatedScale = complementaryAnimVal * imageScale + animVal * newImageScale
|
||||||
|
val animatedRotation = complementaryAnimVal * rotation + animVal * newRotation
|
||||||
|
iv.getLayoutParams().height = animatedHeight
|
||||||
|
val matrix: Matrix = rotationMatrix(
|
||||||
|
animatedRotation,
|
||||||
|
drawableWidth / 2,
|
||||||
|
drawableHeight / 2
|
||||||
|
)
|
||||||
|
matrix.postScale(
|
||||||
|
animatedScale,
|
||||||
|
animatedScale,
|
||||||
|
drawableWidth / 2,
|
||||||
|
drawableHeight / 2
|
||||||
|
)
|
||||||
|
matrix.postTranslate(
|
||||||
|
-(drawableWidth - iv.getMeasuredWidth()) / 2,
|
||||||
|
-(drawableHeight - iv.getMeasuredHeight()) / 2
|
||||||
|
)
|
||||||
|
iv.setImageMatrix(matrix)
|
||||||
|
iv.requestLayout()
|
||||||
|
}
|
||||||
|
|
||||||
|
animator.start()
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Rotates and edits the current image, copies EXIF data, and returns the edited image path.
|
||||||
|
*
|
||||||
|
* This function retrieves the path of the current image specified by `imageUri`,
|
||||||
|
* rotates it based on the `imageRotation` angle using the `rotateImage` method
|
||||||
|
* from the `vm`, and updates the EXIF attributes of the
|
||||||
|
* rotated image based on the `sourceExifAttributeList`. It then copies the EXIF data
|
||||||
|
* using the `copyExifData` method, creates an Intent to return the edited image's file path
|
||||||
|
* as a result, and finishes the current activity.
|
||||||
|
*/
|
||||||
|
fun getRotatedImage() {
|
||||||
|
|
||||||
|
val filePath = imageUri.toUri().path
|
||||||
|
val file = filePath?.let { File(it) }
|
||||||
|
|
||||||
|
|
||||||
|
val rotatedImage = file?.let { vm.rotateImage(imageRotation, it) }
|
||||||
|
if (rotatedImage == null) {
|
||||||
|
Toast.makeText(this, "Failed to rotate to image", Toast.LENGTH_LONG).show()
|
||||||
|
}
|
||||||
|
val editedImageExif = rotatedImage?.path?.let { ExifInterface(it) }
|
||||||
|
copyExifData(editedImageExif)
|
||||||
|
val resultIntent = Intent()
|
||||||
|
resultIntent.putExtra("editedImageFilePath", rotatedImage?.toUri()?.path ?: "Error");
|
||||||
|
setResult(RESULT_OK, resultIntent);
|
||||||
|
finish();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Copies EXIF data from sourceExifAttributeList to the provided ExifInterface object.
|
||||||
|
*
|
||||||
|
* This function iterates over the `sourceExifAttributeList` and sets the EXIF attributes
|
||||||
|
* on the provided `editedImageExif` object.
|
||||||
|
*
|
||||||
|
* @param editedImageExif The ExifInterface object for the edited image.
|
||||||
|
*/
|
||||||
|
private fun copyExifData(editedImageExif: ExifInterface?) {
|
||||||
|
|
||||||
|
for (attr in sourceExifAttributeList) {
|
||||||
|
Log.d("Tag is ${attr.first}", "Value is ${attr.second}")
|
||||||
|
editedImageExif!!.setAttribute(attr.first, attr.second)
|
||||||
|
Log.d("Tag is ${attr.first}", "Value is ${attr.second}")
|
||||||
|
}
|
||||||
|
|
||||||
|
editedImageExif?.saveAttributes()
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
27
app/src/main/java/fr/free/nrw/commons/edit/EditViewModel.kt
Normal file
27
app/src/main/java/fr/free/nrw/commons/edit/EditViewModel.kt
Normal file
|
|
@ -0,0 +1,27 @@
|
||||||
|
package fr.free.nrw.commons.edit
|
||||||
|
|
||||||
|
import androidx.lifecycle.ViewModel
|
||||||
|
import java.io.File
|
||||||
|
|
||||||
|
/**
|
||||||
|
* ViewModel for image editing operations.
|
||||||
|
*
|
||||||
|
* This ViewModel class is responsible for managing image editing operations, such as
|
||||||
|
* rotating images. It utilizes a TransformImage implementation to perform image transformations.
|
||||||
|
*/
|
||||||
|
class EditViewModel() : ViewModel() {
|
||||||
|
|
||||||
|
// Ideally should be injected using DI
|
||||||
|
private val transformImage: TransformImage = TransformImageImpl()
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Rotates the specified image file by the given degree.
|
||||||
|
*
|
||||||
|
* @param degree The degree by which to rotate the image.
|
||||||
|
* @param imageFile The File representing the image to be rotated.
|
||||||
|
* @return The rotated image File, or null if the rotation operation fails.
|
||||||
|
*/
|
||||||
|
fun rotateImage(degree: Int, imageFile: File): File? {
|
||||||
|
return transformImage.rotateImage(imageFile, degree)
|
||||||
|
}
|
||||||
|
}
|
||||||
21
app/src/main/java/fr/free/nrw/commons/edit/TransformImage.kt
Normal file
21
app/src/main/java/fr/free/nrw/commons/edit/TransformImage.kt
Normal file
|
|
@ -0,0 +1,21 @@
|
||||||
|
package fr.free.nrw.commons.edit
|
||||||
|
|
||||||
|
import java.io.File
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Interface for image transformation operations.
|
||||||
|
*
|
||||||
|
* This interface defines a contract for image transformation operations, allowing
|
||||||
|
* implementations to provide specific functionality for tasks like rotating images.
|
||||||
|
*/
|
||||||
|
interface TransformImage {
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Rotates the specified image file by the given degree.
|
||||||
|
*
|
||||||
|
* @param imageFile The File representing the image to be rotated.
|
||||||
|
* @param degree The degree by which to rotate the image.
|
||||||
|
* @return The rotated image File, or null if the rotation operation fails.
|
||||||
|
*/
|
||||||
|
fun rotateImage(imageFile: File, degree : Int ):File?
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,74 @@
|
||||||
|
package fr.free.nrw.commons.edit
|
||||||
|
|
||||||
|
import android.mediautil.image.jpeg.LLJTran
|
||||||
|
import android.mediautil.image.jpeg.LLJTranException
|
||||||
|
import android.os.Environment
|
||||||
|
import timber.log.Timber
|
||||||
|
import java.io.BufferedOutputStream
|
||||||
|
import java.io.File
|
||||||
|
import java.io.FileOutputStream
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Implementation of the TransformImage interface for image rotation operations.
|
||||||
|
*
|
||||||
|
* This class provides an implementation for the TransformImage interface, right now it exposes a
|
||||||
|
* function for rotating images by a specified degree using the LLJTran library. Right now it reads
|
||||||
|
* the input image file, performs the rotation, and saves the rotated image to a new file.
|
||||||
|
*/
|
||||||
|
class TransformImageImpl() : TransformImage {
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Rotates the specified image file by the given degree.
|
||||||
|
*
|
||||||
|
* @param imageFile The File representing the image to be rotated.
|
||||||
|
* @param degree The degree by which to rotate the image.
|
||||||
|
* @return The rotated image File, or null if the rotation operation fails.
|
||||||
|
*/
|
||||||
|
override fun rotateImage(imageFile: File, degree : Int): File? {
|
||||||
|
|
||||||
|
Timber.tag("Trying to rotate image").d("Starting")
|
||||||
|
|
||||||
|
val path = Environment.getExternalStoragePublicDirectory(
|
||||||
|
Environment.DIRECTORY_DOWNLOADS
|
||||||
|
)
|
||||||
|
|
||||||
|
val imagePath = System.currentTimeMillis()
|
||||||
|
val file: File = File(path, "$imagePath.jpg")
|
||||||
|
|
||||||
|
val output = file
|
||||||
|
|
||||||
|
val rotated = try {
|
||||||
|
val lljTran = LLJTran(imageFile)
|
||||||
|
lljTran.read(
|
||||||
|
LLJTran.READ_ALL,
|
||||||
|
false,
|
||||||
|
) // This could throw an LLJTranException. I am not catching it for now... Let's see.
|
||||||
|
lljTran.transform(
|
||||||
|
when(degree){
|
||||||
|
90 -> LLJTran.ROT_90
|
||||||
|
180 -> LLJTran.ROT_180
|
||||||
|
270 -> LLJTran.ROT_270
|
||||||
|
else -> {
|
||||||
|
LLJTran.ROT_90
|
||||||
|
}
|
||||||
|
},
|
||||||
|
LLJTran.OPT_DEFAULTS or LLJTran.OPT_XFORM_ORIENTATION
|
||||||
|
)
|
||||||
|
BufferedOutputStream(FileOutputStream(output)).use { writer ->
|
||||||
|
lljTran.save(writer, LLJTran.OPT_WRITE_ALL )
|
||||||
|
}
|
||||||
|
lljTran.freeMemory()
|
||||||
|
true
|
||||||
|
} catch (e: LLJTranException) {
|
||||||
|
Timber.tag("Error").d(e)
|
||||||
|
return null
|
||||||
|
false
|
||||||
|
}
|
||||||
|
|
||||||
|
if (rotated) {
|
||||||
|
Timber.tag("Done rotating image").d("Done")
|
||||||
|
Timber.tag("Add").d(output.absolutePath)
|
||||||
|
}
|
||||||
|
return output
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -65,6 +65,8 @@ import fr.free.nrw.commons.utils.ViewUtil;
|
||||||
import io.reactivex.android.schedulers.AndroidSchedulers;
|
import io.reactivex.android.schedulers.AndroidSchedulers;
|
||||||
import io.reactivex.disposables.CompositeDisposable;
|
import io.reactivex.disposables.CompositeDisposable;
|
||||||
import io.reactivex.schedulers.Schedulers;
|
import io.reactivex.schedulers.Schedulers;
|
||||||
|
import java.io.File;
|
||||||
|
import java.security.Permission;
|
||||||
import java.util.ArrayList;
|
import java.util.ArrayList;
|
||||||
import java.util.Collections;
|
import java.util.Collections;
|
||||||
import java.util.HashMap;
|
import java.util.HashMap;
|
||||||
|
|
@ -484,6 +486,23 @@ public class UploadActivity extends BaseActivity implements UploadContract.View,
|
||||||
presenter.deletePictureAtIndex(index);
|
presenter.deletePictureAtIndex(index);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Changes the thumbnail of an UploadableFile at the specified index.
|
||||||
|
* This method updates the list of uploadableFiles by replacing the UploadableFile
|
||||||
|
* at the given index with a new UploadableFile created from the provided file path.
|
||||||
|
* After updating the list, it notifies the RecyclerView's adapter to refresh its data,
|
||||||
|
* ensuring that the thumbnail change is reflected in the UI.
|
||||||
|
*
|
||||||
|
* @param index The index of the UploadableFile to be updated.
|
||||||
|
* @param filepath The file path of the new thumbnail image.
|
||||||
|
*/
|
||||||
|
@Override
|
||||||
|
public void changeThumbnail(int index, String filepath) {
|
||||||
|
uploadableFiles.remove(index);
|
||||||
|
uploadableFiles.add(index, new UploadableFile(new File(filepath)));
|
||||||
|
rvThumbnails.getAdapter().notifyDataSetChanged();
|
||||||
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public void onNextButtonClicked(int index) {
|
public void onNextButtonClicked(int index) {
|
||||||
UploadActivity.this.onNextButtonClicked(index);
|
UploadActivity.this.onNextButtonClicked(index);
|
||||||
|
|
|
||||||
|
|
@ -14,7 +14,7 @@ import java.util.List;
|
||||||
|
|
||||||
public class UploadItem {
|
public class UploadItem {
|
||||||
|
|
||||||
private final Uri mediaUri;
|
private Uri mediaUri;
|
||||||
private final String mimeType;
|
private final String mimeType;
|
||||||
private ImageCoordinates gpsCoords;
|
private ImageCoordinates gpsCoords;
|
||||||
private List<UploadMediaDetail> uploadMediaDetails;
|
private List<UploadMediaDetail> uploadMediaDetails;
|
||||||
|
|
@ -31,7 +31,7 @@ public class UploadItem {
|
||||||
* Uri of uploadItem
|
* Uri of uploadItem
|
||||||
* Uri points to image location or name, eg content://media/external/images/camera/10495 (Android 10)
|
* Uri points to image location or name, eg content://media/external/images/camera/10495 (Android 10)
|
||||||
*/
|
*/
|
||||||
private final Uri contentUri;
|
private Uri contentUri;
|
||||||
|
|
||||||
|
|
||||||
@SuppressLint("CheckResult")
|
@SuppressLint("CheckResult")
|
||||||
|
|
@ -160,4 +160,16 @@ public class UploadItem {
|
||||||
public String getCountryCode() {
|
public String getCountryCode() {
|
||||||
return countryCode;
|
return countryCode;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Sets both the contentUri and mediaUri to the specified Uri.
|
||||||
|
* This method allows you to assign the same Uri to both the contentUri and mediaUri
|
||||||
|
* properties.
|
||||||
|
*
|
||||||
|
* @param uri The Uri to be set as both the contentUri and mediaUri.
|
||||||
|
*/
|
||||||
|
public void setContentUri(Uri uri) {
|
||||||
|
contentUri = uri;
|
||||||
|
mediaUri = uri;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -6,7 +6,10 @@ import static fr.free.nrw.commons.utils.ImageUtils.FILE_NAME_EXISTS;
|
||||||
import static fr.free.nrw.commons.utils.ImageUtils.getErrorMessageForResult;
|
import static fr.free.nrw.commons.utils.ImageUtils.getErrorMessageForResult;
|
||||||
|
|
||||||
import android.annotation.SuppressLint;
|
import android.annotation.SuppressLint;
|
||||||
|
import android.content.ContentResolver;
|
||||||
|
import android.content.Context;
|
||||||
import android.content.Intent;
|
import android.content.Intent;
|
||||||
|
import android.net.Uri;
|
||||||
import android.os.Bundle;
|
import android.os.Bundle;
|
||||||
import android.text.TextUtils;
|
import android.text.TextUtils;
|
||||||
import android.view.LayoutInflater;
|
import android.view.LayoutInflater;
|
||||||
|
|
@ -32,6 +35,7 @@ import com.github.chrisbanes.photoview.PhotoView;
|
||||||
import com.mapbox.mapboxsdk.camera.CameraPosition;
|
import com.mapbox.mapboxsdk.camera.CameraPosition;
|
||||||
import fr.free.nrw.commons.LocationPicker.LocationPicker;
|
import fr.free.nrw.commons.LocationPicker.LocationPicker;
|
||||||
import fr.free.nrw.commons.R;
|
import fr.free.nrw.commons.R;
|
||||||
|
import fr.free.nrw.commons.edit.EditActivity;
|
||||||
import fr.free.nrw.commons.contributions.MainActivity;
|
import fr.free.nrw.commons.contributions.MainActivity;
|
||||||
import fr.free.nrw.commons.filepicker.UploadableFile;
|
import fr.free.nrw.commons.filepicker.UploadableFile;
|
||||||
import fr.free.nrw.commons.kvstore.JsonKvStore;
|
import fr.free.nrw.commons.kvstore.JsonKvStore;
|
||||||
|
|
@ -51,6 +55,7 @@ import fr.free.nrw.commons.utils.DialogUtil;
|
||||||
import fr.free.nrw.commons.utils.ImageUtils;
|
import fr.free.nrw.commons.utils.ImageUtils;
|
||||||
import fr.free.nrw.commons.utils.ViewUtil;
|
import fr.free.nrw.commons.utils.ViewUtil;
|
||||||
import fr.free.nrw.commons.R.drawable.*;
|
import fr.free.nrw.commons.R.drawable.*;
|
||||||
|
import java.io.File;
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
import java.util.Locale;
|
import java.util.Locale;
|
||||||
import java.util.Objects;
|
import java.util.Objects;
|
||||||
|
|
@ -63,10 +68,11 @@ public class UploadMediaDetailFragment extends UploadBaseFragment implements
|
||||||
UploadMediaDetailsContract.View, UploadMediaDetailAdapter.EventListener {
|
UploadMediaDetailsContract.View, UploadMediaDetailAdapter.EventListener {
|
||||||
|
|
||||||
private static final int REQUEST_CODE = 1211;
|
private static final int REQUEST_CODE = 1211;
|
||||||
|
private static final int REQUEST_CODE_FOR_EDIT_ACTIVITY = 1212;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* A key for applicationKvStore.
|
* A key for applicationKvStore. By this key we can retrieve the location of last UploadItem ex.
|
||||||
* By this key we can retrieve the location of last UploadItem ex. 12.3433,54.78897
|
* 12.3433,54.78897 from applicationKvStore.
|
||||||
* from applicationKvStore.
|
|
||||||
*/
|
*/
|
||||||
public static final String LAST_LOCATION = "last_location_while_uploading";
|
public static final String LAST_LOCATION = "last_location_while_uploading";
|
||||||
public static final String LAST_ZOOM = "last_zoom_level_while_uploading";
|
public static final String LAST_ZOOM = "last_zoom_level_while_uploading";
|
||||||
|
|
@ -86,8 +92,11 @@ public class UploadMediaDetailFragment extends UploadBaseFragment implements
|
||||||
AppCompatButton btnNext;
|
AppCompatButton btnNext;
|
||||||
@BindView(R.id.btn_previous)
|
@BindView(R.id.btn_previous)
|
||||||
AppCompatButton btnPrevious;
|
AppCompatButton btnPrevious;
|
||||||
|
@BindView(R.id.edit_image)
|
||||||
|
AppCompatButton editImage;
|
||||||
@BindView(R.id.tooltip)
|
@BindView(R.id.tooltip)
|
||||||
ImageView tooltip;
|
ImageView tooltip;
|
||||||
|
|
||||||
private UploadMediaDetailAdapter uploadMediaDetailAdapter;
|
private UploadMediaDetailAdapter uploadMediaDetailAdapter;
|
||||||
@BindView(R.id.btn_copy_subsequent_media)
|
@BindView(R.id.btn_copy_subsequent_media)
|
||||||
AppCompatButton btnCopyToSubsequentMedia;
|
AppCompatButton btnCopyToSubsequentMedia;
|
||||||
|
|
@ -108,14 +117,14 @@ public class UploadMediaDetailFragment extends UploadBaseFragment implements
|
||||||
private boolean isExpanded = true;
|
private boolean isExpanded = true;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* True if location is added via the "missing location" popup dialog (which appears after tapping
|
* True if location is added via the "missing location" popup dialog (which appears after
|
||||||
* "Next" if the picture has no geographical coordinates).
|
* tapping "Next" if the picture has no geographical coordinates).
|
||||||
*/
|
*/
|
||||||
private boolean isMissingLocationDialog;
|
private boolean isMissingLocationDialog;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* showNearbyFound will be true, if any nearby location found that needs pictures and the nearby popup is yet to be shown
|
* showNearbyFound will be true, if any nearby location found that needs pictures and the nearby
|
||||||
* Used to show and check if the nearby found popup is already shown
|
* popup is yet to be shown Used to show and check if the nearby found popup is already shown
|
||||||
*/
|
*/
|
||||||
private boolean showNearbyFound;
|
private boolean showNearbyFound;
|
||||||
|
|
||||||
|
|
@ -125,8 +134,8 @@ public class UploadMediaDetailFragment extends UploadBaseFragment implements
|
||||||
private Place nearbyPlace;
|
private Place nearbyPlace;
|
||||||
private UploadItem uploadItem;
|
private UploadItem uploadItem;
|
||||||
/**
|
/**
|
||||||
* inAppPictureLocation: use location recorded while using the in-app camera if
|
* inAppPictureLocation: use location recorded while using the in-app camera if device camera
|
||||||
* device camera does not record it in the EXIF
|
* does not record it in the EXIF
|
||||||
*/
|
*/
|
||||||
private LatLng inAppPictureLocation;
|
private LatLng inAppPictureLocation;
|
||||||
/**
|
/**
|
||||||
|
|
@ -145,7 +154,8 @@ public class UploadMediaDetailFragment extends UploadBaseFragment implements
|
||||||
super.onCreate(savedInstanceState);
|
super.onCreate(savedInstanceState);
|
||||||
}
|
}
|
||||||
|
|
||||||
public void setImageTobeUploaded(UploadableFile uploadableFile, Place place, LatLng inAppPictureLocation) {
|
public void setImageTobeUploaded(UploadableFile uploadableFile, Place place,
|
||||||
|
LatLng inAppPictureLocation) {
|
||||||
this.uploadableFile = uploadableFile;
|
this.uploadableFile = uploadableFile;
|
||||||
this.place = place;
|
this.place = place;
|
||||||
this.inAppPictureLocation = inAppPictureLocation;
|
this.inAppPictureLocation = inAppPictureLocation;
|
||||||
|
|
@ -228,7 +238,8 @@ public class UploadMediaDetailFragment extends UploadBaseFragment implements
|
||||||
* init the description recycler veiw and caption recyclerview
|
* init the description recycler veiw and caption recyclerview
|
||||||
*/
|
*/
|
||||||
private void initRecyclerView() {
|
private void initRecyclerView() {
|
||||||
uploadMediaDetailAdapter = new UploadMediaDetailAdapter(defaultKvStore.getString(Prefs.DESCRIPTION_LANGUAGE, ""), recentLanguagesDao);
|
uploadMediaDetailAdapter = new UploadMediaDetailAdapter(
|
||||||
|
defaultKvStore.getString(Prefs.DESCRIPTION_LANGUAGE, ""), recentLanguagesDao);
|
||||||
uploadMediaDetailAdapter.setCallback(this::showInfoAlert);
|
uploadMediaDetailAdapter.setCallback(this::showInfoAlert);
|
||||||
uploadMediaDetailAdapter.setEventListener(this);
|
uploadMediaDetailAdapter.setEventListener(this);
|
||||||
rvDescriptions.setLayoutManager(new LinearLayoutManager(getContext()));
|
rvDescriptions.setLayoutManager(new LinearLayoutManager(getContext()));
|
||||||
|
|
@ -241,7 +252,8 @@ public class UploadMediaDetailFragment extends UploadBaseFragment implements
|
||||||
* @param messageStringId
|
* @param messageStringId
|
||||||
*/
|
*/
|
||||||
private void showInfoAlert(int titleStringID, int messageStringId) {
|
private void showInfoAlert(int titleStringID, int messageStringId) {
|
||||||
DialogUtil.showAlertDialog(getActivity(), getString(titleStringID), getString(messageStringId), getString(android.R.string.ok), null, true);
|
DialogUtil.showAlertDialog(getActivity(), getString(titleStringID),
|
||||||
|
getString(messageStringId), getString(android.R.string.ok), null, true);
|
||||||
}
|
}
|
||||||
|
|
||||||
@OnClick(R.id.btn_next)
|
@OnClick(R.id.btn_next)
|
||||||
|
|
@ -267,6 +279,10 @@ public class UploadMediaDetailFragment extends UploadBaseFragment implements
|
||||||
rvDescriptions.smoothScrollToPosition(uploadMediaDetailAdapter.getItemCount()-1);
|
rvDescriptions.smoothScrollToPosition(uploadMediaDetailAdapter.getItemCount()-1);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@OnClick(R.id.edit_image)
|
||||||
|
public void onEditButtonClicked() {
|
||||||
|
presenter.onEditButtonClicked(callback.getIndexInViewFlipper(this));
|
||||||
|
}
|
||||||
@Override
|
@Override
|
||||||
public void showSimilarImageFragment(String originalFilePath, String possibleFilePath,
|
public void showSimilarImageFragment(String originalFilePath, String possibleFilePath,
|
||||||
ImageCoordinates similarImageCoordinates) {
|
ImageCoordinates similarImageCoordinates) {
|
||||||
|
|
@ -330,7 +346,8 @@ public class UploadMediaDetailFragment extends UploadBaseFragment implements
|
||||||
* Shows nearby place found popup
|
* Shows nearby place found popup
|
||||||
* @param place
|
* @param place
|
||||||
*/
|
*/
|
||||||
@SuppressLint("StringFormatInvalid") // To avoid the unwanted lint warning that string 'upload_nearby_place_found_description' is not of a valid format
|
@SuppressLint("StringFormatInvalid")
|
||||||
|
// To avoid the unwanted lint warning that string 'upload_nearby_place_found_description' is not of a valid format
|
||||||
private void showNearbyPlaceFound(Place place) {
|
private void showNearbyPlaceFound(Place place) {
|
||||||
final View customLayout = getLayoutInflater().inflate(R.layout.custom_nearby_found, null);
|
final View customLayout = getLayoutInflater().inflate(R.layout.custom_nearby_found, null);
|
||||||
ImageView nearbyFoundImage = customLayout.findViewById(R.id.nearbyItemImage);
|
ImageView nearbyFoundImage = customLayout.findViewById(R.id.nearbyItemImage);
|
||||||
|
|
@ -465,6 +482,24 @@ public class UploadMediaDetailFragment extends UploadBaseFragment implements
|
||||||
goToLocationPickerActivity(uploadItem);
|
goToLocationPickerActivity(uploadItem);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Launches the image editing activity to edit the specified UploadItem.
|
||||||
|
*
|
||||||
|
* @param uploadItem The UploadItem to be edited.
|
||||||
|
*
|
||||||
|
* This method is called to start the image editing activity for a specific UploadItem.
|
||||||
|
* It sets the UploadItem as the currently editable item, creates an intent to launch the
|
||||||
|
* EditActivity, and passes the image file path as an extra in the intent. The activity
|
||||||
|
* is started with a request code, allowing the result to be handled in onActivityResult.
|
||||||
|
*/
|
||||||
|
@Override
|
||||||
|
public void showEditActivity(UploadItem uploadItem) {
|
||||||
|
editableUploadItem = uploadItem;
|
||||||
|
Intent intent = new Intent(getContext(), EditActivity.class);
|
||||||
|
intent.putExtra("image", uploadableFile.getFilePath().toString());
|
||||||
|
startActivityForResult(intent, REQUEST_CODE_FOR_EDIT_ACTIVITY);
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Start Location picker activity. Show the location first then user can modify it by clicking
|
* Start Location picker activity. Show the location first then user can modify it by clicking
|
||||||
* modify location button.
|
* modify location button.
|
||||||
|
|
@ -546,6 +581,22 @@ public class UploadMediaDetailFragment extends UploadBaseFragment implements
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
if (requestCode == REQUEST_CODE_FOR_EDIT_ACTIVITY && resultCode == RESULT_OK) {
|
||||||
|
String result = data.getStringExtra("editedImageFilePath");
|
||||||
|
|
||||||
|
if (Objects.equals(result, "Error")) {
|
||||||
|
Timber.e("Error in rotating image");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
try {
|
||||||
|
photoViewBackgroundImage.setImageURI(Uri.fromFile(new File(result)));
|
||||||
|
editableUploadItem.setContentUri(Uri.fromFile(new File(result)));
|
||||||
|
callback.changeThumbnail(callback.getIndexInViewFlipper(this),
|
||||||
|
result);
|
||||||
|
} catch (Exception e) {
|
||||||
|
Timber.e(e);
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|
@ -574,7 +625,8 @@ public class UploadMediaDetailFragment extends UploadBaseFragment implements
|
||||||
uploadMediaDetailAdapter.setItems(uploadMediaDetails);
|
uploadMediaDetailAdapter.setItems(uploadMediaDetails);
|
||||||
showNearbyFound =
|
showNearbyFound =
|
||||||
showNearbyFound && (
|
showNearbyFound && (
|
||||||
uploadMediaDetails == null || uploadMediaDetails.isEmpty() || listContainsEmptyDetails(
|
uploadMediaDetails == null || uploadMediaDetails.isEmpty()
|
||||||
|
|| listContainsEmptyDetails(
|
||||||
uploadMediaDetails));
|
uploadMediaDetails));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -656,6 +708,8 @@ public class UploadMediaDetailFragment extends UploadBaseFragment implements
|
||||||
public interface UploadMediaDetailFragmentCallback extends Callback {
|
public interface UploadMediaDetailFragmentCallback extends Callback {
|
||||||
|
|
||||||
void deletePictureAtIndex(int index);
|
void deletePictureAtIndex(int index);
|
||||||
|
|
||||||
|
void changeThumbnail(int index, String uri);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -37,6 +37,8 @@ public interface UploadMediaDetailsContract {
|
||||||
|
|
||||||
void showExternalMap(UploadItem uploadItem);
|
void showExternalMap(UploadItem uploadItem);
|
||||||
|
|
||||||
|
void showEditActivity(UploadItem uploadItem);
|
||||||
|
|
||||||
void updateMediaDetails(List<UploadMediaDetail> uploadMediaDetails);
|
void updateMediaDetails(List<UploadMediaDetail> uploadMediaDetails);
|
||||||
|
|
||||||
void displayAddLocationDialog(Runnable runnable);
|
void displayAddLocationDialog(Runnable runnable);
|
||||||
|
|
@ -56,6 +58,8 @@ public interface UploadMediaDetailsContract {
|
||||||
|
|
||||||
void onMapIconClicked(int indexInViewFlipper);
|
void onMapIconClicked(int indexInViewFlipper);
|
||||||
|
|
||||||
|
void onEditButtonClicked(int indexInViewFlipper);
|
||||||
|
|
||||||
void onUserConfirmedUploadIsOfPlace(Place place, int uploadItemPosition);
|
void onUserConfirmedUploadIsOfPlace(Place place, int uploadItemPosition);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -281,6 +281,11 @@ public class UploadMediaPresenter implements UserActionListener, SimilarImageInt
|
||||||
view.showExternalMap(repository.getUploads().get(indexInViewFlipper));
|
view.showExternalMap(repository.getUploads().get(indexInViewFlipper));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void onEditButtonClicked(int indexInViewFlipper){
|
||||||
|
view.showEditActivity(repository.getUploads().get(indexInViewFlipper));
|
||||||
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public void onUserConfirmedUploadIsOfPlace(Place place, int uploadItemPosition) {
|
public void onUserConfirmedUploadIsOfPlace(Place place, int uploadItemPosition) {
|
||||||
final List<UploadMediaDetail> uploadMediaDetails = repository.getUploads()
|
final List<UploadMediaDetail> uploadMediaDetails = repository.getUploads()
|
||||||
|
|
|
||||||
5
app/src/main/res/drawable/baseline_rotate_right.xml
Normal file
5
app/src/main/res/drawable/baseline_rotate_right.xml
Normal file
|
|
@ -0,0 +1,5 @@
|
||||||
|
<vector android:height="24dp" android:tint="@color/item_white_background"
|
||||||
|
android:viewportHeight="24" android:viewportWidth="24"
|
||||||
|
android:width="24dp" xmlns:android="http://schemas.android.com/apk/res/android">
|
||||||
|
<path android:fillColor="@android:color/white" android:pathData="M15.55,5.55L11,1v3.07C7.06,4.56 4,7.92 4,12s3.05,7.44 7,7.93v-2.02c-2.84,-0.48 -5,-2.94 -5,-5.91s2.16,-5.43 5,-5.91L11,10l4.55,-4.45zM19.93,11c-0.17,-1.39 -0.72,-2.73 -1.62,-3.89l-1.42,1.42c0.54,0.75 0.88,1.6 1.02,2.47h2.02zM13,17.9v2.02c1.39,-0.17 2.74,-0.71 3.9,-1.61l-1.44,-1.44c-0.75,0.54 -1.59,0.89 -2.46,1.03zM16.89,15.48l1.42,1.41c0.9,-1.16 1.45,-2.5 1.62,-3.89h-2.02c-0.14,0.87 -0.48,1.72 -1.02,2.48z"/>
|
||||||
|
</vector>
|
||||||
5
app/src/main/res/drawable/baseline_save_24.xml
Normal file
5
app/src/main/res/drawable/baseline_save_24.xml
Normal file
|
|
@ -0,0 +1,5 @@
|
||||||
|
<vector android:height="24dp" android:tint="@color/bottom_bar_light"
|
||||||
|
android:viewportHeight="24" android:viewportWidth="24"
|
||||||
|
android:width="24dp" xmlns:android="http://schemas.android.com/apk/res/android">
|
||||||
|
<path android:fillColor="@android:color/white" android:pathData="M17,3L5,3c-1.11,0 -2,0.9 -2,2v14c0,1.1 0.89,2 2,2h14c1.1,0 2,-0.9 2,-2L21,7l-4,-4zM12,19c-1.66,0 -3,-1.34 -3,-3s1.34,-3 3,-3 3,1.34 3,3 -1.34,3 -3,3zM15,9L5,9L5,5h10v4z"/>
|
||||||
|
</vector>
|
||||||
46
app/src/main/res/layout/activity_edit.xml
Normal file
46
app/src/main/res/layout/activity_edit.xml
Normal file
|
|
@ -0,0 +1,46 @@
|
||||||
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
|
<FrameLayout xmlns:android="http://schemas.android.com/apk/res/android"
|
||||||
|
xmlns:tools="http://schemas.android.com/tools"
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="match_parent"
|
||||||
|
android:layout_gravity="bottom"
|
||||||
|
tools:context=".edit.EditActivity"
|
||||||
|
android:gravity="center"
|
||||||
|
android:layout_margin="2dp"
|
||||||
|
android:orientation="vertical">
|
||||||
|
|
||||||
|
<ImageView
|
||||||
|
android:id="@+id/iv"
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="match_parent"
|
||||||
|
android:layout_gravity="center"
|
||||||
|
/>
|
||||||
|
<LinearLayout
|
||||||
|
android:elevation="2dp"
|
||||||
|
android:gravity="center"
|
||||||
|
android:orientation="horizontal"
|
||||||
|
android:layout_gravity="bottom|center"
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:layout_margin="2dp">
|
||||||
|
|
||||||
|
<androidx.appcompat.widget.AppCompatButton
|
||||||
|
android:drawablePadding="4dp"
|
||||||
|
android:drawableStart="@drawable/baseline_rotate_right"
|
||||||
|
android:id="@+id/rotate_btn"
|
||||||
|
android:layout_width="wrap_content"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:textColor="@android:color/white"
|
||||||
|
android:text="@string/rotate"/>
|
||||||
|
|
||||||
|
<androidx.appcompat.widget.AppCompatButton
|
||||||
|
android:onClick="getRotatedImage"
|
||||||
|
android:drawablePadding="4dp"
|
||||||
|
android:drawableStart="@drawable/baseline_save_24"
|
||||||
|
android:id="@+id/btn_save"
|
||||||
|
android:layout_width="wrap_content"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:text="@string/menu_save_categories"
|
||||||
|
android:textColor="@android:color/white" />
|
||||||
|
</LinearLayout>
|
||||||
|
</FrameLayout>
|
||||||
|
|
@ -142,6 +142,16 @@
|
||||||
android:layout_marginRight="@dimen/standard_gap"
|
android:layout_marginRight="@dimen/standard_gap"
|
||||||
android:layout_toLeftOf="@+id/btn_next"
|
android:layout_toLeftOf="@+id/btn_next"
|
||||||
android:text="@string/previous" />
|
android:text="@string/previous" />
|
||||||
|
<Button
|
||||||
|
android:id="@+id/edit_image"
|
||||||
|
style="@style/Widget.AppCompat.Button.Borderless"
|
||||||
|
android:layout_width="wrap_content"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:layout_marginEnd="@dimen/standard_gap"
|
||||||
|
android:layout_marginBottom="24dp"
|
||||||
|
android:layout_toStartOf="@id/btn_previous"
|
||||||
|
android:contentDescription="Edit Image"
|
||||||
|
android:text="Edit Image" />
|
||||||
|
|
||||||
</RelativeLayout>
|
</RelativeLayout>
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -353,6 +353,7 @@
|
||||||
<string name="wrong">Wrong Answer</string>
|
<string name="wrong">Wrong Answer</string>
|
||||||
<string name="quiz_screenshot_question">Is this screenshot OK to upload?</string>
|
<string name="quiz_screenshot_question">Is this screenshot OK to upload?</string>
|
||||||
<string name="share_app_title">Share App</string>
|
<string name="share_app_title">Share App</string>
|
||||||
|
<string name="rotate">Rotate</string>
|
||||||
|
|
||||||
<string name="error_fetching_nearby_places">Error fetching nearby places.</string>
|
<string name="error_fetching_nearby_places">Error fetching nearby places.</string>
|
||||||
<string name="no_nearby_places_around">No nearby places around</string>
|
<string name="no_nearby_places_around">No nearby places around</string>
|
||||||
|
|
|
||||||
|
|
@ -158,6 +158,9 @@
|
||||||
<style name="LightFlatNearbyPermissionButton" parent="LightAppTheme">
|
<style name="LightFlatNearbyPermissionButton" parent="LightAppTheme">
|
||||||
<item name="colorControlHighlight">@color/primaryColor</item>
|
<item name="colorControlHighlight">@color/primaryColor</item>
|
||||||
</style>
|
</style>
|
||||||
|
<style name="EditActivityTheme" parent="Theme.AppCompat.DayNight.DarkActionBar">
|
||||||
|
<item name="colorPrimary">@color/primaryColor</item>
|
||||||
|
</style>
|
||||||
|
|
||||||
<style name="ProgressBar" parent="Widget.AppCompat.ProgressBar.Horizontal" />
|
<style name="ProgressBar" parent="Widget.AppCompat.ProgressBar.Horizontal" />
|
||||||
|
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue