Make Post fields final, use a builder object.

Refactoring around Site and ChanLoaders.
Inject FilterEngine.
multisite
Floens 9 years ago
parent 6a6055ea98
commit aa4f96cacf
  1. 135
      Clover/app/src/main/java/org/floens/chan/chan/ChanLoader.java
  2. 33
      Clover/app/src/main/java/org/floens/chan/chan/ChanLoaderRequest.java
  3. 62
      Clover/app/src/main/java/org/floens/chan/chan/ChanLoaderRequestParams.java
  4. 34
      Clover/app/src/main/java/org/floens/chan/chan/ChanLoaderResponse.java
  5. 145
      Clover/app/src/main/java/org/floens/chan/chan/ChanParser.java
  6. 3
      Clover/app/src/main/java/org/floens/chan/core/database/DatabaseSavedReplyManager.java
  7. 7
      Clover/app/src/main/java/org/floens/chan/core/di/ChanGraph.java
  8. 44
      Clover/app/src/main/java/org/floens/chan/core/manager/FilterEngine.java
  9. 19
      Clover/app/src/main/java/org/floens/chan/core/manager/WatchManager.java
  10. 7
      Clover/app/src/main/java/org/floens/chan/core/model/Board.java
  11. 18
      Clover/app/src/main/java/org/floens/chan/core/model/Loadable.java
  12. 331
      Clover/app/src/main/java/org/floens/chan/core/model/Post.java
  13. 114
      Clover/app/src/main/java/org/floens/chan/core/model/PostImage.java
  14. 4
      Clover/app/src/main/java/org/floens/chan/core/model/PostLinkable.java
  15. 30
      Clover/app/src/main/java/org/floens/chan/core/model/SiteReference.java
  16. 31
      Clover/app/src/main/java/org/floens/chan/core/pool/ChanLoaderFactory.java
  17. 48
      Clover/app/src/main/java/org/floens/chan/core/presenter/ThreadPresenter.java
  18. 9
      Clover/app/src/main/java/org/floens/chan/core/site/Site.java
  19. 8
      Clover/app/src/main/java/org/floens/chan/core/site/SiteEndpoints.java
  20. 256
      Clover/app/src/main/java/org/floens/chan/core/site/loaders/Chan4ReaderRequest.java
  21. 36
      Clover/app/src/main/java/org/floens/chan/core/site/loaders/PostParseCallable.java
  22. 27
      Clover/app/src/main/java/org/floens/chan/core/site/sites/chan4/Chan4.java
  23. 4
      Clover/app/src/main/java/org/floens/chan/test/TestActivity.java
  24. 3
      Clover/app/src/main/java/org/floens/chan/ui/adapter/PostsFilter.java
  25. 2
      Clover/app/src/main/java/org/floens/chan/ui/cell/CardPostCell.java
  26. 8
      Clover/app/src/main/java/org/floens/chan/ui/cell/PostCell.java
  27. 2
      Clover/app/src/main/java/org/floens/chan/ui/cell/PostCellInterface.java
  28. 1
      Clover/app/src/main/java/org/floens/chan/ui/controller/BrowseController.java
  29. 6
      Clover/app/src/main/java/org/floens/chan/ui/controller/FiltersController.java
  30. 2
      Clover/app/src/main/java/org/floens/chan/ui/controller/PostRepliesController.java
  31. 32
      Clover/app/src/main/java/org/floens/chan/ui/controller/ThemeSettingsController.java
  32. 12
      Clover/app/src/main/java/org/floens/chan/ui/layout/FilterLayout.java
  33. 5
      Clover/app/src/main/java/org/floens/chan/ui/layout/ThreadLayout.java
  34. 5
      Clover/app/src/main/java/org/floens/chan/ui/layout/ThreadListLayout.java
  35. 2
      Clover/app/src/main/java/org/floens/chan/ui/service/WatchNotifier.java

