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.
filtering
Floens 10 years ago
parent cabe0698a2
commit 50ba7ff553
  1. 21
      Clover/app/src/main/assets/captcha/captcha.html
  2. 2
      Clover/app/src/main/java/org/floens/chan/ChanApplication.java
  3. 2
      Clover/app/src/main/java/org/floens/chan/core/loader/ChanLoader.java
  4. 14
      Clover/app/src/main/java/org/floens/chan/core/manager/ThreadManager.java
  5. 8
      Clover/app/src/main/java/org/floens/chan/core/model/Loadable.java
  6. 20
      Clover/app/src/main/java/org/floens/chan/core/model/Reply.java
  7. 412
      Clover/app/src/main/java/org/floens/chan/core/presenter/ReplyPresenter.java
  8. 70
      Clover/app/src/main/java/org/floens/chan/core/presenter/ThreadPresenter.java
  9. 71
      Clover/app/src/main/java/org/floens/chan/core/reply/HttpCall.java
  10. 112
      Clover/app/src/main/java/org/floens/chan/core/reply/ReplyHttpCall.java
  11. 133
      Clover/app/src/main/java/org/floens/chan/core/reply/ReplyManager.java
  12. 2
      Clover/app/src/main/java/org/floens/chan/database/DatabaseManager.java
  13. 138
      Clover/app/src/main/java/org/floens/chan/ui/activity/ImagePickActivity.java
  14. 4
      Clover/app/src/main/java/org/floens/chan/ui/activity/PassSettingsActivity.java
  15. 17
      Clover/app/src/main/java/org/floens/chan/ui/adapter/PostAdapter.java
  16. 29
      Clover/app/src/main/java/org/floens/chan/ui/animation/HeightAnimation.java
  17. 2
      Clover/app/src/main/java/org/floens/chan/ui/controller/BrowseController.java
  18. 2
      Clover/app/src/main/java/org/floens/chan/ui/controller/PassSettingsController.java
  19. 9
      Clover/app/src/main/java/org/floens/chan/ui/controller/ThreadController.java
  20. 2
      Clover/app/src/main/java/org/floens/chan/ui/controller/ViewThreadController.java
  21. 39
      Clover/app/src/main/java/org/floens/chan/ui/drawable/DropdownArrowDrawable.java
  22. 27
      Clover/app/src/main/java/org/floens/chan/ui/fragment/ReplyFragment.java
  23. 10
      Clover/app/src/main/java/org/floens/chan/ui/fragment/ThreadFragment.java
  24. 5
      Clover/app/src/main/java/org/floens/chan/ui/layout/CaptchaLayout.java
  25. 369
      Clover/app/src/main/java/org/floens/chan/ui/layout/ReplyLayout.java
  26. 18
      Clover/app/src/main/java/org/floens/chan/ui/layout/ThreadLayout.java
  27. 72
      Clover/app/src/main/java/org/floens/chan/ui/layout/ThreadListLayout.java
  28. 3
      Clover/app/src/main/java/org/floens/chan/ui/toolbar/Toolbar.java
  29. 57
      Clover/app/src/main/java/org/floens/chan/ui/view/LoadView.java
  30. 8
      Clover/app/src/main/java/org/floens/chan/ui/view/PostView.java
  31. 38
      Clover/app/src/main/java/org/floens/chan/ui/view/SelectionListeningEditText.java
  32. 22
      Clover/app/src/main/java/org/floens/chan/utils/AndroidUtils.java
  33. 107
      Clover/app/src/main/java/org/floens/chan/utils/AnimationUtils.java
  34. 14
      Clover/app/src/main/java/org/floens/chan/utils/IOUtils.java
  35. 23
      Clover/app/src/main/java/org/floens/chan/utils/ImageDecoder.java
  36. 2
      Clover/app/src/main/java/org/floens/chan/utils/ImageSaver.java
  37. BIN
      Clover/app/src/main/res/drawable-hdpi/ic_close_grey600_24dp.png
  38. BIN
      Clover/app/src/main/res/drawable-hdpi/ic_image_grey600_24dp.png
  39. BIN
      Clover/app/src/main/res/drawable-hdpi/ic_send_grey600_24dp.png
  40. BIN
      Clover/app/src/main/res/drawable-mdpi/ic_close_grey600_24dp.png
  41. BIN
      Clover/app/src/main/res/drawable-mdpi/ic_image_grey600_24dp.png
  42. BIN
      Clover/app/src/main/res/drawable-mdpi/ic_send_grey600_24dp.png
  43. BIN
      Clover/app/src/main/res/drawable-xhdpi/ic_close_grey600_24dp.png
  44. BIN
      Clover/app/src/main/res/drawable-xhdpi/ic_image_grey600_24dp.png
  45. BIN
      Clover/app/src/main/res/drawable-xhdpi/ic_send_grey600_24dp.png
  46. BIN
      Clover/app/src/main/res/drawable-xxhdpi/ic_close_grey600_24dp.png
  47. BIN
      Clover/app/src/main/res/drawable-xxhdpi/ic_image_grey600_24dp.png
  48. BIN
      Clover/app/src/main/res/drawable-xxhdpi/ic_send_grey600_24dp.png
  49. BIN
      Clover/app/src/main/res/drawable-xxxhdpi/ic_close_grey600_24dp.png
  50. BIN
      Clover/app/src/main/res/drawable-xxxhdpi/ic_image_grey600_24dp.png
  51. BIN
      Clover/app/src/main/res/drawable-xxxhdpi/ic_send_grey600_24dp.png
  52. 6
      Clover/app/src/main/res/layout/layout_reply.xml
  53. 169
      Clover/app/src/main/res/layout/layout_reply_input.xml
  54. 30
      Clover/app/src/main/res/layout/layout_thread_list.xml
  55. 4
      Clover/app/src/main/res/layout/reply_input.xml
  56. 9
      Clover/app/src/main/res/values/attrs.xml
  57. 19
      Clover/app/src/main/res/values/strings.xml
  58. 9
      Clover/app/src/main/res/values/styles.xml
  59. 4
      docs/4chanresponses.txt

