feat: reviews in info page

This commit is contained in:
aayush262 2024-05-20 14:45:32 +05:30
parent 91f728150c
commit d12ddc9c0d
10 changed files with 97 additions and 50 deletions

View file

@ -75,7 +75,7 @@ class AnilistQueries {
media.cameFromContinue = false media.cameFromContinue = false
val query = val query =
"""{Media(id:${media.id}){id favourites popularity episodes chapters mediaListEntry{id status score(format:POINT_100)progress private notes repeat customLists updatedAt startedAt{year month day}completedAt{year month day}}isFavourite siteUrl idMal nextAiringEpisode{episode airingAt}source countryOfOrigin format duration season seasonYear startDate{year month day}endDate{year month day}genres studios(isMain:true){nodes{id name siteUrl}}description trailer{site id}synonyms tags{name rank isMediaSpoiler}characters(sort:[ROLE,FAVOURITES_DESC],perPage:25,page:1){edges{role voiceActors { id name { first middle last full native userPreferred } image { large medium } languageV2 } node{id image{medium}name{userPreferred}isFavourite}}}relations{edges{relationType(version:2)node{id idMal mediaListEntry{progress private score(format:POINT_100)status}episodes chapters nextAiringEpisode{episode}popularity meanScore isAdult isFavourite format title{english romaji userPreferred}type status(version:2)bannerImage coverImage{large}}}}staffPreview:staff(perPage:8,sort:[RELEVANCE,ID]){edges{role node{id image{large medium}name{userPreferred}}}}recommendations(sort:RATING_DESC){nodes{mediaRecommendation{id idMal mediaListEntry{progress private score(format:POINT_100)status}episodes chapters nextAiringEpisode{episode}meanScore isAdult isFavourite format title{english romaji userPreferred}type status(version:2)bannerImage coverImage{large}}}}externalLinks{url site}}Page(page:1){pageInfo{total perPage currentPage lastPage hasNextPage}mediaList(isFollowing:true,sort:[STATUS],mediaId:${media.id}){id status score(format: POINT_100) progress progressVolumes user{id name avatar{large medium}}}}}""" """{Media(id:${media.id}){id favourites popularity episodes chapters mediaListEntry{id status score(format:POINT_100)progress private notes repeat customLists updatedAt startedAt{year month day}completedAt{year month day}}reviews(perPage:3, sort:SCORE_DESC){nodes{id mediaId mediaType summary body(asHtml:true) rating ratingAmount userRating score private siteUrl createdAt updatedAt user{id name bannerImage avatar{medium large}}}}isFavourite siteUrl idMal nextAiringEpisode{episode airingAt}source countryOfOrigin format duration season seasonYear startDate{year month day}endDate{year month day}genres studios(isMain:true){nodes{id name siteUrl}}description trailer{site id}synonyms tags{name rank isMediaSpoiler}characters(sort:[ROLE,FAVOURITES_DESC],perPage:25,page:1){edges{role voiceActors { id name { first middle last full native userPreferred } image { large medium } languageV2 } node{id image{medium}name{userPreferred}isFavourite}}}relations{edges{relationType(version:2)node{id idMal mediaListEntry{progress private score(format:POINT_100)status}episodes chapters nextAiringEpisode{episode}popularity meanScore isAdult isFavourite format title{english romaji userPreferred}type status(version:2)bannerImage coverImage{large}}}}staffPreview:staff(perPage:8,sort:[RELEVANCE,ID]){edges{role node{id image{large medium}name{userPreferred}}}}recommendations(sort:RATING_DESC){nodes{mediaRecommendation{id idMal mediaListEntry{progress private score(format:POINT_100)status}episodes chapters nextAiringEpisode{episode}meanScore isAdult isFavourite format title{english romaji userPreferred}type status(version:2)bannerImage coverImage{large}}}}externalLinks{url site}}Page(page:1){pageInfo{total perPage currentPage lastPage hasNextPage}mediaList(isFollowing:true,sort:[STATUS],mediaId:${media.id}){id status score(format: POINT_100) progress progressVolumes user{id name avatar{large medium}}}}}"""
runBlocking { runBlocking {
val anilist = async { val anilist = async {
var response = executeQuery<Query.Media>(query, force = true) var response = executeQuery<Query.Media>(query, force = true)
@ -211,6 +211,9 @@ class AnilistQueries {
} }
} }
} }
if (fetchedMedia.reviews?.nodes != null){
media.review = fetchedMedia.reviews!!.nodes as ArrayList<Query.Review>
}
if (user?.mediaList?.isNotEmpty() == true) { if (user?.mediaList?.isNotEmpty() == true) {
media.users = user.mediaList?.mapNotNull { media.users = user.mediaList?.mapNotNull {
it.user?.let { user -> it.user?.let { user ->
@ -1505,7 +1508,7 @@ Page(page:$page,perPage:50) {
return author return author
} }
suspend fun getReviews(mediaId: Int, page: Int = 1, sort: String = "CREATED_AT_DESC"): Query.ReviewsResponse? { suspend fun getReviews(mediaId: Int, page: Int = 1, sort: String = "SCORE_DESC"): Query.ReviewsResponse? {
return executeQuery<Query.ReviewsResponse>( return executeQuery<Query.ReviewsResponse>(
"""{Page(page:$page,perPage:10){pageInfo{currentPage,hasNextPage,total}reviews(mediaId:$mediaId,sort:$sort){id,mediaId,mediaType,summary,body(asHtml:true)rating,ratingAmount,userRating,score,private,siteUrl,createdAt,updatedAt,user{id,name,bannerImage avatar{medium,large}}}}}""", """{Page(page:$page,perPage:10){pageInfo{currentPage,hasNextPage,total}reviews(mediaId:$mediaId,sort:$sort){id,mediaId,mediaType,summary,body(asHtml:true)rating,ratingAmount,userRating,score,private,siteUrl,createdAt,updatedAt,user{id,name,bannerImage avatar{medium,large}}}}}""",
force = true force = true

View file

@ -152,7 +152,7 @@ data class Media(
@SerialName("mediaListEntry") var mediaListEntry: MediaList?, @SerialName("mediaListEntry") var mediaListEntry: MediaList?,
// User reviews of the media // User reviews of the media
// @SerialName("reviews") var reviews: ReviewConnection?, @SerialName("reviews") var reviews: ReviewConnection?,
// User recommendations for similar media // User recommendations for similar media
@SerialName("recommendations") var recommendations: RecommendationConnection?, @SerialName("recommendations") var recommendations: RecommendationConnection?,
@ -537,4 +537,9 @@ data class MediaListGroup(
@SerialName("isSplitCompletedList") var isSplitCompletedList: Boolean?, @SerialName("isSplitCompletedList") var isSplitCompletedList: Boolean?,
@SerialName("status") var status: MediaListStatus?, @SerialName("status") var status: MediaListStatus?,
) : java.io.Serializable ) : java.io.Serializable
@Serializable
data class ReviewConnection(
@SerialName("nodes") var nodes: List<Query.Review>?,
)

View file

@ -5,6 +5,7 @@ import ani.dantotsu.connections.anilist.api.FuzzyDate
import ani.dantotsu.connections.anilist.api.MediaEdge import ani.dantotsu.connections.anilist.api.MediaEdge
import ani.dantotsu.connections.anilist.api.MediaList import ani.dantotsu.connections.anilist.api.MediaList
import ani.dantotsu.connections.anilist.api.MediaType import ani.dantotsu.connections.anilist.api.MediaType
import ani.dantotsu.connections.anilist.api.Query
import ani.dantotsu.media.anime.Anime import ani.dantotsu.media.anime.Anime
import ani.dantotsu.media.manga.Manga import ani.dantotsu.media.manga.Manga
import ani.dantotsu.profile.User import ani.dantotsu.profile.User
@ -62,6 +63,7 @@ data class Media(
var timeUntilAiring: Long? = null, var timeUntilAiring: Long? = null,
var characters: ArrayList<Character>? = null, var characters: ArrayList<Character>? = null,
var review: ArrayList<Query.Review>? = null,
var staff: ArrayList<Author>? = null, var staff: ArrayList<Author>? = null,
var prequel: Media? = null, var prequel: Media? = null,
var sequel: Media? = null, var sequel: Media? = null,

View file

@ -34,7 +34,6 @@ import ani.dantotsu.databinding.ItemChipBinding
import ani.dantotsu.databinding.ItemQuelsBinding import ani.dantotsu.databinding.ItemQuelsBinding
import ani.dantotsu.databinding.ItemTitleChipgroupBinding import ani.dantotsu.databinding.ItemTitleChipgroupBinding
import ani.dantotsu.databinding.ItemTitleRecyclerBinding import ani.dantotsu.databinding.ItemTitleRecyclerBinding
import ani.dantotsu.databinding.ItemTitleSearchBinding
import ani.dantotsu.databinding.ItemTitleTextBinding import ani.dantotsu.databinding.ItemTitleTextBinding
import ani.dantotsu.databinding.ItemTitleTrailerBinding import ani.dantotsu.databinding.ItemTitleTrailerBinding
import ani.dantotsu.displayTimer import ani.dantotsu.displayTimer
@ -46,6 +45,7 @@ import ani.dantotsu.px
import ani.dantotsu.setSafeOnClickListener import ani.dantotsu.setSafeOnClickListener
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 com.xwray.groupie.GroupieAdapter
import io.noties.markwon.Markwon import io.noties.markwon.Markwon
import io.noties.markwon.SoftBreakAddsNewLinePlugin import io.noties.markwon.SoftBreakAddsNewLinePlugin
import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.Dispatchers
@ -81,7 +81,8 @@ class MediaInfoFragment : Fragment() {
@SuppressLint("SetJavaScriptEnabled") @SuppressLint("SetJavaScriptEnabled")
override fun onViewCreated(view: View, savedInstanceState: Bundle?) { override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
val model: MediaDetailsViewModel by activityViewModels() val model: MediaDetailsViewModel by activityViewModels()
val offline: Boolean = PrefManager.getVal(PrefName.OfflineMode) || !isOnline(requireContext()) val offline: Boolean =
PrefManager.getVal(PrefName.OfflineMode) || !isOnline(requireContext())
binding.mediaInfoProgressBar.isGone = loaded binding.mediaInfoProgressBar.isGone = loaded
binding.mediaInfoContainer.isVisible = loaded binding.mediaInfoContainer.isVisible = loaded
binding.mediaInfoContainer.updateLayoutParams<ViewGroup.MarginLayoutParams> { bottomMargin += 128f.px + navBarHeight } binding.mediaInfoContainer.updateLayoutParams<ViewGroup.MarginLayoutParams> { bottomMargin += 128f.px + navBarHeight }
@ -254,7 +255,8 @@ class MediaInfoFragment : Fragment() {
if (!media.users.isNullOrEmpty() && !offline) { if (!media.users.isNullOrEmpty() && !offline) {
val users: ArrayList<User> = media.users ?: arrayListOf() val users: ArrayList<User> = media.users ?: arrayListOf()
if (Anilist.token != null && media.userStatus != null) { if (Anilist.token != null && media.userStatus != null) {
users.add(0, users.add(
0,
User( User(
id = Anilist.userid!!, id = Anilist.userid!!,
name = getString(R.string.you), name = getString(R.string.you),
@ -263,7 +265,8 @@ class MediaInfoFragment : Fragment() {
status = media.userStatus, status = media.userStatus,
score = media.userScore.toFloat(), score = media.userScore.toFloat(),
progress = media.userProgress, progress = media.userProgress,
totalEpisodes = media.anime?.totalEpisodes ?: media.manga?.totalChapters, totalEpisodes = media.anime?.totalEpisodes
?: media.manga?.totalChapters,
nextAiringEpisode = media.anime?.nextAiringEpisode nextAiringEpisode = media.anime?.nextAiringEpisode
) )
) )
@ -519,22 +522,41 @@ class MediaInfoFragment : Fragment() {
} }
} }
ItemTitleSearchBinding.inflate( if (!media.review.isNullOrEmpty()) {
LayoutInflater.from(context), ItemTitleRecyclerBinding.inflate(
parent, LayoutInflater.from(context),
false parent,
).apply { false
).apply {
titleSearchImage.loadImage(media.banner ?: media.cover) fun onUserClick(userId: Int) {
titleSearchText.text = val review = media.review!!.find { i -> i.id == userId }
getString(R.string.reviews) if (review != null) {
titleSearchCard.setSafeOnClickListener { startActivity(
val query = Intent(requireContext(), ReviewActivity::class.java) Intent(requireContext(), ReviewViewActivity::class.java)
.putExtra("mediaId", media.id) .putExtra("review", review)
ContextCompat.startActivity(requireContext(), query, null) )
}
}
val adapter = GroupieAdapter()
media.review!!.forEach {
adapter.add(ReviewAdapter(it, ::onUserClick))
}
itemTitle.setText(R.string.reviews)
itemRecycler.adapter = adapter
itemRecycler.layoutManager = LinearLayoutManager(
requireContext(),
LinearLayoutManager.VERTICAL,
false
)
itemMore.visibility = View.VISIBLE
itemMore.setSafeOnClickListener {
startActivity(
Intent(requireContext(), ReviewActivity::class.java)
.putExtra("mediaId", media.id)
)
}
parent.addView(root)
} }
parent.addView(root)
} }
ItemTitleRecyclerBinding.inflate( ItemTitleRecyclerBinding.inflate(

View file

@ -125,7 +125,6 @@ class ReviewActivity : AppCompatActivity() {
adapter.add( adapter.add(
ReviewAdapter( ReviewAdapter(
it, it,
this,
this::onUserClick this::onUserClick
) )
) )

View file

@ -1,30 +1,24 @@
package ani.dantotsu.media package ani.dantotsu.media
import android.content.Context
import android.text.SpannableString
import android.view.View import android.view.View
import androidx.lifecycle.lifecycleScope
import ani.dantotsu.R import ani.dantotsu.R
import ani.dantotsu.blurImage
import ani.dantotsu.connections.anilist.Anilist import ani.dantotsu.connections.anilist.Anilist
import ani.dantotsu.connections.anilist.api.Query import ani.dantotsu.connections.anilist.api.Query
import ani.dantotsu.databinding.ItemFollowerBinding
import ani.dantotsu.databinding.ItemReviewsBinding import ani.dantotsu.databinding.ItemReviewsBinding
import ani.dantotsu.getThemeColor
import ani.dantotsu.loadImage import ani.dantotsu.loadImage
import ani.dantotsu.profile.activity.ActivityItemBuilder import ani.dantotsu.profile.activity.ActivityItemBuilder
import ani.dantotsu.toast import ani.dantotsu.toast
import com.xwray.groupie.viewbinding.BindableItem import com.xwray.groupie.viewbinding.BindableItem
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.SupervisorJob
import kotlinx.coroutines.launch import kotlinx.coroutines.launch
import kotlinx.coroutines.withContext import kotlinx.coroutines.withContext
class ReviewAdapter( class ReviewAdapter(
private var review: Query.Review, private var review: Query.Review,
val context: ReviewActivity,
val clickCallback: (Int) -> Unit val clickCallback: (Int) -> Unit
) : BindableItem<ItemReviewsBinding>() { ) : BindableItem<ItemReviewsBinding>() {
private lateinit var binding: ItemReviewsBinding private lateinit var binding: ItemReviewsBinding
@ -34,7 +28,8 @@ class ReviewAdapter(
binding.reviewUserAvatar.loadImage(review.user?.avatar?.medium) binding.reviewUserAvatar.loadImage(review.user?.avatar?.medium)
binding.reviewText.text = review.summary binding.reviewText.text = review.summary
binding.reviewPostTime.text = ActivityItemBuilder.getDateTime(review.createdAt) binding.reviewPostTime.text = ActivityItemBuilder.getDateTime(review.createdAt)
binding.reviewTag.text = "[${review.score}]" val text = "[${review.score/ 10.0f}]"
binding.reviewTag.text = text
binding.root.setOnClickListener { clickCallback(review.id) } binding.root.setOnClickListener { clickCallback(review.id) }
userVote(review.userRating) userVote(review.userRating)
enableVote() enableVote()
@ -75,7 +70,8 @@ class ReviewAdapter(
private fun rateReview(rating: String) { private fun rateReview(rating: String) {
disableVote() disableVote()
context.lifecycleScope.launch { val scope = CoroutineScope(Dispatchers.IO + SupervisorJob())
scope.launch {
val result = Anilist.mutation.rateReview(review.id, rating) val result = Anilist.mutation.rateReview(review.id, rating)
if (result != null) { if (result != null) {
withContext(Dispatchers.Main) { withContext(Dispatchers.Main) {
@ -91,7 +87,7 @@ class ReviewAdapter(
} else { } else {
withContext(Dispatchers.Main) { withContext(Dispatchers.Main) {
toast( toast(
context.getString(R.string.error_message, "response is null") binding.root.context.getString(R.string.error_message, "response is null")
) )
enableVote() enableVote()
} }

View file

@ -27,7 +27,6 @@
android:id="@+id/activityUserAvatar" android:id="@+id/activityUserAvatar"
android:layout_width="42dp" android:layout_width="42dp"
android:layout_height="42dp" android:layout_height="42dp"
android:layout_gravity="center"
app:srcCompat="@drawable/ic_round_add_circle_24" app:srcCompat="@drawable/ic_round_add_circle_24"
tools:ignore="ContentDescription,ImageContrastCheck" tools:ignore="ContentDescription,ImageContrastCheck"
tools:tint="@color/transparent" /> tools:tint="@color/transparent" />

View file

@ -12,14 +12,16 @@
android:layout_width="wrap_content" android:layout_width="wrap_content"
android:layout_height="wrap_content" android:layout_height="wrap_content"
android:layout_gravity="center" android:layout_gravity="center"
android:backgroundTint="@color/bg_white" android:backgroundTint="@color/transparent"
app:strokeColor="@color/transparent"
app:cardCornerRadius="124dp"> app:cardCornerRadius="124dp">
<ImageView <com.google.android.material.imageview.ShapeableImageView
android:id="@+id/profileUserAvatar" android:id="@+id/profileUserAvatar"
android:layout_width="100dp" android:layout_width="92dp"
android:layout_height="100dp" android:layout_height="92dp"
tools:ignore="ContentDescription,ImageContrastCheck" app:srcCompat="@drawable/ic_round_add_circle_24"
tools:ignore="ContentDescription"
tools:tint="@color/transparent" /> tools:tint="@color/transparent" />
<LinearLayout <LinearLayout

View file

@ -24,7 +24,7 @@
android:layout_width="wrap_content" android:layout_width="wrap_content"
android:layout_height="wrap_content" android:layout_height="wrap_content"
android:layout_gravity="center" android:layout_gravity="center"
android:layout_marginTop="12dp"
android:backgroundTint="@color/transparent" android:backgroundTint="@color/transparent"
app:cardCornerRadius="64dp" app:cardCornerRadius="64dp"
app:strokeColor="@color/transparent"> app:strokeColor="@color/transparent">

View file

@ -5,16 +5,35 @@
android:layout_height="match_parent" android:layout_height="match_parent"
android:orientation="vertical"> android:orientation="vertical">
<TextView
android:id="@+id/itemTitle" <LinearLayout
android:layout_width="match_parent" android:layout_width="match_parent"
android:layout_height="wrap_content" android:layout_height="wrap_content"
android:layout_marginStart="32dp" android:orientation="horizontal"
android:layout_marginTop="8dp" tools:ignore="UseCompoundDrawables">
android:layout_marginEnd="32dp"
android:fontFamily="@font/poppins_bold" <TextView
android:text="@string/relations" android:id="@+id/itemTitle"
android:textSize="16sp" /> android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_marginStart="24dp"
android:layout_weight="1"
android:fontFamily="@font/poppins_bold"
android:padding="8dp"
android:text="@string/relations"
android:textSize="16sp" />
<ImageView
android:id="@+id/itemMore"
android:layout_width="wrap_content"
android:layout_height="match_parent"
android:layout_marginEnd="16dp"
android:fontFamily="@font/poppins_bold"
android:padding="8dp"
android:visibility="gone"
android:src="@drawable/arrow_mark"
android:textSize="16sp"
tools:ignore="ContentDescription" />
</LinearLayout>
<ani.dantotsu.FadingEdgeRecyclerView <ani.dantotsu.FadingEdgeRecyclerView
android:id="@+id/itemRecycler" android:id="@+id/itemRecycler"