From 805912013684c4c45de5d140dbba7d8640f4b2fe Mon Sep 17 00:00:00 2001 From: Grishka Date: Sun, 1 May 2022 00:44:28 +0300 Subject: [PATCH] Better account switcher --- mastodon/build.gradle | 2 +- .../android/fragments/HomeFragment.java | 24 +- .../android/ui/AccountSwitcherSheet.java | 247 ++++++++++++++++++ .../ClickableSingleViewRecyclerAdapter.java | 34 +++ .../android/ui/DividerItemDecoration.java | 7 +- .../src/main/res/drawable/bg_bottom_sheet.xml | 9 + .../res/drawable/bg_bottom_sheet_handle.xml | 12 + .../ic_fluent_add_circle_24_filled.xml | 3 + .../ic_fluent_checkmark_24_filled.xml | 3 + .../ic_fluent_chevron_up_down_16_regular.xml | 3 + .../ic_fluent_more_vertical_24_regular.xml | 3 + ...ic_fluent_person_arrow_right_24_filled.xml | 3 + .../main/res/layout/item_account_switcher.xml | 44 ++++ mastodon/src/main/res/layout/tab_bar.xml | 19 +- .../src/main/res/menu/account_switcher.xml | 4 + mastodon/src/main/res/values/strings.xml | 2 + 16 files changed, 396 insertions(+), 23 deletions(-) create mode 100644 mastodon/src/main/java/org/joinmastodon/android/ui/AccountSwitcherSheet.java create mode 100644 mastodon/src/main/java/org/joinmastodon/android/ui/ClickableSingleViewRecyclerAdapter.java create mode 100644 mastodon/src/main/res/drawable/bg_bottom_sheet.xml create mode 100644 mastodon/src/main/res/drawable/bg_bottom_sheet_handle.xml create mode 100644 mastodon/src/main/res/drawable/ic_fluent_add_circle_24_filled.xml create mode 100644 mastodon/src/main/res/drawable/ic_fluent_checkmark_24_filled.xml create mode 100644 mastodon/src/main/res/drawable/ic_fluent_chevron_up_down_16_regular.xml create mode 100644 mastodon/src/main/res/drawable/ic_fluent_more_vertical_24_regular.xml create mode 100644 mastodon/src/main/res/drawable/ic_fluent_person_arrow_right_24_filled.xml create mode 100644 mastodon/src/main/res/layout/item_account_switcher.xml create mode 100644 mastodon/src/main/res/menu/account_switcher.xml diff --git a/mastodon/build.gradle b/mastodon/build.gradle index f01b96d7..449a6f6b 100644 --- a/mastodon/build.gradle +++ b/mastodon/build.gradle @@ -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' diff --git a/mastodon/src/main/java/org/joinmastodon/android/fragments/HomeFragment.java b/mastodon/src/main/java/org/joinmastodon/android/fragments/HomeFragment.java index 3b2685ee..f9a8cae4 100644 --- a/mastodon/src/main/java/org/joinmastodon/android/fragments/HomeFragment.java +++ b/mastodon/src/main/java/org/joinmastodon/android/fragments/HomeFragment.java @@ -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 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 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; diff --git a/mastodon/src/main/java/org/joinmastodon/android/ui/AccountSwitcherSheet.java b/mastodon/src/main/java/org/joinmastodon/android/ui/AccountSwitcherSheet.java new file mode 100644 index 00000000..1ed4125c --- /dev/null +++ b/mastodon/src/main/java/org/joinmastodon/android/ui/AccountSwitcherSheet.java @@ -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 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 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 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)); + } + } +} diff --git a/mastodon/src/main/java/org/joinmastodon/android/ui/ClickableSingleViewRecyclerAdapter.java b/mastodon/src/main/java/org/joinmastodon/android/ui/ClickableSingleViewRecyclerAdapter.java new file mode 100644 index 00000000..8d5a6dee --- /dev/null +++ b/mastodon/src/main/java/org/joinmastodon/android/ui/ClickableSingleViewRecyclerAdapter.java @@ -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(); + } + } +} diff --git a/mastodon/src/main/java/org/joinmastodon/android/ui/DividerItemDecoration.java b/mastodon/src/main/java/org/joinmastodon/android/ui/DividerItemDecoration.java index 7a8bf605..5be3be6c 100644 --- a/mastodon/src/main/java/org/joinmastodon/android/ui/DividerItemDecoration.java +++ b/mastodon/src/main/java/org/joinmastodon/android/ui/DividerItemDecoration.java @@ -18,6 +18,7 @@ public class DividerItemDecoration extends RecyclerView.ItemDecoration{ private Paint paint=new Paint(); private int paddingStart, paddingEnd; private Predicate drawDividerPredicate; + private boolean drawBelowLastItem; public static final Predicate 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 + + + + + + + + \ No newline at end of file diff --git a/mastodon/src/main/res/drawable/bg_bottom_sheet_handle.xml b/mastodon/src/main/res/drawable/bg_bottom_sheet_handle.xml new file mode 100644 index 00000000..199c9cab --- /dev/null +++ b/mastodon/src/main/res/drawable/bg_bottom_sheet_handle.xml @@ -0,0 +1,12 @@ + + + + + + + + + + + + \ No newline at end of file diff --git a/mastodon/src/main/res/drawable/ic_fluent_add_circle_24_filled.xml b/mastodon/src/main/res/drawable/ic_fluent_add_circle_24_filled.xml new file mode 100644 index 00000000..22e96aea --- /dev/null +++ b/mastodon/src/main/res/drawable/ic_fluent_add_circle_24_filled.xml @@ -0,0 +1,3 @@ + + + diff --git a/mastodon/src/main/res/drawable/ic_fluent_checkmark_24_filled.xml b/mastodon/src/main/res/drawable/ic_fluent_checkmark_24_filled.xml new file mode 100644 index 00000000..c241c16a --- /dev/null +++ b/mastodon/src/main/res/drawable/ic_fluent_checkmark_24_filled.xml @@ -0,0 +1,3 @@ + + + diff --git a/mastodon/src/main/res/drawable/ic_fluent_chevron_up_down_16_regular.xml b/mastodon/src/main/res/drawable/ic_fluent_chevron_up_down_16_regular.xml new file mode 100644 index 00000000..54302301 --- /dev/null +++ b/mastodon/src/main/res/drawable/ic_fluent_chevron_up_down_16_regular.xml @@ -0,0 +1,3 @@ + + + diff --git a/mastodon/src/main/res/drawable/ic_fluent_more_vertical_24_regular.xml b/mastodon/src/main/res/drawable/ic_fluent_more_vertical_24_regular.xml new file mode 100644 index 00000000..895913b1 --- /dev/null +++ b/mastodon/src/main/res/drawable/ic_fluent_more_vertical_24_regular.xml @@ -0,0 +1,3 @@ + + + diff --git a/mastodon/src/main/res/drawable/ic_fluent_person_arrow_right_24_filled.xml b/mastodon/src/main/res/drawable/ic_fluent_person_arrow_right_24_filled.xml new file mode 100644 index 00000000..479ba769 --- /dev/null +++ b/mastodon/src/main/res/drawable/ic_fluent_person_arrow_right_24_filled.xml @@ -0,0 +1,3 @@ + + + diff --git a/mastodon/src/main/res/layout/item_account_switcher.xml b/mastodon/src/main/res/layout/item_account_switcher.xml new file mode 100644 index 00000000..f171661c --- /dev/null +++ b/mastodon/src/main/res/layout/item_account_switcher.xml @@ -0,0 +1,44 @@ + + + + + + + + + + + + \ No newline at end of file diff --git a/mastodon/src/main/res/layout/tab_bar.xml b/mastodon/src/main/res/layout/tab_bar.xml index a5e9f797..2ad234cb 100644 --- a/mastodon/src/main/res/layout/tab_bar.xml +++ b/mastodon/src/main/res/layout/tab_bar.xml @@ -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"> + diff --git a/mastodon/src/main/res/menu/account_switcher.xml b/mastodon/src/main/res/menu/account_switcher.xml new file mode 100644 index 00000000..a3934322 --- /dev/null +++ b/mastodon/src/main/res/menu/account_switcher.xml @@ -0,0 +1,4 @@ + + + + \ No newline at end of file diff --git a/mastodon/src/main/res/values/strings.xml b/mastodon/src/main/res/values/strings.xml index 29ab288c..616a15c7 100644 --- a/mastodon/src/main/res/values/strings.xml +++ b/mastodon/src/main/res/values/strings.xml @@ -318,4 +318,6 @@ Pending Follows you Manually approves followers + Current account + Log Out %s \ No newline at end of file