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.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.core.net.ChanReaderRequest;
import org.floens.chan.ui.helper.PostHelper; import org.floens.chan.ui.helper.PostHelper;
import org.floens.chan.utils.AndroidUtils; import org.floens.chan.utils.AndroidUtils;
import org.floens.chan.utils.Logger; import org.floens.chan.utils.Logger;
@ -44,11 +43,18 @@ import javax.inject.Inject;
import static org.floens.chan.Chan.getGraph; 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 String TAG = "ChanLoader";
private static final ScheduledExecutorService executor = Executors.newSingleThreadScheduledExecutor(); 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 @Inject
RequestQueue volleyRequestQueue; RequestQueue volleyRequestQueue;
@ -57,13 +63,16 @@ public class ChanLoader implements Response.ErrorListener, Response.Listener<Cha
private final Loadable loadable; private final Loadable loadable;
private ChanThread thread; private ChanThread thread;
private ChanReaderRequest request; private ChanLoaderRequest request;
private int currentTimeout = 0; private int currentTimeout = 0;
private int lastPostCount; private int lastPostCount;
private long lastLoadTime; private long lastLoadTime;
private ScheduledFuture<?> pendingFuture; 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) { public ChanLoader(Loadable loadable) {
this.loadable = loadable; this.loadable = loadable;
@ -94,7 +103,7 @@ public class ChanLoader implements Response.ErrorListener, Response.Listener<Cha
if (listeners.isEmpty()) { if (listeners.isEmpty()) {
clearTimer(); clearTimer();
if (request != null) { if (request != null) {
request.cancel(); request.getVolleyRequest().cancel();
request = null; request = null;
} }
return true; 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. * Request data for the first time.
*/ */
@ -110,7 +123,7 @@ public class ChanLoader implements Response.ErrorListener, Response.Listener<Cha
clearTimer(); clearTimer();
if (request != null) { if (request != null) {
request.cancel(); request.getVolleyRequest().cancel();
// request = null; // 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() { public boolean requestMoreData() {
clearPendingRunnable(); clearPendingRunnable();
@ -179,6 +193,31 @@ public class ChanLoader implements Response.ErrorListener, Response.Listener<Cha
return loadable; 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 * 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) { if (request != null) {
return 0L; return 0L;
} else { } else {
long waitTime = watchTimeouts[Math.max(0, currentTimeout)] * 1000L; long waitTime = WATCH_TIMEOUTS[Math.max(0, currentTimeout)] * 1000L;
return lastLoadTime + waitTime - Time.get(); return lastLoadTime + waitTime - Time.get();
} }
} }
public ChanThread getThread() { private ChanLoaderRequest getData() {
return thread; 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 @Override
public void onResponse(ChanReaderRequest.ChanReaderResponse response) { public void onResponse(ChanLoaderResponse response) {
request = null; request = null;
if (response.posts.size() == 0) { if (response.posts.isEmpty()) {
onErrorResponse(new VolleyError("Post size is 0")); onErrorResponse(new VolleyError("Post size is 0"));
return; return;
} }
@ -228,7 +275,7 @@ public class ChanLoader implements Response.ErrorListener, Response.Listener<Cha
lastPostCount = postCount; lastPostCount = postCount;
currentTimeout = 0; currentTimeout = 0;
} else { } else {
currentTimeout = Math.min(currentTimeout + 1, watchTimeouts.length - 1); currentTimeout = Math.min(currentTimeout + 1, WATCH_TIMEOUTS.length - 1);
} }
for (ChanLoaderCallback l : listeners) { 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. * Final processing af a response that needs to happen on the main thread.
* *
* @param response Response to process * @param response Response to process
*/ */
private void processResponse(ChanReaderRequest.ChanReaderResponse response) { private void processResponse(ChanLoaderResponse response) {
if (loadable.isThreadMode() && thread.posts.size() > 0) { if (loadable.isThreadMode() && thread.posts.size() > 0) {
// Replace some op parameters to the real op (index 0). // Replace some op parameters to the real op (index 0).
// This is done on the main thread to avoid race conditions. // This is done on the main thread to avoid race conditions.
Post realOp = thread.posts.get(0); Post realOp = thread.posts.get(0);
thread.op = realOp; thread.op = realOp;
Post fakeOp = response.op; Post.Builder fakeOp = response.op;
if (fakeOp != null) { if (fakeOp != null) {
thread.closed = realOp.closed = fakeOp.closed; thread.closed = realOp.closed = fakeOp.closed;
thread.archived = realOp.archived = fakeOp.archived; thread.archived = realOp.archived = fakeOp.archived;
@ -276,29 +308,19 @@ public class ChanLoader implements Response.ErrorListener, Response.Listener<Cha
} }
} }
public void setTimer() { @Override
clearPendingRunnable(); public void onErrorResponse(VolleyError error) {
request = null;
int watchTimeout = watchTimeouts[currentTimeout]; Logger.i(TAG, "Loading error", error);
Logger.d(TAG, "Scheduled reload in " + watchTimeout + "s");
pendingFuture = executor.schedule(new Runnable() { clearTimer();
@Override
public void run() {
AndroidUtils.runOnUiThread(new Runnable() {
@Override
public void run() {
pendingFuture = null;
requestMoreData();
}
});
}
}, watchTimeout, TimeUnit.SECONDS);
}
public void clearTimer() { ChanLoaderException loaderException = new ChanLoaderException(error);
currentTimeout = 0;
clearPendingRunnable(); for (ChanLoaderCallback l : listeners) {
l.onChanLoaderError(loaderException);
}
} }
private void clearPendingRunnable() { 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 { public interface ChanLoaderCallback {
void onChanLoaderData(ChanThread result); 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 @Singleton
public class ChanParser { public class ChanParser {
private static final String TAG = "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 @Inject
DatabaseManager databaseManager; DatabaseManager databaseManager;
@ -70,34 +72,36 @@ public class ChanParser {
public ChanParser() { public ChanParser() {
} }
public void parse(Post post) { public Post parse(Post.Builder post) {
parse(null, post); return parse(null, post);
} }
public void parse(Theme theme, Post post) { public Post parse(Theme theme, Post.Builder builder) {
if (theme == null) { if (theme == null) {
theme = ThemeHelper.getInstance().getTheme(); theme = ThemeHelper.getInstance().getTheme();
} }
try { try {
if (!TextUtils.isEmpty(post.name)) { if (!TextUtils.isEmpty(builder.name)) {
post.name = Parser.unescapeEntities(post.name, false); builder.name = Parser.unescapeEntities(builder.name, false);
} }
if (!TextUtils.isEmpty(post.subject)) { if (!TextUtils.isEmpty(builder.subject)) {
post.subject = Parser.unescapeEntities(post.subject, false); builder.subject = Parser.unescapeEntities(builder.subject, false);
} }
} catch (Exception e) { } catch (Exception e) {
e.printStackTrace(); e.printStackTrace();
} }
parseSpans(theme, post); parseSpans(theme, builder);
if (post.rawComment != null) { if (builder.comment != null) {
post.comment = parseComment(theme, post, post.rawComment); builder.comment = parseComment(theme, builder, builder.comment);
} else { } 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> * 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. * The results will be placed on the Post.*Span members.
* *
* @param theme Theme to use for parsing * @param theme Theme to use for parsing
* @param post Post to get data from * @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 anonymize = ChanSettings.anonymize.get();
boolean anonymizeIds = ChanSettings.anonymizeIds.get(); boolean anonymizeIds = ChanSettings.anonymizeIds.get();
final String defaultName = "Anonymous";
if (anonymize) { if (anonymize) {
post.name = "Anonymous"; builder.name(defaultName);
post.tripcode = ""; builder.tripcode("");
} }
if (anonymizeIds) { 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); int detailsSizePx = sp(Integer.parseInt(ChanSettings.fontSize.get()) - 4);
if (!TextUtils.isEmpty(post.subject)) { if (!TextUtils.isEmpty(builder.subject)) {
post.subjectSpan = new SpannableString(post.subject); subjectSpan = new SpannableString(builder.subject);
// Do not set another color when the post is in stub mode, it sets text_color_secondary // Do not set another color when the post is in stub mode, it sets text_color_secondary
if (!post.filterStub) { if (!builder.filterStub) {
post.subjectSpan.setSpan(new ForegroundColorSpanHashed(theme.subjectColor), 0, post.subjectSpan.length(), 0); subjectSpan.setSpan(new ForegroundColorSpanHashed(theme.subjectColor), 0, subjectSpan.length(), 0);
} }
} }
if (!TextUtils.isEmpty(post.name) && (!post.name.equals("Anonymous") || ChanSettings.showAnonymousName.get())) { if (!TextUtils.isEmpty(builder.name) && (!builder.name.equals(defaultName) || ChanSettings.showAnonymousName.get())) {
post.nameSpan = new SpannableString(post.name); nameSpan = new SpannableString(builder.name);
post.nameSpan.setSpan(new ForegroundColorSpanHashed(theme.nameColor), 0, post.nameSpan.length(), 0); nameSpan.setSpan(new ForegroundColorSpanHashed(theme.nameColor), 0, nameSpan.length(), 0);
} }
if (!TextUtils.isEmpty(post.tripcode)) { if (!TextUtils.isEmpty(builder.tripcode)) {
post.tripcodeSpan = new SpannableString(post.tripcode); tripcodeSpan = new SpannableString(builder.tripcode);
post.tripcodeSpan.setSpan(new ForegroundColorSpanHashed(theme.nameColor), 0, post.tripcodeSpan.length(), 0); tripcodeSpan.setSpan(new ForegroundColorSpanHashed(theme.nameColor), 0, tripcodeSpan.length(), 0);
post.tripcodeSpan.setSpan(new AbsoluteSizeSpanHashed(detailsSizePx), 0, post.tripcodeSpan.length(), 0); tripcodeSpan.setSpan(new AbsoluteSizeSpanHashed(detailsSizePx), 0, tripcodeSpan.length(), 0);
} }
if (!TextUtils.isEmpty(post.id)) { if (!TextUtils.isEmpty(builder.posterId)) {
post.idSpan = new SpannableString(" ID: " + post.id + " "); idSpan = new SpannableString(" ID: " + builder.posterId + " ");
// Stolen from the 4chan extension // Stolen from the 4chan extension
int hash = post.id.hashCode(); int hash = builder.posterId.hashCode();
int r = (hash >> 24) & 0xff; int r = (hash >> 24) & 0xff;
int g = (hash >> 16) & 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; boolean lightColor = (r * 0.299f) + (g * 0.587f) + (b * 0.114f) > 125f;
int idBgColor = lightColor ? theme.idBackgroundLight : theme.idBackgroundDark; int idBgColor = lightColor ? theme.idBackgroundLight : theme.idBackgroundDark;
post.idSpan.setSpan(new ForegroundColorSpanHashed(idColor), 0, post.idSpan.length(), 0); idSpan.setSpan(new ForegroundColorSpanHashed(idColor), 0, idSpan.length(), 0);
post.idSpan.setSpan(new BackgroundColorSpan(idBgColor), 0, post.idSpan.length(), 0); idSpan.setSpan(new BackgroundColorSpan(idBgColor), 0, idSpan.length(), 0);
post.idSpan.setSpan(new AbsoluteSizeSpanHashed(detailsSizePx), 0, post.idSpan.length(), 0); idSpan.setSpan(new AbsoluteSizeSpanHashed(detailsSizePx), 0, idSpan.length(), 0);
} }
if (!TextUtils.isEmpty(post.capcode)) { if (!TextUtils.isEmpty(builder.moderatorCapcode)) {
post.capcodeSpan = new SpannableString("Capcode: " + post.capcode); capcodeSpan = new SpannableString("Capcode: " + builder.moderatorCapcode);
post.capcodeSpan.setSpan(new ForegroundColorSpanHashed(theme.capcodeColor), 0, post.capcodeSpan.length(), 0); capcodeSpan.setSpan(new ForegroundColorSpanHashed(theme.capcodeColor), 0, capcodeSpan.length(), 0);
post.capcodeSpan.setSpan(new AbsoluteSizeSpanHashed(detailsSizePx), 0, post.capcodeSpan.length(), 0); capcodeSpan.setSpan(new AbsoluteSizeSpanHashed(detailsSizePx), 0, capcodeSpan.length(), 0);
} }
post.nameTripcodeIdCapcodeSpan = new SpannableString(""); CharSequence nameTripcodeIdCapcodeSpan = new SpannableString("");
if (post.nameSpan != null) { if (nameSpan != null) {
post.nameTripcodeIdCapcodeSpan = TextUtils.concat(post.nameTripcodeIdCapcodeSpan, post.nameSpan, " "); nameTripcodeIdCapcodeSpan = TextUtils.concat(nameTripcodeIdCapcodeSpan, nameSpan, " ");
} }
if (post.tripcodeSpan != null) { if (tripcodeSpan != null) {
post.nameTripcodeIdCapcodeSpan = TextUtils.concat(post.nameTripcodeIdCapcodeSpan, post.tripcodeSpan, " "); nameTripcodeIdCapcodeSpan = TextUtils.concat(nameTripcodeIdCapcodeSpan, tripcodeSpan, " ");
} }
if (post.idSpan != null) { if (idSpan != null) {
post.nameTripcodeIdCapcodeSpan = TextUtils.concat(post.nameTripcodeIdCapcodeSpan, post.idSpan, " "); nameTripcodeIdCapcodeSpan = TextUtils.concat(nameTripcodeIdCapcodeSpan, idSpan, " ");
} }
if (post.capcodeSpan != null) { if (capcodeSpan != null) {
post.nameTripcodeIdCapcodeSpan = TextUtils.concat(post.nameTripcodeIdCapcodeSpan, post.capcodeSpan, " "); 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(""); CharSequence total = new SpannableString("");
try { try {
String comment = commentRaw.replace("<wbr>", ""); String comment = commentRaw.toString().replace("<wbr>", "");
Document document = Jsoup.parseBodyFragment(comment); Document document = Jsoup.parseBodyFragment(comment);
@ -211,7 +224,7 @@ public class ChanParser {
return total; 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) { if (node instanceof TextNode) {
String text = ((TextNode) node).text(); String text = ((TextNode) node).text();
SpannableString spannable = new SpannableString(text); SpannableString spannable = new SpannableString(text);
@ -243,8 +256,8 @@ public class ChanParser {
if (!TextUtils.isEmpty(style)) { if (!TextUtils.isEmpty(style)) {
style = style.replace(" ", ""); style = style.replace(" ", "");
// 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]*)");
Matcher matcher = colorPattern.matcher(style); Matcher matcher = COLOR_PATTERN.matcher(style);
int hexColor = 0xff0000; int hexColor = 0xff0000;
if (matcher.find()) { if (matcher.find()) {
@ -331,9 +344,9 @@ public class ChanParser {
SpannableString link = new SpannableString(spoiler.text()); 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); link.setSpan(pl, 0, link.length(), 0);
post.linkables.add(pl); post.addLinkable(pl);
return link; 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"); String href = anchor.attr("href");
Set<String> classes = anchor.classNames(); Set<String> classes = anchor.classNames();
@ -411,16 +424,16 @@ public class ChanParser {
t = PostLinkable.Type.QUOTE; t = PostLinkable.Type.QUOTE;
key = anchor.text(); key = anchor.text();
value = id; value = id;
post.repliesTo.add(id); post.addReplyTo(id);
// Append OP when its a reply to OP // Append OP when its a reply to OP
if (id == post.resto) { if (id == post.opId) {
key += " (OP)"; key += OP_REPLY_SUFFIX;
} }
// Append You when it's a reply to an saved reply // Append You when it's a reply to an saved reply
if (databaseManager.getDatabaseSavedReplyManager().isSaved(post.boardId, id)) { if (databaseManager.getDatabaseSavedReplyManager().isSaved(post.board.code, id)) {
key += " (You)"; key += SAVED_REPLY_SUFFIX;
} }
} }
} }
@ -433,9 +446,9 @@ public class ChanParser {
if (t != null && key != null && value != null) { if (t != null && key != null && value != null) {
SpannableString link = new SpannableString(key); 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); link.setSpan(pl, 0, link.length(), 0);
post.linkables.add(pl); post.addLinkable(pl);
return link; return link;
} else { } 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 startPos = 0;
int endPos; int endPos;
while (true) { while (true) {
@ -466,9 +479,9 @@ public class ChanParser {
String linkString = text.substring(startPos, endPos); 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); spannable.setSpan(pl, startPos, endPos, 0);
post.linkables.add(pl); post.addLinkable(pl);
startPos = endPos; startPos = endPos;
} }

@ -17,6 +17,8 @@
*/ */
package org.floens.chan.core.database; package org.floens.chan.core.database;
import android.support.annotation.AnyThread;
import com.j256.ormlite.stmt.QueryBuilder; import com.j256.ormlite.stmt.QueryBuilder;
import com.j256.ormlite.table.TableUtils; import com.j256.ormlite.table.TableUtils;
@ -57,6 +59,7 @@ public class DatabaseSavedReplyManager {
* @param no post number * @param no post number
* @return {@code true} if the post is in the saved reply database, {@code false} otherwise. * @return {@code true} if the post is in the saved reply database, {@code false} otherwise.
*/ */
@AnyThread
public boolean isSaved(String board, int no) { public boolean isSaved(String board, int no) {
// TODO(multi-site) // TODO(multi-site)
synchronized (savedRepliesByNo) { 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.http.ReplyManager;
import org.floens.chan.core.manager.BoardManager; import org.floens.chan.core.manager.BoardManager;
import org.floens.chan.core.manager.FilterEngine; 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.ImageViewerPresenter;
import org.floens.chan.core.presenter.ReplyPresenter; import org.floens.chan.core.presenter.ReplyPresenter;
import org.floens.chan.core.presenter.ThreadPresenter; import org.floens.chan.core.presenter.ThreadPresenter;
@ -66,7 +67,7 @@ public interface ChanGraph {
void inject(ReplyPresenter replyPresenter); void inject(ReplyPresenter replyPresenter);
void inject(ChanReaderRequest chanReaderRequest); void inject(Chan4ReaderRequest chanReaderRequest);
void inject(ThreadLayout threadLayout); void inject(ThreadLayout threadLayout);
@ -113,4 +114,6 @@ public interface ChanGraph {
void inject(ImageSaveTask imageSaveTask); void inject(ImageSaveTask imageSaveTask);
void inject(ViewThreadController viewThreadController); void inject(ViewThreadController viewThreadController);
void inject(WatchManager.PinWatcher pinWatcher);
} }

@ -17,9 +17,9 @@
*/ */
package org.floens.chan.core.manager; package org.floens.chan.core.manager;
import android.support.annotation.AnyThread;
import android.text.TextUtils; import android.text.TextUtils;
import org.floens.chan.Chan;
import org.floens.chan.core.database.DatabaseFilterManager; import org.floens.chan.core.database.DatabaseFilterManager;
import org.floens.chan.core.database.DatabaseManager; import org.floens.chan.core.database.DatabaseManager;
import org.floens.chan.core.model.Board; import org.floens.chan.core.model.Board;
@ -37,18 +37,12 @@ import java.util.regex.Pattern;
import java.util.regex.PatternSyntaxException; import java.util.regex.PatternSyntaxException;
import javax.inject.Inject; import javax.inject.Inject;
import javax.inject.Singleton;
import static org.floens.chan.Chan.getGraph; @Singleton
public class FilterEngine { public class FilterEngine {
private static final String TAG = "FilterEngine"; private static final String TAG = "FilterEngine";
private static final FilterEngine instance = new FilterEngine();
public static FilterEngine getInstance() {
return instance;
}
public enum FilterAction { public enum FilterAction {
HIDE(0), HIDE(0),
COLOR(1), COLOR(1),
@ -73,21 +67,18 @@ public class FilterEngine {
} }
} }
private final Map<String, Pattern> patternCache = new HashMap<>(); private final DatabaseManager databaseManager;
private final BoardManager boardManager;
@Inject
DatabaseManager databaseManager;
@Inject
BoardManager boardManager;
private final DatabaseFilterManager databaseFilterManager; private final DatabaseFilterManager databaseFilterManager;
private List<Filter> filters; private final Map<String, Pattern> patternCache = new HashMap<>();
private final List<Filter> enabledFilters = new ArrayList<>(); private final List<Filter> enabledFilters = new ArrayList<>();
private FilterEngine() { @Inject
getGraph().inject(this); public FilterEngine(DatabaseManager databaseManager, BoardManager boardManager) {
this.databaseManager = databaseManager;
this.boardManager = boardManager;
databaseFilterManager = databaseManager.getDatabaseFilterManager(); databaseFilterManager = databaseManager.getDatabaseFilterManager();
update(); update();
} }
@ -111,7 +102,7 @@ public class FilterEngine {
} }
// threadsafe // 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)) { if ((filter.type & FilterType.TRIPCODE.flag) != 0 && matches(filter, FilterType.TRIPCODE.isRegex, post.tripcode, false)) {
return true; return true;
} }
@ -120,11 +111,11 @@ public class FilterEngine {
return true; 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; 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; return true;
} }
@ -132,14 +123,15 @@ public class FilterEngine {
return true; 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 true;
} }
return false; return false;
} }
// threadsafe @AnyThread
public boolean matches(Filter filter, boolean matchRegex, String text, boolean forceCompile) { public boolean matches(Filter filter, boolean matchRegex, String text, boolean forceCompile) {
if (TextUtils.isEmpty(text)) { if (TextUtils.isEmpty(text)) {
return false; return false;
@ -184,7 +176,7 @@ public class FilterEngine {
private static final Pattern filterFilthyPattern = Pattern.compile("(\\.|\\^|\\$|\\*|\\+|\\?|\\(|\\)|\\[|\\]|\\{|\\}|\\\\|\\||\\-)"); 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 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) { public Pattern compile(String rawPattern) {
if (TextUtils.isEmpty(rawPattern)) { if (TextUtils.isEmpty(rawPattern)) {
return null; return null;
@ -262,7 +254,7 @@ public class FilterEngine {
} }
private void update() { private void update() {
filters = databaseManager.runTaskSync(databaseFilterManager.getFilters()); List<Filter> filters = databaseManager.runTaskSync(databaseFilterManager.getFilters());
List<Filter> enabled = new ArrayList<>(); List<Filter> enabled = new ArrayList<>();
for (Filter filter : filters) { for (Filter filter : filters) {
if (filter.enabled) { 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.Loadable;
import org.floens.chan.core.model.Pin; import org.floens.chan.core.model.Pin;
import org.floens.chan.core.model.Post; 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.core.settings.ChanSettings;
import org.floens.chan.ui.helper.PostHelper; import org.floens.chan.ui.helper.PostHelper;
import org.floens.chan.ui.service.WatchNotifier; import org.floens.chan.ui.service.WatchNotifier;
@ -56,6 +57,7 @@ import javax.inject.Singleton;
import de.greenrobot.event.EventBus; import de.greenrobot.event.EventBus;
import static org.floens.chan.Chan.getGraph;
import static org.floens.chan.utils.AndroidUtils.getAppContext; import static org.floens.chan.utils.AndroidUtils.getAppContext;
/** /**
@ -159,7 +161,8 @@ public class WatchManager {
Pin pin = new Pin(); Pin pin = new Pin();
pin.loadable = loadable; pin.loadable = loadable;
pin.loadable.title = PostHelper.getTitle(opPost, 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); return createPin(pin);
} }
@ -648,6 +651,9 @@ public class WatchManager {
public class PinWatcher implements ChanLoader.ChanLoaderCallback { public class PinWatcher implements ChanLoader.ChanLoaderCallback {
private static final String TAG = "PinWatcher"; private static final String TAG = "PinWatcher";
@Inject
ChanLoaderFactory chanLoaderFactory;
private final Pin pin; private final Pin pin;
private ChanLoader chanLoader; private ChanLoader chanLoader;
@ -658,9 +664,10 @@ public class WatchManager {
public PinWatcher(Pin pin) { public PinWatcher(Pin pin) {
this.pin = pin; this.pin = pin;
getGraph().inject(this);
Logger.d(TAG, "PinWatcher: created for " + pin); Logger.d(TAG, "PinWatcher: created for " + pin);
chanLoader = LoaderPool.getInstance().obtain(pin.loadable, this); chanLoader = chanLoaderFactory.obtain(pin.loadable, this);
} }
public List<Post> getUnviewedPosts() { public List<Post> getUnviewedPosts() {
@ -696,7 +703,7 @@ public class WatchManager {
private void destroy() { private void destroy() {
if (chanLoader != null) { if (chanLoader != null) {
Logger.d(TAG, "PinWatcher: destroyed for " + pin); Logger.d(TAG, "PinWatcher: destroyed for " + pin);
LoaderPool.getInstance().release(chanLoader, this); chanLoaderFactory.release(chanLoader, this);
chanLoader = null; chanLoader = null;
} }
} }
@ -738,8 +745,8 @@ public class WatchManager {
public void onChanLoaderData(ChanThread thread) { public void onChanLoaderData(ChanThread thread) {
pin.isError = false; pin.isError = false;
if (pin.thumbnailUrl == null && thread.op != null && thread.op.hasImage) { if (pin.thumbnailUrl == null && thread.op != null && thread.op.image != null) {
pin.thumbnailUrl = thread.op.thumbnailUrl; pin.thumbnailUrl = thread.op.image.thumbnailUrl;
} }
// Populate posts list // Populate posts list

@ -25,7 +25,7 @@ import com.j256.ormlite.table.DatabaseTable;
import org.floens.chan.core.site.Site; import org.floens.chan.core.site.Site;
@DatabaseTable @DatabaseTable
public class Board { public class Board implements SiteReference {
public Board() { public Board() {
} }
@ -148,6 +148,11 @@ public class Board {
return true; return true;
} }
@Override
public Site getSite() {
return site;
}
public String getName() { public String getName() {
return "/" + code + "/ \u2013 " + name; 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 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 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> * - 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 @DatabaseTable
public class Loadable { public class Loadable implements SiteReference {
@DatabaseField(generatedId = true) @DatabaseField(generatedId = true)
public int id; public int id;
@ -116,6 +118,11 @@ public class Loadable {
return loadable; return loadable;
} }
@Override
public Site getSite() {
return site;
}
public void setTitle(String title) { public void setTitle(String title) {
if (!TextUtils.equals(this.title, title)) { if (!TextUtils.equals(this.title, title)) {
this.title = title; this.title = title;
@ -161,6 +168,10 @@ public class Loadable {
Loadable other = (Loadable) object; Loadable other = (Loadable) object;
if ((site.id() == other.site.id() && (site != other.site))) {
throw new IllegalStateException(); // TODO(multi-site) remove
}
if (site != other.site) { if (site != other.site) {
return false; return false;
} }
@ -220,6 +231,11 @@ public class Loadable {
return mode == Mode.CATALOG; return mode == Mode.CATALOG;
} }
// TODO(multi-site) remove
public boolean isFromDatabase() {
return id > 0;
}
public static Loadable readFromParcel(Parcel parcel) { public static Loadable readFromParcel(Parcel parcel) {
Loadable loadable = new Loadable(); Loadable loadable = new Loadable();
/*loadable.id = */ /*loadable.id = */

@ -17,170 +17,305 @@
*/ */
package org.floens.chan.core.model; 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.ArrayList;
import java.util.Collections; import java.util.Collections;
import java.util.HashSet;
import java.util.List; import java.util.List;
import java.util.Set; import java.util.Set;
import java.util.TreeSet;
import java.util.concurrent.atomic.AtomicBoolean; import java.util.concurrent.atomic.AtomicBoolean;
import static org.floens.chan.Chan.getGraph;
/** /**
* Contains all data needed to represent a single post.<br> * 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> * All {@code final} fields are thread-safe.
* This class has members that are threadsafe and some that are not, see the source for more info.
*/ */
public class Post { public class Post {
// *** These next members don't get changed after finish() is called. Effectively final. *** public final String boardId;
public 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;
/** filterHighlightedColor = builder.filterHighlightedColor;
* This post replies to the these ids. Is an unmodifiable set after finish(). filterStub = builder.filterStub;
*/ filterRemove = builder.filterRemove;
public Set<Integer> repliesTo = new TreeSet<>();
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 String countryCode;
public boolean sticky = false; public String countryName;
public boolean closed = false; public String countryUrl;
public boolean archived = false;
public int replies = -1;
public int images = -1;
public int uniqueIps = 1;
public String title = "";
// *** Threadsafe members, may be read and modified on any thread. *** public String posterId = "";
public AtomicBoolean deleted = new AtomicBoolean(false); public String moderatorCapcode = "";
// *** Manual synchronization needed. *** public int filterHighlightedColor;
/** public boolean filterStub;
* These ids replied to this post.<br> public boolean filterRemove;
* <b>synchronize on this when accessing.</b>
*/
public final List<Integer> repliesFrom = new ArrayList<>();
/** public boolean isSavedReply;
* Finish up the data: parse the comment, check if the data is valid etc.
* public CharSequence subjectSpan;
* @return false if this data is invalid public CharSequence nameTripcodeIdCapcodeSpan;
*/
public boolean finish() { private List<PostLinkable> linkables = new ArrayList<>();
if (boardId == null || no < 0 || resto < 0 || date == null || time < 0) { private Set<Integer> repliesToIds = new HashSet<>();
return false;
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)) { public Builder archived(boolean archived) {
return false; this.archived = archived;
return this;
} }
if (filename != null && ext != null && imageWidth > 0 && imageHeight > 0 && tim >= 0) { public Builder closed(boolean closed) {
hasImage = true; this.closed = closed;
// TODO: only use #image return this;
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);
} }
if (!TextUtils.isEmpty(country)) { public Builder subject(String subject) {
countryUrl = board.site.endpoints().flag(this); this.subject = subject;
return this;
} }
if (ChanSettings.revealImageSpoilers.get()) { public Builder name(String name) {
spoiler = false; this.name = name;
return this;
} }
ChanParser chanParser = getGraph().getChanParser(); public Builder comment(String comment) {
chanParser.parse(this); 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; package org.floens.chan.core.model;
import org.floens.chan.core.settings.ChanSettings;
public class PostImage { public class PostImage {
public enum Type { public enum Type {
STATIC, GIF, MOVIE STATIC, GIF, MOVIE
} }
public String originalName; public final String originalName;
public String thumbnailUrl; public final String thumbnailUrl;
public String imageUrl; public final String imageUrl;
public String filename; public final String filename;
public String extension; public final String extension;
public int imageWidth; public final int imageWidth;
public int imageHeight; public final int imageHeight;
public boolean spoiler; public final boolean spoiler;
public long size; public final long size;
public Type type; public final Type type;
public PostImage(String originalName, String thumbnailUrl, String imageUrl, String filename, String extension, int imageWidth, int imageHeight, boolean spoiler, long size) { private PostImage(Builder builder) {
this.originalName = originalName; this.originalName = builder.originalName;
this.thumbnailUrl = thumbnailUrl; this.thumbnailUrl = builder.thumbnailUrl;
this.imageUrl = imageUrl; this.imageUrl = builder.imageUrl;
this.filename = filename; this.filename = builder.filename;
this.extension = extension; this.extension = builder.extension;
this.imageWidth = imageWidth; this.imageWidth = builder.imageWidth;
this.imageHeight = imageHeight; this.imageHeight = builder.imageHeight;
this.spoiler = spoiler; this.spoiler = builder.spoiler;
this.size = size; this.size = builder.size;
switch (extension) { switch (extension) {
case "gif": case "gif":
@ -57,4 +59,72 @@ public class PostImage {
break; 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 Theme theme;
public final Post post;
public final String key; public final String key;
public final Object value; public final Object value;
public final Type type; public final Type type;
@ -45,9 +44,8 @@ public class PostLinkable extends ClickableSpan {
private boolean spoilerVisible = ChanSettings.revealTextSpoilers.get(); private boolean spoilerVisible = ChanSettings.revealTextSpoilers.get();
private int markedNo = -1; 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.theme = theme;
this.post = post;
this.key = key; this.key = key;
this.value = value; this.value = value;
this.type = type; 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.HashMap;
import java.util.Map; import java.util.Map;
public class LoaderPool { import javax.inject.Inject;
// private static final String TAG = "LoaderPool"; 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; 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 Map<Loadable, ChanLoader> threadLoaders = new HashMap<>();
private LruCache<Loadable, ChanLoader> threadLoadersCache = new LruCache<>(THREAD_LOADERS_CACHE_SIZE); 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) { public ChanLoader obtain(Loadable loadable, ChanLoader.ChanLoaderCallback listener) {
ChanLoader chanLoader; ChanLoader chanLoader;
if (loadable.isThreadMode()) { if (loadable.isThreadMode()) {
if (!loadable.isFromDatabase()) {
throw new IllegalArgumentException();
}
chanLoader = threadLoaders.get(loadable); chanLoader = threadLoaders.get(loadable);
if (chanLoader == null) { if (chanLoader == null) {
chanLoader = threadLoadersCache.get(loadable); 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.exception.ChanLoaderException;
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.core.manager.BoardManager;
import org.floens.chan.core.manager.WatchManager; import org.floens.chan.core.manager.WatchManager;
import org.floens.chan.core.model.Board; import org.floens.chan.core.model.Board;
import org.floens.chan.core.model.ChanThread; 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.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.pool.LoaderPool; import org.floens.chan.core.pool.ChanLoaderFactory;
import org.floens.chan.core.settings.ChanSettings; import org.floens.chan.core.settings.ChanSettings;
import org.floens.chan.core.site.Site; import org.floens.chan.core.site.Site;
import org.floens.chan.ui.adapter.PostAdapter; 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 org.floens.chan.utils.AndroidUtils;
import java.util.ArrayList; import java.util.ArrayList;
import java.util.Collections;
import java.util.List; import java.util.List;
import javax.inject.Inject; import javax.inject.Inject;
@ -85,6 +85,9 @@ public class ThreadPresenter implements ChanLoader.ChanLoaderCallback, PostAdapt
@Inject @Inject
ReplyManager replyManager; ReplyManager replyManager;
@Inject
ChanLoaderFactory chanLoaderFactory;
private ThreadPresenterCallback threadPresenterCallback; private ThreadPresenterCallback threadPresenterCallback;
private Loadable loadable; private Loadable loadable;
@ -115,14 +118,14 @@ public class ThreadPresenter implements ChanLoader.ChanLoaderCallback, PostAdapt
} }
this.loadable = loadable; this.loadable = loadable;
chanLoader = LoaderPool.getInstance().obtain(loadable, this); chanLoader = chanLoaderFactory.obtain(loadable, this);
} }
} }
public void unbindLoadable() { public void unbindLoadable() {
if (chanLoader != null) { if (chanLoader != null) {
chanLoader.clearTimer(); chanLoader.clearTimer();
LoaderPool.getInstance().release(chanLoader, this); chanLoaderFactory.release(chanLoader, this);
chanLoader = null; chanLoader = null;
loadable = null; loadable = null;
historyAdded = false; historyAdded = false;
@ -228,7 +231,7 @@ public class ThreadPresenter implements ChanLoader.ChanLoaderCallback, PostAdapt
int index = 0; int index = 0;
for (int i = 0; i < posts.size(); i++) { for (int i = 0; i < posts.size(); i++) {
Post item = posts.get(i); Post item = posts.get(i);
if (item.hasImage) { if (item.image != null) {
images.add(item.image); images.add(item.image);
} }
if (i == displayPosition) { if (i == displayPosition) {
@ -323,7 +326,7 @@ public class ThreadPresenter implements ChanLoader.ChanLoaderCallback, PostAdapt
List<Post> posts = threadPresenterCallback.getDisplayingPosts(); List<Post> posts = threadPresenterCallback.getDisplayingPosts();
for (int i = 0; i < posts.size(); i++) { for (int i = 0; i < posts.size(); i++) {
Post post = posts.get(i); Post post = posts.get(i);
if (post.hasImage && post.image == postImage) { if (post.image == postImage) {
position = i; position = i;
break; break;
} }
@ -398,7 +401,7 @@ public class ThreadPresenter implements ChanLoader.ChanLoaderCallback, PostAdapt
List<Post> posts = threadPresenterCallback.getDisplayingPosts(); List<Post> posts = threadPresenterCallback.getDisplayingPosts();
for (int i = 0; i < posts.size(); i++) { for (int i = 0; i < posts.size(); i++) {
Post item = posts.get(i); Post item = posts.get(i);
if (item.hasImage) { if (item.image != null) {
images.add(item.image); images.add(item.image);
if (item.no == post.no) { if (item.no == post.no) {
index = images.size() - 1; index = images.size() - 1;
@ -465,7 +468,7 @@ public class ThreadPresenter implements ChanLoader.ChanLoaderCallback, PostAdapt
break; break;
case POST_OPTION_LINKS: case POST_OPTION_LINKS:
if (post.linkables.size() > 0) { if (post.linkables.size() > 0) {
threadPresenterCallback.showPostLinkables(post.linkables); threadPresenterCallback.showPostLinkables(post);
} }
break; break;
case POST_OPTION_COPY_TEXT: case POST_OPTION_COPY_TEXT:
@ -514,13 +517,12 @@ public class ThreadPresenter implements ChanLoader.ChanLoaderCallback, PostAdapt
} }
@Override @Override
public void onPostLinkableClicked(PostLinkable linkable) { public void onPostLinkableClicked(Post post, PostLinkable linkable) {
if (linkable.type == PostLinkable.Type.QUOTE) { if (linkable.type == PostLinkable.Type.QUOTE) {
Post post = findPostById((Integer) linkable.value); Post linked = findPostById((int) linkable.value);
if (linked != null) {
List<Post> list = new ArrayList<>(1); threadPresenterCallback.showPostsPopup(post, Collections.singletonList(linked));
list.add(post); }
threadPresenterCallback.showPostsPopup(linkable.post, list);
} else if (linkable.type == PostLinkable.Type.LINK) { } else if (linkable.type == PostLinkable.Type.LINK) {
threadPresenterCallback.openLink((String) linkable.value); threadPresenterCallback.openLink((String) linkable.value);
} else if (linkable.type == PostLinkable.Type.THREAD) { } else if (linkable.type == PostLinkable.Type.THREAD) {
@ -635,18 +637,19 @@ public class ThreadPresenter implements ChanLoader.ChanLoaderCallback, PostAdapt
private void showPostInfo(Post post) { private void showPostInfo(Post post) {
String text = ""; String text = "";
if (post.hasImage) { if (post.image != null) {
text += "Filename: " + post.filename + "." + post.ext + " \nDimensions: " + post.imageWidth + "x" text += "Filename: " + post.image.filename + "." + post.image.extension + " \nDimensions: " + post.image.imageWidth + "x"
+ post.imageHeight + "\nSize: " + AndroidUtils.getReadableFileSize(post.fileSize, false); + post.image.imageHeight + "\nSize: " + AndroidUtils.getReadableFileSize(post.image.size, false);
if (post.spoiler) { if (post.image.spoiler) {
text += "\nSpoilered"; 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)) { if (!TextUtils.isEmpty(post.id)) {
text += "\nId: " + post.id; text += "\nId: " + post.id;
@ -685,7 +688,8 @@ public class ThreadPresenter implements ChanLoader.ChanLoaderCallback, PostAdapt
historyAdded = true; historyAdded = true;
History history = new History(); History history = new History();
history.loadable = loadable; 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); databaseManager.getDatabaseHistoryManager().add(history);
} }
} }
@ -701,7 +705,7 @@ public class ThreadPresenter implements ChanLoader.ChanLoaderCallback, PostAdapt
void showPostInfo(String info); void showPostInfo(String info);
void showPostLinkables(List<PostLinkable> linkables); void showPostLinkables(Post post);
void clipboardPost(Post post); void clipboardPost(Post post);

@ -1,5 +1,7 @@
package org.floens.chan.core.site; 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; import org.floens.chan.core.model.Board;
public interface Site { public interface Site {
@ -9,11 +11,6 @@ public interface Site {
*/ */
POSTING, POSTING,
/**
* This site supports a 4chan like boards.json endpoint.
*/
DYNAMIC_BOARDS,
/** /**
* This site supports deleting posts. * This site supports deleting posts.
*/ */
@ -80,6 +77,8 @@ public interface Site {
Board board(String name); Board board(String name);
ChanLoaderRequest loaderRequest(ChanLoaderRequestParams request);
interface BoardsListener { interface BoardsListener {
void onBoardsReceived(Boards boards); 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.Loadable;
import org.floens.chan.core.model.Post; import org.floens.chan.core.model.Post;
import java.util.Map;
/** /**
* Endpoints for {@link Site}. * Endpoints for {@link Site}.
*/ */
@ -12,11 +14,11 @@ public interface SiteEndpoints {
String thread(Board board, Loadable loadable); 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(); String boards();

@ -15,22 +15,27 @@
* You should have received a copy of the GNU General Public License * 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/>.
*/ */
package org.floens.chan.core.net; package org.floens.chan.core.site.loaders;
import android.util.JsonReader; import android.util.JsonReader;
import com.android.volley.Response.ErrorListener; import org.floens.chan.chan.ChanLoaderRequestParams;
import com.android.volley.Response.Listener; 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.DatabaseManager;
import org.floens.chan.core.database.DatabaseSavedReplyManager; import org.floens.chan.core.database.DatabaseSavedReplyManager;
import org.floens.chan.core.manager.FilterEngine; import org.floens.chan.core.manager.FilterEngine;
import org.floens.chan.core.model.Filter; import org.floens.chan.core.model.Filter;
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.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.floens.chan.utils.Time;
import org.jsoup.parser.Parser;
import java.util.ArrayList; import java.util.ArrayList;
import java.util.Collections;
import java.util.HashMap; import java.util.HashMap;
import java.util.List; import java.util.List;
import java.util.Map; 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 * This class is highly multithreaded, take good care to not access models that are to be only
* changed on the main thread. * changed on the main thread.
*/ */
public class ChanReaderRequest extends JsonReaderRequest<ChanReaderRequest.ChanReaderResponse> { public class Chan4ReaderRequest extends JsonReaderRequest<ChanLoaderResponse> {
private static final String TAG = "ChanReaderRequest"; private static final String TAG = "Chan4ReaderRequest";
private static final boolean LOG_TIMING = false; private static final boolean LOG_TIMING = false;
private static final int THREAD_COUNT; private static final int THREAD_COUNT;
@ -64,26 +69,54 @@ public class ChanReaderRequest extends JsonReaderRequest<ChanReaderRequest.ChanR
@Inject @Inject
DatabaseManager databaseManager; DatabaseManager databaseManager;
@Inject
FilterEngine filterEngine;
@Inject
ChanParser chanParser;
private Loadable loadable; private Loadable loadable;
private List<Post> cached; private List<Post> cached;
private Post op; private Post.Builder op;
private FilterEngine filterEngine;
private DatabaseSavedReplyManager databaseSavedReplyManager; private DatabaseSavedReplyManager databaseSavedReplyManager;
private List<Filter> filters; private List<Filter> filters;
private long startLoad; private long startLoad;
private ChanReaderRequest(String url, Listener<ChanReaderResponse> listener, ErrorListener errorListener) { public Chan4ReaderRequest(ChanLoaderRequestParams request) {
super(url, listener, errorListener); super(getChanUrl(request.loadable), request.listener, request.errorListener);
getGraph().inject(this); 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(); databaseSavedReplyManager = databaseManager.getDatabaseSavedReplyManager();
} }
public static ChanReaderRequest newInstance( private static String getChanUrl(Loadable loadable) {
Loadable loadable, List<Post> cached, Listener<ChanReaderResponse> listener, ErrorListener errorListener) {
String url; String url;
if (loadable.site == null) { if (loadable.site == null) {
@ -101,36 +134,7 @@ public class ChanReaderRequest extends JsonReaderRequest<ChanReaderRequest.ChanR
} else { } else {
throw new IllegalArgumentException("Unknown mode"); throw new IllegalArgumentException("Unknown mode");
} }
return url;
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;
} }
@Override @Override
@ -139,7 +143,7 @@ public class ChanReaderRequest extends JsonReaderRequest<ChanReaderRequest.ChanR
} }
@Override @Override
public ChanReaderResponse readJson(JsonReader reader) throws Exception { public ChanLoaderResponse readJson(JsonReader reader) throws Exception {
if (LOG_TIMING) { if (LOG_TIMING) {
Time.endTiming("Network", startLoad); Time.endTiming("Network", startLoad);
} }
@ -180,8 +184,8 @@ public class ChanReaderRequest extends JsonReaderRequest<ChanReaderRequest.ChanR
List<Callable<Post>> tasks = new ArrayList<>(queue.toParse.size()); List<Callable<Post>> tasks = new ArrayList<>(queue.toParse.size());
for (int i = 0; i < queue.toParse.size(); i++) { for (int i = 0; i < queue.toParse.size(); i++) {
Post post = queue.toParse.get(i); Post.Builder post = queue.toParse.get(i);
tasks.add(new PostParseCallable(filterEngine, filters, databaseSavedReplyManager, post)); tasks.add(new PostParseCallable(filterEngine, filters, databaseSavedReplyManager, post, chanParser));
} }
if (!tasks.isEmpty()) { if (!tasks.isEmpty()) {
@ -202,10 +206,8 @@ public class ChanReaderRequest extends JsonReaderRequest<ChanReaderRequest.ChanR
return total; return total;
} }
private ChanReaderResponse processPosts(List<Post> serverPosts) throws Exception { private ChanLoaderResponse processPosts(List<Post> serverPosts) throws Exception {
ChanReaderResponse response = new ChanReaderResponse(); ChanLoaderResponse response = new ChanLoaderResponse(op, new ArrayList<Post>(serverPosts.size()));
response.posts = new ArrayList<>(serverPosts.size());
response.op = op;
List<Post> cachedPosts = new ArrayList<>(); List<Post> cachedPosts = new ArrayList<>();
List<Post> newPosts = 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 { private void readPostObject(JsonReader reader, ProcessingQueue queue, Map<Integer, Post> cachedByNo) throws Exception {
Post post = new Post(); Post.Builder builder = new Post.Builder();
post.board = loadable.board; builder.board(loadable.board);
post.boardId = loadable.boardCode;
// 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(); reader.beginObject();
while (reader.hasNext()) { while (reader.hasNext()) {
@ -362,79 +376,81 @@ public class ChanReaderRequest extends JsonReaderRequest<ChanReaderRequest.ChanR
switch (key) { switch (key) {
case "no": case "no":
post.no = reader.nextInt(); builder.id(reader.nextInt());
break; break;
case "now": /*case "now":
post.date = reader.nextString(); post.date = reader.nextString();
break;*/
case "sub":
builder.subject(reader.nextString());
break; break;
case "name": case "name":
post.name = reader.nextString(); builder.name(reader.nextString());
break; break;
case "com": case "com":
post.rawComment = reader.nextString(); builder.comment(reader.nextString());
break; break;
case "tim": case "tim":
post.tim = reader.nextLong(); fileId = reader.nextLong();
break; break;
case "time": case "time":
post.time = reader.nextLong(); builder.setUnixTimestampSeconds(reader.nextLong());
break; break;
case "ext": case "ext":
post.ext = reader.nextString().replace(".", ""); fileExt = reader.nextString().replace(".", "");
break;
case "resto":
post.resto = reader.nextInt();
break; break;
case "w": case "w":
post.imageWidth = reader.nextInt(); fileWidth = reader.nextInt();
break; break;
case "h": case "h":
post.imageHeight = reader.nextInt(); fileHeight = reader.nextInt();
break; break;
case "fsize": case "fsize":
post.fileSize = reader.nextLong(); fileSize = reader.nextLong();
break; break;
case "sub": case "filename":
post.subject = reader.nextString(); fileName = reader.nextString();
break; break;
case "replies": case "trip":
post.replies = reader.nextInt(); builder.tripcode(reader.nextString());
break; break;
case "filename": case "country":
post.filename = reader.nextString(); 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; break;
case "sticky": case "sticky":
post.sticky = reader.nextInt() == 1; builder.sticky(reader.nextInt() == 1);
break; break;
case "closed": case "closed":
post.closed = reader.nextInt() == 1; builder.closed(reader.nextInt() == 1);
break; break;
case "archived": case "archived":
post.archived = reader.nextInt() == 1; builder.archived(reader.nextInt() == 1);
break; break;
case "trip": case "replies":
post.tripcode = reader.nextString(); builder.replies(reader.nextInt());
break; break;
case "country": case "images":
post.country = reader.nextString(); builder.images(reader.nextInt());
break; break;
case "country_name": case "unique_ips":
post.countryName = reader.nextString(); builder.uniqueIps(reader.nextInt());
break; break;
case "id": case "id":
post.id = reader.nextString(); builder.posterId(reader.nextString());
break; break;
case "capcode": case "capcode":
post.capcode = reader.nextString(); builder.moderatorCapcode(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();
break; break;
default: default:
// Unknown/ignored key // Unknown/ignored key
@ -444,34 +460,52 @@ public class ChanReaderRequest extends JsonReaderRequest<ChanReaderRequest.ChanR
} }
reader.endObject(); reader.endObject();
if (post.resto == 0) { if (builder.op) {
// Update OP fields later on the main thread // Update OP fields later on the main thread
op = new Post(); op = new Post.Builder();
op.closed = post.closed; op.closed(builder.closed);
op.archived = post.archived; op.archived(builder.archived);
op.sticky = post.sticky; op.sticky(builder.sticky);
op.replies = post.replies; op.replies(builder.replies);
op.images = post.images; op.images(builder.images);
op.uniqueIps = post.uniqueIps; op.uniqueIps(builder.uniqueIps);
} }
Post cached = cachedByNo.get(post.no); Post cached = cachedByNo.get(builder.id);
if (cached != null) { if (cached != null) {
// Id is known, use the cached post object.
queue.cached.add(cached); queue.cached.add(cached);
} else { return;
queue.toParse.add(post); }
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 { queue.toParse.add(builder);
// 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;
} }
private static class ProcessingQueue { private static class ProcessingQueue {
public List<Post> cached = new ArrayList<>(); 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 * 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/>.
*/ */
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.database.DatabaseSavedReplyManager;
import org.floens.chan.core.manager.FilterEngine; import org.floens.chan.core.manager.FilterEngine;
import org.floens.chan.core.model.Filter; import org.floens.chan.core.model.Filter;
import org.floens.chan.core.model.Post; import org.floens.chan.core.model.Post;
import org.floens.chan.utils.Logger;
import java.util.List; import java.util.List;
import java.util.concurrent.Callable; import java.util.concurrent.Callable;
@ -33,14 +33,19 @@ class PostParseCallable implements Callable<Post> {
private FilterEngine filterEngine; private FilterEngine filterEngine;
private List<Filter> filters; private List<Filter> filters;
private DatabaseSavedReplyManager savedReplyManager; private DatabaseSavedReplyManager savedReplyManager;
private Post post; private Post.Builder post;
private ChanParser parser;
public PostParseCallable(FilterEngine filterEngine, List<Filter> filters, public PostParseCallable(FilterEngine filterEngine,
DatabaseSavedReplyManager savedReplyManager, Post post) { List<Filter> filters,
DatabaseSavedReplyManager savedReplyManager,
Post.Builder post,
ChanParser parser) {
this.filterEngine = filterEngine; this.filterEngine = filterEngine;
this.filters = filters; this.filters = filters;
this.savedReplyManager = savedReplyManager; this.savedReplyManager = savedReplyManager;
this.post = post; this.post = post;
this.parser = parser;
} }
@Override @Override
@ -48,17 +53,16 @@ class PostParseCallable implements Callable<Post> {
// Process the filters before finish, because parsing the html is dependent on filter matches // Process the filters before finish, because parsing the html is dependent on filter matches
processPostFilter(post); processPostFilter(post);
if (!post.finish()) { post.isSavedReply(savedReplyManager.isSaved(post.board.code, post.id));
Logger.e(TAG, "Incorrect data about post received for post " + post.no);
return null;
}
post.isSavedReply = savedReplyManager.isSaved(post.boardId, post.no);
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(); int filterSize = filters.size();
for (int i = 0; i < filterSize; i++) { for (int i = 0; i < filterSize; i++) {
Filter filter = filters.get(i); Filter filter = filters.get(i);
@ -66,13 +70,13 @@ class PostParseCallable implements Callable<Post> {
FilterEngine.FilterAction action = FilterEngine.FilterAction.forId(filter.action); FilterEngine.FilterAction action = FilterEngine.FilterAction.forId(filter.action);
switch (action) { switch (action) {
case COLOR: case COLOR:
post.filterHighlightedColor = filter.color; post.filter(filter.color, false, false);
break; break;
case HIDE: case HIDE:
post.filterStub = true; post.filter(0, true, false);
break; break;
case REMOVE: case REMOVE:
post.filterRemove = true; post.filter(0, false, true);
break; break;
} }
} }

@ -3,18 +3,22 @@ package org.floens.chan.core.site.sites.chan4;
import com.android.volley.Response; import com.android.volley.Response;
import com.android.volley.VolleyError; 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.Board;
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.core.site.Boards; import org.floens.chan.core.site.Boards;
import org.floens.chan.core.site.Site; import org.floens.chan.core.site.Site;
import org.floens.chan.core.site.SiteEndpoints; import org.floens.chan.core.site.SiteEndpoints;
import org.floens.chan.core.site.loaders.Chan4ReaderRequest;
import org.floens.chan.utils.Logger; import org.floens.chan.utils.Logger;
import java.util.ArrayList; import java.util.ArrayList;
import java.util.Collections; import java.util.Collections;
import java.util.List; import java.util.List;
import java.util.Locale; import java.util.Locale;
import java.util.Map;
import java.util.Random; import java.util.Random;
import static org.floens.chan.Chan.getGraph; import static org.floens.chan.Chan.getGraph;
@ -36,13 +40,13 @@ public class Chan4 implements Site {
} }
@Override @Override
public String imageUrl(Post post) { public String imageUrl(Post.Builder post, Map<String, String> arg) {
return "https://i.4cdn.org/" + post.boardId + "/" + Long.toString(post.tim) + "." + post.ext; return "https://i.4cdn.org/" + post.board.code + "/" + arg.get("tim") + "." + arg.get("ext");
} }
@Override @Override
public String thumbnailUrl(Post post) { public String thumbnailUrl(Post.Builder post, boolean spoiler, Map<String, String> arg) {
if (post.spoiler) { if (spoiler) {
if (post.board.customSpoilers >= 0) { if (post.board.customSpoilers >= 0) {
int i = random.nextInt(post.board.customSpoilers) + 1; int i = random.nextInt(post.board.customSpoilers) + 1;
return "https://s.4cdn.org/image/spoiler-" + post.board.code + i + ".png"; 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"; return "https://s.4cdn.org/image/spoiler.png";
} }
} else { } 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 @Override
public String flag(Post post) { public String flag(Post.Builder post, String countryCode, Map<String, String> arg) {
return "https://s.4cdn.org/image/country/" + post.country.toLowerCase(Locale.ENGLISH) + ".gif"; return "https://s.4cdn.org/image/country/" + countryCode.toLowerCase(Locale.ENGLISH) + ".gif";
} }
@Override @Override
@ -90,9 +94,6 @@ public class Chan4 implements Site {
case POSTING: case POSTING:
// yes, we support posting. // yes, we support posting.
return true; return true;
case DYNAMIC_BOARDS:
// yes, boards.json
return true;
case LOGIN: case LOGIN:
// 4chan pass. // 4chan pass.
return true; return true;
@ -106,6 +107,7 @@ public class Chan4 implements Site {
@Override @Override
public BoardsType boardsType() { public BoardsType boardsType() {
// yes, boards.json
return BoardsType.DYNAMIC; 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 @Override
public void onChanLoaderData(ChanThread result) { public void onChanLoaderData(ChanThread result) {
for (Post post : result.posts) { for (Post post : result.posts) {
if (post.hasImage) { if (post.image != null) {
final String imageUrl = post.imageUrl; final String imageUrl = post.image.imageUrl;
fileCache.downloadFile(imageUrl, new FileCache.DownloadedCallback() { fileCache.downloadFile(imageUrl, new FileCache.DownloadedCallback() {
@Override @Override
public void onProgress(long downloaded, long total, boolean done) { public void onProgress(long downloaded, long total, boolean done) {

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

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

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

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

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

@ -30,7 +30,6 @@ import android.view.ViewGroup;
import android.widget.ImageView; import android.widget.ImageView;
import android.widget.TextView; import android.widget.TextView;
import org.floens.chan.Chan;
import org.floens.chan.R; import org.floens.chan.R;
import org.floens.chan.controller.Controller; import org.floens.chan.controller.Controller;
import org.floens.chan.core.database.DatabaseManager; import org.floens.chan.core.database.DatabaseManager;
@ -63,7 +62,8 @@ public class FiltersController extends Controller implements ToolbarMenuItem.Too
@Inject @Inject
DatabaseManager databaseManager; DatabaseManager databaseManager;
private FilterEngine filterEngine; @Inject
FilterEngine filterEngine;
private RecyclerView recyclerView; private RecyclerView recyclerView;
private FloatingActionButton add; private FloatingActionButton add;
@ -108,8 +108,6 @@ public class FiltersController extends Controller implements ToolbarMenuItem.Too
super.onCreate(); super.onCreate();
getGraph().inject(this); getGraph().inject(this);
filterEngine = FilterEngine.getInstance();
navigationItem.setTitle(R.string.filters_screen); navigationItem.setTitle(R.string.filters_screen);
navigationItem.menu = new ToolbarMenu(context); navigationItem.menu = new ToolbarMenu(context);
navigationItem.menu.addItem(new ToolbarMenuItem(context, this, SEARCH_ID, R.drawable.ic_search_white_24dp)); 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) { if (view instanceof PostCellInterface) {
PostCellInterface postView = (PostCellInterface) view; PostCellInterface postView = (PostCellInterface) view;
Post post = postView.getPost(); 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(); thumbnail = postView.getThumbnailView();
break; break;
} }

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

@ -38,7 +38,6 @@ import android.widget.ImageView;
import android.widget.LinearLayout; import android.widget.LinearLayout;
import android.widget.TextView; import android.widget.TextView;
import org.floens.chan.Chan;
import org.floens.chan.R; import org.floens.chan.R;
import org.floens.chan.core.manager.BoardManager; import org.floens.chan.core.manager.BoardManager;
import org.floens.chan.core.manager.FilterEngine; import org.floens.chan.core.manager.FilterEngine;
@ -79,6 +78,9 @@ public class FilterLayout extends LinearLayout implements View.OnClickListener {
@Inject @Inject
BoardManager boardManager; BoardManager boardManager;
@Inject
FilterEngine filterEngine;
private FilterLayoutCallback callback; private FilterLayoutCallback callback;
private Filter filter; private Filter filter;
@ -161,7 +163,7 @@ public class FilterLayout extends LinearLayout implements View.OnClickListener {
public void setFilter(Filter filter) { public void setFilter(Filter filter) {
this.filter = filter; this.filter = filter;
appliedBoards.clear(); appliedBoards.clear();
appliedBoards.addAll(FilterEngine.getInstance().getBoardsForFilter(filter)); appliedBoards.addAll(filterEngine.getBoardsForFilter(filter));
pattern.setText(filter.pattern); pattern.setText(filter.pattern);
@ -180,7 +182,7 @@ public class FilterLayout extends LinearLayout implements View.OnClickListener {
public Filter getFilter() { public Filter getFilter() {
filter.enabled = enabled.isChecked(); filter.enabled = enabled.isChecked();
FilterEngine.getInstance().saveBoardsToFilter(appliedBoards, filter); filterEngine.saveBoardsToFilter(appliedBoards, filter);
return filter; return filter;
} }
@ -341,7 +343,7 @@ public class FilterLayout extends LinearLayout implements View.OnClickListener {
} }
private void updateFilterValidity() { 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) { if (valid != patternContainerErrorShowing) {
patternContainerErrorShowing = valid; patternContainerErrorShowing = valid;
@ -386,7 +388,7 @@ public class FilterLayout extends LinearLayout implements View.OnClickListener {
private void updatePatternPreview() { private void updatePatternPreview() {
String text = patternPreview.getText().toString(); 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); 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(); .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()]; String[] keys = new String[linkables.size()];
for (int i = 0; i < linkables.size(); i++) { for (int i = 0; i < linkables.size(); i++) {
keys[i] = linkables.get(i).key; keys[i] = linkables.get(i).key;
@ -250,7 +251,7 @@ public class ThreadLayout extends CoordinatorLayout implements ThreadPresenter.T
.setItems(keys, new DialogInterface.OnClickListener() { .setItems(keys, new DialogInterface.OnClickListener() {
@Override @Override
public void onClick(DialogInterface dialog, int which) { public void onClick(DialogInterface dialog, int which) {
presenter.onPostLinkableClicked(linkables.get(which)); presenter.onPostLinkableClicked(post, linkables.get(which));
} }
}) })
.show(); .show();

@ -346,7 +346,7 @@ public class ThreadListLayout extends FrameLayout implements ReplyLayout.ReplyLa
if (view instanceof PostCellInterface) { if (view instanceof PostCellInterface) {
PostCellInterface postView = (PostCellInterface) view; PostCellInterface postView = (PostCellInterface) view;
Post post = postView.getPost(); 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(); thumbnail = postView.getThumbnailView();
break; break;
} }
@ -503,7 +503,8 @@ public class ThreadListLayout extends FrameLayout implements ReplyLayout.ReplyLa
View child = parent.getChildAt(i); View child = parent.getChildAt(i);
if (child instanceof PostCellInterface) { if (child instanceof PostCellInterface) {
PostCellInterface postView = (PostCellInterface) child; 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(); RecyclerView.LayoutParams params = (RecyclerView.LayoutParams) child.getLayoutParams();
int top = child.getTop() + params.topMargin; int top = child.getTop() + params.topMargin;
int left = child.getLeft() + params.leftMargin; int left = child.getLeft() + params.leftMargin;

@ -196,7 +196,7 @@ public class WatchNotifier extends Service {
prefix = postForExpandedLine.title.subSequence(0, SUBJECT_LENGTH); 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) { if (postForExpandedLine.comment.length() > 0) {
comment += postForExpandedLine.comment; comment += postForExpandedLine.comment;
} }

Loading…
Cancel
Save