diff --git a/Clover/app/src/main/java/org/floens/chan/chan/ChanHelper.java b/Clover/app/src/main/java/org/floens/chan/chan/ChanHelper.java index 2ed7a9ec..974de6a5 100644 --- a/Clover/app/src/main/java/org/floens/chan/chan/ChanHelper.java +++ b/Clover/app/src/main/java/org/floens/chan/chan/ChanHelper.java @@ -22,7 +22,10 @@ import android.net.Uri; import org.floens.chan.Chan; import org.floens.chan.core.database.DatabaseLoadableManager; import org.floens.chan.core.manager.BoardManager; +import org.floens.chan.core.model.Board; import org.floens.chan.core.model.Loadable; +import org.floens.chan.core.site.Site; +import org.floens.chan.core.site.Sites; import java.util.List; @@ -32,14 +35,18 @@ public class ChanHelper { List parts = uri.getPathSegments(); + // TODO(multi-site) get correct site + Site site = Sites.defaultSite(); + if (parts.size() > 0) { String rawBoard = parts.get(0); BoardManager boardManager = Chan.getBoardManager(); DatabaseLoadableManager loadableManager = Chan.getDatabaseManager().getDatabaseLoadableManager(); - if (boardManager.getBoardExists(rawBoard)) { + Board board = site.board(rawBoard); + if (board != null) { if (parts.size() == 1 || (parts.size() == 2 && "catalog".equals(parts.get(1)))) { // Board mode - loadable = loadableManager.get(Loadable.forCatalog(rawBoard)); + loadable = loadableManager.get(Loadable.forCatalog(board)); } else if (parts.size() >= 3) { // Thread mode int no = -1; @@ -62,7 +69,7 @@ public class ChanHelper { } if (no >= 0) { - loadable = loadableManager.get(Loadable.forThread(rawBoard, no)); + loadable = loadableManager.get(Loadable.forThread(site, board, no)); if (post >= 0) { loadable.markedNo = post; } diff --git a/Clover/app/src/main/java/org/floens/chan/chan/ChanLoader.java b/Clover/app/src/main/java/org/floens/chan/chan/ChanLoader.java index 74cdee35..336cac59 100644 --- a/Clover/app/src/main/java/org/floens/chan/chan/ChanLoader.java +++ b/Clover/app/src/main/java/org/floens/chan/chan/ChanLoader.java @@ -308,7 +308,7 @@ public class ChanLoader implements Response.ErrorListener, Response.Listener cached = thread == null ? new ArrayList() : thread.posts; ChanReaderRequest request = ChanReaderRequest.newInstance(loadable, cached, this, this); diff --git a/Clover/app/src/main/java/org/floens/chan/chan/ChanParser.java b/Clover/app/src/main/java/org/floens/chan/chan/ChanParser.java index 5158917e..f92a115e 100644 --- a/Clover/app/src/main/java/org/floens/chan/chan/ChanParser.java +++ b/Clover/app/src/main/java/org/floens/chan/chan/ChanParser.java @@ -420,7 +420,7 @@ public class ChanParser { } // Append You when it's a reply to an saved reply - if (databaseManager.getDatabaseSavedReplyManager().isSaved(post.board, id)) { + if (databaseManager.getDatabaseSavedReplyManager().isSaved(post.boardId, id)) { key += " (You)"; } } diff --git a/Clover/app/src/main/java/org/floens/chan/chan/ChanUrls.java b/Clover/app/src/main/java/org/floens/chan/chan/ChanUrls.java index 56262fa3..3c4a08d8 100644 --- a/Clover/app/src/main/java/org/floens/chan/chan/ChanUrls.java +++ b/Clover/app/src/main/java/org/floens/chan/chan/ChanUrls.java @@ -19,49 +19,11 @@ package org.floens.chan.chan; import org.floens.chan.core.settings.ChanSettings; -import java.util.Locale; - public class ChanUrls { - public static String getCatalogUrl(String board) { - return scheme() + "://a.4cdn.org/" + board + "/catalog.json"; - } - - public static String getPageUrl(String board, int pageNumber) { - return scheme() + "://a.4cdn.org/" + board + "/" + (pageNumber + 1) + ".json"; - } - - public static String getThreadUrl(String board, int no) { - return scheme() + "://a.4cdn.org/" + board + "/thread/" + no + ".json"; - } - public static String getCaptchaSiteKey() { return "6Ldp2bsSAAAAAAJ5uyx_lx34lJeEpTLVkP5k04qc"; } - public static String getImageUrl(String board, String code, String extension) { - return scheme() + "://i.4cdn.org/" + board + "/" + code + "." + extension; - } - - public static String getThumbnailUrl(String board, String code) { - return scheme() + "://t.4cdn.org/" + board + "/" + code + "s.jpg"; - } - - public static String getSpoilerUrl() { - return scheme() + "://s.4cdn.org/image/spoiler.png"; - } - - public static String getCustomSpoilerUrl(String board, int value) { - return scheme() + "://s.4cdn.org/image/spoiler-" + board + value + ".png"; - } - - public static String getCountryFlagUrl(String countryCode) { - return scheme() + "://s.4cdn.org/image/country/" + countryCode.toLowerCase(Locale.ENGLISH) + ".gif"; - } - - public static String getBoardsUrl() { - return scheme() + "://a.4cdn.org/boards.json"; - } - public static String getReplyUrl(String board) { return "https://sys.4chan.org/" + board + "/post"; } diff --git a/Clover/app/src/main/java/org/floens/chan/core/database/DatabaseBoardManager.java b/Clover/app/src/main/java/org/floens/chan/core/database/DatabaseBoardManager.java new file mode 100644 index 00000000..cdabe2e2 --- /dev/null +++ b/Clover/app/src/main/java/org/floens/chan/core/database/DatabaseBoardManager.java @@ -0,0 +1,83 @@ +package org.floens.chan.core.database; + +import org.floens.chan.core.model.Board; +import org.floens.chan.core.site.Site; +import org.floens.chan.core.site.Sites; +import org.floens.chan.utils.Logger; + +import java.sql.SQLException; +import java.util.List; +import java.util.concurrent.Callable; + +public class DatabaseBoardManager { + private static final String TAG = "DatabaseBoardManager"; + + private DatabaseManager databaseManager; + private DatabaseHelper helper; + + public DatabaseBoardManager(DatabaseManager databaseManager, DatabaseHelper helper) { + this.databaseManager = databaseManager; + this.helper = helper; + } + + /** + * Save the boards listed in the database.
+ * The boards need to either come from a {@link Site} or from {@link #getBoards(Site)}. + * + * @param boards boards to set or update + * @return void + */ + public Callable setBoards(final List boards) { + return new Callable() { + @Override + public Void call() throws Exception { + for (Board b : boards) { + helper.boardsDao.createOrUpdate(b); + } + + return null; + } + }; + } + + public Callable> getBoards(final Site site) { + return new Callable>() { + @Override + public List call() throws Exception { + List boards = null; + try { + boards = helper.boardsDao.queryBuilder() + .where().eq("site", site.id()) + .query(); + for (int i = 0; i < boards.size(); i++) { + Board board = boards.get(i); + board.site = site; + } + } catch (SQLException e) { + Logger.e(TAG, "Error getting boards from db", e); + } + + return boards; + } + }; + } + + public Callable> getAllBoards() { + return new Callable>() { + @Override + public List call() throws Exception { + List boards = null; + try { + boards = helper.boardsDao.queryForAll(); + for (int i = 0; i < boards.size(); i++) { + Board board = boards.get(i); + board.site = Sites.forId(board.siteId); + } + } catch (SQLException e) { + Logger.e(TAG, "Error getting boards from db", e); + } + return boards; + } + }; + } +} 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 eb2999fb..dc337e42 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 @@ -43,7 +43,7 @@ public class DatabaseHelper extends OrmLiteSqliteOpenHelper { private static final String TAG = "DatabaseHelper"; private static final String DATABASE_NAME = "ChanDB"; - private static final int DATABASE_VERSION = 21; + private static final int DATABASE_VERSION = 22; public Dao pinDao; public Dao loadableDao; @@ -203,6 +203,17 @@ public class DatabaseHelper extends OrmLiteSqliteOpenHelper { Logger.e(TAG, "Error upgrading to version 21", e); } } + + if (oldVersion < 22) { + try { + boardsDao.executeRawNoArgs("ALTER TABLE loadable ADD COLUMN site INTEGER default 0;"); + boardsDao.executeRawNoArgs("ALTER TABLE board ADD COLUMN site INTEGER default 0;"); + boardsDao.executeRawNoArgs("ALTER TABLE savedreply ADD COLUMN site INTEGER default 0;"); + boardsDao.executeRawNoArgs("ALTER TABLE threadhide ADD COLUMN site INTEGER default 0;"); + } catch (SQLException e) { + Logger.e(TAG, "Error upgrading to version 22", e); + } + } } public void reset() { diff --git a/Clover/app/src/main/java/org/floens/chan/core/database/DatabaseLoadableManager.java b/Clover/app/src/main/java/org/floens/chan/core/database/DatabaseLoadableManager.java index 0a4ac9fc..17e91cbf 100644 --- a/Clover/app/src/main/java/org/floens/chan/core/database/DatabaseLoadableManager.java +++ b/Clover/app/src/main/java/org/floens/chan/core/database/DatabaseLoadableManager.java @@ -22,6 +22,7 @@ import android.util.Log; import com.j256.ormlite.stmt.QueryBuilder; import org.floens.chan.core.model.Loadable; +import org.floens.chan.core.site.Sites; import org.floens.chan.utils.Logger; import org.floens.chan.utils.Time; @@ -94,7 +95,7 @@ public class DatabaseLoadableManager { if (loadable.isThreadMode()) { long start = Time.startTiming(); Loadable result = databaseManager.runTaskSync(getLoadable(loadable)); - Time.endTiming("get loadable from db " + loadable.board, start); + Time.endTiming("get loadable from db " + loadable.boardCode, start); return result; } else { return loadable; @@ -123,6 +124,8 @@ public class DatabaseLoadableManager { // Add it to the cache, refresh contents helper.loadableDao.refresh(loadable); + loadable.site = Sites.forId(loadable.siteId); + loadable.board = loadable.site.board(loadable.boardCode); cachedLoadables.put(loadable, loadable); return loadable; } @@ -142,8 +145,9 @@ public class DatabaseLoadableManager { } else { QueryBuilder builder = helper.loadableDao.queryBuilder(); List results = builder.where() + .eq("site", loadable.siteId).and() .eq("mode", loadable.mode) - .and().eq("board", loadable.board) + .and().eq("board", loadable.boardCode) .and().eq("no", loadable.no) .query(); @@ -161,6 +165,8 @@ public class DatabaseLoadableManager { result = loadable; } else { Log.d(TAG, "Loadable found in db"); + result.site = Sites.forId(result.siteId); + result.board = result.site.board(result.boardCode); } cachedLoadables.put(result, result); diff --git a/Clover/app/src/main/java/org/floens/chan/core/database/DatabaseManager.java b/Clover/app/src/main/java/org/floens/chan/core/database/DatabaseManager.java index 92d8e7d9..25474b36 100644 --- a/Clover/app/src/main/java/org/floens/chan/core/database/DatabaseManager.java +++ b/Clover/app/src/main/java/org/floens/chan/core/database/DatabaseManager.java @@ -26,7 +26,6 @@ import com.j256.ormlite.misc.TransactionManager; import com.j256.ormlite.table.TableUtils; import org.floens.chan.Chan; -import org.floens.chan.core.model.Board; import org.floens.chan.core.model.Post; import org.floens.chan.core.model.ThreadHide; import org.floens.chan.utils.Logger; @@ -44,8 +43,14 @@ import java.util.concurrent.Future; import de.greenrobot.event.EventBus; -import static com.j256.ormlite.misc.TransactionManager.callInTransaction; - +/** + * The central point for database related access.
+ * All database queries are run on a single database thread, therefor all functions return a + * {@link Callable} that needs to be queued on either {@link #runTask(Callable)}, + * {@link #runTask(Callable, TaskResult)} or {@link #runTaskSync(Callable)}.
+ * You often want the sync flavour for queries that return data, it waits for the task to be finished on the other thread.
+ * Use the async versions when you don't care when the query is done. + */ public class DatabaseManager { private static final String TAG = "DatabaseManager"; @@ -63,6 +68,7 @@ public class DatabaseManager { private final DatabaseHistoryManager databaseHistoryManager; private final DatabaseSavedReplyManager databaseSavedReplyManager; private final DatabaseFilterManager databaseFilterManager; + private final DatabaseBoardManager databaseBoardManager; public DatabaseManager(Context context) { backgroundExecutor = Executors.newSingleThreadExecutor(); @@ -73,6 +79,7 @@ public class DatabaseManager { databaseHistoryManager = new DatabaseHistoryManager(this, helper, databaseLoadableManager); databaseSavedReplyManager = new DatabaseSavedReplyManager(this, helper); databaseFilterManager = new DatabaseFilterManager(this, helper); + databaseBoardManager = new DatabaseBoardManager(this, helper); initialize(); EventBus.getDefault().register(this); } @@ -97,6 +104,10 @@ public class DatabaseManager { return databaseFilterManager; } + public DatabaseBoardManager getDatabaseBoardManager() { + return databaseBoardManager; + } + // Called when the app changes foreground state public void onEvent(Chan.ForegroundChangedMessage message) { if (!message.inForeground) { @@ -118,44 +129,6 @@ public class DatabaseManager { initialize(); } - /** - * Create or updates these boards in the boards table. - * - * @param boards List of boards to create or update - */ - public void setBoards(final List boards) { - try { - callInTransaction(helper.getConnectionSource(), new Callable() { - @Override - public Void call() throws SQLException { - for (Board b : boards) { - helper.boardsDao.createOrUpdate(b); - } - - return null; - } - }); - } catch (Exception e) { - Logger.e(TAG, "Error setting boards in db", e); - } - } - - /** - * Get all boards from the boards table. - * - * @return all boards from the boards table - */ - public List getBoards() { - List boards = null; - try { - boards = helper.boardsDao.queryForAll(); - } catch (SQLException e) { - Logger.e(TAG, "Error getting boards from db", e); - } - - return boards; - } - /** * Check if the post is added in the threadhide table. * @@ -165,7 +138,7 @@ public class DatabaseManager { public boolean isThreadHidden(Post post) { if (threadHidesIds.contains(post.no)) { for (ThreadHide hide : threadHides) { - if (hide.no == post.no && hide.board.equals(post.board)) { + if (hide.no == post.no && hide.board.equals(post.boardId)) { return true; } } diff --git a/Clover/app/src/main/java/org/floens/chan/core/database/DatabaseSavedReplyManager.java b/Clover/app/src/main/java/org/floens/chan/core/database/DatabaseSavedReplyManager.java index 3c531262..2cda0964 100644 --- a/Clover/app/src/main/java/org/floens/chan/core/database/DatabaseSavedReplyManager.java +++ b/Clover/app/src/main/java/org/floens/chan/core/database/DatabaseSavedReplyManager.java @@ -29,14 +29,18 @@ import java.util.List; import java.util.Map; import java.util.concurrent.Callable; +/** + * Saved replies are posts-password combinations used to track what posts are posted by the app, + * and used to delete posts. + */ public class DatabaseSavedReplyManager { private static final String TAG = "DatabaseSavedReplyManager"; private static final long SAVED_REPLY_TRIM_TRIGGER = 250; private static final long SAVED_REPLY_TRIM_COUNT = 50; - private DatabaseManager databaseManager; - private DatabaseHelper helper; + private final DatabaseManager databaseManager; + private final DatabaseHelper helper; private final Map> savedRepliesByNo = new HashMap<>(); @@ -45,8 +49,16 @@ public class DatabaseSavedReplyManager { this.helper = helper; } - // optimized and threadsafe + /** + * Check if the given board-no combination is in the database.
+ * This is unlike other methods in that it immediately returns the result instead of + * a Callable. This method is thread-safe and optimized. + * @param board board code of the post + * @param no post number + * @return {@code true} if the post is in the saved reply database, {@code false} otherwise. + */ public boolean isSaved(String board, int no) { + // TODO(multi-site) synchronized (savedRepliesByNo) { if (savedRepliesByNo.containsKey(no)) { List items = savedRepliesByNo.get(no); diff --git a/Clover/app/src/main/java/org/floens/chan/core/http/ReplyHttpCall.java b/Clover/app/src/main/java/org/floens/chan/core/http/ReplyHttpCall.java index 688d715d..5eca9c22 100644 --- a/Clover/app/src/main/java/org/floens/chan/core/http/ReplyHttpCall.java +++ b/Clover/app/src/main/java/org/floens/chan/core/http/ReplyHttpCall.java @@ -46,6 +46,7 @@ public class ReplyHttpCall extends HttpCall { public String password; public int threadNo = -1; public int postNo = -1; + public boolean probablyBanned; private final Reply reply; @@ -112,6 +113,7 @@ public class ReplyHttpCall extends HttpCall { Matcher errorMessageMatcher = ERROR_MESSAGE.matcher(result); if (errorMessageMatcher.find()) { errorMessage = Jsoup.parse(errorMessageMatcher.group(1)).body().text(); + probablyBanned = errorMessage.contains("banned"); } else { Matcher threadNoMatcher = THREAD_NO_PATTERN.matcher(result); if (threadNoMatcher.find()) { diff --git a/Clover/app/src/main/java/org/floens/chan/core/manager/BoardManager.java b/Clover/app/src/main/java/org/floens/chan/core/manager/BoardManager.java index b7d720b5..a9afda22 100644 --- a/Clover/app/src/main/java/org/floens/chan/core/manager/BoardManager.java +++ b/Clover/app/src/main/java/org/floens/chan/core/manager/BoardManager.java @@ -17,15 +17,11 @@ */ package org.floens.chan.core.manager; -import com.android.volley.Response; -import com.android.volley.VolleyError; - -import org.floens.chan.Chan; -import org.floens.chan.chan.ChanUrls; import org.floens.chan.core.database.DatabaseManager; import org.floens.chan.core.model.Board; -import org.floens.chan.core.net.BoardsRequest; -import org.floens.chan.utils.Logger; +import org.floens.chan.core.site.Boards; +import org.floens.chan.core.site.Site; +import org.floens.chan.core.site.Sites; import java.util.ArrayList; import java.util.Collections; @@ -36,7 +32,7 @@ import java.util.Map; import de.greenrobot.event.EventBus; -public class BoardManager implements Response.Listener>, Response.ErrorListener { +public class BoardManager { private static final String TAG = "BoardManager"; private static final Comparator ORDER_SORT = new Comparator() { @@ -54,6 +50,7 @@ public class BoardManager implements Response.Listener>, Response.Er }; private final DatabaseManager databaseManager; + private final Site defaultSite; private final List boards; private final List savedBoards = new ArrayList<>(); @@ -61,34 +58,31 @@ public class BoardManager implements Response.Listener>, Response.Er public BoardManager(DatabaseManager databaseManager) { this.databaseManager = databaseManager; - boards = databaseManager.getBoards(); + defaultSite = Sites.defaultSite(); + boards = databaseManager.runTaskSync(databaseManager.getDatabaseBoardManager().getBoards(defaultSite)); if (boards.isEmpty()) { - Logger.d(TAG, "Loading default boards"); - boards.addAll(getDefaultBoards()); - saveDatabase(); - update(true); + defaultSite.boards(new Site.BoardsListener() { + @Override + public void onBoardsReceived(Boards boards) { + appendBoards(boards); + } + }); } else { update(false); } - - Chan.getVolleyRequestQueue().add(new BoardsRequest(ChanUrls.getBoardsUrl(), this, this)); } - @Override - public void onResponse(List response) { + private void appendBoards(Boards response) { List boardsToAddWs = new ArrayList<>(); List boardsToAddNws = new ArrayList<>(); - for (int i = 0; i < response.size(); i++) { - Board serverBoard = response.get(i); + for (int i = 0; i < response.boards.size(); i++) { + Board serverBoard = response.boards.get(i); Board existing = getBoardByCode(serverBoard.code); if (existing != null) { - serverBoard.id = existing.id; - serverBoard.saved = existing.saved; - serverBoard.order = existing.order; - boards.set(boards.indexOf(existing), serverBoard); + existing.update(serverBoard); } else { serverBoard.saved = true; if (serverBoard.workSafe) { @@ -118,11 +112,6 @@ public class BoardManager implements Response.Listener>, Response.Er update(true); } - @Override - public void onErrorResponse(VolleyError error) { - Logger.e(TAG, "Failed to get boards from server"); - } - // Thread-safe public boolean getBoardExists(String code) { return getBoardByCode(code) != null; @@ -163,19 +152,7 @@ public class BoardManager implements Response.Listener>, Response.Er } private void saveDatabase() { - databaseManager.setBoards(boards); - } - - private List getDefaultBoards() { - List list = new ArrayList<>(); - list.add(new Board("Technology", "g", true, true)); - list.add(new Board("Food & Cooking", "ck", true, true)); - list.add(new Board("Do It Yourself", "diy", true, true)); - list.add(new Board("Animals & Nature", "an", true, true)); - - Collections.shuffle(list); - - return list; + databaseManager.runTask(databaseManager.getDatabaseBoardManager().setBoards(boards)); } private List filterSaved(List all) { diff --git a/Clover/app/src/main/java/org/floens/chan/core/model/Board.java b/Clover/app/src/main/java/org/floens/chan/core/model/Board.java index a7736c37..236cb693 100644 --- a/Clover/app/src/main/java/org/floens/chan/core/model/Board.java +++ b/Clover/app/src/main/java/org/floens/chan/core/model/Board.java @@ -22,12 +22,16 @@ import android.text.TextUtils; import com.j256.ormlite.field.DatabaseField; import com.j256.ormlite.table.DatabaseTable; +import org.floens.chan.core.site.Site; + @DatabaseTable public class Board { public Board() { } - public Board(String name, String code, boolean saved, boolean workSafe) { + public Board(Site site, String name, String code, boolean saved, boolean workSafe) { + this.siteId = site.id(); + this.site = site; this.name = name; this.code = code; this.saved = saved; @@ -37,23 +41,34 @@ public class Board { @DatabaseField(generatedId = true) public int id; - // named key for legacy support - @DatabaseField(columnName = "key") - public String name; + @DatabaseField(columnName = "site") + public int siteId; - // named value for legacy support - @DatabaseField(columnName = "value") - public String code; + /** + * The site this board belongs to, loaded with {@link #siteId} in the database manager. + */ + public transient Site site; /** - * True if this board appears in the dropdown, false otherwise. + * {@code true} if this board appears in the dropdown, {@code false} otherwise. */ @DatabaseField public boolean saved = false; + /** + * Order of the board in the dropdown, user-set. + */ @DatabaseField public int order; + // named key for legacy support + @DatabaseField(columnName = "key") + public String name; + + // named value for legacy support + @DatabaseField(columnName = "value") + public String code; + @DatabaseField public boolean workSafe = false; @@ -88,10 +103,12 @@ public class Board { public int cooldownImages = 0; // unused, to be removed + @Deprecated @DatabaseField public int cooldownRepliesIntra = -1; // unused, to be removed + @Deprecated @DatabaseField public int cooldownImagesIntra = -1; @@ -131,8 +148,81 @@ public class Board { return true; } - @Override - public String toString() { - return name; + public String getName() { + return "/" + code + "/ \u2013 " + name; + } + + /** + * Updates the board with data from {@code o}.
+ * {@link #id}, {@link #saved}, {@link #order} are skipped because these are user-set. + * + * @param o other board to update from. + */ + public void update(Board o) { + siteId = o.siteId; + site = o.site; + name = o.name; + code = o.code; + workSafe = o.workSafe; + perPage = o.perPage; + pages = o.pages; + maxFileSize = o.maxFileSize; + maxWebmSize = o.maxWebmSize; + maxCommentChars = o.maxCommentChars; + bumpLimit = o.bumpLimit; + imageLimit = o.imageLimit; + cooldownThreads = o.cooldownThreads; + cooldownReplies = o.cooldownReplies; + cooldownImages = o.cooldownImages; + cooldownRepliesIntra = o.cooldownRepliesIntra; + cooldownImagesIntra = o.cooldownImagesIntra; + spoilers = o.spoilers; + customSpoilers = o.customSpoilers; + userIds = o.userIds; + codeTags = o.codeTags; + preuploadCaptcha = o.preuploadCaptcha; + countryFlags = o.countryFlags; + trollFlags = o.trollFlags; + mathTags = o.mathTags; + description = o.description; + } + + /** + * Creates a complete copy of this board. + * + * @return copy of this board. + */ + public Board copy() { + Board b = new Board(); + b.id = id; + b.siteId = siteId; + b.site = site; + b.name = name; + b.code = code; + b.saved = saved; + b.order = order; + b.workSafe = workSafe; + b.perPage = perPage; + b.pages = pages; + b.maxFileSize = maxFileSize; + b.maxWebmSize = maxWebmSize; + b.maxCommentChars = maxCommentChars; + b.bumpLimit = bumpLimit; + b.imageLimit = imageLimit; + b.cooldownThreads = cooldownThreads; + b.cooldownReplies = cooldownReplies; + b.cooldownImages = cooldownImages; + b.cooldownRepliesIntra = cooldownRepliesIntra; + b.cooldownImagesIntra = cooldownImagesIntra; + b.spoilers = spoilers; + b.customSpoilers = customSpoilers; + b.userIds = userIds; + b.codeTags = codeTags; + b.preuploadCaptcha = preuploadCaptcha; + b.countryFlags = countryFlags; + b.trollFlags = trollFlags; + b.mathTags = mathTags; + b.description = description; + return b; } } diff --git a/Clover/app/src/main/java/org/floens/chan/core/model/Filter.java b/Clover/app/src/main/java/org/floens/chan/core/model/Filter.java index 43360be8..fe3dc478 100644 --- a/Clover/app/src/main/java/org/floens/chan/core/model/Filter.java +++ b/Clover/app/src/main/java/org/floens/chan/core/model/Filter.java @@ -40,6 +40,7 @@ public class Filter { @DatabaseField(canBeNull = false) public boolean allBoards = true; + // TODO(multi-site) @DatabaseField(canBeNull = false) public String boards; diff --git a/Clover/app/src/main/java/org/floens/chan/core/model/Loadable.java b/Clover/app/src/main/java/org/floens/chan/core/model/Loadable.java index 84cb208f..c7fb633f 100644 --- a/Clover/app/src/main/java/org/floens/chan/core/model/Loadable.java +++ b/Clover/app/src/main/java/org/floens/chan/core/model/Loadable.java @@ -23,20 +23,40 @@ import android.text.TextUtils; import com.j256.ormlite.field.DatabaseField; import com.j256.ormlite.table.DatabaseTable; +import org.floens.chan.core.site.Site; + /** * Something that can be loaded, like a board or thread. + * Used instead of {@link Board} or {@link Post} because of the unique things a loadable can do and save in the database:
+ * - It keeps track of the list index where the user last viewed.
+ * - It keeps track of what post was last seen and loaded.
+ * - It keeps track of the title the toolbar should show, generated from the first post (so after loading).
*/ @DatabaseTable public class Loadable { @DatabaseField(generatedId = true) public int id; + @DatabaseField(columnName = "site") + public int siteId; + + public transient Site site; + + /** + * Mode for the loadable. + * Either thread or catalog. Board is deprecated. + */ @DatabaseField public int mode = Mode.INVALID; - @DatabaseField(canBeNull = false, index = true) - public String board; + @DatabaseField(columnName = "board", canBeNull = false, index = true) + public String boardCode; + + public Board board; + /** + * Thread number. + */ @DatabaseField(index = true) public int no = -1; @@ -63,28 +83,34 @@ public class Loadable { /** * Constructs an empty loadable. The mode is INVALID. */ - private Loadable() { + protected Loadable() { } public static Loadable emptyLoadable() { return new Loadable(); } - public static Loadable forCatalog(String board) { + public static Loadable forCatalog(Board board) { Loadable loadable = new Loadable(); - loadable.mode = Mode.CATALOG; + loadable.siteId = board.site.id(); + loadable.site = board.site; loadable.board = board; + loadable.boardCode = board.code; + loadable.mode = Mode.CATALOG; return loadable; } - public static Loadable forThread(String board, int no) { - return Loadable.forThread(board, no, ""); + public static Loadable forThread(Site site, Board board, int no) { + return Loadable.forThread(site, board, no, ""); } - public static Loadable forThread(String board, int no, String title) { + public static Loadable forThread(Site site, Board board, int no, String title) { Loadable loadable = new Loadable(); + loadable.siteId = site.id(); + loadable.site = site; loadable.mode = Mode.THREAD; loadable.board = board; + loadable.boardCode = board.code; loadable.no = no; loadable.title = title; return loadable; @@ -135,15 +161,19 @@ public class Loadable { Loadable other = (Loadable) object; + if (site != other.site) { + return false; + } + if (mode == other.mode) { switch (mode) { case Mode.INVALID: return true; case Mode.CATALOG: case Mode.BOARD: - return board.equals(other.board); + return boardCode.equals(other.boardCode); case Mode.THREAD: - return board.equals(other.board) && no == other.no; + return boardCode.equals(other.boardCode) && no == other.no; default: throw new IllegalArgumentException(); } @@ -157,7 +187,7 @@ public class Loadable { int result = mode; if (mode == Mode.THREAD || mode == Mode.CATALOG || mode == Mode.BOARD) { - result = 31 * result + (board != null ? board.hashCode() : 0); + result = 31 * result + (boardCode != null ? boardCode.hashCode() : 0); } if (mode == Mode.THREAD) { result = 31 * result + no; @@ -170,7 +200,7 @@ public class Loadable { return "Loadable{" + "id=" + id + ", mode=" + mode + - ", board='" + board + '\'' + + ", board='" + boardCode + '\'' + ", no=" + no + ", title='" + title + '\'' + ", listViewIndex=" + listViewIndex + @@ -194,8 +224,9 @@ public class Loadable { Loadable loadable = new Loadable(); /*loadable.id = */ parcel.readInt(); + loadable.siteId = parcel.readInt(); loadable.mode = parcel.readInt(); - loadable.board = parcel.readString(); + loadable.boardCode = parcel.readString(); loadable.no = parcel.readInt(); loadable.title = parcel.readString(); loadable.listViewIndex = parcel.readInt(); @@ -205,8 +236,11 @@ public class Loadable { public void writeToParcel(Parcel parcel) { parcel.writeInt(id); + // TODO(multi-site) + parcel.writeInt(siteId); parcel.writeInt(mode); - parcel.writeString(board); + // TODO(multi-site) + parcel.writeString(boardCode); parcel.writeInt(no); parcel.writeString(title); parcel.writeInt(listViewIndex); @@ -215,8 +249,13 @@ public class Loadable { public Loadable copy() { Loadable copy = new Loadable(); + copy.id = id; + copy.siteId = siteId; + copy.site = site; copy.mode = mode; - copy.board = board; + // TODO: for empty loadables + if (board != null) copy.board = board.copy(); + copy.boardCode = boardCode; copy.no = no; copy.title = title; copy.listViewIndex = listViewIndex; diff --git a/Clover/app/src/main/java/org/floens/chan/core/model/Pin.java b/Clover/app/src/main/java/org/floens/chan/core/model/Pin.java index 78ed3223..07c587ae 100644 --- a/Clover/app/src/main/java/org/floens/chan/core/model/Pin.java +++ b/Clover/app/src/main/java/org/floens/chan/core/model/Pin.java @@ -55,6 +55,9 @@ public class Pin { @DatabaseField public boolean archived = false; + public Pin() { + } + public int getNewPostCount() { if (watchLastCount < 0 || watchNewCount < 0) { return 0; diff --git a/Clover/app/src/main/java/org/floens/chan/core/model/Post.java b/Clover/app/src/main/java/org/floens/chan/core/model/Post.java index 3847bcdd..f0030168 100644 --- a/Clover/app/src/main/java/org/floens/chan/core/model/Post.java +++ b/Clover/app/src/main/java/org/floens/chan/core/model/Post.java @@ -20,7 +20,6 @@ package org.floens.chan.core.model; import android.text.SpannableString; import android.text.TextUtils; -import org.floens.chan.Chan; import org.floens.chan.chan.ChanParser; import org.floens.chan.chan.ChanUrls; import org.floens.chan.core.settings.ChanSettings; @@ -29,7 +28,6 @@ import org.jsoup.parser.Parser; import java.util.ArrayList; import java.util.Collections; import java.util.List; -import java.util.Random; import java.util.Set; import java.util.TreeSet; import java.util.concurrent.atomic.AtomicBoolean; @@ -40,10 +38,10 @@ import java.util.concurrent.atomic.AtomicBoolean; * This class has members that are threadsafe and some that are not, see the source for more info. */ public class Post { - private static final Random random = new Random(); - // *** These next members don't get changed after finish() is called. Effectively final. *** - public String board; + public String boardId; + + public Board board; public int no = -1; @@ -150,7 +148,7 @@ public class Post { * @return false if this data is invalid */ public boolean finish() { - if (board == null || no < 0 || resto < 0 || date == null || time < 0) { + if (boardId == null || no < 0 || resto < 0 || date == null || time < 0) { return false; } @@ -162,25 +160,15 @@ public class Post { if (filename != null && ext != null && imageWidth > 0 && imageHeight > 0 && tim >= 0) { hasImage = true; - imageUrl = ChanUrls.getImageUrl(board, Long.toString(tim), ext); + // TODO: only use #image + imageUrl = board.site.endpoints().imageUrl(this); filename = Parser.unescapeEntities(filename, false); - - if (spoiler) { - Board b = Chan.getBoardManager().getBoardByCode(board); - if (b != null && b.customSpoilers >= 0) { - thumbnailUrl = ChanUrls.getCustomSpoilerUrl(board, random.nextInt(b.customSpoilers) + 1); - } else { - thumbnailUrl = ChanUrls.getSpoilerUrl(); - } - } else { - thumbnailUrl = ChanUrls.getThumbnailUrl(board, Long.toString(tim)); - } - + thumbnailUrl = board.site.endpoints().thumbnailUrl(this); image = new PostImage(String.valueOf(tim), thumbnailUrl, imageUrl, filename, ext, imageWidth, imageHeight, spoiler, fileSize); } if (!TextUtils.isEmpty(country)) { - countryUrl = ChanUrls.getCountryFlagUrl(country); + countryUrl = board.site.endpoints().flag(this); } if (ChanSettings.revealImageSpoilers.get()) { diff --git a/Clover/app/src/main/java/org/floens/chan/core/model/SavedReply.java b/Clover/app/src/main/java/org/floens/chan/core/model/SavedReply.java index 3d3571a1..60447c05 100644 --- a/Clover/app/src/main/java/org/floens/chan/core/model/SavedReply.java +++ b/Clover/app/src/main/java/org/floens/chan/core/model/SavedReply.java @@ -34,6 +34,9 @@ public class SavedReply { @DatabaseField(generatedId = true) private int id; + @DatabaseField(columnName = "site") + public int site; + @DatabaseField(index = true, canBeNull = false) public String board; diff --git a/Clover/app/src/main/java/org/floens/chan/core/model/ThreadHide.java b/Clover/app/src/main/java/org/floens/chan/core/model/ThreadHide.java index 23896a59..a88e74ba 100644 --- a/Clover/app/src/main/java/org/floens/chan/core/model/ThreadHide.java +++ b/Clover/app/src/main/java/org/floens/chan/core/model/ThreadHide.java @@ -25,6 +25,9 @@ public class ThreadHide { @DatabaseField(generatedId = true) public int id; + @DatabaseField(columnName = "site") + public int site; + @DatabaseField public String board; diff --git a/Clover/app/src/main/java/org/floens/chan/core/net/ChanReaderRequest.java b/Clover/app/src/main/java/org/floens/chan/core/net/ChanReaderRequest.java index 860ceab1..35373511 100644 --- a/Clover/app/src/main/java/org/floens/chan/core/net/ChanReaderRequest.java +++ b/Clover/app/src/main/java/org/floens/chan/core/net/ChanReaderRequest.java @@ -23,7 +23,6 @@ import com.android.volley.Response.ErrorListener; import com.android.volley.Response.Listener; import org.floens.chan.Chan; -import org.floens.chan.chan.ChanUrls; import org.floens.chan.core.database.DatabaseManager; import org.floens.chan.core.database.DatabaseSavedReplyManager; import org.floens.chan.core.manager.FilterEngine; @@ -42,6 +41,11 @@ import java.util.concurrent.ExecutorService; import java.util.concurrent.Executors; import java.util.concurrent.Future; +/** + * Process a typical imageboard json response.
+ * This class is highly multithreaded, take good care to don't access models that are to be only + * changed on the main thread. + */ public class ChanReaderRequest extends JsonReaderRequest { private static final String TAG = "ChanReaderRequest"; private static final boolean LOG_TIMING = false; @@ -71,13 +75,22 @@ public class ChanReaderRequest extends JsonReaderRequest cached, Listener listener, ErrorListener errorListener) { + public static ChanReaderRequest newInstance( + Loadable loadable, List cached, Listener listener, ErrorListener errorListener) { String url; + if (loadable.site == null) { + throw new NullPointerException("Loadable.site == null"); + } + + if (loadable.board == null) { + throw new NullPointerException("Loadable.board == null"); + } + if (loadable.isThreadMode()) { - url = ChanUrls.getThreadUrl(loadable.board, loadable.no); + url = loadable.site.endpoints().thread(loadable.board, loadable); } else if (loadable.isCatalogMode()) { - url = ChanUrls.getCatalogUrl(loadable.board); + url = loadable.site.endpoints().catalog(loadable.board); } else { throw new IllegalArgumentException("Unknown mode"); } @@ -99,7 +112,7 @@ public class ChanReaderRequest extends JsonReaderRequest cachedByNo) throws Exception { Post post = new Post(); post.board = loadable.board; + post.boardId = loadable.boardCode; reader.beginObject(); while (reader.hasNext()) { diff --git a/Clover/app/src/main/java/org/floens/chan/core/net/JsonReaderRequest.java b/Clover/app/src/main/java/org/floens/chan/core/net/JsonReaderRequest.java index 9da1f632..6dcf723e 100644 --- a/Clover/app/src/main/java/org/floens/chan/core/net/JsonReaderRequest.java +++ b/Clover/app/src/main/java/org/floens/chan/core/net/JsonReaderRequest.java @@ -59,7 +59,6 @@ public abstract class JsonReaderRequest extends Request { try { read = readJson(reader); } catch (Exception e) { - exception = e; } diff --git a/Clover/app/src/main/java/org/floens/chan/core/net/PostParseCallable.java b/Clover/app/src/main/java/org/floens/chan/core/net/PostParseCallable.java index 5a6d8f43..748317a2 100644 --- a/Clover/app/src/main/java/org/floens/chan/core/net/PostParseCallable.java +++ b/Clover/app/src/main/java/org/floens/chan/core/net/PostParseCallable.java @@ -53,7 +53,7 @@ class PostParseCallable implements Callable { return null; } - post.isSavedReply = savedReplyManager.isSaved(post.board, post.no); + post.isSavedReply = savedReplyManager.isSaved(post.boardId, post.no); return post; } 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 f7ee75bb..3f1e2bda 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 @@ -91,7 +91,7 @@ public class ReplyPresenter implements ReplyManager.HttpCallback, bound = true; this.loadable = loadable; - this.board = boardManager.getBoardByCode(loadable.board); + this.board = boardManager.getBoardByCode(loadable.boardCode); draft = replyManager.getReply(loadable); @@ -183,7 +183,7 @@ public class ReplyPresenter implements ReplyManager.HttpCallback, public void onSubmitClicked() { callback.loadViewsIntoDraft(draft); - draft.board = loadable.board; + draft.board = loadable.boardCode; draft.resto = loadable.isThreadMode() ? loadable.no : -1; if (ChanSettings.passLoggedIn()) { @@ -214,7 +214,7 @@ public class ReplyPresenter implements ReplyManager.HttpCallback, } } - SavedReply savedReply = new SavedReply(loadable.board, replyCall.postNo, replyCall.password); + SavedReply savedReply = new SavedReply(loadable.boardCode, replyCall.postNo, replyCall.password); databaseManager.runTask(databaseManager.getDatabaseSavedReplyManager().saveReply(savedReply)); switchPage(Page.INPUT, false); @@ -228,7 +228,7 @@ public class ReplyPresenter implements ReplyManager.HttpCallback, callback.onPosted(); if (bound && !loadable.isThreadMode()) { - callback.showThread(databaseManager.getDatabaseLoadableManager().get(Loadable.forThread(loadable.board, replyCall.postNo))); + callback.showThread(databaseManager.getDatabaseLoadableManager().get(Loadable.forThread(loadable.site, loadable.board, replyCall.postNo))); } } else { if (replyCall.errorMessage == null) { @@ -237,6 +237,9 @@ public class ReplyPresenter implements ReplyManager.HttpCallback, switchPage(Page.INPUT, true); callback.openMessage(true, false, replyCall.errorMessage, true); + if (replyCall.probablyBanned) { +// callback.openMessageWebview(); + } } } @@ -349,8 +352,8 @@ public class ReplyPresenter implements ReplyManager.HttpCallback, if (!captchaInited) { captchaInited = true; String baseUrl = loadable.isThreadMode() ? - ChanUrls.getThreadUrlDesktop(loadable.board, loadable.no) : - ChanUrls.getBoardUrlDesktop(loadable.board); + ChanUrls.getThreadUrlDesktop(loadable.boardCode, loadable.no) : + ChanUrls.getBoardUrlDesktop(loadable.boardCode); callback.initCaptcha(baseUrl, ChanUrls.getCaptchaSiteKey(), this); } break; @@ -417,6 +420,8 @@ public class ReplyPresenter implements ReplyManager.HttpCallback, void openMessage(boolean open, boolean animate, String message, boolean autoHide); + void openMessageWebview(String rawMessage); + void onPosted(); void setCommentHint(String hint); 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 d736c806..183e71be 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 @@ -29,6 +29,7 @@ import org.floens.chan.core.http.DeleteHttpCall; import org.floens.chan.core.http.ReplyManager; import org.floens.chan.core.manager.BoardManager; import org.floens.chan.core.manager.WatchManager; +import org.floens.chan.core.model.Board; import org.floens.chan.core.model.ChanThread; import org.floens.chan.core.model.History; import org.floens.chan.core.model.Loadable; @@ -39,6 +40,7 @@ import org.floens.chan.core.model.PostLinkable; import org.floens.chan.core.model.SavedReply; import org.floens.chan.core.pool.LoaderPool; import org.floens.chan.core.settings.ChanSettings; +import org.floens.chan.core.site.Site; import org.floens.chan.ui.adapter.PostAdapter; import org.floens.chan.ui.adapter.PostsFilter; import org.floens.chan.ui.cell.PostCellInterface; @@ -369,7 +371,7 @@ public class ThreadPresenter implements ChanLoader.ChanLoaderCallback, PostAdapt @Override public void onPostClicked(Post post) { if (loadable.isCatalogMode()) { - Loadable threadLoadable = databaseManager.getDatabaseLoadableManager().get(Loadable.forThread(post.board, post.no)); + Loadable threadLoadable = databaseManager.getDatabaseLoadableManager().get(Loadable.forThread(loadable.site, post.board, post.no)); threadLoadable.title = PostHelper.getTitle(post, loadable); threadPresenterCallback.showThread(threadLoadable); } else { @@ -434,7 +436,8 @@ public class ThreadPresenter implements ChanLoader.ChanLoaderCallback, PostAdapt } } - if (databaseManager.getDatabaseSavedReplyManager().isSaved(post.board, post.no)) { + if (loadable.site.feature(Site.Feature.POST_DELETE) && + databaseManager.getDatabaseSavedReplyManager().isSaved(post.boardId, post.no)) { menu.add(new FloatingMenuItem(POST_OPTION_DELETE, R.string.delete)); } @@ -480,25 +483,25 @@ public class ThreadPresenter implements ChanLoader.ChanLoaderCallback, PostAdapt requestDeletePost(post); break; case POST_OPTION_SAVE: - SavedReply savedReply = new SavedReply(post.board, post.no, ""); + SavedReply savedReply = new SavedReply(post.boardId, post.no, ""); databaseManager.runTask(databaseManager.getDatabaseSavedReplyManager().saveReply(savedReply)); break; case POST_OPTION_PIN: - Loadable pinLoadable = databaseManager.getDatabaseLoadableManager().get(Loadable.forThread(post.board, post.no)); + Loadable pinLoadable = databaseManager.getDatabaseLoadableManager().get(Loadable.forThread(loadable.site, post.board, post.no)); watchManager.createPin(pinLoadable, post); break; case POST_OPTION_OPEN_BROWSER: AndroidUtils.openLink( post.isOP ? - ChanUrls.getThreadUrlDesktop(post.board, post.no) : - ChanUrls.getThreadUrlDesktop(post.board, loadable.no, post.no) + ChanUrls.getThreadUrlDesktop(post.boardId, post.no) : + ChanUrls.getThreadUrlDesktop(post.boardId, loadable.no, post.no) ); break; case POST_OPTION_SHARE: AndroidUtils.shareLink( post.isOP ? - ChanUrls.getThreadUrlDesktop(post.board, post.no) : - ChanUrls.getThreadUrlDesktop(post.board, loadable.no, post.no) + ChanUrls.getThreadUrlDesktop(post.boardId, post.no) : + ChanUrls.getThreadUrlDesktop(post.boardId, loadable.no, post.no) ); break; case POST_OPTION_HIDE: @@ -519,8 +522,9 @@ public class ThreadPresenter implements ChanLoader.ChanLoaderCallback, PostAdapt } else if (linkable.type == PostLinkable.Type.THREAD) { PostLinkable.ThreadLink link = (PostLinkable.ThreadLink) linkable.value; - if (boardManager.getBoardExists(link.board)) { - Loadable thread = databaseManager.getDatabaseLoadableManager().get(Loadable.forThread(link.board, link.threadId)); + Board board = loadable.site.board(link.board); + if (board != null) { + Loadable thread = databaseManager.getDatabaseLoadableManager().get(Loadable.forThread(board.site, board, link.threadId)); thread.markedNo = link.postId; threadPresenterCallback.showThread(thread); @@ -590,7 +594,7 @@ public class ThreadPresenter implements ChanLoader.ChanLoaderCallback, PostAdapt threadPresenterCallback.showDeleting(); SavedReply reply = databaseManager.runTaskSync( - databaseManager.getDatabaseSavedReplyManager().findSavedReply(post.board, post.no) + databaseManager.getDatabaseSavedReplyManager().findSavedReply(post.boardId, post.no) ); if (reply != null) { replyManager.makeHttpCall(new DeleteHttpCall(reply, onlyImageDelete), new ReplyManager.HttpCallback() { @@ -617,7 +621,7 @@ public class ThreadPresenter implements ChanLoader.ChanLoaderCallback, PostAdapt private void requestDeletePost(Post post) { SavedReply reply = databaseManager.runTaskSync( - databaseManager.getDatabaseSavedReplyManager().findSavedReply(post.board, post.no) + databaseManager.getDatabaseSavedReplyManager().findSavedReply(post.boardId, post.no) ); if (reply != null) { threadPresenterCallback.confirmPostDelete(post); diff --git a/Clover/app/src/main/java/org/floens/chan/core/site/Boards.java b/Clover/app/src/main/java/org/floens/chan/core/site/Boards.java new file mode 100644 index 00000000..729fb12c --- /dev/null +++ b/Clover/app/src/main/java/org/floens/chan/core/site/Boards.java @@ -0,0 +1,13 @@ +package org.floens.chan.core.site; + +import org.floens.chan.core.model.Board; + +import java.util.List; + +public class Boards { + public final List boards; + + public Boards(List boards) { + this.boards = boards; + } +} 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 new file mode 100644 index 00000000..61228b2f --- /dev/null +++ b/Clover/app/src/main/java/org/floens/chan/core/site/Site.java @@ -0,0 +1,86 @@ +package org.floens.chan.core.site; + +import org.floens.chan.core.model.Board; + +public interface Site { + enum Feature { + /** + * This site supports posting. (Or rather, we've implemented support for it.) + */ + POSTING, + + /** + * This site supports a 4chan like boards.json endpoint. + */ + DYNAMIC_BOARDS, + + /** + * This site supports deleting posts. + */ + POST_DELETE, + + /** + * This site supports some sort of login (like 4pass). + */ + LOGIN + } + + /** + * Features available to check when {@link Feature#POSTING} is {@code true}. + */ + enum BoardFeature { + /** + * This board supports posting with images. + */ + POSTING_IMAGE, + + /** + * This board supports posting with a checkbox to mark the posted image as a spoiler. + */ + POSTING_SPOILER, + } + + /** + * How the boards are organized for this size. + */ + enum BoardsType { + /** + * The site's boards are static, hard-coded in the site. + */ + STATIC, + + /** + * The site's boards are dynamic, a boards.json like endpoint is available to get the available boards. + */ + DYNAMIC, + + /** + * The site's boards are dynamic and infinite, existence of boards should be checked per board. + */ + INFINITE + } + + /** + * Global positive (>0) integer that uniquely identifies this site.
+ * This id will be persisted in the database. + * + * @return a positive (>0) integer that uniquely identifies this site. + */ + int id(); + + boolean feature(Feature feature); + + boolean boardFeature(BoardFeature boardFeature, Board board); + + SiteEndpoints endpoints(); + + BoardsType boardsType(); + + void boards(BoardsListener boardsListener); + + Board board(String name); + + interface BoardsListener { + void onBoardsReceived(Boards boards); + } +} diff --git a/Clover/app/src/main/java/org/floens/chan/core/site/SiteEndpoints.java b/Clover/app/src/main/java/org/floens/chan/core/site/SiteEndpoints.java new file mode 100644 index 00000000..6d92a375 --- /dev/null +++ b/Clover/app/src/main/java/org/floens/chan/core/site/SiteEndpoints.java @@ -0,0 +1,24 @@ +package org.floens.chan.core.site; + +import org.floens.chan.core.model.Board; +import org.floens.chan.core.model.Loadable; +import org.floens.chan.core.model.Post; + +/** + * Endpoints for {@link Site}. + */ +public interface SiteEndpoints { + String catalog(Board board); + + String thread(Board board, Loadable loadable); + + String imageUrl(Post post); + + String thumbnailUrl(Post post); + + String flag(Post post); + + String boards(); + + String reply(Board board, Loadable thread); +} 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 new file mode 100644 index 00000000..225e63fb --- /dev/null +++ b/Clover/app/src/main/java/org/floens/chan/core/site/Sites.java @@ -0,0 +1,37 @@ +package org.floens.chan.core.site; + +import org.floens.chan.core.site.sites.chan4.Chan4; + +import java.util.Arrays; +import java.util.List; + +public class Sites { + public static final Chan4 CHAN4 = new Chan4(); + + public static final List ALL_SITES = Arrays.asList( + CHAN4 + ); + + private static final Site[] BY_ID; + + static { + int highestId = 0; + for (Site site : ALL_SITES) { + if (site.id() > highestId) { + highestId = site.id(); + } + } + BY_ID = new Site[highestId + 1]; + for (Site site : ALL_SITES) { + BY_ID[site.id()] = site; + } + } + + public static Site forId(int id) { + return BY_ID[id]; + } + + public static Site defaultSite() { + return CHAN4; + } +} 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 new file mode 100644 index 00000000..2eb436fb --- /dev/null +++ b/Clover/app/src/main/java/org/floens/chan/core/site/sites/chan4/Chan4.java @@ -0,0 +1,165 @@ +package org.floens.chan.core.site.sites.chan4; + +import com.android.volley.Response; +import com.android.volley.VolleyError; + +import org.floens.chan.Chan; +import org.floens.chan.core.model.Board; +import org.floens.chan.core.model.Loadable; +import org.floens.chan.core.model.Post; +import org.floens.chan.core.site.Boards; +import org.floens.chan.core.site.Site; +import org.floens.chan.core.site.SiteEndpoints; +import org.floens.chan.utils.Logger; + +import java.util.ArrayList; +import java.util.Collections; +import java.util.List; +import java.util.Locale; +import java.util.Random; + +public class Chan4 implements Site { + private static final String TAG = "Chan4"; + + private static final Random random = new Random(); + + private final SiteEndpoints endpoints = new SiteEndpoints() { + @Override + public String catalog(Board board) { + return "https://a.4cdn.org/" + board.code + "/catalog.json"; + } + + @Override + public String thread(Board board, Loadable loadable) { + return "https://a.4cdn.org/" + board.code + "/thread/" + loadable.no + ".json"; + } + + @Override + public String imageUrl(Post post) { + return "https://i.4cdn.org/" + post.boardId + "/" + Long.toString(post.tim) + "." + post.ext; + } + + @Override + public String thumbnailUrl(Post post) { + if (post.spoiler) { + if (post.board.customSpoilers >= 0) { + int i = random.nextInt(post.board.customSpoilers) + 1; + return "https://s.4cdn.org/image/spoiler-" + post.board.code + i + ".png"; + } else { + return "https://s.4cdn.org/image/spoiler.png"; + } + } else { + return "https://t.4cdn.org/" + post.board.code + "/" + post.tim + "s.jpg"; + } + } + + @Override + public String flag(Post post) { + return "https://s.4cdn.org/image/country/" + post.country.toLowerCase(Locale.ENGLISH) + ".gif"; + } + + @Override + public String boards() { + return "https://a.4cdn.org/boards.json"; + } + + @Override + public String reply(Board board, Loadable thread) { + return "https://sys.4chan.org/" + board.code + "/post"; + } + }; + + public Chan4() { + } + + /** + * Note: very special case, only this site may have 0 as the return value.
+ * This is for backwards compatibility when we didn't support multi-site yet.
+ * + * @return {@inheritDoc} + */ + @Override + public int id() { + return 0; + } + + @Override + public boolean feature(Feature feature) { + switch (feature) { + case POSTING: + // yes, we support posting. + return true; + case DYNAMIC_BOARDS: + // yes, boards.json + return true; + case LOGIN: + // 4chan pass. + return true; + case POST_DELETE: + // yes, with the password saved when posting. + return true; + default: + return false; + } + } + + @Override + public BoardsType boardsType() { + return BoardsType.DYNAMIC; + } + + @Override + public boolean boardFeature(BoardFeature boardFeature, Board board) { + switch (boardFeature) { + case POSTING_IMAGE: + // yes, we support image posting. + return true; + case POSTING_SPOILER: + // depends if the board supports it. + return board.spoilers; + default: + return false; + } + } + + @Override + public Board board(String name) { + List allBoards = Chan.getBoardManager().getAllBoards(); + for (Board board : allBoards) { + if (board.code.equals(name)) { + return board; + } + } + + return null; + } + + @Override + public SiteEndpoints endpoints() { + return endpoints; + } + + @Override + public void boards(final BoardsListener listener) { + Chan.getVolleyRequestQueue().add(new Chan4BoardsRequest(this, new Response.Listener>() { + @Override + public void onResponse(List response) { + listener.onBoardsReceived(new Boards(response)); + } + }, new Response.ErrorListener() { + @Override + public void onErrorResponse(VolleyError 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)); + } + })); + } +} diff --git a/Clover/app/src/main/java/org/floens/chan/core/net/BoardsRequest.java b/Clover/app/src/main/java/org/floens/chan/core/site/sites/chan4/Chan4BoardsRequest.java similarity index 92% rename from Clover/app/src/main/java/org/floens/chan/core/net/BoardsRequest.java rename to Clover/app/src/main/java/org/floens/chan/core/site/sites/chan4/Chan4BoardsRequest.java index 0d615084..6ef43c6d 100644 --- a/Clover/app/src/main/java/org/floens/chan/core/net/BoardsRequest.java +++ b/Clover/app/src/main/java/org/floens/chan/core/site/sites/chan4/Chan4BoardsRequest.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.net; +package org.floens.chan.core.site.sites.chan4; import android.util.JsonReader; @@ -23,13 +23,15 @@ import com.android.volley.Response.ErrorListener; import com.android.volley.Response.Listener; import org.floens.chan.core.model.Board; +import org.floens.chan.core.net.JsonReaderRequest; +import org.floens.chan.core.site.Site; import java.io.IOException; import java.util.ArrayList; import java.util.Collections; import java.util.List; -public class BoardsRequest extends JsonReaderRequest> { +public class Chan4BoardsRequest extends JsonReaderRequest> { public static List BLOCKED = Collections.singletonList( "f" ); @@ -38,8 +40,11 @@ public class BoardsRequest extends JsonReaderRequest> { // "a", "c", "w", "cm", "jp", "mlp", "lgbt" // ); - public BoardsRequest(String url, Listener> listener, ErrorListener errorListener) { - super(url, listener, errorListener); + private final Site site; + + public Chan4BoardsRequest(Site site, Listener> listener, ErrorListener errorListener) { + super(site.endpoints().boards(), listener, errorListener); + this.site = site; } @Override @@ -69,15 +74,12 @@ public class BoardsRequest extends JsonReaderRequest> { return list; } - @Override - public Priority getPriority() { - return Priority.LOW; - } - private Board readBoardEntry(JsonReader reader) throws IOException { reader.beginObject(); Board board = new Board(); + board.siteId = site.id(); + board.site = site; while (reader.hasNext()) { String key = reader.nextName(); diff --git a/Clover/app/src/main/java/org/floens/chan/test/TestActivity.java b/Clover/app/src/main/java/org/floens/chan/test/TestActivity.java index 1b71e6f0..bdcd4b0b 100644 --- a/Clover/app/src/main/java/org/floens/chan/test/TestActivity.java +++ b/Clover/app/src/main/java/org/floens/chan/test/TestActivity.java @@ -200,7 +200,8 @@ public class TestActivity extends Activity implements View.OnClickListener { } private void testCache() { - Loadable loadable = Loadable.forCatalog("g"); +// Loadable loadable = Loadable.forCatalog(Sites.defaultSite().boards().boards.get(0)); + Loadable loadable = null; ChanLoader loader = new ChanLoader(loadable); loader.addListener(new ChanLoader.ChanLoaderCallback() { @Override 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 07e36a7c..d908cb4c 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 @@ -123,7 +123,7 @@ public class StartActivity extends AppCompatActivity implements NfcAdapter.Creat chanState.thread = loadableManager.get(chanState.thread); loadDefault = false; - Board board = boardManager.getBoardByCode(chanState.board.board); + Board board = boardManager.getBoardByCode(chanState.board.boardCode); browseController.loadBoard(board); if (chanState.thread.mode == Loadable.Mode.THREAD) { @@ -136,7 +136,7 @@ public class StartActivity extends AppCompatActivity implements NfcAdapter.Creat Loadable fromUri = ChanHelper.getLoadableFromStartUri(data); if (fromUri != null) { loadDefault = false; - Board board = boardManager.getBoardByCode(fromUri.board); + Board board = boardManager.getBoardByCode(fromUri.boardCode); browseController.loadBoard(board); if (fromUri.isThreadMode()) { @@ -157,7 +157,7 @@ public class StartActivity extends AppCompatActivity implements NfcAdapter.Creat } if (loadDefault) { - browseController.loadBoard(boardManager.getSavedBoards().get(0)); + browseController.loadDefault(); } PreviousVersionHandler previousVersionHandler = new PreviousVersionHandler(); diff --git a/Clover/app/src/main/java/org/floens/chan/ui/adapter/PinAdapter.java b/Clover/app/src/main/java/org/floens/chan/ui/adapter/DrawerAdapter.java similarity index 91% rename from Clover/app/src/main/java/org/floens/chan/ui/adapter/PinAdapter.java rename to Clover/app/src/main/java/org/floens/chan/ui/adapter/DrawerAdapter.java index 0d8b33c8..ba158934 100644 --- a/Clover/app/src/main/java/org/floens/chan/ui/adapter/PinAdapter.java +++ b/Clover/app/src/main/java/org/floens/chan/ui/adapter/DrawerAdapter.java @@ -23,6 +23,7 @@ import android.text.TextUtils; import android.view.LayoutInflater; import android.view.View; import android.view.ViewGroup; +import android.widget.EditText; import android.widget.ImageView; import android.widget.TextView; @@ -47,17 +48,18 @@ import static org.floens.chan.utils.AndroidUtils.getAttrColor; import static org.floens.chan.utils.AndroidUtils.setRoundItemBackground; import static org.floens.chan.utils.AndroidUtils.sp; -public class PinAdapter extends RecyclerView.Adapter { +public class DrawerAdapter extends RecyclerView.Adapter { public enum HeaderAction { SETTINGS, CLEAR, CLEAR_ALL } - private static final int PIN_OFFSET = 3; + private static final int PIN_OFFSET = 4; private static final int TYPE_HEADER = 0; private static final int TYPE_PIN = 1; private static final int TYPE_LINK = 2; - private static final int TYPE_DIVIDER = 3; + private static final int TYPE_BOARD_INPUT = 3; + private static final int TYPE_DIVIDER = 4; private static final Comparator SORT_PINS = new Comparator() { @Override @@ -71,7 +73,7 @@ public class PinAdapter extends RecyclerView.Adapter { private List pins = new ArrayList<>(); private Pin highlighted; - public PinAdapter(Callback callback) { + public DrawerAdapter(Callback callback) { watchManager = Chan.getWatchManager(); this.callback = callback; setHasStableIds(true); @@ -125,6 +127,8 @@ public class PinAdapter extends RecyclerView.Adapter { return new PinViewHolder(LayoutInflater.from(parent.getContext()).inflate(R.layout.cell_pin, parent, false)); case TYPE_LINK: return new LinkHolder(LayoutInflater.from(parent.getContext()).inflate(R.layout.cell_link, parent, false)); + case TYPE_BOARD_INPUT: + return new BoardInputHolder(LayoutInflater.from(parent.getContext()).inflate(R.layout.cell_board_input, parent, false)); case TYPE_DIVIDER: return new DividerHolder(LayoutInflater.from(parent.getContext()).inflate(R.layout.cell_divider, parent, false)); } @@ -160,6 +164,8 @@ public class PinAdapter extends RecyclerView.Adapter { break; } break; + case TYPE_BOARD_INPUT: + break; case TYPE_DIVIDER: ((DividerHolder) holder).withBackground(position != 0); break; @@ -188,6 +194,8 @@ public class PinAdapter extends RecyclerView.Adapter { case 1: return TYPE_LINK; case 2: + return TYPE_BOARD_INPUT; + case 3: return TYPE_HEADER; default: return TYPE_PIN; @@ -229,7 +237,7 @@ public class PinAdapter extends RecyclerView.Adapter { } } - public void updatePinViewHolder(PinViewHolder holder, Pin pin) { + private void updatePinViewHolder(PinViewHolder holder, Pin pin) { CharSequence text = pin.loadable.title; if (pin.archived) { text = TextUtils.concat(PostHelper.addIcon(PostHelper.archivedIcon, sp(14 + 2)), text); @@ -289,13 +297,13 @@ public class PinAdapter extends RecyclerView.Adapter { notifyDataSetChanged(); } - public class PinViewHolder extends RecyclerView.ViewHolder { + private class PinViewHolder extends RecyclerView.ViewHolder { private boolean highlighted; private ThumbnailView image; private TextView textView; private TextView watchCountText; - public PinViewHolder(View itemView) { + private PinViewHolder(View itemView) { super(itemView); image = (ThumbnailView) itemView.findViewById(R.id.thumb); image.setCircular(true); @@ -345,7 +353,7 @@ public class PinAdapter extends RecyclerView.Adapter { private ImageView clear; private ImageView settings; - public HeaderHolder(View itemView) { + private HeaderHolder(View itemView) { super(itemView); text = (TextView) itemView.findViewById(R.id.text); text.setTypeface(ROBOTO_MEDIUM); @@ -375,11 +383,11 @@ public class PinAdapter extends RecyclerView.Adapter { } } - public class LinkHolder extends RecyclerView.ViewHolder { + private class LinkHolder extends RecyclerView.ViewHolder { private ImageView image; private TextView text; - public LinkHolder(View itemView) { + private LinkHolder(View itemView) { super(itemView); image = (ImageView) itemView.findViewById(R.id.image); text = (TextView) itemView.findViewById(R.id.text); @@ -401,16 +409,25 @@ public class PinAdapter extends RecyclerView.Adapter { } } - public class DividerHolder extends RecyclerView.ViewHolder { + private class BoardInputHolder extends RecyclerView.ViewHolder { + private EditText input; + + private BoardInputHolder(View itemView) { + super(itemView); + input = (EditText) itemView.findViewById(R.id.input); + } + } + + private class DividerHolder extends RecyclerView.ViewHolder { private boolean withBackground = false; private View divider; - public DividerHolder(View itemView) { + private DividerHolder(View itemView) { super(itemView); divider = itemView.findViewById(R.id.divider); } - public void withBackground(boolean withBackground) { + private void withBackground(boolean withBackground) { if (withBackground != this.withBackground) { this.withBackground = withBackground; if (withBackground) { diff --git a/Clover/app/src/main/java/org/floens/chan/ui/cell/ThreadStatusCell.java b/Clover/app/src/main/java/org/floens/chan/ui/cell/ThreadStatusCell.java index 0cf4b0ad..64a34f2f 100644 --- a/Clover/app/src/main/java/org/floens/chan/ui/cell/ThreadStatusCell.java +++ b/Clover/app/src/main/java/org/floens/chan/ui/cell/ThreadStatusCell.java @@ -122,7 +122,7 @@ public class ThreadStatusCell extends LinearLayout implements View.OnClickListen Post op = chanThread.op; - Board board = Chan.getBoardManager().getBoardByCode(chanThread.loadable.board); + Board board = Chan.getBoardManager().getBoardByCode(chanThread.loadable.boardCode); if (board != null) { SpannableString replies = new SpannableString(op.replies + "R"); if (op.replies >= board.bumpLimit) { 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 4c4377bc..22a4143e 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 @@ -19,6 +19,7 @@ package org.floens.chan.ui.controller; import android.annotation.SuppressLint; import android.app.Activity; +import android.app.ProgressDialog; import android.content.Context; import android.view.LayoutInflater; import android.view.View; @@ -65,6 +66,8 @@ public class BrowseController extends ThreadController implements ToolbarMenuIte private PostsFilter.Order order; private List boardItems; + private ProgressDialog waitingForBoardsDialog; + private FloatingMenuItem viewModeMenuItem; private ToolbarMenuItem search; private ToolbarMenuItem refresh; @@ -135,7 +138,7 @@ public class BrowseController extends ThreadController implements ToolbarMenuIte break; case SHARE_ID: case OPEN_BROWSER_ID: - String link = ChanUrls.getCatalogUrlDesktop(threadLayout.getPresenter().getLoadable().board); + String link = ChanUrls.getCatalogUrlDesktop(threadLayout.getPresenter().getLoadable().boardCode); if (id == SHARE_ID) { AndroidUtils.shareLink(link); @@ -299,10 +302,18 @@ public class BrowseController extends ThreadController implements ToolbarMenuIte loadBoards(); } + public void loadDefault() { + BoardManager boardManager = Chan.getBoardManager(); + List savedBoards = boardManager.getSavedBoards(); + if (!savedBoards.isEmpty()) { + loadBoard(savedBoards.get(0)); + } + } + public void loadBoard(Board board) { - Loadable loadable = databaseManager.getDatabaseLoadableManager().get(Loadable.forCatalog(board.code)); - loadable.title = board.name; - navigationItem.title = board.name; + Loadable loadable = databaseManager.getDatabaseLoadableManager().get(Loadable.forCatalog(board)); + loadable.title = board.getName(); + navigationItem.title = board.getName(); ThreadPresenter presenter = threadLayout.getPresenter(); presenter.unbindLoadable(); @@ -318,23 +329,47 @@ public class BrowseController extends ThreadController implements ToolbarMenuIte ((ToolbarNavigationController) navigationController).toolbar.updateTitle(navigationItem); } + /** + * Load the menu with saved boards. Called on creation of this controller and when there's a + * board change event. + */ private void loadBoards() { List boards = Chan.getBoardManager().getSavedBoards(); - boardItems = new ArrayList<>(); - for (Board board : boards) { - FloatingMenuItem item = new FloatingMenuItemBoard(board); - boardItems.add(item); - } - navigationItem.middleMenu.setItems(boardItems); - navigationItem.middleMenu.setAdapter(new BoardsAdapter(context, boardItems)); + if (boards.isEmpty()) { + if (waitingForBoardsDialog == null) { + String title = getString(R.string.thread_fetching_boards_title); + String message = getString(R.string.thread_fetching_boards); + waitingForBoardsDialog = ProgressDialog.show(context, title, message, true, false); + waitingForBoardsDialog.show(); + } + } else { + boolean wasWaiting = waitingForBoardsDialog != null; + if (waitingForBoardsDialog != null) { + waitingForBoardsDialog.dismiss(); + waitingForBoardsDialog = null; + } + + boardItems = new ArrayList<>(); + for (Board board : boards) { + FloatingMenuItem item = new FloatingMenuItemBoard(board); + boardItems.add(item); + } + + navigationItem.middleMenu.setItems(boardItems); + navigationItem.middleMenu.setAdapter(new BoardsAdapter(context, boardItems)); + + if (wasWaiting) { + loadDefault(); + } + } } private static class FloatingMenuItemBoard extends FloatingMenuItem { public Board board; public FloatingMenuItemBoard(Board board) { - super(board.id, board.name); + super(board.id, board.getName()); this.board = board; } } diff --git a/Clover/app/src/main/java/org/floens/chan/ui/controller/DrawerController.java b/Clover/app/src/main/java/org/floens/chan/ui/controller/DrawerController.java index 91b9a59b..2a873c58 100644 --- a/Clover/app/src/main/java/org/floens/chan/ui/controller/DrawerController.java +++ b/Clover/app/src/main/java/org/floens/chan/ui/controller/DrawerController.java @@ -41,7 +41,7 @@ import org.floens.chan.controller.NavigationController; import org.floens.chan.core.manager.WatchManager; import org.floens.chan.core.model.Pin; import org.floens.chan.core.settings.ChanSettings; -import org.floens.chan.ui.adapter.PinAdapter; +import org.floens.chan.ui.adapter.DrawerAdapter; import org.floens.chan.utils.AndroidUtils; import java.util.List; @@ -53,7 +53,7 @@ import static org.floens.chan.utils.AndroidUtils.ROBOTO_MEDIUM; import static org.floens.chan.utils.AndroidUtils.dp; import static org.floens.chan.utils.AndroidUtils.fixSnackbarText; -public class DrawerController extends Controller implements PinAdapter.Callback, View.OnClickListener { +public class DrawerController extends Controller implements DrawerAdapter.Callback, View.OnClickListener { private WatchManager watchManager; protected FrameLayout container; @@ -61,7 +61,7 @@ public class DrawerController extends Controller implements PinAdapter.Callback, protected LinearLayout drawer; protected RecyclerView recyclerView; protected LinearLayout settings; - protected PinAdapter pinAdapter; + protected DrawerAdapter drawerAdapter; public DrawerController(Context context) { super(context); @@ -88,12 +88,12 @@ public class DrawerController extends Controller implements PinAdapter.Callback, theme().settingsDrawable.apply((ImageView) settings.findViewById(R.id.image)); ((TextView) settings.findViewById(R.id.text)).setTypeface(ROBOTO_MEDIUM); - pinAdapter = new PinAdapter(this); - recyclerView.setAdapter(pinAdapter); + drawerAdapter = new DrawerAdapter(this); + recyclerView.setAdapter(drawerAdapter); - pinAdapter.onPinsChanged(watchManager.getAllPins()); + drawerAdapter.onPinsChanged(watchManager.getAllPins()); - ItemTouchHelper itemTouchHelper = new ItemTouchHelper(pinAdapter.getItemTouchHelperCallback()); + ItemTouchHelper itemTouchHelper = new ItemTouchHelper(drawerAdapter.getItemTouchHelperCallback()); itemTouchHelper.attachToRecyclerView(recyclerView); updateBadge(); @@ -148,11 +148,11 @@ public class DrawerController extends Controller implements PinAdapter.Callback, } @Override - public void onHeaderClicked(PinAdapter.HeaderHolder holder, PinAdapter.HeaderAction headerAction) { - if (headerAction == PinAdapter.HeaderAction.SETTINGS) { + public void onHeaderClicked(DrawerAdapter.HeaderHolder holder, DrawerAdapter.HeaderAction headerAction) { + if (headerAction == DrawerAdapter.HeaderAction.SETTINGS) { openController(new WatchSettingsController(context)); - } else if (headerAction == PinAdapter.HeaderAction.CLEAR || headerAction == PinAdapter.HeaderAction.CLEAR_ALL) { - boolean all = headerAction == PinAdapter.HeaderAction.CLEAR_ALL || !ChanSettings.watchEnabled.get(); + } else if (headerAction == DrawerAdapter.HeaderAction.CLEAR || headerAction == DrawerAdapter.HeaderAction.CLEAR_ALL) { + boolean all = headerAction == DrawerAdapter.HeaderAction.CLEAR_ALL || !ChanSettings.watchEnabled.get(); final List pins = watchManager.clearPins(all); if (!pins.isEmpty()) { String text = context.getResources().getQuantityString(R.plurals.bookmark, pins.size(), pins.size()); @@ -232,23 +232,23 @@ public class DrawerController extends Controller implements PinAdapter.Callback, } public void setPinHighlighted(Pin pin) { - pinAdapter.setPinHighlighted(pin); - pinAdapter.updateHighlighted(recyclerView); + drawerAdapter.setPinHighlighted(pin); + drawerAdapter.updateHighlighted(recyclerView); } public void onEvent(WatchManager.PinAddedMessage message) { - pinAdapter.onPinAdded(message.pin); + drawerAdapter.onPinAdded(message.pin); drawerLayout.openDrawer(drawer); updateBadge(); } public void onEvent(WatchManager.PinRemovedMessage message) { - pinAdapter.onPinRemoved(message.pin); + drawerAdapter.onPinRemoved(message.pin); updateBadge(); } public void onEvent(WatchManager.PinChangedMessage message) { - pinAdapter.onPinChanged(recyclerView, message.pin); + drawerAdapter.onPinChanged(recyclerView, message.pin); updateBadge(); } diff --git a/Clover/app/src/main/java/org/floens/chan/ui/controller/HistoryController.java b/Clover/app/src/main/java/org/floens/chan/ui/controller/HistoryController.java index 7f318f14..6238e909 100644 --- a/Clover/app/src/main/java/org/floens/chan/ui/controller/HistoryController.java +++ b/Clover/app/src/main/java/org/floens/chan/ui/controller/HistoryController.java @@ -209,7 +209,7 @@ public class HistoryController extends Controller implements CompoundButton.OnCh } holder.text.setText(history.loadable.title); - Board board = boardManager.getBoardByCode(history.loadable.board); + Board board = boardManager.getBoardByCode(history.loadable.boardCode); holder.subtext.setText(board == null ? null : ("/" + board.code + "/ \u2013 " + board.name)); } diff --git a/Clover/app/src/main/java/org/floens/chan/ui/controller/ImageViewerController.java b/Clover/app/src/main/java/org/floens/chan/ui/controller/ImageViewerController.java index 862bb50e..45e958dc 100644 --- a/Clover/app/src/main/java/org/floens/chan/ui/controller/ImageViewerController.java +++ b/Clover/app/src/main/java/org/floens/chan/ui/controller/ImageViewerController.java @@ -232,7 +232,7 @@ public class ImageViewerController extends Controller implements ImageViewerPres ImageSaveTask task = new ImageSaveTask(postImage); task.setShare(share); if (ChanSettings.saveBoardFolder.get()) { - task.setSubFolder(presenter.getLoadable().board); + task.setSubFolder(presenter.getLoadable().boardCode); } ImageSaver.getInstance().startDownloadTask(context, task); } 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 712b7cfe..c3ab73e7 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 @@ -55,7 +55,7 @@ public class ReportController extends Controller { WebView webView = new WebView(context); webView.getSettings().setJavaScriptEnabled(true); webView.getSettings().setDomStorageEnabled(true); - webView.loadUrl(ChanUrls.getReportUrl(post.board, post.no)); + webView.loadUrl(ChanUrls.getReportUrl(post.boardId, post.no)); view = webView; } } 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 6273a147..2be54801 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 @@ -39,10 +39,12 @@ import android.widget.TextView; import org.floens.chan.R; import org.floens.chan.chan.ChanParser; import org.floens.chan.controller.Controller; +import org.floens.chan.core.model.Board; import org.floens.chan.core.model.Loadable; import org.floens.chan.core.model.Post; import org.floens.chan.core.model.PostLinkable; import org.floens.chan.core.settings.ChanSettings; +import org.floens.chan.core.site.Sites; import org.floens.chan.ui.activity.StartActivity; import org.floens.chan.ui.cell.PostCell; import org.floens.chan.ui.theme.Theme; @@ -65,7 +67,7 @@ import static org.floens.chan.utils.AndroidUtils.getString; public class ThemeSettingsController extends Controller implements View.OnClickListener { private PostCell.PostCellCallback DUMMY_POST_CALLBACK = new PostCell.PostCellCallback() { - private Loadable loadable = Loadable.forThread("g", 1234); + private Loadable loadable = Loadable.forThread(Sites.defaultSite(), new Board(Sites.defaultSite(), "a", "a", false, false), 1234); @Override public Loadable getLoadable() { 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 6403cb30..3b85c1b0 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 @@ -142,9 +142,9 @@ public abstract class ThreadController extends Controller implements ThreadLayou if (loadable != null) { if (loadable.isThreadMode()) { - url = ChanUrls.getThreadUrlDesktop(loadable.board, loadable.no); + url = ChanUrls.getThreadUrlDesktop(loadable.boardCode, loadable.no); } else if (loadable.isCatalogMode()) { - url = ChanUrls.getCatalogUrlDesktop(loadable.board); + url = ChanUrls.getCatalogUrlDesktop(loadable.boardCode); } } 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 3f483976..6d0228d6 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 @@ -135,7 +135,7 @@ public class ViewThreadController extends ThreadController implements ThreadLayo } }) .setTitle(R.string.open_thread_confirmation) - .setMessage("/" + threadLoadable.board + "/" + threadLoadable.no) + .setMessage("/" + threadLoadable.boardCode + "/" + threadLoadable.no) .show(); } @@ -198,7 +198,7 @@ public class ViewThreadController extends ThreadController implements ThreadLayo case SHARE_ID: case OPEN_BROWSER_ID: Loadable loadable = threadLayout.getPresenter().getLoadable(); - String link = ChanUrls.getThreadUrlDesktop(loadable.board, loadable.no); + String link = ChanUrls.getThreadUrlDesktop(loadable.boardCode, loadable.no); if (id == SHARE_ID) { AndroidUtils.shareLink(link); diff --git a/Clover/app/src/main/java/org/floens/chan/ui/helper/PostHelper.java b/Clover/app/src/main/java/org/floens/chan/ui/helper/PostHelper.java index f739eb69..c6f8bcbc 100644 --- a/Clover/app/src/main/java/org/floens/chan/ui/helper/PostHelper.java +++ b/Clover/app/src/main/java/org/floens/chan/ui/helper/PostHelper.java @@ -71,15 +71,15 @@ public class PostHelper { if (!TextUtils.isEmpty(post.subject)) { return post.subject; } else if (!TextUtils.isEmpty(post.comment)) { - return "/" + post.board + "/ \u2013 " + post.comment.subSequence(0, Math.min(post.comment.length(), 200)).toString(); + return "/" + post.boardId + "/ \u2013 " + post.comment.subSequence(0, Math.min(post.comment.length(), 200)).toString(); } else { - return "/" + post.board + "/" + post.no; + return "/" + post.boardId + "/" + post.no; } } else if (loadable != null) { if (loadable.mode == Loadable.Mode.CATALOG) { - return "/" + loadable.board + "/"; + return "/" + loadable.boardCode + "/"; } else { - return "/" + loadable.board + "/" + loadable.no; + return "/" + loadable.boardCode + "/" + loadable.no; } } else { return ""; 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 e4956d22..f096263c 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 @@ -301,6 +301,11 @@ public class ReplyLayout extends LoadView implements View.OnClickListener, Anima } } + @Override + public void openMessageWebview(String rawMessage) { +// callback. + } + @Override public void onPosted() { Toast.makeText(getContext(), R.string.reply_success, Toast.LENGTH_SHORT).show(); 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 52c54a84..4f6ccea4 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 @@ -411,7 +411,7 @@ public class ThreadLayout extends CoordinatorLayout implements ThreadPresenter.T @Override public void hideThread(Post post) { - final ThreadHide threadHide = new ThreadHide(post.board, post.no); + final ThreadHide threadHide = new ThreadHide(post.boardId, post.no); databaseManager.addThreadHide(threadHide); presenter.refreshUI(); diff --git a/Clover/app/src/main/res/layout/cell_board_input.xml b/Clover/app/src/main/res/layout/cell_board_input.xml new file mode 100644 index 00000000..07f4c308 --- /dev/null +++ b/Clover/app/src/main/res/layout/cell_board_input.xml @@ -0,0 +1,36 @@ + + + + + + diff --git a/Clover/app/src/main/res/values/strings.xml b/Clover/app/src/main/res/values/strings.xml index 76c052ab..4c09207d 100644 --- a/Clover/app/src/main/res/values/strings.xml +++ b/Clover/app/src/main/res/values/strings.xml @@ -180,6 +180,8 @@ along with this program. If not, see . Failed to open image Spoiler image + Fetching boards + Please wait for the boards to be fetched, this may take up to a minute. Add more… HTTPS error Network error diff --git a/Clover/build.gradle b/Clover/build.gradle index 305dc055..e7e3c60b 100644 --- a/Clover/build.gradle +++ b/Clover/build.gradle @@ -4,7 +4,7 @@ buildscript { jcenter() } dependencies { - classpath 'com.android.tools.build:gradle:2.2.0' + classpath 'com.android.tools.build:gradle:2.2.2' } } diff --git a/docs/database.txt b/docs/database.txt index a954e06a..42cde435 100644 --- a/docs/database.txt +++ b/docs/database.txt @@ -64,3 +64,7 @@ ALTER TABLE loadable ADD COLUMN lastViewed default -1; Changes in version 21: ALTER TABLE loadable ADD COLUMN lastLoaded default -1; + + +Changes in version 22: +ALTER TABLE loadable ADD COLUMN lastLoaded default -1;