Adds a prototype search page. The previous search bar is not yet removed.

This commit is contained in:
Vavassor 2017-06-19 04:18:39 -04:00
parent 00ba3e4df0
commit cc8baac73b
13 changed files with 480 additions and 51 deletions

View File

@ -82,6 +82,12 @@
<activity
android:name="com.theartofdev.edmodo.cropper.CropImageActivity"
android:theme="@style/Base.Theme.AppCompat" />
<activity android:name=".SearchActivity">
<intent-filter>
<action android:name="android.intent.action.SEARCH" />
</intent-filter>
<meta-data android:name="android.app.searchable" android:resource="@xml/searchable" />
</activity>
<receiver android:name=".receiver.NotificationClearBroadcastReceiver" />

View File

@ -293,10 +293,11 @@ public class MainActivity extends BaseActivity {
new PrimaryDrawerItem().withIdentifier(1).withName(getString(R.string.action_view_favourites)).withSelectable(false).withIcon(GoogleMaterial.Icon.gmd_star),
new PrimaryDrawerItem().withIdentifier(2).withName(getString(R.string.action_view_mutes)).withSelectable(false).withIcon(muteDrawable),
new PrimaryDrawerItem().withIdentifier(3).withName(getString(R.string.action_view_blocks)).withSelectable(false).withIcon(GoogleMaterial.Icon.gmd_block),
new PrimaryDrawerItem().withIdentifier(4).withName(getString(R.string.action_search)).withSelectable(false).withIcon(GoogleMaterial.Icon.gmd_search),
new DividerDrawerItem(),
new SecondaryDrawerItem().withIdentifier(4).withName(getString(R.string.action_view_preferences)).withSelectable(false).withIcon(GoogleMaterial.Icon.gmd_settings),
new SecondaryDrawerItem().withIdentifier(5).withName(getString(R.string.about_title_activity)).withSelectable(false).withIcon(GoogleMaterial.Icon.gmd_info),
new SecondaryDrawerItem().withIdentifier(6).withName(getString(R.string.action_logout)).withSelectable(false).withIcon(GoogleMaterial.Icon.gmd_exit_to_app)
new SecondaryDrawerItem().withIdentifier(5).withName(getString(R.string.action_view_preferences)).withSelectable(false).withIcon(GoogleMaterial.Icon.gmd_settings),
new SecondaryDrawerItem().withIdentifier(6).withName(getString(R.string.about_title_activity)).withSelectable(false).withIcon(GoogleMaterial.Icon.gmd_info),
new SecondaryDrawerItem().withIdentifier(7).withName(getString(R.string.action_logout)).withSelectable(false).withIcon(GoogleMaterial.Icon.gmd_exit_to_app)
)
.withOnDrawerItemClickListener(new Drawer.OnDrawerItemClickListener() {
@Override
@ -319,14 +320,17 @@ public class MainActivity extends BaseActivity {
intent.putExtra("type", AccountListActivity.Type.BLOCKS);
startActivity(intent);
} else if (drawerItemIdentifier == 4) {
Intent intent = new Intent(MainActivity.this, PreferencesActivity.class);
Intent intent = new Intent(MainActivity.this, SearchActivity.class);
startActivity(intent);
} else if (drawerItemIdentifier == 5) {
Intent intent = new Intent(MainActivity.this, AboutActivity.class);
Intent intent = new Intent(MainActivity.this, PreferencesActivity.class);
startActivity(intent);
} else if (drawerItemIdentifier == 6) {
logout();
Intent intent = new Intent(MainActivity.this, AboutActivity.class);
startActivity(intent);
} else if (drawerItemIdentifier == 7) {
logout();
} else if (drawerItemIdentifier == 8) {
Intent intent = new Intent(MainActivity.this, AccountListActivity.class);
intent.putExtra("type", AccountListActivity.Type.FOLLOW_REQUESTS);
startActivity(intent);
@ -512,7 +516,7 @@ public class MainActivity extends BaseActivity {
// Show follow requests in the menu, if this is a locked account.
if (me.locked) {
PrimaryDrawerItem followRequestsItem = new PrimaryDrawerItem()
.withIdentifier(7)
.withIdentifier(8)
.withName(R.string.action_view_follow_requests)
.withSelectable(false)
.withIcon(GoogleMaterial.Icon.gmd_person_add);

View File

@ -0,0 +1,195 @@
/* Copyright 2017 Andrew Dawson
*
* This file is a part of Tusky.
*
* This program is free software; you can redistribute it and/or modify it under the terms of the
* GNU General Public License as published by the Free Software Foundation; either version 3 of the
* License, or (at your option) any later version.
*
* Tusky is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even
* the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General
* Public License for more details.
*
* You should have received a copy of the GNU General Public License along with Tusky; if not,
* see <http://www.gnu.org/licenses>. */
package com.keylesspalace.tusky;
import android.app.SearchManager;
import android.app.SearchableInfo;
import android.content.Context;
import android.content.Intent;
import android.os.Bundle;
import android.support.annotation.Nullable;
import android.support.v7.app.ActionBar;
import android.support.v7.widget.LinearLayoutManager;
import android.support.v7.widget.RecyclerView;
import android.support.v7.widget.SearchView;
import android.support.v7.widget.Toolbar;
import android.util.Log;
import android.view.Menu;
import android.view.MenuItem;
import android.view.View;
import android.widget.ProgressBar;
import android.widget.TextView;
import com.keylesspalace.tusky.adapter.SearchResultsAdapter;
import com.keylesspalace.tusky.entity.SearchResults;
import java.util.List;
import butterknife.BindView;
import butterknife.ButterKnife;
import retrofit2.Call;
import retrofit2.Callback;
import retrofit2.Response;
public class SearchActivity extends BaseActivity implements SearchView.OnQueryTextListener {
private static final String TAG = "SearchActivity"; // logging tag
@BindView(R.id.progress_bar) ProgressBar progressBar;
@BindView(R.id.message_no_results) TextView messageNoResults;
private SearchResultsAdapter adapter;
private String currentQuery;
@Override
protected void onCreate(@Nullable Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_search);
ButterKnife.bind(this);
Toolbar toolbar = (Toolbar) findViewById(R.id.toolbar);
setSupportActionBar(toolbar);
ActionBar bar = getSupportActionBar();
if (bar != null) {
bar.setDisplayHomeAsUpEnabled(true);
bar.setDisplayShowHomeEnabled(true);
}
RecyclerView recyclerView = (RecyclerView) findViewById(R.id.recycler_view);
recyclerView.setHasFixedSize(true);
recyclerView.setLayoutManager(new LinearLayoutManager(this));
adapter = new SearchResultsAdapter();
recyclerView.setAdapter(adapter);
handleIntent(getIntent());
}
@Override
protected void onNewIntent(Intent intent) {
super.onNewIntent(intent);
handleIntent(intent);
}
@Override
public boolean onCreateOptionsMenu(Menu menu) {
super.onCreateOptionsMenu(menu);
getMenuInflater().inflate(R.menu.search_toolbar, menu);
SearchView searchView = (SearchView) menu.findItem(R.id.action_search).getActionView();
setupSearchView(searchView);
if (currentQuery != null) {
searchView.setQuery(currentQuery, false);
}
return true;
}
@Override
public boolean onOptionsItemSelected(MenuItem item) {
switch (item.getItemId()) {
case android.R.id.home: {
onBackPressed();
return true;
}
}
return super.onOptionsItemSelected(item);
}
@Override
public boolean onQueryTextChange(String newText) {
return false;
}
@Override
public boolean onQueryTextSubmit(String query) {
return false;
}
private void handleIntent(Intent intent) {
if (Intent.ACTION_SEARCH.equals(intent.getAction())) {
currentQuery = intent.getStringExtra(SearchManager.QUERY);
search(currentQuery);
}
}
private void setupSearchView(SearchView searchView) {
searchView.setIconifiedByDefault(false);
SearchManager searchManager = (SearchManager) getSystemService(Context.SEARCH_SERVICE);
if (searchManager != null) {
List<SearchableInfo> searchables = searchManager.getSearchablesInGlobalSearch();
SearchableInfo searchableInfo = searchManager.getSearchableInfo(getComponentName());
for (SearchableInfo info : searchables) {
if (info.getSuggestAuthority() != null
&& info.getSuggestAuthority().startsWith("applications")) {
searchableInfo = info;
}
}
searchView.setSearchableInfo(searchableInfo);
}
searchView.setOnQueryTextListener(this);
searchView.setFocusable(false);
searchView.setFocusableInTouchMode(false);
}
private void search(String query) {
clearResults();
Callback<SearchResults> callback = new Callback<SearchResults>() {
@Override
public void onResponse(Call<SearchResults> call, Response<SearchResults> response) {
if (response.isSuccessful()) {
SearchResults results = response.body();
if (results.accounts != null || results.hashtags != null) {
adapter.updateSearchResults(results);
hideFeedback();
} else {
displayNoResults();
}
} else {
onSearchFailure();
}
}
@Override
public void onFailure(Call<SearchResults> call, Throwable t) {
onSearchFailure();
}
};
mastodonAPI.search(query, false)
.enqueue(callback);
}
private void onSearchFailure() {
displayNoResults();
Log.e(TAG, "Search request failed.");
}
private void clearResults() {
adapter.updateSearchResults(null);
progressBar.setVisibility(View.VISIBLE);
messageNoResults.setVisibility(View.GONE);
}
private void displayNoResults() {
progressBar.setVisibility(View.GONE);
messageNoResults.setVisibility(View.VISIBLE);
}
private void hideFeedback() {
progressBar.setVisibility(View.GONE);
messageNoResults.setVisibility(View.GONE);
}
}

View File

@ -0,0 +1,51 @@
package com.keylesspalace.tusky.adapter;
import android.content.Context;
import android.support.v7.widget.RecyclerView;
import android.view.View;
import android.widget.TextView;
import com.keylesspalace.tusky.R;
import com.keylesspalace.tusky.entity.Account;
import com.keylesspalace.tusky.interfaces.AccountActionListener;
import com.pkmmte.view.CircularImageView;
import com.squareup.picasso.Picasso;
class AccountViewHolder extends RecyclerView.ViewHolder {
private View container;
private TextView username;
private TextView displayName;
private CircularImageView avatar;
private String id;
AccountViewHolder(View itemView) {
super(itemView);
container = itemView.findViewById(R.id.account_container);
username = (TextView) itemView.findViewById(R.id.account_username);
displayName = (TextView) itemView.findViewById(R.id.account_display_name);
avatar = (CircularImageView) itemView.findViewById(R.id.account_avatar);
}
void setupWithAccount(Account account) {
id = account.id;
String format = username.getContext().getString(R.string.status_username_format);
String formattedUsername = String.format(format, account.username);
username.setText(formattedUsername);
displayName.setText(account.getDisplayName());
Context context = avatar.getContext();
Picasso.with(context)
.load(account.avatar)
.placeholder(R.drawable.avatar_default)
.error(R.drawable.avatar_error)
.into(avatar);
}
void setupActionListener(final AccountActionListener listener) {
container.setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View v) {
listener.onViewAccount(id);
}
});
}
}

View File

@ -15,18 +15,13 @@
package com.keylesspalace.tusky.adapter;
import android.content.Context;
import android.support.v7.widget.RecyclerView;
import android.view.LayoutInflater;
import android.view.View;
import android.view.ViewGroup;
import android.widget.TextView;
import com.keylesspalace.tusky.R;
import com.keylesspalace.tusky.entity.Account;
import com.keylesspalace.tusky.interfaces.AccountActionListener;
import com.pkmmte.view.CircularImageView;
import com.squareup.picasso.Picasso;
/** Both for follows and following lists. */
public class FollowAdapter extends AccountAdapter {
@ -71,43 +66,4 @@ public class FollowAdapter extends AccountAdapter {
return VIEW_TYPE_ACCOUNT;
}
}
private static class AccountViewHolder extends RecyclerView.ViewHolder {
private View container;
private TextView username;
private TextView displayName;
private CircularImageView avatar;
private String id;
AccountViewHolder(View itemView) {
super(itemView);
container = itemView.findViewById(R.id.account_container);
username = (TextView) itemView.findViewById(R.id.account_username);
displayName = (TextView) itemView.findViewById(R.id.account_display_name);
avatar = (CircularImageView) itemView.findViewById(R.id.account_avatar);
}
void setupWithAccount(Account account) {
id = account.id;
String format = username.getContext().getString(R.string.status_username_format);
String formattedUsername = String.format(format, account.username);
username.setText(formattedUsername);
displayName.setText(account.getDisplayName());
Context context = avatar.getContext();
Picasso.with(context)
.load(account.avatar)
.placeholder(R.drawable.avatar_default)
.error(R.drawable.avatar_error)
.into(avatar);
}
void setupActionListener(final AccountActionListener listener) {
container.setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View v) {
listener.onViewAccount(id);
}
});
}
}
}

