diff --git a/Clover/app/src/main/java/org/floens/chan/Chan.java b/Clover/app/src/main/java/org/floens/chan/Chan.java index 15d8d89f..bc3822a1 100644 --- a/Clover/app/src/main/java/org/floens/chan/Chan.java +++ b/Clover/app/src/main/java/org/floens/chan/Chan.java @@ -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); diff --git a/Clover/app/src/main/java/org/floens/chan/core/database/DatabaseHelper.java b/Clover/app/src/main/java/org/floens/chan/core/database/DatabaseHelper.java index c63bf34e..7e33bef8 100644 --- a/Clover/app/src/main/java/org/floens/chan/core/database/DatabaseHelper.java +++ b/Clover/app/src/main/java/org/floens/chan/core/database/DatabaseHelper.java @@ -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(); } } diff --git a/Clover/app/src/main/java/org/floens/chan/core/manager/WatchManager.java b/Clover/app/src/main/java/org/floens/chan/core/manager/WatchManager.java index eb78a372..9ee4a05b 100644 --- a/Clover/app/src/main/java/org/floens/chan/core/manager/WatchManager.java +++ b/Clover/app/src/main/java/org/floens/chan/core/manager/WatchManager.java @@ -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 posts = new ArrayList<>(); private final List quotes = new ArrayList<>(); diff --git a/Clover/app/src/main/java/org/floens/chan/core/model/PostLinkable.java b/Clover/app/src/main/java/org/floens/chan/core/model/PostLinkable.java index 3931aee9..ea93d1ce 100644 --- a/Clover/app/src/main/java/org/floens/chan/core/model/PostLinkable.java +++ b/Clover/app/src/main/java/org/floens/chan/core/model/PostLinkable.java @@ -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.
+ * A Clickable span that handles post clicks. These are created in PostParser for post quotes, spoilers etc.
* 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; diff --git a/Clover/app/src/main/java/org/floens/chan/core/pool/ChanLoaderFactory.java b/Clover/app/src/main/java/org/floens/chan/core/pool/ChanLoaderFactory.java index 5d0e6a6e..b4584437 100644 --- a/Clover/app/src/main/java/org/floens/chan/core/pool/ChanLoaderFactory.java +++ b/Clover/app/src/main/java/org/floens/chan/core/pool/ChanLoaderFactory.java @@ -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. - *

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)}. + *

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 threadLoaders = new HashMap<>(); - private LruCache threadLoadersCache = new LruCache<>(THREAD_LOADERS_CACHE_SIZE); + private Map threadLoaders = new HashMap<>(); + private LruCache 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"); diff --git a/Clover/app/src/main/java/org/floens/chan/core/presenter/ReplyPresenter.java b/Clover/app/src/main/java/org/floens/chan/core/presenter/ReplyPresenter.java index 103c66cb..109443e2 100644 --- a/Clover/app/src/main/java/org/floens/chan/core/presenter/ReplyPresenter.java +++ b/Clover/app/src/main/java/org/floens/chan/core/presenter/ReplyPresenter.java @@ -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(); diff --git a/Clover/app/src/main/java/org/floens/chan/core/presenter/SiteSetupPresenter.java b/Clover/app/src/main/java/org/floens/chan/core/presenter/SiteSetupPresenter.java index 2a9ae74c..534a51f6 100644 --- a/Clover/app/src/main/java/org/floens/chan/core/presenter/SiteSetupPresenter.java +++ b/Clover/app/src/main/java/org/floens/chan/core/presenter/SiteSetupPresenter.java @@ -38,7 +38,7 @@ public class SiteSetupPresenter { public void show() { setBoardCount(callback, site); if (hasLogin) { - callback.setIsLoggedIn(site.isLoggedIn()); + callback.setIsLoggedIn(site.actions().isLoggedIn()); } } diff --git a/Clover/app/src/main/java/org/floens/chan/core/presenter/SitesSetupPresenter.java b/Clover/app/src/main/java/org/floens/chan/core/presenter/SitesSetupPresenter.java index 665ed1de..2b9a9bbd 100644 --- a/Clover/app/src/main/java/org/floens/chan/core/presenter/SitesSetupPresenter.java +++ b/Clover/app/src/main/java/org/floens/chan/core/presenter/SitesSetupPresenter.java @@ -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 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); diff --git a/Clover/app/src/main/java/org/floens/chan/core/presenter/ThreadPresenter.java b/Clover/app/src/main/java/org/floens/chan/core/presenter/ThreadPresenter.java index eb7e7a41..ab25d311 100644 --- a/Clover/app/src/main/java/org/floens/chan/core/presenter/ThreadPresenter.java +++ b/Clover/app/src/main/java/org/floens/chan/core/presenter/ThreadPresenter.java @@ -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; diff --git a/Clover/app/src/main/java/org/floens/chan/core/site/Resolvable.java b/Clover/app/src/main/java/org/floens/chan/core/site/Resolvable.java deleted file mode 100644 index a91c1f13..00000000 --- a/Clover/app/src/main/java/org/floens/chan/core/site/Resolvable.java +++ /dev/null @@ -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 getSiteClass(); -} diff --git a/Clover/app/src/main/java/org/floens/chan/core/site/Site.java b/Clover/app/src/main/java/org/floens/chan/core/site/Site.java index dc33d39c..a262bf36 100644 --- a/Clover/app/src/main/java/org/floens/chan/core/site/Site.java +++ b/Clover/app/src/main/java/org/floens/chan/core/site/Site.java @@ -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. - *

