organize site packages, refactor comment parser

a bunch of renaming of the site classes, because we know know more how
we want to structure this.
adds the commonsite, the easy way to implement a site.
work on the comment parser, to properly recursively parse html, so we
can later easily extend it for each site.
refactor-toolbar
Floens 8 years ago
parent e78d3e7a7f
commit a14b407bf9
  1. 6
      Clover/app/src/main/java/org/floens/chan/Chan.java
  2. 4
      Clover/app/src/main/java/org/floens/chan/core/database/DatabaseHelper.java
  3. 6
      Clover/app/src/main/java/org/floens/chan/core/manager/WatchManager.java
  4. 6
      Clover/app/src/main/java/org/floens/chan/core/model/PostLinkable.java
  5. 24
      Clover/app/src/main/java/org/floens/chan/core/pool/ChanLoaderFactory.java
  6. 13
      Clover/app/src/main/java/org/floens/chan/core/presenter/ReplyPresenter.java
  7. 2
      Clover/app/src/main/java/org/floens/chan/core/presenter/SiteSetupPresenter.java
  8. 10
      Clover/app/src/main/java/org/floens/chan/core/presenter/SitesSetupPresenter.java
  9. 15
      Clover/app/src/main/java/org/floens/chan/core/presenter/ThreadPresenter.java
  10. 12
      Clover/app/src/main/java/org/floens/chan/core/site/Resolvable.java
  11. 84
      Clover/app/src/main/java/org/floens/chan/core/site/Site.java
  12. 83
      Clover/app/src/main/java/org/floens/chan/core/site/SiteActions.java
  13. 24
      Clover/app/src/main/java/org/floens/chan/core/site/SiteAuthentication.java
  14. 45
      Clover/app/src/main/java/org/floens/chan/core/site/SiteBase.java
  15. 31
      Clover/app/src/main/java/org/floens/chan/core/site/SiteResolver.java
  16. 4
      Clover/app/src/main/java/org/floens/chan/core/site/SiteService.java
  17. 38
      Clover/app/src/main/java/org/floens/chan/core/site/SiteUrlHandler.java
  18. 8
      Clover/app/src/main/java/org/floens/chan/core/site/Sites.java
  19. 14
      Clover/app/src/main/java/org/floens/chan/core/site/common/ChanReader.java
  20. 426
      Clover/app/src/main/java/org/floens/chan/core/site/common/CommonSite.java
  21. 238
      Clover/app/src/main/java/org/floens/chan/core/site/common/DefaultFutabaChanParserHandler.java
  22. 153
      Clover/app/src/main/java/org/floens/chan/core/site/common/DefaultPostParser.java
  23. 44
      Clover/app/src/main/java/org/floens/chan/core/site/common/FutabaChanParserHandler.java
  24. 21
      Clover/app/src/main/java/org/floens/chan/core/site/common/FutabaChanReader.java
  25. 66
      Clover/app/src/main/java/org/floens/chan/core/site/common/MultipartHttpCall.java
  26. 37
      Clover/app/src/main/java/org/floens/chan/core/site/http/HttpCall.java
  27. 1
      Clover/app/src/main/java/org/floens/chan/core/site/http/Reply.java
  28. 6
      Clover/app/src/main/java/org/floens/chan/core/site/http/ReplyResponse.java
  29. 4
      Clover/app/src/main/java/org/floens/chan/core/site/loader/ChanLoaderRequestParams.java
  30. 12
      Clover/app/src/main/java/org/floens/chan/core/site/loader/ChanThreadLoader.java
  31. 31
      Clover/app/src/main/java/org/floens/chan/core/site/parser/ChanReader.java
  32. 21
      Clover/app/src/main/java/org/floens/chan/core/site/parser/ChanReaderProcessingQueue.java
  33. 26
      Clover/app/src/main/java/org/floens/chan/core/site/parser/ChanReaderRequest.java
  34. 307
      Clover/app/src/main/java/org/floens/chan/core/site/parser/CommentParser.java
  35. 6
      Clover/app/src/main/java/org/floens/chan/core/site/parser/CommentParserHelper.java
  36. 20
      Clover/app/src/main/java/org/floens/chan/core/site/parser/PostParseCallable.java
  37. 12
      Clover/app/src/main/java/org/floens/chan/core/site/parser/PostParser.java
  38. 352
      Clover/app/src/main/java/org/floens/chan/core/site/sites/chan4/Chan4.java
  39. 698
      Clover/app/src/main/java/org/floens/chan/core/site/sites/vichan/ViChan.java
  40. 47
      Clover/app/src/main/java/org/floens/chan/core/site/sites/vichan/ViChanCommentParser.java
  41. 134
      Clover/app/src/main/java/org/floens/chan/core/site/sites/vichan/ViChanParserHandler.java
  42. 129
      Clover/app/src/main/java/org/floens/chan/core/site/sites/vichan/ViChanReplyHttpCall.java
  43. 6
      Clover/app/src/main/java/org/floens/chan/ui/activity/StartActivity.java
  44. 4
      Clover/app/src/main/java/org/floens/chan/ui/captcha/CaptchaLayout.java
  45. 4
      Clover/app/src/main/java/org/floens/chan/ui/captcha/CaptchaNojsLayout.java
  46. 6
      Clover/app/src/main/java/org/floens/chan/ui/captcha/GenericWebViewAuthenticationLayout.java
  47. 4
      Clover/app/src/main/java/org/floens/chan/ui/captcha/LegacyCaptchaLayout.java
  48. 2
      Clover/app/src/main/java/org/floens/chan/ui/controller/BrowseController.java
  49. 11
      Clover/app/src/main/java/org/floens/chan/ui/controller/LoginController.java
  50. 7
      Clover/app/src/main/java/org/floens/chan/ui/controller/ReportController.java
  51. 21
      Clover/app/src/main/java/org/floens/chan/ui/controller/ThemeSettingsController.java
  52. 2
      Clover/app/src/main/java/org/floens/chan/ui/controller/ThreadController.java
  53. 2
      Clover/app/src/main/java/org/floens/chan/ui/controller/ViewThreadController.java
  54. 4
      Clover/app/src/main/java/org/floens/chan/ui/layout/ReplyLayout.java
  55. 2
      Clover/app/src/main/java/org/floens/chan/ui/layout/ThreadLayout.java
  56. 3
      Clover/app/src/main/java/org/floens/chan/ui/theme/Theme.java
  57. 2
      Clover/app/src/main/java/org/floens/chan/ui/theme/ThemeHelper.java
  58. 3
      Clover/build.gradle

@ -31,7 +31,7 @@ import org.floens.chan.core.database.DatabaseManager;
import org.floens.chan.core.di.AppModule; import org.floens.chan.core.di.AppModule;
import org.floens.chan.core.di.NetModule; import org.floens.chan.core.di.NetModule;
import org.floens.chan.core.di.UserAgentProvider; import org.floens.chan.core.di.UserAgentProvider;
import org.floens.chan.core.site.SiteManager; import org.floens.chan.core.site.SiteService;
import org.floens.chan.utils.AndroidUtils; import org.floens.chan.utils.AndroidUtils;
import org.floens.chan.utils.Logger; import org.floens.chan.utils.Logger;
import org.floens.chan.utils.Time; import org.floens.chan.utils.Time;
@ -53,7 +53,7 @@ public class Chan extends Application implements UserAgentProvider, Application.
private int activityForegroundCounter = 0; private int activityForegroundCounter = 0;
@Inject @Inject
SiteManager siteManager; SiteService siteService;
@Inject @Inject
DatabaseManager databaseManager; DatabaseManager databaseManager;
@ -93,7 +93,7 @@ public class Chan extends Application implements UserAgentProvider, Application.
initializeGraph(); initializeGraph();
siteManager.initialize(); siteService.initialize();
Time.endTiming("Initializing application", startTime); Time.endTiming("Initializing application", startTime);

@ -33,7 +33,7 @@ import org.floens.chan.core.model.orm.Pin;
import org.floens.chan.core.model.orm.SavedReply; import org.floens.chan.core.model.orm.SavedReply;
import org.floens.chan.core.model.orm.SiteModel; import org.floens.chan.core.model.orm.SiteModel;
import org.floens.chan.core.model.orm.ThreadHide; import org.floens.chan.core.model.orm.ThreadHide;
import org.floens.chan.core.site.SiteManager; import org.floens.chan.core.site.SiteService;
import org.floens.chan.utils.Logger; import org.floens.chan.utils.Logger;
import java.sql.SQLException; import java.sql.SQLException;
@ -228,7 +228,7 @@ public class DatabaseHelper extends OrmLiteSqliteOpenHelper {
Logger.e(TAG, "Error upgrading to version 22", e); Logger.e(TAG, "Error upgrading to version 22", e);
} }
SiteManager.addSiteForLegacy(); SiteService.addSiteForLegacy();
} }
} }

