diff --git a/mastodon/build.gradle b/mastodon/build.gradle index 60e8ba425..a7006cc63 100644 --- a/mastodon/build.gradle +++ b/mastodon/build.gradle @@ -9,8 +9,8 @@ android { applicationId "org.joinmastodon.android.sk" minSdk 23 targetSdk 33 - versionCode 77 - versionName "1.2.0+fork.77" + versionCode 78 + versionName "1.2.0+fork.78" testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner" resConfigs "ar-rSA", "be-rBY", "bn-rBD", "bs-rBA", "ca-rES", "cs-rCZ", "da-rDK", "de-rDE", "el-rGR", "es-rES", "eu-rES", "fa-rIR", "fi-rFI", "fil-rPH", "fr-rFR", "ga-rIE", "gd-rGB", "gl-rES", "hi-rIN", "hr-rHR", "hu-rHU", "hy-rAM", "ig-rNG", "in-rID", "is-rIS", "it-rIT", "iw-rIL", "ja-rJP", "kab", "ko-rKR", "my-rMM", "nl-rNL", "no-rNO", "oc-rFR", "pl-rPL", "pt-rBR", "pt-rPT", "ro-rRO", "ru-rRU", "si-rLK", "sl-rSI", "sv-rSE", "th-rTH", "tr-rTR", "uk-rUA", "vi-rVN", "zh-rCN", "zh-rTW" } diff --git a/mastodon/src/main/java/org/joinmastodon/android/GlobalUserPreferences.java b/mastodon/src/main/java/org/joinmastodon/android/GlobalUserPreferences.java index ce645c5c5..6115c1120 100644 --- a/mastodon/src/main/java/org/joinmastodon/android/GlobalUserPreferences.java +++ b/mastodon/src/main/java/org/joinmastodon/android/GlobalUserPreferences.java @@ -44,6 +44,8 @@ public class GlobalUserPreferences{ public static boolean collapseLongPosts; public static boolean spectatorMode; public static boolean autoHideFab; + public static boolean replyLineAboveHeader; + public static boolean compactReblogReplyLine; public static String publishButtonText; public static ThemePreference theme; public static ColorPreference color; @@ -93,6 +95,8 @@ public class GlobalUserPreferences{ collapseLongPosts=prefs.getBoolean("collapseLongPosts", true); spectatorMode=prefs.getBoolean("spectatorMode", false); autoHideFab=prefs.getBoolean("autoHideFab", true); + replyLineAboveHeader=prefs.getBoolean("replyLineAboveHeader", true); + compactReblogReplyLine=prefs.getBoolean("compactReblogReplyLine", true); publishButtonText=prefs.getString("publishButtonText", ""); theme=ThemePreference.values()[prefs.getInt("theme", 0)]; recentLanguages=fromJson(prefs.getString("recentLanguages", null), recentLanguagesType, new HashMap<>()); @@ -134,8 +138,10 @@ public class GlobalUserPreferences{ .putBoolean("collapseLongPosts", collapseLongPosts) .putBoolean("spectatorMode", spectatorMode) .putBoolean("autoHideFab", autoHideFab) + .putBoolean("compactReblogReplyLine", compactReblogReplyLine) .putString("publishButtonText", publishButtonText) .putBoolean("bottomEncoding", bottomEncoding) + .putBoolean("replyLineAboveHeader", replyLineAboveHeader) .putInt("theme", theme.ordinal()) .putString("color", color.name()) .putString("recentLanguages", gson.toJson(recentLanguages)) diff --git a/mastodon/src/main/java/org/joinmastodon/android/fragments/BaseStatusListFragment.java b/mastodon/src/main/java/org/joinmastodon/android/fragments/BaseStatusListFragment.java index 231356c24..1ff5a2d2e 100644 --- a/mastodon/src/main/java/org/joinmastodon/android/fragments/BaseStatusListFragment.java +++ b/mastodon/src/main/java/org/joinmastodon/android/fragments/BaseStatusListFragment.java @@ -13,13 +13,11 @@ import android.text.Layout; import android.text.StaticLayout; import android.text.TextPaint; import android.text.TextUtils; -import android.util.Log; import android.view.View; import android.view.ViewGroup; import android.view.WindowInsets; import android.view.animation.TranslateAnimation; import android.widget.ImageButton; -import android.widget.ImageButton; import android.widget.Toolbar; import org.joinmastodon.android.E; @@ -34,12 +32,10 @@ import org.joinmastodon.android.model.Poll; import org.joinmastodon.android.model.Relationship; import org.joinmastodon.android.model.Status; import org.joinmastodon.android.ui.BetterItemAnimator; -import org.joinmastodon.android.ui.PhotoLayoutHelper; -import org.joinmastodon.android.ui.TileGridLayoutManager; import org.joinmastodon.android.ui.displayitems.ExtendedFooterStatusDisplayItem; import org.joinmastodon.android.ui.displayitems.GapStatusDisplayItem; import org.joinmastodon.android.ui.displayitems.HeaderStatusDisplayItem; -import org.joinmastodon.android.ui.displayitems.ImageStatusDisplayItem; +import org.joinmastodon.android.ui.displayitems.MediaGridStatusDisplayItem; import org.joinmastodon.android.ui.displayitems.PollFooterStatusDisplayItem; import org.joinmastodon.android.ui.displayitems.PollOptionStatusDisplayItem; import org.joinmastodon.android.ui.displayitems.StatusDisplayItem; @@ -47,8 +43,10 @@ import org.joinmastodon.android.ui.displayitems.TextStatusDisplayItem; import org.joinmastodon.android.ui.displayitems.WarningFilteredStatusDisplayItem; import org.joinmastodon.android.ui.photoviewer.PhotoViewer; import org.joinmastodon.android.ui.photoviewer.PhotoViewerHost; +import org.joinmastodon.android.ui.utils.MediaAttachmentViewController; import org.joinmastodon.android.ui.utils.UiUtils; -import org.joinmastodon.android.ui.views.ImageAttachmentFrameLayout; +import org.joinmastodon.android.ui.views.MediaGridLayout; +import org.joinmastodon.android.utils.TypedObjectPool; import java.util.ArrayList; import java.util.Collections; @@ -59,7 +57,6 @@ import java.util.stream.Collectors; import androidx.annotation.NonNull; import androidx.annotation.Nullable; -import androidx.recyclerview.widget.GridLayoutManager; import androidx.recyclerview.widget.RecyclerView; import me.grishka.appkit.Nav; @@ -83,6 +80,7 @@ public abstract class BaseStatusListFragment exten protected HashMap knownAccounts=new HashMap<>(); protected HashMap relationships=new HashMap<>(); protected Rect tmpRect=new Rect(); + protected TypedObjectPool attachmentViewsPool=new TypedObjectPool<>(this::makeNewMediaAttachmentView); public BaseStatusListFragment(){ super(20); @@ -192,21 +190,21 @@ public abstract class BaseStatusListFragment exten } @Override - public void openPhotoViewer(String parentID, Status _status, int attachmentIndex){ - final Status status=_status.reblog!=null ? _status.reblog : _status; + public void openPhotoViewer(String parentID, Status _status, int attachmentIndex, MediaGridStatusDisplayItem.Holder gridHolder){ + final Status status=_status.getContentStatus(); currentPhotoViewer=new PhotoViewer(getActivity(), status.mediaAttachments, attachmentIndex, new PhotoViewer.Listener(){ - private ImageStatusDisplayItem.Holder transitioningHolder; + private MediaAttachmentViewController transitioningHolder; @Override public void setPhotoViewVisibility(int index, boolean visible){ - ImageStatusDisplayItem.Holder holder=findPhotoViewHolder(index); + MediaAttachmentViewController holder=findPhotoViewHolder(index); if(holder!=null) holder.photo.setAlpha(visible ? 1f : 0f); } @Override public boolean startPhotoViewTransition(int index, @NonNull Rect outRect, @NonNull int[] outCornerRadius){ - ImageStatusDisplayItem.Holder holder=findPhotoViewHolder(index); + MediaAttachmentViewController holder=findPhotoViewHolder(index); if(holder!=null){ transitioningHolder=holder; View view=transitioningHolder.photo; @@ -214,7 +212,8 @@ public abstract class BaseStatusListFragment exten view.getLocationOnScreen(pos); outRect.set(pos[0], pos[1], pos[0]+view.getWidth(), pos[1]+view.getHeight()); list.setClipChildren(false); - transitioningHolder.itemView.setElevation(1f); + gridHolder.setClipChildren(false); + transitioningHolder.view.setElevation(1f); return true; } return false; @@ -241,15 +240,16 @@ public abstract class BaseStatusListFragment exten view.setTranslationY(0f); view.setScaleX(1f); view.setScaleY(1f); - transitioningHolder.itemView.setElevation(0f); + transitioningHolder.view.setElevation(0f); if(list!=null) list.setClipChildren(true); + gridHolder.setClipChildren(true); transitioningHolder=null; } @Override public Drawable getPhotoViewCurrentDrawable(int index){ - ImageStatusDisplayItem.Holder holder=findPhotoViewHolder(index); + MediaAttachmentViewController holder=findPhotoViewHolder(index); if(holder!=null) return holder.photo.getDrawable(); return null; @@ -265,23 +265,8 @@ public abstract class BaseStatusListFragment exten requestPermissions(permissions, PhotoViewer.PERMISSION_REQUEST); } - private ImageStatusDisplayItem.Holder findPhotoViewHolder(int index){ - if(list==null) - return null; - int offset=0; - for(StatusDisplayItem item:displayItems){ - if(item.parentID.equals(parentID)){ - if(item instanceof ImageStatusDisplayItem){ - RecyclerView.ViewHolder holder=list.findViewHolderForAdapterPosition(getMainAdapterOffset()+offset+index); - if(holder instanceof ImageStatusDisplayItem.Holder imgHolder){ - return imgHolder; - } - return null; - } - } - offset++; - } - return null; + private MediaAttachmentViewController findPhotoViewHolder(int index){ + return gridHolder.getViewController(index); } }); } @@ -304,12 +289,11 @@ public abstract class BaseStatusListFragment exten 0, fab.getHeight() * 2); animate.setDuration(300); - animate.setFillAfter(true); fab.startAnimation(animate); fab.setVisibility(View.INVISIBLE); scrollDiff = 0; } else if (dy < 0 && fab.getVisibility() != View.VISIBLE) { - if (list.getChildLayoutPosition(list.getChildAt(0)) == 0 || scrollDiff > 400) { + if (list.getChildAt(0).getTop() == 0 || scrollDiff > 400) { fab.setVisibility(View.VISIBLE); TranslateAnimation animate = new TranslateAnimation( 0, @@ -317,7 +301,6 @@ public abstract class BaseStatusListFragment exten fab.getHeight() * 2, 0); animate.setDuration(300); - animate.setFillAfter(true); fab.startAnimation(animate); scrollDiff = 0; } else { @@ -368,31 +351,6 @@ public abstract class BaseStatusListFragment exten } } - @Override - protected RecyclerView.LayoutManager onCreateLayoutManager(){ - GridLayoutManager lm=new TileGridLayoutManager(getActivity(), 1000); - lm.setSpanSizeLookup(new GridLayoutManager.SpanSizeLookup(){ - @Override - public int getSpanSize(int position){ - position-=getMainAdapterOffset(); - if(position>=0 && position exten revealSpoiler(status, holder.getItemID()); } - public void onRevealSpoilerClick(ImageStatusDisplayItem.Holder holder){ + public void onRevealSpoilerClick(MediaGridStatusDisplayItem.Holder holder){ Status status=holder.getItem().status; revealSpoiler(status, holder.getItemID()); } @@ -559,13 +517,14 @@ public abstract class BaseStatusListFragment exten protected void updateImagesSpoilerState(Status status, String itemID){ ArrayList updatedPositions=new ArrayList<>(); - for(ImageStatusDisplayItem.Holder photo:(List)findAllHoldersOfType(itemID, ImageStatusDisplayItem.Holder.class)){ - photo.setRevealed(status.spoilerRevealed); - updatedPositions.add(photo.getAbsoluteAdapterPosition()-getMainAdapterOffset()); + MediaGridStatusDisplayItem.Holder mediaGrid=findHolderOfType(itemID, MediaGridStatusDisplayItem.Holder.class); + if(mediaGrid!=null){ + mediaGrid.setRevealed(status.spoilerRevealed); + updatedPositions.add(mediaGrid.getAbsoluteAdapterPosition()-getMainAdapterOffset()); } int i=0; for(StatusDisplayItem item:displayItems){ - if(itemID.equals(item.parentID) && item instanceof ImageStatusDisplayItem && !updatedPositions.contains(i)){ + if(itemID.equals(item.parentID) && item instanceof MediaGridStatusDisplayItem && !updatedPositions.contains(i)){ adapter.notifyItemChanged(i); } i++; @@ -708,6 +667,15 @@ public abstract class BaseStatusListFragment exten return UiUtils.pickAccountForCompose(getActivity(), accountID); } + private MediaAttachmentViewController makeNewMediaAttachmentView(MediaGridStatusDisplayItem.GridItemType type){ + return new MediaAttachmentViewController(getActivity(), type); + } + + public TypedObjectPool getAttachmentViewsPool(){ + return attachmentViewsPool; + } + + protected class DisplayItemsAdapter extends UsableRecyclerView.Adapter> implements ImageLoaderRecyclerAdapter{ public DisplayItemsAdapter(){ @@ -745,16 +713,6 @@ public abstract class BaseStatusListFragment exten public ImageLoaderRequest getImageRequest(int position, int image){ return displayItems.get(position).getImageRequest(image); } - -// @Override -// public void onViewDetachedFromWindow(@NonNull BindableViewHolder holder){ -// if(holder instanceof ImageLoaderViewHolder){ -// int count=holder.getItem().getImageCount(); -// for(int i=0;i exten for(int i=0;i imgHolder){ + if(holder instanceof MediaGridStatusDisplayItem.Holder imgHolder){ if(!imgHolder.getItem().status.spoilerRevealed && TextUtils.isEmpty(imgHolder.getItem().status.spoilerText)){ hiddenMediaPaint.setColor(0x80000000); - PhotoLayoutHelper.TiledLayoutResult.Tile tile=imgHolder.getItem().thisTile; - float hGap=tile.startCol>0 ? V.dp(1) : 0; - float vGap=tile.startRow>0 ? V.dp(1) : 0; - c.drawRect(child.getX()-hGap, child.getY()-vGap, child.getX()+child.getWidth(), child.getY()+child.getHeight(), hiddenMediaPaint); + c.drawRect(child.getX(), child.getY(), child.getX()+child.getWidth(), child.getY()+child.getHeight(), hiddenMediaPaint); } } } for(int i=0;i imgHolder){ + if(holder instanceof MediaGridStatusDisplayItem.Holder imgHolder){ if(!imgHolder.getItem().status.spoilerRevealed){ - PhotoLayoutHelper.TiledLayoutResult.Tile tile=imgHolder.getItem().thisTile; - if(tile.startCol==0 && tile.startRow==0 && TextUtils.isEmpty(imgHolder.getItem().status.spoilerText)){ + if(TextUtils.isEmpty(imgHolder.getItem().status.spoilerText)){ int listWidth=getListWidthForMediaLayout(); - int width=Math.min(listWidth, V.dp(ImageAttachmentFrameLayout.MAX_WIDTH)); + int width=Math.min(listWidth, V.dp(MediaGridLayout.MAX_WIDTH)); if(currentMediaHiddenLayoutsWidth!=width) rebuildMediaHiddenLayouts(width-V.dp(32)); c.save(); @@ -831,47 +785,6 @@ public abstract class BaseStatusListFragment exten } } - @Override - public void getItemOffsets(@NonNull Rect outRect, @NonNull View view, @NonNull RecyclerView parent, @NonNull RecyclerView.State state){ - RecyclerView.ViewHolder holder=parent.getChildViewHolder(view); - if(holder instanceof ImageStatusDisplayItem.Holder){ - int listWidth=getListWidthForMediaLayout(); - int width=Math.min(listWidth, V.dp(ImageAttachmentFrameLayout.MAX_WIDTH)); - PhotoLayoutHelper.TiledLayoutResult layout=((ImageStatusDisplayItem.Holder) holder).getItem().tiledLayout; - PhotoLayoutHelper.TiledLayoutResult.Tile tile=((ImageStatusDisplayItem.Holder) holder).getItem().thisTile; - if(tile.startCol+tile.colSpan1){ - outRect.bottom=-(Math.round(tile.height/1000f*width)-Math.round(layout.rowSizes[tile.startRow]/1000f*width)); - } - // ...and for its siblings, offset those on rows below first to the right where they belong - if(tile.startCol>0 && layout.tiles[0].rowSpan>1 && tile.startRow>layout.tiles[0].startRow){ - int xOffset=Math.round(layout.tiles[0].width/1000f*listWidth); - outRect.left=xOffset; - outRect.right=-xOffset; - } - - // If the width of the media block is smaller than that of the RecyclerView, offset the views horizontally to center them - if(listWidth>width){ - outRect.left+=(listWidth-V.dp(ImageAttachmentFrameLayout.MAX_WIDTH))/2; - if(tile.startCol>0){ - int spanOffset=0; - for(int i=0;i0 && !Character.isWhitespace(mainEditText.getText().charAt(start-1)) ? " :" : ":"; - mainEditText.getText().replace(start, mainEditText.getSelectionEnd(), prefix+emoji.shortcode+':'); + if(getActivity().getCurrentFocus() instanceof EditText edit){ + int start=edit.getSelectionStart(); + String prefix=start>0 && !Character.isWhitespace(edit.getText().charAt(start-1)) ? " :" : ":"; + edit.getText().replace(start, edit.getSelectionEnd(), prefix+emoji.shortcode+':'); + } } @Override diff --git a/mastodon/src/main/java/org/joinmastodon/android/fragments/HomeTabFragment.java b/mastodon/src/main/java/org/joinmastodon/android/fragments/HomeTabFragment.java index 37ce0e002..adf202bce 100644 --- a/mastodon/src/main/java/org/joinmastodon/android/fragments/HomeTabFragment.java +++ b/mastodon/src/main/java/org/joinmastodon/android/fragments/HomeTabFragment.java @@ -358,7 +358,7 @@ public class HomeTabFragment extends MastodonToolbarFragment implements Scrollab addListsToOverflowMenu(); addHashtagsToOverflowMenu(); - if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.P) { + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.P && !UiUtils.isEMUI()) { m.setGroupDividerEnabled(true); } } diff --git a/mastodon/src/main/java/org/joinmastodon/android/fragments/NotificationsListFragment.java b/mastodon/src/main/java/org/joinmastodon/android/fragments/NotificationsListFragment.java index dd31deeb9..a95f061af 100644 --- a/mastodon/src/main/java/org/joinmastodon/android/fragments/NotificationsListFragment.java +++ b/mastodon/src/main/java/org/joinmastodon/android/fragments/NotificationsListFragment.java @@ -19,9 +19,7 @@ import org.joinmastodon.android.model.Notification; import org.joinmastodon.android.model.PaginatedResponse; import org.joinmastodon.android.model.Status; import org.joinmastodon.android.ui.displayitems.AccountCardStatusDisplayItem; -import org.joinmastodon.android.ui.displayitems.AccountStatusDisplayItem; import org.joinmastodon.android.ui.displayitems.HeaderStatusDisplayItem; -import org.joinmastodon.android.ui.displayitems.ImageStatusDisplayItem; import org.joinmastodon.android.ui.displayitems.StatusDisplayItem; import org.joinmastodon.android.ui.displayitems.TextStatusDisplayItem; import org.joinmastodon.android.ui.utils.DiscoverInfoBannerHelper; @@ -40,7 +38,6 @@ import java.util.stream.Stream; import androidx.recyclerview.widget.RecyclerView; import me.grishka.appkit.Nav; import me.grishka.appkit.api.SimpleCallback; -import me.grishka.appkit.utils.V; public class NotificationsListFragment extends BaseStatusListFragment{ private boolean onlyMentions; @@ -97,14 +94,9 @@ public class NotificationsListFragment extends BaseStatusListFragment items=StatusDisplayItem.buildItems(this, n.status, accountID, n, knownAccounts, titleItem!=null, titleItem==null, n, false, Filter.FilterContext.NOTIFICATIONS, titleItem); - if(titleItem!=null){ - for(StatusDisplayItem item:items){ - if(item instanceof ImageStatusDisplayItem imgItem){ - imgItem.horizontalInset=V.dp(32); - } - } - } + ArrayList items=StatusDisplayItem.buildItems(this, n.status, accountID, n, knownAccounts, titleItem!=null, titleItem==null, n, false, Filter.FilterContext.NOTIFICATIONS); + if(titleItem!=null) + items.add(0, titleItem); return items; }else if(titleItem!=null){ AccountCardStatusDisplayItem card=new AccountCardStatusDisplayItem(n.id, this, @@ -125,6 +117,8 @@ public class NotificationsListFragment extends BaseStatusListFragment items=new ArrayList<>(); private ThemeItem themeItem; private NotificationPolicyItem notificationPolicyItem; - private SwitchItem showNewPostsButtonItem, glitchModeItem; + private SwitchItem showNewPostsButtonItem, glitchModeItem, compactReblogReplyLineItem; private String accountID; private boolean needUpdateNotificationSettings; private boolean needAppRestart; @@ -253,6 +253,21 @@ public class SettingsFragment extends MastodonToolbarFragment{ GlobalUserPreferences.save(); needAppRestart=true; })); + items.add(new SwitchItem(R.string.sk_reply_line_above_avatar, R.drawable.ic_fluent_arrow_reply_24_regular, GlobalUserPreferences.replyLineAboveHeader, i->{ + GlobalUserPreferences.replyLineAboveHeader=i.checked; + GlobalUserPreferences.compactReblogReplyLine=i.checked; + compactReblogReplyLineItem.enabled=i.checked; + compactReblogReplyLineItem.checked= GlobalUserPreferences.replyLineAboveHeader; + if (list.findViewHolderForAdapterPosition(items.indexOf(compactReblogReplyLineItem)) instanceof SwitchViewHolder svh) svh.rebind(); + GlobalUserPreferences.save(); + needAppRestart=true; + })); + items.add(compactReblogReplyLineItem=new SwitchItem(R.string.sk_compact_reblog_reply_line, R.drawable.ic_fluent_re_order_24_regular, GlobalUserPreferences.compactReblogReplyLine, i->{ + GlobalUserPreferences.compactReblogReplyLine=i.checked; + GlobalUserPreferences.save(); + needAppRestart=true; + })); + compactReblogReplyLineItem.enabled=GlobalUserPreferences.replyLineAboveHeader; items.add(new SwitchItem(R.string.sk_settings_translate_only_opened, R.drawable.ic_fluent_translate_24_regular, GlobalUserPreferences.translateButtonOpenedOnly, i->{ GlobalUserPreferences.translateButtonOpenedOnly=i.checked; GlobalUserPreferences.save(); diff --git a/mastodon/src/main/java/org/joinmastodon/android/fragments/StatusListFragment.java b/mastodon/src/main/java/org/joinmastodon/android/fragments/StatusListFragment.java index 7293a2fac..9bdd7b103 100644 --- a/mastodon/src/main/java/org/joinmastodon/android/fragments/StatusListFragment.java +++ b/mastodon/src/main/java/org/joinmastodon/android/fragments/StatusListFragment.java @@ -41,6 +41,8 @@ public abstract class StatusListFragment extends BaseStatusListFragment{ protected void addAccountToKnown(Status s){ if(!knownAccounts.containsKey(s.account.id)) knownAccounts.put(s.account.id, s.account); + if(s.reblog!=null && !knownAccounts.containsKey(s.reblog.account.id)) + knownAccounts.put(s.reblog.account.id, s.reblog.account); } @Override diff --git a/mastodon/src/main/java/org/joinmastodon/android/fragments/report/ReportAddPostsChoiceFragment.java b/mastodon/src/main/java/org/joinmastodon/android/fragments/report/ReportAddPostsChoiceFragment.java index f8c2fc050..c2a8dd2b3 100644 --- a/mastodon/src/main/java/org/joinmastodon/android/fragments/report/ReportAddPostsChoiceFragment.java +++ b/mastodon/src/main/java/org/joinmastodon/android/fragments/report/ReportAddPostsChoiceFragment.java @@ -22,10 +22,8 @@ import org.joinmastodon.android.events.FinishReportFragmentsEvent; import org.joinmastodon.android.fragments.StatusListFragment; import org.joinmastodon.android.model.Account; import org.joinmastodon.android.model.Status; -import org.joinmastodon.android.ui.PhotoLayoutHelper; import org.joinmastodon.android.ui.displayitems.AudioStatusDisplayItem; import org.joinmastodon.android.ui.displayitems.HeaderStatusDisplayItem; -import org.joinmastodon.android.ui.displayitems.ImageStatusDisplayItem; import org.joinmastodon.android.ui.displayitems.LinkCardStatusDisplayItem; import org.joinmastodon.android.ui.displayitems.ReblogOrReplyLineStatusDisplayItem; import org.joinmastodon.android.ui.displayitems.StatusDisplayItem; @@ -132,22 +130,7 @@ public class ReportAddPostsChoiceFragment extends StatusListFragment{ if(holder.getAbsoluteAdapterPosition()==0) return; outRect.left=V.dp(40); - if(holder instanceof ImageStatusDisplayItem.Holder imgHolder){ - PhotoLayoutHelper.TiledLayoutResult layout=imgHolder.getItem().tiledLayout; - PhotoLayoutHelper.TiledLayoutResult.Tile tile=imgHolder.getItem().thisTile; - String siblingID; - if(holder.getAbsoluteAdapterPosition()0) - outRect.left=0; - outRect.left+=V.dp(16); - outRect.right=V.dp(16); - if(!imgHolder.getItemID().equals(siblingID) || tile.startRow+tile.rowSpan==layout.rowSizes.length) - outRect.bottom=V.dp(16); - }else if(holder instanceof AudioStatusDisplayItem.Holder){ + if(holder instanceof AudioStatusDisplayItem.Holder){ outRect.bottom=V.dp(16); }else if(holder instanceof LinkCardStatusDisplayItem.Holder){ outRect.bottom=V.dp(16); @@ -166,10 +149,6 @@ public class ReportAddPostsChoiceFragment extends StatusListFragment{ parent.getDecoratedBoundsWithMargins(child, tmpRect); String id=sdiHolder.getItemID(); int height=tmpRect.height(); - if(holder instanceof ImageStatusDisplayItem.Holder imgHolder){ - if(imgHolder.getItem().thisTile.startCol+imgHolder.getItem().thisTile.colSpan buildDisplayItems(Status s){ - List items=StatusDisplayItem.buildItems(this, s, accountID, s, knownAccounts, true, false, null); - for(StatusDisplayItem item:items){ - if(item instanceof ImageStatusDisplayItem isdi){ - isdi.horizontalInset=V.dp(40+32); - } - } - return items; - } - protected void drawDivider(View child, View bottomSibling, RecyclerView.ViewHolder holder, RecyclerView.ViewHolder siblingHolder, RecyclerView parent, Canvas c, Paint paint){ parent.getDecoratedBoundsWithMargins(child, tmpRect); tmpRect.offset(0, Math.round(child.getTranslationY())); diff --git a/mastodon/src/main/java/org/joinmastodon/android/model/Poll.java b/mastodon/src/main/java/org/joinmastodon/android/model/Poll.java index b07e501d5..5aeeb9e54 100644 --- a/mastodon/src/main/java/org/joinmastodon/android/model/Poll.java +++ b/mastodon/src/main/java/org/joinmastodon/android/model/Poll.java @@ -16,6 +16,7 @@ public class Poll extends BaseModel{ private boolean expired; public boolean multiple; public int votersCount; + public int votesCount; public boolean voted; @RequiredField public List ownVotes; @@ -41,10 +42,12 @@ public class Poll extends BaseModel{ ", expired="+expired+ ", multiple="+multiple+ ", votersCount="+votersCount+ + ", votesCount="+votesCount+ ", voted="+voted+ ", ownVotes="+ownVotes+ ", options="+options+ ", emojis="+emojis+ + ", selectedOptions="+selectedOptions+ '}'; } diff --git a/mastodon/src/main/java/org/joinmastodon/android/ui/PhotoLayoutHelper.java b/mastodon/src/main/java/org/joinmastodon/android/ui/PhotoLayoutHelper.java index 9f1a653a5..fe3accc4f 100644 --- a/mastodon/src/main/java/org/joinmastodon/android/ui/PhotoLayoutHelper.java +++ b/mastodon/src/main/java/org/joinmastodon/android/ui/PhotoLayoutHelper.java @@ -11,8 +11,14 @@ import java.util.List; import androidx.annotation.NonNull; public class PhotoLayoutHelper{ + public static final int MAX_WIDTH=1000; + public static final int MAX_HEIGHT=1910; + @NonNull - public static TiledLayoutResult processThumbs(int _maxW, int _maxH, List thumbs){ + public static TiledLayoutResult processThumbs(List thumbs){ + int _maxW=MAX_WIDTH; + int _maxH=MAX_HEIGHT; + TiledLayoutResult result=new TiledLayoutResult(); if(thumbs.size()==1){ Attachment att=thumbs.get(0); @@ -45,13 +51,8 @@ public class PhotoLayoutHelper{ float avgRatio=!ratios.isEmpty() ? sum(ratios)/ratios.size() : 1.0f; float maxW, maxH, marginW=0, marginH=0; - if(_maxW>0){ - maxW=_maxW; - maxH=_maxH; - }else{ - maxW=510; - maxH=510; - } + maxW=_maxW; + maxH=_maxH; float maxRatio=maxW/maxH; diff --git a/mastodon/src/main/java/org/joinmastodon/android/ui/displayitems/GifVStatusDisplayItem.java b/mastodon/src/main/java/org/joinmastodon/android/ui/displayitems/GifVStatusDisplayItem.java deleted file mode 100644 index 8461121b8..000000000 --- a/mastodon/src/main/java/org/joinmastodon/android/ui/displayitems/GifVStatusDisplayItem.java +++ /dev/null @@ -1,42 +0,0 @@ -package org.joinmastodon.android.ui.displayitems; - -import android.app.Activity; -import android.graphics.Outline; -import android.view.View; -import android.view.ViewGroup; -import android.view.ViewOutlineProvider; - -import org.joinmastodon.android.R; -import org.joinmastodon.android.fragments.BaseStatusListFragment; -import org.joinmastodon.android.model.Attachment; -import org.joinmastodon.android.model.Status; -import org.joinmastodon.android.ui.PhotoLayoutHelper; - -import me.grishka.appkit.imageloader.requests.UrlImageLoaderRequest; - -public class GifVStatusDisplayItem extends ImageStatusDisplayItem{ - public GifVStatusDisplayItem(String parentID, Status status, Attachment attachment, BaseStatusListFragment parentFragment, int index, int totalPhotos, PhotoLayoutHelper.TiledLayoutResult tiledLayout, PhotoLayoutHelper.TiledLayoutResult.Tile thisTile){ - super(parentID, parentFragment, attachment, status, index, totalPhotos, tiledLayout, thisTile); - request=new UrlImageLoaderRequest(attachment.previewUrl, 1000, 1000); - } - - @Override - public Type getType(){ - return Type.GIFV; - } - - public static class Holder extends ImageStatusDisplayItem.Holder{ - - public Holder(Activity activity, ViewGroup parent){ - super(activity, R.layout.display_item_gifv, parent); - View play=findViewById(R.id.play_button); - play.setOutlineProvider(new ViewOutlineProvider(){ - @Override - public void getOutline(View view, Outline outline){ - outline.setOval(0, 0, view.getWidth(), view.getHeight()); - outline.setAlpha(.99f); // fixes shadow rendering - } - }); - } - } -} diff --git a/mastodon/src/main/java/org/joinmastodon/android/ui/displayitems/ImageStatusDisplayItem.java b/mastodon/src/main/java/org/joinmastodon/android/ui/displayitems/ImageStatusDisplayItem.java deleted file mode 100644 index b76cb997b..000000000 --- a/mastodon/src/main/java/org/joinmastodon/android/ui/displayitems/ImageStatusDisplayItem.java +++ /dev/null @@ -1,244 +0,0 @@ -package org.joinmastodon.android.ui.displayitems; - -import android.animation.Animator; -import android.animation.AnimatorListenerAdapter; -import android.animation.AnimatorSet; -import android.animation.ObjectAnimator; -import android.app.Activity; -import android.graphics.drawable.Drawable; -import android.text.TextUtils; -import android.view.View; -import android.view.ViewGroup; -import android.view.ViewTreeObserver; -import android.widget.FrameLayout; -import android.widget.ImageButton; -import android.widget.ImageView; -import android.widget.TextView; - -import org.joinmastodon.android.GlobalUserPreferences; -import org.joinmastodon.android.R; -import org.joinmastodon.android.fragments.BaseStatusListFragment; -import org.joinmastodon.android.model.Attachment; -import org.joinmastodon.android.model.Status; -import org.joinmastodon.android.ui.PhotoLayoutHelper; -import org.joinmastodon.android.ui.drawables.BlurhashCrossfadeDrawable; -import org.joinmastodon.android.ui.photoviewer.PhotoViewerHost; -import org.joinmastodon.android.ui.views.ImageAttachmentFrameLayout; - -import androidx.annotation.LayoutRes; -import me.grishka.appkit.imageloader.ImageLoaderViewHolder; -import me.grishka.appkit.imageloader.requests.ImageLoaderRequest; -import me.grishka.appkit.utils.CubicBezierInterpolator; - -public abstract class ImageStatusDisplayItem extends StatusDisplayItem{ - public final int index; - public final int totalPhotos; - protected Attachment attachment; - protected ImageLoaderRequest request; - public final Status status; - public final PhotoLayoutHelper.TiledLayoutResult tiledLayout; - public final PhotoLayoutHelper.TiledLayoutResult.Tile thisTile; - public int horizontalInset; - - public ImageStatusDisplayItem(String parentID, BaseStatusListFragment parentFragment, Attachment photo, Status status, int index, int totalPhotos, PhotoLayoutHelper.TiledLayoutResult tiledLayout, PhotoLayoutHelper.TiledLayoutResult.Tile thisTile){ - super(parentID, parentFragment); - this.attachment=photo; - this.status=status; - this.index=index; - this.totalPhotos=totalPhotos; - this.tiledLayout=tiledLayout; - this.thisTile=thisTile; - } - - @Override - public int getImageCount(){ - return 1; - } - - @Override - public ImageLoaderRequest getImageRequest(int index){ - return request; - } - - public static abstract class Holder extends StatusDisplayItem.Holder implements ImageLoaderViewHolder{ - public final ImageView photo; - private ImageAttachmentFrameLayout layout; - private BlurhashCrossfadeDrawable crossfadeDrawable=new BlurhashCrossfadeDrawable(); - private boolean didClear; - - private AnimatorSet currentAnim; - private final FrameLayout altTextWrapper; - private final TextView altTextButton; - private final ImageView noAltTextButton; - private final View altTextScroller; - private final ImageButton altTextClose; - private final TextView altText, noAltText; - - private View altOrNoAltButton; - private boolean altTextShown; - - public Holder(Activity activity, @LayoutRes int layout, ViewGroup parent){ - super(activity, layout, parent); - photo=findViewById(R.id.photo); - photo.setOnClickListener(this::onViewClick); - this.layout=(ImageAttachmentFrameLayout)itemView; - - altTextWrapper=findViewById(R.id.alt_text_wrapper); - altTextButton=findViewById(R.id.alt_button); - noAltTextButton=findViewById(R.id.no_alt_button); - altTextScroller=findViewById(R.id.alt_text_scroller); - altTextClose=findViewById(R.id.alt_text_close); - altText=findViewById(R.id.alt_text); - noAltText=findViewById(R.id.no_alt_text); - - altTextButton.setOnClickListener(this::onShowHideClick); - noAltTextButton.setOnClickListener(this::onShowHideClick); - altTextClose.setOnClickListener(this::onShowHideClick); -// altTextScroller.setNestedScrollingEnabled(true); - } - - @Override - public void onBind(ImageStatusDisplayItem item){ - layout.setLayout(item.tiledLayout, item.thisTile, item.horizontalInset); - crossfadeDrawable.setSize(item.attachment.getWidth(), item.attachment.getHeight()); - crossfadeDrawable.setBlurhashDrawable(item.attachment.blurhashPlaceholder); - crossfadeDrawable.setCrossfadeAlpha(item.status.spoilerRevealed ? 0f : 1f); - photo.setImageDrawable(null); - photo.setImageDrawable(crossfadeDrawable); - photo.setContentDescription(TextUtils.isEmpty(item.attachment.description) ? item.parentFragment.getString(R.string.media_no_description) : item.attachment.description); - didClear=false; - - if (currentAnim != null) currentAnim.cancel(); - - boolean altTextMissing = TextUtils.isEmpty(item.attachment.description); - altOrNoAltButton = altTextMissing ? noAltTextButton : altTextButton; - altTextShown=false; - - altTextScroller.setVisibility(View.GONE); - altTextClose.setVisibility(View.GONE); - altTextButton.setVisibility(View.VISIBLE); - noAltTextButton.setVisibility(View.VISIBLE); - altTextButton.setAlpha(1f); - noAltTextButton.setAlpha(1f); - altTextWrapper.setVisibility(View.VISIBLE); - - if (altTextMissing){ - if (GlobalUserPreferences.showNoAltIndicator) { - noAltTextButton.setVisibility(View.VISIBLE); - noAltText.setVisibility(View.VISIBLE); - altTextWrapper.setBackgroundResource(R.drawable.bg_image_no_alt_overlay); - altTextButton.setVisibility(View.GONE); - altText.setVisibility(View.GONE); - } else { - altTextWrapper.setVisibility(View.GONE); - } - }else{ - if (GlobalUserPreferences.showAltIndicator) { - noAltTextButton.setVisibility(View.GONE); - noAltText.setVisibility(View.GONE); - altTextWrapper.setBackgroundResource(R.drawable.bg_image_alt_overlay); - altTextButton.setVisibility(View.VISIBLE); - altTextButton.setText(R.string.sk_alt_button); - altText.setVisibility(View.VISIBLE); - altText.setText(item.attachment.description); - altText.setPadding(0, 0, 0, 0); - } else { - altTextWrapper.setVisibility(View.GONE); - } - } - } - - private void onShowHideClick(View v){ - boolean show=v.getId()==R.id.alt_button || v.getId()==R.id.no_alt_button; - - if(altTextShown==show) - return; - if(currentAnim!=null) - currentAnim.cancel(); - - altTextShown=show; - if(show){ - altTextScroller.setVisibility(View.VISIBLE); - altTextClose.setVisibility(View.VISIBLE); - }else{ - altOrNoAltButton.setVisibility(View.VISIBLE); - // Hide these views temporarily so FrameLayout measures correctly - altTextScroller.setVisibility(View.GONE); - altTextClose.setVisibility(View.GONE); - } - - // This is the current size... - int prevLeft=altTextWrapper.getLeft(); - int prevRight=altTextWrapper.getRight(); - int prevTop=altTextWrapper.getTop(); - altTextWrapper.getViewTreeObserver().addOnPreDrawListener(new ViewTreeObserver.OnPreDrawListener(){ - @Override - public boolean onPreDraw(){ - altTextWrapper.getViewTreeObserver().removeOnPreDrawListener(this); - - // ...and this is after the layout pass, right now the FrameLayout has its final size, but we animate that change - if(!show){ - // Show these views again so they're visible for the duration of the animation. - // No one would notice they were missing during measure/layout. - altTextScroller.setVisibility(View.VISIBLE); - altTextClose.setVisibility(View.VISIBLE); - } - AnimatorSet set=new AnimatorSet(); - set.playTogether( - ObjectAnimator.ofInt(altTextWrapper, "left", prevLeft, altTextWrapper.getLeft()), - ObjectAnimator.ofInt(altTextWrapper, "right", prevRight, altTextWrapper.getRight()), - ObjectAnimator.ofInt(altTextWrapper, "top", prevTop, altTextWrapper.getTop()), - ObjectAnimator.ofFloat(altOrNoAltButton, View.ALPHA, show ? 1f : 0f, show ? 0f : 1f), - ObjectAnimator.ofFloat(altTextScroller, View.ALPHA, show ? 0f : 1f, show ? 1f : 0f), - ObjectAnimator.ofFloat(altTextClose, View.ALPHA, show ? 0f : 1f, show ? 1f : 0f) - ); - set.setDuration(300); - set.setInterpolator(CubicBezierInterpolator.DEFAULT); - set.addListener(new AnimatorListenerAdapter(){ - @Override - public void onAnimationEnd(Animator animation){ - if(show){ - altOrNoAltButton.setVisibility(View.GONE); - }else{ - altTextScroller.setVisibility(View.GONE); - altTextClose.setVisibility(View.GONE); - } - currentAnim=null; - } - }); - set.start(); - currentAnim=set; - - return true; - } - }); - } - - @Override - public void setImage(int index, Drawable drawable){ - crossfadeDrawable.setImageDrawable(drawable); - if(didClear && item.status.spoilerRevealed) - crossfadeDrawable.animateAlpha(0f); - } - - @Override - public void clearImage(int index){ - crossfadeDrawable.setCrossfadeAlpha(1f); - crossfadeDrawable.setImageDrawable(null); - didClear=true; - } - - private void onViewClick(View v){ - if(!item.status.spoilerRevealed){ - item.parentFragment.onRevealSpoilerClick(this); - }else if(item.parentFragment instanceof PhotoViewerHost){ - Status contentStatus=item.status.reblog!=null ? item.status.reblog : item.status; - ((PhotoViewerHost) item.parentFragment).openPhotoViewer(item.parentID, item.status, contentStatus.mediaAttachments.indexOf(item.attachment)); - } - } - - public void setRevealed(boolean revealed){ - crossfadeDrawable.animateAlpha(revealed ? 0f : 1f); - } - } -} diff --git a/mastodon/src/main/java/org/joinmastodon/android/ui/displayitems/MediaGridStatusDisplayItem.java b/mastodon/src/main/java/org/joinmastodon/android/ui/displayitems/MediaGridStatusDisplayItem.java new file mode 100644 index 000000000..f2b3e3ab1 --- /dev/null +++ b/mastodon/src/main/java/org/joinmastodon/android/ui/displayitems/MediaGridStatusDisplayItem.java @@ -0,0 +1,311 @@ +package org.joinmastodon.android.ui.displayitems; + +import static org.joinmastodon.android.ui.utils.MediaAttachmentViewController.altWrapPadding; + +import android.animation.Animator; +import android.animation.AnimatorListenerAdapter; +import android.animation.AnimatorSet; +import android.animation.ObjectAnimator; +import android.app.Activity; +import android.graphics.drawable.Drawable; +import android.text.TextUtils; +import android.view.View; +import android.view.ViewGroup; +import android.view.ViewTreeObserver; +import android.widget.FrameLayout; +import android.widget.ImageButton; +import android.widget.ImageView; +import android.widget.TextView; + +import org.joinmastodon.android.GlobalUserPreferences; +import org.joinmastodon.android.R; +import org.joinmastodon.android.fragments.BaseStatusListFragment; +import org.joinmastodon.android.model.Attachment; +import org.joinmastodon.android.model.Status; +import org.joinmastodon.android.ui.PhotoLayoutHelper; +import org.joinmastodon.android.ui.photoviewer.PhotoViewerHost; +import org.joinmastodon.android.ui.utils.MediaAttachmentViewController; +import org.joinmastodon.android.ui.views.FrameLayoutThatOnlyMeasuresFirstChild; +import org.joinmastodon.android.ui.views.MediaGridLayout; +import org.joinmastodon.android.utils.TypedObjectPool; + +import java.util.ArrayList; +import java.util.List; + +import me.grishka.appkit.imageloader.ImageLoaderViewHolder; +import me.grishka.appkit.imageloader.requests.ImageLoaderRequest; +import me.grishka.appkit.imageloader.requests.UrlImageLoaderRequest; +import me.grishka.appkit.utils.CubicBezierInterpolator; + +public class MediaGridStatusDisplayItem extends StatusDisplayItem{ + private static final String TAG="MediaGridDisplayItem"; + + private final PhotoLayoutHelper.TiledLayoutResult tiledLayout; + private final TypedObjectPool viewPool; + private final List attachments; + private final ArrayList requests=new ArrayList<>(); + public final Status status; + + public MediaGridStatusDisplayItem(String parentID, BaseStatusListFragment parentFragment, PhotoLayoutHelper.TiledLayoutResult tiledLayout, List attachments, Status status){ + super(parentID, parentFragment); + this.tiledLayout=tiledLayout; + this.viewPool=parentFragment.getAttachmentViewsPool(); + this.attachments=attachments; + this.status=status; + for(Attachment att:attachments){ + requests.add(new UrlImageLoaderRequest(switch(att.type){ + case IMAGE -> att.url; + case VIDEO, GIFV -> att.previewUrl; + default -> throw new IllegalStateException("Unexpected value: "+att.type); + }, 1000, 1000)); + } + } + + @Override + public Type getType(){ + return Type.MEDIA_GRID; + } + + @Override + public int getImageCount(){ + return requests.size(); + } + + @Override + public ImageLoaderRequest getImageRequest(int index){ + return requests.get(index); + } + + public enum GridItemType{ + PHOTO, + VIDEO, + GIFV + } + + public static class Holder extends StatusDisplayItem.Holder implements ImageLoaderViewHolder{ + private final FrameLayout wrapper; + private final MediaGridLayout layout; + private final View.OnClickListener clickListener=this::onViewClick, altTextClickListener=this::onAltTextClick; + private final ArrayList controllers=new ArrayList<>(); + + private final FrameLayout altTextWrapper; + private final TextView altTextButton; + private final ImageView noAltTextButton; + private final View altTextScroller; + private final ImageButton altTextClose; + private final TextView altText, noAltText; + + private int altTextIndex=-1; + private Animator altTextAnimator; + + public Holder(Activity activity, ViewGroup parent){ + super(new FrameLayoutThatOnlyMeasuresFirstChild(activity)); + wrapper=(FrameLayout)itemView; + layout=new MediaGridLayout(activity); + wrapper.addView(layout); + + activity.getLayoutInflater().inflate(R.layout.overlay_image_alt_text, wrapper); + altTextWrapper=findViewById(R.id.alt_text_wrapper); + altTextButton=findViewById(R.id.alt_button); + noAltTextButton=findViewById(R.id.no_alt_button); + altTextScroller=findViewById(R.id.alt_text_scroller); + altTextClose=findViewById(R.id.alt_text_close); + altText=findViewById(R.id.alt_text); + noAltText=findViewById(R.id.no_alt_text); + altTextClose.setOnClickListener(this::onAltTextCloseClick); + } + + @Override + public void onBind(MediaGridStatusDisplayItem item){ + if(altTextAnimator!=null) + altTextAnimator.cancel(); + + layout.setTiledLayout(item.tiledLayout); + for(MediaAttachmentViewController c:controllers){ + item.viewPool.reuse(c.type, c); + } + layout.removeAllViews(); + controllers.clear(); + int i=0; + for(Attachment att:item.attachments){ + MediaAttachmentViewController c=item.viewPool.obtain(switch(att.type){ + case IMAGE -> GridItemType.PHOTO; + case VIDEO -> GridItemType.VIDEO; + case GIFV -> GridItemType.GIFV; + default -> throw new IllegalStateException("Unexpected value: "+att.type); + }); + if(c.view.getLayoutParams()==null) + c.view.setLayoutParams(new MediaGridLayout.LayoutParams(item.tiledLayout.tiles[i])); + else + ((MediaGridLayout.LayoutParams) c.view.getLayoutParams()).tile=item.tiledLayout.tiles[i]; + layout.addView(c.view); + c.view.setOnClickListener(clickListener); + c.view.setTag(i); + if(c.btnsWrap!=null){ + c.btnsWrap.setOnClickListener(altTextClickListener); + c.btnsWrap.setTag(i); + c.btnsWrap.setAlpha(1f); + } + controllers.add(c); + c.bind(att, item.status); + i++; + } + altTextButton.setVisibility(View.VISIBLE); + noAltTextButton.setVisibility(View.VISIBLE); + altTextWrapper.setVisibility(View.GONE); + altTextIndex=-1; + } + + @Override + public void setImage(int index, Drawable drawable){ + controllers.get(index).setImage(drawable); + } + + @Override + public void clearImage(int index){ + controllers.get(index).clearImage(); + } + + private void onViewClick(View v){ + int index=(Integer)v.getTag(); + if(!item.status.spoilerRevealed){ + item.parentFragment.onRevealSpoilerClick(this); + }else if(item.parentFragment instanceof PhotoViewerHost){ + ((PhotoViewerHost) item.parentFragment).openPhotoViewer(item.parentID, item.status, index, this); + } + } + + private void onAltTextClick(View v){ + if(altTextAnimator!=null) + altTextAnimator.cancel(); + v.setVisibility(View.INVISIBLE); + int index=(Integer)v.getTag(); + altTextIndex=index; + Attachment att=item.attachments.get(index); + boolean hasAltText = !TextUtils.isEmpty(att.description); + altTextButton.setVisibility(hasAltText && GlobalUserPreferences.showAltIndicator ? View.VISIBLE : View.GONE); + noAltTextButton.setVisibility(!hasAltText && GlobalUserPreferences.showNoAltIndicator ? View.VISIBLE : View.GONE); + altText.setVisibility(hasAltText && GlobalUserPreferences.showAltIndicator ? View.VISIBLE : View.GONE); + noAltText.setVisibility(!hasAltText && GlobalUserPreferences.showNoAltIndicator ? View.VISIBLE : View.GONE); + altText.setText(att.description); + altTextWrapper.setVisibility(View.VISIBLE); + altTextWrapper.setBackgroundResource(hasAltText ? R.drawable.bg_image_alt_overlay : R.drawable.bg_image_no_alt_overlay); + altTextWrapper.getViewTreeObserver().addOnPreDrawListener(new ViewTreeObserver.OnPreDrawListener(){ + @Override + public boolean onPreDraw(){ + altTextWrapper.getViewTreeObserver().removeOnPreDrawListener(this); + + int[] loc={0, 0}; + v.getLocationInWindow(loc); + int btnL=loc[0], btnT=loc[1]; + wrapper.getLocationInWindow(loc); + btnL-=loc[0]; + btnT-=loc[1]; + + ArrayList anims=new ArrayList<>(); + anims.add(ObjectAnimator.ofFloat(altTextButton, View.ALPHA, 1, 0)); + anims.add(ObjectAnimator.ofFloat(noAltTextButton, View.ALPHA, 1, 0)); + anims.add(ObjectAnimator.ofFloat(altTextScroller, View.ALPHA, 0, 1)); + anims.add(ObjectAnimator.ofFloat(altTextClose, View.ALPHA, 0, 1)); + anims.add(ObjectAnimator.ofInt(altTextWrapper, "left", btnL+altWrapPadding[0], altTextWrapper.getLeft())); + anims.add(ObjectAnimator.ofInt(altTextWrapper, "top", btnT+altWrapPadding[1], altTextWrapper.getTop())); + anims.add(ObjectAnimator.ofInt(altTextWrapper, "right", btnL+v.getWidth()-altWrapPadding[2], altTextWrapper.getRight())); + anims.add(ObjectAnimator.ofInt(altTextWrapper, "bottom", btnT+v.getHeight()-altWrapPadding[3], altTextWrapper.getBottom())); + for(Animator a:anims) + a.setDuration(300); + + for(MediaAttachmentViewController c:controllers){ + if(c.btnsWrap!=null && c.btnsWrap!=v){ + anims.add(ObjectAnimator.ofFloat(c.btnsWrap, View.ALPHA, 1, 0).setDuration(150)); + } + } + + AnimatorSet set=new AnimatorSet(); + set.playTogether(anims); + set.setInterpolator(CubicBezierInterpolator.DEFAULT); + set.addListener(new AnimatorListenerAdapter(){ + @Override + public void onAnimationEnd(Animator animation){ + altTextAnimator=null; + for(MediaAttachmentViewController c:controllers){ + if(c.btnsWrap!=null){ + c.btnsWrap.setVisibility(View.INVISIBLE); + } + } + } + }); + altTextAnimator=set; + set.start(); + + return true; + } + }); + } + + private void onAltTextCloseClick(View v){ + if(altTextAnimator!=null) + altTextAnimator.cancel(); + + View btn=controllers.get(altTextIndex).btnsWrap; + for(MediaAttachmentViewController c:controllers){ + if(c.btnsWrap!=null && c.btnsWrap!=btn) { + c.btnsWrap.setVisibility(View.VISIBLE); + } + } + + int[] loc={0, 0}; + btn.getLocationInWindow(loc); + int btnL=loc[0], btnT=loc[1]; + wrapper.getLocationInWindow(loc); + btnL-=loc[0]; + btnT-=loc[1]; + + ArrayList anims=new ArrayList<>(); + anims.add(ObjectAnimator.ofFloat(altTextButton, View.ALPHA, 1)); + anims.add(ObjectAnimator.ofFloat(noAltTextButton, View.ALPHA, 1)); + anims.add(ObjectAnimator.ofFloat(altTextScroller, View.ALPHA, 0)); + anims.add(ObjectAnimator.ofFloat(altTextClose, View.ALPHA, 0)); + anims.add(ObjectAnimator.ofInt(altTextWrapper, "left", btnL+altWrapPadding[0])); + anims.add(ObjectAnimator.ofInt(altTextWrapper, "top", btnT+altWrapPadding[1])); + anims.add(ObjectAnimator.ofInt(altTextWrapper, "right", btnL+btn.getWidth()-altWrapPadding[2])); + anims.add(ObjectAnimator.ofInt(altTextWrapper, "bottom", btnT+btn.getHeight()-altWrapPadding[3])); + for(Animator a:anims) + a.setDuration(300); + + for(MediaAttachmentViewController c:controllers){ +// if(c.btnsWrap!=null && c.btnsWrap!=btn){ + anims.add(ObjectAnimator.ofFloat(c.btnsWrap, View.ALPHA, 1).setDuration(150)); +// } + } + + AnimatorSet set=new AnimatorSet(); + set.playTogether(anims); + set.setInterpolator(CubicBezierInterpolator.DEFAULT); + set.addListener(new AnimatorListenerAdapter(){ + @Override + public void onAnimationEnd(Animator animation){ + altTextAnimator=null; + altTextWrapper.setVisibility(View.GONE); + btn.setVisibility(View.VISIBLE); + } + }); + altTextAnimator=set; + set.start(); + } + + public void setRevealed(boolean revealed){ + for(MediaAttachmentViewController c:controllers){ + c.setRevealed(revealed); + } + } + + public MediaAttachmentViewController getViewController(int index){ + return controllers.get(index); + } + + public void setClipChildren(boolean clip){ + layout.setClipChildren(clip); + wrapper.setClipChildren(clip); + } + } +} diff --git a/mastodon/src/main/java/org/joinmastodon/android/ui/displayitems/PhotoStatusDisplayItem.java b/mastodon/src/main/java/org/joinmastodon/android/ui/displayitems/PhotoStatusDisplayItem.java deleted file mode 100644 index a0609d10c..000000000 --- a/mastodon/src/main/java/org/joinmastodon/android/ui/displayitems/PhotoStatusDisplayItem.java +++ /dev/null @@ -1,45 +0,0 @@ -package org.joinmastodon.android.ui.displayitems; - -import android.animation.Animator; -import android.animation.AnimatorListenerAdapter; -import android.animation.AnimatorSet; -import android.animation.ObjectAnimator; -import android.app.Activity; -import android.text.TextUtils; -import android.view.View; -import android.view.ViewGroup; -import android.view.ViewTreeObserver; -import android.widget.FrameLayout; -import android.widget.ImageButton; -import android.widget.ImageView; -import android.widget.ScrollView; -import android.widget.TextView; - -import org.joinmastodon.android.GlobalUserPreferences; -import org.joinmastodon.android.R; -import org.joinmastodon.android.fragments.BaseStatusListFragment; -import org.joinmastodon.android.model.Attachment; -import org.joinmastodon.android.model.Status; -import org.joinmastodon.android.ui.PhotoLayoutHelper; - -import me.grishka.appkit.imageloader.requests.UrlImageLoaderRequest; -import me.grishka.appkit.utils.CubicBezierInterpolator; -import me.grishka.appkit.utils.V; - -public class PhotoStatusDisplayItem extends ImageStatusDisplayItem{ - public PhotoStatusDisplayItem(String parentID, Status status, Attachment photo, BaseStatusListFragment parentFragment, int index, int totalPhotos, PhotoLayoutHelper.TiledLayoutResult tiledLayout, PhotoLayoutHelper.TiledLayoutResult.Tile thisTile){ - super(parentID, parentFragment, photo, status, index, totalPhotos, tiledLayout, thisTile); - request=new UrlImageLoaderRequest(photo.url, 1000, 1000); - } - - @Override - public Type getType(){ - return Type.PHOTO; - } - - public static class Holder extends ImageStatusDisplayItem.Holder { - public Holder(Activity activity, ViewGroup parent) { - super(activity, R.layout.display_item_photo, parent); - } - } -} diff --git a/mastodon/src/main/java/org/joinmastodon/android/ui/displayitems/PollOptionStatusDisplayItem.java b/mastodon/src/main/java/org/joinmastodon/android/ui/displayitems/PollOptionStatusDisplayItem.java index 22633b8ce..953823d3b 100644 --- a/mastodon/src/main/java/org/joinmastodon/android/ui/displayitems/PollOptionStatusDisplayItem.java +++ b/mastodon/src/main/java/org/joinmastodon/android/ui/displayitems/PollOptionStatusDisplayItem.java @@ -35,8 +35,9 @@ public class PollOptionStatusDisplayItem extends StatusDisplayItem{ text=HtmlParser.parseCustomEmoji(option.title, poll.emojis); emojiHelper.setText(text); showResults=poll.isExpired() || poll.voted; - if(showResults && option.votesCount!=null && poll.votersCount>0){ - votesFraction=(float)option.votesCount/(float)poll.votersCount; + int total=poll.votersCount>0 ? poll.votersCount : poll.votesCount; + if(showResults && option.votesCount!=null && total>0){ + votesFraction=(float)option.votesCount/(float)total; int mostVotedCount=0; for(Poll.Option opt:poll.options) mostVotedCount=Math.max(mostVotedCount, opt.votesCount); diff --git a/mastodon/src/main/java/org/joinmastodon/android/ui/displayitems/ReblogOrReplyLineStatusDisplayItem.java b/mastodon/src/main/java/org/joinmastodon/android/ui/displayitems/ReblogOrReplyLineStatusDisplayItem.java index 366d5ba9f..6f9766c3d 100644 --- a/mastodon/src/main/java/org/joinmastodon/android/ui/displayitems/ReblogOrReplyLineStatusDisplayItem.java +++ b/mastodon/src/main/java/org/joinmastodon/android/ui/displayitems/ReblogOrReplyLineStatusDisplayItem.java @@ -10,8 +10,10 @@ import android.text.SpannableStringBuilder; import android.util.TypedValue; import android.view.View; import android.view.ViewGroup; +import android.widget.LinearLayout; import android.widget.TextView; +import org.joinmastodon.android.GlobalUserPreferences; import org.joinmastodon.android.R; import org.joinmastodon.android.fragments.BaseStatusListFragment; import org.joinmastodon.android.model.Emoji; @@ -27,6 +29,7 @@ import androidx.annotation.Nullable; import me.grishka.appkit.imageloader.ImageLoaderViewHolder; import me.grishka.appkit.imageloader.requests.ImageLoaderRequest; +import me.grishka.appkit.utils.V; public class ReblogOrReplyLineStatusDisplayItem extends StatusDisplayItem{ private CharSequence text; @@ -37,8 +40,10 @@ public class ReblogOrReplyLineStatusDisplayItem extends StatusDisplayItem{ private int iconEnd; private CustomEmojiHelper emojiHelper=new CustomEmojiHelper(); private View.OnClickListener handleClick; + boolean belowHeader, needBottomPadding; + ReblogOrReplyLineStatusDisplayItem extra; - public ReblogOrReplyLineStatusDisplayItem(String parentID, BaseStatusListFragment parentFragment, CharSequence text, List emojis, @DrawableRes int icon, StatusPrivacy visibility, @Nullable View.OnClickListener handleClick){ + public ReblogOrReplyLineStatusDisplayItem(String parentID, BaseStatusListFragment parentFragment, CharSequence text, List emojis, @DrawableRes int icon, StatusPrivacy visibility, @Nullable View.OnClickListener handleClick) { super(parentID, parentFragment); SpannableStringBuilder ssb=new SpannableStringBuilder(text); HtmlParser.parseCustomEmoji(ssb, emojis); @@ -77,29 +82,57 @@ public class ReblogOrReplyLineStatusDisplayItem extends StatusDisplayItem{ } public static class Holder extends StatusDisplayItem.Holder implements ImageLoaderViewHolder{ - private final TextView text; + private final TextView text, extraText; + private final View separator; + public Holder(Activity activity, ViewGroup parent){ super(activity, R.layout.display_item_reblog_or_reply_line, parent); text=findViewById(R.id.text); + extraText=findViewById(R.id.extra_text); + separator=findViewById(R.id.separator); + if (GlobalUserPreferences.replyLineAboveHeader && GlobalUserPreferences.compactReblogReplyLine) { + itemView.getViewTreeObserver().addOnPreDrawListener(() -> { + if (item == null) return true; + int orientation = ((LinearLayout) itemView).getOrientation(); + extraText.setPaddingRelative(extraText.getPaddingStart(), item.extra != null && orientation == LinearLayout.VERTICAL ? 0 : V.dp(16), extraText.getPaddingEnd(), extraText.getPaddingBottom()); + separator.setVisibility(item.extra != null && orientation == LinearLayout.HORIZONTAL ? View.VISIBLE : View.GONE); + return true; + }); + } } - @Override - public void onBind(ReblogOrReplyLineStatusDisplayItem item){ + private void bindLine(ReblogOrReplyLineStatusDisplayItem item, TextView text) { text.setText(item.text); text.setCompoundDrawablesRelativeWithIntrinsicBounds(item.icon, 0, item.iconEnd, 0); - if(item.handleClick!=null) text.setOnClickListener(item.handleClick); - text.setEnabled(!item.inset); - text.setClickable(!item.inset); + text.setOnClickListener(item.handleClick); + text.setEnabled(!item.inset && item.handleClick != null); + text.setClickable(!item.inset && item.handleClick != null); Context ctx = itemView.getContext(); int visibilityText = item.visibility != null ? switch (item.visibility) { case PUBLIC -> R.string.visibility_public; case UNLISTED -> R.string.sk_visibility_unlisted; case PRIVATE -> R.string.visibility_followers_only; + case LOCAL -> R.string.sk_local_only; default -> 0; } : 0; if (visibilityText != 0) text.setContentDescription(item.text + " (" + ctx.getString(visibilityText) + ")"); if(Build.VERSION.SDK_INT new HeaderStatusDisplayItem.Holder(activity, parent); case REBLOG_OR_REPLY_LINE -> new ReblogOrReplyLineStatusDisplayItem.Holder(activity, parent); case TEXT -> new TextStatusDisplayItem.Holder(activity, parent); - case PHOTO -> new PhotoStatusDisplayItem.Holder(activity, parent); - case GIFV -> new GifVStatusDisplayItem.Holder(activity, parent); case AUDIO -> new AudioStatusDisplayItem.Holder(activity, parent); - case VIDEO -> new VideoStatusDisplayItem.Holder(activity, parent); case POLL_OPTION -> new PollOptionStatusDisplayItem.Holder(activity, parent); case POLL_FOOTER -> new PollFooterStatusDisplayItem.Holder(activity, parent); case CARD -> new LinkCardStatusDisplayItem.Holder(activity, parent); @@ -82,6 +76,7 @@ public abstract class StatusDisplayItem{ case HASHTAG -> new HashtagStatusDisplayItem.Holder(activity, parent); case GAP -> new GapStatusDisplayItem.Holder(activity, parent); case EXTENDED_FOOTER -> new ExtendedFooterStatusDisplayItem.Holder(activity, parent); + case MEDIA_GRID -> new MediaGridStatusDisplayItem.Holder(activity, parent); case WARNING -> new WarningFilteredStatusDisplayItem.Holder(activity, parent); }; } @@ -99,10 +94,6 @@ public abstract class StatusDisplayItem{ } public static ArrayList buildItems(BaseStatusListFragment fragment, Status status, String accountID, DisplayItemsParent parentObject, Map knownAccounts, boolean inset, boolean addFooter, Notification notification, boolean disableTranslate, Filter.FilterContext filterContext){ - return buildItems(fragment, status, accountID, parentObject, knownAccounts, inset, addFooter, notification, disableTranslate, filterContext, null); - } - - public static ArrayList buildItems(BaseStatusListFragment fragment, Status status, String accountID, DisplayItemsParent parentObject, Map knownAccounts, boolean inset, boolean addFooter, Notification notification, boolean disableTranslate, Filter.FilterContext filterContext, StatusDisplayItem titleItem){ String parentID=parentObject.getID(); ArrayList items=new ArrayList<>(); @@ -119,24 +110,36 @@ public abstract class StatusDisplayItem{ statusForContent.filterRevealed = filterPredicate.testWithWarning(status); } + ReblogOrReplyLineStatusDisplayItem replyLine = null; + boolean threadReply = statusForContent.inReplyToAccountId != null && + statusForContent.inReplyToAccountId.equals(statusForContent.account.id); + + if(statusForContent.inReplyToAccountId!=null && !(threadReply && fragment instanceof ThreadFragment)){ + Account account = knownAccounts.get(statusForContent.inReplyToAccountId); + String text = threadReply ? fragment.getString(R.string.sk_show_thread) + : account == null ? fragment.getString(R.string.sk_in_reply) + : GlobalUserPreferences.compactReblogReplyLine && status.reblog != null ? account.displayName + : fragment.getString(R.string.in_reply_to, account.displayName); + replyLine = new ReblogOrReplyLineStatusDisplayItem( + parentID, fragment, text, account == null ? List.of() : account.emojis, + R.drawable.ic_fluent_arrow_reply_20_filled, null, null + ); + } + if(status.reblog!=null){ boolean isOwnPost = AccountSessionManager.getInstance().isSelf(fragment.getAccountID(), status.account); - items.add(new ReblogOrReplyLineStatusDisplayItem(parentID, fragment, fragment.getString(R.string.user_boosted, status.account.displayName), status.account.emojis, R.drawable.ic_fluent_arrow_repeat_all_20_filled, isOwnPost ? status.visibility : null, i->{ + String text = GlobalUserPreferences.compactReblogReplyLine && replyLine != null + ? status.account.displayName + : fragment.getString(R.string.user_boosted, status.account.displayName); + + items.add(new ReblogOrReplyLineStatusDisplayItem(parentID, fragment, text, status.account.emojis, R.drawable.ic_fluent_arrow_repeat_all_20_filled, isOwnPost ? status.visibility : null, i->{ args.putParcelable("profileAccount", Parcels.wrap(status.account)); Nav.go(fragment.getActivity(), ProfileFragment.class, args); })); - }else if(status.inReplyToAccountId!=null && knownAccounts.containsKey(status.inReplyToAccountId)){ - Account account=Objects.requireNonNull(knownAccounts.get(status.inReplyToAccountId)); - items.add(new ReblogOrReplyLineStatusDisplayItem(parentID, fragment, fragment.getString(R.string.in_reply_to, account.displayName), account.emojis, R.drawable.ic_fluent_arrow_reply_20_filled, null, i->{ - args.putParcelable("profileAccount", Parcels.wrap(account)); - Nav.go(fragment.getActivity(), ProfileFragment.class, args); - })); - } else if ( - !(status.tags.isEmpty() || - fragment instanceof HashtagTimelineFragment || - fragment instanceof ListTimelineFragment - ) && fragment.getParentFragment() instanceof HomeTabFragment home - ) { + } else if (!(status.tags.isEmpty() || + fragment instanceof HashtagTimelineFragment || + fragment instanceof ListTimelineFragment + ) && fragment.getParentFragment() instanceof HomeTabFragment home) { home.getHashtags().stream() .filter(followed -> status.tags.stream() .anyMatch(hashtag -> followed.name.equalsIgnoreCase(hashtag.name))) @@ -151,28 +154,38 @@ public abstract class StatusDisplayItem{ } ))); } + + if (replyLine != null && GlobalUserPreferences.replyLineAboveHeader) { + Optional primaryLine = items.stream() + .filter(i -> i instanceof ReblogOrReplyLineStatusDisplayItem) + .map(ReblogOrReplyLineStatusDisplayItem.class::cast) + .findFirst(); + + if (primaryLine.isPresent() && GlobalUserPreferences.compactReblogReplyLine) { + primaryLine.get().extra = replyLine; + } else { + items.add(replyLine); + } + } + HeaderStatusDisplayItem header; items.add(header=new HeaderStatusDisplayItem(parentID, statusForContent.account, statusForContent.createdAt, fragment, accountID, statusForContent, null, notification, scheduledStatus)); + + if (replyLine != null && !GlobalUserPreferences.replyLineAboveHeader) { + replyLine.belowHeader = true; + items.add(replyLine); + } + if(!TextUtils.isEmpty(statusForContent.content)) items.add(new TextStatusDisplayItem(parentID, HtmlParser.parse(statusForContent.content, statusForContent.emojis, statusForContent.mentions, statusForContent.tags, accountID), fragment, statusForContent, disableTranslate)); + else if (!GlobalUserPreferences.replyLineAboveHeader && replyLine != null) + replyLine.needBottomPadding=true; else header.needBottomPadding=true; List imageAttachments=statusForContent.mediaAttachments.stream().filter(att->att.type.isImage()).collect(Collectors.toList()); if(!imageAttachments.isEmpty()){ - int photoIndex=0; - PhotoLayoutHelper.TiledLayoutResult layout=PhotoLayoutHelper.processThumbs(1000, 1910, imageAttachments); - for(Attachment attachment:imageAttachments){ - if(attachment.type==Attachment.Type.IMAGE){ - items.add(new PhotoStatusDisplayItem(parentID, statusForContent, attachment, fragment, photoIndex, imageAttachments.size(), layout, layout.tiles[photoIndex])); - }else if(attachment.type==Attachment.Type.GIFV){ - items.add(new GifVStatusDisplayItem(parentID, statusForContent, attachment, fragment, photoIndex, imageAttachments.size(), layout, layout.tiles[photoIndex])); - }else if(attachment.type==Attachment.Type.VIDEO){ - items.add(new VideoStatusDisplayItem(parentID, statusForContent, attachment, fragment, photoIndex, imageAttachments.size(), layout, layout.tiles[photoIndex])); - }else{ - throw new IllegalStateException("This isn't supposed to happen, type is "+attachment.type); - } - photoIndex++; - } + PhotoLayoutHelper.TiledLayoutResult layout=PhotoLayoutHelper.processThumbs(imageAttachments); + items.add(new MediaGridStatusDisplayItem(parentID, fragment, layout, imageAttachments, statusForContent)); } for(Attachment att:statusForContent.mediaAttachments){ if(att.type==Attachment.Type.AUDIO){ @@ -196,8 +209,6 @@ public abstract class StatusDisplayItem{ item.index=i++; } - if (titleItem != null) items.add(0, titleItem); - if (!statusForContent.filterRevealed) { return new ArrayList<>(List.of( new WarningFilteredStatusDisplayItem(parentID, fragment, statusForContent, items) @@ -218,9 +229,6 @@ public abstract class StatusDisplayItem{ HEADER, REBLOG_OR_REPLY_LINE, TEXT, - PHOTO, - VIDEO, - GIFV, AUDIO, POLL_OPTION, POLL_FOOTER, @@ -230,8 +238,9 @@ public abstract class StatusDisplayItem{ ACCOUNT, HASHTAG, GAP, - WARNING, - EXTENDED_FOOTER + EXTENDED_FOOTER, + MEDIA_GRID, + WARNING } public static abstract class Holder extends BindableViewHolder implements UsableRecyclerView.DisableableClickable{ diff --git a/mastodon/src/main/java/org/joinmastodon/android/ui/displayitems/TextStatusDisplayItem.java b/mastodon/src/main/java/org/joinmastodon/android/ui/displayitems/TextStatusDisplayItem.java index 6e94d4195..83a8b7ef1 100644 --- a/mastodon/src/main/java/org/joinmastodon/android/ui/displayitems/TextStatusDisplayItem.java +++ b/mastodon/src/main/java/org/joinmastodon/android/ui/displayitems/TextStatusDisplayItem.java @@ -6,6 +6,7 @@ import android.graphics.drawable.Drawable; import android.text.TextUtils; import android.view.View; import android.view.ViewGroup; +import android.view.ViewTreeObserver; import android.widget.LinearLayout; import android.widget.Button; import android.widget.ScrollView; @@ -227,13 +228,20 @@ public class TextStatusDisplayItem extends StatusDisplayItem{ readMore.setVisibility(View.GONE); } - if (GlobalUserPreferences.collapseLongPosts) text.post(() -> { - boolean tooBig = text.getMeasuredHeight() > textMaxHeight; - boolean inTimeline = !item.textSelectable; - boolean hasSpoiler = !TextUtils.isEmpty(item.status.spoilerText); - boolean expandable = inTimeline && tooBig && !hasSpoiler; - item.parentFragment.onEnableExpandable(this, expandable); - }); + if (GlobalUserPreferences.collapseLongPosts) { + text.getViewTreeObserver().addOnPreDrawListener(new ViewTreeObserver.OnPreDrawListener() { + @Override + public boolean onPreDraw() { + text.getViewTreeObserver().removeOnPreDrawListener(this); + boolean tooBig = text.getMeasuredHeight() > textMaxHeight; + boolean inTimeline = !item.textSelectable; + boolean hasSpoiler = !TextUtils.isEmpty(item.status.spoilerText); + boolean expandable = inTimeline && tooBig && !hasSpoiler; + item.parentFragment.onEnableExpandable(Holder.this, expandable); + return true; + } + }); + } readMore.setVisibility(item.status.textExpandable && !item.status.textExpanded ? View.VISIBLE : View.GONE); textScrollView.setLayoutParams(item.status.textExpandable && !item.status.textExpanded ? collapseParams : wrapParams); diff --git a/mastodon/src/main/java/org/joinmastodon/android/ui/displayitems/VideoStatusDisplayItem.java b/mastodon/src/main/java/org/joinmastodon/android/ui/displayitems/VideoStatusDisplayItem.java deleted file mode 100644 index 41c54aeef..000000000 --- a/mastodon/src/main/java/org/joinmastodon/android/ui/displayitems/VideoStatusDisplayItem.java +++ /dev/null @@ -1,42 +0,0 @@ -package org.joinmastodon.android.ui.displayitems; - -import android.app.Activity; -import android.graphics.Outline; -import android.view.View; -import android.view.ViewGroup; -import android.view.ViewOutlineProvider; - -import org.joinmastodon.android.R; -import org.joinmastodon.android.fragments.BaseStatusListFragment; -import org.joinmastodon.android.model.Attachment; -import org.joinmastodon.android.model.Status; -import org.joinmastodon.android.ui.PhotoLayoutHelper; - -import me.grishka.appkit.imageloader.requests.UrlImageLoaderRequest; - -public class VideoStatusDisplayItem extends ImageStatusDisplayItem{ - public VideoStatusDisplayItem(String parentID, Status status, Attachment attachment, BaseStatusListFragment parentFragment, int index, int totalPhotos, PhotoLayoutHelper.TiledLayoutResult tiledLayout, PhotoLayoutHelper.TiledLayoutResult.Tile thisTile){ - super(parentID, parentFragment, attachment, status, index, totalPhotos, tiledLayout, thisTile); - request=new UrlImageLoaderRequest(attachment.previewUrl, 1000, 1000); - } - - @Override - public Type getType(){ - return Type.VIDEO; - } - - public static class Holder extends ImageStatusDisplayItem.Holder{ - - public Holder(Activity activity, ViewGroup parent){ - super(activity, R.layout.display_item_video, parent); - View play=findViewById(R.id.play_button); - play.setOutlineProvider(new ViewOutlineProvider(){ - @Override - public void getOutline(View view, Outline outline){ - outline.setOval(0, 0, view.getWidth(), view.getHeight()); - outline.setAlpha(.99f); // fixes shadow rendering - } - }); - } - } -} diff --git a/mastodon/src/main/java/org/joinmastodon/android/ui/photoviewer/PhotoViewerHost.java b/mastodon/src/main/java/org/joinmastodon/android/ui/photoviewer/PhotoViewerHost.java index 72dcb47ce..fc168087b 100644 --- a/mastodon/src/main/java/org/joinmastodon/android/ui/photoviewer/PhotoViewerHost.java +++ b/mastodon/src/main/java/org/joinmastodon/android/ui/photoviewer/PhotoViewerHost.java @@ -1,7 +1,8 @@ package org.joinmastodon.android.ui.photoviewer; import org.joinmastodon.android.model.Status; +import org.joinmastodon.android.ui.displayitems.MediaGridStatusDisplayItem; public interface PhotoViewerHost{ - void openPhotoViewer(String parentID, Status status, int attachmentIndex); + void openPhotoViewer(String parentID, Status status, int attachmentIndex, MediaGridStatusDisplayItem.Holder gridHolder); } diff --git a/mastodon/src/main/java/org/joinmastodon/android/ui/text/ClickableLinksDelegate.java b/mastodon/src/main/java/org/joinmastodon/android/ui/text/ClickableLinksDelegate.java index 74189a796..414c78649 100644 --- a/mastodon/src/main/java/org/joinmastodon/android/ui/text/ClickableLinksDelegate.java +++ b/mastodon/src/main/java/org/joinmastodon/android/ui/text/ClickableLinksDelegate.java @@ -8,24 +8,25 @@ import android.graphics.Rect; import android.graphics.RectF; import android.text.Layout; import android.text.Spanned; +import android.view.GestureDetector; import android.view.MotionEvent; import android.view.SoundEffectConstants; -import android.view.View; -import android.view.ViewConfiguration; import android.widget.TextView; +import androidx.annotation.NonNull; + +import org.joinmastodon.android.ui.utils.UiUtils; + import me.grishka.appkit.utils.V; public class ClickableLinksDelegate { - private Paint hlPaint; + private final Paint hlPaint; private Path hlPath; private LinkSpan selectedSpan; - private TextView view; + private final TextView view; - private final Runnable longClickRunnable = () -> { - if (selectedSpan != null) selectedSpan.onLongClick(view); - }; + private final GestureDetector gestureDetector; public ClickableLinksDelegate(TextView view) { this.view=view; @@ -33,11 +34,45 @@ public class ClickableLinksDelegate { hlPaint.setAntiAlias(true); hlPaint.setPathEffect(new CornerPathEffect(V.dp(3))); // view.setHighlightColor(view.getResources().getColor(android.R.color.holo_blue_light)); + gestureDetector = new GestureDetector(view.getContext(), new LinkGestureListener(), view.getHandler()); } public boolean onTouch(MotionEvent event) { - long eventDuration = event.getEventTime() - event.getDownTime(); - if(event.getAction()==MotionEvent.ACTION_DOWN){ + if(event.getAction()==MotionEvent.ACTION_CANCEL){ + // the gestureDetector does not provide a callback for CANCEL, therefore: + // remove background color of view before passing event to gestureDetector + resetAndInvalidate(); + } + return gestureDetector.onTouchEvent(event); + } + + /** + * remove highlighting from span and let the system redraw the view + */ + private void resetAndInvalidate() { + hlPath=null; + selectedSpan=null; + view.invalidate(); + } + + public void onDraw(Canvas canvas){ + if(hlPath!=null){ + canvas.save(); + canvas.translate(0, view.getPaddingTop()); + canvas.drawPath(hlPath, hlPaint); + canvas.restore(); + } + } + + /** + * GestureListener for spans that represent URLs. + * onDown: on start of touch event, set highlighting + * onSingleTapUp: when there was a (short) tap, call onClick and reset highlighting + * onLongPress: copy URL to clipboard, let user know, reset highlighting + */ + private class LinkGestureListener extends GestureDetector.SimpleOnGestureListener { + @Override + public boolean onDown(@NonNull MotionEvent event) { int line=-1; Rect rect=new Rect(); Layout l=view.getLayout(); @@ -52,8 +87,7 @@ public class ClickableLinksDelegate { return false; } CharSequence text=view.getText(); - if(text instanceof Spanned){ - Spanned s=(Spanned)text; + if(text instanceof Spanned s){ LinkSpan[] spans=s.getSpans(0, s.length()-1, LinkSpan.class); if(spans.length>0){ for(LinkSpan span:spans){ @@ -70,7 +104,6 @@ public class ClickableLinksDelegate { } hlPath=new Path(); selectedSpan=span; - view.postDelayed(longClickRunnable, ViewConfiguration.getLongPressTimeout()); hlPaint.setColor((span.getColor() & 0x00FFFFFF) | 0x33000000); //l.getSelectionPath(start, end, hlPath); for(int j=lstart;j<=lend;j++){ @@ -96,35 +129,26 @@ public class ClickableLinksDelegate { } } } + return super.onDown(event); } - if(event.getAction()==MotionEvent.ACTION_UP && selectedSpan!=null){ - if (eventDuration <= ViewConfiguration.getLongPressTimeout()) { + + @Override + public boolean onSingleTapUp(@NonNull MotionEvent event) { + if(selectedSpan!=null){ view.playSoundEffect(SoundEffectConstants.CLICK); selectedSpan.onClick(view.getContext()); + resetAndInvalidate(); + return true; } - view.removeCallbacks(longClickRunnable); - hlPath=null; - selectedSpan=null; - view.invalidate(); return false; } - if(event.getAction()==MotionEvent.ACTION_CANCEL){ - hlPath=null; - selectedSpan=null; - view.removeCallbacks(longClickRunnable); - view.invalidate(); - return false; - } - return false; - } - - public void onDraw(Canvas canvas){ - if(hlPath!=null){ - canvas.save(); - canvas.translate(0, view.getPaddingTop()); - canvas.drawPath(hlPath, hlPaint); - canvas.restore(); - } - } + @Override + public void onLongPress(@NonNull MotionEvent event) { + if (selectedSpan == null) return; + UiUtils.copyText(view, selectedSpan.getType() == LinkSpan.Type.URL ? selectedSpan.getLink() : selectedSpan.getText()); + //reset view + resetAndInvalidate(); + } + } } diff --git a/mastodon/src/main/java/org/joinmastodon/android/ui/text/LinkSpan.java b/mastodon/src/main/java/org/joinmastodon/android/ui/text/LinkSpan.java index e9c4fb0fc..2b80c076b 100644 --- a/mastodon/src/main/java/org/joinmastodon/android/ui/text/LinkSpan.java +++ b/mastodon/src/main/java/org/joinmastodon/android/ui/text/LinkSpan.java @@ -46,14 +46,14 @@ public class LinkSpan extends CharacterStyle { } } - public void onLongClick(View view) { - UiUtils.copyText(view, getType() == Type.URL ? link : text); - } - public String getLink(){ return link; } + public String getText() { + return text; + } + public Type getType(){ return type; } diff --git a/mastodon/src/main/java/org/joinmastodon/android/ui/utils/InsetStatusItemDecoration.java b/mastodon/src/main/java/org/joinmastodon/android/ui/utils/InsetStatusItemDecoration.java index a811fb464..37aca8964 100644 --- a/mastodon/src/main/java/org/joinmastodon/android/ui/utils/InsetStatusItemDecoration.java +++ b/mastodon/src/main/java/org/joinmastodon/android/ui/utils/InsetStatusItemDecoration.java @@ -8,10 +8,8 @@ import android.view.View; import org.joinmastodon.android.R; import org.joinmastodon.android.fragments.BaseStatusListFragment; -import org.joinmastodon.android.fragments.NotificationsListFragment; -import org.joinmastodon.android.ui.PhotoLayoutHelper; -import org.joinmastodon.android.ui.displayitems.ImageStatusDisplayItem; import org.joinmastodon.android.ui.displayitems.LinkCardStatusDisplayItem; +import org.joinmastodon.android.ui.displayitems.MediaGridStatusDisplayItem; import org.joinmastodon.android.ui.displayitems.StatusDisplayItem; import java.util.List; @@ -87,21 +85,11 @@ public class InsetStatusItemDecoration extends RecyclerView.ItemDecoration{ boolean topSiblingInset=pos>0 && displayItems.get(pos-1).inset; boolean bottomSiblingInset=pos img){ - PhotoLayoutHelper.TiledLayoutResult layout=img.getItem().tiledLayout; - PhotoLayoutHelper.TiledLayoutResult.Tile tile=img.getItem().thisTile; - // only inset those items that are on the edges of the layout - insetLeft=tile.startCol==0; - insetRight=tile.startCol+tile.colSpan==layout.columnSizes.length; - // inset all items in the bottom row - if(tile.startRow+tile.rowSpan==layout.rowSizes.length) - bottomSiblingInset=false; - } if(insetLeft) outRect.left=pad; if(insetRight) diff --git a/mastodon/src/main/java/org/joinmastodon/android/ui/utils/MediaAttachmentViewController.java b/mastodon/src/main/java/org/joinmastodon/android/ui/utils/MediaAttachmentViewController.java new file mode 100644 index 000000000..db0d9653f --- /dev/null +++ b/mastodon/src/main/java/org/joinmastodon/android/ui/utils/MediaAttachmentViewController.java @@ -0,0 +1,77 @@ +package org.joinmastodon.android.ui.utils; + +import android.content.Context; +import android.graphics.drawable.Drawable; +import android.text.TextUtils; +import android.view.LayoutInflater; +import android.view.View; +import android.widget.ImageView; + +import org.joinmastodon.android.GlobalUserPreferences; +import org.joinmastodon.android.R; +import org.joinmastodon.android.model.Attachment; +import org.joinmastodon.android.model.Status; +import org.joinmastodon.android.ui.displayitems.MediaGridStatusDisplayItem; +import org.joinmastodon.android.ui.drawables.BlurhashCrossfadeDrawable; + +public class MediaAttachmentViewController{ + public final View view; + public final MediaGridStatusDisplayItem.GridItemType type; + public final ImageView photo; + public final View altButton, noAltButton, btnsWrap; + public static int[] altWrapPadding = null; + private BlurhashCrossfadeDrawable crossfadeDrawable=new BlurhashCrossfadeDrawable(); + private final Context context; + private boolean didClear; + private Status status; + + public MediaAttachmentViewController(Context context, MediaGridStatusDisplayItem.GridItemType type){ + view=context.getSystemService(LayoutInflater.class).inflate(switch(type){ + case PHOTO -> R.layout.display_item_photo; + case VIDEO -> R.layout.display_item_video; + case GIFV -> R.layout.display_item_gifv; + }, null); + photo=view.findViewById(R.id.photo); + altButton=view.findViewById(R.id.alt_button); + noAltButton=view.findViewById(R.id.no_alt_button); + btnsWrap=view.findViewById(R.id.alt_badges); + this.type=type; + this.context=context; + if (altWrapPadding == null) { + altWrapPadding = new int[] { btnsWrap.getPaddingLeft(), btnsWrap.getPaddingTop(), btnsWrap.getPaddingRight(), btnsWrap.getPaddingBottom() }; + } + } + + public void bind(Attachment attachment, Status status){ + this.status=status; + crossfadeDrawable.setSize(attachment.getWidth(), attachment.getHeight()); + crossfadeDrawable.setBlurhashDrawable(attachment.blurhashPlaceholder); + crossfadeDrawable.setCrossfadeAlpha(status.spoilerRevealed ? 0f : 1f); + photo.setImageDrawable(null); + photo.setImageDrawable(crossfadeDrawable); + boolean hasAltText = !TextUtils.isEmpty(attachment.description); + photo.setContentDescription(!hasAltText ? context.getString(R.string.media_no_description) : attachment.description); + if(btnsWrap!=null){ + btnsWrap.setVisibility(View.VISIBLE); + altButton.setVisibility(hasAltText && GlobalUserPreferences.showAltIndicator ? View.VISIBLE : View.GONE); + noAltButton.setVisibility(!hasAltText && GlobalUserPreferences.showNoAltIndicator ? View.VISIBLE : View.GONE); + } + didClear=false; + } + + public void setImage(Drawable drawable){ + crossfadeDrawable.setImageDrawable(drawable); + if(didClear && status.spoilerRevealed) + crossfadeDrawable.animateAlpha(0f); + } + + public void clearImage(){ + crossfadeDrawable.setCrossfadeAlpha(1f); + crossfadeDrawable.setImageDrawable(null); + didClear=true; + } + + public void setRevealed(boolean revealed){ + crossfadeDrawable.animateAlpha(revealed ? 0f : 1f); + } +} 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 c720958a5..3e4bb0b0d 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 @@ -694,6 +694,9 @@ public class UiUtils { TypedArray ta=button.getContext().obtainStyledAttributes(styleRes, new int[]{android.R.attr.background}); button.setBackground(ta.getDrawable(0)); ta.recycle(); + ta=button.getContext().obtainStyledAttributes(styleRes, new int[]{android.R.attr.textColor}); + button.setTextColor(ta.getColorStateList(0)); + ta.recycle(); } public static void performAccountAction(Activity activity, Account account, String accountID, Relationship relationship, Button button, Consumer progressCallback, Consumer resultCallback) { @@ -1143,6 +1146,10 @@ public class UiUtils { return !TextUtils.isEmpty(getSystemProperty("ro.miui.ui.version.code")); } + public static boolean isEMUI() { + return !TextUtils.isEmpty(getSystemProperty("ro.build.version.emui")); + } + public static int alphaBlendColors(int color1, int color2, float alpha) { float alpha0 = 1f - alpha; int r = Math.round(((color1 >> 16) & 0xFF) * alpha0 + ((color2 >> 16) & 0xFF) * alpha); diff --git a/mastodon/src/main/java/org/joinmastodon/android/ui/views/FrameLayoutThatOnlyMeasuresFirstChild.java b/mastodon/src/main/java/org/joinmastodon/android/ui/views/FrameLayoutThatOnlyMeasuresFirstChild.java new file mode 100644 index 000000000..f195ac633 --- /dev/null +++ b/mastodon/src/main/java/org/joinmastodon/android/ui/views/FrameLayoutThatOnlyMeasuresFirstChild.java @@ -0,0 +1,29 @@ +package org.joinmastodon.android.ui.views; + +import android.content.Context; +import android.util.AttributeSet; +import android.view.View; +import android.widget.FrameLayout; + +public class FrameLayoutThatOnlyMeasuresFirstChild extends FrameLayout{ + public FrameLayoutThatOnlyMeasuresFirstChild(Context context){ + this(context, null); + } + + public FrameLayoutThatOnlyMeasuresFirstChild(Context context, AttributeSet attrs){ + this(context, attrs, 0); + } + + public FrameLayoutThatOnlyMeasuresFirstChild(Context context, AttributeSet attrs, int defStyle){ + super(context, attrs, defStyle); + } + + @Override + protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec){ + if(getChildCount()==0) + return; + View child0=getChildAt(0); + measureChild(child0, widthMeasureSpec, heightMeasureSpec); + super.onMeasure(child0.getMeasuredWidth() | MeasureSpec.EXACTLY, child0.getMeasuredHeight() | MeasureSpec.EXACTLY); + } +} diff --git a/mastodon/src/main/java/org/joinmastodon/android/ui/views/ImageAttachmentFrameLayout.java b/mastodon/src/main/java/org/joinmastodon/android/ui/views/ImageAttachmentFrameLayout.java deleted file mode 100644 index 0bfcbdb16..000000000 --- a/mastodon/src/main/java/org/joinmastodon/android/ui/views/ImageAttachmentFrameLayout.java +++ /dev/null @@ -1,54 +0,0 @@ -package org.joinmastodon.android.ui.views; - -import android.content.Context; -import android.util.AttributeSet; -import android.view.View; -import android.widget.FrameLayout; - -import org.joinmastodon.android.ui.PhotoLayoutHelper; - -import androidx.annotation.NonNull; -import androidx.annotation.Nullable; -import me.grishka.appkit.utils.V; - -public class ImageAttachmentFrameLayout extends FrameLayout{ - public static final int MAX_WIDTH=400; // dp - - private PhotoLayoutHelper.TiledLayoutResult tileLayout; - private PhotoLayoutHelper.TiledLayoutResult.Tile tile; - private int horizontalInset; - - public ImageAttachmentFrameLayout(@NonNull Context context){ - super(context); - } - - public ImageAttachmentFrameLayout(@NonNull Context context, @Nullable AttributeSet attrs){ - super(context, attrs); - } - - public ImageAttachmentFrameLayout(@NonNull Context context, @Nullable AttributeSet attrs, int defStyleAttr){ - super(context, attrs, defStyleAttr); - } - - @Override - protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec){ - if(isInEditMode()){ - super.onMeasure(widthMeasureSpec, heightMeasureSpec); - return; - } - int w=Math.min(((View)getParent()).getMeasuredWidth(), V.dp(MAX_WIDTH))-horizontalInset; - int actualHeight=Math.round(tile.height/1000f*w)+V.dp(1)*(tile.rowSpan-1); - int actualWidth=Math.round(tile.width/1000f*w); - if(tile.startCol+tile.colSpanmaxWidth){ + xOffset=(r-l)/2-maxWidth/2; + } + + for(int i=0;i{ + private final Function producer; + private final HashMap> pool=new HashMap<>(); + + public TypedObjectPool(Function producer){ + this.producer=producer; + } + + public V obtain(K type){ + LinkedList tp=pool.get(type); + if(tp==null) + pool.put(type, tp=new LinkedList<>()); + + V value=tp.poll(); + if(value==null) + value=producer.apply(type); + return value; + } + + public void reuse(K type, V obj){ + Objects.requireNonNull(obj); + Objects.requireNonNull(type); + + LinkedList tp=pool.get(type); + if(tp==null) + pool.put(type, tp=new LinkedList<>()); + tp.add(obj); + } +} diff --git a/mastodon/src/main/res/drawable/ic_fluent_re_order_24_regular.xml b/mastodon/src/main/res/drawable/ic_fluent_re_order_24_regular.xml new file mode 100644 index 000000000..15ef7bc46 --- /dev/null +++ b/mastodon/src/main/res/drawable/ic_fluent_re_order_24_regular.xml @@ -0,0 +1,3 @@ + + + diff --git a/mastodon/src/main/res/layout/alt_badge.xml b/mastodon/src/main/res/layout/alt_badge.xml index f165466cf..955f8f5cb 100644 --- a/mastodon/src/main/res/layout/alt_badge.xml +++ b/mastodon/src/main/res/layout/alt_badge.xml @@ -1,79 +1,34 @@ - - + - + - - - - - - - - - - - - - - - + diff --git a/mastodon/src/main/res/layout/display_item_gifv.xml b/mastodon/src/main/res/layout/display_item_gifv.xml index a3575ab06..a60f72ca8 100644 --- a/mastodon/src/main/res/layout/display_item_gifv.xml +++ b/mastodon/src/main/res/layout/display_item_gifv.xml @@ -1,5 +1,5 @@ - @@ -27,4 +27,4 @@ - \ No newline at end of file + diff --git a/mastodon/src/main/res/layout/display_item_header.xml b/mastodon/src/main/res/layout/display_item_header.xml index be6903169..8217f2750 100644 --- a/mastodon/src/main/res/layout/display_item_header.xml +++ b/mastodon/src/main/res/layout/display_item_header.xml @@ -27,6 +27,7 @@ android:visibility="gone" android:background="?android:actionBarItemBackground" android:contentDescription="@string/sk_delete_notification" + android:tooltipText="@string/sk_delete_notification" android:scaleType="center" android:src="@drawable/ic_fluent_dismiss_20_filled" android:tint="?android:textColorSecondary" /> diff --git a/mastodon/src/main/res/layout/display_item_photo.xml b/mastodon/src/main/res/layout/display_item_photo.xml index 462dab84a..71f4c5099 100644 --- a/mastodon/src/main/res/layout/display_item_photo.xml +++ b/mastodon/src/main/res/layout/display_item_photo.xml @@ -1,5 +1,6 @@ - @@ -12,4 +13,4 @@ - \ No newline at end of file + diff --git a/mastodon/src/main/res/layout/display_item_reblog_or_reply_line.xml b/mastodon/src/main/res/layout/display_item_reblog_or_reply_line.xml index b97a3c8a0..2138522c0 100644 --- a/mastodon/src/main/res/layout/display_item_reblog_or_reply_line.xml +++ b/mastodon/src/main/res/layout/display_item_reblog_or_reply_line.xml @@ -1,14 +1,16 @@ - - \ No newline at end of file + + + + + \ No newline at end of file diff --git a/mastodon/src/main/res/layout/display_item_video.xml b/mastodon/src/main/res/layout/display_item_video.xml index 1f1283fab..17e1e40cd 100644 --- a/mastodon/src/main/res/layout/display_item_video.xml +++ b/mastodon/src/main/res/layout/display_item_video.xml @@ -1,5 +1,5 @@ - @@ -20,4 +20,4 @@ - \ 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 ce06d7675..56f797dc6 100644 --- a/mastodon/src/main/res/layout/fragment_compose.xml +++ b/mastodon/src/main/res/layout/fragment_compose.xml @@ -64,10 +64,10 @@ android:paddingBottom="6dp" android:textAppearance="@style/m3_title_small" android:drawableStart="@drawable/ic_fluent_arrow_reply_20_filled" - tools:drawableEnd="@drawable/ic_fluent_earth_20_regular" android:drawableTint="?android:textColorSecondary" android:drawablePadding="6dp" android:singleLine="true" + android:text="@string/sk_in_reply" android:ellipsize="end"/> + + + + + + + + + + + + + + + + + + + + + + + diff --git a/mastodon/src/main/res/values/strings_sk.xml b/mastodon/src/main/res/values/strings_sk.xml index a14d23879..95956b3e5 100644 --- a/mastodon/src/main/res/values/strings_sk.xml +++ b/mastodon/src/main/res/values/strings_sk.xml @@ -268,4 +268,8 @@ Unboost Reply Successfully replied to the post by %s + In reply + “In reply to” line above avatar + Show thread + Compact reblog/reply line \ No newline at end of file