From 89b6f28b9ffe6d64f36f94d1a7dca0bbfcb15116 Mon Sep 17 00:00:00 2001 From: aayush262 Date: Tue, 5 Mar 2024 17:10:04 +0530 Subject: [PATCH] feat(profile): added fav characters and staff --- .../connections/anilist/AnilistQueries.kt | 124 ++++++++++-------- .../connections/anilist/AnilistViewModel.kt | 23 ++-- .../dantotsu/connections/anilist/api/Data.kt | 34 ++--- .../ani/dantotsu/profile/ProfileFragment.kt | 53 ++++++-- .../settings/PlayerSettingsActivity.kt | 2 +- app/src/main/res/layout/fragment_profile.xml | 65 ++++++++- 6 files changed, 212 insertions(+), 89 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 98c0f55c..0f115832 100644 --- a/app/src/main/java/ani/dantotsu/connections/anilist/AnilistQueries.kt +++ b/app/src/main/java/ani/dantotsu/connections/anilist/AnilistQueries.kt @@ -32,13 +32,6 @@ import kotlin.system.measureTimeMillis class AnilistQueries { - suspend fun toggleFollow(id: Int): Query.ToggleFollow? { - val response = executeQuery( - """mutation{ToggleFollow(userId:$id){id, isFollowing, isFollower}}""" - ) - return response - } - suspend fun getUserData(): Boolean { val response: Query.Viewer? measureTimeMillis { @@ -59,47 +52,6 @@ class AnilistQueries { return true } - suspend fun getUserProfile(id: Int): Query.UserProfileResponse? { - return executeQuery( - """{user:User(id:$id){id,name,about(asHtml:true)avatar{medium,large},bannerImage,isFollowing,isFollower,isBlocked,favourites{anime{nodes{id,coverImage{extraLarge,large,medium,color}}}manga{nodes{id,coverImage{extraLarge,large,medium,color}}}characters{nodes{id,image{large,medium}}}staff{nodes{id,image{large,medium}}}studios{nodes{id,name}}}statistics{anime{count,meanScore,standardDeviation,minutesWatched,episodesWatched,chaptersRead,volumesRead}manga{count,meanScore,standardDeviation,minutesWatched,episodesWatched,chaptersRead,volumesRead}}siteUrl}}""", - force = true - ) - } - - suspend fun getUserStatistics(id: Int, sort: String = "ID"): Query.StatisticsResponse? { - return executeQuery( - """{User(id:$id){id name mediaListOptions{scoreFormat}statistics{anime{...UserStatistics}manga{...UserStatistics}}}}fragment UserStatistics on UserStatistics{count meanScore standardDeviation minutesWatched episodesWatched chaptersRead volumesRead formats(sort:$sort){count meanScore minutesWatched chaptersRead mediaIds format}statuses(sort:$sort){count meanScore minutesWatched chaptersRead mediaIds status}scores(sort:$sort){count meanScore minutesWatched chaptersRead mediaIds score}lengths(sort:$sort){count meanScore minutesWatched chaptersRead mediaIds length}releaseYears(sort:$sort){count meanScore minutesWatched chaptersRead mediaIds releaseYear}startYears(sort:$sort){count meanScore minutesWatched chaptersRead mediaIds startYear}genres(sort:$sort){count meanScore minutesWatched chaptersRead mediaIds genre}tags(sort:$sort){count meanScore minutesWatched chaptersRead mediaIds tag{id name}}countries(sort:$sort){count meanScore minutesWatched chaptersRead mediaIds country}voiceActors(sort:$sort){count meanScore minutesWatched chaptersRead mediaIds voiceActor{id name{first middle last full native alternative userPreferred}}characterIds}staff(sort:$sort){count meanScore minutesWatched chaptersRead mediaIds staff{id name{first middle last full native alternative userPreferred}}}studios(sort:$sort){count meanScore minutesWatched chaptersRead mediaIds studio{id name isAnimationStudio}}}""", - force = true, - show = true - ) - } - suspend fun userFavMedia(anime: Boolean, id: Int): ArrayList { - var hasNextPage = true - var page = 0 - - suspend fun getNextPage(page: Int): List { - val response = executeQuery("""{${userFavMediaQuery(anime, page, id)}}""") - val favourites = response?.data?.user?.favourites - val apiMediaList = if (anime) favourites?.anime else favourites?.manga - hasNextPage = apiMediaList?.pageInfo?.hasNextPage ?: false - return apiMediaList?.edges?.mapNotNull { - it.node?.let { i -> - Media(i).apply { isFav = true } - } - } ?: return listOf() - } - - val responseArray = arrayListOf() - while (hasNextPage) { - page++ - responseArray.addAll(getNextPage(page)) - } - return responseArray - } - private fun userFavMediaQuery(anime: Boolean, page: Int, id: Int): String { - return """User(id:${id}){id favourites{${if (anime) "anime" else "manga"}(page:$page){pageInfo{hasNextPage}edges{favouriteOrder node{id idMal isAdult mediaListEntry{ progress private score(format:POINT_100) status } chapters isFavourite format episodes nextAiringEpisode{episode}meanScore isFavourite format startDate{year month day} title{english romaji userPreferred}type status(version:2)bannerImage coverImage{large}}}}}}""" - } - suspend fun getMedia(id: Int, mal: Boolean = false): Media? { val response = executeQuery( """{Media(${if (!mal) "id:" else "idMal:"}$id){id idMal status chapters episodes nextAiringEpisode{episode}type meanScore isAdult isFavourite format bannerImage coverImage{large}title{english romaji userPreferred}mediaListEntry{progress private score(format:POINT_100)status}}}""", @@ -368,12 +320,12 @@ class AnilistQueries { return """ MediaListCollection(userId: ${Anilist.userid}, type: $type, status: $status , sort: UPDATED_TIME ) { lists { entries { progress private score(format:POINT_100) status media { id idMal type isAdult status chapters episodes nextAiringEpisode {episode} meanScore isFavourite format bannerImage coverImage{large} title { english romaji userPreferred } } } } } """ } - suspend fun favMedia(anime: Boolean): ArrayList { + suspend fun favMedia(anime: Boolean, id: Int? = Anilist.userid ): ArrayList { var hasNextPage = true var page = 0 suspend fun getNextPage(page: Int): List { - val response = executeQuery("""{${favMediaQuery(anime, page)}}""") + val response = executeQuery("""{${favMediaQuery(anime, page, id)}}""") val favourites = response?.data?.user?.favourites val apiMediaList = if (anime) favourites?.anime else favourites?.manga hasNextPage = apiMediaList?.pageInfo?.hasNextPage ?: false @@ -392,8 +344,8 @@ class AnilistQueries { return responseArray } - private fun favMediaQuery(anime: Boolean, page: Int): String { - return """User(id:${Anilist.userid}){id favourites{${if (anime) "anime" else "manga"}(page:$page){pageInfo{hasNextPage}edges{favouriteOrder node{id idMal isAdult mediaListEntry{ progress private score(format:POINT_100) status } chapters isFavourite format episodes nextAiringEpisode{episode}meanScore isFavourite format startDate{year month day} title{english romaji userPreferred}type status(version:2)bannerImage coverImage{large}}}}}}""" + private fun favMediaQuery(anime: Boolean, page: Int, id: Int?= Anilist.userid ): String { + return """User(id:${id}){id favourites{${if (anime) "anime" else "manga"}(page:$page){pageInfo{hasNextPage}edges{favouriteOrder node{id idMal isAdult mediaListEntry{ progress private score(format:POINT_100) status } chapters isFavourite format episodes nextAiringEpisode{episode}meanScore isFavourite format startDate{year month day} title{english romaji userPreferred}type status(version:2)bannerImage coverImage{large}}}}}}""" } suspend fun recommendations(): ArrayList { @@ -676,7 +628,7 @@ class AnilistQueries { if (!sorted.containsKey(it.key)) sorted[it.key] = it.value } - sorted["Favourites"] = favMedia(anime) + sorted["Favourites"] = favMedia(anime, userId) sorted["Favourites"]?.sortWith(compareBy { it.userFavOrder }) //favMedia doesn't fill userProgress, so we need to fill it manually by searching :( sorted["Favourites"]?.forEach { fav -> @@ -1281,4 +1233,70 @@ Page(page:$page,perPage:50) { return author } + suspend fun toggleFollow(id: Int): Query.ToggleFollow? { + val response = executeQuery( + """mutation{ToggleFollow(userId:$id){id, isFollowing, isFollower}}""" + ) + return response + } + + suspend fun getUserProfile(id: Int): Query.UserProfileResponse? { + return executeQuery( + """{user:User(id:$id){id,name,about(asHtml:true)avatar{medium,large},bannerImage,isFollowing,isFollower,isBlocked,favourites{anime{nodes{id,coverImage{extraLarge,large,medium,color}}}manga{nodes{id,coverImage{extraLarge,large,medium,color}}}characters{nodes{id,name{first,middle,last,full,native,alternative,userPreferred},image{large,medium}}}staff{nodes{id,name{first,middle,last,full,native,alternative,userPreferred},image{large,medium}}}studios{nodes{id,name}}}statistics{anime{count,meanScore,standardDeviation,minutesWatched,episodesWatched,chaptersRead,volumesRead}manga{count,meanScore,standardDeviation,minutesWatched,episodesWatched,chaptersRead,volumesRead}}siteUrl}}""", + force = true + ) + } + + suspend fun getUserStatistics(id: Int, sort: String = "ID"): Query.StatisticsResponse? { + return executeQuery( + """{User(id:$id){id name mediaListOptions{scoreFormat}statistics{anime{...UserStatistics}manga{...UserStatistics}}}}fragment UserStatistics on UserStatistics{count meanScore standardDeviation minutesWatched episodesWatched chaptersRead volumesRead formats(sort:$sort){count meanScore minutesWatched chaptersRead mediaIds format}statuses(sort:$sort){count meanScore minutesWatched chaptersRead mediaIds status}scores(sort:$sort){count meanScore minutesWatched chaptersRead mediaIds score}lengths(sort:$sort){count meanScore minutesWatched chaptersRead mediaIds length}releaseYears(sort:$sort){count meanScore minutesWatched chaptersRead mediaIds releaseYear}startYears(sort:$sort){count meanScore minutesWatched chaptersRead mediaIds startYear}genres(sort:$sort){count meanScore minutesWatched chaptersRead mediaIds genre}tags(sort:$sort){count meanScore minutesWatched chaptersRead mediaIds tag{id name}}countries(sort:$sort){count meanScore minutesWatched chaptersRead mediaIds country}voiceActors(sort:$sort){count meanScore minutesWatched chaptersRead mediaIds voiceActor{id name{first middle last full native alternative userPreferred}}characterIds}staff(sort:$sort){count meanScore minutesWatched chaptersRead mediaIds staff{id name{first middle last full native alternative userPreferred}}}studios(sort:$sort){count meanScore minutesWatched chaptersRead mediaIds studio{id name isAnimationStudio}}}""", + force = true, + show = true + ) + } + suspend fun userFavMedia(anime: Boolean, id: Int): ArrayList { + var hasNextPage = true + var page = 0 + + suspend fun getNextPage(page: Int): List { + val response = executeQuery("""{${userFavMediaQuery(anime, page, id)}}""") + val favourites = response?.data?.user?.favourites + val apiMediaList = if (anime) favourites?.anime else favourites?.manga + hasNextPage = apiMediaList?.pageInfo?.hasNextPage ?: false + return apiMediaList?.edges?.mapNotNull { + it.node?.let { i -> + Media(i).apply { isFav = true } + } + } ?: return listOf() + } + + val responseArray = arrayListOf() + while (hasNextPage) { + page++ + responseArray.addAll(getNextPage(page)) + } + return responseArray + } + private fun userFavMediaQuery(anime: Boolean, page: Int, id: Int): String { + return """User(id:${id}){id favourites{${if (anime) "anime" else "manga"}(page:$page){pageInfo{hasNextPage}edges{favouriteOrder node{id idMal isAdult mediaListEntry{ progress private score(format:POINT_100) status } chapters isFavourite format episodes nextAiringEpisode{episode}meanScore isFavourite format startDate{year month day} title{english romaji userPreferred}type status(version:2)bannerImage coverImage{large}}}}}}""" + } + private suspend fun userBannerImage(type: String,id: Int?): String? { + val response = + executeQuery("""{ MediaListCollection(userId: ${id}, type: $type, chunk:1,perChunk:25, sort: [SCORE_DESC,UPDATED_TIME_DESC]) { lists { entries{ media { id bannerImage } } } } } """) + val random = response?.data?.mediaListCollection?.lists?.mapNotNull { + it.entries?.mapNotNull { entry -> + val imageUrl = entry.media?.bannerImage + if (imageUrl != null && imageUrl != "null") imageUrl + else null + } + }?.flatten()?.randomOrNull() ?: return null + return random + } + + suspend fun getUserBannerImages(id: Int? = Anilist.userid): ArrayList { + val default = arrayListOf(null, null) + default[0] = userBannerImage("ANIME", id) + default[1] = userBannerImage("MANGA",id) + return default + } } \ No newline at end of file diff --git a/app/src/main/java/ani/dantotsu/connections/anilist/AnilistViewModel.kt b/app/src/main/java/ani/dantotsu/connections/anilist/AnilistViewModel.kt index ce25b539..9fb04d02 100644 --- a/app/src/main/java/ani/dantotsu/connections/anilist/AnilistViewModel.kt +++ b/app/src/main/java/ani/dantotsu/connections/anilist/AnilistViewModel.kt @@ -10,6 +10,7 @@ import ani.dantotsu.R import ani.dantotsu.connections.comments.CommentsAPI import ani.dantotsu.connections.discord.Discord import ani.dantotsu.connections.mal.MAL +import ani.dantotsu.media.Character import ani.dantotsu.media.Media import ani.dantotsu.others.AppUpdater import ani.dantotsu.settings.saving.PrefManager @@ -334,18 +335,24 @@ class GenresViewModel : ViewModel() { } } -class ProfileViewModel : ViewModel(){ - private val animeFav: MutableLiveData> = - MutableLiveData>(null) - fun getAnimeFav(): LiveData> = animeFav - suspend fun setAnimeFav(id: Int) { - animeFav.postValue(Anilist.query.userFavMedia(true, id)) - } +class ProfileViewModel : ViewModel(){ private val mangaFav: MutableLiveData> = MutableLiveData>(null) fun getMangaFav(): LiveData> = mangaFav - suspend fun setMangaFav(id: Int) { + + private val animeFav: MutableLiveData> = + MutableLiveData>(null) + fun getAnimeFav(): LiveData> = animeFav + + private val listImages: MutableLiveData> = + MutableLiveData>(arrayListOf()) + fun getListImages(): LiveData> = listImages + + suspend fun setData(id: Int) { mangaFav.postValue(Anilist.query.userFavMedia(false, id)) + animeFav.postValue(Anilist.query.userFavMedia(true, id)) + listImages.postValue(Anilist.query.getUserBannerImages(id)) + } } \ No newline at end of file diff --git a/app/src/main/java/ani/dantotsu/connections/anilist/api/Data.kt b/app/src/main/java/ani/dantotsu/connections/anilist/api/Data.kt index ab23a5d4..3f912df0 100644 --- a/app/src/main/java/ani/dantotsu/connections/anilist/api/Data.kt +++ b/app/src/main/java/ani/dantotsu/connections/anilist/api/Data.kt @@ -143,7 +143,7 @@ class Query { data class ToggleFollow( @SerialName("data") val data: Data? - ) : java.io.Serializable { + ) { @Serializable data class Data( @SerialName("ToggleFollow") @@ -156,7 +156,7 @@ class Query { data class GenreCollection( @SerialName("data") val data: Data - ) : java.io.Serializable { + ) { @Serializable data class Data( @SerialName("GenreCollection") @@ -168,7 +168,7 @@ class Query { data class MediaTagCollection( @SerialName("data") val data: Data - ) : java.io.Serializable { + ) { @Serializable data class Data( @SerialName("MediaTagCollection") @@ -180,7 +180,7 @@ class Query { data class User( @SerialName("data") val data: Data - ) : java.io.Serializable { + ) { @Serializable data class Data( @SerialName("User") @@ -192,7 +192,7 @@ class Query { data class UserProfileResponse( @SerialName("data") val data: Data - ) : java.io.Serializable { + ) { @Serializable data class Data( @SerialName("user") @@ -219,7 +219,7 @@ class Query { @SerialName("isBlocked") val isBlocked: Boolean, @SerialName("favourites") - val favorites: UserFavorites?, + val favourites: UserFavourites?, @SerialName("statistics") val statistics: NNUserStatisticTypes, @SerialName("siteUrl") @@ -244,21 +244,21 @@ class Query { ): java.io.Serializable @Serializable - data class UserFavorites( + data class UserFavourites( @SerialName("anime") - val anime: UserMediaFavoritesCollection, + val anime: UserMediaFavouritesCollection, @SerialName("manga") - val manga: UserMediaFavoritesCollection, + val manga: UserMediaFavouritesCollection, @SerialName("characters") - val characters: UserCharacterFavoritesCollection, + val characters: UserCharacterFavouritesCollection, @SerialName("staff") - val staff: UserStaffFavoritesCollection, + val staff: UserStaffFavouritesCollection, @SerialName("studios") - val studios: UserStudioFavoritesCollection, + val studios: UserStudioFavouritesCollection, ): java.io.Serializable @Serializable - data class UserMediaFavoritesCollection( + data class UserMediaFavouritesCollection( @SerialName("nodes") val nodes: List, ): java.io.Serializable @@ -272,7 +272,7 @@ class Query { ): java.io.Serializable @Serializable - data class UserCharacterFavoritesCollection( + data class UserCharacterFavouritesCollection( @SerialName("nodes") val nodes: List, ): java.io.Serializable @@ -281,18 +281,20 @@ class Query { data class UserCharacterImageFavorite( @SerialName("id") val id: Int, + @SerialName("name") + val name: CharacterName, @SerialName("image") val image: CharacterImage ): java.io.Serializable @Serializable - data class UserStaffFavoritesCollection( + data class UserStaffFavouritesCollection( @SerialName("nodes") val nodes: List, //downstream it's the same as character ): java.io.Serializable @Serializable - data class UserStudioFavoritesCollection( + data class UserStudioFavouritesCollection( @SerialName("nodes") val nodes: List, ): java.io.Serializable diff --git a/app/src/main/java/ani/dantotsu/profile/ProfileFragment.kt b/app/src/main/java/ani/dantotsu/profile/ProfileFragment.kt index 06590372..bb43f8b2 100644 --- a/app/src/main/java/ani/dantotsu/profile/ProfileFragment.kt +++ b/app/src/main/java/ani/dantotsu/profile/ProfileFragment.kt @@ -18,6 +18,8 @@ import ani.dantotsu.connections.anilist.ProfileViewModel import ani.dantotsu.connections.anilist.api.Query import ani.dantotsu.databinding.FragmentProfileBinding import ani.dantotsu.loadImage +import ani.dantotsu.media.Character +import ani.dantotsu.media.CharacterAdapter import ani.dantotsu.media.Media import ani.dantotsu.media.MediaAdaptor import ani.dantotsu.media.user.ListActivity @@ -45,8 +47,11 @@ class ProfileFragment() : Fragment() { override fun onViewCreated(view: View, savedInstanceState: Bundle?) { super.onViewCreated(view, savedInstanceState) activity = requireActivity() as ProfileActivity - user = arguments?.getSerializable("user") as Query.UserProfile + viewLifecycleOwner.lifecycleScope.launch(Dispatchers.IO) { + model.setData(user.id) + } + user = arguments?.getSerializable("user") as Query.UserProfile val backGroundColorTypedValue = TypedValue() val textColorTypedValue = TypedValue() activity.theme.resolveAttribute( @@ -74,7 +79,8 @@ class ProfileFragment() : Fragment() { "UTF-8", null ) - binding.userInfoContainer.visibility = if (user.about != null) View.VISIBLE else View.GONE + binding.userInfoContainer.visibility = + if (user.about != null) View.VISIBLE else View.GONE binding.profileAnimeList.setOnClickListener { ContextCompat.startActivity( @@ -92,8 +98,6 @@ class ProfileFragment() : Fragment() { .putExtra("username", user.name), null ) } - binding.profileAnimeListImage.loadImage("https://bit.ly/31bsIHq") - binding.profileMangaListImage.loadImage("https://bit.ly/2ZGfcuG") binding.statsEpisodesWatched.text = user.statistics.anime.episodesWatched.toString() binding.statsDaysWatched.text = (user.statistics.anime.minutesWatched / (24 * 60)).toString() @@ -103,13 +107,12 @@ class ProfileFragment() : Fragment() { binding.statsVolumeRead.text = (user.statistics.manga.volumesRead).toString() binding.statsTotalManga.text = user.statistics.manga.count.toString() binding.statsMangaMeanScore.text = user.statistics.manga.meanScore.toString() - - - viewLifecycleOwner.lifecycleScope.launch(Dispatchers.Main) { - model.setAnimeFav(user.id) - model.setMangaFav(user.id) + model.getListImages().observe(viewLifecycleOwner) { + if (it.isNotEmpty()) { + binding.profileAnimeListImage.loadImage(it[0] ?: "https://bit.ly/31bsIHq") + binding.profileMangaListImage.loadImage(it[1] ?: "https://bit.ly/2ZGfcuG") + } } - initRecyclerView( model.getAnimeFav(), binding.profileFavAnimeContainer, @@ -127,12 +130,42 @@ class ProfileFragment() : Fragment() { binding.profileFavMangaEmpty, binding.profileFavManga ) + + val favCharacter = arrayListOf() + user.favourites?.characters?.nodes?.forEach { i -> + favCharacter.add(Character(i.id, i.name.full, i.image.large, i.image.large, "")) + } + if (favCharacter.isEmpty()) { + binding.profileFavCharactersContainer.visibility = View.GONE + } + binding.profileFavCharactersRecycler.adapter = CharacterAdapter(favCharacter) + binding.profileFavCharactersRecycler.layoutManager = LinearLayoutManager( + requireContext(), + LinearLayoutManager.HORIZONTAL, + false + ) + + val favStaff = arrayListOf() + user.favourites?.staff?.nodes?.forEach { i -> + favStaff.add(Character(i.id, i.name.full, i.image.large, i.image.large, "")) + } + if (favStaff.isEmpty()) { + binding.profileFavStaffContainer.visibility = View.GONE + } + binding.profileFavStaffRecycler.adapter = CharacterAdapter(favStaff) + binding.profileFavStaffRecycler.layoutManager = LinearLayoutManager( + requireContext(), + LinearLayoutManager.HORIZONTAL, + false + ) + } override fun onResume() { super.onResume() if (this::binding.isInitialized) { binding.root.requestLayout() + } } diff --git a/app/src/main/java/ani/dantotsu/settings/PlayerSettingsActivity.kt b/app/src/main/java/ani/dantotsu/settings/PlayerSettingsActivity.kt index 28fd61ef..2ec28660 100644 --- a/app/src/main/java/ani/dantotsu/settings/PlayerSettingsActivity.kt +++ b/app/src/main/java/ani/dantotsu/settings/PlayerSettingsActivity.kt @@ -442,7 +442,7 @@ class PlayerSettingsActivity : AppCompatActivity() { "Poppins", "Poppins Thin", "Century Gothic", - "Century Gothic Bold", + "Levenim MT Bold", "Blocky" ) val fontDialog = AlertDialog.Builder(this, R.style.MyPopup) diff --git a/app/src/main/res/layout/fragment_profile.xml b/app/src/main/res/layout/fragment_profile.xml index 81e2207b..3fe0104d 100644 --- a/app/src/main/res/layout/fragment_profile.xml +++ b/app/src/main/res/layout/fragment_profile.xml @@ -4,7 +4,8 @@ xmlns:tools="http://schemas.android.com/tools" android:layout_width="match_parent" android:layout_height="match_parent" - android:orientation="vertical"> + android:orientation="vertical" + tools:ignore="HardcodedText"> + + + + + + + + + + + \ No newline at end of file