feat: creating, deleting comments | markdown, spoiler comments
This commit is contained in:
parent
129adc5825
commit
aaf9bdd00c
15 changed files with 672 additions and 157 deletions
|
@ -1,34 +0,0 @@
|
|||
package ani.dantotsu.media
|
||||
|
||||
import android.os.Bundle
|
||||
import android.view.ViewGroup
|
||||
import androidx.appcompat.app.AppCompatActivity
|
||||
import androidx.core.view.updateLayoutParams
|
||||
import ani.dantotsu.connections.anilist.Anilist
|
||||
import ani.dantotsu.databinding.FragmentCommentsBinding
|
||||
import ani.dantotsu.loadImage
|
||||
import ani.dantotsu.navBarHeight
|
||||
import ani.dantotsu.snackString
|
||||
import ani.dantotsu.statusBarHeight
|
||||
import ani.dantotsu.themes.ThemeManager
|
||||
|
||||
class CommentsFragment : AppCompatActivity(){
|
||||
lateinit var binding: FragmentCommentsBinding
|
||||
//Comments
|
||||
override fun onCreate(savedInstanceState: Bundle?) {
|
||||
super.onCreate(savedInstanceState)
|
||||
ThemeManager(this).applyTheme()
|
||||
binding = FragmentCommentsBinding.inflate(layoutInflater)
|
||||
setContentView(binding.root)
|
||||
|
||||
binding.commentsLayout.updateLayoutParams<ViewGroup.MarginLayoutParams> {
|
||||
topMargin = statusBarHeight
|
||||
bottomMargin = navBarHeight
|
||||
}
|
||||
binding.commentUserAvatar.loadImage(Anilist.avatar)
|
||||
binding.commentTitle.text = "Work in progress"
|
||||
binding.commentSend.setOnClickListener {
|
||||
//TODO
|
||||
}
|
||||
}
|
||||
}
|
|
@ -18,7 +18,7 @@ import ani.dantotsu.*
|
|||
import ani.dantotsu.databinding.DialogLayoutBinding
|
||||
import ani.dantotsu.databinding.ItemAnimeWatchBinding
|
||||
import ani.dantotsu.databinding.ItemChipBinding
|
||||
import ani.dantotsu.media.CommentsFragment
|
||||
import ani.dantotsu.media.comments.CommentsFragment
|
||||
import ani.dantotsu.media.Media
|
||||
import ani.dantotsu.media.MediaDetailsActivity
|
||||
import ani.dantotsu.media.SourceSearchDialogFragment
|
||||
|
@ -59,12 +59,13 @@ class AnimeWatchAdapter(
|
|||
override fun onBindViewHolder(holder: ViewHolder, position: Int) {
|
||||
val binding = holder.binding
|
||||
_binding = binding
|
||||
//Comments
|
||||
//CommentsAPI
|
||||
binding.animeComments.visibility = View.VISIBLE
|
||||
binding.animeComments.setOnClickListener {
|
||||
startActivity(
|
||||
fragment.requireContext(),
|
||||
Intent(fragment.requireContext(), CommentsFragment::class.java),
|
||||
Intent(fragment.requireContext(), CommentsFragment::class.java)
|
||||
.putExtra("mediaId", media.id),
|
||||
null
|
||||
)
|
||||
}
|
||||
|
|
140
app/src/main/java/ani/dantotsu/media/comments/CommentItem.kt
Normal file
140
app/src/main/java/ani/dantotsu/media/comments/CommentItem.kt
Normal file
|
@ -0,0 +1,140 @@
|
|||
package ani.dantotsu.media.comments
|
||||
|
||||
import android.view.View
|
||||
import ani.dantotsu.R
|
||||
import ani.dantotsu.connections.comments.Comment
|
||||
import ani.dantotsu.connections.comments.CommentsAPI
|
||||
import ani.dantotsu.databinding.ItemCommentsBinding
|
||||
import ani.dantotsu.loadImage
|
||||
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
|
||||
|
||||
class CommentItem(private val comment: Comment,
|
||||
private val markwon: Markwon,
|
||||
private val section: Section
|
||||
) : BindableItem<ItemCommentsBinding>() {
|
||||
|
||||
override fun bind(viewBinding: ItemCommentsBinding, position: Int) {
|
||||
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) View.VISIBLE else View.GONE
|
||||
viewBinding.commentEdit.visibility = if (isUserComment) View.VISIBLE else View.GONE
|
||||
viewBinding.commentReply.visibility = View.GONE //TODO: implement reply
|
||||
viewBinding.commentTotalReplies.visibility = View.GONE //TODO: implement reply
|
||||
viewBinding.commentReply.setOnClickListener {
|
||||
|
||||
}
|
||||
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)
|
||||
}
|
||||
}
|
||||
}
|
||||
//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.commentUserTime.text = formatTimestamp(comment.timestamp)
|
||||
}
|
||||
|
||||
override fun getLayout(): Int {
|
||||
return R.layout.item_comments
|
||||
}
|
||||
|
||||
override fun initializeViewBinding(view: View): ItemCommentsBinding {
|
||||
return ItemCommentsBinding.bind(view)
|
||||
}
|
||||
|
||||
private fun setVoteButtons(viewBinding: ItemCommentsBinding) {
|
||||
if (comment.userVoteType == 1) {
|
||||
viewBinding.commentUpVote.setImageResource(R.drawable.ic_round_upvote_active_24)
|
||||
viewBinding.commentDownVote.setImageResource(R.drawable.ic_round_upvote_inactive_24)
|
||||
} else if (comment.userVoteType == -1) {
|
||||
viewBinding.commentUpVote.setImageResource(R.drawable.ic_round_upvote_inactive_24)
|
||||
viewBinding.commentDownVote.setImageResource(R.drawable.ic_round_upvote_active_24)
|
||||
} 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"
|
||||
}
|
||||
}
|
||||
}
|
|
@ -0,0 +1,170 @@
|
|||
package ani.dantotsu.media.comments
|
||||
|
||||
import android.graphics.drawable.Drawable
|
||||
import android.os.Bundle
|
||||
import android.text.TextWatcher
|
||||
import android.view.LayoutInflater
|
||||
import android.view.ViewGroup
|
||||
import androidx.appcompat.app.AppCompatActivity
|
||||
import androidx.core.view.updateLayoutParams
|
||||
import androidx.lifecycle.lifecycleScope
|
||||
import androidx.recyclerview.widget.LinearLayoutManager
|
||||
import ani.dantotsu.connections.anilist.Anilist
|
||||
import ani.dantotsu.connections.comments.Comment
|
||||
import ani.dantotsu.connections.comments.CommentsAPI
|
||||
import ani.dantotsu.databinding.FragmentCommentsBinding
|
||||
import ani.dantotsu.databinding.ItemCommentsBinding
|
||||
import ani.dantotsu.loadImage
|
||||
import ani.dantotsu.navBarHeight
|
||||
import ani.dantotsu.snackString
|
||||
import ani.dantotsu.statusBarHeight
|
||||
import ani.dantotsu.themes.ThemeManager
|
||||
import com.bumptech.glide.Glide
|
||||
import com.bumptech.glide.RequestBuilder
|
||||
import com.bumptech.glide.RequestManager
|
||||
import com.bumptech.glide.load.DataSource
|
||||
import com.bumptech.glide.load.engine.GlideException
|
||||
import com.bumptech.glide.load.resource.gif.GifDrawable
|
||||
import com.bumptech.glide.request.RequestListener
|
||||
import com.bumptech.glide.request.target.Target
|
||||
import com.xwray.groupie.GroupAdapter
|
||||
import com.xwray.groupie.GroupieAdapter
|
||||
import com.xwray.groupie.Section
|
||||
import com.xwray.groupie.viewbinding.GroupieViewHolder
|
||||
import io.noties.markwon.Markwon
|
||||
import io.noties.markwon.SoftBreakAddsNewLinePlugin
|
||||
import io.noties.markwon.editor.MarkwonEditor
|
||||
import io.noties.markwon.editor.MarkwonEditorTextWatcher
|
||||
import io.noties.markwon.ext.strikethrough.StrikethroughPlugin
|
||||
import io.noties.markwon.ext.tables.TablePlugin
|
||||
import io.noties.markwon.ext.tasklist.TaskListPlugin
|
||||
import io.noties.markwon.html.HtmlPlugin
|
||||
import io.noties.markwon.image.AsyncDrawable
|
||||
import io.noties.markwon.image.glide.GlideImagesPlugin
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.launch
|
||||
import kotlinx.coroutines.withContext
|
||||
|
||||
class CommentsFragment : AppCompatActivity(){
|
||||
lateinit var binding: FragmentCommentsBinding
|
||||
|
||||
override fun onCreate(savedInstanceState: Bundle?) {
|
||||
super.onCreate(savedInstanceState)
|
||||
ThemeManager(this).applyTheme()
|
||||
binding = FragmentCommentsBinding.inflate(layoutInflater)
|
||||
setContentView(binding.root)
|
||||
//get the media id from the intent
|
||||
val mediaId = intent.getIntExtra("mediaId", -1)
|
||||
if (mediaId == -1) {
|
||||
snackString("Invalid Media ID")
|
||||
finish()
|
||||
}
|
||||
|
||||
val adapter = GroupieAdapter()
|
||||
val section = Section()
|
||||
val markwon = buildMarkwon()
|
||||
|
||||
binding.commentsLayout.updateLayoutParams<ViewGroup.MarginLayoutParams> {
|
||||
topMargin = statusBarHeight
|
||||
bottomMargin = navBarHeight
|
||||
}
|
||||
binding.commentUserAvatar.loadImage(Anilist.avatar)
|
||||
binding.commentTitle.text = "Work in progress"
|
||||
val markwonEditor = MarkwonEditor.create(markwon)
|
||||
binding.commentInput.addTextChangedListener(MarkwonEditorTextWatcher.withProcess(markwonEditor))
|
||||
binding.commentSend.setOnClickListener {
|
||||
if (CommentsAPI.isBanned) {
|
||||
snackString("You are banned from commenting :(")
|
||||
return@setOnClickListener
|
||||
}
|
||||
binding.commentInput.text.toString().let {
|
||||
if (it.isNotEmpty()) {
|
||||
binding.commentInput.text.clear()
|
||||
lifecycleScope.launch {
|
||||
val success = withContext(Dispatchers.IO) {
|
||||
CommentsAPI.comment(mediaId, null, it)
|
||||
}
|
||||
if (success != null)
|
||||
section.add(CommentItem(success, buildMarkwon(), section))
|
||||
}
|
||||
} else {
|
||||
snackString("Comment cannot be empty")
|
||||
}
|
||||
}
|
||||
}
|
||||
binding.commentInput.addTextChangedListener(object : TextWatcher {
|
||||
override fun beforeTextChanged(s: CharSequence?, start: Int, count: Int, after: Int) {
|
||||
}
|
||||
|
||||
override fun onTextChanged(s: CharSequence?, start: Int, before: Int, count: Int) {
|
||||
}
|
||||
|
||||
override fun afterTextChanged(s: android.text.Editable?) {
|
||||
if (binding.commentInput.text.length > 300) {
|
||||
binding.commentInput.text.delete(300, binding.commentInput.text.length)
|
||||
snackString("Comment cannot be longer than 300 characters")
|
||||
}
|
||||
}
|
||||
})
|
||||
|
||||
binding.commentsList.adapter = adapter
|
||||
binding.commentsList.layoutManager = LinearLayoutManager(this)
|
||||
lifecycleScope.launch {
|
||||
withContext(Dispatchers.IO) {
|
||||
val comments = CommentsAPI.getCommentsForId(mediaId)
|
||||
comments?.comments?.forEach {
|
||||
withContext(Dispatchers.Main) {
|
||||
section.add(CommentItem(it, buildMarkwon(), section))
|
||||
}
|
||||
}
|
||||
}
|
||||
adapter.add(section)
|
||||
}
|
||||
}
|
||||
|
||||
private fun buildMarkwon(): Markwon {
|
||||
val markwon = Markwon.builder(this)
|
||||
.usePlugin(SoftBreakAddsNewLinePlugin.create())
|
||||
.usePlugin(StrikethroughPlugin.create())
|
||||
.usePlugin(HtmlPlugin.create())
|
||||
.usePlugin(TablePlugin.create(this))
|
||||
.usePlugin(TaskListPlugin.create(this))
|
||||
.usePlugin(GlideImagesPlugin.create(object : GlideImagesPlugin.GlideStore {
|
||||
|
||||
private val requestManager: RequestManager = Glide.with(this@CommentsFragment).apply {
|
||||
addDefaultRequestListener(object : RequestListener<Any> {
|
||||
override fun onResourceReady(
|
||||
resource: Any,
|
||||
model: Any,
|
||||
target: Target<Any>,
|
||||
dataSource: DataSource,
|
||||
isFirstResource: Boolean
|
||||
): Boolean {
|
||||
if (resource is GifDrawable) {
|
||||
resource.start()
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
override fun onLoadFailed(
|
||||
e: GlideException?,
|
||||
model: Any?,
|
||||
target: Target<Any>,
|
||||
isFirstResource: Boolean
|
||||
): Boolean {
|
||||
return false
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
override fun load(drawable: AsyncDrawable): RequestBuilder<Drawable> {
|
||||
return requestManager.load(drawable.destination)
|
||||
}
|
||||
override fun cancel(target: Target<*>) {
|
||||
requestManager.clear(target)
|
||||
}
|
||||
}))
|
||||
.build()
|
||||
return markwon
|
||||
}
|
||||
}
|
|
@ -0,0 +1,59 @@
|
|||
package ani.dantotsu.media.comments
|
||||
|
||||
import android.content.Context
|
||||
import android.text.SpannableString
|
||||
import android.text.SpannableStringBuilder
|
||||
import android.text.Spanned
|
||||
import android.text.method.LinkMovementMethod
|
||||
import android.text.style.ClickableSpan
|
||||
import android.util.AttributeSet
|
||||
import android.view.View
|
||||
import androidx.appcompat.widget.AppCompatTextView
|
||||
import io.noties.markwon.Markwon
|
||||
|
||||
class SpoilerTextView @JvmOverloads constructor(
|
||||
context: Context,
|
||||
attrs: AttributeSet? = null,
|
||||
defStyleAttr: Int = 0
|
||||
) : AppCompatTextView(context, attrs, defStyleAttr) {
|
||||
|
||||
private val replaceWith = "▓"
|
||||
private var originalSpanned: SpannableStringBuilder? = null
|
||||
|
||||
fun setSpoilerText(text: Spanned, markwon: Markwon) : Spanned {
|
||||
val pattern = Regex("\\|\\|(.*?)\\|\\|")
|
||||
val matcher = pattern.toPattern().matcher(text)
|
||||
val spannableBuilder = SpannableStringBuilder(text)
|
||||
//remove the "||" from the text
|
||||
val originalBuilder = SpannableStringBuilder(text)
|
||||
originalSpanned = originalBuilder
|
||||
|
||||
val map = mutableMapOf<Int, Int>()
|
||||
while (matcher.find()) {
|
||||
val start = matcher.start()
|
||||
val end = matcher.end()
|
||||
map[start] = end
|
||||
}
|
||||
|
||||
map.forEach { (start, end) ->
|
||||
val replacement = replaceWith.repeat(end - start)
|
||||
spannableBuilder.replace(start, end, replacement)
|
||||
}
|
||||
val spannableString = SpannableString(spannableBuilder)
|
||||
map.forEach { (start, end) ->
|
||||
val clickableSpan = object : ClickableSpan() {
|
||||
override fun onClick(widget: View) {
|
||||
markwon.setParsedMarkdown(this@SpoilerTextView, originalSpanned!!.delete(end - 2, end).delete(start, start + 2))
|
||||
}
|
||||
}
|
||||
spannableString.setSpan(
|
||||
clickableSpan,
|
||||
start,
|
||||
end,
|
||||
SpannableString.SPAN_EXCLUSIVE_EXCLUSIVE
|
||||
)
|
||||
}
|
||||
movementMethod = LinkMovementMethod.getInstance()
|
||||
return spannableString
|
||||
}
|
||||
}
|
Loading…
Add table
Add a link
Reference in a new issue