View File

@ -0,0 +1,115 @@
/* Copyright 2017 Andrew Dawson
*
* This file is a part of Tusky.
*
* This program is free software; you can redistribute it and/or modify it under the terms of the
* GNU General Public License as published by the Free Software Foundation; either version 3 of the
* License, or (at your option) any later version.
*
* Tusky is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even
* the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General
* Public License for more details.
*
* You should have received a copy of the GNU General Public License along with Tusky; if not,
* see <http://www.gnu.org/licenses>. */
package com.keylesspalace.tusky.adapter;
import android.support.v7.widget.RecyclerView;
import android.view.LayoutInflater;
import android.view.View;
import android.view.ViewGroup;
import android.widget.TextView;
import com.keylesspalace.tusky.R;
import com.keylesspalace.tusky.entity.Account;
import com.keylesspalace.tusky.entity.SearchResults;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.List;
public class SearchResultsAdapter extends RecyclerView.Adapter {
private static final int VIEW_TYPE_ACCOUNT = 0;
private static final int VIEW_TYPE_HASHTAG = 1;
private List<Account> accountList;
private List<String> hashtagList;
public SearchResultsAdapter() {
super();
accountList = new ArrayList<>();
hashtagList = new ArrayList<>();
}
@Override
public RecyclerView.ViewHolder onCreateViewHolder(ViewGroup parent, int viewType) {
switch (viewType) {
default:
case VIEW_TYPE_ACCOUNT: {
View view = LayoutInflater.from(parent.getContext())
.inflate(R.layout.item_account, parent, false);
return new AccountViewHolder(view);
}
case VIEW_TYPE_HASHTAG: {
View view = LayoutInflater.from(parent.getContext())
.inflate(R.layout.item_hashtag, parent, false);
return new HashtagViewHolder(view);
}
}
}
@Override
public void onBindViewHolder(RecyclerView.ViewHolder viewHolder, int position) {
if (position < accountList.size()) {
AccountViewHolder holder = (AccountViewHolder) viewHolder;
holder.setupWithAccount(accountList.get(position));
} else {
HashtagViewHolder holder = (HashtagViewHolder) viewHolder;
int index = position - accountList.size();
holder.setHashtag(hashtagList.get(index));
}
}
@Override
public int getItemCount() {
return accountList.size() + hashtagList.size();
}
@Override
public int getItemViewType(int position) {
if (position >= accountList.size()) {
return VIEW_TYPE_HASHTAG;
} else {
return VIEW_TYPE_ACCOUNT;
}
}
public void updateSearchResults(SearchResults results) {
if (results != null) {
if (results.accounts != null) {
accountList.addAll(Arrays.asList(results.accounts));
}
if (results.hashtags != null) {
hashtagList.addAll(Arrays.asList(results.hashtags));
}
} else {
accountList.clear();
hashtagList.clear();
}
notifyDataSetChanged();
}
private static class HashtagViewHolder extends RecyclerView.ViewHolder {
private TextView hashtag;
HashtagViewHolder(View itemView) {
super(itemView);
hashtag = (TextView) itemView.findViewById(R.id.hashtag);
}
void setHashtag(String tag) {
hashtag.setText(String.format("#%s", tag));
}
}
}

