feat: comments targeted at database

This commit is contained in:
rebelonion 2024-02-15 12:44:52 -06:00
parent 1694a1cb24
commit a73c4cd678
17 changed files with 544 additions and 228 deletions

View file

@ -48,7 +48,7 @@ android {
buildTypes { buildTypes {
alpha { alpha {
applicationIdSuffix ".beta" // keep as beta by popular request applicationIdSuffix ".beta" // keep as beta by popular request
versionNameSuffix "-alpha01" versionNameSuffix "-alpha02"
manifestPlaceholders = [icon_placeholder: "@mipmap/ic_launcher_alpha", icon_placeholder_round: "@mipmap/ic_launcher_alpha_round"] manifestPlaceholders = [icon_placeholder: "@mipmap/ic_launcher_alpha", icon_placeholder_round: "@mipmap/ic_launcher_alpha_round"]
debuggable System.getenv("CI") == null debuggable System.getenv("CI") == null
isDefault true isDefault true

View file

@ -109,7 +109,8 @@
android:name=".others.imagesearch.ImageSearchActivity" android:name=".others.imagesearch.ImageSearchActivity"
android:parentActivityName=".MainActivity" /> android:parentActivityName=".MainActivity" />
<activity <activity
android:name=".media.comments.CommentsFragment" /> android:name=".media.comments.CommentsActivity"
android:windowSoftInputMode="adjustResize|stateHidden" />
<activity <activity
android:name=".media.SearchActivity" android:name=".media.SearchActivity"
android:parentActivityName=".MainActivity" /> android:parentActivityName=".MainActivity" />

View file

@ -20,6 +20,7 @@ import ani.dantotsu.others.MalScraper
import ani.dantotsu.settings.saving.PrefManager import ani.dantotsu.settings.saving.PrefManager
import ani.dantotsu.settings.saving.PrefName import ani.dantotsu.settings.saving.PrefName
import ani.dantotsu.snackString import ani.dantotsu.snackString
import ani.dantotsu.toast
import kotlinx.coroutines.async import kotlinx.coroutines.async
import kotlinx.coroutines.awaitAll import kotlinx.coroutines.awaitAll
import kotlinx.coroutines.runBlocking import kotlinx.coroutines.runBlocking

View file

@ -9,8 +9,6 @@ import ani.dantotsu.settings.saving.PrefName
import ani.dantotsu.snackString import ani.dantotsu.snackString
import com.lagradost.nicehttp.Requests import com.lagradost.nicehttp.Requests
import eu.kanade.tachiyomi.network.NetworkHelper import eu.kanade.tachiyomi.network.NetworkHelper
import kotlinx.coroutines.runBlocking
import kotlinx.coroutines.withContext
import kotlinx.serialization.KSerializer import kotlinx.serialization.KSerializer
import kotlinx.serialization.SerialName import kotlinx.serialization.SerialName
import kotlinx.serialization.Serializable import kotlinx.serialization.Serializable
@ -27,7 +25,7 @@ import javax.crypto.Cipher
import javax.crypto.spec.SecretKeySpec import javax.crypto.spec.SecretKeySpec
object CommentsAPI { object CommentsAPI {
val address: String = "http://10.0.2.2:8081" val address: String = "https://1224665.xyz:443"
val appSecret = BuildConfig.APP_SECRET val appSecret = BuildConfig.APP_SECRET
var authToken: String? = null var authToken: String? = null
var userId: String? = null var userId: String? = null
@ -55,7 +53,7 @@ object CommentsAPI {
val json = request.post(url) val json = request.post(url)
val res = json.code == 200 val res = json.code == 200
if (!res) { if (!res) {
errorReason(json.code) errorReason(json.code, json.text)
} }
return res return res
} }
@ -73,7 +71,7 @@ object CommentsAPI {
val json = request.post(url, requestBody = body.build()) val json = request.post(url, requestBody = body.build())
val res = json.code == 200 val res = json.code == 200
if (!res) { if (!res) {
errorReason(json.code) errorReason(json.code, json.text)
return null return null
} }
val parsed = try { val parsed = try {
@ -105,7 +103,32 @@ object CommentsAPI {
val json = request.delete(url) val json = request.delete(url)
val res = json.code == 200 val res = json.code == 200
if (!res) { if (!res) {
errorReason(json.code) errorReason(json.code, json.text)
}
return res
}
suspend fun editComment(commentId: Int, content: String): Boolean {
val url = "$address/comments/$commentId"
val body = FormBody.Builder()
.add("content", content)
.build()
val request = requestBuilder()
val json = request.put(url, requestBody = body)
val res = json.code == 200
if (!res) {
errorReason(json.code, json.text)
}
return res
}
suspend fun banUser(userId: String): Boolean {
val url = "$address/ban/$userId"
val request = requestBuilder()
val json = request.post(url)
val res = json.code == 200
if (!res) {
errorReason(json.code, json.text)
} }
return res return res
} }
@ -125,7 +148,7 @@ object CommentsAPI {
val parsed = try { val parsed = try {
Json.decodeFromString<AuthResponse>(json.text) Json.decodeFromString<AuthResponse>(json.text)
} catch (e: Exception) { } catch (e: Exception) {
println("comment: $e") snackString("Failed to login to comments API: ${e.printStackTrace()}")
return return
} }
authToken = parsed.authToken authToken = parsed.authToken
@ -155,12 +178,12 @@ object CommentsAPI {
) )
} }
private fun errorReason(code: Int) { private fun errorReason(code: Int, reason: String? = null) {
val error = when (code) { val error = when (code) {
429 -> "Rate limited. :(" 429 -> "Rate limited. :("
else -> "Failed to connect" else -> "Failed to connect"
} }
snackString("Error $code: $error") snackString("Error $code: ${reason ?: error}")
} }
@SuppressLint("GetInstance") @SuppressLint("GetInstance")
@ -221,7 +244,7 @@ data class Comment(
@SerialName("parent_comment_id") @SerialName("parent_comment_id")
val parentCommentId: Int?, val parentCommentId: Int?,
@SerialName("content") @SerialName("content")
val content: String, var content: String,
@SerialName("timestamp") @SerialName("timestamp")
val timestamp: String, val timestamp: String,
@SerialName("deleted") @SerialName("deleted")
@ -236,7 +259,13 @@ data class Comment(
@SerialName("username") @SerialName("username")
val username: String, val username: String,
@SerialName("profile_picture_url") @SerialName("profile_picture_url")
val profilePictureUrl: String? val profilePictureUrl: String?,
@SerialName("is_mod")
@Serializable(with = NumericBooleanSerializer::class)
val isMod: Boolean? = null,
@SerialName("is_admin")
@Serializable(with = NumericBooleanSerializer::class)
val isAdmin: Boolean? = null
) )
@Serializable @Serializable

View file

@ -76,7 +76,7 @@ class MediaInfoFragment : Fragment() {
loaded = true loaded = true
//Youtube //Youtube
if (media.anime!!.youtube != null && PrefManager.getVal(PrefName.ShowYtButton)) { if (media.anime?.youtube != null && PrefManager.getVal(PrefName.ShowYtButton)) {
binding.animeSourceYT.visibility = View.VISIBLE binding.animeSourceYT.visibility = View.VISIBLE
binding.animeSourceYT.setOnClickListener { binding.animeSourceYT.setOnClickListener {
val intent = Intent(Intent.ACTION_VIEW, Uri.parse(media.anime.youtube)) val intent = Intent(Intent.ACTION_VIEW, Uri.parse(media.anime.youtube))

View file

@ -15,10 +15,11 @@ import androidx.core.content.ContextCompat.startActivity
import androidx.lifecycle.lifecycleScope import androidx.lifecycle.lifecycleScope
import androidx.recyclerview.widget.RecyclerView import androidx.recyclerview.widget.RecyclerView
import ani.dantotsu.* import ani.dantotsu.*
import ani.dantotsu.connections.comments.CommentsAPI
import ani.dantotsu.databinding.DialogLayoutBinding import ani.dantotsu.databinding.DialogLayoutBinding
import ani.dantotsu.databinding.ItemAnimeWatchBinding import ani.dantotsu.databinding.ItemAnimeWatchBinding
import ani.dantotsu.databinding.ItemChipBinding import ani.dantotsu.databinding.ItemChipBinding
import ani.dantotsu.media.comments.CommentsFragment import ani.dantotsu.media.comments.CommentsActivity
import ani.dantotsu.media.Media import ani.dantotsu.media.Media
import ani.dantotsu.media.MediaDetailsActivity import ani.dantotsu.media.MediaDetailsActivity
import ani.dantotsu.media.SourceSearchDialogFragment import ani.dantotsu.media.SourceSearchDialogFragment
@ -60,11 +61,11 @@ class AnimeWatchAdapter(
val binding = holder.binding val binding = holder.binding
_binding = binding _binding = binding
//CommentsAPI //CommentsAPI
binding.animeComments.visibility = View.VISIBLE binding.animeComments.visibility = if (CommentsAPI.userId == null) View.GONE else View.VISIBLE
binding.animeComments.setOnClickListener { binding.animeComments.setOnClickListener {
startActivity( startActivity(
fragment.requireContext(), fragment.requireContext(),
Intent(fragment.requireContext(), CommentsFragment::class.java) Intent(fragment.requireContext(), CommentsActivity::class.java)
.putExtra("mediaId", media.id), .putExtra("mediaId", media.id),
null null
) )

View file

@ -20,9 +20,10 @@ import java.text.SimpleDateFormat
import java.util.Date import java.util.Date
import java.util.TimeZone import java.util.TimeZone
class CommentItem(private val comment: Comment, class CommentItem(val comment: Comment,
private val markwon: Markwon, private val markwon: Markwon,
private val section: Section private val section: Section,
private val editCallback: (CommentItem) -> Unit
) : BindableItem<ItemCommentsBinding>() { ) : BindableItem<ItemCommentsBinding>() {
override fun bind(viewBinding: ItemCommentsBinding, position: Int) { override fun bind(viewBinding: ItemCommentsBinding, position: Int) {
@ -30,13 +31,29 @@ class CommentItem(private val comment: Comment,
val node = markwon.parse(comment.content) val node = markwon.parse(comment.content)
val spanned = markwon.render(node) val spanned = markwon.render(node)
markwon.setParsedMarkdown(viewBinding.commentText, viewBinding.commentText.setSpoilerText(spanned, markwon)) markwon.setParsedMarkdown(viewBinding.commentText, viewBinding.commentText.setSpoilerText(spanned, markwon))
viewBinding.commentDelete.visibility = if (isUserComment) View.VISIBLE else View.GONE viewBinding.commentDelete.visibility = if (isUserComment || CommentsAPI.isAdmin || CommentsAPI.isMod) View.VISIBLE else View.GONE
viewBinding.commentBanUser.visibility = if (CommentsAPI.isAdmin || CommentsAPI.isMod) View.VISIBLE else View.GONE
viewBinding.commentEdit.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.commentReply.visibility = View.GONE //TODO: implement reply
viewBinding.commentTotalReplies.visibility = View.GONE //TODO: implement reply viewBinding.commentTotalReplies.visibility = View.GONE //TODO: implement reply
viewBinding.commentReply.setOnClickListener { viewBinding.commentReply.setOnClickListener {
} }
var isEditing = false
viewBinding.commentEdit.setOnClickListener {
if (!isEditing) {
viewBinding.commentEdit.text = "Cancel"
isEditing = true
editCallback(this)
} else {
viewBinding.commentEdit.text = "Edit"
isEditing = false
editCallback(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 { viewBinding.commentDelete.setOnClickListener {
val scope = CoroutineScope(Dispatchers.Main + SupervisorJob()) val scope = CoroutineScope(Dispatchers.Main + SupervisorJob())
scope.launch { scope.launch {
@ -47,6 +64,15 @@ class CommentItem(private val comment: Comment,
} }
} }
} }
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 //fill the icon if the user has liked the comment
setVoteButtons(viewBinding) setVoteButtons(viewBinding)
viewBinding.commentUpVote.setOnClickListener { viewBinding.commentUpVote.setOnClickListener {
@ -137,4 +163,11 @@ class CommentItem(private val comment: Comment,
"now" "now"
} }
} }
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
}
} }

View file

@ -0,0 +1,332 @@
package ani.dantotsu.media.comments
import android.graphics.drawable.Drawable
import android.os.Bundle
import android.text.TextWatcher
import android.view.View
import android.view.inputmethod.InputMethodManager
import androidx.appcompat.app.AppCompatActivity
import androidx.appcompat.widget.PopupMenu
import androidx.lifecycle.lifecycleScope
import androidx.recyclerview.widget.LinearLayoutManager
import ani.dantotsu.R
import ani.dantotsu.connections.anilist.Anilist
import ani.dantotsu.connections.comments.CommentsAPI
import ani.dantotsu.databinding.ActivityCommentsBinding
import ani.dantotsu.initActivity
import ani.dantotsu.loadImage
import ani.dantotsu.snackString
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.GroupieAdapter
import com.xwray.groupie.Section
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 CommentsActivity : AppCompatActivity() {
lateinit var binding: ActivityCommentsBinding
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
ThemeManager(this).applyTheme()
binding = ActivityCommentsBinding.inflate(layoutInflater)
setContentView(binding.root)
initActivity(this)
//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.commentUserAvatar.loadImage(Anilist.avatar)
binding.commentTitle.text = getText(R.string.comments)
val markwonEditor = MarkwonEditor.create(markwon)
binding.commentInput.addTextChangedListener(
MarkwonEditorTextWatcher.withProcess(
markwonEditor
)
)
var editing = false
var editingCommentId = -1
fun editCallback(comment: CommentItem) {
if (editingCommentId == comment.comment.commentId) {
editing = false
editingCommentId = -1
binding.commentInput.setText("")
val imm = getSystemService(INPUT_METHOD_SERVICE) as InputMethodManager
imm.hideSoftInputFromWindow(binding.commentInput.windowToken, 0)
} else {
editing = true
editingCommentId = comment.comment.commentId
binding.commentInput.setText(comment.comment.content)
binding.commentInput.requestFocus()
binding.commentInput.setSelection(binding.commentInput.text.length)
val imm = getSystemService(INPUT_METHOD_SERVICE) as InputMethodManager
imm.showSoftInput(binding.commentInput, InputMethodManager.SHOW_IMPLICIT)
}
}
binding.commentsRefresh.setOnRefreshListener {
lifecycleScope.launch {
binding.commentsList.visibility = View.GONE
adapter.clear()
section.clear()
withContext(Dispatchers.IO) {
val comments = CommentsAPI.getCommentsForId(mediaId)
comments?.comments?.forEach {
withContext(Dispatchers.Main) {
section.add(CommentItem(it, buildMarkwon(), section) { comment ->
editCallback(comment)
})
}
}
}
adapter.add(section)
binding.commentsList.visibility = View.VISIBLE
binding.commentsRefresh.isRefreshing = false
}
}
var pagesLoaded = 1
var totalPages = 1
binding.commentsList.adapter = adapter
binding.commentsList.layoutManager = LinearLayoutManager(this)
lifecycleScope.launch {
binding.commentsProgressBar.visibility = View.VISIBLE
binding.commentsList.visibility = View.GONE
withContext(Dispatchers.IO) {
val comments = CommentsAPI.getCommentsForId(mediaId)
comments?.comments?.forEach {
withContext(Dispatchers.Main) {
section.add(CommentItem(it, buildMarkwon(), section) { comment ->
editCallback(comment)
})
}
}
totalPages = comments?.totalPages ?: 1
}
binding.commentsProgressBar.visibility = View.GONE
binding.commentsList.visibility = View.VISIBLE
adapter.add(section)
}
binding.commentSort.setOnClickListener {
val popup = PopupMenu(this, it)
popup.setOnMenuItemClickListener { item ->
when (item.itemId) {
R.id.comment_sort_newest -> {
val groups = section.groups
groups.sortByDescending { comment ->
comment as CommentItem
comment.timestampToMillis(comment.comment.timestamp)
}
section.update(groups)
binding.commentsList.scrollToPosition(0)
}
R.id.comment_sort_oldest -> {
val groups = section.groups
groups.sortBy { comment ->
comment as CommentItem
comment.timestampToMillis(comment.comment.timestamp)
}
section.update(groups)
binding.commentsList.scrollToPosition(0)
}
R.id.comment_sort_highest_rated -> {
val groups = section.groups
groups.sortByDescending { comment ->
comment as CommentItem
comment.comment.upvotes - comment.comment.downvotes
}
section.update(groups)
binding.commentsList.scrollToPosition(0)
}
R.id.comment_sort_lowest_rated -> {
val groups = section.groups
groups.sortBy { comment ->
comment as CommentItem
comment.comment.upvotes - comment.comment.downvotes
}
section.update(groups)
binding.commentsList.scrollToPosition(0)
}
}
true
}
popup.inflate(R.menu.comments_sort_menu)
popup.show()
}
var fetching = false
//if we have scrolled to the bottom of the list, load more comments
binding.commentsList.addOnScrollListener(object :
androidx.recyclerview.widget.RecyclerView.OnScrollListener() {
override fun onScrolled(
recyclerView: androidx.recyclerview.widget.RecyclerView,
dx: Int,
dy: Int
) {
super.onScrolled(recyclerView, dx, dy)
if (!recyclerView.canScrollVertically(1)) {
if (pagesLoaded < totalPages && !fetching) {
fetching = true
lifecycleScope.launch {
withContext(Dispatchers.IO) {
val comments =
CommentsAPI.getCommentsForId(mediaId, pagesLoaded + 1)
comments?.comments?.forEach {
withContext(Dispatchers.Main) {
section.add(
CommentItem(
it,
buildMarkwon(),
section
) { comment ->
editCallback(comment)
})
}
}
totalPages = comments?.totalPages ?: 1
}
pagesLoaded++
fetching = false
}
}
}
}
})
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.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 {
if (editing) {
val success = withContext(Dispatchers.IO) {
CommentsAPI.editComment(editingCommentId, it)
}
if (success) {
val groups = section.groups
groups.forEach { item ->
if (item is CommentItem) {
if (item.comment.commentId == editingCommentId) {
item.comment.content = it
item.notifyChanged()
snackString("Comment edited")
}
}
}
}
} else {
val success = withContext(Dispatchers.IO) {
CommentsAPI.comment(mediaId, null, it)
}
if (success != null)
section.add(0, CommentItem(success, buildMarkwon(), section) { comment ->
editCallback(comment)
})
}
}
} else {
snackString("Comment cannot be empty")
}
}
}
}
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@CommentsActivity).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
}
}

View file

@ -1,183 +0,0 @@
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.View
import android.view.ViewGroup
import androidx.appcompat.app.AppCompatActivity
import androidx.appcompat.widget.PopupMenu
import androidx.core.view.updateLayoutParams
import androidx.lifecycle.lifecycleScope
import androidx.recyclerview.widget.LinearLayoutManager
import ani.dantotsu.R
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.settings.saving.PrefManager
import ani.dantotsu.settings.saving.PrefName
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)
}
binding.commentSort.setOnClickListener {
val popup = PopupMenu(this, it)
popup.setOnMenuItemClickListener { item ->
true
}
popup.inflate(R.menu.comments_sort_menu)
popup.show()
}
}
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
}
}

View file

@ -5,9 +5,6 @@ import android.app.AlertDialog
import android.content.Intent import android.content.Intent
import android.view.LayoutInflater import android.view.LayoutInflater
import android.view.View import android.view.View
import android.os.Bundle
import ani.dantotsu.settings.FAQActivity
import androidx.core.content.ContextCompat.startActivity
import android.view.ViewGroup import android.view.ViewGroup
import android.widget.ArrayAdapter import android.widget.ArrayAdapter
import android.widget.CheckBox import android.widget.CheckBox
@ -15,9 +12,11 @@ import android.widget.ImageButton
import android.widget.LinearLayout import android.widget.LinearLayout
import android.widget.NumberPicker import android.widget.NumberPicker
import androidx.core.content.ContextCompat import androidx.core.content.ContextCompat
import androidx.core.content.ContextCompat.startActivity
import androidx.lifecycle.lifecycleScope import androidx.lifecycle.lifecycleScope
import androidx.recyclerview.widget.RecyclerView import androidx.recyclerview.widget.RecyclerView
import ani.dantotsu.* import ani.dantotsu.*
import ani.dantotsu.connections.comments.CommentsAPI
import ani.dantotsu.databinding.DialogLayoutBinding import ani.dantotsu.databinding.DialogLayoutBinding
import ani.dantotsu.databinding.ItemAnimeWatchBinding import ani.dantotsu.databinding.ItemAnimeWatchBinding
import ani.dantotsu.databinding.ItemChipBinding import ani.dantotsu.databinding.ItemChipBinding
@ -25,11 +24,13 @@ import ani.dantotsu.media.Media
import ani.dantotsu.media.MediaDetailsActivity import ani.dantotsu.media.MediaDetailsActivity
import ani.dantotsu.media.SourceSearchDialogFragment import ani.dantotsu.media.SourceSearchDialogFragment
import ani.dantotsu.media.anime.handleProgress import ani.dantotsu.media.anime.handleProgress
import ani.dantotsu.media.comments.CommentsActivity
import ani.dantotsu.others.LanguageMapper import ani.dantotsu.others.LanguageMapper
import ani.dantotsu.others.webview.CookieCatcher import ani.dantotsu.others.webview.CookieCatcher
import ani.dantotsu.parsers.DynamicMangaParser import ani.dantotsu.parsers.DynamicMangaParser
import ani.dantotsu.parsers.MangaReadSources import ani.dantotsu.parsers.MangaReadSources
import ani.dantotsu.parsers.MangaSources import ani.dantotsu.parsers.MangaSources
import ani.dantotsu.settings.FAQActivity
import ani.dantotsu.settings.saving.PrefManager import ani.dantotsu.settings.saving.PrefManager
import ani.dantotsu.settings.saving.PrefName import ani.dantotsu.settings.saving.PrefName
import ani.dantotsu.subcriptions.Notifications.Companion.openSettings import ani.dantotsu.subcriptions.Notifications.Companion.openSettings
@ -66,9 +67,19 @@ class MangaReadAdapter(
_binding = binding _binding = binding
binding.sourceTitle.setText(R.string.chaps) binding.sourceTitle.setText(R.string.chaps)
binding.animeComments.visibility = if (CommentsAPI.userId == null) View.GONE else View.VISIBLE
binding.animeComments.setOnClickListener {
startActivity(
fragment.requireContext(),
Intent(fragment.requireContext(), CommentsActivity::class.java)
.putExtra("mediaId", media.id),
null
)
}
//Fuck u launch //Fuck u launch
binding.faqbutton.setOnClickListener { binding.faqbutton.setOnClickListener {
val intent = Intent(fragment.requireContext(), FAQActivity::class.java) val intent = Intent(fragment.requireContext(), FAQActivity::class.java)
startActivity(fragment.requireContext(), intent, null) startActivity(fragment.requireContext(), intent, null)
} }
@ -447,11 +458,10 @@ class MangaReadAdapter(
if (media.manga.chapters!!.isNotEmpty()) { if (media.manga.chapters!!.isNotEmpty()) {
binding.animeSourceNotFound.visibility = View.GONE binding.animeSourceNotFound.visibility = View.GONE
binding.faqbutton.visibility = View.GONE binding.faqbutton.visibility = View.GONE
} } else {
else {
binding.animeSourceNotFound.visibility = View.VISIBLE binding.animeSourceNotFound.visibility = View.VISIBLE
binding.faqbutton.visibility = View.VISIBLE binding.faqbutton.visibility = View.VISIBLE
} }
} else { } else {
binding.animeSourceContinue.visibility = View.GONE binding.animeSourceContinue.visibility = View.GONE
binding.animeSourceNotFound.visibility = View.GONE binding.animeSourceNotFound.visibility = View.GONE

View file

@ -0,0 +1,14 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="24dp"
android:height="24dp"
android:tint="#fcba03"
android:viewportWidth="24"
android:viewportHeight="24">
<path
android:fillColor="#00000000"
android:pathData="M16,9L20,5V16H4V5L6,7M8,9L12,5L14,7M4,19H20"
android:strokeWidth="1.5"
android:strokeColor="#fcba03"
android:strokeLineCap="round"
android:strokeLineJoin="round" />
</vector>

View file

@ -0,0 +1,14 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="24dp"
android:height="24dp"
android:tint="#ff0f0f"
android:viewportWidth="24"
android:viewportHeight="24">
<path
android:fillColor="#00000000"
android:pathData="M11.302,21.615C11.523,21.744 11.634,21.809 11.79,21.842C11.912,21.868 12.088,21.868 12.21,21.842C12.366,21.809 12.477,21.744 12.698,21.615C14.646,20.478 20,16.908 20,12V6.6C20,6.042 20,5.763 19.893,5.55C19.797,5.362 19.649,5.212 19.461,5.114C19.25,5.004 18.966,5.001 18.399,4.994C15.427,4.959 13.714,4.714 12,3C10.286,4.714 8.573,4.959 5.601,4.994C5.034,5.001 4.75,5.004 4.539,5.114C4.351,5.212 4.203,5.362 4.107,5.55C4,5.763 4,6.042 4,6.6V12C4,16.908 9.354,20.478 11.302,21.615Z"
android:strokeWidth="2"
android:strokeColor="#ff0f0f"
android:strokeLineCap="round"
android:strokeLineJoin="round" />
</vector>

View file

@ -4,6 +4,7 @@
android:layout_width="match_parent" android:layout_width="match_parent"
android:layout_height="match_parent" android:layout_height="match_parent"
xmlns:app="http://schemas.android.com/apk/res-auto" xmlns:app="http://schemas.android.com/apk/res-auto"
android:fitsSystemWindows="true"
android:id="@+id/commentsLayout"> android:id="@+id/commentsLayout">
<LinearLayout <LinearLayout
@ -38,27 +39,43 @@
android:contentDescription="@string/sort_by" android:contentDescription="@string/sort_by"
app:srcCompat="@drawable/ic_round_sort_24" app:srcCompat="@drawable/ic_round_sort_24"
app:tint="?attr/colorOnBackground" /> app:tint="?attr/colorOnBackground" />
</LinearLayout> </LinearLayout>
<ProgressBar
android:id="@+id/commentsProgressBar"
style="?android:attr/progressBarStyle"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_gravity="bottom|center_horizontal"
android:layout_marginTop="64dp" />
<androidx.swiperefreshlayout.widget.SwipeRefreshLayout
android:id="@+id/commentsRefresh"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginTop="12dp"
android:layout_marginBottom="58dp"
android:clipChildren="false"
android:clipToPadding="false">
<androidx.recyclerview.widget.RecyclerView <androidx.recyclerview.widget.RecyclerView
android:id="@+id/commentsList" android:id="@+id/commentsList"
android:layout_width="match_parent" android:layout_width="match_parent"
android:layout_height="wrap_content" android:layout_height="wrap_content"
android:layout_marginTop="12dp" android:visibility="gone"
android:layout_marginBottom="58dp"
tools:listitem="@layout/item_comments" /> tools:listitem="@layout/item_comments" />
</androidx.swiperefreshlayout.widget.SwipeRefreshLayout>
</LinearLayout> </LinearLayout>
<LinearLayout <LinearLayout
android:id="@+id/commentInputLayout"
android:layout_width="match_parent" android:layout_width="match_parent"
android:layout_height="wrap_content" android:layout_height="wrap_content"
android:orientation="horizontal" android:orientation="horizontal"
android:paddingBottom="8dp" android:paddingBottom="8dp"
android:paddingTop="8dp" android:paddingTop="8dp"
android:layout_gravity="bottom|end" android:layout_gravity="bottom|end"
android:windowSoftInputMode="adjustResize"
android:background="@color/nav_bg"> android:background="@color/nav_bg">
<com.google.android.material.imageview.ShapeableImageView <com.google.android.material.imageview.ShapeableImageView
@ -95,6 +112,5 @@
android:layout_marginEnd="12dp" android:layout_marginEnd="12dp"
android:background="@drawable/ic_round_send_24" android:background="@drawable/ic_round_send_24"
tools:ignore="ContentDescription" /> tools:ignore="ContentDescription" />
</LinearLayout> </LinearLayout>
</FrameLayout> </FrameLayout>

View file

@ -19,6 +19,7 @@
android:layout_width="36dp" android:layout_width="36dp"
android:layout_height="36dp" android:layout_height="36dp"
android:scaleType="center" android:scaleType="center"
style="@style/CircularImageView"
app:srcCompat="@drawable/ic_round_add_circle_24" app:srcCompat="@drawable/ic_round_add_circle_24"
tools:ignore="ContentDescription,ImageContrastCheck" /> tools:ignore="ContentDescription,ImageContrastCheck" />
@ -33,6 +34,7 @@
<LinearLayout <LinearLayout
android:layout_width="match_parent" android:layout_width="match_parent"
android:layout_height="wrap_content" android:layout_height="wrap_content"
android:gravity="center_vertical"
android:orientation="horizontal"> android:orientation="horizontal">
<TextView <TextView
@ -41,9 +43,34 @@
android:layout_height="wrap_content" android:layout_height="wrap_content"
android:fontFamily="@font/poppins_semi_bold" android:fontFamily="@font/poppins_semi_bold"
android:text="Username" android:text="Username"
android:layout_gravity="center_vertical"
android:gravity="center_vertical"
android:textAlignment="gravity"
android:textSize="15sp" android:textSize="15sp"
android:paddingTop="1dp"
android:paddingBottom="0dp"
tools:ignore="HardcodedText,RtlSymmetry"/> tools:ignore="HardcodedText,RtlSymmetry"/>
<ImageView
android:id="@+id/modBadge"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:src="@drawable/ic_shield"
android:layout_gravity="top"
android:visibility="visible"
android:scaleX="0.8"
android:scaleY="0.8" />
<ImageView
android:id="@+id/adminBadge"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:src="@drawable/ic_crown"
android:layout_gravity="top"
android:visibility="visible"
android:scaleX="0.9"
android:scaleY="0.9" />
<TextView <TextView
android:id="@+id/commentUserTime" android:id="@+id/commentUserTime"
android:layout_width="wrap_content" android:layout_width="wrap_content"
@ -103,6 +130,17 @@
android:textSize="12sp" android:textSize="12sp"
android:layout_marginStart="12dp" android:layout_marginStart="12dp"
tools:ignore="HardcodedText" /> tools:ignore="HardcodedText" />
<TextView
android:id="@+id/commentBanUser"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:alpha="0.6"
android:fontFamily="@font/poppins_semi_bold"
android:text="Ban User"
android:textSize="12sp"
android:layout_marginStart="12dp"
tools:ignore="HardcodedText" />
</LinearLayout> </LinearLayout>
<TextView <TextView
android:id="@+id/commentTotalReplies" android:id="@+id/commentTotalReplies"

View file

@ -1,15 +1,15 @@
<?xml version="1.0" encoding="utf-8"?> <?xml version="1.0" encoding="utf-8"?>
<menu xmlns:android="http://schemas.android.com/apk/res/android"> <menu xmlns:android="http://schemas.android.com/apk/res/android">
<item <item
android:id="@+id/comment_sort_ascending" android:id="@+id/comment_sort_newest"
android:title="@string/comments_sort_ascending" /> android:title="@string/newest" />
<item <item
android:id="@+id/comment_sort_descending" android:id="@+id/comment_sort_oldest"
android:title="@string/comments_sort_descending" /> android:title="@string/oldest" />
<item <item
android:id="@+id/comment_sort_upvote" android:id="@+id/comment_sort_highest_rated"
android:title="@string/comments_sort_upvote" /> android:title="@string/highest_rated" />
<item <item
android:id="@+id/comment_sort_downvote" android:id="@+id/comment_sort_lowest_rated"
android:title="@string/comments_sort_downvote" /> android:title="@string/lowest_rated" />
</menu> </menu>

View file

@ -659,8 +659,8 @@
<string name="try_internal_cast_experimental">Try Internal Cast (Experimental)</string> <string name="try_internal_cast_experimental">Try Internal Cast (Experimental)</string>
<string name="comments">Comments</string> <string name="comments">Comments</string>
<string name="comments_sort_ascending">Ascending</string> <string name="newest">newest</string>
<string name="comments_sort_descending">Descending</string> <string name="oldest">oldest</string>
<string name="comments_sort_upvote">Most UpVoted</string> <string name="highest_rated">highest rated</string>
<string name="comments_sort_downvote">Most DownVoted</string> <string name="lowest_rated">lowest rated</string>
</resources> </resources>

View file

@ -91,5 +91,15 @@
<item name="android:background">@android:color/transparent</item> <item name="android:background">@android:color/transparent</item>
</style> </style>
<style name="CircularImageView" parent="">
<item name="shapeAppearanceOverlay">@style/ShapeAppearanceOverlay.App.CircularImageView</item>
</style>
<style name="ShapeAppearanceOverlay.App.CircularImageView" parent="">
<item name="cornerFamily">rounded</item>
<item name="cornerSize">50%</item>
</style>
<style name="ThemeOverlay_Dantotsu_MediaRouter" parent="ThemeOverlay.AppCompat"></style> <style name="ThemeOverlay_Dantotsu_MediaRouter" parent="ThemeOverlay.AppCompat"></style>
</resources> </resources>