Merge branch 'dev' of git@github.com:Floens/Clover.git into http_proxy_settings

Conflicts:
	Clover/app/src/main/java/org/floens/chan/Chan.java
	Clover/app/src/main/java/org/floens/chan/core/settings/ChanSettings.java
multisite
KOSBUM 10 years ago
commit 9654c0ab38
  1. 2
      Clover/app/proguard.cfg
  2. 2
      Clover/app/src/main/AndroidManifest.xml
  3. 13
      Clover/app/src/main/java/org/floens/chan/Chan.java
  4. 176
      Clover/app/src/main/java/org/floens/chan/chan/ChanLoader.java
  5. 9
      Clover/app/src/main/java/org/floens/chan/chan/ChanParser.java
  6. 4
      Clover/app/src/main/java/org/floens/chan/chan/ChanUrls.java
  7. 26
      Clover/app/src/main/java/org/floens/chan/core/database/DatabaseHelper.java
  8. 170
      Clover/app/src/main/java/org/floens/chan/core/database/DatabaseManager.java
  9. 44
      Clover/app/src/main/java/org/floens/chan/core/http/ReplyManager.java
  10. 1
      Clover/app/src/main/java/org/floens/chan/core/manager/BoardManager.java
  11. 32
      Clover/app/src/main/java/org/floens/chan/core/model/Board.java
  12. 36
      Clover/app/src/main/java/org/floens/chan/core/model/History.java
  13. 4
      Clover/app/src/main/java/org/floens/chan/core/model/Loadable.java
  14. 53
      Clover/app/src/main/java/org/floens/chan/core/model/Post.java
  15. 2
      Clover/app/src/main/java/org/floens/chan/core/net/BitmapLruImageCache.java
  16. 6
      Clover/app/src/main/java/org/floens/chan/core/net/BoardsRequest.java
  17. 131
      Clover/app/src/main/java/org/floens/chan/core/net/ChanReaderRequest.java
  18. 9
      Clover/app/src/main/java/org/floens/chan/core/presenter/ReplyPresenter.java
  19. 41
      Clover/app/src/main/java/org/floens/chan/core/presenter/ThreadPresenter.java
  20. 4
      Clover/app/src/main/java/org/floens/chan/core/settings/ChanSettings.java
  21. 2
      Clover/app/src/main/java/org/floens/chan/core/watch/PinWatcher.java
  22. 82
      Clover/app/src/main/java/org/floens/chan/ui/BadgeDrawable.java
  23. 16
      Clover/app/src/main/java/org/floens/chan/ui/activity/StartActivity.java
  24. 2
      Clover/app/src/main/java/org/floens/chan/ui/adapter/PostFilter.java
  25. 4
      Clover/app/src/main/java/org/floens/chan/ui/cell/PostCell.java
  26. 21
      Clover/app/src/main/java/org/floens/chan/ui/cell/ThreadStatusCell.java
  27. 11
      Clover/app/src/main/java/org/floens/chan/ui/controller/BoardEditController.java
  28. 244
      Clover/app/src/main/java/org/floens/chan/ui/controller/HistoryController.java
  29. 21
      Clover/app/src/main/java/org/floens/chan/ui/controller/RootNavigationController.java
  30. 2
      Clover/app/src/main/java/org/floens/chan/ui/controller/ThreadController.java
  31. 96
      Clover/app/src/main/java/org/floens/chan/ui/helper/ImagePickDelegate.java
  32. 7
      Clover/app/src/main/java/org/floens/chan/ui/layout/ReplyLayout.java
  33. 28
      Clover/app/src/main/java/org/floens/chan/ui/layout/ThreadLayout.java
  34. 18
      Clover/app/src/main/java/org/floens/chan/ui/layout/ThreadListLayout.java
  35. 2
      Clover/app/src/main/java/org/floens/chan/ui/settings/SettingsController.java
  36. 8
      Clover/app/src/main/java/org/floens/chan/ui/toolbar/Toolbar.java
  37. BIN
      Clover/app/src/main/res/drawable-hdpi/ic_search_white_24dp.png
  38. BIN
      Clover/app/src/main/res/drawable-mdpi/ic_search_white_24dp.png
  39. BIN
      Clover/app/src/main/res/drawable-xhdpi/ic_search_white_24dp.png
  40. BIN
      Clover/app/src/main/res/drawable-xxhdpi/ic_search_white_24dp.png
  41. BIN
      Clover/app/src/main/res/drawable-xxxhdpi/ic_search_white_24dp.png
  42. 38
      Clover/app/src/main/res/layout/cell_board_edit.xml
  43. 92
      Clover/app/src/main/res/layout/cell_history.xml
  44. 2
      Clover/app/src/main/res/layout/cell_pin.xml
  45. 26
      Clover/app/src/main/res/layout/controller_history.xml
  46. 6
      Clover/app/src/main/res/layout/setting_description.xml
  47. 13
      Clover/app/src/main/res/values/strings.xml
  48. 11
      docs/database.txt