@ -3,19 +3,26 @@
<head>
<meta name=viewport content="width=device-width, initial-scale=1">
<style type="text/css">
#loadingCaptcha {
font-family: sans-serif;
font-size: 20px;
}
#captcha-container div {
margin: 0 auto;
}
#captcha-loading, #captcha-error {
font-family: sans-serif;
font-size: 18x;
text-align: center;
margin: 40px auto 0 auto;
}
</style>
<script src='https://www.google.com/recaptcha/api.js?onload=globalOnCaptchaLoaded&render=explicit'></script>
</head>
<body>
<div id="captcha-loading">Loading captcha...</div>
<div id="captcha-error"></div>
<div id="captcha-container"></div>
<div id="captcha-error"></div>
<div id="captcha-loading">Loading captcha&#8230;</div>
<script type="text/javascript">
(function() {
window.globalOnCaptchaEntered = function(res) {
CaptchaCallback.onCaptchaEntered(res);
}
@ -31,8 +38,10 @@ window.globalOnCaptchaLoaded = function() {
}
window.onerror = function(message, url, line) {
document.getElementById('captcha-loading').style.display = 'none';
document.getElementById('captcha-error').appendChild(document.createTextNode(line + ': ' + message + ' @ ' + url));
}
})();
</script>
</body>
</html>

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

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

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

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

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

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

@ -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<FloatingMenuItem> 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<Post> filter, boolean clearFilter, boolean setEmptyText, boolean hideKeyboard);
void quote(Post post, boolean withText);
}
}

@ -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<? extends HttpCall> callback) {
this.callback = callback;
}
}

@ -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("<!-- thread:([0-9]+),no:([0-9]+) -->");
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) {
}
}