View File

@ -0,0 +1,22 @@
/* Copyright 2017 Andrew Dawson
*
* This file is a part of Tusky.
*
* This program is free software; you can redistribute it and/or modify it under the terms of the
* GNU General Public License as published by the Free Software Foundation; either version 3 of the
* License, or (at your option) any later version.
*
* Tusky is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even
* the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General
* Public License for more details.
*
* You should have received a copy of the GNU General Public License along with Tusky; if not,
* see <http://www.gnu.org/licenses>. */
package com.keylesspalace.tusky.entity;
public class SearchResults {
public Account[] accounts;
public Status[] statuses;
public String[] hashtags;
}

View File

@ -22,6 +22,7 @@ import com.keylesspalace.tusky.entity.Media;
import com.keylesspalace.tusky.entity.Notification;
import com.keylesspalace.tusky.entity.Profile;
import com.keylesspalace.tusky.entity.Relationship;
import com.keylesspalace.tusky.entity.SearchResults;
import com.keylesspalace.tusky.entity.Status;
import com.keylesspalace.tusky.entity.StatusContext;
@ -191,6 +192,9 @@ public interface MastodonAPI {
@POST("api/v1/reports")
Call<ResponseBody> report(@Field("account_id") String accountId, @Field("status_ids[]") List<String> statusIds, @Field("comment") String comment);
@GET("api/v1/search")
Call<SearchResults> search(@Query("q") String q, @Query("resolve") Boolean resolve);
@FormUrlEncoded
@POST("api/v1/apps")
Call<AppCredentials> authenticateApp(

View File

@ -0,0 +1,50 @@
<?xml version="1.0" encoding="utf-8"?>
<android.support.design.widget.CoordinatorLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent"
android:layout_height="match_parent"
xmlns:app="http://schemas.android.com/apk/res-auto"
tools:context="com.keylesspalace.tusky.SearchActivity">
<LinearLayout
android:layout_width="match_parent"
android:layout_height="match_parent"
android:orientation="vertical">
<android.support.v7.widget.Toolbar
android:id="@+id/toolbar"
android:layout_width="match_parent"
android:layout_height="?attr/actionBarSize"
android:background="?attr/toolbar_background_color"
app:navigationIcon="?attr/homeAsUpIndicator" />
<RelativeLayout
android:layout_width="match_parent"
android:layout_height="match_parent">
<android.support.v7.widget.RecyclerView
android:layout_width="match_parent"
android:layout_height="match_parent"
android:id="@+id/recycler_view" />
<ProgressBar
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:id="@+id/progress_bar"
android:layout_centerInParent="true"
android:indeterminate="true"
android:visibility="gone" />
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:id="@+id/message_no_results"
android:text="@string/search_no_results"
android:layout_centerInParent="true"
android:visibility="gone" />
</RelativeLayout>
</LinearLayout>
</android.support.design.widget.CoordinatorLayout>

View File

@ -0,0 +1,7 @@
<?xml version="1.0" encoding="utf-8"?>
<TextView
xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:id="@+id/hashtag"
android:padding="8dp" />

View File

@ -0,0 +1,10 @@
<?xml version="1.0" encoding="utf-8"?>
<menu xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto">
<item
android:id="@+id/action_search"
android:title="@string/action_search"
android:icon="@android:drawable/ic_menu_search"
app:actionViewClass="android.support.v7.widget.SearchView"
app:showAsAction="always" />
</menu>

View File

@ -100,6 +100,7 @@
<string name="action_undo">Undo</string>
<string name="action_accept">Accept</string>
<string name="action_reject">Reject</string>
<string name="action_search">Search</string>
<string name="send_status_link_to">Share toot URL to…</string>
<string name="send_status_content_to">Share toot to…</string>
@ -116,6 +117,9 @@
<string name="hint_content_warning">Content warning</string>
<string name="hint_display_name">Display name</string>
<string name="hint_note">Bio</string>
<string name="hint_search">Search accounts and tags…</string>
<string name="search_no_results">No results</string>
<string name="label_avatar">Avatar</string>
<string name="label_header">Header</string>

View File

@ -0,0 +1,5 @@
<?xml version="1.0" encoding="utf-8"?>
<searchable xmlns:android="http://schemas.android.com/apk/res/android"
android:label="@string/app_name"
android:hint="@string/hint_search">
</searchable>