362 lines
No EOL
13 KiB
Kotlin
362 lines
No EOL
13 KiB
Kotlin
package ani.dantotsu.media.comments
|
|
|
|
import android.annotation.SuppressLint
|
|
import android.graphics.Color
|
|
import android.view.View
|
|
import androidx.recyclerview.widget.LinearLayoutManager
|
|
import ani.dantotsu.R
|
|
import ani.dantotsu.connections.comments.Comment
|
|
import ani.dantotsu.connections.comments.CommentsAPI
|
|
import ani.dantotsu.copyToClipboard
|
|
import ani.dantotsu.currActivity
|
|
import ani.dantotsu.databinding.ItemCommentsBinding
|
|
import ani.dantotsu.loadImage
|
|
import ani.dantotsu.openLinkInBrowser
|
|
import ani.dantotsu.others.ImageViewDialog
|
|
import ani.dantotsu.settings.saving.PrefManager
|
|
import ani.dantotsu.settings.saving.PrefName
|
|
import ani.dantotsu.snackString
|
|
import com.xwray.groupie.GroupieAdapter
|
|
import com.xwray.groupie.Section
|
|
|
|
import com.xwray.groupie.viewbinding.BindableItem
|
|
import io.noties.markwon.Markwon
|
|
import kotlinx.coroutines.CoroutineScope
|
|
import kotlinx.coroutines.Dispatchers
|
|
import kotlinx.coroutines.SupervisorJob
|
|
import kotlinx.coroutines.launch
|
|
import java.text.SimpleDateFormat
|
|
import java.util.Date
|
|
import java.util.TimeZone
|
|
import kotlin.math.abs
|
|
import kotlin.math.pow
|
|
import kotlin.math.sqrt
|
|
|
|
class CommentItem(val comment: Comment,
|
|
private val markwon: Markwon,
|
|
private val section: Section,
|
|
private val commentsActivity: CommentsActivity,
|
|
private val backgroundColor: Int
|
|
) : BindableItem<ItemCommentsBinding>() {
|
|
var binding: ItemCommentsBinding? = null
|
|
val adapter = GroupieAdapter()
|
|
val repliesSection = Section()
|
|
var isEditing = false
|
|
private var isReplying = false
|
|
private var repliesVisible = false
|
|
|
|
init {
|
|
adapter.add(repliesSection)
|
|
}
|
|
|
|
@SuppressLint("SetTextI18n")
|
|
override fun bind(viewBinding: ItemCommentsBinding, position: Int) {
|
|
binding = viewBinding
|
|
viewBinding.commentRepliesList.layoutManager = LinearLayoutManager(currActivity())
|
|
viewBinding.commentRepliesList.adapter = adapter
|
|
val isUserComment = CommentsAPI.userId == comment.userId
|
|
val node = markwon.parse(comment.content)
|
|
val spanned = markwon.render(node)
|
|
markwon.setParsedMarkdown(viewBinding.commentText, viewBinding.commentText.setSpoilerText(spanned, markwon))
|
|
viewBinding.commentDelete.visibility = if (isUserComment || CommentsAPI.isAdmin || CommentsAPI.isMod) View.VISIBLE else View.GONE
|
|
viewBinding.commentBanUser.visibility = if ((CommentsAPI.isAdmin || CommentsAPI.isMod) && !isUserComment) View.VISIBLE else View.GONE
|
|
viewBinding.commentEdit.visibility = if (isUserComment) View.VISIBLE else View.GONE
|
|
replying(isReplying) //sets default text
|
|
editing(isEditing)
|
|
if ((comment.replyCount ?: 0) > 0) {
|
|
viewBinding.commentTotalReplies.visibility = View.VISIBLE
|
|
viewBinding.commentRepliesDivider.visibility = View.VISIBLE
|
|
viewBinding.commentTotalReplies.text = if(repliesVisible) "Hide Replies" else
|
|
"View ${comment.replyCount} repl${if (comment.replyCount == 1) "y" else "ies"}"
|
|
} else {
|
|
viewBinding.commentTotalReplies.visibility = View.GONE
|
|
viewBinding.commentRepliesDivider.visibility = View.GONE
|
|
}
|
|
viewBinding.commentReply.visibility = View.VISIBLE
|
|
viewBinding.commentTotalReplies.setOnClickListener {
|
|
if (repliesVisible) {
|
|
repliesSection.clear()
|
|
viewBinding.commentTotalReplies.text = "View ${comment.replyCount} repl${if (comment.replyCount == 1) "y" else "ies"}"
|
|
repliesVisible = false
|
|
} else {
|
|
viewBinding.commentTotalReplies.text = "Hide Replies"
|
|
repliesSection.clear()
|
|
commentsActivity.viewReplyCallback(this)
|
|
repliesVisible = true
|
|
}
|
|
}
|
|
|
|
viewBinding.commentUserName.setOnClickListener {
|
|
openLinkInBrowser("https://anilist.co/user/${comment.username}")
|
|
}
|
|
viewBinding.commentText.setOnLongClickListener {
|
|
copyToClipboard(comment.content)
|
|
true
|
|
}
|
|
|
|
viewBinding.commentEdit.setOnClickListener {
|
|
editing(!isEditing)
|
|
commentsActivity.editCallback(this)
|
|
}
|
|
viewBinding.commentReply.setOnClickListener {
|
|
replying(!isReplying)
|
|
commentsActivity.replyTo(this, comment.username)
|
|
commentsActivity.replyCallback(this)
|
|
}
|
|
viewBinding.modBadge.visibility = if (comment.isMod == true) View.VISIBLE else View.GONE
|
|
viewBinding.adminBadge.visibility = if (comment.isAdmin == true) View.VISIBLE else View.GONE
|
|
viewBinding.commentDelete.setOnClickListener {
|
|
val scope = CoroutineScope(Dispatchers.Main + SupervisorJob())
|
|
scope.launch {
|
|
val success = CommentsAPI.deleteComment(comment.commentId)
|
|
if (success) {
|
|
snackString("Comment Deleted")
|
|
section.remove(this@CommentItem)
|
|
}
|
|
}
|
|
}
|
|
viewBinding.commentBanUser.setOnClickListener {
|
|
val scope = CoroutineScope(Dispatchers.Main + SupervisorJob())
|
|
scope.launch {
|
|
val success = CommentsAPI.banUser(comment.userId)
|
|
if (success) {
|
|
snackString("User Banned")
|
|
}
|
|
}
|
|
}
|
|
//fill the icon if the user has liked the comment
|
|
setVoteButtons(viewBinding)
|
|
viewBinding.commentUpVote.setOnClickListener {
|
|
val voteType = if (comment.userVoteType == 1) 0 else 1
|
|
val previousVoteType = comment.userVoteType
|
|
val scope = CoroutineScope(Dispatchers.Main + SupervisorJob())
|
|
scope.launch {
|
|
val success = CommentsAPI.vote(comment.commentId, voteType)
|
|
if (success) {
|
|
comment.userVoteType = voteType
|
|
|
|
if (previousVoteType == -1) {
|
|
comment.downvotes -= 1
|
|
}
|
|
comment.upvotes += if (voteType == 1) 1 else -1
|
|
|
|
notifyChanged()
|
|
}
|
|
}
|
|
}
|
|
|
|
viewBinding.commentDownVote.setOnClickListener {
|
|
val voteType = if (comment.userVoteType == -1) 0 else -1
|
|
val previousVoteType = comment.userVoteType
|
|
val scope = CoroutineScope(Dispatchers.Main + SupervisorJob())
|
|
scope.launch {
|
|
val success = CommentsAPI.vote(comment.commentId, voteType)
|
|
if (success) {
|
|
comment.userVoteType = voteType
|
|
|
|
if (previousVoteType == 1) {
|
|
comment.upvotes -= 1
|
|
}
|
|
comment.downvotes += if (voteType == -1) 1 else -1
|
|
|
|
notifyChanged()
|
|
}
|
|
}
|
|
}
|
|
viewBinding.commentTotalVotes.text = (comment.upvotes - comment.downvotes).toString()
|
|
viewBinding.commentUserAvatar
|
|
comment.profilePictureUrl?.let { viewBinding.commentUserAvatar.loadImage(it) }
|
|
viewBinding.commentUserName.text = comment.username
|
|
viewBinding.commentUserName.setTextColor(getAvatarColor(comment.upvotes - comment.downvotes, backgroundColor))
|
|
viewBinding.commentUserTime.text = "● ${formatTimestamp(comment.timestamp)}"
|
|
}
|
|
|
|
override fun getLayout(): Int {
|
|
return R.layout.item_comments
|
|
}
|
|
|
|
override fun initializeViewBinding(view: View): ItemCommentsBinding {
|
|
return ItemCommentsBinding.bind(view)
|
|
}
|
|
|
|
fun replying(isReplying: Boolean) {
|
|
binding?.commentReply?.text = if (isReplying) currActivity()!!.getString(R.string.cancel) else "Reply"
|
|
PrefManager.setVal(PrefName.ReplyTo, isReplying)
|
|
this.isReplying = isReplying
|
|
}
|
|
|
|
fun editing(isEditing: Boolean) {
|
|
binding?.commentEdit?.text = if (isEditing) currActivity()!!.getString(R.string.cancel) else currActivity()!!.getString(R.string.edit)
|
|
this.isEditing = isEditing
|
|
}
|
|
fun test(isEditing: Boolean){
|
|
this.isEditing = isEditing
|
|
}
|
|
private fun setVoteButtons(viewBinding: ItemCommentsBinding) {
|
|
when (comment.userVoteType) {
|
|
1 -> {
|
|
viewBinding.commentUpVote.setImageResource(R.drawable.ic_round_upvote_active_24)
|
|
viewBinding.commentUpVote.alpha = 1f
|
|
viewBinding.commentDownVote.setImageResource(R.drawable.ic_round_upvote_inactive_24)
|
|
}
|
|
-1 -> {
|
|
viewBinding.commentUpVote.setImageResource(R.drawable.ic_round_upvote_inactive_24)
|
|
viewBinding.commentDownVote.setImageResource(R.drawable.ic_round_upvote_active_24)
|
|
viewBinding.commentDownVote.alpha = 1f
|
|
}
|
|
else -> {
|
|
viewBinding.commentUpVote.setImageResource(R.drawable.ic_round_upvote_inactive_24)
|
|
viewBinding.commentDownVote.setImageResource(R.drawable.ic_round_upvote_inactive_24)
|
|
}
|
|
}
|
|
}
|
|
|
|
private fun formatTimestamp(timestamp: String): String {
|
|
return try {
|
|
val dateFormat = SimpleDateFormat("yyyy-MM-dd'T'HH:mm:ss.SSS'Z'")
|
|
dateFormat.timeZone = TimeZone.getTimeZone("UTC")
|
|
val parsedDate = dateFormat.parse(timestamp)
|
|
val currentDate = Date()
|
|
|
|
val diff = currentDate.time - (parsedDate?.time ?: 0)
|
|
|
|
val days = diff / (24 * 60 * 60 * 1000)
|
|
val hours = diff / (60 * 60 * 1000) % 24
|
|
val minutes = diff / (60 * 1000) % 60
|
|
|
|
return when {
|
|
days > 0 -> "$days days ago"
|
|
hours > 0 -> "$hours hours ago"
|
|
minutes > 0 -> "$minutes minutes ago"
|
|
else -> "just now"
|
|
}
|
|
} catch (e: Exception) {
|
|
"now"
|
|
}
|
|
}
|
|
|
|
companion object {
|
|
fun timestampToMillis(timestamp: String): Long {
|
|
val dateFormat = SimpleDateFormat("yyyy-MM-dd'T'HH:mm:ss.SSS'Z'")
|
|
dateFormat.timeZone = TimeZone.getTimeZone("UTC")
|
|
val parsedDate = dateFormat.parse(timestamp)
|
|
return parsedDate?.time ?: 0
|
|
}
|
|
}
|
|
|
|
private fun getLuminance(color: Int): Double {
|
|
val r = Color.red(color) / 255.0
|
|
val g = Color.green(color) / 255.0
|
|
val b = Color.blue(color) / 255.0
|
|
|
|
val rL = if (r <= 0.03928) r / 12.92 else ((r + 0.055) / 1.055).pow(2.4)
|
|
val gL = if (g <= 0.03928) g / 12.92 else ((g + 0.055) / 1.055).pow(2.4)
|
|
val bL = if (b <= 0.03928) b / 12.92 else ((b + 0.055) / 1.055).pow(2.4)
|
|
|
|
return 0.2126 * rL + 0.7152 * gL + 0.0722 * bL
|
|
}
|
|
|
|
private fun getContrastRatio(color1: Int, color2: Int): Double {
|
|
val l1 = getLuminance(color1)
|
|
val l2 = getLuminance(color2)
|
|
|
|
return if (l1 > l2) (l1 + 0.05) / (l2 + 0.05) else (l2 + 0.05) / (l1 + 0.05)
|
|
}
|
|
|
|
private fun getAvatarColor(voteCount: Int, backgroundColor: Int): Int {
|
|
val level = sqrt(abs(voteCount.toDouble()) / 0.8).toInt()
|
|
val colorString = if (level > usernameColors.size - 1) usernameColors[usernameColors.size - 1] else usernameColors[level]
|
|
var color = Color.parseColor(colorString)
|
|
val ratio = getContrastRatio(color, backgroundColor)
|
|
if (ratio < 4.5) {
|
|
color = adjustColorForContrast(color, backgroundColor)
|
|
}
|
|
|
|
return color
|
|
}
|
|
|
|
private fun adjustColorForContrast(originalColor: Int, backgroundColor: Int): Int {
|
|
var adjustedColor = originalColor
|
|
var contrastRatio = getContrastRatio(adjustedColor, backgroundColor)
|
|
val isBackgroundDark = getLuminance(backgroundColor) < 0.5
|
|
|
|
while (contrastRatio < 4.5) {
|
|
// Adjust brightness by modifying the RGB values
|
|
val r = Color.red(adjustedColor)
|
|
val g = Color.green(adjustedColor)
|
|
val b = Color.blue(adjustedColor)
|
|
|
|
// Calculate the amount to adjust
|
|
val adjustment = if (isBackgroundDark) 10 else -10
|
|
|
|
// Adjust the color
|
|
val newR = (r + adjustment).coerceIn(0, 255)
|
|
val newG = (g + adjustment).coerceIn(0, 255)
|
|
val newB = (b + adjustment).coerceIn(0, 255)
|
|
|
|
adjustedColor = Color.rgb(newR, newG, newB)
|
|
contrastRatio = getContrastRatio(adjustedColor, backgroundColor)
|
|
|
|
// Break the loop if the color adjustment does not change (to avoid infinite loop)
|
|
if (newR == r && newG == g && newB == b) {
|
|
break
|
|
}
|
|
}
|
|
return adjustedColor
|
|
}
|
|
|
|
|
|
|
|
private val usernameColors: Array<String> = arrayOf(
|
|
"#9932cc",
|
|
"#a020f0",
|
|
"#8b008b",
|
|
"#7b68ee",
|
|
"#da70d6",
|
|
"#dda0dd",
|
|
"#ffe4b5",
|
|
"#f0e68c",
|
|
"#ffb6c1",
|
|
"#fa8072",
|
|
"#b03060",
|
|
"#ff1493",
|
|
"#ff00ff",
|
|
"#ff69b4",
|
|
"#dc143c",
|
|
"#8b0000",
|
|
"#ff0000",
|
|
"#a0522d",
|
|
"#f4a460",
|
|
"#b8860b",
|
|
"#ffa500",
|
|
"#d2691e",
|
|
"#ff6347",
|
|
"#808000",
|
|
"#ffd700",
|
|
"#ffff54",
|
|
"#8fbc8f",
|
|
"#3cb371",
|
|
"#008000",
|
|
"#00fa9a",
|
|
"#98fb98",
|
|
"#00ff00",
|
|
"#adff2f",
|
|
"#32cd32",
|
|
"#556b2f",
|
|
"#9acd32",
|
|
"#7fffd4",
|
|
"#2f4f4f",
|
|
"#5f9ea0",
|
|
"#87ceeb",
|
|
"#00bfff",
|
|
"#00ffff",
|
|
"#1e90ff",
|
|
"#4682b4",
|
|
"#0000ff",
|
|
"#0000cd",
|
|
"#00008b",
|
|
"#191970",
|
|
"#ffffff",
|
|
)
|
|
|
|
} |