- *

Some sites know beforehand if you need to authenticate, some sites only report it - * after posting. That's why there are two methods.

- * - * @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); - } } diff --git a/Clover/app/src/main/java/org/floens/chan/core/site/SiteActions.java b/Clover/app/src/main/java/org/floens/chan/core/site/SiteActions.java new file mode 100644 index 00000000..6a20d309 --- /dev/null +++ b/Clover/app/src/main/java/org/floens/chan/core/site/SiteActions.java @@ -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 . + */ +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. + *

+ *

Some sites know beforehand if you need to authenticate, some sites only report it + * after posting. That's why there are two methods.

+ * + * @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); + } +} diff --git a/Clover/app/src/main/java/org/floens/chan/core/site/Authentication.java b/Clover/app/src/main/java/org/floens/chan/core/site/SiteAuthentication.java similarity index 65% rename from Clover/app/src/main/java/org/floens/chan/core/site/Authentication.java rename to Clover/app/src/main/java/org/floens/chan/core/site/SiteAuthentication.java index 70efb0c5..8fde4f9e 100644 --- a/Clover/app/src/main/java/org/floens/chan/core/site/Authentication.java +++ b/Clover/app/src/main/java/org/floens/chan/core/site/SiteAuthentication.java @@ -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; } } diff --git a/Clover/app/src/main/java/org/floens/chan/core/site/SiteBase.java b/Clover/app/src/main/java/org/floens/chan/core/site/SiteBase.java index a5044ea5..c5b5e712 100644 --- a/Clover/app/src/main/java/org/floens/chan/core/site/SiteBase.java +++ b/Clover/app/src/main/java/org/floens/chan/core/site/SiteBase.java @@ -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; - } } diff --git a/Clover/app/src/main/java/org/floens/chan/core/site/SiteResolver.java b/Clover/app/src/main/java/org/floens/chan/core/site/SiteResolver.java index 51a68fe7..9959e9b2 100644 --- a/Clover/app/src/main/java/org/floens/chan/core/site/SiteResolver.java +++ b/Clover/app/src/main/java/org/floens/chan/core/site/SiteResolver.java @@ -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 resolvables = Sites.RESOLVABLES; + public SiteResolverResult resolveSiteForUrl(String url) { + List 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, diff --git a/Clover/app/src/main/java/org/floens/chan/core/site/SiteManager.java b/Clover/app/src/main/java/org/floens/chan/core/site/SiteService.java similarity index 98% rename from Clover/app/src/main/java/org/floens/chan/core/site/SiteManager.java rename to Clover/app/src/main/java/org/floens/chan/core/site/SiteService.java index 09a60048..605db46d 100644 --- a/Clover/app/src/main/java/org/floens/chan/core/site/SiteManager.java +++ b/Clover/app/src/main/java/org/floens/chan/core/site/SiteService.java @@ -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; diff --git a/Clover/app/src/main/java/org/floens/chan/core/site/SiteUrlHandler.java b/Clover/app/src/main/java/org/floens/chan/core/site/SiteUrlHandler.java new file mode 100644 index 00000000..543e3923 --- /dev/null +++ b/Clover/app/src/main/java/org/floens/chan/core/site/SiteUrlHandler.java @@ -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 . + */ +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 getSiteClass(); + + boolean matchesName(String value); + + boolean respondsTo(HttpUrl url); + + String desktopUrl(Loadable loadable, @Nullable Post post); + + Loadable resolveLoadable(Site site, HttpUrl url); +} diff --git a/Clover/app/src/main/java/org/floens/chan/core/site/Sites.java b/Clover/app/src/main/java/org/floens/chan/core/site/Sites.java index df663410..c6c8232e 100644 --- a/Clover/app/src/main/java/org/floens/chan/core/site/Sites.java +++ b/Clover/app/src/main/java/org/floens/chan/core/site/Sites.java @@ -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 RESOLVABLES = new ArrayList<>(); + public static final List 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 ALL_SITES = Collections.unmodifiableList(new ArrayList()); diff --git a/Clover/app/src/main/java/org/floens/chan/core/site/common/ChanReader.java b/Clover/app/src/main/java/org/floens/chan/core/site/common/ChanReader.java deleted file mode 100644 index b80ce543..00000000 --- a/Clover/app/src/main/java/org/floens/chan/core/site/common/ChanReader.java +++ /dev/null @@ -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; -} diff --git a/Clover/app/src/main/java/org/floens/chan/core/site/common/CommonSite.java b/Clover/app/src/main/java/org/floens/chan/core/site/common/CommonSite.java new file mode 100644 index 00000000..3aa35c72 --- /dev/null +++ b/Clover/app/src/main/java/org/floens/chan/core/site/common/CommonSite.java @@ -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 . + */ +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 arg) { + return null; + } + + @Override + public HttpUrl thumbnailUrl(Post.Builder post, boolean spoiler, Map arg) { + return null; + } + + @Override + public HttpUrl icon(Post.Builder post, String icon, Map 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() { + @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) { + } + } +} diff --git a/Clover/app/src/main/java/org/floens/chan/core/site/common/DefaultFutabaChanParserHandler.java b/Clover/app/src/main/java/org/floens/chan/core/site/common/DefaultFutabaChanParserHandler.java deleted file mode 100644 index 7d99fb3a..00000000 --- a/Clover/app/src/main/java/org/floens/chan/core/site/common/DefaultFutabaChanParserHandler.java +++ /dev/null @@ -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 . - */ -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 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