@ -15,7 +15,7 @@
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <http://www.gnu.org/licenses/>.
*/
package org.floens.chan.core.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<Loadable, Reply> 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<Loadable, Reply> 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<T extends HttpCall> {
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();

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

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

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

@ -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<RecyclerView.ViewHolder> {
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<RecyclerView.ViewHolder> {
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<RecyclerView.ViewHolder> {
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<RecyclerView.ViewHolder> {
}
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();
}

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

@ -90,7 +90,7 @@ public class BrowseController extends ThreadController implements ToolbarMenuIte
threadLayout.getPresenter().requestData();
break;
case POST_ID:
// TODO
openPost(true);
break;
}
}

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

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

@ -154,7 +154,7 @@ public class ViewThreadController extends ThreadController implements ThreadLayo
setPinIconState(threadLayout.getPresenter().pin());
break;
case POST_ID:
// TODO
openPost(true);
break;
}
}

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

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

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

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

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

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

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

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

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

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

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

@ -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<View> findViewsById(ViewGroup root, int id) {

@ -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<View, ValueAnimator> 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.<br>
* view.measure is used to figure out the height.
* Use knownWidth when the width of the view has not been measured yet.<br>
* You can call this even when a height animation is currently running, it will resolve any issues.<br>
* <b>This does cause some lag on complex views because requestLayout is called on each frame.</b>
*/
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);
}
}

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

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

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

Binary file not shown.

After

Width:  |  Height:  |  Size: 329 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 350 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 352 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 269 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 270 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 295 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 400 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 397 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 448 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 484 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 629 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 565 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 644 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 808 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 750 B

@ -0,0 +1,6 @@
<?xml version="1.0" encoding="utf-8"?>
<org.floens.chan.ui.layout.ReplyLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="match_parent"
android:layout_height="wrap_content">
</org.floens.chan.ui.layout.ReplyLayout>

@ -0,0 +1,169 @@
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:minHeight="124dp"
android:orientation="vertical"
tools:ignore="Suspicious0dp,ContentDescription">
<TextView
android:id="@+id/message"
android:layout_width="match_parent"
android:layout_height="0dp"
android:padding="8dp"
android:textColor="#ffff0000"
android:visibility="gone" />
<LinearLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:baselineAligned="false"
android:orientation="horizontal">
<LinearLayout
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_weight="1"
android:orientation="vertical"
android:paddingBottom="8dp"
android:paddingLeft="8dp"
android:paddingTop="8dp">
<EditText
android:id="@+id/name"
android:layout_width="match_parent"
android:layout_height="0dp"
android:hint="@string/reply_name"
android:inputType="textCapSentences|textAutoCorrect"
android:singleLine="true"
android:textSize="16sp"
android:visibility="gone" />
<EditText
android:id="@+id/subject"
android:layout_width="match_parent"
android:layout_height="0dp"
android:hint="@string/reply_subject"
android:inputType="textCapSentences|textAutoCorrect"
android:singleLine="true"
android:textSize="16sp"
android:visibility="gone" />
<EditText
android:id="@+id/options"
android:layout_width="match_parent"
android:layout_height="0dp"
android:hint="@string/reply_options"
android:singleLine="true"
android:textSize="16sp"
android:visibility="gone" />
<EditText
android:id="@+id/file_name"
android:layout_width="match_parent"
android:layout_height="0dp"
android:hint="@string/reply_file_name"
android:singleLine="true"
android:textSize="16sp"
android:visibility="gone" />
<RelativeLayout
android:layout_width="match_parent"
android:layout_height="wrap_content">
<org.floens.chan.ui.view.SelectionListeningEditText
android:id="@+id/comment"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:imeActionLabel="@string/reply_submit"
android:inputType="textMultiLine|textCapSentences|textAutoCorrect"
android:maxLines="6"
android:minHeight="107dp"
android:textSize="16sp" />
<TextView
android:id="@+id/comment_counter"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_alignParentRight="true"
android:layout_alignParentTop="true"
android:paddingRight="8dp"
android:textColor="?text_color_secondary"
android:textSize="12sp"
tools:ignore="RelativeOverlap" />
</RelativeLayout>
</LinearLayout>
<LinearLayout
android:id="@+id/preview_container"
android:layout_width="0dp"
android:layout_height="match_parent"
android:orientation="vertical"
android:paddingBottom="8dp"
android:paddingLeft="8dp"
android:paddingTop="8dp"
android:visibility="gone">
<CheckBox
android:id="@+id/spoiler"
android:layout_width="100dp"
android:layout_height="0dp"
android:text="@string/reply_spoiler_image"
android:textSize="14sp"
android:visibility="gone" />
<ImageView
android:id="@+id/preview"
android:layout_width="100dp"
android:layout_height="0dp"
android:layout_weight="1"
android:scaleType="centerInside" />
<TextView
android:id="@+id/preview_message"
android:layout_width="100dp"
android:layout_height="wrap_content"
android:textSize="12sp" />
</LinearLayout>
<LinearLayout
android:id="@+id/buttons"
android:layout_width="52dp"
android:layout_height="match_parent"
android:orientation="vertical"
android:padding="8dp">
<ImageView
android:id="@+id/more"
android:layout_width="36dp"
android:layout_height="36dp"
android:padding="10dp" />
<Space
android:layout_width="36dp"
android:layout_height="0dp"
android:layout_weight="1" />
<ImageView
android:id="@+id/attach"
android:layout_width="36dp"
android:layout_height="36dp"
android:padding="6dp"
android:src="@drawable/ic_image_grey600_24dp" />
<ImageView
android:id="@+id/submit"
android:layout_width="36dp"
android:layout_height="36dp"
android:padding="6dp"
android:src="@drawable/ic_send_grey600_24dp" />
</LinearLayout>
</LinearLayout>
</LinearLayout>

