Add searching

filtering
Floens 10 years ago
parent 94d17a4c80
commit 8363c981a3
  1. 21
      Clover/app/src/main/java/org/floens/chan/controller/NavigationController.java
  2. 75
      Clover/app/src/main/java/org/floens/chan/core/presenter/ThreadPresenter.java
  3. 158
      Clover/app/src/main/java/org/floens/chan/ui/adapter/PostAdapter.java
  4. 2
      Clover/app/src/main/java/org/floens/chan/ui/cell/ThreadStatusCell.java
  5. 4
      Clover/app/src/main/java/org/floens/chan/ui/controller/BrowseController.java
  6. 2
      Clover/app/src/main/java/org/floens/chan/ui/controller/PostRepliesController.java
  7. 25
      Clover/app/src/main/java/org/floens/chan/ui/controller/RootNavigationController.java
  8. 14
      Clover/app/src/main/java/org/floens/chan/ui/controller/ThreadController.java
  9. 4
      Clover/app/src/main/java/org/floens/chan/ui/controller/ViewThreadController.java
  10. 5
      Clover/app/src/main/java/org/floens/chan/ui/fragment/ThreadFragment.java
  11. 2
      Clover/app/src/main/java/org/floens/chan/ui/helper/PostPopupHelper.java
  12. 17
      Clover/app/src/main/java/org/floens/chan/ui/layout/ThreadLayout.java
  13. 53
      Clover/app/src/main/java/org/floens/chan/ui/layout/ThreadListLayout.java
  14. 173
      Clover/app/src/main/java/org/floens/chan/ui/toolbar/Toolbar.java
  15. 26
      Clover/app/src/main/java/org/floens/chan/ui/view/LoadView.java
  16. 8
      Clover/app/src/main/java/org/floens/chan/utils/AndroidUtils.java
  17. BIN
      Clover/app/src/main/res/drawable-hdpi/ic_close_white_24dp.png
  18. BIN
      Clover/app/src/main/res/drawable-mdpi/ic_close_white_24dp.png
  19. BIN
      Clover/app/src/main/res/drawable-xhdpi/ic_close_white_24dp.png
  20. BIN
      Clover/app/src/main/res/drawable-xxhdpi/ic_close_white_24dp.png
  21. BIN
      Clover/app/src/main/res/drawable-xxxhdpi/ic_close_white_24dp.png
  22. 16
      Clover/app/src/main/res/layout/layout_thread_list.xml
  23. 11
      Clover/app/src/main/res/values/strings.xml

