diff --git a/ATTRIBUTION.md b/ATTRIBUTION.md index a21b07872f..c41e0bf35d 100644 --- a/ATTRIBUTION.md +++ b/ATTRIBUTION.md @@ -58,3 +58,4 @@ FairEmail uses parts or all of: * [commonmark-java](https://github.com/commonmark/commonmark-java). Copyright (c) 2015, Atlassian Pty Ltd. All rights reserved. [BSD-2-Clause license](https://github.com/commonmark/commonmark-java/blob/main/LICENSE.txt). * [flexmark-java](https://github.com/vsch/flexmark-java). Copyright (c) 2016-2018, Vladimir Schneider. All rights reserved. [BSD-2-Clause license](https://github.com/vsch/flexmark-java/blob/master/LICENSE.txt). * [EvalEx](https://github.com/ezylang/EvalEx). Copyright 2012-2022 Udo Klimaschewski. [Apache License 2.0](https://github.com/ezylang/EvalEx/blob/main/LICENSE). +* [Android Image Cropper](https://github.com/CanHub/Android-Image-Cropper). Copyright 2016, Arthur Teplitzki, 2013, Edmodo, Inc. [Apache License 2.0](https://github.com/CanHub/Android-Image-Cropper/blob/main/LICENSE.txt). diff --git a/app/build.gradle b/app/build.gradle index f489c7a31a..961e3f22c0 100644 --- a/app/build.gradle +++ b/app/build.gradle @@ -617,6 +617,7 @@ dependencies { def tinylog_version = "2.7.0" def zxing_version = "3.5.3" def evalex_version = "3.3.0" + def image_cropper_version = "4.6.0" // https://mvnrepository.com/artifact/com.android.tools/desugar_jdk_libs?repo=google coreLibraryDesugaring "com.android.tools:desugar_jdk_libs:$desugar_version" @@ -900,4 +901,7 @@ dependencies { // https://github.com/ezylang/EvalEx implementation "com.ezylang:EvalEx:$evalex_version" + + // https://github.com/CanHub/Android-Image-Cropper + implementation "com.vanniktech:android-image-cropper:$image_cropper_version" } diff --git a/app/src/main/assets/ATTRIBUTION.md b/app/src/main/assets/ATTRIBUTION.md index a21b07872f..c41e0bf35d 100644 --- a/app/src/main/assets/ATTRIBUTION.md +++ b/app/src/main/assets/ATTRIBUTION.md @@ -58,3 +58,4 @@ FairEmail uses parts or all of: * [commonmark-java](https://github.com/commonmark/commonmark-java). Copyright (c) 2015, Atlassian Pty Ltd. All rights reserved. [BSD-2-Clause license](https://github.com/commonmark/commonmark-java/blob/main/LICENSE.txt). * [flexmark-java](https://github.com/vsch/flexmark-java). Copyright (c) 2016-2018, Vladimir Schneider. All rights reserved. [BSD-2-Clause license](https://github.com/vsch/flexmark-java/blob/master/LICENSE.txt). * [EvalEx](https://github.com/ezylang/EvalEx). Copyright 2012-2022 Udo Klimaschewski. [Apache License 2.0](https://github.com/ezylang/EvalEx/blob/main/LICENSE). +* [Android Image Cropper](https://github.com/CanHub/Android-Image-Cropper). Copyright 2016, Arthur Teplitzki, 2013, Edmodo, Inc. [Apache License 2.0](https://github.com/CanHub/Android-Image-Cropper/blob/main/LICENSE.txt). diff --git a/app/src/main/java/eu/faircode/email/FragmentCompose.java b/app/src/main/java/eu/faircode/email/FragmentCompose.java index 2e047d633a..489ebeac89 100644 --- a/app/src/main/java/eu/faircode/email/FragmentCompose.java +++ b/app/src/main/java/eu/faircode/email/FragmentCompose.java @@ -26,6 +26,7 @@ import static android.view.inputmethod.EditorInfo.IME_FLAG_NO_FULLSCREEN; import android.Manifest; import android.app.Activity; +import android.app.Dialog; import android.app.PendingIntent; import android.content.ClipData; import android.content.ClipboardManager; @@ -79,6 +80,7 @@ import android.text.style.RelativeSizeSpan; import android.text.style.URLSpan; import android.util.Pair; import android.util.TypedValue; +import android.view.GestureDetector; import android.view.Gravity; import android.view.HapticFeedbackConstants; import android.view.KeyEvent; @@ -135,6 +137,7 @@ import androidx.preference.PreferenceManager; import androidx.recyclerview.widget.LinearLayoutManager; import androidx.recyclerview.widget.RecyclerView; +import com.canhub.cropper.CropImageView; import com.google.android.material.bottomnavigation.BottomNavigationView; import com.google.android.material.bottomnavigation.LabelVisibilityMode; import com.google.android.material.snackbar.Snackbar; @@ -819,6 +822,175 @@ public class FragmentCompose extends FragmentBase { } }); + if (BuildConfig.DEBUG) + etBody.setOnTouchListener(new View.OnTouchListener() { + private final GestureDetector gestureDetector = new GestureDetector(getContext(), + new GestureDetector.SimpleOnGestureListener() { + @Override + public boolean onSingleTapConfirmed(@NonNull MotionEvent event) { + try { + Editable buffer = etBody.getText(); + if (buffer == null) + return false; + + int off = Helper.getOffset(etBody, buffer, event); + ImageSpan[] image = buffer.getSpans(off, off, ImageSpan.class); + if (image == null || image.length == 0) + return false; + + String source = image[0].getSource(); + if (source == null || !source.startsWith("cid:")) + return false; + + long id = Long.parseLong(source.substring(source.lastIndexOf('.') + 1)); + + int start = buffer.getSpanStart(image[0]); + int end = buffer.getSpanEnd(image[0]); + + Bundle args = new Bundle(); + args.putLong("id", id); + args.putString("source", source); + args.putInt("start", start); + args.putInt("end", end); + args.putInt("zoom", zoom); + args.putLong("working", working); + + new SimpleTask() { + @Override + protected EntityAttachment onExecute(Context context, Bundle args) { + long id = args.getLong("id"); + + DB db = DB.getInstance(context); + return db.attachment().getAttachment(id); + } + + @Override + protected void onExecuted(Bundle args, EntityAttachment attachment) { + if (attachment == null) + return; + + Context context = etBody.getContext(); + + View dview = LayoutInflater.from(context).inflate(R.layout.dialog_edit_image, null); + ImageButton ibRotate = dview.findViewById(R.id.ibRotate); + ImageButton ibFlip = dview.findViewById(R.id.ibFlip); + ImageButton ibCancel = dview.findViewById(R.id.ibCancel); + ImageButton ibSave = dview.findViewById(R.id.ibSave); + CropImageView civ = dview.findViewById(R.id.civ); + + Dialog dialog = new AlertDialog.Builder(context) + .setView(dview) + .create(); + + ibRotate.setOnClickListener(new View.OnClickListener() { + @Override + public void onClick(View v) { + civ.rotateImage(90); + } + }); + + ibFlip.setOnClickListener(new View.OnClickListener() { + @Override + public void onClick(View v) { + civ.flipImageHorizontally(); + } + }); + + ibCancel.setOnClickListener(new View.OnClickListener() { + @Override + public void onClick(View v) { + dialog.dismiss(); + } + }); + + ibSave.setOnClickListener(new View.OnClickListener() { + @Override + public void onClick(View v) { + dialog.dismiss(); + + new SimpleTask() { + @Override + protected Drawable onExecute(Context context, Bundle args) throws Throwable { + long id = args.getLong("id"); + String source = args.getString("source"); + int zoom = args.getInt("zoom"); + long working = args.getLong("working"); + + DB db = DB.getInstance(context); + EntityAttachment attachment = db.attachment().getAttachment(id); + if (attachment == null) + return null; + + Bitmap bm = civ.getCroppedImage(); + if (bm == null) + return null; + + File file = attachment.getFile(context); + + try (OutputStream os = new BufferedOutputStream(new FileOutputStream(file))) { + bm.compress(Bitmap.CompressFormat.PNG, 90, os); + } + + db.attachment().setName(id, attachment.name, "image/png", file.length()); + + return ImageHelper.decodeImage(context, working, source, true, zoom, 1.0f, etBody); + } + + @Override + protected void onExecuted(Bundle args, Drawable d) { + if (d == null) + return; + + String source = args.getString("source"); + int start = args.getInt("start"); + int end = args.getInt("end"); + + Editable buffer = etBody.getText(); + if (buffer == null) + return; + + ImageSpan[] image = buffer.getSpans(start, end, ImageSpan.class); + if (image == null || image.length == 0) + return; + + int flags = buffer.getSpanFlags(image[0]); + buffer.removeSpan(image[0]); + buffer.setSpan(new ImageSpan(d, source), start, end, flags); + } + + @Override + protected void onException(Bundle args, Throwable ex) { + Log.unexpectedError(getParentFragment(), ex); + } + }.execute(FragmentCompose.this, args, "save:image"); + } + }); + + civ.setImageUriAsync(attachment.getUri(context)); + + dialog.show(); + } + + @Override + protected void onException(Bundle args, Throwable ex) { + Log.unexpectedError(getParentFragmentManager(), ex); + } + }.execute(FragmentCompose.this, args, "edit:image"); + + return true; + } catch (Throwable ex) { + Log.e(ex); + } + return false; + } + }); + + @Override + public boolean onTouch(View v, MotionEvent event) { + return gestureDetector.onTouchEvent(event); + } + }); + if (compose_color != Color.TRANSPARENT) tvSignature.setTextColor(compose_color); tvSignature.setTypeface(StyleHelper.getTypeface(compose_font, getContext())); diff --git a/app/src/main/res/drawable/twotone_flip_24.xml b/app/src/main/res/drawable/twotone_flip_24.xml new file mode 100644 index 0000000000..f2584f0aeb --- /dev/null +++ b/app/src/main/res/drawable/twotone_flip_24.xml @@ -0,0 +1,10 @@ + + + diff --git a/app/src/main/res/drawable/twotone_rotate_right_24.xml b/app/src/main/res/drawable/twotone_rotate_right_24.xml new file mode 100644 index 0000000000..b8188c069d --- /dev/null +++ b/app/src/main/res/drawable/twotone_rotate_right_24.xml @@ -0,0 +1,11 @@ + + + diff --git a/app/src/main/res/drawable/twotone_rotate_right_white_18.png b/app/src/main/res/drawable/twotone_rotate_right_white_18.png new file mode 100644 index 0000000000..2d8da3054c Binary files /dev/null and b/app/src/main/res/drawable/twotone_rotate_right_white_18.png differ diff --git a/app/src/main/res/drawable/twotone_rotate_right_white_20.png b/app/src/main/res/drawable/twotone_rotate_right_white_20.png new file mode 100644 index 0000000000..00153d990e Binary files /dev/null and b/app/src/main/res/drawable/twotone_rotate_right_white_20.png differ diff --git a/app/src/main/res/drawable/twotone_rotate_right_white_24.png b/app/src/main/res/drawable/twotone_rotate_right_white_24.png new file mode 100644 index 0000000000..bf31fe62c8 Binary files /dev/null and b/app/src/main/res/drawable/twotone_rotate_right_white_24.png differ diff --git a/app/src/main/res/drawable/twotone_rotate_right_white_36.png b/app/src/main/res/drawable/twotone_rotate_right_white_36.png new file mode 100644 index 0000000000..87d77c6d5e Binary files /dev/null and b/app/src/main/res/drawable/twotone_rotate_right_white_36.png differ diff --git a/app/src/main/res/drawable/twotone_rotate_right_white_48.png b/app/src/main/res/drawable/twotone_rotate_right_white_48.png new file mode 100644 index 0000000000..162ecc1714 Binary files /dev/null and b/app/src/main/res/drawable/twotone_rotate_right_white_48.png differ diff --git a/app/src/main/res/layout/dialog_edit_image.xml b/app/src/main/res/layout/dialog_edit_image.xml new file mode 100644 index 0000000000..4ef3bc89ca --- /dev/null +++ b/app/src/main/res/layout/dialog_edit_image.xml @@ -0,0 +1,77 @@ + + + + + + + + + + + + + \ No newline at end of file