From 642e96a439f41a5191d4b1d8be2348418490d345 Mon Sep 17 00:00:00 2001 From: Grishka Date: Tue, 9 May 2023 21:34:42 +0300 Subject: [PATCH] Compose M3 redesign wip --- .../android/fragments/ComposeFragment.java | 962 ++++++++++++------ .../ComposeImageDescriptionFragment.java | 215 +++- .../fragments/MastodonToolbarFragment.java | 9 + .../android/fragments/ProfileFragment.java | 4 + .../ui/ComposeAutocompleteViewController.java | 54 +- ...ComposeAutocompleteBackgroundDrawable.java | 84 -- .../android/ui/photoviewer/PhotoViewer.java | 4 + .../ui/text/LengthLimitHighlighter.java | 67 ++ .../android/ui/utils/UiUtils.java | 22 +- .../ui/views/CheckableLinearLayout.java | 50 + .../ui/views/FixedAspectRatioImageView.java | 36 + .../ui/views/FloatingHintEditTextLayout.java | 7 +- ...ntalScrollViewThatRespectsMatchParent.java | 36 + .../ui/views/ReorderableLinearLayout.java | 215 +++- .../src/main/res/color/action_bar_icons.xml | 5 + .../src/main/res/color/compose_button.xml | 5 +- .../main/res/color/m3_on_primary_overlay.xml | 4 + mastodon/src/main/res/drawable-nodpi/poof.png | Bin 0 -> 7549 bytes mastodon/src/main/res/drawable/bg_alert.xml | 6 +- .../src/main/res/drawable/bg_alert_button.xml | 2 +- .../main/res/drawable/bg_compose_button.xml | 25 + mastodon/src/main/res/drawable/bg_cw_edit.xml | 20 +- .../bg_m3_outlined_text_field_error_nopad.xml | 9 + .../main/res/drawable/bg_rect_4dp_ripple.xml | 9 + .../src/main/res/drawable/bg_round_ripple.xml | 2 +- .../drawable/divider_vertical_variant_1dp.xml | 5 + .../res/drawable/fg_compose_attachment.xml | 5 + .../drawable/ic_add_photo_alternate_24px.xml | 9 + .../res/drawable/ic_alternate_email_20px.xml | 9 + .../src/main/res/drawable/ic_compose_cw.xml | 5 + .../main/res/drawable/ic_compose_emoji.xml | 5 + .../src/main/res/drawable/ic_compose_poll.xml | 5 + .../res/drawable/ic_drag_indicator_20px.xml | 9 + .../src/main/res/drawable/ic_group_20px.xml | 9 + .../src/main/res/drawable/ic_help_24px.xml | 9 + .../res/drawable/ic_insert_chart_24px.xml | 9 + .../drawable/ic_insert_chart_fill1_24px.xml | 9 + .../src/main/res/drawable/ic_mood_24px.xml | 9 + .../main/res/drawable/ic_mood_fill1_24px.xml | 9 + .../src/main/res/drawable/ic_public_20px.xml | 9 + .../main/res/drawable/ic_restart_alt_24px.xml | 9 + .../src/main/res/drawable/ic_send_24px.xml | 9 + .../src/main/res/drawable/ic_warning_24px.xml | 9 + .../res/drawable/ic_warning_fill1_24px.xml | 9 + .../src/main/res/drawable/m3_progress.xml | 15 + .../src/main/res/drawable/poll_multiple.xml | 61 ++ .../src/main/res/drawable/poll_single.xml | 75 ++ .../main/res/layout/compose_media_thumb.xml | 206 ++-- .../main/res/layout/compose_poll_option.xml | 58 +- .../src/main/res/layout/fragment_compose.xml | 428 +++++--- .../res/layout/fragment_image_description.xml | 74 +- .../res/layout/item_autocomplete_hashtag.xml | 12 + .../res/layout/item_autocomplete_user.xml | 43 +- .../src/main/res/layout/photo_viewer_ui.xml | 2 +- mastodon/src/main/res/layout/poll_style.xml | 84 ++ .../toolbar_fragment_with_progressbar.xml | 36 + mastodon/src/main/res/menu/compose.xml | 4 + .../res/menu/compose_image_description.xml | 4 + .../src/main/res/menu/compose_visibility.xml | 3 - mastodon/src/main/res/values/strings.xml | 21 +- mastodon/src/main/res/values/styles.xml | 41 +- 61 files changed, 2300 insertions(+), 870 deletions(-) delete mode 100644 mastodon/src/main/java/org/joinmastodon/android/ui/drawables/ComposeAutocompleteBackgroundDrawable.java create mode 100644 mastodon/src/main/java/org/joinmastodon/android/ui/text/LengthLimitHighlighter.java create mode 100644 mastodon/src/main/java/org/joinmastodon/android/ui/views/CheckableLinearLayout.java create mode 100644 mastodon/src/main/java/org/joinmastodon/android/ui/views/FixedAspectRatioImageView.java create mode 100644 mastodon/src/main/java/org/joinmastodon/android/ui/views/HorizontalScrollViewThatRespectsMatchParent.java create mode 100644 mastodon/src/main/res/color/action_bar_icons.xml create mode 100644 mastodon/src/main/res/color/m3_on_primary_overlay.xml create mode 100644 mastodon/src/main/res/drawable-nodpi/poof.png create mode 100644 mastodon/src/main/res/drawable/bg_compose_button.xml create mode 100644 mastodon/src/main/res/drawable/bg_m3_outlined_text_field_error_nopad.xml create mode 100644 mastodon/src/main/res/drawable/bg_rect_4dp_ripple.xml create mode 100644 mastodon/src/main/res/drawable/divider_vertical_variant_1dp.xml create mode 100644 mastodon/src/main/res/drawable/fg_compose_attachment.xml create mode 100644 mastodon/src/main/res/drawable/ic_add_photo_alternate_24px.xml create mode 100644 mastodon/src/main/res/drawable/ic_alternate_email_20px.xml create mode 100644 mastodon/src/main/res/drawable/ic_compose_cw.xml create mode 100644 mastodon/src/main/res/drawable/ic_compose_emoji.xml create mode 100644 mastodon/src/main/res/drawable/ic_compose_poll.xml create mode 100644 mastodon/src/main/res/drawable/ic_drag_indicator_20px.xml create mode 100644 mastodon/src/main/res/drawable/ic_group_20px.xml create mode 100644 mastodon/src/main/res/drawable/ic_help_24px.xml create mode 100644 mastodon/src/main/res/drawable/ic_insert_chart_24px.xml create mode 100644 mastodon/src/main/res/drawable/ic_insert_chart_fill1_24px.xml create mode 100644 mastodon/src/main/res/drawable/ic_mood_24px.xml create mode 100644 mastodon/src/main/res/drawable/ic_mood_fill1_24px.xml create mode 100644 mastodon/src/main/res/drawable/ic_public_20px.xml create mode 100644 mastodon/src/main/res/drawable/ic_restart_alt_24px.xml create mode 100644 mastodon/src/main/res/drawable/ic_send_24px.xml create mode 100644 mastodon/src/main/res/drawable/ic_warning_24px.xml create mode 100644 mastodon/src/main/res/drawable/ic_warning_fill1_24px.xml create mode 100644 mastodon/src/main/res/drawable/m3_progress.xml create mode 100644 mastodon/src/main/res/drawable/poll_multiple.xml create mode 100644 mastodon/src/main/res/drawable/poll_single.xml create mode 100644 mastodon/src/main/res/layout/item_autocomplete_hashtag.xml create mode 100644 mastodon/src/main/res/layout/poll_style.xml create mode 100644 mastodon/src/main/res/layout/toolbar_fragment_with_progressbar.xml create mode 100644 mastodon/src/main/res/menu/compose.xml create mode 100644 mastodon/src/main/res/menu/compose_image_description.xml diff --git a/mastodon/src/main/java/org/joinmastodon/android/fragments/ComposeFragment.java b/mastodon/src/main/java/org/joinmastodon/android/fragments/ComposeFragment.java index ea5ec2a6e..ffb7ee497 100644 --- a/mastodon/src/main/java/org/joinmastodon/android/fragments/ComposeFragment.java +++ b/mastodon/src/main/java/org/joinmastodon/android/fragments/ComposeFragment.java @@ -1,8 +1,12 @@ package org.joinmastodon.android.fragments; +import android.animation.Animator; +import android.animation.AnimatorListenerAdapter; +import android.animation.AnimatorSet; import android.animation.ObjectAnimator; import android.annotation.SuppressLint; import android.app.Activity; +import android.app.AlertDialog; import android.content.ClipData; import android.content.Context; import android.content.Intent; @@ -11,8 +15,9 @@ import android.database.Cursor; import android.graphics.Bitmap; import android.graphics.Outline; import android.graphics.PixelFormat; -import android.graphics.RenderEffect; -import android.graphics.Shader; +import android.graphics.RectF; +import android.graphics.drawable.ColorDrawable; +import android.graphics.drawable.Drawable; import android.graphics.drawable.LayerDrawable; import android.icu.text.BreakIterator; import android.media.MediaMetadataRetriever; @@ -23,13 +28,13 @@ import android.os.Parcelable; import android.provider.MediaStore; import android.provider.OpenableColumns; import android.text.Editable; -import android.text.InputFilter; -import android.text.Layout; import android.text.Spanned; import android.text.TextUtils; import android.text.TextWatcher; +import android.text.style.BackgroundColorSpan; +import android.text.style.ForegroundColorSpan; import android.util.Log; -import android.view.Gravity; +import android.view.HapticFeedbackConstants; import android.view.LayoutInflater; import android.view.Menu; import android.view.MenuInflater; @@ -38,11 +43,12 @@ import android.view.View; import android.view.ViewGroup; import android.view.ViewOutlineProvider; import android.view.WindowManager; -import android.view.animation.LinearInterpolator; import android.view.inputmethod.InputMethodManager; import android.widget.Button; +import android.widget.Checkable; import android.widget.EditText; import android.widget.FrameLayout; +import android.widget.HorizontalScrollView; import android.widget.ImageButton; import android.widget.ImageView; import android.widget.LinearLayout; @@ -63,6 +69,7 @@ import org.joinmastodon.android.api.requests.accounts.GetPreferences; import org.joinmastodon.android.api.requests.statuses.CreateStatus; import org.joinmastodon.android.api.requests.statuses.EditStatus; import org.joinmastodon.android.api.requests.statuses.GetAttachmentByID; +import org.joinmastodon.android.api.requests.statuses.UpdateAttachment; import org.joinmastodon.android.api.requests.statuses.UploadAttachment; import org.joinmastodon.android.api.session.AccountSession; import org.joinmastodon.android.api.session.AccountSessionManager; @@ -82,28 +89,30 @@ import org.joinmastodon.android.model.StatusPrivacy; import org.joinmastodon.android.ui.ComposeAutocompleteViewController; import org.joinmastodon.android.ui.CustomEmojiPopupKeyboard; import org.joinmastodon.android.ui.M3AlertDialogBuilder; +import org.joinmastodon.android.ui.OutlineProviders; import org.joinmastodon.android.ui.PopupKeyboard; +import org.joinmastodon.android.ui.drawables.EmptyDrawable; import org.joinmastodon.android.ui.drawables.SpoilerStripesDrawable; import org.joinmastodon.android.ui.text.ComposeAutocompleteSpan; import org.joinmastodon.android.ui.text.ComposeHashtagOrMentionSpan; import org.joinmastodon.android.ui.text.HtmlParser; +import org.joinmastodon.android.ui.text.LengthLimitHighlighter; import org.joinmastodon.android.ui.utils.SimpleTextWatcher; -import org.joinmastodon.android.utils.TransferSpeedTracker; import org.joinmastodon.android.ui.utils.UiUtils; +import org.joinmastodon.android.ui.views.CheckableLinearLayout; import org.joinmastodon.android.ui.views.ComposeEditText; -import org.joinmastodon.android.ui.views.ComposeMediaLayout; import org.joinmastodon.android.ui.views.ReorderableLinearLayout; import org.joinmastodon.android.ui.views.SizeListenerLinearLayout; +import org.joinmastodon.android.utils.TransferSpeedTracker; import org.parceler.Parcel; import org.parceler.Parcels; -import java.io.InterruptedIOException; -import java.net.SocketException; -import java.net.UnknownHostException; import java.time.temporal.ChronoUnit; import java.util.ArrayList; +import java.util.HashMap; import java.util.List; import java.util.Locale; +import java.util.Objects; import java.util.UUID; import java.util.regex.Matcher; import java.util.regex.Pattern; @@ -115,6 +124,7 @@ import me.grishka.appkit.api.ErrorResponse; import me.grishka.appkit.fragments.OnBackPressedListener; import me.grishka.appkit.imageloader.ViewImageLoader; import me.grishka.appkit.imageloader.requests.UrlImageLoaderRequest; +import me.grishka.appkit.utils.CubicBezierInterpolator; import me.grishka.appkit.utils.V; public class ComposeFragment extends MastodonToolbarFragment implements OnBackPressedListener, ComposeEditText.SelectionListener{ @@ -123,6 +133,15 @@ public class ComposeFragment extends MastodonToolbarFragment implements OnBackPr private static final int IMAGE_DESCRIPTION_RESULT=363; private static final int MAX_ATTACHMENTS=4; private static final String TAG="ComposeFragment"; + private static final int[] POLL_LENGTH_OPTIONS={ + 5*60, + 30*60, + 3600, + 6*3600, + 24*3600, + 3*24*3600, + 7*24*3600, + }; private static final Pattern MENTION_PATTERN=Pattern.compile("(^|[^\\/\\w])@(([a-z0-9_]+)@[a-z0-9\\.\\-]+[a-z0-9]+)", Pattern.CASE_INSENSITIVE); @@ -133,6 +152,7 @@ public class ComposeFragment extends MastodonToolbarFragment implements OnBackPr @SuppressLint("NewApi") // this class actually exists on 6.0 private final BreakIterator breakIterator=BreakIterator.getCharacterInstance(); + private LinearLayout mainLayout; private SizeListenerLinearLayout contentView; private TextView selfName, selfUsername; private ImageView selfAvatar; @@ -144,14 +164,22 @@ public class ComposeFragment extends MastodonToolbarFragment implements OnBackPr private String accountID; private int charCount, charLimit, trimmedCharCount; - private Button publishButton; - private ImageButton mediaBtn, pollBtn, emojiBtn, spoilerBtn, visibilityBtn; - private ComposeMediaLayout attachmentsView; + private ImageButton mediaBtn, pollBtn, emojiBtn, spoilerBtn; + private ReorderableLinearLayout attachmentsView; + private HorizontalScrollView attachmentsScroller; private TextView replyText; private ReorderableLinearLayout pollOptionsView; - private View pollWrap; + private ViewGroup pollWrap; private View addPollOptionBtn; - private TextView pollDurationView; + private Button visibilityBtn; + private LinearLayout bottomBar; + private ImageView deletePollOptionBtn; + private ViewGroup pollSettingsView; + private View pollPoof; + private View autocompleteDivider; + + private View pollDurationButton, pollStyleButton; + private TextView pollDurationValue, pollStyleValue; private ArrayList pollOptions=new ArrayList<>(); @@ -163,11 +191,11 @@ public class ComposeFragment extends MastodonToolbarFragment implements OnBackPr private String initialText; private String uuid; private int pollDuration=24*3600; - private String pollDurationStr; + private boolean pollIsMultipleChoice; private EditText spoilerEdit; + private View spoilerWrap; private boolean hasSpoiler; private ProgressBar sendProgress; - private ImageView sendError; private View sendingOverlay; private WindowManager wm; private StatusPrivacy statusVisibility=StatusPrivacy.PUBLIC; @@ -181,7 +209,18 @@ public class ComposeFragment extends MastodonToolbarFragment implements OnBackPr private boolean pollChanged; private boolean creatingView; private boolean ignoreSelectionChanges=false; - private Runnable updateUploadEtaRunnable; + private MenuItem publishButton; + private boolean wasDetached; + + private BackgroundColorSpan overLimitBG; + private ForegroundColorSpan overLimitFG; + + private int maxPollOptions=4; + private int maxPollOptionLength=50; + + public ComposeFragment(){ + super(R.layout.toolbar_fragment_with_progressbar); + } @Override public void onCreate(Bundle savedInstanceState){ @@ -212,6 +251,11 @@ public class ComposeFragment extends MastodonToolbarFragment implements OnBackPr else charLimit=500; + if(instance.configuration!=null && instance.configuration.polls!=null && instance.configuration.polls.maxOptions>0) + maxPollOptions=instance.configuration.polls.maxOptions; + if(instance.configuration!=null && instance.configuration.polls!=null && instance.configuration.polls.maxCharactersPerOption>0) + maxPollOptionLength=instance.configuration.polls.maxCharactersPerOption; + if (editingStatus == null) loadDefaultStatusVisibility(savedInstanceState); } @@ -222,17 +266,23 @@ public class ComposeFragment extends MastodonToolbarFragment implements OnBackPr if(att.isUploadingOrProcessing()) att.cancelUpload(); } - if(updateUploadEtaRunnable!=null){ - UiUtils.removeCallbacks(updateUploadEtaRunnable); - updateUploadEtaRunnable=null; - } } @Override public void onAttach(Activity activity){ super.onAttach(activity); setHasOptionsMenu(true); + setTitle(R.string.new_post); wm=activity.getSystemService(WindowManager.class); + + overLimitBG=new BackgroundColorSpan(UiUtils.getThemeColor(activity, R.attr.colorM3ErrorContainer)); + overLimitFG=new ForegroundColorSpan(UiUtils.getThemeColor(activity, R.attr.colorM3Error)); + } + + @Override + public void onDetach(){ + wasDetached=true; + super.onDetach(); } @Override @@ -242,6 +292,7 @@ public class ComposeFragment extends MastodonToolbarFragment implements OnBackPr emojiKeyboard.setListener(this::onCustomEmojiClick); View view=inflater.inflate(R.layout.fragment_compose, container, false); + mainLayout=view.findViewById(R.id.compose_main_ll); mainEditText=view.findViewById(R.id.toot_text); mainEditTextWrap=view.findViewById(R.id.toot_text_wrap); charCounter=view.findViewById(R.id.char_counter); @@ -261,6 +312,7 @@ public class ComposeFragment extends MastodonToolbarFragment implements OnBackPr }; selfAvatar.setOutlineProvider(roundCornersOutline); selfAvatar.setClipToOutline(true); + bottomBar=view.findViewById(R.id.bottom_bar); mediaBtn=view.findViewById(R.id.btn_media); pollBtn=view.findViewById(R.id.btn_poll); @@ -274,6 +326,9 @@ public class ComposeFragment extends MastodonToolbarFragment implements OnBackPr emojiBtn.setOnClickListener(v->emojiKeyboard.toggleKeyboardPopup(mainEditText)); spoilerBtn.setOnClickListener(v->toggleSpoiler()); visibilityBtn.setOnClickListener(this::onVisibilityClick); + Drawable arrow=getResources().getDrawable(R.drawable.ic_baseline_arrow_drop_down_18, getActivity().getTheme()).mutate(); + arrow.setTint(UiUtils.getThemeColor(getActivity(), R.attr.colorM3OnSurface)); + visibilityBtn.setCompoundDrawablesRelativeWithIntrinsicBounds(null, null, arrow, null); emojiKeyboard.setOnIconChangedListener(new PopupKeyboard.OnIconChangeListener(){ @Override public void onIconChanged(int icon){ @@ -286,20 +341,203 @@ public class ComposeFragment extends MastodonToolbarFragment implements OnBackPr emojiKeyboard.getView().setElevation(V.dp(2)); attachmentsView=view.findViewById(R.id.attachments); + attachmentsScroller=view.findViewById(R.id.attachments_scroller); pollOptionsView=view.findViewById(R.id.poll_options); pollWrap=view.findViewById(R.id.poll_wrap); addPollOptionBtn=view.findViewById(R.id.add_poll_option); + deletePollOptionBtn=view.findViewById(R.id.delete_poll_option); + pollSettingsView=view.findViewById(R.id.poll_settings); + pollPoof=view.findViewById(R.id.poll_poof); + attachmentsView.setDividerDrawable(new EmptyDrawable(V.dp(8), 0)); + attachmentsView.setDragListener(new ReorderableLinearLayout.OnDragListener(){ + private final HashMap currentAnimations=new HashMap<>(); + + @Override + public void onSwapItems(int oldIndex, int newIndex){ + attachments.add(newIndex, attachments.remove(oldIndex)); + } + + @Override + public void onDragStart(View view){ + if(currentAnimations.containsKey(view)) + currentAnimations.get(view).cancel(); + mainLayout.setClipChildren(false); + AnimatorSet set=new AnimatorSet(); + DraftMediaAttachment att=(DraftMediaAttachment) view.getTag(); + att.dragLayer.setVisibility(View.VISIBLE); + set.playTogether( + ObjectAnimator.ofFloat(view, View.TRANSLATION_Z, V.dp(3)), + ObjectAnimator.ofFloat(att.dragLayer, View.ALPHA, 0.16f) + ); + set.setDuration(150); + set.setInterpolator(CubicBezierInterpolator.DEFAULT); + set.addListener(new AnimatorListenerAdapter(){ + @Override + public void onAnimationEnd(Animator animation){ + currentAnimations.remove(view); + } + }); + currentAnimations.put(view, set); + set.start(); + } + + @Override + public void onDragEnd(View view){ + if(currentAnimations.containsKey(view)) + currentAnimations.get(view).cancel(); + AnimatorSet set=new AnimatorSet(); + DraftMediaAttachment att=(DraftMediaAttachment) view.getTag(); + set.playTogether( + ObjectAnimator.ofFloat(att.dragLayer, View.ALPHA, 0), + ObjectAnimator.ofFloat(view, View.TRANSLATION_Z, 0), + ObjectAnimator.ofFloat(view, View.TRANSLATION_X, 0), + ObjectAnimator.ofFloat(view, View.TRANSLATION_Y, 0) + ); + set.setDuration(200); + set.setInterpolator(CubicBezierInterpolator.DEFAULT); + set.addListener(new AnimatorListenerAdapter(){ + @Override + public void onAnimationEnd(Animator animation){ + if(currentAnimations.isEmpty()) + mainLayout.setClipChildren(true); + att.dragLayer.setVisibility(View.GONE); + currentAnimations.remove(view); + } + }); + currentAnimations.put(view, set); + set.start(); + } + }); + attachmentsView.setMoveInBothDimensions(true); addPollOptionBtn.setOnClickListener(v->{ createDraftPollOption().edit.requestFocus(); updatePollOptionHints(); }); - pollOptionsView.setDragListener(this::onSwapPollOptions); - pollDurationView=view.findViewById(R.id.poll_duration); - pollDurationView.setOnClickListener(v->showPollDurationMenu()); + pollOptionsView.setMoveInBothDimensions(true); + pollOptionsView.setDragListener(new ReorderableLinearLayout.OnDragListener(){ + private boolean isOverDelete; + private RectF rect1=new RectF(), rect2=new RectF(); + private Animator deletionStateAnimator; + + @Override + public void onSwapItems(int oldIndex, int newIndex){ + ComposeFragment.this.onSwapPollOptions(oldIndex, newIndex); + } + + @Override + public void onDragStart(View view){ + isOverDelete=false; + ReorderableLinearLayout.OnDragListener.super.onDragStart(view); + DraftPollOption dpo=(DraftPollOption) view.getTag(); + int color=UiUtils.getThemeColor(getActivity(), R.attr.colorM3OnSurface); + ObjectAnimator anim=ObjectAnimator.ofArgb(dpo.edit, "backgroundColor", color & 0xffffff, color & 0x29ffffff); + anim.setDuration(150); + anim.setInterpolator(CubicBezierInterpolator.DEFAULT); + anim.start(); + mainLayout.setClipChildren(false); + if(pollOptions.size()>2){ +// UiUtils.beginLayoutTransition(pollSettingsView); + deletePollOptionBtn.setVisibility(View.VISIBLE); + addPollOptionBtn.setVisibility(View.GONE); + } + } + + @Override + public void onDragEnd(View view){ + if(pollOptions.size()>2){ +// UiUtils.beginLayoutTransition(pollSettingsView); + deletePollOptionBtn.setVisibility(View.GONE); + addPollOptionBtn.setVisibility(View.VISIBLE); + } + + DraftPollOption dpo=(DraftPollOption) view.getTag(); + if(isOverDelete){ + pollPoof.setVisibility(View.VISIBLE); + AnimatorSet set=new AnimatorSet(); + set.playTogether( + ObjectAnimator.ofFloat(pollPoof, View.ALPHA, 0f, 0.7f, 1f, 0f), + ObjectAnimator.ofFloat(pollPoof, View.SCALE_X, 1f, 4f), + ObjectAnimator.ofFloat(pollPoof, View.SCALE_Y, 1f, 4f), + ObjectAnimator.ofFloat(pollPoof, View.ROTATION, 0f, 60f) + ); + set.setDuration(600); + set.setInterpolator(CubicBezierInterpolator.DEFAULT); + set.addListener(new AnimatorListenerAdapter(){ + @Override + public void onAnimationEnd(Animator animation){ + pollPoof.setVisibility(View.INVISIBLE); + } + }); + set.start(); + UiUtils.beginLayoutTransition(pollWrap); + pollOptions.remove(dpo); + pollOptionsView.removeView(view); + addPollOptionBtn.setEnabled(pollOptions.size()maxPollOptionLength ? R.drawable.bg_m3_outlined_text_field_error_nopad : R.drawable.bg_m3_outlined_text_field_nopad, getActivity().getTheme())); + int errorContainer=UiUtils.getThemeColor(getActivity(), R.attr.colorM3ErrorContainer); + int surface=UiUtils.getThemeColor(getActivity(), R.attr.colorM3Surface); + AnimatorSet set=new AnimatorSet(); + set.playTogether( + ObjectAnimator.ofFloat(view, View.ALPHA, newOverDelete ? .85f : 1), + ObjectAnimator.ofArgb(view, "backgroundColor", newOverDelete ? surface : errorContainer, newOverDelete ? errorContainer : surface) + ); + set.setDuration(150); + set.setInterpolator(CubicBezierInterpolator.DEFAULT); + deletionStateAnimator=set; + set.addListener(new AnimatorListenerAdapter(){ + @Override + public void onAnimationEnd(Animator animation){ + deletionStateAnimator=null; + } + }); + set.start(); + } + } + }); + pollOptionsView.setDividerDrawable(new EmptyDrawable(1, V.dp(8))); + pollDurationButton=view.findViewById(R.id.poll_duration); + pollDurationValue=view.findViewById(R.id.poll_duration_value); + pollDurationButton.setOnClickListener(v->showPollDurationAlert()); + pollStyleButton=view.findViewById(R.id.poll_style); + pollStyleValue=view.findViewById(R.id.poll_style_value); + pollStyleButton.setOnClickListener(v->showPollStyleAlert()); pollOptions.clear(); - if(savedInstanceState!=null && savedInstanceState.containsKey("pollOptions")){ + if(!wasDetached && savedInstanceState!=null && savedInstanceState.containsKey("pollOptions")){ pollBtn.setSelected(true); mediaBtn.setEnabled(false); pollWrap.setVisibility(View.VISIBLE); @@ -308,8 +546,9 @@ public class ComposeFragment extends MastodonToolbarFragment implements OnBackPr opt.edit.setText(oldText); } updatePollOptionHints(); - pollDurationView.setText(getString(R.string.compose_poll_duration, pollDurationStr)); - }else if(savedInstanceState==null && editingStatus!=null && editingStatus.poll!=null){ + pollDurationValue.setText(formatPollDuration(pollDuration)); + pollStyleValue.setText(pollIsMultipleChoice ? R.string.compose_poll_multiple_choice : R.string.compose_poll_single_choice); + }else if(wasDetached && savedInstanceState==null && editingStatus!=null && editingStatus.poll!=null){ pollBtn.setSelected(true); mediaBtn.setEnabled(false); pollWrap.setVisibility(View.VISIBLE); @@ -317,43 +556,50 @@ public class ComposeFragment extends MastodonToolbarFragment implements OnBackPr DraftPollOption opt=createDraftPollOption(); opt.edit.setText(eopt.title); } - pollDuration=(int)editingStatus.poll.expiresAt.minus(System.currentTimeMillis(), ChronoUnit.MILLIS).getEpochSecond(); - pollDurationStr=UiUtils.formatTimeLeft(getActivity(), editingStatus.poll.expiresAt); + pollDuration=(int)editingStatus.poll.expiresAt.minus(editingStatus.createdAt.toEpochMilli(), ChronoUnit.MILLIS).getEpochSecond(); updatePollOptionHints(); - pollDurationView.setText(getString(R.string.compose_poll_duration, pollDurationStr)); + pollDurationValue.setText(formatPollDuration(pollDuration)); + pollIsMultipleChoice=editingStatus.poll.multiple; + pollStyleValue.setText(pollIsMultipleChoice ? R.string.compose_poll_multiple_choice : R.string.compose_poll_single_choice); }else{ - pollDurationView.setText(getString(R.string.compose_poll_duration, pollDurationStr=getResources().getQuantityString(R.plurals.x_days, 1, 1))); + pollDurationValue.setText(formatPollDuration(24*3600)); + pollStyleValue.setText(R.string.compose_poll_single_choice); } spoilerEdit=view.findViewById(R.id.content_warning); - LayerDrawable spoilerBg=(LayerDrawable) spoilerEdit.getBackground().mutate(); + spoilerWrap=view.findViewById(R.id.content_warning_wrap); + LayerDrawable spoilerBg=(LayerDrawable) spoilerWrap.getBackground().mutate(); spoilerBg.setDrawableByLayerId(R.id.left_drawable, new SpoilerStripesDrawable(false)); spoilerBg.setDrawableByLayerId(R.id.right_drawable, new SpoilerStripesDrawable(false)); - spoilerEdit.setBackground(spoilerBg); + spoilerWrap.setBackground(spoilerBg); + spoilerWrap.setClipToOutline(true); + spoilerWrap.setOutlineProvider(OutlineProviders.roundedRect(8)); if((savedInstanceState!=null && savedInstanceState.getBoolean("hasSpoiler", false)) || hasSpoiler){ hasSpoiler=true; - spoilerEdit.setVisibility(View.VISIBLE); + spoilerWrap.setVisibility(View.VISIBLE); spoilerBtn.setSelected(true); }else if(editingStatus!=null && !TextUtils.isEmpty(editingStatus.spoilerText)){ hasSpoiler=true; - spoilerEdit.setVisibility(View.VISIBLE); + spoilerWrap.setVisibility(View.VISIBLE); spoilerEdit.setText(getArguments().getString("sourceSpoiler", editingStatus.spoilerText)); spoilerBtn.setSelected(true); } - if(savedInstanceState!=null && savedInstanceState.containsKey("attachments")){ + if(!wasDetached && savedInstanceState!=null && savedInstanceState.containsKey("attachments")){ ArrayList serializedAttachments=savedInstanceState.getParcelableArrayList("attachments"); for(Parcelable a:serializedAttachments){ DraftMediaAttachment att=Parcels.unwrap(a); attachmentsView.addView(createMediaAttachmentView(att)); attachments.add(att); } - attachmentsView.setVisibility(View.VISIBLE); + attachmentsScroller.setVisibility(View.VISIBLE); + updateMediaAttachmentsLayout(); }else if(!attachments.isEmpty()){ - attachmentsView.setVisibility(View.VISIBLE); + attachmentsScroller.setVisibility(View.VISIBLE); for(DraftMediaAttachment att:attachments){ attachmentsView.addView(createMediaAttachmentView(att)); } + updateMediaAttachmentsLayout(); } if(editingStatus!=null && editingStatus.visibility!=null) { @@ -364,8 +610,9 @@ public class ComposeFragment extends MastodonToolbarFragment implements OnBackPr autocompleteViewController=new ComposeAutocompleteViewController(getActivity(), accountID); autocompleteViewController.setCompletionSelectedListener(this::onAutocompleteOptionSelected); View autocompleteView=autocompleteViewController.getView(); - autocompleteView.setVisibility(View.GONE); - mainEditTextWrap.addView(autocompleteView, new FrameLayout.LayoutParams(ViewGroup.LayoutParams.MATCH_PARENT, V.dp(178), Gravity.TOP)); + autocompleteView.setVisibility(View.INVISIBLE); + bottomBar.addView(autocompleteView, 0, new LinearLayout.LayoutParams(ViewGroup.LayoutParams.MATCH_PARENT, V.dp(56))); + autocompleteDivider=view.findViewById(R.id.bottom_bar_autocomplete_divider); creatingView=false; @@ -382,7 +629,6 @@ public class ComposeFragment extends MastodonToolbarFragment implements OnBackPr } outState.putStringArrayList("pollOptions", opts); outState.putInt("pollDuration", pollDuration); - outState.putString("pollDurationStr", pollDurationStr); } outState.putBoolean("hasSpoiler", hasSpoiler); if(!attachments.isEmpty()){ @@ -409,6 +655,8 @@ public class ComposeFragment extends MastodonToolbarFragment implements OnBackPr view.postDelayed(()->{ imm.showSoftInput(mainEditText, 0); }, 100); + sendProgress=view.findViewById(R.id.progress); + sendProgress.setVisibility(View.GONE); mainEditText.setSelectionListener(this); mainEditText.addTextChangedListener(new TextWatcher(){ @@ -429,8 +677,10 @@ public class ComposeFragment extends MastodonToolbarFragment implements OnBackPr @Override public void afterTextChanged(Editable s){ - if(s.length()==0) + if(s.length()==0){ + updateCharCounter(); return; + } int start=lastChangeStart; int count=lastChangeCount; // offset one char back to catch an already typed '@' or '#' or ':' @@ -504,7 +754,7 @@ public class ComposeFragment extends MastodonToolbarFragment implements OnBackPr ignoreSelectionChanges=false; if(!TextUtils.isEmpty(replyTo.spoilerText) && AccountSessionManager.getInstance().isSelf(accountID, replyTo.account)){ hasSpoiler=true; - spoilerEdit.setVisibility(View.VISIBLE); + spoilerWrap.setVisibility(View.VISIBLE); spoilerEdit.setText(replyTo.spoilerText); spoilerBtn.setSelected(true); } @@ -520,7 +770,7 @@ public class ComposeFragment extends MastodonToolbarFragment implements OnBackPr mainEditText.setSelection(mainEditText.length()); ignoreSelectionChanges=false; if(!editingStatus.mediaAttachments.isEmpty()){ - attachmentsView.setVisibility(View.VISIBLE); + attachmentsScroller.setVisibility(View.VISIBLE); for(Attachment att:editingStatus.mediaAttachments){ DraftMediaAttachment da=new DraftMediaAttachment(); da.serverAttachment=att; @@ -530,6 +780,7 @@ public class ComposeFragment extends MastodonToolbarFragment implements OnBackPr attachmentsView.addView(createMediaAttachmentView(da)); attachments.add(da); } + updateMediaAttachmentsLayout(); pollBtn.setEnabled(false); } }else{ @@ -558,38 +809,16 @@ public class ComposeFragment extends MastodonToolbarFragment implements OnBackPr @Override public void onCreateOptionsMenu(Menu menu, MenuInflater inflater){ - publishButton=new Button(getActivity()); - publishButton.setText(editingStatus==null ? R.string.publish : R.string.save); - publishButton.setOnClickListener(this::onPublishClick); - LinearLayout wrap=new LinearLayout(getActivity()); - wrap.setOrientation(LinearLayout.HORIZONTAL); - - sendProgress=new ProgressBar(getActivity()); - LinearLayout.LayoutParams progressLP=new LinearLayout.LayoutParams(V.dp(24), V.dp(24)); - progressLP.setMarginEnd(V.dp(16)); - progressLP.gravity=Gravity.CENTER_VERTICAL; - wrap.addView(sendProgress, progressLP); - - sendError=new ImageView(getActivity()); - sendError.setImageResource(R.drawable.ic_fluent_error_circle_24_regular); - sendError.setImageTintList(getResources().getColorStateList(R.color.error_600)); - sendError.setScaleType(ImageView.ScaleType.CENTER); - wrap.addView(sendError, progressLP); - - sendError.setVisibility(View.GONE); - sendProgress.setVisibility(View.GONE); - - wrap.addView(publishButton, new LinearLayout.LayoutParams(ViewGroup.LayoutParams.WRAP_CONTENT, ViewGroup.LayoutParams.WRAP_CONTENT)); - wrap.setPadding(V.dp(16), V.dp(4), V.dp(16), V.dp(8)); - wrap.setClipToPadding(false); - MenuItem item=menu.add(editingStatus==null ? R.string.publish : R.string.save); - item.setActionView(wrap); - item.setShowAsAction(MenuItem.SHOW_AS_ACTION_ALWAYS); + inflater.inflate(R.menu.compose, menu); + publishButton=menu.findItem(R.id.publish); updatePublishButtonState(); } @Override public boolean onOptionsItemSelected(MenuItem item){ + if(item.getItemId()==R.id.publish){ + publish(); + } return true; } @@ -601,7 +830,7 @@ public class ComposeFragment extends MastodonToolbarFragment implements OnBackPr @SuppressLint("NewApi") private void updateCharCounter(){ - CharSequence text=mainEditText.getText(); + Editable text=mainEditText.getText(); String countableText=TwitterTextEmojiRegex.VALID_EMOJI_PATTERN.matcher( MENTION_PATTERN.matcher( @@ -618,6 +847,19 @@ public class ComposeFragment extends MastodonToolbarFragment implements OnBackPr charCount+=spoilerEdit.length(); } charCounter.setText(String.valueOf(charLimit-charCount)); + + text.removeSpan(overLimitBG); + text.removeSpan(overLimitFG); + if(charCount>charLimit){ + charCounter.setTextColor(UiUtils.getThemeColor(getActivity(), R.attr.colorM3Error)); + int start=text.length()-(charCount-charLimit); + int end=text.length(); + text.setSpan(overLimitFG, start, end, 0); + text.setSpan(overLimitBG, start, end, 0); + }else{ + charCounter.setTextColor(UiUtils.getThemeColor(getActivity(), R.attr.colorM3OnSurface)); + } + trimmedCharCount=text.toString().trim().length(); updatePublishButtonState(); } @@ -650,14 +892,64 @@ public class ComposeFragment extends MastodonToolbarFragment implements OnBackPr @Override protected void updateToolbar(){ super.updateToolbar(); - getToolbar().setNavigationIcon(R.drawable.ic_fluent_dismiss_24_regular); + int color=UiUtils.alphaBlendThemeColors(getActivity(), R.attr.colorM3Background, R.attr.colorM3Primary, 0.11f); + getToolbar().setBackgroundColor(color); + setStatusBarColor(color); + bottomBar.setBackgroundColor(color); } - private void onPublishClick(View v){ - publish(); + @Override + protected int getNavigationIconDrawableResource(){ + return R.drawable.ic_baseline_close_24; + } + + @Override + public boolean wantsCustomNavigationIcon(){ + return true; } private void publish(){ + sendingOverlay=new View(getActivity()); + WindowManager.LayoutParams overlayParams=new WindowManager.LayoutParams(); + overlayParams.type=WindowManager.LayoutParams.TYPE_APPLICATION_PANEL; + overlayParams.flags=WindowManager.LayoutParams.FLAG_LAYOUT_INSET_DECOR | WindowManager.LayoutParams.FLAG_DRAWS_SYSTEM_BAR_BACKGROUNDS | WindowManager.LayoutParams.FLAG_ALT_FOCUSABLE_IM; + overlayParams.width=overlayParams.height=WindowManager.LayoutParams.MATCH_PARENT; + overlayParams.format=PixelFormat.TRANSLUCENT; + overlayParams.softInputMode=WindowManager.LayoutParams.SOFT_INPUT_STATE_UNCHANGED; + overlayParams.token=mainEditText.getWindowToken(); + wm.addView(sendingOverlay, overlayParams); + + publishButton.setEnabled(false); + V.setVisibilityAnimated(sendProgress, View.VISIBLE); + + ArrayList updateAltTextRequests=new ArrayList<>(); + for(DraftMediaAttachment att:attachments){ + if(!att.descriptionSaved){ + UpdateAttachment req=new UpdateAttachment(att.serverAttachment.id, att.description); + req.setCallback(new Callback<>(){ + @Override + public void onSuccess(Attachment result){ + att.descriptionSaved=true; + att.serverAttachment=result; + updateAltTextRequests.remove(req); + if(updateAltTextRequests.isEmpty()) + actuallyPublish(); + } + + @Override + public void onError(ErrorResponse error){ + handlePublishError(error); + } + }) + .exec(accountID); + updateAltTextRequests.add(req); + } + } + if(updateAltTextRequests.isEmpty()) + actuallyPublish(); + } + + private void actuallyPublish(){ String text=mainEditText.getText().toString(); CreateStatus.Request req=new CreateStatus.Request(); req.status=text; @@ -671,6 +963,7 @@ public class ComposeFragment extends MastodonToolbarFragment implements OnBackPr if(!pollOptions.isEmpty()){ req.poll=new CreateStatus.Request.Poll(); req.poll.expiresIn=pollDuration; + req.poll.multiple=pollIsMultipleChoice; for(DraftPollOption opt:pollOptions) req.poll.options.add(opt.edit.getText().toString()); } @@ -680,20 +973,6 @@ public class ComposeFragment extends MastodonToolbarFragment implements OnBackPr if(uuid==null) uuid=UUID.randomUUID().toString(); - sendingOverlay=new View(getActivity()); - WindowManager.LayoutParams overlayParams=new WindowManager.LayoutParams(); - overlayParams.type=WindowManager.LayoutParams.TYPE_APPLICATION_PANEL; - overlayParams.flags=WindowManager.LayoutParams.FLAG_LAYOUT_INSET_DECOR | WindowManager.LayoutParams.FLAG_DRAWS_SYSTEM_BAR_BACKGROUNDS | WindowManager.LayoutParams.FLAG_ALT_FOCUSABLE_IM; - overlayParams.width=overlayParams.height=WindowManager.LayoutParams.MATCH_PARENT; - overlayParams.format=PixelFormat.TRANSLUCENT; - overlayParams.softInputMode=WindowManager.LayoutParams.SOFT_INPUT_STATE_UNCHANGED; - overlayParams.token=mainEditText.getWindowToken(); - wm.addView(sendingOverlay, overlayParams); - - publishButton.setEnabled(false); - sendProgress.setVisibility(View.VISIBLE); - sendError.setVisibility(View.GONE); - Callback resCallback=new Callback<>(){ @Override public void onSuccess(Status result){ @@ -713,12 +992,7 @@ public class ComposeFragment extends MastodonToolbarFragment implements OnBackPr @Override public void onError(ErrorResponse error){ - wm.removeView(sendingOverlay); - sendingOverlay=null; - sendProgress.setVisibility(View.GONE); - sendError.setVisibility(View.VISIBLE); - publishButton.setEnabled(true); - error.showToast(getActivity()); + handlePublishError(error); } }; @@ -733,6 +1007,23 @@ public class ComposeFragment extends MastodonToolbarFragment implements OnBackPr } } + private void handlePublishError(ErrorResponse error){ + wm.removeView(sendingOverlay); + sendingOverlay=null; + V.setVisibilityAnimated(sendProgress, View.GONE); + publishButton.setEnabled(true); + if(error instanceof MastodonErrorResponse me){ + new M3AlertDialogBuilder(getActivity()) + .setTitle(R.string.post_failed) + .setMessage(me.error) + .setPositiveButton(R.string.retry, (dlg, btn)->publish()) + .setNegativeButton(R.string.cancel, null) + .show(); + }else{ + error.showToast(getActivity()); + } + } + private boolean hasDraft(){ if(editingStatus!=null){ if(!mainEditText.getText().toString().equals(initialText)) @@ -775,12 +1066,13 @@ public class ComposeFragment extends MastodonToolbarFragment implements OnBackPr @Override public void onFragmentResult(int reqCode, boolean success, Bundle result){ if(reqCode==IMAGE_DESCRIPTION_RESULT && success){ - Attachment updated=Parcels.unwrap(result.getParcelable("attachment")); + String attID=result.getString("attachment"); + String text=result.getString("text"); for(DraftMediaAttachment att:attachments){ - if(att.serverAttachment.id.equals(updated.id)){ - att.serverAttachment=updated; - att.description=updated.description; - att.descriptionView.setText(att.description); + if(att.serverAttachment.id.equals(attID)){ + att.descriptionSaved=false; + att.description=text; + att.setDescriptionToTitle(); break; } } @@ -853,6 +1145,14 @@ public class ComposeFragment extends MastodonToolbarFragment implements OnBackPr return false; } String type=getActivity().getContentResolver().getType(uri); + int size; + try(Cursor cursor=MastodonApp.context.getContentResolver().query(uri, new String[]{OpenableColumns.SIZE}, null, null, null)){ + cursor.moveToFirst(); + size=cursor.getInt(0); + }catch(Exception x){ + Log.w("ComposeFragment", x); + return false; + } if(instance!=null && instance.configuration!=null && instance.configuration.mediaAttachments!=null){ if(instance.configuration.mediaAttachments.supportedMimeTypes!=null && !instance.configuration.mediaAttachments.supportedMimeTypes.contains(type)){ showMediaAttachmentError(getString(R.string.media_attachment_unsupported_type, UiUtils.getFileName(uri))); @@ -860,14 +1160,6 @@ public class ComposeFragment extends MastodonToolbarFragment implements OnBackPr } if(!type.startsWith("image/")){ int sizeLimit=instance.configuration.mediaAttachments.videoSizeLimit; - int size; - try(Cursor cursor=MastodonApp.context.getContentResolver().query(uri, new String[]{OpenableColumns.SIZE}, null, null, null)){ - cursor.moveToFirst(); - size=cursor.getInt(0); - }catch(Exception x){ - Log.w("ComposeFragment", x); - return false; - } if(size>sizeLimit){ float mb=sizeLimit/(float) (1024*1024); String sMb=String.format(Locale.getDefault(), mb%1f==0f ? "%.0f" : "%.2f", mb); @@ -881,11 +1173,14 @@ public class ComposeFragment extends MastodonToolbarFragment implements OnBackPr draft.uri=uri; draft.mimeType=type; draft.description=description; + draft.fileSize=size; - attachmentsView.addView(createMediaAttachmentView(draft)); + UiUtils.beginLayoutTransition(attachmentsScroller); + attachmentsView.addView(createMediaAttachmentView(draft), new LinearLayout.LayoutParams(0, ViewGroup.LayoutParams.MATCH_PARENT)); attachments.add(draft); - attachmentsView.setVisibility(View.VISIBLE); - draft.setOverlayVisible(true, false); + attachmentsScroller.setVisibility(View.VISIBLE); + updateMediaAttachmentsLayout(); +// draft.setOverlayVisible(true, false); if(!areThereAnyUploadingAttachments()){ uploadNextQueuedAttachment(); @@ -896,6 +1191,24 @@ public class ComposeFragment extends MastodonToolbarFragment implements OnBackPr return true; } + private void updateMediaAttachmentsLayout(){ + int newWidth=attachments.size()>2 ? ViewGroup.LayoutParams.WRAP_CONTENT : ViewGroup.LayoutParams.MATCH_PARENT; + if(newWidth!=attachmentsView.getLayoutParams().width){ + attachmentsView.getLayoutParams().width=newWidth; + attachmentsScroller.requestLayout(); + } + for(DraftMediaAttachment att:attachments){ + LinearLayout.LayoutParams lp=(LinearLayout.LayoutParams) att.view.getLayoutParams(); + if(attachments.size()<3){ + lp.width=0; + lp.weight=1f; + }else{ + lp.width=V.dp(200); + lp.weight=0f; + } + } + } + private void showMediaAttachmentError(String text){ if(!attachmentsErrorShowing){ Toast.makeText(getActivity(), text, Toast.LENGTH_SHORT).show(); @@ -917,41 +1230,64 @@ public class ComposeFragment extends MastodonToolbarFragment implements OnBackPr loadVideoThumbIntoView(img, draft.uri); } } - TextView fileName=thumb.findViewById(R.id.file_name); - fileName.setText(UiUtils.getFileName(draft.serverAttachment!=null ? Uri.parse(draft.serverAttachment.url) : draft.uri)); draft.view=thumb; draft.imageView=img; draft.progressBar=thumb.findViewById(R.id.progress); - draft.infoBar=thumb.findViewById(R.id.info_bar); - draft.overlay=thumb.findViewById(R.id.overlay); - draft.descriptionView=thumb.findViewById(R.id.description); - draft.uploadStateTitle=thumb.findViewById(R.id.state_title); - draft.uploadStateText=thumb.findViewById(R.id.state_text); - ImageButton btn=thumb.findViewById(R.id.remove_btn); - btn.setTag(draft); - btn.setOnClickListener(this::onRemoveMediaAttachmentClick); - btn=thumb.findViewById(R.id.remove_btn2); - btn.setTag(draft); - btn.setOnClickListener(this::onRemoveMediaAttachmentClick); - ImageButton retry=thumb.findViewById(R.id.retry_or_cancel_upload); - retry.setTag(draft); - retry.setOnClickListener(this::onRetryOrCancelMediaUploadClick); - draft.retryButton=retry; - draft.infoBar.setTag(draft); - draft.infoBar.setOnClickListener(this::onEditMediaDescriptionClick); + draft.titleView=thumb.findViewById(R.id.title); + draft.subtitleView=thumb.findViewById(R.id.subtitle); + draft.removeButton=thumb.findViewById(R.id.delete); + draft.editButton=thumb.findViewById(R.id.edit); + draft.dragLayer=thumb.findViewById(R.id.drag_layer); - if(!TextUtils.isEmpty(draft.description)) - draft.descriptionView.setText(draft.description); + draft.removeButton.setTag(draft); + draft.removeButton.setOnClickListener(this::onRemoveMediaAttachmentClick); + draft.editButton.setTag(draft); - if(Build.VERSION.SDK_INT>=Build.VERSION_CODES.S){ - draft.overlay.setBackgroundColor(0xA6000000); - } + thumb.setOutlineProvider(OutlineProviders.roundedRect(12)); + thumb.setClipToOutline(true); + img.setOutlineProvider(OutlineProviders.roundedRect(12)); + img.setClipToOutline(true); - if(draft.state==AttachmentUploadState.UPLOADING || draft.state==AttachmentUploadState.PROCESSING || draft.state==AttachmentUploadState.QUEUED){ + thumb.setBackgroundColor(UiUtils.getThemeColor(getActivity(), R.attr.colorM3Surface)); + thumb.setOnLongClickListener(v->{ + if(!v.hasTransientState() && attachments.size()>1){ + attachmentsView.startDragging(v); + return true; + } + return false; + }); + thumb.setTag(draft); + + + int subtitleRes=switch(Objects.requireNonNullElse(draft.mimeType, "").split("/")[0]){ + case "image" -> R.string.attachment_description_image; + case "video" -> R.string.attachment_description_video; + case "audio" -> R.string.attachment_description_audio; + default -> R.string.attachment_description_unknown; + }; + draft.titleView.setText(getString(R.string.attachment_x_percent_uploaded, 0)); + draft.subtitleView.setText(getString(subtitleRes, UiUtils.formatFileSize(getActivity(), draft.fileSize, true))); + draft.removeButton.setImageResource(R.drawable.ic_baseline_close_24); + + if(draft.state==AttachmentUploadState.ERROR){ + draft.titleView.setText(R.string.upload_failed); + draft.editButton.setImageResource(R.drawable.ic_restart_alt_24px); + draft.editButton.setOnClickListener(ComposeFragment.this::onRetryOrCancelMediaUploadClick); draft.progressBar.setVisibility(View.GONE); - }else if(draft.state==AttachmentUploadState.ERROR){ - draft.setOverlayVisible(true, false); + draft.setUseErrorColors(true); + }else if(draft.state==AttachmentUploadState.DONE){ + draft.setDescriptionToTitle(); + draft.progressBar.setVisibility(View.GONE); + draft.editButton.setOnClickListener(this::onEditMediaDescriptionClick); + }else{ + draft.editButton.setVisibility(View.GONE); + draft.removeButton.setImageResource(R.drawable.ic_baseline_close_24); + if(draft.state==AttachmentUploadState.PROCESSING){ + draft.titleView.setText(R.string.upload_processing); + }else{ + draft.titleView.setText(getString(R.string.attachment_x_percent_uploaded, 0)); + } } return thumb; @@ -964,7 +1300,8 @@ public class ComposeFragment extends MastodonToolbarFragment implements OnBackPr draft.description=description; attachmentsView.addView(createMediaAttachmentView(draft)); attachments.add(draft); - attachmentsView.setVisibility(View.VISIBLE); + attachmentsScroller.setVisibility(View.VISIBLE); + updateMediaAttachmentsLayout(); } private void uploadMediaAttachment(DraftMediaAttachment attachment){ @@ -973,19 +1310,11 @@ public class ComposeFragment extends MastodonToolbarFragment implements OnBackPr } attachment.state=AttachmentUploadState.UPLOADING; attachment.progressBar.setVisibility(View.VISIBLE); - ObjectAnimator rotationAnimator=ObjectAnimator.ofFloat(attachment.progressBar, View.ROTATION, 0f, 360f); - rotationAnimator.setInterpolator(new LinearInterpolator()); - rotationAnimator.setDuration(1500); - rotationAnimator.setRepeatCount(ObjectAnimator.INFINITE); - rotationAnimator.start(); - attachment.progressBarAnimator=rotationAnimator; int maxSize=0; String contentType=getActivity().getContentResolver().getType(attachment.uri); if(contentType!=null && contentType.startsWith("image/")){ maxSize=2_073_600; // TODO get this from instance configuration when it gets added there } - attachment.uploadStateTitle.setText(""); - attachment.uploadStateText.setText(""); attachment.progressBar.setProgress(0); attachment.speedTracker.reset(); attachment.speedTracker.addSample(0); @@ -993,17 +1322,17 @@ public class ComposeFragment extends MastodonToolbarFragment implements OnBackPr .setProgressListener(new ProgressListener(){ @Override public void onProgress(long transferred, long total){ - if(updateUploadEtaRunnable==null){ - UiUtils.runOnUiThread(updateUploadEtaRunnable=ComposeFragment.this::updateUploadETAs, 100); - } - int progress=Math.round(transferred/(float)total*attachment.progressBar.getMax()); + float progressFraction=transferred/(float)total; + int progress=Math.round(progressFraction*attachment.progressBar.getMax()); if(Build.VERSION.SDK_INT>=24) attachment.progressBar.setProgress(progress, true); else attachment.progressBar.setProgress(progress); + attachment.titleView.setText(getString(R.string.attachment_x_percent_uploaded, Math.round(progressFraction*100f))); + attachment.speedTracker.setTotalBytes(total); - attachment.uploadStateTitle.setText(getString(R.string.file_upload_progress, UiUtils.formatFileSize(getActivity(), transferred, true), UiUtils.formatFileSize(getActivity(), total, true))); +// attachment.uploadStateTitle.setText(getString(R.string.file_upload_progress, UiUtils.formatFileSize(getActivity(), transferred, true), UiUtils.formatFileSize(getActivity(), total, true))); attachment.speedTracker.addSample(transferred); } }) @@ -1016,8 +1345,7 @@ public class ComposeFragment extends MastodonToolbarFragment implements OnBackPr attachment.processingPollingRunnable=()->pollForMediaAttachmentProcessing(attachment); if(getActivity()==null) return; - attachment.uploadStateTitle.setText(R.string.upload_processing); - attachment.uploadStateText.setText(""); + attachment.titleView.setText(R.string.upload_processing); UiUtils.runOnUiThread(attachment.processingPollingRunnable, 1000); if(!areThereAnyUploadingAttachments()) uploadNextQueuedAttachment(); @@ -1029,22 +1357,23 @@ public class ComposeFragment extends MastodonToolbarFragment implements OnBackPr @Override public void onError(ErrorResponse error){ attachment.uploadRequest=null; - attachment.progressBarAnimator=null; attachment.state=AttachmentUploadState.ERROR; - attachment.uploadStateTitle.setText(R.string.upload_failed); - if(error instanceof MastodonErrorResponse er){ - if(er.underlyingException instanceof SocketException || er.underlyingException instanceof UnknownHostException || er.underlyingException instanceof InterruptedIOException) - attachment.uploadStateText.setText(R.string.upload_error_connection_lost); - else - attachment.uploadStateText.setText(er.error); - }else{ - attachment.uploadStateText.setText(""); - } - attachment.retryButton.setImageResource(R.drawable.ic_fluent_arrow_clockwise_24_filled); - attachment.retryButton.setContentDescription(getString(R.string.retry_upload)); + attachment.titleView.setText(R.string.upload_failed); +// if(error instanceof MastodonErrorResponse er){ +// if(er.underlyingException instanceof SocketException || er.underlyingException instanceof UnknownHostException || er.underlyingException instanceof InterruptedIOException) +// attachment.uploadStateText.setText(R.string.upload_error_connection_lost); +// else +// attachment.uploadStateText.setText(er.error); +// }else{ +// attachment.uploadStateText.setText(""); +// } +// attachment.retryButton.setImageResource(R.drawable.ic_fluent_arrow_clockwise_24_filled); +// attachment.retryButton.setContentDescription(getString(R.string.retry_upload)); - rotationAnimator.cancel(); - V.setVisibilityAnimated(attachment.retryButton, View.VISIBLE); + V.setVisibilityAnimated(attachment.editButton, View.VISIBLE); + attachment.editButton.setImageResource(R.drawable.ic_restart_alt_24px); + attachment.editButton.setOnClickListener(ComposeFragment.this::onRetryOrCancelMediaUploadClick); + attachment.setUseErrorColors(true); V.setVisibilityAnimated(attachment.progressBar, View.GONE); if(!areThereAnyUploadingAttachments()) @@ -1061,9 +1390,14 @@ public class ComposeFragment extends MastodonToolbarFragment implements OnBackPr attachments.remove(att); if(!areThereAnyUploadingAttachments()) uploadNextQueuedAttachment(); + if(!attachments.isEmpty()) + UiUtils.beginLayoutTransition(attachmentsScroller); attachmentsView.removeView(att.view); - if(getMediaAttachmentsCount()==0) - attachmentsView.setVisibility(View.GONE); + if(getMediaAttachmentsCount()==0){ + attachmentsScroller.setVisibility(View.GONE); + }else{ + updateMediaAttachmentsLayout(); + } updatePublishButtonState(); pollBtn.setEnabled(attachments.isEmpty()); mediaBtn.setEnabled(true); @@ -1072,10 +1406,13 @@ public class ComposeFragment extends MastodonToolbarFragment implements OnBackPr private void onRetryOrCancelMediaUploadClick(View v){ DraftMediaAttachment att=(DraftMediaAttachment) v.getTag(); if(att.state==AttachmentUploadState.ERROR){ - att.retryButton.setImageResource(R.drawable.ic_fluent_dismiss_24_filled); - att.retryButton.setContentDescription(getString(R.string.cancel)); +// att.retryButton.setImageResource(R.drawable.ic_fluent_dismiss_24_filled); +// att.retryButton.setContentDescription(getString(R.string.cancel)); V.setVisibilityAnimated(att.progressBar, View.VISIBLE); + V.setVisibilityAnimated(att.editButton, View.GONE); + att.titleView.setText(getString(R.string.attachment_x_percent_uploaded, 0)); att.state=AttachmentUploadState.QUEUED; + att.setUseErrorColors(false); if(!areThereAnyUploadingAttachments()){ uploadNextQueuedAttachment(); } @@ -1114,16 +1451,15 @@ public class ComposeFragment extends MastodonToolbarFragment implements OnBackPr throw new IllegalStateException("Unexpected state "+attachment.state); attachment.uploadRequest=null; attachment.state=AttachmentUploadState.DONE; - attachment.progressBar.setVisibility(View.GONE); + attachment.editButton.setImageResource(R.drawable.ic_edit_24px); + attachment.removeButton.setImageResource(R.drawable.ic_delete_24px); + attachment.editButton.setOnClickListener(this::onEditMediaDescriptionClick); + V.setVisibilityAnimated(attachment.progressBar, View.GONE); + V.setVisibilityAnimated(attachment.editButton, View.VISIBLE); + attachment.setDescriptionToTitle(); if(!areThereAnyUploadingAttachments()) uploadNextQueuedAttachment(); updatePublishButtonState(); - - if(attachment.progressBarAnimator!=null){ - attachment.progressBarAnimator.cancel(); - attachment.progressBarAnimator=null; - } - attachment.setOverlayVisible(false, true); } private void uploadNextQueuedAttachment(){ @@ -1143,23 +1479,6 @@ public class ComposeFragment extends MastodonToolbarFragment implements OnBackPr return false; } - private void updateUploadETAs(){ - if(!areThereAnyUploadingAttachments()){ - UiUtils.removeCallbacks(updateUploadEtaRunnable); - updateUploadEtaRunnable=null; - return; - } - for(DraftMediaAttachment att:attachments){ - if(att.state==AttachmentUploadState.UPLOADING){ - long eta=att.speedTracker.updateAndGetETA(); -// Log.i(TAG, "onProgress: transfer speed "+UiUtils.formatFileSize(getActivity(), Math.round(att.speedTracker.getLastSpeed()), false)+" average "+UiUtils.formatFileSize(getActivity(), Math.round(att.speedTracker.getAverageSpeed()), false)+" eta "+eta); - String time=String.format("%d:%02d", eta/60, eta%60); - att.uploadStateText.setText(getString(R.string.file_upload_time_remaining, time)); - } - } - UiUtils.runOnUiThread(updateUploadEtaRunnable, 100); - } - private void onEditMediaDescriptionClick(View v){ DraftMediaAttachment att=(DraftMediaAttachment) v.getTag(); if(att.serverAttachment==null) @@ -1169,6 +1488,12 @@ public class ComposeFragment extends MastodonToolbarFragment implements OnBackPr args.putString("attachment", att.serverAttachment.id); args.putParcelable("uri", att.uri); args.putString("existingDescription", att.description); + args.putString("attachmentType", att.serverAttachment.type.toString()); + Drawable img=att.imageView.getDrawable(); + if(img!=null){ + args.putInt("width", img.getIntrinsicWidth()); + args.putInt("height", img.getIntrinsicHeight()); + } Nav.goForResult(getActivity(), ComposeImageDescriptionFragment.class, args, IMAGE_DESCRIPTION_RESULT, this); } @@ -1207,12 +1532,17 @@ public class ComposeFragment extends MastodonToolbarFragment implements OnBackPr pollChanged=true; updatePublishButtonState(); })); - option.edit.setFilters(new InputFilter[]{new InputFilter.LengthFilter(instance.configuration!=null && instance.configuration.polls!=null && instance.configuration.polls.maxCharactersPerOption>0 ? instance.configuration.polls.maxCharactersPerOption : 50)}); + option.view.setOutlineProvider(OutlineProviders.roundedRect(4)); + option.view.setClipToOutline(true); + option.view.setTag(option); + UiUtils.beginLayoutTransition(pollWrap); pollOptionsView.addView(option.view); pollOptions.add(option); - if(pollOptions.size()==(instance.configuration!=null && instance.configuration.polls!=null && instance.configuration.polls.maxOptions>0 ? instance.configuration.polls.maxOptions : 4)) - addPollOptionBtn.setVisibility(View.GONE); + addPollOptionBtn.setEnabled(pollOptions.size(){ + option.view.setForeground(getResources().getDrawable(isOverLimit ? R.drawable.bg_m3_outlined_text_field_error_nopad : R.drawable.bg_m3_outlined_text_field_nopad, getActivity().getTheme())); + })); return option; } @@ -1229,41 +1559,75 @@ public class ComposeFragment extends MastodonToolbarFragment implements OnBackPr pollChanged=true; } - private void showPollDurationMenu(){ - PopupMenu menu=new PopupMenu(getActivity(), pollDurationView); - menu.getMenu().add(0, 1, 0, getResources().getQuantityString(R.plurals.x_minutes, 5, 5)); - menu.getMenu().add(0, 2, 0, getResources().getQuantityString(R.plurals.x_minutes, 30, 30)); - menu.getMenu().add(0, 3, 0, getResources().getQuantityString(R.plurals.x_hours, 1, 1)); - menu.getMenu().add(0, 4, 0, getResources().getQuantityString(R.plurals.x_hours, 6, 6)); - menu.getMenu().add(0, 5, 0, getResources().getQuantityString(R.plurals.x_days, 1, 1)); - menu.getMenu().add(0, 6, 0, getResources().getQuantityString(R.plurals.x_days, 3, 3)); - menu.getMenu().add(0, 7, 0, getResources().getQuantityString(R.plurals.x_days, 7, 7)); - menu.setOnMenuItemClickListener(item->{ - pollDuration=switch(item.getItemId()){ - case 1 -> 5*60; - case 2 -> 30*60; - case 3 -> 3600; - case 4 -> 6*3600; - case 5 -> 24*3600; - case 6 -> 3*24*3600; - case 7 -> 7*24*3600; - default -> throw new IllegalStateException("Unexpected value: "+item.getItemId()); - }; - pollDurationView.setText(getString(R.string.compose_poll_duration, pollDurationStr=item.getTitle().toString())); - pollChanged=true; - return true; - }); - menu.show(); + private void showPollDurationAlert(){ + String[] options=new String[POLL_LENGTH_OPTIONS.length]; + int selectedOption=-1; + for(int i=0;ichosenOption[0]=which) + .setTitle(R.string.poll_length) + .setPositiveButton(R.string.ok, (dialog, which)->{ + pollDuration=POLL_LENGTH_OPTIONS[chosenOption[0]]; + pollDurationValue.setText(formatPollDuration(pollDuration)); + }) + .setNegativeButton(R.string.cancel, null) + .show(); + } + + private String formatPollDuration(int seconds){ + if(seconds<3600){ + int minutes=seconds/60; + return getResources().getQuantityString(R.plurals.x_minutes, minutes, minutes); + }else if(seconds<24*3600){ + int hours=seconds/3600; + return getResources().getQuantityString(R.plurals.x_hours, hours, hours); + }else{ + int days=seconds/(24*3600); + return getResources().getQuantityString(R.plurals.x_days, days, days); + } + } + + private void showPollStyleAlert(){ + final int[] option={pollIsMultipleChoice ? R.id.multiple_choice : R.id.single_choice}; + AlertDialog alert=new M3AlertDialogBuilder(getActivity()) + .setView(R.layout.poll_style) + .setTitle(R.string.poll_style_title) + .setPositiveButton(R.string.ok, (dlg, which)->{ + pollIsMultipleChoice=option[0]==R.id.multiple_choice; + pollStyleValue.setText(pollIsMultipleChoice ? R.string.compose_poll_multiple_choice : R.string.compose_poll_single_choice); + }) + .setNegativeButton(R.string.cancel, null) + .show(); + CheckableLinearLayout multiple=alert.findViewById(R.id.multiple_choice); + CheckableLinearLayout single=alert.findViewById(R.id.single_choice); + single.setChecked(!pollIsMultipleChoice); + multiple.setChecked(pollIsMultipleChoice); + View.OnClickListener listener=v->{ + int id=v.getId(); + if(id==option[0]) + return; + ((Checkable) alert.findViewById(option[0])).setChecked(false); + ((Checkable) v).setChecked(true); + option[0]=id; + }; + single.setOnClickListener(listener); + multiple.setOnClickListener(listener); } private void toggleSpoiler(){ hasSpoiler=!hasSpoiler; if(hasSpoiler){ - spoilerEdit.setVisibility(View.VISIBLE); + spoilerWrap.setVisibility(View.VISIBLE); spoilerBtn.setSelected(true); spoilerEdit.requestFocus(); }else{ - spoilerEdit.setVisibility(View.GONE); + spoilerWrap.setVisibility(View.GONE); spoilerEdit.setText(""); spoilerBtn.setSelected(false); mainEditText.requestFocus(); @@ -1278,29 +1642,18 @@ public class ComposeFragment extends MastodonToolbarFragment implements OnBackPr private void onVisibilityClick(View v){ PopupMenu menu=new PopupMenu(getActivity(), v); menu.inflate(R.menu.compose_visibility); - Menu m=menu.getMenu(); - UiUtils.enablePopupMenuIcons(getActivity(), menu); - m.setGroupCheckable(0, true, true); - m.findItem(switch(statusVisibility){ - case PUBLIC, UNLISTED -> R.id.vis_public; - case PRIVATE -> R.id.vis_followers; - case DIRECT -> R.id.vis_private; - }).setChecked(true); - menu.setOnMenuItemClickListener(new PopupMenu.OnMenuItemClickListener(){ - @Override - public boolean onMenuItemClick(MenuItem item){ - int id=item.getItemId(); - if(id==R.id.vis_public){ - statusVisibility=StatusPrivacy.PUBLIC; - }else if(id==R.id.vis_followers){ - statusVisibility=StatusPrivacy.PRIVATE; - }else if(id==R.id.vis_private){ - statusVisibility=StatusPrivacy.DIRECT; - } - item.setChecked(true); - updateVisibilityIcon(); - return true; + menu.setOnMenuItemClickListener(item->{ + int id=item.getItemId(); + if(id==R.id.vis_public){ + statusVisibility=StatusPrivacy.PUBLIC; + }else if(id==R.id.vis_followers){ + statusVisibility=StatusPrivacy.PRIVATE; + }else if(id==R.id.vis_private){ + statusVisibility=StatusPrivacy.DIRECT; } + item.setChecked(true); + updateVisibilityIcon(); + return true; }); menu.show(); } @@ -1350,12 +1703,19 @@ public class ComposeFragment extends MastodonToolbarFragment implements OnBackPr if(statusVisibility==null){ // TODO find out why this happens statusVisibility=StatusPrivacy.PUBLIC; } - visibilityBtn.setImageResource(switch(statusVisibility){ - case PUBLIC -> R.drawable.ic_fluent_earth_24_regular; - case UNLISTED -> R.drawable.ic_fluent_people_community_24_regular; - case PRIVATE -> R.drawable.ic_fluent_people_checkmark_24_regular; - case DIRECT -> R.drawable.ic_at_symbol; + visibilityBtn.setText(switch(statusVisibility){ + case PUBLIC, UNLISTED -> R.string.visibility_public; + case PRIVATE -> R.string.visibility_followers_only; + case DIRECT -> R.string.visibility_private; }); + Drawable icon=getResources().getDrawable(switch(statusVisibility){ + case PUBLIC, UNLISTED -> R.drawable.ic_public_20px; + case PRIVATE -> R.drawable.ic_group_20px; + case DIRECT -> R.drawable.ic_alternate_email_20px; + }, getActivity().getTheme()).mutate(); + icon.setBounds(0, 0, V.dp(18), V.dp(18)); + icon.setTint(UiUtils.getThemeColor(getActivity(), R.attr.colorM3Primary)); + visibilityBtn.setCompoundDrawablesRelative(icon, null, visibilityBtn.getCompoundDrawablesRelative()[2], null); } @Override @@ -1374,18 +1734,6 @@ public class ComposeFragment extends MastodonToolbarFragment implements OnBackPr String spanText=e.toString().substring(e.getSpanStart(span), e.getSpanEnd(span)); autocompleteViewController.setText(spanText); } - - View autocompleteView=autocompleteViewController.getView(); - Layout layout=mainEditText.getLayout(); - int line=layout.getLineForOffset(start); - int offsetY=layout.getLineBottom(line); - FrameLayout.LayoutParams lp=(FrameLayout.LayoutParams) autocompleteView.getLayoutParams(); - if(lp.topMargin!=offsetY){ - lp.topMargin=offsetY; - mainEditTextWrap.requestLayout(); - } - int offsetX=Math.round(layout.getPrimaryHorizontal(start))+mainEditText.getPaddingLeft(); - autocompleteViewController.setArrowOffset(offsetX); }else if(currentAutocompleteSpan!=null){ finishAutocomplete(); } @@ -1411,8 +1759,12 @@ public class ComposeFragment extends MastodonToolbarFragment implements OnBackPr Editable e=mainEditText.getText(); String spanText=e.toString().substring(e.getSpanStart(span), e.getSpanEnd(span)); autocompleteViewController.setText(spanText); + UiUtils.beginLayoutTransition(bottomBar); View autocompleteView=autocompleteViewController.getView(); + bottomBar.getLayoutParams().height=ViewGroup.LayoutParams.WRAP_CONTENT; + bottomBar.requestLayout(); autocompleteView.setVisibility(View.VISIBLE); + autocompleteDivider.setVisibility(View.VISIBLE); } private void finishAutocomplete(){ @@ -1420,7 +1772,11 @@ public class ComposeFragment extends MastodonToolbarFragment implements OnBackPr return; autocompleteViewController.setText(null); currentAutocompleteSpan=null; - autocompleteViewController.getView().setVisibility(View.GONE); + UiUtils.beginLayoutTransition(bottomBar); + bottomBar.getLayoutParams().height=V.dp(48); + bottomBar.requestLayout(); + autocompleteViewController.getView().setVisibility(View.INVISIBLE); + autocompleteDivider.setVisibility(View.INVISIBLE); } private void onAutocompleteOptionSelected(String text){ @@ -1480,18 +1836,19 @@ public class ComposeFragment extends MastodonToolbarFragment implements OnBackPr public String description; public String mimeType; public AttachmentUploadState state=AttachmentUploadState.QUEUED; + public int fileSize; + public boolean descriptionSaved=true; public transient View view; public transient ProgressBar progressBar; - public transient TextView descriptionView; - public transient View overlay; - public transient View infoBar; - public transient ImageButton retryButton; - public transient ObjectAnimator progressBarAnimator; + public transient ImageButton removeButton, editButton; public transient Runnable processingPollingRunnable; public transient ImageView imageView; - public transient TextView uploadStateTitle, uploadStateText; + public transient TextView titleView, subtitleView; public transient TransferSpeedTracker speedTracker=new TransferSpeedTracker(); + private transient boolean errorColors; + private transient Animator errorTransitionAnimator; + public transient View dragLayer; public void cancelUpload(){ switch(state){ @@ -1519,24 +1876,51 @@ public class ComposeFragment extends MastodonToolbarFragment implements OnBackPr return state==AttachmentUploadState.UPLOADING || state==AttachmentUploadState.PROCESSING; } - public void setOverlayVisible(boolean visible, boolean animated){ - if(Build.VERSION.SDK_INT>=Build.VERSION_CODES.S){ - if(visible){ - imageView.setRenderEffect(RenderEffect.createBlurEffect(V.dp(16), V.dp(16), Shader.TileMode.REPEAT)); - }else{ - imageView.setRenderEffect(null); - } - } - int infoBarVis=visible ? View.GONE : View.VISIBLE; - int overlayVis=visible ? View.VISIBLE : View.GONE; - if(animated){ - V.setVisibilityAnimated(infoBar, infoBarVis); - V.setVisibilityAnimated(overlay, overlayVis); + public void setDescriptionToTitle(){ + if(TextUtils.isEmpty(description)){ + titleView.setText(R.string.add_alt_text); + titleView.setTextColor(UiUtils.getThemeColor(titleView.getContext(), R.attr.colorM3OnSurfaceVariant)); }else{ - infoBar.setVisibility(infoBarVis); - overlay.setVisibility(overlayVis); + titleView.setText(description); + titleView.setTextColor(UiUtils.getThemeColor(titleView.getContext(), R.attr.colorM3OnSurface)); } } + + public void setUseErrorColors(boolean use){ + if(errorColors==use) + return; + errorColors=use; + if(errorTransitionAnimator!=null) + errorTransitionAnimator.cancel(); + AnimatorSet set=new AnimatorSet(); + int color1, color2, color3; + if(use){ + color1=UiUtils.getThemeColor(view.getContext(), R.attr.colorM3ErrorContainer); + color2=UiUtils.getThemeColor(view.getContext(), R.attr.colorM3Error); + color3=UiUtils.getThemeColor(view.getContext(), R.attr.colorM3OnErrorContainer); + }else{ + color1=UiUtils.getThemeColor(view.getContext(), R.attr.colorM3Surface); + color2=UiUtils.getThemeColor(view.getContext(), R.attr.colorM3OnSurface); + color3=UiUtils.getThemeColor(view.getContext(), R.attr.colorM3OnSurfaceVariant); + } + set.playTogether( + ObjectAnimator.ofArgb(view, "backgroundColor", ((ColorDrawable)view.getBackground()).getColor(), color1), + ObjectAnimator.ofArgb(titleView, "textColor", titleView.getCurrentTextColor(), color2), + ObjectAnimator.ofArgb(subtitleView, "textColor", subtitleView.getCurrentTextColor(), color3), + ObjectAnimator.ofArgb(removeButton.getDrawable(), "tint", subtitleView.getCurrentTextColor(), color3) + ); + editButton.getDrawable().setTint(color3); + set.setDuration(250); + set.setInterpolator(CubicBezierInterpolator.DEFAULT); + set.addListener(new AnimatorListenerAdapter(){ + @Override + public void onAnimationEnd(Animator animation){ + errorTransitionAnimator=null; + } + }); + set.start(); + errorTransitionAnimator=set; + } } enum AttachmentUploadState{ diff --git a/mastodon/src/main/java/org/joinmastodon/android/fragments/ComposeImageDescriptionFragment.java b/mastodon/src/main/java/org/joinmastodon/android/fragments/ComposeImageDescriptionFragment.java index 02bea5f6d..812d6fab6 100644 --- a/mastodon/src/main/java/org/joinmastodon/android/fragments/ComposeImageDescriptionFragment.java +++ b/mastodon/src/main/java/org/joinmastodon/android/fragments/ComposeImageDescriptionFragment.java @@ -1,10 +1,18 @@ package org.joinmastodon.android.fragments; import android.app.Activity; -import android.content.res.TypedArray; +import android.content.Context; +import android.graphics.Bitmap; +import android.graphics.Rect; +import android.graphics.drawable.Drawable; +import android.media.MediaMetadataRetriever; import android.net.Uri; +import android.os.Build; import android.os.Bundle; -import android.view.Gravity; +import android.text.SpannableStringBuilder; +import android.text.style.BulletSpan; +import android.util.Log; +import android.view.ContextThemeWrapper; import android.view.LayoutInflater; import android.view.Menu; import android.view.MenuInflater; @@ -12,28 +20,34 @@ import android.view.MenuItem; import android.view.View; import android.view.ViewGroup; import android.view.inputmethod.InputMethodManager; -import android.widget.Button; import android.widget.EditText; -import android.widget.FrameLayout; import android.widget.ImageView; import org.joinmastodon.android.R; -import org.joinmastodon.android.api.requests.statuses.UpdateAttachment; +import org.joinmastodon.android.api.MastodonAPIController; import org.joinmastodon.android.model.Attachment; -import org.parceler.Parcels; +import org.joinmastodon.android.ui.M3AlertDialogBuilder; +import org.joinmastodon.android.ui.photoviewer.PhotoViewer; +import org.joinmastodon.android.ui.utils.UiUtils; +import org.joinmastodon.android.ui.views.FixedAspectRatioImageView; -import me.grishka.appkit.Nav; -import me.grishka.appkit.api.Callback; -import me.grishka.appkit.api.ErrorResponse; -import me.grishka.appkit.fragments.ToolbarFragment; +import java.util.Collections; + +import androidx.annotation.NonNull; +import androidx.annotation.Nullable; +import me.grishka.appkit.fragments.OnBackPressedListener; import me.grishka.appkit.imageloader.ViewImageLoader; import me.grishka.appkit.imageloader.requests.UrlImageLoaderRequest; import me.grishka.appkit.utils.V; -public class ComposeImageDescriptionFragment extends MastodonToolbarFragment{ +public class ComposeImageDescriptionFragment extends MastodonToolbarFragment implements OnBackPressedListener{ + private static final String TAG="ComposeImageDescription"; + private String accountID, attachmentID; private EditText edit; - private Button saveButton; + private FixedAspectRatioImageView image; + private ContextThemeWrapper themeWrapper; + private PhotoViewer photoViewer; @Override public void onCreate(Bundle savedInstanceState){ @@ -46,7 +60,13 @@ public class ComposeImageDescriptionFragment extends MastodonToolbarFragment{ @Override public void onAttach(Activity activity){ super.onAttach(activity); - setTitle(R.string.edit_image); + themeWrapper=new ContextThemeWrapper(activity, R.style.Theme_Mastodon_Dark); + setTitle(R.string.add_alt_text); + } + + @Override + public View onCreateView(LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState){ + return super.onCreateView(themeWrapper.getSystemService(LayoutInflater.class), container, savedInstanceState); } @Override @@ -54,14 +74,48 @@ public class ComposeImageDescriptionFragment extends MastodonToolbarFragment{ View view=inflater.inflate(R.layout.fragment_image_description, container, false); edit=view.findViewById(R.id.edit); - ImageView image=view.findViewById(R.id.photo); + image=view.findViewById(R.id.photo); + int width=getArguments().getInt("width", 0); + int height=getArguments().getInt("height", 0); + if(width>0 && height>0){ + image.setAspectRatio(Math.max(1f, (float)width/height)); + } + image.setOnClickListener(v->openPhotoViewer()); Uri uri=getArguments().getParcelable("uri"); - ViewImageLoader.load(image, null, new UrlImageLoaderRequest(uri, 1000, 1000)); + Attachment.Type type=Attachment.Type.valueOf(getArguments().getString("attachmentType")); + if(type==Attachment.Type.IMAGE) + ViewImageLoader.load(image, null, new UrlImageLoaderRequest(uri, 1000, 1000)); + else + loadVideoThumbIntoView(image, uri); edit.setText(getArguments().getString("existingDescription")); return view; } + private void loadVideoThumbIntoView(ImageView target, Uri uri){ + MastodonAPIController.runInBackground(()->{ + Context context=getActivity(); + if(context==null) + return; + try{ + MediaMetadataRetriever mmr=new MediaMetadataRetriever(); + mmr.setDataSource(context, uri); + Bitmap frame=mmr.getFrameAtTime(3_000_000); + mmr.release(); + int size=Math.max(frame.getWidth(), frame.getHeight()); + int maxSize=V.dp(250); + if(size>maxSize){ + float factor=maxSize/(float)size; + frame=Bitmap.createScaledBitmap(frame, Math.round(frame.getWidth()*factor), Math.round(frame.getHeight()*factor), true); + } + Bitmap finalFrame=frame; + target.post(()->target.setImageBitmap(finalFrame)); + }catch(Exception x){ + Log.w(TAG, "loadVideoThumbIntoView: error getting video frame", x); + } + }); + } + @Override public void onViewCreated(View view, Bundle savedInstanceState){ super.onViewCreated(view, savedInstanceState); @@ -71,43 +125,114 @@ public class ComposeImageDescriptionFragment extends MastodonToolbarFragment{ @Override public void onCreateOptionsMenu(Menu menu, MenuInflater inflater){ - TypedArray ta=getActivity().obtainStyledAttributes(new int[]{R.attr.secondaryButtonStyle}); - int buttonStyle=ta.getResourceId(0, 0); - ta.recycle(); - saveButton=new Button(getActivity(), null, 0, buttonStyle); - saveButton.setText(R.string.save); - saveButton.setOnClickListener(this::onSaveClick); - FrameLayout wrap=new FrameLayout(getActivity()); - wrap.addView(saveButton, new FrameLayout.LayoutParams(ViewGroup.LayoutParams.WRAP_CONTENT, ViewGroup.LayoutParams.WRAP_CONTENT, Gravity.TOP|Gravity.LEFT)); - wrap.setPadding(V.dp(16), V.dp(4), V.dp(16), V.dp(8)); - wrap.setClipToPadding(false); - MenuItem item=menu.add(R.string.publish); - item.setActionView(wrap); - item.setShowAsAction(MenuItem.SHOW_AS_ACTION_ALWAYS); + inflater.inflate(R.menu.compose_image_description, menu); } @Override public boolean onOptionsItemSelected(MenuItem item){ + if(item.getItemId()==R.id.help){ + SpannableStringBuilder msg=new SpannableStringBuilder(getText(R.string.alt_text_help)); + BulletSpan[] spans=msg.getSpans(0, msg.length(), BulletSpan.class); + for(BulletSpan span:spans){ + BulletSpan betterSpan; + if(Build.VERSION.SDK_INT(){ - @Override - public void onSuccess(Attachment result){ - Bundle r=new Bundle(); - r.putParcelable("attachment", Parcels.wrap(result)); - setResult(true, r); - Nav.finish(ComposeImageDescriptionFragment.this); - } + @Override + public boolean onBackPressed(){ + deliverResult(); + return false; + } - @Override - public void onError(ErrorResponse error){ - error.showToast(getActivity()); - } - }) - .wrapProgress(getActivity(), R.string.saving, false) - .exec(accountID); + @Override + protected LayoutInflater getToolbarLayoutInflater(){ + return LayoutInflater.from(themeWrapper); + } + + private void deliverResult(){ + Bundle r=new Bundle(); + r.putString("text", edit.getText().toString().trim()); + r.putString("attachment", attachmentID); + setResult(true, r); + } + + private void openPhotoViewer(){ + Attachment fakeAttachment=new Attachment(); + fakeAttachment.id="local"; + fakeAttachment.type=Attachment.Type.valueOf(getArguments().getString("attachmentType")); + int width=getArguments().getInt("width", 0); + int height=getArguments().getInt("height", 0); + Uri uri=getArguments().getParcelable("uri"); + fakeAttachment.url=uri.toString(); + fakeAttachment.meta=new Attachment.Metadata(); + fakeAttachment.meta.width=width; + fakeAttachment.meta.height=height; + + photoViewer=new PhotoViewer(getActivity(), Collections.singletonList(fakeAttachment), 0, new PhotoViewer.Listener(){ + @Override + public void setPhotoViewVisibility(int index, boolean visible){ + image.setAlpha(visible ? 1f : 0f); + } + + @Override + public boolean startPhotoViewTransition(int index, @NonNull Rect outRect, @NonNull int[] outCornerRadius){ + int[] pos={0, 0}; + image.getLocationOnScreen(pos); + outRect.set(pos[0], pos[1], pos[0]+image.getWidth(), pos[1]+image.getHeight()); + image.setElevation(1f); + return true; + } + + @Override + public void setTransitioningViewTransform(float translateX, float translateY, float scale){ + image.setTranslationX(translateX); + image.setTranslationY(translateY); + image.setScaleX(scale); + image.setScaleY(scale); + } + + @Override + public void endPhotoViewTransition(){ + Drawable d=image.getDrawable(); + image.setImageDrawable(null); + image.setImageDrawable(d); + + image.setTranslationX(0f); + image.setTranslationY(0f); + image.setScaleX(1f); + image.setScaleY(1f); + image.setElevation(0f); + } + + @Nullable + @Override + public Drawable getPhotoViewCurrentDrawable(int index){ + return image.getDrawable(); + } + + @Override + public void photoViewerDismissed(){ + photoViewer=null; + } + + @Override + public void onRequestPermissions(String[] permissions){ + + } + }); + photoViewer.removeMenu(); } } diff --git a/mastodon/src/main/java/org/joinmastodon/android/fragments/MastodonToolbarFragment.java b/mastodon/src/main/java/org/joinmastodon/android/fragments/MastodonToolbarFragment.java index 93cb88fdf..8bc334a33 100644 --- a/mastodon/src/main/java/org/joinmastodon/android/fragments/MastodonToolbarFragment.java +++ b/mastodon/src/main/java/org/joinmastodon/android/fragments/MastodonToolbarFragment.java @@ -11,6 +11,15 @@ import androidx.annotation.CallSuper; import me.grishka.appkit.fragments.ToolbarFragment; public abstract class MastodonToolbarFragment extends ToolbarFragment{ + + public MastodonToolbarFragment(){ + super(); + } + + protected MastodonToolbarFragment(int layout){ + super(layout); + } + @Override public void onViewCreated(View view, Bundle savedInstanceState){ super.onViewCreated(view, savedInstanceState); diff --git a/mastodon/src/main/java/org/joinmastodon/android/fragments/ProfileFragment.java b/mastodon/src/main/java/org/joinmastodon/android/fragments/ProfileFragment.java index f579efa4b..29d49884d 100644 --- a/mastodon/src/main/java/org/joinmastodon/android/fragments/ProfileFragment.java +++ b/mastodon/src/main/java/org/joinmastodon/android/fragments/ProfileFragment.java @@ -387,6 +387,8 @@ public class ProfileFragment extends LoaderFragment implements OnBackPressedList @Override public void onPageScrollStateChanged(int state){ + if(isInEditMode) + return; refreshLayout.setEnabled(state!=ViewPager2.SCROLL_STATE_DRAGGING); } }); @@ -801,6 +803,7 @@ public class ProfileFragment extends LoaderFragment implements OnBackPressedList bioEdit.setText(account.source.note); aboutFragment.enterEditMode(account.source.fields); + refreshLayout.setEnabled(false); } private void exitEditMode(){ @@ -840,6 +843,7 @@ public class ProfileFragment extends LoaderFragment implements OnBackPressedList username.setVisibility(View.VISIBLE); bio.setVisibility(View.VISIBLE); countersLayout.setVisibility(View.VISIBLE); + refreshLayout.setEnabled(true); bindHeaderView(); } diff --git a/mastodon/src/main/java/org/joinmastodon/android/ui/ComposeAutocompleteViewController.java b/mastodon/src/main/java/org/joinmastodon/android/ui/ComposeAutocompleteViewController.java index f93e66bb7..b4522c0c1 100644 --- a/mastodon/src/main/java/org/joinmastodon/android/ui/ComposeAutocompleteViewController.java +++ b/mastodon/src/main/java/org/joinmastodon/android/ui/ComposeAutocompleteViewController.java @@ -1,6 +1,8 @@ package org.joinmastodon.android.ui; +import android.annotation.SuppressLint; import android.app.Activity; +import android.graphics.Rect; import android.graphics.Typeface; import android.graphics.drawable.Drawable; import android.text.TextUtils; @@ -19,7 +21,6 @@ import org.joinmastodon.android.model.Account; import org.joinmastodon.android.model.Emoji; import org.joinmastodon.android.model.Hashtag; import org.joinmastodon.android.model.SearchResults; -import org.joinmastodon.android.ui.drawables.ComposeAutocompleteBackgroundDrawable; import org.joinmastodon.android.ui.text.HtmlParser; import org.joinmastodon.android.ui.utils.CustomEmojiHelper; import org.joinmastodon.android.ui.utils.UiUtils; @@ -60,7 +61,6 @@ public class ComposeAutocompleteViewController{ private APIRequest currentRequest; private Runnable usersDebouncer=this::doSearchUsers, hashtagsDebouncer=this::doSearchHashtags; private String lastText; - private ComposeAutocompleteBackgroundDrawable background; private boolean listIsHidden=true; private UsersAdapter usersAdapter; @@ -69,19 +69,25 @@ public class ComposeAutocompleteViewController{ private Consumer completionSelectedListener; - private DividerItemDecoration usersDividers, hashtagsDividers; - public ComposeAutocompleteViewController(Activity activity, String accountID){ this.activity=activity; this.accountID=accountID; - background=new ComposeAutocompleteBackgroundDrawable(UiUtils.getThemeColor(activity, android.R.attr.colorBackground)); contentView=new FrameLayout(activity); - contentView.setBackground(background); list=new UsableRecyclerView(activity); - list.setLayoutManager(new LinearLayoutManager(activity)); + list.setLayoutManager(new LinearLayoutManager(activity, LinearLayoutManager.HORIZONTAL, false)); list.setItemAnimator(new BetterItemAnimator()); list.setVisibility(View.GONE); + list.setPadding(V.dp(16), V.dp(12), V.dp(16), V.dp(12)); + list.setClipToPadding(false); + list.setSelector(null); + list.addItemDecoration(new RecyclerView.ItemDecoration(){ + @Override + public void getItemOffsets(@NonNull Rect outRect, @NonNull View view, @NonNull RecyclerView parent, @NonNull RecyclerView.State state){ + if(parent.getChildAdapterPosition(view) implements ImageLoaderViewHolder, UsableRecyclerView.Clickable{ private final ImageView ava; - private final TextView name, username; + private final TextView username; private UserViewHolder(){ super(activity, R.layout.item_autocomplete_user, list); ava=findViewById(R.id.photo); - name=findViewById(R.id.name); username=findViewById(R.id.username); - ava.setOutlineProvider(OutlineProviders.roundedRect(12)); + ava.setOutlineProvider(OutlineProviders.OVAL); ava.setClipToOutline(true); } @Override public void onBind(WrappedAccount item){ - name.setText(item.parsedName); username.setText("@"+item.account.acct); } @@ -300,7 +292,6 @@ public class ComposeAutocompleteViewController{ ava.setImageDrawable(image); }else{ item.emojiHelper.setImageDrawable(index-1, image); - name.invalidate(); } } @@ -333,17 +324,11 @@ public class ComposeAutocompleteViewController{ private final TextView text; private HashtagViewHolder(){ - super(new TextView(activity)); + super(activity, R.layout.item_autocomplete_hashtag, list); text=(TextView) itemView; - text.setLayoutParams(new RecyclerView.LayoutParams(ViewGroup.LayoutParams.MATCH_PARENT, V.dp(48))); - text.setTextAppearance(R.style.m3_title_medium); - text.setTypeface(Typeface.DEFAULT); - text.setSingleLine(); - text.setEllipsize(TextUtils.TruncateAt.END); - text.setGravity(Gravity.CENTER_VERTICAL); - text.setPadding(V.dp(16), 0, V.dp(16), 0); } + @SuppressLint("SetTextI18n") @Override public void onBind(Hashtag item){ text.setText("#"+item.name); @@ -395,7 +380,7 @@ public class ComposeAutocompleteViewController{ private EmojiViewHolder(){ super(activity, R.layout.item_autocomplete_user, list); ava=findViewById(R.id.photo); - name=findViewById(R.id.name); + name=findViewById(R.id.username); } @Override @@ -408,6 +393,7 @@ public class ComposeAutocompleteViewController{ ava.setImageDrawable(null); } + @SuppressLint("SetTextI18n") @Override public void onBind(WrappedEmoji item){ name.setText(":"+item.emoji.shortcode+":"); diff --git a/mastodon/src/main/java/org/joinmastodon/android/ui/drawables/ComposeAutocompleteBackgroundDrawable.java b/mastodon/src/main/java/org/joinmastodon/android/ui/drawables/ComposeAutocompleteBackgroundDrawable.java deleted file mode 100644 index 2e26af4dd..000000000 --- a/mastodon/src/main/java/org/joinmastodon/android/ui/drawables/ComposeAutocompleteBackgroundDrawable.java +++ /dev/null @@ -1,84 +0,0 @@ -package org.joinmastodon.android.ui.drawables; - -import android.graphics.Canvas; -import android.graphics.ColorFilter; -import android.graphics.Paint; -import android.graphics.Path; -import android.graphics.PixelFormat; -import android.graphics.Rect; -import android.graphics.drawable.Drawable; - -import androidx.annotation.NonNull; -import androidx.annotation.Nullable; -import me.grishka.appkit.utils.V; - -public class ComposeAutocompleteBackgroundDrawable extends Drawable{ - private Path path=new Path(); - private Paint paint=new Paint(Paint.ANTI_ALIAS_FLAG); - private int fillColor, arrowOffset; - - public ComposeAutocompleteBackgroundDrawable(int fillColor){ - this.fillColor=fillColor; - } - - @Override - public void draw(@NonNull Canvas canvas){ - Rect bounds=getBounds(); - canvas.save(); - canvas.translate(bounds.left, bounds.top); - paint.setColor(0x80000000); - canvas.drawPath(path, paint); - canvas.translate(0, V.dp(1)); - paint.setColor(fillColor); - canvas.drawPath(path, paint); - int arrowSize=V.dp(10); - canvas.drawRect(0, arrowSize, bounds.width(), bounds.height(), paint); - canvas.restore(); - } - - @Override - public void setAlpha(int alpha){ - - } - - @Override - public void setColorFilter(@Nullable ColorFilter colorFilter){ - - } - - @Override - public int getOpacity(){ - return PixelFormat.TRANSLUCENT; - } - - public void setArrowOffset(int offset){ - arrowOffset=offset; - updatePath(); - invalidateSelf(); - } - - @Override - protected void onBoundsChange(Rect bounds){ - super.onBoundsChange(bounds); - updatePath(); - } - - @Override - public boolean getPadding(@NonNull Rect padding){ - padding.top=V.dp(11); - return true; - } - - private void updatePath(){ - path.rewind(); - int arrowSize=V.dp(10); - path.moveTo(0, arrowSize*2); - path.lineTo(0, arrowSize); - path.lineTo(arrowOffset-arrowSize, arrowSize); - path.lineTo(arrowOffset, 0); - path.lineTo(arrowOffset+arrowSize, arrowSize); - path.lineTo(getBounds().width(), arrowSize); - path.lineTo(getBounds().width(), arrowSize*2); - path.close(); - } -} diff --git a/mastodon/src/main/java/org/joinmastodon/android/ui/photoviewer/PhotoViewer.java b/mastodon/src/main/java/org/joinmastodon/android/ui/photoviewer/PhotoViewer.java index d9c0c8334..47b8a4645 100644 --- a/mastodon/src/main/java/org/joinmastodon/android/ui/photoviewer/PhotoViewer.java +++ b/mastodon/src/main/java/org/joinmastodon/android/ui/photoviewer/PhotoViewer.java @@ -259,6 +259,10 @@ public class PhotoViewer implements ZoomPanView.Listener{ }); } + public void removeMenu(){ + toolbar.getMenu().clear(); + } + @Override public void onTransitionAnimationUpdate(float translateX, float translateY, float scale){ listener.setTransitioningViewTransform(translateX, translateY, scale); diff --git a/mastodon/src/main/java/org/joinmastodon/android/ui/text/LengthLimitHighlighter.java b/mastodon/src/main/java/org/joinmastodon/android/ui/text/LengthLimitHighlighter.java new file mode 100644 index 000000000..e97417b2e --- /dev/null +++ b/mastodon/src/main/java/org/joinmastodon/android/ui/text/LengthLimitHighlighter.java @@ -0,0 +1,67 @@ +package org.joinmastodon.android.ui.text; + +import android.content.Context; +import android.text.Editable; +import android.text.TextWatcher; +import android.text.style.BackgroundColorSpan; +import android.text.style.ForegroundColorSpan; + +import org.joinmastodon.android.R; +import org.joinmastodon.android.ui.utils.UiUtils; + +public class LengthLimitHighlighter implements TextWatcher{ + private final Context context; + private final int lengthLimit; + private BackgroundColorSpan overLimitBG; + private ForegroundColorSpan overLimitFG; + private boolean isOverLimit; + private OverLimitChangeListener listener; + + public LengthLimitHighlighter(Context context, int lengthLimit){ + this.context=context; + overLimitBG=new BackgroundColorSpan(UiUtils.getThemeColor(context, R.attr.colorM3ErrorContainer)); + overLimitFG=new ForegroundColorSpan(UiUtils.getThemeColor(context, R.attr.colorM3Error)); + this.lengthLimit=lengthLimit; + } + + @Override + public void beforeTextChanged(CharSequence s, int start, int count, int after){ + + } + + @Override + public void onTextChanged(CharSequence s, int start, int before, int count){ + + } + + @Override + public void afterTextChanged(Editable s){ + s.removeSpan(overLimitBG); + s.removeSpan(overLimitFG); + boolean newOverLimit=s.length()>lengthLimit; + if(newOverLimit){ + int start=s.length()-(s.length()-lengthLimit); + int end=s.length(); + s.setSpan(overLimitFG, start, end, 0); + s.setSpan(overLimitBG, start, end, 0); + } + if(newOverLimit!=isOverLimit){ + isOverLimit=newOverLimit; + if(listener!=null) + listener.onOverLimitChanged(isOverLimit); + } + } + + public LengthLimitHighlighter setListener(OverLimitChangeListener listener){ + this.listener=listener; + return this; + } + + public boolean isOverLimit(){ + return isOverLimit; + } + + public interface OverLimitChangeListener{ + void onOverLimitChanged(boolean isOverLimit); + } +} diff --git a/mastodon/src/main/java/org/joinmastodon/android/ui/utils/UiUtils.java b/mastodon/src/main/java/org/joinmastodon/android/ui/utils/UiUtils.java index 778755d7c..d89a79dbe 100644 --- a/mastodon/src/main/java/org/joinmastodon/android/ui/utils/UiUtils.java +++ b/mastodon/src/main/java/org/joinmastodon/android/ui/utils/UiUtils.java @@ -11,7 +11,6 @@ import android.content.res.TypedArray; import android.database.Cursor; import android.graphics.Bitmap; import android.graphics.Canvas; -import android.graphics.Typeface; import android.graphics.drawable.BitmapDrawable; import android.graphics.drawable.Drawable; import android.graphics.drawable.InsetDrawable; @@ -27,9 +26,15 @@ import android.provider.OpenableColumns; import android.text.SpannableStringBuilder; import android.text.Spanned; import android.text.TextUtils; +import android.transition.ChangeBounds; +import android.transition.ChangeScroll; +import android.transition.Fade; +import android.transition.TransitionManager; +import android.transition.TransitionSet; import android.view.Menu; import android.view.MenuItem; import android.view.View; +import android.view.ViewGroup; import android.webkit.MimeTypeMap; import android.widget.Button; import android.widget.PopupMenu; @@ -86,6 +91,7 @@ import me.grishka.appkit.api.Callback; import me.grishka.appkit.api.ErrorResponse; import me.grishka.appkit.imageloader.ViewImageLoader; import me.grishka.appkit.imageloader.requests.UrlImageLoaderRequest; +import me.grishka.appkit.utils.CubicBezierInterpolator; import me.grishka.appkit.utils.V; import okhttp3.MediaType; @@ -642,6 +648,10 @@ public class UiUtils{ return 0xFF000000 | (r << 16) | (g << 8) | b; } + public static int alphaBlendThemeColors(Context context, @AttrRes int color1, @AttrRes int color2, float alpha){ + return alphaBlendColors(getThemeColor(context, color1), getThemeColor(context, color2), alpha); + } + /** * Check to see if Android platform photopicker is available on the device\ * @@ -713,4 +723,14 @@ public class UiUtils{ else return String.format("%d:%02d", seconds/60, seconds%60); } + + public static void beginLayoutTransition(ViewGroup sceneRoot){ + TransitionManager.beginDelayedTransition(sceneRoot, new TransitionSet() + .addTransition(new Fade(Fade.IN | Fade.OUT)) + .addTransition(new ChangeBounds()) + .addTransition(new ChangeScroll()) + .setDuration(250) + .setInterpolator(CubicBezierInterpolator.DEFAULT) + ); + } } diff --git a/mastodon/src/main/java/org/joinmastodon/android/ui/views/CheckableLinearLayout.java b/mastodon/src/main/java/org/joinmastodon/android/ui/views/CheckableLinearLayout.java new file mode 100644 index 000000000..193f4f0e2 --- /dev/null +++ b/mastodon/src/main/java/org/joinmastodon/android/ui/views/CheckableLinearLayout.java @@ -0,0 +1,50 @@ +package org.joinmastodon.android.ui.views; + +import android.content.Context; +import android.util.AttributeSet; +import android.widget.Checkable; +import android.widget.LinearLayout; + +public class CheckableLinearLayout extends LinearLayout implements Checkable{ + private boolean checked; + private static final int[] CHECKED_STATE_SET = { + android.R.attr.state_checked + }; + + public CheckableLinearLayout(Context context){ + this(context, null); + } + + public CheckableLinearLayout(Context context, AttributeSet attrs){ + this(context, attrs, 0); + } + + public CheckableLinearLayout(Context context, AttributeSet attrs, int defStyle){ + super(context, attrs, defStyle); + } + + @Override + public void setChecked(boolean checked){ + this.checked=checked; + refreshDrawableState(); + } + + @Override + public boolean isChecked(){ + return checked; + } + + @Override + public void toggle(){ + setChecked(!checked); + } + + @Override + protected int[] onCreateDrawableState(int extraSpace) { + final int[] drawableState = super.onCreateDrawableState(extraSpace + 1); + if (isChecked()) { + mergeDrawableStates(drawableState, CHECKED_STATE_SET); + } + return drawableState; + } +} diff --git a/mastodon/src/main/java/org/joinmastodon/android/ui/views/FixedAspectRatioImageView.java b/mastodon/src/main/java/org/joinmastodon/android/ui/views/FixedAspectRatioImageView.java new file mode 100644 index 000000000..c3529799b --- /dev/null +++ b/mastodon/src/main/java/org/joinmastodon/android/ui/views/FixedAspectRatioImageView.java @@ -0,0 +1,36 @@ +package org.joinmastodon.android.ui.views; + +import android.content.Context; +import android.util.AttributeSet; +import android.widget.ImageView; + +public class FixedAspectRatioImageView extends ImageView{ + private float aspectRatio=1; + + public FixedAspectRatioImageView(Context context){ + this(context, null); + } + + public FixedAspectRatioImageView(Context context, AttributeSet attrs){ + this(context, attrs, 0); + } + + public FixedAspectRatioImageView(Context context, AttributeSet attrs, int defStyle){ + super(context, attrs, defStyle); + } + + @Override + protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec){ + int width=MeasureSpec.getSize(widthMeasureSpec); + heightMeasureSpec=Math.round(width/aspectRatio) | MeasureSpec.EXACTLY; + super.onMeasure(widthMeasureSpec, heightMeasureSpec); + } + + public float getAspectRatio(){ + return aspectRatio; + } + + public void setAspectRatio(float aspectRatio){ + this.aspectRatio=aspectRatio; + } +} diff --git a/mastodon/src/main/java/org/joinmastodon/android/ui/views/FloatingHintEditTextLayout.java b/mastodon/src/main/java/org/joinmastodon/android/ui/views/FloatingHintEditTextLayout.java index 01a24ba50..b02f68652 100644 --- a/mastodon/src/main/java/org/joinmastodon/android/ui/views/FloatingHintEditTextLayout.java +++ b/mastodon/src/main/java/org/joinmastodon/android/ui/views/FloatingHintEditTextLayout.java @@ -127,7 +127,12 @@ public class FloatingHintEditTextLayout extends FrameLayout implements CustomVie edit.getViewTreeObserver().removeOnPreDrawListener(this); float scale=edit.getLineHeight()/(float)label.getLineHeight(); - float transY=edit.getHeight()/2f-edit.getLineHeight()/2f+(edit.getTop()-label.getTop())-(label.getHeight()/2f-label.getLineHeight()/2f); + float transY; + if((edit.getGravity() & Gravity.TOP)==Gravity.TOP){ + transY=edit.getPaddingTop()+(edit.getTop()-label.getTop())-(label.getHeight()/2f-label.getLineHeight()/2f); + }else{ + transY=edit.getHeight()/2f-edit.getLineHeight()/2f+(edit.getTop()-label.getTop())-(label.getHeight()/2f-label.getLineHeight()/2f); + } AnimatorSet anim=new AnimatorSet(); if(hintVisible){ diff --git a/mastodon/src/main/java/org/joinmastodon/android/ui/views/HorizontalScrollViewThatRespectsMatchParent.java b/mastodon/src/main/java/org/joinmastodon/android/ui/views/HorizontalScrollViewThatRespectsMatchParent.java new file mode 100644 index 000000000..eed9dfa4f --- /dev/null +++ b/mastodon/src/main/java/org/joinmastodon/android/ui/views/HorizontalScrollViewThatRespectsMatchParent.java @@ -0,0 +1,36 @@ +package org.joinmastodon.android.ui.views; + +import android.content.Context; +import android.util.AttributeSet; +import android.view.View; +import android.view.ViewGroup; +import android.widget.HorizontalScrollView; + +public class HorizontalScrollViewThatRespectsMatchParent extends HorizontalScrollView{ + public HorizontalScrollViewThatRespectsMatchParent(Context context){ + super(context); + } + + public HorizontalScrollViewThatRespectsMatchParent(Context context, AttributeSet attrs){ + super(context, attrs); + } + + public HorizontalScrollViewThatRespectsMatchParent(Context context, AttributeSet attrs, int defStyleAttr){ + super(context, attrs, defStyleAttr); + } + + @Override + protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec){ + if(getChildCount()==0) + return; + View child=getChildAt(0); + ViewGroup.LayoutParams lp=child.getLayoutParams(); + if(lp.width==ViewGroup.LayoutParams.MATCH_PARENT){ + int hms=getChildMeasureSpec(heightMeasureSpec, getPaddingTop()+getPaddingBottom(), lp.height); + child.measure(MeasureSpec.getSize(widthMeasureSpec) | MeasureSpec.EXACTLY, hms); + setMeasuredDimension(child.getMeasuredWidth(), child.getMeasuredHeight()); + return; + } + super.onMeasure(widthMeasureSpec, heightMeasureSpec); + } +} diff --git a/mastodon/src/main/java/org/joinmastodon/android/ui/views/ReorderableLinearLayout.java b/mastodon/src/main/java/org/joinmastodon/android/ui/views/ReorderableLinearLayout.java index 46cd1c2d1..3285848f2 100644 --- a/mastodon/src/main/java/org/joinmastodon/android/ui/views/ReorderableLinearLayout.java +++ b/mastodon/src/main/java/org/joinmastodon/android/ui/views/ReorderableLinearLayout.java @@ -6,19 +6,48 @@ import android.util.Log; import android.view.MotionEvent; import android.view.View; import android.view.ViewTreeObserver; +import android.view.animation.Interpolator; import android.widget.LinearLayout; +import org.joinmastodon.android.R; + import androidx.annotation.Nullable; import me.grishka.appkit.utils.CubicBezierInterpolator; +import me.grishka.appkit.utils.CustomViewHelper; import me.grishka.appkit.utils.V; -public class ReorderableLinearLayout extends LinearLayout{ +public class ReorderableLinearLayout extends LinearLayout implements CustomViewHelper{ private static final String TAG="ReorderableLinearLayout"; + private static final Interpolator sDragScrollInterpolator=t->t * t * t * t * t; + + private static final Interpolator sDragViewScrollCapInterpolator=t->{ + t -= 1.0f; + return t * t * t * t * t + 1.0f; + }; + private static final long DRAG_SCROLL_ACCELERATION_LIMIT_TIME_MS = 2000; + private View draggedView; private View bottomSibling, topSibling; - private float startY; + private float startX, startY, dX, dY, viewStartX, viewStartY; private OnDragListener dragListener; + private boolean moveInBothDimensions; + private int edgeSize; + private View scrollableParent; + private long dragScrollStartTime; + private int cachedMaxScrollSpeed=-1; + final Runnable scrollRunnable= new Runnable() { + @Override + public void run() { + if (draggedView != null && scrollIfNecessary()) { + if (draggedView != null) { //it might be lost during scrolling +// moveIfNecessary(mSelected); + } + removeCallbacks(scrollRunnable); + postOnAnimation(this); + } + } + }; public ReorderableLinearLayout(Context context){ super(context); @@ -30,12 +59,13 @@ public class ReorderableLinearLayout extends LinearLayout{ public ReorderableLinearLayout(Context context, @Nullable AttributeSet attrs, int defStyleAttr){ super(context, attrs, defStyleAttr); + edgeSize=dp(20); } public void startDragging(View child){ getParent().requestDisallowInterceptTouchEvent(true); draggedView=child; - draggedView.animate().translationZ(V.dp(1f)).setDuration(150).setInterpolator(CubicBezierInterpolator.DEFAULT).start(); + dragListener.onDragStart(draggedView); int index=indexOfChild(child); if(index==-1) @@ -44,11 +74,30 @@ public class ReorderableLinearLayout extends LinearLayout{ topSibling=getChildAt(index-1); if(index=bottomSibling.getY()){ - moveDraggedView(1); + dX=ev.getX()-startX; + dY=ev.getY()-startY; + + if(moveInBothDimensions){ + draggedView.setTranslationX(dX); + draggedView.setTranslationY(dY); + }else if(getOrientation()==VERTICAL){ + draggedView.setTranslationY(dY); + }else{ + draggedView.setTranslationX(dX); } + + removeCallbacks(scrollRunnable); + scrollRunnable.run(); + + if(getOrientation()==VERTICAL){ + if(topSibling!=null && draggedView.getY()<=topSibling.getY()){ + moveDraggedView(-1); + }else if(bottomSibling!=null && draggedView.getY()>=bottomSibling.getY()){ + moveDraggedView(1); + } + }else{ + if(topSibling!=null && draggedView.getX()<=topSibling.getX()){ + moveDraggedView(-1); + }else if(bottomSibling!=null && draggedView.getX()>=bottomSibling.getX()){ + moveDraggedView(1); + } + } + dragListener.onDragMove(draggedView); } } return super.onTouchEvent(ev); } private void endDrag(){ - draggedView.animate().translationY(0f).translationZ(0f).setDuration(200).setInterpolator(CubicBezierInterpolator.DEFAULT).start(); + dragListener.onDragEnd(draggedView); } private void moveDraggedView(int positionOffset){ int index=indexOfChild(draggedView); - int prevTop=draggedView.getTop(); + + boolean isVertical=getOrientation()==VERTICAL; + + int prevOffset=isVertical ? draggedView.getTop() : draggedView.getLeft(); removeView(draggedView); int prevIndex=index; index+=positionOffset; addView(draggedView, index); final View prevSibling=positionOffset<0 ? topSibling : bottomSibling; - int prevSiblingTop=prevSibling.getTop(); + int prevSiblingOffset=isVertical ? prevSibling.getTop() : prevSibling.getLeft(); if(index>0) topSibling=getChildAt(index-1); else @@ -101,11 +177,20 @@ public class ReorderableLinearLayout extends LinearLayout{ @Override public boolean onPreDraw(){ draggedView.getViewTreeObserver().removeOnPreDrawListener(this); - float offset=prevTop-draggedView.getTop(); - startY-=offset; - draggedView.setTranslationY(draggedView.getTranslationY()+offset); - prevSibling.setTranslationY(prevSiblingTop-prevSibling.getTop()); - prevSibling.animate().translationY(0f).setInterpolator(CubicBezierInterpolator.DEFAULT).setDuration(200).start(); + float offset=prevOffset-(isVertical ? draggedView.getTop() : draggedView.getLeft()); + if(isVertical){ + startY-=offset; + viewStartY-=offset; + draggedView.setTranslationY(draggedView.getTranslationY()+offset); + prevSibling.setTranslationY(prevSiblingOffset-prevSibling.getTop()); + prevSibling.animate().translationY(0f).setInterpolator(CubicBezierInterpolator.DEFAULT).setDuration(200).start(); + }else{ + startX-=offset; + viewStartX-=offset; + draggedView.setTranslationX(draggedView.getTranslationX()+offset); + prevSibling.setTranslationX(prevSiblingOffset-prevSibling.getLeft()); + prevSibling.animate().translationX(0f).setInterpolator(CubicBezierInterpolator.DEFAULT).setDuration(200).start(); + } return true; } }); @@ -115,7 +200,105 @@ public class ReorderableLinearLayout extends LinearLayout{ this.dragListener=dragListener; } + public boolean isMoveInBothDimensions(){ + return moveInBothDimensions; + } + + public void setMoveInBothDimensions(boolean moveInBothDimensions){ + this.moveInBothDimensions=moveInBothDimensions; + } + + boolean scrollIfNecessary(){ + if(draggedView==null || scrollableParent==null){ + dragScrollStartTime=Long.MIN_VALUE; + return false; + } + final long now=System.currentTimeMillis(); + final long scrollDuration=dragScrollStartTime==Long.MIN_VALUE ? 0 : now-dragScrollStartTime; + int scrollX=0; + int scrollY=0; + if(getOrientation()==HORIZONTAL){ + int curX=(int) (viewStartX+dX)-scrollableParent.getScrollX(); + final int leftDiff=curX-getPaddingLeft(); + if(dX<0 && leftDiff<0){ + scrollX=leftDiff; + }else if(dX>0){ + final int rightDiff=curX+draggedView.getWidth()-(scrollableParent.getWidth()-getPaddingRight()); + if(rightDiff>0){ + scrollX=rightDiff; + } + } + }else{ + int curY=(int) (viewStartY+dY)-scrollableParent.getScrollY(); + final int topDiff=curY-getPaddingTop(); + if(dY<0 && topDiff<0){ + scrollY=topDiff; + }else if(dY>0){ + final int bottomDiff=curY+draggedView.getHeight()-(scrollableParent.getHeight()-getPaddingBottom()); + if(bottomDiff>0){ + scrollY=bottomDiff; + } + } + } + if(scrollX!=0){ + scrollX=interpolateOutOfBoundsScroll(draggedView.getWidth(), scrollX, scrollableParent.getWidth(), scrollDuration); + } + if(scrollY!=0){ + scrollY=interpolateOutOfBoundsScroll(draggedView.getHeight(), scrollY, scrollableParent.getHeight(), scrollDuration); + } + if(scrollX!=0 || scrollY!=0){ + if(dragScrollStartTime==Long.MIN_VALUE){ + dragScrollStartTime=now; + } + int prevX=scrollableParent.getScrollX(); + int prevY=scrollableParent.getScrollY(); + scrollableParent.scrollBy(scrollX, scrollY); + draggedView.setTranslationX(draggedView.getTranslationX()-(scrollableParent.getScrollX()-prevX)); + draggedView.setTranslationY(draggedView.getTranslationY()-(scrollableParent.getScrollY()-prevY)); + return true; + } + dragScrollStartTime=Long.MIN_VALUE; + return false; + } + + public int interpolateOutOfBoundsScroll(int viewSize, int viewSizeOutOfBounds, int totalSize, long msSinceStartScroll){ + final int maxScroll=getMaxDragScroll(); + final int absOutOfBounds=Math.abs(viewSizeOutOfBounds); + final int direction=(int) Math.signum(viewSizeOutOfBounds); + // might be negative if other direction + float outOfBoundsRatio=Math.min(1f, 1f*absOutOfBounds/viewSize); + final int cappedScroll=(int) (direction*maxScroll*sDragViewScrollCapInterpolator.getInterpolation(outOfBoundsRatio)); + final float timeRatio; + if(msSinceStartScroll>DRAG_SCROLL_ACCELERATION_LIMIT_TIME_MS){ + timeRatio=1f; + }else{ + timeRatio=(float) msSinceStartScroll/DRAG_SCROLL_ACCELERATION_LIMIT_TIME_MS; + } + final int value=(int) (cappedScroll*sDragScrollInterpolator.getInterpolation(timeRatio)); + if(value==0){ + return viewSizeOutOfBounds>0 ? 1 : -1; + } + return value; + } + + private int getMaxDragScroll(){ + if(cachedMaxScrollSpeed==-1){ + cachedMaxScrollSpeed=getResources().getDimensionPixelSize(R.dimen.item_touch_helper_max_drag_scroll_per_frame); + } + return cachedMaxScrollSpeed; + } + public interface OnDragListener{ void onSwapItems(int oldIndex, int newIndex); + + default void onDragStart(View view){ + view.animate().translationZ(V.dp(3f)).setDuration(150).setInterpolator(CubicBezierInterpolator.DEFAULT).start(); + } + + default void onDragEnd(View view){ + view.animate().translationY(0f).translationX(0f).translationZ(0f).setDuration(200).setInterpolator(CubicBezierInterpolator.DEFAULT).start(); + } + + default void onDragMove(View view){} } } diff --git a/mastodon/src/main/res/color/action_bar_icons.xml b/mastodon/src/main/res/color/action_bar_icons.xml new file mode 100644 index 000000000..fbb09a8a0 --- /dev/null +++ b/mastodon/src/main/res/color/action_bar_icons.xml @@ -0,0 +1,5 @@ + + + + + \ No newline at end of file diff --git a/mastodon/src/main/res/color/compose_button.xml b/mastodon/src/main/res/color/compose_button.xml index 07ea752b5..c8c71a186 100644 --- a/mastodon/src/main/res/color/compose_button.xml +++ b/mastodon/src/main/res/color/compose_button.xml @@ -1,5 +1,6 @@ - - + + + \ No newline at end of file diff --git a/mastodon/src/main/res/color/m3_on_primary_overlay.xml b/mastodon/src/main/res/color/m3_on_primary_overlay.xml new file mode 100644 index 000000000..a002a2c60 --- /dev/null +++ b/mastodon/src/main/res/color/m3_on_primary_overlay.xml @@ -0,0 +1,4 @@ + + + + \ No newline at end of file diff --git a/mastodon/src/main/res/drawable-nodpi/poof.png b/mastodon/src/main/res/drawable-nodpi/poof.png new file mode 100644 index 0000000000000000000000000000000000000000..2b13fd99068f5b2ca3dccf2a08b12853ae702c2c GIT binary patch literal 7549 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 - - - + + + \ No newline at end of file diff --git a/mastodon/src/main/res/drawable/bg_alert_button.xml b/mastodon/src/main/res/drawable/bg_alert_button.xml index 1f3f97437..3c721e36f 100644 --- a/mastodon/src/main/res/drawable/bg_alert_button.xml +++ b/mastodon/src/main/res/drawable/bg_alert_button.xml @@ -1,5 +1,5 @@ - + diff --git a/mastodon/src/main/res/drawable/bg_compose_button.xml b/mastodon/src/main/res/drawable/bg_compose_button.xml new file mode 100644 index 000000000..b5ab37301 --- /dev/null +++ b/mastodon/src/main/res/drawable/bg_compose_button.xml @@ -0,0 +1,25 @@ + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/mastodon/src/main/res/drawable/bg_cw_edit.xml b/mastodon/src/main/res/drawable/bg_cw_edit.xml index 29e75582c..28a0e8cb9 100644 --- a/mastodon/src/main/res/drawable/bg_cw_edit.xml +++ b/mastodon/src/main/res/drawable/bg_cw_edit.xml @@ -1,22 +1,32 @@ - + - + + + + + + + + + + + - + - + - + \ No newline at end of file diff --git a/mastodon/src/main/res/drawable/bg_m3_outlined_text_field_error_nopad.xml b/mastodon/src/main/res/drawable/bg_m3_outlined_text_field_error_nopad.xml new file mode 100644 index 000000000..d3469005a --- /dev/null +++ b/mastodon/src/main/res/drawable/bg_m3_outlined_text_field_error_nopad.xml @@ -0,0 +1,9 @@ + + + + + + + + + \ No newline at end of file diff --git a/mastodon/src/main/res/drawable/bg_rect_4dp_ripple.xml b/mastodon/src/main/res/drawable/bg_rect_4dp_ripple.xml new file mode 100644 index 000000000..c0fc0686e --- /dev/null +++ b/mastodon/src/main/res/drawable/bg_rect_4dp_ripple.xml @@ -0,0 +1,9 @@ + + + + + + + + + \ No newline at end of file diff --git a/mastodon/src/main/res/drawable/bg_round_ripple.xml b/mastodon/src/main/res/drawable/bg_round_ripple.xml index efbfcb5c8..85aff75e3 100644 --- a/mastodon/src/main/res/drawable/bg_round_ripple.xml +++ b/mastodon/src/main/res/drawable/bg_round_ripple.xml @@ -1,5 +1,5 @@ - + diff --git a/mastodon/src/main/res/drawable/divider_vertical_variant_1dp.xml b/mastodon/src/main/res/drawable/divider_vertical_variant_1dp.xml new file mode 100644 index 000000000..943e5a094 --- /dev/null +++ b/mastodon/src/main/res/drawable/divider_vertical_variant_1dp.xml @@ -0,0 +1,5 @@ + + + + + \ No newline at end of file diff --git a/mastodon/src/main/res/drawable/fg_compose_attachment.xml b/mastodon/src/main/res/drawable/fg_compose_attachment.xml new file mode 100644 index 000000000..9e96de989 --- /dev/null +++ b/mastodon/src/main/res/drawable/fg_compose_attachment.xml @@ -0,0 +1,5 @@ + + + + + \ No newline at end of file diff --git a/mastodon/src/main/res/drawable/ic_add_photo_alternate_24px.xml b/mastodon/src/main/res/drawable/ic_add_photo_alternate_24px.xml new file mode 100644 index 000000000..e7f7a2d13 --- /dev/null +++ b/mastodon/src/main/res/drawable/ic_add_photo_alternate_24px.xml @@ -0,0 +1,9 @@ + + + diff --git a/mastodon/src/main/res/drawable/ic_alternate_email_20px.xml b/mastodon/src/main/res/drawable/ic_alternate_email_20px.xml new file mode 100644 index 000000000..9846ae685 --- /dev/null +++ b/mastodon/src/main/res/drawable/ic_alternate_email_20px.xml @@ -0,0 +1,9 @@ + + + diff --git a/mastodon/src/main/res/drawable/ic_compose_cw.xml b/mastodon/src/main/res/drawable/ic_compose_cw.xml new file mode 100644 index 000000000..48958d6bc --- /dev/null +++ b/mastodon/src/main/res/drawable/ic_compose_cw.xml @@ -0,0 +1,5 @@ + + + + + \ No newline at end of file diff --git a/mastodon/src/main/res/drawable/ic_compose_emoji.xml b/mastodon/src/main/res/drawable/ic_compose_emoji.xml new file mode 100644 index 000000000..fd423c918 --- /dev/null +++ b/mastodon/src/main/res/drawable/ic_compose_emoji.xml @@ -0,0 +1,5 @@ + + + + + \ No newline at end of file diff --git a/mastodon/src/main/res/drawable/ic_compose_poll.xml b/mastodon/src/main/res/drawable/ic_compose_poll.xml new file mode 100644 index 000000000..7aed17f47 --- /dev/null +++ b/mastodon/src/main/res/drawable/ic_compose_poll.xml @@ -0,0 +1,5 @@ + + + + + \ No newline at end of file diff --git a/mastodon/src/main/res/drawable/ic_drag_indicator_20px.xml b/mastodon/src/main/res/drawable/ic_drag_indicator_20px.xml new file mode 100644 index 000000000..1b7960aa8 --- /dev/null +++ b/mastodon/src/main/res/drawable/ic_drag_indicator_20px.xml @@ -0,0 +1,9 @@ + + + diff --git a/mastodon/src/main/res/drawable/ic_group_20px.xml b/mastodon/src/main/res/drawable/ic_group_20px.xml new file mode 100644 index 000000000..a9fe3ba78 --- /dev/null +++ b/mastodon/src/main/res/drawable/ic_group_20px.xml @@ -0,0 +1,9 @@ + + + diff --git a/mastodon/src/main/res/drawable/ic_help_24px.xml b/mastodon/src/main/res/drawable/ic_help_24px.xml new file mode 100644 index 000000000..89da8f73d --- /dev/null +++ b/mastodon/src/main/res/drawable/ic_help_24px.xml @@ -0,0 +1,9 @@ + + + diff --git a/mastodon/src/main/res/drawable/ic_insert_chart_24px.xml b/mastodon/src/main/res/drawable/ic_insert_chart_24px.xml new file mode 100644 index 000000000..72ef007db --- /dev/null +++ b/mastodon/src/main/res/drawable/ic_insert_chart_24px.xml @@ -0,0 +1,9 @@ + + + diff --git a/mastodon/src/main/res/drawable/ic_insert_chart_fill1_24px.xml b/mastodon/src/main/res/drawable/ic_insert_chart_fill1_24px.xml new file mode 100644 index 000000000..d9cf28ce4 --- /dev/null +++ b/mastodon/src/main/res/drawable/ic_insert_chart_fill1_24px.xml @@ -0,0 +1,9 @@ + + + diff --git a/mastodon/src/main/res/drawable/ic_mood_24px.xml b/mastodon/src/main/res/drawable/ic_mood_24px.xml new file mode 100644 index 000000000..d2f257618 --- /dev/null +++ b/mastodon/src/main/res/drawable/ic_mood_24px.xml @@ -0,0 +1,9 @@ + + + diff --git a/mastodon/src/main/res/drawable/ic_mood_fill1_24px.xml b/mastodon/src/main/res/drawable/ic_mood_fill1_24px.xml new file mode 100644 index 000000000..482ba44fc --- /dev/null +++ b/mastodon/src/main/res/drawable/ic_mood_fill1_24px.xml @@ -0,0 +1,9 @@ + + + diff --git a/mastodon/src/main/res/drawable/ic_public_20px.xml b/mastodon/src/main/res/drawable/ic_public_20px.xml new file mode 100644 index 000000000..68f0bcbcc --- /dev/null +++ b/mastodon/src/main/res/drawable/ic_public_20px.xml @@ -0,0 +1,9 @@ + + + diff --git a/mastodon/src/main/res/drawable/ic_restart_alt_24px.xml b/mastodon/src/main/res/drawable/ic_restart_alt_24px.xml new file mode 100644 index 000000000..f955eb41a --- /dev/null +++ b/mastodon/src/main/res/drawable/ic_restart_alt_24px.xml @@ -0,0 +1,9 @@ + + + diff --git a/mastodon/src/main/res/drawable/ic_send_24px.xml b/mastodon/src/main/res/drawable/ic_send_24px.xml new file mode 100644 index 000000000..a704c3fc1 --- /dev/null +++ b/mastodon/src/main/res/drawable/ic_send_24px.xml @@ -0,0 +1,9 @@ + + + diff --git a/mastodon/src/main/res/drawable/ic_warning_24px.xml b/mastodon/src/main/res/drawable/ic_warning_24px.xml new file mode 100644 index 000000000..c5e52a4c1 --- /dev/null +++ b/mastodon/src/main/res/drawable/ic_warning_24px.xml @@ -0,0 +1,9 @@ + + + diff --git a/mastodon/src/main/res/drawable/ic_warning_fill1_24px.xml b/mastodon/src/main/res/drawable/ic_warning_fill1_24px.xml new file mode 100644 index 000000000..7ceeae25d --- /dev/null +++ b/mastodon/src/main/res/drawable/ic_warning_fill1_24px.xml @@ -0,0 +1,9 @@ + + + diff --git a/mastodon/src/main/res/drawable/m3_progress.xml b/mastodon/src/main/res/drawable/m3_progress.xml new file mode 100644 index 000000000..2128be813 --- /dev/null +++ b/mastodon/src/main/res/drawable/m3_progress.xml @@ -0,0 +1,15 @@ + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/mastodon/src/main/res/drawable/poll_multiple.xml b/mastodon/src/main/res/drawable/poll_multiple.xml new file mode 100644 index 000000000..f7eb513d2 --- /dev/null +++ b/mastodon/src/main/res/drawable/poll_multiple.xml @@ -0,0 +1,61 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/mastodon/src/main/res/drawable/poll_single.xml b/mastodon/src/main/res/drawable/poll_single.xml new file mode 100644 index 000000000..1037aa70e --- /dev/null +++ b/mastodon/src/main/res/drawable/poll_single.xml @@ -0,0 +1,75 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/mastodon/src/main/res/layout/compose_media_thumb.xml b/mastodon/src/main/res/layout/compose_media_thumb.xml index a3432b1fc..631474a27 100644 --- a/mastodon/src/main/res/layout/compose_media_thumb.xml +++ b/mastodon/src/main/res/layout/compose_media_thumb.xml @@ -1,144 +1,92 @@ - + android:layout_width="match_parent" + android:layout_height="match_parent" + android:foreground="@drawable/fg_compose_attachment"> + + - - - - - - - - - - - + + + android:layout_height="20dp" + android:layout_above="@id/delete" + android:layout_marginLeft="16dp" + android:layout_marginRight="16dp" + android:textAppearance="@style/m3_body_medium" + android:singleLine="true" + android:ellipsize="end" + android:textColor="?colorM3OnSurfaceVariant" + android:gravity="center_vertical" + tools:text="Subtitle"/> + + - + - + - - - - - - - - - - - - - \ No newline at end of file + \ No newline at end of file diff --git a/mastodon/src/main/res/layout/compose_poll_option.xml b/mastodon/src/main/res/layout/compose_poll_option.xml index d806da362..62675c6f9 100644 --- a/mastodon/src/main/res/layout/compose_poll_option.xml +++ b/mastodon/src/main/res/layout/compose_poll_option.xml @@ -1,45 +1,39 @@ - - - - - + android:background="@null" + android:paddingStart="16dp" + android:paddingEnd="48dp" + android:textAppearance="@style/m3_body_large" + android:textColor="?colorM3OnSurface" + android:textColorHint="?colorM3OnSurfaceVariant" + android:inputType="textCapSentences" + android:elevation="0dp" + android:saveEnabled="false" + android:singleLine="true"/> + android:tint="?colorM3OnSurfaceVariant" + android:contentDescription="@string/reorder" + android:paddingStart="4dp" + android:src="@drawable/ic_drag_indicator_20px" + tools:ignore="RtlSymmetry" /> - \ No newline at end of file + \ No newline at end of file diff --git a/mastodon/src/main/res/layout/fragment_compose.xml b/mastodon/src/main/res/layout/fragment_compose.xml index 5b49b724e..8b61ab86e 100644 --- a/mastodon/src/main/res/layout/fragment_compose.xml +++ b/mastodon/src/main/res/layout/fragment_compose.xml @@ -1,6 +1,7 @@ @@ -12,24 +13,11 @@ android:fillViewport="true"> - + android:layout_marginEnd="12dp" + android:importantForAccessibility="no" + tools:src="#0f0"/> + +