@ -160,6 +160,10 @@ public abstract class NavigationController extends Controller implements Control
public boolean onBack() { public boolean onBack() {
if (blockingInput) return true; if (blockingInput) return true;
if (toolbar.closeSearch()) {
return true;
}
if (controllerList.size() > 0) { if (controllerList.size() > 0) {
Controller top = controllerList.get(controllerList.size() - 1); Controller top = controllerList.get(controllerList.size() - 1);
if (top.onBack()) { if (top.onBack()) {
@ -190,6 +194,10 @@ public abstract class NavigationController extends Controller implements Control
} }
public void showSearch() {
toolbar.showSearch();
}
@Override @Override
public void onMenuOrBackClicked(boolean isArrow) { public void onMenuOrBackClicked(boolean isArrow) {
if (isArrow) { if (isArrow) {
@ -198,4 +206,17 @@ public abstract class NavigationController extends Controller implements Control
onMenuClicked(); onMenuClicked();
} }
} }
@Override
public void onSearchVisibilityChanged(boolean visible) {
}
@Override
public String getSearchHint() {
return "";
}
@Override
public void onSearchEntered(String entered) {
}
} }

@ -44,12 +44,14 @@ import org.floens.chan.utils.AndroidUtils;
import java.util.ArrayList; import java.util.ArrayList;
import java.util.List; import java.util.List;
import java.util.Locale;
public class ThreadPresenter implements ChanLoader.ChanLoaderCallback, PostAdapter.PostAdapterCallback, PostView.PostViewCallback, ThreadStatusCell.Callback { public class ThreadPresenter implements ChanLoader.ChanLoaderCallback, PostAdapter.PostAdapterCallback, PostView.PostViewCallback, ThreadStatusCell.Callback {
private ThreadPresenterCallback threadPresenterCallback; private ThreadPresenterCallback threadPresenterCallback;
private Loadable loadable; private Loadable loadable;
private ChanLoader chanLoader; private ChanLoader chanLoader;
private boolean searchOpen = false;
public ThreadPresenter(ThreadPresenterCallback threadPresenterCallback) { public ThreadPresenter(ThreadPresenterCallback threadPresenterCallback) {
this.threadPresenterCallback = threadPresenterCallback; this.threadPresenterCallback = threadPresenterCallback;
@ -112,6 +114,21 @@ public class ThreadPresenter implements ChanLoader.ChanLoaderCallback, PostAdapt
return ChanApplication.getWatchManager().findPinByLoadable(loadable) != null; return ChanApplication.getWatchManager().findPinByLoadable(loadable) != null;
} }
public void onSearchVisibilityChanged(boolean visible) {
searchOpen = visible;
threadPresenterCallback.showSearch(visible);
}
public void onSearchEntered(String entered) {
if (chanLoader.getThread() != null) {
if (TextUtils.isEmpty(entered)) {
threadPresenterCallback.filterList(null, null, true, true, false);
} else {
processSearch(chanLoader.getThread().posts, entered);
}
}
}
@Override @Override
public Loadable getLoadable() { public Loadable getLoadable() {
return loadable; return loadable;
@ -134,11 +151,6 @@ public class ThreadPresenter implements ChanLoader.ChanLoaderCallback, PostAdapt
/* /*
* PostAdapter callbacks * PostAdapter callbacks
*/ */
@Override
public void onFilteredResults(String filter, int count, boolean all) {
}
@Override @Override
public void onListScrolledToBottom() { public void onListScrolledToBottom() {
if (loadable.isThreadMode()) { if (loadable.isThreadMode()) {
@ -153,12 +165,12 @@ public class ThreadPresenter implements ChanLoader.ChanLoaderCallback, PostAdapt
} }
} }
@Override public void scrollTo(int position, boolean smooth) {
public void scrollTo(int position) { threadPresenterCallback.scrollTo(position, smooth);
threadPresenterCallback.scrollTo(position);
} }
public void scrollTo(PostImage postImage) { public void scrollToImage(PostImage postImage, boolean smooth) {
if (!searchOpen) {
int position = -1; int position = -1;
for (int i = 0; i < chanLoader.getThread().posts.size(); i++) { for (int i = 0; i < chanLoader.getThread().posts.size(); i++) {
Post post = chanLoader.getThread().posts.get(i); Post post = chanLoader.getThread().posts.get(i);
@ -167,10 +179,11 @@ public class ThreadPresenter implements ChanLoader.ChanLoaderCallback, PostAdapt
break; break;
} }
} }
scrollTo(position); scrollTo(position, smooth);
}
} }
public void scrollToPost(Post needle) { public void scrollToPost(Post needle, boolean smooth) {
int position = -1; int position = -1;
for (int i = 0; i < chanLoader.getThread().posts.size(); i++) { for (int i = 0; i < chanLoader.getThread().posts.size(); i++) {
Post post = chanLoader.getThread().posts.get(i); Post post = chanLoader.getThread().posts.get(i);
@ -179,7 +192,7 @@ public class ThreadPresenter implements ChanLoader.ChanLoaderCallback, PostAdapt
break; break;
} }
} }
scrollTo(position); scrollTo(position, smooth);
} }
public void highlightPost(Post post) { public void highlightPost(Post post) {
@ -195,6 +208,12 @@ public class ThreadPresenter implements ChanLoader.ChanLoaderCallback, PostAdapt
Loadable threadLoadable = new Loadable(post.board, post.no); Loadable threadLoadable = new Loadable(post.board, post.no);
threadLoadable.generateTitle(post); threadLoadable.generateTitle(post);
threadPresenterCallback.showThread(threadLoadable); threadPresenterCallback.showThread(threadLoadable);
} else {
if (searchOpen) {
threadPresenterCallback.filterList(null, null, true, false, true);
highlightPost(post);
scrollToPost(post, false);
}
} }
} }
@ -304,6 +323,7 @@ public class ThreadPresenter implements ChanLoader.ChanLoaderCallback, PostAdapt
@Override @Override
public void onShowPostReplies(Post post) { public void onShowPostReplies(Post post) {
List<Post> posts = new ArrayList<>(); List<Post> posts = new ArrayList<>();
for (int no : post.repliesFrom) { for (int no : post.repliesFrom) {
Post replyPost = findPostById(no); Post replyPost = findPostById(no);
@ -382,6 +402,31 @@ public class ThreadPresenter implements ChanLoader.ChanLoaderCallback, PostAdapt
return null; return null;
} }
private void processSearch(List<Post> all, String originalQuery) {
List<Post> filtered = new ArrayList<>();
String query = originalQuery.toLowerCase(Locale.ENGLISH);
boolean add;
for (Post item : all) {
add = false;
if (item.comment.toString().toLowerCase(Locale.ENGLISH).contains(query)) {
add = true;
} else if (item.subject.toLowerCase(Locale.ENGLISH).contains(query)) {
add = true;
} else if (item.name.toLowerCase(Locale.ENGLISH).contains(query)) {
add = true;
} else if (item.filename != null && item.filename.toLowerCase(Locale.ENGLISH).contains(query)) {
add = true;
}
if (add) {
filtered.add(item);
}
}
threadPresenterCallback.filterList(originalQuery, filtered, false, false, false);
}
public interface ThreadPresenterCallback { public interface ThreadPresenterCallback {
void showPosts(ChanThread thread); void showPosts(ChanThread thread);
@ -403,10 +448,14 @@ public class ThreadPresenter implements ChanLoader.ChanLoaderCallback, PostAdapt
void showImages(List<PostImage> images, int index, Loadable loadable, ThumbnailView thumbnail); void showImages(List<PostImage> images, int index, Loadable loadable, ThumbnailView thumbnail);
void scrollTo(int position); void scrollTo(int position, boolean smooth);
void highlightPost(Post post); void highlightPost(Post post);
void highlightPostId(String id); void highlightPostId(String id);
void showSearch(boolean show);
void filterList(String query, List<Post> filter, boolean clearFilter, boolean setEmptyText, boolean hideKeyboard);
} }
} }

