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.NetModule;
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.Logger;
import org.floens.chan.utils.Time;
@ -53,7 +53,7 @@ public class Chan extends Application implements UserAgentProvider, Application.
private int activityForegroundCounter = 0;
@Inject
SiteManager siteManager;
SiteService siteService;
@Inject
DatabaseManager databaseManager;
@ -93,7 +93,7 @@ public class Chan extends Application implements UserAgentProvider, Application.
initializeGraph();
siteManager.initialize();
siteService.initialize();
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.SiteModel;
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 java.sql.SQLException;
@ -228,7 +228,7 @@ public class DatabaseHelper extends OrmLiteSqliteOpenHelper {
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.pool.ChanLoaderFactory;
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.service.WatchNotifier;
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 final Pin pin;
private ChanLoader chanLoader;
private ChanThreadLoader chanLoader;
private final List<Post> posts = 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;
/**
* 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,
* and handled if it was a PostLinkable.
*/
@ -37,14 +37,14 @@ public class PostLinkable extends ClickableSpan {
}
public final Theme theme;
public final String key;
public final CharSequence key;
public final Object value;
public final Type type;
private boolean spoilerVisible = ChanSettings.revealTextSpoilers.get();
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.key = key;
this.value = value;

@ -19,7 +19,7 @@ package org.floens.chan.core.pool;
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 java.util.HashMap;
@ -30,24 +30,24 @@ import javax.inject.Singleton;
/**
* ChanLoaderFactory is a factory for ChanLoaders. ChanLoaders for threads are cached.
* <p>Each reference to a loader is a {@link ChanLoader.ChanLoaderCallback}, these
* references can be obtained with {@link #obtain(Loadable, ChanLoader.ChanLoaderCallback)}} and released
* with {@link #release(ChanLoader, ChanLoader.ChanLoaderCallback)}.
* <p>Each reference to a loader is a {@link ChanThreadLoader.ChanLoaderCallback}, these
* references can be obtained with {@link #obtain(Loadable, ChanThreadLoader.ChanLoaderCallback)}} and released
* with {@link #release(ChanThreadLoader, ChanThreadLoader.ChanLoaderCallback)}.
*/
@Singleton
public class ChanLoaderFactory {
// private static final String TAG = "ChanLoaderFactory";
public static final int THREAD_LOADERS_CACHE_SIZE = 25;
private Map<Loadable, ChanLoader> threadLoaders = new HashMap<>();
private LruCache<Loadable, ChanLoader> threadLoadersCache = new LruCache<>(THREAD_LOADERS_CACHE_SIZE);
private Map<Loadable, ChanThreadLoader> threadLoaders = new HashMap<>();
private LruCache<Loadable, ChanThreadLoader> threadLoadersCache = new LruCache<>(THREAD_LOADERS_CACHE_SIZE);
@Inject
public ChanLoaderFactory() {
}
public ChanLoader obtain(Loadable loadable, ChanLoader.ChanLoaderCallback listener) {
ChanLoader chanLoader;
public ChanThreadLoader obtain(Loadable loadable, ChanThreadLoader.ChanLoaderCallback listener) {
ChanThreadLoader chanLoader;
if (loadable.isThreadMode()) {
if (!loadable.isFromDatabase()) {
throw new IllegalArgumentException();
@ -63,11 +63,11 @@ public class ChanLoaderFactory {
}
if (chanLoader == null) {
chanLoader = new ChanLoader(loadable);
chanLoader = new ChanThreadLoader(loadable);
threadLoaders.put(loadable, chanLoader);
}
} else {
chanLoader = new ChanLoader(loadable);
chanLoader = new ChanThreadLoader(loadable);
}
chanLoader.addListener(listener);
@ -75,10 +75,10 @@ public class ChanLoaderFactory {
return chanLoader;
}
public void release(ChanLoader chanLoader, ChanLoader.ChanLoaderCallback listener) {
public void release(ChanThreadLoader chanLoader, ChanThreadLoader.ChanLoaderCallback listener) {
Loadable loadable = chanLoader.getLoadable();
if (loadable.isThreadMode()) {
ChanLoader foundChanLoader = threadLoaders.get(loadable);
ChanThreadLoader foundChanLoader = threadLoaders.get(loadable);
if (foundChanLoader == null) {
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.SavedReply;
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.SiteActions;
import org.floens.chan.core.site.http.HttpCall;
import org.floens.chan.core.site.http.Reply;
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.getString;
public class ReplyPresenter implements AuthenticationLayoutCallback, ImagePickDelegate.ImagePickCallback, Site.PostListener {
public class ReplyPresenter implements AuthenticationLayoutCallback, ImagePickDelegate.ImagePickCallback, SiteActions.PostListener {
public enum Page {
INPUT,
AUTHENTICATION,
@ -195,7 +196,7 @@ public class ReplyPresenter implements AuthenticationLayoutCallback, ImagePickDe
draft.spoilerImage = draft.spoilerImage && board.spoilers;
draft.captchaResponse = null;
if (loadable.site.postRequiresAuthentication()) {
if (loadable.site.actions().postRequiresAuthentication()) {
switchPage(Page.AUTHENTICATION, true);
} else {
makeSubmitCall();
@ -369,7 +370,7 @@ public class ReplyPresenter implements AuthenticationLayoutCallback, ImagePickDe
}
private void makeSubmitCall() {
loadable.getSite().post(draft, this);
loadable.getSite().actions().post(draft, this);
switchPage(Page.LOADING, true);
}
@ -384,7 +385,7 @@ public class ReplyPresenter implements AuthenticationLayoutCallback, ImagePickDe
callback.setPage(Page.INPUT, animate);
break;
case AUTHENTICATION:
Authentication authentication = loadable.site.postAuthenticate();
SiteAuthentication authentication = loadable.site.actions().postAuthenticate();
callback.initializeAuthentication(loadable.site, authentication, this);
callback.setPage(Page.AUTHENTICATION, true);
@ -447,7 +448,7 @@ public class ReplyPresenter implements AuthenticationLayoutCallback, ImagePickDe
void setPage(Page page, boolean animate);
void initializeAuthentication(Site site, Authentication authentication,
void initializeAuthentication(Site site, SiteAuthentication authentication,
AuthenticationLayoutCallback callback);
void resetAuthentication();

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

@ -21,7 +21,6 @@ import android.text.TextUtils;
import org.floens.chan.Chan;
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.exception.ChanLoaderException;
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.settings.ChanSettings;
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.DeleteResponse;
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.PostsFilter;
import org.floens.chan.ui.cell.PostCellInterface;
@ -58,7 +59,7 @@ import javax.inject.Inject;
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_TEXT = 1;
private static final int POST_OPTION_INFO = 2;
@ -81,7 +82,7 @@ public class ThreadPresenter implements ChanLoader.ChanLoaderCallback, PostAdapt
private ChanLoaderFactory chanLoaderFactory;
private Loadable loadable;
private ChanLoader chanLoader;
private ChanThreadLoader chanLoader;
private boolean searchOpen = false;
private String searchQuery;
private PostsFilter.Order order = PostsFilter.Order.BUMP;
@ -252,7 +253,7 @@ public class ThreadPresenter implements ChanLoader.ChanLoaderCallback, PostAdapt
}
/*
* ChanLoader callbacks
* ChanThreadLoader callbacks
*/
@Override
public void onChanLoaderData(ChanThread result) {
@ -530,12 +531,12 @@ public class ThreadPresenter implements ChanLoader.ChanLoaderCallback, PostAdapt
watchManager.createPin(pinLoadable, post);
break;
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);
break;
}
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);
break;
}
@ -637,7 +638,7 @@ public class ThreadPresenter implements ChanLoader.ChanLoaderCallback, PostAdapt
);
if (reply != null) {
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
public void onDeleteComplete(HttpCall httpPost, DeleteResponse deleteResponse) {
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;
import android.support.annotation.Nullable;
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.Setting;
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.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;
import java.util.List;
import okhttp3.HttpUrl;
public interface Site {
enum Feature {
/**
* 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)
*/
POSTING,
@ -51,7 +43,7 @@ public interface Site {
/**
* This site supports deleting posts.
*
* @see #delete(DeleteRequest, DeleteListener)
* @see SiteActions#delete(DeleteRequest, SiteActions.DeleteListener)
* @see SiteEndpoints#delete(Post)
*/
POST_DELETE,
@ -66,7 +58,7 @@ public interface Site {
/**
* This site supports some sort of authentication (like 4pass).
*
* @see #login(LoginRequest, LoginListener)
* @see SiteActions#login(LoginRequest, SiteActions.LoginListener)
* @see SiteEndpoints#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 {
/**
@ -134,9 +126,9 @@ public interface Site {
SiteIcon icon();
Resolvable resolvable();
BoardsType boardsType();
Loadable resolveLoadable(HttpUrl url);
SiteUrlHandler resolvable();
boolean feature(Feature feature);
@ -148,15 +140,9 @@ public interface Site {
SiteRequestModifier requestModifier();
BoardsType boardsType();
String desktopUrl(Loadable loadable, @Nullable Post post);
void boards(BoardsListener boardsListener);
ChanReader chanReader();
interface BoardsListener {
void onBoardsReceived(Boards boards);
}
SiteActions actions();
/**
* Return the board for this site with the given {@code code}.
@ -178,56 +164,4 @@ public interface Site {
* @return the created board.
*/
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;
public class Authentication {
public class SiteAuthentication {
public enum Type {
NONE,
CAPTCHA1,
@ -26,33 +26,33 @@ public class Authentication {
GENERIC_WEBVIEW
}
public static Authentication fromNone() {
return new Authentication(Type.NONE);
public static SiteAuthentication fromNone() {
return new SiteAuthentication(Type.NONE);
}
public static Authentication fromCaptcha1(String siteKey, String baseUrl) {
Authentication a = new Authentication(Type.CAPTCHA1);
public static SiteAuthentication fromCaptcha1(String siteKey, String baseUrl) {
SiteAuthentication a = new SiteAuthentication(Type.CAPTCHA1);
a.siteKey = siteKey;
a.baseUrl = baseUrl;
return a;
}
public static Authentication fromCaptcha2(String siteKey, String baseUrl) {
Authentication a = new Authentication(Type.CAPTCHA2);
public static SiteAuthentication fromCaptcha2(String siteKey, String baseUrl) {
SiteAuthentication a = new SiteAuthentication(Type.CAPTCHA2);
a.siteKey = siteKey;
a.baseUrl = baseUrl;
return a;
}
public static Authentication fromCaptcha2nojs(String siteKey, String baseUrl) {
Authentication a = new Authentication(Type.CAPTCHA2_NOJS);
public static SiteAuthentication fromCaptcha2nojs(String siteKey, String baseUrl) {
SiteAuthentication a = new SiteAuthentication(Type.CAPTCHA2_NOJS);
a.siteKey = siteKey;
a.baseUrl = baseUrl;
return a;
}
public static Authentication fromUrl(String url, String retryText, String successText) {
Authentication a = new Authentication(Type.GENERIC_WEBVIEW);
public static SiteAuthentication fromUrl(String url, String retryText, String successText) {
SiteAuthentication a = new SiteAuthentication(Type.GENERIC_WEBVIEW);
a.url = url;
a.retryText = retryText;
a.successText = successText;
@ -70,7 +70,7 @@ public class Authentication {
public String retryText;
public String successText;
private Authentication(Type type) {
private SiteAuthentication(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.json.JsonSettings;
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.LoginRequest;
import org.floens.chan.core.site.http.Reply;
import java.util.ArrayList;
import java.util.Collections;
@ -66,16 +63,16 @@ public abstract class SiteBase implements Site {
requestQueue = injector.instance(RequestQueue.class);
boardManager = injector.instance(BoardManager.class);
loadableProvider = injector.instance(LoadableProvider.class);
SiteManager siteManager = injector.instance(SiteManager.class);
SiteService siteService = injector.instance(SiteService.class);
settingsProvider = new JsonSettingsProvider(userSettings, () -> {
siteManager.updateUserSettings(this, userSettings);
siteService.updateUserSettings(this, userSettings);
});
initializeSettings();
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));
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;
}
Site findSiteForUrl(String url) {
public Site findSiteForUrl(String url) {
HttpUrl httpUrl = sanitizeUrl(url);
if (httpUrl == null) {
for (Site site : Sites.allSites()) {
Resolvable resolvable = site.resolvable();
SiteUrlHandler siteUrlHandler = site.resolvable();
if (resolvable.matchesName(url)) {
if (siteUrlHandler.matchesName(url)) {
return site;
}
}
@ -57,8 +57,8 @@ public class SiteResolver {
}
for (Site site : Sites.allSites()) {
Resolvable resolvable = site.resolvable();
if (resolvable.respondsTo(httpUrl)) {
SiteUrlHandler siteUrlHandler = site.resolvable();
if (siteUrlHandler.respondsTo(httpUrl)) {
return site;
}
}
@ -66,16 +66,16 @@ public class SiteResolver {
return null;
}
SiteResolverResult resolveSiteForUrl(String url) {
List<Resolvable> resolvables = Sites.RESOLVABLES;
public SiteResolverResult resolveSiteForUrl(String url) {
List<SiteUrlHandler> siteUrlHandlers = Sites.URL_HANDLERS;
HttpUrl httpUrl = sanitizeUrl(url);
if (httpUrl == null) {
for (Resolvable resolvable : resolvables) {
if (resolvable.matchesName(url)) {
for (SiteUrlHandler siteUrlHandler : siteUrlHandlers) {
if (siteUrlHandler.matchesName(url)) {
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();
}
for (Resolvable resolvable : resolvables) {
if (resolvable.respondsTo(httpUrl)) {
for (SiteUrlHandler siteUrlHandler : siteUrlHandlers) {
if (siteUrlHandler.respondsTo(httpUrl)) {
return new SiteResolverResult(SiteResolverResult.Match.BUILTIN,
resolvable.getSiteClass(), null);
siteUrlHandler.getSiteClass(), null);
}
}
@ -106,7 +106,8 @@ public class SiteResolver {
for (Site site : Sites.allSites()) {
if (site.resolvable().respondsTo(httpUrl)) {
Loadable resolved = site.resolveLoadable(httpUrl);
Loadable resolved = loadableProvider.get(
site.resolvable().resolveLoadable(site, httpUrl));
if (resolved != null) {
return new LoadableResult(resolved);
@ -133,7 +134,7 @@ public class SiteResolver {
return httpUrl;
}
static class SiteResolverResult {
public static class SiteResolverResult {
enum Match {
NONE,
BUILTIN,

@ -32,7 +32,7 @@ import javax.inject.Inject;
import javax.inject.Singleton;
@Singleton
public class SiteManager {
public class SiteService {
private static boolean addSiteForLegacy = false;
/**
@ -48,7 +48,7 @@ public class SiteManager {
private boolean initialized = false;
@Inject
public SiteManager(SiteRepository siteRepository,
public SiteService(SiteRepository siteRepository,
SiteResolver resolver) {
this.siteRepository = siteRepository;
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 org.floens.chan.core.site.sites.chan4.Chan4;
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.Collections;
@ -21,11 +21,11 @@ public class Sites {
SITE_CLASSES.put(1, ViChan.class);
}
public static final List<Resolvable> RESOLVABLES = new ArrayList<>();
public static final List<SiteUrlHandler> URL_HANDLERS = new ArrayList<>();
static {
RESOLVABLES.add(Chan4.RESOLVABLE);
RESOLVABLES.add(ViChan.RESOLVABLE);
URL_HANDLERS.add(Chan4.SITE_URL_HANDLER);
URL_HANDLERS.add(ViChan.RESOLVABLE);
}
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;
import android.support.annotation.AnyThread;
import android.text.SpannableString;
import android.text.TextUtils;
import android.text.style.BackgroundColorSpan;
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.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.ForegroundColorSpanHashed;
import org.floens.chan.ui.theme.Theme;
@ -42,16 +45,14 @@ import java.util.List;
import static org.floens.chan.utils.AndroidUtils.sp;
public class FutabaChanParser implements ChanParser {
private static final String TAG = "FutabaChanParser";
private static final String SAVED_REPLY_SUFFIX = " (You)";
private static final String OP_REPLY_SUFFIX = " (OP)";
public static final String EXTERN_THREAD_LINK_SUFFIX = " \u2192"; // arrow to the right
@AnyThread
public class DefaultPostParser implements PostParser {
private static final String TAG = "DefaultPostParser";
private FutabaChanParserHandler handler;
private CommentParser commentParser;
public FutabaChanParser(FutabaChanParserHandler handler) {
this.handler = handler;
public DefaultPostParser(CommentParser commentParser) {
this.commentParser = commentParser;
}
@Override
@ -60,16 +61,12 @@ public class FutabaChanParser implements ChanParser {
theme = ThemeHelper.getInstance().getTheme();
}
try {
if (!TextUtils.isEmpty(builder.name)) {
builder.name = Parser.unescapeEntities(builder.name, false);
}
if (!TextUtils.isEmpty(builder.name)) {
builder.name = Parser.unescapeEntities(builder.name, false);
}
if (!TextUtils.isEmpty(builder.subject)) {
builder.subject = Parser.unescapeEntities(builder.subject, false);
}
} catch (Exception e) {
e.printStackTrace();
if (!TextUtils.isEmpty(builder.subject)) {
builder.subject = Parser.unescapeEntities(builder.subject, false);
}
parseSpans(theme, builder);
@ -209,104 +206,44 @@ public class FutabaChanParser implements ChanParser {
String text = ((TextNode) node).text();
SpannableString spannable = new SpannableString(text);
ChanParserHelper.detectLinks(theme, post, text, spannable);
CommentParserHelper.detectLinks(theme, post, text, spannable);
return spannable;
} else {
switch (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;
}
}
}
}
}
} else if (node instanceof Element) {
String nodeName = node.nodeName();
private CharSequence parseAnchor(Theme theme, Post.Builder post, Callback callback,
Element anchor) {
FutabaChanParserHandler.Link handlerLink =
handler.handleAnchor(this, theme, post, anchor);
// Recursively call parseNode with the nodes of the paragraph.
List<Node> innerNodes = node.childNodes();
List<CharSequence> texts = new ArrayList<>(innerNodes.size() + 1);
if (handlerLink != null) {
if (handlerLink.type == PostLinkable.Type.THREAD) {
handlerLink.key += EXTERN_THREAD_LINK_SUFFIX;
}
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;
for (Node innerNode : innerNodes) {
CharSequence nodeParsed = parseNode(theme, post, callback, innerNode);
if (nodeParsed != null) {
texts.add(nodeParsed);
}
}
SpannableString link = new SpannableString(handlerLink.key);
PostLinkable pl = new PostLinkable(theme, handlerLink.key, handlerLink.value, handlerLink.type);
link.setSpan(pl, 0, link.length(), 0);
post.addLinkable(pl);
return link;
// if (node.nextSibling() != null) {
// texts.add("\n");
// }
CharSequence allInnerText = TextUtils.concat(
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 {
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.PostImage;
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.util.ArrayList;
@ -20,19 +23,19 @@ import okhttp3.HttpUrl;
import static org.floens.chan.core.site.SiteEndpoints.makeArgument;
public class FutabaChanReader implements ChanReader {
private final ChanParser chanParser;
private final PostParser postParser;
public FutabaChanReader() {
this.chanParser = new FutabaChanParser(new DefaultFutabaChanParserHandler());
this.postParser = new DefaultPostParser(new CommentParser());
}
public FutabaChanReader(ChanParser chanParser) {
this.chanParser = chanParser;
public FutabaChanReader(PostParser postParser) {
this.postParser = postParser;
}
@Override
public ChanParser getParser() {
return chanParser;
public PostParser getParser() {
return postParser;
}
@Override
@ -226,7 +229,7 @@ public class FutabaChanReader implements ChanReader {
.thumbnailUrl(endpoints.thumbnailUrl(builder, false, args))
.spoilerThumbnailUrl(endpoints.thumbnailUrl(builder, true, args))
.imageUrl(endpoints.imageUrl(builder, args))
.filename(Parser.unescapeEntities(fileName, false))
.filename(org.jsoup.parser.Parser.unescapeEntities(fileName, false))
.extension(fileExt)
.imageWidth(fileWidth)
.imageHeight(fileHeight)
@ -331,7 +334,7 @@ public class FutabaChanReader implements ChanReader {
.thumbnailUrl(endpoints.thumbnailUrl(builder, false, args))
.spoilerThumbnailUrl(endpoints.thumbnailUrl(builder, true, args))
.imageUrl(endpoints.imageUrl(builder, args))
.filename(Parser.unescapeEntities(fileName, false))
.filename(org.jsoup.parser.Parser.unescapeEntities(fileName, false))
.extension(fileExt)
.imageWidth(fileWidth)
.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;
import android.os.Handler;
import android.os.Looper;
import org.floens.chan.core.site.Site;
import org.floens.chan.utils.AndroidUtils;
import org.floens.chan.utils.IOUtils;
import org.floens.chan.utils.Logger;
import java.io.IOException;
@ -42,7 +43,7 @@ public abstract class HttpCall implements Callback {
protected Site site;
private boolean successful = false;
private Handler handler = new Handler(Looper.getMainLooper());
private HttpCallback callback;
private Exception exception;
@ -61,18 +62,16 @@ public abstract class HttpCall implements Callback {
if (body != null) {
String responseString = body.string();
process(response, responseString);
successful = true;
} else {
throw new IOException("HTTP " + response.code());
exception = new IOException("No body. HTTP " + response.code());
}
} catch (Exception e) {
exception = e;
Logger.e(TAG, "IOException processing response", e);
exception = new IOException("Error processing response", e);
} finally {
IOUtils.closeQuietly(body);
}
if (successful) {
if (exception != null) {
callSuccess();
} else {
callFail(exception);
@ -88,28 +87,14 @@ public abstract class HttpCall implements Callback {
return exception;
}
public boolean isSuccessful() {
return successful;
}
@SuppressWarnings("unchecked")
private void callSuccess() {
AndroidUtils.runOnUiThread(new Runnable() {
@SuppressWarnings("unchecked")
@Override
public void run() {
callback.onHttpSuccess(HttpCall.this);
}
});
handler.post(() -> callback.onHttpSuccess(HttpCall.this));
}
@SuppressWarnings("unchecked")
private void callFail(final Exception e) {
AndroidUtils.runOnUiThread(new Runnable() {
@SuppressWarnings("unchecked")
@Override
public void run() {
callback.onHttpFail(HttpCall.this, e);
}
});
handler.post(() -> callback.onHttpFail(HttpCall.this, e));
}
public void setCallback(HttpCallback<? extends HttpCall> callback) {

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

@ -17,10 +17,12 @@
*/
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 {
/**

@ -22,12 +22,12 @@ import com.android.volley.Response;
import org.floens.chan.core.model.Post;
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;
/**
* A request from ChanLoader to load something.
* A request from ChanThreadLoader to load something.
*/
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.Post;
import org.floens.chan.core.model.orm.Loadable;
import org.floens.chan.core.site.common.ChanReader;
import org.floens.chan.core.site.common.ChanReaderRequest;
import org.floens.chan.core.site.parser.ChanReader;
import org.floens.chan.core.site.parser.ChanReaderRequest;
import org.floens.chan.ui.helper.PostHelper;
import org.floens.chan.utils.AndroidUtils;
import org.floens.chan.utils.Logger;
@ -46,14 +46,14 @@ import javax.inject.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>ChanLoaders can load boards and threads, and return {@link ChanThread} objects on success, through
* {@link ChanLoaderCallback}.
* <p>For threads timers can be started with {@link #setTimer()} to do a request later.
*/
public class ChanLoader implements Response.ErrorListener, Response.Listener<ChanLoaderResponse> {
private static final String TAG = "ChanLoader";
public class ChanThreadLoader implements Response.ErrorListener, Response.Listener<ChanLoaderResponse> {
private static final String TAG = "ChanThreadLoader";
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};
@ -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>
*/
public ChanLoader(Loadable loadable) {
public ChanThreadLoader(Loadable loadable) {
this.loadable = loadable;
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;
@ -11,7 +28,7 @@ import java.util.HashMap;
import java.util.List;
import java.util.Map;
class ChanReaderProcessingQueue {
public class ChanReaderProcessingQueue {
@SuppressLint("UseSparseArrays")
private Map<Integer, Post> cachedByNo = new HashMap<>();
private Loadable loadable;

@ -15,7 +15,7 @@
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <http://www.gnu.org/licenses/>.
*/
package org.floens.chan.core.site.common;
package org.floens.chan.core.site.parser;
import android.util.JsonReader;
@ -31,9 +31,12 @@ import org.floens.chan.core.site.loader.ChanLoaderResponse;
import org.floens.chan.utils.Time;
import java.util.ArrayList;
import java.util.Collections;
import java.util.HashMap;
import java.util.HashSet;
import java.util.List;
import java.util.Map;
import java.util.Set;
import java.util.concurrent.Callable;
import java.util.concurrent.ExecutionException;
import java.util.concurrent.ExecutorService;
@ -165,10 +168,29 @@ public class ChanReaderRequest extends JsonReaderRequest<ChanLoaderResponse> {
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());
for (int i = 0; i < toParse.size(); 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()) {

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

@ -15,14 +15,15 @@
* 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;
package org.floens.chan.core.site.parser;
import org.floens.chan.core.database.DatabaseSavedReplyManager;
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.orm.Filter;
import java.util.List;
import java.util.Set;
import java.util.concurrent.Callable;
// 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 Post.Builder post;
private ChanReader reader;
private final Set<Integer> internalIds;
public PostParseCallable(FilterEngine filterEngine,
List<Filter> filters,
DatabaseSavedReplyManager savedReplyManager,
Post.Builder post,
ChanReader reader) {
ChanReader reader, Set<Integer> internalIds) {
this.filterEngine = filterEngine;
this.filters = filters;
this.savedReplyManager = savedReplyManager;
this.post = post;
this.reader = reader;
this.internalIds = internalIds;
}
@Override
@ -55,15 +58,16 @@ class PostParseCallable implements Callable<Post> {
post.isSavedReply(savedReplyManager.isSaved(post.board, post.id));
// if (!post.parse(parser)) {
// Logger.e(TAG, "Incorrect data about post received for post " + post.no);
// return null;
// }
return reader.getParser().parse(null, post, new ChanParser.Callback() {
return reader.getParser().parse(null, post, new PostParser.Callback() {
@Override
public boolean isSaved(int 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
* 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.ui.theme.Theme;
public interface ChanParser {
public interface PostParser {
Post parse(Theme theme, Post.Builder builder, Callback callback);
interface Callback {
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.SharedPreferencesSettingProvider;
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.Resolvable;
import org.floens.chan.core.site.SiteUrlHandler;
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.SiteEndpoints;
import org.floens.chan.core.site.SiteIcon;
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.FutabaChanReader;
import org.floens.chan.core.site.http.DeleteRequest;
@ -61,7 +62,7 @@ import okhttp3.HttpUrl;
import okhttp3.Request;
public class Chan4 extends SiteBase {
public static final Resolvable RESOLVABLE = new Resolvable() {
public static final SiteUrlHandler SITE_URL_HANDLER = new SiteUrlHandler() {
@Override
public Class<? extends Site> getSiteClass() {
return Chan4.class;
@ -78,6 +79,67 @@ public class Chan4 extends SiteBase {
url.host().equals("www.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";
@ -223,7 +285,7 @@ public class Chan4 extends SiteBase {
private SiteRequestModifier siteRequestModifier = new SiteRequestModifier() {
@Override
public void modifyHttpCall(HttpCall httpCall, Request.Builder requestBuilder) {
if (isLoggedIn()) {
if (actions.isLoggedIn()) {
requestBuilder.addHeader("Cookie", "pass_id=" + passToken.get());
}
}
@ -238,7 +300,7 @@ public class Chan4 extends SiteBase {
CookieManager cookieManager = CookieManager.getInstance();
cookieManager.removeAllCookie();
if (isLoggedIn()) {
if (actions.isLoggedIn()) {
String[] passCookies = {
"pass_enabled=1;",
"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
private final StringSetting passUser;
private final StringSetting passPass;
@ -310,55 +480,8 @@ public class Chan4 extends SiteBase {
}
@Override
public Resolvable resolvable() {
return RESOLVABLE;
}
@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;
public SiteUrlHandler resolvable() {
return SITE_URL_HANDLER;
}
@Override
@ -387,21 +510,6 @@ public class Chan4 extends SiteBase {
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
public boolean boardFeature(BoardFeature boardFeature, Board board) {
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
public SiteEndpoints endpoints() {
@ -450,90 +541,7 @@ public class Chan4 extends SiteBase {
}
@Override
public void post(Reply reply, final PostListener postListener) {
httpCallManager.makeHttpCall(new Chan4ReplyCall(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 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());
public SiteActions actions() {
return actions;
}
}

@ -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;
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.PostHttpIcon;
import org.floens.chan.core.model.PostImage;
import org.floens.chan.core.model.orm.Board;
import org.floens.chan.core.model.orm.Loadable;
import org.floens.chan.core.site.Authentication;
import org.floens.chan.core.site.Resolvable;
import org.floens.chan.core.site.SiteAuthentication;
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.SiteIcon;
import org.floens.chan.core.site.SiteRequestModifier;
import org.floens.chan.core.site.common.ChanReader;
import org.floens.chan.core.site.common.CommonReplyHttpCall;
import org.floens.chan.core.site.common.FutabaChanParser;
import org.floens.chan.core.site.parser.ChanReader;
import org.floens.chan.core.site.parser.ChanReaderProcessingQueue;
import org.floens.chan.core.site.common.DefaultPostParser;
import org.floens.chan.core.site.common.FutabaChanReader;
import org.floens.chan.core.site.http.HttpCall;
import org.floens.chan.core.site.http.HttpCallManager;
import org.floens.chan.core.site.common.MultipartHttpCall;
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.Locale;
import java.util.Map;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
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 {
private static final String TAG = "ViChan";
public static final Resolvable RESOLVABLE = new Resolvable() {
public class ViChan extends CommonSite {
public static final CommonSiteUrlHandler RESOLVABLE = new CommonSiteUrlHandler() {
@Override
public Class<? extends Site> getSiteClass() {
return ViChan.class;
@ -67,252 +54,495 @@ public class ViChan extends SiteBase {
public boolean respondsTo(HttpUrl url) {
return url.host().equals("8ch.net");
}
};
private final SiteEndpoints endpoints = new SiteEndpoints() {
private final HttpUrl root = new HttpUrl.Builder()
.scheme("https")
.host("8ch.net")
.build();
@Override
public Loadable resolveLoadable(Site site, HttpUrl url) {
Matcher board = Pattern.compile("/(\\w+)")
.matcher(url.encodedPath());
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()
.scheme("https")
.host("media.8ch.net")
.build();
if (isEmpty(url.fragment())) {
l.markedNo = Integer.parseInt(url.fragment());
}
private final HttpUrl sys = new HttpUrl.Builder()
.scheme("https")
.host("sys.8ch.net")
.build();
return l;
} else if (board.find()) {
Board b = site.board(board.group(1));
if (b == null) {
return null;
}
@Override
public HttpUrl catalog(Board board) {
return root.newBuilder()
.addPathSegment(board.code)
.addPathSegment("catalog.json")
.build();
}
return Loadable.forCatalog(b);
}
} catch (NumberFormatException ignored) {
}
@Override
public HttpUrl thread(Board board, Loadable loadable) {
return root.newBuilder()
.addPathSegment(board.code)
.addPathSegment("res")
.addPathSegment(loadable.no + ".json")
.build();
return null;
}
@Override
public HttpUrl imageUrl(Post.Builder post, Map<String, String> arg) {
return root.newBuilder()
.addPathSegment("file_store")
.addPathSegment(arg.get("tim") + "." + arg.get("ext"))
.build();
public String desktopUrl(Loadable loadable, @Nullable Post post) {
if (loadable.isCatalogMode()) {
return "https://8ch.net/" + loadable.boardCode;
} else if (loadable.isThreadMode()) {
return "https://8ch.net/" + loadable.boardCode + "/res/" + loadable.no + ".html";
} else {
return "https://8ch.net/";
}
}
};
@Override
public HttpUrl thumbnailUrl(Post.Builder post, boolean spoiler, Map<String, String> arg) {
String ext;
switch (arg.get("ext")) {
case "jpeg":
case "jpg":
case "png":
case "gif":
ext = arg.get("ext");
break;
default:
ext = "jpg";
break;
private static final String TAG = "ViChan";
@Override
public ChanReader chanReader() {
return new FutabaChanReader(new DefaultPostParser(new ViChanCommentParser()));
}
@Override
public void setup() {
setName("8chan");
setIcon(SiteIcon.fromAssets("icons/8chan.png"));
setBoardsType(BoardsType.INFINITE);
setResolvable(RESOLVABLE);
setConfig(new CommonConfig() {
@Override
public boolean feature(Feature feature) {
return feature == Feature.POSTING;
}
});
return root.newBuilder()
.addPathSegment("file_store")
.addPathSegment("thumb")
.addPathSegment(arg.get("tim") + "." + ext)
.build();
}
setEndpoints(new CommonEndpoints() {
private final SimpleHttpUrl root = from("https://8ch.net");
private final SimpleHttpUrl sys = from("https://sys.8ch.net");
@Override
public HttpUrl icon(Post.Builder post, String icon, Map<String, String> arg) {
HttpUrl.Builder stat = root.newBuilder().addPathSegment("static");
switch (icon) {
case "country":
stat.addPathSegment("flags");
stat.addPathSegment(arg.get("country_code").toLowerCase(Locale.ENGLISH) + ".png");
break;
@Override
public HttpUrl catalog(Board board) {
return root.builder().s(board.code).s("catalog.json").url();
}
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
public HttpUrl boards() {
return null;
}
@Override
public HttpUrl imageUrl(Post.Builder post, Map<String, String> arg) {
return root.builder().s("file_store").s(arg.get("tim") + "." + arg.get("ext")).url();
}
@Override
public HttpUrl reply(Loadable loadable) {
return sys.newBuilder()
.addPathSegment("post.php")
.build();
}
@Override
public HttpUrl thumbnailUrl(Post.Builder post, boolean spoiler, Map<String, String> arg) {
String ext;
switch (arg.get("ext")) {
case "jpeg":
case "jpg":
case "png":
case "gif":
ext = arg.get("ext");
break;
default:
ext = "jpg";
break;
}
@Override
public HttpUrl delete(Post post) {
return null;
}
return root.builder().s("file_store").s("thumb").s(arg.get("tim") + "." + ext).url();
}
@Override
public HttpUrl report(Post post) {
return null;
}
@Override
public HttpUrl icon(Post.Builder post, String icon, Map<String, String> arg) {
SimpleHttpUrl stat = root.builder().s("static");
@Override
public HttpUrl login() {
return null;
}
};
if (icon.equals("country")) {
stat.s("flags").s(arg.get("country_code").toLowerCase(Locale.ENGLISH) + ".png");
}
private SiteRequestModifier siteRequestModifier = new SiteRequestModifier() {
@Override
public void modifyHttpCall(HttpCall httpCall, Request.Builder requestBuilder) {
}
return stat.url();
}
@SuppressWarnings("deprecation")
@Override
public void modifyWebView(WebView webView) {
}
};
@Override
public HttpUrl reply(Loadable loadable) {
return sys.builder().s("post.php").url();
}
@Override
public String name() {
return "8chan";
}
});
@Override
public SiteIcon icon() {
return SiteIcon.fromAssets("icons/8chan.png");
}
setActions(new CommonActions() {
@Override
public void setupPost(Reply reply, MultipartHttpCall call) {
call.parameter("board", reply.loadable.board.code);
@Override
public Resolvable resolvable() {
return RESOLVABLE;
}
if (reply.loadable.isThreadMode()) {
call.parameter("post", "New Reply");
call.parameter("thread", String.valueOf(reply.loadable.no));
} else {
call.parameter("post", "New Thread");
call.parameter("page", "1");
}
@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;
call.parameter("pwd", reply.password);
call.parameter("name", reply.name);
call.parameter("email", reply.options);
if (!reply.loadable.isThreadMode() && !isEmpty(reply.subject)) {
call.parameter("subject", reply.subject);
}
call.parameter("body", reply.comment);
if (reply.file != null) {
call.fileParameter("file", reply.fileName, reply.file);
}
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 {
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) {
replyResponse.errorMessage = "Error posting: could not find posted thread.";
}
}
}
int post = -1;
String fragment = url.fragment();
if (fragment != null) {
try {
post = Integer.parseInt(fragment);
} catch (NumberFormatException ignored) {
}
}
@Override
public SiteAuthentication postAuthenticate() {
return SiteAuthentication.fromUrl("https://8ch.net/dnsbls_bypass.php",
"You failed the CAPTCHA",
"You may now go back and make your post");
}
});
if (no >= 0) {
Loadable loadable = loadableProvider.get(
Loadable.forThread(this, board, no));
if (post >= 0) {
loadable.markedNo = post;
}
setApi(new ViChanApi());
setParser(new CommonParser() {
@Override
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
public boolean feature(Feature feature) {
switch (feature) {
case POSTING:
return true;
default:
return false;
}
}
while (reader.hasNext()) {
reader.beginObject(); // Page object
@Override
public boolean boardFeature(BoardFeature boardFeature, Board board) {
return false;
}
while (reader.hasNext()) {
if (reader.nextName().equals("threads")) {
reader.beginArray(); // Threads array
@Override
public SiteEndpoints endpoints() {
return endpoints;
}
while (reader.hasNext()) {
readPostObject(reader, queue);
}
@Override
public SiteRequestModifier requestModifier() {
return siteRequestModifier;
}
reader.endArray();
} else {
reader.skipValue();
}
}
@Override
public BoardsType boardsType() {
return BoardsType.INFINITE;
}
reader.endObject();
}
@Override
public String desktopUrl(Loadable loadable, @Nullable Post post) {
return "https://8ch.net/";
}
reader.endArray();
}
@Override
public void boards(BoardsListener boardsListener) {
}
@Override
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
public ChanReader chanReader() {
FutabaChanParser parser = new FutabaChanParser(new ViChanParserHandler());
return new FutabaChanReader(parser);
}
reader.endArray();
break;
default:
// 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
public void post(Reply reply, final PostListener postListener) {
// TODO
HttpCallManager httpCallManager = injector().instance(HttpCallManager.class);
httpCallManager.makeHttpCall(new ViChanReplyHttpCall(this, reply),
new HttpCall.HttpCallback<CommonReplyHttpCall>() {
@Override
public void onHttpSuccess(CommonReplyHttpCall httpPost) {
postListener.onPostComplete(httpPost, httpPost.replyResponse);
}
builder.images(files);
if (builder.op) {
// Update OP fields later on the main thread
Post.Builder op = new Post.Builder();
op.closed(builder.closed);
op.archived(builder.archived);
op.sticky(builder.sticky);
op.replies(builder.replies);
op.images(builder.imagesCount);
op.uniqueIps(builder.uniqueIps);
queue.setOp(op);
}
@Override
public void onHttpFail(CommonReplyHttpCall httpPost, Exception e) {
Logger.e(TAG, "post error", e);
Post cached = queue.getCachedPost(builder.id);
if (cached != null) {
// 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
public Authentication postAuthenticate() {
return Authentication.fromUrl("https://8ch.net/dnsbls_bypass.php",
"You failed the CAPTCHA",
"You may now go back and make your post");
if (trollCountryCode != null && countryName != null) {
HttpUrl countryUrl = endpoints.icon(builder, "troll_country",
makeArgument("troll_country_code", trollCountryCode));
builder.addHttpIcon(new PostHttpIcon(countryUrl, countryName));
}
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.settings.ChanSettings;
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.Sites;
import org.floens.chan.ui.controller.BrowseController;
@ -96,7 +96,7 @@ public class StartActivity extends AppCompatActivity implements NfcAdapter.Creat
SiteResolver siteResolver;
@Inject
SiteManager siteManager;
SiteService siteService;
@Override
protected void onCreate(Bundle savedInstanceState) {
@ -151,7 +151,7 @@ public class StartActivity extends AppCompatActivity implements NfcAdapter.Creat
}
private void restoreFresh() {
if (!siteManager.areSitesSetup()) {
if (!siteService.areSitesSetup()) {
SitesSetupController setupController = new SitesSetupController(this);
if (drawerController.childControllers.get(0) instanceof DoubleNavigationController) {

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

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

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

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

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

@ -29,6 +29,7 @@ import android.widget.TextView;
import org.floens.chan.R;
import org.floens.chan.controller.Controller;
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.LoginRequest;
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;
public class LoginController extends Controller implements View.OnClickListener, Site.LoginListener {
public class LoginController extends Controller implements View.OnClickListener, SiteActions.LoginListener {
private LinearLayout container;
private CrossfadeView crossfadeView;
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.setMovementMethod(LinkMovementMethod.getInstance());
LoginRequest loginDetails = site.getLoginDetails();
LoginRequest loginDetails = site.actions().getLoginDetails();
inputToken.setText(loginDetails.user);
inputPin.setText(loginDetails.pass);
@ -166,11 +167,11 @@ public class LoginController extends Controller implements View.OnClickListener,
String user = inputToken.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() {
site.logout();
site.actions().logout();
}
private void showError(String error) {
@ -184,6 +185,6 @@ public class LoginController extends Controller implements View.OnClickListener,
}
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.core.model.Post;
import org.floens.chan.core.site.Site;
import org.floens.chan.core.site.SiteRequestModifier;
import org.floens.chan.ui.helper.PostHelper;
import okhttp3.HttpUrl;
public class ReportController extends Controller {
private Post post;
private SiteRequestModifier siteRequestModifier;
public ReportController(Context context, Post post) {
super(context);
@ -49,7 +51,10 @@ public class ReportController extends Controller {
WebView webView = new WebView(context);
site.requestModifier().modifyWebView(webView);
siteRequestModifier = site.requestModifier();
if (siteRequestModifier != null) {
siteRequestModifier.modifyWebView(webView);
}
WebSettings settings = webView.getSettings();
settings.setJavaScriptEnabled(true);

@ -37,16 +37,16 @@ import android.widget.LinearLayout;
import android.widget.TextView;
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.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.PostImage;
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.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.cell.PostCell;
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
public boolean isSaved(int postNo) {
return false;
}
@Override
public boolean isInternal(int postNo) {
return false;
}
};
private ViewPager pager;
@ -275,7 +280,7 @@ public class ThemeSettingsController extends Controller implements View.OnClickL
"http://example.com/" +
"<br>" +
"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.setOrientation(LinearLayout.VERTICAL);

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

@ -203,7 +203,7 @@ public class ViewThreadController extends ThreadController implements ThreadLayo
case SHARE_ID:
case OPEN_BROWSER_ID:
Loadable loadable = threadLayout.getPresenter().getLoadable();
String link = loadable.site.desktopUrl(loadable, null);
String link = loadable.site.resolvable().desktopUrl(loadable, null);
if (id == SHARE_ID) {
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.orm.Loadable;
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.http.Reply;
import org.floens.chan.ui.activity.StartActivity;
@ -254,7 +254,7 @@ public class ReplyLayout extends LoadView implements View.OnClickListener, Reply
}
@Override
public void initializeAuthentication(Site site, Authentication authentication,
public void initializeAuthentication(Site site, SiteAuthentication authentication,
AuthenticationLayoutCallback callback) {
if (authenticationLayout == null) {
switch (authentication.type) {

@ -263,7 +263,7 @@ public class ThreadLayout extends CoordinatorLayout implements
final List<PostLinkable> linkables = post.linkables;
String[] keys = new String[linkables.size()];
for (int i = 0; i < linkables.size(); i++) {
keys[i] = linkables.get(i).key;
keys[i] = linkables.get(i).key.toString();
}
new AlertDialog.Builder(getContext())

@ -26,11 +26,12 @@ import android.support.design.widget.FloatingActionButton;
import android.widget.ImageView;
import org.floens.chan.R;
import org.floens.chan.core.site.parser.PostParser;
import org.floens.chan.utils.AndroidUtils;
/**
* 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.
*/
public class Theme {

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

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

Loading…
Cancel
Save