Add catalog view with post cards

filtering
Floens 10 years ago
parent 918d4ac326
commit abcf5d0cf9
  1. 1
      Clover/app/build.gradle
  2. 31
      Clover/app/src/main/java/org/floens/chan/core/model/PostLinkable.java
  3. 8
      Clover/app/src/main/java/org/floens/chan/core/presenter/ThreadPresenter.java
  4. 2
      Clover/app/src/main/java/org/floens/chan/core/settings/ChanSettings.java
  5. 32
      Clover/app/src/main/java/org/floens/chan/ui/adapter/PostAdapter.java
  6. 210
      Clover/app/src/main/java/org/floens/chan/ui/cell/CardPostCell.java
  7. 73
      Clover/app/src/main/java/org/floens/chan/ui/cell/PostCell.java
  8. 56
      Clover/app/src/main/java/org/floens/chan/ui/cell/PostCellInterface.java
  9. 26
      Clover/app/src/main/java/org/floens/chan/ui/controller/BrowseController.java
  10. 15
      Clover/app/src/main/java/org/floens/chan/ui/controller/MainSettingsController.java
  11. 18
      Clover/app/src/main/java/org/floens/chan/ui/controller/PostRepliesController.java
  12. 5
      Clover/app/src/main/java/org/floens/chan/ui/controller/ThreadController.java
  13. 3
      Clover/app/src/main/java/org/floens/chan/ui/controller/ViewThreadController.java
  14. 7
      Clover/app/src/main/java/org/floens/chan/ui/layout/ThreadLayout.java
  15. 142
      Clover/app/src/main/java/org/floens/chan/ui/layout/ThreadListLayout.java
  16. 11
      Clover/app/src/main/java/org/floens/chan/ui/theme/Theme.java
  17. 106
      Clover/app/src/main/java/org/floens/chan/ui/view/FastTextView.java
  18. 47
      Clover/app/src/main/java/org/floens/chan/ui/view/FixedRatioThumbnailView.java
  19. 8
      Clover/app/src/main/java/org/floens/chan/ui/view/FloatingMenuItem.java
  20. 6
      Clover/app/src/main/java/org/floens/chan/ui/view/ThumbnailView.java
  21. 3
      Clover/app/src/main/res/layout/cell_post.xml
  22. 90
      Clover/app/src/main/res/layout/cell_post_card.xml
  23. 4
      Clover/app/src/main/res/layout/layout_thread.xml
  24. 1
      Clover/app/src/main/res/layout/layout_thread_list.xml
  25. 23
      Clover/app/src/main/res/values-sw600dp/dimens.xml
  26. 23
      Clover/app/src/main/res/values/dimens.xml
  27. 4
      Clover/app/src/main/res/values/strings.xml

