feat(profile): added fav characters and staff

This commit is contained in:
aayush262 2024-03-05 17:10:04 +05:30
parent 8a1097cd35
commit 89b6f28b9f
6 changed files with 212 additions and 89 deletions

View file

@ -32,13 +32,6 @@ import kotlin.system.measureTimeMillis
class AnilistQueries {
suspend fun toggleFollow(id: Int): Query.ToggleFollow? {
val response = executeQuery<Query.ToggleFollow>(
"""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<Query.UserProfileResponse>(
"""{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<Query.StatisticsResponse>(
"""{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<Media> {
var hasNextPage = true
var page = 0
suspend fun getNextPage(page: Int): List<Media> {
val response = executeQuery<Query.User>("""{${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<Media>()
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<Query.Media>(
"""{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<Media> {
suspend fun favMedia(anime: Boolean, id: Int? = Anilist.userid ): ArrayList<Media> {
var hasNextPage = true
var page = 0
suspend fun getNextPage(page: Int): List<Media> {
val response = executeQuery<Query.User>("""{${favMediaQuery(anime, page)}}""")
val response = executeQuery<Query.User>("""{${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<Media> {
@ -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<Query.ToggleFollow>(
"""mutation{ToggleFollow(userId:$id){id, isFollowing, isFollower}}"""
)
return response
}
suspend fun getUserProfile(id: Int): Query.UserProfileResponse? {
return executeQuery<Query.UserProfileResponse>(
"""{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<Query.StatisticsResponse>(
"""{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<Media> {
var hasNextPage = true
var page = 0
suspend fun getNextPage(page: Int): List<Media> {
val response = executeQuery<Query.User>("""{${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<Media>()
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<Query.MediaListCollection>("""{ 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<String?> {
val default = arrayListOf<String?>(null, null)
default[0] = userBannerImage("ANIME", id)
default[1] = userBannerImage("MANGA",id)
return default
}
}

View file

@ -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<ArrayList<Media>> =
MutableLiveData<ArrayList<Media>>(null)
fun getAnimeFav(): LiveData<ArrayList<Media>> = animeFav
suspend fun setAnimeFav(id: Int) {
animeFav.postValue(Anilist.query.userFavMedia(true, id))
}
class ProfileViewModel : ViewModel(){
private val mangaFav: MutableLiveData<ArrayList<Media>> =
MutableLiveData<ArrayList<Media>>(null)
fun getMangaFav(): LiveData<ArrayList<Media>> = mangaFav
suspend fun setMangaFav(id: Int) {
private val animeFav: MutableLiveData<ArrayList<Media>> =
MutableLiveData<ArrayList<Media>>(null)
fun getAnimeFav(): LiveData<ArrayList<Media>> = animeFav
private val listImages: MutableLiveData<ArrayList<String?>> =
MutableLiveData<ArrayList<String?>>(arrayListOf())
fun getListImages(): LiveData<ArrayList<String?>> = 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))
}
}

View file

@ -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<UserMediaImageFavorite>,
): java.io.Serializable
@ -272,7 +272,7 @@ class Query {
): java.io.Serializable
@Serializable
data class UserCharacterFavoritesCollection(
data class UserCharacterFavouritesCollection(
@SerialName("nodes")
val nodes: List<UserCharacterImageFavorite>,
): 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<UserCharacterImageFavorite>, //downstream it's the same as character
): java.io.Serializable
@Serializable
data class UserStudioFavoritesCollection(
data class UserStudioFavouritesCollection(
@SerialName("nodes")
val nodes: List<UserStudioFavorite>,
): java.io.Serializable

View file

@ -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<Character>()
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<Character>()
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()
}
}

View file

@ -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)

View file

@ -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">
<LinearLayout
android:id="@+id/userListContainer"
@ -333,6 +334,7 @@
android:id="@+id/userInfoContainer"
android:layout_width="match_parent"
android:layout_height="match_parent"
tools:layout_height="wrap_content"
android:orientation="vertical">
<TextView
@ -506,4 +508,65 @@
tools:orientation="horizontal" />
</FrameLayout>
</LinearLayout>
<LinearLayout
android:id="@+id/profileFavCharactersContainer"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="vertical"
tools:visibility="gone">
<TextView
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginStart="32dp"
android:layout_marginTop="8dp"
android:layout_marginEnd="32dp"
android:fontFamily="@font/poppins_bold"
android:text="Favorite Characters"
android:textSize="16sp" />
<ani.dantotsu.FadingEdgeRecyclerView
android:id="@+id/profileFavCharactersRecycler"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:clipToPadding="false"
android:nestedScrollingEnabled="true"
android:paddingStart="20dp"
android:paddingEnd="20dp"
android:requiresFadingEdge="horizontal"
tools:itemCount="4"
tools:layoutManager="androidx.recyclerview.widget.GridLayoutManager"
tools:listitem="@layout/item_media_compact"
tools:orientation="horizontal" />
</LinearLayout>
<LinearLayout
android:id="@+id/profileFavStaffContainer"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="vertical"
tools:visibility="gone">
<TextView
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginStart="32dp"
android:layout_marginTop="8dp"
android:layout_marginEnd="32dp"
android:fontFamily="@font/poppins_bold"
android:text="Favorite Staff"
android:textSize="16sp" />
<ani.dantotsu.FadingEdgeRecyclerView
android:id="@+id/profileFavStaffRecycler"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:clipToPadding="false"
android:nestedScrollingEnabled="true"
android:paddingStart="20dp"
android:paddingEnd="20dp"
android:requiresFadingEdge="horizontal"
tools:itemCount="4"
tools:layoutManager="androidx.recyclerview.widget.GridLayoutManager"
tools:listitem="@layout/item_media_compact"
tools:orientation="horizontal" />
</LinearLayout>
</LinearLayout>