Begin of multisite support

A lot of random stuff, but mostly support for having Site and Board /objects/ on the database models.
Start of interface for a wide range of sites.
multisite
Floens 9 years ago
parent a613a0dec4
commit 3a652cf51e
  1. 13
      Clover/app/src/main/java/org/floens/chan/chan/ChanHelper.java
  2. 2
      Clover/app/src/main/java/org/floens/chan/chan/ChanLoader.java
  3. 2
      Clover/app/src/main/java/org/floens/chan/chan/ChanParser.java
  4. 38
      Clover/app/src/main/java/org/floens/chan/chan/ChanUrls.java
  5. 83
      Clover/app/src/main/java/org/floens/chan/core/database/DatabaseBoardManager.java
  6. 13
      Clover/app/src/main/java/org/floens/chan/core/database/DatabaseHelper.java
  7. 10
      Clover/app/src/main/java/org/floens/chan/core/database/DatabaseLoadableManager.java
  8. 57
      Clover/app/src/main/java/org/floens/chan/core/database/DatabaseManager.java
  9. 18
      Clover/app/src/main/java/org/floens/chan/core/database/DatabaseSavedReplyManager.java
  10. 2
      Clover/app/src/main/java/org/floens/chan/core/http/ReplyHttpCall.java
  11. 59
      Clover/app/src/main/java/org/floens/chan/core/manager/BoardManager.java
  12. 112
      Clover/app/src/main/java/org/floens/chan/core/model/Board.java
  13. 1
      Clover/app/src/main/java/org/floens/chan/core/model/Filter.java
  14. 69
      Clover/app/src/main/java/org/floens/chan/core/model/Loadable.java
  15. 3
      Clover/app/src/main/java/org/floens/chan/core/model/Pin.java
  16. 28
      Clover/app/src/main/java/org/floens/chan/core/model/Post.java
  17. 3
      Clover/app/src/main/java/org/floens/chan/core/model/SavedReply.java
  18. 3
      Clover/app/src/main/java/org/floens/chan/core/model/ThreadHide.java
  19. 24
      Clover/app/src/main/java/org/floens/chan/core/net/ChanReaderRequest.java
  20. 1
      Clover/app/src/main/java/org/floens/chan/core/net/JsonReaderRequest.java
  21. 2
      Clover/app/src/main/java/org/floens/chan/core/net/PostParseCallable.java
  22. 17
      Clover/app/src/main/java/org/floens/chan/core/presenter/ReplyPresenter.java
  23. 28
      Clover/app/src/main/java/org/floens/chan/core/presenter/ThreadPresenter.java
  24. 13
      Clover/app/src/main/java/org/floens/chan/core/site/Boards.java
  25. 86
      Clover/app/src/main/java/org/floens/chan/core/site/Site.java
  26. 24
      Clover/app/src/main/java/org/floens/chan/core/site/SiteEndpoints.java
  27. 37
      Clover/app/src/main/java/org/floens/chan/core/site/Sites.java
  28. 165
      Clover/app/src/main/java/org/floens/chan/core/site/sites/chan4/Chan4.java
  29. 20
      Clover/app/src/main/java/org/floens/chan/core/site/sites/chan4/Chan4BoardsRequest.java
  30. 3
      Clover/app/src/main/java/org/floens/chan/test/TestActivity.java
  31. 6
      Clover/app/src/main/java/org/floens/chan/ui/activity/StartActivity.java
  32. 43
      Clover/app/src/main/java/org/floens/chan/ui/adapter/DrawerAdapter.java
  33. 2
      Clover/app/src/main/java/org/floens/chan/ui/cell/ThreadStatusCell.java
  34. 59
      Clover/app/src/main/java/org/floens/chan/ui/controller/BrowseController.java
  35. 32
      Clover/app/src/main/java/org/floens/chan/ui/controller/DrawerController.java
  36. 2
      Clover/app/src/main/java/org/floens/chan/ui/controller/HistoryController.java
  37. 2
      Clover/app/src/main/java/org/floens/chan/ui/controller/ImageViewerController.java
  38. 2
      Clover/app/src/main/java/org/floens/chan/ui/controller/ReportController.java
  39. 4
      Clover/app/src/main/java/org/floens/chan/ui/controller/ThemeSettingsController.java
  40. 4
      Clover/app/src/main/java/org/floens/chan/ui/controller/ThreadController.java
  41. 4
      Clover/app/src/main/java/org/floens/chan/ui/controller/ViewThreadController.java
  42. 8
      Clover/app/src/main/java/org/floens/chan/ui/helper/PostHelper.java
  43. 5
      Clover/app/src/main/java/org/floens/chan/ui/layout/ReplyLayout.java
  44. 2
      Clover/app/src/main/java/org/floens/chan/ui/layout/ThreadLayout.java
  45. 36
      Clover/app/src/main/res/layout/cell_board_input.xml
  46. 2
      Clover/app/src/main/res/values/strings.xml
  47. 2
      Clover/build.gradle
  48. 4
      docs/database.txt

