M3 redesign: search/discover

This commit is contained in:
Grishka
2023-06-24 22:56:55 +03:00
parent c9e467ac2f
commit e1db5f15ca
40 changed files with 1300 additions and 774 deletions

View File

@@ -71,7 +71,7 @@ public class MainActivity extends FragmentStackActivity{
}else if(intent.getBooleanExtra("compose", false)){
showCompose();
}else if(Intent.ACTION_VIEW.equals(intent.getAction())){
handleURL(intent.getData());
handleURL(intent.getData(), null);
}else{
maybeRequestNotificationsPermission();
}
@@ -114,29 +114,34 @@ public class MainActivity extends FragmentStackActivity{
}else if(intent.getBooleanExtra("compose", false)){
showCompose();
}else if(Intent.ACTION_VIEW.equals(intent.getAction())){
handleURL(intent.getData());
handleURL(intent.getData(), null);
}/*else if(intent.hasExtra(PackageInstaller.EXTRA_STATUS) && GithubSelfUpdater.needSelfUpdating()){
GithubSelfUpdater.getInstance().handleIntentFromInstaller(intent, this);
}*/
}
private void handleURL(Uri uri){
public void handleURL(Uri uri, String accountID){
if(uri==null)
return;
if(!"https".equals(uri.getScheme()) && !"http".equals(uri.getScheme()))
return;
if(!uri.getPath().startsWith("/@"))
return;
AccountSession session=AccountSessionManager.getInstance().getLastActiveAccount();
AccountSession session;
if(accountID==null)
session=AccountSessionManager.getInstance().getLastActiveAccount();
else
session=AccountSessionManager.get(accountID);
if(session==null || !session.activated)
return;
openSearchQuery(uri.toString(), session.getID(), R.string.opening_link, false);
}
new GetSearchResults(uri.toString(), null, true)
public void openSearchQuery(String q, String accountID, int progressText, boolean fromSearch){
new GetSearchResults(q, null, true)
.setCallback(new Callback<>(){
@Override
public void onSuccess(SearchResults result){
Bundle args=new Bundle();
args.putString("account", session.getID());
args.putString("account", accountID);
if(result.statuses!=null && !result.statuses.isEmpty()){
args.putParcelable("status", Parcels.wrap(result.statuses.get(0)));
Nav.go(MainActivity.this, ThreadFragment.class, args);
@@ -144,7 +149,7 @@ public class MainActivity extends FragmentStackActivity{
args.putParcelable("profileAccount", Parcels.wrap(result.accounts.get(0)));
Nav.go(MainActivity.this, ProfileFragment.class, args);
}else{
Toast.makeText(MainActivity.this, R.string.link_not_supported, Toast.LENGTH_SHORT).show();
Toast.makeText(MainActivity.this, fromSearch ? R.string.no_search_results : R.string.link_not_supported, Toast.LENGTH_SHORT).show();
}
}
@@ -153,8 +158,8 @@ public class MainActivity extends FragmentStackActivity{
error.showToast(MainActivity.this);
}
})
.wrapProgress(this, R.string.opening_link, true)
.exec(session.getID());
.wrapProgress(this, progressText, true)
.exec(accountID);
}
private void showFragmentForNotification(Notification notification, String accountID){

View File

@@ -13,6 +13,11 @@ public class GetSearchResults extends MastodonAPIRequest<SearchResults>{
addQueryParameter("resolve", "true");
}
public GetSearchResults limit(int limit){
addQueryParameter("limit", String.valueOf(limit));
return this;
}
@Override
protected String getPathPrefix(){
return "/api/v2";

View File

@@ -54,6 +54,7 @@ import me.grishka.appkit.imageloader.ImageLoaderRecyclerAdapter;
import me.grishka.appkit.imageloader.ImageLoaderViewHolder;
import me.grishka.appkit.imageloader.requests.ImageLoaderRequest;
import me.grishka.appkit.utils.BindableViewHolder;
import me.grishka.appkit.utils.MergeRecyclerAdapter;
import me.grishka.appkit.utils.V;
import me.grishka.appkit.views.UsableRecyclerView;
@@ -309,6 +310,9 @@ public abstract class BaseStatusListFragment<T extends DisplayItemsParent> exten
}
protected int getMainAdapterOffset(){
if(list.getAdapter() instanceof MergeRecyclerAdapter mergeAdapter){
return mergeAdapter.getPositionForAdapter(adapter);
}
return 0;
}

View File

@@ -242,7 +242,7 @@ public class ProfileFragment extends LoaderFragment implements OnBackPressedList
sizeWrapper.addView(content);
tabbar.setTabTextColors(UiUtils.getThemeColor(getActivity(), R.attr.colorM3OnSurfaceVariant), UiUtils.getThemeColor(getActivity(), R.attr.colorM3Primary));
tabbar.setTabTextSize(V.dp(16));
tabbar.setTabTextSize(V.dp(14));
tabLayoutMediator=new TabLayoutMediator(tabbar, pager, new TabLayoutMediator.TabConfigurationStrategy(){
@Override
public void onConfigureTab(@NonNull TabLayout.Tab tab, int position){

View File

@@ -1,22 +1,14 @@
package org.joinmastodon.android.fragments.account_list;
import android.content.res.ColorStateList;
import android.os.Bundle;
import android.text.InputType;
import android.text.TextUtils;
import android.view.View;
import android.view.ViewGroup;
import android.view.inputmethod.EditorInfo;
import android.widget.EditText;
import android.widget.ImageButton;
import android.widget.LinearLayout;
import android.widget.Toolbar;
import org.joinmastodon.android.R;
import org.joinmastodon.android.api.requests.search.GetSearchResults;
import org.joinmastodon.android.model.SearchResults;
import org.joinmastodon.android.model.viewmodel.AccountViewModel;
import org.joinmastodon.android.ui.utils.SimpleTextWatcher;
import org.joinmastodon.android.ui.SearchViewHelper;
import org.joinmastodon.android.ui.utils.UiUtils;
import org.joinmastodon.android.ui.viewholders.AccountViewHolder;
import org.parceler.Parcels;
@@ -25,23 +17,11 @@ import java.util.stream.Collectors;
import me.grishka.appkit.Nav;
import me.grishka.appkit.api.SimpleCallback;
import me.grishka.appkit.utils.V;
public class ComposeAccountSearchFragment extends BaseAccountListFragment{
private LinearLayout searchLayout;
private EditText searchEdit;
private ImageButton clearSearchButton;
private String currentQuery;
private Runnable debouncer=()->{
currentQuery=searchEdit.getText().toString();
if(currentRequest!=null){
currentRequest.cancel();
currentRequest=null;
}
if(!TextUtils.isEmpty(currentQuery))
loadData();
};
private boolean resultDelivered;
private SearchViewHelper searchViewHelper;
@Override
public void onCreate(Bundle savedInstanceState){
@@ -53,32 +33,9 @@ public class ComposeAccountSearchFragment extends BaseAccountListFragment{
@Override
public void onViewCreated(View view, Bundle savedInstanceState){
searchLayout=new LinearLayout(view.getContext());
searchLayout.setOrientation(LinearLayout.HORIZONTAL);
searchEdit=new EditText(view.getContext());
searchEdit.setHint(R.string.search_hint);
searchEdit.setInputType(InputType.TYPE_TEXT_VARIATION_FILTER);
searchEdit.setBackground(null);
searchEdit.addTextChangedListener(new SimpleTextWatcher(e->{
searchEdit.removeCallbacks(debouncer);
searchEdit.postDelayed(debouncer, 300);
}));
searchEdit.setImeActionLabel(null, EditorInfo.IME_ACTION_SEARCH);
searchEdit.setOnEditorActionListener((v, actionId, event)->{
searchEdit.removeCallbacks(debouncer);
debouncer.run();
return true;
});
searchLayout.addView(searchEdit, new LinearLayout.LayoutParams(0, ViewGroup.LayoutParams.MATCH_PARENT, 1f));
clearSearchButton=new ImageButton(view.getContext());
clearSearchButton.setImageResource(R.drawable.ic_baseline_close_24);
clearSearchButton.setImageTintList(ColorStateList.valueOf(UiUtils.getThemeColor(view.getContext(), R.attr.colorM3OnSurfaceVariant)));
clearSearchButton.setBackground(UiUtils.getThemeDrawable(getToolbarContext(), android.R.attr.actionBarItemBackground));
clearSearchButton.setOnClickListener(v->searchEdit.setText(""));
searchLayout.addView(clearSearchButton, new LinearLayout.LayoutParams(V.dp(56), ViewGroup.LayoutParams.MATCH_PARENT));
searchViewHelper=new SearchViewHelper(getActivity(), getToolbarContext(), getString(R.string.search_hint));
searchViewHelper.setListeners(this::onQueryChanged, null);
searchViewHelper.addDivider(contentView);
super.onViewCreated(view, savedInstanceState);
view.setBackgroundResource(R.drawable.bg_m3_surface3);
@@ -104,11 +61,7 @@ public class ComposeAccountSearchFragment extends BaseAccountListFragment{
@Override
protected void onUpdateToolbar(){
super.onUpdateToolbar();
if(searchLayout.getParent()!=null)
((ViewGroup) searchLayout.getParent()).removeView(searchLayout);
getToolbar().addView(searchLayout, new Toolbar.LayoutParams(ViewGroup.LayoutParams.MATCH_PARENT, ViewGroup.LayoutParams.MATCH_PARENT));
getToolbar().setBackgroundResource(R.drawable.bg_m3_surface3);
searchEdit.requestFocus();
searchViewHelper.install(getToolbar());
}
@Override
@@ -132,4 +85,14 @@ public class ComposeAccountSearchFragment extends BaseAccountListFragment{
setResult(true, res);
Nav.finish(this, false);
}
private void onQueryChanged(String q){
currentQuery=q;
if(currentRequest!=null){
currentRequest.cancel();
currentRequest=null;
}
if(!TextUtils.isEmpty(currentQuery))
loadData();
}
}

View File

@@ -1,80 +1,39 @@
package org.joinmastodon.android.fragments.discover;
import android.graphics.Rect;
import android.graphics.drawable.Animatable;
import android.graphics.drawable.Drawable;
import android.os.Bundle;
import android.text.SpannableStringBuilder;
import android.text.TextUtils;
import android.view.View;
import android.view.ViewGroup;
import android.widget.ImageView;
import android.widget.ProgressBar;
import android.widget.TextView;
import org.joinmastodon.android.R;
import org.joinmastodon.android.api.requests.accounts.GetAccountRelationships;
import org.joinmastodon.android.api.requests.accounts.GetFollowSuggestions;
import org.joinmastodon.android.fragments.ProfileFragment;
import org.joinmastodon.android.fragments.ScrollableToTop;
import org.joinmastodon.android.model.Account;
import org.joinmastodon.android.fragments.account_list.BaseAccountListFragment;
import org.joinmastodon.android.model.FollowSuggestion;
import org.joinmastodon.android.model.Relationship;
import org.joinmastodon.android.ui.OutlineProviders;
import org.joinmastodon.android.ui.text.HtmlParser;
import org.joinmastodon.android.ui.utils.CustomEmojiHelper;
import org.joinmastodon.android.ui.utils.UiUtils;
import org.joinmastodon.android.ui.views.ProgressBarButton;
import org.parceler.Parcels;
import org.joinmastodon.android.model.viewmodel.AccountViewModel;
import org.joinmastodon.android.ui.utils.DiscoverInfoBannerHelper;
import java.util.Collections;
import java.util.List;
import java.util.Map;
import java.util.function.Function;
import java.util.stream.Collectors;
import androidx.annotation.NonNull;
import androidx.recyclerview.widget.RecyclerView;
import me.grishka.appkit.Nav;
import me.grishka.appkit.api.Callback;
import me.grishka.appkit.api.ErrorResponse;
import me.grishka.appkit.api.SimpleCallback;
import me.grishka.appkit.fragments.BaseRecyclerFragment;
import me.grishka.appkit.imageloader.ImageLoaderRecyclerAdapter;
import me.grishka.appkit.imageloader.ImageLoaderViewHolder;
import me.grishka.appkit.imageloader.requests.ImageLoaderRequest;
import me.grishka.appkit.imageloader.requests.UrlImageLoaderRequest;
import me.grishka.appkit.utils.BindableViewHolder;
import me.grishka.appkit.utils.V;
import me.grishka.appkit.views.UsableRecyclerView;
import me.grishka.appkit.utils.MergeRecyclerAdapter;
public class DiscoverAccountsFragment extends BaseRecyclerFragment<DiscoverAccountsFragment.AccountWrapper> implements ScrollableToTop{
private String accountID;
private Map<String, Relationship> relationships=Collections.emptyMap();
private GetAccountRelationships relationshipsRequest;
public DiscoverAccountsFragment(){
super(20);
}
public class DiscoverAccountsFragment extends BaseAccountListFragment implements ScrollableToTop{
private DiscoverInfoBannerHelper bannerHelper;
@Override
public void onCreate(Bundle savedInstanceState){
super.onCreate(savedInstanceState);
accountID=getArguments().getString("account");
bannerHelper=new DiscoverInfoBannerHelper(DiscoverInfoBannerHelper.BannerType.ACCOUNTS, accountID);
}
@Override
protected void doLoadData(int offset, int count){
if(relationshipsRequest!=null){
relationshipsRequest.cancel();
relationshipsRequest=null;
}
currentRequest=new GetFollowSuggestions(count)
.setCallback(new SimpleCallback<>(this){
@Override
public void onSuccess(List<FollowSuggestion> result){
onDataLoaded(result.stream().map(fs->new AccountWrapper(fs.account)).collect(Collectors.toList()), false);
loadRelationships();
List<AccountViewModel> accounts=result.stream().map(fs->new AccountViewModel(fs.account, accountID)).collect(Collectors.toList());
onDataLoaded(accounts, false);
bannerHelper.onBannerBecameVisible();
}
})
.exec(accountID);
@@ -82,219 +41,14 @@ public class DiscoverAccountsFragment extends BaseRecyclerFragment<DiscoverAccou
@Override
protected RecyclerView.Adapter getAdapter(){
return new AccountsAdapter();
}
@Override
public void onViewCreated(View view, Bundle savedInstanceState){
super.onViewCreated(view, savedInstanceState);
list.addItemDecoration(new RecyclerView.ItemDecoration(){
@Override
public void getItemOffsets(@NonNull Rect outRect, @NonNull View view, @NonNull RecyclerView parent, @NonNull RecyclerView.State state){
outRect.bottom=outRect.left=outRect.right=V.dp(16);
if(parent.getChildAdapterPosition(view)==0)
outRect.top=V.dp(16);
}
});
((UsableRecyclerView)list).setDrawSelectorOnTop(true);
}
private void loadRelationships(){
relationships=Collections.emptyMap();
relationshipsRequest=new GetAccountRelationships(data.stream().map(fs->fs.account.id).collect(Collectors.toList()));
relationshipsRequest.setCallback(new Callback<>(){
@Override
public void onSuccess(List<Relationship> result){
relationshipsRequest=null;
relationships=result.stream().collect(Collectors.toMap(rel->rel.id, Function.identity()));
if(list==null)
return;
for(int i=0;i<list.getChildCount();i++){
RecyclerView.ViewHolder holder=list.getChildViewHolder(list.getChildAt(i));
if(holder instanceof AccountViewHolder avh)
avh.rebind();
}
}
@Override
public void onError(ErrorResponse error){
relationshipsRequest=null;
}
}).exec(accountID);
}
@Override
public void onDestroyView(){
super.onDestroyView();
if(relationshipsRequest!=null){
relationshipsRequest.cancel();
relationshipsRequest=null;
}
MergeRecyclerAdapter adapter=new MergeRecyclerAdapter();
bannerHelper.maybeAddBanner(list, adapter);
adapter.addAdapter(super.getAdapter());
return adapter;
}
@Override
public void scrollToTop(){
smoothScrollRecyclerViewToTop(list);
}
private class AccountsAdapter extends UsableRecyclerView.Adapter<AccountViewHolder> implements ImageLoaderRecyclerAdapter{
public AccountsAdapter(){
super(imgLoader);
}
@Override
public void onBindViewHolder(AccountViewHolder holder, int position){
holder.bind(data.get(position));
super.onBindViewHolder(holder, position);
}
@NonNull
@Override
public AccountViewHolder onCreateViewHolder(@NonNull ViewGroup parent, int viewType){
return new AccountViewHolder();
}
@Override
public int getItemCount(){
return data.size();
}
@Override
public int getImageCountForItem(int position){
return 2+data.get(position).emojiHelper.getImageCount();
}
@Override
public ImageLoaderRequest getImageRequest(int position, int image){
AccountWrapper item=data.get(position);
if(image==0)
return item.avaRequest;
else if(image==1)
return item.coverRequest;
else
return item.emojiHelper.getImageRequest(image-2);
}
}
private class AccountViewHolder extends BindableViewHolder<AccountWrapper> implements ImageLoaderViewHolder, UsableRecyclerView.Clickable{
private final ImageView cover, avatar;
private final TextView name, username, bio, followersCount, followingCount, postsCount, followersLabel, followingLabel, postsLabel;
private final ProgressBarButton actionButton;
private final ProgressBar actionProgress;
private final View actionWrap;
private Relationship relationship;
public AccountViewHolder(){
super(getActivity(), R.layout.item_discover_account, list);
cover=findViewById(R.id.cover);
avatar=findViewById(R.id.avatar);
name=findViewById(R.id.name);
username=findViewById(R.id.username);
bio=findViewById(R.id.bio);
followersCount=findViewById(R.id.followers_count);
followersLabel=findViewById(R.id.followers_label);
followingCount=findViewById(R.id.following_count);
followingLabel=findViewById(R.id.following_label);
postsCount=findViewById(R.id.posts_count);
postsLabel=findViewById(R.id.posts_label);
actionButton=findViewById(R.id.action_btn);
actionProgress=findViewById(R.id.action_progress);
actionWrap=findViewById(R.id.action_btn_wrap);
itemView.setOutlineProvider(OutlineProviders.roundedRect(6));
itemView.setClipToOutline(true);
avatar.setOutlineProvider(OutlineProviders.roundedRect(12));
avatar.setClipToOutline(true);
cover.setOutlineProvider(OutlineProviders.roundedRect(3));
cover.setClipToOutline(true);
actionButton.setOnClickListener(this::onActionButtonClick);
}
@Override
public void onBind(AccountWrapper item){
name.setText(item.parsedName);
username.setText('@'+item.account.acct);
bio.setText(item.parsedBio);
followersCount.setText(UiUtils.abbreviateNumber(item.account.followersCount));
followingCount.setText(UiUtils.abbreviateNumber(item.account.followingCount));
postsCount.setText(UiUtils.abbreviateNumber(item.account.statusesCount));
followersLabel.setText(getResources().getQuantityString(R.plurals.followers, (int)Math.min(999, item.account.followersCount)));
followingLabel.setText(getResources().getQuantityString(R.plurals.following, (int)Math.min(999, item.account.followingCount)));
postsLabel.setText(getResources().getQuantityString(R.plurals.posts, (int)Math.min(999, item.account.statusesCount)));
relationship=relationships.get(item.account.id);
if(relationship==null){
actionWrap.setVisibility(View.GONE);
}else{
actionWrap.setVisibility(View.VISIBLE);
UiUtils.setRelationshipToActionButton(relationship, actionButton);
}
}
@Override
public void setImage(int index, Drawable image){
if(index==0){
avatar.setImageDrawable(image);
}else if(index==1){
cover.setImageDrawable(image);
}else{
item.emojiHelper.setImageDrawable(index-2, image);
name.invalidate();
bio.invalidate();
}
if(image instanceof Animatable a && !a.isRunning())
a.start();
}
@Override
public void clearImage(int index){
setImage(index, null);
}
@Override
public void onClick(){
Bundle args=new Bundle();
args.putString("account", accountID);
args.putParcelable("profileAccount", Parcels.wrap(item.account));
Nav.go(getActivity(), ProfileFragment.class, args);
}
private void onActionButtonClick(View v){
itemView.setHasTransientState(true);
UiUtils.performAccountAction(getActivity(), item.account, accountID, relationship, actionButton, this::setActionProgressVisible, rel->{
itemView.setHasTransientState(false);
relationships.put(item.account.id, rel);
rebind();
});
}
private void setActionProgressVisible(boolean visible){
actionButton.setTextVisible(!visible);
actionProgress.setVisibility(visible ? View.VISIBLE : View.GONE);
actionButton.setClickable(!visible);
}
}
protected class AccountWrapper{
public Account account;
public ImageLoaderRequest avaRequest, coverRequest;
public CustomEmojiHelper emojiHelper=new CustomEmojiHelper();
public CharSequence parsedName, parsedBio;
public AccountWrapper(Account account){
this.account=account;
if(!TextUtils.isEmpty(account.avatar))
avaRequest=new UrlImageLoaderRequest(account.avatar, V.dp(50), V.dp(50));
if(!TextUtils.isEmpty(account.header))
coverRequest=new UrlImageLoaderRequest(account.header, 1000, 1000);
parsedBio=HtmlParser.parse(account.note, account.emojis, Collections.emptyList(), Collections.emptyList(), accountID);
if(account.emojis.isEmpty()){
parsedName=account.displayName;
}else{
parsedName=HtmlParser.parseCustomEmoji(account.displayName, account.emojis);
emojiHelper.setText(new SpannableStringBuilder(parsedName).append(parsedBio));
}
}
}
}

View File

@@ -3,22 +3,19 @@ package org.joinmastodon.android.fragments.discover;
import android.app.Fragment;
import android.os.Build;
import android.os.Bundle;
import android.text.Editable;
import android.text.TextWatcher;
import android.view.KeyEvent;
import android.text.TextUtils;
import android.view.LayoutInflater;
import android.view.View;
import android.view.ViewGroup;
import android.view.inputmethod.InputMethodManager;
import android.widget.EditText;
import android.widget.FrameLayout;
import android.widget.ImageButton;
import android.widget.LinearLayout;
import android.widget.ProgressBar;
import android.widget.TextView;
import org.joinmastodon.android.R;
import org.joinmastodon.android.fragments.ScrollableToTop;
import org.joinmastodon.android.model.SearchResult;
import org.joinmastodon.android.ui.OutlineProviders;
import org.joinmastodon.android.ui.SimpleViewHolder;
import org.joinmastodon.android.ui.tabs.TabLayout;
import org.joinmastodon.android.ui.tabs.TabLayoutMediator;
@@ -28,22 +25,24 @@ import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import androidx.recyclerview.widget.RecyclerView;
import androidx.viewpager2.widget.ViewPager2;
import me.grishka.appkit.Nav;
import me.grishka.appkit.fragments.AppKitFragment;
import me.grishka.appkit.fragments.BaseRecyclerFragment;
import me.grishka.appkit.fragments.OnBackPressedListener;
import me.grishka.appkit.utils.V;
public class DiscoverFragment extends AppKitFragment implements ScrollableToTop, OnBackPressedListener{
private static final int QUERY_RESULT=937;
private TabLayout tabLayout;
private ViewPager2 pager;
private FrameLayout[] tabViews;
private TabLayoutMediator tabLayoutMediator;
private EditText searchEdit;
private boolean searchActive;
private FrameLayout searchView;
private ImageButton searchBack, searchClear;
private ProgressBar searchProgress;
private ImageButton searchBack;
private TextView searchText;
private View tabsDivider;
private DiscoverPostsFragment postsFragment;
private TrendingHashtagsFragment hashtagsFragment;
@@ -53,7 +52,7 @@ public class DiscoverFragment extends AppKitFragment implements ScrollableToTop,
private LocalTimelineFragment localTimelineFragment;
private String accountID;
private Runnable searchDebouncer=this::onSearchChangedDebounced;
private String currentQuery;
@Override
public void onCreate(Bundle savedInstanceState){
@@ -88,8 +87,8 @@ public class DiscoverFragment extends AppKitFragment implements ScrollableToTop,
tabViews[i]=tabView;
}
tabLayout.setTabTextSize(V.dp(16));
tabLayout.setTabTextColors(UiUtils.getThemeColor(getActivity(), R.attr.colorTabInactive), UiUtils.getThemeColor(getActivity(), android.R.attr.textColorPrimary));
tabLayout.setTabTextColors(UiUtils.getThemeColor(getActivity(), R.attr.colorM3OnSurfaceVariant), UiUtils.getThemeColor(getActivity(), R.attr.colorM3Primary));
tabLayout.setTabTextSize(V.dp(14));
pager.setOffscreenPageLimit(4);
pager.setAdapter(new DiscoverPagerAdapter());
@@ -146,7 +145,6 @@ public class DiscoverFragment extends AppKitFragment implements ScrollableToTop,
case 4 -> R.string.for_you;
default -> throw new IllegalStateException("Unexpected value: "+position);
});
tab.view.textView.setAllCaps(true);
}
});
tabLayoutMediator.attach();
@@ -163,44 +161,17 @@ public class DiscoverFragment extends AppKitFragment implements ScrollableToTop,
}
});
searchEdit=view.findViewById(R.id.search_edit);
searchEdit.setOnFocusChangeListener(this::onSearchEditFocusChanged);
searchEdit.setOnEditorActionListener(this::onSearchEnterPressed);
searchEdit.addTextChangedListener(new TextWatcher(){
@Override
public void beforeTextChanged(CharSequence s, int start, int count, int after){
if(s.length()==0){
V.setVisibilityAnimated(searchClear, View.VISIBLE);
}
}
@Override
public void onTextChanged(CharSequence s, int start, int before, int count){
searchEdit.removeCallbacks(searchDebouncer);
searchEdit.postDelayed(searchDebouncer, 300);
}
@Override
public void afterTextChanged(Editable s){
if(s.length()==0){
V.setVisibilityAnimated(searchClear, View.INVISIBLE);
}
}
});
searchView=view.findViewById(R.id.search_fragment);
if(searchFragment==null){
searchFragment=new SearchFragment();
Bundle args=new Bundle();
args.putString("account", accountID);
searchFragment.setArguments(args);
searchFragment.setProgressVisibilityListener(this::onSearchProgressVisibilityChanged);
getChildFragmentManager().beginTransaction().add(R.id.search_fragment, searchFragment).commit();
}
searchBack=view.findViewById(R.id.search_back);
searchClear=view.findViewById(R.id.search_clear);
searchProgress=view.findViewById(R.id.search_progress);
searchText=view.findViewById(R.id.search_text);
searchBack.setEnabled(searchActive);
searchBack.setImportantForAccessibility(searchActive ? View.IMPORTANT_FOR_ACCESSIBILITY_YES : View.IMPORTANT_FOR_ACCESSIBILITY_NO);
searchBack.setOnClickListener(v->exitSearch());
@@ -210,11 +181,19 @@ public class DiscoverFragment extends AppKitFragment implements ScrollableToTop,
tabLayout.setVisibility(View.GONE);
searchView.setVisibility(View.VISIBLE);
}
searchClear.setOnClickListener(v->{
searchEdit.setText("");
searchEdit.removeCallbacks(searchDebouncer);
onSearchChangedDebounced();
View searchWrap=view.findViewById(R.id.search_wrap);
searchWrap.setOutlineProvider(OutlineProviders.roundedRect(28));
searchWrap.setClipToOutline(true);
searchText.setOnClickListener(v->{
Bundle args=new Bundle();
args.putString("account", accountID);
if(!TextUtils.isEmpty(currentQuery)){
args.putString("query", currentQuery);
}
Nav.goForResult(getActivity(), SearchQueryFragment.class, args, QUERY_RESULT, DiscoverFragment.this);
});
tabsDivider=view.findViewById(R.id.tabs_divider);
return view;
}
@@ -233,35 +212,32 @@ public class DiscoverFragment extends AppKitFragment implements ScrollableToTop,
postsFragment.loadData();
}
private void onSearchEditFocusChanged(View v, boolean hasFocus){
if(!searchActive && hasFocus){
private void enterSearch(){
if(!searchActive){
searchActive=true;
pager.setVisibility(View.GONE);
tabLayout.setVisibility(View.GONE);
searchView.setVisibility(View.VISIBLE);
searchBack.setImageResource(R.drawable.ic_fluent_arrow_left_24_regular);
searchBack.setImageResource(R.drawable.ic_arrow_back);
searchBack.setEnabled(true);
searchBack.setImportantForAccessibility(View.IMPORTANT_FOR_ACCESSIBILITY_YES);
tabsDivider.setVisibility(View.GONE);
}
}
private void exitSearch(){
if(!searchActive)
return;
searchActive=false;
pager.setVisibility(View.VISIBLE);
tabLayout.setVisibility(View.VISIBLE);
searchView.setVisibility(View.GONE);
searchEdit.clearFocus();
searchEdit.setText("");
searchBack.setImageResource(R.drawable.ic_fluent_search_24_regular);
searchText.setText(R.string.search_mastodon);
searchBack.setImageResource(R.drawable.ic_search_24px);
searchBack.setEnabled(false);
searchBack.setImportantForAccessibility(View.IMPORTANT_FOR_ACCESSIBILITY_NO);
getActivity().getSystemService(InputMethodManager.class).hideSoftInputFromWindow(searchEdit.getWindowToken(), 0);
}
@Override
protected void onHidden(){
super.onHidden();
getActivity().getSystemService(InputMethodManager.class).hideSoftInputFromWindow(searchEdit.getWindowToken(), 0);
tabsDivider.setVisibility(View.VISIBLE);
currentQuery=null;
}
private Fragment getFragmentForPage(int page){
@@ -284,23 +260,20 @@ public class DiscoverFragment extends AppKitFragment implements ScrollableToTop,
return false;
}
private void onSearchChangedDebounced(){
searchFragment.setQuery(searchEdit.getText().toString());
}
private boolean onSearchEnterPressed(TextView v, int actionId, KeyEvent event){
if(event!=null && event.getAction()!=KeyEvent.ACTION_DOWN)
return true;
searchEdit.removeCallbacks(searchDebouncer);
onSearchChangedDebounced();
getActivity().getSystemService(InputMethodManager.class).hideSoftInputFromWindow(searchEdit.getWindowToken(), 0);
return true;
}
private void onSearchProgressVisibilityChanged(boolean visible){
V.setVisibilityAnimated(searchProgress, visible ? View.VISIBLE : View.INVISIBLE);
if(searchEdit.length()>0)
V.setVisibilityAnimated(searchClear, visible ? View.INVISIBLE : View.VISIBLE);
@Override
public void onFragmentResult(int reqCode, boolean success, Bundle result){
if(reqCode==QUERY_RESULT && success){
enterSearch();
currentQuery=result.getString("query");
SearchResult.Type type;
if(result.containsKey("filter")){
type=SearchResult.Type.values()[result.getInt("filter")];
}else{
type=null;
}
searchFragment.setQuery(currentQuery, type);
searchText.setText(currentQuery);
}
}
private class DiscoverPagerAdapter extends RecyclerView.Adapter<SimpleViewHolder>{

View File

@@ -1,5 +1,6 @@
package org.joinmastodon.android.fragments.discover;
import android.graphics.Rect;
import android.graphics.drawable.Drawable;
import android.os.Bundle;
import android.text.TextUtils;
@@ -12,32 +13,43 @@ import org.joinmastodon.android.R;
import org.joinmastodon.android.api.requests.trends.GetTrendingLinks;
import org.joinmastodon.android.fragments.ScrollableToTop;
import org.joinmastodon.android.model.Card;
import org.joinmastodon.android.model.viewmodel.CardViewModel;
import org.joinmastodon.android.ui.DividerItemDecoration;
import org.joinmastodon.android.ui.OutlineProviders;
import org.joinmastodon.android.ui.drawables.BlurhashCrossfadeDrawable;
import org.joinmastodon.android.ui.utils.DiscoverInfoBannerHelper;
import org.joinmastodon.android.ui.utils.UiUtils;
import java.util.ArrayList;
import java.util.Collections;
import java.util.List;
import java.util.stream.Collectors;
import androidx.annotation.NonNull;
import androidx.recyclerview.widget.LinearLayoutManager;
import androidx.recyclerview.widget.RecyclerView;
import me.grishka.appkit.api.SimpleCallback;
import me.grishka.appkit.fragments.BaseRecyclerFragment;
import me.grishka.appkit.imageloader.ImageLoaderRecyclerAdapter;
import me.grishka.appkit.imageloader.ImageLoaderViewHolder;
import me.grishka.appkit.imageloader.ListImageLoaderAdapter;
import me.grishka.appkit.imageloader.ListImageLoaderWrapper;
import me.grishka.appkit.imageloader.RecyclerViewDelegate;
import me.grishka.appkit.imageloader.requests.ImageLoaderRequest;
import me.grishka.appkit.imageloader.requests.UrlImageLoaderRequest;
import me.grishka.appkit.utils.BindableViewHolder;
import me.grishka.appkit.utils.MergeRecyclerAdapter;
import me.grishka.appkit.utils.SingleViewRecyclerAdapter;
import me.grishka.appkit.utils.V;
import me.grishka.appkit.views.UsableRecyclerView;
public class DiscoverNewsFragment extends BaseRecyclerFragment<Card> implements ScrollableToTop{
public class DiscoverNewsFragment extends BaseRecyclerFragment<CardViewModel> implements ScrollableToTop{
private String accountID;
private List<ImageLoaderRequest> imageRequests=Collections.emptyList();
private DiscoverInfoBannerHelper bannerHelper=new DiscoverInfoBannerHelper(DiscoverInfoBannerHelper.BannerType.TRENDING_LINKS);
private DiscoverInfoBannerHelper bannerHelper;
private MergeRecyclerAdapter mergeAdapter;
private UsableRecyclerView cardsList;
private ArrayList<CardViewModel> top3=new ArrayList<>();
private CardLinksAdapter cardsAdapter;
public DiscoverNewsFragment(){
super(10);
@@ -47,6 +59,7 @@ public class DiscoverNewsFragment extends BaseRecyclerFragment<Card> implements
public void onCreate(Bundle savedInstanceState){
super.onCreate(savedInstanceState);
accountID=getArguments().getString("account");
bannerHelper=new DiscoverInfoBannerHelper(DiscoverInfoBannerHelper.BannerType.TRENDING_LINKS, accountID);
}
@Override
@@ -55,10 +68,14 @@ public class DiscoverNewsFragment extends BaseRecyclerFragment<Card> implements
.setCallback(new SimpleCallback<>(this){
@Override
public void onSuccess(List<Card> result){
imageRequests=result.stream()
.map(card->TextUtils.isEmpty(card.image) ? null : new UrlImageLoaderRequest(card.image, V.dp(150), V.dp(150)))
.collect(Collectors.toList());
onDataLoaded(result, false);
top3.clear();
top3.addAll(result.subList(0, Math.min(3, result.size())).stream().map(card->new CardViewModel(card, 280, 140)).collect(Collectors.toList()));
cardsAdapter.notifyDataSetChanged();
onDataLoaded(result.subList(top3.size(), result.size()).stream()
.map(card->new CardViewModel(card, 56, 56))
.collect(Collectors.toList()), false);
bannerHelper.onBannerBecameVisible();
}
})
.exec(accountID);
@@ -66,14 +83,27 @@ public class DiscoverNewsFragment extends BaseRecyclerFragment<Card> implements
@Override
protected RecyclerView.Adapter getAdapter(){
return new LinksAdapter();
}
cardsList=new UsableRecyclerView(getActivity());
cardsList.setLayoutManager(new LinearLayoutManager(getActivity(), LinearLayoutManager.HORIZONTAL, false));
ListImageLoaderWrapper cardsImageLoader=new ListImageLoaderWrapper(getActivity(), cardsList, new RecyclerViewDelegate(cardsList), this);
cardsList.setAdapter(cardsAdapter=new CardLinksAdapter(cardsImageLoader, top3));
cardsList.setLayoutParams(new ViewGroup.LayoutParams(ViewGroup.LayoutParams.MATCH_PARENT, V.dp(256)));
cardsList.setPadding(V.dp(16), V.dp(8), 0, 0);
cardsList.setClipToPadding(false);
cardsList.addItemDecoration(new RecyclerView.ItemDecoration(){
@Override
public void getItemOffsets(@NonNull Rect outRect, @NonNull View view, @NonNull RecyclerView parent, @NonNull RecyclerView.State state){
outRect.right=V.dp(16);
}
});
cardsList.setSelector(R.drawable.bg_rect_12dp_ripple);
cardsList.setDrawSelectorOnTop(true);
@Override
public void onViewCreated(View view, Bundle savedInstanceState){
super.onViewCreated(view, savedInstanceState);
list.addItemDecoration(new DividerItemDecoration(getActivity(), R.attr.colorPollVoted, 1, 0, 0));
bannerHelper.maybeAddBanner(contentWrap);
mergeAdapter=new MergeRecyclerAdapter();
bannerHelper.maybeAddBanner(list, mergeAdapter);
mergeAdapter.addAdapter(new SingleViewRecyclerAdapter(cardsList));
mergeAdapter.addAdapter(new LinksAdapter(imgLoader, data));
return mergeAdapter;
}
@Override
@@ -81,14 +111,17 @@ public class DiscoverNewsFragment extends BaseRecyclerFragment<Card> implements
smoothScrollRecyclerViewToTop(list);
}
private class LinksAdapter extends UsableRecyclerView.Adapter<LinkViewHolder> implements ImageLoaderRecyclerAdapter{
public LinksAdapter(){
private class LinksAdapter extends UsableRecyclerView.Adapter<BaseLinkViewHolder> implements ImageLoaderRecyclerAdapter{
private final List<CardViewModel> data;
public LinksAdapter(ListImageLoaderWrapper imgLoader, List<CardViewModel> data){
super(imgLoader);
this.data=data;
}
@NonNull
@Override
public LinkViewHolder onCreateViewHolder(@NonNull ViewGroup parent, int viewType){
public BaseLinkViewHolder onCreateViewHolder(@NonNull ViewGroup parent, int viewType){
return new LinkViewHolder();
}
@@ -98,46 +131,51 @@ public class DiscoverNewsFragment extends BaseRecyclerFragment<Card> implements
}
@Override
public void onBindViewHolder(LinkViewHolder holder, int position){
holder.bind(data.get(position));
public void onBindViewHolder(BaseLinkViewHolder holder, int position){
holder.bind(data.get(position).card);
super.onBindViewHolder(holder, position);
}
@Override
public int getImageCountForItem(int position){
return imageRequests.get(position)==null ? 0 : 1;
return data.get(position).imageRequest==null ? 0 : 1;
}
@Override
public ImageLoaderRequest getImageRequest(int position, int image){
return imageRequests.get(position);
return data.get(position).imageRequest;
}
}
private class LinkViewHolder extends BindableViewHolder<Card> implements UsableRecyclerView.Clickable, ImageLoaderViewHolder{
private final TextView name, title, subtitle;
private final ImageView photo;
private class CardLinksAdapter extends LinksAdapter{
public CardLinksAdapter(ListImageLoaderWrapper imgLoader, List<CardViewModel> data){
super(imgLoader, data);
}
@NonNull
@Override
public BaseLinkViewHolder onCreateViewHolder(@NonNull ViewGroup parent, int viewType){
return new LinkCardViewHolder();
}
}
private class BaseLinkViewHolder extends BindableViewHolder<Card> implements UsableRecyclerView.Clickable, ImageLoaderViewHolder{
protected final TextView name, title;
protected final ImageView photo;
private BlurhashCrossfadeDrawable crossfadeDrawable=new BlurhashCrossfadeDrawable();
private boolean didClear;
public LinkViewHolder(){
super(getActivity(), R.layout.item_trending_link, list);
public BaseLinkViewHolder(int layout){
super(getActivity(), layout, list);
name=findViewById(R.id.name);
title=findViewById(R.id.title);
subtitle=findViewById(R.id.subtitle);
photo=findViewById(R.id.photo);
photo.setOutlineProvider(OutlineProviders.roundedRect(2));
photo.setClipToOutline(true);
}
@Override
public void onBind(Card item){
name.setText(item.providerName);
title.setText(item.title);
int num=item.history.get(0).uses;
if(item.history.size()>1)
num+=item.history.get(1).uses;
subtitle.setText(getResources().getQuantityString(R.plurals.discussed_x_times, num, num));
crossfadeDrawable.setSize(item.width, item.height);
crossfadeDrawable.setBlurhashDrawable(item.blurhashPlaceholder);
crossfadeDrawable.setCrossfadeAlpha(0f);
@@ -164,4 +202,20 @@ public class DiscoverNewsFragment extends BaseRecyclerFragment<Card> implements
UiUtils.launchWebBrowser(getActivity(), item.url);
}
}
private class LinkViewHolder extends BaseLinkViewHolder{
public LinkViewHolder(){
super(R.layout.item_trending_link);
photo.setOutlineProvider(OutlineProviders.roundedRect(12));
photo.setClipToOutline(true);
}
}
private class LinkCardViewHolder extends BaseLinkViewHolder{
public LinkCardViewHolder(){
super(R.layout.item_trending_link_card);
itemView.setOutlineProvider(OutlineProviders.roundedRect(12));
itemView.setClipToOutline(true);
}
}
}

View File

@@ -1,7 +1,6 @@
package org.joinmastodon.android.fragments.discover;
import android.os.Bundle;
import android.view.View;
import org.joinmastodon.android.api.requests.trends.GetTrendingStatuses;
import org.joinmastodon.android.fragments.StatusListFragment;
@@ -10,10 +9,18 @@ import org.joinmastodon.android.ui.utils.DiscoverInfoBannerHelper;
import java.util.List;
import androidx.recyclerview.widget.RecyclerView;
import me.grishka.appkit.api.SimpleCallback;
import me.grishka.appkit.utils.MergeRecyclerAdapter;
public class DiscoverPostsFragment extends StatusListFragment{
private DiscoverInfoBannerHelper bannerHelper=new DiscoverInfoBannerHelper(DiscoverInfoBannerHelper.BannerType.TRENDING_POSTS);
private DiscoverInfoBannerHelper bannerHelper;
@Override
public void onCreate(Bundle savedInstanceState){
super.onCreate(savedInstanceState);
bannerHelper=new DiscoverInfoBannerHelper(DiscoverInfoBannerHelper.BannerType.TRENDING_POSTS, accountID);
}
@Override
protected void doLoadData(int offset, int count){
@@ -22,13 +29,16 @@ public class DiscoverPostsFragment extends StatusListFragment{
@Override
public void onSuccess(List<Status> result){
onDataLoaded(result, !result.isEmpty());
bannerHelper.onBannerBecameVisible();
}
}).exec(accountID);
}
@Override
public void onViewCreated(View view, Bundle savedInstanceState){
super.onViewCreated(view, savedInstanceState);
bannerHelper.maybeAddBanner(contentWrap);
protected RecyclerView.Adapter getAdapter(){
MergeRecyclerAdapter adapter=new MergeRecyclerAdapter();
bannerHelper.maybeAddBanner(list, adapter);
adapter.addAdapter(super.getAdapter());
return adapter;
}
}

View File

@@ -12,12 +12,21 @@ import org.joinmastodon.android.ui.utils.DiscoverInfoBannerHelper;
import java.util.List;
import androidx.recyclerview.widget.RecyclerView;
import me.grishka.appkit.api.SimpleCallback;
import me.grishka.appkit.utils.MergeRecyclerAdapter;
public class LocalTimelineFragment extends StatusListFragment{
private DiscoverInfoBannerHelper bannerHelper=new DiscoverInfoBannerHelper(DiscoverInfoBannerHelper.BannerType.LOCAL_TIMELINE);
private DiscoverInfoBannerHelper bannerHelper;
private String maxID;
@Override
public void onCreate(Bundle savedInstanceState){
super.onCreate(savedInstanceState);
bannerHelper=new DiscoverInfoBannerHelper(DiscoverInfoBannerHelper.BannerType.LOCAL_TIMELINE, accountID);
}
@Override
protected void doLoadData(int offset, int count){
currentRequest=new GetPublicTimeline(true, false, refreshing ? null : maxID, count)
@@ -29,14 +38,17 @@ public class LocalTimelineFragment extends StatusListFragment{
boolean empty=result.isEmpty();
AccountSessionManager.get(accountID).filterStatuses(result, FilterContext.PUBLIC);
onDataLoaded(result, !empty);
bannerHelper.onBannerBecameVisible();
}
})
.exec(accountID);
}
@Override
public void onViewCreated(View view, Bundle savedInstanceState){
super.onViewCreated(view, savedInstanceState);
bannerHelper.maybeAddBanner(contentWrap);
protected RecyclerView.Adapter getAdapter(){
MergeRecyclerAdapter adapter=new MergeRecyclerAdapter();
bannerHelper.maybeAddBanner(list, adapter);
adapter.addAdapter(super.getAdapter());
return adapter;
}
}

View File

@@ -3,7 +3,6 @@ package org.joinmastodon.android.fragments.discover;
import android.app.Activity;
import android.os.Build;
import android.os.Bundle;
import android.text.TextUtils;
import android.view.View;
import android.view.inputmethod.InputMethodManager;
@@ -22,7 +21,6 @@ import org.joinmastodon.android.ui.displayitems.AccountStatusDisplayItem;
import org.joinmastodon.android.ui.displayitems.HashtagStatusDisplayItem;
import org.joinmastodon.android.ui.displayitems.StatusDisplayItem;
import org.joinmastodon.android.ui.tabs.TabLayout;
import org.joinmastodon.android.ui.utils.HideableSingleViewRecyclerAdapter;
import org.joinmastodon.android.ui.utils.UiUtils;
import org.parceler.Parcels;
@@ -33,11 +31,9 @@ import java.util.List;
import java.util.Objects;
import java.util.stream.Collectors;
import androidx.recyclerview.widget.RecyclerView;
import me.grishka.appkit.Nav;
import me.grishka.appkit.api.Callback;
import me.grishka.appkit.api.ErrorResponse;
import me.grishka.appkit.utils.MergeRecyclerAdapter;
import me.grishka.appkit.utils.V;
public class SearchFragment extends BaseStatusListFragment<SearchResult>{
@@ -45,12 +41,8 @@ public class SearchFragment extends BaseStatusListFragment<SearchResult>{
private List<StatusDisplayItem> prevDisplayItems;
private EnumSet<SearchResult.Type> currentFilter=EnumSet.allOf(SearchResult.Type.class);
private List<SearchResult> unfilteredResults=Collections.emptyList();
private HideableSingleViewRecyclerAdapter headerAdapter;
private ProgressVisibilityListener progressVisibilityListener;
private InputMethodManager imm;
private TabLayout tabLayout;
public SearchFragment(){
setLayout(R.layout.fragment_search);
}
@@ -60,6 +52,7 @@ public class SearchFragment extends BaseStatusListFragment<SearchResult>{
super.onCreate(savedInstanceState);
if(Build.VERSION.SDK_INT>=Build.VERSION_CODES.N)
setRetainInstance(true);
setEmptyText(R.string.no_search_results);
loadData();
}
@@ -118,51 +111,48 @@ public class SearchFragment extends BaseStatusListFragment<SearchResult>{
@Override
protected void doLoadData(int offset, int count){
if(isInRecentMode()){
AccountSessionManager.getInstance().getAccount(accountID).getCacheController().getRecentSearches(sr->{
if(getActivity()==null)
return;
unfilteredResults=sr;
prevDisplayItems=new ArrayList<>(displayItems);
onDataLoaded(sr, false);
});
GetSearchResults.Type type;
if(currentFilter.size()==1){
type=switch(currentFilter.iterator().next()){
case ACCOUNT -> GetSearchResults.Type.ACCOUNTS;
case HASHTAG -> GetSearchResults.Type.HASHTAGS;
case STATUS -> GetSearchResults.Type.STATUSES;
};
}else{
progressVisibilityListener.onProgressVisibilityChanged(true);
currentRequest=new GetSearchResults(currentQuery, null, true)
.setCallback(new Callback<>(){
@Override
public void onSuccess(SearchResults result){
ArrayList<SearchResult> results=new ArrayList<>();
if(result.accounts!=null){
for(Account acc:result.accounts)
results.add(new SearchResult(acc));
}
if(result.hashtags!=null){
for(Hashtag tag:result.hashtags)
results.add(new SearchResult(tag));
}
if(result.statuses!=null){
for(Status status:result.statuses)
results.add(new SearchResult(status));
}
prevDisplayItems=new ArrayList<>(displayItems);
unfilteredResults=results;
onDataLoaded(filterSearchResults(results), false);
}
@Override
public void onError(ErrorResponse error){
currentRequest=null;
Activity a=getActivity();
if(a==null)
return;
error.showToast(a);
if(progressVisibilityListener!=null)
progressVisibilityListener.onProgressVisibilityChanged(false);
}
})
.exec(accountID);
type=null;
}
currentRequest=new GetSearchResults(currentQuery, type, true)
.setCallback(new Callback<>(){
@Override
public void onSuccess(SearchResults result){
ArrayList<SearchResult> results=new ArrayList<>();
if(result.accounts!=null){
for(Account acc:result.accounts)
results.add(new SearchResult(acc));
}
if(result.hashtags!=null){
for(Hashtag tag:result.hashtags)
results.add(new SearchResult(tag));
}
if(result.statuses!=null){
for(Status status:result.statuses)
results.add(new SearchResult(status));
}
prevDisplayItems=new ArrayList<>(displayItems);
unfilteredResults=results;
onDataLoaded(filterSearchResults(results), false);
}
@Override
public void onError(ErrorResponse error){
currentRequest=null;
Activity a=getActivity();
if(a==null)
return;
error.showToast(a);
}
})
.exec(accountID);
}
@Override
@@ -172,86 +162,30 @@ public class SearchFragment extends BaseStatusListFragment<SearchResult>{
return;
}
UiUtils.updateList(prevDisplayItems, displayItems, list, adapter, (i1, i2)->i1.parentID.equals(i2.parentID) && i1.index==i2.index && i1.getType()==i2.getType());
boolean recent=isInRecentMode();
if(recent!=headerAdapter.isVisible())
headerAdapter.setVisible(recent);
imgLoader.forceUpdateImages();
prevDisplayItems=null;
}
@Override
protected void onDataLoaded(List<SearchResult> d, boolean more){
super.onDataLoaded(d, more);
if(progressVisibilityListener!=null)
progressVisibilityListener.onProgressVisibilityChanged(false);
}
@Override
public void onViewCreated(View view, Bundle savedInstanceState){
super.onViewCreated(view, savedInstanceState);
tabLayout=view.findViewById(R.id.tabbar);
tabLayout.setTabTextSize(V.dp(16));
tabLayout.setTabTextColors(UiUtils.getThemeColor(getActivity(), R.attr.colorTabInactive), UiUtils.getThemeColor(getActivity(), android.R.attr.textColorPrimary));
tabLayout.addTab(tabLayout.newTab().setText(R.string.search_all));
tabLayout.addTab(tabLayout.newTab().setText(R.string.search_people));
tabLayout.addTab(tabLayout.newTab().setText(R.string.hashtags));
tabLayout.addTab(tabLayout.newTab().setText(R.string.posts));
for(int i=0;i<tabLayout.getTabCount();i++){
tabLayout.getTabAt(i).view.textView.setAllCaps(true);
}
tabLayout.addOnTabSelectedListener(new TabLayout.OnTabSelectedListener(){
@Override
public void onTabSelected(TabLayout.Tab tab){
setFilter(switch(tab.getPosition()){
case 0 -> EnumSet.allOf(SearchResult.Type.class);
case 1 -> EnumSet.of(SearchResult.Type.ACCOUNT);
case 2 -> EnumSet.of(SearchResult.Type.HASHTAG);
case 3 -> EnumSet.of(SearchResult.Type.STATUS);
default -> throw new IllegalStateException("Unexpected value: "+tab.getPosition());
});
}
@Override
public void onTabUnselected(TabLayout.Tab tab){
}
@Override
public void onTabReselected(TabLayout.Tab tab){
scrollToTop();
}
});
}
@Override
protected RecyclerView.Adapter getAdapter(){
View header=getActivity().getLayoutInflater().inflate(R.layout.item_recent_searches_header, list, false);
header.findViewById(R.id.clear).setOnClickListener(this::onClearRecentClick);
MergeRecyclerAdapter adapter=new MergeRecyclerAdapter();
adapter.addAdapter(headerAdapter=new HideableSingleViewRecyclerAdapter(header));
adapter.addAdapter(super.getAdapter());
headerAdapter.setVisible(isInRecentMode());
return adapter;
}
public void setQuery(String q){
if(Objects.equals(q, currentQuery) || q.isBlank())
public void setQuery(String q, SearchResult.Type filter){
if(q.isBlank())
return;
if(currentRequest!=null){
currentRequest.cancel();
currentRequest=null;
}
currentQuery=q;
if(filter==null)
currentFilter=EnumSet.allOf(SearchResult.Type.class);
else
currentFilter=EnumSet.of(filter);
refreshing=true;
doLoadData(0, 0);
loadData();
}
private void setFilter(EnumSet<SearchResult.Type> filter){
if(filter.equals(currentFilter))
return;
currentFilter=filter;
if(isInRecentMode())
return;
// This can be optimized by not rebuilding display items every time filter is changed, but I'm too lazy
prevDisplayItems=new ArrayList<>(displayItems);
refreshing=true;
@@ -273,23 +207,6 @@ public class SearchFragment extends BaseStatusListFragment<SearchResult>{
return null;
}
private void onClearRecentClick(View v){
AccountSessionManager.getInstance().getAccount(accountID).getCacheController().clearRecentSearches();
if(isInRecentMode()){
prevDisplayItems=new ArrayList<>(displayItems);
refreshing=true;
onDataLoaded(unfilteredResults=Collections.emptyList(), false);
}
}
private boolean isInRecentMode(){
return TextUtils.isEmpty(currentQuery);
}
public void setProgressVisibilityListener(ProgressVisibilityListener progressVisibilityListener){
this.progressVisibilityListener=progressVisibilityListener;
}
@Override
public void onScrollStarted(){
super.onScrollStarted();

View File

@@ -0,0 +1,554 @@
package org.joinmastodon.android.fragments.discover;
import android.animation.Animator;
import android.animation.AnimatorListenerAdapter;
import android.animation.AnimatorSet;
import android.animation.ObjectAnimator;
import android.app.Fragment;
import android.graphics.Outline;
import android.graphics.Rect;
import android.graphics.drawable.Drawable;
import android.graphics.drawable.LayerDrawable;
import android.net.Uri;
import android.os.Build;
import android.os.Bundle;
import android.text.TextUtils;
import android.view.RoundedCorner;
import android.view.View;
import android.view.ViewGroup;
import android.view.ViewOutlineProvider;
import android.view.WindowInsets;
import android.view.animation.AnimationUtils;
import android.view.inputmethod.InputMethodManager;
import android.widget.Button;
import android.widget.TextView;
import android.widget.Toolbar;
import org.joinmastodon.android.MainActivity;
import org.joinmastodon.android.R;
import org.joinmastodon.android.api.requests.search.GetSearchResults;
import org.joinmastodon.android.api.session.AccountSessionManager;
import org.joinmastodon.android.fragments.MastodonRecyclerFragment;
import org.joinmastodon.android.model.Relationship;
import org.joinmastodon.android.model.SearchResult;
import org.joinmastodon.android.model.SearchResults;
import org.joinmastodon.android.model.viewmodel.ListItem;
import org.joinmastodon.android.model.viewmodel.SearchResultViewModel;
import org.joinmastodon.android.ui.DividerItemDecoration;
import org.joinmastodon.android.ui.SearchViewHelper;
import org.joinmastodon.android.ui.adapters.GenericListItemsAdapter;
import org.joinmastodon.android.ui.utils.HideableSingleViewRecyclerAdapter;
import org.joinmastodon.android.ui.utils.UiUtils;
import org.joinmastodon.android.ui.viewholders.AccountViewHolder;
import org.joinmastodon.android.ui.viewholders.SimpleListItemViewHolder;
import java.util.ArrayList;
import java.util.Collections;
import java.util.HashMap;
import java.util.List;
import java.util.function.Function;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
import java.util.stream.Collectors;
import java.util.stream.Stream;
import androidx.annotation.Keep;
import androidx.annotation.NonNull;
import androidx.recyclerview.widget.RecyclerView;
import me.grishka.appkit.Nav;
import me.grishka.appkit.api.SimpleCallback;
import me.grishka.appkit.fragments.CustomTransitionsFragment;
import me.grishka.appkit.fragments.OnBackPressedListener;
import me.grishka.appkit.imageloader.ImageLoaderRecyclerAdapter;
import me.grishka.appkit.imageloader.requests.ImageLoaderRequest;
import me.grishka.appkit.utils.MergeRecyclerAdapter;
import me.grishka.appkit.utils.V;
import me.grishka.appkit.views.UsableRecyclerView;
public class SearchQueryFragment extends MastodonRecyclerFragment<SearchResultViewModel> implements CustomTransitionsFragment, OnBackPressedListener{
private static final Pattern HASHTAG_REGEX=Pattern.compile("^(\\w*[a-zA-Z·]\\w*)$", Pattern.CASE_INSENSITIVE);
private static final Pattern USERNAME_REGEX=Pattern.compile("^@?([a-z0-9_-]+)(@[^\\s]+)?$", Pattern.CASE_INSENSITIVE);
private MergeRecyclerAdapter mergeAdapter=new MergeRecyclerAdapter();
private HideableSingleViewRecyclerAdapter recentsHeader;
private ListItem<Void> openUrlItem, goToHashtagItem, goToAccountItem, goToStatusSearchItem, goToAccountSearchItem;
private ArrayList<ListItem<Void>> topOptions=new ArrayList<>();
private GenericListItemsAdapter<Void> topOptionsAdapter;
private String accountID;
private SearchViewHelper searchViewHelper;
private String currentQuery;
private LayerDrawable navigationIcon;
private Drawable searchIcon, backIcon;
public SearchQueryFragment(){
super(20);
}
@Override
public void onCreate(Bundle savedInstanceState){
super.onCreate(savedInstanceState);
accountID=getArguments().getString("account");
setRefreshEnabled(false);
setEmptyText("");
openUrlItem=new ListItem<>(R.string.search_open_url, 0, R.drawable.ic_link_24px, this::onOpenURLClick);
goToHashtagItem=new ListItem<>("", null, R.drawable.ic_tag_24px, this::onGoToHashtagClick);
goToAccountItem=new ListItem<>("", null, R.drawable.ic_person_24px, this::onGoToAccountClick);
goToStatusSearchItem=new ListItem<>("", null, R.drawable.ic_search_24px, this::onGoToStatusSearchClick);
goToAccountSearchItem=new ListItem<>("", null, R.drawable.ic_group_24px, this::onGoToAccountSearchClick);
currentQuery=getArguments().getString("query");
dataLoaded();
doLoadData(0, 0);
}
@Override
protected void doLoadData(int offset, int count){
if(isInRecentMode()){
AccountSessionManager.getInstance().getAccount(accountID).getCacheController().getRecentSearches(results->{
if(getActivity()==null)
return;
onDataLoaded(results.stream().map(sr->{
SearchResultViewModel vm=new SearchResultViewModel(sr, accountID, true);
if(sr.type==SearchResult.Type.HASHTAG){
vm.hashtagItem.onClick=()->openHashtag(sr);
}
return vm;
}).collect(Collectors.toList()), false);
recentsHeader.setVisible(!data.isEmpty());
});
}else{
currentRequest=new GetSearchResults(currentQuery, null, false)
.limit(2)
.setCallback(new SimpleCallback<>(this){
@Override
public void onSuccess(SearchResults result){
onDataLoaded(Stream.of(result.hashtags.stream().map(SearchResult::new), result.accounts.stream().map(SearchResult::new))
.flatMap(Function.identity())
.map(sr->{
SearchResultViewModel vm=new SearchResultViewModel(sr, accountID, false);
if(sr.type==SearchResult.Type.HASHTAG){
vm.hashtagItem.onClick=()->openHashtag(sr);
}
return vm;
})
.collect(Collectors.toList()), false);
recentsHeader.setVisible(false);
}
})
.exec(accountID);
}
}
@Override
protected RecyclerView.Adapter<?> getAdapter(){
View header=getActivity().getLayoutInflater().inflate(R.layout.display_item_section_header, list, false);
TextView title=header.findViewById(R.id.title);
Button action=header.findViewById(R.id.action_btn);
title.setText(R.string.recent_searches);
action.setText(R.string.clear_all);
action.setOnClickListener(v->onClearRecentClick());
recentsHeader=new HideableSingleViewRecyclerAdapter(header);
mergeAdapter.addAdapter(recentsHeader);
mergeAdapter.addAdapter(topOptionsAdapter=new GenericListItemsAdapter<>(topOptions));
mergeAdapter.addAdapter(new SearchResultsAdapter());
return mergeAdapter;
}
@Override
public void onViewCreated(View view, Bundle savedInstanceState){
searchViewHelper=new SearchViewHelper(getActivity(), getToolbarContext(), getString(R.string.search_mastodon));
searchViewHelper.setListeners(this::onQueryChanged, this::onQueryChangedNoDebounce);
searchViewHelper.addDivider(contentView);
searchViewHelper.setEnterCallback(this::onSearchViewEnter);
navigationIcon=new LayerDrawable(new Drawable[]{
searchIcon=getToolbarContext().getResources().getDrawable(R.drawable.ic_search_24px, getToolbarContext().getTheme()).mutate(),
backIcon=getToolbarContext().getResources().getDrawable(R.drawable.ic_arrow_back, getToolbarContext().getTheme()).mutate()
}){
@Override
public Drawable mutate(){
return this;
}
};
super.onViewCreated(view, savedInstanceState);
view.setBackgroundResource(R.drawable.bg_m3_surface3);
int color=UiUtils.alphaBlendThemeColors(getActivity(), R.attr.colorM3Surface, R.attr.colorM3Primary, 0.11f);
setStatusBarColor(color);
setNavigationBarColor(color);
if(currentQuery!=null){
searchViewHelper.setQuery(currentQuery);
searchIcon.setAlpha(0);
}
list.addItemDecoration(new DividerItemDecoration(getActivity(), R.attr.colorM3OutlineVariant, 1, 0, 0, vh->!isInRecentMode() && vh.getAbsoluteAdapterPosition()==topOptions.size()-1));
}
@Override
protected void onUpdateToolbar(){
super.onUpdateToolbar();
((ViewGroup.MarginLayoutParams)getToolbar().getLayoutParams()).topMargin=V.dp(8);
searchViewHelper.install(getToolbar());
}
@Override
protected boolean wantsElevationOnScrollEffect(){
return false;
}
private void onQueryChanged(String q){
currentQuery=q;
if(currentRequest!=null){
currentRequest.cancel();
currentRequest=null;
}
refreshing=true;
doLoadData(0, 0);
}
private void onQueryChangedNoDebounce(String q){
updateTopOptions(q);
if(!TextUtils.isEmpty(q)){
recentsHeader.setVisible(false);
}
data.clear();
mergeAdapter.notifyDataSetChanged();
}
private void updateTopOptions(String q){
topOptions.clear();
// https://github.com/mastodon/mastodon/blob/a985d587e13494b78ef2879e4d97f78a2df693db/app/javascript/mastodon/features/compose/components/search.jsx#L233
String trimmedValue=q.trim();
if(trimmedValue.length()>0){
boolean couldBeURL=trimmedValue.startsWith("https://") && !trimmedValue.contains(" ");
if(couldBeURL){
topOptions.add(openUrlItem);
}
boolean couldBeHashtag=(trimmedValue.startsWith("#") && trimmedValue.length()>1 && !trimmedValue.contains(" ")) || HASHTAG_REGEX.matcher(trimmedValue).find();
if(couldBeHashtag){
String tag=trimmedValue.startsWith("#") ? trimmedValue.substring(1) : trimmedValue;
goToHashtagItem.title=getString(R.string.posts_matching_hashtag, "#"+tag);
topOptions.add(goToHashtagItem);
}
Matcher usernameMatcher=USERNAME_REGEX.matcher(trimmedValue);
if(usernameMatcher.find()){
String username="@"+usernameMatcher.group(1);
String atDomain=usernameMatcher.group(2);
if(atDomain==null){
username+="@"+AccountSessionManager.get(accountID).domain;
}
goToAccountItem.title=getString(R.string.search_go_to_account, username);
topOptions.add(goToAccountItem);
}
goToStatusSearchItem.title=getString(R.string.posts_matching_string, trimmedValue);
topOptions.add(goToStatusSearchItem);
goToAccountSearchItem.title=getString(R.string.accounts_matching_string, trimmedValue);
topOptions.add(goToAccountSearchItem);
}
topOptionsAdapter.notifyDataSetChanged();
}
@Override
public Animator onCreateEnterTransition(View prev, View container){
return createTransition(prev, container, true);
}
@Override
public Animator onCreateExitTransition(View prev, View container){
return createTransition(prev, container, false);
}
@Override
public boolean wantsCustomNavigationIcon(){
return true;
}
@Override
protected Drawable getNavigationIconDrawable(){
return navigationIcon;
}
@Override
protected void onShown(){
super.onShown();
getActivity().getSystemService(InputMethodManager.class).showSoftInput(getActivity().getCurrentFocus(), 0);
}
@Override
protected void onHidden(){
super.onHidden();
getActivity().getSystemService(InputMethodManager.class).hideSoftInputFromWindow(getActivity().getWindow().getDecorView().getWindowToken(), 0);
}
private Animator createTransition(View prev, View container, boolean enter){
int[] loc={0, 0};
View searchBtn=prev.findViewById(R.id.search_wrap);
searchBtn.getLocationInWindow(loc);
int btnLeft=loc[0], btnTop=loc[1];
container.getLocationInWindow(loc);
int offX=btnLeft-loc[0], offY=btnTop-loc[1];
float screenRadius;
if(Build.VERSION.SDK_INT>=Build.VERSION_CODES.S){
WindowInsets insets=container.getRootWindowInsets();
screenRadius=Math.min(
Math.min(insets.getRoundedCorner(RoundedCorner.POSITION_TOP_LEFT).getRadius(), insets.getRoundedCorner(RoundedCorner.POSITION_TOP_RIGHT).getRadius()),
Math.min(insets.getRoundedCorner(RoundedCorner.POSITION_BOTTOM_LEFT).getRadius(), insets.getRoundedCorner(RoundedCorner.POSITION_BOTTOM_RIGHT).getRadius())
);
}else{
screenRadius=0;
}
float buttonRadius=V.dp(26);
Rect buttonBounds=new Rect(offX, offY, offX+searchBtn.getWidth(), offY+searchBtn.getHeight());
Rect containerBounds=new Rect(0, 0, container.getWidth(), container.getHeight());
AnimatableOutlineProvider outlineProvider=new AnimatableOutlineProvider(enter ? buttonBounds : containerBounds, enter ? containerBounds : buttonBounds, enter ? buttonRadius : screenRadius);
container.setOutlineProvider(outlineProvider);
container.setClipToOutline(true);
AnimatorSet set=new AnimatorSet();
ObjectAnimator boundsAnim;
Toolbar toolbar=getToolbar();
float toolbarTX=offX-toolbar.getX();
float toolbarTY=offY-toolbar.getY()+(searchBtn.getHeight()-toolbar.getHeight())/2f;
ArrayList<Animator> anims=new ArrayList<>();
anims.add(boundsAnim=ObjectAnimator.ofFloat(outlineProvider, "boundsFraction", 0f, 1f));
anims.add(ObjectAnimator.ofFloat(outlineProvider, "radius", enter ? buttonRadius : screenRadius, enter ? screenRadius : buttonRadius));
anims.add(ObjectAnimator.ofFloat(toolbar, View.TRANSLATION_X, enter ? toolbarTX : 0, enter ? 0 : toolbarTX));
anims.add(ObjectAnimator.ofFloat(toolbar, View.TRANSLATION_Y, enter ? toolbarTY : 0, enter ? 0 : toolbarTY));
anims.add(ObjectAnimator.ofFloat(searchViewHelper.getSearchLayout(), View.TRANSLATION_X, enter ? V.dp(-4) : 0, enter ? 0 : V.dp(-4)));
anims.add(ObjectAnimator.ofFloat(searchViewHelper.getDivider(), View.ALPHA, enter ? 0 : 1, enter ? 1 : 0));
View parentContent=prev.findViewById(R.id.discover_content);
View parentContentParent=(View) parentContent.getParent();
parentContentParent.setBackgroundColor(UiUtils.getThemeColor(getActivity(), R.attr.colorM3Surface));
if(enter){
anims.add(ObjectAnimator.ofFloat(contentWrap, View.TRANSLATION_Y, V.dp(-16), 0));
}else{
}
anims.add(ObjectAnimator.ofFloat(contentWrap, View.ALPHA, enter ? 0 : 1, enter ? 1 : 0));
for(Animator anim:anims){
anim.setDuration(enter ? 700 : 300);
}
if(TextUtils.isEmpty(currentQuery)){
anims.add(ObjectAnimator.ofInt(searchIcon, "alpha", enter ? 255 : 0, enter ? 0 : 255).setDuration(200));
anims.add(ObjectAnimator.ofInt(backIcon, "alpha", enter ? 0 : 255, enter ? 255 : 0).setDuration(200));
}
ObjectAnimator parentContentFade;
anims.add(parentContentFade=ObjectAnimator.ofFloat(parentContent, View.ALPHA, enter ? 1 : 0, enter ? 0 : 1).setDuration(enter ? 350 : 250));
if(!enter){
parentContentFade.setStartDelay(50);
ObjectAnimator parentContentTY;
anims.add(parentContentTY=ObjectAnimator.ofFloat(parentContent, View.TRANSLATION_Y, V.dp(16), 0).setDuration(250));
parentContentTY.setStartDelay(50);
}
set.playTogether(anims);
set.setInterpolator(AnimationUtils.loadInterpolator(getActivity(), R.interpolator.m3_sys_motion_easing_emphasized_decelerate));
set.addListener(new AnimatorListenerAdapter(){
@Override
public void onAnimationEnd(Animator animation){
container.setOutlineProvider(null);
container.setClipToOutline(false);
parentContentParent.setBackground(null);
}
});
boundsAnim.addUpdateListener(animation->{
container.invalidateOutline();
navigationIcon.invalidateSelf();
});
return set;
}
private void openHashtag(SearchResult res){
UiUtils.openHashtagTimeline(getActivity(), accountID, res.hashtag.name);
AccountSessionManager.getInstance().getAccount(accountID).getCacheController().putRecentSearch(res);
}
private boolean isInRecentMode(){
return TextUtils.isEmpty(currentQuery);
}
private void onSearchViewEnter(){
deliverResult(currentQuery, null);
}
private void onOpenURLClick(){
((MainActivity)getActivity()).handleURL(Uri.parse(searchViewHelper.getQuery()), accountID);
}
private void onGoToHashtagClick(){
String q=searchViewHelper.getQuery();
if(q.startsWith("#"))
q=q.substring(1);
UiUtils.openHashtagTimeline(getActivity(), accountID, q);
}
private void onGoToAccountClick(){
String q=searchViewHelper.getQuery();
if(!q.startsWith("@")){
q="@"+q;
}
if(q.lastIndexOf('@')==0){
q+="@"+AccountSessionManager.get(accountID).domain;
}
((MainActivity)getActivity()).openSearchQuery(q, accountID, R.string.loading, true);
}
private void onGoToStatusSearchClick(){
deliverResult(searchViewHelper.getQuery(), SearchResult.Type.STATUS);
}
private void onGoToAccountSearchClick(){
deliverResult(searchViewHelper.getQuery(), SearchResult.Type.ACCOUNT);
}
private void onClearRecentClick(){
AccountSessionManager.getInstance().getAccount(accountID).getCacheController().clearRecentSearches();
if(isInRecentMode()){
data.clear();
recentsHeader.setVisible(false);
mergeAdapter.notifyDataSetChanged();
}
}
private void deliverResult(String query, SearchResult.Type typeFilter){
Bundle res=new Bundle();
res.putString("query", query);
if(typeFilter!=null)
res.putInt("filter", typeFilter.ordinal());
setResult(true, res);
Nav.finish(this);
}
@Override
public boolean onBackPressed(){
String initialQuery=getArguments().getString("query");
searchViewHelper.setQuery(TextUtils.isEmpty(initialQuery) ? "" : initialQuery);
currentQuery=initialQuery;
return false;
}
private static class AnimatableOutlineProvider extends ViewOutlineProvider{
private float boundsFraction, radius;
private final Rect boundsFrom, boundsTo;
private AnimatableOutlineProvider(Rect boundsFrom, Rect boundsTo, float radius){
this.boundsFrom=boundsFrom;
this.boundsTo=boundsTo;
this.radius=radius;
}
@Override
public void getOutline(View view, Outline outline){
outline.setRoundRect(
UiUtils.lerp(boundsFrom.left, boundsTo.left, boundsFraction),
UiUtils.lerp(boundsFrom.top, boundsTo.top, boundsFraction),
UiUtils.lerp(boundsFrom.right, boundsTo.right, boundsFraction),
UiUtils.lerp(boundsFrom.bottom, boundsTo.bottom, boundsFraction),
radius
);
}
@Keep
public float getBoundsFraction(){
return boundsFraction;
}
@Keep
public void setBoundsFraction(float boundsFraction){
this.boundsFraction=boundsFraction;
}
@Keep
public float getRadius(){
return radius;
}
@Keep
public void setRadius(float radius){
this.radius=radius;
}
}
private class SearchResultsAdapter extends UsableRecyclerView.Adapter<RecyclerView.ViewHolder> implements ImageLoaderRecyclerAdapter{
public SearchResultsAdapter(){
super(imgLoader);
}
@NonNull
@Override
public RecyclerView.ViewHolder onCreateViewHolder(@NonNull ViewGroup parent, int viewType){
if(viewType==R.id.list_item_account){
return new CustomAccountViewHolder(SearchQueryFragment.this, parent, null);
}else if(viewType==R.id.list_item_simple){
return new SimpleListItemViewHolder(parent.getContext(), parent);
}
throw new IllegalArgumentException();
}
@Override
public void onBindViewHolder(@NonNull RecyclerView.ViewHolder holder, int position){
if(holder instanceof CustomAccountViewHolder avh){
avh.bind(data.get(position).account);
avh.searchResult=data.get(position).result;
}else if(holder instanceof SimpleListItemViewHolder ivh){
ivh.bind(data.get(position).hashtagItem);
}
}
@Override
public int getItemCount(){
return data.size();
}
@Override
public int getItemViewType(int position){
return switch(data.get(position).result.type){
case ACCOUNT -> R.id.list_item_account;
case HASHTAG -> R.id.list_item_simple;
default -> throw new IllegalStateException("Unexpected value: "+data.get(position).result.type);
};
}
@Override
public int getImageCountForItem(int position){
SearchResultViewModel vm=data.get(position);
if(vm.account!=null)
return vm.account.emojiHelper.getImageCount()+1;
return 0;
}
@Override
public ImageLoaderRequest getImageRequest(int position, int image){
SearchResultViewModel vm=data.get(position);
if(vm.account!=null){
if(image==0)
return vm.account.avaRequest;
return vm.account.emojiHelper.getImageRequest(image-1);
}
return null;
}
}
private class CustomAccountViewHolder extends AccountViewHolder{
public SearchResult searchResult;
public CustomAccountViewHolder(Fragment fragment, ViewGroup list, HashMap<String, Relationship> relationships){
super(fragment, list, relationships);
}
@Override
public void onClick(){
super.onClick();
AccountSessionManager.getInstance().getAccount(accountID).getCacheController().putRecentSearch(searchResult);
}
}
}

View File

@@ -25,7 +25,6 @@ import me.grishka.appkit.views.UsableRecyclerView;
public class TrendingHashtagsFragment extends BaseRecyclerFragment<Hashtag> implements ScrollableToTop{
private String accountID;
private DiscoverInfoBannerHelper bannerHelper=new DiscoverInfoBannerHelper(DiscoverInfoBannerHelper.BannerType.TRENDING_HASHTAGS);
public TrendingHashtagsFragment(){
super(10);
@@ -54,13 +53,6 @@ public class TrendingHashtagsFragment extends BaseRecyclerFragment<Hashtag> impl
return new HashtagsAdapter();
}
@Override
public void onViewCreated(View view, Bundle savedInstanceState){
super.onViewCreated(view, savedInstanceState);
list.addItemDecoration(new DividerItemDecoration(getActivity(), R.attr.colorPollVoted, .5f, 16, 16));
bannerHelper.maybeAddBanner(contentWrap);
}
@Override
public void scrollToTop(){
smoothScrollRecyclerViewToTop(list);

View File

@@ -1,13 +1,17 @@
package org.joinmastodon.android.fragments.settings;
import android.content.Context;
import android.content.SharedPreferences;
import android.os.Bundle;
import org.joinmastodon.android.MastodonApp;
import org.joinmastodon.android.api.session.AccountActivationInfo;
import org.joinmastodon.android.api.session.AccountSession;
import org.joinmastodon.android.api.session.AccountSessionManager;
import org.joinmastodon.android.fragments.HomeFragment;
import org.joinmastodon.android.fragments.onboarding.AccountActivationFragment;
import org.joinmastodon.android.model.viewmodel.ListItem;
import org.joinmastodon.android.ui.utils.DiscoverInfoBannerHelper;
import org.joinmastodon.android.updater.GithubSelfUpdater;
import java.util.List;
@@ -23,7 +27,8 @@ public class SettingsDebugFragment extends BaseSettingsFragment<Void>{
onDataLoaded(List.of(
new ListItem<>("Test email confirmation flow", null, this::onTestEmailConfirmClick),
selfUpdateItem=new ListItem<>("Force self-update", null, this::onForceSelfUpdateClick),
resetUpdateItem=new ListItem<>("Reset self-updater", null, this::onResetUpdaterClick)
resetUpdateItem=new ListItem<>("Reset self-updater", null, this::onResetUpdaterClick),
new ListItem<>("Reset search info banners", null, this::onResetDiscoverBannersClick)
));
if(!GithubSelfUpdater.needSelfUpdating()){
resetUpdateItem.isEnabled=selfUpdateItem.isEnabled=false;
@@ -55,6 +60,11 @@ public class SettingsDebugFragment extends BaseSettingsFragment<Void>{
restartUI();
}
private void onResetDiscoverBannersClick(){
DiscoverInfoBannerHelper.reset();
restartUI();
}
private void restartUI(){
Bundle args=new Bundle();
args.putString("account", accountID);

View File

@@ -90,7 +90,7 @@ public class SettingsServerFragment extends AppKitFragment{
tabBar=view.findViewById(R.id.tabbar);
tabBar.setTabTextColors(UiUtils.getThemeColor(getActivity(), R.attr.colorM3OnSurfaceVariant), UiUtils.getThemeColor(getActivity(), R.attr.colorM3Primary));
tabBar.setTabTextSize(V.dp(16));
tabBar.setTabTextSize(V.dp(14));
tabLayoutMediator=new TabLayoutMediator(tabBar, pager, (tab, position)->tab.setText(switch(position){
case 0 -> R.string.about_server;
case 1 -> R.string.server_rules;

View File

@@ -2,6 +2,7 @@ package org.joinmastodon.android.model;
import org.joinmastodon.android.api.ObjectValidationException;
import org.joinmastodon.android.api.RequiredField;
import org.joinmastodon.android.model.viewmodel.AccountViewModel;
public class SearchResult extends BaseModel implements DisplayItemsParent{
public Account account;

View File

@@ -0,0 +1,19 @@
package org.joinmastodon.android.model.viewmodel;
import android.text.TextUtils;
import org.joinmastodon.android.model.Card;
import me.grishka.appkit.imageloader.requests.ImageLoaderRequest;
import me.grishka.appkit.imageloader.requests.UrlImageLoaderRequest;
import me.grishka.appkit.utils.V;
public class CardViewModel{
public final Card card;
public final ImageLoaderRequest imageRequest;
public CardViewModel(Card card, int width, int height){
this.card=card;
this.imageRequest=TextUtils.isEmpty(card.image) ? null : new UrlImageLoaderRequest(card.image, V.dp(width), V.dp(height));
}
}

View File

@@ -0,0 +1,24 @@
package org.joinmastodon.android.model.viewmodel;
import android.content.Context;
import org.joinmastodon.android.R;
import org.joinmastodon.android.model.Hashtag;
import org.joinmastodon.android.model.SearchResult;
public class SearchResultViewModel{
public SearchResult result;
public AccountViewModel account;
public ListItem<Hashtag> hashtagItem;
public SearchResultViewModel(SearchResult result, String accountID, boolean isRecents){
this.result=result;
switch(result.type){
case ACCOUNT -> account=new AccountViewModel(result.account, accountID);
case HASHTAG -> {
hashtagItem=new ListItem<>((isRecents ? "#" : "")+result.hashtag.name, null, isRecents ? R.drawable.ic_history_24px : R.drawable.ic_tag_24px, null, result.hashtag);
hashtagItem.isEnabled=true;
}
}
}
}

View File

@@ -0,0 +1,129 @@
package org.joinmastodon.android.ui;
import android.content.Context;
import android.content.res.ColorStateList;
import android.text.InputType;
import android.text.TextUtils;
import android.view.View;
import android.view.ViewGroup;
import android.view.inputmethod.EditorInfo;
import android.widget.EditText;
import android.widget.ImageButton;
import android.widget.LinearLayout;
import android.widget.Toolbar;
import org.joinmastodon.android.R;
import org.joinmastodon.android.ui.utils.SimpleTextWatcher;
import org.joinmastodon.android.ui.utils.UiUtils;
import java.util.function.Consumer;
import me.grishka.appkit.utils.V;
public class SearchViewHelper{
private LinearLayout searchLayout;
private EditText searchEdit;
private ImageButton clearSearchButton;
private View divider;
private String currentQuery;
private Consumer<String> listener;
private Runnable debouncer=()->{
currentQuery=searchEdit.getText().toString();
if(listener!=null){
listener.accept(currentQuery);
}
};
private boolean isEmpty=true;
private Runnable enterCallback;
private Consumer<String> listenerWithoutDebounce;
public SearchViewHelper(Context context, Context toolbarContext, String hint){
searchLayout=new LinearLayout(context);
searchLayout.setOrientation(LinearLayout.HORIZONTAL);
searchEdit=new EditText(context);
searchEdit.setHint(hint);
searchEdit.setInputType(InputType.TYPE_TEXT_VARIATION_FILTER);
searchEdit.setBackground(null);
searchEdit.addTextChangedListener(new SimpleTextWatcher(e->{
searchEdit.removeCallbacks(debouncer);
searchEdit.postDelayed(debouncer, 300);
boolean newIsEmpty=e.length()==0;
if(isEmpty!=newIsEmpty){
isEmpty=newIsEmpty;
V.setVisibilityAnimated(clearSearchButton, isEmpty ? View.INVISIBLE : View.VISIBLE);
}
if(listenerWithoutDebounce!=null)
listenerWithoutDebounce.accept(e.toString());
}));
searchEdit.setImeOptions(EditorInfo.IME_ACTION_SEARCH);
searchEdit.setOnEditorActionListener((v, actionId, event)->{
searchEdit.removeCallbacks(debouncer);
debouncer.run();
if(enterCallback!=null)
enterCallback.run();
return true;
});
searchEdit.setTextAppearance(R.style.m3_body_large);
searchEdit.setHintTextColor(UiUtils.getThemeColor(toolbarContext, R.attr.colorM3OnSurfaceVariant));
searchEdit.setTextColor(UiUtils.getThemeColor(toolbarContext, R.attr.colorM3OnSurface));
searchLayout.addView(searchEdit, new LinearLayout.LayoutParams(0, ViewGroup.LayoutParams.MATCH_PARENT, 1f));
clearSearchButton=new ImageButton(context);
clearSearchButton.setImageResource(R.drawable.ic_baseline_close_24);
clearSearchButton.setContentDescription(context.getString(R.string.clear));
clearSearchButton.setImageTintList(ColorStateList.valueOf(UiUtils.getThemeColor(context, R.attr.colorM3OnSurfaceVariant)));
clearSearchButton.setBackground(UiUtils.getThemeDrawable(toolbarContext, android.R.attr.actionBarItemBackground));
clearSearchButton.setOnClickListener(v->{
searchEdit.setText("");
searchEdit.removeCallbacks(debouncer);
debouncer.run();
});
clearSearchButton.setVisibility(View.INVISIBLE);
searchLayout.addView(clearSearchButton, new LinearLayout.LayoutParams(V.dp(56), ViewGroup.LayoutParams.MATCH_PARENT));
}
public void setListeners(Consumer<String> listener, Consumer<String> listenerWithoutDebounce){
this.listener=listener;
this.listenerWithoutDebounce=listenerWithoutDebounce;
}
public void install(Toolbar toolbar){
toolbar.getLayoutParams().height=V.dp(72);
toolbar.setMinimumHeight(V.dp(72));
if(searchLayout.getParent()!=null)
((ViewGroup) searchLayout.getParent()).removeView(searchLayout);
toolbar.addView(searchLayout, new Toolbar.LayoutParams(ViewGroup.LayoutParams.MATCH_PARENT, ViewGroup.LayoutParams.MATCH_PARENT));
toolbar.setBackgroundResource(R.drawable.bg_m3_surface3);
searchEdit.requestFocus();
}
public void addDivider(ViewGroup contentView){
divider=new View(contentView.getContext());
divider.setBackgroundColor(UiUtils.getThemeColor(contentView.getContext(), R.attr.colorM3Outline));
contentView.addView(divider, 1, new LinearLayout.LayoutParams(ViewGroup.LayoutParams.MATCH_PARENT, V.dp(1)));
}
public LinearLayout getSearchLayout(){
return searchLayout;
}
public void setEnterCallback(Runnable enterCallback){
this.enterCallback=enterCallback;
}
public void setQuery(String q){
currentQuery=q;
searchEdit.setText(currentQuery);
searchEdit.setSelection(searchEdit.length());
searchEdit.removeCallbacks(debouncer);
}
public String getQuery(){
return currentQuery;
}
public View getDivider(){
return divider;
}
}

View File

@@ -4,61 +4,79 @@ import android.app.Activity;
import android.content.Context;
import android.content.SharedPreferences;
import android.view.View;
import android.view.ViewGroup;
import android.widget.FrameLayout;
import android.widget.ImageView;
import android.widget.TextView;
import org.joinmastodon.android.MastodonApp;
import org.joinmastodon.android.R;
import org.joinmastodon.android.api.session.AccountSessionManager;
import me.grishka.appkit.utils.CubicBezierInterpolator;
import java.util.EnumSet;
import androidx.recyclerview.widget.RecyclerView;
import me.grishka.appkit.utils.MergeRecyclerAdapter;
import me.grishka.appkit.utils.SingleViewRecyclerAdapter;
public class DiscoverInfoBannerHelper{
private View banner;
private final BannerType type;
private final String accountID;
private static EnumSet<BannerType> bannerTypesToShow=EnumSet.noneOf(BannerType.class);
public DiscoverInfoBannerHelper(BannerType type){
this.type=type;
}
private SharedPreferences getPrefs(){
return MastodonApp.context.getSharedPreferences("onboarding", Context.MODE_PRIVATE);
}
public void maybeAddBanner(FrameLayout view){
if(!getPrefs().getBoolean("bannerHidden_"+type, false)){
((Activity)view.getContext()).getLayoutInflater().inflate(R.layout.discover_info_banner, view);
banner=view.findViewById(R.id.discover_info_banner);
view.findViewById(R.id.banner_dismiss).setOnClickListener(this::onDismissClick);
TextView text=view.findViewById(R.id.banner_text);
text.setText(switch(type){
case TRENDING_POSTS -> R.string.trending_posts_info_banner;
case TRENDING_HASHTAGS -> R.string.trending_hashtags_info_banner;
case TRENDING_LINKS -> R.string.trending_links_info_banner;
case LOCAL_TIMELINE -> R.string.local_timeline_info_banner;
});
static{
for(BannerType t:BannerType.values()){
if(!getPrefs().getBoolean("bannerHidden_"+t, false))
bannerTypesToShow.add(t);
}
}
private void onDismissClick(View v){
if(banner==null)
return;
View _banner=banner;
banner.animate()
.alpha(0)
.setDuration(200)
.setInterpolator(CubicBezierInterpolator.DEFAULT)
.withEndAction(()->((ViewGroup)_banner.getParent()).removeView(_banner))
.start();
public DiscoverInfoBannerHelper(BannerType type, String accountID){
this.type=type;
this.accountID=accountID;
}
private static SharedPreferences getPrefs(){
return MastodonApp.context.getSharedPreferences("onboarding", Context.MODE_PRIVATE);
}
public void maybeAddBanner(RecyclerView list, MergeRecyclerAdapter adapter){
if(bannerTypesToShow.contains(type)){
banner=((Activity)list.getContext()).getLayoutInflater().inflate(R.layout.discover_info_banner, list, false);
TextView text=banner.findViewById(R.id.banner_text);
text.setText(switch(type){
case TRENDING_POSTS -> list.getResources().getString(R.string.trending_posts_info_banner);
case TRENDING_LINKS -> list.getResources().getString(R.string.trending_links_info_banner);
case LOCAL_TIMELINE -> list.getResources().getString(R.string.local_timeline_info_banner, AccountSessionManager.get(accountID).domain);
case ACCOUNTS -> list.getResources().getString(R.string.recommended_accounts_info_banner);
});
ImageView icon=banner.findViewById(R.id.icon);
icon.setImageResource(switch(type){
case TRENDING_POSTS -> R.drawable.ic_whatshot_24px;
case TRENDING_LINKS -> R.drawable.ic_feed_24px;
case LOCAL_TIMELINE -> R.drawable.ic_stream_24px;
case ACCOUNTS -> R.drawable.ic_group_add_24px;
});
adapter.addAdapter(new SingleViewRecyclerAdapter(banner));
}
}
public void onBannerBecameVisible(){
getPrefs().edit().putBoolean("bannerHidden_"+type, true).apply();
banner=null;
// bannerTypesToShow is not updated here on purpose so the banner keeps showing until the app is relaunched
}
public static void reset(){
SharedPreferences prefs=getPrefs();
SharedPreferences.Editor e=prefs.edit();
prefs.getAll().keySet().stream().filter(k->k.startsWith("bannerHidden_")).forEach(e::remove);
e.apply();
bannerTypesToShow=EnumSet.allOf(BannerType.class);
}
public enum BannerType{
TRENDING_POSTS,
TRENDING_HASHTAGS,
TRENDING_LINKS,
LOCAL_TIMELINE,
// ACCOUNTS
ACCOUNTS
}
}

View File

@@ -162,6 +162,8 @@ public class AccountViewHolder extends BindableViewHolder<AccountViewModel> impl
@Override
public boolean onLongClick(float x, float y){
if(relationships==null)
return false;
Relationship relationship=relationships.get(item.account.id);
if(relationship==null)
return false;

View File

@@ -32,7 +32,7 @@ public class HashtagChartView extends View implements CustomViewHelper{
public HashtagChartView(Context context, AttributeSet attrs, int defStyle){
super(context, attrs, defStyle);
paint.setStrokeWidth(dp(1.71f));
paint.setStrokeWidth(dp(1));
paint.setStrokeCap(Paint.Cap.ROUND);
paint.setStrokeJoin(Paint.Join.ROUND);
}

View File

@@ -0,0 +1,9 @@
<?xml version="1.0" encoding="utf-8"?>
<ripple xmlns:android="http://schemas.android.com/apk/res/android" android:color="@color/m3_on_surface_overlay">
<item android:id="@android:id/mask">
<shape>
<solid android:color="#000"/>
<corners android:radius="12dp"/>
</shape>
</item>
</ripple>

View 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="M5,21Q4.175,21 3.587,20.413Q3,19.825 3,19V5Q3,4.175 3.587,3.587Q4.175,3 5,3H16L21,8V19Q21,19.825 20.413,20.413Q19.825,21 19,21ZM5,19H19Q19,19 19,19Q19,19 19,19V9H15V5H5Q5,5 5,5Q5,5 5,5V19Q5,19 5,19Q5,19 5,19ZM7,17H17V15H7ZM7,9H12V7H7ZM7,13H17V11H7ZM5,5V9V5V9V19Q5,19 5,19Q5,19 5,19Q5,19 5,19Q5,19 5,19V5Q5,5 5,5Q5,5 5,5Z"/>
</vector>

View 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.5,11.95Q13.225,11.15 13.613,10.125Q14,9.1 14,8Q14,6.9 13.613,5.875Q13.225,4.85 12.5,4.05Q14,4.25 15,5.375Q16,6.5 16,8Q16,9.5 15,10.625Q14,11.75 12.5,11.95ZM18,20V17Q18,16.1 17.6,15.288Q17.2,14.475 16.55,13.85Q17.825,14.3 18.913,15.012Q20,15.725 20,17V20ZM20,13V11H18V9H20V7H22V9H24V11H22V13ZM8,12Q6.35,12 5.175,10.825Q4,9.65 4,8Q4,6.35 5.175,5.175Q6.35,4 8,4Q9.65,4 10.825,5.175Q12,6.35 12,8Q12,9.65 10.825,10.825Q9.65,12 8,12ZM0,20V17.2Q0,16.35 0.438,15.637Q0.875,14.925 1.6,14.55Q3.15,13.775 4.75,13.387Q6.35,13 8,13Q9.65,13 11.25,13.387Q12.85,13.775 14.4,14.55Q15.125,14.925 15.562,15.637Q16,16.35 16,17.2V20ZM8,10Q8.825,10 9.413,9.412Q10,8.825 10,8Q10,7.175 9.413,6.588Q8.825,6 8,6Q7.175,6 6.588,6.588Q6,7.175 6,8Q6,8.825 6.588,9.412Q7.175,10 8,10ZM2,18H14V17.2Q14,16.925 13.863,16.7Q13.725,16.475 13.5,16.35Q12.15,15.675 10.775,15.337Q9.4,15 8,15Q6.6,15 5.225,15.337Q3.85,15.675 2.5,16.35Q2.275,16.475 2.138,16.7Q2,16.925 2,17.2ZM8,8Q8,8 8,8Q8,8 8,8Q8,8 8,8Q8,8 8,8Q8,8 8,8Q8,8 8,8Q8,8 8,8Q8,8 8,8ZM8,18Q8,18 8,18Q8,18 8,18Q8,18 8,18Q8,18 8,18Q8,18 8,18Q8,18 8,18Q8,18 8,18Q8,18 8,18Z"/>
</vector>

View 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="M14.8,16.2 L11,12.4V7H13V11.6L16.2,14.8ZM12,21Q8.55,21 5.988,18.712Q3.425,16.425 3.05,13H5.1Q5.45,15.6 7.412,17.3Q9.375,19 12,19Q14.925,19 16.962,16.962Q19,14.925 19,12Q19,9.075 16.962,7.037Q14.925,5 12,5Q10.275,5 8.775,5.8Q7.275,6.6 6.25,8H9V10H3V4H5V6.35Q6.275,4.75 8.113,3.875Q9.95,3 12,3Q13.875,3 15.513,3.712Q17.15,4.425 18.363,5.637Q19.575,6.85 20.288,8.487Q21,10.125 21,12Q21,13.875 20.288,15.512Q19.575,17.15 18.363,18.362Q17.15,19.575 15.513,20.288Q13.875,21 12,21Z"/>
</vector>

View 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="M11,17H7Q4.925,17 3.463,15.537Q2,14.075 2,12Q2,9.925 3.463,8.462Q4.925,7 7,7H11V9H7Q5.75,9 4.875,9.875Q4,10.75 4,12Q4,13.25 4.875,14.125Q5.75,15 7,15H11ZM8,13V11H16V13ZM13,17V15H17Q18.25,15 19.125,14.125Q20,13.25 20,12Q20,10.75 19.125,9.875Q18.25,9 17,9H13V7H17Q19.075,7 20.538,8.462Q22,9.925 22,12Q22,14.075 20.538,15.537Q19.075,17 17,17Z"/>
</vector>

View 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,12Q10.35,12 9.175,10.825Q8,9.65 8,8Q8,6.35 9.175,5.175Q10.35,4 12,4Q13.65,4 14.825,5.175Q16,6.35 16,8Q16,9.65 14.825,10.825Q13.65,12 12,12ZM4,20V17.2Q4,16.35 4.438,15.637Q4.875,14.925 5.6,14.55Q7.15,13.775 8.75,13.387Q10.35,13 12,13Q13.65,13 15.25,13.387Q16.85,13.775 18.4,14.55Q19.125,14.925 19.562,15.637Q20,16.35 20,17.2V20ZM6,18H18V17.2Q18,16.925 17.863,16.7Q17.725,16.475 17.5,16.35Q16.15,15.675 14.775,15.337Q13.4,15 12,15Q10.6,15 9.225,15.337Q7.85,15.675 6.5,16.35Q6.275,16.475 6.138,16.7Q6,16.925 6,17.2ZM12,10Q12.825,10 13.413,9.412Q14,8.825 14,8Q14,7.175 13.413,6.588Q12.825,6 12,6Q11.175,6 10.588,6.588Q10,7.175 10,8Q10,8.825 10.588,9.412Q11.175,10 12,10ZM12,8Q12,8 12,8Q12,8 12,8Q12,8 12,8Q12,8 12,8Q12,8 12,8Q12,8 12,8Q12,8 12,8Q12,8 12,8ZM12,18Q12,18 12,18Q12,18 12,18Q12,18 12,18Q12,18 12,18Q12,18 12,18Q12,18 12,18Q12,18 12,18Q12,18 12,18Z"/>
</vector>

View 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="M4,14Q3.175,14 2.588,13.412Q2,12.825 2,12Q2,11.175 2.588,10.587Q3.175,10 4,10Q4.825,10 5.412,10.587Q6,11.175 6,12Q6,12.825 5.412,13.412Q4.825,14 4,14ZM5.65,19.7 L4.25,18.3 8.6,13.95 10,15.35ZM8.65,10 L4.3,5.65 5.7,4.25 10.05,8.6ZM12,22Q11.175,22 10.588,21.413Q10,20.825 10,20Q10,19.175 10.588,18.587Q11.175,18 12,18Q12.825,18 13.413,18.587Q14,19.175 14,20Q14,20.825 13.413,21.413Q12.825,22 12,22ZM12,6Q11.175,6 10.588,5.412Q10,4.825 10,4Q10,3.175 10.588,2.587Q11.175,2 12,2Q12.825,2 13.413,2.587Q14,3.175 14,4Q14,4.825 13.413,5.412Q12.825,6 12,6ZM15.35,10.05 L13.95,8.6 18.35,4.25 19.75,5.65ZM18.35,19.7 L14,15.35 15.4,13.95 19.75,18.3ZM20,14Q19.175,14 18.587,13.412Q18,12.825 18,12Q18,11.175 18.587,10.587Q19.175,10 20,10Q20.825,10 21.413,10.587Q22,11.175 22,12Q22,12.825 21.413,13.412Q20.825,14 20,14Z"/>
</vector>

View 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="M6,20 L7,16H3.5L4,14H7.5L8.5,10H4.5L5,8H9L10,4H12L11,8H15L16,4H18L17,8H20.5L20,10H16.5L15.5,14H19.5L19,16H15L14,20H12L13,16H9L8,20ZM9.5,14H13.5L14.5,10H10.5Z"/>
</vector>

View 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,22Q10.35,22 8.812,21.488Q7.275,20.975 6,20L7.45,18.55Q8.5,19.275 9.65,19.637Q10.8,20 12,20Q15.325,20 17.663,17.663Q20,15.325 20,12Q20,8.675 17.663,6.337Q15.325,4 12,4Q8.675,4 6.338,6.337Q4,8.675 4,12H2Q2,9.925 2.788,8.1Q3.575,6.275 4.925,4.925Q6.275,3.575 8.1,2.787Q9.925,2 12,2Q14.075,2 15.887,2.787Q17.7,3.575 19.062,4.938Q20.425,6.3 21.212,8.113Q22,9.925 22,12Q22,14.05 21.212,15.875Q20.425,17.7 19.062,19.062Q17.7,20.425 15.887,21.212Q14.075,22 12,22ZM3.975,17.925 L8.05,13.85 11.05,16.35 16,11.4V14H18V8H12V10H14.6L10.95,13.65L7.95,11.15L2.925,16.175Q3.2,16.75 3.413,17.113Q3.625,17.475 3.975,17.925ZM12,12Q12,12 12,12Q12,12 12,12Q12,12 12,12Q12,12 12,12Q12,12 12,12Q12,12 12,12Q12,12 12,12Q12,12 12,12Q12,12 12,12Q12,12 12,12Q12,12 12,12Q12,12 12,12Q12,12 12,12Q12,12 12,12Q12,12 12,12Q12,12 12,12Z"/>
</vector>

View File

@@ -1,5 +1,5 @@
<?xml version="1.0" encoding="utf-8"?>
<shape xmlns:android="http://schemas.android.com/apk/res/android">
<solid android:color="?colorSearchField"/>
<corners android:radius="6dp"/>
<corners android:radius="12dp"/>
<solid android:color="#000"/>
</shape>

View File

@@ -5,30 +5,27 @@
android:orientation="horizontal"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_gravity="top"
android:elevation="1dp"
android:outlineProvider="background"
android:background="?colorWindowBackground">
android:layout_margin="16dp"
android:padding="16dp"
android:background="@drawable/rect_12dp"
android:backgroundTint="?colorM3SurfaceVariant">
<ImageView
android:id="@+id/icon"
android:layout_width="32dp"
android:layout_height="32dp"
android:layout_marginEnd="16dp"
android:tint="?colorM3OnPrimaryContainer"
android:scaleType="center"
android:importantForAccessibility="no"
tools:src="@drawable/ic_whatshot_24px"/>
<TextView
android:id="@+id/banner_text"
android:layout_width="0dp"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_weight="1"
android:layout_marginStart="16dp"
android:layout_marginTop="16dp"
android:layout_marginBottom="16dp"
android:textAppearance="@style/m3_body_large"
android:textAppearance="@style/m3_body_medium"
android:textColor="?colorM3OnSurface"
tools:text="@string/trending_posts_info_banner"/>
<ImageButton
android:id="@+id/banner_dismiss"
android:layout_width="40dp"
android:layout_height="40dp"
android:layout_margin="8dp"
android:src="@drawable/ic_fluent_dismiss_circle_24_filled"
android:tint="?android:textColorSecondary"
android:contentDescription="@string/dismiss"
android:background="?android:selectableItemBackgroundBorderless"/>
</LinearLayout>

View File

@@ -9,106 +9,74 @@
<FrameLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:paddingTop="8dp"
android:paddingLeft="16dp"
android:paddingRight="16dp"
android:paddingBottom="16dp"
android:background="?android:statusBarColor">
<!-- https://github.com/mastodon/mastodon-android/issues/95 -->
<View
android:layout_width="1px"
android:layout_height="1px"
android:focusable="true"
android:focusableInTouchMode="true"/>
<FrameLayout
android:background="?colorM3Surface">
<LinearLayout
android:id="@+id/search_wrap"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:background="@drawable/bg_search_field"
android:outlineProvider="background"
android:clipToOutline="true">
<EditText
android:id="@+id/search_edit"
android:layout_width="match_parent"
android:layout_height="40dp"
android:hint="@string/search_hint"
android:textColorHint="?colorSearchHint"
android:textColor="?android:textColorPrimary"
android:textSize="16dp"
android:singleLine="true"
android:inputType="textFilter"
android:imeOptions="actionSearch"
android:paddingLeft="48dp"
android:paddingRight="48dp"
android:paddingTop="0dp"
android:paddingBottom="0dp"
android:background="@null"
android:elevation="0dp"/>
android:layout_height="56dp"
android:layout_margin="16dp"
android:orientation="horizontal"
android:background="@drawable/bg_m3_surface3">
<ImageButton
android:id="@+id/search_back"
android:layout_width="40dp"
android:layout_height="40dp"
android:layout_gravity="start"
android:layout_marginStart="4dp"
android:background="?android:selectableItemBackgroundBorderless"
android:tint="?colorSearchHint"
android:elevation="1dp"
android:layout_margin="8dp"
android:contentDescription="@string/back"
android:src="@drawable/ic_fluent_search_24_regular"/>
android:background="@drawable/bg_round_ripple"
android:tint="?colorM3OnSurfaceVariant"
android:src="@drawable/ic_search_24px"/>
<ImageButton
android:id="@+id/search_clear"
android:layout_width="40dp"
android:layout_height="40dp"
android:layout_gravity="end"
android:layout_marginEnd="4dp"
android:background="?android:selectableItemBackgroundBorderless"
android:tint="?colorSearchHint"
android:elevation="1dp"
android:visibility="invisible"
android:contentDescription="@string/clear"
android:src="@drawable/ic_fluent_dismiss_24_regular"/>
<ProgressBar
android:id="@+id/search_progress"
android:layout_width="20dp"
android:layout_height="20dp"
android:layout_gravity="end|center_vertical"
android:layout_marginEnd="14dp"
android:indeterminateTint="?colorSearchHint"
style="?android:progressBarStyleSmall"
android:visibility="invisible"/>
</FrameLayout>
<TextView
android:id="@+id/search_text"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:gravity="center_vertical"
android:singleLine="true"
android:textColor="?colorM3OnSurfaceVariant"
android:textAppearance="@style/m3_body_large"
android:text="@string/search_mastodon"/>
</LinearLayout>
</FrameLayout>
<org.joinmastodon.android.ui.tabs.TabLayout
android:id="@+id/tabbar"
<LinearLayout
android:id="@+id/discover_content"
android:layout_width="match_parent"
android:layout_height="48dp"
app:tabGravity="start"
app:tabMinWidth="120dp"
app:tabIndicator="@drawable/mtrl_tabs_default_indicator"
app:tabIndicatorAnimationMode="elastic"
app:tabIndicatorColor="?android:textColorPrimary"
app:tabMode="scrollable"
android:background="@drawable/bg_discover_tabs"/>
android:layout_height="match_parent"
android:orientation="vertical">
<org.joinmastodon.android.ui.tabs.TabLayout
android:id="@+id/tabbar"
android:layout_width="match_parent"
android:layout_height="48dp"
app:tabGravity="start"
app:tabIndicator="@drawable/tab_indicator_m3"
app:tabIndicatorAnimationMode="elastic"
app:tabIndicatorColor="?colorM3Primary"
app:tabIndicatorFullWidth="false"
app:tabMinWidth="90dp"
app:tabMode="scrollable"
android:background="?colorM3Surface"/>
<androidx.viewpager2.widget.ViewPager2
android:id="@+id/pager"
android:layout_width="match_parent"
android:layout_height="0dp"
android:layout_weight="1"/>
<View
android:id="@+id/tabs_divider"
android:layout_width="match_parent"
android:layout_height="1dp"
android:background="?colorM3SurfaceVariant"/>
<FrameLayout
android:id="@+id/search_fragment"
android:layout_width="match_parent"
android:layout_height="0dp"
android:layout_weight="1"
android:visibility="gone"/>
<androidx.viewpager2.widget.ViewPager2
android:id="@+id/pager"
android:layout_width="match_parent"
android:layout_height="0dp"
android:layout_weight="1"/>
<FrameLayout
android:id="@+id/search_fragment"
android:layout_width="match_parent"
android:layout_height="0dp"
android:layout_weight="1"
android:visibility="gone"/>
</LinearLayout>
</me.grishka.appkit.views.FragmentRootLinearLayout>

View File

@@ -8,18 +8,6 @@
xmlns:android="http://schemas.android.com/apk/res/android"
android:background="?android:windowBackground">
<org.joinmastodon.android.ui.tabs.TabLayout
android:id="@+id/tabbar"
android:layout_width="match_parent"
android:layout_height="48dp"
app:tabGravity="start"
app:tabMinWidth="120dp"
app:tabIndicator="@drawable/mtrl_tabs_default_indicator"
app:tabIndicatorAnimationMode="elastic"
app:tabIndicatorColor="?android:textColorPrimary"
app:tabMode="scrollable"
android:background="@drawable/bg_discover_tabs"/>
<FrameLayout
android:id="@+id/appkit_loader_content"
android:layout_width="match_parent"

View File

@@ -3,28 +3,31 @@
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:padding="16dp">
android:paddingVertical="12dp"
android:paddingStart="16dp"
android:paddingEnd="24dp">
<ImageView
android:id="@+id/photo"
android:layout_width="132dp"
android:layout_height="wrap_content"
android:layout_width="56dp"
android:layout_height="56dp"
android:layout_alignParentTop="true"
android:layout_alignParentEnd="true"
android:layout_alignBottom="@id/subtitle"
android:layout_marginStart="8dp"
android:layout_alignParentStart="true"
android:layout_marginEnd="16dp"
android:scaleType="centerCrop"
android:importantForAccessibility="no"
tools:src="#0f0"/>
<TextView
android:id="@+id/name"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_toStartOf="@id/photo"
android:textAppearance="@style/m3_title_small"
android:textColor="?android:textColorPrimary"
android:layout_height="16dp"
android:layout_toEndOf="@id/photo"
android:textAppearance="@style/m3_label_medium"
android:textColor="?colorM3OnSurfaceVariant"
android:singleLine="true"
android:ellipsize="end"
android:gravity="center_vertical"
tools:text="Site Name"/>
<TextView
@@ -32,24 +35,12 @@
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_below="@id/name"
android:layout_toStartOf="@id/photo"
android:layout_marginTop="4dp"
android:layout_marginBottom="32dp"
android:textAppearance="@style/m3_title_medium"
android:maxLines="5"
android:layout_toEndOf="@id/photo"
android:textAppearance="@style/m3_body_large"
android:textColor="?colorM3OnSurface"
android:paddingVertical="2.5dp"
android:maxLines="2"
android:ellipsize="end"
tools:text="Title title title"/>
<TextView
android:id="@+id/subtitle"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_below="@id/title"
android:layout_toStartOf="@id/photo"
android:textAppearance="@style/m3_label_medium"
android:textColor="?android:textColorSecondary"
android:singleLine="true"
android:ellipsize="end"
tools:text="Discussed 123 times"/>
</RelativeLayout>

View File

@@ -0,0 +1,44 @@
<?xml version="1.0" encoding="utf-8"?>
<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="280dp"
android:layout_height="240dp"
android:foreground="@drawable/bg_settings_banner">
<ImageView
android:id="@+id/photo"
android:layout_width="match_parent"
android:layout_height="140dp"
android:layout_alignParentTop="true"
android:scaleType="centerCrop"
android:importantForAccessibility="no"
tools:src="#0f0"/>
<TextView
android:id="@+id/title"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_below="@id/photo"
android:layout_marginHorizontal="16dp"
android:layout_marginTop="16dp"
android:textAppearance="@style/m3_body_large"
android:paddingVertical="2.5dp"
android:textColor="?colorM3OnSurface"
android:maxLines="2"
android:ellipsize="end"
tools:text="Title title title"/>
<TextView
android:id="@+id/name"
android:layout_width="match_parent"
android:layout_height="20dp"
android:layout_below="@id/title"
android:layout_marginHorizontal="16dp"
android:textAppearance="@style/m3_body_medium"
android:textColor="?colorM3OnSurfaceVariant"
android:singleLine="true"
android:ellipsize="end"
android:gravity="center_vertical"
tools:text="Site Name"/>
</RelativeLayout>

View File

@@ -22,6 +22,7 @@
<item name="list_item_switch" type="id"/>
<item name="list_item_checkbox" type="id"/>
<item name="list_item_radio" type="id"/>
<item name="list_item_account" type="id"/>
<item name="server_about" type="id"/>
<item name="server_rules" type="id"/>

View File

@@ -224,7 +224,7 @@
<string name="visibility_private">Only people mentioned</string>
<string name="search_all">All</string>
<string name="search_people">People</string>
<string name="recent_searches">Recent searches</string>
<string name="recent_searches">Recents</string>
<string name="step_x_of_n">Step %1$d of %2$d</string>
<string name="skip">Skip</string>
<string name="notification_type_follow">New followers</string>
@@ -307,11 +307,12 @@
<string name="file_saved">File saved</string>
<string name="downloading">Downloading…</string>
<string name="no_app_to_handle_action">Theres no app to handle this action</string>
<string name="local_timeline">Community</string>
<string name="trending_posts_info_banner">These are the posts gaining traction in your corner of Mastodon.</string>
<string name="trending_hashtags_info_banner">These are the hashtags gaining traction in your corner of Mastodon.</string>
<string name="trending_links_info_banner">These are the news stories being shared the most in your corner of Mastodon.</string>
<string name="local_timeline_info_banner">These are the most recent posts by the people who use the same Mastodon server as you.</string>
<string name="local_timeline">Local</string>
<string name="trending_posts_info_banner">These are the posts gaining traction across Mastodon.</string>
<string name="trending_links_info_banner">These are the news stories getting talked about on Mastodon.</string>
<!-- %s is the server domain -->
<string name="local_timeline_info_banner">These are all the posts from all users in your server (%s).</string>
<string name="recommended_accounts_info_banner">You might like these accounts based on others you follow.</string>
<string name="dismiss">Dismiss</string>
<string name="see_new_posts">See new posts</string>
<string name="load_missing_posts">Load missing posts</string>
@@ -633,4 +634,11 @@
<string name="downloading_update">Downloading (%d%%)</string>
<!-- Shown like a content warning, %s is the name of the filter -->
<string name="post_matches_filter_x">Matches filter “%s”</string>
<string name="search_mastodon">Search Mastodon</string>
<string name="clear_all">Clear all</string>
<string name="search_open_url">Open URL in Mastodon</string>
<string name="posts_matching_hashtag">Posts with “%s”</string>
<string name="search_go_to_account">Go to %s</string>
<string name="posts_matching_string">Posts with “%s”</string>
<string name="accounts_matching_string">People with “%s”</string>
</resources>

View File

@@ -440,6 +440,7 @@
<item name="android:textSize">16dp</item>
<item name="android:textColor">?android:textColorPrimary</item>
<item name="android:lineSpacingExtra">5dp</item>
<item name="android:lineHeight">24dp</item>
</style>
<style name="m3_body_medium">
@@ -479,6 +480,7 @@
<item name="android:textSize">12dp</item>
<item name="android:textColor">?android:textColorPrimary</item>
<item name="android:lineSpacingMultiplier">1.14</item>
<item name="android:lineHeight">16dp</item>
</style>
<style name="m3_label_large">