mirror of
https://github.com/mastodon/mastodon-android.git
synced 2025-02-07 15:38:55 +01:00
List creation (AND-98)
This commit is contained in:
parent
0ed858b99c
commit
66b7b127f9
@ -0,0 +1,23 @@
|
||||
package org.joinmastodon.android.api.requests.lists;
|
||||
|
||||
import org.joinmastodon.android.api.MastodonAPIRequest;
|
||||
import org.joinmastodon.android.model.FollowList;
|
||||
|
||||
public class CreateList extends MastodonAPIRequest<FollowList>{
|
||||
public CreateList(String title, FollowList.RepliesPolicy repliesPolicy, boolean exclusive){
|
||||
super(HttpMethod.POST, "/lists", FollowList.class);
|
||||
setRequestBody(new Request(title, repliesPolicy, exclusive));
|
||||
}
|
||||
|
||||
private static class Request{
|
||||
public String title;
|
||||
public FollowList.RepliesPolicy repliesPolicy;
|
||||
public boolean exclusive;
|
||||
|
||||
public Request(String title, FollowList.RepliesPolicy repliesPolicy, boolean exclusive){
|
||||
this.title=title;
|
||||
this.repliesPolicy=repliesPolicy;
|
||||
this.exclusive=exclusive;
|
||||
}
|
||||
}
|
||||
}
|
@ -0,0 +1,11 @@
|
||||
package org.joinmastodon.android.events;
|
||||
|
||||
public class FinishListCreationFragmentEvent{
|
||||
public final String accountID;
|
||||
public final String listID;
|
||||
|
||||
public FinishListCreationFragmentEvent(String accountID, String listID){
|
||||
this.accountID=accountID;
|
||||
this.listID=listID;
|
||||
}
|
||||
}
|
@ -0,0 +1,174 @@
|
||||
package org.joinmastodon.android.fragments;
|
||||
|
||||
import android.app.Activity;
|
||||
import android.os.Bundle;
|
||||
import android.view.ViewGroup;
|
||||
import android.widget.ArrayAdapter;
|
||||
import android.widget.EditText;
|
||||
import android.widget.LinearLayout;
|
||||
import android.widget.Spinner;
|
||||
|
||||
import org.joinmastodon.android.E;
|
||||
import org.joinmastodon.android.R;
|
||||
import org.joinmastodon.android.api.requests.lists.DeleteList;
|
||||
import org.joinmastodon.android.api.requests.lists.GetListAccounts;
|
||||
import org.joinmastodon.android.events.ListDeletedEvent;
|
||||
import org.joinmastodon.android.fragments.settings.BaseSettingsFragment;
|
||||
import org.joinmastodon.android.model.Account;
|
||||
import org.joinmastodon.android.model.FollowList;
|
||||
import org.joinmastodon.android.model.HeaderPaginationList;
|
||||
import org.joinmastodon.android.model.viewmodel.AvatarPileListItem;
|
||||
import org.joinmastodon.android.model.viewmodel.CheckableListItem;
|
||||
import org.joinmastodon.android.model.viewmodel.ListItem;
|
||||
import org.joinmastodon.android.ui.views.FloatingHintEditTextLayout;
|
||||
import org.parceler.Parcels;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
|
||||
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.imageloader.requests.UrlImageLoaderRequest;
|
||||
import me.grishka.appkit.utils.MergeRecyclerAdapter;
|
||||
import me.grishka.appkit.utils.SingleViewRecyclerAdapter;
|
||||
import me.grishka.appkit.utils.V;
|
||||
|
||||
public abstract class BaseEditListFragment extends BaseSettingsFragment<Void>{
|
||||
protected FollowList followList;
|
||||
protected AvatarPileListItem<Void> membersItem;
|
||||
protected CheckableListItem<Void> exclusiveItem;
|
||||
protected FloatingHintEditTextLayout titleEditLayout;
|
||||
protected EditText titleEdit;
|
||||
protected Spinner showRepliesSpinner;
|
||||
private APIRequest<?> getMembersRequest;
|
||||
|
||||
@Override
|
||||
public void onCreate(Bundle savedInstanceState){
|
||||
super.onCreate(savedInstanceState);
|
||||
followList=Parcels.unwrap(getArguments().getParcelable("list"));
|
||||
|
||||
membersItem=new AvatarPileListItem<>(getString(R.string.list_members), null, List.of(), 0, i->onMembersClick(), null, false);
|
||||
List<ListItem<Void>> items=new ArrayList<>();
|
||||
if(followList!=null){
|
||||
items.add(membersItem);
|
||||
}
|
||||
exclusiveItem=new CheckableListItem<>(R.string.list_exclusive, R.string.list_exclusive_subtitle, CheckableListItem.Style.SWITCH, followList!=null && followList.exclusive, this::toggleCheckableItem);
|
||||
items.add(exclusiveItem);
|
||||
onDataLoaded(items);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onDestroy(){
|
||||
super.onDestroy();
|
||||
if(getMembersRequest!=null)
|
||||
getMembersRequest.cancel();
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void doLoadData(int offset, int count){}
|
||||
|
||||
@Override
|
||||
protected RecyclerView.Adapter<?> getAdapter(){
|
||||
LinearLayout topView=new LinearLayout(getActivity());
|
||||
topView.setOrientation(LinearLayout.VERTICAL);
|
||||
|
||||
titleEditLayout=(FloatingHintEditTextLayout) getActivity().getLayoutInflater().inflate(R.layout.floating_hint_edit_text, topView, false);
|
||||
titleEdit=titleEditLayout.findViewById(R.id.edit);
|
||||
titleEdit.setHint(R.string.list_name);
|
||||
titleEditLayout.updateHint();
|
||||
if(followList!=null)
|
||||
titleEdit.setText(followList.title);
|
||||
topView.addView(titleEditLayout);
|
||||
|
||||
FloatingHintEditTextLayout showRepliesLayout=(FloatingHintEditTextLayout) getActivity().getLayoutInflater().inflate(R.layout.floating_hint_spinner, topView, false);
|
||||
showRepliesSpinner=showRepliesLayout.findViewById(R.id.spinner);
|
||||
showRepliesLayout.setHint(R.string.list_show_replies_to);
|
||||
topView.addView(showRepliesLayout);
|
||||
ArrayAdapter<String> spinnerAdapter=new ArrayAdapter<>(getActivity(), R.layout.item_spinner, List.of(
|
||||
getString(R.string.list_replies_no_one),
|
||||
getString(R.string.list_replies_members),
|
||||
getString(R.string.list_replies_anyone)
|
||||
));
|
||||
showRepliesSpinner.setAdapter(spinnerAdapter);
|
||||
showRepliesSpinner.setSelection(switch(followList!=null ? followList.repliesPolicy : FollowList.RepliesPolicy.LIST){
|
||||
case FOLLOWED -> 2;
|
||||
case LIST -> 1;
|
||||
case NONE -> 0;
|
||||
});
|
||||
ViewGroup.MarginLayoutParams llp=(ViewGroup.MarginLayoutParams)showRepliesLayout.getLabel().getLayoutParams();
|
||||
llp.setMarginStart(llp.getMarginStart()+V.dp(16));
|
||||
|
||||
MergeRecyclerAdapter adapter=new MergeRecyclerAdapter();
|
||||
adapter.addAdapter(new SingleViewRecyclerAdapter(topView));
|
||||
adapter.addAdapter(super.getAdapter());
|
||||
return adapter;
|
||||
}
|
||||
|
||||
@Override
|
||||
protected int indexOfItemsAdapter(){
|
||||
return 1;
|
||||
}
|
||||
|
||||
protected void doDeleteList(){
|
||||
new DeleteList(followList.id)
|
||||
.setCallback(new Callback<>(){
|
||||
@Override
|
||||
public void onSuccess(Void result){
|
||||
E.post(new ListDeletedEvent(accountID, followList.id));
|
||||
Nav.finish(BaseEditListFragment.this);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onError(ErrorResponse error){
|
||||
Activity activity=getActivity();
|
||||
if(activity==null)
|
||||
return;
|
||||
error.showToast(activity);
|
||||
}
|
||||
})
|
||||
.wrapProgress(getActivity(), R.string.loading, true)
|
||||
.exec(accountID);
|
||||
}
|
||||
|
||||
private void onMembersClick(){
|
||||
Bundle args=new Bundle();
|
||||
args.putString("account", accountID);
|
||||
args.putParcelable("list", Parcels.wrap(followList));
|
||||
Nav.go(getActivity(), ListMembersFragment.class, args);
|
||||
}
|
||||
|
||||
protected void loadMembers(){
|
||||
getMembersRequest=new GetListAccounts(followList.id, null, 3)
|
||||
.setCallback(new Callback<>(){
|
||||
@Override
|
||||
public void onSuccess(HeaderPaginationList<Account> result){
|
||||
getMembersRequest=null;
|
||||
membersItem.avatars=new ArrayList<>();
|
||||
for(int i=0;i<Math.min(3, result.size());i++){
|
||||
Account acc=result.get(i);
|
||||
membersItem.avatars.add(new UrlImageLoaderRequest(acc.avatarStatic, V.dp(32), V.dp(32)));
|
||||
}
|
||||
rebindItem(membersItem);
|
||||
imgLoader.updateImages();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onError(ErrorResponse error){
|
||||
getMembersRequest=null;
|
||||
}
|
||||
})
|
||||
.exec(accountID);
|
||||
}
|
||||
|
||||
protected FollowList.RepliesPolicy getSelectedRepliesPolicy(){
|
||||
return switch(showRepliesSpinner.getSelectedItemPosition()){
|
||||
case 0 -> FollowList.RepliesPolicy.NONE;
|
||||
case 1 -> FollowList.RepliesPolicy.LIST;
|
||||
case 2 -> FollowList.RepliesPolicy.FOLLOWED;
|
||||
default -> throw new IllegalStateException("Unexpected value: "+showRepliesSpinner.getSelectedItemPosition());
|
||||
};
|
||||
}
|
||||
}
|
@ -0,0 +1,312 @@
|
||||
package org.joinmastodon.android.fragments;
|
||||
|
||||
import android.content.res.TypedArray;
|
||||
import android.os.Bundle;
|
||||
import android.view.Gravity;
|
||||
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.ViewStub;
|
||||
import android.view.WindowInsets;
|
||||
import android.widget.Button;
|
||||
import android.widget.FrameLayout;
|
||||
import android.widget.TextView;
|
||||
|
||||
import org.joinmastodon.android.E;
|
||||
import org.joinmastodon.android.R;
|
||||
import org.joinmastodon.android.api.requests.lists.AddAccountsToList;
|
||||
import org.joinmastodon.android.api.requests.lists.GetListAccounts;
|
||||
import org.joinmastodon.android.api.requests.lists.RemoveAccountsFromList;
|
||||
import org.joinmastodon.android.events.FinishListCreationFragmentEvent;
|
||||
import org.joinmastodon.android.fragments.account_list.AddNewListMembersFragment;
|
||||
import org.joinmastodon.android.fragments.account_list.BaseAccountListFragment;
|
||||
import org.joinmastodon.android.model.Account;
|
||||
import org.joinmastodon.android.model.FollowList;
|
||||
import org.joinmastodon.android.model.HeaderPaginationList;
|
||||
import org.joinmastodon.android.model.viewmodel.AccountViewModel;
|
||||
import org.joinmastodon.android.ui.utils.UiUtils;
|
||||
import org.joinmastodon.android.ui.viewholders.AccountViewHolder;
|
||||
import org.joinmastodon.android.ui.views.CurlyArrowEmptyView;
|
||||
import org.parceler.Parcels;
|
||||
|
||||
import java.util.HashSet;
|
||||
import java.util.List;
|
||||
import java.util.Set;
|
||||
import java.util.stream.Collectors;
|
||||
|
||||
import me.grishka.appkit.Nav;
|
||||
import me.grishka.appkit.api.Callback;
|
||||
import me.grishka.appkit.api.ErrorResponse;
|
||||
import me.grishka.appkit.api.SimpleCallback;
|
||||
import me.grishka.appkit.fragments.OnBackPressedListener;
|
||||
import me.grishka.appkit.fragments.WindowInsetsAwareFragment;
|
||||
import me.grishka.appkit.utils.CubicBezierInterpolator;
|
||||
import me.grishka.appkit.utils.V;
|
||||
import me.grishka.appkit.views.FragmentRootLinearLayout;
|
||||
|
||||
public class CreateListAddMembersFragment extends BaseAccountListFragment implements OnBackPressedListener, AddNewListMembersFragment.Listener{
|
||||
private FollowList followList;
|
||||
private Button nextButton;
|
||||
private View buttonBar;
|
||||
private FragmentRootLinearLayout rootView;
|
||||
private FrameLayout searchFragmentContainer;
|
||||
private FrameLayout fragmentContentWrap;
|
||||
private AddNewListMembersFragment searchFragment;
|
||||
private WindowInsets lastInsets;
|
||||
private boolean dismissingSearchFragment;
|
||||
private HashSet<String> accountIDsInList=new HashSet<>();
|
||||
|
||||
@Override
|
||||
public void onCreate(Bundle savedInstanceState){
|
||||
super.onCreate(savedInstanceState);
|
||||
setTitle(R.string.manage_list_members);
|
||||
setSubtitle(getString(R.string.step_x_of_y, 2, 2));
|
||||
setLayout(R.layout.fragment_login);
|
||||
setEmptyText(R.string.list_no_members);
|
||||
setHasOptionsMenu(true);
|
||||
|
||||
followList=Parcels.unwrap(getArguments().getParcelable("list"));
|
||||
if(savedInstanceState!=null || getArguments().getBoolean("needLoadMembers", false)){
|
||||
loadData();
|
||||
}else{
|
||||
onDataLoaded(List.of());
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void doLoadData(int offset, int count){
|
||||
currentRequest=new GetListAccounts(followList.id, null, 0)
|
||||
.setCallback(new SimpleCallback<>(this){
|
||||
@Override
|
||||
public void onSuccess(HeaderPaginationList<Account> result){
|
||||
for(Account acc:result)
|
||||
accountIDsInList.add(acc.id);
|
||||
onDataLoaded(result.stream().map(a->new AccountViewModel(a, accountID)).collect(Collectors.toList()));
|
||||
}
|
||||
})
|
||||
.exec(accountID);
|
||||
}
|
||||
|
||||
@Override
|
||||
public View onCreateView(LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState){
|
||||
View view=super.onCreateView(inflater, container, savedInstanceState);
|
||||
FrameLayout wrapper=new FrameLayout(getActivity());
|
||||
wrapper.addView(view);
|
||||
rootView=(FragmentRootLinearLayout) view;
|
||||
fragmentContentWrap=wrapper;
|
||||
return wrapper;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onViewCreated(View view, Bundle savedInstanceState){
|
||||
nextButton=view.findViewById(R.id.btn_next);
|
||||
nextButton.setOnClickListener(this::onNextClick);
|
||||
nextButton.setText(R.string.done);
|
||||
buttonBar=view.findViewById(R.id.button_bar);
|
||||
|
||||
super.onViewCreated(view, savedInstanceState);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onApplyWindowInsets(WindowInsets insets){
|
||||
lastInsets=insets;
|
||||
if(searchFragment!=null)
|
||||
searchFragment.onApplyWindowInsets(insets);
|
||||
insets=UiUtils.applyBottomInsetToFixedView(buttonBar, insets);
|
||||
rootView.dispatchApplyWindowInsets(insets);
|
||||
}
|
||||
|
||||
@Override
|
||||
protected List<View> getViewsForElevationEffect(){
|
||||
return List.of(getToolbar(), buttonBar);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onCreateOptionsMenu(Menu menu, MenuInflater inflater){
|
||||
MenuItem item=menu.add(R.string.add_list_member);
|
||||
item.setIcon(R.drawable.ic_add_24px);
|
||||
item.setShowAsAction(MenuItem.SHOW_AS_ACTION_ALWAYS);
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean onOptionsItemSelected(MenuItem item){
|
||||
if(searchFragmentContainer!=null)
|
||||
return true;
|
||||
|
||||
searchFragmentContainer=new FrameLayout(getActivity());
|
||||
searchFragmentContainer.setId(R.id.search_fragment);
|
||||
fragmentContentWrap.addView(searchFragmentContainer);
|
||||
|
||||
Bundle args=new Bundle();
|
||||
args.putString("account", accountID);
|
||||
args.putParcelable("list", Parcels.wrap(followList));
|
||||
args.putBoolean("_can_go_back", true);
|
||||
searchFragment=new AddNewListMembersFragment(this);
|
||||
searchFragment.setArguments(args);
|
||||
getChildFragmentManager().beginTransaction().add(R.id.search_fragment, searchFragment).commit();
|
||||
getChildFragmentManager().executePendingTransactions();
|
||||
if(lastInsets!=null)
|
||||
searchFragment.onApplyWindowInsets(lastInsets);
|
||||
searchFragmentContainer.setTranslationX(V.dp(100));
|
||||
searchFragmentContainer.setAlpha(0f);
|
||||
searchFragmentContainer.animate().translationX(0).alpha(1).setDuration(300).setInterpolator(CubicBezierInterpolator.DEFAULT).start();
|
||||
return true;
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void initializeEmptyView(View contentView){
|
||||
ViewStub emptyStub=contentView.findViewById(R.id.empty);
|
||||
emptyStub.setLayoutResource(R.layout.empty_with_arrow);
|
||||
super.initializeEmptyView(contentView);
|
||||
TextView emptySecondary=contentView.findViewById(R.id.empty_text_secondary);
|
||||
emptySecondary.setText(R.string.list_find_users);
|
||||
CurlyArrowEmptyView arrowView=(CurlyArrowEmptyView) emptyView;
|
||||
arrowView.setGravityAndOffsets(Gravity.TOP | Gravity.END, 24, 2);
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void setStatusBarColor(int color){
|
||||
rootView.setStatusBarColor(color);
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void setNavigationBarColor(int color){
|
||||
rootView.setNavigationBarColor(color);
|
||||
}
|
||||
|
||||
private void dismissSearchFragment(){
|
||||
if(searchFragment==null || dismissingSearchFragment)
|
||||
return;
|
||||
dismissingSearchFragment=true;
|
||||
searchFragmentContainer.animate().translationX(V.dp(100)).alpha(0).setDuration(200).setInterpolator(CubicBezierInterpolator.DEFAULT).withEndAction(()->{
|
||||
getChildFragmentManager().beginTransaction().remove(searchFragment).commit();
|
||||
getChildFragmentManager().executePendingTransactions();
|
||||
fragmentContentWrap.removeView(searchFragmentContainer);
|
||||
searchFragmentContainer=null;
|
||||
searchFragment=null;
|
||||
dismissingSearchFragment=false;
|
||||
}).start();
|
||||
}
|
||||
|
||||
private void onNextClick(View v){
|
||||
E.post(new FinishListCreationFragmentEvent(accountID, followList.id));
|
||||
Nav.finish(this);
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean onBackPressed(){
|
||||
if(searchFragment!=null){
|
||||
dismissSearchFragment();
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean isAccountInList(AccountViewModel account){
|
||||
return accountIDsInList.contains(account.account.id);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void addAccountToList(AccountViewModel account, Runnable onDone){
|
||||
new AddAccountsToList(followList.id, Set.of(account.account.id))
|
||||
.setCallback(new Callback<>(){
|
||||
@Override
|
||||
public void onSuccess(Void result){
|
||||
accountIDsInList.add(account.account.id);
|
||||
if(onDone!=null)
|
||||
onDone.run();
|
||||
int i=0;
|
||||
for(AccountViewModel acc:data){
|
||||
if(acc.account.id.equals(account.account.id)){
|
||||
list.getAdapter().notifyItemChanged(i);
|
||||
return;
|
||||
}
|
||||
i++;
|
||||
}
|
||||
int pos=data.size();
|
||||
data.add(account);
|
||||
list.getAdapter().notifyItemInserted(pos);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onError(ErrorResponse error){
|
||||
error.showToast(getActivity());
|
||||
}
|
||||
})
|
||||
.exec(accountID);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void removeAccountAccountFromList(AccountViewModel account, Runnable onDone){
|
||||
new RemoveAccountsFromList(followList.id, Set.of(account.account.id))
|
||||
.setCallback(new Callback<>(){
|
||||
@Override
|
||||
public void onSuccess(Void result){
|
||||
accountIDsInList.remove(account.account.id);
|
||||
if(onDone!=null)
|
||||
onDone.run();
|
||||
int i=0;
|
||||
for(AccountViewModel acc:data){
|
||||
if(acc.account.id.equals(account.account.id)){
|
||||
list.getAdapter().notifyItemChanged(i);
|
||||
return;
|
||||
}
|
||||
i++;
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onError(ErrorResponse error){
|
||||
error.showToast(getActivity());
|
||||
}
|
||||
})
|
||||
.exec(accountID);
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void onConfigureViewHolder(AccountViewHolder holder){
|
||||
holder.setStyle(AccountViewHolder.AccessoryType.CUSTOM_BUTTON, false);
|
||||
Button button=holder.getButton();
|
||||
button.setPadding(V.dp(24), 0, V.dp(24), 0);
|
||||
button.setMinimumWidth(0);
|
||||
button.setMinWidth(0);
|
||||
button.setOnClickListener(v->{
|
||||
holder.setActionProgressVisible(true);
|
||||
holder.itemView.setHasTransientState(true);
|
||||
Runnable onDone=()->{
|
||||
holder.setActionProgressVisible(false);
|
||||
holder.itemView.setHasTransientState(false);
|
||||
};
|
||||
AccountViewModel account=holder.getItem();
|
||||
if(isAccountInList(account)){
|
||||
removeAccountAccountFromList(account, onDone);
|
||||
}else{
|
||||
addAccountToList(account, onDone);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void onBindViewHolder(AccountViewHolder holder){
|
||||
Button button=holder.getButton();
|
||||
int textRes, styleRes;
|
||||
if(isAccountInList(holder.getItem())){
|
||||
textRes=R.string.remove;
|
||||
styleRes=R.style.Widget_Mastodon_M3_Button_Tonal_Error;
|
||||
}else{
|
||||
textRes=R.string.add;
|
||||
styleRes=R.style.Widget_Mastodon_M3_Button_Filled;
|
||||
}
|
||||
button.setText(textRes);
|
||||
TypedArray ta=button.getContext().obtainStyledAttributes(styleRes, new int[]{android.R.attr.background});
|
||||
button.setBackground(ta.getDrawable(0));
|
||||
ta.recycle();
|
||||
ta=button.getContext().obtainStyledAttributes(styleRes, new int[]{android.R.attr.textColor});
|
||||
button.setTextColor(ta.getColorStateList(0));
|
||||
ta.recycle();
|
||||
}
|
||||
}
|
@ -0,0 +1,140 @@
|
||||
package org.joinmastodon.android.fragments;
|
||||
|
||||
import android.os.Bundle;
|
||||
import android.text.TextUtils;
|
||||
import android.view.View;
|
||||
import android.view.WindowInsets;
|
||||
import android.widget.Button;
|
||||
|
||||
import com.squareup.otto.Subscribe;
|
||||
|
||||
import org.joinmastodon.android.E;
|
||||
import org.joinmastodon.android.R;
|
||||
import org.joinmastodon.android.api.requests.lists.CreateList;
|
||||
import org.joinmastodon.android.api.requests.lists.UpdateList;
|
||||
import org.joinmastodon.android.events.FinishListCreationFragmentEvent;
|
||||
import org.joinmastodon.android.model.FollowList;
|
||||
import org.joinmastodon.android.ui.utils.UiUtils;
|
||||
import org.parceler.Parcels;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
import me.grishka.appkit.Nav;
|
||||
import me.grishka.appkit.api.Callback;
|
||||
import me.grishka.appkit.api.ErrorResponse;
|
||||
|
||||
public class CreateListFragment extends BaseEditListFragment{
|
||||
private Button nextButton;
|
||||
private View buttonBar;
|
||||
private FollowList followList;
|
||||
|
||||
@Override
|
||||
public void onCreate(Bundle savedInstanceState){
|
||||
super.onCreate(savedInstanceState);
|
||||
setTitle(R.string.create_list);
|
||||
setSubtitle(getString(R.string.step_x_of_y, 1, 2));
|
||||
setLayout(R.layout.fragment_login);
|
||||
if(savedInstanceState!=null)
|
||||
followList=Parcels.unwrap(savedInstanceState.getParcelable("list"));
|
||||
E.register(this);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onDestroy(){
|
||||
super.onDestroy();
|
||||
E.unregister(this);
|
||||
}
|
||||
|
||||
@Override
|
||||
protected int getNavigationIconDrawableResource(){
|
||||
return R.drawable.ic_baseline_close_24;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean wantsCustomNavigationIcon(){
|
||||
return true;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onViewCreated(View view, Bundle savedInstanceState){
|
||||
nextButton=view.findViewById(R.id.btn_next);
|
||||
nextButton.setOnClickListener(this::onNextClick);
|
||||
nextButton.setText(R.string.create);
|
||||
buttonBar=view.findViewById(R.id.button_bar);
|
||||
super.onViewCreated(view, savedInstanceState);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onApplyWindowInsets(WindowInsets insets){
|
||||
super.onApplyWindowInsets(UiUtils.applyBottomInsetToFixedView(buttonBar, insets));
|
||||
}
|
||||
|
||||
@Override
|
||||
protected List<View> getViewsForElevationEffect(){
|
||||
return List.of(getToolbar(), buttonBar);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onSaveInstanceState(Bundle outState){
|
||||
super.onSaveInstanceState(outState);
|
||||
outState.putParcelable("list", Parcels.wrap(followList));
|
||||
}
|
||||
|
||||
private void onNextClick(View v){
|
||||
String title=titleEdit.getText().toString().trim();
|
||||
if(TextUtils.isEmpty(title)){
|
||||
titleEditLayout.setErrorState(getString(R.string.required_form_field_blank));
|
||||
return;
|
||||
}
|
||||
if(followList==null){
|
||||
new CreateList(title, getSelectedRepliesPolicy(), exclusiveItem.checked)
|
||||
.setCallback(new Callback<>(){
|
||||
@Override
|
||||
public void onSuccess(FollowList result){
|
||||
followList=result;
|
||||
proceed(false);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onError(ErrorResponse error){
|
||||
error.showToast(getActivity());
|
||||
}
|
||||
})
|
||||
.wrapProgress(getActivity(), R.string.loading, true)
|
||||
.exec(accountID);
|
||||
}else if(!title.equals(followList.title) || getSelectedRepliesPolicy()!=followList.repliesPolicy || exclusiveItem.checked!=followList.exclusive){
|
||||
new UpdateList(followList.id, title, getSelectedRepliesPolicy(), exclusiveItem.checked)
|
||||
.setCallback(new Callback<>(){
|
||||
@Override
|
||||
public void onSuccess(FollowList result){
|
||||
followList=result;
|
||||
proceed(true);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onError(ErrorResponse error){
|
||||
error.showToast(getActivity());
|
||||
}
|
||||
})
|
||||
.wrapProgress(getActivity(), R.string.loading, true)
|
||||
.exec(accountID);
|
||||
}else{
|
||||
proceed(true);
|
||||
}
|
||||
}
|
||||
|
||||
private void proceed(boolean needLoadMembers){
|
||||
Bundle args=new Bundle();
|
||||
args.putString("account", accountID);
|
||||
args.putParcelable("list", Parcels.wrap(followList));
|
||||
args.putBoolean("needLoadMembers", needLoadMembers);
|
||||
Nav.go(getActivity(), CreateListAddMembersFragment.class, args);
|
||||
}
|
||||
|
||||
@Subscribe
|
||||
public void onFinishListCreationFragment(FinishListCreationFragmentEvent ev){
|
||||
if(ev.accountID.equals(accountID) && followList!=null && ev.listID.equals(followList.id)){
|
||||
Nav.finish(this);
|
||||
}
|
||||
}
|
||||
}
|
@ -1,138 +1,29 @@
|
||||
package org.joinmastodon.android.fragments;
|
||||
|
||||
import android.app.Activity;
|
||||
import android.os.Bundle;
|
||||
import android.view.Menu;
|
||||
import android.view.MenuInflater;
|
||||
import android.view.MenuItem;
|
||||
import android.view.ViewGroup;
|
||||
import android.widget.ArrayAdapter;
|
||||
import android.widget.EditText;
|
||||
import android.widget.LinearLayout;
|
||||
import android.widget.Spinner;
|
||||
|
||||
import org.joinmastodon.android.E;
|
||||
import org.joinmastodon.android.R;
|
||||
import org.joinmastodon.android.api.requests.lists.DeleteList;
|
||||
import org.joinmastodon.android.api.requests.lists.GetListAccounts;
|
||||
import org.joinmastodon.android.api.requests.lists.UpdateList;
|
||||
import org.joinmastodon.android.events.ListDeletedEvent;
|
||||
import org.joinmastodon.android.events.ListUpdatedEvent;
|
||||
import org.joinmastodon.android.fragments.settings.BaseSettingsFragment;
|
||||
import org.joinmastodon.android.model.Account;
|
||||
import org.joinmastodon.android.model.FollowList;
|
||||
import org.joinmastodon.android.model.HeaderPaginationList;
|
||||
import org.joinmastodon.android.model.viewmodel.AvatarPileListItem;
|
||||
import org.joinmastodon.android.model.viewmodel.CheckableListItem;
|
||||
import org.joinmastodon.android.ui.M3AlertDialogBuilder;
|
||||
import org.joinmastodon.android.ui.views.FloatingHintEditTextLayout;
|
||||
import org.parceler.Parcels;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
|
||||
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.imageloader.requests.UrlImageLoaderRequest;
|
||||
import me.grishka.appkit.utils.MergeRecyclerAdapter;
|
||||
import me.grishka.appkit.utils.SingleViewRecyclerAdapter;
|
||||
import me.grishka.appkit.utils.V;
|
||||
|
||||
public class EditListFragment extends BaseSettingsFragment<Void>{
|
||||
private FollowList followList;
|
||||
private AvatarPileListItem<Void> membersItem;
|
||||
private CheckableListItem<Void> exclusiveItem;
|
||||
private FloatingHintEditTextLayout titleEditLayout;
|
||||
private EditText titleEdit;
|
||||
private Spinner showRepliesSpinner;
|
||||
private APIRequest<?> getMembersRequest;
|
||||
|
||||
public class EditListFragment extends BaseEditListFragment{
|
||||
@Override
|
||||
public void onCreate(Bundle savedInstanceState){
|
||||
super.onCreate(savedInstanceState);
|
||||
followList=Parcels.unwrap(getArguments().getParcelable("list"));
|
||||
setTitle(R.string.edit_list);
|
||||
onDataLoaded(List.of(
|
||||
membersItem=new AvatarPileListItem<>(getString(R.string.list_members), null, List.of(), 0, i->onMembersClick(), null, false),
|
||||
exclusiveItem=new CheckableListItem<>(R.string.list_exclusive, R.string.list_exclusive_subtitle, CheckableListItem.Style.SWITCH, followList.exclusive, this::toggleCheckableItem)
|
||||
));
|
||||
loadMembers();
|
||||
setHasOptionsMenu(true);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onDestroy(){
|
||||
super.onDestroy();
|
||||
if(getMembersRequest!=null)
|
||||
getMembersRequest.cancel();
|
||||
String newTitle=titleEdit.getText().toString();
|
||||
FollowList.RepliesPolicy newRepliesPolicy=FollowList.RepliesPolicy.values()[showRepliesSpinner.getSelectedItemPosition()];
|
||||
boolean newExclusive=exclusiveItem.checked;
|
||||
if(!newTitle.equals(followList.title) || newRepliesPolicy!=followList.repliesPolicy || newExclusive!=followList.exclusive){
|
||||
new UpdateList(followList.id, newTitle, newRepliesPolicy, newExclusive)
|
||||
.setCallback(new Callback<>(){
|
||||
@Override
|
||||
public void onSuccess(FollowList result){
|
||||
E.post(new ListUpdatedEvent(accountID, result));
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onError(ErrorResponse error){
|
||||
// TODO handle errors somehow
|
||||
}
|
||||
})
|
||||
.exec(accountID);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void doLoadData(int offset, int count){}
|
||||
|
||||
@Override
|
||||
protected RecyclerView.Adapter<?> getAdapter(){
|
||||
LinearLayout topView=new LinearLayout(getActivity());
|
||||
topView.setOrientation(LinearLayout.VERTICAL);
|
||||
|
||||
titleEditLayout=(FloatingHintEditTextLayout) getActivity().getLayoutInflater().inflate(R.layout.floating_hint_edit_text, topView, false);
|
||||
titleEdit=titleEditLayout.findViewById(R.id.edit);
|
||||
titleEdit.setHint(R.string.list_name);
|
||||
titleEditLayout.updateHint();
|
||||
if(followList!=null)
|
||||
titleEdit.setText(followList.title);
|
||||
topView.addView(titleEditLayout);
|
||||
|
||||
FloatingHintEditTextLayout showRepliesLayout=(FloatingHintEditTextLayout) getActivity().getLayoutInflater().inflate(R.layout.floating_hint_spinner, topView, false);
|
||||
showRepliesSpinner=showRepliesLayout.findViewById(R.id.spinner);
|
||||
showRepliesLayout.setHint(R.string.list_show_replies_to);
|
||||
topView.addView(showRepliesLayout);
|
||||
ArrayAdapter<String> spinnerAdapter=new ArrayAdapter<>(getActivity(), R.layout.item_spinner, List.of(
|
||||
getString(R.string.list_replies_no_one),
|
||||
getString(R.string.list_replies_members),
|
||||
getString(R.string.list_replies_anyone)
|
||||
));
|
||||
showRepliesSpinner.setAdapter(spinnerAdapter);
|
||||
showRepliesSpinner.setSelection(switch(followList.repliesPolicy){
|
||||
case FOLLOWED -> 2;
|
||||
case LIST -> 1;
|
||||
case NONE -> 0;
|
||||
});
|
||||
ViewGroup.MarginLayoutParams llp=(ViewGroup.MarginLayoutParams)showRepliesLayout.getLabel().getLayoutParams();
|
||||
llp.setMarginStart(llp.getMarginStart()+V.dp(16));
|
||||
|
||||
MergeRecyclerAdapter adapter=new MergeRecyclerAdapter();
|
||||
adapter.addAdapter(new SingleViewRecyclerAdapter(topView));
|
||||
adapter.addAdapter(super.getAdapter());
|
||||
return adapter;
|
||||
}
|
||||
|
||||
@Override
|
||||
protected int indexOfItemsAdapter(){
|
||||
return 1;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onCreateOptionsMenu(Menu menu, MenuInflater inflater){
|
||||
menu.add(R.string.delete_list);
|
||||
@ -149,54 +40,26 @@ public class EditListFragment extends BaseSettingsFragment<Void>{
|
||||
return true;
|
||||
}
|
||||
|
||||
private void doDeleteList(){
|
||||
new DeleteList(followList.id)
|
||||
.setCallback(new Callback<>(){
|
||||
@Override
|
||||
public void onSuccess(Void result){
|
||||
E.post(new ListDeletedEvent(accountID, followList.id));
|
||||
Nav.finish(EditListFragment.this);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onError(ErrorResponse error){
|
||||
Activity activity=getActivity();
|
||||
if(activity==null)
|
||||
return;
|
||||
error.showToast(activity);
|
||||
}
|
||||
})
|
||||
.wrapProgress(getActivity(), R.string.loading, true)
|
||||
.exec(accountID);
|
||||
}
|
||||
|
||||
private void onMembersClick(){
|
||||
Bundle args=new Bundle();
|
||||
args.putString("account", accountID);
|
||||
args.putParcelable("list", Parcels.wrap(followList));
|
||||
Nav.go(getActivity(), ListMembersFragment.class, args);
|
||||
}
|
||||
|
||||
private void loadMembers(){
|
||||
getMembersRequest=new GetListAccounts(followList.id, null, 3)
|
||||
.setCallback(new Callback<>(){
|
||||
@Override
|
||||
public void onSuccess(HeaderPaginationList<Account> result){
|
||||
getMembersRequest=null;
|
||||
membersItem.avatars=new ArrayList<>();
|
||||
for(int i=0;i<Math.min(3, result.size());i++){
|
||||
Account acc=result.get(i);
|
||||
membersItem.avatars.add(new UrlImageLoaderRequest(acc.avatarStatic, V.dp(32), V.dp(32)));
|
||||
@Override
|
||||
public void onDestroy(){
|
||||
super.onDestroy();
|
||||
String newTitle=titleEdit.getText().toString();
|
||||
FollowList.RepliesPolicy newRepliesPolicy=getSelectedRepliesPolicy();
|
||||
boolean newExclusive=exclusiveItem.checked;
|
||||
if(!newTitle.equals(followList.title) || newRepliesPolicy!=followList.repliesPolicy || newExclusive!=followList.exclusive){
|
||||
new UpdateList(followList.id, newTitle, newRepliesPolicy, newExclusive)
|
||||
.setCallback(new Callback<>(){
|
||||
@Override
|
||||
public void onSuccess(FollowList result){
|
||||
E.post(new ListUpdatedEvent(accountID, result));
|
||||
}
|
||||
rebindItem(membersItem);
|
||||
imgLoader.updateImages();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onError(ErrorResponse error){
|
||||
getMembersRequest=null;
|
||||
}
|
||||
})
|
||||
.exec(accountID);
|
||||
@Override
|
||||
public void onError(ErrorResponse error){
|
||||
// TODO handle errors somehow
|
||||
}
|
||||
})
|
||||
.exec(accountID);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -7,6 +7,7 @@ import android.animation.ObjectAnimator;
|
||||
import android.app.Activity;
|
||||
import android.content.res.Configuration;
|
||||
import android.os.Bundle;
|
||||
import android.text.TextUtils;
|
||||
import android.view.Gravity;
|
||||
import android.view.Menu;
|
||||
import android.view.MenuInflater;
|
||||
@ -473,6 +474,7 @@ public class HomeTimelineFragment extends StatusListFragment implements ToolbarD
|
||||
listsDropdownText=new TextView(getActivity());
|
||||
listsDropdownText.setTextAppearance(R.style.action_bar_title);
|
||||
listsDropdownText.setSingleLine();
|
||||
listsDropdownText.setEllipsize(TextUtils.TruncateAt.END);
|
||||
listsDropdownText.setGravity(Gravity.START | Gravity.CENTER_VERTICAL);
|
||||
listsDropdownText.setPaddingRelative(V.dp(4), 0, V.dp(16), 0);
|
||||
listsDropdownText.setText(getCurrentListTitle());
|
||||
|
@ -6,6 +6,7 @@ import android.view.Menu;
|
||||
import android.view.MenuInflater;
|
||||
import android.view.MenuItem;
|
||||
import android.view.View;
|
||||
import android.view.WindowInsets;
|
||||
import android.widget.ImageButton;
|
||||
|
||||
import com.squareup.otto.Subscribe;
|
||||
@ -29,7 +30,6 @@ import org.joinmastodon.android.ui.utils.UiUtils;
|
||||
import org.joinmastodon.android.ui.viewholders.AccountViewHolder;
|
||||
import org.parceler.Parcels;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.Collection;
|
||||
import java.util.HashSet;
|
||||
import java.util.List;
|
||||
@ -76,11 +76,6 @@ public class ListMembersFragment extends PaginatedAccountListFragment{
|
||||
return new GetListAccounts(followList.id, maxID, count);
|
||||
}
|
||||
|
||||
@Override
|
||||
protected boolean hasSubtitle(){
|
||||
return false;
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void onConfigureViewHolder(AccountViewHolder holder){
|
||||
super.onConfigureViewHolder(holder);
|
||||
@ -135,6 +130,12 @@ public class ListMembersFragment extends PaginatedAccountListFragment{
|
||||
fab.setOnClickListener(v->onFabClick());
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onApplyWindowInsets(WindowInsets insets){
|
||||
super.onApplyWindowInsets(insets);
|
||||
UiUtils.applyBottomInsetToFAB(fab, insets);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onFragmentResult(int reqCode, boolean success, Bundle result){
|
||||
if(reqCode==ADD_MEMBER_RESULT && success){
|
||||
|
@ -4,6 +4,9 @@ import android.app.Activity;
|
||||
import android.os.Bundle;
|
||||
import android.view.Menu;
|
||||
import android.view.MenuItem;
|
||||
import android.view.View;
|
||||
import android.view.WindowInsets;
|
||||
import android.widget.ImageButton;
|
||||
|
||||
import com.squareup.otto.Subscribe;
|
||||
|
||||
@ -18,6 +21,7 @@ import org.joinmastodon.android.model.FollowList;
|
||||
import org.joinmastodon.android.model.viewmodel.ListItem;
|
||||
import org.joinmastodon.android.model.viewmodel.ListItemWithOptionsMenu;
|
||||
import org.joinmastodon.android.ui.M3AlertDialogBuilder;
|
||||
import org.joinmastodon.android.ui.utils.UiUtils;
|
||||
import org.parceler.Parcels;
|
||||
|
||||
import java.util.List;
|
||||
@ -29,6 +33,12 @@ import me.grishka.appkit.api.ErrorResponse;
|
||||
import me.grishka.appkit.api.SimpleCallback;
|
||||
|
||||
public class ManageListsFragment extends BaseSettingsFragment<FollowList> implements ListItemWithOptionsMenu.OptionsMenuListener<FollowList>{
|
||||
private ImageButton fab;
|
||||
|
||||
public ManageListsFragment(){
|
||||
setListLayoutId(R.layout.recycler_fragment_with_fab);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onCreate(Bundle savedInstanceState){
|
||||
super.onCreate(savedInstanceState);
|
||||
@ -94,6 +104,21 @@ public class ManageListsFragment extends BaseSettingsFragment<FollowList> implem
|
||||
}
|
||||
}
|
||||
|
||||
@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.create_list));
|
||||
fab.setOnClickListener(v->onFabClick());
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onApplyWindowInsets(WindowInsets insets){
|
||||
super.onApplyWindowInsets(insets);
|
||||
UiUtils.applyBottomInsetToFAB(fab, insets);
|
||||
}
|
||||
|
||||
private void doDeleteList(FollowList list){
|
||||
new DeleteList(list.id)
|
||||
.setCallback(new Callback<>(){
|
||||
@ -149,4 +174,10 @@ public class ManageListsFragment extends BaseSettingsFragment<FollowList> implem
|
||||
i++;
|
||||
}
|
||||
}
|
||||
|
||||
private void onFabClick(){
|
||||
Bundle args=new Bundle();
|
||||
args.putString("account", accountID);
|
||||
Nav.go(getActivity(), CreateListFragment.class, args);
|
||||
}
|
||||
}
|
||||
|
@ -36,8 +36,14 @@ public abstract class MastodonRecyclerFragment<T> extends BaseRecyclerFragment<T
|
||||
@CallSuper
|
||||
public void onViewCreated(View view, Bundle savedInstanceState){
|
||||
super.onViewCreated(view, savedInstanceState);
|
||||
if(wantsElevationOnScrollEffect())
|
||||
list.addOnScrollListener(elevationOnScrollListener=new ElevationOnScrollListener((FragmentRootLinearLayout) view, getViewsForElevationEffect()));
|
||||
if(wantsElevationOnScrollEffect()){
|
||||
FragmentRootLinearLayout rootView;
|
||||
if(view instanceof FragmentRootLinearLayout frl)
|
||||
rootView=frl;
|
||||
else
|
||||
rootView=view.findViewById(R.id.appkit_loader_root);
|
||||
list.addOnScrollListener(elevationOnScrollListener=new ElevationOnScrollListener(rootView, getViewsForElevationEffect()));
|
||||
}
|
||||
list.setItemAnimator(new BetterItemAnimator());
|
||||
if(refreshLayout!=null){
|
||||
int colorBackground=UiUtils.getThemeColor(getActivity(), R.attr.colorM3Background);
|
||||
|
@ -31,7 +31,6 @@ public class AccountSearchFragment extends BaseAccountListFragment{
|
||||
super.onCreate(savedInstanceState);
|
||||
setRefreshEnabled(false);
|
||||
setEmptyText("");
|
||||
dataLoaded();
|
||||
}
|
||||
|
||||
@Override
|
||||
|
@ -1,5 +1,7 @@
|
||||
package org.joinmastodon.android.fragments.account_list;
|
||||
|
||||
import android.os.Bundle;
|
||||
|
||||
import org.joinmastodon.android.R;
|
||||
import org.joinmastodon.android.api.requests.accounts.SearchAccounts;
|
||||
import org.joinmastodon.android.model.Account;
|
||||
@ -9,6 +11,12 @@ import java.util.List;
|
||||
import me.grishka.appkit.api.SimpleCallback;
|
||||
|
||||
public class AddListMembersFragment extends AccountSearchFragment{
|
||||
@Override
|
||||
public void onCreate(Bundle savedInstanceState){
|
||||
super.onCreate(savedInstanceState);
|
||||
dataLoaded();
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void doLoadData(int offset, int count){
|
||||
refreshing=true;
|
||||
|
@ -0,0 +1,116 @@
|
||||
package org.joinmastodon.android.fragments.account_list;
|
||||
|
||||
import android.annotation.SuppressLint;
|
||||
import android.content.res.TypedArray;
|
||||
import android.os.Bundle;
|
||||
import android.text.TextUtils;
|
||||
import android.view.ViewGroup;
|
||||
import android.widget.Button;
|
||||
|
||||
import org.joinmastodon.android.R;
|
||||
import org.joinmastodon.android.api.requests.accounts.GetAccountFollowing;
|
||||
import org.joinmastodon.android.api.requests.accounts.SearchAccounts;
|
||||
import org.joinmastodon.android.api.session.AccountSessionManager;
|
||||
import org.joinmastodon.android.model.Account;
|
||||
import org.joinmastodon.android.model.HeaderPaginationList;
|
||||
import org.joinmastodon.android.model.viewmodel.AccountViewModel;
|
||||
import org.joinmastodon.android.ui.viewholders.AccountViewHolder;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
import me.grishka.appkit.api.SimpleCallback;
|
||||
import me.grishka.appkit.utils.V;
|
||||
|
||||
@SuppressLint("ValidFragment") // This shouldn't be part of any saved states anyway
|
||||
public class AddNewListMembersFragment extends AccountSearchFragment{
|
||||
private Listener listener;
|
||||
|
||||
public AddNewListMembersFragment(Listener listener){
|
||||
this.listener=listener;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onCreate(Bundle savedInstanceState){
|
||||
super.onCreate(savedInstanceState);
|
||||
loadData();
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void doLoadData(int offset, int count){
|
||||
if(TextUtils.isEmpty(currentQuery)){
|
||||
currentRequest=new GetAccountFollowing(AccountSessionManager.get(accountID).self.id, null, 10)
|
||||
.setCallback(new SimpleCallback<>(this){
|
||||
@Override
|
||||
public void onSuccess(HeaderPaginationList<Account> result){
|
||||
AddNewListMembersFragment.this.onSuccess(result);
|
||||
}
|
||||
})
|
||||
.exec(accountID);
|
||||
}else{
|
||||
refreshing=true;
|
||||
currentRequest=new SearchAccounts(currentQuery, 0, 0, false, true)
|
||||
.setCallback(new SimpleCallback<>(this){
|
||||
@Override
|
||||
public void onSuccess(List<Account> result){
|
||||
AddNewListMembersFragment.this.onSuccess(result);
|
||||
}
|
||||
})
|
||||
.exec(accountID);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
protected String getSearchViewPlaceholder(){
|
||||
return getString(R.string.search_among_people_you_follow);
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void onConfigureViewHolder(AccountViewHolder holder){
|
||||
holder.setStyle(AccountViewHolder.AccessoryType.CUSTOM_BUTTON, false);
|
||||
Button button=holder.getButton();
|
||||
button.setPadding(V.dp(24), 0, V.dp(24), 0);
|
||||
button.setMinimumWidth(0);
|
||||
button.setMinWidth(0);
|
||||
button.setOnClickListener(v->{
|
||||
holder.setActionProgressVisible(true);
|
||||
holder.itemView.setHasTransientState(true);
|
||||
Runnable onDone=()->{
|
||||
holder.setActionProgressVisible(false);
|
||||
holder.itemView.setHasTransientState(false);
|
||||
onBindViewHolder(holder);
|
||||
};
|
||||
AccountViewModel account=holder.getItem();
|
||||
if(listener.isAccountInList(account)){
|
||||
listener.removeAccountAccountFromList(account, onDone);
|
||||
}else{
|
||||
listener.addAccountToList(account, onDone);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void onBindViewHolder(AccountViewHolder holder){
|
||||
Button button=holder.getButton();
|
||||
int textRes, styleRes;
|
||||
if(listener.isAccountInList(holder.getItem())){
|
||||
textRes=R.string.remove;
|
||||
styleRes=R.style.Widget_Mastodon_M3_Button_Tonal_Error;
|
||||
}else{
|
||||
textRes=R.string.add;
|
||||
styleRes=R.style.Widget_Mastodon_M3_Button_Filled;
|
||||
}
|
||||
button.setText(textRes);
|
||||
TypedArray ta=button.getContext().obtainStyledAttributes(styleRes, new int[]{android.R.attr.background});
|
||||
button.setBackground(ta.getDrawable(0));
|
||||
ta.recycle();
|
||||
ta=button.getContext().obtainStyledAttributes(styleRes, new int[]{android.R.attr.textColor});
|
||||
button.setTextColor(ta.getColorStateList(0));
|
||||
ta.recycle();
|
||||
}
|
||||
|
||||
public interface Listener{
|
||||
boolean isAccountInList(AccountViewModel account);
|
||||
void addAccountToList(AccountViewModel account, Runnable onDone);
|
||||
void removeAccountAccountFromList(AccountViewModel account, Runnable onDone);
|
||||
}
|
||||
}
|
@ -73,6 +73,8 @@ public abstract class BaseAccountListFragment extends MastodonRecyclerFragment<A
|
||||
|
||||
protected void loadRelationships(List<AccountViewModel> accounts){
|
||||
Set<String> ids=accounts.stream().map(ai->ai.account.id).collect(Collectors.toSet());
|
||||
if(ids.isEmpty())
|
||||
return;
|
||||
GetAccountRelationships req=new GetAccountRelationships(ids);
|
||||
relationshipsRequests.add(req);
|
||||
req.setCallback(new Callback<>(){
|
||||
@ -122,20 +124,9 @@ public abstract class BaseAccountListFragment extends MastodonRecyclerFragment<A
|
||||
Toolbar toolbar=getToolbar();
|
||||
if(toolbar!=null && toolbar.getNavigationIcon()!=null){
|
||||
toolbar.setNavigationContentDescription(R.string.back);
|
||||
if(hasSubtitle()){
|
||||
toolbar.setTitleTextAppearance(getActivity(), R.style.m3_title_medium);
|
||||
toolbar.setSubtitleTextAppearance(getActivity(), R.style.m3_body_medium);
|
||||
int color=UiUtils.getThemeColor(getActivity(), android.R.attr.textColorPrimary);
|
||||
toolbar.setTitleTextColor(color);
|
||||
toolbar.setSubtitleTextColor(color);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
protected boolean hasSubtitle(){
|
||||
return true;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onApplyWindowInsets(WindowInsets insets){
|
||||
if(Build.VERSION.SDK_INT>=29 && insets.getTappableElementInsets().bottom==0){
|
||||
|
@ -14,8 +14,4 @@ public abstract class StatusRelatedAccountListFragment extends PaginatedAccountL
|
||||
status=Parcels.unwrap(getArguments().getParcelable("status"));
|
||||
}
|
||||
|
||||
@Override
|
||||
protected boolean hasSubtitle(){
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
@ -761,6 +761,17 @@ public class UiUtils{
|
||||
return insets;
|
||||
}
|
||||
|
||||
public static void applyBottomInsetToFAB(View fab, WindowInsets insets){
|
||||
int inset;
|
||||
if(Build.VERSION.SDK_INT>=29 && insets.getTappableElementInsets().bottom==0 /*&& wantsOverlaySystemNavigation()*/){
|
||||
int bottomInset=insets.getSystemWindowInsetBottom();
|
||||
inset=bottomInset>0 ? Math.max(V.dp(40), bottomInset) : 0;
|
||||
}else{
|
||||
inset=0;
|
||||
}
|
||||
((ViewGroup.MarginLayoutParams) fab.getLayoutParams()).bottomMargin=V.dp(16)+inset;
|
||||
}
|
||||
|
||||
public static String formatDuration(Context context, int seconds){
|
||||
if(seconds<3600){
|
||||
int minutes=seconds/60;
|
||||
|
@ -3,6 +3,7 @@ package org.joinmastodon.android.ui.viewcontrollers;
|
||||
import android.os.Bundle;
|
||||
|
||||
import org.joinmastodon.android.R;
|
||||
import org.joinmastodon.android.fragments.CreateListFragment;
|
||||
import org.joinmastodon.android.fragments.ManageListsFragment;
|
||||
import org.joinmastodon.android.model.FollowList;
|
||||
import org.joinmastodon.android.ui.utils.HideableSingleViewRecyclerAdapter;
|
||||
@ -25,7 +26,13 @@ public class HomeTimelineListsMenuController extends DropdownSubmenuController{
|
||||
for(FollowList l:lists){
|
||||
items.add(new Item<>(l.title, false, false, l, this::onListSelected));
|
||||
}
|
||||
items.add(new Item<Void>(dropdownController.getActivity().getString(R.string.manage_lists), false, true, i->{
|
||||
items.add(new Item<Void>(dropdownController.getActivity().getString(R.string.create_list), false, true, i->{
|
||||
dropdownController.dismiss();
|
||||
Bundle args=new Bundle();
|
||||
args.putString("account", dropdownController.getAccountID());
|
||||
Nav.go(dropdownController.getActivity(), CreateListFragment.class, args);
|
||||
}));
|
||||
items.add(new Item<Void>(dropdownController.getActivity().getString(R.string.manage_lists), false, false, i->{
|
||||
dropdownController.dismiss();
|
||||
Bundle args=new Bundle();
|
||||
args.putString("account", dropdownController.getAccountID());
|
||||
|
@ -210,7 +210,7 @@ public class AccountViewHolder extends BindableViewHolder<AccountViewModel> impl
|
||||
});
|
||||
}
|
||||
|
||||
private void setActionProgressVisible(boolean visible){
|
||||
public void setActionProgressVisible(boolean visible){
|
||||
if(visible)
|
||||
actionProgress.setIndeterminateTintList(button.getTextColors());
|
||||
button.setTextVisible(!visible);
|
||||
@ -312,7 +312,7 @@ public class AccountViewHolder extends BindableViewHolder<AccountViewModel> impl
|
||||
menuButton.setVisibility(View.GONE);
|
||||
checkbox.setBackground(new RadioButton(checkbox.getContext()).getButtonDrawable());
|
||||
}
|
||||
case BUTTON -> {
|
||||
case BUTTON, CUSTOM_BUTTON -> {
|
||||
button.setVisibility(View.VISIBLE);
|
||||
checkbox.setVisibility(View.GONE);
|
||||
menuButton.setVisibility(View.GONE);
|
||||
@ -381,11 +381,16 @@ public class AccountViewHolder extends BindableViewHolder<AccountViewModel> impl
|
||||
return contextMenu;
|
||||
}
|
||||
|
||||
public ProgressBarButton getButton(){
|
||||
return button;
|
||||
}
|
||||
|
||||
public enum AccessoryType{
|
||||
NONE,
|
||||
BUTTON,
|
||||
CHECKBOX,
|
||||
RADIOBUTTON,
|
||||
MENU
|
||||
MENU,
|
||||
CUSTOM_BUTTON
|
||||
}
|
||||
}
|
||||
|
@ -0,0 +1,146 @@
|
||||
package org.joinmastodon.android.ui.views;
|
||||
|
||||
import android.content.Context;
|
||||
import android.graphics.Canvas;
|
||||
import android.graphics.Matrix;
|
||||
import android.graphics.Paint;
|
||||
import android.graphics.Path;
|
||||
import android.graphics.PathMeasure;
|
||||
import android.graphics.Rect;
|
||||
import android.graphics.RectF;
|
||||
import android.util.AttributeSet;
|
||||
import android.view.Gravity;
|
||||
import android.view.View;
|
||||
import android.widget.LinearLayout;
|
||||
import android.widget.TextView;
|
||||
|
||||
import org.joinmastodon.android.R;
|
||||
import org.joinmastodon.android.ui.utils.UiUtils;
|
||||
|
||||
import me.grishka.appkit.utils.CustomViewHelper;
|
||||
|
||||
public class CurlyArrowEmptyView extends LinearLayout implements CustomViewHelper{
|
||||
private int pointGravity=Gravity.TOP | Gravity.END;
|
||||
private int pointOffsetX, pointOffsetY;
|
||||
private float startingPointX, startingPointY, endingPointX, endingPointY;
|
||||
private Path path=new Path(), arrowhead=new Path();
|
||||
private Paint paint=new Paint(Paint.ANTI_ALIAS_FLAG);
|
||||
private Rect tmpRect1=new Rect(), tmpRect2=new Rect();
|
||||
private Matrix matrix=new Matrix();
|
||||
|
||||
public CurlyArrowEmptyView(Context context){
|
||||
this(context, null);
|
||||
}
|
||||
|
||||
public CurlyArrowEmptyView(Context context, AttributeSet attrs){
|
||||
this(context, attrs, 0);
|
||||
}
|
||||
|
||||
public CurlyArrowEmptyView(Context context, AttributeSet attrs, int defStyle){
|
||||
super(context, attrs, defStyle);
|
||||
setWillNotDraw(false);
|
||||
|
||||
paint.setColor(UiUtils.getThemeColor(getContext(), R.attr.colorM3OutlineVariant));
|
||||
paint.setStyle(Paint.Style.STROKE);
|
||||
paint.setStrokeWidth(dp(3));
|
||||
paint.setStrokeCap(Paint.Cap.ROUND);
|
||||
paint.setStrokeJoin(Paint.Join.ROUND);
|
||||
|
||||
// 22x12
|
||||
arrowhead.moveTo(dp(-11), dp(12));
|
||||
arrowhead.lineTo(0, 0);
|
||||
arrowhead.lineTo(dp(11), dp(12));
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onViewAdded(View child){
|
||||
if(getChildCount()==1 && child instanceof TextView tv){
|
||||
tv.setShadowLayer(dp(5), 0, 0, UiUtils.getThemeColor(getContext(), R.attr.colorM3Surface));
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void onLayout(boolean changed, int l, int t, int r, int b){
|
||||
super.onLayout(changed, l, t, r, b);
|
||||
updatePoints();
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void onDraw(Canvas canvas){
|
||||
super.onDraw(canvas);
|
||||
canvas.drawPath(path, paint);
|
||||
}
|
||||
|
||||
private void updatePoints(){
|
||||
if(getChildCount()==0)
|
||||
return;
|
||||
View lastChild=getChildAt(getChildCount()-1);
|
||||
if(isRTL()){
|
||||
startingPointX=lastChild.getLeft()-dp(8);
|
||||
}else{
|
||||
startingPointX=lastChild.getRight()+dp(8);
|
||||
}
|
||||
startingPointY=lastChild.getTop()+lastChild.getHeight()/2f;
|
||||
|
||||
tmpRect1.set(0, 0, getWidth(), getHeight());
|
||||
tmpRect1.inset(pointOffsetX, pointOffsetY);
|
||||
Gravity.apply(pointGravity, 2, 2, tmpRect1, tmpRect2, getLayoutDirection());
|
||||
endingPointX=tmpRect2.centerX();
|
||||
endingPointY=tmpRect2.centerY();
|
||||
updatePath();
|
||||
}
|
||||
|
||||
public void setGravityAndOffsets(int gravity, int offsetX, int offsetY){
|
||||
pointGravity=gravity;
|
||||
pointOffsetX=dp(offsetX);
|
||||
pointOffsetY=dp(offsetY);
|
||||
updatePoints();
|
||||
}
|
||||
|
||||
private boolean isRTL(){
|
||||
return getLayoutDirection()==LAYOUT_DIRECTION_RTL;
|
||||
}
|
||||
|
||||
private void updatePath(){
|
||||
RectF src=new RectF(), dst=new RectF();
|
||||
dst.set(Math.min(startingPointX, endingPointX), Math.min(startingPointY, endingPointY), Math.max(startingPointX, endingPointX), Math.max(startingPointY, endingPointY));
|
||||
boolean isReversed=false;
|
||||
|
||||
path.rewind();
|
||||
path.moveTo(0.92f,292.1f);
|
||||
path.cubicTo(53.42f,283.6f,71.67f,171.4f,46.42f,148.09f);
|
||||
path.cubicTo(33.42f,136.09f,16.92f,137.59f,16.92f,156.09f);
|
||||
path.cubicTo(16.92f,175.59f,36.66f,193.48f,55.39f,186.59f);
|
||||
path.cubicTo(100.7f,169.94f,104.24f,38.38f,93.92f,1.1f);
|
||||
if(dst.width()>dst.height()){
|
||||
path.computeBounds(src, false);
|
||||
matrix.setRotate(90, src.centerX(), src.centerY());
|
||||
matrix.postScale(-1f, 1f, src.centerX(), src.centerY());
|
||||
path.transform(matrix);
|
||||
isReversed=true;
|
||||
}
|
||||
PathMeasure pm=new PathMeasure(path, false);
|
||||
float[] pos=new float[2], tan=new float[2];
|
||||
pm.getPosTan(isReversed ? pm.getLength() : 0, pos, null);
|
||||
src.left=pos[0];
|
||||
src.bottom=pos[1];
|
||||
pm.getPosTan(isReversed ? 0 : pm.getLength(), pos, null);
|
||||
src.right=pos[0];
|
||||
src.top=pos[1];
|
||||
|
||||
matrix.setRectToRect(src, dst, Matrix.ScaleToFit.FILL);
|
||||
if(startingPointX>endingPointX)
|
||||
matrix.postScale(-1f, 1f, dst.centerX(), dst.centerY());
|
||||
if(startingPointY<endingPointY)
|
||||
matrix.postScale(1f, -1f, dst.centerX(), dst.centerY());
|
||||
path.transform(matrix);
|
||||
pm.setPath(path, false);
|
||||
pm.getPosTan(isReversed ? 0 : pm.getLength(), pos, tan);
|
||||
matrix.setTranslate(pos[0], pos[1]);
|
||||
float angle=(float)Math.toDegrees(Math.atan2(tan[1], tan[0]))+90f;
|
||||
if(isReversed)
|
||||
angle+=180f;
|
||||
matrix.postRotate(angle%360f, pos[0], pos[1]);
|
||||
path.addPath(arrowhead, matrix);
|
||||
}
|
||||
}
|
30
mastodon/src/main/res/layout/empty_with_arrow.xml
Normal file
30
mastodon/src/main/res/layout/empty_with_arrow.xml
Normal file
@ -0,0 +1,30 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<org.joinmastodon.android.ui.views.CurlyArrowEmptyView xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
xmlns:tools="http://schemas.android.com/tools"
|
||||
android:orientation="vertical"
|
||||
android:gravity="center"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="match_parent"
|
||||
android:padding="16dp">
|
||||
|
||||
<TextView
|
||||
android:id="@+id/empty_text"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:gravity="center"
|
||||
android:textAppearance="@style/m3_title_large"
|
||||
android:fontFamily="sans-serif"
|
||||
android:textColor="?colorM3Outline"
|
||||
tools:text="@string/empty_list"/>
|
||||
|
||||
<TextView
|
||||
android:id="@+id/empty_text_secondary"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginTop="4dp"
|
||||
android:textAppearance="@style/m3_title_small"
|
||||
android:gravity="center_horizontal"
|
||||
android:textColor="?colorM3Outline"
|
||||
tools:text="Because there's nothing in it"/>
|
||||
|
||||
</org.joinmastodon.android.ui.views.CurlyArrowEmptyView>
|
@ -43,7 +43,7 @@
|
||||
android:layout_marginLeft="16dp"
|
||||
android:layout_marginRight="16dp"
|
||||
android:layout_marginTop="8dp"
|
||||
android:layout_marginBottom="16dp"
|
||||
android:layout_marginBottom="8dp"
|
||||
android:minWidth="145dp"
|
||||
style="@style/Widget.Mastodon.M3.Button.Filled"
|
||||
android:text="@string/next" />
|
||||
|
@ -650,4 +650,10 @@
|
||||
<item quantity="one">%,d post recently</item>
|
||||
<item quantity="other">%,d posts recently</item>
|
||||
</plurals>
|
||||
<string name="create_list">Create list</string>
|
||||
<string name="step_x_of_y">Step %1$d of %2$d</string>
|
||||
<string name="create">Create</string>
|
||||
<string name="manage_list_members">Manage list members</string>
|
||||
<string name="list_no_members">No members yet</string>
|
||||
<string name="list_find_users">Find users to add</string>
|
||||
</resources>
|
Loading…
x
Reference in New Issue
Block a user