From d12ddc9c0d8bb3c9d3a72647c6bcd56d5a70101c Mon Sep 17 00:00:00 2001 From: aayush262 Date: Mon, 20 May 2024 14:45:32 +0530 Subject: [PATCH] feat: reviews in info page --- .../connections/anilist/AnilistQueries.kt | 7 ++- .../dantotsu/connections/anilist/api/Media.kt | 9 ++- app/src/main/java/ani/dantotsu/media/Media.kt | 2 + .../ani/dantotsu/media/MediaInfoFragment.kt | 60 +++++++++++++------ .../java/ani/dantotsu/media/ReviewActivity.kt | 1 - .../java/ani/dantotsu/media/ReviewAdapter.kt | 18 +++--- app/src/main/res/layout/item_activity.xml | 1 - .../main/res/layout/item_follower_grid.xml | 12 ++-- app/src/main/res/layout/item_reviews.xml | 2 +- .../main/res/layout/item_title_recycler.xml | 35 ++++++++--- 10 files changed, 97 insertions(+), 50 deletions(-) diff --git a/app/src/main/java/ani/dantotsu/connections/anilist/AnilistQueries.kt b/app/src/main/java/ani/dantotsu/connections/anilist/AnilistQueries.kt index c77cf105..168eb619 100644 --- a/app/src/main/java/ani/dantotsu/connections/anilist/AnilistQueries.kt +++ b/app/src/main/java/ani/dantotsu/connections/anilist/AnilistQueries.kt @@ -75,7 +75,7 @@ class AnilistQueries { media.cameFromContinue = false 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 { val anilist = async { var response = executeQuery(query, force = true) @@ -211,6 +211,9 @@ class AnilistQueries { } } } + if (fetchedMedia.reviews?.nodes != null){ + media.review = fetchedMedia.reviews!!.nodes as ArrayList + } if (user?.mediaList?.isNotEmpty() == true) { media.users = user.mediaList?.mapNotNull { it.user?.let { user -> @@ -1505,7 +1508,7 @@ Page(page:$page,perPage:50) { 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( """{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 diff --git a/app/src/main/java/ani/dantotsu/connections/anilist/api/Media.kt b/app/src/main/java/ani/dantotsu/connections/anilist/api/Media.kt index b7a5134b..23c79045 100644 --- a/app/src/main/java/ani/dantotsu/connections/anilist/api/Media.kt +++ b/app/src/main/java/ani/dantotsu/connections/anilist/api/Media.kt @@ -152,7 +152,7 @@ data class Media( @SerialName("mediaListEntry") var mediaListEntry: MediaList?, // User reviews of the media - // @SerialName("reviews") var reviews: ReviewConnection?, + @SerialName("reviews") var reviews: ReviewConnection?, // User recommendations for similar media @SerialName("recommendations") var recommendations: RecommendationConnection?, @@ -537,4 +537,9 @@ data class MediaListGroup( @SerialName("isSplitCompletedList") var isSplitCompletedList: Boolean?, @SerialName("status") var status: MediaListStatus?, -) : java.io.Serializable \ No newline at end of file +) : java.io.Serializable + +@Serializable +data class ReviewConnection( + @SerialName("nodes") var nodes: List?, +) \ No newline at end of file diff --git a/app/src/main/java/ani/dantotsu/media/Media.kt b/app/src/main/java/ani/dantotsu/media/Media.kt index 92847073..a935dd4a 100644 --- a/app/src/main/java/ani/dantotsu/media/Media.kt +++ b/app/src/main/java/ani/dantotsu/media/Media.kt @@ -5,6 +5,7 @@ import ani.dantotsu.connections.anilist.api.FuzzyDate import ani.dantotsu.connections.anilist.api.MediaEdge import ani.dantotsu.connections.anilist.api.MediaList import ani.dantotsu.connections.anilist.api.MediaType +import ani.dantotsu.connections.anilist.api.Query import ani.dantotsu.media.anime.Anime import ani.dantotsu.media.manga.Manga import ani.dantotsu.profile.User @@ -62,6 +63,7 @@ data class Media( var timeUntilAiring: Long? = null, var characters: ArrayList? = null, + var review: ArrayList? = null, var staff: ArrayList? = null, var prequel: Media? = null, var sequel: Media? = null, diff --git a/app/src/main/java/ani/dantotsu/media/MediaInfoFragment.kt b/app/src/main/java/ani/dantotsu/media/MediaInfoFragment.kt index 671c96ce..6cafbc05 100644 --- a/app/src/main/java/ani/dantotsu/media/MediaInfoFragment.kt +++ b/app/src/main/java/ani/dantotsu/media/MediaInfoFragment.kt @@ -34,7 +34,6 @@ import ani.dantotsu.databinding.ItemChipBinding import ani.dantotsu.databinding.ItemQuelsBinding import ani.dantotsu.databinding.ItemTitleChipgroupBinding import ani.dantotsu.databinding.ItemTitleRecyclerBinding -import ani.dantotsu.databinding.ItemTitleSearchBinding import ani.dantotsu.databinding.ItemTitleTextBinding import ani.dantotsu.databinding.ItemTitleTrailerBinding import ani.dantotsu.displayTimer @@ -46,6 +45,7 @@ import ani.dantotsu.px import ani.dantotsu.setSafeOnClickListener import ani.dantotsu.settings.saving.PrefManager import ani.dantotsu.settings.saving.PrefName +import com.xwray.groupie.GroupieAdapter import io.noties.markwon.Markwon import io.noties.markwon.SoftBreakAddsNewLinePlugin import kotlinx.coroutines.Dispatchers @@ -81,7 +81,8 @@ class MediaInfoFragment : Fragment() { @SuppressLint("SetJavaScriptEnabled") override fun onViewCreated(view: View, savedInstanceState: Bundle?) { 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.mediaInfoContainer.isVisible = loaded binding.mediaInfoContainer.updateLayoutParams { bottomMargin += 128f.px + navBarHeight } @@ -254,7 +255,8 @@ class MediaInfoFragment : Fragment() { if (!media.users.isNullOrEmpty() && !offline) { val users: ArrayList = media.users ?: arrayListOf() if (Anilist.token != null && media.userStatus != null) { - users.add(0, + users.add( + 0, User( id = Anilist.userid!!, name = getString(R.string.you), @@ -263,7 +265,8 @@ class MediaInfoFragment : Fragment() { status = media.userStatus, score = media.userScore.toFloat(), progress = media.userProgress, - totalEpisodes = media.anime?.totalEpisodes ?: media.manga?.totalChapters, + totalEpisodes = media.anime?.totalEpisodes + ?: media.manga?.totalChapters, nextAiringEpisode = media.anime?.nextAiringEpisode ) ) @@ -519,22 +522,41 @@ class MediaInfoFragment : Fragment() { } } - ItemTitleSearchBinding.inflate( - LayoutInflater.from(context), - parent, - false - ).apply { - - titleSearchImage.loadImage(media.banner ?: media.cover) - titleSearchText.text = - getString(R.string.reviews) - titleSearchCard.setSafeOnClickListener { - val query = Intent(requireContext(), ReviewActivity::class.java) - .putExtra("mediaId", media.id) - ContextCompat.startActivity(requireContext(), query, null) + if (!media.review.isNullOrEmpty()) { + ItemTitleRecyclerBinding.inflate( + LayoutInflater.from(context), + parent, + false + ).apply { + fun onUserClick(userId: Int) { + val review = media.review!!.find { i -> i.id == userId } + if (review != null) { + startActivity( + Intent(requireContext(), ReviewViewActivity::class.java) + .putExtra("review", review) + ) + } + } + 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( diff --git a/app/src/main/java/ani/dantotsu/media/ReviewActivity.kt b/app/src/main/java/ani/dantotsu/media/ReviewActivity.kt index 8f3c3b12..caefe07b 100644 --- a/app/src/main/java/ani/dantotsu/media/ReviewActivity.kt +++ b/app/src/main/java/ani/dantotsu/media/ReviewActivity.kt @@ -125,7 +125,6 @@ class ReviewActivity : AppCompatActivity() { adapter.add( ReviewAdapter( it, - this, this::onUserClick ) ) diff --git a/app/src/main/java/ani/dantotsu/media/ReviewAdapter.kt b/app/src/main/java/ani/dantotsu/media/ReviewAdapter.kt index 1f8034d3..e88238c7 100644 --- a/app/src/main/java/ani/dantotsu/media/ReviewAdapter.kt +++ b/app/src/main/java/ani/dantotsu/media/ReviewAdapter.kt @@ -1,30 +1,24 @@ package ani.dantotsu.media -import android.content.Context -import android.text.SpannableString import android.view.View -import androidx.lifecycle.lifecycleScope import ani.dantotsu.R -import ani.dantotsu.blurImage import ani.dantotsu.connections.anilist.Anilist import ani.dantotsu.connections.anilist.api.Query -import ani.dantotsu.databinding.ItemFollowerBinding import ani.dantotsu.databinding.ItemReviewsBinding -import ani.dantotsu.getThemeColor import ani.dantotsu.loadImage import ani.dantotsu.profile.activity.ActivityItemBuilder import ani.dantotsu.toast import com.xwray.groupie.viewbinding.BindableItem +import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.SupervisorJob import kotlinx.coroutines.launch import kotlinx.coroutines.withContext class ReviewAdapter( private var review: Query.Review, - val context: ReviewActivity, val clickCallback: (Int) -> Unit - ) : BindableItem() { private lateinit var binding: ItemReviewsBinding @@ -34,7 +28,8 @@ class ReviewAdapter( binding.reviewUserAvatar.loadImage(review.user?.avatar?.medium) binding.reviewText.text = review.summary 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) } userVote(review.userRating) enableVote() @@ -75,7 +70,8 @@ class ReviewAdapter( private fun rateReview(rating: String) { disableVote() - context.lifecycleScope.launch { + val scope = CoroutineScope(Dispatchers.IO + SupervisorJob()) + scope.launch { val result = Anilist.mutation.rateReview(review.id, rating) if (result != null) { withContext(Dispatchers.Main) { @@ -91,7 +87,7 @@ class ReviewAdapter( } else { withContext(Dispatchers.Main) { toast( - context.getString(R.string.error_message, "response is null") + binding.root.context.getString(R.string.error_message, "response is null") ) enableVote() } diff --git a/app/src/main/res/layout/item_activity.xml b/app/src/main/res/layout/item_activity.xml index b324c3ba..ccbdb634 100644 --- a/app/src/main/res/layout/item_activity.xml +++ b/app/src/main/res/layout/item_activity.xml @@ -27,7 +27,6 @@ android:id="@+id/activityUserAvatar" android:layout_width="42dp" android:layout_height="42dp" - android:layout_gravity="center" app:srcCompat="@drawable/ic_round_add_circle_24" tools:ignore="ContentDescription,ImageContrastCheck" tools:tint="@color/transparent" /> diff --git a/app/src/main/res/layout/item_follower_grid.xml b/app/src/main/res/layout/item_follower_grid.xml index 3655ff88..04bc4c70 100644 --- a/app/src/main/res/layout/item_follower_grid.xml +++ b/app/src/main/res/layout/item_follower_grid.xml @@ -12,14 +12,16 @@ android:layout_width="wrap_content" android:layout_height="wrap_content" android:layout_gravity="center" - android:backgroundTint="@color/bg_white" + android:backgroundTint="@color/transparent" + app:strokeColor="@color/transparent" app:cardCornerRadius="124dp"> - diff --git a/app/src/main/res/layout/item_title_recycler.xml b/app/src/main/res/layout/item_title_recycler.xml index ae659bc7..3961d479 100644 --- a/app/src/main/res/layout/item_title_recycler.xml +++ b/app/src/main/res/layout/item_title_recycler.xml @@ -5,16 +5,35 @@ android:layout_height="match_parent" android:orientation="vertical"> - + android:orientation="horizontal" + tools:ignore="UseCompoundDrawables"> + + + +