@ -1,26 +1,34 @@
<?xml version="1.0" encoding="utf-8"?>
<org.floens.chan.ui.layout.ThreadListLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:orientation="vertical"
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent"
android:layout_height="match_parent">
android:layout_height="match_parent"
android:orientation="vertical">
<org.floens.chan.ui.layout.ReplyLayout
android:id="@+id/reply"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:visibility="gone" />
<TextView
android:id="@+id/search_status"
android:visibility="gone"
android:gravity="center"
android:textColor="#ff757575"
android:layout_width="match_parent"
android:layout_height="0dp"
android:background="#ffffffff"
android:singleLine="true"
android:elevation="4dp"
android:gravity="center"
android:padding="8dp"
android:layout_width="match_parent"
android:layout_height="0dp" />
android:singleLine="true"
android:textColor="#ff757575"
android:visibility="gone"
tools:ignore="UnusedAttribute" />
<android.support.v7.widget.RecyclerView
android:id="@+id/recycler_view"
android:scrollbars="vertical"
android:layout_weight="1"
android:layout_width="match_parent"
android:layout_height="0dp" />
android:layout_height="0dp"
android:layout_weight="1"
android:scrollbars="vertical" />
</org.floens.chan.ui.layout.ThreadListLayout>

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

@ -76,6 +76,10 @@ along with this program. If not, see <http://www.gnu.org/licenses/>.
<attr name="board_edit_item_style" format="reference" />
<attr name="text_color_primary" format="color" />
<attr name="text_color_secondary" format="color" />
<attr name="text_color_hint" format="color" />
<attr name="post_thumbnail_background" format="color" />
<attr name="post_saved_reply_color" format="color" />
<attr name="post_highlighted_color" format="color" />
@ -94,4 +98,9 @@ along with this program. If not, see <http://www.gnu.org/licenses/>.
<attr name="post_inline_quote_color" format="color" />
<attr name="post_divider_color" format="color" />
<attr name="post_options_drawable" format="integer" />
<attr name="dropdown_light_color" format="color" />
<attr name="dropdown_light_pressed_color" format="color" />
<attr name="dropdown_dark_color" format="color" />
<attr name="dropdown_dark_pressed_color" format="color" />
</resources>

