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
@ -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); |
||||
} |
||||
} |
@ -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) { |
||||
} |
||||
} |
@ -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; |
||||
} |
||||
} |
@ -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); |
||||
} |
||||
} |
@ -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); |
||||
} |
||||
} |
After Width: | Height: | Size: 329 B |
After Width: | Height: | Size: 350 B |
After Width: | Height: | Size: 352 B |
After Width: | Height: | Size: 269 B |
After Width: | Height: | Size: 270 B |
After Width: | Height: | Size: 295 B |
After Width: | Height: | Size: 400 B |
After Width: | Height: | Size: 397 B |
After Width: | Height: | Size: 448 B |
After Width: | Height: | Size: 484 B |
After Width: | Height: | Size: 629 B |
After Width: | Height: | Size: 565 B |
After Width: | Height: | Size: 644 B |
After Width: | Height: | Size: 808 B |
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> |
||||
|