@ -38,7 +38,7 @@ import org.floens.chan.core.model.orm.Loadable;
import org.floens.chan.core.model.orm.Pin; import org.floens.chan.core.model.orm.Pin;
import org.floens.chan.core.pool.ChanLoaderFactory; 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.loader.ChanLoader; import org.floens.chan.core.site.loader.ChanThreadLoader;
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;
import org.floens.chan.utils.Logger; import org.floens.chan.utils.Logger;
@ -668,11 +668,11 @@ public class WatchManager {
} }
} }
public class PinWatcher implements ChanLoader.ChanLoaderCallback { public class PinWatcher implements ChanThreadLoader.ChanLoaderCallback {
private static final String TAG = "PinWatcher"; private static final String TAG = "PinWatcher";
private final Pin pin; private final Pin pin;
private ChanLoader chanLoader; private ChanThreadLoader chanLoader;
private final List<Post> posts = new ArrayList<>(); private final List<Post> posts = new ArrayList<>();
private final List<Post> quotes = new ArrayList<>(); private final List<Post> quotes = new ArrayList<>();

@ -27,7 +27,7 @@ import org.floens.chan.ui.cell.PostCell;
import org.floens.chan.ui.theme.Theme; import org.floens.chan.ui.theme.Theme;
/** /**
* A Clickable span that handles post clicks. These are created in ChanParser for post quotes, spoilers etc.<br> * A Clickable span that handles post clicks. These are created in PostParser for post quotes, spoilers etc.<br>
* PostCell has a {@link PostCell.PostViewMovementMethod}, that searches spans at the location the TextView was tapped, * PostCell has a {@link PostCell.PostViewMovementMethod}, that searches spans at the location the TextView was tapped,
* and handled if it was a PostLinkable. * and handled if it was a PostLinkable.
*/ */
@ -37,14 +37,14 @@ public class PostLinkable extends ClickableSpan {
} }
public final Theme theme; public final Theme theme;
public final String key; public final CharSequence key;
public final Object value; public final Object value;
public final Type type; public final Type type;
private boolean spoilerVisible = ChanSettings.revealTextSpoilers.get(); private boolean spoilerVisible = ChanSettings.revealTextSpoilers.get();
private int markedNo = -1; private int markedNo = -1;
public PostLinkable(Theme theme, String key, Object value, Type type) { public PostLinkable(Theme theme, CharSequence key, Object value, Type type) {
this.theme = theme; this.theme = theme;
this.key = key; this.key = key;
this.value = value; this.value = value;

@ -19,7 +19,7 @@ package org.floens.chan.core.pool;
import android.util.LruCache; import android.util.LruCache;
import org.floens.chan.core.site.loader.ChanLoader; import org.floens.chan.core.site.loader.ChanThreadLoader;
import org.floens.chan.core.model.orm.Loadable; import org.floens.chan.core.model.orm.Loadable;
import java.util.HashMap; import java.util.HashMap;
@ -30,24 +30,24 @@ import javax.inject.Singleton;
/** /**
* ChanLoaderFactory is a factory for ChanLoaders. ChanLoaders for threads are cached. * ChanLoaderFactory is a factory for ChanLoaders. ChanLoaders for threads are cached.
* <p>Each reference to a loader is a {@link ChanLoader.ChanLoaderCallback}, these * <p>Each reference to a loader is a {@link ChanThreadLoader.ChanLoaderCallback}, these
* references can be obtained with {@link #obtain(Loadable, ChanLoader.ChanLoaderCallback)}} and released * references can be obtained with {@link #obtain(Loadable, ChanThreadLoader.ChanLoaderCallback)}} and released
* with {@link #release(ChanLoader, ChanLoader.ChanLoaderCallback)}. * with {@link #release(ChanThreadLoader, ChanThreadLoader.ChanLoaderCallback)}.
*/ */
@Singleton @Singleton
public class ChanLoaderFactory { public class ChanLoaderFactory {
// private static final String TAG = "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 Map<Loadable, ChanLoader> threadLoaders = new HashMap<>(); private Map<Loadable, ChanThreadLoader> threadLoaders = new HashMap<>();
private LruCache<Loadable, ChanLoader> threadLoadersCache = new LruCache<>(THREAD_LOADERS_CACHE_SIZE); private LruCache<Loadable, ChanThreadLoader> threadLoadersCache = new LruCache<>(THREAD_LOADERS_CACHE_SIZE);
@Inject @Inject
public ChanLoaderFactory() { public ChanLoaderFactory() {
} }
public ChanLoader obtain(Loadable loadable, ChanLoader.ChanLoaderCallback listener) { public ChanThreadLoader obtain(Loadable loadable, ChanThreadLoader.ChanLoaderCallback listener) {
ChanLoader chanLoader; ChanThreadLoader chanLoader;
if (loadable.isThreadMode()) { if (loadable.isThreadMode()) {
if (!loadable.isFromDatabase()) { if (!loadable.isFromDatabase()) {
throw new IllegalArgumentException(); throw new IllegalArgumentException();
@ -63,11 +63,11 @@ public class ChanLoaderFactory {
} }
if (chanLoader == null) { if (chanLoader == null) {
chanLoader = new ChanLoader(loadable); chanLoader = new ChanThreadLoader(loadable);
threadLoaders.put(loadable, chanLoader); threadLoaders.put(loadable, chanLoader);
} }
} else { } else {
chanLoader = new ChanLoader(loadable); chanLoader = new ChanThreadLoader(loadable);
} }
chanLoader.addListener(listener); chanLoader.addListener(listener);
@ -75,10 +75,10 @@ public class ChanLoaderFactory {
return chanLoader; return chanLoader;
} }
public void release(ChanLoader chanLoader, ChanLoader.ChanLoaderCallback listener) { public void release(ChanThreadLoader chanLoader, ChanThreadLoader.ChanLoaderCallback listener) {
Loadable loadable = chanLoader.getLoadable(); Loadable loadable = chanLoader.getLoadable();
if (loadable.isThreadMode()) { if (loadable.isThreadMode()) {
ChanLoader foundChanLoader = threadLoaders.get(loadable); ChanThreadLoader foundChanLoader = threadLoaders.get(loadable);
if (foundChanLoader == null) { if (foundChanLoader == null) {
throw new IllegalStateException("The released loader does not exist"); throw new IllegalStateException("The released loader does not exist");

@ -29,8 +29,9 @@ import org.floens.chan.core.model.orm.Board;
import org.floens.chan.core.model.orm.Loadable; import org.floens.chan.core.model.orm.Loadable;
import org.floens.chan.core.model.orm.SavedReply; import org.floens.chan.core.model.orm.SavedReply;
import org.floens.chan.core.settings.ChanSettings; import org.floens.chan.core.settings.ChanSettings;
import org.floens.chan.core.site.Authentication; import org.floens.chan.core.site.SiteAuthentication;
import org.floens.chan.core.site.Site; import org.floens.chan.core.site.Site;
import org.floens.chan.core.site.SiteActions;
import org.floens.chan.core.site.http.HttpCall; import org.floens.chan.core.site.http.HttpCall;
import org.floens.chan.core.site.http.Reply; import org.floens.chan.core.site.http.Reply;
import org.floens.chan.core.site.http.ReplyResponse; import org.floens.chan.core.site.http.ReplyResponse;
@ -49,7 +50,7 @@ import static org.floens.chan.utils.AndroidUtils.getReadableFileSize;
import static org.floens.chan.utils.AndroidUtils.getRes; import static org.floens.chan.utils.AndroidUtils.getRes;
import static org.floens.chan.utils.AndroidUtils.getString; import static org.floens.chan.utils.AndroidUtils.getString;
public class ReplyPresenter implements AuthenticationLayoutCallback, ImagePickDelegate.ImagePickCallback, Site.PostListener { public class ReplyPresenter implements AuthenticationLayoutCallback, ImagePickDelegate.ImagePickCallback, SiteActions.PostListener {
public enum Page { public enum Page {
INPUT, INPUT,
AUTHENTICATION, AUTHENTICATION,
@ -195,7 +196,7 @@ public class ReplyPresenter implements AuthenticationLayoutCallback, ImagePickDe
draft.spoilerImage = draft.spoilerImage && board.spoilers; draft.spoilerImage = draft.spoilerImage && board.spoilers;
draft.captchaResponse = null; draft.captchaResponse = null;
if (loadable.site.postRequiresAuthentication()) { if (loadable.site.actions().postRequiresAuthentication()) {
switchPage(Page.AUTHENTICATION, true); switchPage(Page.AUTHENTICATION, true);
} else { } else {
makeSubmitCall(); makeSubmitCall();
@ -369,7 +370,7 @@ public class ReplyPresenter implements AuthenticationLayoutCallback, ImagePickDe
} }
private void makeSubmitCall() { private void makeSubmitCall() {
loadable.getSite().post(draft, this); loadable.getSite().actions().post(draft, this);
switchPage(Page.LOADING, true); switchPage(Page.LOADING, true);
} }
@ -384,7 +385,7 @@ public class ReplyPresenter implements AuthenticationLayoutCallback, ImagePickDe
callback.setPage(Page.INPUT, animate); callback.setPage(Page.INPUT, animate);
break; break;
case AUTHENTICATION: case AUTHENTICATION:
Authentication authentication = loadable.site.postAuthenticate(); SiteAuthentication authentication = loadable.site.actions().postAuthenticate();
callback.initializeAuthentication(loadable.site, authentication, this); callback.initializeAuthentication(loadable.site, authentication, this);
callback.setPage(Page.AUTHENTICATION, true); callback.setPage(Page.AUTHENTICATION, true);
@ -447,7 +448,7 @@ public class ReplyPresenter implements AuthenticationLayoutCallback, ImagePickDe
void setPage(Page page, boolean animate); void setPage(Page page, boolean animate);
void initializeAuthentication(Site site, Authentication authentication, void initializeAuthentication(Site site, SiteAuthentication authentication,
AuthenticationLayoutCallback callback); AuthenticationLayoutCallback callback);
void resetAuthentication(); void resetAuthentication();

@ -38,7 +38,7 @@ public class SiteSetupPresenter {
public void show() { public void show() {
setBoardCount(callback, site); setBoardCount(callback, site);
if (hasLogin) { if (hasLogin) {
callback.setIsLoggedIn(site.isLoggedIn()); callback.setIsLoggedIn(site.actions().isLoggedIn());
} }
} }

@ -20,7 +20,7 @@ package org.floens.chan.core.presenter;
import org.floens.chan.core.manager.BoardManager; import org.floens.chan.core.manager.BoardManager;
import org.floens.chan.core.site.Site; import org.floens.chan.core.site.Site;
import org.floens.chan.core.site.SiteManager; import org.floens.chan.core.site.SiteService;
import org.floens.chan.core.site.Sites; import org.floens.chan.core.site.Sites;
import java.util.ArrayList; import java.util.ArrayList;
@ -29,7 +29,7 @@ import java.util.List;
import javax.inject.Inject; import javax.inject.Inject;
public class SitesSetupPresenter { public class SitesSetupPresenter {
private SiteManager siteManager; private SiteService siteService;
private BoardManager boardManager; private BoardManager boardManager;
private Callback callback; private Callback callback;
@ -38,8 +38,8 @@ public class SitesSetupPresenter {
private List<Site> sites = new ArrayList<>(); private List<Site> sites = new ArrayList<>();
@Inject @Inject
public SitesSetupPresenter(SiteManager siteManager, BoardManager boardManager) { public SitesSetupPresenter(SiteService siteService, BoardManager boardManager) {
this.siteManager = siteManager; this.siteService = siteService;
this.boardManager = boardManager; this.boardManager = boardManager;
} }
@ -84,7 +84,7 @@ public class SitesSetupPresenter {
} }
public void onAddClicked(String url) { public void onAddClicked(String url) {
siteManager.addSite(url, new SiteManager.SiteAddCallback() { siteService.addSite(url, new SiteService.SiteAddCallback() {
@Override @Override
public void onSiteAdded(Site site) { public void onSiteAdded(Site site) {
siteAdded(site); siteAdded(site);

@ -21,7 +21,6 @@ import android.text.TextUtils;
import org.floens.chan.Chan; import org.floens.chan.Chan;
import org.floens.chan.R; import org.floens.chan.R;
import org.floens.chan.core.site.loader.ChanLoader;
import org.floens.chan.core.database.DatabaseManager; 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.manager.WatchManager; import org.floens.chan.core.manager.WatchManager;
@ -37,9 +36,11 @@ import org.floens.chan.core.model.orm.SavedReply;
import org.floens.chan.core.pool.ChanLoaderFactory; 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.core.site.SiteActions;
import org.floens.chan.core.site.http.DeleteRequest; import org.floens.chan.core.site.http.DeleteRequest;
import org.floens.chan.core.site.http.DeleteResponse; import org.floens.chan.core.site.http.DeleteResponse;
import org.floens.chan.core.site.http.HttpCall; import org.floens.chan.core.site.http.HttpCall;
import org.floens.chan.core.site.loader.ChanThreadLoader;
import org.floens.chan.ui.adapter.PostAdapter; import org.floens.chan.ui.adapter.PostAdapter;
import org.floens.chan.ui.adapter.PostsFilter; import org.floens.chan.ui.adapter.PostsFilter;
import org.floens.chan.ui.cell.PostCellInterface; import org.floens.chan.ui.cell.PostCellInterface;
@ -58,7 +59,7 @@ import javax.inject.Inject;
import static org.floens.chan.utils.AndroidUtils.getString; import static org.floens.chan.utils.AndroidUtils.getString;
public class ThreadPresenter implements ChanLoader.ChanLoaderCallback, PostAdapter.PostAdapterCallback, PostCellInterface.PostCellCallback, ThreadStatusCell.Callback, ThreadListLayout.ThreadListLayoutPresenterCallback { public class ThreadPresenter implements ChanThreadLoader.ChanLoaderCallback, PostAdapter.PostAdapterCallback, PostCellInterface.PostCellCallback, ThreadStatusCell.Callback, ThreadListLayout.ThreadListLayoutPresenterCallback {
private static final int POST_OPTION_QUOTE = 0; private static final int POST_OPTION_QUOTE = 0;
private static final int POST_OPTION_QUOTE_TEXT = 1; private static final int POST_OPTION_QUOTE_TEXT = 1;
private static final int POST_OPTION_INFO = 2; private static final int POST_OPTION_INFO = 2;
@ -81,7 +82,7 @@ public class ThreadPresenter implements ChanLoader.ChanLoaderCallback, PostAdapt
private ChanLoaderFactory chanLoaderFactory; private ChanLoaderFactory chanLoaderFactory;
private Loadable loadable; private Loadable loadable;
private ChanLoader chanLoader; private ChanThreadLoader chanLoader;
private boolean searchOpen = false; private boolean searchOpen = false;
private String searchQuery; private String searchQuery;
private PostsFilter.Order order = PostsFilter.Order.BUMP; private PostsFilter.Order order = PostsFilter.Order.BUMP;
@ -252,7 +253,7 @@ public class ThreadPresenter implements ChanLoader.ChanLoaderCallback, PostAdapt
} }
/* /*
* ChanLoader callbacks * ChanThreadLoader callbacks
*/ */
@Override @Override
public void onChanLoaderData(ChanThread result) { public void onChanLoaderData(ChanThread result) {
@ -530,12 +531,12 @@ public class ThreadPresenter implements ChanLoader.ChanLoaderCallback, PostAdapt
watchManager.createPin(pinLoadable, post); watchManager.createPin(pinLoadable, post);
break; break;
case POST_OPTION_OPEN_BROWSER: { case POST_OPTION_OPEN_BROWSER: {
String url = loadable.site.desktopUrl(loadable, post.isOP ? null : post); String url = loadable.site.resolvable().desktopUrl(loadable, post.isOP ? null : post);
AndroidUtils.openLink(url); AndroidUtils.openLink(url);
break; break;
} }
case POST_OPTION_SHARE: { case POST_OPTION_SHARE: {
String url = loadable.site.desktopUrl(loadable, post.isOP ? null : post); String url = loadable.site.resolvable().desktopUrl(loadable, post.isOP ? null : post);
AndroidUtils.shareLink(url); AndroidUtils.shareLink(url);
break; break;
} }
@ -637,7 +638,7 @@ public class ThreadPresenter implements ChanLoader.ChanLoaderCallback, PostAdapt
); );
if (reply != null) { if (reply != null) {
Site site = loadable.getSite(); Site site = loadable.getSite();
site.delete(new DeleteRequest(post, reply, onlyImageDelete), new Site.DeleteListener() { site.actions().delete(new DeleteRequest(post, reply, onlyImageDelete), new SiteActions.DeleteListener() {
@Override @Override
public void onDeleteComplete(HttpCall httpPost, DeleteResponse deleteResponse) { public void onDeleteComplete(HttpCall httpPost, DeleteResponse deleteResponse) {
String message; String message;

@ -1,12 +0,0 @@
package org.floens.chan.core.site;
import okhttp3.HttpUrl;
public interface Resolvable {
boolean matchesName(String value);
boolean respondsTo(HttpUrl url);
Class<? extends Site> getSiteClass();
}

@ -17,33 +17,25 @@
*/ */
package org.floens.chan.core.site; package org.floens.chan.core.site;
import android.support.annotation.Nullable;
import org.floens.chan.core.model.Post; import org.floens.chan.core.model.Post;
import org.floens.chan.core.model.json.site.SiteConfig; import org.floens.chan.core.model.json.site.SiteConfig;
import org.floens.chan.core.model.orm.Board; import org.floens.chan.core.model.orm.Board;
import org.floens.chan.core.model.orm.Loadable; import org.floens.chan.core.model.orm.Loadable;
import org.floens.chan.core.settings.Setting; import org.floens.chan.core.settings.Setting;
import org.floens.chan.core.settings.json.JsonSettings; import org.floens.chan.core.settings.json.JsonSettings;
import org.floens.chan.core.site.common.ChanReader; import org.floens.chan.core.site.parser.ChanReader;
import org.floens.chan.core.site.http.DeleteRequest; import org.floens.chan.core.site.http.DeleteRequest;
import org.floens.chan.core.site.http.DeleteResponse;
import org.floens.chan.core.site.http.HttpCall;
import org.floens.chan.core.site.http.LoginRequest; import org.floens.chan.core.site.http.LoginRequest;
import org.floens.chan.core.site.http.LoginResponse;
import org.floens.chan.core.site.http.Reply; import org.floens.chan.core.site.http.Reply;
import org.floens.chan.core.site.http.ReplyResponse;
import java.util.List; import java.util.List;
import okhttp3.HttpUrl;
public interface Site { public interface Site {
enum Feature { enum Feature {
/** /**
* This site supports posting. (Or rather, we've implemented support for it.) * This site supports posting. (Or rather, we've implemented support for it.)
* *
* @see #post(Reply, PostListener) * @see SiteActions#post(Reply, SiteActions.PostListener)
* @see SiteEndpoints#reply(Loadable) * @see SiteEndpoints#reply(Loadable)
*/ */
POSTING, POSTING,
@ -51,7 +43,7 @@ public interface Site {
/** /**
* This site supports deleting posts. * This site supports deleting posts.
* *
* @see #delete(DeleteRequest, DeleteListener) * @see SiteActions#delete(DeleteRequest, SiteActions.DeleteListener)
* @see SiteEndpoints#delete(Post) * @see SiteEndpoints#delete(Post)
*/ */
POST_DELETE, POST_DELETE,
@ -66,7 +58,7 @@ public interface Site {
/** /**
* This site supports some sort of authentication (like 4pass). * This site supports some sort of authentication (like 4pass).
* *
* @see #login(LoginRequest, LoginListener) * @see SiteActions#login(LoginRequest, SiteActions.LoginListener)
* @see SiteEndpoints#login() * @see SiteEndpoints#login()
*/ */
LOGIN LOGIN
@ -90,7 +82,7 @@ public interface Site {
} }
/** /**
* How the boards are organized for this size. * How the boards are organized for this site.
*/ */
enum BoardsType { enum BoardsType {
/** /**
@ -134,9 +126,9 @@ public interface Site {
SiteIcon icon(); SiteIcon icon();
Resolvable resolvable(); BoardsType boardsType();
Loadable resolveLoadable(HttpUrl url); SiteUrlHandler resolvable();
boolean feature(Feature feature); boolean feature(Feature feature);
@ -148,15 +140,9 @@ public interface Site {
SiteRequestModifier requestModifier(); SiteRequestModifier requestModifier();
BoardsType boardsType(); ChanReader chanReader();
String desktopUrl(Loadable loadable, @Nullable Post post);
void boards(BoardsListener boardsListener);
interface BoardsListener { SiteActions actions();
void onBoardsReceived(Boards boards);
}
/** /**
* Return the board for this site with the given {@code code}. * Return the board for this site with the given {@code code}.
@ -178,56 +164,4 @@ public interface Site {
* @return the created board. * @return the created board.
*/ */
Board createBoard(String name, String code); Board createBoard(String name, String code);
ChanReader chanReader();
void post(Reply reply, PostListener postListener);
interface PostListener {
void onPostComplete(HttpCall httpCall, ReplyResponse replyResponse);
void onPostError(HttpCall httpCall);
}
boolean postRequiresAuthentication();
/**
* If {@link ReplyResponse#requireAuthentication} was {@code true}, or if
* {@link #postRequiresAuthentication()} is {@code true}, get the authentication
* required to post.
* <p>
* <p>Some sites know beforehand if you need to authenticate, some sites only report it
* after posting. That's why there are two methods.</p>
*
* @return an {@link Authentication} model that describes the way to authenticate.
*/
Authentication postAuthenticate();
void delete(DeleteRequest deleteRequest, DeleteListener deleteListener);
interface DeleteListener {
void onDeleteComplete(HttpCall httpCall, DeleteResponse deleteResponse);
void onDeleteError(HttpCall httpCall);
}
/* TODO(multi-site) this login mechanism is probably not generic enough right now,
* especially if we're thinking about what a login really is
* We'll expand this later when we have a better idea of what other sites require.
*/
void login(LoginRequest loginRequest, LoginListener loginListener);
void logout();
boolean isLoggedIn();
LoginRequest getLoginDetails();
interface LoginListener {
void onLoginComplete(HttpCall httpCall, LoginResponse loginResponse);
void onLoginError(HttpCall httpCall);
}
} }

@ -0,0 +1,83 @@
/*
* 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.site;
import org.floens.chan.core.site.http.DeleteRequest;
import org.floens.chan.core.site.http.DeleteResponse;
import org.floens.chan.core.site.http.HttpCall;
import org.floens.chan.core.site.http.LoginRequest;
import org.floens.chan.core.site.http.LoginResponse;
import org.floens.chan.core.site.http.Reply;
import org.floens.chan.core.site.http.ReplyResponse;
public interface SiteActions {
void boards(BoardsListener boardsListener);
interface BoardsListener {
void onBoardsReceived(Boards boards);
}
void post(Reply reply, PostListener postListener);
interface PostListener {
void onPostComplete(HttpCall httpCall, ReplyResponse replyResponse);
void onPostError(HttpCall httpCall);
}
boolean postRequiresAuthentication();
/**
* If {@link ReplyResponse#requireAuthentication} was {@code true}, or if
* {@link #postRequiresAuthentication()} is {@code true}, get the authentication
* required to post.
* <p>
* <p>Some sites know beforehand if you need to authenticate, some sites only report it
* after posting. That's why there are two methods.</p>
*
* @return an {@link SiteAuthentication} model that describes the way to authenticate.
*/
SiteAuthentication postAuthenticate();
void delete(DeleteRequest deleteRequest, DeleteListener deleteListener);
interface DeleteListener {
void onDeleteComplete(HttpCall httpCall, DeleteResponse deleteResponse);
void onDeleteError(HttpCall httpCall);
}
/* TODO(multi-site) this login mechanism is probably not generic enough right now,
* especially if we're thinking about what a login really is
* We'll expand this later when we have a better idea of what other sites require.
*/
void login(LoginRequest loginRequest, LoginListener loginListener);
void logout();
boolean isLoggedIn();
LoginRequest getLoginDetails();
interface LoginListener {
void onLoginComplete(HttpCall httpCall, LoginResponse loginResponse);
void onLoginError(HttpCall httpCall);
}
}

@ -17,7 +17,7 @@
*/ */
package org.floens.chan.core.site; package org.floens.chan.core.site;
public class Authentication { public class SiteAuthentication {
public enum Type { public enum Type {
NONE, NONE,
CAPTCHA1, CAPTCHA1,
@ -26,33 +26,33 @@ public class Authentication {
GENERIC_WEBVIEW GENERIC_WEBVIEW
} }
public static Authentication fromNone() { public static SiteAuthentication fromNone() {
return new Authentication(Type.NONE); return new SiteAuthentication(Type.NONE);
} }
public static Authentication fromCaptcha1(String siteKey, String baseUrl) { public static SiteAuthentication fromCaptcha1(String siteKey, String baseUrl) {
Authentication a = new Authentication(Type.CAPTCHA1); SiteAuthentication a = new SiteAuthentication(Type.CAPTCHA1);
a.siteKey = siteKey; a.siteKey = siteKey;
a.baseUrl = baseUrl; a.baseUrl = baseUrl;
return a; return a;
} }
public static Authentication fromCaptcha2(String siteKey, String baseUrl) { public static SiteAuthentication fromCaptcha2(String siteKey, String baseUrl) {
Authentication a = new Authentication(Type.CAPTCHA2); SiteAuthentication a = new SiteAuthentication(Type.CAPTCHA2);
a.siteKey = siteKey; a.siteKey = siteKey;
a.baseUrl = baseUrl; a.baseUrl = baseUrl;
return a; return a;
} }
public static Authentication fromCaptcha2nojs(String siteKey, String baseUrl) { public static SiteAuthentication fromCaptcha2nojs(String siteKey, String baseUrl) {
Authentication a = new Authentication(Type.CAPTCHA2_NOJS); SiteAuthentication a = new SiteAuthentication(Type.CAPTCHA2_NOJS);
a.siteKey = siteKey; a.siteKey = siteKey;
a.baseUrl = baseUrl; a.baseUrl = baseUrl;
return a; return a;
} }
public static Authentication fromUrl(String url, String retryText, String successText) { public static SiteAuthentication fromUrl(String url, String retryText, String successText) {
Authentication a = new Authentication(Type.GENERIC_WEBVIEW); SiteAuthentication a = new SiteAuthentication(Type.GENERIC_WEBVIEW);
a.url = url; a.url = url;
a.retryText = retryText; a.retryText = retryText;
a.successText = successText; a.successText = successText;
@ -70,7 +70,7 @@ public class Authentication {
public String retryText; public String retryText;
public String successText; public String successText;
private Authentication(Type type) { private SiteAuthentication(Type type) {
this.type = type; this.type = type;
} }
} }

@ -29,10 +29,7 @@ import org.floens.chan.core.settings.Setting;
import org.floens.chan.core.settings.SettingProvider; import org.floens.chan.core.settings.SettingProvider;
import org.floens.chan.core.settings.json.JsonSettings; import org.floens.chan.core.settings.json.JsonSettings;
import org.floens.chan.core.settings.json.JsonSettingsProvider; import org.floens.chan.core.settings.json.JsonSettingsProvider;
import org.floens.chan.core.site.http.DeleteRequest;
import org.floens.chan.core.site.http.HttpCallManager; import org.floens.chan.core.site.http.HttpCallManager;
import org.floens.chan.core.site.http.LoginRequest;
import org.floens.chan.core.site.http.Reply;
import java.util.ArrayList; import java.util.ArrayList;
import java.util.Collections; import java.util.Collections;
@ -66,16 +63,16 @@ public abstract class SiteBase implements Site {
requestQueue = injector.instance(RequestQueue.class); requestQueue = injector.instance(RequestQueue.class);
boardManager = injector.instance(BoardManager.class); boardManager = injector.instance(BoardManager.class);
loadableProvider = injector.instance(LoadableProvider.class); loadableProvider = injector.instance(LoadableProvider.class);
SiteManager siteManager = injector.instance(SiteManager.class); SiteService siteService = injector.instance(SiteService.class);
settingsProvider = new JsonSettingsProvider(userSettings, () -> { settingsProvider = new JsonSettingsProvider(userSettings, () -> {
siteManager.updateUserSettings(this, userSettings); siteService.updateUserSettings(this, userSettings);
}); });
initializeSettings(); initializeSettings();
if (boardsType() == BoardsType.DYNAMIC) { if (boardsType() == BoardsType.DYNAMIC) {
boards(boards -> boardManager.createAll(boards.boards)); actions().boards(boards -> boardManager.createAll(boards.boards));
} }
} }
@ -108,40 +105,4 @@ public abstract class SiteBase implements Site {
boardManager.createAll(Collections.singletonList(board)); boardManager.createAll(Collections.singletonList(board));
return board; return board;
} }
@Override
public boolean postRequiresAuthentication() {
return false;
}
@Override
public void post(Reply reply, PostListener postListener) {
}
@Override
public Authentication postAuthenticate() {
return Authentication.fromNone();
}
@Override
public void delete(DeleteRequest deleteRequest, DeleteListener deleteListener) {
}
@Override
public void login(LoginRequest loginRequest, LoginListener loginListener) {
}
@Override
public void logout() {
}
@Override
public boolean isLoggedIn() {
return false;
}
@Override
public LoginRequest getLoginDetails() {
return null;
}
} }

@ -37,14 +37,14 @@ public class SiteResolver {
this.loadableProvider = loadableProvider; this.loadableProvider = loadableProvider;
} }
Site findSiteForUrl(String url) { public Site findSiteForUrl(String url) {
HttpUrl httpUrl = sanitizeUrl(url); HttpUrl httpUrl = sanitizeUrl(url);
if (httpUrl == null) { if (httpUrl == null) {
for (Site site : Sites.allSites()) { for (Site site : Sites.allSites()) {
Resolvable resolvable = site.resolvable(); SiteUrlHandler siteUrlHandler = site.resolvable();
if (resolvable.matchesName(url)) { if (siteUrlHandler.matchesName(url)) {
return site; return site;
} }
} }
@ -57,8 +57,8 @@ public class SiteResolver {
} }
for (Site site : Sites.allSites()) { for (Site site : Sites.allSites()) {
Resolvable resolvable = site.resolvable(); SiteUrlHandler siteUrlHandler = site.resolvable();
if (resolvable.respondsTo(httpUrl)) { if (siteUrlHandler.respondsTo(httpUrl)) {
return site; return site;
} }
} }
@ -66,16 +66,16 @@ public class SiteResolver {
return null; return null;
} }
SiteResolverResult resolveSiteForUrl(String url) { public SiteResolverResult resolveSiteForUrl(String url) {
List<Resolvable> resolvables = Sites.RESOLVABLES; List<SiteUrlHandler> siteUrlHandlers = Sites.URL_HANDLERS;
HttpUrl httpUrl = sanitizeUrl(url); HttpUrl httpUrl = sanitizeUrl(url);
if (httpUrl == null) { if (httpUrl == null) {
for (Resolvable resolvable : resolvables) { for (SiteUrlHandler siteUrlHandler : siteUrlHandlers) {
if (resolvable.matchesName(url)) { if (siteUrlHandler.matchesName(url)) {
return new SiteResolverResult(SiteResolverResult.Match.BUILTIN, return new SiteResolverResult(SiteResolverResult.Match.BUILTIN,
resolvable.getSiteClass(), null); siteUrlHandler.getSiteClass(), null);
} }
} }
@ -87,10 +87,10 @@ public class SiteResolver {
httpUrl = httpUrl.newBuilder().scheme("https").build(); httpUrl = httpUrl.newBuilder().scheme("https").build();
} }
for (Resolvable resolvable : resolvables) { for (SiteUrlHandler siteUrlHandler : siteUrlHandlers) {
if (resolvable.respondsTo(httpUrl)) { if (siteUrlHandler.respondsTo(httpUrl)) {
return new SiteResolverResult(SiteResolverResult.Match.BUILTIN, return new SiteResolverResult(SiteResolverResult.Match.BUILTIN,
resolvable.getSiteClass(), null); siteUrlHandler.getSiteClass(), null);
} }
} }
@ -106,7 +106,8 @@ public class SiteResolver {
for (Site site : Sites.allSites()) { for (Site site : Sites.allSites()) {
if (site.resolvable().respondsTo(httpUrl)) { if (site.resolvable().respondsTo(httpUrl)) {
Loadable resolved = site.resolveLoadable(httpUrl); Loadable resolved = loadableProvider.get(
site.resolvable().resolveLoadable(site, httpUrl));
if (resolved != null) { if (resolved != null) {
return new LoadableResult(resolved); return new LoadableResult(resolved);
@ -133,7 +134,7 @@ public class SiteResolver {
return httpUrl; return httpUrl;
} }
static class SiteResolverResult { public static class SiteResolverResult {
enum Match { enum Match {
NONE, NONE,
BUILTIN, BUILTIN,

@ -32,7 +32,7 @@ import javax.inject.Inject;
import javax.inject.Singleton; import javax.inject.Singleton;
@Singleton @Singleton
public class SiteManager { public class SiteService {
private static boolean addSiteForLegacy = false; private static boolean addSiteForLegacy = false;
/** /**
@ -48,7 +48,7 @@ public class SiteManager {
private boolean initialized = false; private boolean initialized = false;
@Inject @Inject
public SiteManager(SiteRepository siteRepository, public SiteService(SiteRepository siteRepository,
SiteResolver resolver) { SiteResolver resolver) {
this.siteRepository = siteRepository; this.siteRepository = siteRepository;
this.resolver = resolver; this.resolver = resolver;

@ -0,0 +1,38 @@
/*
* 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.site;
import android.support.annotation.Nullable;
import org.floens.chan.core.model.Post;
import org.floens.chan.core.model.orm.Loadable;
import okhttp3.HttpUrl;
public interface SiteUrlHandler {
Class<? extends Site> getSiteClass();
boolean matchesName(String value);
boolean respondsTo(HttpUrl url);
String desktopUrl(Loadable loadable, @Nullable Post post);
Loadable resolveLoadable(Site site, HttpUrl url);
}

@ -2,8 +2,8 @@ package org.floens.chan.core.site;
import android.util.SparseArray; import android.util.SparseArray;
import org.floens.chan.core.site.sites.chan4.Chan4;
import org.floens.chan.core.site.sites.vichan.ViChan; import org.floens.chan.core.site.sites.vichan.ViChan;
import org.floens.chan.core.site.sites.chan4.Chan4;
import java.util.ArrayList; import java.util.ArrayList;
import java.util.Collections; import java.util.Collections;
@ -21,11 +21,11 @@ public class Sites {
SITE_CLASSES.put(1, ViChan.class); SITE_CLASSES.put(1, ViChan.class);
} }
public static final List<Resolvable> RESOLVABLES = new ArrayList<>(); public static final List<SiteUrlHandler> URL_HANDLERS = new ArrayList<>();
static { static {
RESOLVABLES.add(Chan4.RESOLVABLE); URL_HANDLERS.add(Chan4.SITE_URL_HANDLER);
RESOLVABLES.add(ViChan.RESOLVABLE); URL_HANDLERS.add(ViChan.RESOLVABLE);
} }
private static List<Site> ALL_SITES = Collections.unmodifiableList(new ArrayList<Site>()); private static List<Site> ALL_SITES = Collections.unmodifiableList(new ArrayList<Site>());

@ -1,14 +0,0 @@
package org.floens.chan.core.site.common;
import android.util.JsonReader;
public interface ChanReader {
ChanParser getParser();
void loadThread(JsonReader reader, ChanReaderProcessingQueue queue) throws Exception;
void loadCatalog(JsonReader reader, ChanReaderProcessingQueue queue) throws Exception;
void readPostObject(JsonReader reader, ChanReaderProcessingQueue queue) throws Exception;
}

@ -0,0 +1,426 @@
/*
* 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.site.common;
import android.support.annotation.NonNull;
import android.support.annotation.Nullable;
import android.webkit.WebView;
import org.floens.chan.core.model.Post;
import org.floens.chan.core.model.json.site.SiteConfig;
import org.floens.chan.core.model.orm.Board;
import org.floens.chan.core.model.orm.Loadable;
import org.floens.chan.core.settings.json.JsonSettings;
import org.floens.chan.core.site.Site;
import org.floens.chan.core.site.SiteActions;
import org.floens.chan.core.site.SiteAuthentication;
import org.floens.chan.core.site.SiteBase;
import org.floens.chan.core.site.SiteEndpoints;
import org.floens.chan.core.site.SiteIcon;
import org.floens.chan.core.site.SiteRequestModifier;
import org.floens.chan.core.site.SiteUrlHandler;
import org.floens.chan.core.site.http.DeleteRequest;
import org.floens.chan.core.site.http.HttpCall;
import org.floens.chan.core.site.http.LoginRequest;
import org.floens.chan.core.site.http.Reply;
import org.floens.chan.core.site.http.ReplyResponse;
import org.floens.chan.core.site.parser.ChanReader;
import org.floens.chan.core.site.parser.PostParser;
import java.io.IOException;
import java.security.SecureRandom;
import java.util.Map;
import java.util.Random;
import okhttp3.HttpUrl;
import okhttp3.Request;
import okhttp3.Response;
public abstract class CommonSite extends SiteBase {
private final Random secureRandom = new SecureRandom();
private String name;
private SiteIcon icon;
private BoardsType boardsType;
private CommonConfig config;
private CommonSiteUrlHandler resolvable;
private CommonEndpoints endpoints;
private CommonActions actions;
private CommonApi api;
private CommonParser parser;
private CommonRequestModifier requestModifier;
@Override
public void initialize(int id, SiteConfig config, JsonSettings userSettings) {
super.initialize(id, config, userSettings);
setup();
if (name == null) {
throw new NullPointerException("setName not called");
}
if (icon == null) {
throw new NullPointerException("setIcon not called");
}
if (boardsType == null) {
throw new NullPointerException("setBoardsType not called");
}
if (this.config == null) {
throw new NullPointerException("setConfig not called");
}
if (resolvable == null) {
throw new NullPointerException("setResolvable not called");
}
if (endpoints == null) {
throw new NullPointerException("setEndpoints not called");
}
if (actions == null) {
throw new NullPointerException("setActions not called");
}
if (api == null) {
throw new NullPointerException("setApi not called");
}
if (parser == null) {
throw new NullPointerException("setParser not called");
}
if (requestModifier == null) {
// No-op implementation.
requestModifier = new CommonRequestModifier() {
};
}
}
public abstract void setup();
public void setName(String name) {
this.name = name;
}
public void setIcon(SiteIcon icon) {
this.icon = icon;
}
public void setBoardsType(BoardsType boardsType) {
this.boardsType = boardsType;
}
public void setConfig(CommonConfig config) {
this.config = config;
}
public void setResolvable(CommonSiteUrlHandler resolvable) {
this.resolvable = resolvable;
}
public void setEndpoints(CommonEndpoints endpoints) {
this.endpoints = endpoints;
}
public void setActions(CommonActions actions) {
this.actions = actions;
}
public void setApi(CommonApi api) {
this.api = api;
}
public void setParser(CommonParser parser) {
this.parser = parser;
}
public void setRequestModifier(CommonRequestModifier requestModifier) {
this.requestModifier = requestModifier;
}
/*
* Site implementation:
*/
@Override
public String name() {
return name;
}
@Override
public SiteIcon icon() {
return icon;
}
@Override
public BoardsType boardsType() {
return boardsType;
}
@Override
public SiteUrlHandler resolvable() {
return resolvable;
}
@Override
public boolean feature(Feature feature) {
return config.feature(feature);
}
@Override
public boolean boardFeature(BoardFeature boardFeature, Board board) {
return config.boardFeature(boardFeature, board);
}
@Override
public SiteEndpoints endpoints() {
return endpoints;
}
@Override
public SiteActions actions() {
return actions;
}
@Override
public SiteRequestModifier requestModifier() {
return requestModifier;
}
@Override
public ChanReader chanReader() {
return api;
}
public abstract class CommonConfig {
public boolean feature(Feature feature) {
return false;
}
public boolean boardFeature(BoardFeature boardFeature, Board board) {
return false;
}
}
public static abstract class CommonSiteUrlHandler implements SiteUrlHandler {
@Override
public boolean matchesName(String value) {
return false;
}
@Override
public boolean respondsTo(HttpUrl url) {
return false;
}
@Override
public String desktopUrl(Loadable loadable, @Nullable Post post) {
return null;
}
@Override
public Loadable resolveLoadable(Site site, HttpUrl url) {
return null;
}
}
public abstract class CommonEndpoints implements SiteEndpoints {
@NonNull
public SimpleHttpUrl from(String url) {
return new SimpleHttpUrl(url);
}
@Override
public HttpUrl catalog(Board board) {
return null;
}
@Override
public HttpUrl thread(Board board, Loadable loadable) {
return null;
}
@Override
public HttpUrl imageUrl(Post.Builder post, Map<String, String> arg) {
return null;
}
@Override
public HttpUrl thumbnailUrl(Post.Builder post, boolean spoiler, Map<String, String> arg) {
return null;
}
@Override
public HttpUrl icon(Post.Builder post, String icon, Map<String, String> arg) {
return null;
}
@Override
public HttpUrl boards() {
return null;
}
@Override
public HttpUrl reply(Loadable thread) {
return null;
}
@Override
public HttpUrl delete(Post post) {
return null;
}
@Override
public HttpUrl report(Post post) {
return null;
}
@Override
public HttpUrl login() {
return null;
}
}
public class SimpleHttpUrl {
@NonNull
public HttpUrl.Builder url;
public SimpleHttpUrl(String from) {
HttpUrl res = HttpUrl.parse(from);
if (res == null) {
throw new NullPointerException();
}
url = res.newBuilder();
}
public SimpleHttpUrl(@NonNull HttpUrl.Builder from) {
url = from;
}
public SimpleHttpUrl builder() {
return new SimpleHttpUrl(url.build().newBuilder());
}
public SimpleHttpUrl s(String segment) {
url.addPathSegment(segment);
return this;
}
public HttpUrl url() {
return url.build();
}
}
public abstract class CommonActions implements SiteActions {
public void setupPost(Reply reply, MultipartHttpCall call) {
}
public void handlePost(ReplyResponse response, Response httpResponse, String responseBody) {
}
@Override
public void boards(BoardsListener boardsListener) {
}
@Override
public void post(Reply reply, PostListener postListener) {
ReplyResponse replyResponse = new ReplyResponse();
reply.password = Long.toHexString(secureRandom.nextLong());
replyResponse.password = reply.password;
MultipartHttpCall call = new MultipartHttpCall(CommonSite.this) {
@Override
public void process(Response response, String result) throws IOException {
handlePost(replyResponse, response, result);
}
};
setupPost(reply, call);
httpCallManager.makeHttpCall(call, new HttpCall.HttpCallback<HttpCall>() {
@Override
public void onHttpSuccess(HttpCall httpCall) {
postListener.onPostComplete(httpCall, replyResponse);
}
@Override
public void onHttpFail(HttpCall httpCall, Exception e) {
postListener.onPostError(httpCall);
}
});
}
@Override
public boolean postRequiresAuthentication() {
return false;
}
@Override
public SiteAuthentication postAuthenticate() {
return SiteAuthentication.fromNone();
}
@Override
public void delete(DeleteRequest deleteRequest, DeleteListener deleteListener) {
}
@Override
public void login(LoginRequest loginRequest, LoginListener loginListener) {
}
@Override
public void logout() {
}
@Override
public boolean isLoggedIn() {
return false;
}
@Override
public LoginRequest getLoginDetails() {
return null;
}
}
public abstract class CommonApi implements ChanReader {
@Override
public PostParser getParser() {
return parser;
}
}
public abstract class CommonParser implements PostParser {
}
public abstract class CommonRequestModifier implements SiteRequestModifier {
@Override
public void modifyHttpCall(HttpCall httpCall, Request.Builder requestBuilder) {
}
@Override
public void modifyWebView(WebView webView) {
}
}
}

@ -1,238 +0,0 @@
/*
* 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.site.common;
import android.graphics.Typeface;
import android.text.SpannableString;
import android.text.TextUtils;
import android.text.style.StrikethroughSpan;
import android.text.style.StyleSpan;
import android.text.style.TypefaceSpan;
import android.text.style.UnderlineSpan;
import org.floens.chan.core.model.Post;
import org.floens.chan.core.model.PostLinkable;
import org.floens.chan.ui.span.AbsoluteSizeSpanHashed;
import org.floens.chan.ui.span.ForegroundColorSpanHashed;
import org.floens.chan.ui.theme.Theme;
import org.jsoup.nodes.Element;
import org.jsoup.select.Elements;
import java.util.ArrayList;
import java.util.List;
import java.util.Set;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
import static org.floens.chan.utils.AndroidUtils.sp;
public class DefaultFutabaChanParserHandler implements FutabaChanParserHandler {
private static final Pattern COLOR_PATTERN = Pattern.compile("color:#([0-9a-fA-F]*)");
@Override
public CharSequence handleParagraph(FutabaChanParser parser, Theme theme, Post.Builder post, CharSequence text, Element element) {
return text;
}
@Override
public CharSequence handleSpan(FutabaChanParser parser, Theme theme, Post.Builder post, Element span) {
SpannableString quote;
Set<String> classes = span.classNames();
if (classes.contains("deadlink")) {
quote = new SpannableString(span.text());
quote.setSpan(new ForegroundColorSpanHashed(theme.quoteColor), 0, quote.length(), 0);
quote.setSpan(new StrikethroughSpan(), 0, quote.length(), 0);
} else if (classes.contains("fortune")) {
// html looks like <span class="fortune" style="color:#0893e1"><br><br><b>Your fortune:</b>
// manually add these <br>
quote = new SpannableString("\n\n" + span.text());
String style = span.attr("style");
if (!TextUtils.isEmpty(style)) {
style = style.replace(" ", "");
// private static final Pattern COLOR_PATTERN = Pattern.compile("color:#([0-9a-fA-F]*)");
Matcher matcher = COLOR_PATTERN.matcher(style);
int hexColor = 0xff0000;
if (matcher.find()) {
String group = matcher.group(1);
if (!TextUtils.isEmpty(group)) {
try {
hexColor = Integer.parseInt(group, 16);
} catch (NumberFormatException ignored) {
}
}
}
if (hexColor >= 0 && hexColor <= 0xffffff) {
quote.setSpan(new ForegroundColorSpanHashed(0xff000000 + hexColor), 0, quote.length(), 0);
quote.setSpan(new StyleSpan(Typeface.BOLD), 0, quote.length(), 0);
}
}
} else if (classes.contains("abbr")) {
return null;
} else {
quote = new SpannableString(span.text());
quote.setSpan(new ForegroundColorSpanHashed(theme.inlineQuoteColor), 0, quote.length(), 0);
ChanParserHelper.detectLinks(theme, post, span.text(), quote);
}
return quote;
}
@Override
public CharSequence handleTable(FutabaChanParser parser, Theme theme, Post.Builder post, Element table) {
List<CharSequence> parts = new ArrayList<>();
Elements tableRows = table.getElementsByTag("tr");
for (int i = 0; i < tableRows.size(); i++) {
Element tableRow = tableRows.get(i);
if (tableRow.text().length() > 0) {
Elements tableDatas = tableRow.getElementsByTag("td");
for (int j = 0; j < tableDatas.size(); j++) {
Element tableData = tableDatas.get(j);
SpannableString tableDataPart = new SpannableString(tableData.text());
if (tableData.getElementsByTag("b").size() > 0) {
tableDataPart.setSpan(new StyleSpan(Typeface.BOLD), 0, tableDataPart.length(), 0);
tableDataPart.setSpan(new UnderlineSpan(), 0, tableDataPart.length(), 0);
}
parts.add(tableDataPart);
if (j < tableDatas.size() - 1) {
parts.add(": ");
}
}
if (i < tableRows.size() - 1) {
parts.add("\n");
}
}
}
SpannableString tableTotal = new SpannableString(TextUtils.concat(parts.toArray(new CharSequence[parts.size()])));
tableTotal.setSpan(new ForegroundColorSpanHashed(theme.inlineQuoteColor), 0, tableTotal.length(), 0);
tableTotal.setSpan(new AbsoluteSizeSpanHashed(sp(12f)), 0, tableTotal.length(), 0);
return tableTotal;
}
@Override
public CharSequence handleStrong(FutabaChanParser parser, Theme theme, Post.Builder post, Element strong) {
SpannableString red = new SpannableString(strong.text());
red.setSpan(new ForegroundColorSpanHashed(theme.quoteColor), 0, red.length(), 0);
red.setSpan(new StyleSpan(Typeface.BOLD), 0, red.length(), 0);
return red;
}
@Override
public CharSequence handlePre(FutabaChanParser parser, Theme theme, Post.Builder post, Element pre) {
Set<String> classes = pre.classNames();
if (classes.contains("prettyprint")) {
String text = ChanParserHelper.getNodeTextPreservingLineBreaks(pre);
SpannableString monospace = new SpannableString(text);
monospace.setSpan(new TypefaceSpan("monospace"), 0, monospace.length(), 0);
monospace.setSpan(new AbsoluteSizeSpanHashed(sp(12f)), 0, monospace.length(), 0);
return monospace;
} else {
return pre.text();
}
}
@Override
public CharSequence handleStrike(FutabaChanParser parser, Theme theme, Post.Builder post, Element strike) {
SpannableString link = new SpannableString(strike.text());
PostLinkable pl = new PostLinkable(theme, strike.text(), strike.text(), PostLinkable.Type.SPOILER);
link.setSpan(pl, 0, link.length(), 0);
post.addLinkable(pl);
return link;
}
@Override
public Link handleAnchor(FutabaChanParser parser, Theme theme, Post.Builder post, Element anchor) {
String href = anchor.attr("href");
Set<String> classes = anchor.classNames();
PostLinkable.Type t = null;
String key = null;
Object value = null;
if (classes.contains("quotelink")) {
if (href.contains("/thread/")) {
// link to another thread
PostLinkable.ThreadLink threadLink = null;
String[] slashSplit = href.split("/");
if (slashSplit.length == 4) {
String board = slashSplit[1];
String nums = slashSplit[3];
String[] numsSplitted = nums.split("#p");
if (numsSplitted.length == 2) {
try {
int tId = Integer.parseInt(numsSplitted[0]);
int pId = Integer.parseInt(numsSplitted[1]);
threadLink = new PostLinkable.ThreadLink(board, tId, pId);
} catch (NumberFormatException ignored) {
}
}
}
if (threadLink != null) {
t = PostLinkable.Type.THREAD;
key = anchor.text();
value = threadLink;
}
} else {
// normal quote
int id = -1;
String[] splitted = href.split("#p");
if (splitted.length == 2) {
try {
id = Integer.parseInt(splitted[1]);
} catch (NumberFormatException ignored) {
}
}
if (id >= 0) {
t = PostLinkable.Type.QUOTE;
key = anchor.text();
value = id;
}
}
} else {
// normal link
t = PostLinkable.Type.LINK;
key = anchor.text();
value = href;
}
if (t != null && key != null && value != null) {
Link link = new Link();
link.type = t;
link.key = key;
link.value = value;
return link;
} else {
return null;
}
}
}

@ -18,13 +18,16 @@
package org.floens.chan.core.site.common; package org.floens.chan.core.site.common;
import android.support.annotation.AnyThread;
import android.text.SpannableString; import android.text.SpannableString;
import android.text.TextUtils; import android.text.TextUtils;
import android.text.style.BackgroundColorSpan; import android.text.style.BackgroundColorSpan;
import org.floens.chan.core.model.Post; import org.floens.chan.core.model.Post;
import org.floens.chan.core.model.PostLinkable;
import org.floens.chan.core.settings.ChanSettings; import org.floens.chan.core.settings.ChanSettings;
import org.floens.chan.core.site.parser.CommentParser;
import org.floens.chan.core.site.parser.CommentParserHelper;
import org.floens.chan.core.site.parser.PostParser;
import org.floens.chan.ui.span.AbsoluteSizeSpanHashed; import org.floens.chan.ui.span.AbsoluteSizeSpanHashed;
import org.floens.chan.ui.span.ForegroundColorSpanHashed; import org.floens.chan.ui.span.ForegroundColorSpanHashed;
import org.floens.chan.ui.theme.Theme; import org.floens.chan.ui.theme.Theme;
@ -42,16 +45,14 @@ import java.util.List;
import static org.floens.chan.utils.AndroidUtils.sp; import static org.floens.chan.utils.AndroidUtils.sp;
public class FutabaChanParser implements ChanParser { @AnyThread
private static final String TAG = "FutabaChanParser"; public class DefaultPostParser implements PostParser {
private static final String SAVED_REPLY_SUFFIX = " (You)"; private static final String TAG = "DefaultPostParser";
private static final String OP_REPLY_SUFFIX = " (OP)";
public static final String EXTERN_THREAD_LINK_SUFFIX = " \u2192"; // arrow to the right
private FutabaChanParserHandler handler; private CommentParser commentParser;
public FutabaChanParser(FutabaChanParserHandler handler) { public DefaultPostParser(CommentParser commentParser) {
this.handler = handler; this.commentParser = commentParser;
} }
@Override @Override
@ -60,16 +61,12 @@ public class FutabaChanParser implements ChanParser {
theme = ThemeHelper.getInstance().getTheme(); theme = ThemeHelper.getInstance().getTheme();
} }
try { if (!TextUtils.isEmpty(builder.name)) {
if (!TextUtils.isEmpty(builder.name)) { builder.name = Parser.unescapeEntities(builder.name, false);
builder.name = Parser.unescapeEntities(builder.name, false); }
}
if (!TextUtils.isEmpty(builder.subject)) { if (!TextUtils.isEmpty(builder.subject)) {
builder.subject = Parser.unescapeEntities(builder.subject, false); builder.subject = Parser.unescapeEntities(builder.subject, false);
}
} catch (Exception e) {
e.printStackTrace();
} }
parseSpans(theme, builder); parseSpans(theme, builder);
@ -209,104 +206,44 @@ public class FutabaChanParser implements ChanParser {
String text = ((TextNode) node).text(); String text = ((TextNode) node).text();
SpannableString spannable = new SpannableString(text); SpannableString spannable = new SpannableString(text);
ChanParserHelper.detectLinks(theme, post, text, spannable); CommentParserHelper.detectLinks(theme, post, text, spannable);
return spannable; return spannable;
} else { } else if (node instanceof Element) {
switch (node.nodeName()) { String nodeName = node.nodeName();
case "p": {
// Recursively call parseNode with the nodes of the paragraph.
List<Node> innerNodes = node.childNodes();
List<CharSequence> texts = new ArrayList<>(innerNodes.size() + 1);
for (Node innerNode : innerNodes) {
CharSequence nodeParsed = parseNode(theme, post, callback, innerNode);
if (nodeParsed != null) {
texts.add(nodeParsed);
}
}
if (node.nextSibling() != null) {
texts.add("\n");
}
CharSequence res = TextUtils.concat(texts.toArray(new CharSequence[texts.size()]));
return handler.handleParagraph(this, theme, post, res, (Element) node);
}
case "br": {
return "\n";
}
case "span": {
return handler.handleSpan(this, theme, post, (Element) node);
}
case "table": {
return handler.handleTable(this, theme, post, (Element) node);
}
case "strong": {
return handler.handleStrong(this, theme, post, (Element) node);
}
case "a": {
CharSequence anchor = parseAnchor(theme, post, callback, (Element) node);
if (anchor != null) {
return anchor;
} else {
return ((Element) node).text();
}
}
case "s": {
return handler.handleStrike(this, theme, post, (Element) node);
}
case "pre": {
return handler.handlePre(this, theme, post, (Element) node);
}
default: {
// Unknown tag, add the inner part
if (node instanceof Element) {
return ((Element) node).text();
} else {
return null;
}
}
}
}
}
private CharSequence parseAnchor(Theme theme, Post.Builder post, Callback callback, // Recursively call parseNode with the nodes of the paragraph.
Element anchor) { List<Node> innerNodes = node.childNodes();
FutabaChanParserHandler.Link handlerLink = List<CharSequence> texts = new ArrayList<>(innerNodes.size() + 1);
handler.handleAnchor(this, theme, post, anchor);
if (handlerLink != null) { for (Node innerNode : innerNodes) {
if (handlerLink.type == PostLinkable.Type.THREAD) { CharSequence nodeParsed = parseNode(theme, post, callback, innerNode);
handlerLink.key += EXTERN_THREAD_LINK_SUFFIX; if (nodeParsed != null) {
} texts.add(nodeParsed);
if (handlerLink.type == PostLinkable.Type.QUOTE) {
int postNo = (int) handlerLink.value;
post.addReplyTo(postNo);
// Append (OP) when its a reply to OP
if (postNo == post.opId) {
handlerLink.key += OP_REPLY_SUFFIX;
}
// Append (You) when it's a reply to an saved reply
if (callback.isSaved(postNo)) {
handlerLink.key += SAVED_REPLY_SUFFIX;
} }
} }
SpannableString link = new SpannableString(handlerLink.key); // if (node.nextSibling() != null) {
PostLinkable pl = new PostLinkable(theme, handlerLink.key, handlerLink.value, handlerLink.type); // texts.add("\n");
link.setSpan(pl, 0, link.length(), 0); // }
post.addLinkable(pl);
CharSequence allInnerText = TextUtils.concat(
return link; texts.toArray(new CharSequence[texts.size()]));
CharSequence result = commentParser.handleTag(
callback,
theme,
post,
nodeName,
allInnerText,
(Element) node);
if (result != null) {
return result;
} else {
return allInnerText;
}
} else { } else {
return null; return ""; // ?
} }
} }
} }

@ -1,44 +0,0 @@
/*
* 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.site.common;
import org.floens.chan.core.model.Post;
import org.floens.chan.core.model.PostLinkable;
import org.floens.chan.ui.theme.Theme;
import org.jsoup.nodes.Element;
public interface FutabaChanParserHandler {
CharSequence handleParagraph(FutabaChanParser parser, Theme theme, Post.Builder post, CharSequence text, Element element);
CharSequence handleSpan(FutabaChanParser parser, Theme theme, Post.Builder post, Element span);
CharSequence handleTable(FutabaChanParser parser, Theme theme, Post.Builder post, Element table);
CharSequence handleStrong(FutabaChanParser parser, Theme theme, Post.Builder post, Element strong);
CharSequence handlePre(FutabaChanParser parser, Theme theme, Post.Builder post, Element pre);
CharSequence handleStrike(FutabaChanParser parser, Theme theme, Post.Builder post, Element strike);
Link handleAnchor(FutabaChanParser parser, Theme theme, Post.Builder post, Element anchor);
class Link {
public PostLinkable.Type type;
public String key;
public Object value;
}
}

@ -7,7 +7,10 @@ import org.floens.chan.core.model.Post;
import org.floens.chan.core.model.PostHttpIcon; import org.floens.chan.core.model.PostHttpIcon;
import org.floens.chan.core.model.PostImage; import org.floens.chan.core.model.PostImage;
import org.floens.chan.core.site.SiteEndpoints; import org.floens.chan.core.site.SiteEndpoints;
import org.jsoup.parser.Parser; import org.floens.chan.core.site.parser.ChanReader;
import org.floens.chan.core.site.parser.ChanReaderProcessingQueue;
import org.floens.chan.core.site.parser.CommentParser;
import org.floens.chan.core.site.parser.PostParser;
import java.io.IOException; import java.io.IOException;
import java.util.ArrayList; import java.util.ArrayList;
@ -20,19 +23,19 @@ import okhttp3.HttpUrl;
import static org.floens.chan.core.site.SiteEndpoints.makeArgument; import static org.floens.chan.core.site.SiteEndpoints.makeArgument;
public class FutabaChanReader implements ChanReader { public class FutabaChanReader implements ChanReader {
private final ChanParser chanParser; private final PostParser postParser;
public FutabaChanReader() { public FutabaChanReader() {
this.chanParser = new FutabaChanParser(new DefaultFutabaChanParserHandler()); this.postParser = new DefaultPostParser(new CommentParser());
} }
public FutabaChanReader(ChanParser chanParser) { public FutabaChanReader(PostParser postParser) {
this.chanParser = chanParser; this.postParser = postParser;
} }
@Override @Override
public ChanParser getParser() { public PostParser getParser() {
return chanParser; return postParser;
} }
@Override @Override
@ -226,7 +229,7 @@ public class FutabaChanReader implements ChanReader {
.thumbnailUrl(endpoints.thumbnailUrl(builder, false, args)) .thumbnailUrl(endpoints.thumbnailUrl(builder, false, args))
.spoilerThumbnailUrl(endpoints.thumbnailUrl(builder, true, args)) .spoilerThumbnailUrl(endpoints.thumbnailUrl(builder, true, args))
.imageUrl(endpoints.imageUrl(builder, args)) .imageUrl(endpoints.imageUrl(builder, args))
.filename(Parser.unescapeEntities(fileName, false)) .filename(org.jsoup.parser.Parser.unescapeEntities(fileName, false))
.extension(fileExt) .extension(fileExt)
.imageWidth(fileWidth) .imageWidth(fileWidth)
.imageHeight(fileHeight) .imageHeight(fileHeight)
@ -331,7 +334,7 @@ public class FutabaChanReader implements ChanReader {
.thumbnailUrl(endpoints.thumbnailUrl(builder, false, args)) .thumbnailUrl(endpoints.thumbnailUrl(builder, false, args))
.spoilerThumbnailUrl(endpoints.thumbnailUrl(builder, true, args)) .spoilerThumbnailUrl(endpoints.thumbnailUrl(builder, true, args))
.imageUrl(endpoints.imageUrl(builder, args)) .imageUrl(endpoints.imageUrl(builder, args))
.filename(Parser.unescapeEntities(fileName, false)) .filename(org.jsoup.parser.Parser.unescapeEntities(fileName, false))
.extension(fileExt) .extension(fileExt)
.imageWidth(fileWidth) .imageWidth(fileWidth)
.imageHeight(fileHeight) .imageHeight(fileHeight)

@ -0,0 +1,66 @@
/*
* 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.site.common;
import org.floens.chan.core.site.Site;
import org.floens.chan.core.site.http.HttpCall;
import java.io.File;
import okhttp3.HttpUrl;
import okhttp3.MediaType;
import okhttp3.MultipartBody;
import okhttp3.Request;
import okhttp3.RequestBody;
public abstract class MultipartHttpCall extends HttpCall {
private final MultipartBody.Builder formBuilder;
private HttpUrl url;
public MultipartHttpCall(Site site) {
super(site);
formBuilder = new MultipartBody.Builder();
formBuilder.setType(MultipartBody.FORM);
}
public MultipartHttpCall url(HttpUrl url) {
this.url = url;
return this;
}
public MultipartHttpCall parameter(String name, String value) {
formBuilder.addFormDataPart(name, value);
return this;
}
public MultipartHttpCall fileParameter(String name, String filename, File file) {
formBuilder.addFormDataPart(name, filename, RequestBody.create(
MediaType.parse("application/octet-stream"), file
));
return this;
}
@Override
public void setup(Request.Builder requestBuilder) {
requestBuilder.url(url);
requestBuilder.addHeader("Referer", url.toString());
requestBuilder.post(formBuilder.build());
}
}

@ -17,10 +17,11 @@
*/ */
package org.floens.chan.core.site.http; package org.floens.chan.core.site.http;
import android.os.Handler;
import android.os.Looper;
import org.floens.chan.core.site.Site; import org.floens.chan.core.site.Site;
import org.floens.chan.utils.AndroidUtils;
import org.floens.chan.utils.IOUtils; import org.floens.chan.utils.IOUtils;
import org.floens.chan.utils.Logger;
import java.io.IOException; import java.io.IOException;
@ -42,7 +43,7 @@ public abstract class HttpCall implements Callback {
protected Site site; protected Site site;
private boolean successful = false; private Handler handler = new Handler(Looper.getMainLooper());
private HttpCallback callback; private HttpCallback callback;
private Exception exception; private Exception exception;
@ -61,18 +62,16 @@ public abstract class HttpCall implements Callback {
if (body != null) { if (body != null) {
String responseString = body.string(); String responseString = body.string();
process(response, responseString); process(response, responseString);
successful = true;
} else { } else {
throw new IOException("HTTP " + response.code()); exception = new IOException("No body. HTTP " + response.code());
} }
} catch (Exception e) { } catch (Exception e) {
exception = e; exception = new IOException("Error processing response", e);
Logger.e(TAG, "IOException processing response", e);
} finally { } finally {
IOUtils.closeQuietly(body); IOUtils.closeQuietly(body);
} }
if (successful) { if (exception != null) {
callSuccess(); callSuccess();
} else { } else {
callFail(exception); callFail(exception);
@ -88,28 +87,14 @@ public abstract class HttpCall implements Callback {
return exception; return exception;
} }
public boolean isSuccessful() { @SuppressWarnings("unchecked")
return successful;
}
private void callSuccess() { private void callSuccess() {
AndroidUtils.runOnUiThread(new Runnable() { handler.post(() -> callback.onHttpSuccess(HttpCall.this));
@SuppressWarnings("unchecked")
@Override
public void run() {
callback.onHttpSuccess(HttpCall.this);
}
});
} }
@SuppressWarnings("unchecked")
private void callFail(final Exception e) { private void callFail(final Exception e) {
AndroidUtils.runOnUiThread(new Runnable() { handler.post(() -> callback.onHttpFail(HttpCall.this, e));
@SuppressWarnings("unchecked")
@Override
public void run() {
callback.onHttpFail(HttpCall.this, e);
}
});
} }
public void setCallback(HttpCallback<? extends HttpCall> callback) { public void setCallback(HttpCallback<? extends HttpCall> callback) {

@ -45,4 +45,5 @@ public class Reply {
public String comment = ""; public String comment = "";
public int selection; public int selection;
public boolean spoilerImage = false; public boolean spoilerImage = false;
public String password = "";
} }

@ -17,10 +17,12 @@
*/ */
package org.floens.chan.core.site.http; package org.floens.chan.core.site.http;
import org.floens.chan.core.site.Site; import org.floens.chan.core.site.SiteActions;
/** /**
* Generic response for {@link Site#post(Reply, Site.PostListener)} that the reply layout uses. * Generic response for
* {@link org.floens.chan.core.site.SiteActions#post(Reply, SiteActions.PostListener)} that the
* reply layout uses.
*/ */
public class ReplyResponse { public class ReplyResponse {
/** /**

@ -22,12 +22,12 @@ import com.android.volley.Response;
import org.floens.chan.core.model.Post; import org.floens.chan.core.model.Post;
import org.floens.chan.core.model.orm.Loadable; import org.floens.chan.core.model.orm.Loadable;
import org.floens.chan.core.site.common.ChanReader; import org.floens.chan.core.site.parser.ChanReader;
import java.util.List; import java.util.List;
/** /**
* A request from ChanLoader to load something. * A request from ChanThreadLoader to load something.
*/ */
public class ChanLoaderRequestParams { public class ChanLoaderRequestParams {
/** /**

@ -27,8 +27,8 @@ 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.Post; import org.floens.chan.core.model.Post;
import org.floens.chan.core.model.orm.Loadable; import org.floens.chan.core.model.orm.Loadable;
import org.floens.chan.core.site.common.ChanReader; import org.floens.chan.core.site.parser.ChanReader;
import org.floens.chan.core.site.common.ChanReaderRequest; import org.floens.chan.core.site.parser.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;
@ -46,14 +46,14 @@ import javax.inject.Inject;
import static org.floens.chan.Chan.inject; import static org.floens.chan.Chan.inject;
/** /**
* A ChanLoader is the loader for Loadables. * A ChanThreadLoader is the loader for Loadables.
* <p>Obtain ChanLoaders with {@link org.floens.chan.core.pool.ChanLoaderFactory}. * <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 * <p>ChanLoaders can load boards and threads, and return {@link ChanThread} objects on success, through
* {@link ChanLoaderCallback}. * {@link ChanLoaderCallback}.
* <p>For threads timers can be started with {@link #setTimer()} to do a request later. * <p>For threads timers can be started with {@link #setTimer()} to do a request later.
*/ */
public class ChanLoader implements Response.ErrorListener, Response.Listener<ChanLoaderResponse> { public class ChanThreadLoader implements Response.ErrorListener, Response.Listener<ChanLoaderResponse> {
private static final String TAG = "ChanLoader"; private static final String TAG = "ChanThreadLoader";
private static final ScheduledExecutorService executor = Executors.newSingleThreadScheduledExecutor(); private static final ScheduledExecutorService executor = Executors.newSingleThreadScheduledExecutor();
private static final int[] WATCH_TIMEOUTS = {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};
@ -75,7 +75,7 @@ public class ChanLoader implements Response.ErrorListener, Response.Listener<Cha
/** /**
* <b>Do not call this constructor yourself, obtain ChanLoaders through {@link org.floens.chan.core.pool.ChanLoaderFactory}</b> * <b>Do not call this constructor yourself, obtain ChanLoaders through {@link org.floens.chan.core.pool.ChanLoaderFactory}</b>
*/ */
public ChanLoader(Loadable loadable) { public ChanThreadLoader(Loadable loadable) {
this.loadable = loadable; this.loadable = loadable;
inject(this); inject(this);

@ -0,0 +1,31 @@
/*
* 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.site.parser;
import android.util.JsonReader;
public interface ChanReader {
PostParser getParser();
void loadThread(JsonReader reader, ChanReaderProcessingQueue queue) throws Exception;
void loadCatalog(JsonReader reader, ChanReaderProcessingQueue queue) throws Exception;
void readPostObject(JsonReader reader, ChanReaderProcessingQueue queue) throws Exception;
}

@ -1,4 +1,21 @@
package org.floens.chan.core.site.common; /*
* 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.site.parser;
import android.annotation.SuppressLint; import android.annotation.SuppressLint;
@ -11,7 +28,7 @@ import java.util.HashMap;
import java.util.List; import java.util.List;
import java.util.Map; import java.util.Map;
class ChanReaderProcessingQueue { public class ChanReaderProcessingQueue {
@SuppressLint("UseSparseArrays") @SuppressLint("UseSparseArrays")
private Map<Integer, Post> cachedByNo = new HashMap<>(); private Map<Integer, Post> cachedByNo = new HashMap<>();
private Loadable loadable; private Loadable loadable;

@ -15,7 +15,7 @@
* 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.site.common; package org.floens.chan.core.site.parser;
import android.util.JsonReader; import android.util.JsonReader;
@ -31,9 +31,12 @@ import org.floens.chan.core.site.loader.ChanLoaderResponse;
import org.floens.chan.utils.Time; import org.floens.chan.utils.Time;
import java.util.ArrayList; import java.util.ArrayList;
import java.util.Collections;
import java.util.HashMap; import java.util.HashMap;
import java.util.HashSet;
import java.util.List; import java.util.List;
import java.util.Map; import java.util.Map;
import java.util.Set;
import java.util.concurrent.Callable; import java.util.concurrent.Callable;
import java.util.concurrent.ExecutionException; import java.util.concurrent.ExecutionException;
import java.util.concurrent.ExecutorService; import java.util.concurrent.ExecutorService;
@ -165,10 +168,29 @@ public class ChanReaderRequest extends JsonReaderRequest<ChanLoaderResponse> {
List<Post.Builder> toParse = queue.getToParse(); List<Post.Builder> toParse = queue.getToParse();
// A list of all ids in the thread. Used for checking if a quote if for the current
// thread or externally.
Set<Integer> internalIds = new HashSet<>();
// All ids of cached posts.
for (int i = 0; i < cached.size(); i++) {
internalIds.add(cached.get(i).no);
}
// And ids for posts to parse, from the builder.
for (int i = 0; i < toParse.size(); i++) {
internalIds.add(toParse.get(i).id);
}
// Do not modify internalIds after this point.
internalIds = Collections.unmodifiableSet(internalIds);
List<Callable<Post>> tasks = new ArrayList<>(toParse.size()); List<Callable<Post>> tasks = new ArrayList<>(toParse.size());
for (int i = 0; i < toParse.size(); i++) { for (int i = 0; i < toParse.size(); i++) {
Post.Builder post = toParse.get(i); Post.Builder post = toParse.get(i);
tasks.add(new PostParseCallable(filterEngine, filters, databaseSavedReplyManager, post, reader)); tasks.add(new PostParseCallable(filterEngine,
filters,
databaseSavedReplyManager,
post,
reader,
internalIds));
} }
if (!tasks.isEmpty()) { if (!tasks.isEmpty()) {

@ -0,0 +1,307 @@
/*
* 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.site.parser;
import android.graphics.Typeface;
import android.support.annotation.AnyThread;
import android.text.SpannableString;
import android.text.TextUtils;
import android.text.style.StrikethroughSpan;
import android.text.style.StyleSpan;
import android.text.style.TypefaceSpan;
import android.text.style.UnderlineSpan;
import org.floens.chan.core.model.Post;
import org.floens.chan.core.model.PostLinkable;
import org.floens.chan.ui.span.AbsoluteSizeSpanHashed;
import org.floens.chan.ui.span.ForegroundColorSpanHashed;
import org.floens.chan.ui.theme.Theme;
import org.jsoup.nodes.Element;
import org.jsoup.select.Elements;
import java.util.ArrayList;
import java.util.List;
import java.util.Set;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
import static org.floens.chan.utils.AndroidUtils.sp;
@AnyThread
public class CommentParser {
private static final String SAVED_REPLY_SUFFIX = " (You)";
private static final String OP_REPLY_SUFFIX = " (OP)";
private static final String EXTERN_THREAD_LINK_SUFFIX = " \u2192"; // arrow to the right
private Pattern fullQuotePattern = Pattern.compile("/(\\w+)/\\w+/(\\d+)#p(\\d+)");
private Pattern quotePattern = Pattern.compile(".*#p(\\d+)");
private Pattern colorPattern = Pattern.compile("color:#([0-9a-fA-F]*)");
public void setQuotePattern(Pattern quotePattern) {
this.quotePattern = quotePattern;
}
public void setFullQuotePattern(Pattern fullQuotePattern) {
this.fullQuotePattern = fullQuotePattern;
}
public CharSequence handleTag(PostParser.Callback callback,
Theme theme,
Post.Builder post,
String tag,
CharSequence text,
Element element) {
switch (tag) {
case "br":
return "\n";
case "span":
return handleSpan(theme, post, text, element);
case "p":
return appendBreakIfNotLastSibling(
handleParagraph(theme, post, text, element), element);
case "table":
return handleTable(theme, post, text, element);
case "strong":
return handleStrong(theme, post, text, element);
case "a":
return handleAnchor(theme, post, text, element, callback);
case "s":
return handleStrike(theme, post, text, element);
case "pre":
return handlePre(theme, post, text, element);
default:
// Unknown tag, return the text;
return text;
}
}
private CharSequence appendBreakIfNotLastSibling(CharSequence text, Element element) {
if (element.nextSibling() != null) {
return TextUtils.concat(text, "\n");
} else {
return text;
}
}
private CharSequence handleAnchor(Theme theme,
Post.Builder post,
CharSequence text,
Element anchor,
PostParser.Callback callback) {
CommentParser.Link handlerLink = matchAnchor(post, text, anchor, callback);
if (handlerLink != null) {
if (handlerLink.type == PostLinkable.Type.THREAD) {
handlerLink.key = TextUtils.concat(handlerLink.key, EXTERN_THREAD_LINK_SUFFIX);
}
if (handlerLink.type == PostLinkable.Type.QUOTE) {
int postNo = (int) handlerLink.value;
post.addReplyTo(postNo);
// Append (OP) when it's a reply to OP
if (postNo == post.opId) {
handlerLink.key = TextUtils.concat(handlerLink.key, OP_REPLY_SUFFIX);
}
// Append (You) when it's a reply to an saved reply
if (callback.isSaved(postNo)) {
handlerLink.key = TextUtils.concat(handlerLink.key, SAVED_REPLY_SUFFIX);
}
}
SpannableString res = new SpannableString(handlerLink.key);
PostLinkable pl = new PostLinkable(theme, handlerLink.key, handlerLink.value, handlerLink.type);
res.setSpan(pl, 0, res.length(), 0);
post.addLinkable(pl);
return res;
} else {
return null;
}
}
public CharSequence handleSpan(Theme theme, Post.Builder post, CharSequence text, Element span) {
SpannableString quote;
Set<String> classes = span.classNames();
if (classes.contains("deadlink")) {
quote = new SpannableString(span.text());
quote.setSpan(new ForegroundColorSpanHashed(theme.quoteColor), 0, quote.length(), 0);
quote.setSpan(new StrikethroughSpan(), 0, quote.length(), 0);
} else if (classes.contains("fortune")) {
// html looks like <span class="fortune" style="color:#0893e1"><br><br><b>Your fortune:</b>
// manually add these <br>
quote = new SpannableString("\n\n" + span.text());
String style = span.attr("style");
if (!TextUtils.isEmpty(style)) {
style = style.replace(" ", "");
Matcher matcher = colorPattern.matcher(style);
int hexColor = 0xff0000;
if (matcher.find()) {
String group = matcher.group(1);
if (!TextUtils.isEmpty(group)) {
try {
hexColor = Integer.parseInt(group, 16);
} catch (NumberFormatException ignored) {
}
}
}
if (hexColor >= 0 && hexColor <= 0xffffff) {
quote.setSpan(new ForegroundColorSpanHashed(0xff000000 + hexColor), 0, quote.length(), 0);
quote.setSpan(new StyleSpan(Typeface.BOLD), 0, quote.length(), 0);
}
}
} else if (classes.contains("spoiler")) {
PostLinkable pl = new PostLinkable(theme, span.text(), span.text(), PostLinkable.Type.SPOILER);
post.addLinkable(pl);
return span(span.text(), pl);
} else if (classes.contains("abbr")) {
return null;
} else {
quote = new SpannableString(span.text());
quote.setSpan(new ForegroundColorSpanHashed(theme.inlineQuoteColor), 0, quote.length(), 0);
CommentParserHelper.detectLinks(theme, post, span.text(), quote);
}
return quote;
}
public CharSequence handleParagraph(Theme theme, Post.Builder post, CharSequence text, Element span) {
return text;
}
public CharSequence handleTable(Theme theme, Post.Builder post, CharSequence text, Element table) {
List<CharSequence> parts = new ArrayList<>();
Elements tableRows = table.getElementsByTag("tr");
for (int i = 0; i < tableRows.size(); i++) {
Element tableRow = tableRows.get(i);
if (tableRow.text().length() > 0) {
Elements tableDatas = tableRow.getElementsByTag("td");
for (int j = 0; j < tableDatas.size(); j++) {
Element tableData = tableDatas.get(j);
SpannableString tableDataPart = new SpannableString(tableData.text());
if (tableData.getElementsByTag("b").size() > 0) {
tableDataPart.setSpan(new StyleSpan(Typeface.BOLD), 0, tableDataPart.length(), 0);
tableDataPart.setSpan(new UnderlineSpan(), 0, tableDataPart.length(), 0);
}
parts.add(tableDataPart);
if (j < tableDatas.size() - 1) parts.add(": ");
}
if (i < tableRows.size() - 1) parts.add("\n");
}
}
// Overrides the text (possibly) parsed by child nodes.
return span(TextUtils.concat(parts.toArray(new CharSequence[parts.size()])),
new ForegroundColorSpanHashed(theme.inlineQuoteColor),
new AbsoluteSizeSpanHashed(sp(12f)));
}
public CharSequence handleStrong(Theme theme, Post.Builder post, CharSequence text, Element strong) {
return span(text,
new ForegroundColorSpanHashed(theme.quoteColor),
new StyleSpan(Typeface.BOLD));
}
public CharSequence handlePre(Theme theme, Post.Builder post, CharSequence text, Element pre) {
Set<String> classes = pre.classNames();
if (classes.contains("prettyprint")) {
// String linebreakText = CommentParserHelper.getNodeTextPreservingLineBreaks(pre);
return span(text,
new TypefaceSpan("monospace"),
new AbsoluteSizeSpanHashed(sp(12f)));
} else {
return pre.text();
}
}
public CharSequence handleStrike(Theme theme, Post.Builder post, CharSequence text, Element strike) {
PostLinkable pl = new PostLinkable(theme, text.toString(), text, PostLinkable.Type.SPOILER);
post.addLinkable(pl);
return span(text, pl);
}
public Link matchAnchor(Post.Builder post, CharSequence text, Element anchor, PostParser.Callback callback) {
String href = anchor.attr("href");
PostLinkable.Type t;
Object value;
Matcher externalMatcher = fullQuotePattern.matcher(href);
if (externalMatcher.matches()) {
String board = externalMatcher.group(1);
int threadId = Integer.parseInt(externalMatcher.group(2));
int postId = Integer.parseInt(externalMatcher.group(3));
if (board.equals(post.board.code) && callback.isInternal(postId)) {
t = PostLinkable.Type.QUOTE;
value = postId;
} else {
t = PostLinkable.Type.THREAD;
value = new PostLinkable.ThreadLink(board, threadId, postId);
}
} else {
Matcher quoteMatcher = quotePattern.matcher(href);
if (quoteMatcher.matches()) {
t = PostLinkable.Type.QUOTE;
value = Integer.parseInt(quoteMatcher.group(1));
} else {
// normal link
t = PostLinkable.Type.LINK;
value = href;
}
}
Link link = new Link();
link.type = t;
link.key = text;
link.value = value;
return link;
}
public SpannableString span(CharSequence text, Object... additionalSpans) {
SpannableString result = new SpannableString(text);
int l = result.length();
if (additionalSpans != null && additionalSpans.length > 0) {
for (Object additionalSpan : additionalSpans) {
if (additionalSpan != null) {
result.setSpan(additionalSpan, 0, l, 0);
}
}
}
return result;
}
public class Link {
public PostLinkable.Type type;
public CharSequence key;
public Object value;
}
}

@ -15,8 +15,9 @@
* 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.site.common; package org.floens.chan.core.site.parser;
import android.support.annotation.AnyThread;
import android.text.SpannableString; import android.text.SpannableString;
import org.floens.chan.core.model.Post; import org.floens.chan.core.model.Post;
@ -34,7 +35,8 @@ import org.nibor.autolink.LinkType;
import java.util.EnumSet; import java.util.EnumSet;
public class ChanParserHelper { @AnyThread
public class CommentParserHelper {
private static final LinkExtractor LINK_EXTRACTOR = LinkExtractor.builder() private static final LinkExtractor LINK_EXTRACTOR = LinkExtractor.builder()
.linkTypes(EnumSet.of(LinkType.URL)) .linkTypes(EnumSet.of(LinkType.URL))
.build(); .build();

@ -15,14 +15,15 @@
* 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.site.common; package org.floens.chan.core.site.parser;
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.orm.Filter;
import org.floens.chan.core.model.Post; import org.floens.chan.core.model.Post;
import org.floens.chan.core.model.orm.Filter;
import java.util.List; import java.util.List;
import java.util.Set;
import java.util.concurrent.Callable; import java.util.concurrent.Callable;
// Called concurrently to parse the post html and the filters on it // Called concurrently to parse the post html and the filters on it
@ -35,17 +36,19 @@ class PostParseCallable implements Callable<Post> {
private DatabaseSavedReplyManager savedReplyManager; private DatabaseSavedReplyManager savedReplyManager;
private Post.Builder post; private Post.Builder post;
private ChanReader reader; private ChanReader reader;
private final Set<Integer> internalIds;
public PostParseCallable(FilterEngine filterEngine, public PostParseCallable(FilterEngine filterEngine,
List<Filter> filters, List<Filter> filters,
DatabaseSavedReplyManager savedReplyManager, DatabaseSavedReplyManager savedReplyManager,
Post.Builder post, Post.Builder post,
ChanReader reader) { ChanReader reader, Set<Integer> internalIds) {
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.reader = reader; this.reader = reader;
this.internalIds = internalIds;
} }
@Override @Override
@ -55,15 +58,16 @@ class PostParseCallable implements Callable<Post> {
post.isSavedReply(savedReplyManager.isSaved(post.board, post.id)); post.isSavedReply(savedReplyManager.isSaved(post.board, post.id));
// if (!post.parse(parser)) { return reader.getParser().parse(null, post, new PostParser.Callback() {
// Logger.e(TAG, "Incorrect data about post received for post " + post.no);
// return null;
// }
return reader.getParser().parse(null, post, new ChanParser.Callback() {
@Override @Override
public boolean isSaved(int postNo) { public boolean isSaved(int postNo) {
return savedReplyManager.isSaved(post.board, postNo); return savedReplyManager.isSaved(post.board, postNo);
} }
@Override
public boolean isInternal(int postNo) {
return internalIds.contains(postNo);
}
}); });
} }

@ -15,15 +15,23 @@
* 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.site.common; package org.floens.chan.core.site.parser;
import org.floens.chan.core.model.Post; import org.floens.chan.core.model.Post;
import org.floens.chan.ui.theme.Theme; import org.floens.chan.ui.theme.Theme;
public interface ChanParser { public interface PostParser {
Post parse(Theme theme, Post.Builder builder, Callback callback); Post parse(Theme theme, Post.Builder builder, Callback callback);
interface Callback { interface Callback {
boolean isSaved(int postNo); boolean isSaved(int postNo);
/**
* Is the post id from this thread.
*
* @param postNo the post id
* @return {@code true} if referring to a post in the thread, {@code false} otherwise.
*/
boolean isInternal(int postNo);
} }
} }

@ -30,15 +30,16 @@ import org.floens.chan.core.settings.Setting;
import org.floens.chan.core.settings.SettingProvider; import org.floens.chan.core.settings.SettingProvider;
import org.floens.chan.core.settings.SharedPreferencesSettingProvider; import org.floens.chan.core.settings.SharedPreferencesSettingProvider;
import org.floens.chan.core.settings.StringSetting; import org.floens.chan.core.settings.StringSetting;
import org.floens.chan.core.site.Authentication; import org.floens.chan.core.site.SiteAuthentication;
import org.floens.chan.core.site.Boards; import org.floens.chan.core.site.Boards;
import org.floens.chan.core.site.Resolvable; import org.floens.chan.core.site.SiteUrlHandler;
import org.floens.chan.core.site.Site; import org.floens.chan.core.site.Site;
import org.floens.chan.core.site.SiteActions;
import org.floens.chan.core.site.SiteBase; import org.floens.chan.core.site.SiteBase;
import org.floens.chan.core.site.SiteEndpoints; import org.floens.chan.core.site.SiteEndpoints;
import org.floens.chan.core.site.SiteIcon; import org.floens.chan.core.site.SiteIcon;
import org.floens.chan.core.site.SiteRequestModifier; import org.floens.chan.core.site.SiteRequestModifier;
import org.floens.chan.core.site.common.ChanReader; import org.floens.chan.core.site.parser.ChanReader;
import org.floens.chan.core.site.common.CommonReplyHttpCall; import org.floens.chan.core.site.common.CommonReplyHttpCall;
import org.floens.chan.core.site.common.FutabaChanReader; import org.floens.chan.core.site.common.FutabaChanReader;
import org.floens.chan.core.site.http.DeleteRequest; import org.floens.chan.core.site.http.DeleteRequest;
@ -61,7 +62,7 @@ import okhttp3.HttpUrl;
import okhttp3.Request; import okhttp3.Request;
public class Chan4 extends SiteBase { public class Chan4 extends SiteBase {
public static final Resolvable RESOLVABLE = new Resolvable() { public static final SiteUrlHandler SITE_URL_HANDLER = new SiteUrlHandler() {
@Override @Override
public Class<? extends Site> getSiteClass() { public Class<? extends Site> getSiteClass() {
return Chan4.class; return Chan4.class;
@ -78,6 +79,67 @@ public class Chan4 extends SiteBase {
url.host().equals("www.4chan.org") || url.host().equals("www.4chan.org") ||
url.host().equals("boards.4chan.org"); url.host().equals("boards.4chan.org");
} }
@Override
public String desktopUrl(Loadable loadable, @Nullable Post post) {
if (loadable.isCatalogMode()) {
return "https://boards.4chan.org/" + loadable.board.code + "/";
} else if (loadable.isThreadMode()) {
String url = "https://boards.4chan.org/" + loadable.board.code + "/thread/" + loadable.no;
if (post != null) {
url += "#p" + post.no;
}
return url;
} else {
throw new IllegalArgumentException();
}
}
@Override
public Loadable resolveLoadable(Site site, HttpUrl url) {
List<String> parts = url.pathSegments();
if (!parts.isEmpty()) {
String boardCode = parts.get(0);
Board board = site.board(boardCode);
if (board != null) {
if (parts.size() < 3) {
// Board mode
return Loadable.forCatalog(board);
} else if (parts.size() >= 3) {
// Thread mode
int no = -1;
try {
no = Integer.parseInt(parts.get(2));
} catch (NumberFormatException ignored) {
}
int post = -1;
String fragment = url.fragment();
if (fragment != null) {
int index = fragment.indexOf("p");
if (index >= 0) {
try {
post = Integer.parseInt(fragment.substring(index + 1));
} catch (NumberFormatException ignored) {
}
}
}
if (no >= 0) {
Loadable loadable = Loadable.forThread(site, board, no);
if (post >= 0) {
loadable.markedNo = post;
}
return loadable;
}
}
}
}
return null;
}
}; };
private static final String TAG = "Chan4"; private static final String TAG = "Chan4";
@ -223,7 +285,7 @@ public class Chan4 extends SiteBase {
private SiteRequestModifier siteRequestModifier = new SiteRequestModifier() { private SiteRequestModifier siteRequestModifier = new SiteRequestModifier() {
@Override @Override
public void modifyHttpCall(HttpCall httpCall, Request.Builder requestBuilder) { public void modifyHttpCall(HttpCall httpCall, Request.Builder requestBuilder) {
if (isLoggedIn()) { if (actions.isLoggedIn()) {
requestBuilder.addHeader("Cookie", "pass_id=" + passToken.get()); requestBuilder.addHeader("Cookie", "pass_id=" + passToken.get());
} }
} }
@ -238,7 +300,7 @@ public class Chan4 extends SiteBase {
CookieManager cookieManager = CookieManager.getInstance(); CookieManager cookieManager = CookieManager.getInstance();
cookieManager.removeAllCookie(); cookieManager.removeAllCookie();
if (isLoggedIn()) { if (actions.isLoggedIn()) {
String[] passCookies = { String[] passCookies = {
"pass_enabled=1;", "pass_enabled=1;",
"pass_id=" + passToken.get() + ";" "pass_id=" + passToken.get() + ";"
@ -251,6 +313,114 @@ public class Chan4 extends SiteBase {
} }
}; };
private SiteActions actions = new SiteActions() {
@Override
public void boards(final BoardsListener listener) {
requestQueue.add(new Chan4BoardsRequest(Chan4.this, response -> {
listener.onBoardsReceived(new Boards(response));
}, (error) -> {
Logger.e(TAG, "Failed to get boards from server", error);
// API fail, provide some default boards
List<Board> list = new ArrayList<>();
list.add(new Board(Chan4.this, "Technology", "g", true, true));
list.add(new Board(Chan4.this, "Food & Cooking", "ck", true, true));
list.add(new Board(Chan4.this, "Do It Yourself", "diy", true, true));
list.add(new Board(Chan4.this, "Animals & Nature", "an", true, true));
Collections.shuffle(list);
listener.onBoardsReceived(new Boards(list));
}));
}
@Override
public void post(Reply reply, final PostListener postListener) {
httpCallManager.makeHttpCall(new Chan4ReplyCall(Chan4.this, reply), new HttpCall.HttpCallback<CommonReplyHttpCall>() {
@Override
public void onHttpSuccess(CommonReplyHttpCall httpPost) {
postListener.onPostComplete(httpPost, httpPost.replyResponse);
}
@Override
public void onHttpFail(CommonReplyHttpCall httpPost, Exception e) {
postListener.onPostError(httpPost);
}
});
}
@Override
public boolean postRequiresAuthentication() {
return !isLoggedIn();
}
@Override
public SiteAuthentication postAuthenticate() {
if (isLoggedIn()) {
return SiteAuthentication.fromNone();
} else {
switch (captchaType.get()) {
case V2JS:
return SiteAuthentication.fromCaptcha2(CAPTCHA_KEY, "https://boards.4chan.org");
case V2NOJS:
return SiteAuthentication.fromCaptcha2nojs(CAPTCHA_KEY, "https://boards.4chan.org");
default:
throw new IllegalArgumentException();
}
}
}
@Override
public void delete(DeleteRequest deleteRequest, final DeleteListener deleteListener) {
httpCallManager.makeHttpCall(new Chan4DeleteHttpCall(Chan4.this, deleteRequest), new HttpCall.HttpCallback<Chan4DeleteHttpCall>() {
@Override
public void onHttpSuccess(Chan4DeleteHttpCall httpPost) {
deleteListener.onDeleteComplete(httpPost, httpPost.deleteResponse);
}
@Override
public void onHttpFail(Chan4DeleteHttpCall httpPost, Exception e) {
deleteListener.onDeleteError(httpPost);
}
});
}
@Override
public void login(LoginRequest loginRequest, final LoginListener loginListener) {
passUser.set(loginRequest.user);
passPass.set(loginRequest.pass);
httpCallManager.makeHttpCall(new Chan4PassHttpCall(Chan4.this, loginRequest), new HttpCall.HttpCallback<Chan4PassHttpCall>() {
@Override
public void onHttpSuccess(Chan4PassHttpCall httpCall) {
LoginResponse loginResponse = httpCall.loginResponse;
if (loginResponse.success) {
passToken.set(loginResponse.token);
}
loginListener.onLoginComplete(httpCall, loginResponse);
}
@Override
public void onHttpFail(Chan4PassHttpCall httpCall, Exception e) {
loginListener.onLoginError(httpCall);
}
});
}
@Override
public void logout() {
passToken.set("");
}
@Override
public boolean isLoggedIn() {
return !passToken.get().isEmpty();
}
@Override
public LoginRequest getLoginDetails() {
return new LoginRequest(passUser.get(), passPass.get());
}
};
// Legacy settings that were global before // Legacy settings that were global before
private final StringSetting passUser; private final StringSetting passUser;
private final StringSetting passPass; private final StringSetting passPass;
@ -310,55 +480,8 @@ public class Chan4 extends SiteBase {
} }
@Override @Override
public Resolvable resolvable() { public SiteUrlHandler resolvable() {
return RESOLVABLE; return SITE_URL_HANDLER;
}
@Override
public Loadable resolveLoadable(HttpUrl url) {
List<String> parts = url.pathSegments();
if (!parts.isEmpty()) {
String boardCode = parts.get(0);
Board board = board(boardCode);
if (board != null) {
if (parts.size() < 3) {
// Board mode
return loadableProvider.get(Loadable.forCatalog(board));
} else if (parts.size() >= 3) {
// Thread mode
int no = -1;
try {
no = Integer.parseInt(parts.get(2));
} catch (NumberFormatException ignored) {
}
int post = -1;
String fragment = url.fragment();
if (fragment != null) {
int index = fragment.indexOf("p");
if (index >= 0) {
try {
post = Integer.parseInt(fragment.substring(index + 1));
} catch (NumberFormatException ignored) {
}
}
}
if (no >= 0) {
Loadable loadable = loadableProvider.get(
Loadable.forThread(this, board, no));
if (post >= 0) {
loadable.markedNo = post;
}
return loadable;
}
}
}
}
return null;
} }
@Override @Override
@ -387,21 +510,6 @@ public class Chan4 extends SiteBase {
return BoardsType.DYNAMIC; return BoardsType.DYNAMIC;
} }
@Override
public String desktopUrl(Loadable loadable, @Nullable Post post) {
if (loadable.isCatalogMode()) {
return "https://boards.4chan.org/" + loadable.board.code + "/";
} else if (loadable.isThreadMode()) {
String url = "https://boards.4chan.org/" + loadable.board.code + "/thread/" + loadable.no;
if (post != null) {
url += "#p" + post.no;
}
return url;
} else {
throw new IllegalArgumentException();
}
}
@Override @Override
public boolean boardFeature(BoardFeature boardFeature, Board board) { public boolean boardFeature(BoardFeature boardFeature, Board board) {
switch (boardFeature) { switch (boardFeature) {
@ -416,23 +524,6 @@ public class Chan4 extends SiteBase {
} }
} }
@Override
public void boards(final BoardsListener listener) {
requestQueue.add(new Chan4BoardsRequest(this, response -> {
listener.onBoardsReceived(new Boards(response));
}, (error) -> {
Logger.e(TAG, "Failed to get boards from server", error);
// API fail, provide some default boards
List<Board> list = new ArrayList<>();
list.add(new Board(Chan4.this, "Technology", "g", true, true));
list.add(new Board(Chan4.this, "Food & Cooking", "ck", true, true));
list.add(new Board(Chan4.this, "Do It Yourself", "diy", true, true));
list.add(new Board(Chan4.this, "Animals & Nature", "an", true, true));
Collections.shuffle(list);
listener.onBoardsReceived(new Boards(list));
}));
}
@Override @Override
public SiteEndpoints endpoints() { public SiteEndpoints endpoints() {
@ -450,90 +541,7 @@ public class Chan4 extends SiteBase {
} }
@Override @Override
public void post(Reply reply, final PostListener postListener) { public SiteActions actions() {
httpCallManager.makeHttpCall(new Chan4ReplyCall(this, reply), new HttpCall.HttpCallback<CommonReplyHttpCall>() { return actions;
@Override
public void onHttpSuccess(CommonReplyHttpCall httpPost) {
postListener.onPostComplete(httpPost, httpPost.replyResponse);
}
@Override
public void onHttpFail(CommonReplyHttpCall httpPost, Exception e) {
postListener.onPostError(httpPost);
}
});
}
@Override
public boolean postRequiresAuthentication() {
return !isLoggedIn();
}
@Override
public Authentication postAuthenticate() {
if (isLoggedIn()) {
return Authentication.fromNone();
} else {
switch (captchaType.get()) {
case V2JS:
return Authentication.fromCaptcha2(CAPTCHA_KEY, "https://boards.4chan.org");
case V2NOJS:
return Authentication.fromCaptcha2nojs(CAPTCHA_KEY, "https://boards.4chan.org");
default:
throw new IllegalArgumentException();
}
}
}
@Override
public void delete(DeleteRequest deleteRequest, final DeleteListener deleteListener) {
httpCallManager.makeHttpCall(new Chan4DeleteHttpCall(this, deleteRequest), new HttpCall.HttpCallback<Chan4DeleteHttpCall>() {
@Override
public void onHttpSuccess(Chan4DeleteHttpCall httpPost) {
deleteListener.onDeleteComplete(httpPost, httpPost.deleteResponse);
}
@Override
public void onHttpFail(Chan4DeleteHttpCall httpPost, Exception e) {
deleteListener.onDeleteError(httpPost);
}
});
}
@Override
public void login(LoginRequest loginRequest, final LoginListener loginListener) {
passUser.set(loginRequest.user);
passPass.set(loginRequest.pass);
httpCallManager.makeHttpCall(new Chan4PassHttpCall(this, loginRequest), new HttpCall.HttpCallback<Chan4PassHttpCall>() {
@Override
public void onHttpSuccess(Chan4PassHttpCall httpCall) {
LoginResponse loginResponse = httpCall.loginResponse;
if (loginResponse.success) {
passToken.set(loginResponse.token);
}
loginListener.onLoginComplete(httpCall, loginResponse);
}
@Override
public void onHttpFail(Chan4PassHttpCall httpCall, Exception e) {
loginListener.onLoginError(httpCall);
}
});
}
@Override
public void logout() {
passToken.set("");
}
@Override
public boolean isLoggedIn() {
return !passToken.get().isEmpty();
}
@Override
public LoginRequest getLoginDetails() {
return new LoginRequest(passUser.get(), passPass.get());
} }
} }

@ -1,58 +1,45 @@
/*
* 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.site.sites.vichan; package org.floens.chan.core.site.sites.vichan;
import android.support.annotation.Nullable; import android.support.annotation.Nullable;
import android.webkit.WebView; import android.util.JsonReader;
import org.floens.chan.core.model.Post; import org.floens.chan.core.model.Post;
import org.floens.chan.core.model.PostHttpIcon;
import org.floens.chan.core.model.PostImage;
import org.floens.chan.core.model.orm.Board; import org.floens.chan.core.model.orm.Board;
import org.floens.chan.core.model.orm.Loadable; import org.floens.chan.core.model.orm.Loadable;
import org.floens.chan.core.site.Authentication; import org.floens.chan.core.site.SiteAuthentication;
import org.floens.chan.core.site.Resolvable;
import org.floens.chan.core.site.Site; import org.floens.chan.core.site.Site;
import org.floens.chan.core.site.SiteBase;
import org.floens.chan.core.site.SiteEndpoints; import org.floens.chan.core.site.SiteEndpoints;
import org.floens.chan.core.site.SiteIcon; import org.floens.chan.core.site.SiteIcon;
import org.floens.chan.core.site.SiteRequestModifier; import org.floens.chan.core.site.parser.ChanReader;
import org.floens.chan.core.site.common.ChanReader; import org.floens.chan.core.site.parser.ChanReaderProcessingQueue;
import org.floens.chan.core.site.common.CommonReplyHttpCall; import org.floens.chan.core.site.common.DefaultPostParser;
import org.floens.chan.core.site.common.FutabaChanParser;
import org.floens.chan.core.site.common.FutabaChanReader; import org.floens.chan.core.site.common.FutabaChanReader;
import org.floens.chan.core.site.http.HttpCall; import org.floens.chan.core.site.common.MultipartHttpCall;
import org.floens.chan.core.site.http.HttpCallManager;
import org.floens.chan.core.site.http.Reply; import org.floens.chan.core.site.http.Reply;
import org.floens.chan.utils.Logger; import org.floens.chan.core.site.http.ReplyResponse;
import org.floens.chan.core.site.common.CommonSite;
import org.floens.chan.ui.theme.Theme;
import org.jsoup.Jsoup;
import org.jsoup.parser.Parser;
import java.io.IOException;
import java.util.ArrayList;
import java.util.List; import java.util.List;
import java.util.Locale; import java.util.Locale;
import java.util.Map; import java.util.Map;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
import okhttp3.HttpUrl; import okhttp3.HttpUrl;
import okhttp3.Request; import okhttp3.Response;
import static org.floens.chan.Chan.injector; import static android.text.TextUtils.isEmpty;
import static org.floens.chan.core.site.SiteEndpoints.makeArgument;
public class ViChan extends SiteBase { public class ViChan extends CommonSite {
private static final String TAG = "ViChan"; public static final CommonSiteUrlHandler RESOLVABLE = new CommonSiteUrlHandler() {
public static final Resolvable RESOLVABLE = new Resolvable() {
@Override @Override
public Class<? extends Site> getSiteClass() { public Class<? extends Site> getSiteClass() {
return ViChan.class; return ViChan.class;
@ -67,252 +54,495 @@ public class ViChan extends SiteBase {
public boolean respondsTo(HttpUrl url) { public boolean respondsTo(HttpUrl url) {
return url.host().equals("8ch.net"); return url.host().equals("8ch.net");
} }
};
private final SiteEndpoints endpoints = new SiteEndpoints() { @Override
private final HttpUrl root = new HttpUrl.Builder() public Loadable resolveLoadable(Site site, HttpUrl url) {
.scheme("https") Matcher board = Pattern.compile("/(\\w+)")
.host("8ch.net") .matcher(url.encodedPath());
.build(); Matcher thread = Pattern.compile("/(\\w+)/res/(\\d+).html")
.matcher(url.encodedPath());
try {
if (thread.find()) {
Board b = site.board(thread.group(1));
if (b == null) {
return null;
}
Loadable l = Loadable.forThread(site, b, Integer.parseInt(thread.group(3)));
private final HttpUrl media = new HttpUrl.Builder() if (isEmpty(url.fragment())) {
.scheme("https") l.markedNo = Integer.parseInt(url.fragment());
.host("media.8ch.net") }
.build();
private final HttpUrl sys = new HttpUrl.Builder() return l;
.scheme("https") } else if (board.find()) {
.host("sys.8ch.net") Board b = site.board(board.group(1));
.build(); if (b == null) {
return null;
}
@Override return Loadable.forCatalog(b);
public HttpUrl catalog(Board board) { }
return root.newBuilder() } catch (NumberFormatException ignored) {
.addPathSegment(board.code) }
.addPathSegment("catalog.json")
.build();
}
@Override return null;
public HttpUrl thread(Board board, Loadable loadable) {
return root.newBuilder()
.addPathSegment(board.code)
.addPathSegment("res")
.addPathSegment(loadable.no + ".json")
.build();
} }
@Override @Override
public HttpUrl imageUrl(Post.Builder post, Map<String, String> arg) { public String desktopUrl(Loadable loadable, @Nullable Post post) {
return root.newBuilder() if (loadable.isCatalogMode()) {
.addPathSegment("file_store") return "https://8ch.net/" + loadable.boardCode;
.addPathSegment(arg.get("tim") + "." + arg.get("ext")) } else if (loadable.isThreadMode()) {
.build(); return "https://8ch.net/" + loadable.boardCode + "/res/" + loadable.no + ".html";
} else {
return "https://8ch.net/";
}
} }
};
@Override private static final String TAG = "ViChan";
public HttpUrl thumbnailUrl(Post.Builder post, boolean spoiler, Map<String, String> arg) {
String ext; @Override
switch (arg.get("ext")) { public ChanReader chanReader() {
case "jpeg": return new FutabaChanReader(new DefaultPostParser(new ViChanCommentParser()));
case "jpg": }
case "png":
case "gif": @Override
ext = arg.get("ext"); public void setup() {
break; setName("8chan");
default: setIcon(SiteIcon.fromAssets("icons/8chan.png"));
ext = "jpg"; setBoardsType(BoardsType.INFINITE);
break;
setResolvable(RESOLVABLE);
setConfig(new CommonConfig() {
@Override
public boolean feature(Feature feature) {
return feature == Feature.POSTING;
} }
});
return root.newBuilder() setEndpoints(new CommonEndpoints() {
.addPathSegment("file_store") private final SimpleHttpUrl root = from("https://8ch.net");
.addPathSegment("thumb") private final SimpleHttpUrl sys = from("https://sys.8ch.net");
.addPathSegment(arg.get("tim") + "." + ext)
.build();
}
@Override @Override
public HttpUrl icon(Post.Builder post, String icon, Map<String, String> arg) { public HttpUrl catalog(Board board) {
HttpUrl.Builder stat = root.newBuilder().addPathSegment("static"); return root.builder().s(board.code).s("catalog.json").url();
switch (icon) {
case "country":
stat.addPathSegment("flags");
stat.addPathSegment(arg.get("country_code").toLowerCase(Locale.ENGLISH) + ".png");
break;
} }
return stat.build(); @Override
} public HttpUrl thread(Board board, Loadable loadable) {
return root.builder().s(board.code).s("res").s(loadable.no + ".json").url();
}
@Override @Override
public HttpUrl boards() { public HttpUrl imageUrl(Post.Builder post, Map<String, String> arg) {
return null; return root.builder().s("file_store").s(arg.get("tim") + "." + arg.get("ext")).url();
} }
@Override @Override
public HttpUrl reply(Loadable loadable) { public HttpUrl thumbnailUrl(Post.Builder post, boolean spoiler, Map<String, String> arg) {
return sys.newBuilder() String ext;
.addPathSegment("post.php") switch (arg.get("ext")) {
.build(); case "jpeg":
} case "jpg":
case "png":
case "gif":
ext = arg.get("ext");
break;
default:
ext = "jpg";
break;
}
@Override return root.builder().s("file_store").s("thumb").s(arg.get("tim") + "." + ext).url();
public HttpUrl delete(Post post) { }
return null;
}
@Override @Override
public HttpUrl report(Post post) { public HttpUrl icon(Post.Builder post, String icon, Map<String, String> arg) {
return null; SimpleHttpUrl stat = root.builder().s("static");
}
@Override if (icon.equals("country")) {
public HttpUrl login() { stat.s("flags").s(arg.get("country_code").toLowerCase(Locale.ENGLISH) + ".png");
return null; }
}
};
private SiteRequestModifier siteRequestModifier = new SiteRequestModifier() { return stat.url();
@Override }
public void modifyHttpCall(HttpCall httpCall, Request.Builder requestBuilder) {
}
@SuppressWarnings("deprecation") @Override
@Override public HttpUrl reply(Loadable loadable) {
public void modifyWebView(WebView webView) { return sys.builder().s("post.php").url();
} }
};
@Override });
public String name() {
return "8chan";
}
@Override setActions(new CommonActions() {
public SiteIcon icon() { @Override
return SiteIcon.fromAssets("icons/8chan.png"); public void setupPost(Reply reply, MultipartHttpCall call) {
} call.parameter("board", reply.loadable.board.code);
@Override if (reply.loadable.isThreadMode()) {
public Resolvable resolvable() { call.parameter("post", "New Reply");
return RESOLVABLE; call.parameter("thread", String.valueOf(reply.loadable.no));
} } else {
call.parameter("post", "New Thread");
call.parameter("page", "1");
}
@Override call.parameter("pwd", reply.password);
public Loadable resolveLoadable(HttpUrl url) { call.parameter("name", reply.name);
List<String> parts = url.pathSegments(); call.parameter("email", reply.options);
if (!parts.isEmpty()) { if (!reply.loadable.isThreadMode() && !isEmpty(reply.subject)) {
String boardCode = parts.get(0); call.parameter("subject", reply.subject);
Board board = board(boardCode); }
if (board != null) {
if (parts.size() < 3) { call.parameter("body", reply.comment);
// Board mode
return loadableProvider.get(Loadable.forCatalog(board)); if (reply.file != null) {
} else if (parts.size() >= 3) { call.fileParameter("file", reply.fileName, reply.file);
// Thread mode }
int no = -1;
if (reply.spoilerImage) {
call.parameter("spoiler", "on");
}
}
@Override
public void handlePost(ReplyResponse replyResponse, Response response, String result) {
Matcher auth = Pattern.compile(".*\"captcha\": ?true.*").matcher(result);
Matcher err = Pattern.compile(".*<h1>Error</h1>.*<h2[^>]*>(.*?)</h2>.*").matcher(result);
if (auth.find()) {
replyResponse.requireAuthentication = true;
replyResponse.errorMessage = result;
} else if (err.find()) {
replyResponse.errorMessage = Jsoup.parse(err.group(1)).body().text();
} else {
HttpUrl url = response.request().url();
Matcher m = Pattern.compile("/\\w+/\\w+/(\\d+).html").matcher(url.encodedPath());
try { try {
no = Integer.parseInt(parts.get(2).replace(".html", "")); if (m.find()) {
replyResponse.threadNo = Integer.parseInt(m.group(1));
replyResponse.postNo = Integer.parseInt(url.encodedFragment());
replyResponse.posted = true;
}
} catch (NumberFormatException ignored) { } catch (NumberFormatException ignored) {
replyResponse.errorMessage = "Error posting: could not find posted thread.";
} }
}
}
int post = -1; @Override
String fragment = url.fragment(); public SiteAuthentication postAuthenticate() {
if (fragment != null) { return SiteAuthentication.fromUrl("https://8ch.net/dnsbls_bypass.php",
try { "You failed the CAPTCHA",
post = Integer.parseInt(fragment); "You may now go back and make your post");
} catch (NumberFormatException ignored) { }
} });
}
if (no >= 0) { setApi(new ViChanApi());
Loadable loadable = loadableProvider.get(
Loadable.forThread(this, board, no)); setParser(new CommonParser() {
if (post >= 0) { @Override
loadable.markedNo = post; public Post parse(Theme theme, Post.Builder builder, Callback callback) {
} return null;
}
});
}
return loadable; private class ViChanApi extends CommonApi {
@Override
public void loadThread(JsonReader reader, ChanReaderProcessingQueue queue) throws Exception {
reader.beginObject();
// Page object
while (reader.hasNext()) {
String key = reader.nextName();
if (key.equals("posts")) {
reader.beginArray();
// Thread array
while (reader.hasNext()) {
// Thread object
readPostObject(reader, queue);
} }
reader.endArray();
} else {
reader.skipValue();
} }
} }
reader.endObject();
} }
return null; @Override
} public void loadCatalog(JsonReader reader, ChanReaderProcessingQueue queue) throws Exception {
reader.beginArray(); // Array of pages
@Override while (reader.hasNext()) {
public boolean feature(Feature feature) { reader.beginObject(); // Page object
switch (feature) {
case POSTING:
return true;
default:
return false;
}
}
@Override while (reader.hasNext()) {
public boolean boardFeature(BoardFeature boardFeature, Board board) { if (reader.nextName().equals("threads")) {
return false; reader.beginArray(); // Threads array
}
@Override while (reader.hasNext()) {
public SiteEndpoints endpoints() { readPostObject(reader, queue);
return endpoints; }
}
@Override reader.endArray();
public SiteRequestModifier requestModifier() { } else {
return siteRequestModifier; reader.skipValue();
} }
}
@Override reader.endObject();
public BoardsType boardsType() { }
return BoardsType.INFINITE;
}
@Override reader.endArray();
public String desktopUrl(Loadable loadable, @Nullable Post post) { }
return "https://8ch.net/";
}
@Override @Override
public void boards(BoardsListener boardsListener) { public void readPostObject(JsonReader reader, ChanReaderProcessingQueue queue) throws Exception {
} Post.Builder builder = new Post.Builder();
builder.board(queue.getLoadable().board);
SiteEndpoints endpoints = queue.getLoadable().getSite().endpoints();
// File
String fileId = null;
String fileExt = null;
int fileWidth = 0;
int fileHeight = 0;
long fileSize = 0;
boolean fileSpoiler = false;
String fileName = null;
List<PostImage> files = new ArrayList<>();
// Country flag
String countryCode = null;
String trollCountryCode = null;
String countryName = null;
reader.beginObject();
while (reader.hasNext()) {
String key = reader.nextName();
switch (key) {
case "no":
builder.id(reader.nextInt());
break;
case "sub":
builder.subject(reader.nextString());
break;
case "name":
builder.name(reader.nextString());
break;
case "com":
builder.comment(reader.nextString());
break;
case "tim":
fileId = reader.nextString();
break;
case "time":
builder.setUnixTimestampSeconds(reader.nextLong());
break;
case "ext":
fileExt = reader.nextString().replace(".", "");
break;
case "w":
fileWidth = reader.nextInt();
break;
case "h":
fileHeight = reader.nextInt();
break;
case "fsize":
fileSize = reader.nextLong();
break;
case "filename":
fileName = reader.nextString();
break;
case "trip":
builder.tripcode(reader.nextString());
break;
case "country":
countryCode = reader.nextString();
break;
case "troll_country":
trollCountryCode = reader.nextString();
break;
case "country_name":
countryName = reader.nextString();
break;
case "spoiler":
fileSpoiler = reader.nextInt() == 1;
break;
case "resto":
int opId = reader.nextInt();
builder.op(opId == 0);
builder.opId(opId);
break;
case "sticky":
builder.sticky(reader.nextInt() == 1);
break;
case "closed":
builder.closed(reader.nextInt() == 1);
break;
case "archived":
builder.archived(reader.nextInt() == 1);
break;
case "replies":
builder.replies(reader.nextInt());
break;
case "images":
builder.images(reader.nextInt());
break;
case "unique_ips":
builder.uniqueIps(reader.nextInt());
break;
case "id":
builder.posterId(reader.nextString());
break;
case "capcode":
builder.moderatorCapcode(reader.nextString());
break;
case "extra_files":
reader.beginArray();
while (reader.hasNext()) {
PostImage postImage = readPostImage(reader, builder, endpoints);
if (postImage != null) {
files.add(postImage);
}
}
@Override reader.endArray();
public ChanReader chanReader() { break;
FutabaChanParser parser = new FutabaChanParser(new ViChanParserHandler()); default:
return new FutabaChanReader(parser); // Unknown/ignored key
} reader.skipValue();
break;
}
}
reader.endObject();
// The file from between the other values.
if (fileId != null && fileName != null && fileExt != null) {
Map<String, String> args = makeArgument("tim", fileId,
"ext", fileExt);
PostImage image = new PostImage.Builder()
.originalName(String.valueOf(fileId))
.thumbnailUrl(endpoints.thumbnailUrl(builder, false, args))
.spoilerThumbnailUrl(endpoints.thumbnailUrl(builder, true, args))
.imageUrl(endpoints.imageUrl(builder, args))
.filename(Parser.unescapeEntities(fileName, false))
.extension(fileExt)
.imageWidth(fileWidth)
.imageHeight(fileHeight)
.spoiler(fileSpoiler)
.size(fileSize)
.build();
// Insert it at the beginning.
files.add(0, image);
}
@Override builder.images(files);
public void post(Reply reply, final PostListener postListener) {
// TODO if (builder.op) {
HttpCallManager httpCallManager = injector().instance(HttpCallManager.class); // Update OP fields later on the main thread
httpCallManager.makeHttpCall(new ViChanReplyHttpCall(this, reply), Post.Builder op = new Post.Builder();
new HttpCall.HttpCallback<CommonReplyHttpCall>() { op.closed(builder.closed);
@Override op.archived(builder.archived);
public void onHttpSuccess(CommonReplyHttpCall httpPost) { op.sticky(builder.sticky);
postListener.onPostComplete(httpPost, httpPost.replyResponse); op.replies(builder.replies);
} op.images(builder.imagesCount);
op.uniqueIps(builder.uniqueIps);
queue.setOp(op);
}
@Override Post cached = queue.getCachedPost(builder.id);
public void onHttpFail(CommonReplyHttpCall httpPost, Exception e) { if (cached != null) {
Logger.e(TAG, "post error", e); // Id is known, use the cached post object.
queue.addForReuse(cached);
return;
}
postListener.onPostError(httpPost); if (countryCode != null && countryName != null) {
} HttpUrl countryUrl = endpoints.icon(builder, "country",
}); makeArgument("country_code", countryCode));
} builder.addHttpIcon(new PostHttpIcon(countryUrl, countryName));
}
@Override if (trollCountryCode != null && countryName != null) {
public Authentication postAuthenticate() { HttpUrl countryUrl = endpoints.icon(builder, "troll_country",
return Authentication.fromUrl("https://8ch.net/dnsbls_bypass.php", makeArgument("troll_country_code", trollCountryCode));
"You failed the CAPTCHA", builder.addHttpIcon(new PostHttpIcon(countryUrl, countryName));
"You may now go back and make your post"); }
queue.addForParse(builder);
}
private PostImage readPostImage(JsonReader reader, Post.Builder builder,
SiteEndpoints endpoints) throws IOException {
reader.beginObject();
String fileId = null;
long fileSize = 0;
String fileExt = null;
int fileWidth = 0;
int fileHeight = 0;
boolean fileSpoiler = false;
String fileName = null;
while (reader.hasNext()) {
switch (reader.nextName()) {
case "tim":
fileId = reader.nextString();
break;
case "fsize":
fileSize = reader.nextLong();
break;
case "w":
fileWidth = reader.nextInt();
break;
case "h":
fileHeight = reader.nextInt();
break;
case "spoiler":
fileSpoiler = reader.nextInt() == 1;
break;
case "ext":
fileExt = reader.nextString().replace(".", "");
break;
case "filename":
fileName = reader.nextString();
break;
default:
reader.skipValue();
break;
}
}
reader.endObject();
if (fileId != null && fileName != null && fileExt != null) {
Map<String, String> args = makeArgument("tim", fileId,
"ext", fileExt);
return new PostImage.Builder()
.originalName(String.valueOf(fileId))
.thumbnailUrl(endpoints.thumbnailUrl(builder, false, args))
.spoilerThumbnailUrl(endpoints.thumbnailUrl(builder, true, args))
.imageUrl(endpoints.imageUrl(builder, args))
.filename(Parser.unescapeEntities(fileName, false))
.extension(fileExt)
.imageWidth(fileWidth)
.imageHeight(fileHeight)
.spoiler(fileSpoiler)
.size(fileSize)
.build();
}
return null;
}
} }
} }

@ -0,0 +1,47 @@
/*
* Clover - 4chan browser https://github.com/Floens/Clover/
* Copyright (C) 2014 Floens
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <http://www.gnu.org/licenses/>.
*/
package org.floens.chan.core.site.sites.vichan;
import android.text.SpannableString;
import org.floens.chan.core.model.Post;
import org.floens.chan.core.site.parser.CommentParser;
import org.floens.chan.core.site.parser.CommentParserHelper;
import org.floens.chan.ui.span.ForegroundColorSpanHashed;
import org.floens.chan.ui.theme.Theme;
import org.jsoup.nodes.Element;
import java.util.regex.Pattern;
public class ViChanCommentParser extends CommentParser {
public ViChanCommentParser() {
setQuotePattern(Pattern.compile(".*#(\\d+)"));
setFullQuotePattern(Pattern.compile("/(\\w+)/\\w+/(\\d+)\\.html#(\\d+)"));
}
@Override
public CharSequence handleParagraph(Theme theme, Post.Builder post, CharSequence text, Element element) {
if (element.hasClass("quote")) {
SpannableString res = span(text, new ForegroundColorSpanHashed(theme.inlineQuoteColor));
CommentParserHelper.detectLinks(theme, post, res.toString(), res);
return res;
} else {
return text;
}
}
}

@ -1,134 +0,0 @@
/*
* 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.site.sites.vichan;
import android.text.SpannableString;
import org.floens.chan.core.model.Post;
import org.floens.chan.core.model.PostLinkable;
import org.floens.chan.core.site.common.ChanParserHelper;
import org.floens.chan.core.site.common.DefaultFutabaChanParserHandler;
import org.floens.chan.core.site.common.FutabaChanParser;
import org.floens.chan.ui.span.ForegroundColorSpanHashed;
import org.floens.chan.ui.theme.Theme;
import org.jsoup.nodes.Element;
import java.util.Set;
public class ViChanParserHandler extends DefaultFutabaChanParserHandler {
@Override
public CharSequence handleParagraph(FutabaChanParser parser, Theme theme, Post.Builder post, CharSequence text, Element element) {
if (element.hasClass("quote")) {
SpannableString quote = new SpannableString(text);
quote.setSpan(new ForegroundColorSpanHashed(theme.inlineQuoteColor), 0, quote.length(), 0);
ChanParserHelper.detectLinks(theme, post, quote.toString(), quote);
return quote;
} else {
return text;
}
}
@Override
public CharSequence handleSpan(FutabaChanParser parser, Theme theme, Post.Builder post, Element span) {
SpannableString quote;
Set<String> classes = span.classNames();
if (classes.contains("abbr")) {
return null;
} else if (classes.contains("spoiler")) {
quote = new SpannableString(span.text());
PostLinkable pl = new PostLinkable(theme, span.text(), span.text(), PostLinkable.Type.SPOILER);
quote.setSpan(pl, 0, quote.length(), 0);
post.addLinkable(pl);
} else {
quote = new SpannableString(span.text());
quote.setSpan(new ForegroundColorSpanHashed(theme.inlineQuoteColor), 0, quote.length(), 0);
ChanParserHelper.detectLinks(theme, post, span.text(), quote);
}
return quote;
}
@Override
public Link handleAnchor(FutabaChanParser parser, Theme theme, Post.Builder post, Element anchor) {
String href = anchor.attr("href");
PostLinkable.Type t = null;
String key = null;
Object value = null;
if (href.startsWith("/")) {
if (!href.startsWith("/" + post.board.code + "/res/")) {
// link to another thread
PostLinkable.ThreadLink threadLink = null;
String[] slashSplit = href.split("/");
if (slashSplit.length == 4) {
String board = slashSplit[1];
String nums = slashSplit[3];
String[] numsSplitted = nums.split("#");
if (numsSplitted.length == 2) {
try {
int tId = Integer.parseInt(numsSplitted[0].replace(".html", ""));
int pId = Integer.parseInt(numsSplitted[1]);
threadLink = new PostLinkable.ThreadLink(board, tId, pId);
} catch (NumberFormatException ignored) {
}
}
}
if (threadLink != null) {
t = PostLinkable.Type.THREAD;
key = anchor.text();
value = threadLink;
}
} else {
// normal quote
int id = -1;
String[] splitted = href.split("#");
if (splitted.length == 2) {
try {
id = Integer.parseInt(splitted[1]);
} catch (NumberFormatException ignored) {
}
}
if (id >= 0) {
t = PostLinkable.Type.QUOTE;
key = anchor.text();
value = id;
}
}
} else {
// normal link
t = PostLinkable.Type.LINK;
key = anchor.text();
value = href;
}
if (t != null && key != null && value != null) {
Link link = new Link();
link.type = t;
link.key = key;
link.value = value;
return link;
} else {
return null;
}
}
}

@ -1,129 +0,0 @@
/*
* 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.site.sites.vichan;
import android.text.TextUtils;
import org.floens.chan.core.site.Site;
import org.floens.chan.core.site.common.CommonReplyHttpCall;
import org.floens.chan.core.site.http.Reply;
import org.floens.chan.utils.Logger;
import org.jsoup.Jsoup;
import java.io.IOException;
import java.util.List;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
import okhttp3.HttpUrl;
import okhttp3.MediaType;
import okhttp3.MultipartBody;
import okhttp3.RequestBody;
import okhttp3.Response;
public class ViChanReplyHttpCall extends CommonReplyHttpCall {
private static final String TAG = "ViChanReplyHttpCall";
private static final Pattern REQUIRE_AUTHENTICATION = Pattern.compile(".*\"captcha\": ?true.*");
private static final Pattern ERROR_MESSAGE =
Pattern.compile(".*<h1>Error</h1>.*<h2[^>]*>(.*?)<\\/h2>.*");
public ViChanReplyHttpCall(Site site, Reply reply) {
super(site, reply);
}
@Override
public void addParameters(MultipartBody.Builder formBuilder) {
// formBuilder.addFormDataPart("pwd", replyResponse.password);
formBuilder.addFormDataPart("board", reply.loadable.board.code);
if (reply.loadable.isThreadMode()) {
formBuilder.addFormDataPart("post", "New Reply");
formBuilder.addFormDataPart("thread", String.valueOf(reply.loadable.no));
} else {
formBuilder.addFormDataPart("post", "New Thread");
formBuilder.addFormDataPart("page", "1");
}
formBuilder.addFormDataPart("name", reply.name);
formBuilder.addFormDataPart("email", reply.options);
if (!reply.loadable.isThreadMode() && !TextUtils.isEmpty(reply.subject)) {
formBuilder.addFormDataPart("subject", reply.subject);
}
formBuilder.addFormDataPart("body", reply.comment);
if (reply.file != null) {
formBuilder.addFormDataPart("file", reply.fileName, RequestBody.create(
MediaType.parse("application/octet-stream"), reply.file
));
}
if (reply.spoilerImage) {
formBuilder.addFormDataPart("spoiler", "on");
}
}
@Override
public void process(Response response, String result) throws IOException {
Matcher authenticationMatcher = REQUIRE_AUTHENTICATION.matcher(result);
Matcher errorMessageMatcher = ERROR_MESSAGE.matcher(result);
if (authenticationMatcher.find()) {
replyResponse.requireAuthentication = true;
replyResponse.errorMessage = result;
} else if (errorMessageMatcher.find()) {
replyResponse.errorMessage = Jsoup.parse(errorMessageMatcher.group(1)).body().text();
} else {
Logger.d(TAG, "url: " + response.request().url().toString());
Logger.d(TAG, "body: " + response);
// TODO(multisite): 8ch redirects us, but the result is a 404, and we need that
// redirect url to figure out what we posted.
HttpUrl url = response.request().url();
List<String> segments = url.pathSegments();
String board = null;
int threadId = 0, postId = 0;
try {
if (segments.size() == 3) {
board = segments.get(0);
threadId = Integer.parseInt(
segments.get(2).replace(".html", ""));
postId = Integer.parseInt(url.encodedFragment());
}
} catch (NumberFormatException ignored) {
}
if (postId == 0) {
postId = threadId;
}
if (board != null && threadId != 0) {
replyResponse.threadNo = threadId;
replyResponse.postNo = postId;
replyResponse.posted = true;
} else {
replyResponse.errorMessage = "Error posting: could not find posted thread.";
}
}
}
}

@ -44,7 +44,7 @@ import org.floens.chan.core.model.orm.Loadable;
import org.floens.chan.core.model.orm.Pin; import org.floens.chan.core.model.orm.Pin;
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.core.site.SiteManager; import org.floens.chan.core.site.SiteService;
import org.floens.chan.core.site.SiteResolver; import org.floens.chan.core.site.SiteResolver;
import org.floens.chan.core.site.Sites; import org.floens.chan.core.site.Sites;
import org.floens.chan.ui.controller.BrowseController; import org.floens.chan.ui.controller.BrowseController;
@ -96,7 +96,7 @@ public class StartActivity extends AppCompatActivity implements NfcAdapter.Creat
SiteResolver siteResolver; SiteResolver siteResolver;
@Inject @Inject
SiteManager siteManager; SiteService siteService;
@Override @Override
protected void onCreate(Bundle savedInstanceState) { protected void onCreate(Bundle savedInstanceState) {
@ -151,7 +151,7 @@ public class StartActivity extends AppCompatActivity implements NfcAdapter.Creat
} }
private void restoreFresh() { private void restoreFresh() {
if (!siteManager.areSitesSetup()) { if (!siteService.areSitesSetup()) {
SitesSetupController setupController = new SitesSetupController(this); SitesSetupController setupController = new SitesSetupController(this);
if (drawerController.childControllers.get(0) instanceof DoubleNavigationController) { if (drawerController.childControllers.get(0) instanceof DoubleNavigationController) {

@ -30,7 +30,7 @@ import android.webkit.WebSettings;
import android.webkit.WebView; import android.webkit.WebView;
import android.webkit.WebViewClient; import android.webkit.WebViewClient;
import org.floens.chan.core.site.Authentication; import org.floens.chan.core.site.SiteAuthentication;
import org.floens.chan.core.site.Site; import org.floens.chan.core.site.Site;
import org.floens.chan.utils.AndroidUtils; import org.floens.chan.utils.AndroidUtils;
import org.floens.chan.utils.IOUtils; import org.floens.chan.utils.IOUtils;
@ -65,7 +65,7 @@ public class CaptchaLayout extends WebView implements AuthenticationLayoutInterf
this.callback = callback; this.callback = callback;
this.lightTheme = theme().isLightTheme; this.lightTheme = theme().isLightTheme;
Authentication authentication = site.postAuthenticate(); SiteAuthentication authentication = site.actions().postAuthenticate();
this.siteKey = authentication.siteKey; this.siteKey = authentication.siteKey;
this.baseUrl = authentication.baseUrl; this.baseUrl = authentication.baseUrl;

@ -30,7 +30,7 @@ import android.webkit.WebSettings;
import android.webkit.WebView; import android.webkit.WebView;
import android.webkit.WebViewClient; import android.webkit.WebViewClient;
import org.floens.chan.core.site.Authentication; import org.floens.chan.core.site.SiteAuthentication;
import org.floens.chan.core.site.Site; import org.floens.chan.core.site.Site;
import org.floens.chan.utils.AndroidUtils; import org.floens.chan.utils.AndroidUtils;
import org.floens.chan.utils.Logger; import org.floens.chan.utils.Logger;
@ -76,7 +76,7 @@ public class CaptchaNojsLayout extends WebView implements AuthenticationLayoutIn
public void initialize(Site site, AuthenticationLayoutCallback callback) { public void initialize(Site site, AuthenticationLayoutCallback callback) {
this.callback = callback; this.callback = callback;
Authentication authentication = site.postAuthenticate(); SiteAuthentication authentication = site.actions().postAuthenticate();
this.siteKey = authentication.siteKey; this.siteKey = authentication.siteKey;
this.baseUrl = authentication.baseUrl; this.baseUrl = authentication.baseUrl;

@ -26,7 +26,7 @@ import android.webkit.JavascriptInterface;
import android.webkit.WebSettings; import android.webkit.WebSettings;
import android.webkit.WebView; import android.webkit.WebView;
import org.floens.chan.core.site.Authentication; import org.floens.chan.core.site.SiteAuthentication;
import org.floens.chan.core.site.Site; import org.floens.chan.core.site.Site;
import org.floens.chan.utils.AndroidUtils; import org.floens.chan.utils.AndroidUtils;
@ -38,7 +38,7 @@ public class GenericWebViewAuthenticationLayout extends WebView implements Authe
private Site site; private Site site;
private AuthenticationLayoutCallback callback; private AuthenticationLayoutCallback callback;
private Authentication authentication; private SiteAuthentication authentication;
private boolean resettingFromFoundText = false; private boolean resettingFromFoundText = false;
public GenericWebViewAuthenticationLayout(Context context) { public GenericWebViewAuthenticationLayout(Context context) {
@ -61,7 +61,7 @@ public class GenericWebViewAuthenticationLayout extends WebView implements Authe
this.site = site; this.site = site;
this.callback = callback; this.callback = callback;
authentication = site.postAuthenticate(); authentication = site.actions().postAuthenticate();
// Older versions just have to manually go back or something. // Older versions just have to manually go back or something.
if (Build.VERSION.SDK_INT >= 17) { if (Build.VERSION.SDK_INT >= 17) {

@ -34,7 +34,7 @@ import android.widget.LinearLayout;
import android.widget.TextView; import android.widget.TextView;
import org.floens.chan.R; import org.floens.chan.R;
import org.floens.chan.core.site.Authentication; import org.floens.chan.core.site.SiteAuthentication;
import org.floens.chan.core.site.Site; import org.floens.chan.core.site.Site;
import org.floens.chan.ui.view.FixedRatioThumbnailView; import org.floens.chan.ui.view.FixedRatioThumbnailView;
import org.floens.chan.utils.AndroidUtils; import org.floens.chan.utils.AndroidUtils;
@ -121,7 +121,7 @@ public class LegacyCaptchaLayout extends LinearLayout implements AuthenticationL
public void initialize(Site site, AuthenticationLayoutCallback callback) { public void initialize(Site site, AuthenticationLayoutCallback callback) {
this.callback = callback; this.callback = callback;
Authentication authentication = site.postAuthenticate(); SiteAuthentication authentication = site.actions().postAuthenticate();
this.siteKey = authentication.siteKey; this.siteKey = authentication.siteKey;
this.baseUrl = authentication.baseUrl; this.baseUrl = authentication.baseUrl;

@ -217,7 +217,7 @@ public class BrowseController extends ThreadController implements ToolbarMenuIte
private void handleShareAndOpenInBrowser(ThreadPresenter presenter, Integer id) { private void handleShareAndOpenInBrowser(ThreadPresenter presenter, Integer id) {
if (presenter.isBound()) { if (presenter.isBound()) {
Loadable loadable = presenter.getLoadable(); Loadable loadable = presenter.getLoadable();
String link = loadable.site.desktopUrl(loadable, null); String link = loadable.site.resolvable().desktopUrl(loadable, null);
if (id == SHARE_ID) { if (id == SHARE_ID) {
AndroidUtils.shareLink(link); AndroidUtils.shareLink(link);

@ -29,6 +29,7 @@ import android.widget.TextView;
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.site.Site; import org.floens.chan.core.site.Site;
import org.floens.chan.core.site.SiteActions;
import org.floens.chan.core.site.http.HttpCall; import org.floens.chan.core.site.http.HttpCall;
import org.floens.chan.core.site.http.LoginRequest; import org.floens.chan.core.site.http.LoginRequest;
import org.floens.chan.core.site.http.LoginResponse; import org.floens.chan.core.site.http.LoginResponse;
@ -37,7 +38,7 @@ import org.floens.chan.utils.AndroidUtils;
import static org.floens.chan.utils.AndroidUtils.getString; import static org.floens.chan.utils.AndroidUtils.getString;
public class LoginController extends Controller implements View.OnClickListener, Site.LoginListener { public class LoginController extends Controller implements View.OnClickListener, SiteActions.LoginListener {
private LinearLayout container; private LinearLayout container;
private CrossfadeView crossfadeView; private CrossfadeView crossfadeView;
private TextView errors; private TextView errors;
@ -82,7 +83,7 @@ public class LoginController extends Controller implements View.OnClickListener,
bottomDescription.setText(Html.fromHtml(getString(R.string.setting_pass_bottom_description))); bottomDescription.setText(Html.fromHtml(getString(R.string.setting_pass_bottom_description)));
bottomDescription.setMovementMethod(LinkMovementMethod.getInstance()); bottomDescription.setMovementMethod(LinkMovementMethod.getInstance());
LoginRequest loginDetails = site.getLoginDetails(); LoginRequest loginDetails = site.actions().getLoginDetails();
inputToken.setText(loginDetails.user); inputToken.setText(loginDetails.user);
inputPin.setText(loginDetails.pass); inputPin.setText(loginDetails.pass);
@ -166,11 +167,11 @@ public class LoginController extends Controller implements View.OnClickListener,
String user = inputToken.getText().toString(); String user = inputToken.getText().toString();
String pass = inputPin.getText().toString(); String pass = inputPin.getText().toString();
site.login(new LoginRequest(user, pass), this); site.actions().login(new LoginRequest(user, pass), this);
} }
private void deauth() { private void deauth() {
site.logout(); site.actions().logout();
} }
private void showError(String error) { private void showError(String error) {
@ -184,6 +185,6 @@ public class LoginController extends Controller implements View.OnClickListener,
} }
private boolean loggedIn() { private boolean loggedIn() {
return site.isLoggedIn(); return site.actions().isLoggedIn();
} }
} }

@ -26,12 +26,14 @@ import org.floens.chan.R;
import org.floens.chan.controller.Controller; import org.floens.chan.controller.Controller;
import org.floens.chan.core.model.Post; import org.floens.chan.core.model.Post;
import org.floens.chan.core.site.Site; import org.floens.chan.core.site.Site;
import org.floens.chan.core.site.SiteRequestModifier;
import org.floens.chan.ui.helper.PostHelper; import org.floens.chan.ui.helper.PostHelper;
import okhttp3.HttpUrl; import okhttp3.HttpUrl;
public class ReportController extends Controller { public class ReportController extends Controller {
private Post post; private Post post;
private SiteRequestModifier siteRequestModifier;
public ReportController(Context context, Post post) { public ReportController(Context context, Post post) {
super(context); super(context);
@ -49,7 +51,10 @@ public class ReportController extends Controller {
WebView webView = new WebView(context); WebView webView = new WebView(context);
site.requestModifier().modifyWebView(webView); siteRequestModifier = site.requestModifier();
if (siteRequestModifier != null) {
siteRequestModifier.modifyWebView(webView);
}
WebSettings settings = webView.getSettings(); WebSettings settings = webView.getSettings();
settings.setJavaScriptEnabled(true); settings.setJavaScriptEnabled(true);

@ -37,16 +37,16 @@ import android.widget.LinearLayout;
import android.widget.TextView; import android.widget.TextView;
import org.floens.chan.R; import org.floens.chan.R;
import org.floens.chan.core.model.PostImage;
import org.floens.chan.core.site.common.ChanParser;
import org.floens.chan.core.site.common.DefaultFutabaChanParserHandler;
import org.floens.chan.core.site.common.FutabaChanParser;
import org.floens.chan.controller.Controller; import org.floens.chan.controller.Controller;
import org.floens.chan.core.model.orm.Board;
import org.floens.chan.core.model.orm.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.model.PostLinkable; import org.floens.chan.core.model.PostLinkable;
import org.floens.chan.core.model.orm.Board;
import org.floens.chan.core.model.orm.Loadable;
import org.floens.chan.core.settings.ChanSettings; import org.floens.chan.core.settings.ChanSettings;
import org.floens.chan.core.site.parser.CommentParser;
import org.floens.chan.core.site.common.DefaultPostParser;
import org.floens.chan.core.site.parser.PostParser;
import org.floens.chan.ui.activity.StartActivity; import org.floens.chan.ui.activity.StartActivity;
import org.floens.chan.ui.cell.PostCell; import org.floens.chan.ui.cell.PostCell;
import org.floens.chan.ui.theme.Theme; import org.floens.chan.ui.theme.Theme;
@ -123,11 +123,16 @@ public class ThemeSettingsController extends Controller implements View.OnClickL
} }
}; };
private ChanParser.Callback parserCallback = new ChanParser.Callback() { private PostParser.Callback parserCallback = new PostParser.Callback() {
@Override @Override
public boolean isSaved(int postNo) { public boolean isSaved(int postNo) {
return false; return false;
} }
@Override
public boolean isInternal(int postNo) {
return false;
}
}; };
private ViewPager pager; private ViewPager pager;
@ -275,7 +280,7 @@ public class ThemeSettingsController extends Controller implements View.OnClickL
"http://example.com/" + "http://example.com/" +
"<br>" + "<br>" +
"Phasellus consequat semper sodales. Donec dolor lectus, aliquet nec mollis vel, rutrum vel enim."); "Phasellus consequat semper sodales. Donec dolor lectus, aliquet nec mollis vel, rutrum vel enim.");
Post post = new FutabaChanParser(new DefaultFutabaChanParserHandler()).parse(theme, builder, parserCallback); Post post = new DefaultPostParser(new CommentParser()).parse(theme, builder, parserCallback);
LinearLayout linearLayout = new LinearLayout(themeContext); LinearLayout linearLayout = new LinearLayout(themeContext);
linearLayout.setOrientation(LinearLayout.VERTICAL); linearLayout.setOrientation(LinearLayout.VERTICAL);

@ -146,7 +146,7 @@ public abstract class ThreadController extends Controller implements
NdefMessage message = null; NdefMessage message = null;
if (loadable != null) { if (loadable != null) {
url = loadable.site.desktopUrl(loadable, null); url = loadable.site.resolvable().desktopUrl(loadable, null);
} }
if (url != null) { if (url != null) {

@ -203,7 +203,7 @@ public class ViewThreadController extends ThreadController implements ThreadLayo
case SHARE_ID: case SHARE_ID:
case OPEN_BROWSER_ID: case OPEN_BROWSER_ID:
Loadable loadable = threadLayout.getPresenter().getLoadable(); Loadable loadable = threadLayout.getPresenter().getLoadable();
String link = loadable.site.desktopUrl(loadable, null); String link = loadable.site.resolvable().desktopUrl(loadable, null);
if (id == SHARE_ID) { if (id == SHARE_ID) {
AndroidUtils.shareLink(link); AndroidUtils.shareLink(link);

@ -42,7 +42,7 @@ import org.floens.chan.R;
import org.floens.chan.core.model.ChanThread; import org.floens.chan.core.model.ChanThread;
import org.floens.chan.core.model.orm.Loadable; import org.floens.chan.core.model.orm.Loadable;
import org.floens.chan.core.presenter.ReplyPresenter; import org.floens.chan.core.presenter.ReplyPresenter;
import org.floens.chan.core.site.Authentication; import org.floens.chan.core.site.SiteAuthentication;
import org.floens.chan.core.site.Site; import org.floens.chan.core.site.Site;
import org.floens.chan.core.site.http.Reply; import org.floens.chan.core.site.http.Reply;
import org.floens.chan.ui.activity.StartActivity; import org.floens.chan.ui.activity.StartActivity;
@ -254,7 +254,7 @@ public class ReplyLayout extends LoadView implements View.OnClickListener, Reply
} }
@Override @Override
public void initializeAuthentication(Site site, Authentication authentication, public void initializeAuthentication(Site site, SiteAuthentication authentication,
AuthenticationLayoutCallback callback) { AuthenticationLayoutCallback callback) {
if (authenticationLayout == null) { if (authenticationLayout == null) {
switch (authentication.type) { switch (authentication.type) {

@ -263,7 +263,7 @@ public class ThreadLayout extends CoordinatorLayout implements
final List<PostLinkable> linkables = post.linkables; 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.toString();
} }
new AlertDialog.Builder(getContext()) new AlertDialog.Builder(getContext())

@ -26,11 +26,12 @@ import android.support.design.widget.FloatingActionButton;
import android.widget.ImageView; import android.widget.ImageView;
import org.floens.chan.R; import org.floens.chan.R;
import org.floens.chan.core.site.parser.PostParser;
import org.floens.chan.utils.AndroidUtils; import org.floens.chan.utils.AndroidUtils;
/** /**
* A Theme<br> * A Theme<br>
* Used for setting the toolbar color, and passed around {@link org.floens.chan.core.site.common.ChanParser} to give the spans the correct color.<br> * Used for setting the toolbar color, and passed around {@link PostParser} to give the spans the correct color.<br>
* Technically should the parser not do UI, but it is important that the spans do not get created on an UI thread for performance. * Technically should the parser not do UI, but it is important that the spans do not get created on an UI thread for performance.
*/ */
public class Theme { public class Theme {

@ -22,6 +22,7 @@ import android.app.ActivityManager;
import android.graphics.Bitmap; import android.graphics.Bitmap;
import android.graphics.BitmapFactory; import android.graphics.BitmapFactory;
import android.os.Build; import android.os.Build;
import android.support.annotation.AnyThread;
import org.floens.chan.R; import org.floens.chan.R;
import org.floens.chan.core.settings.ChanSettings; import org.floens.chan.core.settings.ChanSettings;
@ -101,6 +102,7 @@ public class ThemeHelper {
} }
} }
@AnyThread
public Theme getTheme() { public Theme getTheme() {
return theme; return theme;
} }

@ -13,8 +13,5 @@ allprojects {
repositories { repositories {
google() google()
jcenter() jcenter()
maven {
url 'https://maven.google.com'
}
} }
} }

Loading…
Cancel
Save