Merge branch 'main' into pr/FineFindus/439

This commit is contained in:
sk 2023-03-15 23:38:33 +01:00
commit aefa0b89ae
42 changed files with 1009 additions and 813 deletions

View File

@ -9,8 +9,8 @@ android {
applicationId "org.joinmastodon.android.sk" applicationId "org.joinmastodon.android.sk"
minSdk 23 minSdk 23
targetSdk 33 targetSdk 33
versionCode 77 versionCode 78
versionName "1.2.0+fork.77" versionName "1.2.0+fork.78"
testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner" 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" 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"
} }

View File

@ -44,6 +44,8 @@ public class GlobalUserPreferences{
public static boolean collapseLongPosts; public static boolean collapseLongPosts;
public static boolean spectatorMode; public static boolean spectatorMode;
public static boolean autoHideFab; public static boolean autoHideFab;
public static boolean replyLineAboveHeader;
public static boolean compactReblogReplyLine;
public static String publishButtonText; public static String publishButtonText;
public static ThemePreference theme; public static ThemePreference theme;
public static ColorPreference color; public static ColorPreference color;
@ -93,6 +95,8 @@ public class GlobalUserPreferences{
collapseLongPosts=prefs.getBoolean("collapseLongPosts", true); collapseLongPosts=prefs.getBoolean("collapseLongPosts", true);
spectatorMode=prefs.getBoolean("spectatorMode", false); spectatorMode=prefs.getBoolean("spectatorMode", false);
autoHideFab=prefs.getBoolean("autoHideFab", true); autoHideFab=prefs.getBoolean("autoHideFab", true);
replyLineAboveHeader=prefs.getBoolean("replyLineAboveHeader", true);
compactReblogReplyLine=prefs.getBoolean("compactReblogReplyLine", true);
publishButtonText=prefs.getString("publishButtonText", ""); publishButtonText=prefs.getString("publishButtonText", "");
theme=ThemePreference.values()[prefs.getInt("theme", 0)]; theme=ThemePreference.values()[prefs.getInt("theme", 0)];
recentLanguages=fromJson(prefs.getString("recentLanguages", null), recentLanguagesType, new HashMap<>()); recentLanguages=fromJson(prefs.getString("recentLanguages", null), recentLanguagesType, new HashMap<>());
@ -134,8 +138,10 @@ public class GlobalUserPreferences{
.putBoolean("collapseLongPosts", collapseLongPosts) .putBoolean("collapseLongPosts", collapseLongPosts)
.putBoolean("spectatorMode", spectatorMode) .putBoolean("spectatorMode", spectatorMode)
.putBoolean("autoHideFab", autoHideFab) .putBoolean("autoHideFab", autoHideFab)
.putBoolean("compactReblogReplyLine", compactReblogReplyLine)
.putString("publishButtonText", publishButtonText) .putString("publishButtonText", publishButtonText)
.putBoolean("bottomEncoding", bottomEncoding) .putBoolean("bottomEncoding", bottomEncoding)
.putBoolean("replyLineAboveHeader", replyLineAboveHeader)
.putInt("theme", theme.ordinal()) .putInt("theme", theme.ordinal())
.putString("color", color.name()) .putString("color", color.name())
.putString("recentLanguages", gson.toJson(recentLanguages)) .putString("recentLanguages", gson.toJson(recentLanguages))

View File

@ -13,13 +13,11 @@ import android.text.Layout;
import android.text.StaticLayout; import android.text.StaticLayout;
import android.text.TextPaint; import android.text.TextPaint;
import android.text.TextUtils; import android.text.TextUtils;
import android.util.Log;
import android.view.View; import android.view.View;
import android.view.ViewGroup; import android.view.ViewGroup;
import android.view.WindowInsets; import android.view.WindowInsets;
import android.view.animation.TranslateAnimation; import android.view.animation.TranslateAnimation;
import android.widget.ImageButton; import android.widget.ImageButton;
import android.widget.ImageButton;
import android.widget.Toolbar; import android.widget.Toolbar;
import org.joinmastodon.android.E; 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.Relationship;
import org.joinmastodon.android.model.Status; import org.joinmastodon.android.model.Status;
import org.joinmastodon.android.ui.BetterItemAnimator; 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.ExtendedFooterStatusDisplayItem;
import org.joinmastodon.android.ui.displayitems.GapStatusDisplayItem; import org.joinmastodon.android.ui.displayitems.GapStatusDisplayItem;
import org.joinmastodon.android.ui.displayitems.HeaderStatusDisplayItem; 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.PollFooterStatusDisplayItem;
import org.joinmastodon.android.ui.displayitems.PollOptionStatusDisplayItem; import org.joinmastodon.android.ui.displayitems.PollOptionStatusDisplayItem;
import org.joinmastodon.android.ui.displayitems.StatusDisplayItem; 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.displayitems.WarningFilteredStatusDisplayItem;
import org.joinmastodon.android.ui.photoviewer.PhotoViewer; import org.joinmastodon.android.ui.photoviewer.PhotoViewer;
import org.joinmastodon.android.ui.photoviewer.PhotoViewerHost; 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.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.ArrayList;
import java.util.Collections; import java.util.Collections;
@ -59,7 +57,6 @@ import java.util.stream.Collectors;
import androidx.annotation.NonNull; import androidx.annotation.NonNull;
import androidx.annotation.Nullable; import androidx.annotation.Nullable;
import androidx.recyclerview.widget.GridLayoutManager;
import androidx.recyclerview.widget.RecyclerView; import androidx.recyclerview.widget.RecyclerView;
import me.grishka.appkit.Nav; import me.grishka.appkit.Nav;
@ -83,6 +80,7 @@ public abstract class BaseStatusListFragment<T extends DisplayItemsParent> exten
protected HashMap<String, Account> knownAccounts=new HashMap<>(); protected HashMap<String, Account> knownAccounts=new HashMap<>();
protected HashMap<String, Relationship> relationships=new HashMap<>(); protected HashMap<String, Relationship> relationships=new HashMap<>();
protected Rect tmpRect=new Rect(); protected Rect tmpRect=new Rect();
protected TypedObjectPool<MediaGridStatusDisplayItem.GridItemType, MediaAttachmentViewController> attachmentViewsPool=new TypedObjectPool<>(this::makeNewMediaAttachmentView);
public BaseStatusListFragment(){ public BaseStatusListFragment(){
super(20); super(20);
@ -192,21 +190,21 @@ public abstract class BaseStatusListFragment<T extends DisplayItemsParent> exten
} }
@Override @Override
public void openPhotoViewer(String parentID, Status _status, int attachmentIndex){ public void openPhotoViewer(String parentID, Status _status, int attachmentIndex, MediaGridStatusDisplayItem.Holder gridHolder){
final Status status=_status.reblog!=null ? _status.reblog : _status; final Status status=_status.getContentStatus();
currentPhotoViewer=new PhotoViewer(getActivity(), status.mediaAttachments, attachmentIndex, new PhotoViewer.Listener(){ currentPhotoViewer=new PhotoViewer(getActivity(), status.mediaAttachments, attachmentIndex, new PhotoViewer.Listener(){
private ImageStatusDisplayItem.Holder<?> transitioningHolder; private MediaAttachmentViewController transitioningHolder;
@Override @Override
public void setPhotoViewVisibility(int index, boolean visible){ public void setPhotoViewVisibility(int index, boolean visible){
ImageStatusDisplayItem.Holder<?> holder=findPhotoViewHolder(index); MediaAttachmentViewController holder=findPhotoViewHolder(index);
if(holder!=null) if(holder!=null)
holder.photo.setAlpha(visible ? 1f : 0f); holder.photo.setAlpha(visible ? 1f : 0f);
} }
@Override @Override
public boolean startPhotoViewTransition(int index, @NonNull Rect outRect, @NonNull int[] outCornerRadius){ public boolean startPhotoViewTransition(int index, @NonNull Rect outRect, @NonNull int[] outCornerRadius){
ImageStatusDisplayItem.Holder<?> holder=findPhotoViewHolder(index); MediaAttachmentViewController holder=findPhotoViewHolder(index);
if(holder!=null){ if(holder!=null){
transitioningHolder=holder; transitioningHolder=holder;
View view=transitioningHolder.photo; View view=transitioningHolder.photo;
@ -214,7 +212,8 @@ public abstract class BaseStatusListFragment<T extends DisplayItemsParent> exten
view.getLocationOnScreen(pos); view.getLocationOnScreen(pos);
outRect.set(pos[0], pos[1], pos[0]+view.getWidth(), pos[1]+view.getHeight()); outRect.set(pos[0], pos[1], pos[0]+view.getWidth(), pos[1]+view.getHeight());
list.setClipChildren(false); list.setClipChildren(false);
transitioningHolder.itemView.setElevation(1f); gridHolder.setClipChildren(false);
transitioningHolder.view.setElevation(1f);
return true; return true;
} }
return false; return false;
@ -241,15 +240,16 @@ public abstract class BaseStatusListFragment<T extends DisplayItemsParent> exten
view.setTranslationY(0f); view.setTranslationY(0f);
view.setScaleX(1f); view.setScaleX(1f);
view.setScaleY(1f); view.setScaleY(1f);
transitioningHolder.itemView.setElevation(0f); transitioningHolder.view.setElevation(0f);
if(list!=null) if(list!=null)
list.setClipChildren(true); list.setClipChildren(true);
gridHolder.setClipChildren(true);
transitioningHolder=null; transitioningHolder=null;
} }
@Override @Override
public Drawable getPhotoViewCurrentDrawable(int index){ public Drawable getPhotoViewCurrentDrawable(int index){
ImageStatusDisplayItem.Holder<?> holder=findPhotoViewHolder(index); MediaAttachmentViewController holder=findPhotoViewHolder(index);
if(holder!=null) if(holder!=null)
return holder.photo.getDrawable(); return holder.photo.getDrawable();
return null; return null;
@ -265,23 +265,8 @@ public abstract class BaseStatusListFragment<T extends DisplayItemsParent> exten
requestPermissions(permissions, PhotoViewer.PERMISSION_REQUEST); requestPermissions(permissions, PhotoViewer.PERMISSION_REQUEST);
} }
private ImageStatusDisplayItem.Holder<?> findPhotoViewHolder(int index){ private MediaAttachmentViewController findPhotoViewHolder(int index){
if(list==null) return gridHolder.getViewController(index);
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;
} }
}); });
} }
@ -366,31 +351,6 @@ public abstract class BaseStatusListFragment<T extends DisplayItemsParent> 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<displayItems.size()){
StatusDisplayItem item=displayItems.get(position);
if(item instanceof ImageStatusDisplayItem imgItem){
PhotoLayoutHelper.TiledLayoutResult layout=imgItem.tiledLayout;
PhotoLayoutHelper.TiledLayoutResult.Tile tile=imgItem.thisTile;
int spans=0;
for(int i=0;i<tile.colSpan;i++){
spans+=layout.columnSizes[tile.startCol+i];
}
return spans;
}
}
return 1000;
}
});
return lm;
}
@Override @Override
public void onConfigurationChanged(Configuration newConfig){ public void onConfigurationChanged(Configuration newConfig){
super.onConfigurationChanged(newConfig); super.onConfigurationChanged(newConfig);
@ -509,7 +469,7 @@ public abstract class BaseStatusListFragment<T extends DisplayItemsParent> exten
revealSpoiler(status, holder.getItemID()); revealSpoiler(status, holder.getItemID());
} }
public void onRevealSpoilerClick(ImageStatusDisplayItem.Holder<?> holder){ public void onRevealSpoilerClick(MediaGridStatusDisplayItem.Holder holder){
Status status=holder.getItem().status; Status status=holder.getItem().status;
revealSpoiler(status, holder.getItemID()); revealSpoiler(status, holder.getItemID());
} }
@ -557,13 +517,14 @@ public abstract class BaseStatusListFragment<T extends DisplayItemsParent> exten
protected void updateImagesSpoilerState(Status status, String itemID){ protected void updateImagesSpoilerState(Status status, String itemID){
ArrayList<Integer> updatedPositions=new ArrayList<>(); ArrayList<Integer> updatedPositions=new ArrayList<>();
for(ImageStatusDisplayItem.Holder photo:(List<ImageStatusDisplayItem.Holder>)findAllHoldersOfType(itemID, ImageStatusDisplayItem.Holder.class)){ MediaGridStatusDisplayItem.Holder mediaGrid=findHolderOfType(itemID, MediaGridStatusDisplayItem.Holder.class);
photo.setRevealed(status.spoilerRevealed); if(mediaGrid!=null){
updatedPositions.add(photo.getAbsoluteAdapterPosition()-getMainAdapterOffset()); mediaGrid.setRevealed(status.spoilerRevealed);
updatedPositions.add(mediaGrid.getAbsoluteAdapterPosition()-getMainAdapterOffset());
} }
int i=0; int i=0;
for(StatusDisplayItem item:displayItems){ 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); adapter.notifyItemChanged(i);
} }
i++; i++;
@ -706,6 +667,15 @@ public abstract class BaseStatusListFragment<T extends DisplayItemsParent> exten
return UiUtils.pickAccountForCompose(getActivity(), accountID); return UiUtils.pickAccountForCompose(getActivity(), accountID);
} }
private MediaAttachmentViewController makeNewMediaAttachmentView(MediaGridStatusDisplayItem.GridItemType type){
return new MediaAttachmentViewController(getActivity(), type);
}
public TypedObjectPool<MediaGridStatusDisplayItem.GridItemType, MediaAttachmentViewController> getAttachmentViewsPool(){
return attachmentViewsPool;
}
protected class DisplayItemsAdapter extends UsableRecyclerView.Adapter<BindableViewHolder<StatusDisplayItem>> implements ImageLoaderRecyclerAdapter{ protected class DisplayItemsAdapter extends UsableRecyclerView.Adapter<BindableViewHolder<StatusDisplayItem>> implements ImageLoaderRecyclerAdapter{
public DisplayItemsAdapter(){ public DisplayItemsAdapter(){
@ -743,16 +713,6 @@ public abstract class BaseStatusListFragment<T extends DisplayItemsParent> exten
public ImageLoaderRequest getImageRequest(int position, int image){ public ImageLoaderRequest getImageRequest(int position, int image){
return displayItems.get(position).getImageRequest(image); return displayItems.get(position).getImageRequest(image);
} }
// @Override
// public void onViewDetachedFromWindow(@NonNull BindableViewHolder<StatusDisplayItem> holder){
// if(holder instanceof ImageLoaderViewHolder){
// int count=holder.getItem().getImageCount();
// for(int i=0;i<count;i++){
// ((ImageLoaderViewHolder) holder).clearImage(i);
// }
// }
// }
} }
private class StatusListItemDecoration extends RecyclerView.ItemDecoration{ private class StatusListItemDecoration extends RecyclerView.ItemDecoration{
@ -786,25 +746,21 @@ public abstract class BaseStatusListFragment<T extends DisplayItemsParent> exten
for(int i=0;i<parent.getChildCount();i++){ for(int i=0;i<parent.getChildCount();i++){
View child=parent.getChildAt(i); View child=parent.getChildAt(i);
RecyclerView.ViewHolder holder=parent.getChildViewHolder(child); RecyclerView.ViewHolder holder=parent.getChildViewHolder(child);
if(holder instanceof ImageStatusDisplayItem.Holder<?> imgHolder){ if(holder instanceof MediaGridStatusDisplayItem.Holder imgHolder){
if(!imgHolder.getItem().status.spoilerRevealed && TextUtils.isEmpty(imgHolder.getItem().status.spoilerText)){ if(!imgHolder.getItem().status.spoilerRevealed && TextUtils.isEmpty(imgHolder.getItem().status.spoilerText)){
hiddenMediaPaint.setColor(0x80000000); hiddenMediaPaint.setColor(0x80000000);
PhotoLayoutHelper.TiledLayoutResult.Tile tile=imgHolder.getItem().thisTile; c.drawRect(child.getX(), child.getY(), child.getX()+child.getWidth(), child.getY()+child.getHeight(), hiddenMediaPaint);
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);
} }
} }
} }
for(int i=0;i<parent.getChildCount();i++){ for(int i=0;i<parent.getChildCount();i++){
View child=parent.getChildAt(i); View child=parent.getChildAt(i);
RecyclerView.ViewHolder holder=parent.getChildViewHolder(child); RecyclerView.ViewHolder holder=parent.getChildViewHolder(child);
if(holder instanceof ImageStatusDisplayItem.Holder<?> imgHolder){ if(holder instanceof MediaGridStatusDisplayItem.Holder imgHolder){
if(!imgHolder.getItem().status.spoilerRevealed){ if(!imgHolder.getItem().status.spoilerRevealed){
PhotoLayoutHelper.TiledLayoutResult.Tile tile=imgHolder.getItem().thisTile; if(TextUtils.isEmpty(imgHolder.getItem().status.spoilerText)){
if(tile.startCol==0 && tile.startRow==0 && TextUtils.isEmpty(imgHolder.getItem().status.spoilerText)){
int listWidth=getListWidthForMediaLayout(); 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) if(currentMediaHiddenLayoutsWidth!=width)
rebuildMediaHiddenLayouts(width-V.dp(32)); rebuildMediaHiddenLayouts(width-V.dp(32));
c.save(); c.save();
@ -829,47 +785,6 @@ public abstract class BaseStatusListFragment<T extends DisplayItemsParent> 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.colSpan<layout.columnSizes.length){
outRect.right=V.dp(1);
}
if(tile.startRow+tile.rowSpan<layout.rowSizes.length){
outRect.bottom=V.dp(1);
}
// For a view that spans rows, compensate its additional height so the row it's in stays the right height
if(tile.rowSpan>1){
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;i<tile.startCol;i++){
spanOffset+=layout.columnSizes[i];
}
outRect.left-=Math.round(spanOffset/1000f*listWidth);
outRect.left+=Math.round(spanOffset/1000f*width);
}
}
}
}
private void rebuildMediaHiddenLayouts(int width){ private void rebuildMediaHiddenLayouts(int width){
currentMediaHiddenLayoutsWidth=width; currentMediaHiddenLayoutsWidth=width;
String title=getString(R.string.sensitive_content); String title=getString(R.string.sensitive_content);

View File

@ -109,7 +109,7 @@ import org.joinmastodon.android.ui.text.ComposeAutocompleteSpan;
import org.joinmastodon.android.ui.text.ComposeHashtagOrMentionSpan; import org.joinmastodon.android.ui.text.ComposeHashtagOrMentionSpan;
import org.joinmastodon.android.ui.text.HtmlParser; import org.joinmastodon.android.ui.text.HtmlParser;
import org.joinmastodon.android.ui.utils.SimpleTextWatcher; import org.joinmastodon.android.ui.utils.SimpleTextWatcher;
import org.joinmastodon.android.ui.utils.TransferSpeedTracker; import org.joinmastodon.android.utils.TransferSpeedTracker;
import org.joinmastodon.android.ui.utils.UiUtils; import org.joinmastodon.android.ui.utils.UiUtils;
import org.joinmastodon.android.ui.views.ComposeEditText; import org.joinmastodon.android.ui.views.ComposeEditText;
import org.joinmastodon.android.ui.views.ComposeMediaLayout; import org.joinmastodon.android.ui.views.ComposeMediaLayout;
@ -333,7 +333,9 @@ public class ComposeFragment extends MastodonToolbarFragment implements OnBackPr
scheduleTimeBtn=view.findViewById(R.id.scheduled_time_btn); scheduleTimeBtn=view.findViewById(R.id.scheduled_time_btn);
sensitiveIcon=view.findViewById(R.id.sensitive_icon); sensitiveIcon=view.findViewById(R.id.sensitive_icon);
sensitiveItem=view.findViewById(R.id.sensitive_item); sensitiveItem=view.findViewById(R.id.sensitive_item);
replyText=view.findViewById(R.id.reply_text); replyText=view.findViewById(GlobalUserPreferences.replyLineAboveHeader ? R.id.reply_text : R.id.reply_text_below);
view.findViewById(GlobalUserPreferences.replyLineAboveHeader ? R.id.reply_text_below : R.id.reply_text)
.setVisibility(View.GONE);
if (isPhotoPickerAvailable()) { if (isPhotoPickerAvailable()) {
PopupMenu attachPopup = new PopupMenu(getContext(), mediaBtn); PopupMenu attachPopup = new PopupMenu(getContext(), mediaBtn);
@ -971,9 +973,11 @@ public class ComposeFragment extends MastodonToolbarFragment implements OnBackPr
} }
private void onCustomEmojiClick(Emoji emoji){ private void onCustomEmojiClick(Emoji emoji){
int start=mainEditText.getSelectionStart(); if(getActivity().getCurrentFocus() instanceof EditText edit){
String prefix=start>0 && !Character.isWhitespace(mainEditText.getText().charAt(start-1)) ? " :" : ":"; int start=edit.getSelectionStart();
mainEditText.getText().replace(start, mainEditText.getSelectionEnd(), prefix+emoji.shortcode+':'); String prefix=start>0 && !Character.isWhitespace(edit.getText().charAt(start-1)) ? " :" : ":";
edit.getText().replace(start, edit.getSelectionEnd(), prefix+emoji.shortcode+':');
}
} }
@Override @Override

View File

@ -358,7 +358,7 @@ public class HomeTabFragment extends MastodonToolbarFragment implements Scrollab
addListsToOverflowMenu(); addListsToOverflowMenu();
addHashtagsToOverflowMenu(); addHashtagsToOverflowMenu();
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.P) { if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.P && !UiUtils.isEMUI()) {
m.setGroupDividerEnabled(true); m.setGroupDividerEnabled(true);
} }
} }

View File

@ -19,9 +19,7 @@ import org.joinmastodon.android.model.Notification;
import org.joinmastodon.android.model.PaginatedResponse; import org.joinmastodon.android.model.PaginatedResponse;
import org.joinmastodon.android.model.Status; import org.joinmastodon.android.model.Status;
import org.joinmastodon.android.ui.displayitems.AccountCardStatusDisplayItem; 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.HeaderStatusDisplayItem;
import org.joinmastodon.android.ui.displayitems.ImageStatusDisplayItem;
import org.joinmastodon.android.ui.displayitems.StatusDisplayItem; import org.joinmastodon.android.ui.displayitems.StatusDisplayItem;
import org.joinmastodon.android.ui.displayitems.TextStatusDisplayItem; import org.joinmastodon.android.ui.displayitems.TextStatusDisplayItem;
import org.joinmastodon.android.ui.utils.DiscoverInfoBannerHelper; import org.joinmastodon.android.ui.utils.DiscoverInfoBannerHelper;
@ -40,7 +38,6 @@ import java.util.stream.Stream;
import androidx.recyclerview.widget.RecyclerView; import androidx.recyclerview.widget.RecyclerView;
import me.grishka.appkit.Nav; import me.grishka.appkit.Nav;
import me.grishka.appkit.api.SimpleCallback; import me.grishka.appkit.api.SimpleCallback;
import me.grishka.appkit.utils.V;
public class NotificationsListFragment extends BaseStatusListFragment<Notification>{ public class NotificationsListFragment extends BaseStatusListFragment<Notification>{
private boolean onlyMentions; private boolean onlyMentions;
@ -97,14 +94,9 @@ public class NotificationsListFragment extends BaseStatusListFragment<Notificati
}; };
HeaderStatusDisplayItem titleItem=extraText!=null ? new HeaderStatusDisplayItem(n.id, n.account, n.createdAt, this, accountID, n.status, extraText, n, null) : null; HeaderStatusDisplayItem titleItem=extraText!=null ? new HeaderStatusDisplayItem(n.id, n.account, n.createdAt, this, accountID, n.status, extraText, n, null) : null;
if(n.status!=null){ if(n.status!=null){
ArrayList<StatusDisplayItem> items=StatusDisplayItem.buildItems(this, n.status, accountID, n, knownAccounts, titleItem!=null, titleItem==null, n, false, Filter.FilterContext.NOTIFICATIONS, titleItem); ArrayList<StatusDisplayItem> items=StatusDisplayItem.buildItems(this, n.status, accountID, n, knownAccounts, titleItem!=null, titleItem==null, n, false, Filter.FilterContext.NOTIFICATIONS);
if(titleItem!=null){ if(titleItem!=null)
for(StatusDisplayItem item:items){ items.add(0, titleItem);
if(item instanceof ImageStatusDisplayItem imgItem){
imgItem.horizontalInset=V.dp(32);
}
}
}
return items; return items;
}else if(titleItem!=null){ }else if(titleItem!=null){
AccountCardStatusDisplayItem card=new AccountCardStatusDisplayItem(n.id, this, AccountCardStatusDisplayItem card=new AccountCardStatusDisplayItem(n.id, this,
@ -125,6 +117,8 @@ public class NotificationsListFragment extends BaseStatusListFragment<Notificati
knownAccounts.put(s.account.id, s.account); knownAccounts.put(s.account.id, s.account);
if(s.status!=null && !knownAccounts.containsKey(s.status.account.id)) if(s.status!=null && !knownAccounts.containsKey(s.status.account.id))
knownAccounts.put(s.status.account.id, s.status.account); knownAccounts.put(s.status.account.id, s.status.account);
if(s.status!=null && s.status.reblog!=null && !knownAccounts.containsKey(s.status.reblog.account.id))
knownAccounts.put(s.status.reblog.account.id, s.status.reblog.account);
} }
@Override @Override

View File

@ -80,7 +80,7 @@ public class SettingsFragment extends MastodonToolbarFragment{
private ArrayList<Item> items=new ArrayList<>(); private ArrayList<Item> items=new ArrayList<>();
private ThemeItem themeItem; private ThemeItem themeItem;
private NotificationPolicyItem notificationPolicyItem; private NotificationPolicyItem notificationPolicyItem;
private SwitchItem showNewPostsButtonItem, glitchModeItem; private SwitchItem showNewPostsButtonItem, glitchModeItem, compactReblogReplyLineItem;
private String accountID; private String accountID;
private boolean needUpdateNotificationSettings; private boolean needUpdateNotificationSettings;
private boolean needAppRestart; private boolean needAppRestart;
@ -253,6 +253,21 @@ public class SettingsFragment extends MastodonToolbarFragment{
GlobalUserPreferences.save(); GlobalUserPreferences.save();
needAppRestart=true; 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->{ 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.translateButtonOpenedOnly=i.checked;
GlobalUserPreferences.save(); GlobalUserPreferences.save();

View File

@ -41,6 +41,8 @@ public abstract class StatusListFragment extends BaseStatusListFragment<Status>{
protected void addAccountToKnown(Status s){ protected void addAccountToKnown(Status s){
if(!knownAccounts.containsKey(s.account.id)) if(!knownAccounts.containsKey(s.account.id))
knownAccounts.put(s.account.id, s.account); 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 @Override

View File

@ -22,10 +22,8 @@ import org.joinmastodon.android.events.FinishReportFragmentsEvent;
import org.joinmastodon.android.fragments.StatusListFragment; import org.joinmastodon.android.fragments.StatusListFragment;
import org.joinmastodon.android.model.Account; import org.joinmastodon.android.model.Account;
import org.joinmastodon.android.model.Status; 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.AudioStatusDisplayItem;
import org.joinmastodon.android.ui.displayitems.HeaderStatusDisplayItem; 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.LinkCardStatusDisplayItem;
import org.joinmastodon.android.ui.displayitems.ReblogOrReplyLineStatusDisplayItem; import org.joinmastodon.android.ui.displayitems.ReblogOrReplyLineStatusDisplayItem;
import org.joinmastodon.android.ui.displayitems.StatusDisplayItem; import org.joinmastodon.android.ui.displayitems.StatusDisplayItem;
@ -132,22 +130,7 @@ public class ReportAddPostsChoiceFragment extends StatusListFragment{
if(holder.getAbsoluteAdapterPosition()==0) if(holder.getAbsoluteAdapterPosition()==0)
return; return;
outRect.left=V.dp(40); outRect.left=V.dp(40);
if(holder instanceof ImageStatusDisplayItem.Holder<?> imgHolder){ if(holder instanceof AudioStatusDisplayItem.Holder){
PhotoLayoutHelper.TiledLayoutResult layout=imgHolder.getItem().tiledLayout;
PhotoLayoutHelper.TiledLayoutResult.Tile tile=imgHolder.getItem().thisTile;
String siblingID;
if(holder.getAbsoluteAdapterPosition()<parent.getAdapter().getItemCount()-1){
siblingID=displayItems.get(holder.getAbsoluteAdapterPosition()-getMainAdapterOffset()+1).parentID;
}else{
siblingID=null;
}
if(tile.startCol>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){
outRect.bottom=V.dp(16); outRect.bottom=V.dp(16);
}else if(holder instanceof LinkCardStatusDisplayItem.Holder){ }else if(holder instanceof LinkCardStatusDisplayItem.Holder){
outRect.bottom=V.dp(16); outRect.bottom=V.dp(16);
@ -166,10 +149,6 @@ public class ReportAddPostsChoiceFragment extends StatusListFragment{
parent.getDecoratedBoundsWithMargins(child, tmpRect); parent.getDecoratedBoundsWithMargins(child, tmpRect);
String id=sdiHolder.getItemID(); String id=sdiHolder.getItemID();
int height=tmpRect.height(); int height=tmpRect.height();
if(holder instanceof ImageStatusDisplayItem.Holder<?> imgHolder){
if(imgHolder.getItem().thisTile.startCol+imgHolder.getItem().thisTile.colSpan<imgHolder.getItem().tiledLayout.columnSizes.length)
height=0;
}
if(!(holder instanceof HeaderStatusDisplayItem.Holder) && !(holder instanceof ReblogOrReplyLineStatusDisplayItem.Holder)) if(!(holder instanceof HeaderStatusDisplayItem.Holder) && !(holder instanceof ReblogOrReplyLineStatusDisplayItem.Holder))
postsWithKnownNonHeaderHeights.add(id); postsWithKnownNonHeaderHeights.add(id);
knownDisplayItemHeights.put(holder.getAbsoluteAdapterPosition(), height); knownDisplayItemHeights.put(holder.getAbsoluteAdapterPosition(), height);
@ -236,17 +215,6 @@ public class ReportAddPostsChoiceFragment extends StatusListFragment{
return adapter; return adapter;
} }
@Override
protected List<StatusDisplayItem> buildDisplayItems(Status s){
List<StatusDisplayItem> 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){ protected void drawDivider(View child, View bottomSibling, RecyclerView.ViewHolder holder, RecyclerView.ViewHolder siblingHolder, RecyclerView parent, Canvas c, Paint paint){
parent.getDecoratedBoundsWithMargins(child, tmpRect); parent.getDecoratedBoundsWithMargins(child, tmpRect);
tmpRect.offset(0, Math.round(child.getTranslationY())); tmpRect.offset(0, Math.round(child.getTranslationY()));

View File

@ -16,6 +16,7 @@ public class Poll extends BaseModel{
private boolean expired; private boolean expired;
public boolean multiple; public boolean multiple;
public int votersCount; public int votersCount;
public int votesCount;
public boolean voted; public boolean voted;
@RequiredField @RequiredField
public List<Integer> ownVotes; public List<Integer> ownVotes;
@ -41,10 +42,12 @@ public class Poll extends BaseModel{
", expired="+expired+ ", expired="+expired+
", multiple="+multiple+ ", multiple="+multiple+
", votersCount="+votersCount+ ", votersCount="+votersCount+
", votesCount="+votesCount+
", voted="+voted+ ", voted="+voted+
", ownVotes="+ownVotes+ ", ownVotes="+ownVotes+
", options="+options+ ", options="+options+
", emojis="+emojis+ ", emojis="+emojis+
", selectedOptions="+selectedOptions+
'}'; '}';
} }

View File

@ -11,8 +11,14 @@ import java.util.List;
import androidx.annotation.NonNull; import androidx.annotation.NonNull;
public class PhotoLayoutHelper{ public class PhotoLayoutHelper{
public static final int MAX_WIDTH=1000;
public static final int MAX_HEIGHT=1910;
@NonNull @NonNull
public static TiledLayoutResult processThumbs(int _maxW, int _maxH, List<Attachment> thumbs){ public static TiledLayoutResult processThumbs(List<Attachment> thumbs){
int _maxW=MAX_WIDTH;
int _maxH=MAX_HEIGHT;
TiledLayoutResult result=new TiledLayoutResult(); TiledLayoutResult result=new TiledLayoutResult();
if(thumbs.size()==1){ if(thumbs.size()==1){
Attachment att=thumbs.get(0); Attachment att=thumbs.get(0);
@ -45,13 +51,8 @@ public class PhotoLayoutHelper{
float avgRatio=!ratios.isEmpty() ? sum(ratios)/ratios.size() : 1.0f; float avgRatio=!ratios.isEmpty() ? sum(ratios)/ratios.size() : 1.0f;
float maxW, maxH, marginW=0, marginH=0; float maxW, maxH, marginW=0, marginH=0;
if(_maxW>0){
maxW=_maxW; maxW=_maxW;
maxH=_maxH; maxH=_maxH;
}else{
maxW=510;
maxH=510;
}
float maxRatio=maxW/maxH; float maxRatio=maxW/maxH;

View File

@ -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<GifVStatusDisplayItem>{
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
}
});
}
}
}

View File

@ -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<T extends ImageStatusDisplayItem> extends StatusDisplayItem.Holder<T> 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);
}
}
}

View File

@ -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<GridItemType, MediaAttachmentViewController> viewPool;
private final List<Attachment> attachments;
private final ArrayList<ImageLoaderRequest> requests=new ArrayList<>();
public final Status status;
public MediaGridStatusDisplayItem(String parentID, BaseStatusListFragment<?> parentFragment, PhotoLayoutHelper.TiledLayoutResult tiledLayout, List<Attachment> 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<MediaGridStatusDisplayItem> implements ImageLoaderViewHolder{
private final FrameLayout wrapper;
private final MediaGridLayout layout;
private final View.OnClickListener clickListener=this::onViewClick, altTextClickListener=this::onAltTextClick;
private final ArrayList<MediaAttachmentViewController> 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<Animator> 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<Animator> 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);
}
}
}

View File

@ -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<PhotoStatusDisplayItem> {
public Holder(Activity activity, ViewGroup parent) {
super(activity, R.layout.display_item_photo, parent);
}
}
}

View File

@ -35,8 +35,9 @@ public class PollOptionStatusDisplayItem extends StatusDisplayItem{
text=HtmlParser.parseCustomEmoji(option.title, poll.emojis); text=HtmlParser.parseCustomEmoji(option.title, poll.emojis);
emojiHelper.setText(text); emojiHelper.setText(text);
showResults=poll.isExpired() || poll.voted; showResults=poll.isExpired() || poll.voted;
if(showResults && option.votesCount!=null && poll.votersCount>0){ int total=poll.votersCount>0 ? poll.votersCount : poll.votesCount;
votesFraction=(float)option.votesCount/(float)poll.votersCount; if(showResults && option.votesCount!=null && total>0){
votesFraction=(float)option.votesCount/(float)total;
int mostVotedCount=0; int mostVotedCount=0;
for(Poll.Option opt:poll.options) for(Poll.Option opt:poll.options)
mostVotedCount=Math.max(mostVotedCount, opt.votesCount); mostVotedCount=Math.max(mostVotedCount, opt.votesCount);

View File

@ -10,8 +10,10 @@ import android.text.SpannableStringBuilder;
import android.util.TypedValue; import android.util.TypedValue;
import android.view.View; import android.view.View;
import android.view.ViewGroup; import android.view.ViewGroup;
import android.widget.LinearLayout;
import android.widget.TextView; import android.widget.TextView;
import org.joinmastodon.android.GlobalUserPreferences;
import org.joinmastodon.android.R; import org.joinmastodon.android.R;
import org.joinmastodon.android.fragments.BaseStatusListFragment; import org.joinmastodon.android.fragments.BaseStatusListFragment;
import org.joinmastodon.android.model.Emoji; 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.ImageLoaderViewHolder;
import me.grishka.appkit.imageloader.requests.ImageLoaderRequest; import me.grishka.appkit.imageloader.requests.ImageLoaderRequest;
import me.grishka.appkit.utils.V;
public class ReblogOrReplyLineStatusDisplayItem extends StatusDisplayItem{ public class ReblogOrReplyLineStatusDisplayItem extends StatusDisplayItem{
private CharSequence text; private CharSequence text;
@ -37,6 +40,8 @@ public class ReblogOrReplyLineStatusDisplayItem extends StatusDisplayItem{
private int iconEnd; private int iconEnd;
private CustomEmojiHelper emojiHelper=new CustomEmojiHelper(); private CustomEmojiHelper emojiHelper=new CustomEmojiHelper();
private View.OnClickListener handleClick; private View.OnClickListener handleClick;
boolean belowHeader, needBottomPadding;
ReblogOrReplyLineStatusDisplayItem extra;
public ReblogOrReplyLineStatusDisplayItem(String parentID, BaseStatusListFragment parentFragment, CharSequence text, List<Emoji> emojis, @DrawableRes int icon, StatusPrivacy visibility, @Nullable View.OnClickListener handleClick) { public ReblogOrReplyLineStatusDisplayItem(String parentID, BaseStatusListFragment parentFragment, CharSequence text, List<Emoji> emojis, @DrawableRes int icon, StatusPrivacy visibility, @Nullable View.OnClickListener handleClick) {
super(parentID, parentFragment); super(parentID, parentFragment);
@ -77,29 +82,57 @@ public class ReblogOrReplyLineStatusDisplayItem extends StatusDisplayItem{
} }
public static class Holder extends StatusDisplayItem.Holder<ReblogOrReplyLineStatusDisplayItem> implements ImageLoaderViewHolder{ public static class Holder extends StatusDisplayItem.Holder<ReblogOrReplyLineStatusDisplayItem> implements ImageLoaderViewHolder{
private final TextView text; private final TextView text, extraText;
private final View separator;
public Holder(Activity activity, ViewGroup parent){ public Holder(Activity activity, ViewGroup parent){
super(activity, R.layout.display_item_reblog_or_reply_line, parent); super(activity, R.layout.display_item_reblog_or_reply_line, parent);
text=findViewById(R.id.text); 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 private void bindLine(ReblogOrReplyLineStatusDisplayItem item, TextView text) {
public void onBind(ReblogOrReplyLineStatusDisplayItem item){
text.setText(item.text); text.setText(item.text);
text.setCompoundDrawablesRelativeWithIntrinsicBounds(item.icon, 0, item.iconEnd, 0); text.setCompoundDrawablesRelativeWithIntrinsicBounds(item.icon, 0, item.iconEnd, 0);
if(item.handleClick!=null) text.setOnClickListener(item.handleClick); text.setOnClickListener(item.handleClick);
text.setEnabled(!item.inset); text.setEnabled(!item.inset && item.handleClick != null);
text.setClickable(!item.inset); text.setClickable(!item.inset && item.handleClick != null);
Context ctx = itemView.getContext(); Context ctx = itemView.getContext();
int visibilityText = item.visibility != null ? switch (item.visibility) { int visibilityText = item.visibility != null ? switch (item.visibility) {
case PUBLIC -> R.string.visibility_public; case PUBLIC -> R.string.visibility_public;
case UNLISTED -> R.string.sk_visibility_unlisted; case UNLISTED -> R.string.sk_visibility_unlisted;
case PRIVATE -> R.string.visibility_followers_only; case PRIVATE -> R.string.visibility_followers_only;
case LOCAL -> R.string.sk_local_only;
default -> 0; default -> 0;
} : 0; } : 0;
if (visibilityText != 0) text.setContentDescription(item.text + " (" + ctx.getString(visibilityText) + ")"); if (visibilityText != 0) text.setContentDescription(item.text + " (" + ctx.getString(visibilityText) + ")");
if(Build.VERSION.SDK_INT<Build.VERSION_CODES.N) if(Build.VERSION.SDK_INT<Build.VERSION_CODES.N)
UiUtils.fixCompoundDrawableTintOnAndroid6(text); UiUtils.fixCompoundDrawableTintOnAndroid6(text);
text.setTextAppearance(item.belowHeader ? R.style.m3_label_large : R.style.m3_title_small);
text.setCompoundDrawableTintList(text.getTextColors());
}
@Override
public void onBind(ReblogOrReplyLineStatusDisplayItem item){
bindLine(item, text);
if (item.extra != null) bindLine(item.extra, extraText);
extraText.setVisibility(item.extra == null ? View.GONE : View.VISIBLE);
separator.setVisibility(item.extra == null ? View.GONE : View.VISIBLE);
ViewGroup.MarginLayoutParams params = new ViewGroup.MarginLayoutParams(ViewGroup.LayoutParams.MATCH_PARENT, ViewGroup.LayoutParams.WRAP_CONTENT);
params.bottomMargin = item.belowHeader ? V.dp(-6) : V.dp(-12);
params.topMargin = item.belowHeader ? V.dp(-6) : 0;
itemView.setLayoutParams(params);
itemView.setPadding(itemView.getPaddingLeft(), itemView.getPaddingTop(), itemView.getPaddingRight(), item.needBottomPadding ? V.dp(16) : 0);
} }
@Override @Override

View File

@ -7,12 +7,12 @@ import android.text.TextUtils;
import android.view.View; import android.view.View;
import android.view.ViewGroup; import android.view.ViewGroup;
import org.joinmastodon.android.GlobalUserPreferences;
import org.joinmastodon.android.R; import org.joinmastodon.android.R;
import org.joinmastodon.android.api.session.AccountSessionManager; import org.joinmastodon.android.api.session.AccountSessionManager;
import org.joinmastodon.android.fragments.BaseStatusListFragment; import org.joinmastodon.android.fragments.BaseStatusListFragment;
import org.joinmastodon.android.fragments.HashtagTimelineFragment; import org.joinmastodon.android.fragments.HashtagTimelineFragment;
import org.joinmastodon.android.fragments.HomeTabFragment; import org.joinmastodon.android.fragments.HomeTabFragment;
import org.joinmastodon.android.fragments.HomeTimelineFragment;
import org.joinmastodon.android.fragments.ListTimelineFragment; import org.joinmastodon.android.fragments.ListTimelineFragment;
import org.joinmastodon.android.fragments.ProfileFragment; import org.joinmastodon.android.fragments.ProfileFragment;
import org.joinmastodon.android.fragments.ThreadFragment; import org.joinmastodon.android.fragments.ThreadFragment;
@ -20,7 +20,6 @@ import org.joinmastodon.android.model.Account;
import org.joinmastodon.android.model.Attachment; import org.joinmastodon.android.model.Attachment;
import org.joinmastodon.android.model.DisplayItemsParent; import org.joinmastodon.android.model.DisplayItemsParent;
import org.joinmastodon.android.model.Filter; import org.joinmastodon.android.model.Filter;
import org.joinmastodon.android.model.Hashtag;
import org.joinmastodon.android.model.Notification; import org.joinmastodon.android.model.Notification;
import org.joinmastodon.android.model.Poll; import org.joinmastodon.android.model.Poll;
import org.joinmastodon.android.model.ScheduledStatus; import org.joinmastodon.android.model.ScheduledStatus;
@ -31,10 +30,8 @@ import org.joinmastodon.android.utils.StatusFilterPredicate;
import org.parceler.Parcels; import org.parceler.Parcels;
import java.util.ArrayList; import java.util.ArrayList;
import java.util.Arrays;
import java.util.List; import java.util.List;
import java.util.Map; import java.util.Map;
import java.util.Objects;
import java.util.Optional; import java.util.Optional;
import java.util.stream.Collectors; import java.util.stream.Collectors;
@ -69,10 +66,7 @@ public abstract class StatusDisplayItem{
case HEADER -> new HeaderStatusDisplayItem.Holder(activity, parent); case HEADER -> new HeaderStatusDisplayItem.Holder(activity, parent);
case REBLOG_OR_REPLY_LINE -> new ReblogOrReplyLineStatusDisplayItem.Holder(activity, parent); case REBLOG_OR_REPLY_LINE -> new ReblogOrReplyLineStatusDisplayItem.Holder(activity, parent);
case TEXT -> new TextStatusDisplayItem.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 AUDIO -> new AudioStatusDisplayItem.Holder(activity, parent);
case VIDEO -> new VideoStatusDisplayItem.Holder(activity, parent);
case POLL_OPTION -> new PollOptionStatusDisplayItem.Holder(activity, parent); case POLL_OPTION -> new PollOptionStatusDisplayItem.Holder(activity, parent);
case POLL_FOOTER -> new PollFooterStatusDisplayItem.Holder(activity, parent); case POLL_FOOTER -> new PollFooterStatusDisplayItem.Holder(activity, parent);
case CARD -> new LinkCardStatusDisplayItem.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 HASHTAG -> new HashtagStatusDisplayItem.Holder(activity, parent);
case GAP -> new GapStatusDisplayItem.Holder(activity, parent); case GAP -> new GapStatusDisplayItem.Holder(activity, parent);
case EXTENDED_FOOTER -> new ExtendedFooterStatusDisplayItem.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); case WARNING -> new WarningFilteredStatusDisplayItem.Holder(activity, parent);
}; };
} }
@ -99,10 +94,6 @@ public abstract class StatusDisplayItem{
} }
public static ArrayList<StatusDisplayItem> buildItems(BaseStatusListFragment<?> fragment, Status status, String accountID, DisplayItemsParent parentObject, Map<String, Account> knownAccounts, boolean inset, boolean addFooter, Notification notification, boolean disableTranslate, Filter.FilterContext filterContext){ public static ArrayList<StatusDisplayItem> buildItems(BaseStatusListFragment<?> fragment, Status status, String accountID, DisplayItemsParent parentObject, Map<String, Account> 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<StatusDisplayItem> buildItems(BaseStatusListFragment<?> fragment, Status status, String accountID, DisplayItemsParent parentObject, Map<String, Account> knownAccounts, boolean inset, boolean addFooter, Notification notification, boolean disableTranslate, Filter.FilterContext filterContext, StatusDisplayItem titleItem){
String parentID=parentObject.getID(); String parentID=parentObject.getID();
ArrayList<StatusDisplayItem> items=new ArrayList<>(); ArrayList<StatusDisplayItem> items=new ArrayList<>();
@ -119,24 +110,36 @@ public abstract class StatusDisplayItem{
statusForContent.filterRevealed = filterPredicate.testWithWarning(status); 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){ if(status.reblog!=null){
boolean isOwnPost = AccountSessionManager.getInstance().isSelf(fragment.getAccountID(), status.account); 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)); args.putParcelable("profileAccount", Parcels.wrap(status.account));
Nav.go(fragment.getActivity(), ProfileFragment.class, args); Nav.go(fragment.getActivity(), ProfileFragment.class, args);
})); }));
}else if(status.inReplyToAccountId!=null && knownAccounts.containsKey(status.inReplyToAccountId)){ } else if (!(status.tags.isEmpty() ||
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 HashtagTimelineFragment ||
fragment instanceof ListTimelineFragment fragment instanceof ListTimelineFragment
) && fragment.getParentFragment() instanceof HomeTabFragment home ) && fragment.getParentFragment() instanceof HomeTabFragment home) {
) {
home.getHashtags().stream() home.getHashtags().stream()
.filter(followed -> status.tags.stream() .filter(followed -> status.tags.stream()
.anyMatch(hashtag -> followed.name.equalsIgnoreCase(hashtag.name))) .anyMatch(hashtag -> followed.name.equalsIgnoreCase(hashtag.name)))
@ -151,28 +154,38 @@ public abstract class StatusDisplayItem{
} }
))); )));
} }
if (replyLine != null && GlobalUserPreferences.replyLineAboveHeader) {
Optional<ReblogOrReplyLineStatusDisplayItem> 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; HeaderStatusDisplayItem header;
items.add(header=new HeaderStatusDisplayItem(parentID, statusForContent.account, statusForContent.createdAt, fragment, accountID, statusForContent, null, notification, scheduledStatus)); 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)) if(!TextUtils.isEmpty(statusForContent.content))
items.add(new TextStatusDisplayItem(parentID, HtmlParser.parse(statusForContent.content, statusForContent.emojis, statusForContent.mentions, statusForContent.tags, accountID), fragment, statusForContent, disableTranslate)); 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 else
header.needBottomPadding=true; header.needBottomPadding=true;
List<Attachment> imageAttachments=statusForContent.mediaAttachments.stream().filter(att->att.type.isImage()).collect(Collectors.toList()); List<Attachment> imageAttachments=statusForContent.mediaAttachments.stream().filter(att->att.type.isImage()).collect(Collectors.toList());
if(!imageAttachments.isEmpty()){ if(!imageAttachments.isEmpty()){
int photoIndex=0; PhotoLayoutHelper.TiledLayoutResult layout=PhotoLayoutHelper.processThumbs(imageAttachments);
PhotoLayoutHelper.TiledLayoutResult layout=PhotoLayoutHelper.processThumbs(1000, 1910, imageAttachments); items.add(new MediaGridStatusDisplayItem(parentID, fragment, layout, imageAttachments, statusForContent));
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++;
}
} }
for(Attachment att:statusForContent.mediaAttachments){ for(Attachment att:statusForContent.mediaAttachments){
if(att.type==Attachment.Type.AUDIO){ if(att.type==Attachment.Type.AUDIO){
@ -196,8 +209,6 @@ public abstract class StatusDisplayItem{
item.index=i++; item.index=i++;
} }
if (titleItem != null) items.add(0, titleItem);
if (!statusForContent.filterRevealed) { if (!statusForContent.filterRevealed) {
return new ArrayList<>(List.of( return new ArrayList<>(List.of(
new WarningFilteredStatusDisplayItem(parentID, fragment, statusForContent, items) new WarningFilteredStatusDisplayItem(parentID, fragment, statusForContent, items)
@ -218,9 +229,6 @@ public abstract class StatusDisplayItem{
HEADER, HEADER,
REBLOG_OR_REPLY_LINE, REBLOG_OR_REPLY_LINE,
TEXT, TEXT,
PHOTO,
VIDEO,
GIFV,
AUDIO, AUDIO,
POLL_OPTION, POLL_OPTION,
POLL_FOOTER, POLL_FOOTER,
@ -230,8 +238,9 @@ public abstract class StatusDisplayItem{
ACCOUNT, ACCOUNT,
HASHTAG, HASHTAG,
GAP, GAP,
WARNING, EXTENDED_FOOTER,
EXTENDED_FOOTER MEDIA_GRID,
WARNING
} }
public static abstract class Holder<T extends StatusDisplayItem> extends BindableViewHolder<T> implements UsableRecyclerView.DisableableClickable{ public static abstract class Holder<T extends StatusDisplayItem> extends BindableViewHolder<T> implements UsableRecyclerView.DisableableClickable{

View File

@ -6,6 +6,7 @@ import android.graphics.drawable.Drawable;
import android.text.TextUtils; import android.text.TextUtils;
import android.view.View; import android.view.View;
import android.view.ViewGroup; import android.view.ViewGroup;
import android.view.ViewTreeObserver;
import android.widget.LinearLayout; import android.widget.LinearLayout;
import android.widget.Button; import android.widget.Button;
import android.widget.ScrollView; import android.widget.ScrollView;
@ -227,13 +228,20 @@ public class TextStatusDisplayItem extends StatusDisplayItem{
readMore.setVisibility(View.GONE); readMore.setVisibility(View.GONE);
} }
if (GlobalUserPreferences.collapseLongPosts) text.post(() -> { if (GlobalUserPreferences.collapseLongPosts) {
text.getViewTreeObserver().addOnPreDrawListener(new ViewTreeObserver.OnPreDrawListener() {
@Override
public boolean onPreDraw() {
text.getViewTreeObserver().removeOnPreDrawListener(this);
boolean tooBig = text.getMeasuredHeight() > textMaxHeight; boolean tooBig = text.getMeasuredHeight() > textMaxHeight;
boolean inTimeline = !item.textSelectable; boolean inTimeline = !item.textSelectable;
boolean hasSpoiler = !TextUtils.isEmpty(item.status.spoilerText); boolean hasSpoiler = !TextUtils.isEmpty(item.status.spoilerText);
boolean expandable = inTimeline && tooBig && !hasSpoiler; boolean expandable = inTimeline && tooBig && !hasSpoiler;
item.parentFragment.onEnableExpandable(this, expandable); item.parentFragment.onEnableExpandable(Holder.this, expandable);
return true;
}
}); });
}
readMore.setVisibility(item.status.textExpandable && !item.status.textExpanded ? View.VISIBLE : View.GONE); readMore.setVisibility(item.status.textExpandable && !item.status.textExpanded ? View.VISIBLE : View.GONE);
textScrollView.setLayoutParams(item.status.textExpandable && !item.status.textExpanded ? collapseParams : wrapParams); textScrollView.setLayoutParams(item.status.textExpandable && !item.status.textExpanded ? collapseParams : wrapParams);

View File

@ -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<VideoStatusDisplayItem>{
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
}
});
}
}
}

View File

@ -1,7 +1,8 @@
package org.joinmastodon.android.ui.photoviewer; package org.joinmastodon.android.ui.photoviewer;
import org.joinmastodon.android.model.Status; import org.joinmastodon.android.model.Status;
import org.joinmastodon.android.ui.displayitems.MediaGridStatusDisplayItem;
public interface PhotoViewerHost{ public interface PhotoViewerHost{
void openPhotoViewer(String parentID, Status status, int attachmentIndex); void openPhotoViewer(String parentID, Status status, int attachmentIndex, MediaGridStatusDisplayItem.Holder gridHolder);
} }

View File

@ -8,24 +8,25 @@ import android.graphics.Rect;
import android.graphics.RectF; import android.graphics.RectF;
import android.text.Layout; import android.text.Layout;
import android.text.Spanned; import android.text.Spanned;
import android.view.GestureDetector;
import android.view.MotionEvent; import android.view.MotionEvent;
import android.view.SoundEffectConstants; import android.view.SoundEffectConstants;
import android.view.View;
import android.view.ViewConfiguration;
import android.widget.TextView; import android.widget.TextView;
import androidx.annotation.NonNull;
import org.joinmastodon.android.ui.utils.UiUtils;
import me.grishka.appkit.utils.V; import me.grishka.appkit.utils.V;
public class ClickableLinksDelegate { public class ClickableLinksDelegate {
private Paint hlPaint; private final Paint hlPaint;
private Path hlPath; private Path hlPath;
private LinkSpan selectedSpan; private LinkSpan selectedSpan;
private TextView view; private final TextView view;
private final Runnable longClickRunnable = () -> { private final GestureDetector gestureDetector;
if (selectedSpan != null) selectedSpan.onLongClick(view);
};
public ClickableLinksDelegate(TextView view) { public ClickableLinksDelegate(TextView view) {
this.view=view; this.view=view;
@ -33,11 +34,45 @@ public class ClickableLinksDelegate {
hlPaint.setAntiAlias(true); hlPaint.setAntiAlias(true);
hlPaint.setPathEffect(new CornerPathEffect(V.dp(3))); hlPaint.setPathEffect(new CornerPathEffect(V.dp(3)));
// view.setHighlightColor(view.getResources().getColor(android.R.color.holo_blue_light)); // 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) { public boolean onTouch(MotionEvent event) {
long eventDuration = event.getEventTime() - event.getDownTime(); if(event.getAction()==MotionEvent.ACTION_CANCEL){
if(event.getAction()==MotionEvent.ACTION_DOWN){ // 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; int line=-1;
Rect rect=new Rect(); Rect rect=new Rect();
Layout l=view.getLayout(); Layout l=view.getLayout();
@ -52,8 +87,7 @@ public class ClickableLinksDelegate {
return false; return false;
} }
CharSequence text=view.getText(); CharSequence text=view.getText();
if(text instanceof Spanned){ if(text instanceof Spanned s){
Spanned s=(Spanned)text;
LinkSpan[] spans=s.getSpans(0, s.length()-1, LinkSpan.class); LinkSpan[] spans=s.getSpans(0, s.length()-1, LinkSpan.class);
if(spans.length>0){ if(spans.length>0){
for(LinkSpan span:spans){ for(LinkSpan span:spans){
@ -70,7 +104,6 @@ public class ClickableLinksDelegate {
} }
hlPath=new Path(); hlPath=new Path();
selectedSpan=span; selectedSpan=span;
view.postDelayed(longClickRunnable, ViewConfiguration.getLongPressTimeout());
hlPaint.setColor((span.getColor() & 0x00FFFFFF) | 0x33000000); hlPaint.setColor((span.getColor() & 0x00FFFFFF) | 0x33000000);
//l.getSelectionPath(start, end, hlPath); //l.getSelectionPath(start, end, hlPath);
for(int j=lstart;j<=lend;j++){ 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); view.playSoundEffect(SoundEffectConstants.CLICK);
selectedSpan.onClick(view.getContext()); selectedSpan.onClick(view.getContext());
} resetAndInvalidate();
view.removeCallbacks(longClickRunnable); return true;
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; return false;
} }
public void onDraw(Canvas canvas){ @Override
if(hlPath!=null){ public void onLongPress(@NonNull MotionEvent event) {
canvas.save(); if (selectedSpan == null) return;
canvas.translate(0, view.getPaddingTop()); UiUtils.copyText(view, selectedSpan.getType() == LinkSpan.Type.URL ? selectedSpan.getLink() : selectedSpan.getText());
canvas.drawPath(hlPath, hlPaint); //reset view
canvas.restore(); resetAndInvalidate();
} }
} }
} }

View File

@ -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(){ public String getLink(){
return link; return link;
} }
public String getText() {
return text;
}
public Type getType(){ public Type getType(){
return type; return type;
} }

View File

@ -8,10 +8,8 @@ import android.view.View;
import org.joinmastodon.android.R; import org.joinmastodon.android.R;
import org.joinmastodon.android.fragments.BaseStatusListFragment; 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.LinkCardStatusDisplayItem;
import org.joinmastodon.android.ui.displayitems.MediaGridStatusDisplayItem;
import org.joinmastodon.android.ui.displayitems.StatusDisplayItem; import org.joinmastodon.android.ui.displayitems.StatusDisplayItem;
import java.util.List; import java.util.List;
@ -87,21 +85,11 @@ public class InsetStatusItemDecoration extends RecyclerView.ItemDecoration{
boolean topSiblingInset=pos>0 && displayItems.get(pos-1).inset; boolean topSiblingInset=pos>0 && displayItems.get(pos-1).inset;
boolean bottomSiblingInset=pos<displayItems.size()-1 && displayItems.get(pos+1).inset; boolean bottomSiblingInset=pos<displayItems.size()-1 && displayItems.get(pos+1).inset;
int pad; int pad;
if(holder instanceof ImageStatusDisplayItem.Holder || holder instanceof LinkCardStatusDisplayItem.Holder) if(holder instanceof MediaGridStatusDisplayItem.Holder || holder instanceof LinkCardStatusDisplayItem.Holder)
pad=V.dp(16); pad=V.dp(16);
else else
pad=V.dp(12); pad=V.dp(12);
boolean insetLeft=true, insetRight=true; boolean insetLeft=true, insetRight=true;
if(holder instanceof ImageStatusDisplayItem.Holder<?> 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) if(insetLeft)
outRect.left=pad; outRect.left=pad;
if(insetRight) if(insetRight)

View File

@ -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);
}
}

View File

@ -694,6 +694,9 @@ public class UiUtils {
TypedArray ta=button.getContext().obtainStyledAttributes(styleRes, new int[]{android.R.attr.background}); TypedArray ta=button.getContext().obtainStyledAttributes(styleRes, new int[]{android.R.attr.background});
button.setBackground(ta.getDrawable(0)); button.setBackground(ta.getDrawable(0));
ta.recycle(); 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<Boolean> progressCallback, Consumer<Relationship> resultCallback) { public static void performAccountAction(Activity activity, Account account, String accountID, Relationship relationship, Button button, Consumer<Boolean> progressCallback, Consumer<Relationship> resultCallback) {
@ -1143,6 +1146,10 @@ public class UiUtils {
return !TextUtils.isEmpty(getSystemProperty("ro.miui.ui.version.code")); 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) { public static int alphaBlendColors(int color1, int color2, float alpha) {
float alpha0 = 1f - alpha; float alpha0 = 1f - alpha;
int r = Math.round(((color1 >> 16) & 0xFF) * alpha0 + ((color2 >> 16) & 0xFF) * alpha); int r = Math.round(((color1 >> 16) & 0xFF) * alpha0 + ((color2 >> 16) & 0xFF) * alpha);

View File

@ -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);
}
}

View File

@ -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.colSpan<tileLayout.columnSizes.length)
actualWidth-=V.dp(1);
heightMeasureSpec=actualHeight | MeasureSpec.EXACTLY;
widthMeasureSpec=actualWidth | MeasureSpec.EXACTLY;
super.onMeasure(widthMeasureSpec, heightMeasureSpec);
}
public void setLayout(PhotoLayoutHelper.TiledLayoutResult layout, PhotoLayoutHelper.TiledLayoutResult.Tile tile, int horizontalInset){
tileLayout=layout;
this.tile=tile;
this.horizontalInset=horizontalInset;
}
}

View File

@ -0,0 +1,105 @@
package org.joinmastodon.android.ui.views;
import android.content.Context;
import android.util.AttributeSet;
import android.view.View;
import android.view.ViewGroup;
import org.joinmastodon.android.ui.PhotoLayoutHelper;
import me.grishka.appkit.utils.V;
public class MediaGridLayout extends ViewGroup{
private static final String TAG="MediaGridLayout";
public static final int MAX_WIDTH=400; // dp
private static final int GAP=1; // dp
private PhotoLayoutHelper.TiledLayoutResult tiledLayout;
private int[] columnStarts=new int[10], columnEnds=new int[10], rowStarts=new int[10], rowEnds=new int[10];
public MediaGridLayout(Context context){
this(context, null);
}
public MediaGridLayout(Context context, AttributeSet attrs){
this(context, attrs, 0);
}
public MediaGridLayout(Context context, AttributeSet attrs, int defStyle){
super(context, attrs, defStyle);
}
@Override
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec){
if(tiledLayout==null){
setMeasuredDimension(MeasureSpec.getSize(widthMeasureSpec), 0);
return;
}
int width=Math.min(V.dp(MAX_WIDTH), MeasureSpec.getSize(widthMeasureSpec));
int height=Math.round(width*(tiledLayout.height/(float)PhotoLayoutHelper.MAX_WIDTH));
int offset=0;
for(int i=0;i<tiledLayout.columnSizes.length;i++){
columnStarts[i]=offset;
offset+=Math.round(tiledLayout.columnSizes[i]/(float)tiledLayout.width*width);
columnEnds[i]=offset;
offset+=V.dp(GAP);
}
columnEnds[tiledLayout.columnSizes.length-1]=width;
offset=0;
for(int i=0;i<tiledLayout.rowSizes.length;i++){
rowStarts[i]=offset;
offset+=Math.round(tiledLayout.rowSizes[i]/(float)tiledLayout.height*height);
rowEnds[i]=offset;
offset+=V.dp(GAP);
}
rowEnds[tiledLayout.rowSizes.length-1]=height;
for(int i=0;i<getChildCount();i++){
View child=getChildAt(i);
LayoutParams lp=(LayoutParams) child.getLayoutParams();
int colSpan=Math.max(1, lp.tile.colSpan)-1;
int rowSpan=Math.max(1, lp.tile.rowSpan)-1;
int w=columnEnds[lp.tile.startCol+colSpan]-columnStarts[lp.tile.startCol];
int h=rowEnds[lp.tile.startRow+rowSpan]-rowStarts[lp.tile.startRow];
child.measure(w | MeasureSpec.EXACTLY, h | MeasureSpec.EXACTLY);
}
setMeasuredDimension(MeasureSpec.getSize(widthMeasureSpec), height);
}
@Override
protected void onLayout(boolean changed, int l, int t, int r, int b){
if(tiledLayout==null)
return;
int maxWidth=V.dp(MAX_WIDTH);
int xOffset=0;
if(r-l>maxWidth){
xOffset=(r-l)/2-maxWidth/2;
}
for(int i=0;i<getChildCount();i++){
View child=getChildAt(i);
LayoutParams lp=(LayoutParams) child.getLayoutParams();
int colSpan=Math.max(1, lp.tile.colSpan)-1;
int rowSpan=Math.max(1, lp.tile.rowSpan)-1;
child.layout(columnStarts[lp.tile.startCol]+xOffset, rowStarts[lp.tile.startRow], columnEnds[lp.tile.startCol+colSpan]+xOffset, rowEnds[lp.tile.startRow+rowSpan]);
}
}
public void setTiledLayout(PhotoLayoutHelper.TiledLayoutResult tiledLayout){
this.tiledLayout=tiledLayout;
requestLayout();
}
public static class LayoutParams extends ViewGroup.LayoutParams{
public PhotoLayoutHelper.TiledLayoutResult.Tile tile;
public LayoutParams(PhotoLayoutHelper.TiledLayoutResult.Tile tile){
super(WRAP_CONTENT, WRAP_CONTENT);
this.tile=tile;
}
}
}

View File

@ -1,4 +1,4 @@
package org.joinmastodon.android.ui.utils; package org.joinmastodon.android.utils;
import android.os.SystemClock; import android.os.SystemClock;

View File

@ -0,0 +1,36 @@
package org.joinmastodon.android.utils;
import java.util.HashMap;
import java.util.LinkedList;
import java.util.Objects;
import java.util.function.Function;
public class TypedObjectPool<K, V>{
private final Function<K, V> producer;
private final HashMap<K, LinkedList<V>> pool=new HashMap<>();
public TypedObjectPool(Function<K, V> producer){
this.producer=producer;
}
public V obtain(K type){
LinkedList<V> 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<V> tp=pool.get(type);
if(tp==null)
pool.put(type, tp=new LinkedList<>());
tp.add(obj);
}
}

View File

@ -0,0 +1,3 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android" android:width="24dp" android:height="24dp" android:viewportWidth="24" android:viewportHeight="24">
<path android:pathData="M2.75 13.25h18.5c0.414 0 0.75 0.336 0.75 0.75 0 0.38-0.282 0.694-0.648 0.743L21.25 14.75H2.75C2.336 14.75 2 14.414 2 14c0-0.38 0.282-0.694 0.648-0.743L2.75 13.25h18.5-18.5zm0-4h18.5C21.664 9.25 22 9.586 22 10c0 0.38-0.282 0.694-0.648 0.743L21.25 10.75H2.75C2.336 10.75 2 10.414 2 10c0-0.38 0.282-0.694 0.648-0.743L2.75 9.25h18.5-18.5z" android:fillColor="@color/fluent_default_icon_tint"/>
</vector>

View File

@ -1,79 +1,34 @@
<?xml version="1.0" encoding="utf-8"?> <?xml version="1.0" encoding="utf-8"?>
<!-- This is hidden from screenreaders because that same alt text is set as content description on the ImageView --> <FrameLayout xmlns:android="http://schemas.android.com/apk/res/android"
<FrameLayout android:id="@+id/alt_badges"
xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools"
android:id="@+id/alt_text_wrapper"
android:layout_width="wrap_content" android:layout_width="wrap_content"
android:layout_height="wrap_content" android:layout_height="wrap_content"
android:layout_gravity="start|bottom" android:layout_gravity="start|bottom"
android:layout_margin="12dp" android:padding="12dp"
android:importantForAccessibility="noHideDescendants" android:importantForAccessibility="noHideDescendants">
android:background="@drawable/bg_image_alt_overlay">
<ImageView <ImageView
android:id="@+id/no_alt_button" android:id="@+id/no_alt_button"
android:layout_width="wrap_content" android:layout_width="wrap_content"
android:layout_height="wrap_content" android:layout_height="wrap_content"
android:layout_gravity="bottom"
android:padding="4dp" android:padding="4dp"
android:src="@drawable/ic_fluent_important_20_filled" android:src="@drawable/ic_fluent_important_20_filled"
android:background="@drawable/bg_image_no_alt_overlay"
android:tint="?colorGray25" /> android:tint="?colorGray25" />
<TextView <TextView
android:id="@+id/alt_button" android:id="@+id/alt_button"
android:layout_width="wrap_content" android:layout_width="wrap_content"
android:layout_height="wrap_content" android:layout_height="wrap_content"
android:layout_gravity="bottom"
android:textAppearance="@style/m3_label_large" android:textAppearance="@style/m3_label_large"
android:textColor="?colorGray25" android:textColor="?colorGray25"
android:gravity="center" android:gravity="center"
android:includeFontPadding="false" android:includeFontPadding="false"
android:paddingHorizontal="5dp" android:paddingHorizontal="5dp"
android:paddingVertical="1dp" android:paddingVertical="1dp"
android:background="@drawable/bg_image_alt_overlay"
android:text="@string/sk_alt_button"/> android:text="@string/sk_alt_button"/>
<ImageButton
android:id="@+id/alt_text_close"
android:layout_width="40dp"
android:layout_height="40dp"
android:layout_gravity="end|top"
android:src="@drawable/ic_baseline_close_24"
android:tint="#FFF"
android:background="?android:actionBarItemBackground"/>
<org.joinmastodon.android.ui.views.NestableScrollView
android:id="@+id/alt_text_scroller"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_gravity="center_vertical"
android:layout_marginEnd="40dp">
<LinearLayout
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:orientation="vertical">
<TextView
android:id="@+id/alt_text"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_margin="8dp"
android:textAppearance="@style/m3_body_medium"
android:textColor="?colorGray25"
tools:text="Alt text goes here"/>
<TextView
android:id="@+id/no_alt_text"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginVertical="8dp"
android:layout_marginEnd="8dp"
android:layout_marginStart="14dp"
android:textAppearance="@style/m3_label_large"
android:textColor="?colorGray25"
android:text="@string/sk_no_alt_text"/>
</LinearLayout>
</org.joinmastodon.android.ui.views.NestableScrollView>
</FrameLayout> </FrameLayout>

View File

@ -1,5 +1,5 @@
<?xml version="1.0" encoding="utf-8"?> <?xml version="1.0" encoding="utf-8"?>
<org.joinmastodon.android.ui.views.ImageAttachmentFrameLayout xmlns:android="http://schemas.android.com/apk/res/android" <FrameLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="match_parent" android:layout_width="match_parent"
android:layout_height="wrap_content"> android:layout_height="wrap_content">
@ -27,4 +27,4 @@
<include layout="@layout/alt_badge" /> <include layout="@layout/alt_badge" />
</org.joinmastodon.android.ui.views.ImageAttachmentFrameLayout> </FrameLayout>

View File

@ -27,6 +27,7 @@
android:visibility="gone" android:visibility="gone"
android:background="?android:actionBarItemBackground" android:background="?android:actionBarItemBackground"
android:contentDescription="@string/sk_delete_notification" android:contentDescription="@string/sk_delete_notification"
android:tooltipText="@string/sk_delete_notification"
android:scaleType="center" android:scaleType="center"
android:src="@drawable/ic_fluent_dismiss_20_filled" android:src="@drawable/ic_fluent_dismiss_20_filled"
android:tint="?android:textColorSecondary" /> android:tint="?android:textColorSecondary" />

View File

@ -1,5 +1,6 @@
<?xml version="1.0" encoding="utf-8"?> <?xml version="1.0" encoding="utf-8"?>
<org.joinmastodon.android.ui.views.ImageAttachmentFrameLayout xmlns:android="http://schemas.android.com/apk/res/android" <FrameLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent" android:layout_width="match_parent"
android:layout_height="wrap_content"> android:layout_height="wrap_content">
@ -12,4 +13,4 @@
<include layout="@layout/alt_badge" /> <include layout="@layout/alt_badge" />
</org.joinmastodon.android.ui.views.ImageAttachmentFrameLayout> </FrameLayout>

View File

@ -1,14 +1,16 @@
<?xml version="1.0" encoding="utf-8"?> <?xml version="1.0" encoding="utf-8"?>
<FrameLayout xmlns:android="http://schemas.android.com/apk/res/android" <org.joinmastodon.android.ui.views.AutoOrientationLinearLayout
xmlns:android="http://schemas.android.com/apk/res/android"
android:orientation="horizontal"
android:layout_width="match_parent" android:layout_width="match_parent"
android:layout_height="wrap_content" android:layout_height="wrap_content"
android:paddingHorizontal="16dp"
android:layout_marginBottom="-12dp"> android:layout_marginBottom="-12dp">
<TextView <TextView
android:id="@+id/text" android:id="@+id/text"
android:layout_width="wrap_content" android:layout_width="wrap_content"
android:layout_height="wrap_content" android:layout_height="wrap_content"
android:paddingHorizontal="16dp"
android:paddingTop="16dp" android:paddingTop="16dp"
android:paddingBottom="6dp" android:paddingBottom="6dp"
android:textAppearance="@style/m3_title_small" android:textAppearance="@style/m3_title_small"
@ -18,4 +20,30 @@
android:singleLine="true" android:singleLine="true"
android:ellipsize="end"/> android:ellipsize="end"/>
</FrameLayout> <TextView
android:id="@+id/separator"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginHorizontal="6dp"
android:paddingTop="16dp"
android:paddingHorizontal="1dp"
android:textAppearance="@style/m3_title_small"
android:gravity="center_horizontal"
android:importantForAccessibility="no"
android:includeFontPadding="false"
android:text="@string/sk_separator" />
<TextView
android:id="@+id/extra_text"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:paddingTop="16dp"
android:paddingBottom="6dp"
android:textAppearance="@style/m3_title_small"
android:drawableStart="@drawable/ic_fluent_arrow_reply_20_filled"
android:drawableTint="?android:textColorSecondary"
android:drawablePadding="6dp"
android:singleLine="true"
android:ellipsize="end"/>
</org.joinmastodon.android.ui.views.AutoOrientationLinearLayout>

View File

@ -1,5 +1,5 @@
<?xml version="1.0" encoding="utf-8"?> <?xml version="1.0" encoding="utf-8"?>
<org.joinmastodon.android.ui.views.ImageAttachmentFrameLayout xmlns:android="http://schemas.android.com/apk/res/android" <FrameLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="match_parent" android:layout_width="match_parent"
android:layout_height="wrap_content"> android:layout_height="wrap_content">
@ -20,4 +20,4 @@
<include layout="@layout/alt_badge" /> <include layout="@layout/alt_badge" />
</org.joinmastodon.android.ui.views.ImageAttachmentFrameLayout> </FrameLayout>

View File

@ -64,10 +64,10 @@
android:paddingBottom="6dp" android:paddingBottom="6dp"
android:textAppearance="@style/m3_title_small" android:textAppearance="@style/m3_title_small"
android:drawableStart="@drawable/ic_fluent_arrow_reply_20_filled" android:drawableStart="@drawable/ic_fluent_arrow_reply_20_filled"
tools:drawableEnd="@drawable/ic_fluent_earth_20_regular"
android:drawableTint="?android:textColorSecondary" android:drawableTint="?android:textColorSecondary"
android:drawablePadding="6dp" android:drawablePadding="6dp"
android:singleLine="true" android:singleLine="true"
android:text="@string/sk_in_reply"
android:ellipsize="end"/> android:ellipsize="end"/>
<RelativeLayout <RelativeLayout
@ -101,9 +101,9 @@
android:id="@+id/self_extra_text" android:id="@+id/self_extra_text"
android:layout_width="wrap_content" android:layout_width="wrap_content"
android:layout_height="wrap_content" android:layout_height="wrap_content"
android:layout_marginTop="2sp"
android:layout_marginStart="8sp" android:layout_marginStart="8sp"
android:layout_toEndOf="@id/self_name" android:layout_toEndOf="@id/self_name"
android:paddingTop="4sp"
android:ellipsize="end" android:ellipsize="end"
android:fontFamily="sans-serif" android:fontFamily="sans-serif"
android:singleLine="true" android:singleLine="true"
@ -125,6 +125,23 @@
</RelativeLayout> </RelativeLayout>
<TextView
android:id="@+id/reply_text_below"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginTop="-6dp"
android:layout_marginBottom="-6dp"
android:layout_marginStart="16dp"
android:paddingTop="16dp"
android:paddingBottom="6dp"
android:textAppearance="@style/m3_title_small"
android:drawableStart="@drawable/ic_fluent_arrow_reply_20_filled"
android:drawableTint="?android:textColorSecondary"
android:drawablePadding="6dp"
android:singleLine="true"
android:text="@string/sk_in_reply"
android:ellipsize="end"/>
<FrameLayout <FrameLayout
android:id="@+id/toot_text_wrap" android:id="@+id/toot_text_wrap"
android:layout_width="match_parent" android:layout_width="match_parent"

View File

@ -36,7 +36,7 @@
android:layout_marginTop="16dp" android:layout_marginTop="16dp"
android:layout_marginBottom="8dp" android:layout_marginBottom="8dp"
android:layout_marginHorizontal="16dp" android:layout_marginHorizontal="16dp"
android:inputType="textFilter|textNoSuggestions" android:inputType="textUri|textNoSuggestions"
android:singleLine="true" android:singleLine="true"
android:imeOptions="actionGo" android:imeOptions="actionGo"
android:drawableStart="@drawable/ic_fluent_globe_20_regular" android:drawableStart="@drawable/ic_fluent_globe_20_regular"

View File

@ -0,0 +1,77 @@
<?xml version="1.0" encoding="utf-8"?>
<FrameLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools"
android:id="@+id/alt_text_wrapper"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_gravity="start|bottom"
android:layout_margin="12dp"
android:importantForAccessibility="noHideDescendants"
android:background="@drawable/bg_image_alt_overlay">
<ImageView
android:id="@+id/no_alt_button"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:padding="4dp"
android:src="@drawable/ic_fluent_important_20_filled"
android:tint="?colorGray25" />
<TextView
android:id="@+id/alt_button"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:textAppearance="@style/m3_label_large"
android:textColor="?colorGray25"
android:gravity="center"
android:includeFontPadding="false"
android:paddingHorizontal="5dp"
android:paddingVertical="1dp"
android:text="@string/sk_alt_button"/>
<ImageButton
android:id="@+id/alt_text_close"
android:layout_width="40dp"
android:layout_height="40dp"
android:layout_gravity="end|top"
android:src="@drawable/ic_baseline_close_24"
android:tint="#FFF"
android:background="?android:actionBarItemBackground"/>
<org.joinmastodon.android.ui.views.NestableScrollView
android:id="@+id/alt_text_scroller"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_gravity="center_vertical"
android:layout_marginEnd="40dp">
<LinearLayout
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:orientation="vertical">
<TextView
android:id="@+id/alt_text"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_margin="8dp"
android:textAppearance="@style/m3_body_medium"
android:textColor="#FFF"
tools:text="Alt text goes here"/>
<TextView
android:id="@+id/no_alt_text"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginVertical="8dp"
android:layout_marginEnd="8dp"
android:layout_marginStart="14dp"
android:textAppearance="@style/m3_label_large"
android:textColor="?colorGray25"
android:text="@string/sk_no_alt_text"/>
</LinearLayout>
</org.joinmastodon.android.ui.views.NestableScrollView>
</FrameLayout>

View File

@ -262,4 +262,8 @@
<string name="sk_follow_as">Follow from other account</string> <string name="sk_follow_as">Follow from other account</string>
<string name="sk_followed_as">Followed from %s</string> <string name="sk_followed_as">Followed from %s</string>
<string name="sk_settings_hide_fab">Auto-hide Compose button</string> <string name="sk_settings_hide_fab">Auto-hide Compose button</string>
<string name="sk_in_reply">In reply</string>
<string name="sk_reply_line_above_avatar">“In reply to” line above avatar</string>
<string name="sk_show_thread">Show thread</string>
<string name="sk_compact_reblog_reply_line">Compact reblog/reply line</string>
</resources> </resources>