embedded photo editor

This commit is contained in:
kosyak 2023-05-26 12:56:22 +03:00 committed by Konstantin Aleksashin
parent e1161bcb22
commit 7c5826c945
65 changed files with 3250 additions and 8 deletions

View file

@ -4,18 +4,22 @@ buildscript {
repositories { repositories {
google() google()
mavenCentral() mavenCentral()
maven { url "https://www.jitpack.io" }
} }
dependencies { dependencies {
classpath 'com.android.tools.build:gradle:7.4.2' classpath 'com.android.tools.build:gradle:7.4.2'
classpath "org.jetbrains.kotlin:kotlin-gradle-plugin:1.8.21"
} }
} }
apply plugin: 'com.android.application' apply plugin: 'com.android.application'
apply plugin: 'org.jetbrains.kotlin.android'
repositories { repositories {
google() google()
mavenCentral() mavenCentral()
jcenter() jcenter()
maven { url "https://www.jitpack.io" }
} }
configurations { configurations {
@ -46,7 +50,7 @@ dependencies {
implementation 'androidx.exifinterface:exifinterface:1.3.6' implementation 'androidx.exifinterface:exifinterface:1.3.6'
implementation 'androidx.cardview:cardview:1.0.0' implementation 'androidx.cardview:cardview:1.0.0'
implementation 'androidx.swiperefreshlayout:swiperefreshlayout:1.1.0' implementation 'androidx.swiperefreshlayout:swiperefreshlayout:1.1.0'
implementation 'com.google.android.material:material:1.8.0' implementation 'com.google.android.material:material:1.9.0'
implementation "androidx.emoji2:emoji2:1.2.0" implementation "androidx.emoji2:emoji2:1.2.0"
freeImplementation "androidx.emoji2:emoji2-bundled:1.2.0" freeImplementation "androidx.emoji2:emoji2-bundled:1.2.0"
@ -77,6 +81,12 @@ dependencies {
implementation 'com.google.guava:guava:31.1-android' implementation 'com.google.guava:guava:31.1-android'
quicksyImplementation 'io.michaelrocks:libphonenumber-android:8.12.49' quicksyImplementation 'io.michaelrocks:libphonenumber-android:8.12.49'
implementation 'im.conversations.webrtc:webrtc-android:104.0.0' implementation 'im.conversations.webrtc:webrtc-android:104.0.0'
implementation 'androidx.constraintlayout:constraintlayout:2.1.4'
implementation "androidx.recyclerview:recyclerview:1.2.1"
implementation 'com.github.bumptech.glide:glide:4.15.1'
implementation 'info.androidhive:imagefilters:1.0.7'
implementation 'com.github.chrisbanes:PhotoView:2.3.0'
} }
ext { ext {

View file

@ -306,6 +306,12 @@
android:name=".ui.MediaBrowserActivity" android:name=".ui.MediaBrowserActivity"
android:label="@string/media_browser" /> android:label="@string/media_browser" />
<activity
android:name="eu.siacs.conversations.medialib.activities.EditActivity"
android:theme="@style/ConversationsTheme.FullScreen"
android:configChanges="orientation"
android:exported="false"/>
<service android:name=".services.ExportBackupService" /> <service android:name=".services.ExportBackupService" />
<service android:name=".services.ImportBackupService" /> <service android:name=".services.ImportBackupService" />
<service <service

View file

@ -0,0 +1,739 @@
package eu.siacs.conversations.medialib.activities
import android.Manifest
import android.annotation.TargetApi
import android.app.Activity
import android.content.Intent
import android.content.pm.PackageManager
import android.graphics.Bitmap
import android.graphics.Bitmap.CompressFormat
import android.graphics.Color
import android.graphics.Point
import android.net.Uri
import android.os.Build
import android.os.Bundle
import android.os.Handler
import android.provider.MediaStore
import android.view.ViewGroup.MarginLayoutParams
import android.view.WindowInsets
import android.view.WindowManager
import android.widget.ImageView
import android.widget.RelativeLayout
import androidx.annotation.ColorInt
import androidx.appcompat.app.AppCompatActivity
import androidx.core.content.ContextCompat
import androidx.core.net.toUri
import androidx.core.view.marginTop
import androidx.exifinterface.media.ExifInterface
import androidx.recyclerview.widget.LinearLayoutManager
import com.bumptech.glide.Glide
import com.bumptech.glide.load.DataSource
import com.bumptech.glide.load.DecodeFormat
import com.bumptech.glide.load.engine.DiskCacheStrategy
import com.bumptech.glide.load.engine.GlideException
import com.bumptech.glide.request.RequestListener
import com.bumptech.glide.request.RequestOptions
import com.bumptech.glide.request.target.Target
import com.leinardi.android.speeddial.UiUtils.getPrimaryColor
import com.theartofdev.edmodo.cropper.CropImageView
import com.zomato.photofilters.FilterPack
import com.zomato.photofilters.imageprocessors.Filter
import eu.siacs.conversations.R
import eu.siacs.conversations.databinding.ActivityEditBinding
import eu.siacs.conversations.medialib.adapters.FiltersAdapter
import eu.siacs.conversations.medialib.dialogs.ColorPickerDialog
import eu.siacs.conversations.medialib.dialogs.OtherAspectRatioDialog
import eu.siacs.conversations.medialib.dialogs.ResizeDialog
import eu.siacs.conversations.medialib.extensions.*
import eu.siacs.conversations.medialib.helpers.*
import eu.siacs.conversations.medialib.models.FileDirItem
import eu.siacs.conversations.medialib.models.FilterItem
import java.io.*
import java.lang.Float.max
import java.util.UUID
class EditActivity : AppCompatActivity(), CropImageView.OnCropImageCompleteListener {
companion object {
const val KEY_CHAT_NAME = "editActivity_chatName"
const val KEY_EDITED_URI = "editActivity_edited_uri"
init {
System.loadLibrary("NativeImageProcessor")
}
}
private val ASPECT_X = "aspectX"
private val ASPECT_Y = "aspectY"
private val CROP = "crop"
// constants for bottom primary action groups
private val PRIMARY_ACTION_NONE = 0
private val PRIMARY_ACTION_FILTER = 1
private val PRIMARY_ACTION_CROP_ROTATE = 2
private val PRIMARY_ACTION_DRAW = 3
private val CROP_ROTATE_NONE = 0
private val CROP_ROTATE_ASPECT_RATIO = 1
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 currAspectRatio = ASPECT_RATIO_FREE
private var wasDrawCanvasPositioned = false
private var oldExif: ExifInterface? = null
private var filterInitialBitmap: Bitmap? = null
private var originalUri: Uri? = null
private lateinit var binding: ActivityEditBinding
private val launchSavingToastRunnable = Runnable {
toast(R.string.saving)
}
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
binding = ActivityEditBinding.inflate(layoutInflater)
val view = binding.root
setContentView(view)
supportActionBar?.hide();
binding.editorToolbar.title = intent.getStringExtra(KEY_CHAT_NAME)
binding.editorToolbar.setTitleTextColor(Color.WHITE)
binding.editorToolbar.setNavigationIconTint(Color.WHITE)
binding.editorToolbar.setNavigationOnClickListener {
onBackPressed()
}
binding.editorToolbar.inflateMenu(R.menu.menu_done)
binding.editorToolbar.setOnMenuItemClickListener {
when (it.itemId) {
R.id.action_done -> saveImage()
}
true
}
window.statusBarColor = ContextCompat.getColor(this, R.color.black26)
initEditActivity()
}
private fun initEditActivity() {
if (intent.data == null) {
toast(R.string.invalid_image_path)
finish()
return
}
uri = intent.data!!
originalUri = uri
if (uri!!.scheme != "file" && uri!!.scheme != "content") {
toast(R.string.unknown_file_location)
finish()
return
}
if (intent.extras?.containsKey(REAL_FILE_PATH) == true) {
val realPath = intent.extras!!.getString(REAL_FILE_PATH)
uri = when {
realPath!!.startsWith("file:/") -> Uri.parse(realPath)
else -> Uri.fromFile(File(realPath))
}
} else {
(getRealPathFromURI(uri!!))?.apply {
uri = Uri.fromFile(File(this))
}
}
loadDefaultImageView()
setupBottomActions()
if (config.lastEditorCropAspectRatio == ASPECT_RATIO_OTHER) {
if (config.lastEditorCropOtherAspectRatioX == 0f) {
config.lastEditorCropOtherAspectRatioX = 1f
}
if (config.lastEditorCropOtherAspectRatioY == 0f) {
config.lastEditorCropOtherAspectRatioY = 1f
}
lastOtherAspectRatio = Pair(config.lastEditorCropOtherAspectRatioX, config.lastEditorCropOtherAspectRatioY)
}
updateAspectRatio(config.lastEditorCropAspectRatio)
binding.cropImageView.guidelines = CropImageView.Guidelines.ON
binding.bottomAspectRatios.root.beVisible()
}
private fun loadDefaultImageView() {
binding.defaultImageView.beVisible()
binding.cropImageView.beGone()
binding.editorDrawCanvas.beGone()
val options = RequestOptions()
.skipMemoryCache(true)
.diskCacheStrategy(DiskCacheStrategy.NONE)
Glide.with(this)
.asBitmap()
.load(uri)
.apply(options)
.listener(object : RequestListener<Bitmap> {
override fun onLoadFailed(e: GlideException?, model: Any?, target: Target<Bitmap>?, isFirstResource: Boolean): Boolean {
if (uri != originalUri) {
uri = originalUri
Handler().post {
loadDefaultImageView()
}
}
return false
}
override fun onResourceReady(
bitmap: Bitmap?,
model: Any?,
target: Target<Bitmap>?,
dataSource: DataSource?,
isFirstResource: Boolean
): Boolean {
val currentFilter = getFiltersAdapter()?.getCurrentFilter()
if (filterInitialBitmap == null) {
loadCropImageView()
bottomCropRotateClicked()
}
if (filterInitialBitmap != null && currentFilter != null && currentFilter.filter.name != getString(R.string.none)) {
binding.defaultImageView.onGlobalLayout {
applyFilter(currentFilter)
}
} else {
filterInitialBitmap = bitmap
}
return false
}
}).into(binding.defaultImageView)
}
private fun loadCropImageView() {
binding.defaultImageView.beGone()
binding.editorDrawCanvas.beGone()
binding.cropImageView.apply {
beVisible()
setOnCropImageCompleteListener(this@EditActivity)
setImageUriAsync(uri)
guidelines = CropImageView.Guidelines.ON
}
}
private fun loadDrawCanvas() {
binding.defaultImageView.beGone()
binding.cropImageView.beGone()
binding.editorDrawCanvas.beVisible()
if (!wasDrawCanvasPositioned) {
wasDrawCanvasPositioned = true
binding.editorDrawCanvas.onGlobalLayout {
ensureBackgroundThread {
fillCanvasBackground()
}
}
}
}
private fun fillCanvasBackground() {
val size = Point()
windowManager.defaultDisplay.getSize(size)
val options = RequestOptions()
.format(DecodeFormat.PREFER_ARGB_8888)
.skipMemoryCache(true)
.diskCacheStrategy(DiskCacheStrategy.NONE)
.fitCenter()
try {
val builder = Glide.with(applicationContext)
.asBitmap()
.load(uri)
.apply(options)
.into(binding.editorDrawCanvas.width, binding.editorDrawCanvas.height)
val bitmap = builder.get()
runOnUiThread {
binding.editorDrawCanvas.apply {
updateBackgroundBitmap(bitmap)
layoutParams.width = bitmap.width
layoutParams.height = bitmap.height
android.util.Log.e("31fd", bitmap.height.toString() + " " + height)
translationY = max((height - bitmap.height) / 2f, 0f)
requestLayout()
}
}
} catch (e: Exception) {
showErrorToast(e)
}
}
@TargetApi(Build.VERSION_CODES.N)
private fun saveImage() {
setOldExif()
if (binding.cropImageView.isVisible()) {
binding.cropImageView.getCroppedImageAsync()
} else if (binding.editorDrawCanvas.isVisible()) {
val bitmap = binding.editorDrawCanvas.getBitmap()
saveBitmapToFile(bitmap, true)
} else {
val currentFilter = getFiltersAdapter()?.getCurrentFilter() ?: return
toast(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, false)
} catch (e: OutOfMemoryError) {
toast(R.string.out_of_memory_error)
}
}
}
}
@TargetApi(Build.VERSION_CODES.N)
private fun setOldExif() {
var inputStream: InputStream? = null
try {
if (isNougatPlus()) {
inputStream = contentResolver.openInputStream(uri!!)
oldExif = ExifInterface(inputStream!!)
}
} catch (e: Exception) {
} finally {
inputStream?.close()
}
}
private fun getFiltersAdapter() = binding.bottomEditorFilterActions.bottomActionsFilterList.adapter as? FiltersAdapter
private fun setupBottomActions() {
setupPrimaryActionButtons()
setupCropRotateActionButtons()
setupAspectRatioButtons()
setupDrawButtons()
}
private fun setupPrimaryActionButtons() {
binding.bottomEditorPrimaryActions.bottomPrimaryFilter.setOnClickListener {
bottomFilterClicked()
}
binding.bottomEditorPrimaryActions.bottomPrimaryCropRotate.setOnClickListener {
bottomCropRotateClicked()
}
binding.bottomEditorPrimaryActions.bottomPrimaryDraw.setOnClickListener {
bottomDrawClicked()
}
arrayOf(binding.bottomEditorPrimaryActions.bottomPrimaryFilter, binding.bottomEditorPrimaryActions.bottomPrimaryCropRotate, binding.bottomEditorPrimaryActions.bottomPrimaryDraw).forEach {
setupLongPress(it)
}
}
private fun bottomFilterClicked() {
currPrimaryAction = if (currPrimaryAction == PRIMARY_ACTION_FILTER) {
PRIMARY_ACTION_NONE
} else {
PRIMARY_ACTION_FILTER
}
updatePrimaryActionButtons()
}
private fun bottomCropRotateClicked() {
currPrimaryAction = if (currPrimaryAction == PRIMARY_ACTION_CROP_ROTATE) {
PRIMARY_ACTION_NONE
} else {
PRIMARY_ACTION_CROP_ROTATE
}
updatePrimaryActionButtons()
}
private fun bottomDrawClicked() {
currPrimaryAction = if (currPrimaryAction == PRIMARY_ACTION_DRAW) {
PRIMARY_ACTION_NONE
} else {
PRIMARY_ACTION_DRAW
}
updatePrimaryActionButtons()
}
private fun setupCropRotateActionButtons() {
binding.bottomEditorCropRotateActions.bottomRotate.setOnClickListener {
binding.cropImageView.rotateImage(90)
}
binding.bottomEditorCropRotateActions.bottomResize.setOnClickListener {
resizeImage()
}
binding.bottomEditorCropRotateActions.bottomFlipHorizontally.setOnClickListener {
binding.cropImageView.flipImageHorizontally()
}
binding.bottomEditorCropRotateActions.bottomFlipVertically.setOnClickListener {
binding.cropImageView.flipImageVertically()
}
binding.bottomEditorCropRotateActions.bottomAspectRatio.setOnClickListener {
currCropRotateAction = if (currCropRotateAction == CROP_ROTATE_ASPECT_RATIO) {
binding.cropImageView.guidelines = CropImageView.Guidelines.OFF
binding.bottomAspectRatios.root.beGone()
CROP_ROTATE_NONE
} else {
binding.cropImageView.guidelines = CropImageView.Guidelines.ON
binding.bottomAspectRatios.root.beVisible()
CROP_ROTATE_ASPECT_RATIO
}
updateCropRotateActionButtons()
}
arrayOf(binding.bottomEditorCropRotateActions.bottomRotate, binding.bottomEditorCropRotateActions.bottomResize, binding.bottomEditorCropRotateActions.bottomFlipHorizontally, binding.bottomEditorCropRotateActions.bottomFlipVertically, binding.bottomEditorCropRotateActions.bottomAspectRatio).forEach {
setupLongPress(it)
}
}
private fun setupAspectRatioButtons() {
binding.bottomAspectRatios.bottomAspectRatioFree.setOnClickListener {
updateAspectRatio(ASPECT_RATIO_FREE)
}
binding.bottomAspectRatios.bottomAspectRatioOneOne.setOnClickListener {
updateAspectRatio(ASPECT_RATIO_ONE_ONE)
}
binding.bottomAspectRatios.bottomAspectRatioFourThree.setOnClickListener {
updateAspectRatio(ASPECT_RATIO_FOUR_THREE)
}
binding.bottomAspectRatios.bottomAspectRatioSixteenNine.setOnClickListener {
updateAspectRatio(ASPECT_RATIO_SIXTEEN_NINE)
}
binding.bottomAspectRatios.bottomAspectRatioOther.setOnClickListener {
OtherAspectRatioDialog(this, lastOtherAspectRatio) {
lastOtherAspectRatio = it
config.lastEditorCropOtherAspectRatioX = it.first
config.lastEditorCropOtherAspectRatioY = it.second
updateAspectRatio(ASPECT_RATIO_OTHER)
}
}
updateAspectRatioButtons()
}
private fun setupDrawButtons() {
updateDrawColor(config.lastEditorDrawColor)
binding.bottomEditorDrawActions.bottomDrawWidth.progress = config.lastEditorBrushSize
updateBrushSize(config.lastEditorBrushSize)
binding.bottomEditorDrawActions.bottomDrawColorClickable.setOnClickListener {
ColorPickerDialog(this, drawColor) { wasPositivePressed, color ->
if (wasPositivePressed) {
updateDrawColor(color)
}
}
}
binding.bottomEditorDrawActions.bottomDrawWidth.onSeekBarChangeListener {
config.lastEditorBrushSize = it
updateBrushSize(it)
}
binding.bottomEditorDrawActions.bottomDrawUndo.setOnClickListener {
binding.editorDrawCanvas.undo()
}
}
private fun updateBrushSize(percent: Int) {
binding.editorDrawCanvas.updateBrushSize(percent)
val scale = Math.max(0.03f, percent / 100f)
binding.bottomEditorDrawActions.bottomDrawColor.scaleX = scale
binding.bottomEditorDrawActions.bottomDrawColor.scaleY = scale
}
private fun updatePrimaryActionButtons() {
if (binding.cropImageView.isGone() && currPrimaryAction == PRIMARY_ACTION_CROP_ROTATE) {
loadCropImageView()
} else if (binding.defaultImageView.isGone() && currPrimaryAction == PRIMARY_ACTION_FILTER) {
loadDefaultImageView()
} else if (binding.editorDrawCanvas.isGone() && currPrimaryAction == PRIMARY_ACTION_DRAW) {
loadDrawCanvas()
}
arrayOf(binding.bottomEditorPrimaryActions.bottomPrimaryFilter, binding.bottomEditorPrimaryActions.bottomPrimaryCropRotate, binding.bottomEditorPrimaryActions.bottomPrimaryDraw).forEach {
it.applyColorFilter(Color.WHITE)
}
val currentPrimaryActionButton = when (currPrimaryAction) {
PRIMARY_ACTION_FILTER -> binding.bottomEditorPrimaryActions.bottomPrimaryFilter
PRIMARY_ACTION_CROP_ROTATE -> binding.bottomEditorPrimaryActions.bottomPrimaryCropRotate
PRIMARY_ACTION_DRAW -> binding.bottomEditorPrimaryActions.bottomPrimaryDraw
else -> null
}
currentPrimaryActionButton?.applyColorFilter(getPrimaryColor(this))
binding.bottomEditorFilterActions
binding.bottomEditorFilterActions.root.beVisibleIf(currPrimaryAction == PRIMARY_ACTION_FILTER)
binding.bottomEditorCropRotateActions.root.beVisibleIf(currPrimaryAction == PRIMARY_ACTION_CROP_ROTATE)
binding.bottomEditorDrawActions.root.beVisibleIf(currPrimaryAction == PRIMARY_ACTION_DRAW)
if (currPrimaryAction == PRIMARY_ACTION_FILTER && binding.bottomEditorFilterActions.bottomActionsFilterList.adapter == null) {
ensureBackgroundThread {
val thumbnailSize = resources.getDimension(R.dimen.bottom_filters_thumbnail_size).toInt()
val bitmap = try {
Glide.with(this)
.asBitmap()
.load(uri).listener(object : RequestListener<Bitmap> {
override fun onLoadFailed(e: GlideException?, model: Any?, target: Target<Bitmap>?, isFirstResource: Boolean): Boolean {
showErrorToast(e.toString())
return false
}
override fun onResourceReady(
resource: Bitmap?,
model: Any?,
target: Target<Bitmap>?,
dataSource: DataSource?,
isFirstResource: Boolean
) = false
})
.submit(thumbnailSize, thumbnailSize)
.get()
} catch (e: GlideException) {
showErrorToast(e)
finish()
return@ensureBackgroundThread
}
runOnUiThread {
val filterThumbnailsManager = FilterThumbnailsManager()
filterThumbnailsManager.clearThumbs()
val noFilter = Filter(getString(R.string.none))
filterThumbnailsManager.addThumb(FilterItem(bitmap, noFilter))
FilterPack.getFilterPack(this).forEach {
val filterItem = FilterItem(bitmap, it)
filterThumbnailsManager.addThumb(filterItem)
}
val filterItems = filterThumbnailsManager.processThumbs()
val adapter = FiltersAdapter(applicationContext, filterItems) {
val layoutManager = binding.bottomEditorFilterActions.bottomActionsFilterList.layoutManager as LinearLayoutManager
applyFilter(filterItems[it])
if (it == layoutManager.findLastCompletelyVisibleItemPosition() || it == layoutManager.findLastVisibleItemPosition()) {
binding.bottomEditorFilterActions.bottomActionsFilterList.smoothScrollBy(thumbnailSize, 0)
} else if (it == layoutManager.findFirstCompletelyVisibleItemPosition() || it == layoutManager.findFirstVisibleItemPosition()) {
binding.bottomEditorFilterActions.bottomActionsFilterList.smoothScrollBy(-thumbnailSize, 0)
}
}
binding.bottomEditorFilterActions.bottomActionsFilterList.adapter = adapter
adapter.notifyDataSetChanged()
}
}
}
if (currPrimaryAction != PRIMARY_ACTION_CROP_ROTATE) {
binding.bottomAspectRatios.root.beGone()
currCropRotateAction = CROP_ROTATE_NONE
}
updateCropRotateActionButtons()
}
private fun applyFilter(filterItem: FilterItem) {
val newBitmap = Bitmap.createBitmap(filterInitialBitmap!!)
binding.defaultImageView.setImageBitmap(filterItem.filter.processFilter(newBitmap))
}
private fun updateAspectRatio(aspectRatio: Int) {
currAspectRatio = aspectRatio
config.lastEditorCropAspectRatio = aspectRatio
updateAspectRatioButtons()
binding.cropImageView.apply {
if (aspectRatio == ASPECT_RATIO_FREE) {
setFixedAspectRatio(false)
} else {
val newAspectRatio = when (aspectRatio) {
ASPECT_RATIO_ONE_ONE -> Pair(1f, 1f)
ASPECT_RATIO_FOUR_THREE -> Pair(4f, 3f)
ASPECT_RATIO_SIXTEEN_NINE -> Pair(16f, 9f)
else -> Pair(lastOtherAspectRatio!!.first, lastOtherAspectRatio!!.second)
}
setAspectRatio(newAspectRatio.first.toInt(), newAspectRatio.second.toInt())
}
}
}
private fun updateAspectRatioButtons() {
arrayOf(
binding.bottomAspectRatios.bottomAspectRatioFree,
binding.bottomAspectRatios.bottomAspectRatioOneOne,
binding.bottomAspectRatios.bottomAspectRatioFourThree,
binding.bottomAspectRatios.bottomAspectRatioSixteenNine,
binding.bottomAspectRatios.bottomAspectRatioOther,
).forEach {
it.setTextColor(Color.WHITE)
}
val currentAspectRatioButton = when (currAspectRatio) {
ASPECT_RATIO_FREE -> binding.bottomAspectRatios.bottomAspectRatioFree
ASPECT_RATIO_ONE_ONE -> binding.bottomAspectRatios.bottomAspectRatioOneOne
ASPECT_RATIO_FOUR_THREE -> binding.bottomAspectRatios.bottomAspectRatioFourThree
ASPECT_RATIO_SIXTEEN_NINE -> binding.bottomAspectRatios.bottomAspectRatioSixteenNine
else -> binding.bottomAspectRatios.bottomAspectRatioOther
}
currentAspectRatioButton.setTextColor(getPrimaryColor(this))
}
private fun updateCropRotateActionButtons() {
arrayOf(binding.bottomEditorCropRotateActions.bottomAspectRatio).forEach {
it.applyColorFilter(Color.WHITE)
}
val primaryActionView = when (currCropRotateAction) {
CROP_ROTATE_ASPECT_RATIO -> binding.bottomEditorCropRotateActions.bottomAspectRatio
else -> null
}
primaryActionView?.applyColorFilter(getPrimaryColor(this))
}
private fun updateDrawColor(color: Int) {
drawColor = color
binding.bottomEditorDrawActions.bottomDrawColor.applyColorFilter(color)
config.lastEditorDrawColor = color
binding.editorDrawCanvas.updateColor(color)
}
private fun resizeImage() {
val point = getAreaSize()
if (point == null) {
toast(R.string.unknown_error_occurred)
return
}
ResizeDialog(this, point) {
resizeWidth = it.x
resizeHeight = it.y
binding.cropImageView.getCroppedImageAsync()
}
}
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)
} else {
false
}
}
private fun getAreaSize(): Point? {
val rect = binding.cropImageView.cropRect ?: return null
val rotation = binding.cropImageView.rotatedDegrees
return if (rotation == 0 || rotation == 180) {
Point(rect.width(), rect.height())
} else {
Point(rect.height(), rect.width())
}
}
override fun onCropImageComplete(view: CropImageView, result: CropImageView.CropResult) {
if (result.error == null) {
setOldExif()
val bitmap = result.bitmap
saveBitmapToFile(bitmap, true)
} else {
toast("${getString(R.string.image_editing_failed)}: ${result.error.message}")
}
}
private fun saveBitmapToFile(bitmap: Bitmap, showSavingToast: Boolean) {
val file = File(cacheDir, "editedImages/${UUID.randomUUID()}.jpg")
file.deleteRecursively()
file.parentFile?.mkdirs()
try {
ensureBackgroundThread {
try {
val out = FileOutputStream(file)
saveBitmap(file, bitmap, out, showSavingToast)
} catch (e: Exception) {
toast(R.string.image_editing_failed)
}
}
} catch (e: Exception) {
showErrorToast(e)
} catch (e: OutOfMemoryError) {
toast(R.string.out_of_memory_error)
}
}
@TargetApi(Build.VERSION_CODES.N)
private fun saveBitmap(file: File, bitmap: Bitmap, out: OutputStream, showSavingToast: Boolean) {
if (showSavingToast) {
binding.root.postDelayed(launchSavingToastRunnable, 500)
}
if (resizeWidth > 0 && resizeHeight > 0) {
val resized = Bitmap.createScaledBitmap(bitmap, resizeWidth, resizeHeight, false)
resized.compress(file.absolutePath.getCompressionFormat(), 90, out)
} else {
bitmap.compress(file.absolutePath.getCompressionFormat(), 90, out)
}
try {
if (isNougatPlus()) {
val newExif = ExifInterface(file.absolutePath)
oldExif?.copyNonDimensionAttributesTo(newExif)
}
} catch (e: Exception) {
}
intent.putExtra(KEY_EDITED_URI, file.toUri())
setResult(Activity.RESULT_OK, intent)
out.close()
binding.root.removeCallbacks(launchSavingToastRunnable)
finish()
}
private fun setupLongPress(view: ImageView) {
view.setOnLongClickListener {
val contentDescription = view.contentDescription
if (contentDescription != null) {
toast(contentDescription.toString())
}
true
}
}
}

