/* * Copyright 2016 Hippo Seven * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package com.hippo.ehviewer.ui.scene; import android.animation.Animator; import android.annotation.SuppressLint; import android.app.Activity; import android.content.ClipData; import android.content.ClipboardManager; import android.content.Context; import android.content.DialogInterface; import android.content.res.Resources; import android.graphics.Color; import android.graphics.Typeface; import android.graphics.drawable.ColorDrawable; import android.graphics.drawable.Drawable; import android.os.Bundle; import android.os.Looper; import android.os.Parcelable; import android.text.Spannable; import android.text.SpannableString; import android.text.SpannableStringBuilder; import android.text.TextUtils; import android.text.style.ClickableSpan; import android.text.style.ForegroundColorSpan; import android.text.style.RelativeSizeSpan; import android.text.style.StyleSpan; import android.text.style.URLSpan; import android.util.Log; import android.view.LayoutInflater; import android.view.View; import android.view.ViewGroup; import android.widget.EditText; import android.widget.ImageView; import android.widget.TextView; import androidx.annotation.NonNull; import androidx.annotation.Nullable; import androidx.appcompat.app.AlertDialog; import androidx.recyclerview.widget.DefaultItemAnimator; import androidx.recyclerview.widget.LinearLayoutManager; import androidx.recyclerview.widget.RecyclerView; import com.axlecho.api.MHApi; import com.google.android.material.floatingactionbutton.FloatingActionButton; import com.hippo.android.resource.AttrResources; import com.hippo.easyrecyclerview.EasyRecyclerView; import com.hippo.easyrecyclerview.LinearDividerItemDecoration; import com.hippo.ehviewer.EhApplication; import com.hippo.ehviewer.R; import com.hippo.ehviewer.UrlOpener; import com.hippo.ehviewer.client.EhClient; import com.hippo.ehviewer.client.EhRequest; import com.hippo.ehviewer.client.EhUrl; import com.hippo.ehviewer.client.data.GalleryComment; import com.hippo.ehviewer.client.parser.VoteCommentParser; import com.hippo.ehviewer.ui.MainActivity; import com.hippo.reveal.ViewAnimationUtils; import com.hippo.ripple.Ripple; import com.hippo.scene.SceneFragment; import com.hippo.text.Html; import com.hippo.text.URLImageGetter; import com.hippo.util.DrawableManager; import com.hippo.util.ExceptionUtils; import com.hippo.util.ReadableTime; import com.hippo.util.TextUrl; import com.hippo.view.ViewTransition; import com.hippo.widget.FabLayout; import com.hippo.widget.LinkifyTextView; import com.hippo.widget.ObservedTextView; import com.hippo.yorozuya.AnimationUtils; import com.hippo.yorozuya.AssertUtils; import com.hippo.yorozuya.LayoutUtils; import com.hippo.yorozuya.ResourcesUtils; import com.hippo.yorozuya.SimpleAnimatorListener; import com.hippo.yorozuya.StringUtils; import com.hippo.yorozuya.ViewUtils; import com.hippo.yorozuya.collect.IntList; import java.util.ArrayList; import java.util.List; public final class GalleryCommentsScene extends ToolbarScene implements EasyRecyclerView.OnItemClickListener, View.OnClickListener { public static final String TAG = GalleryCommentsScene.class.getSimpleName(); public static final String KEY_API_UID = "api_uid"; public static final String KEY_API_KEY = "api_key"; public static final String KEY_GID = "gid"; public static final String KEY_TOKEN = "token"; public static final String KEY_COMMENTS = "comments"; private long mApiUid; private String mApiKey; private String mGid; private String mToken; @Nullable private GalleryComment[] mComments; @Nullable private EasyRecyclerView mRecyclerView; @Nullable private FabLayout mFabLayout; @Nullable private FloatingActionButton mFab; @Nullable private View mEditPanel; @Nullable private ImageView mSendImage; @Nullable private EditText mEditText; @Nullable private CommentAdapter mAdapter; @Nullable private ViewTransition mViewTransition; private boolean mInAnimation = false; @Override public void onCreate(@Nullable Bundle savedInstanceState) { super.onCreate(savedInstanceState); if (savedInstanceState == null) { onInit(); } else { onRestore(savedInstanceState); } } private void handleArgs(Bundle args) { if (args == null) { return; } mApiUid = args.getLong(KEY_API_UID, -1L); mApiKey = args.getString(KEY_API_KEY); mGid = args.getString(KEY_GID); mToken = args.getString(KEY_TOKEN, null); Parcelable[] parcelables = args.getParcelableArray(KEY_COMMENTS); if (parcelables instanceof GalleryComment[]) { mComments = (GalleryComment[]) parcelables; } } private void onInit() { handleArgs(getArguments()); } private void onRestore(@NonNull Bundle savedInstanceState) { mApiUid = savedInstanceState.getLong(KEY_API_UID, -1L); mApiKey = savedInstanceState.getString(KEY_API_KEY); mGid = savedInstanceState.getString(KEY_GID); mToken = savedInstanceState.getString(KEY_TOKEN, null); Parcelable[] parcelables = savedInstanceState.getParcelableArray(KEY_COMMENTS); if (parcelables instanceof GalleryComment[]) { mComments = (GalleryComment[]) parcelables; } } @Override public void onSaveInstanceState(Bundle outState) { super.onSaveInstanceState(outState); outState.putLong(KEY_API_UID, mApiUid); outState.putString(KEY_API_KEY, mApiKey); outState.putString(KEY_GID, mGid); outState.putString(KEY_TOKEN, mToken); outState.putParcelableArray(KEY_COMMENTS, mComments); } @Nullable @Override public View onCreateView3(LayoutInflater inflater, @Nullable ViewGroup container, @Nullable Bundle savedInstanceState) { View view = inflater.inflate(R.layout.scene_gallery_comments, container, false); mRecyclerView = (EasyRecyclerView) ViewUtils.$$(view, R.id.recycler_view); TextView tip = (TextView) ViewUtils.$$(view, R.id.tip); mEditPanel = ViewUtils.$$(view, R.id.edit_panel); mSendImage = (ImageView) ViewUtils.$$(mEditPanel, R.id.send); mEditText = (EditText) ViewUtils.$$(mEditPanel, R.id.edit_text); mFabLayout = (FabLayout) ViewUtils.$$(view, R.id.fab_layout); mFab = (FloatingActionButton) ViewUtils.$$(view, R.id.fab); Context context = getContext2(); AssertUtils.assertNotNull(context); Resources resources = context.getResources(); int paddingBottomFab = resources.getDimensionPixelOffset(R.dimen.gallery_padding_bottom_fab); Drawable drawable = DrawableManager.getVectorDrawable(context, R.drawable.big_sad_pandroid); drawable.setBounds(0, 0, drawable.getIntrinsicWidth(), drawable.getIntrinsicHeight()); tip.setCompoundDrawables(null, drawable, null, null); mAdapter = new CommentAdapter(); mRecyclerView.setAdapter(mAdapter); mRecyclerView.setLayoutManager(new LinearLayoutManager(context, RecyclerView.VERTICAL, false)); LinearDividerItemDecoration decoration = new LinearDividerItemDecoration( LinearDividerItemDecoration.VERTICAL, AttrResources.getAttrColor(context, R.attr.dividerColor), LayoutUtils.dp2pix(context, 1)); decoration.setShowLastDivider(true); mRecyclerView.addItemDecoration(decoration); mRecyclerView.setSelector(Ripple.generateRippleDrawable(context, !AttrResources.getAttrBoolean(context, R.attr.isLightTheme), new ColorDrawable(Color.TRANSPARENT))); mRecyclerView.setHasFixedSize(true); mRecyclerView.setOnItemClickListener(this); mRecyclerView.setPadding(mRecyclerView.getPaddingLeft(), mRecyclerView.getPaddingTop(), mRecyclerView.getPaddingRight(), mRecyclerView.getPaddingBottom() + paddingBottomFab); // Cancel change animator RecyclerView.ItemAnimator itemAnimator = mRecyclerView.getItemAnimator(); if (itemAnimator instanceof DefaultItemAnimator) { ((DefaultItemAnimator) itemAnimator).setSupportsChangeAnimations(false); } mSendImage.setOnClickListener(this); mFab.setOnClickListener(this); addAboveSnackView(mEditPanel); addAboveSnackView(mFabLayout); mViewTransition = new ViewTransition(mRecyclerView, tip); updateView(false); return view; } @Override public void onDestroyView() { super.onDestroyView(); if (null != mRecyclerView) { mRecyclerView.stopScroll(); mRecyclerView = null; } if (null != mEditPanel) { removeAboveSnackView(mEditPanel); mEditPanel = null; } if (null != mFabLayout) { removeAboveSnackView(mFabLayout); mFabLayout = null; } mFab = null; mSendImage = null; mEditText = null; mAdapter = null; mViewTransition = null; } @Override public void onViewCreated(View view, @Nullable Bundle savedInstanceState) { super.onViewCreated(view, savedInstanceState); setTitle(R.string.gallery_comments); setNavigationIcon(R.drawable.v_arrow_left_dark_x24); } @Override public void onNavigationClick() { onBackPressed(); } private void voteComment(long id, int vote) { Context context = getContext2(); MainActivity activity = getActivity2(); if (null == context || null == activity) { return; } EhRequest request = new EhRequest() .setMethod(EhClient.METHOD_VOTE_COMMENT) .setArgs(mApiUid, mApiKey, mGid, mToken, id, vote) .setCallback(new VoteCommentListener(context, activity.getStageId(), getTag())); EhApplication.getEhClient(context).execute(request); } private class InfoHolder extends RecyclerView.ViewHolder { private final TextView key; private final TextView value; public InfoHolder(View itemView) { super(itemView); key = (TextView) ViewUtils.$$(itemView, R.id.key); value = (TextView) ViewUtils.$$(itemView, R.id.value); } } @SuppressLint("InflateParams") public void showVoteStatusDialog(Context context, String voteStatus) { String[] temp = StringUtils.split(voteStatus, ','); final int length = temp.length; final String[] userArray = new String[length]; final String[] voteArray = new String[length]; for (int i = 0; i < length; i++) { String str = StringUtils.trim(temp[i]); int index = str.lastIndexOf(' '); if (index < 0) { Log.d(TAG, "Something wrong happened about vote state"); userArray[i] = str; voteArray[i] = ""; } else { userArray[i] = StringUtils.trim(str.substring(0, index)); voteArray[i] = StringUtils.trim(str.substring(index + 1)); } } AlertDialog.Builder builder = new AlertDialog.Builder(context); context = builder.getContext(); final LayoutInflater inflater = LayoutInflater.from(context); EasyRecyclerView rv = (EasyRecyclerView) inflater.inflate(R.layout.dialog_recycler_view, null); rv.setAdapter(new RecyclerView.Adapter<InfoHolder>() { @Override public InfoHolder onCreateViewHolder(ViewGroup parent, int viewType) { return new InfoHolder(inflater.inflate(R.layout.item_drawer_favorites, parent, false)); } @Override public void onBindViewHolder(InfoHolder holder, int position) { holder.key.setText(userArray[position]); holder.value.setText(voteArray[position]); } @Override public int getItemCount() { return length; } }); rv.setLayoutManager(new LinearLayoutManager(context)); LinearDividerItemDecoration decoration = new LinearDividerItemDecoration( LinearDividerItemDecoration.VERTICAL, AttrResources.getAttrColor(context, R.attr.dividerColor), LayoutUtils.dp2pix(context, 1)); decoration.setPadding(ResourcesUtils.getAttrDimensionPixelOffset(context, R.attr.dialogPreferredPadding)); rv.addItemDecoration(decoration); rv.setSelector(Ripple.generateRippleDrawable(context, !AttrResources.getAttrBoolean(context, R.attr.isLightTheme), new ColorDrawable(Color.TRANSPARENT))); rv.setClipToPadding(false); builder.setView(rv).show(); } private void showCommentDialog(int position) { final Context context = getContext2(); if (null == context || null == mComments || position >= mComments.length || position < 0) { return; } final GalleryComment comment = mComments[position]; List<String> menu = new ArrayList<>(); final IntList menuId = new IntList(); Resources resources = context.getResources(); if (0 == comment.id || mApiUid < 0) { // 0 id is uploader comment, can't vote // Not sign in, can't vote menu.add(resources.getString(R.string.copy_comment_text)); menuId.add(R.id.copy); } else { menu.add(resources.getString(R.string.copy_comment_text)); menuId.add(R.id.copy); menu.add(resources.getString(comment.voteUp ? R.string.cancel_vote_up : R.string.vote_up)); menuId.add(R.id.vote_up); menu.add(resources.getString(comment.voteDown ? R.string.cancel_vote_down : R.string.vote_down)); menuId.add(R.id.vote_down); } if (!TextUtils.isEmpty(comment.voteState)) { menu.add(resources.getString(R.string.check_vote_status)); menuId.add(R.id.check_vote_status); } new AlertDialog.Builder(context) .setItems(menu.toArray(new String[menu.size()]), new DialogInterface.OnClickListener() { @Override public void onClick(DialogInterface dialog, int which) { if (which < 0 || which >= menuId.size()) { return; } int id = menuId.get(which); switch (id) { case R.id.copy: ClipboardManager cmb = (ClipboardManager) context.getSystemService(Context.CLIPBOARD_SERVICE); cmb.setPrimaryClip(ClipData.newPlainText(null, comment.comment)); showTip(R.string.copied_to_clipboard, LENGTH_SHORT); break; case R.id.vote_up: voteComment(comment.id, 1); break; case R.id.vote_down: voteComment(comment.id, -1); break; case R.id.check_vote_status: showVoteStatusDialog(context, comment.voteState); break; } } }).show(); } @Override public boolean onItemClick(EasyRecyclerView parent, View view, int position, long id) { Activity activity = getActivity2(); if (null == activity) { return false; } RecyclerView.ViewHolder holder = parent.getChildViewHolder(view); if (holder instanceof CommentHolder) { CommentHolder commentHolder = (CommentHolder) holder; ClickableSpan span = commentHolder.comment.getCurrentSpan(); commentHolder.comment.clearCurrentSpan(); if (span instanceof URLSpan) { UrlOpener.openUrl(activity, ((URLSpan) span).getURL(), true); return true; } } showCommentDialog(position); return true; } private void updateView(boolean animation) { if (null == mViewTransition) { return; } if (mComments == null || mComments.length <= 0) { mViewTransition.showView(1, animation); } else { mViewTransition.showView(0, animation); } } private void showEditPanelWithAnimation() { if (null == mFab || null == mEditPanel) { return; } mInAnimation = true; mFab.setTranslationX(0.0f); mFab.setTranslationY(0.0f); mFab.setScaleX(1.0f); mFab.setScaleY(1.0f); int fabEndX = mEditPanel.getLeft() + (mEditPanel.getWidth() / 2) - (mFab.getWidth() / 2); int fabEndY = mEditPanel.getTop() + (mEditPanel.getHeight() / 2) - (mFab.getHeight() / 2); mFab.animate().x(fabEndX).y(fabEndY).scaleX(0.0f).scaleY(0.0f) .setInterpolator(AnimationUtils.SLOW_FAST_SLOW_INTERPOLATOR) .setDuration(300L).setListener(new SimpleAnimatorListener() { @Override public void onAnimationEnd(Animator animation) { if (null == mFab || null == mEditPanel) { return; } ((View) mFab).setVisibility(View.INVISIBLE); mEditPanel.setVisibility(View.VISIBLE); int halfW = mEditPanel.getWidth() / 2; int halfH = mEditPanel.getHeight() / 2; Animator animator = ViewAnimationUtils.createCircularReveal(mEditPanel, halfW, halfH, 0, (float) Math.hypot(halfW, halfH)).setDuration(300L); animator.addListener(new SimpleAnimatorListener() { @Override public void onAnimationEnd(Animator a) { mInAnimation = false; } }); animator.start(); } }).start(); } private void showEditPanel(boolean animation) { if (animation) { showEditPanelWithAnimation(); } else { if (null == mFab || null == mEditPanel) { return; } ((View) mFab).setVisibility(View.INVISIBLE); mEditPanel.setVisibility(View.VISIBLE); } } private void hideEditPanelWithAnimation() { if (null == mFab || null == mEditPanel) { return; } mInAnimation = true; int halfW = mEditPanel.getWidth() / 2; int halfH = mEditPanel.getHeight() / 2; Animator animator = ViewAnimationUtils.createCircularReveal(mEditPanel, halfW, halfH, (float) Math.hypot(halfW, halfH), 0.0f).setDuration(300L); animator.addListener(new SimpleAnimatorListener() { @Override public void onAnimationEnd(Animator a) { if (null == mFab || null == mEditPanel) { return; } if (Looper.myLooper() != Looper.getMainLooper()) { // Some devices may run this block in non-UI thread. // It might be a bug of Android OS. // Check it here to avoid crash. return; } mEditPanel.setVisibility(View.GONE); ((View) mFab).setVisibility(View.VISIBLE); int fabStartX = mEditPanel.getLeft() + (mEditPanel.getWidth() / 2) - (mFab.getWidth() / 2); int fabStartY = mEditPanel.getTop() + (mEditPanel.getHeight() / 2) - (mFab.getHeight() / 2); mFab.setX(fabStartX); mFab.setY(fabStartY); mFab.setScaleX(0.0f); mFab.setScaleY(0.0f); mFab.setRotation(-45.0f); mFab.animate().translationX(0.0f).translationY(0.0f).scaleX(1.0f).scaleY(1.0f).rotation(0.0f) .setInterpolator(AnimationUtils.SLOW_FAST_SLOW_INTERPOLATOR) .setDuration(300L).setListener(new SimpleAnimatorListener() { @Override public void onAnimationEnd(Animator animation) { mInAnimation = false; } }).start(); } }); animator.start(); } private void hideEditPanel(boolean animation) { if (animation) { hideEditPanelWithAnimation(); } else { if (null == mFab || null == mEditPanel) { return; } ((View) mFab).setVisibility(View.VISIBLE); mEditPanel.setVisibility(View.INVISIBLE); } } @Nullable private String getGalleryDetailUrl() { return MHApi.Companion.getINSTANCE().get(currentSource).pageUrl(mGid); } @Override public void onClick(View v) { Context context = getContext2(); MainActivity activity = getActivity2(); if (null == context || null == activity || null == mEditText) { return; } if (mFab == v) { if (!mInAnimation) { showEditPanel(true); } } else if (mSendImage == v) { if (!mInAnimation) { String comment = mEditText.getText().toString(); if (TextUtils.isEmpty(comment)) { // Comment is empty return; } String url = getGalleryDetailUrl(); if (url == null) { return; } // Request EhRequest request = new EhRequest() .setMethod(EhClient.METHOD_GET_COMMENT_GALLERY) .setArgs(url, comment) .setCallback(new CommentGalleryListener(context, activity.getStageId(), getTag())); EhApplication.getEhClient(context).execute(request); hideSoftInput(); hideEditPanel(true); } } } @Override public void onBackPressed() { if (mInAnimation) { return; } if (null != mEditPanel && mEditPanel.getVisibility() == View.VISIBLE) { hideEditPanel(true); } else { finish(); } } private class CommentHolder extends RecyclerView.ViewHolder { public final TextView user; public final TextView time; public final LinkifyTextView comment; public CommentHolder(View itemView) { super(itemView); user = (TextView) itemView.findViewById(R.id.user); time = (TextView) itemView.findViewById(R.id.time); comment = (LinkifyTextView) itemView.findViewById(R.id.comment); } } private class CommentAdapter extends RecyclerView.Adapter<CommentHolder> { private final LayoutInflater mInflater; public CommentAdapter() { mInflater = getLayoutInflater2(); AssertUtils.assertNotNull(mInflater); } @Override public CommentHolder onCreateViewHolder(ViewGroup parent, int viewType) { return new CommentHolder(mInflater.inflate(R.layout.item_gallery_comment, parent, false)); } public CharSequence generateComment(Context context, ObservedTextView textView, GalleryComment comment) { SpannableStringBuilder ssb = Html.fromHtml(comment.comment, new URLImageGetter(textView, EhApplication.getConaco(context)), null); if (0 != comment.id && 0 != comment.score) { int score = comment.score; String scoreString = score > 0 ? "+" + score : Integer.toString(score); SpannableString ss = new SpannableString(scoreString); ss.setSpan(new RelativeSizeSpan(0.8f), 0, scoreString.length(), Spannable.SPAN_EXCLUSIVE_EXCLUSIVE); ss.setSpan(new StyleSpan(Typeface.BOLD), 0, scoreString.length(), Spannable.SPAN_EXCLUSIVE_EXCLUSIVE); ss.setSpan(new ForegroundColorSpan(AttrResources.getAttrColor(context, android.R.attr.textColorSecondary)) , 0, scoreString.length(), Spannable.SPAN_EXCLUSIVE_EXCLUSIVE); ssb.append(" ").append(ss); } return TextUrl.handleTextUrl(ssb); } @Override public void onBindViewHolder(CommentHolder holder, int position) { Context context = getContext2(); if (null == context || null == mComments) { return; } GalleryComment comment = mComments[position]; holder.user.setText(comment.user); holder.time.setText(comment.time); holder.comment.setText(generateComment(context, holder.comment, comment)); } @Override public int getItemCount() { return mComments == null ? 0 : mComments.length; } } private void onCommentGallerySuccess(GalleryComment[] result) { if (null == mAdapter) { return; } mComments = result; mAdapter.notifyDataSetChanged(); Bundle re = new Bundle(); re.putParcelableArray(KEY_COMMENTS, result); setResult(SceneFragment.RESULT_OK, re); // Remove text if (mEditText != null) { mEditText.setText(""); } updateView(true); } private void onVoteCommentSuccess(VoteCommentParser.Result result) { if (null == mAdapter || null == mComments) { return; } int position = -1; for (int i = 0, n = mComments.length; i < n; i++) { GalleryComment comment = mComments[i]; if (comment.id == result.id) { position = i; break; } } if (-1 == position) { Log.d(TAG, "Can't find comment with id " + result.id); return; } // Update comment GalleryComment comment = mComments[position]; comment.score = result.score; if (result.expectVote > 0) { comment.voteUp = 0 != result.vote; comment.voteDown = false; } else { comment.voteDown = 0 != result.vote; comment.voteUp = false; } mAdapter.notifyItemChanged(position); Bundle re = new Bundle(); re.putParcelableArray(KEY_COMMENTS, mComments); setResult(SceneFragment.RESULT_OK, re); } private static class CommentGalleryListener extends EhCallback<GalleryCommentsScene, GalleryComment[]> { public CommentGalleryListener(Context context, int stageId, String sceneTag) { super(context, stageId, sceneTag); } @Override public void onSuccess(GalleryComment[] result) { showTip(R.string.comment_successfully, LENGTH_SHORT); GalleryCommentsScene scene = getScene(); if (scene != null) { scene.onCommentGallerySuccess(result); } } @Override public void onFailure(Exception e) { showTip(getContent().getString(R.string.comment_failed) + "\n" + ExceptionUtils.getReadableString(e), LENGTH_LONG); } @Override public void onCancel() { } @Override public boolean isInstance(SceneFragment scene) { return scene instanceof GalleryCommentsScene; } } private static class VoteCommentListener extends EhCallback<GalleryCommentsScene, VoteCommentParser.Result> { public VoteCommentListener(Context context, int stageId, String sceneTag) { super(context, stageId, sceneTag); } @Override public void onSuccess(VoteCommentParser.Result result) { showTip(result.expectVote > 0 ? (0 != result.vote ? R.string.vote_up_successfully : R.string.cancel_vote_up_successfully) : (0 != result.vote ? R.string.vote_down_successfully : R.string.cancel_vote_down_successfully), LENGTH_SHORT); GalleryCommentsScene scene = getScene(); if (scene != null) { scene.onVoteCommentSuccess(result); } } @Override public void onFailure(Exception e) { showTip(R.string.vote_failed, LENGTH_LONG); } @Override public void onCancel() { } @Override public boolean isInstance(SceneFragment scene) { return scene instanceof GalleryCommentsScene; } } @Override public void loadSource() { } }