mirror of
https://github.com/FossifyOrg/Gallery.git
synced 2025-12-23 23:58:28 -05:00
feat: add option to overwrite original file in editor (#686)
Refs: https://github.com/FossifyOrg/Gallery/issues/62
This commit is contained in:
@@ -5,6 +5,8 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/),
|
||||
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
|
||||
|
||||
## [Unreleased]
|
||||
### Added
|
||||
- Option to overwrite the original image when saving edits ([#62])
|
||||
|
||||
## [1.6.0] - 2025-10-01
|
||||
### Added
|
||||
@@ -165,6 +167,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
|
||||
- Fixed privacy policy link
|
||||
|
||||
[#29]: https://github.com/FossifyOrg/Gallery/issues/29
|
||||
[#62]: https://github.com/FossifyOrg/Gallery/issues/62
|
||||
[#128]: https://github.com/FossifyOrg/Gallery/issues/128
|
||||
[#166]: https://github.com/FossifyOrg/Gallery/issues/166
|
||||
[#199]: https://github.com/FossifyOrg/Gallery/issues/199
|
||||
|
||||
@@ -10,7 +10,6 @@ import android.os.Bundle
|
||||
import android.os.Handler
|
||||
import android.provider.MediaStore
|
||||
import android.view.View
|
||||
import android.widget.ImageView
|
||||
import android.widget.RelativeLayout
|
||||
import androidx.core.view.isInvisible
|
||||
import androidx.exifinterface.media.ExifInterface
|
||||
@@ -42,7 +41,6 @@ import org.fossify.gallery.dialogs.OtherAspectRatioDialog
|
||||
import org.fossify.gallery.dialogs.ResizeDialog
|
||||
import org.fossify.gallery.dialogs.SaveAsDialog
|
||||
import org.fossify.gallery.extensions.config
|
||||
import org.fossify.gallery.extensions.copyNonDimensionAttributesTo
|
||||
import org.fossify.gallery.extensions.fixDateTaken
|
||||
import org.fossify.gallery.extensions.openEditor
|
||||
import org.fossify.gallery.helpers.*
|
||||
@@ -51,6 +49,14 @@ import java.io.*
|
||||
import kotlin.math.max
|
||||
import androidx.core.graphics.scale
|
||||
import androidx.core.net.toUri
|
||||
import org.fossify.gallery.extensions.writeExif
|
||||
import org.fossify.gallery.extensions.ensureWritablePath
|
||||
import org.fossify.gallery.extensions.getCompressionFormatFromUri
|
||||
import org.fossify.gallery.extensions.readExif
|
||||
import org.fossify.gallery.extensions.proposeNewFilePath
|
||||
import org.fossify.gallery.extensions.resolveUriScheme
|
||||
import org.fossify.gallery.extensions.showContentDescriptionOnLongClick
|
||||
import org.fossify.gallery.extensions.writeBitmapToCache
|
||||
|
||||
class EditActivity : SimpleActivity() {
|
||||
companion object {
|
||||
@@ -58,7 +64,6 @@ class EditActivity : SimpleActivity() {
|
||||
System.loadLibrary("NativeImageProcessor")
|
||||
}
|
||||
|
||||
private const val TEMP_FOLDER_NAME = "images"
|
||||
private const val ASPECT_X = "aspectX"
|
||||
private const val ASPECT_Y = "aspectY"
|
||||
private const val CROP = "crop"
|
||||
@@ -73,17 +78,14 @@ class EditActivity : SimpleActivity() {
|
||||
private const val CROP_ROTATE_ASPECT_RATIO = 1
|
||||
}
|
||||
|
||||
|
||||
private lateinit var saveUri: Uri
|
||||
private var uri: Uri? = null
|
||||
private var resizeWidth = 0
|
||||
private var resizeHeight = 0
|
||||
private var drawColor = 0
|
||||
private var lastOtherAspectRatio: Pair<Float, Float>? = null
|
||||
private var currPrimaryAction =
|
||||
PRIMARY_ACTION_NONE
|
||||
private var currCropRotateAction =
|
||||
CROP_ROTATE_ASPECT_RATIO
|
||||
private var currPrimaryAction = PRIMARY_ACTION_NONE
|
||||
private var currCropRotateAction = CROP_ROTATE_ASPECT_RATIO
|
||||
private var currAspectRatio = ASPECT_RATIO_FREE
|
||||
private var isCropIntent = false
|
||||
private var isEditingWithThirdParty = false
|
||||
@@ -95,6 +97,8 @@ class EditActivity : SimpleActivity() {
|
||||
private var bitmapCroppingJob: Job? = null
|
||||
private val binding by viewBinding(ActivityEditBinding::inflate)
|
||||
|
||||
private var overwriteRequested = false
|
||||
|
||||
override fun onCreate(savedInstanceState: Bundle?) {
|
||||
super.onCreate(savedInstanceState)
|
||||
setContentView(binding.root)
|
||||
@@ -130,7 +134,8 @@ class EditActivity : SimpleActivity() {
|
||||
private fun setupOptionsMenu() {
|
||||
binding.editorToolbar.setOnMenuItemClickListener { menuItem ->
|
||||
when (menuItem.itemId) {
|
||||
R.id.save_as -> saveImage()
|
||||
R.id.save_as -> startSaveFlow(overwrite = false)
|
||||
R.id.overwrite_original -> startSaveFlow(overwrite = true)
|
||||
R.id.edit -> editWith()
|
||||
R.id.share -> shareImage()
|
||||
else -> return@setOnMenuItemClickListener false
|
||||
@@ -154,11 +159,12 @@ class EditActivity : SimpleActivity() {
|
||||
return
|
||||
}
|
||||
|
||||
if (intent.extras?.containsKey(REAL_FILE_PATH) == true) {
|
||||
val extras = intent.extras
|
||||
if (extras?.containsKey(REAL_FILE_PATH) == true) {
|
||||
val realPath = intent.extras!!.getString(REAL_FILE_PATH)
|
||||
uri = when {
|
||||
isPathOnOTG(realPath!!) -> uri
|
||||
realPath.startsWith("file:/") -> Uri.parse(realPath)
|
||||
realPath.startsWith("file:/") -> realPath.toUri()
|
||||
else -> Uri.fromFile(File(realPath))
|
||||
}
|
||||
} else {
|
||||
@@ -168,14 +174,16 @@ class EditActivity : SimpleActivity() {
|
||||
}
|
||||
|
||||
saveUri = when {
|
||||
intent.extras?.containsKey(MediaStore.EXTRA_OUTPUT) == true && intent.extras!!.get(MediaStore.EXTRA_OUTPUT) is Uri -> intent.extras!!.get(MediaStore.EXTRA_OUTPUT) as Uri
|
||||
extras?.containsKey(MediaStore.EXTRA_OUTPUT) == true
|
||||
&& extras.get(MediaStore.EXTRA_OUTPUT) is Uri -> extras.get(MediaStore.EXTRA_OUTPUT) as Uri
|
||||
else -> uri!!
|
||||
}
|
||||
|
||||
isCropIntent = intent.extras?.get(CROP) == "true"
|
||||
isCropIntent = extras?.get(CROP) == "true"
|
||||
if (isCropIntent) {
|
||||
binding.bottomEditorPrimaryActions.root.beGone()
|
||||
(binding.bottomEditorCropRotateActions.root.layoutParams as RelativeLayout.LayoutParams).addRule(RelativeLayout.ALIGN_PARENT_BOTTOM, 1)
|
||||
binding.editorToolbar.menu.findItem(R.id.overwrite_original).isVisible = false
|
||||
}
|
||||
|
||||
loadDefaultImageView()
|
||||
@@ -321,76 +329,110 @@ class EditActivity : SimpleActivity() {
|
||||
}
|
||||
}
|
||||
|
||||
private fun saveImage() {
|
||||
private fun setOldExif() {
|
||||
oldExif = readExif(uri!!)
|
||||
}
|
||||
|
||||
private fun startSaveFlow(overwrite: Boolean) {
|
||||
overwriteRequested = overwrite
|
||||
setOldExif()
|
||||
|
||||
if (binding.cropImageView.isVisible()) {
|
||||
cropImageAsync()
|
||||
} else if (binding.editorDrawCanvas.isVisible()) {
|
||||
val bitmap = binding.editorDrawCanvas.getBitmap()
|
||||
if (saveUri.scheme == "file") {
|
||||
SaveAsDialog(this, saveUri.path!!, true) {
|
||||
saveBitmapToFile(bitmap, it, true)
|
||||
}
|
||||
} else if (saveUri.scheme == "content") {
|
||||
val filePathGetter = getNewFilePath()
|
||||
SaveAsDialog(this, filePathGetter.first, filePathGetter.second) {
|
||||
saveBitmapToFile(bitmap, it, true)
|
||||
}
|
||||
}
|
||||
} else {
|
||||
val currentFilter = getFiltersAdapter()?.getCurrentFilter() ?: return
|
||||
val filePathGetter = getNewFilePath()
|
||||
SaveAsDialog(this, filePathGetter.first, filePathGetter.second) {
|
||||
toast(org.fossify.commons.R.string.saving)
|
||||
|
||||
// clean up everything to free as much memory as possible
|
||||
binding.defaultImageView.setImageResource(0)
|
||||
binding.cropImageView.setImageBitmap(null)
|
||||
binding.bottomEditorFilterActions.bottomActionsFilterList.adapter = null
|
||||
binding.bottomEditorFilterActions.bottomActionsFilterList.beGone()
|
||||
|
||||
ensureBackgroundThread {
|
||||
try {
|
||||
val originalBitmap = Glide.with(applicationContext).asBitmap().load(uri).submit(Target.SIZE_ORIGINAL, Target.SIZE_ORIGINAL).get()
|
||||
currentFilter.filter.processFilter(originalBitmap)
|
||||
saveBitmapToFile(originalBitmap, it, false)
|
||||
} catch (e: OutOfMemoryError) {
|
||||
toast(org.fossify.commons.R.string.out_of_memory_error)
|
||||
}
|
||||
}
|
||||
}
|
||||
when {
|
||||
binding.cropImageView.isVisible() -> saveCroppedImage()
|
||||
binding.editorDrawCanvas.isVisible() -> saveDrawnImage()
|
||||
else -> saveFilteredImage(overwrite)
|
||||
}
|
||||
}
|
||||
|
||||
private fun setCropProgressBarVisibility(visible: Boolean) {
|
||||
val progressBar: View? = binding.cropImageView.findViewById(com.canhub.cropper.R.id.CropProgressBar)
|
||||
progressBar?.isInvisible = visible.not()
|
||||
private fun saveDrawnImage() {
|
||||
saveBitmap(
|
||||
overwrite = overwriteRequested,
|
||||
bitmap = binding.editorDrawCanvas.getBitmap()
|
||||
)
|
||||
}
|
||||
|
||||
private fun cropImageAsync() {
|
||||
setCropProgressBarVisibility(visible = true)
|
||||
private fun setCropProgressBarVisibility(visible: Boolean) {
|
||||
binding.cropImageView
|
||||
.findViewById<View>(com.canhub.cropper.R.id.CropProgressBar)
|
||||
?.isInvisible = visible.not()
|
||||
}
|
||||
|
||||
private fun saveCroppedImage() {
|
||||
setCropProgressBarVisibility(true)
|
||||
bitmapCroppingJob?.cancel()
|
||||
bitmapCroppingJob = lifecycleScope.launch(CoroutineExceptionHandler { _, t ->
|
||||
onCropImageComplete(bitmap = null, error = Exception(t))
|
||||
onImageCropped(bitmap = null, error = Exception(t))
|
||||
}) {
|
||||
val bitmap = withContext(Dispatchers.Default) {
|
||||
binding.cropImageView.getCroppedImage()
|
||||
}
|
||||
onCropImageComplete(bitmap, null)
|
||||
onImageCropped(bitmap, null)
|
||||
}.apply {
|
||||
invokeOnCompletion { setCropProgressBarVisibility(visible = false) }
|
||||
invokeOnCompletion { setCropProgressBarVisibility(false) }
|
||||
}
|
||||
}
|
||||
|
||||
private fun setOldExif() {
|
||||
var inputStream: InputStream? = null
|
||||
try {
|
||||
inputStream = contentResolver.openInputStream(uri!!)
|
||||
oldExif = ExifInterface(inputStream!!)
|
||||
} catch (e: Exception) {
|
||||
} finally {
|
||||
inputStream?.close()
|
||||
private fun onImageCropped(bitmap: Bitmap?, error: Exception?) {
|
||||
if (error != null || bitmap == null) {
|
||||
toast("${getString(R.string.image_editing_failed)}: ${error?.message}")
|
||||
return
|
||||
}
|
||||
|
||||
setOldExif()
|
||||
|
||||
if (isSharingBitmap) {
|
||||
isSharingBitmap = false
|
||||
shareBitmap(bitmap)
|
||||
return
|
||||
}
|
||||
|
||||
if (isCropIntent) {
|
||||
resolveUriScheme(
|
||||
uri = saveUri,
|
||||
onPath = { saveBitmapToPath(bitmap, it, true) },
|
||||
onContentUri = {
|
||||
saveBitmapToContentUri(bitmap, it, showSavingToast = true, isCropCommit = true)
|
||||
}
|
||||
)
|
||||
return
|
||||
}
|
||||
|
||||
saveBitmap(overwriteRequested, bitmap, showSavingToast = true)
|
||||
}
|
||||
|
||||
private fun getOriginalBitmap(): Bitmap {
|
||||
return Glide.with(applicationContext)
|
||||
.asBitmap()
|
||||
.load(uri)
|
||||
.submit(Target.SIZE_ORIGINAL, Target.SIZE_ORIGINAL)
|
||||
.get()
|
||||
}
|
||||
|
||||
private fun withFilteredImage(callback: (Bitmap) -> Unit) {
|
||||
val currentFilter = getFiltersAdapter()?.getCurrentFilter() ?: return
|
||||
ensureBackgroundThread {
|
||||
try {
|
||||
val original = getOriginalBitmap()
|
||||
currentFilter.filter.processFilter(original)
|
||||
callback(original)
|
||||
} catch (_: OutOfMemoryError) {
|
||||
toast(org.fossify.commons.R.string.out_of_memory_error)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun saveFilteredImage(overwrite: Boolean) {
|
||||
if (overwrite) {
|
||||
freeMemory()
|
||||
withFilteredImage {
|
||||
saveBitmap(true, it)
|
||||
}
|
||||
} else {
|
||||
resolveSaveAsPath { path ->
|
||||
freeMemory()
|
||||
withFilteredImage {
|
||||
saveBitmapToPath(it, path, showSavingToast = true)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -404,7 +446,7 @@ class EditActivity : SimpleActivity() {
|
||||
return@ensureBackgroundThread
|
||||
}
|
||||
|
||||
val originalBitmap = Glide.with(applicationContext).asBitmap().load(uri).submit(Target.SIZE_ORIGINAL, Target.SIZE_ORIGINAL).get()
|
||||
val originalBitmap = getOriginalBitmap()
|
||||
currentFilter.filter.processFilter(originalBitmap)
|
||||
shareBitmap(originalBitmap)
|
||||
}
|
||||
@@ -412,7 +454,7 @@ class EditActivity : SimpleActivity() {
|
||||
binding.cropImageView.isVisible() -> {
|
||||
isSharingBitmap = true
|
||||
runOnUiThread {
|
||||
cropImageAsync()
|
||||
saveCroppedImage()
|
||||
}
|
||||
}
|
||||
|
||||
@@ -421,41 +463,8 @@ class EditActivity : SimpleActivity() {
|
||||
}
|
||||
}
|
||||
|
||||
private fun getTempImagePath(bitmap: Bitmap, callback: (path: String?) -> Unit) {
|
||||
val bytes = ByteArrayOutputStream()
|
||||
bitmap.compress(CompressFormat.PNG, 0, bytes)
|
||||
|
||||
val folder = File(
|
||||
cacheDir,
|
||||
TEMP_FOLDER_NAME
|
||||
)
|
||||
if (!folder.exists()) {
|
||||
if (!folder.mkdir()) {
|
||||
callback(null)
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
val filename = applicationContext.getFilenameFromContentUri(saveUri) ?: "tmp-${System.currentTimeMillis()}.jpg"
|
||||
val newPath = "$folder/$filename"
|
||||
val fileDirItem = FileDirItem(newPath, filename)
|
||||
getFileOutputStream(fileDirItem, true) {
|
||||
if (it != null) {
|
||||
try {
|
||||
it.write(bytes.toByteArray())
|
||||
callback(newPath)
|
||||
} catch (e: Exception) {
|
||||
} finally {
|
||||
it.close()
|
||||
}
|
||||
} else {
|
||||
callback("")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun shareBitmap(bitmap: Bitmap) {
|
||||
getTempImagePath(bitmap) {
|
||||
writeBitmapToCache(saveUri, bitmap) {
|
||||
if (it != null) {
|
||||
sharePathIntent(it, BuildConfig.APPLICATION_ID)
|
||||
} else {
|
||||
@@ -464,7 +473,9 @@ class EditActivity : SimpleActivity() {
|
||||
}
|
||||
}
|
||||
|
||||
private fun getFiltersAdapter() = binding.bottomEditorFilterActions.bottomActionsFilterList.adapter as? FiltersAdapter
|
||||
private fun getFiltersAdapter(): FiltersAdapter? {
|
||||
return binding.bottomEditorFilterActions.bottomActionsFilterList.adapter as? FiltersAdapter
|
||||
}
|
||||
|
||||
private fun setupBottomActions() {
|
||||
setupPrimaryActionButtons()
|
||||
@@ -490,7 +501,7 @@ class EditActivity : SimpleActivity() {
|
||||
binding.bottomEditorPrimaryActions.bottomPrimaryCropRotate,
|
||||
binding.bottomEditorPrimaryActions.bottomPrimaryDraw
|
||||
).forEach {
|
||||
setupLongPress(it)
|
||||
it.showContentDescriptionOnLongClick()
|
||||
}
|
||||
}
|
||||
|
||||
@@ -559,7 +570,7 @@ class EditActivity : SimpleActivity() {
|
||||
binding.bottomEditorCropRotateActions.bottomFlipVertically,
|
||||
binding.bottomEditorCropRotateActions.bottomAspectRatio
|
||||
).forEach {
|
||||
setupLongPress(it)
|
||||
it.showContentDescriptionOnLongClick()
|
||||
}
|
||||
}
|
||||
|
||||
@@ -712,8 +723,7 @@ class EditActivity : SimpleActivity() {
|
||||
|
||||
if (currPrimaryAction != PRIMARY_ACTION_CROP_ROTATE) {
|
||||
binding.bottomAspectRatios.root.beGone()
|
||||
currCropRotateAction =
|
||||
CROP_ROTATE_NONE
|
||||
currCropRotateAction = CROP_ROTATE_NONE
|
||||
}
|
||||
updateCropRotateActionButtons()
|
||||
}
|
||||
@@ -796,19 +806,14 @@ class EditActivity : SimpleActivity() {
|
||||
ResizeDialog(this, point) {
|
||||
resizeWidth = it.x
|
||||
resizeHeight = it.y
|
||||
cropImageAsync()
|
||||
saveCroppedImage()
|
||||
}
|
||||
}
|
||||
|
||||
private fun shouldCropSquare(): Boolean {
|
||||
val extras = intent.extras
|
||||
return if (extras != null && extras.containsKey(ASPECT_X) && extras.containsKey(
|
||||
ASPECT_Y
|
||||
)
|
||||
) {
|
||||
extras.getInt(ASPECT_X) == extras.getInt(
|
||||
ASPECT_Y
|
||||
)
|
||||
return if (extras != null && extras.containsKey(ASPECT_X) && extras.containsKey(ASPECT_Y)) {
|
||||
extras.getInt(ASPECT_X) == extras.getInt(ASPECT_Y)
|
||||
} else {
|
||||
false
|
||||
}
|
||||
@@ -824,98 +829,70 @@ class EditActivity : SimpleActivity() {
|
||||
}
|
||||
}
|
||||
|
||||
private fun onCropImageComplete(bitmap: Bitmap?, error: Exception?) {
|
||||
if (error == null && bitmap != null) {
|
||||
setOldExif()
|
||||
private fun resolveSaveAsPath(callback: (String) -> Unit) {
|
||||
runOnUiThread {
|
||||
resolveUriScheme(
|
||||
uri = saveUri,
|
||||
onPath = {
|
||||
SaveAsDialog(this, it, true, callback = callback)
|
||||
},
|
||||
onContentUri = {
|
||||
val (path, append) = proposeNewFilePath(it)
|
||||
SaveAsDialog(this, path, append, callback = callback)
|
||||
}
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
if (isSharingBitmap) {
|
||||
isSharingBitmap = false
|
||||
shareBitmap(bitmap)
|
||||
return
|
||||
}
|
||||
|
||||
if (isCropIntent) {
|
||||
if (saveUri.scheme == "file") {
|
||||
saveBitmapToFile(bitmap, saveUri.path!!, true)
|
||||
} else {
|
||||
var inputStream: InputStream? = null
|
||||
var outputStream: OutputStream? = null
|
||||
try {
|
||||
val stream = ByteArrayOutputStream()
|
||||
bitmap.compress(CompressFormat.JPEG, 100, stream)
|
||||
inputStream = ByteArrayInputStream(stream.toByteArray())
|
||||
outputStream = contentResolver.openOutputStream(saveUri)
|
||||
inputStream.copyTo(outputStream!!)
|
||||
} catch (e: Exception) {
|
||||
showErrorToast(e)
|
||||
return
|
||||
} finally {
|
||||
inputStream?.close()
|
||||
outputStream?.close()
|
||||
private fun saveBitmap(overwrite: Boolean, bitmap: Bitmap, showSavingToast: Boolean = true) {
|
||||
if (overwrite) {
|
||||
resolveUriScheme(
|
||||
uri = saveUri,
|
||||
onPath = { path ->
|
||||
ensureWritablePath(targetPath = path, confirmOverwrite = false) {
|
||||
saveBitmapToPath(bitmap, it, showSavingToast)
|
||||
}
|
||||
|
||||
copyExifToUri(oldExif, saveUri)
|
||||
Intent().apply {
|
||||
data = saveUri
|
||||
addFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION)
|
||||
setResult(RESULT_OK, this)
|
||||
}
|
||||
finish()
|
||||
},
|
||||
onContentUri = { contentUri ->
|
||||
saveBitmapToContentUri(bitmap, contentUri, showSavingToast, isCropCommit = false)
|
||||
}
|
||||
} else if (saveUri.scheme == "file") {
|
||||
SaveAsDialog(this, saveUri.path!!, true) {
|
||||
saveBitmapToFile(bitmap, it, true)
|
||||
}
|
||||
} else if (saveUri.scheme == "content") {
|
||||
val filePathGetter = getNewFilePath()
|
||||
SaveAsDialog(this, filePathGetter.first, filePathGetter.second) {
|
||||
saveBitmapToFile(bitmap, it, true)
|
||||
}
|
||||
} else {
|
||||
toast(R.string.unknown_file_location)
|
||||
}
|
||||
)
|
||||
} else {
|
||||
toast("${getString(R.string.image_editing_failed)}: ${error?.message}")
|
||||
}
|
||||
}
|
||||
|
||||
private fun getNewFilePath(): Pair<String, Boolean> {
|
||||
var newPath = applicationContext.getRealPathFromURI(saveUri) ?: ""
|
||||
if (newPath.startsWith("/mnt/")) {
|
||||
newPath = ""
|
||||
}
|
||||
|
||||
var shouldAppendFilename = true
|
||||
if (newPath.isEmpty()) {
|
||||
val filename = applicationContext.getFilenameFromContentUri(saveUri) ?: ""
|
||||
if (filename.isNotEmpty()) {
|
||||
val path =
|
||||
if (intent.extras?.containsKey(REAL_FILE_PATH) == true) intent.getStringExtra(REAL_FILE_PATH)?.getParentPath() else internalStoragePath
|
||||
newPath = "$path/$filename"
|
||||
shouldAppendFilename = false
|
||||
resolveSaveAsPath { path ->
|
||||
saveBitmapToPath(bitmap, path, showSavingToast)
|
||||
}
|
||||
}
|
||||
|
||||
if (newPath.isEmpty()) {
|
||||
newPath = "$internalStoragePath/${getCurrentFormattedDateTime()}.${saveUri.toString().getFilenameExtension()}"
|
||||
shouldAppendFilename = false
|
||||
}
|
||||
|
||||
return Pair(newPath, shouldAppendFilename)
|
||||
}
|
||||
|
||||
private fun saveBitmapToFile(bitmap: Bitmap, path: String, showSavingToast: Boolean) {
|
||||
private fun finishCropResultForContent(uri: Uri) {
|
||||
val result = Intent().apply {
|
||||
data = uri
|
||||
addFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION)
|
||||
}
|
||||
setResult(RESULT_OK, result)
|
||||
finish()
|
||||
}
|
||||
|
||||
private fun freeMemory() {
|
||||
// clean up everything to free as much memory as possible
|
||||
binding.defaultImageView.setImageResource(0)
|
||||
binding.cropImageView.setImageBitmap(null)
|
||||
binding.bottomEditorFilterActions.bottomActionsFilterList.adapter = null
|
||||
binding.bottomEditorFilterActions.bottomActionsFilterList.beGone()
|
||||
}
|
||||
|
||||
private fun saveBitmapToPath(bitmap: Bitmap, path: String, showSavingToast: Boolean) {
|
||||
try {
|
||||
ensureBackgroundThread {
|
||||
val file = File(path)
|
||||
val fileDirItem = FileDirItem(path, path.getFilenameFromPath())
|
||||
try {
|
||||
val out = FileOutputStream(file)
|
||||
saveBitmap(file, bitmap, out, showSavingToast)
|
||||
saveBitmapToFile(file, bitmap, out, showSavingToast)
|
||||
} catch (e: Exception) {
|
||||
getFileOutputStream(fileDirItem, true) {
|
||||
if (it != null) {
|
||||
saveBitmap(file, bitmap, it, showSavingToast)
|
||||
saveBitmapToFile(file, bitmap, it, showSavingToast)
|
||||
} else {
|
||||
toast(R.string.image_editing_failed)
|
||||
}
|
||||
@@ -929,7 +906,7 @@ class EditActivity : SimpleActivity() {
|
||||
}
|
||||
}
|
||||
|
||||
private fun saveBitmap(file: File, bitmap: Bitmap, out: OutputStream, showSavingToast: Boolean) {
|
||||
private fun saveBitmapToFile(file: File, bitmap: Bitmap, out: OutputStream, showSavingToast: Boolean) {
|
||||
if (showSavingToast) {
|
||||
toast(org.fossify.commons.R.string.saving)
|
||||
}
|
||||
@@ -943,22 +920,55 @@ class EditActivity : SimpleActivity() {
|
||||
}
|
||||
}
|
||||
|
||||
copyExifToUri(oldExif, file.toUri())
|
||||
writeExif(oldExif, file.toUri())
|
||||
setResult(RESULT_OK, intent)
|
||||
scanFinalPath(file.absolutePath)
|
||||
}
|
||||
|
||||
private fun copyExifToUri(source: ExifInterface?, destination: Uri?) {
|
||||
if (source == null || destination == null) return
|
||||
if (destination.scheme == "content") {
|
||||
contentResolver.openFileDescriptor(destination, "rw")?.use { pfd ->
|
||||
val destExif = ExifInterface(pfd.fileDescriptor)
|
||||
source.copyNonDimensionAttributesTo(destExif)
|
||||
private fun saveBitmapToContentUri(
|
||||
bitmap: Bitmap,
|
||||
uri: Uri,
|
||||
showSavingToast: Boolean,
|
||||
isCropCommit: Boolean
|
||||
) {
|
||||
if (showSavingToast) {
|
||||
toast(org.fossify.commons.R.string.saving)
|
||||
}
|
||||
|
||||
ensureBackgroundThread {
|
||||
var out: OutputStream? = null
|
||||
try {
|
||||
out = contentResolver.openOutputStream(uri, "wt")
|
||||
?: contentResolver.openOutputStream(uri)
|
||||
if (out == null) {
|
||||
val (path, append) = proposeNewFilePath(uri)
|
||||
runOnUiThread {
|
||||
SaveAsDialog(this, path, append) { path ->
|
||||
saveBitmapToPath(bitmap, path, showSavingToast)
|
||||
}
|
||||
}
|
||||
return@ensureBackgroundThread
|
||||
}
|
||||
|
||||
val quality = if (isCropCommit) 100 else 90
|
||||
bitmap.compress(getCompressionFormatFromUri(uri), quality, out)
|
||||
out.flush()
|
||||
writeExif(oldExif, uri)
|
||||
|
||||
runOnUiThread {
|
||||
if (isCropCommit) {
|
||||
finishCropResultForContent(uri)
|
||||
} else {
|
||||
setResult(RESULT_OK, intent)
|
||||
toast(org.fossify.commons.R.string.file_saved)
|
||||
finish()
|
||||
}
|
||||
}
|
||||
} catch (e: Exception) {
|
||||
showErrorToast(e)
|
||||
} finally {
|
||||
try { out?.close() } catch (_: Exception) {}
|
||||
}
|
||||
} else {
|
||||
val file = File(destination.path!!)
|
||||
val destExif = ExifInterface(file.absolutePath)
|
||||
source.copyNonDimensionAttributesTo(destExif)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -976,14 +986,4 @@ class EditActivity : SimpleActivity() {
|
||||
finish()
|
||||
}
|
||||
}
|
||||
|
||||
private fun setupLongPress(view: ImageView) {
|
||||
view.setOnLongClickListener {
|
||||
val contentDescription = view.contentDescription
|
||||
if (contentDescription != null) {
|
||||
toast(contentDescription.toString())
|
||||
}
|
||||
true
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -2,24 +2,41 @@ package org.fossify.gallery.dialogs
|
||||
|
||||
import androidx.appcompat.app.AlertDialog
|
||||
import org.fossify.commons.activities.BaseSimpleActivity
|
||||
import org.fossify.commons.dialogs.ConfirmationDialog
|
||||
import org.fossify.commons.dialogs.FilePickerDialog
|
||||
import org.fossify.commons.extensions.*
|
||||
import org.fossify.commons.helpers.isRPlus
|
||||
import org.fossify.commons.extensions.getAlertDialogBuilder
|
||||
import org.fossify.commons.extensions.getFilenameFromPath
|
||||
import org.fossify.commons.extensions.getParentPath
|
||||
import org.fossify.commons.extensions.getPicturesDirectoryPath
|
||||
import org.fossify.commons.extensions.hideKeyboard
|
||||
import org.fossify.commons.extensions.humanizePath
|
||||
import org.fossify.commons.extensions.isAValidFilename
|
||||
import org.fossify.commons.extensions.isInDownloadDir
|
||||
import org.fossify.commons.extensions.isRestrictedWithSAFSdk30
|
||||
import org.fossify.commons.extensions.setupDialogStuff
|
||||
import org.fossify.commons.extensions.showKeyboard
|
||||
import org.fossify.commons.extensions.toast
|
||||
import org.fossify.commons.extensions.value
|
||||
import org.fossify.gallery.databinding.DialogSaveAsBinding
|
||||
import java.io.File
|
||||
import org.fossify.gallery.extensions.ensureWritablePath
|
||||
|
||||
class SaveAsDialog(
|
||||
val activity: BaseSimpleActivity, val path: String, val appendFilename: Boolean, val cancelCallback: (() -> Unit)? = null,
|
||||
val activity: BaseSimpleActivity,
|
||||
val path: String,
|
||||
val appendFilename: Boolean,
|
||||
val cancelCallback: (() -> Unit)? = null,
|
||||
val callback: (savePath: String) -> Unit
|
||||
) {
|
||||
init {
|
||||
var realPath = path.getParentPath()
|
||||
if (activity.isRestrictedWithSAFSdk30(realPath) && !activity.isInDownloadDir(realPath)) {
|
||||
realPath = activity.getPicturesDirectoryPath(realPath)
|
||||
private val binding = DialogSaveAsBinding.inflate(activity.layoutInflater)
|
||||
private var realPath = path.getParentPath().run {
|
||||
if (activity.isRestrictedWithSAFSdk30(this) && !activity.isInDownloadDir(this)) {
|
||||
activity.getPicturesDirectoryPath(this)
|
||||
} else {
|
||||
this
|
||||
}
|
||||
}
|
||||
|
||||
val binding = DialogSaveAsBinding.inflate(activity.layoutInflater).apply {
|
||||
init {
|
||||
binding.apply {
|
||||
folderValue.setText("${activity.humanizePath(realPath).trimEnd('/')}/")
|
||||
|
||||
val fullName = path.getFilenameFromPath()
|
||||
@@ -39,7 +56,14 @@ class SaveAsDialog(
|
||||
filenameValue.setText(name)
|
||||
folderValue.setOnClickListener {
|
||||
activity.hideKeyboard(folderValue)
|
||||
FilePickerDialog(activity, realPath, false, false, true, true) {
|
||||
FilePickerDialog(
|
||||
activity = activity,
|
||||
currPath = realPath,
|
||||
pickFile = false,
|
||||
showHidden = false,
|
||||
showFAB = true,
|
||||
canAddShowHiddenButton = true
|
||||
) {
|
||||
folderValue.setText(activity.humanizePath(it))
|
||||
realPath = it
|
||||
}
|
||||
@@ -51,59 +75,47 @@ class SaveAsDialog(
|
||||
.setNegativeButton(org.fossify.commons.R.string.cancel) { dialog, which -> cancelCallback?.invoke() }
|
||||
.setOnCancelListener { cancelCallback?.invoke() }
|
||||
.apply {
|
||||
activity.setupDialogStuff(binding.root, this, org.fossify.commons.R.string.save_as) { alertDialog ->
|
||||
activity.setupDialogStuff(
|
||||
view = binding.root,
|
||||
dialog = this,
|
||||
titleId = org.fossify.commons.R.string.save_as
|
||||
) { alertDialog ->
|
||||
alertDialog.showKeyboard(binding.filenameValue)
|
||||
alertDialog.getButton(AlertDialog.BUTTON_POSITIVE).setOnClickListener {
|
||||
val filename = binding.filenameValue.value
|
||||
val extension = binding.extensionValue.value
|
||||
|
||||
if (filename.isEmpty()) {
|
||||
activity.toast(org.fossify.commons.R.string.filename_cannot_be_empty)
|
||||
return@setOnClickListener
|
||||
}
|
||||
|
||||
if (extension.isEmpty()) {
|
||||
activity.toast(org.fossify.commons.R.string.extension_cannot_be_empty)
|
||||
return@setOnClickListener
|
||||
}
|
||||
|
||||
val newFilename = "$filename.$extension"
|
||||
val newPath = "${realPath.trimEnd('/')}/$newFilename"
|
||||
if (!newFilename.isAValidFilename()) {
|
||||
activity.toast(org.fossify.commons.R.string.filename_invalid_characters)
|
||||
return@setOnClickListener
|
||||
}
|
||||
|
||||
if (activity.getDoesFilePathExist(newPath)) {
|
||||
val title = String.format(activity.getString(org.fossify.commons.R.string.file_already_exists_overwrite), newFilename)
|
||||
ConfirmationDialog(activity, title) {
|
||||
if ((isRPlus() && !isExternalStorageManager())) {
|
||||
val fileDirItem = arrayListOf(File(newPath).toFileDirItem(activity))
|
||||
val fileUris = activity.getFileUrisFromFileDirItems(fileDirItem)
|
||||
activity.updateSDK30Uris(fileUris) { success ->
|
||||
if (success) {
|
||||
selectPath(alertDialog, newPath)
|
||||
}
|
||||
}
|
||||
} else {
|
||||
selectPath(alertDialog, newPath)
|
||||
}
|
||||
}
|
||||
} else {
|
||||
selectPath(alertDialog, newPath)
|
||||
}
|
||||
validateAndConfirmPath(alertDialog::dismiss)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun selectPath(alertDialog: AlertDialog, newPath: String) {
|
||||
activity.handleSAFDialogSdk30(newPath) {
|
||||
if (!it) {
|
||||
return@handleSAFDialogSdk30
|
||||
}
|
||||
private fun validateAndConfirmPath(dismiss: () -> Unit) {
|
||||
val filename = binding.filenameValue.value
|
||||
val extension = binding.extensionValue.value
|
||||
|
||||
if (filename.isEmpty()) {
|
||||
activity.toast(org.fossify.commons.R.string.filename_cannot_be_empty)
|
||||
return
|
||||
}
|
||||
|
||||
if (extension.isEmpty()) {
|
||||
activity.toast(org.fossify.commons.R.string.extension_cannot_be_empty)
|
||||
return
|
||||
}
|
||||
|
||||
val newFilename = "$filename.$extension"
|
||||
val newPath = "${realPath.trimEnd('/')}/$newFilename"
|
||||
if (!newFilename.isAValidFilename()) {
|
||||
activity.toast(org.fossify.commons.R.string.filename_invalid_characters)
|
||||
return
|
||||
}
|
||||
|
||||
activity.ensureWritablePath(
|
||||
targetPath = newPath,
|
||||
confirmOverwrite = true,
|
||||
onCancel = cancelCallback
|
||||
) {
|
||||
callback(newPath)
|
||||
alertDialog.dismiss()
|
||||
dismiss()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -5,6 +5,7 @@ import android.content.ContentProviderOperation
|
||||
import android.content.ContentValues
|
||||
import android.content.Intent
|
||||
import android.graphics.Bitmap
|
||||
import android.graphics.Bitmap.CompressFormat
|
||||
import android.graphics.BitmapFactory
|
||||
import android.graphics.Matrix
|
||||
import android.graphics.Point
|
||||
@@ -29,6 +30,8 @@ import org.fossify.commons.activities.BaseSimpleActivity
|
||||
import org.fossify.commons.dialogs.ConfirmationDialog
|
||||
import org.fossify.commons.dialogs.SecurityDialog
|
||||
import org.fossify.commons.extensions.*
|
||||
import org.fossify.commons.extensions.getCurrentFormattedDateTime
|
||||
import org.fossify.commons.extensions.internalStoragePath
|
||||
import org.fossify.commons.helpers.*
|
||||
import org.fossify.commons.models.FAQItem
|
||||
import org.fossify.commons.models.FileDirItem
|
||||
@@ -43,10 +46,12 @@ import org.fossify.gallery.dialogs.ResizeMultipleImagesDialog
|
||||
import org.fossify.gallery.dialogs.ResizeWithPathDialog
|
||||
import org.fossify.gallery.helpers.DIRECTORY
|
||||
import org.fossify.gallery.helpers.RECYCLE_BIN
|
||||
import org.fossify.gallery.helpers.TEMP_FOLDER_NAME
|
||||
import org.fossify.gallery.models.DateTaken
|
||||
import java.io.*
|
||||
import java.text.SimpleDateFormat
|
||||
import java.util.Locale
|
||||
import androidx.core.net.toUri
|
||||
|
||||
fun Activity.sharePath(path: String) {
|
||||
sharePathIntent(path, BuildConfig.APPLICATION_ID)
|
||||
@@ -164,7 +169,7 @@ fun BaseSimpleActivity.launchGrantAllFilesIntent() {
|
||||
try {
|
||||
val intent = Intent(Settings.ACTION_MANAGE_APP_ALL_FILES_ACCESS_PERMISSION)
|
||||
intent.addCategory("android.intent.category.DEFAULT")
|
||||
intent.data = Uri.parse("package:$packageName")
|
||||
intent.data = "package:$packageName".toUri()
|
||||
startActivity(intent)
|
||||
} catch (e: Exception) {
|
||||
val intent = Intent()
|
||||
@@ -914,7 +919,7 @@ fun Activity.getShortcutImage(tmb: String, drawable: Drawable, callback: () -> U
|
||||
fun Activity.showFileOnMap(path: String) {
|
||||
val exif = try {
|
||||
if (path.startsWith("content://")) {
|
||||
ExifInterface(contentResolver.openInputStream(Uri.parse(path))!!)
|
||||
ExifInterface(contentResolver.openInputStream(path.toUri())!!)
|
||||
} else {
|
||||
ExifInterface(path)
|
||||
}
|
||||
@@ -949,3 +954,110 @@ fun Activity.openRecycleBin() {
|
||||
startActivity(this)
|
||||
}
|
||||
}
|
||||
|
||||
fun BaseSimpleActivity.writeBitmapToCache(
|
||||
source: Uri,
|
||||
bitmap: Bitmap,
|
||||
callback: (path: String?) -> Unit
|
||||
) {
|
||||
val bytes = ByteArrayOutputStream()
|
||||
bitmap.compress(CompressFormat.PNG, 0, bytes)
|
||||
|
||||
val folder = File(cacheDir, TEMP_FOLDER_NAME)
|
||||
if (!folder.exists()) {
|
||||
if (!folder.mkdir()) {
|
||||
callback(null)
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
val filename = applicationContext.getFilenameFromContentUri(source)
|
||||
?: "tmp-${System.currentTimeMillis()}.jpg"
|
||||
val newPath = "$folder/$filename"
|
||||
val fileDirItem = FileDirItem(newPath, filename)
|
||||
getFileOutputStream(fileDirItem, true) {
|
||||
if (it != null) {
|
||||
try {
|
||||
it.write(bytes.toByteArray())
|
||||
callback(newPath)
|
||||
} catch (_: Exception) {
|
||||
callback(null)
|
||||
} finally {
|
||||
it.close()
|
||||
}
|
||||
} else {
|
||||
callback(null)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fun BaseSimpleActivity.ensureWritablePath(
|
||||
targetPath: String,
|
||||
confirmOverwrite: Boolean = true,
|
||||
onCancel: (() -> Unit)? = null,
|
||||
callback: (String) -> Unit,
|
||||
) {
|
||||
fun proceedAfterGrants() {
|
||||
handleSAFDialogSdk30(targetPath) { granted ->
|
||||
if (!granted) {
|
||||
onCancel?.invoke()
|
||||
return@handleSAFDialogSdk30
|
||||
}
|
||||
callback(targetPath)
|
||||
}
|
||||
}
|
||||
|
||||
fun requestGrantsThenProceed() {
|
||||
if (isRPlus() && !isExternalStorageManager()) {
|
||||
val fileDirItem = arrayListOf(File(targetPath).toFileDirItem(this))
|
||||
val fileUris = getFileUrisFromFileDirItems(fileDirItem)
|
||||
updateSDK30Uris(fileUris) { success ->
|
||||
if (success) proceedAfterGrants() else onCancel?.invoke()
|
||||
}
|
||||
} else {
|
||||
proceedAfterGrants()
|
||||
}
|
||||
}
|
||||
|
||||
if (confirmOverwrite && getDoesFilePathExist(targetPath)) {
|
||||
val title = String.format(
|
||||
getString(org.fossify.commons.R.string.file_already_exists_overwrite),
|
||||
targetPath.getFilenameFromPath()
|
||||
)
|
||||
ConfirmationDialog(this, title) {
|
||||
requestGrantsThenProceed()
|
||||
}
|
||||
} else {
|
||||
requestGrantsThenProceed()
|
||||
}
|
||||
}
|
||||
|
||||
fun Activity.proposeNewFilePath(uri: Uri): Pair<String, Boolean> {
|
||||
var newPath = applicationContext.getRealPathFromURI(uri) ?: ""
|
||||
if (newPath.startsWith("/mnt/")) {
|
||||
newPath = ""
|
||||
}
|
||||
|
||||
var shouldAppendFilename = true
|
||||
if (newPath.isEmpty()) {
|
||||
val filename = applicationContext.getFilenameFromContentUri(uri) ?: ""
|
||||
if (filename.isNotEmpty()) {
|
||||
val path = if (intent.extras?.containsKey(REAL_FILE_PATH) == true) {
|
||||
intent.getStringExtra(REAL_FILE_PATH)?.getParentPath()
|
||||
} else {
|
||||
internalStoragePath
|
||||
}
|
||||
newPath = "$path/$filename"
|
||||
shouldAppendFilename = false
|
||||
}
|
||||
}
|
||||
|
||||
if (newPath.isEmpty()) {
|
||||
newPath = "$internalStoragePath/${getCurrentFormattedDateTime()}.${
|
||||
uri.toString().getFilenameExtension()
|
||||
}"
|
||||
shouldAppendFilename = false
|
||||
}
|
||||
|
||||
return Pair(newPath, shouldAppendFilename)
|
||||
}
|
||||
|
||||
@@ -7,9 +7,11 @@ import android.content.Context
|
||||
import android.content.Intent
|
||||
import android.database.Cursor
|
||||
import android.graphics.Bitmap
|
||||
import android.graphics.Bitmap.CompressFormat
|
||||
import android.graphics.drawable.Drawable
|
||||
import android.graphics.drawable.PictureDrawable
|
||||
import android.media.AudioManager
|
||||
import android.net.Uri
|
||||
import android.os.Process
|
||||
import android.provider.MediaStore.Files
|
||||
import android.provider.MediaStore.Images
|
||||
@@ -39,6 +41,7 @@ import org.fossify.commons.extensions.getDoesFilePathExist
|
||||
import org.fossify.commons.extensions.getDuration
|
||||
import org.fossify.commons.extensions.getFilenameFromPath
|
||||
import org.fossify.commons.extensions.getLongValue
|
||||
import org.fossify.commons.extensions.getMimeTypeFromUri
|
||||
import org.fossify.commons.extensions.getOTGPublicPath
|
||||
import org.fossify.commons.extensions.getParentPath
|
||||
import org.fossify.commons.extensions.getStringValue
|
||||
@@ -977,7 +980,7 @@ fun Context.getCachedMedia(
|
||||
}
|
||||
}) as ArrayList<Medium>
|
||||
|
||||
val pathToUse = if (path.isEmpty()) SHOW_ALL else path
|
||||
val pathToUse = path.ifEmpty { SHOW_ALL }
|
||||
mediaFetcher.sortMedia(media, config.getFolderSorting(pathToUse))
|
||||
val grouped = mediaFetcher.groupMedia(media, pathToUse)
|
||||
callback(grouped.clone() as ArrayList<ThumbnailItem>)
|
||||
@@ -1402,3 +1405,25 @@ fun Context.getFileDateTaken(path: String): Long {
|
||||
|
||||
return 0L
|
||||
}
|
||||
|
||||
fun Context.getCompressionFormatFromUri(uri: Uri): CompressFormat {
|
||||
val type = getMimeTypeFromUri(uri)
|
||||
return when {
|
||||
type.equals("image/png", true) -> CompressFormat.PNG
|
||||
type.equals("image/webp", true) -> CompressFormat.WEBP
|
||||
else -> CompressFormat.JPEG
|
||||
}
|
||||
}
|
||||
|
||||
fun Context.resolveUriScheme(
|
||||
uri: Uri,
|
||||
onPath: (String) -> Unit,
|
||||
onContentUri: (Uri) -> Unit,
|
||||
onUnknown: () -> Unit = { toast(R.string.unknown_file_location) }
|
||||
) {
|
||||
when (uri.scheme) {
|
||||
"file" -> onPath(uri.path!!)
|
||||
"content" -> onContentUri(uri)
|
||||
else -> onUnknown()
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,6 +1,10 @@
|
||||
package org.fossify.gallery.extensions
|
||||
|
||||
import android.content.Context
|
||||
import android.net.Uri
|
||||
import androidx.exifinterface.media.ExifInterface
|
||||
import java.io.File
|
||||
import java.io.InputStream
|
||||
|
||||
/**
|
||||
* A non-exhaustive list of all Exif attributes *excluding* dimension related attributes.
|
||||
@@ -141,3 +145,33 @@ fun ExifInterface.copyNonDimensionAttributesTo(destination: ExifInterface) {
|
||||
} catch (_: Exception) {
|
||||
}
|
||||
}
|
||||
|
||||
fun Context.readExif(uri: Uri): ExifInterface? {
|
||||
var inputStream: InputStream? = null
|
||||
return try {
|
||||
inputStream = contentResolver.openInputStream(uri)
|
||||
ExifInterface(inputStream!!)
|
||||
} catch (_: Exception) {
|
||||
null
|
||||
} finally {
|
||||
inputStream?.close()
|
||||
}
|
||||
}
|
||||
|
||||
fun Context.writeExif(exif: ExifInterface?, uri: Uri?) {
|
||||
if (exif == null || uri == null) return
|
||||
resolveUriScheme(
|
||||
uri = uri,
|
||||
onContentUri = {
|
||||
contentResolver.openFileDescriptor(it, "rw")?.use { pfd ->
|
||||
val destExif = ExifInterface(pfd.fileDescriptor)
|
||||
exif.copyNonDimensionAttributesTo(destExif)
|
||||
}
|
||||
},
|
||||
onPath = {
|
||||
val file = File(it)
|
||||
val destExif = ExifInterface(file.absolutePath)
|
||||
exif.copyNonDimensionAttributesTo(destExif)
|
||||
}
|
||||
)
|
||||
}
|
||||
@@ -3,6 +3,7 @@ package org.fossify.gallery.extensions
|
||||
import android.os.SystemClock
|
||||
import android.view.MotionEvent
|
||||
import android.view.View
|
||||
import org.fossify.commons.extensions.toast
|
||||
|
||||
fun View.sendFakeClick(x: Float, y: Float) {
|
||||
val uptime = SystemClock.uptimeMillis()
|
||||
@@ -11,3 +12,13 @@ fun View.sendFakeClick(x: Float, y: Float) {
|
||||
event.action = MotionEvent.ACTION_UP
|
||||
dispatchTouchEvent(event)
|
||||
}
|
||||
|
||||
fun View.showContentDescriptionOnLongClick() {
|
||||
setOnLongClickListener {
|
||||
val contentDescription = contentDescription
|
||||
if (contentDescription != null) {
|
||||
context.toast(contentDescription.toString())
|
||||
}
|
||||
true
|
||||
}
|
||||
}
|
||||
|
||||
@@ -159,6 +159,9 @@ const val SHOULD_INIT_FRAGMENT = "should_init_fragment"
|
||||
const val PORTRAIT_PATH = "portrait_path"
|
||||
const val SKIP_AUTHENTICATION = "skip_authentication"
|
||||
|
||||
// editor
|
||||
const val TEMP_FOLDER_NAME = "images"
|
||||
|
||||
// rotations
|
||||
const val ROTATE_BY_SYSTEM_SETTING = 0
|
||||
const val ROTATE_BY_DEVICE_ROTATION = 1
|
||||
|
||||
@@ -5,15 +5,20 @@
|
||||
android:id="@+id/save_as"
|
||||
android:icon="@drawable/ic_check_vector"
|
||||
android:title="@string/save_as"
|
||||
app:showAsAction="always" />
|
||||
app:showAsAction="ifRoom" />
|
||||
<item
|
||||
android:id="@+id/overwrite_original"
|
||||
android:icon="@drawable/ic_check_vector"
|
||||
android:title="@string/overwrite_original"
|
||||
app:showAsAction="never" />
|
||||
<item
|
||||
android:id="@+id/edit"
|
||||
android:icon="@drawable/ic_edit_vector"
|
||||
android:title="@string/edit_with"
|
||||
app:showAsAction="always" />
|
||||
app:showAsAction="never" />
|
||||
<item
|
||||
android:id="@+id/share"
|
||||
android:icon="@drawable/ic_share_vector"
|
||||
android:title="@string/share"
|
||||
app:showAsAction="always" />
|
||||
app:showAsAction="ifRoom" />
|
||||
</menu>
|
||||
|
||||
@@ -104,6 +104,7 @@
|
||||
<string name="no_image_editor_found">No image editor found</string>
|
||||
<string name="no_video_editor_found">No video editor found</string>
|
||||
<string name="unknown_file_location">Unknown file location</string>
|
||||
<string name="overwrite_original">Overwrite original</string>
|
||||
<string name="error_saving_file">Could not overwrite the source file</string>
|
||||
<string name="rotate_left">Rotate left</string>
|
||||
<string name="rotate_right">Rotate right</string>
|
||||
|
||||
@@ -40,6 +40,11 @@ style:
|
||||
maxLineLength: 120
|
||||
excludePackageStatements: true
|
||||
excludeImportStatements: true
|
||||
ReturnCount:
|
||||
active: true
|
||||
max: 4
|
||||
excludeGuardClauses: true
|
||||
excludes: ["**/test/**", "**/androidTest/**"]
|
||||
|
||||
naming:
|
||||
FunctionNaming:
|
||||
|
||||
Reference in New Issue
Block a user