Better account switcher

This commit is contained in:
Grishka 2022-05-01 00:44:28 +03:00
parent ec38210dde
commit 8059120136
16 changed files with 396 additions and 23 deletions

View File

@ -58,7 +58,7 @@ dependencies {
implementation 'me.grishka.litex:dynamicanimation:1.1.0-alpha03'
implementation 'me.grishka.litex:viewpager:1.0.0'
implementation 'me.grishka.litex:viewpager2:1.0.0'
implementation 'me.grishka.appkit:appkit:1.2.3'
implementation 'me.grishka.appkit:appkit:1.2.4'
implementation 'com.google.code.gson:gson:2.8.9'
implementation 'org.jsoup:jsoup:1.14.3'
implementation 'com.squareup:otto:1.3.8'

View File

@ -5,6 +5,7 @@ import android.app.NotificationManager;
import android.content.Intent;
import android.content.res.Configuration;
import android.graphics.Outline;
import android.graphics.drawable.ColorDrawable;
import android.os.Build;
import android.os.Bundle;
import android.util.Log;
@ -17,6 +18,7 @@ import android.view.WindowInsets;
import android.widget.FrameLayout;
import android.widget.ImageView;
import android.widget.LinearLayout;
import android.widget.TextView;
import org.joinmastodon.android.MainActivity;
import org.joinmastodon.android.MastodonApp;
@ -28,6 +30,7 @@ import org.joinmastodon.android.api.session.AccountSessionManager;
import org.joinmastodon.android.fragments.discover.DiscoverFragment;
import org.joinmastodon.android.fragments.discover.SearchFragment;
import org.joinmastodon.android.model.Account;
import org.joinmastodon.android.ui.AccountSwitcherSheet;
import org.joinmastodon.android.ui.M3AlertDialogBuilder;
import org.joinmastodon.android.ui.utils.UiUtils;
import org.joinmastodon.android.ui.views.TabBar;
@ -46,6 +49,7 @@ import me.grishka.appkit.fragments.OnBackPressedListener;
import me.grishka.appkit.imageloader.ViewImageLoader;
import me.grishka.appkit.imageloader.requests.UrlImageLoaderRequest;
import me.grishka.appkit.utils.V;
import me.grishka.appkit.views.BottomSheet;
import me.grishka.appkit.views.FragmentRootLinearLayout;
public class HomeFragment extends AppKitFragment implements OnBackPressedListener{
@ -238,21 +242,11 @@ public class HomeFragment extends AppKitFragment implements OnBackPressedListene
private boolean onTabLongClick(@IdRes int tab){
if(tab==R.id.tab_profile){
ArrayList<String> options=new ArrayList<>();
for(AccountSession session:AccountSessionManager.getInstance().getLoggedInAccounts()){
options.add(session.self.displayName+"\n("+session.self.username+"@"+session.domain+")");
}
new M3AlertDialogBuilder(getActivity())
.setItems(options.toArray(new String[0]), (dialog, which)->{
AccountSession session=AccountSessionManager.getInstance().getLoggedInAccounts().get(which);
AccountSessionManager.getInstance().setLastActiveAccountID(session.getID());
getActivity().finish();
getActivity().startActivity(new Intent(getActivity(), MainActivity.class));
})
.setNegativeButton(R.string.add_account, (dialog, which)->{
Nav.go(getActivity(), SplashFragment.class, null);
})
.show();
ArrayList<String> options=new ArrayList<>();
for(AccountSession session:AccountSessionManager.getInstance().getLoggedInAccounts()){
options.add(session.self.displayName+"\n("+session.self.username+"@"+session.domain+")");
}
new AccountSwitcherSheet(getActivity()).show();
return true;
}
return false;

View File

@ -0,0 +1,247 @@
package org.joinmastodon.android.ui;
import android.annotation.SuppressLint;
import android.app.Activity;
import android.content.Context;
import android.content.Intent;
import android.content.res.ColorStateList;
import android.graphics.drawable.Animatable;
import android.graphics.drawable.ColorDrawable;
import android.graphics.drawable.Drawable;
import android.os.Build;
import android.os.Bundle;
import android.view.View;
import android.view.ViewGroup;
import android.view.WindowInsets;
import android.widget.FrameLayout;
import android.widget.ImageButton;
import android.widget.ImageView;
import android.widget.PopupMenu;
import android.widget.TextView;
import org.joinmastodon.android.GlobalUserPreferences;
import org.joinmastodon.android.MainActivity;
import org.joinmastodon.android.R;
import org.joinmastodon.android.api.requests.oauth.RevokeOauthToken;
import org.joinmastodon.android.api.session.AccountSession;
import org.joinmastodon.android.api.session.AccountSessionManager;
import org.joinmastodon.android.fragments.SplashFragment;
import org.joinmastodon.android.model.Account;
import org.joinmastodon.android.ui.utils.UiUtils;
import java.util.List;
import java.util.stream.Collectors;
import androidx.annotation.NonNull;
import androidx.recyclerview.widget.LinearLayoutManager;
import me.grishka.appkit.Nav;
import me.grishka.appkit.api.Callback;
import me.grishka.appkit.api.ErrorResponse;
import me.grishka.appkit.imageloader.ImageLoaderRecyclerAdapter;
import me.grishka.appkit.imageloader.ImageLoaderViewHolder;
import me.grishka.appkit.imageloader.ListImageLoaderWrapper;
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.SingleViewRecyclerAdapter;
import me.grishka.appkit.utils.V;
import me.grishka.appkit.views.BottomSheet;
import me.grishka.appkit.views.UsableRecyclerView;
public class AccountSwitcherSheet extends BottomSheet{
private final Activity activity;
private UsableRecyclerView list;
private List<WrappedAccount> accounts;
private ListImageLoaderWrapper imgLoader;
public AccountSwitcherSheet(@NonNull Activity activity){
super(activity);
this.activity=activity;
accounts=AccountSessionManager.getInstance().getLoggedInAccounts().stream().map(WrappedAccount::new).collect(Collectors.toList());
list=new UsableRecyclerView(activity);
imgLoader=new ListImageLoaderWrapper(activity, list, new RecyclerViewDelegate(list), null);
list.setClipToPadding(false);
list.setLayoutManager(new LinearLayoutManager(activity));
MergeRecyclerAdapter adapter=new MergeRecyclerAdapter();
View handle=new View(activity);
handle.setBackgroundResource(R.drawable.bg_bottom_sheet_handle);
adapter.addAdapter(new SingleViewRecyclerAdapter(handle));
adapter.addAdapter(new AccountsAdapter());
AccountViewHolder holder=new AccountViewHolder();
holder.more.setVisibility(View.GONE);
holder.currentIcon.setVisibility(View.GONE);
holder.name.setText(R.string.add_account);
holder.avatar.setScaleType(ImageView.ScaleType.CENTER);
holder.avatar.setImageResource(R.drawable.ic_fluent_add_circle_24_filled);
holder.avatar.setImageTintList(ColorStateList.valueOf(UiUtils.getThemeColor(activity, android.R.attr.textColorPrimary)));
adapter.addAdapter(new ClickableSingleViewRecyclerAdapter(holder.itemView, ()->{
Nav.go(activity, SplashFragment.class, null);
dismiss();
}));
list.setAdapter(adapter);
DividerItemDecoration divider=new DividerItemDecoration(activity, R.attr.colorPollVoted, .5f, 72, 16, DividerItemDecoration.NOT_FIRST);
divider.setDrawBelowLastItem(true);
list.addItemDecoration(divider);
FrameLayout content=new FrameLayout(activity);
content.setBackgroundResource(R.drawable.bg_bottom_sheet);
content.addView(list);
setContentView(content);
setNavigationBarBackground(new ColorDrawable(UiUtils.getThemeColor(activity, R.attr.colorWindowBackground)), !UiUtils.isDarkTheme());
}
private void confirmLogOut(String accountID){
new M3AlertDialogBuilder(activity)
.setTitle(R.string.log_out)
.setMessage(R.string.confirm_log_out)
.setPositiveButton(R.string.log_out, (dialog, which) -> logOut(accountID))
.setNegativeButton(R.string.cancel, null)
.show();
}
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);
}
private void onLoggedOut(String accountID){
AccountSessionManager.getInstance().removeAccount(accountID);
dismiss();
}
@Override
protected void onWindowInsetsUpdated(WindowInsets insets){
if(Build.VERSION.SDK_INT>=29){
int tappableBottom=insets.getTappableElementInsets().bottom;
int insetBottom=insets.getSystemWindowInsetBottom();
if(tappableBottom==0 && insetBottom>0){
list.setPadding(0, 0, 0, V.dp(48)-insetBottom);
}else{
list.setPadding(0, 0, 0, 0);
}
}
}
private class AccountsAdapter extends UsableRecyclerView.Adapter<AccountViewHolder> implements ImageLoaderRecyclerAdapter{
public AccountsAdapter(){
super(imgLoader);
}
@NonNull
@Override
public AccountViewHolder onCreateViewHolder(@NonNull ViewGroup parent, int viewType){
return new AccountViewHolder();
}
@Override
public int getItemCount(){
return accounts.size();
}
@Override
public void onBindViewHolder(AccountViewHolder holder, int position){
holder.bind(accounts.get(position).session);
super.onBindViewHolder(holder, position);
}
@Override
public int getImageCountForItem(int position){
return 1;
}
@Override
public ImageLoaderRequest getImageRequest(int position, int image){
return accounts.get(position).req;
}
}
private class AccountViewHolder extends BindableViewHolder<AccountSession> implements ImageLoaderViewHolder, UsableRecyclerView.Clickable{
private final TextView name;
private final ImageView avatar;
private final ImageButton more;
private final View currentIcon;
private final PopupMenu menu;
public AccountViewHolder(){
super(activity, R.layout.item_account_switcher, list);
name=findViewById(R.id.name);
avatar=findViewById(R.id.avatar);
more=findViewById(R.id.more);
currentIcon=findViewById(R.id.current);
avatar.setOutlineProvider(OutlineProviders.roundedRect(12));
avatar.setClipToOutline(true);
menu=new PopupMenu(activity, more);
menu.inflate(R.menu.account_switcher);
menu.setOnMenuItemClickListener(item1 -> {
confirmLogOut(item.getID());
return true;
});
more.setOnClickListener(v->menu.show());
}
@SuppressLint("SetTextI18n")
@Override
public void onBind(AccountSession item){
name.setText("@"+item.self.username+"@"+item.domain);
if(AccountSessionManager.getInstance().getLastActiveAccountID().equals(item.getID())){
more.setVisibility(View.GONE);
currentIcon.setVisibility(View.VISIBLE);
}else{
more.setVisibility(View.VISIBLE);
currentIcon.setVisibility(View.GONE);
}
menu.getMenu().findItem(R.id.log_out).setTitle(activity.getString(R.string.log_out_account, "@"+item.self.username));
UiUtils.enablePopupMenuIcons(activity, menu);
}
@Override
public void setImage(int index, Drawable image){
avatar.setImageDrawable(image);
if(image instanceof Animatable a)
a.start();
}
@Override
public void clearImage(int index){
setImage(index, null);
}
@Override
public void onClick(){
AccountSessionManager.getInstance().setLastActiveAccountID(item.getID());
activity.finish();
activity.startActivity(new Intent(activity, MainActivity.class));
}
}
private static class WrappedAccount{
public final AccountSession session;
public final ImageLoaderRequest req;
public WrappedAccount(AccountSession session){
this.session=session;
req=new UrlImageLoaderRequest(GlobalUserPreferences.playGifs ? session.self.avatar : session.self.avatarStatic, V.dp(50), V.dp(50));
}
}
}

View File

@ -0,0 +1,34 @@
package org.joinmastodon.android.ui;
import android.view.View;
import android.view.ViewGroup;
import androidx.annotation.NonNull;
import me.grishka.appkit.utils.SingleViewRecyclerAdapter;
import me.grishka.appkit.views.UsableRecyclerView;
public class ClickableSingleViewRecyclerAdapter extends SingleViewRecyclerAdapter{
private final Runnable onClick;
public ClickableSingleViewRecyclerAdapter(View view, Runnable onClick){
super(view);
this.onClick=onClick;
}
@NonNull
@Override
public ViewViewHolder onCreateViewHolder(@NonNull ViewGroup parent, int viewType){
return new ClickableViewViewHolder(view);
}
public class ClickableViewViewHolder extends ViewViewHolder implements UsableRecyclerView.Clickable{
public ClickableViewViewHolder(@NonNull View itemView){
super(itemView);
}
@Override
public void onClick(){
onClick.run();
}
}
}

View File

@ -18,6 +18,7 @@ public class DividerItemDecoration extends RecyclerView.ItemDecoration{
private Paint paint=new Paint();
private int paddingStart, paddingEnd;
private Predicate<RecyclerView.ViewHolder> drawDividerPredicate;
private boolean drawBelowLastItem;
public static final Predicate<RecyclerView.ViewHolder> NOT_FIRST=vh->vh.getAbsoluteAdapterPosition()>0;
@ -34,6 +35,10 @@ public class DividerItemDecoration extends RecyclerView.ItemDecoration{
this.drawDividerPredicate=drawDividerPredicate;
}
public void setDrawBelowLastItem(boolean drawBelowLastItem){
this.drawBelowLastItem=drawBelowLastItem;
}
@Override
public void onDraw(@NonNull Canvas c, @NonNull RecyclerView parent, @NonNull RecyclerView.State state){
boolean isRTL=parent.getLayoutDirection()==View.LAYOUT_DIRECTION_RTL;
@ -43,7 +48,7 @@ public class DividerItemDecoration extends RecyclerView.ItemDecoration{
for(int i=0;i<parent.getChildCount();i++){
View child=parent.getChildAt(i);
int pos=parent.getChildAdapterPosition(child);
if(pos<totalItems-1 && (drawDividerPredicate==null || drawDividerPredicate.test(parent.getChildViewHolder(child)))){
if((drawBelowLastItem || pos<totalItems-1) && (drawDividerPredicate==null || drawDividerPredicate.test(parent.getChildViewHolder(child)))){
float y=Math.round(child.getY()+child.getHeight());
y-=(y-paint.getStrokeWidth()/2f)%1f; // Make sure the line aligns with the pixel grid
paint.setAlpha(Math.round(255f*child.getAlpha()));

View File

@ -0,0 +1,9 @@
<?xml version="1.0" encoding="utf-8"?>
<layer-list xmlns:android="http://schemas.android.com/apk/res/android">
<item>
<shape>
<solid android:color="?colorWindowBackground"/>
<corners android:topLeftRadius="12dp" android:topRightRadius="12dp"/>
</shape>
</item>
</layer-list>

View File

@ -0,0 +1,12 @@
<?xml version="1.0" encoding="utf-8"?>
<layer-list xmlns:android="http://schemas.android.com/apk/res/android">
<item android:gravity="center" android:width="36dp" android:height="4dp">
<shape>
<solid android:color="?android:textColorSecondary"/>
<corners android:radius="2dp"/>
</shape>
</item>
<item android:height="20dp">
<color android:color="#00000000"/>
</item>
</layer-list>

View File

@ -0,0 +1,3 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android" android:width="24dp" android:height="24dp" android:viewportWidth="24" android:viewportHeight="24">
<path android:pathData="M12 2c5.523 0 10 4.477 10 10s-4.477 10-10 10S2 17.523 2 12 6.477 2 12 2zm0 5c-0.38 0-0.694 0.282-0.743 0.648L11.25 7.75v3.5h-3.5C7.336 11.25 7 11.586 7 12c0 0.38 0.282 0.694 0.648 0.743L7.75 12.75h3.5v3.5c0 0.414 0.336 0.75 0.75 0.75 0.38 0 0.694-0.282 0.743-0.648l0.007-0.102v-3.5h3.5c0.414 0 0.75-0.336 0.75-0.75 0-0.38-0.282-0.694-0.648-0.743L16.25 11.25h-3.5v-3.5C12.75 7.336 12.414 7 12 7z" android:fillColor="@color/fluent_default_icon_tint"/>
</vector>

View File

@ -0,0 +1,3 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android" android:width="24dp" android:height="24dp" android:viewportWidth="24" android:viewportHeight="24">
<path android:pathData="M8.5 16.586l-3.793-3.793c-0.39-0.39-1.024-0.39-1.414 0-0.39 0.39-0.39 1.024 0 1.414l4.5 4.5c0.39 0.39 1.024 0.39 1.414 0l11-11c0.39-0.39 0.39-1.024 0-1.414-0.39-0.39-1.024-0.39-1.414 0L8.5 16.586z" android:fillColor="@color/fluent_default_icon_tint"/>
</vector>

View File

@ -0,0 +1,3 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android" android:width="16dp" android:height="16dp" android:viewportWidth="16" android:viewportHeight="16">
<path android:pathData="M4.146 6.354c0.196 0.195 0.512 0.195 0.708 0L8 3.207l3.146 3.147c0.196 0.195 0.512 0.195 0.708 0 0.195-0.196 0.195-0.512 0-0.708l-3.5-3.5c-0.196-0.195-0.512-0.195-0.707 0l-3.5 3.5c-0.196 0.196-0.196 0.512 0 0.708zm0 3.292c0.196-0.195 0.512-0.195 0.708 0L8 12.793l3.146-3.146c0.196-0.196 0.512-0.196 0.708 0 0.195 0.195 0.195 0.511 0 0.707l-3.5 3.5c-0.196 0.195-0.512 0.195-0.707 0l-3.5-3.5c-0.196-0.196-0.196-0.512 0-0.707z" android:fillColor="@color/fluent_default_icon_tint"/>
</vector>

View File

@ -0,0 +1,3 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android" android:width="24dp" android:height="24dp" android:viewportWidth="24" android:viewportHeight="24">
<path android:pathData="M12 7.75c-0.966 0-1.75-0.784-1.75-1.75S11.034 4.25 12 4.25 13.75 5.034 13.75 6 12.966 7.75 12 7.75zm0 6c-0.966 0-1.75-0.784-1.75-1.75s0.784-1.75 1.75-1.75 1.75 0.784 1.75 1.75-0.784 1.75-1.75 1.75zM10.25 18c0 0.966 0.784 1.75 1.75 1.75s1.75-0.784 1.75-1.75-0.784-1.75-1.75-1.75-1.75 0.784-1.75 1.75z" android:fillColor="@color/fluent_default_icon_tint"/>
</vector>

View File

@ -0,0 +1,3 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android" android:width="25dp" android:height="24dp" android:viewportWidth="25" android:viewportHeight="24">
<path android:pathData="M11.416 17.5c0-1.288 0.375-2.49 1.022-3.5h-7.77c-1.242 0-2.249 1.007-2.249 2.25v0.919c0 0.572 0.179 1.13 0.51 1.596C4.473 20.929 6.996 22 10.417 22c0.931 0 1.796-0.08 2.592-0.238-0.992-1.142-1.592-2.632-1.592-4.263zm-1-15.495c2.761 0 5 2.239 5 5s-2.239 5-5 5c-2.762 0-5-2.239-5-5s2.238-5 5-5zm13 15.495c0 3.038-2.463 5.5-5.5 5.5-3.038 0-5.5-2.462-5.5-5.5 0-3.037 2.462-5.5 5.5-5.5 3.037 0 5.5 2.463 5.5 5.5zm-4.647-2.853c-0.195-0.196-0.511-0.196-0.707 0-0.195 0.195-0.195 0.512 0 0.707L19.71 17h-4.293c-0.276 0-0.5 0.224-0.5 0.5s0.224 0.5 0.5 0.5h4.293l-1.647 1.647c-0.195 0.195-0.195 0.512 0 0.707 0.196 0.195 0.512 0.195 0.707 0l2.5-2.5c0.054-0.053 0.092-0.116 0.117-0.182 0.018-0.05 0.029-0.105 0.03-0.163V17.5c0-0.077-0.018-0.15-0.049-0.215-0.015-0.032-0.034-0.063-0.057-0.092-0.013-0.018-0.028-0.035-0.045-0.05l-2.496-2.496z" android:fillColor="@color/fluent_default_icon_tint"/>
</vector>

View File

@ -0,0 +1,44 @@
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:orientation="horizontal"
android:layout_width="match_parent"
android:layout_height="48dp"
android:paddingLeft="20dp"
android:paddingRight="20dp"
android:gravity="center_vertical">
<ImageView
android:id="@+id/avatar"
android:layout_width="32dp"
android:layout_height="32dp"
android:importantForAccessibility="no"/>
<TextView
android:id="@+id/name"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_weight="1"
android:layout_marginStart="24dp"
android:textSize="16dp"
android:textColor="?android:textColorPrimary"
android:singleLine="true"
android:ellipsize="end"/>
<View
android:id="@+id/current"
android:layout_width="24dp"
android:layout_height="24dp"
android:background="@drawable/ic_fluent_checkmark_24_filled"
android:backgroundTint="?android:textColorSecondary"
android:contentDescription="@string/current_account"/>
<ImageButton
android:id="@+id/more"
android:layout_width="24dp"
android:layout_height="24dp"
android:src="@drawable/ic_fluent_more_vertical_24_regular"
android:tint="?android:textColorSecondary"
android:contentDescription="@string/more_options"
android:background="?android:selectableItemBackgroundBorderless"/>
</LinearLayout>

View File

@ -12,11 +12,11 @@
android:id="@+id/tabbar"
android:layout_width="match_parent"
android:layout_height="52dp"
android:paddingLeft="20dp"
android:paddingRight="20dp">
android:paddingLeft="16dp"
android:paddingRight="16dp">
<ImageView
android:id="@+id/tab_home"
android:layout_width="52dp"
android:layout_width="60dp"
android:layout_height="52dp"
android:scaleType="center"
android:contentDescription="@string/home_timeline"
@ -31,7 +31,7 @@
<ImageView
android:id="@+id/tab_search"
android:layout_width="52dp"
android:layout_width="60dp"
android:layout_height="52dp"
android:scaleType="center"
android:contentDescription="@string/search_hint"
@ -46,7 +46,7 @@
<ImageView
android:id="@+id/tab_notifications"
android:layout_width="52dp"
android:layout_width="60dp"
android:layout_height="52dp"
android:scaleType="center"
android:contentDescription="@string/notifications"
@ -61,7 +61,7 @@
<FrameLayout
android:id="@+id/tab_profile"
android:layout_width="52dp"
android:layout_width="60dp"
android:layout_height="52dp"
android:contentDescription="@string/my_profile"
android:foreground="@drawable/bg_tab_profile"
@ -73,6 +73,13 @@
android:layout_gravity="center"
android:scaleType="centerCrop"
android:src="@null"/>
<View
android:layout_width="16dp"
android:layout_height="16dp"
android:layout_gravity="end|center_vertical"
android:layout_marginEnd="-4dp"
android:backgroundTint="?android:colorPrimary"
android:background="@drawable/ic_fluent_chevron_up_down_16_regular"/>
</FrameLayout>
</org.joinmastodon.android.ui.views.TabBar>

View File

@ -0,0 +1,4 @@
<?xml version="1.0" encoding="utf-8"?>
<menu xmlns:android="http://schemas.android.com/apk/res/android">
<item android:id="@+id/log_out" android:title="@string/log_out_account" android:icon="@drawable/ic_fluent_person_arrow_right_24_filled"/>
</menu>

View File

@ -318,4 +318,6 @@
<string name="button_follow_pending">Pending</string>
<string name="follows_you">Follows you</string>
<string name="manually_approves_followers">Manually approves followers</string>
<string name="current_account">Current account</string>
<string name="log_out_account">Log Out %s</string>
</resources>