From 50ba7ff553424b4c98b2744652566472c1163ef4 Mon Sep 17 00:00:00 2001 From: Floens Date: Sun, 17 May 2015 17:42:13 +0200 Subject: [PATCH] Add reply layout. Placing a cursor on a >>1234 quote highlights that quote. Added a more organized way of calling the 4chan http post endpoints: Extend HttpCall and implement the setup and process methods for that call. ImagePickActivity was some of the worst code I've ever written. It leaked resources and would copy without size limits. That's fixed now. AnimationUtils animateLayout now handles starting animations while an animation is still ruinning. --- .../app/src/main/assets/captcha/captcha.html | 21 +- .../java/org/floens/chan/ChanApplication.java | 2 +- .../floens/chan/core/loader/ChanLoader.java | 2 +- .../chan/core/manager/ThreadManager.java | 14 +- .../org/floens/chan/core/model/Loadable.java | 8 + .../org/floens/chan/core/model/Reply.java | 20 +- .../chan/core/presenter/ReplyPresenter.java | 412 ++++++++++++++++++ .../chan/core/presenter/ThreadPresenter.java | 70 +-- .../org/floens/chan/core/reply/HttpCall.java | 71 +++ .../floens/chan/core/reply/ReplyHttpCall.java | 112 +++++ .../core/{manager => reply}/ReplyManager.java | 133 +++--- .../floens/chan/database/DatabaseManager.java | 2 +- .../chan/ui/activity/ImagePickActivity.java | 138 +++--- .../ui/activity/PassSettingsActivity.java | 4 +- .../floens/chan/ui/adapter/PostAdapter.java | 17 +- .../chan/ui/animation/HeightAnimation.java | 29 -- .../chan/ui/controller/BrowseController.java | 2 +- .../ui/controller/PassSettingsController.java | 2 +- .../chan/ui/controller/ThreadController.java | 9 + .../ui/controller/ViewThreadController.java | 2 +- .../ui/drawable/DropdownArrowDrawable.java | 39 +- .../chan/ui/fragment/ReplyFragment.java | 27 +- .../chan/ui/fragment/ThreadFragment.java | 10 +- .../floens/chan/ui/layout/CaptchaLayout.java | 5 +- .../floens/chan/ui/layout/ReplyLayout.java | 369 ++++++++++++++++ .../floens/chan/ui/layout/ThreadLayout.java | 18 +- .../chan/ui/layout/ThreadListLayout.java | 72 ++- .../org/floens/chan/ui/toolbar/Toolbar.java | 3 +- .../org/floens/chan/ui/view/LoadView.java | 57 ++- .../org/floens/chan/ui/view/PostView.java | 8 +- .../ui/view/SelectionListeningEditText.java | 38 ++ .../org/floens/chan/utils/AndroidUtils.java | 22 +- .../org/floens/chan/utils/AnimationUtils.java | 107 ++++- .../java/org/floens/chan/utils/IOUtils.java | 14 + .../org/floens/chan/utils/ImageDecoder.java | 23 +- .../org/floens/chan/utils/ImageSaver.java | 2 +- .../drawable-hdpi/ic_close_grey600_24dp.png | Bin 0 -> 329 bytes .../drawable-hdpi/ic_image_grey600_24dp.png | Bin 0 -> 350 bytes .../drawable-hdpi/ic_send_grey600_24dp.png | Bin 0 -> 352 bytes .../drawable-mdpi/ic_close_grey600_24dp.png | Bin 0 -> 269 bytes .../drawable-mdpi/ic_image_grey600_24dp.png | Bin 0 -> 270 bytes .../drawable-mdpi/ic_send_grey600_24dp.png | Bin 0 -> 295 bytes .../drawable-xhdpi/ic_close_grey600_24dp.png | Bin 0 -> 400 bytes .../drawable-xhdpi/ic_image_grey600_24dp.png | Bin 0 -> 397 bytes .../drawable-xhdpi/ic_send_grey600_24dp.png | Bin 0 -> 448 bytes .../drawable-xxhdpi/ic_close_grey600_24dp.png | Bin 0 -> 484 bytes .../drawable-xxhdpi/ic_image_grey600_24dp.png | Bin 0 -> 629 bytes .../drawable-xxhdpi/ic_send_grey600_24dp.png | Bin 0 -> 565 bytes .../ic_close_grey600_24dp.png | Bin 0 -> 644 bytes .../ic_image_grey600_24dp.png | Bin 0 -> 808 bytes .../drawable-xxxhdpi/ic_send_grey600_24dp.png | Bin 0 -> 750 bytes .../app/src/main/res/layout/layout_reply.xml | 6 + .../main/res/layout/layout_reply_input.xml | 169 +++++++ .../main/res/layout/layout_thread_list.xml | 30 +- .../app/src/main/res/layout/reply_input.xml | 4 +- Clover/app/src/main/res/values/attrs.xml | 9 + Clover/app/src/main/res/values/strings.xml | 19 +- Clover/app/src/main/res/values/styles.xml | 9 + docs/4chanresponses.txt | 4 +- 59 files changed, 1796 insertions(+), 338 deletions(-) create mode 100644 Clover/app/src/main/java/org/floens/chan/core/presenter/ReplyPresenter.java create mode 100644 Clover/app/src/main/java/org/floens/chan/core/reply/HttpCall.java create mode 100644 Clover/app/src/main/java/org/floens/chan/core/reply/ReplyHttpCall.java rename Clover/app/src/main/java/org/floens/chan/core/{manager => reply}/ReplyManager.java (85%) delete mode 100644 Clover/app/src/main/java/org/floens/chan/ui/animation/HeightAnimation.java create mode 100644 Clover/app/src/main/java/org/floens/chan/ui/layout/ReplyLayout.java create mode 100644 Clover/app/src/main/java/org/floens/chan/ui/view/SelectionListeningEditText.java create mode 100644 Clover/app/src/main/res/drawable-hdpi/ic_close_grey600_24dp.png create mode 100644 Clover/app/src/main/res/drawable-hdpi/ic_image_grey600_24dp.png create mode 100644 Clover/app/src/main/res/drawable-hdpi/ic_send_grey600_24dp.png create mode 100644 Clover/app/src/main/res/drawable-mdpi/ic_close_grey600_24dp.png create mode 100644 Clover/app/src/main/res/drawable-mdpi/ic_image_grey600_24dp.png create mode 100644 Clover/app/src/main/res/drawable-mdpi/ic_send_grey600_24dp.png create mode 100644 Clover/app/src/main/res/drawable-xhdpi/ic_close_grey600_24dp.png create mode 100644 Clover/app/src/main/res/drawable-xhdpi/ic_image_grey600_24dp.png create mode 100644 Clover/app/src/main/res/drawable-xhdpi/ic_send_grey600_24dp.png create mode 100644 Clover/app/src/main/res/drawable-xxhdpi/ic_close_grey600_24dp.png create mode 100644 Clover/app/src/main/res/drawable-xxhdpi/ic_image_grey600_24dp.png create mode 100644 Clover/app/src/main/res/drawable-xxhdpi/ic_send_grey600_24dp.png create mode 100644 Clover/app/src/main/res/drawable-xxxhdpi/ic_close_grey600_24dp.png create mode 100644 Clover/app/src/main/res/drawable-xxxhdpi/ic_image_grey600_24dp.png create mode 100644 Clover/app/src/main/res/drawable-xxxhdpi/ic_send_grey600_24dp.png create mode 100644 Clover/app/src/main/res/layout/layout_reply.xml create mode 100644 Clover/app/src/main/res/layout/layout_reply_input.xml 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 0000000000000000000000000000000000000000..32580318072ce6eb9fcb7c9da9993c41ddb54d82 GIT binary patch literal 329 zcmeAS@N?(olHy`uVBq!ia0vp^Dj>|k0wldT1B8K;Lb6AYF9SoB8UsT^3j@P1pisjL z28L1t28LG&3=CE?7#PG0=Ijcz0ZK3>dAqwX{BQ3+vmeOgEbxddW?p<|k0wldT1B8K;Lb6AYF9SoB8UsT^3j@P1pisjL z28L1t28LG&3=CE?7#PG0=Ijcz0ZK3>dAqwX{BQ3+vmeOgEbxddW?pgbV2%3A&+nWHA|he+;;{zajkdU-qk4S9kS`n4ZsMx%2>L-Cz0i7ioGdI0n5&C0txa`k>|k0wldT1B8K;Lb6AYF9SoB8UsT^3j@P1pisjL z28L1t28LG&3=CE?7#PG0=Ijcz0ZK3>dAqwX{BQ3+vmeOgEbxddW?AL~`x1rW}?Jxo?=F4jO4N$iMJ$(A&xU$0E+I zGIx{bJH3;wxk^84FU*`(#~t8*daCr@DglS%b0?MAX3Q`ZDZA)@K_D%VGewNI(Pxq2 zgs2eqppI9Lf`QT+3cuJ=W%f>-9Oe3avYD!UsbiO=~G=WkL+ z>gnPbVsZNJ%Vgndp7Z&*==MmTxJo` z%3%|xSCMkWQBdYYLCl7Z=AaJ+xkdH&loXXi8y-}>SjKTY(kn=`>vM#6epkKHmi_KR zMuNXqOw%@wY4>4~P2sQHCFQu*&8hAAA+y+hPxp&|U*Y#=j+dojU)mF(gBUzr{an^L HB{Ts5OS)F* literal 0 HcmV?d00001 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 0000000000000000000000000000000000000000..9470dddfa7e8eb115ae201d39dfb9167481ee9e1 GIT binary patch literal 270 zcmeAS@N?(olHy`uVBq!ia0vp^5+KaM0wlfaz7_*1g=CK)Uj~LMH3o);76yi2K%s^g z3=E|P3=FRl7#OT(FffQ0%-I!a1C(G&@^*J&_}|`tWO=~G=WkL+ z=IP=XVsScIBUORd=z_0v!pqZ_{#QI$9aJB}`(tYJ1Md*t|D6qsrB*C@7@S$~GW?L# zxdV}k4j);Zq_#~pn!b)@bJap4?hlze;(il@!fvkf`LH1(m@-Y)OM--DDj{8xoA zX|`rL);29vluTdbx4>z`KBX0ZSXZ&tGg&@zU8%qKv&lsP1|wOSO9xJ=E(f}Z!PC{x JWt~$(6953}TI~P; literal 0 HcmV?d00001 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 0000000000000000000000000000000000000000..6fef4883d32630714d7fb7f593d2af325f01d2fc GIT binary patch literal 295 zcmeAS@N?(olHy`uVBq!ia0vp^5+KaM0wlfaz7_*1g=CK)Uj~LMH3o);76yi2K%s^g z3=E|P3=FRl7#OT(FffQ0%-I!a1C(G&@^*J&_}|`tWO=~G=WkJ` z-_yl0#NzbP$p?9x4R~DTg_sX8EDf5uv3tq%64n<=DMGuLWL`9G^HRBOH2vDnXRi9+ z%1fU{gxWKy~b};Kse^YX0*8-Cr zv({BCILn&)Aco7MxxaPh9@}^1y*t*{?kmD%rnJ{ot~A!iK}@9xpzI ksXSpdQu(*}GW#pm750IG(~fR)2RfC()78&qol`;+0AvGZ{r~^~ literal 0 HcmV?d00001 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 0000000000000000000000000000000000000000..fb9f88d2ad464925546cc86c22dbcf22f3c1a30f GIT binary patch literal 400 zcmeAS@N?(olHy`uVBq!ia0vp^1|ZDA0wn)(8}a}tg=CK)Uj~LMH3o);76yi2K%s^g z3=E|}g|8AA7_4S6Fo+k-*%fF5lweBoc6VX;-`;;_Kaj^+;1OBOz`!jG!i)^F=14Fw zFmiahIEF+VetXTEugO59^`Y7Zmm`jiv2$1#E;yze6~rzjx3IrhFxf|H`KS8MJ8`QQ z?E4+a>EXas$NuC+;Sysh^%Nn6-370-%ycV4PyxSD0)>Y@{G{XEX$ z!_VgLFJ7*7kd5I|d3gSKIiIV+&q=oeqnJ$I-1nSQV1I4$Pt}W`9NMB^EPJ40b@HgU rqO<$6co*&6R?A*jGYNf&KOoF`O!GsbP0l+XkKU8tDG literal 0 HcmV?d00001 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 0000000000000000000000000000000000000000..56cba791718ed34d03658d5a66302ca02f124e26 GIT binary patch literal 397 zcmeAS@N?(olHy`uVBq!ia0vp^1|ZDA0wn)(8}a}tg=CK)Uj~LMH3o);76yi2K%s^g z3=E|}g|8AA7_4S6Fo+k-*%fF5lweBoc6VX;-`;;_Kaj^+;1OBOz`!jG!i)^F=14Fw zFtU2OIEF+VetX@J>yUxSv5y>0508_ByU^?hY>~E%?OZ zm8s3ARJckaH#>LM$q%`Wd*t`9KXg?%;}-GYKSTQjiAZD3>^VjU6j#OCcrOvs63Ci9 z-6QO%!yS$ZBFvi>MjT)g+t9$e>eyj(&NnYEq~>#37&<6O6kcSvnU=3!zU;j8I=u~x zooBBt+7|Hchn2&%F1hySH`}HaspkucHeL_ou~eQY^RiwieE#GW94pq&D0qCzqP)KN zHN*S00%vB5uIu=>-XTu=)FQt}+5uO(Hthk@2CKJv?*9K_*0N>O@0Th&Bz$8oN{jf{ n8Z|M)L_Og2?ff;YvraHu-6?jO@~MFl7)lJDu6{1-oD!M004R> z004l5008;`004mK004C`008P>0026e000+ooVrmw00002VoOIv0RM-N%)bBt010qN zS#tmY3ljhU3ljkVnw%H_00B5jL_t(o!|j*RZNe}RKwl68lmVbpnUO_o37l?F!W@Ot zK}uE#0{Ri95J<3nw!fOoXQDj&*}gkx3(`h50R9tT%aRYObbu2hO5R!VlMVoIWP@g# z4p7H8I*za_;7oS+t|Z5V3VQ}kcOb_!!ekZAKEH}h zEldh{Z2?|700oM@kD~d_52qGI2199EWZM|~7Fn_8*u72N$JiBrQc@hW*1n0KDilSU zR8di&AJ1+az!NFq089Q1E18`MEl$yQmLdUoN%3vGyruZGj)pv5>v-S%DybBE`|i*y q$S)uf`sqHvC(AfehHLBqh#kMS#iSv<*6SGn0000I(3)EF2VS{N990fib~ zFff!FFfhDIU|_JC!N4G1FlSew4N!t9$=lt9;eUJonf*W>XMsm#F#`j)FbFd;%$g&? zz`z*e>EalYaqsO-N4_Ql0oH)BuNkiAjE@IJa#Wd=bLKGP~9JF5lIsRcecul|FLFQ+cq&RQ*@qW7k5V1D>~2B{&}~ndj{H%k`sO zkj#@M^RHB@%$R)H>XIjm`ugchCu;cR757f|@T=QiUKDNf!K_;0tmC8O`IDH~Pdg`0 z;$r8`Py~|xJ+HqnJd~*PMy!*gI7Vl0cJYk~@=CEgzx)kfaHww6A-h+Hw$;7}d3U0) zfuVJF%VovP!XKQhEV*A@yd^Vh%lQDG3EyU2OuUq5T$LiD;7}3P z=JC7GA!tpCx7k_Kmxmhn{F*v1egDF!ua_=ozuA-=dHoP;+??C>vhcw1D70l<;y=g# UyXpHiz`$qlboFyt=akR{03ZjyWdHyG literal 0 HcmV?d00001 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 0000000000000000000000000000000000000000..b69c520404b91fd415cf181c4f93807ef4a14363 GIT binary patch literal 629 zcmV-*0*d{KP)004R> z004l5008;`004mK004C`008P>0026e000+ooVrmw00002VoOIv0RM-N%)bBt010qN zS#tmY3ljhU3ljkVnw%H_00HeuL_t(&-tF4GFGEon$MJ6s72-ee9}sNJMlq0hTbf81 z#A_@VCG5l`1_@yxW%ROll9mV}MuTFqp!LpDl%DoJr`nS%d7duk+>_Ju$+^FK+B6M} z;jbz|K?+ikg8T^zGRz*gycgm1K#WmJ%?k&NCv_!T)D?Lvj%gw;+o7NX4%DzsPzLIA zzyYYGUj_;}OK?aAs&WRZmVqjqfy(q?RFHxcq#y+;NI?oxkb?dP=#F+0eggHd$)JBg zvxI>qmi+;WFrI1+aOwuMpJVKrE?!dY1ReHCvIp(ZKsd|T3!Pl0^-t}SWD7bc#4Fud zc4>mhuPfF`vI9NP#to39h0Bc5Ff)cZmPxV$4RMfa#c3t^W9*Dx;~dK**?<;VOzTDI zN$cIy#%-2)R!J(cc(#7O?Hbcx?cSU>#v(~l&<*W8W?Y?Q1AsZ!J?4v(Wcn5MJ>$w7 z&FmWImlGjLBhQ{IRS~5;bN&2wXetW_6131M;E0nX0qroAWAwss0Q@0I-k@_rhMSmQ z36kUm`aB8?Q-U+e3-o;y7N$ItBzww=SAl%bA^nn{UXRXJPs%_i&Oj$J(5f@giVQT* zu@lZA3(^cVapZv0%#-vfi&M`iF;nOY66Gi(G?+gF1NwIz1t~~D3i??e?Bt!g>TNs8 P00000NkvXXu0mjfW*r5c literal 0 HcmV?d00001 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 0000000000000000000000000000000000000000..2c7a80266242bf7f54b74bcb9cbe98281e7f2088 GIT binary patch literal 565 zcmV-50?Pe~P)004R> z004l5008;`004mK004C`008P>0026e000+ooVrmw00002VoOIv0RM-N%)bBt010qN zS#tmY3ljhU3ljkVnw%H_00FH@L_t(&-tF02Zh}w{fZ^Z91=I!9ChZ!#5l9regNo~@ ztuCap1xzmwgjACP=Q1;r=HRO)U*-ql9A-~U>zE}_0wquaO$xMQ#S6`JK%d-D^URu$ z>3{%EY{}V82h<)1a{GWoAb^^Z!ZP3(s0*0!b`8)Mv)*qS1#|})1@r?M1oQ_f2Mh(N z1-t}(d;eZMU@S;HpbV%BxaK$=P#dr!j|(UZYL7iRo6vw7ps0Xapx}U-prC;IAkTm{ zpe|s>yB}!5!`Wxa+&7t9I|8c6*h?pSJOkW$Br7PWB~EIe8T6gh85>UFmwng_Mk4wAF~-lp z;kp{Wb;!-2KT_p>>(IQp_C~q}XfTrf1KLO=J5WPKKK=w`1M-Muu?+HxqzMX&qy-9& zqydUrt4eRjIISgYHmr`6l)4*M?jXTPu^{nCF`$}4K2j*?K&BIE2-Np#K{b*e$RLtC z$S9HvNMQ3)4rKm@YzXwq%080R?neS8Py!{;KLz~&$HDh8V7w~v00000NkvXXu0mjf D^l|4S literal 0 HcmV?d00001 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 0000000000000000000000000000000000000000..e3121dbff3cfa643282098b9525297313970e214 GIT binary patch literal 644 zcmeAS@N?(olHy`uVBq!ia0vp^2_VeD0wg^q?%xcg6p}rHd>I(3)EF2VS{N990fib~ zFff!FFfhDIU|_JC!N4G1FlSew4N!t9$=lt9;eUJonf*W>XMsm#F#`j)FbFd;%$g&? zz`*#&)5S5Q;?~<+fkB51L|Px_ITRW$a6Y~5-Ms@BuSb;{TI^`}ae!HY_ler`B?iUI z^6h5)pDj4+$nA*Y`~J!e3``sf$i!9#V?JhSF}d3w+PXcpHO=RP&F-hRM%`eLgW zXY7;hcE)zh6;6fwzDIL-Gd%fypV@**#s0Z(k7C28&)=M$ww$Xo{JC@XP9rCs`oLoI zS^f>rrspjG%<QkCISyyt<8zh@Ecp3j_RegU0=vgqAB7m_o!=?l zE6DLv#zC6#O-?0$!?#281sVg4=R2*K?XPg z_qi3A?C1NenCP#1<)pfh*Js<~qFjv=_ZQ@S?&2{0tl6^bCyUeD8Vg1B`p_CzZWW)2 z{Hi{a`Bi+T@+`1=NYEIDRMq>Us$Ur_TC-LxSDxh{(RWdlg|^*lmZh6 NgQu&X%Q~loCIC6n3v>Vg literal 0 HcmV?d00001 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 0000000000000000000000000000000000000000..c5cb5e969ddb11be915be7dbe9cb8e314353938e GIT binary patch literal 808 zcmeAS@N?(olHy`uVBq!ia0vp^2_VeD0wg^q?%xcg6p}rHd>I(3)EF2VS{N990fib~ zFff!FFfhDIU|_JC!N4G1FlSew4N!t9$=lt9;eUJonf*W>XMsm#F#`j)FbFd;%$g$s zRI$L*#WAGf*4x{svxEa>j(^No&A8P&U0mQ`wz*=Oq$`Kl)i1IlYt~NJYTB{wD`(3s zDW~9C&e$fUyK0L%XNz1eIXBaCZn2v2hyUx-jM@$6fBAE+e%Z_( z8>O43N(_t~4h&2J4Gb&~geIi6F}dHA|2XsSvXjzYH9X7`sg2Uke;amBsdUKBZwP!f zr~jZ%liA75{k+E}bKX#L7nVqG-uQ&k$|vY; zZv1oCPYj>We0{su#LJ0?!L+O7*SF(6lO6~JEIccFKhU=*hj&lfB$o6Bw#t7&PZyPi z%(mg}zuv5VP%xo7)bh-(Z_#IG>uC3}_%;bUv@31B_f7Mf#S)3_i+-N7&k&l!@NCBW z+q;iwX}Z3?WV24tfRm3Qw2OIhTI3DWSK^j+KlTJqP>Eo#SgZA6PBim-qmx+;`6Mtc!(zlZ@~6-8Iu(L-w004R> z004l5008;`004mK004C`008P>0026e000+ooVrmw00002VoOIv0RM-N%)bBt010qN zS#tmY3ljhU3ljkVnw%H_00L%7L_t(|+U?tIPQpMCfZ;*W-*Nyo(KC9Hk_dPL5cL$w zmv{}OdIG3l+Y%yL>bBjP+1X^uKNFB=_iZWd(zt-Iw}t`~pa2CZKmiI+04Cr8uAmRg zZ~;p=gg1k@gy(Pp>6XA9L@){$u-=}aEjy?@UPQW$3_x^HWdZALZh{)f z3aT1FU{HBL0!{0)61X*UF$xzTC@2?zh@kR-ci6A^8fj1wjKc-|1l2c(fM5aX7H2`d zgbVNxR37jFdkC-lm>r>3;R4u#q6Dmix-oMx<^+5~1O2}c2E_v)42lmx6cjIjAgDax zi{c9w(K)pLqz%c6GXG z2-~0*TTnwY7ZZ1Y-}#vt#P$C{x!oV&m>)|}xHmYc<|hp*f+;Y7K)wG-DJe zV{0_HA7r>=TLSL-A*Mq#<(b&_U_MK`{^|27}tnK_7|EG=bv)FQWl=D z$p=)5MjD`UG;#nnqmcoKO)@>2ON$&AcO#c*d;&^eYmf*)yB_TWfEtYrfPIq>JsLg$ zQ8YXN!f0#&_oW61_g#DeSfZf<%+0tD!dz~{9E}Z7b;((edxa1lz*96B0P-~`E&%(& zSI^P707OJn58!oar7J*KG&aERA3z9PuU-})HkvX3(ZAWS0q)J4AB6lftiUz}C_n)U gP=Epypa6&M8+B7kP%l#F6951J07*qoM6N<$f(ecxUH||9 literal 0 HcmV?d00001 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!