Your fortune: - // manually add these
- 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 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 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 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; - } - } -} diff --git a/Clover/app/src/main/java/org/floens/chan/core/site/common/FutabaChanParser.java b/Clover/app/src/main/java/org/floens/chan/core/site/common/DefaultPostParser.java similarity index 62% rename from Clover/app/src/main/java/org/floens/chan/core/site/common/FutabaChanParser.java rename to Clover/app/src/main/java/org/floens/chan/core/site/common/DefaultPostParser.java index 2ae1de28..78f2070f 100644 --- a/Clover/app/src/main/java/org/floens/chan/core/site/common/FutabaChanParser.java +++ b/Clover/app/src/main/java/org/floens/chan/core/site/common/DefaultPostParser.java @@ -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 innerNodes = node.childNodes(); - List 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 innerNodes = node.childNodes(); + List 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 ""; // ? } } - - } diff --git a/Clover/app/src/main/java/org/floens/chan/core/site/common/FutabaChanParserHandler.java b/Clover/app/src/main/java/org/floens/chan/core/site/common/FutabaChanParserHandler.java deleted file mode 100644 index c84890d5..00000000 --- a/Clover/app/src/main/java/org/floens/chan/core/site/common/FutabaChanParserHandler.java +++ /dev/null @@ -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 . - */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; - } -} diff --git a/Clover/app/src/main/java/org/floens/chan/core/site/common/FutabaChanReader.java b/Clover/app/src/main/java/org/floens/chan/core/site/common/FutabaChanReader.java index e56dad36..4ba503a0 100644 --- a/Clover/app/src/main/java/org/floens/chan/core/site/common/FutabaChanReader.java +++ b/Clover/app/src/main/java/org/floens/chan/core/site/common/FutabaChanReader.java @@ -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) diff --git a/Clover/app/src/main/java/org/floens/chan/core/site/common/MultipartHttpCall.java b/Clover/app/src/main/java/org/floens/chan/core/site/common/MultipartHttpCall.java new file mode 100644 index 00000000..4c243290 --- /dev/null +++ b/Clover/app/src/main/java/org/floens/chan/core/site/common/MultipartHttpCall.java @@ -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 . + */ +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()); + } +} diff --git a/Clover/app/src/main/java/org/floens/chan/core/site/http/HttpCall.java b/Clover/app/src/main/java/org/floens/chan/core/site/http/HttpCall.java index d6192e15..a44c7381 100644 --- a/Clover/app/src/main/java/org/floens/chan/core/site/http/HttpCall.java +++ b/Clover/app/src/main/java/org/floens/chan/core/site/http/HttpCall.java @@ -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 callback) { diff --git a/Clover/app/src/main/java/org/floens/chan/core/site/http/Reply.java b/Clover/app/src/main/java/org/floens/chan/core/site/http/Reply.java index 480256eb..8131a606 100644 --- a/Clover/app/src/main/java/org/floens/chan/core/site/http/Reply.java +++ b/Clover/app/src/main/java/org/floens/chan/core/site/http/Reply.java @@ -45,4 +45,5 @@ public class Reply { public String comment = ""; public int selection; public boolean spoilerImage = false; + public String password = ""; } diff --git a/Clover/app/src/main/java/org/floens/chan/core/site/http/ReplyResponse.java b/Clover/app/src/main/java/org/floens/chan/core/site/http/ReplyResponse.java index d82c0a0c..f70f2a12 100644 --- a/Clover/app/src/main/java/org/floens/chan/core/site/http/ReplyResponse.java +++ b/Clover/app/src/main/java/org/floens/chan/core/site/http/ReplyResponse.java @@ -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 { /** diff --git a/Clover/app/src/main/java/org/floens/chan/core/site/loader/ChanLoaderRequestParams.java b/Clover/app/src/main/java/org/floens/chan/core/site/loader/ChanLoaderRequestParams.java index 3bd1b86d..5f3d8e0e 100644 --- a/Clover/app/src/main/java/org/floens/chan/core/site/loader/ChanLoaderRequestParams.java +++ b/Clover/app/src/main/java/org/floens/chan/core/site/loader/ChanLoaderRequestParams.java @@ -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 { /** diff --git a/Clover/app/src/main/java/org/floens/chan/core/site/loader/ChanLoader.java b/Clover/app/src/main/java/org/floens/chan/core/site/loader/ChanThreadLoader.java similarity index 96% rename from Clover/app/src/main/java/org/floens/chan/core/site/loader/ChanLoader.java rename to Clover/app/src/main/java/org/floens/chan/core/site/loader/ChanThreadLoader.java index 50ea05b5..90910abd 100644 --- a/Clover/app/src/main/java/org/floens/chan/core/site/loader/ChanLoader.java +++ b/Clover/app/src/main/java/org/floens/chan/core/site/loader/ChanThreadLoader.java @@ -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. *

