diff --git a/mastodon/src/main/java/org/joinmastodon/android/api/session/AccountSession.java b/mastodon/src/main/java/org/joinmastodon/android/api/session/AccountSession.java index c1930eabc..321b70729 100644 --- a/mastodon/src/main/java/org/joinmastodon/android/api/session/AccountSession.java +++ b/mastodon/src/main/java/org/joinmastodon/android/api/session/AccountSession.java @@ -73,4 +73,8 @@ public class AccountSession{ pushSubscriptionManager=new PushSubscriptionManager(getID()); return pushSubscriptionManager; } + + public String getFullUsername(){ + return '@'+self.username+'@'+domain; + } } 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 302ef7075..7a2f87563 100644 --- a/mastodon/src/main/java/org/joinmastodon/android/fragments/HomeFragment.java +++ b/mastodon/src/main/java/org/joinmastodon/android/fragments/HomeFragment.java @@ -197,6 +197,13 @@ public class HomeFragment extends AppKitFragment implements OnBackPressedListene throw new IllegalArgumentException(); } + public void setCurrentTab(@IdRes int tab){ + if(tab==currentTab) + return; + tabBar.selectTab(tab); + onTabSelected(tab); + } + private void onTabSelected(@IdRes int tab){ Fragment newFragment=fragmentForTab(tab); if(tab==currentTab){ @@ -230,7 +237,7 @@ public class HomeFragment extends AppKitFragment implements OnBackPressedListene for(AccountSession session:AccountSessionManager.getInstance().getLoggedInAccounts()){ options.add(session.self.displayName+"\n("+session.self.username+"@"+session.domain+")"); } - new AccountSwitcherSheet(getActivity()).show(); + new AccountSwitcherSheet(getActivity(), this).show(); return true; } if(tab==R.id.tab_home){ diff --git a/mastodon/src/main/java/org/joinmastodon/android/fragments/onboarding/AccountActivationFragment.java b/mastodon/src/main/java/org/joinmastodon/android/fragments/onboarding/AccountActivationFragment.java index 19bb17590..b1a77e9c0 100644 --- a/mastodon/src/main/java/org/joinmastodon/android/fragments/onboarding/AccountActivationFragment.java +++ b/mastodon/src/main/java/org/joinmastodon/android/fragments/onboarding/AccountActivationFragment.java @@ -103,7 +103,7 @@ public class AccountActivationFragment extends ToolbarFragment{ @Override public void onToolbarNavigationClick(){ - new AccountSwitcherSheet(getActivity()).show(); + new AccountSwitcherSheet(getActivity(), null).show(); } @Override diff --git a/mastodon/src/main/java/org/joinmastodon/android/ui/AccountSwitcherSheet.java b/mastodon/src/main/java/org/joinmastodon/android/ui/AccountSwitcherSheet.java index 7c85a3dc5..fbd19d8ad 100644 --- a/mastodon/src/main/java/org/joinmastodon/android/ui/AccountSwitcherSheet.java +++ b/mastodon/src/main/java/org/joinmastodon/android/ui/AccountSwitcherSheet.java @@ -2,21 +2,18 @@ package org.joinmastodon.android.ui; import android.annotation.SuppressLint; import android.app.Activity; -import android.content.Context; +import android.app.ProgressDialog; 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.RadioButton; import android.widget.TextView; import org.joinmastodon.android.GlobalUserPreferences; @@ -25,14 +22,18 @@ 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.HomeFragment; import org.joinmastodon.android.fragments.SplashFragment; -import org.joinmastodon.android.model.Account; import org.joinmastodon.android.ui.utils.UiUtils; +import java.util.ArrayList; import java.util.List; import java.util.stream.Collectors; +import androidx.annotation.DrawableRes; import androidx.annotation.NonNull; +import androidx.annotation.Nullable; +import androidx.annotation.StringRes; import androidx.recyclerview.widget.LinearLayoutManager; import me.grishka.appkit.Nav; import me.grishka.appkit.api.Callback; @@ -52,13 +53,15 @@ import me.grishka.appkit.views.UsableRecyclerView; public class AccountSwitcherSheet extends BottomSheet{ private final Activity activity; + private final HomeFragment fragment; private UsableRecyclerView list; private List accounts; private ListImageLoaderWrapper imgLoader; - public AccountSwitcherSheet(@NonNull Activity activity){ + public AccountSwitcherSheet(@NonNull Activity activity, @Nullable HomeFragment fragment){ super(activity); this.activity=activity; + this.fragment=fragment; accounts=AccountSessionManager.getInstance().getLoggedInAccounts().stream().map(WrappedAccount::new).collect(Collectors.toList()); @@ -70,41 +73,42 @@ public class AccountSwitcherSheet extends BottomSheet{ MergeRecyclerAdapter adapter=new MergeRecyclerAdapter(); View handle=new View(activity); handle.setBackgroundResource(R.drawable.bg_bottom_sheet_handle); + handle.setLayoutParams(new ViewGroup.LayoutParams(ViewGroup.LayoutParams.MATCH_PARENT, V.dp(36))); 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, ()->{ + adapter.addAdapter(new ClickableSingleViewRecyclerAdapter(makeSimpleListItem(R.string.add_account, R.drawable.ic_add_24px), ()->{ Nav.go(activity, SplashFragment.class, null); dismiss(); })); + adapter.addAdapter(new ClickableSingleViewRecyclerAdapter(makeSimpleListItem(R.string.log_out_all_accounts, R.drawable.ic_logout_24px), this::confirmLogOutAll)); 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()); + setNavigationBarBackground(new ColorDrawable(UiUtils.alphaBlendColors(UiUtils.getThemeColor(activity, R.attr.colorM3Surface), + UiUtils.getThemeColor(activity, R.attr.colorM3Primary), 0.05f)), !UiUtils.isDarkTheme()); } private void confirmLogOut(String accountID){ + AccountSession session=AccountSessionManager.getInstance().getAccount(accountID); new M3AlertDialogBuilder(activity) - .setTitle(R.string.log_out) - .setMessage(R.string.confirm_log_out) + .setMessage(activity.getString(R.string.confirm_log_out, session.getFullUsername())) .setPositiveButton(R.string.log_out, (dialog, which) -> logOut(accountID)) .setNegativeButton(R.string.cancel, null) .show(); } + private void confirmLogOutAll(){ + new M3AlertDialogBuilder(activity) + .setMessage(R.string.confirm_log_out_all_accounts) + .setPositiveButton(R.string.log_out, (dialog, which) -> logOutAll()) + .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) @@ -123,6 +127,41 @@ public class AccountSwitcherSheet extends BottomSheet{ .exec(accountID); } + private void logOutAll(){ + final ProgressDialog progress=new ProgressDialog(activity); + progress.setMessage(activity.getString(R.string.loading)); + progress.setCancelable(false); + progress.show(); + ArrayList sessions=new ArrayList<>(AccountSessionManager.getInstance().getLoggedInAccounts()); + for(AccountSession session:sessions){ + new RevokeOauthToken(session.app.clientId, session.app.clientSecret, session.token.accessToken) + .setCallback(new Callback<>(){ + @Override + public void onSuccess(Object result){ + AccountSessionManager.getInstance().removeAccount(session.getID()); + sessions.remove(session); + if(sessions.isEmpty()){ + progress.dismiss(); + Nav.goClearingStack(activity, SplashFragment.class, null); + dismiss(); + } + } + + @Override + public void onError(ErrorResponse error){ + AccountSessionManager.getInstance().removeAccount(session.getID()); + sessions.remove(session); + if(sessions.isEmpty()){ + progress.dismiss(); + Nav.goClearingStack(activity, SplashFragment.class, null); + dismiss(); + } + } + }) + .exec(session.getID()); + } + } + private void onLoggedOut(String accountID){ AccountSessionManager.getInstance().removeAccount(accountID); dismiss(); @@ -143,6 +182,13 @@ public class AccountSwitcherSheet extends BottomSheet{ } } + private View makeSimpleListItem(@StringRes int title, @DrawableRes int icon){ + TextView tv=(TextView) activity.getLayoutInflater().inflate(R.layout.item_text_with_icon, list, false); + tv.setText(title); + tv.setCompoundDrawablesRelativeWithIntrinsicBounds(icon, 0, 0, 0); + return tv; + } + private class AccountsAdapter extends UsableRecyclerView.Adapter implements ImageLoaderRecyclerAdapter{ public AccountsAdapter(){ super(imgLoader); @@ -176,45 +222,27 @@ public class AccountSwitcherSheet extends BottomSheet{ } } - private class AccountViewHolder extends BindableViewHolder implements ImageLoaderViewHolder, UsableRecyclerView.Clickable{ - private final TextView name; + private class AccountViewHolder extends BindableViewHolder implements ImageLoaderViewHolder, UsableRecyclerView.Clickable, UsableRecyclerView.LongClickable{ + private final TextView name, username; private final ImageView avatar; - private final ImageButton more; - private final View currentIcon; - private final PopupMenu menu; + private final RadioButton radioButton; public AccountViewHolder(){ super(activity, R.layout.item_account_switcher, list); name=findViewById(R.id.name); + username=findViewById(R.id.username); + radioButton=findViewById(R.id.radiobtn); avatar=findViewById(R.id.avatar); - more=findViewById(R.id.more); - currentIcon=findViewById(R.id.current); - - avatar.setOutlineProvider(OutlineProviders.roundedRect(12)); + avatar.setOutlineProvider(OutlineProviders.roundedRect(OutlineProviders.RADIUS_MEDIUM)); 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); + name.setText(item.self.displayName); + username.setText(item.getFullUsername()); + radioButton.setChecked(AccountSessionManager.getInstance().getLastActiveAccountID().equals(item.getID())); } @Override @@ -231,10 +259,23 @@ public class AccountSwitcherSheet extends BottomSheet{ @Override public void onClick(){ + if(AccountSessionManager.getInstance().getLastActiveAccountID().equals(item.getID())){ + dismiss(); + if(fragment!=null){ + fragment.setCurrentTab(R.id.tab_profile); + } + return; + } AccountSessionManager.getInstance().setLastActiveAccountID(item.getID()); activity.finish(); activity.startActivity(new Intent(activity, MainActivity.class)); } + + @Override + public boolean onLongClick(){ + confirmLogOut(item.getID()); + return true; + } } private static class WrappedAccount{ diff --git a/mastodon/src/main/java/org/joinmastodon/android/ui/OutlineProviders.java b/mastodon/src/main/java/org/joinmastodon/android/ui/OutlineProviders.java index 67972412e..81f05df36 100644 --- a/mastodon/src/main/java/org/joinmastodon/android/ui/OutlineProviders.java +++ b/mastodon/src/main/java/org/joinmastodon/android/ui/OutlineProviders.java @@ -8,7 +8,15 @@ import android.view.ViewOutlineProvider; import me.grishka.appkit.utils.V; public class OutlineProviders{ - private static SparseArray roundedRects=new SparseArray<>(); + private static final SparseArray roundedRects=new SparseArray<>(); + private static final SparseArray topRoundedRects=new SparseArray<>(); + private static final SparseArray endRoundedRects=new SparseArray<>(); + + public static final int RADIUS_XSMALL=4; + public static final int RADIUS_SMALL=8; + public static final int RADIUS_MEDIUM=12; + public static final int RADIUS_LARGE=16; + public static final int RADIUS_XLARGE=28; private OutlineProviders(){ //no instance @@ -37,6 +45,24 @@ public class OutlineProviders{ return provider; } + public static ViewOutlineProvider topRoundedRect(int dp){ + ViewOutlineProvider provider=topRoundedRects.get(dp); + if(provider!=null) + return provider; + provider=new TopRoundRectOutlineProvider(V.dp(dp)); + topRoundedRects.put(dp, provider); + return provider; + } + + public static ViewOutlineProvider endRoundedRect(int dp){ + ViewOutlineProvider provider=endRoundedRects.get(dp); + if(provider!=null) + return provider; + provider=new EndRoundRectOutlineProvider(V.dp(dp)); + endRoundedRects.put(dp, provider); + return provider; + } + private static class RoundRectOutlineProvider extends ViewOutlineProvider{ private final int radius; @@ -49,4 +75,34 @@ public class OutlineProviders{ outline.setRoundRect(0, 0, view.getWidth(), view.getHeight(), radius); } } + + private static class TopRoundRectOutlineProvider extends ViewOutlineProvider{ + private final int radius; + + private TopRoundRectOutlineProvider(int radius){ + this.radius=radius; + } + + @Override + public void getOutline(View view, Outline outline){ + outline.setRoundRect(0, 0, view.getWidth(), view.getHeight()+radius, radius); + } + } + + private static class EndRoundRectOutlineProvider extends ViewOutlineProvider{ + private final int radius; + + private EndRoundRectOutlineProvider(int radius){ + this.radius=radius; + } + + @Override + public void getOutline(View view, Outline outline){ + if(view.getLayoutDirection()==View.LAYOUT_DIRECTION_RTL){ + outline.setRoundRect(-radius, 0, view.getWidth(), view.getHeight(), radius); + }else{ + outline.setRoundRect(0, 0, view.getWidth()+radius, view.getHeight(), radius); + } + } + } } diff --git a/mastodon/src/main/res/drawable/ic_logout_24px.xml b/mastodon/src/main/res/drawable/ic_logout_24px.xml new file mode 100644 index 000000000..a02592125 --- /dev/null +++ b/mastodon/src/main/res/drawable/ic_logout_24px.xml @@ -0,0 +1,9 @@ + + + diff --git a/mastodon/src/main/res/drawable/ic_unfold_more_24px.xml b/mastodon/src/main/res/drawable/ic_unfold_more_24px.xml new file mode 100644 index 000000000..b32d759f3 --- /dev/null +++ b/mastodon/src/main/res/drawable/ic_unfold_more_24px.xml @@ -0,0 +1,9 @@ + + + diff --git a/mastodon/src/main/res/layout/item_account_switcher.xml b/mastodon/src/main/res/layout/item_account_switcher.xml index f171661ce..6276c783d 100644 --- a/mastodon/src/main/res/layout/item_account_switcher.xml +++ b/mastodon/src/main/res/layout/item_account_switcher.xml @@ -1,44 +1,60 @@ - + + + + + + - - - - - \ No newline at end of file + \ No newline at end of file diff --git a/mastodon/src/main/res/layout/item_text_with_icon.xml b/mastodon/src/main/res/layout/item_text_with_icon.xml new file mode 100644 index 000000000..01c27abab --- /dev/null +++ b/mastodon/src/main/res/layout/item_text_with_icon.xml @@ -0,0 +1,15 @@ + + \ 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 a17ee1d54..fa940847e 100644 --- a/mastodon/src/main/res/layout/tab_bar.xml +++ b/mastodon/src/main/res/layout/tab_bar.xml @@ -91,13 +91,15 @@ android:layout_gravity="center" android:scaleType="centerCrop" android:src="@null"/> - + android:layout_marginStart="24dp" + android:importantForAccessibility="no" + android:scaleType="center" + android:tint="?colorM3OnSurfaceVariant" + android:src="@drawable/ic_unfold_more_24px"/> diff --git a/mastodon/src/main/res/values/strings.xml b/mastodon/src/main/res/values/strings.xml index 712af65e1..ed500e5da 100644 --- a/mastodon/src/main/res/values/strings.xml +++ b/mastodon/src/main/res/values/strings.xml @@ -133,7 +133,7 @@ Audio playback Play Pause - Sign out + Log out Add account Search Hashtags @@ -267,7 +267,7 @@ Clear media cache Mastodon for Android v%1$s (%2$d) Media cache cleared - Are you sure you want to sign out? + Log out of %s? The author marked this media as sensitive. Tap to reveal Go to %s\'s profile @@ -453,4 +453,6 @@ Opening link… This link is not supported in the app + Log out of all accounts + Log out of all accounts? \ No newline at end of file