View file

@ -0,0 +1,60 @@
package eu.siacs.conversations.medialib.adapters
import android.content.Context
import android.view.LayoutInflater
import android.view.View
import android.view.ViewGroup
import androidx.recyclerview.widget.RecyclerView
import eu.siacs.conversations.R
import eu.siacs.conversations.databinding.EditorFilterItemBinding
import eu.siacs.conversations.medialib.models.FilterItem
class FiltersAdapter(val context: Context, val filterItems: ArrayList<FilterItem>, val itemClick: (Int) -> Unit) :
RecyclerView.Adapter<FiltersAdapter.ViewHolder>() {
private var currentSelection = filterItems.first()
private var strokeBackground = context.resources.getDrawable(R.drawable.stroke_background)
override fun onBindViewHolder(holder: ViewHolder, position: Int) {
holder.bindView(filterItems[position])
}
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): ViewHolder {
val view = LayoutInflater.from(parent.context).inflate(R.layout.editor_filter_item, parent, false)
return ViewHolder(view)
}
override fun getItemCount() = filterItems.size
fun getCurrentFilter() = currentSelection
private fun setCurrentFilter(position: Int) {
val filterItem = filterItems.getOrNull(position) ?: return
if (currentSelection != filterItem) {
currentSelection = filterItem
notifyDataSetChanged()
itemClick.invoke(position)
}
}
inner class ViewHolder(view: View) : RecyclerView.ViewHolder(view) {
private val binding = EditorFilterItemBinding.bind(view)
fun bindView(filterItem: FilterItem): View {
itemView.apply {
binding.editorFilterItemLabel.text = filterItem.filter.name
binding.editorFilterItemThumbnail.setImageBitmap(filterItem.bitmap)
binding.editorFilterItemThumbnail.background = if (getCurrentFilter() == filterItem) {
strokeBackground
} else {
null
}
setOnClickListener {
setCurrentFilter(adapterPosition)
}
}
return itemView
}
}
}

View file