Obtain ChanLoaders with {@link org.floens.chan.core.pool.ChanLoaderFactory}. *

ChanLoaders can load boards and threads, and return {@link ChanThread} objects on success, through * {@link ChanLoaderCallback}. *

For threads timers can be started with {@link #setTimer()} to do a request later. */ -public class ChanLoader implements Response.ErrorListener, Response.Listener { - private static final String TAG = "ChanLoader"; +public class ChanThreadLoader implements Response.ErrorListener, Response.Listener { + 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.ListenerDo not call this constructor yourself, obtain ChanLoaders through {@link org.floens.chan.core.pool.ChanLoaderFactory} */ - public ChanLoader(Loadable loadable) { + public ChanThreadLoader(Loadable loadable) { this.loadable = loadable; inject(this); diff --git a/Clover/app/src/main/java/org/floens/chan/core/site/parser/ChanReader.java b/Clover/app/src/main/java/org/floens/chan/core/site/parser/ChanReader.java new file mode 100644 index 00000000..4e433348 --- /dev/null +++ b/Clover/app/src/main/java/org/floens/chan/core/site/parser/ChanReader.java @@ -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 . + */ +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; +} diff --git a/Clover/app/src/main/java/org/floens/chan/core/site/common/ChanReaderProcessingQueue.java b/Clover/app/src/main/java/org/floens/chan/core/site/parser/ChanReaderProcessingQueue.java similarity index 61% rename from Clover/app/src/main/java/org/floens/chan/core/site/common/ChanReaderProcessingQueue.java rename to Clover/app/src/main/java/org/floens/chan/core/site/parser/ChanReaderProcessingQueue.java index bbe68093..1db3a46d 100644 --- a/Clover/app/src/main/java/org/floens/chan/core/site/common/ChanReaderProcessingQueue.java +++ b/Clover/app/src/main/java/org/floens/chan/core/site/parser/ChanReaderProcessingQueue.java @@ -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 . + */ +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 cachedByNo = new HashMap<>(); private Loadable loadable; diff --git a/Clover/app/src/main/java/org/floens/chan/core/site/common/ChanReaderRequest.java b/Clover/app/src/main/java/org/floens/chan/core/site/parser/ChanReaderRequest.java similarity index 91% rename from Clover/app/src/main/java/org/floens/chan/core/site/common/ChanReaderRequest.java rename to Clover/app/src/main/java/org/floens/chan/core/site/parser/ChanReaderRequest.java index 8968a417..dd4ead5f 100644 --- a/Clover/app/src/main/java/org/floens/chan/core/site/common/ChanReaderRequest.java +++ b/Clover/app/src/main/java/org/floens/chan/core/site/parser/ChanReaderRequest.java @@ -15,7 +15,7 @@ * You should have received a copy of the GNU General Public License * along with this program. If not, see . */ -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 { List 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 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> 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()) { diff --git a/Clover/app/src/main/java/org/floens/chan/core/site/parser/CommentParser.java b/Clover/app/src/main/java/org/floens/chan/core/site/parser/CommentParser.java new file mode 100644 index 00000000..b992e9df --- /dev/null +++ b/Clover/app/src/main/java/org/floens/chan/core/site/parser/CommentParser.java @@ -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 . + */ +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 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

Your fortune: + // manually add these
+ 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 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 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; + } +} diff --git a/Clover/app/src/main/java/org/floens/chan/core/site/common/ChanParserHelper.java b/Clover/app/src/main/java/org/floens/chan/core/site/parser/CommentParserHelper.java similarity index 97% rename from Clover/app/src/main/java/org/floens/chan/core/site/common/ChanParserHelper.java rename to Clover/app/src/main/java/org/floens/chan/core/site/parser/CommentParserHelper.java index 1f73a9e8..aeb21019 100644 --- a/Clover/app/src/main/java/org/floens/chan/core/site/common/ChanParserHelper.java +++ b/Clover/app/src/main/java/org/floens/chan/core/site/parser/CommentParserHelper.java @@ -15,8 +15,9 @@ * You should have received a copy of the GNU General Public License * along with this program. If not, see . */ -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(); diff --git a/Clover/app/src/main/java/org/floens/chan/core/site/common/PostParseCallable.java b/Clover/app/src/main/java/org/floens/chan/core/site/parser/PostParseCallable.java similarity index 87% rename from Clover/app/src/main/java/org/floens/chan/core/site/common/PostParseCallable.java rename to Clover/app/src/main/java/org/floens/chan/core/site/parser/PostParseCallable.java index ed44d7a8..a8ac4256 100644 --- a/Clover/app/src/main/java/org/floens/chan/core/site/common/PostParseCallable.java +++ b/Clover/app/src/main/java/org/floens/chan/core/site/parser/PostParseCallable.java @@ -15,14 +15,15 @@ * You should have received a copy of the GNU General Public License * along with this program. If not, see . */ -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 { private DatabaseSavedReplyManager savedReplyManager; private Post.Builder post; private ChanReader reader; + private final Set internalIds; public PostParseCallable(FilterEngine filterEngine, List filters, DatabaseSavedReplyManager savedReplyManager, Post.Builder post, - ChanReader reader) { + ChanReader reader, Set 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.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); + } }); } diff --git a/Clover/app/src/main/java/org/floens/chan/core/site/common/ChanParser.java b/Clover/app/src/main/java/org/floens/chan/core/site/parser/PostParser.java similarity index 74% rename from Clover/app/src/main/java/org/floens/chan/core/site/common/ChanParser.java rename to Clover/app/src/main/java/org/floens/chan/core/site/parser/PostParser.java index 7fb8e979..9ff65073 100644 --- a/Clover/app/src/main/java/org/floens/chan/core/site/common/ChanParser.java +++ b/Clover/app/src/main/java/org/floens/chan/core/site/parser/PostParser.java @@ -15,15 +15,23 @@ * You should have received a copy of the GNU General Public License * along with this program. If not, see . */ -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); } } diff --git a/Clover/app/src/main/java/org/floens/chan/core/site/sites/chan4/Chan4.java b/Clover/app/src/main/java/org/floens/chan/core/site/sites/chan4/Chan4.java index e2fcc831..ce0353b4 100644 --- a/Clover/app/src/main/java/org/floens/chan/core/site/sites/chan4/Chan4.java +++ b/Clover/app/src/main/java/org/floens/chan/core/site/sites/chan4/Chan4.java @@ -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 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 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 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() { + @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() { + @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() { + @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 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 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() { - @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() { - @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() { - @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; } } diff --git a/Clover/app/src/main/java/org/floens/chan/core/site/sites/vichan/ViChan.java b/Clover/app/src/main/java/org/floens/chan/core/site/sites/vichan/ViChan.java index 14f46f1c..c2f1c60b 100644 --- a/Clover/app/src/main/java/org/floens/chan/core/site/sites/vichan/ViChan.java +++ b/Clover/app/src/main/java/org/floens/chan/core/site/sites/vichan/ViChan.java @@ -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 . - */ 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 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 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 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 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 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 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 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 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(".*

Error

.*]*>(.*?).*").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 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 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() { - @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 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; + } } } diff --git a/Clover/app/src/main/java/org/floens/chan/core/site/sites/vichan/ViChanCommentParser.java b/Clover/app/src/main/java/org/floens/chan/core/site/sites/vichan/ViChanCommentParser.java new file mode 100644 index 00000000..2ef49c78 --- /dev/null +++ b/Clover/app/src/main/java/org/floens/chan/core/site/sites/vichan/ViChanCommentParser.java @@ -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 . + */ +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; + } + } +} diff --git a/Clover/app/src/main/java/org/floens/chan/core/site/sites/vichan/ViChanParserHandler.java b/Clover/app/src/main/java/org/floens/chan/core/site/sites/vichan/ViChanParserHandler.java deleted file mode 100644 index 84c6d82a..00000000 --- a/Clover/app/src/main/java/org/floens/chan/core/site/sites/vichan/ViChanParserHandler.java +++ /dev/null @@ -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 . - */ -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 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; - } - } -} diff --git a/Clover/app/src/main/java/org/floens/chan/core/site/sites/vichan/ViChanReplyHttpCall.java b/Clover/app/src/main/java/org/floens/chan/core/site/sites/vichan/ViChanReplyHttpCall.java deleted file mode 100644 index 8fbcaa81..00000000 --- a/Clover/app/src/main/java/org/floens/chan/core/site/sites/vichan/ViChanReplyHttpCall.java +++ /dev/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 . - */ -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(".*

Error

.*]*>(.*?)<\\/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 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."; - } - } - } -} diff --git a/Clover/app/src/main/java/org/floens/chan/ui/activity/StartActivity.java b/Clover/app/src/main/java/org/floens/chan/ui/activity/StartActivity.java index 66846331..49689e84 100644 --- a/Clover/app/src/main/java/org/floens/chan/ui/activity/StartActivity.java +++ b/Clover/app/src/main/java/org/floens/chan/ui/activity/StartActivity.java @@ -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) { diff --git a/Clover/app/src/main/java/org/floens/chan/ui/captcha/CaptchaLayout.java b/Clover/app/src/main/java/org/floens/chan/ui/captcha/CaptchaLayout.java index 7ffd7079..a3c0af4b 100644 --- a/Clover/app/src/main/java/org/floens/chan/ui/captcha/CaptchaLayout.java +++ b/Clover/app/src/main/java/org/floens/chan/ui/captcha/CaptchaLayout.java @@ -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; diff --git a/Clover/app/src/main/java/org/floens/chan/ui/captcha/CaptchaNojsLayout.java b/Clover/app/src/main/java/org/floens/chan/ui/captcha/CaptchaNojsLayout.java index 4b8555ff..254b0daa 100644 --- a/Clover/app/src/main/java/org/floens/chan/ui/captcha/CaptchaNojsLayout.java +++ b/Clover/app/src/main/java/org/floens/chan/ui/captcha/CaptchaNojsLayout.java @@ -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; diff --git a/Clover/app/src/main/java/org/floens/chan/ui/captcha/GenericWebViewAuthenticationLayout.java b/Clover/app/src/main/java/org/floens/chan/ui/captcha/GenericWebViewAuthenticationLayout.java index 86021c51..6e717587 100644 --- a/Clover/app/src/main/java/org/floens/chan/ui/captcha/GenericWebViewAuthenticationLayout.java +++ b/Clover/app/src/main/java/org/floens/chan/ui/captcha/GenericWebViewAuthenticationLayout.java @@ -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) { diff --git a/Clover/app/src/main/java/org/floens/chan/ui/captcha/LegacyCaptchaLayout.java b/Clover/app/src/main/java/org/floens/chan/ui/captcha/LegacyCaptchaLayout.java index cfc308dd..e7e6e9b3 100644 --- a/Clover/app/src/main/java/org/floens/chan/ui/captcha/LegacyCaptchaLayout.java +++ b/Clover/app/src/main/java/org/floens/chan/ui/captcha/LegacyCaptchaLayout.java @@ -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; diff --git a/Clover/app/src/main/java/org/floens/chan/ui/controller/BrowseController.java b/Clover/app/src/main/java/org/floens/chan/ui/controller/BrowseController.java index 85c40a99..dbd7be71 100644 --- a/Clover/app/src/main/java/org/floens/chan/ui/controller/BrowseController.java +++ b/Clover/app/src/main/java/org/floens/chan/ui/controller/BrowseController.java @@ -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); diff --git a/Clover/app/src/main/java/org/floens/chan/ui/controller/LoginController.java b/Clover/app/src/main/java/org/floens/chan/ui/controller/LoginController.java index 7374346a..7257d28b 100644 --- a/Clover/app/src/main/java/org/floens/chan/ui/controller/LoginController.java +++ b/Clover/app/src/main/java/org/floens/chan/ui/controller/LoginController.java @@ -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(); } } diff --git a/Clover/app/src/main/java/org/floens/chan/ui/controller/ReportController.java b/Clover/app/src/main/java/org/floens/chan/ui/controller/ReportController.java index 80385353..9195e4fd 100644 --- a/Clover/app/src/main/java/org/floens/chan/ui/controller/ReportController.java +++ b/Clover/app/src/main/java/org/floens/chan/ui/controller/ReportController.java @@ -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); diff --git a/Clover/app/src/main/java/org/floens/chan/ui/controller/ThemeSettingsController.java b/Clover/app/src/main/java/org/floens/chan/ui/controller/ThemeSettingsController.java index d061ae57..864bd1dc 100644 --- a/Clover/app/src/main/java/org/floens/chan/ui/controller/ThemeSettingsController.java +++ b/Clover/app/src/main/java/org/floens/chan/ui/controller/ThemeSettingsController.java @@ -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/" + "
" + "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); diff --git a/Clover/app/src/main/java/org/floens/chan/ui/controller/ThreadController.java b/Clover/app/src/main/java/org/floens/chan/ui/controller/ThreadController.java index c11e01b8..8d4f178d 100644 --- a/Clover/app/src/main/java/org/floens/chan/ui/controller/ThreadController.java +++ b/Clover/app/src/main/java/org/floens/chan/ui/controller/ThreadController.java @@ -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) { diff --git a/Clover/app/src/main/java/org/floens/chan/ui/controller/ViewThreadController.java b/Clover/app/src/main/java/org/floens/chan/ui/controller/ViewThreadController.java index 6fc57ed1..73537e15 100644 --- a/Clover/app/src/main/java/org/floens/chan/ui/controller/ViewThreadController.java +++ b/Clover/app/src/main/java/org/floens/chan/ui/controller/ViewThreadController.java @@ -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); diff --git a/Clover/app/src/main/java/org/floens/chan/ui/layout/ReplyLayout.java b/Clover/app/src/main/java/org/floens/chan/ui/layout/ReplyLayout.java index 76acb698..0286f130 100644 --- a/Clover/app/src/main/java/org/floens/chan/ui/layout/ReplyLayout.java +++ b/Clover/app/src/main/java/org/floens/chan/ui/layout/ReplyLayout.java @@ -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) { diff --git a/Clover/app/src/main/java/org/floens/chan/ui/layout/ThreadLayout.java b/Clover/app/src/main/java/org/floens/chan/ui/layout/ThreadLayout.java index 7d174616..29dcb744 100644 --- a/Clover/app/src/main/java/org/floens/chan/ui/layout/ThreadLayout.java +++ b/Clover/app/src/main/java/org/floens/chan/ui/layout/ThreadLayout.java @@ -263,7 +263,7 @@ public class ThreadLayout extends CoordinatorLayout implements final List 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()) diff --git a/Clover/app/src/main/java/org/floens/chan/ui/theme/Theme.java b/Clover/app/src/main/java/org/floens/chan/ui/theme/Theme.java index b3343e5a..a25807cf 100644 --- a/Clover/app/src/main/java/org/floens/chan/ui/theme/Theme.java +++ b/Clover/app/src/main/java/org/floens/chan/ui/theme/Theme.java @@ -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
- * Used for setting the toolbar color, and passed around {@link org.floens.chan.core.site.common.ChanParser} to give the spans the correct color.
+ * Used for setting the toolbar color, and passed around {@link PostParser} to give the spans the correct color.
* 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 { diff --git a/Clover/app/src/main/java/org/floens/chan/ui/theme/ThemeHelper.java b/Clover/app/src/main/java/org/floens/chan/ui/theme/ThemeHelper.java index 4d290500..4402c6b0 100644 --- a/Clover/app/src/main/java/org/floens/chan/ui/theme/ThemeHelper.java +++ b/Clover/app/src/main/java/org/floens/chan/ui/theme/ThemeHelper.java @@ -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; } diff --git a/Clover/build.gradle b/Clover/build.gradle index 4e7d5fe7..277c10f0 100644 --- a/Clover/build.gradle +++ b/Clover/build.gradle @@ -13,8 +13,5 @@ allprojects { repositories { google() jcenter() - maven { - url 'https://maven.google.com' - } } }