@ -147,3 +147,5 @@
-keepclassmembers class ** {
public void onEvent*(**);
}
-keep public class * extends android.support.design.**

@ -76,8 +76,6 @@ along with this program. If not, see <http://www.gnu.org/licenses/>.
<activity android:name=".test.TestActivity" />
<activity android:name=".ui.activity.ImagePickActivity" />
<service
android:name=".ui.service.WatchNotifier"
android:exported="false" />

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

@ -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<ChanReaderRequest.ChanReaderResponse> {
private static final String TAG = "ChanLoader";
private static final ScheduledExecutorService executor = Executors.newSingleThreadScheduledExecutor();
@ -47,6 +48,7 @@ public class ChanLoader {
private final List<ChanLoaderCallback> 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<Post>());
}
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<Post> cached = thread == null ? new ArrayList<Post>() : thread.posts;
ChanReaderRequest request = ChanReaderRequest.newInstance(loadable, cached,
new Response.Listener<List<Post>>() {
@Override
public void onResponse(List<Post> 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<Post> result) {
if (destroyed)
return;
if (thread == null) {
thread = new ChanThread(loadable, new ArrayList<Post>());
}
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);

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

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

@ -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.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<Pin, Integer> pinDao;
public Dao<Loadable, Integer> loadableDao;
public Dao<SavedReply, Integer> savedDao;
public Dao<Board, Integer> boardsDao;
public Dao<ThreadHide, Integer> threadHideDao;
public Dao<History, Integer> 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() {

@ -15,14 +15,18 @@
* 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.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<SavedReply> savedReplies = new ArrayList<>();
private HashSet<Integer> savedRepliesIds = new HashSet<>();
private final Object savedRepliesLock = new Object();
private final List<SavedReply> savedReplies = new ArrayList<>();
private final HashSet<Integer> savedRepliesIds = new HashSet<>();
private final List<ThreadHide> threadHides = new ArrayList<>();
private final HashSet<Integer> threadHidesIds = new HashSet<>();
private List<ThreadHide> threadHides = new ArrayList<>();
private HashSet<Integer> threadHidesIds = new HashSet<>();
private final Object historyLock = new Object();
private final HashMap<Loadable, History> 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<Void>() {
@Override
public Void call() throws Exception {
List<History> 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<History> getHistory() {
List<History> list = null;
try {
QueryBuilder<History, Integer> 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<History> 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<Void>() {
@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.
*

@ -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<Loadable, Reply> 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);

@ -52,6 +52,7 @@ public class BoardManager {
loadFromServer();
}
// TODO: synchronize
public Board getBoardByValue(String value) {
return allBoardsByValue.get(value);
}

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

@ -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 <http://www.gnu.org/licenses/>.
*/
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;
}

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

@ -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.<br>
* Call {@link #finish()} to parse the comment etc. The post data is invalid if finish returns false.<br>
* 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<Integer> repliesTo = new ArrayList<>();
/**
* These ids replied to this post
*/
public List<Integer> repliesFrom = new ArrayList<>();
public final ArrayList<PostLinkable> 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<Integer> 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;
}
}

@ -29,7 +29,7 @@ public class BitmapLruImageCache extends LruCache<String, Bitmap> implements Ima
@Override
protected int sizeOf(String key, Bitmap value) {
return value.getRowBytes() * value.getHeight();
return value.getRowBytes() * value.getHeight() / 1024;
}
@Override

@ -150,12 +150,12 @@ public class BoardsRequest extends JsonReaderRequest<List<Board>> {
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;

@ -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<List<Post>> {
public class ChanReaderRequest extends JsonReaderRequest<ChanReaderRequest.ChanReaderResponse> {
private static final String TAG = "ChanReaderRequest";
private Loadable loadable;
private List<Post> cached;
private Post op;
private ChanReaderRequest(String url, Listener<List<Post>> listener, ErrorListener errorListener) {
private ChanReaderRequest(String url, Listener<ChanReaderResponse> listener, ErrorListener errorListener) {
super(url, listener, errorListener);
}
public static ChanReaderRequest newInstance(Loadable loadable, List<Post> cached, Listener<List<Post>> listener, ErrorListener errorListener) {
public static ChanReaderRequest newInstance(Loadable loadable, List<Post> cached, Listener<ChanReaderResponse> listener, ErrorListener errorListener) {
String url;
if (loadable.isThreadMode()) {
@ -67,7 +68,7 @@ public class ChanReaderRequest extends JsonReaderRequest<List<Post>> {
}
@Override
public List<Post> readJson(JsonReader reader) throws Exception {
public ChanReaderResponse readJson(JsonReader reader) throws Exception {
List<Post> list;
if (loadable.isThreadMode()) {
@ -81,11 +82,14 @@ public class ChanReaderRequest extends JsonReaderRequest<List<Post>> {
return processPosts(list);
}
private List<Post> processPosts(List<Post> serverList) throws Exception {
List<Post> totalList = new ArrayList<>(serverList.size());
private ChanReaderResponse processPosts(List<Post> 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<List<Post>> {
}
}
cache.deleted = !serverHas;
cache.deleted.set(!serverHas);
}
}
@ -115,68 +119,19 @@ public class ChanReaderRequest extends JsonReaderRequest<List<Post>> {
}
}
// 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<Post>() {
@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<Integer> 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<Post> loadThread(JsonReader reader) throws Exception {
@ -191,7 +146,10 @@ public class ChanReaderRequest extends JsonReaderRequest<List<Post>> {
// 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<List<Post>> {
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<List<Post>> {
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<List<Post>> {
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.<br>
// Used to later copy members like image count to the real op on the main thread.
public Post op;
public List<Post> posts;
}
}

@ -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<ReplyHttpCall>, CaptchaLayout.CaptchaCallback {
public class ReplyPresenter implements ReplyManager.HttpCallback<ReplyHttpCall>, 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();
}
}

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

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

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

@ -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 <http://www.gnu.org/licenses/>.
*/
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);
}
}

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

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

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

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

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

@ -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 <http://www.gnu.org/licenses/>.
*/
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<FloatingMenuItem> 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<HistoryCell> {
private List<History> sourceList = new ArrayList<>();
private List<History> 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);
}
}
}
}
}

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

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

@ -15,13 +15,12 @@
* 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.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);
}
}

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

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

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

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

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

Binary file not shown.

After

Width:  |  Height:  |  Size: 396 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 247 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 465 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 728 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 915 B

@ -18,7 +18,7 @@ along with this program. If not, see <http://www.gnu.org/licenses/>.
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent"
android:layout_height="48dp"
android:layout_height="wrap_content"
android:background="?attr/selectableItemBackground"
android:orientation="horizontal">
@ -32,17 +32,35 @@ along with this program. If not, see <http://www.gnu.org/licenses/>.
android:scaleType="center"
tools:ignore="ContentDescription" />
<TextView
android:id="@+id/text"
<LinearLayout
android:layout_width="0dp"
android:layout_height="match_parent"
android:layout_weight="1"
android:ellipsize="end"
android:gravity="center_vertical"
android:paddingLeft="16dp"
android:paddingRight="16dp"
android:singleLine="true"
android:textColor="?text_color_primary"
android:textSize="14sp" />
android:orientation="vertical">
<TextView
android:id="@+id/text"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:ellipsize="end"
android:paddingLeft="16dp"
android:paddingRight="16dp"
android:paddingTop="8dp"
android:singleLine="true"
android:textColor="?text_color_primary"
android:textSize="14sp" />
<TextView
android:id="@+id/description"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:ellipsize="end"
android:paddingBottom="8dp"
android:paddingLeft="16dp"
android:paddingRight="16dp"
android:textColor="?text_color_secondary"
android:textSize="12sp" />
</LinearLayout>
</LinearLayout>

@ -0,0 +1,92 @@
<?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"
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:background="@drawable/item_background"
android:orientation="vertical">
<LinearLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="horizontal">
<org.floens.chan.ui.view.ThumbnailView
android:id="@+id/thumbnail"
android:layout_width="48dp"
android:layout_height="48dp"
android:layout_gravity="center_vertical"
android:paddingBottom="4dp"
android:paddingLeft="4dp"
android:paddingRight="4dp"
android:paddingTop="4dp" />
<LinearLayout
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_weight="1"
android:orientation="vertical">
<TextView
android:id="@+id/text"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:ellipsize="end"
android:paddingLeft="16dp"
android:paddingRight="16dp"
android:paddingTop="8dp"
android:singleLine="true"
android:textColor="?text_color_primary"
android:textSize="14sp" />
<TextView
android:id="@+id/subtext"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:paddingBottom="8dp"
android:paddingLeft="16dp"
android:paddingRight="16dp"
android:textColor="?text_color_secondary"
android:textSize="12sp" />
</LinearLayout>
<ImageView
android:id="@+id/delete"
android:layout_width="32dp"
android:layout_height="32dp"
android:layout_gravity="center_vertical"
android:paddingBottom="4dp"
android:paddingLeft="4dp"
android:paddingRight="4dp"
android:paddingTop="4dp"
tools:ignore="ContentDescription" />
</LinearLayout>
<View
android:id="@+id/divider"
android:layout_width="match_parent"
android:layout_height="1dp"
android:layout_alignParentBottom="true"
android:layout_alignParentLeft="true"
android:layout_alignParentRight="true"
android:background="?attr/divider_color" />
</LinearLayout>

@ -18,7 +18,7 @@ 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:background="?android:attr/selectableItemBackground"
android:background="@drawable/item_background"
android:orientation="horizontal">
<org.floens.chan.ui.view.ThumbnailView

@ -0,0 +1,26 @@
<?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/>.
-->
<android.support.v7.widget.RecyclerView xmlns:android="http://schemas.android.com/apk/res/android"
android:id="@+id/recycler_view"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:background="?backcolor"
android:clipToPadding="false"
android:padding="16dp"
android:scrollbarStyle="outsideOverlay"
android:scrollbars="vertical" />

@ -21,11 +21,8 @@ along with this program. If not, see <http://www.gnu.org/licenses/>.
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 <http://www.gnu.org/licenses/>.
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" />

@ -33,8 +33,8 @@ along with this program. If not, see <http://www.gnu.org/licenses/>.
</plurals>
<plurals name="posts">
<item quantity="one">%d post</item>
<item quantity="other">%d posts</item>
<item quantity="one">post</item>
<item quantity="other">posts</item>
</plurals>
<plurals name="reply">
@ -71,7 +71,7 @@ along with this program. If not, see <http://www.gnu.org/licenses/>.
<string name="order_oldest">Oldest</string>
<string name="search_hint">Search</string>
<string name="search_results">Found %1$s for "%2$s"</string>
<string name="search_results">Found %1$d %2$s for "%3$s"</string>
<string name="search_empty">Search subjects, comments, names and filenames</string>
<string name="open_link_confirmation">Open link?</string>
@ -103,7 +103,8 @@ along with this program. If not, see <http://www.gnu.org/licenses/>.
<string name="thread_load_failed_retry">Retry</string>
<string name="thread_archived">Archived</string>
<string name="thread_closed">Closed</string>
<string name="thread_stats">%1$sR / %2$sI / %3$sP</string>
<string name="thread_new_posts">%1$d new %2$s</string>
<string name="thread_new_posts_goto">View</string>
<string name="board_edit">Board editor</string>
<string name="board_edit_header">Add, remove and reorder your boards here.\nThe topmost board will be loaded automatically.</string>
@ -115,6 +116,10 @@ along with this program. If not, see <http://www.gnu.org/licenses/>.
<string name="board_add_unknown">The board with code %1$s is not known.</string>
<string name="board_edit_sort_a_z">Sort A-Z</string>
<string name="history_clear">Clear history</string>
<string name="history_clear_confirm">Clear history?</string>
<string name="history_clear_confirm_button">Clear</string>
<string name="drawer_board">Board</string>
<string name="drawer_catalog">Catalog</string>
<string name="drawer_pinned">Watching threads</string>

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

Loading…
Cancel
Save