@ -0,0 +1,254 @@
package eu.siacs.conversations.medialib.dialogs
import android.app.Activity
import android.graphics.Color
import android.view.MotionEvent
import android.view.View
import android.view.View.OnTouchListener
import android.view.ViewGroup
import android.view.WindowManager
import android.widget.EditText
import android.widget.ImageView
import androidx.appcompat.app.AlertDialog
import com.google.android.material.dialog.MaterialAlertDialogBuilder
import eu.siacs.conversations.R
import eu.siacs.conversations.databinding.DialogColorPickerBinding
import eu.siacs.conversations.medialib.extensions.*
import eu.siacs.conversations.medialib.views.ColorPickerSquare
import java.util.LinkedList
private const val RECENT_COLORS_NUMBER = 5
// forked from https://github.com/yukuku/ambilwarna
class ColorPickerDialog(
val activity: Activity,
color: Int,
val removeDimmedBackground: Boolean = false,
val addDefaultColorButton: Boolean = false,
val currentColorCallback: ((color: Int) -> Unit)? = null,
val callback: (wasPositivePressed: Boolean, color: Int) -> Unit
) {
var viewHue: View
var viewSatVal: ColorPickerSquare
var viewCursor: ImageView
var viewNewColor: ImageView
var viewTarget: ImageView
var newHexField: EditText
var viewContainer: ViewGroup
private val baseConfig = activity.config
private val currentColorHsv = FloatArray(3)
private val backgroundColor = Color.BLACK
private var isHueBeingDragged = false
private var wasDimmedBackgroundRemoved = false
private var dialog: AlertDialog? = null
init {
Color.colorToHSV(color, currentColorHsv)
val binding = DialogColorPickerBinding.inflate(activity.layoutInflater)
val view = binding.root.apply {
if (isQPlus()) {
isForceDarkAllowed = false
}
viewHue = binding.colorPickerHue
viewSatVal = binding.colorPickerSquare
viewCursor = binding.colorPickerCursor
viewNewColor = binding.colorPickerNewColor
viewTarget = binding.colorPickerCursor
viewContainer = binding.colorPickerHolder
newHexField = binding.colorPickerNewHex
viewSatVal.setHue(getHue())
viewNewColor.setFillWithStroke(getColor(), backgroundColor)
binding.colorPickerOldColor.setFillWithStroke(color, backgroundColor)
val hexCode = getHexCode(color)
binding.colorPickerOldHex.text = "#$hexCode"
binding.colorPickerOldHex.setOnLongClickListener {
activity.copyToClipboard(hexCode)
true
}
newHexField.setText(hexCode)
setupRecentColors(binding)
}
viewHue.setOnTouchListener(OnTouchListener { v, event ->
if (event.action == MotionEvent.ACTION_DOWN) {
isHueBeingDragged = true
}
if (event.action == MotionEvent.ACTION_MOVE || event.action == MotionEvent.ACTION_DOWN || event.action == MotionEvent.ACTION_UP) {
var y = event.y
if (y < 0f)
y = 0f
if (y > viewHue.measuredHeight) {
y = viewHue.measuredHeight - 0.001f // to avoid jumping the cursor from bottom to top.
}
var hue = 360f - 360f / viewHue.measuredHeight * y
if (hue == 360f)
hue = 0f
currentColorHsv[0] = hue
updateHue()
newHexField.setText(getHexCode(getColor()))
if (event.action == MotionEvent.ACTION_UP) {
isHueBeingDragged = false
}
return@OnTouchListener true
}
false
})
viewSatVal.setOnTouchListener(OnTouchListener { v, event ->
if (event.action == MotionEvent.ACTION_MOVE || event.action == MotionEvent.ACTION_DOWN || event.action == MotionEvent.ACTION_UP) {
var x = event.x
var y = event.y
if (x < 0f)
x = 0f
if (x > viewSatVal.measuredWidth)
x = viewSatVal.measuredWidth.toFloat()
if (y < 0f)
y = 0f
if (y > viewSatVal.measuredHeight)
y = viewSatVal.measuredHeight.toFloat()
currentColorHsv[1] = 1f / viewSatVal.measuredWidth * x
currentColorHsv[2] = 1f - 1f / viewSatVal.measuredHeight * y
moveColorPicker()
viewNewColor.setFillWithStroke(getColor(), backgroundColor)
newHexField.setText(getHexCode(getColor()))
return@OnTouchListener true
}
false
})
newHexField.onTextChangeListener {
if (it.length == 6 && !isHueBeingDragged) {
try {
val newColor = Color.parseColor("#$it")
Color.colorToHSV(newColor, currentColorHsv)
updateHue()
moveColorPicker()
} catch (ignored: Exception) {
}
}
}
// val textColor = activity.getProperTextColor()
val builder = MaterialAlertDialogBuilder(activity)
.setPositiveButton(R.string.ok) { _, _ -> confirmNewColor() }
.setNegativeButton(R.string.cancel) { _, _ -> dialogDismissed() }
.setOnCancelListener { dialogDismissed() }
.apply {
if (addDefaultColorButton) {
setNeutralButton(R.string.default_color) { _, _ -> confirmDefaultColor() }
}
}
builder.apply {
activity.setupDialogStuff(view, this) { alertDialog ->
dialog = alertDialog
//view.color_picker_arrow.applyColorFilter(textColor)
//view.color_picker_hex_arrow.applyColorFilter(textColor)
// viewCursor.applyColorFilter(textColor)
}
}
view.onGlobalLayout {
moveHuePicker()
moveColorPicker()
}
}
private fun View.setupRecentColors(binding: DialogColorPickerBinding) {
val recentColors = baseConfig.colorPickerRecentColors
if (recentColors.isNotEmpty()) {
binding.recentColors.beVisible()
val squareSize = context.resources.getDimensionPixelSize(R.dimen.colorpicker_hue_width)
recentColors.take(RECENT_COLORS_NUMBER).forEach { recentColor ->
val recentColorView = ImageView(context)
recentColorView.id = View.generateViewId()
recentColorView.layoutParams = ViewGroup.LayoutParams(squareSize, squareSize)
recentColorView.setFillWithStroke(recentColor, backgroundColor)
recentColorView.setOnClickListener { newHexField.setText(getHexCode(recentColor)) }
binding.recentColors.addView(recentColorView)
binding.recentColorsFlow.addView(recentColorView)
}
}
}
private fun dialogDismissed() {
callback(false, 0)
}
private fun confirmDefaultColor() {
callback(true, 0)
}
private fun confirmNewColor() {
val hexValue = newHexField.value
val newColor = if (hexValue.length == 6) {
Color.parseColor("#$hexValue")
} else {
getColor()
}
addRecentColor(newColor)
callback(true, newColor)
}
private fun addRecentColor(color: Int) {
var recentColors = baseConfig.colorPickerRecentColors
recentColors.remove(color)
if (recentColors.size >= RECENT_COLORS_NUMBER) {
val numberOfColorsToDrop = recentColors.size - RECENT_COLORS_NUMBER + 1
recentColors = LinkedList(recentColors.dropLast(numberOfColorsToDrop))
}
recentColors.addFirst(color)
baseConfig.colorPickerRecentColors = recentColors
}
private fun getHexCode(color: Int) = color.toHex().substring(1)
private fun updateHue() {
viewSatVal.setHue(getHue())
moveHuePicker()
viewNewColor.setFillWithStroke(getColor(), backgroundColor)
if (removeDimmedBackground && !wasDimmedBackgroundRemoved) {
dialog?.window?.clearFlags(WindowManager.LayoutParams.FLAG_DIM_BEHIND)
wasDimmedBackgroundRemoved = true
}
currentColorCallback?.invoke(getColor())
}
private fun moveHuePicker() {
var y = viewHue.measuredHeight - getHue() * viewHue.measuredHeight / 360f
if (y == viewHue.measuredHeight.toFloat())
y = 0f
viewCursor.x = (viewHue.left - viewCursor.width).toFloat()
viewCursor.y = viewHue.top + y - viewCursor.height / 2
}
private fun moveColorPicker() {
val x = getSat() * viewSatVal.measuredWidth
val y = (1f - getVal()) * viewSatVal.measuredHeight
viewTarget.x = viewSatVal.left + x - viewTarget.width / 2
viewTarget.y = viewSatVal.top + y - viewTarget.height / 2
}
private fun getColor() = Color.HSVToColor(currentColorHsv)
private fun getHue() = currentColorHsv[0]
private fun getSat() = currentColorHsv[1]
private fun getVal() = currentColorHsv[2]
}

View file

@ -0,0 +1,44 @@
package eu.siacs.conversations.medialib.dialogs
import android.app.Activity
import android.widget.EditText
import androidx.appcompat.app.AlertDialog
import androidx.appcompat.app.AppCompatActivity
import com.google.android.material.dialog.MaterialAlertDialogBuilder
import eu.siacs.conversations.R
import eu.siacs.conversations.databinding.DialogCustomAspectRatioBinding
import eu.siacs.conversations.medialib.extensions.setupDialogStuff
import eu.siacs.conversations.medialib.extensions.showKeyboard
import eu.siacs.conversations.medialib.extensions.value
class CustomAspectRatioDialog(
val activity: Activity, val defaultCustomAspectRatio: Pair<Float, Float>?, val callback: (aspectRatio: Pair<Float, Float>) -> Unit
) {
init {
val binding = DialogCustomAspectRatioBinding.inflate(activity.layoutInflater)
val view = binding.root.apply {
binding.aspectRatioWidth.setText(defaultCustomAspectRatio?.first?.toInt()?.toString() ?: "")
binding.aspectRatioHeight.setText(defaultCustomAspectRatio?.second?.toInt()?.toString() ?: "")
}
MaterialAlertDialogBuilder(activity)
.setPositiveButton(R.string.ok, null)
.setNegativeButton(R.string.cancel, null)
.apply {
activity.setupDialogStuff(view, this) { alertDialog ->
alertDialog.showKeyboard(binding.aspectRatioWidth)
alertDialog.getButton(AlertDialog.BUTTON_POSITIVE).setOnClickListener {
val width = getViewValue(binding.aspectRatioWidth)
val height = getViewValue(binding.aspectRatioHeight)
callback(Pair(width, height))
alertDialog.dismiss()
}
}
}
}
private fun getViewValue(view: EditText): Float {
val textValue = view.value
return if (textValue.isEmpty()) 0f else textValue.toFloat()
}
}

View file

@ -0,0 +1,82 @@
package eu.siacs.conversations.medialib.dialogs
import android.app.Activity
import androidx.appcompat.app.AlertDialog
import com.google.android.material.dialog.MaterialAlertDialogBuilder
import eu.siacs.conversations.R
import eu.siacs.conversations.databinding.DialogOtherAspectRatioBinding
import eu.siacs.conversations.medialib.extensions.setupDialogStuff
class OtherAspectRatioDialog(
val activity: Activity,
val lastOtherAspectRatio: Pair<Float, Float>?,
val callback: (aspectRatio: Pair<Float, Float>) -> Unit
) {
private var dialog: AlertDialog? = null
init {
val binding = DialogOtherAspectRatioBinding.inflate(activity.layoutInflater)
val view = binding.root.apply {
binding.otherAspectRatio21.setOnClickListener { ratioPicked(Pair(2f, 1f)) }
binding.otherAspectRatio32.setOnClickListener { ratioPicked(Pair(3f, 2f)) }
binding.otherAspectRatio43.setOnClickListener { ratioPicked(Pair(4f, 3f)) }
binding.otherAspectRatio53.setOnClickListener { ratioPicked(Pair(5f, 3f)) }
binding.otherAspectRatio169.setOnClickListener { ratioPicked(Pair(16f, 9f)) }
binding.otherAspectRatio199.setOnClickListener { ratioPicked(Pair(19f, 9f)) }
binding.otherAspectRatioCustom.setOnClickListener { customRatioPicked() }
binding.otherAspectRatio12.setOnClickListener { ratioPicked(Pair(1f, 2f)) }
binding.otherAspectRatio23.setOnClickListener { ratioPicked(Pair(2f, 3f)) }
binding.otherAspectRatio34.setOnClickListener { ratioPicked(Pair(3f, 4f)) }
binding.otherAspectRatio35.setOnClickListener { ratioPicked(Pair(3f, 5f)) }
binding.otherAspectRatio916.setOnClickListener { ratioPicked(Pair(9f, 16f)) }
binding.otherAspectRatio919.setOnClickListener { ratioPicked(Pair(9f, 19f)) }
val radio1SelectedItemId = when (lastOtherAspectRatio) {
Pair(2f, 1f) -> binding.otherAspectRatio21.id
Pair(3f, 2f) -> binding.otherAspectRatio32.id
Pair(4f, 3f) -> binding.otherAspectRatio43.id
Pair(5f, 3f) -> binding.otherAspectRatio53.id
Pair(16f, 9f) -> binding.otherAspectRatio169.id
Pair(19f, 9f) -> binding.otherAspectRatio199.id
else -> 0
}
binding.otherAspectRatioDialogRadio1.check(radio1SelectedItemId)
val radio2SelectedItemId = when (lastOtherAspectRatio) {
Pair(1f, 2f) -> binding.otherAspectRatio12.id
Pair(2f, 3f) -> binding.otherAspectRatio23.id
Pair(3f, 4f) -> binding.otherAspectRatio34.id
Pair(3f, 5f) -> binding.otherAspectRatio35.id
Pair(9f, 16f) -> binding.otherAspectRatio916.id
Pair(9f, 19f) -> binding.otherAspectRatio919.id
else -> 0
}
binding.otherAspectRatioDialogRadio2.check(radio2SelectedItemId)
if (radio1SelectedItemId == 0 && radio2SelectedItemId == 0) {
binding.otherAspectRatioDialogRadio1.check(binding.otherAspectRatioCustom.id)
}
}
MaterialAlertDialogBuilder(activity)
.setNegativeButton(R.string.cancel, null)
.apply {
activity.setupDialogStuff(view, this) { alertDialog ->
dialog = alertDialog
}
}
}
private fun customRatioPicked() {
CustomAspectRatioDialog(activity, lastOtherAspectRatio) {
callback(it)
dialog?.dismiss()
}
}
private fun ratioPicked(pair: Pair<Float, Float>) {
callback(pair)
dialog?.dismiss()
}
}

View file

@ -0,0 +1,78 @@
package eu.siacs.conversations.medialib.dialogs
import android.app.Activity
import android.graphics.Point
import android.widget.EditText
import androidx.appcompat.app.AlertDialog
import com.google.android.material.dialog.MaterialAlertDialogBuilder
import eu.siacs.conversations.R
import eu.siacs.conversations.databinding.DialogResizeImageBinding
import eu.siacs.conversations.medialib.extensions.*
class ResizeDialog(val activity: Activity, val size: Point, val callback: (newSize: Point) -> Unit) {
init {
val binding = DialogResizeImageBinding.inflate(activity.layoutInflater)
val view = binding.root
val widthView = binding.resizeImageWidth
val heightView = binding.resizeImageHeight
widthView.setText(size.x.toString())
heightView.setText(size.y.toString())
val ratio = size.x / size.y.toFloat()
widthView.onTextChangeListener {
if (widthView.hasFocus()) {
var width = getViewValue(widthView)
if (width > size.x) {
widthView.setText(size.x.toString())
width = size.x
}
if (binding.keepAspectRatio.isChecked) {
heightView.setText((width / ratio).toInt().toString())
}
}
}
heightView.onTextChangeListener {
if (heightView.hasFocus()) {
var height = getViewValue(heightView)
if (height > size.y) {
heightView.setText(size.y.toString())
height = size.y
}
if (binding.keepAspectRatio.isChecked) {
widthView.setText((height * ratio).toInt().toString())
}
}
}
MaterialAlertDialogBuilder(activity)
.setPositiveButton(R.string.ok, null)
.setNegativeButton(R.string.cancel, null)
.apply {
activity.setupDialogStuff(view, this, R.string.resize_and_save) { alertDialog ->
alertDialog.showKeyboard(binding.resizeImageWidth)
alertDialog.getButton(AlertDialog.BUTTON_POSITIVE).setOnClickListener {
val width = getViewValue(widthView)
val height = getViewValue(heightView)
if (width <= 0 || height <= 0) {
activity.toast(R.string.invalid_values)
return@setOnClickListener
}
val newSize = Point(getViewValue(widthView), getViewValue(heightView))
callback(newSize)
alertDialog.dismiss()
}
}
}
}
private fun getViewValue(view: EditText): Int {
val textValue = view.value
return if (textValue.isEmpty()) 0 else textValue.toInt()
}
}

View file

@ -0,0 +1,39 @@
package eu.siacs.conversations.medialib.extensions
import android.app.Activity
import android.view.View
import androidx.appcompat.app.AlertDialog
import eu.siacs.conversations.R
import eu.siacs.conversations.medialib.models.FileDirItem
import java.io.File
import java.io.FileNotFoundException
import java.io.OutputStream
fun Activity.setupDialogStuff(
view: View,
dialog: AlertDialog.Builder,
titleId: Int = 0,
titleText: String = "",
cancelOnTouchOutside: Boolean = true,
callback: ((alertDialog: AlertDialog) -> Unit)? = null
) {
if (isDestroyed || isFinishing) {
return
}
dialog.create().apply {
if (titleId != 0) {
setTitle(titleId)
} else if (titleText.isNotEmpty()) {
setTitle(titleText)
}
setView(view)
setCancelable(cancelOnTouchOutside)
if (!isFinishing) {
show()
}
callback?.invoke(this)
}
}

View file

@ -0,0 +1,20 @@
package eu.siacs.conversations.medialib.extensions
import android.view.WindowManager
import androidx.appcompat.app.AlertDialog
import androidx.appcompat.widget.AppCompatEditText
// in dialogs, lets use findViewById, because while some dialogs use MyEditText, material theme dialogs use TextInputEditText so the system takes care of it
fun AlertDialog.showKeyboard(editText: AppCompatEditText) {
window!!.setSoftInputMode(WindowManager.LayoutParams.SOFT_INPUT_STATE_ALWAYS_VISIBLE)
editText.apply {
requestFocus()
onGlobalLayout {
setSelection(text.toString().length)
}
}
}
fun AlertDialog.hideKeyboard() {
window!!.setSoftInputMode(WindowManager.LayoutParams.SOFT_INPUT_STATE_ALWAYS_HIDDEN)
}

View file

@ -0,0 +1,42 @@
package eu.siacs.conversations.medialib.extensions
import android.os.Build
import androidx.annotation.ChecksSdkIntAtLeast
@ChecksSdkIntAtLeast(api = Build.VERSION_CODES.N)
fun isNougatPlus() = Build.VERSION.SDK_INT >= Build.VERSION_CODES.N
@ChecksSdkIntAtLeast(api = Build.VERSION_CODES.N_MR1)
fun isNougatMR1Plus() = Build.VERSION.SDK_INT >= Build.VERSION_CODES.N_MR1
@ChecksSdkIntAtLeast(api = Build.VERSION_CODES.O)
fun isOreoPlus() = Build.VERSION.SDK_INT >= Build.VERSION_CODES.O
@ChecksSdkIntAtLeast(api = Build.VERSION_CODES.O_MR1)
fun isOreoMr1Plus() = Build.VERSION.SDK_INT >= Build.VERSION_CODES.O_MR1
@ChecksSdkIntAtLeast(api = Build.VERSION_CODES.P)
fun isPiePlus() = Build.VERSION.SDK_INT >= Build.VERSION_CODES.P
@ChecksSdkIntAtLeast(api = Build.VERSION_CODES.Q)
fun isQPlus() = Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q
@ChecksSdkIntAtLeast(api = Build.VERSION_CODES.R)
fun isRPlus() = Build.VERSION.SDK_INT >= Build.VERSION_CODES.R
@ChecksSdkIntAtLeast(api = Build.VERSION_CODES.S)
fun isSPlus() = Build.VERSION.SDK_INT >= Build.VERSION_CODES.S
//@ChecksSdkIntAtLeast(api = Build.VERSION_CODES.TIRAMISU)
//fun isTiramisuPlus() = Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU
fun ensureBackgroundThread(callback: () -> Unit) {
if (isOnMainThread()) {
Thread {
callback()
}.start()
} else {
callback()
}
}

View file

