diff --git a/Clover/app/src/main/assets/captcha/captcha.html b/Clover/app/src/main/assets/captcha/captcha.html index f5913829..8ecec8d4 100644 --- a/Clover/app/src/main/assets/captcha/captcha.html +++ b/Clover/app/src/main/assets/captcha/captcha.html @@ -3,19 +3,26 @@ -
Loading captcha...
-
+
+
Loading captcha…
diff --git a/Clover/app/src/main/java/org/floens/chan/ChanApplication.java b/Clover/app/src/main/java/org/floens/chan/ChanApplication.java index 8898fb61..2f137ced 100644 --- a/Clover/app/src/main/java/org/floens/chan/ChanApplication.java +++ b/Clover/app/src/main/java/org/floens/chan/ChanApplication.java @@ -31,7 +31,7 @@ import com.squareup.leakcanary.RefWatcher; import org.floens.chan.chan.ChanUrls; import org.floens.chan.core.manager.BoardManager; -import org.floens.chan.core.manager.ReplyManager; +import org.floens.chan.core.reply.ReplyManager; import org.floens.chan.core.manager.WatchManager; import org.floens.chan.core.net.BitmapLruImageCache; import org.floens.chan.core.settings.ChanSettings; diff --git a/Clover/app/src/main/java/org/floens/chan/core/loader/ChanLoader.java b/Clover/app/src/main/java/org/floens/chan/core/loader/ChanLoader.java index c4ee95cf..72f9d4af 100644 --- a/Clover/app/src/main/java/org/floens/chan/core/loader/ChanLoader.java +++ b/Clover/app/src/main/java/org/floens/chan/core/loader/ChanLoader.java @@ -344,7 +344,7 @@ public class ChanLoader { if (destroyed) return; - Logger.e(TAG, "Loading error", error); + Logger.e(TAG, "Loading error"); // 404 with more pages already loaded means endofline if ((error instanceof ServerError) && loadable.isBoardMode() && loadable.no > 0) { diff --git a/Clover/app/src/main/java/org/floens/chan/core/manager/ThreadManager.java b/Clover/app/src/main/java/org/floens/chan/core/manager/ThreadManager.java index 98fcd059..5de96cfc 100644 --- a/Clover/app/src/main/java/org/floens/chan/core/manager/ThreadManager.java +++ b/Clover/app/src/main/java/org/floens/chan/core/manager/ThreadManager.java @@ -43,8 +43,8 @@ import org.floens.chan.chan.ChanUrls; import org.floens.chan.core.settings.ChanSettings; import org.floens.chan.core.loader.ChanLoader; import org.floens.chan.core.loader.LoaderPool; -import org.floens.chan.core.manager.ReplyManager.DeleteListener; -import org.floens.chan.core.manager.ReplyManager.DeleteResponse; +import org.floens.chan.core.reply.ReplyManager.DeleteListener; +import org.floens.chan.core.reply.ReplyManager.DeleteResponse; import org.floens.chan.core.model.ChanThread; import org.floens.chan.core.model.Loadable; import org.floens.chan.core.model.Pin; @@ -231,9 +231,9 @@ public class ThreadManager implements ChanLoader.ChanLoaderCallback { menu.add(Menu.NONE, 9, Menu.NONE, activity.getString(R.string.action_pin)); } - if (chanLoader.getLoadable().isThreadMode()) { - menu.add(Menu.NONE, 10, Menu.NONE, activity.getString(R.string.post_quick_reply)); - } +// if (chanLoader.getLoadable().isThreadMode()) { +// menu.add(Menu.NONE, 10, Menu.NONE, activity.getString(R.string.post_quick_reply)); +// } String[] baseOptions = activity.getResources().getStringArray(R.array.post_options); for (int i = 0; i < baseOptions.length; i++) { @@ -261,10 +261,10 @@ public class ThreadManager implements ChanLoader.ChanLoaderCallback { openReply(false); // Pass through case 0: // Quote - ChanApplication.getReplyManager().quote(post.no); +// ChanApplication.getReplyManager().quote(post.no); break; case 1: // Quote inline - ChanApplication.getReplyManager().quoteInline(post.no, post.comment.toString()); +// ChanApplication.getReplyManager().quoteInline(post.no, post.comment.toString()); break; case 2: // Info showPostInfo(post); 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 2bb9cc40..a20b8c23 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 @@ -107,6 +107,14 @@ public class Loadable { return mode == other.mode && board.equals(other.board) && no == other.no; } + @Override + public int hashCode() { + int result = mode; + result = 31 * result + (board != null ? board.hashCode() : 0); + result = 31 * result + no; + return result; + } + public boolean isBoardMode() { return mode == Mode.BOARD; } diff --git a/Clover/app/src/main/java/org/floens/chan/core/model/Reply.java b/Clover/app/src/main/java/org/floens/chan/core/model/Reply.java index 4f8c3aee..5845e79f 100644 --- a/Clover/app/src/main/java/org/floens/chan/core/model/Reply.java +++ b/Clover/app/src/main/java/org/floens/chan/core/model/Reply.java @@ -23,18 +23,18 @@ import java.io.File; * The data needed to send a reply. */ public class Reply { + public String captchaResponse; + public boolean usePass = false; + public String board; + public int resto; + + public String passId; + public File file; + public String fileName = ""; public String name = ""; - public String email = ""; + public String options = ""; public String subject = ""; public String comment = ""; - public String board = ""; - public int resto = 0; - public File file; - public String fileName = ""; - public String captchaResponse = ""; - public String password = ""; - public boolean usePass = false; - public String passId = ""; + public int selection; public boolean spoilerImage = false; - public int cursorPosition; } 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 new file mode 100644 index 00000000..03abce65 --- /dev/null +++ b/Clover/app/src/main/java/org/floens/chan/core/presenter/ReplyPresenter.java @@ -0,0 +1,412 @@ +package org.floens.chan.core.presenter; + +import android.text.TextUtils; + +import org.floens.chan.ChanApplication; +import org.floens.chan.R; +import org.floens.chan.chan.ChanUrls; +import org.floens.chan.core.manager.BoardManager; +import org.floens.chan.core.manager.WatchManager; +import org.floens.chan.core.model.Board; +import org.floens.chan.core.model.Loadable; +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.reply.ReplyHttpCall; +import org.floens.chan.core.reply.ReplyManager; +import org.floens.chan.core.settings.ChanSettings; +import org.floens.chan.database.DatabaseManager; +import org.floens.chan.ui.layout.CaptchaLayout; + +import java.io.File; +import java.util.regex.Matcher; +import java.util.regex.Pattern; + +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 enum Page { + INPUT, + CAPTCHA, + LOADING + } + + private static final Pattern QUOTE_PATTERN = Pattern.compile(">>\\d+"); + + private ReplyPresenterCallback callback; + + private ReplyManager replyManager; + private BoardManager boardManager; + private WatchManager watchManager; + private DatabaseManager databaseManager; + + private Loadable loadable; + private Board board; + private Reply draft; + + private Page page = Page.INPUT; + private boolean moreOpen; + private boolean previewOpen; + private boolean pickingFile; + private boolean captchaInited; + private int selectedQuote = -1; + + public ReplyPresenter(ReplyPresenterCallback callback) { + this.callback = callback; + replyManager = ChanApplication.getReplyManager(); + boardManager = ChanApplication.getBoardManager(); + watchManager = ChanApplication.getWatchManager(); + databaseManager = ChanApplication.getDatabaseManager(); + } + + public void bindLoadable(Loadable loadable) { + if (this.loadable != null) { + unbindLoadable(); + } + this.loadable = loadable; + + Board board = boardManager.getBoardByValue(loadable.board); + if (board != null) { + this.board = board; + } + + draft = replyManager.getReply(loadable); + + if (TextUtils.isEmpty(draft.name)) { + draft.name = ChanSettings.postDefaultName.get(); + } + + callback.loadDraftIntoViews(draft); + callback.updateCommentCount(0, board.maxCommentChars, false); + callback.setCommentHint(getString(loadable.isThreadMode() ? R.string.reply_comment_thread : R.string.reply_comment_board)); + + if (draft.file != null) { + showPreview(draft.fileName, draft.file); + } + + if (captchaInited) { + callback.resetCaptcha(); + } + + switchPage(Page.INPUT, false); + } + + public void unbindLoadable() { + callback.loadViewsIntoDraft(draft); + replyManager.putReply(loadable, draft); + + loadable = null; + board = null; + closeAll(); + } + + public boolean onBack() { + if (page == Page.LOADING) { + return true; + } else if (page == Page.CAPTCHA) { + switchPage(Page.INPUT, true); + return true; + } else if (moreOpen) { + onMoreClicked(); + return true; + } + return false; + } + + public void onMoreClicked() { + moreOpen = !moreOpen; + callback.openName(moreOpen); + if (!loadable.isThreadMode()) { + callback.openSubject(moreOpen); + } + callback.openOptions(moreOpen); + if (previewOpen) { + callback.openFileName(moreOpen); + if (board.spoilers) { + callback.openSpoiler(moreOpen, false); + } + } + } + + public void onAttachClicked() { + if (!pickingFile) { + if (previewOpen) { + callback.openPreview(false, null); + draft.file = null; + draft.fileName = ""; + if (moreOpen) { + callback.openFileName(false); + if (board.spoilers) { + callback.openSpoiler(false, false); + } + } + previewOpen = false; + } else { + ChanApplication.getReplyManager().pickFile(this); + pickingFile = true; + } + } + } + + public void onSubmitClicked() { + callback.loadViewsIntoDraft(draft); + draft.board = loadable.board; + draft.resto = loadable.isThreadMode() ? loadable.no : -1; + + if (ChanSettings.passLoggedIn()) { + draft.usePass = true; + draft.passId = ChanSettings.passId.get(); + } else { + draft.usePass = false; + draft.passId = null; + } + + draft.spoilerImage = draft.spoilerImage && board.spoilers; + + draft.captchaResponse = null; + if (draft.usePass) { + makeSubmitCall(); + } else { + switchPage(Page.CAPTCHA, true); + } + } + + @Override + public void onHttpSuccess(ReplyHttpCall replyCall) { + if (replyCall.posted) { + if (ChanSettings.postPinThread.get() && loadable.isThreadMode()) { + watchManager.addPin(loadable); + } + + databaseManager.saveReply(new SavedReply(loadable.board, replyCall.postNo, replyCall.password)); + + switchPage(Page.INPUT, false); + closeAll(); + highlightQuotes(); + draft = new Reply(); + replyManager.putReply(loadable, draft); + callback.loadDraftIntoViews(draft); + callback.onPosted(); + + if (!loadable.isThreadMode()) { + callback.showThread(new Loadable(loadable.board, replyCall.postNo)); + } + } else { + if (replyCall.errorMessage == null) { + replyCall.errorMessage = getString(R.string.reply_error); + } + + switchPage(Page.INPUT, true); + callback.openMessage(true, false, replyCall.errorMessage, true); + } + } + + @Override + public void onHttpFail(ReplyHttpCall httpPost) { + switchPage(Page.INPUT, true); + callback.openMessage(true, false, getString(R.string.reply_error), true); + } + + @Override + public void captchaLoaded(CaptchaLayout captchaLayout) { + } + + @Override + public void captchaEntered(CaptchaLayout captchaLayout, String response) { + draft.captchaResponse = response; + captchaLayout.reset(); + makeSubmitCall(); + } + + public void onCommentTextChanged(int length) { + callback.updateCommentCount(length, board.maxCommentChars, length > board.maxCommentChars); + } + + public void onSelectionChanged() { + callback.loadViewsIntoDraft(draft); + highlightQuotes(); + } + + public void quote(Post post, boolean withText) { + callback.loadViewsIntoDraft(draft); + + String textToInsert = ""; + if (draft.selection - 1 >= 0 && draft.selection - 1 < draft.comment.length() && draft.comment.charAt(draft.selection - 1) != '\n') { + textToInsert += "\n"; + } + + textToInsert += ">>" + post.no + "\n"; + + if (withText) { + String[] lines = post.comment.toString().split("\n+"); + for (String line : lines) { + textToInsert += ">" + line + "\n"; + } + } + + draft.comment = new StringBuilder(draft.comment).insert(draft.selection, textToInsert).toString(); + + draft.selection += textToInsert.length(); + + callback.loadDraftIntoViews(draft); + + highlightQuotes(); + } + + @Override + public void onFilePickLoading() { + callback.onFilePickLoading(); + } + + @Override + public void onFilePicked(String name, File file) { + pickingFile = false; + draft.file = file; + draft.fileName = name; + showPreview(name, file); + } + + @Override + public void onFilePickError(boolean cancelled) { + pickingFile = false; + if (!cancelled) { + callback.onFilePickError(); + } + } + + private void closeAll() { + moreOpen = false; + previewOpen = false; + selectedQuote = -1; + callback.openMessage(false, true, "", false); + callback.openName(false); + callback.openSubject(false); + callback.openOptions(false); + callback.openFileName(false); + callback.openSpoiler(false, false); + callback.openPreview(false, null); + callback.openPreviewMessage(false, null); + } + + private void makeSubmitCall() { + replyManager.makeHttpCall(new ReplyHttpCall(draft), this); + switchPage(Page.LOADING, true); + } + + private void switchPage(Page page, boolean animate) { + if (this.page != page) { + this.page = page; + switch (page) { + case LOADING: + callback.setPage(Page.LOADING, true); + break; + case INPUT: + callback.setPage(Page.INPUT, animate); + break; + case CAPTCHA: + callback.setPage(Page.CAPTCHA, true); + + if (!captchaInited) { + captchaInited = true; + String baseUrl = loadable.isThreadMode() ? + ChanUrls.getThreadUrlDesktop(loadable.board, loadable.no) : + ChanUrls.getBoardUrlDesktop(loadable.board); + callback.initCaptcha(baseUrl, ChanUrls.getCaptchaSiteKey(), ChanApplication.getInstance().getUserAgent(), this); + } + break; + } + } + } + + private void highlightQuotes() { + Matcher matcher = QUOTE_PATTERN.matcher(draft.comment); + + // Find all occurrences of >>\d+ with start and end between selection + int no = -1; + while (matcher.find()) { + if (matcher.start() <= draft.selection && matcher.end() >= draft.selection - 1) { + String quote = matcher.group().substring(2); + try { + no = Integer.parseInt(quote); + break; + } catch (NumberFormatException ignored) { + } + } + } + + // Allow no = -1 removing the highlight + if (no != selectedQuote) { + selectedQuote = no; + callback.highlightPostNo(no); + } + } + + private void showPreview(String name, File file) { + callback.openPreview(true, file); + if (moreOpen) { + callback.openFileName(true); + if (board.spoilers) { + callback.openSpoiler(true, false); + } + } + callback.setFileName(name); + previewOpen = true; + + boolean probablyWebm = file.getName().endsWith(".webm"); + int maxSize = probablyWebm ? board.maxWebmSize : board.maxFileSize; + if (file.length() > maxSize) { + String fileSize = getReadableFileSize(file.length(), false); + String maxSizeString = getReadableFileSize(maxSize, false); + String text = getRes().getString(probablyWebm ? R.string.reply_webm_too_big : R.string.reply_file_too_big, fileSize, maxSizeString); + callback.openPreviewMessage(true, text); + } else { + callback.openPreviewMessage(false, null); + } + } + + public interface ReplyPresenterCallback { + void loadViewsIntoDraft(Reply draft); + + void loadDraftIntoViews(Reply draft); + + void setPage(Page page, boolean animate); + + void initCaptcha(String baseUrl, String siteKey, String userAgent, CaptchaLayout.CaptchaCallback callback); + + void resetCaptcha(); + + void openMessage(boolean open, boolean animate, String message, boolean autoHide); + + void onPosted(); + + void setCommentHint(String hint); + + void openName(boolean open); + + void openSubject(boolean open); + + void openOptions(boolean open); + + void openFileName(boolean open); + + void setFileName(String fileName); + + void updateCommentCount(int count, int maxCount, boolean over); + + void openPreview(boolean show, File previewFile); + + void openPreviewMessage(boolean show, String message); + + void openSpoiler(boolean show, boolean checked); + + void onFilePickLoading(); + + void onFilePickError(); + + void highlightPostNo(int no); + + void showThread(Loadable loadable); + } +} 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 3e1c6460..54e6b206 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 @@ -18,7 +18,6 @@ package org.floens.chan.core.presenter; import android.text.TextUtils; -import android.view.Menu; import com.android.volley.VolleyError; @@ -36,11 +35,12 @@ import org.floens.chan.core.model.PostImage; import org.floens.chan.core.model.PostLinkable; import org.floens.chan.core.model.SavedReply; 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.cell.PostCell; import org.floens.chan.ui.cell.ThreadStatusCell; +import org.floens.chan.ui.layout.ThreadListLayout; import org.floens.chan.ui.view.FloatingMenuItem; -import org.floens.chan.ui.view.PostView; import org.floens.chan.ui.view.ThumbnailView; import org.floens.chan.utils.AndroidUtils; @@ -48,9 +48,7 @@ import java.util.ArrayList; import java.util.List; import java.util.Locale; -import static org.floens.chan.utils.AndroidUtils.getString; - -public class ThreadPresenter implements ChanLoader.ChanLoaderCallback, PostAdapter.PostAdapterCallback, PostCell.PostCellCallback, ThreadStatusCell.Callback { +public class ThreadPresenter implements ChanLoader.ChanLoaderCallback, PostAdapter.PostAdapterCallback, PostCell.PostCellCallback, ThreadStatusCell.Callback, ThreadListLayout.ThreadListLayoutCallback { private static final int POST_OPTION_QUOTE = 0; private static final int POST_OPTION_QUOTE_TEXT = 1; private static final int POST_OPTION_INFO = 2; @@ -61,7 +59,9 @@ public class ThreadPresenter implements ChanLoader.ChanLoaderCallback, PostAdapt private static final int POST_OPTION_DELETE = 7; private static final int POST_OPTION_SAVE = 8; private static final int POST_OPTION_PIN = 9; - private static final int POST_OPTION_QUICK_REPLY = 10; + + private WatchManager watchManager; + private DatabaseManager databaseManager; private ThreadPresenterCallback threadPresenterCallback; @@ -71,6 +71,9 @@ public class ThreadPresenter implements ChanLoader.ChanLoaderCallback, PostAdapt public ThreadPresenter(ThreadPresenterCallback threadPresenterCallback) { this.threadPresenterCallback = threadPresenterCallback; + + watchManager = ChanApplication.getWatchManager(); + databaseManager = ChanApplication.getDatabaseManager(); } public void bindLoadable(Loadable loadable) { @@ -80,7 +83,7 @@ public class ThreadPresenter implements ChanLoader.ChanLoaderCallback, PostAdapt } this.loadable = loadable; - Pin pin = ChanApplication.getWatchManager().findPinByLoadable(loadable); + Pin pin = watchManager.findPinByLoadable(loadable); if (pin != null) { // Use the loadable from the pin. // This way we can store the list position in the pin loadable, @@ -121,20 +124,19 @@ public class ThreadPresenter implements ChanLoader.ChanLoaderCallback, PostAdapt public boolean pin() { if (chanLoader.getThread() != null) { - WatchManager wm = ChanApplication.getWatchManager(); - Pin pin = wm.findPinByLoadable(loadable); + Pin pin = watchManager.findPinByLoadable(loadable); if (pin == null) { Post op = chanLoader.getThread().op; - wm.addPin(loadable, op); + watchManager.addPin(loadable, op); } else { - wm.removePin(pin); + watchManager.removePin(pin); } } return isPinned(); } public boolean isPinned() { - return ChanApplication.getWatchManager().findPinByLoadable(loadable) != null; + return watchManager.findPinByLoadable(loadable) != null; } public void onSearchVisibilityChanged(boolean visible) { @@ -181,10 +183,10 @@ public class ThreadPresenter implements ChanLoader.ChanLoaderCallback, PostAdapt loadable.lastViewed = posts.get(posts.size() - 1).no; } - Pin pin = ChanApplication.getWatchManager().findPinByLoadable(loadable); + Pin pin = watchManager.findPinByLoadable(loadable); if (pin != null) { pin.onBottomPostViewed(); - ChanApplication.getWatchManager().updatePin(pin); + watchManager.updatePin(pin); } } @@ -202,7 +204,9 @@ public class ThreadPresenter implements ChanLoader.ChanLoaderCallback, PostAdapt break; } } - scrollTo(position, smooth); + if (position >= 0) { + scrollTo(position, smooth); + } } } @@ -215,7 +219,9 @@ public class ThreadPresenter implements ChanLoader.ChanLoaderCallback, PostAdapt break; } } - scrollTo(position, smooth); + if (position >= 0) { + scrollTo(position, smooth); + } } public void highlightPost(Post post) { @@ -261,16 +267,13 @@ public class ThreadPresenter implements ChanLoader.ChanLoaderCallback, PostAdapt @Override public void onPopulatePostOptions(Post post, List menu) { - if (loadable.isBoardMode() || loadable.isCatalogMode()) { + if (!loadable.isThreadMode()) { menu.add(new FloatingMenuItem(POST_OPTION_PIN, R.string.action_pin)); + } else { + menu.add(new FloatingMenuItem(POST_OPTION_QUOTE, R.string.post_quote)); + menu.add(new FloatingMenuItem(POST_OPTION_QUOTE_TEXT, R.string.post_quote_text)); } - if (loadable.isThreadMode()) { - menu.add(new FloatingMenuItem(POST_OPTION_QUICK_REPLY, R.string.post_quick_reply)); - } - - menu.add(new FloatingMenuItem(POST_OPTION_QUOTE, R.string.post_quote)); - menu.add(new FloatingMenuItem(POST_OPTION_QUOTE_TEXT, R.string.post_quote_text)); menu.add(new FloatingMenuItem(POST_OPTION_INFO, R.string.post_info)); menu.add(new FloatingMenuItem(POST_OPTION_LINKS, R.string.post_show_links)); menu.add(new FloatingMenuItem(POST_OPTION_COPY_TEXT, R.string.post_copy_text)); @@ -280,8 +283,7 @@ public class ThreadPresenter implements ChanLoader.ChanLoaderCallback, PostAdapt menu.add(new FloatingMenuItem(POST_OPTION_HIGHLIGHT_ID, R.string.post_highlight_id)); } - // Only add the delete option when the post is a saved reply - if (ChanApplication.getDatabaseManager().isSavedReply(post.board, post.no)) { + if (databaseManager.isSavedReply(post.board, post.no)) { menu.add(new FloatingMenuItem(POST_OPTION_DELETE, R.string.delete)); } @@ -292,14 +294,11 @@ public class ThreadPresenter implements ChanLoader.ChanLoaderCallback, PostAdapt public void onPostOptionClicked(Post post, Object id) { switch ((Integer) id) { - case POST_OPTION_QUICK_REPLY: -// openReply(false); TODO - // Pass through case POST_OPTION_QUOTE: - ChanApplication.getReplyManager().quote(post.no); + threadPresenterCallback.quote(post, false); break; case POST_OPTION_QUOTE_TEXT: - ChanApplication.getReplyManager().quoteInline(post.no, post.comment.toString()); + threadPresenterCallback.quote(post, true); break; case POST_OPTION_INFO: showPostInfo(post); @@ -322,10 +321,10 @@ public class ThreadPresenter implements ChanLoader.ChanLoaderCallback, PostAdapt // deletePost(post); TODO break; case POST_OPTION_SAVE: - ChanApplication.getDatabaseManager().saveReply(new SavedReply(post.board, post.no, "foo")); + databaseManager.saveReply(new SavedReply(post.board, post.no, "foo")); break; case POST_OPTION_PIN: - ChanApplication.getWatchManager().addPin(post); + watchManager.addPin(post); break; } } @@ -387,6 +386,11 @@ public class ThreadPresenter implements ChanLoader.ChanLoaderCallback, PostAdapt chanLoader.requestMoreDataAndResetTimer(); } + @Override + public void showThread(Loadable loadable) { + threadPresenterCallback.showThread(loadable); + } + private void showPostInfo(Post post) { String text = ""; @@ -484,5 +488,7 @@ public class ThreadPresenter implements ChanLoader.ChanLoaderCallback, PostAdapt void showSearch(boolean show); void filterList(String query, List filter, boolean clearFilter, boolean setEmptyText, boolean hideKeyboard); + + void quote(Post post, boolean withText); } } diff --git a/Clover/app/src/main/java/org/floens/chan/core/reply/HttpCall.java b/Clover/app/src/main/java/org/floens/chan/core/reply/HttpCall.java new file mode 100644 index 00000000..fb359b57 --- /dev/null +++ b/Clover/app/src/main/java/org/floens/chan/core/reply/HttpCall.java @@ -0,0 +1,71 @@ +package org.floens.chan.core.reply; + +import com.squareup.okhttp.Callback; +import com.squareup.okhttp.Request; +import com.squareup.okhttp.Response; + +import org.floens.chan.utils.AndroidUtils; +import org.floens.chan.utils.IOUtils; +import org.floens.chan.utils.Logger; + +import java.io.IOException; + +public abstract class HttpCall implements Callback { + private static final String TAG = "HttpCall"; + + private boolean successful = false; + private ReplyManager.HttpCallback callback; + + public void setSuccessful(boolean successful) { + this.successful = successful; + } + + public abstract void setup(Request.Builder requestBuilder); + + public abstract void process(Response response, String result) throws IOException; + + public void fail(Request request, IOException e) { + } + + @SuppressWarnings("unchecked") + public void postUI(boolean successful) { + if (successful) { + callback.onHttpSuccess(this); + } else { + callback.onHttpFail(this); + } + } + + @Override + public void onResponse(Response response) { + try { + if (response.isSuccessful() && response.body() != null) { + String responseString = response.body().string(); + process(response, responseString); + successful = true; + } else { + onFailure(response.request(), null); + } + } catch (IOException e) { + Logger.e(TAG, "IOException processing response", e); + } finally { + IOUtils.closeQuietly(response.body()); + } + + AndroidUtils.runOnUiThread(new Runnable() { + @Override + public void run() { + postUI(successful); + } + }); + } + + @Override + public void onFailure(Request request, IOException e) { + fail(request, e); + } + + void setCallback(ReplyManager.HttpCallback callback) { + this.callback = callback; + } +} diff --git a/Clover/app/src/main/java/org/floens/chan/core/reply/ReplyHttpCall.java b/Clover/app/src/main/java/org/floens/chan/core/reply/ReplyHttpCall.java new file mode 100644 index 00000000..21642ac2 --- /dev/null +++ b/Clover/app/src/main/java/org/floens/chan/core/reply/ReplyHttpCall.java @@ -0,0 +1,112 @@ +package org.floens.chan.core.reply; + +import android.text.TextUtils; + +import com.squareup.okhttp.MediaType; +import com.squareup.okhttp.MultipartBuilder; +import com.squareup.okhttp.Request; +import com.squareup.okhttp.RequestBody; +import com.squareup.okhttp.Response; + +import org.floens.chan.chan.ChanUrls; +import org.floens.chan.core.model.Reply; +import org.jsoup.Jsoup; + +import java.io.IOException; +import java.util.Random; +import java.util.regex.Matcher; +import java.util.regex.Pattern; + +public class ReplyHttpCall extends HttpCall { + private static final String TAG = "ReplyHttpCall"; + private static final Random RANDOM = new Random(); + private static final Pattern THREAD_NO_PATTERN = Pattern.compile(""); + private static final Pattern ERROR_MESSAGE = Pattern.compile("\"errmsg\"[^>]*>(.*?)<\\/span"); + + public boolean posted; + public String errorMessage; + public String text; + public String password; + public int threadNo = -1; + public int postNo = -1; + + private final Reply reply; + + public ReplyHttpCall(Reply reply) { + this.reply = reply; + } + + @Override + public void setup(Request.Builder requestBuilder) { + boolean thread = reply.resto >= 0; + + password = Long.toHexString(RANDOM.nextLong()); + + MultipartBuilder formBuilder = new MultipartBuilder(); + formBuilder.type(MultipartBuilder.FORM); + + formBuilder.addFormDataPart("mode", "regist"); + formBuilder.addFormDataPart("pwd", password); + + if (thread) { + formBuilder.addFormDataPart("resto", String.valueOf(reply.resto)); + } + + formBuilder.addFormDataPart("name", reply.name); + formBuilder.addFormDataPart("email", reply.options); + + if (!thread && !TextUtils.isEmpty(reply.subject)) { + formBuilder.addFormDataPart("sub", reply.subject); + } + + formBuilder.addFormDataPart("com", reply.comment); + + if (reply.captchaResponse != null) { + formBuilder.addFormDataPart("g-recaptcha-response", reply.captchaResponse); + } + + if (reply.file != null) { + formBuilder.addFormDataPart("upfile", reply.fileName, RequestBody.create( + MediaType.parse("application/octet-stream"), reply.file + )); + } + + if (reply.spoilerImage) { + formBuilder.addFormDataPart("spoiler", "on"); + } + + requestBuilder.url(ChanUrls.getReplyUrl(reply.board)); + requestBuilder.post(formBuilder.build()); + + if (reply.usePass) { + requestBuilder.addHeader("Cookie", "pass_id=" + reply.passId); + } + } + + @Override + public void process(Response response, String result) throws IOException { + text = result; + + Matcher errorMessageMatcher = ERROR_MESSAGE.matcher(result); + if (errorMessageMatcher.find()) { + errorMessage = Jsoup.parse(errorMessageMatcher.group(1)).body().ownText().replace("[]", ""); + } else { + Matcher threadNoMatcher = THREAD_NO_PATTERN.matcher(result); + if (threadNoMatcher.find()) { + try { + threadNo = Integer.parseInt(threadNoMatcher.group(1)); + postNo = Integer.parseInt(threadNoMatcher.group(2)); + } catch (NumberFormatException ignored) { + } + + if (threadNo >= 0 && postNo >= 0) { + posted = true; + } + } + } + } + + @Override + public void fail(Request request, IOException e) { + } +} diff --git a/Clover/app/src/main/java/org/floens/chan/core/manager/ReplyManager.java b/Clover/app/src/main/java/org/floens/chan/core/reply/ReplyManager.java similarity index 85% rename from Clover/app/src/main/java/org/floens/chan/core/manager/ReplyManager.java rename to Clover/app/src/main/java/org/floens/chan/core/reply/ReplyManager.java index 43d55887..533c801e 100644 --- a/Clover/app/src/main/java/org/floens/chan/core/manager/ReplyManager.java +++ b/Clover/app/src/main/java/org/floens/chan/core/reply/ReplyManager.java @@ -15,7 +15,7 @@ * You should have received a copy of the GNU General Public License * along with this program. If not, see . */ -package org.floens.chan.core.manager; +package org.floens.chan.core.reply; import android.content.Context; import android.content.Intent; @@ -33,6 +33,7 @@ import com.squareup.okhttp.Response; import org.floens.chan.ChanApplication; import org.floens.chan.R; import org.floens.chan.chan.ChanUrls; +import org.floens.chan.core.model.Loadable; import org.floens.chan.core.model.Reply; import org.floens.chan.core.model.SavedReply; import org.floens.chan.ui.activity.ImagePickActivity; @@ -42,8 +43,10 @@ import org.floens.chan.utils.Logger; import java.io.File; import java.io.IOException; import java.net.HttpCookie; +import java.util.HashMap; import java.util.List; import java.util.Locale; +import java.util.Map; import java.util.Random; import java.util.concurrent.TimeUnit; import java.util.regex.Matcher; @@ -59,14 +62,14 @@ public class ReplyManager { private static final int TIMEOUT = 10000; private final Context context; - private Reply draft; private FileListener fileListener; private final Random random = new Random(); private OkHttpClient client; + private Map drafts = new HashMap<>(); + public ReplyManager(Context context) { this.context = context; - draft = new Reply(); client = new OkHttpClient(); client.setConnectTimeout(TIMEOUT, TimeUnit.MILLISECONDS); @@ -74,51 +77,27 @@ public class ReplyManager { client.setWriteTimeout(TIMEOUT, TimeUnit.MILLISECONDS); } - /** - * Clear the draft - */ - public void removeReplyDraft() { - draft = new Reply(); - } - - /** - * Set an reply draft. - * - * @param value the draft to save. - */ - public void setReplyDraft(Reply value) { - draft = value; - } - - /** - * Gets the saved reply draft. - * - * @return the saved draft or an empty draft. - */ - public Reply getReplyDraft() { - return draft; - } - - /** - * Add an quote to the comment field. Looks like >>123456789\n - * - * @param no the raw no to quote to. - */ - public void quote(int no) { - String textToInsert = ">>" + no + "\n"; - draft.comment = new StringBuilder(draft.comment).insert(draft.cursorPosition, textToInsert).toString(); - draft.cursorPosition += textToInsert.length(); + public Reply getReply(Loadable loadable) { + Reply reply = drafts.get(loadable); + if (reply == null) { + reply = new Reply(); + drafts.put(loadable, reply); + } + return reply; } - public void quoteInline(int no, String text) { - String textToInsert = ">>" + no + "\n"; - String[] lines = text.split("\n+"); - for (String line : lines) { - textToInsert += ">" + line + "\n"; + public void putReply(Loadable loadable, Reply reply) { + // Remove files from all other replies because there can only be one picked_file at the same time. + // Not doing this would be confusing and cause invalid fileNames. + for (Map.Entry entry : drafts.entrySet()) { + if (!entry.getKey().equals(loadable)) { + Reply value = entry.getValue(); + value.file = null; + value.fileName = ""; + } } - draft.comment = new StringBuilder(draft.comment).insert(draft.cursorPosition, textToInsert).toString(); - draft.cursorPosition += textToInsert.length(); + drafts.put(loadable, reply); } /** @@ -134,24 +113,28 @@ public class ReplyManager { context.startActivity(intent); } - /** - * Called from ImagePickActivity, sends onFileLoading to the fileListener. - */ - public void _onPickedFileLoading() { + public File getPickFile() { + return new File(context.getCacheDir(), "picked_file"); + } + + public void _onFilePickLoading() { if (fileListener != null) { - fileListener.onFileLoading(); + fileListener.onFilePickLoading(); } } - /** - * Called from ImagePickActivity. Sends the file to the listening - * fileListener, and deletes the fileListener. - */ - public void _onPickedFile(String name, File file) { + public void _onFilePicked(String name, File file) { if (fileListener != null) { - fileListener.onFile(name, file); + fileListener.onFilePicked(name, file); + fileListener = null; + } + } + + public void _onFilePickError(boolean cancelled) { + if (fileListener != null) { + fileListener.onFilePickError(cancelled); + fileListener = null; } - fileListener = null; } /** @@ -161,10 +144,12 @@ public class ReplyManager { fileListener = null; } - public static abstract class FileListener { - public abstract void onFile(String name, File file); + public interface FileListener { + void onFilePickLoading(); - public abstract void onFileLoading(); + void onFilePicked(String name, File file); + + void onFilePickError(boolean cancelled); } public void postPass(String token, String pin, final PassListener passListener) { @@ -341,22 +326,22 @@ public class ReplyManager { } private void postReplyInternal(final Reply reply, final ReplyListener replyListener, String captchaHash) { - reply.password = Long.toHexString(random.nextLong()); +// reply.password = Long.toHexString(random.nextLong()); MultipartBuilder formBuilder = new MultipartBuilder(); formBuilder.type(MultipartBuilder.FORM); formBuilder.addFormDataPart("mode", "regist"); - formBuilder.addFormDataPart("pwd", reply.password); +// formBuilder.addFormDataPart("pwd", reply.password); if (reply.resto >= 0) { formBuilder.addFormDataPart("resto", String.valueOf(reply.resto)); } formBuilder.addFormDataPart("name", reply.name); - formBuilder.addFormDataPart("email", reply.email); + formBuilder.addFormDataPart("email", reply.options); - if (!TextUtils.isEmpty(reply.subject)) { + if (reply.resto >= 0 && !TextUtils.isEmpty(reply.subject)) { formBuilder.addFormDataPart("sub", reply.subject); } @@ -444,7 +429,7 @@ public class ReplyManager { SavedReply savedReply = new SavedReply(); savedReply.board = reply.board; savedReply.no = no; - savedReply.password = reply.password; +// savedReply.password = reply.password; ChanApplication.getDatabaseManager().saveReply(savedReply); @@ -457,6 +442,26 @@ public class ReplyManager { return res; } + public void makeHttpCall(HttpCall httpCall, HttpCallback callback) { + //noinspection unchecked + httpCall.setCallback(callback); + + Request.Builder requestBuilder = new Request.Builder(); + + httpCall.setup(requestBuilder); + + requestBuilder.header("User-Agent", ChanApplication.getInstance().getUserAgent()); + Request request = requestBuilder.build(); + + client.newCall(request).enqueue(httpCall); + } + + public interface HttpCallback { + void onHttpSuccess(T httpPost); + + void onHttpFail(T httpPost); + } + private void makeOkHttpCall(Request.Builder requestBuilder, Callback callback) { requestBuilder.header("User-Agent", ChanApplication.getInstance().getUserAgent()); Request request = requestBuilder.build(); diff --git a/Clover/app/src/main/java/org/floens/chan/database/DatabaseManager.java b/Clover/app/src/main/java/org/floens/chan/database/DatabaseManager.java index a9fa808e..266bafc3 100644 --- a/Clover/app/src/main/java/org/floens/chan/database/DatabaseManager.java +++ b/Clover/app/src/main/java/org/floens/chan/database/DatabaseManager.java @@ -45,7 +45,7 @@ public class DatabaseManager { } public void saveReply(SavedReply saved) { - Logger.i(TAG, "Saving " + saved.board + ", " + saved.no); + Logger.d(TAG, "Saving " + saved.board + ", " + saved.no); try { helper.savedDao.create(saved); 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/activity/ImagePickActivity.java index 26a9681a..e99e01f5 100644 --- a/Clover/app/src/main/java/org/floens/chan/ui/activity/ImagePickActivity.java +++ b/Clover/app/src/main/java/org/floens/chan/ui/activity/ImagePickActivity.java @@ -18,108 +18,128 @@ package org.floens.chan.ui.activity; import android.app.Activity; -import android.content.ActivityNotFoundException; import android.content.Intent; import android.database.Cursor; import android.net.Uri; import android.os.Bundle; import android.os.ParcelFileDescriptor; import android.provider.OpenableColumns; -import android.widget.Toast; import org.floens.chan.ChanApplication; -import org.floens.chan.R; -import org.floens.chan.utils.AndroidUtils; +import org.floens.chan.core.reply.ReplyManager; import org.floens.chan.utils.IOUtils; +import org.floens.chan.utils.Logger; import java.io.File; import java.io.FileInputStream; import java.io.FileOutputStream; import java.io.IOException; +import java.io.InputStream; +import java.io.OutputStream; + +public class ImagePickActivity extends Activity implements Runnable { + private static final String TAG = "ImagePickActivity"; -public class ImagePickActivity extends Activity { private static final int IMAGE_RESULT = 1; + private static final long MAX_FILE_SIZE = 15 * 1024 * 1024; + + private ReplyManager replyManager; + private Uri uri; + private String fileName = "file"; + private boolean success = false; + private File cacheFile; @Override public void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); + replyManager = ChanApplication.getReplyManager(); + Intent intent = new Intent(Intent.ACTION_GET_CONTENT); intent.addCategory(Intent.CATEGORY_OPENABLE); intent.setType("*/*"); - try { + if (intent.resolveActivity(getPackageManager()) != null) { startActivityForResult(intent, IMAGE_RESULT); - } catch (ActivityNotFoundException e) { - e.printStackTrace(); - Toast.makeText(this, R.string.file_open_failed, Toast.LENGTH_LONG).show(); - finish(); + } else { + Logger.e(TAG, "No activity found to get file with"); + replyManager._onFilePickError(false); } } @Override public void onActivityResult(int requestCode, int resultCode, Intent data) { - finish(); - - if (requestCode == IMAGE_RESULT && resultCode == Activity.RESULT_OK) { - if (data != null) { - final Uri uri = data.getData(); - - String name = "file"; + boolean ok = false; + boolean cancelled = false; + if (requestCode == IMAGE_RESULT) { + if (resultCode == RESULT_OK && data != null) { + uri = data.getData(); Cursor returnCursor = getContentResolver().query(uri, null, null, null, null); if (returnCursor != null) { int nameIndex = returnCursor.getColumnIndex(OpenableColumns.DISPLAY_NAME); returnCursor.moveToFirst(); if (nameIndex > -1) { - name = returnCursor.getString(nameIndex); + fileName = returnCursor.getString(nameIndex); } returnCursor.close(); } - ChanApplication.getReplyManager()._onPickedFileLoading(); - - final String finalName = name; - new Thread(new Runnable() { - @Override - public void run() { - try { - final File cacheFile = new File(getCacheDir() + File.separator + "picked_file"); - - if (cacheFile.exists()) { - cacheFile.delete(); - } - - ParcelFileDescriptor fileDescriptor = getContentResolver().openFileDescriptor(uri, "r"); - - FileInputStream is = new FileInputStream(fileDescriptor.getFileDescriptor()); - FileOutputStream os = new FileOutputStream(cacheFile); - IOUtils.copy(is, os); - IOUtils.closeQuietly(is); - IOUtils.closeQuietly(os); - - runOnUiThread(new Runnable() { - @Override - public void run() { - ChanApplication.getReplyManager()._onPickedFile(finalName, cacheFile); - } - - }); - } catch (IOException | SecurityException e) { - e.printStackTrace(); - - AndroidUtils.runOnUiThread(new Runnable() { - @Override - public void run() { - ChanApplication.getReplyManager()._onPickedFile("", null); - Toast.makeText(ImagePickActivity.this, R.string.file_open_failed, Toast.LENGTH_LONG).show(); - } - }); - } - } - }).start(); + replyManager._onFilePickLoading(); + + new Thread(this).start(); + ok = true; + } else if (resultCode == RESULT_CANCELED) { + cancelled = true; + } + } + + if (!ok) { + replyManager._onFilePickError(cancelled); + } + + finish(); + } + + @Override + public void run() { + cacheFile = replyManager.getPickFile(); + + ParcelFileDescriptor fileDescriptor = null; + InputStream is = null; + OutputStream os = null; + try { + fileDescriptor = getContentResolver().openFileDescriptor(uri, "r"); + is = new FileInputStream(fileDescriptor.getFileDescriptor()); + os = new FileOutputStream(cacheFile); + boolean fullyCopied = IOUtils.copy(is, os, MAX_FILE_SIZE); + if (fullyCopied) { + success = true; } + } catch (IOException | SecurityException e) { + Logger.e(TAG, "Error copying file from the file descriptor", e); + } finally { + IOUtils.closeQuietly(fileDescriptor); + IOUtils.closeQuietly(is); + IOUtils.closeQuietly(os); } + + if (!success) { + if (!cacheFile.delete()) { + Logger.e(TAG, "Could not delete picked_file after copy fail"); + } + } + + runOnUiThread(new Runnable() { + @Override + public void run() { + if (success) { + replyManager._onFilePicked(fileName, cacheFile); + } else { + replyManager._onFilePickError(false); + } + } + }); } } diff --git a/Clover/app/src/main/java/org/floens/chan/ui/activity/PassSettingsActivity.java b/Clover/app/src/main/java/org/floens/chan/ui/activity/PassSettingsActivity.java index e8bd1c56..a1770d0b 100644 --- a/Clover/app/src/main/java/org/floens/chan/ui/activity/PassSettingsActivity.java +++ b/Clover/app/src/main/java/org/floens/chan/ui/activity/PassSettingsActivity.java @@ -37,8 +37,8 @@ import android.widget.TextView; import org.floens.chan.ChanApplication; import org.floens.chan.R; -import org.floens.chan.core.manager.ReplyManager; -import org.floens.chan.core.manager.ReplyManager.PassResponse; +import org.floens.chan.core.reply.ReplyManager; +import org.floens.chan.core.reply.ReplyManager.PassResponse; import org.floens.chan.core.settings.ChanSettings; import org.floens.chan.ui.ThemeActivity; import org.floens.chan.utils.AndroidUtils; diff --git a/Clover/app/src/main/java/org/floens/chan/ui/adapter/PostAdapter.java b/Clover/app/src/main/java/org/floens/chan/ui/adapter/PostAdapter.java index ec9fa782..89d7aff8 100644 --- a/Clover/app/src/main/java/org/floens/chan/ui/adapter/PostAdapter.java +++ b/Clover/app/src/main/java/org/floens/chan/ui/adapter/PostAdapter.java @@ -27,7 +27,6 @@ import org.floens.chan.core.model.Loadable; import org.floens.chan.core.model.Post; import org.floens.chan.ui.cell.PostCell; import org.floens.chan.ui.cell.ThreadStatusCell; -import org.floens.chan.ui.view.PostView; import java.util.ArrayList; import java.util.List; @@ -47,6 +46,7 @@ public class PostAdapter extends RecyclerView.Adapter { private String error = null; private Post highlightedPost; private String highlightedPostId; + private int highlightedPostNo = -1; private boolean filtering = false; public PostAdapter(RecyclerView recyclerView, PostAdapterCallback postAdapterCallback, PostCell.PostCellCallback postCellCallback, ThreadStatusCell.Callback statusCellCallback) { @@ -76,7 +76,7 @@ public class PostAdapter extends RecyclerView.Adapter { if (getItemViewType(position) == TYPE_POST) { PostViewHolder postViewHolder = (PostViewHolder) holder; Post post = displayList.get(position); - boolean highlight = post == highlightedPost || post.id.equals(highlightedPostId); + boolean highlight = post == highlightedPost || post.id.equals(highlightedPostId) || post.no == highlightedPostNo; postViewHolder.postView.setPost(post, postCellCallback, highlight, -1); } else if (getItemViewType(position) == TYPE_STATUS) { ((StatusViewHolder) holder).threadStatusCell.update(); @@ -131,6 +131,8 @@ public class PostAdapter extends RecyclerView.Adapter { public void cleanup() { highlightedPost = null; + highlightedPostId = null; + highlightedPostNo = -1; filtering = false; sourceList.clear(); displayList.clear(); @@ -178,14 +180,23 @@ public class PostAdapter extends RecyclerView.Adapter { } public void highlightPost(Post post) { - highlightedPostId = null; highlightedPost = post; + highlightedPostId = null; + highlightedPostNo = -1; notifyDataSetChanged(); } public void highlightPostId(String id) { highlightedPost = null; highlightedPostId = id; + highlightedPostNo = -1; + notifyDataSetChanged(); + } + + public void highlightPostNo(int no) { + highlightedPost = null; + highlightedPostId = null; + highlightedPostNo = no; notifyDataSetChanged(); } diff --git a/Clover/app/src/main/java/org/floens/chan/ui/animation/HeightAnimation.java b/Clover/app/src/main/java/org/floens/chan/ui/animation/HeightAnimation.java deleted file mode 100644 index 4dba820c..00000000 --- a/Clover/app/src/main/java/org/floens/chan/ui/animation/HeightAnimation.java +++ /dev/null @@ -1,29 +0,0 @@ -package org.floens.chan.ui.animation; - -import android.view.View; -import android.view.animation.Animation; -import android.view.animation.Transformation; - -public class HeightAnimation extends Animation { - final int startHeight; - final int targetHeight; - View view; - - public HeightAnimation(View view, int startHeight, int targetHeight, int duration) { - this.view = view; - this.startHeight = startHeight; - this.targetHeight = targetHeight; - setDuration(duration); - } - - @Override - protected void applyTransformation(float interpolatedTime, Transformation t) { - view.getLayoutParams().height = (int) (startHeight + (targetHeight - startHeight) * interpolatedTime); - view.requestLayout(); - } - - @Override - public boolean willChangeBounds() { - return true; - } -} diff --git a/Clover/app/src/main/java/org/floens/chan/ui/controller/BrowseController.java b/Clover/app/src/main/java/org/floens/chan/ui/controller/BrowseController.java index bd962ed8..1fa42095 100644 --- a/Clover/app/src/main/java/org/floens/chan/ui/controller/BrowseController.java +++ b/Clover/app/src/main/java/org/floens/chan/ui/controller/BrowseController.java @@ -90,7 +90,7 @@ public class BrowseController extends ThreadController implements ToolbarMenuIte threadLayout.getPresenter().requestData(); break; case POST_ID: - // TODO + openPost(true); break; } } diff --git a/Clover/app/src/main/java/org/floens/chan/ui/controller/PassSettingsController.java b/Clover/app/src/main/java/org/floens/chan/ui/controller/PassSettingsController.java index 1f5e087e..d0432671 100644 --- a/Clover/app/src/main/java/org/floens/chan/ui/controller/PassSettingsController.java +++ b/Clover/app/src/main/java/org/floens/chan/ui/controller/PassSettingsController.java @@ -15,7 +15,7 @@ import android.widget.TextView; import org.floens.chan.ChanApplication; import org.floens.chan.R; import org.floens.chan.controller.Controller; -import org.floens.chan.core.manager.ReplyManager; +import org.floens.chan.core.reply.ReplyManager; import org.floens.chan.core.settings.ChanSettings; import org.floens.chan.ui.view.CrossfadeView; import org.floens.chan.utils.AndroidUtils; 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 f7fb58c1..1813a228 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 @@ -40,10 +40,19 @@ public abstract class ThreadController extends Controller implements ThreadLayou EventBus.getDefault().unregister(this); } + @Override + public boolean onBack() { + return threadLayout.onBack(); + } + public void onEvent(ChanApplication.ForegroundChangedMessage message) { threadLayout.getPresenter().onForegroundChanged(message.inForeground); } + public void openPost(boolean open) { + threadLayout.openPost(open); + } + public void presentRepliesController(Controller controller) { presentController(controller); } diff --git a/Clover/app/src/main/java/org/floens/chan/ui/controller/ViewThreadController.java b/Clover/app/src/main/java/org/floens/chan/ui/controller/ViewThreadController.java index cafcc3df..32ba1bcc 100644 --- a/Clover/app/src/main/java/org/floens/chan/ui/controller/ViewThreadController.java +++ b/Clover/app/src/main/java/org/floens/chan/ui/controller/ViewThreadController.java @@ -154,7 +154,7 @@ public class ViewThreadController extends ThreadController implements ThreadLayo setPinIconState(threadLayout.getPresenter().pin()); break; case POST_ID: - // TODO + openPost(true); break; } } diff --git a/Clover/app/src/main/java/org/floens/chan/ui/drawable/DropdownArrowDrawable.java b/Clover/app/src/main/java/org/floens/chan/ui/drawable/DropdownArrowDrawable.java index a93161c3..37e05f8b 100644 --- a/Clover/app/src/main/java/org/floens/chan/ui/drawable/DropdownArrowDrawable.java +++ b/Clover/app/src/main/java/org/floens/chan/ui/drawable/DropdownArrowDrawable.java @@ -7,31 +7,44 @@ import android.graphics.Path; import android.graphics.PixelFormat; import android.graphics.drawable.Drawable; -import static org.floens.chan.utils.AndroidUtils.dp; - public class DropdownArrowDrawable extends Drawable { private Paint paint = new Paint(); private Path path = new Path(); private int width; private int height; + private float rotation; + private int color; + private int pressedColor; - public DropdownArrowDrawable() { - width = dp(12); - height = dp(6); + public DropdownArrowDrawable(int width, int height, boolean down, int color, int pressedColor) { + this.width = width; + this.height = height; + rotation = down ? 0f : 1f; + this.color = color; + this.pressedColor = pressedColor; paint.setStyle(Paint.Style.FILL); - paint.setColor(0xffffffff); - - path.moveTo(0, 0); - path.lineTo(width, 0); - path.lineTo(width / 2, height); - path.lineTo(0, 0); - path.close(); + paint.setColor(color); } @Override public void draw(Canvas canvas) { + path.rewind(); + path.moveTo(0, height / 2); + path.lineTo(width, height / 2); + path.lineTo(width / 2, height); + path.lineTo(0, height / 2); + path.close(); + + canvas.save(); + canvas.rotate(rotation * 180f, width / 2f, height / 2f); canvas.drawPath(path, paint); + canvas.restore(); + } + + public void setRotation(float rotation) { + this.rotation = rotation; + invalidateSelf(); } @Override @@ -52,7 +65,7 @@ public class DropdownArrowDrawable extends Drawable { pressed = true; } } - int color = pressed ? 0x88ffffff : 0xffffffff; + int color = pressed ? pressedColor : this.color; if (color != paint.getColor()) { paint.setColor(color); invalidateSelf(); diff --git a/Clover/app/src/main/java/org/floens/chan/ui/fragment/ReplyFragment.java b/Clover/app/src/main/java/org/floens/chan/ui/fragment/ReplyFragment.java index 6b1d3f9d..2f1c0368 100644 --- a/Clover/app/src/main/java/org/floens/chan/ui/fragment/ReplyFragment.java +++ b/Clover/app/src/main/java/org/floens/chan/ui/fragment/ReplyFragment.java @@ -46,7 +46,7 @@ import android.widget.ViewFlipper; import org.floens.chan.ChanApplication; import org.floens.chan.R; import org.floens.chan.chan.ChanUrls; -import org.floens.chan.core.manager.ReplyManager; +import org.floens.chan.core.reply.ReplyManager; import org.floens.chan.core.model.Board; import org.floens.chan.core.model.Loadable; import org.floens.chan.core.model.Reply; @@ -132,7 +132,7 @@ public class ReplyFragment extends DialogFragment implements CaptchaLayout.Captc setClosable(true); Dialog dialog = getDialog(); - String title = (loadable.isThreadMode() ? context.getString(R.string.reply) : context.getString(R.string.reply_to_board)) + " " + loadable.title; + String title = (loadable.isThreadMode() ? context.getString(R.string.reply_to_board) : context.getString(R.string.reply_to_board)) + " " + loadable.title; if (dialog == null) { context.getSupportActionBar().setTitle(title); @@ -152,17 +152,17 @@ public class ReplyFragment extends DialogFragment implements CaptchaLayout.Captc }); } - Reply draft = ChanApplication.getReplyManager().getReplyDraft(); + Reply draft = null; if (TextUtils.isEmpty(draft.name)) { draft.name = ChanSettings.getDefaultName(); } nameView.setText(draft.name); - emailView.setText(draft.email); + emailView.setText(draft.options); subjectView.setText(draft.subject); commentView.setText(draft.comment); - commentView.setSelection(draft.cursorPosition); + commentView.setSelection(draft.selection); setFile(draft.fileName, draft.file); spoilerImageView.setChecked(draft.spoilerImage); @@ -219,16 +219,16 @@ public class ReplyFragment extends DialogFragment implements CaptchaLayout.Captc if (shouldSaveDraft) { draft.name = nameView.getText().toString(); - draft.email = emailView.getText().toString(); + draft.options = emailView.getText().toString(); draft.subject = subjectView.getText().toString(); draft.comment = commentView.getText().toString(); draft.fileName = fileNameView.getText().toString(); draft.spoilerImage = spoilerImageView.isChecked(); - draft.cursorPosition = commentView.getSelectionStart(); + draft.selection = commentView.getSelectionStart(); - replyManager.setReplyDraft(draft); +// replyManager.setReplyDraft(draft); } else { - replyManager.removeReplyDraft(); +// replyManager.removeReplyDraft(); setFile(null, null); } } @@ -278,7 +278,7 @@ public class ReplyFragment extends DialogFragment implements CaptchaLayout.Captc @Override public void onClick(View view) { if (draft.file == null) { - ChanApplication.getReplyManager().pickFile(new ReplyManager.FileListener() { + /*ChanApplication.getReplyManager().pickFile(new ReplyManager.FileListener() { @Override public void onFile(String name, File file) { setFile(name, file); @@ -289,7 +289,7 @@ public class ReplyFragment extends DialogFragment implements CaptchaLayout.Captc imageViewContainer.setVisibility(View.VISIBLE); imageViewContainer.setView(null); } - }); + });*/ } else { setFile(null, null); } @@ -531,7 +531,7 @@ public class ReplyFragment extends DialogFragment implements CaptchaLayout.Captc responseContainer.setView(null); draft.name = nameView.getText().toString(); - draft.email = emailView.getText().toString(); + draft.options = emailView.getText().toString(); draft.subject = subjectView.getText().toString(); draft.comment = commentView.getText().toString(); draft.captchaResponse = captchaResponse; @@ -571,8 +571,7 @@ public class ReplyFragment extends DialogFragment implements CaptchaLayout.Captc return; if (response.isNetworkError || response.isUserError) { - int resId = response.isCaptchaError ? R.string.reply_error_captcha - : (response.isFileError ? R.string.reply_error_file : R.string.reply_error); + int resId = R.string.reply_error; Toast.makeText(context, resId, Toast.LENGTH_LONG).show(); submitButton.setEnabled(true); cancelButton.setEnabled(true); diff --git a/Clover/app/src/main/java/org/floens/chan/ui/fragment/ThreadFragment.java b/Clover/app/src/main/java/org/floens/chan/ui/fragment/ThreadFragment.java index 0d704b3e..810eab92 100644 --- a/Clover/app/src/main/java/org/floens/chan/ui/fragment/ThreadFragment.java +++ b/Clover/app/src/main/java/org/floens/chan/ui/fragment/ThreadFragment.java @@ -65,7 +65,7 @@ import java.util.List; import javax.net.ssl.SSLException; import static org.floens.chan.utils.AndroidUtils.dp; -import static org.floens.chan.utils.AndroidUtils.setPressedDrawable; +import static org.floens.chan.utils.AndroidUtils.setItemBackground; public class ThreadFragment extends Fragment implements ThreadManager.ThreadManagerListener, PostAdapter.PostAdapterCallback { private ThreadManager threadManager; @@ -602,7 +602,7 @@ public class ThreadFragment extends Fragment implements ThreadManager.ThreadMana searchViewParams.width = dp(48); searchViewParams.height = LayoutParams.MATCH_PARENT; closeButton.setLayoutParams(closeButtonParams); - setPressedDrawable(closeButton); + setItemBackground(closeButton); int padding = dp(8); closeButton.setPadding(padding, padding, padding, padding); @@ -639,9 +639,9 @@ public class ThreadFragment extends Fragment implements ThreadManager.ThreadMana if (all) { textView.setText(""); } else { - String posts = getContext().getString(count == 1 ? R.string.one_post : R.string.multiple_posts); - String text = getContext().getString(R.string.search_results, Integer.toString(count), posts, filter); - textView.setText(text); +// String posts = getContext().getString(count == 1 ? R.string.one_post : R.string.multiple_posts); +// String text = getContext().getString(R.string.search_results, Integer.toString(count), posts, filter); +// textView.setText(text); } } } diff --git a/Clover/app/src/main/java/org/floens/chan/ui/layout/CaptchaLayout.java b/Clover/app/src/main/java/org/floens/chan/ui/layout/CaptchaLayout.java index a681ca13..7e12afc7 100644 --- a/Clover/app/src/main/java/org/floens/chan/ui/layout/CaptchaLayout.java +++ b/Clover/app/src/main/java/org/floens/chan/ui/layout/CaptchaLayout.java @@ -19,6 +19,7 @@ package org.floens.chan.ui.layout; import android.annotation.SuppressLint; import android.content.Context; +import android.support.annotation.NonNull; import android.text.TextUtils; import android.util.AttributeSet; import android.util.Log; @@ -52,7 +53,7 @@ public class CaptchaLayout extends WebView { super(context, attrs, defStyle); } - @SuppressLint("SetJavaScriptEnabled") + @SuppressLint({"SetJavaScriptEnabled", "AddJavascriptInterface"}) public void initCaptcha(String baseUrl, String siteKey, boolean lightTheme, String userAgent, CaptchaCallback callback) { this.callback = callback; this.baseUrl = baseUrl; @@ -65,7 +66,7 @@ public class CaptchaLayout extends WebView { setWebChromeClient(new WebChromeClient() { @Override - public boolean onConsoleMessage(ConsoleMessage consoleMessage) { + public boolean onConsoleMessage(@NonNull ConsoleMessage consoleMessage) { Log.i(TAG, consoleMessage.lineNumber() + ":" + consoleMessage.message() + " " + consoleMessage.sourceId()); return true; } 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 new file mode 100644 index 00000000..d7b7e388 --- /dev/null +++ b/Clover/app/src/main/java/org/floens/chan/ui/layout/ReplyLayout.java @@ -0,0 +1,369 @@ +package org.floens.chan.ui.layout; + +import android.content.Context; +import android.graphics.Bitmap; +import android.os.Build; +import android.text.Editable; +import android.text.TextWatcher; +import android.util.AttributeSet; +import android.view.LayoutInflater; +import android.view.View; +import android.view.ViewGroup; +import android.widget.CheckBox; +import android.widget.EditText; +import android.widget.ImageView; +import android.widget.LinearLayout; +import android.widget.TextView; +import android.widget.Toast; + +import org.floens.chan.R; +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.ui.drawable.DropdownArrowDrawable; +import org.floens.chan.ui.view.LoadView; +import org.floens.chan.ui.view.SelectionListeningEditText; +import org.floens.chan.utils.AnimationUtils; +import org.floens.chan.utils.ImageDecoder; +import org.floens.chan.utils.ThemeHelper; + +import java.io.File; + +import static org.floens.chan.utils.AndroidUtils.dp; +import static org.floens.chan.utils.AndroidUtils.getAttrColor; +import static org.floens.chan.utils.AndroidUtils.getString; +import static org.floens.chan.utils.AndroidUtils.setRoundItemBackground; + +public class ReplyLayout extends LoadView implements View.OnClickListener, AnimationUtils.LayoutAnimationProgress, ReplyPresenter.ReplyPresenterCallback, TextWatcher, ImageDecoder.ImageDecoderCallback, SelectionListeningEditText.SelectionChangedListener { + private ReplyPresenter presenter; + private ReplyLayoutCallback callback; + + private View replyInputLayout; + private CaptchaLayout captchaLayout; + + private boolean openingName; + private boolean blockSelectionChange = false; + private TextView message; + private EditText name; + private EditText subject; + private EditText options; + private EditText fileName; + private SelectionListeningEditText comment; + private TextView commentCounter; + private LinearLayout previewContainer; + private CheckBox spoiler; + private ImageView preview; + private TextView previewMessage; + private ImageView more; + private DropdownArrowDrawable moreDropdown; + private ImageView attach; + private ImageView submit; + + private Runnable closeMessageRunnable = new Runnable() { + @Override + public void run() { + AnimationUtils.animateHeight(message, false, getWidth()); + } + }; + + public ReplyLayout(Context context) { + super(context); + } + + public ReplyLayout(Context context, AttributeSet attrs) { + super(context, attrs); + } + + public ReplyLayout(Context context, AttributeSet attrs, int defStyleAttr) { + super(context, attrs, defStyleAttr); + } + + @Override + protected void onFinishInflate() { + super.onFinishInflate(); + + setAnimateLayout(true, true); + + presenter = new ReplyPresenter(this); + + replyInputLayout = LayoutInflater.from(getContext()).inflate(R.layout.layout_reply_input, this, false); + message = (TextView) replyInputLayout.findViewById(R.id.message); + name = (EditText) replyInputLayout.findViewById(R.id.name); + subject = (EditText) replyInputLayout.findViewById(R.id.subject); + options = (EditText) replyInputLayout.findViewById(R.id.options); + fileName = (EditText) replyInputLayout.findViewById(R.id.file_name); + comment = (SelectionListeningEditText) replyInputLayout.findViewById(R.id.comment); + comment.addTextChangedListener(this); + comment.setSelectionChangedListener(this); + commentCounter = (TextView) replyInputLayout.findViewById(R.id.comment_counter); + previewContainer = (LinearLayout) replyInputLayout.findViewById(R.id.preview_container); + spoiler = (CheckBox) replyInputLayout.findViewById(R.id.spoiler); + preview = (ImageView) replyInputLayout.findViewById(R.id.preview); + previewMessage = (TextView) replyInputLayout.findViewById(R.id.preview_message); + preview.setOnClickListener(this); + more = (ImageView) replyInputLayout.findViewById(R.id.more); + moreDropdown = new DropdownArrowDrawable(dp(16), dp(16), true, getAttrColor(getContext(), R.attr.dropdown_dark_color), getAttrColor(getContext(), R.attr.dropdown_dark_pressed_color)); + more.setImageDrawable(moreDropdown); + setRoundItemBackground(more); + more.setOnClickListener(this); + attach = (ImageView) replyInputLayout.findViewById(R.id.attach); + setRoundItemBackground(attach); + attach.setOnClickListener(this); + submit = (ImageView) replyInputLayout.findViewById(R.id.submit); + setRoundItemBackground(submit); + submit.setOnClickListener(this); + + setView(replyInputLayout); + + setBackgroundColor(0xffffffff); + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) { + setElevation(dp(4f)); + } + } + + public void setCallback(ReplyLayoutCallback callback) { + this.callback = callback; + } + + public ReplyPresenter getPresenter() { + return presenter; + } + + public void bindLoadable(Loadable loadable) { + presenter.bindLoadable(loadable); + } + + public void cleanup() { + presenter.unbindLoadable(); + removeCallbacks(closeMessageRunnable); + } + + @Override + public LayoutParams getLayoutParamsForView(View view) { + if (view == replyInputLayout) { + return new LayoutParams(LayoutParams.MATCH_PARENT, LayoutParams.WRAP_CONTENT); + } else { + // Captcha and the loadbar + return new LayoutParams(LayoutParams.MATCH_PARENT, dp(200)); + } + } + + @Override + public void onLayoutAnimationProgress(boolean vertical, View view, float progress) { + if (view == name) { + moreDropdown.setRotation(openingName ? progress : 1f - progress); + } + } + + public boolean onBack() { + return presenter.onBack(); + } + + @Override + public void onClick(View v) { + if (v == more) { + presenter.onMoreClicked(); + } else if (v == attach) { + presenter.onAttachClicked(); + } else if (v == submit) { + presenter.onSubmitClicked(); + }/* else if (v == preview) { + // TODO + }*/ + } + + @Override + public void setPage(ReplyPresenter.Page page, boolean animate) { + setAnimateLayout(animate, true); + switch (page) { + case LOADING: + setView(null); + break; + case INPUT: + setView(replyInputLayout); + break; + case CAPTCHA: + if (captchaLayout == null) { + captchaLayout = new CaptchaLayout(getContext()); + } + + setView(captchaLayout); + break; + } + } + + @Override + public void initCaptcha(String baseUrl, String siteKey, String userAgent, CaptchaLayout.CaptchaCallback callback) { + captchaLayout.initCaptcha(baseUrl, siteKey, ThemeHelper.getInstance().getTheme().isLightTheme, userAgent, callback); + captchaLayout.load(); + } + + @Override + public void resetCaptcha() { + captchaLayout.reset(); + } + + @Override + public void loadDraftIntoViews(Reply draft) { + name.setText(draft.name); + subject.setText(draft.subject); + options.setText(draft.options); + blockSelectionChange = true; + comment.setText(draft.comment); + comment.setSelection(draft.selection); + blockSelectionChange = false; + fileName.setText(draft.fileName); + spoiler.setChecked(draft.spoilerImage); + } + + @Override + public void loadViewsIntoDraft(Reply draft) { + draft.name = name.getText().toString(); + draft.subject = subject.getText().toString(); + draft.options = options.getText().toString(); + draft.comment = comment.getText().toString(); + draft.selection = comment.getSelectionStart(); + draft.fileName = fileName.getText().toString(); + draft.spoilerImage = spoiler.isChecked(); + } + + @Override + public void openMessage(boolean open, boolean animate, String text, boolean autoHide) { + removeCallbacks(closeMessageRunnable); + message.setText(text); + + if (animate) { + AnimationUtils.animateHeight(message, open, getWidth()); + } else { + message.setVisibility(open ? VISIBLE : GONE); + message.getLayoutParams().height = open ? ViewGroup.LayoutParams.WRAP_CONTENT : 0; + message.requestLayout(); + } + + if (autoHide) { + postDelayed(closeMessageRunnable, 5000); + } + } + + @Override + public void onPosted() { + Toast.makeText(getContext(), R.string.reply_success, Toast.LENGTH_SHORT).show(); + callback.openReply(false); + } + + @Override + public void setCommentHint(String hint) { + comment.setHint(hint); + } + + @Override + public void openName(boolean open) { + openingName = open; + AnimationUtils.animateHeight(name, open, comment.getWidth(), 300, this); + } + + @Override + public void openSubject(boolean open) { + AnimationUtils.animateHeight(subject, open, comment.getWidth()); + } + + @Override + public void openOptions(boolean open) { + AnimationUtils.animateHeight(options, open, comment.getWidth()); + } + + @Override + public void openFileName(boolean open) { + AnimationUtils.animateHeight(fileName, open, comment.getWidth()); + } + + @Override + public void setFileName(String name) { + fileName.setText(name); + } + + @Override + public void updateCommentCount(int count, int maxCount, boolean over) { + commentCounter.setText(count + "/" + maxCount); + commentCounter.setTextColor(over ? 0xffff0000 : getAttrColor(getContext(), R.attr.text_color_secondary)); + } + + @Override + public void openPreview(boolean show, File previewFile) { + attach.setImageResource(show ? R.drawable.ic_close_grey600_24dp : R.drawable.ic_image_grey600_24dp); + if (show) { + ImageDecoder.decodeFileOnBackgroundThread(previewFile, dp(100), dp(100), this); + } else { + AnimationUtils.animateLayout(false, previewContainer, previewContainer.getWidth(), 0, 300, false, null); + } + } + + @Override + public void openPreviewMessage(boolean show, String message) { + previewMessage.setVisibility(show ? VISIBLE : GONE); + previewMessage.setText(message); + } + + @Override + public void openSpoiler(boolean show, boolean checked) { + AnimationUtils.animateHeight(spoiler, show); + spoiler.setChecked(checked); + } + + @Override + public void onImageBitmap(File file, Bitmap bitmap) { + if (bitmap != null) { + preview.setImageBitmap(bitmap); + AnimationUtils.animateLayout(false, previewContainer, 0, dp(100), 300, false, null); + } else { + openPreviewMessage(true, getString(R.string.reply_no_preview)); + } + } + + @Override + public void onFilePickLoading() { + } + + @Override + public void onFilePickError() { + Toast.makeText(getContext(), R.string.file_open_failed, Toast.LENGTH_LONG).show(); + } + + @Override + public void highlightPostNo(int no) { + callback.highlightPostNo(no); + } + + @Override + public void onSelectionChanged(int selStart, int selEnd) { + if (!blockSelectionChange) { + presenter.onSelectionChanged(); + } + } + + @Override + public void beforeTextChanged(CharSequence s, int start, int count, int after) { + } + + @Override + public void onTextChanged(CharSequence s, int start, int before, int count) { + } + + @Override + public void afterTextChanged(Editable s) { + presenter.onCommentTextChanged(comment.length()); + } + + @Override + public void showThread(Loadable loadable) { + callback.showThread(loadable); + } + + public interface ReplyLayoutCallback { + void highlightPostNo(int no); + + void openReply(boolean open); + + void showThread(Loadable loadable); + } +} 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 1531e22e..3b6d6a49 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 @@ -61,7 +61,7 @@ public class ThreadLayout extends LoadView implements ThreadPresenter.ThreadPres private enum Visible { LOADING, THREAD, - ERROR; + ERROR } private ThreadLayoutCallback callback; @@ -94,7 +94,7 @@ public class ThreadLayout extends LoadView implements ThreadPresenter.ThreadPres presenter = new ThreadPresenter(this); threadListLayout = (ThreadListLayout) LayoutInflater.from(getContext()).inflate(R.layout.layout_thread_list, this, false); - threadListLayout.setCallbacks(presenter, presenter, presenter); + threadListLayout.setCallbacks(presenter, presenter, presenter, presenter); postPopupHelper = new PostPopupHelper(getContext(), presenter, this); @@ -115,6 +115,10 @@ public class ThreadLayout extends LoadView implements ThreadPresenter.ThreadPres } } + public boolean onBack() { + return threadListLayout.onBack(); + } + public void setCallback(ThreadLayoutCallback callback) { this.callback = callback; } @@ -123,6 +127,10 @@ public class ThreadLayout extends LoadView implements ThreadPresenter.ThreadPres return presenter; } + public void openPost(boolean open) { + threadListLayout.openReply(open); + } + @Override public void showPosts(ChanThread thread) { threadListLayout.showPosts(thread, visible != Visible.THREAD); @@ -256,6 +264,12 @@ public class ThreadLayout extends LoadView implements ThreadPresenter.ThreadPres threadListLayout.filterList(query, filter, clearFilter, setEmptyText, hideKeyboard); } + @Override + public void quote(Post post, boolean withText) { + openPost(true); + threadListLayout.getReplyPresenter().quote(post, withText); + } + public ThumbnailView getThumbnail(PostImage postImage) { if (postPopupHelper.isOpen()) { return postPopupHelper.getThumbnail(postImage); 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 beef03e6..84789a1c 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 @@ -18,33 +18,25 @@ package org.floens.chan.ui.layout; import android.content.Context; -import android.os.Parcelable; import android.support.v7.widget.LinearLayoutManager; import android.support.v7.widget.RecyclerView; -import android.text.SpannedString; import android.util.AttributeSet; import android.view.View; import android.widget.LinearLayout; import android.widget.TextView; -import com.squareup.leakcanary.RefWatcher; - -import org.floens.chan.ChanApplication; -import org.floens.chan.ChanBuild; import org.floens.chan.R; import org.floens.chan.core.model.ChanThread; -import org.floens.chan.core.model.Pin; +import org.floens.chan.core.model.Loadable; import org.floens.chan.core.model.Post; import org.floens.chan.core.model.PostImage; -import org.floens.chan.core.model.PostLinkable; +import org.floens.chan.core.presenter.ReplyPresenter; import org.floens.chan.ui.adapter.PostAdapter; import org.floens.chan.ui.cell.PostCell; import org.floens.chan.ui.cell.ThreadStatusCell; -import org.floens.chan.ui.view.PostView; import org.floens.chan.ui.view.ThumbnailView; import org.floens.chan.utils.AndroidUtils; import org.floens.chan.utils.AnimationUtils; -import org.floens.chan.utils.Logger; import java.util.List; @@ -53,14 +45,15 @@ import static org.floens.chan.utils.AndroidUtils.ROBOTO_MEDIUM; /** * A layout that wraps around a {@link RecyclerView} to manage showing posts. */ -public class ThreadListLayout extends LinearLayout { +public class ThreadListLayout extends LinearLayout implements ReplyLayout.ReplyLayoutCallback { + private ReplyLayout reply; private TextView searchStatus; private RecyclerView recyclerView; private LinearLayoutManager linearLayoutManager; private PostAdapter postAdapter; - private PostAdapter.PostAdapterCallback postAdapterCallback; - private PostView.PostViewCallback postViewCallback; private ChanThread showingThread; + private ThreadListLayoutCallback callback; + private boolean replyOpen; public ThreadListLayout(Context context, AttributeSet attrs) { super(context, attrs); @@ -70,6 +63,9 @@ public class ThreadListLayout extends LinearLayout { protected void onFinishInflate() { super.onFinishInflate(); + reply = (ReplyLayout) findViewById(R.id.reply); + reply.setCallback(this); + searchStatus = (TextView) findViewById(R.id.search_status); searchStatus.setTypeface(ROBOTO_MEDIUM); @@ -78,9 +74,8 @@ public class ThreadListLayout extends LinearLayout { recyclerView.setLayoutManager(linearLayoutManager); } - public void setCallbacks(PostAdapter.PostAdapterCallback postAdapterCallback, PostCell.PostCellCallback postCellCallback, ThreadStatusCell.Callback statusCellCallback) { - this.postAdapterCallback = postAdapterCallback; - this.postViewCallback = postViewCallback; + public void setCallbacks(PostAdapter.PostAdapterCallback postAdapterCallback, PostCell.PostCellCallback postCellCallback, ThreadStatusCell.Callback statusCellCallback, ThreadListLayoutCallback callback) { + this.callback = callback; postAdapter = new PostAdapter(recyclerView, postAdapterCallback, postCellCallback, statusCellCallback); recyclerView.setAdapter(postAdapter); recyclerView.addOnScrollListener(new RecyclerView.OnScrollListener() { @@ -94,9 +89,32 @@ public class ThreadListLayout extends LinearLayout { }); } + public boolean onBack() { + if (reply.onBack()) { + return true; + } else if (replyOpen) { + openReply(false); + return true; + } else { + return false; + } + } + + public void openReply(boolean open) { + if (showingThread != null && replyOpen != open) { + this.replyOpen = open; + AnimationUtils.animateHeight(reply, replyOpen, getWidth(), 500, reply); + } + } + + public ReplyPresenter getReplyPresenter() { + return reply.getPresenter(); + } + public void showPosts(ChanThread thread, boolean initial) { showingThread = thread; if (initial) { + reply.bindLoadable(showingThread.loadable); linearLayoutManager.scrollToPositionWithOffset(thread.loadable.listViewIndex, 0); } postAdapter.setThread(thread); @@ -138,7 +156,7 @@ public class ThreadListLayout extends LinearLayout { } public void cleanup() { - if (ChanBuild.DEVELOPER_MODE) { + /*if (ChanBuild.DEVELOPER_MODE) { Pin pin = ChanApplication.getWatchManager().findPinByLoadable(showingThread.loadable); if (pin == null) { for (Post post : showingThread.posts) { @@ -146,14 +164,16 @@ public class ThreadListLayout extends LinearLayout { SpannedString commentSpannable = (SpannedString) post.comment; PostLinkable[] linkables = commentSpannable.getSpans(0, commentSpannable.length(), PostLinkable.class); for (PostLinkable linkable : linkables) { -// ChanApplication.getRefWatcher().watch(linkable, linkable.key + " " + linkable.value); + ChanApplication.getRefWatcher().watch(linkable, linkable.key + " " + linkable.value); } } } } - } + }*/ postAdapter.cleanup(); + reply.cleanup(); + openReply(false); showingThread = null; } @@ -190,4 +210,18 @@ public class ThreadListLayout extends LinearLayout { public void highlightPostId(String id) { postAdapter.highlightPostId(id); } + + @Override + public void highlightPostNo(int no) { + postAdapter.highlightPostNo(no); + } + + @Override + public void showThread(Loadable loadable) { + callback.showThread(loadable); + } + + public interface ThreadListLayoutCallback { + void showThread(Loadable loadable); + } } 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 cb656255..66780594 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 @@ -53,6 +53,7 @@ import java.util.ArrayList; import java.util.List; import static org.floens.chan.utils.AndroidUtils.dp; +import static org.floens.chan.utils.AndroidUtils.getAttrColor; import static org.floens.chan.utils.AndroidUtils.getAttrDrawable; public class Toolbar extends LinearLayout implements View.OnClickListener, LoadView.Listener { @@ -364,7 +365,7 @@ public class Toolbar extends LinearLayout implements View.OnClickListener, LoadV if (item.middleMenu != null) { item.middleMenu.setAnchor(titleView, Gravity.LEFT, dp(5), dp(5)); - Drawable drawable = new DropdownArrowDrawable(); + Drawable drawable = new DropdownArrowDrawable(dp(12), dp(12), true, getAttrColor(getContext(), R.attr.dropdown_light_color), getAttrColor(getContext(), R.attr.dropdown_light_pressed_color)); titleView.setCompoundDrawablesWithIntrinsicBounds(null, null, drawable, null); titleView.setOnClickListener(new OnClickListener() { diff --git a/Clover/app/src/main/java/org/floens/chan/ui/view/LoadView.java b/Clover/app/src/main/java/org/floens/chan/ui/view/LoadView.java index bfc7fa62..0dfbb9ec 100644 --- a/Clover/app/src/main/java/org/floens/chan/ui/view/LoadView.java +++ b/Clover/app/src/main/java/org/floens/chan/ui/view/LoadView.java @@ -28,12 +28,17 @@ import android.view.animation.LinearInterpolator; import android.widget.FrameLayout; import android.widget.ProgressBar; +import org.floens.chan.utils.AnimationUtils; + /** * Container for a view with an ProgressBar. Toggles between the view and a * ProgressBar. */ public class LoadView extends FrameLayout { private int fadeDuration = 200; + private boolean animateLayout; + private boolean animateVertical; + private int layoutAnimationDuration = 500; private Listener listener; public LoadView(Context context) { @@ -50,14 +55,25 @@ public class LoadView extends FrameLayout { /** * Set the duration of the fades in ms + * * @param fadeDuration the duration of the fade animation in ms */ public void setFadeDuration(int fadeDuration) { this.fadeDuration = fadeDuration; } + public void setLayoutAnimationDuration(int layoutAnimationDuration) { + this.layoutAnimationDuration = layoutAnimationDuration; + } + + public void setAnimateLayout(boolean animateLayout, boolean animateVertical) { + this.animateLayout = animateLayout; + this.animateVertical = animateVertical; + } + /** * Set a listener that gives a call when a view gets removed + * * @param listener the listener */ public void setListener(Listener listener) { @@ -130,23 +146,56 @@ public class LoadView extends FrameLayout { // Assume view already attached to this view (fading out) if (newView.getParent() == null) { - addView(newView, new LayoutParams(LayoutParams.MATCH_PARENT, LayoutParams.MATCH_PARENT)); + addView(newView, getLayoutParamsForView(newView)); + } + + // Animate this view its size to the new view size if the width or height is WRAP_CONTENT + if (animateLayout && getChildCount() >= 2) { + final int currentSize = animateVertical ? getHeight() : getWidth(); + int newSize = animateVertical ? newView.getHeight() : newView.getWidth(); + if (newSize == 0) { + if (animateVertical) { + if (newView.getLayoutParams().height == LayoutParams.WRAP_CONTENT) { + newView.measure( + View.MeasureSpec.makeMeasureSpec(getWidth(), View.MeasureSpec.EXACTLY), + View.MeasureSpec.UNSPECIFIED); + newSize = newView.getMeasuredHeight(); + } else { + newSize = newView.getLayoutParams().height; + } + } else { + if (newView.getLayoutParams().width == LayoutParams.WRAP_CONTENT) { + newView.measure( + View.MeasureSpec.UNSPECIFIED, + View.MeasureSpec.makeMeasureSpec(getHeight(), View.MeasureSpec.EXACTLY)); + newSize = newView.getMeasuredWidth(); + } else { + newSize = newView.getLayoutParams().width; + } + } + } + + AnimationUtils.animateLayout(animateVertical, this, currentSize, newSize, layoutAnimationDuration, true, null); } } else { for (int i = 0; i < getChildCount(); i++) { View child = getChildAt(i); - child.clearAnimation(); + child.animate().cancel(); if (listener != null) { listener.onLoadViewRemoved(child); } } removeAllViews(); - newView.clearAnimation(); + newView.animate().cancel(); newView.setAlpha(1f); - addView(newView, new LayoutParams(LayoutParams.MATCH_PARENT, LayoutParams.MATCH_PARENT)); + addView(newView, getLayoutParamsForView(newView)); } } + public LayoutParams getLayoutParamsForView(View view) { + return new LayoutParams(LayoutParams.MATCH_PARENT, LayoutParams.MATCH_PARENT); + } + public interface Listener { void onLoadViewRemoved(View view); } diff --git a/Clover/app/src/main/java/org/floens/chan/ui/view/PostView.java b/Clover/app/src/main/java/org/floens/chan/ui/view/PostView.java index 204bf105..88f62311 100644 --- a/Clover/app/src/main/java/org/floens/chan/ui/view/PostView.java +++ b/Clover/app/src/main/java/org/floens/chan/ui/view/PostView.java @@ -54,7 +54,7 @@ import org.floens.chan.utils.IconCache; import org.floens.chan.utils.ThemeHelper; import org.floens.chan.utils.Time; -import static org.floens.chan.utils.AndroidUtils.setPressedDrawable; +import static org.floens.chan.utils.AndroidUtils.setItemBackground; public class PostView extends LinearLayout implements View.OnClickListener, PostLinkable.Callback { private final static LinearLayout.LayoutParams matchParams = new LinearLayout.LayoutParams( @@ -428,7 +428,7 @@ public class PostView extends LinearLayout implements View.OnClickListener, Post } repliesCountView = new TextView(context); - setPressedDrawable(repliesCountView); + setItemBackground(repliesCountView); repliesCountView.setTextColor(replyCountColor); repliesCountView.setPadding(postPadding, postPadding, postPadding, postPadding); repliesCountView.setTextSize(TypedValue.COMPLEX_UNIT_PX, repliesCountSize); @@ -441,14 +441,14 @@ public class PostView extends LinearLayout implements View.OnClickListener, Post contentContainer.addView(lastSeen, new LayoutParams(LayoutParams.MATCH_PARENT, lastSeenHeight)); if (!loadable.isThreadMode()) { - setPressedDrawable(contentContainer); + setItemBackground(contentContainer); } full.addView(contentContainer, matchWrapParams); optionsView = new ImageView(context); optionsView.setImageResource(R.drawable.ic_overflow); - setPressedDrawable(optionsView); + setItemBackground(optionsView); optionsView.setPadding(optionsLeftPadding, optionsTopPadding, optionsRightPadding, optionsBottomPadding); optionsView.setOnClickListener(new OnClickListener() { @Override diff --git a/Clover/app/src/main/java/org/floens/chan/ui/view/SelectionListeningEditText.java b/Clover/app/src/main/java/org/floens/chan/ui/view/SelectionListeningEditText.java new file mode 100644 index 00000000..6bbb3a7b --- /dev/null +++ b/Clover/app/src/main/java/org/floens/chan/ui/view/SelectionListeningEditText.java @@ -0,0 +1,38 @@ +package org.floens.chan.ui.view; + +import android.content.Context; +import android.util.AttributeSet; +import android.widget.EditText; + +public class SelectionListeningEditText extends EditText { + private SelectionChangedListener listener; + + public SelectionListeningEditText(Context context) { + super(context); + } + + public SelectionListeningEditText(Context context, AttributeSet attrs) { + super(context, attrs); + } + + public SelectionListeningEditText(Context context, AttributeSet attrs, int defStyleAttr) { + super(context, attrs, defStyleAttr); + } + + public void setSelectionChangedListener(SelectionChangedListener listener) { + this.listener = listener; + } + + @Override + protected void onSelectionChanged(int selStart, int selEnd) { + super.onSelectionChanged(selStart, selEnd); + + if (listener != null) { + listener.onSelectionChanged(selStart, selEnd); + } + } + + public interface SelectionChangedListener { + void onSelectionChanged(int selStart, int selEnd); + } +} diff --git a/Clover/app/src/main/java/org/floens/chan/utils/AndroidUtils.java b/Clover/app/src/main/java/org/floens/chan/utils/AndroidUtils.java index a28dafdc..82f0e231 100644 --- a/Clover/app/src/main/java/org/floens/chan/utils/AndroidUtils.java +++ b/Clover/app/src/main/java/org/floens/chan/utils/AndroidUtils.java @@ -17,6 +17,7 @@ */ package org.floens.chan.utils; +import android.annotation.SuppressLint; import android.app.Activity; import android.app.Dialog; import android.content.Context; @@ -28,6 +29,7 @@ import android.content.res.TypedArray; import android.graphics.Typeface; import android.graphics.drawable.Drawable; import android.net.Uri; +import android.os.Build; import android.os.Handler; import android.os.Looper; import android.preference.PreferenceManager; @@ -81,6 +83,7 @@ public class AndroidUtils { return PreferenceManager.getDefaultSharedPreferences(ChanApplication.con); } + @SuppressLint("SetJavaScriptEnabled") public static void openWebView(Activity activity, String title, String link) { Dialog dialog = new Dialog(activity); dialog.setContentView(R.layout.web_dialog); @@ -190,8 +193,8 @@ public class AndroidUtils { imm.hideSoftInputFromWindow(view.getWindowToken(), 0); } - public static String getReadableFileSize(int bytes, boolean si) { - int unit = si ? 1000 : 1024; + public static String getReadableFileSize(long bytes, boolean si) { + long unit = si ? 1000 : 1024; if (bytes < unit) return bytes + " B"; int exp = (int) (Math.log(bytes) / Math.log(unit)); @@ -253,13 +256,16 @@ public class AndroidUtils { } } - public static void setPressedDrawable(View view) { - TypedArray arr = view.getContext().obtainStyledAttributes(new int[]{android.R.attr.selectableItemBackground}); - - Drawable drawable = arr.getDrawable(0); + public static void setItemBackground(View view) { + view.setBackgroundResource(R.drawable.item_background); + } - arr.recycle(); - view.setBackgroundDrawable(drawable); + public static void setRoundItemBackground(View view) { + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) { + view.setBackground(getAttrDrawable(view.getContext(), android.R.attr.selectableItemBackgroundBorderless)); + } else { + view.setBackgroundResource(R.drawable.item_background); + } } public static List findViewsById(ViewGroup root, int id) { diff --git a/Clover/app/src/main/java/org/floens/chan/utils/AnimationUtils.java b/Clover/app/src/main/java/org/floens/chan/utils/AnimationUtils.java index d274d8e6..567cf7a3 100644 --- a/Clover/app/src/main/java/org/floens/chan/utils/AnimationUtils.java +++ b/Clover/app/src/main/java/org/floens/chan/utils/AnimationUtils.java @@ -17,11 +17,15 @@ */ package org.floens.chan.utils; +import android.animation.Animator; +import android.animation.AnimatorListenerAdapter; +import android.animation.ValueAnimator; import android.view.View; -import android.view.animation.Animation; +import android.view.ViewGroup; import android.view.animation.DecelerateInterpolator; -import org.floens.chan.ui.animation.HeightAnimation; +import java.util.HashMap; +import java.util.Map; public class AnimationUtils { public static void setHeight(View view, boolean expand, boolean animated) { @@ -36,44 +40,111 @@ public class AnimationUtils { } } + private static Map layoutAnimations = new HashMap<>(); + public static void animateHeight(final View view, boolean expand) { animateHeight(view, expand, -1); } - public static void animateHeight(final View view, boolean expand, int knownWidth) { - if (view.getAnimation() == null && ((view.getHeight() > 0 && expand) || (view.getHeight() == 0 && !expand))) { - return; - } + public static void animateHeight(final View view, final boolean expand, int knownWidth) { + animateHeight(view, expand, knownWidth, 300); + } + + public static void animateHeight(final View view, final boolean expand, int knownWidth, int duration) { + animateHeight(view, expand, knownWidth, duration, null); + } - view.clearAnimation(); - HeightAnimation heightAnimation; + /** + * Animate the height of a view by changing the layoutParams.height value.
+ * view.measure is used to figure out the height. + * Use knownWidth when the width of the view has not been measured yet.
+ * You can call this even when a height animation is currently running, it will resolve any issues.
+ * This does cause some lag on complex views because requestLayout is called on each frame. + */ + public static void animateHeight(final View view, final boolean expand, int knownWidth, int duration, final LayoutAnimationProgress progressCallback) { + final int fromHeight; + int toHeight; if (expand) { int width = knownWidth < 0 ? view.getWidth() : knownWidth; view.measure( View.MeasureSpec.makeMeasureSpec(width, View.MeasureSpec.EXACTLY), View.MeasureSpec.UNSPECIFIED); - heightAnimation = new HeightAnimation(view, 0, view.getMeasuredHeight(), 300); + fromHeight = view.getHeight(); + toHeight = view.getMeasuredHeight(); } else { - heightAnimation = new HeightAnimation(view, view.getHeight(), 0, 300); + fromHeight = view.getHeight(); + toHeight = 0; + } + + animateLayout(true, view, fromHeight, toHeight, duration, true, progressCallback); + } + + public static void animateLayout(final boolean vertical, final View view, final int from, final int to, int duration, final boolean wrapAfterwards, final LayoutAnimationProgress callback) { + ValueAnimator running = layoutAnimations.remove(view); + if (running != null) { + running.cancel(); } - view.startAnimation(heightAnimation); - view.getAnimation().setInterpolator(new DecelerateInterpolator(2f)); - view.getAnimation().setAnimationListener(new Animation.AnimationListener() { + + ValueAnimator valueAnimator = ValueAnimator.ofInt(from, to); + valueAnimator.addUpdateListener(new ValueAnimator.AnimatorUpdateListener() { @Override - public void onAnimationStart(Animation animation) { - view.setVisibility(View.VISIBLE); + public void onAnimationUpdate(ValueAnimator animation) { + int value = (int) animation.getAnimatedValue(); + // Looks better + if (value == 1) { + value = 0; + } + if (vertical) { + view.getLayoutParams().height = value; + } else { + view.getLayoutParams().width = value; + } + view.requestLayout(); + + if (callback != null) { + callback.onLayoutAnimationProgress(vertical, view, animation.getAnimatedFraction()); + } } + }); + valueAnimator.addListener(new AnimatorListenerAdapter() { @Override - public void onAnimationEnd(Animation animation) { - + public void onAnimationStart(Animator animation) { + view.setVisibility(View.VISIBLE); } @Override - public void onAnimationRepeat(Animation animation) { + public void onAnimationEnd(Animator animation) { + if (to > 0) { + if (wrapAfterwards) { + if (vertical) { + view.getLayoutParams().height = ViewGroup.LayoutParams.WRAP_CONTENT; + } else { + view.getLayoutParams().width = ViewGroup.LayoutParams.WRAP_CONTENT; + } + } + } else { + if (vertical) { + view.getLayoutParams().height = 0; + } else { + view.getLayoutParams().width = 0; + } + view.setVisibility(View.GONE); + } + view.requestLayout(); + layoutAnimations.remove(view); } }); + valueAnimator.setInterpolator(new DecelerateInterpolator(2f)); + valueAnimator.setDuration(duration); + valueAnimator.start(); + + layoutAnimations.put(view, valueAnimator); + } + + public interface LayoutAnimationProgress { + void onLayoutAnimationProgress(boolean vertical, View view, float progress); } } diff --git a/Clover/app/src/main/java/org/floens/chan/utils/IOUtils.java b/Clover/app/src/main/java/org/floens/chan/utils/IOUtils.java index 08fd21fe..70d014a6 100644 --- a/Clover/app/src/main/java/org/floens/chan/utils/IOUtils.java +++ b/Clover/app/src/main/java/org/floens/chan/utils/IOUtils.java @@ -74,6 +74,20 @@ public class IOUtils { } } + public static boolean copy(InputStream is, OutputStream os, long maxBytes) throws IOException { + long total = 0; + int read; + byte[] buffer = new byte[DEFAULT_BUFFER_SIZE]; + while ((read = is.read(buffer)) != -1) { + os.write(buffer, 0, read); + total += read; + if (total >= maxBytes) { + return false; + } + } + return true; + } + public static void copy(Reader input, Writer output) throws IOException { char[] buffer = new char[DEFAULT_BUFFER_SIZE]; int read; diff --git a/Clover/app/src/main/java/org/floens/chan/utils/ImageDecoder.java b/Clover/app/src/main/java/org/floens/chan/utils/ImageDecoder.java index 0639dad6..be6bf4f6 100644 --- a/Clover/app/src/main/java/org/floens/chan/utils/ImageDecoder.java +++ b/Clover/app/src/main/java/org/floens/chan/utils/ImageDecoder.java @@ -27,9 +27,30 @@ import java.io.FileNotFoundException; import java.io.IOException; /** - * Simple ImageDecoder with no threads. Taken from Volley ImageRequest. + * Simple ImageDecoder. Taken from Volley ImageRequest. */ public class ImageDecoder { + public static void decodeFileOnBackgroundThread(final File file, final int maxWidth, final int maxHeight, final ImageDecoderCallback callback) { + Thread thread = new Thread(new Runnable() { + @Override + public void run() { + final Bitmap bitmap = decodeFile(file, maxWidth, maxHeight); + + AndroidUtils.runOnUiThread(new Runnable() { + @Override + public void run() { + callback.onImageBitmap(file, bitmap); + } + }); + } + }); + thread.start(); + } + + public interface ImageDecoderCallback { + void onImageBitmap(File file, Bitmap bitmap); + } + public static Bitmap decodeFile(File file, int maxWidth, int maxHeight) { if (!file.exists()) return null; diff --git a/Clover/app/src/main/java/org/floens/chan/utils/ImageSaver.java b/Clover/app/src/main/java/org/floens/chan/utils/ImageSaver.java index 6a43dca6..940a29b1 100644 --- a/Clover/app/src/main/java/org/floens/chan/utils/ImageSaver.java +++ b/Clover/app/src/main/java/org/floens/chan/utils/ImageSaver.java @@ -274,7 +274,7 @@ public class ImageSaver { AndroidUtils.runOnUiThread(new Runnable() { @Override public void run() { - Logger.i(TAG, "Media scan succeeded: " + uri); + Logger.d(TAG, "Media scan succeeded: " + uri); if (shareAfterwards) { Intent intent = new Intent(Intent.ACTION_SEND); diff --git a/Clover/app/src/main/res/drawable-hdpi/ic_close_grey600_24dp.png b/Clover/app/src/main/res/drawable-hdpi/ic_close_grey600_24dp.png new file mode 100644 index 00000000..32580318 Binary files /dev/null and b/Clover/app/src/main/res/drawable-hdpi/ic_close_grey600_24dp.png differ diff --git a/Clover/app/src/main/res/drawable-hdpi/ic_image_grey600_24dp.png b/Clover/app/src/main/res/drawable-hdpi/ic_image_grey600_24dp.png new file mode 100644 index 00000000..c837fd89 Binary files /dev/null and b/Clover/app/src/main/res/drawable-hdpi/ic_image_grey600_24dp.png differ diff --git a/Clover/app/src/main/res/drawable-hdpi/ic_send_grey600_24dp.png b/Clover/app/src/main/res/drawable-hdpi/ic_send_grey600_24dp.png new file mode 100644 index 00000000..b771392e Binary files /dev/null and b/Clover/app/src/main/res/drawable-hdpi/ic_send_grey600_24dp.png differ diff --git a/Clover/app/src/main/res/drawable-mdpi/ic_close_grey600_24dp.png b/Clover/app/src/main/res/drawable-mdpi/ic_close_grey600_24dp.png new file mode 100644 index 00000000..1c382e5f Binary files /dev/null and b/Clover/app/src/main/res/drawable-mdpi/ic_close_grey600_24dp.png differ diff --git a/Clover/app/src/main/res/drawable-mdpi/ic_image_grey600_24dp.png b/Clover/app/src/main/res/drawable-mdpi/ic_image_grey600_24dp.png new file mode 100644 index 00000000..9470dddf Binary files /dev/null and b/Clover/app/src/main/res/drawable-mdpi/ic_image_grey600_24dp.png differ diff --git a/Clover/app/src/main/res/drawable-mdpi/ic_send_grey600_24dp.png b/Clover/app/src/main/res/drawable-mdpi/ic_send_grey600_24dp.png new file mode 100644 index 00000000..6fef4883 Binary files /dev/null and b/Clover/app/src/main/res/drawable-mdpi/ic_send_grey600_24dp.png differ diff --git a/Clover/app/src/main/res/drawable-xhdpi/ic_close_grey600_24dp.png b/Clover/app/src/main/res/drawable-xhdpi/ic_close_grey600_24dp.png new file mode 100644 index 00000000..fb9f88d2 Binary files /dev/null and b/Clover/app/src/main/res/drawable-xhdpi/ic_close_grey600_24dp.png differ diff --git a/Clover/app/src/main/res/drawable-xhdpi/ic_image_grey600_24dp.png b/Clover/app/src/main/res/drawable-xhdpi/ic_image_grey600_24dp.png new file mode 100644 index 00000000..56cba791 Binary files /dev/null and b/Clover/app/src/main/res/drawable-xhdpi/ic_image_grey600_24dp.png differ diff --git a/Clover/app/src/main/res/drawable-xhdpi/ic_send_grey600_24dp.png b/Clover/app/src/main/res/drawable-xhdpi/ic_send_grey600_24dp.png new file mode 100644 index 00000000..e1380462 Binary files /dev/null and b/Clover/app/src/main/res/drawable-xhdpi/ic_send_grey600_24dp.png differ diff --git a/Clover/app/src/main/res/drawable-xxhdpi/ic_close_grey600_24dp.png b/Clover/app/src/main/res/drawable-xxhdpi/ic_close_grey600_24dp.png new file mode 100644 index 00000000..3179d765 Binary files /dev/null and b/Clover/app/src/main/res/drawable-xxhdpi/ic_close_grey600_24dp.png differ diff --git a/Clover/app/src/main/res/drawable-xxhdpi/ic_image_grey600_24dp.png b/Clover/app/src/main/res/drawable-xxhdpi/ic_image_grey600_24dp.png new file mode 100644 index 00000000..b69c5204 Binary files /dev/null and b/Clover/app/src/main/res/drawable-xxhdpi/ic_image_grey600_24dp.png differ diff --git a/Clover/app/src/main/res/drawable-xxhdpi/ic_send_grey600_24dp.png b/Clover/app/src/main/res/drawable-xxhdpi/ic_send_grey600_24dp.png new file mode 100644 index 00000000..2c7a8026 Binary files /dev/null and b/Clover/app/src/main/res/drawable-xxhdpi/ic_send_grey600_24dp.png differ diff --git a/Clover/app/src/main/res/drawable-xxxhdpi/ic_close_grey600_24dp.png b/Clover/app/src/main/res/drawable-xxxhdpi/ic_close_grey600_24dp.png new file mode 100644 index 00000000..e3121dbf Binary files /dev/null and b/Clover/app/src/main/res/drawable-xxxhdpi/ic_close_grey600_24dp.png differ diff --git a/Clover/app/src/main/res/drawable-xxxhdpi/ic_image_grey600_24dp.png b/Clover/app/src/main/res/drawable-xxxhdpi/ic_image_grey600_24dp.png new file mode 100644 index 00000000..c5cb5e96 Binary files /dev/null and b/Clover/app/src/main/res/drawable-xxxhdpi/ic_image_grey600_24dp.png differ diff --git a/Clover/app/src/main/res/drawable-xxxhdpi/ic_send_grey600_24dp.png b/Clover/app/src/main/res/drawable-xxxhdpi/ic_send_grey600_24dp.png new file mode 100644 index 00000000..9b2c6196 Binary files /dev/null and b/Clover/app/src/main/res/drawable-xxxhdpi/ic_send_grey600_24dp.png differ diff --git a/Clover/app/src/main/res/layout/layout_reply.xml b/Clover/app/src/main/res/layout/layout_reply.xml new file mode 100644 index 00000000..1ef45416 --- /dev/null +++ b/Clover/app/src/main/res/layout/layout_reply.xml @@ -0,0 +1,6 @@ + + + + diff --git a/Clover/app/src/main/res/layout/layout_reply_input.xml b/Clover/app/src/main/res/layout/layout_reply_input.xml new file mode 100644 index 00000000..440af286 --- /dev/null +++ b/Clover/app/src/main/res/layout/layout_reply_input.xml @@ -0,0 +1,169 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/Clover/app/src/main/res/layout/layout_thread_list.xml b/Clover/app/src/main/res/layout/layout_thread_list.xml index fda19b4a..f37a5155 100644 --- a/Clover/app/src/main/res/layout/layout_thread_list.xml +++ b/Clover/app/src/main/res/layout/layout_thread_list.xml @@ -1,26 +1,34 @@ + android:layout_height="match_parent" + android:orientation="vertical"> + + + android:singleLine="true" + android:textColor="#ff757575" + android:visibility="gone" + tools:ignore="UnusedAttribute" /> + android:layout_height="0dp" + android:layout_weight="1" + android:scrollbars="vertical" /> diff --git a/Clover/app/src/main/res/layout/reply_input.xml b/Clover/app/src/main/res/layout/reply_input.xml index 0d93a0f6..75294561 100644 --- a/Clover/app/src/main/res/layout/reply_input.xml +++ b/Clover/app/src/main/res/layout/reply_input.xml @@ -44,7 +44,7 @@ along with this program. If not, see . android:id="@+id/reply_email" android:layout_width="match_parent" android:layout_height="wrap_content" - android:hint="@string/reply_email" + android:hint="@string/reply_options" android:minHeight="40dp" android:textSize="14sp" /> @@ -57,7 +57,7 @@ along with this program. If not, see . android:layout_width="match_parent" android:layout_height="match_parent" android:gravity="top" - android:hint="@string/reply_comment" + android:hint="@string/reply_comment_board" android:imeActionLabel="@string/reply_submit" android:inputType="textMultiLine|textCapSentences|textAutoCorrect" android:minLines="4" diff --git a/Clover/app/src/main/res/values/attrs.xml b/Clover/app/src/main/res/values/attrs.xml index 3d52f03b..2c49af90 100644 --- a/Clover/app/src/main/res/values/attrs.xml +++ b/Clover/app/src/main/res/values/attrs.xml @@ -76,6 +76,10 @@ along with this program. If not, see . + + + + @@ -94,4 +98,9 @@ along with this program. If not, see . + + + + + diff --git a/Clover/app/src/main/res/values/strings.xml b/Clover/app/src/main/res/values/strings.xml index 6822e0e3..71c5d1ff 100644 --- a/Clover/app/src/main/res/values/strings.xml +++ b/Clover/app/src/main/res/values/strings.xml @@ -125,8 +125,7 @@ along with this program. If not, see . Open drawer Close drawer - posts - Quick reply + Reply Highlight ID Text copied to clipboard Quote @@ -136,7 +135,6 @@ along with this program. If not, see . Show links Copy text Report - Reply reply replies @@ -152,27 +150,24 @@ along with this program. If not, see . Report - Reply to Make thread in Name - Options + Options Subject - Comment + Comment reply + Comment new thread File name Warning: File size too big (%1$s / %2$s) Warning: WebM size too big (%1$s / %2$s) Spoiler image - No preview available + No preview [spoiler] [code] Submit - Enter the text - Error sending reply - Invalid captcha - No file selected - Post Successful + Error posting + Post successful Failed to load captcha Tap to reload the captcha diff --git a/Clover/app/src/main/res/values/styles.xml b/Clover/app/src/main/res/values/styles.xml index d519ab79..9cb5011c 100644 --- a/Clover/app/src/main/res/values/styles.xml +++ b/Clover/app/src/main/res/values/styles.xml @@ -27,6 +27,10 @@ along with this program. If not, see . @style/ToolbarDropDownListViewStyle + #DE000000 + #89000000 + #42000000 + #FFDDDDDD #FFBCBCBC #FFD6BAD0 @@ -45,6 +49,11 @@ along with this program. If not, see . #ff789922 #1E000000 @drawable/ic_overflow + + #ffffffff + #88ffffff + #ff757575 + #887d7d7d diff --git a/docs/4chanresponses.txt b/docs/4chanresponses.txt index 675c7d58..63ea33fe 100644 --- a/docs/4chanresponses.txt +++ b/docs/4chanresponses.txt @@ -3,6 +3,4 @@ Error: You forgot to solve the CAPTCHA. Please try again. Error: You seem to have mistyped the CAPTCHA. Please try again. - -Post successful!

Post successful!

- +Post successful!

image.jpg uploaded!