Settings M3 redesign wip
This commit is contained in:
@@ -111,21 +111,12 @@ public class AccountSwitcherSheet extends BottomSheet{
|
||||
}
|
||||
|
||||
private void logOut(String accountID){
|
||||
AccountSession session=AccountSessionManager.getInstance().getAccount(accountID);
|
||||
new RevokeOauthToken(session.app.clientId, session.app.clientSecret, session.token.accessToken)
|
||||
.setCallback(new Callback<>(){
|
||||
@Override
|
||||
public void onSuccess(Object result){
|
||||
onLoggedOut(accountID);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onError(ErrorResponse error){
|
||||
onLoggedOut(accountID);
|
||||
}
|
||||
})
|
||||
.wrapProgress(activity, R.string.loading, false)
|
||||
.exec(accountID);
|
||||
AccountSessionManager.get(accountID).logOut(activity, ()->{
|
||||
dismiss();
|
||||
activity.finish();
|
||||
Intent intent=new Intent(activity, MainActivity.class);
|
||||
activity.startActivity(intent);
|
||||
});
|
||||
}
|
||||
|
||||
private void logOutAll(){
|
||||
@@ -163,11 +154,6 @@ public class AccountSwitcherSheet extends BottomSheet{
|
||||
}
|
||||
}
|
||||
|
||||
private void onLoggedOut(String accountID){
|
||||
AccountSessionManager.getInstance().removeAccount(accountID);
|
||||
dismiss();
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void onWindowInsetsUpdated(WindowInsets insets){
|
||||
if(Build.VERSION.SDK_INT>=29){
|
||||
|
||||
@@ -2,12 +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.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);
|
||||
}
|
||||
@@ -18,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
|
||||
@@ -49,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,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();
|
||||
}
|
||||
}
|
||||
@@ -9,6 +9,7 @@ import android.widget.ImageView;
|
||||
import android.widget.TextView;
|
||||
|
||||
import org.joinmastodon.android.R;
|
||||
import org.joinmastodon.android.api.session.AccountSessionManager;
|
||||
import org.joinmastodon.android.fragments.BaseStatusListFragment;
|
||||
import org.joinmastodon.android.model.Account;
|
||||
import org.joinmastodon.android.ui.OutlineProviders;
|
||||
@@ -29,7 +30,10 @@ public class AccountStatusDisplayItem extends StatusDisplayItem{
|
||||
public AccountStatusDisplayItem(String parentID, BaseStatusListFragment parentFragment, Account account){
|
||||
super(parentID, parentFragment);
|
||||
this.account=account;
|
||||
parsedName=HtmlParser.parseCustomEmoji(account.displayName, account.emojis);
|
||||
if(AccountSessionManager.get(parentFragment.getAccountID()).getLocalPreferences().customEmojiInNames)
|
||||
parsedName=HtmlParser.parseCustomEmoji(account.displayName, account.emojis);
|
||||
else
|
||||
parsedName=account.displayName;
|
||||
emojiHelper.setText(parsedName);
|
||||
if(!TextUtils.isEmpty(account.avatar))
|
||||
avaRequest=new UrlImageLoaderRequest(account.avatar, V.dp(50), V.dp(50));
|
||||
|
||||
@@ -103,7 +103,7 @@ public class AudioStatusDisplayItem extends StatusDisplayItem{
|
||||
@Override
|
||||
public void onBind(AudioStatusDisplayItem item){
|
||||
int seconds=(int)item.attachment.getDuration();
|
||||
String duration=UiUtils.formatDuration(seconds);
|
||||
String duration=UiUtils.formatMediaDuration(seconds);
|
||||
AudioPlayerService service=AudioPlayerService.getInstance();
|
||||
if(service!=null && service.getAttachmentID().equals(item.attachment.id)){
|
||||
forwardBtn.setVisibility(View.VISIBLE);
|
||||
@@ -168,7 +168,7 @@ public class AudioStatusDisplayItem extends StatusDisplayItem{
|
||||
setPlayButtonPlaying(false, true);
|
||||
forwardBtn.setVisibility(View.INVISIBLE);
|
||||
rewindBtn.setVisibility(View.INVISIBLE);
|
||||
time.setText(UiUtils.formatDuration((int)item.attachment.getDuration()));
|
||||
time.setText(UiUtils.formatMediaDuration((int)item.attachment.getDuration()));
|
||||
}
|
||||
}
|
||||
|
||||
@@ -187,7 +187,7 @@ public class AudioStatusDisplayItem extends StatusDisplayItem{
|
||||
int posSeconds=(int)pos;
|
||||
if(posSeconds!=lastPosSeconds){
|
||||
lastPosSeconds=posSeconds;
|
||||
time.setText(UiUtils.formatDuration(posSeconds)+"/"+UiUtils.formatDuration((int)item.attachment.getDuration()));
|
||||
time.setText(UiUtils.formatMediaDuration(posSeconds)+"/"+UiUtils.formatMediaDuration((int)item.attachment.getDuration()));
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -68,7 +68,7 @@ public class ExtendedFooterStatusDisplayItem extends StatusDisplayItem{
|
||||
reblogs.setText(itemView.getResources().getQuantityString(R.plurals.x_reblogs, (int)item.status.reblogsCount, item.status.reblogsCount));
|
||||
if(s.editedAt!=null){
|
||||
editHistory.setVisibility(View.VISIBLE);
|
||||
editHistory.setText(item.parentFragment.getString(R.string.last_edit_at_x, UiUtils.formatRelativeTimestampAsMinutesAgo(itemView.getContext(), s.editedAt)));
|
||||
editHistory.setText(item.parentFragment.getString(R.string.last_edit_at_x, UiUtils.formatRelativeTimestampAsMinutesAgo(itemView.getContext(), s.editedAt, false)));
|
||||
}else{
|
||||
editHistory.setVisibility(View.GONE);
|
||||
}
|
||||
|
||||
@@ -6,13 +6,16 @@ import android.content.res.ColorStateList;
|
||||
import android.graphics.Color;
|
||||
import android.os.Build;
|
||||
import android.os.Bundle;
|
||||
import android.view.MenuItem;
|
||||
import android.view.View;
|
||||
import android.view.ViewGroup;
|
||||
import android.view.accessibility.AccessibilityNodeInfo;
|
||||
import android.widget.Button;
|
||||
import android.widget.ImageView;
|
||||
import android.widget.PopupMenu;
|
||||
import android.widget.TextView;
|
||||
|
||||
import org.joinmastodon.android.GlobalUserPreferences;
|
||||
import org.joinmastodon.android.R;
|
||||
import org.joinmastodon.android.api.session.AccountSessionManager;
|
||||
import org.joinmastodon.android.fragments.BaseStatusListFragment;
|
||||
@@ -133,6 +136,20 @@ public class FooterStatusDisplayItem extends StatusDisplayItem{
|
||||
}
|
||||
|
||||
private void onBoostClick(View v){
|
||||
if(GlobalUserPreferences.confirmBoost){
|
||||
PopupMenu menu=new PopupMenu(itemView.getContext(), boost);
|
||||
menu.getMenu().add(R.string.button_reblog);
|
||||
menu.setOnMenuItemClickListener(item->{
|
||||
doBoost();
|
||||
return true;
|
||||
});
|
||||
menu.show();
|
||||
}else{
|
||||
doBoost();
|
||||
}
|
||||
}
|
||||
|
||||
private void doBoost(){
|
||||
AccountSessionManager.getInstance().getAccount(item.accountID).getStatusInteractionController().setReblogged(item.status, !item.status.reblogged);
|
||||
boost.setSelected(item.status.reblogged);
|
||||
bindButton(boost, item.status.reblogsCount);
|
||||
|
||||
@@ -71,7 +71,8 @@ public class HeaderStatusDisplayItem extends StatusDisplayItem{
|
||||
this.accountID=accountID;
|
||||
parsedName=new SpannableStringBuilder(user.displayName);
|
||||
this.status=status;
|
||||
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);
|
||||
|
||||
@@ -6,7 +6,9 @@ import android.text.TextUtils;
|
||||
import android.view.View;
|
||||
import android.view.ViewGroup;
|
||||
|
||||
import org.joinmastodon.android.GlobalUserPreferences;
|
||||
import org.joinmastodon.android.R;
|
||||
import org.joinmastodon.android.api.session.AccountSessionManager;
|
||||
import org.joinmastodon.android.fragments.BaseStatusListFragment;
|
||||
import org.joinmastodon.android.fragments.ThreadFragment;
|
||||
import org.joinmastodon.android.model.Account;
|
||||
@@ -90,6 +92,7 @@ public abstract class StatusDisplayItem{
|
||||
ArrayList<StatusDisplayItem> items=new ArrayList<>();
|
||||
Status statusForContent=status.getContentStatus();
|
||||
HeaderStatusDisplayItem header=null;
|
||||
boolean hideCounts=!AccountSessionManager.get(accountID).getLocalPreferences().showInteractionCounts;
|
||||
if((flags & FLAG_NO_HEADER)==0){
|
||||
if(status.reblog!=null){
|
||||
items.add(new ReblogOrReplyLineStatusDisplayItem(parentID, fragment, fragment.getString(R.string.user_boosted, status.account.displayName), status.account.emojis, R.drawable.ic_repeat_20px));
|
||||
@@ -104,7 +107,7 @@ public abstract class StatusDisplayItem{
|
||||
}
|
||||
|
||||
ArrayList<StatusDisplayItem> contentItems;
|
||||
if(!TextUtils.isEmpty(statusForContent.spoilerText)){
|
||||
if(!TextUtils.isEmpty(statusForContent.spoilerText) && AccountSessionManager.get(accountID).getLocalPreferences().showCWs){
|
||||
SpoilerStatusDisplayItem spoilerItem=new SpoilerStatusDisplayItem(parentID, fragment, statusForContent);
|
||||
items.add(spoilerItem);
|
||||
contentItems=spoilerItem.contentItems;
|
||||
@@ -126,6 +129,8 @@ public abstract class StatusDisplayItem{
|
||||
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().hideSensitiveMedia)
|
||||
mediaGrid.sensitiveRevealed=true;
|
||||
contentItems.add(mediaGrid);
|
||||
}
|
||||
for(Attachment att:statusForContent.mediaAttachments){
|
||||
@@ -140,7 +145,9 @@ public abstract class StatusDisplayItem{
|
||||
contentItems.add(new LinkCardStatusDisplayItem(parentID, fragment, statusForContent));
|
||||
}
|
||||
if((flags & FLAG_NO_FOOTER)==0){
|
||||
items.add(new FooterStatusDisplayItem(parentID, fragment, statusForContent, accountID));
|
||||
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));
|
||||
}
|
||||
|
||||
@@ -60,7 +60,7 @@ public class MediaAttachmentViewController{
|
||||
altButton.setVisibility(TextUtils.isEmpty(attachment.description) ? View.GONE : View.VISIBLE);
|
||||
}
|
||||
if(type==MediaGridStatusDisplayItem.GridItemType.VIDEO){
|
||||
duration.setText(UiUtils.formatDuration((int)attachment.getDuration()));
|
||||
duration.setText(UiUtils.formatMediaDuration((int)attachment.getDuration()));
|
||||
}
|
||||
didClear=false;
|
||||
}
|
||||
|
||||
@@ -71,6 +71,7 @@ import org.parceler.Parcels;
|
||||
import java.io.File;
|
||||
import java.lang.reflect.Method;
|
||||
import java.time.Instant;
|
||||
import java.time.LocalDate;
|
||||
import java.time.ZoneId;
|
||||
import java.time.ZonedDateTime;
|
||||
import java.time.format.DateTimeFormatter;
|
||||
@@ -99,6 +100,7 @@ import okhttp3.MediaType;
|
||||
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");
|
||||
private static final DateTimeFormatter TIME_FORMATTER=DateTimeFormatter.ofLocalizedTime(FormatStyle.SHORT);
|
||||
public static final DateTimeFormatter DATE_TIME_FORMATTER=DateTimeFormatter.ofLocalizedDateTime(FormatStyle.LONG, FormatStyle.SHORT);
|
||||
|
||||
private UiUtils(){}
|
||||
@@ -144,21 +146,52 @@ public class UiUtils{
|
||||
}
|
||||
}
|
||||
|
||||
public static String formatRelativeTimestampAsMinutesAgo(Context context, Instant instant){
|
||||
public static String formatRelativeTimestampAsMinutesAgo(Context context, Instant instant, boolean relativeHours){
|
||||
long t=instant.toEpochMilli();
|
||||
long now=System.currentTimeMillis();
|
||||
long diff=now-t;
|
||||
if(diff<1000L){
|
||||
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 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{
|
||||
return DATE_TIME_FORMATTER.format(instant.atZone(ZoneId.systemDefault()));
|
||||
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){
|
||||
@@ -317,7 +350,7 @@ public class UiUtils{
|
||||
}
|
||||
|
||||
public static void showConfirmationAlert(Context context, @StringRes int title, @StringRes int message, @StringRes int confirmButton, Runnable onConfirmed){
|
||||
showConfirmationAlert(context, context.getString(title), context.getString(message), context.getString(confirmButton), onConfirmed);
|
||||
showConfirmationAlert(context, context.getString(title), message==0 ? null : context.getString(message), context.getString(confirmButton), onConfirmed);
|
||||
}
|
||||
|
||||
public static void showConfirmationAlert(Context context, CharSequence title, CharSequence message, CharSequence confirmButton, Runnable onConfirmed){
|
||||
@@ -399,24 +432,26 @@ public class UiUtils{
|
||||
}
|
||||
|
||||
public static void confirmDeletePost(Activity activity, String accountID, Status status, Consumer<Status> resultCallback){
|
||||
showConfirmationAlert(activity, R.string.confirm_delete_title, R.string.confirm_delete, R.string.delete, ()->{
|
||||
new DeleteStatus(status.id)
|
||||
.setCallback(new Callback<>(){
|
||||
@Override
|
||||
public void onSuccess(Status result){
|
||||
resultCallback.accept(result);
|
||||
AccountSessionManager.getInstance().getAccount(accountID).getCacheController().deleteStatus(status.id);
|
||||
E.post(new StatusDeletedEvent(status.id, accountID));
|
||||
}
|
||||
Runnable delete=()->new DeleteStatus(status.id)
|
||||
.setCallback(new Callback<>(){
|
||||
@Override
|
||||
public void onSuccess(Status result){
|
||||
resultCallback.accept(result);
|
||||
AccountSessionManager.getInstance().getAccount(accountID).getCacheController().deleteStatus(status.id);
|
||||
E.post(new StatusDeletedEvent(status.id, accountID));
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onError(ErrorResponse error){
|
||||
error.showToast(activity);
|
||||
}
|
||||
})
|
||||
.wrapProgress(activity, R.string.deleting, false)
|
||||
.exec(accountID);
|
||||
});
|
||||
@Override
|
||||
public void onError(ErrorResponse error){
|
||||
error.showToast(activity);
|
||||
}
|
||||
})
|
||||
.wrapProgress(activity, R.string.deleting, false)
|
||||
.exec(accountID);
|
||||
if(GlobalUserPreferences.confirmDeletePost)
|
||||
showConfirmationAlert(activity, R.string.confirm_delete_title, R.string.confirm_delete, R.string.delete, delete);
|
||||
else
|
||||
delete.run();
|
||||
}
|
||||
|
||||
public static void setRelationshipToActionButton(Relationship relationship, Button button){
|
||||
@@ -488,25 +523,32 @@ public class UiUtils{
|
||||
}else if(relationship.muting){
|
||||
confirmToggleMuteUser(activity, accountID, account, true, resultCallback);
|
||||
}else{
|
||||
progressCallback.accept(true);
|
||||
new SetAccountFollowed(account.id, !relationship.following && !relationship.requested, true)
|
||||
.setCallback(new Callback<>(){
|
||||
@Override
|
||||
public void onSuccess(Relationship result){
|
||||
resultCallback.accept(result);
|
||||
progressCallback.accept(false);
|
||||
if(!result.following && !result.requested){
|
||||
E.post(new RemoveAccountPostsEvent(accountID, account.id, true));
|
||||
Runnable action=()->{
|
||||
progressCallback.accept(true);
|
||||
new SetAccountFollowed(account.id, !relationship.following && !relationship.requested, true)
|
||||
.setCallback(new Callback<>(){
|
||||
@Override
|
||||
public void onSuccess(Relationship result){
|
||||
resultCallback.accept(result);
|
||||
progressCallback.accept(false);
|
||||
if(!result.following && !result.requested){
|
||||
E.post(new RemoveAccountPostsEvent(accountID, account.id, true));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onError(ErrorResponse error){
|
||||
error.showToast(activity);
|
||||
progressCallback.accept(false);
|
||||
}
|
||||
})
|
||||
.exec(accountID);
|
||||
@Override
|
||||
public void onError(ErrorResponse error){
|
||||
error.showToast(activity);
|
||||
progressCallback.accept(false);
|
||||
}
|
||||
})
|
||||
.exec(accountID);
|
||||
};
|
||||
if(relationship.following && GlobalUserPreferences.confirmUnfollow){
|
||||
showConfirmationAlert(activity, null, activity.getString(R.string.unfollow_confirmation, account.getDisplayUsername()), activity.getString(R.string.unfollow), action);
|
||||
}else{
|
||||
action.run();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -586,9 +628,9 @@ public class UiUtils{
|
||||
|
||||
public static void setUserPreferredTheme(Context context){
|
||||
context.setTheme(switch(GlobalUserPreferences.theme){
|
||||
case AUTO -> GlobalUserPreferences.trueBlackTheme ? R.style.Theme_Mastodon_AutoLightDark_TrueBlack : R.style.Theme_Mastodon_AutoLightDark;
|
||||
case AUTO -> R.style.Theme_Mastodon_AutoLightDark;
|
||||
case LIGHT -> R.style.Theme_Mastodon_Light;
|
||||
case DARK -> GlobalUserPreferences.trueBlackTheme ? R.style.Theme_Mastodon_Dark_TrueBlack : R.style.Theme_Mastodon_Dark;
|
||||
case DARK -> R.style.Theme_Mastodon_Dark;
|
||||
});
|
||||
}
|
||||
|
||||
@@ -718,7 +760,7 @@ public class UiUtils{
|
||||
}
|
||||
|
||||
@SuppressLint("DefaultLocale")
|
||||
public static String formatDuration(int seconds){
|
||||
public static String formatMediaDuration(int seconds){
|
||||
if(seconds>=3600)
|
||||
return String.format("%d:%02d:%02d", seconds/3600, seconds%3600/60, seconds%60);
|
||||
else
|
||||
@@ -750,4 +792,20 @@ public class UiUtils{
|
||||
}
|
||||
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);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -19,6 +19,7 @@ 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.model.viewmodel.AccountViewModel;
|
||||
import org.joinmastodon.android.ui.BetterItemAnimator;
|
||||
import org.joinmastodon.android.ui.OutlineProviders;
|
||||
import org.joinmastodon.android.ui.text.HtmlParser;
|
||||
@@ -58,7 +59,7 @@ public class ComposeAutocompleteViewController{
|
||||
private FrameLayout contentView;
|
||||
private UsableRecyclerView list;
|
||||
private ListImageLoaderWrapper imgLoader;
|
||||
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;
|
||||
@@ -226,8 +227,8 @@ public class ComposeAutocompleteViewController{
|
||||
@Override
|
||||
public void onSuccess(SearchResults result){
|
||||
currentRequest=null;
|
||||
List<WrappedAccount> oldList=users;
|
||||
users=result.accounts.stream().map(WrappedAccount::new).collect(Collectors.toList());
|
||||
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){
|
||||
@@ -313,7 +314,7 @@ public class ComposeAutocompleteViewController{
|
||||
|
||||
@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);
|
||||
@@ -325,7 +326,7 @@ public class ComposeAutocompleteViewController{
|
||||
}
|
||||
}
|
||||
|
||||
private class UserViewHolder extends BindableViewHolder<WrappedAccount> implements ImageLoaderViewHolder, UsableRecyclerView.Clickable{
|
||||
private class UserViewHolder extends BindableViewHolder<AccountViewModel> implements ImageLoaderViewHolder, UsableRecyclerView.Clickable{
|
||||
protected final ImageView ava;
|
||||
protected final TextView username;
|
||||
|
||||
@@ -338,7 +339,7 @@ public class ComposeAutocompleteViewController{
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onBind(WrappedAccount item){
|
||||
public void onBind(AccountViewModel item){
|
||||
username.setText("@"+item.account.acct);
|
||||
}
|
||||
|
||||
@@ -483,21 +484,6 @@ public class ComposeAutocompleteViewController{
|
||||
}
|
||||
}
|
||||
|
||||
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));
|
||||
}
|
||||
}
|
||||
|
||||
private static class WrappedEmoji{
|
||||
private Emoji emoji;
|
||||
private ImageLoaderRequest request;
|
||||
|
||||
@@ -89,10 +89,32 @@ public class ComposeLanguageAlertViewController{
|
||||
}
|
||||
|
||||
if(previouslySelected!=null){
|
||||
if((previouslySelected.index<specialLocales.size() && Objects.equals(previouslySelected.locale, specialLocales.get(previouslySelected.index).locale)) ||
|
||||
(previouslySelected.index<specialLocales.size()+allLocales.size() && Objects.equals(previouslySelected.locale, allLocales.get(previouslySelected.index-specialLocales.size()).locale))){
|
||||
if(previouslySelected.index!=-1 && ((previouslySelected.index<specialLocales.size() && Objects.equals(previouslySelected.locale, specialLocales.get(previouslySelected.index).locale)) ||
|
||||
(previouslySelected.index<specialLocales.size()+allLocales.size() && Objects.equals(previouslySelected.locale, allLocales.get(previouslySelected.index-specialLocales.size()).locale)))){
|
||||
selectedIndex=previouslySelected.index;
|
||||
selectedLocale=previouslySelected.locale;
|
||||
}else{
|
||||
int i=0;
|
||||
boolean found=false;
|
||||
for(SpecialLocaleInfo li:specialLocales){
|
||||
if(li.locale.equals(previouslySelected.locale)){
|
||||
selectedLocale=li.locale;
|
||||
selectedIndex=i;
|
||||
found=true;
|
||||
break;
|
||||
}
|
||||
i++;
|
||||
}
|
||||
if(!found){
|
||||
for(LocaleInfo li:allLocales){
|
||||
if(li.locale.equals(previouslySelected.locale)){
|
||||
selectedLocale=li.locale;
|
||||
selectedIndex=i;
|
||||
break;
|
||||
}
|
||||
i++;
|
||||
}
|
||||
}
|
||||
}
|
||||
}else{
|
||||
selectedLocale=specialLocales.get(0).locale;
|
||||
|
||||
@@ -543,6 +543,23 @@ public class ComposeMediaViewController{
|
||||
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.startsWith("image/"))
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
public int getMaxAttachments(){
|
||||
return MAX_ATTACHMENTS;
|
||||
}
|
||||
|
||||
@@ -108,7 +108,7 @@ public class ComposePollViewController{
|
||||
updatePollOptionHints();
|
||||
pollDuration=savedInstanceState.getInt("pollDuration");
|
||||
pollIsMultipleChoice=savedInstanceState.getBoolean("pollMultiple");
|
||||
pollDurationValue.setText(formatPollDuration(pollDuration));
|
||||
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);
|
||||
@@ -119,7 +119,7 @@ public class ComposePollViewController{
|
||||
opt.edit.setText(oldOpt.edit.getText());
|
||||
}
|
||||
updatePollOptionHints();
|
||||
pollDurationValue.setText(formatPollDuration(pollDuration));
|
||||
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);
|
||||
@@ -129,11 +129,11 @@ public class ComposePollViewController{
|
||||
}
|
||||
pollDuration=(int)fragment.editingStatus.poll.expiresAt.minus(fragment.editingStatus.createdAt.toEpochMilli(), ChronoUnit.MILLIS).getEpochSecond();
|
||||
updatePollOptionHints();
|
||||
pollDurationValue.setText(formatPollDuration(pollDuration));
|
||||
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(formatPollDuration(24*3600));
|
||||
pollDurationValue.setText(UiUtils.formatDuration(fragment.getContext(), 24*3600));
|
||||
pollStyleValue.setText(R.string.compose_poll_single_choice);
|
||||
}
|
||||
}
|
||||
@@ -186,7 +186,7 @@ public class ComposePollViewController{
|
||||
int selectedOption=-1;
|
||||
for(int i=0;i<POLL_LENGTH_OPTIONS.length;i++){
|
||||
int l=POLL_LENGTH_OPTIONS[i];
|
||||
options[i]=formatPollDuration(l);
|
||||
options[i]=UiUtils.formatDuration(fragment.getContext(), l);
|
||||
if(l==pollDuration)
|
||||
selectedOption=i;
|
||||
}
|
||||
@@ -196,25 +196,12 @@ public class ComposePollViewController{
|
||||
.setTitle(R.string.poll_length)
|
||||
.setPositiveButton(R.string.ok, (dialog, which)->{
|
||||
pollDuration=POLL_LENGTH_OPTIONS[chosenOption[0]];
|
||||
pollDurationValue.setText(formatPollDuration(pollDuration));
|
||||
pollDurationValue.setText(UiUtils.formatDuration(fragment.getContext(), pollDuration));
|
||||
})
|
||||
.setNegativeButton(R.string.cancel, null)
|
||||
.show();
|
||||
}
|
||||
|
||||
private String formatPollDuration(int seconds){
|
||||
if(seconds<3600){
|
||||
int minutes=seconds/60;
|
||||
return fragment.getResources().getQuantityString(R.plurals.x_minutes, minutes, minutes);
|
||||
}else if(seconds<24*3600){
|
||||
int hours=seconds/3600;
|
||||
return fragment.getResources().getQuantityString(R.plurals.x_hours, hours, hours);
|
||||
}else{
|
||||
int days=seconds/(24*3600);
|
||||
return fragment.getResources().getQuantityString(R.plurals.x_days, days, days);
|
||||
}
|
||||
}
|
||||
|
||||
private void showPollStyleAlert(){
|
||||
final int[] option={pollIsMultipleChoice ? R.id.multiple_choice : R.id.single_choice};
|
||||
AlertDialog alert=new M3AlertDialogBuilder(fragment.getActivity())
|
||||
|
||||
@@ -110,6 +110,8 @@ public class AccountViewHolder extends BindableViewHolder<AccountViewModel> impl
|
||||
}
|
||||
|
||||
public void bindRelationship(){
|
||||
if(relationships==null)
|
||||
return;
|
||||
Relationship rel=relationships.get(item.account.id);
|
||||
if(rel==null || AccountSessionManager.getInstance().isSelf(accountID, item.account)){
|
||||
button.setVisibility(View.GONE);
|
||||
@@ -193,6 +195,8 @@ public class AccountViewHolder extends BindableViewHolder<AccountViewModel> impl
|
||||
}
|
||||
|
||||
private void onButtonClick(View v){
|
||||
if(relationships==null)
|
||||
return;
|
||||
ProgressDialog progress=new ProgressDialog(fragment.getActivity());
|
||||
progress.setMessage(fragment.getString(R.string.loading));
|
||||
progress.setCancelable(false);
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
@@ -98,7 +98,7 @@ public class FloatingHintEditTextLayout extends FrameLayout implements CustomVie
|
||||
|
||||
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(dp(16), dp(4), dp(16), 0);
|
||||
@@ -106,6 +106,10 @@ public class FloatingHintEditTextLayout extends FrameLayout implements CustomVie
|
||||
addView(errorView);
|
||||
}
|
||||
|
||||
public void updateHint(){
|
||||
label.setText(edit.getHint());
|
||||
}
|
||||
|
||||
private void onTextChanged(Editable text){
|
||||
if(errorState){
|
||||
errorView.setVisibility(View.GONE);
|
||||
|
||||
@@ -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,19 +15,22 @@ 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
|
||||
@@ -33,7 +41,7 @@ public class NestedRecyclerScrollView extends CustomScrollView{
|
||||
consumed[1]=dy;
|
||||
return;
|
||||
}
|
||||
}else if((dy<0 && target instanceof RecyclerView rv && isScrolledToTop(rv)) || (dy>0 && !isScrolledToBottom())){
|
||||
}else if((dy<0 && isScrolledToTop(target)) || (dy>0 && !isScrolledToBottom())){
|
||||
scrollBy(0, dy);
|
||||
consumed[1]=dy;
|
||||
return;
|
||||
@@ -48,7 +56,7 @@ public class NestedRecyclerScrollView extends CustomScrollView{
|
||||
fling((int)velY);
|
||||
return true;
|
||||
}
|
||||
}else if((velY<0 && target instanceof RecyclerView rv && isScrolledToTop(rv)) || (velY>0 && !isScrolledToBottom())){
|
||||
}else if((velY<0 && isScrolledToTop(target)) || (velY>0 && !isScrolledToBottom())){
|
||||
fling((int) velY);
|
||||
return true;
|
||||
}
|
||||
@@ -59,22 +67,40 @@ public class NestedRecyclerScrollView extends CustomScrollView{
|
||||
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 || takePriorityOverChildViews){
|
||||
RecyclerView view=scrollableChildSupplier.get();
|
||||
if(view!=null){
|
||||
return view.fling(0, (int) velocity);
|
||||
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;
|
||||
|
||||
Reference in New Issue
Block a user