From 9ba758fdaa168f26025dfd408b4e44f235b6beba Mon Sep 17 00:00:00 2001 From: M66B Date: Mon, 6 Jan 2025 14:33:22 +0100 Subject: [PATCH] Image editor: refactoring --- FAQ.md | 6 + .../eu/faircode/email/FragmentCompose.java | 182 ++++++------------ .../email/FragmentDialogEditImage.java | 175 +++++++++++++++++ app/src/main/res/layout/dialog_edit_image.xml | 24 ++- index.html | 3 + 5 files changed, 256 insertions(+), 134 deletions(-) create mode 100644 app/src/main/java/eu/faircode/email/FragmentDialogEditImage.java diff --git a/FAQ.md b/FAQ.md index 93e38b0369..127e39a687 100644 --- a/FAQ.md +++ b/FAQ.md @@ -4301,6 +4301,12 @@ Show force light menu item / button (when configured) to force a light theme for
+*Basic image editor (1.2257+)* + +Display a basic image editor when tapping an inserted image. + +
+ **(126) Can message previews be sent to my smartwatch?** diff --git a/app/src/main/java/eu/faircode/email/FragmentCompose.java b/app/src/main/java/eu/faircode/email/FragmentCompose.java index 489ebeac89..e5caf7c2fa 100644 --- a/app/src/main/java/eu/faircode/email/FragmentCompose.java +++ b/app/src/main/java/eu/faircode/email/FragmentCompose.java @@ -26,7 +26,6 @@ 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; @@ -137,7 +136,6 @@ 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; @@ -354,6 +352,7 @@ public class FragmentCompose extends FragmentBase { private static final int REQUEST_SEND = 16; static final int REQUEST_EDIT_ATTACHMENT = 17; private static final int REQUEST_REMOVE_ATTACHMENTS = 18; + private static final int REQUEST_EDIT_IMAGE = 19; ActivityResultLauncher pickImages; @@ -460,6 +459,7 @@ public class FragmentCompose extends FragmentBase { final boolean suggest_account = prefs.getBoolean("suggest_account", false); final boolean cc_bcc = prefs.getBoolean("cc_bcc", false); final boolean circular = prefs.getBoolean("circular", true); + final boolean experiments = prefs.getBoolean("experiments", false); final float dp3 = Helper.dp2pixels(getContext(), 3); @@ -822,7 +822,7 @@ public class FragmentCompose extends FragmentBase { } }); - if (BuildConfig.DEBUG) + if (experiments) etBody.setOnTouchListener(new View.OnTouchListener() { private final GestureDetector gestureDetector = new GestureDetector(getContext(), new GestureDetector.SimpleOnGestureListener() { @@ -852,136 +852,17 @@ public class FragmentCompose extends FragmentBase { 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"); + FragmentDialogEditImage fragment = new FragmentDialogEditImage(); + fragment.setArguments(args); + fragment.setTargetFragment(FragmentCompose.this, REQUEST_EDIT_IMAGE); + fragment.show(getParentFragmentManager(), "edit:image"); return true; } catch (Throwable ex) { Log.e(ex); + return false; } - return false; } }); @@ -3632,6 +3513,10 @@ public class FragmentCompose extends FragmentBase { if (resultCode == RESULT_OK) onRemoveAttachments(); break; + case REQUEST_EDIT_IMAGE: + if (resultCode == RESULT_OK && data != null) + onEditImage(data.getBundleExtra("args")); + break; } } catch (Throwable ex) { Log.e(ex); @@ -5303,6 +5188,49 @@ public class FragmentCompose extends FragmentBase { }.serial().execute(FragmentCompose.this, args, "attachments:remove"); } + private void onEditImage(Bundle args) { + args.putInt("zoom", zoom); + args.putLong("working", working); + + new SimpleTask() { + @Override + protected Drawable onExecute(Context context, Bundle args) throws Throwable { + String source = args.getString("source"); + int zoom = args.getInt("zoom"); + long working = args.getLong("working"); + + 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(this, args, "update:image"); + } + private void onExit() { if (state == State.LOADED) { state = State.NONE; diff --git a/app/src/main/java/eu/faircode/email/FragmentDialogEditImage.java b/app/src/main/java/eu/faircode/email/FragmentDialogEditImage.java new file mode 100644 index 0000000000..035635efd3 --- /dev/null +++ b/app/src/main/java/eu/faircode/email/FragmentDialogEditImage.java @@ -0,0 +1,175 @@ +package eu.faircode.email; + +/* + This file is part of FairEmail. + + FairEmail is free software: you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation, either version 3 of the License, or + (at your option) any later version. + + FairEmail is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + + You should have received a copy of the GNU General Public License + along with FairEmail. If not, see . + + Copyright 2018-2025 by Marcel Bokhorst (M66B) +*/ + +import static android.app.Activity.RESULT_OK; + +import android.app.Dialog; +import android.content.Context; +import android.graphics.Bitmap; +import android.os.Bundle; +import android.view.LayoutInflater; +import android.view.View; +import android.widget.ImageButton; +import android.widget.ProgressBar; + +import androidx.annotation.NonNull; +import androidx.annotation.Nullable; +import androidx.appcompat.app.AlertDialog; + +import com.canhub.cropper.CropImageView; + +import java.io.BufferedOutputStream; +import java.io.File; +import java.io.FileOutputStream; +import java.io.OutputStream; + +public class FragmentDialogEditImage extends FragmentDialogBase { + @NonNull + @Override + public Dialog onCreateDialog(@Nullable Bundle savedInstanceState) { + Bundle args = getArguments(); + + Context context = 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); + ProgressBar pbWait = dview.findViewById(R.id.pbWait); + + 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) { + new SimpleTask() { + @Override + protected void onPreExecute(Bundle args) { + ibSave.setEnabled(true); + pbWait.setVisibility(View.VISIBLE); + } + + @Override + protected void onPostExecute(Bundle args) { + ibSave.setEnabled(true); + pbWait.setVisibility(View.GONE); + } + + @Override + protected Void onExecute(Context context, Bundle args) throws Throwable { + long id = args.getLong("id"); + + 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 null; + } + + @Override + protected void onExecuted(Bundle args, Void data) { + sendResult(RESULT_OK); + dismiss(); + } + + @Override + protected void onException(Bundle args, Throwable ex) { + Log.unexpectedError(getParentFragment(), ex); + } + }.execute(FragmentDialogEditImage.this, args, "save:image"); + } + }); + + new SimpleTask() { + @Override + protected void onPreExecute(Bundle args) { + pbWait.setVisibility(View.VISIBLE); + ibSave.setEnabled(false); + } + + @Override + protected void onPostExecute(Bundle args) { + pbWait.setVisibility(View.GONE); + ibSave.setEnabled(true); + } + + @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; + + civ.setImageUriAsync(attachment.getUri(context)); + } + + @Override + protected void onException(Bundle args, Throwable ex) { + Log.unexpectedError(getParentFragmentManager(), ex); + } + }.execute(this, args, "edit:image"); + + return dialog; + } +} diff --git a/app/src/main/res/layout/dialog_edit_image.xml b/app/src/main/res/layout/dialog_edit_image.xml index 4ef3bc89ca..3ccc556e0d 100644 --- a/app/src/main/res/layout/dialog_edit_image.xml +++ b/app/src/main/res/layout/dialog_edit_image.xml @@ -9,6 +9,7 @@ android:id="@+id/civ" android:layout_width="match_parent" android:layout_height="wrap_content" + android:minHeight="60dp" app:layout_constraintBottom_toTopOf="@+id/ibRotate" app:layout_constraintEnd_toEndOf="parent" app:layout_constraintStart_toStartOf="parent" @@ -20,9 +21,7 @@ android:layout_height="36dp" android:layout_marginTop="12dp" android:background="?android:attr/selectableItemBackgroundBorderless" - android:contentDescription="@string/title_info" android:scaleType="fitCenter" - android:tooltipText="@string/title_info" app:layout_constraintBottom_toBottomOf="parent" app:layout_constraintStart_toStartOf="parent" app:layout_constraintTop_toBottomOf="@id/civ" @@ -36,9 +35,7 @@ android:layout_marginStart="24dp" android:layout_marginTop="12dp" android:background="?android:attr/selectableItemBackgroundBorderless" - android:contentDescription="@string/title_info" android:scaleType="fitCenter" - android:tooltipText="@string/title_info" app:layout_constraintBottom_toBottomOf="parent" app:layout_constraintStart_toEndOf="@id/ibRotate" app:layout_constraintTop_toBottomOf="@id/civ" @@ -52,13 +49,13 @@ android:layout_marginStart="24dp" android:layout_marginTop="12dp" android:background="?android:attr/selectableItemBackgroundBorderless" - android:contentDescription="@string/title_save" + android:contentDescription="@android:string/cancel" android:scaleType="fitCenter" - android:tooltipText="@string/title_save" + android:tooltipText="@android:string/cancel" app:layout_constraintBottom_toBottomOf="parent" app:layout_constraintStart_toEndOf="@id/ibFlip" app:layout_constraintTop_toBottomOf="@id/civ" - app:srcCompat="@drawable/twotone_cancel_24" /> + app:srcCompat="@drawable/twotone_close_24" /> + + \ No newline at end of file diff --git a/index.html b/index.html index 20ce7aaea2..976685568b 100644 --- a/index.html +++ b/index.html @@ -2237,6 +2237,9 @@ $NotDisplayed

Force light for reformatted message view (1.2254+)

Show force light menu item / button (when configured) to force a light theme for reformatted messages.


+

Basic image editor (1.2257+)

+

Display a basic image editor when tapping an inserted image.

+


(126) Can message previews be sent to my smartwatch?

🌎 Google Translate

FairEmail fetches a message in two steps: