Merge upstream redesign (#714)
* merge toolbar fragment * Fix store screenshot generator * Fix alert color * Fix #609 * Fix crash * bigger hitbox for chips * support mastodon languages * merge ui utils * merge stuff * fix icon * ensure 48dp touch target * init local prefs, add helper function for enum values * update compose action layout * merge compose-adj files * update extended footer * fix poll wrong option checked closes sk22#641 * no border when disabled closes sk22#640 * Fix #610 * Minor fixes * Fix alert color * Fix #609 * Fix crash * Fix #610 * Minor fixes * add resources * more compatible mastodon language * fix html parser * mark as read on refresh * update tab bar * tweak m3 buttons * update compose-adj files * tweak and update styles * m3 expand button * flag icon should be 18dp, actually * More minor fixes closes #612 * More minor fixes closes #612 * Bump version * fix no create status event when redrafting * add material 3 assets * New translations strings.xml (Greek) * New translations strings.xml (Greek) * New translations strings.xml (Italian) * New translations strings.xml (Greek) * New translations strings.xml (Italian) * New translations strings.xml (Thai) * New translations strings.xml (Thai) * New translations strings.xml (Italian) * New translations strings.xml (Thai) * use new buttons for profile fragment * merge compose fragment * merge all the styles! oh dear * New translations full_description.txt (Indonesian) * New translations full_description.txt (Chinese Simplified) * New translations strings.xml (Chinese Simplified) * New translations full_description.txt (Chinese Simplified) * Fix #615 * Minor fixes * Fix #611 * A bunch of crash fixes * New translations strings.xml (Greek) * Make the default server configurable * Pass the system timezone to server when signing up * New translations strings.xml (Chinese Simplified) * New translations strings.xml (Japanese) * Fix #615 * Minor fixes * Fix #611 * A bunch of crash fixes * Make the default server configurable * Pass the system timezone to server when signing up * oops. accidentally pasted the commit message in the code * Remove unused code that caused a crash for some users ¯\_(ツ)_/¯ * New translations strings.xml (Japanese) * New translations strings.xml (Japanese) * Remove unused code that caused a crash for some users ¯\_(ツ)_/¯ * New translations strings.xml (Polish) * New translations strings.xml (Polish) * New translations strings.xml (Turkish) * New translations strings.xml (Belarusian) * prepare merging profile fragment * merge profile fragment * New translations strings.xml (Belarusian) * New translations strings.xml (Greek) * fix icon padding * apply post header changes * minor margin tweaks * fix footer buttons * fix header announcement buttons * New translations strings.xml (Japanese) * New translations strings.xml (Japanese) * New translations strings.xml (Japanese) * New translations strings.xml (Japanese) * New translations strings.xml (Japanese) * New translations strings.xml (Japanese) * New translations full_description.txt (Japanese) * New translations strings.xml (Icelandic) * New translations strings.xml (Icelandic) * New translations strings.xml (Icelandic) * fix replying * New translations strings.xml (Icelandic) * fix translate button * fix more button visibility * fix counts label styling * fix disabled boost button opacity * fix tab layouts * fix notification icon color crash * New translations strings.xml (Greek) * implement elevation listener in home tab * fix elevation and listener in home tab * add elevation scroll listener to notifications * New translations strings.xml (Scottish Gaelic) * Add editorconfig So that PRs like #625 don't happen again * Crash fix * 🤔 * New translations strings.xml (Greek) * New translations strings.xml (Japanese) * New translations strings.xml (French) * New translations strings.xml (French) * New translations strings.xml (French) * fix notification elevation and integrate divider * 🤔 * Crash fix * Add editorconfig So that PRs like #625 don't happen again * New translations strings.xml (Turkish) * save interactions in cache * New translations strings.xml (Turkish) * merge new discover/search * New translations strings.xml (Bengali) * New translations strings.xml (Scottish Gaelic) * New translations strings.xml (Bengali) * merge new settings fragments * fix no auth callback always being executed * allow opening server info from profile closes sk22#593 * fix hide boosts icon color closes sk22#676 * New translations strings.xml (Turkish) * New translations strings.xml (Turkish) * New translations strings.xml (Turkish) * New translations strings.xml (Chinese Simplified) * New translations strings.xml (Turkish) * New translations strings.xml (Chinese Simplified) * New translations strings.xml (German) * New translations strings.xml (German) * New translations strings.xml (Turkish) * update fedinuke list from source; doesn't contain any modifications regarding a recent issue * New translations strings.xml (Turkish) * remove unused class * fix crash * darken m3 outline color a bit * use m3 outline again * fix misalignment closes sk22#682 * New translations strings.xml (Turkish) * New translations full_description.txt (Turkish) * New translations short_description.txt (Turkish) * fix crash * fix metadata sorting * show pronouns in header/account lists * fix broken divider line closes sk22#679 * trim pronouns * improve pronoun display * New translations strings.xml (French) * New translations strings.xml (Japanese) * fix broken federated timeline closes sk22#685 * fix broken -1 fallback behavior closes sk22#681 * don't display nothing if server about request fails closes sk22#678 * New translations strings.xml (Ukrainian) * migrate global prefs to local prefs * do confirm unfollow by default * New translations strings.xml (Ukrainian) * New translations strings.xml (Ukrainian) * New translations full_description.txt (Ukrainian) * New translations strings.xml (Ukrainian) * New translations strings.xml (Ukrainian) * New translations strings.xml (Ukrainian) * New translations strings.xml (Ukrainian) * New translations strings.xml (Ukrainian) * New translations strings.xml (Russian) * New translations strings.xml (Vietnamese) * New translations strings.xml (Ukrainian) * New translations strings.xml (Vietnamese) * New translations full_description.txt (Ukrainian) * New translations strings.xml (Ukrainian) * New translations strings.xml (Vietnamese) * New translations strings.xml (Ukrainian) * New translations strings.xml (Ukrainian) * make sure list in prefs are always mutable and nut null * New translations strings.xml (Ukrainian) * New translations strings.xml (Ukrainian) * New translations strings.xml (Russian) * fix pronouns edge case * add back fix for stretched images closes sk22#636 * fix null pointer on missing default posting language * fix default posting language not being applied * bigger username hitbox closes sk22#688 * fix rtl header username alignment closes sk22#689 * New translations strings.xml (Ukrainian) * New translations strings.xml (Ukrainian) * hopefully fix crashes closes sk22#692 * New translations strings.xml (Ukrainian) * New translations full_description.txt (Ukrainian) * fix pronoun crash * New translations strings.xml (Persian) * New translations strings.xml (Ukrainian) * re-add true black mode * asterisk can be a pronoun * New translations strings.xml (Persian) * true black mode fixes and clean-ups * material 3 button background for switcher * darker tab bar selected background * better align follow/following button widths * restore rainbow refresh colors * fix search transition * fix min width issue with switcher button * fix no elevation when true black is enabled in light theme * use statusForContent to determine spoilerRevealed closes sk22#694 * New translations strings.xml (Persian) * New translations strings.xml (Persian) * New translations strings.xml (Persian) * New translations strings.xml (Persian) * New translations strings.xml (Persian) * New translations strings.xml (Persian) * fix profile tab bar in true black theme * fix m3 default button style closes sk22#697 * prettier role badges closes sk22#663 * fix translate button spacing closes sk22#655 * use m3 switches in dialogs closes sk22#653 * implement color palette switcher * fix color palettes being overwritten * add display and notification settings * clean up code * per-account single notification setting * add missing items to notification types * add prefix replies setting * add show replies/boosts and reply visibility * add load/see new posts settings * fix spectator mode missing spoiler padding * add a bunch of display settings * update fedinuke * add content type settings * add settings for local-onlu * add missing settings items * fix visibility button icon tint * hopefully fix some crashes * normalize padding above edit text * apparently, some people don't like pills closes sk22#706 * fix play button color closes sk22#705
This commit is contained in:
@@ -331,8 +331,8 @@ public class AccountSwitcherSheet extends BottomSheet{
|
||||
onClick.accept(item.getID(), false);
|
||||
return;
|
||||
}
|
||||
|
||||
AccountSessionManager.getInstance().setLastActiveAccountID(item.getID());
|
||||
if(AccountSessionManager.getInstance().tryGetAccount(item.getID())!=null)
|
||||
AccountSessionManager.getInstance().setLastActiveAccountID(item.getID());
|
||||
activity.finish();
|
||||
activity.startActivity(new Intent(activity, MainActivity.class));
|
||||
}
|
||||
|
||||
@@ -18,15 +18,13 @@ package org.joinmastodon.android.ui;
|
||||
import android.animation.Animator;
|
||||
import android.animation.AnimatorListenerAdapter;
|
||||
import android.animation.TimeInterpolator;
|
||||
import android.animation.ValueAnimator;
|
||||
import android.view.View;
|
||||
import android.view.ViewPropertyAnimator;
|
||||
|
||||
import androidx.annotation.NonNull;
|
||||
import androidx.recyclerview.widget.RecyclerView;
|
||||
import androidx.recyclerview.widget.SimpleItemAnimator;
|
||||
|
||||
import org.joinmastodon.android.ui.displayitems.MediaGridStatusDisplayItem;
|
||||
|
||||
import me.grishka.appkit.utils.CubicBezierInterpolator;
|
||||
|
||||
import java.util.ArrayList;
|
||||
@@ -360,14 +358,7 @@ public class BetterItemAnimator extends SimpleItemAnimator{
|
||||
mChangeAnimations.add(changeInfo.oldHolder);
|
||||
oldViewAnim.translationX(changeInfo.toX - changeInfo.fromX);
|
||||
oldViewAnim.translationY(changeInfo.toY - changeInfo.fromY);
|
||||
float alpha = 0;
|
||||
if (holder instanceof MediaGridStatusDisplayItem.Holder mediaItemHolder) {
|
||||
if (mediaItemHolder.isSizeUpdating()) {
|
||||
alpha = 1; // Image will flicker out and then in if alpha is 0
|
||||
mediaItemHolder.sizeUpdated();
|
||||
}
|
||||
}
|
||||
oldViewAnim.alpha(alpha).setListener(new AnimatorListenerAdapter() {
|
||||
oldViewAnim.alpha(0).setListener(new AnimatorListenerAdapter() {
|
||||
@Override
|
||||
public void onAnimationStart(Animator animator) {
|
||||
dispatchChangeStarting(changeInfo.oldHolder, true);
|
||||
|
||||
@@ -2,14 +2,19 @@ package org.joinmastodon.android.ui;
|
||||
|
||||
import android.annotation.SuppressLint;
|
||||
import android.app.Activity;
|
||||
import android.content.res.TypedArray;
|
||||
import android.content.res.ColorStateList;
|
||||
import android.graphics.Rect;
|
||||
import android.graphics.drawable.Animatable;
|
||||
import android.graphics.drawable.ColorDrawable;
|
||||
import android.graphics.drawable.Drawable;
|
||||
import android.text.TextUtils;
|
||||
import android.view.Gravity;
|
||||
import android.view.View;
|
||||
import android.view.ViewGroup;
|
||||
import android.widget.FrameLayout;
|
||||
import android.widget.ImageButton;
|
||||
import android.widget.ImageView;
|
||||
import android.widget.LinearLayout;
|
||||
import android.widget.TextView;
|
||||
|
||||
import com.squareup.otto.Subscribe;
|
||||
@@ -22,7 +27,6 @@ import org.joinmastodon.android.model.EmojiCategory;
|
||||
import org.joinmastodon.android.ui.utils.UiUtils;
|
||||
|
||||
import java.util.List;
|
||||
import java.util.function.Consumer;
|
||||
import java.util.stream.Collectors;
|
||||
|
||||
import androidx.annotation.NonNull;
|
||||
@@ -45,9 +49,8 @@ public class CustomEmojiPopupKeyboard extends PopupKeyboard{
|
||||
private ListImageLoaderWrapper imgLoader;
|
||||
private MergeRecyclerAdapter adapter=new MergeRecyclerAdapter();
|
||||
private String domain;
|
||||
private int gridGap;
|
||||
private int spanCount=6;
|
||||
private Consumer<Emoji> listener;
|
||||
private Listener listener;
|
||||
|
||||
public CustomEmojiPopupKeyboard(Activity activity, List<EmojiCategory> emojis, String domain){
|
||||
super(activity);
|
||||
@@ -62,11 +65,8 @@ public class CustomEmojiPopupKeyboard extends PopupKeyboard{
|
||||
@Override
|
||||
protected void onMeasure(int widthSpec, int heightSpec){
|
||||
// it's important to do this in onMeasure so the child views will be measured with correct paddings already set
|
||||
spanCount=Math.round(MeasureSpec.getSize(widthSpec)/(float)V.dp(44+20));
|
||||
spanCount=Math.round((MeasureSpec.getSize(widthSpec)-V.dp(32-8))/(float)V.dp(48+8));
|
||||
lm.setSpanCount(spanCount);
|
||||
int pad=V.dp(16);
|
||||
gridGap=(MeasureSpec.getSize(widthSpec)-pad*2-V.dp(44)*spanCount)/(spanCount-1);
|
||||
setPadding(pad, 0, pad-gridGap, 0);
|
||||
invalidateItemDecorations();
|
||||
super.onMeasure(widthSpec, heightSpec);
|
||||
}
|
||||
@@ -80,6 +80,7 @@ public class CustomEmojiPopupKeyboard extends PopupKeyboard{
|
||||
}
|
||||
});
|
||||
list.setLayoutManager(lm);
|
||||
list.setPadding(V.dp(16), 0, V.dp(16), 0);
|
||||
imgLoader=new ListImageLoaderWrapper(activity, list, new RecyclerViewDelegate(list), null);
|
||||
|
||||
for(EmojiCategory category:emojis)
|
||||
@@ -88,22 +89,52 @@ public class CustomEmojiPopupKeyboard extends PopupKeyboard{
|
||||
list.addItemDecoration(new RecyclerView.ItemDecoration(){
|
||||
@Override
|
||||
public void getItemOffsets(@NonNull Rect outRect, @NonNull View view, @NonNull RecyclerView parent, @NonNull RecyclerView.State state){
|
||||
outRect.right=gridGap;
|
||||
if(view instanceof TextView){ // section header
|
||||
if(parent.getChildAdapterPosition(view)>0)
|
||||
outRect.top=-gridGap; // negate the margin added by the emojis above
|
||||
outRect.left=outRect.right=V.dp(-16);
|
||||
}else{
|
||||
outRect.bottom=gridGap;
|
||||
EmojiViewHolder evh=(EmojiViewHolder) parent.getChildViewHolder(view);
|
||||
int col=evh.positionWithinCategory%spanCount;
|
||||
if(col<spanCount-1){
|
||||
outRect.right=V.dp(8);
|
||||
}
|
||||
outRect.bottom=V.dp(8);
|
||||
}
|
||||
}
|
||||
});
|
||||
list.setBackgroundColor(UiUtils.getThemeColor(activity, android.R.attr.colorBackground));
|
||||
list.setSelector(null);
|
||||
list.setClipToPadding(false);
|
||||
new StickyHeadersOverlay(activity, 0).install(list);
|
||||
|
||||
return list;
|
||||
LinearLayout ll=new LinearLayout(activity);
|
||||
ll.setOrientation(LinearLayout.VERTICAL);
|
||||
ll.setElevation(V.dp(3));
|
||||
ll.setBackgroundResource(R.drawable.bg_m3_surface1);
|
||||
|
||||
ll.addView(list, new LinearLayout.LayoutParams(ViewGroup.LayoutParams.MATCH_PARENT, 0, 1f));
|
||||
|
||||
FrameLayout bottomPanel=new FrameLayout(activity);
|
||||
bottomPanel.setPadding(V.dp(16), V.dp(8), V.dp(16), V.dp(8));
|
||||
bottomPanel.setBackgroundResource(R.drawable.bg_m3_surface2);
|
||||
ll.addView(bottomPanel, new LinearLayout.LayoutParams(ViewGroup.LayoutParams.MATCH_PARENT, ViewGroup.LayoutParams.WRAP_CONTENT));
|
||||
|
||||
ImageButton hideKeyboard=new ImageButton(activity);
|
||||
hideKeyboard.setImageResource(R.drawable.ic_fluent_keyboard_dock_24_regular);
|
||||
hideKeyboard.setImageTintList(ColorStateList.valueOf(UiUtils.getThemeColor(activity, R.attr.colorM3OnSurfaceVariant)));
|
||||
hideKeyboard.setBackgroundResource(R.drawable.bg_round_ripple);
|
||||
hideKeyboard.setOnClickListener(v->hide());
|
||||
bottomPanel.addView(hideKeyboard, new FrameLayout.LayoutParams(V.dp(36), V.dp(36), Gravity.LEFT));
|
||||
|
||||
ImageButton backspace=new ImageButton(activity);
|
||||
backspace.setImageResource(R.drawable.ic_fluent_backspace_24_regular);
|
||||
backspace.setImageTintList(ColorStateList.valueOf(UiUtils.getThemeColor(activity, R.attr.colorM3OnSurfaceVariant)));
|
||||
backspace.setBackgroundResource(R.drawable.bg_round_ripple);
|
||||
backspace.setOnClickListener(v->listener.onBackspace());
|
||||
bottomPanel.addView(backspace, new FrameLayout.LayoutParams(V.dp(36), V.dp(36), Gravity.RIGHT));
|
||||
|
||||
return ll;
|
||||
}
|
||||
|
||||
public void setListener(Consumer<Emoji> listener){
|
||||
public void setListener(Listener listener){
|
||||
this.listener=listener;
|
||||
}
|
||||
|
||||
@@ -123,7 +154,7 @@ public class CustomEmojiPopupKeyboard extends PopupKeyboard{
|
||||
public SingleCategoryAdapter(EmojiCategory category){
|
||||
super(imgLoader);
|
||||
this.category=category;
|
||||
requests=category.emojis.stream().map(e->new UrlImageLoaderRequest(e.url, V.dp(44), V.dp(44))).collect(Collectors.toList());
|
||||
requests=category.emojis.stream().map(e->new UrlImageLoaderRequest(e.url, V.dp(24), V.dp(24))).collect(Collectors.toList());
|
||||
}
|
||||
|
||||
@NonNull
|
||||
@@ -134,11 +165,11 @@ public class CustomEmojiPopupKeyboard extends PopupKeyboard{
|
||||
|
||||
@Override
|
||||
public void onBindViewHolder(RecyclerView.ViewHolder holder, int position){
|
||||
if(holder instanceof EmojiViewHolder){
|
||||
((EmojiViewHolder) holder).bind(category.emojis.get(position-1));
|
||||
((EmojiViewHolder) holder).positionWithinCategory=position-1;
|
||||
}else if(holder instanceof SectionHeaderViewHolder){
|
||||
((SectionHeaderViewHolder) holder).bind(TextUtils.isEmpty(category.title) ? domain : category.title);
|
||||
if(holder instanceof EmojiViewHolder evh){
|
||||
evh.bind(category.emojis.get(position-1));
|
||||
evh.positionWithinCategory=position-1;
|
||||
}else if(holder instanceof SectionHeaderViewHolder shvh){
|
||||
shvh.bind(TextUtils.isEmpty(category.title) ? domain : category.title);
|
||||
}
|
||||
super.onBindViewHolder(holder, position);
|
||||
}
|
||||
@@ -164,14 +195,24 @@ public class CustomEmojiPopupKeyboard extends PopupKeyboard{
|
||||
}
|
||||
}
|
||||
|
||||
private class SectionHeaderViewHolder extends BindableViewHolder<String>{
|
||||
private class SectionHeaderViewHolder extends BindableViewHolder<String> implements StickyHeadersOverlay.HeaderViewHolder{
|
||||
private Drawable background;
|
||||
|
||||
public SectionHeaderViewHolder(){
|
||||
super(activity, R.layout.item_emoji_section, list);
|
||||
background=new ColorDrawable(UiUtils.alphaBlendThemeColors(activity, R.attr.colorM3Surface, R.attr.colorM3Primary, .08f));
|
||||
itemView.setBackground(background);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onBind(String item){
|
||||
((TextView)itemView).setText(item);
|
||||
setStickyFactor(0);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void setStickyFactor(float factor){
|
||||
background.setAlpha(Math.round(255*factor));
|
||||
}
|
||||
}
|
||||
|
||||
@@ -180,8 +221,11 @@ public class CustomEmojiPopupKeyboard extends PopupKeyboard{
|
||||
public EmojiViewHolder(){
|
||||
super(new ImageView(activity));
|
||||
ImageView img=(ImageView) itemView;
|
||||
img.setLayoutParams(new RecyclerView.LayoutParams(ViewGroup.LayoutParams.MATCH_PARENT, V.dp(44)));
|
||||
img.setLayoutParams(new RecyclerView.LayoutParams(V.dp(48), V.dp(48)));
|
||||
img.setScaleType(ImageView.ScaleType.FIT_CENTER);
|
||||
int pad=V.dp(12);
|
||||
img.setPadding(pad, pad, pad, pad);
|
||||
img.setBackgroundResource(R.drawable.bg_custom_emoji);
|
||||
}
|
||||
|
||||
@Override
|
||||
@@ -203,7 +247,12 @@ public class CustomEmojiPopupKeyboard extends PopupKeyboard{
|
||||
|
||||
@Override
|
||||
public void onClick(){
|
||||
listener.accept(item);
|
||||
listener.onEmojiSelected(item);
|
||||
}
|
||||
}
|
||||
|
||||
public interface Listener{
|
||||
void onEmojiSelected(Emoji emoji);
|
||||
void onBackspace();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -67,7 +67,7 @@ public class ImageDescriptionSheet extends BottomSheet{
|
||||
list.setClipToPadding(false);
|
||||
|
||||
setContentView(list);
|
||||
setNavigationBarBackground(new ColorDrawable(UiUtils.getThemeColor(activity, R.attr.colorWindowBackground)), !UiUtils.isDarkTheme());
|
||||
setNavigationBarBackground(new ColorDrawable(UiUtils.getThemeColor(activity, R.attr.colorM3Surface)), !UiUtils.isDarkTheme());
|
||||
}
|
||||
|
||||
@Override
|
||||
|
||||
@@ -2,13 +2,21 @@ package org.joinmastodon.android.ui;
|
||||
|
||||
import android.app.AlertDialog;
|
||||
import android.content.Context;
|
||||
import android.text.TextUtils;
|
||||
import android.view.LayoutInflater;
|
||||
import android.view.View;
|
||||
import android.widget.Button;
|
||||
import android.widget.ImageView;
|
||||
import android.widget.TextView;
|
||||
|
||||
import org.joinmastodon.android.R;
|
||||
|
||||
import androidx.annotation.StringRes;
|
||||
import me.grishka.appkit.utils.V;
|
||||
|
||||
public class M3AlertDialogBuilder extends AlertDialog.Builder{
|
||||
private CharSequence supportingText, title, helpText;
|
||||
private AlertDialog alert;
|
||||
|
||||
public M3AlertDialogBuilder(Context context){
|
||||
super(context);
|
||||
}
|
||||
@@ -19,12 +27,36 @@ public class M3AlertDialogBuilder extends AlertDialog.Builder{
|
||||
|
||||
@Override
|
||||
public AlertDialog create(){
|
||||
AlertDialog alert=super.create();
|
||||
if(!TextUtils.isEmpty(helpText) && !TextUtils.isEmpty(supportingText))
|
||||
throw new IllegalStateException("You can't have both help text and supporting text in the same alert");
|
||||
|
||||
if(!TextUtils.isEmpty(supportingText)){
|
||||
View titleLayout=getContext().getSystemService(LayoutInflater.class).inflate(R.layout.alert_title_with_supporting_text, null);
|
||||
TextView title=titleLayout.findViewById(R.id.title);
|
||||
TextView subtitle=titleLayout.findViewById(R.id.subtitle);
|
||||
title.setText(this.title);
|
||||
subtitle.setText(supportingText);
|
||||
setCustomTitle(titleLayout);
|
||||
}else if(!TextUtils.isEmpty(helpText)){
|
||||
View titleLayout=getContext().getSystemService(LayoutInflater.class).inflate(R.layout.alert_title_with_help, null);
|
||||
TextView title=titleLayout.findViewById(R.id.title);
|
||||
TextView helpText=titleLayout.findViewById(R.id.help_text);
|
||||
View helpButton=titleLayout.findViewById(R.id.help);
|
||||
title.setText(this.title);
|
||||
helpText.setText(this.helpText);
|
||||
helpButton.setOnClickListener(v->{
|
||||
helpText.setVisibility(helpText.getVisibility()==View.VISIBLE ? View.GONE : View.VISIBLE);
|
||||
helpButton.setSelected(helpText.getVisibility()==View.VISIBLE);
|
||||
});
|
||||
setCustomTitle(titleLayout);
|
||||
}
|
||||
|
||||
alert=super.create();
|
||||
alert.create();
|
||||
Button btn=alert.getButton(AlertDialog.BUTTON_POSITIVE);
|
||||
if(btn!=null){
|
||||
View buttonBar=(View) btn.getParent();
|
||||
buttonBar.setPadding(V.dp(16), 0, V.dp(16), V.dp(24));
|
||||
buttonBar.setPadding(V.dp(16), V.dp(16), V.dp(16), V.dp(16));
|
||||
((View)buttonBar.getParent()).setPadding(0, 0, 0, 0);
|
||||
}
|
||||
// hacc
|
||||
@@ -32,16 +64,8 @@ public class M3AlertDialogBuilder extends AlertDialog.Builder{
|
||||
if(titleID!=0){
|
||||
View title=alert.findViewById(titleID);
|
||||
if(title!=null){
|
||||
int iconID=getContext().getResources().getIdentifier("icon", "id", "android");
|
||||
int alertTitleID=getContext().getResources().getIdentifier("alertTitle", "id", "android");
|
||||
if (alertTitleID != 0 && iconID != 0) {
|
||||
ImageView icon = title.findViewById(iconID);
|
||||
if (icon.getDrawable() != null) {
|
||||
title.findViewById(alertTitleID).setPadding(V.dp(8), 0, 0, 0);
|
||||
}
|
||||
}
|
||||
int pad=V.dp(24);
|
||||
title.setPadding(pad, pad, pad, V.dp(12));
|
||||
title.setPadding(pad, pad, pad, pad);
|
||||
}
|
||||
}
|
||||
int titleDividerID=getContext().getResources().getIdentifier("titleDividerNoCustom", "id", "android");
|
||||
@@ -58,13 +82,40 @@ public class M3AlertDialogBuilder extends AlertDialog.Builder{
|
||||
scrollView.setPadding(0, 0, 0, 0);
|
||||
}
|
||||
}
|
||||
int messageID=getContext().getResources().getIdentifier("message", "id", "android");
|
||||
if(messageID!=0){
|
||||
View message=alert.findViewById(messageID);
|
||||
if(message!=null){
|
||||
message.setPadding(message.getPaddingLeft(), message.getPaddingTop(), message.getPaddingRight(), V.dp(24));
|
||||
}
|
||||
}
|
||||
return alert;
|
||||
}
|
||||
|
||||
public M3AlertDialogBuilder setSupportingText(CharSequence text){
|
||||
supportingText=text;
|
||||
return this;
|
||||
}
|
||||
|
||||
public M3AlertDialogBuilder setSupportingText(@StringRes int text){
|
||||
supportingText=getContext().getString(text);
|
||||
return this;
|
||||
}
|
||||
|
||||
@Override
|
||||
public M3AlertDialogBuilder setTitle(CharSequence title){
|
||||
super.setTitle(title);
|
||||
this.title=title;
|
||||
return this;
|
||||
}
|
||||
|
||||
@Override
|
||||
public M3AlertDialogBuilder setTitle(@StringRes int title){
|
||||
super.setTitle(title);
|
||||
this.title=getContext().getString(title);
|
||||
return this;
|
||||
}
|
||||
|
||||
public M3AlertDialogBuilder setHelpText(CharSequence text){
|
||||
helpText=text;
|
||||
return this;
|
||||
}
|
||||
|
||||
public M3AlertDialogBuilder setHelpText(@StringRes int text){
|
||||
helpText=getContext().getString(text);
|
||||
return this;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -10,6 +10,7 @@ import me.grishka.appkit.utils.V;
|
||||
public class OutlineProviders{
|
||||
private static final SparseArray<ViewOutlineProvider> roundedRects=new SparseArray<>();
|
||||
private static final SparseArray<ViewOutlineProvider> topRoundedRects=new SparseArray<>();
|
||||
private static final SparseArray<ViewOutlineProvider> bottomRoundedRects=new SparseArray<>();
|
||||
private static final SparseArray<ViewOutlineProvider> endRoundedRects=new SparseArray<>();
|
||||
|
||||
public static final int RADIUS_XSMALL=4;
|
||||
@@ -54,6 +55,15 @@ public class OutlineProviders{
|
||||
return provider;
|
||||
}
|
||||
|
||||
public static ViewOutlineProvider bottomRoundedRect(int dp){
|
||||
ViewOutlineProvider provider=bottomRoundedRects.get(dp);
|
||||
if(provider!=null)
|
||||
return provider;
|
||||
provider=new BottomRoundRectOutlineProvider(V.dp(dp));
|
||||
bottomRoundedRects.put(dp, provider);
|
||||
return provider;
|
||||
}
|
||||
|
||||
public static ViewOutlineProvider endRoundedRect(int dp){
|
||||
ViewOutlineProvider provider=endRoundedRects.get(dp);
|
||||
if(provider!=null)
|
||||
@@ -89,6 +99,19 @@ public class OutlineProviders{
|
||||
}
|
||||
}
|
||||
|
||||
private static class BottomRoundRectOutlineProvider extends ViewOutlineProvider{
|
||||
private final int radius;
|
||||
|
||||
private BottomRoundRectOutlineProvider(int radius){
|
||||
this.radius=radius;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void getOutline(View view, Outline outline){
|
||||
outline.setRoundRect(0, -radius, view.getWidth(), view.getHeight(), radius);
|
||||
}
|
||||
}
|
||||
|
||||
private static class EndRoundRectOutlineProvider extends ViewOutlineProvider{
|
||||
private final int radius;
|
||||
|
||||
|
||||
@@ -0,0 +1,134 @@
|
||||
package org.joinmastodon.android.ui;
|
||||
|
||||
import android.content.Context;
|
||||
import android.content.res.ColorStateList;
|
||||
import android.text.InputType;
|
||||
import android.text.TextUtils;
|
||||
import android.view.View;
|
||||
import android.view.ViewGroup;
|
||||
import android.view.inputmethod.EditorInfo;
|
||||
import android.widget.EditText;
|
||||
import android.widget.ImageButton;
|
||||
import android.widget.LinearLayout;
|
||||
import android.widget.Toolbar;
|
||||
|
||||
import org.joinmastodon.android.R;
|
||||
import org.joinmastodon.android.ui.utils.SimpleTextWatcher;
|
||||
import org.joinmastodon.android.ui.utils.UiUtils;
|
||||
|
||||
import java.util.function.Consumer;
|
||||
|
||||
import me.grishka.appkit.utils.V;
|
||||
|
||||
public class SearchViewHelper{
|
||||
private LinearLayout searchLayout;
|
||||
private EditText searchEdit;
|
||||
private ImageButton clearSearchButton;
|
||||
private View divider;
|
||||
private String currentQuery;
|
||||
private Consumer<String> listener;
|
||||
private Runnable debouncer=()->{
|
||||
currentQuery=searchEdit.getText().toString();
|
||||
if(listener!=null){
|
||||
listener.accept(currentQuery);
|
||||
}
|
||||
};
|
||||
private boolean isEmpty=true;
|
||||
private Runnable enterCallback;
|
||||
private Consumer<String> listenerWithoutDebounce;
|
||||
|
||||
public SearchViewHelper(Context context, Context toolbarContext, String hint){
|
||||
searchLayout=new LinearLayout(context);
|
||||
searchLayout.setOrientation(LinearLayout.HORIZONTAL);
|
||||
|
||||
searchEdit=new EditText(context);
|
||||
searchEdit.setHint(hint);
|
||||
searchEdit.setInputType(InputType.TYPE_TEXT_VARIATION_FILTER);
|
||||
searchEdit.setBackground(null);
|
||||
searchEdit.setPadding(0, 0, 0, 0);
|
||||
searchEdit.addTextChangedListener(new SimpleTextWatcher(e->{
|
||||
searchEdit.removeCallbacks(debouncer);
|
||||
searchEdit.postDelayed(debouncer, 300);
|
||||
boolean newIsEmpty=e.length()==0;
|
||||
if(isEmpty!=newIsEmpty){
|
||||
isEmpty=newIsEmpty;
|
||||
V.setVisibilityAnimated(clearSearchButton, isEmpty ? View.GONE : View.VISIBLE);
|
||||
}
|
||||
if(listenerWithoutDebounce!=null)
|
||||
listenerWithoutDebounce.accept(e.toString());
|
||||
}));
|
||||
searchEdit.setImeOptions(EditorInfo.IME_ACTION_SEARCH);
|
||||
searchEdit.setOnEditorActionListener((v, actionId, event)->{
|
||||
searchEdit.removeCallbacks(debouncer);
|
||||
debouncer.run();
|
||||
if(enterCallback!=null)
|
||||
enterCallback.run();
|
||||
return true;
|
||||
});
|
||||
searchEdit.setTextAppearance(R.style.m3_body_large);
|
||||
searchEdit.setHintTextColor(UiUtils.getThemeColor(toolbarContext, R.attr.colorM3OnSurfaceVariant));
|
||||
searchEdit.setTextColor(UiUtils.getThemeColor(toolbarContext, R.attr.colorM3OnSurface));
|
||||
searchLayout.addView(searchEdit, new LinearLayout.LayoutParams(0, ViewGroup.LayoutParams.MATCH_PARENT, 1f));
|
||||
|
||||
clearSearchButton=new ImageButton(context);
|
||||
clearSearchButton.setImageResource(R.drawable.ic_baseline_close_24);
|
||||
clearSearchButton.setContentDescription(context.getString(R.string.clear));
|
||||
clearSearchButton.setImageTintList(ColorStateList.valueOf(UiUtils.getThemeColor(context, R.attr.colorM3OnSurfaceVariant)));
|
||||
clearSearchButton.setBackground(UiUtils.getThemeDrawable(toolbarContext, android.R.attr.actionBarItemBackground));
|
||||
clearSearchButton.setOnClickListener(v->{
|
||||
searchEdit.setText("");
|
||||
searchEdit.removeCallbacks(debouncer);
|
||||
debouncer.run();
|
||||
});
|
||||
clearSearchButton.setVisibility(View.GONE);
|
||||
searchLayout.addView(clearSearchButton, new LinearLayout.LayoutParams(V.dp(56), ViewGroup.LayoutParams.MATCH_PARENT));
|
||||
}
|
||||
|
||||
public void setListeners(Consumer<String> listener, Consumer<String> listenerWithoutDebounce){
|
||||
this.listener=listener;
|
||||
this.listenerWithoutDebounce=listenerWithoutDebounce;
|
||||
}
|
||||
|
||||
public void install(Toolbar toolbar){
|
||||
toolbar.getLayoutParams().height=V.dp(72);
|
||||
toolbar.setMinimumHeight(V.dp(72));
|
||||
if(searchLayout.getParent()!=null)
|
||||
((ViewGroup) searchLayout.getParent()).removeView(searchLayout);
|
||||
toolbar.addView(searchLayout, new Toolbar.LayoutParams(ViewGroup.LayoutParams.MATCH_PARENT, ViewGroup.LayoutParams.MATCH_PARENT));
|
||||
toolbar.setBackgroundResource(R.drawable.bg_m3_surface3);
|
||||
searchEdit.requestFocus();
|
||||
}
|
||||
|
||||
public void addDivider(ViewGroup contentView){
|
||||
divider=new View(contentView.getContext());
|
||||
divider.setBackgroundColor(UiUtils.getThemeColor(contentView.getContext(), R.attr.colorM3Outline));
|
||||
contentView.addView(divider, 1, new LinearLayout.LayoutParams(ViewGroup.LayoutParams.MATCH_PARENT, V.dp(1)));
|
||||
}
|
||||
|
||||
public LinearLayout getSearchLayout(){
|
||||
return searchLayout;
|
||||
}
|
||||
|
||||
public void setEnterCallback(Runnable enterCallback){
|
||||
this.enterCallback=enterCallback;
|
||||
}
|
||||
|
||||
public void setQuery(String q){
|
||||
currentQuery=q;
|
||||
searchEdit.setText(currentQuery);
|
||||
searchEdit.setSelection(searchEdit.length());
|
||||
searchEdit.removeCallbacks(debouncer);
|
||||
}
|
||||
|
||||
public String getQuery(){
|
||||
return currentQuery;
|
||||
}
|
||||
|
||||
public View getDivider(){
|
||||
return divider;
|
||||
}
|
||||
|
||||
public EditText getSearchEdit(){
|
||||
return searchEdit;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,87 @@
|
||||
package org.joinmastodon.android.ui;
|
||||
|
||||
import android.content.Context;
|
||||
import android.view.View;
|
||||
import android.widget.FrameLayout;
|
||||
|
||||
import java.util.Objects;
|
||||
|
||||
import androidx.annotation.NonNull;
|
||||
import androidx.recyclerview.widget.RecyclerView;
|
||||
|
||||
public class StickyHeadersOverlay{
|
||||
private static final String TAG="StickyHeadersOverlay";
|
||||
|
||||
private FrameLayout headerWrapper;
|
||||
private Context context;
|
||||
private RecyclerView parent;
|
||||
private RecyclerView.ViewHolder currentHeaderHolder;
|
||||
private int headerViewType;
|
||||
|
||||
public StickyHeadersOverlay(Context context, int headerViewType){
|
||||
this.context=context;
|
||||
this.headerViewType=headerViewType;
|
||||
headerWrapper=new FrameLayout(context);
|
||||
}
|
||||
|
||||
public void install(RecyclerView parent){
|
||||
if(this.parent!=null)
|
||||
throw new IllegalStateException();
|
||||
this.parent=parent;
|
||||
parent.getViewTreeObserver().addOnPreDrawListener(()->{
|
||||
if(parent.getWidth()!=headerWrapper.getWidth() || parent.getHeight()!=headerWrapper.getHeight()){
|
||||
headerWrapper.measure(parent.getWidth() | View.MeasureSpec.EXACTLY, parent.getHeight() | View.MeasureSpec.EXACTLY);
|
||||
headerWrapper.layout(0, 0, parent.getWidth(), parent.getHeight());
|
||||
}
|
||||
return true;
|
||||
});
|
||||
parent.getOverlay().add(headerWrapper);
|
||||
|
||||
parent.addOnScrollListener(new RecyclerView.OnScrollListener(){
|
||||
@Override
|
||||
public void onScrolled(@NonNull RecyclerView recyclerView, int dx, int dy){
|
||||
if(currentHeaderHolder==null){
|
||||
currentHeaderHolder=parent.getAdapter().createViewHolder(parent, headerViewType);
|
||||
headerWrapper.addView(currentHeaderHolder.itemView);
|
||||
}
|
||||
int firstVisiblePos=parent.getChildAdapterPosition(parent.getChildAt(0));
|
||||
RecyclerView.Adapter<RecyclerView.ViewHolder> adapter=Objects.requireNonNull(parent.getAdapter());
|
||||
// Go backwards from the first visible position to find the previous header
|
||||
for(int i=firstVisiblePos;i>=0;i--){
|
||||
if(adapter.getItemViewType(i)==headerViewType){
|
||||
if(currentHeaderHolder.getAbsoluteAdapterPosition()!=i){
|
||||
adapter.bindViewHolder(currentHeaderHolder, i);
|
||||
}
|
||||
break;
|
||||
}
|
||||
}
|
||||
if(currentHeaderHolder instanceof HeaderViewHolder hvh){
|
||||
hvh.setStickyFactor(firstVisiblePos==0 && parent.getChildAt(0).getTop()==0 ? 0 : 1);
|
||||
}
|
||||
// Now go forward and find the next header view to possibly offset the current one
|
||||
for(int i=firstVisiblePos+1;i<adapter.getItemCount();i++){
|
||||
if(adapter.getItemViewType(i)==headerViewType){
|
||||
RecyclerView.ViewHolder holder=parent.findViewHolderForAdapterPosition(i);
|
||||
if(holder!=null){
|
||||
float factor;
|
||||
if(holder.itemView.getTop()<currentHeaderHolder.itemView.getBottom()){
|
||||
currentHeaderHolder.itemView.setTranslationY(holder.itemView.getTop()-currentHeaderHolder.itemView.getBottom());
|
||||
factor=1f-holder.itemView.getTop()/(float)currentHeaderHolder.itemView.getBottom();
|
||||
}else{
|
||||
currentHeaderHolder.itemView.setTranslationY(0);
|
||||
factor=0;
|
||||
}
|
||||
if(holder instanceof HeaderViewHolder hvh)
|
||||
hvh.setStickyFactor(factor);
|
||||
}
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
public interface HeaderViewHolder{
|
||||
void setStickyFactor(float factor);
|
||||
}
|
||||
}
|
||||
@@ -1,44 +0,0 @@
|
||||
package org.joinmastodon.android.ui;
|
||||
|
||||
import android.content.Context;
|
||||
import android.util.AttributeSet;
|
||||
import android.view.View;
|
||||
|
||||
import androidx.annotation.NonNull;
|
||||
import androidx.recyclerview.widget.GridLayoutManager;
|
||||
import androidx.recyclerview.widget.RecyclerView;
|
||||
|
||||
public class TileGridLayoutManager extends GridLayoutManager{
|
||||
private static final String TAG="TileGridLayoutManager";
|
||||
private int lastWidth=0;
|
||||
|
||||
public TileGridLayoutManager(Context context, AttributeSet attrs, int defStyleAttr, int defStyleRes){
|
||||
super(context, attrs, defStyleAttr, defStyleRes);
|
||||
}
|
||||
|
||||
public TileGridLayoutManager(Context context, int spanCount){
|
||||
super(context, spanCount);
|
||||
}
|
||||
|
||||
public TileGridLayoutManager(Context context, int spanCount, int orientation, boolean reverseLayout){
|
||||
super(context, spanCount, orientation, reverseLayout);
|
||||
}
|
||||
|
||||
@Override
|
||||
public int getColumnCountForAccessibility(RecyclerView.Recycler recycler, RecyclerView.State state){
|
||||
return 1;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onMeasure(@NonNull RecyclerView.Recycler recycler, @NonNull RecyclerView.State state, int widthSpec, int heightSpec){
|
||||
int width=View.MeasureSpec.getSize(widthSpec);
|
||||
// Is there a better way to invalidate item decorations when the size changes?
|
||||
if(lastWidth!=width){
|
||||
lastWidth=width;
|
||||
if(getChildCount()>0){
|
||||
((RecyclerView)getChildAt(0).getParent()).invalidateItemDecorations();
|
||||
}
|
||||
}
|
||||
super.onMeasure(recycler, state, widthSpec, heightSpec);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,54 @@
|
||||
package org.joinmastodon.android.ui.adapters;
|
||||
|
||||
import android.view.ViewGroup;
|
||||
|
||||
import org.joinmastodon.android.R;
|
||||
import org.joinmastodon.android.model.viewmodel.ListItem;
|
||||
import org.joinmastodon.android.ui.viewholders.CheckboxOrRadioListItemViewHolder;
|
||||
import org.joinmastodon.android.ui.viewholders.ListItemViewHolder;
|
||||
import org.joinmastodon.android.ui.viewholders.SimpleListItemViewHolder;
|
||||
import org.joinmastodon.android.ui.viewholders.SwitchListItemViewHolder;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
import androidx.annotation.NonNull;
|
||||
import androidx.recyclerview.widget.RecyclerView;
|
||||
|
||||
public class GenericListItemsAdapter<T> extends RecyclerView.Adapter<ListItemViewHolder<?>>{
|
||||
private List<ListItem<T>> items;
|
||||
|
||||
public GenericListItemsAdapter(List<ListItem<T>> items){
|
||||
this.items=items;
|
||||
}
|
||||
|
||||
@NonNull
|
||||
@Override
|
||||
public ListItemViewHolder<?> onCreateViewHolder(@NonNull ViewGroup parent, int viewType){
|
||||
if(viewType==R.id.list_item_simple || viewType==R.id.list_item_simple_tinted)
|
||||
return new SimpleListItemViewHolder(parent.getContext(), parent);
|
||||
if(viewType==R.id.list_item_switch)
|
||||
return new SwitchListItemViewHolder(parent.getContext(), parent);
|
||||
if(viewType==R.id.list_item_checkbox)
|
||||
return new CheckboxOrRadioListItemViewHolder(parent.getContext(), parent, false);
|
||||
if(viewType==R.id.list_item_radio)
|
||||
return new CheckboxOrRadioListItemViewHolder(parent.getContext(), parent, true);
|
||||
|
||||
throw new IllegalArgumentException("Unexpected view type "+viewType);
|
||||
}
|
||||
|
||||
@SuppressWarnings("unchecked")
|
||||
@Override
|
||||
public void onBindViewHolder(@NonNull ListItemViewHolder<?> holder, int position){
|
||||
((ListItemViewHolder<ListItem<T>>)holder).bind(items.get(position));
|
||||
}
|
||||
|
||||
@Override
|
||||
public int getItemCount(){
|
||||
return items.size();
|
||||
}
|
||||
|
||||
@Override
|
||||
public int getItemViewType(int position){
|
||||
return items.get(position).getItemViewType();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,36 @@
|
||||
package org.joinmastodon.android.ui.adapters;
|
||||
|
||||
import android.view.ViewGroup;
|
||||
|
||||
import org.joinmastodon.android.model.Instance;
|
||||
import org.joinmastodon.android.ui.viewholders.InstanceRuleViewHolder;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
import androidx.annotation.NonNull;
|
||||
import androidx.recyclerview.widget.RecyclerView;
|
||||
|
||||
public class InstanceRulesAdapter extends RecyclerView.Adapter<InstanceRuleViewHolder>{
|
||||
private final List<Instance.Rule> rules;
|
||||
|
||||
public InstanceRulesAdapter(List<Instance.Rule> rules){
|
||||
this.rules=rules;
|
||||
}
|
||||
|
||||
@NonNull
|
||||
@Override
|
||||
public InstanceRuleViewHolder onCreateViewHolder(@NonNull ViewGroup parent, int viewType){
|
||||
return new InstanceRuleViewHolder(parent);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onBindViewHolder(@NonNull InstanceRuleViewHolder holder, int position){
|
||||
holder.setPosition(position);
|
||||
holder.bind(rules.get(position));
|
||||
}
|
||||
|
||||
@Override
|
||||
public int getItemCount(){
|
||||
return rules.size();
|
||||
}
|
||||
}
|
||||
@@ -132,7 +132,7 @@ public class AccountCardStatusDisplayItem extends StatusDisplayItem{
|
||||
postsCount.setText(UiUtils.abbreviateNumber(item.account.statusesCount));
|
||||
followersLabel.setText(item.parentFragment.getResources().getQuantityString(R.plurals.followers, (int)Math.min(999, item.account.followersCount)));
|
||||
followingLabel.setText(item.parentFragment.getResources().getQuantityString(R.plurals.following, (int)Math.min(999, item.account.followingCount)));
|
||||
postsLabel.setText(item.parentFragment.getResources().getQuantityString(R.plurals.posts, (int)Math.min(999, item.account.statusesCount)));
|
||||
postsLabel.setText(item.parentFragment.getResources().getQuantityString(R.plurals.x_posts, (int)(item.account.statusesCount%1000), item.account.statusesCount));
|
||||
followersCount.setVisibility(item.account.followersCount < 0 ? View.GONE : View.VISIBLE);
|
||||
followersLabel.setVisibility(item.account.followersCount < 0 ? View.GONE : View.VISIBLE);
|
||||
followingCount.setVisibility(item.account.followingCount < 0 ? View.GONE : View.VISIBLE);
|
||||
@@ -156,13 +156,14 @@ public class AccountCardStatusDisplayItem extends StatusDisplayItem{
|
||||
actionWrap.setVisibility(View.VISIBLE);
|
||||
acceptWrap.setVisibility(View.GONE);
|
||||
rejectWrap.setVisibility(View.GONE);
|
||||
UiUtils.setRelationshipToActionButton(relationship, actionButton);
|
||||
UiUtils.setRelationshipToActionButtonM3(relationship, actionButton);
|
||||
}
|
||||
}
|
||||
|
||||
private void onFollowRequestButtonClick(View v) {
|
||||
itemView.setHasTransientState(true);
|
||||
UiUtils.handleFollowRequest((Activity) v.getContext(), item.account, item.parentFragment.getAccountID(), null, v == acceptButton, relationship, rel -> {
|
||||
if(v.getContext()==null) return;
|
||||
itemView.setHasTransientState(false);
|
||||
item.parentFragment.putRelationship(item.account.id, rel);
|
||||
RecyclerView.Adapter<? extends RecyclerView.ViewHolder> adapter = getBindingAdapter();
|
||||
@@ -180,6 +181,7 @@ public class AccountCardStatusDisplayItem extends StatusDisplayItem{
|
||||
private void onActionButtonClick(View v){
|
||||
itemView.setHasTransientState(true);
|
||||
UiUtils.performAccountAction((Activity) v.getContext(), item.account, item.parentFragment.getAccountID(), relationship, actionButton, this::setActionProgressVisible, rel->{
|
||||
if(v.getContext()==null) return;
|
||||
itemView.setHasTransientState(false);
|
||||
item.parentFragment.putRelationship(item.account.id, rel);
|
||||
rebind();
|
||||
|
||||
@@ -1,38 +1,21 @@
|
||||
package org.joinmastodon.android.ui.displayitems;
|
||||
|
||||
import android.content.Context;
|
||||
import android.graphics.drawable.Animatable;
|
||||
import android.graphics.drawable.Drawable;
|
||||
import android.text.TextUtils;
|
||||
import android.view.ViewGroup;
|
||||
import android.widget.ImageView;
|
||||
import android.widget.TextView;
|
||||
|
||||
import org.joinmastodon.android.R;
|
||||
import org.joinmastodon.android.fragments.BaseStatusListFragment;
|
||||
import org.joinmastodon.android.model.Account;
|
||||
import org.joinmastodon.android.ui.OutlineProviders;
|
||||
import org.joinmastodon.android.ui.text.HtmlParser;
|
||||
import org.joinmastodon.android.ui.utils.CustomEmojiHelper;
|
||||
import org.joinmastodon.android.model.viewmodel.AccountViewModel;
|
||||
import org.joinmastodon.android.ui.viewholders.AccountViewHolder;
|
||||
|
||||
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.V;
|
||||
|
||||
public class AccountStatusDisplayItem extends StatusDisplayItem{
|
||||
public final Account account;
|
||||
private CustomEmojiHelper emojiHelper=new CustomEmojiHelper();
|
||||
private CharSequence parsedName;
|
||||
public ImageLoaderRequest avaRequest;
|
||||
public final AccountViewModel account;
|
||||
|
||||
public AccountStatusDisplayItem(String parentID, BaseStatusListFragment parentFragment, Account account){
|
||||
super(parentID, parentFragment);
|
||||
this.account=account;
|
||||
parsedName=HtmlParser.parseCustomEmoji(account.displayName, account.emojis);
|
||||
emojiHelper.setText(parsedName);
|
||||
if(!TextUtils.isEmpty(account.avatar))
|
||||
avaRequest=new UrlImageLoaderRequest(account.avatar, V.dp(50), V.dp(50));
|
||||
this.account=new AccountViewModel(account, parentFragment.getAccountID());
|
||||
}
|
||||
|
||||
@Override
|
||||
@@ -42,51 +25,43 @@ public class AccountStatusDisplayItem extends StatusDisplayItem{
|
||||
|
||||
@Override
|
||||
public int getImageCount(){
|
||||
return 1+emojiHelper.getImageCount();
|
||||
return 1+account.emojiHelper.getImageCount();
|
||||
}
|
||||
|
||||
@Override
|
||||
public ImageLoaderRequest getImageRequest(int index){
|
||||
if(index==0)
|
||||
return avaRequest;
|
||||
return emojiHelper.getImageRequest(index-1);
|
||||
return account.avaRequest;
|
||||
return account.emojiHelper.getImageRequest(index-1);
|
||||
}
|
||||
|
||||
public static class Holder extends StatusDisplayItem.Holder<AccountStatusDisplayItem> implements ImageLoaderViewHolder{
|
||||
private final TextView name, username;
|
||||
private final ImageView photo;
|
||||
private final AccountViewHolder realHolder;
|
||||
|
||||
public Holder(Context context, ViewGroup parent){
|
||||
super(context, R.layout.display_item_account, parent);
|
||||
name=findViewById(R.id.name);
|
||||
username=findViewById(R.id.username);
|
||||
photo=findViewById(R.id.photo);
|
||||
|
||||
photo.setOutlineProvider(OutlineProviders.roundedRect(12));
|
||||
photo.setClipToOutline(true);
|
||||
public Holder(AccountViewHolder realHolder){
|
||||
super(realHolder.itemView);
|
||||
this.realHolder=realHolder;
|
||||
realHolder.setStyle(AccountViewHolder.AccessoryType.NONE, false);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onBind(AccountStatusDisplayItem item){
|
||||
name.setText(item.parsedName);
|
||||
username.setText("@"+item.account.acct);
|
||||
realHolder.bind(item.account);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void setImage(int index, Drawable image){
|
||||
if(image instanceof Animatable && !((Animatable) image).isRunning())
|
||||
((Animatable) image).start();
|
||||
if(index==0){
|
||||
photo.setImageDrawable(image);
|
||||
}else{
|
||||
item.emojiHelper.setImageDrawable(index-1, image);
|
||||
name.invalidate();
|
||||
}
|
||||
realHolder.setImage(index, image);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void clearImage(int index){
|
||||
setImage(index, null);
|
||||
realHolder.clearImage(index);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onClick(){
|
||||
realHolder.onClick();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,11 +1,19 @@
|
||||
package org.joinmastodon.android.ui.displayitems;
|
||||
|
||||
import android.annotation.SuppressLint;
|
||||
import android.content.Context;
|
||||
import android.graphics.Bitmap;
|
||||
import android.graphics.Color;
|
||||
import android.graphics.drawable.BitmapDrawable;
|
||||
import android.graphics.drawable.Drawable;
|
||||
import android.os.Build;
|
||||
import android.os.SystemClock;
|
||||
import android.text.TextUtils;
|
||||
import android.view.View;
|
||||
import android.view.ViewGroup;
|
||||
import android.widget.FrameLayout;
|
||||
import android.widget.ImageButton;
|
||||
import android.widget.SeekBar;
|
||||
import android.widget.ImageView;
|
||||
import android.widget.TextView;
|
||||
|
||||
import org.joinmastodon.android.AudioPlayerService;
|
||||
@@ -13,16 +21,26 @@ 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.drawables.SeekBarThumbDrawable;
|
||||
import org.joinmastodon.android.ui.OutlineProviders;
|
||||
import org.joinmastodon.android.ui.drawables.AudioAttachmentBackgroundDrawable;
|
||||
import org.joinmastodon.android.ui.utils.UiUtils;
|
||||
|
||||
import androidx.palette.graphics.Palette;
|
||||
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.V;
|
||||
|
||||
public class AudioStatusDisplayItem extends StatusDisplayItem{
|
||||
public final Status status;
|
||||
public final Attachment attachment;
|
||||
private final ImageLoaderRequest imageRequest;
|
||||
|
||||
public AudioStatusDisplayItem(String parentID, BaseStatusListFragment parentFragment, Status status, Attachment attachment){
|
||||
super(parentID, parentFragment);
|
||||
this.status=status;
|
||||
this.attachment=attachment;
|
||||
imageRequest=new UrlImageLoaderRequest(TextUtils.isEmpty(attachment.previewUrl) ? status.account.avatarStatic : attachment.previewUrl, V.dp(100), V.dp(100));
|
||||
}
|
||||
|
||||
@Override
|
||||
@@ -30,25 +48,38 @@ public class AudioStatusDisplayItem extends StatusDisplayItem{
|
||||
return Type.AUDIO;
|
||||
}
|
||||
|
||||
public static class Holder extends StatusDisplayItem.Holder<AudioStatusDisplayItem> implements AudioPlayerService.Callback{
|
||||
private final ImageButton playPauseBtn;
|
||||
@Override
|
||||
public int getImageCount(){
|
||||
return 1;
|
||||
}
|
||||
|
||||
@Override
|
||||
public ImageLoaderRequest getImageRequest(int index){
|
||||
return imageRequest;
|
||||
}
|
||||
|
||||
public static class Holder extends StatusDisplayItem.Holder<AudioStatusDisplayItem> implements AudioPlayerService.Callback, ImageLoaderViewHolder{
|
||||
private final ImageButton playPauseBtn, forwardBtn, rewindBtn;
|
||||
private final TextView time;
|
||||
private final SeekBar seekBar;
|
||||
private final ImageView image;
|
||||
private final FrameLayout content;
|
||||
private final AudioAttachmentBackgroundDrawable bgDrawable;
|
||||
|
||||
private int lastKnownPosition;
|
||||
private long lastKnownPositionTime;
|
||||
private boolean playing;
|
||||
private int lastRemainingSeconds=-1;
|
||||
private boolean seekbarBeingDragged;
|
||||
private int lastPosSeconds=-1;
|
||||
private AudioPlayerService.PlayState state;
|
||||
|
||||
private Runnable positionUpdater=this::updatePosition;
|
||||
private final Runnable positionUpdater=this::updatePosition;
|
||||
|
||||
public Holder(Context context, ViewGroup parent){
|
||||
super(context, R.layout.display_item_audio, parent);
|
||||
playPauseBtn=findViewById(R.id.play_pause_btn);
|
||||
time=findViewById(R.id.time);
|
||||
seekBar=findViewById(R.id.seekbar);
|
||||
seekBar.setThumb(new SeekBarThumbDrawable(context));
|
||||
image=findViewById(R.id.image);
|
||||
content=findViewById(R.id.content);
|
||||
forwardBtn=findViewById(R.id.forward_btn);
|
||||
rewindBtn=findViewById(R.id.rewind_btn);
|
||||
playPauseBtn.setOnClickListener(this::onPlayPauseClick);
|
||||
itemView.addOnAttachStateChangeListener(new View.OnAttachStateChangeListener(){
|
||||
@Override
|
||||
@@ -61,76 +92,71 @@ public class AudioStatusDisplayItem extends StatusDisplayItem{
|
||||
AudioPlayerService.unregisterCallback(Holder.this);
|
||||
}
|
||||
});
|
||||
seekBar.setOnSeekBarChangeListener(new SeekBar.OnSeekBarChangeListener(){
|
||||
@Override
|
||||
public void onProgressChanged(SeekBar seekBar, int progress, boolean fromUser){
|
||||
if(fromUser){
|
||||
int seconds=(int)(seekBar.getProgress()/10000.0*item.attachment.getDuration());
|
||||
time.setText(formatDuration(seconds));
|
||||
}
|
||||
}
|
||||
forwardBtn.setOnClickListener(this::onSeekButtonClick);
|
||||
rewindBtn.setOnClickListener(this::onSeekButtonClick);
|
||||
|
||||
@Override
|
||||
public void onStartTrackingTouch(SeekBar seekBar){
|
||||
seekbarBeingDragged=true;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onStopTrackingTouch(SeekBar seekBar){
|
||||
AudioPlayerService service=AudioPlayerService.getInstance();
|
||||
if(service!=null && service.getAttachmentID().equals(item.attachment.id)){
|
||||
service.seekTo((int)(seekBar.getProgress()/10000.0*item.attachment.getDuration()*1000.0));
|
||||
}
|
||||
seekbarBeingDragged=false;
|
||||
if(playing)
|
||||
itemView.postOnAnimation(positionUpdater);
|
||||
}
|
||||
});
|
||||
image.setOutlineProvider(OutlineProviders.OVAL);
|
||||
image.setClipToOutline(true);
|
||||
content.setBackground(bgDrawable=new AudioAttachmentBackgroundDrawable());
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onBind(AudioStatusDisplayItem item){
|
||||
int seconds=(int)item.attachment.getDuration();
|
||||
String duration=formatDuration(seconds);
|
||||
// Some fonts (not Roboto) have different-width digits. 0 is supposedly the widest.
|
||||
time.getLayoutParams().width=(int)Math.ceil(Math.max(time.getPaint().measureText("-"+duration),
|
||||
time.getPaint().measureText("-"+duration.replaceAll("\\d", "0"))));
|
||||
time.setText(duration);
|
||||
String duration=UiUtils.formatMediaDuration(seconds);
|
||||
AudioPlayerService service=AudioPlayerService.getInstance();
|
||||
if(service!=null && service.getAttachmentID().equals(item.attachment.id)){
|
||||
seekBar.setEnabled(true);
|
||||
onPlayStateChanged(item.attachment.id, service.isPlaying(), service.getPosition());
|
||||
forwardBtn.setVisibility(View.VISIBLE);
|
||||
rewindBtn.setVisibility(View.VISIBLE);
|
||||
onPlayStateChanged(item.attachment.id, service.getPlayState(), service.getPosition());
|
||||
actuallyUpdatePosition();
|
||||
}else{
|
||||
seekBar.setEnabled(false);
|
||||
state=null;
|
||||
time.setText(duration);
|
||||
forwardBtn.setVisibility(View.INVISIBLE);
|
||||
rewindBtn.setVisibility(View.INVISIBLE);
|
||||
setPlayButtonPlaying(false, false);
|
||||
}
|
||||
|
||||
int mainColor;
|
||||
if(item.attachment.meta!=null && item.attachment.meta.colors!=null){
|
||||
try{
|
||||
mainColor=Color.parseColor(item.attachment.meta.colors.background);
|
||||
}catch(IllegalArgumentException x){
|
||||
mainColor=0xff808080;
|
||||
}
|
||||
}else{
|
||||
mainColor=0xff808080;
|
||||
}
|
||||
updateColors(mainColor);
|
||||
}
|
||||
|
||||
private void onPlayPauseClick(View v){
|
||||
AudioPlayerService service=AudioPlayerService.getInstance();
|
||||
if(service!=null && service.getAttachmentID().equals(item.attachment.id)){
|
||||
if(playing)
|
||||
if(state!=AudioPlayerService.PlayState.PAUSED)
|
||||
service.pause(true);
|
||||
else
|
||||
service.play();
|
||||
}else{
|
||||
AudioPlayerService.start(v.getContext(), item.status, item.attachment);
|
||||
onPlayStateChanged(item.attachment.id, true, 0);
|
||||
seekBar.setEnabled(true);
|
||||
onPlayStateChanged(item.attachment.id, AudioPlayerService.PlayState.BUFFERING, 0);
|
||||
forwardBtn.setVisibility(View.VISIBLE);
|
||||
rewindBtn.setVisibility(View.VISIBLE);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onPlayStateChanged(String attachmentID, boolean playing, int position){
|
||||
public void onPlayStateChanged(String attachmentID, AudioPlayerService.PlayState state, int position){
|
||||
if(attachmentID.equals(item.attachment.id)){
|
||||
this.lastKnownPosition=position;
|
||||
lastKnownPositionTime=SystemClock.uptimeMillis();
|
||||
this.playing=playing;
|
||||
playPauseBtn.setImageResource(playing ? R.drawable.ic_fluent_pause_circle_24_filled : R.drawable.ic_fluent_play_circle_24_filled);
|
||||
if(!playing){
|
||||
lastRemainingSeconds=-1;
|
||||
time.setText(formatDuration((int) item.attachment.getDuration()));
|
||||
}else{
|
||||
this.state=state;
|
||||
setPlayButtonPlaying(state!=AudioPlayerService.PlayState.PAUSED, true);
|
||||
if(state==AudioPlayerService.PlayState.PLAYING){
|
||||
itemView.postOnAnimation(positionUpdater);
|
||||
}else if(state==AudioPlayerService.PlayState.BUFFERING){
|
||||
actuallyUpdatePosition();
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -138,32 +164,88 @@ public class AudioStatusDisplayItem extends StatusDisplayItem{
|
||||
@Override
|
||||
public void onPlaybackStopped(String attachmentID){
|
||||
if(attachmentID.equals(item.attachment.id)){
|
||||
playing=false;
|
||||
playPauseBtn.setImageResource(R.drawable.ic_fluent_play_circle_24_filled);
|
||||
seekBar.setProgress(0);
|
||||
seekBar.setEnabled(false);
|
||||
time.setText(formatDuration((int)item.attachment.getDuration()));
|
||||
state=null;
|
||||
setPlayButtonPlaying(false, true);
|
||||
forwardBtn.setVisibility(View.INVISIBLE);
|
||||
rewindBtn.setVisibility(View.INVISIBLE);
|
||||
time.setText(UiUtils.formatMediaDuration((int)item.attachment.getDuration()));
|
||||
}
|
||||
}
|
||||
|
||||
private String formatDuration(int seconds){
|
||||
if(seconds>=3600)
|
||||
return String.format("%d:%02d:%02d", seconds/3600, seconds%3600/60, seconds%60);
|
||||
else
|
||||
return String.format("%d:%02d", seconds/60, seconds%60);
|
||||
}
|
||||
|
||||
private void updatePosition(){
|
||||
if(!playing || seekbarBeingDragged)
|
||||
if(state!=AudioPlayerService.PlayState.PLAYING)
|
||||
return;
|
||||
double pos=lastKnownPosition/1000.0+(SystemClock.uptimeMillis()-lastKnownPositionTime)/1000.0;
|
||||
seekBar.setProgress((int)Math.round(pos/item.attachment.getDuration()*10000.0));
|
||||
actuallyUpdatePosition();
|
||||
itemView.postOnAnimation(positionUpdater);
|
||||
int remainingSeconds=(int)(item.attachment.getDuration()-pos);
|
||||
if(remainingSeconds!=lastRemainingSeconds){
|
||||
lastRemainingSeconds=remainingSeconds;
|
||||
time.setText("-"+formatDuration(remainingSeconds));
|
||||
}
|
||||
|
||||
@SuppressLint("SetTextI18n")
|
||||
private void actuallyUpdatePosition(){
|
||||
double pos=lastKnownPosition/1000.0;
|
||||
if(state==AudioPlayerService.PlayState.PLAYING)
|
||||
pos+=(SystemClock.uptimeMillis()-lastKnownPositionTime)/1000.0;
|
||||
int posSeconds=(int)pos;
|
||||
if(posSeconds!=lastPosSeconds){
|
||||
lastPosSeconds=posSeconds;
|
||||
time.setText(UiUtils.formatMediaDuration(posSeconds)+"/"+UiUtils.formatMediaDuration((int)item.attachment.getDuration()));
|
||||
}
|
||||
}
|
||||
|
||||
private void updateColors(int mainColor){
|
||||
float[] hsv={0, 0, 0};
|
||||
float[] hsv2={0, 0, 0};
|
||||
Color.colorToHSV(mainColor, hsv);
|
||||
boolean isGray=hsv[1]<0.2f;
|
||||
boolean isDarkTheme=UiUtils.isDarkTheme();
|
||||
hsv2[0]=hsv[0];
|
||||
hsv2[1]=isGray ? hsv[1] : (isDarkTheme ? 0.6f : 0.4f);
|
||||
hsv2[2]=isDarkTheme ? 0.3f : 0.75f;
|
||||
int bgColor=Color.HSVToColor(hsv2);
|
||||
hsv2[1]=isGray ? hsv[1] : (isDarkTheme ? 0.3f : 0.6f);
|
||||
hsv2[2]=isDarkTheme ? 0.6f : 0.4f;
|
||||
bgDrawable.setColors(bgColor, Color.HSVToColor(128, hsv2));
|
||||
|
||||
hsv2[1]=isGray ? hsv[1] : 0.1f;
|
||||
hsv2[2]=1;
|
||||
int controlsColor=Color.HSVToColor(hsv2);
|
||||
time.setTextColor(controlsColor);
|
||||
forwardBtn.setColorFilter(controlsColor);
|
||||
rewindBtn.setColorFilter(controlsColor);
|
||||
}
|
||||
|
||||
private void setPlayButtonPlaying(boolean playing, boolean animated){
|
||||
playPauseBtn.setImageResource(playing ? R.drawable.ic_fluent_pause_48_regular : R.drawable.ic_fluent_play_48_regular);
|
||||
playPauseBtn.setContentDescription(item.parentFragment.getString(playing ? R.string.pause : R.string.play));
|
||||
if(playing)
|
||||
bgDrawable.startAnimation();
|
||||
else
|
||||
bgDrawable.stopAnimation(animated);
|
||||
}
|
||||
|
||||
private void onSeekButtonClick(View v){
|
||||
int seekAmount=v.getId()==R.id.forward_btn ? 10_000 : -5_000;
|
||||
AudioPlayerService service=AudioPlayerService.getInstance();
|
||||
if(service!=null && service.getAttachmentID().equals(item.attachment.id)){
|
||||
int newPos=Math.min(Math.max(0, service.getPosition()+seekAmount), (int)(item.attachment.getDuration()*1000));
|
||||
service.seekTo(newPos);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void setImage(int index, Drawable image){
|
||||
this.image.setImageDrawable(image);
|
||||
if((item.attachment.meta==null || item.attachment.meta.colors==null) && image instanceof BitmapDrawable bd){
|
||||
Bitmap bitmap=bd.getBitmap();
|
||||
if(Build.VERSION.SDK_INT>=26 && bitmap.getConfig()==Bitmap.Config.HARDWARE)
|
||||
bitmap=bitmap.copy(Bitmap.Config.ARGB_8888, false);
|
||||
int color=Palette.from(bitmap).maximumColorCount(1).generate().getDominantColor(0xff808080);
|
||||
updateColors(color);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void clearImage(int index){
|
||||
setImage(index, null);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,53 @@
|
||||
package org.joinmastodon.android.ui.displayitems;
|
||||
|
||||
import android.app.Activity;
|
||||
import android.view.View;
|
||||
import android.view.ViewGroup;
|
||||
import android.widget.CheckBox;
|
||||
|
||||
import org.joinmastodon.android.R;
|
||||
import org.joinmastodon.android.fragments.BaseStatusListFragment;
|
||||
import org.joinmastodon.android.model.Account;
|
||||
import org.joinmastodon.android.model.Notification;
|
||||
import org.joinmastodon.android.model.ScheduledStatus;
|
||||
import org.joinmastodon.android.model.Status;
|
||||
import org.joinmastodon.android.ui.views.CheckableRelativeLayout;
|
||||
|
||||
import java.time.Instant;
|
||||
import java.util.function.Predicate;
|
||||
|
||||
public class CheckableHeaderStatusDisplayItem extends HeaderStatusDisplayItem{
|
||||
public CheckableHeaderStatusDisplayItem(String parentID, Account user, Instant createdAt, BaseStatusListFragment<?> parentFragment, String accountID, Status status, CharSequence extraText){
|
||||
super(parentID, user, createdAt, parentFragment, accountID, status, extraText, null, null);
|
||||
}
|
||||
|
||||
@Override
|
||||
public Type getType(){
|
||||
return Type.HEADER_CHECKABLE;
|
||||
}
|
||||
|
||||
public static class Holder extends HeaderStatusDisplayItem.Holder{
|
||||
private final View checkbox;
|
||||
private final CheckableRelativeLayout view;
|
||||
private Predicate<Holder> isChecked;
|
||||
|
||||
public Holder(Activity activity, ViewGroup parent){
|
||||
super(activity, R.layout.display_item_header_checkable, parent);
|
||||
checkbox=findViewById(R.id.checkbox);
|
||||
view=(CheckableRelativeLayout) itemView;
|
||||
checkbox.setBackground(new CheckBox(activity).getButtonDrawable());
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onBind(HeaderStatusDisplayItem item){
|
||||
super.onBind(item);
|
||||
if(isChecked!=null){
|
||||
view.setChecked(isChecked.test(this));
|
||||
}
|
||||
}
|
||||
|
||||
public void setIsChecked(Predicate<Holder> isChecked){
|
||||
this.isChecked=isChecked;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -20,6 +20,7 @@ import org.joinmastodon.android.fragments.account_list.StatusFavoritesListFragme
|
||||
import org.joinmastodon.android.fragments.account_list.StatusReblogsListFragment;
|
||||
import org.joinmastodon.android.fragments.account_list.StatusRelatedAccountListFragment;
|
||||
import org.joinmastodon.android.model.Status;
|
||||
import org.joinmastodon.android.model.StatusPrivacy;
|
||||
import org.joinmastodon.android.ui.utils.UiUtils;
|
||||
import org.parceler.Parcels;
|
||||
|
||||
@@ -75,11 +76,11 @@ public class ExtendedFooterStatusDisplayItem extends StatusDisplayItem{
|
||||
Status s=item.status;
|
||||
favorites.setText(context.getResources().getQuantityString(R.plurals.x_favorites, (int)(s.favouritesCount%1000), s.favouritesCount));
|
||||
reblogs.setText(context.getResources().getQuantityString(R.plurals.x_reblogs, (int) (s.reblogsCount % 1000), s.reblogsCount));
|
||||
reblogs.setVisibility(s.isReblogPermitted(item.accountID) ? View.VISIBLE : View.GONE);
|
||||
reblogs.setVisibility(s.visibility != StatusPrivacy.DIRECT ? View.VISIBLE : View.GONE);
|
||||
|
||||
if(s.editedAt!=null){
|
||||
editHistory.setVisibility(View.VISIBLE);
|
||||
editHistory.setText(UiUtils.formatRelativeTimestampAsMinutesAgo(itemView.getContext(), s.editedAt));
|
||||
editHistory.setText(UiUtils.formatRelativeTimestampAsMinutesAgo(itemView.getContext(), s.editedAt, false));
|
||||
}else{
|
||||
editHistory.setVisibility(View.GONE);
|
||||
}
|
||||
|
||||
@@ -149,7 +149,8 @@ public class FooterStatusDisplayItem extends StatusDisplayItem{
|
||||
}
|
||||
|
||||
private void bindText(TextView btn, long count){
|
||||
if(GlobalUserPreferences.showInteractionCounts && count>0 && !item.hideCounts){
|
||||
if(AccountSessionManager.get(item.accountID).getLocalPreferences().showInteractionCounts
|
||||
&& count>0 && !item.hideCounts){
|
||||
btn.setText(UiUtils.abbreviateNumber(count));
|
||||
btn.setCompoundDrawablePadding(V.dp(8));
|
||||
}else{
|
||||
@@ -205,7 +206,7 @@ public class FooterStatusDisplayItem extends StatusDisplayItem{
|
||||
}
|
||||
|
||||
private void onBoostClick(View v){
|
||||
if (GlobalUserPreferences.confirmBeforeReblog) {
|
||||
if (GlobalUserPreferences.confirmBoost) {
|
||||
v.startAnimation(opacityIn);
|
||||
onBoostLongClick(v);
|
||||
return;
|
||||
|
||||
@@ -3,7 +3,6 @@ package org.joinmastodon.android.ui.displayitems;
|
||||
import android.content.Context;
|
||||
import android.view.View;
|
||||
import android.view.ViewGroup;
|
||||
import android.widget.RelativeLayout;
|
||||
import android.widget.TextView;
|
||||
|
||||
import org.joinmastodon.android.R;
|
||||
@@ -27,13 +26,6 @@ public class HashtagStatusDisplayItem extends StatusDisplayItem{
|
||||
public static class Holder extends StatusDisplayItem.Holder<HashtagStatusDisplayItem>{
|
||||
private final TextView title, subtitle;
|
||||
private final HashtagChartView chart;
|
||||
public static final RelativeLayout.LayoutParams
|
||||
withHistoryParams = new RelativeLayout.LayoutParams(ViewGroup.LayoutParams.MATCH_PARENT, ViewGroup.LayoutParams.WRAP_CONTENT),
|
||||
withoutHistoryParams = new RelativeLayout.LayoutParams(ViewGroup.LayoutParams.MATCH_PARENT, ViewGroup.LayoutParams.WRAP_CONTENT);
|
||||
|
||||
static {
|
||||
withoutHistoryParams.addRule(RelativeLayout.CENTER_VERTICAL, RelativeLayout.TRUE);
|
||||
}
|
||||
|
||||
public Holder(Context context, ViewGroup parent){
|
||||
super(context, R.layout.item_trending_hashtag, parent);
|
||||
@@ -46,20 +38,17 @@ public class HashtagStatusDisplayItem extends StatusDisplayItem{
|
||||
public void onBind(HashtagStatusDisplayItem _item){
|
||||
Hashtag item=_item.tag;
|
||||
title.setText('#'+item.name);
|
||||
if (item.history == null || item.history.isEmpty()) {
|
||||
subtitle.setText(null);
|
||||
if(item.history!=null && !item.history.isEmpty()){
|
||||
int numPeople=item.history.get(0).accounts;
|
||||
if(item.history.size()>1)
|
||||
numPeople+=item.history.get(1).accounts;
|
||||
subtitle.setText(itemView.getResources().getQuantityString(R.plurals.x_people_talking, numPeople, numPeople));
|
||||
chart.setData(item.history);
|
||||
chart.setVisibility(View.VISIBLE);
|
||||
}else{
|
||||
subtitle.setText(itemView.getResources().getQuantityString(R.plurals.x_posts, item.statusesCount, item.statusesCount));
|
||||
chart.setVisibility(View.GONE);
|
||||
title.setLayoutParams(withoutHistoryParams);
|
||||
return;
|
||||
}
|
||||
chart.setVisibility(View.VISIBLE);
|
||||
title.setLayoutParams(withHistoryParams);
|
||||
int numPeople=item.history.get(0).accounts;
|
||||
if(item.history.size()>1)
|
||||
numPeople+=item.history.get(1).accounts;
|
||||
subtitle.setText(_item.parentFragment.getResources().getQuantityString(R.plurals.x_people_talking, numPeople, numPeople));
|
||||
chart.setData(item.history);
|
||||
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,8 +1,8 @@
|
||||
package org.joinmastodon.android.ui.displayitems;
|
||||
|
||||
import android.annotation.SuppressLint;
|
||||
import android.app.Activity;
|
||||
import android.app.ProgressDialog;
|
||||
import android.graphics.Outline;
|
||||
import android.graphics.Paint;
|
||||
import android.graphics.Typeface;
|
||||
import android.graphics.drawable.Animatable;
|
||||
@@ -16,7 +16,6 @@ import android.view.MenuItem;
|
||||
import android.view.SubMenu;
|
||||
import android.view.View;
|
||||
import android.view.ViewGroup;
|
||||
import android.view.ViewOutlineProvider;
|
||||
import android.widget.ImageView;
|
||||
import android.widget.PopupMenu;
|
||||
import android.widget.TextView;
|
||||
@@ -39,11 +38,12 @@ import org.joinmastodon.android.fragments.ThreadFragment;
|
||||
import org.joinmastodon.android.fragments.report.ReportReasonChoiceFragment;
|
||||
import org.joinmastodon.android.model.Account;
|
||||
import org.joinmastodon.android.model.Announcement;
|
||||
import org.joinmastodon.android.model.Attachment;
|
||||
import org.joinmastodon.android.model.Notification;
|
||||
import org.joinmastodon.android.model.Relationship;
|
||||
import org.joinmastodon.android.model.ScheduledStatus;
|
||||
import org.joinmastodon.android.model.Status;
|
||||
import org.joinmastodon.android.model.StatusPrivacy;
|
||||
import org.joinmastodon.android.ui.OutlineProviders;
|
||||
import org.joinmastodon.android.ui.text.HtmlParser;
|
||||
import org.joinmastodon.android.ui.utils.CustomEmojiHelper;
|
||||
import org.joinmastodon.android.ui.utils.UiUtils;
|
||||
@@ -58,6 +58,7 @@ import java.util.List;
|
||||
import java.util.Locale;
|
||||
import java.util.function.Consumer;
|
||||
|
||||
import androidx.annotation.LayoutRes;
|
||||
import me.grishka.appkit.Nav;
|
||||
import me.grishka.appkit.api.APIRequest;
|
||||
import me.grishka.appkit.api.Callback;
|
||||
@@ -94,18 +95,12 @@ public class HeaderStatusDisplayItem extends StatusDisplayItem{
|
||||
this.status=status;
|
||||
this.notification=notification;
|
||||
this.scheduledStatus=scheduledStatus;
|
||||
HtmlParser.parseCustomEmoji(parsedName, user.emojis);
|
||||
if(AccountSessionManager.get(accountID).getLocalPreferences().customEmojiInNames)
|
||||
HtmlParser.parseCustomEmoji(parsedName, user.emojis);
|
||||
emojiHelper.setText(parsedName);
|
||||
if(status!=null){
|
||||
hasVisibilityToggle=status.sensitive || !TextUtils.isEmpty(status.spoilerText);
|
||||
if(!hasVisibilityToggle && !status.mediaAttachments.isEmpty()){
|
||||
for(Attachment att:status.mediaAttachments){
|
||||
if(att.type!=Attachment.Type.AUDIO){
|
||||
hasVisibilityToggle=true;
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
// visibility toggle can't do much for non-"image" attachments
|
||||
hasVisibilityToggle=status.mediaAttachments.stream().anyMatch(m -> m.type.isImage());
|
||||
}
|
||||
this.extraText=extraText;
|
||||
emojiHelper.addText(extraText);
|
||||
@@ -137,36 +132,33 @@ public class HeaderStatusDisplayItem extends StatusDisplayItem{
|
||||
}
|
||||
|
||||
public static class Holder extends StatusDisplayItem.Holder<HeaderStatusDisplayItem> implements ImageLoaderViewHolder{
|
||||
private final TextView name, username, timestamp, extraText, separator;
|
||||
private final TextView name, timeAndUsername, extraText, pronouns;
|
||||
private final View collapseBtn;
|
||||
private final ImageView avatar, more, visibility, deleteNotification, unreadIndicator, collapseBtnIcon;
|
||||
private final ImageView avatar, more, visibility, deleteNotification, unreadIndicator, markAsRead, collapseBtnIcon;
|
||||
private final PopupMenu optionsMenu;
|
||||
private Relationship relationship;
|
||||
private APIRequest<?> currentRelationshipRequest;
|
||||
|
||||
private static final ViewOutlineProvider roundCornersOutline=new ViewOutlineProvider(){
|
||||
@Override
|
||||
public void getOutline(View view, Outline outline){
|
||||
outline.setRoundRect(0, 0, view.getWidth(), view.getHeight(), V.dp(12));
|
||||
}
|
||||
};
|
||||
|
||||
public Holder(Activity activity, ViewGroup parent){
|
||||
super(activity, R.layout.display_item_header, parent);
|
||||
this(activity, R.layout.display_item_header, parent);
|
||||
}
|
||||
|
||||
protected Holder(Activity activity, @LayoutRes int layout, ViewGroup parent){
|
||||
super(activity, layout, parent);
|
||||
name=findViewById(R.id.name);
|
||||
username=findViewById(R.id.username);
|
||||
separator=findViewById(R.id.separator);
|
||||
timestamp=findViewById(R.id.timestamp);
|
||||
timeAndUsername=findViewById(R.id.time_and_username);
|
||||
avatar=findViewById(R.id.avatar);
|
||||
more=findViewById(R.id.more);
|
||||
visibility=findViewById(R.id.visibility);
|
||||
deleteNotification=findViewById(R.id.delete_notification);
|
||||
unreadIndicator=findViewById(R.id.unread_indicator);
|
||||
markAsRead=findViewById(R.id.mark_as_read);
|
||||
collapseBtn=findViewById(R.id.collapse_btn);
|
||||
collapseBtnIcon=findViewById(R.id.collapse_btn_icon);
|
||||
extraText=findViewById(R.id.extra_text);
|
||||
pronouns=findViewById(R.id.pronouns);
|
||||
avatar.setOnClickListener(this::onAvaClick);
|
||||
avatar.setOutlineProvider(roundCornersOutline);
|
||||
avatar.setOutlineProvider(OutlineProviders.roundedRect(12));
|
||||
avatar.setClipToOutline(true);
|
||||
more.setOnClickListener(this::onMoreClick);
|
||||
visibility.setOnClickListener(v->item.parentFragment.onVisibilityIconClick(this));
|
||||
@@ -179,15 +171,16 @@ public class HeaderStatusDisplayItem extends StatusDisplayItem{
|
||||
|
||||
optionsMenu=new PopupMenu(activity, more);
|
||||
optionsMenu.inflate(R.menu.post);
|
||||
if(Build.VERSION.SDK_INT>=Build.VERSION_CODES.P)
|
||||
optionsMenu.getMenu().setGroupDividerEnabled(true);
|
||||
optionsMenu.setOnMenuItemClickListener(menuItem->{
|
||||
Account account=item.user;
|
||||
int id=menuItem.getItemId();
|
||||
|
||||
if(id==R.id.edit || id==R.id.delete_and_redraft) {
|
||||
if(id==R.id.edit || id==R.id.delete_and_redraft){
|
||||
final Bundle args=new Bundle();
|
||||
args.putString("account", item.parentFragment.getAccountID());
|
||||
args.putParcelable("editStatus", Parcels.wrap(item.status));
|
||||
boolean redraft = id==R.id.delete_and_redraft;
|
||||
boolean redraft = id == R.id.delete_and_redraft;
|
||||
if (redraft) {
|
||||
args.putBoolean("redraftStatus", true);
|
||||
if (item.parentFragment instanceof ThreadFragment thread && !thread.isItemEnabled(item.status.id)) {
|
||||
@@ -255,8 +248,9 @@ public class HeaderStatusDisplayItem extends StatusDisplayItem{
|
||||
args.putString("account", item.parentFragment.getAccountID());
|
||||
args.putParcelable("status", Parcels.wrap(item.status));
|
||||
args.putParcelable("reportAccount", Parcels.wrap(item.status.account));
|
||||
args.putParcelable("relationship", Parcels.wrap(relationship));
|
||||
Nav.go(item.parentFragment.getActivity(), ReportReasonChoiceFragment.class, args);
|
||||
}else if(id==R.id.open_in_browser) {
|
||||
}else if(id==R.id.open_in_browser){
|
||||
UiUtils.launchWebBrowser(activity, item.status.url);
|
||||
}else if(id==R.id.copy_link){
|
||||
UiUtils.copyText(parent, item.status.url);
|
||||
@@ -285,6 +279,8 @@ public class HeaderStatusDisplayItem extends StatusDisplayItem{
|
||||
args.putString("profileAccount", account.id);
|
||||
args.putString("profileDisplayUsername", account.getDisplayUsername());
|
||||
Nav.go(item.parentFragment.getActivity(), ListsFragment.class, args);
|
||||
}else if(id==R.id.share){
|
||||
UiUtils.openSystemShareSheet(activity, item.status.url);
|
||||
}
|
||||
return true;
|
||||
});
|
||||
@@ -302,47 +298,55 @@ public class HeaderStatusDisplayItem extends StatusDisplayItem{
|
||||
});
|
||||
}
|
||||
|
||||
@SuppressLint("SetTextI18n")
|
||||
@Override
|
||||
public void onBind(HeaderStatusDisplayItem item){
|
||||
name.setText(item.parsedName);
|
||||
username.setText('@'+item.user.acct);
|
||||
separator.setVisibility(View.VISIBLE);
|
||||
|
||||
if (item.scheduledStatus!=null)
|
||||
String time = null;
|
||||
if (item.scheduledStatus!=null) {
|
||||
if (item.scheduledStatus.scheduledAt.isAfter(CreateStatus.DRAFTS_AFTER_INSTANT)) {
|
||||
timestamp.setText(R.string.sk_draft);
|
||||
time = item.parentFragment.getString(R.string.sk_draft);
|
||||
} else {
|
||||
DateTimeFormatter formatter = DateTimeFormatter.ofLocalizedDateTime(FormatStyle.MEDIUM).withLocale(Locale.getDefault());
|
||||
timestamp.setText(item.scheduledStatus.scheduledAt.atZone(ZoneId.systemDefault()).format(formatter));
|
||||
time = item.scheduledStatus.scheduledAt.atZone(ZoneId.systemDefault()).format(formatter);
|
||||
}
|
||||
else if ((!item.inset || item.status==null || item.status.editedAt==null) && item.createdAt != null)
|
||||
timestamp.setText(UiUtils.formatRelativeTimestamp(itemView.getContext(), item.createdAt));
|
||||
} else if(item.status==null || item.status.editedAt==null)
|
||||
time=UiUtils.formatRelativeTimestamp(itemView.getContext(), item.createdAt);
|
||||
else if (item.status != null && item.status.editedAt != null)
|
||||
timestamp.setText(item.parentFragment.getString(R.string.edited_timestamp, UiUtils.formatRelativeTimestamp(itemView.getContext(), item.status.editedAt)));
|
||||
else {
|
||||
separator.setVisibility(View.GONE);
|
||||
timestamp.setText("");
|
||||
}
|
||||
visibility.setVisibility(item.hasVisibilityToggle && !item.inset ? View.VISIBLE : View.GONE);
|
||||
time=item.parentFragment.getString(R.string.edited_timestamp, UiUtils.formatRelativeTimestamp(itemView.getContext(), item.status.editedAt));
|
||||
|
||||
String sepp = item.parentFragment.getString(R.string.sk_separator);
|
||||
String username = "@" + item.user.acct;
|
||||
timeAndUsername.setText(time == null ? username :
|
||||
username + " " + sepp + " " + time);
|
||||
|
||||
deleteNotification.setVisibility(GlobalUserPreferences.enableDeleteNotifications && item.notification!=null && !item.inset ? View.VISIBLE : View.GONE);
|
||||
if(item.hasVisibilityToggle){
|
||||
visibility.setImageResource(item.status.spoilerRevealed ? R.drawable.ic_visibility_off : R.drawable.ic_visibility);
|
||||
visibility.setContentDescription(item.parentFragment.getString(item.status.spoilerRevealed ? R.string.hide_content : R.string.reveal_content));
|
||||
if (item.hasVisibilityToggle){
|
||||
boolean disabled = !item.status.sensitiveRevealed ||
|
||||
(!TextUtils.isEmpty(item.status.spoilerText) &&
|
||||
!item.status.spoilerRevealed);
|
||||
visibility.setEnabled(!disabled);
|
||||
V.setVisibilityAnimated(visibility, disabled ? View.INVISIBLE : View.VISIBLE);
|
||||
visibility.setContentDescription(item.parentFragment.getString(item.status.sensitiveRevealed ? R.string.spoiler_hide : R.string.spoiler_show));
|
||||
if(Build.VERSION.SDK_INT>=Build.VERSION_CODES.O){
|
||||
visibility.setTooltipText(visibility.getContentDescription());
|
||||
}
|
||||
} else {
|
||||
visibility.setVisibility(View.GONE);
|
||||
}
|
||||
itemView.setPadding(itemView.getPaddingLeft(), itemView.getPaddingTop(), itemView.getPaddingRight(), item.needBottomPadding ? V.dp(16) : 0);
|
||||
if(TextUtils.isEmpty(item.extraText)){
|
||||
if (item.status != null) {
|
||||
UiUtils.setExtraTextInfo(item.parentFragment.getContext(), extraText, item.status.visibility, item.status.localOnly);
|
||||
UiUtils.setExtraTextInfo(item.parentFragment.getContext(), extraText, pronouns, item.status.visibility==StatusPrivacy.DIRECT, item.status.localOnly || item.status.visibility==StatusPrivacy.LOCAL, item.status.account);
|
||||
}
|
||||
}else{
|
||||
extraText.setVisibility(View.VISIBLE);
|
||||
extraText.setText(item.extraText);
|
||||
}
|
||||
more.setVisibility(item.inset || (item.notification != null && item.notification.report != null)
|
||||
more.setVisibility(item.announcement != null || item.inset ||
|
||||
(item.notification != null && item.notification.report != null)
|
||||
? View.GONE : View.VISIBLE);
|
||||
more.setOnClickListener(this::onMoreClick);
|
||||
avatar.setClickable(!item.inset);
|
||||
avatar.setContentDescription(item.parentFragment.getString(R.string.avatar_description, item.user.acct));
|
||||
if(currentRelationshipRequest!=null){
|
||||
@@ -350,20 +354,13 @@ public class HeaderStatusDisplayItem extends StatusDisplayItem{
|
||||
}
|
||||
relationship=null;
|
||||
|
||||
String desc;
|
||||
if (item.announcement != null) {
|
||||
if (unreadIndicator.getVisibility() == View.GONE) {
|
||||
more.setAlpha(0f);
|
||||
unreadIndicator.setAlpha(0f);
|
||||
unreadIndicator.setVisibility(View.VISIBLE);
|
||||
}
|
||||
float alpha = item.announcement.read ? 0 : 1;
|
||||
more.setImageResource(R.drawable.ic_fluent_checkmark_20_filled);
|
||||
desc = item.parentFragment.getString(R.string.sk_mark_as_read);
|
||||
more.animate().alpha(alpha);
|
||||
unreadIndicator.animate().alpha(alpha);
|
||||
more.setEnabled(!item.announcement.read);
|
||||
more.setOnClickListener(v -> {
|
||||
int vis = item.announcement.read ? View.GONE : View.VISIBLE;
|
||||
V.setVisibilityAnimated(unreadIndicator, vis);
|
||||
V.setVisibilityAnimated(markAsRead, vis);
|
||||
|
||||
markAsRead.setEnabled(!item.announcement.read);
|
||||
markAsRead.setOnClickListener(v -> {
|
||||
if (item.announcement.read) return;
|
||||
new DismissAnnouncement(item.announcement.id).setCallback(new Callback<>() {
|
||||
@Override
|
||||
@@ -381,14 +378,9 @@ public class HeaderStatusDisplayItem extends StatusDisplayItem{
|
||||
}).exec(item.accountID);
|
||||
});
|
||||
} else {
|
||||
more.setImageResource(R.drawable.ic_fluent_more_vertical_20_filled);
|
||||
desc = item.parentFragment.getString(R.string.more_options);
|
||||
more.setOnClickListener(this::onMoreClick);
|
||||
markAsRead.setVisibility(View.GONE);
|
||||
}
|
||||
|
||||
more.setContentDescription(desc);
|
||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) more.setTooltipText(desc);
|
||||
|
||||
if (item.status == null || !item.status.textExpandable) {
|
||||
collapseBtn.setVisibility(View.GONE);
|
||||
} else {
|
||||
@@ -418,6 +410,10 @@ public class HeaderStatusDisplayItem extends StatusDisplayItem{
|
||||
|
||||
@Override
|
||||
public void clearImage(int index){
|
||||
if(index==0){
|
||||
avatar.setImageResource(R.drawable.image_placeholder);
|
||||
return;
|
||||
}
|
||||
setImage(index, null);
|
||||
}
|
||||
|
||||
@@ -457,9 +453,11 @@ public class HeaderStatusDisplayItem extends StatusDisplayItem{
|
||||
}
|
||||
|
||||
private void updateOptionsMenu(){
|
||||
if (item.parentFragment.getActivity() == null) return;
|
||||
if(item.parentFragment.getActivity()==null)
|
||||
return;
|
||||
if (item.announcement != null) return;
|
||||
boolean hasMultipleAccounts = AccountSessionManager.getInstance().getLoggedInAccounts().size() > 1;
|
||||
Account account=item.user;
|
||||
Menu menu=optionsMenu.getMenu();
|
||||
|
||||
MenuItem openWithAccounts = menu.findItem(R.id.open_with_account);
|
||||
@@ -474,7 +472,6 @@ public class HeaderStatusDisplayItem extends StatusDisplayItem{
|
||||
openWithAccounts.setVisible(false);
|
||||
}
|
||||
|
||||
Account account=item.user;
|
||||
String username = account.getShortUsername();
|
||||
boolean isOwnPost=AccountSessionManager.getInstance().isSelf(item.parentFragment.getAccountID(), account);
|
||||
boolean isPostScheduled=item.scheduledStatus!=null;
|
||||
@@ -492,9 +489,9 @@ public class HeaderStatusDisplayItem extends StatusDisplayItem{
|
||||
MenuItem report=menu.findItem(R.id.report);
|
||||
MenuItem follow=menu.findItem(R.id.follow);
|
||||
MenuItem manageUserLists = menu.findItem(R.id.manage_user_lists);
|
||||
/* disabled in megalodon: add/remove bookmark is already available through status footer
|
||||
MenuItem bookmark=menu.findItem(R.id.bookmark);
|
||||
bookmark.setVisible(false);
|
||||
/* disabled in megalodon: add/remove bookmark is already available through status footer
|
||||
if(item.status!=null){
|
||||
bookmark.setVisible(true);
|
||||
bookmark.setTitle(item.status.bookmarked ? R.string.remove_bookmark : R.string.add_bookmark);
|
||||
|
||||
@@ -0,0 +1,41 @@
|
||||
package org.joinmastodon.android.ui.displayitems;
|
||||
|
||||
import android.content.Context;
|
||||
import android.view.ViewGroup;
|
||||
import android.widget.Space;
|
||||
import android.widget.TextView;
|
||||
|
||||
import org.joinmastodon.android.fragments.BaseStatusListFragment;
|
||||
|
||||
import me.grishka.appkit.utils.V;
|
||||
|
||||
public class InsetDummyStatusDisplayItem extends StatusDisplayItem {
|
||||
private final boolean addMediaGridMargin;
|
||||
|
||||
public InsetDummyStatusDisplayItem(String parentID, BaseStatusListFragment<?> parentFragment, boolean addMediaGridMargin) {
|
||||
super(parentID, parentFragment);
|
||||
this.addMediaGridMargin = addMediaGridMargin;
|
||||
}
|
||||
|
||||
@Override
|
||||
public Type getType() {
|
||||
return Type.DUMMY;
|
||||
}
|
||||
|
||||
public static class Holder extends StatusDisplayItem.Holder<InsetDummyStatusDisplayItem> {
|
||||
public Holder(Context context) {
|
||||
super(new Space(context));
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onBind(InsetDummyStatusDisplayItem item) {
|
||||
// BetterItemAnimator appears not to handle InsetStatusItemDecoration's getItemOffsets
|
||||
// correctly, causing removed inset views to jump while animating. i don't quite
|
||||
// understand it, but this workaround appears to work.
|
||||
// see InsetStatusItemDecoration#getItemOffsets
|
||||
ViewGroup.MarginLayoutParams params = new ViewGroup.MarginLayoutParams(ViewGroup.LayoutParams.MATCH_PARENT, 0);
|
||||
params.setMargins(0, item.addMediaGridMargin ? V.dp(4) : 0, 0, V.dp(16));
|
||||
itemView.setLayoutParams(params);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -8,11 +8,14 @@ import android.animation.AnimatorSet;
|
||||
import android.animation.ObjectAnimator;
|
||||
import android.app.Activity;
|
||||
import android.graphics.Rect;
|
||||
import android.graphics.drawable.ColorDrawable;
|
||||
import android.graphics.drawable.Drawable;
|
||||
import android.graphics.drawable.LayerDrawable;
|
||||
import android.text.TextUtils;
|
||||
import android.view.Gravity;
|
||||
import android.view.View;
|
||||
import android.view.ViewGroup;
|
||||
import android.view.ViewPropertyAnimator;
|
||||
import android.view.ViewTreeObserver;
|
||||
import android.widget.FrameLayout;
|
||||
import android.widget.ImageButton;
|
||||
@@ -24,6 +27,7 @@ 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.SpoilerStripesDrawable;
|
||||
import org.joinmastodon.android.ui.photoviewer.PhotoViewerHost;
|
||||
import org.joinmastodon.android.ui.utils.MediaAttachmentViewController;
|
||||
import org.joinmastodon.android.ui.utils.UiUtils;
|
||||
@@ -39,6 +43,7 @@ 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;
|
||||
import me.grishka.appkit.utils.V;
|
||||
|
||||
public class MediaGridStatusDisplayItem extends StatusDisplayItem{
|
||||
private static final String TAG="MediaGridDisplayItem";
|
||||
@@ -48,6 +53,7 @@ public class MediaGridStatusDisplayItem extends StatusDisplayItem{
|
||||
private final List<Attachment> attachments;
|
||||
private final ArrayList<ImageLoaderRequest> requests=new ArrayList<>();
|
||||
public final Status status;
|
||||
public String sensitiveTitle;
|
||||
|
||||
public MediaGridStatusDisplayItem(String parentID, BaseStatusListFragment<?> parentFragment, PhotoLayoutHelper.TiledLayoutResult tiledLayout, List<Attachment> attachments, Status status){
|
||||
super(parentID, parentFragment);
|
||||
@@ -99,11 +105,15 @@ public class MediaGridStatusDisplayItem extends StatusDisplayItem{
|
||||
private final ImageButton altTextClose;
|
||||
private final TextView altText, noAltText;
|
||||
|
||||
private final View sensitiveOverlay;
|
||||
private final LayerDrawable sensitiveOverlayBG;
|
||||
private static final ColorDrawable drawableForWhenThereIsNoBlurhash=new ColorDrawable(0xffffffff);
|
||||
// private final FrameLayout hideSensitiveButton;
|
||||
private final TextView sensitiveText;
|
||||
|
||||
private int altTextIndex=-1;
|
||||
private Animator altTextAnimator;
|
||||
|
||||
private boolean sizeUpdating = false;
|
||||
|
||||
public Holder(Activity activity, ViewGroup parent){
|
||||
super(new FrameLayoutThatOnlyMeasuresFirstChild(activity));
|
||||
wrapper=(FrameLayout)itemView;
|
||||
@@ -124,10 +134,28 @@ public class MediaGridStatusDisplayItem extends StatusDisplayItem{
|
||||
altText=findViewById(R.id.alt_text);
|
||||
noAltText=findViewById(R.id.no_alt_text);
|
||||
altTextClose.setOnClickListener(this::onAltTextCloseClick);
|
||||
|
||||
// megalodon: no sensitive hide button because the visibility toggle looks prettier imo
|
||||
// hideSensitiveButton=(FrameLayout) activity.getLayoutInflater().inflate(R.layout.alt_text_badge, overlays, false);
|
||||
// ((TextView) hideSensitiveButton.findViewById(R.id.alt_button)).setText(R.string.hide);
|
||||
// overlays.addView(hideSensitiveButton, new FrameLayout.LayoutParams(ViewGroup.LayoutParams.WRAP_CONTENT, ViewGroup.LayoutParams.WRAP_CONTENT, Gravity.END | Gravity.TOP));
|
||||
|
||||
activity.getLayoutInflater().inflate(R.layout.overlay_image_sensitive, overlays);
|
||||
sensitiveOverlay=findViewById(R.id.sensitive_overlay);
|
||||
sensitiveOverlayBG=(LayerDrawable) sensitiveOverlay.getBackground().mutate();
|
||||
sensitiveOverlayBG.setDrawableByLayerId(R.id.left_drawable, new SpoilerStripesDrawable(false));
|
||||
sensitiveOverlayBG.setDrawableByLayerId(R.id.right_drawable, new SpoilerStripesDrawable(true));
|
||||
sensitiveOverlay.setBackground(sensitiveOverlayBG);
|
||||
sensitiveOverlay.setOnClickListener(v->revealSensitive());
|
||||
// hideSensitiveButton.setOnClickListener(v->hideSensitive());
|
||||
|
||||
sensitiveText=findViewById(R.id.sensitive_text);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onBind(MediaGridStatusDisplayItem item){
|
||||
wrapper.setPadding(0, 0, 0, 0); // item.inset ? 0 : V.dp(8));
|
||||
|
||||
if(altTextAnimator!=null)
|
||||
altTextAnimator.cancel();
|
||||
|
||||
@@ -139,6 +167,7 @@ public class MediaGridStatusDisplayItem extends StatusDisplayItem{
|
||||
controllers.clear();
|
||||
|
||||
int i=0;
|
||||
if (!item.attachments.isEmpty()) updateBlurhashInSensitiveOverlay();
|
||||
for(Attachment att:item.attachments){
|
||||
MediaAttachmentViewController c=item.viewPool.obtain(switch(att.type){
|
||||
case IMAGE -> GridItemType.PHOTO;
|
||||
@@ -166,23 +195,36 @@ public class MediaGridStatusDisplayItem extends StatusDisplayItem{
|
||||
noAltTextButton.setVisibility(View.VISIBLE);
|
||||
altTextWrapper.setVisibility(View.GONE);
|
||||
altTextIndex=-1;
|
||||
|
||||
if(!item.status.sensitiveRevealed){
|
||||
sensitiveOverlay.setVisibility(View.VISIBLE);
|
||||
layout.setVisibility(View.INVISIBLE);
|
||||
}else{
|
||||
sensitiveOverlay.setVisibility(View.GONE);
|
||||
layout.setVisibility(View.VISIBLE);
|
||||
}
|
||||
// hideSensitiveButton.setVisibility(item.status.sensitive ? View.VISIBLE : View.GONE);
|
||||
if(!TextUtils.isEmpty(item.sensitiveTitle))
|
||||
sensitiveText.setText(item.sensitiveTitle);
|
||||
else if (!item.status.sensitive)
|
||||
sensitiveText.setText(R.string.media_hidden);
|
||||
else
|
||||
sensitiveText.setText(R.string.sensitive_content_explain);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void setImage(int index, Drawable drawable){
|
||||
Rect bounds=drawable.getBounds();
|
||||
drawable.setBounds(bounds.left, bounds.top, bounds.left+drawable.getIntrinsicWidth(), bounds.top+drawable.getIntrinsicHeight());
|
||||
if(item.attachments.get(index).meta==null){
|
||||
Rect bounds=drawable.getBounds();
|
||||
drawable.setBounds(bounds.left, bounds.top, bounds.left+drawable.getIntrinsicWidth(), bounds.top+drawable.getIntrinsicHeight());
|
||||
Attachment.Metadata metadata = new Attachment.Metadata();
|
||||
metadata.width=drawable.getIntrinsicWidth();
|
||||
metadata.height=drawable.getIntrinsicHeight();
|
||||
item.attachments.get(index).meta=metadata;
|
||||
|
||||
item.tiledLayout=PhotoLayoutHelper.processThumbs(item.attachments);
|
||||
sizeUpdating = true;
|
||||
item.parentFragment.onImageUpdated(this, index);
|
||||
UiUtils.beginLayoutTransition((ViewGroup) itemView);
|
||||
rebind();
|
||||
}
|
||||
|
||||
controllers.get(index).setImage(drawable);
|
||||
}
|
||||
|
||||
@@ -193,16 +235,13 @@ public class MediaGridStatusDisplayItem extends StatusDisplayItem{
|
||||
|
||||
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);
|
||||
}
|
||||
((PhotoViewerHost) item.parentFragment).openPhotoViewer(item.parentID, item.status, index, this);
|
||||
}
|
||||
|
||||
private void onAltTextClick(View v){
|
||||
if(altTextAnimator!=null)
|
||||
altTextAnimator.cancel();
|
||||
// V.setVisibilityAnimated(hideSensitiveButton, View.GONE);
|
||||
v.setVisibility(View.INVISIBLE);
|
||||
int index=(Integer)v.getTag();
|
||||
altTextIndex=index;
|
||||
@@ -245,6 +284,9 @@ public class MediaGridStatusDisplayItem extends StatusDisplayItem{
|
||||
if(c.btnsWrap!=null && c.btnsWrap!=v){
|
||||
anims.add(ObjectAnimator.ofFloat(c.btnsWrap, View.ALPHA, 1, 0).setDuration(150));
|
||||
}
|
||||
if (c.extraBadge != null) {
|
||||
anims.add(ObjectAnimator.ofFloat(c.extraBadge, View.ALPHA, 1, 0).setDuration(150));
|
||||
}
|
||||
}
|
||||
|
||||
AnimatorSet set=new AnimatorSet();
|
||||
@@ -258,6 +300,7 @@ public class MediaGridStatusDisplayItem extends StatusDisplayItem{
|
||||
if(c.btnsWrap!=null){
|
||||
c.btnsWrap.setVisibility(View.INVISIBLE);
|
||||
}
|
||||
if (c.extraBadge != null) c.extraBadge.setVisibility(View.INVISIBLE);
|
||||
}
|
||||
}
|
||||
});
|
||||
@@ -273,6 +316,7 @@ public class MediaGridStatusDisplayItem extends StatusDisplayItem{
|
||||
if(altTextAnimator!=null)
|
||||
altTextAnimator.cancel();
|
||||
|
||||
// V.setVisibilityAnimated(hideSensitiveButton, item.status.sensitive ? View.VISIBLE : View.GONE);
|
||||
View btn=controllers.get(altTextIndex).btnsWrap;
|
||||
int i=0;
|
||||
for(MediaAttachmentViewController c:controllers){
|
||||
@@ -281,6 +325,7 @@ public class MediaGridStatusDisplayItem extends StatusDisplayItem{
|
||||
&& c.btnsWrap!=btn
|
||||
&& ((hasAltText && showAltIndicator) || (!hasAltText && showNoAltIndicator))
|
||||
) c.btnsWrap.setVisibility(View.VISIBLE);
|
||||
if (c.extraBadge != null) c.extraBadge.setVisibility(View.VISIBLE);
|
||||
i++;
|
||||
}
|
||||
|
||||
@@ -305,9 +350,12 @@ public class MediaGridStatusDisplayItem extends StatusDisplayItem{
|
||||
a.setDuration(300);
|
||||
|
||||
for(MediaAttachmentViewController c:controllers){
|
||||
// if(c.btnsWrap!=null && c.btnsWrap!=btn){
|
||||
if(c.btnsWrap!=null && c.btnsWrap!=btn){
|
||||
anims.add(ObjectAnimator.ofFloat(c.btnsWrap, View.ALPHA, 1).setDuration(150));
|
||||
// }
|
||||
}
|
||||
if (c.extraBadge != null) {
|
||||
anims.add(ObjectAnimator.ofFloat(c.extraBadge, View.ALPHA, 1).setDuration(150));
|
||||
}
|
||||
}
|
||||
|
||||
AnimatorSet set=new AnimatorSet();
|
||||
@@ -319,18 +367,13 @@ public class MediaGridStatusDisplayItem extends StatusDisplayItem{
|
||||
altTextAnimator=null;
|
||||
altTextWrapper.setVisibility(View.GONE);
|
||||
btn.setVisibility(View.VISIBLE);
|
||||
btn.setAlpha(1);
|
||||
}
|
||||
});
|
||||
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);
|
||||
}
|
||||
@@ -340,12 +383,35 @@ public class MediaGridStatusDisplayItem extends StatusDisplayItem{
|
||||
wrapper.setClipChildren(clip);
|
||||
}
|
||||
|
||||
public boolean isSizeUpdating() {
|
||||
return sizeUpdating;
|
||||
private void updateBlurhashInSensitiveOverlay(){
|
||||
Drawable d = item.attachments.get(0).blurhashPlaceholder;
|
||||
sensitiveOverlayBG.setDrawableByLayerId(R.id.blurhash, d==null ? drawableForWhenThereIsNoBlurhash : d.mutate());
|
||||
sensitiveOverlay.setBackground(sensitiveOverlayBG);
|
||||
}
|
||||
|
||||
public void sizeUpdated() {
|
||||
sizeUpdating = false;
|
||||
public void revealSensitive(){
|
||||
if(item.status.sensitiveRevealed)
|
||||
return;
|
||||
item.status.sensitiveRevealed=true;
|
||||
V.setVisibilityAnimated(sensitiveOverlay, View.GONE);
|
||||
layout.setVisibility(View.VISIBLE);
|
||||
item.parentFragment.onSensitiveRevealed(this);
|
||||
}
|
||||
|
||||
public void hideSensitive(){
|
||||
if(!item.status.sensitiveRevealed)
|
||||
return;
|
||||
updateBlurhashInSensitiveOverlay();
|
||||
item.status.sensitiveRevealed=false;
|
||||
V.setVisibilityAnimated(sensitiveOverlay, View.VISIBLE, ()->layout.setVisibility(View.INVISIBLE));
|
||||
}
|
||||
|
||||
public MediaGridLayout getLayout(){
|
||||
return layout;
|
||||
}
|
||||
|
||||
public View getSensitiveOverlay(){
|
||||
return sensitiveOverlay;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,184 @@
|
||||
package org.joinmastodon.android.ui.displayitems;
|
||||
|
||||
import static org.joinmastodon.android.MastodonApp.context;
|
||||
import static org.joinmastodon.android.model.Notification.Type.PLEROMA_EMOJI_REACTION;
|
||||
import static org.joinmastodon.android.ui.utils.UiUtils.generateFormattedString;
|
||||
|
||||
import android.annotation.SuppressLint;
|
||||
import android.app.Activity;
|
||||
import android.content.res.ColorStateList;
|
||||
import android.graphics.drawable.Drawable;
|
||||
import android.os.Bundle;
|
||||
import android.text.Html;
|
||||
import android.text.SpannableStringBuilder;
|
||||
import android.text.TextUtils;
|
||||
import android.util.TypedValue;
|
||||
import android.view.View;
|
||||
import android.view.ViewGroup;
|
||||
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.fragments.ProfileFragment;
|
||||
import org.joinmastodon.android.model.Emoji;
|
||||
import org.joinmastodon.android.model.Notification;
|
||||
import org.joinmastodon.android.ui.OutlineProviders;
|
||||
import org.joinmastodon.android.ui.text.HtmlParser;
|
||||
import org.joinmastodon.android.ui.utils.CustomEmojiHelper;
|
||||
import org.joinmastodon.android.ui.utils.UiUtils;
|
||||
import org.parceler.Parcels;
|
||||
|
||||
import java.util.Collections;
|
||||
|
||||
import me.grishka.appkit.Nav;
|
||||
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.V;
|
||||
|
||||
public class NotificationHeaderStatusDisplayItem extends StatusDisplayItem{
|
||||
public final Notification notification;
|
||||
private ImageLoaderRequest avaRequest;
|
||||
private final String accountID;
|
||||
private final CustomEmojiHelper emojiHelper=new CustomEmojiHelper();
|
||||
private final CharSequence text;
|
||||
|
||||
public NotificationHeaderStatusDisplayItem(String parentID, BaseStatusListFragment parentFragment, Notification notification, String accountID){
|
||||
super(parentID, parentFragment);
|
||||
this.notification=notification;
|
||||
this.accountID=accountID;
|
||||
|
||||
if(notification.type==Notification.Type.POLL){
|
||||
text=parentFragment.getString(R.string.poll_ended);
|
||||
}else{
|
||||
avaRequest=new UrlImageLoaderRequest(GlobalUserPreferences.playGifs ? notification.account.avatar : notification.account.avatarStatic, V.dp(50), V.dp(50));
|
||||
SpannableStringBuilder parsedName=new SpannableStringBuilder(notification.account.displayName);
|
||||
HtmlParser.parseCustomEmoji(parsedName, notification.account.emojis);
|
||||
String str = parentFragment.getString(switch(notification.type){
|
||||
case FOLLOW -> R.string.user_followed_you;
|
||||
case FOLLOW_REQUEST -> R.string.user_sent_follow_request;
|
||||
case REBLOG -> R.string.notification_boosted;
|
||||
case FAVORITE -> R.string.user_favorited;
|
||||
case POLL -> R.string.poll_ended;
|
||||
case UPDATE -> R.string.sk_post_edited;
|
||||
case SIGN_UP -> R.string.sk_signed_up;
|
||||
case REPORT -> R.string.sk_reported;
|
||||
case REACTION, PLEROMA_EMOJI_REACTION ->
|
||||
!TextUtils.isEmpty(notification.emoji) ? R.string.sk_reacted_with : R.string.sk_reacted;
|
||||
default -> throw new IllegalStateException("Unexpected value: "+notification.type);
|
||||
});
|
||||
|
||||
if (!TextUtils.isEmpty(notification.emoji)) {
|
||||
SpannableStringBuilder emoji = new SpannableStringBuilder(notification.emoji);
|
||||
if (!TextUtils.isEmpty(notification.emojiUrl)) {
|
||||
HtmlParser.parseCustomEmoji(emoji, Collections.singletonList(new Emoji(
|
||||
notification.emoji, notification.emojiUrl, notification.emojiUrl
|
||||
)));
|
||||
}
|
||||
this.text = generateFormattedString(str, parsedName, emoji);
|
||||
} else {
|
||||
this.text = generateFormattedString(str, parsedName);
|
||||
}
|
||||
|
||||
emojiHelper.setText(text);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public Type getType(){
|
||||
return Type.NOTIFICATION_HEADER;
|
||||
}
|
||||
|
||||
@Override
|
||||
public int getImageCount(){
|
||||
return 1+emojiHelper.getImageCount();
|
||||
}
|
||||
|
||||
@Override
|
||||
public ImageLoaderRequest getImageRequest(int index){
|
||||
if(index>0){
|
||||
return emojiHelper.getImageRequest(index-1);
|
||||
}
|
||||
return avaRequest;
|
||||
}
|
||||
|
||||
public static class Holder extends StatusDisplayItem.Holder<NotificationHeaderStatusDisplayItem> implements ImageLoaderViewHolder{
|
||||
private final ImageView icon, avatar;
|
||||
private final TextView text;
|
||||
private final int selectableItemBackground;
|
||||
|
||||
public Holder(Activity activity, ViewGroup parent){
|
||||
super(activity, R.layout.display_item_notification_header, parent);
|
||||
icon=findViewById(R.id.icon);
|
||||
avatar=findViewById(R.id.avatar);
|
||||
text=findViewById(R.id.text);
|
||||
|
||||
avatar.setOutlineProvider(OutlineProviders.roundedRect(8));
|
||||
avatar.setClipToOutline(true);
|
||||
|
||||
itemView.setOnClickListener(this::onItemClick);
|
||||
TypedValue outValue = new TypedValue();
|
||||
context.getTheme().resolveAttribute(android.R.attr.selectableItemBackground, outValue, true);
|
||||
selectableItemBackground = outValue.resourceId;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void setImage(int index, Drawable image){
|
||||
if(index==0){
|
||||
avatar.setImageDrawable(image);
|
||||
}else{
|
||||
item.emojiHelper.setImageDrawable(index-1, image);
|
||||
text.invalidate();
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void clearImage(int index){
|
||||
if(index==0)
|
||||
avatar.setImageResource(R.drawable.image_placeholder);
|
||||
else
|
||||
ImageLoaderViewHolder.super.clearImage(index);
|
||||
}
|
||||
|
||||
@SuppressLint("ResourceType")
|
||||
@Override
|
||||
public void onBind(NotificationHeaderStatusDisplayItem item){
|
||||
text.setText(item.text);
|
||||
avatar.setVisibility(item.notification.type==Notification.Type.POLL ? View.GONE : View.VISIBLE);
|
||||
icon.setImageResource(switch(item.notification.type){
|
||||
case FAVORITE -> R.drawable.ic_fluent_star_24_filled;
|
||||
case REBLOG -> R.drawable.ic_fluent_arrow_repeat_all_24_filled;
|
||||
case FOLLOW, FOLLOW_REQUEST -> R.drawable.ic_fluent_person_add_24_filled;
|
||||
case POLL -> R.drawable.ic_fluent_poll_24_filled;
|
||||
case REPORT -> R.drawable.ic_fluent_warning_24_filled;
|
||||
case SIGN_UP -> R.drawable.ic_fluent_person_available_24_filled;
|
||||
case UPDATE -> R.drawable.ic_fluent_edit_24_filled;
|
||||
case REACTION, PLEROMA_EMOJI_REACTION -> R.drawable.ic_fluent_add_24_filled;
|
||||
default -> throw new IllegalStateException("Unexpected value: "+item.notification.type);
|
||||
});
|
||||
icon.setImageTintList(ColorStateList.valueOf(UiUtils.getThemeColor(item.parentFragment.getActivity(), switch(item.notification.type){
|
||||
case FAVORITE -> R.attr.colorFavorite;
|
||||
case REBLOG -> R.attr.colorBoost;
|
||||
case POLL -> R.attr.colorPoll;
|
||||
default -> android.R.attr.colorAccent;
|
||||
})));
|
||||
itemView.setBackgroundResource(item.notification.type != Notification.Type.POLL
|
||||
&& item.notification.type != Notification.Type.REPORT ?
|
||||
selectableItemBackground : 0);
|
||||
itemView.setClickable(item.notification.type != Notification.Type.POLL);
|
||||
}
|
||||
|
||||
public void onItemClick(View v) {
|
||||
if (item.notification.type == Notification.Type.REPORT) {
|
||||
UiUtils.showFragmentForNotification(item.parentFragment.getContext(), item.notification, item.accountID, null);
|
||||
return;
|
||||
}
|
||||
Bundle args=new Bundle();
|
||||
args.putString("account", item.accountID);
|
||||
args.putParcelable("profileAccount", Parcels.wrap(item.notification.account));
|
||||
Nav.go(item.parentFragment.getActivity(), ProfileFragment.class, args);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -38,10 +38,12 @@ public class PollFooterStatusDisplayItem extends StatusDisplayItem{
|
||||
|
||||
@Override
|
||||
public void onBind(PollFooterStatusDisplayItem item){
|
||||
String text=item.parentFragment.getResources().getQuantityString(R.plurals.x_voters, item.poll.votersCount, item.poll.votersCount);
|
||||
String text=item.parentFragment.getResources().getQuantityString(R.plurals.x_votes, item.poll.votesCount, item.poll.votesCount);
|
||||
String sep=item.parentFragment.getString(R.string.sk_separator);
|
||||
if(item.poll.expiresAt!=null && !item.poll.isExpired()){
|
||||
text+=" "+sep+" "+UiUtils.formatTimeLeft(itemView.getContext(), item.poll.expiresAt);
|
||||
if(item.poll.multiple)
|
||||
text+=" "+sep+" "+item.parentFragment.getString(R.string.poll_multiple_choice);
|
||||
}else if(item.poll.isExpired()){
|
||||
text+=" "+sep+" "+item.parentFragment.getString(R.string.poll_closed);
|
||||
}
|
||||
|
||||
@@ -11,8 +11,10 @@ import android.widget.TextView;
|
||||
import org.joinmastodon.android.R;
|
||||
import org.joinmastodon.android.fragments.BaseStatusListFragment;
|
||||
import org.joinmastodon.android.model.Poll;
|
||||
import org.joinmastodon.android.ui.OutlineProviders;
|
||||
import org.joinmastodon.android.ui.text.HtmlParser;
|
||||
import org.joinmastodon.android.ui.utils.CustomEmojiHelper;
|
||||
import org.joinmastodon.android.ui.utils.UiUtils;
|
||||
|
||||
import java.util.Locale;
|
||||
|
||||
@@ -26,11 +28,13 @@ public class PollOptionStatusDisplayItem extends StatusDisplayItem{
|
||||
private boolean showResults;
|
||||
private float votesFraction; // 0..1
|
||||
private boolean isMostVoted;
|
||||
private final int optionIndex;
|
||||
public final Poll poll;
|
||||
|
||||
public PollOptionStatusDisplayItem(String parentID, Poll poll, Poll.Option option, BaseStatusListFragment parentFragment){
|
||||
public PollOptionStatusDisplayItem(String parentID, Poll poll, int optionIndex, BaseStatusListFragment parentFragment){
|
||||
super(parentID, parentFragment);
|
||||
this.option=option;
|
||||
this.optionIndex=optionIndex;
|
||||
option=poll.options.get(optionIndex);
|
||||
this.poll=poll;
|
||||
text=HtmlParser.parseCustomEmoji(option.title, poll.emojis);
|
||||
emojiHelper.setText(text);
|
||||
@@ -64,7 +68,7 @@ public class PollOptionStatusDisplayItem extends StatusDisplayItem{
|
||||
private final TextView text, percent;
|
||||
private final View button;
|
||||
private final ImageView icon;
|
||||
private final Drawable progressBg;
|
||||
private final Drawable progressBg, progressBgInset;
|
||||
|
||||
public Holder(Activity activity, ViewGroup parent){
|
||||
super(activity, R.layout.display_item_poll_option, parent);
|
||||
@@ -73,7 +77,10 @@ public class PollOptionStatusDisplayItem extends StatusDisplayItem{
|
||||
icon=findViewById(R.id.icon);
|
||||
button=findViewById(R.id.button);
|
||||
progressBg=activity.getResources().getDrawable(R.drawable.bg_poll_option_voted, activity.getTheme()).mutate();
|
||||
progressBgInset=activity.getResources().getDrawable(R.drawable.bg_poll_option_voted_inset, activity.getTheme()).mutate();
|
||||
itemView.setOnClickListener(this::onButtonClick);
|
||||
button.setOutlineProvider(OutlineProviders.roundedRect(24));
|
||||
button.setClipToOutline(true);
|
||||
}
|
||||
|
||||
@Override
|
||||
@@ -86,17 +93,21 @@ public class PollOptionStatusDisplayItem extends StatusDisplayItem{
|
||||
item.showResults ? R.drawable.ic_poll_option_button : R.drawable.ic_fluent_radio_button_24_selector
|
||||
));
|
||||
if(item.showResults){
|
||||
progressBg.setLevel(Math.round(10000f*item.votesFraction));
|
||||
button.setBackground(progressBg);
|
||||
itemView.setSelected(item.isMostVoted);
|
||||
icon.setSelected(item.poll.ownVotes.contains(item.poll.options.indexOf(item.option)));
|
||||
icon.setVisibility(item.poll.voted && item.poll.ownVotes.isEmpty() ? View.GONE : View.VISIBLE);
|
||||
Drawable bg=item.inset ? progressBgInset : progressBg;
|
||||
bg.setLevel(Math.round(10000f*item.votesFraction));
|
||||
button.setBackground(bg);
|
||||
itemView.setSelected(item.poll.ownVotes!=null && item.poll.ownVotes.contains(item.optionIndex));
|
||||
percent.setText(String.format(Locale.getDefault(), "%d%%", Math.round(item.votesFraction*100f)));
|
||||
}else{
|
||||
itemView.setSelected(item.poll.selectedOptions!=null && item.poll.selectedOptions.contains(item.option));
|
||||
button.setBackgroundResource(R.drawable.bg_poll_option_clickable);
|
||||
icon.setSelected(itemView.isSelected());
|
||||
icon.setVisibility(View.VISIBLE);
|
||||
button.setBackgroundResource(item.inset ? R.drawable.bg_poll_option_clickable_inset : R.drawable.bg_poll_option_clickable);
|
||||
}
|
||||
if(item.inset){
|
||||
text.setTextColor(itemView.getContext().getColorStateList(R.color.poll_option_text_inset));
|
||||
percent.setTextColor(itemView.getContext().getColorStateList(R.color.poll_option_text_inset));
|
||||
}else{
|
||||
text.setTextColor(UiUtils.getThemeColor(itemView.getContext(), android.R.attr.textColorPrimary));
|
||||
percent.setTextColor(UiUtils.getThemeColor(itemView.getContext(), R.attr.colorM3OnSecondaryContainer));
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -106,7 +106,7 @@ public class ReblogOrReplyLineStatusDisplayItem extends StatusDisplayItem{
|
||||
text=findViewById(R.id.text);
|
||||
extraText=findViewById(R.id.extra_text);
|
||||
separator=findViewById(R.id.separator);
|
||||
if (GlobalUserPreferences.replyLineAboveHeader && GlobalUserPreferences.compactReblogReplyLine) {
|
||||
if (GlobalUserPreferences.compactReblogReplyLine) {
|
||||
parent.addOnLayoutChangeListener((v, l, t, right, b, ol, ot, oldRight, ob) -> {
|
||||
if (right != oldRight) layoutLine();
|
||||
});
|
||||
@@ -151,9 +151,7 @@ public class ReblogOrReplyLineStatusDisplayItem extends StatusDisplayItem{
|
||||
|
||||
private void layoutLine() {
|
||||
// layout line only if above header, compact and has extra
|
||||
if (!GlobalUserPreferences.replyLineAboveHeader
|
||||
|| !GlobalUserPreferences.compactReblogReplyLine
|
||||
|| item.extra == null) return;
|
||||
if (!GlobalUserPreferences.compactReblogReplyLine || item.extra == null) return;
|
||||
itemView.measure(
|
||||
View.MeasureSpec.makeMeasureSpec(parent.getWidth(), View.MeasureSpec.EXACTLY),
|
||||
View.MeasureSpec.UNSPECIFIED);
|
||||
|
||||
@@ -0,0 +1,55 @@
|
||||
package org.joinmastodon.android.ui.displayitems;
|
||||
|
||||
import android.content.Context;
|
||||
import android.view.View;
|
||||
import android.view.ViewGroup;
|
||||
import android.widget.Button;
|
||||
import android.widget.TextView;
|
||||
|
||||
import org.joinmastodon.android.R;
|
||||
import org.joinmastodon.android.fragments.BaseStatusListFragment;
|
||||
|
||||
public class SectionHeaderStatusDisplayItem extends StatusDisplayItem{
|
||||
public final String title, buttonText;
|
||||
public final Runnable onButtonClick;
|
||||
|
||||
public SectionHeaderStatusDisplayItem(BaseStatusListFragment parentFragment, String title, String buttonText, Runnable onButtonClick){
|
||||
super("", parentFragment);
|
||||
this.title=title;
|
||||
this.buttonText=buttonText;
|
||||
this.onButtonClick=onButtonClick;
|
||||
}
|
||||
|
||||
@Override
|
||||
public Type getType(){
|
||||
return Type.SECTION_HEADER;
|
||||
}
|
||||
|
||||
public static class Holder extends StatusDisplayItem.Holder<SectionHeaderStatusDisplayItem>{
|
||||
private final TextView title;
|
||||
private final Button actionBtn;
|
||||
|
||||
public Holder(Context context, ViewGroup parent){
|
||||
super(context, R.layout.display_item_section_header, parent);
|
||||
title=findViewById(R.id.title);
|
||||
actionBtn=findViewById(R.id.action_btn);
|
||||
actionBtn.setOnClickListener(v->item.onButtonClick.run());
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onBind(SectionHeaderStatusDisplayItem item){
|
||||
title.setText(item.title);
|
||||
if(item.onButtonClick!=null){
|
||||
actionBtn.setVisibility(View.VISIBLE);
|
||||
actionBtn.setText(item.buttonText);
|
||||
}else{
|
||||
actionBtn.setVisibility(View.GONE);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean isEnabled(){
|
||||
return false;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,110 @@
|
||||
package org.joinmastodon.android.ui.displayitems;
|
||||
|
||||
import android.content.Context;
|
||||
import android.graphics.drawable.Drawable;
|
||||
import android.graphics.drawable.LayerDrawable;
|
||||
import android.text.TextUtils;
|
||||
import android.view.View;
|
||||
import android.view.ViewGroup;
|
||||
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.Status;
|
||||
import org.joinmastodon.android.ui.OutlineProviders;
|
||||
import org.joinmastodon.android.ui.drawables.SpoilerStripesDrawable;
|
||||
import org.joinmastodon.android.ui.drawables.TiledDrawable;
|
||||
import org.joinmastodon.android.ui.text.HtmlParser;
|
||||
import org.joinmastodon.android.ui.utils.CustomEmojiHelper;
|
||||
|
||||
import java.util.ArrayList;
|
||||
|
||||
import me.grishka.appkit.imageloader.ImageLoaderViewHolder;
|
||||
import me.grishka.appkit.imageloader.requests.ImageLoaderRequest;
|
||||
|
||||
public class SpoilerStatusDisplayItem extends StatusDisplayItem{
|
||||
public final Status status;
|
||||
public final ArrayList<StatusDisplayItem> contentItems=new ArrayList<>();
|
||||
private final CharSequence parsedTitle;
|
||||
private final CustomEmojiHelper emojiHelper;
|
||||
private final Type type;
|
||||
|
||||
public SpoilerStatusDisplayItem(String parentID, BaseStatusListFragment<?> parentFragment, String title, Status statusForContent, Type type){
|
||||
super(parentID, parentFragment);
|
||||
this.status=statusForContent;
|
||||
this.type=type;
|
||||
if(TextUtils.isEmpty(title)){
|
||||
parsedTitle=HtmlParser.parseCustomEmoji(statusForContent.spoilerText, statusForContent.emojis);
|
||||
emojiHelper=new CustomEmojiHelper();
|
||||
emojiHelper.setText(parsedTitle);
|
||||
}else{
|
||||
parsedTitle=title;
|
||||
emojiHelper=null;
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public int getImageCount(){
|
||||
return emojiHelper==null ? 0 : emojiHelper.getImageCount();
|
||||
}
|
||||
|
||||
@Override
|
||||
public ImageLoaderRequest getImageRequest(int index){
|
||||
return emojiHelper.getImageRequest(index);
|
||||
}
|
||||
|
||||
@Override
|
||||
public Type getType(){
|
||||
return type;
|
||||
}
|
||||
|
||||
public static class Holder extends StatusDisplayItem.Holder<SpoilerStatusDisplayItem> implements ImageLoaderViewHolder{
|
||||
private final TextView title, action;
|
||||
private final View button;
|
||||
|
||||
public Holder(Context context, ViewGroup parent, Type type){
|
||||
super(context, R.layout.display_item_spoiler, parent);
|
||||
title=findViewById(R.id.spoiler_title);
|
||||
action=findViewById(R.id.spoiler_action);
|
||||
button=findViewById(R.id.spoiler_button);
|
||||
|
||||
button.setOutlineProvider(OutlineProviders.roundedRect(8));
|
||||
button.setClipToOutline(true);
|
||||
LayerDrawable spoilerBg=(LayerDrawable) button.getBackground().mutate();
|
||||
if(type==Type.SPOILER){
|
||||
spoilerBg.setDrawableByLayerId(R.id.left_drawable, new SpoilerStripesDrawable(true));
|
||||
spoilerBg.setDrawableByLayerId(R.id.right_drawable, new SpoilerStripesDrawable(false));
|
||||
}else if(type==Type.FILTER_SPOILER){
|
||||
Drawable texture=context.getDrawable(R.drawable.filter_banner_stripe_texture);
|
||||
spoilerBg.setDrawableByLayerId(R.id.left_drawable, new TiledDrawable(texture));
|
||||
spoilerBg.setDrawableByLayerId(R.id.right_drawable, new TiledDrawable(texture));
|
||||
}
|
||||
button.setBackground(spoilerBg);
|
||||
button.setOnClickListener(v->item.parentFragment.onRevealSpoilerClick(this));
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onBind(SpoilerStatusDisplayItem item){
|
||||
title.setText(item.parsedTitle);
|
||||
action.setText(item.status.spoilerRevealed ? R.string.spoiler_hide : R.string.sk_spoiler_show);
|
||||
itemView.setPadding(
|
||||
itemView.getPaddingLeft(),
|
||||
itemView.getPaddingTop(),
|
||||
itemView.getPaddingRight(),
|
||||
item.inset || GlobalUserPreferences.spectatorMode ? itemView.getPaddingTop() : 0
|
||||
);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void setImage(int index, Drawable image){
|
||||
item.emojiHelper.setImageDrawable(index, image);
|
||||
title.invalidate();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void clearImage(int index){
|
||||
setImage(index, null);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,9 +1,10 @@
|
||||
package org.joinmastodon.android.ui.displayitems;
|
||||
|
||||
import android.app.Activity;
|
||||
import android.app.Fragment;
|
||||
import android.content.Context;
|
||||
import android.graphics.drawable.ColorDrawable;
|
||||
import android.os.Bundle;
|
||||
import android.text.SpannableStringBuilder;
|
||||
import android.text.TextUtils;
|
||||
import android.view.View;
|
||||
import android.view.ViewGroup;
|
||||
@@ -20,14 +21,17 @@ import org.joinmastodon.android.fragments.ThreadFragment;
|
||||
import org.joinmastodon.android.model.Account;
|
||||
import org.joinmastodon.android.model.Attachment;
|
||||
import org.joinmastodon.android.model.DisplayItemsParent;
|
||||
import org.joinmastodon.android.model.Filter;
|
||||
import org.joinmastodon.android.model.LegacyFilter;
|
||||
import org.joinmastodon.android.model.FilterAction;
|
||||
import org.joinmastodon.android.model.FilterContext;
|
||||
import org.joinmastodon.android.model.FilterResult;
|
||||
import org.joinmastodon.android.model.Notification;
|
||||
import org.joinmastodon.android.model.Poll;
|
||||
import org.joinmastodon.android.model.ScheduledStatus;
|
||||
import org.joinmastodon.android.model.Status;
|
||||
import org.joinmastodon.android.ui.PhotoLayoutHelper;
|
||||
import org.joinmastodon.android.ui.text.HtmlParser;
|
||||
import org.joinmastodon.android.ui.utils.UiUtils;
|
||||
import org.joinmastodon.android.ui.viewholders.AccountViewHolder;
|
||||
import org.joinmastodon.android.utils.StatusFilterPredicate;
|
||||
import org.parceler.Parcels;
|
||||
|
||||
@@ -53,6 +57,13 @@ public abstract class StatusDisplayItem{
|
||||
isMainStatus = true,
|
||||
isDirectDescendant = false;
|
||||
|
||||
public static final int FLAG_INSET=1;
|
||||
public static final int FLAG_NO_FOOTER=1 << 1;
|
||||
public static final int FLAG_CHECKABLE=1 << 2;
|
||||
public static final int FLAG_MEDIA_FORCE_HIDDEN=1 << 3;
|
||||
public static final int FLAG_NO_HEADER=1 << 4;
|
||||
public static final int FLAG_NO_TRANSLATE=1 << 5;
|
||||
|
||||
public void setAncestryInfo(
|
||||
boolean hasDescendantNeighbor,
|
||||
boolean hasAncestoringNeighbor,
|
||||
@@ -80,9 +91,10 @@ public abstract class StatusDisplayItem{
|
||||
return null;
|
||||
}
|
||||
|
||||
public static BindableViewHolder<? extends StatusDisplayItem> createViewHolder(Type type, Activity activity, ViewGroup parent){
|
||||
public static BindableViewHolder<? extends StatusDisplayItem> createViewHolder(Type type, Activity activity, ViewGroup parent, Fragment parentFragment){
|
||||
return switch(type){
|
||||
case HEADER -> new HeaderStatusDisplayItem.Holder(activity, parent);
|
||||
case HEADER_CHECKABLE -> new CheckableHeaderStatusDisplayItem.Holder(activity, parent);
|
||||
case REBLOG_OR_REPLY_LINE -> new ReblogOrReplyLineStatusDisplayItem.Holder(activity, parent);
|
||||
case TEXT -> new TextStatusDisplayItem.Holder(activity, parent);
|
||||
case AUDIO -> new AudioStatusDisplayItem.Holder(activity, parent);
|
||||
@@ -91,18 +103,29 @@ public abstract class StatusDisplayItem{
|
||||
case CARD -> new LinkCardStatusDisplayItem.Holder(activity, parent);
|
||||
case FOOTER -> new FooterStatusDisplayItem.Holder(activity, parent);
|
||||
case ACCOUNT_CARD -> new AccountCardStatusDisplayItem.Holder(activity, parent);
|
||||
case ACCOUNT -> new AccountStatusDisplayItem.Holder(activity, parent);
|
||||
case ACCOUNT -> new AccountStatusDisplayItem.Holder(new AccountViewHolder(parentFragment, parent, null));
|
||||
case HASHTAG -> new HashtagStatusDisplayItem.Holder(activity, parent);
|
||||
case GAP -> new GapStatusDisplayItem.Holder(activity, parent);
|
||||
case EXTENDED_FOOTER -> new ExtendedFooterStatusDisplayItem.Holder(activity, parent);
|
||||
case MEDIA_GRID -> new MediaGridStatusDisplayItem.Holder(activity, parent);
|
||||
case WARNING -> new WarningFilteredStatusDisplayItem.Holder(activity, parent);
|
||||
case FILE -> new FileStatusDisplayItem.Holder(activity, parent);
|
||||
case SPOILER, FILTER_SPOILER -> new SpoilerStatusDisplayItem.Holder(activity, parent, type);
|
||||
case SECTION_HEADER -> null; // new SectionHeaderStatusDisplayItem.Holder(activity, parent);
|
||||
case NOTIFICATION_HEADER -> new NotificationHeaderStatusDisplayItem.Holder(activity, parent);
|
||||
case DUMMY -> new InsetDummyStatusDisplayItem.Holder(activity);
|
||||
};
|
||||
}
|
||||
|
||||
public static ArrayList<StatusDisplayItem> buildItems(BaseStatusListFragment<?> fragment, Status status, String accountID, DisplayItemsParent parentObject, Map<String, Account> knownAccounts, boolean inset, boolean addFooter, Notification notification, Filter.FilterContext filterContext){
|
||||
return buildItems(fragment, status, accountID, parentObject, knownAccounts, inset, addFooter, notification, false, filterContext);
|
||||
public static ArrayList<StatusDisplayItem> buildItems(BaseStatusListFragment<?> fragment, Status status, String accountID, DisplayItemsParent parentObject, Map<String, Account> knownAccounts, boolean inset, boolean addFooter, boolean disableTranslate, FilterContext filterContext) {
|
||||
int flags=0;
|
||||
if(inset)
|
||||
flags|=FLAG_INSET;
|
||||
if(!addFooter)
|
||||
flags|=FLAG_NO_FOOTER;
|
||||
if (disableTranslate)
|
||||
flags|=FLAG_NO_TRANSLATE;
|
||||
return buildItems(fragment, status, accountID, parentObject, knownAccounts, filterContext, flags);
|
||||
}
|
||||
|
||||
public static ReblogOrReplyLineStatusDisplayItem buildReplyLine(BaseStatusListFragment<?> fragment, Status status, String accountID, DisplayItemsParent parent, Account account, boolean threadReply) {
|
||||
@@ -120,70 +143,91 @@ 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, FilterContext filterContext, int flags){
|
||||
String parentID=parentObject.getID();
|
||||
ArrayList<StatusDisplayItem> items=new ArrayList<>();
|
||||
|
||||
Status statusForContent=status.getContentStatus();
|
||||
Bundle args=new Bundle();
|
||||
args.putString("account", accountID);
|
||||
ScheduledStatus scheduledStatus = parentObject instanceof ScheduledStatus ? (ScheduledStatus) parentObject : null;
|
||||
|
||||
ReblogOrReplyLineStatusDisplayItem replyLine = null;
|
||||
boolean threadReply = statusForContent.inReplyToAccountId != null &&
|
||||
statusForContent.inReplyToAccountId.equals(statusForContent.account.id);
|
||||
HeaderStatusDisplayItem header=null;
|
||||
boolean hideCounts=!AccountSessionManager.get(accountID).getLocalPreferences().showInteractionCounts;
|
||||
|
||||
if(statusForContent.inReplyToAccountId!=null && !(threadReply && fragment instanceof ThreadFragment)){
|
||||
Account account = knownAccounts.get(statusForContent.inReplyToAccountId);
|
||||
replyLine = buildReplyLine(fragment, status, accountID, parentObject, account, threadReply);
|
||||
if((flags & FLAG_NO_HEADER)==0){
|
||||
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);
|
||||
replyLine = buildReplyLine(fragment, status, accountID, parentObject, account, threadReply);
|
||||
}
|
||||
|
||||
if(status.reblog!=null){
|
||||
boolean isOwnPost = AccountSessionManager.getInstance().isSelf(fragment.getAccountID(), status.account);
|
||||
String fullText = fragment.getString(R.string.user_boosted, status.account.displayName);
|
||||
String text = GlobalUserPreferences.compactReblogReplyLine && replyLine != null ? status.account.displayName : fullText;
|
||||
items.add(new ReblogOrReplyLineStatusDisplayItem(parentID, fragment, text, status.account.emojis, R.drawable.ic_fluent_arrow_repeat_all_20sp_filled, isOwnPost ? status.visibility : null, i->{
|
||||
args.putParcelable("profileAccount", Parcels.wrap(status.account));
|
||||
Nav.go(fragment.getActivity(), ProfileFragment.class, args);
|
||||
}, fullText));
|
||||
} else if (!(status.tags.isEmpty() ||
|
||||
fragment instanceof HashtagTimelineFragment ||
|
||||
fragment instanceof ListTimelineFragment
|
||||
) && fragment.getParentFragment() instanceof HomeTabFragment home) {
|
||||
home.getHashtags().stream()
|
||||
.filter(followed -> status.tags.stream()
|
||||
.anyMatch(hashtag -> followed.name.equalsIgnoreCase(hashtag.name)))
|
||||
.findAny()
|
||||
// post contains a hashtag the user is following
|
||||
.ifPresent(hashtag -> items.add(new ReblogOrReplyLineStatusDisplayItem(
|
||||
parentID, fragment, hashtag.name, List.of(),
|
||||
R.drawable.ic_fluent_number_symbol_20sp_filled, null,
|
||||
i -> {
|
||||
args.putString("hashtag", hashtag.name);
|
||||
Nav.go(fragment.getActivity(), HashtagTimelineFragment.class, args);
|
||||
}
|
||||
)));
|
||||
}
|
||||
|
||||
if (replyLine != null) {
|
||||
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);
|
||||
}
|
||||
}
|
||||
|
||||
if((flags & FLAG_CHECKABLE)!=0)
|
||||
items.add(header=new CheckableHeaderStatusDisplayItem(parentID, statusForContent.account, statusForContent.createdAt, fragment, accountID, statusForContent, null));
|
||||
else
|
||||
items.add(header=new HeaderStatusDisplayItem(parentID, statusForContent.account, statusForContent.createdAt, fragment, accountID, statusForContent, null, null, scheduledStatus));
|
||||
}
|
||||
|
||||
if(status.reblog!=null){
|
||||
boolean isOwnPost = AccountSessionManager.getInstance().isSelf(fragment.getAccountID(), status.account);
|
||||
String fullText = fragment.getString(R.string.user_boosted, status.account.displayName);
|
||||
String text = GlobalUserPreferences.compactReblogReplyLine && replyLine != null ? status.account.displayName : fullText;
|
||||
items.add(new ReblogOrReplyLineStatusDisplayItem(parentID, fragment, text, status.account.emojis, R.drawable.ic_fluent_arrow_repeat_all_20sp_filled, isOwnPost ? status.visibility : null, i->{
|
||||
args.putParcelable("profileAccount", Parcels.wrap(status.account));
|
||||
Nav.go(fragment.getActivity(), ProfileFragment.class, args);
|
||||
}, fullText));
|
||||
} else if (!(status.tags.isEmpty() ||
|
||||
fragment instanceof HashtagTimelineFragment ||
|
||||
fragment instanceof ListTimelineFragment
|
||||
) && fragment.getParentFragment() instanceof HomeTabFragment home) {
|
||||
home.getHashtags().stream()
|
||||
.filter(followed -> status.tags.stream()
|
||||
.anyMatch(hashtag -> followed.name.equalsIgnoreCase(hashtag.name)))
|
||||
.findAny()
|
||||
// post contains a hashtag the user is following
|
||||
.ifPresent(hashtag -> items.add(new ReblogOrReplyLineStatusDisplayItem(
|
||||
parentID, fragment, hashtag.name, List.of(),
|
||||
R.drawable.ic_fluent_number_symbol_20sp_filled, null,
|
||||
i -> {
|
||||
args.putString("hashtag", hashtag.name);
|
||||
Nav.go(fragment.getActivity(), HashtagTimelineFragment.class, args);
|
||||
}
|
||||
)));
|
||||
}
|
||||
|
||||
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);
|
||||
boolean filtered=false;
|
||||
if(status.filtered!=null){
|
||||
for(FilterResult filter:status.filtered){
|
||||
if(filter.filter.isActive()){
|
||||
filtered=true;
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
HeaderStatusDisplayItem header;
|
||||
items.add(header=new HeaderStatusDisplayItem(parentID, statusForContent.account, statusForContent.createdAt, fragment, accountID, statusForContent, null, notification, scheduledStatus));
|
||||
|
||||
if (replyLine != null && !GlobalUserPreferences.replyLineAboveHeader) {
|
||||
replyLine.belowHeader = true;
|
||||
items.add(replyLine);
|
||||
ArrayList<StatusDisplayItem> contentItems;
|
||||
if(!TextUtils.isEmpty(statusForContent.spoilerText)){
|
||||
if (AccountSessionManager.get(accountID).getLocalPreferences().revealCWs) statusForContent.spoilerRevealed = true;
|
||||
SpoilerStatusDisplayItem spoilerItem=new SpoilerStatusDisplayItem(parentID, fragment, null, statusForContent, Type.SPOILER);
|
||||
items.add(spoilerItem);
|
||||
contentItems=spoilerItem.contentItems;
|
||||
}else{
|
||||
contentItems=items;
|
||||
}
|
||||
|
||||
if (statusForContent.quote != null) {
|
||||
@@ -195,74 +239,85 @@ public abstract class StatusDisplayItem{
|
||||
statusForContent.content += quoteInline;
|
||||
}
|
||||
}
|
||||
if(!TextUtils.isEmpty(statusForContent.content))
|
||||
items.add(new TextStatusDisplayItem(parentID, HtmlParser.parse(statusForContent.content, statusForContent.emojis, statusForContent.mentions, statusForContent.tags, accountID), fragment, statusForContent, disableTranslate));
|
||||
else if (!GlobalUserPreferences.replyLineAboveHeader && replyLine != null)
|
||||
replyLine.needBottomPadding=true;
|
||||
else
|
||||
header.needBottomPadding=true;
|
||||
|
||||
List<Attachment> imageAttachments=statusForContent.mediaAttachments.stream()
|
||||
.filter(att->att.type.isImage() && !att.type.equals(Attachment.Type.UNKNOWN))
|
||||
.collect(Collectors.toList());
|
||||
if(!TextUtils.isEmpty(statusForContent.content)){
|
||||
SpannableStringBuilder parsedText=HtmlParser.parse(statusForContent.content, statusForContent.emojis, statusForContent.mentions, statusForContent.tags, accountID);
|
||||
HtmlParser.applyFilterHighlights(fragment.getActivity(), parsedText, status.filtered);
|
||||
TextStatusDisplayItem text=new TextStatusDisplayItem(parentID, HtmlParser.parse(statusForContent.content, statusForContent.emojis, statusForContent.mentions, statusForContent.tags, accountID), fragment, statusForContent, (flags & FLAG_NO_TRANSLATE) != 0);
|
||||
contentItems.add(text);
|
||||
} else if (header!=null){
|
||||
header.needBottomPadding=true;
|
||||
}
|
||||
|
||||
List<Attachment> imageAttachments=statusForContent.mediaAttachments.stream().filter(att->att.type.isImage()).collect(Collectors.toList());
|
||||
if(!imageAttachments.isEmpty()){
|
||||
int color = UiUtils.getThemeColor(fragment.getContext(), R.attr.colorAccentLightest);
|
||||
for (Attachment att : imageAttachments) {
|
||||
if (att.blurhashPlaceholder == null) {
|
||||
att.blurhashPlaceholder = new ColorDrawable(color);
|
||||
}
|
||||
}
|
||||
PhotoLayoutHelper.TiledLayoutResult layout=PhotoLayoutHelper.processThumbs(imageAttachments);
|
||||
items.add(new MediaGridStatusDisplayItem(parentID, fragment, layout, imageAttachments, statusForContent));
|
||||
MediaGridStatusDisplayItem mediaGrid=new MediaGridStatusDisplayItem(parentID, fragment, layout, imageAttachments, statusForContent);
|
||||
if((flags & FLAG_MEDIA_FORCE_HIDDEN)!=0)
|
||||
mediaGrid.sensitiveTitle=fragment.getString(R.string.media_hidden);
|
||||
else if(statusForContent.sensitive && AccountSessionManager.get(accountID).getLocalPreferences().revealCWs && !AccountSessionManager.get(accountID).getLocalPreferences().hideSensitiveMedia)
|
||||
statusForContent.sensitiveRevealed=true;
|
||||
contentItems.add(mediaGrid);
|
||||
}
|
||||
for(Attachment att:statusForContent.mediaAttachments){
|
||||
if(att.type==Attachment.Type.AUDIO){
|
||||
items.add(new AudioStatusDisplayItem(parentID, fragment, statusForContent, att));
|
||||
contentItems.add(new AudioStatusDisplayItem(parentID, fragment, statusForContent, att));
|
||||
}
|
||||
if(att.type==Attachment.Type.UNKNOWN){
|
||||
contentItems.add(new FileStatusDisplayItem(parentID, fragment, att));
|
||||
}
|
||||
}
|
||||
|
||||
statusForContent.mediaAttachments.stream()
|
||||
.filter(att->att.type.equals(Attachment.Type.UNKNOWN))
|
||||
.map(att -> new FileStatusDisplayItem(parentID, fragment, att))
|
||||
.forEach(items::add);
|
||||
|
||||
if(statusForContent.poll!=null){
|
||||
buildPollItems(parentID, fragment, statusForContent.poll, items);
|
||||
buildPollItems(parentID, fragment, statusForContent.poll, contentItems);
|
||||
}
|
||||
if(statusForContent.card!=null && statusForContent.mediaAttachments.isEmpty() && TextUtils.isEmpty(statusForContent.spoilerText)){
|
||||
items.add(new LinkCardStatusDisplayItem(parentID, fragment, statusForContent));
|
||||
if(statusForContent.card!=null && statusForContent.mediaAttachments.isEmpty()){
|
||||
contentItems.add(new LinkCardStatusDisplayItem(parentID, fragment, statusForContent));
|
||||
}
|
||||
if(addFooter){
|
||||
items.add(new FooterStatusDisplayItem(parentID, fragment, statusForContent, accountID));
|
||||
if(contentItems!=items && statusForContent.spoilerRevealed){
|
||||
items.addAll(contentItems);
|
||||
}
|
||||
if((flags & FLAG_NO_FOOTER)==0){
|
||||
FooterStatusDisplayItem footer=new FooterStatusDisplayItem(parentID, fragment, statusForContent, accountID);
|
||||
footer.hideCounts=hideCounts;
|
||||
items.add(footer);
|
||||
if(status.hasGapAfter && !(fragment instanceof ThreadFragment))
|
||||
items.add(new GapStatusDisplayItem(parentID, fragment));
|
||||
}
|
||||
int i=1;
|
||||
boolean inset=(flags & FLAG_INSET)!=0;
|
||||
// add inset dummy so last content item doesn't clip out of inset bounds
|
||||
if (inset) {
|
||||
items.add(new InsetDummyStatusDisplayItem(parentID, fragment,
|
||||
!contentItems.isEmpty() && contentItems
|
||||
.get(contentItems.size() - 1) instanceof MediaGridStatusDisplayItem));
|
||||
}
|
||||
for(StatusDisplayItem item:items){
|
||||
item.inset=inset;
|
||||
item.index=i++;
|
||||
}
|
||||
if(items!=contentItems && !statusForContent.spoilerRevealed){
|
||||
for(StatusDisplayItem item:contentItems){
|
||||
item.inset=inset;
|
||||
item.index=i++;
|
||||
}
|
||||
}
|
||||
|
||||
Filter applyingFilter = null;
|
||||
LegacyFilter applyingFilter = null;
|
||||
if (!statusForContent.filterRevealed) {
|
||||
StatusFilterPredicate predicate = new StatusFilterPredicate(accountID, filterContext, Filter.FilterAction.WARN);
|
||||
StatusFilterPredicate predicate = new StatusFilterPredicate(accountID, filterContext, FilterAction.WARN);
|
||||
statusForContent.filterRevealed = predicate.test(status);
|
||||
applyingFilter = predicate.getApplyingFilter();
|
||||
}
|
||||
|
||||
ArrayList<StatusDisplayItem> result = statusForContent.filterRevealed ? items :
|
||||
return statusForContent.filterRevealed ? items :
|
||||
new ArrayList<>(List.of(new WarningFilteredStatusDisplayItem(parentID, fragment, statusForContent, items, applyingFilter)));
|
||||
|
||||
if (addFooter && status.hasGapAfter && !(fragment instanceof ThreadFragment)) {
|
||||
StatusDisplayItem gap = new GapStatusDisplayItem(parentID, fragment);
|
||||
gap.index = i++;
|
||||
result.add(gap);
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
public static void buildPollItems(String parentID, BaseStatusListFragment fragment, Poll poll, List<StatusDisplayItem> items){
|
||||
int i=0;
|
||||
for(Poll.Option opt:poll.options){
|
||||
items.add(new PollOptionStatusDisplayItem(parentID, poll, opt, fragment));
|
||||
items.add(new PollOptionStatusDisplayItem(parentID, poll, i, fragment));
|
||||
i++;
|
||||
}
|
||||
items.add(new PollFooterStatusDisplayItem(parentID, fragment, poll));
|
||||
}
|
||||
@@ -283,7 +338,13 @@ public abstract class StatusDisplayItem{
|
||||
EXTENDED_FOOTER,
|
||||
MEDIA_GRID,
|
||||
WARNING,
|
||||
FILE
|
||||
FILE,
|
||||
SPOILER,
|
||||
SECTION_HEADER,
|
||||
HEADER_CHECKABLE,
|
||||
NOTIFICATION_HEADER,
|
||||
FILTER_SPOILER,
|
||||
DUMMY
|
||||
}
|
||||
|
||||
public static abstract class Holder<T extends StatusDisplayItem> extends BindableViewHolder<T> implements UsableRecyclerView.DisableableClickable{
|
||||
|
||||
@@ -6,8 +6,8 @@ import android.graphics.drawable.Drawable;
|
||||
import android.text.TextUtils;
|
||||
import android.view.View;
|
||||
import android.view.ViewGroup;
|
||||
import android.widget.LinearLayout;
|
||||
import android.widget.Button;
|
||||
import android.widget.LinearLayout;
|
||||
import android.widget.ScrollView;
|
||||
import android.widget.TextView;
|
||||
import android.widget.Toast;
|
||||
@@ -44,9 +44,9 @@ import me.grishka.appkit.utils.V;
|
||||
|
||||
public class TextStatusDisplayItem extends StatusDisplayItem{
|
||||
private CharSequence text;
|
||||
private CustomEmojiHelper emojiHelper=new CustomEmojiHelper(), spoilerEmojiHelper;
|
||||
private CharSequence parsedSpoilerText;
|
||||
private CustomEmojiHelper emojiHelper=new CustomEmojiHelper();
|
||||
public boolean textSelectable;
|
||||
public boolean reduceTopPadding;
|
||||
public final Status status;
|
||||
public boolean disableTranslate, translationShown;
|
||||
private AccountSession session;
|
||||
@@ -59,11 +59,6 @@ public class TextStatusDisplayItem extends StatusDisplayItem{
|
||||
this.disableTranslate=disableTranslate;
|
||||
this.translationShown=status.translationShown;
|
||||
emojiHelper.setText(text);
|
||||
if(!TextUtils.isEmpty(status.spoilerText)){
|
||||
parsedSpoilerText=HtmlParser.parseCustomEmoji(status.spoilerText, status.emojis);
|
||||
spoilerEmojiHelper=new CustomEmojiHelper();
|
||||
spoilerEmojiHelper.setText(parsedSpoilerText);
|
||||
}
|
||||
session = AccountSessionManager.getInstance().getAccount(parentFragment.getAccountID());
|
||||
}
|
||||
|
||||
@@ -79,24 +74,18 @@ public class TextStatusDisplayItem extends StatusDisplayItem{
|
||||
|
||||
@Override
|
||||
public int getImageCount(){
|
||||
if(spoilerEmojiHelper!=null && !status.spoilerRevealed)
|
||||
return spoilerEmojiHelper.getImageCount();
|
||||
return emojiHelper.getImageCount();
|
||||
}
|
||||
|
||||
@Override
|
||||
public ImageLoaderRequest getImageRequest(int index){
|
||||
if(spoilerEmojiHelper!=null && !status.spoilerRevealed)
|
||||
return spoilerEmojiHelper.getImageRequest(index);
|
||||
return emojiHelper.getImageRequest(index);
|
||||
}
|
||||
|
||||
public static class Holder extends StatusDisplayItem.Holder<TextStatusDisplayItem> implements ImageLoaderViewHolder{
|
||||
private final LinkedTextView text;
|
||||
private final LinearLayout spoilerHeader;
|
||||
private final TextView spoilerTitle, spoilerTitleInline, translateInfo, readMore;
|
||||
private final View spoilerOverlay, borderTop, borderBottom, textWrap, translateWrap, translateProgress, spaceBelowText;
|
||||
private final int backgroundColor, borderColor;
|
||||
private final TextView translateInfo, readMore;
|
||||
private final View textWrap, translateWrap, translateProgress;
|
||||
private final Button translateButton;
|
||||
private final ScrollView textScrollView;
|
||||
|
||||
@@ -108,23 +97,13 @@ public class TextStatusDisplayItem extends StatusDisplayItem{
|
||||
super(activity, R.layout.display_item_text, parent);
|
||||
this.parent=parent;
|
||||
text=findViewById(R.id.text);
|
||||
spoilerTitle=findViewById(R.id.spoiler_title);
|
||||
spoilerTitleInline=findViewById(R.id.spoiler_title_inline);
|
||||
spoilerHeader=findViewById(R.id.spoiler_header);
|
||||
spoilerOverlay=findViewById(R.id.spoiler_overlay);
|
||||
borderTop=findViewById(R.id.border_top);
|
||||
borderBottom=findViewById(R.id.border_bottom);
|
||||
textWrap=findViewById(R.id.text_wrap);
|
||||
textWrap = (LinearLayout) itemView;
|
||||
translateWrap=findViewById(R.id.translate_wrap);
|
||||
translateButton=findViewById(R.id.translate_btn);
|
||||
translateInfo=findViewById(R.id.translate_info);
|
||||
translateProgress=findViewById(R.id.translate_progress);
|
||||
itemView.setOnClickListener(v->item.parentFragment.onRevealSpoilerClick(this));
|
||||
backgroundColor=UiUtils.getThemeColor(activity, R.attr.colorBackgroundLight);
|
||||
borderColor=UiUtils.getThemeColor(activity, R.attr.colorPollVoted);
|
||||
textScrollView=findViewById(R.id.text_scroll_view);
|
||||
readMore=findViewById(R.id.read_more);
|
||||
spaceBelowText=findViewById(R.id.space_below_text);
|
||||
textMaxHeight=activity.getResources().getDimension(R.dimen.text_max_height);
|
||||
textCollapsedHeight=activity.getResources().getDimension(R.dimen.text_collapsed_height);
|
||||
collapseParams=new LinearLayout.LayoutParams(ViewGroup.LayoutParams.MATCH_PARENT, (int) textCollapsedHeight);
|
||||
@@ -134,6 +113,7 @@ public class TextStatusDisplayItem extends StatusDisplayItem{
|
||||
|
||||
@Override
|
||||
public void onBind(TextStatusDisplayItem item){
|
||||
boolean hasSpoiler = !TextUtils.isEmpty(item.status.spoilerText);
|
||||
text.setText(item.translationShown
|
||||
? HtmlParser.parse(item.status.translation.content, item.status.emojis, item.status.mentions, item.status.tags, item.parentFragment.getAccountID())
|
||||
: item.text);
|
||||
@@ -141,32 +121,10 @@ public class TextStatusDisplayItem extends StatusDisplayItem{
|
||||
if (item.textSelectable) {
|
||||
textScrollView.setLayoutParams(new LinearLayout.LayoutParams(LinearLayout.LayoutParams.MATCH_PARENT, LinearLayout.LayoutParams.WRAP_CONTENT));
|
||||
}
|
||||
spoilerTitleInline.setTextIsSelectable(item.textSelectable);
|
||||
text.setInvalidateOnEveryFrame(false);
|
||||
spoilerTitleInline.setBackgroundColor(item.inset ? 0 : backgroundColor);
|
||||
spoilerTitleInline.setPadding(spoilerTitleInline.getPaddingLeft(), item.inset ? 0 : V.dp(14), spoilerTitleInline.getPaddingRight(), item.inset ? 0 : V.dp(14));
|
||||
borderTop.setBackgroundColor(item.inset ? 0 : borderColor);
|
||||
borderBottom.setBackgroundColor(item.inset ? 0 : borderColor);
|
||||
if(!TextUtils.isEmpty(item.status.spoilerText)){
|
||||
spoilerTitle.setText(item.parsedSpoilerText);
|
||||
spoilerTitleInline.setText(item.parsedSpoilerText);
|
||||
if(item.status.spoilerRevealed){
|
||||
spoilerOverlay.setVisibility(View.GONE);
|
||||
spoilerHeader.setVisibility(View.VISIBLE);
|
||||
textWrap.setVisibility(View.VISIBLE);
|
||||
itemView.setClickable(false);
|
||||
}else{
|
||||
spoilerOverlay.setVisibility(View.VISIBLE);
|
||||
spoilerHeader.setVisibility(View.GONE);
|
||||
textWrap.setVisibility(View.GONE);
|
||||
itemView.setClickable(true);
|
||||
}
|
||||
}else{
|
||||
spoilerOverlay.setVisibility(View.GONE);
|
||||
spoilerHeader.setVisibility(View.GONE);
|
||||
textWrap.setVisibility(View.VISIBLE);
|
||||
itemView.setClickable(false);
|
||||
}
|
||||
itemView.setClickable(false);
|
||||
itemView.setPadding(itemView.getPaddingLeft(), item.reduceTopPadding ? V.dp(6) : V.dp(12), itemView.getPaddingRight(), itemView.getPaddingBottom());
|
||||
text.setTextColor(UiUtils.getThemeColor(text.getContext(), item.inset ? R.attr.colorM3OnSurfaceVariant : R.attr.colorM3OnSurface));
|
||||
|
||||
Instance instanceInfo = AccountSessionManager.getInstance().getInstanceInfo(item.session.domain);
|
||||
boolean translateEnabled = !item.disableTranslate && instanceInfo != null &&
|
||||
@@ -234,7 +192,6 @@ public class TextStatusDisplayItem extends StatusDisplayItem{
|
||||
});
|
||||
|
||||
readMore.setText(item.status.textExpanded ? R.string.sk_collapse : R.string.sk_expand);
|
||||
spaceBelowText.setVisibility(translateVisible ? View.VISIBLE : View.GONE);
|
||||
|
||||
// remove additional padding when (transparently padded) translate button is visible
|
||||
int nextPos = getAbsoluteAdapterPosition() + 1;
|
||||
@@ -267,21 +224,25 @@ public class TextStatusDisplayItem extends StatusDisplayItem{
|
||||
|
||||
if (GlobalUserPreferences.collapseLongPosts && !item.status.textExpandable) {
|
||||
boolean tooBig = text.getMeasuredHeight() > textMaxHeight;
|
||||
boolean hasSpoiler = !TextUtils.isEmpty(item.status.spoilerText);
|
||||
boolean expandable = tooBig && !hasSpoiler;
|
||||
item.parentFragment.onEnableExpandable(Holder.this, expandable);
|
||||
}
|
||||
|
||||
readMore.setVisibility(item.status.textExpandable && !item.status.textExpanded ? View.VISIBLE : View.GONE);
|
||||
boolean expandButtonShown=item.status.textExpandable && !item.status.textExpanded;
|
||||
translateWrap.setPadding(0, V.dp(expandButtonShown ? 0 : 4), 0, 0);
|
||||
readMore.setVisibility(expandButtonShown ? View.VISIBLE : View.GONE);
|
||||
textScrollView.setLayoutParams(item.status.textExpandable && !item.status.textExpanded ? collapseParams : wrapParams);
|
||||
if (item.status.textExpandable && !translateVisible) spaceBelowText.setVisibility(View.VISIBLE);
|
||||
|
||||
// compensate for spoiler's bottom margin
|
||||
ViewGroup.MarginLayoutParams params = (ViewGroup.MarginLayoutParams) itemView.getLayoutParams();
|
||||
params.setMargins(params.leftMargin, (item.inset || GlobalUserPreferences.spectatorMode) && hasSpoiler ? V.dp(-16) : 0,
|
||||
params.rightMargin, params.bottomMargin);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void setImage(int index, Drawable image){
|
||||
getEmojiHelper().setImageDrawable(index, image);
|
||||
text.invalidate();
|
||||
spoilerTitle.invalidate();
|
||||
if(image instanceof Animatable){
|
||||
((Animatable) image).start();
|
||||
if(image instanceof MovieDrawable)
|
||||
@@ -296,7 +257,7 @@ public class TextStatusDisplayItem extends StatusDisplayItem{
|
||||
}
|
||||
|
||||
private CustomEmojiHelper getEmojiHelper(){
|
||||
return item.spoilerEmojiHelper!=null && !item.status.spoilerRevealed ? item.spoilerEmojiHelper : item.emojiHelper;
|
||||
return item.emojiHelper;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -7,7 +7,7 @@ import android.widget.TextView;
|
||||
|
||||
import org.joinmastodon.android.R;
|
||||
import org.joinmastodon.android.fragments.BaseStatusListFragment;
|
||||
import org.joinmastodon.android.model.Filter;
|
||||
import org.joinmastodon.android.model.LegacyFilter;
|
||||
import org.joinmastodon.android.model.Status;
|
||||
|
||||
import java.util.List;
|
||||
@@ -16,9 +16,9 @@ public class WarningFilteredStatusDisplayItem extends StatusDisplayItem{
|
||||
public boolean loading;
|
||||
public final Status status;
|
||||
public List<StatusDisplayItem> filteredItems;
|
||||
public Filter applyingFilter;
|
||||
public LegacyFilter applyingFilter;
|
||||
|
||||
public WarningFilteredStatusDisplayItem(String parentID, BaseStatusListFragment<?> parentFragment, Status status, List<StatusDisplayItem> filteredItems, Filter applyingFilter){
|
||||
public WarningFilteredStatusDisplayItem(String parentID, BaseStatusListFragment<?> parentFragment, Status status, List<StatusDisplayItem> filteredItems, LegacyFilter applyingFilter){
|
||||
super(parentID, parentFragment);
|
||||
this.status=status;
|
||||
this.filteredItems = filteredItems;
|
||||
|
||||
@@ -0,0 +1,103 @@
|
||||
package org.joinmastodon.android.ui.drawables;
|
||||
|
||||
import android.graphics.Canvas;
|
||||
import android.graphics.ColorFilter;
|
||||
import android.graphics.Paint;
|
||||
import android.graphics.PixelFormat;
|
||||
import android.graphics.Rect;
|
||||
import android.graphics.drawable.Drawable;
|
||||
import android.os.SystemClock;
|
||||
|
||||
import androidx.annotation.NonNull;
|
||||
import androidx.annotation.Nullable;
|
||||
import me.grishka.appkit.utils.CubicBezierInterpolator;
|
||||
import me.grishka.appkit.utils.V;
|
||||
|
||||
public class AudioAttachmentBackgroundDrawable extends Drawable{
|
||||
private int bgColor, wavesColor;
|
||||
private Paint paint=new Paint(Paint.ANTI_ALIAS_FLAG);
|
||||
private long[] animationStartTimes={0, 0};
|
||||
private boolean animationRunning;
|
||||
private Runnable[] restartRunnables={()->restartAnimation(0), ()->restartAnimation(1)};
|
||||
|
||||
@Override
|
||||
public void draw(@NonNull Canvas canvas){
|
||||
Rect bounds=getBounds();
|
||||
paint.setColor(bgColor);
|
||||
canvas.drawRect(bounds, paint);
|
||||
|
||||
float initialRadius=V.dp(48);
|
||||
float finalRadius=bounds.width()/2f;
|
||||
long time=SystemClock.uptimeMillis();
|
||||
boolean animationsStillRunning=false;
|
||||
|
||||
for(int i=0;i<animationStartTimes.length;i++){
|
||||
long t=time-animationStartTimes[i];
|
||||
if(t<0)
|
||||
continue;
|
||||
float fraction=t/3000f;
|
||||
if(fraction>1)
|
||||
continue;
|
||||
fraction=CubicBezierInterpolator.EASE_OUT.getInterpolation(fraction);
|
||||
paint.setColor(wavesColor);
|
||||
paint.setAlpha(Math.round(paint.getAlpha()*(1f-fraction)));
|
||||
canvas.drawCircle(bounds.centerX(), bounds.centerY(), initialRadius+(finalRadius-initialRadius)*fraction, paint);
|
||||
animationsStillRunning=true;
|
||||
}
|
||||
|
||||
if(animationsStillRunning){
|
||||
invalidateSelf();
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void setAlpha(int alpha){
|
||||
|
||||
}
|
||||
|
||||
@Override
|
||||
public void setColorFilter(@Nullable ColorFilter colorFilter){
|
||||
|
||||
}
|
||||
|
||||
@Override
|
||||
public int getOpacity(){
|
||||
return PixelFormat.OPAQUE;
|
||||
}
|
||||
|
||||
public void setColors(int bg, int waves){
|
||||
bgColor=bg;
|
||||
wavesColor=waves;
|
||||
}
|
||||
|
||||
public void startAnimation(){
|
||||
if(animationRunning)
|
||||
return;
|
||||
|
||||
long time=SystemClock.uptimeMillis();
|
||||
animationStartTimes[0]=time;
|
||||
scheduleSelf(restartRunnables[0], time+3000);
|
||||
scheduleSelf(restartRunnables[1], time+1500);
|
||||
animationRunning=true;
|
||||
invalidateSelf();
|
||||
}
|
||||
|
||||
public void stopAnimation(boolean gracefully){
|
||||
if(!animationRunning)
|
||||
return;
|
||||
|
||||
animationRunning=false;
|
||||
for(Runnable r:restartRunnables)
|
||||
unscheduleSelf(r);
|
||||
if(!gracefully){
|
||||
animationStartTimes[0]=animationStartTimes[1]=0;
|
||||
}
|
||||
}
|
||||
|
||||
private void restartAnimation(int index){
|
||||
long time=SystemClock.uptimeMillis();
|
||||
animationStartTimes[index]=time;
|
||||
if(animationRunning)
|
||||
scheduleSelf(restartRunnables[index], time+3000);
|
||||
}
|
||||
}
|
||||
@@ -1,84 +0,0 @@
|
||||
package org.joinmastodon.android.ui.drawables;
|
||||
|
||||
import android.graphics.Canvas;
|
||||
import android.graphics.ColorFilter;
|
||||
import android.graphics.Paint;
|
||||
import android.graphics.Path;
|
||||
import android.graphics.PixelFormat;
|
||||
import android.graphics.Rect;
|
||||
import android.graphics.drawable.Drawable;
|
||||
|
||||
import androidx.annotation.NonNull;
|
||||
import androidx.annotation.Nullable;
|
||||
import me.grishka.appkit.utils.V;
|
||||
|
||||
public class ComposeAutocompleteBackgroundDrawable extends Drawable{
|
||||
private Path path=new Path();
|
||||
private Paint paint=new Paint(Paint.ANTI_ALIAS_FLAG);
|
||||
private int fillColor, arrowOffset;
|
||||
|
||||
public ComposeAutocompleteBackgroundDrawable(int fillColor){
|
||||
this.fillColor=fillColor;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void draw(@NonNull Canvas canvas){
|
||||
Rect bounds=getBounds();
|
||||
canvas.save();
|
||||
canvas.translate(bounds.left, bounds.top);
|
||||
paint.setColor(0x80000000);
|
||||
canvas.drawPath(path, paint);
|
||||
canvas.translate(0, V.dp(1));
|
||||
paint.setColor(fillColor);
|
||||
canvas.drawPath(path, paint);
|
||||
int arrowSize=V.dp(10);
|
||||
canvas.drawRect(0, arrowSize, bounds.width(), bounds.height(), paint);
|
||||
canvas.restore();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void setAlpha(int alpha){
|
||||
|
||||
}
|
||||
|
||||
@Override
|
||||
public void setColorFilter(@Nullable ColorFilter colorFilter){
|
||||
|
||||
}
|
||||
|
||||
@Override
|
||||
public int getOpacity(){
|
||||
return PixelFormat.TRANSLUCENT;
|
||||
}
|
||||
|
||||
public void setArrowOffset(int offset){
|
||||
arrowOffset=offset;
|
||||
updatePath();
|
||||
invalidateSelf();
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void onBoundsChange(Rect bounds){
|
||||
super.onBoundsChange(bounds);
|
||||
updatePath();
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean getPadding(@NonNull Rect padding){
|
||||
padding.top=V.dp(11);
|
||||
return true;
|
||||
}
|
||||
|
||||
private void updatePath(){
|
||||
path.rewind();
|
||||
int arrowSize=V.dp(10);
|
||||
path.moveTo(0, arrowSize*2);
|
||||
path.lineTo(0, arrowSize);
|
||||
path.lineTo(arrowOffset-arrowSize, arrowSize);
|
||||
path.lineTo(arrowOffset, 0);
|
||||
path.lineTo(arrowOffset+arrowSize, arrowSize);
|
||||
path.lineTo(getBounds().width(), arrowSize);
|
||||
path.lineTo(getBounds().width(), arrowSize*2);
|
||||
path.close();
|
||||
}
|
||||
}
|
||||
@@ -1,58 +0,0 @@
|
||||
package org.joinmastodon.android.ui.drawables;
|
||||
|
||||
import android.graphics.Canvas;
|
||||
import android.graphics.ColorFilter;
|
||||
import android.graphics.LinearGradient;
|
||||
import android.graphics.Matrix;
|
||||
import android.graphics.Paint;
|
||||
import android.graphics.PixelFormat;
|
||||
import android.graphics.Rect;
|
||||
import android.graphics.Shader;
|
||||
import android.graphics.drawable.Drawable;
|
||||
|
||||
import androidx.annotation.NonNull;
|
||||
import androidx.annotation.Nullable;
|
||||
import me.grishka.appkit.utils.V;
|
||||
|
||||
public class CoverOverlayGradientDrawable extends Drawable{
|
||||
private LinearGradient gradient=new LinearGradient(0f, 0f, 0f, 100f, 0xB0000000, 0, Shader.TileMode.CLAMP);
|
||||
private Matrix gradientMatrix=new Matrix();
|
||||
private int topPadding, topOffset;
|
||||
private Paint paint=new Paint();
|
||||
|
||||
public CoverOverlayGradientDrawable(){
|
||||
paint.setShader(gradient);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void draw(@NonNull Canvas canvas){
|
||||
Rect bounds=getBounds();
|
||||
gradientMatrix.setScale(1f, (bounds.height()-V.dp(40)-topPadding)/100f);
|
||||
gradientMatrix.postTranslate(0, topPadding+topOffset);
|
||||
gradient.setLocalMatrix(gradientMatrix);
|
||||
canvas.drawRect(bounds, paint);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void setAlpha(int alpha){
|
||||
|
||||
}
|
||||
|
||||
@Override
|
||||
public void setColorFilter(@Nullable ColorFilter colorFilter){
|
||||
|
||||
}
|
||||
|
||||
@Override
|
||||
public int getOpacity(){
|
||||
return PixelFormat.TRANSLUCENT;
|
||||
}
|
||||
|
||||
public void setTopPadding(int topPadding){
|
||||
this.topPadding=topPadding;
|
||||
}
|
||||
|
||||
public void setTopOffset(int topOffset){
|
||||
this.topOffset=topOffset;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,48 @@
|
||||
package org.joinmastodon.android.ui.drawables;
|
||||
|
||||
import android.graphics.Canvas;
|
||||
import android.graphics.ColorFilter;
|
||||
import android.graphics.PixelFormat;
|
||||
import android.graphics.drawable.Drawable;
|
||||
|
||||
import androidx.annotation.NonNull;
|
||||
import androidx.annotation.Nullable;
|
||||
|
||||
public class EmptyDrawable extends Drawable{
|
||||
private final int width, height;
|
||||
|
||||
public EmptyDrawable(int width, int height){
|
||||
this.width=width;
|
||||
this.height=height;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void draw(@NonNull Canvas canvas){
|
||||
|
||||
}
|
||||
|
||||
@Override
|
||||
public void setAlpha(int alpha){
|
||||
|
||||
}
|
||||
|
||||
@Override
|
||||
public void setColorFilter(@Nullable ColorFilter colorFilter){
|
||||
|
||||
}
|
||||
|
||||
@Override
|
||||
public int getOpacity(){
|
||||
return PixelFormat.TRANSPARENT;
|
||||
}
|
||||
|
||||
@Override
|
||||
public int getIntrinsicWidth(){
|
||||
return width;
|
||||
}
|
||||
|
||||
@Override
|
||||
public int getIntrinsicHeight(){
|
||||
return height;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,74 @@
|
||||
package org.joinmastodon.android.ui.drawables;
|
||||
|
||||
import android.content.Context;
|
||||
import android.graphics.Canvas;
|
||||
import android.graphics.ColorFilter;
|
||||
import android.graphics.Matrix;
|
||||
import android.graphics.Paint;
|
||||
import android.graphics.Path;
|
||||
import android.graphics.PixelFormat;
|
||||
import android.graphics.Rect;
|
||||
import android.graphics.drawable.Drawable;
|
||||
|
||||
import androidx.annotation.NonNull;
|
||||
import androidx.annotation.Nullable;
|
||||
import me.grishka.appkit.utils.V;
|
||||
|
||||
public class PlayIconDrawable extends Drawable{
|
||||
private final Paint paint=new Paint(Paint.ANTI_ALIAS_FLAG);
|
||||
private final Path path=new Path();
|
||||
|
||||
public PlayIconDrawable(Context context){
|
||||
paint.setShadowLayer(V.dp(32), 0, 0, 0x80000000);
|
||||
paint.setColor(0xffffffff);
|
||||
path.moveTo(19.15f,32.5f);
|
||||
path.lineTo(32.5f,24.0f);
|
||||
path.lineTo(19.15f,15.5f);
|
||||
path.moveTo(24.0f,44.0f);
|
||||
path.quadTo(19.9f,44.0f,16.25f,42.42f);
|
||||
path.quadTo(12.6f,40.85f,9.88f,38.13f);
|
||||
path.quadTo(7.15f,35.4f,5.58f,31.75f);
|
||||
path.quadTo(4.0f,28.1f,4.0f,24.0f);
|
||||
path.quadTo(4.0f,19.85f,5.58f,16.2f);
|
||||
path.quadTo(7.15f,12.55f,9.88f,9.85f);
|
||||
path.quadTo(12.6f,7.15f,16.25f,5.58f);
|
||||
path.quadTo(19.9f,4.0f,24.0f,4.0f);
|
||||
path.quadTo(28.15f,4.0f,31.8f,5.58f);
|
||||
path.quadTo(35.45f,7.15f,38.15f,9.85f);
|
||||
path.quadTo(40.85f,12.55f,42.42f,16.2f);
|
||||
path.quadTo(44.0f,19.85f,44.0f,24.0f);
|
||||
path.quadTo(44.0f,28.1f,42.42f,31.75f);
|
||||
path.quadTo(40.85f,35.4f,38.15f,38.13f);
|
||||
path.quadTo(35.45f,40.85f,31.8f,42.42f);
|
||||
path.quadTo(28.15f,44.0f,24.0f,44.0f);
|
||||
|
||||
Matrix matrix=new Matrix();
|
||||
float density=context.getResources().getDisplayMetrics().density;
|
||||
matrix.postScale(density*1.3333f, density*1.3333f);
|
||||
path.transform(matrix);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void draw(@NonNull Canvas c){
|
||||
c.save();
|
||||
Rect bounds=getBounds();
|
||||
c.translate(bounds.width()/2f-V.dp(32), bounds.height()/2f-V.dp(32));
|
||||
c.drawPath(path, paint);
|
||||
c.restore();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void setAlpha(int alpha){
|
||||
|
||||
}
|
||||
|
||||
@Override
|
||||
public void setColorFilter(@Nullable ColorFilter colorFilter){
|
||||
|
||||
}
|
||||
|
||||
@Override
|
||||
public int getOpacity(){
|
||||
return PixelFormat.TRANSPARENT;
|
||||
}
|
||||
}
|
||||
@@ -61,9 +61,9 @@ public class SawtoothTearDrawable extends Drawable{
|
||||
}
|
||||
path.close();
|
||||
Paint paint=new Paint(Paint.ANTI_ALIAS_FLAG);
|
||||
paint.setColor(UiUtils.getThemeColor(context, R.attr.colorWindowBackground));
|
||||
paint.setColor(UiUtils.getThemeColor(context, R.attr.colorM3Surface));
|
||||
c.drawPath(path, paint);
|
||||
paint.setColor(UiUtils.getThemeColor(context, R.attr.colorPollVoted));
|
||||
paint.setColor(UiUtils.getThemeColor(context, R.attr.colorM3OutlineVariant));
|
||||
paint.setStrokeWidth(actualStrokeWidth);
|
||||
paint.setStyle(Paint.Style.STROKE);
|
||||
c.drawPath(path, paint);
|
||||
|
||||
@@ -1,76 +0,0 @@
|
||||
package org.joinmastodon.android.ui.drawables;
|
||||
|
||||
import android.content.Context;
|
||||
import android.graphics.Bitmap;
|
||||
import android.graphics.Canvas;
|
||||
import android.graphics.ColorFilter;
|
||||
import android.graphics.Paint;
|
||||
import android.graphics.PixelFormat;
|
||||
import android.graphics.drawable.Drawable;
|
||||
|
||||
import org.joinmastodon.android.R;
|
||||
import org.joinmastodon.android.ui.utils.UiUtils;
|
||||
|
||||
import androidx.annotation.NonNull;
|
||||
import androidx.annotation.Nullable;
|
||||
import me.grishka.appkit.utils.V;
|
||||
|
||||
public class SeekBarThumbDrawable extends Drawable{
|
||||
private Bitmap shadow1, shadow2;
|
||||
private Paint paint=new Paint(Paint.ANTI_ALIAS_FLAG);
|
||||
private Context context;
|
||||
|
||||
public SeekBarThumbDrawable(Context context){
|
||||
this.context=context;
|
||||
shadow1=Bitmap.createBitmap(V.dp(24), V.dp(24), Bitmap.Config.ALPHA_8);
|
||||
shadow2=Bitmap.createBitmap(V.dp(24), V.dp(24), Bitmap.Config.ALPHA_8);
|
||||
Paint paint=new Paint(Paint.ANTI_ALIAS_FLAG);
|
||||
paint.setColor(0xFF000000);
|
||||
paint.setShadowLayer(V.dp(2), 0, V.dp(1), 0xFF000000);
|
||||
new Canvas(shadow1).drawCircle(V.dp(12), V.dp(12), V.dp(9), paint);
|
||||
paint.setShadowLayer(V.dp(3), 0, V.dp(1), 0xFF000000);
|
||||
new Canvas(shadow2).drawCircle(V.dp(12), V.dp(12), V.dp(9), paint);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void draw(@NonNull Canvas canvas){
|
||||
float centerX=getBounds().centerX();
|
||||
float centerY=getBounds().centerY();
|
||||
paint.setStyle(Paint.Style.FILL);
|
||||
paint.setColor(0x4d000000);
|
||||
canvas.drawBitmap(shadow1, centerX-shadow1.getWidth()/2f, centerY-shadow1.getHeight()/2f, paint);
|
||||
paint.setColor(0x26000000);
|
||||
canvas.drawBitmap(shadow2, centerX-shadow2.getWidth()/2f, centerY-shadow2.getHeight()/2f, paint);
|
||||
paint.setColor(UiUtils.getThemeColor(context, R.attr.colorButtonText));
|
||||
canvas.drawCircle(centerX, centerY, V.dp(7), paint);
|
||||
paint.setStyle(Paint.Style.STROKE);
|
||||
paint.setColor(UiUtils.getThemeColor(context, R.attr.colorAccentLight));
|
||||
paint.setStrokeWidth(V.dp(4));
|
||||
canvas.drawCircle(centerX, centerY, V.dp(7), paint);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void setAlpha(int alpha){
|
||||
|
||||
}
|
||||
|
||||
@Override
|
||||
public void setColorFilter(@Nullable ColorFilter colorFilter){
|
||||
|
||||
}
|
||||
|
||||
@Override
|
||||
public int getOpacity(){
|
||||
return PixelFormat.TRANSLUCENT;
|
||||
}
|
||||
|
||||
@Override
|
||||
public int getIntrinsicWidth(){
|
||||
return V.dp(24);
|
||||
}
|
||||
|
||||
@Override
|
||||
public int getIntrinsicHeight(){
|
||||
return V.dp(24);
|
||||
}
|
||||
}
|
||||
@@ -11,14 +11,20 @@ import org.joinmastodon.android.MastodonApp;
|
||||
|
||||
import androidx.annotation.NonNull;
|
||||
import androidx.annotation.Nullable;
|
||||
import me.grishka.appkit.utils.V;
|
||||
|
||||
public class SpoilerStripesDrawable extends Drawable{
|
||||
private Paint paint=new Paint(Paint.ANTI_ALIAS_FLAG);
|
||||
private boolean flipped;
|
||||
|
||||
public SpoilerStripesDrawable(){
|
||||
private static final float X1=-0.860365f;
|
||||
private static final float X2=10.6078f;
|
||||
|
||||
public SpoilerStripesDrawable(boolean flipped){
|
||||
paint.setColor(0xff000000);
|
||||
paint.setStyle(Paint.Style.STROKE);
|
||||
paint.setStrokeWidth(3);
|
||||
this.flipped=flipped;
|
||||
}
|
||||
|
||||
@Override
|
||||
@@ -28,13 +34,15 @@ public class SpoilerStripesDrawable extends Drawable{
|
||||
canvas.translate(bounds.left, bounds.top);
|
||||
canvas.clipRect(0, 0, bounds.width(), bounds.height());
|
||||
float scale=MastodonApp.context.getResources().getDisplayMetrics().density;
|
||||
if(bounds.width()>V.dp(10))
|
||||
scale*=2;
|
||||
canvas.scale(scale, scale, 0, 0);
|
||||
|
||||
float height=bounds.height()/scale;
|
||||
float y1=6.80133f;
|
||||
float y2=-1.22874f;
|
||||
while(y2<height){
|
||||
canvas.drawLine(-0.860365f, y1, 10.6078f, y2, paint);
|
||||
canvas.drawLine(flipped ? X2 : X1, y1, flipped ? X1 : X2, y2, paint);
|
||||
y1+=8.03007f;
|
||||
y2+=8.03007f;
|
||||
}
|
||||
|
||||
@@ -0,0 +1,48 @@
|
||||
package org.joinmastodon.android.ui.drawables;
|
||||
|
||||
import android.graphics.Canvas;
|
||||
import android.graphics.ColorFilter;
|
||||
import android.graphics.Rect;
|
||||
import android.graphics.drawable.Drawable;
|
||||
|
||||
import androidx.annotation.NonNull;
|
||||
import androidx.annotation.Nullable;
|
||||
|
||||
public class TiledDrawable extends Drawable{
|
||||
private final Drawable drawable;
|
||||
|
||||
public TiledDrawable(Drawable drawable){
|
||||
this.drawable=drawable;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void draw(@NonNull Canvas canvas){
|
||||
Rect bounds=getBounds();
|
||||
canvas.save();
|
||||
canvas.clipRect(bounds);
|
||||
int w=drawable.getIntrinsicWidth();
|
||||
int h=drawable.getIntrinsicHeight();
|
||||
for(int y=bounds.top;y<bounds.bottom;y+=h){
|
||||
for(int x=bounds.left;x<bounds.right;x+=w){
|
||||
drawable.setBounds(x, y, x+w, y+h);
|
||||
drawable.draw(canvas);
|
||||
}
|
||||
}
|
||||
canvas.restore();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void setAlpha(int alpha){
|
||||
drawable.setAlpha(alpha);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void setColorFilter(@Nullable ColorFilter colorFilter){
|
||||
drawable.setColorFilter(colorFilter);
|
||||
}
|
||||
|
||||
@Override
|
||||
public int getOpacity(){
|
||||
return drawable.getOpacity();
|
||||
}
|
||||
}
|
||||
@@ -19,7 +19,6 @@ import android.media.AudioManager;
|
||||
import android.media.MediaPlayer;
|
||||
import android.media.MediaScannerConnection;
|
||||
import android.net.Uri;
|
||||
import android.opengl.Visibility;
|
||||
import android.os.Build;
|
||||
import android.os.Environment;
|
||||
import android.os.SystemClock;
|
||||
@@ -200,7 +199,7 @@ public class PhotoViewer implements ZoomPanView.Listener{
|
||||
videoSeekBar=uiOverlay.findViewById(R.id.seekbar);
|
||||
videoTimeView=uiOverlay.findViewById(R.id.time);
|
||||
videoPlayPauseButton=uiOverlay.findViewById(R.id.play_pause_btn);
|
||||
if(attachments.get(index).type!=Attachment.Type.VIDEO){
|
||||
if(attachments.get(index).type==Attachment.Type.IMAGE){
|
||||
videoControls.setVisibility(View.GONE);
|
||||
}else{
|
||||
videoDuration=(int)Math.round(attachments.get(index).getDuration()*1000);
|
||||
@@ -275,6 +274,10 @@ public class PhotoViewer implements ZoomPanView.Listener{
|
||||
});
|
||||
}
|
||||
|
||||
public void removeMenu(){
|
||||
toolbar.getMenu().clear();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onTransitionAnimationUpdate(float translateX, float translateY, float scale){
|
||||
listener.setTransitioningViewTransform(translateX, translateY, scale);
|
||||
@@ -333,7 +336,7 @@ public class PhotoViewer implements ZoomPanView.Listener{
|
||||
listener.setPhotoViewVisibility(pager.getCurrentItem(), true);
|
||||
if(!uiVisible){
|
||||
windowView.setSystemUiVisibility(windowView.getSystemUiVisibility() | View.SYSTEM_UI_FLAG_HIDE_NAVIGATION | View.SYSTEM_UI_FLAG_IMMERSIVE_STICKY | View.SYSTEM_UI_FLAG_FULLSCREEN);
|
||||
}else if(attachments.get(currentIndex).type==Attachment.Type.VIDEO){
|
||||
}else if(attachments.get(currentIndex).type!=Attachment.Type.IMAGE){
|
||||
hideUiDelayed();
|
||||
}
|
||||
}
|
||||
@@ -372,7 +375,7 @@ public class PhotoViewer implements ZoomPanView.Listener{
|
||||
.setInterpolator(CubicBezierInterpolator.DEFAULT)
|
||||
.start();
|
||||
windowView.setSystemUiVisibility(windowView.getSystemUiVisibility() & ~(View.SYSTEM_UI_FLAG_HIDE_NAVIGATION | View.SYSTEM_UI_FLAG_IMMERSIVE_STICKY | View.SYSTEM_UI_FLAG_FULLSCREEN));
|
||||
if(attachments.get(currentIndex).type==Attachment.Type.VIDEO)
|
||||
if(attachments.get(currentIndex).type!=Attachment.Type.IMAGE)
|
||||
hideUiDelayed(5000);
|
||||
}
|
||||
uiVisible=!uiVisible;
|
||||
@@ -391,8 +394,7 @@ public class PhotoViewer implements ZoomPanView.Listener{
|
||||
currentIndex=index;
|
||||
Attachment att=attachments.get(index);
|
||||
imageDescriptionButton.setVisible(att.description != null && !att.description.isEmpty());
|
||||
toolbar.invalidate();
|
||||
V.setVisibilityAnimated(videoControls, att.type==Attachment.Type.VIDEO ? View.VISIBLE : View.GONE);
|
||||
V.setVisibilityAnimated(videoControls, att.type!=Attachment.Type.IMAGE ? View.VISIBLE : View.GONE);
|
||||
if(att.type==Attachment.Type.VIDEO){
|
||||
videoSeekBar.setSecondaryProgress(0);
|
||||
videoDuration=(int)Math.round(att.getDuration()*1000);
|
||||
@@ -821,12 +823,13 @@ public class PhotoViewer implements ZoomPanView.Listener{
|
||||
if(item.type==Attachment.Type.VIDEO){
|
||||
incKeepScreenOn();
|
||||
keepingScreenOn=true;
|
||||
}
|
||||
if(getAbsoluteAdapterPosition()==currentIndex){
|
||||
player.start();
|
||||
startUpdatingVideoPosition(player);
|
||||
hideUiDelayed();
|
||||
}
|
||||
}else{
|
||||
if (item.type == Attachment.Type.GIFV) {
|
||||
keepingScreenOn=false;
|
||||
player.setLooping(true);
|
||||
player.start();
|
||||
@@ -846,7 +849,7 @@ public class PhotoViewer implements ZoomPanView.Listener{
|
||||
player.setOnPreparedListener(this);
|
||||
player.setOnErrorListener(this);
|
||||
player.setOnVideoSizeChangedListener(this);
|
||||
if(item.type==Attachment.Type.VIDEO){
|
||||
if(item.type!=Attachment.Type.IMAGE){
|
||||
player.setOnBufferingUpdateListener(this);
|
||||
player.setOnInfoListener(this);
|
||||
player.setOnSeekCompleteListener(this);
|
||||
|
||||
@@ -79,6 +79,7 @@ import androidx.recyclerview.widget.util.Pools;
|
||||
import androidx.viewpager.widget.PagerAdapter;
|
||||
import androidx.viewpager.widget.ViewPager;
|
||||
import me.grishka.appkit.utils.CubicBezierInterpolator;
|
||||
import me.grishka.appkit.utils.CustomViewHelper;
|
||||
import me.grishka.appkit.utils.V;
|
||||
|
||||
import java.lang.annotation.Retention;
|
||||
@@ -157,7 +158,7 @@ import java.util.Iterator;
|
||||
* @attr ref com.google.android.material.R.styleable#TabLayout_tabTextAppearance
|
||||
*/
|
||||
@ViewPager.DecorView
|
||||
public class TabLayout extends HorizontalScrollView {
|
||||
public class TabLayout extends HorizontalScrollView implements CustomViewHelper{
|
||||
|
||||
private static final CubicBezierInterpolator FAST_OUT_SLOW_IN_INTERPOLATOR=new CubicBezierInterpolator(.4f, 0f, .2f, 1f);
|
||||
private static final int DEF_STYLE_RES = R.style.Widget_Design_TabLayout;
|
||||
@@ -1657,7 +1658,7 @@ public class TabLayout extends HorizontalScrollView {
|
||||
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
|
||||
// If we have a MeasureSpec which allows us to decide our height, try and use the default
|
||||
// height
|
||||
final int idealHeight = Math.round(V.dp(getDefaultHeight()));
|
||||
final int idealHeight = Math.round(dp(getDefaultHeight()));
|
||||
switch (MeasureSpec.getMode(heightMeasureSpec)) {
|
||||
case MeasureSpec.AT_MOST:
|
||||
if (getChildCount() == 1 && MeasureSpec.getSize(heightMeasureSpec) >= idealHeight) {
|
||||
@@ -1680,7 +1681,7 @@ public class TabLayout extends HorizontalScrollView {
|
||||
tabMaxWidth =
|
||||
requestedTabMaxWidth > 0
|
||||
? requestedTabMaxWidth
|
||||
: (int) (specWidth - V.dp(TAB_MIN_WIDTH_MARGIN));
|
||||
: (int) (specWidth - dp(TAB_MIN_WIDTH_MARGIN));
|
||||
}
|
||||
|
||||
// Now super measure itself using the (possibly) modified height spec
|
||||
@@ -2842,7 +2843,7 @@ public class TabLayout extends HorizontalScrollView {
|
||||
int iconMargin = 0;
|
||||
if (hasText && iconView.getVisibility() == VISIBLE) {
|
||||
// If we're showing both text and icon, add some margin bottom to the icon
|
||||
iconMargin = (int) V.dp(DEFAULT_GAP_TEXT_ICON);
|
||||
iconMargin = (int) dp(DEFAULT_GAP_TEXT_ICON);
|
||||
}
|
||||
if (inlineLabel) {
|
||||
if (iconMargin != lp.getMarginEnd()) {
|
||||
@@ -3043,7 +3044,7 @@ public class TabLayout extends HorizontalScrollView {
|
||||
return;
|
||||
}
|
||||
|
||||
final int gutter = (int) V.dp(FIXED_WRAP_GUTTER_MIN);
|
||||
final int gutter = (int) dp(FIXED_WRAP_GUTTER_MIN);
|
||||
boolean remeasure = false;
|
||||
|
||||
if (largestTabWidth * count <= getMeasuredWidth() - gutter * 2) {
|
||||
|
||||
@@ -31,7 +31,10 @@ public class CustomEmojiSpan extends ReplacementSpan{
|
||||
public void draw(@NonNull Canvas canvas, CharSequence text, int start, int end, float x, int top, int y, int bottom, @NonNull Paint paint){
|
||||
int size=Math.round(paint.descent()-paint.ascent());
|
||||
if(drawable==null){
|
||||
canvas.drawRect(x, top, x+size, top+size, paint);
|
||||
int alpha=paint.getAlpha();
|
||||
paint.setAlpha(alpha >> 1);
|
||||
canvas.drawRoundRect(x, top, x+size, top+size, V.dp(2), V.dp(2), paint);
|
||||
paint.setAlpha(alpha);
|
||||
}else{
|
||||
// AnimatedImageDrawable doesn't like when its bounds don't start at (0, 0)
|
||||
Rect bounds=drawable.getBounds();
|
||||
|
||||
@@ -1,10 +1,13 @@
|
||||
package org.joinmastodon.android.ui.text;
|
||||
|
||||
import android.content.Context;
|
||||
import android.graphics.Typeface;
|
||||
import android.text.SpannableStringBuilder;
|
||||
import android.text.Spanned;
|
||||
import android.text.TextUtils;
|
||||
import android.text.style.BackgroundColorSpan;
|
||||
import android.text.style.BulletSpan;
|
||||
import android.text.style.ForegroundColorSpan;
|
||||
import android.text.style.LeadingMarginSpan;
|
||||
import android.text.style.RelativeSizeSpan;
|
||||
import android.text.style.StrikethroughSpan;
|
||||
@@ -17,14 +20,18 @@ import android.widget.TextView;
|
||||
|
||||
import com.twitter.twittertext.Regex;
|
||||
|
||||
import org.joinmastodon.android.R;
|
||||
import org.joinmastodon.android.model.Emoji;
|
||||
import org.joinmastodon.android.model.FilterResult;
|
||||
import org.joinmastodon.android.model.Hashtag;
|
||||
import org.joinmastodon.android.model.Mention;
|
||||
import org.joinmastodon.android.ui.utils.UiUtils;
|
||||
import org.jsoup.Jsoup;
|
||||
import org.jsoup.nodes.Document;
|
||||
import org.jsoup.nodes.Element;
|
||||
import org.jsoup.nodes.Node;
|
||||
import org.jsoup.nodes.TextNode;
|
||||
import org.jsoup.safety.Cleaner;
|
||||
import org.jsoup.safety.Safelist;
|
||||
import org.jsoup.select.NodeVisitor;
|
||||
|
||||
@@ -129,7 +136,7 @@ public class HtmlParser{
|
||||
}else{
|
||||
linkType=LinkSpan.Type.URL;
|
||||
}
|
||||
openSpans.add(new SpanInfo(new LinkSpan(href, null, linkType, accountID, text), ssb.length(), el));
|
||||
openSpans.add(new SpanInfo(new LinkSpan(href, null, linkType, accountID), ssb.length(), el));
|
||||
}
|
||||
case "br" -> ssb.append('\n');
|
||||
case "span" -> {
|
||||
@@ -244,6 +251,13 @@ public class HtmlParser{
|
||||
return Jsoup.clean(html, Safelist.none());
|
||||
}
|
||||
|
||||
public static String stripAndRemoveInvisibleSpans(String html){
|
||||
Document doc=Jsoup.parseBodyFragment(html);
|
||||
doc.body().select("span.invisible").remove();
|
||||
Cleaner cleaner=new Cleaner(Safelist.none());
|
||||
return cleaner.clean(doc).body().html();
|
||||
}
|
||||
|
||||
public static String text(String html) {
|
||||
return Jsoup.parse(html).body().wholeText();
|
||||
}
|
||||
@@ -257,8 +271,27 @@ public class HtmlParser{
|
||||
String url=matcher.group(3);
|
||||
if(TextUtils.isEmpty(matcher.group(4)))
|
||||
url="http://"+url;
|
||||
ssb.setSpan(new LinkSpan(url, null, LinkSpan.Type.URL, null, url), matcher.start(3), matcher.end(3), 0);
|
||||
ssb.setSpan(new LinkSpan(url, null, LinkSpan.Type.URL, null), matcher.start(3), matcher.end(3), 0);
|
||||
}while(matcher.find()); // Find more URLs
|
||||
return ssb;
|
||||
}
|
||||
|
||||
public static void applyFilterHighlights(Context context, SpannableStringBuilder text, List<FilterResult> filters){
|
||||
if (filters == null) return;
|
||||
int fgColor=UiUtils.getThemeColor(context, R.attr.colorM3Error);
|
||||
int bgColor=UiUtils.getThemeColor(context, R.attr.colorM3ErrorContainer);
|
||||
for(FilterResult filter:filters){
|
||||
if(!filter.filter.isActive())
|
||||
continue;;
|
||||
for(String word:filter.keywordMatches){
|
||||
Matcher matcher=Pattern.compile("\\b"+Pattern.quote(word)+"\\b", Pattern.CASE_INSENSITIVE).matcher(text);
|
||||
while(matcher.find()){
|
||||
ForegroundColorSpan fg=new ForegroundColorSpan(fgColor);
|
||||
BackgroundColorSpan bg=new BackgroundColorSpan(bgColor);
|
||||
text.setSpan(bg, matcher.start(), matcher.end(), 0);
|
||||
text.setSpan(fg, matcher.start(), matcher.end(), 0);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,67 @@
|
||||
package org.joinmastodon.android.ui.text;
|
||||
|
||||
import android.content.Context;
|
||||
import android.text.Editable;
|
||||
import android.text.TextWatcher;
|
||||
import android.text.style.BackgroundColorSpan;
|
||||
import android.text.style.ForegroundColorSpan;
|
||||
|
||||
import org.joinmastodon.android.R;
|
||||
import org.joinmastodon.android.ui.utils.UiUtils;
|
||||
|
||||
public class LengthLimitHighlighter implements TextWatcher{
|
||||
private final Context context;
|
||||
private final int lengthLimit;
|
||||
private BackgroundColorSpan overLimitBG;
|
||||
private ForegroundColorSpan overLimitFG;
|
||||
private boolean isOverLimit;
|
||||
private OverLimitChangeListener listener;
|
||||
|
||||
public LengthLimitHighlighter(Context context, int lengthLimit){
|
||||
this.context=context;
|
||||
overLimitBG=new BackgroundColorSpan(UiUtils.getThemeColor(context, R.attr.colorM3ErrorContainer));
|
||||
overLimitFG=new ForegroundColorSpan(UiUtils.getThemeColor(context, R.attr.colorM3Error));
|
||||
this.lengthLimit=lengthLimit;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void beforeTextChanged(CharSequence s, int start, int count, int after){
|
||||
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onTextChanged(CharSequence s, int start, int before, int count){
|
||||
|
||||
}
|
||||
|
||||
@Override
|
||||
public void afterTextChanged(Editable s){
|
||||
s.removeSpan(overLimitBG);
|
||||
s.removeSpan(overLimitFG);
|
||||
boolean newOverLimit=s.length()>lengthLimit;
|
||||
if(newOverLimit){
|
||||
int start=s.length()-(s.length()-lengthLimit);
|
||||
int end=s.length();
|
||||
s.setSpan(overLimitFG, start, end, 0);
|
||||
s.setSpan(overLimitBG, start, end, 0);
|
||||
}
|
||||
if(newOverLimit!=isOverLimit){
|
||||
isOverLimit=newOverLimit;
|
||||
if(listener!=null)
|
||||
listener.onOverLimitChanged(isOverLimit);
|
||||
}
|
||||
}
|
||||
|
||||
public LengthLimitHighlighter setListener(OverLimitChangeListener listener){
|
||||
this.listener=listener;
|
||||
return this;
|
||||
}
|
||||
|
||||
public boolean isOverLimit(){
|
||||
return isOverLimit;
|
||||
}
|
||||
|
||||
public interface OverLimitChangeListener{
|
||||
void onOverLimitChanged(boolean isOverLimit);
|
||||
}
|
||||
}
|
||||
@@ -35,6 +35,7 @@ public class LinkSpan extends CharacterStyle {
|
||||
@Override
|
||||
public void updateDrawState(TextPaint tp) {
|
||||
tp.setColor(color=tp.linkColor);
|
||||
tp.setUnderlineText(true);
|
||||
}
|
||||
|
||||
public void onClick(Context context){
|
||||
|
||||
@@ -1,2 +0,0 @@
|
||||
package org.joinmastodon.android.ui.text;public class TagEditText {
|
||||
}
|
||||
@@ -50,4 +50,10 @@ public class BlurHashDrawable extends Drawable{
|
||||
public int getIntrinsicHeight(){
|
||||
return height;
|
||||
}
|
||||
|
||||
@NonNull
|
||||
@Override
|
||||
public Drawable mutate(){
|
||||
return new BlurHashDrawable(bitmap, width, height);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -55,12 +55,18 @@ public class ColorPalette {
|
||||
}
|
||||
|
||||
Resources.Theme t = context.getTheme();
|
||||
t.applyStyle(R.style.ColorPalette_Fallback, true);
|
||||
if (base != 0) t.applyStyle(base, true);
|
||||
if (light != 0 && theme.equals(ThemePreference.LIGHT)) t.applyStyle(light, true);
|
||||
else if (theme.equals(ThemePreference.DARK)) {
|
||||
if (light != 0 && theme.equals(ThemePreference.LIGHT)) {
|
||||
t.applyStyle(light, true);
|
||||
} else if (theme.equals(ThemePreference.DARK)) {
|
||||
t.applyStyle(R.style.ColorPalette_Dark, true);
|
||||
if (trueBlackTheme) t.applyStyle(R.style.ColorPalette_Dark_TrueBlack, true);
|
||||
if (dark != 0 && !trueBlackTheme) t.applyStyle(dark, true);
|
||||
else if (black != 0 && trueBlackTheme) t.applyStyle(black, true);
|
||||
} else if (theme.equals(ThemePreference.AUTO)) {
|
||||
t.applyStyle(R.style.ColorPalette_AutoLightDark, true);
|
||||
if (trueBlackTheme) t.applyStyle(R.style.ColorPalette_AutoLightDark_TrueBlack, true);
|
||||
if (autoDark != 0 && !trueBlackTheme) t.applyStyle(autoDark, true);
|
||||
else if (autoBlack != 0 && trueBlackTheme) t.applyStyle(autoBlack, true);
|
||||
}
|
||||
|
||||
@@ -4,67 +4,86 @@ import android.app.Activity;
|
||||
import android.content.Context;
|
||||
import android.content.SharedPreferences;
|
||||
import android.view.View;
|
||||
import android.view.ViewGroup;
|
||||
import android.widget.FrameLayout;
|
||||
import android.widget.ImageView;
|
||||
import android.widget.TextView;
|
||||
|
||||
import org.joinmastodon.android.MastodonApp;
|
||||
import org.joinmastodon.android.R;
|
||||
import org.joinmastodon.android.api.session.AccountSessionManager;
|
||||
|
||||
import me.grishka.appkit.utils.CubicBezierInterpolator;
|
||||
import java.util.EnumSet;
|
||||
|
||||
import androidx.recyclerview.widget.RecyclerView;
|
||||
import me.grishka.appkit.utils.MergeRecyclerAdapter;
|
||||
import me.grishka.appkit.utils.SingleViewRecyclerAdapter;
|
||||
|
||||
public class DiscoverInfoBannerHelper{
|
||||
private View banner;
|
||||
private final BannerType type;
|
||||
private final String accountID;
|
||||
private static EnumSet<BannerType> bannerTypesToShow=EnumSet.noneOf(BannerType.class);
|
||||
|
||||
public DiscoverInfoBannerHelper(BannerType type){
|
||||
this.type=type;
|
||||
}
|
||||
|
||||
private SharedPreferences getPrefs(){
|
||||
return MastodonApp.context.getSharedPreferences("onboarding", Context.MODE_PRIVATE);
|
||||
}
|
||||
|
||||
public void maybeAddBanner(FrameLayout view){
|
||||
if(!getPrefs().getBoolean("bannerHidden_"+type, false)){
|
||||
((Activity)view.getContext()).getLayoutInflater().inflate(R.layout.discover_info_banner, view);
|
||||
banner=view.findViewById(R.id.discover_info_banner);
|
||||
view.findViewById(R.id.banner_dismiss).setOnClickListener(this::onDismissClick);
|
||||
TextView text=view.findViewById(R.id.banner_text);
|
||||
text.setText(switch(type){
|
||||
case TRENDING_POSTS -> R.string.trending_posts_info_banner;
|
||||
case TRENDING_HASHTAGS -> R.string.trending_hashtags_info_banner;
|
||||
case TRENDING_LINKS -> R.string.trending_links_info_banner;
|
||||
case LOCAL_TIMELINE -> R.string.local_timeline_info_banner;
|
||||
case FEDERATED_TIMELINE -> R.string.sk_federated_timeline_info_banner;
|
||||
case POST_NOTIFICATIONS -> R.string.sk_notify_posts_info_banner;
|
||||
case BUBBLE_TIMELINE -> R.string.sk_bubble_timeline_info_banner;
|
||||
});
|
||||
static{
|
||||
for(BannerType t:BannerType.values()){
|
||||
if(!getPrefs().getBoolean("bannerHidden_"+t, false))
|
||||
bannerTypesToShow.add(t);
|
||||
}
|
||||
}
|
||||
|
||||
private void onDismissClick(View v){
|
||||
if(banner==null)
|
||||
return;
|
||||
View _banner=banner;
|
||||
banner.animate()
|
||||
.alpha(0)
|
||||
.setDuration(200)
|
||||
.setInterpolator(CubicBezierInterpolator.DEFAULT)
|
||||
.withEndAction(()->((ViewGroup)_banner.getParent()).removeView(_banner))
|
||||
.start();
|
||||
public DiscoverInfoBannerHelper(BannerType type, String accountID){
|
||||
this.type=type;
|
||||
this.accountID=accountID;
|
||||
}
|
||||
|
||||
private static SharedPreferences getPrefs(){
|
||||
return MastodonApp.context.getSharedPreferences("onboarding", Context.MODE_PRIVATE);
|
||||
}
|
||||
|
||||
public void maybeAddBanner(RecyclerView list, MergeRecyclerAdapter adapter){
|
||||
if(bannerTypesToShow.contains(type)){
|
||||
banner=((Activity)list.getContext()).getLayoutInflater().inflate(R.layout.discover_info_banner, list, false);
|
||||
TextView text=banner.findViewById(R.id.banner_text);
|
||||
text.setText(switch(type){
|
||||
case TRENDING_POSTS -> list.getResources().getString(R.string.trending_posts_info_banner);
|
||||
case TRENDING_LINKS -> list.getResources().getString(R.string.trending_links_info_banner);
|
||||
case FEDERATED_TIMELINE -> list.getResources().getString(R.string.sk_federated_timeline_info_banner);
|
||||
case POST_NOTIFICATIONS -> list.getResources().getString(R.string.sk_notify_posts_info_banner);
|
||||
case BUBBLE_TIMELINE -> list.getResources().getString(R.string.sk_bubble_timeline_info_banner);
|
||||
case LOCAL_TIMELINE -> list.getResources().getString(R.string.local_timeline_info_banner, AccountSessionManager.get(accountID).domain);
|
||||
case ACCOUNTS -> list.getResources().getString(R.string.recommended_accounts_info_banner);
|
||||
});
|
||||
ImageView icon=banner.findViewById(R.id.icon);
|
||||
icon.setImageResource(switch(type){
|
||||
case TRENDING_POSTS -> R.drawable.ic_fluent_arrow_trending_24_regular;
|
||||
case TRENDING_LINKS -> R.drawable.ic_fluent_news_24_regular;
|
||||
case ACCOUNTS -> R.drawable.ic_fluent_people_add_24_regular;
|
||||
// no icon because those are displayed as timelines - with icon in top left
|
||||
case LOCAL_TIMELINE, FEDERATED_TIMELINE, BUBBLE_TIMELINE, POST_NOTIFICATIONS -> 0;
|
||||
});
|
||||
adapter.addAdapter(new SingleViewRecyclerAdapter(banner));
|
||||
}
|
||||
}
|
||||
|
||||
public void onBannerBecameVisible(){
|
||||
getPrefs().edit().putBoolean("bannerHidden_"+type, true).apply();
|
||||
banner=null;
|
||||
// bannerTypesToShow is not updated here on purpose so the banner keeps showing until the app is relaunched
|
||||
}
|
||||
|
||||
public static void reset(){
|
||||
SharedPreferences prefs=getPrefs();
|
||||
SharedPreferences.Editor e=prefs.edit();
|
||||
prefs.getAll().keySet().stream().filter(k->k.startsWith("bannerHidden_")).forEach(e::remove);
|
||||
e.apply();
|
||||
bannerTypesToShow=EnumSet.allOf(BannerType.class);
|
||||
}
|
||||
|
||||
public enum BannerType{
|
||||
TRENDING_POSTS,
|
||||
TRENDING_HASHTAGS,
|
||||
TRENDING_LINKS,
|
||||
LOCAL_TIMELINE,
|
||||
FEDERATED_TIMELINE,
|
||||
POST_NOTIFICATIONS,
|
||||
// ACCOUNTS,
|
||||
ACCOUNTS,
|
||||
BUBBLE_TIMELINE
|
||||
}
|
||||
}
|
||||
|
||||
@@ -27,8 +27,8 @@ public class InsetStatusItemDecoration extends RecyclerView.ItemDecoration{
|
||||
|
||||
public InsetStatusItemDecoration(BaseStatusListFragment<?> listFragment){
|
||||
this.listFragment=listFragment;
|
||||
bgColor=UiUtils.getThemeColor(listFragment.getActivity(), android.R.attr.colorBackground);
|
||||
borderColor=UiUtils.getThemeColor(listFragment.getActivity(), R.attr.colorPollVoted);
|
||||
bgColor=UiUtils.getThemeColor(listFragment.getActivity(), R.attr.colorM3SurfaceVariant);
|
||||
borderColor=UiUtils.getThemeColor(listFragment.getActivity(), R.attr.colorM3OutlineVariant);
|
||||
}
|
||||
|
||||
@Override
|
||||
@@ -94,10 +94,14 @@ public class InsetStatusItemDecoration extends RecyclerView.ItemDecoration{
|
||||
outRect.left=pad;
|
||||
if(insetRight)
|
||||
outRect.right=pad;
|
||||
if(!topSiblingInset)
|
||||
outRect.top=pad;
|
||||
if(!bottomSiblingInset)
|
||||
outRect.bottom=pad;
|
||||
|
||||
// had to comment this out because animations with offsets aren't handled properly.
|
||||
// can be worked around by manually applying top margins to items
|
||||
// see InsetDummyStatusDisplayItem#onBinds
|
||||
// if(!topSiblingInset)
|
||||
// outRect.top=pad;
|
||||
// if(!bottomSiblingInset)
|
||||
// outRect.bottom=pad;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -2,10 +2,12 @@ package org.joinmastodon.android.ui.utils;
|
||||
|
||||
import android.content.Context;
|
||||
import android.graphics.drawable.Drawable;
|
||||
import android.os.Build;
|
||||
import android.text.TextUtils;
|
||||
import android.view.LayoutInflater;
|
||||
import android.view.View;
|
||||
import android.widget.ImageView;
|
||||
import android.widget.TextView;
|
||||
|
||||
import org.joinmastodon.android.GlobalUserPreferences;
|
||||
import org.joinmastodon.android.R;
|
||||
@@ -13,12 +15,15 @@ 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;
|
||||
import org.joinmastodon.android.ui.drawables.PlayIconDrawable;
|
||||
|
||||
public class MediaAttachmentViewController{
|
||||
public final View view;
|
||||
public final MediaGridStatusDisplayItem.GridItemType type;
|
||||
public final ImageView photo;
|
||||
public final View altButton, noAltButton, btnsWrap;
|
||||
public final View altButton, noAltButton, btnsWrap, extraBadge;
|
||||
public final TextView duration;
|
||||
public final View playButton;
|
||||
private BlurhashCrossfadeDrawable crossfadeDrawable=new BlurhashCrossfadeDrawable();
|
||||
private final Context context;
|
||||
private boolean didClear;
|
||||
@@ -34,15 +39,24 @@ public class MediaAttachmentViewController{
|
||||
altButton=view.findViewById(R.id.alt_button);
|
||||
noAltButton=view.findViewById(R.id.no_alt_button);
|
||||
btnsWrap=view.findViewById(R.id.alt_badges);
|
||||
duration=view.findViewById(R.id.duration);
|
||||
playButton=view.findViewById(R.id.play_button);
|
||||
extraBadge=view.findViewById(R.id.extra_badge);
|
||||
this.type=type;
|
||||
this.context=context;
|
||||
if(playButton!=null){
|
||||
// https://developer.android.com/topic/performance/hardware-accel#drawing-support
|
||||
if(Build.VERSION.SDK_INT<28)
|
||||
playButton.setLayerType(View.LAYER_TYPE_SOFTWARE, null);
|
||||
playButton.setBackground(new PlayIconDrawable(context));
|
||||
}
|
||||
}
|
||||
|
||||
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);
|
||||
crossfadeDrawable.setCrossfadeAlpha(0f);
|
||||
photo.setImageDrawable(null);
|
||||
photo.setImageDrawable(crossfadeDrawable);
|
||||
boolean hasAltText = !TextUtils.isEmpty(attachment.description);
|
||||
@@ -52,12 +66,15 @@ public class MediaAttachmentViewController{
|
||||
altButton.setVisibility(hasAltText && GlobalUserPreferences.showAltIndicator ? View.VISIBLE : View.GONE);
|
||||
noAltButton.setVisibility(!hasAltText && GlobalUserPreferences.showNoAltIndicator ? View.VISIBLE : View.GONE);
|
||||
}
|
||||
if(type==MediaGridStatusDisplayItem.GridItemType.VIDEO){
|
||||
duration.setText(UiUtils.formatMediaDuration((int)attachment.getDuration()));
|
||||
}
|
||||
didClear=false;
|
||||
}
|
||||
|
||||
public void setImage(Drawable drawable){
|
||||
crossfadeDrawable.setImageDrawable(drawable);
|
||||
if(didClear && status.spoilerRevealed)
|
||||
if(didClear)
|
||||
crossfadeDrawable.animateAlpha(0f);
|
||||
}
|
||||
|
||||
@@ -66,8 +83,4 @@ public class MediaAttachmentViewController{
|
||||
crossfadeDrawable.setImageDrawable(null);
|
||||
didClear=true;
|
||||
}
|
||||
|
||||
public void setRevealed(boolean revealed){
|
||||
crossfadeDrawable.animateAlpha(revealed ? 0f : 1f);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -37,6 +37,12 @@ import android.provider.OpenableColumns;
|
||||
import android.text.SpannableStringBuilder;
|
||||
import android.text.Spanned;
|
||||
import android.text.TextUtils;
|
||||
import android.text.style.TypefaceSpan;
|
||||
import android.transition.ChangeBounds;
|
||||
import android.transition.ChangeScroll;
|
||||
import android.transition.Fade;
|
||||
import android.transition.TransitionManager;
|
||||
import android.transition.TransitionSet;
|
||||
import android.util.Log;
|
||||
import android.util.Pair;
|
||||
import android.view.HapticFeedbackConstants;
|
||||
@@ -44,6 +50,8 @@ import android.view.Menu;
|
||||
import android.view.MenuItem;
|
||||
import android.view.SubMenu;
|
||||
import android.view.View;
|
||||
import android.view.ViewGroup;
|
||||
import android.view.WindowInsets;
|
||||
import android.webkit.MimeTypeMap;
|
||||
import android.widget.Button;
|
||||
import android.widget.ImageView;
|
||||
@@ -86,6 +94,7 @@ import org.joinmastodon.android.fragments.HashtagTimelineFragment;
|
||||
import org.joinmastodon.android.fragments.ProfileFragment;
|
||||
import org.joinmastodon.android.fragments.ThreadFragment;
|
||||
import org.joinmastodon.android.model.Account;
|
||||
import org.joinmastodon.android.model.AccountField;
|
||||
import org.joinmastodon.android.model.Emoji;
|
||||
import org.joinmastodon.android.model.Instance;
|
||||
import org.joinmastodon.android.model.Notification;
|
||||
@@ -97,6 +106,7 @@ import org.joinmastodon.android.model.Status;
|
||||
import org.joinmastodon.android.model.StatusPrivacy;
|
||||
import org.joinmastodon.android.ui.M3AlertDialogBuilder;
|
||||
import org.joinmastodon.android.ui.text.CustomEmojiSpan;
|
||||
import org.joinmastodon.android.ui.text.HtmlParser;
|
||||
import org.parceler.Parcels;
|
||||
|
||||
import java.io.File;
|
||||
@@ -106,20 +116,29 @@ import java.net.IDN;
|
||||
import java.net.URI;
|
||||
import java.net.URISyntaxException;
|
||||
import java.time.Instant;
|
||||
import java.time.LocalDate;
|
||||
import java.time.ZoneId;
|
||||
import java.time.ZonedDateTime;
|
||||
import java.time.format.DateTimeFormatter;
|
||||
import java.time.format.FormatStyle;
|
||||
import java.util.ArrayList;
|
||||
import java.util.Arrays;
|
||||
import java.util.Collections;
|
||||
import java.util.Comparator;
|
||||
import java.util.HashMap;
|
||||
import java.util.List;
|
||||
import java.util.Locale;
|
||||
import java.util.Map;
|
||||
import java.util.Objects;
|
||||
import java.util.Optional;
|
||||
import java.util.function.BiConsumer;
|
||||
import java.util.function.BiPredicate;
|
||||
import java.util.function.Consumer;
|
||||
import java.util.function.Function;
|
||||
import java.util.function.Predicate;
|
||||
import java.util.function.ToIntFunction;
|
||||
import java.util.regex.Matcher;
|
||||
import java.util.regex.Pattern;
|
||||
import java.util.stream.Collectors;
|
||||
|
||||
import androidx.annotation.AttrRes;
|
||||
@@ -137,6 +156,7 @@ import me.grishka.appkit.api.Callback;
|
||||
import me.grishka.appkit.api.ErrorResponse;
|
||||
import me.grishka.appkit.imageloader.ViewImageLoader;
|
||||
import me.grishka.appkit.imageloader.requests.UrlImageLoaderRequest;
|
||||
import me.grishka.appkit.utils.CubicBezierInterpolator;
|
||||
import me.grishka.appkit.utils.V;
|
||||
import okhttp3.MediaType;
|
||||
|
||||
@@ -144,6 +164,7 @@ public class UiUtils {
|
||||
private static Handler mainHandler = new Handler(Looper.getMainLooper());
|
||||
private static final DateTimeFormatter DATE_FORMATTER_SHORT_WITH_YEAR = DateTimeFormatter.ofPattern("d MMM uuuu"), DATE_FORMATTER_SHORT = DateTimeFormatter.ofPattern("d MMM");
|
||||
public static final DateTimeFormatter DATE_TIME_FORMATTER = DateTimeFormatter.ofLocalizedDateTime(FormatStyle.LONG, FormatStyle.SHORT);
|
||||
private static final DateTimeFormatter TIME_FORMATTER=DateTimeFormatter.ofLocalizedTime(FormatStyle.SHORT);
|
||||
public static int MAX_WIDTH, SCROLL_TO_TOP_DELTA;
|
||||
|
||||
private UiUtils() {
|
||||
@@ -168,14 +189,14 @@ public class UiUtils {
|
||||
long t = instant.toEpochMilli();
|
||||
long now = System.currentTimeMillis();
|
||||
long diff = now - t;
|
||||
if (diff < 1000L) {
|
||||
if(diff<1000L){
|
||||
return context.getString(R.string.time_now);
|
||||
} else if (diff < 60_000L) {
|
||||
return context.getString(R.string.time_seconds, diff / 1000L);
|
||||
} else if (diff < 3600_000L) {
|
||||
return context.getString(R.string.time_minutes, diff / 60_000L);
|
||||
} else if (diff < 3600_000L * 24L) {
|
||||
return context.getString(R.string.time_hours, diff / 3600_000L);
|
||||
}else if(diff<60_000L){
|
||||
return context.getString(R.string.time_seconds_ago_short, diff/1000L);
|
||||
}else if(diff<3600_000L){
|
||||
return context.getString(R.string.time_minutes_ago_short, diff/60_000L);
|
||||
}else if(diff<3600_000L*24L){
|
||||
return context.getString(R.string.time_hours_ago_short, diff/3600_000L);
|
||||
} else {
|
||||
int days = (int) (diff / (3600_000L * 24L));
|
||||
if (days > 30) {
|
||||
@@ -186,25 +207,56 @@ public class UiUtils {
|
||||
return DATE_FORMATTER_SHORT_WITH_YEAR.format(dt);
|
||||
}
|
||||
}
|
||||
return context.getString(R.string.time_days, days);
|
||||
return context.getString(R.string.time_days_ago_short, days);
|
||||
}
|
||||
}
|
||||
|
||||
public static String formatRelativeTimestampAsMinutesAgo(Context context, Instant instant) {
|
||||
long t = instant.toEpochMilli();
|
||||
long now = System.currentTimeMillis();
|
||||
long diff = now - t;
|
||||
if (diff < 1000L) {
|
||||
public static String formatRelativeTimestampAsMinutesAgo(Context context, Instant instant, boolean relativeHours){
|
||||
long t=instant.toEpochMilli();
|
||||
long diff=System.currentTimeMillis()-t;
|
||||
if(diff<1000L && diff>-1000L){
|
||||
return context.getString(R.string.time_just_now);
|
||||
} else if (diff < 60_000L) {
|
||||
int secs = (int) (diff / 1000L);
|
||||
return context.getResources().getQuantityString(R.plurals.x_seconds_ago, secs, secs);
|
||||
} else if (diff < 3600_000L) {
|
||||
int mins = (int) (diff / 60_000L);
|
||||
return context.getResources().getQuantityString(R.plurals.x_minutes_ago, mins, mins);
|
||||
} else {
|
||||
return DATE_TIME_FORMATTER.format(instant.atZone(ZoneId.systemDefault()));
|
||||
}else if(diff>0){
|
||||
if(diff<60_000L){
|
||||
int secs=(int)(diff/1000L);
|
||||
return context.getResources().getQuantityString(R.plurals.x_seconds_ago, secs, secs);
|
||||
}else if(diff<3600_000L){
|
||||
int mins=(int)(diff/60_000L);
|
||||
return context.getResources().getQuantityString(R.plurals.x_minutes_ago, mins, mins);
|
||||
}else if(relativeHours && diff<24*3600_000L){
|
||||
int hours=(int)(diff/3600_000L);
|
||||
return context.getResources().getQuantityString(R.plurals.x_hours_ago, hours, hours);
|
||||
}
|
||||
}else{
|
||||
if(diff>-60_000L){
|
||||
int secs=-(int)(diff/1000L);
|
||||
return context.getResources().getQuantityString(R.plurals.in_x_seconds, secs, secs);
|
||||
}else if(diff>-3600_000L){
|
||||
int mins=-(int)(diff/60_000L);
|
||||
return context.getResources().getQuantityString(R.plurals.in_x_minutes, mins, mins);
|
||||
}else if(relativeHours && diff>-24*3600_000L){
|
||||
int hours=-(int)(diff/3600_000L);
|
||||
return context.getResources().getQuantityString(R.plurals.in_x_hours, hours, hours);
|
||||
}
|
||||
}
|
||||
ZonedDateTime dt=instant.atZone(ZoneId.systemDefault());
|
||||
ZonedDateTime now=ZonedDateTime.now();
|
||||
String formattedTime=TIME_FORMATTER.format(dt);
|
||||
String formattedDate;
|
||||
LocalDate today=now.toLocalDate();
|
||||
LocalDate date=dt.toLocalDate();
|
||||
if(date.equals(today)){
|
||||
formattedDate=context.getString(R.string.today);
|
||||
}else if(date.equals(today.minusDays(1))){
|
||||
formattedDate=context.getString(R.string.yesterday);
|
||||
}else if(date.equals(today.plusDays(1))){
|
||||
formattedDate=context.getString(R.string.tomorrow);
|
||||
}else if(date.getYear()==today.getYear()){
|
||||
formattedDate=DATE_FORMATTER_SHORT.format(dt);
|
||||
}else{
|
||||
formattedDate=DATE_FORMATTER_SHORT_WITH_YEAR.format(dt);
|
||||
}
|
||||
return context.getString(R.string.date_at_time, formattedDate, formattedTime);
|
||||
}
|
||||
|
||||
public static String formatTimeLeft(Context context, Instant instant) {
|
||||
@@ -381,7 +433,7 @@ public class UiUtils {
|
||||
}
|
||||
|
||||
public static void showConfirmationAlert(Context context, @StringRes int title, @StringRes int message, @StringRes int confirmButton, @DrawableRes int icon, Runnable onConfirmed) {
|
||||
showConfirmationAlert(context, context.getString(title), context.getString(message), context.getString(confirmButton), icon, onConfirmed);
|
||||
showConfirmationAlert(context, context.getString(title), message==0 ? null : context.getString(message), context.getString(confirmButton), icon, onConfirmed);
|
||||
}
|
||||
|
||||
public static void showConfirmationAlert(Context context, CharSequence title, CharSequence message, CharSequence confirmButton, int icon, Runnable onConfirmed) {
|
||||
@@ -626,49 +678,6 @@ public class UiUtils {
|
||||
.exec(accountID));
|
||||
}
|
||||
|
||||
public static void setRelationshipToActionButton(Relationship relationship, Button button) {
|
||||
setRelationshipToActionButton(relationship, button, false);
|
||||
}
|
||||
|
||||
public static void setRelationshipToActionButton(Relationship relationship, Button button, boolean keepText) {
|
||||
CharSequence textBefore = keepText ? button.getText() : null;
|
||||
boolean secondaryStyle;
|
||||
if (relationship.blocking) {
|
||||
button.setText(R.string.button_blocked);
|
||||
secondaryStyle = true;
|
||||
// } else if (relationship.blockedBy) {
|
||||
// button.setText(R.string.button_follow);
|
||||
// secondaryStyle = false;
|
||||
} else if (relationship.requested) {
|
||||
button.setText(R.string.button_follow_pending);
|
||||
secondaryStyle = true;
|
||||
} else if (!relationship.following) {
|
||||
button.setText(relationship.followedBy ? R.string.follow_back : R.string.button_follow);
|
||||
secondaryStyle = false;
|
||||
} else {
|
||||
button.setText(R.string.button_following);
|
||||
secondaryStyle = true;
|
||||
}
|
||||
|
||||
if (keepText) button.setText(textBefore);
|
||||
|
||||
// https://github.com/sk22/megalodon/issues/526
|
||||
// button.setEnabled(!relationship.blockedBy);
|
||||
int attr = secondaryStyle ? R.attr.secondaryButtonStyle : android.R.attr.buttonStyle;
|
||||
TypedArray ta = button.getContext().obtainStyledAttributes(new int[]{attr});
|
||||
int styleRes = ta.getResourceId(0, 0);
|
||||
ta.recycle();
|
||||
ta = button.getContext().obtainStyledAttributes(styleRes, new int[]{android.R.attr.background});
|
||||
button.setBackground(ta.getDrawable(0));
|
||||
ta.recycle();
|
||||
ta = button.getContext().obtainStyledAttributes(styleRes, new int[]{android.R.attr.textColor});
|
||||
if (relationship.blocking)
|
||||
button.setTextColor(button.getResources().getColorStateList(R.color.error_600));
|
||||
else
|
||||
button.setTextColor(ta.getColorStateList(0));
|
||||
ta.recycle();
|
||||
}
|
||||
|
||||
public static void performToggleAccountNotifications(Activity activity, Account account, String accountID, Relationship relationship, Button button, Consumer<Boolean> progressCallback, Consumer<Relationship> resultCallback) {
|
||||
progressCallback.accept(true);
|
||||
new SetAccountFollowed(account.id, true, relationship.showingReblogs, !relationship.notifying)
|
||||
@@ -689,26 +698,25 @@ public class UiUtils {
|
||||
}
|
||||
|
||||
public static void setRelationshipToActionButtonM3(Relationship relationship, Button button){
|
||||
boolean secondaryStyle;
|
||||
int styleRes;
|
||||
if(relationship.blocking){
|
||||
button.setText(R.string.button_blocked);
|
||||
secondaryStyle=true;
|
||||
styleRes=R.style.Widget_Mastodon_M3_Button_Tonal_Error;
|
||||
}else if(relationship.blockedBy){
|
||||
button.setText(R.string.button_follow);
|
||||
secondaryStyle=false;
|
||||
styleRes=R.style.Widget_Mastodon_M3_Button_Filled;
|
||||
}else if(relationship.requested){
|
||||
button.setText(R.string.button_follow_pending);
|
||||
secondaryStyle=true;
|
||||
styleRes=R.style.Widget_Mastodon_M3_Button_Tonal;
|
||||
}else if(!relationship.following){
|
||||
button.setText(relationship.followedBy ? R.string.follow_back : R.string.button_follow);
|
||||
secondaryStyle=false;
|
||||
styleRes=R.style.Widget_Mastodon_M3_Button_Filled;
|
||||
}else{
|
||||
button.setText(R.string.button_following);
|
||||
secondaryStyle=true;
|
||||
styleRes=R.style.Widget_Mastodon_M3_Button_Tonal;
|
||||
}
|
||||
|
||||
button.setEnabled(!relationship.blockedBy);
|
||||
int styleRes=secondaryStyle ? R.style.Widget_Mastodon_M3_Button_Tonal : R.style.Widget_Mastodon_M3_Button_Filled;
|
||||
TypedArray ta=button.getContext().obtainStyledAttributes(styleRes, new int[]{android.R.attr.background});
|
||||
button.setBackground(ta.getDrawable(0));
|
||||
ta.recycle();
|
||||
@@ -891,8 +899,8 @@ public class UiUtils {
|
||||
public static void setUserPreferredTheme(Context context) {
|
||||
context.setTheme(switch (theme) {
|
||||
case LIGHT -> R.style.Theme_Mastodon_Light;
|
||||
case DARK -> trueBlackTheme ? R.style.Theme_Mastodon_Dark_TrueBlack : R.style.Theme_Mastodon_Dark;
|
||||
default -> trueBlackTheme ? R.style.Theme_Mastodon_AutoLightDark_TrueBlack : R.style.Theme_Mastodon_AutoLightDark;
|
||||
case DARK -> R.style.Theme_Mastodon_Dark;
|
||||
default -> R.style.Theme_Mastodon_AutoLightDark;
|
||||
});
|
||||
|
||||
ColorPalette palette = ColorPalette.palettes.get(GlobalUserPreferences.color);
|
||||
@@ -903,6 +911,15 @@ public class UiUtils {
|
||||
SCROLL_TO_TOP_DELTA = (int) res.getDimension(R.dimen.scroll_to_top_delta);
|
||||
}
|
||||
|
||||
public static int alphaBlendThemeColors(Context context, @AttrRes int color1, @AttrRes int color2, float alpha){
|
||||
if(UiUtils.isTrueBlackTheme()) return getThemeColor(context, color1);
|
||||
return alphaBlendColors(getThemeColor(context, color1), getThemeColor(context, color2), alpha);
|
||||
}
|
||||
|
||||
public static boolean isTrueBlackTheme(){
|
||||
return isDarkTheme() && trueBlackTheme;
|
||||
}
|
||||
|
||||
public static boolean isDarkTheme() {
|
||||
if (theme == GlobalUserPreferences.ThemePreference.AUTO)
|
||||
return (MastodonApp.context.getResources().getConfiguration().uiMode & Configuration.UI_MODE_NIGHT_MASK) == Configuration.UI_MODE_NIGHT_YES;
|
||||
@@ -1011,18 +1028,29 @@ public class UiUtils {
|
||||
return back;
|
||||
}
|
||||
|
||||
public static boolean setExtraTextInfo(Context ctx, TextView extraText, StatusPrivacy visibility, boolean localOnly) {
|
||||
public static boolean setExtraTextInfo(Context ctx, TextView extraText, TextView pronouns, boolean mentionedOnly, boolean localOnly, @Nullable Account account) {
|
||||
List<String> extraParts = new ArrayList<>();
|
||||
if (localOnly || (visibility != null && visibility.equals(StatusPrivacy.LOCAL)))
|
||||
Optional<String> p=pronouns==null ? Optional.empty() : extractPronouns(ctx, account);
|
||||
boolean setPronouns=false;
|
||||
if(p.isPresent()) {
|
||||
HtmlParser.setTextWithCustomEmoji(pronouns, p.get(), account.emojis);
|
||||
setPronouns=true;
|
||||
pronouns.setVisibility(View.VISIBLE);
|
||||
}else if(pronouns!=null){
|
||||
pronouns.setVisibility(View.GONE);
|
||||
}
|
||||
if(localOnly)
|
||||
extraParts.add(ctx.getString(R.string.sk_inline_local_only));
|
||||
if (visibility != null && visibility.equals(StatusPrivacy.DIRECT))
|
||||
if(mentionedOnly)
|
||||
extraParts.add(ctx.getString(R.string.sk_inline_direct));
|
||||
if (!extraParts.isEmpty()) {
|
||||
String sep = ctx.getString(R.string.sk_separator);
|
||||
extraText.setText(String.join(" " + sep + " ", extraParts));
|
||||
if(!extraParts.isEmpty()) {
|
||||
String sepp = ctx.getString(R.string.sk_separator);
|
||||
String text = String.join(" " + sepp + " ", extraParts);
|
||||
if(account == null) extraText.setText(text);
|
||||
else HtmlParser.setTextWithCustomEmoji(extraText, text, account.emojis);
|
||||
extraText.setVisibility(View.VISIBLE);
|
||||
return true;
|
||||
} else {
|
||||
}else{
|
||||
extraText.setVisibility(View.GONE);
|
||||
return false;
|
||||
}
|
||||
@@ -1443,4 +1471,182 @@ public class UiUtils {
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
@SuppressLint("DefaultLocale")
|
||||
public static String formatMediaDuration(int seconds){
|
||||
if(seconds>=3600)
|
||||
return String.format("%d:%02d:%02d", seconds/3600, seconds%3600/60, seconds%60);
|
||||
else
|
||||
return String.format("%d:%02d", seconds/60, seconds%60);
|
||||
}
|
||||
|
||||
public static void beginLayoutTransition(ViewGroup sceneRoot){
|
||||
TransitionManager.beginDelayedTransition(sceneRoot, new TransitionSet()
|
||||
.addTransition(new Fade(Fade.IN | Fade.OUT))
|
||||
.addTransition(new ChangeBounds())
|
||||
.addTransition(new ChangeScroll())
|
||||
.setDuration(250)
|
||||
.setInterpolator(CubicBezierInterpolator.DEFAULT)
|
||||
);
|
||||
}
|
||||
|
||||
public static Drawable getThemeDrawable(Context context, @AttrRes int attr){
|
||||
TypedArray ta=context.obtainStyledAttributes(new int[]{attr});
|
||||
Drawable d=ta.getDrawable(0);
|
||||
ta.recycle();
|
||||
return d;
|
||||
}
|
||||
|
||||
public static WindowInsets applyBottomInsetToFixedView(View view, WindowInsets insets){
|
||||
if(Build.VERSION.SDK_INT>=27){
|
||||
int inset=insets.getSystemWindowInsetBottom();
|
||||
view.setPadding(0, 0, 0, inset>0 ? Math.max(inset, V.dp(40)) : 0);
|
||||
return insets.replaceSystemWindowInsets(insets.getSystemWindowInsetLeft(), insets.getSystemWindowInsetTop(), insets.getSystemWindowInsetRight(), 0);
|
||||
}
|
||||
return insets;
|
||||
}
|
||||
|
||||
public static String formatDuration(Context context, int seconds){
|
||||
if(seconds<3600){
|
||||
int minutes=seconds/60;
|
||||
return context.getResources().getQuantityString(R.plurals.x_minutes, minutes, minutes);
|
||||
}else if(seconds<24*3600){
|
||||
int hours=seconds/3600;
|
||||
return context.getResources().getQuantityString(R.plurals.x_hours, hours, hours);
|
||||
}else if(seconds>=7*24*3600 && seconds%(7*24*3600)<24*3600){
|
||||
int weeks=seconds/(7*24*3600);
|
||||
return context.getResources().getQuantityString(R.plurals.x_weeks, weeks, weeks);
|
||||
}else{
|
||||
int days=seconds/(24*3600);
|
||||
return context.getResources().getQuantityString(R.plurals.x_days, days, days);
|
||||
}
|
||||
}
|
||||
|
||||
public static void openSystemShareSheet(Context context, String url){
|
||||
Intent intent=new Intent(Intent.ACTION_SEND);
|
||||
intent.setType("text/plain");
|
||||
intent.putExtra(Intent.EXTRA_TEXT, url);
|
||||
context.startActivity(Intent.createChooser(intent, context.getString(R.string.share_toot_title)));
|
||||
}
|
||||
|
||||
private static final Pattern formatStringSubstitutionPattern = Pattern.compile("%(?:(\\d)\\$)?s");
|
||||
public static CharSequence generateFormattedString(String format, CharSequence... args) {
|
||||
if (format.startsWith(" ")) format = format.substring(1);
|
||||
if (format.endsWith(" ")) format = format.substring(0, format.length() - 1);
|
||||
|
||||
Map<Integer, Integer> formatIndices = new HashMap<>();
|
||||
String[] partsInBetween = formatStringSubstitutionPattern.split(format, -1);
|
||||
SpannableStringBuilder text = new SpannableStringBuilder();
|
||||
|
||||
Matcher m = formatStringSubstitutionPattern.matcher(format);
|
||||
int argsMaxIndex = 0;
|
||||
while (m.find()) {
|
||||
String group = m.groupCount() < 1 ? null : m.group(1);
|
||||
int index = formatIndices.size();
|
||||
try { index = Integer.parseInt(group); }
|
||||
catch (Exception ignored) {}
|
||||
formatIndices.put(index, argsMaxIndex++);
|
||||
}
|
||||
|
||||
int formatOffset = formatIndices.size() > 0 ? Collections.min(formatIndices.keySet()) : 0;
|
||||
int argsOffset = 0;
|
||||
|
||||
// say, string is just 'reacted with %s', but there are two arguments
|
||||
if (args.length > argsMaxIndex) {
|
||||
text.append(args[0], new TypefaceSpan("sans-serif-medium"), 0).append(' ');
|
||||
argsOffset++;
|
||||
}
|
||||
|
||||
// join the args with the parts in between
|
||||
for (int i = 0; i < partsInBetween.length; i++) {
|
||||
text.append(partsInBetween[i]);
|
||||
Integer pos = formatIndices.get(i + formatOffset);
|
||||
if (pos != null && pos < args.length) {
|
||||
text.append(args[pos + argsOffset], new TypefaceSpan("sans-serif-medium"), 0);
|
||||
}
|
||||
}
|
||||
|
||||
// add additional args to the end of the string
|
||||
if (args.length > argsMaxIndex + 1) {
|
||||
for (int i = argsMaxIndex + 1; i < args.length; i++) {
|
||||
text.append(' ').append(args[i], new TypefaceSpan("sans-serif-medium"), 0);
|
||||
}
|
||||
}
|
||||
|
||||
return text;
|
||||
}
|
||||
|
||||
private static final String[] pronounsUrls= new String[] {
|
||||
"pronouns.within.lgbt/",
|
||||
"pronouns.cc/pronouns/",
|
||||
"pronouns.page/"
|
||||
};
|
||||
|
||||
private static final Pattern trimPronouns=Pattern.compile("[^\\w*]*([\\w*].*[\\w*]|[\\w*])\\W*");
|
||||
private static String extractPronounsFromField(String localizedPronouns, AccountField field) {
|
||||
if(!field.name.toLowerCase().contains(localizedPronouns) &&
|
||||
!field.name.toLowerCase().contains("pronouns")) return null;
|
||||
String text=HtmlParser.strip(field.value);
|
||||
if(field.value.toLowerCase().contains("https://")){
|
||||
for(String pronounUrl : pronounsUrls){
|
||||
int index=text.indexOf(pronounUrl);
|
||||
int beginPronouns=index+pronounUrl.length();
|
||||
// we only want to display the info from the urls if they're not usernames
|
||||
if(index>-1 && beginPronouns<text.length() && text.charAt(beginPronouns)!='@'){
|
||||
return text.substring(beginPronouns);
|
||||
}
|
||||
}
|
||||
// maybe it's like "they and them (https://pronouns.page/...)"
|
||||
String[] parts=text.substring(0, text.toLowerCase().indexOf("https://"))
|
||||
.split(" ");
|
||||
if (parts.length==0) return null;
|
||||
text=String.join(" ", parts);
|
||||
}
|
||||
|
||||
Matcher matcher=trimPronouns.matcher(text);
|
||||
if(!matcher.find()) return null;
|
||||
String matched=matcher.group(1);
|
||||
// crude fix to allow for pronouns like "it(/she)"
|
||||
int missingClosingParens=0;
|
||||
for(char c : matched.toCharArray()){
|
||||
if(c=='(') missingClosingParens++;
|
||||
if(c==')') missingClosingParens--;
|
||||
}
|
||||
return matched+")".repeat(Math.max(0, missingClosingParens));
|
||||
}
|
||||
|
||||
// https://stackoverflow.com/questions/9475589/how-to-get-string-from-different-locales-in-android
|
||||
public static Context getLocalizedContext(Context context, Locale desiredLocale) {
|
||||
Configuration conf = context.getResources().getConfiguration();
|
||||
conf = new Configuration(conf);
|
||||
conf.setLocale(desiredLocale);
|
||||
return context.createConfigurationContext(conf);
|
||||
}
|
||||
|
||||
public static Optional<String> extractPronouns(Context context, @Nullable Account account) {
|
||||
if (account == null) return Optional.empty();
|
||||
String localizedPronouns=context.getString(R.string.sk_pronouns_label).toLowerCase();
|
||||
|
||||
// higher = worse. the lowest number wins. also i'm sorry for writing this
|
||||
ToIntFunction<AccountField> comparePronounFields=(f)->{
|
||||
String t=f.name.toLowerCase();
|
||||
int localizedIndex = t.indexOf(localizedPronouns);
|
||||
int englishIndex = t.indexOf("pronouns");
|
||||
// neutralizing an english fallback failure if the localized pronoun already succeeded
|
||||
// -t.length() + t.length() = 0 -> so the low localized score doesn't get obscured
|
||||
if (englishIndex < 0) englishIndex = localizedIndex > -1 ? -t.length() : t.length();
|
||||
if (localizedIndex < 0) localizedIndex = t.length();
|
||||
return (localizedIndex + t.length()) + (englishIndex + t.length()) * 100;
|
||||
};
|
||||
|
||||
// debugging:
|
||||
// List<Integer> ints = account.fields.stream().map(comparePronounFields::applyAsInt).collect(Collectors.toList());
|
||||
// List<AccountField> sorted = account.fields.stream().sorted(Comparator.comparingInt(comparePronounFields)).collect(Collectors.toList());
|
||||
|
||||
return account.fields.stream()
|
||||
.sorted(Comparator.comparingInt(comparePronounFields))
|
||||
.map(f->UiUtils.extractPronounsFromField(localizedPronouns, f))
|
||||
.filter(Objects::nonNull)
|
||||
.findFirst();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,15 +1,15 @@
|
||||
package org.joinmastodon.android.ui;
|
||||
package org.joinmastodon.android.ui.viewcontrollers;
|
||||
|
||||
import android.annotation.SuppressLint;
|
||||
import android.app.Activity;
|
||||
import android.graphics.Typeface;
|
||||
import android.graphics.Rect;
|
||||
import android.graphics.drawable.ColorDrawable;
|
||||
import android.graphics.drawable.Drawable;
|
||||
import android.text.TextUtils;
|
||||
import android.view.Gravity;
|
||||
import android.view.View;
|
||||
import android.view.ViewGroup;
|
||||
import android.widget.FrameLayout;
|
||||
import android.widget.ImageView;
|
||||
import android.widget.ProgressBar;
|
||||
import android.widget.LinearLayout;
|
||||
import android.widget.TextView;
|
||||
|
||||
import org.joinmastodon.android.R;
|
||||
@@ -19,14 +19,18 @@ import org.joinmastodon.android.model.Account;
|
||||
import org.joinmastodon.android.model.Emoji;
|
||||
import org.joinmastodon.android.model.Hashtag;
|
||||
import org.joinmastodon.android.model.SearchResults;
|
||||
import org.joinmastodon.android.ui.drawables.ComposeAutocompleteBackgroundDrawable;
|
||||
import org.joinmastodon.android.model.viewmodel.AccountViewModel;
|
||||
import org.joinmastodon.android.ui.BetterItemAnimator;
|
||||
import org.joinmastodon.android.ui.OutlineProviders;
|
||||
import org.joinmastodon.android.ui.text.HtmlParser;
|
||||
import org.joinmastodon.android.ui.utils.CustomEmojiHelper;
|
||||
import org.joinmastodon.android.ui.utils.HideableSingleViewRecyclerAdapter;
|
||||
import org.joinmastodon.android.ui.utils.UiUtils;
|
||||
import org.joinmastodon.android.ui.views.FilterChipView;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.Collections;
|
||||
import java.util.List;
|
||||
import java.util.function.Consumer;
|
||||
import java.util.stream.Collectors;
|
||||
import java.util.stream.Stream;
|
||||
|
||||
@@ -43,54 +47,66 @@ import me.grishka.appkit.imageloader.RecyclerViewDelegate;
|
||||
import me.grishka.appkit.imageloader.requests.ImageLoaderRequest;
|
||||
import me.grishka.appkit.imageloader.requests.UrlImageLoaderRequest;
|
||||
import me.grishka.appkit.utils.BindableViewHolder;
|
||||
import me.grishka.appkit.utils.MergeRecyclerAdapter;
|
||||
import me.grishka.appkit.utils.V;
|
||||
import me.grishka.appkit.views.UsableRecyclerView;
|
||||
|
||||
public class ComposeAutocompleteViewController{
|
||||
private static final int LOADING_FAKE_USER_COUNT=3;
|
||||
|
||||
private Activity activity;
|
||||
private String accountID;
|
||||
private FrameLayout contentView;
|
||||
private UsableRecyclerView list;
|
||||
private ListImageLoaderWrapper imgLoader;
|
||||
private ProgressBar progress;
|
||||
private List<WrappedAccount> users=Collections.emptyList();
|
||||
private List<AccountViewModel> users=Collections.emptyList();
|
||||
private List<Hashtag> hashtags=Collections.emptyList();
|
||||
private List<WrappedEmoji> emojis=Collections.emptyList();
|
||||
private Mode mode;
|
||||
private APIRequest currentRequest;
|
||||
private Runnable usersDebouncer=this::doSearchUsers, hashtagsDebouncer=this::doSearchHashtags;
|
||||
private String lastText;
|
||||
private ComposeAutocompleteBackgroundDrawable background;
|
||||
private boolean listIsHidden=true;
|
||||
private boolean isLoading;
|
||||
private FilterChipView emptyButton;
|
||||
private HideableSingleViewRecyclerAdapter emptyButtonAdapter;
|
||||
|
||||
private UsersAdapter usersAdapter;
|
||||
private HashtagsAdapter hashtagsAdapter;
|
||||
private EmojisAdapter emojisAdapter;
|
||||
private MergeRecyclerAdapter usersMergeAdapter;
|
||||
private MergeRecyclerAdapter emojisMergeAdapter;
|
||||
|
||||
private Consumer<String> completionSelectedListener;
|
||||
|
||||
private DividerItemDecoration usersDividers, hashtagsDividers;
|
||||
private AutocompleteListener completionSelectedListener;
|
||||
|
||||
public ComposeAutocompleteViewController(Activity activity, String accountID){
|
||||
this.activity=activity;
|
||||
this.accountID=accountID;
|
||||
background=new ComposeAutocompleteBackgroundDrawable(UiUtils.getThemeColor(activity, android.R.attr.colorBackground));
|
||||
contentView=new FrameLayout(activity);
|
||||
contentView.setBackground(background);
|
||||
|
||||
list=new UsableRecyclerView(activity);
|
||||
list.setLayoutManager(new LinearLayoutManager(activity));
|
||||
list.setLayoutManager(new LinearLayoutManager(activity, LinearLayoutManager.HORIZONTAL, false));
|
||||
list.setItemAnimator(new BetterItemAnimator());
|
||||
list.setVisibility(View.GONE);
|
||||
list.setPadding(V.dp(16), V.dp(4), V.dp(16), V.dp(4));
|
||||
list.setClipToPadding(false);
|
||||
list.setSelector(null);
|
||||
list.addItemDecoration(new RecyclerView.ItemDecoration(){
|
||||
@Override
|
||||
public void getItemOffsets(@NonNull Rect outRect, @NonNull View view, @NonNull RecyclerView parent, @NonNull RecyclerView.State state){
|
||||
if(parent.getChildAdapterPosition(view)<parent.getAdapter().getItemCount()-1)
|
||||
outRect.right=V.dp(8);
|
||||
}
|
||||
});
|
||||
contentView.addView(list, new FrameLayout.LayoutParams(ViewGroup.LayoutParams.MATCH_PARENT, ViewGroup.LayoutParams.MATCH_PARENT));
|
||||
|
||||
progress=new ProgressBar(activity);
|
||||
FrameLayout.LayoutParams progressLP=new FrameLayout.LayoutParams(V.dp(48), V.dp(48), Gravity.CENTER_HORIZONTAL|Gravity.TOP);
|
||||
progressLP.topMargin=V.dp(16);
|
||||
contentView.addView(progress, progressLP);
|
||||
|
||||
usersDividers=new DividerItemDecoration(activity, R.attr.colorPollVoted, 1, 72, 16);
|
||||
hashtagsDividers=new DividerItemDecoration(activity, R.attr.colorPollVoted, 1, 16, 16);
|
||||
emptyButton=new FilterChipView(activity);
|
||||
emptyButtonAdapter=new HideableSingleViewRecyclerAdapter(emptyButton);
|
||||
emptyButton.setOnClickListener(v->{
|
||||
if(mode==Mode.EMOJIS){
|
||||
completionSelectedListener.onSetEmojiPanelOpen(true);
|
||||
}else if(mode==Mode.USERS){
|
||||
completionSelectedListener.onLaunchAccountSearch();
|
||||
}
|
||||
});
|
||||
|
||||
imgLoader=new ListImageLoaderWrapper(activity, list, new RecyclerViewDelegate(list), null);
|
||||
}
|
||||
@@ -101,13 +117,15 @@ public class ComposeAutocompleteViewController{
|
||||
}else if(mode==Mode.HASHTAGS){
|
||||
list.removeCallbacks(hashtagsDebouncer);
|
||||
}
|
||||
if(text==null)
|
||||
return;
|
||||
Mode prevMode=mode;
|
||||
if(currentRequest!=null){
|
||||
currentRequest.cancel();
|
||||
currentRequest=null;
|
||||
}
|
||||
if(text==null){
|
||||
reset();
|
||||
return;
|
||||
}
|
||||
Mode prevMode=mode;
|
||||
mode=switch(text.charAt(0)){
|
||||
case '@' -> Mode.USERS;
|
||||
case '#' -> Mode.HASHTAGS;
|
||||
@@ -115,16 +133,33 @@ public class ComposeAutocompleteViewController{
|
||||
default -> throw new IllegalStateException("Unexpected value: "+text.charAt(0));
|
||||
};
|
||||
if(prevMode!=mode){
|
||||
if(mode==Mode.USERS){
|
||||
isLoading=true;
|
||||
emptyButtonAdapter.setVisible(false);
|
||||
}
|
||||
|
||||
list.setAdapter(switch(mode){
|
||||
case USERS -> {
|
||||
if(usersAdapter==null)
|
||||
if(usersAdapter==null){
|
||||
usersAdapter=new UsersAdapter();
|
||||
yield usersAdapter;
|
||||
usersMergeAdapter=new MergeRecyclerAdapter();
|
||||
usersMergeAdapter.addAdapter(emptyButtonAdapter);
|
||||
usersMergeAdapter.addAdapter(usersAdapter);
|
||||
}
|
||||
emptyButton.setText(R.string.compose_autocomplete_users_empty);
|
||||
emptyButton.setDrawableStartTinted(R.drawable.ic_fluent_search_20_regular);
|
||||
yield usersMergeAdapter;
|
||||
}
|
||||
case EMOJIS -> {
|
||||
if(emojisAdapter==null)
|
||||
if(emojisAdapter==null){
|
||||
emojisAdapter=new EmojisAdapter();
|
||||
yield emojisAdapter;
|
||||
emojisMergeAdapter=new MergeRecyclerAdapter();
|
||||
emojisMergeAdapter.addAdapter(emptyButtonAdapter);
|
||||
emojisMergeAdapter.addAdapter(emojisAdapter);
|
||||
}
|
||||
emptyButton.setText(R.string.compose_autocomplete_emoji_empty);
|
||||
emptyButton.setDrawableStartTinted(R.drawable.ic_fluent_emoji_20_regular);
|
||||
yield emojisMergeAdapter;
|
||||
}
|
||||
case HASHTAGS -> {
|
||||
if(hashtagsAdapter==null)
|
||||
@@ -132,25 +167,18 @@ public class ComposeAutocompleteViewController{
|
||||
yield hashtagsAdapter;
|
||||
}
|
||||
});
|
||||
if(mode!=Mode.EMOJIS){
|
||||
list.setVisibility(View.GONE);
|
||||
progress.setVisibility(View.VISIBLE);
|
||||
listIsHidden=true;
|
||||
}else if(listIsHidden){
|
||||
list.setVisibility(View.VISIBLE);
|
||||
progress.setVisibility(View.GONE);
|
||||
listIsHidden=false;
|
||||
}
|
||||
if((prevMode==Mode.HASHTAGS)!=(mode==Mode.HASHTAGS) || prevMode==null){
|
||||
if(prevMode!=null)
|
||||
list.removeItemDecoration(prevMode==Mode.HASHTAGS ? hashtagsDividers : usersDividers);
|
||||
list.addItemDecoration(mode==Mode.HASHTAGS ? hashtagsDividers : usersDividers);
|
||||
}
|
||||
}
|
||||
lastText=text;
|
||||
if(mode==Mode.USERS){
|
||||
list.postDelayed(usersDebouncer, 300);
|
||||
}else if(mode==Mode.HASHTAGS){
|
||||
List<Hashtag> oldList=hashtags;
|
||||
hashtags=new ArrayList<>();
|
||||
Hashtag tag=new Hashtag();
|
||||
tag.name=lastText.substring(1);
|
||||
hashtags.add(tag);
|
||||
UiUtils.updateList(oldList, hashtags, list, hashtagsAdapter, (t1, t2)->t1.name.equals(t2.name));
|
||||
|
||||
list.postDelayed(hashtagsDebouncer, 300);
|
||||
}else if(mode==Mode.EMOJIS){
|
||||
String _text=text.substring(1); // remove ':'
|
||||
@@ -167,38 +195,56 @@ public class ComposeAutocompleteViewController{
|
||||
.filter(e -> e.shortcode.toLowerCase().contains(_text.toLowerCase())))
|
||||
.map(WrappedEmoji::new)
|
||||
.collect(Collectors.toList());
|
||||
emptyButtonAdapter.setVisible(emojis.isEmpty());
|
||||
UiUtils.updateList(oldList, emojis, list, emojisAdapter, (e1, e2)->e1.emoji.shortcode.equals(e2.emoji.shortcode));
|
||||
list.invalidateItemDecorations();
|
||||
imgLoader.updateImages();
|
||||
}
|
||||
}
|
||||
|
||||
public void setCompletionSelectedListener(Consumer<String> completionSelectedListener){
|
||||
public void setCompletionSelectedListener(AutocompleteListener completionSelectedListener){
|
||||
this.completionSelectedListener=completionSelectedListener;
|
||||
}
|
||||
|
||||
public void setArrowOffset(int offset){
|
||||
background.setArrowOffset(offset);
|
||||
}
|
||||
|
||||
public View getView(){
|
||||
return contentView;
|
||||
}
|
||||
|
||||
public void reset(){
|
||||
mode=null;
|
||||
users.clear();
|
||||
emojis.clear();
|
||||
hashtags.clear();
|
||||
}
|
||||
|
||||
public Mode getMode(){
|
||||
return mode;
|
||||
}
|
||||
|
||||
private void doSearchUsers(){
|
||||
currentRequest=new GetSearchResults(lastText, GetSearchResults.Type.ACCOUNTS, false)
|
||||
.setCallback(new Callback<>(){
|
||||
@Override
|
||||
public void onSuccess(SearchResults result){
|
||||
currentRequest=null;
|
||||
List<WrappedAccount> oldList=users;
|
||||
users=result.accounts.stream().map(WrappedAccount::new).collect(Collectors.toList());
|
||||
UiUtils.updateList(oldList, users, list, usersAdapter, (a1, a2)->a1.account.id.equals(a2.account.id));
|
||||
imgLoader.updateImages();
|
||||
if(listIsHidden){
|
||||
listIsHidden=false;
|
||||
V.setVisibilityAnimated(list, View.VISIBLE);
|
||||
V.setVisibilityAnimated(progress, View.GONE);
|
||||
List<AccountViewModel> oldList=users;
|
||||
users=result.accounts.stream().map(a->new AccountViewModel(a, accountID)).collect(Collectors.toList());
|
||||
if(isLoading){
|
||||
isLoading=false;
|
||||
if(users.size()>=LOADING_FAKE_USER_COUNT){
|
||||
usersAdapter.notifyItemRangeChanged(0, LOADING_FAKE_USER_COUNT);
|
||||
if(users.size()>LOADING_FAKE_USER_COUNT)
|
||||
usersAdapter.notifyItemRangeInserted(LOADING_FAKE_USER_COUNT, users.size()-LOADING_FAKE_USER_COUNT);
|
||||
}else{
|
||||
usersAdapter.notifyItemRangeChanged(0, users.size());
|
||||
usersAdapter.notifyItemRangeRemoved(users.size(), LOADING_FAKE_USER_COUNT-users.size());
|
||||
}
|
||||
}else{
|
||||
UiUtils.updateList(oldList, users, list, usersAdapter, (a1, a2)->a1.account.id.equals(a2.account.id));
|
||||
}
|
||||
list.invalidateItemDecorations();
|
||||
emptyButtonAdapter.setVisible(users.isEmpty());
|
||||
imgLoader.updateImages();
|
||||
}
|
||||
|
||||
@Override
|
||||
@@ -215,15 +261,12 @@ public class ComposeAutocompleteViewController{
|
||||
@Override
|
||||
public void onSuccess(SearchResults result){
|
||||
currentRequest=null;
|
||||
if(result.hashtags.isEmpty() || (result.hashtags.size()==1 && result.hashtags.get(0).name.equals(lastText.substring(1))))
|
||||
return;
|
||||
List<Hashtag> oldList=hashtags;
|
||||
hashtags=result.hashtags;
|
||||
UiUtils.updateList(oldList, hashtags, list, hashtagsAdapter, (t1, t2)->t1.name.equals(t2.name));
|
||||
imgLoader.updateImages();
|
||||
if(listIsHidden){
|
||||
listIsHidden=false;
|
||||
V.setVisibilityAnimated(list, View.VISIBLE);
|
||||
V.setVisibilityAnimated(progress, View.GONE);
|
||||
}
|
||||
list.invalidateItemDecorations();
|
||||
}
|
||||
|
||||
@Override
|
||||
@@ -242,56 +285,67 @@ public class ComposeAutocompleteViewController{
|
||||
@NonNull
|
||||
@Override
|
||||
public UserViewHolder onCreateViewHolder(@NonNull ViewGroup parent, int viewType){
|
||||
return new UserViewHolder();
|
||||
return switch(viewType){
|
||||
case 0 -> new UserViewHolder();
|
||||
case 1 -> new LoadingUserViewHolder();
|
||||
default -> throw new IllegalStateException("Unexpected value: "+viewType);
|
||||
};
|
||||
}
|
||||
|
||||
@Override
|
||||
public int getItemCount(){
|
||||
if(isLoading)
|
||||
return LOADING_FAKE_USER_COUNT;
|
||||
return users.size();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onBindViewHolder(UserViewHolder holder, int position){
|
||||
holder.bind(users.get(position));
|
||||
super.onBindViewHolder(holder, position);
|
||||
if(!isLoading){
|
||||
holder.bind(users.get(position));
|
||||
super.onBindViewHolder(holder, position);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public int getImageCountForItem(int position){
|
||||
return 1+users.get(position).emojiHelper.getImageCount();
|
||||
return isLoading ? 0 : 1;
|
||||
}
|
||||
|
||||
@Override
|
||||
public ImageLoaderRequest getImageRequest(int position, int image){
|
||||
WrappedAccount a=users.get(position);
|
||||
AccountViewModel a=users.get(position);
|
||||
if(image==0)
|
||||
return a.avaRequest;
|
||||
return a.emojiHelper.getImageRequest(image-1);
|
||||
}
|
||||
|
||||
@Override
|
||||
public int getItemViewType(int position){
|
||||
return isLoading ? 1 : 0;
|
||||
}
|
||||
}
|
||||
|
||||
private class UserViewHolder extends BindableViewHolder<WrappedAccount> implements ImageLoaderViewHolder, UsableRecyclerView.Clickable{
|
||||
private final ImageView ava;
|
||||
private final TextView name, username;
|
||||
private class UserViewHolder extends BindableViewHolder<AccountViewModel> implements ImageLoaderViewHolder, UsableRecyclerView.Clickable{
|
||||
protected final ImageView ava;
|
||||
protected final TextView username;
|
||||
|
||||
private UserViewHolder(){
|
||||
public UserViewHolder(){
|
||||
super(activity, R.layout.item_autocomplete_user, list);
|
||||
ava=findViewById(R.id.photo);
|
||||
name=findViewById(R.id.name);
|
||||
username=findViewById(R.id.username);
|
||||
ava.setOutlineProvider(OutlineProviders.roundedRect(12));
|
||||
ava.setOutlineProvider(OutlineProviders.OVAL);
|
||||
ava.setClipToOutline(true);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onBind(WrappedAccount item){
|
||||
name.setText(item.parsedName);
|
||||
public void onBind(AccountViewModel item){
|
||||
username.setText("@"+item.account.acct);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onClick(){
|
||||
completionSelectedListener.accept("@"+item.account.acct);
|
||||
completionSelectedListener.onCompletionSelected("@"+item.account.acct);
|
||||
}
|
||||
|
||||
@Override
|
||||
@@ -300,13 +354,29 @@ public class ComposeAutocompleteViewController{
|
||||
ava.setImageDrawable(image);
|
||||
}else{
|
||||
item.emojiHelper.setImageDrawable(index-1, image);
|
||||
name.invalidate();
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void clearImage(int index){
|
||||
setImage(index, null);
|
||||
if(index==0)
|
||||
ava.setImageResource(R.drawable.image_placeholder);
|
||||
else
|
||||
setImage(index, null);
|
||||
}
|
||||
}
|
||||
|
||||
private class LoadingUserViewHolder extends UserViewHolder implements UsableRecyclerView.DisableableClickable{
|
||||
public LoadingUserViewHolder(){
|
||||
int color=UiUtils.getThemeColor(activity, R.attr.colorM3OutlineVariant);
|
||||
ava.setImageDrawable(new ColorDrawable(color));
|
||||
username.setLayoutParams(new LinearLayout.LayoutParams(V.dp(64), V.dp(10)));
|
||||
username.setBackgroundColor(color);
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean isEnabled(){
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -333,17 +403,11 @@ public class ComposeAutocompleteViewController{
|
||||
private final TextView text;
|
||||
|
||||
private HashtagViewHolder(){
|
||||
super(new TextView(activity));
|
||||
super(activity, R.layout.item_autocomplete_hashtag, list);
|
||||
text=(TextView) itemView;
|
||||
text.setLayoutParams(new RecyclerView.LayoutParams(ViewGroup.LayoutParams.MATCH_PARENT, V.dp(48)));
|
||||
text.setTextAppearance(R.style.m3_title_medium);
|
||||
text.setTypeface(Typeface.DEFAULT);
|
||||
text.setSingleLine();
|
||||
text.setEllipsize(TextUtils.TruncateAt.END);
|
||||
text.setGravity(Gravity.CENTER_VERTICAL);
|
||||
text.setPadding(V.dp(16), 0, V.dp(16), 0);
|
||||
}
|
||||
|
||||
@SuppressLint("SetTextI18n")
|
||||
@Override
|
||||
public void onBind(Hashtag item){
|
||||
text.setText("#"+item.name);
|
||||
@@ -351,7 +415,7 @@ public class ComposeAutocompleteViewController{
|
||||
|
||||
@Override
|
||||
public void onClick(){
|
||||
completionSelectedListener.accept("#"+item.name);
|
||||
completionSelectedListener.onCompletionSelected("#"+item.name);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -395,7 +459,7 @@ public class ComposeAutocompleteViewController{
|
||||
private EmojiViewHolder(){
|
||||
super(activity, R.layout.item_autocomplete_user, list);
|
||||
ava=findViewById(R.id.photo);
|
||||
name=findViewById(R.id.name);
|
||||
name=findViewById(R.id.username);
|
||||
}
|
||||
|
||||
@Override
|
||||
@@ -408,6 +472,7 @@ public class ComposeAutocompleteViewController{
|
||||
ava.setImageDrawable(null);
|
||||
}
|
||||
|
||||
@SuppressLint("SetTextI18n")
|
||||
@Override
|
||||
public void onBind(WrappedEmoji item){
|
||||
name.setText(":"+item.emoji.shortcode+":");
|
||||
@@ -415,22 +480,7 @@ public class ComposeAutocompleteViewController{
|
||||
|
||||
@Override
|
||||
public void onClick(){
|
||||
completionSelectedListener.accept(":"+item.emoji.shortcode+":");
|
||||
}
|
||||
}
|
||||
|
||||
private static class WrappedAccount{
|
||||
private Account account;
|
||||
private CharSequence parsedName;
|
||||
private CustomEmojiHelper emojiHelper;
|
||||
private ImageLoaderRequest avaRequest;
|
||||
|
||||
public WrappedAccount(Account account){
|
||||
this.account=account;
|
||||
parsedName=HtmlParser.parseCustomEmoji(account.displayName, account.emojis);
|
||||
emojiHelper=new CustomEmojiHelper();
|
||||
emojiHelper.setText(parsedName);
|
||||
avaRequest=new UrlImageLoaderRequest(account.avatar, V.dp(50), V.dp(50));
|
||||
completionSelectedListener.onCompletionSelected(":"+item.emoji.shortcode+":");
|
||||
}
|
||||
}
|
||||
|
||||
@@ -444,9 +494,15 @@ public class ComposeAutocompleteViewController{
|
||||
}
|
||||
}
|
||||
|
||||
private enum Mode{
|
||||
public enum Mode{
|
||||
USERS,
|
||||
HASHTAGS,
|
||||
EMOJIS
|
||||
}
|
||||
|
||||
public interface AutocompleteListener{
|
||||
void onCompletionSelected(String completion);
|
||||
void onSetEmojiPanelOpen(boolean open);
|
||||
void onLaunchAccountSearch();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,382 @@
|
||||
package org.joinmastodon.android.ui.viewcontrollers;
|
||||
|
||||
import android.content.Context;
|
||||
import android.graphics.Canvas;
|
||||
import android.graphics.Paint;
|
||||
import android.os.Build;
|
||||
import android.text.TextUtils;
|
||||
import android.view.View;
|
||||
import android.view.ViewGroup;
|
||||
import android.view.ViewTreeObserver;
|
||||
import android.view.textclassifier.TextClassificationManager;
|
||||
import android.view.textclassifier.TextLanguage;
|
||||
import android.widget.Checkable;
|
||||
import android.widget.CheckedTextView;
|
||||
import android.widget.RadioButton;
|
||||
import android.widget.TextView;
|
||||
|
||||
import org.joinmastodon.android.GlobalUserPreferences;
|
||||
import org.joinmastodon.android.R;
|
||||
import org.joinmastodon.android.api.MastodonAPIController;
|
||||
import org.joinmastodon.android.api.session.AccountSession;
|
||||
import org.joinmastodon.android.api.session.AccountSessionManager;
|
||||
import org.joinmastodon.android.ui.DividerItemDecoration;
|
||||
import org.joinmastodon.android.ui.utils.UiUtils;
|
||||
import org.joinmastodon.android.ui.views.CheckableLinearLayout;
|
||||
import org.joinmastodon.android.utils.MastodonLanguage;
|
||||
import org.parceler.Parcel;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.Comparator;
|
||||
import java.util.List;
|
||||
import java.util.Locale;
|
||||
import java.util.Objects;
|
||||
import java.util.stream.Collectors;
|
||||
|
||||
import androidx.annotation.NonNull;
|
||||
import androidx.annotation.RequiresApi;
|
||||
import androidx.recyclerview.widget.LinearLayoutManager;
|
||||
import androidx.recyclerview.widget.RecyclerView;
|
||||
import me.grishka.appkit.utils.BindableViewHolder;
|
||||
import me.grishka.appkit.utils.MergeRecyclerAdapter;
|
||||
import me.grishka.appkit.utils.V;
|
||||
import me.grishka.appkit.views.UsableRecyclerView;
|
||||
|
||||
public class ComposeLanguageAlertViewController{
|
||||
private Context context;
|
||||
private UsableRecyclerView list;
|
||||
private List<LocaleInfo> allLocales;
|
||||
private List<SpecialLocaleInfo> specialLocales=new ArrayList<>();
|
||||
private int selectedIndex=0;
|
||||
private MastodonLanguage selectedLocale;
|
||||
private String selectedEncoding;
|
||||
private MastodonLanguage.LanguageResolver resolver;
|
||||
|
||||
public ComposeLanguageAlertViewController(Context context, String preferred, SelectedOption previouslySelected, String postText, MastodonLanguage.LanguageResolver resolver){
|
||||
this(context, preferred, previouslySelected, postText, resolver, null);
|
||||
}
|
||||
|
||||
public ComposeLanguageAlertViewController(Context context, String preferred, SelectedOption previouslySelected, String postText, MastodonLanguage.LanguageResolver resolver, AccountSession session){
|
||||
this.context=context;
|
||||
this.resolver=resolver;
|
||||
|
||||
allLocales=MastodonLanguage.allLanguages.stream()
|
||||
.map(l -> new LocaleInfo(l, l.getDisplayName(context)))
|
||||
.sorted(Comparator.comparing(a->a.displayName))
|
||||
.collect(Collectors.toList());
|
||||
|
||||
if(!TextUtils.isEmpty(preferred)){
|
||||
MastodonLanguage lang = resolver.fromOrFallback(preferred);
|
||||
specialLocales.add(new SpecialLocaleInfo(
|
||||
lang,
|
||||
lang.getDisplayName(context),
|
||||
context.getString(R.string.language_default)
|
||||
));
|
||||
}
|
||||
|
||||
if(!Locale.getDefault().getLanguage().equals(preferred)){
|
||||
MastodonLanguage lang = resolver.getDefault();
|
||||
specialLocales.add(new SpecialLocaleInfo(
|
||||
lang,
|
||||
lang.getDisplayName(context),
|
||||
context.getString(R.string.language_system)
|
||||
));
|
||||
}
|
||||
|
||||
if(Build.VERSION.SDK_INT>=29 && !TextUtils.isEmpty(postText)){
|
||||
SpecialLocaleInfo detected=new SpecialLocaleInfo();
|
||||
detected.displayName=context.getString(R.string.language_detecting);
|
||||
detected.enabled=false;
|
||||
specialLocales.add(detected);
|
||||
detectLanguage(detected, postText);
|
||||
}
|
||||
|
||||
if (session!=null && session.getLocalPreferences().bottomEncoding) {
|
||||
specialLocales.add(new SpecialLocaleInfo(null, "\uD83E\uDD7A\uD83D\uDC49\uD83D\uDC48", "bottom"));
|
||||
}
|
||||
|
||||
if(previouslySelected!=null){
|
||||
if(previouslySelected.index!=-1 && ((previouslySelected.index<specialLocales.size() && Objects.equals(previouslySelected.language, specialLocales.get(previouslySelected.index).language)) ||
|
||||
(previouslySelected.index<specialLocales.size()+allLocales.size() && Objects.equals(previouslySelected.language, allLocales.get(previouslySelected.index-specialLocales.size()).language)))){
|
||||
selectedIndex=previouslySelected.index;
|
||||
selectedLocale=previouslySelected.language;
|
||||
}else{
|
||||
int i=0;
|
||||
boolean found=false;
|
||||
for(SpecialLocaleInfo li:specialLocales){
|
||||
if(previouslySelected.language != null && previouslySelected.language.equals(li.language)){
|
||||
selectedLocale=li.language;
|
||||
selectedIndex=i;
|
||||
found=true;
|
||||
break;
|
||||
}
|
||||
i++;
|
||||
}
|
||||
if(!found){
|
||||
for(LocaleInfo li:allLocales){
|
||||
if(li.language.equals(previouslySelected.language)){
|
||||
selectedLocale=li.language;
|
||||
selectedIndex=i;
|
||||
break;
|
||||
}
|
||||
i++;
|
||||
}
|
||||
}
|
||||
}
|
||||
}else{
|
||||
selectedLocale=specialLocales.get(0).language;
|
||||
}
|
||||
|
||||
list=new UsableRecyclerView(context);
|
||||
MergeRecyclerAdapter adapter=new MergeRecyclerAdapter();
|
||||
adapter.addAdapter(new SpecialLanguagesAdapter());
|
||||
adapter.addAdapter(new AllLocalesAdapter());
|
||||
list.setAdapter(adapter);
|
||||
list.setLayoutManager(new LinearLayoutManager(context));
|
||||
|
||||
list.addItemDecoration(new DividerItemDecoration(context, R.attr.colorM3Outline, 1, 0, 0, vh->vh.getAbsoluteAdapterPosition()==specialLocales.size()-1));
|
||||
list.addItemDecoration(new RecyclerView.ItemDecoration(){
|
||||
private Paint paint=new Paint();
|
||||
|
||||
{
|
||||
paint.setColor(UiUtils.getThemeColor(context, R.attr.colorM3Outline));
|
||||
paint.setStyle(Paint.Style.STROKE);
|
||||
paint.setStrokeWidth(V.dp(1));
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onDrawOver(@NonNull Canvas c, @NonNull RecyclerView parent, @NonNull RecyclerView.State state){
|
||||
if(parent.canScrollVertically(1)){
|
||||
float y=parent.getHeight()-paint.getStrokeWidth()/2f;
|
||||
c.drawLine(0, y, parent.getWidth(), y, paint);
|
||||
}
|
||||
if(parent.canScrollVertically(-1)){
|
||||
float y=paint.getStrokeWidth()/2f;
|
||||
c.drawLine(0, y, parent.getWidth(), y, paint);
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
if(previouslySelected!=null && selectedIndex>0){
|
||||
list.getViewTreeObserver().addOnPreDrawListener(new ViewTreeObserver.OnPreDrawListener(){
|
||||
@Override
|
||||
public boolean onPreDraw(){
|
||||
list.getViewTreeObserver().removeOnPreDrawListener(this);
|
||||
|
||||
if(list.findViewHolderForAdapterPosition(selectedIndex)==null)
|
||||
list.scrollToPosition(selectedIndex);
|
||||
|
||||
return true;
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
@RequiresApi(api = Build.VERSION_CODES.Q)
|
||||
private void detectLanguage(SpecialLocaleInfo info, String text){
|
||||
MastodonAPIController.runInBackground(()->{
|
||||
TextLanguage lang=context.getSystemService(TextClassificationManager.class).getTextClassifier().detectLanguage(new TextLanguage.Request.Builder(text).build());
|
||||
list.post(()->{
|
||||
SpecialLanguageViewHolder holder=(SpecialLanguageViewHolder) list.findViewHolderForAdapterPosition(specialLocales.indexOf(info));
|
||||
if(lang.getLocaleHypothesisCount()==0 || lang.getConfidenceScore(lang.getLocale(0))<0.75f){
|
||||
info.displayName=context.getString(R.string.language_cant_detect);
|
||||
}else{
|
||||
MastodonLanguage language = resolver.fromOrFallback(lang.getLocale(0).toLanguageTag());
|
||||
info.language=language;
|
||||
info.displayName=language.getDisplayName(context);
|
||||
info.title=context.getString(R.string.language_detected);
|
||||
info.enabled=true;
|
||||
if(holder!=null)
|
||||
UiUtils.beginLayoutTransition(holder.view);
|
||||
}
|
||||
if(holder!=null)
|
||||
holder.rebind();
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
public View getView(){
|
||||
return list;
|
||||
}
|
||||
|
||||
// Needed because in some languages (e.g. Slavic ones) these names returned by the system start with a lowercase letter
|
||||
private String capitalizeLanguageName(String name){
|
||||
return name.substring(0, 1).toUpperCase(Locale.getDefault())+name.substring(1);
|
||||
}
|
||||
|
||||
public SelectedOption getSelectedOption(){
|
||||
return new SelectedOption(selectedIndex, selectedLocale, selectedEncoding);
|
||||
}
|
||||
|
||||
private void selectItem(int index){
|
||||
if(index==selectedIndex)
|
||||
return;
|
||||
if(selectedIndex!=-1){
|
||||
RecyclerView.ViewHolder holder=list.findViewHolderForAdapterPosition(selectedIndex);
|
||||
if(holder!=null && holder.itemView instanceof Checkable checkable)
|
||||
checkable.setChecked(false);
|
||||
}
|
||||
RecyclerView.ViewHolder holder=list.findViewHolderForAdapterPosition(index);
|
||||
if(holder!=null && holder.itemView instanceof Checkable checkable)
|
||||
checkable.setChecked(true);
|
||||
selectedIndex=index;
|
||||
}
|
||||
|
||||
private class AllLocalesAdapter extends RecyclerView.Adapter<SimpleLanguageViewHolder>{
|
||||
|
||||
@NonNull
|
||||
@Override
|
||||
public SimpleLanguageViewHolder onCreateViewHolder(@NonNull ViewGroup parent, int viewType){
|
||||
return new SimpleLanguageViewHolder();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onBindViewHolder(@NonNull SimpleLanguageViewHolder holder, int position){
|
||||
holder.bind(allLocales.get(position));
|
||||
}
|
||||
|
||||
@Override
|
||||
public int getItemCount(){
|
||||
return allLocales.size();
|
||||
}
|
||||
|
||||
@Override
|
||||
public int getItemViewType(int position){
|
||||
return 1;
|
||||
}
|
||||
}
|
||||
|
||||
private class SimpleLanguageViewHolder extends BindableViewHolder<LocaleInfo> implements UsableRecyclerView.Clickable{
|
||||
private final CheckedTextView text;
|
||||
|
||||
public SimpleLanguageViewHolder(){
|
||||
super(context, R.layout.item_alert_single_choice_1line, list);
|
||||
text=(CheckedTextView) itemView;
|
||||
text.setCompoundDrawablesRelativeWithIntrinsicBounds(new RadioButton(context).getButtonDrawable(), null, null, null);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onBind(LocaleInfo item){
|
||||
text.setText(item.displayName);
|
||||
text.setChecked(selectedIndex==getAbsoluteAdapterPosition());
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onClick(){
|
||||
selectItem(getAbsoluteAdapterPosition());
|
||||
selectedLocale=item.language;
|
||||
selectedEncoding=null;
|
||||
}
|
||||
}
|
||||
|
||||
private class SpecialLanguagesAdapter extends RecyclerView.Adapter<SpecialLanguageViewHolder>{
|
||||
|
||||
@NonNull
|
||||
@Override
|
||||
public SpecialLanguageViewHolder onCreateViewHolder(@NonNull ViewGroup parent, int viewType){
|
||||
return new SpecialLanguageViewHolder();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onBindViewHolder(@NonNull SpecialLanguageViewHolder holder, int position){
|
||||
holder.bind(specialLocales.get(position));
|
||||
}
|
||||
|
||||
@Override
|
||||
public int getItemCount(){
|
||||
return specialLocales.size();
|
||||
}
|
||||
|
||||
@Override
|
||||
public int getItemViewType(int position){
|
||||
return 2;
|
||||
}
|
||||
}
|
||||
|
||||
private class SpecialLanguageViewHolder extends BindableViewHolder<SpecialLocaleInfo> implements UsableRecyclerView.DisableableClickable{
|
||||
private final TextView text, title;
|
||||
private final CheckableLinearLayout view;
|
||||
|
||||
public SpecialLanguageViewHolder(){
|
||||
super(context, R.layout.item_alert_single_choice_2lines, list);
|
||||
text=findViewById(R.id.text);
|
||||
title=findViewById(R.id.title);
|
||||
view=((CheckableLinearLayout) itemView);
|
||||
findViewById(R.id.radiobutton).setBackground(new RadioButton(context).getButtonDrawable());
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onBind(SpecialLocaleInfo item){
|
||||
text.setText(item.displayName);
|
||||
if(!TextUtils.isEmpty(item.title)){
|
||||
title.setVisibility(View.VISIBLE);
|
||||
title.setText(item.title);
|
||||
}else{
|
||||
title.setVisibility(View.GONE);
|
||||
}
|
||||
text.setEnabled(item.enabled);
|
||||
view.setEnabled(item.enabled);
|
||||
view.setChecked(selectedIndex==getAbsoluteAdapterPosition());
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onClick(){
|
||||
selectItem(getAbsoluteAdapterPosition());
|
||||
selectedLocale=item.language;
|
||||
selectedEncoding = item.title.equals("bottom") ? "bottom" : null;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean isEnabled(){
|
||||
return item.enabled;
|
||||
}
|
||||
}
|
||||
|
||||
private static class LocaleInfo{
|
||||
public final MastodonLanguage language;
|
||||
public final String displayName;
|
||||
|
||||
private LocaleInfo(MastodonLanguage language, String displayName){
|
||||
this.language=language;
|
||||
this.displayName=displayName;
|
||||
}
|
||||
}
|
||||
|
||||
private static class SpecialLocaleInfo{
|
||||
public SpecialLocaleInfo() {}
|
||||
|
||||
public SpecialLocaleInfo(MastodonLanguage lang, String displayName, String title) {
|
||||
this.language = lang;
|
||||
this.displayName = displayName;
|
||||
this.title = title;
|
||||
}
|
||||
|
||||
public MastodonLanguage language;
|
||||
public String displayName;
|
||||
public String title;
|
||||
public boolean enabled=true;
|
||||
}
|
||||
|
||||
@Parcel
|
||||
public static class SelectedOption {
|
||||
public int index;
|
||||
public MastodonLanguage language;
|
||||
public String encoding;
|
||||
|
||||
public SelectedOption(){}
|
||||
|
||||
public SelectedOption(int index, MastodonLanguage language, String encoding) {
|
||||
this.index = index;
|
||||
this.language = language;
|
||||
this.encoding = encoding;
|
||||
}
|
||||
|
||||
public SelectedOption(int index, MastodonLanguage language) {
|
||||
this(index, language, null);
|
||||
}
|
||||
|
||||
public SelectedOption(MastodonLanguage language) {
|
||||
this(-1, language, null);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,793 @@
|
||||
package org.joinmastodon.android.ui.viewcontrollers;
|
||||
|
||||
import android.animation.Animator;
|
||||
import android.animation.AnimatorListenerAdapter;
|
||||
import android.animation.AnimatorSet;
|
||||
import android.animation.ObjectAnimator;
|
||||
import android.content.Context;
|
||||
import android.database.Cursor;
|
||||
import android.graphics.Bitmap;
|
||||
import android.graphics.drawable.ColorDrawable;
|
||||
import android.graphics.drawable.Drawable;
|
||||
import android.media.MediaMetadataRetriever;
|
||||
import android.net.Uri;
|
||||
import android.os.Build;
|
||||
import android.os.Bundle;
|
||||
import android.os.Parcelable;
|
||||
import android.provider.OpenableColumns;
|
||||
import android.text.TextUtils;
|
||||
import android.util.Log;
|
||||
import android.view.View;
|
||||
import android.view.ViewGroup;
|
||||
import android.widget.HorizontalScrollView;
|
||||
import android.widget.ImageButton;
|
||||
import android.widget.ImageView;
|
||||
import android.widget.LinearLayout;
|
||||
import android.widget.ProgressBar;
|
||||
import android.widget.TextView;
|
||||
import android.widget.Toast;
|
||||
|
||||
import org.joinmastodon.android.MastodonApp;
|
||||
import org.joinmastodon.android.R;
|
||||
import org.joinmastodon.android.api.MastodonAPIController;
|
||||
import org.joinmastodon.android.api.ProgressListener;
|
||||
import org.joinmastodon.android.api.requests.statuses.GetAttachmentByID;
|
||||
import org.joinmastodon.android.api.requests.statuses.UpdateAttachment;
|
||||
import org.joinmastodon.android.api.requests.statuses.UploadAttachment;
|
||||
import org.joinmastodon.android.fragments.ComposeFragment;
|
||||
import org.joinmastodon.android.fragments.ComposeImageDescriptionFragment;
|
||||
import org.joinmastodon.android.model.Attachment;
|
||||
import org.joinmastodon.android.model.Instance;
|
||||
import org.joinmastodon.android.ui.OutlineProviders;
|
||||
import org.joinmastodon.android.ui.drawables.EmptyDrawable;
|
||||
import org.joinmastodon.android.ui.utils.UiUtils;
|
||||
import org.joinmastodon.android.ui.views.ReorderableLinearLayout;
|
||||
import org.joinmastodon.android.utils.TransferSpeedTracker;
|
||||
import org.parceler.Parcel;
|
||||
import org.parceler.Parcels;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.HashMap;
|
||||
import java.util.List;
|
||||
import java.util.Locale;
|
||||
import java.util.Objects;
|
||||
import java.util.function.Consumer;
|
||||
import java.util.stream.Collectors;
|
||||
|
||||
import me.grishka.appkit.Nav;
|
||||
import me.grishka.appkit.api.Callback;
|
||||
import me.grishka.appkit.api.ErrorResponse;
|
||||
import me.grishka.appkit.imageloader.ViewImageLoader;
|
||||
import me.grishka.appkit.imageloader.requests.UrlImageLoaderRequest;
|
||||
import me.grishka.appkit.utils.CubicBezierInterpolator;
|
||||
import me.grishka.appkit.utils.V;
|
||||
|
||||
public class ComposeMediaViewController{
|
||||
private static final int MAX_ATTACHMENTS=4;
|
||||
private static final String TAG="ComposeMediaViewControl";
|
||||
|
||||
private final ComposeFragment fragment;
|
||||
|
||||
private ReorderableLinearLayout attachmentsView;
|
||||
private HorizontalScrollView attachmentsScroller;
|
||||
|
||||
private ArrayList<DraftMediaAttachment> attachments=new ArrayList<>();
|
||||
private boolean attachmentsErrorShowing;
|
||||
|
||||
public ComposeMediaViewController(ComposeFragment fragment){
|
||||
this.fragment=fragment;
|
||||
}
|
||||
|
||||
public void setView(View view, Bundle savedInstanceState){
|
||||
attachmentsView=view.findViewById(R.id.attachments);
|
||||
attachmentsScroller=view.findViewById(R.id.attachments_scroller);
|
||||
attachmentsView.setDividerDrawable(new EmptyDrawable(V.dp(8), 0));
|
||||
attachmentsView.setDragListener(new AttachmentDragListener());
|
||||
attachmentsView.setMoveInBothDimensions(true);
|
||||
|
||||
if(!fragment.getWasDetached() && savedInstanceState!=null && savedInstanceState.containsKey("attachments")){
|
||||
ArrayList<Parcelable> serializedAttachments=savedInstanceState.getParcelableArrayList("attachments");
|
||||
for(Parcelable a:serializedAttachments){
|
||||
DraftMediaAttachment att=Parcels.unwrap(a);
|
||||
attachmentsView.addView(createMediaAttachmentView(att));
|
||||
attachments.add(att);
|
||||
}
|
||||
attachmentsScroller.setVisibility(View.VISIBLE);
|
||||
updateMediaAttachmentsLayout();
|
||||
}else if(!attachments.isEmpty()){
|
||||
attachmentsScroller.setVisibility(View.VISIBLE);
|
||||
for(DraftMediaAttachment att:attachments){
|
||||
attachmentsView.addView(createMediaAttachmentView(att));
|
||||
}
|
||||
updateMediaAttachmentsLayout();
|
||||
}
|
||||
}
|
||||
|
||||
public void onViewCreated(Bundle savedInstanceState){
|
||||
if(savedInstanceState==null && !fragment.editingStatus.mediaAttachments.isEmpty()){
|
||||
attachmentsScroller.setVisibility(View.VISIBLE);
|
||||
for(Attachment att:fragment.editingStatus.mediaAttachments){
|
||||
DraftMediaAttachment da=new DraftMediaAttachment();
|
||||
da.serverAttachment=att;
|
||||
da.description=att.description;
|
||||
da.uri=att.previewUrl!=null ? Uri.parse(att.previewUrl) : null;
|
||||
da.state=AttachmentUploadState.DONE;
|
||||
attachmentsView.addView(createMediaAttachmentView(da));
|
||||
attachments.add(da);
|
||||
}
|
||||
updateMediaAttachmentsLayout();
|
||||
}
|
||||
}
|
||||
|
||||
public boolean addMediaAttachment(Uri uri, String description){
|
||||
if(getMediaAttachmentsCount()==MAX_ATTACHMENTS){
|
||||
showMediaAttachmentError(fragment.getResources().getQuantityString(R.plurals.cant_add_more_than_x_attachments, MAX_ATTACHMENTS, MAX_ATTACHMENTS));
|
||||
return false;
|
||||
}
|
||||
String type=fragment.getActivity().getContentResolver().getType(uri);
|
||||
int size;
|
||||
try(Cursor cursor=MastodonApp.context.getContentResolver().query(uri, new String[]{OpenableColumns.SIZE}, null, null, null)){
|
||||
cursor.moveToFirst();
|
||||
size=cursor.getInt(0);
|
||||
}catch(Exception x){
|
||||
Log.w("ComposeFragment", x);
|
||||
return false;
|
||||
}
|
||||
Instance instance=fragment.instance;
|
||||
if(instance!=null && instance.configuration!=null && instance.configuration.mediaAttachments!=null){
|
||||
if(instance.configuration.mediaAttachments.supportedMimeTypes!=null && !instance.configuration.mediaAttachments.supportedMimeTypes.contains(type)){
|
||||
showMediaAttachmentError(fragment.getString(R.string.media_attachment_unsupported_type, UiUtils.getFileName(uri)));
|
||||
return false;
|
||||
}
|
||||
if(!type.startsWith("image/")){
|
||||
int sizeLimit=instance.configuration.mediaAttachments.videoSizeLimit;
|
||||
if(size>sizeLimit){
|
||||
float mb=sizeLimit/(float) (1024*1024);
|
||||
String sMb=String.format(Locale.getDefault(), mb%1f==0f ? "%.0f" : "%.2f", mb);
|
||||
showMediaAttachmentError(fragment.getString(R.string.media_attachment_too_big, UiUtils.getFileName(uri), sMb));
|
||||
return false;
|
||||
}
|
||||
}
|
||||
}
|
||||
DraftMediaAttachment draft=new DraftMediaAttachment();
|
||||
draft.uri=uri;
|
||||
draft.mimeType=type;
|
||||
draft.description=description;
|
||||
draft.fileSize=size;
|
||||
|
||||
UiUtils.beginLayoutTransition(attachmentsScroller);
|
||||
attachmentsView.addView(createMediaAttachmentView(draft), new LinearLayout.LayoutParams(0, ViewGroup.LayoutParams.MATCH_PARENT));
|
||||
attachments.add(draft);
|
||||
attachmentsScroller.setVisibility(View.VISIBLE);
|
||||
updateMediaAttachmentsLayout();
|
||||
// draft.setOverlayVisible(true, false);
|
||||
|
||||
if(!areThereAnyUploadingAttachments()){
|
||||
uploadNextQueuedAttachment();
|
||||
}
|
||||
fragment.updatePublishButtonState();
|
||||
fragment.updateMediaPollStates();
|
||||
fragment.updateSensitive();
|
||||
return true;
|
||||
}
|
||||
|
||||
private void updateMediaAttachmentsLayout(){
|
||||
int newWidth=attachments.size()>2 ? ViewGroup.LayoutParams.WRAP_CONTENT : ViewGroup.LayoutParams.MATCH_PARENT;
|
||||
if(newWidth!=attachmentsView.getLayoutParams().width){
|
||||
attachmentsView.getLayoutParams().width=newWidth;
|
||||
attachmentsScroller.requestLayout();
|
||||
}
|
||||
for(DraftMediaAttachment att:attachments){
|
||||
LinearLayout.LayoutParams lp=(LinearLayout.LayoutParams) att.view.getLayoutParams();
|
||||
if(attachments.size()<3){
|
||||
lp.width=0;
|
||||
lp.weight=1f;
|
||||
}else{
|
||||
lp.width=V.dp(200);
|
||||
lp.weight=0f;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private void showMediaAttachmentError(String text){
|
||||
if(!attachmentsErrorShowing){
|
||||
Toast.makeText(fragment.getActivity(), text, Toast.LENGTH_SHORT).show();
|
||||
attachmentsErrorShowing=true;
|
||||
attachmentsView.postDelayed(()->attachmentsErrorShowing=false, 2000);
|
||||
}
|
||||
}
|
||||
|
||||
private View createMediaAttachmentView(DraftMediaAttachment draft){
|
||||
View thumb=fragment.getActivity().getLayoutInflater().inflate(R.layout.compose_media_thumb, attachmentsView, false);
|
||||
ImageView img=thumb.findViewById(R.id.thumb);
|
||||
if(draft.serverAttachment!=null){
|
||||
if(draft.serverAttachment.previewUrl!=null)
|
||||
ViewImageLoader.load(img, draft.serverAttachment.blurhashPlaceholder, new UrlImageLoaderRequest(draft.serverAttachment.previewUrl, V.dp(250), V.dp(250)));
|
||||
}else{
|
||||
if(draft.mimeType.startsWith("image/")){
|
||||
ViewImageLoader.load(img, null, new UrlImageLoaderRequest(draft.uri, V.dp(250), V.dp(250)));
|
||||
}else if(draft.mimeType.startsWith("video/")){
|
||||
loadVideoThumbIntoView(img, draft.uri);
|
||||
}
|
||||
}
|
||||
|
||||
draft.view=thumb;
|
||||
draft.imageView=img;
|
||||
draft.progressBar=thumb.findViewById(R.id.progress);
|
||||
draft.titleView=thumb.findViewById(R.id.title);
|
||||
draft.subtitleView=thumb.findViewById(R.id.subtitle);
|
||||
draft.removeButton=thumb.findViewById(R.id.delete);
|
||||
draft.editButton=thumb.findViewById(R.id.edit);
|
||||
draft.dragLayer=thumb.findViewById(R.id.drag_layer);
|
||||
|
||||
draft.removeButton.setTag(draft);
|
||||
draft.removeButton.setOnClickListener(this::onRemoveMediaAttachmentClick);
|
||||
draft.editButton.setTag(draft);
|
||||
|
||||
thumb.setOutlineProvider(OutlineProviders.roundedRect(12));
|
||||
thumb.setClipToOutline(true);
|
||||
img.setOutlineProvider(OutlineProviders.roundedRect(12));
|
||||
img.setClipToOutline(true);
|
||||
|
||||
thumb.setBackgroundColor(UiUtils.getThemeColor(fragment.getActivity(), R.attr.colorM3Surface));
|
||||
thumb.setOnLongClickListener(v->{
|
||||
if(!v.hasTransientState() && attachments.size()>1){
|
||||
attachmentsView.startDragging(v);
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
});
|
||||
thumb.setTag(draft);
|
||||
|
||||
|
||||
if(draft.fileSize>0){
|
||||
int subtitleRes=switch(Objects.requireNonNullElse(draft.mimeType, "").split("/")[0]){
|
||||
case "image" -> R.string.attachment_description_image;
|
||||
case "video" -> R.string.attachment_description_video;
|
||||
case "audio" -> R.string.attachment_description_audio;
|
||||
default -> R.string.attachment_description_unknown;
|
||||
};
|
||||
draft.subtitleView.setText(fragment.getString(subtitleRes, UiUtils.formatFileSize(fragment.getActivity(), draft.fileSize, true)));
|
||||
}else if(draft.serverAttachment!=null){
|
||||
int subtitleRes=switch(draft.serverAttachment.type){
|
||||
case IMAGE -> R.string.attachment_type_image;
|
||||
case VIDEO -> R.string.attachment_type_video;
|
||||
case GIFV -> R.string.attachment_type_gif;
|
||||
case AUDIO -> R.string.attachment_type_audio;
|
||||
case UNKNOWN -> R.string.attachment_type_unknown;
|
||||
};
|
||||
draft.subtitleView.setText(subtitleRes);
|
||||
}
|
||||
draft.titleView.setText(fragment.getString(R.string.attachment_x_percent_uploaded, 0));
|
||||
draft.removeButton.setImageResource(R.drawable.ic_baseline_close_24);
|
||||
|
||||
if(draft.state==AttachmentUploadState.ERROR){
|
||||
draft.titleView.setText(R.string.upload_failed);
|
||||
draft.editButton.setImageResource(R.drawable.ic_fluent_arrow_counterclockwise_24_regular);
|
||||
draft.editButton.setOnClickListener(this::onRetryOrCancelMediaUploadClick);
|
||||
draft.progressBar.setVisibility(View.GONE);
|
||||
draft.setUseErrorColors(true);
|
||||
}else if(draft.state==AttachmentUploadState.DONE){
|
||||
draft.setDescriptionToTitle();
|
||||
draft.progressBar.setVisibility(View.GONE);
|
||||
draft.editButton.setOnClickListener(this::onEditMediaDescriptionClick);
|
||||
}else{
|
||||
draft.editButton.setVisibility(View.GONE);
|
||||
draft.removeButton.setImageResource(R.drawable.ic_baseline_close_24);
|
||||
if(draft.state==AttachmentUploadState.PROCESSING){
|
||||
draft.titleView.setText(R.string.upload_processing);
|
||||
}else{
|
||||
draft.titleView.setText(fragment.getString(R.string.attachment_x_percent_uploaded, 0));
|
||||
}
|
||||
}
|
||||
|
||||
return thumb;
|
||||
}
|
||||
|
||||
public void addFakeMediaAttachment(Uri uri, String description){
|
||||
DraftMediaAttachment draft=new DraftMediaAttachment();
|
||||
draft.uri=uri;
|
||||
draft.description=description;
|
||||
draft.mimeType="image/jpeg";
|
||||
draft.fileSize=2473276;
|
||||
draft.state=AttachmentUploadState.PROCESSING;
|
||||
attachmentsView.addView(createMediaAttachmentView(draft));
|
||||
attachments.add(draft);
|
||||
attachmentsScroller.setVisibility(View.VISIBLE);
|
||||
updateMediaAttachmentsLayout();
|
||||
finishMediaAttachmentUpload(draft);
|
||||
}
|
||||
|
||||
private void uploadMediaAttachment(DraftMediaAttachment attachment){
|
||||
if(areThereAnyUploadingAttachments()){
|
||||
throw new IllegalStateException("there is already an attachment being uploaded");
|
||||
}
|
||||
attachment.state=AttachmentUploadState.UPLOADING;
|
||||
attachment.progressBar.setVisibility(View.VISIBLE);
|
||||
int maxSize=0;
|
||||
String contentType=fragment.getActivity().getContentResolver().getType(attachment.uri);
|
||||
if(contentType!=null && contentType.startsWith("image/")){
|
||||
maxSize=2_073_600; // TODO get this from instance configuration when it gets added there
|
||||
}
|
||||
attachment.progressBar.setProgress(0);
|
||||
attachment.speedTracker.reset();
|
||||
attachment.speedTracker.addSample(0);
|
||||
attachment.uploadRequest=(UploadAttachment) new UploadAttachment(attachment.uri, maxSize, attachment.description)
|
||||
.setProgressListener(new ProgressListener(){
|
||||
@Override
|
||||
public void onProgress(long transferred, long total){
|
||||
if(fragment.getActivity()==null)
|
||||
return;
|
||||
float progressFraction=transferred/(float)total;
|
||||
int progress=Math.round(progressFraction*attachment.progressBar.getMax());
|
||||
if(Build.VERSION.SDK_INT>=24)
|
||||
attachment.progressBar.setProgress(progress, true);
|
||||
else
|
||||
attachment.progressBar.setProgress(progress);
|
||||
|
||||
attachment.titleView.setText(fragment.getString(R.string.attachment_x_percent_uploaded, Math.round(progressFraction*100f)));
|
||||
|
||||
attachment.speedTracker.setTotalBytes(total);
|
||||
// attachment.uploadStateTitle.setText(fragment.getString(R.string.file_upload_progress, UiUtils.formatFileSize(fragment.getActivity(), transferred, true), UiUtils.formatFileSize(fragment.getActivity(), total, true)));
|
||||
attachment.speedTracker.addSample(transferred);
|
||||
}
|
||||
})
|
||||
.setCallback(new Callback<>(){
|
||||
@Override
|
||||
public void onSuccess(Attachment result){
|
||||
attachment.serverAttachment=result;
|
||||
if(TextUtils.isEmpty(result.url)){
|
||||
attachment.state=AttachmentUploadState.PROCESSING;
|
||||
attachment.processingPollingRunnable=()->pollForMediaAttachmentProcessing(attachment);
|
||||
if(fragment.getActivity()==null)
|
||||
return;
|
||||
attachment.titleView.setText(R.string.upload_processing);
|
||||
UiUtils.runOnUiThread(attachment.processingPollingRunnable, 1000);
|
||||
if(!areThereAnyUploadingAttachments())
|
||||
uploadNextQueuedAttachment();
|
||||
}else{
|
||||
finishMediaAttachmentUpload(attachment);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onError(ErrorResponse error){
|
||||
attachment.uploadRequest=null;
|
||||
attachment.state=AttachmentUploadState.ERROR;
|
||||
attachment.titleView.setText(R.string.upload_failed);
|
||||
// if(error instanceof MastodonErrorResponse er){
|
||||
// if(er.underlyingException instanceof SocketException || er.underlyingException instanceof UnknownHostException || er.underlyingException instanceof InterruptedIOException)
|
||||
// attachment.uploadStateText.setText(R.string.upload_error_connection_lost);
|
||||
// else
|
||||
// attachment.uploadStateText.setText(er.error);
|
||||
// }else{
|
||||
// attachment.uploadStateText.setText("");
|
||||
// }
|
||||
// attachment.retryButton.setImageResource(R.drawable.ic_fluent_arrow_clockwise_24_filled);
|
||||
// attachment.retryButton.setContentDescription(fragment.getString(R.string.retry_upload));
|
||||
|
||||
V.setVisibilityAnimated(attachment.editButton, View.VISIBLE);
|
||||
attachment.editButton.setImageResource(R.drawable.ic_fluent_arrow_counterclockwise_24_regular);
|
||||
attachment.editButton.setOnClickListener(ComposeMediaViewController.this::onRetryOrCancelMediaUploadClick);
|
||||
attachment.setUseErrorColors(true);
|
||||
V.setVisibilityAnimated(attachment.progressBar, View.GONE);
|
||||
|
||||
if(!areThereAnyUploadingAttachments())
|
||||
uploadNextQueuedAttachment();
|
||||
}
|
||||
})
|
||||
.exec(fragment.getAccountID());
|
||||
}
|
||||
|
||||
private void onRemoveMediaAttachmentClick(View v){
|
||||
DraftMediaAttachment att=(DraftMediaAttachment) v.getTag();
|
||||
if(att.isUploadingOrProcessing())
|
||||
att.cancelUpload();
|
||||
attachments.remove(att);
|
||||
if(!areThereAnyUploadingAttachments())
|
||||
uploadNextQueuedAttachment();
|
||||
if(!attachments.isEmpty())
|
||||
UiUtils.beginLayoutTransition(attachmentsScroller);
|
||||
attachmentsView.removeView(att.view);
|
||||
if(getMediaAttachmentsCount()==0){
|
||||
attachmentsScroller.setVisibility(View.GONE);
|
||||
}else{
|
||||
updateMediaAttachmentsLayout();
|
||||
}
|
||||
fragment.updatePublishButtonState();
|
||||
fragment.updateMediaPollStates();
|
||||
fragment.updateSensitive();
|
||||
}
|
||||
|
||||
private void onRetryOrCancelMediaUploadClick(View v){
|
||||
DraftMediaAttachment att=(DraftMediaAttachment) v.getTag();
|
||||
if(att.state==AttachmentUploadState.ERROR){
|
||||
// att.retryButton.setImageResource(R.drawable.ic_fluent_dismiss_24_filled);
|
||||
// att.retryButton.setContentDescription(fragment.getString(R.string.cancel));
|
||||
V.setVisibilityAnimated(att.progressBar, View.VISIBLE);
|
||||
V.setVisibilityAnimated(att.editButton, View.GONE);
|
||||
att.titleView.setText(fragment.getString(R.string.attachment_x_percent_uploaded, 0));
|
||||
att.state=AttachmentUploadState.QUEUED;
|
||||
att.setUseErrorColors(false);
|
||||
if(!areThereAnyUploadingAttachments()){
|
||||
uploadNextQueuedAttachment();
|
||||
}
|
||||
}else{
|
||||
onRemoveMediaAttachmentClick(v);
|
||||
}
|
||||
}
|
||||
|
||||
private void loadVideoThumbIntoView(ImageView target, Uri uri){
|
||||
MastodonAPIController.runInBackground(()->{
|
||||
Context context=fragment.getActivity();
|
||||
if(context==null)
|
||||
return;
|
||||
try{
|
||||
MediaMetadataRetriever mmr=new MediaMetadataRetriever();
|
||||
mmr.setDataSource(context, uri);
|
||||
Bitmap frame=mmr.getFrameAtTime(3_000_000);
|
||||
mmr.release();
|
||||
int size=Math.max(frame.getWidth(), frame.getHeight());
|
||||
int maxSize=V.dp(250);
|
||||
if(size>maxSize){
|
||||
float factor=maxSize/(float)size;
|
||||
frame=Bitmap.createScaledBitmap(frame, Math.round(frame.getWidth()*factor), Math.round(frame.getHeight()*factor), true);
|
||||
}
|
||||
Bitmap finalFrame=frame;
|
||||
target.post(()->target.setImageBitmap(finalFrame));
|
||||
}catch(Exception x){
|
||||
Log.w(TAG, "loadVideoThumbIntoView: error getting video frame", x);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
private void pollForMediaAttachmentProcessing(DraftMediaAttachment attachment){
|
||||
attachment.processingPollingRequest=(GetAttachmentByID) new GetAttachmentByID(attachment.serverAttachment.id)
|
||||
.setCallback(new Callback<>(){
|
||||
@Override
|
||||
public void onSuccess(Attachment result){
|
||||
attachment.processingPollingRequest=null;
|
||||
if(!TextUtils.isEmpty(result.url)){
|
||||
attachment.processingPollingRunnable=null;
|
||||
attachment.serverAttachment=result;
|
||||
finishMediaAttachmentUpload(attachment);
|
||||
}else if(fragment.getActivity()!=null){
|
||||
UiUtils.runOnUiThread(attachment.processingPollingRunnable, 1000);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onError(ErrorResponse error){
|
||||
attachment.processingPollingRequest=null;
|
||||
if(fragment.getActivity()!=null)
|
||||
UiUtils.runOnUiThread(attachment.processingPollingRunnable, 1000);
|
||||
}
|
||||
})
|
||||
.exec(fragment.getAccountID());
|
||||
}
|
||||
|
||||
private void finishMediaAttachmentUpload(DraftMediaAttachment attachment){
|
||||
if(attachment.state!=AttachmentUploadState.PROCESSING && attachment.state!=AttachmentUploadState.UPLOADING)
|
||||
throw new IllegalStateException("Unexpected state "+attachment.state);
|
||||
attachment.uploadRequest=null;
|
||||
attachment.state=AttachmentUploadState.DONE;
|
||||
attachment.editButton.setImageResource(R.drawable.ic_fluent_edit_24_regular);
|
||||
attachment.removeButton.setImageResource(R.drawable.ic_fluent_delete_24_regular);
|
||||
attachment.editButton.setOnClickListener(this::onEditMediaDescriptionClick);
|
||||
V.setVisibilityAnimated(attachment.progressBar, View.GONE);
|
||||
V.setVisibilityAnimated(attachment.editButton, View.VISIBLE);
|
||||
attachment.setDescriptionToTitle();
|
||||
if(!areThereAnyUploadingAttachments())
|
||||
uploadNextQueuedAttachment();
|
||||
fragment.updatePublishButtonState();
|
||||
}
|
||||
|
||||
private void uploadNextQueuedAttachment(){
|
||||
for(DraftMediaAttachment att:attachments){
|
||||
if(att.state==AttachmentUploadState.QUEUED){
|
||||
uploadMediaAttachment(att);
|
||||
return;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public boolean areThereAnyUploadingAttachments(){
|
||||
for(DraftMediaAttachment att:attachments){
|
||||
if(att.state==AttachmentUploadState.UPLOADING)
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
public boolean areAnyAttachmentsNotDone() {
|
||||
for(DraftMediaAttachment att:attachments){
|
||||
if(att.state!=AttachmentUploadState.DONE)
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
private void onEditMediaDescriptionClick(View v){
|
||||
DraftMediaAttachment att=(DraftMediaAttachment) v.getTag();
|
||||
if(att.serverAttachment==null)
|
||||
return;
|
||||
Bundle args=new Bundle();
|
||||
args.putString("account", fragment.getAccountID());
|
||||
args.putString("attachment", att.serverAttachment.id);
|
||||
args.putParcelable("uri", att.uri);
|
||||
args.putString("existingDescription", att.description);
|
||||
args.putString("attachmentType", att.serverAttachment.type.toString());
|
||||
Drawable img=att.imageView.getDrawable();
|
||||
if(img!=null){
|
||||
args.putInt("width", img.getIntrinsicWidth());
|
||||
args.putInt("height", img.getIntrinsicHeight());
|
||||
}
|
||||
Nav.goForResult(fragment.getActivity(), ComposeImageDescriptionFragment.class, args, ComposeFragment.IMAGE_DESCRIPTION_RESULT, fragment);
|
||||
}
|
||||
|
||||
public int getMediaAttachmentsCount(){
|
||||
return attachments.size();
|
||||
}
|
||||
|
||||
public void cancelAllUploads(){
|
||||
for(DraftMediaAttachment att:attachments){
|
||||
if(att.isUploadingOrProcessing())
|
||||
att.cancelUpload();
|
||||
}
|
||||
}
|
||||
|
||||
public void setAltTextByID(String attID, String text){
|
||||
for(DraftMediaAttachment att:attachments){
|
||||
if(att.serverAttachment.id.equals(attID)){
|
||||
att.descriptionSaved=false;
|
||||
att.description=text;
|
||||
att.setDescriptionToTitle();
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public List<String> getAttachmentIDs(){
|
||||
return attachments.stream().map(a->a.serverAttachment.id).collect(Collectors.toList());
|
||||
}
|
||||
|
||||
public boolean isEmpty(){
|
||||
return attachments.isEmpty();
|
||||
}
|
||||
|
||||
public boolean canAddMoreAttachments(){
|
||||
return attachments.size()<MAX_ATTACHMENTS;
|
||||
}
|
||||
|
||||
public int getMissingAltTextAttachmentCount(){
|
||||
int count=0;
|
||||
for(DraftMediaAttachment att:attachments){
|
||||
if(TextUtils.isEmpty(att.description))
|
||||
count++;
|
||||
}
|
||||
return count;
|
||||
}
|
||||
|
||||
public boolean areAllAttachmentsImages(){
|
||||
for(DraftMediaAttachment att:attachments){
|
||||
if((att.mimeType==null && att.serverAttachment.type==Attachment.Type.IMAGE) || (att.mimeType!=null && !att.mimeType.startsWith("image/")))
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
public int getMaxAttachments(){
|
||||
return MAX_ATTACHMENTS;
|
||||
}
|
||||
|
||||
public int getNonDoneAttachmentCount(){
|
||||
int nonDoneAttachmentCount=0;
|
||||
for(DraftMediaAttachment att:attachments){
|
||||
if(att.state!=AttachmentUploadState.DONE)
|
||||
nonDoneAttachmentCount++;
|
||||
}
|
||||
return nonDoneAttachmentCount;
|
||||
}
|
||||
|
||||
public void saveAltTextsBeforePublishing(Runnable onSuccess, Consumer<ErrorResponse> onError){
|
||||
ArrayList<UpdateAttachment> updateAltTextRequests=new ArrayList<>();
|
||||
for(DraftMediaAttachment att:attachments){
|
||||
if(!att.descriptionSaved){
|
||||
UpdateAttachment req=new UpdateAttachment(att.serverAttachment.id, att.description);
|
||||
req.setCallback(new Callback<>(){
|
||||
@Override
|
||||
public void onSuccess(Attachment result){
|
||||
att.descriptionSaved=true;
|
||||
att.serverAttachment=result;
|
||||
updateAltTextRequests.remove(req);
|
||||
if(updateAltTextRequests.isEmpty())
|
||||
onSuccess.run();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onError(ErrorResponse error){
|
||||
onError.accept(error);
|
||||
}
|
||||
})
|
||||
.exec(fragment.getAccountID());
|
||||
updateAltTextRequests.add(req);
|
||||
}
|
||||
}
|
||||
if(updateAltTextRequests.isEmpty())
|
||||
onSuccess.run();
|
||||
}
|
||||
|
||||
public void onSaveInstanceState(Bundle outState){
|
||||
if(!attachments.isEmpty()){
|
||||
ArrayList<Parcelable> serializedAttachments=new ArrayList<>(attachments.size());
|
||||
for(DraftMediaAttachment att:attachments){
|
||||
serializedAttachments.add(Parcels.wrap(att));
|
||||
}
|
||||
outState.putParcelableArrayList("attachments", serializedAttachments);
|
||||
}
|
||||
}
|
||||
|
||||
@Parcel
|
||||
static class DraftMediaAttachment{
|
||||
public Attachment serverAttachment;
|
||||
public Uri uri;
|
||||
public transient UploadAttachment uploadRequest;
|
||||
public transient GetAttachmentByID processingPollingRequest;
|
||||
public String description;
|
||||
public String mimeType;
|
||||
public AttachmentUploadState state=AttachmentUploadState.QUEUED;
|
||||
public int fileSize;
|
||||
public boolean descriptionSaved=true;
|
||||
|
||||
public transient View view;
|
||||
public transient ProgressBar progressBar;
|
||||
public transient ImageButton removeButton, editButton;
|
||||
public transient Runnable processingPollingRunnable;
|
||||
public transient ImageView imageView;
|
||||
public transient TextView titleView, subtitleView;
|
||||
public transient TransferSpeedTracker speedTracker=new TransferSpeedTracker();
|
||||
private transient boolean errorColors;
|
||||
private transient Animator errorTransitionAnimator;
|
||||
public transient View dragLayer;
|
||||
|
||||
public void cancelUpload(){
|
||||
switch(state){
|
||||
case UPLOADING -> {
|
||||
if(uploadRequest!=null){
|
||||
uploadRequest.cancel();
|
||||
uploadRequest=null;
|
||||
}
|
||||
}
|
||||
case PROCESSING -> {
|
||||
if(processingPollingRunnable!=null){
|
||||
UiUtils.removeCallbacks(processingPollingRunnable);
|
||||
processingPollingRunnable=null;
|
||||
}
|
||||
if(processingPollingRequest!=null){
|
||||
processingPollingRequest.cancel();
|
||||
processingPollingRequest=null;
|
||||
}
|
||||
}
|
||||
default -> throw new IllegalStateException("Unexpected state "+state);
|
||||
}
|
||||
}
|
||||
|
||||
public boolean isUploadingOrProcessing(){
|
||||
return state==AttachmentUploadState.UPLOADING || state==AttachmentUploadState.PROCESSING;
|
||||
}
|
||||
|
||||
public void setDescriptionToTitle(){
|
||||
if(TextUtils.isEmpty(description)){
|
||||
titleView.setText(R.string.add_alt_text);
|
||||
titleView.setTextColor(UiUtils.getThemeColor(titleView.getContext(), R.attr.colorM3OnSurfaceVariant));
|
||||
}else{
|
||||
titleView.setText(description);
|
||||
titleView.setTextColor(UiUtils.getThemeColor(titleView.getContext(), R.attr.colorM3OnSurface));
|
||||
}
|
||||
}
|
||||
|
||||
public void setUseErrorColors(boolean use){
|
||||
if(errorColors==use)
|
||||
return;
|
||||
errorColors=use;
|
||||
if(errorTransitionAnimator!=null)
|
||||
errorTransitionAnimator.cancel();
|
||||
AnimatorSet set=new AnimatorSet();
|
||||
int color1, color2, color3;
|
||||
if(use){
|
||||
color1=UiUtils.getThemeColor(view.getContext(), R.attr.colorM3ErrorContainer);
|
||||
color2=UiUtils.getThemeColor(view.getContext(), R.attr.colorM3Error);
|
||||
color3=UiUtils.getThemeColor(view.getContext(), R.attr.colorM3OnErrorContainer);
|
||||
}else{
|
||||
color1=UiUtils.getThemeColor(view.getContext(), R.attr.colorM3Surface);
|
||||
color2=UiUtils.getThemeColor(view.getContext(), R.attr.colorM3OnSurface);
|
||||
color3=UiUtils.getThemeColor(view.getContext(), R.attr.colorM3OnSurfaceVariant);
|
||||
}
|
||||
set.playTogether(
|
||||
ObjectAnimator.ofArgb(view, "backgroundColor", ((ColorDrawable)view.getBackground()).getColor(), color1),
|
||||
ObjectAnimator.ofArgb(titleView, "textColor", titleView.getCurrentTextColor(), color2),
|
||||
ObjectAnimator.ofArgb(subtitleView, "textColor", subtitleView.getCurrentTextColor(), color3),
|
||||
ObjectAnimator.ofArgb(removeButton.getDrawable(), "tint", subtitleView.getCurrentTextColor(), color3)
|
||||
);
|
||||
editButton.getDrawable().setTint(color3);
|
||||
set.setDuration(250);
|
||||
set.setInterpolator(CubicBezierInterpolator.DEFAULT);
|
||||
set.addListener(new AnimatorListenerAdapter(){
|
||||
@Override
|
||||
public void onAnimationEnd(Animator animation){
|
||||
errorTransitionAnimator=null;
|
||||
}
|
||||
});
|
||||
set.start();
|
||||
errorTransitionAnimator=set;
|
||||
}
|
||||
}
|
||||
|
||||
enum AttachmentUploadState{
|
||||
QUEUED,
|
||||
UPLOADING,
|
||||
PROCESSING,
|
||||
ERROR,
|
||||
DONE
|
||||
}
|
||||
|
||||
private class AttachmentDragListener implements ReorderableLinearLayout.OnDragListener{
|
||||
private final HashMap<View, Animator> currentAnimations=new HashMap<>();
|
||||
|
||||
@Override
|
||||
public void onSwapItems(int oldIndex, int newIndex){
|
||||
attachments.add(newIndex, attachments.remove(oldIndex));
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onDragStart(View view){
|
||||
if(currentAnimations.containsKey(view))
|
||||
currentAnimations.get(view).cancel();
|
||||
fragment.mainLayout.setClipChildren(false);
|
||||
AnimatorSet set=new AnimatorSet();
|
||||
DraftMediaAttachment att=(DraftMediaAttachment) view.getTag();
|
||||
att.dragLayer.setVisibility(View.VISIBLE);
|
||||
set.playTogether(
|
||||
ObjectAnimator.ofFloat(view, View.TRANSLATION_Z, V.dp(3)),
|
||||
ObjectAnimator.ofFloat(att.dragLayer, View.ALPHA, 0.16f)
|
||||
);
|
||||
set.setDuration(150);
|
||||
set.setInterpolator(CubicBezierInterpolator.DEFAULT);
|
||||
set.addListener(new AnimatorListenerAdapter(){
|
||||
@Override
|
||||
public void onAnimationEnd(Animator animation){
|
||||
currentAnimations.remove(view);
|
||||
}
|
||||
});
|
||||
currentAnimations.put(view, set);
|
||||
set.start();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onDragEnd(View view){
|
||||
if(currentAnimations.containsKey(view))
|
||||
currentAnimations.get(view).cancel();
|
||||
AnimatorSet set=new AnimatorSet();
|
||||
DraftMediaAttachment att=(DraftMediaAttachment) view.getTag();
|
||||
set.playTogether(
|
||||
ObjectAnimator.ofFloat(att.dragLayer, View.ALPHA, 0),
|
||||
ObjectAnimator.ofFloat(view, View.TRANSLATION_Z, 0),
|
||||
ObjectAnimator.ofFloat(view, View.TRANSLATION_X, 0),
|
||||
ObjectAnimator.ofFloat(view, View.TRANSLATION_Y, 0)
|
||||
);
|
||||
set.setDuration(200);
|
||||
set.setInterpolator(CubicBezierInterpolator.DEFAULT);
|
||||
set.addListener(new AnimatorListenerAdapter(){
|
||||
@Override
|
||||
public void onAnimationEnd(Animator animation){
|
||||
if(currentAnimations.isEmpty())
|
||||
fragment.mainLayout.setClipChildren(true);
|
||||
att.dragLayer.setVisibility(View.GONE);
|
||||
currentAnimations.remove(view);
|
||||
}
|
||||
});
|
||||
currentAnimations.put(view, set);
|
||||
set.start();
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,413 @@
|
||||
package org.joinmastodon.android.ui.viewcontrollers;
|
||||
|
||||
import android.animation.Animator;
|
||||
import android.animation.AnimatorListenerAdapter;
|
||||
import android.animation.AnimatorSet;
|
||||
import android.animation.ObjectAnimator;
|
||||
import android.app.AlertDialog;
|
||||
import android.graphics.RectF;
|
||||
import android.os.Bundle;
|
||||
import android.view.HapticFeedbackConstants;
|
||||
import android.view.LayoutInflater;
|
||||
import android.view.View;
|
||||
import android.view.ViewGroup;
|
||||
import android.widget.Checkable;
|
||||
import android.widget.EditText;
|
||||
import android.widget.ImageView;
|
||||
import android.widget.TextView;
|
||||
|
||||
import org.joinmastodon.android.R;
|
||||
import org.joinmastodon.android.api.requests.statuses.CreateStatus;
|
||||
import org.joinmastodon.android.fragments.ComposeFragment;
|
||||
import org.joinmastodon.android.model.Instance;
|
||||
import org.joinmastodon.android.model.Poll;
|
||||
import org.joinmastodon.android.ui.M3AlertDialogBuilder;
|
||||
import org.joinmastodon.android.ui.OutlineProviders;
|
||||
import org.joinmastodon.android.ui.drawables.EmptyDrawable;
|
||||
import org.joinmastodon.android.ui.text.LengthLimitHighlighter;
|
||||
import org.joinmastodon.android.ui.utils.SimpleTextWatcher;
|
||||
import org.joinmastodon.android.ui.utils.UiUtils;
|
||||
import org.joinmastodon.android.ui.views.CheckableLinearLayout;
|
||||
import org.joinmastodon.android.ui.views.ReorderableLinearLayout;
|
||||
|
||||
import java.time.temporal.ChronoUnit;
|
||||
import java.util.ArrayList;
|
||||
|
||||
import me.grishka.appkit.utils.CubicBezierInterpolator;
|
||||
import me.grishka.appkit.utils.V;
|
||||
|
||||
public class ComposePollViewController{
|
||||
private static final int[] POLL_LENGTH_OPTIONS={
|
||||
5*60,
|
||||
30*60,
|
||||
3600,
|
||||
6*3600,
|
||||
24*3600,
|
||||
3*24*3600,
|
||||
7*24*3600,
|
||||
};
|
||||
|
||||
private final ComposeFragment fragment;
|
||||
private ViewGroup pollWrap;
|
||||
|
||||
private ReorderableLinearLayout pollOptionsView;
|
||||
private View addPollOptionBtn;
|
||||
private ImageView deletePollOptionBtn;
|
||||
private ViewGroup pollSettingsView;
|
||||
private View pollPoof;
|
||||
private View pollDurationButton, pollStyleButton;
|
||||
private TextView pollDurationValue, pollStyleValue;
|
||||
|
||||
private int pollDuration=24*3600;
|
||||
private boolean pollIsMultipleChoice;
|
||||
private ArrayList<DraftPollOption> pollOptions=new ArrayList<>();
|
||||
private boolean pollChanged;
|
||||
|
||||
private int maxPollOptions=4;
|
||||
private int maxPollOptionLength=50;
|
||||
|
||||
public ComposePollViewController(ComposeFragment fragment){
|
||||
this.fragment=fragment;
|
||||
}
|
||||
|
||||
public void setView(View view, Bundle savedInstanceState){
|
||||
pollWrap=view.findViewById(R.id.poll_wrap);
|
||||
|
||||
Instance instance=fragment.instance;
|
||||
if(instance.configuration!=null && instance.configuration.polls!=null && instance.configuration.polls.maxOptions>0)
|
||||
maxPollOptions=instance.configuration.polls.maxOptions;
|
||||
if(instance.configuration!=null && instance.configuration.polls!=null && instance.configuration.polls.maxCharactersPerOption>0)
|
||||
maxPollOptionLength=instance.configuration.polls.maxCharactersPerOption;
|
||||
|
||||
pollOptionsView=pollWrap.findViewById(R.id.poll_options);
|
||||
addPollOptionBtn=pollWrap.findViewById(R.id.add_poll_option);
|
||||
deletePollOptionBtn=pollWrap.findViewById(R.id.delete_poll_option);
|
||||
pollSettingsView=pollWrap.findViewById(R.id.poll_settings);
|
||||
pollPoof=pollWrap.findViewById(R.id.poll_poof);
|
||||
|
||||
addPollOptionBtn.setOnClickListener(v->{
|
||||
createDraftPollOption(true).edit.requestFocus();
|
||||
updatePollOptionHints();
|
||||
});
|
||||
pollOptionsView.setMoveInBothDimensions(true);
|
||||
pollOptionsView.setDragListener(new OptionDragListener());
|
||||
pollOptionsView.setDividerDrawable(new EmptyDrawable(1, V.dp(8)));
|
||||
pollDurationButton=pollWrap.findViewById(R.id.poll_duration);
|
||||
pollDurationValue=pollWrap.findViewById(R.id.poll_duration_value);
|
||||
pollDurationButton.setOnClickListener(v->showPollDurationAlert());
|
||||
pollStyleButton=pollWrap.findViewById(R.id.poll_style);
|
||||
pollStyleValue=pollWrap.findViewById(R.id.poll_style_value);
|
||||
pollStyleButton.setOnClickListener(v->showPollStyleAlert());
|
||||
|
||||
if(!fragment.getWasDetached() && savedInstanceState!=null && savedInstanceState.containsKey("pollOptions")){ // Fragment was recreated without retaining instance
|
||||
pollWrap.setVisibility(View.VISIBLE);
|
||||
for(String oldText:savedInstanceState.getStringArrayList("pollOptions")){
|
||||
DraftPollOption opt=createDraftPollOption(false);
|
||||
opt.edit.setText(oldText);
|
||||
}
|
||||
updatePollOptionHints();
|
||||
pollDuration=savedInstanceState.getInt("pollDuration");
|
||||
pollIsMultipleChoice=savedInstanceState.getBoolean("pollMultiple");
|
||||
pollDurationValue.setText(UiUtils.formatDuration(fragment.getContext(), pollDuration));
|
||||
pollStyleValue.setText(pollIsMultipleChoice ? R.string.compose_poll_multiple_choice : R.string.compose_poll_single_choice);
|
||||
}else if(savedInstanceState!=null && !pollOptions.isEmpty()){ // Fragment was recreated but instance was retained
|
||||
pollWrap.setVisibility(View.VISIBLE);
|
||||
ArrayList<DraftPollOption> oldOptions=new ArrayList<>(pollOptions);
|
||||
pollOptions.clear();
|
||||
for(DraftPollOption oldOpt:oldOptions){
|
||||
DraftPollOption opt=createDraftPollOption(false);
|
||||
opt.edit.setText(oldOpt.edit.getText());
|
||||
}
|
||||
updatePollOptionHints();
|
||||
pollDurationValue.setText(UiUtils.formatDuration(fragment.getContext(), pollDuration));
|
||||
pollStyleValue.setText(pollIsMultipleChoice ? R.string.compose_poll_multiple_choice : R.string.compose_poll_single_choice);
|
||||
}else if(savedInstanceState==null && fragment.editingStatus!=null && fragment.editingStatus.poll!=null){
|
||||
pollWrap.setVisibility(View.VISIBLE);
|
||||
for(Poll.Option eopt:fragment.editingStatus.poll.options){
|
||||
DraftPollOption opt=createDraftPollOption(false);
|
||||
opt.edit.setText(eopt.title);
|
||||
}
|
||||
pollDuration=(int)fragment.editingStatus.poll.expiresAt.minus(fragment.editingStatus.createdAt.toEpochMilli(), ChronoUnit.MILLIS).getEpochSecond();
|
||||
updatePollOptionHints();
|
||||
pollDurationValue.setText(UiUtils.formatDuration(fragment.getContext(), pollDuration));
|
||||
pollIsMultipleChoice=fragment.editingStatus.poll.multiple;
|
||||
pollStyleValue.setText(pollIsMultipleChoice ? R.string.compose_poll_multiple_choice : R.string.compose_poll_single_choice);
|
||||
}else{
|
||||
pollDurationValue.setText(UiUtils.formatDuration(fragment.getContext(), 24*3600));
|
||||
pollStyleValue.setText(R.string.compose_poll_single_choice);
|
||||
}
|
||||
}
|
||||
|
||||
private DraftPollOption createDraftPollOption(boolean animated){
|
||||
DraftPollOption option=new DraftPollOption();
|
||||
option.view=LayoutInflater.from(fragment.getActivity()).inflate(R.layout.compose_poll_option, pollOptionsView, false);
|
||||
option.edit=option.view.findViewById(R.id.edit);
|
||||
option.dragger=option.view.findViewById(R.id.dragger_thingy);
|
||||
|
||||
option.dragger.setOnLongClickListener(v->{
|
||||
pollOptionsView.startDragging(option.view);
|
||||
return true;
|
||||
});
|
||||
option.edit.addTextChangedListener(new SimpleTextWatcher(e->{
|
||||
if(!fragment.isCreatingView())
|
||||
pollChanged=true;
|
||||
fragment.updatePublishButtonState();
|
||||
}));
|
||||
option.view.setOutlineProvider(OutlineProviders.roundedRect(4));
|
||||
option.view.setClipToOutline(true);
|
||||
option.view.setTag(option);
|
||||
|
||||
if(animated)
|
||||
UiUtils.beginLayoutTransition(pollWrap);
|
||||
pollOptionsView.addView(option.view);
|
||||
pollOptions.add(option);
|
||||
addPollOptionBtn.setEnabled(pollOptions.size()<maxPollOptions);
|
||||
option.edit.addTextChangedListener(new LengthLimitHighlighter(fragment.getActivity(), maxPollOptionLength).setListener(isOverLimit->{
|
||||
option.view.setForeground(fragment.getResources().getDrawable(isOverLimit ? R.drawable.bg_m3_outlined_text_field_error_nopad : R.drawable.bg_m3_outlined_text_field_nopad, fragment.getActivity().getTheme()));
|
||||
}));
|
||||
return option;
|
||||
}
|
||||
|
||||
private void updatePollOptionHints(){
|
||||
int i=0;
|
||||
for(DraftPollOption option:pollOptions){
|
||||
option.edit.setHint(fragment.getString(R.string.poll_option_hint, ++i));
|
||||
}
|
||||
}
|
||||
|
||||
private void onSwapPollOptions(int oldIndex, int newIndex){
|
||||
pollOptions.add(newIndex, pollOptions.remove(oldIndex));
|
||||
updatePollOptionHints();
|
||||
pollChanged=true;
|
||||
}
|
||||
|
||||
private void showPollDurationAlert(){
|
||||
String[] options=new String[POLL_LENGTH_OPTIONS.length];
|
||||
int selectedOption=-1;
|
||||
for(int i=0;i<POLL_LENGTH_OPTIONS.length;i++){
|
||||
int l=POLL_LENGTH_OPTIONS[i];
|
||||
options[i]=UiUtils.formatDuration(fragment.getContext(), l);
|
||||
if(l==pollDuration)
|
||||
selectedOption=i;
|
||||
}
|
||||
int[] chosenOption={0};
|
||||
new M3AlertDialogBuilder(fragment.getActivity())
|
||||
.setSingleChoiceItems(options, selectedOption, (dialog, which)->chosenOption[0]=which)
|
||||
.setTitle(R.string.poll_length)
|
||||
.setPositiveButton(R.string.ok, (dialog, which)->{
|
||||
pollDuration=POLL_LENGTH_OPTIONS[chosenOption[0]];
|
||||
pollDurationValue.setText(UiUtils.formatDuration(fragment.getContext(), pollDuration));
|
||||
})
|
||||
.setNegativeButton(R.string.cancel, null)
|
||||
.show();
|
||||
}
|
||||
|
||||
private void showPollStyleAlert(){
|
||||
final int[] option={pollIsMultipleChoice ? R.id.multiple_choice : R.id.single_choice};
|
||||
AlertDialog alert=new M3AlertDialogBuilder(fragment.getActivity())
|
||||
.setView(R.layout.poll_style)
|
||||
.setTitle(R.string.poll_style_title)
|
||||
.setPositiveButton(R.string.ok, (dlg, which)->{
|
||||
pollIsMultipleChoice=option[0]==R.id.multiple_choice;
|
||||
pollStyleValue.setText(pollIsMultipleChoice ? R.string.compose_poll_multiple_choice : R.string.compose_poll_single_choice);
|
||||
})
|
||||
.setNegativeButton(R.string.cancel, null)
|
||||
.show();
|
||||
CheckableLinearLayout multiple=alert.findViewById(R.id.multiple_choice);
|
||||
CheckableLinearLayout single=alert.findViewById(R.id.single_choice);
|
||||
single.setChecked(!pollIsMultipleChoice);
|
||||
multiple.setChecked(pollIsMultipleChoice);
|
||||
View.OnClickListener listener=v->{
|
||||
int id=v.getId();
|
||||
if(id==option[0])
|
||||
return;
|
||||
((Checkable) alert.findViewById(option[0])).setChecked(false);
|
||||
((Checkable) v).setChecked(true);
|
||||
option[0]=id;
|
||||
};
|
||||
single.setOnClickListener(listener);
|
||||
multiple.setOnClickListener(listener);
|
||||
}
|
||||
|
||||
public void onSaveInstanceState(Bundle outState){
|
||||
if(!pollOptions.isEmpty()){
|
||||
ArrayList<String> opts=new ArrayList<>();
|
||||
for(DraftPollOption opt:pollOptions){
|
||||
opts.add(opt.edit.getText().toString());
|
||||
}
|
||||
outState.putStringArrayList("pollOptions", opts);
|
||||
outState.putInt("pollDuration", pollDuration);
|
||||
outState.putBoolean("pollMultiple", pollIsMultipleChoice);
|
||||
}
|
||||
}
|
||||
|
||||
public boolean isEmpty(){
|
||||
return pollOptions.isEmpty();
|
||||
}
|
||||
|
||||
public int getNonEmptyOptionsCount(){
|
||||
int nonEmptyPollOptionsCount=0;
|
||||
for(DraftPollOption opt:pollOptions){
|
||||
if(opt.edit.length()>0)
|
||||
nonEmptyPollOptionsCount++;
|
||||
}
|
||||
return nonEmptyPollOptionsCount;
|
||||
}
|
||||
|
||||
public void toggle(){
|
||||
if(pollOptions.isEmpty()){
|
||||
pollWrap.setVisibility(View.VISIBLE);
|
||||
for(int i=0;i<2;i++)
|
||||
createDraftPollOption(false);
|
||||
updatePollOptionHints();
|
||||
}else{
|
||||
pollWrap.setVisibility(View.GONE);
|
||||
addPollOptionBtn.setVisibility(View.VISIBLE);
|
||||
pollOptionsView.removeAllViews();
|
||||
pollOptions.clear();
|
||||
pollDuration=24*3600;
|
||||
}
|
||||
}
|
||||
|
||||
public boolean isShown(){
|
||||
return !pollOptions.isEmpty();
|
||||
}
|
||||
|
||||
public boolean isPollChanged(){
|
||||
return pollChanged;
|
||||
}
|
||||
|
||||
public CreateStatus.Request.Poll getPollForRequest(){
|
||||
CreateStatus.Request.Poll poll=new CreateStatus.Request.Poll();
|
||||
poll.expiresIn=pollDuration;
|
||||
poll.multiple=pollIsMultipleChoice;
|
||||
for(DraftPollOption opt:pollOptions)
|
||||
poll.options.add(opt.edit.getText().toString());
|
||||
return poll;
|
||||
}
|
||||
|
||||
private static class DraftPollOption{
|
||||
public EditText edit;
|
||||
public View view;
|
||||
public View dragger;
|
||||
}
|
||||
|
||||
private class OptionDragListener implements ReorderableLinearLayout.OnDragListener{
|
||||
private boolean isOverDelete;
|
||||
private RectF rect1, rect2;
|
||||
private Animator deletionStateAnimator;
|
||||
|
||||
public OptionDragListener(){
|
||||
rect1=new RectF();
|
||||
rect2=new RectF();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onSwapItems(int oldIndex, int newIndex){
|
||||
onSwapPollOptions(oldIndex, newIndex);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onDragStart(View view){
|
||||
isOverDelete=false;
|
||||
ReorderableLinearLayout.OnDragListener.super.onDragStart(view);
|
||||
DraftPollOption dpo=(DraftPollOption) view.getTag();
|
||||
int color=UiUtils.getThemeColor(fragment.getActivity(), R.attr.colorM3OnSurface);
|
||||
ObjectAnimator anim=ObjectAnimator.ofArgb(dpo.edit, "backgroundColor", color & 0xffffff, color & 0x29ffffff);
|
||||
anim.setDuration(150);
|
||||
anim.setInterpolator(CubicBezierInterpolator.DEFAULT);
|
||||
anim.start();
|
||||
fragment.mainLayout.setClipChildren(false);
|
||||
if(pollOptions.size()>2){
|
||||
// UiUtils.beginLayoutTransition(pollSettingsView);
|
||||
deletePollOptionBtn.setVisibility(View.VISIBLE);
|
||||
addPollOptionBtn.setVisibility(View.GONE);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onDragEnd(View view){
|
||||
if(pollOptions.size()>2){
|
||||
// UiUtils.beginLayoutTransition(pollSettingsView);
|
||||
deletePollOptionBtn.setVisibility(View.GONE);
|
||||
addPollOptionBtn.setVisibility(View.VISIBLE);
|
||||
}
|
||||
|
||||
DraftPollOption dpo=(DraftPollOption) view.getTag();
|
||||
if(isOverDelete){
|
||||
pollPoof.setVisibility(View.VISIBLE);
|
||||
AnimatorSet set=new AnimatorSet();
|
||||
set.playTogether(
|
||||
ObjectAnimator.ofFloat(pollPoof, View.ALPHA, 0f, 0.7f, 1f, 0f),
|
||||
ObjectAnimator.ofFloat(pollPoof, View.SCALE_X, 1f, 4f),
|
||||
ObjectAnimator.ofFloat(pollPoof, View.SCALE_Y, 1f, 4f),
|
||||
ObjectAnimator.ofFloat(pollPoof, View.ROTATION, 0f, 60f)
|
||||
);
|
||||
set.setDuration(600);
|
||||
set.setInterpolator(CubicBezierInterpolator.DEFAULT);
|
||||
set.addListener(new AnimatorListenerAdapter(){
|
||||
@Override
|
||||
public void onAnimationEnd(Animator animation){
|
||||
pollPoof.setVisibility(View.INVISIBLE);
|
||||
}
|
||||
});
|
||||
set.start();
|
||||
UiUtils.beginLayoutTransition(pollWrap);
|
||||
pollOptions.remove(dpo);
|
||||
pollOptionsView.removeView(view);
|
||||
addPollOptionBtn.setEnabled(pollOptions.size()<maxPollOptions);
|
||||
return;
|
||||
}
|
||||
ReorderableLinearLayout.OnDragListener.super.onDragEnd(view);
|
||||
int color=UiUtils.getThemeColor(fragment.getActivity(), R.attr.colorM3OnSurface);
|
||||
ObjectAnimator anim=ObjectAnimator.ofArgb(dpo.edit, "backgroundColor", color & 0x29ffffff, color & 0xffffff);
|
||||
anim.setDuration(200);
|
||||
anim.setInterpolator(CubicBezierInterpolator.DEFAULT);
|
||||
anim.addListener(new AnimatorListenerAdapter(){
|
||||
@Override
|
||||
public void onAnimationEnd(Animator animation){
|
||||
fragment.mainLayout.setClipChildren(true);
|
||||
}
|
||||
});
|
||||
anim.start();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onDragMove(View view){
|
||||
if(pollOptions.size()<3)
|
||||
return;
|
||||
DraftPollOption dpo=(DraftPollOption) view.getTag();
|
||||
// Yes, I don't like this either.
|
||||
float draggerX=view.getX()+dpo.dragger.getX()+pollOptionsView.getX();
|
||||
float deleteButtonX=pollSettingsView.getX()+deletePollOptionBtn.getX();
|
||||
rect1.set(deleteButtonX, pollOptionsView.getHeight(), deleteButtonX+deletePollOptionBtn.getWidth(), pollWrap.getHeight());
|
||||
rect2.set(draggerX, view.getY(), draggerX+dpo.dragger.getWidth(), view.getY()+view.getHeight());
|
||||
boolean newOverDelete=rect1.intersect(rect2);
|
||||
if(newOverDelete!=isOverDelete){
|
||||
if(deletionStateAnimator!=null)
|
||||
deletionStateAnimator.cancel();
|
||||
isOverDelete=newOverDelete;
|
||||
if(newOverDelete)
|
||||
view.performHapticFeedback(HapticFeedbackConstants.KEYBOARD_TAP);
|
||||
dpo.view.setForeground(fragment.getResources().getDrawable(newOverDelete || dpo.edit.length()>maxPollOptionLength ? R.drawable.bg_m3_outlined_text_field_error_nopad : R.drawable.bg_m3_outlined_text_field_nopad, fragment.getActivity().getTheme()));
|
||||
int errorContainer=UiUtils.getThemeColor(fragment.getActivity(), R.attr.colorM3ErrorContainer);
|
||||
int surface=UiUtils.getThemeColor(fragment.getActivity(), R.attr.colorM3Surface);
|
||||
AnimatorSet set=new AnimatorSet();
|
||||
set.playTogether(
|
||||
ObjectAnimator.ofFloat(view, View.ALPHA, newOverDelete ? .85f : 1),
|
||||
ObjectAnimator.ofArgb(view, "backgroundColor", newOverDelete ? surface : errorContainer, newOverDelete ? errorContainer : surface)
|
||||
);
|
||||
set.setDuration(150);
|
||||
set.setInterpolator(CubicBezierInterpolator.DEFAULT);
|
||||
deletionStateAnimator=set;
|
||||
set.addListener(new AnimatorListenerAdapter(){
|
||||
@Override
|
||||
public void onAnimationEnd(Animator animation){
|
||||
deletionStateAnimator=null;
|
||||
}
|
||||
});
|
||||
set.start();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,353 @@
|
||||
package org.joinmastodon.android.ui.viewholders;
|
||||
|
||||
import android.annotation.SuppressLint;
|
||||
import android.app.Activity;
|
||||
import android.app.Fragment;
|
||||
import android.content.Intent;
|
||||
import android.graphics.drawable.Animatable;
|
||||
import android.graphics.drawable.Drawable;
|
||||
import android.os.Bundle;
|
||||
import android.text.SpannableStringBuilder;
|
||||
import android.text.style.TypefaceSpan;
|
||||
import android.view.Menu;
|
||||
import android.view.MenuItem;
|
||||
import android.view.View;
|
||||
import android.view.ViewGroup;
|
||||
import android.widget.CheckBox;
|
||||
import android.widget.FrameLayout;
|
||||
import android.widget.ImageView;
|
||||
import android.widget.PopupMenu;
|
||||
import android.widget.ProgressBar;
|
||||
import android.widget.RadioButton;
|
||||
import android.widget.TextView;
|
||||
|
||||
import org.joinmastodon.android.R;
|
||||
import org.joinmastodon.android.api.requests.accounts.SetAccountFollowed;
|
||||
import org.joinmastodon.android.api.session.AccountSessionManager;
|
||||
import org.joinmastodon.android.fragments.ProfileFragment;
|
||||
import org.joinmastodon.android.fragments.report.ReportReasonChoiceFragment;
|
||||
import org.joinmastodon.android.model.Account;
|
||||
import org.joinmastodon.android.model.Relationship;
|
||||
import org.joinmastodon.android.model.viewmodel.AccountViewModel;
|
||||
import org.joinmastodon.android.ui.OutlineProviders;
|
||||
import org.joinmastodon.android.ui.text.HtmlParser;
|
||||
import org.joinmastodon.android.ui.utils.UiUtils;
|
||||
import org.joinmastodon.android.ui.views.CheckableRelativeLayout;
|
||||
import org.joinmastodon.android.ui.views.ProgressBarButton;
|
||||
import org.parceler.Parcels;
|
||||
|
||||
import java.util.HashMap;
|
||||
import java.util.Objects;
|
||||
import java.util.Optional;
|
||||
import java.util.function.Consumer;
|
||||
|
||||
import me.grishka.appkit.Nav;
|
||||
import me.grishka.appkit.api.Callback;
|
||||
import me.grishka.appkit.api.ErrorResponse;
|
||||
import me.grishka.appkit.imageloader.ImageLoaderViewHolder;
|
||||
import me.grishka.appkit.utils.BindableViewHolder;
|
||||
import me.grishka.appkit.views.UsableRecyclerView;
|
||||
|
||||
public class AccountViewHolder extends BindableViewHolder<AccountViewModel> implements ImageLoaderViewHolder, UsableRecyclerView.Clickable, UsableRecyclerView.LongClickable{
|
||||
private final TextView name, username, followers, pronouns, bio;
|
||||
private final ImageView avatar;
|
||||
private final FrameLayout accessory;
|
||||
private final ProgressBarButton button;
|
||||
private final PopupMenu contextMenu;
|
||||
private final View menuAnchor;
|
||||
private final TypefaceSpan mediumSpan=new TypefaceSpan("sans-serif-medium");
|
||||
private final CheckableRelativeLayout view;
|
||||
private final View checkbox;
|
||||
private final ProgressBar actionProgress;
|
||||
|
||||
private final String accountID;
|
||||
private final Fragment fragment;
|
||||
private final HashMap<String, Relationship> relationships;
|
||||
|
||||
private Consumer<AccountViewHolder> onClick;
|
||||
private AccessoryType accessoryType;
|
||||
private boolean showBio;
|
||||
private boolean checked;
|
||||
|
||||
public AccountViewHolder(Fragment fragment, ViewGroup list, HashMap<String, Relationship> relationships){
|
||||
super(fragment.getActivity(), R.layout.item_account_list, list);
|
||||
this.fragment=fragment;
|
||||
this.accountID=Objects.requireNonNull(fragment.getArguments().getString("account"));
|
||||
this.relationships=relationships;
|
||||
|
||||
view=(CheckableRelativeLayout) itemView;
|
||||
name=findViewById(R.id.name);
|
||||
username=findViewById(R.id.username);
|
||||
avatar=findViewById(R.id.avatar);
|
||||
accessory=findViewById(R.id.accessory);
|
||||
button=findViewById(R.id.button);
|
||||
menuAnchor=findViewById(R.id.menu_anchor);
|
||||
followers=findViewById(R.id.followers_count);
|
||||
pronouns=findViewById(R.id.pronouns);
|
||||
bio=findViewById(R.id.bio);
|
||||
checkbox=findViewById(R.id.checkbox);
|
||||
actionProgress=findViewById(R.id.action_progress);
|
||||
|
||||
avatar.setOutlineProvider(OutlineProviders.roundedRect(10));
|
||||
avatar.setClipToOutline(true);
|
||||
|
||||
button.setOnClickListener(this::onButtonClick);
|
||||
accessory.setOnClickListener(v -> button.performClick());
|
||||
|
||||
contextMenu=new PopupMenu(fragment.getActivity(), menuAnchor);
|
||||
contextMenu.inflate(R.menu.profile);
|
||||
contextMenu.setOnMenuItemClickListener(this::onContextMenuItemSelected);
|
||||
|
||||
setStyle(AccessoryType.BUTTON, false);
|
||||
}
|
||||
|
||||
@SuppressLint("SetTextI18n")
|
||||
@Override
|
||||
public void onBind(AccountViewModel item){
|
||||
name.setText(item.parsedName);
|
||||
username.setText("@"+item.account.acct);
|
||||
long num=item.account.followersCount > -1 ? item.account.followersCount : item.account.followingCount;
|
||||
String followersStr = fragment.getResources().getQuantityString(item.account.followersCount > -1
|
||||
? R.plurals.x_followers : R.plurals.x_following, num>1000 ? 999 : (int)num);
|
||||
String followersNum=UiUtils.abbreviateNumber(num);
|
||||
int index=followersStr.indexOf("%,d");
|
||||
followersStr=followersStr.replace("%,d", followersNum);
|
||||
SpannableStringBuilder followersFormatted=new SpannableStringBuilder(followersStr);
|
||||
if(index!=-1){
|
||||
followersFormatted.setSpan(mediumSpan, index, index+followersNum.length(), 0);
|
||||
}
|
||||
if (item.account.followingCount > -1 || item.account.followersCount > -1) {
|
||||
followers.setVisibility(View.VISIBLE);
|
||||
followers.setText(followersFormatted);
|
||||
} else {
|
||||
followers.setVisibility(View.GONE);
|
||||
}
|
||||
|
||||
// you know what's cooler than followers or verified links? yep. pronouns
|
||||
Optional<String> pronounsString = UiUtils.extractPronouns(itemView.getContext(), item.account);
|
||||
pronouns.setVisibility(pronounsString.isPresent() ? View.VISIBLE : View.GONE);
|
||||
pronounsString.ifPresent(p -> HtmlParser.setTextWithCustomEmoji(pronouns, p, item.account.emojis));
|
||||
|
||||
/* unused in megalodon
|
||||
boolean hasVerifiedLink=item.verifiedLink!=null;
|
||||
if(!hasVerifiedLink)
|
||||
verifiedLink.setText(R.string.no_verified_link);
|
||||
else
|
||||
verifiedLink.setText(item.verifiedLink);
|
||||
verifiedLink.setCompoundDrawablesRelativeWithIntrinsicBounds(hasVerifiedLink ? R.drawable.ic_fluent_checkmark_16_filled : R.drawable.ic_help_16px, 0, 0, 0);
|
||||
int tintColor=UiUtils.getThemeColor(fragment.getActivity(), hasVerifiedLink ? R.attr.colorM3Primary : R.attr.colorM3Secondary);
|
||||
verifiedLink.setTextColor(tintColor);
|
||||
verifiedLink.setCompoundDrawableTintList(ColorStateList.valueOf(tintColor));
|
||||
*/
|
||||
bindRelationship();
|
||||
if(showBio){
|
||||
bio.setText(item.parsedBio);
|
||||
}
|
||||
}
|
||||
|
||||
public void bindRelationship(){
|
||||
if(relationships==null || accessoryType!=AccessoryType.BUTTON)
|
||||
return;
|
||||
Relationship rel=relationships.get(item.account.id);
|
||||
if(rel==null || AccountSessionManager.getInstance().isSelf(accountID, item.account)){
|
||||
button.setVisibility(View.GONE);
|
||||
}else{
|
||||
button.setVisibility(View.VISIBLE);
|
||||
UiUtils.setRelationshipToActionButtonM3(rel, button);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void setImage(int index, Drawable image){
|
||||
if(index==0){
|
||||
avatar.setImageDrawable(image);
|
||||
}else{
|
||||
item.emojiHelper.setImageDrawable(index-1, image);
|
||||
name.invalidate();
|
||||
bio.invalidate();
|
||||
}
|
||||
|
||||
if(image instanceof Animatable a && !a.isRunning())
|
||||
a.start();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void clearImage(int index){
|
||||
if(index==0){
|
||||
avatar.setImageResource(R.drawable.image_placeholder);
|
||||
}else{
|
||||
setImage(index, null);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onClick(){
|
||||
if(onClick!=null){
|
||||
onClick.accept(this);
|
||||
return;
|
||||
}
|
||||
Bundle args=new Bundle();
|
||||
args.putString("account", accountID);
|
||||
args.putParcelable("profileAccount", Parcels.wrap(item.account));
|
||||
Nav.go(fragment.getActivity(), ProfileFragment.class, args);
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean onLongClick(){
|
||||
return false;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean onLongClick(float x, float y){
|
||||
if(relationships==null)
|
||||
return false;
|
||||
Relationship relationship=relationships.get(item.account.id);
|
||||
if(relationship==null)
|
||||
return false;
|
||||
Menu menu=contextMenu.getMenu();
|
||||
Account account=item.account;
|
||||
|
||||
menu.findItem(R.id.share).setTitle(fragment.getString(R.string.share_user, account.getDisplayUsername()));
|
||||
menu.findItem(R.id.mute).setTitle(fragment.getString(relationship.muting ? R.string.unmute_user : R.string.mute_user, account.getDisplayUsername()));
|
||||
menu.findItem(R.id.block).setTitle(fragment.getString(relationship.blocking ? R.string.unblock_user : R.string.block_user, account.getDisplayUsername()));
|
||||
menu.findItem(R.id.report).setTitle(fragment.getString(R.string.report_user, account.getDisplayUsername()));
|
||||
MenuItem hideBoosts=menu.findItem(R.id.hide_boosts);
|
||||
if(relationship.following){
|
||||
hideBoosts.setTitle(fragment.getString(relationship.showingReblogs ? R.string.hide_boosts_from_user : R.string.show_boosts_from_user, account.getDisplayUsername()));
|
||||
hideBoosts.setVisible(true);
|
||||
}else{
|
||||
hideBoosts.setVisible(false);
|
||||
}
|
||||
MenuItem blockDomain=menu.findItem(R.id.block_domain);
|
||||
if(!account.isLocal()){
|
||||
blockDomain.setTitle(fragment.getString(relationship.domainBlocking ? R.string.unblock_domain : R.string.block_domain, account.getDomain()));
|
||||
blockDomain.setVisible(true);
|
||||
}else{
|
||||
blockDomain.setVisible(false);
|
||||
}
|
||||
|
||||
menuAnchor.setTranslationX(x);
|
||||
menuAnchor.setTranslationY(y);
|
||||
contextMenu.show();
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
private void onButtonClick(View v){
|
||||
if(relationships==null)
|
||||
return;
|
||||
itemView.setHasTransientState(true);
|
||||
UiUtils.performAccountAction((Activity) v.getContext(), item.account, accountID, relationships.get(item.account.id), button, this::setActionProgressVisible, rel->{
|
||||
itemView.setHasTransientState(false);
|
||||
relationships.put(item.account.id, rel);
|
||||
bindRelationship();
|
||||
});
|
||||
}
|
||||
|
||||
private void setActionProgressVisible(boolean visible){
|
||||
if(visible)
|
||||
actionProgress.setIndeterminateTintList(button.getTextColors());
|
||||
// TODO button.setTextVisible(!visible);
|
||||
actionProgress.setVisibility(visible ? View.VISIBLE : View.GONE);
|
||||
button.setClickable(!visible);
|
||||
}
|
||||
|
||||
private boolean onContextMenuItemSelected(MenuItem item){
|
||||
Relationship relationship=relationships.get(this.item.account.id);
|
||||
if(relationship==null)
|
||||
return false;
|
||||
Account account=this.item.account;
|
||||
|
||||
int id=item.getItemId();
|
||||
if(id==R.id.share){
|
||||
Intent intent=new Intent(Intent.ACTION_SEND);
|
||||
intent.setType("text/plain");
|
||||
intent.putExtra(Intent.EXTRA_TEXT, account.url);
|
||||
fragment.startActivity(Intent.createChooser(intent, item.getTitle()));
|
||||
}else if(id==R.id.mute){
|
||||
UiUtils.confirmToggleMuteUser(fragment.getActivity(), accountID, account, relationship.muting, this::updateRelationship);
|
||||
}else if(id==R.id.block){
|
||||
UiUtils.confirmToggleBlockUser(fragment.getActivity(), accountID, account, relationship.blocking, this::updateRelationship);
|
||||
}else if(id==R.id.report){
|
||||
Bundle args=new Bundle();
|
||||
args.putString("account", accountID);
|
||||
args.putParcelable("reportAccount", Parcels.wrap(account));
|
||||
args.putParcelable("relationship", Parcels.wrap(relationship));
|
||||
Nav.go(fragment.getActivity(), ReportReasonChoiceFragment.class, args);
|
||||
}else if(id==R.id.open_in_browser){
|
||||
UiUtils.launchWebBrowser(fragment.getActivity(), account.url);
|
||||
}else if(id==R.id.block_domain){
|
||||
UiUtils.confirmToggleBlockDomain(fragment.getActivity(), accountID, account.getDomain(), relationship.domainBlocking, ()->{
|
||||
relationship.domainBlocking=!relationship.domainBlocking;
|
||||
bindRelationship();
|
||||
});
|
||||
}else if(id==R.id.hide_boosts){
|
||||
new SetAccountFollowed(account.id, true, !relationship.showingReblogs)
|
||||
.setCallback(new Callback<>(){
|
||||
@Override
|
||||
public void onSuccess(Relationship result){
|
||||
relationships.put(AccountViewHolder.this.item.account.id, result);
|
||||
bindRelationship();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onError(ErrorResponse error){
|
||||
error.showToast(fragment.getActivity());
|
||||
}
|
||||
})
|
||||
.wrapProgress(fragment.getActivity(), R.string.loading, false)
|
||||
.exec(accountID);
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
private void updateRelationship(Relationship r){
|
||||
relationships.put(item.account.id, r);
|
||||
bindRelationship();
|
||||
}
|
||||
|
||||
public void setOnClickListener(Consumer<AccountViewHolder> listener){
|
||||
onClick=listener;
|
||||
}
|
||||
|
||||
public void setStyle(AccessoryType accessoryType, boolean showBio){
|
||||
if(accessoryType!=this.accessoryType){
|
||||
this.accessoryType=accessoryType;
|
||||
switch(accessoryType){
|
||||
case NONE -> {
|
||||
button.setVisibility(View.GONE);
|
||||
checkbox.setVisibility(View.GONE);
|
||||
}
|
||||
case CHECKBOX -> {
|
||||
button.setVisibility(View.GONE);
|
||||
checkbox.setVisibility(View.VISIBLE);
|
||||
checkbox.setBackground(new CheckBox(checkbox.getContext()).getButtonDrawable());
|
||||
}
|
||||
case RADIOBUTTON -> {
|
||||
button.setVisibility(View.GONE);
|
||||
checkbox.setVisibility(View.VISIBLE);
|
||||
checkbox.setBackground(new RadioButton(checkbox.getContext()).getButtonDrawable());
|
||||
}
|
||||
case BUTTON -> {
|
||||
button.setVisibility(View.VISIBLE);
|
||||
checkbox.setVisibility(View.GONE);
|
||||
}
|
||||
}
|
||||
view.setCheckable(accessoryType==AccessoryType.CHECKBOX || accessoryType==AccessoryType.RADIOBUTTON);
|
||||
}
|
||||
this.showBio=showBio;
|
||||
bio.setVisibility(showBio ? View.VISIBLE : View.GONE);
|
||||
}
|
||||
|
||||
public void setChecked(boolean checked){
|
||||
this.checked=checked;
|
||||
view.setChecked(checked);
|
||||
}
|
||||
|
||||
public enum AccessoryType{
|
||||
NONE,
|
||||
BUTTON,
|
||||
CHECKBOX,
|
||||
RADIOBUTTON
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,23 @@
|
||||
package org.joinmastodon.android.ui.viewholders;
|
||||
|
||||
import android.content.Context;
|
||||
import android.view.ViewGroup;
|
||||
|
||||
import org.joinmastodon.android.R;
|
||||
import org.joinmastodon.android.model.viewmodel.CheckableListItem;
|
||||
import org.joinmastodon.android.ui.views.CheckableLinearLayout;
|
||||
|
||||
public abstract class CheckableListItemViewHolder extends ListItemViewHolder<CheckableListItem<?>>{
|
||||
protected final CheckableLinearLayout checkableLayout;
|
||||
|
||||
public CheckableListItemViewHolder(Context context, ViewGroup parent){
|
||||
super(context, R.layout.item_generic_list_checkable, parent);
|
||||
checkableLayout=(CheckableLinearLayout) itemView;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onBind(CheckableListItem<?> item){
|
||||
super.onBind(item);
|
||||
checkableLayout.setChecked(item.checked);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,25 @@
|
||||
package org.joinmastodon.android.ui.viewholders;
|
||||
|
||||
import android.content.Context;
|
||||
import android.view.View;
|
||||
import android.view.ViewGroup;
|
||||
import android.widget.CheckBox;
|
||||
import android.widget.CompoundButton;
|
||||
import android.widget.LinearLayout;
|
||||
import android.widget.RadioButton;
|
||||
|
||||
import me.grishka.appkit.utils.V;
|
||||
|
||||
public class CheckboxOrRadioListItemViewHolder extends CheckableListItemViewHolder{
|
||||
public CheckboxOrRadioListItemViewHolder(Context context, ViewGroup parent, boolean radio){
|
||||
super(context, parent);
|
||||
View iconView=new View(context);
|
||||
iconView.setDuplicateParentStateEnabled(true);
|
||||
CompoundButton terribleHack=radio ? new RadioButton(context) : new CheckBox(context);
|
||||
iconView.setBackground(terribleHack.getButtonDrawable());
|
||||
LinearLayout.LayoutParams lp=new LinearLayout.LayoutParams(V.dp(32), V.dp(32));
|
||||
lp.setMarginStart(V.dp(12));
|
||||
lp.setMarginEnd(V.dp(4));
|
||||
checkableLayout.addView(iconView, lp);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,36 @@
|
||||
package org.joinmastodon.android.ui.viewholders;
|
||||
|
||||
import android.annotation.SuppressLint;
|
||||
import android.view.ViewGroup;
|
||||
import android.widget.TextView;
|
||||
|
||||
import org.joinmastodon.android.R;
|
||||
import org.joinmastodon.android.model.Instance;
|
||||
import org.joinmastodon.android.ui.text.HtmlParser;
|
||||
|
||||
import me.grishka.appkit.utils.BindableViewHolder;
|
||||
|
||||
public class InstanceRuleViewHolder extends BindableViewHolder<Instance.Rule>{
|
||||
private final TextView text, number;
|
||||
private int position;
|
||||
|
||||
public InstanceRuleViewHolder(ViewGroup parent){
|
||||
super(parent.getContext(), R.layout.item_server_rule, parent);
|
||||
text=findViewById(R.id.text);
|
||||
number=findViewById(R.id.number);
|
||||
}
|
||||
|
||||
public void setPosition(int position){
|
||||
this.position=position;
|
||||
}
|
||||
|
||||
@SuppressLint("DefaultLocale")
|
||||
@Override
|
||||
public void onBind(Instance.Rule item){
|
||||
if(item.parsedText==null){
|
||||
item.parsedText=HtmlParser.parseLinks(item.text);
|
||||
}
|
||||
text.setText(item.parsedText);
|
||||
number.setText(String.format("%d", position+1));
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,80 @@
|
||||
package org.joinmastodon.android.ui.viewholders;
|
||||
|
||||
import android.content.Context;
|
||||
import android.content.res.ColorStateList;
|
||||
import android.text.TextUtils;
|
||||
import android.view.View;
|
||||
import android.view.ViewGroup;
|
||||
import android.widget.ImageView;
|
||||
import android.widget.LinearLayout;
|
||||
import android.widget.TextView;
|
||||
|
||||
import org.joinmastodon.android.R;
|
||||
import org.joinmastodon.android.model.viewmodel.ListItem;
|
||||
import org.joinmastodon.android.ui.utils.UiUtils;
|
||||
|
||||
import me.grishka.appkit.utils.BindableViewHolder;
|
||||
import me.grishka.appkit.utils.V;
|
||||
import me.grishka.appkit.views.UsableRecyclerView;
|
||||
|
||||
public abstract class ListItemViewHolder<T extends ListItem<?>> extends BindableViewHolder<T> implements UsableRecyclerView.DisableableClickable{
|
||||
protected final TextView title;
|
||||
protected final TextView subtitle;
|
||||
protected final ImageView icon;
|
||||
protected final LinearLayout view;
|
||||
|
||||
public ListItemViewHolder(Context context, int layout, ViewGroup parent){
|
||||
super(context, layout, parent);
|
||||
title=findViewById(R.id.title);
|
||||
subtitle=findViewById(R.id.subtitle);
|
||||
icon=findViewById(R.id.icon);
|
||||
view=(LinearLayout) itemView;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onBind(T item){
|
||||
if(TextUtils.isEmpty(item.title))
|
||||
title.setText(item.titleRes);
|
||||
else
|
||||
title.setText(item.title);
|
||||
|
||||
if(TextUtils.isEmpty(item.subtitle) && item.subtitleRes==0){
|
||||
subtitle.setVisibility(View.GONE);
|
||||
title.setMaxLines(2);
|
||||
view.setMinimumHeight(V.dp(56));
|
||||
}else{
|
||||
subtitle.setVisibility(View.VISIBLE);
|
||||
title.setMaxLines(1);
|
||||
view.setMinimumHeight(V.dp(72));
|
||||
if(TextUtils.isEmpty(item.subtitle))
|
||||
subtitle.setText(item.subtitleRes);
|
||||
else
|
||||
subtitle.setText(item.subtitle);
|
||||
}
|
||||
|
||||
if(item.iconRes!=0){
|
||||
icon.setVisibility(View.VISIBLE);
|
||||
icon.setImageResource(item.iconRes);
|
||||
}else{
|
||||
icon.setVisibility(View.GONE);
|
||||
}
|
||||
|
||||
if(item.colorOverrideAttr!=0){
|
||||
int color=UiUtils.getThemeColor(view.getContext(), item.colorOverrideAttr);
|
||||
title.setTextColor(color);
|
||||
icon.setImageTintList(ColorStateList.valueOf(color));
|
||||
}
|
||||
|
||||
view.setAlpha(item.isEnabled ? 1 : .4f);
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean isEnabled(){
|
||||
return item.isEnabled;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onClick(){
|
||||
item.onClick.run();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,13 @@
|
||||
package org.joinmastodon.android.ui.viewholders;
|
||||
|
||||
import android.content.Context;
|
||||
import android.view.ViewGroup;
|
||||
|
||||
import org.joinmastodon.android.R;
|
||||
import org.joinmastodon.android.model.viewmodel.ListItem;
|
||||
|
||||
public class SimpleListItemViewHolder extends ListItemViewHolder<ListItem<?>>{
|
||||
public SimpleListItemViewHolder(Context context, ViewGroup parent){
|
||||
super(context, R.layout.item_generic_list, parent);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,43 @@
|
||||
package org.joinmastodon.android.ui.viewholders;
|
||||
|
||||
import android.content.Context;
|
||||
import android.view.Gravity;
|
||||
import android.view.ViewGroup;
|
||||
import android.widget.LinearLayout;
|
||||
|
||||
import org.joinmastodon.android.model.viewmodel.CheckableListItem;
|
||||
import org.joinmastodon.android.ui.views.M3Switch;
|
||||
|
||||
import me.grishka.appkit.utils.V;
|
||||
|
||||
public class SwitchListItemViewHolder extends CheckableListItemViewHolder{
|
||||
private final M3Switch sw;
|
||||
private boolean ignoreListener;
|
||||
|
||||
public SwitchListItemViewHolder(Context context, ViewGroup parent){
|
||||
super(context, parent);
|
||||
sw=new M3Switch(context);
|
||||
LinearLayout.LayoutParams lp=new LinearLayout.LayoutParams(V.dp(52), V.dp(32));
|
||||
lp.gravity=Gravity.TOP;
|
||||
lp.setMarginStart(V.dp(16));
|
||||
checkableLayout.addView(sw, lp);
|
||||
sw.setOnCheckedChangeListener((buttonView, isChecked)->{
|
||||
if(ignoreListener)
|
||||
return;
|
||||
if(item.checkedChangeListener!=null)
|
||||
item.checkedChangeListener.accept(isChecked);
|
||||
else
|
||||
item.checked=isChecked;
|
||||
});
|
||||
sw.setClickable(true);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onBind(CheckableListItem<?> item){
|
||||
super.onBind(item);
|
||||
ignoreListener=true;
|
||||
sw.setChecked(item.checked);
|
||||
sw.setEnabled(item.isEnabled);
|
||||
ignoreListener=false;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,39 @@
|
||||
package org.joinmastodon.android.ui.views;
|
||||
|
||||
import android.content.Context;
|
||||
import android.graphics.drawable.Drawable;
|
||||
import android.util.AttributeSet;
|
||||
import android.widget.TextView;
|
||||
|
||||
import org.joinmastodon.android.R;
|
||||
import org.joinmastodon.android.ui.utils.UiUtils;
|
||||
|
||||
public class CheckIconSelectableTextView extends TextView{
|
||||
|
||||
private boolean currentlySelected;
|
||||
|
||||
public CheckIconSelectableTextView(Context context){
|
||||
this(context, null);
|
||||
}
|
||||
|
||||
public CheckIconSelectableTextView(Context context, AttributeSet attrs){
|
||||
this(context, attrs, 0);
|
||||
}
|
||||
|
||||
public CheckIconSelectableTextView(Context context, AttributeSet attrs, int defStyle){
|
||||
super(context, attrs, defStyle);
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void drawableStateChanged(){
|
||||
super.drawableStateChanged();
|
||||
if(currentlySelected==isSelected())
|
||||
return;
|
||||
currentlySelected=isSelected();
|
||||
Drawable start=currentlySelected ? getResources().getDrawable(R.drawable.ic_baseline_check_18, getContext().getTheme()).mutate() : null;
|
||||
if(start!=null)
|
||||
start.setTint(UiUtils.getThemeColor(getContext(), R.attr.colorM3OnSurface));
|
||||
Drawable end=getCompoundDrawablesRelative()[2];
|
||||
setCompoundDrawablesRelativeWithIntrinsicBounds(start, null, end, null);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,58 @@
|
||||
package org.joinmastodon.android.ui.views;
|
||||
|
||||
import android.content.Context;
|
||||
import android.util.AttributeSet;
|
||||
import android.view.accessibility.AccessibilityNodeInfo;
|
||||
import android.widget.Checkable;
|
||||
import android.widget.LinearLayout;
|
||||
|
||||
public class CheckableLinearLayout extends LinearLayout implements Checkable{
|
||||
private boolean checked;
|
||||
private static final int[] CHECKED_STATE_SET = {
|
||||
android.R.attr.state_checked
|
||||
};
|
||||
|
||||
public CheckableLinearLayout(Context context){
|
||||
this(context, null);
|
||||
}
|
||||
|
||||
public CheckableLinearLayout(Context context, AttributeSet attrs){
|
||||
this(context, attrs, 0);
|
||||
}
|
||||
|
||||
public CheckableLinearLayout(Context context, AttributeSet attrs, int defStyle){
|
||||
super(context, attrs, defStyle);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void setChecked(boolean checked){
|
||||
this.checked=checked;
|
||||
refreshDrawableState();
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean isChecked(){
|
||||
return checked;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void toggle(){
|
||||
setChecked(!checked);
|
||||
}
|
||||
|
||||
@Override
|
||||
protected int[] onCreateDrawableState(int extraSpace) {
|
||||
final int[] drawableState = super.onCreateDrawableState(extraSpace + 1);
|
||||
if (isChecked()) {
|
||||
mergeDrawableStates(drawableState, CHECKED_STATE_SET);
|
||||
}
|
||||
return drawableState;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onInitializeAccessibilityNodeInfo(AccessibilityNodeInfo info){
|
||||
super.onInitializeAccessibilityNodeInfo(info);
|
||||
info.setCheckable(true);
|
||||
info.setChecked(checked);
|
||||
}
|
||||
}
|
||||
@@ -7,7 +7,7 @@ import android.widget.Checkable;
|
||||
import android.widget.RelativeLayout;
|
||||
|
||||
public class CheckableRelativeLayout extends RelativeLayout implements Checkable{
|
||||
private boolean checked, checkable = true;
|
||||
private boolean checked, checkable=true;
|
||||
private static final int[] CHECKED_STATE_SET = {
|
||||
android.R.attr.state_checked
|
||||
};
|
||||
@@ -30,10 +30,6 @@ public class CheckableRelativeLayout extends RelativeLayout implements Checkable
|
||||
refreshDrawableState();
|
||||
}
|
||||
|
||||
public void setCheckable(boolean checkable) {
|
||||
this.checkable = checkable;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean isChecked(){
|
||||
return checked;
|
||||
@@ -44,6 +40,10 @@ public class CheckableRelativeLayout extends RelativeLayout implements Checkable
|
||||
setChecked(!checked);
|
||||
}
|
||||
|
||||
public void setCheckable(boolean checkable){
|
||||
this.checkable=checkable;
|
||||
}
|
||||
|
||||
@Override
|
||||
protected int[] onCreateDrawableState(int extraSpace) {
|
||||
final int[] drawableState = super.onCreateDrawableState(extraSpace + 1);
|
||||
|
||||
@@ -0,0 +1,5 @@
|
||||
package org.joinmastodon.android.ui.views;
|
||||
|
||||
public interface ChildDrawingOrderCallback{
|
||||
int getChildDrawingOrder(int childCount, int drawingPosition);
|
||||
}
|
||||
@@ -21,6 +21,7 @@ import androidx.annotation.RequiresApi;
|
||||
|
||||
public class ComposeEditText extends EditText{
|
||||
private SelectionListener selectionListener;
|
||||
private InputConnection currentInputConnection;
|
||||
|
||||
public ComposeEditText(Context context){
|
||||
super(context);
|
||||
@@ -49,15 +50,19 @@ public class ComposeEditText extends EditText{
|
||||
this.selectionListener=selectionListener;
|
||||
}
|
||||
|
||||
public InputConnection getCurrentInputConnection(){
|
||||
return currentInputConnection;
|
||||
}
|
||||
|
||||
// Support receiving images from keyboards
|
||||
@Override
|
||||
public InputConnection onCreateInputConnection(EditorInfo outAttrs){
|
||||
final InputConnection ic=super.onCreateInputConnection(outAttrs);
|
||||
if(Build.VERSION.SDK_INT>=Build.VERSION_CODES.N_MR1){
|
||||
outAttrs.contentMimeTypes=selectionListener.onGetAllowedMediaMimeTypes();
|
||||
return new MediaAcceptingInputConnection(ic);
|
||||
return currentInputConnection=new MediaAcceptingInputConnection(ic);
|
||||
}
|
||||
return ic;
|
||||
return currentInputConnection=ic;
|
||||
}
|
||||
|
||||
// Support pasting images
|
||||
|
||||
@@ -8,7 +8,7 @@ import android.widget.ImageView;
|
||||
import androidx.annotation.Nullable;
|
||||
|
||||
public class CoverImageView extends ImageView{
|
||||
private float imageTranslationY, imageScale=1f;
|
||||
private float imageTranslationY;
|
||||
|
||||
public CoverImageView(Context context){
|
||||
super(context);
|
||||
@@ -30,8 +30,7 @@ public class CoverImageView extends ImageView{
|
||||
canvas.restore();
|
||||
}
|
||||
|
||||
public void setTransform(float transY, float scale){
|
||||
public void setTransform(float transY){
|
||||
imageTranslationY=transY;
|
||||
imageScale=scale;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,33 @@
|
||||
package org.joinmastodon.android.ui.views;
|
||||
|
||||
import android.content.Context;
|
||||
import android.util.AttributeSet;
|
||||
import android.widget.LinearLayout;
|
||||
|
||||
public class CustomDrawingOrderLinearLayout extends LinearLayout{
|
||||
private ChildDrawingOrderCallback drawingOrderCallback;
|
||||
|
||||
public CustomDrawingOrderLinearLayout(Context context){
|
||||
this(context, null);
|
||||
}
|
||||
|
||||
public CustomDrawingOrderLinearLayout(Context context, AttributeSet attrs){
|
||||
this(context, attrs, 0);
|
||||
}
|
||||
|
||||
public CustomDrawingOrderLinearLayout(Context context, AttributeSet attrs, int defStyle){
|
||||
super(context, attrs, defStyle);
|
||||
setChildrenDrawingOrderEnabled(true);
|
||||
}
|
||||
|
||||
@Override
|
||||
protected int getChildDrawingOrder(int childCount, int drawingPosition){
|
||||
if(drawingOrderCallback!=null)
|
||||
return drawingOrderCallback.getChildDrawingOrder(childCount, drawingPosition);
|
||||
return super.getChildDrawingOrder(childCount, drawingPosition);
|
||||
}
|
||||
|
||||
public void setDrawingOrderCallback(ChildDrawingOrderCallback drawingOrderCallback){
|
||||
this.drawingOrderCallback=drawingOrderCallback;
|
||||
}
|
||||
}
|
||||
@@ -1,10 +1,9 @@
|
||||
package org.joinmastodon.android.ui.views;
|
||||
|
||||
import android.content.Context;
|
||||
import android.content.res.ColorStateList;
|
||||
import android.graphics.drawable.Drawable;
|
||||
import android.util.AttributeSet;
|
||||
import android.widget.Button;
|
||||
import android.view.Gravity;
|
||||
|
||||
import org.joinmastodon.android.R;
|
||||
import org.joinmastodon.android.ui.utils.UiUtils;
|
||||
@@ -12,9 +11,7 @@ import org.joinmastodon.android.ui.utils.UiUtils;
|
||||
import androidx.annotation.DrawableRes;
|
||||
import me.grishka.appkit.utils.V;
|
||||
|
||||
public class FilterChipView extends Button{
|
||||
|
||||
private boolean currentlySelected;
|
||||
public class FilterChipView extends CheckIconSelectableTextView{
|
||||
|
||||
public FilterChipView(Context context){
|
||||
this(context, null);
|
||||
@@ -29,31 +26,37 @@ public class FilterChipView extends Button{
|
||||
setCompoundDrawablePadding(V.dp(8));
|
||||
setBackgroundResource(R.drawable.bg_filter_chip);
|
||||
setTextAppearance(R.style.m3_label_large);
|
||||
setHeight(V.dp(48));
|
||||
setGravity(Gravity.CENTER_VERTICAL);
|
||||
setTextColor(getResources().getColorStateList(R.color.filter_chip_text, context.getTheme()));
|
||||
setCompoundDrawableTintList(ColorStateList.valueOf(UiUtils.getThemeColor(context, R.attr.colorM3OnSurface)));
|
||||
updatePadding();
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void drawableStateChanged(){
|
||||
super.drawableStateChanged();
|
||||
if(currentlySelected==isSelected())
|
||||
return;
|
||||
currentlySelected=isSelected();
|
||||
Drawable start=currentlySelected ? getResources().getDrawable(R.drawable.ic_baseline_check_18, getContext().getTheme()) : null;
|
||||
Drawable end=getCompoundDrawablesRelative()[2];
|
||||
setCompoundDrawablesRelativeWithIntrinsicBounds(start, null, end, null);
|
||||
updatePadding();
|
||||
}
|
||||
|
||||
private void updatePadding(){
|
||||
int vertical=V.dp(6);
|
||||
int vertical=0;
|
||||
Drawable[] drawables=getCompoundDrawablesRelative();
|
||||
setPaddingRelative(V.dp(drawables[0]==null ? 16 : 8), vertical, V.dp(drawables[2]==null ? 16 : 8), vertical);
|
||||
}
|
||||
|
||||
public void setDrawableEnd(@DrawableRes int drawable){
|
||||
setCompoundDrawablesRelativeWithIntrinsicBounds(0, 0, drawable, 0);
|
||||
Drawable icon=getResources().getDrawable(drawable, getContext().getTheme()).mutate();
|
||||
icon.setBounds(0, 0, V.dp(18), V.dp(18));
|
||||
icon.setTint(UiUtils.getThemeColor(getContext(), R.attr.colorM3OnSurface));
|
||||
setCompoundDrawablesRelativeWithIntrinsicBounds(getCompoundDrawablesRelative()[0], null, icon, null);
|
||||
updatePadding();
|
||||
}
|
||||
|
||||
public void setDrawableStartTinted(@DrawableRes int drawable){
|
||||
Drawable icon=getResources().getDrawable(drawable, getContext().getTheme()).mutate();
|
||||
icon.setBounds(0, 0, V.dp(18), V.dp(18));
|
||||
icon.setTint(UiUtils.getThemeColor(getContext(), R.attr.colorM3Primary));
|
||||
setCompoundDrawablesRelative(icon, null, getCompoundDrawablesRelative()[2], null);
|
||||
updatePadding();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,36 @@
|
||||
package org.joinmastodon.android.ui.views;
|
||||
|
||||
import android.content.Context;
|
||||
import android.util.AttributeSet;
|
||||
import android.widget.ImageView;
|
||||
|
||||
public class FixedAspectRatioImageView extends ImageView{
|
||||
private float aspectRatio=1;
|
||||
|
||||
public FixedAspectRatioImageView(Context context){
|
||||
this(context, null);
|
||||
}
|
||||
|
||||
public FixedAspectRatioImageView(Context context, AttributeSet attrs){
|
||||
this(context, attrs, 0);
|
||||
}
|
||||
|
||||
public FixedAspectRatioImageView(Context context, AttributeSet attrs, int defStyle){
|
||||
super(context, attrs, defStyle);
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec){
|
||||
int width=MeasureSpec.getSize(widthMeasureSpec);
|
||||
heightMeasureSpec=Math.round(width/aspectRatio) | MeasureSpec.EXACTLY;
|
||||
super.onMeasure(widthMeasureSpec, heightMeasureSpec);
|
||||
}
|
||||
|
||||
public float getAspectRatio(){
|
||||
return aspectRatio;
|
||||
}
|
||||
|
||||
public void setAspectRatio(float aspectRatio){
|
||||
this.aspectRatio=aspectRatio;
|
||||
}
|
||||
}
|
||||
@@ -35,9 +35,9 @@ import androidx.annotation.Keep;
|
||||
import androidx.annotation.NonNull;
|
||||
import androidx.annotation.Nullable;
|
||||
import me.grishka.appkit.utils.CubicBezierInterpolator;
|
||||
import me.grishka.appkit.utils.V;
|
||||
import me.grishka.appkit.utils.CustomViewHelper;
|
||||
|
||||
public class FloatingHintEditTextLayout extends FrameLayout{
|
||||
public class FloatingHintEditTextLayout extends FrameLayout implements CustomViewHelper{
|
||||
private EditText edit;
|
||||
private TextView label;
|
||||
private int labelTextSize;
|
||||
@@ -60,10 +60,8 @@ public class FloatingHintEditTextLayout extends FrameLayout{
|
||||
|
||||
public FloatingHintEditTextLayout(Context context, AttributeSet attrs, int defStyle){
|
||||
super(context, attrs, defStyle);
|
||||
if(isInEditMode())
|
||||
V.setApplicationContext(context);
|
||||
TypedArray ta=context.obtainStyledAttributes(attrs, R.styleable.FloatingHintEditTextLayout);
|
||||
labelTextSize=ta.getDimensionPixelSize(R.styleable.FloatingHintEditTextLayout_android_labelTextSize, V.dp(12));
|
||||
labelTextSize=ta.getDimensionPixelSize(R.styleable.FloatingHintEditTextLayout_android_labelTextSize, dp(12));
|
||||
offsetY=ta.getDimensionPixelOffset(R.styleable.FloatingHintEditTextLayout_editTextOffsetY, 0);
|
||||
labelColors=ta.getColorStateList(R.styleable.FloatingHintEditTextLayout_labelTextColor);
|
||||
ta.recycle();
|
||||
@@ -100,14 +98,18 @@ public class FloatingHintEditTextLayout extends FrameLayout{
|
||||
|
||||
errorView=new LinkedTextView(getContext());
|
||||
errorView.setTextAppearance(R.style.m3_body_small);
|
||||
errorView.setTextColor(UiUtils.getThemeColor(getContext(), R.attr.colorM3OnSurfaceVariant));
|
||||
errorView.setTextColor(UiUtils.getThemeColor(getContext(), R.attr.colorM3Error));
|
||||
errorView.setLinkTextColor(UiUtils.getThemeColor(getContext(), R.attr.colorM3Primary));
|
||||
errorView.setLayoutParams(new LayoutParams(ViewGroup.LayoutParams.WRAP_CONTENT, ViewGroup.LayoutParams.WRAP_CONTENT));
|
||||
errorView.setPadding(V.dp(16), V.dp(4), V.dp(16), 0);
|
||||
errorView.setPadding(dp(16), dp(4), dp(16), 0);
|
||||
errorView.setVisibility(View.GONE);
|
||||
addView(errorView);
|
||||
}
|
||||
|
||||
public void updateHint(){
|
||||
label.setText(edit.getHint());
|
||||
}
|
||||
|
||||
private void onTextChanged(Editable text){
|
||||
if(errorState){
|
||||
errorView.setVisibility(View.GONE);
|
||||
@@ -129,7 +131,12 @@ public class FloatingHintEditTextLayout extends FrameLayout{
|
||||
edit.getViewTreeObserver().removeOnPreDrawListener(this);
|
||||
|
||||
float scale=edit.getLineHeight()/(float)label.getLineHeight();
|
||||
float transY=edit.getHeight()/2f-edit.getLineHeight()/2f+(edit.getTop()-label.getTop())-(label.getHeight()/2f-label.getLineHeight()/2f);
|
||||
float transY;
|
||||
if((edit.getGravity() & Gravity.TOP)==Gravity.TOP){
|
||||
transY=edit.getPaddingTop()+(edit.getTop()-label.getTop())-(label.getHeight()/2f-label.getLineHeight()/2f);
|
||||
}else{
|
||||
transY=edit.getHeight()/2f-edit.getLineHeight()/2f+(edit.getTop()-label.getTop())-(label.getHeight()/2f-label.getLineHeight()/2f);
|
||||
}
|
||||
|
||||
AnimatorSet anim=new AnimatorSet();
|
||||
if(hintVisible){
|
||||
@@ -187,7 +194,7 @@ public class FloatingHintEditTextLayout extends FrameLayout{
|
||||
public void onDrawForeground(Canvas canvas){
|
||||
if(getForeground()!=null && animProgress>0){
|
||||
canvas.save();
|
||||
float width=(label.getWidth()+V.dp(8))*animProgress;
|
||||
float width=(label.getWidth()+dp(8))*animProgress;
|
||||
float centerX=label.getLeft()+label.getWidth()/2f;
|
||||
tmpRect.set(centerX-width/2f, label.getTop(), centerX+width/2f, label.getBottom());
|
||||
if(Build.VERSION.SDK_INT>=Build.VERSION_CODES.O)
|
||||
@@ -347,7 +354,7 @@ public class FloatingHintEditTextLayout extends FrameLayout{
|
||||
@Override
|
||||
protected void onBoundsChange(@NonNull Rect bounds){
|
||||
super.onBoundsChange(bounds);
|
||||
int offset=V.dp(12);
|
||||
int offset=dp(12);
|
||||
wrapped.setBounds(edit.getLeft()-offset, edit.getTop()-offset, edit.getRight()+offset, edit.getBottom()+offset);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -24,6 +24,7 @@ public class FrameLayoutThatOnlyMeasuresFirstChild extends FrameLayout{
|
||||
return;
|
||||
View child0=getChildAt(0);
|
||||
measureChild(child0, widthMeasureSpec, heightMeasureSpec);
|
||||
super.onMeasure(child0.getMeasuredWidth() | MeasureSpec.EXACTLY, child0.getMeasuredHeight() | MeasureSpec.EXACTLY);
|
||||
int vpad=getPaddingTop()+getPaddingBottom();
|
||||
super.onMeasure(child0.getMeasuredWidth() | MeasureSpec.EXACTLY, (child0.getMeasuredHeight()+vpad) | MeasureSpec.EXACTLY);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -14,12 +14,12 @@ import org.joinmastodon.android.ui.utils.UiUtils;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
import me.grishka.appkit.utils.V;
|
||||
import me.grishka.appkit.utils.CustomViewHelper;
|
||||
|
||||
public class HashtagChartView extends View{
|
||||
public class HashtagChartView extends View implements CustomViewHelper{
|
||||
private Paint paint=new Paint(Paint.ANTI_ALIAS_FLAG);
|
||||
private Path strokePath=new Path(), fillPath=new Path();
|
||||
private CornerPathEffect pathEffect=new CornerPathEffect(V.dp(3));
|
||||
private final CornerPathEffect pathEffect=new CornerPathEffect(dp(3));
|
||||
private float[] relativeOffsets=new float[7];
|
||||
|
||||
public HashtagChartView(Context context){
|
||||
@@ -32,7 +32,7 @@ public class HashtagChartView extends View{
|
||||
|
||||
public HashtagChartView(Context context, AttributeSet attrs, int defStyle){
|
||||
super(context, attrs, defStyle);
|
||||
paint.setStrokeWidth(V.dp(1.71f));
|
||||
paint.setStrokeWidth(dp(1));
|
||||
paint.setStrokeCap(Paint.Cap.ROUND);
|
||||
paint.setStrokeJoin(Paint.Join.ROUND);
|
||||
}
|
||||
@@ -57,20 +57,20 @@ public class HashtagChartView extends View{
|
||||
return;
|
||||
strokePath.rewind();
|
||||
fillPath.rewind();
|
||||
float step=(getWidth()-V.dp(2))/(float)(relativeOffsets.length-1);
|
||||
float maxH=getHeight()-V.dp(2);
|
||||
float x=getWidth()-V.dp(1);
|
||||
strokePath.moveTo(x, maxH-maxH*relativeOffsets[0]+V.dp(1));
|
||||
fillPath.moveTo(getWidth(), getHeight()-V.dp(1));
|
||||
fillPath.lineTo(x, maxH-maxH*relativeOffsets[0]+V.dp(1));
|
||||
float step=(getWidth()-dp(2))/(float)(relativeOffsets.length-1);
|
||||
float maxH=getHeight()-dp(2);
|
||||
float x=getWidth()-dp(1);
|
||||
strokePath.moveTo(x, maxH-maxH*relativeOffsets[0]+dp(1));
|
||||
fillPath.moveTo(getWidth(), getHeight()-dp(1));
|
||||
fillPath.lineTo(x, maxH-maxH*relativeOffsets[0]+dp(1));
|
||||
for(int i=1;i<relativeOffsets.length;i++){
|
||||
float offset=relativeOffsets[i];
|
||||
x-=step;
|
||||
float y=maxH-maxH*offset+V.dp(1);
|
||||
float y=maxH-maxH*offset+dp(1);
|
||||
strokePath.lineTo(x, y);
|
||||
fillPath.lineTo(x, y);
|
||||
}
|
||||
fillPath.lineTo(V.dp(1), getHeight()-V.dp(1));
|
||||
fillPath.lineTo(dp(1), getHeight()-dp(1));
|
||||
fillPath.close();
|
||||
}
|
||||
|
||||
@@ -83,11 +83,11 @@ public class HashtagChartView extends View{
|
||||
@Override
|
||||
protected void onDraw(Canvas canvas){
|
||||
paint.setStyle(Paint.Style.FILL);
|
||||
paint.setColor(UiUtils.getThemeColor(getContext(), R.attr.colorAccentLightest));
|
||||
paint.setColor(UiUtils.getThemeColor(getContext(), R.attr.colorM3PrimaryInverse));
|
||||
paint.setPathEffect(null);
|
||||
canvas.drawPath(fillPath, paint);
|
||||
paint.setStyle(Paint.Style.STROKE);
|
||||
paint.setColor(UiUtils.getThemeColor(getContext(), android.R.attr.colorAccent));
|
||||
paint.setColor(UiUtils.getThemeColor(getContext(), R.attr.colorM3Primary));
|
||||
paint.setPathEffect(pathEffect);
|
||||
canvas.drawPath(strokePath, paint);
|
||||
}
|
||||
|
||||
@@ -0,0 +1,36 @@
|
||||
package org.joinmastodon.android.ui.views;
|
||||
|
||||
import android.content.Context;
|
||||
import android.util.AttributeSet;
|
||||
import android.view.View;
|
||||
import android.view.ViewGroup;
|
||||
import android.widget.HorizontalScrollView;
|
||||
|
||||
public class HorizontalScrollViewThatRespectsMatchParent extends HorizontalScrollView{
|
||||
public HorizontalScrollViewThatRespectsMatchParent(Context context){
|
||||
super(context);
|
||||
}
|
||||
|
||||
public HorizontalScrollViewThatRespectsMatchParent(Context context, AttributeSet attrs){
|
||||
super(context, attrs);
|
||||
}
|
||||
|
||||
public HorizontalScrollViewThatRespectsMatchParent(Context context, AttributeSet attrs, int defStyleAttr){
|
||||
super(context, attrs, defStyleAttr);
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec){
|
||||
if(getChildCount()==0)
|
||||
return;
|
||||
View child=getChildAt(0);
|
||||
ViewGroup.LayoutParams lp=child.getLayoutParams();
|
||||
if(lp.width==ViewGroup.LayoutParams.MATCH_PARENT){
|
||||
int hms=getChildMeasureSpec(heightMeasureSpec, getPaddingTop()+getPaddingBottom(), lp.height);
|
||||
child.measure(MeasureSpec.getSize(widthMeasureSpec) | MeasureSpec.EXACTLY, hms);
|
||||
setMeasuredDimension(child.getMeasuredWidth(), child.getMeasuredHeight());
|
||||
return;
|
||||
}
|
||||
super.onMeasure(widthMeasureSpec, heightMeasureSpec);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,83 @@
|
||||
package org.joinmastodon.android.ui.views;
|
||||
|
||||
import android.content.Context;
|
||||
import android.graphics.Canvas;
|
||||
import android.graphics.ColorFilter;
|
||||
import android.graphics.drawable.Drawable;
|
||||
import android.util.AttributeSet;
|
||||
import android.widget.Switch;
|
||||
|
||||
import java.lang.reflect.Field;
|
||||
|
||||
import androidx.annotation.NonNull;
|
||||
import androidx.annotation.Nullable;
|
||||
import me.grishka.appkit.utils.V;
|
||||
|
||||
public class M3Switch extends Switch{
|
||||
private boolean ignoreRequestLayout;
|
||||
private DummyDrawable dummyDrawable=new DummyDrawable();
|
||||
|
||||
public M3Switch(Context context){
|
||||
super(context);
|
||||
}
|
||||
|
||||
public M3Switch(Context context, AttributeSet attrs){
|
||||
super(context, attrs);
|
||||
}
|
||||
|
||||
public M3Switch(Context context, AttributeSet attrs, int defStyleAttr){
|
||||
super(context, attrs, defStyleAttr);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onMeasure(int widthMeasureSpec, int heightMeasureSpec){
|
||||
ignoreRequestLayout=true;
|
||||
Drawable prevThumbDrawable=getThumbDrawable();
|
||||
setThumbDrawable(dummyDrawable);
|
||||
ignoreRequestLayout=false;
|
||||
super.onMeasure(widthMeasureSpec, heightMeasureSpec);
|
||||
ignoreRequestLayout=true;
|
||||
setThumbDrawable(prevThumbDrawable);
|
||||
ignoreRequestLayout=false;
|
||||
try{
|
||||
Field fld=Switch.class.getDeclaredField("mThumbWidth");
|
||||
fld.setAccessible(true);
|
||||
fld.set(this, V.dp(32));
|
||||
}catch(Exception ignore){}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void requestLayout(){
|
||||
if(ignoreRequestLayout)
|
||||
return;
|
||||
super.requestLayout();
|
||||
}
|
||||
|
||||
private static class DummyDrawable extends Drawable{
|
||||
|
||||
@Override
|
||||
public void draw(@NonNull Canvas canvas){
|
||||
|
||||
}
|
||||
|
||||
@Override
|
||||
public void setAlpha(int alpha){
|
||||
|
||||
}
|
||||
|
||||
@Override
|
||||
public void setColorFilter(@Nullable ColorFilter colorFilter){
|
||||
|
||||
}
|
||||
|
||||
@Override
|
||||
public int getOpacity(){
|
||||
return 0;
|
||||
}
|
||||
|
||||
@Override
|
||||
public int getIntrinsicWidth(){
|
||||
return V.dp(26);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,8 +1,13 @@
|
||||
package org.joinmastodon.android.ui.views;
|
||||
|
||||
import android.content.Context;
|
||||
import android.content.res.TypedArray;
|
||||
import android.util.AttributeSet;
|
||||
import android.view.View;
|
||||
import android.webkit.WebView;
|
||||
import android.widget.ScrollView;
|
||||
|
||||
import org.joinmastodon.android.R;
|
||||
|
||||
import java.util.function.Supplier;
|
||||
|
||||
@@ -10,63 +15,102 @@ import androidx.recyclerview.widget.LinearLayoutManager;
|
||||
import androidx.recyclerview.widget.RecyclerView;
|
||||
|
||||
public class NestedRecyclerScrollView extends CustomScrollView{
|
||||
private Supplier<RecyclerView> scrollableChildSupplier;
|
||||
private Supplier<View> scrollableChildSupplier;
|
||||
private boolean takePriorityOverChildViews;
|
||||
|
||||
public NestedRecyclerScrollView(Context context){
|
||||
super(context);
|
||||
this(context, null);
|
||||
}
|
||||
|
||||
public NestedRecyclerScrollView(Context context, AttributeSet attrs){
|
||||
super(context, attrs);
|
||||
this(context, attrs, 0);
|
||||
}
|
||||
|
||||
public NestedRecyclerScrollView(Context context, AttributeSet attrs, int defStyleAttr){
|
||||
super(context, attrs, defStyleAttr);
|
||||
public NestedRecyclerScrollView(Context context, AttributeSet attrs, int defStyle){
|
||||
super(context, attrs, defStyle);
|
||||
TypedArray ta=context.obtainStyledAttributes(attrs, R.styleable.NestedRecyclerScrollView);
|
||||
takePriorityOverChildViews=ta.getBoolean(R.styleable.NestedRecyclerScrollView_takePriorityOverChildViews, false);
|
||||
ta.recycle();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onNestedPreScroll(View target, int dx, int dy, int[] consumed) {
|
||||
final RecyclerView rv = (RecyclerView) target;
|
||||
if ((dy < 0 && isScrolledToTop(rv)) || (dy > 0 && !isScrolledToBottom())) {
|
||||
public void onNestedPreScroll(View target, int dx, int dy, int[] consumed){
|
||||
if(takePriorityOverChildViews){
|
||||
if((dy<0 && getScrollY()>0) || (dy>0 && canScrollVertically(1))){
|
||||
scrollBy(0, dy);
|
||||
consumed[1]=dy;
|
||||
return;
|
||||
}
|
||||
}else if((dy<0 && isScrolledToTop(target)) || (dy>0 && !isScrolledToBottom())){
|
||||
scrollBy(0, dy);
|
||||
consumed[1] = dy;
|
||||
consumed[1]=dy;
|
||||
return;
|
||||
}
|
||||
super.onNestedPreScroll(target, dx, dy, consumed);
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean onNestedPreFling(View target, float velX, float velY) {
|
||||
final RecyclerView rv = (RecyclerView) target;
|
||||
if ((velY < 0 && isScrolledToTop(rv)) || (velY > 0 && !isScrolledToBottom())) {
|
||||
public boolean onNestedPreFling(View target, float velX, float velY){
|
||||
if(takePriorityOverChildViews){
|
||||
if((velY<0 && getScrollY()>0) || (velY>0 && canScrollVertically(1))){
|
||||
fling((int)velY);
|
||||
return true;
|
||||
}
|
||||
}else if((velY<0 && isScrolledToTop(target)) || (velY>0 && !isScrolledToBottom())){
|
||||
fling((int) velY);
|
||||
return true;
|
||||
}
|
||||
return super.onNestedPreFling(target, velX, velY);
|
||||
}
|
||||
|
||||
private boolean isScrolledToBottom() {
|
||||
private boolean isScrolledToBottom(){
|
||||
return !canScrollVertically(1);
|
||||
}
|
||||
|
||||
private boolean isScrolledToTop(RecyclerView rv) {
|
||||
final LinearLayoutManager lm = (LinearLayoutManager) rv.getLayoutManager();
|
||||
return lm.findFirstVisibleItemPosition() == 0
|
||||
&& lm.findViewByPosition(0).getTop() == rv.getPaddingTop();
|
||||
private boolean isScrolledToTop(View view){
|
||||
if(view instanceof RecyclerView rv){
|
||||
final LinearLayoutManager lm=(LinearLayoutManager) rv.getLayoutManager();
|
||||
return lm.findFirstVisibleItemPosition()==0
|
||||
&& lm.findViewByPosition(0).getTop()==rv.getPaddingTop();
|
||||
}
|
||||
return !view.canScrollVertically(-1);
|
||||
}
|
||||
|
||||
public void setScrollableChildSupplier(Supplier<RecyclerView> scrollableChildSupplier){
|
||||
public void setScrollableChildSupplier(Supplier<View> scrollableChildSupplier){
|
||||
this.scrollableChildSupplier=scrollableChildSupplier;
|
||||
}
|
||||
|
||||
@Override
|
||||
protected boolean onScrollingHitEdge(float velocity){
|
||||
if(velocity>0){
|
||||
RecyclerView view=scrollableChildSupplier.get();
|
||||
if(view!=null){
|
||||
return view.fling(0, (int)velocity);
|
||||
if(velocity>0 || takePriorityOverChildViews){
|
||||
View view=scrollableChildSupplier==null ? null : scrollableChildSupplier.get();
|
||||
if(view instanceof RecyclerView rv){
|
||||
return rv.fling(0, (int) velocity);
|
||||
}else if(view instanceof ScrollView sv){
|
||||
if(sv.canScrollVertically((int)velocity)){
|
||||
sv.fling((int)velocity);
|
||||
return true;
|
||||
}
|
||||
}else if(view instanceof CustomScrollView sv){
|
||||
if(sv.canScrollVertically((int)velocity)){
|
||||
sv.fling((int)velocity);
|
||||
return true;
|
||||
}
|
||||
}else if(view instanceof WebView wv){
|
||||
if(wv.canScrollVertically((int)velocity)){
|
||||
wv.flingScroll(0, (int)velocity);
|
||||
return true;
|
||||
}
|
||||
}
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
public boolean isTakePriorityOverChildViews(){
|
||||
return takePriorityOverChildViews;
|
||||
}
|
||||
|
||||
public void setTakePriorityOverChildViews(boolean takePriorityOverChildViews){
|
||||
this.takePriorityOverChildViews=takePriorityOverChildViews;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -2,23 +2,49 @@ package org.joinmastodon.android.ui.views;
|
||||
|
||||
import android.content.Context;
|
||||
import android.util.AttributeSet;
|
||||
import android.util.Log;
|
||||
import android.view.MotionEvent;
|
||||
import android.view.View;
|
||||
import android.view.ViewTreeObserver;
|
||||
import android.view.animation.Interpolator;
|
||||
import android.widget.LinearLayout;
|
||||
|
||||
import androidx.annotation.Nullable;
|
||||
import me.grishka.appkit.utils.CubicBezierInterpolator;
|
||||
import me.grishka.appkit.utils.CustomViewHelper;
|
||||
import me.grishka.appkit.utils.V;
|
||||
|
||||
public class ReorderableLinearLayout extends LinearLayout{
|
||||
public class ReorderableLinearLayout extends LinearLayout implements CustomViewHelper{
|
||||
private static final String TAG="ReorderableLinearLayout";
|
||||
|
||||
private static final Interpolator sDragScrollInterpolator=t->t * t * t * t * t;
|
||||
|
||||
private static final Interpolator sDragViewScrollCapInterpolator=t->{
|
||||
t -= 1.0f;
|
||||
return t * t * t * t * t + 1.0f;
|
||||
};
|
||||
private static final long DRAG_SCROLL_ACCELERATION_LIMIT_TIME_MS = 2000;
|
||||
|
||||
private View draggedView;
|
||||
private View bottomSibling, topSibling;
|
||||
private float startY;
|
||||
private float startX, startY, dX, dY, viewStartX, viewStartY;
|
||||
private OnDragListener dragListener;
|
||||
private boolean moveInBothDimensions;
|
||||
private int edgeSize;
|
||||
private View scrollableParent;
|
||||
private long dragScrollStartTime;
|
||||
private int cachedMaxScrollSpeed=-1;
|
||||
final Runnable scrollRunnable= new Runnable() {
|
||||
@Override
|
||||
public void run() {
|
||||
if (draggedView != null && scrollIfNecessary()) {
|
||||
if (draggedView != null) { //it might be lost during scrolling
|
||||
// moveIfNecessary(mSelected);
|
||||
}
|
||||
removeCallbacks(scrollRunnable);
|
||||
postOnAnimation(this);
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
public ReorderableLinearLayout(Context context){
|
||||
super(context);
|
||||
@@ -30,12 +56,13 @@ public class ReorderableLinearLayout extends LinearLayout{
|
||||
|
||||
public ReorderableLinearLayout(Context context, @Nullable AttributeSet attrs, int defStyleAttr){
|
||||
super(context, attrs, defStyleAttr);
|
||||
edgeSize=dp(20);
|
||||
}
|
||||
|
||||
public void startDragging(View child){
|
||||
getParent().requestDisallowInterceptTouchEvent(true);
|
||||
draggedView=child;
|
||||
draggedView.animate().translationZ(V.dp(1f)).setDuration(150).setInterpolator(CubicBezierInterpolator.DEFAULT).start();
|
||||
dragListener.onDragStart(draggedView);
|
||||
|
||||
int index=indexOfChild(child);
|
||||
if(index==-1)
|
||||
@@ -44,11 +71,30 @@ public class ReorderableLinearLayout extends LinearLayout{
|
||||
topSibling=getChildAt(index-1);
|
||||
if(index<getChildCount()-1)
|
||||
bottomSibling=getChildAt(index+1);
|
||||
|
||||
scrollableParent=findScrollableParent(this);
|
||||
|
||||
viewStartX=child.getX();
|
||||
viewStartY=child.getY();
|
||||
}
|
||||
|
||||
private View findScrollableParent(View child){
|
||||
if(getOrientation()==VERTICAL){
|
||||
if(child.canScrollVertically(-1) || child.canScrollVertically(1))
|
||||
return child;
|
||||
}else{
|
||||
if(child.canScrollHorizontally(-1) || child.canScrollHorizontally(1))
|
||||
return child;
|
||||
}
|
||||
if(child.getParent() instanceof View v)
|
||||
return findScrollableParent(v);
|
||||
return null;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean onInterceptTouchEvent(MotionEvent ev){
|
||||
if(draggedView!=null){
|
||||
startX=ev.getX();
|
||||
startY=ev.getY();
|
||||
return true;
|
||||
}
|
||||
@@ -60,34 +106,61 @@ public class ReorderableLinearLayout extends LinearLayout{
|
||||
if(draggedView!=null){
|
||||
if(ev.getAction()==MotionEvent.ACTION_UP || ev.getAction()==MotionEvent.ACTION_CANCEL){
|
||||
endDrag();
|
||||
removeCallbacks(scrollRunnable);
|
||||
draggedView=null;
|
||||
bottomSibling=null;
|
||||
topSibling=null;
|
||||
}else if(ev.getAction()==MotionEvent.ACTION_MOVE){
|
||||
draggedView.setTranslationY(ev.getY()-startY);
|
||||
if(topSibling!=null && draggedView.getY()<=topSibling.getY()){
|
||||
moveDraggedView(-1);
|
||||
}else if(bottomSibling!=null && draggedView.getY()>=bottomSibling.getY()){
|
||||
moveDraggedView(1);
|
||||
dX=ev.getX()-startX;
|
||||
dY=ev.getY()-startY;
|
||||
|
||||
if(moveInBothDimensions){
|
||||
draggedView.setTranslationX(dX);
|
||||
draggedView.setTranslationY(dY);
|
||||
}else if(getOrientation()==VERTICAL){
|
||||
draggedView.setTranslationY(dY);
|
||||
}else{
|
||||
draggedView.setTranslationX(dX);
|
||||
}
|
||||
|
||||
removeCallbacks(scrollRunnable);
|
||||
scrollRunnable.run();
|
||||
|
||||
if(getOrientation()==VERTICAL){
|
||||
if(topSibling!=null && draggedView.getY()<=topSibling.getY()){
|
||||
moveDraggedView(-1);
|
||||
}else if(bottomSibling!=null && draggedView.getY()>=bottomSibling.getY()){
|
||||
moveDraggedView(1);
|
||||
}
|
||||
}else{
|
||||
if(topSibling!=null && draggedView.getX()<=topSibling.getX()){
|
||||
moveDraggedView(-1);
|
||||
}else if(bottomSibling!=null && draggedView.getX()>=bottomSibling.getX()){
|
||||
moveDraggedView(1);
|
||||
}
|
||||
}
|
||||
dragListener.onDragMove(draggedView);
|
||||
}
|
||||
}
|
||||
return super.onTouchEvent(ev);
|
||||
}
|
||||
|
||||
private void endDrag(){
|
||||
draggedView.animate().translationY(0f).translationZ(0f).setDuration(200).setInterpolator(CubicBezierInterpolator.DEFAULT).start();
|
||||
dragListener.onDragEnd(draggedView);
|
||||
}
|
||||
|
||||
private void moveDraggedView(int positionOffset){
|
||||
int index=indexOfChild(draggedView);
|
||||
int prevTop=draggedView.getTop();
|
||||
|
||||
boolean isVertical=getOrientation()==VERTICAL;
|
||||
|
||||
int prevOffset=isVertical ? draggedView.getTop() : draggedView.getLeft();
|
||||
removeView(draggedView);
|
||||
int prevIndex=index;
|
||||
index+=positionOffset;
|
||||
addView(draggedView, index);
|
||||
final View prevSibling=positionOffset<0 ? topSibling : bottomSibling;
|
||||
int prevSiblingTop=prevSibling.getTop();
|
||||
int prevSiblingOffset=isVertical ? prevSibling.getTop() : prevSibling.getLeft();
|
||||
if(index>0)
|
||||
topSibling=getChildAt(index-1);
|
||||
else
|
||||
@@ -101,11 +174,20 @@ public class ReorderableLinearLayout extends LinearLayout{
|
||||
@Override
|
||||
public boolean onPreDraw(){
|
||||
draggedView.getViewTreeObserver().removeOnPreDrawListener(this);
|
||||
float offset=prevTop-draggedView.getTop();
|
||||
startY-=offset;
|
||||
draggedView.setTranslationY(draggedView.getTranslationY()+offset);
|
||||
prevSibling.setTranslationY(prevSiblingTop-prevSibling.getTop());
|
||||
prevSibling.animate().translationY(0f).setInterpolator(CubicBezierInterpolator.DEFAULT).setDuration(200).start();
|
||||
float offset=prevOffset-(isVertical ? draggedView.getTop() : draggedView.getLeft());
|
||||
if(isVertical){
|
||||
startY-=offset;
|
||||
viewStartY-=offset;
|
||||
draggedView.setTranslationY(draggedView.getTranslationY()+offset);
|
||||
prevSibling.setTranslationY(prevSiblingOffset-prevSibling.getTop());
|
||||
prevSibling.animate().translationY(0f).setInterpolator(CubicBezierInterpolator.DEFAULT).setDuration(200).start();
|
||||
}else{
|
||||
startX-=offset;
|
||||
viewStartX-=offset;
|
||||
draggedView.setTranslationX(draggedView.getTranslationX()+offset);
|
||||
prevSibling.setTranslationX(prevSiblingOffset-prevSibling.getLeft());
|
||||
prevSibling.animate().translationX(0f).setInterpolator(CubicBezierInterpolator.DEFAULT).setDuration(200).start();
|
||||
}
|
||||
return true;
|
||||
}
|
||||
});
|
||||
@@ -115,7 +197,105 @@ public class ReorderableLinearLayout extends LinearLayout{
|
||||
this.dragListener=dragListener;
|
||||
}
|
||||
|
||||
public boolean isMoveInBothDimensions(){
|
||||
return moveInBothDimensions;
|
||||
}
|
||||
|
||||
public void setMoveInBothDimensions(boolean moveInBothDimensions){
|
||||
this.moveInBothDimensions=moveInBothDimensions;
|
||||
}
|
||||
|
||||
boolean scrollIfNecessary(){
|
||||
if(draggedView==null || scrollableParent==null){
|
||||
dragScrollStartTime=Long.MIN_VALUE;
|
||||
return false;
|
||||
}
|
||||
final long now=System.currentTimeMillis();
|
||||
final long scrollDuration=dragScrollStartTime==Long.MIN_VALUE ? 0 : now-dragScrollStartTime;
|
||||
int scrollX=0;
|
||||
int scrollY=0;
|
||||
if(getOrientation()==HORIZONTAL){
|
||||
int curX=(int) (viewStartX+dX)-scrollableParent.getScrollX();
|
||||
final int leftDiff=curX-getPaddingLeft();
|
||||
if(dX<0 && leftDiff<0){
|
||||
scrollX=leftDiff;
|
||||
}else if(dX>0){
|
||||
final int rightDiff=curX+draggedView.getWidth()-(scrollableParent.getWidth()-getPaddingRight());
|
||||
if(rightDiff>0){
|
||||
scrollX=rightDiff;
|
||||
}
|
||||
}
|
||||
}else{
|
||||
int curY=(int) (viewStartY+dY)-scrollableParent.getScrollY();
|
||||
final int topDiff=curY-getPaddingTop();
|
||||
if(dY<0 && topDiff<0){
|
||||
scrollY=topDiff;
|
||||
}else if(dY>0){
|
||||
final int bottomDiff=curY+draggedView.getHeight()-(scrollableParent.getHeight()-getPaddingBottom());
|
||||
if(bottomDiff>0){
|
||||
scrollY=bottomDiff;
|
||||
}
|
||||
}
|
||||
}
|
||||
if(scrollX!=0){
|
||||
scrollX=interpolateOutOfBoundsScroll(draggedView.getWidth(), scrollX, scrollableParent.getWidth(), scrollDuration);
|
||||
}
|
||||
if(scrollY!=0){
|
||||
scrollY=interpolateOutOfBoundsScroll(draggedView.getHeight(), scrollY, scrollableParent.getHeight(), scrollDuration);
|
||||
}
|
||||
if(scrollX!=0 || scrollY!=0){
|
||||
if(dragScrollStartTime==Long.MIN_VALUE){
|
||||
dragScrollStartTime=now;
|
||||
}
|
||||
int prevX=scrollableParent.getScrollX();
|
||||
int prevY=scrollableParent.getScrollY();
|
||||
scrollableParent.scrollBy(scrollX, scrollY);
|
||||
draggedView.setTranslationX(draggedView.getTranslationX()-(scrollableParent.getScrollX()-prevX));
|
||||
draggedView.setTranslationY(draggedView.getTranslationY()-(scrollableParent.getScrollY()-prevY));
|
||||
return true;
|
||||
}
|
||||
dragScrollStartTime=Long.MIN_VALUE;
|
||||
return false;
|
||||
}
|
||||
|
||||
public int interpolateOutOfBoundsScroll(int viewSize, int viewSizeOutOfBounds, int totalSize, long msSinceStartScroll){
|
||||
final int maxScroll=getMaxDragScroll();
|
||||
final int absOutOfBounds=Math.abs(viewSizeOutOfBounds);
|
||||
final int direction=(int) Math.signum(viewSizeOutOfBounds);
|
||||
// might be negative if other direction
|
||||
float outOfBoundsRatio=Math.min(1f, 1f*absOutOfBounds/viewSize);
|
||||
final int cappedScroll=(int) (direction*maxScroll*sDragViewScrollCapInterpolator.getInterpolation(outOfBoundsRatio));
|
||||
final float timeRatio;
|
||||
if(msSinceStartScroll>DRAG_SCROLL_ACCELERATION_LIMIT_TIME_MS){
|
||||
timeRatio=1f;
|
||||
}else{
|
||||
timeRatio=(float) msSinceStartScroll/DRAG_SCROLL_ACCELERATION_LIMIT_TIME_MS;
|
||||
}
|
||||
final int value=(int) (cappedScroll*sDragScrollInterpolator.getInterpolation(timeRatio));
|
||||
if(value==0){
|
||||
return viewSizeOutOfBounds>0 ? 1 : -1;
|
||||
}
|
||||
return value;
|
||||
}
|
||||
|
||||
private int getMaxDragScroll(){
|
||||
if(cachedMaxScrollSpeed==-1){
|
||||
cachedMaxScrollSpeed=getResources().getDimensionPixelSize(androidx.recyclerview.R.dimen.item_touch_helper_max_drag_scroll_per_frame);
|
||||
}
|
||||
return cachedMaxScrollSpeed;
|
||||
}
|
||||
|
||||
public interface OnDragListener{
|
||||
void onSwapItems(int oldIndex, int newIndex);
|
||||
|
||||
default void onDragStart(View view){
|
||||
view.animate().translationZ(V.dp(3f)).setDuration(150).setInterpolator(CubicBezierInterpolator.DEFAULT).start();
|
||||
}
|
||||
|
||||
default void onDragEnd(View view){
|
||||
view.animate().translationY(0f).translationX(0f).translationZ(0f).setDuration(200).setInterpolator(CubicBezierInterpolator.DEFAULT).start();
|
||||
}
|
||||
|
||||
default void onDragMove(View view){}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -40,7 +40,7 @@ public class TextInputFrameLayout extends FrameLayout {
|
||||
editText.setHint(hint);
|
||||
editText.setText(text);
|
||||
FrameLayout.LayoutParams params = new FrameLayout.LayoutParams(ViewGroup.LayoutParams.MATCH_PARENT, ViewGroup.LayoutParams.WRAP_CONTENT);
|
||||
params.setMargins(V.dp(24), V.dp(4), V.dp(24), V.dp(16));
|
||||
params.setMargins(V.dp(24), V.dp(4), V.dp(24), 0);
|
||||
editText.setLayoutParams(params);
|
||||
addView(editText);
|
||||
}
|
||||
|
||||
@@ -0,0 +1,29 @@
|
||||
package org.joinmastodon.android.ui.views;
|
||||
|
||||
import android.content.Context;
|
||||
import android.util.AttributeSet;
|
||||
import android.widget.LinearLayout;
|
||||
|
||||
public class TopBarsScrollAwayLinearLayout extends LinearLayout{
|
||||
public TopBarsScrollAwayLinearLayout(Context context){
|
||||
this(context, null);
|
||||
}
|
||||
|
||||
public TopBarsScrollAwayLinearLayout(Context context, AttributeSet attrs){
|
||||
this(context, attrs, 0);
|
||||
}
|
||||
|
||||
public TopBarsScrollAwayLinearLayout(Context context, AttributeSet attrs, int defStyle){
|
||||
super(context, attrs, defStyle);
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec){
|
||||
super.onMeasure(widthMeasureSpec, heightMeasureSpec);
|
||||
int topBarsHeight=0;
|
||||
for(int i=0;i<getChildCount()-1;i++){
|
||||
topBarsHeight+=getChildAt(i).getMeasuredHeight();
|
||||
}
|
||||
super.onMeasure(widthMeasureSpec, (MeasureSpec.getSize(heightMeasureSpec)+topBarsHeight) | MeasureSpec.EXACTLY);
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user