diff --git a/Clover/app/proguard.cfg b/Clover/app/proguard.cfg
index fe0b56e0..11489a3d 100644
--- a/Clover/app/proguard.cfg
+++ b/Clover/app/proguard.cfg
@@ -147,3 +147,5 @@
-keepclassmembers class ** {
public void onEvent*(**);
}
+
+-keep public class * extends android.support.design.**
diff --git a/Clover/app/src/main/AndroidManifest.xml b/Clover/app/src/main/AndroidManifest.xml
index 86bb2077..f1efbee5 100644
--- a/Clover/app/src/main/AndroidManifest.xml
+++ b/Clover/app/src/main/AndroidManifest.xml
@@ -76,8 +76,6 @@ along with this program. If not, see .
-
-
diff --git a/Clover/app/src/main/java/org/floens/chan/Chan.java b/Clover/app/src/main/java/org/floens/chan/Chan.java
index 979e0c6b..2a9aee84 100644
--- a/Clover/app/src/main/java/org/floens/chan/Chan.java
+++ b/Clover/app/src/main/java/org/floens/chan/Chan.java
@@ -30,13 +30,13 @@ import com.squareup.leakcanary.RefWatcher;
import org.floens.chan.chan.ChanUrls;
import org.floens.chan.core.cache.FileCache;
+import org.floens.chan.core.database.DatabaseManager;
+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.net.BitmapLruImageCache;
-import org.floens.chan.core.http.ReplyManager;
import org.floens.chan.core.net.ProxiedHurlStack;
import org.floens.chan.core.settings.ChanSettings;
-import org.floens.chan.database.DatabaseManager;
import org.floens.chan.utils.AndroidUtils;
import org.floens.chan.utils.Logger;
@@ -51,7 +51,6 @@ public class Chan extends Application {
private static final long FILE_CACHE_DISK_SIZE = 50 * 1024 * 1024;
private static final String FILE_CACHE_NAME = "filecache";
- private static final int VOLLEY_LRU_CACHE_SIZE = 8 * 1024 * 1024;
private static final int VOLLEY_CACHE_SIZE = 10 * 1024 * 1024;
public static Context con;
@@ -152,9 +151,13 @@ public class Chan extends Application {
replyManager = new ReplyManager(this);
- String userAgent = getUserAgent();
+ String userAgent = getUserAgent();
volleyRequestQueue = Volley.newRequestQueue(this, userAgent, new ProxiedHurlStack(userAgent), new File(cacheDir, Volley.DEFAULT_CACHE_DIR), VOLLEY_CACHE_SIZE);
- imageLoader = new ImageLoader(volleyRequestQueue, new BitmapLruImageCache(VOLLEY_LRU_CACHE_SIZE));
+
+ final int runtimeMemory = (int) (Runtime.getRuntime().maxMemory() / 1024);
+ final int lruImageCacheSize = runtimeMemory / 8;
+
+ imageLoader = new ImageLoader(volleyRequestQueue, new BitmapLruImageCache(lruImageCacheSize));
fileCache = new FileCache(new File(cacheDir, FILE_CACHE_NAME), FILE_CACHE_DISK_SIZE, getUserAgent());
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 5bb70a13..127dca1f 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
@@ -19,6 +19,7 @@ package org.floens.chan.chan;
import android.text.TextUtils;
+import com.android.volley.RequestQueue;
import com.android.volley.Response;
import com.android.volley.VolleyError;
@@ -39,7 +40,7 @@ import java.util.concurrent.ScheduledExecutorService;
import java.util.concurrent.ScheduledFuture;
import java.util.concurrent.TimeUnit;
-public class ChanLoader {
+public class ChanLoader implements Response.ErrorListener, Response.Listener {
private static final String TAG = "ChanLoader";
private static final ScheduledExecutorService executor = Executors.newSingleThreadScheduledExecutor();
@@ -47,6 +48,7 @@ public class ChanLoader {
private final List listeners = new ArrayList<>();
private final Loadable loadable;
+ private final RequestQueue volleyRequestQueue;
private ChanThread thread;
private boolean destroyed = false;
@@ -64,6 +66,8 @@ public class ChanLoader {
if (loadable.mode == Loadable.Mode.BOARD) {
loadable.mode = Loadable.Mode.CATALOG;
}
+
+ volleyRequestQueue = Chan.getVolleyRequestQueue();
}
/**
@@ -149,11 +153,7 @@ public class ChanLoader {
public void requestMoreData() {
clearTimer();
- if (loadable.isThreadMode()) {
- if (request != null) {
- return;
- }
-
+ if (loadable.isThreadMode() && request == null) {
request = getData();
}
}
@@ -166,9 +166,6 @@ public class ChanLoader {
requestMoreData();
}
- /**
- * @return Returns if this loader is currently loading
- */
public boolean isLoading() {
return request != null;
}
@@ -193,10 +190,102 @@ public class ChanLoader {
return thread;
}
+ @Override
+ public void onResponse(ChanReaderRequest.ChanReaderResponse response) {
+ request = null;
+ if (destroyed)
+ return;
+
+ if (response.posts.size() == 0) {
+ onErrorResponse(new VolleyError("Post size is 0"));
+ return;
+ }
+
+ if (thread == null) {
+ thread = new ChanThread(loadable, new ArrayList());
+ }
+
+ thread.posts.clear();
+ thread.posts.addAll(response.posts);
+
+ processResponse(response);
+
+ if (TextUtils.isEmpty(loadable.title)) {
+ loadable.title = PostHelper.getTitle(thread.op, loadable);
+ }
+
+ for (Post post : thread.posts) {
+ post.title = loadable.title;
+ }
+
+ lastLoadTime = Time.get();
+
+ if (loadable.isThreadMode()) {
+ setTimer(response.posts.size());
+ }
+
+ for (ChanLoaderCallback l : listeners) {
+ l.onChanLoaderData(thread);
+ }
+ }
+
+ @Override
+ public void onErrorResponse(VolleyError error) {
+ request = null;
+ if (destroyed)
+ return;
+
+ Logger.i(TAG, "Loading error", error);
+
+ clearTimer();
+
+ for (ChanLoaderCallback l : listeners) {
+ l.onChanLoaderError(error);
+ }
+ }
+
+ /**
+ * Final processing af a response that needs to happen on the main thread.
+ *
+ * @param response Response to process
+ */
+ private void processResponse(ChanReaderRequest.ChanReaderResponse response) {
+ if (loadable.isThreadMode() && thread.posts.size() > 0) {
+ // Replace some op parameters to the real op (index 0).
+ // This is done on the main thread to avoid race conditions.
+ Post realOp = thread.posts.get(0);
+ thread.op = realOp;
+ Post fakeOp = response.op;
+ if (fakeOp != null) {
+ thread.closed = realOp.closed = fakeOp.closed;
+ thread.archived = realOp.archived = fakeOp.archived;
+ realOp.sticky = fakeOp.sticky;
+ realOp.replies = fakeOp.replies;
+ realOp.images = fakeOp.images;
+ realOp.uniqueIps = fakeOp.uniqueIps;
+ } else {
+ Logger.e(TAG, "Thread has no op!");
+ }
+ }
+
+ for (Post sourcePost : thread.posts) {
+ sourcePost.repliesFrom.clear();
+
+ for (Post replyToSource : thread.posts) {
+ if (replyToSource != sourcePost) {
+ if (replyToSource.repliesTo.contains(sourcePost.no)) {
+ sourcePost.repliesFrom.add(replyToSource.no);
+ }
+ }
+ }
+ }
+ }
+
private void setTimer(int postCount) {
clearTimer();
if (postCount > lastPostCount) {
+ lastPostCount = postCount;
currentTimeout = 0;
} else {
currentTimeout++;
@@ -209,8 +298,6 @@ public class ChanLoader {
currentTimeout = 4; // At least 60 seconds in the background
}
- lastPostCount = postCount;
-
if (autoReload) {
Runnable pendingRunnable = new Runnable() {
@Override
@@ -243,76 +330,13 @@ public class ChanLoader {
Logger.d(TAG, "Requested " + loadable.board + ", " + loadable.no);
List cached = thread == null ? new ArrayList() : thread.posts;
- ChanReaderRequest request = ChanReaderRequest.newInstance(loadable, cached,
- new Response.Listener>() {
- @Override
- public void onResponse(List list) {
- ChanLoader.this.request = null;
- onData(list);
- }
- }, new Response.ErrorListener() {
- @Override
- public void onErrorResponse(VolleyError error) {
- ChanLoader.this.request = null;
- onError(error);
- }
- }
- );
+ ChanReaderRequest request = ChanReaderRequest.newInstance(loadable, cached, this, this);
- Chan.getVolleyRequestQueue().add(request);
+ volleyRequestQueue.add(request);
return request;
}
- private void onData(List result) {
- if (destroyed)
- return;
-
- if (thread == null) {
- thread = new ChanThread(loadable, new ArrayList());
- }
-
- thread.posts.clear();
- thread.posts.addAll(result);
-
- if (loadable.isThreadMode() && thread.posts.size() > 0) {
- thread.op = thread.posts.get(0);
- thread.closed = thread.op.closed;
- thread.archived = thread.op.archived;
- }
-
- if (TextUtils.isEmpty(loadable.title)) {
- loadable.title = PostHelper.getTitle(thread.op, loadable);
- }
-
- for (Post post : thread.posts) {
- post.title = loadable.title;
- }
-
- lastLoadTime = Time.get();
-
- if (loadable.isThreadMode()) {
- setTimer(result.size());
- }
-
- for (ChanLoaderCallback l : listeners) {
- l.onChanLoaderData(thread);
- }
- }
-
- private void onError(VolleyError error) {
- if (destroyed)
- return;
-
- Logger.e(TAG, "Loading error");
-
- clearTimer();
-
- for (ChanLoaderCallback l : listeners) {
- l.onChanLoaderError(error);
- }
- }
-
public interface ChanLoaderCallback {
void onChanLoaderData(ChanThread result);
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 397acb6d..4f13f147 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
@@ -33,6 +33,7 @@ import org.floens.chan.Chan;
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.database.DatabaseManager;
import org.floens.chan.ui.theme.Theme;
import org.floens.chan.ui.theme.ThemeHelper;
import org.floens.chan.utils.Logger;
@@ -60,6 +61,11 @@ public class ChanParser {
private static final Pattern colorPattern = Pattern.compile("color:#([0-9a-fA-F]*)");
private static ChanParser instance = new ChanParser();
+ private final DatabaseManager databaseManager;
+
+ public ChanParser() {
+ databaseManager = Chan.getDatabaseManager();
+ }
public static ChanParser getInstance() {
return instance;
@@ -404,8 +410,7 @@ public class ChanParser {
}
// Append You when it's a reply to an saved reply
- // todo synchronized
- if (Chan.getDatabaseManager().isSavedReply(post.board, id)) {
+ if (databaseManager.isSavedReply(post.board, 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 3d72c3e0..e448c9b5 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
@@ -62,10 +62,6 @@ public class ChanUrls {
return scheme + "://s.4cdn.org/image/country/" + countryCode.toLowerCase(Locale.ENGLISH) + ".gif";
}
- public static String getTrollCountryFlagUrl(String countryCode) {
- return scheme + "://s.4cdn.org/image/country/troll/" + countryCode.toLowerCase(Locale.ENGLISH) + ".gif";
- }
-
public static String getBoardsUrl() {
return scheme + "://a.4cdn.org/boards.json";
}
diff --git a/Clover/app/src/main/java/org/floens/chan/database/DatabaseHelper.java b/Clover/app/src/main/java/org/floens/chan/core/database/DatabaseHelper.java
similarity index 86%
rename from Clover/app/src/main/java/org/floens/chan/database/DatabaseHelper.java
rename to Clover/app/src/main/java/org/floens/chan/core/database/DatabaseHelper.java
index 5e7d32be..6e57372d 100644
--- a/Clover/app/src/main/java/org/floens/chan/database/DatabaseHelper.java
+++ b/Clover/app/src/main/java/org/floens/chan/core/database/DatabaseHelper.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.database;
+package org.floens.chan.core.database;
import android.content.Context;
import android.database.sqlite.SQLiteDatabase;
@@ -26,6 +26,7 @@ import com.j256.ormlite.support.ConnectionSource;
import com.j256.ormlite.table.TableUtils;
import org.floens.chan.core.model.Board;
+import org.floens.chan.core.model.History;
import org.floens.chan.core.model.Loadable;
import org.floens.chan.core.model.Pin;
import org.floens.chan.core.model.SavedReply;
@@ -41,13 +42,14 @@ public class DatabaseHelper extends OrmLiteSqliteOpenHelper {
private static final String TAG = "DatabaseHelper";
private static final String DATABASE_NAME = "ChanDB";
- private static final int DATABASE_VERSION = 16;
+ private static final int DATABASE_VERSION = 18;
public Dao pinDao;
public Dao loadableDao;
public Dao savedDao;
public Dao boardsDao;
public Dao threadHideDao;
+ public Dao historyDao;
private final Context context;
@@ -62,6 +64,7 @@ public class DatabaseHelper extends OrmLiteSqliteOpenHelper {
savedDao = getDao(SavedReply.class);
boardsDao = getDao(Board.class);
threadHideDao = getDao(ThreadHide.class);
+ historyDao = getDao(History.class);
} catch (SQLException e) {
Logger.e(TAG, "Error creating Daos", e);
}
@@ -75,6 +78,7 @@ public class DatabaseHelper extends OrmLiteSqliteOpenHelper {
TableUtils.createTable(connectionSource, SavedReply.class);
TableUtils.createTable(connectionSource, Board.class);
TableUtils.createTable(connectionSource, ThreadHide.class);
+ TableUtils.createTable(connectionSource, History.class);
} catch (SQLException e) {
Logger.e(TAG, "Error creating db", e);
throw new RuntimeException(e);
@@ -150,11 +154,27 @@ public class DatabaseHelper extends OrmLiteSqliteOpenHelper {
if (oldVersion < 16) {
try {
- TableUtils.createTable(connectionSource, ThreadHide.class);
+ threadHideDao.executeRawNoArgs("CREATE TABLE `threadhide` (`board` VARCHAR , `id` INTEGER PRIMARY KEY AUTOINCREMENT , `no` INTEGER );");
} catch (SQLException e) {
Logger.e(TAG, "Error upgrading to version 16", e);
}
}
+
+ if (oldVersion < 17) {
+ try {
+ boardsDao.executeRawNoArgs("ALTER TABLE board ADD COLUMN description TEXT;");
+ } catch (SQLException e) {
+ Logger.e(TAG, "Error upgrading to version 17", e);
+ }
+ }
+
+ if (oldVersion < 18) {
+ try {
+ historyDao.executeRawNoArgs("CREATE TABLE `history` (`date` BIGINT , `id` INTEGER PRIMARY KEY AUTOINCREMENT , `loadable_id` INTEGER NOT NULL , `thumbnailUrl` VARCHAR );");
+ } catch (SQLException e) {
+ Logger.e(TAG, "Error upgrading to version 18", e);
+ }
+ }
}
public void reset() {
diff --git a/Clover/app/src/main/java/org/floens/chan/database/DatabaseManager.java b/Clover/app/src/main/java/org/floens/chan/core/database/DatabaseManager.java
similarity index 66%
rename from Clover/app/src/main/java/org/floens/chan/database/DatabaseManager.java
rename to Clover/app/src/main/java/org/floens/chan/core/database/DatabaseManager.java
index f3ffc508..60149818 100644
--- a/Clover/app/src/main/java/org/floens/chan/database/DatabaseManager.java
+++ b/Clover/app/src/main/java/org/floens/chan/core/database/DatabaseManager.java
@@ -15,14 +15,18 @@
* You should have received a copy of the GNU General Public License
* along with this program. If not, see .
*/
-package org.floens.chan.database;
+package org.floens.chan.core.database;
import android.content.Context;
import com.j256.ormlite.dao.Dao;
+import com.j256.ormlite.misc.TransactionManager;
+import com.j256.ormlite.stmt.QueryBuilder;
import com.j256.ormlite.table.TableUtils;
import org.floens.chan.core.model.Board;
+import org.floens.chan.core.model.History;
+import org.floens.chan.core.model.Loadable;
import org.floens.chan.core.model.Pin;
import org.floens.chan.core.model.Post;
import org.floens.chan.core.model.SavedReply;
@@ -31,9 +35,12 @@ import org.floens.chan.utils.Logger;
import java.sql.SQLException;
import java.util.ArrayList;
+import java.util.HashMap;
import java.util.HashSet;
import java.util.List;
import java.util.concurrent.Callable;
+import java.util.concurrent.ExecutorService;
+import java.util.concurrent.Executors;
import static com.j256.ormlite.misc.TransactionManager.callInTransaction;
@@ -44,14 +51,22 @@ public class DatabaseManager {
private static final long SAVED_REPLY_TRIM_COUNT = 50;
private static final long THREAD_HIDE_TRIM_TRIGGER = 250;
private static final long THREAD_HIDE_TRIM_COUNT = 50;
+ private static final long HISTORY_TRIM_TRIGGER = 500;
+ private static final long HISTORY_TRIM_COUNT = 50;
+
+ private final ExecutorService backgroundExecutor = Executors.newSingleThreadExecutor();
private final DatabaseHelper helper;
- private List savedReplies = new ArrayList<>();
- private HashSet savedRepliesIds = new HashSet<>();
+ private final Object savedRepliesLock = new Object();
+ private final List savedReplies = new ArrayList<>();
+ private final HashSet savedRepliesIds = new HashSet<>();
+
+ private final List threadHides = new ArrayList<>();
+ private final HashSet threadHidesIds = new HashSet<>();
- private List threadHides = new ArrayList<>();
- private HashSet threadHidesIds = new HashSet<>();
+ private final Object historyLock = new Object();
+ private final HashMap historyByLoadable = new HashMap<>();
public DatabaseManager(Context context) {
helper = new DatabaseHelper(context);
@@ -60,6 +75,7 @@ public class DatabaseManager {
/**
* Save a reply to the savedreply table.
+ * Threadsafe.
*
* @param saved the {@link SavedReply} to save
*/
@@ -70,22 +86,27 @@ public class DatabaseManager {
Logger.e(TAG, "Error saving reply", e);
}
- savedReplies.add(saved);
- savedRepliesIds.add(saved.no);
+ synchronized (savedRepliesLock) {
+ savedReplies.add(saved);
+ savedRepliesIds.add(saved.no);
+ }
}
/**
* Searches a saved reply. This is done through caching members, no database lookups.
+ * Threadsafe.
*
* @param board board for the reply to search
* @param no no for the reply to search
* @return A {@link SavedReply} that matches {@code board} and {@code no}, or {@code null}
*/
public SavedReply getSavedReply(String board, int no) {
- if (savedRepliesIds.contains(no)) {
- for (SavedReply r : savedReplies) {
- if (r.no == no && r.board.equals(board)) {
- return r;
+ synchronized (savedRepliesLock) {
+ if (savedRepliesIds.contains(no)) {
+ for (SavedReply r : savedReplies) {
+ if (r.no == no && r.board.equals(board)) {
+ return r;
+ }
}
}
}
@@ -95,6 +116,7 @@ public class DatabaseManager {
/**
* Searches if a saved reply exists. This is done through caching members, no database lookups.
+ * Threadsafe.
*
* @param board board for the reply to search
* @param no no for the reply to search
@@ -191,6 +213,74 @@ public class DatabaseManager {
return list;
}
+ /**
+ * Adds or updates a {@link History} to the history table.
+ * Only updates the date if the history is already in the table.
+ *
+ * @param history History to save
+ */
+ public void addHistory(final History history) {
+ backgroundExecutor.submit(new Runnable() {
+ @Override
+ public void run() {
+ addHistoryInternal(history);
+ }
+ });
+ }
+
+ /**
+ * Deletes a {@link History} from the history table.
+ *
+ * @param history History to delete
+ */
+ public void removeHistory(History history) {
+ try {
+ helper.historyDao.delete(history);
+ helper.loadableDao.delete(history.loadable);
+ historyByLoadable.remove(history.loadable);
+ } catch (SQLException e) {
+ Logger.e(TAG, "Error removing history from db", e);
+ }
+ }
+
+ /**
+ * Clears all history and the referenced loadables from the database.
+ */
+ public void clearHistory() {
+ try {
+ TransactionManager.callInTransaction(helper.getConnectionSource(), new Callable() {
+ @Override
+ public Void call() throws Exception {
+ List historyList = getHistory();
+ for (History history : historyList) {
+ removeHistory(history);
+ }
+
+ return null;
+ }
+ });
+ } catch (SQLException e) {
+ Logger.e(TAG, "Error clearing history", e);
+ }
+ }
+
+ /**
+ * Get a list of {@link History} entries from the history table.
+ *
+ * @return List of History
+ */
+ public List getHistory() {
+ List list = null;
+ try {
+ QueryBuilder historyQuery = helper.historyDao.queryBuilder();
+ list = historyQuery.orderBy("date", false).query();
+ } catch (SQLException e) {
+ Logger.e(TAG, "Error getting history from db", e);
+ }
+
+ return list;
+ }
+
/**
* Create or updates these boards in the boards table.
*
@@ -322,17 +412,23 @@ public class DatabaseManager {
private void initialize() {
loadSavedReplies();
loadThreadHides();
+ loadHistory();
}
+ /**
+ * Threadsafe.
+ */
private void loadSavedReplies() {
try {
trimTable(helper.savedDao, "savedreply", SAVED_REPLY_TRIM_TRIGGER, SAVED_REPLY_TRIM_COUNT);
- savedReplies.clear();
- savedReplies.addAll(helper.savedDao.queryForAll());
- savedRepliesIds.clear();
- for (SavedReply reply : savedReplies) {
- savedRepliesIds.add(reply.no);
+ synchronized (savedRepliesLock) {
+ savedReplies.clear();
+ savedReplies.addAll(helper.savedDao.queryForAll());
+ savedRepliesIds.clear();
+ for (SavedReply reply : savedReplies) {
+ savedRepliesIds.add(reply.no);
+ }
}
} catch (SQLException e) {
Logger.e(TAG, "Error loading saved replies", e);
@@ -354,6 +450,48 @@ public class DatabaseManager {
}
}
+ private void loadHistory() {
+ synchronized (historyLock) {
+ try {
+ trimTable(helper.historyDao, "history", HISTORY_TRIM_TRIGGER, HISTORY_TRIM_COUNT);
+
+ historyByLoadable.clear();
+ List historyList = helper.historyDao.queryForAll();
+ for (History history : historyList) {
+ historyByLoadable.put(history.loadable, history);
+ }
+ } catch (SQLException e) {
+ e.printStackTrace();
+ }
+ }
+ }
+
+ private void addHistoryInternal(final History history) {
+ try {
+ TransactionManager.callInTransaction(helper.getConnectionSource(), new Callable() {
+ @Override
+ public Void call() throws Exception {
+ synchronized (historyLock) {
+ History existingHistory = historyByLoadable.get(history.loadable);
+ if (existingHistory != null) {
+ existingHistory.date = System.currentTimeMillis();
+ helper.historyDao.update(existingHistory);
+ } else {
+ history.date = System.currentTimeMillis();
+ helper.loadableDao.create(history.loadable);
+ helper.historyDao.create(history);
+ historyByLoadable.put(history.loadable, history);
+ }
+ }
+
+ return null;
+ }
+ });
+ } catch (SQLException e) {
+ Logger.e(TAG, "Error adding history", e);
+ }
+ }
+
/**
* Trim a table with the specified trigger and trim count.
*
diff --git a/Clover/app/src/main/java/org/floens/chan/core/http/ReplyManager.java b/Clover/app/src/main/java/org/floens/chan/core/http/ReplyManager.java
index 8d1a9e47..7ea1427f 100644
--- a/Clover/app/src/main/java/org/floens/chan/core/http/ReplyManager.java
+++ b/Clover/app/src/main/java/org/floens/chan/core/http/ReplyManager.java
@@ -18,7 +18,6 @@
package org.floens.chan.core.http;
import android.content.Context;
-import android.content.Intent;
import com.squareup.okhttp.OkHttpClient;
import com.squareup.okhttp.Request;
@@ -26,7 +25,6 @@ import com.squareup.okhttp.Request;
import org.floens.chan.Chan;
import org.floens.chan.core.model.Loadable;
import org.floens.chan.core.model.Reply;
-import org.floens.chan.ui.activity.ImagePickActivity;
import java.io.File;
import java.util.HashMap;
@@ -40,7 +38,6 @@ public class ReplyManager {
private static final int TIMEOUT = 30000;
private final Context context;
- private FileListener fileListener;
private OkHttpClient client;
private Map drafts = new HashMap<>();
@@ -77,51 +74,10 @@ public class ReplyManager {
drafts.put(loadable, reply);
}
- /**
- * Pick an file. Starts up the ImagePickActivity.
- *
- * @param listener FileListener to listen on.
- */
- public void pickFile(FileListener listener) {
- fileListener = listener;
-
- Intent intent = new Intent(context, ImagePickActivity.class);
- intent.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
- context.startActivity(intent);
- }
-
public File getPickFile() {
return new File(context.getCacheDir(), "picked_file");
}
- public void _onFilePickLoading() {
- if (fileListener != null) {
- fileListener.onFilePickLoading();
- }
- }
-
- public void _onFilePicked(String name, File file) {
- if (fileListener != null) {
- fileListener.onFilePicked(name, file);
- fileListener = null;
- }
- }
-
- public void _onFilePickError(boolean cancelled) {
- if (fileListener != null) {
- fileListener.onFilePickError(cancelled);
- fileListener = null;
- }
- }
-
- public interface FileListener {
- void onFilePickLoading();
-
- void onFilePicked(String name, File file);
-
- void onFilePickError(boolean cancelled);
- }
-
public void makeHttpCall(HttpCall httpCall, HttpCallback extends HttpCall> callback) {
httpCall.setCallback(callback);
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 4dde2ed7..3d581388 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
@@ -52,6 +52,7 @@ public class BoardManager {
loadFromServer();
}
+ // TODO: synchronize
public Board getBoardByValue(String value) {
return allBoardsByValue.get(value);
}
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 2f9baa10..0e3dd652 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
@@ -46,53 +46,85 @@ public class Board {
*/
@DatabaseField
public String value;
+
+ /**
+ * True if this board appears in the dropdown, false otherwise.
+ */
@DatabaseField
public boolean saved = false;
+
@DatabaseField
public int order;
+
@DatabaseField
public boolean workSafe = false;
+
@DatabaseField
public int perPage = -1;
+
@DatabaseField
public int pages = -1;
+
@DatabaseField
public int maxFileSize = -1;
+
@DatabaseField
public int maxWebmSize = -1;
+
@DatabaseField
public int maxCommentChars = -1;
+
@DatabaseField
public int bumpLimit = -1;
+
@DatabaseField
public int imageLimit = -1;
+
@DatabaseField
public int cooldownThreads = -1;
+
@DatabaseField
public int cooldownReplies = -1;
+
@DatabaseField
public int cooldownImages = -1;
+
@DatabaseField
public int cooldownRepliesIntra = -1;
+
@DatabaseField
public int cooldownImagesIntra = -1;
+
@DatabaseField
public boolean spoilers = false;
+
@DatabaseField
public int customSpoilers = -1;
+
@DatabaseField
public boolean userIds = false;
+
@DatabaseField
public boolean codeTags = false;
+
@DatabaseField
public boolean preuploadCaptcha = false;
+
@DatabaseField
public boolean countryFlags = false;
+
+ /**
+ * Not used anymore.
+ */
@DatabaseField
public boolean trollFlags = false;
+
@DatabaseField
public boolean mathTags = false;
+ @DatabaseField
+ public String description;
+
public boolean finish() {
if (key == null || value == null || perPage < 0 || pages < 0)
return false;
diff --git a/Clover/app/src/main/java/org/floens/chan/core/model/History.java b/Clover/app/src/main/java/org/floens/chan/core/model/History.java
new file mode 100644
index 00000000..43c14710
--- /dev/null
+++ b/Clover/app/src/main/java/org/floens/chan/core/model/History.java
@@ -0,0 +1,36 @@
+/*
+ * Clover - 4chan browser https://github.com/Floens/Clover/
+ * Copyright (C) 2014 Floens
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program. If not, see .
+ */
+package org.floens.chan.core.model;
+
+import com.j256.ormlite.field.DatabaseField;
+import com.j256.ormlite.table.DatabaseTable;
+
+@DatabaseTable
+public class History {
+ @DatabaseField(generatedId = true)
+ public int id;
+
+ @DatabaseField(canBeNull = false, foreign = true, foreignAutoRefresh = true)
+ public Loadable loadable;
+
+ @DatabaseField
+ public String thumbnailUrl;
+
+ @DatabaseField
+ public long date;
+}
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 6c2ac3e2..60331747 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
@@ -28,7 +28,7 @@ import com.j256.ormlite.table.DatabaseTable;
@DatabaseTable
public class Loadable {
@DatabaseField(generatedId = true)
- private int id;
+ public int id;
@DatabaseField
public int mode = Mode.INVALID;
@@ -93,7 +93,7 @@ public class Loadable {
Loadable other = (Loadable) object;
- return mode == other.mode && board.equals(other.board) && no == other.no;
+ return no == other.no && mode == other.mode && board.equals(other.board);
}
@Override
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 b6d59a8b..5f95b9e8 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
@@ -21,20 +21,25 @@ import android.text.SpannableString;
import android.text.TextUtils;
import org.floens.chan.Chan;
-import org.floens.chan.chan.ChanUrls;
import org.floens.chan.chan.ChanParser;
+import org.floens.chan.chan.ChanUrls;
import org.jsoup.parser.Parser;
import java.util.ArrayList;
+import java.util.Collections;
import java.util.List;
import java.util.Random;
+import java.util.concurrent.atomic.AtomicBoolean;
/**
- * Contains all data needed to represent a single post.
+ * Contains all data needed to represent a single post.
+ * Call {@link #finish()} to parse the comment etc. The post data is invalid if finish returns false.
+ * 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 int no = -1;
public int resto = -1;
@@ -46,42 +51,25 @@ public class Post {
public long tim = -1;
public String ext;
public String filename;
- public int replies = -1;
public int imageWidth;
public int imageHeight;
public boolean hasImage = false;
public String thumbnailUrl;
public String imageUrl;
- public boolean sticky = false;
- public boolean closed = false;
- public boolean archived = false;
public String tripcode = "";
public String id = "";
public String capcode = "";
public String country = "";
public String countryName = "";
public long time = -1;
- public boolean isSavedReply = false;
- public String title = "";
public int fileSize;
- public int images = -1;
public String rawComment;
public String countryUrl;
public boolean spoiler = false;
- public int uniqueIps = 1;
-
- public boolean deleted = false;
-
/**
- * This post replies to the these ids
+ * This post replies to the these ids. Is an unmodifiable list after finish().
*/
public List repliesTo = new ArrayList<>();
-
- /**
- * These ids replied to this post
- */
- public List repliesFrom = new ArrayList<>();
-
public final ArrayList linkables = new ArrayList<>();
public boolean parsedSpans = false;
public SpannableString subjectSpan;
@@ -91,11 +79,25 @@ public class Post {
public SpannableString capcodeSpan;
public CharSequence nameTripcodeIdCapcodeSpan;
- public Post() {
- }
+ // *** These next members may only change on the main thread after finish(). ***
+ public boolean sticky = false;
+ public boolean closed = false;
+ public boolean archived = false;
+ public int replies = -1;
+ public int images = -1;
+ public int uniqueIps = 1;
+ public String title = "";
+ /**
+ * These ids replied to this post. Only modify this on the main thread.
+ */
+ public List repliesFrom = new ArrayList<>();
+
+ // *** Threadsafe members, may be read and modified on any thread. ***
+ public AtomicBoolean deleted = new AtomicBoolean(false);
+ public AtomicBoolean isSavedReply = new AtomicBoolean(false);
/**
- * Finish up the data
+ * Finish up the data: parse the comment, check if the data is valid etc.
*
* @return false if this data is invalid
*/
@@ -129,12 +131,13 @@ public class Post {
}
if (!TextUtils.isEmpty(country)) {
- Board b = Chan.getBoardManager().getBoardByValue(board);
- countryUrl = b.trollFlags ? ChanUrls.getTrollCountryFlagUrl(country) : ChanUrls.getCountryFlagUrl(country);
+ countryUrl = ChanUrls.getCountryFlagUrl(country);
}
ChanParser.getInstance().parse(this);
+ repliesTo = Collections.unmodifiableList(repliesTo);
+
return true;
}
}
diff --git a/Clover/app/src/main/java/org/floens/chan/core/net/BitmapLruImageCache.java b/Clover/app/src/main/java/org/floens/chan/core/net/BitmapLruImageCache.java
index 5aa060c5..665c9850 100644
--- a/Clover/app/src/main/java/org/floens/chan/core/net/BitmapLruImageCache.java
+++ b/Clover/app/src/main/java/org/floens/chan/core/net/BitmapLruImageCache.java
@@ -29,7 +29,7 @@ public class BitmapLruImageCache extends LruCache implements Ima
@Override
protected int sizeOf(String key, Bitmap value) {
- return value.getRowBytes() * value.getHeight();
+ return value.getRowBytes() * value.getHeight() / 1024;
}
@Override
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/net/BoardsRequest.java
index 3da18890..b9c6891e 100644
--- a/Clover/app/src/main/java/org/floens/chan/core/net/BoardsRequest.java
+++ b/Clover/app/src/main/java/org/floens/chan/core/net/BoardsRequest.java
@@ -150,12 +150,12 @@ public class BoardsRequest extends JsonReaderRequest> {
case "country_flags":
board.countryFlags = reader.nextInt() == 1;
break;
- case "troll_flags":
- board.trollFlags = reader.nextInt() == 1;
- break;
case "math_tags":
board.mathTags = reader.nextInt() == 1;
break;
+ case "meta_description":
+ board.description = reader.nextString();
+ break;
default:
reader.skipValue();
break;
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 f634dac5..779e0657 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
@@ -26,22 +26,23 @@ import org.floens.chan.Chan;
import org.floens.chan.chan.ChanUrls;
import org.floens.chan.core.model.Loadable;
import org.floens.chan.core.model.Post;
+import org.floens.chan.utils.Logger;
-import java.io.IOException;
import java.util.ArrayList;
-import java.util.HashSet;
import java.util.List;
-import java.util.Set;
-public class ChanReaderRequest extends JsonReaderRequest> {
+public class ChanReaderRequest extends JsonReaderRequest {
+ private static final String TAG = "ChanReaderRequest";
+
private Loadable loadable;
private List cached;
+ private Post op;
- private ChanReaderRequest(String url, Listener> listener, ErrorListener errorListener) {
+ private ChanReaderRequest(String url, Listener listener, ErrorListener errorListener) {
super(url, listener, errorListener);
}
- public static ChanReaderRequest newInstance(Loadable loadable, List cached, Listener> listener, ErrorListener errorListener) {
+ public static ChanReaderRequest newInstance(Loadable loadable, List cached, Listener listener, ErrorListener errorListener) {
String url;
if (loadable.isThreadMode()) {
@@ -67,7 +68,7 @@ public class ChanReaderRequest extends JsonReaderRequest> {
}
@Override
- public List readJson(JsonReader reader) throws Exception {
+ public ChanReaderResponse readJson(JsonReader reader) throws Exception {
List list;
if (loadable.isThreadMode()) {
@@ -81,11 +82,14 @@ public class ChanReaderRequest extends JsonReaderRequest> {
return processPosts(list);
}
- private List processPosts(List serverList) throws Exception {
- List totalList = new ArrayList<>(serverList.size());
+ private ChanReaderResponse processPosts(List serverList) throws Exception {
+ ChanReaderResponse response = new ChanReaderResponse();
+ response.posts = new ArrayList<>(serverList.size());
+ response.op = op;
if (cached.size() > 0) {
- totalList.addAll(cached);
+ // Add all posts that were parsed before
+ response.posts.addAll(cached);
// If there's a cached post but it's not in the list received from the server, mark it as deleted
if (loadable.isThreadMode()) {
@@ -99,7 +103,7 @@ public class ChanReaderRequest extends JsonReaderRequest> {
}
}
- cache.deleted = !serverHas;
+ cache.deleted.set(!serverHas);
}
}
@@ -115,68 +119,19 @@ public class ChanReaderRequest extends JsonReaderRequest> {
}
}
- // serverPost is not in finalList
if (!known) {
- totalList.add(post);
+ response.posts.add(post);
}
}
-
- // Replace OPs
- if (totalList.get(0).isOP && serverList.size() > 0 && serverList.get(0).isOP) {
- totalList.set(0, serverList.get(0));
- }
-
- // Sort if it got out of order due to posts disappearing/reappearing
- /*if (loadable.isThreadMode()) {
- Collections.sort(totalList, new Comparator() {
- @Override
- public int compare(Post lhs, Post rhs) {
- return lhs.time == rhs.time ? 0 : (lhs.time < rhs.time ? -1 : 1);
- }
- });
- }*/
-
} else {
- totalList.addAll(serverList);
- }
-
- Set postsReplyingToDeleted = new HashSet<>();
- for (Post post : totalList) {
- if (!post.deleted) {
- post.repliesFrom.clear();
-
- for (Post other : totalList) {
- if (other.repliesTo.contains(post.no) && !other.deleted) {
- post.repliesFrom.add(other.no);
- }
- }
- } else {
- post.repliesTo.clear();
-
- for (int no : post.repliesFrom) {
- postsReplyingToDeleted.add(no);
- }
-
- post.repliesFrom.clear();
- }
+ response.posts.addAll(serverList);
}
- for (int no : postsReplyingToDeleted) {
- for (Post post : totalList) {
- if (post.no == no) {
- if (!post.finish()) {
- throw new IOException("Incorrect data about post received.");
- }
- break;
- }
- }
+ for (Post post : response.posts) {
+ post.isSavedReply.set(Chan.getDatabaseManager().isSavedReply(post.board, post.no));
}
- for (Post post : totalList) {
- post.isSavedReply = Chan.getDatabaseManager().isSavedReply(post.board, post.no);
- }
-
- return totalList;
+ return response;
}
private List loadThread(JsonReader reader) throws Exception {
@@ -191,7 +146,10 @@ public class ChanReaderRequest extends JsonReaderRequest> {
// Thread array
while (reader.hasNext()) {
// Thread object
- list.add(readPostObject(reader));
+ Post post = readPostObject(reader);
+ if (post != null) {
+ list.add(post);
+ }
}
reader.endArray();
} else {
@@ -216,7 +174,10 @@ public class ChanReaderRequest extends JsonReaderRequest> {
reader.beginArray(); // Threads array
while (reader.hasNext()) {
- list.add(readPostObject(reader));
+ Post post = readPostObject(reader);
+ if (post != null) {
+ list.add(post);
+ }
}
reader.endArray();
@@ -324,28 +285,28 @@ public class ChanReaderRequest extends JsonReaderRequest> {
break;
default:
// Unknown/ignored key
- // log("Unknown/ignored key: " + key + ".");
reader.skipValue();
break;
}
}
reader.endObject();
+ if (post.resto == 0) {
+ // Update OP fields later on the main thread
+ op = new Post();
+ op.closed = post.closed;
+ op.archived = post.archived;
+ op.sticky = post.sticky;
+ op.replies = post.replies;
+ op.images = post.images;
+ op.uniqueIps = post.uniqueIps;
+ }
+
Post cached = null;
for (Post item : this.cached) {
if (item.no == post.no) {
cached = item;
- if (post.resto == 0) {
- // Update OP fields
- cached.sticky = post.sticky;
- cached.closed = post.closed;
- cached.archived = post.archived;
- cached.replies = post.replies;
- cached.images = post.images;
- cached.uniqueIps = post.uniqueIps;
- }
-
break;
}
}
@@ -354,10 +315,18 @@ public class ChanReaderRequest extends JsonReaderRequest> {
return cached;
} else {
if (!post.finish()) {
- throw new IOException("Incorrect data about post received.");
+ Logger.e(TAG, "Incorrect data about post received for post " + post.no);
+ return null;
+ } else {
+ return post;
}
-
- return post;
}
}
+
+ public static class ChanReaderResponse {
+ // Op Post that is created new each time.
+ // Used to later copy members like image count to the real op on the main thread.
+ public Post op;
+ public List posts;
+ }
}
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 6e0005a3..8b564a4f 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
@@ -32,7 +32,8 @@ import org.floens.chan.core.model.Post;
import org.floens.chan.core.model.Reply;
import org.floens.chan.core.model.SavedReply;
import org.floens.chan.core.settings.ChanSettings;
-import org.floens.chan.database.DatabaseManager;
+import org.floens.chan.core.database.DatabaseManager;
+import org.floens.chan.ui.helper.ImagePickDelegate;
import org.floens.chan.ui.layout.CaptchaLayout;
import java.io.File;
@@ -43,7 +44,7 @@ import static org.floens.chan.utils.AndroidUtils.getReadableFileSize;
import static org.floens.chan.utils.AndroidUtils.getRes;
import static org.floens.chan.utils.AndroidUtils.getString;
-public class ReplyPresenter implements ReplyManager.FileListener, ReplyManager.HttpCallback, CaptchaLayout.CaptchaCallback {
+public class ReplyPresenter implements ReplyManager.HttpCallback, CaptchaLayout.CaptchaCallback, ImagePickDelegate.ImagePickCallback {
public enum Page {
INPUT,
CAPTCHA,
@@ -160,7 +161,7 @@ public class ReplyPresenter implements ReplyManager.FileListener, ReplyManager.H
}
previewOpen = false;
} else {
- Chan.getReplyManager().pickFile(this);
+ callback.getImagePickDelegate().pick(this);
pickingFile = true;
}
}
@@ -424,5 +425,7 @@ public class ReplyPresenter implements ReplyManager.FileListener, ReplyManager.H
void highlightPostNo(int no);
void showThread(Loadable loadable);
+
+ ImagePickDelegate getImagePickDelegate();
}
}
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 dc07e68e..ed0e7bd5 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
@@ -25,10 +25,12 @@ import org.floens.chan.Chan;
import org.floens.chan.R;
import org.floens.chan.chan.ChanLoader;
import org.floens.chan.chan.ChanUrls;
+import org.floens.chan.core.database.DatabaseManager;
import org.floens.chan.core.http.DeleteHttpCall;
import org.floens.chan.core.http.ReplyManager;
import org.floens.chan.core.manager.WatchManager;
import org.floens.chan.core.model.ChanThread;
+import org.floens.chan.core.model.History;
import org.floens.chan.core.model.Loadable;
import org.floens.chan.core.model.Pin;
import org.floens.chan.core.model.Post;
@@ -37,7 +39,6 @@ import org.floens.chan.core.model.PostLinkable;
import org.floens.chan.core.model.SavedReply;
import org.floens.chan.core.net.LoaderPool;
import org.floens.chan.core.settings.ChanSettings;
-import org.floens.chan.database.DatabaseManager;
import org.floens.chan.ui.adapter.PostAdapter;
import org.floens.chan.ui.adapter.PostFilter;
import org.floens.chan.ui.cell.PostCellInterface;
@@ -79,6 +80,8 @@ public class ThreadPresenter implements ChanLoader.ChanLoaderCallback, PostAdapt
private boolean searchOpen = false;
private String searchQuery;
private PostFilter.Order order = PostFilter.Order.BUMP;
+ private boolean historyAdded = false;
+ private int notificationPostCount = -1;
public ThreadPresenter(ThreadPresenterCallback threadPresenterCallback) {
this.threadPresenterCallback = threadPresenterCallback;
@@ -112,7 +115,8 @@ public class ThreadPresenter implements ChanLoader.ChanLoaderCallback, PostAdapt
LoaderPool.getInstance().release(chanLoader, this);
chanLoader = null;
loadable = null;
- order = PostFilter.Order.BUMP;
+ historyAdded = false;
+ notificationPostCount = -1;
threadPresenterCallback.showLoading();
}
@@ -214,6 +218,20 @@ public class ThreadPresenter implements ChanLoader.ChanLoaderCallback, PostAdapt
}
}
+ if (loadable.isThreadMode()) {
+ int postsSize = result.posts.size();
+ if (notificationPostCount < 0) {
+ notificationPostCount = postsSize;
+ } else {
+ if (postsSize > notificationPostCount) {
+ int more = postsSize - notificationPostCount;
+ notificationPostCount = postsSize;
+
+ threadPresenterCallback.showNewPostsNotification(true, more);
+ }
+ }
+ }
+
chanLoader.setAutoLoadMore(isWatching());
showPosts();
@@ -225,6 +243,8 @@ public class ThreadPresenter implements ChanLoader.ChanLoaderCallback, PostAdapt
}
loadable.markedNo = -1;
}
+
+ addHistory();
}
@Override
@@ -247,6 +267,8 @@ public class ThreadPresenter implements ChanLoader.ChanLoaderCallback, PostAdapt
pin.onBottomPostViewed();
watchManager.updatePin(pin);
}
+
+ threadPresenterCallback.showNewPostsNotification(false, -1);
}
public void scrollTo(int position, boolean smooth) {
@@ -563,6 +585,19 @@ public class ThreadPresenter implements ChanLoader.ChanLoaderCallback, PostAdapt
threadPresenterCallback.showPosts(chanLoader.getThread(), new PostFilter(order, searchQuery));
}
+ private void addHistory() {
+ if (!historyAdded && ChanSettings.historyEnabled.get() && loadable.isThreadMode()) {
+ historyAdded = true;
+ History history = new History();
+ // Copy the loadable when adding to history
+ // Otherwise the database will possible use the loadable from a pin, and when clearing the history also deleting the loadable from the pin.
+ history.loadable = loadable.copy();
+ history.loadable.id = 0;
+ history.thumbnailUrl = chanLoader.getThread().op.thumbnailUrl;
+ databaseManager.addHistory(history);
+ }
+ }
+
public interface ThreadPresenterCallback {
void showPosts(ChanThread thread, PostFilter filter);
@@ -609,5 +644,7 @@ public class ThreadPresenter implements ChanLoader.ChanLoaderCallback, PostAdapt
void hideDeleting(String message);
void hideThread(Post post);
+
+ void showNewPostsNotification(boolean show, int more);
}
}
diff --git a/Clover/app/src/main/java/org/floens/chan/core/settings/ChanSettings.java b/Clover/app/src/main/java/org/floens/chan/core/settings/ChanSettings.java
index 0fe4b500..affd4b91 100644
--- a/Clover/app/src/main/java/org/floens/chan/core/settings/ChanSettings.java
+++ b/Clover/app/src/main/java/org/floens/chan/core/settings/ChanSettings.java
@@ -94,6 +94,8 @@ public class ChanSettings {
public static final StringSetting passPin;
public static final StringSetting passId;
+ public static final BooleanSetting historyEnabled;
+
public static final BooleanSetting proxyEnabled;
public static final StringSetting proxyAddress;
public static final IntegerSetting proxyPort;
@@ -159,6 +161,8 @@ public class ChanSettings {
passPin = new StringSetting(p, "preference_pass_pin", "");
passId = new StringSetting(p, "preference_pass_id", "");
+ historyEnabled = new BooleanSetting(p, "preference_history_enabled", true);
+
proxyEnabled = new BooleanSetting(p, "preference_proxy_enabled", false);
proxyAddress = new StringSetting(p, "preference_proxy_address", "");
proxyPort = new IntegerSetting(p, "preference_proxy_port", 80);
diff --git a/Clover/app/src/main/java/org/floens/chan/core/watch/PinWatcher.java b/Clover/app/src/main/java/org/floens/chan/core/watch/PinWatcher.java
index 6df7e77c..ae354aa2 100644
--- a/Clover/app/src/main/java/org/floens/chan/core/watch/PinWatcher.java
+++ b/Clover/app/src/main/java/org/floens/chan/core/watch/PinWatcher.java
@@ -147,7 +147,7 @@ public class PinWatcher implements ChanLoader.ChanLoaderCallback {
for (Post item : thread.posts) {
// saved.title = pin.loadable.title;
- if (item.isSavedReply) {
+ if (item.isSavedReply.get()) {
savedReplies.add(item);
}
}
diff --git a/Clover/app/src/main/java/org/floens/chan/ui/BadgeDrawable.java b/Clover/app/src/main/java/org/floens/chan/ui/BadgeDrawable.java
deleted file mode 100644
index 3cf8dfc7..00000000
--- a/Clover/app/src/main/java/org/floens/chan/ui/BadgeDrawable.java
+++ /dev/null
@@ -1,82 +0,0 @@
-/*
- * Clover - 4chan browser https://github.com/Floens/Clover/
- * Copyright (C) 2014 Floens
- *
- * This program is free software: you can redistribute it and/or modify
- * it under the terms of the GNU General Public License as published by
- * the Free Software Foundation, either version 3 of the License, or
- * (at your option) any later version.
- *
- * This program is distributed in the hope that it will be useful,
- * but WITHOUT ANY WARRANTY; without even the implied warranty of
- * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
- * GNU General Public License for more details.
- *
- * You should have received a copy of the GNU General Public License
- * along with this program. If not, see .
- */
-package org.floens.chan.ui;
-
-import android.content.res.Resources;
-import android.graphics.Bitmap;
-import android.graphics.BitmapFactory;
-import android.graphics.Canvas;
-import android.graphics.Paint;
-import android.graphics.Rect;
-import android.graphics.RectF;
-import android.graphics.drawable.BitmapDrawable;
-import android.graphics.drawable.Drawable;
-
-public class BadgeDrawable {
- public static Drawable get(Resources resources, int id, int count, boolean red) {
- BitmapFactory.Options opt = new BitmapFactory.Options();
- opt.inMutable = true;
- Bitmap bitmap = BitmapFactory.decodeResource(resources, id, opt);
- int w = bitmap.getWidth();
- int h = bitmap.getHeight();
-
- Paint paint = new Paint();
- paint.setAntiAlias(true);
-
- Canvas canvas = new Canvas(bitmap);
-
- float badgeX = w * 0.3f;
- float badgeY = h * 0.3f;
- float badgeW = w * 0.6f;
- float badgeH = h * 0.6f;
-
- RectF rect = new RectF(badgeX, badgeY, badgeX + badgeW, badgeY + badgeH);
- if (red) {
- paint.setColor(0xddff4444);
- } else {
- paint.setColor(0xaa000000);
- }
- canvas.drawRoundRect(rect, w * 0.1f, h * 0.1f, paint);
-
- String text = Integer.toString(count);
- if (count > 999) {
- text = "1k+";
- }
-
- paint.setColor(0xffffffff);
-
- float textHeight;
- float bottomOffset;
- if (text.length() <= 2) {
- textHeight = badgeH * 0.8f;
- bottomOffset = badgeH * 0.2f;
- } else {
- textHeight = badgeH * 0.5f;
- bottomOffset = badgeH * 0.3f;
- }
-
- paint.setTextSize(textHeight);
-
- Rect bounds = new Rect();
- paint.getTextBounds(text, 0, text.length(), bounds);
-
- canvas.drawText(text, badgeX + badgeW / 2f - bounds.right / 2f, badgeY + badgeH - bottomOffset, paint);
-
- return new BitmapDrawable(resources, bitmap);
- }
-}
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 592af0ce..e8834129 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
@@ -37,6 +37,7 @@ import org.floens.chan.core.settings.ChanSettings;
import org.floens.chan.ui.controller.BrowseController;
import org.floens.chan.ui.controller.RootNavigationController;
import org.floens.chan.ui.controller.ViewThreadController;
+import org.floens.chan.ui.helper.ImagePickDelegate;
import org.floens.chan.ui.state.ChanState;
import org.floens.chan.ui.theme.ThemeHelper;
import org.floens.chan.utils.Logger;
@@ -56,6 +57,8 @@ public class StartActivity extends AppCompatActivity {
private RootNavigationController rootNavigationController;
private BrowseController browseController;
+ private ImagePickDelegate imagePickDelegate;
+
public StartActivity() {
boardManager = Chan.getBoardManager();
}
@@ -66,6 +69,8 @@ public class StartActivity extends AppCompatActivity {
ThemeHelper.getInstance().setupContext(this);
+ imagePickDelegate = new ImagePickDelegate(this);
+
contentView = (ViewGroup) findViewById(android.R.id.content);
rootNavigationController = new RootNavigationController(this);
@@ -180,6 +185,10 @@ public class StartActivity extends AppCompatActivity {
return contentView;
}
+ public ImagePickDelegate getImagePickDelegate() {
+ return imagePickDelegate;
+ }
+
@Override
public void onConfigurationChanged(Configuration newConfig) {
super.onConfigurationChanged(newConfig);
@@ -233,6 +242,13 @@ public class StartActivity extends AppCompatActivity {
Chan.getInstance().activityEnteredBackground();
}
+ @Override
+ protected void onActivityResult(int requestCode, int resultCode, Intent data) {
+ super.onActivityResult(requestCode, resultCode, data);
+
+ imagePickDelegate.onActivityResult(requestCode, resultCode, data);
+ }
+
private Controller stackTop() {
return stack.get(stack.size() - 1);
}
diff --git a/Clover/app/src/main/java/org/floens/chan/ui/adapter/PostFilter.java b/Clover/app/src/main/java/org/floens/chan/ui/adapter/PostFilter.java
index 9ebc3b17..19de204f 100644
--- a/Clover/app/src/main/java/org/floens/chan/ui/adapter/PostFilter.java
+++ b/Clover/app/src/main/java/org/floens/chan/ui/adapter/PostFilter.java
@@ -21,7 +21,7 @@ import android.text.TextUtils;
import org.floens.chan.Chan;
import org.floens.chan.core.model.Post;
-import org.floens.chan.database.DatabaseManager;
+import org.floens.chan.core.database.DatabaseManager;
import java.util.ArrayList;
import java.util.Collections;
diff --git a/Clover/app/src/main/java/org/floens/chan/ui/cell/PostCell.java b/Clover/app/src/main/java/org/floens/chan/ui/cell/PostCell.java
index e8c9b1d3..18c48f57 100644
--- a/Clover/app/src/main/java/org/floens/chan/ui/cell/PostCell.java
+++ b/Clover/app/src/main/java/org/floens/chan/ui/cell/PostCell.java
@@ -273,7 +273,7 @@ public class PostCell extends RelativeLayout implements PostCellInterface, PostL
if (highlighted) {
setBackgroundColor(theme.highlightedColor);
- } else if (post.isSavedReply) {
+ } else if (post.isSavedReply.get()) {
setBackgroundColor(theme.savedReplyColor);
} else if (threadMode) {
setBackgroundResource(0);
@@ -320,7 +320,7 @@ public class PostCell extends RelativeLayout implements PostCellInterface, PostL
iconsSpannable = PostHelper.addIcon(iconsSpannable, PostHelper.closedIcon, iconsTextSize);
}
- if (post.deleted) {
+ if (post.deleted.get()) {
iconsSpannable = PostHelper.addIcon(iconsSpannable, PostHelper.trashIcon, iconsTextSize);
}
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 14f03cfc..84ccbca4 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
@@ -18,14 +18,20 @@
package org.floens.chan.ui.cell;
import android.content.Context;
+import android.graphics.Typeface;
import android.os.Handler;
import android.os.Message;
+import android.text.SpannableString;
+import android.text.TextUtils;
+import android.text.style.StyleSpan;
import android.util.AttributeSet;
import android.view.View;
import android.widget.LinearLayout;
import android.widget.TextView;
+import org.floens.chan.Chan;
import org.floens.chan.R;
+import org.floens.chan.core.model.Board;
import org.floens.chan.core.model.ChanThread;
import org.floens.chan.core.model.Post;
@@ -108,9 +114,20 @@ public class ThreadStatusCell extends LinearLayout implements View.OnClickListen
}
Post op = chanThread.op;
- statusText += getContext().getString(R.string.thread_stats, op.replies, op.images, op.uniqueIps);
- text.setText(statusText);
+ Board board = Chan.getBoardManager().getBoardByValue(chanThread.loadable.board);
+ if (board != null) {
+ SpannableString replies = new SpannableString(op.replies + "R");
+ if (op.replies >= board.bumpLimit) {
+ replies.setSpan(new StyleSpan(Typeface.ITALIC), 0, replies.length(), 0);
+ }
+ SpannableString images = new SpannableString(op.images + "I");
+ if (op.images >= board.imageLimit) {
+ images.setSpan(new StyleSpan(Typeface.ITALIC), 0, images.length(), 0);
+ }
+
+ text.setText(TextUtils.concat(statusText, replies, " / ", images, " / ", String.valueOf(op.uniqueIps)));
+ }
}
}
diff --git a/Clover/app/src/main/java/org/floens/chan/ui/controller/BoardEditController.java b/Clover/app/src/main/java/org/floens/chan/ui/controller/BoardEditController.java
index ed256894..f814ab44 100644
--- a/Clover/app/src/main/java/org/floens/chan/ui/controller/BoardEditController.java
+++ b/Clover/app/src/main/java/org/floens/chan/ui/controller/BoardEditController.java
@@ -17,6 +17,7 @@
*/
package org.floens.chan.ui.controller;
+import android.annotation.SuppressLint;
import android.app.AlertDialog;
import android.content.Context;
import android.content.DialogInterface;
@@ -49,6 +50,7 @@ import org.floens.chan.ui.toolbar.ToolbarMenu;
import org.floens.chan.ui.toolbar.ToolbarMenuItem;
import org.floens.chan.ui.view.FloatingMenuItem;
import org.floens.chan.utils.AndroidUtils;
+import org.jsoup.parser.Parser;
import java.util.ArrayList;
import java.util.Collections;
@@ -109,7 +111,7 @@ public class BoardEditController extends Controller implements SwipeListener.Cal
Collections.sort(boards, new Comparator() {
@Override
public int compare(Board lhs, Board rhs) {
- return lhs.key.compareTo(rhs.key);
+ return lhs.value.compareTo(rhs.value);
}
});
adapter.notifyDataSetChanged();
@@ -303,9 +305,10 @@ public class BoardEditController extends Controller implements SwipeListener.Cal
public View getView(int position, View convertView, ViewGroup parent) {
LayoutInflater inflater = (LayoutInflater) getContext().getSystemService(Context.LAYOUT_INFLATER_SERVICE);
+ @SuppressLint("ViewHolder")
TextView view = (TextView) inflater.inflate(android.R.layout.simple_list_item_1, parent, false);
Board b = filtered.get(position);
- view.setText("/" + b.value + "/ " + b.key);
+ view.setText("/" + b.value + "/ - " + b.key);
view.setOnTouchListener(new View.OnTouchListener() {
@Override
@@ -394,6 +397,8 @@ public class BoardEditController extends Controller implements SwipeListener.Cal
BoardEditItem item = (BoardEditItem) holder;
Board board = boards.get(position - 1);
item.text.setText("/" + board.value + "/ " + board.key);
+
+ item.description.setText(board.description == null ? null : Parser.unescapeEntities(board.description, false));
}
}
@@ -420,11 +425,13 @@ public class BoardEditController extends Controller implements SwipeListener.Cal
private class BoardEditItem extends RecyclerView.ViewHolder {
private ImageView image;
private TextView text;
+ private TextView description;
public BoardEditItem(View itemView) {
super(itemView);
image = (ImageView) itemView.findViewById(R.id.thumb);
text = (TextView) itemView.findViewById(R.id.text);
+ description = (TextView) itemView.findViewById(R.id.description);
image.setImageDrawable(new ThumbDrawable());
}
}
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
new file mode 100644
index 00000000..030b9565
--- /dev/null
+++ b/Clover/app/src/main/java/org/floens/chan/ui/controller/HistoryController.java
@@ -0,0 +1,244 @@
+/*
+ * Clover - 4chan browser https://github.com/Floens/Clover/
+ * Copyright (C) 2014 Floens
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program. If not, see .
+ */
+package org.floens.chan.ui.controller;
+
+import android.app.AlertDialog;
+import android.content.Context;
+import android.content.DialogInterface;
+import android.support.v7.widget.LinearLayoutManager;
+import android.support.v7.widget.RecyclerView;
+import android.support.v7.widget.SwitchCompat;
+import android.text.TextUtils;
+import android.view.LayoutInflater;
+import android.view.View;
+import android.view.ViewGroup;
+import android.widget.CompoundButton;
+import android.widget.ImageView;
+import android.widget.TextView;
+
+import org.floens.chan.Chan;
+import org.floens.chan.R;
+import org.floens.chan.controller.Controller;
+import org.floens.chan.core.database.DatabaseManager;
+import org.floens.chan.core.model.Board;
+import org.floens.chan.core.model.History;
+import org.floens.chan.core.settings.ChanSettings;
+import org.floens.chan.ui.toolbar.ToolbarMenu;
+import org.floens.chan.ui.toolbar.ToolbarMenuItem;
+import org.floens.chan.ui.view.FloatingMenuItem;
+import org.floens.chan.ui.view.ThumbnailView;
+
+import java.util.ArrayList;
+import java.util.List;
+import java.util.Locale;
+
+import static org.floens.chan.ui.theme.ThemeHelper.theme;
+import static org.floens.chan.utils.AndroidUtils.dp;
+
+public class HistoryController extends Controller implements CompoundButton.OnCheckedChangeListener, ToolbarMenuItem.ToolbarMenuItemCallback, RootNavigationController.ToolbarSearchCallback {
+ private static final int SEARCH_ID = 1;
+ private static final int CLEAR_ID = 101;
+
+ private DatabaseManager databaseManager;
+ private RecyclerView recyclerView;
+ private HistoryAdapter adapter;
+
+ public HistoryController(Context context) {
+ super(context);
+ }
+
+ @Override
+ public void onCreate() {
+ super.onCreate();
+
+ databaseManager = Chan.getDatabaseManager();
+
+ navigationItem.title = string(R.string.history_screen);
+ List items = new ArrayList<>();
+ items.add(new FloatingMenuItem(CLEAR_ID, R.string.history_clear));
+ navigationItem.menu = new ToolbarMenu(context);
+ navigationItem.menu.addItem(new ToolbarMenuItem(context, this, SEARCH_ID, R.drawable.ic_search_white_24dp));
+ navigationItem.createOverflow(context, this, items);
+
+ view = inflateRes(R.layout.controller_history);
+
+ SwitchCompat globalSwitch = new SwitchCompat(context);
+ globalSwitch.setChecked(ChanSettings.historyEnabled.get());
+ globalSwitch.setOnCheckedChangeListener(this);
+ navigationItem.rightView = globalSwitch;
+
+ recyclerView = (RecyclerView) view.findViewById(R.id.recycler_view);
+ recyclerView.setHasFixedSize(true);
+ recyclerView.setLayoutManager(new LinearLayoutManager(context));
+
+ adapter = new HistoryAdapter();
+ recyclerView.setAdapter(adapter);
+ adapter.load();
+ }
+
+ @Override
+ public void onMenuItemClicked(ToolbarMenuItem item) {
+ if ((Integer) item.getId() == SEARCH_ID) {
+ navigationController.showSearch();
+ }
+ }
+
+ @Override
+ public void onSubMenuItemClicked(ToolbarMenuItem parent, FloatingMenuItem item) {
+ if ((Integer) item.getId() == CLEAR_ID) {
+ new AlertDialog.Builder(context)
+ .setTitle(R.string.history_clear_confirm)
+ .setNegativeButton(R.string.cancel, null)
+ .setPositiveButton(R.string.history_clear_confirm_button, new DialogInterface.OnClickListener() {
+ @Override
+ public void onClick(DialogInterface dialog, int which) {
+ databaseManager.clearHistory();
+ adapter.load();
+ }
+ })
+ .show();
+ }
+ }
+
+ @Override
+ public void onCheckedChanged(CompoundButton buttonView, boolean isChecked) {
+ ChanSettings.historyEnabled.set(isChecked);
+ }
+
+ private void openThread(History history) {
+ ViewThreadController viewThreadController = new ViewThreadController(context);
+ viewThreadController.setLoadable(history.loadable);
+ navigationController.pushController(viewThreadController);
+ }
+
+ private void deleteHistory(History history) {
+ databaseManager.removeHistory(history);
+ adapter.load();
+ }
+
+ @Override
+ public void onSearchVisibilityChanged(boolean visible) {
+ if (!visible) {
+ adapter.search(null);
+ }
+ }
+
+ @Override
+ public void onSearchEntered(String entered) {
+ adapter.search(entered);
+ }
+
+ private class HistoryAdapter extends RecyclerView.Adapter {
+ private List sourceList = new ArrayList<>();
+ private List displayList = new ArrayList<>();
+ private String searchQuery;
+
+ public HistoryAdapter() {
+ setHasStableIds(true);
+ }
+
+ @Override
+ public HistoryCell onCreateViewHolder(ViewGroup parent, int viewType) {
+ return new HistoryCell(LayoutInflater.from(parent.getContext()).inflate(R.layout.cell_history, parent, false));
+ }
+
+ @Override
+ public void onBindViewHolder(HistoryCell holder, int position) {
+ History history = displayList.get(position);
+ holder.thumbnail.setUrl(history.thumbnailUrl, dp(48), dp(48));
+ holder.text.setText(history.loadable.title);
+ Board board = Chan.getBoardManager().getBoardByValue(history.loadable.board);
+ holder.subtext.setText(board == null ? null : ("/" + board.value + "/ - " + board.key));
+ }
+
+ @Override
+ public int getItemCount() {
+ return displayList.size();
+ }
+
+ @Override
+ public long getItemId(int position) {
+ return displayList.get(position).id;
+ }
+
+ public void search(String query) {
+ this.searchQuery = query;
+ filter();
+ }
+
+ private void load() {
+ sourceList.clear();
+ sourceList.addAll(databaseManager.getHistory());
+
+ filter();
+ }
+
+ private void filter() {
+ displayList.clear();
+ if (!TextUtils.isEmpty(searchQuery)) {
+ String query = searchQuery.toLowerCase(Locale.ENGLISH);
+ for (History history : sourceList) {
+ if (history.loadable.title.toLowerCase(Locale.ENGLISH).contains(query)) {
+ displayList.add(history);
+ }
+ }
+ } else {
+ displayList.addAll(sourceList);
+ }
+
+ notifyDataSetChanged();
+ }
+ }
+
+ private class HistoryCell extends RecyclerView.ViewHolder implements View.OnClickListener {
+ private ThumbnailView thumbnail;
+ private TextView text;
+ private TextView subtext;
+ private ImageView delete;
+
+ public HistoryCell(View itemView) {
+ super(itemView);
+
+ thumbnail = (ThumbnailView) itemView.findViewById(R.id.thumbnail);
+ thumbnail.setCircular(true);
+ text = (TextView) itemView.findViewById(R.id.text);
+ subtext = (TextView) itemView.findViewById(R.id.subtext);
+ delete = (ImageView) itemView.findViewById(R.id.delete);
+
+ theme().clearDrawable.apply(delete);
+
+ delete.setOnClickListener(this);
+
+ itemView.setOnClickListener(this);
+ }
+
+ @Override
+ public void onClick(View v) {
+ int position = getAdapterPosition();
+ if (position >= 0 && position < adapter.getItemCount()) {
+ History history = adapter.displayList.get(position);
+ if (v == itemView) {
+ openThread(history);
+ } else if (v == delete) {
+ deleteHistory(history);
+ }
+ }
+
+ }
+ }
+}
diff --git a/Clover/app/src/main/java/org/floens/chan/ui/controller/RootNavigationController.java b/Clover/app/src/main/java/org/floens/chan/ui/controller/RootNavigationController.java
index d5dbea5e..8256fa14 100644
--- a/Clover/app/src/main/java/org/floens/chan/ui/controller/RootNavigationController.java
+++ b/Clover/app/src/main/java/org/floens/chan/ui/controller/RootNavigationController.java
@@ -178,8 +178,8 @@ public class RootNavigationController extends NavigationController implements Pi
@Override
public void onPinClicked(Pin pin) {
Controller top = getTop();
- if (top instanceof DrawerCallbacks) {
- ((DrawerCallbacks) top).onPinClicked(pin);
+ if (top instanceof DrawerCallback) {
+ ((DrawerCallback) top).onPinClicked(pin);
drawerLayout.closeDrawer(Gravity.LEFT);
pinAdapter.updateHighlighted(recyclerView);
}
@@ -187,8 +187,8 @@ public class RootNavigationController extends NavigationController implements Pi
public boolean isHighlighted(Pin pin) {
Controller top = getTop();
- if (top instanceof DrawerCallbacks) {
- return ((DrawerCallbacks) top).isPinCurrent(pin);
+ if (top instanceof DrawerCallback) {
+ return ((DrawerCallback) top).isPinCurrent(pin);
}
return false;
}
@@ -256,6 +256,7 @@ public class RootNavigationController extends NavigationController implements Pi
@Override
public void openHistory() {
+ pushController(new HistoryController(context));
}
public void onEvent(WatchManager.PinAddedMessage message) {
@@ -313,24 +314,26 @@ public class RootNavigationController extends NavigationController implements Pi
@Override
public void onSearchVisibilityChanged(boolean visible) {
Controller top = getTop();
- if (top instanceof DrawerCallbacks) {
- ((DrawerCallbacks) top).onSearchVisibilityChanged(visible);
+ if (top instanceof ToolbarSearchCallback) {
+ ((ToolbarSearchCallback) top).onSearchVisibilityChanged(visible);
}
}
@Override
public void onSearchEntered(String entered) {
Controller top = getTop();
- if (top instanceof DrawerCallbacks) {
- ((DrawerCallbacks) top).onSearchEntered(entered);
+ if (top instanceof ToolbarSearchCallback) {
+ ((ToolbarSearchCallback) top).onSearchEntered(entered);
}
}
- public interface DrawerCallbacks {
+ public interface DrawerCallback {
void onPinClicked(Pin pin);
boolean isPinCurrent(Pin pin);
+ }
+ public interface ToolbarSearchCallback {
void onSearchVisibilityChanged(boolean visible);
void onSearchEntered(String entered);
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 60ce48eb..02869bea 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
@@ -33,7 +33,7 @@ import java.util.List;
import de.greenrobot.event.EventBus;
-public abstract class ThreadController extends Controller implements ThreadLayout.ThreadLayoutCallback, ImageViewerController.PreviewCallback, RootNavigationController.DrawerCallbacks, SwipeRefreshLayout.OnRefreshListener {
+public abstract class ThreadController extends Controller implements ThreadLayout.ThreadLayoutCallback, ImageViewerController.PreviewCallback, RootNavigationController.DrawerCallback, SwipeRefreshLayout.OnRefreshListener, RootNavigationController.ToolbarSearchCallback {
protected ThreadLayout threadLayout;
private SwipeRefreshLayout swipeRefreshLayout;
diff --git a/Clover/app/src/main/java/org/floens/chan/ui/activity/ImagePickActivity.java b/Clover/app/src/main/java/org/floens/chan/ui/helper/ImagePickDelegate.java
similarity index 57%
rename from Clover/app/src/main/java/org/floens/chan/ui/activity/ImagePickActivity.java
rename to Clover/app/src/main/java/org/floens/chan/ui/helper/ImagePickDelegate.java
index bdabaabe..0c994005 100644
--- a/Clover/app/src/main/java/org/floens/chan/ui/activity/ImagePickActivity.java
+++ b/Clover/app/src/main/java/org/floens/chan/ui/helper/ImagePickDelegate.java
@@ -15,13 +15,12 @@
* You should have received a copy of the GNU General Public License
* along with this program. If not, see .
*/
-package org.floens.chan.ui.activity;
+package org.floens.chan.ui.helper;
import android.app.Activity;
import android.content.Intent;
import android.database.Cursor;
import android.net.Uri;
-import android.os.Bundle;
import android.os.ParcelFileDescriptor;
import android.provider.OpenableColumns;
@@ -37,45 +36,60 @@ import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
-public class ImagePickActivity extends Activity implements Runnable {
+import static org.floens.chan.utils.AndroidUtils.runOnUiThread;
+
+public class ImagePickDelegate implements Runnable {
private static final String TAG = "ImagePickActivity";
- private static final int IMAGE_RESULT = 1;
+ private static final int IMAGE_PICK_RESULT = 2;
private static final long MAX_FILE_SIZE = 15 * 1024 * 1024;
+ private static final String DEFAULT_FILE_NAME = "file";
private ReplyManager replyManager;
+ private Activity activity;
+
+ private ImagePickCallback callback;
private Uri uri;
- private String fileName = "file";
+ private String fileName;
private boolean success = false;
private File cacheFile;
- @Override
- public void onCreate(Bundle savedInstanceState) {
- super.onCreate(savedInstanceState);
+ public ImagePickDelegate(Activity activity) {
+ this.activity = activity;
replyManager = Chan.getReplyManager();
+ }
- Intent intent = new Intent(Intent.ACTION_GET_CONTENT);
- intent.addCategory(Intent.CATEGORY_OPENABLE);
- intent.setType("*/*");
-
- if (intent.resolveActivity(getPackageManager()) != null) {
- startActivityForResult(intent, IMAGE_RESULT);
+ public boolean pick(ImagePickCallback callback) {
+ if (this.callback != null) {
+ return false;
} else {
- Logger.e(TAG, "No activity found to get file with");
- replyManager._onFilePickError(false);
+ this.callback = callback;
+
+ Intent intent = new Intent(Intent.ACTION_GET_CONTENT);
+ intent.addCategory(Intent.CATEGORY_OPENABLE);
+ intent.setType("*/*");
+
+ if (intent.resolveActivity(activity.getPackageManager()) != null) {
+ activity.startActivityForResult(intent, IMAGE_PICK_RESULT);
+ return true;
+ } else {
+ Logger.e(TAG, "No activity found to get file with");
+ callback.onFilePickError(false);
+ reset();
+ return false;
+ }
}
}
- @Override
public void onActivityResult(int requestCode, int resultCode, Intent data) {
boolean ok = false;
boolean cancelled = false;
- if (requestCode == IMAGE_RESULT) {
- if (resultCode == RESULT_OK && data != null) {
+ if (requestCode == IMAGE_PICK_RESULT) {
+ if (resultCode == Activity.RESULT_OK && data != null) {
uri = data.getData();
- Cursor returnCursor = getContentResolver().query(uri, null, null, null, null);
+ Cursor returnCursor = activity.getContentResolver().query(uri, null, null, null, null);
if (returnCursor != null) {
int nameIndex = returnCursor.getColumnIndex(OpenableColumns.DISPLAY_NAME);
returnCursor.moveToFirst();
@@ -86,20 +100,29 @@ public class ImagePickActivity extends Activity implements Runnable {
returnCursor.close();
}
- replyManager._onFilePickLoading();
+ if (fileName == null) {
+ // As per the comment on OpenableColumns.DISPLAY_NAME:
+ // If this is not provided then the name should default to the last segment of the file's URI.
+ fileName = uri.getLastPathSegment();
+ }
+
+ if (fileName == null) {
+ fileName = DEFAULT_FILE_NAME;
+ }
+
+ callback.onFilePickLoading();
new Thread(this).start();
ok = true;
- } else if (resultCode == RESULT_CANCELED) {
+ } else if (resultCode == Activity.RESULT_CANCELED) {
cancelled = true;
}
}
if (!ok) {
- replyManager._onFilePickError(cancelled);
+ callback.onFilePickError(cancelled);
+ reset();
}
-
- finish();
}
@Override
@@ -110,7 +133,7 @@ public class ImagePickActivity extends Activity implements Runnable {
InputStream is = null;
OutputStream os = null;
try {
- fileDescriptor = getContentResolver().openFileDescriptor(uri, "r");
+ fileDescriptor = activity.getContentResolver().openFileDescriptor(uri, "r");
is = new FileInputStream(fileDescriptor.getFileDescriptor());
os = new FileOutputStream(cacheFile);
boolean fullyCopied = IOUtils.copy(is, os, MAX_FILE_SIZE);
@@ -135,11 +158,28 @@ public class ImagePickActivity extends Activity implements Runnable {
@Override
public void run() {
if (success) {
- replyManager._onFilePicked(fileName, cacheFile);
+ callback.onFilePicked(fileName, cacheFile);
} else {
- replyManager._onFilePickError(false);
+ callback.onFilePickError(false);
}
+ reset();
}
});
}
+
+ private void reset() {
+ callback = null;
+ cacheFile = null;
+ success = false;
+ fileName = null;
+ uri = null;
+ }
+
+ public interface ImagePickCallback {
+ void onFilePickLoading();
+
+ void onFilePicked(String fileName, File file);
+
+ void onFilePickError(boolean cancelled);
+ }
}
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 ae7d61d0..211e5f14 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
@@ -38,6 +38,8 @@ import org.floens.chan.core.model.Loadable;
import org.floens.chan.core.model.Reply;
import org.floens.chan.core.presenter.ReplyPresenter;
import org.floens.chan.core.settings.ChanSettings;
+import org.floens.chan.ui.helper.ImagePickDelegate;
+import org.floens.chan.ui.activity.StartActivity;
import org.floens.chan.ui.drawable.DropdownArrowDrawable;
import org.floens.chan.ui.theme.ThemeHelper;
import org.floens.chan.ui.view.LoadView;
@@ -396,6 +398,11 @@ public class ReplyLayout extends LoadView implements View.OnClickListener, Anima
callback.showThread(loadable);
}
+ @Override
+ public ImagePickDelegate getImagePickDelegate() {
+ return ((StartActivity) getContext()).getImagePickDelegate();
+ }
+
public interface ReplyLayoutCallback {
void highlightPostNo(int no);
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 b3bc6e5e..59daf19a 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
@@ -46,6 +46,7 @@ import com.android.volley.VolleyError;
import org.floens.chan.Chan;
import org.floens.chan.R;
import org.floens.chan.controller.Controller;
+import org.floens.chan.core.database.DatabaseManager;
import org.floens.chan.core.model.ChanThread;
import org.floens.chan.core.model.Loadable;
import org.floens.chan.core.model.Post;
@@ -54,7 +55,6 @@ import org.floens.chan.core.model.PostLinkable;
import org.floens.chan.core.model.ThreadHide;
import org.floens.chan.core.presenter.ThreadPresenter;
import org.floens.chan.core.settings.ChanSettings;
-import org.floens.chan.database.DatabaseManager;
import org.floens.chan.ui.adapter.PostFilter;
import org.floens.chan.ui.cell.PostCellInterface;
import org.floens.chan.ui.helper.PostPopupHelper;
@@ -97,6 +97,7 @@ public class ThreadLayout extends CoordinatorLayout implements ThreadPresenter.T
private ProgressDialog deletingDialog;
private boolean refreshedFromSwipe;
private boolean showingReplyButton = false;
+ private Snackbar newPostsNotification;
public ThreadLayout(Context context) {
super(context);
@@ -379,6 +380,30 @@ public class ThreadLayout extends CoordinatorLayout implements ThreadPresenter.T
fixSnackbarText(getContext(), snackbar);
}
+ @Override
+ public void showNewPostsNotification(boolean show, int more) {
+ if (show) {
+ if (!threadListLayout.scrolledToBottom()) {
+ String text = getContext().getString(R.string.thread_new_posts,
+ more, getContext().getResources().getQuantityString(R.plurals.posts, more, more));
+
+ newPostsNotification = Snackbar.make(this, text, Snackbar.LENGTH_LONG);
+ newPostsNotification.setAction(R.string.thread_new_posts_goto, new OnClickListener() {
+ @Override
+ public void onClick(View v) {
+ presenter.scrollTo(-1, false);
+ }
+ }).show();
+ fixSnackbarText(getContext(), newPostsNotification);
+ }
+ } else {
+ if (newPostsNotification != null) {
+ newPostsNotification.dismiss();
+ newPostsNotification = null;
+ }
+ }
+ }
+
public ThumbnailView getThumbnail(PostImage postImage) {
if (postPopupHelper.isOpen()) {
return postPopupHelper.getThumbnail(postImage);
@@ -414,6 +439,7 @@ public class ThreadLayout extends CoordinatorLayout implements ThreadPresenter.T
postPopupHelper.popAll();
showSearch(false);
showReplyButton(false);
+ newPostsNotification = null;
break;
}
}
diff --git a/Clover/app/src/main/java/org/floens/chan/ui/layout/ThreadListLayout.java b/Clover/app/src/main/java/org/floens/chan/ui/layout/ThreadListLayout.java
index e4c102f8..d9774254 100644
--- a/Clover/app/src/main/java/org/floens/chan/ui/layout/ThreadListLayout.java
+++ b/Clover/app/src/main/java/org/floens/chan/ui/layout/ThreadListLayout.java
@@ -248,7 +248,7 @@ public class ThreadListLayout extends LinearLayout implements ReplyLayout.ReplyL
if (query != null) {
int size = postAdapter.getDisplaySize();
searchStatus.setText(getContext().getString(R.string.search_results,
- getContext().getResources().getQuantityString(R.plurals.posts, size, size), query));
+ size, getContext().getResources().getQuantityString(R.plurals.posts, size, size), query));
}
}
@@ -274,6 +274,22 @@ public class ThreadListLayout extends LinearLayout implements ReplyLayout.ReplyL
return true;
}
+ public boolean scrolledToBottom() {
+ switch (postViewMode) {
+ case LIST:
+ if (((LinearLayoutManager) layoutManager).findLastVisibleItemPosition() == postAdapter.getItemCount() - 1) {
+ return true;
+ }
+ break;
+ case CARD:
+ if (((GridLayoutManager) layoutManager).findLastVisibleItemPosition() == postAdapter.getItemCount() - 1) {
+ return true;
+ }
+ break;
+ }
+ return false;
+ }
+
public void cleanup() {
/*if (ChanBuild.DEVELOPER_MODE) {
Pin pin = ChanApplication.getWatchManager().findPinByLoadable(showingThread.loadable);
diff --git a/Clover/app/src/main/java/org/floens/chan/ui/settings/SettingsController.java b/Clover/app/src/main/java/org/floens/chan/ui/settings/SettingsController.java
index 1b2ce8d8..2d23dc86 100644
--- a/Clover/app/src/main/java/org/floens/chan/ui/settings/SettingsController.java
+++ b/Clover/app/src/main/java/org/floens/chan/ui/settings/SettingsController.java
@@ -74,7 +74,7 @@ public class SettingsController extends Controller implements AndroidUtils.OnMea
}
private void setMargins() {
- boolean tablet = view.getWidth() > dp(500); // TODO is tablet
+ boolean tablet = context.getResources().getBoolean(R.bool.is_tablet);
int margin = 0;
if (tablet) {
diff --git a/Clover/app/src/main/java/org/floens/chan/ui/toolbar/Toolbar.java b/Clover/app/src/main/java/org/floens/chan/ui/toolbar/Toolbar.java
index 9c9d1fc6..9bf27ab1 100644
--- a/Clover/app/src/main/java/org/floens/chan/ui/toolbar/Toolbar.java
+++ b/Clover/app/src/main/java/org/floens/chan/ui/toolbar/Toolbar.java
@@ -394,15 +394,15 @@ public class Toolbar extends LinearLayout implements View.OnClickListener, LoadV
titleContainer.removeView(subtitleView);
}
- if (item.menu != null) {
- menu.addView(item.menu, new LayoutParams(LayoutParams.WRAP_CONTENT, LayoutParams.MATCH_PARENT));
- }
-
if (item.rightView != null) {
item.rightView.setPadding(0, 0, dp(16), 0);
menu.addView(item.rightView, new LayoutParams(LayoutParams.WRAP_CONTENT, LayoutParams.MATCH_PARENT));
}
+ if (item.menu != null) {
+ menu.addView(item.menu, new LayoutParams(LayoutParams.WRAP_CONTENT, LayoutParams.MATCH_PARENT));
+ }
+
AndroidUtils.waitForMeasure(titleView, new AndroidUtils.OnMeasuredCallback() {
@Override
public boolean onMeasured(View view) {
diff --git a/Clover/app/src/main/res/drawable-hdpi/ic_search_white_24dp.png b/Clover/app/src/main/res/drawable-hdpi/ic_search_white_24dp.png
new file mode 100644
index 00000000..bbfbc96c
Binary files /dev/null and b/Clover/app/src/main/res/drawable-hdpi/ic_search_white_24dp.png differ
diff --git a/Clover/app/src/main/res/drawable-mdpi/ic_search_white_24dp.png b/Clover/app/src/main/res/drawable-mdpi/ic_search_white_24dp.png
new file mode 100644
index 00000000..faefc59c
Binary files /dev/null and b/Clover/app/src/main/res/drawable-mdpi/ic_search_white_24dp.png differ
diff --git a/Clover/app/src/main/res/drawable-xhdpi/ic_search_white_24dp.png b/Clover/app/src/main/res/drawable-xhdpi/ic_search_white_24dp.png
new file mode 100644
index 00000000..bfc3e393
Binary files /dev/null and b/Clover/app/src/main/res/drawable-xhdpi/ic_search_white_24dp.png differ
diff --git a/Clover/app/src/main/res/drawable-xxhdpi/ic_search_white_24dp.png b/Clover/app/src/main/res/drawable-xxhdpi/ic_search_white_24dp.png
new file mode 100644
index 00000000..abbb9895
Binary files /dev/null and b/Clover/app/src/main/res/drawable-xxhdpi/ic_search_white_24dp.png differ
diff --git a/Clover/app/src/main/res/drawable-xxxhdpi/ic_search_white_24dp.png b/Clover/app/src/main/res/drawable-xxxhdpi/ic_search_white_24dp.png
new file mode 100644
index 00000000..dd5adfc7
Binary files /dev/null and b/Clover/app/src/main/res/drawable-xxxhdpi/ic_search_white_24dp.png differ
diff --git a/Clover/app/src/main/res/layout/cell_board_edit.xml b/Clover/app/src/main/res/layout/cell_board_edit.xml
index 31d5b5e0..52a498c5 100644
--- a/Clover/app/src/main/res/layout/cell_board_edit.xml
+++ b/Clover/app/src/main/res/layout/cell_board_edit.xml
@@ -18,7 +18,7 @@ along with this program. If not, see .
@@ -32,17 +32,35 @@ along with this program. If not, see .
android:scaleType="center"
tools:ignore="ContentDescription" />
-
+ android:orientation="vertical">
+
+
+
+
+
+
diff --git a/Clover/app/src/main/res/layout/cell_history.xml b/Clover/app/src/main/res/layout/cell_history.xml
new file mode 100644
index 00000000..08c29766
--- /dev/null
+++ b/Clover/app/src/main/res/layout/cell_history.xml
@@ -0,0 +1,92 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/Clover/app/src/main/res/layout/cell_pin.xml b/Clover/app/src/main/res/layout/cell_pin.xml
index 257cb680..304d71cb 100644
--- a/Clover/app/src/main/res/layout/cell_pin.xml
+++ b/Clover/app/src/main/res/layout/cell_pin.xml
@@ -18,7 +18,7 @@ along with this program. If not, see .
+
diff --git a/Clover/app/src/main/res/layout/setting_description.xml b/Clover/app/src/main/res/layout/setting_description.xml
index 0d1d0ebf..820bc445 100644
--- a/Clover/app/src/main/res/layout/setting_description.xml
+++ b/Clover/app/src/main/res/layout/setting_description.xml
@@ -21,11 +21,8 @@ along with this program. If not, see .
android:id="@+id/top"
android:layout_width="match_parent"
android:layout_height="wrap_content"
- android:ellipsize="end"
- android:maxLines="1"
android:paddingLeft="16dp"
android:paddingRight="16dp"
- android:singleLine="true"
android:textColor="?setting_description_top"
android:textSize="16sp" />
@@ -33,11 +30,8 @@ along with this program. If not, see .
android:id="@+id/bottom"
android:layout_width="match_parent"
android:layout_height="wrap_content"
- android:ellipsize="end"
- android:maxLines="1"
android:paddingLeft="16dp"
android:paddingRight="16dp"
- android:singleLine="true"
android:textColor="?setting_description_bottom"
android:textSize="14sp" />
diff --git a/Clover/app/src/main/res/values/strings.xml b/Clover/app/src/main/res/values/strings.xml
index 517910ea..cedfdbd7 100644
--- a/Clover/app/src/main/res/values/strings.xml
+++ b/Clover/app/src/main/res/values/strings.xml
@@ -33,8 +33,8 @@ along with this program. If not, see .
- - %d post
- - %d posts
+ - post
+ - posts
@@ -71,7 +71,7 @@ along with this program. If not, see .
Oldest
Search
- Found %1$s for "%2$s"
+ Found %1$d %2$s for "%3$s"
Search subjects, comments, names and filenames
Open link?
@@ -103,7 +103,8 @@ along with this program. If not, see .
Retry
Archived
Closed
- %1$sR / %2$sI / %3$sP
+ %1$d new %2$s
+ View
Board editor
Add, remove and reorder your boards here.\nThe topmost board will be loaded automatically.
@@ -115,6 +116,10 @@ along with this program. If not, see .
The board with code %1$s is not known.
Sort A-Z
+ Clear history
+ Clear history?
+ Clear
+
Board
Catalog
Watching threads
diff --git a/docs/database.txt b/docs/database.txt
index f65791f4..cf5d2edc 100644
--- a/docs/database.txt
+++ b/docs/database.txt
@@ -41,5 +41,14 @@ ALTER TABLE pin ADD COLUMN order INTEGER;
Changes is version 15:
ALTER TABLE pin ADD COLUMN archived INTEGER;
+
Changes in version 16:
-Table ThreadHide added
+CREATE TABLE `threadhide` (`board` VARCHAR , `id` INTEGER PRIMARY KEY AUTOINCREMENT , `no` INTEGER )
+
+
+Changes is version 17:
+ALTER TABLE board ADD COLUMN description TEXT;
+
+
+Changes in version 18:
+CREATE TABLE `history` (`date` BIGINT , `id` INTEGER PRIMARY KEY AUTOINCREMENT , `loadable_id` INTEGER NOT NULL , `thumbnailUrl` VARCHAR )