@ -18,7 +18,6 @@
package org.floens.chan.ui.adapter; package org.floens.chan.ui.adapter;
import android.support.v7.widget.RecyclerView; import android.support.v7.widget.RecyclerView;
import android.text.TextUtils;
import android.view.LayoutInflater; import android.view.LayoutInflater;
import android.view.ViewGroup; import android.view.ViewGroup;
@ -45,10 +44,9 @@ public class PostAdapter extends RecyclerView.Adapter<RecyclerView.ViewHolder> {
private final List<Post> displayList = new ArrayList<>(); private final List<Post> displayList = new ArrayList<>();
private int lastPostCount = 0; private int lastPostCount = 0;
private String error = null; private String error = null;
private String filter = "";
private int pendingScrollToPost = -1;
private Post highlightedPost; private Post highlightedPost;
private String highlightedPostId; private String highlightedPostId;
private boolean filtering = false;
public PostAdapter(RecyclerView recyclerView, PostAdapterCallback postAdapterCallback, PostView.PostViewCallback postViewCallback, ThreadStatusCell.Callback statusCellCallback) { public PostAdapter(RecyclerView recyclerView, PostAdapterCallback postAdapterCallback, PostView.PostViewCallback postViewCallback, ThreadStatusCell.Callback statusCellCallback) {
this.recyclerView = recyclerView; this.recyclerView = recyclerView;
@ -121,8 +119,10 @@ public class PostAdapter extends RecyclerView.Adapter<RecyclerView.ViewHolder> {
sourceList.clear(); sourceList.clear();
sourceList.addAll(thread.posts); sourceList.addAll(thread.posts);
if (!filtering) {
displayList.clear(); displayList.clear();
displayList.addAll(sourceList); displayList.addAll(sourceList);
}
// Update all, recyclerview will figure out all the animations // Update all, recyclerview will figure out all the animations
notifyDataSetChanged(); notifyDataSetChanged();
@ -130,6 +130,7 @@ public class PostAdapter extends RecyclerView.Adapter<RecyclerView.ViewHolder> {
public void cleanup() { public void cleanup() {
highlightedPost = null; highlightedPost = null;
filtering = false;
sourceList.clear(); sourceList.clear();
displayList.clear(); displayList.clear();
lastPostCount = 0; lastPostCount = 0;
@ -148,6 +149,33 @@ public class PostAdapter extends RecyclerView.Adapter<RecyclerView.ViewHolder> {
} }
} }
public void filterList(List<Post> filter) {
filtering = true;
displayList.clear();
for (Post item : sourceList) {
for (Post filterItem : filter) {
if (filterItem.no == item.no) {
displayList.add(item);
break;
}
}
}
notifyDataSetChanged();
}
public void clearFilter() {
if (filtering) {
filtering = false;
displayList.clear();
displayList.addAll(sourceList);
notifyDataSetChanged();
}
}
public void highlightPost(Post post) { public void highlightPost(Post post) {
highlightedPostId = null; highlightedPostId = null;
highlightedPost = post; highlightedPost = post;
@ -161,7 +189,7 @@ public class PostAdapter extends RecyclerView.Adapter<RecyclerView.ViewHolder> {
} }
private void onScrolledToBottom() { private void onScrolledToBottom() {
if (lastPostCount != sourceList.size()) { if (!filtering && lastPostCount != sourceList.size()) {
lastPostCount = sourceList.size(); lastPostCount = sourceList.size();
postAdapterCallback.onListScrolledToBottom(); postAdapterCallback.onListScrolledToBottom();
} }
@ -171,10 +199,6 @@ public class PostAdapter extends RecyclerView.Adapter<RecyclerView.ViewHolder> {
return postAdapterCallback.getLoadable().isThreadMode(); return postAdapterCallback.getLoadable().isThreadMode();
} }
private boolean isFiltering() {
return !TextUtils.isEmpty(filter);
}
public static class PostViewHolder extends RecyclerView.ViewHolder { public static class PostViewHolder extends RecyclerView.ViewHolder {
private PostView postView; private PostView postView;
@ -193,127 +217,9 @@ public class PostAdapter extends RecyclerView.Adapter<RecyclerView.ViewHolder> {
} }
} }
/*
@Override
public long getItemId(int position) {
return position;
}
@Override
public View getView(int position, View convertView, ViewGroup parent) {
if (position >= getCount() - 1) {
onScrolledToBottom();
}
switch (getItemViewType(position)) {
case VIEW_TYPE_ITEM: {
if (convertView == null || convertView.getTag() == null || (Integer) convertView.getTag() != VIEW_TYPE_ITEM) {
convertView = new PostView(context);
convertView.setTag(VIEW_TYPE_ITEM);
}
PostView postView = (PostView) convertView;
postView.setPost(getItem(position), postViewCallback);
return postView;
}
case VIEW_TYPE_STATUS: {
return new StatusView(context);
}
}
return null;
}
public Filter getFilter() {
return new Filter() {
@Override
protected FilterResults performFiltering(CharSequence constraintRaw) {
FilterResults results = new FilterResults();
if (TextUtils.isEmpty(constraintRaw)) {
ArrayList<Post> tmp;
synchronized (lock) {
tmp = new ArrayList<>(sourceList);
}
results.values = tmp;
} else {
List<Post> all;
synchronized (lock) {
all = new ArrayList<>(sourceList);
}
List<Post> accepted = new ArrayList<>();
String constraint = constraintRaw.toString().toLowerCase(Locale.ENGLISH);
for (Post post : all) {
if (post.comment.toString().toLowerCase(Locale.ENGLISH).contains(constraint) ||
post.subject.toLowerCase(Locale.ENGLISH).contains(constraint)) {
accepted.add(post);
}
}
results.values = accepted;
}
return results;
}
@SuppressWarnings("unchecked")
@Override
protected void publishResults(CharSequence constraint, final FilterResults results) {
filter = constraint.toString();
synchronized (lock) {
displayList.clear();
displayList.addAll((List<Post>) results.values);
}
notifyDataSetChanged();
postAdapterCallback.onFilteredResults(filter, ((List<Post>) results.values).size(), TextUtils.isEmpty(filter));
if (pendingScrollToPost >= 0) {
final int to = pendingScrollToPost;
pendingScrollToPost = -1;
postAdapterCallback.scrollTo(to);
}
}
};
}
public void setFilter(String filter) {
getFilter().filter(filter);
notifyDataSetChanged();
}
public void setThread(ChanThread thread) {
synchronized (lock) {
if (thread.archived) {
statusPrefix = context.getString(R.string.thread_archived) + " - ";
} else if (thread.closed) {
statusPrefix = context.getString(R.string.thread_closed) + " - ";
} else {
statusPrefix = "";
}
sourceList.clear();
sourceList.addAll(thread.posts);
if (!isFiltering()) {
displayList.clear();
displayList.addAll(sourceList);
} else {
setFilter(filter);
}
}
notifyDataSetChanged();
}*/
public interface PostAdapterCallback { public interface PostAdapterCallback {
void onFilteredResults(String filter, int count, boolean all);
Loadable getLoadable(); Loadable getLoadable();
void onListScrolledToBottom(); void onListScrolledToBottom();
void scrollTo(int position);
} }
} }

@ -13,6 +13,7 @@ import org.floens.chan.R;
import org.floens.chan.core.model.ChanThread; import org.floens.chan.core.model.ChanThread;
import org.floens.chan.core.model.Post; import org.floens.chan.core.model.Post;
import static org.floens.chan.utils.AndroidUtils.ROBOTO_MEDIUM;
import static org.floens.chan.utils.AndroidUtils.getAttrDrawable; import static org.floens.chan.utils.AndroidUtils.getAttrDrawable;
public class ThreadStatusCell extends LinearLayout implements View.OnClickListener { public class ThreadStatusCell extends LinearLayout implements View.OnClickListener {
@ -55,6 +56,7 @@ public class ThreadStatusCell extends LinearLayout implements View.OnClickListen
protected void onFinishInflate() { protected void onFinishInflate() {
super.onFinishInflate(); super.onFinishInflate();
text = (TextView) findViewById(R.id.text); text = (TextView) findViewById(R.id.text);
text.setTypeface(ROBOTO_MEDIUM);
setOnClickListener(this); setOnClickListener(this);
} }

@ -42,7 +42,7 @@ import org.floens.chan.utils.AndroidUtils;
import java.util.ArrayList; import java.util.ArrayList;
import java.util.List; import java.util.List;
public class BrowseController extends ThreadController implements ToolbarMenuItem.ToolbarMenuItemCallback, ThreadLayout.ThreadLayoutCallback, FloatingMenu.FloatingMenuCallback, RootNavigationController.DrawerCallbacks { public class BrowseController extends ThreadController implements ToolbarMenuItem.ToolbarMenuItemCallback, ThreadLayout.ThreadLayoutCallback, FloatingMenu.FloatingMenuCallback {
private static final int REFRESH_ID = 1; private static final int REFRESH_ID = 1;
private static final int POST_ID = 2; private static final int POST_ID = 2;
private static final int SEARCH_ID = 101; private static final int SEARCH_ID = 101;
@ -99,7 +99,7 @@ public class BrowseController extends ThreadController implements ToolbarMenuIte
public void onSubMenuItemClicked(ToolbarMenuItem parent, FloatingMenuItem item) { public void onSubMenuItemClicked(ToolbarMenuItem parent, FloatingMenuItem item) {
switch ((Integer) item.getId()) { switch ((Integer) item.getId()) {
case SEARCH_ID: case SEARCH_ID:
// TODO navigationController.showSearch();
break; break;
case SHARE_ID: case SHARE_ID:
String link = ChanUrls.getCatalogUrlDesktop(threadLayout.getPresenter().getLoadable().board); String link = ChanUrls.getCatalogUrlDesktop(threadLayout.getPresenter().getLoadable().board);

@ -54,7 +54,7 @@ public class PostRepliesController extends Controller {
view.setOnClickListener(new View.OnClickListener() { view.setOnClickListener(new View.OnClickListener() {
@Override @Override
public void onClick(View v) { public void onClick(View v) {
postPopupHelper.popAll(); postPopupHelper.pop();
} }
}); });

@ -188,9 +188,34 @@ public class RootNavigationController extends NavigationController implements Pi
} }
} }
@Override
public String getSearchHint() {
return context.getString(R.string.search_hint);
}
@Override
public void onSearchVisibilityChanged(boolean visible) {
Controller top = getTop();
if (top instanceof DrawerCallbacks) {
((DrawerCallbacks) top).onSearchVisibilityChanged(visible);
}
}
@Override
public void onSearchEntered(String entered) {
Controller top = getTop();
if (top instanceof DrawerCallbacks) {
((DrawerCallbacks) top).onSearchEntered(entered);
}
}
public interface DrawerCallbacks { public interface DrawerCallbacks {
void onPinClicked(Pin pin); void onPinClicked(Pin pin);
boolean isPinCurrent(Pin pin); boolean isPinCurrent(Pin pin);
void onSearchVisibilityChanged(boolean visible);
void onSearchEntered(String entered);
} }
} }

@ -13,7 +13,7 @@ import java.util.List;
import de.greenrobot.event.EventBus; import de.greenrobot.event.EventBus;
public abstract class ThreadController extends Controller implements ThreadLayout.ThreadLayoutCallback, ImageViewerController.PreviewCallback { public abstract class ThreadController extends Controller implements ThreadLayout.ThreadLayoutCallback, ImageViewerController.PreviewCallback, RootNavigationController.DrawerCallbacks {
protected ThreadLayout threadLayout; protected ThreadLayout threadLayout;
public ThreadController(Context context) { public ThreadController(Context context) {
@ -75,11 +75,21 @@ public abstract class ThreadController extends Controller implements ThreadLayou
public void scrollToImage(PostImage postImage) { public void scrollToImage(PostImage postImage) {
if (!threadLayout.postRepliesOpen()) { if (!threadLayout.postRepliesOpen()) {
threadLayout.getPresenter().scrollTo(postImage); threadLayout.getPresenter().scrollToImage(postImage, true);
} }
} }
@Override @Override
public void onShowPosts() { public void onShowPosts() {
} }
@Override
public void onSearchVisibilityChanged(boolean visible) {
threadLayout.getPresenter().onSearchVisibilityChanged(visible);
}
@Override
public void onSearchEntered(String entered) {
threadLayout.getPresenter().onSearchEntered(entered);
}
} }

@ -35,7 +35,7 @@ import org.floens.chan.utils.AndroidUtils;
import java.util.Arrays; import java.util.Arrays;
public class ViewThreadController extends ThreadController implements ThreadLayout.ThreadLayoutCallback, ToolbarMenuItem.ToolbarMenuItemCallback, RootNavigationController.DrawerCallbacks { public class ViewThreadController extends ThreadController implements ThreadLayout.ThreadLayoutCallback, ToolbarMenuItem.ToolbarMenuItemCallback {
private static final int POST_ID = 1; private static final int POST_ID = 1;
private static final int PIN_ID = 2; private static final int PIN_ID = 2;
private static final int REFRESH_ID = 101; private static final int REFRESH_ID = 101;
@ -166,7 +166,7 @@ public class ViewThreadController extends ThreadController implements ThreadLayo
threadLayout.getPresenter().requestData(); threadLayout.getPresenter().requestData();
break; break;
case SEARCH_ID: case SEARCH_ID:
// TODO navigationController.showSearch();
break; break;
case SHARE_ID: case SHARE_ID:
Loadable loadable = threadLayout.getPresenter().getLoadable(); Loadable loadable = threadLayout.getPresenter().getLoadable();

@ -320,11 +320,6 @@ public class ThreadFragment extends Fragment implements ThreadManager.ThreadMana
} }
@Override
public void scrollTo(int position) {
}
private RelativeLayout createView() { private RelativeLayout createView() {
RelativeLayout compound = new RelativeLayout(getActivity()); RelativeLayout compound = new RelativeLayout(getActivity());

@ -82,7 +82,7 @@ public class PostPopupHelper {
public void postClicked(Post p) { public void postClicked(Post p) {
popAll(); popAll();
presenter.highlightPost(p); presenter.highlightPost(p);
presenter.scrollToPost(p); presenter.scrollToPost(p, true);
} }
private void dismiss() { private void dismiss() {

@ -72,7 +72,7 @@ public class ThreadLayout extends LoadView implements ThreadPresenter.ThreadPres
private TextView errorText; private TextView errorText;
private Button errorRetryButton; private Button errorRetryButton;
private PostPopupHelper postPopupHelper; private PostPopupHelper postPopupHelper;
private Visible visible = Visible.LOADING; private Visible visible;
public ThreadLayout(Context context) { public ThreadLayout(Context context) {
super(context); super(context);
@ -220,8 +220,8 @@ public class ThreadLayout extends LoadView implements ThreadPresenter.ThreadPres
} }
@Override @Override
public void scrollTo(int position) { public void scrollTo(int position, boolean smooth) {
threadListLayout.scrollTo(position); threadListLayout.scrollTo(position, smooth);
} }
@Override @Override
@ -234,6 +234,15 @@ public class ThreadLayout extends LoadView implements ThreadPresenter.ThreadPres
threadListLayout.highlightPostId(id); threadListLayout.highlightPostId(id);
} }
@Override
public void showSearch(boolean show) {
threadListLayout.showSearch(show);
}
public void filterList(String query, List<Post> filter, boolean clearFilter, boolean setEmptyText, boolean hideKeyboard) {
threadListLayout.filterList(query, filter, clearFilter, setEmptyText, hideKeyboard);
}
public ThumbnailView getThumbnail(PostImage postImage) { public ThumbnailView getThumbnail(PostImage postImage) {
if (postPopupHelper.isOpen()) { if (postPopupHelper.isOpen()) {
return postPopupHelper.getThumbnail(postImage); return postPopupHelper.getThumbnail(postImage);
@ -248,11 +257,13 @@ public class ThreadLayout extends LoadView implements ThreadPresenter.ThreadPres
private void switchVisible(Visible visible) { private void switchVisible(Visible visible) {
if (this.visible != visible) { if (this.visible != visible) {
if (this.visible != null) {
switch (this.visible) { switch (this.visible) {
case THREAD: case THREAD:
threadListLayout.cleanup(); threadListLayout.cleanup();
break; break;
} }
}
this.visible = visible; this.visible = visible;
switch (visible) { switch (visible) {

@ -22,7 +22,8 @@ import android.support.v7.widget.LinearLayoutManager;
import android.support.v7.widget.RecyclerView; import android.support.v7.widget.RecyclerView;
import android.util.AttributeSet; import android.util.AttributeSet;
import android.view.View; import android.view.View;
import android.widget.RelativeLayout; import android.widget.LinearLayout;
import android.widget.TextView;
import org.floens.chan.R; import org.floens.chan.R;
import org.floens.chan.core.model.ChanThread; import org.floens.chan.core.model.ChanThread;
@ -32,11 +33,18 @@ import org.floens.chan.ui.adapter.PostAdapter;
import org.floens.chan.ui.cell.ThreadStatusCell; import org.floens.chan.ui.cell.ThreadStatusCell;
import org.floens.chan.ui.view.PostView; import org.floens.chan.ui.view.PostView;
import org.floens.chan.ui.view.ThumbnailView; import org.floens.chan.ui.view.ThumbnailView;
import org.floens.chan.utils.AndroidUtils;
import org.floens.chan.utils.AnimationUtils;
import java.util.List;
import static org.floens.chan.utils.AndroidUtils.ROBOTO_MEDIUM;
/** /**
* A layout that wraps around a {@link RecyclerView} to manage showing posts. * A layout that wraps around a {@link RecyclerView} to manage showing posts.
*/ */
public class ThreadListLayout extends RelativeLayout { public class ThreadListLayout extends LinearLayout {
private TextView searchStatus;
private RecyclerView recyclerView; private RecyclerView recyclerView;
private PostAdapter postAdapter; private PostAdapter postAdapter;
private PostAdapter.PostAdapterCallback postAdapterCallback; private PostAdapter.PostAdapterCallback postAdapterCallback;
@ -49,6 +57,10 @@ public class ThreadListLayout extends RelativeLayout {
@Override @Override
protected void onFinishInflate() { protected void onFinishInflate() {
super.onFinishInflate(); super.onFinishInflate();
searchStatus = (TextView) findViewById(R.id.search_status);
searchStatus.setTypeface(ROBOTO_MEDIUM);
recyclerView = (RecyclerView) findViewById(R.id.recycler_view); recyclerView = (RecyclerView) findViewById(R.id.recycler_view);
LinearLayoutManager lm = new LinearLayoutManager(getContext()); LinearLayoutManager lm = new LinearLayoutManager(getContext());
recyclerView.setLayoutManager(lm); recyclerView.setLayoutManager(lm);
@ -77,6 +89,37 @@ public class ThreadListLayout extends RelativeLayout {
postAdapter.showError(error); postAdapter.showError(error);
} }
public void showSearch(boolean show) {
AnimationUtils.animateHeight(searchStatus, show);
if (show) {
searchStatus.setText(R.string.search_empty);
} else {
postAdapter.clearFilter();
recyclerView.scrollToPosition(0);
}
}
public void filterList(String query, List<Post> filter, boolean clearFilter, boolean setEmptyText, boolean hideKeyboard) {
if (clearFilter) {
postAdapter.clearFilter();
}
if (hideKeyboard) {
AndroidUtils.hideKeyboard(this);
}
if (setEmptyText) {
searchStatus.setText(R.string.search_empty);
}
if (query != null) {
postAdapter.filterList(filter);
searchStatus.setText(getContext().getString(R.string.search_results,
getContext().getResources().getQuantityString(R.plurals.posts, filter.size(), filter.size()), query));
}
}
public void cleanup() { public void cleanup() {
postAdapter.cleanup(); postAdapter.cleanup();
} }
@ -99,8 +142,12 @@ public class ThreadListLayout extends RelativeLayout {
return thumbnail; return thumbnail;
} }
public void scrollTo(int position) { public void scrollTo(int position, boolean smooth) {
if (smooth) {
recyclerView.smoothScrollToPosition(position); recyclerView.smoothScrollToPosition(position);
} else {
recyclerView.scrollToPosition(position);
}
} }
public void highlightPost(Post post) { public void highlightPost(Post post) {

@ -26,11 +26,18 @@ import android.content.Context;
import android.content.res.Configuration; import android.content.res.Configuration;
import android.graphics.drawable.Drawable; import android.graphics.drawable.Drawable;
import android.os.Build; import android.os.Build;
import android.text.Editable;
import android.text.TextWatcher;
import android.util.AttributeSet; import android.util.AttributeSet;
import android.util.TypedValue;
import android.view.Gravity; import android.view.Gravity;
import android.view.KeyEvent;
import android.view.LayoutInflater; import android.view.LayoutInflater;
import android.view.View; import android.view.View;
import android.view.ViewGroup;
import android.view.animation.DecelerateInterpolator; import android.view.animation.DecelerateInterpolator;
import android.view.inputmethod.EditorInfo;
import android.widget.EditText;
import android.widget.FrameLayout; import android.widget.FrameLayout;
import android.widget.ImageView; import android.widget.ImageView;
import android.widget.LinearLayout; import android.widget.LinearLayout;
@ -39,6 +46,7 @@ import android.widget.TextView;
import org.floens.chan.R; import org.floens.chan.R;
import org.floens.chan.ui.drawable.ArrowMenuDrawable; import org.floens.chan.ui.drawable.ArrowMenuDrawable;
import org.floens.chan.ui.drawable.DropdownArrowDrawable; import org.floens.chan.ui.drawable.DropdownArrowDrawable;
import org.floens.chan.ui.view.LoadView;
import org.floens.chan.utils.AndroidUtils; import org.floens.chan.utils.AndroidUtils;
import java.util.ArrayList; import java.util.ArrayList;
@ -47,14 +55,15 @@ import java.util.List;
import static org.floens.chan.utils.AndroidUtils.dp; import static org.floens.chan.utils.AndroidUtils.dp;
import static org.floens.chan.utils.AndroidUtils.getAttrDrawable; import static org.floens.chan.utils.AndroidUtils.getAttrDrawable;
public class Toolbar extends LinearLayout implements View.OnClickListener { public class Toolbar extends LinearLayout implements View.OnClickListener, LoadView.Listener {
private ImageView arrowMenuView; private ImageView arrowMenuView;
private ArrowMenuDrawable arrowMenuDrawable; private ArrowMenuDrawable arrowMenuDrawable;
private FrameLayout navigationItemContainer; private LoadView navigationItemContainer;
private ToolbarCallback callback; private ToolbarCallback callback;
private NavigationItem navigationItem; private NavigationItem navigationItem;
private boolean search = false;
public Toolbar(Context context) { public Toolbar(Context context) {
super(context); super(context);
@ -72,20 +81,101 @@ public class Toolbar extends LinearLayout implements View.OnClickListener {
} }
public void updateNavigation() { public void updateNavigation() {
closeSearchInternal(true);
setNavigationItem(false, false, navigationItem); setNavigationItem(false, false, navigationItem);
} }
public boolean showSearch() {
if (!search) {
search = true;
LinearLayout searchViewWrapper = new LinearLayout(getContext());
final EditText searchView = new EditText(getContext());
searchView.setImeOptions(EditorInfo.IME_FLAG_NO_FULLSCREEN | EditorInfo.IME_ACTION_DONE);
searchView.setHint(callback.getSearchHint());
searchView.setTextSize(TypedValue.COMPLEX_UNIT_DIP, 18);
searchView.setHintTextColor(0x88ffffff);
searchView.setTextColor(0xffffffff);
searchView.setSingleLine(true);
searchView.setBackgroundResource(0);
searchView.setPadding(0, 0, 0, 0);
final ImageView clearButton = new ImageView(getContext());
searchView.addTextChangedListener(new TextWatcher() {
@Override
public void onTextChanged(CharSequence s, int start, int before, int count) {
callback.onSearchEntered(s.toString());
clearButton.setAlpha(s.length() == 0 ? 0.6f : 1.0f);
}
@Override
public void beforeTextChanged(CharSequence s, int start, int count, int after) {
}
@Override
public void afterTextChanged(Editable s) {
}
});
searchView.setOnEditorActionListener(new TextView.OnEditorActionListener() {
@Override
public boolean onEditorAction(TextView v, int actionId, KeyEvent event) {
if (actionId == EditorInfo.IME_ACTION_DONE) {
AndroidUtils.hideKeyboard(searchView);
callback.onSearchEntered(searchView.getText().toString());
return true;
}
return false;
}
});
LinearLayout.LayoutParams searchViewParams = new LinearLayout.LayoutParams(0, dp(36), 1);
searchViewParams.gravity = Gravity.CENTER_VERTICAL;
searchViewWrapper.addView(searchView, searchViewParams);
clearButton.setImageResource(R.drawable.ic_close_white_24dp);
clearButton.setAlpha(0.6f);
clearButton.setScaleType(ImageView.ScaleType.CENTER);
clearButton.setOnClickListener(new OnClickListener() {
@Override
public void onClick(View v) {
searchView.setText("");
AndroidUtils.requestKeyboardFocus(searchView);
}
});
searchViewWrapper.addView(clearButton, dp(48), LayoutParams.MATCH_PARENT);
searchViewWrapper.setPadding(dp(16), 0, 0, 0);
searchView.post(new Runnable() {
@Override
public void run() {
searchView.requestFocus();
AndroidUtils.requestKeyboardFocus(searchView);
}
});
navigationItemContainer.setView(searchViewWrapper, true);
animateArrow(true, 0);
callback.onSearchVisibilityChanged(true);
return true;
} else {
return false;
}
}
public boolean closeSearch() {
return closeSearchInternal(false);
}
public void setNavigationItem(final boolean animate, final boolean pushing, final NavigationItem item) { public void setNavigationItem(final boolean animate, final boolean pushing, final NavigationItem item) {
closeSearchInternal(true);
if (item.menu != null) { if (item.menu != null) {
AndroidUtils.waitForMeasure(this, new AndroidUtils.OnMeasuredCallback() { AndroidUtils.waitForMeasure(this, new AndroidUtils.OnMeasuredCallback() {
@Override @Override
public boolean onMeasured(View view) { public boolean onMeasured(View view) {
setNavigationItemView(animate, pushing, item); setNavigationItemInternal(animate, pushing, false, item);
return true; return true;
} }
}); });
} else { } else {
setNavigationItemView(animate, pushing, item); setNavigationItemInternal(animate, pushing, false, item);
} }
} }
@ -109,6 +199,14 @@ public class Toolbar extends LinearLayout implements View.OnClickListener {
public void onConfigurationChanged(Configuration newConfig) { public void onConfigurationChanged(Configuration newConfig) {
} }
@Override
public void onLoadViewRemoved(View view) {
// TODO: this is kinda a hack
if (view instanceof ViewGroup) {
((ViewGroup) view).removeAllViews();
}
}
private void init() { private void init() {
setOrientation(HORIZONTAL); setOrientation(HORIZONTAL);
@ -130,11 +228,27 @@ public class Toolbar extends LinearLayout implements View.OnClickListener {
leftButtonContainer.addView(arrowMenuView, new FrameLayout.LayoutParams(dp(56), FrameLayout.LayoutParams.MATCH_PARENT, Gravity.CENTER_VERTICAL)); leftButtonContainer.addView(arrowMenuView, new FrameLayout.LayoutParams(dp(56), FrameLayout.LayoutParams.MATCH_PARENT, Gravity.CENTER_VERTICAL));
navigationItemContainer = new FrameLayout(getContext()); navigationItemContainer = new LoadView(getContext());
navigationItemContainer.setListener(this);
addView(navigationItemContainer, new LayoutParams(0, LayoutParams.MATCH_PARENT, 1f)); addView(navigationItemContainer, new LayoutParams(0, LayoutParams.MATCH_PARENT, 1f));
} }
private void setNavigationItemView(boolean animate, boolean pushing, NavigationItem toItem) { private boolean closeSearchInternal(boolean fromSetNavigation) {
if (search) {
search = false;
setNavigationItemInternal(true, false, true, navigationItem);
AndroidUtils.hideKeyboard(navigationItemContainer);
callback.onSearchVisibilityChanged(false);
if (!fromSetNavigation) {
animateArrow(navigationItem.hasBack, 0);
}
return true;
} else {
return false;
}
}
private void setNavigationItemInternal(boolean animate, boolean pushing, boolean fromSearch, NavigationItem toItem) {
final NavigationItem fromItem = navigationItem; final NavigationItem fromItem = navigationItem;
if (!animate) { if (!animate) {
@ -146,27 +260,25 @@ public class Toolbar extends LinearLayout implements View.OnClickListener {
toItem.view = createNavigationItemView(toItem); toItem.view = createNavigationItemView(toItem);
// use the LoadView animation when from a search
if (fromSearch) {
navigationItemContainer.setView(toItem.view, true);
} else {
navigationItemContainer.addView(toItem.view, LayoutParams.MATCH_PARENT, LayoutParams.MATCH_PARENT); navigationItemContainer.addView(toItem.view, LayoutParams.MATCH_PARENT, LayoutParams.MATCH_PARENT);
}
final int duration = 300; final int duration = 300;
final int offset = dp(16); final int offset = dp(16);
final int delay = pushing ? 0 : 100;
if (animate) { // Use the LoadView animation when from a search
if (animate && !fromSearch) {
toItem.view.setAlpha(0f); toItem.view.setAlpha(0f);
List<Animator> animations = new ArrayList<>(5); List<Animator> animations = new ArrayList<>(5);
if (fromItem != null && fromItem.hasBack != toItem.hasBack) { if (fromItem != null && fromItem.hasBack != toItem.hasBack) {
ValueAnimator arrowAnimation = ValueAnimator.ofFloat(fromItem.hasBack ? 1f : 0f, toItem.hasBack ? 1f : 0f); animateArrow(toItem.hasBack, delay);
arrowAnimation.setDuration(duration);
arrowAnimation.setInterpolator(new DecelerateInterpolator(2f));
arrowAnimation.addUpdateListener(new ValueAnimator.AnimatorUpdateListener() {
@Override
public void onAnimationUpdate(ValueAnimator animation) {
setArrowMenuProgress((float) animation.getAnimatedValue());
}
});
animations.add(arrowAnimation);
} else { } else {
setArrowMenuProgress(toItem.hasBack ? 1f : 0f); setArrowMenuProgress(toItem.hasBack ? 1f : 0f);
} }
@ -202,7 +314,7 @@ public class Toolbar extends LinearLayout implements View.OnClickListener {
} }
AnimatorSet set = new AnimatorSet(); AnimatorSet set = new AnimatorSet();
set.setStartDelay(pushing ? 0 : 100); set.setStartDelay(delay);
set.playTogether(animations); set.playTogether(animations);
set.start(); set.start();
} }
@ -252,7 +364,7 @@ public class Toolbar extends LinearLayout implements View.OnClickListener {
@Override @Override
public boolean onMeasured(View view) { public boolean onMeasured(View view) {
if (item.middleMenu != null) { if (item.middleMenu != null) {
item.middleMenu.setPopupWidth(Math.max(dp(150), titleView.getWidth())); item.middleMenu.setPopupWidth(Math.max(dp(200), titleView.getWidth()));
} }
return false; return false;
} }
@ -261,7 +373,30 @@ public class Toolbar extends LinearLayout implements View.OnClickListener {
return wrapper; return wrapper;
} }
private void animateArrow(boolean toArrow, long delay) {
float to = toArrow ? 1f : 0f;
if (to != arrowMenuDrawable.getProgress()) {
ValueAnimator arrowAnimation = ValueAnimator.ofFloat(arrowMenuDrawable.getProgress(), to);
arrowAnimation.setDuration(300);
arrowAnimation.setInterpolator(new DecelerateInterpolator(2f));
arrowAnimation.addUpdateListener(new ValueAnimator.AnimatorUpdateListener() {
@Override
public void onAnimationUpdate(ValueAnimator animation) {
setArrowMenuProgress((float) animation.getAnimatedValue());
}
});
arrowAnimation.setStartDelay(delay);
arrowAnimation.start();
}
}
public interface ToolbarCallback { public interface ToolbarCallback {
void onMenuOrBackClicked(boolean isArrow); void onMenuOrBackClicked(boolean isArrow);
void onSearchVisibilityChanged(boolean visible);
String getSearchHint();
void onSearchEntered(String entered);
} }
} }

@ -34,24 +34,18 @@ import android.widget.ProgressBar;
*/ */
public class LoadView extends FrameLayout { public class LoadView extends FrameLayout {
private int fadeDuration = 200; private int fadeDuration = 200;
private Listener listener;
public LoadView(Context context) { public LoadView(Context context) {
super(context); super(context);
init();
} }
public LoadView(Context context, AttributeSet attrs) { public LoadView(Context context, AttributeSet attrs) {
super(context, attrs); super(context, attrs);
init();
} }
public LoadView(Context context, AttributeSet attrs, int defStyleAttr) { public LoadView(Context context, AttributeSet attrs, int defStyleAttr) {
super(context, attrs, defStyleAttr); super(context, attrs, defStyleAttr);
init();
}
private void init() {
setView(null, false);
} }
/** /**
@ -62,6 +56,14 @@ public class LoadView extends FrameLayout {
this.fadeDuration = fadeDuration; this.fadeDuration = fadeDuration;
} }
/**
* Set a listener that gives a call when a view gets removed
* @param listener the listener
*/
public void setListener(Listener listener) {
this.listener = listener;
}
/** /**
* Set the content of this container. It will fade the attached views out with the * Set the content of this container. It will fade the attached views out with the
* new one. Set view to null to show the progressbar. * new one. Set view to null to show the progressbar.
@ -107,6 +109,9 @@ public class LoadView extends FrameLayout {
// Animation ended without interruptions, remove listener for future animations. // Animation ended without interruptions, remove listener for future animations.
childAnimation.setListener(null); childAnimation.setListener(null);
removeView(child); removeView(child);
if (listener != null) {
listener.onLoadViewRemoved(child);
}
} }
}).start(); }).start();
} }
@ -131,6 +136,9 @@ public class LoadView extends FrameLayout {
for (int i = 0; i < getChildCount(); i++) { for (int i = 0; i < getChildCount(); i++) {
View child = getChildAt(i); View child = getChildAt(i);
child.clearAnimation(); child.clearAnimation();
if (listener != null) {
listener.onLoadViewRemoved(child);
}
} }
removeAllViews(); removeAllViews();
newView.clearAnimation(); newView.clearAnimation();
@ -138,4 +146,8 @@ public class LoadView extends FrameLayout {
addView(newView, new LayoutParams(LayoutParams.MATCH_PARENT, LayoutParams.MATCH_PARENT)); addView(newView, new LayoutParams(LayoutParams.MATCH_PARENT, LayoutParams.MATCH_PARENT));
} }
} }
public interface Listener {
void onLoadViewRemoved(View view);
}
} }

@ -154,12 +154,16 @@ public class AndroidUtils {
dialog.setOnShowListener(new DialogInterface.OnShowListener() { dialog.setOnShowListener(new DialogInterface.OnShowListener() {
@Override @Override
public void onShow(DialogInterface dialog) { public void onShow(DialogInterface dialog) {
InputMethodManager imm = (InputMethodManager) view.getContext().getSystemService(Context.INPUT_METHOD_SERVICE); requestKeyboardFocus(view);
imm.showSoftInput(view, 0);
} }
}); });
} }
public static void requestKeyboardFocus(final View view) {
InputMethodManager inputManager = (InputMethodManager) view.getContext().getSystemService(Context.INPUT_METHOD_SERVICE);
inputManager.showSoftInput(view, InputMethodManager.SHOW_IMPLICIT);
}
public static void hideKeyboard(View view) { public static void hideKeyboard(View view) {
InputMethodManager imm = (InputMethodManager) view.getContext().getSystemService(Context.INPUT_METHOD_SERVICE); InputMethodManager imm = (InputMethodManager) view.getContext().getSystemService(Context.INPUT_METHOD_SERVICE);
imm.hideSoftInputFromWindow(view.getWindowToken(), 0); imm.hideSoftInputFromWindow(view.getWindowToken(), 0);

Binary file not shown.

After

Width:  |  Height:  |  Size: 324 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 279 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 402 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 492 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 662 B

@ -1,12 +1,26 @@
<?xml version="1.0" encoding="utf-8"?> <?xml version="1.0" encoding="utf-8"?>
<org.floens.chan.ui.layout.ThreadListLayout xmlns:android="http://schemas.android.com/apk/res/android" <org.floens.chan.ui.layout.ThreadListLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:orientation="vertical"
android:layout_width="match_parent" android:layout_width="match_parent"
android:layout_height="match_parent"> android:layout_height="match_parent">
<TextView
android:id="@+id/search_status"
android:visibility="gone"
android:gravity="center"
android:textColor="#ff757575"
android:background="#ffffffff"
android:singleLine="true"
android:elevation="4dp"
android:padding="8dp"
android:layout_width="match_parent"
android:layout_height="0dp" />
<android.support.v7.widget.RecyclerView <android.support.v7.widget.RecyclerView
android:id="@+id/recycler_view" android:id="@+id/recycler_view"
android:scrollbars="vertical" android:scrollbars="vertical"
android:layout_weight="1"
android:layout_width="match_parent" android:layout_width="match_parent"
android:layout_height="match_parent" /> android:layout_height="0dp" />
</org.floens.chan.ui.layout.ThreadListLayout> </org.floens.chan.ui.layout.ThreadListLayout>

@ -32,6 +32,11 @@ along with this program. If not, see <http://www.gnu.org/licenses/>.
<item quantity="other">%d minutes</item> <item quantity="other">%d minutes</item>
</plurals> </plurals>
<plurals name="posts">
<item quantity="one">%d post</item>
<item quantity="other">%d posts</item>
</plurals>
<string name="action_settings">Settings</string> <string name="action_settings">Settings</string>
<string name="action_reload">Reload</string> <string name="action_reload">Reload</string>
<string name="action_reload_board">Reload board</string> <string name="action_reload_board">Reload board</string>
@ -51,8 +56,9 @@ along with this program. If not, see <http://www.gnu.org/licenses/>.
<string name="action_search_thread">Search thread</string> <string name="action_search_thread">Search thread</string>
<string name="action_search_image">Image search</string> <string name="action_search_image">Image search</string>
<string name="search_hint">Search posts</string> <string name="search_hint">Search</string>
<string name="search_results">Found %1$s %2$s for "%3$s"</string> <string name="search_results">Found %1$s for "%2$s"</string>
<string name="search_empty">Search subjects, comments, names and filenames</string>
<string name="open_unknown_title">Unsupported link</string> <string name="open_unknown_title">Unsupported link</string>
<string name="open_unknown">Clover can\'t open this link. Opening it in your browser instead.</string> <string name="open_unknown">Clover can\'t open this link. Opening it in your browser instead.</string>
@ -256,7 +262,6 @@ Don't have a 4chan Pass?&lt;br>
<string name="setting_screen_licenses">Open Source Licenses</string> <string name="setting_screen_licenses">Open Source Licenses</string>
<string name="preference_board_edit">Edit boards</string> <string name="preference_board_edit">Edit boards</string>
<string name="preference_board_edit_summary">Add or remove boards</string> <string name="preference_board_edit_summary">Add or remove boards</string>
<string name="preference_watch_settings">Thread watcher</string> <string name="preference_watch_settings">Thread watcher</string>

Loading…
Cancel
Save