parent
6c1c5b7759
commit
dff2217e80
@ -9,8 +9,8 @@ android {
|
||||
applicationId "org.joinmastodon.android"
|
||||
minSdk 23
|
||||
targetSdk 33
|
||||
versionCode 72
|
||||
versionName "2.1.6"
|
||||
versionCode 73
|
||||
versionName "2.2.0"
|
||||
testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner"
|
||||
resConfigs "ar-rSA", "be-rBY", "bn-rBD", "bs-rBA", "ca-rES", "cs-rCZ", "da-rDK", "de-rDE", "el-rGR", "es-rES", "eu-rES", "fa-rIR", "fi-rFI", "fil-rPH", "fr-rFR", "ga-rIE", "gd-rGB", "gl-rES", "hi-rIN", "hr-rHR", "hu-rHU", "hy-rAM", "ig-rNG", "in-rID", "is-rIS", "it-rIT", "iw-rIL", "ja-rJP", "kab", "ko-rKR", "my-rMM", "nl-rNL", "no-rNO", "oc-rFR", "pl-rPL", "pt-rBR", "pt-rPT", "ro-rRO", "ru-rRU", "si-rLK", "sl-rSI", "sv-rSE", "th-rTH", "tr-rTR", "uk-rUA", "ur-rIN", "vi-rVN", "zh-rCN", "zh-rTW"
|
||||
}
|
||||
@ -76,7 +76,7 @@ dependencies {
|
||||
implementation 'me.grishka.litex:viewpager:1.0.0'
|
||||
implementation 'me.grishka.litex:viewpager2:1.0.0'
|
||||
implementation 'me.grishka.litex:palette:1.0.0'
|
||||
implementation 'me.grishka.appkit:appkit:1.2.14'
|
||||
implementation 'me.grishka.appkit:appkit:1.2.15'
|
||||
implementation 'com.google.code.gson:gson:2.8.9'
|
||||
implementation 'org.jsoup:jsoup:1.14.3'
|
||||
implementation 'com.squareup:otto:1.3.8'
|
||||
|
@ -9,20 +9,31 @@ import android.os.Handler;
|
||||
import android.os.Looper;
|
||||
import android.util.Log;
|
||||
|
||||
import com.google.gson.reflect.TypeToken;
|
||||
|
||||
import org.joinmastodon.android.BuildConfig;
|
||||
import org.joinmastodon.android.MastodonApp;
|
||||
import org.joinmastodon.android.api.requests.lists.GetLists;
|
||||
import org.joinmastodon.android.api.requests.notifications.GetNotifications;
|
||||
import org.joinmastodon.android.api.requests.timelines.GetHomeTimeline;
|
||||
import org.joinmastodon.android.api.session.AccountSessionManager;
|
||||
import org.joinmastodon.android.model.CacheablePaginatedResponse;
|
||||
import org.joinmastodon.android.model.FilterContext;
|
||||
import org.joinmastodon.android.model.FollowList;
|
||||
import org.joinmastodon.android.model.Notification;
|
||||
import org.joinmastodon.android.model.PaginatedResponse;
|
||||
import org.joinmastodon.android.model.SearchResult;
|
||||
import org.joinmastodon.android.model.Status;
|
||||
import org.joinmastodon.android.ui.utils.UiUtils;
|
||||
|
||||
import java.io.File;
|
||||
import java.io.FileInputStream;
|
||||
import java.io.FileOutputStream;
|
||||
import java.io.IOException;
|
||||
import java.io.InputStreamReader;
|
||||
import java.io.OutputStreamWriter;
|
||||
import java.util.ArrayList;
|
||||
import java.util.Comparator;
|
||||
import java.util.EnumSet;
|
||||
import java.util.List;
|
||||
import java.util.function.Consumer;
|
||||
@ -42,6 +53,7 @@ public class CacheController{
|
||||
private final Runnable databaseCloseRunnable=this::closeDatabase;
|
||||
private boolean loadingNotifications;
|
||||
private final ArrayList<Callback<PaginatedResponse<List<Notification>>>> pendingNotificationsCallbacks=new ArrayList<>();
|
||||
private List<FollowList> lists;
|
||||
|
||||
private static final int POST_FLAG_GAP_AFTER=1;
|
||||
|
||||
@ -300,6 +312,67 @@ public class CacheController{
|
||||
}, 0);
|
||||
}
|
||||
|
||||
public void reloadLists(Callback<List<FollowList>> callback){
|
||||
new GetLists()
|
||||
.setCallback(new Callback<>(){
|
||||
@Override
|
||||
public void onSuccess(List<FollowList> result){
|
||||
result.sort(Comparator.comparing(l->l.title));
|
||||
lists=result;
|
||||
if(callback!=null)
|
||||
callback.onSuccess(result);
|
||||
databaseThread.postRunnable(()->{
|
||||
try(OutputStreamWriter out=new OutputStreamWriter(new FileOutputStream(getListsFile()))){
|
||||
MastodonAPIController.gson.toJson(result, out);
|
||||
}catch(IOException x){
|
||||
Log.w(TAG, "failed to write lists to cache file", x);
|
||||
}
|
||||
}, 0);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onError(ErrorResponse error){
|
||||
if(callback!=null)
|
||||
callback.onError(error);
|
||||
}
|
||||
})
|
||||
.exec(accountID);
|
||||
}
|
||||
|
||||
private List<FollowList> loadListsFromFile(){
|
||||
File file=getListsFile();
|
||||
if(!file.exists())
|
||||
return null;
|
||||
try(InputStreamReader in=new InputStreamReader(new FileInputStream(file))){
|
||||
return MastodonAPIController.gson.fromJson(in, new TypeToken<List<FollowList>>(){}.getType());
|
||||
}catch(Exception x){
|
||||
Log.w(TAG, "failed to read lists from cache file", x);
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
public void getLists(Callback<List<FollowList>> callback){
|
||||
if(lists!=null){
|
||||
if(callback!=null)
|
||||
callback.onSuccess(lists);
|
||||
return;
|
||||
}
|
||||
databaseThread.postRunnable(()->{
|
||||
List<FollowList> lists=loadListsFromFile();
|
||||
if(lists!=null){
|
||||
this.lists=lists;
|
||||
if(callback!=null)
|
||||
uiHandler.post(()->callback.onSuccess(lists));
|
||||
return;
|
||||
}
|
||||
reloadLists(callback);
|
||||
}, 0);
|
||||
}
|
||||
|
||||
public File getListsFile(){
|
||||
return new File(MastodonApp.context.getFilesDir(), "lists_"+accountID+".json");
|
||||
}
|
||||
|
||||
private class DatabaseHelper extends SQLiteOpenHelper{
|
||||
|
||||
public DatabaseHelper(){
|
||||
|
@ -154,6 +154,8 @@ public abstract class MastodonAPIRequest<T> extends APIRequest<T>{
|
||||
}
|
||||
|
||||
public RequestBody getRequestBody() throws IOException{
|
||||
if(requestBody instanceof RequestBody rb)
|
||||
return rb;
|
||||
return requestBody==null ? null : new JsonObjectRequestBody(requestBody);
|
||||
}
|
||||
|
||||
|
@ -0,0 +1,14 @@
|
||||
package org.joinmastodon.android.api.requests.accounts;
|
||||
|
||||
import com.google.gson.reflect.TypeToken;
|
||||
|
||||
import org.joinmastodon.android.api.MastodonAPIRequest;
|
||||
import org.joinmastodon.android.model.FollowList;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
public class GetAccountLists extends MastodonAPIRequest<List<FollowList>>{
|
||||
public GetAccountLists(String id){
|
||||
super(HttpMethod.GET, "/accounts/"+id+"/lists", new TypeToken<>(){});
|
||||
}
|
||||
}
|
@ -0,0 +1,23 @@
|
||||
package org.joinmastodon.android.api.requests.accounts;
|
||||
|
||||
import com.google.gson.reflect.TypeToken;
|
||||
|
||||
import org.joinmastodon.android.api.MastodonAPIRequest;
|
||||
import org.joinmastodon.android.model.Account;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
public class SearchAccounts extends MastodonAPIRequest<List<Account>>{
|
||||
public SearchAccounts(String q, int limit, int offset, boolean resolve, boolean following){
|
||||
super(HttpMethod.GET, "/accounts/search", new TypeToken<>(){});
|
||||
addQueryParameter("q", q);
|
||||
if(limit>0)
|
||||
addQueryParameter("limit", limit+"");
|
||||
if(offset>0)
|
||||
addQueryParameter("offset", offset+"");
|
||||
if(resolve)
|
||||
addQueryParameter("resolve", "true");
|
||||
if(following)
|
||||
addQueryParameter("following", "true");
|
||||
}
|
||||
}
|
@ -2,6 +2,9 @@ package org.joinmastodon.android.api.requests.filters;
|
||||
|
||||
import com.google.gson.annotations.SerializedName;
|
||||
|
||||
import androidx.annotation.Keep;
|
||||
|
||||
@Keep
|
||||
class KeywordAttribute{
|
||||
public String id;
|
||||
@SerializedName("_destroy")
|
||||
|
@ -0,0 +1,19 @@
|
||||
package org.joinmastodon.android.api.requests.lists;
|
||||
|
||||
import org.joinmastodon.android.api.ResultlessMastodonAPIRequest;
|
||||
|
||||
import java.nio.charset.StandardCharsets;
|
||||
import java.util.Collection;
|
||||
|
||||
import okhttp3.FormBody;
|
||||
|
||||
public class AddAccountsToList extends ResultlessMastodonAPIRequest{
|
||||
public AddAccountsToList(String listID, Collection<String> accountIDs){
|
||||
super(HttpMethod.POST, "/lists/"+listID+"/accounts");
|
||||
FormBody.Builder builder=new FormBody.Builder(StandardCharsets.UTF_8);
|
||||
for(String id:accountIDs){
|
||||
builder.add("account_ids[]", id);
|
||||
}
|
||||
setRequestBody(builder.build());
|
||||
}
|
||||
}
|
@ -0,0 +1,9 @@
|
||||
package org.joinmastodon.android.api.requests.lists;
|
||||
|
||||
import org.joinmastodon.android.api.ResultlessMastodonAPIRequest;
|
||||
|
||||
public class DeleteList extends ResultlessMastodonAPIRequest{
|
||||
public DeleteList(String id){
|
||||
super(HttpMethod.DELETE, "/lists/"+id);
|
||||
}
|
||||
}
|
@ -0,0 +1,17 @@
|
||||
package org.joinmastodon.android.api.requests.lists;
|
||||
|
||||
import android.text.TextUtils;
|
||||
|
||||
import com.google.gson.reflect.TypeToken;
|
||||
|
||||
import org.joinmastodon.android.api.requests.HeaderPaginationRequest;
|
||||
import org.joinmastodon.android.model.Account;
|
||||
|
||||
public class GetListAccounts extends HeaderPaginationRequest<Account>{
|
||||
public GetListAccounts(String listID, String maxID, int limit){
|
||||
super(HttpMethod.GET, "/lists/"+listID+"/accounts", new TypeToken<>(){});
|
||||
if(!TextUtils.isEmpty(maxID))
|
||||
addQueryParameter("max_id", maxID);
|
||||
addQueryParameter("limit", String.valueOf(limit));
|
||||
}
|
||||
}
|
@ -0,0 +1,14 @@
|
||||
package org.joinmastodon.android.api.requests.lists;
|
||||
|
||||
import com.google.gson.reflect.TypeToken;
|
||||
|
||||
import org.joinmastodon.android.api.MastodonAPIRequest;
|
||||
import org.joinmastodon.android.model.FollowList;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
public class GetLists extends MastodonAPIRequest<List<FollowList>>{
|
||||
public GetLists(){
|
||||
super(HttpMethod.GET, "/lists", new TypeToken<>(){});
|
||||
}
|
||||
}
|
@ -0,0 +1,19 @@
|
||||
package org.joinmastodon.android.api.requests.lists;
|
||||
|
||||
import org.joinmastodon.android.api.ResultlessMastodonAPIRequest;
|
||||
|
||||
import java.nio.charset.StandardCharsets;
|
||||
import java.util.Collection;
|
||||
|
||||
import okhttp3.FormBody;
|
||||
|
||||
public class RemoveAccountsFromList extends ResultlessMastodonAPIRequest{
|
||||
public RemoveAccountsFromList(String listID, Collection<String> accountIDs){
|
||||
super(HttpMethod.DELETE, "/lists/"+listID+"/accounts");
|
||||
FormBody.Builder builder=new FormBody.Builder(StandardCharsets.UTF_8);
|
||||
for(String id:accountIDs){
|
||||
builder.add("account_ids[]", id);
|
||||
}
|
||||
setRequestBody(builder.build());
|
||||
}
|
||||
}
|
@ -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 UpdateList extends MastodonAPIRequest<FollowList>{
|
||||
public UpdateList(String listID, String title, FollowList.RepliesPolicy repliesPolicy, boolean exclusive){
|
||||
super(HttpMethod.PUT, "/lists/"+listID, 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,16 @@
|
||||
package org.joinmastodon.android.api.requests.tags;
|
||||
|
||||
import com.google.gson.reflect.TypeToken;
|
||||
|
||||
import org.joinmastodon.android.api.requests.HeaderPaginationRequest;
|
||||
import org.joinmastodon.android.model.Hashtag;
|
||||
|
||||
public class GetFollowedTags extends HeaderPaginationRequest<Hashtag>{
|
||||
public GetFollowedTags(String maxID, int limit){
|
||||
super(HttpMethod.GET, "/followed_tags", new TypeToken<>(){});
|
||||
if(maxID!=null)
|
||||
addQueryParameter("max_id", maxID);
|
||||
if(limit>0)
|
||||
addQueryParameter("limit", limit+"");
|
||||
}
|
||||
}
|
@ -0,0 +1,22 @@
|
||||
package org.joinmastodon.android.api.requests.timelines;
|
||||
|
||||
import com.google.gson.reflect.TypeToken;
|
||||
|
||||
import org.joinmastodon.android.api.MastodonAPIRequest;
|
||||
import org.joinmastodon.android.model.Status;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
public class GetListTimeline extends MastodonAPIRequest<List<Status>>{
|
||||
public GetListTimeline(String listID, String maxID, String minID, int limit, String sinceID){
|
||||
super(HttpMethod.GET, "/timelines/list/"+listID, new TypeToken<>(){});
|
||||
if(maxID!=null)
|
||||
addQueryParameter("max_id", maxID);
|
||||
if(minID!=null)
|
||||
addQueryParameter("min_id", minID);
|
||||
if(limit>0)
|
||||
addQueryParameter("limit", ""+limit);
|
||||
if(sinceID!=null)
|
||||
addQueryParameter("since_id", sinceID);
|
||||
}
|
||||
}
|
@ -10,7 +10,7 @@ import org.joinmastodon.android.model.Status;
|
||||
import java.util.List;
|
||||
|
||||
public class GetPublicTimeline extends MastodonAPIRequest<List<Status>>{
|
||||
public GetPublicTimeline(boolean local, boolean remote, String maxID, int limit){
|
||||
public GetPublicTimeline(boolean local, boolean remote, String maxID, String minID, int limit, String sinceID){
|
||||
super(HttpMethod.GET, "/timelines/public", new TypeToken<>(){});
|
||||
if(local)
|
||||
addQueryParameter("local", "true");
|
||||
@ -18,6 +18,10 @@ public class GetPublicTimeline extends MastodonAPIRequest<List<Status>>{
|
||||
addQueryParameter("remote", "true");
|
||||
if(!TextUtils.isEmpty(maxID))
|
||||
addQueryParameter("max_id", maxID);
|
||||
if(!TextUtils.isEmpty(minID))
|
||||
addQueryParameter("min_id", minID);
|
||||
if(!TextUtils.isEmpty(sinceID))
|
||||
addQueryParameter("since_id", sinceID);
|
||||
if(limit>0)
|
||||
addQueryParameter("limit", limit+"");
|
||||
}
|
||||
|
@ -24,6 +24,7 @@ import org.joinmastodon.android.model.Application;
|
||||
import org.joinmastodon.android.model.FilterAction;
|
||||
import org.joinmastodon.android.model.FilterContext;
|
||||
import org.joinmastodon.android.model.FilterResult;
|
||||
import org.joinmastodon.android.model.FollowList;
|
||||
import org.joinmastodon.android.model.LegacyFilter;
|
||||
import org.joinmastodon.android.model.Preferences;
|
||||
import org.joinmastodon.android.model.PushSubscription;
|
||||
@ -32,6 +33,7 @@ import org.joinmastodon.android.model.TimelineMarkers;
|
||||
import org.joinmastodon.android.model.Token;
|
||||
import org.joinmastodon.android.utils.ObjectIdComparator;
|
||||
|
||||
import java.io.File;
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
import java.util.function.Consumer;
|
||||
@ -66,6 +68,7 @@ public class AccountSession{
|
||||
private transient SharedPreferences prefs;
|
||||
private transient boolean preferencesNeedSaving;
|
||||
private transient AccountLocalPreferences localPreferences;
|
||||
private transient List<FollowList> lists;
|
||||
|
||||
AccountSession(Token token, Account self, Application app, String domain, boolean activated, AccountActivationInfo activationInfo){
|
||||
this.token=token;
|
||||
|
@ -175,6 +175,7 @@ public class AccountSessionManager{
|
||||
public void removeAccount(String id){
|
||||
AccountSession session=getAccount(id);
|
||||
session.getCacheController().closeDatabase();
|
||||
session.getCacheController().getListsFile().delete();
|
||||
MastodonApp.context.deleteDatabase(id+".db");
|
||||
MastodonApp.context.getSharedPreferences(id, 0).edit().clear().commit();
|
||||
if(Build.VERSION.SDK_INT>=Build.VERSION_CODES.N){
|
||||
|
@ -0,0 +1,15 @@
|
||||
package org.joinmastodon.android.events;
|
||||
|
||||
import org.joinmastodon.android.model.Account;
|
||||
|
||||
public class AccountAddedToListEvent{
|
||||
public final String accountID;
|
||||
public final String listID;
|
||||
public final Account account;
|
||||
|
||||
public AccountAddedToListEvent(String accountID, String listID, Account account){
|
||||
this.accountID=accountID;
|
||||
this.listID=listID;
|
||||
this.account=account;
|
||||
}
|
||||
}
|
@ -0,0 +1,13 @@
|
||||
package org.joinmastodon.android.events;
|
||||
|
||||
public class AccountRemovedFromListEvent{
|
||||
public final String accountID;
|
||||
public final String listID;
|
||||
public final String targetAccountID;
|
||||
|
||||
public AccountRemovedFromListEvent(String accountID, String listID, String targetAccountID){
|
||||
this.accountID=accountID;
|
||||
this.listID=listID;
|
||||
this.targetAccountID=targetAccountID;
|
||||
}
|
||||
}
|
@ -0,0 +1,11 @@
|
||||
package org.joinmastodon.android.events;
|
||||
|
||||
public class ListDeletedEvent{
|
||||
public final String accountID;
|
||||
public final String listID;
|
||||
|
||||
public ListDeletedEvent(String accountID, String listID){
|
||||
this.accountID=accountID;
|
||||
this.listID=listID;
|
||||
}
|
||||
}
|
@ -0,0 +1,13 @@
|
||||
package org.joinmastodon.android.events;
|
||||
|
||||
import org.joinmastodon.android.model.FollowList;
|
||||
|
||||
public class ListUpdatedEvent{
|
||||
public final String accountID;
|
||||
public final FollowList list;
|
||||
|
||||
public ListUpdatedEvent(String accountID, FollowList list){
|
||||
this.accountID=accountID;
|
||||
this.list=list;
|
||||
}
|
||||
}
|
@ -0,0 +1,114 @@
|
||||
package org.joinmastodon.android.fragments;
|
||||
|
||||
import android.os.Bundle;
|
||||
import android.widget.TextView;
|
||||
|
||||
import org.joinmastodon.android.E;
|
||||
import org.joinmastodon.android.R;
|
||||
import org.joinmastodon.android.api.ResultlessMastodonAPIRequest;
|
||||
import org.joinmastodon.android.api.requests.accounts.GetAccountLists;
|
||||
import org.joinmastodon.android.api.requests.lists.AddAccountsToList;
|
||||
import org.joinmastodon.android.api.requests.lists.RemoveAccountsFromList;
|
||||
import org.joinmastodon.android.api.session.AccountSessionManager;
|
||||
import org.joinmastodon.android.events.AccountAddedToListEvent;
|
||||
import org.joinmastodon.android.events.AccountRemovedFromListEvent;
|
||||
import org.joinmastodon.android.fragments.settings.BaseSettingsFragment;
|
||||
import org.joinmastodon.android.model.Account;
|
||||
import org.joinmastodon.android.model.FollowList;
|
||||
import org.joinmastodon.android.model.viewmodel.CheckableListItem;
|
||||
import org.joinmastodon.android.ui.utils.UiUtils;
|
||||
import org.parceler.Parcels;
|
||||
|
||||
import java.util.List;
|
||||
import java.util.Set;
|
||||
import java.util.stream.Collectors;
|
||||
|
||||
import androidx.recyclerview.widget.RecyclerView;
|
||||
import me.grishka.appkit.api.Callback;
|
||||
import me.grishka.appkit.api.ErrorResponse;
|
||||
import me.grishka.appkit.api.SimpleCallback;
|
||||
import me.grishka.appkit.utils.MergeRecyclerAdapter;
|
||||
import me.grishka.appkit.utils.SingleViewRecyclerAdapter;
|
||||
import me.grishka.appkit.utils.V;
|
||||
|
||||
public class AddAccountToListsFragment extends BaseSettingsFragment<FollowList>{
|
||||
private Account account;
|
||||
|
||||
@Override
|
||||
public void onCreate(Bundle savedInstanceState){
|
||||
super.onCreate(savedInstanceState);
|
||||
setTitle(R.string.add_user_to_list_title);
|
||||
account=Parcels.unwrap(getArguments().getParcelable("targetAccount"));
|
||||
loadData();
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void doLoadData(int offset, int count){
|
||||
AccountSessionManager.get(accountID).getCacheController().getLists(new SimpleCallback<>(this){
|
||||
@Override
|
||||
public void onSuccess(List<FollowList> allLists){
|
||||
if(getActivity()==null)
|
||||
return;
|
||||
loadAccountLists(allLists);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
private void loadAccountLists(final List<FollowList> allLists){
|
||||
currentRequest=new GetAccountLists(account.id)
|
||||
.setCallback(new SimpleCallback<>(this){
|
||||
@Override
|
||||
public void onSuccess(List<FollowList> result){
|
||||
Set<String> lists=result.stream().map(l->l.id).collect(Collectors.toSet());
|
||||
onDataLoaded(allLists.stream()
|
||||
.map(l->new CheckableListItem<>(l.title, null, CheckableListItem.Style.CHECKBOX, lists.contains(l.id),
|
||||
R.drawable.ic_list_alt_24px, AddAccountToListsFragment.this::onItemClick, l))
|
||||
.collect(Collectors.toList()), false);
|
||||
}
|
||||
})
|
||||
.exec(accountID);
|
||||
}
|
||||
|
||||
@Override
|
||||
protected int indexOfItemsAdapter(){
|
||||
return 1;
|
||||
}
|
||||
|
||||
@Override
|
||||
protected RecyclerView.Adapter<?> getAdapter(){
|
||||
TextView topText=new TextView(getActivity());
|
||||
topText.setTextAppearance(R.style.m3_body_medium);
|
||||
topText.setTextColor(UiUtils.getThemeColor(getActivity(), R.attr.colorM3OnSurface));
|
||||
topText.setPadding(V.dp(16), V.dp(8), V.dp(16), V.dp(8));
|
||||
topText.setText(getString(R.string.manage_user_lists, account.getDisplayUsername()));
|
||||
|
||||
MergeRecyclerAdapter mergeAdapter=new MergeRecyclerAdapter();
|
||||
mergeAdapter.addAdapter(new SingleViewRecyclerAdapter(topText));
|
||||
mergeAdapter.addAdapter(super.getAdapter());
|
||||
return mergeAdapter;
|
||||
}
|
||||
|
||||
private void onItemClick(CheckableListItem<FollowList> item){
|
||||
boolean add=!item.checked;
|
||||
ResultlessMastodonAPIRequest req=add ? new AddAccountsToList(item.parentObject.id, Set.of(account.id)) : new RemoveAccountsFromList(item.parentObject.id, Set.of(account.id));
|
||||
req.setCallback(new Callback<>(){
|
||||
@Override
|
||||
public void onSuccess(Void result){
|
||||
item.checked=add;
|
||||
rebindItem(item);
|
||||
if(add){
|
||||
E.post(new AccountAddedToListEvent(accountID, item.parentObject.id, account));
|
||||
}else{
|
||||
E.post(new AccountRemovedFromListEvent(accountID, item.parentObject.id, account.id));
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onError(ErrorResponse error){
|
||||
error.showToast(getActivity());
|
||||
}
|
||||
})
|
||||
.wrapProgress(getActivity(), R.string.loading, false)
|
||||
.exec(accountID);
|
||||
}
|
||||
}
|
@ -19,17 +19,14 @@ import android.text.TextUtils;
|
||||
import android.text.TextWatcher;
|
||||
import android.text.style.BackgroundColorSpan;
|
||||
import android.text.style.ForegroundColorSpan;
|
||||
import android.util.Log;
|
||||
import android.view.KeyEvent;
|
||||
import android.view.LayoutInflater;
|
||||
import android.view.Menu;
|
||||
import android.view.MenuInflater;
|
||||
import android.view.MenuItem;
|
||||
import android.view.SoundEffectConstants;
|
||||
import android.view.View;
|
||||
import android.view.ViewGroup;
|
||||
import android.view.ViewOutlineProvider;
|
||||
import android.view.ViewTreeObserver;
|
||||
import android.view.WindowManager;
|
||||
import android.view.inputmethod.InputConnection;
|
||||
import android.view.inputmethod.InputMethodManager;
|
||||
@ -49,7 +46,6 @@ 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;
|
||||
import org.joinmastodon.android.api.requests.statuses.CreateStatus;
|
||||
import org.joinmastodon.android.api.requests.statuses.EditStatus;
|
||||
import org.joinmastodon.android.api.session.AccountSession;
|
||||
@ -57,7 +53,7 @@ import org.joinmastodon.android.api.session.AccountSessionManager;
|
||||
import org.joinmastodon.android.events.StatusCountersUpdatedEvent;
|
||||
import org.joinmastodon.android.events.StatusCreatedEvent;
|
||||
import org.joinmastodon.android.events.StatusUpdatedEvent;
|
||||
import org.joinmastodon.android.fragments.account_list.ComposeAccountSearchFragment;
|
||||
import org.joinmastodon.android.fragments.account_list.AccountSearchFragment;
|
||||
import org.joinmastodon.android.model.Account;
|
||||
import org.joinmastodon.android.model.Emoji;
|
||||
import org.joinmastodon.android.model.EmojiCategory;
|
||||
@ -340,7 +336,7 @@ public class ComposeFragment extends MastodonToolbarFragment implements OnBackPr
|
||||
public void onLaunchAccountSearch(){
|
||||
Bundle args=new Bundle();
|
||||
args.putString("account", accountID);
|
||||
Nav.goForResult(getActivity(), ComposeAccountSearchFragment.class, args, AUTOCOMPLETE_ACCOUNT_RESULT, ComposeFragment.this);
|
||||
Nav.goForResult(getActivity(), AccountSearchFragment.class, args, AUTOCOMPLETE_ACCOUNT_RESULT, ComposeFragment.this);
|
||||
}
|
||||
});
|
||||
View autocompleteView=autocompleteViewController.getView();
|
||||
|
@ -0,0 +1,202 @@
|
||||
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;
|
||||
|
||||
@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);
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean onOptionsItemSelected(MenuItem item){
|
||||
new M3AlertDialogBuilder(getActivity())
|
||||
.setTitle(R.string.delete_list)
|
||||
.setMessage(getString(R.string.delete_list_confirm, followList.title))
|
||||
.setPositiveButton(R.string.delete, (dlg, which)->doDeleteList())
|
||||
.setNegativeButton(R.string.cancel, null)
|
||||
.show();
|
||||
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)));
|
||||
}
|
||||
rebindItem(membersItem);
|
||||
imgLoader.updateImages();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onError(ErrorResponse error){
|
||||
getMembersRequest=null;
|
||||
}
|
||||
})
|
||||
.exec(accountID);
|
||||
}
|
||||
}
|
@ -5,7 +5,6 @@ import android.animation.AnimatorListenerAdapter;
|
||||
import android.animation.AnimatorSet;
|
||||
import android.animation.ObjectAnimator;
|
||||
import android.app.Activity;
|
||||
import android.content.res.ColorStateList;
|
||||
import android.content.res.Configuration;
|
||||
import android.os.Build;
|
||||
import android.os.Bundle;
|
||||
@ -15,30 +14,40 @@ import android.view.MenuInflater;
|
||||
import android.view.MenuItem;
|
||||
import android.view.View;
|
||||
import android.view.ViewGroup;
|
||||
import android.view.accessibility.AccessibilityNodeInfo;
|
||||
import android.widget.Button;
|
||||
import android.widget.FrameLayout;
|
||||
import android.widget.ImageButton;
|
||||
import android.widget.ImageView;
|
||||
import android.widget.LinearLayout;
|
||||
import android.widget.TextView;
|
||||
import android.widget.Toolbar;
|
||||
|
||||
import com.squareup.otto.Subscribe;
|
||||
|
||||
import org.joinmastodon.android.E;
|
||||
import org.joinmastodon.android.R;
|
||||
import org.joinmastodon.android.api.MastodonAPIRequest;
|
||||
import org.joinmastodon.android.api.requests.markers.SaveMarkers;
|
||||
import org.joinmastodon.android.api.requests.timelines.GetHomeTimeline;
|
||||
import org.joinmastodon.android.api.requests.timelines.GetListTimeline;
|
||||
import org.joinmastodon.android.api.requests.timelines.GetPublicTimeline;
|
||||
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.FilterContext;
|
||||
import org.joinmastodon.android.model.FollowList;
|
||||
import org.joinmastodon.android.model.Status;
|
||||
import org.joinmastodon.android.model.TimelineMarkers;
|
||||
import org.joinmastodon.android.ui.displayitems.GapStatusDisplayItem;
|
||||
import org.joinmastodon.android.ui.displayitems.StatusDisplayItem;
|
||||
import org.joinmastodon.android.ui.utils.UiUtils;
|
||||
import org.joinmastodon.android.ui.viewcontrollers.HomeTimelineMenuController;
|
||||
import org.joinmastodon.android.ui.viewcontrollers.ToolbarDropdownMenuController;
|
||||
import org.joinmastodon.android.ui.views.FixedAspectRatioImageView;
|
||||
import org.joinmastodon.android.updater.GithubSelfUpdater;
|
||||
import org.parceler.Parcels;
|
||||
|
||||
import java.util.Collections;
|
||||
import java.util.HashSet;
|
||||
@ -54,12 +63,19 @@ import me.grishka.appkit.api.SimpleCallback;
|
||||
import me.grishka.appkit.utils.CubicBezierInterpolator;
|
||||
import me.grishka.appkit.utils.V;
|
||||
|
||||
public class HomeTimelineFragment extends StatusListFragment{
|
||||
public class HomeTimelineFragment extends StatusListFragment implements ToolbarDropdownMenuController.HostFragment{
|
||||
private ImageButton fab;
|
||||
private ImageView toolbarLogo;
|
||||
private LinearLayout listsDropdown;
|
||||
private FixedAspectRatioImageView listsDropdownArrow;
|
||||
private TextView listsDropdownText;
|
||||
private Button toolbarShowNewPostsBtn;
|
||||
private boolean newPostsBtnShown;
|
||||
private AnimatorSet currentNewPostsAnim;
|
||||
private ToolbarDropdownMenuController dropdownController;
|
||||
private HomeTimelineMenuController dropdownMainMenuController;
|
||||
private List<FollowList> lists=List.of();
|
||||
private ListMode listMode=ListMode.FOLLOWING;
|
||||
private FollowList currentList;
|
||||
|
||||
private String maxID;
|
||||
private String lastSavedMarkerID;
|
||||
@ -71,25 +87,103 @@ public class HomeTimelineFragment extends StatusListFragment{
|
||||
@Override
|
||||
public void onAttach(Activity activity){
|
||||
super.onAttach(activity);
|
||||
dropdownController=new ToolbarDropdownMenuController(this);
|
||||
dropdownMainMenuController=new HomeTimelineMenuController(dropdownController, new HomeTimelineMenuController.Callback(){
|
||||
@Override
|
||||
public void onFollowingSelected(){
|
||||
if(listMode==ListMode.FOLLOWING)
|
||||
return;
|
||||
listMode=ListMode.FOLLOWING;
|
||||
reload();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onLocalSelected(){
|
||||
if(listMode==ListMode.LOCAL)
|
||||
return;
|
||||
listMode=ListMode.LOCAL;
|
||||
reload();
|
||||
}
|
||||
|
||||
@Override
|
||||
public List<FollowList> getLists(){
|
||||
return lists;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onListSelected(FollowList list){
|
||||
if(listMode==ListMode.LIST && currentList==list)
|
||||
return;
|
||||
listMode=ListMode.LIST;
|
||||
currentList=list;
|
||||
reload();
|
||||
}
|
||||
});
|
||||
setHasOptionsMenu(true);
|
||||
loadData();
|
||||
AccountSessionManager.get(accountID).getCacheController().getLists(new Callback<>(){
|
||||
@Override
|
||||
public void onSuccess(List<FollowList> result){
|
||||
lists=result;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onError(ErrorResponse error){}
|
||||
});
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void doLoadData(int offset, int count){
|
||||
AccountSessionManager.getInstance()
|
||||
.getAccount(accountID).getCacheController()
|
||||
.getHomeTimeline(offset>0 ? maxID : null, count, refreshing, new SimpleCallback<>(this){
|
||||
@Override
|
||||
public void onSuccess(CacheablePaginatedResponse<List<Status>> result){
|
||||
if(getActivity()==null)
|
||||
return;
|
||||
onDataLoaded(result.items, !result.items.isEmpty());
|
||||
maxID=result.maxID;
|
||||
if(result.isFromCache())
|
||||
loadNewPosts();
|
||||
}
|
||||
});
|
||||
switch(listMode){
|
||||
case FOLLOWING -> {
|
||||
AccountSessionManager.getInstance()
|
||||
.getAccount(accountID).getCacheController()
|
||||
.getHomeTimeline(offset>0 ? maxID : null, count, refreshing, new SimpleCallback<>(this){
|
||||
@Override
|
||||
public void onSuccess(CacheablePaginatedResponse<List<Status>> result){
|
||||
if(getActivity()==null || listMode!=ListMode.FOLLOWING)
|
||||
return;
|
||||
if(refreshing)
|
||||
list.scrollToPosition(0);
|
||||
onDataLoaded(result.items, !result.items.isEmpty());
|
||||
maxID=result.maxID;
|
||||
if(result.isFromCache())
|
||||
loadNewPosts();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onError(ErrorResponse error){
|
||||
if(listMode!=ListMode.FOLLOWING)
|
||||
return;
|
||||
super.onError(error);
|
||||
}
|
||||
});
|
||||
}
|
||||
case LOCAL -> {
|
||||
currentRequest=new GetPublicTimeline(true, false, offset>0 ? maxID : null, null, count, null)
|
||||
.setCallback(new SimpleCallback<>(this){
|
||||
@Override
|
||||
public void onSuccess(List<Status> result){
|
||||
if(refreshing)
|
||||
list.scrollToPosition(0);
|
||||
onDataLoaded(result, !result.isEmpty());
|
||||
}
|
||||
})
|
||||
.exec(accountID);
|
||||
}
|
||||
case LIST -> {
|
||||
currentRequest=new GetListTimeline(currentList.id, offset>0 ? maxID : null, null, count, null)
|
||||
.setCallback(new SimpleCallback<>(this){
|
||||
@Override
|
||||
public void onSuccess(List<Status> result){
|
||||
if(refreshing)
|
||||
list.scrollToPosition(0);
|
||||
onDataLoaded(result, !result.isEmpty());
|
||||
}
|
||||
})
|
||||
.exec(accountID);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
@ -116,13 +210,26 @@ public class HomeTimelineFragment extends StatusListFragment{
|
||||
@Override
|
||||
public void onCreateOptionsMenu(Menu menu, MenuInflater inflater){
|
||||
inflater.inflate(R.menu.home, menu);
|
||||
menu.findItem(R.id.edit_list).setVisible(listMode==ListMode.LIST);
|
||||
GithubSelfUpdater.UpdateState state=GithubSelfUpdater.UpdateState.NO_UPDATE;
|
||||
GithubSelfUpdater updater=GithubSelfUpdater.getInstance();
|
||||
if(updater!=null)
|
||||
state=updater.getState();
|
||||
if(state!=GithubSelfUpdater.UpdateState.NO_UPDATE && state!=GithubSelfUpdater.UpdateState.CHECKING)
|
||||
getToolbar().getMenu().findItem(R.id.settings).setIcon(R.drawable.ic_settings_updateready_24px);
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean onOptionsItemSelected(MenuItem item){
|
||||
Bundle args=new Bundle();
|
||||
args.putString("account", accountID);
|
||||
Nav.go(getActivity(), SettingsMainFragment.class, args);
|
||||
int id=item.getItemId();
|
||||
if(id==R.id.settings){
|
||||
Nav.go(getActivity(), SettingsMainFragment.class, args);
|
||||
}else if(id==R.id.edit_list){
|
||||
args.putParcelable("list", Parcels.wrap(currentList));
|
||||
Nav.go(getActivity(), EditListFragment.class, args);
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
@ -147,7 +254,7 @@ public class HomeTimelineFragment extends StatusListFragment{
|
||||
@Override
|
||||
protected void onHidden(){
|
||||
super.onHidden();
|
||||
if(!data.isEmpty()){
|
||||
if(!data.isEmpty() && listMode==ListMode.FOLLOWING){
|
||||
String topPostID=displayItems.get(Math.max(0, list.getChildAdapterPosition(list.getChildAt(0))-getMainAdapterOffset())).parentID;
|
||||
if(!topPostID.equals(lastSavedMarkerID)){
|
||||
lastSavedMarkerID=topPostID;
|
||||
@ -183,8 +290,8 @@ public class HomeTimelineFragment extends StatusListFragment{
|
||||
// we'll get the currently topmost post as last in the response. This way we know there's no gap
|
||||
// between the existing and newly loaded parts of the timeline.
|
||||
String sinceID=data.size()>1 ? data.get(1).id : "1";
|
||||
currentRequest=new GetHomeTimeline(null, null, 20, sinceID)
|
||||
.setCallback(new Callback<>(){
|
||||
boolean needCache=listMode==ListMode.FOLLOWING;
|
||||
loadAdditionalPosts(null, null, 20, sinceID, new Callback<>(){
|
||||
@Override
|
||||
public void onSuccess(List<Status> result){
|
||||
currentRequest=null;
|
||||
@ -199,11 +306,13 @@ public class HomeTimelineFragment extends StatusListFragment{
|
||||
result.get(result.size()-1).hasGapAfter=true;
|
||||
toAdd=result;
|
||||
}
|
||||
AccountSessionManager.get(accountID).filterStatuses(toAdd, FilterContext.HOME);
|
||||
if(needCache)
|
||||
AccountSessionManager.get(accountID).filterStatuses(toAdd, FilterContext.HOME);
|
||||
if(!toAdd.isEmpty()){
|
||||
prependItems(toAdd, true);
|
||||
showNewPostsButton();
|
||||
AccountSessionManager.getInstance().getAccount(accountID).getCacheController().putHomeTimeline(toAdd, false);
|
||||
if(needCache)
|
||||
AccountSessionManager.getInstance().getAccount(accountID).getCacheController().putHomeTimeline(toAdd, false);
|
||||
}
|
||||
}
|
||||
|
||||
@ -212,8 +321,7 @@ public class HomeTimelineFragment extends StatusListFragment{
|
||||
currentRequest=null;
|
||||
dataLoading=false;
|
||||
}
|
||||
})
|
||||
.exec(accountID);
|
||||
});
|
||||
}
|
||||
|
||||
@Override
|
||||
@ -225,10 +333,11 @@ public class HomeTimelineFragment extends StatusListFragment{
|
||||
V.setVisibilityAnimated(item.text, View.GONE);
|
||||
GapStatusDisplayItem gap=item.getItem();
|
||||
dataLoading=true;
|
||||
currentRequest=new GetHomeTimeline(item.getItemID(), null, 20, null)
|
||||
.setCallback(new Callback<>(){
|
||||
boolean needCache=listMode==ListMode.FOLLOWING;
|
||||
loadAdditionalPosts(item.getItemID(), null, 20, null, new Callback<>(){
|
||||
@Override
|
||||
public void onSuccess(List<Status> result){
|
||||
|
||||
currentRequest=null;
|
||||
dataLoading=false;
|
||||
if(getActivity()==null)
|
||||
@ -242,7 +351,8 @@ public class HomeTimelineFragment extends StatusListFragment{
|
||||
Status gapStatus=getStatusByID(gap.parentID);
|
||||
if(gapStatus!=null){
|
||||
gapStatus.hasGapAfter=false;
|
||||
AccountSessionManager.getInstance().getAccount(accountID).getCacheController().putHomeTimeline(Collections.singletonList(gapStatus), false);
|
||||
if(needCache)
|
||||
AccountSessionManager.getInstance().getAccount(accountID).getCacheController().putHomeTimeline(Collections.singletonList(gapStatus), false);
|
||||
}
|
||||
}else{
|
||||
Set<String> idsBelowGap=new HashSet<>();
|
||||
@ -254,7 +364,8 @@ public class HomeTimelineFragment extends StatusListFragment{
|
||||
}else if(s.id.equals(gap.parentID)){
|
||||
belowGap=true;
|
||||
s.hasGapAfter=false;
|
||||
AccountSessionManager.getInstance().getAccount(accountID).getCacheController().putHomeTimeline(Collections.singletonList(s), false);
|
||||
if(needCache)
|
||||
AccountSessionManager.getInstance().getAccount(accountID).getCacheController().putHomeTimeline(Collections.singletonList(s), false);
|
||||
}else{
|
||||
gapPostIndex++;
|
||||
}
|
||||
@ -270,7 +381,8 @@ public class HomeTimelineFragment extends StatusListFragment{
|
||||
}else{
|
||||
result=result.subList(0, endIndex);
|
||||
}
|
||||
AccountSessionManager.get(accountID).filterStatuses(result, FilterContext.HOME);
|
||||
if(needCache)
|
||||
AccountSessionManager.get(accountID).filterStatuses(result, FilterContext.HOME);
|
||||
List<StatusDisplayItem> targetList=displayItems.subList(gapPos, gapPos+1);
|
||||
targetList.clear();
|
||||
List<Status> insertedPosts=data.subList(gapPostIndex+1, gapPostIndex+1);
|
||||
@ -287,7 +399,8 @@ public class HomeTimelineFragment extends StatusListFragment{
|
||||
adapter.notifyItemChanged(getMainAdapterOffset()+gapPos);
|
||||
adapter.notifyItemRangeInserted(getMainAdapterOffset()+gapPos+1, targetList.size()-1);
|
||||
}
|
||||
AccountSessionManager.getInstance().getAccount(accountID).getCacheController().putHomeTimeline(insertedPosts, false);
|
||||
if(needCache)
|
||||
AccountSessionManager.getInstance().getAccount(accountID).getCacheController().putHomeTimeline(insertedPosts, false);
|
||||
}
|
||||
}
|
||||
|
||||
@ -304,9 +417,17 @@ public class HomeTimelineFragment extends StatusListFragment{
|
||||
adapter.notifyItemChanged(gapPos);
|
||||
}
|
||||
}
|
||||
})
|
||||
.exec(accountID);
|
||||
});
|
||||
}
|
||||
|
||||
private void loadAdditionalPosts(String maxID, String minID, int limit, String sinceID, Callback<List<Status>> callback){
|
||||
MastodonAPIRequest<List<Status>> req=switch(listMode){
|
||||
case FOLLOWING -> new GetHomeTimeline(maxID, minID, limit, sinceID);
|
||||
case LOCAL -> new GetPublicTimeline(true, false, maxID, minID, limit, sinceID);
|
||||
case LIST -> new GetListTimeline(currentList.id, maxID, minID, limit, sinceID);
|
||||
};
|
||||
currentRequest=req;
|
||||
req.setCallback(callback).exec(accountID);
|
||||
}
|
||||
|
||||
@Override
|
||||
@ -320,10 +441,31 @@ public class HomeTimelineFragment extends StatusListFragment{
|
||||
}
|
||||
|
||||
private void updateToolbarLogo(){
|
||||
toolbarLogo=new ImageView(getActivity());
|
||||
toolbarLogo.setScaleType(ImageView.ScaleType.CENTER);
|
||||
toolbarLogo.setImageResource(R.drawable.logo);
|
||||
toolbarLogo.setImageTintList(ColorStateList.valueOf(UiUtils.getThemeColor(getActivity(), android.R.attr.textColorPrimary)));
|
||||
listsDropdown=new LinearLayout(getActivity());
|
||||
listsDropdown.setOnClickListener(this::onListsDropdownClick);
|
||||
listsDropdown.setBackgroundResource(R.drawable.bg_button_m3_text);
|
||||
listsDropdown.setAccessibilityDelegate(new View.AccessibilityDelegate(){
|
||||
@Override
|
||||
public void onInitializeAccessibilityNodeInfo(@NonNull View host, @NonNull AccessibilityNodeInfo info){
|
||||
super.onInitializeAccessibilityNodeInfo(host, info);
|
||||
info.setClassName("android.widget.Spinner");
|
||||
}
|
||||
});
|
||||
listsDropdownArrow=new FixedAspectRatioImageView(getActivity());
|
||||
listsDropdownArrow.setUseHeight(true);
|
||||
listsDropdownArrow.setImageResource(R.drawable.ic_arrow_drop_down_24px);
|
||||
listsDropdownArrow.setScaleType(ImageView.ScaleType.CENTER);
|
||||
listsDropdownArrow.setImportantForAccessibility(View.IMPORTANT_FOR_ACCESSIBILITY_NO);
|
||||
listsDropdown.addView(listsDropdownArrow, new LinearLayout.LayoutParams(ViewGroup.LayoutParams.WRAP_CONTENT, ViewGroup.LayoutParams.MATCH_PARENT));
|
||||
listsDropdownText=new TextView(getActivity());
|
||||
listsDropdownText.setTextAppearance(R.style.action_bar_title);
|
||||
listsDropdownText.setSingleLine();
|
||||
listsDropdownText.setGravity(Gravity.START | Gravity.CENTER_VERTICAL);
|
||||
listsDropdownText.setPaddingRelative(V.dp(4), 0, V.dp(16), 0);
|
||||
listsDropdownText.setText(getCurrentListTitle());
|
||||
listsDropdownArrow.setImageTintList(listsDropdownText.getTextColors());
|
||||
listsDropdown.setBackgroundTintList(listsDropdownText.getTextColors());
|
||||
listsDropdown.addView(listsDropdownText, new LinearLayout.LayoutParams(ViewGroup.LayoutParams.WRAP_CONTENT, ViewGroup.LayoutParams.MATCH_PARENT));
|
||||
|
||||
toolbarShowNewPostsBtn=new Button(getActivity());
|
||||
toolbarShowNewPostsBtn.setTextAppearance(R.style.m3_title_medium);
|
||||
@ -340,22 +482,33 @@ public class HomeTimelineFragment extends StatusListFragment{
|
||||
|
||||
if(newPostsBtnShown){
|
||||
toolbarShowNewPostsBtn.setVisibility(View.VISIBLE);
|
||||
toolbarLogo.setVisibility(View.INVISIBLE);
|
||||
toolbarLogo.setAlpha(0f);
|
||||
listsDropdown.setVisibility(View.INVISIBLE);
|
||||
listsDropdown.setAlpha(0f);
|
||||
}else{
|
||||
toolbarShowNewPostsBtn.setVisibility(View.INVISIBLE);
|
||||
toolbarShowNewPostsBtn.setAlpha(0f);
|
||||
toolbarShowNewPostsBtn.setScaleX(.8f);
|
||||
toolbarShowNewPostsBtn.setScaleY(.8f);
|
||||
toolbarLogo.setVisibility(View.VISIBLE);
|
||||
listsDropdown.setVisibility(View.VISIBLE);
|
||||
}
|
||||
|
||||
FrameLayout logoWrap=new FrameLayout(getActivity());
|
||||
logoWrap.addView(toolbarLogo, new FrameLayout.LayoutParams(ViewGroup.LayoutParams.WRAP_CONTENT, ViewGroup.LayoutParams.WRAP_CONTENT, Gravity.CENTER));
|
||||
FrameLayout logoWrap=new FrameLayout(getActivity()){
|
||||
@Override
|
||||
protected void onLayout(boolean changed, int left, int top, int right, int bottom){
|
||||
super.onLayout(changed, left, top, right, bottom);
|
||||
// I'm sorry for doing this. This centers the button within the entire toolbar
|
||||
int rightGap=getToolbar().getWidth()-right;
|
||||
toolbarShowNewPostsBtn.offsetLeftAndRight((rightGap-left)/2);
|
||||
}
|
||||
};
|
||||
FrameLayout.LayoutParams ddlp=new FrameLayout.LayoutParams(ViewGroup.LayoutParams.WRAP_CONTENT, ViewGroup.LayoutParams.MATCH_PARENT, Gravity.START);
|
||||
ddlp.topMargin=ddlp.bottomMargin=V.dp(8);
|
||||
logoWrap.addView(listsDropdown, ddlp);
|
||||
logoWrap.addView(toolbarShowNewPostsBtn, new FrameLayout.LayoutParams(ViewGroup.LayoutParams.WRAP_CONTENT, V.dp(32), Gravity.CENTER));
|
||||
|
||||
Toolbar toolbar=getToolbar();
|
||||
toolbar.addView(logoWrap, new Toolbar.LayoutParams(Gravity.CENTER));
|
||||
toolbar.addView(logoWrap, new Toolbar.LayoutParams(ViewGroup.LayoutParams.MATCH_PARENT, ViewGroup.LayoutParams.MATCH_PARENT));
|
||||
toolbar.setContentInsetsRelative(V.dp(16), 0);
|
||||
}
|
||||
|
||||
private void showNewPostsButton(){
|
||||
@ -368,7 +521,7 @@ public class HomeTimelineFragment extends StatusListFragment{
|
||||
toolbarShowNewPostsBtn.setVisibility(View.VISIBLE);
|
||||
AnimatorSet set=new AnimatorSet();
|
||||
set.playTogether(
|
||||
ObjectAnimator.ofFloat(toolbarLogo, View.ALPHA, 0f),
|
||||
ObjectAnimator.ofFloat(listsDropdown, View.ALPHA, 0f),
|
||||
ObjectAnimator.ofFloat(toolbarShowNewPostsBtn, View.ALPHA, 1f),
|
||||
ObjectAnimator.ofFloat(toolbarShowNewPostsBtn, View.SCALE_X, 1f),
|
||||
ObjectAnimator.ofFloat(toolbarShowNewPostsBtn, View.SCALE_Y, 1f)
|
||||
@ -378,7 +531,7 @@ public class HomeTimelineFragment extends StatusListFragment{
|
||||
set.addListener(new AnimatorListenerAdapter(){
|
||||
@Override
|
||||
public void onAnimationEnd(Animator animation){
|
||||
toolbarLogo.setVisibility(View.INVISIBLE);
|
||||
listsDropdown.setVisibility(View.INVISIBLE);
|
||||
currentNewPostsAnim=null;
|
||||
}
|
||||
});
|
||||
@ -393,10 +546,10 @@ public class HomeTimelineFragment extends StatusListFragment{
|
||||
if(currentNewPostsAnim!=null){
|
||||
currentNewPostsAnim.cancel();
|
||||
}
|
||||
toolbarLogo.setVisibility(View.VISIBLE);
|
||||
listsDropdown.setVisibility(View.VISIBLE);
|
||||
AnimatorSet set=new AnimatorSet();
|
||||
set.playTogether(
|
||||
ObjectAnimator.ofFloat(toolbarLogo, View.ALPHA, 1f),
|
||||
ObjectAnimator.ofFloat(listsDropdown, View.ALPHA, 1f),
|
||||
ObjectAnimator.ofFloat(toolbarShowNewPostsBtn, View.ALPHA, 0f),
|
||||
ObjectAnimator.ofFloat(toolbarShowNewPostsBtn, View.SCALE_X, .8f),
|
||||
ObjectAnimator.ofFloat(toolbarShowNewPostsBtn, View.SCALE_Y, .8f)
|
||||
@ -421,6 +574,20 @@ public class HomeTimelineFragment extends StatusListFragment{
|
||||
}
|
||||
}
|
||||
|
||||
private void onListsDropdownClick(View v){
|
||||
listsDropdownArrow.animate().rotation(-180f).setDuration(150).setInterpolator(CubicBezierInterpolator.DEFAULT).start();
|
||||
dropdownController.show(dropdownMainMenuController);
|
||||
AccountSessionManager.get(accountID).getCacheController().reloadLists(new Callback<>(){
|
||||
@Override
|
||||
public void onSuccess(java.util.List<FollowList> result){
|
||||
lists=result;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onError(ErrorResponse error){}
|
||||
});
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onDestroyView(){
|
||||
super.onDestroyView();
|
||||
@ -443,4 +610,47 @@ public class HomeTimelineFragment extends StatusListFragment{
|
||||
protected boolean shouldRemoveAccountPostsWhenUnfollowing(){
|
||||
return true;
|
||||
}
|
||||
|
||||
@Override
|
||||
public Toolbar getToolbar(){
|
||||
return super.getToolbar();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onDropdownWillDismiss(){
|
||||
listsDropdownArrow.animate().rotation(0f).setDuration(150).setInterpolator(CubicBezierInterpolator.DEFAULT).start();
|
||||
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onDropdownDismissed(){
|
||||
|
||||
}
|
||||
|
||||
@Override
|
||||
public void reload(){
|
||||
if(currentRequest!=null){
|
||||
currentRequest.cancel();
|
||||
currentRequest=null;
|
||||
}
|
||||
refreshing=true;
|
||||
showProgress();
|
||||
loadData();
|
||||
listsDropdownText.setText(getCurrentListTitle());
|
||||
invalidateOptionsMenu();
|
||||
}
|
||||
|
||||
private String getCurrentListTitle(){
|
||||
return switch(listMode){
|
||||
case FOLLOWING -> getString(R.string.timeline_following);
|
||||
case LOCAL -> getString(R.string.local_timeline);
|
||||
case LIST -> currentList.title;
|
||||
};
|
||||
}
|
||||
|
||||
private enum ListMode{
|
||||
FOLLOWING,
|
||||
LOCAL,
|
||||
LIST
|
||||
}
|
||||
}
|
||||
|
@ -0,0 +1,300 @@
|
||||
package org.joinmastodon.android.fragments;
|
||||
|
||||
import android.os.Bundle;
|
||||
import android.view.ActionMode;
|
||||
import android.view.Menu;
|
||||
import android.view.MenuInflater;
|
||||
import android.view.MenuItem;
|
||||
import android.view.View;
|
||||
import android.widget.ImageButton;
|
||||
|
||||
import com.squareup.otto.Subscribe;
|
||||
|
||||
import org.joinmastodon.android.E;
|
||||
import org.joinmastodon.android.R;
|
||||
import org.joinmastodon.android.api.requests.HeaderPaginationRequest;
|
||||
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.AccountAddedToListEvent;
|
||||
import org.joinmastodon.android.events.AccountRemovedFromListEvent;
|
||||
import org.joinmastodon.android.fragments.account_list.AddListMembersFragment;
|
||||
import org.joinmastodon.android.fragments.account_list.PaginatedAccountListFragment;
|
||||
import org.joinmastodon.android.model.Account;
|
||||
import org.joinmastodon.android.model.FollowList;
|
||||
import org.joinmastodon.android.model.viewmodel.AccountViewModel;
|
||||
import org.joinmastodon.android.ui.M3AlertDialogBuilder;
|
||||
import org.joinmastodon.android.ui.utils.ActionModeHelper;
|
||||
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;
|
||||
import java.util.Objects;
|
||||
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.utils.V;
|
||||
|
||||
public class ListMembersFragment extends PaginatedAccountListFragment{
|
||||
private static final int ADD_MEMBER_RESULT=600;
|
||||
|
||||
private ImageButton fab;
|
||||
private FollowList followList;
|
||||
private boolean inSelectionMode;
|
||||
private Set<String> selectedAccounts=new HashSet<>();
|
||||
private ActionMode actionMode;
|
||||
private MenuItem deleteItem;
|
||||
|
||||
public ListMembersFragment(){
|
||||
setListLayoutId(R.layout.recycler_fragment_with_fab);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onCreate(Bundle savedInstanceState){
|
||||
super.onCreate(savedInstanceState);
|
||||
followList=Parcels.unwrap(getArguments().getParcelable("list"));
|
||||
setTitle(R.string.list_members);
|
||||
setHasOptionsMenu(true);
|
||||
E.register(this);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onDestroy(){
|
||||
super.onDestroy();
|
||||
E.unregister(this);
|
||||
}
|
||||
|
||||
@Override
|
||||
public HeaderPaginationRequest<Account> onCreateRequest(String maxID, int count){
|
||||
return new GetListAccounts(followList.id, maxID, count);
|
||||
}
|
||||
|
||||
@Override
|
||||
protected boolean hasSubtitle(){
|
||||
return false;
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void onConfigureViewHolder(AccountViewHolder holder){
|
||||
super.onConfigureViewHolder(holder);
|
||||
holder.setStyle(inSelectionMode ? AccountViewHolder.AccessoryType.CHECKBOX : AccountViewHolder.AccessoryType.MENU, false);
|
||||
holder.setOnClickListener(this::onItemClick);
|
||||
holder.setOnLongClickListener(this::onItemLongClick);
|
||||
holder.getContextMenu().getMenu().add(0, R.id.remove_from_list, 0, R.string.remove_from_list);
|
||||
holder.setOnCustomMenuItemSelectedListener(item->onItemMenuItemSelected(holder, item));
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void onBindViewHolder(AccountViewHolder holder){
|
||||
super.onBindViewHolder(holder);
|
||||
holder.setStyle(inSelectionMode ? AccountViewHolder.AccessoryType.CHECKBOX : AccountViewHolder.AccessoryType.MENU, false);
|
||||
if(inSelectionMode){
|
||||
holder.setChecked(selectedAccounts.contains(holder.getItem().account.id));
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean wantsLightStatusBar(){
|
||||
if(actionMode!=null)
|
||||
return UiUtils.isDarkTheme();
|
||||
return super.wantsLightStatusBar();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onCreateOptionsMenu(Menu menu, MenuInflater inflater){
|
||||
inflater.inflate(R.menu.selectable_list, menu);
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean onOptionsItemSelected(MenuItem item){
|
||||
int id=item.getItemId();
|
||||
if(id==R.id.select){
|
||||
enterSelectionMode();
|
||||
}else if(id==R.id.select_all){
|
||||
for(AccountViewModel a:data){
|
||||
selectedAccounts.add(a.account.id);
|
||||
}
|
||||
enterSelectionMode();
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
@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_list_member));
|
||||
fab.setOnClickListener(v->onFabClick());
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onFragmentResult(int reqCode, boolean success, Bundle result){
|
||||
if(reqCode==ADD_MEMBER_RESULT && success){
|
||||
Account acc=Objects.requireNonNull(Parcels.unwrap(result.getParcelable("selectedAccount")));
|
||||
addAccounts(List.of(acc));
|
||||
}
|
||||
}
|
||||
|
||||
@Subscribe
|
||||
public void onAccountRemovedFromList(AccountRemovedFromListEvent ev){
|
||||
if(ev.accountID.equals(accountID) && ev.listID.equals(followList.id)){
|
||||
removeAccountRows(Set.of(ev.targetAccountID));
|
||||
}
|
||||
}
|
||||
|
||||
@Subscribe
|
||||
public void onAccountAddedToList(AccountAddedToListEvent ev){
|
||||
if(ev.accountID.equals(accountID) && ev.listID.equals(followList.id)){
|
||||
data.add(new AccountViewModel(ev.account, accountID));
|
||||
list.getAdapter().notifyItemInserted(data.size()-1);
|
||||
}
|
||||
}
|
||||
|
||||
private void onFabClick(){
|
||||
Bundle args=new Bundle();
|
||||
args.putString("account", accountID);
|
||||
Nav.goForResult(getActivity(), AddListMembersFragment.class, args, ADD_MEMBER_RESULT, this);
|
||||
}
|
||||
|
||||
private void onItemClick(AccountViewHolder holder){
|
||||
if(inSelectionMode){
|
||||
String id=holder.getItem().account.id;
|
||||
if(selectedAccounts.contains(id)){
|
||||
selectedAccounts.remove(id);
|
||||
holder.setChecked(false);
|
||||
}else{
|
||||
selectedAccounts.add(id);
|
||||
holder.setChecked(true);
|
||||
}
|
||||
updateActionModeTitle();
|
||||
deleteItem.setEnabled(!selectedAccounts.isEmpty());
|
||||
return;
|
||||
}
|
||||
Bundle args=new Bundle();
|
||||
args.putString("account", accountID);
|
||||
args.putParcelable("profileAccount", Parcels.wrap(holder.getItem().account));
|
||||
Nav.go(getActivity(), ProfileFragment.class, args);
|
||||
}
|
||||
|
||||
private boolean onItemLongClick(AccountViewHolder holder){
|
||||
if(inSelectionMode)
|
||||
return false;
|
||||
selectedAccounts.add(holder.getItem().account.id);
|
||||
enterSelectionMode();
|
||||
return true;
|
||||
}
|
||||
|
||||
private void onItemMenuItemSelected(AccountViewHolder holder, MenuItem item){
|
||||
int id=item.getItemId();
|
||||
if(id==R.id.remove_from_list){
|
||||
new M3AlertDialogBuilder(getActivity())
|
||||
.setTitle(R.string.confirm_remove_list_member)
|
||||
.setPositiveButton(R.string.remove, (dlg, which)->removeAccounts(Set.of(holder.getItem().account.id)))
|
||||
.setNegativeButton(R.string.cancel, null)
|
||||
.show();
|
||||
}
|
||||
}
|
||||
|
||||
private void updateItemsForSelectionModeTransition(){
|
||||
list.getAdapter().notifyItemRangeChanged(0, data.size());
|
||||
}
|
||||
|
||||
private void enterSelectionMode(){
|
||||
inSelectionMode=true;
|
||||
updateItemsForSelectionModeTransition();
|
||||
V.setVisibilityAnimated(fab, View.INVISIBLE);
|
||||
actionMode=ActionModeHelper.startActionMode(this, ()->elevationOnScrollListener.getCurrentStatusBarColor(), new ActionMode.Callback(){
|
||||
@Override
|
||||
public boolean onCreateActionMode(ActionMode mode, Menu menu){
|
||||
return true;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean onPrepareActionMode(ActionMode mode, Menu menu){
|
||||
mode.getMenuInflater().inflate(R.menu.settings_filter_words_action_mode, menu);
|
||||
deleteItem=menu.findItem(R.id.delete);
|
||||
return true;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean onActionItemClicked(ActionMode mode, MenuItem item){
|
||||
new M3AlertDialogBuilder(getActivity())
|
||||
.setTitle(R.string.confirm_remove_list_members)
|
||||
.setPositiveButton(R.string.remove, (dlg, which)->removeAccounts(new HashSet<>(selectedAccounts)))
|
||||
.setNegativeButton(R.string.cancel, null)
|
||||
.show();
|
||||
return true;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onDestroyActionMode(ActionMode mode){
|
||||
actionMode=null;
|
||||
inSelectionMode=false;
|
||||
selectedAccounts.clear();
|
||||
updateItemsForSelectionModeTransition();
|
||||
V.setVisibilityAnimated(fab, View.VISIBLE);
|
||||
}
|
||||
});
|
||||
updateActionModeTitle();
|
||||
}
|
||||
|
||||
private void updateActionModeTitle(){
|
||||
actionMode.setTitle(getResources().getQuantityString(R.plurals.x_items_selected, selectedAccounts.size(), selectedAccounts.size()));
|
||||
}
|
||||
|
||||
private void removeAccounts(Set<String> ids){
|
||||
new RemoveAccountsFromList(followList.id, ids)
|
||||
.setCallback(new Callback<>(){
|
||||
@Override
|
||||
public void onSuccess(Void result){
|
||||
if(inSelectionMode)
|
||||
actionMode.finish();
|
||||
removeAccountRows(ids);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onError(ErrorResponse error){
|
||||
error.showToast(getActivity());
|
||||
}
|
||||
})
|
||||
.wrapProgress(getActivity(), R.string.loading, true)
|
||||
.exec(accountID);
|
||||
}
|
||||
|
||||
private void addAccounts(Collection<Account> accounts){
|
||||
new AddAccountsToList(followList.id, accounts.stream().map(a->a.id).collect(Collectors.toSet()))
|
||||
.setCallback(new Callback<>(){
|
||||
@Override
|
||||
public void onSuccess(Void result){
|
||||
for(Account acc:accounts){
|
||||
data.add(new AccountViewModel(acc, accountID));
|
||||
}
|
||||
list.getAdapter().notifyItemRangeInserted(data.size()-accounts.size(), accounts.size());
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onError(ErrorResponse error){
|
||||
error.showToast(getActivity());
|
||||
}
|
||||
})
|
||||
.wrapProgress(getActivity(), R.string.loading, true)
|
||||
.exec(accountID);
|
||||
}
|
||||
|
||||
private void removeAccountRows(Set<String> ids){
|
||||
for(int i=data.size()-1;i>=0;i--){
|
||||
if(ids.contains(data.get(i).account.id)){
|
||||
data.remove(i);
|
||||
list.getAdapter().notifyItemRemoved(i);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
@ -0,0 +1,61 @@
|
||||
package org.joinmastodon.android.fragments;
|
||||
|
||||
import android.os.Bundle;
|
||||
import android.view.Menu;
|
||||
import android.view.MenuInflater;
|
||||
import android.view.MenuItem;
|
||||
|
||||
import org.joinmastodon.android.R;
|
||||
import org.joinmastodon.android.api.requests.timelines.GetListTimeline;
|
||||
import org.joinmastodon.android.model.FollowList;
|
||||
import org.joinmastodon.android.model.Status;
|
||||
import org.parceler.Parcels;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
import me.grishka.appkit.Nav;
|
||||
import me.grishka.appkit.api.SimpleCallback;
|
||||
|
||||
public class ListTimelineFragment extends StatusListFragment{
|
||||
private FollowList followList;
|
||||
|
||||
@Override
|
||||
public void onCreate(Bundle savedInstanceState){
|
||||
super.onCreate(savedInstanceState);
|
||||
followList=Parcels.unwrap(getArguments().getParcelable("list"));
|
||||
setTitle(followList.title);
|
||||
setHasOptionsMenu(true);
|
||||
loadData();
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void doLoadData(int offset, int count){
|
||||
currentRequest=new GetListTimeline(followList.id, offset>0 ? getMaxID() : null, null, count, null)
|
||||
.setCallback(new SimpleCallback<>(this){
|
||||
@Override
|
||||
public void onSuccess(List<Status> result){
|
||||
onDataLoaded(result, !result.isEmpty());
|
||||
}
|
||||
})
|
||||
.exec(accountID);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onCreateOptionsMenu(Menu menu, MenuInflater inflater){
|
||||
inflater.inflate(R.menu.standalone_list_timeline, menu);
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean onOptionsItemSelected(MenuItem item){
|
||||
int id=item.getItemId();
|
||||
Bundle args=new Bundle();
|
||||
args.putString("account", accountID);
|
||||
args.putParcelable("list", Parcels.wrap(followList));
|
||||
if(id==R.id.members){
|
||||
Nav.go(getActivity(), ListMembersFragment.class, args);
|
||||
}else if(id==R.id.edit_list){
|
||||
Nav.go(getActivity(), EditListFragment.class, args);
|
||||
}
|
||||
return true;
|
||||
}
|
||||
}
|
@ -0,0 +1,95 @@
|
||||
package org.joinmastodon.android.fragments;
|
||||
|
||||
import android.os.Bundle;
|
||||
import android.view.Menu;
|
||||
import android.view.MenuItem;
|
||||
|
||||
import org.joinmastodon.android.R;
|
||||
import org.joinmastodon.android.api.requests.tags.GetFollowedTags;
|
||||
import org.joinmastodon.android.api.requests.tags.SetTagFollowed;
|
||||
import org.joinmastodon.android.fragments.settings.BaseSettingsFragment;
|
||||
import org.joinmastodon.android.model.Hashtag;
|
||||
import org.joinmastodon.android.model.HeaderPaginationList;
|
||||
import org.joinmastodon.android.model.viewmodel.ListItemWithOptionsMenu;
|
||||
import org.joinmastodon.android.ui.M3AlertDialogBuilder;
|
||||
import org.joinmastodon.android.ui.utils.UiUtils;
|
||||
|
||||
import java.util.stream.Collectors;
|
||||
|
||||
import me.grishka.appkit.api.Callback;
|
||||
import me.grishka.appkit.api.ErrorResponse;
|
||||
import me.grishka.appkit.api.SimpleCallback;
|
||||
|
||||
public class ManageFollowedHashtagsFragment extends BaseSettingsFragment<Hashtag> implements ListItemWithOptionsMenu.OptionsMenuListener<Hashtag>{
|
||||
private String maxID;
|
||||
|
||||
public ManageFollowedHashtagsFragment(){
|
||||
super(100);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onCreate(Bundle savedInstanceState){
|
||||
super.onCreate(savedInstanceState);
|
||||
setTitle(R.string.manage_hashtags);
|
||||
loadData();
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void doLoadData(int offset, int count){
|
||||
currentRequest=new GetFollowedTags(offset>0 ? maxID : null, count)
|
||||
.setCallback(new SimpleCallback<>(this){
|
||||
@Override
|
||||
public void onSuccess(HeaderPaginationList<Hashtag> result){
|
||||
maxID=null;
|
||||
if(result.nextPageUri!=null)
|
||||
maxID=result.nextPageUri.getQueryParameter("max_id");
|
||||
onDataLoaded(result.stream().map(t->{
|
||||
int posts=t.getWeekPosts();
|
||||
return new ListItemWithOptionsMenu<>(t.name, getResources().getQuantityString(R.plurals.x_posts_recently, posts, posts), ManageFollowedHashtagsFragment.this,
|
||||
R.drawable.ic_tag_24px, ManageFollowedHashtagsFragment.this::onItemClick, t, false);
|
||||
}).collect(Collectors.toList()), maxID!=null);
|
||||
}
|
||||
})
|
||||
.exec(accountID);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onConfigureListItemOptionsMenu(ListItemWithOptionsMenu<Hashtag> item, Menu menu){
|
||||
menu.clear();
|
||||
menu.add(getString(R.string.unfollow_user, "#"+item.parentObject.name));
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onListItemOptionSelected(ListItemWithOptionsMenu<Hashtag> item, MenuItem menuItem){
|
||||
new M3AlertDialogBuilder(getActivity())
|
||||
.setTitle(getString(R.string.unfollow_confirmation, "#"+item.parentObject.name))
|
||||
.setPositiveButton(R.string.unfollow, (dlg, which)->doUnfollow(item))
|
||||
.setNegativeButton(R.string.cancel, null)
|
||||
.show();
|
||||
}
|
||||
|
||||
private void onItemClick(ListItemWithOptionsMenu<Hashtag> item){
|
||||
UiUtils.openHashtagTimeline(getActivity(), accountID, item.parentObject);
|
||||
}
|
||||
|
||||
private void doUnfollow(ListItemWithOptionsMenu<Hashtag> item){
|
||||
new SetTagFollowed(item.parentObject.name, false)
|
||||
.setCallback(new Callback<>(){
|
||||
@Override
|
||||
public void onSuccess(Hashtag result){
|
||||
int index=data.indexOf(item);
|
||||
if(index==-1)
|
||||
return;
|
||||
data.remove(index);
|
||||
list.getAdapter().notifyItemRemoved(index);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onError(ErrorResponse error){
|
||||
error.showToast(getActivity());
|
||||
}
|
||||
})
|
||||
.wrapProgress(getActivity(), R.string.loading, true)
|
||||
.exec(accountID);
|
||||
}
|
||||
}
|
@ -0,0 +1,152 @@
|
||||
package org.joinmastodon.android.fragments;
|
||||
|
||||
import android.app.Activity;
|
||||
import android.os.Bundle;
|
||||
import android.view.Menu;
|
||||
import android.view.MenuItem;
|
||||
|
||||
import com.squareup.otto.Subscribe;
|
||||
|
||||
import org.joinmastodon.android.E;
|
||||
import org.joinmastodon.android.R;
|
||||
import org.joinmastodon.android.api.requests.lists.DeleteList;
|
||||
import org.joinmastodon.android.api.session.AccountSessionManager;
|
||||
import org.joinmastodon.android.events.ListDeletedEvent;
|
||||
import org.joinmastodon.android.events.ListUpdatedEvent;
|
||||
import org.joinmastodon.android.fragments.settings.BaseSettingsFragment;
|
||||
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.parceler.Parcels;
|
||||
|
||||
import java.util.List;
|
||||
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;
|
||||
|
||||
public class ManageListsFragment extends BaseSettingsFragment<FollowList> implements ListItemWithOptionsMenu.OptionsMenuListener<FollowList>{
|
||||
@Override
|
||||
public void onCreate(Bundle savedInstanceState){
|
||||
super.onCreate(savedInstanceState);
|
||||
setTitle(R.string.manage_lists);
|
||||
loadData();
|
||||
setRefreshEnabled(true);
|
||||
E.register(this);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onDestroy(){
|
||||
super.onDestroy();
|
||||
E.unregister(this);
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void doLoadData(int offset, int count){
|
||||
Callback<List<FollowList>> callback=new SimpleCallback<>(this){
|
||||
@Override
|
||||
public void onSuccess(List<FollowList> result){
|
||||
onDataLoaded(result.stream().map(l->new ListItemWithOptionsMenu<>(l.title, null, ManageListsFragment.this, R.drawable.ic_list_alt_24px, ManageListsFragment.this::onListClick, l, false)).collect(Collectors.toList()), false);
|
||||
}
|
||||
};
|
||||
if(refreshing){
|
||||
AccountSessionManager.get(accountID)
|
||||
.getCacheController()
|
||||
.reloadLists(callback);
|
||||
}else{
|
||||
AccountSessionManager.get(accountID)
|
||||
.getCacheController()
|
||||
.getLists(callback);
|
||||
}
|
||||
}
|
||||
|
||||
private void onListClick(ListItemWithOptionsMenu<FollowList> item){
|
||||
Bundle args=new Bundle();
|
||||
args.putString("account", accountID);
|
||||
args.putParcelable("list", Parcels.wrap(item.parentObject));
|
||||
Nav.go(getActivity(), ListTimelineFragment.class, args);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onConfigureListItemOptionsMenu(ListItemWithOptionsMenu<FollowList> item, Menu menu){
|
||||
menu.add(0, R.id.edit, 0, R.string.edit_list);
|
||||
menu.add(0, R.id.delete, 1, R.string.delete_list);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onListItemOptionSelected(ListItemWithOptionsMenu<FollowList> item, MenuItem menuItem){
|
||||
int id=menuItem.getItemId();
|
||||
if(id==R.id.edit){
|
||||
Bundle args=new Bundle();
|
||||
args.putString("account", accountID);
|
||||
args.putParcelable("list", Parcels.wrap(item.parentObject));
|
||||
Nav.go(getActivity(), EditListFragment.class, args);
|
||||
}else if(id==R.id.delete){
|
||||
new M3AlertDialogBuilder(getActivity())
|
||||
.setTitle(R.string.delete_list)
|
||||
.setMessage(getString(R.string.delete_list_confirm, item.parentObject.title))
|
||||
.setPositiveButton(R.string.delete, (dlg, which)->doDeleteList(item.parentObject))
|
||||
.setNegativeButton(R.string.cancel, null)
|
||||
.show();
|
||||
}
|
||||
}
|
||||
|
||||
private void doDeleteList(FollowList list){
|
||||
new DeleteList(list.id)
|
||||
.setCallback(new Callback<>(){
|
||||
@Override
|
||||
public void onSuccess(Void result){
|
||||
for(int i=0;i<data.size();i++){
|
||||
if(data.get(i).parentObject==list){
|
||||
data.remove(i);
|
||||
itemsAdapter.notifyItemRemoved(i);
|
||||
AccountSessionManager.get(accountID).getCacheController().reloadLists(null);
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onError(ErrorResponse error){
|
||||
Activity activity=getActivity();
|
||||
if(activity==null)
|
||||
return;
|
||||
error.showToast(activity);
|
||||
}
|
||||
})
|
||||
.wrapProgress(getActivity(), R.string.loading, true)
|
||||
.exec(accountID);
|
||||
}
|
||||
|
||||
@Subscribe
|
||||
public void onListUpdated(ListUpdatedEvent ev){
|
||||
if(!ev.accountID.equals(accountID))
|
||||
return;
|
||||
for(ListItem<FollowList> item:data){
|
||||
if(item.parentObject.id.equals(ev.list.id)){
|
||||
item.parentObject=ev.list;
|
||||
item.title=ev.list.title;
|
||||
rebindItem(item);
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Subscribe
|
||||
public void onListDeleted(ListDeletedEvent ev){
|
||||
if(!ev.accountID.equals(accountID))
|
||||
return;
|
||||
int i=0;
|
||||
for(ListItem<FollowList> item:data){
|
||||
if(item.parentObject.id.equals(ev.listID)){
|
||||
data.remove(i);
|
||||
itemsAdapter.notifyItemRemoved(i);
|
||||
break;
|
||||
}
|
||||
i++;
|
||||
}
|
||||
}
|
||||
}
|
@ -5,6 +5,7 @@ import android.view.View;
|
||||
import android.widget.Toolbar;
|
||||
|
||||
import org.joinmastodon.android.R;
|
||||
import org.joinmastodon.android.ui.BetterItemAnimator;
|
||||
import org.joinmastodon.android.ui.utils.UiUtils;
|
||||
import org.joinmastodon.android.utils.ElevationOnScrollListener;
|
||||
|
||||
@ -37,6 +38,7 @@ public abstract class MastodonRecyclerFragment<T> extends BaseRecyclerFragment<T
|
||||
super.onViewCreated(view, savedInstanceState);
|
||||
if(wantsElevationOnScrollEffect())
|
||||
list.addOnScrollListener(elevationOnScrollListener=new ElevationOnScrollListener((FragmentRootLinearLayout) view, getViewsForElevationEffect()));
|
||||
list.setItemAnimator(new BetterItemAnimator());
|
||||
if(refreshLayout!=null){
|
||||
int colorBackground=UiUtils.getThemeColor(getActivity(), R.attr.colorM3Background);
|
||||
int colorPrimary=UiUtils.getThemeColor(getActivity(), R.attr.colorM3Primary);
|
||||
|
@ -609,6 +609,7 @@ public class ProfileFragment extends LoaderFragment implements OnBackPressedList
|
||||
menu.findItem(R.id.block_domain).setTitle(getString(relationship.domainBlocking ? R.string.unblock_domain : R.string.block_domain, account.getDomain()));
|
||||
else
|
||||
menu.findItem(R.id.block_domain).setVisible(false);
|
||||
menu.findItem(R.id.add_to_list).setVisible(relationship.following);
|
||||
}
|
||||
|
||||
@Override
|
||||
@ -662,6 +663,11 @@ public class ProfileFragment extends LoaderFragment implements OnBackPressedList
|
||||
}else if(id==R.id.save){
|
||||
if(isInEditMode)
|
||||
saveAndExitEditMode();
|
||||
}else if(id==R.id.add_to_list){
|
||||
Bundle args=new Bundle();
|
||||
args.putString("account", accountID);
|
||||
args.putParcelable("targetAccount", Parcels.wrap(account));
|
||||
Nav.go(getActivity(), AddAccountToListsFragment.class, args);
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
@ -5,7 +5,9 @@ import android.text.TextUtils;
|
||||
import android.view.View;
|
||||
|
||||
import org.joinmastodon.android.R;
|
||||
import org.joinmastodon.android.api.MastodonAPIRequest;
|
||||
import org.joinmastodon.android.api.requests.search.GetSearchResults;
|
||||
import org.joinmastodon.android.model.Account;
|
||||
import org.joinmastodon.android.model.SearchResults;
|
||||
import org.joinmastodon.android.model.viewmodel.AccountViewModel;
|
||||
import org.joinmastodon.android.ui.SearchViewHelper;
|
||||
@ -13,13 +15,14 @@ import org.joinmastodon.android.ui.utils.UiUtils;
|
||||
import org.joinmastodon.android.ui.viewholders.AccountViewHolder;
|
||||
import org.parceler.Parcels;
|
||||
|
||||
import java.util.List;
|
||||
import java.util.stream.Collectors;
|
||||
|
||||
import me.grishka.appkit.Nav;
|
||||
import me.grishka.appkit.api.SimpleCallback;
|
||||
|
||||
public class ComposeAccountSearchFragment extends BaseAccountListFragment{
|
||||
private String currentQuery;
|
||||
public class AccountSearchFragment extends BaseAccountListFragment{
|
||||
protected String currentQuery;
|
||||
private boolean resultDelivered;
|
||||
private SearchViewHelper searchViewHelper;
|
||||
|
||||
@ -33,7 +36,7 @@ public class ComposeAccountSearchFragment extends BaseAccountListFragment{
|
||||
|
||||
@Override
|
||||
public void onViewCreated(View view, Bundle savedInstanceState){
|
||||
searchViewHelper=new SearchViewHelper(getActivity(), getToolbarContext(), getString(R.string.search_hint));
|
||||
searchViewHelper=new SearchViewHelper(getActivity(), getToolbarContext(), getSearchViewPlaceholder());
|
||||
searchViewHelper.setListeners(this::onQueryChanged, null);
|
||||
searchViewHelper.addDivider(contentView);
|
||||
super.onViewCreated(view, savedInstanceState);
|
||||
@ -51,13 +54,21 @@ public class ComposeAccountSearchFragment extends BaseAccountListFragment{
|
||||
.setCallback(new SimpleCallback<>(this){
|
||||
@Override
|
||||
public void onSuccess(SearchResults result){
|
||||
setEmptyText(R.string.no_search_results);
|
||||
onDataLoaded(result.accounts.stream().map(a->new AccountViewModel(a, accountID)).collect(Collectors.toList()), false);
|
||||
AccountSearchFragment.this.onSuccess(result.accounts);
|
||||
}
|
||||
})
|
||||
.exec(accountID);
|
||||
}
|
||||
|
||||
protected void onSuccess(List<Account> result){
|
||||
setEmptyText(R.string.no_search_results);
|
||||
onDataLoaded(result.stream().map(a->new AccountViewModel(a, accountID)).collect(Collectors.toList()), false);
|
||||
}
|
||||
|
||||
protected String getSearchViewPlaceholder(){
|
||||
return getString(R.string.search_hint);
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void onUpdateToolbar(){
|
||||
super.onUpdateToolbar();
|
@ -0,0 +1,29 @@
|
||||
package org.joinmastodon.android.fragments.account_list;
|
||||
|
||||
import org.joinmastodon.android.R;
|
||||
import org.joinmastodon.android.api.requests.accounts.SearchAccounts;
|
||||
import org.joinmastodon.android.model.Account;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
import me.grishka.appkit.api.SimpleCallback;
|
||||
|
||||
public class AddListMembersFragment extends AccountSearchFragment{
|
||||
@Override
|
||||
protected void doLoadData(int offset, int count){
|
||||
refreshing=true;
|
||||
currentRequest=new SearchAccounts(currentQuery, 0, 0, false, true)
|
||||
.setCallback(new SimpleCallback<>(this){
|
||||
@Override
|
||||
public void onSuccess(List<Account> result){
|
||||
AddListMembersFragment.this.onSuccess(result);
|
||||
}
|
||||
})
|
||||
.exec(accountID);
|
||||
}
|
||||
|
||||
@Override
|
||||
protected String getSearchViewPlaceholder(){
|
||||
return getString(R.string.search_among_people_you_follow);
|
||||
}
|
||||
}
|
@ -150,6 +150,7 @@ public abstract class BaseAccountListFragment extends MastodonRecyclerFragment<A
|
||||
}
|
||||
|
||||
protected void onConfigureViewHolder(AccountViewHolder holder){}
|
||||
protected void onBindViewHolder(AccountViewHolder holder){}
|
||||
|
||||
protected class AccountsAdapter extends UsableRecyclerView.Adapter<AccountViewHolder> implements ImageLoaderRecyclerAdapter{
|
||||
public AccountsAdapter(){
|
||||
@ -167,6 +168,7 @@ public abstract class BaseAccountListFragment extends MastodonRecyclerFragment<A
|
||||
@Override
|
||||
public void onBindViewHolder(AccountViewHolder holder, int position){
|
||||
holder.bind(data.get(position));
|
||||
BaseAccountListFragment.this.onBindViewHolder(holder);
|
||||
super.onBindViewHolder(holder, position);
|
||||
}
|
||||
|
||||
|
@ -1,7 +1,6 @@
|
||||
package org.joinmastodon.android.fragments.discover;
|
||||
|
||||
import android.os.Bundle;
|
||||
import android.view.View;
|
||||
|
||||
import org.joinmastodon.android.api.requests.timelines.GetPublicTimeline;
|
||||
import org.joinmastodon.android.api.session.AccountSessionManager;
|
||||
@ -29,7 +28,7 @@ public class LocalTimelineFragment extends StatusListFragment{
|
||||
|
||||
@Override
|
||||
protected void doLoadData(int offset, int count){
|
||||
currentRequest=new GetPublicTimeline(true, false, refreshing ? null : maxID, count)
|
||||
currentRequest=new GetPublicTimeline(true, false, refreshing ? null : maxID, null, count, null)
|
||||
.setCallback(new SimpleCallback<>(this){
|
||||
@Override
|
||||
public void onSuccess(List<Status> result){
|
||||
|
@ -112,7 +112,7 @@ public class SearchQueryFragment extends MastodonRecyclerFragment<SearchResultVi
|
||||
onDataLoaded(results.stream().map(sr->{
|
||||
SearchResultViewModel vm=new SearchResultViewModel(sr, accountID, true);
|
||||
if(sr.type==SearchResult.Type.HASHTAG){
|
||||
vm.hashtagItem.onClick=()->openHashtag(sr);
|
||||
vm.hashtagItem.setOnClick(i->openHashtag(sr));
|
||||
}
|
||||
return vm;
|
||||
}).collect(Collectors.toList()), false);
|
||||
@ -129,7 +129,7 @@ public class SearchQueryFragment extends MastodonRecyclerFragment<SearchResultVi
|
||||
.map(sr->{
|
||||
SearchResultViewModel vm=new SearchResultViewModel(sr, accountID, false);
|
||||
if(sr.type==SearchResult.Type.HASHTAG){
|
||||
vm.hashtagItem.onClick=()->openHashtag(sr);
|
||||
vm.hashtagItem.setOnClick(i->openHashtag(sr));
|
||||
}
|
||||
return vm;
|
||||
})
|
||||
@ -389,18 +389,18 @@ public class SearchQueryFragment extends MastodonRecyclerFragment<SearchResultVi
|
||||
deliverResult(currentQuery, null);
|
||||
}
|
||||
|
||||
private void onOpenURLClick(){
|
||||
private void onOpenURLClick(ListItem<?> item_){
|
||||
((MainActivity)getActivity()).handleURL(Uri.parse(searchViewHelper.getQuery()), accountID);
|
||||
}
|
||||
|
||||
private void onGoToHashtagClick(){
|
||||
private void onGoToHashtagClick(ListItem<?> item_){
|
||||
String q=searchViewHelper.getQuery();
|
||||
if(q.startsWith("#"))
|
||||
q=q.substring(1);
|
||||
UiUtils.openHashtagTimeline(getActivity(), accountID, q);
|
||||
}
|
||||
|
||||
private void onGoToAccountClick(){
|
||||
private void onGoToAccountClick(ListItem<?> item_){
|
||||
String q=searchViewHelper.getQuery();
|
||||
if(!q.startsWith("@")){
|
||||
q="@"+q;
|
||||
@ -411,11 +411,11 @@ public class SearchQueryFragment extends MastodonRecyclerFragment<SearchResultVi
|
||||
((MainActivity)getActivity()).openSearchQuery(q, accountID, R.string.loading, true);
|
||||
}
|
||||
|
||||
private void onGoToStatusSearchClick(){
|
||||
private void onGoToStatusSearchClick(ListItem<?> item_){
|
||||
deliverResult(searchViewHelper.getQuery(), SearchResult.Type.STATUS);
|
||||
}
|
||||
|
||||
private void onGoToAccountSearchClick(){
|
||||
private void onGoToAccountSearchClick(ListItem<?> item_){
|
||||
deliverResult(searchViewHelper.getQuery(), SearchResult.Type.ACCOUNT);
|
||||
}
|
||||
|
||||
|
@ -12,12 +12,10 @@ 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;
|
||||
@ -45,7 +43,7 @@ public abstract class BaseSettingsFragment<T> extends MastodonRecyclerFragment<L
|
||||
|
||||
@Override
|
||||
protected RecyclerView.Adapter<?> getAdapter(){
|
||||
return itemsAdapter=new GenericListItemsAdapter<T>(data);
|
||||
return itemsAdapter=new GenericListItemsAdapter<T>(imgLoader, data);
|
||||
}
|
||||
|
||||
@Override
|
||||
@ -59,12 +57,13 @@ public abstract class BaseSettingsFragment<T> extends MastodonRecyclerFragment<L
|
||||
return 0;
|
||||
}
|
||||
|
||||
protected void toggleCheckableItem(CheckableListItem<T> item){
|
||||
item.toggle();
|
||||
protected void toggleCheckableItem(ListItem<?> item){
|
||||
if(item instanceof CheckableListItem<?> checkable)
|
||||
checkable.toggle();
|
||||
rebindItem(item);
|
||||
}
|
||||
|
||||
protected void rebindItem(ListItem<T> item){
|
||||
protected void rebindItem(ListItem<?> item){
|
||||
if(list==null)
|
||||
return;
|
||||
if(list.findViewHolderForAdapterPosition(indexOfItemsAdapter()+data.indexOf(item)) instanceof ListItemViewHolder<?> holder){
|
||||
|
@ -73,7 +73,7 @@ public class EditFilterFragment extends BaseSettingsFragment<Void> implements On
|
||||
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))
|
||||
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, this::toggleCheckableItem)
|
||||
));
|
||||
|
||||
if(filter!=null){
|
||||
@ -113,7 +113,7 @@ public class EditFilterFragment extends BaseSettingsFragment<Void> implements On
|
||||
return 1;
|
||||
}
|
||||
|
||||
private void onDurationClick(){
|
||||
private void onDurationClick(ListItem<Void> item_){
|
||||
int[] durationOptions={
|
||||
1800,
|
||||
3600,
|
||||
@ -182,21 +182,21 @@ public class EditFilterFragment extends BaseSettingsFragment<Void> implements On
|
||||
alert.setOnDismissListener(dialog->callback.accept(null));
|
||||
}
|
||||
|
||||
private void onWordsClick(){
|
||||
private void onWordsClick(ListItem<Void> item){
|
||||
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(){
|
||||
private void onContextClick(ListItem<Void> item){
|
||||
Bundle args=new Bundle();
|
||||
args.putString("account", accountID);
|
||||
args.putSerializable("context", context);
|
||||
Nav.goForResult(getActivity(), FilterContextFragment.class, args, CONTEXT_RESULT, this);
|
||||
}
|
||||
|
||||
private void onDeleteClick(){
|
||||
private void onDeleteClick(ListItem<Void> item_){
|
||||
AlertDialog alert=new M3AlertDialogBuilder(getActivity())
|
||||
.setTitle(getString(R.string.settings_delete_filter_title, filter.title))
|
||||
.setMessage(R.string.settings_delete_filter_confirmation)
|
||||
|
@ -22,10 +22,9 @@ public class FilterContextFragment extends BaseSettingsFragment<FilterContext> i
|
||||
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);
|
||||
CheckableListItem<FilterContext> item=new CheckableListItem<>(c.getDisplayNameRes(), 0, CheckableListItem.Style.CHECKBOX, context.contains(c), this::toggleCheckableItem);
|
||||
item.parentObject=c;
|
||||
item.isEnabled=true;
|
||||
item.onClick=()->toggleCheckableItem(item);
|
||||
return item;
|
||||
}).collect(Collectors.toList()));
|
||||
}
|
||||
|
@ -1,11 +1,6 @@
|
||||
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;
|
||||
@ -27,6 +22,7 @@ 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.ActionModeHelper;
|
||||
import org.joinmastodon.android.ui.utils.SimpleTextWatcher;
|
||||
import org.joinmastodon.android.ui.utils.UiUtils;
|
||||
import org.joinmastodon.android.ui.views.FloatingHintEditTextLayout;
|
||||
@ -37,7 +33,6 @@ 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;
|
||||
|
||||
@ -60,7 +55,7 @@ public class FilterWordsFragment extends BaseSettingsFragment<FilterKeyword> imp
|
||||
FilterKeyword word=Parcels.unwrap(p);
|
||||
ListItem<FilterKeyword> item=new ListItem<>(word.keyword, null, null, word);
|
||||
item.isEnabled=true;
|
||||
item.onClick=()->onWordClick(item);
|
||||
item.setOnClick(this::onWordClick);
|
||||
return item;
|
||||
}).collect(Collectors.toList()));
|
||||
setHasOptionsMenu(true);
|
||||
@ -114,7 +109,7 @@ public class FilterWordsFragment extends BaseSettingsFragment<FilterKeyword> imp
|
||||
|
||||
@Override
|
||||
public void onCreateOptionsMenu(Menu menu, MenuInflater inflater){
|
||||
inflater.inflate(R.menu.settings_filter_words, menu);
|
||||
inflater.inflate(R.menu.selectable_list, menu);
|
||||
}
|
||||
|
||||
@Override
|
||||
@ -174,7 +169,7 @@ public class FilterWordsFragment extends BaseSettingsFragment<FilterKeyword> imp
|
||||
w.keyword=input;
|
||||
ListItem<FilterKeyword> item=new ListItem<>(w.keyword, null, null, w);
|
||||
item.isEnabled=true;
|
||||
item.onClick=()->onWordClick(item);
|
||||
item.setOnClick(this::onWordClick);
|
||||
data.add(item);
|
||||
itemsAdapter.notifyItemInserted(data.size()-1);
|
||||
}else{
|
||||
@ -228,29 +223,15 @@ public class FilterWordsFragment extends BaseSettingsFragment<FilterKeyword> imp
|
||||
return;
|
||||
V.setVisibilityAnimated(fab, View.GONE);
|
||||
|
||||
actionMode=getActivity().startActionMode(new ActionMode.Callback(){
|
||||
actionMode=ActionModeHelper.startActionMode(this, ()->elevationOnScrollListener.getCurrentStatusBarColor(), 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;
|
||||
}
|
||||
@ -266,21 +247,6 @@ public class FilterWordsFragment extends BaseSettingsFragment<FilterKeyword> imp
|
||||
@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);
|
||||
}
|
||||
});
|
||||
|
||||
@ -289,7 +255,7 @@ public class FilterWordsFragment extends BaseSettingsFragment<FilterKeyword> imp
|
||||
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.setOnClick(this::onSelectionModeWordClick);
|
||||
newItem.parentObject=item.parentObject;
|
||||
if(selectAll)
|
||||
selectedItems.add(newItem);
|
||||
@ -313,7 +279,7 @@ public class FilterWordsFragment extends BaseSettingsFragment<FilterKeyword> imp
|
||||
ListItem<FilterKeyword> item=data.get(i);
|
||||
ListItem<FilterKeyword> newItem=new ListItem<>(item.title, null, null);
|
||||
newItem.isEnabled=true;
|
||||
newItem.onClick=()->onWordClick(newItem);
|
||||
newItem.setOnClick(this::onWordClick);
|
||||
newItem.parentObject=item.parentObject;
|
||||
data.set(i, newItem);
|
||||
}
|
||||
|
@ -32,10 +32,10 @@ public class SettingsAboutAppFragment extends BaseSettingsFragment<Void>{
|
||||
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),
|
||||
new ListItem<>(R.string.settings_even_more, 0, i->UiUtils.launchWebBrowser(getActivity(), "https://"+s.domain+"/auth/edit")),
|
||||
new ListItem<>(R.string.settings_contribute, 0, i->UiUtils.launchWebBrowser(getActivity(), getString(R.string.github_url))),
|
||||
new ListItem<>(R.string.settings_tos, 0, i->UiUtils.launchWebBrowser(getActivity(), "https://"+s.domain+"/terms")),
|
||||
new ListItem<>(R.string.settings_privacy_policy, 0, i->UiUtils.launchWebBrowser(getActivity(), getString(R.string.privacy_policy_url)), 0, true),
|
||||
mediaCacheItem=new ListItem<>(R.string.settings_clear_cache, 0, this::onClearMediaCacheClick)
|
||||
));
|
||||
|
||||
@ -62,7 +62,7 @@ public class SettingsAboutAppFragment extends BaseSettingsFragment<Void>{
|
||||
return adapter;
|
||||
}
|
||||
|
||||
private void onClearMediaCacheClick(){
|
||||
private void onClearMediaCacheClick(ListItem<?> item){
|
||||
MastodonAPIController.runInBackground(()->{
|
||||
Activity activity=getActivity();
|
||||
ImageCache.getInstance(getActivity()).clear();
|
||||
|
@ -33,19 +33,19 @@ public class SettingsBehaviorFragment extends BaseSettingsFragment<Void>{
|
||||
|
||||
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))
|
||||
altTextItem=new CheckableListItem<>(R.string.settings_alt_text_reminders, 0, CheckableListItem.Style.SWITCH, GlobalUserPreferences.altTextReminders, R.drawable.ic_alt_24px, this::toggleCheckableItem),
|
||||
playGifsItem=new CheckableListItem<>(R.string.settings_gif, 0, CheckableListItem.Style.SWITCH, GlobalUserPreferences.playGifs, R.drawable.ic_animation_24px, this::toggleCheckableItem),
|
||||
customTabsItem=new CheckableListItem<>(R.string.settings_custom_tabs, 0, CheckableListItem.Style.SWITCH, GlobalUserPreferences.useCustomTabs, R.drawable.ic_open_in_browser_24px, this::toggleCheckableItem),
|
||||
confirmUnfollowItem=new CheckableListItem<>(R.string.settings_confirm_unfollow, 0, CheckableListItem.Style.SWITCH, GlobalUserPreferences.confirmUnfollow, R.drawable.ic_person_remove_24px, this::toggleCheckableItem),
|
||||
confirmBoostItem=new CheckableListItem<>(R.string.settings_confirm_boost, 0, CheckableListItem.Style.SWITCH, GlobalUserPreferences.confirmBoost, R.drawable.ic_repeat_24px, this::toggleCheckableItem),
|
||||
confirmDeleteItem=new CheckableListItem<>(R.string.settings_confirm_delete_post, 0, CheckableListItem.Style.SWITCH, GlobalUserPreferences.confirmDeletePost, R.drawable.ic_delete_24px, this::toggleCheckableItem)
|
||||
));
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void doLoadData(int offset, int count){}
|
||||
|
||||
private void onDefaultLanguageClick(){
|
||||
private void onDefaultLanguageClick(ListItem<?> item){
|
||||
ComposeLanguageAlertViewController vc=new ComposeLanguageAlertViewController(getActivity(), null, new ComposeLanguageAlertViewController.SelectedOption(-1, postLanguage), null);
|
||||
new M3AlertDialogBuilder(getActivity())
|
||||
.setTitle(R.string.default_post_language)
|
||||
|
@ -39,7 +39,7 @@ public class SettingsDebugFragment extends BaseSettingsFragment<Void>{
|
||||
@Override
|
||||
protected void doLoadData(int offset, int count){}
|
||||
|
||||
private void onTestEmailConfirmClick(){
|
||||
private void onTestEmailConfirmClick(ListItem<?> item){
|
||||
AccountSession sess=AccountSessionManager.getInstance().getAccount(accountID);
|
||||
sess.activated=false;
|
||||
sess.activationInfo=new AccountActivationInfo("test@email", System.currentTimeMillis());
|
||||
@ -49,18 +49,18 @@ public class SettingsDebugFragment extends BaseSettingsFragment<Void>{
|
||||
Nav.goClearingStack(getActivity(), AccountActivationFragment.class, args);
|
||||
}
|
||||
|
||||
private void onForceSelfUpdateClick(){
|
||||
private void onForceSelfUpdateClick(ListItem<?> item){
|
||||
GithubSelfUpdater.forceUpdate=true;
|
||||
GithubSelfUpdater.getInstance().maybeCheckForUpdates();
|
||||
restartUI();
|
||||
}
|
||||
|
||||
private void onResetUpdaterClick(){
|
||||
private void onResetUpdaterClick(ListItem<?> item){
|
||||
GithubSelfUpdater.getInstance().reset();
|
||||
restartUI();
|
||||
}
|
||||
|
||||
private void onResetDiscoverBannersClick(){
|
||||
private void onResetDiscoverBannersClick(ListItem<?> item){
|
||||
DiscoverInfoBannerHelper.reset();
|
||||
restartUI();
|
||||
}
|
||||
|
@ -39,10 +39,10 @@ public class SettingsDisplayFragment extends BaseSettingsFragment<Void>{
|
||||
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))
|
||||
showCWsItem=new CheckableListItem<>(R.string.settings_show_cws, 0, CheckableListItem.Style.SWITCH, lp.showCWs, R.drawable.ic_warning_24px, this::toggleCheckableItem),
|
||||
hideSensitiveMediaItem=new CheckableListItem<>(R.string.settings_hide_sensitive_media, 0, CheckableListItem.Style.SWITCH, lp.hideSensitiveMedia, R.drawable.ic_no_adult_content_24px, this::toggleCheckableItem),
|
||||
interactionCountsItem=new CheckableListItem<>(R.string.settings_show_interaction_counts, 0, CheckableListItem.Style.SWITCH, lp.showInteractionCounts, R.drawable.ic_social_leaderboard_24px, this::toggleCheckableItem),
|
||||
emojiInNamesItem=new CheckableListItem<>(R.string.settings_show_emoji_in_names, 0, CheckableListItem.Style.SWITCH, lp.customEmojiInNames, R.drawable.ic_emoticon_24px, this::toggleCheckableItem)
|
||||
));
|
||||
}
|
||||
|
||||
@ -80,7 +80,7 @@ public class SettingsDisplayFragment extends BaseSettingsFragment<Void>{
|
||||
};
|
||||
}
|
||||
|
||||
private void onAppearanceClick(){
|
||||
private void onAppearanceClick(ListItem<?> item_){
|
||||
int selected=switch(GlobalUserPreferences.theme){
|
||||
case LIGHT -> 0;
|
||||
case DARK -> 1;
|
||||
|
@ -67,16 +67,14 @@ public class SettingsFiltersFragment extends BaseSettingsFragment<Filter>{
|
||||
Nav.go(getActivity(), EditFilterFragment.class, args);
|
||||
}
|
||||
|
||||
private void onAddFilterClick(){
|
||||
private void onAddFilterClick(ListItem<?> item){
|
||||
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;
|
||||
ListItem<Filter> item=new ListItem<>(f.title, getString(f.isActive() ? R.string.filter_active : R.string.filter_inactive), this::onFilterClick, f);
|
||||
return item;
|
||||
}
|
||||
|
||||
|
@ -61,7 +61,7 @@ public class SettingsMainFragment extends BaseSettingsFragment<Void>{
|
||||
));
|
||||
|
||||
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));
|
||||
data.add(0, new ListItem<>("Debug settings", null, R.drawable.ic_settings_24px, i->Nav.go(getActivity(), SettingsDebugFragment.class, makeFragmentArgs()), null, 0, true));
|
||||
}
|
||||
|
||||
AccountSession session=AccountSessionManager.get(accountID);
|
||||
@ -122,35 +122,35 @@ public class SettingsMainFragment extends BaseSettingsFragment<Void>{
|
||||
return args;
|
||||
}
|
||||
|
||||
private void onBehaviorClick(){
|
||||
private void onBehaviorClick(ListItem<?> item_){
|
||||
Nav.go(getActivity(), SettingsBehaviorFragment.class, makeFragmentArgs());
|
||||
}
|
||||
|
||||
private void onDisplayClick(){
|
||||
private void onDisplayClick(ListItem<?> item_){
|
||||
Nav.go(getActivity(), SettingsDisplayFragment.class, makeFragmentArgs());
|
||||
}
|
||||
|
||||
private void onPrivacyClick(){
|
||||
private void onPrivacyClick(ListItem<?> item_){
|
||||
Nav.go(getActivity(), SettingsPrivacyFragment.class, makeFragmentArgs());
|
||||
}
|
||||
|
||||
private void onFiltersClick(){
|
||||
private void onFiltersClick(ListItem<?> item_){
|
||||
Nav.go(getActivity(), SettingsFiltersFragment.class, makeFragmentArgs());
|
||||
}
|
||||
|
||||
private void onNotificationsClick(){
|
||||
private void onNotificationsClick(ListItem<?> item_){
|
||||
Nav.go(getActivity(), SettingsNotificationsFragment.class, makeFragmentArgs());
|
||||
}
|
||||
|
||||
private void onServerClick(){
|
||||
private void onServerClick(ListItem<?> item_){
|
||||
Nav.go(getActivity(), SettingsServerFragment.class, makeFragmentArgs());
|
||||
}
|
||||
|
||||
private void onAboutClick(){
|
||||
private void onAboutClick(ListItem<?> item_){
|
||||
Nav.go(getActivity(), SettingsAboutAppFragment.class, makeFragmentArgs());
|
||||
}
|
||||
|
||||
private void onLogOutClick(){
|
||||
private void onLogOutClick(ListItem<?> item_){
|
||||
AccountSession session=AccountSessionManager.getInstance().getAccount(accountID);
|
||||
new M3AlertDialogBuilder(getActivity())
|
||||
.setMessage(getString(R.string.confirm_log_out, session.getFullUsername()))
|
||||
|
@ -55,14 +55,14 @@ public class SettingsNotificationsFragment extends BaseSettingsFragment<Void>{
|
||||
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)),
|
||||
pauseItem=new CheckableListItem<>(getString(R.string.pause_all_notifications), getPauseItemSubtitle(), CheckableListItem.Style.SWITCH, false, R.drawable.ic_notifications_paused_24px, i->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))
|
||||
mentionsItem=new CheckableListItem<>(R.string.notification_type_mentions_and_replies, 0, CheckableListItem.Style.CHECKBOX, pushSubscription.alerts.mention, this::toggleCheckableItem),
|
||||
boostsItem=new CheckableListItem<>(R.string.notification_type_reblog, 0, CheckableListItem.Style.CHECKBOX, pushSubscription.alerts.reblog, this::toggleCheckableItem),
|
||||
favoritesItem=new CheckableListItem<>(R.string.notification_type_favorite, 0, CheckableListItem.Style.CHECKBOX, pushSubscription.alerts.favourite, this::toggleCheckableItem),
|
||||
followersItem=new CheckableListItem<>(R.string.notification_type_follow, 0, CheckableListItem.Style.CHECKBOX, pushSubscription.alerts.follow, this::toggleCheckableItem),
|
||||
pollsItem=new CheckableListItem<>(R.string.notification_type_poll, 0, CheckableListItem.Style.CHECKBOX, pushSubscription.alerts.poll, this::toggleCheckableItem)
|
||||
));
|
||||
|
||||
typeItems=List.of(mentionsItem, boostsItem, favoritesItem, followersItem, pollsItem);
|
||||
@ -209,7 +209,7 @@ public class SettingsNotificationsFragment extends BaseSettingsFragment<Void>{
|
||||
alert.getButton(AlertDialog.BUTTON_POSITIVE).setEnabled(false);
|
||||
}
|
||||
|
||||
private void onNotificationsPolicyClick(){
|
||||
private void onNotificationsPolicyClick(ListItem<?> item_){
|
||||
String[] items=Stream.of(
|
||||
R.string.notifications_policy_anyone,
|
||||
R.string.notifications_policy_followed,
|
||||
|
@ -18,8 +18,8 @@ public class SettingsPrivacyFragment extends BaseSettingsFragment<Void>{
|
||||
setTitle(R.string.settings_privacy);
|
||||
Account self=AccountSessionManager.get(accountID).self;
|
||||
onDataLoaded(List.of(
|
||||
discoverableItem=new CheckableListItem<>(R.string.settings_discoverable, 0, CheckableListItem.Style.SWITCH, self.discoverable, R.drawable.ic_thumbs_up_down_24px, ()->toggleCheckableItem(discoverableItem)),
|
||||
indexableItem=new CheckableListItem<>(R.string.settings_indexable, 0, CheckableListItem.Style.SWITCH, self.source.indexable!=null ? self.source.indexable : true, R.drawable.ic_search_24px, ()->toggleCheckableItem(indexableItem))
|
||||
discoverableItem=new CheckableListItem<>(R.string.settings_discoverable, 0, CheckableListItem.Style.SWITCH, self.discoverable, R.drawable.ic_thumbs_up_down_24px, this::toggleCheckableItem),
|
||||
indexableItem=new CheckableListItem<>(R.string.settings_indexable, 0, CheckableListItem.Style.SWITCH, self.source.indexable!=null ? self.source.indexable : true, R.drawable.ic_search_24px, this::toggleCheckableItem)
|
||||
));
|
||||
if(self.source.indexable==null)
|
||||
indexableItem.isEnabled=false;
|
||||
|
@ -139,7 +139,7 @@ public class SettingsServerAboutFragment extends LoaderFragment{
|
||||
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, ()->{});
|
||||
ListItem<Void> item=new ListItem<>(R.string.send_email_to_server_admin, 0, R.drawable.ic_mail_24px, i->{});
|
||||
holder.bind(item);
|
||||
holder.itemView.setBackground(UiUtils.getThemeDrawable(getActivity(), android.R.attr.selectableItemBackground));
|
||||
holder.itemView.setOnClickListener(v->openAdminEmail());
|
||||
|
@ -0,0 +1,43 @@
|
||||
package org.joinmastodon.android.model;
|
||||
|
||||
import com.google.gson.annotations.SerializedName;
|
||||
|
||||
import org.joinmastodon.android.api.AllFieldsAreRequired;
|
||||
import org.joinmastodon.android.api.ObjectValidationException;
|
||||
import org.parceler.Parcel;
|
||||
|
||||
// Called like this to avoid conflict with java.util.List
|
||||
@AllFieldsAreRequired
|
||||
@Parcel
|
||||
public class FollowList extends BaseModel{
|
||||
public String id;
|
||||
public String title;
|
||||
public RepliesPolicy repliesPolicy=RepliesPolicy.LIST;
|
||||
public boolean exclusive;
|
||||
|
||||
@Override
|
||||
public String toString(){
|
||||
return "FollowList{"+
|
||||
"id='"+id+'\''+
|
||||
", title='"+title+'\''+
|
||||
", repliesPolicy="+repliesPolicy+
|
||||
", exclusive="+exclusive+
|
||||
'}';
|
||||
}
|
||||
|
||||
@Override
|
||||
public void postprocess() throws ObjectValidationException{
|
||||
if(repliesPolicy==null)
|
||||
repliesPolicy=RepliesPolicy.LIST;
|
||||
super.postprocess();
|
||||
}
|
||||
|
||||
public enum RepliesPolicy{
|
||||
@SerializedName("followed")
|
||||
FOLLOWED,
|
||||
@SerializedName("list")
|
||||
LIST,
|
||||
@SerializedName("none")
|
||||
NONE
|
||||
}
|
||||
}
|
@ -45,4 +45,8 @@ public class Hashtag extends BaseModel implements DisplayItemsParent{
|
||||
public int hashCode(){
|
||||
return name.hashCode();
|
||||
}
|
||||
|
||||
public int getWeekPosts(){
|
||||
return history.stream().mapToInt(h->h.uses).sum();
|
||||
}
|
||||
}
|
||||
|
@ -0,0 +1,22 @@
|
||||
package org.joinmastodon.android.model.viewmodel;
|
||||
|
||||
import org.joinmastodon.android.R;
|
||||
|
||||
import java.util.List;
|
||||
import java.util.function.Consumer;
|
||||
|
||||
import me.grishka.appkit.imageloader.requests.ImageLoaderRequest;
|
||||
|
||||
public class AvatarPileListItem<T> extends ListItem<T>{
|
||||
public List<ImageLoaderRequest> avatars;
|
||||
|
||||
public AvatarPileListItem(String title, String subtitle, List<ImageLoaderRequest> avatars, int iconRes, Consumer<AvatarPileListItem<T>> onClick, T parentObject, boolean dividerAfter){
|
||||
super(title, subtitle, iconRes, (Consumer<ListItem<T>>)(Object)onClick, parentObject, 0, dividerAfter);
|
||||
this.avatars=avatars;
|
||||
}
|
||||
|
||||
@Override
|
||||
public int getItemViewType(){
|
||||
return R.id.list_item_avatar_pile;
|
||||
}
|
||||
}
|
@ -9,42 +9,42 @@ public class CheckableListItem<T> extends ListItem<T>{
|
||||
public boolean checked;
|
||||
public Consumer<Boolean> checkedChangeListener;
|
||||
|
||||
public CheckableListItem(String title, String subtitle, Style style, boolean checked, int iconRes, Runnable onClick, T parentObject, boolean dividerAfter){
|
||||
super(title, subtitle, iconRes, onClick, parentObject, 0, dividerAfter);
|
||||
public CheckableListItem(String title, String subtitle, Style style, boolean checked, int iconRes, Consumer<CheckableListItem<T>> onClick, T parentObject, boolean dividerAfter){
|
||||
super(title, subtitle, iconRes, (Consumer<ListItem<T>>)(Object)onClick, parentObject, 0, dividerAfter);
|
||||
this.style=style;
|
||||
this.checked=checked;
|
||||
}
|
||||
|
||||
public CheckableListItem(String title, String subtitle, Style style, boolean checked, Runnable onClick){
|
||||
public CheckableListItem(String title, String subtitle, Style style, boolean checked, Consumer<CheckableListItem<T>> onClick){
|
||||
this(title, subtitle, style, checked, 0, onClick, null, false);
|
||||
}
|
||||
|
||||
public CheckableListItem(String title, String subtitle, Style style, boolean checked, Runnable onClick, T parentObject){
|
||||
public CheckableListItem(String title, String subtitle, Style style, boolean checked, Consumer<CheckableListItem<T>> onClick, T parentObject){
|
||||
this(title, subtitle, style, checked, 0, onClick, parentObject, false);
|
||||
}
|
||||
|
||||
public CheckableListItem(String title, String subtitle, Style style, boolean checked, int iconRes, Runnable onClick){
|
||||
public CheckableListItem(String title, String subtitle, Style style, boolean checked, int iconRes, Consumer<CheckableListItem<T>> onClick){
|
||||
this(title, subtitle, style, checked, iconRes, onClick, null, false);
|
||||
}
|
||||
|
||||
public CheckableListItem(String title, String subtitle, Style style, boolean checked, int iconRes, Runnable onClick, T parentObject){
|
||||
public CheckableListItem(String title, String subtitle, Style style, boolean checked, int iconRes, Consumer<CheckableListItem<T>> onClick, T parentObject){
|
||||
this(title, subtitle, style, checked, iconRes, onClick, parentObject, false);
|
||||
}
|
||||
|
||||
public CheckableListItem(int titleRes, int subtitleRes, Style style, boolean checked, Runnable onClick){
|
||||
public CheckableListItem(int titleRes, int subtitleRes, Style style, boolean checked, Consumer<CheckableListItem<T>> onClick){
|
||||
this(titleRes, subtitleRes, style, checked, 0, onClick, false);
|
||||
}
|
||||
|
||||
public CheckableListItem(int titleRes, int subtitleRes, Style style, boolean checked, Runnable onClick, boolean dividerAfter){
|
||||
public CheckableListItem(int titleRes, int subtitleRes, Style style, boolean checked, Consumer<CheckableListItem<T>> onClick, boolean dividerAfter){
|
||||
this(titleRes, subtitleRes, style, checked, 0, onClick, dividerAfter);
|
||||
}
|
||||
|
||||
public CheckableListItem(int titleRes, int subtitleRes, Style style, boolean checked, int iconRes, Runnable onClick){
|
||||
public CheckableListItem(int titleRes, int subtitleRes, Style style, boolean checked, int iconRes, Consumer<CheckableListItem<T>> onClick){
|
||||
this(titleRes, subtitleRes, style, checked, iconRes, onClick, false);
|
||||
}
|
||||
|
||||
public CheckableListItem(int titleRes, int subtitleRes, Style style, boolean checked, int iconRes, Runnable onClick, boolean dividerAfter){
|
||||
super(titleRes, subtitleRes, iconRes, onClick, 0, dividerAfter);
|
||||
public CheckableListItem(int titleRes, int subtitleRes, Style style, boolean checked, int iconRes, Consumer<CheckableListItem<T>> onClick, boolean dividerAfter){
|
||||
super(titleRes, subtitleRes, iconRes, (Consumer<ListItem<T>>)(Object)onClick, 0, dividerAfter);
|
||||
this.style=style;
|
||||
this.checked=checked;
|
||||
}
|
||||
|
@ -2,6 +2,8 @@ package org.joinmastodon.android.model.viewmodel;
|
||||
|
||||
import org.joinmastodon.android.R;
|
||||
|
||||
import java.util.function.Consumer;
|
||||
|
||||
import androidx.annotation.DrawableRes;
|
||||
import androidx.annotation.StringRes;
|
||||
|
||||
@ -16,11 +18,11 @@ public class ListItem<T>{
|
||||
public int iconRes;
|
||||
public int colorOverrideAttr;
|
||||
public boolean dividerAfter;
|
||||
public Runnable onClick;
|
||||
private Consumer<ListItem<T>> onClick;
|
||||
public boolean isEnabled=true;
|
||||
public T parentObject;
|
||||
|
||||
public ListItem(String title, String subtitle, int iconRes, Runnable onClick, T parentObject, int colorOverrideAttr, boolean dividerAfter){
|
||||
public ListItem(String title, String subtitle, int iconRes, Consumer<ListItem<T>> onClick, T parentObject, int colorOverrideAttr, boolean dividerAfter){
|
||||
this.title=title;
|
||||
this.subtitle=subtitle;
|
||||
this.iconRes=iconRes;
|
||||
@ -32,41 +34,41 @@ public class ListItem<T>{
|
||||
isEnabled=false;
|
||||
}
|
||||
|
||||
public ListItem(String title, String subtitle, Runnable onClick){
|
||||
public ListItem(String title, String subtitle, Consumer<ListItem<T>> onClick){
|
||||
this(title, subtitle, 0, onClick, null, 0, false);
|
||||
}
|
||||
|
||||
public ListItem(String title, String subtitle, Runnable onClick, T parentObject){
|
||||
public ListItem(String title, String subtitle, Consumer<ListItem<T>> onClick, T parentObject){
|
||||
this(title, subtitle, 0, onClick, parentObject, 0, false);
|
||||
}
|
||||
|
||||
public ListItem(String title, String subtitle, @DrawableRes int iconRes, Runnable onClick){
|
||||
public ListItem(String title, String subtitle, @DrawableRes int iconRes, Consumer<ListItem<T>> onClick){
|
||||
this(title, subtitle, iconRes, onClick, null, 0, false);
|
||||
}
|
||||
|
||||
public ListItem(String title, String subtitle, @DrawableRes int iconRes, Runnable onClick, T parentObject){
|
||||
public ListItem(String title, String subtitle, @DrawableRes int iconRes, Consumer<ListItem<T>> onClick, T parentObject){
|
||||
this(title, subtitle, iconRes, onClick, parentObject, 0, false);
|
||||
}
|
||||
|
||||
public ListItem(@StringRes int titleRes, @StringRes int subtitleRes, Runnable onClick){
|
||||
public ListItem(@StringRes int titleRes, @StringRes int subtitleRes, Consumer<ListItem<T>> onClick){
|
||||
this(null, null, 0, onClick, null, 0, false);
|
||||
this.titleRes=titleRes;
|
||||
this.subtitleRes=subtitleRes;
|
||||
}
|
||||
|
||||
public ListItem(@StringRes int titleRes, @StringRes int subtitleRes, Runnable onClick, int colorOverrideAttr, boolean dividerAfter){
|
||||
public ListItem(@StringRes int titleRes, @StringRes int subtitleRes, Consumer<ListItem<T>> onClick, int colorOverrideAttr, boolean dividerAfter){
|
||||
this(null, null, 0, onClick, null, colorOverrideAttr, dividerAfter);
|
||||
this.titleRes=titleRes;
|
||||
this.subtitleRes=subtitleRes;
|
||||
}
|
||||
|
||||
public ListItem(@StringRes int titleRes, @StringRes int subtitleRes, @DrawableRes int iconRes, Runnable onClick){
|
||||
public ListItem(@StringRes int titleRes, @StringRes int subtitleRes, @DrawableRes int iconRes, Consumer<ListItem<T>> onClick){
|
||||
this(null, null, iconRes, onClick, null, 0, false);
|
||||
this.titleRes=titleRes;
|
||||
this.subtitleRes=subtitleRes;
|
||||
}
|
||||
|
||||
public ListItem(@StringRes int titleRes, @StringRes int subtitleRes, @DrawableRes int iconRes, Runnable onClick, int colorOverrideAttr, boolean dividerAfter){
|
||||
public ListItem(@StringRes int titleRes, @StringRes int subtitleRes, @DrawableRes int iconRes, Consumer<ListItem<T>> onClick, int colorOverrideAttr, boolean dividerAfter){
|
||||
this(null, null, iconRes, onClick, null, colorOverrideAttr, dividerAfter);
|
||||
this.titleRes=titleRes;
|
||||
this.subtitleRes=subtitleRes;
|
||||
@ -75,4 +77,13 @@ public class ListItem<T>{
|
||||
public int getItemViewType(){
|
||||
return colorOverrideAttr==0 ? R.id.list_item_simple : R.id.list_item_simple_tinted;
|
||||
}
|
||||
|
||||
public void performClick(){
|
||||
if(onClick!=null)
|
||||
onClick.accept(this);
|
||||
}
|
||||
|
||||
public <I extends ListItem<T>> void setOnClick(Consumer<I> onClick){
|
||||
this.onClick=(Consumer<ListItem<T>>) onClick;
|
||||
}
|
||||
}
|
||||
|
@ -0,0 +1,35 @@
|
||||
package org.joinmastodon.android.model.viewmodel;
|
||||
|
||||
import android.view.Menu;
|
||||
import android.view.MenuItem;
|
||||
|
||||
import org.joinmastodon.android.R;
|
||||
|
||||
import java.util.function.Consumer;
|
||||
|
||||
public class ListItemWithOptionsMenu<T> extends ListItem<T>{
|
||||
public OptionsMenuListener<T> listener;
|
||||
|
||||
public ListItemWithOptionsMenu(String title, String subtitle, OptionsMenuListener<T> listener, int iconRes, Consumer<ListItemWithOptionsMenu<T>> onClick, T parentObject, boolean dividerAfter){
|
||||
super(title, subtitle, iconRes, (Consumer<ListItem<T>>)(Object)onClick, parentObject, 0, dividerAfter);
|
||||
this.listener=listener;
|
||||
}
|
||||
|
||||
@Override
|
||||
public int getItemViewType(){
|
||||
return R.id.list_item_options;
|
||||
}
|
||||
|
||||
public void performConfigureMenu(Menu menu){
|
||||
listener.onConfigureListItemOptionsMenu(this, menu);
|
||||
}
|
||||
|
||||
public void performItemSelected(MenuItem item){
|
||||
listener.onListItemOptionSelected(this, item);
|
||||
}
|
||||
|
||||
public interface OptionsMenuListener<T>{
|
||||
void onConfigureListItemOptionsMenu(ListItemWithOptionsMenu<T> item, Menu menu);
|
||||
void onListItemOptionSelected(ListItemWithOptionsMenu<T> item, MenuItem menuItem);
|
||||
}
|
||||
}
|
@ -47,7 +47,7 @@ public class SearchViewHelper{
|
||||
searchEdit.setBackground(null);
|
||||
searchEdit.addTextChangedListener(new SimpleTextWatcher(e->{
|
||||
searchEdit.removeCallbacks(debouncer);
|
||||
searchEdit.postDelayed(debouncer, 300);
|
||||
searchEdit.postDelayed(debouncer, 500);
|
||||
boolean newIsEmpty=e.length()==0;
|
||||
if(isEmpty!=newIsEmpty){
|
||||
isEmpty=newIsEmpty;
|
||||
|
@ -3,9 +3,12 @@ package org.joinmastodon.android.ui.adapters;
|
||||
import android.view.ViewGroup;
|
||||
|
||||
import org.joinmastodon.android.R;
|
||||
import org.joinmastodon.android.model.viewmodel.AvatarPileListItem;
|
||||
import org.joinmastodon.android.model.viewmodel.ListItem;
|
||||
import org.joinmastodon.android.ui.viewholders.AvatarPileListItemViewHolder;
|
||||
import org.joinmastodon.android.ui.viewholders.CheckboxOrRadioListItemViewHolder;
|
||||
import org.joinmastodon.android.ui.viewholders.ListItemViewHolder;
|
||||
import org.joinmastodon.android.ui.viewholders.OptionsListItemViewHolder;
|
||||
import org.joinmastodon.android.ui.viewholders.SimpleListItemViewHolder;
|
||||
import org.joinmastodon.android.ui.viewholders.SwitchListItemViewHolder;
|
||||
|
||||
@ -13,11 +16,21 @@ import java.util.List;
|
||||
|
||||
import androidx.annotation.NonNull;
|
||||
import androidx.recyclerview.widget.RecyclerView;
|
||||
import me.grishka.appkit.imageloader.ImageLoaderRecyclerAdapter;
|
||||
import me.grishka.appkit.imageloader.ListImageLoaderWrapper;
|
||||
import me.grishka.appkit.imageloader.requests.ImageLoaderRequest;
|
||||
import me.grishka.appkit.views.UsableRecyclerView;
|
||||
|
||||
public class GenericListItemsAdapter<T> extends RecyclerView.Adapter<ListItemViewHolder<?>>{
|
||||
public class GenericListItemsAdapter<T> extends UsableRecyclerView.Adapter<ListItemViewHolder<?>> implements ImageLoaderRecyclerAdapter{
|
||||
private List<ListItem<T>> items;
|
||||
|
||||
public GenericListItemsAdapter(List<ListItem<T>> items){
|
||||
super(null);
|
||||
this.items=items;
|
||||
}
|
||||
|
||||
public GenericListItemsAdapter(ListImageLoaderWrapper imgLoader, List<ListItem<T>> items){
|
||||
super(imgLoader);
|
||||
this.items=items;
|
||||
}
|
||||
|
||||
@ -32,6 +45,10 @@ public class GenericListItemsAdapter<T> extends RecyclerView.Adapter<ListItemVie
|
||||
return new CheckboxOrRadioListItemViewHolder(parent.getContext(), parent, false);
|
||||
if(viewType==R.id.list_item_radio)
|
||||
return new CheckboxOrRadioListItemViewHolder(parent.getContext(), parent, true);
|
||||
if(viewType==R.id.list_item_options)
|
||||
return new OptionsListItemViewHolder(parent.getContext(), parent);
|
||||
if(viewType==R.id.list_item_avatar_pile)
|
||||
return new AvatarPileListItemViewHolder(parent.getContext(), parent);
|
||||
|
||||
throw new IllegalArgumentException("Unexpected view type "+viewType);
|
||||
}
|
||||
@ -51,4 +68,20 @@ public class GenericListItemsAdapter<T> extends RecyclerView.Adapter<ListItemVie
|
||||
public int getItemViewType(int position){
|
||||
return items.get(position).getItemViewType();
|
||||
}
|
||||
|
||||
@Override
|
||||
public int getImageCountForItem(int position){
|
||||
ListItem<?> item=items.get(position);
|
||||
if(item instanceof AvatarPileListItem<?> avatarPileListItem)
|
||||
return avatarPileListItem.avatars.size();
|
||||
return 0;
|
||||
}
|
||||
|
||||
@Override
|
||||
public ImageLoaderRequest getImageRequest(int position, int image){
|
||||
ListItem<?> item=items.get(position);
|
||||
if(item instanceof AvatarPileListItem<?> avatarPileListItem)
|
||||
return avatarPileListItem.avatars.get(image);
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
@ -24,6 +24,7 @@ import org.joinmastodon.android.R;
|
||||
import org.joinmastodon.android.api.requests.accounts.GetAccountRelationships;
|
||||
import org.joinmastodon.android.api.requests.statuses.GetStatusSourceText;
|
||||
import org.joinmastodon.android.api.session.AccountSessionManager;
|
||||
import org.joinmastodon.android.fragments.AddAccountToListsFragment;
|
||||
import org.joinmastodon.android.fragments.BaseStatusListFragment;
|
||||
import org.joinmastodon.android.fragments.ComposeFragment;
|
||||
import org.joinmastodon.android.fragments.ProfileFragment;
|
||||
@ -198,6 +199,11 @@ public class HeaderStatusDisplayItem extends StatusDisplayItem{
|
||||
UiUtils.openSystemShareSheet(activity, item.status.url);
|
||||
}else if(id==R.id.translate){
|
||||
item.parentFragment.togglePostTranslation(item.status, item.parentID);
|
||||
}else if(id==R.id.add_to_list){
|
||||
Bundle args=new Bundle();
|
||||
args.putString("account", item.parentFragment.getAccountID());
|
||||
args.putParcelable("targetAccount", Parcels.wrap(account));
|
||||
Nav.go(activity, AddAccountToListsFragment.class, args);
|
||||
}
|
||||
return true;
|
||||
});
|
||||
@ -326,6 +332,7 @@ public class HeaderStatusDisplayItem extends StatusDisplayItem{
|
||||
report.setTitle(item.parentFragment.getString(R.string.report_user, account.displayName));
|
||||
follow.setTitle(item.parentFragment.getString(relationship!=null && relationship.following ? R.string.unfollow_user : R.string.follow_user, account.displayName));
|
||||
}
|
||||
menu.findItem(R.id.add_to_list).setVisible(relationship!=null && relationship.following);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -0,0 +1,89 @@
|
||||
package org.joinmastodon.android.ui.utils;
|
||||
|
||||
import android.animation.Animator;
|
||||
import android.animation.AnimatorListenerAdapter;
|
||||
import android.animation.IntEvaluator;
|
||||
import android.animation.ObjectAnimator;
|
||||
import android.graphics.drawable.Drawable;
|
||||
import android.view.ActionMode;
|
||||
import android.view.Menu;
|
||||
import android.view.MenuItem;
|
||||
import android.view.View;
|
||||
import android.view.ViewGroup;
|
||||
|
||||
import org.joinmastodon.android.R;
|
||||
|
||||
import java.util.function.IntSupplier;
|
||||
|
||||
import me.grishka.appkit.FragmentStackActivity;
|
||||
import me.grishka.appkit.fragments.AppKitFragment;
|
||||
|
||||
public class ActionModeHelper{
|
||||
public static ActionMode startActionMode(AppKitFragment fragment, IntSupplier statusBarColorSupplier, ActionMode.Callback callback){
|
||||
FragmentStackActivity activity=(FragmentStackActivity) fragment.getActivity();
|
||||
return activity.startActionMode(new ActionMode.Callback(){
|
||||
@Override
|
||||
public boolean onCreateActionMode(ActionMode mode, Menu menu){
|
||||
if(!callback.onCreateActionMode(mode, menu))
|
||||
return false;
|
||||
ObjectAnimator anim=ObjectAnimator.ofInt(activity.getWindow(), "statusBarColor", statusBarColorSupplier.getAsInt(), UiUtils.getThemeColor(activity, 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();
|
||||
activity.invalidateSystemBarColors(fragment);
|
||||
View fakeView=new View(activity);
|
||||
// mode.setCustomView(fakeView);
|
||||
// int buttonID=activity.getResources().getIdentifier("action_mode_close_button", "id", "android");
|
||||
// View btn=activity.getWindow().getDecorView().findViewById(buttonID);
|
||||
// if(btn!=null){
|
||||
// ((ViewGroup.MarginLayoutParams)btn.getLayoutParams()).setMarginEnd(0);
|
||||
// }
|
||||
return true;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean onPrepareActionMode(ActionMode mode, Menu menu){
|
||||
if(!callback.onPrepareActionMode(mode, menu))
|
||||
return false;
|
||||
for(int i=0;i<menu.size();i++){
|
||||
Drawable icon=menu.getItem(i).getIcon();
|
||||
if(icon!=null){
|
||||
icon=icon.mutate();
|
||||
icon.setTint(UiUtils.getThemeColor(activity, R.attr.colorM3OnPrimary));
|
||||
menu.getItem(i).setIcon(icon);
|
||||
}
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean onActionItemClicked(ActionMode mode, MenuItem item){
|
||||
return callback.onActionItemClicked(mode, item);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onDestroyActionMode(ActionMode mode){
|
||||
ObjectAnimator anim=ObjectAnimator.ofInt(activity.getWindow(), "statusBarColor", UiUtils.getThemeColor(activity, R.attr.colorM3Primary), statusBarColorSupplier.getAsInt());
|
||||
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){
|
||||
activity.getWindow().setStatusBarColor(0);
|
||||
}
|
||||
});
|
||||
anim.start();
|
||||
activity.invalidateSystemBarColors(fragment);
|
||||
callback.onDestroyActionMode(mode);
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
@ -0,0 +1,177 @@
|
||||
package org.joinmastodon.android.ui.viewcontrollers;
|
||||
|
||||
import android.graphics.Canvas;
|
||||
import android.graphics.Paint;
|
||||
import android.graphics.Rect;
|
||||
import android.text.TextUtils;
|
||||
import android.view.View;
|
||||
import android.view.ViewGroup;
|
||||
import android.view.accessibility.AccessibilityNodeInfo;
|
||||
import android.widget.LinearLayout;
|
||||
import android.widget.TextView;
|
||||
|
||||
import org.joinmastodon.android.R;
|
||||
import org.joinmastodon.android.ui.BetterItemAnimator;
|
||||
import org.joinmastodon.android.ui.utils.UiUtils;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
import java.util.function.Consumer;
|
||||
|
||||
import androidx.annotation.NonNull;
|
||||
import androidx.annotation.StringRes;
|
||||
import androidx.recyclerview.widget.LinearLayoutManager;
|
||||
import androidx.recyclerview.widget.RecyclerView;
|
||||
import me.grishka.appkit.utils.BindableViewHolder;
|
||||
import me.grishka.appkit.utils.MergeRecyclerAdapter;
|
||||
import me.grishka.appkit.utils.V;
|
||||
import me.grishka.appkit.views.UsableRecyclerView;
|
||||
|
||||
public abstract class DropdownSubmenuController{
|
||||
protected List<Item<?>> items;
|
||||
protected LinearLayout contentView;
|
||||
protected UsableRecyclerView list;
|
||||
protected TextView backItem;
|
||||
protected final ToolbarDropdownMenuController dropdownController;
|
||||
protected MergeRecyclerAdapter mergeAdapter;
|
||||
protected ItemsAdapter itemsAdapter;
|
||||
|
||||
public DropdownSubmenuController(ToolbarDropdownMenuController dropdownController){
|
||||
this.dropdownController=dropdownController;
|
||||
}
|
||||
|
||||
protected abstract CharSequence getBackItemTitle();
|
||||
public void onDismiss(){}
|
||||
|
||||
protected void createView(){
|
||||
contentView=new LinearLayout(dropdownController.getActivity());
|
||||
contentView.setOrientation(LinearLayout.VERTICAL);
|
||||
CharSequence backTitle=getBackItemTitle();
|
||||
if(!TextUtils.isEmpty(backTitle)){
|
||||
backItem=(TextView) dropdownController.getActivity().getLayoutInflater().inflate(R.layout.item_dropdown_menu, contentView, false);
|
||||
((LinearLayout.LayoutParams) backItem.getLayoutParams()).topMargin=V.dp(8);
|
||||
backItem.setText(backTitle);
|
||||
backItem.setCompoundDrawablesRelativeWithIntrinsicBounds(R.drawable.ic_arrow_back, 0, 0, 0);
|
||||
backItem.setBackground(UiUtils.getThemeDrawable(dropdownController.getActivity(), android.R.attr.selectableItemBackground));
|
||||
backItem.setOnClickListener(v->dropdownController.popSubmenuController());
|
||||
backItem.setAccessibilityDelegate(new View.AccessibilityDelegate(){
|
||||
@Override
|
||||
public void onInitializeAccessibilityNodeInfo(@NonNull View host, @NonNull AccessibilityNodeInfo info){
|
||||
super.onInitializeAccessibilityNodeInfo(host, info);
|
||||
info.setText(info.getText()+". "+host.getResources().getString(R.string.back));
|
||||
}
|
||||
});
|
||||
contentView.addView(backItem);
|
||||
}
|
||||
list=new UsableRecyclerView(dropdownController.getActivity());
|
||||
list.setLayoutManager(new LinearLayoutManager(dropdownController.getActivity()));
|
||||
itemsAdapter=new ItemsAdapter();
|
||||
mergeAdapter=new MergeRecyclerAdapter();
|
||||
mergeAdapter.addAdapter(itemsAdapter);
|
||||
list.setAdapter(mergeAdapter);
|
||||
list.setPadding(0, backItem!=null ? 0 : V.dp(8), 0, V.dp(8));
|
||||
list.setClipToPadding(false);
|
||||
list.setItemAnimator(new BetterItemAnimator());
|
||||
list.addItemDecoration(new RecyclerView.ItemDecoration(){
|
||||
private final Paint paint=new Paint();
|
||||
{
|
||||
paint.setStyle(Paint.Style.STROKE);
|
||||
paint.setStrokeWidth(V.dp(1));
|
||||
paint.setColor(UiUtils.getThemeColor(dropdownController.getActivity(), R.attr.colorM3OutlineVariant));
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onDraw(@NonNull Canvas c, @NonNull RecyclerView parent, @NonNull RecyclerView.State state){
|
||||
for(int i=0;i<parent.getChildCount();i++){
|
||||
View view=parent.getChildAt(i);
|
||||
if(parent.getChildViewHolder(view) instanceof ItemHolder ih && ih.getItem().dividerBefore){
|
||||
paint.setAlpha(Math.round(view.getAlpha()*255));
|
||||
float y=view.getTop()-V.dp(8)-paint.getStrokeWidth()/2f;
|
||||
c.drawLine(0, y, parent.getWidth(), y, paint);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void getItemOffsets(@NonNull Rect outRect, @NonNull View view, @NonNull RecyclerView parent, @NonNull RecyclerView.State state){
|
||||
if(parent.getChildViewHolder(view) instanceof ItemHolder ih && ih.getItem().dividerBefore){
|
||||
outRect.top=V.dp(17);
|
||||
}
|
||||
}
|
||||
});
|
||||
contentView.addView(list, new LinearLayout.LayoutParams(ViewGroup.LayoutParams.MATCH_PARENT, ViewGroup.LayoutParams.WRAP_CONTENT));
|
||||
}
|
||||
|
||||
public View getView(){
|
||||
if(contentView==null)
|
||||
createView();
|
||||
return contentView;
|
||||
}
|
||||
|
||||
protected final class Item<T>{
|
||||
public final String title;
|
||||
public final boolean hasSubmenu;
|
||||
public final boolean dividerBefore;
|
||||
public final T parentObject;
|
||||
public final Consumer<Item<T>> onClick;
|
||||
|
||||
public Item(String title, boolean hasSubmenu, boolean dividerBefore, T parentObject, Consumer<Item<T>> onClick){
|
||||
this.title=title;
|
||||
this.hasSubmenu=hasSubmenu;
|
||||
this.dividerBefore=dividerBefore;
|
||||
this.parentObject=parentObject;
|
||||
this.onClick=onClick;
|
||||
}
|
||||
|
||||
public Item(String title, boolean hasSubmenu, boolean dividerBefore, Consumer<Item<T>> onClick){
|
||||
this(title, hasSubmenu, dividerBefore, null, onClick);
|
||||
}
|
||||
|
||||
public Item(@StringRes int titleRes, boolean hasSubmenu, boolean dividerBefore, Consumer<Item<T>> onClick){
|
||||
this(dropdownController.getActivity().getString(titleRes), hasSubmenu, dividerBefore, null, onClick);
|
||||
}
|
||||
|
||||
private void performClick(){
|
||||
onClick.accept(this);
|
||||
}
|
||||
}
|
||||
|
||||
protected class ItemsAdapter extends RecyclerView.Adapter<ItemHolder>{
|
||||
|
||||
@NonNull
|
||||
@Override
|
||||
public ItemHolder onCreateViewHolder(@NonNull ViewGroup parent, int viewType){
|
||||
return new ItemHolder();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onBindViewHolder(@NonNull ItemHolder holder, int position){
|
||||
holder.bind(items.get(position));
|
||||
}
|
||||
|
||||
@Override
|
||||
public int getItemCount(){
|
||||
return items.size();
|
||||
}
|
||||
}
|
||||
|
||||
private class ItemHolder extends BindableViewHolder<Item<?>> implements UsableRecyclerView.Clickable{
|
||||
private final TextView text;
|
||||
|
||||
public ItemHolder(){
|
||||
super(dropdownController.getActivity(), R.layout.item_dropdown_menu, list);
|
||||
text=(TextView) itemView;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onBind(Item<?> item){
|
||||
text.setText(item.title);
|
||||
text.setCompoundDrawablesRelativeWithIntrinsicBounds(0, 0, item.hasSubmenu ? R.drawable.ic_arrow_right_24px : 0, 0);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onClick(){
|
||||
item.performClick();
|
||||
}
|
||||
}
|
||||
}
|
@ -0,0 +1,106 @@
|
||||
package org.joinmastodon.android.ui.viewcontrollers;
|
||||
|
||||
import android.app.Activity;
|
||||
import android.os.Bundle;
|
||||
import android.view.Gravity;
|
||||
import android.view.ViewGroup;
|
||||
import android.widget.FrameLayout;
|
||||
import android.widget.ProgressBar;
|
||||
|
||||
import org.joinmastodon.android.R;
|
||||
import org.joinmastodon.android.api.requests.tags.GetFollowedTags;
|
||||
import org.joinmastodon.android.fragments.ManageFollowedHashtagsFragment;
|
||||
import org.joinmastodon.android.model.Hashtag;
|
||||
import org.joinmastodon.android.model.HeaderPaginationList;
|
||||
import org.joinmastodon.android.ui.utils.HideableSingleViewRecyclerAdapter;
|
||||
import org.joinmastodon.android.ui.utils.UiUtils;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.Comparator;
|
||||
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.utils.V;
|
||||
|
||||
public class HomeTimelineHashtagsMenuController extends DropdownSubmenuController{
|
||||
private HideableSingleViewRecyclerAdapter largeProgressAdapter;
|
||||
private APIRequest<?> currentRequest;
|
||||
|
||||
public HomeTimelineHashtagsMenuController(ToolbarDropdownMenuController dropdownController){
|
||||
super(dropdownController);
|
||||
items=new ArrayList<>();
|
||||
loadHashtags();
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void createView(){
|
||||
super.createView();
|
||||
FrameLayout largeProgressView=new FrameLayout(dropdownController.getActivity());
|
||||
int pad=V.dp(32);
|
||||
largeProgressView.setPadding(0, pad, 0, pad);
|
||||
largeProgressView.setLayoutParams(new RecyclerView.LayoutParams(ViewGroup.LayoutParams.MATCH_PARENT, ViewGroup.LayoutParams.WRAP_CONTENT));
|
||||
ProgressBar progress=new ProgressBar(dropdownController.getActivity());
|
||||
largeProgressView.addView(progress, new FrameLayout.LayoutParams(V.dp(48), V.dp(48), Gravity.CENTER));
|
||||
largeProgressAdapter=new HideableSingleViewRecyclerAdapter(largeProgressView);
|
||||
mergeAdapter.addAdapter(0, largeProgressAdapter);
|
||||
}
|
||||
|
||||
@Override
|
||||
protected CharSequence getBackItemTitle(){
|
||||
return dropdownController.getActivity().getString(R.string.followed_hashtags);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onDismiss(){
|
||||
if(currentRequest!=null){
|
||||
currentRequest.cancel();
|
||||
currentRequest=null;
|
||||
}
|
||||
}
|
||||
|
||||
private void onTagClick(Item<Hashtag> item){
|
||||
dropdownController.dismiss();
|
||||
UiUtils.openHashtagTimeline(dropdownController.getActivity(), dropdownController.getAccountID(), item.parentObject);
|
||||
}
|
||||
|
||||
private void onManageTagsClick(){
|
||||
dropdownController.dismiss();
|
||||
Bundle args=new Bundle();
|
||||
args.putString("account", dropdownController.getAccountID());
|
||||
Nav.go(dropdownController.getActivity(), ManageFollowedHashtagsFragment.class, args);
|
||||
}
|
||||
|
||||
private void loadHashtags(){
|
||||
currentRequest=new GetFollowedTags(null, 200)
|
||||
.setCallback(new Callback<>(){
|
||||
@Override
|
||||
public void onSuccess(HeaderPaginationList<Hashtag> result){
|
||||
currentRequest=null;
|
||||
dropdownController.resizeOnNextFrame();
|
||||
largeProgressAdapter.setVisible(false);
|
||||
((List<Hashtag>) result).sort(Comparator.comparing(tag->tag.name));
|
||||
int prevSize=items.size();
|
||||
for(Hashtag tag:result){
|
||||
items.add(new Item<>("#"+tag.name, false, false, tag, HomeTimelineHashtagsMenuController.this::onTagClick));
|
||||
}
|
||||
items.add(new Item<Void>(R.string.manage_hashtags, false, true, i->onManageTagsClick()));
|
||||
itemsAdapter.notifyItemRangeInserted(prevSize, result.size()+1);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onError(ErrorResponse error){
|
||||
currentRequest=null;
|
||||
Activity activity=dropdownController.getActivity();
|
||||
if(activity!=null)
|
||||
error.showToast(activity);
|
||||
dropdownController.popSubmenuController();
|
||||
|
||||
}
|
||||
})
|
||||
.exec(dropdownController.getAccountID());
|
||||
}
|
||||
}
|
@ -0,0 +1,43 @@
|
||||
package org.joinmastodon.android.ui.viewcontrollers;
|
||||
|
||||
import android.os.Bundle;
|
||||
|
||||
import org.joinmastodon.android.R;
|
||||
import org.joinmastodon.android.fragments.ManageListsFragment;
|
||||
import org.joinmastodon.android.model.FollowList;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
|
||||
import me.grishka.appkit.Nav;
|
||||
|
||||
public class HomeTimelineListsMenuController extends DropdownSubmenuController{
|
||||
private final List<FollowList> lists;
|
||||
private final HomeTimelineMenuController.Callback callback;
|
||||
|
||||
public HomeTimelineListsMenuController(ToolbarDropdownMenuController dropdownController, HomeTimelineMenuController.Callback callback){
|
||||
super(dropdownController);
|
||||
this.lists=new ArrayList<>(callback.getLists());
|
||||
this.callback=callback;
|
||||
items=new ArrayList<>();
|
||||
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->{
|
||||
dropdownController.dismiss();
|
||||
Bundle args=new Bundle();
|
||||
args.putString("account", dropdownController.getAccountID());
|
||||
Nav.go(dropdownController.getActivity(), ManageListsFragment.class, args);
|
||||
}));
|
||||
}
|
||||
|
||||
@Override
|
||||
protected CharSequence getBackItemTitle(){
|
||||
return dropdownController.getActivity().getString(R.string.lists);
|
||||
}
|
||||
|
||||
private void onListSelected(Item<FollowList> item){
|
||||
callback.onListSelected(item.parentObject);
|
||||
dropdownController.dismiss();
|
||||
}
|
||||
}
|
@ -0,0 +1,39 @@
|
||||
package org.joinmastodon.android.ui.viewcontrollers;
|
||||
|
||||
import org.joinmastodon.android.R;
|
||||
import org.joinmastodon.android.model.FollowList;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
public class HomeTimelineMenuController extends DropdownSubmenuController{
|
||||
private Callback callback;
|
||||
|
||||
public HomeTimelineMenuController(ToolbarDropdownMenuController dropdownController, Callback callback){
|
||||
super(dropdownController);
|
||||
this.callback=callback;
|
||||
items=List.of(
|
||||
new Item<Void>(R.string.timeline_following, false, false, i->{
|
||||
callback.onFollowingSelected();
|
||||
dropdownController.dismiss();
|
||||
}),
|
||||
new Item<Void>(R.string.local_timeline, false, false, i->{
|
||||
callback.onLocalSelected();
|
||||
dropdownController.dismiss();
|
||||
}),
|
||||
new Item<Void>(R.string.lists, true, true, i->dropdownController.pushSubmenuController(new HomeTimelineListsMenuController(dropdownController, callback))),
|
||||
new Item<Void>(R.string.followed_hashtags, true, false, i->dropdownController.pushSubmenuController(new HomeTimelineHashtagsMenuController(dropdownController)))
|
||||
);
|
||||
}
|
||||
|
||||
@Override
|
||||
protected CharSequence getBackItemTitle(){
|
||||
return null;
|
||||
}
|
||||
|
||||
public interface Callback{
|
||||
void onFollowingSelected();
|
||||
void onLocalSelected();
|
||||
List<FollowList> getLists();
|
||||
void onListSelected(FollowList list);
|
||||
}
|
||||
}
|
@ -0,0 +1,268 @@
|
||||
package org.joinmastodon.android.ui.viewcontrollers;
|
||||
|
||||
import android.animation.Animator;
|
||||
import android.animation.AnimatorListenerAdapter;
|
||||
import android.animation.AnimatorSet;
|
||||
import android.animation.ObjectAnimator;
|
||||
import android.animation.ValueAnimator;
|
||||
import android.app.Activity;
|
||||
import android.content.Context;
|
||||
import android.content.res.Resources;
|
||||
import android.graphics.PixelFormat;
|
||||
import android.graphics.Rect;
|
||||
import android.view.Gravity;
|
||||
import android.view.KeyEvent;
|
||||
import android.view.MotionEvent;
|
||||
import android.view.View;
|
||||
import android.view.ViewGroup;
|
||||
import android.view.ViewTreeObserver;
|
||||
import android.view.WindowManager;
|
||||
import android.widget.FrameLayout;
|
||||
import android.widget.Toolbar;
|
||||
|
||||
import org.joinmastodon.android.R;
|
||||
import org.joinmastodon.android.ui.OutlineProviders;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
|
||||
import androidx.annotation.NonNull;
|
||||
import me.grishka.appkit.utils.CubicBezierInterpolator;
|
||||
import me.grishka.appkit.utils.V;
|
||||
|
||||
public class ToolbarDropdownMenuController{
|
||||
private final HostFragment fragment;
|
||||
private FrameLayout windowView;
|
||||
private FrameLayout menuContainer;
|
||||
private boolean dismissing;
|
||||
private List<DropdownSubmenuController> controllerStack=new ArrayList<>();
|
||||
private Animator currentTransition;
|
||||
|
||||
public ToolbarDropdownMenuController(HostFragment fragment){
|
||||
this.fragment=fragment;
|
||||
}
|
||||
|
||||
public void show(DropdownSubmenuController initialSubmenu){
|
||||
if(windowView!=null)
|
||||
return;
|
||||
|
||||
menuContainer=new FrameLayout(fragment.getActivity());
|
||||
menuContainer.setBackgroundResource(R.drawable.bg_m3_surface2);
|
||||
menuContainer.setOutlineProvider(OutlineProviders.roundedRect(4));
|
||||
menuContainer.setClipToOutline(true);
|
||||
menuContainer.setElevation(V.dp(6));
|
||||
View menuView=initialSubmenu.getView();
|
||||
menuView.setVisibility(View.VISIBLE);
|
||||
menuContainer.addView(menuView, new FrameLayout.LayoutParams(ViewGroup.LayoutParams.MATCH_PARENT, ViewGroup.LayoutParams.WRAP_CONTENT));
|
||||
|
||||
windowView=new WindowView(fragment.getActivity());
|
||||
int pad=V.dp(16);
|
||||
windowView.setPadding(pad, fragment.getToolbar().getHeight(), pad, pad);
|
||||
windowView.setClipToPadding(false);
|
||||
windowView.addView(menuContainer, new FrameLayout.LayoutParams(V.dp(200), ViewGroup.LayoutParams.WRAP_CONTENT, Gravity.TOP | Gravity.START));
|
||||
|
||||
WindowManager.LayoutParams wlp=new WindowManager.LayoutParams(WindowManager.LayoutParams.TYPE_APPLICATION_PANEL);
|
||||
wlp.format=PixelFormat.TRANSLUCENT;
|
||||
wlp.token=fragment.getActivity().getWindow().getDecorView().getWindowToken();
|
||||
wlp.width=wlp.height=ViewGroup.LayoutParams.MATCH_PARENT;
|
||||
wlp.flags=WindowManager.LayoutParams.FLAG_LAYOUT_ATTACHED_IN_DECOR;
|
||||
wlp.setTitle(fragment.getActivity().getString(R.string.dropdown_menu));
|
||||
fragment.getActivity().getWindowManager().addView(windowView, wlp);
|
||||
|
||||
menuContainer.setPivotX(V.dp(100));
|
||||
menuContainer.setPivotY(0);
|
||||
menuContainer.setScaleX(.8f);
|
||||
menuContainer.setScaleY(.8f);
|
||||
menuContainer.setAlpha(0f);
|
||||
menuContainer.animate()
|
||||
.scaleX(1f)
|
||||
.scaleY(1f)
|
||||
.alpha(1f)
|
||||
.setInterpolator(CubicBezierInterpolator.DEFAULT)
|
||||
.setDuration(150)
|
||||
.withLayer()
|
||||
.start();
|
||||
controllerStack.add(initialSubmenu);
|
||||
}
|
||||
|
||||
public void dismiss(){
|
||||
if(windowView==null || dismissing)
|
||||
return;
|
||||
dismissing=true;
|
||||
fragment.onDropdownWillDismiss();
|
||||
menuContainer.animate()
|
||||
.scaleX(.8f)
|
||||
.scaleY(.8f)
|
||||
.alpha(0f)
|
||||
.setInterpolator(CubicBezierInterpolator.DEFAULT)
|
||||
.setDuration(150)
|
||||
.withLayer()
|
||||
.withEndAction(()->{
|
||||
controllerStack.clear();
|
||||
fragment.getActivity().getWindowManager().removeView(windowView);
|
||||
menuContainer.removeAllViews();
|
||||
dismissing=false;
|
||||
windowView=null;
|
||||
menuContainer=null;
|
||||
fragment.onDropdownDismissed();
|
||||
})
|
||||
.start();
|
||||
}
|
||||
|
||||
public void pushSubmenuController(DropdownSubmenuController controller){
|
||||
View prevView=menuContainer.getChildAt(menuContainer.getChildCount()-1);
|
||||
View newView=controller.getView();
|
||||
newView.setVisibility(View.VISIBLE);
|
||||
menuContainer.addView(newView, new FrameLayout.LayoutParams(ViewGroup.LayoutParams.MATCH_PARENT, ViewGroup.LayoutParams.WRAP_CONTENT));
|
||||
controllerStack.add(controller);
|
||||
animateTransition(prevView, newView, true);
|
||||
}
|
||||
|
||||
public void popSubmenuController(){
|
||||
if(menuContainer.getChildCount()<=1)
|
||||
throw new IllegalStateException();
|
||||
DropdownSubmenuController controller=controllerStack.remove(controllerStack.size()-1);
|
||||
controller.onDismiss();
|
||||
View top=menuContainer.getChildAt(menuContainer.getChildCount()-1);
|
||||
View prev=menuContainer.getChildAt(menuContainer.getChildCount()-2);
|
||||
prev.setVisibility(View.VISIBLE);
|
||||
animateTransition(prev, top, false);
|
||||
}
|
||||
|
||||
private void animateTransition(View bottomView, View topView, boolean adding){
|
||||
if(currentTransition!=null)
|
||||
currentTransition.cancel();
|
||||
int origBottom=menuContainer.getBottom();
|
||||
menuContainer.getViewTreeObserver().addOnPreDrawListener(new ViewTreeObserver.OnPreDrawListener(){
|
||||
private final Rect tmpRect=new Rect();
|
||||
|
||||
@Override
|
||||
public boolean onPreDraw(){
|
||||
menuContainer.getViewTreeObserver().removeOnPreDrawListener(this);
|
||||
|
||||
AnimatorSet set=new AnimatorSet();
|
||||
ObjectAnimator slideIn;
|
||||
set.playTogether(
|
||||
ObjectAnimator.ofInt(menuContainer, "bottom", origBottom, menuContainer.getTop()+(adding ? topView : bottomView).getHeight()),
|
||||
slideIn=ObjectAnimator.ofFloat(topView, View.TRANSLATION_X, adding ? menuContainer.getWidth() : 0, adding ? 0 : menuContainer.getWidth()),
|
||||
ObjectAnimator.ofFloat(bottomView, View.TRANSLATION_X, adding ? 0 : -menuContainer.getWidth()/4f, adding ? -menuContainer.getWidth()/4f : 0),
|
||||
ObjectAnimator.ofFloat(bottomView, View.ALPHA, adding ? 1f : 0f, adding ? 0f : 1f)
|
||||
);
|
||||
set.setDuration(300);
|
||||
set.setInterpolator(CubicBezierInterpolator.DEFAULT);
|
||||
set.addListener(new AnimatorListenerAdapter(){
|
||||
@Override
|
||||
public void onAnimationEnd(Animator animation){
|
||||
bottomView.setClipBounds(null);
|
||||
bottomView.setTranslationX(0);
|
||||
bottomView.setAlpha(1f);
|
||||
topView.setTranslationX(0);
|
||||
topView.setAlpha(1f);
|
||||
if(adding){
|
||||
bottomView.setVisibility(View.GONE);
|
||||
}else{
|
||||
menuContainer.removeView(topView);
|
||||
}
|
||||
currentTransition=null;
|
||||
}
|
||||
});
|
||||
slideIn.addUpdateListener(animation->{
|
||||
tmpRect.set(0, 0, Math.round(topView.getX()-bottomView.getX()), bottomView.getHeight());
|
||||
bottomView.setClipBounds(tmpRect);
|
||||
});
|
||||
currentTransition=set;
|
||||
set.start();
|
||||
|
||||
return true;
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
public void resizeOnNextFrame(){
|
||||
if(currentTransition!=null)
|
||||
currentTransition.cancel();
|
||||
int origBottom=menuContainer.getBottom();
|
||||
menuContainer.getViewTreeObserver().addOnPreDrawListener(new ViewTreeObserver.OnPreDrawListener(){
|
||||
@Override
|
||||
public boolean onPreDraw(){
|
||||
menuContainer.getViewTreeObserver().removeOnPreDrawListener(this);
|
||||
|
||||
ObjectAnimator anim=ObjectAnimator.ofInt(menuContainer, "bottom", origBottom, menuContainer.getBottom());
|
||||
anim.setDuration(300);
|
||||
anim.setInterpolator(CubicBezierInterpolator.DEFAULT);
|
||||
anim.addListener(new AnimatorListenerAdapter(){
|
||||
@Override
|
||||
public void onAnimationEnd(Animator animation){
|
||||
currentTransition=null;
|
||||
}
|
||||
});
|
||||
currentTransition=anim;
|
||||
anim.start();
|
||||
|
||||
return true;
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
Activity getActivity(){
|
||||
return fragment.getActivity();
|
||||
}
|
||||
|
||||
String getAccountID(){
|
||||
return fragment.getAccountID();
|
||||
}
|
||||
|
||||
private class WindowView extends FrameLayout{
|
||||
private final Rect tmpRect=new Rect();
|
||||
public WindowView(@NonNull Context context){
|
||||
super(context);
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean onTouchEvent(MotionEvent ev){
|
||||
for(int i=0;i<getChildCount();i++){
|
||||
View child=getChildAt(i);
|
||||
child.getHitRect(tmpRect);
|
||||
if(tmpRect.contains(Math.round(ev.getX()), Math.round(ev.getY())))
|
||||
return super.onTouchEvent(ev);
|
||||
}
|
||||
if(ev.getAction()==MotionEvent.ACTION_DOWN){
|
||||
dismiss();
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean dispatchTouchEvent(MotionEvent ev){
|
||||
if(currentTransition!=null)
|
||||
return false;
|
||||
return super.dispatchTouchEvent(ev);
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean dispatchKeyEvent(KeyEvent event){
|
||||
if(event.getKeyCode()==KeyEvent.KEYCODE_BACK){
|
||||
if(event.getAction()==KeyEvent.ACTION_DOWN){
|
||||
if(controllerStack.size()>1)
|
||||
popSubmenuController();
|
||||
else
|
||||
dismiss();
|
||||
}
|
||||
return true;
|
||||
}
|
||||
return super.dispatchKeyEvent(event);
|
||||
}
|
||||
}
|
||||
|
||||
public interface HostFragment{
|
||||
// Fragment methods
|
||||
Activity getActivity();
|
||||
Resources getResources();
|
||||
Toolbar getToolbar();
|
||||
String getAccountID();
|
||||
|
||||
// Callbacks
|
||||
void onDropdownWillDismiss();
|
||||
void onDropdownDismissed();
|
||||
}
|
||||
}
|
@ -17,6 +17,7 @@ import android.view.View;
|
||||
import android.view.ViewGroup;
|
||||
import android.widget.Button;
|
||||
import android.widget.CheckBox;
|
||||
import android.widget.ImageButton;
|
||||
import android.widget.ImageView;
|
||||
import android.widget.PopupMenu;
|
||||
import android.widget.ProgressBar;
|
||||
@ -26,6 +27,7 @@ import android.widget.TextView;
|
||||
import org.joinmastodon.android.R;
|
||||
import org.joinmastodon.android.api.requests.accounts.SetAccountFollowed;
|
||||
import org.joinmastodon.android.api.session.AccountSessionManager;
|
||||
import org.joinmastodon.android.fragments.AddAccountToListsFragment;
|
||||
import org.joinmastodon.android.fragments.ProfileFragment;
|
||||
import org.joinmastodon.android.fragments.report.ReportReasonChoiceFragment;
|
||||
import org.joinmastodon.android.model.Account;
|
||||
@ -40,6 +42,7 @@ import org.parceler.Parcels;
|
||||
import java.util.HashMap;
|
||||
import java.util.Objects;
|
||||
import java.util.function.Consumer;
|
||||
import java.util.function.Predicate;
|
||||
|
||||
import me.grishka.appkit.Nav;
|
||||
import me.grishka.appkit.api.Callback;
|
||||
@ -58,12 +61,15 @@ public class AccountViewHolder extends BindableViewHolder<AccountViewModel> impl
|
||||
private final CheckableRelativeLayout view;
|
||||
private final View checkbox;
|
||||
private final ProgressBar actionProgress;
|
||||
private final ImageButton menuButton;
|
||||
|
||||
private final String accountID;
|
||||
private final Fragment fragment;
|
||||
private final HashMap<String, Relationship> relationships;
|
||||
|
||||
private Consumer<AccountViewHolder> onClick;
|
||||
private Predicate<AccountViewHolder> onLongClick;
|
||||
private Consumer<MenuItem> onCustomMenuItemSelected;
|
||||
private AccessoryType accessoryType;
|
||||
private boolean showBio;
|
||||
private boolean checked;
|
||||
@ -85,6 +91,7 @@ public class AccountViewHolder extends BindableViewHolder<AccountViewModel> impl
|
||||
bio=findViewById(R.id.bio);
|
||||
checkbox=findViewById(R.id.checkbox);
|
||||
actionProgress=findViewById(R.id.action_progress);
|
||||
menuButton=findViewById(R.id.options_btn);
|
||||
|
||||
avatar.setOutlineProvider(OutlineProviders.roundedRect(10));
|
||||
avatar.setClipToOutline(true);
|
||||
@ -94,6 +101,7 @@ public class AccountViewHolder extends BindableViewHolder<AccountViewModel> impl
|
||||
contextMenu=new PopupMenu(fragment.getActivity(), menuAnchor);
|
||||
contextMenu.inflate(R.menu.profile);
|
||||
contextMenu.setOnMenuItemClickListener(this::onContextMenuItemSelected);
|
||||
menuButton.setOnClickListener(v->showMenuFromButton());
|
||||
|
||||
setStyle(AccessoryType.BUTTON, false);
|
||||
}
|
||||
@ -181,37 +189,13 @@ public class AccountViewHolder extends BindableViewHolder<AccountViewModel> impl
|
||||
|
||||
@Override
|
||||
public boolean onLongClick(float x, float y){
|
||||
if(relationships==null)
|
||||
if(onLongClick!=null && onLongClick.test(this))
|
||||
return true;
|
||||
if(accessoryType==AccessoryType.MENU || !prepareMenu())
|
||||
return false;
|
||||
Relationship relationship=relationships.get(item.account.id);
|
||||
if(relationship==null)
|
||||
return false;
|
||||
Menu menu=contextMenu.getMenu();
|
||||
Account account=item.account;
|
||||
|
||||
menu.findItem(R.id.share).setTitle(fragment.getString(R.string.share_user, account.getDisplayUsername()));
|
||||
menu.findItem(R.id.mute).setTitle(fragment.getString(relationship.muting ? R.string.unmute_user : R.string.mute_user, account.getDisplayUsername()));
|
||||
menu.findItem(R.id.block).setTitle(fragment.getString(relationship.blocking ? R.string.unblock_user : R.string.block_user, account.getDisplayUsername()));
|
||||
menu.findItem(R.id.report).setTitle(fragment.getString(R.string.report_user, account.getDisplayUsername()));
|
||||
MenuItem hideBoosts=menu.findItem(R.id.hide_boosts);
|
||||
if(relationship.following){
|
||||
hideBoosts.setTitle(fragment.getString(relationship.showingReblogs ? R.string.hide_boosts_from_user : R.string.show_boosts_from_user, account.getDisplayUsername()));
|
||||
hideBoosts.setVisible(true);
|
||||
}else{
|
||||
hideBoosts.setVisible(false);
|
||||
}
|
||||
MenuItem blockDomain=menu.findItem(R.id.block_domain);
|
||||
if(!account.isLocal()){
|
||||
blockDomain.setTitle(fragment.getString(relationship.domainBlocking ? R.string.unblock_domain : R.string.block_domain, account.getDomain()));
|
||||
blockDomain.setVisible(true);
|
||||
}else{
|
||||
blockDomain.setVisible(false);
|
||||
}
|
||||
|
||||
menuAnchor.setTranslationX(x);
|
||||
menuAnchor.setTranslationY(y);
|
||||
contextMenu.show();
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
@ -279,6 +263,13 @@ public class AccountViewHolder extends BindableViewHolder<AccountViewModel> impl
|
||||
})
|
||||
.wrapProgress(fragment.getActivity(), R.string.loading, false)
|
||||
.exec(accountID);
|
||||
}else if(id==R.id.add_to_list){
|
||||
Bundle args=new Bundle();
|
||||
args.putString("account", accountID);
|
||||
args.putParcelable("targetAccount", Parcels.wrap(account));
|
||||
Nav.go(fragment.getActivity(), AddAccountToListsFragment.class, args);
|
||||
}else if(onCustomMenuItemSelected!=null){
|
||||
onCustomMenuItemSelected.accept(item);
|
||||
}
|
||||
return true;
|
||||
}
|
||||
@ -292,6 +283,14 @@ public class AccountViewHolder extends BindableViewHolder<AccountViewModel> impl
|
||||
onClick=listener;
|
||||
}
|
||||
|
||||
public void setOnLongClickListener(Predicate<AccountViewHolder> onLongClick){
|
||||
this.onLongClick=onLongClick;
|
||||
}
|
||||
|
||||
public void setOnCustomMenuItemSelectedListener(Consumer<MenuItem> onCustomMenuItemSelected){
|
||||
this.onCustomMenuItemSelected=onCustomMenuItemSelected;
|
||||
}
|
||||
|
||||
public void setStyle(AccessoryType accessoryType, boolean showBio){
|
||||
if(accessoryType!=this.accessoryType){
|
||||
this.accessoryType=accessoryType;
|
||||
@ -299,20 +298,29 @@ public class AccountViewHolder extends BindableViewHolder<AccountViewModel> impl
|
||||
case NONE -> {
|
||||
button.setVisibility(View.GONE);
|
||||
checkbox.setVisibility(View.GONE);
|
||||
menuButton.setVisibility(View.GONE);
|
||||
}
|
||||
case CHECKBOX -> {
|
||||
button.setVisibility(View.GONE);
|
||||
checkbox.setVisibility(View.VISIBLE);
|
||||
menuButton.setVisibility(View.GONE);
|
||||
checkbox.setBackground(new CheckBox(checkbox.getContext()).getButtonDrawable());
|
||||
}
|
||||
case RADIOBUTTON -> {
|
||||
button.setVisibility(View.GONE);
|
||||
checkbox.setVisibility(View.VISIBLE);
|
||||
menuButton.setVisibility(View.GONE);
|
||||
checkbox.setBackground(new RadioButton(checkbox.getContext()).getButtonDrawable());
|
||||
}
|
||||
case BUTTON -> {
|
||||
button.setVisibility(View.VISIBLE);
|
||||
checkbox.setVisibility(View.GONE);
|
||||
menuButton.setVisibility(View.GONE);
|
||||
}
|
||||
case MENU -> {
|
||||
button.setVisibility(View.GONE);
|
||||
checkbox.setVisibility(View.GONE);
|
||||
menuButton.setVisibility(View.VISIBLE);
|
||||
}
|
||||
}
|
||||
view.setCheckable(accessoryType==AccessoryType.CHECKBOX || accessoryType==AccessoryType.RADIOBUTTON);
|
||||
@ -321,15 +329,63 @@ public class AccountViewHolder extends BindableViewHolder<AccountViewModel> impl
|
||||
bio.setVisibility(showBio ? View.VISIBLE : View.GONE);
|
||||
}
|
||||
|
||||
private boolean prepareMenu(){
|
||||
if(relationships==null)
|
||||
return false;
|
||||
Relationship relationship=relationships.get(item.account.id);
|
||||
if(relationship==null)
|
||||
return false;
|
||||
Menu menu=contextMenu.getMenu();
|
||||
Account account=item.account;
|
||||
|
||||
menu.findItem(R.id.share).setTitle(fragment.getString(R.string.share_user, account.getDisplayUsername()));
|
||||
menu.findItem(R.id.mute).setTitle(fragment.getString(relationship.muting ? R.string.unmute_user : R.string.mute_user, account.getDisplayUsername()));
|
||||
menu.findItem(R.id.block).setTitle(fragment.getString(relationship.blocking ? R.string.unblock_user : R.string.block_user, account.getDisplayUsername()));
|
||||
menu.findItem(R.id.report).setTitle(fragment.getString(R.string.report_user, account.getDisplayUsername()));
|
||||
MenuItem hideBoosts=menu.findItem(R.id.hide_boosts);
|
||||
if(relationship.following){
|
||||
hideBoosts.setTitle(fragment.getString(relationship.showingReblogs ? R.string.hide_boosts_from_user : R.string.show_boosts_from_user, account.getDisplayUsername()));
|
||||
hideBoosts.setVisible(true);
|
||||
}else{
|
||||
hideBoosts.setVisible(false);
|
||||
}
|
||||
MenuItem blockDomain=menu.findItem(R.id.block_domain);
|
||||
if(!account.isLocal()){
|
||||
blockDomain.setTitle(fragment.getString(relationship.domainBlocking ? R.string.unblock_domain : R.string.block_domain, account.getDomain()));
|
||||
blockDomain.setVisible(true);
|
||||
}else{
|
||||
blockDomain.setVisible(false);
|
||||
}
|
||||
menu.findItem(R.id.add_to_list).setVisible(relationship.following);
|
||||
return true;
|
||||
}
|
||||
|
||||
private void showMenuFromButton(){
|
||||
if(!prepareMenu())
|
||||
return;
|
||||
int[] xy={0, 0};
|
||||
itemView.getLocationInWindow(xy);
|
||||
int x=xy[0], y=xy[1];
|
||||
menuButton.getLocationInWindow(xy);
|
||||
menuAnchor.setTranslationX(xy[0]-x+menuButton.getWidth()/2f);
|
||||
menuAnchor.setTranslationY(xy[1]-y+menuButton.getHeight());
|
||||
contextMenu.show();
|
||||
}
|
||||
|
||||
public void setChecked(boolean checked){
|
||||
this.checked=checked;
|
||||
view.setChecked(checked);
|
||||
}
|
||||
|
||||
public PopupMenu getContextMenu(){
|
||||
return contextMenu;
|
||||
}
|
||||
|
||||
public enum AccessoryType{
|
||||
NONE,
|
||||
BUTTON,
|
||||
CHECKBOX,
|
||||
RADIOBUTTON
|
||||
RADIOBUTTON,
|
||||
MENU
|
||||
}
|
||||
}
|
||||
|
@ -0,0 +1,42 @@
|
||||
package org.joinmastodon.android.ui.viewholders;
|
||||
|
||||
import android.content.Context;
|
||||
import android.graphics.drawable.Drawable;
|
||||
import android.view.ViewGroup;
|
||||
import android.widget.LinearLayout;
|
||||
|
||||
import org.joinmastodon.android.R;
|
||||
import org.joinmastodon.android.model.viewmodel.AvatarPileListItem;
|
||||
import org.joinmastodon.android.ui.views.AvatarPileView;
|
||||
|
||||
import me.grishka.appkit.imageloader.ImageLoaderViewHolder;
|
||||
import me.grishka.appkit.utils.V;
|
||||
|
||||
public class AvatarPileListItemViewHolder extends ListItemViewHolder<AvatarPileListItem<?>> implements ImageLoaderViewHolder{
|
||||
private final AvatarPileView pile;
|
||||
|
||||
public AvatarPileListItemViewHolder(Context context, ViewGroup parent){
|
||||
super(context, R.layout.item_generic_list, parent);
|
||||
pile=new AvatarPileView(context);
|
||||
LinearLayout.LayoutParams lp=new LinearLayout.LayoutParams(ViewGroup.LayoutParams.WRAP_CONTENT, ViewGroup.LayoutParams.MATCH_PARENT);
|
||||
lp.topMargin=lp.bottomMargin=V.dp(-8);
|
||||
view.addView(pile, lp);
|
||||
view.setClipToPadding(false);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onBind(AvatarPileListItem<?> item){
|
||||
super.onBind(item);
|
||||
pile.setVisibleAvatarCount(item.avatars.size());
|
||||
}
|
||||
|
||||
@Override
|
||||
public void setImage(int index, Drawable image){
|
||||
pile.avatars[index].setImageDrawable(image);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void clearImage(int index){
|
||||
pile.avatars[index].setImageResource(R.drawable.image_placeholder);
|
||||
}
|
||||
}
|
@ -75,6 +75,6 @@ public abstract class ListItemViewHolder<T extends ListItem<?>> extends Bindable
|
||||
|
||||
@Override
|
||||
public void onClick(){
|
||||
item.onClick.run();
|
||||
item.performClick();
|
||||
}
|
||||
}
|
||||
|
@ -0,0 +1,33 @@
|
||||
package org.joinmastodon.android.ui.viewholders;
|
||||
|
||||
import android.content.Context;
|
||||
import android.view.View;
|
||||
import android.view.ViewGroup;
|
||||
import android.widget.ImageButton;
|
||||
import android.widget.PopupMenu;
|
||||
|
||||
import org.joinmastodon.android.R;
|
||||
import org.joinmastodon.android.model.viewmodel.ListItemWithOptionsMenu;
|
||||
|
||||
public class OptionsListItemViewHolder extends ListItemViewHolder<ListItemWithOptionsMenu<?>>{
|
||||
private final PopupMenu menu;
|
||||
private final ImageButton menuBtn;
|
||||
|
||||
public OptionsListItemViewHolder(Context context, ViewGroup parent){
|
||||
super(context, R.layout.item_generic_list_options, parent);
|
||||
menuBtn=findViewById(R.id.options_btn);
|
||||
menu=new PopupMenu(context, menuBtn);
|
||||
menuBtn.setOnClickListener(this::onMenuBtnClick);
|
||||
|
||||
menu.setOnMenuItemClickListener(menuItem->{
|
||||
item.performItemSelected(menuItem);
|
||||
return true;
|
||||
});
|
||||
}
|
||||
|
||||
private void onMenuBtnClick(View v){
|
||||
menu.getMenu().clear();
|
||||
item.performConfigureMenu(menu.getMenu());
|
||||
menu.show();
|
||||
}
|
||||
}
|
@ -0,0 +1,81 @@
|
||||
package org.joinmastodon.android.ui.views;
|
||||
|
||||
import android.content.Context;
|
||||
import android.graphics.Canvas;
|
||||
import android.graphics.Paint;
|
||||
import android.graphics.PorterDuff;
|
||||
import android.graphics.PorterDuffXfermode;
|
||||
import android.graphics.RectF;
|
||||
import android.util.AttributeSet;
|
||||
import android.view.Gravity;
|
||||
import android.view.View;
|
||||
import android.widget.ImageView;
|
||||
import android.widget.LinearLayout;
|
||||
|
||||
import org.joinmastodon.android.R;
|
||||
import org.joinmastodon.android.ui.OutlineProviders;
|
||||
|
||||
import androidx.annotation.Nullable;
|
||||
import me.grishka.appkit.utils.CustomViewHelper;
|
||||
|
||||
public class AvatarPileView extends LinearLayout implements CustomViewHelper{
|
||||
public final ImageView[] avatars=new ImageView[3];
|
||||
private final Paint borderPaint=new Paint(Paint.ANTI_ALIAS_FLAG);
|
||||
private final RectF tmpRect=new RectF();
|
||||
|
||||
public AvatarPileView(Context context){
|
||||
super(context);
|
||||
init();
|
||||
}
|
||||
|
||||
public AvatarPileView(Context context, @Nullable AttributeSet attrs){
|
||||
super(context, attrs);
|
||||
init();
|
||||
}
|
||||
|
||||
public AvatarPileView(Context context, @Nullable AttributeSet attrs, int defStyleAttr){
|
||||
super(context, attrs, defStyleAttr);
|
||||
init();
|
||||
}
|
||||
|
||||
private void init(){
|
||||
setLayerType(LAYER_TYPE_HARDWARE, null);
|
||||
setPaddingRelative(dp(16), 0, 0, 0);
|
||||
setClipToPadding(false);
|
||||
for(int i=0;i<avatars.length;i++){
|
||||
ImageView ava=new ImageView(getContext());
|
||||
ava.setScaleType(ImageView.ScaleType.CENTER_CROP);
|
||||
ava.setOutlineProvider(OutlineProviders.roundedRect(6));
|
||||
ava.setClipToOutline(true);
|
||||
ava.setImageResource(R.drawable.image_placeholder);
|
||||
ava.setPivotX(dp(16));
|
||||
ava.setPivotY(dp(32));
|
||||
ava.setRotation((avatars.length-1-i)*(-2f));
|
||||
LayoutParams lp=new LayoutParams(dp(32), dp(32));
|
||||
lp.gravity=Gravity.CENTER_VERTICAL;
|
||||
if(i<avatars.length-1)
|
||||
lp.setMarginEnd(dp(-16));
|
||||
addView(ava, lp);
|
||||
avatars[avatars.length-1-i]=ava;
|
||||
}
|
||||
borderPaint.setXfermode(new PorterDuffXfermode(PorterDuff.Mode.CLEAR));
|
||||
}
|
||||
|
||||
public void setVisibleAvatarCount(int count){
|
||||
for(int i=0;i<avatars.length;i++){
|
||||
avatars[i].setVisibility(i<count ? VISIBLE : INVISIBLE);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
protected boolean drawChild(Canvas canvas, View child, long drawingTime){
|
||||
tmpRect.set(child.getLeft(), child.getTop(), child.getRight(), child.getBottom());
|
||||
tmpRect.offset(child.getTranslationX(), child.getTranslationY());
|
||||
tmpRect.inset(dp(-2), dp(-2));
|
||||
canvas.save();
|
||||
canvas.rotate(child.getRotation(), child.getLeft()+child.getPivotX(), child.getTop()+child.getPivotY());
|
||||
canvas.drawRoundRect(tmpRect, dp(8), dp(8), borderPaint);
|
||||
canvas.restore();
|
||||
return super.drawChild(canvas, child, drawingTime);
|
||||
}
|
||||
}
|
@ -6,6 +6,7 @@ import android.widget.ImageView;
|
||||
|
||||
public class FixedAspectRatioImageView extends ImageView{
|
||||
private float aspectRatio=1;
|
||||
private boolean useHeight;
|
||||
|
||||
public FixedAspectRatioImageView(Context context){
|
||||
this(context, null);
|
||||
@ -21,8 +22,13 @@ public class FixedAspectRatioImageView extends ImageView{
|
||||
|
||||
@Override
|
||||
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec){
|
||||
int width=MeasureSpec.getSize(widthMeasureSpec);
|
||||
heightMeasureSpec=Math.round(width/aspectRatio) | MeasureSpec.EXACTLY;
|
||||
if(useHeight){
|
||||
int height=MeasureSpec.getSize(heightMeasureSpec);
|
||||
widthMeasureSpec=Math.round(height*aspectRatio) | MeasureSpec.EXACTLY;
|
||||
}else{
|
||||
int width=MeasureSpec.getSize(widthMeasureSpec);
|
||||
heightMeasureSpec=Math.round(width/aspectRatio) | MeasureSpec.EXACTLY;
|
||||
}
|
||||
super.onMeasure(widthMeasureSpec, heightMeasureSpec);
|
||||
}
|
||||
|
||||
@ -33,4 +39,12 @@ public class FixedAspectRatioImageView extends ImageView{
|
||||
public void setAspectRatio(float aspectRatio){
|
||||
this.aspectRatio=aspectRatio;
|
||||
}
|
||||
|
||||
public boolean isUseHeight(){
|
||||
return useHeight;
|
||||
}
|
||||
|
||||
public void setUseHeight(boolean useHeight){
|
||||
this.useHeight=useHeight;
|
||||
}
|
||||
}
|
||||
|
@ -34,11 +34,13 @@ import org.joinmastodon.android.ui.utils.UiUtils;
|
||||
import androidx.annotation.Keep;
|
||||
import androidx.annotation.NonNull;
|
||||
import androidx.annotation.Nullable;
|
||||
import androidx.annotation.StringRes;
|
||||
import me.grishka.appkit.utils.CubicBezierInterpolator;
|
||||
import me.grishka.appkit.utils.CustomViewHelper;
|
||||
|
||||
public class FloatingHintEditTextLayout extends FrameLayout implements CustomViewHelper{
|
||||
private EditText edit;
|
||||
private View firstChild;
|
||||
private TextView label;
|
||||
private int labelTextSize;
|
||||
private int offsetY;
|
||||
@ -71,30 +73,37 @@ public class FloatingHintEditTextLayout extends FrameLayout implements CustomVie
|
||||
@Override
|
||||
protected void onFinishInflate(){
|
||||
super.onFinishInflate();
|
||||
if(getChildCount()>0 && getChildAt(0) instanceof EditText et){
|
||||
edit=et;
|
||||
if(getChildCount()>0){
|
||||
firstChild=getChildAt(0);
|
||||
if(firstChild instanceof EditText et)
|
||||
edit=et;
|
||||
}else{
|
||||
throw new IllegalStateException("First child must be an EditText");
|
||||
throw new IllegalStateException("Must contain at least one child view");
|
||||
}
|
||||
|
||||
label=new TextView(getContext());
|
||||
label.setTextSize(TypedValue.COMPLEX_UNIT_PX, labelTextSize);
|
||||
// label.setTextColor(labelColors==null ? edit.getHintTextColors() : labelColors);
|
||||
origHintColors=edit.getHintTextColors();
|
||||
label.setText(edit.getHint());
|
||||
if(edit!=null){
|
||||
origHintColors=edit.getHintTextColors();
|
||||
label.setText(edit.getHint());
|
||||
}
|
||||
label.setSingleLine();
|
||||
label.setPivotX(0f);
|
||||
label.setPivotY(0f);
|
||||
label.setImportantForAccessibility(IMPORTANT_FOR_ACCESSIBILITY_NO);
|
||||
LayoutParams lp=new LayoutParams(ViewGroup.LayoutParams.WRAP_CONTENT, ViewGroup.LayoutParams.WRAP_CONTENT, Gravity.START | Gravity.TOP);
|
||||
lp.setMarginStart(edit.getPaddingStart()+((LayoutParams)edit.getLayoutParams()).getMarginStart());
|
||||
lp.setMarginStart(firstChild.getPaddingStart()+((LayoutParams)firstChild.getLayoutParams()).getMarginStart());
|
||||
addView(label, lp);
|
||||
|
||||
hintVisible=edit.getText().length()==0;
|
||||
hintVisible=edit!=null && edit.getText().length()==0;
|
||||
if(hintVisible)
|
||||
label.setAlpha(0f);
|
||||
else
|
||||
animProgress=1;
|
||||
|
||||
edit.addTextChangedListener(new SimpleTextWatcher(this::onTextChanged));
|
||||
if(edit!=null)
|
||||
edit.addTextChangedListener(new SimpleTextWatcher(this::onTextChanged));
|
||||
|
||||
errorView=new LinkedTextView(getContext());
|
||||
errorView.setTextAppearance(R.style.m3_body_small);
|
||||
@ -110,6 +119,18 @@ public class FloatingHintEditTextLayout extends FrameLayout implements CustomVie
|
||||
label.setText(edit.getHint());
|
||||
}
|
||||
|
||||
public void setHint(CharSequence hint){
|
||||
label.setText(hint);
|
||||
}
|
||||
|
||||
public void setHint(@StringRes int hint){
|
||||
label.setText(hint);
|
||||
}
|
||||
|
||||
public TextView getLabel(){
|
||||
return label;
|
||||
}
|
||||
|
||||
private void onTextChanged(Editable text){
|
||||
if(errorState){
|
||||
errorView.setVisibility(View.GONE);
|
||||
@ -244,7 +265,7 @@ public class FloatingHintEditTextLayout extends FrameLayout implements CustomVie
|
||||
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec){
|
||||
if(errorView.getVisibility()!=GONE){
|
||||
int width=MeasureSpec.getSize(widthMeasureSpec)-getPaddingLeft()-getPaddingRight();
|
||||
LayoutParams editLP=(LayoutParams) edit.getLayoutParams();
|
||||
LayoutParams editLP=(LayoutParams) firstChild.getLayoutParams();
|
||||
width-=editLP.leftMargin+editLP.rightMargin;
|
||||
errorView.measure(width | MeasureSpec.EXACTLY, MeasureSpec.UNSPECIFIED);
|
||||
LayoutParams lp=(LayoutParams) errorView.getLayoutParams();
|
||||
@ -254,7 +275,7 @@ public class FloatingHintEditTextLayout extends FrameLayout implements CustomVie
|
||||
lp.leftMargin=editLP.leftMargin;
|
||||
editLP.bottomMargin=errorView.getMeasuredHeight();
|
||||
}else{
|
||||
LayoutParams editLP=(LayoutParams) edit.getLayoutParams();
|
||||
LayoutParams editLP=(LayoutParams) firstChild.getLayoutParams();
|
||||
editLP.bottomMargin=0;
|
||||
}
|
||||
super.onMeasure(widthMeasureSpec, heightMeasureSpec);
|
||||
@ -355,7 +376,7 @@ public class FloatingHintEditTextLayout extends FrameLayout implements CustomVie
|
||||
protected void onBoundsChange(@NonNull Rect bounds){
|
||||
super.onBoundsChange(bounds);
|
||||
int offset=dp(12);
|
||||
wrapped.setBounds(edit.getLeft()-offset, edit.getTop()-offset, edit.getRight()+offset, edit.getBottom()+offset);
|
||||
wrapped.setBounds(firstChild.getLeft()-offset, firstChild.getTop()-offset, firstChild.getRight()+offset, firstChild.getBottom()+offset);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
8
mastodon/src/main/res/drawable/bg_spinner.xml
Normal file
8
mastodon/src/main/res/drawable/bg_spinner.xml
Normal file
@ -0,0 +1,8 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<layer-list xmlns:android="http://schemas.android.com/apk/res/android">
|
||||
<item
|
||||
android:drawable="@drawable/ic_arrow_drop_down_24px"
|
||||
android:gravity="end|center_vertical"
|
||||
android:left="12dp"
|
||||
android:right="12dp"/>
|
||||
</layer-list>
|
@ -0,0 +1,9 @@
|
||||
<vector xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
android:width="24dp"
|
||||
android:height="24dp"
|
||||
android:viewportWidth="24"
|
||||
android:viewportHeight="24">
|
||||
<path
|
||||
android:fillColor="@android:color/white"
|
||||
android:pathData="M12,15 L7,10H17Z"/>
|
||||
</vector>
|
9
mastodon/src/main/res/drawable/ic_arrow_right_24px.xml
Normal file
9
mastodon/src/main/res/drawable/ic_arrow_right_24px.xml
Normal file
@ -0,0 +1,9 @@
|
||||
<vector xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
android:width="24dp"
|
||||
android:height="24dp"
|
||||
android:viewportWidth="24"
|
||||
android:viewportHeight="24">
|
||||
<path
|
||||
android:fillColor="@android:color/white"
|
||||
android:pathData="M10,17V7L15,12Z"/>
|
||||
</vector>
|
9
mastodon/src/main/res/drawable/ic_list_alt_24px.xml
Normal file
9
mastodon/src/main/res/drawable/ic_list_alt_24px.xml
Normal file
@ -0,0 +1,9 @@
|
||||
<vector xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
android:width="24dp"
|
||||
android:height="24dp"
|
||||
android:viewportWidth="24"
|
||||
android:viewportHeight="24">
|
||||
<path
|
||||
android:fillColor="@android:color/white"
|
||||
android:pathData="M8,17Q8.425,17 8.713,16.712Q9,16.425 9,16Q9,15.575 8.713,15.287Q8.425,15 8,15Q7.575,15 7.287,15.287Q7,15.575 7,16Q7,16.425 7.287,16.712Q7.575,17 8,17ZM8,13Q8.425,13 8.713,12.712Q9,12.425 9,12Q9,11.575 8.713,11.287Q8.425,11 8,11Q7.575,11 7.287,11.287Q7,11.575 7,12Q7,12.425 7.287,12.712Q7.575,13 8,13ZM8,9Q8.425,9 8.713,8.712Q9,8.425 9,8Q9,7.575 8.713,7.287Q8.425,7 8,7Q7.575,7 7.287,7.287Q7,7.575 7,8Q7,8.425 7.287,8.712Q7.575,9 8,9ZM11,17H17V15H11ZM11,13H17V11H11ZM11,9H17V7H11ZM5,21Q4.175,21 3.587,20.413Q3,19.825 3,19V5Q3,4.175 3.587,3.587Q4.175,3 5,3H19Q19.825,3 20.413,3.587Q21,4.175 21,5V19Q21,19.825 20.413,20.413Q19.825,21 19,21ZM5,19H19Q19,19 19,19Q19,19 19,19V5Q19,5 19,5Q19,5 19,5H5Q5,5 5,5Q5,5 5,5V19Q5,19 5,19Q5,19 5,19ZM5,5Q5,5 5,5Q5,5 5,5V19Q5,19 5,19Q5,19 5,19Q5,19 5,19Q5,19 5,19V5Q5,5 5,5Q5,5 5,5Z"/>
|
||||
</vector>
|
9
mastodon/src/main/res/drawable/ic_more_vert_24px.xml
Normal file
9
mastodon/src/main/res/drawable/ic_more_vert_24px.xml
Normal file
@ -0,0 +1,9 @@
|
||||
<vector xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
android:width="24dp"
|
||||
android:height="24dp"
|
||||
android:viewportWidth="24"
|
||||
android:viewportHeight="24">
|
||||
<path
|
||||
android:fillColor="@android:color/white"
|
||||
android:pathData="M12,20Q11.175,20 10.588,19.413Q10,18.825 10,18Q10,17.175 10.588,16.587Q11.175,16 12,16Q12.825,16 13.413,16.587Q14,17.175 14,18Q14,18.825 13.413,19.413Q12.825,20 12,20ZM12,14Q11.175,14 10.588,13.412Q10,12.825 10,12Q10,11.175 10.588,10.587Q11.175,10 12,10Q12.825,10 13.413,10.587Q14,11.175 14,12Q14,12.825 13.413,13.412Q12.825,14 12,14ZM12,8Q11.175,8 10.588,7.412Q10,6.825 10,6Q10,5.175 10.588,4.588Q11.175,4 12,4Q12.825,4 13.413,4.588Q14,5.175 14,6Q14,6.825 13.413,7.412Q12.825,8 12,8Z"/>
|
||||
</vector>
|
20
mastodon/src/main/res/layout/floating_hint_spinner.xml
Normal file
20
mastodon/src/main/res/layout/floating_hint_spinner.xml
Normal file
@ -0,0 +1,20 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<org.joinmastodon.android.ui.views.FloatingHintEditTextLayout xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
xmlns:app="http://schemas.android.com/apk/res-auto"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:paddingTop="4dp"
|
||||
android:paddingBottom="12dp"
|
||||
app:labelTextColor="@color/m3_outlined_text_field_label"
|
||||
android:foreground="@drawable/bg_m3_outlined_text_field">
|
||||
|
||||
<Spinner
|
||||
android:id="@+id/spinner"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginStart="16dp"
|
||||
android:layout_marginEnd="16dp"
|
||||
android:layout_marginTop="8dp"
|
||||
android:elevation="0dp"/>
|
||||
|
||||
</org.joinmastodon.android.ui.views.FloatingHintEditTextLayout>
|
@ -119,6 +119,17 @@
|
||||
android:layout_marginTop="2dp"
|
||||
android:duplicateParentState="true"
|
||||
android:visibility="gone"/>
|
||||
|
||||
<ImageButton
|
||||
android:id="@+id/options_btn"
|
||||
android:layout_width="40dp"
|
||||
android:layout_height="36dp"
|
||||
android:layout_gravity="top"
|
||||
android:background="?android:actionBarItemBackground"
|
||||
android:tint="?colorM3OnSurfaceVariant"
|
||||
android:contentDescription="@string/more_options"
|
||||
android:src="@drawable/ic_more_vert_24px"
|
||||
android:visibility="gone"/>
|
||||
|
||||
</FrameLayout>
|
||||
|
||||
|
17
mastodon/src/main/res/layout/item_dropdown_menu.xml
Normal file
17
mastodon/src/main/res/layout/item_dropdown_menu.xml
Normal file
@ -0,0 +1,17 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<TextView xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
xmlns:tools="http://schemas.android.com/tools"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="56dp"
|
||||
android:paddingStart="16dp"
|
||||
android:paddingEnd="24dp"
|
||||
android:drawablePadding="16dp"
|
||||
android:maxLines="2"
|
||||
android:gravity="center_vertical"
|
||||
android:textAppearance="@style/m3_body_large"
|
||||
android:textColor="?colorM3OnSurface"
|
||||
android:drawableTint="?colorM3OnSurface"
|
||||
android:ellipsize="end"
|
||||
tools:text="Menu item">
|
||||
|
||||
</TextView>
|
25
mastodon/src/main/res/layout/item_generic_list_options.xml
Normal file
25
mastodon/src/main/res/layout/item_generic_list_options.xml
Normal file
@ -0,0 +1,25 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
xmlns:tools="http://schemas.android.com/tools"
|
||||
android:orientation="horizontal"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:minHeight="56dp"
|
||||
android:paddingHorizontal="16dp"
|
||||
android:paddingVertical="12dp"
|
||||
android:gravity="center_vertical">
|
||||
|
||||
<include layout="@layout/item_generic_list_content"/>
|
||||
|
||||
<ImageButton
|
||||
android:id="@+id/options_btn"
|
||||
android:layout_width="32dp"
|
||||
android:layout_height="32dp"
|
||||
android:layout_marginStart="8dp"
|
||||
android:layout_gravity="top"
|
||||
android:contentDescription="@string/more_options"
|
||||
android:src="@drawable/ic_more_vert_24px"
|
||||
android:tint="?colorM3OnSurface"
|
||||
android:background="?android:actionBarItemBackground"/>
|
||||
|
||||
</LinearLayout>
|
9
mastodon/src/main/res/layout/item_spinner.xml
Normal file
9
mastodon/src/main/res/layout/item_spinner.xml
Normal file
@ -0,0 +1,9 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<TextView xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
android:id="@android:id/text1"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="match_parent"
|
||||
android:padding="16dp"
|
||||
android:textAppearance="@style/m3_body_large"
|
||||
android:textColor="?colorM3OnSurface"
|
||||
android:singleLine="true"/>
|
4
mastodon/src/main/res/menu/edit_list_action_mode.xml
Normal file
4
mastodon/src/main/res/menu/edit_list_action_mode.xml
Normal file
@ -0,0 +1,4 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<menu xmlns:android="http://schemas.android.com/apk/res/android">
|
||||
<item android:id="@+id/delete" android:showAsAction="always" android:icon="@drawable/ic_delete_24px" android:title="@string/delete"/>
|
||||
</menu>
|
@ -1,5 +1,10 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<menu xmlns:android="http://schemas.android.com/apk/res/android">
|
||||
<item
|
||||
android:id="@+id/edit_list"
|
||||
android:icon="@drawable/ic_edit_24px"
|
||||
android:showAsAction="always"
|
||||
android:title="@string/edit_list"/>
|
||||
<item
|
||||
android:id="@+id/settings"
|
||||
android:icon="@drawable/ic_settings_24px"
|
||||
|
@ -9,7 +9,7 @@
|
||||
<item android:id="@+id/delete" android:title="@string/delete"/>
|
||||
</group>
|
||||
<group android:id="@+id/menu_group2">
|
||||
<!-- TODO add to list -->
|
||||
<item android:id="@+id/add_to_list" android:title="@string/add_user_to_list"/>
|
||||
<item android:id="@+id/follow" android:title="@string/follow_user"/>
|
||||
<item android:id="@+id/mute" android:title="@string/mute_user"/>
|
||||
<item android:id="@+id/block" android:title="@string/block_user"/>
|
||||
|
@ -1,11 +1,13 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<menu xmlns:android="http://schemas.android.com/apk/res/android">
|
||||
<item android:id="@+id/share" android:title="@string/share_user"/>
|
||||
<!-- TODO add to list -->
|
||||
<item android:id="@+id/mute" android:title="@string/mute_user"/>
|
||||
<item android:id="@+id/block" android:title="@string/block_user"/>
|
||||
<item android:id="@+id/report" android:title="@string/report_user"/>
|
||||
<item android:id="@+id/block_domain" android:title="@string/block_domain"/>
|
||||
<item android:id="@+id/hide_boosts" android:title="@string/hide_boosts_from_user"/>
|
||||
<item android:id="@+id/open_in_browser" android:title="@string/open_in_browser"/>
|
||||
<group android:orderInCategory="1">
|
||||
<item android:id="@+id/share" android:title="@string/share_user"/>
|
||||
<item android:id="@+id/add_to_list" android:title="@string/add_user_to_list"/>
|
||||
<item android:id="@+id/mute" android:title="@string/mute_user"/>
|
||||
<item android:id="@+id/block" android:title="@string/block_user"/>
|
||||
<item android:id="@+id/report" android:title="@string/report_user"/>
|
||||
<item android:id="@+id/block_domain" android:title="@string/block_domain"/>
|
||||
<item android:id="@+id/hide_boosts" android:title="@string/hide_boosts_from_user"/>
|
||||
<item android:id="@+id/open_in_browser" android:title="@string/open_in_browser"/>
|
||||
</group>
|
||||
</menu>
|
13
mastodon/src/main/res/menu/standalone_list_timeline.xml
Normal file
13
mastodon/src/main/res/menu/standalone_list_timeline.xml
Normal file
@ -0,0 +1,13 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<menu xmlns:android="http://schemas.android.com/apk/res/android">
|
||||
<item
|
||||
android:id="@+id/members"
|
||||
android:icon="@drawable/ic_group_24px"
|
||||
android:title="@string/list_members"
|
||||
android:showAsAction="always"/>
|
||||
<item
|
||||
android:id="@+id/edit_list"
|
||||
android:icon="@drawable/ic_edit_24px"
|
||||
android:showAsAction="always"
|
||||
android:title="@string/edit_list"/>
|
||||
</menu>
|
@ -16,6 +16,7 @@
|
||||
<item name="notifications_mentions" type="id"/>
|
||||
|
||||
<item name="save" type="id"/>
|
||||
<item name="remove_from_list" type="id"/>
|
||||
|
||||
<item name="list_item_simple" type="id"/>
|
||||
<item name="list_item_simple_tinted" type="id"/>
|
||||
@ -23,6 +24,8 @@
|
||||
<item name="list_item_checkbox" type="id"/>
|
||||
<item name="list_item_radio" type="id"/>
|
||||
<item name="list_item_account" type="id"/>
|
||||
<item name="list_item_options" type="id"/>
|
||||
<item name="list_item_avatar_pile" type="id"/>
|
||||
|
||||
<item name="server_about" type="id"/>
|
||||
<item name="server_rules" type="id"/>
|
||||
|
@ -610,4 +610,39 @@
|
||||
</plurals>
|
||||
|
||||
<string name="error_playing_video">Error playing video</string>
|
||||
<string name="timeline_following">Following</string>
|
||||
<string name="lists">Lists</string>
|
||||
<string name="followed_hashtags">Followed hashtags</string>
|
||||
<string name="no_lists">You don\'t have any lists yet.</string>
|
||||
<string name="no_followed_hashtags">You don\'t follow any hashtags.</string>
|
||||
<string name="manage_lists">Manage lists</string>
|
||||
<string name="manage_hashtags">Manage hashtags</string>
|
||||
<!-- Screen reader description for the menu on the home timeline screen -->
|
||||
<string name="dropdown_menu">Dropdown menu</string>
|
||||
<string name="edit_list">Edit list</string>
|
||||
<string name="list_members">List members</string>
|
||||
<string name="delete_list">Delete list</string>
|
||||
<!-- %s is the name of the list -->
|
||||
<string name="delete_list_confirm">Delete “%s”?</string>
|
||||
<string name="list_exclusive">Hide members in Following</string>
|
||||
<string name="list_exclusive_subtitle">If someone is on this list, hide them in your Following timeline to avoid seeing their posts twice.</string>
|
||||
<string name="list_name">List name</string>
|
||||
<string name="list_show_replies_to">Show replies to</string>
|
||||
<string name="list_replies_no_one">No one</string>
|
||||
<string name="list_replies_members">Members of the list</string>
|
||||
<string name="list_replies_anyone">Anyone I follow</string>
|
||||
<string name="confirm_remove_list_members">Remove members?</string>
|
||||
<string name="remove">Remove</string>
|
||||
<string name="add_list_member">Add member</string>
|
||||
<string name="search_among_people_you_follow">Search among people you follow</string>
|
||||
<string name="add_user_to_list">Add to list…</string>
|
||||
<string name="add_user_to_list_title">Add to list</string>
|
||||
<!-- %s is a username -->
|
||||
<string name="manage_user_lists">Manage the lists %s appears on</string>
|
||||
<string name="remove_from_list">Remove from list</string>
|
||||
<string name="confirm_remove_list_member">Remove member?</string>
|
||||
<plurals name="x_posts_recently">
|
||||
<item quantity="one">%,d post recently</item>
|
||||
<item quantity="other">%,d posts recently</item>
|
||||
</plurals>
|
||||
</resources>
|
@ -7,6 +7,8 @@
|
||||
<item name="android:splitMotionEvents">false</item>
|
||||
<item name="android:windowBackground">@color/m3_sys_light_surface</item>
|
||||
<item name="android:editTextStyle">@style/Widget.Mastodon.EditText</item>
|
||||
<item name="android:spinnerStyle">@style/Widget.Mastodon.Spinner</item>
|
||||
<item name="android:actionModeCloseButtonStyle">@style/action_mode_close</item>
|
||||
|
||||
<item name="android:buttonStyle">@style/Widget.Mastodon.M3.Button.Filled</item>
|
||||
<item name="android:actionBarTheme">@style/Theme.Mastodon.Toolbar</item>
|
||||
@ -73,6 +75,8 @@
|
||||
<item name="android:splitMotionEvents">false</item>
|
||||
<item name="android:windowBackground">@color/m3_sys_dark_surface</item>
|
||||
<item name="android:editTextStyle">@style/Widget.Mastodon.EditText</item>
|
||||
<item name="android:spinnerStyle">@style/Widget.Mastodon.Spinner</item>
|
||||
<item name="android:actionModeCloseButtonStyle">@style/action_mode_close</item>
|
||||
|
||||
<item name="android:buttonStyle">@style/Widget.Mastodon.M3.Button</item>
|
||||
<item name="android:actionBarTheme">@style/Theme.Mastodon.Toolbar</item>
|
||||
@ -165,7 +169,12 @@
|
||||
<item name="android:titleTextStyle">@style/action_mode_title</item>
|
||||
</style>
|
||||
|
||||
<style name="action_mode_close" parent="@android:style/Widget.Material.ActionButton.CloseMode">
|
||||
<item name="android:layout_margin">0dp</item>
|
||||
</style>
|
||||
|
||||
<style name="action_mode_title" parent="android:TextAppearance.Material.Widget.ActionMode.Title">
|
||||
<item name="android:fontFamily">sans-serif</item>
|
||||
<item name="android:textColor">?colorM3OnPrimary</item>
|
||||
</style>
|
||||
|
||||
@ -192,6 +201,12 @@
|
||||
<item name="android:textAppearance">@style/m3_body_large</item>
|
||||
</style>
|
||||
|
||||
<style name="Widget.Mastodon.Spinner" parent="android:Widget.Material.Light.Spinner">
|
||||
<item name="android:popupBackground">@drawable/bg_popup</item>
|
||||
<item name="android:background">@drawable/bg_spinner</item>
|
||||
<item name="android:backgroundTint">?colorM3OnSurface</item>
|
||||
</style>
|
||||
|
||||
<style name="Theme.Mastodon.Dialog.Alert" parent="android:Theme.Material.Light.Dialog.Alert">
|
||||
<item name="android:windowTitleStyle">@style/alert_title</item>
|
||||
<item name="android:dialogPreferredPadding">24dp</item>
|
||||
|
Loading…
x
Reference in New Issue
Block a user