@ -74,6 +74,7 @@ dependencies {
compile 'com.android.support:support-v13:22.2.0' compile 'com.android.support:support-v13:22.2.0'
compile 'com.android.support:appcompat-v7:22.2.0' compile 'com.android.support:appcompat-v7:22.2.0'
compile 'com.android.support:recyclerview-v7:22.2.0' compile 'com.android.support:recyclerview-v7:22.2.0'
compile 'com.android.support:cardview-v7:22.2.0'
compile 'com.android.support:support-annotations:22.2.0' compile 'com.android.support:support-annotations:22.2.0'
compile 'com.android.support:design:22.2.0' compile 'com.android.support:design:22.2.0'

@ -48,20 +48,51 @@ public class PostLinkable extends ClickableSpan {
private List<Callback> callbacks = new ArrayList<>(); private List<Callback> callbacks = new ArrayList<>();
private boolean spoilerVisible = false; private boolean spoilerVisible = false;
// private static boolean testingCallbacks = false;
// private static HashMap<PostLinkable, List<Callback>> callbacksTest = new HashMap<>();
public PostLinkable(Theme theme, Post post, String key, Object value, Type type) { public PostLinkable(Theme theme, Post post, String key, Object value, Type type) {
this.theme = theme; this.theme = theme;
this.post = post; this.post = post;
this.key = key; this.key = key;
this.value = value; this.value = value;
this.type = type; this.type = type;
/*if (!testingCallbacks) {
testingCallbacks = true;
AndroidUtils.runOnUiThread(testCallbacksRunnable, 1000);
}*/
} }
/*private static Runnable testCallbacksRunnable = new Runnable() {
@Override
public void run() {
AndroidUtils.runOnUiThread(testCallbacksRunnable, 1000);
Logger.test("Callbacks:");
for (Map.Entry<PostLinkable, List<Callback>> entry : callbacksTest.entrySet()) {
if (entry.getValue().size() > 0) {
Logger.test(entry.getKey().key + " still has " + entry.getValue().size() + " bounded callbacks");
}
}
}
};*/
public void addCallback(Callback callback) { public void addCallback(Callback callback) {
callbacks.add(callback); callbacks.add(callback);
/*if (!callbacksTest.containsKey(this)) {
callbacksTest.put(this, new ArrayList<Callback>());
}
callbacksTest.get(this).add(callback);*/
} }
public void removeCallback(Callback callback) { public void removeCallback(Callback callback) {
callbacks.remove(callback); callbacks.remove(callback);
/*callbacksTest.get(this).remove(callback);*/
} }
public boolean hasCallback(Callback callback) { public boolean hasCallback(Callback callback) {

@ -23,11 +23,10 @@ import com.android.volley.VolleyError;
import org.floens.chan.Chan; import org.floens.chan.Chan;
import org.floens.chan.R; import org.floens.chan.R;
import org.floens.chan.chan.ChanLoader;
import org.floens.chan.chan.ChanUrls; import org.floens.chan.chan.ChanUrls;
import org.floens.chan.core.http.DeleteHttpCall; import org.floens.chan.core.http.DeleteHttpCall;
import org.floens.chan.core.http.ReplyManager; import org.floens.chan.core.http.ReplyManager;
import org.floens.chan.chan.ChanLoader;
import org.floens.chan.core.net.LoaderPool;
import org.floens.chan.core.manager.WatchManager; import org.floens.chan.core.manager.WatchManager;
import org.floens.chan.core.model.ChanThread; import org.floens.chan.core.model.ChanThread;
import org.floens.chan.core.model.Loadable; import org.floens.chan.core.model.Loadable;
@ -36,10 +35,11 @@ import org.floens.chan.core.model.Post;
import org.floens.chan.core.model.PostImage; import org.floens.chan.core.model.PostImage;
import org.floens.chan.core.model.PostLinkable; import org.floens.chan.core.model.PostLinkable;
import org.floens.chan.core.model.SavedReply; import org.floens.chan.core.model.SavedReply;
import org.floens.chan.core.net.LoaderPool;
import org.floens.chan.core.settings.ChanSettings; import org.floens.chan.core.settings.ChanSettings;
import org.floens.chan.database.DatabaseManager; import org.floens.chan.database.DatabaseManager;
import org.floens.chan.ui.adapter.PostAdapter; import org.floens.chan.ui.adapter.PostAdapter;
import org.floens.chan.ui.cell.PostCell; import org.floens.chan.ui.cell.PostCellInterface;
import org.floens.chan.ui.cell.ThreadStatusCell; import org.floens.chan.ui.cell.ThreadStatusCell;
import org.floens.chan.ui.helper.PostHelper; import org.floens.chan.ui.helper.PostHelper;
import org.floens.chan.ui.layout.ThreadListLayout; import org.floens.chan.ui.layout.ThreadListLayout;
@ -53,7 +53,7 @@ import java.util.Locale;
import static org.floens.chan.utils.AndroidUtils.getString; import static org.floens.chan.utils.AndroidUtils.getString;
public class ThreadPresenter implements ChanLoader.ChanLoaderCallback, PostAdapter.PostAdapterCallback, PostCell.PostCellCallback, ThreadStatusCell.Callback, ThreadListLayout.ThreadListLayoutCallback { public class ThreadPresenter implements ChanLoader.ChanLoaderCallback, PostAdapter.PostAdapterCallback, PostCellInterface.PostCellCallback, ThreadStatusCell.Callback, ThreadListLayout.ThreadListLayoutCallback {
private static final int POST_OPTION_QUOTE = 0; private static final int POST_OPTION_QUOTE = 0;
private static final int POST_OPTION_QUOTE_TEXT = 1; private static final int POST_OPTION_QUOTE_TEXT = 1;
private static final int POST_OPTION_INFO = 2; private static final int POST_OPTION_INFO = 2;

@ -35,6 +35,7 @@ public class ChanSettings {
public static final BooleanSetting videoAutoLoad; public static final BooleanSetting videoAutoLoad;
public static final BooleanSetting videoOpenExternal; public static final BooleanSetting videoOpenExternal;
public static final BooleanSetting videoErrorIgnore; public static final BooleanSetting videoErrorIgnore;
public static final StringSetting boardViewMode;
public static final StringSetting postDefaultName; public static final StringSetting postDefaultName;
public static final BooleanSetting postPinThread; public static final BooleanSetting postPinThread;
@ -74,6 +75,7 @@ public class ChanSettings {
videoAutoLoad = new BooleanSetting(p, "preference_autoplay", false); videoAutoLoad = new BooleanSetting(p, "preference_autoplay", false);
videoOpenExternal = new BooleanSetting(p, "preference_video_external", false); videoOpenExternal = new BooleanSetting(p, "preference_video_external", false);
videoErrorIgnore = new BooleanSetting(p, "preference_video_error_ignore", false); videoErrorIgnore = new BooleanSetting(p, "preference_video_error_ignore", false);
boardViewMode = new StringSetting(p, "preference_board_view_mode", "list"); // "list" or "grid"
postDefaultName = new StringSetting(p, "preference_default_name", ""); postDefaultName = new StringSetting(p, "preference_default_name", "");
postPinThread = new BooleanSetting(p, "preference_pin_on_post", false); postPinThread = new BooleanSetting(p, "preference_pin_on_post", false);

@ -19,13 +19,14 @@ package org.floens.chan.ui.adapter;
import android.support.v7.widget.RecyclerView; import android.support.v7.widget.RecyclerView;
import android.view.LayoutInflater; import android.view.LayoutInflater;
import android.view.View;
import android.view.ViewGroup; import android.view.ViewGroup;
import org.floens.chan.R; 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.Loadable; import org.floens.chan.core.model.Loadable;
import org.floens.chan.core.model.Post; import org.floens.chan.core.model.Post;
import org.floens.chan.ui.cell.PostCell; import org.floens.chan.ui.cell.PostCellInterface;
import org.floens.chan.ui.cell.ThreadStatusCell; import org.floens.chan.ui.cell.ThreadStatusCell;
import java.util.ArrayList; import java.util.ArrayList;
@ -36,7 +37,7 @@ public class PostAdapter extends RecyclerView.Adapter<RecyclerView.ViewHolder> {
private static final int TYPE_STATUS = 1; private static final int TYPE_STATUS = 1;
private final PostAdapterCallback postAdapterCallback; private final PostAdapterCallback postAdapterCallback;
private final PostCell.PostCellCallback postCellCallback; private final PostCellInterface.PostCellCallback postCellCallback;
private final ThreadStatusCell.Callback statusCellCallback; private final ThreadStatusCell.Callback statusCellCallback;
private RecyclerView recyclerView; private RecyclerView recyclerView;
@ -48,8 +49,9 @@ public class PostAdapter extends RecyclerView.Adapter<RecyclerView.ViewHolder> {
private String highlightedPostId; private String highlightedPostId;
private int highlightedPostNo = -1; private int highlightedPostNo = -1;
private boolean filtering = false; private boolean filtering = false;
private PostCellInterface.PostViewMode postViewMode;
public PostAdapter(RecyclerView recyclerView, PostAdapterCallback postAdapterCallback, PostCell.PostCellCallback postCellCallback, ThreadStatusCell.Callback statusCellCallback) { public PostAdapter(RecyclerView recyclerView, PostAdapterCallback postAdapterCallback, PostCellInterface.PostCellCallback postCellCallback, ThreadStatusCell.Callback statusCellCallback) {
this.recyclerView = recyclerView; this.recyclerView = recyclerView;
this.postAdapterCallback = postAdapterCallback; this.postAdapterCallback = postAdapterCallback;
this.postCellCallback = postCellCallback; this.postCellCallback = postCellCallback;
@ -61,7 +63,17 @@ public class PostAdapter extends RecyclerView.Adapter<RecyclerView.ViewHolder> {
@Override @Override
public RecyclerView.ViewHolder onCreateViewHolder(ViewGroup parent, int viewType) { public RecyclerView.ViewHolder onCreateViewHolder(ViewGroup parent, int viewType) {
if (viewType == TYPE_POST) { if (viewType == TYPE_POST) {
PostCell postCell = (PostCell) LayoutInflater.from(parent.getContext()).inflate(R.layout.cell_post, parent, false); int layout = 0;
switch (postViewMode) {
case LIST:
layout = R.layout.cell_post;
break;
case CARD:
layout = R.layout.cell_post_card;
break;
}
PostCellInterface postCell = (PostCellInterface) LayoutInflater.from(parent.getContext()).inflate(layout, parent, false);
return new PostViewHolder(postCell); return new PostViewHolder(postCell);
} else { } else {
StatusViewHolder statusViewHolder = new StatusViewHolder((ThreadStatusCell) LayoutInflater.from(parent.getContext()).inflate(R.layout.cell_thread_status, parent, false)); StatusViewHolder statusViewHolder = new StatusViewHolder((ThreadStatusCell) LayoutInflater.from(parent.getContext()).inflate(R.layout.cell_thread_status, parent, false));
@ -77,7 +89,7 @@ public class PostAdapter extends RecyclerView.Adapter<RecyclerView.ViewHolder> {
PostViewHolder postViewHolder = (PostViewHolder) holder; PostViewHolder postViewHolder = (PostViewHolder) holder;
Post post = displayList.get(position); Post post = displayList.get(position);
boolean highlight = post == highlightedPost || post.id.equals(highlightedPostId) || post.no == highlightedPostNo; boolean highlight = post == highlightedPost || post.id.equals(highlightedPostId) || post.no == highlightedPostNo;
postViewHolder.postView.setPost(post, postCellCallback, highlight, -1); postViewHolder.postView.setPost(null, post, postCellCallback, highlight, -1);
} else if (getItemViewType(position) == TYPE_STATUS) { } else if (getItemViewType(position) == TYPE_STATUS) {
((StatusViewHolder) holder).threadStatusCell.update(); ((StatusViewHolder) holder).threadStatusCell.update();
onScrolledToBottom(); onScrolledToBottom();
@ -209,11 +221,15 @@ public class PostAdapter extends RecyclerView.Adapter<RecyclerView.ViewHolder> {
return postAdapterCallback.getLoadable().isThreadMode(); return postAdapterCallback.getLoadable().isThreadMode();
} }
public void setPostViewMode(PostCellInterface.PostViewMode postViewMode) {
this.postViewMode = postViewMode;
}
public static class PostViewHolder extends RecyclerView.ViewHolder { public static class PostViewHolder extends RecyclerView.ViewHolder {
private PostCell postView; private PostCellInterface postView;
public PostViewHolder(PostCell postView) { public PostViewHolder(PostCellInterface postView) {
super(postView); super((View) postView);
this.postView = postView; this.postView = postView;
} }
} }

@ -0,0 +1,210 @@
/*
* Clover - 4chan browser https://github.com/Floens/Clover/
* Copyright (C) 2014 Floens
*
* 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.
*
* This program 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 this program. If not, see <http://www.gnu.org/licenses/>.
*/
package org.floens.chan.ui.cell;
import android.annotation.TargetApi;
import android.content.Context;
import android.os.Build;
import android.support.v7.widget.CardView;
import android.text.TextUtils;
import android.util.AttributeSet;
import android.view.View;
import android.widget.ImageView;
import android.widget.TextView;
import org.floens.chan.R;
import org.floens.chan.core.model.Post;
import org.floens.chan.core.settings.ChanSettings;
import org.floens.chan.ui.theme.Theme;
import org.floens.chan.ui.theme.ThemeHelper;
import org.floens.chan.ui.view.FastTextView;
import org.floens.chan.ui.view.FloatingMenu;
import org.floens.chan.ui.view.FloatingMenuItem;
import org.floens.chan.ui.view.ThumbnailView;
import java.util.ArrayList;
import java.util.List;
import static org.floens.chan.utils.AndroidUtils.setRoundItemBackground;
public class CardPostCell extends CardView implements PostCellInterface, View.OnClickListener {
private static final int COMMENT_MAX_LENGTH = 200;
private boolean bound;
private Theme theme;
private Post post;
private PostCellInterface.PostCellCallback callback;
private ThumbnailView thumbnailView;
private TextView title;
private FastTextView comment;
private TextView replies;
private ImageView options;
public CardPostCell(Context context) {
super(context);
}
public CardPostCell(Context context, AttributeSet attrs) {
super(context, attrs);
}
public CardPostCell(Context context, AttributeSet attrs, int defStyleAttr) {
super(context, attrs, defStyleAttr);
}
@Override
protected void onFinishInflate() {
super.onFinishInflate();
thumbnailView = (ThumbnailView) findViewById(R.id.thumbnail);
thumbnailView.setOnClickListener(this);
title = (TextView) findViewById(R.id.title);
comment = (FastTextView) findViewById(R.id.comment);
replies = (TextView) findViewById(R.id.replies);
options = (ImageView) findViewById(R.id.options);
setRoundItemBackground(options);
int textSizeSp = Integer.parseInt(ChanSettings.fontSize.get());
title.setTextSize(textSizeSp);
comment.setTextSize(textSizeSp);
replies.setTextSize(textSizeSp);
setOnClickListener(this);
options.setOnClickListener(new OnClickListener() {
@Override
public void onClick(View v) {
List<FloatingMenuItem> items = new ArrayList<>();
callback.onPopulatePostOptions(post, items);
FloatingMenu menu = new FloatingMenu(getContext(), v, items);
menu.setCallback(new FloatingMenu.FloatingMenuCallback() {
@Override
public void onFloatingMenuItemClicked(FloatingMenu menu, FloatingMenuItem item) {
callback.onPostOptionClicked(post, item.getId());
}
@Override
public void onFloatingMenuDismissed(FloatingMenu menu) {
}
});
menu.show();
}
});
}
@Override
public void onClick(View v) {
if (v == thumbnailView) {
callback.onThumbnailClicked(post, thumbnailView);
} else if (v == this) {
callback.onPostClicked(post);
}
}
@Override
protected void onDetachedFromWindow() {
super.onDetachedFromWindow();
if (post != null && bound) {
unbindPost(post);
}
}
@Override
protected void onAttachedToWindow() {
super.onAttachedToWindow();
if (post != null && !bound) {
bindPost(theme, post);
}
}
public void setPost(Theme theme, final Post post, PostCellInterface.PostCellCallback callback, boolean highlighted, int markedNo) {
if (this.post == post) {
return;
}
if (theme == null) {
theme = ThemeHelper.theme();
}
if (this.post != null && bound) {
unbindPost(this.post);
this.post = null;
}
this.theme = theme;
this.post = post;
this.callback = callback;
bindPost(theme, post);
}
public Post getPost() {
return post;
}
public ThumbnailView getThumbnailView() {
return thumbnailView;
}
@Override
@TargetApi(Build.VERSION_CODES.JELLY_BEAN)
public boolean hasOverlappingRendering() {
return false;
}
private void bindPost(Theme theme, Post post) {
bound = true;
if (post.hasImage) {
thumbnailView.setVisibility(View.VISIBLE);
thumbnailView.setUrl(post.thumbnailUrl, thumbnailView.getWidth(), thumbnailView.getHeight());
} else {
thumbnailView.setVisibility(View.GONE);
thumbnailView.setUrl(null, 0, 0);
}
if (!TextUtils.isEmpty(post.subjectSpan)) {
title.setVisibility(View.VISIBLE);
title.setText(post.subjectSpan);
} else {
title.setVisibility(View.GONE);
title.setText(null);
}
CharSequence commentText;
if (post.comment.length() > COMMENT_MAX_LENGTH) {
commentText = post.comment.subSequence(0, COMMENT_MAX_LENGTH);
} else {
commentText = post.comment;
}
comment.setText(commentText);
comment.setTextColor(theme.textPrimary);
replies.setText(getResources().getString(R.string.card_stats, post.replies, post.images));
}
private void unbindPost(Post post) {
bound = false;
}
}

@ -47,7 +47,6 @@ import com.android.volley.toolbox.ImageLoader;
import org.floens.chan.Chan; import org.floens.chan.Chan;
import org.floens.chan.R; import org.floens.chan.R;
import org.floens.chan.core.model.Loadable;
import org.floens.chan.core.model.Post; import org.floens.chan.core.model.Post;
import org.floens.chan.core.model.PostLinkable; import org.floens.chan.core.model.PostLinkable;
import org.floens.chan.core.settings.ChanSettings; import org.floens.chan.core.settings.ChanSettings;
@ -62,18 +61,14 @@ import org.floens.chan.utils.Time;
import java.util.ArrayList; import java.util.ArrayList;
import java.util.List; import java.util.List;
import static org.floens.chan.ui.theme.ThemeHelper.theme;
import static org.floens.chan.utils.AndroidUtils.dp; import static org.floens.chan.utils.AndroidUtils.dp;
import static org.floens.chan.utils.AndroidUtils.getRes; import static org.floens.chan.utils.AndroidUtils.getRes;
import static org.floens.chan.utils.AndroidUtils.setRoundItemBackground; import static org.floens.chan.utils.AndroidUtils.setRoundItemBackground;
import static org.floens.chan.utils.AndroidUtils.sp; import static org.floens.chan.utils.AndroidUtils.sp;
public class PostCell extends RelativeLayout implements PostLinkable.Callback { public class PostCell extends RelativeLayout implements PostCellInterface, PostLinkable.Callback {
private static final int COMMENT_MAX_LENGTH_BOARD = 500; private static final int COMMENT_MAX_LENGTH_BOARD = 500;
private Post post;
private boolean threadMode;
private ThumbnailView thumbnailView; private ThumbnailView thumbnailView;
private TextView title; private TextView title;
private TextView icons; private TextView icons;
@ -87,9 +82,13 @@ public class PostCell extends RelativeLayout implements PostLinkable.Callback {
private int detailsSizePx; private int detailsSizePx;
private int iconsTextSize; private int iconsTextSize;
private int countrySizePx; private int countrySizePx;
private int paddingPx;
private boolean threadMode;
private boolean ignoreNextOnClick; private boolean ignoreNextOnClick;
private int paddingPx; private boolean bound = false;
private Theme theme;
private Post post;
private PostCellCallback callback; private PostCellCallback callback;
private boolean highlighted; private boolean highlighted;
private int markedNo; private int markedNo;
@ -104,6 +103,7 @@ public class PostCell extends RelativeLayout implements PostLinkable.Callback {
} }
} }
}; };
private ImageLoader.ImageContainer countryIconRequest;
public PostCell(Context context) { public PostCell(Context context) {
super(context); super(context);
@ -206,20 +206,35 @@ public class PostCell extends RelativeLayout implements PostLinkable.Callback {
protected void onDetachedFromWindow() { protected void onDetachedFromWindow() {
super.onDetachedFromWindow(); super.onDetachedFromWindow();
if (post != null) { if (post != null && bound) {
unbindPost(post); unbindPost(post);
} }
} }
public void setPost(final Post post, PostCellCallback callback, boolean highlighted, int markedNo) { @Override
setPost(theme(), post, callback, highlighted, markedNo); protected void onAttachedToWindow() {
super.onAttachedToWindow();
if (post != null && !bound) {
bindPost(theme, post);
}
}
public void setPost(Theme theme, final Post post, PostCellInterface.PostCellCallback callback, boolean highlighted, int markedNo) {
if (this.post == post && this.highlighted == highlighted && this.markedNo == markedNo) {
return;
}
if (theme == null) {
theme = ThemeHelper.theme();
} }
public void setPost(Theme theme, final Post post, PostCellCallback callback, boolean highlighted, int markedNo) { if (this.post != null && bound) {
if (this.post != null) {
unbindPost(this.post); unbindPost(this.post);
this.post = null;
} }
this.theme = theme;
this.post = post; this.post = post;
this.callback = callback; this.callback = callback;
this.highlighted = highlighted; this.highlighted = highlighted;
@ -243,6 +258,8 @@ public class PostCell extends RelativeLayout implements PostLinkable.Callback {
} }
private void bindPost(Theme theme, Post post) { private void bindPost(Theme theme, Post post) {
bound = true;
threadMode = callback.getLoadable().isThreadMode(); threadMode = callback.getLoadable().isThreadMode();
setPostLinkableListener(post, this); setPostLinkableListener(post, this);
@ -364,6 +381,13 @@ public class PostCell extends RelativeLayout implements PostLinkable.Callback {
} }
private void unbindPost(Post post) { private void unbindPost(Post post) {
bound = false;
if (countryIconRequest != null) {
countryIconRequest.cancelRequest();
countryIconRequest = null;
}
setPostLinkableListener(post, null); setPostLinkableListener(post, null);
} }
@ -373,22 +397,23 @@ public class PostCell extends RelativeLayout implements PostLinkable.Callback {
PostLinkable[] linkables = commentSpannable.getSpans(0, commentSpannable.length(), PostLinkable.class); PostLinkable[] linkables = commentSpannable.getSpans(0, commentSpannable.length(), PostLinkable.class);
for (PostLinkable linkable : linkables) { for (PostLinkable linkable : linkables) {
if (callback == null) { if (callback == null) {
if (linkable.hasCallback(this)) { while (linkable.hasCallback(this)) {
linkable.removeCallback(this); linkable.removeCallback(this);
} }
} else { } else {
if (!linkable.hasCallback(this)) {
linkable.addCallback(callback); linkable.addCallback(callback);
} }
} }
} }
} }
}
private void loadCountryIcon(final Theme theme) { private void loadCountryIcon(final Theme theme) {
final Post requestedPost = post; countryIconRequest = Chan.getVolleyImageLoader().get(post.countryUrl, new ImageLoader.ImageListener() {
Chan.getVolleyImageLoader().get(post.countryUrl, new ImageLoader.ImageListener() {
@Override @Override
public void onResponse(ImageLoader.ImageContainer response, boolean isImmediate) { public void onResponse(ImageLoader.ImageContainer response, boolean isImmediate) {
if (response.getBitmap() != null && PostCell.this.post == requestedPost) { if (response.getBitmap() != null) {
CharSequence countryIcon = PostHelper.addIcon(new BitmapDrawable(getRes(), response.getBitmap()), iconsTextSize); CharSequence countryIcon = PostHelper.addIcon(new BitmapDrawable(getRes(), response.getBitmap()), iconsTextSize);
SpannableString countryText = new SpannableString(post.countryName); SpannableString countryText = new SpannableString(post.countryName);
@ -421,22 +446,6 @@ public class PostCell extends RelativeLayout implements PostLinkable.Callback {
return markedNo; return markedNo;
} }
public interface PostCellCallback {
Loadable getLoadable();
void onPostClicked(Post post);
void onThumbnailClicked(Post post, ThumbnailView thumbnail);
void onShowPostReplies(Post post);
void onPopulatePostOptions(Post post, List<FloatingMenuItem> menu);
void onPostOptionClicked(Post post, Object id);
void onPostLinkableClicked(PostLinkable linkable);
}
private static BackgroundColorSpan BACKGROUND_SPAN = new BackgroundColorSpan(0x6633B5E5); private static BackgroundColorSpan BACKGROUND_SPAN = new BackgroundColorSpan(0x6633B5E5);
/** /**

@ -0,0 +1,56 @@
/*
* Clover - 4chan browser https://github.com/Floens/Clover/
* Copyright (C) 2014 Floens
*
* 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.
*
* This program 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 this program. If not, see <http://www.gnu.org/licenses/>.
*/
package org.floens.chan.ui.cell;
import org.floens.chan.core.model.Loadable;
import org.floens.chan.core.model.Post;
import org.floens.chan.core.model.PostLinkable;
import org.floens.chan.ui.theme.Theme;
import org.floens.chan.ui.view.FloatingMenuItem;
import org.floens.chan.ui.view.ThumbnailView;
import java.util.List;
public interface PostCellInterface {
void setPost(Theme theme, Post post, PostCellCallback callback, boolean highlighted, int markedN);
Post getPost();
ThumbnailView getThumbnailView();
enum PostViewMode {
LIST,
CARD
}
interface PostCellCallback {
Loadable getLoadable();
void onPostClicked(Post post);
void onThumbnailClicked(Post post, ThumbnailView thumbnail);
void onShowPostReplies(Post post);
void onPopulatePostOptions(Post post, List<FloatingMenuItem> menu);
void onPostOptionClicked(Post post, Object id);
void onPostLinkableClicked(PostLinkable linkable);
}
}

@ -32,6 +32,8 @@ import org.floens.chan.core.manager.BoardManager;
import org.floens.chan.core.model.Board; import org.floens.chan.core.model.Board;
import org.floens.chan.core.model.Loadable; import org.floens.chan.core.model.Loadable;
import org.floens.chan.core.model.Pin; import org.floens.chan.core.model.Pin;
import org.floens.chan.core.settings.ChanSettings;
import org.floens.chan.ui.cell.PostCellInterface;
import org.floens.chan.ui.layout.ThreadLayout; import org.floens.chan.ui.layout.ThreadLayout;
import org.floens.chan.ui.toolbar.ToolbarMenu; import org.floens.chan.ui.toolbar.ToolbarMenu;
import org.floens.chan.ui.toolbar.ToolbarMenuItem; import org.floens.chan.ui.toolbar.ToolbarMenuItem;
@ -46,8 +48,11 @@ public class BrowseController extends ThreadController implements ToolbarMenuIte
private static final int REFRESH_ID = 1; private static final int REFRESH_ID = 1;
private static final int SEARCH_ID = 101; private static final int SEARCH_ID = 101;
private static final int SHARE_ID = 102; private static final int SHARE_ID = 102;
private static final int VIEW_MODE_ID = 103;
private PostCellInterface.PostViewMode postViewMode;
private List<FloatingMenuItem> boardItems; private List<FloatingMenuItem> boardItems;
private FloatingMenuItem viewModeMenuItem;
public BrowseController(Context context) { public BrowseController(Context context) {
super(context); super(context);
@ -57,6 +62,9 @@ public class BrowseController extends ThreadController implements ToolbarMenuIte
public void onCreate() { public void onCreate() {
super.onCreate(); super.onCreate();
postViewMode = ChanSettings.boardViewMode.get().equals("list") ? PostCellInterface.PostViewMode.LIST : PostCellInterface.PostViewMode.CARD;
threadLayout.setPostViewMode(postViewMode);
navigationItem.hasDrawer = true; navigationItem.hasDrawer = true;
navigationItem.middleMenu = new FloatingMenu(context); navigationItem.middleMenu = new FloatingMenu(context);
navigationItem.middleMenu.setCallback(this); navigationItem.middleMenu.setCallback(this);
@ -73,6 +81,9 @@ public class BrowseController extends ThreadController implements ToolbarMenuIte
List<FloatingMenuItem> items = new ArrayList<>(); List<FloatingMenuItem> items = new ArrayList<>();
items.add(new FloatingMenuItem(SEARCH_ID, context.getString(R.string.action_search))); items.add(new FloatingMenuItem(SEARCH_ID, context.getString(R.string.action_search)));
items.add(new FloatingMenuItem(SHARE_ID, context.getString(R.string.action_share))); items.add(new FloatingMenuItem(SHARE_ID, context.getString(R.string.action_share)));
viewModeMenuItem = new FloatingMenuItem(VIEW_MODE_ID, context.getString(
postViewMode == PostCellInterface.PostViewMode.LIST ? R.string.action_switch_catalog : R.string.action_switch_board));
items.add(viewModeMenuItem);
overflow.setSubMenu(new FloatingMenu(context, overflow.getView(), items)); overflow.setSubMenu(new FloatingMenu(context, overflow.getView(), items));
} }
@ -95,6 +106,21 @@ public class BrowseController extends ThreadController implements ToolbarMenuIte
case SHARE_ID: case SHARE_ID:
String link = ChanUrls.getCatalogUrlDesktop(threadLayout.getPresenter().getLoadable().board); String link = ChanUrls.getCatalogUrlDesktop(threadLayout.getPresenter().getLoadable().board);
AndroidUtils.shareLink(link); AndroidUtils.shareLink(link);
break;
case VIEW_MODE_ID:
if (postViewMode == PostCellInterface.PostViewMode.LIST) {
postViewMode = PostCellInterface.PostViewMode.CARD;
} else {
postViewMode = PostCellInterface.PostViewMode.LIST;
}
ChanSettings.boardViewMode.set(postViewMode == PostCellInterface.PostViewMode.LIST ? "list" : "grid");
viewModeMenuItem.setText(context.getString(
postViewMode == PostCellInterface.PostViewMode.LIST ? R.string.action_switch_catalog : R.string.action_switch_board));
threadLayout.setPostViewMode(postViewMode);
break; break;
} }
} }

@ -41,6 +41,8 @@ import java.util.ArrayList;
import java.util.Arrays; import java.util.Arrays;
import java.util.List; import java.util.List;
import de.greenrobot.event.EventBus;
public class MainSettingsController extends SettingsController implements ToolbarMenuItem.ToolbarMenuItemCallback, WatchSettingsController.WatchSettingControllerListener, PassSettingsController.PassSettingControllerListener { public class MainSettingsController extends SettingsController implements ToolbarMenuItem.ToolbarMenuItemCallback, WatchSettingsController.WatchSettingControllerListener, PassSettingsController.PassSettingControllerListener {
private static final int ADVANCED_SETTINGS = 1; private static final int ADVANCED_SETTINGS = 1;
private SettingView imageAutoLoadView; private SettingView imageAutoLoadView;
@ -50,6 +52,7 @@ public class MainSettingsController extends SettingsController implements Toolba
private LinkSettingView passLink; private LinkSettingView passLink;
private int clickCount; private int clickCount;
private SettingView developerView; private SettingView developerView;
private SettingView fontView;
public MainSettingsController(Context context) { public MainSettingsController(Context context) {
super(context); super(context);
@ -99,6 +102,8 @@ public class MainSettingsController extends SettingsController implements Toolba
if (item == imageAutoLoadView) { if (item == imageAutoLoadView) {
videoAutoLoadView.setEnabled(ChanSettings.imageAutoLoad.get()); videoAutoLoadView.setEnabled(ChanSettings.imageAutoLoad.get());
} else if (item == fontView) {
EventBus.getDefault().post(new RefreshUIMessage("fontsize"));
} }
} }
@ -146,7 +151,7 @@ public class MainSettingsController extends SettingsController implements Toolba
fontSizes.add(new ListSettingView.Item(name, String.valueOf(size))); fontSizes.add(new ListSettingView.Item(name, String.valueOf(size)));
} }
appearance.add(new ListSettingView(this, ChanSettings.fontSize, s(R.string.setting_font_size), fontSizes.toArray(new ListSettingView.Item[fontSizes.size()]))); fontView = appearance.add(new ListSettingView(this, ChanSettings.fontSize, s(R.string.setting_font_size), fontSizes.toArray(new ListSettingView.Item[fontSizes.size()])));
groups.add(appearance); groups.add(appearance);
@ -229,4 +234,12 @@ public class MainSettingsController extends SettingsController implements Toolba
private String s(int id) { private String s(int id) {
return string(id); return string(id);
} }
public static class RefreshUIMessage {
public String reason;
public RefreshUIMessage(String reason) {
this.reason = reason;
}
}
} }

@ -39,7 +39,7 @@ import org.floens.chan.core.model.Post;
import org.floens.chan.core.model.PostImage; import org.floens.chan.core.model.PostImage;
import org.floens.chan.core.presenter.ThreadPresenter; import org.floens.chan.core.presenter.ThreadPresenter;
import org.floens.chan.core.settings.ChanSettings; import org.floens.chan.core.settings.ChanSettings;
import org.floens.chan.ui.cell.PostCell; import org.floens.chan.ui.cell.PostCellInterface;
import org.floens.chan.ui.helper.PostPopupHelper; import org.floens.chan.ui.helper.PostPopupHelper;
import org.floens.chan.ui.view.LoadView; import org.floens.chan.ui.view.LoadView;
import org.floens.chan.ui.view.ThumbnailView; import org.floens.chan.ui.view.ThumbnailView;
@ -105,8 +105,8 @@ public class PostRepliesController extends Controller {
ThumbnailView thumbnail = null; ThumbnailView thumbnail = null;
for (int i = 0; i < listView.getChildCount(); i++) { for (int i = 0; i < listView.getChildCount(); i++) {
View view = listView.getChildAt(i); View view = listView.getChildAt(i);
if (view instanceof PostCell) { if (view instanceof PostCellInterface) {
PostCell postView = (PostCell) view; PostCellInterface postView = (PostCellInterface) view;
Post post = postView.getPost(); Post post = postView.getPost();
if (post.hasImage && post.imageUrl.equals(postImage.imageUrl)) { if (post.hasImage && post.imageUrl.equals(postImage.imageUrl)) {
thumbnail = postView.getThumbnailView(); thumbnail = postView.getThumbnailView();
@ -169,17 +169,17 @@ public class PostRepliesController extends Controller {
ArrayAdapter<Post> adapter = new ArrayAdapter<Post>(context, 0) { ArrayAdapter<Post> adapter = new ArrayAdapter<Post>(context, 0) {
@Override @Override
public View getView(int position, View convertView, ViewGroup parent) { public View getView(int position, View convertView, ViewGroup parent) {
PostCell postCell; PostCellInterface postCell;
if (convertView instanceof PostCell) { if (convertView instanceof PostCellInterface) {
postCell = (PostCell) convertView; postCell = (PostCellInterface) convertView;
} else { } else {
postCell = (PostCell) LayoutInflater.from(parent.getContext()).inflate(R.layout.cell_post, parent, false); postCell = (PostCellInterface) LayoutInflater.from(parent.getContext()).inflate(R.layout.cell_post, parent, false);
} }
final Post p = getItem(position); final Post p = getItem(position);
postCell.setPost(p, presenter, false, data.forPost.no); postCell.setPost(null, p, presenter, false, data.forPost.no);
return postCell; return (View) postCell;
} }
}; };

@ -49,6 +49,7 @@ public abstract class ThreadController extends Controller implements ThreadLayou
threadLayout = (ThreadLayout) LayoutInflater.from(context).inflate(R.layout.layout_thread, null); threadLayout = (ThreadLayout) LayoutInflater.from(context).inflate(R.layout.layout_thread, null);
threadLayout.setCallback(this); threadLayout.setCallback(this);
swipeRefreshLayout = new SwipeRefreshLayout(context) { swipeRefreshLayout = new SwipeRefreshLayout(context) {
@Override @Override
public boolean canChildScrollUp() { public boolean canChildScrollUp() {
@ -87,6 +88,10 @@ public abstract class ThreadController extends Controller implements ThreadLayou
threadLayout.getPresenter().onForegroundChanged(message.inForeground); threadLayout.getPresenter().onForegroundChanged(message.inForeground);
} }
public void onEvent(MainSettingsController.RefreshUIMessage message) {
threadLayout.getPresenter().requestData();
}
@Override @Override
public void onRefresh() { public void onRefresh() {
threadLayout.refreshFromSwipe(); threadLayout.refreshFromSwipe();

@ -27,6 +27,7 @@ import org.floens.chan.chan.ChanUrls;
import org.floens.chan.core.manager.WatchManager; import org.floens.chan.core.manager.WatchManager;
import org.floens.chan.core.model.Loadable; import org.floens.chan.core.model.Loadable;
import org.floens.chan.core.model.Pin; import org.floens.chan.core.model.Pin;
import org.floens.chan.ui.cell.PostCellInterface;
import org.floens.chan.ui.layout.ThreadLayout; import org.floens.chan.ui.layout.ThreadLayout;
import org.floens.chan.ui.toolbar.ToolbarMenu; import org.floens.chan.ui.toolbar.ToolbarMenu;
import org.floens.chan.ui.toolbar.ToolbarMenuItem; import org.floens.chan.ui.toolbar.ToolbarMenuItem;
@ -58,6 +59,8 @@ public class ViewThreadController extends ThreadController implements ThreadLayo
public void onCreate() { public void onCreate() {
super.onCreate(); super.onCreate();
threadLayout.setPostViewMode(PostCellInterface.PostViewMode.LIST);
view.setBackgroundColor(getAttrColor(context, R.attr.backcolor)); view.setBackgroundColor(getAttrColor(context, R.attr.backcolor));
navigationItem.hasDrawer = true; navigationItem.hasDrawer = true;

@ -51,6 +51,7 @@ import org.floens.chan.core.model.PostImage;
import org.floens.chan.core.model.PostLinkable; import org.floens.chan.core.model.PostLinkable;
import org.floens.chan.core.presenter.ThreadPresenter; import org.floens.chan.core.presenter.ThreadPresenter;
import org.floens.chan.core.settings.ChanSettings; import org.floens.chan.core.settings.ChanSettings;
import org.floens.chan.ui.cell.PostCellInterface;
import org.floens.chan.ui.helper.PostPopupHelper; import org.floens.chan.ui.helper.PostPopupHelper;
import org.floens.chan.ui.view.LoadView; import org.floens.chan.ui.view.LoadView;
import org.floens.chan.ui.view.ThumbnailView; import org.floens.chan.ui.view.ThumbnailView;
@ -88,6 +89,7 @@ public class ThreadLayout extends CoordinatorLayout implements ThreadPresenter.T
private ProgressDialog deletingDialog; private ProgressDialog deletingDialog;
private boolean refreshedFromSwipe; private boolean refreshedFromSwipe;
private boolean showingReplyButton = false; private boolean showingReplyButton = false;
private PostCellInterface.PostViewMode postViewMode;
public ThreadLayout(Context context) { public ThreadLayout(Context context) {
super(context); super(context);
@ -159,6 +161,11 @@ public class ThreadLayout extends CoordinatorLayout implements ThreadPresenter.T
presenter.requestData(); presenter.requestData();
} }
public void setPostViewMode(PostCellInterface.PostViewMode postViewMode) {
this.postViewMode = postViewMode;
threadListLayout.setPostViewMode(postViewMode);
}
@Override @Override
public void replyLayoutOpen(boolean open) { public void replyLayoutOpen(boolean open) {
showReplyButton(!open); showReplyButton(!open);

@ -18,6 +18,7 @@
package org.floens.chan.ui.layout; package org.floens.chan.ui.layout;
import android.content.Context; import android.content.Context;
import android.support.v7.widget.GridLayoutManager;
import android.support.v7.widget.LinearLayoutManager; 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;
@ -33,6 +34,7 @@ import org.floens.chan.core.model.PostImage;
import org.floens.chan.core.presenter.ReplyPresenter; import org.floens.chan.core.presenter.ReplyPresenter;
import org.floens.chan.ui.adapter.PostAdapter; import org.floens.chan.ui.adapter.PostAdapter;
import org.floens.chan.ui.cell.PostCell; import org.floens.chan.ui.cell.PostCell;
import org.floens.chan.ui.cell.PostCellInterface;
import org.floens.chan.ui.cell.ThreadStatusCell; import org.floens.chan.ui.cell.ThreadStatusCell;
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.AndroidUtils;
@ -41,6 +43,8 @@ import org.floens.chan.utils.AnimationUtils;
import java.util.List; import java.util.List;
import static org.floens.chan.utils.AndroidUtils.ROBOTO_MEDIUM; import static org.floens.chan.utils.AndroidUtils.ROBOTO_MEDIUM;
import static org.floens.chan.utils.AndroidUtils.dp;
import static org.floens.chan.utils.AndroidUtils.getAttrColor;
/** /**
* A layout that wraps around a {@link RecyclerView} to manage showing posts. * A layout that wraps around a {@link RecyclerView} to manage showing posts.
@ -49,12 +53,15 @@ public class ThreadListLayout extends LinearLayout implements ReplyLayout.ReplyL
private ReplyLayout reply; private ReplyLayout reply;
private TextView searchStatus; private TextView searchStatus;
private RecyclerView recyclerView; private RecyclerView recyclerView;
private LinearLayoutManager linearLayoutManager; private RecyclerView.LayoutManager layoutManager;
private PostAdapter postAdapter; private PostAdapter postAdapter;
private ChanThread showingThread; private ChanThread showingThread;
private ThreadListLayoutCallback callback; private ThreadListLayoutCallback callback;
private ReplyLayoutStateCallback replyLayoutStateCallback; private ReplyLayoutStateCallback replyLayoutStateCallback;
private boolean replyOpen; private boolean replyOpen;
private PostCellInterface.PostViewMode postViewMode;
private int spanCount = 2;
private int background;
public ThreadListLayout(Context context, AttributeSet attrs) { public ThreadListLayout(Context context, AttributeSet attrs) {
super(context, attrs); super(context, attrs);
@ -71,8 +78,6 @@ public class ThreadListLayout extends LinearLayout implements ReplyLayout.ReplyL
searchStatus.setTypeface(ROBOTO_MEDIUM); searchStatus.setTypeface(ROBOTO_MEDIUM);
recyclerView = (RecyclerView) findViewById(R.id.recycler_view); recyclerView = (RecyclerView) findViewById(R.id.recycler_view);
linearLayoutManager = new LinearLayoutManager(getContext());
recyclerView.setLayoutManager(linearLayoutManager);
} }
public void setCallbacks(PostAdapter.PostAdapterCallback postAdapterCallback, PostCell.PostCellCallback postCellCallback, public void setCallbacks(PostAdapter.PostAdapterCallback postAdapterCallback, PostCell.PostCellCallback postCellCallback,
@ -80,6 +85,7 @@ public class ThreadListLayout extends LinearLayout implements ReplyLayout.ReplyL
ReplyLayoutStateCallback replyLayoutStateCallback) { ReplyLayoutStateCallback replyLayoutStateCallback) {
this.callback = callback; this.callback = callback;
this.replyLayoutStateCallback = replyLayoutStateCallback; this.replyLayoutStateCallback = replyLayoutStateCallback;
postAdapter = new PostAdapter(recyclerView, postAdapterCallback, postCellCallback, statusCellCallback); postAdapter = new PostAdapter(recyclerView, postAdapterCallback, postCellCallback, statusCellCallback);
recyclerView.setAdapter(postAdapter); recyclerView.setAdapter(postAdapter);
recyclerView.addOnScrollListener(new RecyclerView.OnScrollListener() { recyclerView.addOnScrollListener(new RecyclerView.OnScrollListener() {
@ -87,12 +93,110 @@ public class ThreadListLayout extends LinearLayout implements ReplyLayout.ReplyL
public void onScrolled(RecyclerView recyclerView, int dx, int dy) { public void onScrolled(RecyclerView recyclerView, int dx, int dy) {
// onScrolled can be called after cleanup() // onScrolled can be called after cleanup()
if (showingThread != null) { if (showingThread != null) {
showingThread.loadable.listViewIndex = Math.max(0, linearLayoutManager.findFirstVisibleItemPosition()); switch (postViewMode) {
case LIST:
showingThread.loadable.listViewIndex = Math.max(0, ((LinearLayoutManager) layoutManager).findFirstVisibleItemPosition());
break;
case CARD:
showingThread.loadable.listViewIndex = Math.max(0, ((GridLayoutManager) layoutManager).findFirstVisibleItemPosition());
break;
}
} }
} }
}); });
} }
@Override
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
super.onMeasure(widthMeasureSpec, heightMeasureSpec);
int cardWidth = getResources().getDimensionPixelSize(R.dimen.grid_card_width);
int maxSpans = getResources().getInteger(R.integer.grid_card_max_spans);
spanCount = Math.max(1, Math.round(getMeasuredWidth() / cardWidth));
if (maxSpans > 1 && spanCount > maxSpans) {
spanCount = maxSpans;
}
if (postViewMode == PostCellInterface.PostViewMode.CARD) {
((GridLayoutManager) layoutManager).setSpanCount(spanCount);
}
}
public void setPostViewMode(PostCellInterface.PostViewMode postViewMode) {
if (this.postViewMode != postViewMode) {
this.postViewMode = postViewMode;
layoutManager = null;
switch (postViewMode) {
case LIST:
LinearLayoutManager linearLayoutManager = new LinearLayoutManager(getContext());
recyclerView.setPadding(0, 0, 0, 0);
recyclerView.setLayoutManager(linearLayoutManager);
layoutManager = linearLayoutManager;
if (background != R.attr.backcolor) {
background = R.attr.backcolor;
setBackgroundColor(getAttrColor(getContext(), R.attr.backcolor));
}
break;
case CARD:
GridLayoutManager gridLayoutManager = new GridLayoutManager(null, spanCount, GridLayoutManager.VERTICAL, false);
// The cards have a 4dp padding, this way there is always 8dp between the edges
recyclerView.setPadding(dp(4), dp(4), dp(4), dp(4));
recyclerView.setLayoutManager(gridLayoutManager);
layoutManager = gridLayoutManager;
if (background != R.attr.backcolor_secondary) {
background = R.attr.backcolor_secondary;
setBackgroundColor(getAttrColor(getContext(), R.attr.backcolor_secondary));
}
break;
}
recyclerView.getRecycledViewPool().clear();
postAdapter.setPostViewMode(postViewMode);
}
}
public void showPosts(ChanThread thread, boolean initial) {
showingThread = thread;
if (initial) {
reply.bindLoadable(showingThread.loadable);
recyclerView.setLayoutManager(null);
recyclerView.setLayoutManager(layoutManager);
recyclerView.getRecycledViewPool().clear();
switch (postViewMode) {
case LIST:
((LinearLayoutManager) layoutManager).scrollToPositionWithOffset(thread.loadable.listViewIndex, 0);
break;
case CARD:
((GridLayoutManager) layoutManager).scrollToPositionWithOffset(thread.loadable.listViewIndex, 0);
break;
}
} else {
switch (postViewMode) {
case LIST:
LinearLayoutManager linearLayoutManager = (LinearLayoutManager) layoutManager;
if (linearLayoutManager.findLastCompletelyVisibleItemPosition() == postAdapter.getItemCount() - 1) {
linearLayoutManager.scrollToPositionWithOffset(postAdapter.getItemCount() - 1, 0);
}
break;
case CARD:
// No op
break;
}
}
postAdapter.setThread(thread);
}
public boolean onBack() { public boolean onBack() {
if (reply.onBack()) { if (reply.onBack()) {
return true; return true;
@ -121,20 +225,6 @@ public class ThreadListLayout extends LinearLayout implements ReplyLayout.ReplyL
return reply.getPresenter(); return reply.getPresenter();
} }
public void showPosts(ChanThread thread, boolean initial) {
showingThread = thread;
if (initial) {
reply.bindLoadable(showingThread.loadable);
linearLayoutManager.scrollToPositionWithOffset(thread.loadable.listViewIndex, 0);
} else {
if (linearLayoutManager.findLastVisibleItemPosition() == postAdapter.getItemCount() - 1) {
linearLayoutManager.scrollToPositionWithOffset(postAdapter.getItemCount() - 1, 0);
}
}
postAdapter.setThread(thread);
}
public void showError(String error) { public void showError(String error) {
postAdapter.showError(error); postAdapter.showError(error);
} }
@ -172,9 +262,19 @@ public class ThreadListLayout extends LinearLayout implements ReplyLayout.ReplyL
public boolean canChildScrollUp() { public boolean canChildScrollUp() {
View top = recyclerView.getChildAt(0); View top = recyclerView.getChildAt(0);
if (top != null) { if (top != null) {
if (top.getTop() == 0 && linearLayoutManager.findFirstVisibleItemPosition() == 0) {
switch (postViewMode) {
case LIST:
if (top.getTop() == 0 && ((LinearLayoutManager) layoutManager).findFirstVisibleItemPosition() == 0) {
return false;
}
break;
case CARD:
if (top.getTop() == 0 && ((GridLayoutManager) layoutManager).findFirstVisibleItemPosition() == 0) {
return false; return false;
} }
break;
}
} }
return true; return true;
} }
@ -208,8 +308,8 @@ public class ThreadListLayout extends LinearLayout implements ReplyLayout.ReplyL
ThumbnailView thumbnail = null; ThumbnailView thumbnail = null;
for (int i = 0; i < layoutManager.getChildCount(); i++) { for (int i = 0; i < layoutManager.getChildCount(); i++) {
View view = layoutManager.getChildAt(i); View view = layoutManager.getChildAt(i);
if (view instanceof PostCell) { if (view instanceof PostCellInterface) {
PostCell postView = (PostCell) view; PostCellInterface postView = (PostCellInterface) view;
Post post = postView.getPost(); Post post = postView.getPost();
if (post.hasImage && post.imageUrl.equals(postImage.imageUrl)) { if (post.hasImage && post.imageUrl.equals(postImage.imageUrl)) {
thumbnail = postView.getThumbnailView(); thumbnail = postView.getThumbnailView();

@ -38,6 +38,9 @@ public class Theme {
public boolean isLightTheme = true; public boolean isLightTheme = true;
public ThemeHelper.PrimaryColor primaryColor; public ThemeHelper.PrimaryColor primaryColor;
public int textPrimary;
public int textSecondary;
public int textHint;
public int quoteColor; public int quoteColor;
public int highlightQuoteColor; public int highlightQuoteColor;
public int linkColor; public int linkColor;
@ -96,7 +99,10 @@ public class Theme {
R.attr.post_capcode_color, R.attr.post_capcode_color,
R.attr.post_details_color, R.attr.post_details_color,
R.attr.post_highlighted_color, R.attr.post_highlighted_color,
R.attr.post_saved_reply_color R.attr.post_saved_reply_color,
R.attr.text_color_primary,
R.attr.text_color_secondary,
R.attr.text_color_hint
}); });
quoteColor = ta.getColor(0, 0); quoteColor = ta.getColor(0, 0);
@ -112,6 +118,9 @@ public class Theme {
detailsColor = ta.getColor(10, 0); detailsColor = ta.getColor(10, 0);
highlightedColor = ta.getColor(11, 0); highlightedColor = ta.getColor(11, 0);
savedReplyColor = ta.getColor(12, 0); savedReplyColor = ta.getColor(12, 0);
textPrimary = ta.getColor(13, 0);
textSecondary = ta.getColor(14, 0);
textHint = ta.getColor(15, 0);
ta.recycle(); ta.recycle();
} }

@ -0,0 +1,106 @@
/*
* Clover - 4chan browser https://github.com/Floens/Clover/
* Copyright (C) 2014 Floens
*
* 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.
*
* This program 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 this program. If not, see <http://www.gnu.org/licenses/>.
*/
package org.floens.chan.ui.view;
import android.content.Context;
import android.graphics.Canvas;
import android.graphics.Paint;
import android.text.Layout;
import android.text.StaticLayout;
import android.text.TextPaint;
import android.text.TextUtils;
import android.util.AttributeSet;
import android.view.View;
import static org.floens.chan.utils.AndroidUtils.sp;
public class FastTextView extends View {
private TextPaint paint;
private CharSequence text;
private boolean update = false;
private StaticLayout layout;
public FastTextView(Context context) {
super(context);
init();
}
public FastTextView(Context context, AttributeSet attrs) {
super(context, attrs);
init();
}
public FastTextView(Context context, AttributeSet attrs, int defStyleAttr) {
super(context, attrs, defStyleAttr);
init();
}
private void init() {
paint = new TextPaint(Paint.ANTI_ALIAS_FLAG);
}
public void setText(CharSequence text) {
if (!TextUtils.equals(this.text, text)) {
this.text = text;
if (text == null) {
layout = null;
} else {
update = true;
}
}
}
public void setTextSize(float size) {
int sizeSp = sp(size);
if (paint.getTextSize() != sizeSp) {
paint.setTextSize(sizeSp);
update = true;
}
}
public void setTextColor(int color) {
if (paint.getColor() != color) {
paint.setColor(color);
update = true;
}
}
@Override
protected void onSizeChanged(int w, int h, int oldw, int oldh) {
update = true;
}
@Override
protected void onDraw(Canvas canvas) {
if (update) {
int width = getWidth() - getPaddingLeft() - getPaddingRight();
layout = new StaticLayout(text, paint, width, Layout.Alignment.ALIGN_NORMAL, 1, 0, false);
update = false;
}
if (layout != null) {
canvas.save();
canvas.translate(getPaddingLeft(), getPaddingTop());
layout.draw(canvas);
canvas.restore();
}
}
}

@ -0,0 +1,47 @@
/*
* Clover - 4chan browser https://github.com/Floens/Clover/
* Copyright (C) 2014 Floens
*
* 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.
*
* This program 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 this program. If not, see <http://www.gnu.org/licenses/>.
*/
package org.floens.chan.ui.view;
import android.content.Context;
import android.util.AttributeSet;
public class FixedRatioThumbnailView extends ThumbnailView {
public FixedRatioThumbnailView(Context context) {
super(context);
}
public FixedRatioThumbnailView(Context context, AttributeSet attrs) {
super(context, attrs);
}
public FixedRatioThumbnailView(Context context, AttributeSet attrs, int defStyleAttr) {
super(context, attrs, defStyleAttr);
}
@Override
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
int heightMode = MeasureSpec.getMode(heightMeasureSpec);
if (MeasureSpec.getMode(widthMeasureSpec) == MeasureSpec.EXACTLY && (heightMode == MeasureSpec.UNSPECIFIED || heightMode == MeasureSpec.AT_MOST)) {
int width = MeasureSpec.getSize(widthMeasureSpec);
super.onMeasure(widthMeasureSpec, MeasureSpec.makeMeasureSpec((int) (width / 16f * 9f), MeasureSpec.EXACTLY));
} else {
super.onMeasure(widthMeasureSpec, heightMeasureSpec);
}
}
}

@ -37,7 +37,15 @@ public class FloatingMenuItem {
return id; return id;
} }
public void setId(Object id) {
this.id = id;
}
public String getText() { public String getText() {
return text; return text;
} }
public void setText(String text) {
this.text = text;
}
} }

@ -146,6 +146,12 @@ public class ThumbnailView extends View implements ImageLoader.ImageListener {
return true; return true;
} }
@Override
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
super.onMeasure(widthMeasureSpec, heightMeasureSpec);
calculate = true;
}
@Override @Override
protected void onDraw(Canvas canvas) { protected void onDraw(Canvas canvas) {
if (getAlpha() == 0f) { if (getAlpha() == 0f) {

@ -18,7 +18,8 @@ along with this program. If not, see <http://www.gnu.org/licenses/>.
<org.floens.chan.ui.cell.PostCell xmlns:android="http://schemas.android.com/apk/res/android" <org.floens.chan.ui.cell.PostCell xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools" xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent" android:layout_width="match_parent"
android:layout_height="wrap_content"> android:layout_height="wrap_content"
tools:ignore="RtlHardcoded,RtlSymmetry">
<org.floens.chan.ui.view.ThumbnailView <org.floens.chan.ui.view.ThumbnailView
android:id="@+id/thumbnail_view" android:id="@+id/thumbnail_view"

@ -0,0 +1,90 @@
<?xml version="1.0" encoding="utf-8"?><!--
Clover - 4chan browser https://github.com/Floens/Clover/
Copyright (C) 2014 Floens
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.
This program 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 this program. If not, see <http://www.gnu.org/licenses/>.
-->
<org.floens.chan.ui.cell.CardPostCell xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:card_view="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_margin="4dp"
android:padding="50dp"
card_view:cardBackgroundColor="?backcolor"
card_view:cardCornerRadius="2dp"
card_view:cardElevation="4dp">
<LinearLayout
android:layout_width="match_parent"
android:layout_height="280dp"
android:background="@drawable/item_background"
android:orientation="vertical">
<FrameLayout
android:layout_width="match_parent"
android:layout_height="wrap_content">
<org.floens.chan.ui.view.FixedRatioThumbnailView
android:id="@+id/thumbnail"
android:layout_width="match_parent"
android:layout_height="wrap_content" />
<ImageView
android:id="@+id/options"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_gravity="top|right"
android:paddingBottom="15dp"
android:paddingLeft="15dp"
android:paddingRight="5dp"
android:paddingTop="5dp"
android:src="?post_options_drawable"
tools:ignore="ContentDescription,RtlHardcoded" />
</FrameLayout>
<TextView
android:id="@+id/title"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:maxLines="3"
android:paddingLeft="8dp"
android:paddingRight="8dp"
android:paddingTop="8dp" />
<org.floens.chan.ui.view.FastTextView
android:id="@+id/comment"
android:layout_width="match_parent"
android:layout_height="0dp"
android:layout_weight="1"
android:paddingLeft="8dp"
android:paddingRight="8dp"
android:paddingTop="8dp"
android:textColor="?attr/text_color_primary" />
<TextView
android:id="@+id/replies"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:paddingBottom="8dp"
android:paddingLeft="8dp"
android:paddingRight="8dp"
android:paddingTop="4dp"
android:singleLine="true"
android:textColor="?attr/text_color_secondary" />
</LinearLayout>
</org.floens.chan.ui.cell.CardPostCell>

@ -16,6 +16,7 @@ You should have received a copy of the GNU General Public License
along with this program. If not, see <http://www.gnu.org/licenses/>. along with this program. If not, see <http://www.gnu.org/licenses/>.
--> -->
<org.floens.chan.ui.layout.ThreadLayout xmlns:android="http://schemas.android.com/apk/res/android" <org.floens.chan.ui.layout.ThreadLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent" android:layout_width="match_parent"
android:layout_height="match_parent"> android:layout_height="match_parent">
@ -33,6 +34,7 @@ along with this program. If not, see <http://www.gnu.org/licenses/>.
android:alpha="0" android:alpha="0"
android:scaleX="0" android:scaleX="0"
android:scaleY="0" android:scaleY="0"
android:src="@drawable/ic_create_white_24dp" /> android:src="@drawable/ic_create_white_24dp"
tools:ignore="RtlHardcoded" />
</org.floens.chan.ui.layout.ThreadLayout> </org.floens.chan.ui.layout.ThreadLayout>

@ -45,6 +45,7 @@ along with this program. If not, see <http://www.gnu.org/licenses/>.
android:layout_width="match_parent" android:layout_width="match_parent"
android:layout_height="0dp" android:layout_height="0dp"
android:layout_weight="1" android:layout_weight="1"
android:clipToPadding="false"
android:scrollbars="vertical" /> android:scrollbars="vertical" />
</org.floens.chan.ui.layout.ThreadListLayout> </org.floens.chan.ui.layout.ThreadListLayout>

@ -0,0 +1,23 @@
<?xml version="1.0" encoding="utf-8"?><!--
Clover - 4chan browser https://github.com/Floens/Clover/
Copyright (C) 2014 Floens
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.
This program 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 this program. If not, see <http://www.gnu.org/licenses/>.
-->
<resources>
<bool name="is_tablet">true</bool>
<dimen name="grid_card_width">180dp</dimen>
<integer name="grid_card_max_spans">5</integer>
</resources>

@ -1,4 +1,25 @@
<?xml version="1.0" encoding="utf-8"?> <?xml version="1.0" encoding="utf-8"?><!--
Clover - 4chan browser https://github.com/Floens/Clover/
Copyright (C) 2014 Floens
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.
This program 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 this program. If not, see <http://www.gnu.org/licenses/>.
-->
<resources> <resources>
<bool name="is_tablet">false</bool>
<dimen name="toolbar_height">56dp</dimen> <dimen name="toolbar_height">56dp</dimen>
<dimen name="grid_card_width">140dp</dimen>
<integer name="grid_card_max_spans">-1</integer>
</resources> </resources>

@ -46,6 +46,8 @@ along with this program. If not, see <http://www.gnu.org/licenses/>.
<item quantity="other">%d images</item> <item quantity="other">%d images</item>
</plurals> </plurals>
<string name="card_stats">%1$dR %2$dI</string>
<string name="action_reload">Reload</string> <string name="action_reload">Reload</string>
<string name="action_pin">Pin</string> <string name="action_pin">Pin</string>
<string name="action_open_browser">Open in browser</string> <string name="action_open_browser">Open in browser</string>
@ -53,6 +55,8 @@ along with this program. If not, see <http://www.gnu.org/licenses/>.
<string name="action_download_album">Download album</string> <string name="action_download_album">Download album</string>
<string name="action_search">Search</string> <string name="action_search">Search</string>
<string name="action_search_image">Image search</string> <string name="action_search_image">Image search</string>
<string name="action_switch_catalog">Catalog mode</string>
<string name="action_switch_board">Board mode</string>
<string name="search_hint">Search</string> <string name="search_hint">Search</string>
<string name="search_results">Found %1$s for "%2$s"</string> <string name="search_results">Found %1$s for "%2$s"</string>

Loading…
Cancel
Save