megalodon/mastodon/src/main/java/org/joinmastodon/android/fragments/settings/EditFilterFragment.java

325 lines
12 KiB
Java

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;
}
}