@ -125,8 +125,7 @@ along with this program. If not, see <http://www.gnu.org/licenses/>.
<string name="drawer_open">Open drawer</string>
<string name="drawer_close">Close drawer</string>
<string name="multiple_posts">posts</string>
<string name="post_quick_reply">Quick reply</string>
<string name="post_reply">Reply</string>
<string name="post_highlight_id">Highlight ID</string>
<string name="post_text_copied_to_clipboard">Text copied to clipboard</string>
<string name="post_quote">Quote</string>
@ -136,7 +135,6 @@ along with this program. If not, see <http://www.gnu.org/licenses/>.
<string name="post_show_links">Show links</string>
<string name="post_copy_text">Copy text</string>
<string name="post_report">Report</string>
<string name="post_reply">Reply</string>
<string name="one_reply">reply</string>
<string name="multiple_replies">replies</string>
@ -152,27 +150,24 @@ along with this program. If not, see <http://www.gnu.org/licenses/>.
<item>Report</item>
</string-array>
<string name="reply">Reply to</string>
<string name="reply_to_board">Make thread in</string>
<string name="reply_name">Name</string>
<string name="reply_email">Options</string>
<string name="reply_options">Options</string>
<string name="reply_subject">Subject</string>
<string name="reply_comment">Comment</string>
<string name="reply_comment_thread">Comment reply</string>
<string name="reply_comment_board">Comment new thread</string>
<string name="reply_file_name">File name</string>
<string name="reply_file_too_big">Warning: File size too big (%1$s / %2$s)</string>
<string name="reply_webm_too_big">Warning: WebM size too big (%1$s / %2$s)</string>
<string name="reply_spoiler_image">Spoiler image</string>
<string name="reply_no_preview">No preview available</string>
<string name="reply_no_preview">No preview</string>
<string name="reply_insert_spoiler">[spoiler]</string>
<string name="reply_insert_code">[code]</string>
<string name="reply_submit">Submit</string>
<string name="reply_captcha">Enter the text</string>
<string name="reply_error">Error sending reply</string>
<string name="reply_error_captcha">Invalid captcha</string>
<string name="reply_error_file">No file selected</string>
<string name="reply_success">Post Successful</string>
<string name="reply_error">Error posting</string>
<string name="reply_success">Post successful</string>
<string name="reply_captcha_load_error">Failed to load captcha</string>
<string name="reply_captcha_tap_to_reload">Tap to reload the captcha</string>

@ -27,6 +27,10 @@ along with this program. If not, see <http://www.gnu.org/licenses/>.
<item name="dropDownListViewStyle">@style/ToolbarDropDownListViewStyle</item>
<item name="text_color_primary">#DE000000</item>
<item name="text_color_secondary">#89000000</item>
<item name="text_color_hint">#42000000</item>
<item name="post_thumbnail_background">#FFDDDDDD</item>
<item name="post_saved_reply_color">#FFBCBCBC</item>
<item name="post_highlighted_color">#FFD6BAD0</item>
@ -45,6 +49,11 @@ along with this program. If not, see <http://www.gnu.org/licenses/>.
<item name="post_inline_quote_color">#ff789922</item>
<item name="post_divider_color">#1E000000</item>
<item name="post_options_drawable">@drawable/ic_overflow</item>
<item name="dropdown_light_color">#ffffffff</item>
<item name="dropdown_light_pressed_color">#88ffffff</item>
<item name="dropdown_dark_color">#ff757575</item>
<item name="dropdown_dark_pressed_color">#887d7d7d</item>
</style>
<!-- For the toolbar dropdown list -->

@ -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.
<!DOCTYPE html><head><meta http-equiv="refresh" content="1;URL=http://boards.4chan.org/b/res/515317358#p515318219"><link rel="shortcut icon" href="//static.4chan.org/image/favicon.ico"><title>Post successful!</title><link rel="stylesheet" title="switch" href="//static.4chan.org/css/yotsubanew.525.css"></head><body style="margin-top: 20%; text-align: center;"><h1 style="font-size:36pt;">Post successful!</h1><!-- thread:515317358,no:515318219 --></body></html>
<!DOCTYPE html><head><meta http-equiv="refresh" content="1;URL=http://boards.4chan.org/board/thread/12345"><link rel="shortcut icon" href="//s.4cdn.org/image/favicon.ico"><title>Post successful!</title><link rel="stylesheet" title="switch" href="//s.4cdn.org/css/yotsubanew.607.css"></head><body style="margin-top: 20%; text-align: center;"><h1 style="font-size:36pt;">image.jpg uploaded!</h1><!-- thread:0,no:12345 --></body></html>

Loading…
Cancel
Save