@ -0,0 +1,183 @@
package eu.siacs.conversations.medialib.extensions
import android.app.Activity
import android.content.*
import android.media.MediaScannerConnection
import android.net.Uri
import android.os.Environment
import android.os.Handler
import android.os.Looper
import android.provider.DocumentsContract
import android.provider.MediaStore
import android.provider.OpenableColumns
import android.widget.Toast
import eu.siacs.conversations.R
import eu.siacs.conversations.medialib.helpers.Config
import java.io.File
import java.text.SimpleDateFormat
import java.util.*
import java.util.regex.Pattern
val Context.config: Config get() = Config.newInstance(applicationContext)
private const val ANDROID_DATA_DIR = "/Android/data/"
private const val ANDROID_OBB_DIR = "/Android/obb/"
val DIRS_ACCESSIBLE_ONLY_WITH_SAF = listOf(ANDROID_DATA_DIR, ANDROID_OBB_DIR)
fun isOnMainThread() = Looper.myLooper() == Looper.getMainLooper()
fun Context.toast(id: Int, length: Int = Toast.LENGTH_SHORT) {
toast(getString(id), length)
}
fun Context.toast(msg: String, length: Int = Toast.LENGTH_SHORT) {
try {
if (isOnMainThread()) {
doToast(this, msg, length)
} else {
Handler(Looper.getMainLooper()).post {
doToast(this, msg, length)
}
}
} catch (e: Exception) {
}
}
fun Context.showErrorToast(msg: String, length: Int = Toast.LENGTH_LONG) {
toast(String.format(getString(R.string.error), msg), length)
}
fun Context.showErrorToast(exception: Exception, length: Int = Toast.LENGTH_LONG) {
showErrorToast(exception.toString(), length)
}
fun Context.copyToClipboard(text: String) {
val clip = ClipData.newPlainText(getString(R.string.simple_commons), text)
(getSystemService(Context.CLIPBOARD_SERVICE) as ClipboardManager).setPrimaryClip(clip)
val toastText = String.format(getString(R.string.value_copied_to_clipboard_show), text)
toast(toastText)
}
fun Context.getFilenameFromContentUri(uri: Uri): String? {
val projection = arrayOf(
OpenableColumns.DISPLAY_NAME
)
try {
val cursor = contentResolver.query(uri, projection, null, null, null)
cursor?.use {
if (cursor.moveToFirst()) {
return cursor.getStringValue(OpenableColumns.DISPLAY_NAME)
}
}
} catch (e: Exception) {
}
return null
}
// avoid calling this multiple times in row, it can delete whole folder contents
fun Context.rescanPaths(paths: List<String>, callback: (() -> Unit)? = null) {
if (paths.isEmpty()) {
callback?.invoke()
return
}
for (path in paths) {
Intent(Intent.ACTION_MEDIA_SCANNER_SCAN_FILE).apply {
data = Uri.fromFile(File(path))
sendBroadcast(this)
}
}
var cnt = paths.size
MediaScannerConnection.scanFile(applicationContext, paths.toTypedArray(), null) { s, uri ->
if (--cnt == 0) {
callback?.invoke()
}
}
}
// some helper functions were taken from https://github.com/iPaulPro/aFileChooser/blob/master/aFileChooser/src/com/ipaulpro/afilechooser/utils/FileUtils.java
fun Context.getRealPathFromURI(uri: Uri): String? {
if (uri.scheme == "file") {
return uri.path
}
if (isDownloadsDocument(uri)) {
val id = DocumentsContract.getDocumentId(uri)
if (id.areDigitsOnly()) {
val newUri = ContentUris.withAppendedId(Uri.parse("content://downloads/public_downloads"), id.toLong())
val path = getDataColumn(newUri)
if (path != null) {
return path
}
}
} else if (isExternalStorageDocument(uri)) {
val documentId = DocumentsContract.getDocumentId(uri)
val parts = documentId.split(":")
if (parts[0].equals("primary", true)) {
return "${Environment.getExternalStorageDirectory().absolutePath}/${parts[1]}"
}
} else if (isMediaDocument(uri)) {
val documentId = DocumentsContract.getDocumentId(uri)
val split = documentId.split(":").dropLastWhile { it.isEmpty() }.toTypedArray()
val type = split[0]
val contentUri = when (type) {
"video" -> MediaStore.Video.Media.EXTERNAL_CONTENT_URI
"audio" -> MediaStore.Audio.Media.EXTERNAL_CONTENT_URI
else -> MediaStore.Images.Media.EXTERNAL_CONTENT_URI
}
val selection = "_id=?"
val selectionArgs = arrayOf(split[1])
val path = getDataColumn(contentUri, selection, selectionArgs)
if (path != null) {
return path
}
}
return getDataColumn(uri)
}
fun Context.getDataColumn(uri: Uri, selection: String? = null, selectionArgs: Array<String>? = null): String? {
try {
val projection = arrayOf(MediaStore.Files.FileColumns.DATA)
val cursor = contentResolver.query(uri, projection, selection, selectionArgs, null)
cursor?.use {
if (cursor.moveToFirst()) {
val data = cursor.getStringValue(MediaStore.Files.FileColumns.DATA)
if (data != "null") {
return data
}
}
}
} catch (e: Exception) {
}
return null
}
fun Context.getInternalStoragePath() =
if (File("/storage/emulated/0").exists()) "/storage/emulated/0" else Environment.getExternalStorageDirectory().absolutePath.trimEnd('/')
fun Context.getCurrentFormattedDateTime(): String {
val simpleDateFormat = SimpleDateFormat("yyyy_MM_dd_HH_mm_ss", Locale.getDefault())
return simpleDateFormat.format(Date(System.currentTimeMillis()))
}
private fun isDownloadsDocument(uri: Uri) = uri.authority == "com.android.providers.downloads.documents"
private fun isExternalStorageDocument(uri: Uri) = uri.authority == "com.android.externalstorage.documents"
private fun isMediaDocument(uri: Uri) = uri.authority == "com.android.providers.media.documents"
private fun doToast(context: Context, message: String, length: Int) {
if (context is Activity) {
if (!context.isFinishing && !context.isDestroyed) {
Toast.makeText(context, message, length).show()
}
} else {
Toast.makeText(context, message, length).show()
}
}

View file

@ -0,0 +1,25 @@
package eu.siacs.conversations.medialib.extensions
import android.annotation.SuppressLint
import android.database.Cursor
@SuppressLint("Range")
fun Cursor.getStringValue(key: String) = getString(getColumnIndex(key))
@SuppressLint("Range")
fun Cursor.getStringValueOrNull(key: String) = if (isNull(getColumnIndex(key))) null else getString(getColumnIndex(key))
@SuppressLint("Range")
fun Cursor.getIntValue(key: String) = getInt(getColumnIndex(key))
@SuppressLint("Range")
fun Cursor.getIntValueOrNull(key: String) = if (isNull(getColumnIndex(key))) null else getInt(getColumnIndex(key))
@SuppressLint("Range")
fun Cursor.getLongValue(key: String) = getLong(getColumnIndex(key))
@SuppressLint("Range")
fun Cursor.getLongValueOrNull(key: String) = if (isNull(getColumnIndex(key))) null else getLong(getColumnIndex(key))
@SuppressLint("Range")
fun Cursor.getBlobValue(key: String) = getBlob(getColumnIndex(key))

View file

@ -0,0 +1,22 @@
package eu.siacs.conversations.medialib.extensions
import android.text.Editable
import android.text.Spannable
import android.text.SpannableString
import android.text.TextWatcher
import android.text.style.BackgroundColorSpan
import android.widget.EditText
import android.widget.TextView
import androidx.core.graphics.ColorUtils
val EditText.value: String get() = text.toString().trim()
fun EditText.onTextChangeListener(onTextChangedAction: (newText: String) -> Unit) = addTextChangedListener(object : TextWatcher {
override fun afterTextChanged(s: Editable?) {
onTextChangedAction(s.toString())
}
override fun beforeTextChanged(s: CharSequence, start: Int, count: Int, after: Int) {}
override fun onTextChanged(s: CharSequence, start: Int, before: Int, count: Int) {}
})

View file

@ -0,0 +1,58 @@
package eu.siacs.conversations.medialib.extensions
import androidx.exifinterface.media.ExifInterface
import java.lang.reflect.Field
import java.lang.reflect.Modifier
fun ExifInterface.copyNonDimensionAttributesTo(destination: ExifInterface) {
val attributes = ExifInterfaceAttributes.AllNonDimensionAttributes
attributes.forEach {
val value = getAttribute(it)
if (value != null) {
destination.setAttribute(it, value)
}
}
try {
destination.saveAttributes()
} catch (ignored: Exception) {
}
}
private class ExifInterfaceAttributes {
companion object {
val AllNonDimensionAttributes = getAllNonDimensionExifAttributes()
private fun getAllNonDimensionExifAttributes(): List<String> {
val tagFields = ExifInterface::class.java.fields.filter { field -> isExif(field) }
val excludeAttributes = arrayListOf(
ExifInterface.TAG_IMAGE_LENGTH,
ExifInterface.TAG_IMAGE_WIDTH,
ExifInterface.TAG_PIXEL_X_DIMENSION,
ExifInterface.TAG_PIXEL_Y_DIMENSION,
ExifInterface.TAG_THUMBNAIL_IMAGE_LENGTH,
ExifInterface.TAG_THUMBNAIL_IMAGE_WIDTH,
ExifInterface.TAG_ORIENTATION
)
return tagFields
.map { tagField -> tagField.get(null) as String }
.filter { x -> !excludeAttributes.contains(x) }
.distinct()
}
private fun isExif(field: Field): Boolean {
return field.type == String::class.java &&
isPublicStaticFinal(field.modifiers) &&
field.name.startsWith("TAG_")
}
private const val publicStaticFinal = Modifier.PUBLIC or Modifier.STATIC or Modifier.FINAL
private fun isPublicStaticFinal(modifiers: Int): Boolean {
return modifiers and publicStaticFinal > 0
}
}
}

View file

@ -0,0 +1,30 @@
package eu.siacs.conversations.medialib.extensions
import android.graphics.PorterDuff
import android.graphics.drawable.GradientDrawable
import android.widget.ImageView
import androidx.annotation.DrawableRes
fun ImageView.setFillWithStroke(fillColor: Int, backgroundColor: Int, drawRectangle: Boolean = false) {
GradientDrawable().apply {
shape = if (drawRectangle) GradientDrawable.RECTANGLE else GradientDrawable.OVAL
setColor(fillColor)
background = this
if (backgroundColor == fillColor || fillColor == -2 && backgroundColor == -1) {
val strokeColor = backgroundColor.getContrastColor().adjustAlpha(0.5f)
setStroke(2, strokeColor)
}
}
}
fun ImageView.applyColorFilter(color: Int) = setColorFilter(color, PorterDuff.Mode.SRC_IN)
fun ImageView.setImageResourceOrBeGone(@DrawableRes imageRes: Int?) {
if (imageRes != null) {
beVisible()
setImageResource(imageRes)
} else {
beGone()
}
}

View file

@ -0,0 +1,19 @@
package eu.siacs.conversations.medialib.extensions
import android.graphics.Color
import eu.siacs.conversations.medialib.helpers.DARK_GREY
fun Int.getContrastColor(): Int {
val y = (299 * Color.red(this) + 587 * Color.green(this) + 114 * Color.blue(this)) / 1000
return if (y >= 149 && this != Color.BLACK) DARK_GREY else Color.WHITE
}
fun Int.toHex() = String.format("#%06X", 0xFFFFFF and this).toUpperCase()
fun Int.adjustAlpha(factor: Float): Int {
val alpha = Math.round(Color.alpha(this) * factor)
val red = Color.red(this)
val green = Color.green(this)
val blue = Color.blue(this)
return Color.argb(alpha, red, green, blue)
}

View file

@ -0,0 +1,13 @@
package eu.siacs.conversations.medialib.extensions
import android.widget.SeekBar
fun SeekBar.onSeekBarChangeListener(seekBarChangeListener: (progress: Int) -> Unit) = setOnSeekBarChangeListener(object : SeekBar.OnSeekBarChangeListener {
override fun onProgressChanged(seekBar: SeekBar, progress: Int, fromUser: Boolean) {
seekBarChangeListener(progress)
}
override fun onStartTrackingTouch(seekBar: SeekBar) {}
override fun onStopTrackingTouch(seekBar: SeekBar) {}
})

View file

@ -0,0 +1,17 @@
package eu.siacs.conversations.medialib.extensions
import android.graphics.Bitmap
fun String.getFilenameFromPath() = substring(lastIndexOf("/") + 1)
fun String.getFilenameExtension() = substring(lastIndexOf(".") + 1)
fun String.getCompressionFormat() = when (getFilenameExtension().lowercase()) {
"png" -> Bitmap.CompressFormat.PNG
"webp" -> Bitmap.CompressFormat.WEBP
else -> Bitmap.CompressFormat.JPEG
}
fun String.areDigitsOnly() = matches(Regex("[0-9]+"))
fun String.getParentPath() = removeSuffix("/${getFilenameFromPath()}")

View file

@ -0,0 +1,40 @@
package eu.siacs.conversations.medialib.extensions
import android.view.HapticFeedbackConstants
import android.view.View
import android.view.ViewTreeObserver
fun View.beInvisibleIf(beInvisible: Boolean) = if (beInvisible) beInvisible() else beVisible()
fun View.beVisibleIf(beVisible: Boolean) = if (beVisible) beVisible() else beGone()
fun View.beGoneIf(beGone: Boolean) = beVisibleIf(!beGone)
fun View.beInvisible() {
visibility = View.INVISIBLE
}
fun View.beVisible() {
visibility = View.VISIBLE
}
fun View.beGone() {
visibility = View.GONE
}
fun View.onGlobalLayout(callback: () -> Unit) {
viewTreeObserver?.addOnGlobalLayoutListener(object : ViewTreeObserver.OnGlobalLayoutListener {
override fun onGlobalLayout() {
if (viewTreeObserver != null) {
viewTreeObserver.removeOnGlobalLayoutListener(this)
callback()
}
}
})
}
fun View.isVisible() = visibility == View.VISIBLE
fun View.isInvisible() = visibility == View.INVISIBLE
fun View.isGone() = visibility == View.GONE

View file

@ -0,0 +1,50 @@
package eu.siacs.conversations.medialib.helpers
import android.content.Context
import android.graphics.Color
import eu.siacs.conversations.R
import java.util.*
class Config(private val context: Context) {
protected val prefs = context.getSharedPreferences("media_config_prefs", Context.MODE_PRIVATE)
companion object {
fun newInstance(context: Context) = Config(context)
}
// color picker last used colors
var colorPickerRecentColors: LinkedList<Int>
get(): LinkedList<Int> {
val defaultList = arrayListOf(
Color.RED,
Color.BLUE,
Color.GREEN,
Color.YELLOW,
Color.BLACK
)
return LinkedList(prefs.getString(COLOR_PICKER_RECENT_COLORS, null)?.lines()?.map { it.toInt() } ?: defaultList)
}
set(recentColors) = prefs.edit().putString(COLOR_PICKER_RECENT_COLORS, recentColors.joinToString(separator = "\n")).apply()
var lastEditorCropAspectRatio: Int
get() = prefs.getInt(LAST_EDITOR_CROP_ASPECT_RATIO, ASPECT_RATIO_FREE)
set(lastEditorCropAspectRatio) = prefs.edit().putInt(LAST_EDITOR_CROP_ASPECT_RATIO, lastEditorCropAspectRatio).apply()
var lastEditorCropOtherAspectRatioX: Float
get() = prefs.getFloat(LAST_EDITOR_CROP_OTHER_ASPECT_RATIO_X, 2f)
set(lastEditorCropOtherAspectRatioX) = prefs.edit().putFloat(LAST_EDITOR_CROP_OTHER_ASPECT_RATIO_X, lastEditorCropOtherAspectRatioX).apply()
var lastEditorCropOtherAspectRatioY: Float
get() = prefs.getFloat(LAST_EDITOR_CROP_OTHER_ASPECT_RATIO_Y, 1f)
set(lastEditorCropOtherAspectRatioY) = prefs.edit().putFloat(LAST_EDITOR_CROP_OTHER_ASPECT_RATIO_Y, lastEditorCropOtherAspectRatioY).apply()
var lastEditorDrawColor: Int
get() = prefs.getInt(LAST_EDITOR_DRAW_COLOR, context.getColor(R.color.editor_draw_default_color))
set(lastEditorDrawColor) = prefs.edit().putInt(LAST_EDITOR_DRAW_COLOR, lastEditorDrawColor).apply()
var lastEditorBrushSize: Int
get() = prefs.getInt(LAST_EDITOR_BRUSH_SIZE, 50)
set(lastEditorBrushSize) = prefs.edit().putInt(LAST_EDITOR_BRUSH_SIZE, lastEditorBrushSize).apply()
}

View file