@ -27,7 +27,6 @@ import org.floens.chan.core.exception.ChanLoaderException;
import org.floens.chan.core.model.ChanThread;
import org.floens.chan.core.model.Loadable;
import org.floens.chan.core.model.Post;
import org.floens.chan.core.net.ChanReaderRequest;
import org.floens.chan.ui.helper.PostHelper;
import org.floens.chan.utils.AndroidUtils;
import org.floens.chan.utils.Logger;
@ -44,11 +43,18 @@ import javax.inject.Inject;
import static org.floens.chan.Chan.getGraph;
public class ChanLoader implements Response.ErrorListener, Response.Listener<ChanReaderRequest.ChanReaderResponse> {
/**
* A ChanLoader is the loader for Loadables.
* <p>Obtain ChanLoaders with {@link org.floens.chan.core.pool.ChanLoaderFactory}.
* <p>ChanLoaders can load boards and threads, and return {@link ChanThread} objects on success, through
* {@link ChanLoaderCallback}.
* <p>For threads timers can be started with {@link #setTimer()} to do a request later.
*/
public class ChanLoader implements Response.ErrorListener, Response.Listener<ChanLoaderResponse> {
private static final String TAG = "ChanLoader";
private static final ScheduledExecutorService executor = Executors.newSingleThreadScheduledExecutor();
private static final int[] watchTimeouts = {10, 15, 20, 30, 60, 90, 120, 180, 240, 300, 600, 1800, 3600};
private static final int[] WATCH_TIMEOUTS = {10, 15, 20, 30, 60, 90, 120, 180, 240, 300, 600, 1800, 3600};
@Inject
RequestQueue volleyRequestQueue;
@ -57,13 +63,16 @@ public class ChanLoader implements Response.ErrorListener, Response.Listener<Cha
private final Loadable loadable;
private ChanThread thread;
private ChanReaderRequest request;
private ChanLoaderRequest request;
private int currentTimeout = 0;
private int lastPostCount;
private long lastLoadTime;
private ScheduledFuture<?> pendingFuture;
/**
* <b>Do not call this constructor yourself, obtain ChanLoaders through {@link org.floens.chan.core.pool.ChanLoaderFactory}</b>
*/
public ChanLoader(Loadable loadable) {
this.loadable = loadable;
@ -94,7 +103,7 @@ public class ChanLoader implements Response.ErrorListener, Response.Listener<Cha
if (listeners.isEmpty()) {
clearTimer();
if (request != null) {
request.cancel();
request.getVolleyRequest().cancel();
request = null;
}
return true;
@ -103,6 +112,10 @@ public class ChanLoader implements Response.ErrorListener, Response.Listener<Cha
}
}
public ChanThread getThread() {
return thread;
}
/**
* Request data for the first time.
*/
@ -110,7 +123,7 @@ public class ChanLoader implements Response.ErrorListener, Response.Listener<Cha
clearTimer();
if (request != null) {
request.cancel();
request.getVolleyRequest().cancel();
// request = null;
}
@ -127,9 +140,10 @@ public class ChanLoader implements Response.ErrorListener, Response.Listener<Cha
}
/**
* Request more data
* Request more data. This only works for thread loaders.<br>
* This clears any pending pending timers, created with {@link #setTimer()}.
*
* @return true if a request was started, false otherwise
* @return {@code true} if a new request was started, {@code false} otherwise.
*/
public boolean requestMoreData() {
clearPendingRunnable();
@ -179,6 +193,31 @@ public class ChanLoader implements Response.ErrorListener, Response.Listener<Cha
return loadable;
}
public void setTimer() {
clearPendingRunnable();
int watchTimeout = WATCH_TIMEOUTS[currentTimeout];
Logger.d(TAG, "Scheduled reload in " + watchTimeout + "s");
pendingFuture = executor.schedule(new Runnable() {
@Override
public void run() {
AndroidUtils.runOnUiThread(new Runnable() {
@Override
public void run() {
pendingFuture = null;
requestMoreData();
}
});
}
}, watchTimeout, TimeUnit.SECONDS);
}
public void clearTimer() {
currentTimeout = 0;
clearPendingRunnable();
}
/**
* Get the time in milliseconds until another loadMore is recommended
*/
@ -186,20 +225,28 @@ public class ChanLoader implements Response.ErrorListener, Response.Listener<Cha
if (request != null) {
return 0L;
} else {
long waitTime = watchTimeouts[Math.max(0, currentTimeout)] * 1000L;
long waitTime = WATCH_TIMEOUTS[Math.max(0, currentTimeout)] * 1000L;
return lastLoadTime + waitTime - Time.get();
}
}
public ChanThread getThread() {
return thread;
private ChanLoaderRequest getData() {
Logger.d(TAG, "Requested " + loadable.boardCode + ", " + loadable.no);
List<Post> cached = thread == null ? new ArrayList<Post>() : thread.posts;
request = loadable.getSite().loaderRequest(new ChanLoaderRequestParams(loadable, cached, this, this));
volleyRequestQueue.add(request.getVolleyRequest());
return request;
}
@Override
public void onResponse(ChanReaderRequest.ChanReaderResponse response) {
public void onResponse(ChanLoaderResponse response) {
request = null;
if (response.posts.size() == 0) {
if (response.posts.isEmpty()) {
onErrorResponse(new VolleyError("Post size is 0"));
return;
}
@ -228,7 +275,7 @@ public class ChanLoader implements Response.ErrorListener, Response.Listener<Cha
lastPostCount = postCount;
currentTimeout = 0;
} else {
currentTimeout = Math.min(currentTimeout + 1, watchTimeouts.length - 1);
currentTimeout = Math.min(currentTimeout + 1, WATCH_TIMEOUTS.length - 1);
}
for (ChanLoaderCallback l : listeners) {
@ -236,33 +283,18 @@ public class ChanLoader implements Response.ErrorListener, Response.Listener<Cha
}
}
@Override
public void onErrorResponse(VolleyError error) {
request = null;
Logger.i(TAG, "Loading error", error);
clearTimer();
ChanLoaderException loaderException = new ChanLoaderException(error);
for (ChanLoaderCallback l : listeners) {
l.onChanLoaderError(loaderException);
}
}
/**
* Final processing af a response that needs to happen on the main thread.
*
* @param response Response to process
*/
private void processResponse(ChanReaderRequest.ChanReaderResponse response) {
private void processResponse(ChanLoaderResponse response) {
if (loadable.isThreadMode() && thread.posts.size() > 0) {
// Replace some op parameters to the real op (index 0).
// This is done on the main thread to avoid race conditions.
Post realOp = thread.posts.get(0);
thread.op = realOp;
Post fakeOp = response.op;
Post.Builder fakeOp = response.op;
if (fakeOp != null) {
thread.closed = realOp.closed = fakeOp.closed;
thread.archived = realOp.archived = fakeOp.archived;
@ -276,29 +308,19 @@ public class ChanLoader implements Response.ErrorListener, Response.Listener<Cha
}
}
public void setTimer() {
clearPendingRunnable();
@Override
public void onErrorResponse(VolleyError error) {
request = null;
int watchTimeout = watchTimeouts[currentTimeout];
Logger.d(TAG, "Scheduled reload in " + watchTimeout + "s");
Logger.i(TAG, "Loading error", error);
pendingFuture = executor.schedule(new Runnable() {
@Override
public void run() {
AndroidUtils.runOnUiThread(new Runnable() {
@Override
public void run() {
pendingFuture = null;
requestMoreData();
}
});
}
}, watchTimeout, TimeUnit.SECONDS);
}
clearTimer();
public void clearTimer() {
currentTimeout = 0;
clearPendingRunnable();
ChanLoaderException loaderException = new ChanLoaderException(error);
for (ChanLoaderCallback l : listeners) {
l.onChanLoaderError(loaderException);
}
}
private void clearPendingRunnable() {
@ -309,17 +331,6 @@ public class ChanLoader implements Response.ErrorListener, Response.Listener<Cha
}
}
private ChanReaderRequest getData() {
Logger.d(TAG, "Requested " + loadable.boardCode + ", " + loadable.no);
List<Post> cached = thread == null ? new ArrayList<Post>() : thread.posts;
ChanReaderRequest request = ChanReaderRequest.newInstance(loadable, cached, this, this);
volleyRequestQueue.add(request);
return request;
}
public interface ChanLoaderCallback {
void onChanLoaderData(ChanThread result);

@ -0,0 +1,33 @@
/*
* 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.chan;
import com.android.volley.Request;
public class ChanLoaderRequest {
private Request<ChanLoaderResponse> volleyRequest;
public ChanLoaderRequest(Request<ChanLoaderResponse> volleyRequest) {
this.volleyRequest = volleyRequest;
}
public Request<ChanLoaderResponse> getVolleyRequest() {
return volleyRequest;
}
}

@ -0,0 +1,62 @@
/*
* 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.chan;
import com.android.volley.Response;
import org.floens.chan.core.model.Loadable;
import org.floens.chan.core.model.Post;
import java.util.List;
/**
* A request from ChanLoader to load something.
*/
public class ChanLoaderRequestParams {
/**
* Related loadable for the request.
*/
public final Loadable loadable;
/**
* Cached Post objects from previous loads, or an empty list.
*/
public final List<Post> cached;
/**
* Success listener.
*/
public final Response.Listener<ChanLoaderResponse> listener;
/**
* Error listener.
*/
public final Response.ErrorListener errorListener;
public ChanLoaderRequestParams(Loadable loadable,
List<Post> cached,
Response.Listener<ChanLoaderResponse> listener,
Response.ErrorListener errorListener) {
this.loadable = loadable;
this.cached = cached;
this.listener = listener;
this.errorListener = errorListener;
}
}

@ -0,0 +1,34 @@
/*
* 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.chan;
import org.floens.chan.core.model.Post;
import java.util.List;
public class ChanLoaderResponse {
// Op Post that is created new each time.
// Used to later copy members like image count to the real op on the main thread.
public final Post.Builder op;
public final List<Post> posts;
public ChanLoaderResponse(Post.Builder op, List<Post> posts) {
this.op = op;
this.posts = posts;
}
}

@ -61,7 +61,9 @@ import static org.floens.chan.utils.AndroidUtils.sp;
@Singleton
public class ChanParser {
private static final String TAG = "ChanParser";
private static final Pattern colorPattern = Pattern.compile("color:#([0-9a-fA-F]*)");
private static final Pattern COLOR_PATTERN = Pattern.compile("color:#([0-9a-fA-F]*)");
private static final String SAVED_REPLY_SUFFIX = " (You)";
private static final String OP_REPLY_SUFFIX = " (OP)";
@Inject
DatabaseManager databaseManager;
@ -70,34 +72,36 @@ public class ChanParser {
public ChanParser() {
}
public void parse(Post post) {
parse(null, post);
public Post parse(Post.Builder post) {
return parse(null, post);
}
public void parse(Theme theme, Post post) {
public Post parse(Theme theme, Post.Builder builder) {
if (theme == null) {
theme = ThemeHelper.getInstance().getTheme();
}
try {
if (!TextUtils.isEmpty(post.name)) {
post.name = Parser.unescapeEntities(post.name, false);
if (!TextUtils.isEmpty(builder.name)) {
builder.name = Parser.unescapeEntities(builder.name, false);
}
if (!TextUtils.isEmpty(post.subject)) {
post.subject = Parser.unescapeEntities(post.subject, false);
if (!TextUtils.isEmpty(builder.subject)) {
builder.subject = Parser.unescapeEntities(builder.subject, false);
}
} catch (Exception e) {
e.printStackTrace();
}
parseSpans(theme, post);
parseSpans(theme, builder);
if (post.rawComment != null) {
post.comment = parseComment(theme, post, post.rawComment);
if (builder.comment != null) {
builder.comment = parseComment(theme, builder, builder.comment);
} else {
post.comment = "";
builder.comment = "";
}
return builder.build();
}
/**
@ -105,48 +109,55 @@ public class ChanParser {
* This is done on a background thread for performance, even when it is UI code.<br>
* The results will be placed on the Post.*Span members.
*
* @param theme Theme to use for parsing
* @param post Post to get data from
* @param theme Theme to use for parsing
* @param builder Post builder to get data from
*/
private void parseSpans(Theme theme, Post post) {
private void parseSpans(Theme theme, Post.Builder builder) {
boolean anonymize = ChanSettings.anonymize.get();
boolean anonymizeIds = ChanSettings.anonymizeIds.get();
final String defaultName = "Anonymous";
if (anonymize) {
post.name = "Anonymous";
post.tripcode = "";
builder.name(defaultName);
builder.tripcode("");
}
if (anonymizeIds) {
post.id = "";
builder.posterId("");
}
SpannableString subjectSpan = null;
SpannableString nameSpan = null;
SpannableString tripcodeSpan = null;
SpannableString idSpan = null;
SpannableString capcodeSpan = null;
int detailsSizePx = sp(Integer.parseInt(ChanSettings.fontSize.get()) - 4);
if (!TextUtils.isEmpty(post.subject)) {
post.subjectSpan = new SpannableString(post.subject);
if (!TextUtils.isEmpty(builder.subject)) {
subjectSpan = new SpannableString(builder.subject);
// Do not set another color when the post is in stub mode, it sets text_color_secondary
if (!post.filterStub) {
post.subjectSpan.setSpan(new ForegroundColorSpanHashed(theme.subjectColor), 0, post.subjectSpan.length(), 0);
if (!builder.filterStub) {
subjectSpan.setSpan(new ForegroundColorSpanHashed(theme.subjectColor), 0, subjectSpan.length(), 0);
}
}
if (!TextUtils.isEmpty(post.name) && (!post.name.equals("Anonymous") || ChanSettings.showAnonymousName.get())) {
post.nameSpan = new SpannableString(post.name);
post.nameSpan.setSpan(new ForegroundColorSpanHashed(theme.nameColor), 0, post.nameSpan.length(), 0);
if (!TextUtils.isEmpty(builder.name) && (!builder.name.equals(defaultName) || ChanSettings.showAnonymousName.get())) {
nameSpan = new SpannableString(builder.name);
nameSpan.setSpan(new ForegroundColorSpanHashed(theme.nameColor), 0, nameSpan.length(), 0);
}
if (!TextUtils.isEmpty(post.tripcode)) {
post.tripcodeSpan = new SpannableString(post.tripcode);
post.tripcodeSpan.setSpan(new ForegroundColorSpanHashed(theme.nameColor), 0, post.tripcodeSpan.length(), 0);
post.tripcodeSpan.setSpan(new AbsoluteSizeSpanHashed(detailsSizePx), 0, post.tripcodeSpan.length(), 0);
if (!TextUtils.isEmpty(builder.tripcode)) {
tripcodeSpan = new SpannableString(builder.tripcode);
tripcodeSpan.setSpan(new ForegroundColorSpanHashed(theme.nameColor), 0, tripcodeSpan.length(), 0);
tripcodeSpan.setSpan(new AbsoluteSizeSpanHashed(detailsSizePx), 0, tripcodeSpan.length(), 0);
}
if (!TextUtils.isEmpty(post.id)) {
post.idSpan = new SpannableString(" ID: " + post.id + " ");
if (!TextUtils.isEmpty(builder.posterId)) {
idSpan = new SpannableString(" ID: " + builder.posterId + " ");
// Stolen from the 4chan extension
int hash = post.id.hashCode();
int hash = builder.posterId.hashCode();
int r = (hash >> 24) & 0xff;
int g = (hash >> 16) & 0xff;
@ -156,40 +167,42 @@ public class ChanParser {
boolean lightColor = (r * 0.299f) + (g * 0.587f) + (b * 0.114f) > 125f;
int idBgColor = lightColor ? theme.idBackgroundLight : theme.idBackgroundDark;
post.idSpan.setSpan(new ForegroundColorSpanHashed(idColor), 0, post.idSpan.length(), 0);
post.idSpan.setSpan(new BackgroundColorSpan(idBgColor), 0, post.idSpan.length(), 0);
post.idSpan.setSpan(new AbsoluteSizeSpanHashed(detailsSizePx), 0, post.idSpan.length(), 0);
idSpan.setSpan(new ForegroundColorSpanHashed(idColor), 0, idSpan.length(), 0);
idSpan.setSpan(new BackgroundColorSpan(idBgColor), 0, idSpan.length(), 0);
idSpan.setSpan(new AbsoluteSizeSpanHashed(detailsSizePx), 0, idSpan.length(), 0);
}
if (!TextUtils.isEmpty(post.capcode)) {
post.capcodeSpan = new SpannableString("Capcode: " + post.capcode);
post.capcodeSpan.setSpan(new ForegroundColorSpanHashed(theme.capcodeColor), 0, post.capcodeSpan.length(), 0);
post.capcodeSpan.setSpan(new AbsoluteSizeSpanHashed(detailsSizePx), 0, post.capcodeSpan.length(), 0);
if (!TextUtils.isEmpty(builder.moderatorCapcode)) {
capcodeSpan = new SpannableString("Capcode: " + builder.moderatorCapcode);
capcodeSpan.setSpan(new ForegroundColorSpanHashed(theme.capcodeColor), 0, capcodeSpan.length(), 0);
capcodeSpan.setSpan(new AbsoluteSizeSpanHashed(detailsSizePx), 0, capcodeSpan.length(), 0);
}
post.nameTripcodeIdCapcodeSpan = new SpannableString("");
if (post.nameSpan != null) {
post.nameTripcodeIdCapcodeSpan = TextUtils.concat(post.nameTripcodeIdCapcodeSpan, post.nameSpan, " ");
CharSequence nameTripcodeIdCapcodeSpan = new SpannableString("");
if (nameSpan != null) {
nameTripcodeIdCapcodeSpan = TextUtils.concat(nameTripcodeIdCapcodeSpan, nameSpan, " ");
}
if (post.tripcodeSpan != null) {
post.nameTripcodeIdCapcodeSpan = TextUtils.concat(post.nameTripcodeIdCapcodeSpan, post.tripcodeSpan, " ");
if (tripcodeSpan != null) {
nameTripcodeIdCapcodeSpan = TextUtils.concat(nameTripcodeIdCapcodeSpan, tripcodeSpan, " ");
}
if (post.idSpan != null) {
post.nameTripcodeIdCapcodeSpan = TextUtils.concat(post.nameTripcodeIdCapcodeSpan, post.idSpan, " ");
if (idSpan != null) {
nameTripcodeIdCapcodeSpan = TextUtils.concat(nameTripcodeIdCapcodeSpan, idSpan, " ");
}
if (post.capcodeSpan != null) {
post.nameTripcodeIdCapcodeSpan = TextUtils.concat(post.nameTripcodeIdCapcodeSpan, post.capcodeSpan, " ");
if (capcodeSpan != null) {
nameTripcodeIdCapcodeSpan = TextUtils.concat(nameTripcodeIdCapcodeSpan, capcodeSpan, " ");
}
builder.spans(subjectSpan, nameTripcodeIdCapcodeSpan);
}
private CharSequence parseComment(Theme theme, Post post, String commentRaw) {
private CharSequence parseComment(Theme theme, Post.Builder post, CharSequence commentRaw) {
CharSequence total = new SpannableString("");
try {
String comment = commentRaw.replace("<wbr>", "");
String comment = commentRaw.toString().replace("<wbr>", "");
Document document = Jsoup.parseBodyFragment(comment);
@ -211,7 +224,7 @@ public class ChanParser {
return total;
}
private CharSequence parseNode(Theme theme, Post post, Node node) {
private CharSequence parseNode(Theme theme, Post.Builder post, Node node) {
if (node instanceof TextNode) {
String text = ((TextNode) node).text();
SpannableString spannable = new SpannableString(text);
@ -243,8 +256,8 @@ public class ChanParser {
if (!TextUtils.isEmpty(style)) {
style = style.replace(" ", "");
// private static final Pattern colorPattern = Pattern.compile("color:#([0-9a-fA-F]*)");
Matcher matcher = colorPattern.matcher(style);
// private static final Pattern COLOR_PATTERN = Pattern.compile("color:#([0-9a-fA-F]*)");
Matcher matcher = COLOR_PATTERN.matcher(style);
int hexColor = 0xff0000;
if (matcher.find()) {
@ -331,9 +344,9 @@ public class ChanParser {
SpannableString link = new SpannableString(spoiler.text());
PostLinkable pl = new PostLinkable(theme, post, spoiler.text(), spoiler.text(), PostLinkable.Type.SPOILER);
PostLinkable pl = new PostLinkable(theme, spoiler.text(), spoiler.text(), PostLinkable.Type.SPOILER);
link.setSpan(pl, 0, link.length(), 0);
post.linkables.add(pl);
post.addLinkable(pl);
return link;
}
@ -363,7 +376,7 @@ public class ChanParser {
}
}
private CharSequence parseAnchor(Theme theme, Post post, Element anchor) {
private CharSequence parseAnchor(Theme theme, Post.Builder post, Element anchor) {
String href = anchor.attr("href");
Set<String> classes = anchor.classNames();
@ -411,16 +424,16 @@ public class ChanParser {
t = PostLinkable.Type.QUOTE;
key = anchor.text();
value = id;
post.repliesTo.add(id);
post.addReplyTo(id);
// Append OP when its a reply to OP
if (id == post.resto) {
key += " (OP)";
if (id == post.opId) {
key += OP_REPLY_SUFFIX;
}
// Append You when it's a reply to an saved reply
if (databaseManager.getDatabaseSavedReplyManager().isSaved(post.boardId, id)) {
key += " (You)";
if (databaseManager.getDatabaseSavedReplyManager().isSaved(post.board.code, id)) {
key += SAVED_REPLY_SUFFIX;
}
}
}
@ -433,9 +446,9 @@ public class ChanParser {
if (t != null && key != null && value != null) {
SpannableString link = new SpannableString(key);
PostLinkable pl = new PostLinkable(theme, post, key, value, t);
PostLinkable pl = new PostLinkable(theme, key, value, t);
link.setSpan(pl, 0, link.length(), 0);
post.linkables.add(pl);
post.addLinkable(pl);
return link;
} else {
@ -443,7 +456,7 @@ public class ChanParser {
}
}
private void detectLinks(Theme theme, Post post, String text, SpannableString spannable) {
private void detectLinks(Theme theme, Post.Builder post, String text, SpannableString spannable) {
int startPos = 0;
int endPos;
while (true) {
@ -466,9 +479,9 @@ public class ChanParser {
String linkString = text.substring(startPos, endPos);
PostLinkable pl = new PostLinkable(theme, post, linkString, linkString, PostLinkable.Type.LINK);
PostLinkable pl = new PostLinkable(theme, linkString, linkString, PostLinkable.Type.LINK);
spannable.setSpan(pl, startPos, endPos, 0);
post.linkables.add(pl);
post.addLinkable(pl);
startPos = endPos;
}

@ -17,6 +17,8 @@
*/
package org.floens.chan.core.database;
import android.support.annotation.AnyThread;
import com.j256.ormlite.stmt.QueryBuilder;
import com.j256.ormlite.table.TableUtils;
@ -57,6 +59,7 @@ public class DatabaseSavedReplyManager {
* @param no post number
* @return {@code true} if the post is in the saved reply database, {@code false} otherwise.
*/
@AnyThread
public boolean isSaved(String board, int no) {
// TODO(multi-site)
synchronized (savedRepliesByNo) {

@ -11,7 +11,8 @@ import org.floens.chan.core.database.DatabaseManager;
import org.floens.chan.core.http.ReplyManager;
import org.floens.chan.core.manager.BoardManager;
import org.floens.chan.core.manager.FilterEngine;
import org.floens.chan.core.net.ChanReaderRequest;
import org.floens.chan.core.manager.WatchManager;
import org.floens.chan.core.site.loaders.Chan4ReaderRequest;
import org.floens.chan.core.presenter.ImageViewerPresenter;
import org.floens.chan.core.presenter.ReplyPresenter;
import org.floens.chan.core.presenter.ThreadPresenter;
@ -66,7 +67,7 @@ public interface ChanGraph {
void inject(ReplyPresenter replyPresenter);
void inject(ChanReaderRequest chanReaderRequest);
void inject(Chan4ReaderRequest chanReaderRequest);
void inject(ThreadLayout threadLayout);
@ -113,4 +114,6 @@ public interface ChanGraph {
void inject(ImageSaveTask imageSaveTask);
void inject(ViewThreadController viewThreadController);
void inject(WatchManager.PinWatcher pinWatcher);
}

@ -17,9 +17,9 @@
*/
package org.floens.chan.core.manager;
import android.support.annotation.AnyThread;
import android.text.TextUtils;
import org.floens.chan.Chan;
import org.floens.chan.core.database.DatabaseFilterManager;
import org.floens.chan.core.database.DatabaseManager;
import org.floens.chan.core.model.Board;
@ -37,18 +37,12 @@ import java.util.regex.Pattern;
import java.util.regex.PatternSyntaxException;
import javax.inject.Inject;
import javax.inject.Singleton;
import static org.floens.chan.Chan.getGraph;
@Singleton
public class FilterEngine {
private static final String TAG = "FilterEngine";
private static final FilterEngine instance = new FilterEngine();
public static FilterEngine getInstance() {
return instance;
}
public enum FilterAction {
HIDE(0),
COLOR(1),
@ -73,21 +67,18 @@ public class FilterEngine {
}
}
private final Map<String, Pattern> patternCache = new HashMap<>();
@Inject
DatabaseManager databaseManager;
@Inject
BoardManager boardManager;
private final DatabaseManager databaseManager;
private final BoardManager boardManager;
private final DatabaseFilterManager databaseFilterManager;
private List<Filter> filters;
private final Map<String, Pattern> patternCache = new HashMap<>();
private final List<Filter> enabledFilters = new ArrayList<>();
private FilterEngine() {
getGraph().inject(this);
@Inject
public FilterEngine(DatabaseManager databaseManager, BoardManager boardManager) {
this.databaseManager = databaseManager;
this.boardManager = boardManager;
databaseFilterManager = databaseManager.getDatabaseFilterManager();
update();
}
@ -111,7 +102,7 @@ public class FilterEngine {
}
// threadsafe
public boolean matches(Filter filter, Post post) {
public boolean matches(Filter filter, Post.Builder post) {
if ((filter.type & FilterType.TRIPCODE.flag) != 0 && matches(filter, FilterType.TRIPCODE.isRegex, post.tripcode, false)) {
return true;
}
@ -120,11 +111,11 @@ public class FilterEngine {
return true;
}
if ((filter.type & FilterType.COMMENT.flag) != 0 && matches(filter, FilterType.COMMENT.isRegex, post.rawComment, false)) {
if ((filter.type & FilterType.COMMENT.flag) != 0 && matches(filter, FilterType.COMMENT.isRegex, post.comment.toString(), false)) {
return true;
}
if ((filter.type & FilterType.ID.flag) != 0 && matches(filter, FilterType.ID.isRegex, post.id, false)) {
if ((filter.type & FilterType.ID.flag) != 0 && matches(filter, FilterType.ID.isRegex, post.posterId, false)) {
return true;
}
@ -132,14 +123,15 @@ public class FilterEngine {
return true;
}
if ((filter.type & FilterType.FILENAME.flag) != 0 && matches(filter, FilterType.FILENAME.isRegex, post.filename, false)) {
String filename = post.image != null ? post.image.filename : null;
if (filename != null && (filter.type & FilterType.FILENAME.flag) != 0 && matches(filter, FilterType.FILENAME.isRegex, filename, false)) {
return true;
}
return false;
}
// threadsafe
@AnyThread
public boolean matches(Filter filter, boolean matchRegex, String text, boolean forceCompile) {
if (TextUtils.isEmpty(text)) {
return false;
@ -184,7 +176,7 @@ public class FilterEngine {
private static final Pattern filterFilthyPattern = Pattern.compile("(\\.|\\^|\\$|\\*|\\+|\\?|\\(|\\)|\\[|\\]|\\{|\\}|\\\\|\\||\\-)");
private static final Pattern wildcardPattern = Pattern.compile("\\\\\\*"); // an escaped \ and an escaped *, to replace an escaped * from escapeRegex
// threadsafe
@AnyThread
public Pattern compile(String rawPattern) {
if (TextUtils.isEmpty(rawPattern)) {
return null;
@ -262,7 +254,7 @@ public class FilterEngine {
}
private void update() {
filters = databaseManager.runTaskSync(databaseFilterManager.getFilters());
List<Filter> filters = databaseManager.runTaskSync(databaseFilterManager.getFilters());
List<Filter> enabled = new ArrayList<>();
for (Filter filter : filters) {
if (filter.enabled) {

@ -35,7 +35,8 @@ import org.floens.chan.core.model.ChanThread;
import org.floens.chan.core.model.Loadable;
import org.floens.chan.core.model.Pin;
import org.floens.chan.core.model.Post;
import org.floens.chan.core.pool.LoaderPool;
import org.floens.chan.core.model.PostImage;
import org.floens.chan.core.pool.ChanLoaderFactory;
import org.floens.chan.core.settings.ChanSettings;
import org.floens.chan.ui.helper.PostHelper;
import org.floens.chan.ui.service.WatchNotifier;
@ -56,6 +57,7 @@ import javax.inject.Singleton;
import de.greenrobot.event.EventBus;
import static org.floens.chan.Chan.getGraph;
import static org.floens.chan.utils.AndroidUtils.getAppContext;
/**
@ -159,7 +161,8 @@ public class WatchManager {
Pin pin = new Pin();
pin.loadable = loadable;
pin.loadable.title = PostHelper.getTitle(opPost, loadable);
pin.thumbnailUrl = opPost.thumbnailUrl;
PostImage image = opPost.image;
pin.thumbnailUrl = image == null ? "" : image.thumbnailUrl;
return createPin(pin);
}
@ -648,6 +651,9 @@ public class WatchManager {
public class PinWatcher implements ChanLoader.ChanLoaderCallback {
private static final String TAG = "PinWatcher";
@Inject
ChanLoaderFactory chanLoaderFactory;
private final Pin pin;
private ChanLoader chanLoader;
@ -658,9 +664,10 @@ public class WatchManager {
public PinWatcher(Pin pin) {
this.pin = pin;
getGraph().inject(this);
Logger.d(TAG, "PinWatcher: created for " + pin);
chanLoader = LoaderPool.getInstance().obtain(pin.loadable, this);
chanLoader = chanLoaderFactory.obtain(pin.loadable, this);
}
public List<Post> getUnviewedPosts() {
@ -696,7 +703,7 @@ public class WatchManager {
private void destroy() {
if (chanLoader != null) {
Logger.d(TAG, "PinWatcher: destroyed for " + pin);
LoaderPool.getInstance().release(chanLoader, this);
chanLoaderFactory.release(chanLoader, this);
chanLoader = null;
}
}
@ -738,8 +745,8 @@ public class WatchManager {
public void onChanLoaderData(ChanThread thread) {
pin.isError = false;
if (pin.thumbnailUrl == null && thread.op != null && thread.op.hasImage) {
pin.thumbnailUrl = thread.op.thumbnailUrl;
if (pin.thumbnailUrl == null && thread.op != null && thread.op.image != null) {
pin.thumbnailUrl = thread.op.image.thumbnailUrl;
}
// Populate posts list

@ -25,7 +25,7 @@ import com.j256.ormlite.table.DatabaseTable;
import org.floens.chan.core.site.Site;
@DatabaseTable
public class Board {
public class Board implements SiteReference {
public Board() {
}
@ -148,6 +148,11 @@ public class Board {
return true;
}
@Override
public Site getSite() {
return site;
}
public String getName() {
return "/" + code + "/ \u2013 " + name;
}

@ -31,9 +31,11 @@ import org.floens.chan.core.site.Site;
* - It keeps track of the list index where the user last viewed.<br>
* - It keeps track of what post was last seen and loaded.<br>
* - It keeps track of the title the toolbar should show, generated from the first post (so after loading).<br>
* <p>Obtain Loadables through {@link org.floens.chan.core.database.DatabaseLoadableManager} to make sure everyone
* references the same loadable and that the loadable is properly saved in the database.
*/
@DatabaseTable
public class Loadable {
public class Loadable implements SiteReference {
@DatabaseField(generatedId = true)
public int id;
@ -116,6 +118,11 @@ public class Loadable {
return loadable;
}
@Override
public Site getSite() {
return site;
}
public void setTitle(String title) {
if (!TextUtils.equals(this.title, title)) {
this.title = title;
@ -161,6 +168,10 @@ public class Loadable {
Loadable other = (Loadable) object;
if ((site.id() == other.site.id() && (site != other.site))) {
throw new IllegalStateException(); // TODO(multi-site) remove
}
if (site != other.site) {
return false;
}
@ -220,6 +231,11 @@ public class Loadable {
return mode == Mode.CATALOG;
}
// TODO(multi-site) remove
public boolean isFromDatabase() {
return id > 0;
}
public static Loadable readFromParcel(Parcel parcel) {
Loadable loadable = new Loadable();
/*loadable.id = */

@ -17,170 +17,305 @@
*/
package org.floens.chan.core.model;
import android.text.SpannableString;
import android.text.TextUtils;
import org.floens.chan.chan.ChanParser;
import org.floens.chan.core.settings.ChanSettings;
import org.jsoup.parser.Parser;
import java.util.ArrayList;
import java.util.Collections;
import java.util.HashSet;
import java.util.List;
import java.util.Set;
import java.util.TreeSet;
import java.util.concurrent.atomic.AtomicBoolean;
import static org.floens.chan.Chan.getGraph;
/**
* Contains all data needed to represent a single post.<br>
* Call {@link #finish()} to parse the comment etc. The post data is invalid if finish returns false.<br>
* This class has members that are threadsafe and some that are not, see the source for more info.
* All {@code final} fields are thread-safe.
*/
public class Post {
// *** These next members don't get changed after finish() is called. Effectively final. ***
public String boardId;
public final String boardId;
public Board board;
public final Board board;
public int no = -1;
public final int no;
public int resto = -1;
public final boolean isOP;
public boolean isOP = false;
// public final String date;
public String date;
public final String name;
public String name = "";
public final CharSequence comment;
public CharSequence comment = "";
public final String subject;
public String subject = "";
public final PostImage image;
public long tim = -1;
public final String tripcode;
public String ext;
public final String id;
public String filename;
public final String capcode;
public int imageWidth;
public final String country;
public int imageHeight;
public final String countryName;
public boolean hasImage = false;
/**
* Unix timestamp, in seconds.
*/
public final long time;
public PostImage image;
public final String countryUrl;
public String thumbnailUrl;
public final boolean isSavedReply;
public String imageUrl;
public final int filterHighlightedColor;
public String tripcode = "";
public final boolean filterStub;
public String id = "";
public final boolean filterRemove;
public String capcode = "";
/**
* This post replies to the these ids.
*/
public final Set<Integer> repliesTo;
public String country = "";
public final List<PostLinkable> linkables;
public String countryName = "";
public final CharSequence subjectSpan;
public long time = -1;
public final CharSequence nameTripcodeIdCapcodeSpan;
public long fileSize;
// These members may only mutate on the main thread.
public boolean sticky = false;
public boolean closed = false;
public boolean archived = false;
public int replies = -1;
public int images = -1;
public int uniqueIps = 1;
public String title = "";
public String rawComment;
// Atomic, any thread.
public final AtomicBoolean deleted = new AtomicBoolean(false);
public String countryUrl;
/**
* These ids replied to this post.<br>
* <b>synchronize on this when accessing.</b>
*/
public final List<Integer> repliesFrom = new ArrayList<>();
public boolean spoiler = false;
private Post(Builder builder) {
board = builder.board;
boardId = builder.board.code;
no = builder.id;
public boolean isSavedReply = false;
isOP = builder.op;
replies = builder.replies;
images = builder.images;
uniqueIps = builder.uniqueIps;
sticky = builder.sticky;
closed = builder.closed;
archived = builder.archived;
public int filterHighlightedColor = 0;
subject = builder.subject;
name = builder.name;
comment = builder.comment;
tripcode = builder.tripcode;
public boolean filterStub = false;
time = builder.unixTimestampSeconds;
image = builder.image;
public boolean filterRemove = false;
country = builder.countryCode;
countryName = builder.countryName;
countryUrl = builder.countryUrl;
id = builder.posterId;
capcode = builder.moderatorCapcode;
/**
* This post replies to the these ids. Is an unmodifiable set after finish().
*/
public Set<Integer> repliesTo = new TreeSet<>();
filterHighlightedColor = builder.filterHighlightedColor;
filterStub = builder.filterStub;
filterRemove = builder.filterRemove;
public final ArrayList<PostLinkable> linkables = new ArrayList<>();
isSavedReply = builder.isSavedReply;
public SpannableString subjectSpan;
subjectSpan = builder.subjectSpan;
nameTripcodeIdCapcodeSpan = builder.nameTripcodeIdCapcodeSpan;
public SpannableString nameSpan;
linkables = Collections.unmodifiableList(builder.linkables);
repliesTo = Collections.unmodifiableSet(builder.repliesToIds);
}
public SpannableString tripcodeSpan;
public static final class Builder {
public Board board;
public int id = -1;
public int opId = -1;
public SpannableString idSpan;
public boolean op;
public int replies;
public int images;
public int uniqueIps;
public boolean sticky;
public boolean closed;
public boolean archived;
public SpannableString capcodeSpan;
public String subject = "";
public String name = "";
public CharSequence comment = "";
public String tripcode = "";
public CharSequence nameTripcodeIdCapcodeSpan;
public long unixTimestampSeconds = -1;
public PostImage image;
// *** These next members may only change on the main thread after finish(). ***
public boolean sticky = false;
public boolean closed = false;
public boolean archived = false;
public int replies = -1;
public int images = -1;
public int uniqueIps = 1;
public String title = "";
public String countryCode;
public String countryName;
public String countryUrl;
// *** Threadsafe members, may be read and modified on any thread. ***
public AtomicBoolean deleted = new AtomicBoolean(false);
public String posterId = "";
public String moderatorCapcode = "";
// *** Manual synchronization needed. ***
/**
* These ids replied to this post.<br>
* <b>synchronize on this when accessing.</b>
*/
public final List<Integer> repliesFrom = new ArrayList<>();
public int filterHighlightedColor;
public boolean filterStub;
public boolean filterRemove;
/**
* Finish up the data: parse the comment, check if the data is valid etc.
*
* @return false if this data is invalid
*/
public boolean finish() {
if (boardId == null || no < 0 || resto < 0 || date == null || time < 0) {
return false;
public boolean isSavedReply;
public CharSequence subjectSpan;
public CharSequence nameTripcodeIdCapcodeSpan;
private List<PostLinkable> linkables = new ArrayList<>();
private Set<Integer> repliesToIds = new HashSet<>();
public Builder() {
}
isOP = resto == 0;
public Builder board(Board board) {
this.board = board;
return this;
}
public Builder id(int id) {
this.id = id;
return this;
}
public Builder opId(int opId) {
this.opId = opId;
return this;
}
public Builder op(boolean op) {
this.op = op;
return this;
}
public Builder replies(int replies) {
this.replies = replies;
return this;
}
public Builder images(int images) {
this.images = images;
return this;
}
public Builder uniqueIps(int uniqueIps) {
this.uniqueIps = uniqueIps;
return this;
}
public Builder sticky(boolean sticky) {
this.sticky = sticky;
return this;
}
if (isOP && (replies < 0 || images < 0)) {
return false;
public Builder archived(boolean archived) {
this.archived = archived;
return this;
}
if (filename != null && ext != null && imageWidth > 0 && imageHeight > 0 && tim >= 0) {
hasImage = true;
// TODO: only use #image
imageUrl = board.site.endpoints().imageUrl(this);
filename = Parser.unescapeEntities(filename, false);
thumbnailUrl = board.site.endpoints().thumbnailUrl(this);
image = new PostImage(String.valueOf(tim), thumbnailUrl, imageUrl, filename, ext, imageWidth, imageHeight, spoiler, fileSize);
public Builder closed(boolean closed) {
this.closed = closed;
return this;
}
if (!TextUtils.isEmpty(country)) {
countryUrl = board.site.endpoints().flag(this);
public Builder subject(String subject) {
this.subject = subject;
return this;
}
if (ChanSettings.revealImageSpoilers.get()) {
spoiler = false;
public Builder name(String name) {
this.name = name;
return this;
}
ChanParser chanParser = getGraph().getChanParser();
chanParser.parse(this);
public Builder comment(String comment) {
this.comment = comment;
return this;
}
repliesTo = Collections.unmodifiableSet(repliesTo);
public Builder tripcode(String tripcode) {
this.tripcode = tripcode;
return this;
}
return true;
public Builder setUnixTimestampSeconds(long unixTimestampSeconds) {
this.unixTimestampSeconds = unixTimestampSeconds;
return this;
}
public Builder image(PostImage image) {
this.image = image;
return this;
}
public Builder posterId(String posterId) {
this.posterId = posterId;
return this;
}
public Builder moderatorCapcode(String moderatorCapcode) {
this.moderatorCapcode = moderatorCapcode;
return this;
}
public Builder country(String countryCode, String countryName, String countryUrl) {
this.countryCode = countryCode;
this.countryName = countryName;
this.countryUrl = countryUrl;
return this;
}
public Builder filter(int highlightedColor, boolean stub, boolean remove) {
filterHighlightedColor = highlightedColor;
filterStub = stub;
filterRemove = remove;
return this;
}
public Builder isSavedReply(boolean isSavedReply) {
this.isSavedReply = isSavedReply;
return this;
}
public Builder spans(CharSequence subjectSpan, CharSequence nameTripcodeIdCapcodeSpan) {
this.subjectSpan = subjectSpan;
this.nameTripcodeIdCapcodeSpan = nameTripcodeIdCapcodeSpan;
return this;
}
public Builder addLinkable(PostLinkable linkable) {
linkables.add(linkable);
return this;
}
public Builder addReplyTo(int postId) {
repliesToIds.add(postId);
return this;
}
public Post build() {
if (board == null || id < 0 || opId < 0 || unixTimestampSeconds < 0 || comment == null) {
throw new IllegalArgumentException("Post data not complete");
}
return new Post(this);
}
}
}

@ -17,33 +17,35 @@
*/
package org.floens.chan.core.model;
import org.floens.chan.core.settings.ChanSettings;
public class PostImage {
public enum Type {
STATIC, GIF, MOVIE
}
public String originalName;
public String thumbnailUrl;
public String imageUrl;
public String filename;
public String extension;
public int imageWidth;
public int imageHeight;
public boolean spoiler;
public long size;
public Type type;
public PostImage(String originalName, String thumbnailUrl, String imageUrl, String filename, String extension, int imageWidth, int imageHeight, boolean spoiler, long size) {
this.originalName = originalName;
this.thumbnailUrl = thumbnailUrl;
this.imageUrl = imageUrl;
this.filename = filename;
this.extension = extension;
this.imageWidth = imageWidth;
this.imageHeight = imageHeight;
this.spoiler = spoiler;
this.size = size;
public final String originalName;
public final String thumbnailUrl;
public final String imageUrl;
public final String filename;
public final String extension;
public final int imageWidth;
public final int imageHeight;
public final boolean spoiler;
public final long size;
public final Type type;
private PostImage(Builder builder) {
this.originalName = builder.originalName;
this.thumbnailUrl = builder.thumbnailUrl;
this.imageUrl = builder.imageUrl;
this.filename = builder.filename;
this.extension = builder.extension;
this.imageWidth = builder.imageWidth;
this.imageHeight = builder.imageHeight;
this.spoiler = builder.spoiler;
this.size = builder.size;
switch (extension) {
case "gif":
@ -57,4 +59,72 @@ public class PostImage {
break;
}
}
public static final class Builder {
private String originalName;
private String thumbnailUrl;
private String imageUrl;
private String filename;
private String extension;
private int imageWidth;
private int imageHeight;
private boolean spoiler;
private long size;
public Builder() {
}
public Builder originalName(String originalName) {
this.originalName = originalName;
return this;
}
public Builder thumbnailUrl(String thumbnailUrl) {
this.thumbnailUrl = thumbnailUrl;
return this;
}
public Builder imageUrl(String imageUrl) {
this.imageUrl = imageUrl;
return this;
}
public Builder filename(String filename) {
this.filename = filename;
return this;
}
public Builder extension(String extension) {
this.extension = extension;
return this;
}
public Builder imageWidth(int imageWidth) {
this.imageWidth = imageWidth;
return this;
}
public Builder imageHeight(int imageHeight) {
this.imageHeight = imageHeight;
return this;
}
public Builder spoiler(boolean spoiler) {
this.spoiler = spoiler;
return this;
}
public Builder size(long size) {
this.size = size;
return this;
}
public PostImage build() {
if (ChanSettings.revealImageSpoilers.get()) {
spoiler = false;
}
return new PostImage(this);
}
}
}

@ -37,7 +37,6 @@ public class PostLinkable extends ClickableSpan {
}
public final Theme theme;
public final Post post;
public final String key;
public final Object value;
public final Type type;
@ -45,9 +44,8 @@ public class PostLinkable extends ClickableSpan {
private boolean spoilerVisible = ChanSettings.revealTextSpoilers.get();
private int markedNo = -1;
public PostLinkable(Theme theme, Post post, String key, Object value, Type type) {
public PostLinkable(Theme theme, String key, Object value, Type type) {
this.theme = theme;
this.post = post;
this.key = key;
this.value = value;
this.type = type;

@ -0,0 +1,30 @@
/*
* 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.core.model;
import org.floens.chan.core.site.Site;
public interface SiteReference {
/**
* Get the Site object that this model references.
*
* @return a {@link Site}
*/
Site getSite();
}

@ -25,29 +25,34 @@ import org.floens.chan.core.model.Loadable;
import java.util.HashMap;
import java.util.Map;
public class LoaderPool {
// private static final String TAG = "LoaderPool";
import javax.inject.Inject;
import javax.inject.Singleton;
/**
* ChanLoaderFactory is a factory for ChanLoaders. ChanLoaders for threads are cached.
* <p>Each reference to a loader is a {@link org.floens.chan.chan.ChanLoader.ChanLoaderCallback}, these
* references can be obtained with {@link #obtain(Loadable, ChanLoader.ChanLoaderCallback)}} and released
* with {@link #release(ChanLoader, ChanLoader.ChanLoaderCallback)}.
*/
@Singleton
public class ChanLoaderFactory {
// private static final String TAG = "ChanLoaderFactory";
public static final int THREAD_LOADERS_CACHE_SIZE = 25;
private static LoaderPool instance = new LoaderPool();
public static LoaderPool getInstance() {
return instance;
}
private Map<Loadable, ChanLoader> threadLoaders = new HashMap<>();
private LruCache<Loadable, ChanLoader> threadLoadersCache = new LruCache<>(THREAD_LOADERS_CACHE_SIZE);
private LoaderPool() {
@Inject
public ChanLoaderFactory() {
}
// public Loadable obtainLoadable() {
//
// }
public ChanLoader obtain(Loadable loadable, ChanLoader.ChanLoaderCallback listener) {
ChanLoader chanLoader;
if (loadable.isThreadMode()) {
if (!loadable.isFromDatabase()) {
throw new IllegalArgumentException();
}
chanLoader = threadLoaders.get(loadable);
if (chanLoader == null) {
chanLoader = threadLoadersCache.get(loadable);

@ -27,7 +27,6 @@ import org.floens.chan.core.database.DatabaseManager;
import org.floens.chan.core.exception.ChanLoaderException;
import org.floens.chan.core.http.DeleteHttpCall;
import org.floens.chan.core.http.ReplyManager;
import org.floens.chan.core.manager.BoardManager;
import org.floens.chan.core.manager.WatchManager;
import org.floens.chan.core.model.Board;
import org.floens.chan.core.model.ChanThread;
@ -38,7 +37,7 @@ import org.floens.chan.core.model.Post;
import org.floens.chan.core.model.PostImage;
import org.floens.chan.core.model.PostLinkable;
import org.floens.chan.core.model.SavedReply;
import org.floens.chan.core.pool.LoaderPool;
import org.floens.chan.core.pool.ChanLoaderFactory;
import org.floens.chan.core.settings.ChanSettings;
import org.floens.chan.core.site.Site;
import org.floens.chan.ui.adapter.PostAdapter;
@ -52,6 +51,7 @@ import org.floens.chan.ui.view.ThumbnailView;
import org.floens.chan.utils.AndroidUtils;
import java.util.ArrayList;
import java.util.Collections;
import java.util.List;
import javax.inject.Inject;
@ -85,6 +85,9 @@ public class ThreadPresenter implements ChanLoader.ChanLoaderCallback, PostAdapt
@Inject
ReplyManager replyManager;
@Inject
ChanLoaderFactory chanLoaderFactory;
private ThreadPresenterCallback threadPresenterCallback;
private Loadable loadable;
@ -115,14 +118,14 @@ public class ThreadPresenter implements ChanLoader.ChanLoaderCallback, PostAdapt
}
this.loadable = loadable;
chanLoader = LoaderPool.getInstance().obtain(loadable, this);
chanLoader = chanLoaderFactory.obtain(loadable, this);
}
}
public void unbindLoadable() {
if (chanLoader != null) {
chanLoader.clearTimer();
LoaderPool.getInstance().release(chanLoader, this);
chanLoaderFactory.release(chanLoader, this);
chanLoader = null;
loadable = null;
historyAdded = false;
@ -228,7 +231,7 @@ public class ThreadPresenter implements ChanLoader.ChanLoaderCallback, PostAdapt
int index = 0;
for (int i = 0; i < posts.size(); i++) {
Post item = posts.get(i);
if (item.hasImage) {
if (item.image != null) {
images.add(item.image);
}
if (i == displayPosition) {
@ -323,7 +326,7 @@ public class ThreadPresenter implements ChanLoader.ChanLoaderCallback, PostAdapt
List<Post> posts = threadPresenterCallback.getDisplayingPosts();
for (int i = 0; i < posts.size(); i++) {
Post post = posts.get(i);
if (post.hasImage && post.image == postImage) {
if (post.image == postImage) {
position = i;
break;
}
@ -398,7 +401,7 @@ public class ThreadPresenter implements ChanLoader.ChanLoaderCallback, PostAdapt
List<Post> posts = threadPresenterCallback.getDisplayingPosts();
for (int i = 0; i < posts.size(); i++) {
Post item = posts.get(i);
if (item.hasImage) {
if (item.image != null) {
images.add(item.image);
if (item.no == post.no) {
index = images.size() - 1;
@ -465,7 +468,7 @@ public class ThreadPresenter implements ChanLoader.ChanLoaderCallback, PostAdapt
break;
case POST_OPTION_LINKS:
if (post.linkables.size() > 0) {
threadPresenterCallback.showPostLinkables(post.linkables);
threadPresenterCallback.showPostLinkables(post);
}
break;
case POST_OPTION_COPY_TEXT:
@ -514,13 +517,12 @@ public class ThreadPresenter implements ChanLoader.ChanLoaderCallback, PostAdapt
}
@Override
public void onPostLinkableClicked(PostLinkable linkable) {
public void onPostLinkableClicked(Post post, PostLinkable linkable) {
if (linkable.type == PostLinkable.Type.QUOTE) {
Post post = findPostById((Integer) linkable.value);
List<Post> list = new ArrayList<>(1);
list.add(post);
threadPresenterCallback.showPostsPopup(linkable.post, list);
Post linked = findPostById((int) linkable.value);
if (linked != null) {
threadPresenterCallback.showPostsPopup(post, Collections.singletonList(linked));
}
} else if (linkable.type == PostLinkable.Type.LINK) {
threadPresenterCallback.openLink((String) linkable.value);
} else if (linkable.type == PostLinkable.Type.THREAD) {
@ -635,18 +637,19 @@ public class ThreadPresenter implements ChanLoader.ChanLoaderCallback, PostAdapt
private void showPostInfo(Post post) {
String text = "";
if (post.hasImage) {
text += "Filename: " + post.filename + "." + post.ext + " \nDimensions: " + post.imageWidth + "x"
+ post.imageHeight + "\nSize: " + AndroidUtils.getReadableFileSize(post.fileSize, false);
if (post.image != null) {
text += "Filename: " + post.image.filename + "." + post.image.extension + " \nDimensions: " + post.image.imageWidth + "x"
+ post.image.imageHeight + "\nSize: " + AndroidUtils.getReadableFileSize(post.image.size, false);
if (post.spoiler) {
if (post.image.spoiler) {
text += "\nSpoilered";
}
text += "\n\n";
text += "\n";
}
text += "Date: " + post.date;
// TODO(multi-site) get this from the timestamp
// text += "Date: " + post.date;
if (!TextUtils.isEmpty(post.id)) {
text += "\nId: " + post.id;
@ -685,7 +688,8 @@ public class ThreadPresenter implements ChanLoader.ChanLoaderCallback, PostAdapt
historyAdded = true;
History history = new History();
history.loadable = loadable;
history.thumbnailUrl = chanLoader.getThread().op.thumbnailUrl;
PostImage image = chanLoader.getThread().op.image;
history.thumbnailUrl = image == null ? "" : image.thumbnailUrl;
databaseManager.getDatabaseHistoryManager().add(history);
}
}
@ -701,7 +705,7 @@ public class ThreadPresenter implements ChanLoader.ChanLoaderCallback, PostAdapt
void showPostInfo(String info);
void showPostLinkables(List<PostLinkable> linkables);
void showPostLinkables(Post post);
void clipboardPost(Post post);

@ -1,5 +1,7 @@
package org.floens.chan.core.site;
import org.floens.chan.chan.ChanLoaderRequest;
import org.floens.chan.chan.ChanLoaderRequestParams;
import org.floens.chan.core.model.Board;
public interface Site {
@ -9,11 +11,6 @@ public interface Site {
*/
POSTING,
/**
* This site supports a 4chan like boards.json endpoint.
*/
DYNAMIC_BOARDS,
/**
* This site supports deleting posts.
*/
@ -80,6 +77,8 @@ public interface Site {
Board board(String name);
ChanLoaderRequest loaderRequest(ChanLoaderRequestParams request);
interface BoardsListener {
void onBoardsReceived(Boards boards);
}

@ -4,6 +4,8 @@ import org.floens.chan.core.model.Board;
import org.floens.chan.core.model.Loadable;
import org.floens.chan.core.model.Post;
import java.util.Map;
/**
* Endpoints for {@link Site}.
*/
@ -12,11 +14,11 @@ public interface SiteEndpoints {
String thread(Board board, Loadable loadable);
String imageUrl(Post post);
String imageUrl(Post.Builder post, Map<String, String> arg);
String thumbnailUrl(Post post);
String thumbnailUrl(Post.Builder post, boolean spoiler, Map<String, String> arg);
String flag(Post post);
String flag(Post.Builder post, String countryCode, Map<String, String> arg);
String boards();

@ -15,22 +15,27 @@
* 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.core.net;
package org.floens.chan.core.site.loaders;
import android.util.JsonReader;
import com.android.volley.Response.ErrorListener;
import com.android.volley.Response.Listener;
import org.floens.chan.chan.ChanLoaderRequestParams;
import org.floens.chan.chan.ChanLoaderResponse;
import org.floens.chan.chan.ChanParser;
import org.floens.chan.core.database.DatabaseManager;
import org.floens.chan.core.database.DatabaseSavedReplyManager;
import org.floens.chan.core.manager.FilterEngine;
import org.floens.chan.core.model.Filter;
import org.floens.chan.core.model.Loadable;
import org.floens.chan.core.model.Post;
import org.floens.chan.core.model.PostImage;
import org.floens.chan.core.net.JsonReaderRequest;
import org.floens.chan.core.site.SiteEndpoints;
import org.floens.chan.utils.Time;
import org.jsoup.parser.Parser;
import java.util.ArrayList;
import java.util.Collections;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
@ -49,8 +54,8 @@ import static org.floens.chan.Chan.getGraph;
* This class is highly multithreaded, take good care to not access models that are to be only
* changed on the main thread.
*/
public class ChanReaderRequest extends JsonReaderRequest<ChanReaderRequest.ChanReaderResponse> {
private static final String TAG = "ChanReaderRequest";
public class Chan4ReaderRequest extends JsonReaderRequest<ChanLoaderResponse> {
private static final String TAG = "Chan4ReaderRequest";
private static final boolean LOG_TIMING = false;
private static final int THREAD_COUNT;
@ -64,26 +69,54 @@ public class ChanReaderRequest extends JsonReaderRequest<ChanReaderRequest.ChanR
@Inject
DatabaseManager databaseManager;
@Inject
FilterEngine filterEngine;
@Inject
ChanParser chanParser;
private Loadable loadable;
private List<Post> cached;
private Post op;
private FilterEngine filterEngine;
private Post.Builder op;
private DatabaseSavedReplyManager databaseSavedReplyManager;
private List<Filter> filters;
private long startLoad;
private ChanReaderRequest(String url, Listener<ChanReaderResponse> listener, ErrorListener errorListener) {
super(url, listener, errorListener);
public Chan4ReaderRequest(ChanLoaderRequestParams request) {
super(getChanUrl(request.loadable), request.listener, request.errorListener);
getGraph().inject(this);
filterEngine = FilterEngine.getInstance();
// Copy the loadable and cached list. The cached array may changed/cleared by other threads.
loadable = request.loadable.copy();
cached = new ArrayList<>(request.cached);
filters = new ArrayList<>();
List<Filter> enabledFilters = filterEngine.getEnabledFilters();
for (int i = 0; i < enabledFilters.size(); i++) {
Filter filter = enabledFilters.get(i);
if (filter.allBoards) {
// copy the filter because it will get used on other threads
filters.add(filter.copy());
} else {
String[] boardCodes = filter.boardCodes();
for (String code : boardCodes) {
if (code.equals(loadable.boardCode)) {
// copy the filter because it will get used on other threads
filters.add(filter.copy());
break;
}
}
}
}
startLoad = Time.startTiming();
databaseSavedReplyManager = databaseManager.getDatabaseSavedReplyManager();
}
public static ChanReaderRequest newInstance(
Loadable loadable, List<Post> cached, Listener<ChanReaderResponse> listener, ErrorListener errorListener) {
private static String getChanUrl(Loadable loadable) {
String url;
if (loadable.site == null) {
@ -101,36 +134,7 @@ public class ChanReaderRequest extends JsonReaderRequest<ChanReaderRequest.ChanR
} else {
throw new IllegalArgumentException("Unknown mode");
}
ChanReaderRequest request = new ChanReaderRequest(url, listener, errorListener);
// Copy the loadable and cached list. The cached array may changed/cleared by other threads.
request.loadable = loadable.copy();
request.cached = new ArrayList<>(cached);
request.filters = new ArrayList<>();
List<Filter> enabledFilters = request.filterEngine.getEnabledFilters();
for (int i = 0; i < enabledFilters.size(); i++) {
Filter filter = enabledFilters.get(i);
if (filter.allBoards) {
// copy the filter because it will get used on other threads
request.filters.add(filter.copy());
} else {
String[] boardCodes = filter.boardCodes();
for (String code : boardCodes) {
if (code.equals(loadable.boardCode)) {
// copy the filter because it will get used on other threads
request.filters.add(filter.copy());
break;
}
}
}
}
request.startLoad = Time.startTiming();
return request;
return url;
}
@Override
@ -139,7 +143,7 @@ public class ChanReaderRequest extends JsonReaderRequest<ChanReaderRequest.ChanR
}
@Override
public ChanReaderResponse readJson(JsonReader reader) throws Exception {
public ChanLoaderResponse readJson(JsonReader reader) throws Exception {
if (LOG_TIMING) {
Time.endTiming("Network", startLoad);
}
@ -180,8 +184,8 @@ public class ChanReaderRequest extends JsonReaderRequest<ChanReaderRequest.ChanR
List<Callable<Post>> tasks = new ArrayList<>(queue.toParse.size());
for (int i = 0; i < queue.toParse.size(); i++) {
Post post = queue.toParse.get(i);
tasks.add(new PostParseCallable(filterEngine, filters, databaseSavedReplyManager, post));
Post.Builder post = queue.toParse.get(i);
tasks.add(new PostParseCallable(filterEngine, filters, databaseSavedReplyManager, post, chanParser));
}
if (!tasks.isEmpty()) {
@ -202,10 +206,8 @@ public class ChanReaderRequest extends JsonReaderRequest<ChanReaderRequest.ChanR
return total;
}
private ChanReaderResponse processPosts(List<Post> serverPosts) throws Exception {
ChanReaderResponse response = new ChanReaderResponse();
response.posts = new ArrayList<>(serverPosts.size());
response.op = op;
private ChanLoaderResponse processPosts(List<Post> serverPosts) throws Exception {
ChanLoaderResponse response = new ChanLoaderResponse(op, new ArrayList<Post>(serverPosts.size()));
List<Post> cachedPosts = new ArrayList<>();
List<Post> newPosts = new ArrayList<>();
@ -352,9 +354,21 @@ public class ChanReaderRequest extends JsonReaderRequest<ChanReaderRequest.ChanR
}
private void readPostObject(JsonReader reader, ProcessingQueue queue, Map<Integer, Post> cachedByNo) throws Exception {
Post post = new Post();
post.board = loadable.board;
post.boardId = loadable.boardCode;
Post.Builder builder = new Post.Builder();
builder.board(loadable.board);
// File
long fileId = 0;
String fileExt = null;
int fileWidth = 0;
int fileHeight = 0;
long fileSize = 0;
boolean fileSpoiler = false;
String fileName = null;
// Country flag
String countryCode = null;
String countryName = null;
reader.beginObject();
while (reader.hasNext()) {
@ -362,79 +376,81 @@ public class ChanReaderRequest extends JsonReaderRequest<ChanReaderRequest.ChanR
switch (key) {
case "no":
post.no = reader.nextInt();
builder.id(reader.nextInt());
break;
case "now":
/*case "now":
post.date = reader.nextString();
break;*/
case "sub":
builder.subject(reader.nextString());
break;
case "name":
post.name = reader.nextString();
builder.name(reader.nextString());
break;
case "com":
post.rawComment = reader.nextString();
builder.comment(reader.nextString());
break;
case "tim":
post.tim = reader.nextLong();
fileId = reader.nextLong();
break;
case "time":
post.time = reader.nextLong();
builder.setUnixTimestampSeconds(reader.nextLong());
break;
case "ext":
post.ext = reader.nextString().replace(".", "");
break;
case "resto":
post.resto = reader.nextInt();
fileExt = reader.nextString().replace(".", "");
break;
case "w":
post.imageWidth = reader.nextInt();
fileWidth = reader.nextInt();
break;
case "h":
post.imageHeight = reader.nextInt();
fileHeight = reader.nextInt();
break;
case "fsize":
post.fileSize = reader.nextLong();
fileSize = reader.nextLong();
break;
case "sub":
post.subject = reader.nextString();
case "filename":
fileName = reader.nextString();
break;
case "replies":
post.replies = reader.nextInt();
case "trip":
builder.tripcode(reader.nextString());
break;
case "filename":
post.filename = reader.nextString();
case "country":
countryCode = reader.nextString();
break;
case "country_name":
countryName = reader.nextString();
break;
case "spoiler":
fileSpoiler = reader.nextInt() == 1;
break;
case "resto":
int opId = reader.nextInt();
builder.op(opId == 0);
builder.opId(opId);
break;
case "sticky":
post.sticky = reader.nextInt() == 1;
builder.sticky(reader.nextInt() == 1);
break;
case "closed":
post.closed = reader.nextInt() == 1;
builder.closed(reader.nextInt() == 1);
break;
case "archived":
post.archived = reader.nextInt() == 1;
builder.archived(reader.nextInt() == 1);
break;
case "trip":
post.tripcode = reader.nextString();
case "replies":
builder.replies(reader.nextInt());
break;
case "country":
post.country = reader.nextString();
case "images":
builder.images(reader.nextInt());
break;
case "country_name":
post.countryName = reader.nextString();
case "unique_ips":
builder.uniqueIps(reader.nextInt());
break;
case "id":
post.id = reader.nextString();
builder.posterId(reader.nextString());
break;
case "capcode":
post.capcode = reader.nextString();
break;
case "images":
post.images = reader.nextInt();
break;
case "spoiler":
post.spoiler = reader.nextInt() == 1;
break;
case "unique_ips":
post.uniqueIps = reader.nextInt();
builder.moderatorCapcode(reader.nextString());
break;
default:
// Unknown/ignored key
@ -444,34 +460,52 @@ public class ChanReaderRequest extends JsonReaderRequest<ChanReaderRequest.ChanR
}
reader.endObject();
if (post.resto == 0) {
if (builder.op) {
// Update OP fields later on the main thread
op = new Post();
op.closed = post.closed;
op.archived = post.archived;
op.sticky = post.sticky;
op.replies = post.replies;
op.images = post.images;
op.uniqueIps = post.uniqueIps;
op = new Post.Builder();
op.closed(builder.closed);
op.archived(builder.archived);
op.sticky(builder.sticky);
op.replies(builder.replies);
op.images(builder.images);
op.uniqueIps(builder.uniqueIps);
}
Post cached = cachedByNo.get(post.no);
Post cached = cachedByNo.get(builder.id);
if (cached != null) {
// Id is known, use the cached post object.
queue.cached.add(cached);
} else {
queue.toParse.add(post);
return;
}
SiteEndpoints endpoints = loadable.getSite().endpoints();
if (fileId != 0 && fileName != null && fileExt != null) {
Map<String, String> hack = new HashMap<>(2);
hack.put("tim", String.valueOf(fileId));
hack.put("ext", fileExt);
builder.image(new PostImage.Builder()
.originalName(String.valueOf(fileId))
.thumbnailUrl(endpoints.thumbnailUrl(builder, fileSpoiler, hack))
.imageUrl(endpoints.imageUrl(builder, hack))
.filename(Parser.unescapeEntities(fileName, false))
.extension(fileExt)
.imageWidth(fileWidth)
.imageHeight(fileHeight)
.spoiler(fileSpoiler)
.size(fileSize)
.build());
}
if (countryCode != null && countryName != null) {
String countryUrl = endpoints.flag(builder, countryCode, Collections.<String, String>emptyMap());
builder.country(countryCode, countryName, countryUrl);
}
}
public static class ChanReaderResponse {
// Op Post that is created new each time.
// Used to later copy members like image count to the real op on the main thread.
public Post op;
public List<Post> posts;
queue.toParse.add(builder);
}
private static class ProcessingQueue {
public List<Post> cached = new ArrayList<>();
public List<Post> toParse = new ArrayList<>();
public List<Post.Builder> toParse = new ArrayList<>();
}
}

@ -15,13 +15,13 @@
* 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.core.net;
package org.floens.chan.core.site.loaders;
import org.floens.chan.chan.ChanParser;
import org.floens.chan.core.database.DatabaseSavedReplyManager;
import org.floens.chan.core.manager.FilterEngine;
import org.floens.chan.core.model.Filter;
import org.floens.chan.core.model.Post;
import org.floens.chan.utils.Logger;
import java.util.List;
import java.util.concurrent.Callable;
@ -33,14 +33,19 @@ class PostParseCallable implements Callable<Post> {
private FilterEngine filterEngine;
private List<Filter> filters;
private DatabaseSavedReplyManager savedReplyManager;
private Post post;
private Post.Builder post;
private ChanParser parser;
public PostParseCallable(FilterEngine filterEngine, List<Filter> filters,
DatabaseSavedReplyManager savedReplyManager, Post post) {
public PostParseCallable(FilterEngine filterEngine,
List<Filter> filters,
DatabaseSavedReplyManager savedReplyManager,
Post.Builder post,
ChanParser parser) {
this.filterEngine = filterEngine;
this.filters = filters;
this.savedReplyManager = savedReplyManager;
this.post = post;
this.parser = parser;
}
@Override
@ -48,17 +53,16 @@ class PostParseCallable implements Callable<Post> {
// Process the filters before finish, because parsing the html is dependent on filter matches
processPostFilter(post);
if (!post.finish()) {
Logger.e(TAG, "Incorrect data about post received for post " + post.no);
return null;
}
post.isSavedReply = savedReplyManager.isSaved(post.boardId, post.no);
post.isSavedReply(savedReplyManager.isSaved(post.board.code, post.id));
return post;
// if (!post.parse(parser)) {
// Logger.e(TAG, "Incorrect data about post received for post " + post.no);
// return null;
// }
return parser.parse(post);
}
private void processPostFilter(Post post) {
private void processPostFilter(Post.Builder post) {
int filterSize = filters.size();
for (int i = 0; i < filterSize; i++) {
Filter filter = filters.get(i);
@ -66,13 +70,13 @@ class PostParseCallable implements Callable<Post> {
FilterEngine.FilterAction action = FilterEngine.FilterAction.forId(filter.action);
switch (action) {
case COLOR:
post.filterHighlightedColor = filter.color;
post.filter(filter.color, false, false);
break;
case HIDE:
post.filterStub = true;
post.filter(0, true, false);
break;
case REMOVE:
post.filterRemove = true;
post.filter(0, false, true);
break;
}
}

@ -3,18 +3,22 @@ package org.floens.chan.core.site.sites.chan4;
import com.android.volley.Response;
import com.android.volley.VolleyError;
import org.floens.chan.chan.ChanLoaderRequest;
import org.floens.chan.chan.ChanLoaderRequestParams;
import org.floens.chan.core.model.Board;
import org.floens.chan.core.model.Loadable;
import org.floens.chan.core.model.Post;
import org.floens.chan.core.site.Boards;
import org.floens.chan.core.site.Site;
import org.floens.chan.core.site.SiteEndpoints;
import org.floens.chan.core.site.loaders.Chan4ReaderRequest;
import org.floens.chan.utils.Logger;
import java.util.ArrayList;
import java.util.Collections;
import java.util.List;
import java.util.Locale;
import java.util.Map;
import java.util.Random;
import static org.floens.chan.Chan.getGraph;
@ -36,13 +40,13 @@ public class Chan4 implements Site {
}
@Override
public String imageUrl(Post post) {
return "https://i.4cdn.org/" + post.boardId + "/" + Long.toString(post.tim) + "." + post.ext;
public String imageUrl(Post.Builder post, Map<String, String> arg) {
return "https://i.4cdn.org/" + post.board.code + "/" + arg.get("tim") + "." + arg.get("ext");
}
@Override
public String thumbnailUrl(Post post) {
if (post.spoiler) {
public String thumbnailUrl(Post.Builder post, boolean spoiler, Map<String, String> arg) {
if (spoiler) {
if (post.board.customSpoilers >= 0) {
int i = random.nextInt(post.board.customSpoilers) + 1;
return "https://s.4cdn.org/image/spoiler-" + post.board.code + i + ".png";
@ -50,13 +54,13 @@ public class Chan4 implements Site {
return "https://s.4cdn.org/image/spoiler.png";
}
} else {
return "https://t.4cdn.org/" + post.board.code + "/" + post.tim + "s.jpg";
return "https://t.4cdn.org/" + post.board.code + "/" + arg.get("tim") + "s.jpg";
}
}
@Override
public String flag(Post post) {
return "https://s.4cdn.org/image/country/" + post.country.toLowerCase(Locale.ENGLISH) + ".gif";
public String flag(Post.Builder post, String countryCode, Map<String, String> arg) {
return "https://s.4cdn.org/image/country/" + countryCode.toLowerCase(Locale.ENGLISH) + ".gif";
}
@Override
@ -90,9 +94,6 @@ public class Chan4 implements Site {
case POSTING:
// yes, we support posting.
return true;
case DYNAMIC_BOARDS:
// yes, boards.json
return true;
case LOGIN:
// 4chan pass.
return true;
@ -106,6 +107,7 @@ public class Chan4 implements Site {
@Override
public BoardsType boardsType() {
// yes, boards.json
return BoardsType.DYNAMIC;
}
@ -163,4 +165,9 @@ public class Chan4 implements Site {
}
}));
}
@Override
public ChanLoaderRequest loaderRequest(ChanLoaderRequestParams request) {
return new ChanLoaderRequest(new Chan4ReaderRequest(request));
}
}

@ -207,8 +207,8 @@ public class TestActivity extends Activity implements View.OnClickListener {
@Override
public void onChanLoaderData(ChanThread result) {
for (Post post : result.posts) {
if (post.hasImage) {
final String imageUrl = post.imageUrl;
if (post.image != null) {
final String imageUrl = post.image.imageUrl;
fileCache.downloadFile(imageUrl, new FileCache.DownloadedCallback() {
@Override
public void onProgress(long downloaded, long total, boolean done) {

@ -19,7 +19,6 @@ package org.floens.chan.ui.adapter;
import android.text.TextUtils;
import org.floens.chan.Chan;
import org.floens.chan.core.database.DatabaseManager;
import org.floens.chan.core.model.Post;
@ -117,7 +116,7 @@ public class PostsFilter {
add = true;
} else if (item.name.toLowerCase(Locale.ENGLISH).contains(lowerQuery)) {
add = true;
} else if (item.filename != null && item.filename.toLowerCase(Locale.ENGLISH).contains(lowerQuery)) {
} else if (item.image != null && item.image.filename != null && item.image.filename.toLowerCase(Locale.ENGLISH).contains(lowerQuery)) {
add = true;
}
if (!add) {

@ -184,7 +184,7 @@ public class CardPostCell extends CardView implements PostCellInterface, View.On
private void bindPost(Theme theme, Post post) {
bound = true;
if (post.hasImage && !ChanSettings.textOnly.get()) {
if (post.image != null && !ChanSettings.textOnly.get()) {
thumbnailView.setVisibility(View.VISIBLE);
thumbnailView.setPostImage(post.image, thumbnailView.getWidth(), thumbnailView.getHeight());
} else {

@ -324,7 +324,7 @@ public class PostCell extends LinearLayout implements PostCellInterface {
filterMatchColor.setVisibility(View.GONE);
}
if (post.hasImage && !ChanSettings.textOnly.get()) {
if (post.image != null && !ChanSettings.textOnly.get()) {
thumbnailView.setVisibility(View.VISIBLE);
thumbnailView.setPostImage(post.image, thumbnailView.getLayoutParams().width, thumbnailView.getLayoutParams().height);
} else {
@ -369,7 +369,7 @@ public class PostCell extends LinearLayout implements PostCellInterface {
titleParts.add(date);
if (post.hasImage) {
if (post.image != null) {
PostImage image = post.image;
boolean postFileName = ChanSettings.postFilename.get();
@ -421,7 +421,7 @@ public class PostCell extends LinearLayout implements PostCellInterface {
}
comment.setText(commentText);
comment.setVisibility(isEmpty(commentText) && !post.hasImage ? GONE : VISIBLE);
comment.setVisibility(isEmpty(commentText) && post.image == null ? GONE : VISIBLE);
if (commentClickable != threadMode) {
commentClickable = threadMode;
@ -525,7 +525,7 @@ public class PostCell extends LinearLayout implements PostCellInterface {
ignoreNextOnClick = true;
clickableSpan.onClick(widget);
if (clickableSpan instanceof PostLinkable) {
callback.onPostLinkableClicked((PostLinkable) clickableSpan);
callback.onPostLinkableClicked(post, (PostLinkable) clickableSpan);
}
buffer.removeSpan(BACKGROUND_SPAN);
} else if (action == MotionEvent.ACTION_DOWN && clickableSpan instanceof PostLinkable) {

@ -47,7 +47,7 @@ public interface PostCellInterface {
void onPostOptionClicked(Post post, Object id);
void onPostLinkableClicked(PostLinkable linkable);
void onPostLinkableClicked(Post post, PostLinkable linkable);
void onPostNoClicked(Post post);
}

@ -28,7 +28,6 @@ import android.view.animation.DecelerateInterpolator;
import android.widget.BaseAdapter;
import android.widget.TextView;
import org.floens.chan.Chan;
import org.floens.chan.R;
import org.floens.chan.chan.ChanUrls;
import org.floens.chan.core.database.DatabaseManager;

@ -30,7 +30,6 @@ import android.view.ViewGroup;
import android.widget.ImageView;
import android.widget.TextView;
import org.floens.chan.Chan;
import org.floens.chan.R;
import org.floens.chan.controller.Controller;
import org.floens.chan.core.database.DatabaseManager;
@ -63,7 +62,8 @@ public class FiltersController extends Controller implements ToolbarMenuItem.Too
@Inject
DatabaseManager databaseManager;
private FilterEngine filterEngine;
@Inject
FilterEngine filterEngine;
private RecyclerView recyclerView;
private FloatingActionButton add;
@ -108,8 +108,6 @@ public class FiltersController extends Controller implements ToolbarMenuItem.Too
super.onCreate();
getGraph().inject(this);
filterEngine = FilterEngine.getInstance();
navigationItem.setTitle(R.string.filters_screen);
navigationItem.menu = new ToolbarMenu(context);
navigationItem.menu.addItem(new ToolbarMenuItem(context, this, SEARCH_ID, R.drawable.ic_search_white_24dp));

@ -111,7 +111,7 @@ public class PostRepliesController extends Controller {
if (view instanceof PostCellInterface) {
PostCellInterface postView = (PostCellInterface) view;
Post post = postView.getPost();
if (post.hasImage && post.imageUrl.equals(postImage.imageUrl)) {
if (post.image != null && post.image.imageUrl.equals(postImage.imageUrl)) {
thumbnail = postView.getThumbnailView();
break;
}

@ -96,7 +96,7 @@ public class ThemeSettingsController extends Controller implements View.OnClickL
}
@Override
public void onPostLinkableClicked(PostLinkable linkable) {
public void onPostLinkableClicked(Post post, PostLinkable linkable) {
}
@Override
@ -236,22 +236,20 @@ public class ThemeSettingsController extends Controller implements View.OnClickL
Context themeContext = new ContextThemeWrapper(context, theme.resValue);
Post post = new Post();
post.no = 123456789;
post.time = (Time.get() - (30 * 60 * 1000)) / 1000;
// No synchronization needed, this is a dummy
post.repliesFrom.add(1);
post.repliesFrom.add(2);
post.repliesFrom.add(3);
post.subject = "Lorem ipsum";
post.rawComment = "<a href=\"#p123456789\" class=\"quotelink\">&gt;&gt;123456789</a><br>" +
"Lorem ipsum dolor sit amet, consectetur adipiscing elit.<br>" +
"<br>" +
"<a href=\"#p123456789\" class=\"quotelink\">&gt;&gt;123456789</a><br>" +
"http://example.com/" +
"<br>" +
"Phasellus consequat semper sodales. Donec dolor lectus, aliquet nec mollis vel, rutrum vel enim.";
getGraph().getChanParser().parse(theme, post);
Post.Builder builder = new Post.Builder()
.board(new Board(Sites.defaultSite(), "a", "a", false, false))
.id(123456789)
.opId(1)
.setUnixTimestampSeconds((Time.get() - (30 * 60 * 1000)) / 1000)
.subject("Lorem ipsum")
.comment("<a href=\"#p123456789\" class=\"quotelink\">&gt;&gt;123456789</a><br>" +
"Lorem ipsum dolor sit amet, consectetur adipiscing elit.<br>" +
"<br>" +
"<a href=\"#p123456789\" class=\"quotelink\">&gt;&gt;123456789</a><br>" +
"http://example.com/" +
"<br>" +
"Phasellus consequat semper sodales. Donec dolor lectus, aliquet nec mollis vel, rutrum vel enim.");
Post post = getGraph().getChanParser().parse(theme, builder);
LinearLayout linearLayout = new LinearLayout(themeContext);
linearLayout.setOrientation(LinearLayout.VERTICAL);

@ -38,7 +38,6 @@ import android.widget.ImageView;
import android.widget.LinearLayout;
import android.widget.TextView;
import org.floens.chan.Chan;
import org.floens.chan.R;
import org.floens.chan.core.manager.BoardManager;
import org.floens.chan.core.manager.FilterEngine;
@ -79,6 +78,9 @@ public class FilterLayout extends LinearLayout implements View.OnClickListener {
@Inject
BoardManager boardManager;
@Inject
FilterEngine filterEngine;
private FilterLayoutCallback callback;
private Filter filter;
@ -161,7 +163,7 @@ public class FilterLayout extends LinearLayout implements View.OnClickListener {
public void setFilter(Filter filter) {
this.filter = filter;
appliedBoards.clear();
appliedBoards.addAll(FilterEngine.getInstance().getBoardsForFilter(filter));
appliedBoards.addAll(filterEngine.getBoardsForFilter(filter));
pattern.setText(filter.pattern);
@ -180,7 +182,7 @@ public class FilterLayout extends LinearLayout implements View.OnClickListener {
public Filter getFilter() {
filter.enabled = enabled.isChecked();
FilterEngine.getInstance().saveBoardsToFilter(appliedBoards, filter);
filterEngine.saveBoardsToFilter(appliedBoards, filter);
return filter;
}
@ -341,7 +343,7 @@ public class FilterLayout extends LinearLayout implements View.OnClickListener {
}
private void updateFilterValidity() {
boolean valid = !TextUtils.isEmpty(filter.pattern) && FilterEngine.getInstance().compile(filter.pattern) != null;
boolean valid = !TextUtils.isEmpty(filter.pattern) && filterEngine.compile(filter.pattern) != null;
if (valid != patternContainerErrorShowing) {
patternContainerErrorShowing = valid;
@ -386,7 +388,7 @@ public class FilterLayout extends LinearLayout implements View.OnClickListener {
private void updatePatternPreview() {
String text = patternPreview.getText().toString();
boolean matches = text.length() > 0 && FilterEngine.getInstance().matches(filter, true, text, true);
boolean matches = text.length() > 0 && filterEngine.matches(filter, true, text, true);
patternPreviewStatus.setText(matches ? R.string.filter_matches : R.string.filter_no_matches);
}

@ -240,7 +240,8 @@ public class ThreadLayout extends CoordinatorLayout implements ThreadPresenter.T
.show();
}
public void showPostLinkables(final List<PostLinkable> linkables) {
public void showPostLinkables(final Post post) {
final List<PostLinkable> linkables = post.linkables;
String[] keys = new String[linkables.size()];
for (int i = 0; i < linkables.size(); i++) {
keys[i] = linkables.get(i).key;
@ -250,7 +251,7 @@ public class ThreadLayout extends CoordinatorLayout implements ThreadPresenter.T
.setItems(keys, new DialogInterface.OnClickListener() {
@Override
public void onClick(DialogInterface dialog, int which) {
presenter.onPostLinkableClicked(linkables.get(which));
presenter.onPostLinkableClicked(post, linkables.get(which));
}
})
.show();

@ -346,7 +346,7 @@ public class ThreadListLayout extends FrameLayout implements ReplyLayout.ReplyLa
if (view instanceof PostCellInterface) {
PostCellInterface postView = (PostCellInterface) view;
Post post = postView.getPost();
if (post.hasImage && post.imageUrl.equals(postImage.imageUrl)) {
if (post.image != null && post.image.imageUrl.equals(postImage.imageUrl)) {
thumbnail = postView.getThumbnailView();
break;
}
@ -503,7 +503,8 @@ public class ThreadListLayout extends FrameLayout implements ReplyLayout.ReplyLa
View child = parent.getChildAt(i);
if (child instanceof PostCellInterface) {
PostCellInterface postView = (PostCellInterface) child;
if (postView.getPost().hasImage) {
Post post = postView.getPost();
if (post.isOP && post.image != null) {
RecyclerView.LayoutParams params = (RecyclerView.LayoutParams) child.getLayoutParams();
int top = child.getTop() + params.topMargin;
int left = child.getLeft() + params.leftMargin;

@ -196,7 +196,7 @@ public class WatchNotifier extends Service {
prefix = postForExpandedLine.title.subSequence(0, SUBJECT_LENGTH);
}
String comment = postForExpandedLine.hasImage ? IMAGE_TEXT : "";
String comment = postForExpandedLine.image != null ? IMAGE_TEXT : "";
if (postForExpandedLine.comment.length() > 0) {
comment += postForExpandedLine.comment;
}

Loading…
Cancel
Save