diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml index 50de3686bd..41bcf55a06 100644 --- a/app/src/main/AndroidManifest.xml +++ b/app/src/main/AndroidManifest.xml @@ -815,6 +815,14 @@ + + + + () { - override fun onSuccess(data: ResponseBody) { - ToastUtils.toast("举报成功") - } + interface CommunityReporter { - override fun onFailure(exception: Exception) { - super.onFailure(exception) - if (exception is HttpException) { - ErrorHelper.handleError( - HaloApp.getInstance().application, - exception.response().errorBody()?.string() - ) + fun createReportSingle(content: JSONObject): Single + + @SuppressLint("CheckResult") + fun postReport(content: JSONObject) { + createReportSingle(content) + .compose(singleToMain()) + .subscribe(object : BiResponse() { + override fun onSuccess(data: ResponseBody) { + ToastUtils.toast("举报成功") } - } - }) + + override fun onFailure(exception: Exception) { + super.onFailure(exception) + if (exception is HttpException) { + ErrorHelper.handleError( + HaloApp.getInstance().application, + exception.response().errorBody()?.string() + ) + } + } + }) + } + } + + class PostsReporter(private val contentId: String) : CommunityReporter { + + override fun createReportSingle(content: JSONObject): Single = + RetrofitManager.getInstance() + .api + .postBbsReport(contentId, content.toRequestBody()) + } + + class ImageArticleReporter(private val contentId: String) : CommunityReporter { + + override fun createReportSingle(content: JSONObject): Single = + RetrofitManager.getInstance().api + .postImageArticleReport(contentId, content.toRequestBody()) } } \ No newline at end of file diff --git a/app/src/main/java/com/gh/common/util/CommentHelper.kt b/app/src/main/java/com/gh/common/util/CommentHelper.kt index 667f987aa1..f5a3d0ba63 100644 --- a/app/src/main/java/com/gh/common/util/CommentHelper.kt +++ b/app/src/main/java/com/gh/common/util/CommentHelper.kt @@ -119,6 +119,23 @@ object CommentHelper { ) } + fun showImageArticleCommentOptions( + view: View, + commentEntity: CommentEntity, + imageArticleId: String, + isShowTop: Boolean = false, + listener: OnCommentOptionClickListener? + ) { + showCommentOptions( + view, + commentEntity, + imageArticleId = imageArticleId, + showConversation = false, + isShowTopOrAccept = isShowTop, + listener = listener + ) + } + private fun showCommentOptions( view: View, commentEntity: CommentEntity, @@ -129,6 +146,7 @@ object CommentHelper { questionId: String? = null, videoId: String? = null, gameCollectionId: String? = null, + imageArticleId: String? = null, isShowTopOrAccept: Boolean = false, ignoreModerator: Boolean = false, isVideoAuthor: Boolean = false, @@ -137,8 +155,7 @@ object CommentHelper { val context = view.context val dialogOptions = ArrayList() val isContentAuthor = commentEntity.me?.isContentAuthor == true - - if (isShowTopOrAccept && (articleId != null || questionId != null || videoId != null) && isContentAuthor) { + if (isShowTopOrAccept && (articleId != null || questionId != null || videoId != null || imageArticleId != null) && isContentAuthor) { dialogOptions.add(if (commentEntity.isTop) "取消置顶" else "置顶") } if (isShowTopOrAccept && questionId != null && isContentAuthor) { @@ -229,6 +246,7 @@ object CommentHelper { commentListener ) } + articleId != null -> { PostCommentUtils.reportCommunityArticleComment( commentEntity.id, @@ -236,6 +254,7 @@ object CommentHelper { commentListener ) } + questionId != null -> { PostCommentUtils.reportQuestionComment( questionId, @@ -244,6 +263,7 @@ object CommentHelper { commentListener ) } + gameCollectionId != null -> { PostCommentUtils.reportGameCollectionComment( gameCollectionId, @@ -252,6 +272,15 @@ object CommentHelper { commentListener ) } + + imageArticleId != null -> { + PostCommentUtils.reportImageArticleComment( + commentEntity.id, + reportType, + commentListener + ) + } + else -> { PostCommentUtils.reportVideoComment( videoId, @@ -287,6 +316,17 @@ object CommentHelper { null ) ) + } else if (imageArticleId != null) { + context.startActivity( + CommentDetailActivity + .getImageArticleCommentIntent( + context, + imageArticleId, + commentEntity.id, + communityId, + null + ) + ) } else { context.startActivity( CommentDetailActivity diff --git a/app/src/main/java/com/gh/common/util/CommentUtils.java b/app/src/main/java/com/gh/common/util/CommentUtils.java index af4da8c50d..0d45262734 100644 --- a/app/src/main/java/com/gh/common/util/CommentUtils.java +++ b/app/src/main/java/com/gh/common/util/CommentUtils.java @@ -305,6 +305,7 @@ public class CommentUtils { String articleCommunityId, String videoId, String questionId, + String imageArticleId, final CommentEntity commentEntity, final TextView commentLikeCountTv, final ImageView commentLikeIv, @@ -328,7 +329,7 @@ public class CommentUtils { commentLikeCountTv.setText(NumberUtils.transSimpleCount(commentEntity.getVote())); commentLikeCountTv.setVisibility(View.VISIBLE); - PostCommentUtils.likeComment(answerId, articleId, videoId, questionId, commentEntity.getId(), + PostCommentUtils.likeComment(answerId, articleId, videoId, questionId, imageArticleId, commentEntity.getId(), new PostCommentUtils.PostCommentListener() { @Override public void postSuccess(JSONObject response) { @@ -374,6 +375,7 @@ public class CommentUtils { String articleId, String articleCommunityId, String videoId, + String imageArticleId, final CommentEntity commentEntity, final TextView commentLikeCountTv, final ImageView commentLikeIv, @@ -382,7 +384,7 @@ public class CommentUtils { String entrance = "视频流-评论-点赞"; CheckLoginUtils.checkLogin(context, entrance, () -> { - PostCommentUtils.likeComment(answerId, articleId, videoId, "", commentEntity.getId(), + PostCommentUtils.likeComment(answerId, articleId, videoId, "", imageArticleId, commentEntity.getId(), new PostCommentUtils.PostCommentListener() { @Override public void postSuccess(JSONObject response) { diff --git a/app/src/main/java/com/gh/common/util/DirectUtils.kt b/app/src/main/java/com/gh/common/util/DirectUtils.kt index 4b0583a25b..5bb60656f8 100644 --- a/app/src/main/java/com/gh/common/util/DirectUtils.kt +++ b/app/src/main/java/com/gh/common/util/DirectUtils.kt @@ -568,6 +568,10 @@ object DirectUtils { } } + "image_article" -> ARouter.getInstance().build(RouteConsts.activity.imageArticleDetailActivity) + .withString(KEY_IMAGE_ARTICLE_ID, linkEntity.link) + .navigation() + "" -> { // do nothing } diff --git a/app/src/main/java/com/gh/common/util/PostCommentUtils.java b/app/src/main/java/com/gh/common/util/PostCommentUtils.java index 8e0b9e94ca..7ce612976e 100644 --- a/app/src/main/java/com/gh/common/util/PostCommentUtils.java +++ b/app/src/main/java/com/gh/common/util/PostCommentUtils.java @@ -77,6 +77,7 @@ public class PostCommentUtils { String articleId, String videoId, String questionId, + final String imageArticleId, final String commentId, final PostCommentListener listener) { @@ -88,6 +89,8 @@ public class PostCommentUtils { observable = RetrofitManager.getInstance().getApi().postVoteCommunityArticleComment(commentId); } else if (!TextUtils.isEmpty(questionId)) { observable = RetrofitManager.getInstance().getApi().postVoteQuestionComment(questionId, commentId); + } else if (!TextUtils.isEmpty(imageArticleId)) { + observable = RetrofitManager.getInstance().getApi().postVoteToImageArticle(commentId); } else { observable = RetrofitManager.getInstance().getApi().postVoteToVideo(videoId, commentId); } @@ -116,6 +119,7 @@ public class PostCommentUtils { String articleCommunityId, String videoId, String questionId, + String imageArticleId, final String commentId, final PostCommentListener listener) { Observable observable; @@ -124,6 +128,8 @@ public class PostCommentUtils { observable = RetrofitManager.getInstance().getApi().postUnVoteQuestionComment(questionId, commentId); } else if (!TextUtils.isEmpty(articleId)) { observable = RetrofitManager.getInstance().getApi().postUnVoteArticleComment(commentId); + } else if (!TextUtils.isEmpty(imageArticleId)) { + observable = RetrofitManager.getInstance().getApi().postUnVoteImageArticle(commentId); } else { observable = RetrofitManager.getInstance().getApi().postUnVoteVideoComment(videoId, commentId); } @@ -300,6 +306,25 @@ public class PostCommentUtils { }); } + public static void reportImageArticleComment(String commentId, String reportData, PostCommentListener listener) { + RequestBody body = RequestBody.create(MediaType.parse("application/json"), reportData); + RetrofitManager.getInstance().getApi() + .reportImageArticleComment(commentId, body) + .subscribeOn(Schedulers.io()) + .observeOn(AndroidSchedulers.mainThread()) + .subscribe(new Response() { + @Override + public void onResponse(ResponseBody response) { + listener.postSuccess(null); + } + + @Override + public void onFailure(HttpException e) { + listener.postFailed(e); + } + }); + } + public interface PostCommentListener { void postSuccess(JSONObject response); diff --git a/app/src/main/java/com/gh/common/view/RedBookBannerIndicatorView.kt b/app/src/main/java/com/gh/common/view/RedBookBannerIndicatorView.kt new file mode 100644 index 0000000000..5e5022be67 --- /dev/null +++ b/app/src/main/java/com/gh/common/view/RedBookBannerIndicatorView.kt @@ -0,0 +1,268 @@ +package com.gh.common.view + +import android.animation.Animator +import android.animation.ValueAnimator +import android.content.Context +import android.graphics.Canvas +import android.graphics.Paint +import android.util.AttributeSet +import android.view.View +import com.gh.gamecenter.R +import com.gh.gamecenter.common.utils.dip2px +import com.gh.gamecenter.common.utils.toColor + +class RedBookBannerIndicatorView @JvmOverloads constructor( + context: Context, + attrs: AttributeSet? = null, + defStyleAttr: Int = 0, +) : View(context, attrs, defStyleAttr) { + + private var defaultColor = R.color.text_instance.toColor(context) + private var selectedColor = R.color.primary_theme.toColor(context) + private var largeRadius = 2F.dip2px().toFloat() + private var smallRadius = 1.2F.dip2px().toFloat() + private var distanceBetweenCenters = 8F.dip2px().toFloat() + private var maxShowDotCount = 5 + + private var lastPosition = 0 + private var currentPosition = 0 + private var firstShowPosition = 0 + private var lastShowPosition = 0 + + private var animationProgress = 0F + + private var animationState: AnimationState = AnimationState.No + + private var valueAnimator: ValueAnimator? = null + + private val paint by lazy { + Paint().apply { + style = Paint.Style.FILL + isAntiAlias = true + } + } + + init { + initAttrs(context, attrs) + } + + private fun initAttrs(context: Context, attrs: AttributeSet?) { + val typedArray = context.obtainStyledAttributes(attrs, R.styleable.RedBookBannerIndicatorView) + defaultColor = typedArray.getColor(R.styleable.RedBookBannerIndicatorView_default_color, defaultColor) + selectedColor = typedArray.getColor(R.styleable.RedBookBannerIndicatorView_selected_color, selectedColor) + largeRadius = typedArray.getDimension(R.styleable.RedBookBannerIndicatorView_large_radius, largeRadius) + smallRadius = typedArray.getDimension(R.styleable.RedBookBannerIndicatorView_small_radius, smallRadius) + distanceBetweenCenters = typedArray.getDimension( + R.styleable.RedBookBannerIndicatorView_distance_between_centers, + distanceBetweenCenters + ) + maxShowDotCount = + typedArray.getInt(R.styleable.RedBookBannerIndicatorView_max_show_dot_count, maxShowDotCount) + typedArray.recycle() + } + + override fun onMeasure(widthMeasureSpec: Int, heightMeasureSpec: Int) { + val width = if (lastPosition >= maxShowDotCount) { + (maxShowDotCount - 1) * distanceBetweenCenters + 2 * largeRadius + } else { + lastShowPosition * distanceBetweenCenters + 2 * largeRadius + } + val height = largeRadius * 2 + setMeasuredDimension(width.toInt(), height.toInt()) + } + + fun show(total: Int) { + valueAnimator?.cancel() + lastPosition = total - 1 + lastShowPosition = if (total > maxShowDotCount) { + maxShowDotCount - 1 + } else { + lastPosition + } + requestLayout() + } + + /** + * 请注意,selectPosition只能一步一步跳 + * 比如:当currentPosition ==0 时,selectPosition为2,仍然只会往前跳一步,最终 currentPosition == 1 + */ + fun selectPosition(position: Int) { + if (currentPosition == position || position < 0 || position > lastPosition) { + return + } + valueAnimator?.cancel() + if (position > currentPosition) { + currentPosition++ + } else if (position < currentPosition) { + currentPosition-- + } + + when (currentPosition) { + 0 -> { + firstShowPosition = 0 + lastShowPosition = maxShowDotCount - 1 + if (lastShowPosition > lastPosition) { + lastShowPosition = lastPosition + } + } + + lastPosition -> { + lastShowPosition = lastPosition + firstShowPosition = lastPosition - maxShowDotCount + 1 + if (firstShowPosition < 0) { + firstShowPosition = (0) + } + } + + firstShowPosition -> { + animationState = AnimationState.RightReady + } + + lastShowPosition -> { + animationState = AnimationState.LeftReady + } + + } + invalidate() + } + + override fun onDraw(canvas: Canvas) { + super.onDraw(canvas) + // 保存 Canvas 的当前状态 + canvas.save(); + + // 限制绘制区域为 View 的边界 + canvas.clipRect(0, 0, getWidth(), getHeight()); + if (lastShowPosition > firstShowPosition) { + val first = if (firstShowPosition > 0) { + firstShowPosition - 1 + } else { + firstShowPosition + } + val last = if (lastShowPosition < lastPosition) { + lastShowPosition + 1 + } else { + lastPosition + } + + + for (position in first..last) { + var radius = when (position) { + firstShowPosition -> if (firstShowPosition == 0) { + largeRadius + } else { + smallRadius + } + + lastShowPosition -> if (lastShowPosition == lastPosition) { + largeRadius + } else { + smallRadius + } + + else -> largeRadius + } + + val offset = if (animationState == AnimationState.LeftPlaying) { + -animationProgress * distanceBetweenCenters + } else { + animationProgress * distanceBetweenCenters + } + val centerX = ((position - firstShowPosition) * distanceBetweenCenters + largeRadius) + offset + paint.color = if (position == currentPosition) { + selectedColor + } else { + defaultColor + } + + radius = getScaleRadius(position, radius) + canvas.drawCircle(centerX, largeRadius, radius, paint) + } + + if (animationState == AnimationState.LeftReady || animationState == AnimationState.RightReady) { + startAnimation() + } + } + // 恢复 Canvas 到之前的状态 + canvas.restore(); + } + + private fun getScaleRadius(position: Int, radius: Float): Float { + var newRadius = radius + if (animationState == AnimationState.LeftPlaying) { + if (position - 1 == firstShowPosition) { + newRadius = largeRadius - (largeRadius - smallRadius) * animationProgress + } + if (position == lastShowPosition) { + newRadius = smallRadius + (largeRadius - smallRadius) * animationProgress + } + } + if (animationState == AnimationState.RightPlaying) { + if (position == firstShowPosition) { + newRadius = smallRadius + (largeRadius - smallRadius) * animationProgress + } + if (position + 1 == lastShowPosition) { + newRadius = largeRadius - (largeRadius - smallRadius) * animationProgress + } + } + return newRadius + } + + /** + * 执行向左的平移动画 + */ + private fun startAnimation() { + animationState = if (animationState == AnimationState.LeftReady) { + AnimationState.LeftPlaying + } else { + AnimationState.RightPlaying + } + fun resetShowPosition() { + if (animationState == AnimationState.LeftPlaying) { + firstShowPosition++ + lastShowPosition++ + } else { + firstShowPosition-- + lastShowPosition-- + } + animationState = AnimationState.No + animationProgress = 0F + invalidate() + } + valueAnimator = ValueAnimator.ofFloat(0F, 1F).apply { + setDuration(ANIMATION_DURATION) + addUpdateListener { + val progress = it.animatedValue as Float + animationProgress = progress + invalidate() + } + addListener(object : Animator.AnimatorListener { + override fun onAnimationStart(animation: Animator) = Unit + + override fun onAnimationEnd(animation: Animator) { + resetShowPosition() + } + + override fun onAnimationCancel(animation: Animator) { + resetShowPosition() + } + + override fun onAnimationRepeat(animation: Animator) = Unit + + }) + start() + } + } + + companion object { + private const val ANIMATION_DURATION = 300L + } + + sealed class AnimationState { + object No : AnimationState() + object LeftReady : AnimationState() + object RightReady : AnimationState() + object LeftPlaying : AnimationState() + object RightPlaying : AnimationState() + } +} \ No newline at end of file diff --git a/app/src/main/java/com/gh/common/view/VoteStateView.kt b/app/src/main/java/com/gh/common/view/VoteStateView.kt new file mode 100644 index 0000000000..a9c8bca371 --- /dev/null +++ b/app/src/main/java/com/gh/common/view/VoteStateView.kt @@ -0,0 +1,107 @@ +package com.gh.common.view + +import android.content.Context +import android.util.AttributeSet +import android.view.LayoutInflater +import android.view.View +import android.view.View.OnClickListener +import android.widget.LinearLayout +import com.gh.gamecenter.R +import com.gh.gamecenter.common.retrofit.BiResponse +import com.gh.gamecenter.common.utils.doOnAnimationEnd +import com.gh.gamecenter.common.utils.singleToMain +import com.gh.gamecenter.common.utils.toColor +import com.gh.gamecenter.common.utils.toResString +import com.gh.gamecenter.core.utils.ToastUtils +import com.gh.gamecenter.databinding.LayoutVoteBinding +import com.gh.gamecenter.entity.VoteEntity +import com.gh.gamecenter.forum.home.recommend.ImageArticleUseCase.Companion.notifyVoteChanged +import com.gh.gamecenter.livedata.Event + +class VoteStateView @JvmOverloads constructor( + context: Context, + attrs: AttributeSet? = null, + defStyleAttr: Int = 0 +) : LinearLayout(context, attrs, defStyleAttr) { + + private var binding: LayoutVoteBinding = LayoutVoteBinding.inflate(LayoutInflater.from(context), this, true) + + private var status = IMAGE_ARTICLE_PENDING + + private var isAnimatorPlaying = false + + private var outClickListener: OnClickListener? = null + + private val inClickListener = OnClickListener { + if (isAnimatorPlaying) { + return@OnClickListener + } + when (status) { + IMAGE_ARTICLE_PENDING -> { + ToastUtils.showToast(R.string.content_pending_status.toResString()) + } + + IMAGE_ARTICLE_FAIL -> { + ToastUtils.showToast(R.string.fail_status.toResString()) + } + + IMAGE_ARTICLE_PASS -> { + outClickListener?.onClick(it) + if (!binding.ivVote.isChecked) { + playVoteAnimation() + } + } + } + + } + + fun setVote(isVote: Boolean, count: Int, status: String) { + this.status = status + binding.ivVote.isChecked = isVote + binding.tvVoteCount.text = getVoteCountText(count, context) + val textColorResId = if (isVote) { + R.color.text_theme + } else { + R.color.text_secondary + } + binding.tvVoteCount.setTextColor(textColorResId.toColor(context)) + } + + override fun setOnClickListener(l: OnClickListener?) { + outClickListener = l + super.setOnClickListener(inClickListener) + } + + private fun getVoteCountText(count: Int, context: Context) = + when { + count <= 0 -> R.string.like.toResString() + count in 1 until VOTE_TEN_THOUSAND -> "$count" + else -> context.getString(R.string.ten_thousand, "$count") + } + + private fun playVoteAnimation() { + with(binding) { + isAnimatorPlaying = true + tvVoteCount.setTextColor(R.color.text_theme.toColor(context)) + ivVote.isChecked = true + ivVote.visibility = View.INVISIBLE + voteAnimation.visibility = View.VISIBLE + voteAnimation.setAnimation("lottie/community_vote.json") + voteAnimation.playAnimation() + voteAnimation.doOnAnimationEnd { + voteAnimation.visibility = View.GONE + ivVote.visibility = View.VISIBLE + isAnimatorPlaying = false + } + } + + } + + companion object { + private const val VOTE_TEN_THOUSAND = 10000 + + private const val IMAGE_ARTICLE_PENDING = "pending" + private const val IMAGE_ARTICLE_FAIL = "fail" + private const val IMAGE_ARTICLE_PASS = "pass" + } +} \ No newline at end of file diff --git a/app/src/main/java/com/gh/gamecenter/CommentDetailActivity.java b/app/src/main/java/com/gh/gamecenter/CommentDetailActivity.java index cbde880161..7add7c0268 100644 --- a/app/src/main/java/com/gh/gamecenter/CommentDetailActivity.java +++ b/app/src/main/java/com/gh/gamecenter/CommentDetailActivity.java @@ -76,6 +76,19 @@ public class CommentDetailActivity extends ToolBarActivity { return getTargetIntent(context, CommentDetailActivity.class, NewCommentConversationFragment.class, args); } + public static Intent getImageArticleCommentIntent(Context context, + String articleId, + String articleCommentId, + String communityId, + LinkEntity linkEntity) { + Bundle args = new Bundle(); + args.putString(CommentActivity.IMAGE_ARTICLE_ID, articleId); + args.putString(EntranceConsts.KEY_COMMENTID, articleCommentId); + args.putString(CommentActivity.COMMUNITY_ID, communityId); + args.putParcelable(EntranceConsts.KEY_LINK, linkEntity); + return getTargetIntent(context, CommentDetailActivity.class, NewCommentConversationFragment.class, args); + } + public static Intent getVideoCommentIntent(Context context, String commentId, String videoId, diff --git a/app/src/main/java/com/gh/gamecenter/entity/ImageArticleEntity.kt b/app/src/main/java/com/gh/gamecenter/entity/ImageArticleEntity.kt new file mode 100644 index 0000000000..7314acf2c5 --- /dev/null +++ b/app/src/main/java/com/gh/gamecenter/entity/ImageArticleEntity.kt @@ -0,0 +1,224 @@ +package com.gh.gamecenter.entity + +import android.os.Parcelable +import com.gh.gamecenter.common.entity.CommunityEntity +import com.gh.gamecenter.feature.entity.* +import com.gh.gamecenter.feature.entity.TimeEntity +import com.google.gson.annotations.SerializedName +import kotlinx.parcelize.IgnoredOnParcel +import kotlinx.parcelize.Parcelize + +@Parcelize +data class ImageArticleEntity( + @SerializedName("_id") + private var _id: String? = null, + @SerializedName("title") + private var _title: String? = null, + @SerializedName("content") + private val _content: String? = null, + @SerializedName("community_id") + private var _communityId: String? = null, + @SerializedName("community_type") + private val _communityType: String? = null, + @SerializedName("sections") + private val _sections: List? = null, + @SerializedName("images") + private var _images: List? = null, + @SerializedName("images_info") + private val _imagesInfos: List? = null, + @SerializedName("community") + val community: Community? = null, + @SerializedName("user") + private val _user: UserEntity? = null, + @SerializedName("time") + private val _time: TimeEntity? = null, + @SerializedName("status") + private val _status: String? = null, + @SerializedName("source") + private val _source: SourceEntity? = null, + @SerializedName("count") + private val _count: Count? = null, + @SerializedName("me") + private val _me: MeEntity? = null, + @SerializedName("_seq") + private val _shortId: String? = null, + @SerializedName("choiceness_status") + private var _choicenessStatus: String? = null,// 精选状态 apply(申请), pass already(已精选) cancel not_yet(未精选) + /** + * 临时变量,区分是编辑发布还是编辑草稿 + */ + var isDraft: Boolean = false +) : Parcelable { + + val id: String + get() = _id ?: "" + + val title: String + get() = _title ?: "" + + val content: String + get() = _content ?: "" + + val communityId: String + get() = _communityId ?: "" + + val communityType: String + get() = _communityType ?: "" + + val sections: List + get() = _sections ?: emptyList() + + val images: List + get() = _images ?: emptyList() + + val imagesInfos: List + get() = _imagesInfos ?: emptyList() + + val user: UserEntity + get() = _user ?: UserEntity() + + val time: TimeEntity + get() = _time ?: TimeEntity() + + val status: String + get() = _status ?: "pending" + + val source: SourceEntity + get() = _source ?: SourceEntity() + + val count: Count + get() = _count ?: Count() + + val me: MeEntity + get() = _me ?: MeEntity() + + val shortId: String + get() = _shortId ?: "" + + var choicenessStatus: String + get() = _choicenessStatus ?: "" + set(value) { + _choicenessStatus = value + } + + fun getSimplifyChoicenessStatus(): String { + return when (choicenessStatus) { + "already" -> "pass" + "not_yet" -> "cancel" + else -> choicenessStatus ?: "" + } + } + + companion object { + + const val IMAGE_ARTICLE_TYPE = "image_article" + + fun getImageRadio(imageInfo: ImageInfo?, minRadio: Float, maxRadio: Float): Float { + return if (imageInfo != null && imageInfo.width > 0 && imageInfo.height > 0) { + val imageRadio = imageInfo.height.toFloat() / imageInfo.width + when { + imageRadio < minRadio -> { + minRadio + } + + imageRadio in 0.5..1.33 -> { + imageRadio + } + + else -> { + maxRadio + } + } + } else { + minRadio + } + + } + } + + @Parcelize + data class Community( + @SerializedName("_id") + private val _id: String? = null, + @SerializedName("name") + private val _name: String? = null, + @SerializedName("icon") + private val _icon: String? = null, + @SerializedName("hot") + private val _hot: Int? = null, + @SerializedName("game") + val game: GameEntity? = null, + @SerializedName("type") + private val _type: String? = null, + @SerializedName("me") + private val _me: MeEntity? + ) : Parcelable { + + val id: String + get() = _id ?: "" + + val name: String + get() = _name ?: "" + + val icon: String + get() = _icon ?: "" + + val hot: Int + get() = _hot ?: 0 + + val type: String + get() = _type ?: "game_bbs" + + val typeChinese: String + get() = when (type) { + "game_bbs" -> "游戏论坛" + else -> "综合论坛" + } + + var isFollowed: Boolean + get() = _me?.isFollower ?: false + set(value) { + _me?.isFollower = value + } + + fun toCommunityEntity() = CommunityEntity( + id = id, + name = name, + icon = icon, + type = type, + ) + } + + fun toPersonHistoryEntity() = PersonalHistoryEntity().also { + it.id = id + it.title = title + it.brief = content + it.community = community?.toCommunityEntity() ?: CommunityEntity() + it.sections = sections + it.images = images + it.imagesInfo = imagesInfos + it.count = count + it.user = user + it.time = time.update + it.status = status + it.me = me + } + + fun toAnswerEntity() = AnswerEntity().also { + it.id = id + it.title = title + it.brief = content + it.content = content + it.community = community?.toCommunityEntity() ?: CommunityEntity() + it.sections = sections + it.images = images + it.imagesInfo = imagesInfos + it.count = count + it.user = user + it.time = time.update + it.status = status + it.me = me + it.type = IMAGE_ARTICLE_TYPE + } + +} \ No newline at end of file diff --git a/app/src/main/java/com/gh/gamecenter/entity/PersonalHistoryEntity.kt b/app/src/main/java/com/gh/gamecenter/entity/PersonalHistoryEntity.kt index 82516c3a2d..d2d69edbfb 100644 --- a/app/src/main/java/com/gh/gamecenter/entity/PersonalHistoryEntity.kt +++ b/app/src/main/java/com/gh/gamecenter/entity/PersonalHistoryEntity.kt @@ -36,12 +36,13 @@ data class PersonalHistoryEntity( var length: Long = 0, @SerializedName("count") private var _count: Count = Count(), - val time: Long = 0, + var time: Long = 0, @SerializedName("title") + private var _title: String = "", var description: String = "", @SerializedName("community") - private var _community: CommunityEntity = CommunityEntity(), + private var _community: CommunityEntity? = null, var videos: List = ArrayList(), @SerializedName("user") private var _user: UserEntity? = null, @@ -63,8 +64,17 @@ data class PersonalHistoryEntity( @SerializedName("sections") @Ignore private var _sections: List = ArrayList(), + + // 图文新增字段 + @SerializedName("content") + private val _content: String? = null, + @SerializedName("source") + private val _source: SourceEntity? = null, ) : Parcelable, CommunityItemData { + val content: String + get() = _content ?: "" + override var id: String get() = _id set(value) { @@ -104,7 +114,7 @@ data class PersonalHistoryEntity( } override var community: CommunityEntity - get() = _community + get() = _community ?: CommunityEntity() set(value) { _community = value } diff --git a/app/src/main/java/com/gh/gamecenter/entity/PublishImageTextRequest.kt b/app/src/main/java/com/gh/gamecenter/entity/PublishImageTextRequest.kt new file mode 100644 index 0000000000..120e146473 --- /dev/null +++ b/app/src/main/java/com/gh/gamecenter/entity/PublishImageTextRequest.kt @@ -0,0 +1,17 @@ +package com.gh.gamecenter.entity + +import com.google.gson.annotations.SerializedName + +data class PublishImageTextRequest( + val title: String, + val content: String, + @SerializedName("community_type") + val communityType: String, + @SerializedName("community_id") + val communityId: String, + @SerializedName("section_id") + val sectionId: List, + var images: List = listOf(), + @SerializedName("draft_id") + val draftId: String = "" +) diff --git a/app/src/main/java/com/gh/gamecenter/entity/VoteEntity.kt b/app/src/main/java/com/gh/gamecenter/entity/VoteEntity.kt index 07d175c4d3..bd9a6b0221 100644 --- a/app/src/main/java/com/gh/gamecenter/entity/VoteEntity.kt +++ b/app/src/main/java/com/gh/gamecenter/entity/VoteEntity.kt @@ -2,8 +2,15 @@ package com.gh.gamecenter.entity import com.google.gson.annotations.SerializedName -class VoteEntity( - var vote: Int? = 0, +data class VoteEntity( + @SerializedName("vote") + private val _vote: Int? = null, @SerializedName("is_guide_follow") - var isGuideFollow: Boolean = false -) \ No newline at end of file + private var _isGuideFollow: Boolean? = null +) { + val vote: Int + get() = _vote ?: 0 + + val isGuideFollow: Boolean + get() = _isGuideFollow ?: false +} \ No newline at end of file diff --git a/app/src/main/java/com/gh/gamecenter/eventbus/EBImageArticleChanged.kt b/app/src/main/java/com/gh/gamecenter/eventbus/EBImageArticleChanged.kt new file mode 100644 index 0000000000..d4c8e6dc8e --- /dev/null +++ b/app/src/main/java/com/gh/gamecenter/eventbus/EBImageArticleChanged.kt @@ -0,0 +1,16 @@ +package com.gh.gamecenter.eventbus + +import com.gh.gamecenter.entity.ImageArticleEntity + +sealed class EBImageArticleChanged { + + data class VoteChanged(val id: String, val isVoted: Boolean, val count: Int) : EBImageArticleChanged() + + data class CommentChanged(val id: String, val count: Int) : EBImageArticleChanged() + + data class DataChanged(val data: ImageArticleEntity) : EBImageArticleChanged() + + data class DataDeleted(val imageArticleId: String) : EBImageArticleChanged() + + data class DataCreated(val imageArticle: ImageArticleEntity, val draftId: String = "") : EBImageArticleChanged() +} \ No newline at end of file diff --git a/app/src/main/java/com/gh/gamecenter/forum/detail/ForumArticleAskListAdapter.kt b/app/src/main/java/com/gh/gamecenter/forum/detail/ForumArticleAskListAdapter.kt index 2731b9ed89..e71f8f633b 100644 --- a/app/src/main/java/com/gh/gamecenter/forum/detail/ForumArticleAskListAdapter.kt +++ b/app/src/main/java/com/gh/gamecenter/forum/detail/ForumArticleAskListAdapter.kt @@ -6,6 +6,7 @@ import android.view.ViewGroup import android.widget.LinearLayout import androidx.core.content.ContextCompat import androidx.recyclerview.widget.RecyclerView +import com.alibaba.android.arouter.launcher.ARouter import com.gh.gamecenter.common.base.activity.BaseActivity import com.gh.gamecenter.common.constant.ItemViewType import com.gh.gamecenter.common.syncpage.ISyncAdapterHandler @@ -14,6 +15,8 @@ import com.gh.common.util.NewLogUtils import com.gh.gamecenter.R import com.gh.gamecenter.common.viewholder.FooterViewHolder import com.gh.gamecenter.common.baselist.ListAdapter +import com.gh.gamecenter.common.constant.EntranceConsts +import com.gh.gamecenter.common.constant.RouteConsts import com.gh.gamecenter.common.utils.goneIf import com.gh.gamecenter.databinding.CommunityAnswerItemBinding import com.gh.gamecenter.common.entity.CommunityEntity @@ -21,6 +24,7 @@ import com.gh.gamecenter.common.utils.toBinding import com.gh.gamecenter.common.utils.toColor import com.gh.gamecenter.common.utils.toDrawable import com.gh.gamecenter.databinding.ItemForumArticleHeadBinding +import com.gh.gamecenter.entity.ImageArticleEntity import com.gh.gamecenter.forum.home.ForumArticleAskItemViewHolder import com.gh.gamecenter.qa.article.detail.ArticleDetailActivity import com.gh.gamecenter.feature.entity.AnswerEntity @@ -47,7 +51,7 @@ class ForumArticleAskListAdapter( } override fun areItemsTheSame(oldItem: AnswerEntity?, newItem: AnswerEntity?): Boolean { - return oldItem?.id == newItem?.id + return oldItem?.id == newItem?.id && oldItem?.type == newItem?.type } override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): RecyclerView.ViewHolder { @@ -55,9 +59,11 @@ class ForumArticleAskListAdapter( ItemViewType.ITEM_HEADER -> { ForumArticleHeadViewHolder(parent.toBinding()) } + ItemViewType.ITEM_FOOTER -> { FooterViewHolder(mLayoutInflater.inflate(com.gh.gamecenter.common.R.layout.refresh_footerview, parent, false)) } + else -> { ForumArticleAskItemViewHolder( CommunityAnswerItemBinding.bind( @@ -95,7 +101,8 @@ class ForumArticleAskListAdapter( questions.answerCount = answer.count.answer answer.questions = questions } - if (path == "精华" && answer.type != "answer" && answer.type != "video") answer.type = "community_article" + if (path == "精华" && answer.type != "answer" && answer.type != "video") answer.type = + "community_article" if (path == "问答") answer.type = "question" if (path == "视频") answer.type = "video" @@ -148,6 +155,7 @@ class ForumArticleAskListAdapter( ) ) } + "video" -> { NewLogUtils.logForumDetailFeedContentClick( "click_forum_detail_content", @@ -166,6 +174,7 @@ class ForumArticleAskListAdapter( ) mContext.startActivity(ForumVideoDetailActivity.getIntent(mContext, answer.id, bbsId, "论坛详情-信息流")) } + "question" -> { NewLogUtils.logForumDetailFeedContentClick( "click_forum_detail_content", @@ -188,6 +197,7 @@ class ForumArticleAskListAdapter( ) ) } + "answer" -> { NewLogUtils.logForumDetailFeedContentClick( "click_forum_detail_content", @@ -215,9 +225,16 @@ class ForumArticleAskListAdapter( ) ) } + + ImageArticleEntity.IMAGE_ARTICLE_TYPE -> { + ARouter.getInstance().build(RouteConsts.activity.imageArticleDetailActivity) + .withString(EntranceConsts.KEY_IMAGE_ARTICLE_ID, answer.id) + .navigation() + } } } } + ItemViewType.ITEM_FOOTER -> { val footerViewHolder = holder as FooterViewHolder footerViewHolder.initItemPadding() @@ -225,6 +242,7 @@ class ForumArticleAskListAdapter( footerViewHolder.hint.textSize = 12f footerViewHolder.hint.setTextColor(ContextCompat.getColor(mContext, com.gh.gamecenter.common.R.color.aaaaaa)) } + ItemViewType.ITEM_HEADER -> { if (holder is ForumArticleHeadViewHolder) { val articleListHead = if (path == "全部") { diff --git a/app/src/main/java/com/gh/gamecenter/forum/detail/ForumArticleAskListFragment.kt b/app/src/main/java/com/gh/gamecenter/forum/detail/ForumArticleAskListFragment.kt index d7a09bd5a6..d2e171c0a8 100644 --- a/app/src/main/java/com/gh/gamecenter/forum/detail/ForumArticleAskListFragment.kt +++ b/app/src/main/java/com/gh/gamecenter/forum/detail/ForumArticleAskListFragment.kt @@ -10,6 +10,7 @@ import com.gh.gamecenter.R import com.gh.gamecenter.common.baselist.LazyListFragment import com.gh.gamecenter.common.constant.Constants import com.gh.gamecenter.common.constant.EntranceConsts +import com.gh.gamecenter.common.entity.CommunityEntity import com.gh.gamecenter.common.utils.safelyGetInRelease import com.gh.gamecenter.common.utils.toColor import com.gh.gamecenter.common.utils.tryCatchInRelease @@ -18,10 +19,13 @@ import com.gh.gamecenter.core.iinterface.IScrollable import com.gh.gamecenter.core.utils.MD5Utils import com.gh.gamecenter.databinding.FragmentForumArticleAskListBinding import com.gh.gamecenter.entity.ForumDetailEntity +import com.gh.gamecenter.entity.ImageArticleEntity import com.gh.gamecenter.eventbus.EBDeleteDetail +import com.gh.gamecenter.eventbus.EBImageArticleChanged import com.gh.gamecenter.eventbus.EBUserFollow import com.gh.gamecenter.feature.entity.AnswerEntity import com.gh.gamecenter.forum.home.ForumScrollCalculatorHelper +import com.gh.gamecenter.personalhome.home.UserHistoryAdapter.Companion.USER_HISTORY_PAYLOADS_VOTE_CHANGED import com.gh.gamecenter.video.detail.CustomManager import com.shuyu.gsyvideoplayer.video.base.GSYVideoView import org.greenrobot.eventbus.Subscribe @@ -132,6 +136,73 @@ class ForumArticleAskListFragment : LazyListFragment { + updateImageArticleItem(changed.id, true) { + val newCount = it.count + newCount.vote = changed.count + it.count = newCount + it.me.isCommunityArticleVote = changed.isVoted + } + } + + changed is EBImageArticleChanged.CommentChanged -> { + updateImageArticleItem(changed.id, true) { + val newCount = it.count + newCount.comment = changed.count + it.count = newCount + } + } + + changed is EBImageArticleChanged.DataDeleted -> { + mViewModel?.deleteImageArticle(changed.imageArticleId) + } + + changed is EBImageArticleChanged.DataChanged -> { + val imageArticle = changed.data + updateImageArticleItem(changed.data.id, false) { + it.title = imageArticle.title + it.brief = imageArticle.content + it.images = imageArticle.images + it.imagesInfo = imageArticle.imagesInfos + it.community = imageArticle.community?.toCommunityEntity() ?: CommunityEntity() + it.sections = imageArticle.sections + it.count = imageArticle.count + it.user = imageArticle.user + it.time = imageArticle.time.update + it.status = imageArticle.status + } + } + + changed is EBImageArticleChanged.DataCreated -> { + println("kayn -->DataCreated:${changed.imageArticle}") + mViewModel?.addNewestImageArticle(changed.imageArticle.toAnswerEntity()) + } + } + } + + private fun updateImageArticleItem( + id: String, + hasPayload: Boolean, + block: (AnswerEntity) -> Unit + ) { + val dataList = mAdapter?.entityList ?: return + val position = + dataList.indexOfFirst { it.type == ImageArticleEntity.IMAGE_ARTICLE_TYPE && it.id == id } + if (position != -1) { + val item = dataList[position] + block(item) + if (hasPayload) { + mAdapter?.notifyItemChanged(position, USER_HISTORY_PAYLOADS_VOTE_CHANGED) + } else { + mAdapter?.notifyItemChanged(position) + } + + } + } + override fun onLoadRefresh() { super.onLoadRefresh() if (::mBinding.isInitialized) { diff --git a/app/src/main/java/com/gh/gamecenter/forum/detail/ForumArticleAskListViewModel.kt b/app/src/main/java/com/gh/gamecenter/forum/detail/ForumArticleAskListViewModel.kt index caa540a02e..682722b5d4 100644 --- a/app/src/main/java/com/gh/gamecenter/forum/detail/ForumArticleAskListViewModel.kt +++ b/app/src/main/java/com/gh/gamecenter/forum/detail/ForumArticleAskListViewModel.kt @@ -4,11 +4,13 @@ import android.app.Application import androidx.lifecycle.ViewModel import androidx.lifecycle.ViewModelProvider import com.gh.gamecenter.common.baselist.ListViewModel +import com.gh.gamecenter.common.baselist.LoadType import com.gh.gamecenter.common.constant.EntranceConsts import com.gh.gamecenter.core.utils.PageSwitchDataHelper import com.gh.gamecenter.core.utils.SPUtils import com.gh.gamecenter.core.utils.UrlFilterUtils import com.gh.gamecenter.entity.ForumDetailEntity +import com.gh.gamecenter.entity.ImageArticleEntity import com.gh.gamecenter.feature.entity.ForumVideoEntity import com.gh.gamecenter.feature.entity.AnswerEntity import com.gh.gamecenter.retrofit.RetrofitManager @@ -58,9 +60,11 @@ class ForumArticleAskListViewModel(application: Application, val bbsId: String = } } + "精华" -> { RetrofitManager.getInstance().api.getEssenceForumList(bbsId, page) } + "问答" -> { RetrofitManager.getInstance().api.getAskForumList( bbsId, @@ -68,6 +72,7 @@ class ForumArticleAskListViewModel(application: Application, val bbsId: String = page ) } + else -> { RetrofitManager.getInstance().api.getVideoForumList( bbsId, @@ -94,6 +99,28 @@ class ForumArticleAskListViewModel(application: Application, val bbsId: String = } } + fun deleteImageArticle(imageArticleId: String) { + val oldData = mResultLiveData.value ?: return + val newData = oldData.toMutableList() + val hasRemoved = + newData.removeAll { it.type == ImageArticleEntity.IMAGE_ARTICLE_TYPE && it.id == imageArticleId } + if (hasRemoved) { + mResultLiveData.value = newData + } + } + + fun addNewestImageArticle(item: AnswerEntity) { + val oldData = mResultLiveData.value ?: return + val newData = oldData.toMutableList() + if (newData.isEmpty()) { + load(LoadType.REFRESH) + } else { + newData.add(1, item) + mResultLiveData.value = newData + } + + } + class Factory(private val bbsId: String, val path: String) : ViewModelProvider.NewInstanceFactory() { override fun create(modelClass: Class): T { return ForumArticleAskListViewModel(HaloApp.getInstance().application, bbsId, path) as T diff --git a/app/src/main/java/com/gh/gamecenter/forum/detail/ForumDetailFragment.kt b/app/src/main/java/com/gh/gamecenter/forum/detail/ForumDetailFragment.kt index c98302b75a..72b3802754 100644 --- a/app/src/main/java/com/gh/gamecenter/forum/detail/ForumDetailFragment.kt +++ b/app/src/main/java/com/gh/gamecenter/forum/detail/ForumDetailFragment.kt @@ -35,6 +35,7 @@ import androidx.recyclerview.widget.LinearLayoutManager import androidx.recyclerview.widget.RecyclerView import androidx.recyclerview.widget.RecyclerView.OnScrollListener import androidx.viewpager.widget.PagerAdapter +import com.alibaba.android.arouter.launcher.ARouter import com.ethanhua.skeleton.Skeleton import com.ethanhua.skeleton.ViewSkeletonScreen import com.facebook.drawee.drawable.ScalingUtils @@ -56,6 +57,7 @@ import com.gh.gamecenter.common.base.fragment.BaseLazyTabFragment import com.gh.gamecenter.common.callback.BiCallback import com.gh.gamecenter.common.constant.Constants import com.gh.gamecenter.common.constant.EntranceConsts +import com.gh.gamecenter.common.constant.RouteConsts import com.gh.gamecenter.common.databinding.ItemIconTabBinding import com.gh.gamecenter.common.databinding.PopupAllTabsBinding import com.gh.gamecenter.common.entity.CommunityEntity @@ -79,6 +81,7 @@ import com.gh.gamecenter.feature.entity.ForumVideoEntity import com.gh.gamecenter.feature.entity.GameEntity import com.gh.gamecenter.feature.entity.UserEntity import com.gh.gamecenter.forum.home.CommunityHomeFragment +import com.gh.gamecenter.forum.home.recommend.PublishImageArticleActivity import com.gh.gamecenter.forum.moderator.ApplyModeratorActivity import com.gh.gamecenter.forum.moderator.ModeratorListActivity import com.gh.gamecenter.forum.search.ForumOrUserSearchActivity @@ -265,12 +268,14 @@ class ForumDetailFragment : BaseLazyTabFragment(), IScrollable { if (!mIsFromTabWrapper) { ViewCompat.setOnApplyWindowInsetsListener(mBinding.forumAppbar) { _, insets -> - (mBinding.toolbar.layoutParams as MarginLayoutParams).topMargin = insets.getInsets(WindowInsetsCompat.Type.systemBars()).top + (mBinding.toolbar.layoutParams as MarginLayoutParams).topMargin = + insets.getInsets(WindowInsetsCompat.Type.systemBars()).top WindowInsetsCompat.CONSUMED } } if (mIsFromMainWrapper) { - (mBinding.toolbar.layoutParams as MarginLayoutParams).topMargin = DisplayUtils.getStatusBarHeight(requireContext().resources) + (mBinding.toolbar.layoutParams as MarginLayoutParams).topMargin = + DisplayUtils.getStatusBarHeight(requireContext().resources) } mBinding.toolbar.setNavigationOnClickListener { NewLogUtils.logForumDetailEnterOrClick("click_forum_detail_return", mBbsId, mBbsType) @@ -881,7 +886,7 @@ class ForumDetailFragment : BaseLazyTabFragment(), IScrollable { forumName = mForumDetail?.name ?: "", bbsType = mForumDetail?.typeChinese ?: "", buttonName = mBinding.followTv.text.toString(), - gameForumType = mForumDetail?.game?.categoryChinese?:"" + gameForumType = mForumDetail?.game?.categoryChinese ?: "" ) ifLogin(mEntrance) { val forumEntity = ForumEntity( @@ -1089,6 +1094,23 @@ class ForumDetailFragment : BaseLazyTabFragment(), IScrollable { } } + contentView.findViewById(R.id.community_edit_recommend_container).setOnClickListener { + val icon = if ("official_bbs" == mForumDetail?.type) { + mForumDetail?.icon ?: "" + } else { + mForumDetail?.game?.getIcon() + } + context?.ifLogin("论坛详情-发布-图文动态", action = { + ARouter.getInstance().build(RouteConsts.activity.publishImageArticleActivity) + .withParcelable( + EntranceConsts.KEY_COMMUNITY_DATA, + CommunityEntity(mBbsId, name = mForumDetail?.name ?: "", icon = icon) + ) + .navigation() + dialog.dismiss() + }) + } + contentView.findViewById(R.id.community_edit_article_container).setOnClickListener { context?.ifLogin("论坛详情-发布-发帖子", action = { checkStoragePermissionBeforeAction { diff --git a/app/src/main/java/com/gh/gamecenter/forum/home/CommunityHomeFragment.kt b/app/src/main/java/com/gh/gamecenter/forum/home/CommunityHomeFragment.kt index 965c5401b8..4bdeb8b3eb 100644 --- a/app/src/main/java/com/gh/gamecenter/forum/home/CommunityHomeFragment.kt +++ b/app/src/main/java/com/gh/gamecenter/forum/home/CommunityHomeFragment.kt @@ -17,11 +17,13 @@ import androidx.core.graphics.ColorUtils import androidx.core.os.bundleOf import androidx.fragment.app.Fragment import androidx.lifecycle.Observer +import androidx.recyclerview.widget.RecyclerView import androidx.viewpager.widget.ViewPager import com.airbnb.lottie.LottieProperty import com.airbnb.lottie.SimpleColorFilter import com.airbnb.lottie.model.KeyPath import com.airbnb.lottie.value.LottieValueCallback +import com.alibaba.android.arouter.launcher.ARouter import com.gh.common.browse.BrowseTimer import com.gh.common.iinterface.ISuperiorChain import com.gh.common.prioritychain.CommunityHomeGuideHandler @@ -36,6 +38,7 @@ import com.gh.gamecenter.common.base.fragment.LazyFragment import com.gh.gamecenter.common.constant.Constants import com.gh.gamecenter.common.constant.EntranceConsts import com.gh.gamecenter.common.constant.EntranceConsts.IS_DETAIL_PAGE +import com.gh.gamecenter.common.constant.RouteConsts import com.gh.gamecenter.common.retrofit.ApiResponse import com.gh.gamecenter.common.utils.* import com.gh.gamecenter.common.view.AvatarBorderView @@ -52,6 +55,8 @@ import com.gh.gamecenter.feature.entity.ArticleEntity import com.gh.gamecenter.feature.entity.ForumVideoEntity import com.gh.gamecenter.forum.home.follow.FollowHomeFilterPopWindow import com.gh.gamecenter.forum.home.follow.fragment.FollowHomeFragment +import com.gh.gamecenter.forum.home.recommend.PublishImageArticleActivity +import com.gh.gamecenter.forum.home.recommend.fragment.ImageArticleHomeFragment import com.gh.gamecenter.forum.search.ForumOrUserSearchActivity import com.gh.gamecenter.login.entity.UserInfoEntity import com.gh.gamecenter.login.user.UserManager @@ -127,6 +132,7 @@ class CommunityHomeFragment : LazyFragment() { } + override fun getRealLayoutId(): Int { return R.layout.fragment_community_home } @@ -135,7 +141,6 @@ class CommunityHomeFragment : LazyFragment() { mBinding = FragmentCommunityHomeBinding.bind(inflatedView) } - override fun onFragmentFirstVisible() { ArticleDetailWebCacheManager.init(requireContext().applicationContext) @@ -306,14 +311,14 @@ class CommunityHomeFragment : LazyFragment() { ?: FollowHomeFragment() mFragmentList.add(followFragment) - val forumArticleListFragment = childFragmentManager.findFragmentByTag("$tag$TAB_RECOMMEND_INDEX") - ?: ForumArticleListFragment().with( + val recommendFragment = childFragmentManager.findFragmentByTag("$tag$TAB_RECOMMEND_INDEX") + ?: ImageArticleHomeFragment.newInstance().with( bundleOf( EntranceConsts.KEY_ENTRANCE to "社区", EntranceConsts.KEY_PATH to "推荐" ) ) - mFragmentList.add(forumArticleListFragment) + mFragmentList.add(recommendFragment) val forumFragment = childFragmentManager.findFragmentByTag("${tag}$TAB_FORUM_INDEX") ?: ForumFragment().with(bundleOf(EntranceConsts.KEY_ENTRANCE to "社区")) @@ -587,6 +592,16 @@ class CommunityHomeFragment : LazyFragment() { UserRepository.getInstance().loginUserInfo.removeObserver(observer) } + contentView.findViewById(R.id.community_edit_recommend_container).setOnClickListener { + context?.ifLogin("论坛首页-发布-图文动态", action = { + showRegulationTestDialogIfNeeded { + NewLogUtils.logArticleEditEnter("推荐信息流", "", "") + ARouter.getInstance().build(RouteConsts.activity.publishImageArticleActivity).navigation() + } + dialog.dismiss() + }) + } + contentView.findViewById(R.id.community_edit_article_container).setOnClickListener { context?.ifLogin("论坛首页-发布-发帖子", action = { showRegulationTestDialogIfNeeded { @@ -637,6 +652,7 @@ class CommunityHomeFragment : LazyFragment() { } } + private fun resetFollowTab() { mBinding?.tabLayout?.run { if (selectedTabPosition == TAB_FOLLOW_INDEX) { @@ -675,13 +691,13 @@ class CommunityHomeFragment : LazyFragment() { } private fun insertDataToRecommendTab(entity: ArticleEntity) { - (mFragmentList[TAB_RECOMMEND_INDEX] as? ForumArticleListFragment)?.insertDataToFirstIndex(entity) + // 废弃 后续会逐渐将 activityForResult迁移到 launcher } override fun onBackPressed(): Boolean { mBinding?.viewPager?.run { if (currentItem == 1) { - return (mFragmentList[1] as ForumArticleListFragment).onBackPressed() + return (mFragmentList[1] as ImageArticleHomeFragment).onBackPressed() } } return super.onBackPressed() diff --git a/app/src/main/java/com/gh/gamecenter/forum/home/CommunityHomeViewModel.kt b/app/src/main/java/com/gh/gamecenter/forum/home/CommunityHomeViewModel.kt index 126f6e3168..1a5fc1ec12 100644 --- a/app/src/main/java/com/gh/gamecenter/forum/home/CommunityHomeViewModel.kt +++ b/app/src/main/java/com/gh/gamecenter/forum/home/CommunityHomeViewModel.kt @@ -8,6 +8,7 @@ import androidx.lifecycle.MutableLiveData import com.gh.gamecenter.common.entity.CommunityEntity import com.gh.gamecenter.common.retrofit.Response import com.gh.gamecenter.common.utils.* +import com.gh.gamecenter.entity.ImageArticleEntity import com.gh.gamecenter.feature.entity.ArticleEntity import com.gh.gamecenter.feature.entity.ForumVideoEntity import com.gh.gamecenter.feature.entity.TimeEntity diff --git a/app/src/main/java/com/gh/gamecenter/forum/home/ForumArticleAskItemViewHolder.kt b/app/src/main/java/com/gh/gamecenter/forum/home/ForumArticleAskItemViewHolder.kt index e8131716cb..4f627d61a5 100644 --- a/app/src/main/java/com/gh/gamecenter/forum/home/ForumArticleAskItemViewHolder.kt +++ b/app/src/main/java/com/gh/gamecenter/forum/home/ForumArticleAskItemViewHolder.kt @@ -6,6 +6,7 @@ import android.graphics.Color import android.graphics.drawable.GradientDrawable import android.text.SpannableStringBuilder import android.view.View +import com.alibaba.android.arouter.launcher.ARouter import com.facebook.drawee.view.SimpleDraweeView import com.gh.common.util.* import com.gh.common.util.DialogUtils @@ -17,6 +18,8 @@ import com.gh.gamecenter.ImageViewerActivity import com.gh.gamecenter.R import com.gh.gamecenter.common.base.activity.BaseActivity import com.gh.gamecenter.common.callback.ConfirmListener +import com.gh.gamecenter.common.constant.EntranceConsts +import com.gh.gamecenter.common.constant.RouteConsts import com.gh.gamecenter.common.entity.AdditionalParamsEntity import com.gh.gamecenter.common.entity.CommunityEntity import com.gh.gamecenter.common.utils.* @@ -24,6 +27,7 @@ import com.gh.gamecenter.core.utils.MtaHelper import com.gh.gamecenter.core.utils.SpanBuilder import com.gh.gamecenter.core.utils.ToastUtils import com.gh.gamecenter.databinding.CommunityAnswerItemBinding +import com.gh.gamecenter.entity.ImageArticleEntity import com.gh.gamecenter.feature.entity.ForumVideoEntity import com.gh.gamecenter.eventbus.EBUserFollow import com.gh.gamecenter.forum.detail.ForumDetailActivity @@ -528,6 +532,12 @@ class ForumArticleAskItemViewHolder( ) ) } + + ImageArticleEntity.IMAGE_ARTICLE_TYPE -> { + ARouter.getInstance().build(RouteConsts.activity.imageArticleDetailActivity) + .withString(EntranceConsts.KEY_IMAGE_ARTICLE_ID, entity.id) + .navigation() + } } } diff --git a/app/src/main/java/com/gh/gamecenter/forum/home/recommend/ImageArticleDetailActivity.kt b/app/src/main/java/com/gh/gamecenter/forum/home/recommend/ImageArticleDetailActivity.kt new file mode 100644 index 0000000000..4118325e1a --- /dev/null +++ b/app/src/main/java/com/gh/gamecenter/forum/home/recommend/ImageArticleDetailActivity.kt @@ -0,0 +1,33 @@ +package com.gh.gamecenter.forum.home.recommend + +import android.graphics.Color +import android.os.Bundle +import com.alibaba.android.arouter.facade.annotation.Route +import com.gh.gamecenter.R +import com.gh.gamecenter.common.base.activity.BaseActivity +import com.gh.gamecenter.common.constant.RouteConsts +import com.gh.gamecenter.core.utils.DisplayUtils +import com.gh.gamecenter.forum.home.recommend.fragment.ImageArticleDetailFragment + +@Route(path = RouteConsts.activity.imageArticleDetailActivity) +class ImageArticleDetailActivity : BaseActivity() { + + override fun getLayoutId(): Int { + return R.layout.activity_image_article_detail + } + + override fun onCreate(savedInstanceState: Bundle?) { + super.onCreate(savedInstanceState) + DisplayUtils.setLightStatusBar(this, true) + setStatusBarColor(Color.TRANSPARENT) + + val tag = ImageArticleDetailFragment::class.java.toString() + val fragment = + supportFragmentManager.findFragmentByTag(tag) ?: ImageArticleDetailFragment().apply { + arguments = intent.extras + } + val transaction = supportFragmentManager.beginTransaction() + transaction.replace(R.id.layout_activity_content, fragment, tag) + transaction.commitAllowingStateLoss() + } +} \ No newline at end of file diff --git a/app/src/main/java/com/gh/gamecenter/forum/home/recommend/ImageArticleFooterWrapperAdapter.kt b/app/src/main/java/com/gh/gamecenter/forum/home/recommend/ImageArticleFooterWrapperAdapter.kt new file mode 100644 index 0000000000..9c9b1a8d8d --- /dev/null +++ b/app/src/main/java/com/gh/gamecenter/forum/home/recommend/ImageArticleFooterWrapperAdapter.kt @@ -0,0 +1,154 @@ +package com.gh.gamecenter.forum.home.recommend + +import android.view.LayoutInflater +import android.view.ViewGroup +import androidx.recyclerview.widget.DiffUtil.ItemCallback +import androidx.recyclerview.widget.ListAdapter +import androidx.recyclerview.widget.RecyclerView +import androidx.recyclerview.widget.RecyclerView.NO_POSITION +import androidx.recyclerview.widget.RecyclerView.ViewHolder +import androidx.recyclerview.widget.StaggeredGridLayoutManager +import com.gh.gamecenter.R +import com.gh.gamecenter.common.baselist.PageLoader +import com.gh.gamecenter.common.databinding.RefreshFooterviewBinding +import com.gh.gamecenter.forum.home.follow.viewholder.FollowFooterViewHolder + +abstract class ImageArticleFooterWrapperAdapter(diffCallback: ItemCallback) : + ListAdapter(diffCallback) { + + private var pageState: PageLoader.PageState? = null + + private var _recyclerView: RecyclerView? = null + + val dataCount: Int + get() = super.getItemCount() + + fun setPageState(pageState: PageLoader.PageState) { + this.pageState = pageState + _recyclerView?.post { + notifyItemChanged(itemCount - 1) + } + + } + + override fun getItemCount(): Int { + return if (dataCount > 0) dataCount + 1 else dataCount + } + + override fun getItemViewType(position: Int): Int { + return if (position < dataCount) ITEM_TYPE_DATA else ITEM_TYPE_FOOTER + } + + override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): ViewHolder { + return if (viewType == ITEM_TYPE_DATA) { + onCreateDataViewHolder(parent) + } else { + onCreateFooterViewHolder(parent) + } + } + + override fun onBindViewHolder(holder: ViewHolder, position: Int) { + if (holder.itemViewType == ITEM_TYPE_FOOTER) { + onBindFooterViewHolder(holder) + } else { + onBindDataViewHolder(holder as VH, position) + } + } + + override fun onBindViewHolder(holder: ViewHolder, position: Int, payloads: MutableList) { + if (payloads.isEmpty()) { + onBindViewHolder(holder, position) + } else { + if (holder.itemViewType != ITEM_TYPE_FOOTER) { + onBindDataViewHolder(holder as VH, position, payloads) + } + } + } + + open fun onBindFooterViewHolder(holder: ViewHolder) { + if (holder is FollowFooterViewHolder) { + holder.initFooterViewHolder( + pageState == PageLoader.PageState.PageLoadMoreLoading, + pageState == PageLoader.PageState.PageLoadMoreFailure, + pageState == PageLoader.PageState.PageLoadCompleted, + R.string.load_over_with_click_hint + ) { + if (pageState == PageLoader.PageState.PageLoadCompleted) { + _recyclerView?.scrollToPosition(0) + } else if (pageState == PageLoader.PageState.PageLoadMoreFailure) { + pageState = PageLoader.PageState.PageLoadMoreLoading + loadMore() + notifyItemChanged(itemCount - 1) + } + } + } + } + + abstract fun onCreateDataViewHolder(parent: ViewGroup): VH + + + open fun onCreateFooterViewHolder(parent: ViewGroup): ViewHolder { + val inflater = LayoutInflater.from(parent.context) + val binding = RefreshFooterviewBinding.inflate(inflater, parent, false) + val viewHolder = FollowFooterViewHolder(binding) + val layoutParams = binding.root.layoutParams + if (layoutParams is StaggeredGridLayoutManager.LayoutParams) { + layoutParams.isFullSpan = true + binding.root.layoutParams = layoutParams + } + return viewHolder + } + + abstract fun onBindDataViewHolder(holder: VH, position: Int) + + open fun onBindDataViewHolder(holder: VH, position: Int, payloads: MutableList) { + onBindDataViewHolder(holder, position) + } + + abstract fun loadMore() + + private val onLoadMoreListener = object : RecyclerView.OnScrollListener() { + + override fun onScrolled(recyclerView: RecyclerView, dx: Int, dy: Int) { + if (dy != 0) { + refreshIfNeed(recyclerView) + } + } + + override fun onScrollStateChanged(recyclerView: RecyclerView, newState: Int) { + refreshIfNeed(recyclerView) + } + + private fun refreshIfNeed(recyclerView: RecyclerView) { + val layoutManger = recyclerView.layoutManager + if (layoutManger is StaggeredGridLayoutManager) { + val lastVisibleItemPositions = IntArray(2) + layoutManger.findLastVisibleItemPositions(lastVisibleItemPositions) + val lastVisibleItemPosition = lastVisibleItemPositions.maxOrNull() ?: NO_POSITION + if (lastVisibleItemPosition >= itemCount - 3 // 提前两个开始加载下一页 + && pageState is PageLoader.PageState.PageLoadMoreReady + ) { + pageState = PageLoader.PageState.PageLoadMoreLoading + loadMore() + } + } + } + + } + + override fun onAttachedToRecyclerView(recyclerView: RecyclerView) { + _recyclerView = recyclerView + recyclerView.addOnScrollListener(onLoadMoreListener) + } + + override fun onDetachedFromRecyclerView(recyclerView: RecyclerView) { + recyclerView.removeOnScrollListener(onLoadMoreListener) + _recyclerView = null + } + + companion object { + + private const val ITEM_TYPE_DATA = 0 + const val ITEM_TYPE_FOOTER = 101 + } +} \ No newline at end of file diff --git a/app/src/main/java/com/gh/gamecenter/forum/home/recommend/ImageArticleRepository.kt b/app/src/main/java/com/gh/gamecenter/forum/home/recommend/ImageArticleRepository.kt new file mode 100644 index 0000000000..f38b2a062d --- /dev/null +++ b/app/src/main/java/com/gh/gamecenter/forum/home/recommend/ImageArticleRepository.kt @@ -0,0 +1,241 @@ +package com.gh.gamecenter.forum.home.recommend + +import com.gh.gamecenter.BuildConfig +import com.gh.gamecenter.common.constant.Constants +import com.gh.gamecenter.common.utils.UploadImageUtils +import com.gh.gamecenter.core.utils.SPUtils +import com.gh.gamecenter.entity.ImageArticleEntity +import com.gh.gamecenter.entity.PersonalHistoryEntity +import com.gh.gamecenter.entity.PublishImageTextRequest +import com.gh.gamecenter.entity.VoteEntity +import com.gh.gamecenter.feature.entity.CommentEntity +import com.gh.gamecenter.forum.home.recommend.adapter.ImageAndTextAdapter +import com.gh.gamecenter.login.user.UserManager +import com.gh.gamecenter.retrofit.RetrofitManager +import com.gh.gamecenter.retrofit.service.ApiService +import com.halo.assistant.HaloApp +import com.zhihu.matisse.internal.utils.PathUtils +import io.reactivex.Completable +import io.reactivex.Observable +import io.reactivex.Single +import io.reactivex.schedulers.Schedulers +import okhttp3.MediaType +import okhttp3.ResponseBody + +class ImageArticleRepository private constructor( + private val api: ApiService, + private val newApi: ApiService +) { + + + fun loadRecommends(page: Int): Single> { + val version = BuildConfig.VERSION_NAME + val channel = HaloApp.getInstance().channel + return api.loadImageArticleRecommends(IMAGE_ARTICLE_SORT, page, version, channel) + } + + + fun isShowDragTips() = + Single.create { + val hasShow = SPUtils.getBoolean(Constants.SP_HAS_SHOW_IMAGE_AND_TEXT_DRAG_TIPS) + it.onSuccess(!hasShow) + } + + fun saveHasShowDragTips() { + SPUtils.setBoolean(Constants.SP_HAS_SHOW_IMAGE_AND_TEXT_DRAG_TIPS, true) + } + + fun checkHasSections(bbsId: String) = + newApi.getForumSections(bbsId) + .map { + it.size > 0 + } + + fun getModeratorsInfo(bbsId: String) = + api.getModeratorsInfo(bbsId) + .map { + it["is_moderators"].asBoolean + } + + fun createOrEditDraft( + request: PublishImageTextRequest, + localImages: List + ): Single { + // 需要先上传本地图片 + val userId = UserManager.getInstance().userId + return updatePic(localImages) + .observeOn(Schedulers.io()) + .flatMap { + request.images = it + if (request.draftId.isNotBlank()) { + // 编辑草稿 + api.editImageArticleDrafts(userId, request.draftId, request) + .toSingleDefault(ResponseBody.create(MediaType.parse("application/json"), "{}")) + } else { + api.createImageArticleDrafts(userId, request) + } + + } + } + + fun publishImageText( + request: PublishImageTextRequest, + localImages: List + ): Single { + return updatePic(localImages) + .observeOn(Schedulers.io()) + .flatMap { + request.images = it + api.publishImageText(request) + .flatMap { + val view = "detail" + val versionName = BuildConfig.VERSION_NAME + val channel = HaloApp.getInstance().channel + api.loadImageArticleDetail(it["_id"].asString, view, versionName, channel) + } + } + + } + + fun editImageArticle( + id: String, + request: PublishImageTextRequest, + images: List + ): Single { + return updatePic(images) + .observeOn(Schedulers.io()) + .flatMap { + request.images = it + api.editImageArticle(id, request) + .toSingleDefault(id) + .flatMap { + val view = "detail" + val versionName = BuildConfig.VERSION_NAME + val channel = HaloApp.getInstance().channel + api.loadImageArticleDetail(it, view, versionName, channel) + } + } + } + + fun loadDraft(): Single> { + val userId = UserManager.getInstance().userId + val versionName = BuildConfig.VERSION_NAME + val channel = HaloApp.getInstance().channel + return api.loadImageTextDrafts(userId, versionName, channel) + } + + fun deleteDraft(draftId: String): Completable { + val userId = UserManager.getInstance().userId + return api.deleteImageArticleDraft(userId, draftId) + } + + fun loadImageArticleDetail(imageArticleId: String): Single { + val view = "detail" + return api.loadImageArticleDetail(imageArticleId, view, BuildConfig.VERSION_NAME, HaloApp.getInstance().channel) + + } + + fun loadMyImageArticleList(page: Int): Observable> { + val userId = UserManager.getInstance().userId + val version = BuildConfig.VERSION_NAME + val channel = HaloApp.getInstance().channel + return api.getPersonalHistory(userId, page, channel, IMAGE_ARTICLE_FILTER) + } + + private fun updatePic(localImages: List): Single> = + Single.create { + // 已上传的图片 + val urls = localImages.mapNotNull { image -> image.url } + val uris = localImages.mapNotNull { image -> image.uri } + if (uris.isEmpty()) { + it.onSuccess(urls) + return@create + } + // 需要上传的图片 + val localPaths = uris.map { uri -> PathUtils.getPath(HaloApp.getInstance(), uri) } + UploadImageUtils.compressAndUploadImageList( + UploadImageUtils.UploadType.community_article, + localPaths, + false, + object : UploadImageUtils.OnUploadImageListListener { + override fun onSuccess( + imageUrlMap: LinkedHashMap, + errorMap: Map + ) { + val finalUrls = urls + imageUrlMap.values + it.onSuccess(finalUrls) + } + + override fun onSingleSuccess(imageUrlMap: Map) = Unit + + override fun onError(errorMap: Map) { + // 图片上传失败,忽略 + + } + + override fun onProgress(total: Long, progress: Long) = Unit + + }) + } + + fun getSingleCommentById(commentId: String): Single { + return api.getImageArticleCommentDetail( + commentId, + BuildConfig.VERSION_NAME, + HaloApp.getInstance().channel, + System.currentTimeMillis() + ) + } + + fun followBbs(communityId: String, isFollow: Boolean) = + if (isFollow) { + api.followForum(communityId) + } else { + api.unFollowForum(communityId) + } + + fun collect(imageArticleId: String, isCollected: Boolean) = + if (isCollected) { + api.collectImageArticle(imageArticleId) + .flatMapCompletable { + Completable.complete() + } + } else { + api.cancelImageArticleCollection(imageArticleId) + } + + fun voteImageArticle(imageArticleId: String, vote: Boolean): Single = if (vote) { + api.voteArticleImage(imageArticleId) + } else { + api.unVoteArticleImage(imageArticleId) + } + + fun applyHighlightForImageArticle(imageArticleId: String): Completable = + api.applyHighlightForImageArticle(imageArticleId) + + fun addHighlight(isAdd: Boolean, imageArticleId: String) = + if (isAdd) { + api.addHighlightForImageArticle(imageArticleId) + } else { + api.cancelHighlightForImageArticle(imageArticleId) + } + + fun deleteOrHideImageArticle(imageArticleId: String) = + api.deleteOrHideImageArticle(imageArticleId) + + fun followUser(follow: Boolean, userId: String) = + if (follow) { + api.postFollowing(userId) + } else { + api.deleteFollowing(userId) + } + + companion object { + + private const val IMAGE_ARTICLE_FILTER = "scene:question_answer,type:image_article" + private const val IMAGE_ARTICLE_SORT = "time.comment:-1" + + fun newInstance() = + ImageArticleRepository(RetrofitManager.getInstance().api, RetrofitManager.getInstance().newApi) + } +} \ No newline at end of file diff --git a/app/src/main/java/com/gh/gamecenter/forum/home/recommend/ImageArticleRequestPermissionFragment.kt b/app/src/main/java/com/gh/gamecenter/forum/home/recommend/ImageArticleRequestPermissionFragment.kt new file mode 100644 index 0000000000..dbb2749e3e --- /dev/null +++ b/app/src/main/java/com/gh/gamecenter/forum/home/recommend/ImageArticleRequestPermissionFragment.kt @@ -0,0 +1,11 @@ +package com.gh.gamecenter.forum.home.recommend + +import com.gh.gamecenter.R +import com.gh.gamecenter.common.base.fragment.ToolbarFragment + +class ImageArticleRequestPermissionFragment : ToolbarFragment() { + + override fun getLayoutId(): Int { + return R.layout.fragment_image_article_request_permission + } +} \ No newline at end of file diff --git a/app/src/main/java/com/gh/gamecenter/forum/home/recommend/ImageArticleUseCase.kt b/app/src/main/java/com/gh/gamecenter/forum/home/recommend/ImageArticleUseCase.kt new file mode 100644 index 0000000000..1e90c2e0ed --- /dev/null +++ b/app/src/main/java/com/gh/gamecenter/forum/home/recommend/ImageArticleUseCase.kt @@ -0,0 +1,56 @@ +package com.gh.gamecenter.forum.home.recommend + +import androidx.lifecycle.LiveData +import androidx.lifecycle.MutableLiveData +import com.gh.gamecenter.R +import com.gh.gamecenter.common.retrofit.BiResponse +import com.gh.gamecenter.common.utils.singleToMain +import com.gh.gamecenter.core.utils.ToastUtils +import com.gh.gamecenter.entity.ImageArticleEntity +import com.gh.gamecenter.entity.VoteEntity +import com.gh.gamecenter.eventbus.EBImageArticleChanged +import com.gh.gamecenter.livedata.Event +import io.reactivex.disposables.CompositeDisposable +import org.greenrobot.eventbus.EventBus + +class ImageArticleUseCase( + private val repository: ImageArticleRepository, + private val compositeDisposable: CompositeDisposable +) { + + fun voteImageArticle(imageArticleId: String, isVote: Boolean) { + repository.voteImageArticle(imageArticleId, isVote) + .compose(singleToMain()) + .subscribe(object : BiResponse() { + override fun onSuccess(data: VoteEntity) { + notifyVoteChanged(imageArticleId, isVote, data.vote) + } + + }).let(compositeDisposable::add) + } + + companion object { + + + fun notifyVoteChanged(id: String, isVoted: Boolean, count: Int) { + EventBus.getDefault().post(EBImageArticleChanged.VoteChanged(id, isVoted, count)) + } + + fun notifyCommentChanged(id: String, count: Int) { + EventBus.getDefault().post(EBImageArticleChanged.CommentChanged(id, count)) + } + + fun notifyDataDeleted(id: String) { + EventBus.getDefault().post(EBImageArticleChanged.DataDeleted(id)) + } + + fun notifyDataCreated(data: ImageArticleEntity, draftId: String = "") { + EventBus.getDefault().post(EBImageArticleChanged.DataCreated(data, draftId)) + } + + fun notifyDataEdited(data: ImageArticleEntity) { + EventBus.getDefault().post(EBImageArticleChanged.DataChanged(data)) + } + + } +} \ No newline at end of file diff --git a/app/src/main/java/com/gh/gamecenter/forum/home/recommend/PublishImageArticleActivity.kt b/app/src/main/java/com/gh/gamecenter/forum/home/recommend/PublishImageArticleActivity.kt new file mode 100644 index 0000000000..ce15862fe9 --- /dev/null +++ b/app/src/main/java/com/gh/gamecenter/forum/home/recommend/PublishImageArticleActivity.kt @@ -0,0 +1,178 @@ +package com.gh.gamecenter.forum.home.recommend + +import android.content.Context +import android.content.Intent +import android.graphics.Color +import android.os.Bundle +import android.widget.ImageView +import android.widget.TextView +import androidx.activity.result.ActivityResultCallback +import androidx.activity.result.ActivityResultLauncher +import androidx.activity.result.contract.ActivityResultContract +import androidx.activity.viewModels +import androidx.fragment.app.Fragment +import com.alibaba.android.arouter.facade.annotation.Autowired +import com.alibaba.android.arouter.facade.annotation.Route +import com.alibaba.android.arouter.launcher.ARouter +import com.gh.gamecenter.R +import com.gh.gamecenter.common.base.activity.BaseActivity +import com.gh.gamecenter.common.constant.EntranceConsts +import com.gh.gamecenter.common.constant.RouteConsts +import com.gh.gamecenter.common.entity.CommunityEntity +import com.gh.gamecenter.common.utils.PermissionHelper +import com.gh.gamecenter.core.utils.DisplayUtils +import com.gh.gamecenter.entity.ImageArticleEntity +import com.gh.gamecenter.forum.home.recommend.fragment.ImageArticleDraftFragment +import com.gh.gamecenter.forum.home.recommend.fragment.PublishImageArticleFragment +import com.gh.gamecenter.forum.home.recommend.viewmodel.PublishImageArticleActivityViewModel +import com.gh.gamecenter.livedata.EventObserver +import com.gh.gamecenter.qa.editor.LocalMediaActivity +import com.zhihu.matisse.Matisse + +@Route(path = RouteConsts.activity.publishImageArticleActivity) +class PublishImageArticleActivity : BaseActivity() { + + private val viewModel by viewModels() + + private lateinit var chooseImageLauncher: ActivityResultLauncher + + private var isFirstEnter = true + + @JvmField + @Autowired(name = EntranceConsts.KEY_IMAGE_ARTICLE_ENTITY) + var imageArticleEntity: ImageArticleEntity? = null + + @JvmField + @Autowired(name = EntranceConsts.KEY_COMMUNITY_DATA) + var communityEntity: CommunityEntity? = null + + private lateinit var tvTitle: TextView + + override fun getLayoutId(): Int { + return R.layout.activity_publish_image_article + } + + + override fun onCreate(savedInstanceState: Bundle?) { + super.onCreate(savedInstanceState) + ARouter.getInstance().inject(this) + + DisplayUtils.setLightStatusBar(this, true) + setStatusBarColor(Color.TRANSPARENT) + + tvTitle = findViewById(R.id.tv_title) + val ivBack = findViewById(R.id.iv_back) + ivBack.setOnClickListener { + onBackPressed() + } + + supportFragmentManager.addOnBackStackChangedListener { + val fragment = supportFragmentManager.fragments.lastOrNull() + val titleResId = if (fragment is ImageArticleDraftFragment) { + R.string.image_article_draft + } else { + R.string.post_image_and_text + } + tvTitle.setText(titleResId) + } + + chooseImageLauncher = registerForActivityResult(object : ActivityResultContract() { + override fun createIntent(context: Context, input: Int): Intent { + return LocalMediaActivity.getIntent( + context, + LocalMediaActivity.ChooseType.IMAGE, + input, + "图文动态" + ) + } + + override fun parseResult(resultCode: Int, intent: Intent?): Intent? { + return intent + } + }, ActivityResultCallback { + if (it != null) { + val uris = Matisse.obtainResult(it) + if (uris.isEmpty()) { + if (isFirstEnter) { + finish() + } + + } else { + viewModel.addImages(uris) + // 打开发布页 + if (isFirstEnter) { + replaceFragment(true) { + PublishImageArticleFragment.newInstance() + } + } + } + } else { + if (isFirstEnter) { + finish() + } + } + + }) + + if (imageArticleEntity == null) { + // 新建图文,优先进入图库选择页 + replaceFragment(false, ::ImageArticleRequestPermissionFragment) + PermissionHelper.checkStoragePermissionBeforeActionForResult(this) { grant -> + if (grant) { + chooseImage(9) + } else { + finish() + } + } + } else { + // 编辑图文草稿,不用弹出授权页,直接进入发布页 + replaceFragment(true) { + PublishImageArticleFragment.newInstance() + } + } + + + + with(viewModel) { + val lifecycleOwner = this@PublishImageArticleActivity + chooseImageAction.observe(lifecycleOwner, EventObserver { + isFirstEnter = false + PermissionHelper.checkStoragePermissionBeforeActionForResult(this@PublishImageArticleActivity) { grant -> + if (grant) { + this@PublishImageArticleActivity.chooseImage(it) + } + } + }) + + draftBoxDestination.observe(lifecycleOwner, EventObserver { + replaceFragment(true) { + ImageArticleDraftFragment.newInstance(true) + } + }) + + setEditImageArticle(imageArticleEntity) + communityEntity?.let(::setCommunity) + } + } + + + private fun chooseImage(limit: Int) { + try { + chooseImageLauncher.launch(limit) + } catch (e: Exception) { + toast(R.string.media_image_hint) + e.printStackTrace() + } + } + + private inline fun replaceFragment(addBackStack: Boolean = true, creator: () -> T) { + val tag = T::class.java.toString() + val fragment = supportFragmentManager.findFragmentByTag(tag) ?: creator() + val transaction = supportFragmentManager.beginTransaction() + transaction.replace(R.id.layout_activity_content, fragment, tag) + if (addBackStack) { + transaction.addToBackStack(tag) + } + transaction.commitAllowingStateLoss() + } +} \ No newline at end of file diff --git a/app/src/main/java/com/gh/gamecenter/forum/home/recommend/adapter/ImageAndTextAdapter.kt b/app/src/main/java/com/gh/gamecenter/forum/home/recommend/adapter/ImageAndTextAdapter.kt new file mode 100644 index 0000000000..d3018888a6 --- /dev/null +++ b/app/src/main/java/com/gh/gamecenter/forum/home/recommend/adapter/ImageAndTextAdapter.kt @@ -0,0 +1,107 @@ +package com.gh.gamecenter.forum.home.recommend.adapter + +import android.net.Uri +import android.view.ViewGroup +import androidx.recyclerview.widget.DiffUtil.ItemCallback +import androidx.recyclerview.widget.ListAdapter +import androidx.recyclerview.widget.RecyclerView +import androidx.recyclerview.widget.RecyclerView.ViewHolder +import com.gh.gamecenter.R +import com.gh.gamecenter.common.utils.display +import com.gh.gamecenter.common.utils.toBinding +import com.gh.gamecenter.common.utils.visibleIf +import com.gh.gamecenter.core.utils.ToastUtils +import com.gh.gamecenter.databinding.RecyclerImageAndTextBinding +import com.gh.gamecenter.forum.home.recommend.viewmodel.PublishImageArticleViewModel + +open class ImageAndTextAdapter( + private val viewModel: PublishImageArticleViewModel, + private val startDrag: (ViewHolder) -> Unit +) : + ListAdapter( + createItemDiffCallback() + ) { + + override fun submitList(list: List?) { + val size = list?.size ?: 0 + if (size < 9 && list != null) { + super.submitList(list + LocalImage(INVALID_ID, false, showClose = false)) + } else { + super.submitList(list) + } + } + + public override fun getItem(position: Int): LocalImage { + return super.getItem(position) + } + + override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): ImageViewHolder { + return ImageViewHolder(parent.toBinding()) + } + + override fun onBindViewHolder(holder: ImageViewHolder, position: Int) { + val item = getItem(position) + with(holder.binding) { + tvCover.visibleIf(item.isCover) + ivClose.visibleIf(item.showClose) { + ivClose.setOnClickListener { + if (itemCount <= 2) { + ToastUtils.showToast(root.context.getString(R.string.post_image_limit)) + return@setOnClickListener + } + viewModel.removeImage(holder.bindingAdapterPosition) + } + } + if (item.id != INVALID_ID) { + if (item.uri != null) { + ivIcon.setImageURI(item.uri) + } else { + ivIcon.display(item.url) + } + + ivIcon.setOnClickListener(null) + ivIcon.setOnLongClickListener { + startDrag(holder) + return@setOnLongClickListener true + } + } else { + ivIcon.setImageResource(R.drawable.ic_image_and_text_add) + ivIcon.setOnClickListener { + viewModel.chooseImage() + } + root.setOnLongClickListener(null) + } + + } + + } + + companion object { + + const val INVALID_ID = -1 + + private fun createItemDiffCallback() = object : ItemCallback() { + + override fun areItemsTheSame(oldItem: LocalImage, newItem: LocalImage): Boolean { + return oldItem.id == newItem.id + } + + override fun areContentsTheSame(oldItem: LocalImage, newItem: LocalImage): Boolean { + return oldItem == newItem + } + + } + } + + data class LocalImage( + val id: Int, + var isCover: Boolean, + var showClose: Boolean, + val uri: Uri? = null, // 本地图片 + val url: String? = null // 线上图片 + ) + + data class ImageViewHolder( + val binding: RecyclerImageAndTextBinding + ) : RecyclerView.ViewHolder(binding.root) +} diff --git a/app/src/main/java/com/gh/gamecenter/forum/home/recommend/adapter/ImageArticleDetailBannerAdapter.kt b/app/src/main/java/com/gh/gamecenter/forum/home/recommend/adapter/ImageArticleDetailBannerAdapter.kt new file mode 100644 index 0000000000..719e4aaae9 --- /dev/null +++ b/app/src/main/java/com/gh/gamecenter/forum/home/recommend/adapter/ImageArticleDetailBannerAdapter.kt @@ -0,0 +1,42 @@ +package com.gh.gamecenter.forum.home.recommend.adapter + +import android.view.ViewGroup +import androidx.core.view.updateLayoutParams +import androidx.recyclerview.widget.DiffUtil +import androidx.recyclerview.widget.ListAdapter +import androidx.recyclerview.widget.RecyclerView.ViewHolder +import com.gh.gamecenter.common.utils.ImageUtils +import com.gh.gamecenter.common.utils.dip2px +import com.gh.gamecenter.common.utils.toBinding +import com.gh.gamecenter.databinding.RecyclerImageArticleDetailBannerBinding + +class ImageArticleDetailBannerAdapter : ListAdapter( + createDiffCallback() +) { + + override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): BannerViewHolder { + return BannerViewHolder(parent.toBinding()) + } + + override fun onBindViewHolder(holder: BannerViewHolder, position: Int) { + with(holder.binding) { + ImageUtils.display(ivBanner, getItem(position)) + } + } + + companion object { + + fun createDiffCallback() = object : DiffUtil.ItemCallback() { + override fun areItemsTheSame(oldItem: String, newItem: String): Boolean { + return oldItem == newItem + } + + override fun areContentsTheSame(oldItem: String, newItem: String): Boolean { + return oldItem == newItem + } + + } + } + + class BannerViewHolder(val binding: RecyclerImageArticleDetailBannerBinding) : ViewHolder(binding.root) +} \ No newline at end of file diff --git a/app/src/main/java/com/gh/gamecenter/forum/home/recommend/adapter/ImageArticleDetailCommentAdapter.kt b/app/src/main/java/com/gh/gamecenter/forum/home/recommend/adapter/ImageArticleDetailCommentAdapter.kt new file mode 100644 index 0000000000..dcd88cacfc --- /dev/null +++ b/app/src/main/java/com/gh/gamecenter/forum/home/recommend/adapter/ImageArticleDetailCommentAdapter.kt @@ -0,0 +1,15 @@ +package com.gh.gamecenter.forum.home.recommend.adapter + +import android.content.Context +import com.gh.gamecenter.feature.entity.CommentEntity +import com.gh.gamecenter.qa.comment.base.BaseCommentAdapter +import com.gh.gamecenter.qa.comment.base.BaseCommentAdapter.AdapterType +import com.gh.gamecenter.qa.comment.base.BaseCommentViewModel + +class ImageArticleDetailCommentAdapter( + context: Context, + private val viewModel: BaseCommentViewModel, + type: AdapterType, + entrance: String +) : BaseCommentAdapter(context, viewModel, type, entrance) { +} \ No newline at end of file diff --git a/app/src/main/java/com/gh/gamecenter/forum/home/recommend/adapter/ImageArticleDraftAdapter.kt b/app/src/main/java/com/gh/gamecenter/forum/home/recommend/adapter/ImageArticleDraftAdapter.kt new file mode 100644 index 0000000000..a045f5dbad --- /dev/null +++ b/app/src/main/java/com/gh/gamecenter/forum/home/recommend/adapter/ImageArticleDraftAdapter.kt @@ -0,0 +1,79 @@ +package com.gh.gamecenter.forum.home.recommend.adapter + +import android.content.Context +import android.view.ViewGroup +import androidx.core.view.updateLayoutParams +import androidx.recyclerview.widget.DiffUtil.ItemCallback +import androidx.recyclerview.widget.ListAdapter +import androidx.recyclerview.widget.RecyclerView.ViewHolder +import com.gh.common.util.NewsUtils +import com.gh.gamecenter.common.utils.ImageUtils +import com.gh.gamecenter.common.utils.dip2px +import com.gh.gamecenter.common.utils.goneIf +import com.gh.gamecenter.common.utils.toBinding +import com.gh.gamecenter.databinding.RecyclerImageArticleDraftBinding +import com.gh.gamecenter.entity.ImageArticleEntity +import com.gh.gamecenter.forum.home.recommend.viewmodel.ImageArticleDraftViewModel +import com.lzf.easyfloat.utils.DisplayUtils + +class ImageArticleDraftAdapter( + private val context: Context, + private val viewModel: ImageArticleDraftViewModel +) : + ListAdapter(createDiffCallback()) { + + private val itemWidth by lazy { + (DisplayUtils.getScreenWidth(context) - 20F.dip2px()) / 2F + } + + override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): DraftViewHolder { + return DraftViewHolder(parent.toBinding()) + } + + override fun onBindViewHolder(holder: DraftViewHolder, position: Int) { + val item = getItem(position) + with(holder.binding) { + val title = item.title.ifBlank { item.content } + tvTitle.goneIf(title.isBlank()) { + tvTitle.text = title + } + + val imageInfo = item.imagesInfos.firstOrNull() + val imageRadio = ImageArticleEntity.getImageRadio(imageInfo, DRAFT_RADIO_MIN, DRAFT_RADIO_MAX) + ivCover.updateLayoutParams { + height = (itemWidth * imageRadio).toInt() + } + ImageUtils.display(ivCover, item.images.firstOrNull() ?: "") + tvTime.text = NewsUtils.getFormattedTime(item.time.update) + vDelete.setOnClickListener { + viewModel.showDeleteDraftDialog(item.id) + } + + root.setOnClickListener { + viewModel.editDraft(item) + } + } + } + + companion object { + + const val DRAFT_RADIO_MIN = 0.75F + const val DRAFT_RADIO_MAX = 1.33F + + private fun createDiffCallback() = object : ItemCallback() { + override fun areItemsTheSame(oldItem: ImageArticleEntity, newItem: ImageArticleEntity): Boolean { + return oldItem.id == newItem.id + } + + override fun areContentsTheSame( + oldItem: ImageArticleEntity, + newItem: ImageArticleEntity + ): Boolean { + return oldItem == newItem + } + + } + } + + data class DraftViewHolder(val binding: RecyclerImageArticleDraftBinding) : ViewHolder(binding.root) +} \ No newline at end of file diff --git a/app/src/main/java/com/gh/gamecenter/forum/home/recommend/adapter/MyImageArticleAdapter.kt b/app/src/main/java/com/gh/gamecenter/forum/home/recommend/adapter/MyImageArticleAdapter.kt new file mode 100644 index 0000000000..53336b905d --- /dev/null +++ b/app/src/main/java/com/gh/gamecenter/forum/home/recommend/adapter/MyImageArticleAdapter.kt @@ -0,0 +1,187 @@ +package com.gh.gamecenter.forum.home.recommend.adapter + +import android.content.Context +import android.view.ViewGroup +import androidx.core.view.updateLayoutParams +import androidx.recyclerview.widget.DiffUtil.ItemCallback +import androidx.recyclerview.widget.RecyclerView +import com.gh.gamecenter.common.utils.dip2px +import com.gh.gamecenter.common.utils.display +import com.gh.gamecenter.common.utils.goneIf +import com.gh.gamecenter.common.utils.toBinding +import com.gh.gamecenter.databinding.RecyclerRecommendHomeBinding +import com.gh.gamecenter.entity.ImageArticleEntity +import com.gh.gamecenter.entity.PersonalHistoryEntity +import com.gh.gamecenter.forum.home.recommend.ImageArticleFooterWrapperAdapter +import com.gh.gamecenter.forum.home.recommend.adapter.ImageArticleDraftAdapter.Companion.DRAFT_RADIO_MAX +import com.gh.gamecenter.forum.home.recommend.adapter.ImageArticleDraftAdapter.Companion.DRAFT_RADIO_MIN +import com.gh.gamecenter.forum.home.recommend.adapter.RecommendHomeAdapter.Companion.PAYLOADS_CHANGED_COVER +import com.gh.gamecenter.forum.home.recommend.adapter.RecommendHomeAdapter.Companion.PAYLOADS_CHANGED_TITLE +import com.gh.gamecenter.forum.home.recommend.adapter.RecommendHomeAdapter.Companion.PAYLOADS_CHANGED_VOTE +import com.gh.gamecenter.forum.home.recommend.viewmodel.MyImageArticleListViewModel +import com.lzf.easyfloat.utils.DisplayUtils + +class MyImageArticleAdapter( + private val context: Context, + private val viewModel: MyImageArticleListViewModel +) : + ImageArticleFooterWrapperAdapter( + createDiffCallback() + ) { + + override fun onCreateDataViewHolder(parent: ViewGroup): MyImageArticleViewHolder { + return MyImageArticleViewHolder(parent.toBinding(), object : MyImageArticleViewHolder.OnMyArticleImageListener { + override fun navigateToImageArticleDetailPage(id: String) { + viewModel.navigateToImageArticleDetailPage(id) + } + + override fun voteImageArticle(id: String, vote: Boolean) { + viewModel.useCase.voteImageArticle(id, vote) + } + + }) + } + + override fun onBindDataViewHolder(holder: MyImageArticleViewHolder, position: Int) { + val item = getItem(position) + holder.bind(item) + + } + + override fun onBindDataViewHolder(holder: MyImageArticleViewHolder, position: Int, payloads: MutableList) { + val item = getItem(position) + holder.bind(item, payloads) + } + + override fun loadMore() { + viewModel.loadMore() + } + + companion object { + + fun createDiffCallback() = object : ItemCallback() { + override fun areItemsTheSame(oldItem: PersonalHistoryEntity, newItem: PersonalHistoryEntity): Boolean { + return oldItem.id == newItem.id + } + + override fun areContentsTheSame(oldItem: PersonalHistoryEntity, newItem: PersonalHistoryEntity): Boolean { + return oldItem.title == newItem.title + && oldItem.brief == newItem.brief + && oldItem.user.id == newItem.user.id + && oldItem.user.icon == newItem.user.icon + && oldItem.user.name == newItem.user.name + && oldItem.count.vote == newItem.count.vote + && oldItem.status == newItem.status + } + + override fun getChangePayload(oldItem: PersonalHistoryEntity, newItem: PersonalHistoryEntity): Any { + val payloads = mutableListOf() + if (oldItem.images.firstOrNull() != newItem.images.firstOrNull() + || oldItem.imagesInfo.firstOrNull() != newItem.imagesInfo.firstOrNull() + ) { + payloads.add(PAYLOADS_CHANGED_COVER) + } + + val oldTitle = oldItem.title.ifBlank { oldItem.content } + val newTitle = newItem.title.ifBlank { newItem.content } + if (oldTitle != newTitle) { + payloads.add(PAYLOADS_CHANGED_TITLE) + } + + if (oldItem.me.isCommunityArticleVote != newItem.me.isCommunityArticleVote + || oldItem.count.vote != newItem.count.vote + ) { + payloads.add(PAYLOADS_CHANGED_VOTE) + } + return payloads + } + + } + } + + class MyImageArticleViewHolder( + val binding: RecyclerRecommendHomeBinding, + private val listener: OnMyArticleImageListener + ) : RecyclerView.ViewHolder(binding.root) { + + private val itemWidth by lazy { + (DisplayUtils.getScreenWidth(itemView.context) - 20F.dip2px()) / 2F + } + + fun bind(item: PersonalHistoryEntity, payloads: List? = null) { + println("kayn -->bind:$payloads") + fun setCover() { + val imageInfo = item.imagesInfo.firstOrNull() + val imageRadio = ImageArticleEntity.getImageRadio(imageInfo, DRAFT_RADIO_MIN, DRAFT_RADIO_MAX) + binding.ivCover.updateLayoutParams { + height = (itemWidth * imageRadio).toInt() + } + binding.ivCover.display(item.images.firstOrNull()) + } + + fun setTitle() { + val title = item.title.ifBlank { item.content } + binding.tvTitle.goneIf(title.isBlank()) { + binding.tvTitle.text = title + } + } + + fun setVote() { + binding.voteState.setVote(item.me.isCommunityArticleVote, item.count.vote, item.status) + } + + with(binding) { + if (payloads.isNullOrEmpty()) { + ivIcon.displayGameIcon(item.user.icon, null) + tvAuthorName.text = item.user.name + + setTitle() + + setCover() + + setVote() + + } else { + payloads.filterIsInstance>() + .flatten() + .forEach { change -> + when (change) { + PAYLOADS_CHANGED_COVER -> { + setCover() + } + + PAYLOADS_CHANGED_TITLE -> { + setTitle() + } + + PAYLOADS_CHANGED_VOTE -> { + binding.voteState.setVote( + item.me.isCommunityArticleVote, + item.count.vote, + item.status + ) + } + } + } + } + + root.setOnClickListener { + listener.navigateToImageArticleDetailPage(item.id) + } + + setVote() + voteState.setOnClickListener { + val vote = !item.me.isCommunityArticleVote + listener.voteImageArticle(item.id, vote) + } + } + } + + interface OnMyArticleImageListener { + + fun navigateToImageArticleDetailPage(id: String) + + fun voteImageArticle(id: String, vote: Boolean) + } + } +} \ No newline at end of file diff --git a/app/src/main/java/com/gh/gamecenter/forum/home/recommend/adapter/RecommendHomeAdapter.kt b/app/src/main/java/com/gh/gamecenter/forum/home/recommend/adapter/RecommendHomeAdapter.kt new file mode 100644 index 0000000000..c56b950200 --- /dev/null +++ b/app/src/main/java/com/gh/gamecenter/forum/home/recommend/adapter/RecommendHomeAdapter.kt @@ -0,0 +1,196 @@ +package com.gh.gamecenter.forum.home.recommend.adapter + +import android.content.Context +import android.view.ViewGroup +import androidx.core.view.updateLayoutParams +import androidx.recyclerview.widget.DiffUtil.ItemCallback +import androidx.recyclerview.widget.RecyclerView.ViewHolder +import com.gh.gamecenter.common.utils.dip2px +import com.gh.gamecenter.common.utils.display +import com.gh.gamecenter.common.utils.goneIf +import com.gh.gamecenter.common.utils.toBinding +import com.gh.gamecenter.databinding.RecyclerRecommendHomeBinding +import com.gh.gamecenter.entity.ImageArticleEntity +import com.gh.gamecenter.forum.home.recommend.ImageArticleFooterWrapperAdapter +import com.gh.gamecenter.forum.home.recommend.adapter.ImageArticleDraftAdapter.Companion.DRAFT_RADIO_MAX +import com.gh.gamecenter.forum.home.recommend.adapter.ImageArticleDraftAdapter.Companion.DRAFT_RADIO_MIN +import com.gh.gamecenter.forum.home.recommend.viewmodel.ImageArticleHomeViewModel +import com.lzf.easyfloat.utils.DisplayUtils + +class RecommendHomeAdapter( + private val context: Context, + private val viewModel: ImageArticleHomeViewModel +) : + ImageArticleFooterWrapperAdapter( + createDiffCallback() + ) { + + override fun onCreateDataViewHolder(parent: ViewGroup): RecommendHomeViewHolder { + return RecommendHomeViewHolder(parent.toBinding(), object : OnImageArticleListener { + override fun navigateToImageArticleDetailPage(id: String) { + viewModel.navigateToImageArticleDetailPage(id) + } + + override fun voteImageArticle(id: String, vote: Boolean) { + viewModel.useCase.voteImageArticle(id, vote) + } + + }) + } + + + override fun onBindDataViewHolder(holder: RecommendHomeViewHolder, position: Int) { + val item = getItem(position) + holder.bind(item, position) + } + + override fun onBindDataViewHolder(holder: RecommendHomeViewHolder, position: Int, payloads: MutableList) { + holder.bind(getItem(position), position, payloads) + } + + override fun loadMore() { + viewModel.loadMore() + } + + fun notifyItemChanged(result: ImageArticleEntity) { + val position = currentList.indexOfFirst { it.id == result.id } + if (position != -1) { + val newData = currentList.toMutableList() + newData[position] = result + submitList(newData) + } + } + + companion object { + + const val PAYLOADS_CHANGED_COVER = "payloads_changed_cover" + const val PAYLOADS_CHANGED_TITLE = "payloads_changed_title" + const val PAYLOADS_CHANGED_VOTE = "payloads_changed_vote" + + + fun createDiffCallback() = object : ItemCallback() { + override fun areItemsTheSame(oldItem: ImageArticleEntity, newItem: ImageArticleEntity): Boolean { + return oldItem.id == newItem.id + } + + override fun areContentsTheSame(oldItem: ImageArticleEntity, newItem: ImageArticleEntity): Boolean { + return oldItem.title == newItem.title + && oldItem.content == newItem.content + && oldItem.user.id == newItem.user.id + && oldItem.user.icon == newItem.user.icon + && oldItem.user.name == newItem.user.name + && oldItem.count.vote == newItem.count.vote + + } + + override fun getChangePayload(oldItem: ImageArticleEntity, newItem: ImageArticleEntity): Any? { + val payloads = mutableListOf() + if (oldItem.images.firstOrNull() != newItem.images.firstOrNull() + || oldItem.imagesInfos.firstOrNull() != newItem.imagesInfos.firstOrNull() + ) { + payloads.add(PAYLOADS_CHANGED_COVER) + } + + val oldTitle = oldItem.title.ifBlank { oldItem.content } + val newTitle = newItem.title.ifBlank { newItem.content } + if (oldTitle != newTitle) { + payloads.add(PAYLOADS_CHANGED_TITLE) + } + + if (oldItem.me.isCommunityArticleVote != newItem.me.isCommunityArticleVote + || oldItem.count.vote != newItem.count.vote + ) { + payloads.add(PAYLOADS_CHANGED_VOTE) + } + return payloads + } + + } + } + + class RecommendHomeViewHolder( + val binding: RecyclerRecommendHomeBinding, + private val listener: OnImageArticleListener + ) : ViewHolder(binding.root) { + + private val itemWidth by lazy { + (DisplayUtils.getScreenWidth(itemView.context) - 20F.dip2px()) / 2F + } + + fun bind(item: ImageArticleEntity, position: Int, payloads: MutableList = mutableListOf()) { + fun setCover() { + val imageInfo = item.imagesInfos.firstOrNull() + val imageRadio = ImageArticleEntity.getImageRadio(imageInfo, DRAFT_RADIO_MIN, DRAFT_RADIO_MAX) + binding.ivCover.updateLayoutParams { + height = (itemWidth * imageRadio).toInt() + } + binding.ivCover.display(item.images.firstOrNull()) + } + + fun setTitle() { + val title = item.title.ifBlank { item.content } + binding.tvTitle.goneIf(title.isBlank()){ + binding.tvTitle.text = title + } + } + + fun setVote() { + binding.voteState.setVote(item.me.isCommunityArticleVote, item.count.vote, item.status) + } + + with(binding) { + if (payloads.isEmpty()) { + ivIcon.displayGameIcon(item.user.icon, null) + tvAuthorName.text = item.user.name + + setTitle() + + setCover() + + setVote() + + } else { + payloads.filterIsInstance>() + .flatten() + .forEach { change -> + when (change) { + PAYLOADS_CHANGED_COVER -> { + setCover() + } + + PAYLOADS_CHANGED_TITLE -> { + setTitle() + } + + PAYLOADS_CHANGED_VOTE -> { + setVote() + } + } + } + } + + root.setOnClickListener { + listener.navigateToImageArticleDetailPage(item.id) + + } + voteState.setOnClickListener { + val vote = !item.me.isCommunityArticleVote + listener.voteImageArticle(item.id, vote) + } + + } + } + } + + interface OnImageArticleListener { + + + + + + + fun navigateToImageArticleDetailPage(id: String) + + fun voteImageArticle(id: String, vote: Boolean) + } +} \ No newline at end of file diff --git a/app/src/main/java/com/gh/gamecenter/forum/home/recommend/fragment/ImageArticleCommentFragment.kt b/app/src/main/java/com/gh/gamecenter/forum/home/recommend/fragment/ImageArticleCommentFragment.kt new file mode 100644 index 0000000000..247e234dd4 --- /dev/null +++ b/app/src/main/java/com/gh/gamecenter/forum/home/recommend/fragment/ImageArticleCommentFragment.kt @@ -0,0 +1,136 @@ +package com.gh.gamecenter.forum.home.recommend.fragment + +import android.os.Bundle +import android.view.View +import androidx.core.content.ContextCompat +import androidx.fragment.app.Fragment +import androidx.fragment.app.viewModels +import androidx.lifecycle.Observer +import androidx.recyclerview.widget.RecyclerView +import com.gh.gamecenter.R +import com.gh.gamecenter.common.baselist.ListAdapter +import com.gh.gamecenter.common.baselist.ListFragment +import com.gh.gamecenter.common.constant.Constants +import com.gh.gamecenter.common.eventbus.EBReuse +import com.gh.gamecenter.common.utils.goneIf +import com.gh.gamecenter.common.utils.viewModelProvider +import com.gh.gamecenter.common.view.CustomDividerItemDecoration +import com.gh.gamecenter.forum.home.recommend.adapter.ImageArticleDetailCommentAdapter +import com.gh.gamecenter.forum.home.recommend.viewmodel.ImageArticleCommentViewModel +import com.gh.gamecenter.forum.home.recommend.viewmodel.ImageArticleDetailViewModel +import com.gh.gamecenter.livedata.EventObserver +import com.gh.gamecenter.qa.article.detail.CommentItemData +import com.gh.gamecenter.qa.comment.base.BaseCommentAdapter +import com.halo.assistant.HaloApp +import org.greenrobot.eventbus.Subscribe +import org.greenrobot.eventbus.ThreadMode + +class ImageArticleCommentFragment : ListFragment() { + + private val parentViewModel by viewModels(ownerProducer = { parentFragment ?: this }) + + private val adapter by lazy { + ImageArticleDetailCommentAdapter( + requireContext(), + mListViewModel, + BaseCommentAdapter.AdapterType.COMMENT, + mEntrance + ) + } + + override fun provideListAdapter(): ListAdapter<*> = adapter + + override fun provideListViewModel(): ImageArticleCommentViewModel { + return viewModelProvider( + ImageArticleCommentViewModel.Factory( + HaloApp.getInstance().application, + arguments?.getString(KEY_IMAGE_ARTICLE_ID) ?: "" + ) + ) + } + + override fun getItemDecoration(): RecyclerView.ItemDecoration { + + val drawable = ContextCompat.getDrawable(requireContext(), R.drawable.divider_article_detail_comment) + val itemDecoration = CustomDividerItemDecoration( + requireContext(), + notDecorateTheFirstItem = true, + notDecorateTheLastItem = true + ) + + itemDecoration.setDrawable(drawable!!) + mItemDecoration = itemDecoration + return itemDecoration + } + + override fun addSyncPageObserver() = true + + override fun provideSyncAdapter() = adapter + + override fun getLayoutId(): Int { + return R.layout.fragment_list_article_detail_comment + } + + override fun onCreate(savedInstanceState: Bundle?) { + super.onCreate(savedInstanceState) + with(parentViewModel) { + sortType.observe(this@ImageArticleCommentFragment, EventObserver { + mListViewModel.changeSort(it) + }) + + insertNewestCommentAction.observe(this@ImageArticleCommentFragment, EventObserver { + mListViewModel.insertNewestComment(it) + }) + + commentCount.observe(this@ImageArticleCommentFragment) { + mListViewModel.commentCount = it + } + } + } + + override fun onViewCreated(view: View, savedInstanceState: Bundle?) { + super.onViewCreated(view, savedInstanceState) + mListRefresh?.isEnabled = false + + with(mListViewModel) { + updateCommentCountAction.observe(viewLifecycleOwner, EventObserver { + parentViewModel.updateComment("", commentCount) + }) + } + } + + override fun onLoadError() { + super.onLoadError() + mListRv.goneIf(false) + } + + override fun onLoadNotFound() { + super.onLoadNotFound() + mListRv.goneIf(false) + } + + override fun onLoadEmpty() { + super.onLoadEmpty() + mListRv.goneIf(false) + } + + + override fun onDarkModeChanged() { + super.onDarkModeChanged() + if (mListRv != null) { + mListRv.removeItemDecoration(mItemDecoration) + mListRv.addItemDecoration(itemDecoration) + } + } + + companion object { + private const val KEY_IMAGE_ARTICLE_ID = "key_image_article_id" + + fun newInstance(imageArticleId: String): Fragment = + ImageArticleCommentFragment().apply { + arguments = Bundle().apply { + putString(KEY_IMAGE_ARTICLE_ID, imageArticleId) + } + } + } +} \ No newline at end of file diff --git a/app/src/main/java/com/gh/gamecenter/forum/home/recommend/fragment/ImageArticleDetailFragment.kt b/app/src/main/java/com/gh/gamecenter/forum/home/recommend/fragment/ImageArticleDetailFragment.kt new file mode 100644 index 0000000000..f74901e50a --- /dev/null +++ b/app/src/main/java/com/gh/gamecenter/forum/home/recommend/fragment/ImageArticleDetailFragment.kt @@ -0,0 +1,790 @@ +package com.gh.gamecenter.forum.home.recommend.fragment + +import android.app.Activity +import android.content.Intent +import android.graphics.Path +import android.os.Bundle +import android.os.Handler +import android.os.Looper +import android.os.Message +import android.view.View +import androidx.appcompat.app.AppCompatActivity +import androidx.core.content.ContextCompat +import androidx.core.view.updateLayoutParams +import androidx.core.widget.TextViewCompat +import androidx.fragment.app.viewModels +import androidx.lifecycle.Lifecycle +import androidx.viewpager2.widget.ViewPager2.OnPageChangeCallback +import com.alibaba.android.arouter.facade.annotation.Autowired +import com.alibaba.android.arouter.launcher.ARouter +import com.gh.common.databind.BindingAdapters +import com.gh.common.util.BbsReportHelper +import com.gh.common.util.DirectUtils +import com.gh.common.util.DownloadItemUtils +import com.gh.common.util.NewsUtils +import com.gh.download.DownloadManager +import com.gh.gamecenter.GameDetailActivity +import com.gh.gamecenter.R +import com.gh.gamecenter.adapter.viewholder.GameViewHolder +import com.gh.gamecenter.common.base.fragment.BaseFragment +import com.gh.gamecenter.common.baselist.LoadStatus +import com.gh.gamecenter.common.constant.Constants +import com.gh.gamecenter.common.constant.EntranceConsts +import com.gh.gamecenter.common.constant.RouteConsts +import com.gh.gamecenter.common.entity.AdditionalParamsEntity +import com.gh.gamecenter.common.entity.NormalShareEntity +import com.gh.gamecenter.common.eventbus.EBReuse +import com.gh.gamecenter.common.exposure.ExposureSource +import com.gh.gamecenter.common.utils.* +import com.gh.gamecenter.core.utils.ToastUtils +import com.gh.gamecenter.databinding.FragmentImageArticleDetailBinding +import com.gh.gamecenter.entity.ImageArticleEntity +import com.gh.gamecenter.entity.MenuItemEntity +import com.gh.gamecenter.eventbus.EBDownloadStatus +import com.gh.gamecenter.eventbus.EBImageArticleChanged +import com.gh.gamecenter.eventbus.EBPackage +import com.gh.gamecenter.feature.entity.GameEntity +import com.gh.gamecenter.feature.entity.ImageInfo +import com.gh.gamecenter.feature.entity.Permissions +import com.gh.gamecenter.feature.exposure.ExposureEvent +import com.gh.gamecenter.forum.home.recommend.PublishImageArticleActivity +import com.gh.gamecenter.forum.home.recommend.adapter.ImageArticleDetailBannerAdapter +import com.gh.gamecenter.forum.home.recommend.viewmodel.ImageArticleDetailViewModel +import com.gh.gamecenter.livedata.EventObserver +import com.gh.gamecenter.login.user.UserManager +import com.gh.gamecenter.qa.comment.CommentActivity +import com.gh.gamecenter.qa.comment.base.BaseCommentViewModel +import com.gh.gamecenter.qa.dialog.MoreFunctionPanelDialog +import com.gh.gamecenter.qa.entity.ArticleDetailEntity +import com.lightgame.download.DataWatcher +import com.lightgame.download.DownloadEntity +import org.greenrobot.eventbus.EventBus +import org.greenrobot.eventbus.Subscribe +import org.greenrobot.eventbus.ThreadMode + +class ImageArticleDetailFragment : BaseFragment() { + + private val viewModel by viewModels() + + @JvmField + @Autowired(name = EntranceConsts.KEY_IMAGE_ARTICLE_ID) + var imageArticleId = "" + + @JvmField + @Autowired(name = EntranceConsts.KEY_TOP_COMMENT_ID) + var topCommentId: String? = null + + private var showIndicator = false + + private val binding: FragmentImageArticleDetailBinding by lazy { + FragmentImageArticleDetailBinding.inflate(layoutInflater) + } + + private val adapter by lazy { + ImageArticleDetailBannerAdapter() + } + + private val handler = object : Handler(Looper.getMainLooper()) { + override fun handleMessage(msg: Message) { + super.handleMessage(msg) + if (msg.what == HIDE_BANNER_NUMBER_INDICATOR) { + binding.llNumberIndicator.goneIf(true) + } + } + } + + private val dataWatcher = object : DataWatcher() { + override fun onDataChanged(downloadEntity: DownloadEntity) { + setDownloadButton(viewModel.imageArticleDetailEntity.value?.community?.game) + } + + override fun onDataInit(downloadEntity: DownloadEntity) { + onDataChanged(downloadEntity) + } + } + + override fun getInflatedLayout() = binding.root + + override fun getLayoutId() = 0 + + override fun onCreate(savedInstanceState: Bundle?) { + super.onCreate(savedInstanceState) + ARouter.getInstance().inject(this) + } + + override fun onViewCreated(view: View, savedInstanceState: Bundle?) { + super.onViewCreated(view, savedInstanceState) + + val tag = ImageArticleDetailFragment::class.java.name + val fragment = + childFragmentManager.findFragmentByTag(tag) ?: ImageArticleCommentFragment.newInstance(imageArticleId) + childFragmentManager.beginTransaction() + .replace(R.id.fcv_comment_container, fragment, tag) + .commitNowAllowingStateLoss() + + with(viewModel) { + pageStatus.observe(viewLifecycleOwner) { + binding.reuseNoConnection.root.goneIf(it != LoadStatus.INIT_FAILED) + binding.clContent.goneIf(it != LoadStatus.INIT_LOADED) + binding.layoutLoading.root.goneIf(it != LoadStatus.INIT_LOADING) + } + + imageArticleDetailEntity.observe(viewLifecycleOwner) { + fillUi(it) + } + + isUserFollowed.observe(viewLifecycleOwner) { + binding.ctvFollow.isChecked = it + binding.ctvFollow.setText( + if (it) { + R.string.concerned + } else { + R.string.follow + } + ) + } + + commentCount.observe(viewLifecycleOwner) { + binding.inputContainer.bottomCommentTv.text = if (it == 0) { + R.string.comment.toResString() + } else { + "$it" + } + binding.tvCommentCount.text = "$it" + } + + vote.observe(viewLifecycleOwner) { + val (isVoted, count) = it + val (imageDrawableResId, textColorResId) = if (isVoted) { + R.drawable.ic_article_detail_liked_bottom_bar to R.color.text_theme + } else { + R.drawable.ic_article_detail_like_bottom_bar to R.color.text_secondary + } + binding.inputContainer.bottomLikeIv.setImageResource(imageDrawableResId) + binding.inputContainer.bottomLikeTv.setTextColor(textColorResId.toColor(requireContext())) + binding.inputContainer.bottomLikeTv.text = if (count == 0) { + R.string.agree.toResString() + } else { + "$count" + } + + } + + isBbsFollowed.observe(viewLifecycleOwner) { + binding.ctvFollowBbs.isChecked = it + binding.ctvFollowBbs.setText( + if (it) { + R.string.concerned + } else { + R.string.follow + } + ) + } + + isFavorite.observe(viewLifecycleOwner) { + val (drawableResId, textColorResId) = if (it) { + R.drawable.ic_article_detail_stared_bottom_bar to R.color.text_theme + } else { + R.drawable.ic_article_detail_star_bottom_bar to R.color.text_secondary + } + binding.inputContainer.bottomStarIv.setImageResource(drawableResId) + binding.inputContainer.bottomStarTv.setTextColor(textColorResId.toColor(requireContext())) + binding.inputContainer.bottomStarTv.setText( + if (it) { + R.string.already_saved + } else { + R.string.menu_collect + } + ) + } + + applyHighlightAction.observe(viewLifecycleOwner, EventObserver { + val toastResId = if (it) { + R.string.apply_successfully + } else { + R.string.apply_failure + } + toast(toastResId) + }) + + addHighlightAction.observe(viewLifecycleOwner, EventObserver { + toast(it) + }) + + deleteOrHideImageArticleSuccessfully.observe(viewLifecycleOwner, EventObserver { + val (isSuccess, toastResId) = it + toast(toastResId) + if (isSuccess) { + EventBus.getDefault().post(EBImageArticleChanged.DataDeleted(imageArticleId)) + requireActivity().finish() + } + }) + + loadImageArticleDetail(this@ImageArticleDetailFragment.imageArticleId) + } + } + + override fun initView(view: View?) { + super.initView(view) + binding.vpBanner.adapter = adapter + + binding.vMore.setOnClickListener { + showMoreItemDialog() + } + binding.sfvOrder.setItemList( + listOf(R.string.positive_older.toResString(), R.string.reverse_order.toResString()), 0 + ) + + binding.inputContainer.replyTv.setText(R.string.message_detail_comment_hint2) + binding.inputContainer.replyTv.setRoundedColorBackground(R.color.ui_container_2, 19F) + binding.inputContainer.replyTv.setDebouncedClickListener { + clickToastByStatus(viewModel.imageArticleDetailEntity.value?.status ?: "") { + val imageArticle = viewModel.imageArticleDetailEntity.value ?: return@clickToastByStatus + val intent = CommentActivity.getImageArticleCommentIntent( + requireContext(), + imageArticle.id, + imageArticle.communityId, + viewModel.commentCount.value + ) + startActivityForResult(intent, CommentActivity.REQUEST_CODE) + } + } + binding.inputContainer.bottomCommentIv.setOnClickListener { + binding.appBar.setExpanded(false, true) + } + binding.inputContainer.bottomCommentTv.setOnClickListener { binding.inputContainer.bottomCommentIv.performClick() } + + binding.inputContainer.bottomStarIv.setOnClickListener { + requireContext().ifLogin(entrance = "帖子详情-收藏") { + clickToastByStatus(viewModel.imageArticleDetailEntity.value?.status ?: "") { + viewModel.collect() + } + } + } + binding.inputContainer.bottomStarTv.setOnClickListener { binding.inputContainer.bottomStarIv.performClick() } + + binding.inputContainer.bottomLikeIv.setOnClickListener { + requireContext().ifLogin("帖子详情-赞同") { + clickToastByStatus(viewModel.imageArticleDetailEntity.value?.status ?: "") { + viewModel.vote() + } + } + } + binding.inputContainer.bottomLikeTv.setOnClickListener { binding.inputContainer.bottomLikeIv.performClick() } + + binding.sfvOrder.setOnCheckedCallback { + val newSort = if (it == 0) { + BaseCommentViewModel.SortType.OLDEST + } else { + BaseCommentViewModel.SortType.LATEST + } + viewModel.changeSort(newSort) + } + binding.ctvFollow.setOnClickListener { + ifLogin("图文详情-关注") { + viewModel.followUser() + } + } + binding.vBack.setOnClickListener { + activity?.finish() + } + binding.ivAvatar.setOnClickListener { + DirectUtils.directToHomeActivity( + requireContext(), + viewModel.imageArticleDetailEntity.value?.user?.id, + 1, + mEntrance, + "图文详情" + ) + } + + binding.vpBanner.registerOnPageChangeCallback(object : OnPageChangeCallback() { + override fun onPageSelected(position: Int) { + showNumberIndicator(position) + } + }) + } + + private fun fillUi(imageArticle: ImageArticleEntity) { + binding.tvTitle.goneIf(imageArticle.title.isBlank()) { + binding.tvTitle.text = imageArticle.title + } + binding.tvContent.goneIf(imageArticle.content.isBlank()) { + binding.tvContent.text = imageArticle.content + } + + binding.ivAvatar.display(imageArticle.user.border, imageArticle.user.icon, imageArticle.user.auth?.icon) + binding.tvName.text = imageArticle.user.name + if (imageArticle.user.auth == null) { + binding.tvAuth.goneIf(true) + TextViewCompat.setTextAppearance(binding.tvName, R.style.TextHeadline) + } else { + binding.tvAuth.goneIf(false) + binding.tvAuth.text = imageArticle.user.auth?.text + TextViewCompat.setTextAppearance(binding.tvName, R.style.TextBody2) + } + + binding.ctvFollow.goneIf(imageArticle.me.isContentOwner) + binding.tvTime.text = NewsUtils.getFormattedTime(imageArticle.time.edit) + + val imagesSize = imageArticle.images.size + showIndicator = imagesSize >= SHOW_DOT_INDICATOR_MIN + binding.dorIndicator.goneIf(!showIndicator) { + binding.dorIndicator.show(imagesSize) + } + binding.llNumberIndicator.goneIf(!showIndicator) { + binding.tvIndicatorTotal.text = "/${imagesSize}" + showNumberIndicator(0) + } + + + setBannerSize(imageArticle.imagesInfos.firstOrNull()) + adapter.submitList(imageArticle.images) + + val community = imageArticle.community + val game = community?.game + when { + game != null -> { + // 显示游戏 + showGame(game, imageArticle.community.id) + } + + community != null -> { + showCommunity(community) + } + + else -> { + binding.clBbsContainer.goneIf(true) + binding.clGameContainer.goneIf(true) + } + } + } + + private fun showCommunity(community: ImageArticleEntity.Community) { + binding.clBbsContainer.goneIf(false) + binding.clGameContainer.goneIf(true) + binding.ivBbsIcon.display(community.icon) + binding.tvBbsName.text = community.name + binding.tvHeat.text = "${community.hot}" + binding.clBbsContainer.setOnClickListener { + DirectUtils.directForumDetail(requireContext(), community.id, "帖子详情") + } + binding.ctvFollowBbs.setOnClickListener { + viewModel.followBbs(community.id) + } + } + + private fun showGame(game: GameEntity, communityId: String) { + game.exposureEvent = ExposureEvent.createEventWithSourceConcat( + game, + listOf(), + listOf(ExposureSource("其他"), ExposureSource("图文详情")) + ) + binding.clBbsContainer.goneIf(true) + binding.clGameContainer.goneIf(false) + binding.ivGameIcon.displayGameIcon(game) + binding.tvGameName.text = game.name + binding.tvGameRating.goneIf(!(game.commentCount > 3 && game.star >= 7)) { + binding.tvGameRating.text = game.star.toString() + } + BindingAdapters.setGameTags(binding.gtcvGameTags, game) + + setDownloadButton(game) + binding.clGameContainer.setOnClickListener { + GameDetailActivity.startGameDetailActivity( + requireContext(), + game, + "图文详情", + traceEvent = game.exposureEvent + ) + } + + binding.tvGoBbs.setOnClickListener { + DirectUtils.directForumDetail(requireContext(), communityId, "帖子详情") + } + } + + private fun setDownloadButton(gameEntity: GameEntity?) { + if (gameEntity == null) return + DownloadItemUtils.setOnClickListener( + requireContext(), binding.btnDownload, + gameEntity, 0, null, + mEntrance, + location = "视频详情", + traceEvent = gameEntity.exposureEvent, + clickCallback = { + + }, + refreshCallback = { setDownloadButton(gameEntity) }, + allStateClickCallback = null + ) + DownloadItemUtils.updateItem( + requireContext(), + gameEntity, + GameViewHolder(binding.clGameContainer).apply { + gameDownloadBtn = binding.btnDownload + gameDownloadTips = binding.downloadTipsLottie + multiVersionDownloadTv = binding.multiVersionDownloadTv + } + ) + } + + private fun setBannerSize(imageInfo: ImageInfo?) { + binding.vpBanner.goneIf(imageInfo == null) { + val bannerRadio = ImageArticleEntity.getImageRadio(imageInfo, BANNER_RADIO_MIN, BANNER_RADIO_MAX) + binding.vpBanner.updateLayoutParams { + height = (resources.displayMetrics.widthPixels * bannerRadio).toInt() + } + } + + } + + private fun showNumberIndicator(position: Int) { + if (showIndicator) { + binding.llNumberIndicator.goneIf(false) + binding.tvIndicatorPosition.text = "${position + 1}" + handler.removeMessages(HIDE_BANNER_NUMBER_INDICATOR) + handler.sendEmptyMessageDelayed(HIDE_BANNER_NUMBER_INDICATOR, SHOW_NUMBER_INDICATOR_DURATION) + binding.dorIndicator.selectPosition(position) + } + } + + private fun showMoreItemDialog() { + val imageArticle = viewModel.imageArticleDetailEntity.value + if (lifecycle.currentState.isAtLeast(Lifecycle.State.STARTED) && imageArticle != null) { + val entities = ArrayList() + // 申请加精 + val simplifyChoicenessStatus = imageArticle.getSimplifyChoicenessStatus() + if (imageArticle.user.id == UserManager.getInstance().userId && !imageArticle.me.isModerator && imageArticle.status == "pass") { + val isEnable = + simplifyChoicenessStatus != ArticleDetailEntity.STATUS_PASS + entities.add( + MenuItemEntity( + getString(R.string.article_detail_more_apply_select_title), + if (isEnable) + R.drawable.icon_more_panel_essence else R.drawable.icon_more_panel_essence_unenable, + isEnable = isEnable + ) + ) + } + + // 修改 + if (imageArticle.me.isContentOwner + && imageArticle.status == ArticleDetailEntity.STATUS_PASS + ) { + entities.add( + MenuItemEntity(getString(R.string.article_detail_more_edit_title), R.drawable.icon_more_panel_edit) + ) + } + + // 举报 + if (!imageArticle.me.isContentOwner) { + entities.add( + MenuItemEntity( + getString(R.string.article_detail_more_complaint_title), + R.drawable.icon_gamedetail_copyright + ) + ) + } + + val moderatorPermissions = imageArticle.me.moderatorPermissions + // 取消精选 + if (imageArticle.me.isModerator + && imageArticle.status == ArticleDetailEntity.STATUS_PASS + ) { + if (simplifyChoicenessStatus == ArticleDetailEntity.STATUS_PASS) { + if ((moderatorPermissions.cancelHighlightCommunityArticle) > Permissions.GUEST + ) { + entities.add( + MenuItemEntity( + getString(R.string.article_detail_more_unselect_title), + R.drawable.icon_more_panel_essence_cancel + ) + ) + } + } else { + if ((moderatorPermissions.highlightCommunityArticle) > Permissions.GUEST + ) { + entities.add( + MenuItemEntity( + getString(R.string.article_detail_more_select_title), + R.drawable.icon_more_panel_essence + ) + ) + } + } + } + + // 修改活动标签 + if (imageArticle.me.isModerator && + (moderatorPermissions.updateArticleActivityTag) > Permissions.GUEST && + imageArticle.status == ArticleDetailEntity.STATUS_PASS + ) { + entities.add( + MenuItemEntity( + getString(R.string.article_detail_more_edit_activity_tag_title), + R.drawable.icon_more_panel_modify_label + ) + ) + } + + // 隐藏/删除 + if (imageArticle.me.isModerator && moderatorPermissions.hideCommunityArticle > Permissions.GUEST + ) { + entities.add( + MenuItemEntity( + getString(R.string.article_detail_more_hide_title), + R.drawable.icon_more_panel_delete + ) + ) + } else { + if (imageArticle.me.isContentOwner) { + entities.add( + MenuItemEntity( + getString(R.string.article_detail_more_delete_title), + R.drawable.icon_more_panel_delete + ) + ) + } + } + + // 置顶/取消置顶 + if (imageArticle.me.isModerator + && !imageArticle.me.isCommunityTop + && moderatorPermissions.topCommunityArticle > Permissions.GUEST + ) { + entities.add( + MenuItemEntity( + getString(R.string.article_detail_more_top_title), + R.drawable.icon_more_panel_top + ) + ) + } else if (imageArticle.me.isModerator + && imageArticle.me.isCommunityTop + && moderatorPermissions.cancelTopCommunityArticle > Permissions.GUEST + ) { + entities.add( + MenuItemEntity( + getString(R.string.article_detail_more_cancel_top_title), + R.drawable.icon_more_panel_top_cancel + ) + ) + } + + MoreFunctionPanelDialog.showMoreDialog( + requireActivity() as AppCompatActivity, + entities, + imageArticle.title.ifBlank { imageArticle.content }, + getShareEntity(imageArticle), + imageArticle.status, + tag ?: "" + ) + } + } + + private fun getShareEntity(imageArticle: ImageArticleEntity): NormalShareEntity { + val params = AdditionalParamsEntity().apply { + contentType = "帖子" + contentId = imageArticle.id + bbsId = imageArticle.communityId + bbsType = imageArticle.community?.typeChinese ?: "综合论坛" + customerType = imageArticle.user.auth?.text ?: "" + activityTagName = "" + gameForumType = imageArticle.community?.game?.categoryChinese ?: "" + refUserId = UserManager.getInstance().userId + } + return NormalShareEntity( + id = imageArticle.id, + shareUrl = requireContext().getString( + R.string.share_community_image_article_url, + imageArticle.shortId + ), + shareIcon = if (imageArticle.images.isNotEmpty()) { + imageArticle.images.first() + } else { + requireContext().getString(R.string.share_ghzs_logo) + }, + shareTitle = imageArticle.title.ifBlank { imageArticle.content } + .ifBlank { getString(R.string.image_article) }, + shareSummary = imageArticle.content, + shareEntrance = ShareUtils.ShareEntrance.communityArticle, + additionalParams = params + ) + } + + override fun onResume() { + super.onResume() + DownloadManager.getInstance().addObserver(dataWatcher) + } + + override fun onPause() { + super.onPause() + DownloadManager.getInstance().removeObserver(dataWatcher) + } + + //下载被删除事件 + @Subscribe(threadMode = ThreadMode.MAIN) + fun onEventMainThread(status: EBDownloadStatus) { + if ("delete" == status.status) { + setDownloadButton(viewModel.imageArticleDetailEntity.value?.community?.game) + } + } + + //安装、卸载事件 + @Subscribe(threadMode = ThreadMode.MAIN) + fun onEventMainThread(busFour: EBPackage) { + setDownloadButton(viewModel.imageArticleDetailEntity.value?.community?.game) + } + + @Subscribe(threadMode = ThreadMode.MAIN) + fun onEventMainThread(reuse: EBReuse) { + if (reuse.type == Constants.LOGIN_TAG) { // 登入 + viewModel.loadImageArticleDetail(imageArticleId) + + } + val path = Path() + path.reset() + } + + // + @Subscribe(threadMode = ThreadMode.MAIN) + fun onEventMainThread(changed: EBImageArticleChanged) { + viewModel.imageArticleChanged(changed) + } + + override fun onDarkModeChanged() { + super.onDarkModeChanged() + setDownloadButton(viewModel.imageArticleDetailEntity.value?.community?.game) + } + + override fun onActivityResult(requestCode: Int, resultCode: Int, data: Intent?) { + super.onActivityResult(requestCode, resultCode, data) + when { + requestCode == CommentActivity.REQUEST_CODE && resultCode == Activity.RESULT_OK -> { + val commentCount = data?.getIntExtra(CommentActivity.COMMENT_COUNT, 0) ?: 0 + val commentId = data?.getStringExtra(EntranceConsts.KEY_COMMENT_ID) ?: "" + viewModel.updateComment(commentId, commentCount) + } + + requestCode == MoreFunctionPanelDialog.REQUEST_CODE && resultCode == Activity.RESULT_OK -> { + getItemClickCallback().invoke(data?.getParcelableExtra(EntranceConsts.KEY_DATA)) + } + } + } + + private fun getItemClickCallback(): (menuItem: MenuItemEntity?) -> Unit { + return { + when (it?.text) { + getString(R.string.article_detail_more_edit_title) -> { + ARouter.getInstance().build(RouteConsts.activity.publishImageArticleActivity) + .withParcelable( + EntranceConsts.KEY_IMAGE_ARTICLE_ENTITY, + viewModel.imageArticleDetailEntity.value + ) + .navigation() + } + + getString(R.string.article_detail_more_complaint_title) -> { + ifLogin("图文详情") { + BbsReportHelper.showReportDialog(BbsReportHelper.ImageArticleReporter(imageArticleId)) + } + + } + + getString(R.string.article_detail_more_apply_select_title) -> { + if (viewModel.imageArticleDetailEntity.value?.getSimplifyChoicenessStatus() == "apply") { + ToastUtils.showToast("申请加精审核中") + } else { + viewModel.applyHighlightForImageArticle() + } + } + + getString(R.string.article_detail_more_select_title) -> { + if (viewModel.imageArticleDetailEntity.value?.getSimplifyChoicenessStatus() == "apply") { + ToastUtils.showToast("加精审核中") + } else { + showHighlightDialog(true) + } + } + + getString(R.string.article_detail_more_unselect_title) -> { + showHighlightDialog(false) + } + + getString(R.string.article_detail_more_delete_title), + getString(R.string.article_detail_more_hide_title) -> { + DialogHelper.showDialog( + requireContext(), + "提示", + "${it.text}帖子后,其中的所有评论及回复都将被${it.text}", + it.text, + "取消", + confirmClickCallback = { + viewModel.deleteOrHideImageArticle() + }, extraConfig = DialogHelper.Config(centerTitle = true, centerContent = true) + ) + } + + getString(R.string.article_detail_more_top_title) -> { +// TopCommunityCategoryDialog.show( +// childFragmentManager +// ) { category -> +// mViewModel.topCommunityArticle(category.id) +// } + } + + getString(R.string.article_detail_more_cancel_top_title) -> { + DialogHelper.showDialog( + requireContext(), + getString(R.string.article_detail_cancel_top_dialog_title), + getString(R.string.article_detail_cancel_top_dialog_hint), + getString(R.string.article_detail_cancel_top_dialog_confirm), + getString(R.string.article_detail_cancel_top_dialog_cancel), + confirmClickCallback = { +// mViewModel.cancelTopCommunityArticle() + }, + extraConfig = DialogHelper.Config(centerTitle = true, centerContent = true) + ) + } + } + } + } + + private fun showHighlightDialog(isHighlight: Boolean) { + val imageArticle = viewModel.imageArticleDetailEntity.value ?: return + var highlightDialogHintContent = "" + val permissions = imageArticle.me.moderatorPermissions + if ((isHighlight && permissions.highlightCommunityArticle > Permissions.GUEST) || + (!isHighlight && permissions.cancelHighlightCommunityArticle > Permissions.GUEST) + ) { + highlightDialogHintContent = + if ((isHighlight && permissions.highlightCommunityArticle == Permissions.REPORTER) || + (!isHighlight && permissions.cancelHighlightCommunityArticle == Permissions.REPORTER) + ) { + "你的操作将提交给小编审核,确定提交吗?" + } else { + "你的操作将立即生效,确定提交吗?" + } + } + val title = if (isHighlight) "加精帖子" else "取消精选" + DialogHelper.showDialog( + requireContext(), + title, + highlightDialogHintContent, + "确定", + "取消", + { + viewModel.addHighlight(isHighlight) + }, extraConfig = DialogHelper.Config(centerTitle = true, centerContent = true) + ) + } + + companion object { + private const val HIDE_BANNER_NUMBER_INDICATOR = 1 + private const val SHOW_NUMBER_INDICATOR_DURATION = 5000L + private const val SHOW_DOT_INDICATOR_MIN = 2 + private const val BANNER_RADIO_MIN = 0.5F + private const val BANNER_RADIO_MAX = 1.33F + + } +} \ No newline at end of file diff --git a/app/src/main/java/com/gh/gamecenter/forum/home/recommend/fragment/ImageArticleDraftFragment.kt b/app/src/main/java/com/gh/gamecenter/forum/home/recommend/fragment/ImageArticleDraftFragment.kt new file mode 100644 index 0000000000..50390bb017 --- /dev/null +++ b/app/src/main/java/com/gh/gamecenter/forum/home/recommend/fragment/ImageArticleDraftFragment.kt @@ -0,0 +1,161 @@ +package com.gh.gamecenter.forum.home.recommend.fragment + +import android.graphics.Rect +import android.os.Bundle +import android.view.View +import androidx.fragment.app.activityViewModels +import androidx.fragment.app.viewModels +import androidx.recyclerview.widget.RecyclerView +import androidx.recyclerview.widget.StaggeredGridLayoutManager +import com.alibaba.android.arouter.launcher.ARouter +import com.gh.gamecenter.R +import com.gh.gamecenter.common.base.fragment.LazyFragment +import com.gh.gamecenter.common.baselist.PageLoader +import com.gh.gamecenter.common.constant.EntranceConsts +import com.gh.gamecenter.common.constant.RouteConsts +import com.gh.gamecenter.common.utils.DialogHelper +import com.gh.gamecenter.common.utils.dip2px +import com.gh.gamecenter.common.utils.goneIf +import com.gh.gamecenter.common.utils.toResString +import com.gh.gamecenter.databinding.FragmentImageArticleDraftBinding +import com.gh.gamecenter.eventbus.EBImageArticleChanged +import com.gh.gamecenter.forum.home.recommend.adapter.ImageArticleDraftAdapter +import com.gh.gamecenter.forum.home.recommend.viewmodel.ImageArticleDraftViewModel +import com.gh.gamecenter.forum.home.recommend.viewmodel.PublishImageArticleActivityViewModel +import com.gh.gamecenter.livedata.EventObserver +import org.greenrobot.eventbus.Subscribe +import org.greenrobot.eventbus.ThreadMode + +class ImageArticleDraftFragment : LazyFragment() { + + private val viewModel by viewModels() + private val activityViewModel by activityViewModels() + + private lateinit var binding: FragmentImageArticleDraftBinding + + private var isFromPublishPage = false + + private val adapter by lazy { + ImageArticleDraftAdapter(requireContext(), viewModel) + } + + override fun getRealLayoutId(): Int { + return R.layout.fragment_image_article_draft + } + + override fun onRealLayoutInflated(inflatedView: View) { + binding = FragmentImageArticleDraftBinding.bind(inflatedView) + } + + override fun initRealView() { + super.initRealView() + isFromPublishPage = arguments?.getBoolean(KEY_IS_FROM_PUBLISH_PAGE) ?: false + binding.rvDrafts.layoutManager = StaggeredGridLayoutManager(2, StaggeredGridLayoutManager.VERTICAL) + binding.rvDrafts.addItemDecoration(MyItemDecoration()) + binding.rvDrafts.adapter = adapter + + binding.srlRefresh.setOnRefreshListener { + viewModel.loadDraft(true) + } + + with(viewModel) { + pageState.observe(viewLifecycleOwner) { + + binding.srlRefresh.goneIf( + it == PageLoader.PageState.PageNoData || it == PageLoader.PageState.PageInitFailure + ) { + binding.srlRefresh.isRefreshing = false + } + binding.reuseLlLoading.root.goneIf(it != PageLoader.PageState.PageInitLoading) + binding.reuseNoneData.root.goneIf(it != PageLoader.PageState.PageNoData) + binding.reuseDataException.root.goneIf(it != PageLoader.PageState.PageInitFailure) + + } + + drafts.observe(viewLifecycleOwner) { + adapter.submitList(it) + } + + showDeleteDraftDialogAction.observe(viewLifecycleOwner, EventObserver { + DialogHelper.showDialog( + requireContext(), + R.string.warning.toResString(), + R.string.delete_image_article_warning.toResString(), + R.string.confirm.toResString(), + R.string.cancel.toResString(), + confirmClickCallback = { + viewModel.deleteDraft(it) + }, extraConfig = DialogHelper.Config(centerTitle = true, centerContent = true) + ) + }) + + publishImageArticleDestination.observe(viewLifecycleOwner, EventObserver { + it.isDraft = true + if (isFromPublishPage) { + // 如果是从发布页进入草稿箱,则回到发布页,并将草稿数据回传 + activityViewModel.setEditImageArticle(it) + activity?.onBackPressedDispatcher?.onBackPressed() + } else { + // 否则,直接打开新的发布页 + ARouter.getInstance().build(RouteConsts.activity.publishImageArticleActivity) + .withParcelable(EntranceConsts.KEY_IMAGE_ARTICLE_ENTITY, it) + .navigation() + } + }) + + loadDraft(false) + } + } + + @Subscribe(threadMode = ThreadMode.MAIN) + fun onEventMainThread(changed: EBImageArticleChanged) { + if (changed is EBImageArticleChanged.DataCreated + && changed.draftId.isNotBlank() + ) { + // 如果时选择的草稿发布,则删除当前选中草稿 + val draftList = viewModel.drafts.value?.toMutableList() + if (draftList != null) { + val hasRemoved = draftList.removeAll { it.id == changed.draftId } + if (hasRemoved) { + if (draftList.isEmpty()) { + viewModel.loadDraft(true) + } else { + adapter.submitList(null) + adapter.notifyDataSetChanged() + viewModel.updateDrafts(draftList) + } + } + } + } + } + + private class MyItemDecoration : RecyclerView.ItemDecoration() { + + override fun getItemOffsets(outRect: Rect, view: View, parent: RecyclerView, state: RecyclerView.State) { + super.getItemOffsets(outRect, view, parent, state) + val layoutParams = view.layoutParams + if (layoutParams is StaggeredGridLayoutManager.LayoutParams) { + val spanIndex = layoutParams.spanIndex + val (left, right) = if (spanIndex == 0) { + 8F.dip2px() to 2F.dip2px() + } else { + 2F.dip2px() to 8F.dip2px() + } + outRect.left = left + outRect.right = right + } + } + } + + companion object { + + private const val KEY_IS_FROM_PUBLISH_PAGE = "key_is_from_publish_page" + + fun newInstance(isFromPublishPage: Boolean) = + ImageArticleDraftFragment().apply { + arguments = Bundle().apply { + putBoolean(KEY_IS_FROM_PUBLISH_PAGE, isFromPublishPage) + } + } + } +} \ No newline at end of file diff --git a/app/src/main/java/com/gh/gamecenter/forum/home/recommend/fragment/ImageArticleHomeFragment.kt b/app/src/main/java/com/gh/gamecenter/forum/home/recommend/fragment/ImageArticleHomeFragment.kt new file mode 100644 index 0000000000..f4cad8749c --- /dev/null +++ b/app/src/main/java/com/gh/gamecenter/forum/home/recommend/fragment/ImageArticleHomeFragment.kt @@ -0,0 +1,233 @@ +package com.gh.gamecenter.forum.home.recommend.fragment + +import android.graphics.Rect +import android.os.Bundle +import android.os.Handler +import android.os.Looper +import android.view.View +import androidx.fragment.app.viewModels +import androidx.recyclerview.widget.RecyclerView +import androidx.recyclerview.widget.StaggeredGridLayoutManager +import com.alibaba.android.arouter.launcher.ARouter +import com.ethanhua.skeleton.Skeleton +import com.gh.gamecenter.R +import com.gh.gamecenter.common.base.fragment.LazyFragment +import com.gh.gamecenter.common.baselist.PageLoader +import com.gh.gamecenter.common.constant.Constants +import com.gh.gamecenter.common.constant.EntranceConsts +import com.gh.gamecenter.common.constant.RouteConsts +import com.gh.gamecenter.common.utils.dip2px +import com.gh.gamecenter.common.utils.goneIf +import com.gh.gamecenter.common.utils.toJson +import com.gh.gamecenter.databinding.FragmentImageArticleHomeBinding +import com.gh.gamecenter.eventbus.EBImageArticleChanged +import com.gh.gamecenter.forum.home.CommunityHomeViewModel +import com.gh.gamecenter.forum.home.recommend.ImageArticleDetailActivity +import com.gh.gamecenter.forum.home.recommend.ImageArticleFooterWrapperAdapter +import com.gh.gamecenter.forum.home.recommend.adapter.RecommendHomeAdapter +import com.gh.gamecenter.forum.home.recommend.viewmodel.ImageArticleHomeViewModel +import com.gh.gamecenter.livedata.EventObserver +import org.greenrobot.eventbus.Subscribe +import org.greenrobot.eventbus.ThreadMode + +class ImageArticleHomeFragment : LazyFragment() { + + private val viewModel: ImageArticleHomeViewModel by viewModels() + + private lateinit var binding: FragmentImageArticleHomeBinding + + private val handler = Handler(Looper.getMainLooper()) + + private val skeletonScreen by lazy { + Skeleton.bind(binding.flSkeleton) + .shimmer(true) + .angle(Constants.SHIMMER_ANGLE) + .color(R.color.ui_skeleton_highlight) + .duration(Constants.SHIMMER_DURATION) + .maskWidth(Constants.MASK_WIDTH) + .gradientCenterColorWidth(Constants.GRADIENT_CENTER_COLOR_WIDTH) + .load(R.layout.fragment_image_article_home_skeleton) + .show() + } + + private val adapter by lazy { + RecommendHomeAdapter(requireContext(), viewModel) + } + + override fun getRealLayoutId(): Int = R.layout.fragment_image_article_home + + override fun onRealLayoutInflated(inflatedView: View) { + binding = FragmentImageArticleHomeBinding.bind(inflatedView) + } + + override fun initRealView() { + super.initRealView() + binding.rvRecommends.layoutManager = StaggeredGridLayoutManager(2, StaggeredGridLayoutManager.VERTICAL).also { + it.gapStrategy = StaggeredGridLayoutManager.GAP_HANDLING_NONE + } + binding.rvRecommends.adapter = adapter + binding.rvRecommends.addItemDecoration(MyItemDecoration()) + + binding.srlRefresh.setOnRefreshListener { + viewModel.pageLoader.pullToRefresh() + } + + binding.reuseNoConnection.connectionReloadTv.setOnClickListener { + viewModel.initLoad() + } + + with(viewModel) { + pageLoader.pageState.observe(viewLifecycleOwner) { + if (it == PageLoader.PageState.PageInitLoading) { + skeletonScreen + } else { + skeletonScreen.hide() + } + + if (it is PageLoader.PageState.PagePullToRefreshLoading) { + // 正在下拉刷新,页面保持不变 + binding.srlRefresh.isRefreshing = true + } else { + binding.reuseNoneData.root.goneIf(it != PageLoader.PageState.PageNoData) + binding.reuseNoConnection.root.goneIf(it != PageLoader.PageState.PageInitFailure) + + binding.srlRefresh.isEnabled = it != PageLoader.PageState.PageInitLoading + && it != PageLoader.PageState.PageInitFailure + binding.srlRefresh.isRefreshing = false + } + + if (it is PageLoader.PageState.PageLoadMoreReady + && (it.previousState is PageLoader.PageState.PagePullToRefreshLoading + || it.previousState is PageLoader.PageState.PageInitLoading) + ) { + // 加载第一页完成 + if (it.previousState is PageLoader.PageState.PagePullToRefreshLoading) { + // 下拉刷新成功,先清空上一次内容 + adapter.submitList(null) + adapter.notifyDataSetChanged() + } + // 延时一下,检查第一页数据是否已铺满第一页,如果未铺满,则直接请求下一页 + handler.removeCallbacksAndMessages(null) + handler.postDelayed({ + checkIfLoadNextPage(binding.rvRecommends, viewModel::loadMore) + }, 16) + } + adapter.setPageState(it) + + } + + pageLoader.dataList.observe(viewLifecycleOwner) { + adapter.submitList(it) + } + + imageDetailDestination.observe(viewLifecycleOwner, EventObserver { + ARouter.getInstance().build(RouteConsts.activity.imageArticleDetailActivity) + .withString(EntranceConsts.KEY_IMAGE_ARTICLE_ID, it) + .navigation() + }) + + initLoad() + } + } + + @Subscribe(threadMode = ThreadMode.MAIN) + fun onEventMainThread(changed: EBImageArticleChanged) { + when (changed) { + is EBImageArticleChanged.VoteChanged -> { + viewModel.voteChanged(changed) + } + + is EBImageArticleChanged.DataChanged -> { + viewModel.itemChanged(changed) + } + + is EBImageArticleChanged.DataDeleted -> { + itemDeleted(changed) + } + + is EBImageArticleChanged.DataCreated -> { + itemCreated(changed) + } + + else -> Unit + + } + } + + private fun itemCreated(changed: EBImageArticleChanged.DataCreated) { + val newData = viewModel.pageLoader.dataList.value?.toMutableList() ?: mutableListOf() + if (newData.isEmpty()) { + viewModel.initLoad() + } else { + // 瀑布流布局,在中间添加或者移除item时,会出现错位的现象,这里需要先将原来的数据清空,在重新添加可以避免这个问题 + adapter.submitList(null) + newData.add(0, changed.imageArticle) + viewModel.pageLoader.updateData(newData) + } + } + + private fun itemDeleted(changed: EBImageArticleChanged.DataDeleted) { + val oldData = viewModel.pageLoader.dataList.value ?: return + val newData = oldData.toMutableList() + val hasRemoved = newData.removeAll { + it.id == changed.imageArticleId + } + if (hasRemoved) { + if (newData.isEmpty()) { + viewModel.initLoad() + } else { + adapter.submitList(null) + viewModel.pageLoader.updateData(newData) + } + } + + } + + override fun onDestroyView() { + super.onDestroyView() + handler.removeCallbacksAndMessages(null) + } + + companion object { + + private const val KEY_IS_IN_MY_PUBLISH = "key_is_in_my_publish" + + fun newInstance() = ImageArticleHomeFragment() + + fun checkIfLoadNextPage(recyclerView: RecyclerView, loadNext: () -> Unit) { + val layoutManager = recyclerView.layoutManager + if (layoutManager is StaggeredGridLayoutManager) { + val itemPositions = layoutManager.findLastCompletelyVisibleItemPositions(null) + val lastPosition = itemPositions.maxOrNull() ?: 0 + val itemType = recyclerView.adapter?.getItemViewType(lastPosition) ?: 0 + if (itemType == ImageArticleFooterWrapperAdapter.ITEM_TYPE_FOOTER) { + // 底部加载更多的itemView完全显示在屏幕内,说明数据未铺满一屏:直接加载下一页 + loadNext() + } + } + } + } + + class MyItemDecoration : RecyclerView.ItemDecoration() { + + override fun getItemOffsets(outRect: Rect, view: View, parent: RecyclerView, state: RecyclerView.State) { + val layoutParams = view.layoutParams + if (layoutParams is StaggeredGridLayoutManager.LayoutParams) { + if (parent.getChildAdapterPosition(view) == state.itemCount - 1) { + layoutParams.isFullSpan = true + } else { + val spanIndex = layoutParams.spanIndex + val (left, right) = if (spanIndex % 2 == 0) { + 8F.dip2px() to 2F.dip2px() + } else { + 2F.dip2px() to 8F.dip2px() + } + outRect.left = left + outRect.right = right + } + } + } + + + } +} \ No newline at end of file diff --git a/app/src/main/java/com/gh/gamecenter/forum/home/recommend/fragment/MyImageArticleListFragment.kt b/app/src/main/java/com/gh/gamecenter/forum/home/recommend/fragment/MyImageArticleListFragment.kt new file mode 100644 index 0000000000..cd9f8466d1 --- /dev/null +++ b/app/src/main/java/com/gh/gamecenter/forum/home/recommend/fragment/MyImageArticleListFragment.kt @@ -0,0 +1,213 @@ +package com.gh.gamecenter.forum.home.recommend.fragment + +import android.annotation.SuppressLint +import android.graphics.Rect +import android.os.Handler +import android.os.Looper +import android.view.View +import androidx.fragment.app.viewModels +import androidx.recyclerview.widget.RecyclerView +import androidx.recyclerview.widget.StaggeredGridLayoutManager +import com.alibaba.android.arouter.launcher.ARouter +import com.ethanhua.skeleton.Skeleton +import com.gh.gamecenter.R +import com.gh.gamecenter.common.base.fragment.LazyFragment +import com.gh.gamecenter.common.baselist.PageLoader +import com.gh.gamecenter.common.constant.Constants +import com.gh.gamecenter.common.constant.EntranceConsts +import com.gh.gamecenter.common.constant.RouteConsts +import com.gh.gamecenter.common.utils.dip2px +import com.gh.gamecenter.common.utils.goneIf +import com.gh.gamecenter.databinding.FragmentImageArticleHomeBinding +import com.gh.gamecenter.eventbus.EBImageArticleChanged +import com.gh.gamecenter.forum.home.recommend.adapter.MyImageArticleAdapter +import com.gh.gamecenter.forum.home.recommend.fragment.ImageArticleHomeFragment.Companion.checkIfLoadNextPage +import com.gh.gamecenter.forum.home.recommend.viewmodel.MyImageArticleListViewModel +import com.gh.gamecenter.livedata.EventObserver +import org.greenrobot.eventbus.Subscribe +import org.greenrobot.eventbus.ThreadMode + +class MyImageArticleListFragment : LazyFragment() { + + private val handler = Handler(Looper.getMainLooper()) + + private val viewModel by viewModels() + + private lateinit var binding: FragmentImageArticleHomeBinding + + private val skeletonScreen by lazy { + Skeleton.bind(binding.flSkeleton) + .shimmer(true) + .angle(Constants.SHIMMER_ANGLE) + .color(R.color.ui_skeleton_highlight) + .duration(Constants.SHIMMER_DURATION) + .maskWidth(Constants.MASK_WIDTH) + .gradientCenterColorWidth(Constants.GRADIENT_CENTER_COLOR_WIDTH) + .load(R.layout.fragment_image_article_home_skeleton) + .show() + } + + private val adapter by lazy { + MyImageArticleAdapter(requireContext(), viewModel) + } + + override fun getRealLayoutId(): Int = R.layout.fragment_image_article_home + + override fun onRealLayoutInflated(inflatedView: View) { + binding = FragmentImageArticleHomeBinding.bind(inflatedView) + } + + @SuppressLint("NotifyDataSetChanged") + override fun initRealView() { + super.initRealView() + binding.rvRecommends.layoutManager = StaggeredGridLayoutManager(2, StaggeredGridLayoutManager.VERTICAL) + binding.rvRecommends.adapter = adapter + binding.rvRecommends.addItemDecoration(MyItemDecoration()) + + binding.srlRefresh.setOnRefreshListener { + viewModel.pageLoader.pullToRefresh() + } + + binding.reuseNoConnection.connectionReloadTv.setOnClickListener { + viewModel.initLoad() + } + + with(viewModel) { + pageLoader.pageState.observe(viewLifecycleOwner) { + if (it == PageLoader.PageState.PageInitLoading) { + skeletonScreen + } else { + skeletonScreen.hide() + } + + if (it is PageLoader.PageState.PagePullToRefreshLoading) { + // 正在下拉刷新,页面保持不变 + binding.srlRefresh.isRefreshing = true + } else { + binding.reuseNoneData.root.goneIf(it != PageLoader.PageState.PageNoData) + binding.reuseNoConnection.root.goneIf(it != PageLoader.PageState.PageInitFailure) + + binding.srlRefresh.isEnabled = it != PageLoader.PageState.PageInitLoading + && it != PageLoader.PageState.PageInitFailure + binding.srlRefresh.isRefreshing = false + } + + if (it is PageLoader.PageState.PageLoadMoreReady + && (it.previousState is PageLoader.PageState.PagePullToRefreshLoading + || it.previousState is PageLoader.PageState.PageInitLoading) + ) { + if (it.previousState is PageLoader.PageState.PagePullToRefreshLoading) { + adapter.submitList(null) + adapter.notifyDataSetChanged() + } + // 延时一下,检查第一页数据是否已铺满第一页,如果未铺满,则直接请求下一页 + handler.removeCallbacksAndMessages(null) + handler.postDelayed({ + checkIfLoadNextPage(binding.rvRecommends, viewModel::loadMore) + }, 16) + } + adapter.setPageState(it) + } + + pageLoader.dataList.observe(viewLifecycleOwner) { + adapter.submitList(it) + } + + imageArticleDetailDestination.observe(viewLifecycleOwner, EventObserver { + ARouter.getInstance().build(RouteConsts.activity.imageArticleDetailActivity) + .withString(EntranceConsts.KEY_IMAGE_ARTICLE_ID, it) + .navigation() + }) + + initLoad() + } + } + + @Subscribe(threadMode = ThreadMode.MAIN) + fun onEventMainThread(changed: EBImageArticleChanged) { + when (changed) { + is EBImageArticleChanged.VoteChanged -> { + viewModel.voteChanged(changed) + } + + is EBImageArticleChanged.DataChanged -> { + viewModel.itemChanged(changed) + } + + is EBImageArticleChanged.DataDeleted -> { + itemDeleted(changed) + } + + is EBImageArticleChanged.DataCreated -> { + itemCreated(changed) + } + + else -> Unit + } + } + + @SuppressLint("NotifyDataSetChanged") + private fun itemCreated(changed: EBImageArticleChanged.DataCreated) { + val newData = viewModel.pageLoader.dataList.value?.toMutableList() ?: mutableListOf() + if (newData.isEmpty()) { + viewModel.initLoad() + } else { + adapter.submitList(null) + // 请注意:这一行代码不能省 + adapter.notifyDataSetChanged() + newData.add(0, changed.imageArticle.toPersonHistoryEntity()) + viewModel.pageLoader.updateData(newData) + } + + } + + @SuppressLint("NotifyDataSetChanged") + private fun itemDeleted(changed: EBImageArticleChanged.DataDeleted) { + val oldData = viewModel.pageLoader.dataList.value ?: return + val newData = oldData.toMutableList() + val hasRemoved = newData.removeAll { + it.id == changed.imageArticleId + } + if (hasRemoved) { + adapter.submitList(null) + adapter.notifyDataSetChanged() + if (newData.isEmpty()) { + viewModel.initLoad() + } else { + viewModel.pageLoader.updateData(newData) + } + + } + } + + override fun onDestroyView() { + super.onDestroyView() + handler.removeCallbacksAndMessages(null) + } + + companion object { + + fun newInstance() = MyImageArticleListFragment() + } + + private class MyItemDecoration : RecyclerView.ItemDecoration() { + + override fun getItemOffsets(outRect: Rect, view: View, parent: RecyclerView, state: RecyclerView.State) { + val layoutParams = view.layoutParams + if (layoutParams is StaggeredGridLayoutManager.LayoutParams) { + if (parent.getChildAdapterPosition(view) == state.itemCount - 1) { + layoutParams.isFullSpan = true + } else { + val spanIndex = layoutParams.spanIndex + val (left, right) = if (spanIndex % 2 == 0) { + 8F.dip2px() to 2F.dip2px() + } else { + 2F.dip2px() to 8F.dip2px() + } + outRect.left = left + outRect.right = right + } + } + } + } +} \ No newline at end of file diff --git a/app/src/main/java/com/gh/gamecenter/forum/home/recommend/fragment/PublishImageArticleFragment.kt b/app/src/main/java/com/gh/gamecenter/forum/home/recommend/fragment/PublishImageArticleFragment.kt new file mode 100644 index 0000000000..c4598d746d --- /dev/null +++ b/app/src/main/java/com/gh/gamecenter/forum/home/recommend/fragment/PublishImageArticleFragment.kt @@ -0,0 +1,519 @@ +package com.gh.gamecenter.forum.home.recommend.fragment + +import android.content.Context +import android.content.Intent +import android.graphics.Rect +import android.os.Bundle +import android.os.Handler +import android.os.Looper +import android.text.Editable +import android.view.LayoutInflater +import android.view.View +import android.view.ViewGroup +import android.view.ViewTreeObserver.OnGlobalLayoutListener +import android.widget.EditText +import androidx.activity.result.ActivityResultLauncher +import androidx.activity.result.contract.ActivityResultContract +import androidx.appcompat.app.AppCompatActivity +import androidx.core.view.updateLayoutParams +import androidx.core.widget.addTextChangedListener +import androidx.fragment.app.activityViewModels +import androidx.fragment.app.viewModels +import androidx.recyclerview.widget.ItemTouchHelper +import androidx.recyclerview.widget.ItemTouchHelper.ACTION_STATE_IDLE +import androidx.recyclerview.widget.LinearLayoutManager +import androidx.recyclerview.widget.RecyclerView +import com.gh.gamecenter.R +import com.gh.gamecenter.common.base.fragment.BaseFragment +import com.gh.gamecenter.common.constant.EntranceConsts +import com.gh.gamecenter.common.entity.CommunityEntity +import com.gh.gamecenter.common.utils.* +import com.gh.gamecenter.core.AppExecutor +import com.gh.gamecenter.core.utils.ToastUtils +import com.gh.gamecenter.databinding.FragmentPublishImageArticleBinding +import com.gh.gamecenter.entity.ForumDetailEntity +import com.gh.gamecenter.entity.ImageArticleEntity +import com.gh.gamecenter.forum.home.recommend.adapter.ImageAndTextAdapter +import com.gh.gamecenter.forum.home.recommend.viewmodel.PublishImageArticleActivityViewModel +import com.gh.gamecenter.forum.home.recommend.viewmodel.PublishImageArticleViewModel +import com.gh.gamecenter.livedata.EventObserver +import com.gh.gamecenter.qa.dialog.ChooseForumActivity +import com.gh.gamecenter.qa.dialog.ChooseSectionDialogFragment +import com.lightgame.listeners.OnBackPressedListener +import com.lightgame.utils.Util_System_Keyboard + +class PublishImageArticleFragment : BaseFragment(), OnBackPressedListener { + + private val activityViewModel by activityViewModels() + private val viewModel by viewModels() + + private lateinit var binding: FragmentPublishImageArticleBinding + + private val rootView: View? + get() = activity?.findViewById(android.R.id.content) + + private val itemTouchHelper by lazy { + ItemTouchHelper(MyItemTouchCallback(activityViewModel)) + } + + private val adapter by lazy { + ImageAndTextAdapter(viewModel) { + itemTouchHelper.startDrag(it) + } + } + + private val savingDraftLoading by lazy { + DialogHelper.getProgressDialog(requireContext(), R.string.draft_saving.toResString()) + } + + private val publishingLoading by lazy { + DialogHelper.getProgressDialog(requireContext(), R.string.image_article_publishing.toResString()) + } + + override fun getInflatedLayout(): View { + return View(requireContext()) + } + + private val onGlobalLayoutListener = OnGlobalLayoutListener { + onKeyboardChanged() + } + + private val handler = Handler(Looper.getMainLooper()) + + private var isKeyBoardShow = false + + private lateinit var chooseForumLauncher: ActivityResultLauncher + + override fun getLayoutId(): Int = 0 + + override fun onCreate(savedInstanceState: Bundle?) { + super.onCreate(savedInstanceState) + + chooseForumLauncher = registerForActivityResult(object : + ActivityResultContract() { + + override fun createIntent(context: Context, input: String): Intent { + val intent = Intent(context, ChooseForumActivity::class.java) + intent.putExtra(EntranceConsts.KEY_SOURCE_ENTRANCE, input) + return intent + } + + override fun parseResult(resultCode: Int, intent: Intent?): CommunityEntity? { + return intent?.getParcelableExtra(EntranceConsts.KEY_COMMUNITY_DATA) + } + } + ) { result -> result?.let(viewModel::changeLinkBbs) } + + activityViewModel.editImageArticle.observe(this, EventObserver { + viewModel.setEditImageArticle(it) + }) + + activityViewModel.community.observe(this, EventObserver { + viewModel.changeLinkBbs(it) + }) + } + + override fun initView(view: View?) = Unit + + override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View? { + return FragmentPublishImageArticleBinding.inflate(inflater, container, false) + .also { + binding = it + }.root + } + + override fun onViewCreated(view: View, savedInstanceState: Bundle?) { + super.onViewCreated(view, savedInstanceState) + + view.post { + setContentHeight(0) + } + + + binding.rvImages.layoutManager = LinearLayoutManager(context, RecyclerView.HORIZONTAL, false) + binding.rvImages.addItemDecoration(MyItemDecoration()) + binding.rvImages.adapter = adapter + itemTouchHelper.attachToRecyclerView(binding.rvImages) + + binding.etTitle.addTextChangedListener { text -> + if (text != null) { + checkMaxLimitReached(binding.etTitle, text, TITLE_MAX_LENGTH, R.string.image_and_text_title_limit) + var remainingSize = TITLE_MAX_LENGTH - text.length + if (remainingSize < 0) { + remainingSize = 0 + } + binding.tvRemainingWords.text = "$remainingSize" + } + binding.ivClear.visibleIf(!text.isNullOrBlank() && binding.etTitle.hasFocus()) + } + + binding.etTitle.setOnFocusChangeListener { _, hasFocus -> + binding.ivClear.visibleIf(hasFocus && binding.etTitle.text.isNotBlank()) + binding.tvRemainingWords.visibleIf(hasFocus) + } + + binding.tvSelectBbs.setOnClickListener { + showSelectBbsDialog() + } + + binding.ivClear.setOnClickListener { + binding.etTitle.text = null + } + + binding.vCloseDragTips.setOnClickListener { + handler.removeCallbacksAndMessages(null) + binding.gDragTips.goneIf(true) + } + + binding.etContent.addTextChangedListener { text -> + if (text != null) { + checkMaxLimitReached(binding.etContent, text, CONTENT_MAX_LENGTH, R.string.image_and_text_content_limit) + } + } + + binding.tvBbsName.setOnClickListener { + showSelectBbsDialog() + } + + binding.tvSubSection.setOnClickListener { + showSelectSubSectionDialog() + } + + binding.vClearBbs.setOnClickListener { + if (viewModel.isCreating) { + viewModel.changeLinkBbs(null) + } else { + viewModel.changeLinkSection(null) + toast(R.string.cannot_be_modified_after_publishing) + } + + } + + binding.tvDraftBox.setOnClickListener { + activityViewModel.navigateToDraftBox() + } + + binding.tvPublish.setOnClickListener { + publish() + } + + activityViewModel.localImages.observe(viewLifecycleOwner) { + adapter.submitList(it) + } + + activityViewModel.showDragTipsAction.observe(viewLifecycleOwner, EventObserver { + // 倒计时开始,5s 后消失 + binding.gDragTips.visibleIf(true) + handler.removeCallbacksAndMessages(null) + handler.postDelayed({ + binding.gDragTips.goneIf(true) + }, DRAG_TIPS_SHOW_DURATION) + + }) + + with(viewModel) { + + chooseImageAction.observe(viewLifecycleOwner, EventObserver { + activityViewModel.chooseImage() + }) + + removeImageAction.observe(viewLifecycleOwner, EventObserver { + activityViewModel.removeImage(it) + }) + + linkBbs.observe(viewLifecycleOwner) { + updateLinkBbs(it) + } + + draftCreated.observe(viewLifecycleOwner, EventObserver { + if (savingDraftLoading.isShowing) { + savingDraftLoading.dismiss() + } + val toastText = if (it) { + R.string.save_draft_successfully.toResString() + } else { + R.string.save_draft_failure.toResString() + } + ToastUtils.showToast(toastText) + if (it) { + activity?.finish() + } + }) + + imageTextPublished.observe(viewLifecycleOwner, EventObserver { + if (publishingLoading.isShowing) { + publishingLoading.dismiss() + } + val toastText = if (it) { + // 发布成功 + R.string.publish_image_article_successfully.toResString() + } else { + // 发布失败 + R.string.publish_image_article_failure.toResString() + } + ToastUtils.showToast(toastText) + if (it) { + activity?.finish() + } + }) + + editImageArticle.observe(viewLifecycleOwner, EventObserver { + fillUiByDraft(it) + }) + } + } + + private fun fillUiByDraft(imageArticle: ImageArticleEntity) { + binding.etTitle.setText(imageArticle.title) + binding.etContent.setText(imageArticle.content) + } + + /** + * 设置内容输入框的高度 + */ + private fun setContentHeight(keyboardHeight: Int) { + val rootHeight = binding.root.height + val remainHeight = + if (keyboardHeight > 0) { + rootHeight - 116F.dip2px() - 50F.dip2px() - 50F.dip2px() - keyboardHeight + } else { + rootHeight - 116F.dip2px() - 50F.dip2px() - 60F.dip2px() - 50F.dip2px() + } + + val contentHeight = if (remainHeight > 360F.dip2px()) { + 360F.dip2px() + } else { + remainHeight + } + binding.etContent.updateLayoutParams { + height = contentHeight + } + } + + private fun createDraft() { + if (!savingDraftLoading.isShowing) { + savingDraftLoading.show() + } + val title = binding.etTitle.text?.toString() ?: "" + val content = binding.etContent.text?.toString() ?: "" + val images = activityViewModel.localImages.value ?: emptyList() + viewModel.createOrEditDraft(title, content, images) + } + + private fun publish() { + if (!publishingLoading.isShowing) { + publishingLoading.show() + } + val title = binding.etTitle.text?.toString() ?: "" + val content = binding.etContent.text?.toString() ?: "" + val images = activityViewModel.localImages.value ?: emptyList() + viewModel.publishImageText(title, content, images) + } + + private fun updateLinkBbs(linkBbs: PublishImageArticleViewModel.LinkBbs) { + if (linkBbs.communityEntity == null) { + // 还没有关联论坛 + binding.tvSelectBbs.visibleIf(true) + binding.gBbsSelected.visibleIf(false) + } else { + // 已关联论坛 + binding.tvSelectBbs.visibleIf(false) + binding.gBbsSelected.visibleIf(true) + binding.ivBbsIcon.displayGameIcon(linkBbs.communityEntity.icon, null, null) + binding.tvBbsName.text = linkBbs.communityEntity.name + if (linkBbs.selectedSection == null) { + // 还没有选择子模块 + + if (linkBbs.hasSection) { + // 有子模块 + binding.tvSubSection.visibleIf(true) + binding.ivBbsArrRight.visibleIf(true) + binding.tvSubSection.text = "" + } else { + // 没有子模块 + binding.tvSubSection.visibleIf(false) + binding.ivBbsArrRight.visibleIf(false) + } + } else { + binding.tvSubSection.text = linkBbs.selectedSection.name + } + } + } + + private fun checkMaxLimitReached(editText: EditText, text: Editable, maxLength: Int, toastResId: Int) { + if (text.length > maxLength) { + editText.setText(text.subSequence(0, TITLE_MAX_LENGTH)) + editText.setSelection(TITLE_MAX_LENGTH) // 将光标移动到文本末尾 + ToastUtils.showToast(toastResId.toResString()) + } + } + + private fun onKeyboardChanged() { + val rect = Rect() + rootView?.let { + it.getWindowVisibleDisplayFrame(rect) + val screenHeight = it.height + val keyboardHeight = screenHeight - rect.bottom + if (keyboardHeight > screenHeight * 0.15) { + // 键盘弹起 + if (!isKeyBoardShow) { + isKeyBoardShow = true + + // 当软键盘弹起时,必须要保证EditText的光标和内容不被挡住 + setContentHeight(keyboardHeight) + } + + } else { + // 键盘隐藏 + if (isKeyBoardShow) { + isKeyBoardShow = false + setContentHeight(0) + } + + } + } + + } + + private fun showSelectBbsDialog() { + if (viewModel.isCreating) { + val delayTime = if (isKeyBoardShow) { + Util_System_Keyboard.hideSoftKeyboard(requireActivity()) + 200L + } else 0L + AppExecutor.uiExecutor.executeWithDelay({ + chooseForumLauncher.launch("社区") + }, delayTime) + } else { + toast(R.string.cannot_be_modified_after_publishing) + } + } + + private fun showSelectSubSectionDialog() { + val delayTime = if (isKeyBoardShow) { + Util_System_Keyboard.hideSoftKeyboard(requireActivity()) + 200L + } else 0L + AppExecutor.uiExecutor.executeWithDelay({ + ChooseSectionDialogFragment.show( + requireActivity() as AppCompatActivity, + viewModel.linkBbs.value?.communityEntity?.id ?: "", + viewModel.linkBbs.value?.selectedSection?.id ?: "", + viewModel.isModerator, + tag ?: "" + ) + }, delayTime) + } + + + override fun onResume() { + super.onResume() + requireActivity().findViewById(android.R.id.content)?.viewTreeObserver?.addOnGlobalLayoutListener( + onGlobalLayoutListener + ) + } + + override fun onPause() { + super.onPause() + requireActivity().findViewById(android.R.id.content)?.viewTreeObserver?.removeOnGlobalLayoutListener( + onGlobalLayoutListener + ) + } + + override fun onDestroyView() { + super.onDestroyView() + handler.removeCallbacksAndMessages(null) + } + + override fun onHandleBackPressed(): Boolean { + showSaveDraftDialog() + return true + } + + private fun showSaveDraftDialog() { + DialogHelper.showDialog( + requireContext(), + R.string.miui_optimization_warning_dialog_title.toResString(), + R.string.save_to_draft_box_and_exit.toResString(), + confirmText = R.string.save_and_exit.toResString(), + cancelText = R.string.do_not_save.toResString(), + confirmClickCallback = { + createDraft() + }, + cancelClickCallback = { + activity?.finish() + }, + extraConfig = DialogHelper.Config(centerTitle = true, centerContent = true) + + ) + } + + override fun onActivityResult(requestCode: Int, resultCode: Int, data: Intent?) { + super.onActivityResult(requestCode, resultCode, data) + if (requestCode == ChooseSectionDialogFragment.REQUEST_CODE) { + val section = data?.getParcelableExtra(EntranceConsts.KEY_DATA) + if (section != null) { + viewModel.changeLinkSection(section) + } + } + } + + companion object { + + private const val TITLE_MAX_LENGTH = 20 + private const val CONTENT_MAX_LENGTH = 1000 + private const val DRAG_TIPS_SHOW_DURATION = 5000L + + fun newInstance() = PublishImageArticleFragment() + } + + private class MyItemTouchCallback( + private val viewModel: PublishImageArticleActivityViewModel + ) : ItemTouchHelper.Callback() { + override fun getMovementFlags(recyclerView: RecyclerView, viewHolder: RecyclerView.ViewHolder): Int { + return makeMovementFlags(ItemTouchHelper.LEFT or ItemTouchHelper.RIGHT, 1) + } + + override fun onMove( + recyclerView: RecyclerView, + viewHolder: RecyclerView.ViewHolder, + target: RecyclerView.ViewHolder + ): Boolean { + val adapter = recyclerView.adapter as? ImageAndTextAdapter + if (adapter != null) { + val position = viewHolder.bindingAdapterPosition + val targetPosition = target.bindingAdapterPosition + val targetItem = adapter.getItem(targetPosition) + if (targetItem.id != ImageAndTextAdapter.INVALID_ID) { + viewModel.swap(position, targetPosition) + return true + } + + } + return false + } + + override fun isLongPressDragEnabled(): Boolean = true + + override fun onSwiped(viewHolder: RecyclerView.ViewHolder, direction: Int) = Unit + + override fun onSelectedChanged(viewHolder: RecyclerView.ViewHolder?, actionState: Int) { + super.onSelectedChanged(viewHolder, actionState) + if (actionState == ACTION_STATE_IDLE) { + // 拖拽完毕,刷新数据:将第一位的图片设为封面 + viewModel.refreshImages() + } + } + } + + class MyItemDecoration : RecyclerView.ItemDecoration() { + + override fun getItemOffsets(outRect: Rect, view: View, parent: RecyclerView, state: RecyclerView.State) { + outRect.left = if (parent.getChildAdapterPosition(view) == 0) { + 0 + } else { + 4F.dip2px() + } + } + } +} \ No newline at end of file diff --git a/app/src/main/java/com/gh/gamecenter/forum/home/recommend/viewmodel/ImageArticleCommentViewModel.kt b/app/src/main/java/com/gh/gamecenter/forum/home/recommend/viewmodel/ImageArticleCommentViewModel.kt new file mode 100644 index 0000000000..fdf2fea5d6 --- /dev/null +++ b/app/src/main/java/com/gh/gamecenter/forum/home/recommend/viewmodel/ImageArticleCommentViewModel.kt @@ -0,0 +1,116 @@ +package com.gh.gamecenter.forum.home.recommend.viewmodel + +import android.app.Application +import androidx.lifecycle.ViewModel +import androidx.lifecycle.ViewModelProvider +import com.gh.gamecenter.common.baselist.LoadStatus +import com.gh.gamecenter.common.baselist.LoadType +import com.gh.gamecenter.common.baselist.PageLoader +import com.gh.gamecenter.common.utils.toArrayList +import com.gh.gamecenter.common.utils.toJson +import com.gh.gamecenter.core.utils.GsonUtils +import com.gh.gamecenter.feature.entity.CommentEntity +import com.gh.gamecenter.feature.entity.MeEntity +import com.gh.gamecenter.forum.home.recommend.ImageArticleRepository +import com.gh.gamecenter.qa.article.detail.CommentItemData +import com.gh.gamecenter.qa.comment.base.BaseCommentViewModel +import com.gh.gamecenter.retrofit.RetrofitManager +import com.google.gson.reflect.TypeToken +import io.reactivex.Observable +import io.reactivex.disposables.CompositeDisposable + +class ImageArticleCommentViewModel( + application: Application, + imageArticleId: String +) : + BaseCommentViewModel(application, "", "", "", "", "", "", imageArticleId) { + + private val repository = ImageArticleRepository.newInstance() + + override fun provideDataObservable(page: Int): Observable> { + val map = hashMapOf() + if (!isHandleTopComment && topCommentId.isNotEmpty()) { + map["top_comment_id"] = topCommentId + } + return RetrofitManager.getInstance().api.getImageArticleComments( + imageArticleId, + currentSortType.value, + page, + map + ).map { + mTotal = it.headers().get("total")?.toInt() ?: 0 + val type = object : TypeToken>() {}.type + GsonUtils.gson.fromJson(it.body()?.toJson() ?: "", type) + } + } + + override fun mergeResultLiveData() { + mResultLiveData.addSource(mListLiveData) { + mergeListData(mListLiveData.value) + } + } + + override fun mergeListData(commentList: List?, hasFilter: Boolean) { + val mergedList = arrayListOf().apply { + if (commentList.isNullOrEmpty() && mLoadStatusLiveData.value == LoadStatus.INIT_EMPTY) { + add(CommentItemData(errorEmpty = true)) + } else if (commentList.isNullOrEmpty() && mLoadStatusLiveData.value == LoadStatus.INIT_FAILED) { + add(CommentItemData(errorConnection = true)) + } else { + commentList?.forEachIndexed { index, entity -> + // 没有 me 会导致不能跨页面更新点赞 + if (entity.me == null) { + entity.me = MeEntity() + } + handleTopComment(index, entity) + add(CommentItemData(commentNormal = entity)) + } + add(CommentItemData(footer = true)) + } + } + mResultLiveData.postValue(mergedList) + } + + fun insertNewestComment(newComment: CommentEntity) { + val data = (mResultLiveData.value ?: emptyList()).toArrayList() + val count = data.count { it.commentNormal != null } + if (count == 0) { + load(LoadType.REFRESH) + return + } + when (currentSortType) { + SortType.LATEST -> { + val index = data.indexOfFirst { it.commentNormal != null } + if (index >= 0) { + mTotal++ + data.add(index, CommentItemData(commentNormal = newComment)) + mResultLiveData.postValue(data) + } else { + load(LoadType.REFRESH) + } + } + + SortType.OLDEST -> { + //根据total判断是否插入到最后 + if (count >= mTotal) { + mTotal++ + val index = data.indexOfLast { it.commentNormal != null } + data.add(index + 1, CommentItemData(commentNormal = newComment)) + mResultLiveData.postValue(data) + } else { + load(LoadType.REFRESH) + } + } + } + } + + class Factory( + private val application: Application, + private val imageArticleId: String + ) : ViewModelProvider.NewInstanceFactory() { + + override fun create(modelClass: Class): T { + return ImageArticleCommentViewModel(application, imageArticleId) as T + } + } +} \ No newline at end of file diff --git a/app/src/main/java/com/gh/gamecenter/forum/home/recommend/viewmodel/ImageArticleDetailViewModel.kt b/app/src/main/java/com/gh/gamecenter/forum/home/recommend/viewmodel/ImageArticleDetailViewModel.kt new file mode 100644 index 0000000000..de6a88de0d --- /dev/null +++ b/app/src/main/java/com/gh/gamecenter/forum/home/recommend/viewmodel/ImageArticleDetailViewModel.kt @@ -0,0 +1,293 @@ +package com.gh.gamecenter.forum.home.recommend.viewmodel + +import androidx.lifecycle.LiveData +import androidx.lifecycle.MutableLiveData +import androidx.lifecycle.Transformations +import androidx.lifecycle.ViewModel +import com.gh.gamecenter.R +import com.gh.gamecenter.common.baselist.LoadStatus +import com.gh.gamecenter.common.retrofit.BiResponse +import com.gh.gamecenter.common.retrofit.Response +import com.gh.gamecenter.common.utils.observableToMain +import com.gh.gamecenter.common.utils.singleToMain +import com.gh.gamecenter.entity.ImageArticleEntity +import com.gh.gamecenter.eventbus.EBImageArticleChanged +import com.gh.gamecenter.feature.entity.CommentEntity +import com.gh.gamecenter.feature.entity.Permissions +import com.gh.gamecenter.forum.home.recommend.ImageArticleRepository +import com.gh.gamecenter.forum.home.recommend.ImageArticleUseCase +import com.gh.gamecenter.livedata.Event +import com.gh.gamecenter.qa.comment.base.BaseCommentViewModel.SortType +import io.reactivex.CompletableObserver +import io.reactivex.android.schedulers.AndroidSchedulers +import io.reactivex.disposables.CompositeDisposable +import io.reactivex.disposables.Disposable +import io.reactivex.schedulers.Schedulers +import okhttp3.ResponseBody + +class ImageArticleDetailViewModel : ViewModel() { + + private val repository = ImageArticleRepository.newInstance() + + private val compositeDisposable = CompositeDisposable() + + var imageArticleId: String = "" + + private val useCase = ImageArticleUseCase(repository, compositeDisposable) + + private val _imageArticleDetailEntity = MutableLiveData() + val imageArticleDetailEntity: LiveData = _imageArticleDetailEntity + + val isUserFollowed: LiveData = + Transformations.distinctUntilChanged(Transformations.map(imageArticleDetailEntity) { + it.me.isFollower + }) + + fun followUser() { + val isFollowed = isUserFollowed.value ?: false + val userId = imageArticleDetailEntity.value?.user?.id ?: "" + repository.followUser(!isFollowed, userId) + .compose(observableToMain()) + .subscribe(object : Response() { + override fun onSubscribe(d: Disposable) { + compositeDisposable.add(d) + } + + override fun onResponse(response: ResponseBody?) { + updateImageArticle { + it.me.isFollower = !isFollowed + } + } + }) + } + + val commentCount: LiveData = + Transformations.distinctUntilChanged(Transformations.map(imageArticleDetailEntity) { + it.count.comment + }) + + fun updateComment(commentId: String, commentCount: Int) { + updateImageArticle { + it.count.comment = commentCount + } + ImageArticleUseCase.notifyCommentChanged(imageArticleId, commentCount) + if (commentId.isNotEmpty()) { + insertNewestComment(commentId) + } + } + + private val _insertNewestCommentAction = MutableLiveData>() + val insertNewestCommentAction: LiveData> = _insertNewestCommentAction + private fun insertNewestComment(commentId: String) { + // 将刚刚评论的内容插入到评论列表中 + repository.getSingleCommentById(commentId) + .compose(singleToMain()) + .subscribe(object : BiResponse() { + override fun onSuccess(data: CommentEntity) { + _insertNewestCommentAction.value = Event(data) + } + }).let(compositeDisposable::add) + } + + val vote: LiveData> = + Transformations.distinctUntilChanged(Transformations.map(imageArticleDetailEntity) { + it.me.isCommunityArticleVote to it.count.vote + }) + + fun vote() { + val isVoted = vote.value?.first ?: false + useCase.voteImageArticle(imageArticleId, !isVoted) + } + + private val _pageStatus = MutableLiveData() + val pageStatus: LiveData = _pageStatus + + fun loadImageArticleDetail(imageArticleId: String) { + _pageStatus.value = LoadStatus.INIT_LOADING + this.imageArticleId = imageArticleId + repository.loadImageArticleDetail(imageArticleId) + .compose(singleToMain()) + .subscribe(object : BiResponse() { + override fun onSuccess(data: ImageArticleEntity) { + _pageStatus.value = LoadStatus.INIT_LOADED + setImageArticle(data) + } + + override fun onFailure(exception: Exception) { + _pageStatus.value = LoadStatus.INIT_FAILED + } + + }).let(compositeDisposable::add) + } + + private val _sortType = MutableLiveData>() + val sortType: LiveData> = _sortType + fun changeSort(newSort: SortType) { + _sortType.value = Event(newSort) + } + + val isBbsFollowed: LiveData = + Transformations.distinctUntilChanged(Transformations.map(imageArticleDetailEntity) { + it.community?.isFollowed ?: false + }) + + fun followBbs(communityId: String) { + val isFollowed = isBbsFollowed.value ?: false + repository.followBbs(communityId, !isFollowed) + .compose(singleToMain()) + .subscribe(object : BiResponse() { + override fun onSuccess(data: ResponseBody) { + updateImageArticle { + it.community?.isFollowed = !isFollowed + } + } + + }).let(compositeDisposable::add) + } + + val isFavorite: LiveData = + Transformations.distinctUntilChanged(Transformations.map(imageArticleDetailEntity) { + it.me.isFavorite + }) + + fun collect() { + val imageArticleId = imageArticleId + val isCollected = isFavorite.value ?: false + repository.collect(imageArticleId, !isCollected) + .subscribeOn(Schedulers.io()) + .observeOn(AndroidSchedulers.mainThread()) + .subscribe(object : CompletableObserver { + override fun onSubscribe(d: Disposable) { + compositeDisposable.add(d) + } + + override fun onComplete() { + updateImageArticle { + it.me.isFavorite = !isCollected + } + } + + override fun onError(e: Throwable) = Unit + + }) + } + + fun setImageArticle(newData: ImageArticleEntity) { + _imageArticleDetailEntity.value = newData + } + + fun updateImageArticle(block: (ImageArticleEntity) -> Unit) { + val data = imageArticleDetailEntity.value ?: return + block(data) + _imageArticleDetailEntity.value = data + } + + private val _applyHighlightAction = MutableLiveData>() + val applyHighlightAction: LiveData> = _applyHighlightAction + fun applyHighlightForImageArticle() { + repository.applyHighlightForImageArticle(imageArticleId) + .subscribeOn(Schedulers.io()) + .observeOn(AndroidSchedulers.mainThread()) + .subscribe(object : CompletableObserver { + override fun onSubscribe(d: Disposable) { + compositeDisposable.add(d) + } + + override fun onComplete() { + imageArticleDetailEntity.value?.choicenessStatus = "apply" + _applyHighlightAction.value = Event(true) + } + + override fun onError(e: Throwable) { + _applyHighlightAction.value = Event(false) + } + }) + } + + private val _addHighlightAction = MutableLiveData>() + val addHighlightAction: LiveData> = _addHighlightAction + fun addHighlight(isAdd: Boolean) { + repository.addHighlight(isAdd, imageArticleId) + .compose(singleToMain()) + .subscribe(object : BiResponse() { + override fun onSuccess(data: ResponseBody) { + val imageArticle = imageArticleDetailEntity.value ?: return + if (isAdd) { + if (imageArticle.me.moderatorPermissions.choicenessImageArticle == Permissions.REPORTER) { + _addHighlightAction.value = Event(R.string.apply_successfully) + updateImageArticle { + it.choicenessStatus = "apply" + } + } else { + _addHighlightAction.value = Event(R.string.operation_successful) + updateImageArticle { + it.choicenessStatus = "pass" + } + + } + } else { + if (imageArticle.me.moderatorPermissions.cancelChoicenessImageArticle == Permissions.REPORTER) { + _addHighlightAction.value = Event(R.string.apply_successfully) + } else { + _addHighlightAction.value = Event(R.string.operation_successful) + updateImageArticle { + it.choicenessStatus = "cancel" + } + } + } + } + + override fun onFailure(exception: Exception) { + _addHighlightAction.value = Event(R.string.permission_error) + } + }).let(compositeDisposable::add) + } + + private val _deleteOrHideImageArticleSuccessfully = MutableLiveData>>() + val deleteOrHideImageArticleSuccessfully: LiveData>> = + _deleteOrHideImageArticleSuccessfully + + fun deleteOrHideImageArticle() { + repository.deleteOrHideImageArticle(imageArticleId) + .compose(singleToMain()) + .subscribe(object : BiResponse() { + override fun onSuccess(data: ResponseBody) { + val imageArticle = imageArticleDetailEntity.value ?: return + val toastResId = if (imageArticle.me.isModerator) { + if (imageArticle.me.moderatorPermissions.hideImageArticle == Permissions.REPORTER) { + R.string.apply_successfully + } else { + R.string.hidden + } + } else { + R.string.deleted + } + ImageArticleUseCase.notifyDataDeleted(imageArticleId) + _deleteOrHideImageArticleSuccessfully.value = Event(true to toastResId) + + } + + override fun onFailure(exception: Exception) { + super.onFailure(exception) + _deleteOrHideImageArticleSuccessfully.value = Event(false to R.string.permission_error_and_retry) + } + + }).let(compositeDisposable::add) + } + + fun imageArticleChanged(changed: EBImageArticleChanged) { + when { + changed is EBImageArticleChanged.VoteChanged -> { + updateImageArticle { + it.me.isCommunityArticleVote = changed.isVoted + it.count.vote = changed.count + } + } + + changed is EBImageArticleChanged.DataChanged -> { + setImageArticle(changed.data) + } + } + } + +} \ No newline at end of file diff --git a/app/src/main/java/com/gh/gamecenter/forum/home/recommend/viewmodel/ImageArticleDraftViewModel.kt b/app/src/main/java/com/gh/gamecenter/forum/home/recommend/viewmodel/ImageArticleDraftViewModel.kt new file mode 100644 index 0000000000..934d6e1677 --- /dev/null +++ b/app/src/main/java/com/gh/gamecenter/forum/home/recommend/viewmodel/ImageArticleDraftViewModel.kt @@ -0,0 +1,101 @@ +package com.gh.gamecenter.forum.home.recommend.viewmodel + +import androidx.lifecycle.LiveData +import androidx.lifecycle.MutableLiveData +import androidx.lifecycle.ViewModel +import com.gh.gamecenter.common.baselist.PageLoader +import com.gh.gamecenter.common.retrofit.BiResponse +import com.gh.gamecenter.common.utils.singleToMain +import com.gh.gamecenter.entity.ImageArticleEntity +import com.gh.gamecenter.forum.home.recommend.ImageArticleRepository +import com.gh.gamecenter.livedata.Event +import io.reactivex.CompletableObserver +import io.reactivex.android.schedulers.AndroidSchedulers +import io.reactivex.disposables.CompositeDisposable +import io.reactivex.disposables.Disposable +import io.reactivex.schedulers.Schedulers + +class ImageArticleDraftViewModel : ViewModel() { + + private val repository = ImageArticleRepository.newInstance() + + private val composeDisposable = CompositeDisposable() + + private val _pageState = MutableLiveData() + val pageState: LiveData = _pageState + + private val _drafts = MutableLiveData>() + val drafts: LiveData> = _drafts + + fun loadDraft(isPullToRefresh: Boolean) { + _pageState.value = if (isPullToRefresh) { + PageLoader.PageState.PagePullToRefreshLoading + } else { + PageLoader.PageState.PageInitLoading + } + + repository.loadDraft() + .compose(singleToMain()) + .subscribe(object : BiResponse>() { + override fun onSuccess(data: List) { + _pageState.value = if (data.isEmpty()) { + PageLoader.PageState.PageNoData + } else { + PageLoader.PageState.PageLoadCompleted + } + _drafts.value = data + } + + override fun onFailure(exception: Exception) { + super.onFailure(exception) + _pageState.value = if (isPullToRefresh) { + PageLoader.PageState.PagePullToRefreshFailure + } else { + PageLoader.PageState.PageInitFailure + } + + } + + }).let(composeDisposable::add) + } + + private val _showDeleteDraftDialogAction = MutableLiveData>() + val showDeleteDraftDialogAction: LiveData> = _showDeleteDraftDialogAction + fun showDeleteDraftDialog(draftId: String) { + _showDeleteDraftDialogAction.value = Event(draftId) + } + + fun deleteDraft(draftId: String) { + repository.deleteDraft(draftId) + .subscribeOn(Schedulers.io()) + .observeOn(AndroidSchedulers.mainThread()) + .subscribe(object : CompletableObserver { + override fun onSubscribe(d: Disposable) { + composeDisposable.add(d) + } + + override fun onComplete() { + val oldData = drafts.value ?: emptyList() + val newList = oldData.toMutableList() + newList.removeAll { it.id == draftId } + if (newList.isEmpty()) { + _pageState.value = PageLoader.PageState.PageNoData + } else { + _drafts.value = newList + } + } + + override fun onError(e: Throwable) = Unit + }) + } + + private val _publishImageArticleDestination = MutableLiveData>() + val publishImageArticleDestination: LiveData> = _publishImageArticleDestination + fun editDraft(draftEntity: ImageArticleEntity) { + _publishImageArticleDestination.value = Event(draftEntity) + } + + fun updateDrafts(draftList: MutableList) { + _drafts.value = draftList + } +} \ No newline at end of file diff --git a/app/src/main/java/com/gh/gamecenter/forum/home/recommend/viewmodel/ImageArticleHomeViewModel.kt b/app/src/main/java/com/gh/gamecenter/forum/home/recommend/viewmodel/ImageArticleHomeViewModel.kt new file mode 100644 index 0000000000..7fff7c3511 --- /dev/null +++ b/app/src/main/java/com/gh/gamecenter/forum/home/recommend/viewmodel/ImageArticleHomeViewModel.kt @@ -0,0 +1,78 @@ +package com.gh.gamecenter.forum.home.recommend.viewmodel + +import androidx.lifecycle.LiveData +import androidx.lifecycle.MutableLiveData +import androidx.lifecycle.ViewModel +import com.gh.gamecenter.common.baselist.PageLoader +import com.gh.gamecenter.entity.ImageArticleEntity +import com.gh.gamecenter.eventbus.EBImageArticleChanged +import com.gh.gamecenter.forum.home.recommend.ImageArticleRepository +import com.gh.gamecenter.forum.home.recommend.ImageArticleUseCase +import com.gh.gamecenter.livedata.Event +import io.reactivex.disposables.CompositeDisposable + +class ImageArticleHomeViewModel : ViewModel() { + + private val repository = ImageArticleRepository.newInstance() + + private val compositeDisposable = CompositeDisposable() + + val useCase = ImageArticleUseCase(repository, compositeDisposable) + + val pageLoader = PageLoader(compositeDisposable = compositeDisposable) { pageNo, _ -> + repository.loadRecommends(pageNo) + } + + fun initLoad() { + pageLoader.loadInit() + } + + private val _imageDetailDestination = MutableLiveData>() + val imageDetailDestination: LiveData> = _imageDetailDestination + fun navigateToImageArticleDetailPage(imageArticleId: String) { + _imageDetailDestination.value = Event(imageArticleId) + } + + fun loadMore() { + pageLoader.loadMore() + } + + override fun onCleared() { + compositeDisposable.clear() + } + + fun voteChanged(changed: EBImageArticleChanged.VoteChanged) { + val oldData = pageLoader.dataList.value ?: return + val newData = oldData.toMutableList() + val position = newData.indexOfFirst { it.id == changed.id } + if (position != -1) { + val oldItem = newData[position] + val oldCount = oldItem.count.vote + val newCount = if (changed.isVoted) { + oldCount + 1 + } else { + oldCount - 1 + } + val newItem = oldItem.copy( + _me = oldItem.me.copy(isCommunityArticleVote = changed.isVoted), + _count = oldItem.count.copy(vote = newCount) + ) + newData[position] = newItem + pageLoader.updateData(newData) + } + } + + fun itemChanged(changed: EBImageArticleChanged.DataChanged) { + val oldData = pageLoader.dataList.value ?: return + val newData = oldData.toMutableList() + val position = newData.indexOfFirst { it.id == changed.data.id } + if (position != -1) { + newData[position] = changed.data + pageLoader.updateData(newData) + } + + } + + + +} \ No newline at end of file diff --git a/app/src/main/java/com/gh/gamecenter/forum/home/recommend/viewmodel/MyImageArticleListViewModel.kt b/app/src/main/java/com/gh/gamecenter/forum/home/recommend/viewmodel/MyImageArticleListViewModel.kt new file mode 100644 index 0000000000..edc77b0807 --- /dev/null +++ b/app/src/main/java/com/gh/gamecenter/forum/home/recommend/viewmodel/MyImageArticleListViewModel.kt @@ -0,0 +1,81 @@ +package com.gh.gamecenter.forum.home.recommend.viewmodel + +import androidx.lifecycle.LiveData +import androidx.lifecycle.MutableLiveData +import androidx.lifecycle.ViewModel +import com.gh.gamecenter.common.baselist.PageLoader +import com.gh.gamecenter.entity.PersonalHistoryEntity.Count +import com.gh.gamecenter.eventbus.EBImageArticleChanged +import com.gh.gamecenter.forum.home.recommend.ImageArticleRepository +import com.gh.gamecenter.forum.home.recommend.ImageArticleUseCase +import com.gh.gamecenter.livedata.Event +import io.reactivex.disposables.CompositeDisposable + +class MyImageArticleListViewModel : ViewModel() { + + private val repository = ImageArticleRepository.newInstance() + + private val compositeDisposable = CompositeDisposable() + + val useCase = ImageArticleUseCase(repository, compositeDisposable) + + val pageLoader = PageLoader(compositeDisposable = compositeDisposable) { pageSize, _ -> + repository.loadMyImageArticleList(pageSize) + .singleOrError() + } + + fun initLoad() { + pageLoader.loadInit() + } + + private val _imageArticleDetailDestination = MutableLiveData>() + val imageArticleDetailDestination: LiveData> = _imageArticleDetailDestination + fun navigateToImageArticleDetailPage(imageArticleId: String) { + _imageArticleDetailDestination.value = Event(imageArticleId) + } + + fun loadMore() { + pageLoader.loadMore() + } + + override fun onCleared() { + compositeDisposable.clear() + } + + fun voteChanged(changed: EBImageArticleChanged.VoteChanged) { + val oldData = pageLoader.dataList.value ?: return + val newData = oldData.toMutableList() + val position = newData.indexOfFirst { it.id == changed.id } + if (position != -1) { + val oldItem = newData[position] + val oldCount = oldItem.count.vote + val newCount = if (changed.isVoted) { + oldCount + 1 + } else { + oldCount - 1 + } + val newItem = oldItem.copy( + _me = oldItem.me.copy(isCommunityArticleVote = changed.isVoted), + _count = Count( + comment = oldItem.count.comment, + vote = newCount, + reply = oldItem.count.reply, + ) + ) + + newData[position] = newItem + pageLoader.updateData(newData) + } + } + + fun itemChanged(changed: EBImageArticleChanged.DataChanged) { + val oldData = pageLoader.dataList.value ?: return + val newData = oldData.toMutableList() + val position = newData.indexOfFirst { it.id == changed.data.id } + if (position != -1) { + newData[position] = changed.data.toPersonHistoryEntity() + pageLoader.updateData(newData) + } + + } +} \ No newline at end of file diff --git a/app/src/main/java/com/gh/gamecenter/forum/home/recommend/viewmodel/PublishImageArticleActivityViewModel.kt b/app/src/main/java/com/gh/gamecenter/forum/home/recommend/viewmodel/PublishImageArticleActivityViewModel.kt new file mode 100644 index 0000000000..37c9f5821e --- /dev/null +++ b/app/src/main/java/com/gh/gamecenter/forum/home/recommend/viewmodel/PublishImageArticleActivityViewModel.kt @@ -0,0 +1,129 @@ +package com.gh.gamecenter.forum.home.recommend.viewmodel + +import android.net.Uri +import androidx.lifecycle.LiveData +import androidx.lifecycle.MutableLiveData +import androidx.lifecycle.ViewModel +import com.gh.gamecenter.common.entity.CommunityEntity +import com.gh.gamecenter.common.retrofit.BiResponse +import com.gh.gamecenter.common.utils.singleToMain +import com.gh.gamecenter.entity.ImageArticleEntity +import com.gh.gamecenter.forum.home.recommend.ImageArticleRepository +import com.gh.gamecenter.forum.home.recommend.adapter.ImageAndTextAdapter +import com.gh.gamecenter.livedata.Event +import io.reactivex.disposables.CompositeDisposable +import java.util.* + +class PublishImageArticleActivityViewModel : ViewModel() { + + private val repository = ImageArticleRepository.newInstance() + + private val compositeDisposable = CompositeDisposable() + + private var imageId: Int = 0 + + private val _showDragTipsAction = MutableLiveData>() + val showDragTipsAction: LiveData> = _showDragTipsAction + + private val _localImages = MutableLiveData>() + val localImages: LiveData> = _localImages + + fun addImages(uris: List) { + val oldData = localImages.value?.toMutableList() ?: mutableListOf() + + val newData = (oldData + uris.map { + ImageAndTextAdapter.LocalImage(id = imageId++, isCover = false, showClose = true, uri = it) + }).toMutableList() + + resetFirstItem(newData) + + repository.isShowDragTips() + .compose(singleToMain()) + .subscribe(object : BiResponse() { + override fun onSuccess(data: Boolean) { + if (data && newData.size > 1) { + repository.saveHasShowDragTips() + _showDragTipsAction.value = Event(Unit) + } + } + + }).let(compositeDisposable::add) + } + + private val _chooseImageAction = MutableLiveData>() + val chooseImageAction: LiveData> = _chooseImageAction + fun chooseImage() { + val currentImageSize = localImages.value?.size ?: 0 + val remainingSize = 9 - currentImageSize + _chooseImageAction.value = Event(remainingSize) + } + + fun removeImage(position: Int) { + val images = localImages.value?.toMutableList() ?: mutableListOf() + images.removeAt(position) + resetFirstItem(images) + } + + private fun resetFirstItem(data: MutableList) { + if (data.isEmpty()) { + _localImages.value = mutableListOf() + } else { + // 给第一个设置为封面 + val firstItem = data[0].copy(isCover = true) + data[0] = firstItem + _localImages.value = data + } + } + + fun swap(dataPosition: Int, targetDataPosition: Int) { + val oldList = localImages.value ?: arrayListOf() + val newList = oldList.toMutableList() + Collections.swap(newList, dataPosition, targetDataPosition) + _localImages.value = newList + + } + + fun refreshImages() { + val oldList = localImages.value ?: arrayListOf() + val newList = oldList.toMutableList() + for (index in newList.indices) { + val item = newList[index] + if (index == 0 && !item.isCover) { + newList[0] = item.copy(isCover = true) + } + + if (index != 0 && item.isCover) { + newList[index] = item.copy(isCover = false) + } + } + _localImages.value = newList + } + + private val _draftBoxDestination = MutableLiveData>() + val draftBoxDestination: LiveData> = _draftBoxDestination + fun navigateToDraftBox() { + _draftBoxDestination.value = Event(Unit) + } + + private val _editImageArticle = MutableLiveData>() + val editImageArticle: LiveData> = _editImageArticle + fun setEditImageArticle(imageArticleEntity: ImageArticleEntity?) { + imageArticleEntity ?: return + addImagesFromEdit(imageArticleEntity.images) + _editImageArticle.value = Event(imageArticleEntity) + } + + private fun addImagesFromEdit(urls: List) { + val newImages = urls.map { + ImageAndTextAdapter.LocalImage(id = imageId++, isCover = false, showClose = true, url = it) + }.toMutableList() + + resetFirstItem(newImages) + } + + private val _community = MutableLiveData>() + val community: LiveData> = _community + fun setCommunity(communityEntity: CommunityEntity) { + _community.value = Event(communityEntity) + } +} \ No newline at end of file diff --git a/app/src/main/java/com/gh/gamecenter/forum/home/recommend/viewmodel/PublishImageArticleViewModel.kt b/app/src/main/java/com/gh/gamecenter/forum/home/recommend/viewmodel/PublishImageArticleViewModel.kt new file mode 100644 index 0000000000..37dd2e786f --- /dev/null +++ b/app/src/main/java/com/gh/gamecenter/forum/home/recommend/viewmodel/PublishImageArticleViewModel.kt @@ -0,0 +1,198 @@ +package com.gh.gamecenter.forum.home.recommend.viewmodel + +import androidx.lifecycle.LiveData +import androidx.lifecycle.MutableLiveData +import androidx.lifecycle.ViewModel +import com.gh.gamecenter.common.entity.CommunityEntity +import com.gh.gamecenter.common.retrofit.BiResponse +import com.gh.gamecenter.common.retrofit.Response +import com.gh.gamecenter.common.utils.observableToMain +import com.gh.gamecenter.common.utils.singleToMain +import com.gh.gamecenter.entity.ForumDetailEntity +import com.gh.gamecenter.entity.ImageArticleEntity +import com.gh.gamecenter.entity.PublishImageTextRequest +import com.gh.gamecenter.forum.home.recommend.ImageArticleRepository +import com.gh.gamecenter.forum.home.recommend.ImageArticleUseCase +import com.gh.gamecenter.forum.home.recommend.adapter.ImageAndTextAdapter +import com.gh.gamecenter.livedata.Event +import io.reactivex.disposables.CompositeDisposable +import io.reactivex.disposables.Disposable +import okhttp3.ResponseBody + +class PublishImageArticleViewModel : ViewModel() { + + private val repository = ImageArticleRepository.newInstance() + + private val compositeDisposable = CompositeDisposable() + + var isModerator: Boolean = false + + private var imageArticle: ImageArticleEntity? = null + + val isCreating: Boolean + get() = imageArticle == null || imageArticle!!.isDraft + + private val _chooseImageAction = MutableLiveData>() + val chooseImageAction: LiveData> = _chooseImageAction + fun chooseImage() { + _chooseImageAction.value = Event(Unit) + } + + private val _removeImageAction = MutableLiveData>() + val removeImageAction: LiveData> = _removeImageAction + fun removeImage(position: Int) { + _removeImageAction.value = Event(position) + } + + private val _linkBbs = MutableLiveData() + val linkBbs: LiveData = _linkBbs + fun changeLinkBbs(newCommunity: CommunityEntity?) { + if (newCommunity == null) { + _linkBbs.value = LinkBbs(null, false, null) + return + } + val oldCommunity = linkBbs.value?.communityEntity + if (newCommunity != oldCommunity) { + // 用户重新选择了关联论坛 + _linkBbs.value = LinkBbs(newCommunity, false, null) + getForumSections(newCommunity) + getModeratorsInfo(newCommunity.id) + } + } + + private fun getForumSections(newCommunity: CommunityEntity) { + repository.checkHasSections(newCommunity.id) + .compose(observableToMain()) + .subscribe(object : Response() { + override fun onSubscribe(d: Disposable) { + compositeDisposable.add(d) + } + + override fun onResponse(response: Boolean?) { + if (response == true) { + // 此论坛有子板块,更新关联论坛ui状态 + _linkBbs.value = LinkBbs(newCommunity, true, null) + } + } + + }) + } + + private fun getModeratorsInfo(bbsId: String) { + repository.getModeratorsInfo(bbsId) + .compose(singleToMain()) + .subscribe(object : BiResponse() { + override fun onSuccess(data: Boolean) { + isModerator = data + } + + }).let(compositeDisposable::add) + } + + fun changeLinkSection(section: ForumDetailEntity.Section?) { + val oldLink = linkBbs.value + if (oldLink != null) { + _linkBbs.value = oldLink.copy(selectedSection = section) + } + } + + private val _draftCreated = MutableLiveData>() + val draftCreated: LiveData> = _draftCreated + fun createOrEditDraft(title: String, content: String, localImages: List) { + val request = createPublishRequest(title, content) + repository.createOrEditDraft(request, localImages) + .compose(singleToMain()) + .subscribe(object : BiResponse() { + override fun onSuccess(data: ResponseBody) { + _draftCreated.value = Event(true) + } + + override fun onFailure(exception: Exception) { + _draftCreated.value = Event(false) + } + + }).let(compositeDisposable::add) + } + + private val _imageTextPublished = MutableLiveData>() + val imageTextPublished: LiveData> = _imageTextPublished + fun publishImageText(title: String, content: String, images: List) { + val request = createPublishRequest(title, content) + if (isCreating) { + repository.publishImageText(request, images) + } else { + repository.editImageArticle(imageArticle!!.id, request, images) + } + .compose(singleToMain()) + .subscribe(object : BiResponse() { + override fun onSuccess(data: ImageArticleEntity) { + if (isCreating) { + ImageArticleUseCase.notifyDataCreated(data, request.draftId) + } else { + ImageArticleUseCase.notifyDataEdited(data) + } + _imageTextPublished.value = Event(true) + } + + override fun onFailure(exception: Exception) { + _imageTextPublished.value = Event(false) + } + + }).let(compositeDisposable::add) + } + + private fun createPublishRequest(title: String, content: String): PublishImageTextRequest { + val draftId = if (imageArticle?.isDraft == true) { + // 发布或者编辑草稿 + imageArticle?.id ?: "" + } else { + "" + } + val selectedBbs = linkBbs.value + val communityId = selectedBbs?.communityEntity?.id ?: "" + val communityType = selectedBbs?.communityEntity?.type ?: "" + val sectionId = selectedBbs?.selectedSection?.id ?: "" + val sectionIds = if (sectionId.isNotEmpty()) listOf(sectionId) else listOf() + return PublishImageTextRequest(title, content, communityType, communityId, sectionIds, emptyList(), draftId) + } + + private val _editImageArticle = MutableLiveData>() + val editImageArticle: LiveData> = _editImageArticle + fun setEditImageArticle(imageArticleEntity: ImageArticleEntity?) { + imageArticleEntity ?: return + imageArticle = imageArticleEntity + val community = imageArticleEntity.community?.toCommunityEntity() + val section = imageArticleEntity.sections.firstOrNull() + val game = imageArticleEntity.community?.game + val icon = if (game == null) { + community?.icon ?: "" + } else { + game.rawIcon ?: game.icon + } + community?.icon = icon + when { + community != null && section != null -> { // 绑定了论坛和子板块 + val formSection = ForumDetailEntity.Section(section.id, name = section.name) + _linkBbs.value = LinkBbs(community, hasSection = true, formSection) + } + + community != null -> { // 只绑定了论坛未绑定子板块 + changeLinkBbs(community) + } + + else -> { // 未绑定论坛 + changeLinkBbs(null) + } + } + + changeLinkBbs(community) + _editImageArticle.value = Event(imageArticleEntity) + } + + + data class LinkBbs( + val communityEntity: CommunityEntity?, + val hasSection: Boolean, + val selectedSection: ForumDetailEntity.Section? = null + ) +} \ No newline at end of file diff --git a/app/src/main/java/com/gh/gamecenter/mypost/MyPostWrapperFragment.kt b/app/src/main/java/com/gh/gamecenter/mypost/MyPostWrapperFragment.kt index 762389f237..3c57905dd8 100644 --- a/app/src/main/java/com/gh/gamecenter/mypost/MyPostWrapperFragment.kt +++ b/app/src/main/java/com/gh/gamecenter/mypost/MyPostWrapperFragment.kt @@ -9,6 +9,9 @@ import com.gh.common.util.NewFlatLogUtils import com.gh.gamecenter.R import com.gh.gamecenter.common.base.fragment.BaseFragment_TabLayout import com.gh.gamecenter.common.utils.goneIf +import com.gh.gamecenter.common.utils.toResString +import com.gh.gamecenter.forum.home.recommend.fragment.ImageArticleHomeFragment +import com.gh.gamecenter.forum.home.recommend.fragment.MyImageArticleListFragment import com.gh.gamecenter.login.user.UserManager import com.gh.gamecenter.personalhome.home.UserHistoryViewModel import com.gh.gamecenter.personalhome.home.game.UserCommentHistoryFragment @@ -25,6 +28,7 @@ class MyPostWrapperFragment : BaseFragment_TabLayout() { override fun initTabTitleList(tabTitleList: MutableList) { tabTitleList.add("评价") + tabTitleList.add(R.string.image_article.toResString()) tabTitleList.add(getString(R.string.collection_article)) tabTitleList.add(getString(R.string.ask_search_questions)) tabTitleList.add(getString(R.string.video)) @@ -50,10 +54,11 @@ class MyPostWrapperFragment : BaseFragment_TabLayout() { menuItem?.run { if (itemId == R.id.menu_draft && mLastPosition != 0) { val tabName = when (mLastPosition) { - 1 -> getString(R.string.collection_article) - 2 -> getString(R.string.ask_search_questions) - 3 -> getString(R.string.video) - 4 -> getString(R.string.answer) + 1 -> getString(R.string.image_article) + 2 -> getString(R.string.collection_article) + 3 -> getString(R.string.ask_search_questions) + 4 -> getString(R.string.video) + 5 -> getString(R.string.answer) else -> "" } NewFlatLogUtils.logHaloSelfPublishContent(tabName, "草稿箱") @@ -64,6 +69,7 @@ class MyPostWrapperFragment : BaseFragment_TabLayout() { override fun initFragmentList(fragments: MutableList) { fragments.add(UserCommentHistoryFragment.getInstance("我的发布", UserManager.getInstance().userId)) + fragments.add(MyImageArticleListFragment.newInstance()) fragments.add(MyPostFragment.getInstance(UserHistoryViewModel.TYPE.COMMUNITY_ARTICLE)) fragments.add(MyPostFragment.getInstance(UserHistoryViewModel.TYPE.QUESTION)) fragments.add(MyPostFragment.getInstance(UserHistoryViewModel.TYPE.VIDEO)) @@ -75,10 +81,11 @@ class MyPostWrapperFragment : BaseFragment_TabLayout() { NewFlatLogUtils.logHaloSelfPublishTab( when (position) { 0 -> "评价" - 1 -> getString(R.string.collection_article) - 2 -> getString(R.string.ask_search_questions) - 3 -> getString(R.string.video) - 4 -> getString(R.string.answer) + 1 -> "图文" + 2 -> getString(R.string.collection_article) + 3 -> getString(R.string.ask_search_questions) + 4 -> getString(R.string.video) + 5 -> getString(R.string.answer) else -> "" } ) diff --git a/app/src/main/java/com/gh/gamecenter/personalhome/PersonalItemViewHolder.kt b/app/src/main/java/com/gh/gamecenter/personalhome/PersonalItemViewHolder.kt index b90bde4be4..67f236ac18 100644 --- a/app/src/main/java/com/gh/gamecenter/personalhome/PersonalItemViewHolder.kt +++ b/app/src/main/java/com/gh/gamecenter/personalhome/PersonalItemViewHolder.kt @@ -36,7 +36,18 @@ class PersonalItemViewHolder( ) : BaseAnswerOrArticleItemViewHolder(binding.root) { - fun bindPersonalItem(historyEntity: PersonalHistoryEntity, entrance: String, position: Int) { + fun bindPersonalItem( + historyEntity: PersonalHistoryEntity, + entrance: String, + position: Int, + payloads: List? = null + ) { + if(!payloads.isNullOrEmpty()){ + // 局部更新 + commentCount.isClickable = true + bindCommendAndVote(historyEntity, entrance) + return + } commentCount.isClickable = true bindCommendAndVote(historyEntity, entrance) diff --git a/app/src/main/java/com/gh/gamecenter/personalhome/home/UserHistoryAdapter.kt b/app/src/main/java/com/gh/gamecenter/personalhome/home/UserHistoryAdapter.kt index 260724596e..70113158fa 100644 --- a/app/src/main/java/com/gh/gamecenter/personalhome/home/UserHistoryAdapter.kt +++ b/app/src/main/java/com/gh/gamecenter/personalhome/home/UserHistoryAdapter.kt @@ -1,42 +1,34 @@ package com.gh.gamecenter.personalhome.home -import android.app.Activity import android.content.Context -import android.text.SpannableStringBuilder import android.util.SparseBooleanArray +import android.view.LayoutInflater import android.view.View import android.view.ViewGroup import androidx.recyclerview.widget.RecyclerView -import com.facebook.drawee.view.SimpleDraweeView -import com.gh.common.util.DialogUtils -import com.gh.common.util.DirectUtils -import com.gh.common.util.NewsUtils -import com.gh.common.view.ImageContainerView -import com.gh.common.view.ImageContainerView.Companion.toImageContainerData -import com.gh.gamecenter.ImageViewerActivity +import androidx.recyclerview.widget.StaggeredGridLayoutManager +import com.alibaba.android.arouter.launcher.ARouter import com.gh.gamecenter.R import com.gh.gamecenter.adapter.viewholder.PersonalHomeRatingViewHolder import com.gh.gamecenter.common.baselist.ListAdapter -import com.gh.gamecenter.common.callback.ConfirmListener +import com.gh.gamecenter.common.constant.EntranceConsts import com.gh.gamecenter.common.constant.ItemViewType -import com.gh.gamecenter.common.utils.* +import com.gh.gamecenter.common.constant.RouteConsts +import com.gh.gamecenter.common.databinding.RefreshFooterviewBinding +import com.gh.gamecenter.common.utils.TextHelper +import com.gh.gamecenter.common.utils.ifLogin +import com.gh.gamecenter.common.utils.setTextWithHighlightedTextWrappedInsideWrapper +import com.gh.gamecenter.common.utils.toBinding import com.gh.gamecenter.common.viewholder.FooterViewHolder import com.gh.gamecenter.core.utils.NumberUtils -import com.gh.gamecenter.core.utils.SpanBuilder import com.gh.gamecenter.databinding.CommunityAnswerItemBinding import com.gh.gamecenter.databinding.PersonalHomeRatingBinding import com.gh.gamecenter.entity.PersonalHistoryEntity -import com.gh.gamecenter.feature.entity.AnswerEntity -import com.gh.gamecenter.feature.entity.ForumVideoEntity -import com.gh.gamecenter.forum.home.AnswerArticleVideoViewEventHelper -import com.gh.gamecenter.forum.home.ArticleItemVideoView -import com.gh.gamecenter.forum.home.ArticleItemVideoView.Companion.toArticleVideoData +import com.gh.gamecenter.forum.home.follow.viewholder.FollowFooterViewHolder +import com.gh.gamecenter.forum.home.recommend.ImageArticleDetailActivity +import com.gh.gamecenter.forum.home.recommend.adapter.MyImageArticleAdapter.MyImageArticleViewHolder import com.gh.gamecenter.gamedetail.rating.edit.RatingEditActivity import com.gh.gamecenter.personalhome.PersonalItemViewHolder -import com.gh.gamecenter.video.detail.VideoDetailContainerViewModel -import com.shuyu.gsyvideoplayer.builder.GSYVideoOptionBuilder -import com.shuyu.gsyvideoplayer.listener.GSYSampleCallBack -import com.shuyu.gsyvideoplayer.utils.OrientationUtils import java.util.regex.Pattern class UserHistoryAdapter( @@ -48,10 +40,15 @@ class UserHistoryAdapter( private var mExpandSparseBooleanArray = SparseBooleanArray() + override fun setListData(updateData: MutableList?) { + super.setListData(updateData) + } + override fun getItemViewType(position: Int): Int { return when { position == itemCount - 1 -> ItemViewType.ITEM_FOOTER mEntityList[position].type == "game_comment" -> ItemViewType.RATING_ITEM + mListViewModel.type == UserHistoryViewModel.TYPE.IMAGE_ARTICLE -> ITEM_TYPE_IMAGE_ARTICLE else -> ItemViewType.ITEM_BODY } } @@ -61,6 +58,11 @@ class UserHistoryAdapter( return when (viewType) { ItemViewType.ITEM_FOOTER -> { view = mLayoutInflater.inflate(com.gh.gamecenter.common.R.layout.refresh_footerview, parent, false) + val layoutParams = view.layoutParams + if (layoutParams is StaggeredGridLayoutManager.LayoutParams) { + layoutParams.isFullSpan = true + view.layoutParams = layoutParams + } FooterViewHolder(view) } @@ -69,6 +71,22 @@ class UserHistoryAdapter( PersonalHomeRatingViewHolder(PersonalHomeRatingBinding.bind(view)) } + ITEM_TYPE_IMAGE_ARTICLE -> { + MyImageArticleViewHolder(parent.toBinding(), + object : MyImageArticleViewHolder.OnMyArticleImageListener { + override fun navigateToImageArticleDetailPage(id: String) { + ARouter.getInstance().build(RouteConsts.activity.imageArticleDetailActivity) + .withString(EntranceConsts.KEY_IMAGE_ARTICLE_ID, id) + .navigation() + } + + override fun voteImageArticle(id: String, vote: Boolean) { + mListViewModel.useCase.voteImageArticle(id, vote) + } + + }) + } + else -> { view = mLayoutInflater.inflate(R.layout.community_answer_item, parent, false) PersonalItemViewHolder(mContext, CommunityAnswerItemBinding.bind(view), itemClickCallback) @@ -76,8 +94,26 @@ class UserHistoryAdapter( } } + override fun onBindViewHolder(holder: RecyclerView.ViewHolder, position: Int, payloads: List) { + if (payloads.isNotEmpty()) { + when (holder) { + is PersonalItemViewHolder -> { + bindNormalItem(holder, position, payloads) + } + + is MyImageArticleViewHolder -> { + bindImageArticle(holder, position, payloads) + } + } + } else { + super.onBindViewHolder(holder, position, payloads) + } + + } + override fun onBindViewHolder(holder: RecyclerView.ViewHolder, position: Int) { when (holder) { + is MyImageArticleViewHolder -> bindImageArticle(holder, position) is PersonalItemViewHolder -> bindNormalItem(holder, position) is PersonalHomeRatingViewHolder -> bindRatingItem(holder, position) is FooterViewHolder -> { @@ -87,10 +123,18 @@ class UserHistoryAdapter( } } - private fun bindNormalItem(holder: PersonalItemViewHolder, position: Int) { - val historyEntity = mEntityList[holder.adapterPosition] + private fun bindImageArticle( + holder: MyImageArticleViewHolder, + position: Int, + payloads: List? = null + ) { + val historyEntity = mEntityList[position] + holder.bind(historyEntity, payloads) + } - holder.bindPersonalItem(historyEntity, mEntrance, position) + private fun bindNormalItem(holder: PersonalItemViewHolder, position: Int, payloads: List? = null) { + val historyEntity = mEntityList[holder.adapterPosition] + holder.bindPersonalItem(historyEntity, mEntrance, position, payloads) } private fun bindRatingItem(holder: PersonalHomeRatingViewHolder, position: Int) { @@ -159,4 +203,9 @@ class UserHistoryAdapter( override fun getItemCount(): Int { return if (mEntityList == null || mEntityList.isEmpty()) 0 else mEntityList.size + 1 } + + companion object { + const val USER_HISTORY_PAYLOADS_VOTE_CHANGED = "user_history_payloads_vote_changed" + private const val ITEM_TYPE_IMAGE_ARTICLE = 110 + } } \ No newline at end of file diff --git a/app/src/main/java/com/gh/gamecenter/personalhome/home/UserHistoryFragment.kt b/app/src/main/java/com/gh/gamecenter/personalhome/home/UserHistoryFragment.kt index 501979cccf..ae960db22b 100644 --- a/app/src/main/java/com/gh/gamecenter/personalhome/home/UserHistoryFragment.kt +++ b/app/src/main/java/com/gh/gamecenter/personalhome/home/UserHistoryFragment.kt @@ -7,21 +7,32 @@ import android.view.View import android.widget.TextView import androidx.core.os.bundleOf import androidx.recyclerview.widget.RecyclerView +import androidx.recyclerview.widget.RecyclerView.ItemDecoration +import androidx.recyclerview.widget.StaggeredGridLayoutManager +import com.alibaba.android.arouter.launcher.ARouter import com.gh.gamecenter.core.AppExecutor import com.gh.common.util.* import com.gh.common.util.NewFlatLogUtils import com.gh.gamecenter.R import com.gh.gamecenter.common.baselist.ListAdapter import com.gh.gamecenter.common.baselist.ListFragment +import com.gh.gamecenter.common.constant.EntranceConsts +import com.gh.gamecenter.common.constant.RouteConsts +import com.gh.gamecenter.common.entity.CommunityEntity import com.gh.gamecenter.feature.entity.CommentEntity import com.gh.gamecenter.common.utils.* +import com.gh.gamecenter.common.view.FixLinearLayoutManager import com.gh.gamecenter.core.utils.MD5Utils import com.gh.gamecenter.databinding.FragmentUserPublishBinding import com.gh.gamecenter.entity.* +import com.gh.gamecenter.eventbus.EBImageArticleChanged import com.gh.gamecenter.feature.entity.ForumVideoEntity import com.gh.gamecenter.feature.entity.PersonalEntity import com.gh.gamecenter.forum.home.ForumScrollCalculatorHelper +import com.gh.gamecenter.forum.home.recommend.ImageArticleDetailActivity +import com.gh.gamecenter.forum.home.recommend.fragment.ImageArticleHomeFragment import com.gh.gamecenter.gamedetail.rating.RatingReplyActivity +import com.gh.gamecenter.personalhome.home.UserHistoryAdapter.Companion.USER_HISTORY_PAYLOADS_VOTE_CHANGED import com.gh.gamecenter.qa.article.detail.ArticleDetailActivity import com.gh.gamecenter.qa.entity.ArticleDetailEntity import com.gh.gamecenter.qa.entity.QuestionsDetailEntity @@ -30,6 +41,8 @@ import com.gh.gamecenter.qa.video.detail.ForumVideoDetailActivity import com.gh.gamecenter.video.detail.CustomManager import com.halo.assistant.HaloApp import com.shuyu.gsyvideoplayer.video.base.GSYVideoView +import org.greenrobot.eventbus.Subscribe +import org.greenrobot.eventbus.ThreadMode class UserHistoryFragment : ListFragment() { @@ -79,6 +92,68 @@ class UserHistoryFragment : ListFragment { + updateImageArticleItem(changed.id, true) { + val newCount = it.count + newCount.vote = changed.count + it.count = newCount + it.me.isCommunityArticleVote = changed.isVoted + } + } + + changed is EBImageArticleChanged.CommentChanged -> { + updateImageArticleItem(changed.id, true) { + val newCount = it.count + newCount.comment = changed.count + it.count = newCount + } + } + + changed is EBImageArticleChanged.DataDeleted -> { + mViewModel?.deleteImageArticle(changed.imageArticleId) + } + + changed is EBImageArticleChanged.DataChanged -> { + val imageArticle = changed.data + updateImageArticleItem(changed.data.id, false) { + it.title = imageArticle.title + it.brief = imageArticle.content + it.images = imageArticle.images + it.imagesInfo = imageArticle.imagesInfos + it.community = imageArticle.community?.toCommunityEntity() ?: CommunityEntity() + it.sections = imageArticle.sections + it.count = imageArticle.count + it.user = imageArticle.user + it.time = imageArticle.time.update + it.status = imageArticle.status + } + } + } + } + + private fun updateImageArticleItem( + id: String, + hasPayload: Boolean, + block: (PersonalHistoryEntity) -> Unit + ) { + val dataList = mAdapter?.entityList ?: return + val position = + dataList.indexOfFirst { it.type == ImageArticleEntity.IMAGE_ARTICLE_TYPE && it.id == id } + if (position != -1) { + val item = dataList[position] + block(item) + if (hasPayload) { + mAdapter?.notifyItemChanged(position, USER_HISTORY_PAYLOADS_VOTE_CHANGED) + } else { + mAdapter?.notifyItemChanged(position) + } + + } + } + override fun onResume() { resumeVideo() super.onResume() @@ -148,12 +223,14 @@ class UserHistoryFragment : ListFragment { allType.text = "全部 $count" } + + UserHistoryViewModel.TYPE.IMAGE_ARTICLE -> { + imageArticleType.text = getString(R.string.image_article_with_count, "$count") + } + UserHistoryViewModel.TYPE.VIDEO -> { mCount.video = count videoType.text = "视频 $count" } + UserHistoryViewModel.TYPE.COMMUNITY_ARTICLE -> { mCount.communityArticle = count articleType.text = "帖子 $count" } + UserHistoryViewModel.TYPE.QUESTION -> { mCount.question = count questionType.text = "提问 $count" } + UserHistoryViewModel.TYPE.ANSWER -> { mCount.answer = count answerType.text = "回答 $count" @@ -288,6 +373,10 @@ class UserHistoryFragment : ListFragment?) { + if (ts != null) { + val layoutManager = mListRv.layoutManager ?: return + if (mViewModel?.type == UserHistoryViewModel.TYPE.IMAGE_ARTICLE) { + if (layoutManager !is StaggeredGridLayoutManager) { + mListRv.layoutManager = StaggeredGridLayoutManager(2, RecyclerView.VERTICAL) + if (mListRv.itemDecorationCount == 0) { + itemDecoration?.let(mListRv::addItemDecoration) + } + mListRv.adapter = mAdapter + } + } else { + if (layoutManager is StaggeredGridLayoutManager) { + mListRv.layoutManager = FixLinearLayoutManager(requireContext()) + if (mListRv.itemDecorationCount > 0) { + mItemDecoration?.let(mListRv::removeItemDecoration) + } + mListRv.adapter = mAdapter + } + } + } + super.onChanged(ts) + } + private fun getCurrentTabName(): String { return when (mCurrentType) { UserHistoryViewModel.TYPE.ALL -> "全部" + UserHistoryViewModel.TYPE.IMAGE_ARTICLE -> "图文" UserHistoryViewModel.TYPE.VIDEO -> "视频" UserHistoryViewModel.TYPE.COMMUNITY_ARTICLE -> "帖子" UserHistoryViewModel.TYPE.QUESTION -> "提问" @@ -339,7 +454,14 @@ class UserHistoryFragment : ListFragment { val resultData = data.getParcelableExtra(ArticleDetailEntity::class.java.simpleName) @@ -411,6 +534,7 @@ class UserHistoryFragment : ListFragment { val resultData = data.getParcelableExtra(CommentEntity::class.java.simpleName) @@ -421,6 +545,8 @@ class UserHistoryFragment : ListFragment { val resultData = data.getParcelableExtra(QuestionsDetailEntity::class.java.simpleName) @@ -431,6 +557,7 @@ class UserHistoryFragment : ListFragment { val resultData = data.getParcelableExtra(ForumVideoEntity::class.java.simpleName) @@ -468,6 +595,7 @@ class UserHistoryFragment : ListFragment() var count = MutableLiveData() + private val imageArticleRepository = ImageArticleRepository.newInstance() + private val compositeDisposable = CompositeDisposable() + val useCase = ImageArticleUseCase(imageArticleRepository, compositeDisposable) + init { setOverLimitSize(1) } @@ -50,7 +59,6 @@ class UserHistoryViewModel( override fun mergeResultLiveData() { mResultLiveData.addSource(mListLiveData) { list -> - videoList = ArrayList(list.map { it.transformForumVideoEntity() }) mResultLiveData.postValue(list) @@ -132,6 +140,16 @@ class UserHistoryViewModel( } } + fun deleteImageArticle(imageArticleId: String) { + val oldData = mResultLiveData.value ?: return + val newData = oldData.toMutableList() + val hasRemoved = + newData.removeAll { it.type == ImageArticleEntity.IMAGE_ARTICLE_TYPE && it.id == imageArticleId } + if (hasRemoved) { + mResultLiveData.value = newData + } + } + enum class SCENE(val value: String) { COMMENT("comment"), QUESTION_ANSWER("question_answer") @@ -139,6 +157,7 @@ class UserHistoryViewModel( enum class TYPE(val value: String) { ALL("all"), + IMAGE_ARTICLE("image_article"), VIDEO("video"), COMMUNITY_ARTICLE("community_article"), ANSWER("answer"), diff --git a/app/src/main/java/com/gh/gamecenter/qa/answer/BaseAnswerOrArticleItemViewHolder.kt b/app/src/main/java/com/gh/gamecenter/qa/answer/BaseAnswerOrArticleItemViewHolder.kt index f5e214af59..65f86143c4 100644 --- a/app/src/main/java/com/gh/gamecenter/qa/answer/BaseAnswerOrArticleItemViewHolder.kt +++ b/app/src/main/java/com/gh/gamecenter/qa/answer/BaseAnswerOrArticleItemViewHolder.kt @@ -4,17 +4,21 @@ import android.annotation.SuppressLint import android.view.View import android.widget.TextView import com.airbnb.lottie.LottieAnimationView +import com.alibaba.android.arouter.launcher.ARouter import com.gh.common.util.* import com.gh.common.util.LogUtils import com.gh.common.util.NewLogUtils import com.gh.gamecenter.feature.view.GameIconView import com.gh.gamecenter.R import com.gh.gamecenter.common.base.BaseRecyclerViewHolder +import com.gh.gamecenter.common.constant.EntranceConsts +import com.gh.gamecenter.common.constant.RouteConsts import com.gh.gamecenter.common.entity.CommunityEntity import com.gh.gamecenter.common.retrofit.BiResponse import com.gh.gamecenter.common.retrofit.Response import com.gh.gamecenter.common.utils.* import com.gh.gamecenter.core.utils.* +import com.gh.gamecenter.entity.ImageArticleEntity import com.gh.gamecenter.entity.PersonalHistoryEntity import com.gh.gamecenter.entity.VoteEntity import com.gh.gamecenter.forum.detail.ForumDetailActivity @@ -24,6 +28,7 @@ import com.gh.gamecenter.feature.entity.AnswerEntity import com.gh.gamecenter.feature.entity.CommunityItemData import com.gh.gamecenter.feature.entity.ArticleEntity import com.gh.gamecenter.feature.entity.Count +import com.gh.gamecenter.forum.home.recommend.ImageArticleDetailActivity import com.gh.gamecenter.qa.entity.QuestionsDetailEntity import com.gh.gamecenter.qa.questions.invite.QuestionsInviteActivity import com.gh.gamecenter.qa.questions.newdetail.NewQuestionDetailActivity @@ -267,6 +272,12 @@ open class BaseAnswerOrArticleItemViewHolder(itemView: View) : BaseRecyclerViewH ) } + ImageArticleEntity.IMAGE_ARTICLE_TYPE -> { + ARouter.getInstance().build(RouteConsts.activity.imageArticleDetailActivity) + .withString(EntranceConsts.KEY_IMAGE_ARTICLE_ID, entity.id) + .navigation() + } + else -> { val communityId = entity.community.id val intent = ArticleDetailActivity.getCommentIntent( @@ -398,19 +409,27 @@ open class BaseAnswerOrArticleItemViewHolder(itemView: View) : BaseRecyclerViewH } }) } else { - val voteObservable = if (entity.type == "community_article") { - RetrofitManager.getInstance().api.postCommunityArticleVote(entity.id) - } else { - RetrofitManager.getInstance().api - .postVoteQuestionComment(entity.questions.id, entity.id) - .map { GsonUtils.fromJson(it.string(), VoteEntity::class.java) } + when (entity.type) { + "community_article" -> { + RetrofitManager.getInstance().api.postCommunityArticleVote(entity.id) + } + + ImageArticleEntity.IMAGE_ARTICLE_TYPE -> { + RetrofitManager.getInstance().api.voteArticleImage(entity.id) + .toObservable() + } + + else -> { + RetrofitManager.getInstance().api + .postVoteQuestionComment(entity.questions.id, entity.id) + .map { GsonUtils.fromJson(it.string(), VoteEntity::class.java) } + } } - voteObservable .subscribeOn(Schedulers.io()) .observeOn(AndroidSchedulers.mainThread()) .subscribe(object : Response() { override fun onResponse(response: VoteEntity?) { - if (entity.type == "community_article") { + if (entity.type == "community_article" || entity.type == ImageArticleEntity.IMAGE_ARTICLE_TYPE) { entity.me.isCommunityArticleVote = true } else { entity.me.isAnswerVoted = true @@ -484,18 +503,26 @@ open class BaseAnswerOrArticleItemViewHolder(itemView: View) : BaseRecyclerViewH } }) } else { - val unVoteObservable = if (entity.type == "community_article") { - RetrofitManager.getInstance().api.postCommunityArticleUnVote(entity.id) - } else { - RetrofitManager.getInstance().api - .postAnswerUnvote(entity.id) + when (entity.type) { + "community_article" -> { + RetrofitManager.getInstance().api.postCommunityArticleUnVote(entity.id) + } + + ImageArticleEntity.IMAGE_ARTICLE_TYPE -> { + RetrofitManager.getInstance().api.unVoteArticleImage(entity.id) + .toObservable() + } + + else -> { + RetrofitManager.getInstance().api + .postAnswerUnvote(entity.id) + } } - unVoteObservable .subscribeOn(Schedulers.io()) .observeOn(AndroidSchedulers.mainThread()) .subscribe(object : Response() { override fun onResponse(response: VoteEntity?) { - if (entity.type == "community_article") { + if (entity.type == "community_article" || entity.type == ImageArticleEntity.IMAGE_ARTICLE_TYPE) { entity.me.isCommunityArticleVote = false } else { entity.me.isAnswerVoted = false diff --git a/app/src/main/java/com/gh/gamecenter/qa/article/detail/ArticleDetailCommentListFragment.kt b/app/src/main/java/com/gh/gamecenter/qa/article/detail/ArticleDetailCommentListFragment.kt index e59c98e811..546f103f1d 100644 --- a/app/src/main/java/com/gh/gamecenter/qa/article/detail/ArticleDetailCommentListFragment.kt +++ b/app/src/main/java/com/gh/gamecenter/qa/article/detail/ArticleDetailCommentListFragment.kt @@ -13,7 +13,6 @@ import com.gh.gamecenter.common.utils.viewModelProvider import com.gh.gamecenter.common.view.CustomDividerItemDecoration import com.gh.gamecenter.qa.comment.base.BaseCommentAdapter import com.halo.assistant.HaloApp -import splitties.views.backgroundColor class ArticleDetailCommentListFragment : ListFragment() { diff --git a/app/src/main/java/com/gh/gamecenter/qa/article/detail/ArticleDetailFragment.kt b/app/src/main/java/com/gh/gamecenter/qa/article/detail/ArticleDetailFragment.kt index 21c7e16ae7..5c2e9d2150 100644 --- a/app/src/main/java/com/gh/gamecenter/qa/article/detail/ArticleDetailFragment.kt +++ b/app/src/main/java/com/gh/gamecenter/qa/article/detail/ArticleDetailFragment.kt @@ -793,7 +793,8 @@ class ArticleDetailFragment : BaseCommentFragment { ifLogin("帖子详情") { - BbsReportHelper.showReportDialog(mViewModel.detailEntity?.id ?: "") + + BbsReportHelper.showReportDialog(BbsReportHelper.PostsReporter(mViewModel.detailEntity?.id ?: "")) } NewLogUtils.logSharePanelClick( "click_report", @@ -991,7 +992,6 @@ class ArticleDetailFragment : BaseCommentFragment { GameCollectionCommentConversationFragment().with(intent.extras) } + isCommentConversation && commentId.isNotEmpty() -> { CommentConversationFragment().with(intent.extras) } + answerId.isNotEmpty() -> { NewCommentFragment.getAnswerCommentInstance( answerId, @@ -94,6 +97,7 @@ class CommentActivity : BaseActivity() { commentCallback ) } + articleId.isNotEmpty() -> { NewCommentFragment.getCommunityArticleCommentInstance( articleId, @@ -106,6 +110,7 @@ class CommentActivity : BaseActivity() { commentCallback ) } + questionId.isNotEmpty() -> { NewCommentFragment.getCommunityQuestionCommentInstance( questionId, @@ -118,6 +123,7 @@ class CommentActivity : BaseActivity() { commentCallback ) } + gameCollectionId.isNotEmpty() -> { NewCommentFragment.getGameCollectionCommentInstance( gameCollectionId, @@ -131,6 +137,18 @@ class CommentActivity : BaseActivity() { commentCallback ) } + + imageArticleId.isNotEmpty() -> { + NewCommentFragment.getImageArticleCommentInstance( + imageArticleId, + showKeyboard, + commentCount, + mShowInputOnly, + commentEntity, + commentCallback + ) + } + else -> { NewCommentFragment.getVideoCommentInstance( videoId, @@ -197,6 +215,8 @@ class CommentActivity : BaseActivity() { const val REQUEST_CODE = 8123 + const val IMAGE_ARTICLE_ID = "image_article_id" + @JvmStatic fun getAnswerCommentIntent( context: Context, @@ -280,6 +300,7 @@ class CommentActivity : BaseActivity() { articleId: String, videoId: String, questionId: String, + imageArticleId: String, showKeyboard: Boolean = false, position: Int = -1, entrance: String, @@ -290,6 +311,7 @@ class CommentActivity : BaseActivity() { intent.putExtra(EntranceConsts.KEY_COMMUNITY_ARTICLE_ID, articleId) intent.putExtra(VIDEO_ID, videoId) intent.putExtra(QUESTION_ID, questionId) + intent.putExtra(IMAGE_ARTICLE_ID, imageArticleId) intent.putExtra(EntranceConsts.KEY_COMMUNITY_ID, communityId) intent.putExtra(EntranceConsts.KEY_POSITION, position) intent.putExtra(KEY_COMMENT_ID, commentId) @@ -446,6 +468,30 @@ class CommentActivity : BaseActivity() { return intent } + /** + * 回复图文评论 + */ + fun getImageArticleCommentReplyIntent( + context: Context, + imageArticleId: String, + commentId: String, + commentCount: Int? = 0, + commentEntity: CommentEntity? = null + ): Intent { + val intent = Intent(context, CommentActivity::class.java) + intent.putExtra(KEY_COMMENT_ID, commentId) + intent.putExtra(COMMENT_COUNT, commentCount) + intent.putExtra(SHOW_KEYBOARD, true) + intent.putExtra(SHOW_INPUT_ONLY, true) + intent.putExtra(COMMENT_ENTITY, commentEntity) + intent.putExtra(IMAGE_ARTICLE_ID, imageArticleId) + intent.putExtra(USE_REPLY_API, true) + if (context is Activity) { + context.overridePendingTransition(0, 0) + } + return intent + } + /** * 游戏单评论对话 */ @@ -467,6 +513,28 @@ class CommentActivity : BaseActivity() { intent.putExtra(EntranceConsts.KEY_IS_COMMENT_CONVERSATION, true) return intent } + + /** + * 评论图文 + */ + @JvmStatic + fun getImageArticleCommentIntent( + context: Context, + imageArticleId: String, + communityId: String, + commentCount: Int? = 0, + ): Intent { + val intent = Intent(context, CommentActivity::class.java) + intent.putExtra(IMAGE_ARTICLE_ID, imageArticleId) + intent.putExtra(COMMENT_COUNT, commentCount) + intent.putExtra(SHOW_KEYBOARD, true) + intent.putExtra(COMMUNITY_ID, communityId) + intent.putExtra(SHOW_INPUT_ONLY, true) + if (context is Activity) { + context.overridePendingTransition(0, 0) + } + return intent + } } interface CommentListener { diff --git a/app/src/main/java/com/gh/gamecenter/qa/comment/NewCommentAdapter.kt b/app/src/main/java/com/gh/gamecenter/qa/comment/NewCommentAdapter.kt index bfc41b5155..fa5e3d145a 100644 --- a/app/src/main/java/com/gh/gamecenter/qa/comment/NewCommentAdapter.kt +++ b/app/src/main/java/com/gh/gamecenter/qa/comment/NewCommentAdapter.kt @@ -172,6 +172,7 @@ class NewCommentAdapter( mViewModel.communityId, mViewModel.videoId, mViewModel.questionId, + mViewModel.imageArticleId, commentEntity, holder.commentLikeCountTv, holder.commentLikeIv, @@ -251,6 +252,8 @@ class NewCommentAdapter( CommentType.GAME_COLLECTION, CommentType.GAME_COLLECTION_CONVERSATION -> "游戏单详情-评论管理" + + CommentType.IMAGE_ARTICLE -> "图文详情-评论管理" } val userHomePageTabPosition = if (mViewModel.commentType.isVideo()) 2 else 1 diff --git a/app/src/main/java/com/gh/gamecenter/qa/comment/NewCommentConversationFragment.kt b/app/src/main/java/com/gh/gamecenter/qa/comment/NewCommentConversationFragment.kt index 990f722a72..be5b0c4b2f 100644 --- a/app/src/main/java/com/gh/gamecenter/qa/comment/NewCommentConversationFragment.kt +++ b/app/src/main/java/com/gh/gamecenter/qa/comment/NewCommentConversationFragment.kt @@ -44,9 +44,12 @@ class NewCommentConversationFragment : NewCommentFragment() { mVideoId = arguments?.getString(CommentActivity.VIDEO_ID) ?: "" mIsVideoAuthor = arguments?.getBoolean(CommentActivity.IS_VIDEO_AUTHOR, false) ?: false + mImageArticleId = arguments?.getString(CommentActivity.IMAGE_ARTICLE_ID) ?: "" + mCommentType = when { mAnswerId.isNotEmpty() -> CommentType.ANSWER_CONVERSATION mArticleId.isNotEmpty() -> CommentType.COMMUNITY_ARTICLE_CONVERSATION + mImageArticleId.isNotEmpty() -> CommentType.IMAGE_ARTICLE else -> CommentType.VIDEO_CONVERSATION } diff --git a/app/src/main/java/com/gh/gamecenter/qa/comment/NewCommentFragment.kt b/app/src/main/java/com/gh/gamecenter/qa/comment/NewCommentFragment.kt index 1c9175acb7..fc10ec2867 100644 --- a/app/src/main/java/com/gh/gamecenter/qa/comment/NewCommentFragment.kt +++ b/app/src/main/java/com/gh/gamecenter/qa/comment/NewCommentFragment.kt @@ -42,6 +42,7 @@ import com.gh.gamecenter.feature.eventbus.EBDeleteComment import com.gh.gamecenter.feature.selector.ChooseType import com.gh.gamecenter.qa.comment.CommentActivity.Companion.GAME_COLLECTION_ID import com.gh.gamecenter.qa.comment.CommentActivity.Companion.GAME_COLLECTION_TITLE +import com.gh.gamecenter.qa.comment.CommentActivity.Companion.IMAGE_ARTICLE_ID import com.gh.gamecenter.qa.comment.CommentActivity.Companion.QUESTION_ID import com.halo.assistant.HaloApp import com.lightgame.utils.Util_System_Keyboard @@ -82,6 +83,7 @@ open class NewCommentFragment : ListFragment protected var mGameCollectionTitle: String = "" protected var mCommentId: String = "" protected var mRootCommentId: String = "" + protected var mImageArticleId: String = "" protected var mShowInputOnly: Boolean = false // 是否只显示输入框,不显示列表 protected var mIsVideoAuthor: Boolean = false//是否是视频作者 protected var mCommentType = CommentType.ANSWER @@ -130,6 +132,7 @@ open class NewCommentFragment : ListFragment mShowInputOnly = getBoolean(SHOW_INPUT_ONLY, false) mCommentEntity = getParcelable(COMMENT_ENTITY) mIsVideoAuthor = getBoolean(IS_VIDEO_AUTHOR, false) + mImageArticleId = getString(IMAGE_ARTICLE_ID) ?: "" } super.onCreate(savedInstanceState) @@ -229,6 +232,8 @@ open class NewCommentFragment : ListFragment CommentType.GAME_COLLECTION, CommentType.GAME_COLLECTION_CONVERSATION -> "游戏单评论及回复" + CommentType.IMAGE_ARTICLE -> "图文的评论和回复" + else -> "" } ErrorHelper.handleError( @@ -412,7 +417,8 @@ open class NewCommentFragment : ListFragment gameCollectionId = mGameCollectionId, rootCommentId = mRootCommentId, commentType = mCommentType, - isVideoAuthor = mIsVideoAuthor + isVideoAuthor = mIsVideoAuthor, + imageArticleId = mImageArticleId ) ) return mViewModel @@ -435,6 +441,8 @@ open class NewCommentFragment : ListFragment CommentType.GAME_COLLECTION, CommentType.GAME_COLLECTION_CONVERSATION -> "(游戏单详情-评论列表)" + + CommentType.IMAGE_ARTICLE -> "图文详情-评论列表" } mAdapter = NewCommentAdapter(requireContext(), mViewModel, true, this, this, entrance) } @@ -598,6 +606,10 @@ open class NewCommentFragment : ListFragment "游戏单详情-评论-回复" } } + + CommentType.IMAGE_ARTICLE -> { + "图文详情-评论-写评论" + } } } @@ -945,5 +957,28 @@ open class NewCommentFragment : ListFragment ) } } + + fun getImageArticleCommentInstance( + imageArticleId: String, + showSoftKeyboardOnStartUp: Boolean, + commentCount: Int, + showInputOnly: Boolean, + commentEntity: CommentEntity?, + listener: CommentActivity.CommentListener + ): NewCommentFragment { + return NewCommentFragment().apply { + mCommentListener = listener + with( + bundleOf( + SHOW_SOFT_KEY_BOARD_ON_STARTUP to showSoftKeyboardOnStartUp, + IMAGE_ARTICLE_ID to imageArticleId, + COMMENT_COUNT to commentCount, + COMMENT_TYPE to CommentType.IMAGE_ARTICLE, + SHOW_INPUT_ONLY to showInputOnly, + COMMENT_ENTITY to commentEntity + ) + ) + } + } } } diff --git a/app/src/main/java/com/gh/gamecenter/qa/comment/NewCommentViewModel.kt b/app/src/main/java/com/gh/gamecenter/qa/comment/NewCommentViewModel.kt index 07ae53161b..b541449f97 100644 --- a/app/src/main/java/com/gh/gamecenter/qa/comment/NewCommentViewModel.kt +++ b/app/src/main/java/com/gh/gamecenter/qa/comment/NewCommentViewModel.kt @@ -29,7 +29,6 @@ import io.reactivex.Observable import io.reactivex.android.schedulers.AndroidSchedulers import io.reactivex.schedulers.Schedulers import okhttp3.ResponseBody -import org.json.JSONArray import org.json.JSONObject import retrofit2.HttpException @@ -44,7 +43,8 @@ open class NewCommentViewModel( var gameCollectionId: String = "", var rootCommentId: String = "", var commentType: CommentType = CommentType.ANSWER, - var isVideoAuthor: Boolean = false + var isVideoAuthor: Boolean = false, + var imageArticleId: String = "" ) : ListViewModel(application) { private val mPostCommentLiveData = MutableLiveData>() @@ -138,6 +138,7 @@ open class NewCommentViewModel( this.articleDetail = it }, {}) } + CommentType.VIDEO, CommentType.VIDEO_CONVERSATION -> { api.getBbsVideoDetail(videoId) @@ -147,6 +148,7 @@ open class NewCommentViewModel( this.videoDetail = it }, {}) } + CommentType.COMMUNITY_QUESTION, CommentType.COMMUNITY_QUESTION_CONVERSATION -> { api.getQuestionsById(questionId) @@ -156,6 +158,7 @@ open class NewCommentViewModel( this.questionDetail = it }, {}) } + else -> {} } } @@ -165,8 +168,7 @@ open class NewCommentViewModel( CommentType.COMMUNITY_QUESTION, CommentType.COMMUNITY_QUESTION_CONVERSATION -> { questionDetail?.let { - if (reply) - { + if (reply) { SensorsBridge.trackArticleReply( customerType = it.user.auth?.text ?: "", articleId = it.id ?: "", @@ -439,6 +441,15 @@ open class NewCommentViewModel( api.postReplyToGameCollectionComment(gameCollectionId, commentEntity.id, body) } } + + CommentType.IMAGE_ARTICLE -> { + if (commentEntity == null) { + api.postImageArticleComment(imageArticleId, body) + } else { + api.postReplyToImageArticleComment(commentEntity.id, body) + } + + } } // TODO Remove this apiResponse crap. @@ -632,7 +643,8 @@ open class NewCommentViewModel( private val gameCollectionId: String = "", private val rootCommentId: String = "", private val isVideoAuthor: Boolean = false, - private val commentType: CommentType + private val commentType: CommentType, + private val imageArticleId: String = "" ) : ViewModelProvider.NewInstanceFactory() { override fun create(modelClass: Class): T { @@ -647,7 +659,8 @@ open class NewCommentViewModel( commentId = commentId, rootCommentId = rootCommentId, commentType = commentType, - isVideoAuthor = isVideoAuthor + isVideoAuthor = isVideoAuthor, + imageArticleId = imageArticleId ) as T } } @@ -667,7 +680,9 @@ enum class CommentType { VIDEO_CONVERSATION, GAME_COLLECTION, - GAME_COLLECTION_CONVERSATION; + GAME_COLLECTION_CONVERSATION, + IMAGE_ARTICLE; + fun isVideo(): Boolean { return (this == VIDEO || this == VIDEO_CONVERSATION) diff --git a/app/src/main/java/com/gh/gamecenter/qa/comment/StairsCommentFragment.kt b/app/src/main/java/com/gh/gamecenter/qa/comment/StairsCommentFragment.kt index 3c97c37caa..f6a0a49b03 100644 --- a/app/src/main/java/com/gh/gamecenter/qa/comment/StairsCommentFragment.kt +++ b/app/src/main/java/com/gh/gamecenter/qa/comment/StairsCommentFragment.kt @@ -39,6 +39,8 @@ class StairsCommentFragment : NewCommentFragment() { CommentType.GAME_COLLECTION, CommentType.GAME_COLLECTION_CONVERSATION -> "(游戏单详情-评论列表)" + + CommentType.IMAGE_ARTICLE -> "(图文详情-评论列表)" } mAdapter = StairsCommentAdapter(requireContext(), mViewModel, true, this, this, entrance) } diff --git a/app/src/main/java/com/gh/gamecenter/qa/comment/StairsCommentViewHolder.kt b/app/src/main/java/com/gh/gamecenter/qa/comment/StairsCommentViewHolder.kt index bbf79c7fb8..414132bed7 100644 --- a/app/src/main/java/com/gh/gamecenter/qa/comment/StairsCommentViewHolder.kt +++ b/app/src/main/java/com/gh/gamecenter/qa/comment/StairsCommentViewHolder.kt @@ -261,6 +261,7 @@ class StairsCommentViewHolder( mViewModel.articleId, mViewModel.communityId, mViewModel.videoId, + mViewModel.imageArticleId, commentEntity, holder.binding.commentLikeCount, holder.binding.commentLike, @@ -314,6 +315,8 @@ class StairsCommentViewHolder( CommentType.GAME_COLLECTION, CommentType.GAME_COLLECTION_CONVERSATION -> "游戏单详情-评论管理" + + CommentType.IMAGE_ARTICLE ->"图文详情-评论管理" } holder.binding.commentUserIcon.setOnClickListener { DirectUtils.directToHomeActivity( diff --git a/app/src/main/java/com/gh/gamecenter/qa/comment/base/BaseCommentAdapter.kt b/app/src/main/java/com/gh/gamecenter/qa/comment/base/BaseCommentAdapter.kt index 661b63db4b..636a216c7a 100644 --- a/app/src/main/java/com/gh/gamecenter/qa/comment/base/BaseCommentAdapter.kt +++ b/app/src/main/java/com/gh/gamecenter/qa/comment/base/BaseCommentAdapter.kt @@ -32,6 +32,7 @@ import com.gh.gamecenter.core.utils.SpanBuilder import com.gh.gamecenter.databinding.* import com.gh.gamecenter.feature.entity.CommentEntity import com.gh.gamecenter.feature.entity.Permissions +import com.gh.gamecenter.forum.home.recommend.ImageArticleUseCase import com.gh.gamecenter.qa.article.detail.* import com.gh.gamecenter.qa.comment.CommentActivity import com.gh.gamecenter.qa.comment.CommentPictureAdapter @@ -178,6 +179,7 @@ abstract class BaseCommentAdapter( //刷新 ITEM_FILTER mViewModel.commentCount -= 1 + mViewModel.updateCommentCount(mViewModel.commentCount) notifyItemChanged(1) val isArticleDetail = this is ArticleDetailAdapter @@ -241,14 +243,17 @@ abstract class BaseCommentAdapter( binding.progressBar.visibility = View.GONE binding.footerTv.setText(com.gh.gamecenter.common.R.string.loading_failed_retry) } + isOver -> { binding.progressBar.visibility = View.GONE binding.footerTv.setText(loadOverHint) } + isLoading -> { binding.progressBar.visibility = View.VISIBLE binding.footerTv.setText(com.gh.gamecenter.common.R.string.loading) } + else -> { binding.progressBar.visibility = View.GONE } @@ -285,18 +290,23 @@ abstract class BaseCommentAdapter( article != null -> { "全部评论" } + questions != null -> { "全部回答" } + comment != null -> { "全部回复" } + gameCollection != null -> { "玩家评论" } + commentDetail != null -> { "全部讨论" } + else -> { "" } @@ -325,6 +335,7 @@ abstract class BaseCommentAdapter( } } + 1 -> { mViewModel.changeSort(BaseCommentViewModel.SortType.LATEST) @@ -380,6 +391,7 @@ abstract class BaseCommentAdapter( } MtaHelper.onEvent("帖子详情", "全部评论", "评论正文") } + viewModel.videoId.isNotEmpty() -> { CommentActivity.getVideoCommentReplyIntent( binding.root.context, @@ -396,6 +408,7 @@ abstract class BaseCommentAdapter( ) } } + viewModel.questionId.isNotEmpty() -> { CommentActivity.getQuestionCommentReplyIntent( binding.root.context, @@ -426,11 +439,13 @@ abstract class BaseCommentAdapter( viewModel.articleId, viewModel.videoId, viewModel.questionId, + viewModel.imageArticleId, false, comment.floor, entrance, - PATH_ARTICLE_DETAIL - ).apply { + PATH_ARTICLE_DETAIL, + + ).apply { binding.root.context.startActivity(this) } MtaHelper.onEvent("帖子详情", "全部评论", "回复") @@ -527,9 +542,11 @@ abstract class BaseCommentAdapter( viewModel.articleId.isNotEmpty() -> { viewModel.topItemData?.articleDetail?.user?.id ?: "" } + viewModel.questionId.isNotEmpty() -> { viewModel.topItemData?.questionDetail?.user?.id ?: "" } + else -> { "" } @@ -577,6 +594,7 @@ abstract class BaseCommentAdapter( viewModel.articleId, viewModel.videoId, viewModel.questionId, + viewModel.imageArticleId, false, comment.floor, entrance, @@ -665,12 +683,19 @@ abstract class BaseCommentAdapter( viewModel.articleId.isNotEmpty() -> { if (viewModel is CommentConversationViewModel) PATH_ARTICLE_DETAIL_COMMENT else PATH_ARTICLE_DETAIL } + viewModel.questionId.isNotEmpty() -> { if (viewModel is CommentConversationViewModel) PATH_QUESTION_DETAIL_COMMENT else PATH_QUESTION_DETAIL } + viewModel.videoId.isNotEmpty() -> { if (viewModel is CommentConversationViewModel) PATH_VIDEO_DETAIL_COMMENT else PATH_VIDEO_DETAIL } + + viewModel.imageArticleId.isNotEmpty() -> { + if (viewModel is CommentConversationViewModel) PATH_IMAGE_ARTICLE_DETAIL_COMMENT else PATH_IMAGE_ARTICLE_DETAIL + } + else -> "" } val mtaKey = if (viewModel is ArticleDetailViewModel) "全部评论" else "评论详情-全部回复" @@ -843,6 +868,7 @@ abstract class BaseCommentAdapter( deleteCallBack ) } + viewModel.videoId.isNotEmpty() -> { showVideoCommentOptions( view, @@ -852,6 +878,7 @@ abstract class BaseCommentAdapter( deleteCallBack ) } + viewModel.questionId.isNotEmpty() -> { showQuestionCommentOption( view, @@ -861,6 +888,16 @@ abstract class BaseCommentAdapter( deleteCallBack ) } + + viewModel.imageArticleId.isNotEmpty() -> { + showImageArticleOption( + view, + comment, + viewModel, + path, + deleteCallBack + ) + } } } @@ -872,10 +909,12 @@ abstract class BaseCommentAdapter( bbsId = viewModel.topItemData?.articleDetail?.community?.id ?: "" type = viewModel.topItemData?.articleDetail?.type ?: "" } + viewModel.questionId.isNotEmpty() -> { bbsId = viewModel.topItemData?.questionDetail?.community?.id ?: "" type = viewModel.topItemData?.questionDetail?.type ?: "" } + else -> { bbsId = (viewModel as? VideoCommentViewModel)?.videoDetail?.bbs?.id ?: "" type = (viewModel as? VideoCommentViewModel)?.videoDetail?.type ?: "" @@ -916,6 +955,7 @@ abstract class BaseCommentAdapter( }, extraConfig = DialogHelper.Config(centerTitle = true, centerContent = true) ) } + "采纳" -> { DialogHelper.showDialog( view.context, @@ -929,6 +969,7 @@ abstract class BaseCommentAdapter( extraConfig = DialogHelper.Config(centerContent = true, centerTitle = true) ) } + "取消采纳" -> { viewModel.acceptQuestionComment( viewModel.questionId, @@ -936,12 +977,15 @@ abstract class BaseCommentAdapter( false ) } + "加精选" -> { showHighlightQuestionCommentDialog(comment, view.context, viewModel, true) } + "取消精选" -> { showHighlightQuestionCommentDialog(comment, view.context, viewModel, false) } + "置顶" -> { DialogHelper.showDialog( view.context, @@ -955,6 +999,7 @@ abstract class BaseCommentAdapter( extraConfig = DialogHelper.Config(centerContent = true, centerTitle = true) ) } + "取消置顶" -> { DialogHelper.showDialog( view.context, @@ -1048,6 +1093,7 @@ abstract class BaseCommentAdapter( }, extraConfig = DialogHelper.Config(centerTitle = true, centerContent = true) ) } + "置顶" -> { DialogHelper.showDialog( view.context, @@ -1061,6 +1107,7 @@ abstract class BaseCommentAdapter( extraConfig = DialogHelper.Config(centerTitle = true, centerContent = true) ) } + "取消置顶" -> { DialogHelper.showDialog( view.context, @@ -1119,6 +1166,7 @@ abstract class BaseCommentAdapter( }, extraConfig = DialogHelper.Config(centerTitle = true, centerContent = true) ) } + "置顶" -> { DialogHelper.showDialog( view.context, @@ -1132,6 +1180,7 @@ abstract class BaseCommentAdapter( extraConfig = DialogHelper.Config(centerTitle = true, centerContent = true) ) } + "取消置顶" -> { DialogHelper.showDialog( view.context, @@ -1166,6 +1215,78 @@ abstract class BaseCommentAdapter( } } + private fun showImageArticleOption( + view: View, + comment: CommentEntity, + viewModel: BaseCommentViewModel, + path: String, + deleteCallBack: ((comment: CommentEntity) -> Unit)? + ) { + CommentHelper.showImageArticleCommentOptions( + view, + comment, + viewModel.imageArticleId, + isShowTop = path == PATH_IMAGE_ARTICLE_DETAIL, + listener = object : OnCommentOptionClickListener { + override fun onCommentOptionClick(entity: CommentEntity, option: String) { + when (option) { + "删除评论" -> { + DialogHelper.showDialog( + view.context, + "提示", + "删除评论后,评论下所有的回复都将被删除", + "删除", + "取消", + { + viewModel.deleteComment(comment) { + deleteCallBack?.invoke(comment) + } + }, extraConfig = DialogHelper.Config(centerTitle = true, centerContent = true) + ) + } + + "置顶" -> { + DialogHelper.showDialog( + view.context, + "提示", + "是否将此条评论置顶?", + "确认", + "取消", + confirmClickCallback = { + commentTop(view.context, viewModel, comment, false) + }, + extraConfig = DialogHelper.Config(centerTitle = true, centerContent = true) + ) + } + + "取消置顶" -> { + DialogHelper.showDialog( + view.context, + "提示", + "是否将此条评论取消置顶?", + "确认", + "取消", + confirmClickCallback = { + viewModel.updateCommentTop( + comment.id ?: "", + top = false, + isAgain = false + ) { isSuccess, _ -> + if (isSuccess) { + viewModel.onUpdateCommentTopSuccess() + } + } + }, + extraConfig = DialogHelper.Config(centerContent = true, centerTitle = true) + ) + } + } + + } + + }) + } + private fun commentTop( context: Context, viewModel: BaseCommentViewModel, @@ -1212,6 +1333,9 @@ abstract class BaseCommentAdapter( const val PATH_VIDEO_DETAIL = "视频详情" const val PATH_VIDEO_DETAIL_COMMENT = "视频评论详情" + + const val PATH_IMAGE_ARTICLE_DETAIL = "帖子详情" + const val PATH_IMAGE_ARTICLE_DETAIL_COMMENT = "帖子评论详情" } enum class AdapterType { diff --git a/app/src/main/java/com/gh/gamecenter/qa/comment/base/BaseCommentViewModel.kt b/app/src/main/java/com/gh/gamecenter/qa/comment/base/BaseCommentViewModel.kt index 4e86e91e27..314dc48736 100644 --- a/app/src/main/java/com/gh/gamecenter/qa/comment/base/BaseCommentViewModel.kt +++ b/app/src/main/java/com/gh/gamecenter/qa/comment/base/BaseCommentViewModel.kt @@ -2,10 +2,12 @@ package com.gh.gamecenter.qa.comment.base import android.annotation.SuppressLint import android.app.Application +import androidx.lifecycle.LiveData import androidx.lifecycle.MutableLiveData import com.gh.common.util.ErrorHelper import com.gh.common.util.NewLogUtils import com.gh.common.util.PostCommentUtils +import com.gh.gamecenter.BuildConfig import com.gh.gamecenter.common.baselist.ListViewModel import com.gh.gamecenter.common.baselist.LoadParams import com.gh.gamecenter.common.baselist.LoadStatus @@ -24,10 +26,12 @@ import com.gh.gamecenter.entity.CommentDraft import com.gh.gamecenter.feature.entity.CommentEntity import com.gh.gamecenter.feature.entity.MeEntity import com.gh.gamecenter.feature.entity.Permissions +import com.gh.gamecenter.livedata.Event import com.gh.gamecenter.qa.article.detail.CommentItemData import com.gh.gamecenter.retrofit.RetrofitManager import com.gh.gamecenter.retrofit.service.ApiService import com.gh.gamecenter.room.AppDatabase +import com.halo.assistant.HaloApp import com.lightgame.utils.Utils import io.reactivex.android.schedulers.AndroidSchedulers import io.reactivex.schedulers.Schedulers @@ -42,7 +46,8 @@ abstract class BaseCommentViewModel( var questionId: String, var communityId: String, var gameCollectionId: String, - var topCommentId: String = "" + var topCommentId: String = "", + val imageArticleId: String = "", ) : ListViewModel(application) { protected val mApi: ApiService = RetrofitManager.getInstance().api protected var mTotal: Int = 0 @@ -56,20 +61,29 @@ abstract class BaseCommentViewModel( var isHandleTopComment = false + private val _updateCommentCountAction = MutableLiveData>() + val updateCommentCountAction: LiveData> = _updateCommentCountAction + fun updateCommentCount(count: Int) { + _updateCommentCountAction.value = Event(count) + } + override fun loadStatusControl(size: Int) { if (mCurLoadParams.loadOffset == LoadParams.DEFAULT_OFFSET) { // 初始化列表 when { size == 0 -> { mLoadStatusLiveData.setValue(LoadStatus.INIT_EMPTY) } + size == REQUEST_FAILURE_SIZE -> { // TODO 处理列表加载失败问题 mLoadStatusLiveData.setValue(LoadStatus.INIT_LOADED) } + size < mOverLimitSize -> { // 避免一个屏幕出现两次分页 mLoadStatusLiveData.setValue(LoadStatus.INIT_OVER) } + else -> mLoadStatusLiveData.setValue(LoadStatus.INIT_LOADED) } } else { @@ -146,15 +160,28 @@ abstract class BaseCommentViewModel( articleId.isNotEmpty() -> { mApi.getCommunityArticleComment(commentId) } + videoId.isNotEmpty() -> { mApi.getCommunityVideoComment(commentId) } + questionId.isNotEmpty() -> { mApi.getCommunityQuestionComment(questionId, commentId) } + gameCollectionId.isNotEmpty() -> { mApi.getGameCollectionComment(gameCollectionId, commentId) } + + imageArticleId.isNotEmpty() -> { + mApi.getImageArticleCommentDetail( + commentId, + BuildConfig.VERSION_NAME, + HaloApp.getInstance().channel, + System.currentTimeMillis() + ) + } + else -> null } ?: return single.compose(singleToMain()) @@ -172,6 +199,7 @@ abstract class BaseCommentViewModel( load(LoadType.REFRESH) } } + SortType.OLDEST -> { //根据total判断是否插入到最后 if (count >= mTotal) { @@ -185,6 +213,7 @@ abstract class BaseCommentViewModel( } } } + override fun onFailure(exception: Exception) { super.onFailure(exception) load(LoadType.REFRESH) @@ -221,6 +250,7 @@ abstract class BaseCommentViewModel( articleId, videoId, questionId, + imageArticleId, comment.id, object : PostCommentUtils.PostCommentListener { override fun postSuccess(response: JSONObject?) { @@ -273,7 +303,7 @@ abstract class BaseCommentViewModel( } fun unLike(comment: CommentEntity) { - PostCommentUtils.unLikeComment(articleId, communityId, videoId, questionId, comment.id, + PostCommentUtils.unLikeComment(articleId, communityId, videoId, questionId, imageArticleId, comment.id, object : PostCommentUtils.PostCommentListener { override fun postSuccess(response: JSONObject?) { val cloneComment = comment.clone() @@ -319,12 +349,19 @@ abstract class BaseCommentViewModel( mApi.deleteVideoComment(entity.id).toObservable() } } + questionId.isNotEmpty() -> { mApi.deleteQuestionComment(questionId, entity.id).toObservable() } + articleId.isNotEmpty() -> { mApi.hideCommunityArticleComment(entity.id) } + + imageArticleId.isNotEmpty() -> { + mApi.deleteImageArticleComment(entity.id) + } + else -> null } ?: return observable.compose(observableToMain()) @@ -373,12 +410,19 @@ abstract class BaseCommentViewModel( articleId.isNotEmpty() -> { mApi.postArticleCommentTop(commentId, map) } + questionId.isNotEmpty() -> { mApi.postQuestionCommentTop(questionId, commentId, map) } + videoId.isNotEmpty() -> { mApi.postVideoCommentTop(commentId, map) } + + imageArticleId.isNotEmpty() -> { + mApi.postImageArticleTop(commentId, map) + } + else -> null } } else { @@ -386,12 +430,19 @@ abstract class BaseCommentViewModel( articleId.isNotEmpty() -> { mApi.postArticleCommentUnTop(commentId) } + questionId.isNotEmpty() -> { mApi.postQuestionCommentUnTop(questionId, commentId) } + videoId.isNotEmpty() -> { mApi.postVideoCommentUnTop(commentId) } + + imageArticleId.isNotEmpty() -> { + mApi.postImageArticleUnTop(commentId) + } + else -> null } } ?: return @@ -502,6 +553,7 @@ abstract class BaseCommentViewModel( when { commentNormal?.id == cloneComment.id -> it[index] = CommentItemData(commentNormal = cloneComment) + commentTop?.id == cloneComment.id -> it[index] = CommentItemData(commentTop = cloneComment) } } diff --git a/app/src/main/java/com/gh/gamecenter/qa/comment/conversation/CommentConversationFragment.kt b/app/src/main/java/com/gh/gamecenter/qa/comment/conversation/CommentConversationFragment.kt index a956f973aa..dcf2c24476 100644 --- a/app/src/main/java/com/gh/gamecenter/qa/comment/conversation/CommentConversationFragment.kt +++ b/app/src/main/java/com/gh/gamecenter/qa/comment/conversation/CommentConversationFragment.kt @@ -29,6 +29,7 @@ import com.gh.gamecenter.qa.comment.base.BaseCommentFragment import com.gh.gamecenter.qa.comment.base.BaseCommentViewModel import com.gh.gamecenter.qa.article.detail.CommentItemData import com.gh.gamecenter.qa.comment.CommentActivity +import com.gh.gamecenter.qa.comment.CommentActivity.Companion.IMAGE_ARTICLE_ID import com.halo.assistant.HaloApp class CommentConversationFragment : @@ -118,7 +119,8 @@ class CommentConversationFragment : // 取消掉 if (mUseAlternativeLayout) { toolbarContainer.root.visibility = View.GONE - container.updateLayoutParams { this as FrameLayout.LayoutParams + container.updateLayoutParams { + this as FrameLayout.LayoutParams setMargins(0, 0, 0, 0) } } @@ -192,6 +194,7 @@ class CommentConversationFragment : } }, 100) } + else -> { if (it == BaseCommentViewModel.LoadResult.DELETED) { mReuseNoConn?.visibility = View.GONE @@ -228,7 +231,8 @@ class CommentConversationFragment : arguments?.getString(EntranceConsts.KEY_COMMUNITY_ID) ?: "", arguments?.getString(CommentActivity.GAME_COLLECTION_ID) ?: "", arguments?.getString(EntranceConsts.KEY_COMMENT_ID) ?: "", - arguments?.getString(EntranceConsts.KEY_TOP_COMMENT_ID) ?: "" + arguments?.getString(EntranceConsts.KEY_TOP_COMMENT_ID) ?: "", + arguments?.getString(IMAGE_ARTICLE_ID) ?: "" ) ) } @@ -284,6 +288,15 @@ class CommentConversationFragment : comment ) startActivityForResult(intent, CommentActivity.REQUEST_CODE) + } else if (mViewModel.imageArticleId.isNotEmpty()) { + val intent = CommentActivity.getImageArticleCommentReplyIntent( + requireContext(), + mViewModel.imageArticleId, + arguments?.getString(EntranceConsts.KEY_COMMENT_ID) ?: "", + mViewModel.commentCount, + comment + ) + startActivityForResult(intent, CommentActivity.REQUEST_CODE) } } } \ No newline at end of file diff --git a/app/src/main/java/com/gh/gamecenter/qa/comment/conversation/CommentConversationViewModel.kt b/app/src/main/java/com/gh/gamecenter/qa/comment/conversation/CommentConversationViewModel.kt index 94d741e8dd..de9ac40e1c 100644 --- a/app/src/main/java/com/gh/gamecenter/qa/comment/conversation/CommentConversationViewModel.kt +++ b/app/src/main/java/com/gh/gamecenter/qa/comment/conversation/CommentConversationViewModel.kt @@ -4,10 +4,12 @@ import android.annotation.SuppressLint import android.app.Application import androidx.lifecycle.ViewModel import androidx.lifecycle.ViewModelProvider +import com.gh.gamecenter.BuildConfig import com.gh.gamecenter.common.retrofit.BiResponse import com.gh.gamecenter.feature.entity.CommentEntity import com.gh.gamecenter.qa.article.detail.CommentItemData import com.gh.gamecenter.qa.comment.base.BaseCommentViewModel +import com.halo.assistant.HaloApp import io.reactivex.Observable import io.reactivex.Single import io.reactivex.schedulers.Schedulers @@ -21,8 +23,9 @@ class CommentConversationViewModel( communityId: String = "", gameCollectionId: String = "", var commentId: String = "", - topCommentId: String = "" -) : BaseCommentViewModel(application, articleId, videoId, questionId, communityId, topCommentId) { + topCommentId: String = "", + imageArticleId: String = "" +) : BaseCommentViewModel(application, articleId, videoId, questionId, communityId,gameCollectionId, topCommentId, imageArticleId) { var commentDetail: CommentEntity? = null var positionInOriginList = -1 @@ -42,15 +45,30 @@ class CommentConversationViewModel( map ) } + videoId.isNotEmpty() -> { mApi.getVideoCommentReply(videoId, commentId, currentSortType.value, page, map) } + questionId.isNotEmpty() -> { mApi.getQuestionCommentReply(questionId, commentId, currentSortType.value, page, map) } + gameCollectionId.isNotEmpty() -> { mApi.getGameCollectionCommentReply(gameCollectionId, commentId, page, map) } + + imageArticleId.isNotEmpty() -> { + mApi.getImageArticleDetailCommentReply( + commentId, + currentSortType.value, + page, + BuildConfig.VERSION_NAME, + HaloApp.getInstance().channel, + System.currentTimeMillis() + ) + } + else -> null } } @@ -61,15 +79,28 @@ class CommentConversationViewModel( articleId.isNotEmpty() -> { mApi.getCommunityArticleComment(commentId) } + videoId.isNotEmpty() -> { mApi.getCommunityVideoComment(commentId) } + questionId.isNotEmpty() -> { mApi.getCommunityQuestionComment(questionId, commentId) } + gameCollectionId.isNotEmpty() -> { mApi.getGameCollectionComment(gameCollectionId, commentId) } + + imageArticleId.isNotEmpty() -> { + mApi.getImageArticleCommentDetail( + commentId, + BuildConfig.VERSION_NAME, + HaloApp.getInstance().channel, + System.currentTimeMillis() + ) + } + else -> null } ?: return single.subscribeOn(Schedulers.io()) @@ -113,6 +144,7 @@ class CommentConversationViewModel( when { commentNormal?.id == cloneComment.id -> it[index] = CommentItemData(commentNormal = cloneComment) + commentTop?.id == cloneComment.id -> { commentDetail = cloneComment it[index] = CommentItemData(commentTop = cloneComment) @@ -132,7 +164,8 @@ class CommentConversationViewModel( private val communityId: String = "", private val gameCollectionId: String = "", private val commentId: String, - private val topCommentId: String + private val topCommentId: String, + private val imageArticleId: String, ) : ViewModelProvider.NewInstanceFactory() { override fun create(modelClass: Class): T { @@ -144,7 +177,8 @@ class CommentConversationViewModel( communityId = communityId, gameCollectionId = gameCollectionId, commentId = commentId, - topCommentId = topCommentId + topCommentId = topCommentId, + imageArticleId = imageArticleId ) as T } } diff --git a/app/src/main/java/com/gh/gamecenter/qa/dialog/ChooseForumActivity.kt b/app/src/main/java/com/gh/gamecenter/qa/dialog/ChooseForumActivity.kt index b53f8749fd..40c1dccc19 100644 --- a/app/src/main/java/com/gh/gamecenter/qa/dialog/ChooseForumActivity.kt +++ b/app/src/main/java/com/gh/gamecenter/qa/dialog/ChooseForumActivity.kt @@ -2,12 +2,15 @@ package com.gh.gamecenter.qa.dialog import android.annotation.SuppressLint import android.app.Activity +import android.content.Context import android.content.Intent import android.content.pm.ActivityInfo import android.os.Build import android.os.Bundle import android.view.MotionEvent import android.view.View +import android.view.inputmethod.EditorInfo +import android.view.inputmethod.InputMethodManager import androidx.core.widget.doOnTextChanged import androidx.fragment.app.Fragment import com.gh.gamecenter.common.base.activity.BaseActivity @@ -45,8 +48,8 @@ class ChooseForumActivity : BaseActivity() { val sourceEntrance = intent.getStringExtra(EntranceConsts.KEY_SOURCE_ENTRANCE) ?: "" initViewPager() - binding.searchEt.doOnTextChanged { _, _, _, _ -> - val searchKey = binding.searchEt.text?.toString() + binding.searchEt.doOnTextChanged { text, _, _, _ -> + val searchKey = text?.toString() if (searchKey.isNullOrEmpty()) { switchUI(false) } else { @@ -61,6 +64,25 @@ class ChooseForumActivity : BaseActivity() { } false } + + binding.searchEt.setOnEditorActionListener { v, actionId, event -> + if (actionId == EditorInfo.IME_ACTION_SEARCH) { + // 开始搜索 + mSearchResultFragment?.setSearchKeyAndType( + binding.searchEt.text?.toString() ?: "", + SearchType.MANUAL.value + ) + // 清除焦点以保持搜索按钮 + binding.searchEt.clearFocus() + + // 隐藏软键盘 + val imm = getSystemService(Context.INPUT_METHOD_SERVICE) as InputMethodManager + imm.hideSoftInputFromWindow(binding.searchEt.windowToken, 0) + return@setOnEditorActionListener true + } else { + return@setOnEditorActionListener false + } + } binding.closeIv.setOnClickListener { NewLogUtils.logChooseForumPanelClick("click_select_forum_panel_close", "", "", "") finish() diff --git a/app/src/main/java/com/gh/gamecenter/qa/draft/CommunityDraftWrapperActivity.kt b/app/src/main/java/com/gh/gamecenter/qa/draft/CommunityDraftWrapperActivity.kt index abc36a54f6..6d06cf36dd 100644 --- a/app/src/main/java/com/gh/gamecenter/qa/draft/CommunityDraftWrapperActivity.kt +++ b/app/src/main/java/com/gh/gamecenter/qa/draft/CommunityDraftWrapperActivity.kt @@ -5,7 +5,10 @@ import android.content.Intent import android.os.Bundle import androidx.fragment.app.Fragment import com.gh.gamecenter.common.base.activity.BaseActivity_TabLayout +import com.gh.gamecenter.R +import com.gh.gamecenter.common.utils.toResString import com.gh.gamecenter.common.utils.updateStatusBarColor +import com.gh.gamecenter.forum.home.recommend.fragment.ImageArticleDraftFragment import com.gh.gamecenter.qa.article.draft.ArticleDraftFragment import com.gh.gamecenter.qa.questions.draft.QuestionDraftFragment import com.gh.gamecenter.video.videomanager.VideoDraftFragment @@ -22,15 +25,17 @@ class CommunityDraftWrapperActivity : BaseActivity_TabLayout() { } override fun initFragmentList(fragments: MutableList) { + fragments.add(ImageArticleDraftFragment()) fragments.add(ArticleDraftFragment()) fragments.add(QuestionDraftFragment()) fragments.add(VideoDraftFragment()) } override fun initTabTitleList(tabTitleList: MutableList) { - tabTitleList.add("帖子草稿") - tabTitleList.add("问题草稿") - tabTitleList.add("视频草稿") + tabTitleList.add(R.string.image_article.toResString()) + tabTitleList.add("帖子") + tabTitleList.add("问题") + tabTitleList.add("视频") } override fun isAutoResetViewBackgroundEnabled(): Boolean = true diff --git a/app/src/main/java/com/gh/gamecenter/qa/questions/newdetail/NewQuestionDetailFragment.kt b/app/src/main/java/com/gh/gamecenter/qa/questions/newdetail/NewQuestionDetailFragment.kt index 5113117fee..b1345525d8 100644 --- a/app/src/main/java/com/gh/gamecenter/qa/questions/newdetail/NewQuestionDetailFragment.kt +++ b/app/src/main/java/com/gh/gamecenter/qa/questions/newdetail/NewQuestionDetailFragment.kt @@ -324,6 +324,7 @@ class NewQuestionDetailFragment : mBinding.root.setBackgroundColor(Color.TRANSPARENT) updateView() } + else -> { if (it == BaseCommentViewModel.LoadResult.DELETED) { mReuseNoConn?.visibility = View.GONE @@ -430,7 +431,7 @@ class NewQuestionDetailFragment : mAdapter?.questionDetailVH?.bindView(it) } mViewModel.top.observeNonNull(this) { top -> - val topSuccessToast = if(top) { + val topSuccessToast = if (top) { R.string.article_detail_top_success_toast } else { R.string.article_detail_cancel_top_success_toast @@ -626,12 +627,24 @@ class NewQuestionDetailFragment : // 置顶/取消置顶 if (questionEntity.me.isModerator && !questionEntity.me.isCommunityTop - && moderatorPermissions.topQuestion > Permissions.GUEST) { - entities.add(MenuItemEntity(getString(R.string.article_detail_more_top_title), R.drawable.icon_more_panel_top)) + && moderatorPermissions.topQuestion > Permissions.GUEST + ) { + entities.add( + MenuItemEntity( + getString(R.string.article_detail_more_top_title), + R.drawable.icon_more_panel_top + ) + ) } else if (questionEntity.me.isModerator && questionEntity.me.isCommunityTop - && moderatorPermissions.cancelTopQuestion > Permissions.GUEST) { - entities.add(MenuItemEntity(getString(R.string.article_detail_more_cancel_top_title), R.drawable.icon_more_panel_top_cancel)) + && moderatorPermissions.cancelTopQuestion > Permissions.GUEST + ) { + entities.add( + MenuItemEntity( + getString(R.string.article_detail_more_cancel_top_title), + R.drawable.icon_more_panel_top_cancel + ) + ) } MoreFunctionPanelDialog.showMoreDialog( @@ -682,8 +695,7 @@ class NewQuestionDetailFragment : "投诉" -> { ifLogin("提问贴") { BbsReportHelper.showReportDialog( - mViewModel.questionDetail?.id - ?: "" + BbsReportHelper.PostsReporter(mViewModel.questionDetail?.id ?: "") ) } NewLogUtils.logSharePanelClick( @@ -695,6 +707,7 @@ class NewQuestionDetailFragment : mViewModel.questionDetail?.community?.typeChinese ?: "综合论坛" ) } + "编辑" -> { val intent = if (questionEntity.me.isModerator) { QuestionEditActivity.getManagerIntent( @@ -737,6 +750,7 @@ class NewQuestionDetailFragment : mViewModel.questionDetail?.community?.typeChinese ?: "综合论坛" ) } + "解决", "已解决" -> { val content = if (!questionEntity.finish) "该问题确定标记已解决?" else "该问题确定标记未解决?" @@ -752,6 +766,7 @@ class NewQuestionDetailFragment : mViewModel.questionDetail?.community?.typeChinese ?: "综合论坛" ) } + getString(R.string.article_detail_more_top_title) -> { TopCommunityCategoryDialog.show( childFragmentManager @@ -759,6 +774,7 @@ class NewQuestionDetailFragment : mViewModel.topCommunityQuestion(category.id) } } + getString(R.string.article_detail_more_cancel_top_title) -> { DialogHelper.showDialog( requireContext(), diff --git a/app/src/main/java/com/gh/gamecenter/qa/video/detail/ForumVideoDetailFragment.kt b/app/src/main/java/com/gh/gamecenter/qa/video/detail/ForumVideoDetailFragment.kt index 9d1fcd49e2..57d3634957 100644 --- a/app/src/main/java/com/gh/gamecenter/qa/video/detail/ForumVideoDetailFragment.kt +++ b/app/src/main/java/com/gh/gamecenter/qa/video/detail/ForumVideoDetailFragment.kt @@ -171,7 +171,8 @@ class ForumVideoDetailFragment : BaseLazyTabFragment() { mVideoId = arguments?.getString(EntranceConsts.KEY_VIDEO_ID) ?: "" mBbsId = arguments?.getString(EntranceConsts.KEY_BBS_ID) ?: "" mTopCommentId = arguments?.getString(EntranceConsts.KEY_TOP_COMMENT_ID) ?: "" - mBasicExposureSourceList = arguments?.getParcelableArrayList(EntranceConsts.KEY_EXPOSURE_SOURCE_LIST) ?: arrayListOf() + mBasicExposureSourceList = + arguments?.getParcelableArrayList(EntranceConsts.KEY_EXPOSURE_SOURCE_LIST) ?: arrayListOf() sourceEntrance = arguments?.getString(EntranceConsts.KEY_SOURCE_ENTRANCE) ?: "" super.onCreate(savedInstanceState) NewLogUtils.logVideoDetailClick("view_video_detail") @@ -189,7 +190,8 @@ class ForumVideoDetailFragment : BaseLazyTabFragment() { } } if (mIsFromMainWrapper) { - (mBinding.toolbar.layoutParams as ViewGroup.MarginLayoutParams).topMargin = DisplayUtils.getStatusBarHeight(requireContext().resources) + (mBinding.toolbar.layoutParams as ViewGroup.MarginLayoutParams).topMargin = + DisplayUtils.getStatusBarHeight(requireContext().resources) } mViewModel = viewModelProviderFromParent( ForumVideoDetailViewModel.Factory( @@ -704,7 +706,7 @@ class ForumVideoDetailFragment : BaseLazyTabFragment() { } "投诉" -> { - BbsReportHelper.showReportDialog(mForumVideoEntity?.id ?: "") + BbsReportHelper.showReportDialog(BbsReportHelper.PostsReporter(mForumVideoEntity?.id ?: "")) NewLogUtils.logSharePanelClick( "click_report", mForumVideoEntity?.user?.id ?: "", @@ -746,9 +748,11 @@ class ForumVideoDetailFragment : BaseLazyTabFragment() { ) } } + "取消精选" -> { showHighlightDialog(false) } + "修改活动标签" -> { ChooseActivityDialogFragment.show( requireActivity() as AppCompatActivity, @@ -758,6 +762,7 @@ class ForumVideoDetailFragment : BaseLazyTabFragment() { tag ?: "" ) } + "删除", "隐藏" -> { DialogHelper.showDialog( requireContext(), @@ -770,7 +775,7 @@ class ForumVideoDetailFragment : BaseLazyTabFragment() { ) NewLogUtils.logSharePanelClick( "click_delete", - mForumVideoEntity?.user?.id ?: "", + mForumVideoEntity?.user?.id ?: "", "视频帖", mForumVideoEntity?.id ?: "", mForumVideoEntity?.bbs?.id ?: "", @@ -785,6 +790,7 @@ class ForumVideoDetailFragment : BaseLazyTabFragment() { mViewModel.topCommunityVideo(category.id) } } + getString(R.string.article_detail_more_cancel_top_title) -> { DialogHelper.showDialog( requireContext(), diff --git a/app/src/main/java/com/gh/gamecenter/retrofit/service/ApiService.java b/app/src/main/java/com/gh/gamecenter/retrofit/service/ApiService.java index 4fee92cc4c..bb2d360b3f 100644 --- a/app/src/main/java/com/gh/gamecenter/retrofit/service/ApiService.java +++ b/app/src/main/java/com/gh/gamecenter/retrofit/service/ApiService.java @@ -55,6 +55,7 @@ import com.gh.gamecenter.entity.GamesCollectionEntity; import com.gh.gamecenter.entity.HaloAddonEntity; import com.gh.gamecenter.entity.HomeGameCollectionEntity; import com.gh.gamecenter.entity.HomeItemTestV2Entity; +import com.gh.gamecenter.entity.ImageArticleEntity; import com.gh.gamecenter.entity.ImageInfoEntity; import com.gh.gamecenter.entity.InterestedGameEntity; import com.gh.gamecenter.entity.LibaoDetailEntity; @@ -66,6 +67,7 @@ import com.gh.gamecenter.entity.NewsDetailEntity; import com.gh.gamecenter.entity.PackageFilter; import com.gh.gamecenter.entity.PackageGame; import com.gh.gamecenter.entity.PersonalHistoryEntity; +import com.gh.gamecenter.entity.PublishImageTextRequest; import com.gh.gamecenter.entity.PullDownPush; import com.gh.gamecenter.entity.Rating; import com.gh.gamecenter.entity.RatingComment; @@ -154,6 +156,7 @@ import java.util.LinkedHashMap; import java.util.List; import java.util.Map; +import io.reactivex.Completable; import io.reactivex.Observable; import io.reactivex.Single; import okhttp3.RequestBody; @@ -3425,4 +3428,195 @@ public interface ApiService { @GET("game_lists/search") Single> searchGameList(@Query("keyword") String keyword, @Query("page") int page); + + /** + * 发布图文 + */ + @POST("communities/image_articles") + Single publishImageText(@Body PublishImageTextRequest request); + + /** + * 编辑图文 + */ + @PUT("communities/image_articles/{id}") + Completable editImageArticle(@Path("id") String id, @Body PublishImageTextRequest request); + + + /** + * 新建草稿 + */ + @POST("users/{user_id}/communities/image_article_drafts") + Single createImageArticleDrafts(@Path("user_id") String userId, @Body PublishImageTextRequest request); + + /** + * 编辑草稿 + */ + @PUT("users/{user_id}/communities/image_article_drafts/{id}") + Completable editImageArticleDrafts(@Path("user_id") String userId, @Path("id") String draftId, @Body PublishImageTextRequest request); + + /** + * 草稿箱 + */ + @GET("users/{user_id}/communities/image_article_drafts") + Single> loadImageTextDrafts(@Path("user_id") String userId, @Query("version") String version, @Query("channel") String channel); + + /** + * 删除草稿 + */ + @DELETE("users/{user_id}/communities/image_article_drafts/{id}") + Completable deleteImageArticleDraft(@Path("user_id") String userId, @Path("id") String id); + + /** + * 图文详情 + */ + @GET("communities/image_articles/{id}") + Single loadImageArticleDetail( + @Path("id") String imageArticleId, + @Query("view") String view, + @Query("version") String version, + @Query("channel") String channel); + + /** + * 推荐-图文列表 + */ + @GET("bbses/image_article_recommends") + Single> loadImageArticleRecommends( + @Query("sort") String sort, + @Query("page") int page, + @Query("version") String version, + @Query("channel") String channel); + + /** + * 评论图文 + */ + @POST("communities/image_articles/{article_id}/comments") + Observable postImageArticleComment(@Path("article_id") String articleId, @Body RequestBody body); + + /** + * 回复图文评论 + */ + @POST("communities/image_articles/comments/{comment_id}:reply") + Observable postReplyToImageArticleComment(@Path("comment_id") String commentId, @Body RequestBody body); + + /** + * 图文详情评论列表 + */ + @GET("communities/image_articles/{article_id}/comments") + Observable> getImageArticleComments(@Path("article_id") String articleId, + @Query("sort") String type, + @Query("page") int page, + @QueryMap Map params); + + /** + * 图文评论详情 + */ + @GET("communities/image_articles/comments/{comment_id}") + Single getImageArticleCommentDetail( + @Path("comment_id") String commentId, + @Query("version") String version, + @Query("channel") String channel, + @Query("timestamp") long timeStamp); + + /** + * 图文详情回复列表 + */ + @GET("communities/image_articles/comments/{comment_id}/replies") + Single> getImageArticleDetailCommentReply( + @Path("comment_id") String commentId, + @Query("sort") String sort, + @Query("page") int page, + @Query("version") String version, + @Query("channel") String channel, + @Query("timestamp") long timestamp); + + /** + * 投诉图文评论 + */ + @POST("communities/image_articles/comments/{comment_id}:report") + Observable reportImageArticleComment(@Path("comment_id") String commentId, @Body RequestBody body); + + /** + * 删除图文评论 + */ + @POST("communities/image_articles/comments/{comment_id}:hide") + Observable deleteImageArticleComment(@Path("comment_id") String commendId); + + /** + * 收藏图文 + */ + @POST("users/favorites/communities/image_articles/{id}") + Single collectImageArticle(@Path("id") String id); + + /** + * 取消收藏图文 + */ + @DELETE("users/favorites/communities/image_articles/{id}") + Completable cancelImageArticleCollection(@Path("id") String id); + + /** + * 点赞图文 + */ + @POST("communities/image_articles/{id}:vote") + Single voteArticleImage(@Path("id") String id); + + /** + * 取消点赞图文 + */ + @POST("communities/image_articles/{id}:unvote") + Single unVoteArticleImage(@Path("id") String id); + + /** + * 置顶图文 + */ + @POST("communities/image_articles/comments/{comment_id}:set-top") + Observable postImageArticleTop(@Path("comment_id") String commentId, @QueryMap Map params); + + /** + * 取消置顶图文 + */ + @POST("communities/image_articles/comments/{comment_id}:unset-top") + Observable postImageArticleUnTop(@Path("comment_id") String commentId); + + /** + * 点赞图文 + */ + @POST("communities/image_articles/comments/{comment_id}:vote") + Observable postVoteToImageArticle(@Path("comment_id") String commentId); + + /** + * 取消点赞图文 + */ + @POST("communities/image_articles/comments/{comment_id}:unvote") + Observable postUnVoteImageArticle(@Path("comment_id") String commentId); + + /** + * 图文申请加精 + */ + @POST("communities/image_articles/{id}:choiceness") + Completable applyHighlightForImageArticle(@Path("id") String id); + + /** + * 图文加精选 + */ + @POST("communities/image_articles/{id}:moderator_choiceness") + Single addHighlightForImageArticle(@Path("id") String imageArticleId); + + /** + * 图文取消加精 + */ + @POST("communities/image_articles/{id}:cancel_choiceness") + Single cancelHighlightForImageArticle(@Path("id") String imageArticleId); + + /** + * 删除/隐藏图文 + */ + @POST("communities/image_articles/{id}:hide") + Single deleteOrHideImageArticle(@Path("id") String imageArticleId); + + /** + * 投诉图文 + */ + @POST("bbses/contents/{content_id}:report") + Single postImageArticleReport(@Path("content_id") String contentId, @Body RequestBody body); + } \ No newline at end of file diff --git a/app/src/main/java/com/gh/gamecenter/video/detail/VideoDetailContainerFragment.kt b/app/src/main/java/com/gh/gamecenter/video/detail/VideoDetailContainerFragment.kt index ffe12e0ef1..8f1ac12243 100644 --- a/app/src/main/java/com/gh/gamecenter/video/detail/VideoDetailContainerFragment.kt +++ b/app/src/main/java/com/gh/gamecenter/video/detail/VideoDetailContainerFragment.kt @@ -94,7 +94,8 @@ class VideoDetailContainerFragment : BaseLazyFragment(), OnBackPressedListener { override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) - mBasicExposureSourceList = arguments?.getParcelableArrayList(EntranceConsts.KEY_EXPOSURE_SOURCE_LIST) ?: arrayListOf() + mBasicExposureSourceList = + arguments?.getParcelableArrayList(EntranceConsts.KEY_EXPOSURE_SOURCE_LIST) ?: arrayListOf() mInitialVideoId = arguments?.getString(EntranceConsts.KEY_ID) ?: "" mLocation = arguments?.getString(EntranceConsts.KEY_LOCATION) ?: "" mReferer = arguments?.getString(EntranceConsts.KEY_REFERER) ?: "" @@ -154,7 +155,8 @@ class VideoDetailContainerFragment : BaseLazyFragment(), OnBackPressedListener { mBinding.recyclerview.visibility = View.VISIBLE mBinding.attentionNoDataContainer.visibility = View.GONE if (!::mAdapter.isInitialized) { - mAdapter = VideoAdapter(this, mBinding.recyclerview, mBinding.refresh, mViewModel, mBasicExposureSourceList) + mAdapter = + VideoAdapter(this, mBinding.recyclerview, mBinding.refresh, mViewModel, mBasicExposureSourceList) mExposureListener = ExposureListener(this, mAdapter) mBinding.recyclerview.addOnScrollListener(mExposureListener!!) mBinding.recyclerview.adapter = mAdapter @@ -574,6 +576,7 @@ class VideoDetailContainerFragment : BaseLazyFragment(), OnBackPressedListener { CheckLoginUtils.checkLogin(context, "(视频详情)") {} } } + "取消收藏" -> { if (UserManager.getInstance().isLoggedIn) { MtaHelper.onEvent("视频详情", "更多-取消收藏", combinedTitleAndId) @@ -587,9 +590,10 @@ class VideoDetailContainerFragment : BaseLazyFragment(), OnBackPressedListener { CheckLoginUtils.checkLogin(context, "(视频详情)") {} } } + "投诉" -> { ifLogin("视频详情") { - BbsReportHelper.showReportDialog(videoEntity.id) + BbsReportHelper.showReportDialog(BbsReportHelper.PostsReporter(videoEntity.id)) } } } diff --git a/app/src/main/res/drawable-xxxhdpi/community_edit_recommend.webp b/app/src/main/res/drawable-xxxhdpi/community_edit_recommend.webp new file mode 100644 index 0000000000..f79227aa15 Binary files /dev/null and b/app/src/main/res/drawable-xxxhdpi/community_edit_recommend.webp differ diff --git a/app/src/main/res/drawable/bg_image_article_detail_indicator.xml b/app/src/main/res/drawable/bg_image_article_detail_indicator.xml new file mode 100644 index 0000000000..7855fb3622 --- /dev/null +++ b/app/src/main/res/drawable/bg_image_article_detail_indicator.xml @@ -0,0 +1,5 @@ + + + + + \ No newline at end of file diff --git a/app/src/main/res/drawable/btn_fab_container__1__1.xml b/app/src/main/res/drawable/btn_fab_container__1__1.xml new file mode 100644 index 0000000000..1e704d174a --- /dev/null +++ b/app/src/main/res/drawable/btn_fab_container__1__1.xml @@ -0,0 +1,70 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/app/src/main/res/drawable/ic_community_float.xml b/app/src/main/res/drawable/ic_community_float.xml new file mode 100644 index 0000000000..567ad0e612 --- /dev/null +++ b/app/src/main/res/drawable/ic_community_float.xml @@ -0,0 +1,66 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/app/src/main/res/drawable/ic_image_and_text_add.xml b/app/src/main/res/drawable/ic_image_and_text_add.xml new file mode 100644 index 0000000000..b73d876b58 --- /dev/null +++ b/app/src/main/res/drawable/ic_image_and_text_add.xml @@ -0,0 +1,13 @@ + + + + diff --git a/app/src/main/res/drawable/ic_image_and_text_arr_right.xml b/app/src/main/res/drawable/ic_image_and_text_arr_right.xml new file mode 100644 index 0000000000..143ccf260e --- /dev/null +++ b/app/src/main/res/drawable/ic_image_and_text_arr_right.xml @@ -0,0 +1,13 @@ + + + diff --git a/app/src/main/res/drawable/ic_image_and_text_bbs.xml b/app/src/main/res/drawable/ic_image_and_text_bbs.xml new file mode 100644 index 0000000000..a606231d72 --- /dev/null +++ b/app/src/main/res/drawable/ic_image_and_text_bbs.xml @@ -0,0 +1,33 @@ + + + + + + + + + + + + diff --git a/app/src/main/res/drawable/ic_image_and_text_clear.xml b/app/src/main/res/drawable/ic_image_and_text_clear.xml new file mode 100644 index 0000000000..f8d12adbfe --- /dev/null +++ b/app/src/main/res/drawable/ic_image_and_text_clear.xml @@ -0,0 +1,16 @@ + + + + diff --git a/app/src/main/res/drawable/ic_image_and_text_drag_tips.xml b/app/src/main/res/drawable/ic_image_and_text_drag_tips.xml new file mode 100644 index 0000000000..5384506125 --- /dev/null +++ b/app/src/main/res/drawable/ic_image_and_text_drag_tips.xml @@ -0,0 +1,25 @@ + + + + + + + + + diff --git a/app/src/main/res/drawable/ic_image_and_text_right.xml b/app/src/main/res/drawable/ic_image_and_text_right.xml new file mode 100644 index 0000000000..03cc75d79c --- /dev/null +++ b/app/src/main/res/drawable/ic_image_and_text_right.xml @@ -0,0 +1,13 @@ + + + diff --git a/app/src/main/res/drawable/ic_image_article_detail_go_bbs.xml b/app/src/main/res/drawable/ic_image_article_detail_go_bbs.xml new file mode 100644 index 0000000000..6d452a33f4 --- /dev/null +++ b/app/src/main/res/drawable/ic_image_article_detail_go_bbs.xml @@ -0,0 +1,13 @@ + + + diff --git a/app/src/main/res/drawable/ic_image_article_draft.xml b/app/src/main/res/drawable/ic_image_article_draft.xml new file mode 100644 index 0000000000..2c363cee5a --- /dev/null +++ b/app/src/main/res/drawable/ic_image_article_draft.xml @@ -0,0 +1,14 @@ + + + + + + diff --git a/app/src/main/res/drawable/ic_image_article_liked.xml b/app/src/main/res/drawable/ic_image_article_liked.xml new file mode 100644 index 0000000000..d0fdd03374 --- /dev/null +++ b/app/src/main/res/drawable/ic_image_article_liked.xml @@ -0,0 +1,16 @@ + + + + diff --git a/app/src/main/res/drawable/ic_image_article_unlike.xml b/app/src/main/res/drawable/ic_image_article_unlike.xml new file mode 100644 index 0000000000..f5a4868ec3 --- /dev/null +++ b/app/src/main/res/drawable/ic_image_article_unlike.xml @@ -0,0 +1,17 @@ + + + + diff --git a/app/src/main/res/drawable/selector_recommend_home_vote.xml b/app/src/main/res/drawable/selector_recommend_home_vote.xml new file mode 100644 index 0000000000..8ab2c6acff --- /dev/null +++ b/app/src/main/res/drawable/selector_recommend_home_vote.xml @@ -0,0 +1,5 @@ + + + + + \ No newline at end of file diff --git a/app/src/main/res/drawable/shape_image_and_text_cover.xml b/app/src/main/res/drawable/shape_image_and_text_cover.xml new file mode 100644 index 0000000000..8edc204102 --- /dev/null +++ b/app/src/main/res/drawable/shape_image_and_text_cover.xml @@ -0,0 +1,5 @@ + + + + + \ No newline at end of file diff --git a/app/src/main/res/drawable/shape_skeleton_circle.xml b/app/src/main/res/drawable/shape_skeleton_circle.xml new file mode 100644 index 0000000000..dc8c0eddac --- /dev/null +++ b/app/src/main/res/drawable/shape_skeleton_circle.xml @@ -0,0 +1,5 @@ + + + + + \ No newline at end of file diff --git a/app/src/main/res/drawable/shape_skeleton_radius_4.xml b/app/src/main/res/drawable/shape_skeleton_radius_4.xml new file mode 100644 index 0000000000..3f0c7b07ba --- /dev/null +++ b/app/src/main/res/drawable/shape_skeleton_radius_4.xml @@ -0,0 +1,5 @@ + + + + + \ No newline at end of file diff --git a/app/src/main/res/drawable/shape_skeleton_radius_8_only_top.xml b/app/src/main/res/drawable/shape_skeleton_radius_8_only_top.xml new file mode 100644 index 0000000000..95dbfa730a --- /dev/null +++ b/app/src/main/res/drawable/shape_skeleton_radius_8_only_top.xml @@ -0,0 +1,7 @@ + + + + + \ No newline at end of file diff --git a/app/src/main/res/layout/activity_publish_image_article.xml b/app/src/main/res/layout/activity_publish_image_article.xml new file mode 100644 index 0000000000..1cbd5ebe0f --- /dev/null +++ b/app/src/main/res/layout/activity_publish_image_article.xml @@ -0,0 +1,43 @@ + + + + + + + + + + + + + + \ No newline at end of file diff --git a/app/src/main/res/layout/community_edit_window.xml b/app/src/main/res/layout/community_edit_window.xml index d8d7611277..4ee7b1ade5 100644 --- a/app/src/main/res/layout/community_edit_window.xml +++ b/app/src/main/res/layout/community_edit_window.xml @@ -60,6 +60,34 @@ + + + + + + + + \ No newline at end of file diff --git a/app/src/main/res/layout/fragment_forum_detail.xml b/app/src/main/res/layout/fragment_forum_detail.xml index 200785d435..5bbcf2e0fb 100644 --- a/app/src/main/res/layout/fragment_forum_detail.xml +++ b/app/src/main/res/layout/fragment_forum_detail.xml @@ -432,17 +432,13 @@ diff --git a/app/src/main/res/layout/fragment_image_article_detail.xml b/app/src/main/res/layout/fragment_image_article_detail.xml new file mode 100644 index 0000000000..73a1b014f4 --- /dev/null +++ b/app/src/main/res/layout/fragment_image_article_detail.xml @@ -0,0 +1,524 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/app/src/main/res/layout/fragment_image_article_draft.xml b/app/src/main/res/layout/fragment_image_article_draft.xml new file mode 100644 index 0000000000..9de91345f9 --- /dev/null +++ b/app/src/main/res/layout/fragment_image_article_draft.xml @@ -0,0 +1,51 @@ + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/app/src/main/res/layout/fragment_image_article_home.xml b/app/src/main/res/layout/fragment_image_article_home.xml new file mode 100644 index 0000000000..500d8a8d63 --- /dev/null +++ b/app/src/main/res/layout/fragment_image_article_home.xml @@ -0,0 +1,37 @@ + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/app/src/main/res/layout/fragment_image_article_home_skeleton.xml b/app/src/main/res/layout/fragment_image_article_home_skeleton.xml new file mode 100644 index 0000000000..5b8ad76c05 --- /dev/null +++ b/app/src/main/res/layout/fragment_image_article_home_skeleton.xml @@ -0,0 +1,68 @@ + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/app/src/main/res/layout/fragment_image_article_request_permission.xml b/app/src/main/res/layout/fragment_image_article_request_permission.xml new file mode 100644 index 0000000000..f225944eeb --- /dev/null +++ b/app/src/main/res/layout/fragment_image_article_request_permission.xml @@ -0,0 +1,64 @@ + + + + + + + + + + + diff --git a/app/src/main/res/layout/fragment_publish_image_article.xml b/app/src/main/res/layout/fragment_publish_image_article.xml new file mode 100644 index 0000000000..31191dec58 --- /dev/null +++ b/app/src/main/res/layout/fragment_publish_image_article.xml @@ -0,0 +1,277 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/app/src/main/res/layout/fragment_user_publish.xml b/app/src/main/res/layout/fragment_user_publish.xml index b57f4b88de..4590116837 100644 --- a/app/src/main/res/layout/fragment_user_publish.xml +++ b/app/src/main/res/layout/fragment_user_publish.xml @@ -53,6 +53,13 @@ android:text="全部" android:textColor="@color/text_theme" /> + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/app/src/main/res/layout/layout_skeleton_recommend_home_2.xml b/app/src/main/res/layout/layout_skeleton_recommend_home_2.xml new file mode 100644 index 0000000000..3412b3374f --- /dev/null +++ b/app/src/main/res/layout/layout_skeleton_recommend_home_2.xml @@ -0,0 +1,58 @@ + + + + + + + + + + + + + \ No newline at end of file diff --git a/app/src/main/res/layout/layout_skeleton_recommend_home_3.xml b/app/src/main/res/layout/layout_skeleton_recommend_home_3.xml new file mode 100644 index 0000000000..c467c66ee0 --- /dev/null +++ b/app/src/main/res/layout/layout_skeleton_recommend_home_3.xml @@ -0,0 +1,47 @@ + + + + + + + + + + + \ No newline at end of file diff --git a/app/src/main/res/layout/layout_vote.xml b/app/src/main/res/layout/layout_vote.xml new file mode 100644 index 0000000000..343864b1ab --- /dev/null +++ b/app/src/main/res/layout/layout_vote.xml @@ -0,0 +1,46 @@ + + + + + + + + + + + \ No newline at end of file diff --git a/app/src/main/res/layout/recycler_image_and_text.xml b/app/src/main/res/layout/recycler_image_and_text.xml new file mode 100644 index 0000000000..4b4cf86824 --- /dev/null +++ b/app/src/main/res/layout/recycler_image_and_text.xml @@ -0,0 +1,43 @@ + + + + + + + + + + \ No newline at end of file diff --git a/app/src/main/res/layout/recycler_image_article_detail_banner.xml b/app/src/main/res/layout/recycler_image_article_detail_banner.xml new file mode 100644 index 0000000000..9eb84dc804 --- /dev/null +++ b/app/src/main/res/layout/recycler_image_article_detail_banner.xml @@ -0,0 +1,12 @@ + + + + + \ No newline at end of file diff --git a/app/src/main/res/layout/recycler_image_article_draft.xml b/app/src/main/res/layout/recycler_image_article_draft.xml new file mode 100644 index 0000000000..12577829b3 --- /dev/null +++ b/app/src/main/res/layout/recycler_image_article_draft.xml @@ -0,0 +1,70 @@ + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/app/src/main/res/layout/recycler_recommend_home.xml b/app/src/main/res/layout/recycler_recommend_home.xml new file mode 100644 index 0000000000..e26f60b4d7 --- /dev/null +++ b/app/src/main/res/layout/recycler_recommend_home.xml @@ -0,0 +1,73 @@ + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/app/src/main/res/values-zh-rTW/strings.xml b/app/src/main/res/values-zh-rTW/strings.xml index 86bfa927e1..a9ae8a5454 100644 --- a/app/src/main/res/values-zh-rTW/strings.xml +++ b/app/src/main/res/values-zh-rTW/strings.xml @@ -60,6 +60,7 @@ 首頁 我的光環 說點什麽吧 + 說點什麽吧~ 發表評論... 用戶協議 隱私政策 @@ -649,5 +650,44 @@ 我的遊戲 玩過 預約 + + 圖文動態 + 發圖文 + 填寫標題可以讓更多人看見哦~ + 在這裡展開說說你的想法吧 + 發布圖文動態 + 發佈到論壇 + 封面 + 至少發布一張圖片哦 + 標題最多20個字哦 + 正文最多1000個字哦 + 選擇子版塊 + 圖文草稿 + 儲存至草稿箱並退出? + 不保存 + 儲存並退出 + + 確定要刪除該圖文草稿嗎?刪除之後不可恢復 + 正序 + 倒序 + 發布後不可修改論壇~ + 草稿保存成功 + 草稿保存失敗 + 草稿保存中 + 圖文發布中 + 發布圖文成功 + 發布圖文失敗 + 圖文%1$s + 圖文 + 已收藏 + 提交成功 + 提交失敗 + 權限錯誤 + 權限錯誤,請刷新後重試 + 操作成功 + 已删除 + 已隱藏 + %1$s萬 + https://m.ghzs666.com/bbs/note-%1$s diff --git a/app/src/main/res/values/attrs.xml b/app/src/main/res/values/attrs.xml index fb05df8772..7ac405b95c 100644 --- a/app/src/main/res/values/attrs.xml +++ b/app/src/main/res/values/attrs.xml @@ -88,4 +88,13 @@ + + + + + + + + + \ No newline at end of file diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index b6b1377527..63727d3956 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -60,6 +60,7 @@ 首页 我的光环 说点什么吧 + 说点什么吧~ 发表评论... 用户协议 隐私政策 @@ -649,4 +650,43 @@ 我的游戏 玩过 预约 + + 图文动态 + 发图文 + 填写标题可以让更多人看见哦~ + 在这里展开说说你的想法吧 + 发布图文动态 + 发布到论坛 + 封面 + 至少发布一张图片哦 + 标题最多20个字哦 + 正文最多1000个字哦 + 选择子版块 + 图文草稿 + 是否保存至草稿箱再退出? + 不保存 + 保存并退出 + 警告 + 确定要删除该图文草稿吗?删除之后不可恢复 + 正序 + 倒序 + 发布后不可修改论坛~ + 草稿保存成功 + 草稿保存失败 + 草稿保存中 + 图文发布中 + 发布图文成功 + 发布图文失败 + 图文%1$s + 图文 + 已收藏 + 提交成功 + 提交失败 + 权限错误 + 权限错误,请刷新后重试 + 操作成功 + 已删除 + 已隐藏 + %1$s万 + https://m.ghzs666.com/bbs/note-%1$s diff --git a/module_common/src/main/java/com/gh/gamecenter/common/base/fragment/BaseFragment.java b/module_common/src/main/java/com/gh/gamecenter/common/base/fragment/BaseFragment.java index fffb9bc9b4..9870cb6b3f 100644 --- a/module_common/src/main/java/com/gh/gamecenter/common/base/fragment/BaseFragment.java +++ b/module_common/src/main/java/com/gh/gamecenter/common/base/fragment/BaseFragment.java @@ -138,6 +138,7 @@ public abstract class BaseFragment extends Fragment implements OnRequestCallB @Override public void onCreate(@Nullable Bundle savedInstanceState) { super.onCreate(savedInstanceState); + System.out.println("kayn -->onCreate:"+this.getClass().getSimpleName()); final Intent intent = getActivity().getIntent(); mEntrance = intent.getStringExtra(KEY_ENTRANCE); if (getArguments() != null) { diff --git a/module_common/src/main/java/com/gh/gamecenter/common/baselist/ListAdapter.java b/module_common/src/main/java/com/gh/gamecenter/common/baselist/ListAdapter.java index e10603d79b..d0e8d1bfa1 100644 --- a/module_common/src/main/java/com/gh/gamecenter/common/baselist/ListAdapter.java +++ b/module_common/src/main/java/com/gh/gamecenter/common/baselist/ListAdapter.java @@ -9,6 +9,7 @@ import com.lightgame.adapter.BaseRecyclerAdapter; import java.util.ArrayList; import java.util.List; +import androidx.annotation.Nullable; import androidx.recyclerview.widget.DiffUtil; /** diff --git a/module_common/src/main/java/com/gh/gamecenter/common/baselist/PageLoader.kt b/module_common/src/main/java/com/gh/gamecenter/common/baselist/PageLoader.kt new file mode 100644 index 0000000000..d837ea2eb6 --- /dev/null +++ b/module_common/src/main/java/com/gh/gamecenter/common/baselist/PageLoader.kt @@ -0,0 +1,126 @@ +package com.gh.gamecenter.common.baselist + +import androidx.lifecycle.LiveData +import androidx.lifecycle.MutableLiveData +import com.gh.gamecenter.common.retrofit.BiResponse +import com.gh.gamecenter.common.utils.singleToMain +import io.reactivex.Single +import io.reactivex.disposables.CompositeDisposable + +/** + * 分页加载简易封装,只处理分页逻辑和页面状态 + */ +class PageLoader( + private val pageSize: Int = PAGE_SIZE_DEFAULT, + private val compositeDisposable: CompositeDisposable = CompositeDisposable(), + private val load: (Int, Int) -> Single> +) { + + private var _pageNo = 1 + val pageNo: Int + get() = _pageNo + + private val _pageState = MutableLiveData() + val pageState: LiveData = _pageState + + private val _dataList = MutableLiveData>() + val dataList: LiveData> = _dataList + fun updateData(newData: MutableList) { + _dataList.value = newData + } + + fun loadInit() { + _pageState.value = PageState.PageInitLoading + load(pageNo, pageSize) + .compose(singleToMain()) + .subscribe(object : BiResponse>() { + override fun onSuccess(data: List) { + _pageNo++ + if (data.isEmpty()) { + _pageState.value = PageState.PageNoData + } else { + _pageState.value = PageState.PageLoadMoreReady(PageState.PageInitLoading, data.size) + _dataList.value = data + } + } + + override fun onFailure(exception: Exception) { + _pageState.value = PageState.PageInitFailure + } + + }).let(compositeDisposable::add) + } + + fun pullToRefresh() { + _pageNo = 1 + _pageState.value = PageState.PagePullToRefreshLoading + load(pageNo, pageSize) + .compose(singleToMain()) + .subscribe(object : BiResponse>() { + override fun onSuccess(data: List) { + _pageNo++ + if (data.isEmpty()) { + _pageState.value = PageState.PageNoData + } else { + _pageState.value = PageState.PageLoadMoreReady(PageState.PagePullToRefreshLoading, data.size) + } + + _dataList.value = data + } + + override fun onFailure(exception: Exception) { + _pageState.value = PageState.PagePullToRefreshFailure + } + }).let(compositeDisposable::add) + } + + fun loadMore() { + _pageState.value = PageState.PageLoadMoreLoading + load(pageNo, pageSize) + .compose(singleToMain()) + .subscribe(object : BiResponse>() { + override fun onSuccess(data: List) { + _pageNo++ + if (data.isEmpty()) { + _pageState.value = PageState.PageLoadCompleted + } else { + _pageState.value = PageState.PageLoadMoreReady(PageState.PageLoadMoreLoading, data.size) + + val oldData = dataList.value ?: emptyList() + _dataList.value = oldData + data + } + } + + override fun onFailure(exception: Exception) { + _pageState.value = PageState.PageLoadMoreFailure + } + + }).let(compositeDisposable::add) + } + + companion object { + + private const val PAGE_SIZE_DEFAULT = 10 + } + + sealed class PageState { + + object PageInitLoading : PageState() + + object PageNoData : PageState() + + object PageInitFailure : PageState() + + data class PageLoadMoreReady(val previousState: PageState, val dataCount: Int) : PageState() + + object PageLoadMoreLoading : PageState() + + object PageLoadMoreFailure : PageState() + + object PageLoadCompleted : PageState() + + object PagePullToRefreshLoading : PageState() + + object PagePullToRefreshFailure : PageState() + } +} \ No newline at end of file diff --git a/module_common/src/main/java/com/gh/gamecenter/common/constant/Constants.java b/module_common/src/main/java/com/gh/gamecenter/common/constant/Constants.java index b31a615502..cd40f103de 100644 --- a/module_common/src/main/java/com/gh/gamecenter/common/constant/Constants.java +++ b/module_common/src/main/java/com/gh/gamecenter/common/constant/Constants.java @@ -476,4 +476,6 @@ public class Constants { public static final String TOOL_MAP_PACKAGE_NAME = "com.gh.toolmap";// 光环工具服务APP包名 + + public static final String SP_HAS_SHOW_IMAGE_AND_TEXT_DRAG_TIPS = "has_show_image_and_text_drag_tips"; } diff --git a/module_common/src/main/java/com/gh/gamecenter/common/constant/EntranceConsts.java b/module_common/src/main/java/com/gh/gamecenter/common/constant/EntranceConsts.java index 2288176822..7fc27a8b9a 100644 --- a/module_common/src/main/java/com/gh/gamecenter/common/constant/EntranceConsts.java +++ b/module_common/src/main/java/com/gh/gamecenter/common/constant/EntranceConsts.java @@ -278,7 +278,7 @@ public class EntranceConsts { public static final String KEY_FORUM_NAME = "forum_name";//版块名称 public static final String KEY_GAME_COLLECTION_TITLE = "game_collection_title";//游戏单标题 public static final String KEY_GAME_COLLECTION_ID = "game_collection_id";//游戏单ID - public static final String KEY_COLUMN_COLLECTION_NAME= "column_collection_name";//专题合集名称 + public static final String KEY_COLUMN_COLLECTION_NAME = "column_collection_name";//专题合集名称 public static final String KEY_COLUMN_COLLECTION_ID = "column_collection_id";//专题合集ID public static final String KEY_COLUMN_COLLECTION_STYLE = "column_collection_style";//专题合集样式 public static final String KEY_ASSIST_RES = "assist_res"; @@ -351,4 +351,7 @@ public class EntranceConsts { public static final String KEY_SHOW_ADD_GAMES_DIALOG = "show_add_games_dialog"; public static final String KEY_IS_AUTO_LOAD = "key_is_auto_load"; + + public static final String KEY_IMAGE_ARTICLE_ID = "KEY_IMAGE_ARTICLE_ID"; + public static final String KEY_IMAGE_ARTICLE_ENTITY = "KEY_IMAGE_ARTICLE_entity"; } diff --git a/module_common/src/main/java/com/gh/gamecenter/common/constant/RouteConsts.kt b/module_common/src/main/java/com/gh/gamecenter/common/constant/RouteConsts.kt index fe52b9c9b9..3e69b2893e 100644 --- a/module_common/src/main/java/com/gh/gamecenter/common/constant/RouteConsts.kt +++ b/module_common/src/main/java/com/gh/gamecenter/common/constant/RouteConsts.kt @@ -14,6 +14,8 @@ object RouteConsts { const val forumVideoDetailActivity = "/app/forumVideoDetailActivity" const val libaoDetailActivity = "/app/libaoDetailActivity" const val fullScreenVideoActivity = "/app/FullScreenVideoActivity" + const val imageArticleDetailActivity = "/app/imageArticleDetailActivity" + const val publishImageArticleActivity = "/app/publishImageArticleActivity" const val aboutActivity = "/settings/AboutActivity" const val webActivity = "/setting/WebActivity" diff --git a/module_common/src/main/java/com/gh/gamecenter/common/utils/PermissionHelper.kt b/module_common/src/main/java/com/gh/gamecenter/common/utils/PermissionHelper.kt index 0b3a734e58..a2de0ac1c9 100644 --- a/module_common/src/main/java/com/gh/gamecenter/common/utils/PermissionHelper.kt +++ b/module_common/src/main/java/com/gh/gamecenter/common/utils/PermissionHelper.kt @@ -113,7 +113,8 @@ object PermissionHelper { ) { // 无需申请存储权限的场景 (gameFormat 等于 APK/XAPK/APKS) - val ignoreStoragePermission = gameFormat == Constants.APK_FORMAT || gameFormat == Constants.XAPK_FORMAT || gameFormat == Constants.XAPK_APKS_FORMAT + val ignoreStoragePermission = + gameFormat == Constants.APK_FORMAT || gameFormat == Constants.XAPK_FORMAT || gameFormat == Constants.XAPK_APKS_FORMAT if (ignoreStoragePermission) { emptyCallback.onCallback() @@ -127,7 +128,11 @@ object PermissionHelper { emptyCallback.onCallback() SPUtils.setBoolean(Constants.SP_USER_HAS_PERMANENTLY_DENIED_STORAGE_PERMISSION, false) } else { - showStoragePermissionDialogAndRequestPermission(context, emptyCallback) + showStoragePermissionDialogAndRequestPermission(context) { + if (it) { + emptyCallback.onCallback() + } + } } } } @@ -317,7 +322,7 @@ object PermissionHelper { */ private fun showStoragePermissionDialogAndRequestPermission( activity: FragmentActivity, - emptyCallback: EmptyCallback + resultCallback: (Boolean) -> Unit ) { val solidContext = activity as? Activity ?: AppManager.getInstance().currentActivity() ?: return val dialog = Dialog(solidContext, R.style.GhAlertDialog) @@ -358,9 +363,7 @@ object PermissionHelper { dialog.window?.setBackgroundDrawable(ColorDrawable(Color.TRANSPARENT)) requestStoragePermissionIfNeeded(activity) { granted -> - if (granted) { - emptyCallback.onCallback() - } + resultCallback(granted) dialog.dismiss() } @@ -368,7 +371,8 @@ object PermissionHelper { } fun checkManageAllFilesOrStoragePermissionBeforeAction(context: Context, emptyCallback: EmptyCallback) { - val activity = context as? FragmentActivity ?: AppManager.getInstance().currentActivity() as? FragmentActivity ?: return + val activity = + context as? FragmentActivity ?: AppManager.getInstance().currentActivity() as? FragmentActivity ?: return if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.R) { if (Environment.isExternalStorageManager()) { emptyCallback.onCallback() @@ -550,4 +554,31 @@ object PermissionHelper { } } + @JvmStatic + fun checkStoragePermissionBeforeActionForResult( + context: Context, + gameFormat: String? = "", + resultCallback: (Boolean) -> Unit + ) { + // 无需申请存储权限的场景 (gameFormat 等于 APK/XAPK/APKS) + val ignoreStoragePermission = + gameFormat == Constants.APK_FORMAT || gameFormat == Constants.XAPK_FORMAT || gameFormat == Constants.XAPK_APKS_FORMAT + + if (ignoreStoragePermission) { + resultCallback(true) + return + } + + if (context is FragmentActivity) { + if (context.checkCallingOrSelfPermission(Manifest.permission.WRITE_EXTERNAL_STORAGE) == PackageManager.PERMISSION_GRANTED + && context.checkCallingOrSelfPermission(Manifest.permission.READ_EXTERNAL_STORAGE) == PackageManager.PERMISSION_GRANTED + ) { + resultCallback(true) + SPUtils.setBoolean(Constants.SP_USER_HAS_PERMANENTLY_DENIED_STORAGE_PERMISSION, false) + } else { + showStoragePermissionDialogAndRequestPermission(context, resultCallback) + } + } + } + } \ No newline at end of file diff --git a/module_common/src/main/res/color/color_selector_common_aw_primary_or_secondary.xml b/module_common/src/main/res/color/color_selector_common_aw_primary_or_secondary.xml new file mode 100644 index 0000000000..d1f1ef6769 --- /dev/null +++ b/module_common/src/main/res/color/color_selector_common_aw_primary_or_secondary.xml @@ -0,0 +1,5 @@ + + + + + \ No newline at end of file diff --git a/module_common/src/main/res/color/color_selector_common_primary_or_secondary.xml b/module_common/src/main/res/color/color_selector_common_primary_or_secondary.xml new file mode 100644 index 0000000000..964fbc5559 --- /dev/null +++ b/module_common/src/main/res/color/color_selector_common_primary_or_secondary.xml @@ -0,0 +1,5 @@ + + + + + \ No newline at end of file diff --git a/module_common/src/main/res/drawable/bg_common_button_light_fill_primary_theme.xml b/module_common/src/main/res/drawable/bg_common_button_light_fill_primary_theme.xml new file mode 100644 index 0000000000..afbd9eb75f --- /dev/null +++ b/module_common/src/main/res/drawable/bg_common_button_light_fill_primary_theme.xml @@ -0,0 +1,5 @@ + + + + + \ No newline at end of file diff --git a/module_common/src/main/res/drawable/selector_common_light_fill_blue_or_gray.xml b/module_common/src/main/res/drawable/selector_common_light_fill_blue_or_gray.xml new file mode 100644 index 0000000000..e3b794946d --- /dev/null +++ b/module_common/src/main/res/drawable/selector_common_light_fill_blue_or_gray.xml @@ -0,0 +1,5 @@ + + + + + \ No newline at end of file diff --git a/module_common/src/main/res/drawable/selector_common_primary_theme_or_gray.xml b/module_common/src/main/res/drawable/selector_common_primary_theme_or_gray.xml new file mode 100644 index 0000000000..9402cdf254 --- /dev/null +++ b/module_common/src/main/res/drawable/selector_common_primary_theme_or_gray.xml @@ -0,0 +1,5 @@ + + + + + \ No newline at end of file diff --git a/module_common/src/main/res/layout/activity_image_article_detail.xml b/module_common/src/main/res/layout/activity_image_article_detail.xml new file mode 100644 index 0000000000..b3a5d83384 --- /dev/null +++ b/module_common/src/main/res/layout/activity_image_article_detail.xml @@ -0,0 +1,15 @@ + + + + + + + \ No newline at end of file diff --git a/module_core_feature/src/main/java/com/gh/gamecenter/feature/entity/ArticleEntity.kt b/module_core_feature/src/main/java/com/gh/gamecenter/feature/entity/ArticleEntity.kt index 0feeeb656c..05738fec09 100644 --- a/module_core_feature/src/main/java/com/gh/gamecenter/feature/entity/ArticleEntity.kt +++ b/module_core_feature/src/main/java/com/gh/gamecenter/feature/entity/ArticleEntity.kt @@ -280,7 +280,7 @@ data class ArticleEntity( } @Parcelize -class Count( +data class Count( @SyncPage(syncNames = [SyncFieldConstants.ARTICLE_COMMENT_COUNT, SyncFieldConstants.ANSWER_COMMENT_COUNT]) var comment: Int = 0, @SyncPage(syncNames = [SyncFieldConstants.ARTICLE_VOTE_COUNT, SyncFieldConstants.ANSWER_VOTE_COUNT]) diff --git a/module_core_feature/src/main/java/com/gh/gamecenter/feature/entity/MeEntity.kt b/module_core_feature/src/main/java/com/gh/gamecenter/feature/entity/MeEntity.kt index 98e85c84d3..016de6138e 100644 --- a/module_core_feature/src/main/java/com/gh/gamecenter/feature/entity/MeEntity.kt +++ b/module_core_feature/src/main/java/com/gh/gamecenter/feature/entity/MeEntity.kt @@ -26,9 +26,9 @@ data class MeEntity( @SerializedName( value = "is_answer_own", - alternate = ["is_community_article_own", "is_question_own", "is_video_own", "is_game_list_own"] + alternate = ["is_community_article_own", "is_question_own", "is_video_own", "is_game_list_own", "is_image_article_own"] ) - var isContentOwner: Boolean = false, // 是否是当前内容(回答/社区文章/问题/视频/游戏单)的拥有者 + var isContentOwner: Boolean = false, // 是否是当前内容(回答/社区文章/问题/视频/游戏单/图文)的拥有者 @SerializedName( value = "is_community_article_top", @@ -68,14 +68,14 @@ data class MeEntity( @SerializedName( "is_comment_own", - alternate = ["is_answer_commented", "is_community_article_commented", "is_video_commented", "is_question_commented"] + alternate = ["is_answer_commented", "is_community_article_commented", "is_video_commented", "is_question_commented", "is_image_article_commented"] ) var isCommentOwner: Boolean = false, // 是否是当前评论的拥有者 @SyncPage(syncNames = [SyncFieldConstants.ARTICLE_COMMENT_VOTE]) @SerializedName( "is_comment_voted", - alternate = ["is_answer_comment_voted", "is_video_comment_voted", "is_community_article_comment_voted", "is_question_comment_voted", "is_game_list_voted"] + alternate = ["is_answer_comment_voted", "is_video_comment_voted", "is_community_article_comment_voted", "is_question_comment_voted", "is_game_list_voted", "is_image_article_comment_voted"] ) var isCommentVoted: Boolean = false, // 是否已经点赞过当前评论 @@ -123,7 +123,7 @@ data class MeEntity( @SerializedName( "is_answer_author", - alternate = ["is_community_article_author", "is_question_author", "is_video_author"] + alternate = ["is_community_article_author", "is_question_author", "is_video_author", "is_image_article_author"] ) val isContentAuthor: Boolean = false, //用于判断当前登录用户是否是此评论关联的回答/文章/问题/视频的作者 @@ -230,8 +230,30 @@ class Permissions( var hideVideo: Int = GUEST, //隐藏视频帖 @SerializedName("update-video-activity-tag") - var updateVideoActivityTag: Int = GUEST // 修改视频活动标签 + var updateVideoActivityTag: Int = GUEST, // 修改视频活动标签 + + @SerializedName("cancel-choiceness-image-article") + private var _cancelChoicenessImageArticle: Int? = null, + @SerializedName("choiceness-image-article") + private var _choicenessImageArticle: Int? = null, + @SerializedName("hide-image-article") + private var _hideImageArticle: Int? = null, + @SerializedName("hide-image-article-comment") + private var _hideImageArticleComment: Int? = null ) : Parcelable { + + val cancelChoicenessImageArticle: Int + get() = _cancelChoicenessImageArticle ?: GUEST + + val choicenessImageArticle: Int + get() = _choicenessImageArticle ?: GUEST + + val hideImageArticle: Int + get() = _hideImageArticle ?: GUEST + + val hideImageArticleComment: Int + get() = _hideImageArticleComment ?: GUEST + companion object { // -1 为无权限,0 为初级权限,1 为高级权限 const val ADMIN = 1 diff --git a/module_core_feature/src/main/java/com/gh/gamecenter/feature/entity/MessageEntity.kt b/module_core_feature/src/main/java/com/gh/gamecenter/feature/entity/MessageEntity.kt index 566e678c9c..edbdb50ba2 100644 --- a/module_core_feature/src/main/java/com/gh/gamecenter/feature/entity/MessageEntity.kt +++ b/module_core_feature/src/main/java/com/gh/gamecenter/feature/entity/MessageEntity.kt @@ -52,6 +52,8 @@ class MessageEntity { var title: String? = null + var content: String? = null + var thumb: String? = null @SerializedName("community_id") @@ -64,6 +66,7 @@ class MessageEntity { constructor(parcel: Parcel) : this() { id = parcel.readString() title = parcel.readString() + content = parcel.readString() thumb = parcel.readString() communityId = parcel.readString() images = parcel.createStringArrayList() ?: arrayListOf() @@ -73,6 +76,7 @@ class MessageEntity { override fun writeToParcel(parcel: Parcel, flags: Int) { parcel.writeString(id) parcel.writeString(title) + parcel.writeString(content) parcel.writeString(thumb) parcel.writeString(communityId) parcel.writeStringList(images) diff --git a/module_core_feature/src/main/java/com/gh/gamecenter/feature/entity/PersonalEntity.kt b/module_core_feature/src/main/java/com/gh/gamecenter/feature/entity/PersonalEntity.kt index 7ffed14e3a..b3df95ce48 100644 --- a/module_core_feature/src/main/java/com/gh/gamecenter/feature/entity/PersonalEntity.kt +++ b/module_core_feature/src/main/java/com/gh/gamecenter/feature/entity/PersonalEntity.kt @@ -49,10 +49,15 @@ data class PersonalEntity( @SerializedName("game_list") val gameList: Int = 0, @SerializedName("today_visit") - val todayVisit: Int? = 0 + val todayVisit: Int? = 0, + @SerializedName("image_article") + private val _imageArticle: Int? = null ) : Parcelable { - fun getTotalCount() = video + answer + question + communityArticle + val imageArticle: Int + get() = _imageArticle ?: 0 + + fun getTotalCount() = video + answer + question + communityArticle + imageArticle } fun getShortUserId(): String { diff --git a/module_message/src/main/java/com/gh/gamecenter/message/view/message/MessageItemViewHolder.java b/module_message/src/main/java/com/gh/gamecenter/message/view/message/MessageItemViewHolder.java index 69e25503d6..7a284424f9 100644 --- a/module_message/src/main/java/com/gh/gamecenter/message/view/message/MessageItemViewHolder.java +++ b/module_message/src/main/java/com/gh/gamecenter/message/view/message/MessageItemViewHolder.java @@ -538,6 +538,46 @@ public class MessageItemViewHolder extends BaseRecyclerViewHolder } voteMoreUser(messageEntity); break; + case "image_article_vote": + mBinding.messageCommand.setText("赞了你的图文动态"); + mBinding.messageContent.setVisibility(View.GONE); + mBinding.messageOriginalTitle.setText(messageEntity.getArticle().getTitle()); + videos = messageEntity.getArticle().getVideos(); + if (videos.size() > 0) { + mBinding.messageOriginalIcon.setVisibility(View.VISIBLE); + ImageUtils.display(mBinding.messageOriginalIcon, videos.get(0).getPoster()); + } else { + images = messageEntity.getArticle().getImages(); + if (images.size() > 0) { + mBinding.messageOriginalIcon.setVisibility(View.VISIBLE); + ImageUtils.display(mBinding.messageOriginalIcon, images.get(0)); + } else { + mBinding.messageOriginalIcon.setVisibility(View.GONE); + } + } + voteMoreUser(messageEntity); + break; + case "image_article_comment": + mBinding.messageCommand.setText("评论了你的图文动态"); + mBinding.messageContent.setVisibility(View.VISIBLE); + content = messageEntity.getComment().getContent(); + String title = messageEntity.getArticle().getTitle(); + if (TextUtils.isEmpty(title)) { + title = messageEntity.getArticle().getContent(); + } + mBinding.messageOriginalTitle.setText(title); + images = messageEntity.getArticle().getImages(); + if (!images.isEmpty()) { + targetUrl = images.get(0); + } + if (TextUtils.isEmpty(targetUrl)) { + mBinding.messageOriginalIcon.setVisibility(View.GONE); + } else { + mBinding.messageOriginalIcon.setVisibility(View.VISIBLE); + ImageUtils.display(mBinding.messageOriginalIcon, targetUrl); + } + voteMoreUser(messageEntity); + break; } TextHelper.highlightTextThatIsWrappedInsideWrapperByDefault( @@ -1256,6 +1296,15 @@ public class MessageItemViewHolder extends BaseRecyclerViewHolder )); } break; + case "image_article_comment": + ARouter.getInstance().build(RouteConsts.activity.imageArticleDetailActivity) + .withString(EntranceConsts.KEY_ENTRANCE, BaseActivity.mergeEntranceAndPath(entrance, path)) + .withString(EntranceConsts.KEY_TOP_COMMENT_ID, entity.getComment().getId()) + .withBoolean(EntranceConsts.KEY_SCROLL_TO_COMMENT_AREA, true) + .withString(EntranceConsts.KEY_PATH, path) + .withString(EntranceConsts.KEY_IMAGE_ARTICLE_ID, entity.getId()) + .navigation(); + break; } } } \ No newline at end of file diff --git a/module_message/src/main/java/com/gh/gamecenter/message/view/message/MessageListAdapter.kt b/module_message/src/main/java/com/gh/gamecenter/message/view/message/MessageListAdapter.kt index 7e3322a3c1..98cb606a8c 100644 --- a/module_message/src/main/java/com/gh/gamecenter/message/view/message/MessageListAdapter.kt +++ b/module_message/src/main/java/com/gh/gamecenter/message/view/message/MessageListAdapter.kt @@ -201,6 +201,7 @@ class MessageListAdapter( holder.binding.messageKefuSuggestion.visibility = View.GONE } + holder.binding.ivImageArticleCover.goneIf(true) val newLinks = entity.newLinks if (!newLinks.isNullOrEmpty()) { holder.binding.messageSkipList.visibility = View.VISIBLE diff --git a/module_message/src/main/res/layout/message_kefu_item.xml b/module_message/src/main/res/layout/message_kefu_item.xml index 630f3d2bb1..d5b0c2f238 100644 --- a/module_message/src/main/res/layout/message_kefu_item.xml +++ b/module_message/src/main/res/layout/message_kefu_item.xml @@ -14,7 +14,7 @@ + +