@ -0,0 +1,21 @@
package eu.siacs.conversations.medialib.helpers
// shared preferences
const val LAST_EDITOR_CROP_ASPECT_RATIO = "last_editor_crop_aspect_ratio"
const val LAST_EDITOR_CROP_OTHER_ASPECT_RATIO_X = "last_editor_crop_other_aspect_ratio_x_2"
const val LAST_EDITOR_CROP_OTHER_ASPECT_RATIO_Y = "last_editor_crop_other_aspect_ratio_y_2"
const val LAST_EDITOR_DRAW_COLOR = "last_editor_draw_color"
const val LAST_EDITOR_BRUSH_SIZE = "last_editor_brush_size"
const val REAL_FILE_PATH = "real_file_path_2"
const val COLOR_PICKER_RECENT_COLORS = "color_picker_recent_colors"
val DARK_GREY = 0xFF333333.toInt()
// aspect ratios used at the editor for cropping
const val ASPECT_RATIO_FREE = 0
const val ASPECT_RATIO_ONE_ONE = 1
const val ASPECT_RATIO_FOUR_THREE = 2
const val ASPECT_RATIO_SIXTEEN_NINE = 3
const val ASPECT_RATIO_OTHER = 4

View file

@ -0,0 +1,27 @@
package eu.siacs.conversations.medialib.helpers
import android.graphics.Bitmap
import eu.siacs.conversations.medialib.models.FilterItem
import java.util.*
class FilterThumbnailsManager {
private var filterThumbnails = ArrayList<FilterItem>(10)
private var processedThumbnails = ArrayList<FilterItem>(10)
fun addThumb(filterItem: FilterItem) {
filterThumbnails.add(filterItem)
}
fun processThumbs(): ArrayList<FilterItem> {
for (filterItem in filterThumbnails) {
filterItem.bitmap = filterItem.filter.processFilter(Bitmap.createBitmap(filterItem.bitmap))
processedThumbnails.add(filterItem)
}
return processedThumbnails
}
fun clearThumbs() {
filterThumbnails = ArrayList()
processedThumbnails = ArrayList()
}
}

View file

@ -0,0 +1,18 @@
package eu.siacs.conversations.medialib.models
import eu.siacs.conversations.medialib.extensions.getParentPath
open class FileDirItem(
val path: String,
val name: String = "",
var isDirectory: Boolean = false,
var children: Int = 0,
var size: Long = 0L,
var modified: Long = 0L,
var mediaStoreId: Long = 0L
) {
override fun toString() =
"FileDirItem(path=$path, name=$name, isDirectory=$isDirectory, children=$children, size=$size, modified=$modified, mediaStoreId=$mediaStoreId)"
fun getParentPath() = path.getParentPath()
}

View file

@ -0,0 +1,6 @@
package eu.siacs.conversations.medialib.models
import android.graphics.Bitmap
import com.zomato.photofilters.imageprocessors.Filter
data class FilterItem(var bitmap: Bitmap, val filter: Filter)

View file

@ -0,0 +1,5 @@
package eu.siacs.conversations.medialib.models
import android.graphics.Color
data class PaintOptions(var color: Int = Color.BLACK, var strokeWidth: Float = 5f)

View file

@ -0,0 +1,33 @@
package eu.siacs.conversations.medialib.views
import android.annotation.SuppressLint
import android.content.Context
import android.graphics.*
import android.graphics.Shader.TileMode
import android.util.AttributeSet
import android.view.View
class ColorPickerSquare(context: Context, attrs: AttributeSet) : View(context, attrs) {
var paint: Paint? = null
var luar: Shader = LinearGradient(0f, 0f, 0f, measuredHeight.toFloat(), Color.WHITE, Color.BLACK, Shader.TileMode.CLAMP)
val color = floatArrayOf(1f, 1f, 1f)
@SuppressLint("DrawAllocation")
override fun onDraw(canvas: Canvas) {
super.onDraw(canvas)
if (paint == null) {
paint = Paint()
luar = LinearGradient(0f, 0f, 0f, measuredHeight.toFloat(), Color.WHITE, Color.BLACK, TileMode.CLAMP)
}
val rgb = Color.HSVToColor(color)
val dalam = LinearGradient(0f, 0f, measuredWidth.toFloat(), 0f, Color.WHITE, rgb, TileMode.CLAMP)
val shader = ComposeShader(luar, dalam, PorterDuff.Mode.MULTIPLY)
paint!!.shader = shader
canvas.drawRect(0f, 0f, measuredWidth.toFloat(), measuredHeight.toFloat(), paint!!)
}
fun setHue(hue: Float) {
color[0] = hue
invalidate()
}
}

View file

@ -0,0 +1,145 @@
package eu.siacs.conversations.medialib.views
import android.content.Context
import android.graphics.*
import android.util.AttributeSet
import android.view.MotionEvent
import android.view.View
import eu.siacs.conversations.R
import eu.siacs.conversations.medialib.models.PaintOptions
class EditorDrawCanvas(context: Context, attrs: AttributeSet) : View(context, attrs) {
private var mCurX = 0f
private var mCurY = 0f
private var mStartX = 0f
private var mStartY = 0f
private var mColor = 0
private var mWasMultitouch = false
private var mPaths = LinkedHashMap<Path, PaintOptions>()
private var mPaint = Paint()
private var mPath = Path()
private var mPaintOptions = PaintOptions()
private var backgroundBitmap: Bitmap? = null
init {
mColor = context.getColor(R.color.editor_draw_default_color)
mPaint.apply {
color = mColor
style = Paint.Style.STROKE
strokeJoin = Paint.Join.ROUND
strokeCap = Paint.Cap.ROUND
strokeWidth = 40f
isAntiAlias = true
}
}
override fun onDraw(canvas: Canvas) {
super.onDraw(canvas)
canvas.save()
if (backgroundBitmap != null) {
canvas.drawBitmap(backgroundBitmap!!, 0f, 0f, null)
}
for ((key, value) in mPaths) {
changePaint(value)
canvas.drawPath(key, mPaint)
}
changePaint(mPaintOptions)
canvas.drawPath(mPath, mPaint)
canvas.restore()
}
override fun onTouchEvent(event: MotionEvent): Boolean {
val x = event.x
val y = event.y
when (event.action and MotionEvent.ACTION_MASK) {
MotionEvent.ACTION_DOWN -> {
mWasMultitouch = false
mStartX = x
mStartY = y
actionDown(x, y)
}
MotionEvent.ACTION_MOVE -> {
if (event.pointerCount == 1 && !mWasMultitouch) {
actionMove(x, y)
}
}
MotionEvent.ACTION_UP, MotionEvent.ACTION_CANCEL -> actionUp()
MotionEvent.ACTION_POINTER_DOWN -> mWasMultitouch = true
}
invalidate()
return true
}
private fun actionDown(x: Float, y: Float) {
mPath.reset()
mPath.moveTo(x, y)
mCurX = x
mCurY = y
}
private fun actionMove(x: Float, y: Float) {
mPath.quadTo(mCurX, mCurY, (x + mCurX) / 2, (y + mCurY) / 2)
mCurX = x
mCurY = y
}
private fun actionUp() {
if (!mWasMultitouch) {
mPath.lineTo(mCurX, mCurY)
// draw a dot on click
if (mStartX == mCurX && mStartY == mCurY) {
mPath.lineTo(mCurX, mCurY + 2)
mPath.lineTo(mCurX + 1, mCurY + 2)
mPath.lineTo(mCurX + 1, mCurY)
}
}
mPaths[mPath] = mPaintOptions
mPath = Path()
mPaintOptions = PaintOptions(mPaintOptions.color, mPaintOptions.strokeWidth)
}
private fun changePaint(paintOptions: PaintOptions) {
mPaint.color = paintOptions.color
mPaint.strokeWidth = paintOptions.strokeWidth
}
fun updateColor(newColor: Int) {
mPaintOptions.color = newColor
}
fun updateBrushSize(newBrushSize: Int) {
mPaintOptions.strokeWidth = resources.getDimension(R.dimen.full_brush_size) * (newBrushSize / 100f)
}
fun updateBackgroundBitmap(bitmap: Bitmap) {
backgroundBitmap = bitmap
invalidate()
}
fun getBitmap(): Bitmap {
val bitmap = Bitmap.createBitmap(width, height, Bitmap.Config.ARGB_8888)
val canvas = Canvas(bitmap)
canvas.drawColor(Color.WHITE)
draw(canvas)
return bitmap
}
fun undo() {
if (mPaths.isEmpty()) {
return
}
val lastKey = mPaths.keys.lastOrNull()
mPaths.remove(lastKey)
invalidate()
}
}

View file