@ -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<String> 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;
}

@ -308,7 +308,7 @@ public class ChanLoader implements Response.ErrorListener, Response.Listener<Cha
}
private ChanReaderRequest getData() {
Logger.d(TAG, "Requested " + loadable.board + ", " + loadable.no);
Logger.d(TAG, "Requested " + loadable.boardCode + ", " + loadable.no);
List<Post> cached = thread == null ? new ArrayList<Post>() : thread.posts;
ChanReaderRequest request = ChanReaderRequest.newInstance(loadable, cached, this, this);

@ -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)";
}
}

@ -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";
}

@ -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.<br>
* 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<Void> setBoards(final List<Board> boards) {
return new Callable<Void>() {
@Override
public Void call() throws Exception {
for (Board b : boards) {
helper.boardsDao.createOrUpdate(b);
}
return null;
}
};
}
public Callable<List<Board>> getBoards(final Site site) {
return new Callable<List<Board>>() {
@Override
public List<Board> call() throws Exception {
List<Board> 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<List<Board>> getAllBoards() {
return new Callable<List<Board>>() {
@Override
public List<Board> call() throws Exception {
List<Board> 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;
}
};
}
}

@ -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<Pin, Integer> pinDao;
public Dao<Loadable, Integer> 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() {

@ -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<Loadable, Integer> builder = helper.loadableDao.queryBuilder();
List<Loadable> 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);

@ -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.<br>
* <b>All database queries are run on a single database thread</b>, 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)}.<br>
* You often want the sync flavour for queries that return data, it waits for the task to be finished on the other thread.<br>
* 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<Board> boards) {
try {
callInTransaction(helper.getConnectionSource(), new Callable<Void>() {
@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<Board> getBoards() {
List<Board> 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;
}
}

@ -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<Integer, List<SavedReply>> 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.<br>
* 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<SavedReply> items = savedRepliesByNo.get(no);

@ -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()) {

@ -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<List<Board>>, Response.ErrorListener {
public class BoardManager {
private static final String TAG = "BoardManager";
private static final Comparator<Board> ORDER_SORT = new Comparator<Board>() {
@ -54,6 +50,7 @@ public class BoardManager implements Response.Listener<List<Board>>, Response.Er
};
private final DatabaseManager databaseManager;
private final Site defaultSite;
private final List<Board> boards;
private final List<Board> savedBoards = new ArrayList<>();
@ -61,34 +58,31 @@ public class BoardManager implements Response.Listener<List<Board>>, 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<Board> response) {
private void appendBoards(Boards response) {
List<Board> boardsToAddWs = new ArrayList<>();
List<Board> 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<List<Board>>, 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<List<Board>>, Response.Er
}
private void saveDatabase() {
databaseManager.setBoards(boards);
}
private List<Board> getDefaultBoards() {
List<Board> 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<Board> filterSaved(List<Board> all) {

@ -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}.<br>
* {@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;
}
}

@ -40,6 +40,7 @@ public class Filter {
@DatabaseField(canBeNull = false)
public boolean allBoards = true;
// TODO(multi-site)
@DatabaseField(canBeNull = false)
public String boards;

@ -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:<br>
* - It keeps track of the list index where the user last viewed.<br>
* - It keeps track of what post was last seen and loaded.<br>
* - It keeps track of the title the toolbar should show, generated from the first post (so after loading).<br>
*/
@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;

@ -55,6 +55,9 @@ public class Pin {
@DatabaseField
public boolean archived = false;
public Pin() {
}
public int getNewPostCount() {
if (watchLastCount < 0 || watchNewCount < 0) {
return 0;

@ -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()) {

@ -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;

@ -25,6 +25,9 @@ public class ThreadHide {
@DatabaseField(generatedId = true)
public int id;
@DatabaseField(columnName = "site")
public int site;
@DatabaseField
public String board;

@ -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.<br>
* 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<ChanReaderRequest.ChanReaderResponse> {
private static final String TAG = "ChanReaderRequest";
private static final boolean LOG_TIMING = false;
@ -71,13 +75,22 @@ public class ChanReaderRequest extends JsonReaderRequest<ChanReaderRequest.ChanR
databaseSavedReplyManager = databaseManager.getDatabaseSavedReplyManager();
}
public static ChanReaderRequest newInstance(Loadable loadable, List<Post> cached, Listener<ChanReaderResponse> listener, ErrorListener errorListener) {
public static ChanReaderRequest newInstance(
Loadable loadable, List<Post> cached, Listener<ChanReaderResponse> 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<ChanReaderRequest.ChanR
} else {
String[] boardCodes = filter.boardCodes();
for (String code : boardCodes) {
if (code.equals(loadable.board)) {
if (code.equals(loadable.boardCode)) {
// copy the filter because it will get used on other threads
request.filters.add(filter.copy());
break;
@ -334,6 +347,7 @@ public class ChanReaderRequest extends JsonReaderRequest<ChanReaderRequest.ChanR
private void readPostObject(JsonReader reader, ProcessingQueue queue, Map<Integer, Post> cachedByNo) throws Exception {
Post post = new Post();
post.board = loadable.board;
post.boardId = loadable.boardCode;
reader.beginObject();
while (reader.hasNext()) {

@ -59,7 +59,6 @@ public abstract class JsonReaderRequest<T> extends Request<T> {
try {
read = readJson(reader);
} catch (Exception e) {
exception = e;
}

@ -53,7 +53,7 @@ class PostParseCallable implements Callable<Post> {
return null;
}
post.isSavedReply = savedReplyManager.isSaved(post.board, post.no);
post.isSavedReply = savedReplyManager.isSaved(post.boardId, post.no);
return post;
}

@ -91,7 +91,7 @@ public class ReplyPresenter implements ReplyManager.HttpCallback<ReplyHttpCall>,
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<ReplyHttpCall>,
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<ReplyHttpCall>,
}
}
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<ReplyHttpCall>,
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<ReplyHttpCall>,
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<ReplyHttpCall>,
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<ReplyHttpCall>,
void openMessage(boolean open, boolean animate, String message, boolean autoHide);
void openMessageWebview(String rawMessage);
void onPosted();
void setCommentHint(String hint);

@ -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<DeleteHttpCall>() {
@ -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);

@ -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<Board> boards;
public Boards(List<Board> boards) {
this.boards = boards;
}
}

@ -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.<br>
* 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);
}
}

@ -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);
}

@ -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<? extends Site> 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;
}
}

@ -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() {
}
/**
* <b>Note: very special case, only this site may have 0 as the return value.<br>
* This is for backwards compatibility when we didn't support multi-site yet.</b>
*
* @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<Board> 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<List<Board>>() {
@Override
public void onResponse(List<Board> 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<Board> list = new ArrayList<>();
list.add(new Board(Chan4.this, "Technology", "g", true, true));
list.add(new Board(Chan4.this, "Food & Cooking", "ck", true, true));
list.add(new Board(Chan4.this, "Do It Yourself", "diy", true, true));
list.add(new Board(Chan4.this, "Animals & Nature", "an", true, true));
Collections.shuffle(list);
listener.onBoardsReceived(new Boards(list));
}
}));
}
}

@ -15,7 +15,7 @@
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <http://www.gnu.org/licenses/>.
*/
package org.floens.chan.core.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<List<Board>> {
public class Chan4BoardsRequest extends JsonReaderRequest<List<Board>> {
public static List<String> BLOCKED = Collections.singletonList(
"f"
);
@ -38,8 +40,11 @@ public class BoardsRequest extends JsonReaderRequest<List<Board>> {
// "a", "c", "w", "cm", "jp", "mlp", "lgbt"
// );
public BoardsRequest(String url, Listener<List<Board>> listener, ErrorListener errorListener) {
super(url, listener, errorListener);
private final Site site;
public Chan4BoardsRequest(Site site, Listener<List<Board>> listener, ErrorListener errorListener) {
super(site.endpoints().boards(), listener, errorListener);
this.site = site;
}
@Override
@ -69,15 +74,12 @@ public class BoardsRequest extends JsonReaderRequest<List<Board>> {
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();

@ -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

@ -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();

@ -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<RecyclerView.ViewHolder> {
public class DrawerAdapter extends RecyclerView.Adapter<RecyclerView.ViewHolder> {
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<Pin> SORT_PINS = new Comparator<Pin>() {
@Override
@ -71,7 +73,7 @@ public class PinAdapter extends RecyclerView.Adapter<RecyclerView.ViewHolder> {
private List<Pin> 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<RecyclerView.ViewHolder> {
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<RecyclerView.ViewHolder> {
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<RecyclerView.ViewHolder> {
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<RecyclerView.ViewHolder> {
}
}
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<RecyclerView.ViewHolder> {
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<RecyclerView.ViewHolder> {
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<RecyclerView.ViewHolder> {
}
}
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<RecyclerView.ViewHolder> {
}
}
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) {

@ -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) {

@ -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<FloatingMenuItem> 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<Board> 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<Board> 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;
}
}

@ -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<Pin> 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();
}

@ -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));
}

@ -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);
}

@ -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;
}
}

@ -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() {

@ -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);
}
}

@ -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);

@ -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 "";

@ -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();

@ -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();

@ -0,0 +1,36 @@
<?xml version="1.0" encoding="utf-8"?><!--
Clover - 4chan browser https://github.com/Floens/Clover/
Copyright (C) 2014 Floens
This program is free software: you can redistribute it and/or modify
it under the terms of the GNU General Public License as published by
the Free Software Foundation, either version 3 of the License, or
(at your option) any later version.
This program is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU General Public License for more details.
You should have received a copy of the GNU General Public License
along with this program. If not, see <http://www.gnu.org/licenses/>.
-->
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="match_parent"
android:layout_height="48dp"
android:orientation="horizontal">
<EditText
android:id="@+id/input"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:ellipsize="end"
android:gravity="center_vertical"
android:inputType="text"
android:maxLines="1"
android:paddingLeft="16dp"
android:paddingRight="16dp"
android:textColor="?text_color_primary"
android:textSize="14sp" />
</LinearLayout>

@ -180,6 +180,8 @@ along with this program. If not, see <http://www.gnu.org/licenses/>.
<string name="image_open_failed">Failed to open image</string>
<string name="image_spoiler_filename">Spoiler image</string>
<string name="thread_fetching_boards_title">Fetching boards</string>
<string name="thread_fetching_boards">Please wait for the boards to be fetched, this may take up to a minute.</string>
<string name="thread_board_select_add">Add more&#8230;</string>
<string name="thread_load_failed_ssl">HTTPS error</string>
<string name="thread_load_failed_network">Network error</string>

@ -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'
}
}

@ -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;

Loading…
Cancel
Save