Settings M3 redesign wip
This commit is contained in:
@@ -554,6 +554,14 @@ public abstract class BaseStatusListFragment<T extends DisplayItemsParent> exten
|
||||
return attachmentViewsPool;
|
||||
}
|
||||
|
||||
public void rebuildAllDisplayItems(){
|
||||
displayItems.clear();
|
||||
for(T item:data){
|
||||
displayItems.addAll(buildDisplayItems(item));
|
||||
}
|
||||
adapter.notifyDataSetChanged();
|
||||
}
|
||||
|
||||
protected void onModifyItemViewHolder(BindableViewHolder<StatusDisplayItem> holder){}
|
||||
|
||||
protected class DisplayItemsAdapter extends UsableRecyclerView.Adapter<BindableViewHolder<StatusDisplayItem>> implements ImageLoaderRecyclerAdapter{
|
||||
|
||||
@@ -46,6 +46,7 @@ import android.widget.TextView;
|
||||
import com.twitter.twittertext.TwitterTextEmojiRegex;
|
||||
|
||||
import org.joinmastodon.android.E;
|
||||
import org.joinmastodon.android.GlobalUserPreferences;
|
||||
import org.joinmastodon.android.R;
|
||||
import org.joinmastodon.android.api.MastodonErrorResponse;
|
||||
import org.joinmastodon.android.api.requests.accounts.GetPreferences;
|
||||
@@ -546,7 +547,10 @@ public class ComposeFragment extends MastodonToolbarFragment implements OnBackPr
|
||||
@Override
|
||||
public boolean onOptionsItemSelected(MenuItem item){
|
||||
if(item.getItemId()==R.id.publish){
|
||||
publish();
|
||||
if(GlobalUserPreferences.altTextReminders)
|
||||
checkAltTextsAndPublish();
|
||||
else
|
||||
publish();
|
||||
}
|
||||
return true;
|
||||
}
|
||||
@@ -641,6 +645,28 @@ public class ComposeFragment extends MastodonToolbarFragment implements OnBackPr
|
||||
return true;
|
||||
}
|
||||
|
||||
private void checkAltTextsAndPublish(){
|
||||
int count=mediaViewController.getMissingAltTextAttachmentCount();
|
||||
if(count==0){
|
||||
publish();
|
||||
}else{
|
||||
String msg=getResources().getQuantityString(mediaViewController.areAllAttachmentsImages() ? R.plurals.alt_text_reminder_x_images : R.plurals.alt_text_reminder_x_attachments,
|
||||
count, switch(count){
|
||||
case 1 -> getString(R.string.count_one);
|
||||
case 2 -> getString(R.string.count_two);
|
||||
case 3 -> getString(R.string.count_three);
|
||||
case 4 -> getString(R.string.count_four);
|
||||
default -> String.valueOf(count);
|
||||
});
|
||||
new M3AlertDialogBuilder(getActivity())
|
||||
.setTitle(R.string.alt_text_reminder_title)
|
||||
.setMessage(msg)
|
||||
.setPositiveButton(R.string.alt_text_reminder_post_anyway, (dlg, item)->publish())
|
||||
.setNegativeButton(R.string.cancel, null)
|
||||
.show();
|
||||
}
|
||||
}
|
||||
|
||||
private void publish(){
|
||||
sendingOverlay=new View(getActivity());
|
||||
WindowManager.LayoutParams overlayParams=new WindowManager.LayoutParams();
|
||||
@@ -655,7 +681,6 @@ public class ComposeFragment extends MastodonToolbarFragment implements OnBackPr
|
||||
publishButton.setEnabled(false);
|
||||
V.setVisibilityAnimated(sendProgress, View.VISIBLE);
|
||||
|
||||
|
||||
mediaViewController.saveAltTextsBeforePublishing(this::actuallyPublish, this::handlePublishError);
|
||||
}
|
||||
|
||||
|
||||
@@ -3,14 +3,11 @@ package org.joinmastodon.android.fragments;
|
||||
import android.annotation.SuppressLint;
|
||||
import android.app.Fragment;
|
||||
import android.app.NotificationManager;
|
||||
import android.graphics.Outline;
|
||||
import android.os.Build;
|
||||
import android.os.Bundle;
|
||||
import android.text.TextUtils;
|
||||
import android.view.LayoutInflater;
|
||||
import android.view.View;
|
||||
import android.view.ViewGroup;
|
||||
import android.view.ViewOutlineProvider;
|
||||
import android.view.ViewTreeObserver;
|
||||
import android.view.WindowInsets;
|
||||
import android.widget.FrameLayout;
|
||||
@@ -20,19 +17,19 @@ import android.widget.TextView;
|
||||
|
||||
import com.squareup.otto.Subscribe;
|
||||
|
||||
import org.joinmastodon.android.BuildConfig;
|
||||
import org.joinmastodon.android.E;
|
||||
import org.joinmastodon.android.PushNotificationReceiver;
|
||||
import org.joinmastodon.android.R;
|
||||
import org.joinmastodon.android.api.requests.markers.GetMarkers;
|
||||
import org.joinmastodon.android.api.session.AccountSession;
|
||||
import org.joinmastodon.android.api.session.AccountSessionManager;
|
||||
import org.joinmastodon.android.events.NotificationsMarkerUpdatedEvent;
|
||||
import org.joinmastodon.android.events.StatusDisplaySettingsChangedEvent;
|
||||
import org.joinmastodon.android.fragments.discover.DiscoverFragment;
|
||||
import org.joinmastodon.android.fragments.onboarding.OnboardingFollowSuggestionsFragment;
|
||||
import org.joinmastodon.android.model.Account;
|
||||
import org.joinmastodon.android.model.Notification;
|
||||
import org.joinmastodon.android.model.PaginatedResponse;
|
||||
import org.joinmastodon.android.model.TimelineMarkers;
|
||||
import org.joinmastodon.android.ui.AccountSwitcherSheet;
|
||||
import org.joinmastodon.android.ui.OutlineProviders;
|
||||
import org.joinmastodon.android.ui.utils.UiUtils;
|
||||
@@ -265,7 +262,7 @@ public class HomeFragment extends AppKitFragment implements OnBackPressedListene
|
||||
new AccountSwitcherSheet(getActivity(), this).show();
|
||||
return true;
|
||||
}
|
||||
if(tab==R.id.tab_home){
|
||||
if(tab==R.id.tab_home && BuildConfig.DEBUG){
|
||||
Bundle args=new Bundle();
|
||||
args.putString("account", accountID);
|
||||
Nav.go(getActivity(), OnboardingFollowSuggestionsFragment.class, args);
|
||||
@@ -328,7 +325,7 @@ public class HomeFragment extends AppKitFragment implements OnBackPressedListene
|
||||
notificationsBadge.setVisibility(View.GONE);
|
||||
}else{
|
||||
notificationsBadge.setVisibility(View.VISIBLE);
|
||||
if(notifications.get(notifications.size()-1).id.compareTo(marker)<=0){
|
||||
if(notifications.get(notifications.size()-1).id.compareTo(marker)>0){
|
||||
notificationsBadge.setText(String.format("%d+", notifications.size()));
|
||||
}else{
|
||||
int count=0;
|
||||
@@ -349,4 +346,14 @@ public class HomeFragment extends AppKitFragment implements OnBackPressedListene
|
||||
if(ev.clearUnread)
|
||||
notificationsBadge.setVisibility(View.GONE);
|
||||
}
|
||||
|
||||
@Subscribe
|
||||
public void onStatusDisplaySettingsChanged(StatusDisplaySettingsChangedEvent ev){
|
||||
if(!ev.accountID.equals(accountID))
|
||||
return;
|
||||
if(homeTimelineFragment.loaded)
|
||||
homeTimelineFragment.rebuildAllDisplayItems();
|
||||
if(notificationsFragment.loaded)
|
||||
notificationsFragment.rebuildAllDisplayItems();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -30,8 +30,10 @@ import org.joinmastodon.android.api.requests.timelines.GetHomeTimeline;
|
||||
import org.joinmastodon.android.api.session.AccountSessionManager;
|
||||
import org.joinmastodon.android.events.SelfUpdateStateChangedEvent;
|
||||
import org.joinmastodon.android.events.StatusCreatedEvent;
|
||||
import org.joinmastodon.android.fragments.settings.SettingsMainFragment;
|
||||
import org.joinmastodon.android.model.CacheablePaginatedResponse;
|
||||
import org.joinmastodon.android.model.Filter;
|
||||
import org.joinmastodon.android.model.FilterContext;
|
||||
import org.joinmastodon.android.model.LegacyFilter;
|
||||
import org.joinmastodon.android.model.Status;
|
||||
import org.joinmastodon.android.model.TimelineMarkers;
|
||||
import org.joinmastodon.android.ui.displayitems.GapStatusDisplayItem;
|
||||
@@ -123,7 +125,7 @@ public class HomeTimelineFragment extends StatusListFragment{
|
||||
public boolean onOptionsItemSelected(MenuItem item){
|
||||
Bundle args=new Bundle();
|
||||
args.putString("account", accountID);
|
||||
Nav.go(getActivity(), SettingsFragment.class, args);
|
||||
Nav.go(getActivity(), SettingsMainFragment.class, args);
|
||||
return true;
|
||||
}
|
||||
|
||||
@@ -200,7 +202,7 @@ public class HomeTimelineFragment extends StatusListFragment{
|
||||
result.get(result.size()-1).hasGapAfter=true;
|
||||
toAdd=result;
|
||||
}
|
||||
List<Filter> filters=AccountSessionManager.getInstance().getAccount(accountID).wordFilters.stream().filter(f->f.context.contains(Filter.FilterContext.HOME)).collect(Collectors.toList());
|
||||
List<LegacyFilter> filters=AccountSessionManager.getInstance().getAccount(accountID).wordFilters.stream().filter(f->f.context.contains(FilterContext.HOME)).collect(Collectors.toList());
|
||||
if(!filters.isEmpty()){
|
||||
toAdd=toAdd.stream().filter(new StatusFilterPredicate(filters)).collect(Collectors.toList());
|
||||
}
|
||||
@@ -277,12 +279,12 @@ public class HomeTimelineFragment extends StatusListFragment{
|
||||
List<StatusDisplayItem> targetList=displayItems.subList(gapPos, gapPos+1);
|
||||
targetList.clear();
|
||||
List<Status> insertedPosts=data.subList(gapPostIndex+1, gapPostIndex+1);
|
||||
List<Filter> filters=AccountSessionManager.getInstance().getAccount(accountID).wordFilters.stream().filter(f->f.context.contains(Filter.FilterContext.HOME)).collect(Collectors.toList());
|
||||
List<LegacyFilter> filters=AccountSessionManager.getInstance().getAccount(accountID).wordFilters.stream().filter(f->f.context.contains(FilterContext.HOME)).collect(Collectors.toList());
|
||||
outer:
|
||||
for(Status s:result){
|
||||
if(idsBelowGap.contains(s.id))
|
||||
break;
|
||||
for(Filter filter:filters){
|
||||
for(LegacyFilter filter:filters){
|
||||
if(filter.matches(s)){
|
||||
continue outer;
|
||||
}
|
||||
@@ -444,6 +446,11 @@ public class HomeTimelineFragment extends StatusListFragment{
|
||||
getToolbar().getMenu().findItem(R.id.settings).setIcon(R.drawable.ic_settings_24_badged);
|
||||
}
|
||||
|
||||
@Override
|
||||
protected boolean wantsToolbarMenuIconsTinted(){
|
||||
return false;
|
||||
}
|
||||
|
||||
@Subscribe
|
||||
public void onSelfUpdateStateChanged(SelfUpdateStateChangedEvent ev){
|
||||
updateUpdateState(ev.state);
|
||||
|
||||
@@ -315,10 +315,12 @@ public class NotificationsListFragment extends BaseStatusListFragment<Notificati
|
||||
|
||||
private void markAsRead(){
|
||||
String id=data.get(0).id;
|
||||
new SaveMarkers(null, id).exec(accountID);
|
||||
AccountSessionManager.get(accountID).setNotificationsMarker(id, true);
|
||||
realUnreadMarker=id;
|
||||
updateMarkAllReadButton();
|
||||
if(ObjectIdComparator.INSTANCE.compare(id, realUnreadMarker)>0){
|
||||
new SaveMarkers(null, id).exec(accountID);
|
||||
AccountSessionManager.get(accountID).setNotificationsMarker(id, true);
|
||||
realUnreadMarker=id;
|
||||
updateMarkAllReadButton();
|
||||
}
|
||||
}
|
||||
|
||||
private void resetUnreadBackground(){
|
||||
|
||||
@@ -458,7 +458,8 @@ public class ProfileFragment extends LoaderFragment implements OnBackPressedList
|
||||
ViewImageLoader.load(avatar, null, new UrlImageLoaderRequest(GlobalUserPreferences.playGifs ? account.avatar : account.avatarStatic, V.dp(100), V.dp(100)));
|
||||
ViewImageLoader.load(cover, null, new UrlImageLoaderRequest(GlobalUserPreferences.playGifs ? account.header : account.headerStatic, 1000, 1000));
|
||||
SpannableStringBuilder ssb=new SpannableStringBuilder(account.displayName);
|
||||
HtmlParser.parseCustomEmoji(ssb, account.emojis);
|
||||
if(AccountSessionManager.get(accountID).getLocalPreferences().customEmojiInNames)
|
||||
HtmlParser.parseCustomEmoji(ssb, account.emojis);
|
||||
name.setText(ssb);
|
||||
setTitle(ssb);
|
||||
|
||||
|
||||
@@ -1,761 +0,0 @@
|
||||
package org.joinmastodon.android.fragments;
|
||||
|
||||
import android.animation.ObjectAnimator;
|
||||
import android.annotation.SuppressLint;
|
||||
import android.app.Activity;
|
||||
import android.content.Intent;
|
||||
import android.graphics.Bitmap;
|
||||
import android.graphics.Canvas;
|
||||
import android.graphics.Rect;
|
||||
import android.os.Build;
|
||||
import android.os.Bundle;
|
||||
import android.view.Gravity;
|
||||
import android.view.LayoutInflater;
|
||||
import android.view.View;
|
||||
import android.view.ViewGroup;
|
||||
import android.view.WindowInsets;
|
||||
import android.view.WindowManager;
|
||||
import android.view.animation.LinearInterpolator;
|
||||
import android.widget.Button;
|
||||
import android.widget.ImageButton;
|
||||
import android.widget.ImageView;
|
||||
import android.widget.PopupMenu;
|
||||
import android.widget.ProgressBar;
|
||||
import android.widget.RadioButton;
|
||||
import android.widget.Switch;
|
||||
import android.widget.TextView;
|
||||
import android.widget.Toast;
|
||||
|
||||
import com.squareup.otto.Subscribe;
|
||||
|
||||
import org.joinmastodon.android.BuildConfig;
|
||||
import org.joinmastodon.android.E;
|
||||
import org.joinmastodon.android.GlobalUserPreferences;
|
||||
import org.joinmastodon.android.MainActivity;
|
||||
import org.joinmastodon.android.MastodonApp;
|
||||
import org.joinmastodon.android.R;
|
||||
import org.joinmastodon.android.api.MastodonAPIController;
|
||||
import org.joinmastodon.android.api.PushSubscriptionManager;
|
||||
import org.joinmastodon.android.api.requests.oauth.RevokeOauthToken;
|
||||
import org.joinmastodon.android.api.session.AccountActivationInfo;
|
||||
import org.joinmastodon.android.api.session.AccountSession;
|
||||
import org.joinmastodon.android.api.session.AccountSessionManager;
|
||||
import org.joinmastodon.android.events.SelfUpdateStateChangedEvent;
|
||||
import org.joinmastodon.android.fragments.onboarding.AccountActivationFragment;
|
||||
import org.joinmastodon.android.model.PushNotification;
|
||||
import org.joinmastodon.android.model.PushSubscription;
|
||||
import org.joinmastodon.android.ui.M3AlertDialogBuilder;
|
||||
import org.joinmastodon.android.ui.OutlineProviders;
|
||||
import org.joinmastodon.android.ui.utils.UiUtils;
|
||||
import org.joinmastodon.android.updater.GithubSelfUpdater;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.function.Consumer;
|
||||
|
||||
import androidx.annotation.DrawableRes;
|
||||
import androidx.annotation.NonNull;
|
||||
import androidx.annotation.StringRes;
|
||||
import androidx.recyclerview.widget.LinearLayoutManager;
|
||||
import androidx.recyclerview.widget.RecyclerView;
|
||||
import me.grishka.appkit.Nav;
|
||||
import me.grishka.appkit.api.Callback;
|
||||
import me.grishka.appkit.api.ErrorResponse;
|
||||
import me.grishka.appkit.imageloader.ImageCache;
|
||||
import me.grishka.appkit.utils.BindableViewHolder;
|
||||
import me.grishka.appkit.utils.V;
|
||||
import me.grishka.appkit.views.UsableRecyclerView;
|
||||
|
||||
public class SettingsFragment extends MastodonToolbarFragment{
|
||||
private UsableRecyclerView list;
|
||||
private ArrayList<Item> items=new ArrayList<>();
|
||||
private ThemeItem themeItem;
|
||||
private NotificationPolicyItem notificationPolicyItem;
|
||||
private String accountID;
|
||||
private boolean needUpdateNotificationSettings;
|
||||
private PushSubscription pushSubscription;
|
||||
|
||||
private ImageView themeTransitionWindowView;
|
||||
|
||||
@Override
|
||||
public void onCreate(Bundle savedInstanceState){
|
||||
super.onCreate(savedInstanceState);
|
||||
if(Build.VERSION.SDK_INT>=Build.VERSION_CODES.N)
|
||||
setRetainInstance(true);
|
||||
setTitle(R.string.settings);
|
||||
accountID=getArguments().getString("account");
|
||||
AccountSession session=AccountSessionManager.getInstance().getAccount(accountID);
|
||||
|
||||
if(GithubSelfUpdater.needSelfUpdating()){
|
||||
GithubSelfUpdater updater=GithubSelfUpdater.getInstance();
|
||||
GithubSelfUpdater.UpdateState state=updater.getState();
|
||||
if(state!=GithubSelfUpdater.UpdateState.NO_UPDATE && state!=GithubSelfUpdater.UpdateState.CHECKING){
|
||||
items.add(new UpdateItem());
|
||||
}
|
||||
}
|
||||
|
||||
items.add(new HeaderItem(R.string.settings_theme));
|
||||
items.add(themeItem=new ThemeItem());
|
||||
items.add(new SwitchItem(R.string.theme_true_black, R.drawable.ic_fluent_dark_theme_24_regular, GlobalUserPreferences.trueBlackTheme, this::onTrueBlackThemeChanged));
|
||||
|
||||
items.add(new HeaderItem(R.string.settings_behavior));
|
||||
items.add(new SwitchItem(R.string.settings_gif, R.drawable.ic_fluent_gif_24_regular, GlobalUserPreferences.playGifs, i->{
|
||||
GlobalUserPreferences.playGifs=i.checked;
|
||||
GlobalUserPreferences.save();
|
||||
}));
|
||||
items.add(new SwitchItem(R.string.settings_custom_tabs, R.drawable.ic_fluent_link_24_regular, GlobalUserPreferences.useCustomTabs, i->{
|
||||
GlobalUserPreferences.useCustomTabs=i.checked;
|
||||
GlobalUserPreferences.save();
|
||||
}));
|
||||
|
||||
items.add(new HeaderItem(R.string.settings_notifications));
|
||||
items.add(notificationPolicyItem=new NotificationPolicyItem());
|
||||
PushSubscription pushSubscription=getPushSubscription();
|
||||
items.add(new SwitchItem(R.string.notify_favorites, R.drawable.ic_fluent_star_24_regular, pushSubscription.alerts.favourite, i->onNotificationsChanged(PushNotification.Type.FAVORITE, i.checked)));
|
||||
items.add(new SwitchItem(R.string.notify_follow, R.drawable.ic_fluent_person_add_24_regular, pushSubscription.alerts.follow, i->onNotificationsChanged(PushNotification.Type.FOLLOW, i.checked)));
|
||||
items.add(new SwitchItem(R.string.notify_reblog, R.drawable.ic_fluent_arrow_repeat_all_24_regular, pushSubscription.alerts.reblog, i->onNotificationsChanged(PushNotification.Type.REBLOG, i.checked)));
|
||||
items.add(new SwitchItem(R.string.notify_mention, R.drawable.ic_at_symbol, pushSubscription.alerts.mention, i->onNotificationsChanged(PushNotification.Type.MENTION, i.checked)));
|
||||
|
||||
items.add(new HeaderItem(R.string.settings_boring));
|
||||
items.add(new TextItem(R.string.settings_account, ()->UiUtils.launchWebBrowser(getActivity(), "https://"+session.domain+"/auth/edit")));
|
||||
items.add(new TextItem(R.string.settings_contribute, ()->UiUtils.launchWebBrowser(getActivity(), "https://github.com/mastodon/mastodon-android")));
|
||||
items.add(new TextItem(R.string.settings_tos, ()->UiUtils.launchWebBrowser(getActivity(), "https://"+session.domain+"/terms")));
|
||||
items.add(new TextItem(R.string.settings_privacy_policy, ()->UiUtils.launchWebBrowser(getActivity(), "https://"+session.domain+"/terms")));
|
||||
|
||||
items.add(new RedHeaderItem(R.string.settings_spicy));
|
||||
items.add(new TextItem(R.string.settings_clear_cache, this::clearImageCache));
|
||||
items.add(new TextItem(R.string.log_out, this::confirmLogOut));
|
||||
|
||||
if(BuildConfig.DEBUG){
|
||||
items.add(new RedHeaderItem("Debug options"));
|
||||
items.add(new TextItem("Test e-mail confirmation flow", ()->{
|
||||
AccountSession sess=AccountSessionManager.getInstance().getAccount(accountID);
|
||||
sess.activated=false;
|
||||
sess.activationInfo=new AccountActivationInfo("test@email", System.currentTimeMillis());
|
||||
Bundle args=new Bundle();
|
||||
args.putString("account", accountID);
|
||||
args.putBoolean("debug", true);
|
||||
Nav.goClearingStack(getActivity(), AccountActivationFragment.class, args);
|
||||
}));
|
||||
}
|
||||
|
||||
items.add(new FooterItem(getString(R.string.settings_app_version, BuildConfig.VERSION_NAME, BuildConfig.VERSION_CODE)));
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onAttach(Activity activity){
|
||||
super.onAttach(activity);
|
||||
if(themeTransitionWindowView!=null){
|
||||
// Activity has finished recreating. Remove the overlay.
|
||||
MastodonApp.context.getSystemService(WindowManager.class).removeView(themeTransitionWindowView);
|
||||
themeTransitionWindowView=null;
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public View onCreateContentView(LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState){
|
||||
list=new UsableRecyclerView(getActivity());
|
||||
list.setLayoutManager(new LinearLayoutManager(getActivity()));
|
||||
list.setAdapter(new SettingsAdapter());
|
||||
list.setBackgroundColor(UiUtils.getThemeColor(getActivity(), android.R.attr.colorBackground));
|
||||
list.setPadding(0, V.dp(16), 0, V.dp(12));
|
||||
list.setClipToPadding(false);
|
||||
list.addItemDecoration(new RecyclerView.ItemDecoration(){
|
||||
@Override
|
||||
public void getItemOffsets(@NonNull Rect outRect, @NonNull View view, @NonNull RecyclerView parent, @NonNull RecyclerView.State state){
|
||||
// Add 32dp gaps between sections
|
||||
RecyclerView.ViewHolder holder=parent.getChildViewHolder(view);
|
||||
if((holder instanceof HeaderViewHolder || holder instanceof FooterViewHolder) && holder.getAbsoluteAdapterPosition()>1)
|
||||
outRect.top=V.dp(32);
|
||||
}
|
||||
});
|
||||
return list;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onApplyWindowInsets(WindowInsets insets){
|
||||
if(Build.VERSION.SDK_INT>=29 && insets.getTappableElementInsets().bottom==0){
|
||||
list.setPadding(0, V.dp(16), 0, V.dp(12)+insets.getSystemWindowInsetBottom());
|
||||
insets=insets.inset(0, 0, 0, insets.getSystemWindowInsetBottom());
|
||||
}
|
||||
super.onApplyWindowInsets(insets);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onDestroy(){
|
||||
super.onDestroy();
|
||||
if(needUpdateNotificationSettings && PushSubscriptionManager.arePushNotificationsAvailable()){
|
||||
AccountSessionManager.getInstance().getAccount(accountID).getPushSubscriptionManager().updatePushSettings(pushSubscription);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onViewCreated(View view, Bundle savedInstanceState){
|
||||
super.onViewCreated(view, savedInstanceState);
|
||||
if(GithubSelfUpdater.needSelfUpdating())
|
||||
E.register(this);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onDestroyView(){
|
||||
super.onDestroyView();
|
||||
if(GithubSelfUpdater.needSelfUpdating())
|
||||
E.unregister(this);
|
||||
}
|
||||
|
||||
private void onThemePreferenceClick(GlobalUserPreferences.ThemePreference theme){
|
||||
GlobalUserPreferences.theme=theme;
|
||||
GlobalUserPreferences.save();
|
||||
restartActivityToApplyNewTheme();
|
||||
}
|
||||
|
||||
private void onTrueBlackThemeChanged(SwitchItem item){
|
||||
GlobalUserPreferences.trueBlackTheme=item.checked;
|
||||
GlobalUserPreferences.save();
|
||||
|
||||
RecyclerView.ViewHolder themeHolder=list.findViewHolderForAdapterPosition(items.indexOf(themeItem));
|
||||
if(themeHolder!=null){
|
||||
((ThemeViewHolder)themeHolder).bindSubitems();
|
||||
}else{
|
||||
list.getAdapter().notifyItemChanged(items.indexOf(themeItem));
|
||||
}
|
||||
|
||||
if(UiUtils.isDarkTheme()){
|
||||
restartActivityToApplyNewTheme();
|
||||
}
|
||||
}
|
||||
|
||||
private void restartActivityToApplyNewTheme(){
|
||||
// Calling activity.recreate() causes a black screen for like half a second.
|
||||
// So, let's take a screenshot and overlay it on top to create the illusion of a smoother transition.
|
||||
// As a bonus, we can fade it out to make it even smoother.
|
||||
if(Build.VERSION.SDK_INT>=Build.VERSION_CODES.N){
|
||||
View activityDecorView=getActivity().getWindow().getDecorView();
|
||||
Bitmap bitmap=Bitmap.createBitmap(activityDecorView.getWidth(), activityDecorView.getHeight(), Bitmap.Config.ARGB_8888);
|
||||
activityDecorView.draw(new Canvas(bitmap));
|
||||
themeTransitionWindowView=new ImageView(MastodonApp.context);
|
||||
themeTransitionWindowView.setImageBitmap(bitmap);
|
||||
WindowManager.LayoutParams lp=new WindowManager.LayoutParams(WindowManager.LayoutParams.TYPE_APPLICATION);
|
||||
lp.flags=WindowManager.LayoutParams.FLAG_NOT_FOCUSABLE | WindowManager.LayoutParams.FLAG_NOT_TOUCHABLE |
|
||||
WindowManager.LayoutParams.FLAG_LAYOUT_INSET_DECOR | WindowManager.LayoutParams.FLAG_LAYOUT_IN_SCREEN | WindowManager.LayoutParams.FLAG_DRAWS_SYSTEM_BAR_BACKGROUNDS;
|
||||
lp.systemUiVisibility=View.SYSTEM_UI_FLAG_LAYOUT_FULLSCREEN | View.SYSTEM_UI_FLAG_LAYOUT_HIDE_NAVIGATION | View.SYSTEM_UI_FLAG_LAYOUT_STABLE;
|
||||
lp.systemUiVisibility|=(activityDecorView.getWindowSystemUiVisibility() & (View.SYSTEM_UI_FLAG_LIGHT_NAVIGATION_BAR | View.SYSTEM_UI_FLAG_LIGHT_STATUS_BAR));
|
||||
lp.width=lp.height=WindowManager.LayoutParams.MATCH_PARENT;
|
||||
lp.token=getActivity().getWindow().getAttributes().token;
|
||||
lp.windowAnimations=R.style.window_fade_out;
|
||||
MastodonApp.context.getSystemService(WindowManager.class).addView(themeTransitionWindowView, lp);
|
||||
}
|
||||
getActivity().recreate();
|
||||
}
|
||||
|
||||
private PushSubscription getPushSubscription(){
|
||||
if(pushSubscription!=null)
|
||||
return pushSubscription;
|
||||
AccountSession session=AccountSessionManager.getInstance().getAccount(accountID);
|
||||
if(session.pushSubscription==null){
|
||||
pushSubscription=new PushSubscription();
|
||||
pushSubscription.alerts=PushSubscription.Alerts.ofAll();
|
||||
}else{
|
||||
pushSubscription=session.pushSubscription.clone();
|
||||
}
|
||||
return pushSubscription;
|
||||
}
|
||||
|
||||
private void onNotificationsChanged(PushNotification.Type type, boolean enabled){
|
||||
PushSubscription subscription=getPushSubscription();
|
||||
switch(type){
|
||||
case FAVORITE -> subscription.alerts.favourite=enabled;
|
||||
case FOLLOW -> subscription.alerts.follow=enabled;
|
||||
case REBLOG -> subscription.alerts.reblog=enabled;
|
||||
case MENTION -> subscription.alerts.mention=subscription.alerts.poll=enabled;
|
||||
}
|
||||
needUpdateNotificationSettings=true;
|
||||
}
|
||||
|
||||
private void onNotificationsPolicyChanged(PushSubscription.Policy policy){
|
||||
PushSubscription subscription=getPushSubscription();
|
||||
PushSubscription.Policy prevPolicy=subscription.policy;
|
||||
if(prevPolicy==policy)
|
||||
return;
|
||||
subscription.policy=policy;
|
||||
int index=items.indexOf(notificationPolicyItem);
|
||||
RecyclerView.ViewHolder policyHolder=list.findViewHolderForAdapterPosition(index);
|
||||
if(policyHolder!=null){
|
||||
((NotificationPolicyViewHolder)policyHolder).rebind();
|
||||
}else{
|
||||
list.getAdapter().notifyItemChanged(index);
|
||||
}
|
||||
if((prevPolicy==PushSubscription.Policy.NONE)!=(policy==PushSubscription.Policy.NONE)){
|
||||
index++;
|
||||
while(items.get(index) instanceof SwitchItem si){
|
||||
si.enabled=si.checked=policy!=PushSubscription.Policy.NONE;
|
||||
RecyclerView.ViewHolder holder=list.findViewHolderForAdapterPosition(index);
|
||||
if(holder!=null)
|
||||
((BindableViewHolder<?>)holder).rebind();
|
||||
else
|
||||
list.getAdapter().notifyItemChanged(index);
|
||||
index++;
|
||||
}
|
||||
}
|
||||
needUpdateNotificationSettings=true;
|
||||
}
|
||||
|
||||
private void confirmLogOut(){
|
||||
new M3AlertDialogBuilder(getActivity())
|
||||
.setTitle(R.string.log_out)
|
||||
.setMessage(R.string.confirm_log_out)
|
||||
.setPositiveButton(R.string.log_out, (dialog, which) -> logOut())
|
||||
.setNegativeButton(R.string.cancel, null)
|
||||
.show();
|
||||
}
|
||||
|
||||
private void logOut(){
|
||||
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();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onError(ErrorResponse error){
|
||||
onLoggedOut();
|
||||
}
|
||||
})
|
||||
.wrapProgress(getActivity(), R.string.loading, false)
|
||||
.exec(accountID);
|
||||
}
|
||||
|
||||
private void onLoggedOut(){
|
||||
AccountSessionManager.getInstance().removeAccount(accountID);
|
||||
getActivity().finish();
|
||||
Intent intent=new Intent(getActivity(), MainActivity.class);
|
||||
startActivity(intent);
|
||||
}
|
||||
|
||||
private void clearImageCache(){
|
||||
MastodonAPIController.runInBackground(()->{
|
||||
Activity activity=getActivity();
|
||||
ImageCache.getInstance(getActivity()).clear();
|
||||
Toast.makeText(activity, R.string.media_cache_cleared, Toast.LENGTH_SHORT).show();
|
||||
});
|
||||
}
|
||||
|
||||
@Subscribe
|
||||
public void onSelfUpdateStateChanged(SelfUpdateStateChangedEvent ev){
|
||||
if(items.get(0) instanceof UpdateItem item){
|
||||
RecyclerView.ViewHolder holder=list.findViewHolderForAdapterPosition(0);
|
||||
if(holder instanceof UpdateViewHolder uvh){
|
||||
uvh.bind(item);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private static abstract class Item{
|
||||
public abstract int getViewType();
|
||||
}
|
||||
|
||||
private class HeaderItem extends Item{
|
||||
private String text;
|
||||
|
||||
public HeaderItem(@StringRes int text){
|
||||
this.text=getString(text);
|
||||
}
|
||||
|
||||
public HeaderItem(String text){
|
||||
this.text=text;
|
||||
}
|
||||
|
||||
@Override
|
||||
public int getViewType(){
|
||||
return 0;
|
||||
}
|
||||
}
|
||||
|
||||
private class SwitchItem extends Item{
|
||||
private String text;
|
||||
private int icon;
|
||||
private boolean checked;
|
||||
private Consumer<SwitchItem> onChanged;
|
||||
private boolean enabled=true;
|
||||
|
||||
public SwitchItem(@StringRes int text, @DrawableRes int icon, boolean checked, Consumer<SwitchItem> onChanged){
|
||||
this.text=getString(text);
|
||||
this.icon=icon;
|
||||
this.checked=checked;
|
||||
this.onChanged=onChanged;
|
||||
}
|
||||
|
||||
public SwitchItem(@StringRes int text, int icon, boolean checked, Consumer<SwitchItem> onChanged, boolean enabled){
|
||||
this.text=getString(text);
|
||||
this.icon=icon;
|
||||
this.checked=checked;
|
||||
this.onChanged=onChanged;
|
||||
this.enabled=enabled;
|
||||
}
|
||||
|
||||
@Override
|
||||
public int getViewType(){
|
||||
return 1;
|
||||
}
|
||||
}
|
||||
|
||||
private static class ThemeItem extends Item{
|
||||
|
||||
@Override
|
||||
public int getViewType(){
|
||||
return 2;
|
||||
}
|
||||
}
|
||||
|
||||
private static class NotificationPolicyItem extends Item{
|
||||
|
||||
@Override
|
||||
public int getViewType(){
|
||||
return 3;
|
||||
}
|
||||
}
|
||||
|
||||
private class TextItem extends Item{
|
||||
private String text;
|
||||
private Runnable onClick;
|
||||
|
||||
public TextItem(@StringRes int text, Runnable onClick){
|
||||
this.text=getString(text);
|
||||
this.onClick=onClick;
|
||||
}
|
||||
|
||||
public TextItem(String text, Runnable onClick){
|
||||
this.text=text;
|
||||
this.onClick=onClick;
|
||||
}
|
||||
|
||||
@Override
|
||||
public int getViewType(){
|
||||
return 4;
|
||||
}
|
||||
}
|
||||
|
||||
private class RedHeaderItem extends HeaderItem{
|
||||
|
||||
public RedHeaderItem(int text){
|
||||
super(text);
|
||||
}
|
||||
|
||||
public RedHeaderItem(String text){
|
||||
super(text);
|
||||
}
|
||||
|
||||
@Override
|
||||
public int getViewType(){
|
||||
return 5;
|
||||
}
|
||||
}
|
||||
|
||||
private class FooterItem extends Item{
|
||||
private String text;
|
||||
|
||||
public FooterItem(String text){
|
||||
this.text=text;
|
||||
}
|
||||
|
||||
@Override
|
||||
public int getViewType(){
|
||||
return 6;
|
||||
}
|
||||
}
|
||||
|
||||
private class UpdateItem extends Item{
|
||||
|
||||
@Override
|
||||
public int getViewType(){
|
||||
return 7;
|
||||
}
|
||||
}
|
||||
|
||||
private class SettingsAdapter extends RecyclerView.Adapter<BindableViewHolder<Item>>{
|
||||
@NonNull
|
||||
@Override
|
||||
public BindableViewHolder<Item> onCreateViewHolder(@NonNull ViewGroup parent, int viewType){
|
||||
//noinspection unchecked
|
||||
return (BindableViewHolder<Item>) switch(viewType){
|
||||
case 0 -> new HeaderViewHolder(false);
|
||||
case 1 -> new SwitchViewHolder();
|
||||
case 2 -> new ThemeViewHolder();
|
||||
case 3 -> new NotificationPolicyViewHolder();
|
||||
case 4 -> new TextViewHolder();
|
||||
case 5 -> new HeaderViewHolder(true);
|
||||
case 6 -> new FooterViewHolder();
|
||||
case 7 -> new UpdateViewHolder();
|
||||
default -> throw new IllegalStateException("Unexpected value: "+viewType);
|
||||
};
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onBindViewHolder(@NonNull BindableViewHolder<Item> holder, int position){
|
||||
holder.bind(items.get(position));
|
||||
}
|
||||
|
||||
@Override
|
||||
public int getItemCount(){
|
||||
return items.size();
|
||||
}
|
||||
|
||||
@Override
|
||||
public int getItemViewType(int position){
|
||||
return items.get(position).getViewType();
|
||||
}
|
||||
}
|
||||
|
||||
private class HeaderViewHolder extends BindableViewHolder<HeaderItem>{
|
||||
private final TextView text;
|
||||
public HeaderViewHolder(boolean red){
|
||||
super(getActivity(), R.layout.item_settings_header, list);
|
||||
text=(TextView) itemView;
|
||||
if(red)
|
||||
text.setTextColor(getResources().getColor(UiUtils.isDarkTheme() ? R.color.error_400 : R.color.error_700));
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onBind(HeaderItem item){
|
||||
text.setText(item.text);
|
||||
}
|
||||
}
|
||||
|
||||
private class SwitchViewHolder extends BindableViewHolder<SwitchItem> implements UsableRecyclerView.DisableableClickable{
|
||||
private final TextView text;
|
||||
private final ImageView icon;
|
||||
private final Switch checkbox;
|
||||
|
||||
public SwitchViewHolder(){
|
||||
super(getActivity(), R.layout.item_settings_switch, list);
|
||||
text=findViewById(R.id.text);
|
||||
icon=findViewById(R.id.icon);
|
||||
checkbox=findViewById(R.id.checkbox);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onBind(SwitchItem item){
|
||||
text.setText(item.text);
|
||||
icon.setImageResource(item.icon);
|
||||
checkbox.setChecked(item.checked && item.enabled);
|
||||
checkbox.setEnabled(item.enabled);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onClick(){
|
||||
item.checked=!item.checked;
|
||||
checkbox.setChecked(item.checked);
|
||||
item.onChanged.accept(item);
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean isEnabled(){
|
||||
return item.enabled;
|
||||
}
|
||||
}
|
||||
|
||||
private class ThemeViewHolder extends BindableViewHolder<ThemeItem>{
|
||||
private SubitemHolder autoHolder, lightHolder, darkHolder;
|
||||
|
||||
public ThemeViewHolder(){
|
||||
super(getActivity(), R.layout.item_settings_theme, list);
|
||||
autoHolder=new SubitemHolder(findViewById(R.id.theme_auto));
|
||||
lightHolder=new SubitemHolder(findViewById(R.id.theme_light));
|
||||
darkHolder=new SubitemHolder(findViewById(R.id.theme_dark));
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onBind(ThemeItem item){
|
||||
bindSubitems();
|
||||
}
|
||||
|
||||
public void bindSubitems(){
|
||||
autoHolder.bind(R.string.theme_auto, GlobalUserPreferences.trueBlackTheme ? R.drawable.theme_auto_trueblack : R.drawable.theme_auto, GlobalUserPreferences.theme==GlobalUserPreferences.ThemePreference.AUTO);
|
||||
lightHolder.bind(R.string.theme_light, R.drawable.theme_light, GlobalUserPreferences.theme==GlobalUserPreferences.ThemePreference.LIGHT);
|
||||
darkHolder.bind(R.string.theme_dark, GlobalUserPreferences.trueBlackTheme ? R.drawable.theme_dark_trueblack : R.drawable.theme_dark, GlobalUserPreferences.theme==GlobalUserPreferences.ThemePreference.DARK);
|
||||
}
|
||||
|
||||
private void onSubitemClick(View v){
|
||||
GlobalUserPreferences.ThemePreference pref;
|
||||
if(v.getId()==R.id.theme_auto)
|
||||
pref=GlobalUserPreferences.ThemePreference.AUTO;
|
||||
else if(v.getId()==R.id.theme_light)
|
||||
pref=GlobalUserPreferences.ThemePreference.LIGHT;
|
||||
else if(v.getId()==R.id.theme_dark)
|
||||
pref=GlobalUserPreferences.ThemePreference.DARK;
|
||||
else
|
||||
return;
|
||||
onThemePreferenceClick(pref);
|
||||
}
|
||||
|
||||
private class SubitemHolder{
|
||||
public TextView text;
|
||||
public ImageView icon;
|
||||
public RadioButton checkbox;
|
||||
|
||||
public SubitemHolder(View view){
|
||||
text=view.findViewById(R.id.text);
|
||||
icon=view.findViewById(R.id.icon);
|
||||
checkbox=view.findViewById(R.id.checkbox);
|
||||
view.setOnClickListener(ThemeViewHolder.this::onSubitemClick);
|
||||
|
||||
icon.setClipToOutline(true);
|
||||
icon.setOutlineProvider(OutlineProviders.roundedRect(4));
|
||||
}
|
||||
|
||||
public void bind(int text, int icon, boolean checked){
|
||||
this.text.setText(text);
|
||||
this.icon.setImageResource(icon);
|
||||
checkbox.setChecked(checked);
|
||||
}
|
||||
|
||||
public void setChecked(boolean checked){
|
||||
checkbox.setChecked(checked);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private class NotificationPolicyViewHolder extends BindableViewHolder<NotificationPolicyItem>{
|
||||
private final Button button;
|
||||
private final PopupMenu popupMenu;
|
||||
|
||||
@SuppressLint("ClickableViewAccessibility")
|
||||
public NotificationPolicyViewHolder(){
|
||||
super(getActivity(), R.layout.item_settings_notification_policy, list);
|
||||
button=findViewById(R.id.button);
|
||||
popupMenu=new PopupMenu(getActivity(), button, Gravity.CENTER_HORIZONTAL);
|
||||
popupMenu.inflate(R.menu.notification_policy);
|
||||
popupMenu.setOnMenuItemClickListener(item->{
|
||||
PushSubscription.Policy policy;
|
||||
int id=item.getItemId();
|
||||
if(id==R.id.notify_anyone)
|
||||
policy=PushSubscription.Policy.ALL;
|
||||
else if(id==R.id.notify_followed)
|
||||
policy=PushSubscription.Policy.FOLLOWED;
|
||||
else if(id==R.id.notify_follower)
|
||||
policy=PushSubscription.Policy.FOLLOWER;
|
||||
else if(id==R.id.notify_none)
|
||||
policy=PushSubscription.Policy.NONE;
|
||||
else
|
||||
return false;
|
||||
onNotificationsPolicyChanged(policy);
|
||||
return true;
|
||||
});
|
||||
UiUtils.enablePopupMenuIcons(getActivity(), popupMenu);
|
||||
button.setOnTouchListener(popupMenu.getDragToOpenListener());
|
||||
button.setOnClickListener(v->popupMenu.show());
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onBind(NotificationPolicyItem item){
|
||||
button.setText(switch(getPushSubscription().policy){
|
||||
case ALL -> R.string.notify_anyone;
|
||||
case FOLLOWED -> R.string.notify_followed;
|
||||
case FOLLOWER -> R.string.notify_follower;
|
||||
case NONE -> R.string.notify_none;
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
private class TextViewHolder extends BindableViewHolder<TextItem> implements UsableRecyclerView.Clickable{
|
||||
private final TextView text;
|
||||
public TextViewHolder(){
|
||||
super(getActivity(), R.layout.item_settings_text, list);
|
||||
text=(TextView) itemView;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onBind(TextItem item){
|
||||
text.setText(item.text);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onClick(){
|
||||
item.onClick.run();
|
||||
}
|
||||
}
|
||||
|
||||
private class FooterViewHolder extends BindableViewHolder<FooterItem>{
|
||||
private final TextView text;
|
||||
public FooterViewHolder(){
|
||||
super(getActivity(), R.layout.item_settings_footer, list);
|
||||
text=(TextView) itemView;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onBind(FooterItem item){
|
||||
text.setText(item.text);
|
||||
}
|
||||
}
|
||||
|
||||
private class UpdateViewHolder extends BindableViewHolder<UpdateItem>{
|
||||
|
||||
private final TextView text;
|
||||
private final Button button;
|
||||
private final ImageButton cancelBtn;
|
||||
private final ProgressBar progress;
|
||||
|
||||
private ObjectAnimator rotationAnimator;
|
||||
private Runnable progressUpdater=this::updateProgress;
|
||||
|
||||
public UpdateViewHolder(){
|
||||
super(getActivity(), R.layout.item_settings_update, list);
|
||||
text=findViewById(R.id.text);
|
||||
button=findViewById(R.id.button);
|
||||
cancelBtn=findViewById(R.id.cancel_btn);
|
||||
progress=findViewById(R.id.progress);
|
||||
button.setOnClickListener(v->{
|
||||
GithubSelfUpdater updater=GithubSelfUpdater.getInstance();
|
||||
switch(updater.getState()){
|
||||
case UPDATE_AVAILABLE -> updater.downloadUpdate();
|
||||
case DOWNLOADED -> updater.installUpdate(getActivity());
|
||||
}
|
||||
});
|
||||
cancelBtn.setOnClickListener(v->GithubSelfUpdater.getInstance().cancelDownload());
|
||||
rotationAnimator=ObjectAnimator.ofFloat(progress, View.ROTATION, 0f, 360f);
|
||||
rotationAnimator.setInterpolator(new LinearInterpolator());
|
||||
rotationAnimator.setDuration(1500);
|
||||
rotationAnimator.setRepeatCount(ObjectAnimator.INFINITE);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onBind(UpdateItem item){
|
||||
GithubSelfUpdater updater=GithubSelfUpdater.getInstance();
|
||||
GithubSelfUpdater.UpdateInfo info=updater.getUpdateInfo();
|
||||
GithubSelfUpdater.UpdateState state=updater.getState();
|
||||
if(state!=GithubSelfUpdater.UpdateState.DOWNLOADED){
|
||||
text.setText(getString(R.string.update_available, info.version));
|
||||
button.setText(getString(R.string.download_update, UiUtils.formatFileSize(getActivity(), info.size, false)));
|
||||
}else{
|
||||
text.setText(getString(R.string.update_ready, info.version));
|
||||
button.setText(R.string.install_update);
|
||||
}
|
||||
if(state==GithubSelfUpdater.UpdateState.DOWNLOADING){
|
||||
rotationAnimator.start();
|
||||
button.setVisibility(View.INVISIBLE);
|
||||
cancelBtn.setVisibility(View.VISIBLE);
|
||||
progress.setVisibility(View.VISIBLE);
|
||||
updateProgress();
|
||||
}else{
|
||||
rotationAnimator.cancel();
|
||||
button.setVisibility(View.VISIBLE);
|
||||
cancelBtn.setVisibility(View.GONE);
|
||||
progress.setVisibility(View.GONE);
|
||||
progress.removeCallbacks(progressUpdater);
|
||||
}
|
||||
}
|
||||
|
||||
private void updateProgress(){
|
||||
GithubSelfUpdater updater=GithubSelfUpdater.getInstance();
|
||||
if(updater.getState()!=GithubSelfUpdater.UpdateState.DOWNLOADING)
|
||||
return;
|
||||
int value=Math.round(progress.getMax()*updater.getDownloadProgress());
|
||||
if(Build.VERSION.SDK_INT>=24)
|
||||
progress.setProgress(value, true);
|
||||
else
|
||||
progress.setProgress(value);
|
||||
progress.postDelayed(progressUpdater, 1000);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,8 +1,6 @@
|
||||
package org.joinmastodon.android.fragments;
|
||||
|
||||
import android.content.res.ColorStateList;
|
||||
import android.graphics.Canvas;
|
||||
import android.graphics.Paint;
|
||||
import android.os.Bundle;
|
||||
import android.view.View;
|
||||
import android.view.ViewGroup;
|
||||
@@ -13,7 +11,8 @@ import org.joinmastodon.android.api.requests.statuses.GetStatusContext;
|
||||
import org.joinmastodon.android.api.session.AccountSessionManager;
|
||||
import org.joinmastodon.android.events.StatusCreatedEvent;
|
||||
import org.joinmastodon.android.model.Account;
|
||||
import org.joinmastodon.android.model.Filter;
|
||||
import org.joinmastodon.android.model.FilterContext;
|
||||
import org.joinmastodon.android.model.LegacyFilter;
|
||||
import org.joinmastodon.android.model.Status;
|
||||
import org.joinmastodon.android.model.StatusContext;
|
||||
import org.joinmastodon.android.ui.displayitems.ExtendedFooterStatusDisplayItem;
|
||||
@@ -47,7 +46,10 @@ public class ThreadFragment extends StatusListFragment{
|
||||
knownAccounts.put(inReplyToAccount.id, inReplyToAccount);
|
||||
data.add(mainStatus);
|
||||
onAppendItems(Collections.singletonList(mainStatus));
|
||||
setTitle(HtmlParser.parseCustomEmoji(getString(R.string.post_from_user, mainStatus.account.displayName), mainStatus.account.emojis));
|
||||
if(AccountSessionManager.get(accountID).getLocalPreferences().customEmojiInNames)
|
||||
setTitle(HtmlParser.parseCustomEmoji(getString(R.string.post_from_user, mainStatus.account.displayName), mainStatus.account.emojis));
|
||||
else
|
||||
setTitle(getString(R.string.post_from_user, mainStatus.account.displayName));
|
||||
}
|
||||
|
||||
@Override
|
||||
@@ -102,11 +104,11 @@ public class ThreadFragment extends StatusListFragment{
|
||||
}
|
||||
|
||||
private List<Status> filterStatuses(List<Status> statuses){
|
||||
List<Filter> filters=AccountSessionManager.getInstance().getAccount(accountID).wordFilters.stream().filter(f->f.context.contains(Filter.FilterContext.THREAD)).collect(Collectors.toList());
|
||||
List<LegacyFilter> filters=AccountSessionManager.getInstance().getAccount(accountID).wordFilters.stream().filter(f->f.context.contains(FilterContext.THREAD)).collect(Collectors.toList());
|
||||
if(filters.isEmpty())
|
||||
return statuses;
|
||||
return statuses.stream().filter(status->{
|
||||
for(Filter filter:filters){
|
||||
for(LegacyFilter filter:filters){
|
||||
if(filter.matches(status))
|
||||
return false;
|
||||
}
|
||||
|
||||
@@ -95,7 +95,7 @@ public class ComposeAccountSearchFragment extends BaseAccountListFragment{
|
||||
@Override
|
||||
public void onSuccess(SearchResults result){
|
||||
setEmptyText(R.string.no_search_results);
|
||||
onDataLoaded(result.accounts.stream().map(AccountViewModel::new).collect(Collectors.toList()), false);
|
||||
onDataLoaded(result.accounts.stream().map(a->new AccountViewModel(a, accountID)).collect(Collectors.toList()), false);
|
||||
}
|
||||
})
|
||||
.exec(accountID);
|
||||
|
||||
@@ -24,7 +24,7 @@ public abstract class PaginatedAccountListFragment extends BaseAccountListFragme
|
||||
nextMaxID=result.nextPageUri.getQueryParameter("max_id");
|
||||
else
|
||||
nextMaxID=null;
|
||||
onDataLoaded(result.stream().map(AccountViewModel::new).collect(Collectors.toList()), nextMaxID!=null);
|
||||
onDataLoaded(result.stream().map(a->new AccountViewModel(a, accountID)).collect(Collectors.toList()), nextMaxID!=null);
|
||||
}
|
||||
})
|
||||
.exec(accountID);
|
||||
|
||||
@@ -5,7 +5,7 @@ import android.view.View;
|
||||
|
||||
import org.joinmastodon.android.api.requests.timelines.GetPublicTimeline;
|
||||
import org.joinmastodon.android.fragments.StatusListFragment;
|
||||
import org.joinmastodon.android.model.Filter;
|
||||
import org.joinmastodon.android.model.FilterContext;
|
||||
import org.joinmastodon.android.model.Status;
|
||||
import org.joinmastodon.android.ui.utils.DiscoverInfoBannerHelper;
|
||||
import org.joinmastodon.android.utils.StatusFilterPredicate;
|
||||
@@ -27,7 +27,7 @@ public class LocalTimelineFragment extends StatusListFragment{
|
||||
public void onSuccess(List<Status> result){
|
||||
if(!result.isEmpty())
|
||||
maxID=result.get(result.size()-1).id;
|
||||
onDataLoaded(result.stream().filter(new StatusFilterPredicate(accountID, Filter.FilterContext.PUBLIC)).collect(Collectors.toList()), !result.isEmpty());
|
||||
onDataLoaded(result.stream().filter(new StatusFilterPredicate(accountID, FilterContext.PUBLIC)).collect(Collectors.toList()), !result.isEmpty());
|
||||
}
|
||||
})
|
||||
.exec(accountID);
|
||||
|
||||
@@ -3,7 +3,6 @@ package org.joinmastodon.android.fragments.onboarding;
|
||||
import android.annotation.SuppressLint;
|
||||
import android.content.ActivityNotFoundException;
|
||||
import android.content.Intent;
|
||||
import android.os.Build;
|
||||
import android.os.Bundle;
|
||||
import android.os.Handler;
|
||||
import android.os.Looper;
|
||||
@@ -23,8 +22,7 @@ import org.joinmastodon.android.api.requests.accounts.UpdateAccountCredentials;
|
||||
import org.joinmastodon.android.api.session.AccountActivationInfo;
|
||||
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.SettingsFragment;
|
||||
import org.joinmastodon.android.fragments.settings.SettingsMainFragment;
|
||||
import org.joinmastodon.android.model.Account;
|
||||
import org.joinmastodon.android.ui.AccountSwitcherSheet;
|
||||
import org.joinmastodon.android.ui.utils.UiUtils;
|
||||
@@ -38,7 +36,6 @@ import me.grishka.appkit.api.APIRequest;
|
||||
import me.grishka.appkit.api.Callback;
|
||||
import me.grishka.appkit.api.ErrorResponse;
|
||||
import me.grishka.appkit.fragments.ToolbarFragment;
|
||||
import me.grishka.appkit.utils.V;
|
||||
|
||||
public class AccountActivationFragment extends ToolbarFragment{
|
||||
private String accountID;
|
||||
@@ -70,7 +67,7 @@ public class AccountActivationFragment extends ToolbarFragment{
|
||||
openEmailBtn.setOnLongClickListener(v->{
|
||||
Bundle args=new Bundle();
|
||||
args.putString("account", accountID);
|
||||
Nav.go(getActivity(), SettingsFragment.class, args);
|
||||
Nav.go(getActivity(), SettingsMainFragment.class, args);
|
||||
return true;
|
||||
});
|
||||
resendBtn=view.findViewById(R.id.btn_resend);
|
||||
|
||||
@@ -1,8 +1,6 @@
|
||||
package org.joinmastodon.android.fragments.onboarding;
|
||||
|
||||
import android.annotation.SuppressLint;
|
||||
import android.app.Activity;
|
||||
import android.os.Build;
|
||||
import android.os.Bundle;
|
||||
import android.text.Html;
|
||||
import android.view.LayoutInflater;
|
||||
@@ -15,20 +13,16 @@ import android.widget.TextView;
|
||||
import org.joinmastodon.android.R;
|
||||
import org.joinmastodon.android.model.Instance;
|
||||
import org.joinmastodon.android.ui.DividerItemDecoration;
|
||||
import org.joinmastodon.android.ui.text.HtmlParser;
|
||||
import org.joinmastodon.android.ui.adapters.InstanceRulesAdapter;
|
||||
import org.joinmastodon.android.ui.utils.UiUtils;
|
||||
import org.joinmastodon.android.utils.ElevationOnScrollListener;
|
||||
import org.parceler.Parcels;
|
||||
|
||||
import androidx.annotation.NonNull;
|
||||
import androidx.recyclerview.widget.LinearLayoutManager;
|
||||
import androidx.recyclerview.widget.RecyclerView;
|
||||
import me.grishka.appkit.Nav;
|
||||
import me.grishka.appkit.fragments.ToolbarFragment;
|
||||
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.FragmentRootLinearLayout;
|
||||
import me.grishka.appkit.views.UsableRecyclerView;
|
||||
|
||||
@@ -68,9 +62,8 @@ public class InstanceRulesFragment extends ToolbarFragment{
|
||||
|
||||
adapter=new MergeRecyclerAdapter();
|
||||
adapter.addAdapter(new SingleViewRecyclerAdapter(headerView));
|
||||
adapter.addAdapter(new ItemsAdapter());
|
||||
adapter.addAdapter(new InstanceRulesAdapter(instance.rules));
|
||||
list.setAdapter(adapter);
|
||||
list.addItemDecoration(new DividerItemDecoration(getActivity(), R.attr.colorM3SurfaceVariant, 1, 56, 0, DividerItemDecoration.NOT_FIRST));
|
||||
|
||||
btn=view.findViewById(R.id.btn_next);
|
||||
btn.setOnClickListener(v->onButtonClick());
|
||||
@@ -113,43 +106,4 @@ public class InstanceRulesFragment extends ToolbarFragment{
|
||||
public void onApplyWindowInsets(WindowInsets insets){
|
||||
super.onApplyWindowInsets(UiUtils.applyBottomInsetToFixedView(buttonBar, insets));
|
||||
}
|
||||
|
||||
private class ItemsAdapter extends RecyclerView.Adapter<ItemViewHolder>{
|
||||
|
||||
@NonNull
|
||||
@Override
|
||||
public ItemViewHolder onCreateViewHolder(@NonNull ViewGroup parent, int viewType){
|
||||
return new ItemViewHolder();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onBindViewHolder(@NonNull ItemViewHolder holder, int position){
|
||||
holder.bind(instance.rules.get(position));
|
||||
}
|
||||
|
||||
@Override
|
||||
public int getItemCount(){
|
||||
return instance.rules.size();
|
||||
}
|
||||
}
|
||||
|
||||
private class ItemViewHolder extends BindableViewHolder<Instance.Rule>{
|
||||
private final TextView text, number;
|
||||
|
||||
public ItemViewHolder(){
|
||||
super(getActivity(), R.layout.item_server_rule, list);
|
||||
text=findViewById(R.id.text);
|
||||
number=findViewById(R.id.number);
|
||||
}
|
||||
|
||||
@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", getAbsoluteAdapterPosition()));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -22,8 +22,8 @@ import org.joinmastodon.android.api.requests.accounts.SetAccountFollowed;
|
||||
import org.joinmastodon.android.fragments.HomeFragment;
|
||||
import org.joinmastodon.android.fragments.ProfileFragment;
|
||||
import org.joinmastodon.android.model.FollowSuggestion;
|
||||
import org.joinmastodon.android.model.ParsedAccount;
|
||||
import org.joinmastodon.android.model.Relationship;
|
||||
import org.joinmastodon.android.model.viewmodel.AccountViewModel;
|
||||
import org.joinmastodon.android.ui.OutlineProviders;
|
||||
import org.joinmastodon.android.ui.utils.UiUtils;
|
||||
import org.joinmastodon.android.ui.views.ProgressBarButton;
|
||||
@@ -52,7 +52,7 @@ import me.grishka.appkit.utils.V;
|
||||
import me.grishka.appkit.views.FragmentRootLinearLayout;
|
||||
import me.grishka.appkit.views.UsableRecyclerView;
|
||||
|
||||
public class OnboardingFollowSuggestionsFragment extends BaseRecyclerFragment<ParsedAccount>{
|
||||
public class OnboardingFollowSuggestionsFragment extends BaseRecyclerFragment<AccountViewModel>{
|
||||
private String accountID;
|
||||
private Map<String, Relationship> relationships=Collections.emptyMap();
|
||||
private GetAccountRelationships relationshipsRequest;
|
||||
@@ -97,7 +97,7 @@ public class OnboardingFollowSuggestionsFragment extends BaseRecyclerFragment<Pa
|
||||
.setCallback(new SimpleCallback<>(this){
|
||||
@Override
|
||||
public void onSuccess(List<FollowSuggestion> result){
|
||||
onDataLoaded(result.stream().map(fs->new ParsedAccount(fs.account, accountID)).collect(Collectors.toList()), false);
|
||||
onDataLoaded(result.stream().map(fs->new AccountViewModel(fs.account, accountID)).collect(Collectors.toList()), false);
|
||||
loadRelationships();
|
||||
}
|
||||
})
|
||||
@@ -146,7 +146,7 @@ public class OnboardingFollowSuggestionsFragment extends BaseRecyclerFragment<Pa
|
||||
return;
|
||||
}
|
||||
ArrayList<String> accountIdsToFollow=new ArrayList<>();
|
||||
for(ParsedAccount acc:data){
|
||||
for(AccountViewModel acc:data){
|
||||
Relationship rel=relationships.get(acc.account.id);
|
||||
if(rel==null)
|
||||
continue;
|
||||
@@ -239,14 +239,14 @@ public class OnboardingFollowSuggestionsFragment extends BaseRecyclerFragment<Pa
|
||||
|
||||
@Override
|
||||
public ImageLoaderRequest getImageRequest(int position, int image){
|
||||
ParsedAccount account=data.get(position);
|
||||
AccountViewModel account=data.get(position);
|
||||
if(image==0)
|
||||
return account.avatarRequest;
|
||||
return account.avaRequest;
|
||||
return account.emojiHelper.getImageRequest(image-1);
|
||||
}
|
||||
}
|
||||
|
||||
private class SuggestionViewHolder extends BindableViewHolder<ParsedAccount> implements ImageLoaderViewHolder, UsableRecyclerView.Clickable{
|
||||
private class SuggestionViewHolder extends BindableViewHolder<AccountViewModel> implements ImageLoaderViewHolder, UsableRecyclerView.Clickable{
|
||||
private final TextView name, username, bio;
|
||||
private final ImageView avatar;
|
||||
private final ProgressBarButton actionButton;
|
||||
@@ -271,7 +271,7 @@ public class OnboardingFollowSuggestionsFragment extends BaseRecyclerFragment<Pa
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onBind(ParsedAccount item){
|
||||
public void onBind(AccountViewModel item){
|
||||
name.setText(item.parsedName);
|
||||
username.setText(item.account.getDisplayUsername());
|
||||
if(TextUtils.isEmpty(item.parsedBio)){
|
||||
|
||||
@@ -0,0 +1,87 @@
|
||||
package org.joinmastodon.android.fragments.settings;
|
||||
|
||||
import android.os.Build;
|
||||
import android.os.Bundle;
|
||||
import android.view.View;
|
||||
import android.view.WindowInsets;
|
||||
|
||||
import org.joinmastodon.android.R;
|
||||
import org.joinmastodon.android.fragments.MastodonRecyclerFragment;
|
||||
import org.joinmastodon.android.model.viewmodel.CheckableListItem;
|
||||
import org.joinmastodon.android.model.viewmodel.ListItem;
|
||||
import org.joinmastodon.android.ui.BetterItemAnimator;
|
||||
import org.joinmastodon.android.ui.DividerItemDecoration;
|
||||
import org.joinmastodon.android.ui.adapters.GenericListItemsAdapter;
|
||||
import org.joinmastodon.android.ui.viewholders.CheckableListItemViewHolder;
|
||||
import org.joinmastodon.android.ui.viewholders.ListItemViewHolder;
|
||||
import org.joinmastodon.android.ui.viewholders.SimpleListItemViewHolder;
|
||||
|
||||
import androidx.recyclerview.widget.RecyclerView;
|
||||
import me.grishka.appkit.utils.V;
|
||||
|
||||
public abstract class BaseSettingsFragment<T> extends MastodonRecyclerFragment<ListItem<T>>{
|
||||
protected GenericListItemsAdapter<T> itemsAdapter;
|
||||
protected String accountID;
|
||||
|
||||
public BaseSettingsFragment(){
|
||||
super(20);
|
||||
}
|
||||
|
||||
public BaseSettingsFragment(int perPage){
|
||||
super(perPage);
|
||||
}
|
||||
|
||||
public BaseSettingsFragment(int layout, int perPage){
|
||||
super(layout, perPage);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onCreate(Bundle savedInstanceState){
|
||||
super.onCreate(savedInstanceState);
|
||||
setRetainInstance(true);
|
||||
accountID=getArguments().getString("account");
|
||||
setRefreshEnabled(false);
|
||||
}
|
||||
|
||||
@Override
|
||||
protected RecyclerView.Adapter<?> getAdapter(){
|
||||
return itemsAdapter=new GenericListItemsAdapter<T>(data);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onViewCreated(View view, Bundle savedInstanceState){
|
||||
super.onViewCreated(view, savedInstanceState);
|
||||
list.addItemDecoration(new DividerItemDecoration(getActivity(), R.attr.colorM3OutlineVariant, 1, 0, 0, vh->vh instanceof SimpleListItemViewHolder ivh && ivh.getItem().dividerAfter));
|
||||
list.setItemAnimator(new BetterItemAnimator());
|
||||
}
|
||||
|
||||
protected int indexOfItemsAdapter(){
|
||||
return 0;
|
||||
}
|
||||
|
||||
protected void toggleCheckableItem(CheckableListItem<T> item){
|
||||
item.toggle();
|
||||
rebindItem(item);
|
||||
}
|
||||
|
||||
protected void rebindItem(ListItem<T> item){
|
||||
if(list==null)
|
||||
return;
|
||||
if(list.findViewHolderForAdapterPosition(indexOfItemsAdapter()+data.indexOf(item)) instanceof ListItemViewHolder<?> holder){
|
||||
holder.rebind();
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onApplyWindowInsets(WindowInsets insets){
|
||||
if(Build.VERSION.SDK_INT>=29 && insets.getTappableElementInsets().bottom==0){
|
||||
list.setPadding(0, 0, 0, insets.getSystemWindowInsetBottom());
|
||||
emptyView.setPadding(0, 0, 0, insets.getSystemWindowInsetBottom());
|
||||
progress.setPadding(0, 0, 0, insets.getSystemWindowInsetBottom());
|
||||
insets=insets.inset(0, 0, 0, insets.getSystemWindowInsetBottom());
|
||||
}else{
|
||||
list.setPadding(0, 0, 0, 0);
|
||||
}
|
||||
super.onApplyWindowInsets(insets);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,324 @@
|
||||
package org.joinmastodon.android.fragments.settings;
|
||||
|
||||
import android.app.AlertDialog;
|
||||
import android.content.DialogInterface;
|
||||
import android.os.Bundle;
|
||||
import android.os.Parcelable;
|
||||
import android.view.Menu;
|
||||
import android.view.MenuInflater;
|
||||
import android.view.MenuItem;
|
||||
import android.widget.DatePicker;
|
||||
import android.widget.EditText;
|
||||
|
||||
import org.joinmastodon.android.E;
|
||||
import org.joinmastodon.android.R;
|
||||
import org.joinmastodon.android.api.MastodonAPIRequest;
|
||||
import org.joinmastodon.android.api.requests.filters.CreateFilter;
|
||||
import org.joinmastodon.android.api.requests.filters.DeleteFilter;
|
||||
import org.joinmastodon.android.api.requests.filters.UpdateFilter;
|
||||
import org.joinmastodon.android.events.SettingsFilterCreatedOrUpdatedEvent;
|
||||
import org.joinmastodon.android.events.SettingsFilterDeletedEvent;
|
||||
import org.joinmastodon.android.model.Filter;
|
||||
import org.joinmastodon.android.model.FilterAction;
|
||||
import org.joinmastodon.android.model.FilterContext;
|
||||
import org.joinmastodon.android.model.FilterKeyword;
|
||||
import org.joinmastodon.android.model.viewmodel.CheckableListItem;
|
||||
import org.joinmastodon.android.model.viewmodel.ListItem;
|
||||
import org.joinmastodon.android.ui.M3AlertDialogBuilder;
|
||||
import org.joinmastodon.android.ui.utils.UiUtils;
|
||||
import org.joinmastodon.android.ui.views.FloatingHintEditTextLayout;
|
||||
import org.parceler.Parcels;
|
||||
|
||||
import java.time.Instant;
|
||||
import java.time.LocalDate;
|
||||
import java.time.ZoneId;
|
||||
import java.time.ZonedDateTime;
|
||||
import java.util.ArrayList;
|
||||
import java.util.Arrays;
|
||||
import java.util.EnumSet;
|
||||
import java.util.List;
|
||||
import java.util.Objects;
|
||||
import java.util.function.Consumer;
|
||||
import java.util.stream.Collectors;
|
||||
|
||||
import androidx.recyclerview.widget.RecyclerView;
|
||||
import me.grishka.appkit.Nav;
|
||||
import me.grishka.appkit.api.APIRequest;
|
||||
import me.grishka.appkit.api.Callback;
|
||||
import me.grishka.appkit.api.ErrorResponse;
|
||||
import me.grishka.appkit.fragments.OnBackPressedListener;
|
||||
import me.grishka.appkit.utils.MergeRecyclerAdapter;
|
||||
import me.grishka.appkit.utils.SingleViewRecyclerAdapter;
|
||||
|
||||
public class EditFilterFragment extends BaseSettingsFragment<Void> implements OnBackPressedListener{
|
||||
private static final int WORDS_RESULT=370;
|
||||
private static final int CONTEXT_RESULT=651;
|
||||
|
||||
private Filter filter;
|
||||
private ListItem<Void> durationItem, wordsItem, contextItem;
|
||||
private CheckableListItem<Void> cwItem;
|
||||
private FloatingHintEditTextLayout titleEditLayout;
|
||||
private EditText titleEdit;
|
||||
|
||||
private Instant endsAt;
|
||||
private ArrayList<FilterKeyword> keywords=new ArrayList<>();
|
||||
private ArrayList<String> deletedWordIDs=new ArrayList<>();
|
||||
private EnumSet<FilterContext> context=EnumSet.allOf(FilterContext.class);
|
||||
private boolean dirty;
|
||||
|
||||
@Override
|
||||
public void onCreate(Bundle savedInstanceState){
|
||||
super.onCreate(savedInstanceState);
|
||||
filter=Parcels.unwrap(getArguments().getParcelable("filter"));
|
||||
setTitle(filter==null ? R.string.settings_add_filter : R.string.settings_edit_filter);
|
||||
onDataLoaded(List.of(
|
||||
durationItem=new ListItem<>(R.string.settings_filter_duration, 0, this::onDurationClick),
|
||||
wordsItem=new ListItem<>(R.string.settings_filter_muted_words, 0, this::onWordsClick),
|
||||
contextItem=new ListItem<>(R.string.settings_filter_context, 0, this::onContextClick),
|
||||
cwItem=new CheckableListItem<>(R.string.settings_filter_show_cw, R.string.settings_filter_show_cw_explanation, CheckableListItem.Style.SWITCH, filter==null || filter.filterAction==FilterAction.WARN, ()->toggleCheckableItem(cwItem))
|
||||
));
|
||||
|
||||
if(filter!=null){
|
||||
endsAt=filter.expiresAt;
|
||||
keywords.addAll(filter.keywords);
|
||||
context=filter.context;
|
||||
data.add(new ListItem<>(R.string.settings_delete_filter, 0, this::onDeleteClick, R.attr.colorM3Error, false));
|
||||
}
|
||||
|
||||
updateDurationItem();
|
||||
updateWordsItem();
|
||||
updateContextItem();
|
||||
setHasOptionsMenu(true);
|
||||
setRetainInstance(true);
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void doLoadData(int offset, int count){}
|
||||
|
||||
@Override
|
||||
protected RecyclerView.Adapter<?> getAdapter(){
|
||||
titleEditLayout=(FloatingHintEditTextLayout) getActivity().getLayoutInflater().inflate(R.layout.floating_hint_edit_text, list, false);
|
||||
titleEdit=titleEditLayout.findViewById(R.id.edit);
|
||||
titleEdit.setHint(R.string.settings_filter_title);
|
||||
titleEditLayout.updateHint();
|
||||
if(filter!=null)
|
||||
titleEdit.setText(filter.title);
|
||||
|
||||
MergeRecyclerAdapter adapter=new MergeRecyclerAdapter();
|
||||
adapter.addAdapter(new SingleViewRecyclerAdapter(titleEditLayout));
|
||||
adapter.addAdapter(super.getAdapter());
|
||||
return adapter;
|
||||
}
|
||||
|
||||
@Override
|
||||
protected int indexOfItemsAdapter(){
|
||||
return 1;
|
||||
}
|
||||
|
||||
private void onDurationClick(){
|
||||
int[] durationOptions={
|
||||
1800,
|
||||
3600,
|
||||
6*3600,
|
||||
12*3600,
|
||||
24*3600,
|
||||
3*24*3600,
|
||||
7*24*3600
|
||||
};
|
||||
ArrayList<String> options=Arrays.stream(durationOptions).mapToObj(d->UiUtils.formatDuration(getActivity(), d)).collect(Collectors.toCollection(ArrayList<String>::new));
|
||||
options.add(0, getString(R.string.filter_duration_forever));
|
||||
options.add(getString(R.string.filter_duration_custom));
|
||||
Instant[] newEnd={null};
|
||||
AlertDialog alert=new M3AlertDialogBuilder(getActivity())
|
||||
.setTitle(R.string.settings_filter_duration_title)
|
||||
.setSupportingText(endsAt==null ? null : getString(R.string.settings_filter_ends, UiUtils.formatRelativeTimestampAsMinutesAgo(getActivity(), endsAt, false)))
|
||||
.setSingleChoiceItems(options.toArray(new String[0]), -1, (dlg, item)->{
|
||||
AlertDialog a=(AlertDialog) dlg;
|
||||
if(item==options.size()-1){ // custom
|
||||
showCustomDurationAlert(date->{
|
||||
if(date==null){
|
||||
a.getListView().setItemChecked(item, false);
|
||||
}else{
|
||||
newEnd[0]=date;
|
||||
a.getButton(AlertDialog.BUTTON_POSITIVE).setEnabled(true);
|
||||
}
|
||||
});
|
||||
}else{
|
||||
if(item==0){
|
||||
newEnd[0]=null;
|
||||
}else{
|
||||
newEnd[0]=Instant.now().plusSeconds(durationOptions[item-1]);
|
||||
}
|
||||
a.getButton(AlertDialog.BUTTON_POSITIVE).setEnabled(true);
|
||||
}
|
||||
})
|
||||
.setPositiveButton(R.string.ok, (dlg, item)->{
|
||||
if(!Objects.equals(endsAt, newEnd[0])){
|
||||
endsAt=newEnd[0];
|
||||
updateDurationItem();
|
||||
dirty=true;
|
||||
}
|
||||
})
|
||||
.setNegativeButton(R.string.cancel, null)
|
||||
.show();
|
||||
alert.getButton(AlertDialog.BUTTON_POSITIVE).setEnabled(false);
|
||||
}
|
||||
|
||||
private void showCustomDurationAlert(Consumer<Instant> callback){
|
||||
DatePicker picker=new DatePicker(getActivity());
|
||||
picker.setMinDate(LocalDate.now().plusDays(1).atStartOfDay(ZoneId.systemDefault()).toEpochSecond()*1000L);
|
||||
AlertDialog alert=new M3AlertDialogBuilder(getActivity())
|
||||
.setView(picker)
|
||||
.setPositiveButton(R.string.ok, (dlg, item)->{
|
||||
((AlertDialog)dlg).setOnDismissListener(null);
|
||||
callback.accept(LocalDate.of(picker.getYear(), picker.getMonth()+1, picker.getDayOfMonth()).atStartOfDay(ZoneId.systemDefault()).toInstant());
|
||||
})
|
||||
.setNegativeButton(R.string.cancel, null)
|
||||
.show();
|
||||
alert.setOnDismissListener(dialog->callback.accept(null));
|
||||
}
|
||||
|
||||
private void onWordsClick(){
|
||||
Bundle args=new Bundle();
|
||||
args.putString("account", accountID);
|
||||
args.putParcelableArrayList("words", (ArrayList<? extends Parcelable>) keywords.stream().map(Parcels::wrap).collect(Collectors.toCollection(ArrayList::new)));
|
||||
Nav.goForResult(getActivity(), FilterWordsFragment.class, args, WORDS_RESULT, this);
|
||||
}
|
||||
|
||||
private void onContextClick(){
|
||||
Bundle args=new Bundle();
|
||||
args.putString("account", accountID);
|
||||
args.putSerializable("context", context);
|
||||
Nav.goForResult(getActivity(), FilterContextFragment.class, args, CONTEXT_RESULT, this);
|
||||
}
|
||||
|
||||
private void onDeleteClick(){
|
||||
AlertDialog alert=new M3AlertDialogBuilder(getActivity())
|
||||
.setTitle(getString(R.string.settings_delete_filter_title, filter.title))
|
||||
.setMessage(R.string.settings_delete_filter_confirmation)
|
||||
.setPositiveButton(R.string.delete, (dlg, item)->deleteFilter())
|
||||
.setNegativeButton(R.string.cancel, null)
|
||||
.show();
|
||||
alert.getButton(AlertDialog.BUTTON_POSITIVE).setTextColor(UiUtils.getThemeColor(getActivity(), R.attr.colorM3Error));
|
||||
}
|
||||
|
||||
private void updateDurationItem(){
|
||||
if(endsAt==null){
|
||||
durationItem.subtitle=getString(R.string.filter_duration_forever);
|
||||
}else{
|
||||
durationItem.subtitle=getString(R.string.settings_filter_ends, UiUtils.formatRelativeTimestampAsMinutesAgo(getActivity(), endsAt, false));
|
||||
}
|
||||
rebindItem(durationItem);
|
||||
}
|
||||
|
||||
private void updateWordsItem(){
|
||||
wordsItem.subtitle=getResources().getQuantityString(R.plurals.settings_x_muted_words, keywords.size(), keywords.size());
|
||||
rebindItem(wordsItem);
|
||||
}
|
||||
|
||||
private void updateContextItem(){
|
||||
List<String> values=context.stream().map(c->getString(c.getDisplayNameRes())).collect(Collectors.toList());
|
||||
contextItem.subtitle=switch(values.size()){
|
||||
case 0 -> null;
|
||||
case 1 -> values.get(0);
|
||||
case 2 -> getString(R.string.selection_2_options, values.get(0), values.get(1));
|
||||
case 3 -> getString(R.string.selection_3_options, values.get(0), values.get(1), values.get(2));
|
||||
default -> getString(R.string.selection_4_or_more, values.get(0), values.get(1), values.size()-2);
|
||||
};
|
||||
rebindItem(contextItem);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onCreateOptionsMenu(Menu menu, MenuInflater inflater){
|
||||
inflater.inflate(R.menu.settings_edit_filter, menu);
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean onOptionsItemSelected(MenuItem item){
|
||||
if(item.getItemId()==R.id.save){
|
||||
saveFilter();
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
private void saveFilter(){
|
||||
if(titleEdit.length()==0){
|
||||
titleEditLayout.setErrorState(getString(R.string.required_form_field_blank));
|
||||
return;
|
||||
}
|
||||
MastodonAPIRequest<Filter> req;
|
||||
if(filter==null){
|
||||
req=new CreateFilter(titleEdit.getText().toString(), context, cwItem.checked ? FilterAction.WARN : FilterAction.HIDE, endsAt==null ? 0 : (int)(endsAt.getEpochSecond()-Instant.now().getEpochSecond()), keywords);
|
||||
}else{
|
||||
req=new UpdateFilter(filter.id, titleEdit.getText().toString(), context, cwItem.checked ? FilterAction.WARN : FilterAction.HIDE, endsAt==null ? 0 : (int)(endsAt.getEpochSecond()-Instant.now().getEpochSecond()), keywords, deletedWordIDs);
|
||||
}
|
||||
req.setCallback(new Callback<>(){
|
||||
@Override
|
||||
public void onSuccess(Filter result){
|
||||
E.post(new SettingsFilterCreatedOrUpdatedEvent(accountID, result));
|
||||
Nav.finish(EditFilterFragment.this);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onError(ErrorResponse error){
|
||||
error.showToast(getActivity());
|
||||
}
|
||||
})
|
||||
.wrapProgress(getActivity(), R.string.saving, true)
|
||||
.exec(accountID);
|
||||
}
|
||||
|
||||
private void deleteFilter(){
|
||||
new DeleteFilter(filter.id)
|
||||
.setCallback(new Callback<>(){
|
||||
@Override
|
||||
public void onSuccess(Void result){
|
||||
E.post(new SettingsFilterDeletedEvent(accountID, filter.id));
|
||||
Nav.finish(EditFilterFragment.this);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onError(ErrorResponse error){
|
||||
error.showToast(getActivity());
|
||||
}
|
||||
})
|
||||
.wrapProgress(getActivity(), R.string.deleting, false)
|
||||
.exec(accountID);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onFragmentResult(int reqCode, boolean success, Bundle result){
|
||||
if(success){
|
||||
if(reqCode==CONTEXT_RESULT){
|
||||
EnumSet<FilterContext> context=(EnumSet<FilterContext>) result.getSerializable("context");
|
||||
if(!context.equals(this.context)){
|
||||
this.context=context;
|
||||
dirty=true;
|
||||
updateContextItem();
|
||||
}
|
||||
}else if(reqCode==WORDS_RESULT){
|
||||
ArrayList<FilterKeyword> old=new ArrayList<>(keywords);
|
||||
keywords.clear();
|
||||
result.getParcelableArrayList("words").stream().map(p->(FilterKeyword)Parcels.unwrap(p)).forEach(keywords::add);
|
||||
if(!old.equals(keywords)){
|
||||
dirty=true;
|
||||
updateWordsItem();
|
||||
}
|
||||
deletedWordIDs.addAll(result.getStringArrayList("deleted"));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private boolean isDirty(){
|
||||
return dirty || (filter!=null && !titleEdit.getText().toString().equals(filter.title));
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean onBackPressed(){
|
||||
if(isDirty()){
|
||||
UiUtils.showConfirmationAlert(getActivity(), R.string.discard_changes, 0, R.string.discard, ()->Nav.finish(this));
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,48 @@
|
||||
package org.joinmastodon.android.fragments.settings;
|
||||
|
||||
import android.os.Bundle;
|
||||
|
||||
import org.joinmastodon.android.R;
|
||||
import org.joinmastodon.android.model.FilterContext;
|
||||
import org.joinmastodon.android.model.viewmodel.CheckableListItem;
|
||||
import org.joinmastodon.android.model.viewmodel.ListItem;
|
||||
|
||||
import java.util.Arrays;
|
||||
import java.util.EnumSet;
|
||||
import java.util.stream.Collectors;
|
||||
|
||||
import me.grishka.appkit.fragments.OnBackPressedListener;
|
||||
|
||||
public class FilterContextFragment extends BaseSettingsFragment<FilterContext> implements OnBackPressedListener{
|
||||
private EnumSet<FilterContext> context;
|
||||
|
||||
@Override
|
||||
public void onCreate(Bundle savedInstanceState){
|
||||
super.onCreate(savedInstanceState);
|
||||
setTitle(R.string.settings_filter_context);
|
||||
context=(EnumSet<FilterContext>) getArguments().getSerializable("context");
|
||||
onDataLoaded(Arrays.stream(FilterContext.values()).map(c->{
|
||||
CheckableListItem<FilterContext> item=new CheckableListItem<>(c.getDisplayNameRes(), 0, CheckableListItem.Style.CHECKBOX, context.contains(c), null);
|
||||
item.parentObject=c;
|
||||
item.isEnabled=true;
|
||||
item.onClick=()->toggleCheckableItem(item);
|
||||
return item;
|
||||
}).collect(Collectors.toList()));
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void doLoadData(int offset, int count){}
|
||||
|
||||
@Override
|
||||
public boolean onBackPressed(){
|
||||
context=EnumSet.noneOf(FilterContext.class);
|
||||
for(ListItem<FilterContext> item:data){
|
||||
if(((CheckableListItem<FilterContext>) item).checked)
|
||||
context.add(item.parentObject);
|
||||
}
|
||||
Bundle args=new Bundle();
|
||||
args.putSerializable("context", context);
|
||||
setResult(true, args);
|
||||
return false;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,327 @@
|
||||
package org.joinmastodon.android.fragments.settings;
|
||||
|
||||
import android.animation.Animator;
|
||||
import android.animation.AnimatorListenerAdapter;
|
||||
import android.animation.IntEvaluator;
|
||||
import android.animation.ObjectAnimator;
|
||||
import android.app.AlertDialog;
|
||||
import android.graphics.drawable.Drawable;
|
||||
import android.os.Build;
|
||||
import android.os.Bundle;
|
||||
import android.os.Parcelable;
|
||||
import android.text.InputType;
|
||||
import android.view.ActionMode;
|
||||
import android.view.LayoutInflater;
|
||||
import android.view.Menu;
|
||||
import android.view.MenuInflater;
|
||||
import android.view.MenuItem;
|
||||
import android.view.View;
|
||||
import android.view.ViewGroup;
|
||||
import android.view.WindowInsets;
|
||||
import android.widget.Button;
|
||||
import android.widget.EditText;
|
||||
import android.widget.ImageButton;
|
||||
|
||||
import org.joinmastodon.android.R;
|
||||
import org.joinmastodon.android.model.FilterKeyword;
|
||||
import org.joinmastodon.android.model.viewmodel.CheckableListItem;
|
||||
import org.joinmastodon.android.model.viewmodel.ListItem;
|
||||
import org.joinmastodon.android.ui.M3AlertDialogBuilder;
|
||||
import org.joinmastodon.android.ui.utils.SimpleTextWatcher;
|
||||
import org.joinmastodon.android.ui.utils.UiUtils;
|
||||
import org.joinmastodon.android.ui.views.FloatingHintEditTextLayout;
|
||||
import org.parceler.Parcels;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.Collections;
|
||||
import java.util.List;
|
||||
import java.util.stream.Collectors;
|
||||
|
||||
import me.grishka.appkit.FragmentStackActivity;
|
||||
import me.grishka.appkit.fragments.OnBackPressedListener;
|
||||
import me.grishka.appkit.utils.V;
|
||||
|
||||
public class FilterWordsFragment extends BaseSettingsFragment<FilterKeyword> implements OnBackPressedListener{
|
||||
private ImageButton fab;
|
||||
private ActionMode actionMode;
|
||||
private ArrayList<ListItem<FilterKeyword>> selectedItems=new ArrayList<>();
|
||||
private ArrayList<String> deletedItemIDs=new ArrayList<>();
|
||||
private MenuItem deleteItem;
|
||||
|
||||
public FilterWordsFragment(){
|
||||
setListLayoutId(R.layout.recycler_fragment_with_fab);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onCreate(Bundle savedInstanceState){
|
||||
super.onCreate(savedInstanceState);
|
||||
setTitle(R.string.settings_filter_muted_words);
|
||||
onDataLoaded(getArguments().getParcelableArrayList("words").stream().map(p->{
|
||||
FilterKeyword word=Parcels.unwrap(p);
|
||||
ListItem<FilterKeyword> item=new ListItem<>(word.keyword, null, null, word);
|
||||
item.isEnabled=true;
|
||||
item.onClick=()->onWordClick(item);
|
||||
return item;
|
||||
}).collect(Collectors.toList()));
|
||||
setHasOptionsMenu(true);
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void doLoadData(int offset, int count){}
|
||||
|
||||
private void onWordClick(ListItem<FilterKeyword> item){
|
||||
showAlertForWord(item.parentObject);
|
||||
}
|
||||
|
||||
private void onSelectionModeWordClick(CheckableListItem<FilterKeyword> item){
|
||||
if(selectedItems.remove(item)){
|
||||
item.checked=false;
|
||||
}else{
|
||||
item.checked=true;
|
||||
selectedItems.add(item);
|
||||
}
|
||||
rebindItem(item);
|
||||
updateActionModeTitle();
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean onBackPressed(){
|
||||
Bundle result=new Bundle();
|
||||
result.putParcelableArrayList("words", (ArrayList<? extends Parcelable>) data.stream().map(i->i.parentObject).map(Parcels::wrap).collect(Collectors.toCollection(ArrayList::new)));
|
||||
result.putStringArrayList("deleted", deletedItemIDs);
|
||||
setResult(true, result);
|
||||
return false;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onViewCreated(View view, Bundle savedInstanceState){
|
||||
super.onViewCreated(view, savedInstanceState);
|
||||
fab=view.findViewById(R.id.fab);
|
||||
fab.setImageResource(R.drawable.ic_add_24px);
|
||||
fab.setContentDescription(getString(R.string.add_muted_word));
|
||||
fab.setOnClickListener(v->onFabClick());
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onApplyWindowInsets(WindowInsets insets){
|
||||
int fabInset=0;
|
||||
if(Build.VERSION.SDK_INT>=29 && insets.getTappableElementInsets().bottom==0){
|
||||
fabInset=insets.getSystemWindowInsetBottom();
|
||||
}
|
||||
((ViewGroup.MarginLayoutParams) fab.getLayoutParams()).bottomMargin=V.dp(16)+fabInset;
|
||||
super.onApplyWindowInsets(insets);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onCreateOptionsMenu(Menu menu, MenuInflater inflater){
|
||||
inflater.inflate(R.menu.settings_filter_words, menu);
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean onOptionsItemSelected(MenuItem item){
|
||||
enterSelectionMode(item.getItemId()==R.id.select_all);
|
||||
return true;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean wantsLightStatusBar(){
|
||||
if(actionMode!=null)
|
||||
return UiUtils.isDarkTheme();
|
||||
return super.wantsLightStatusBar();
|
||||
}
|
||||
|
||||
private void onFabClick(){
|
||||
showAlertForWord(null);
|
||||
}
|
||||
|
||||
private void showAlertForWord(FilterKeyword word){
|
||||
AlertDialog.Builder bldr=new M3AlertDialogBuilder(getActivity())
|
||||
.setHelpText(R.string.filter_add_word_help)
|
||||
.setTitle(word==null ? R.string.add_muted_word : R.string.edit_muted_word)
|
||||
.setNegativeButton(R.string.cancel, null);
|
||||
|
||||
FloatingHintEditTextLayout editWrap=(FloatingHintEditTextLayout) bldr.getContext().getSystemService(LayoutInflater.class).inflate(R.layout.floating_hint_edit_text, null);
|
||||
EditText edit=editWrap.findViewById(R.id.edit);
|
||||
edit.setHint(R.string.filter_word_or_phrase);
|
||||
edit.setInputType(InputType.TYPE_TEXT_VARIATION_FILTER);
|
||||
editWrap.updateHint();
|
||||
bldr.setView(editWrap)
|
||||
.setPositiveButton(word==null ? R.string.add : R.string.save, null);
|
||||
|
||||
if(word!=null){
|
||||
edit.setText(word.keyword);
|
||||
bldr.setNeutralButton(R.string.delete, null);
|
||||
}
|
||||
AlertDialog alert=bldr.show();
|
||||
if(word!=null){
|
||||
Button deleteBtn=alert.getButton(AlertDialog.BUTTON_NEUTRAL);
|
||||
deleteBtn.setTextColor(UiUtils.getThemeColor(getActivity(), R.attr.colorM3Error));
|
||||
deleteBtn.setOnClickListener(v->confirmDeleteWords(Collections.singletonList(word), alert::dismiss));
|
||||
}
|
||||
Button saveBtn=alert.getButton(AlertDialog.BUTTON_POSITIVE);
|
||||
saveBtn.setEnabled(false);
|
||||
saveBtn.setOnClickListener(v->{
|
||||
String input=edit.getText().toString();
|
||||
for(ListItem<FilterKeyword> item:data){
|
||||
if(item.parentObject.keyword.equalsIgnoreCase(input)){
|
||||
editWrap.setErrorState(getString(R.string.filter_word_already_in_list));
|
||||
return;
|
||||
}
|
||||
}
|
||||
if(word==null){
|
||||
FilterKeyword w=new FilterKeyword();
|
||||
w.wholeWord=true;
|
||||
w.keyword=input;
|
||||
ListItem<FilterKeyword> item=new ListItem<>(w.keyword, null, null, w);
|
||||
item.isEnabled=true;
|
||||
item.onClick=()->onWordClick(item);
|
||||
data.add(item);
|
||||
itemsAdapter.notifyItemInserted(data.size()-1);
|
||||
}else{
|
||||
word.keyword=input;
|
||||
word.wholeWord=true;
|
||||
for(ListItem<FilterKeyword> item:data){
|
||||
if(item.parentObject==word){
|
||||
rebindItem(item);
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
alert.dismiss();
|
||||
});
|
||||
edit.addTextChangedListener(new SimpleTextWatcher(e->saveBtn.setEnabled(e.length()>0)));
|
||||
}
|
||||
|
||||
private void confirmDeleteWords(List<FilterKeyword> words, Runnable onConfirmed){
|
||||
AlertDialog alert=new M3AlertDialogBuilder(getActivity())
|
||||
.setTitle(words.size()==1 ? getString(R.string.settings_delete_filter_word, words.get(0).keyword) : getResources().getQuantityString(R.plurals.settings_delete_x_filter_words, words.size(), words.size()))
|
||||
// .setMessage(R.string.settings_delete_filter_confirmation)
|
||||
.setPositiveButton(R.string.delete, (dlg, item)->{
|
||||
if(onConfirmed!=null)
|
||||
onConfirmed.run();
|
||||
removeWords(words);
|
||||
})
|
||||
.setNegativeButton(R.string.cancel, null)
|
||||
.show();
|
||||
alert.getButton(AlertDialog.BUTTON_POSITIVE).setTextColor(UiUtils.getThemeColor(getActivity(), R.attr.colorM3Error));
|
||||
}
|
||||
|
||||
private void removeWords(List<FilterKeyword> words){
|
||||
ArrayList<Integer> indexes=new ArrayList<>();
|
||||
for(int i=0;i<data.size();i++){
|
||||
if(words.contains(data.get(i).parentObject)){
|
||||
indexes.add(0, i);
|
||||
}
|
||||
}
|
||||
for(int index:indexes){
|
||||
data.remove(index);
|
||||
itemsAdapter.notifyItemRemoved(index);
|
||||
}
|
||||
for(FilterKeyword w:words){
|
||||
if(w.id!=null)
|
||||
deletedItemIDs.add(w.id);
|
||||
}
|
||||
}
|
||||
|
||||
private void enterSelectionMode(boolean selectAll){
|
||||
if(actionMode!=null)
|
||||
return;
|
||||
V.setVisibilityAnimated(fab, View.GONE);
|
||||
|
||||
actionMode=getActivity().startActionMode(new ActionMode.Callback(){
|
||||
@Override
|
||||
public boolean onCreateActionMode(ActionMode mode, Menu menu){
|
||||
ObjectAnimator anim=ObjectAnimator.ofInt(getActivity().getWindow(), "statusBarColor", elevationOnScrollListener.getCurrentStatusBarColor(), UiUtils.getThemeColor(getActivity(), R.attr.colorM3Primary));
|
||||
anim.setEvaluator(new IntEvaluator(){
|
||||
@Override
|
||||
public Integer evaluate(float fraction, Integer startValue, Integer endValue){
|
||||
return UiUtils.alphaBlendColors(startValue, endValue, fraction);
|
||||
}
|
||||
});
|
||||
anim.start();
|
||||
((FragmentStackActivity) getActivity()).invalidateSystemBarColors(FilterWordsFragment.this);
|
||||
return true;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean onPrepareActionMode(ActionMode mode, Menu menu){
|
||||
mode.getMenuInflater().inflate(R.menu.settings_filter_words_action_mode, menu);
|
||||
for(int i=0;i<menu.size();i++){
|
||||
Drawable icon=menu.getItem(i).getIcon().mutate();
|
||||
icon.setTint(UiUtils.getThemeColor(getActivity(), R.attr.colorM3OnPrimary));
|
||||
menu.getItem(i).setIcon(icon);
|
||||
}
|
||||
deleteItem=menu.findItem(R.id.delete);
|
||||
return true;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean onActionItemClicked(ActionMode mode, MenuItem item){
|
||||
if(item.getItemId()==R.id.delete){
|
||||
confirmDeleteWords(selectedItems.stream().map(i->i.parentObject).collect(Collectors.toList()), ()->leaveSelectionMode(false));
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onDestroyActionMode(ActionMode mode){
|
||||
leaveSelectionMode(true);
|
||||
ObjectAnimator anim=ObjectAnimator.ofInt(getActivity().getWindow(), "statusBarColor", UiUtils.getThemeColor(getActivity(), R.attr.colorM3Primary), elevationOnScrollListener.getCurrentStatusBarColor());
|
||||
anim.setEvaluator(new IntEvaluator(){
|
||||
@Override
|
||||
public Integer evaluate(float fraction, Integer startValue, Integer endValue){
|
||||
return UiUtils.alphaBlendColors(startValue, endValue, fraction);
|
||||
}
|
||||
});
|
||||
anim.addListener(new AnimatorListenerAdapter(){
|
||||
@Override
|
||||
public void onAnimationEnd(Animator animation){
|
||||
getActivity().getWindow().setStatusBarColor(0);
|
||||
}
|
||||
});
|
||||
anim.start();
|
||||
((FragmentStackActivity) getActivity()).invalidateSystemBarColors(FilterWordsFragment.this);
|
||||
}
|
||||
});
|
||||
|
||||
selectedItems.clear();
|
||||
for(int i=0;i<data.size();i++){
|
||||
ListItem<FilterKeyword> item=data.get(i);
|
||||
CheckableListItem<FilterKeyword> newItem=new CheckableListItem<>(item.title, null, CheckableListItem.Style.CHECKBOX, selectAll, null);
|
||||
newItem.isEnabled=true;
|
||||
newItem.onClick=()->onSelectionModeWordClick(newItem);
|
||||
newItem.parentObject=item.parentObject;
|
||||
if(selectAll)
|
||||
selectedItems.add(newItem);
|
||||
data.set(i, newItem);
|
||||
}
|
||||
itemsAdapter.notifyItemRangeChanged(0, data.size());
|
||||
updateActionModeTitle();
|
||||
}
|
||||
|
||||
private void leaveSelectionMode(boolean fromActionMode){
|
||||
if(actionMode==null)
|
||||
return;
|
||||
ActionMode actionMode=this.actionMode;
|
||||
this.actionMode=null;
|
||||
if(!fromActionMode)
|
||||
actionMode.finish();
|
||||
V.setVisibilityAnimated(fab, View.VISIBLE);
|
||||
selectedItems.clear();
|
||||
|
||||
for(int i=0;i<data.size();i++){
|
||||
ListItem<FilterKeyword> item=data.get(i);
|
||||
ListItem<FilterKeyword> newItem=new ListItem<>(item.title, null, null);
|
||||
newItem.isEnabled=true;
|
||||
newItem.onClick=()->onWordClick(newItem);
|
||||
newItem.parentObject=item.parentObject;
|
||||
data.set(i, newItem);
|
||||
}
|
||||
itemsAdapter.notifyItemRangeChanged(0, data.size());
|
||||
}
|
||||
|
||||
private void updateActionModeTitle(){
|
||||
actionMode.setTitle(getResources().getQuantityString(R.plurals.x_items_selected, selectedItems.size(), selectedItems.size()));
|
||||
deleteItem.setEnabled(!selectedItems.isEmpty());
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,82 @@
|
||||
package org.joinmastodon.android.fragments.settings;
|
||||
|
||||
import android.app.Activity;
|
||||
import android.os.Bundle;
|
||||
import android.view.Gravity;
|
||||
import android.view.ViewGroup;
|
||||
import android.widget.TextView;
|
||||
import android.widget.Toast;
|
||||
|
||||
import org.joinmastodon.android.BuildConfig;
|
||||
import org.joinmastodon.android.R;
|
||||
import org.joinmastodon.android.api.MastodonAPIController;
|
||||
import org.joinmastodon.android.api.session.AccountSession;
|
||||
import org.joinmastodon.android.api.session.AccountSessionManager;
|
||||
import org.joinmastodon.android.model.viewmodel.ListItem;
|
||||
import org.joinmastodon.android.ui.utils.UiUtils;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
import androidx.recyclerview.widget.RecyclerView;
|
||||
import me.grishka.appkit.imageloader.ImageCache;
|
||||
import me.grishka.appkit.utils.MergeRecyclerAdapter;
|
||||
import me.grishka.appkit.utils.SingleViewRecyclerAdapter;
|
||||
import me.grishka.appkit.utils.V;
|
||||
|
||||
public class SettingsAboutAppFragment extends BaseSettingsFragment<Void>{
|
||||
private ListItem<Void> mediaCacheItem;
|
||||
|
||||
@Override
|
||||
public void onCreate(Bundle savedInstanceState){
|
||||
super.onCreate(savedInstanceState);
|
||||
setTitle(getString(R.string.about_app, getString(R.string.app_name)));
|
||||
AccountSession s=AccountSessionManager.get(accountID);
|
||||
onDataLoaded(List.of(
|
||||
new ListItem<>(R.string.settings_even_more, 0, ()->UiUtils.launchWebBrowser(getActivity(), "https://"+s.domain+"/auth/edit")),
|
||||
new ListItem<>(R.string.settings_contribute, 0, ()->UiUtils.launchWebBrowser(getActivity(), getString(R.string.github_url))),
|
||||
new ListItem<>(R.string.settings_tos, 0, ()->UiUtils.launchWebBrowser(getActivity(), "https://"+s.domain+"/terms")),
|
||||
new ListItem<>(R.string.settings_privacy_policy, 0, ()->UiUtils.launchWebBrowser(getActivity(), getString(R.string.privacy_policy_url)), 0, true),
|
||||
mediaCacheItem=new ListItem<>(R.string.settings_clear_cache, 0, this::onClearMediaCacheClick)
|
||||
));
|
||||
|
||||
updateMediaCacheItem();
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void doLoadData(int offset, int count){}
|
||||
|
||||
@Override
|
||||
protected RecyclerView.Adapter<?> getAdapter(){
|
||||
MergeRecyclerAdapter adapter=new MergeRecyclerAdapter();
|
||||
adapter.addAdapter(super.getAdapter());
|
||||
|
||||
TextView versionInfo=new TextView(getActivity());
|
||||
versionInfo.setSingleLine();
|
||||
versionInfo.setLayoutParams(new ViewGroup.LayoutParams(ViewGroup.LayoutParams.MATCH_PARENT, V.dp(32)));
|
||||
versionInfo.setTextAppearance(R.style.m3_label_medium);
|
||||
versionInfo.setTextColor(UiUtils.getThemeColor(getActivity(), R.attr.colorM3Outline));
|
||||
versionInfo.setGravity(Gravity.CENTER);
|
||||
versionInfo.setText(getString(R.string.settings_app_version, BuildConfig.VERSION_NAME, BuildConfig.VERSION_CODE));
|
||||
adapter.addAdapter(new SingleViewRecyclerAdapter(versionInfo));
|
||||
|
||||
return adapter;
|
||||
}
|
||||
|
||||
private void onClearMediaCacheClick(){
|
||||
MastodonAPIController.runInBackground(()->{
|
||||
Activity activity=getActivity();
|
||||
ImageCache.getInstance(getActivity()).clear();
|
||||
activity.runOnUiThread(()->{
|
||||
Toast.makeText(activity, R.string.media_cache_cleared, Toast.LENGTH_SHORT).show();
|
||||
updateMediaCacheItem();
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
private void updateMediaCacheItem(){
|
||||
long size=ImageCache.getInstance(getActivity()).getDiskCache().size();
|
||||
mediaCacheItem.subtitle=UiUtils.formatFileSize(getActivity(), size, false);
|
||||
mediaCacheItem.isEnabled=size>0;
|
||||
rebindItem(mediaCacheItem);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,83 @@
|
||||
package org.joinmastodon.android.fragments.settings;
|
||||
|
||||
import android.os.Bundle;
|
||||
|
||||
import org.joinmastodon.android.GlobalUserPreferences;
|
||||
import org.joinmastodon.android.R;
|
||||
import org.joinmastodon.android.api.session.AccountSession;
|
||||
import org.joinmastodon.android.api.session.AccountSessionManager;
|
||||
import org.joinmastodon.android.model.Preferences;
|
||||
import org.joinmastodon.android.model.viewmodel.CheckableListItem;
|
||||
import org.joinmastodon.android.model.viewmodel.ListItem;
|
||||
import org.joinmastodon.android.ui.M3AlertDialogBuilder;
|
||||
import org.joinmastodon.android.ui.viewcontrollers.ComposeLanguageAlertViewController;
|
||||
|
||||
import java.util.List;
|
||||
import java.util.Locale;
|
||||
|
||||
public class SettingsBehaviorFragment extends BaseSettingsFragment<Void>{
|
||||
private ListItem<Void> languageItem;
|
||||
private CheckableListItem<Void> altTextItem, playGifsItem, customTabsItem, confirmUnfollowItem, confirmBoostItem, confirmDeleteItem;
|
||||
private Locale postLanguage;
|
||||
private ComposeLanguageAlertViewController.SelectedOption newPostLanguage;
|
||||
|
||||
@Override
|
||||
public void onCreate(Bundle savedInstanceState){
|
||||
super.onCreate(savedInstanceState);
|
||||
setTitle(R.string.settings_behavior);
|
||||
|
||||
AccountSession s=AccountSessionManager.get(accountID);
|
||||
if(s.preferences!=null && s.preferences.postingDefaultLanguage!=null){
|
||||
postLanguage=Locale.forLanguageTag(s.preferences.postingDefaultLanguage);
|
||||
}
|
||||
|
||||
onDataLoaded(List.of(
|
||||
languageItem=new ListItem<>(getString(R.string.default_post_language), postLanguage!=null ? postLanguage.getDisplayName(Locale.getDefault()) : null, R.drawable.ic_language_24px, this::onDefaultLanguageClick),
|
||||
altTextItem=new CheckableListItem<>(R.string.settings_alt_text_reminders, 0, CheckableListItem.Style.SWITCH, GlobalUserPreferences.altTextReminders, R.drawable.ic_alt_24px, ()->toggleCheckableItem(altTextItem)),
|
||||
playGifsItem=new CheckableListItem<>(R.string.settings_gif, 0, CheckableListItem.Style.SWITCH, GlobalUserPreferences.playGifs, R.drawable.ic_animation_24px, ()->toggleCheckableItem(playGifsItem)),
|
||||
customTabsItem=new CheckableListItem<>(R.string.settings_custom_tabs, 0, CheckableListItem.Style.SWITCH, GlobalUserPreferences.useCustomTabs, R.drawable.ic_open_in_browser_24px, ()->toggleCheckableItem(customTabsItem)),
|
||||
confirmUnfollowItem=new CheckableListItem<>(R.string.settings_confirm_unfollow, 0, CheckableListItem.Style.SWITCH, GlobalUserPreferences.confirmUnfollow, R.drawable.ic_person_remove_24px, ()->toggleCheckableItem(confirmUnfollowItem)),
|
||||
confirmBoostItem=new CheckableListItem<>(R.string.settings_confirm_boost, 0, CheckableListItem.Style.SWITCH, GlobalUserPreferences.confirmBoost, R.drawable.ic_repeat_24px, ()->toggleCheckableItem(confirmBoostItem)),
|
||||
confirmDeleteItem=new CheckableListItem<>(R.string.settings_confirm_delete_post, 0, CheckableListItem.Style.SWITCH, GlobalUserPreferences.confirmDeletePost, R.drawable.ic_delete_24px, ()->toggleCheckableItem(confirmDeleteItem))
|
||||
));
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void doLoadData(int offset, int count){}
|
||||
|
||||
private void onDefaultLanguageClick(){
|
||||
ComposeLanguageAlertViewController vc=new ComposeLanguageAlertViewController(getActivity(), null, new ComposeLanguageAlertViewController.SelectedOption(-1, postLanguage), null);
|
||||
new M3AlertDialogBuilder(getActivity())
|
||||
.setTitle(R.string.default_post_language)
|
||||
.setView(vc.getView())
|
||||
.setPositiveButton(R.string.ok, (dlg, which)->{
|
||||
ComposeLanguageAlertViewController.SelectedOption opt=vc.getSelectedOption();
|
||||
if(!opt.locale.equals(postLanguage)){
|
||||
newPostLanguage=opt;
|
||||
languageItem.subtitle=newPostLanguage.locale.getDisplayLanguage(Locale.getDefault());
|
||||
rebindItem(languageItem);
|
||||
}
|
||||
})
|
||||
.setNegativeButton(R.string.cancel, null)
|
||||
.show();
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void onHidden(){
|
||||
super.onHidden();
|
||||
GlobalUserPreferences.playGifs=playGifsItem.checked;
|
||||
GlobalUserPreferences.useCustomTabs=customTabsItem.checked;
|
||||
GlobalUserPreferences.altTextReminders=altTextItem.checked;
|
||||
GlobalUserPreferences.confirmUnfollow=customTabsItem.checked;
|
||||
GlobalUserPreferences.confirmBoost=confirmBoostItem.checked;
|
||||
GlobalUserPreferences.confirmDeletePost=confirmDeleteItem.checked;
|
||||
GlobalUserPreferences.save();
|
||||
if(newPostLanguage!=null){
|
||||
AccountSession s=AccountSessionManager.get(accountID);
|
||||
if(s.preferences==null)
|
||||
s.preferences=new Preferences();
|
||||
s.preferences.postingDefaultLanguage=newPostLanguage.locale.toLanguageTag();
|
||||
s.savePreferencesLater();
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,63 @@
|
||||
package org.joinmastodon.android.fragments.settings;
|
||||
|
||||
import android.os.Bundle;
|
||||
|
||||
import org.joinmastodon.android.api.session.AccountActivationInfo;
|
||||
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.onboarding.AccountActivationFragment;
|
||||
import org.joinmastodon.android.model.viewmodel.ListItem;
|
||||
import org.joinmastodon.android.updater.GithubSelfUpdater;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
import me.grishka.appkit.Nav;
|
||||
|
||||
public class SettingsDebugFragment extends BaseSettingsFragment<Void>{
|
||||
@Override
|
||||
public void onCreate(Bundle savedInstanceState){
|
||||
super.onCreate(savedInstanceState);
|
||||
setTitle("Debug settings");
|
||||
ListItem<Void> selfUpdateItem, resetUpdateItem;
|
||||
onDataLoaded(List.of(
|
||||
new ListItem<>("Test email confirmation flow", null, this::onTestEmailConfirmClick),
|
||||
selfUpdateItem=new ListItem<>("Force self-update", null, this::onForceSelfUpdateClick),
|
||||
resetUpdateItem=new ListItem<>("Reset self-updater", null, this::onResetUpdaterClick)
|
||||
));
|
||||
if(!GithubSelfUpdater.needSelfUpdating()){
|
||||
resetUpdateItem.isEnabled=selfUpdateItem.isEnabled=false;
|
||||
selfUpdateItem.subtitle="Self-updater is unavailable in this build flavor";
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void doLoadData(int offset, int count){}
|
||||
|
||||
private void onTestEmailConfirmClick(){
|
||||
AccountSession sess=AccountSessionManager.getInstance().getAccount(accountID);
|
||||
sess.activated=false;
|
||||
sess.activationInfo=new AccountActivationInfo("test@email", System.currentTimeMillis());
|
||||
Bundle args=new Bundle();
|
||||
args.putString("account", accountID);
|
||||
args.putBoolean("debug", true);
|
||||
Nav.goClearingStack(getActivity(), AccountActivationFragment.class, args);
|
||||
}
|
||||
|
||||
private void onForceSelfUpdateClick(){
|
||||
GithubSelfUpdater.forceUpdate=true;
|
||||
GithubSelfUpdater.getInstance().maybeCheckForUpdates();
|
||||
restartUI();
|
||||
}
|
||||
|
||||
private void onResetUpdaterClick(){
|
||||
GithubSelfUpdater.getInstance().reset();
|
||||
restartUI();
|
||||
}
|
||||
|
||||
private void restartUI(){
|
||||
Bundle args=new Bundle();
|
||||
args.putString("account", accountID);
|
||||
Nav.goClearingStack(getActivity(), HomeFragment.class, args);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,152 @@
|
||||
package org.joinmastodon.android.fragments.settings;
|
||||
|
||||
import android.app.Activity;
|
||||
import android.graphics.Bitmap;
|
||||
import android.graphics.Canvas;
|
||||
import android.os.Build;
|
||||
import android.os.Bundle;
|
||||
import android.view.View;
|
||||
import android.view.WindowManager;
|
||||
import android.widget.ImageView;
|
||||
|
||||
import org.joinmastodon.android.E;
|
||||
import org.joinmastodon.android.GlobalUserPreferences;
|
||||
import org.joinmastodon.android.MastodonApp;
|
||||
import org.joinmastodon.android.R;
|
||||
import org.joinmastodon.android.api.session.AccountLocalPreferences;
|
||||
import org.joinmastodon.android.api.session.AccountSession;
|
||||
import org.joinmastodon.android.api.session.AccountSessionManager;
|
||||
import org.joinmastodon.android.events.StatusDisplaySettingsChangedEvent;
|
||||
import org.joinmastodon.android.model.viewmodel.CheckableListItem;
|
||||
import org.joinmastodon.android.model.viewmodel.ListItem;
|
||||
import org.joinmastodon.android.ui.M3AlertDialogBuilder;
|
||||
|
||||
import java.util.List;
|
||||
import java.util.stream.IntStream;
|
||||
|
||||
import me.grishka.appkit.FragmentStackActivity;
|
||||
|
||||
public class SettingsDisplayFragment extends BaseSettingsFragment<Void>{
|
||||
private ImageView themeTransitionWindowView;
|
||||
private ListItem<Void> themeItem;
|
||||
private CheckableListItem<Void> showCWsItem, hideSensitiveMediaItem, interactionCountsItem, emojiInNamesItem;
|
||||
|
||||
@Override
|
||||
public void onCreate(Bundle savedInstanceState){
|
||||
super.onCreate(savedInstanceState);
|
||||
setTitle(R.string.settings_display);
|
||||
AccountSession s=AccountSessionManager.get(accountID);
|
||||
AccountLocalPreferences lp=s.getLocalPreferences();
|
||||
onDataLoaded(List.of(
|
||||
themeItem=new ListItem<>(R.string.settings_theme, getAppearanceValue(), R.drawable.ic_dark_mode_24px, this::onAppearanceClick),
|
||||
showCWsItem=new CheckableListItem<>(R.string.settings_show_cws, 0, CheckableListItem.Style.SWITCH, lp.showCWs, R.drawable.ic_warning_24px, ()->toggleCheckableItem(showCWsItem)),
|
||||
hideSensitiveMediaItem=new CheckableListItem<>(R.string.settings_hide_sensitive_media, 0, CheckableListItem.Style.SWITCH, lp.hideSensitiveMedia, R.drawable.ic_no_adult_content_24px, ()->toggleCheckableItem(hideSensitiveMediaItem)),
|
||||
interactionCountsItem=new CheckableListItem<>(R.string.settings_show_interaction_counts, 0, CheckableListItem.Style.SWITCH, lp.showInteractionCounts, R.drawable.ic_social_leaderboard_24px, ()->toggleCheckableItem(interactionCountsItem)),
|
||||
emojiInNamesItem=new CheckableListItem<>(R.string.settings_show_emoji_in_names, 0, CheckableListItem.Style.SWITCH, lp.customEmojiInNames, R.drawable.ic_emoticon_24px, ()->toggleCheckableItem(emojiInNamesItem))
|
||||
));
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void doLoadData(int offset, int count){}
|
||||
|
||||
@Override
|
||||
public void onAttach(Activity activity){
|
||||
super.onAttach(activity);
|
||||
if(themeTransitionWindowView!=null){
|
||||
// Activity has finished recreating. Remove the overlay.
|
||||
activity.getSystemService(WindowManager.class).removeView(themeTransitionWindowView);
|
||||
themeTransitionWindowView=null;
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void onHidden(){
|
||||
super.onHidden();
|
||||
AccountSession s=AccountSessionManager.get(accountID);
|
||||
AccountLocalPreferences lp=s.getLocalPreferences();
|
||||
lp.showCWs=showCWsItem.checked;
|
||||
lp.hideSensitiveMedia=hideSensitiveMediaItem.checked;
|
||||
lp.showInteractionCounts=interactionCountsItem.checked;
|
||||
lp.customEmojiInNames=emojiInNamesItem.checked;
|
||||
lp.save();
|
||||
E.post(new StatusDisplaySettingsChangedEvent(accountID));
|
||||
}
|
||||
|
||||
private int getAppearanceValue(){
|
||||
return switch(GlobalUserPreferences.theme){
|
||||
case AUTO -> R.string.theme_auto;
|
||||
case LIGHT -> R.string.theme_light;
|
||||
case DARK -> R.string.theme_dark;
|
||||
};
|
||||
}
|
||||
|
||||
private void onAppearanceClick(){
|
||||
int selected=switch(GlobalUserPreferences.theme){
|
||||
case LIGHT -> 0;
|
||||
case DARK -> 1;
|
||||
case AUTO -> 2;
|
||||
};
|
||||
int[] newSelected={selected};
|
||||
new M3AlertDialogBuilder(getActivity())
|
||||
.setTitle(R.string.settings_theme)
|
||||
.setSingleChoiceItems((String[])IntStream.of(R.string.theme_light, R.string.theme_dark, R.string.theme_auto).mapToObj(this::getString).toArray(String[]::new),
|
||||
selected, (dlg, item)->newSelected[0]=item)
|
||||
.setPositiveButton(R.string.ok, (dlg, item)->{
|
||||
GlobalUserPreferences.ThemePreference pref=switch(newSelected[0]){
|
||||
case 0 -> GlobalUserPreferences.ThemePreference.LIGHT;
|
||||
case 1 -> GlobalUserPreferences.ThemePreference.DARK;
|
||||
case 2 -> GlobalUserPreferences.ThemePreference.AUTO;
|
||||
default -> throw new IllegalStateException("Unexpected value: "+newSelected[0]);
|
||||
};
|
||||
if(pref!=GlobalUserPreferences.theme){
|
||||
GlobalUserPreferences.ThemePreference prev=GlobalUserPreferences.theme;
|
||||
GlobalUserPreferences.theme=pref;
|
||||
GlobalUserPreferences.save();
|
||||
themeItem.subtitleRes=getAppearanceValue();
|
||||
rebindItem(themeItem);
|
||||
maybeApplyNewThemeRightNow(prev);
|
||||
}
|
||||
})
|
||||
.setNegativeButton(R.string.cancel, null)
|
||||
.show();
|
||||
}
|
||||
|
||||
private void maybeApplyNewThemeRightNow(GlobalUserPreferences.ThemePreference prev){
|
||||
boolean isCurrentDark=prev==GlobalUserPreferences.ThemePreference.DARK ||
|
||||
(prev==GlobalUserPreferences.ThemePreference.AUTO && Build.VERSION.SDK_INT>=30 && getResources().getConfiguration().isNightModeActive());
|
||||
boolean isNewDark=GlobalUserPreferences.theme==GlobalUserPreferences.ThemePreference.DARK ||
|
||||
(GlobalUserPreferences.theme==GlobalUserPreferences.ThemePreference.AUTO && Build.VERSION.SDK_INT>=30 && getResources().getConfiguration().isNightModeActive());
|
||||
if(isCurrentDark!=isNewDark){
|
||||
restartActivityToApplyNewTheme();
|
||||
}
|
||||
}
|
||||
|
||||
private void restartActivityToApplyNewTheme(){
|
||||
// Calling activity.recreate() causes a black screen for like half a second.
|
||||
// So, let's take a screenshot and overlay it on top to create the illusion of a smoother transition.
|
||||
// As a bonus, we can fade it out to make it even smoother.
|
||||
if(Build.VERSION.SDK_INT>=Build.VERSION_CODES.N && Build.VERSION.SDK_INT<Build.VERSION_CODES.S){
|
||||
View activityDecorView=getActivity().getWindow().getDecorView();
|
||||
Bitmap bitmap=Bitmap.createBitmap(activityDecorView.getWidth(), activityDecorView.getHeight(), Bitmap.Config.ARGB_8888);
|
||||
activityDecorView.draw(new Canvas(bitmap));
|
||||
themeTransitionWindowView=new ImageView(MastodonApp.context);
|
||||
themeTransitionWindowView.setImageBitmap(bitmap);
|
||||
WindowManager.LayoutParams lp=new WindowManager.LayoutParams(WindowManager.LayoutParams.TYPE_APPLICATION);
|
||||
lp.flags=WindowManager.LayoutParams.FLAG_NOT_FOCUSABLE | WindowManager.LayoutParams.FLAG_NOT_TOUCHABLE |
|
||||
WindowManager.LayoutParams.FLAG_LAYOUT_INSET_DECOR | WindowManager.LayoutParams.FLAG_LAYOUT_IN_SCREEN | WindowManager.LayoutParams.FLAG_DRAWS_SYSTEM_BAR_BACKGROUNDS;
|
||||
lp.systemUiVisibility=View.SYSTEM_UI_FLAG_LAYOUT_FULLSCREEN | View.SYSTEM_UI_FLAG_LAYOUT_HIDE_NAVIGATION | View.SYSTEM_UI_FLAG_LAYOUT_STABLE;
|
||||
lp.systemUiVisibility|=(activityDecorView.getWindowSystemUiVisibility() & (View.SYSTEM_UI_FLAG_LIGHT_NAVIGATION_BAR | View.SYSTEM_UI_FLAG_LIGHT_STATUS_BAR));
|
||||
lp.width=lp.height=WindowManager.LayoutParams.MATCH_PARENT;
|
||||
lp.token=getActivity().getWindow().getAttributes().token;
|
||||
lp.windowAnimations=R.style.window_fade_out;
|
||||
MastodonApp.context.getSystemService(WindowManager.class).addView(themeTransitionWindowView, lp);
|
||||
}
|
||||
getActivity().recreate();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onViewCreated(View view, Bundle savedInstanceState){
|
||||
super.onViewCreated(view, savedInstanceState);
|
||||
((FragmentStackActivity)getActivity()).invalidateSystemBarColors(this);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,112 @@
|
||||
package org.joinmastodon.android.fragments.settings;
|
||||
|
||||
import android.os.Bundle;
|
||||
|
||||
import com.squareup.otto.Subscribe;
|
||||
|
||||
import org.joinmastodon.android.E;
|
||||
import org.joinmastodon.android.R;
|
||||
import org.joinmastodon.android.api.requests.filters.GetFilters;
|
||||
import org.joinmastodon.android.events.SettingsFilterCreatedOrUpdatedEvent;
|
||||
import org.joinmastodon.android.events.SettingsFilterDeletedEvent;
|
||||
import org.joinmastodon.android.model.Filter;
|
||||
import org.joinmastodon.android.model.viewmodel.ListItem;
|
||||
import org.joinmastodon.android.ui.adapters.GenericListItemsAdapter;
|
||||
import org.parceler.Parcels;
|
||||
|
||||
import java.util.Collections;
|
||||
import java.util.List;
|
||||
import java.util.stream.Collectors;
|
||||
|
||||
import androidx.recyclerview.widget.RecyclerView;
|
||||
import me.grishka.appkit.Nav;
|
||||
import me.grishka.appkit.api.SimpleCallback;
|
||||
import me.grishka.appkit.utils.MergeRecyclerAdapter;
|
||||
|
||||
public class SettingsFiltersFragment extends BaseSettingsFragment<Filter>{
|
||||
@Override
|
||||
public void onCreate(Bundle savedInstanceState){
|
||||
super.onCreate(savedInstanceState);
|
||||
setTitle(R.string.settings_filters);
|
||||
loadData();
|
||||
E.register(this);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onDestroy(){
|
||||
super.onDestroy();
|
||||
E.unregister(this);
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void doLoadData(int offset, int count){
|
||||
new GetFilters()
|
||||
.setCallback(new SimpleCallback<>(this){
|
||||
@Override
|
||||
public void onSuccess(List<Filter> result){
|
||||
onDataLoaded(result.stream().map(f->makeListItem(f)).collect(Collectors.toList()));
|
||||
}
|
||||
})
|
||||
.exec(accountID);
|
||||
}
|
||||
|
||||
@Override
|
||||
protected RecyclerView.Adapter<?> getAdapter(){
|
||||
MergeRecyclerAdapter adapter=new MergeRecyclerAdapter();
|
||||
adapter.addAdapter(super.getAdapter());
|
||||
adapter.addAdapter(new GenericListItemsAdapter<>(Collections.singletonList(
|
||||
new ListItem<Void>(R.string.settings_add_filter, 0, R.drawable.ic_add_24px, this::onAddFilterClick)
|
||||
)));
|
||||
return adapter;
|
||||
}
|
||||
|
||||
private void onFilterClick(ListItem<Filter> filter){
|
||||
Bundle args=new Bundle();
|
||||
args.putString("account", accountID);
|
||||
args.putParcelable("filter", Parcels.wrap(filter.parentObject));
|
||||
Nav.go(getActivity(), EditFilterFragment.class, args);
|
||||
}
|
||||
|
||||
private void onAddFilterClick(){
|
||||
Bundle args=new Bundle();
|
||||
args.putString("account", accountID);
|
||||
Nav.go(getActivity(), EditFilterFragment.class, args);
|
||||
}
|
||||
|
||||
private ListItem<Filter> makeListItem(Filter f){
|
||||
ListItem<Filter> item=new ListItem<>(f.title, getString(f.isActive() ? R.string.filter_active : R.string.filter_inactive), null, f);
|
||||
item.onClick=()->onFilterClick(item);
|
||||
item.isEnabled=true;
|
||||
return item;
|
||||
}
|
||||
|
||||
@Subscribe
|
||||
public void onFilterDeleted(SettingsFilterDeletedEvent ev){
|
||||
if(!ev.accountID.equals(accountID))
|
||||
return;
|
||||
for(int i=0;i<data.size();i++){
|
||||
if(data.get(i).parentObject.id.equals(ev.filterID)){
|
||||
data.remove(i);
|
||||
itemsAdapter.notifyItemRemoved(i);
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Subscribe
|
||||
public void onFilterCreatedOrUpdated(SettingsFilterCreatedOrUpdatedEvent ev){
|
||||
if(!ev.accountID.equals(accountID))
|
||||
return;
|
||||
for(ListItem<Filter> item:data){
|
||||
if(item.parentObject.id.equals(ev.filter.id)){
|
||||
item.parentObject=ev.filter;
|
||||
item.title=ev.filter.title;
|
||||
item.subtitle=getString(ev.filter.isActive() ? R.string.filter_active : R.string.filter_inactive);
|
||||
rebindItem(item);
|
||||
return;
|
||||
}
|
||||
}
|
||||
data.add(makeListItem(ev.filter));
|
||||
itemsAdapter.notifyItemInserted(data.size()-1);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,213 @@
|
||||
package org.joinmastodon.android.fragments.settings;
|
||||
|
||||
import android.content.Intent;
|
||||
import android.os.Bundle;
|
||||
import android.view.View;
|
||||
import android.widget.Button;
|
||||
import android.widget.ImageView;
|
||||
import android.widget.TextView;
|
||||
|
||||
import com.squareup.otto.Subscribe;
|
||||
|
||||
import org.joinmastodon.android.BuildConfig;
|
||||
import org.joinmastodon.android.E;
|
||||
import org.joinmastodon.android.MainActivity;
|
||||
import org.joinmastodon.android.R;
|
||||
import org.joinmastodon.android.api.session.AccountSession;
|
||||
import org.joinmastodon.android.api.session.AccountSessionManager;
|
||||
import org.joinmastodon.android.events.SelfUpdateStateChangedEvent;
|
||||
import org.joinmastodon.android.model.viewmodel.ListItem;
|
||||
import org.joinmastodon.android.ui.M3AlertDialogBuilder;
|
||||
import org.joinmastodon.android.ui.utils.HideableSingleViewRecyclerAdapter;
|
||||
import org.joinmastodon.android.ui.utils.UiUtils;
|
||||
import org.joinmastodon.android.updater.GithubSelfUpdater;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
import androidx.recyclerview.widget.RecyclerView;
|
||||
import me.grishka.appkit.Nav;
|
||||
import me.grishka.appkit.utils.MergeRecyclerAdapter;
|
||||
|
||||
public class SettingsMainFragment extends BaseSettingsFragment<Void>{
|
||||
private boolean loggedOut;
|
||||
private HideableSingleViewRecyclerAdapter bannerAdapter;
|
||||
private Button updateButton1, updateButton2;
|
||||
private TextView updateText;
|
||||
private Runnable updateDownloadProgressUpdater=new Runnable(){
|
||||
@Override
|
||||
public void run(){
|
||||
GithubSelfUpdater.UpdateState state=GithubSelfUpdater.getInstance().getState();
|
||||
if(state==GithubSelfUpdater.UpdateState.DOWNLOADING){
|
||||
updateButton1.setText(getString(R.string.downloading_update, Math.round(GithubSelfUpdater.getInstance().getDownloadProgress()*100f)));
|
||||
list.postDelayed(this, 250);
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
@Override
|
||||
public void onCreate(Bundle savedInstanceState){
|
||||
super.onCreate(savedInstanceState);
|
||||
setTitle(R.string.settings);
|
||||
setSubtitle(AccountSessionManager.get(accountID).getFullUsername());
|
||||
onDataLoaded(List.of(
|
||||
new ListItem<>(R.string.settings_behavior, 0, R.drawable.ic_settings_24px, this::onBehaviorClick),
|
||||
new ListItem<>(R.string.settings_display, 0, R.drawable.ic_style_24px, this::onDisplayClick),
|
||||
new ListItem<>(R.string.settings_filters, 0, R.drawable.ic_filter_alt_24px, this::onFiltersClick),
|
||||
new ListItem<>(R.string.settings_notifications, 0, R.drawable.ic_notifications_24px, this::onNotificationsClick),
|
||||
new ListItem<>(R.string.settings_privacy, 0, R.drawable.ic_lock_24px, this::onPrivacyClick, 0, true),
|
||||
new ListItem<>(AccountSessionManager.get(accountID).domain, getString(R.string.settings_server_explanation), R.drawable.ic_dns_24px, this::onServerClick),
|
||||
new ListItem<>(getString(R.string.about_app, getString(R.string.app_name)), null, R.drawable.ic_info_24px, this::onAboutClick, null, 0, true),
|
||||
new ListItem<>(R.string.log_out, 0, R.drawable.ic_logout_24px, this::onLogOutClick, R.attr.colorM3Error, false)
|
||||
));
|
||||
|
||||
if(BuildConfig.DEBUG || BuildConfig.BUILD_TYPE.equals("appcenterPrivateBeta")){
|
||||
data.add(0, new ListItem<>("Debug settings", null, R.drawable.ic_settings_24px, ()->Nav.go(getActivity(), SettingsDebugFragment.class, makeFragmentArgs()), null, 0, true));
|
||||
}
|
||||
|
||||
AccountSessionManager.get(accountID).reloadPreferences(null);
|
||||
E.register(this);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onDestroy(){
|
||||
super.onDestroy();
|
||||
E.unregister(this);
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void doLoadData(int offset, int count){}
|
||||
|
||||
@Override
|
||||
protected void onUpdateToolbar(){
|
||||
super.onUpdateToolbar();
|
||||
UiUtils.setToolbarWithSubtitleAppearance(getToolbar());
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void onHidden(){
|
||||
super.onHidden();
|
||||
if(!loggedOut)
|
||||
AccountSessionManager.get(accountID).savePreferencesIfPending();
|
||||
}
|
||||
|
||||
@Override
|
||||
protected RecyclerView.Adapter<?> getAdapter(){
|
||||
View banner=getActivity().getLayoutInflater().inflate(R.layout.item_settings_banner, list, false);
|
||||
updateText=banner.findViewById(R.id.text);
|
||||
TextView bannerTitle=banner.findViewById(R.id.title);
|
||||
ImageView bannerIcon=banner.findViewById(R.id.icon);
|
||||
updateButton1=banner.findViewById(R.id.button);
|
||||
updateButton2=banner.findViewById(R.id.button2);
|
||||
bannerAdapter=new HideableSingleViewRecyclerAdapter(banner);
|
||||
bannerAdapter.setVisible(false);
|
||||
updateButton1.setOnClickListener(this::onUpdateButtonClick);
|
||||
updateButton2.setOnClickListener(this::onUpdateButtonClick);
|
||||
|
||||
bannerTitle.setText(R.string.app_update_ready);
|
||||
bannerIcon.setImageResource(R.drawable.ic_apk_install_24px);
|
||||
|
||||
MergeRecyclerAdapter adapter=new MergeRecyclerAdapter();
|
||||
adapter.addAdapter(bannerAdapter);
|
||||
adapter.addAdapter(super.getAdapter());
|
||||
return adapter;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onViewCreated(View view, Bundle savedInstanceState){
|
||||
super.onViewCreated(view, savedInstanceState);
|
||||
if(GithubSelfUpdater.needSelfUpdating()){
|
||||
updateUpdateBanner();
|
||||
}
|
||||
}
|
||||
|
||||
private Bundle makeFragmentArgs(){
|
||||
Bundle args=new Bundle();
|
||||
args.putString("account", accountID);
|
||||
return args;
|
||||
}
|
||||
|
||||
private void onBehaviorClick(){
|
||||
Nav.go(getActivity(), SettingsBehaviorFragment.class, makeFragmentArgs());
|
||||
}
|
||||
|
||||
private void onDisplayClick(){
|
||||
Nav.go(getActivity(), SettingsDisplayFragment.class, makeFragmentArgs());
|
||||
}
|
||||
|
||||
private void onFiltersClick(){
|
||||
Nav.go(getActivity(), SettingsFiltersFragment.class, makeFragmentArgs());
|
||||
}
|
||||
|
||||
private void onNotificationsClick(){
|
||||
Nav.go(getActivity(), SettingsNotificationsFragment.class, makeFragmentArgs());
|
||||
}
|
||||
|
||||
private void onPrivacyClick(){
|
||||
|
||||
}
|
||||
|
||||
private void onServerClick(){
|
||||
Nav.go(getActivity(), SettingsServerFragment.class, makeFragmentArgs());
|
||||
}
|
||||
|
||||
private void onAboutClick(){
|
||||
Nav.go(getActivity(), SettingsAboutAppFragment.class, makeFragmentArgs());
|
||||
}
|
||||
|
||||
private void onLogOutClick(){
|
||||
AccountSession session=AccountSessionManager.getInstance().getAccount(accountID);
|
||||
new M3AlertDialogBuilder(getActivity())
|
||||
.setMessage(getString(R.string.confirm_log_out, session.getFullUsername()))
|
||||
.setPositiveButton(R.string.log_out, (dialog, which)->AccountSessionManager.get(accountID).logOut(getActivity(), ()->{
|
||||
loggedOut=true;
|
||||
getActivity().finish();
|
||||
Intent intent=new Intent(getActivity(), MainActivity.class);
|
||||
startActivity(intent);
|
||||
}))
|
||||
.setNegativeButton(R.string.cancel, null)
|
||||
.show();
|
||||
}
|
||||
|
||||
@Subscribe
|
||||
public void onSelfUpdateStateChanged(SelfUpdateStateChangedEvent ev){
|
||||
updateUpdateBanner();
|
||||
}
|
||||
|
||||
private void updateUpdateBanner(){
|
||||
GithubSelfUpdater.UpdateState state=GithubSelfUpdater.getInstance().getState();
|
||||
if(state==GithubSelfUpdater.UpdateState.NO_UPDATE || state==GithubSelfUpdater.UpdateState.CHECKING){
|
||||
bannerAdapter.setVisible(false);
|
||||
}else{
|
||||
bannerAdapter.setVisible(true);
|
||||
updateText.setText(getString(R.string.app_update_version, GithubSelfUpdater.getInstance().getUpdateInfo().version));
|
||||
if(state==GithubSelfUpdater.UpdateState.UPDATE_AVAILABLE){
|
||||
updateButton2.setVisibility(View.GONE);
|
||||
updateButton1.setEnabled(true);
|
||||
updateButton1.setText(getString(R.string.download_update, UiUtils.formatFileSize(getActivity(), GithubSelfUpdater.getInstance().getUpdateInfo().size, true)));
|
||||
}else if(state==GithubSelfUpdater.UpdateState.DOWNLOADING){
|
||||
updateButton2.setVisibility(View.VISIBLE);
|
||||
updateButton2.setText(R.string.cancel);
|
||||
updateButton1.setEnabled(false);
|
||||
list.removeCallbacks(updateDownloadProgressUpdater);
|
||||
updateDownloadProgressUpdater.run();
|
||||
}else if(state==GithubSelfUpdater.UpdateState.DOWNLOADED){
|
||||
updateButton2.setVisibility(View.GONE);
|
||||
updateButton1.setEnabled(true);
|
||||
updateButton1.setText(R.string.install_update);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private void onUpdateButtonClick(View v){
|
||||
if(v.getId()==R.id.button){
|
||||
GithubSelfUpdater.UpdateState state=GithubSelfUpdater.getInstance().getState();
|
||||
if(state==GithubSelfUpdater.UpdateState.UPDATE_AVAILABLE){
|
||||
GithubSelfUpdater.getInstance().downloadUpdate();
|
||||
}else if(state==GithubSelfUpdater.UpdateState.DOWNLOADED){
|
||||
GithubSelfUpdater.getInstance().installUpdate(getActivity());
|
||||
}
|
||||
}else if(v.getId()==R.id.button2){
|
||||
GithubSelfUpdater.getInstance().cancelDownload();
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,286 @@
|
||||
package org.joinmastodon.android.fragments.settings;
|
||||
|
||||
import android.app.AlertDialog;
|
||||
import android.app.NotificationManager;
|
||||
import android.content.Intent;
|
||||
import android.net.Uri;
|
||||
import android.os.Build;
|
||||
import android.os.Bundle;
|
||||
import android.provider.Settings;
|
||||
import android.view.View;
|
||||
import android.widget.Button;
|
||||
import android.widget.ImageView;
|
||||
import android.widget.TextView;
|
||||
|
||||
import org.joinmastodon.android.R;
|
||||
import org.joinmastodon.android.api.PushSubscriptionManager;
|
||||
import org.joinmastodon.android.api.session.AccountSession;
|
||||
import org.joinmastodon.android.api.session.AccountSessionManager;
|
||||
import org.joinmastodon.android.model.PushSubscription;
|
||||
import org.joinmastodon.android.model.viewmodel.CheckableListItem;
|
||||
import org.joinmastodon.android.model.viewmodel.ListItem;
|
||||
import org.joinmastodon.android.ui.M3AlertDialogBuilder;
|
||||
import org.joinmastodon.android.ui.utils.HideableSingleViewRecyclerAdapter;
|
||||
import org.joinmastodon.android.ui.utils.UiUtils;
|
||||
|
||||
import java.time.Instant;
|
||||
import java.util.Arrays;
|
||||
import java.util.List;
|
||||
import java.util.stream.Stream;
|
||||
|
||||
import androidx.recyclerview.widget.RecyclerView;
|
||||
import me.grishka.appkit.utils.MergeRecyclerAdapter;
|
||||
|
||||
public class SettingsNotificationsFragment extends BaseSettingsFragment<Void>{
|
||||
private PushSubscription pushSubscription;
|
||||
private CheckableListItem<Void> pauseItem;
|
||||
private ListItem<Void> policyItem;
|
||||
private MergeRecyclerAdapter mergeAdapter;
|
||||
|
||||
private HideableSingleViewRecyclerAdapter bannerAdapter;
|
||||
private ImageView bannerIcon;
|
||||
private TextView bannerText;
|
||||
private Button bannerButton;
|
||||
|
||||
private CheckableListItem<Void> mentionsItem, boostsItem, favoritesItem, followersItem, pollsItem;
|
||||
private List<CheckableListItem<Void>> typeItems;
|
||||
private boolean needUpdateNotificationSettings;
|
||||
private boolean notificationsAllowed=true;
|
||||
|
||||
@Override
|
||||
public void onCreate(Bundle savedInstanceState){
|
||||
super.onCreate(savedInstanceState);
|
||||
setTitle(R.string.settings_notifications);
|
||||
|
||||
getPushSubscription();
|
||||
|
||||
onDataLoaded(List.of(
|
||||
pauseItem=new CheckableListItem<>(getString(R.string.pause_all_notifications), getPauseItemSubtitle(), CheckableListItem.Style.SWITCH, false, R.drawable.ic_notifications_paused_24px, ()->onPauseNotificationsClick(false)),
|
||||
policyItem=new ListItem<>(R.string.settings_notifications_policy, 0, R.drawable.ic_group_24px, this::onNotificationsPolicyClick),
|
||||
|
||||
mentionsItem=new CheckableListItem<>(R.string.notification_type_mentions_and_replies, 0, CheckableListItem.Style.CHECKBOX, pushSubscription.alerts.mention, ()->toggleCheckableItem(mentionsItem)),
|
||||
boostsItem=new CheckableListItem<>(R.string.notification_type_reblog, 0, CheckableListItem.Style.CHECKBOX, pushSubscription.alerts.reblog, ()->toggleCheckableItem(boostsItem)),
|
||||
favoritesItem=new CheckableListItem<>(R.string.notification_type_favorite, 0, CheckableListItem.Style.CHECKBOX, pushSubscription.alerts.favourite, ()->toggleCheckableItem(favoritesItem)),
|
||||
followersItem=new CheckableListItem<>(R.string.notification_type_follow, 0, CheckableListItem.Style.CHECKBOX, pushSubscription.alerts.follow, ()->toggleCheckableItem(followersItem)),
|
||||
pollsItem=new CheckableListItem<>(R.string.notification_type_poll, 0, CheckableListItem.Style.CHECKBOX, pushSubscription.alerts.poll, ()->toggleCheckableItem(pollsItem))
|
||||
));
|
||||
|
||||
typeItems=List.of(mentionsItem, boostsItem, favoritesItem, followersItem, pollsItem);
|
||||
pauseItem.checkedChangeListener=checked->onPauseNotificationsClick(true);
|
||||
updatePolicyItem(null);
|
||||
updatePauseItem();
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void doLoadData(int offset, int count){}
|
||||
|
||||
@Override
|
||||
protected void onHidden(){
|
||||
super.onHidden();
|
||||
PushSubscription ps=getPushSubscription();
|
||||
needUpdateNotificationSettings|=mentionsItem.checked!=ps.alerts.mention
|
||||
|| boostsItem.checked!=ps.alerts.reblog
|
||||
|| favoritesItem.checked!=ps.alerts.favourite
|
||||
|| followersItem.checked!=ps.alerts.follow
|
||||
|| pollsItem.checked!=ps.alerts.poll;
|
||||
if(needUpdateNotificationSettings && PushSubscriptionManager.arePushNotificationsAvailable()){
|
||||
ps.alerts.mention=mentionsItem.checked;
|
||||
ps.alerts.reblog=boostsItem.checked;
|
||||
ps.alerts.favourite=favoritesItem.checked;
|
||||
ps.alerts.follow=followersItem.checked;
|
||||
ps.alerts.poll=pollsItem.checked;
|
||||
AccountSessionManager.getInstance().getAccount(accountID).getPushSubscriptionManager().updatePushSettings(pushSubscription);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void onShown(){
|
||||
super.onShown();
|
||||
boolean allowed=areNotificationsAllowed();
|
||||
PushSubscription ps=getPushSubscription();
|
||||
if(allowed!=notificationsAllowed){
|
||||
notificationsAllowed=allowed;
|
||||
updateBanner();
|
||||
pauseItem.isEnabled=allowed;
|
||||
policyItem.isEnabled=allowed;
|
||||
rebindItem(pauseItem);
|
||||
rebindItem(policyItem);
|
||||
for(CheckableListItem<Void> item:typeItems){
|
||||
item.isEnabled=allowed && ps.policy!=PushSubscription.Policy.NONE;
|
||||
rebindItem(item);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
protected RecyclerView.Adapter<?> getAdapter(){
|
||||
View banner=getActivity().getLayoutInflater().inflate(R.layout.item_settings_banner, list, false);
|
||||
bannerText=banner.findViewById(R.id.text);
|
||||
bannerIcon=banner.findViewById(R.id.icon);
|
||||
bannerButton=banner.findViewById(R.id.button);
|
||||
bannerAdapter=new HideableSingleViewRecyclerAdapter(banner);
|
||||
bannerAdapter.setVisible(false);
|
||||
banner.findViewById(R.id.button2).setVisibility(View.GONE);
|
||||
banner.findViewById(R.id.title).setVisibility(View.GONE);
|
||||
|
||||
mergeAdapter=new MergeRecyclerAdapter();
|
||||
mergeAdapter.addAdapter(bannerAdapter);
|
||||
mergeAdapter.addAdapter(super.getAdapter());
|
||||
return mergeAdapter;
|
||||
}
|
||||
|
||||
@Override
|
||||
protected int indexOfItemsAdapter(){
|
||||
return mergeAdapter.getPositionForAdapter(itemsAdapter);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onViewCreated(View view, Bundle savedInstanceState){
|
||||
super.onViewCreated(view, savedInstanceState);
|
||||
updateBanner();
|
||||
}
|
||||
|
||||
private boolean areNotificationsAllowed(){
|
||||
return Build.VERSION.SDK_INT<Build.VERSION_CODES.N || getActivity().getSystemService(NotificationManager.class).areNotificationsEnabled();
|
||||
}
|
||||
|
||||
private PushSubscription getPushSubscription(){
|
||||
if(pushSubscription!=null)
|
||||
return pushSubscription;
|
||||
AccountSession session=AccountSessionManager.getInstance().getAccount(accountID);
|
||||
if(session.pushSubscription==null){
|
||||
pushSubscription=new PushSubscription();
|
||||
pushSubscription.alerts=PushSubscription.Alerts.ofAll();
|
||||
}else{
|
||||
pushSubscription=session.pushSubscription.clone();
|
||||
}
|
||||
return pushSubscription;
|
||||
}
|
||||
|
||||
private String getPauseItemSubtitle(){
|
||||
return getString(R.string.pause_notifications_off);
|
||||
}
|
||||
|
||||
private void resumePausedNotifications(){
|
||||
AccountSessionManager.get(accountID).getLocalPreferences().setNotificationsPauseEndTime(0);
|
||||
updatePauseItem();
|
||||
}
|
||||
|
||||
private void openSystemNotificationSettings(){
|
||||
Intent intent;
|
||||
if(Build.VERSION.SDK_INT<Build.VERSION_CODES.O){
|
||||
intent=new Intent(Settings.ACTION_APPLICATION_DETAILS_SETTINGS, Uri.fromParts("package", getActivity().getPackageName(), null));
|
||||
}else{
|
||||
intent=new Intent(Settings.ACTION_APP_NOTIFICATION_SETTINGS);
|
||||
intent.putExtra(Settings.EXTRA_APP_PACKAGE, getActivity().getPackageName());
|
||||
}
|
||||
intent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
|
||||
startActivity(intent);
|
||||
}
|
||||
|
||||
private void onPauseNotificationsClick(boolean fromSwitch){
|
||||
long time=AccountSessionManager.get(accountID).getLocalPreferences().getNotificationsPauseEndTime();
|
||||
if(time>System.currentTimeMillis() && fromSwitch){
|
||||
resumePausedNotifications();
|
||||
return;
|
||||
}
|
||||
int[] durationOptions={
|
||||
1800,
|
||||
3600,
|
||||
6*3600,
|
||||
12*3600,
|
||||
24*3600,
|
||||
3*24*3600,
|
||||
7*24*3600
|
||||
};
|
||||
int[] selectedOption={0};
|
||||
AlertDialog alert=new M3AlertDialogBuilder(getActivity())
|
||||
.setTitle(R.string.pause_all_notifications_title)
|
||||
.setSupportingText(time>System.currentTimeMillis() ? getString(R.string.pause_notifications_ends, UiUtils.formatRelativeTimestampAsMinutesAgo(getActivity(), Instant.ofEpochMilli(time), false)) : null)
|
||||
.setSingleChoiceItems((String[])Arrays.stream(durationOptions).mapToObj(d->UiUtils.formatDuration(getActivity(), d)).toArray(String[]::new), -1, (dlg, item)->{
|
||||
if(selectedOption[0]==0){
|
||||
((AlertDialog)dlg).getButton(AlertDialog.BUTTON_POSITIVE).setEnabled(true);
|
||||
}
|
||||
selectedOption[0]=durationOptions[item];
|
||||
})
|
||||
.setPositiveButton(R.string.ok, (dlg, item)->AccountSessionManager.get(accountID).getLocalPreferences().setNotificationsPauseEndTime(System.currentTimeMillis()+selectedOption[0]*1000L))
|
||||
.setNegativeButton(R.string.cancel, null)
|
||||
.show();
|
||||
alert.setOnDismissListener(dialog->updatePauseItem());
|
||||
alert.getButton(AlertDialog.BUTTON_POSITIVE).setEnabled(false);
|
||||
}
|
||||
|
||||
private void onNotificationsPolicyClick(){
|
||||
String[] items=Stream.of(
|
||||
R.string.notifications_policy_anyone,
|
||||
R.string.notifications_policy_followed,
|
||||
R.string.notifications_policy_follower,
|
||||
R.string.notifications_policy_no_one
|
||||
).map(this::getString).toArray(String[]::new);
|
||||
int[] selectedItem={getPushSubscription().policy.ordinal()};
|
||||
new M3AlertDialogBuilder(getActivity())
|
||||
.setTitle(R.string.settings_notifications_policy)
|
||||
.setSingleChoiceItems(items, selectedItem[0], (dlg, which)->selectedItem[0]=which)
|
||||
.setPositiveButton(R.string.ok, (dlg, which)->{
|
||||
PushSubscription.Policy prevValue=getPushSubscription().policy;
|
||||
PushSubscription.Policy newValue=PushSubscription.Policy.values()[selectedItem[0]];
|
||||
if(prevValue==newValue)
|
||||
return;
|
||||
getPushSubscription().policy=newValue;
|
||||
updatePolicyItem(prevValue);
|
||||
needUpdateNotificationSettings=true;
|
||||
})
|
||||
.setNegativeButton(R.string.cancel, null)
|
||||
.show();
|
||||
}
|
||||
|
||||
private void updatePolicyItem(PushSubscription.Policy prevValue){
|
||||
policyItem.subtitleRes=switch(getPushSubscription().policy){
|
||||
case ALL -> R.string.notifications_policy_anyone;
|
||||
case FOLLOWED -> R.string.notifications_policy_followed;
|
||||
case FOLLOWER -> R.string.notifications_policy_follower;
|
||||
case NONE -> R.string.notifications_policy_no_one;
|
||||
};
|
||||
rebindItem(policyItem);
|
||||
if(pushSubscription.policy==PushSubscription.Policy.NONE || prevValue==PushSubscription.Policy.NONE){
|
||||
for(CheckableListItem<Void> item:typeItems){
|
||||
item.checked=item.isEnabled=prevValue==PushSubscription.Policy.NONE;
|
||||
rebindItem(item);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private void updatePauseItem(){
|
||||
long time=AccountSessionManager.get(accountID).getLocalPreferences().getNotificationsPauseEndTime();
|
||||
if(time<System.currentTimeMillis()){
|
||||
pauseItem.subtitle=getString(R.string.pause_notifications_off);
|
||||
pauseItem.checked=false;
|
||||
}else{
|
||||
pauseItem.subtitle=getString(R.string.pause_notifications_ends, UiUtils.formatRelativeTimestampAsMinutesAgo(getActivity(), Instant.ofEpochMilli(time), false));
|
||||
pauseItem.checked=true;
|
||||
}
|
||||
rebindItem(pauseItem);
|
||||
updateBanner();
|
||||
}
|
||||
|
||||
private void updateBanner(){
|
||||
if(bannerAdapter==null)
|
||||
return;
|
||||
long pauseTime=AccountSessionManager.get(accountID).getLocalPreferences().getNotificationsPauseEndTime();
|
||||
if(!areNotificationsAllowed()){
|
||||
bannerAdapter.setVisible(true);
|
||||
bannerIcon.setImageResource(R.drawable.ic_app_badging_24px);
|
||||
bannerText.setText(R.string.notifications_disabled_in_system);
|
||||
bannerButton.setText(R.string.open_system_notification_settings);
|
||||
bannerButton.setOnClickListener(v->openSystemNotificationSettings());
|
||||
}else if(pauseTime>System.currentTimeMillis()){
|
||||
bannerAdapter.setVisible(true);
|
||||
bannerIcon.setImageResource(R.drawable.ic_notifications_paused_24px);
|
||||
bannerText.setText(getString(R.string.pause_notifications_banner, UiUtils.formatRelativeTimestampAsMinutesAgo(getActivity(), Instant.ofEpochMilli(pauseTime), false)));
|
||||
bannerButton.setText(R.string.resume_notifications_now);
|
||||
bannerButton.setOnClickListener(v->resumePausedNotifications());
|
||||
}else{
|
||||
bannerAdapter.setVisible(false);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,238 @@
|
||||
package org.joinmastodon.android.fragments.settings;
|
||||
|
||||
import android.annotation.SuppressLint;
|
||||
import android.app.Activity;
|
||||
import android.content.ActivityNotFoundException;
|
||||
import android.content.Intent;
|
||||
import android.net.Uri;
|
||||
import android.os.Build;
|
||||
import android.os.Bundle;
|
||||
import android.text.TextUtils;
|
||||
import android.view.Gravity;
|
||||
import android.view.LayoutInflater;
|
||||
import android.view.View;
|
||||
import android.view.ViewGroup;
|
||||
import android.view.WindowInsets;
|
||||
import android.webkit.WebView;
|
||||
import android.webkit.WebViewClient;
|
||||
import android.widget.ImageView;
|
||||
import android.widget.LinearLayout;
|
||||
import android.widget.ScrollView;
|
||||
import android.widget.TextView;
|
||||
import android.widget.Toast;
|
||||
|
||||
import org.joinmastodon.android.R;
|
||||
import org.joinmastodon.android.api.MastodonAPIController;
|
||||
import org.joinmastodon.android.api.requests.instance.GetInstanceExtendedDescription;
|
||||
import org.joinmastodon.android.api.session.AccountSessionManager;
|
||||
import org.joinmastodon.android.model.Instance;
|
||||
import org.joinmastodon.android.model.viewmodel.AccountViewModel;
|
||||
import org.joinmastodon.android.model.viewmodel.ListItem;
|
||||
import org.joinmastodon.android.ui.OutlineProviders;
|
||||
import org.joinmastodon.android.ui.utils.UiUtils;
|
||||
import org.joinmastodon.android.ui.viewholders.AccountViewHolder;
|
||||
import org.joinmastodon.android.ui.viewholders.SimpleListItemViewHolder;
|
||||
import org.joinmastodon.android.ui.views.FixedAspectRatioImageView;
|
||||
import org.joinmastodon.android.utils.ViewImageLoaderHolderTarget;
|
||||
|
||||
import java.io.BufferedReader;
|
||||
import java.io.IOException;
|
||||
import java.io.InputStreamReader;
|
||||
import java.util.HashMap;
|
||||
import java.util.Locale;
|
||||
import java.util.Map;
|
||||
|
||||
import me.grishka.appkit.api.SimpleCallback;
|
||||
import me.grishka.appkit.fragments.LoaderFragment;
|
||||
import me.grishka.appkit.imageloader.ViewImageLoader;
|
||||
import me.grishka.appkit.imageloader.requests.UrlImageLoaderRequest;
|
||||
import me.grishka.appkit.utils.V;
|
||||
|
||||
public class SettingsServerAboutFragment extends LoaderFragment{
|
||||
private String accountID;
|
||||
private Instance instance;
|
||||
|
||||
private WebView webView;
|
||||
private LinearLayout scrollingLayout;
|
||||
public ScrollView scroller;
|
||||
|
||||
@Override
|
||||
public void onCreate(Bundle savedInstanceState){
|
||||
super.onCreate(savedInstanceState);
|
||||
accountID=getArguments().getString("account");
|
||||
instance=AccountSessionManager.getInstance().getInstanceInfo(AccountSessionManager.get(accountID).domain);
|
||||
loadData();
|
||||
}
|
||||
|
||||
@SuppressLint("SetJavaScriptEnabled")
|
||||
@Override
|
||||
public View onCreateContentView(LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState){
|
||||
webView=new WebView(getActivity());
|
||||
webView.getSettings().setJavaScriptEnabled(true);
|
||||
webView.setWebViewClient(new WebViewClient(){
|
||||
@Override
|
||||
public void onPageFinished(WebView view, String url){
|
||||
dataLoaded();
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean shouldOverrideUrlLoading(WebView view, String url){
|
||||
Uri uri=Uri.parse(url);
|
||||
if(uri.getScheme().equals("http") || uri.getScheme().equals("https")){
|
||||
UiUtils.launchWebBrowser(getActivity(), url);
|
||||
}else{
|
||||
Intent intent=new Intent(Intent.ACTION_VIEW, uri);
|
||||
intent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
|
||||
try{
|
||||
startActivity(intent);
|
||||
}catch(ActivityNotFoundException x){
|
||||
Toast.makeText(getActivity(), R.string.no_app_to_handle_action, Toast.LENGTH_SHORT).show();
|
||||
}
|
||||
}
|
||||
return true;
|
||||
}
|
||||
});
|
||||
|
||||
scrollingLayout=new LinearLayout(getActivity());
|
||||
scrollingLayout.setOrientation(LinearLayout.VERTICAL);
|
||||
scroller=new ScrollView(getActivity());
|
||||
scroller.setNestedScrollingEnabled(true);
|
||||
scroller.setClipToPadding(false);
|
||||
scroller.addView(scrollingLayout);
|
||||
|
||||
FixedAspectRatioImageView banner=new FixedAspectRatioImageView(getActivity());
|
||||
banner.setAspectRatio(1.914893617f);
|
||||
banner.setScaleType(ImageView.ScaleType.CENTER_CROP);
|
||||
banner.setOutlineProvider(OutlineProviders.bottomRoundedRect(16));
|
||||
banner.setClipToOutline(true);
|
||||
ViewImageLoader.loadWithoutAnimation(banner, getResources().getDrawable(R.drawable.image_placeholder, getActivity().getTheme()), new UrlImageLoaderRequest(instance.thumbnail));
|
||||
LinearLayout.LayoutParams blp=new LinearLayout.LayoutParams(ViewGroup.LayoutParams.MATCH_PARENT, ViewGroup.LayoutParams.WRAP_CONTENT);
|
||||
blp.bottomMargin=V.dp(24);
|
||||
scrollingLayout.addView(banner, blp);
|
||||
|
||||
boolean needDivider=false;
|
||||
if(instance.contactAccount!=null){
|
||||
needDivider=true;
|
||||
TextView heading=new TextView(getActivity());
|
||||
heading.setTextAppearance(R.style.m3_title_small);
|
||||
heading.setTextColor(UiUtils.getThemeColor(getActivity(), R.attr.colorM3OnSurfaceVariant));
|
||||
heading.setSingleLine();
|
||||
heading.setText(R.string.server_administrator);
|
||||
heading.setGravity(Gravity.CENTER_VERTICAL);
|
||||
LinearLayout.LayoutParams hlp=new LinearLayout.LayoutParams(ViewGroup.LayoutParams.MATCH_PARENT, V.dp(20));
|
||||
hlp.bottomMargin=V.dp(4);
|
||||
hlp.leftMargin=hlp.rightMargin=V.dp(16);
|
||||
scrollingLayout.addView(heading, hlp);
|
||||
|
||||
AccountViewModel model=new AccountViewModel(instance.contactAccount, accountID);
|
||||
AccountViewHolder holder=new AccountViewHolder(this, scrollingLayout, null);
|
||||
holder.bind(model);
|
||||
holder.itemView.setBackground(UiUtils.getThemeDrawable(getActivity(), android.R.attr.selectableItemBackground));
|
||||
holder.itemView.setOnClickListener(v->holder.onClick());
|
||||
scrollingLayout.addView(holder.itemView);
|
||||
ViewImageLoader.load(new ViewImageLoaderHolderTarget(holder, 0), null, model.avaRequest, false);
|
||||
for(int i=0;i<model.emojiHelper.getImageCount();i++){
|
||||
ViewImageLoader.load(new ViewImageLoaderHolderTarget(holder, i+1), null, model.emojiHelper.getImageRequest(i), false);
|
||||
}
|
||||
}
|
||||
if(!TextUtils.isEmpty(instance.email)){
|
||||
needDivider=true;
|
||||
SimpleListItemViewHolder holder=new SimpleListItemViewHolder(getActivity(), scrollingLayout);
|
||||
ListItem<Void> item=new ListItem<>(R.string.send_email_to_server_admin, 0, R.drawable.ic_mail_24px, ()->{});
|
||||
holder.bind(item);
|
||||
holder.itemView.setBackground(UiUtils.getThemeDrawable(getActivity(), android.R.attr.selectableItemBackground));
|
||||
holder.itemView.setOnClickListener(v->openAdminEmail());
|
||||
scrollingLayout.addView(holder.itemView);
|
||||
}
|
||||
if(needDivider){
|
||||
View divider=new View(getActivity());
|
||||
divider.setBackgroundColor(UiUtils.getThemeColor(getActivity(), R.attr.colorM3OutlineVariant));
|
||||
LinearLayout.LayoutParams dlp=new LinearLayout.LayoutParams(ViewGroup.LayoutParams.MATCH_PARENT, V.dp(1));
|
||||
dlp.leftMargin=dlp.rightMargin=V.dp(16);
|
||||
scrollingLayout.addView(divider, dlp);
|
||||
}
|
||||
scrollingLayout.addView(webView, new LinearLayout.LayoutParams(ViewGroup.LayoutParams.MATCH_PARENT, ViewGroup.LayoutParams.WRAP_CONTENT));
|
||||
|
||||
return scroller;
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void doLoadData(){
|
||||
new GetInstanceExtendedDescription()
|
||||
.setCallback(new SimpleCallback<>(this){
|
||||
@Override
|
||||
public void onSuccess(GetInstanceExtendedDescription.Response result){
|
||||
MastodonAPIController.runInBackground(()->{
|
||||
Activity activity=getActivity();
|
||||
if(activity==null)
|
||||
return;
|
||||
String template;
|
||||
try(BufferedReader reader=new BufferedReader(new InputStreamReader(getActivity().getAssets().open("server_about_template.htm")))){
|
||||
StringBuilder sb=new StringBuilder();
|
||||
String line;
|
||||
while((line=reader.readLine())!=null){
|
||||
sb.append(line);
|
||||
sb.append('\n');
|
||||
}
|
||||
template=sb.toString();
|
||||
}catch(IOException x){
|
||||
throw new RuntimeException(x);
|
||||
}
|
||||
|
||||
HashMap<String, String> templateParams=new HashMap<>();
|
||||
templateParams.put("content", result.content);
|
||||
templateParams.put("colorSurface", getThemeColorAsCss(R.attr.colorM3Surface, 1));
|
||||
templateParams.put("colorOnSurface", getThemeColorAsCss(R.attr.colorM3OnSurface, 1));
|
||||
templateParams.put("colorPrimary", getThemeColorAsCss(R.attr.colorM3Primary, 1));
|
||||
templateParams.put("colorPrimaryTransparent", getThemeColorAsCss(R.attr.colorM3Primary, 0.2f));
|
||||
for(Map.Entry<String, String> param:templateParams.entrySet()){
|
||||
template=template.replace("{{"+param.getKey()+"}}", param.getValue());
|
||||
}
|
||||
|
||||
final String html=template;
|
||||
activity.runOnUiThread(()->{
|
||||
webView.loadDataWithBaseURL(null, html, "text/html; charset=utf-8", null, null);
|
||||
});
|
||||
});
|
||||
}
|
||||
})
|
||||
.exec(accountID);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onRefresh(){}
|
||||
|
||||
private void openAdminEmail(){
|
||||
Intent intent=new Intent(Intent.ACTION_VIEW, Uri.fromParts("mailto", instance.email, null));
|
||||
intent.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
|
||||
try{
|
||||
startActivity(intent);
|
||||
}catch(ActivityNotFoundException x){
|
||||
Toast.makeText(getActivity(), R.string.no_app_to_handle_action, Toast.LENGTH_SHORT).show();
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onApplyWindowInsets(WindowInsets insets){
|
||||
if(Build.VERSION.SDK_INT>=29 && insets.getTappableElementInsets().bottom==0){
|
||||
scroller.setPadding(0, 0, 0, insets.getSystemWindowInsetBottom());
|
||||
progress.setPadding(0, 0, 0, insets.getSystemWindowInsetBottom());
|
||||
insets=insets.inset(0, 0, 0, insets.getSystemWindowInsetBottom());
|
||||
}else{
|
||||
scroller.setPadding(0, 0, 0, 0);
|
||||
}
|
||||
super.onApplyWindowInsets(insets);
|
||||
}
|
||||
|
||||
private String getThemeColorAsCss(int attr, float alpha){
|
||||
int color=UiUtils.getThemeColor(getActivity(), attr);
|
||||
if(alpha==1f){
|
||||
return String.format(Locale.US, "#%06X", color & 0xFFFFFF);
|
||||
}else{
|
||||
int r=(color >> 16) & 0xFF;
|
||||
int g=(color >> 8) & 0xFF;
|
||||
int b=color & 0xFF;
|
||||
return "rgba("+r+","+g+","+b+","+alpha+")";
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,185 @@
|
||||
package org.joinmastodon.android.fragments.settings;
|
||||
|
||||
import android.app.Fragment;
|
||||
import android.os.Build;
|
||||
import android.os.Bundle;
|
||||
import android.view.LayoutInflater;
|
||||
import android.view.View;
|
||||
import android.view.ViewGroup;
|
||||
import android.view.ViewTreeObserver;
|
||||
import android.view.WindowInsets;
|
||||
import android.widget.FrameLayout;
|
||||
import android.widget.TextView;
|
||||
|
||||
import org.joinmastodon.android.R;
|
||||
import org.joinmastodon.android.api.session.AccountSessionManager;
|
||||
import org.joinmastodon.android.model.Instance;
|
||||
import org.joinmastodon.android.ui.SimpleViewHolder;
|
||||
import org.joinmastodon.android.ui.tabs.TabLayout;
|
||||
import org.joinmastodon.android.ui.tabs.TabLayoutMediator;
|
||||
import org.joinmastodon.android.ui.utils.UiUtils;
|
||||
import org.joinmastodon.android.ui.views.NestedRecyclerScrollView;
|
||||
|
||||
import androidx.annotation.NonNull;
|
||||
import androidx.annotation.Nullable;
|
||||
import androidx.recyclerview.widget.RecyclerView;
|
||||
import androidx.viewpager2.widget.ViewPager2;
|
||||
import me.grishka.appkit.fragments.AppKitFragment;
|
||||
import me.grishka.appkit.utils.V;
|
||||
|
||||
public class SettingsServerFragment extends AppKitFragment{
|
||||
private String accountID;
|
||||
private Instance instance;
|
||||
private TabLayout tabBar;
|
||||
private TabLayoutMediator tabLayoutMediator;
|
||||
private ViewPager2 pager;
|
||||
private FrameLayout[] tabViews;
|
||||
private View contentView;
|
||||
private WindowInsets childInsets;
|
||||
|
||||
private SettingsServerAboutFragment aboutFragment;
|
||||
private SettingsServerRulesFragment rulesFragment;
|
||||
|
||||
@Override
|
||||
public void onCreate(Bundle savedInstanceState){
|
||||
super.onCreate(savedInstanceState);
|
||||
accountID=getArguments().getString("account");
|
||||
setTitle(AccountSessionManager.get(accountID).domain);
|
||||
|
||||
Bundle args=new Bundle();
|
||||
args.putString("account", accountID);
|
||||
args.putBoolean("__is_tab", true);
|
||||
aboutFragment=new SettingsServerAboutFragment();
|
||||
aboutFragment.setArguments(args);
|
||||
rulesFragment=new SettingsServerRulesFragment();
|
||||
rulesFragment.setArguments(args);
|
||||
}
|
||||
|
||||
@Nullable
|
||||
@Override
|
||||
public View onCreateView(LayoutInflater inflater, @Nullable ViewGroup container, Bundle savedInstanceState){
|
||||
View view=inflater.inflate(R.layout.fragment_settings_server, container, false);
|
||||
|
||||
TextView realTitle=view.findViewById(R.id.real_title);
|
||||
realTitle.setText(getTitle());
|
||||
realTitle.setSelected(true);
|
||||
|
||||
pager=view.findViewById(R.id.pager);
|
||||
pager.setAdapter(new ServerPagerAdapter());
|
||||
|
||||
FrameLayout sizeWrapper=new FrameLayout(getActivity()){
|
||||
@Override
|
||||
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec){
|
||||
pager.getLayoutParams().height=MeasureSpec.getSize(heightMeasureSpec)-getPaddingTop()-getPaddingBottom()-V.dp(48);
|
||||
super.onMeasure(widthMeasureSpec, heightMeasureSpec);
|
||||
}
|
||||
};
|
||||
|
||||
tabViews=new FrameLayout[2];
|
||||
for(int i=0;i<tabViews.length;i++){
|
||||
FrameLayout tabView=new FrameLayout(getActivity());
|
||||
tabView.setId(switch(i){
|
||||
case 0 -> R.id.server_about;
|
||||
case 1 -> R.id.server_rules;
|
||||
default -> throw new IllegalStateException("Unexpected value: "+i);
|
||||
});
|
||||
tabView.setVisibility(View.GONE);
|
||||
sizeWrapper.addView(tabView); // needed so the fragment manager will have somewhere to restore the tab fragment
|
||||
tabViews[i]=tabView;
|
||||
}
|
||||
|
||||
tabBar=view.findViewById(R.id.tabbar);
|
||||
tabBar.setTabTextColors(UiUtils.getThemeColor(getActivity(), R.attr.colorM3OnSurfaceVariant), UiUtils.getThemeColor(getActivity(), R.attr.colorM3Primary));
|
||||
tabBar.setTabTextSize(V.dp(16));
|
||||
tabLayoutMediator=new TabLayoutMediator(tabBar, pager, (tab, position)->tab.setText(switch(position){
|
||||
case 0 -> R.string.about_server;
|
||||
case 1 -> R.string.server_rules;
|
||||
default -> throw new IllegalStateException("Unexpected value: "+position);
|
||||
}));
|
||||
tabLayoutMediator.attach();
|
||||
|
||||
NestedRecyclerScrollView scrollView=view.findViewById(R.id.scroller);
|
||||
scrollView.setScrollableChildSupplier(()->switch(pager.getCurrentItem()){
|
||||
case 0 -> aboutFragment.scroller;
|
||||
case 1 -> rulesFragment.getList();
|
||||
default -> throw new IllegalStateException("Unexpected value: "+pager.getCurrentItem());
|
||||
});
|
||||
|
||||
return contentView=view;
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void onUpdateToolbar(){
|
||||
super.onUpdateToolbar();
|
||||
getToolbar().setTitle(null);
|
||||
}
|
||||
|
||||
private Fragment getFragmentForPage(int page){
|
||||
return switch(page){
|
||||
case 0 -> aboutFragment;
|
||||
case 1 -> rulesFragment;
|
||||
default -> throw new IllegalStateException();
|
||||
};
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onApplyWindowInsets(WindowInsets insets){
|
||||
if(contentView!=null){
|
||||
if(Build.VERSION.SDK_INT>=29 && insets.getTappableElementInsets().bottom==0){
|
||||
int insetBottom=insets.getSystemWindowInsetBottom();
|
||||
childInsets=insets.inset(insets.getSystemWindowInsetLeft(), insets.getSystemWindowInsetTop(), insets.getSystemWindowInsetRight(), 0);
|
||||
applyChildWindowInsets();
|
||||
insets=insets.inset(0, 0, 0, insetBottom);
|
||||
}
|
||||
}
|
||||
super.onApplyWindowInsets(insets);
|
||||
}
|
||||
|
||||
private void applyChildWindowInsets(){
|
||||
if(aboutFragment!=null && aboutFragment.isAdded() && childInsets!=null){
|
||||
aboutFragment.onApplyWindowInsets(childInsets);
|
||||
rulesFragment.onApplyWindowInsets(childInsets);
|
||||
}
|
||||
}
|
||||
|
||||
private class ServerPagerAdapter extends RecyclerView.Adapter<SimpleViewHolder>{
|
||||
@NonNull
|
||||
@Override
|
||||
public SimpleViewHolder onCreateViewHolder(@NonNull ViewGroup parent, int viewType){
|
||||
FrameLayout view=tabViews[viewType];
|
||||
((ViewGroup)view.getParent()).removeView(view);
|
||||
view.setVisibility(View.VISIBLE);
|
||||
view.setLayoutParams(new RecyclerView.LayoutParams(ViewGroup.LayoutParams.MATCH_PARENT, ViewGroup.LayoutParams.MATCH_PARENT));
|
||||
return new SimpleViewHolder(view);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onBindViewHolder(@NonNull SimpleViewHolder holder, int position){
|
||||
Fragment fragment=getFragmentForPage(position);
|
||||
if(!fragment.isAdded()){
|
||||
getChildFragmentManager().beginTransaction().add(holder.itemView.getId(), fragment).commit();
|
||||
holder.itemView.getViewTreeObserver().addOnPreDrawListener(new ViewTreeObserver.OnPreDrawListener(){
|
||||
@Override
|
||||
public boolean onPreDraw(){
|
||||
getChildFragmentManager().executePendingTransactions();
|
||||
if(fragment.isAdded()){
|
||||
holder.itemView.getViewTreeObserver().removeOnPreDrawListener(this);
|
||||
applyChildWindowInsets();
|
||||
}
|
||||
return true;
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public int getItemCount(){
|
||||
return 2;
|
||||
}
|
||||
|
||||
@Override
|
||||
public int getItemViewType(int position){
|
||||
return position;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,47 @@
|
||||
package org.joinmastodon.android.fragments.settings;
|
||||
|
||||
import android.os.Bundle;
|
||||
import android.view.LayoutInflater;
|
||||
import android.view.View;
|
||||
|
||||
import org.joinmastodon.android.R;
|
||||
import org.joinmastodon.android.api.session.AccountSessionManager;
|
||||
import org.joinmastodon.android.fragments.MastodonRecyclerFragment;
|
||||
import org.joinmastodon.android.model.Instance;
|
||||
import org.joinmastodon.android.ui.adapters.InstanceRulesAdapter;
|
||||
|
||||
import androidx.recyclerview.widget.RecyclerView;
|
||||
|
||||
public class SettingsServerRulesFragment extends MastodonRecyclerFragment<Instance.Rule>{
|
||||
private String accountID;
|
||||
|
||||
public SettingsServerRulesFragment(){
|
||||
super(20);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onCreate(Bundle savedInstanceState){
|
||||
super.onCreate(savedInstanceState);
|
||||
accountID=getArguments().getString("account");
|
||||
Instance instance=AccountSessionManager.getInstance().getInstanceInfo(AccountSessionManager.get(accountID).domain);
|
||||
onDataLoaded(instance.rules);
|
||||
setRefreshEnabled(false);
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void doLoadData(int offset, int count){}
|
||||
|
||||
@Override
|
||||
protected RecyclerView.Adapter<?> getAdapter(){
|
||||
return new InstanceRulesAdapter(data);
|
||||
}
|
||||
|
||||
@Override
|
||||
protected View onCreateFooterView(LayoutInflater inflater){
|
||||
return inflater.inflate(R.layout.load_more_with_end_mark, null);
|
||||
}
|
||||
|
||||
public RecyclerView getList(){
|
||||
return list;
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user