@ -98,6 +98,7 @@ import eu.siacs.conversations.entities.ReadByMarker;
import eu.siacs.conversations.entities.Transferable; import eu.siacs.conversations.entities.Transferable;
import eu.siacs.conversations.entities.TransferablePlaceholder; import eu.siacs.conversations.entities.TransferablePlaceholder;
import eu.siacs.conversations.http.HttpDownloadConnection; import eu.siacs.conversations.http.HttpDownloadConnection;
import eu.siacs.conversations.medialib.activities.EditActivity;
import eu.siacs.conversations.persistance.FileBackend; import eu.siacs.conversations.persistance.FileBackend;
import eu.siacs.conversations.services.MessageArchiveService; import eu.siacs.conversations.services.MessageArchiveService;
import eu.siacs.conversations.services.QuickConversationsService; import eu.siacs.conversations.services.QuickConversationsService;
@ -163,6 +164,7 @@ public class ConversationFragment extends XmppFragment
public static final int ATTACHMENT_CHOICE_LOCATION = 0x0305; public static final int ATTACHMENT_CHOICE_LOCATION = 0x0305;
public static final int ATTACHMENT_CHOICE_INVALID = 0x0306; public static final int ATTACHMENT_CHOICE_INVALID = 0x0306;
public static final int ATTACHMENT_CHOICE_RECORD_VIDEO = 0x0307; public static final int ATTACHMENT_CHOICE_RECORD_VIDEO = 0x0307;
public static final int ATTACHMENT_CHOICE_EDIT_PHOTO = 0x0308;
public static final String RECENTLY_USED_QUICK_ACTION = "recently_used_quick_action"; public static final String RECENTLY_USED_QUICK_ACTION = "recently_used_quick_action";
public static final String STATE_CONVERSATION_UUID = public static final String STATE_CONVERSATION_UUID =
@ -1032,14 +1034,25 @@ public class ConversationFragment extends XmppFragment
case ATTACHMENT_CHOICE_CHOOSE_IMAGE: case ATTACHMENT_CHOICE_CHOOSE_IMAGE:
final List<Attachment> imageUris = final List<Attachment> imageUris =
Attachment.extractAttachments(getActivity(), data, Attachment.Type.IMAGE); Attachment.extractAttachments(getActivity(), data, Attachment.Type.IMAGE);
mediaPreviewAdapter.addMediaPreviews(imageUris); if (imageUris.size() == 1) {
toggleInputMethod(); editImage(imageUris.get(0).getUri());
} else {
mediaPreviewAdapter.addMediaPreviews(imageUris);
toggleInputMethod();
}
break; break;
case ATTACHMENT_CHOICE_TAKE_PHOTO: case ATTACHMENT_CHOICE_TAKE_PHOTO:
final Uri takePhotoUri = pendingTakePhotoUri.pop(); final Uri takePhotoUri = pendingTakePhotoUri.pop();
if (takePhotoUri != null) { if (takePhotoUri != null) {
mediaPreviewAdapter.addMediaPreviews( editImage(takePhotoUri);
Attachment.of(getActivity(), takePhotoUri, Attachment.Type.IMAGE)); } else {
Log.d(Config.LOGTAG, "lost take photo uri. unable to to attach");
}
break;
case ATTACHMENT_CHOICE_EDIT_PHOTO:
final Uri editedUriPhoto = data.getParcelableExtra(EditActivity.KEY_EDITED_URI);
if (editedUriPhoto != null) {
mediaPreviewAdapter.replaceOrAddMediaPreview(data.getData(), editedUriPhoto, Attachment.Type.IMAGE);
toggleInputMethod(); toggleInputMethod();
} else { } else {
Log.d(Config.LOGTAG, "lost take photo uri. unable to to attach"); Log.d(Config.LOGTAG, "lost take photo uri. unable to to attach");
@ -1085,6 +1098,13 @@ public class ConversationFragment extends XmppFragment
} }
} }
public void editImage(Uri uri) {
Intent intent = new Intent(activity, EditActivity.class);
intent.setData(uri);
intent.putExtra(EditActivity.KEY_CHAT_NAME, conversation.getName());
startActivityForResult(intent, ATTACHMENT_CHOICE_EDIT_PHOTO);
}
private void commitAttachments() { private void commitAttachments() {
final List<Attachment> attachments = mediaPreviewAdapter.getAttachments(); final List<Attachment> attachments = mediaPreviewAdapter.getAttachments();
if (anyNeedsExternalStoragePermission(attachments) if (anyNeedsExternalStoragePermission(attachments)

View file

@ -10,6 +10,7 @@ import android.graphics.drawable.Drawable;
import android.net.Uri; import android.net.Uri;
import android.os.AsyncTask; import android.os.AsyncTask;
import android.view.LayoutInflater; import android.view.LayoutInflater;
import android.view.View;
import android.view.ViewGroup; import android.view.ViewGroup;
import android.widget.ImageView; import android.widget.ImageView;
import android.widget.Toast; import android.widget.Toast;
@ -22,7 +23,6 @@ import java.lang.ref.WeakReference;
import java.util.ArrayList; import java.util.ArrayList;
import java.util.List; import java.util.List;
import java.util.concurrent.RejectedExecutionException; import java.util.concurrent.RejectedExecutionException;
import eu.siacs.conversations.R; import eu.siacs.conversations.R;
import eu.siacs.conversations.databinding.MediaPreviewBinding; import eu.siacs.conversations.databinding.MediaPreviewBinding;
import eu.siacs.conversations.persistance.FileBackend; import eu.siacs.conversations.persistance.FileBackend;
@ -65,7 +65,13 @@ public class MediaPreviewAdapter extends RecyclerView.Adapter<MediaPreviewAdapte
notifyItemRemoved(pos); notifyItemRemoved(pos);
conversationFragment.toggleInputMethod(); conversationFragment.toggleInputMethod();
}); });
holder.binding.mediaPreview.setOnClickListener(v -> view(context, attachment)); holder.binding.mediaPreview.setOnClickListener(v -> {
if (attachment.getType() == Attachment.Type.IMAGE) {
conversationFragment.editImage(attachment.getUri());
} else {
view(context, attachment);
}
});
} }
private static void view(final Context context, Attachment attachment) { private static void view(final Context context, Attachment attachment) {
@ -82,6 +88,23 @@ public class MediaPreviewAdapter extends RecyclerView.Adapter<MediaPreviewAdapte
} }
} }
public void replaceOrAddMediaPreview(Uri originalUri, Uri editedUri, Attachment.Type type) {
boolean replaced = false;
for(int i = 0; i < mediaPreviews.size(); i++) {
Attachment current = mediaPreviews.get(i);
if (current.getUri().equals(originalUri)) {
replaced = true;
mediaPreviews.set(i, Attachment.of(conversationFragment.getActivity(), editedUri, current.getType()).get(0));
}
}
if (!replaced) {
mediaPreviews.addAll(Attachment.of(conversationFragment.getActivity(), editedUri, type));
}
notifyDataSetChanged();
}
public void addMediaPreviews(List<Attachment> attachments) { public void addMediaPreviews(List<Attachment> attachments) {
this.mediaPreviews.addAll(attachments); this.mediaPreviews.addAll(attachments);
notifyDataSetChanged(); notifyDataSetChanged();

Binary file not shown.

After

Width:  |  Height:  |  Size: 768 B

View file

@ -0,0 +1,9 @@
<?xml version="1.0" encoding="utf-8"?>
<shape
xmlns:android="http://schemas.android.com/apk/res/android"
android:shape="oval">
<solid
android:color="?colorPrimary"/>
</shape>

View file

@ -0,0 +1,10 @@
<?xml version="1.0" encoding="utf-8"?>
<shape
xmlns:android="http://schemas.android.com/apk/res/android"
android:shape="oval">
<stroke
android:width="2dp"
android:color="@color/white"/>
</shape>

View file

@ -0,0 +1,13 @@
<?xml version="1.0" encoding="utf-8"?>
<layer-list xmlns:android="http://schemas.android.com/apk/res/android">
<item>
<shape android:shape="ring" android:thicknessRatio="1" android:useLevel="false">
<stroke android:width="4px" android:color="@android:color/white"/>
</shape>
</item>
<item>
<shape android:shape="ring" android:thicknessRatio="1" android:useLevel="false">
<stroke android:width="2px" android:color="@android:color/black"/>
</shape>
</item>
</layer-list>

View file

@ -0,0 +1,7 @@
<?xml version="1.0" encoding="utf-8"?>
<shape xmlns:android="http://schemas.android.com/apk/res/android">
<gradient
android:angle="90"
android:endColor="@android:color/transparent"
android:startColor="#CC000000"/>
</shape>

View file

@ -0,0 +1,3 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android" android:width="24dp" android:height="24dp" android:viewportWidth="24" android:viewportHeight="24">
<path android:fillColor="#FFFFFFFF" android:pathData="M5 13h11.17l-4.88 4.88c-0.39 0.39-0.39 1.03 0 1.42 0.39 0.39 1.02 0.39 1.41 0l6.59-6.59c0.39-0.39 0.39-1.02 0-1.41l-6.58-6.6c-0.39-0.39-1.02-0.39-1.41 0-0.39 0.39-0.39 1.02 0 1.41L16.17 11H5c-0.55 0-1 0.45-1 1s0.45 1 1 1z"/>
</vector>

View file

@ -0,0 +1,3 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android" android:width="24dp" android:height="24dp" android:viewportWidth="24" android:viewportHeight="24">
<path android:fillColor="#FFFFFFFF" android:pathData="M18 12c-0.55 0-1 0.45-1 1v2h-2c-0.55 0-1 0.45-1 1s0.45 1 1 1h3c0.55 0 1-0.45 1-1v-3c0-0.55-0.45-1-1-1zM7 9h2c0.55 0 1-0.45 1-1S9.55 7 9 7H6C5.45 7 5 7.45 5 8v3c0 0.55 0.45 1 1 1s1-0.45 1-1V9zm14-6H3C1.9 3 1 3.9 1 5v14c0 1.1 0.9 2 2 2h18c1.1 0 2-0.9 2-2V5c0-1.1-0.9-2-2-2zm-1 16.01H4c-0.55 0-1-0.45-1-1V5.99c0-0.55 0.45-1 1-1h16c0.55 0 1 0.45 1 1v12.02c0 0.55-0.45 1-1 1z"/>
</vector>

View file

@ -0,0 +1,3 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android" android:width="24dp" android:height="24dp" android:viewportWidth="24" android:viewportHeight="24">
<path android:pathData="M6.102 1.09c-0.804 0.804-0.804 2.103 0 2.908L14.105 12l-8.003 8.002c-0.804 0.805-0.804 2.104 0 2.909 0.805 0.804 2.104 0.804 2.908 0l9.468-9.468c0.804-0.804 0.804-2.103 0-2.908L9.01 1.07C8.227 0.286 6.907 0.286 6.102 1.09z" android:strokeWidth="2.06257677" android:fillColor="#FFFFFFFF"/>
</vector>

View file

@ -0,0 +1,3 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android" android:width="24dp" android:height="24dp" android:viewportWidth="24" android:viewportHeight="24">
<path android:fillColor="#FFFFFFFF" android:pathData="M16 9v5h2V8c0-1.1-0.9-2-2-2h-6v2h5c0.55 0 1 0.45 1 1zm3 7H9c-0.55 0-1-0.45-1-1V5c0-0.55-0.45-1-1-1S6 4.45 6 5v1H5C4.45 6 4 6.45 4 7s0.45 1 1 1h1v8c0 1.1 0.9 2 2 2h8v1c0 0.55 0.45 1 1 1s1-0.45 1-1v-1h1c0.55 0 1-0.45 1-1s-0.45-1-1-1zM17.66 1.4c-1.67-0.89-3.83-1.51-6.27-1.36l3.81 3.81 1.33-1.33c3.09 1.46 5.34 4.37 5.89 7.86 0.06 0.41 0.44 0.69 0.86 0.62 0.41-0.06 0.69-0.45 0.62-0.86-0.6-3.8-2.96-7-6.24-8.74zM7.47 21.49c-3.09-1.46-5.34-4.37-5.89-7.86-0.06-0.41-0.44-0.69-0.86-0.62-0.41 0.06-0.69 0.45-0.62 0.86 0.6 3.81 2.96 7.01 6.24 8.75 1.67 0.89 3.83 1.51 6.27 1.36L8.8 20.16l-1.33 1.33z"/>
</vector>

View file

@ -0,0 +1,9 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="24dp"
android:height="24dp"
android:viewportWidth="960"
android:viewportHeight="960">
<path
android:fillColor="@android:color/white"
android:pathData="M382,727.65L146.35,492L211.24,427.11L382,597.87L748.76,231.11L813.65,296L382,727.65Z"/>
</vector>

View file

@ -0,0 +1,3 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android" android:width="24dp" android:height="24dp" android:viewportWidth="24" android:viewportHeight="24">
<path android:fillColor="#FFFFFFFF" android:pathData="M18.85 10.39l1.06-1.06c0.78-0.78 0.78-2.05 0-2.83L18.5 5.09c-0.78-0.78-2.05-0.78-2.83 0l-1.06 1.06 4.24 4.24zm-5.66-2.83l-9.05 9.05C4.05 16.7 4 16.83 4 16.96v3.54C4 20.78 4.22 21 4.5 21h3.54c0.13 0 0.26-0.05 0.35-0.15l9.05-9.05-4.25-4.24zM19 17.5c0 2.19-2.54 3.5-5 3.5-0.55 0-1-0.45-1-1s0.45-1 1-1c1.54 0 3-0.73 3-1.5 0-0.47-0.48-0.87-1.23-1.2l1.48-1.48C18.32 15.45 19 16.29 19 17.5zM4.58 13.35C3.61 12.79 3 12.06 3 11c0-1.8 1.89-2.63 3.56-3.36C7.59 7.18 9 6.56 9 6c0-0.41-0.78-1-2-1-1.26 0-1.8 0.61-1.83 0.64-0.35 0.41-0.98 0.46-1.4 0.12-0.41-0.34-0.49-0.95-0.15-1.38C3.73 4.24 4.76 3 7 3s4 1.32 4 3c0 1.87-1.93 2.72-3.64 3.47C6.42 9.88 5 10.5 5 11c0 0.31 0.43 0.6 1.07 0.86l-1.49 1.49z"/>
</vector>

View file

@ -0,0 +1,3 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android" android:width="24dp" android:height="24dp" android:viewportWidth="24" android:viewportHeight="24">
<path android:pathData="M15 21h2v-2h-2v2zm4-12h2V7h-2v2zM3 5v14c0 1.1 0.9 2 2 2h3c0.55 0 1-0.45 1-1s-0.45-1-1-1H6c-0.55 0-1-0.45-1-1V6c0-0.55 0.45-1 1-1h2c0.55 0 1-0.45 1-1S8.55 3 8 3H5C3.9 3 3 3.9 3 5zm16-2v2h2c0-1.1-0.9-2-2-2zm-7 20c0.55 0 1-0.45 1-1V2c0-0.55-0.45-1-1-1s-1 0.45-1 1v20c0 0.55 0.45 1 1 1zm7-6h2v-2h-2v2zM15 5h2V3h-2v2zm4 8h2v-2h-2v2zm0 8c1.1 0 2-0.9 2-2h-2v2z" android:fillColor="#FFFFFF"/>
</vector>

View file

@ -0,0 +1,3 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android" android:width="24dp" android:height="24dp" android:viewportWidth="24" android:viewportHeight="24">
<path android:pathData="M21 9V7h-2v2zM9 5V3H7v2zM5 21h14c1.1 0 2-0.9 2-2v-3c0-0.55-0.45-1-1-1s-1 0.45-1 1v2c0 0.55-0.45 1-1 1H6c-0.55 0-1-0.45-1-1v-2c0-0.55-0.45-1-1-1s-1 0.45-1 1v3c0 1.1 0.9 2 2 2zM3 5h2V3C3.9 3 3 3.9 3 5zm20 7c0-0.55-0.45-1-1-1H2c-0.55 0-1 0.45-1 1s0.45 1 1 1h20c0.55 0 1-0.45 1-1zm-6-7V3h-2v2zM5 9V7H3v2zm8-4V3h-2v2zm8 0c0-1.1-0.9-2-2-2v2z" android:fillColor="#FFFFFF"/>
</vector>

View file

@ -0,0 +1,3 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android" android:width="24dp" android:height="24dp" android:viewportWidth="24" android:viewportHeight="24">
<path android:pathData="M3 8c0 0.55 0.45 1 1 1h4c0.55 0 1-0.45 1-1V4c0-0.55-0.45-1-1-1S7 3.45 7 4v1.59L4.62 3.21c-0.39-0.39-1.02-0.39-1.41 0-0.39 0.39-0.39 1.02 0 1.41L5.59 7H4C3.45 7 3 7.45 3 8zm17-1h-1.59l2.38-2.38c0.39-0.39 0.39-1.02 0-1.41-0.39-0.39-1.02-0.39-1.41 0L17 5.59V4c0-0.55-0.45-1-1-1s-1 0.45-1 1v4c0 0.55 0.45 1 1 1h4c0.55 0 1-0.45 1-1s-0.45-1-1-1zM4 17h1.59l-2.38 2.38c-0.39 0.39-0.39 1.02 0 1.41 0.39 0.39 1.02 0.39 1.41 0L7 18.41V20c0 0.55 0.45 1 1 1s1-0.45 1-1v-4c0-0.55-0.45-1-1-1H4c-0.55 0-1 0.45-1 1s0.45 1 1 1zm17-1c0-0.55-0.45-1-1-1h-4c-0.55 0-1 0.45-1 1v4c0 0.55 0.45 1 1 1s1-0.45 1-1v-1.59l2.38 2.38c0.39 0.39 1.02 0.39 1.41 0 0.39-0.39 0.39-1.02 0-1.41L18.41 17H20c0.55 0 1-0.45 1-1z" android:fillColor="#FFFFFF"/>
</vector>

View file

@ -0,0 +1,3 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android" android:width="24dp" android:height="24dp" android:viewportWidth="24" android:viewportHeight="24">
<path android:fillColor="#FFFFFF" android:pathData="M19.02 10.99V18c0 0.55-0.45 1-1 1H6c-0.55 0-1-0.45-1-1V6c0-0.55 0.45-1 1-1h7c0.55 0 1-0.45 1-1s-0.45-1-1-1H5.02c-1.1 0-2 0.9-2 2v14c0 1.1 0.9 2 2 2H19c1.1 0 2-0.89 2-2v-8.01c0-0.55-0.44-0.99-0.99-0.99s-0.99 0.44-0.99 0.99zm-5.77-0.24L12.46 9c-0.18-0.39-0.73-0.39-0.91 0l-0.79 1.75L9 11.54c-0.39 0.18-0.39 0.73 0 0.91l1.75 0.79 0.79 1.76c0.18 0.39 0.73 0.39 0.91 0l0.79-1.75L15 12.46c0.39-0.18 0.39-0.73 0-0.91l-1.75-0.8zm4.69-4.69l-0.6-1.32c-0.13-0.29-0.55-0.29-0.69 0l-0.6 1.32-1.32 0.6c-0.29 0.13-0.29 0.55 0 0.69l1.32 0.6 0.6 1.32c0.13 0.29 0.55 0.29 0.69 0l0.6-1.32 1.32-0.6c0.29-0.13 0.29-0.55 0-0.69l-1.32-0.6z"/>
</vector>

View file

@ -0,0 +1,3 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android" android:width="24dp" android:height="24dp" android:viewportWidth="24" android:viewportHeight="24">
<path android:fillColor="#FFFFFF" android:pathData="M14.83 4.83L12.7 2.7C12.08 2.08 11 2.52 11 3.41v0.66C7.06 4.56 4 7.92 4 12c0 3.64 2.43 6.71 5.77 7.68 0.62 0.18 1.23-0.32 1.23-0.96v-0.03c0-0.43-0.27-0.82-0.68-0.94C7.82 17.03 6 14.73 6 12c0-2.97 2.16-5.43 5-5.91v1.53c0 0.89 1.07 1.33 1.7 0.71l2.13-2.08c0.4-0.38 0.4-1.02 0-1.42zm4.84 4.93c-0.16-0.55-0.38-1.08-0.66-1.59-0.31-0.57-1.1-0.66-1.56-0.2l-0.01 0.01c-0.31 0.31-0.38 0.78-0.17 1.16 0.2 0.37 0.36 0.76 0.48 1.16 0.12 0.42 0.51 0.7 0.94 0.7h0.02c0.65 0 1.15-0.62 0.96-1.24zM13 18.68v0.02c0 0.65 0.62 1.14 1.24 0.96 0.55-0.16 1.08-0.38 1.59-0.66 0.57-0.31 0.66-1.1 0.2-1.56l-0.02-0.02c-0.31-0.31-0.78-0.38-1.16-0.17-0.37 0.21-0.76 0.37-1.16 0.49-0.41 0.12-0.69 0.51-0.69 0.94zm4.44-2.65c0.46 0.46 1.25 0.37 1.56-0.2 0.28-0.51 0.5-1.04 0.67-1.59 0.18-0.62-0.31-1.24-0.96-1.24h-0.02c-0.44 0-0.82 0.28-0.94 0.7-0.12 0.4-0.28 0.79-0.48 1.17-0.21 0.38-0.13 0.86 0.17 1.16z"/>
</vector>

View file

@ -0,0 +1,3 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android" android:width="24dp" android:height="24dp" android:viewportWidth="24" android:viewportHeight="24">
<path android:pathData="M12.5 8c-2.65 0-5.05 0.99-6.9 2.6L3.71 8.71C3.08 8.08 2 8.52 2 9.41V15c0 0.55 0.45 1 1 1h5.59c0.89 0 1.34-1.08 0.71-1.71l-1.91-1.91c1.39-1.16 3.16-1.88 5.12-1.88 3.16 0 5.89 1.84 7.19 4.5 0.27 0.56 0.91 0.84 1.5 0.64 0.71-0.23 1.07-1.04 0.75-1.72C20.23 10.42 16.65 8 12.5 8z" android:fillColor="#FFFFFFFF"/>
</vector>

View file

@ -0,0 +1,46 @@
<?xml version="1.0" encoding="utf-8"?>
<resources>
<string name="app_name">Simple Gallery</string>
<string name="app_launcher_name">Gallery</string>
<string name="ok">OK</string>
<string name="cancel">Cancel</string>
<string name="custom">Custom</string>
<string name="resize_and_save">Resize selection and save</string>
<string name="width">Width</string>
<string name="height">Height</string>
<string name="keep_aspect_ratio">Keep aspect ratio</string>
<string name="invalid_values">Please enter a valid resolution</string>
<string name="editor">Editor</string>
<string name="basic_editor">Basic Editor</string>
<string name="rotate">Rotate</string>
<string name="invalid_image_path">Invalid image path</string>
<string name="image_editing_failed">Image editing failed</string>
<string name="unknown_file_location">Unknown file location</string>
<string name="transform">Transform</string>
<string name="crop">Crop</string>
<string name="draw">Draw</string>
<string name="flip_horizontally">Flip horizontally</string>
<string name="flip_vertically">Flip vertically</string>
<string name="free_aspect_ratio">Free</string>
<string name="other_aspect_ratio">Other</string>
<string name="thumbnails">Thumbnails</string>
<string name="saving">saving</string>
<string name="out_of_memory_error">out_of_memory_error</string>
<string name="none">none</string>
<string name="file_saved">file_saved</string>
<string name="error">error</string>
<string name="simple_commons">simple_commons</string>
<string name="value_copied_to_clipboard_show">value_copied_to_clipboard_show</string>
<string name="default_color">default_color</string>
<string name="unknown_error_occurred">unknown_error_occurred</string>
<string name="undo">undo</string>
<string name="change_color">change_color</string>
<string name="resize">resize</string>
<string name="filter">filter</string>
<string name="could_not_create_file">could_not_create_file</string>
</resources>

View file

@ -0,0 +1,8 @@
<?xml version="1.0" encoding="utf-8"?>
<selector xmlns:android="http://schemas.android.com/apk/res/android">
<item>
<shape android:shape="rectangle">
<stroke android:width="@dimen/one_dp" android:color="#FFFFFFFF"/>
</shape>
</item>
</selector>

View file

@ -0,0 +1,89 @@
<?xml version="1.0" encoding="utf-8"?>
<layout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto">
<androidx.coordinatorlayout.widget.CoordinatorLayout
android:layout_width="match_parent"
android:layout_height="match_parent">
<RelativeLayout
android:layout_width="match_parent"
android:layout_height="match_parent">
<com.github.chrisbanes.photoview.PhotoView
android:id="@+id/default_image_view"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:paddingTop="?attr/actionBarSize"
android:paddingBottom="@dimen/bottom_actions_height_bigger" />
<com.theartofdev.edmodo.cropper.CropImageView
android:id="@+id/crop_image_view"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:layout_marginTop="?attr/actionBarSize"
android:layout_marginBottom="@dimen/bottom_actions_height_bigger"
android:visibility="gone"
app:cropBackgroundColor="@color/crop_image_view_background"
app:cropInitialCropWindowPaddingRatio="0" />
<eu.siacs.conversations.medialib.views.EditorDrawCanvas
android:id="@+id/editor_draw_canvas"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:layout_centerHorizontal="true"
android:layout_marginTop="?attr/actionBarSize"
android:layout_marginBottom="@dimen/bottom_actions_height_double"
android:background="@android:color/transparent"
android:visibility="gone" />
<include
android:id="@+id/bottom_editor_primary_actions"
layout="@layout/bottom_editor_primary_actions" />
<include
android:id="@+id/bottom_aspect_ratios"
layout="@layout/bottom_actions_aspect_ratio"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_above="@+id/bottom_editor_crop_rotate_actions"
android:visibility="gone" />
<include
android:id="@+id/bottom_editor_filter_actions"
layout="@layout/bottom_editor_actions_filter"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_above="@+id/bottom_editor_primary_actions"
android:visibility="gone" />
<include
android:id="@+id/bottom_editor_crop_rotate_actions"
layout="@layout/bottom_editor_crop_rotate_actions"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_above="@+id/bottom_editor_primary_actions"
android:visibility="gone" />
<include
android:id="@+id/bottom_editor_draw_actions"
layout="@layout/bottom_editor_draw_actions"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_above="@+id/bottom_editor_primary_actions"
android:visibility="gone" />
</RelativeLayout>
<com.google.android.material.appbar.MaterialToolbar
android:id="@+id/editor_toolbar"
android:layout_width="match_parent"
android:layout_height="?attr/actionBarSize"
android:background="@color/black26"
android:elevation="0dp"
app:title="@string/editor"
app:navigationIcon="?attr/homeAsUpIndicator" />
</androidx.coordinatorlayout.widget.CoordinatorLayout>
</layout>

View file

@ -0,0 +1,94 @@
<?xml version="1.0" encoding="utf-8"?>
<layout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto">
<androidx.constraintlayout.widget.ConstraintLayout
android:layout_width="match_parent"
android:background="@color/black26"
android:layout_height="@dimen/bottom_actions_height">
<TextView
android:id="@+id/bottom_aspect_ratio_free"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:background="?attr/selectableItemBackgroundBorderless"
android:padding="@dimen/large_margin"
android:text="@string/free_aspect_ratio"
android:textAllCaps="true"
android:textColor="@android:color/white"
android:textSize="@dimen/big_text_size"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toStartOf="@+id/bottom_aspect_ratio_one_one"
app:layout_constraintHorizontal_bias="0.5"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent"
app:layout_constraintVertical_bias="0.0"/>
<TextView
android:id="@+id/bottom_aspect_ratio_one_one"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:background="?attr/selectableItemBackgroundBorderless"
android:padding="@dimen/large_margin"
android:text="1:1"
android:textColor="@android:color/white"
android:textSize="@dimen/big_text_size"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toStartOf="@+id/bottom_aspect_ratio_four_three"
app:layout_constraintHorizontal_bias="0.5"
app:layout_constraintStart_toEndOf="@+id/bottom_aspect_ratio_free"
app:layout_constraintTop_toTopOf="parent"
app:layout_constraintVertical_bias="0.0"/>
<TextView
android:id="@+id/bottom_aspect_ratio_four_three"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:background="?attr/selectableItemBackgroundBorderless"
android:padding="@dimen/large_margin"
android:text="4:3"
android:textColor="@android:color/white"
android:textSize="@dimen/big_text_size"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toStartOf="@+id/bottom_aspect_ratio_sixteen_nine"
app:layout_constraintHorizontal_bias="0.5"
app:layout_constraintStart_toEndOf="@+id/bottom_aspect_ratio_one_one"
app:layout_constraintTop_toTopOf="parent"
app:layout_constraintVertical_bias="0.0"/>
<TextView
android:id="@+id/bottom_aspect_ratio_sixteen_nine"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:background="?attr/selectableItemBackgroundBorderless"
android:padding="@dimen/large_margin"
android:text="16:9"
android:textColor="@android:color/white"
android:textSize="@dimen/big_text_size"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toStartOf="@+id/bottom_aspect_ratio_other"
app:layout_constraintHorizontal_bias="0.5"
app:layout_constraintStart_toEndOf="@+id/bottom_aspect_ratio_four_three"
app:layout_constraintTop_toTopOf="parent"
app:layout_constraintVertical_bias="0.0"/>
<TextView
android:id="@+id/bottom_aspect_ratio_other"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:background="?attr/selectableItemBackgroundBorderless"
android:padding="@dimen/large_margin"
android:text="@string/other_aspect_ratio"
android:textAllCaps="true"
android:textColor="@android:color/white"
android:textSize="@dimen/big_text_size"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintHorizontal_bias="0.5"
app:layout_constraintStart_toEndOf="@+id/bottom_aspect_ratio_sixteen_nine"
app:layout_constraintTop_toTopOf="parent"
app:layout_constraintVertical_bias="0.0"/>
</androidx.constraintlayout.widget.ConstraintLayout>
</layout>

View file

@ -0,0 +1,21 @@
<?xml version="1.0" encoding="utf-8"?>
<layout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto">
<RelativeLayout
android:layout_width="match_parent"
android:layout_height="@dimen/bottom_filters_height"
android:background="@color/black26"
android:paddingTop="@dimen/medium_margin">
<androidx.recyclerview.widget.RecyclerView
android:id="@+id/bottom_actions_filter_list"
android:layout_width="match_parent"
android:layout_height="@dimen/bottom_filters_height"
android:orientation="horizontal"
android:overScrollMode="never"
app:layoutManager="androidx.recyclerview.widget.LinearLayoutManager"/>
</RelativeLayout>
</layout>

View file

@ -0,0 +1,82 @@
<?xml version="1.0" encoding="utf-8"?>
<layout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto">
<androidx.constraintlayout.widget.ConstraintLayout
android:layout_width="match_parent"
android:layout_height="@dimen/bottom_actions_small_height"
android:background="@color/black26"
android:layout_alignParentBottom="true">
<ImageView
android:id="@+id/bottom_rotate"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:background="?attr/selectableItemBackgroundBorderless"
android:contentDescription="@string/rotate"
android:padding="@dimen/normal_margin"
android:src="@drawable/ic_rotate_right_vector"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toStartOf="@+id/bottom_resize"
app:layout_constraintHorizontal_bias="0.5"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent"/>
<ImageView
android:id="@+id/bottom_resize"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:background="?attr/selectableItemBackgroundBorderless"
android:contentDescription="@string/resize"
android:padding="@dimen/normal_margin"
android:src="@drawable/ic_minimize_vector"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toStartOf="@+id/bottom_aspect_ratio"
app:layout_constraintHorizontal_bias="0.5"
app:layout_constraintStart_toEndOf="@+id/bottom_rotate"
app:layout_constraintTop_toTopOf="parent"/>
<ImageView
android:id="@+id/bottom_aspect_ratio"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:background="?attr/selectableItemBackgroundBorderless"
android:contentDescription="@string/crop"
android:padding="@dimen/normal_margin"
android:src="@drawable/ic_aspect_ratio_vector"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toStartOf="@+id/bottom_flip_horizontally"
app:layout_constraintHorizontal_bias="0.5"
app:layout_constraintStart_toEndOf="@+id/bottom_resize"
app:layout_constraintTop_toTopOf="parent"/>
<ImageView
android:id="@+id/bottom_flip_horizontally"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:background="?attr/selectableItemBackgroundBorderless"
android:contentDescription="@string/flip_horizontally"
android:padding="@dimen/normal_margin"
android:src="@drawable/ic_flip_horizontally_vector"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toStartOf="@+id/bottom_flip_vertically"
app:layout_constraintHorizontal_bias="0.5"
app:layout_constraintStart_toEndOf="@+id/bottom_aspect_ratio"
app:layout_constraintTop_toTopOf="parent"/>
<ImageView
android:id="@+id/bottom_flip_vertically"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:background="?attr/selectableItemBackgroundBorderless"
android:contentDescription="@string/flip_vertically"
android:padding="@dimen/normal_margin"
android:src="@drawable/ic_flip_vertically_vector"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintHorizontal_bias="0.5"
app:layout_constraintStart_toEndOf="@+id/bottom_flip_horizontally"
app:layout_constraintTop_toTopOf="parent"/>
</androidx.constraintlayout.widget.ConstraintLayout>
</layout>

View file

@ -0,0 +1,64 @@
<?xml version="1.0" encoding="utf-8"?>
<layout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto">
<androidx.constraintlayout.widget.ConstraintLayout
android:layout_width="match_parent"
android:layout_height="@dimen/bottom_actions_height"
android:background="@color/black26"
android:layout_alignParentBottom="true">
<androidx.appcompat.widget.AppCompatSeekBar
android:id="@+id/bottom_draw_width"
android:layout_width="0dp"
android:layout_height="0dp"
android:layout_marginLeft="@dimen/large_margin"
android:layout_marginRight="@dimen/large_margin"
android:max="100"
android:progress="50"
app:layout_constraintBottom_toBottomOf="@id/bottom_draw_color"
app:layout_constraintLeft_toLeftOf="parent"
app:layout_constraintRight_toLeftOf="@+id/bottom_draw_color"
app:layout_constraintTop_toTopOf="@+id/bottom_draw_color"/>
<ImageView
android:id="@+id/bottom_draw_color_clickable"
android:layout_width="@dimen/bottom_editor_color_picker_size"
android:layout_height="@dimen/bottom_editor_color_picker_size"
android:layout_marginEnd="@dimen/small_margin"
android:background="?attr/selectableItemBackgroundBorderless"
android:contentDescription="@string/change_color"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintRight_toLeftOf="@+id/bottom_draw_undo"
app:layout_constraintTop_toTopOf="parent"/>
<ImageView
android:id="@+id/bottom_draw_color"
android:layout_width="@dimen/bottom_editor_color_picker_size"
android:layout_height="@dimen/bottom_editor_color_picker_size"
android:layout_marginEnd="@dimen/small_margin"
android:clickable="false"
android:contentDescription="@null"
android:padding="@dimen/small_margin"
android:src="@drawable/circle_background"
android:foreground="@drawable/circle_stroke_white"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintRight_toLeftOf="@+id/bottom_draw_undo"
app:layout_constraintTop_toTopOf="parent"/>
<ImageView
android:id="@+id/bottom_draw_undo"
android:layout_width="@dimen/bottom_editor_color_picker_size"
android:layout_height="@dimen/bottom_editor_color_picker_size"
android:layout_marginEnd="@dimen/normal_margin"
android:background="?attr/selectableItemBackgroundBorderless"
android:clickable="false"
android:contentDescription="@string/undo"
android:padding="@dimen/medium_margin"
android:src="@drawable/ic_undo_vector"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintRight_toRightOf="parent"
app:layout_constraintTop_toTopOf="parent"/>
</androidx.constraintlayout.widget.ConstraintLayout>
</layout>

View file

@ -0,0 +1,52 @@
<?xml version="1.0" encoding="utf-8"?>
<layout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto">
<androidx.constraintlayout.widget.ConstraintLayout
android:layout_width="match_parent"
android:layout_height="@dimen/bottom_actions_height"
android:background="@color/black26"
android:layout_alignParentBottom="true">
<ImageView
android:id="@+id/bottom_primary_filter"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:background="?attr/selectableItemBackgroundBorderless"
android:contentDescription="@string/filter"
android:padding="@dimen/normal_margin"
android:src="@drawable/ic_photo_filter_vector"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toStartOf="@+id/bottom_primary_crop_rotate"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent" />
<ImageView
android:id="@+id/bottom_primary_crop_rotate"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:background="?attr/selectableItemBackgroundBorderless"
android:contentDescription="@string/transform"
android:padding="@dimen/normal_margin"
android:src="@drawable/ic_crop_rotate_vector"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toStartOf="@+id/bottom_primary_draw"
app:layout_constraintStart_toEndOf="@+id/bottom_primary_filter"
app:layout_constraintTop_toTopOf="parent" />
<ImageView
android:id="@+id/bottom_primary_draw"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:background="?attr/selectableItemBackgroundBorderless"
android:contentDescription="@string/draw"
android:padding="@dimen/normal_margin"
android:src="@drawable/ic_draw_vector"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toEndOf="@+id/bottom_primary_crop_rotate"
app:layout_constraintTop_toTopOf="parent" />
</androidx.constraintlayout.widget.ConstraintLayout>
</layout>

View file

@ -0,0 +1,166 @@
<?xml version="1.0" encoding="utf-8"?>
<layout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto">
<ScrollView
android:id="@+id/color_picker_scrollview"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:layout_marginTop="@dimen/large_margin"
android:layout_marginBottom="@dimen/large_margin">
<RelativeLayout
android:id="@+id/color_picker_holder"
android:layout_width="match_parent"
android:layout_height="wrap_content">
<androidx.constraintlayout.widget.ConstraintLayout
android:id="@+id/recent_colors"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginHorizontal="@dimen/large_margin"
android:layout_marginTop="@dimen/large_margin">
<androidx.constraintlayout.helper.widget.Flow
android:id="@+id/recent_colors_flow"
android:layout_width="match_parent"
android:layout_height="0dp"
app:flow_horizontalAlign="start"
app:flow_horizontalGap="@dimen/large_margin"
app:flow_horizontalStyle="packed"
app:flow_maxElementsWrap="5"
app:flow_verticalGap="@dimen/medium_margin"
app:flow_wrapMode="chain"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent" />
</androidx.constraintlayout.widget.ConstraintLayout>
<RelativeLayout
android:id="@+id/color_picker_top_holder"
android:layout_width="match_parent"
android:layout_height="272dp"
android:layout_below="@+id/recent_colors"
android:gravity="center_horizontal">
<eu.siacs.conversations.medialib.views.ColorPickerSquare
android:id="@+id/color_picker_square"
android:layout_width="240dp"
android:layout_height="240dp"
android:layout_centerVertical="true"
android:layerType="software" />
<ImageView
android:id="@+id/color_picker_hue"
android:layout_width="@dimen/colorpicker_hue_width"
android:layout_height="240dp"
android:layout_centerVertical="true"
android:layout_marginStart="@dimen/medium_margin"
android:layout_toEndOf="@id/color_picker_square"
android:scaleType="fitXY"
android:src="@drawable/img_color_picker_hue" />
<ImageView
android:id="@+id/color_picker_hue_cursor"
android:layout_width="8dp"
android:layout_height="wrap_content"
android:src="@drawable/ic_chevron_right_unpadded_vector" />
<ImageView
android:id="@+id/color_picker_cursor"
android:layout_width="@dimen/large_margin"
android:layout_height="@dimen/large_margin"
android:adjustViewBounds="true"
android:scaleType="fitCenter"
android:src="@drawable/color_picker_circle" />
</RelativeLayout>
<RelativeLayout
android:id="@+id/color_picker_hex_codes_holder"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_below="@id/color_picker_top_holder">
<TextView
android:id="@+id/color_picker_old_hex"
android:layout_width="70dp"
android:layout_height="match_parent"
android:layout_centerInParent="true"
android:layout_toStartOf="@+id/color_picker_hex_arrow"
android:gravity="center"
android:textSize="@dimen/normal_text_size" />
<ImageView
android:id="@+id/color_picker_hex_arrow"
android:layout_width="20dp"
android:layout_height="20dp"
android:layout_alignTop="@+id/color_picker_new_hex"
android:layout_alignBottom="@+id/color_picker_new_hex"
android:layout_centerInParent="true"
android:layout_marginStart="@dimen/normal_margin"
android:layout_marginEnd="@dimen/normal_margin"
android:scaleType="centerInside"
android:src="@drawable/ic_arrow_right_vector" />
<TextView
android:id="@+id/color_picker_new_hex_label"
android:layout_width="wrap_content"
android:layout_height="30dp"
android:layout_alignTop="@+id/color_picker_new_hex"
android:layout_alignBottom="@+id/color_picker_new_hex"
android:layout_toEndOf="@+id/color_picker_hex_arrow"
android:gravity="center"
android:text="#"
android:textSize="@dimen/normal_text_size" />
<EditText
android:id="@+id/color_picker_new_hex"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_toEndOf="@+id/color_picker_new_hex_label"
android:digits="0123456789ABCDEFabcdef"
android:ems="5"
android:gravity="start"
android:lines="1"
android:maxLength="6"
android:textCursorDrawable="@null"
android:textSize="@dimen/normal_text_size" />
</RelativeLayout>
<RelativeLayout
android:id="@+id/color_picker_bottom_holder"
android:layout_width="match_parent"
android:layout_height="30dp"
android:layout_below="@id/color_picker_hex_codes_holder"
android:layout_marginTop="@dimen/medium_margin"
android:layout_marginBottom="@dimen/medium_margin">
<ImageView
android:id="@+id/color_picker_old_color"
android:layout_width="30dp"
android:layout_height="30dp"
android:layout_toStartOf="@+id/color_picker_arrow"
android:background="@android:color/white" />
<ImageView
android:id="@+id/color_picker_arrow"
android:layout_width="20dp"
android:layout_height="20dp"
android:layout_centerInParent="true"
android:layout_marginStart="@dimen/normal_margin"
android:layout_marginEnd="@dimen/normal_margin"
android:scaleType="centerInside"
android:src="@drawable/ic_arrow_right_vector" />
<ImageView
android:id="@+id/color_picker_new_color"
android:layout_width="30dp"
android:layout_height="30dp"
android:layout_toEndOf="@+id/color_picker_arrow"
android:background="@android:color/white" />
</RelativeLayout>
</RelativeLayout>
</ScrollView>
</layout>

View file

@ -0,0 +1,63 @@
<?xml version="1.0" encoding="utf-8"?>
<layout xmlns:android="http://schemas.android.com/apk/res/android">
<RelativeLayout
android:layout_width="match_parent"
android:layout_height="match_parent"
android:orientation="vertical"
android:paddingLeft="@dimen/large_margin"
android:paddingTop="@dimen/large_margin"
android:paddingRight="@dimen/large_margin">
<com.google.android.material.textfield.TextInputLayout
android:id="@+id/aspect_ratio_width_hint"
android:layout_width="100dp"
android:layout_height="wrap_content"
android:layout_marginBottom="@dimen/large_margin"
android:hint="@string/width">
<com.google.android.material.textfield.TextInputEditText
android:id="@+id/aspect_ratio_width"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:inputType="number"
android:maxLength="6"
android:maxLines="1"
android:textCursorDrawable="@null"
android:textSize="@dimen/bigger_text_size" />
</com.google.android.material.textfield.TextInputLayout>
<TextView
android:id="@+id/aspect_ratio_colon"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_alignTop="@+id/aspect_ratio_width_hint"
android:layout_alignBottom="@+id/aspect_ratio_width_hint"
android:layout_marginStart="20dp"
android:layout_marginEnd="20dp"
android:layout_toEndOf="@+id/aspect_ratio_width_hint"
android:gravity="center"
android:text=":"
android:textStyle="bold" />
<com.google.android.material.textfield.TextInputLayout
android:layout_width="100dp"
android:layout_height="wrap_content"
android:layout_alignTop="@+id/aspect_ratio_width_hint"
android:layout_alignBottom="@+id/aspect_ratio_width_hint"
android:layout_toEndOf="@+id/aspect_ratio_colon"
android:hint="@string/height">
<com.google.android.material.textfield.TextInputEditText
android:id="@+id/aspect_ratio_height"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:inputType="number"
android:maxLength="6"
android:maxLines="1"
android:textCursorDrawable="@null"
android:textSize="@dimen/bigger_text_size" />
</com.google.android.material.textfield.TextInputLayout>
</RelativeLayout>
</layout>

View file

@ -0,0 +1,168 @@
<?xml version="1.0" encoding="utf-8"?>
<layout xmlns:android="http://schemas.android.com/apk/res/android">
<ScrollView
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent"
android:layout_height="match_parent">
<LinearLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="horizontal"
android:paddingLeft="@dimen/large_margin"
android:paddingTop="@dimen/large_margin"
android:paddingRight="@dimen/large_margin"
tools:ignore="HardcodedText">
<RadioGroup
android:id="@+id/other_aspect_ratio_dialog_radio_1"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_marginBottom="@dimen/medium_margin"
android:layout_weight="1">
<androidx.appcompat.widget.AppCompatRadioButton
android:id="@+id/other_aspect_ratio_2_1"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:paddingStart="@dimen/small_margin"
android:paddingTop="@dimen/normal_margin"
android:paddingBottom="@dimen/normal_margin"
android:text="2:1"
android:textSize="@dimen/bigger_text_size" />
<androidx.appcompat.widget.AppCompatRadioButton
android:id="@+id/other_aspect_ratio_3_2"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:paddingStart="@dimen/small_margin"
android:paddingTop="@dimen/normal_margin"
android:paddingBottom="@dimen/normal_margin"
android:text="3:2"
android:textSize="@dimen/bigger_text_size" />
<androidx.appcompat.widget.AppCompatRadioButton
android:id="@+id/other_aspect_ratio_4_3"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:paddingStart="@dimen/small_margin"
android:paddingTop="@dimen/normal_margin"
android:paddingBottom="@dimen/normal_margin"
android:text="4:3"
android:textSize="@dimen/bigger_text_size" />
<androidx.appcompat.widget.AppCompatRadioButton
android:id="@+id/other_aspect_ratio_5_3"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:paddingStart="@dimen/small_margin"
android:paddingTop="@dimen/normal_margin"
android:paddingBottom="@dimen/normal_margin"
android:text="5:3"
android:textSize="@dimen/bigger_text_size" />
<androidx.appcompat.widget.AppCompatRadioButton
android:id="@+id/other_aspect_ratio_16_9"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:paddingStart="@dimen/small_margin"
android:paddingTop="@dimen/normal_margin"
android:paddingBottom="@dimen/normal_margin"
android:text="16:9"
android:textSize="@dimen/bigger_text_size" />
<androidx.appcompat.widget.AppCompatRadioButton
android:id="@+id/other_aspect_ratio_19_9"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:paddingStart="@dimen/small_margin"
android:paddingTop="@dimen/normal_margin"
android:paddingBottom="@dimen/normal_margin"
android:text="19:9"
android:textSize="@dimen/bigger_text_size" />
<androidx.appcompat.widget.AppCompatRadioButton
android:id="@+id/other_aspect_ratio_custom"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:paddingStart="@dimen/small_margin"
android:paddingTop="@dimen/normal_margin"
android:paddingBottom="@dimen/normal_margin"
android:text="@string/custom"
android:textSize="@dimen/bigger_text_size" />
</RadioGroup>
<RadioGroup
android:id="@+id/other_aspect_ratio_dialog_radio_2"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_marginBottom="@dimen/medium_margin"
android:layout_weight="1">
<androidx.appcompat.widget.AppCompatRadioButton
android:id="@+id/other_aspect_ratio_1_2"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:paddingStart="@dimen/small_margin"
android:paddingTop="@dimen/normal_margin"
android:paddingBottom="@dimen/normal_margin"
android:text="1:2"
android:textSize="@dimen/bigger_text_size" />
<androidx.appcompat.widget.AppCompatRadioButton
android:id="@+id/other_aspect_ratio_2_3"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:paddingStart="@dimen/small_margin"
android:paddingTop="@dimen/normal_margin"
android:paddingBottom="@dimen/normal_margin"
android:text="2:3"
android:textSize="@dimen/bigger_text_size" />
<androidx.appcompat.widget.AppCompatRadioButton
android:id="@+id/other_aspect_ratio_3_4"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:paddingStart="@dimen/small_margin"
android:paddingTop="@dimen/normal_margin"
android:paddingBottom="@dimen/normal_margin"
android:text="3:4"
android:textSize="@dimen/bigger_text_size" />
<androidx.appcompat.widget.AppCompatRadioButton
android:id="@+id/other_aspect_ratio_3_5"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:paddingStart="@dimen/small_margin"
android:paddingTop="@dimen/normal_margin"
android:paddingBottom="@dimen/normal_margin"
android:text="3:5"
android:textSize="@dimen/bigger_text_size" />
<androidx.appcompat.widget.AppCompatRadioButton
android:id="@+id/other_aspect_ratio_9_16"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:paddingStart="@dimen/small_margin"
android:paddingTop="@dimen/normal_margin"
android:paddingBottom="@dimen/normal_margin"
android:text="9:16"
android:textSize="@dimen/bigger_text_size" />
<androidx.appcompat.widget.AppCompatRadioButton
android:id="@+id/other_aspect_ratio_9_19"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:paddingStart="@dimen/small_margin"
android:paddingTop="@dimen/normal_margin"
android:paddingBottom="@dimen/normal_margin"
android:text="9:19"
android:textSize="@dimen/bigger_text_size" />
</RadioGroup>
</LinearLayout>
</ScrollView>
</layout>

View file

@ -0,0 +1,74 @@
<?xml version="1.0" encoding="utf-8"?>
<layout xmlns:android="http://schemas.android.com/apk/res/android">
<RelativeLayout
android:layout_width="match_parent"
android:layout_height="match_parent"
android:orientation="vertical"
android:paddingLeft="@dimen/large_margin"
android:paddingTop="@dimen/large_margin"
android:paddingRight="@dimen/large_margin">
<com.google.android.material.textfield.TextInputLayout
android:id="@+id/resize_image_width_hint"
android:layout_width="100dp"
android:layout_height="wrap_content"
android:layout_marginBottom="@dimen/large_margin"
android:hint="@string/width">
<com.google.android.material.textfield.TextInputEditText
android:id="@+id/resize_image_width"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:inputType="number"
android:maxLength="6"
android:maxLines="1"
android:textCursorDrawable="@null"
android:textSize="@dimen/bigger_text_size" />
</com.google.android.material.textfield.TextInputLayout>
<com.google.android.material.textfield.TextInputLayout
android:id="@+id/resize_image_colon"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_alignTop="@+id/resize_image_width_hint"
android:layout_alignBottom="@+id/resize_image_width_hint"
android:layout_marginStart="20dp"
android:layout_marginEnd="20dp"
android:layout_toEndOf="@+id/resize_image_width_hint"
android:gravity="center"
android:text=":"
android:textStyle="bold" />
<com.google.android.material.textfield.TextInputLayout
android:layout_width="100dp"
android:layout_height="wrap_content"
android:layout_alignTop="@+id/resize_image_width_hint"
android:layout_alignBottom="@+id/resize_image_width_hint"
android:layout_toEndOf="@+id/resize_image_colon"
android:hint="@string/height">
<com.google.android.material.textfield.TextInputEditText
android:id="@+id/resize_image_height"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:inputType="number"
android:maxLength="6"
android:maxLines="1"
android:textCursorDrawable="@null"
android:textSize="@dimen/bigger_text_size" />
</com.google.android.material.textfield.TextInputLayout>
<androidx.appcompat.widget.AppCompatCheckBox
android:id="@+id/keep_aspect_ratio"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_below="@+id/resize_image_width_hint"
android:checked="true"
android:paddingTop="@dimen/large_margin"
android:paddingBottom="@dimen/large_margin"
android:text="@string/keep_aspect_ratio" />
</RelativeLayout>
</layout>

View file

@ -0,0 +1,31 @@
<?xml version="1.0" encoding="utf-8"?>
<layout xmlns:android="http://schemas.android.com/apk/res/android">
<RelativeLayout
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="wrap_content"
android:layout_height="wrap_content">
<ImageView
android:id="@+id/editor_filter_item_thumbnail"
android:layout_width="@dimen/bottom_filters_thumbnail_size"
android:layout_height="@dimen/bottom_filters_thumbnail_size"
android:layout_above="@+id/editor_filter_item_label"
android:background="@drawable/stroke_background"
android:contentDescription="@null"
android:padding="1dp"/>
<TextView
android:id="@+id/editor_filter_item_label"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_alignParentBottom="true"
android:layout_centerHorizontal="true"
android:gravity="center_horizontal"
android:textColor="@android:color/white"
android:textSize="@dimen/smaller_text_size"
tools:text="Filter"/>
</RelativeLayout>
</layout>

View file

@ -0,0 +1,9 @@
<menu
xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto">
<item android:id="@+id/action_done"
app:showAsAction="ifRoom"
android:showAsAction="ifRoom"
android:title="@string/done"
android:icon="@drawable/ic_done_24dp"/>
</menu>

View file

@ -46,4 +46,7 @@
<color name="blue500">#ff2196f3</color> <color name="blue500">#ff2196f3</color>
<color name="blue_alpha">#aa82B1FF</color> <color name="blue_alpha">#aa82B1FF</color>
<color name="crop_image_view_background">#BB000000</color>
<color name="editor_draw_default_color">#000000</color>
</resources> </resources>

View file

@ -44,4 +44,34 @@
<dimen name="local_video_preview_height">128dp</dimen> <dimen name="local_video_preview_height">128dp</dimen>
<dimen name="local_video_preview_width">96dp</dimen> <dimen name="local_video_preview_width">96dp</dimen>
<dimen name="rtp_session_duration_top_margin">24dp</dimen> <dimen name="rtp_session_duration_top_margin">24dp</dimen>
<dimen name="selection_check_size">26dp</dimen>
<dimen name="bottom_actions_height">64dp</dimen>
<dimen name="bottom_actions_small_height">48dp</dimen>
<dimen name="bottom_actions_height_double">120dp</dimen>
<dimen name="bottom_actions_height_bigger">172dp</dimen>
<dimen name="bottom_editor_color_picker_size">48dp</dimen>
<dimen name="bottom_filters_thumbnail_size">76dp</dimen>
<dimen name="bottom_filters_height">90dp</dimen>
<dimen name="bottom_filters_height_with_margin">98dp</dimen>
<dimen name="bottom_editor_actions_shadow_height">180dp</dimen>
<dimen name="full_brush_size">40dp</dimen>
<dimen name="one_dp">1dp</dimen>
<dimen name="tiny_margin">2dp</dimen>
<dimen name="small_margin">4dp</dimen>
<dimen name="smaller_margin">6dp</dimen>
<dimen name="medium_margin">8dp</dimen>
<dimen name="normal_margin">12dp</dimen>
<dimen name="large_margin">16dp</dimen>
<dimen name="tiny_text_size">8sp</dimen>
<dimen name="small_text_size">10sp</dimen>
<dimen name="smaller_text_size">12sp</dimen>
<dimen name="normal_text_size">14sp</dimen>
<dimen name="medium_text_size">15sp</dimen>
<dimen name="bigger_text_size">16sp</dimen>
<dimen name="big_text_size">18sp</dimen>
<dimen name="colorpicker_hue_width">30dp</dimen>
</resources> </resources>

View file

@ -1011,4 +1011,35 @@
<string name="delete_from_server">Remove account from server</string> <string name="delete_from_server">Remove account from server</string>
<string name="could_not_delete_account_from_server">Could not delete account from server</string> <string name="could_not_delete_account_from_server">Could not delete account from server</string>
<string name="custom">Custom</string>
<string name="resize_and_save">Resize selection and save</string>
<string name="width">Width</string>
<string name="height">Height</string>
<string name="keep_aspect_ratio">Keep aspect ratio</string>
<string name="invalid_values">Please enter a valid resolution</string>
<string name="editor">Editor</string>
<string name="basic_editor">Basic Editor</string>
<string name="rotate">Rotate</string>
<string name="invalid_image_path">Invalid image path</string>
<string name="image_editing_failed">Image editing failed</string>
<string name="unknown_file_location">Unknown file location</string>
<string name="transform">Transform</string>
<string name="crop">Crop</string>
<string name="draw">Draw</string>
<string name="flip_horizontally">Flip horizontally</string>
<string name="flip_vertically">Flip vertically</string>
<string name="free_aspect_ratio">Free</string>
<string name="other_aspect_ratio">Other</string>
<string name="thumbnails">Thumbnails</string>
<string name="saving">saving</string>
<string name="out_of_memory_error">out_of_memory_error</string>
<string name="file_saved">file_saved</string>
<string name="simple_commons">simple_commons</string>
<string name="value_copied_to_clipboard_show">value_copied_to_clipboard_show</string>
<string name="default_color">default_color</string>
<string name="unknown_error_occurred">unknown_error_occurred</string>
<string name="change_color">change_color</string>
<string name="resize">resize</string>
<string name="filter">filter</string>
<string name="could_not_create_file">could_not_create_file</string>
</resources> </resources>

View file

@ -376,9 +376,10 @@
<style name="ConversationsTheme.FullScreen" parent="@style/Theme.AppCompat.Light"> <style name="ConversationsTheme.FullScreen" parent="@style/Theme.AppCompat.Light">
<item name="colorPrimary">@color/green600</item> <item name="colorPrimary">@color/green600</item>
<item name="colorPrimaryDark">@color/green700</item> <item name="colorPrimaryDark">@color/green700</item>
<item name="colorAccent">@color/green600</item>
<item name="colorSurface">@color/white</item>
<item name="android:windowNoTitle">true</item> <item name="android:windowNoTitle">true</item>
<item name="android:windowActionBar">false</item> <item name="android:windowActionBar">false</item>
<item name="android:windowFullscreen">true</item>
<item name="android:windowContentOverlay">@null</item> <item name="android:windowContentOverlay">@null</item>
<item name="android:windowBackground">@android:color/black</item> <item name="android:windowBackground">@android:color/black</item>
<item name="android:navigationBarColor" tools:targetApi="21">@color/black</item> <item name="android:navigationBarColor" tools:targetApi="21">@color/black</item>