From 7b8af6ea8a59296457fbc51c6b258fd200f750bc Mon Sep 17 00:00:00 2001 From: rebel onion <87634197+rebelonion@users.noreply.github.com> Date: Fri, 3 Jan 2025 09:01:09 -0600 Subject: [PATCH] feat: searching --- ...rchResults.kt => AniMangaSearchResults.kt} | 54 +- .../dantotsu/connections/anilist/Anilist.kt | 1 - .../connections/anilist/AnilistQueries.kt | 545 +++++++++--------- .../connections/anilist/AnilistViewModel.kt | 210 ++++++- .../connections/anilist/anilistGraphql.kt | 429 ++++++++++++++ .../dantotsu/connections/anilist/api/Media.kt | 2 +- .../java/ani/dantotsu/home/AnimeFragment.kt | 24 +- .../ani/dantotsu/home/AnimePageAdapter.kt | 12 +- .../java/ani/dantotsu/home/MangaFragment.kt | 24 +- .../ani/dantotsu/home/MangaPageAdapter.kt | 9 +- .../ani/dantotsu/home/SearchBottomSheet.kt | 74 +++ .../main/java/ani/dantotsu/media/Author.kt | 8 +- .../java/ani/dantotsu/media/AuthorActivity.kt | 178 +++++- .../java/ani/dantotsu/media/AuthorAdapter.kt | 4 +- .../ani/dantotsu/media/CharacterAdapter.kt | 7 +- .../media/CharacterDetailsActivity.kt | 16 +- .../ani/dantotsu/media/HeaderInterface.kt | 77 +++ .../java/ani/dantotsu/media/SearchActivity.kt | 373 +++++++++--- .../java/ani/dantotsu/media/SearchAdapter.kt | 114 +--- .../media/SearchFilterBottomDialog.kt | 70 +-- .../dantotsu/media/SearchHistoryAdapter.kt | 15 +- .../main/java/ani/dantotsu/media/Studio.kt | 3 + .../java/ani/dantotsu/media/StudioAdapter.kt | 61 ++ .../dantotsu/media/SupportingSearchAdapter.kt | 142 +++++ .../java/ani/dantotsu/profile/UsersAdapter.kt | 33 +- .../dantotsu/settings/saving/Preferences.kt | 4 + .../extension/api/ExtensionGithubApi.kt | 9 +- app/src/main/res/layout/activity_author.xml | 111 ---- .../main/res/layout/activity_character.xml | 81 ++- .../main/res/layout/bottom_sheet_search.xml | 117 ++++ app/src/main/res/values/strings.xml | 4 + 31 files changed, 2109 insertions(+), 702 deletions(-) rename app/src/main/java/ani/dantotsu/connections/anilist/{SearchResults.kt => AniMangaSearchResults.kt} (70%) create mode 100644 app/src/main/java/ani/dantotsu/connections/anilist/anilistGraphql.kt create mode 100644 app/src/main/java/ani/dantotsu/home/SearchBottomSheet.kt create mode 100644 app/src/main/java/ani/dantotsu/media/HeaderInterface.kt create mode 100644 app/src/main/java/ani/dantotsu/media/StudioAdapter.kt create mode 100644 app/src/main/java/ani/dantotsu/media/SupportingSearchAdapter.kt delete mode 100644 app/src/main/res/layout/activity_author.xml create mode 100644 app/src/main/res/layout/bottom_sheet_search.xml diff --git a/app/src/main/java/ani/dantotsu/connections/anilist/SearchResults.kt b/app/src/main/java/ani/dantotsu/connections/anilist/AniMangaSearchResults.kt similarity index 70% rename from app/src/main/java/ani/dantotsu/connections/anilist/SearchResults.kt rename to app/src/main/java/ani/dantotsu/connections/anilist/AniMangaSearchResults.kt index 6c2ca504..e8ef1a39 100644 --- a/app/src/main/java/ani/dantotsu/connections/anilist/SearchResults.kt +++ b/app/src/main/java/ani/dantotsu/connections/anilist/AniMangaSearchResults.kt @@ -2,15 +2,25 @@ package ani.dantotsu.connections.anilist import ani.dantotsu.R import ani.dantotsu.currContext +import ani.dantotsu.media.Author +import ani.dantotsu.media.Character import ani.dantotsu.media.Media +import ani.dantotsu.media.Studio +import ani.dantotsu.profile.User import java.io.Serializable -data class SearchResults( +interface SearchResults { + var search: String? + var page: Int + var results: MutableList + var hasNextPage: Boolean +} + +data class AniMangaSearchResults( val type: String, var isAdult: Boolean, var onList: Boolean? = null, var perPage: Int? = null, - var search: String? = null, var countryOfOrigin: String? = null, var sort: String? = null, var genres: MutableList? = null, @@ -23,10 +33,11 @@ data class SearchResults( var seasonYear: Int? = null, var startYear: Int? = null, var season: String? = null, - var page: Int = 1, - var results: MutableList, - var hasNextPage: Boolean, -) : Serializable { + override var search: String? = null, + override var page: Int = 1, + override var results: MutableList, + override var hasNextPage: Boolean, +) : SearchResults, Serializable { fun toChipList(): List { val list = mutableListOf() sort?.let { @@ -108,4 +119,33 @@ data class SearchResults( val type: String, val text: String ) -} \ No newline at end of file +} + +data class CharacterSearchResults( + override var search: String?, + override var page: Int = 1, + override var results: MutableList, + override var hasNextPage: Boolean, +) : SearchResults, Serializable + +data class StudioSearchResults( + override var search: String?, + override var page: Int = 1, + override var results: MutableList, + override var hasNextPage: Boolean, +) : SearchResults, Serializable + + +data class StaffSearchResults( + override var search: String?, + override var page: Int = 1, + override var results: MutableList, + override var hasNextPage: Boolean, +) : SearchResults, Serializable + +data class UserSearchResults( + override var search: String?, + override var page: Int = 1, + override var results: MutableList, + override var hasNextPage: Boolean, +) : SearchResults, Serializable \ No newline at end of file diff --git a/app/src/main/java/ani/dantotsu/connections/anilist/Anilist.kt b/app/src/main/java/ani/dantotsu/connections/anilist/Anilist.kt index 6acb67a4..02d7fc14 100644 --- a/app/src/main/java/ani/dantotsu/connections/anilist/Anilist.kt +++ b/app/src/main/java/ani/dantotsu/connections/anilist/Anilist.kt @@ -311,7 +311,6 @@ object Anilist { ) val remaining = json.headers["X-RateLimit-Remaining"]?.toIntOrNull() ?: -1 Logger.log("Remaining requests: $remaining") - println("Remaining requests: $remaining") if (json.code == 429) { val retry = json.headers["Retry-After"]?.toIntOrNull() ?: -1 val passedLimitReset = json.headers["X-RateLimit-Reset"]?.toLongOrNull() ?: 0 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 4c1ececd..237ef453 100644 --- a/app/src/main/java/ani/dantotsu/connections/anilist/AnilistQueries.kt +++ b/app/src/main/java/ani/dantotsu/connections/anilist/AnilistQueries.kt @@ -44,7 +44,8 @@ class AnilistQueries { val response: Query.Viewer? measureTimeMillis { response = executeQuery( - """{Viewer{name options{timezone titleLanguage staffNameLanguage activityMergeTime airingNotifications displayAdultContent restrictMessagesToFollowing} avatar{medium} bannerImage id mediaListOptions{scoreFormat rowOrder animeList{customLists} mangaList{customLists}} statistics{anime{episodesWatched} manga{chaptersRead}} unreadNotificationCount}}""") + """{Viewer{name options{timezone titleLanguage staffNameLanguage activityMergeTime airingNotifications displayAdultContent restrictMessagesToFollowing} avatar{medium} bannerImage id mediaListOptions{scoreFormat rowOrder animeList{customLists} mangaList{customLists}} statistics{anime{episodesWatched} manga{chaptersRead}} unreadNotificationCount}}""" + ) }.also { println("time : $it") } val user = response?.data?.user ?: return false @@ -96,12 +97,10 @@ class AnilistQueries { fun mediaDetails(media: Media): Media { media.cameFromContinue = false - - val query = - """{Media(id:${media.id}){id favourites popularity episodes chapters streamingEpisodes {title thumbnail url site} 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) + var response = + executeQuery(fullMediaInformation(media.id), force = true) if (response != null) { fun parse() { val fetchedMedia = response?.data?.media ?: return @@ -291,7 +290,10 @@ class AnilistQueries { val firstStudio = get(0) media.anime.mainStudio = Studio( firstStudio.id.toString(), - firstStudio.name ?: "N/A" + firstStudio.name ?: "N/A", + firstStudio.isFavourite ?: false, + firstStudio.favourites ?: 0, + null ) } } @@ -333,7 +335,11 @@ class AnilistQueries { if (response.data?.media != null) parse() else { snackString(currContext()?.getString(R.string.adult_stuff)) - response = executeQuery(query, force = true, useToken = false) + response = executeQuery( + fullMediaInformation(media.id), + force = true, + useToken = false + ) if (response?.data?.media != null) parse() else snackString(currContext()?.getString(R.string.what_did_you_open)) } @@ -400,8 +406,6 @@ class AnilistQueries { return media } - - private suspend fun favMedia(anime: Boolean, id: Int? = Anilist.userid): ArrayList { var hasNextPage = true var page = 0 @@ -426,8 +430,6 @@ class AnilistQueries { return responseArray } - - suspend fun getUserStatus(): ArrayList? { val toShow: List = PrefManager.getVal(PrefName.HomeLayout) @@ -485,19 +487,23 @@ class AnilistQueries { return list.toCollection(ArrayList()) } else return null } + 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}}}}}}""" + return """User(id:${id}){id favourites{${if (anime) "anime" else "manga"}(page:$page){$standardPageInformation 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 recommendationQuery(): String { - return """ Page(page: 1, perPage:30) { pageInfo { total currentPage hasNextPage } recommendations(sort: RATING_DESC, onList: true) { rating userRating mediaRecommendation { id idMal isAdult mediaListEntry { progress private score(format:POINT_100) status } chapters isFavourite format episodes nextAiringEpisode {episode} popularity meanScore isFavourite format title {english romaji userPreferred } type status(version: 2) bannerImage coverImage { large } } } } """ + return """ Page(page: 1, perPage:30) { $standardPageInformation recommendations(sort: RATING_DESC, onList: true) { rating userRating mediaRecommendation { id idMal isAdult mediaListEntry { progress private score(format:POINT_100) status } chapters isFavourite format episodes nextAiringEpisode {episode} popularity meanScore isFavourite format title {english romaji userPreferred } type status(version: 2) bannerImage coverImage { large } } } } """ } private fun recommendationPlannedQuery(type: String): String { return """ MediaListCollection(userId: ${Anilist.userid}, type: $type, status: PLANNING${if (type == "ANIME") ", sort: MEDIA_POPULARITY_DESC" else ""} ) { lists { entries { media { id mediaListEntry { progress private score(format:POINT_100) status } idMal type isAdult popularity status(version: 2) chapters episodes nextAiringEpisode {episode} meanScore isFavourite format bannerImage coverImage{large} title { english romaji userPreferred } } } } }""" } + private fun continueMediaQuery(type: String, status: String): String { 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 initHomePage(): Map> { val removeList = PrefManager.getCustomVal("removeList", setOf()) val hidePrivate = PrefManager.getVal(PrefName.HidePrivate) @@ -888,7 +894,7 @@ class AnilistQueries { return null } - suspend fun search( + suspend fun searchAniManga( type: String, page: Int? = null, perPage: Int? = null, @@ -910,52 +916,7 @@ class AnilistQueries { id: Int? = null, hd: Boolean = false, adultOnly: Boolean = false - ): SearchResults? { - val query = """ -query (${"$"}page: Int = 1, ${"$"}id: Int, ${"$"}type: MediaType, ${"$"}isAdult: Boolean = false, ${"$"}search: String, ${"$"}format: [MediaFormat], ${"$"}status: MediaStatus, ${"$"}countryOfOrigin: CountryCode, ${"$"}source: MediaSource, ${"$"}season: MediaSeason, ${"$"}seasonYear: Int, ${"$"}year: String, ${"$"}onList: Boolean, ${"$"}yearLesser: FuzzyDateInt, ${"$"}yearGreater: FuzzyDateInt, ${"$"}episodeLesser: Int, ${"$"}episodeGreater: Int, ${"$"}durationLesser: Int, ${"$"}durationGreater: Int, ${"$"}chapterLesser: Int, ${"$"}chapterGreater: Int, ${"$"}volumeLesser: Int, ${"$"}volumeGreater: Int, ${"$"}licensedBy: [String], ${"$"}isLicensed: Boolean, ${"$"}genres: [String], ${"$"}excludedGenres: [String], ${"$"}tags: [String], ${"$"}excludedTags: [String], ${"$"}minimumTagRank: Int, ${"$"}sort: [MediaSort] = [POPULARITY_DESC, SCORE_DESC, START_DATE_DESC]) { - Page(page: ${"$"}page, perPage: ${perPage ?: 50}) { - pageInfo { - total - perPage - currentPage - lastPage - hasNextPage - } - media(id: ${"$"}id, type: ${"$"}type, season: ${"$"}season, format_in: ${"$"}format, status: ${"$"}status, countryOfOrigin: ${"$"}countryOfOrigin, source: ${"$"}source, search: ${"$"}search, onList: ${"$"}onList, seasonYear: ${"$"}seasonYear, startDate_like: ${"$"}year, startDate_lesser: ${"$"}yearLesser, startDate_greater: ${"$"}yearGreater, episodes_lesser: ${"$"}episodeLesser, episodes_greater: ${"$"}episodeGreater, duration_lesser: ${"$"}durationLesser, duration_greater: ${"$"}durationGreater, chapters_lesser: ${"$"}chapterLesser, chapters_greater: ${"$"}chapterGreater, volumes_lesser: ${"$"}volumeLesser, volumes_greater: ${"$"}volumeGreater, licensedBy_in: ${"$"}licensedBy, isLicensed: ${"$"}isLicensed, genre_in: ${"$"}genres, genre_not_in: ${"$"}excludedGenres, tag_in: ${"$"}tags, tag_not_in: ${"$"}excludedTags, minimumTagRank: ${"$"}minimumTagRank, sort: ${"$"}sort, isAdult: ${"$"}isAdult) { - id - idMal - isAdult - status - chapters - episodes - nextAiringEpisode { - episode - } - type - genres - meanScore - isFavourite - format - bannerImage - coverImage { - large - extraLarge - } - title { - english - romaji - userPreferred - } - mediaListEntry { - progress - private - score(format: POINT_100) - status - } - } - } -} - """.replace("\n", " ").replace(""" """, "") + ): AniMangaSearchResults? { val variables = """{"type":"$type","isAdult":$isAdult ${if (adultOnly) ""","isAdult":true""" else ""} ${if (onList != null) ""","onList":$onList""" else ""} @@ -1000,8 +961,9 @@ query (${"$"}page: Int = 1, ${"$"}id: Int, ${"$"}type: MediaType, ${"$"}isAdult: }]""" else "" } - }""".replace("\n", " ").replace(""" """, "") - val response = executeQuery(query, variables, true)?.data?.page + }""".prepare() + val response = + executeQuery(aniMangaSearch(perPage), variables, true)?.data?.page if (response?.media != null) { val responseArray = arrayListOf() response.media?.forEach { i -> @@ -1021,7 +983,7 @@ query (${"$"}page: Int = 1, ${"$"}id: Int, ${"$"}type: MediaType, ${"$"}isAdult: val pageInfo = response.pageInfo ?: return null - return SearchResults( + return AniMangaSearchResults( type = type, perPage = perPage, search = search, @@ -1047,6 +1009,169 @@ query (${"$"}page: Int = 1, ${"$"}id: Int, ${"$"}type: MediaType, ${"$"}isAdult: return null } + suspend fun searchCharacters(page: Int, search: String?): CharacterSearchResults? { + if (search.isNullOrBlank()) return null + val query = """ + { + Page(page: $page, perPage: $ITEMS_PER_PAGE) { + $standardPageInformation + characters(search: "$search") { + ${characterInformation(false)} + } + } + } + """.prepare() + + val response = executeQuery(query, force = true)?.data?.page + + if (response?.characters != null) { + val responseArray = arrayListOf() + response.characters?.forEach { i -> + responseArray.add( + Character( + i.id, + i.name?.full, + i.image?.medium ?: i.image?.large, + null, + null.toString(), + i.isFavourite ?: false, + i.description, + i.age, + i.gender, + i.dateOfBirth, + ) + ) + } + + val pageInfo = response.pageInfo ?: return null + + return CharacterSearchResults( + search = search, + results = responseArray, + page = pageInfo.currentPage ?: 0, + hasNextPage = pageInfo.hasNextPage == true + ) + } + return null + } + + suspend fun searchStudios(page: Int, search: String?): StudioSearchResults? { + if (search.isNullOrBlank()) return null + val query = """ + { + Page(page: $page, perPage: $ITEMS_PER_PAGE) { + $standardPageInformation + studios(search: "$search") { + ${studioInformation(1, 1)} + } + } + } + """.prepare() + + val response = executeQuery(query, force = true)?.data?.page + + if (response?.studios != null) { + val responseArray = arrayListOf() + response.studios?.forEach { i -> + responseArray.add( + Studio( + i.id.toString(), + i.name ?: return null, + i.isFavourite ?: false, + i.favourites, + i.media?.edges?.firstOrNull()?.node?.let { it.coverImage?.large } + ) + ) + } + + val pageInfo = response.pageInfo ?: return null + + return StudioSearchResults( + search = search, + results = responseArray, + page = pageInfo.currentPage ?: 0, + hasNextPage = pageInfo.hasNextPage == true + ) + } + return null + } + + suspend fun searchStaff(page: Int, search: String?): StaffSearchResults? { + if (search.isNullOrBlank()) return null + val query = """ + { + Page(page: $page, perPage: $ITEMS_PER_PAGE) { + $standardPageInformation + staff(search: "$search") { + ${staffInformation(1, 1)} + } + } + } + """.prepare() + + val response = executeQuery(query, force = true)?.data?.page + + if (response?.staff != null) { + val responseArray = arrayListOf() + response.staff?.forEach { i -> + responseArray.add( + Author( + i.id, + i.name?.userPreferred ?: return null, + i.image?.large, + null, + null, + null + ) + ) + } + + val pageInfo = response.pageInfo ?: return null + + return StaffSearchResults( + search = search, + results = responseArray, + page = pageInfo.currentPage ?: 0, + hasNextPage = pageInfo.hasNextPage == true + ) + } + return null + } + + suspend fun searchUsers(page: Int, search: String?): UserSearchResults? { + val query = """ + { + Page(page: $page, perPage: $ITEMS_PER_PAGE) { + $standardPageInformation + users(search: "$search") { + ${userInformation()} + } + } + } + """.prepare() + + val response = executeQuery(query, force = true)?.data?.page + + if (response?.users != null) { + val users = response.users?.map { user -> + User( + user.id, + user.name ?: return null, + user.avatar?.medium, + user.bannerImage + ) + } ?: return null + + return UserSearchResults( + search = search, + results = users.toMutableList(), + page = response.pageInfo?.currentPage ?: 0, + hasNextPage = response.pageInfo?.hasNextPage == true + ) + } + return null + } + private fun mediaList(media1: Page?): ArrayList { val combinedList = arrayListOf() media1?.media?.mapTo(combinedList) { Media(it) } @@ -1071,26 +1196,65 @@ query (${"$"}page: Int = 1, ${"$"}id: Int, ${"$"}type: MediaType, ${"$"}isAdult: val countryFilter = country?.let { "countryOfOrigin:$it, " } ?: "" return buildString { - append("""Page(page:1,perPage:50){pageInfo{hasNextPage total}media(sort:$sort, type:$type, $formatFilter $countryFilter $includeList $isAdult){id idMal status chapters episodes nextAiringEpisode{episode} isAdult type meanScore isFavourite format bannerImage countryOfOrigin coverImage{large} title{english romaji userPreferred} mediaListEntry{progress private score(format:POINT_100) status}}}""") + append("""Page(page:1,perPage:50){$standardPageInformation media(sort:$sort, type:$type, $formatFilter $countryFilter $includeList $isAdult){id idMal status chapters episodes nextAiringEpisode{episode} isAdult type meanScore isFavourite format bannerImage countryOfOrigin coverImage{large} title{english romaji userPreferred} mediaListEntry{progress private score(format:POINT_100) status}}}""") } } private fun recentAnimeUpdates(page: Int): String { val currentTime = System.currentTimeMillis() / 1000 return buildString { - append("""Page(page:$page,perPage:50){pageInfo{hasNextPage total}airingSchedules(airingAt_greater:0 airingAt_lesser:${currentTime - 10000} sort:TIME_DESC){episode airingAt media{id idMal status chapters episodes nextAiringEpisode{episode} isAdult type meanScore isFavourite format bannerImage countryOfOrigin coverImage{large} title{english romaji userPreferred} mediaListEntry{progress private score(format:POINT_100) status}}}}""") + append("""Page(page:$page,perPage:50){$standardPageInformation airingSchedules(airingAt_greater:0 airingAt_lesser:${currentTime - 10000} sort:TIME_DESC){episode airingAt media{id idMal status chapters episodes nextAiringEpisode{episode} isAdult type meanScore isFavourite format bannerImage countryOfOrigin coverImage{large} title{english romaji userPreferred} mediaListEntry{progress private score(format:POINT_100) status}}}}""") } } private fun queryAnimeList(): String { return buildString { - append("""{recentUpdates:${recentAnimeUpdates(1)} recentUpdates2:${recentAnimeUpdates(2)} trendingMovies:${buildQueryString("POPULARITY_DESC", "ANIME", "MOVIE")} topRated:${buildQueryString("SCORE_DESC", "ANIME")} mostFav:${buildQueryString("FAVOURITES_DESC", "ANIME")}}""") + append( + """{recentUpdates:${recentAnimeUpdates(1)} recentUpdates2:${recentAnimeUpdates(2)} trendingMovies:${ + buildQueryString( + "POPULARITY_DESC", + "ANIME", + "MOVIE" + ) + } topRated:${ + buildQueryString( + "SCORE_DESC", + "ANIME" + ) + } mostFav:${buildQueryString("FAVOURITES_DESC", "ANIME")}}""" + ) } } private fun queryMangaList(): String { return buildString { - append("""{trendingManga:${buildQueryString("POPULARITY_DESC", "MANGA", country = "JP")} trendingManhwa:${buildQueryString("POPULARITY_DESC", "MANGA", country = "KR")} trendingNovel:${buildQueryString("POPULARITY_DESC", "MANGA", format = "NOVEL", country = "JP")} topRated:${buildQueryString("SCORE_DESC", "MANGA")} mostFav:${buildQueryString("FAVOURITES_DESC", "MANGA")}}""") + append( + """{trendingManga:${ + buildQueryString( + "POPULARITY_DESC", + "MANGA", + country = "JP" + ) + } trendingManhwa:${ + buildQueryString( + "POPULARITY_DESC", + "MANGA", + country = "KR" + ) + } trendingNovel:${ + buildQueryString( + "POPULARITY_DESC", + "MANGA", + format = "NOVEL", + country = "JP" + ) + } topRated:${ + buildQueryString( + "SCORE_DESC", + "MANGA" + ) + } mostFav:${buildQueryString("FAVOURITES_DESC", "MANGA")}}""" + ) } } @@ -1145,7 +1309,6 @@ query (${"$"}page: Int = 1, ${"$"}id: Int, ${"$"}type: MediaType, ${"$"}isAdult: list } - suspend fun recentlyUpdated( greater: Long = 0, lesser: Long = System.currentTimeMillis() / 1000 - 10000 @@ -1153,10 +1316,7 @@ query (${"$"}page: Int = 1, ${"$"}id: Int, ${"$"}type: MediaType, ${"$"}isAdult: suspend fun execute(page: Int = 1): Page? { val query = """{ Page(page:$page,perPage:50) { - pageInfo { - hasNextPage - total - } + $standardPageInformation airingSchedules( airingAt_greater: $greater airingAt_lesser: $lesser @@ -1165,35 +1325,11 @@ Page(page:$page,perPage:50) { episode airingAt media { - id - idMal - status - chapters - episodes - nextAiringEpisode { episode } - isAdult - type - meanScore - isFavourite - format - bannerImage - countryOfOrigin - coverImage { large } - title { - english - romaji - userPreferred - } - mediaListEntry { - progress - private - score(format: POINT_100) - status - } + ${standardMediaInformation()} } } } - }""".replace("\n", " ").replace(""" """, "") + }""".prepare() return executeQuery(query, force = true)?.data?.page } @@ -1221,68 +1357,37 @@ Page(page:$page,perPage:50) { suspend fun getCharacterDetails(character: Character): Character { val query = """ { Character(id: ${character.id}) { - id - age - gender - description - dateOfBirth { - year - month - day - } - media(page: 0,sort:[POPULARITY_DESC,SCORE_DESC]) { - pageInfo { - total - perPage - currentPage - lastPage - hasNextPage - } - edges { - id - characterRole - node { - id - idMal - isAdult - status - chapters - episodes - nextAiringEpisode { episode } - type - meanScore - isFavourite - format - bannerImage - countryOfOrigin - coverImage { large } - title { - english - romaji - userPreferred - } - mediaListEntry { - progress - private - score(format: POINT_100) - status - } - } - } - } + ${characterInformation(true)} } -}""".replace("\n", " ").replace(""" """, "") - executeQuery(query, force = true)?.data?.character?.apply { - character.age = age - character.gender = gender - character.description = description - character.dateOfBirth = dateOfBirth - character.roles = arrayListOf() - media?.edges?.forEach { i -> - val m = Media(i) - m.relation = i.characterRole.toString() - character.roles?.add(m) - } +}""".prepare() + executeQuery(query, force = true)?.data?.character?.let { i -> + return Character( + i.id, + i.name?.full, + i.image?.large ?: i.image?.medium, + null, + null.toString(), + i.isFavourite ?: false, + i.description, + i.age, + i.gender, + i.dateOfBirth, + i.media?.edges?.map { + val m = Media(it) + m.relation = it.characterRole.toString() + m + }?.let { ArrayList(it) }, + i.media?.edges?.flatMap { edge -> + edge.voiceActors?.map { va -> + Author( + va.id, + va.name?.userPreferred, + va.image?.large ?: va.image?.medium, + va.languageV2 + ) + } ?: emptyList() + } as ArrayList? + ) } return character } @@ -1290,45 +1395,9 @@ Page(page:$page,perPage:50) { suspend fun getStudioDetails(studio: Studio): Studio { fun query(page: Int = 0) = """ { Studio(id: ${studio.id}) { - id - media(page: $page,sort:START_DATE_DESC) { - pageInfo{ - hasNextPage - } - edges { - id - node { - id - idMal - isAdult - status - chapters - episodes - nextAiringEpisode { episode } - type - meanScore - startDate{ year } - isFavourite - format - bannerImage - countryOfOrigin - coverImage { large } - title { - english - romaji - userPreferred - } - mediaListEntry { - progress - private - score(format: POINT_100) - status - } - } - } - } + ${studioInformation(page, ITEMS_PER_PAGE)} } -}""".replace("\n", " ").replace(""" """, "") +}""".prepare() var hasNextPage = true val yearMedia = mutableMapOf>() @@ -1363,66 +1432,15 @@ Page(page:$page,perPage:50) { suspend fun getAuthorDetails(author: Author): Author { fun query(page: Int = 0) = """ { Staff(id: ${author.id}) { - id - staffMedia(page: $page,sort:START_DATE_DESC) { - pageInfo{ - hasNextPage - } - edges { - staffRole - id - node { - id - idMal - isAdult - status - chapters - episodes - nextAiringEpisode { episode } - type - meanScore - startDate{ year } - isFavourite - format - bannerImage - countryOfOrigin - coverImage { large } - title { - english - romaji - userPreferred - } - mediaListEntry { - progress - private - score(format: POINT_100) - status - } - } - } - } + ${staffInformation(page, ITEMS_PER_PAGE)} characters(page: $page,sort:FAVOURITES_DESC) { - pageInfo{ - hasNextPage - } + $standardPageInformation nodes{ - id - name { - first - middle - last - full - native - userPreferred - } - image { - large - medium - } + ${characterInformation(false)} } } } -}""".replace("\n", " ").replace(""" """, "") +}""".prepare() var hasNextPage = true val yearMedia = mutableMapOf>() @@ -1433,6 +1451,16 @@ Page(page:$page,perPage:50) { val query = executeQuery( query(page), force = true )?.data?.author + author.age = query?.age + author.yearsActive = + if (query?.yearsActive?.isEmpty() == true) null else query?.yearsActive + author.homeTown = if (query?.homeTown?.isBlank() == true) null else query?.homeTown + author.dateOfDeath = if (query?.dateOfDeath?.toStringOrEmpty() + ?.isBlank() == true + ) null else query?.dateOfDeath?.toStringOrEmpty() + author.dateOfBirth = if (query?.dateOfBirth?.toStringOrEmpty() + ?.isBlank() == true + ) null else query?.dateOfBirth?.toStringOrEmpty() hasNextPage = query?.staffMedia?.let { it.edges?.forEach { i -> i.node?.apply { @@ -1480,14 +1508,14 @@ Page(page:$page,perPage:50) { 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}}}}}""", + """{Page(page:$page,perPage:10){$standardPageInformation 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 ) } suspend fun getUserProfile(id: Int): Query.UserProfileResponse? { return executeQuery( - """{followerPage:Page{followers(userId:$id){id}pageInfo{total}}followingPage:Page{following(userId:$id){id}pageInfo{total}}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}isFavourite}}staff{nodes{id name{first middle last full native alternative userPreferred}image{large medium}isFavourite}}studios{nodes{id name isFavourite}}}statistics{anime{count meanScore standardDeviation minutesWatched episodesWatched chaptersRead volumesRead}manga{count meanScore standardDeviation minutesWatched episodesWatched chaptersRead volumesRead}}siteUrl}}""", + """{followerPage:Page{followers(userId:$id){id}$standardPageInformation}followingPage:Page{following(userId:$id){id}$standardPageInformation}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}isFavourite}}staff{nodes{id name{first middle last full native alternative userPreferred}image{large medium}isFavourite}}studios{nodes{id name isFavourite}}}statistics{anime{count meanScore standardDeviation minutesWatched episodesWatched chaptersRead volumesRead}manga{count meanScore standardDeviation minutesWatched episodesWatched chaptersRead volumesRead}}siteUrl}}""", force = true ) } @@ -1513,7 +1541,7 @@ Page(page:$page,perPage:50) { } private fun userFavMediaQuery(anime: Boolean, id: Int): String { - return """User(id:${id}){id favourites{${if (anime) "anime" else "manga"}(page:1){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}}}}}}""" + return """User(id:${id}){id favourites{${if (anime) "anime" else "manga"}(page:1){$standardPageInformation 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 userFollowing(id: Int): Query.Following? { @@ -1535,7 +1563,7 @@ Page(page:$page,perPage:50) { """{ favoriteAnime:${userFavMediaQuery(true, id)} favoriteManga:${userFavMediaQuery(false, id)} - }""".trimIndent(), force = true + }""".prepare(), force = true ) } @@ -1549,7 +1577,7 @@ Page(page:$page,perPage:50) { val typeIn = "type_in:[AIRING,MEDIA_MERGE,MEDIA_DELETION,MEDIA_DATA_CHANGE]" val reset = if (resetNotification) "true" else "false" val res = executeQuery( - """{User(id:$id){unreadNotificationCount}Page(page:$page,perPage:$ITEMS_PER_PAGE){pageInfo{currentPage,hasNextPage}notifications(resetNotificationCount:$reset , ${if (type == true) typeIn else ""}){__typename...on AiringNotification{id,type,animeId,episode,contexts,createdAt,media{id,title{romaji,english,native,userPreferred}bannerImage,coverImage{medium,large}},}...on FollowingNotification{id,userId,type,context,createdAt,user{id,name,bannerImage,avatar{medium,large,}}}...on ActivityMessageNotification{id,userId,type,activityId,context,createdAt,message{id}user{id,name,bannerImage,avatar{medium,large,}}}...on ActivityMentionNotification{id,userId,type,activityId,context,createdAt,activity{__typename}user{id,name,bannerImage,avatar{medium,large,}}}...on ActivityReplyNotification{id,userId,type,activityId,context,createdAt,activity{__typename}user{id,name,bannerImage,avatar{medium,large,}}}...on ActivityReplySubscribedNotification{id,userId,type,activityId,context,createdAt,activity{__typename}user{id,name,bannerImage,avatar{medium,large,}}}...on ActivityLikeNotification{id,userId,type,activityId,context,createdAt,activity{__typename}user{id,name,bannerImage,avatar{medium,large,}}}...on ActivityReplyLikeNotification{id,userId,type,activityId,context,createdAt,activity{__typename}user{id,name,bannerImage,avatar{medium,large,}}}...on ThreadCommentMentionNotification{id,userId,type,commentId,context,createdAt,thread{id}comment{id}user{id,name,bannerImage,avatar{medium,large,}}}...on ThreadCommentReplyNotification{id,userId,type,commentId,context,createdAt,thread{id}comment{id}user{id,name,bannerImage,avatar{medium,large,}}}...on ThreadCommentSubscribedNotification{id,userId,type,commentId,context,createdAt,thread{id}comment{id}user{id,name,bannerImage,avatar{medium,large,}}}...on ThreadCommentLikeNotification{id,userId,type,commentId,context,createdAt,thread{id}comment{id}user{id,name,bannerImage,avatar{medium,large,}}}...on ThreadLikeNotification{id,userId,type,threadId,context,createdAt,thread{id}comment{id}user{id,name,bannerImage,avatar{medium,large,}}}...on RelatedMediaAdditionNotification{id,type,context,createdAt,media{id,title{romaji,english,native,userPreferred}bannerImage,coverImage{medium,large}}}...on MediaDataChangeNotification{id,type,mediaId,context,reason,createdAt,media{id,title{romaji,english,native,userPreferred}bannerImage,coverImage{medium,large}}}...on MediaMergeNotification{id,type,mediaId,deletedMediaTitles,context,reason,createdAt,media{id,title{romaji,english,native,userPreferred}bannerImage,coverImage{medium,large}}}...on MediaDeletionNotification{id,type,deletedMediaTitle,context,reason,createdAt,}}}}""", + """{User(id:$id){unreadNotificationCount}Page(page:$page,perPage:$ITEMS_PER_PAGE){$standardPageInformation notifications(resetNotificationCount:$reset , ${if (type == true) typeIn else ""}){__typename...on AiringNotification{id,type,animeId,episode,contexts,createdAt,media{id,title{romaji,english,native,userPreferred}bannerImage,coverImage{medium,large}},}...on FollowingNotification{id,userId,type,context,createdAt,user{id,name,bannerImage,avatar{medium,large,}}}...on ActivityMessageNotification{id,userId,type,activityId,context,createdAt,message{id}user{id,name,bannerImage,avatar{medium,large,}}}...on ActivityMentionNotification{id,userId,type,activityId,context,createdAt,activity{__typename}user{id,name,bannerImage,avatar{medium,large,}}}...on ActivityReplyNotification{id,userId,type,activityId,context,createdAt,activity{__typename}user{id,name,bannerImage,avatar{medium,large,}}}...on ActivityReplySubscribedNotification{id,userId,type,activityId,context,createdAt,activity{__typename}user{id,name,bannerImage,avatar{medium,large,}}}...on ActivityLikeNotification{id,userId,type,activityId,context,createdAt,activity{__typename}user{id,name,bannerImage,avatar{medium,large,}}}...on ActivityReplyLikeNotification{id,userId,type,activityId,context,createdAt,activity{__typename}user{id,name,bannerImage,avatar{medium,large,}}}...on ThreadCommentMentionNotification{id,userId,type,commentId,context,createdAt,thread{id}comment{id}user{id,name,bannerImage,avatar{medium,large,}}}...on ThreadCommentReplyNotification{id,userId,type,commentId,context,createdAt,thread{id}comment{id}user{id,name,bannerImage,avatar{medium,large,}}}...on ThreadCommentSubscribedNotification{id,userId,type,commentId,context,createdAt,thread{id}comment{id}user{id,name,bannerImage,avatar{medium,large,}}}...on ThreadCommentLikeNotification{id,userId,type,commentId,context,createdAt,thread{id}comment{id}user{id,name,bannerImage,avatar{medium,large,}}}...on ThreadLikeNotification{id,userId,type,threadId,context,createdAt,thread{id}comment{id}user{id,name,bannerImage,avatar{medium,large,}}}...on RelatedMediaAdditionNotification{id,type,context,createdAt,media{id,title{romaji,english,native,userPreferred}bannerImage,coverImage{medium,large}}}...on MediaDataChangeNotification{id,type,mediaId,context,reason,createdAt,media{id,title{romaji,english,native,userPreferred}bannerImage,coverImage{medium,large}}}...on MediaMergeNotification{id,type,mediaId,deletedMediaTitles,context,reason,createdAt,media{id,title{romaji,english,native,userPreferred}bannerImage,coverImage{medium,large}}}...on MediaDeletionNotification{id,type,deletedMediaTitle,context,reason,createdAt,}}}}""", force = true ) if (res != null && resetNotification) { @@ -1571,7 +1599,8 @@ Page(page:$page,perPage:50) { else if (userId != null) "userId:$userId," else if (global) "isFollowing:false,hasRepliesOrTypeText:true," else "isFollowing:true," - val typeIn = if (filter == "isFollowing:true,") "type_in:[TEXT,ANIME_LIST,MANGA_LIST,MEDIA_LIST]," else "" + val typeIn = + if (filter == "isFollowing:true,") "type_in:[TEXT,ANIME_LIST,MANGA_LIST,MEDIA_LIST]," else "" return executeQuery( """{Page(page:$page,perPage:$ITEMS_PER_PAGE){activities(${filter}${typeIn}sort:ID_DESC){__typename ... on TextActivity{id userId type replyCount text(asHtml:true)siteUrl isLocked isSubscribed likeCount isLiked isPinned createdAt user{id name bannerImage avatar{medium large}}replies{id userId activityId text(asHtml:true)likeCount isLiked createdAt user{id name bannerImage avatar{medium large}}likes{id name bannerImage avatar{medium large}}}likes{id name bannerImage avatar{medium large}}}... on ListActivity{id userId type replyCount status progress siteUrl isLocked isSubscribed likeCount isLiked isPinned createdAt user{id name bannerImage avatar{medium large}}media{id title{english romaji native userPreferred}bannerImage coverImage{medium large}isAdult}replies{id userId activityId text(asHtml:true)likeCount isLiked createdAt user{id name bannerImage avatar{medium large}}likes{id name bannerImage avatar{medium large}}}likes{id name bannerImage avatar{medium large}}}... on MessageActivity{id recipientId messengerId type replyCount likeCount message(asHtml:true)isLocked isSubscribed isLiked isPrivate siteUrl createdAt recipient{id name bannerImage avatar{medium large}}messenger{id name bannerImage avatar{medium large}}replies{id userId activityId text(asHtml:true)likeCount isLiked createdAt user{id name bannerImage avatar{medium large}}likes{id name bannerImage avatar{medium large}}}likes{id name bannerImage avatar{medium large}}}}}}""", force = true 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 ee978214..572f69ed 100644 --- a/app/src/main/java/ani/dantotsu/connections/anilist/AnilistViewModel.kt +++ b/app/src/main/java/ani/dantotsu/connections/anilist/AnilistViewModel.kt @@ -128,7 +128,7 @@ class AnilistHomeViewModel : ViewModel() { class AnilistAnimeViewModel : ViewModel() { var searched = false var notSet = true - lateinit var searchResults: SearchResults + lateinit var aniMangaSearchResults: AniMangaSearchResults private val type = "ANIME" private val trending: MutableLiveData> = MutableLiveData>(null) @@ -137,7 +137,7 @@ class AnilistAnimeViewModel : ViewModel() { suspend fun loadTrending(i: Int) { val (season, year) = Anilist.currentSeasons[i] trending.postValue( - Anilist.query.search( + Anilist.query.searchAniManga( type, perPage = 12, sort = Anilist.sortBy[2], @@ -150,9 +150,9 @@ class AnilistAnimeViewModel : ViewModel() { } - private val animePopular = MutableLiveData(null) + private val animePopular = MutableLiveData(null) - fun getPopular(): LiveData = animePopular + fun getPopular(): LiveData = animePopular suspend fun loadPopular( type: String, searchVal: String? = null, @@ -161,7 +161,7 @@ class AnilistAnimeViewModel : ViewModel() { onList: Boolean = true, ) { animePopular.postValue( - Anilist.query.search( + Anilist.query.searchAniManga( type, search = searchVal, onList = if (onList) null else false, @@ -173,8 +173,8 @@ class AnilistAnimeViewModel : ViewModel() { } - suspend fun loadNextPage(r: SearchResults) = animePopular.postValue( - Anilist.query.search( + suspend fun loadNextPage(r: AniMangaSearchResults) = animePopular.postValue( + Anilist.query.searchAniManga( r.type, r.page + 1, r.perPage, @@ -224,7 +224,7 @@ class AnilistAnimeViewModel : ViewModel() { class AnilistMangaViewModel : ViewModel() { var searched = false var notSet = true - lateinit var searchResults: SearchResults + lateinit var aniMangaSearchResults: AniMangaSearchResults private val type = "MANGA" private val trending: MutableLiveData> = MutableLiveData>(null) @@ -232,7 +232,7 @@ class AnilistMangaViewModel : ViewModel() { fun getTrending(): LiveData> = trending suspend fun loadTrending() = trending.postValue( - Anilist.query.search( + Anilist.query.searchAniManga( type, perPage = 10, sort = Anilist.sortBy[2], @@ -242,8 +242,8 @@ class AnilistMangaViewModel : ViewModel() { ) - private val mangaPopular = MutableLiveData(null) - fun getPopular(): LiveData = mangaPopular + private val mangaPopular = MutableLiveData(null) + fun getPopular(): LiveData = mangaPopular suspend fun loadPopular( type: String, searchVal: String? = null, @@ -252,7 +252,7 @@ class AnilistMangaViewModel : ViewModel() { onList: Boolean = true, ) { mangaPopular.postValue( - Anilist.query.search( + Anilist.query.searchAniManga( type, search = searchVal, onList = if (onList) null else false, @@ -264,8 +264,8 @@ class AnilistMangaViewModel : ViewModel() { } - suspend fun loadNextPage(r: SearchResults) = mangaPopular.postValue( - Anilist.query.search( + suspend fun loadNextPage(r: AniMangaSearchResults) = mangaPopular.postValue( + Anilist.query.searchAniManga( r.type, r.page + 1, r.perPage, @@ -325,14 +325,126 @@ class AnilistMangaViewModel : ViewModel() { } class AnilistSearch : ViewModel() { + + enum class SearchType { + ANIME, MANGA, CHARACTER, STAFF, STUDIO, USER; + + companion object { + + fun SearchType.toAnilistString(): String { + return when (this) { + ANIME -> "ANIME" + MANGA -> "MANGA" + CHARACTER -> "CHARACTER" + STAFF -> "STAFF" + STUDIO -> "STUDIO" + USER -> "USER" + } + } + + fun fromString(string: String): SearchType { + return when (string.uppercase()) { + "ANIME" -> ANIME + "MANGA" -> MANGA + "CHARACTER" -> CHARACTER + "STAFF" -> STAFF + "STUDIO" -> STUDIO + "USER" -> USER + else -> throw IllegalArgumentException("Invalid search type") + } + } + } + } + var searched = false var notSet = true - lateinit var searchResults: SearchResults - private val result: MutableLiveData = MutableLiveData(null) + lateinit var aniMangaSearchResults: AniMangaSearchResults + private val aniMangaResult: MutableLiveData = MutableLiveData(null) - fun getSearch(): LiveData = result - suspend fun loadSearch(r: SearchResults) = result.postValue( - Anilist.query.search( + lateinit var characterSearchResults: CharacterSearchResults + private val characterResult: MutableLiveData = MutableLiveData(null) + + lateinit var studioSearchResults: StudioSearchResults + private val studioResult: MutableLiveData = MutableLiveData(null) + + lateinit var staffSearchResults: StaffSearchResults + private val staffResult: MutableLiveData = MutableLiveData(null) + + lateinit var userSearchResults: UserSearchResults + private val userResult: MutableLiveData = MutableLiveData(null) + + fun getSearch(type: SearchType): MutableLiveData { + return when (type) { + SearchType.ANIME, SearchType.MANGA -> aniMangaResult as MutableLiveData + SearchType.CHARACTER -> characterResult as MutableLiveData + SearchType.STUDIO -> studioResult as MutableLiveData + SearchType.STAFF -> staffResult as MutableLiveData + SearchType.USER -> userResult as MutableLiveData + } + } + + suspend fun loadSearch(type: SearchType) { + when (type) { + SearchType.ANIME, SearchType.MANGA -> loadAniMangaSearch(aniMangaSearchResults) + SearchType.CHARACTER -> loadCharacterSearch(characterSearchResults) + SearchType.STUDIO -> loadStudiosSearch(studioSearchResults) + SearchType.STAFF -> loadStaffSearch(staffSearchResults) + SearchType.USER -> loadUserSearch(userSearchResults) + } + } + + suspend fun loadNextPage(type: SearchType) { + when (type) { + SearchType.ANIME, SearchType.MANGA -> loadNextAniMangaPage(aniMangaSearchResults) + SearchType.CHARACTER -> loadNextCharacterPage(characterSearchResults) + SearchType.STUDIO -> loadNextStudiosPage(studioSearchResults) + SearchType.STAFF -> loadNextStaffPage(staffSearchResults) + SearchType.USER -> loadNextUserPage(userSearchResults) + } + } + + fun hasNextPage(type: SearchType): Boolean { + return when (type) { + SearchType.ANIME, SearchType.MANGA -> aniMangaSearchResults.hasNextPage + SearchType.CHARACTER -> characterSearchResults.hasNextPage + SearchType.STUDIO -> studioSearchResults.hasNextPage + SearchType.STAFF -> staffSearchResults.hasNextPage + SearchType.USER -> userSearchResults.hasNextPage + } + } + + fun resultsIsNotEmpty(type: SearchType): Boolean { + return when (type) { + SearchType.ANIME, SearchType.MANGA -> aniMangaSearchResults.results.isNotEmpty() + SearchType.CHARACTER -> characterSearchResults.results.isNotEmpty() + SearchType.STUDIO -> studioSearchResults.results.isNotEmpty() + SearchType.STAFF -> staffSearchResults.results.isNotEmpty() + SearchType.USER -> userSearchResults.results.isNotEmpty() + } + } + + fun size(type: SearchType): Int { + return when (type) { + SearchType.ANIME, SearchType.MANGA -> aniMangaSearchResults.results.size + SearchType.CHARACTER -> characterSearchResults.results.size + SearchType.STUDIO -> studioSearchResults.results.size + SearchType.STAFF -> staffSearchResults.results.size + SearchType.USER -> userSearchResults.results.size + } + } + + fun clearResults(type: SearchType) { + when (type) { + SearchType.ANIME, SearchType.MANGA -> aniMangaSearchResults.results.clear() + SearchType.CHARACTER -> characterSearchResults.results.clear() + SearchType.STUDIO -> studioSearchResults.results.clear() + SearchType.STAFF -> staffSearchResults.results.clear() + SearchType.USER -> userSearchResults.results.clear() + } + } + + private suspend fun loadAniMangaSearch(r: AniMangaSearchResults) = aniMangaResult.postValue( + Anilist.query.searchAniManga( r.type, r.page, r.perPage, @@ -354,8 +466,36 @@ class AnilistSearch : ViewModel() { ) ) - suspend fun loadNextPage(r: SearchResults) = result.postValue( - Anilist.query.search( + private suspend fun loadCharacterSearch(r: CharacterSearchResults) = characterResult.postValue( + Anilist.query.searchCharacters( + r.page, + r.search, + ) + ) + + private suspend fun loadStudiosSearch(r: StudioSearchResults) = studioResult.postValue( + Anilist.query.searchStudios( + r.page, + r.search, + ) + ) + + private suspend fun loadStaffSearch(r: StaffSearchResults) = staffResult.postValue( + Anilist.query.searchStaff( + r.page, + r.search, + ) + ) + + private suspend fun loadUserSearch(r: UserSearchResults) = userResult.postValue( + Anilist.query.searchUsers( + r.page, + r.search, + ) + ) + + private suspend fun loadNextAniMangaPage(r: AniMangaSearchResults) = aniMangaResult.postValue( + Anilist.query.searchAniManga( r.type, r.page + 1, r.perPage, @@ -376,6 +516,34 @@ class AnilistSearch : ViewModel() { r.season ) ) + + private suspend fun loadNextCharacterPage(r: CharacterSearchResults) = characterResult.postValue( + Anilist.query.searchCharacters( + r.page + 1, + r.search, + ) + ) + + private suspend fun loadNextStudiosPage(r: StudioSearchResults) = studioResult.postValue( + Anilist.query.searchStudios( + r.page + 1, + r.search, + ) + ) + + private suspend fun loadNextStaffPage(r: StaffSearchResults) = staffResult.postValue( + Anilist.query.searchStaff( + r.page + 1, + r.search, + ) + ) + + private suspend fun loadNextUserPage(r: UserSearchResults) = userResult.postValue( + Anilist.query.searchUsers( + r.page + 1, + r.search, + ) + ) } class GenresViewModel : ViewModel() { diff --git a/app/src/main/java/ani/dantotsu/connections/anilist/anilistGraphql.kt b/app/src/main/java/ani/dantotsu/connections/anilist/anilistGraphql.kt new file mode 100644 index 00000000..2e8f06a5 --- /dev/null +++ b/app/src/main/java/ani/dantotsu/connections/anilist/anilistGraphql.kt @@ -0,0 +1,429 @@ +package ani.dantotsu.connections.anilist + +val standardPageInformation = """ + pageInfo { + total + perPage + currentPage + lastPage + hasNextPage + } +""".prepare() + +fun String.prepare() = this.trimIndent().replace("\n", " ").replace(""" """, "") + +fun characterInformation(includeMediaInfo: Boolean) = """ + id + name { + first + middle + last + full + native + userPreferred + } + image { + large + medium + } + age + gender + description(asHtml: true) + dateOfBirth { + year + month + day + } + ${if (includeMediaInfo) """ + media(page: 0,sort:[POPULARITY_DESC,SCORE_DESC]) { + $standardPageInformation + edges { + id + voiceActors { + id, + name { + userPreferred + } + languageV2, + image { + medium, + large + } + } + characterRole + node { + id + idMal + isAdult + status + chapters + episodes + nextAiringEpisode { episode } + type + meanScore + isFavourite + format + bannerImage + countryOfOrigin + coverImage { large } + title { + english + romaji + userPreferred + } + mediaListEntry { + progress + private + score(format: POINT_100) + status + } + } + } + }""".prepare() else ""} +""".prepare() + +fun studioInformation(page: Int, perPage: Int) = """ + id + name + isFavourite + favourites + media(page: $page, sort:START_DATE_DESC, perPage: $perPage) { + $standardPageInformation + edges { + id + node { + id + idMal + isAdult + status + chapters + episodes + nextAiringEpisode { episode } + type + meanScore + startDate{ year } + isFavourite + format + bannerImage + countryOfOrigin + coverImage { large } + title { + english + romaji + userPreferred + } + mediaListEntry { + progress + private + score(format: POINT_100) + status + } + } + } + } +""".prepare() + +fun staffInformation(page: Int, perPage: Int) = """ + id + name { + first + middle + last + full + native + userPreferred + } + image { + large + medium + } + dateOfBirth { + year + month + day + } + dateOfDeath { + year + month + day + } + age + yearsActive + homeTown + staffMedia(page: $page,sort:START_DATE_DESC, perPage: $perPage) { + $standardPageInformation + edges { + staffRole + id + node { + id + idMal + isAdult + status + chapters + episodes + nextAiringEpisode { episode } + type + meanScore + startDate{ year } + isFavourite + format + bannerImage + countryOfOrigin + coverImage { large } + title { + english + romaji + userPreferred + } + mediaListEntry { + progress + private + score(format: POINT_100) + status + } + } + } + } +""".prepare() + +fun userInformation() = """ + id + name + about(asHtml: true) + avatar { + large + medium + } + bannerImage + isFollowing + isFollower + isBlocked + siteUrl +""".prepare() + +fun aniMangaSearch(perPage: Int?) = """ + query (${"$"}page: Int = 1, ${"$"}id: Int, ${"$"}type: MediaType, ${"$"}isAdult: Boolean = false, ${"$"}search: String, ${"$"}format: [MediaFormat], ${"$"}status: MediaStatus, ${"$"}countryOfOrigin: CountryCode, ${"$"}source: MediaSource, ${"$"}season: MediaSeason, ${"$"}seasonYear: Int, ${"$"}year: String, ${"$"}onList: Boolean, ${"$"}yearLesser: FuzzyDateInt, ${"$"}yearGreater: FuzzyDateInt, ${"$"}episodeLesser: Int, ${"$"}episodeGreater: Int, ${"$"}durationLesser: Int, ${"$"}durationGreater: Int, ${"$"}chapterLesser: Int, ${"$"}chapterGreater: Int, ${"$"}volumeLesser: Int, ${"$"}volumeGreater: Int, ${"$"}licensedBy: [String], ${"$"}isLicensed: Boolean, ${"$"}genres: [String], ${"$"}excludedGenres: [String], ${"$"}tags: [String], ${"$"}excludedTags: [String], ${"$"}minimumTagRank: Int, ${"$"}sort: [MediaSort] = [POPULARITY_DESC, SCORE_DESC, START_DATE_DESC]) { + Page(page: ${"$"}page, perPage: ${perPage ?: 50}) { + $standardPageInformation + media(id: ${"$"}id, type: ${"$"}type, season: ${"$"}season, format_in: ${"$"}format, status: ${"$"}status, countryOfOrigin: ${"$"}countryOfOrigin, source: ${"$"}source, search: ${"$"}search, onList: ${"$"}onList, seasonYear: ${"$"}seasonYear, startDate_like: ${"$"}year, startDate_lesser: ${"$"}yearLesser, startDate_greater: ${"$"}yearGreater, episodes_lesser: ${"$"}episodeLesser, episodes_greater: ${"$"}episodeGreater, duration_lesser: ${"$"}durationLesser, duration_greater: ${"$"}durationGreater, chapters_lesser: ${"$"}chapterLesser, chapters_greater: ${"$"}chapterGreater, volumes_lesser: ${"$"}volumeLesser, volumes_greater: ${"$"}volumeGreater, licensedBy_in: ${"$"}licensedBy, isLicensed: ${"$"}isLicensed, genre_in: ${"$"}genres, genre_not_in: ${"$"}excludedGenres, tag_in: ${"$"}tags, tag_not_in: ${"$"}excludedTags, minimumTagRank: ${"$"}minimumTagRank, sort: ${"$"}sort, isAdult: ${"$"}isAdult) { + ${standardMediaInformation()} + } + } + } +""".prepare() + +fun standardMediaInformation() = """ +id +idMal +siteUrl +isAdult +status(version: 2) +chapters +episodes +nextAiringEpisode { + episode + airingAt +} +type +genres +meanScore +popularity +favourites +isFavourite +format +bannerImage +countryOfOrigin +coverImage { + large + extraLarge +} +title { + english + romaji + userPreferred +} +mediaListEntry { + progress + private + score(format: POINT_100) + status +} +""".prepare() + +fun fullMediaInformation(id: Int) = """ +{ + Media(id: $id) { + streamingEpisodes { + title + thumbnail + url + site + } + 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 + } + } + } + } + ${standardMediaInformation()} + source + duration + season + seasonYear + startDate { + year + month + day + } + endDate { + year + month + day + } + 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 { + ${standardMediaInformation()} + } + } + } + staffPreview: staff(perPage: 8, sort: [RELEVANCE, ID]) { + edges { + role + node { + id + image { + large + medium + } + name { + userPreferred + } + } + } + } + recommendations(sort: RATING_DESC) { + nodes { + mediaRecommendation { + ${standardMediaInformation()} + } + } + } + externalLinks { + url + site + } + } + Page(page: 1) { + $standardPageInformation + mediaList(isFollowing: true, sort: [STATUS], mediaId: $id) { + id + status + score(format: POINT_100) + progress + progressVolumes + user { + id + name + avatar { + large + medium + } + } + } + } +} + +""".prepare() \ No newline at end of file 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 a54e3ad8..b833ae8e 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 @@ -446,7 +446,7 @@ data class MediaEdge( @SerialName("staffRole") var staffRole: String?, // The voice actors of the character - // @SerialName("voiceActors") var voiceActors: List?, + @SerialName("voiceActors") var voiceActors: List?, // The voice actors of the character with role date // @SerialName("voiceActorRoles") var voiceActorRoles: List?, diff --git a/app/src/main/java/ani/dantotsu/home/AnimeFragment.kt b/app/src/main/java/ani/dantotsu/home/AnimeFragment.kt index 87713885..f11b8b4a 100644 --- a/app/src/main/java/ani/dantotsu/home/AnimeFragment.kt +++ b/app/src/main/java/ani/dantotsu/home/AnimeFragment.kt @@ -24,7 +24,7 @@ import ani.dantotsu.Refresh import ani.dantotsu.bottomBar import ani.dantotsu.connections.anilist.Anilist import ani.dantotsu.connections.anilist.AnilistAnimeViewModel -import ani.dantotsu.connections.anilist.SearchResults +import ani.dantotsu.connections.anilist.AniMangaSearchResults import ani.dantotsu.connections.anilist.getUserId import ani.dantotsu.databinding.FragmentAnimeBinding import ani.dantotsu.media.MediaAdaptor @@ -100,7 +100,7 @@ class AnimeFragment : Fragment() { var loading = true if (model.notSet) { model.notSet = false - model.searchResults = SearchResults( + model.aniMangaSearchResults = AniMangaSearchResults( "ANIME", isAdult = false, onList = false, @@ -109,7 +109,7 @@ class AnimeFragment : Fragment() { sort = Anilist.sortBy[1] ) } - val popularAdaptor = MediaAdaptor(1, model.searchResults.results, requireActivity()) + val popularAdaptor = MediaAdaptor(1, model.aniMangaSearchResults.results, requireActivity()) val progressAdaptor = ProgressAdapter(searched = model.searched) val adapter = ConcatAdapter(animePageAdapter, popularAdaptor, progressAdaptor) binding.animePageRecyclerView.adapter = adapter @@ -142,7 +142,7 @@ class AnimeFragment : Fragment() { animePageAdapter.onIncludeListClick = { checked -> oldIncludeList = !checked loading = true - model.searchResults.results.clear() + model.aniMangaSearchResults.results.clear() popularAdaptor.notifyDataSetChanged() scope.launch(Dispatchers.IO) { model.loadPopular("ANIME", sort = Anilist.sortBy[1], onList = checked) @@ -152,17 +152,17 @@ class AnimeFragment : Fragment() { model.getPopular().observe(viewLifecycleOwner) { if (it != null) { if (oldIncludeList == (it.onList != false)) { - val prev = model.searchResults.results.size - model.searchResults.results.addAll(it.results) + val prev = model.aniMangaSearchResults.results.size + model.aniMangaSearchResults.results.addAll(it.results) popularAdaptor.notifyItemRangeInserted(prev, it.results.size) } else { - model.searchResults.results.addAll(it.results) + model.aniMangaSearchResults.results.addAll(it.results) popularAdaptor.notifyDataSetChanged() oldIncludeList = it.onList ?: true } - model.searchResults.onList = it.onList - model.searchResults.hasNextPage = it.hasNextPage - model.searchResults.page = it.page + model.aniMangaSearchResults.onList = it.onList + model.aniMangaSearchResults.hasNextPage = it.hasNextPage + model.aniMangaSearchResults.page = it.page if (it.hasNextPage) progressAdaptor.bar?.visibility = View.VISIBLE else { @@ -177,10 +177,10 @@ class AnimeFragment : Fragment() { RecyclerView.OnScrollListener() { override fun onScrolled(v: RecyclerView, dx: Int, dy: Int) { if (!v.canScrollVertically(1)) { - if (model.searchResults.hasNextPage && model.searchResults.results.isNotEmpty() && !loading) { + if (model.aniMangaSearchResults.hasNextPage && model.aniMangaSearchResults.results.isNotEmpty() && !loading) { scope.launch(Dispatchers.IO) { loading = true - model.loadNextPage(model.searchResults) + model.loadNextPage(model.aniMangaSearchResults) } } } diff --git a/app/src/main/java/ani/dantotsu/home/AnimePageAdapter.kt b/app/src/main/java/ani/dantotsu/home/AnimePageAdapter.kt index 0f31fca2..164affd3 100644 --- a/app/src/main/java/ani/dantotsu/home/AnimePageAdapter.kt +++ b/app/src/main/java/ani/dantotsu/home/AnimePageAdapter.kt @@ -13,7 +13,6 @@ import androidx.core.content.ContextCompat import androidx.core.view.isVisible import androidx.core.view.updateLayoutParams import androidx.core.view.updatePadding -import androidx.lifecycle.LiveData import androidx.lifecycle.MutableLiveData import androidx.recyclerview.widget.LinearLayoutManager import androidx.recyclerview.widget.RecyclerView @@ -21,7 +20,6 @@ import androidx.viewpager2.widget.ViewPager2 import ani.dantotsu.MediaPageTransformer import ani.dantotsu.R import ani.dantotsu.connections.anilist.Anilist -import ani.dantotsu.currContext import ani.dantotsu.databinding.ItemAnimePageBinding import ani.dantotsu.databinding.LayoutTrendingBinding import ani.dantotsu.getAppString @@ -32,7 +30,6 @@ import ani.dantotsu.media.GenreActivity import ani.dantotsu.media.Media import ani.dantotsu.media.MediaAdaptor import ani.dantotsu.media.MediaListViewActivity -import ani.dantotsu.media.SearchActivity import ani.dantotsu.profile.ProfileActivity import ani.dantotsu.px import ani.dantotsu.setSafeOnClickListener @@ -83,12 +80,11 @@ class AnimePageAdapter : RecyclerView.Adapter oldIncludeList = !checked loading = true - model.searchResults.results.clear() + model.aniMangaSearchResults.results.clear() popularAdaptor.notifyDataSetChanged() scope.launch(Dispatchers.IO) { model.loadPopular("MANGA", sort = Anilist.sortBy[1], onList = checked) @@ -230,17 +230,17 @@ class MangaFragment : Fragment() { model.getPopular().observe(viewLifecycleOwner) { if (it != null) { if (oldIncludeList == (it.onList != false)) { - val prev = model.searchResults.results.size - model.searchResults.results.addAll(it.results) + val prev = model.aniMangaSearchResults.results.size + model.aniMangaSearchResults.results.addAll(it.results) popularAdaptor.notifyItemRangeInserted(prev, it.results.size) } else { - model.searchResults.results.addAll(it.results) + model.aniMangaSearchResults.results.addAll(it.results) popularAdaptor.notifyDataSetChanged() oldIncludeList = it.onList ?: true } - model.searchResults.onList = it.onList - model.searchResults.hasNextPage = it.hasNextPage - model.searchResults.page = it.page + model.aniMangaSearchResults.onList = it.onList + model.aniMangaSearchResults.hasNextPage = it.hasNextPage + model.aniMangaSearchResults.page = it.page if (it.hasNextPage) progressAdaptor.bar?.visibility = View.VISIBLE else { diff --git a/app/src/main/java/ani/dantotsu/home/MangaPageAdapter.kt b/app/src/main/java/ani/dantotsu/home/MangaPageAdapter.kt index 2577e3b2..ecada291 100644 --- a/app/src/main/java/ani/dantotsu/home/MangaPageAdapter.kt +++ b/app/src/main/java/ani/dantotsu/home/MangaPageAdapter.kt @@ -82,12 +82,11 @@ class MangaPageAdapter : RecyclerView.Adapter 0 && PrefManager.getVal(PrefName.ShowNotificationRedDot) == true trendingBinding.notificationCount.text = Anilist.unreadNotificationCount.toString() - trendingBinding.searchBar.hint = "MANGA" + trendingBinding.searchBar.hint = binding.root.context.getString(R.string.search) trendingBinding.searchBarText.setOnClickListener { - ContextCompat.startActivity( - it.context, - Intent(it.context, SearchActivity::class.java).putExtra("type", "MANGA"), - null + SearchBottomSheet.newInstance().show( + (binding.root.context as AppCompatActivity).supportFragmentManager, + "search" ) } diff --git a/app/src/main/java/ani/dantotsu/home/SearchBottomSheet.kt b/app/src/main/java/ani/dantotsu/home/SearchBottomSheet.kt new file mode 100644 index 00000000..9eadae0d --- /dev/null +++ b/app/src/main/java/ani/dantotsu/home/SearchBottomSheet.kt @@ -0,0 +1,74 @@ +package ani.dantotsu.home + +import android.content.Context +import android.content.Intent +import android.os.Bundle +import android.view.LayoutInflater +import android.view.View +import android.view.ViewGroup +import androidx.core.content.ContextCompat +import ani.dantotsu.BottomSheetDialogFragment +import ani.dantotsu.connections.anilist.AnilistSearch.SearchType +import ani.dantotsu.connections.anilist.AnilistSearch.SearchType.Companion.toAnilistString +import ani.dantotsu.databinding.BottomSheetSearchBinding +import ani.dantotsu.media.SearchActivity + +class SearchBottomSheet : BottomSheetDialogFragment() { + private var _binding: BottomSheetSearchBinding? = null + private val binding get() = _binding!! + + override fun onCreateView( + inflater: LayoutInflater, + container: ViewGroup?, + savedInstanceState: Bundle? + ): View { + _binding = BottomSheetSearchBinding.inflate(inflater, container, false) + return binding.root + } + + override fun onViewCreated(view: View, savedInstanceState: Bundle?) { + super.onViewCreated(view, savedInstanceState) + + binding.animeSearch.setOnClickListener { + startActivity(requireContext(), SearchType.ANIME) + dismiss() + } + binding.mangaSearch.setOnClickListener { + startActivity(requireContext(), SearchType.MANGA) + dismiss() + } + binding.characterSearch.setOnClickListener { + startActivity(requireContext(), SearchType.CHARACTER) + dismiss() + } + binding.staffSearch.setOnClickListener { + startActivity(requireContext(), SearchType.STAFF) + dismiss() + } + binding.studioSearch.setOnClickListener { + startActivity(requireContext(), SearchType.STUDIO) + dismiss() + } + binding.userSearch.setOnClickListener { + startActivity(requireContext(), SearchType.USER) + dismiss() + } + } + + private fun startActivity(context: Context, type: SearchType) { + ContextCompat.startActivity( + context, + Intent(context, SearchActivity::class.java).putExtra("type", type.toAnilistString()), + null + ) + } + + override fun onDestroyView() { + super.onDestroyView() + _binding = null + } + + companion object { + fun newInstance() = SearchBottomSheet() + } +} \ No newline at end of file diff --git a/app/src/main/java/ani/dantotsu/media/Author.kt b/app/src/main/java/ani/dantotsu/media/Author.kt index 803ef989..46baf07d 100644 --- a/app/src/main/java/ani/dantotsu/media/Author.kt +++ b/app/src/main/java/ani/dantotsu/media/Author.kt @@ -7,6 +7,12 @@ data class Author( var name: String?, var image: String?, var role: String?, + var age: Int? = null, + var yearsActive: List? = null, + var dateOfBirth: String? = null, + var dateOfDeath: String? = null, + var homeTown: String? = null, var yearMedia: MutableMap>? = null, - var character: ArrayList? = null + var character: ArrayList? = null, + var isFav: Boolean = false ) : Serializable diff --git a/app/src/main/java/ani/dantotsu/media/AuthorActivity.kt b/app/src/main/java/ani/dantotsu/media/AuthorActivity.kt index e30d6049..b3ecde2d 100644 --- a/app/src/main/java/ani/dantotsu/media/AuthorActivity.kt +++ b/app/src/main/java/ani/dantotsu/media/AuthorActivity.kt @@ -1,11 +1,13 @@ package ani.dantotsu.media +import android.content.Intent import android.os.Bundle import android.view.View import android.view.ViewGroup import androidx.activity.viewModels import androidx.appcompat.app.AppCompatActivity import androidx.core.content.ContextCompat +import androidx.core.math.MathUtils.clamp import androidx.core.view.updateLayoutParams import androidx.core.view.updatePadding import androidx.lifecycle.MutableLiveData @@ -16,57 +18,127 @@ import androidx.recyclerview.widget.LinearLayoutManager import ani.dantotsu.EmptyAdapter import ani.dantotsu.R import ani.dantotsu.Refresh -import ani.dantotsu.databinding.ActivityAuthorBinding +import ani.dantotsu.connections.anilist.Anilist +import ani.dantotsu.connections.anilist.AnilistMutations +import ani.dantotsu.databinding.ActivityCharacterBinding import ani.dantotsu.initActivity +import ani.dantotsu.loadImage import ani.dantotsu.navBarHeight +import ani.dantotsu.openLinkInBrowser +import ani.dantotsu.others.ImageViewDialog +import ani.dantotsu.others.SpoilerPlugin import ani.dantotsu.others.getSerialized import ani.dantotsu.px +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.google.android.material.appbar.AppBarLayout +import io.noties.markwon.Markwon +import io.noties.markwon.SoftBreakAddsNewLinePlugin import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.launch import kotlinx.coroutines.withContext +import kotlin.math.abs -class AuthorActivity : AppCompatActivity() { - private lateinit var binding: ActivityAuthorBinding +class AuthorActivity : AppCompatActivity(), AppBarLayout.OnOffsetChangedListener { + private lateinit var binding: ActivityCharacterBinding private val scope = lifecycleScope private val model: OtherDetailsViewModel by viewModels() - private var author: Author? = null + private lateinit var author: Author private var loaded = false + + private var screenWidth: Float = 0f + private val percent = 30 + private var mMaxScrollSize = 0 + private var isCollapsed = false + override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) ThemeManager(this).applyTheme() - binding = ActivityAuthorBinding.inflate(layoutInflater) + binding = ActivityCharacterBinding.inflate(layoutInflater) setContentView(binding.root) initActivity(this) - this.window.statusBarColor = ContextCompat.getColor(this, R.color.nav_bg) + screenWidth = resources.displayMetrics.run { widthPixels / density } + if (PrefManager.getVal(PrefName.ImmersiveMode)) this.window.statusBarColor = + ContextCompat.getColor(this, R.color.transparent) - val screenWidth = resources.displayMetrics.run { widthPixels / density } + val banner = + if (PrefManager.getVal(PrefName.BannerAnimations)) binding.characterBanner else binding.characterBannerNoKen - binding.root.updateLayoutParams { topMargin += statusBarHeight } - binding.studioRecycler.updatePadding(bottom = 64f.px + navBarHeight) - binding.studioTitle.isSelected = true + banner.updateLayoutParams { height += statusBarHeight } + binding.characterClose.updateLayoutParams { topMargin += statusBarHeight } + binding.characterCollapsing.minimumHeight = statusBarHeight + binding.characterCover.updateLayoutParams { topMargin += statusBarHeight } + binding.characterRecyclerView.updatePadding(bottom = 64f.px + navBarHeight) + binding.characterTitle.isSelected = true + binding.characterAppBar.addOnOffsetChangedListener(this) - author = intent.getSerialized("author") - binding.studioTitle.text = author?.name - - binding.studioClose.setOnClickListener { + binding.characterClose.setOnClickListener { onBackPressedDispatcher.onBackPressed() } + + author = intent.getSerialized("author") ?: return + binding.characterTitle.text = author.name + binding.characterCoverImage.loadImage(author.image) + binding.characterCoverImage.setOnLongClickListener { + ImageViewDialog.newInstance( + this, + author.name, + author.image + ) + } + val link = "https://anilist.co/staff/${author.id}" + binding.characterShare.setOnClickListener { + val i = Intent(Intent.ACTION_SEND) + i.type = "text/plain" + i.putExtra(Intent.EXTRA_TEXT, link) + startActivity(Intent.createChooser(i, author.name)) + } + binding.characterShare.setOnLongClickListener { + openLinkInBrowser(link) + true + } + lifecycleScope.launch { + withContext(Dispatchers.IO) { + author.isFav = + Anilist.query.isUserFav(AnilistMutations.FavType.STAFF, author.id) + } + withContext(Dispatchers.Main) { + binding.characterFav.setImageResource( + if (author.isFav) R.drawable.ic_round_favorite_24 else R.drawable.ic_round_favorite_border_24 + ) + } + } + binding.characterFav.setOnClickListener { + scope.launch { + lifecycleScope.launch { + if (Anilist.mutation.toggleFav(AnilistMutations.FavType.CHARACTER, author.id)) { + author.isFav = !author.isFav + binding.characterFav.setImageResource( + if (author.isFav) R.drawable.ic_round_favorite_24 else R.drawable.ic_round_favorite_border_24 + ) + } else { + snackString("Failed to toggle favorite") + } + } + } + } model.getAuthor().observe(this) { if (it != null) { author = it loaded = true - binding.studioProgressBar.visibility = View.GONE - binding.studioRecycler.visibility = View.VISIBLE - if (author!!.yearMedia.isNullOrEmpty()) { - binding.studioRecycler.visibility = View.GONE + binding.characterProgress.visibility = View.GONE + binding.characterRecyclerView.visibility = View.VISIBLE + if (author.yearMedia.isNullOrEmpty()) { + binding.characterRecyclerView.visibility = View.GONE } val titlePosition = arrayListOf() val concatAdapter = ConcatAdapter() - val map = author!!.yearMedia ?: return@observe + val map = author.yearMedia ?: return@observe val keys = map.keys.toTypedArray() var pos = 0 @@ -80,6 +152,10 @@ class AuthorActivity : AppCompatActivity() { } } } + val desc = createDesc(author) + val markWon = Markwon.builder(this).usePlugin(SoftBreakAddsNewLinePlugin.create()) + .usePlugin(SpoilerPlugin()).build() + markWon.setMarkdown(binding.authorCharacterDesc, desc) for (i in keys.indices) { val medias = map[keys[i]]!! val empty = if (medias.size >= 4) medias.size % 4 else 4 - medias.size @@ -90,18 +166,18 @@ class AuthorActivity : AppCompatActivity() { concatAdapter.addAdapter(MediaAdaptor(0, medias, this, true)) concatAdapter.addAdapter(EmptyAdapter(empty)) } - binding.studioRecycler.adapter = concatAdapter - binding.studioRecycler.layoutManager = gridLayoutManager + binding.characterRecyclerView.adapter = concatAdapter + binding.characterRecyclerView.layoutManager = gridLayoutManager - binding.charactersRecycler.visibility = View.VISIBLE - binding.charactersText.visibility = View.VISIBLE - binding.charactersRecycler.adapter = - CharacterAdapter(author!!.character ?: arrayListOf()) - binding.charactersRecycler.layoutManager = + binding.authorCharactersRecycler.visibility = View.VISIBLE + binding.AuthorCharactersText.visibility = View.VISIBLE + binding.authorCharactersRecycler.adapter = + CharacterAdapter(author.character ?: arrayListOf()) + binding.authorCharactersRecycler.layoutManager = LinearLayoutManager(this, LinearLayoutManager.HORIZONTAL, false) - if (author!!.character.isNullOrEmpty()) { - binding.charactersRecycler.visibility = View.GONE - binding.charactersText.visibility = View.GONE + if (author.character.isNullOrEmpty()) { + binding.authorCharactersRecycler.visibility = View.GONE + binding.AuthorCharactersText.visibility = View.GONE } } } @@ -109,14 +185,28 @@ class AuthorActivity : AppCompatActivity() { live.observe(this) { if (it) { scope.launch { - if (author != null) - withContext(Dispatchers.IO) { model.loadAuthor(author!!) } + withContext(Dispatchers.IO) { model.loadAuthor(author) } live.postValue(false) } } } } + private fun createDesc(author: Author): String { + val age = if (author.age != null) "${getString(R.string.age)} ${author.age}" else "" + val yearsActive = + if (author.yearsActive != null) "${getString(R.string.years_active)} ${author.yearsActive}" else "" + val dob = + if (author.dateOfBirth != null) "${getString(R.string.birthday)} ${author.dateOfBirth}" else "" + val homeTown = + if (author.homeTown != null) "${getString(R.string.hometown)} ${author.homeTown}" else "" + val dod = + if (author.dateOfDeath != null) "${getString(R.string.date_of_death)} ${author.dateOfDeath}" else "" + + return "$age $yearsActive $dob $homeTown $dod" + } + + override fun onDestroy() { if (Refresh.activity.containsKey(this.hashCode())) { Refresh.activity.remove(this.hashCode()) @@ -125,7 +215,31 @@ class AuthorActivity : AppCompatActivity() { } override fun onResume() { - binding.studioProgressBar.visibility = if (!loaded) View.VISIBLE else View.GONE + binding.characterProgress.visibility = if (!loaded) View.VISIBLE else View.GONE super.onResume() } + + override fun onOffsetChanged(appBar: AppBarLayout, i: Int) { + if (mMaxScrollSize == 0) mMaxScrollSize = appBar.totalScrollRange + val percentage = abs(i) * 100 / mMaxScrollSize + val cap = clamp((percent - percentage) / percent.toFloat(), 0f, 1f) + + binding.characterCover.scaleX = 1f * cap + binding.characterCover.scaleY = 1f * cap + binding.characterCover.cardElevation = 32f * cap + + binding.characterCover.visibility = + if (binding.characterCover.scaleX == 0f) View.GONE else View.VISIBLE + val immersiveMode: Boolean = PrefManager.getVal(PrefName.ImmersiveMode) + if (percentage >= percent && !isCollapsed) { + isCollapsed = true + if (immersiveMode) this.window.statusBarColor = + ContextCompat.getColor(this, R.color.nav_bg) + } + if (percentage <= percent && isCollapsed) { + isCollapsed = false + if (immersiveMode) this.window.statusBarColor = + ContextCompat.getColor(this, R.color.transparent) + } + } } \ No newline at end of file diff --git a/app/src/main/java/ani/dantotsu/media/AuthorAdapter.kt b/app/src/main/java/ani/dantotsu/media/AuthorAdapter.kt index 7b0953fc..d37093ca 100644 --- a/app/src/main/java/ani/dantotsu/media/AuthorAdapter.kt +++ b/app/src/main/java/ani/dantotsu/media/AuthorAdapter.kt @@ -15,7 +15,7 @@ import ani.dantotsu.setAnimation import java.io.Serializable class AuthorAdapter( - private val authorList: ArrayList, + private val authorList: MutableList, ) : RecyclerView.Adapter() { override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): AuthorViewHolder { val binding = @@ -26,7 +26,7 @@ class AuthorAdapter( override fun onBindViewHolder(holder: AuthorViewHolder, position: Int) { val binding = holder.binding setAnimation(binding.root.context, holder.binding.root) - val author = authorList[position] + val author = authorList.getOrNull(position) ?: return binding.itemCompactRelation.text = author.role binding.itemCompactImage.loadImage(author.image) binding.itemCompactTitle.text = author.name diff --git a/app/src/main/java/ani/dantotsu/media/CharacterAdapter.kt b/app/src/main/java/ani/dantotsu/media/CharacterAdapter.kt index 2186791d..c3de3df3 100644 --- a/app/src/main/java/ani/dantotsu/media/CharacterAdapter.kt +++ b/app/src/main/java/ani/dantotsu/media/CharacterAdapter.kt @@ -16,7 +16,7 @@ import ani.dantotsu.setAnimation import java.io.Serializable class CharacterAdapter( - private val characterList: ArrayList + private val characterList: MutableList ) : RecyclerView.Adapter() { override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): CharacterViewHolder { val binding = @@ -27,9 +27,8 @@ class CharacterAdapter( override fun onBindViewHolder(holder: CharacterViewHolder, position: Int) { val binding = holder.binding setAnimation(binding.root.context, holder.binding.root) - val character = characterList[position] - val whitespace = "${character.role} " - character.voiceActor + val character = characterList.getOrNull(position) ?: return + val whitespace = "${if (character.role.lowercase() == "null") "" else character.role} " binding.itemCompactRelation.text = whitespace binding.itemCompactImage.loadImage(character.image) binding.itemCompactTitle.text = character.name diff --git a/app/src/main/java/ani/dantotsu/media/CharacterDetailsActivity.kt b/app/src/main/java/ani/dantotsu/media/CharacterDetailsActivity.kt index 323b689e..ad90ee6b 100644 --- a/app/src/main/java/ani/dantotsu/media/CharacterDetailsActivity.kt +++ b/app/src/main/java/ani/dantotsu/media/CharacterDetailsActivity.kt @@ -9,6 +9,7 @@ import androidx.appcompat.app.AppCompatActivity import androidx.core.content.ContextCompat import androidx.core.math.MathUtils.clamp import androidx.core.view.isGone +import androidx.core.view.isVisible import androidx.core.view.updateLayoutParams import androidx.core.view.updatePadding import androidx.lifecycle.MutableLiveData @@ -45,6 +46,11 @@ class CharacterDetailsActivity : AppCompatActivity(), AppBarLayout.OnOffsetChang private lateinit var character: Character private var loaded = false + private var isCollapsed = false + private val percent = 30 + private var mMaxScrollSize = 0 + private var screenWidth: Float = 0f + override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) @@ -71,6 +77,11 @@ class CharacterDetailsActivity : AppCompatActivity(), AppBarLayout.OnOffsetChang binding.characterClose.setOnClickListener { onBackPressedDispatcher.onBackPressed() } + + binding.authorCharactersRecycler.isVisible = false + binding.AuthorCharactersText.isVisible = false + binding.authorCharacterDesc.isVisible = false + character = intent.getSerialized("character") ?: return binding.characterTitle.text = character.name banner.loadImage(character.banner) @@ -158,11 +169,6 @@ class CharacterDetailsActivity : AppCompatActivity(), AppBarLayout.OnOffsetChang super.onResume() } - private var isCollapsed = false - private val percent = 30 - private var mMaxScrollSize = 0 - private var screenWidth: Float = 0f - override fun onOffsetChanged(appBar: AppBarLayout, i: Int) { if (mMaxScrollSize == 0) mMaxScrollSize = appBar.totalScrollRange val percentage = abs(i) * 100 / mMaxScrollSize diff --git a/app/src/main/java/ani/dantotsu/media/HeaderInterface.kt b/app/src/main/java/ani/dantotsu/media/HeaderInterface.kt new file mode 100644 index 00000000..46937ef0 --- /dev/null +++ b/app/src/main/java/ani/dantotsu/media/HeaderInterface.kt @@ -0,0 +1,77 @@ +package ani.dantotsu.media + +import android.text.TextWatcher +import android.view.LayoutInflater +import android.view.View +import android.view.ViewGroup +import android.view.animation.AlphaAnimation +import android.view.animation.Animation +import androidx.recyclerview.widget.RecyclerView +import ani.dantotsu.databinding.ItemSearchHeaderBinding + +abstract class HeaderInterface: RecyclerView.Adapter() { + private val itemViewType = 6969 + var search: Runnable? = null + var requestFocus: Runnable? = null + protected var textWatcher: TextWatcher? = null + protected lateinit var searchHistoryAdapter: SearchHistoryAdapter + protected lateinit var binding: ItemSearchHeaderBinding + + override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): SearchHeaderViewHolder { + val binding = + ItemSearchHeaderBinding.inflate(LayoutInflater.from(parent.context), parent, false) + return SearchHeaderViewHolder(binding) + } + + fun setHistoryVisibility(visible: Boolean) { + if (visible) { + binding.searchResultLayout.startAnimation(fadeOutAnimation()) + binding.searchHistoryList.startAnimation(fadeInAnimation()) + binding.searchResultLayout.visibility = View.GONE + binding.searchHistoryList.visibility = View.VISIBLE + binding.searchByImage.visibility = View.VISIBLE + } else { + if (binding.searchResultLayout.visibility != View.VISIBLE) { + binding.searchResultLayout.startAnimation(fadeInAnimation()) + binding.searchHistoryList.startAnimation(fadeOutAnimation()) + } + + binding.searchResultLayout.visibility = View.VISIBLE + binding.clearHistory.visibility = View.GONE + binding.searchHistoryList.visibility = View.GONE + binding.searchByImage.visibility = View.GONE + } + } + + private fun fadeInAnimation(): Animation { + return AlphaAnimation(0f, 1f).apply { + duration = 150 + } + } + + protected fun fadeOutAnimation(): Animation { + return AlphaAnimation(1f, 0f).apply { + duration = 150 + } + } + + protected fun updateClearHistoryVisibility() { + binding.clearHistory.visibility = + if (searchHistoryAdapter.itemCount > 0) View.VISIBLE else View.GONE + } + + fun addHistory() { + if (::searchHistoryAdapter.isInitialized && binding.searchBarText.text.toString() + .isNotBlank() + ) searchHistoryAdapter.add(binding.searchBarText.text.toString()) + } + + inner class SearchHeaderViewHolder(val binding: ItemSearchHeaderBinding) : + RecyclerView.ViewHolder(binding.root) + + override fun getItemCount(): Int = 1 + + override fun getItemViewType(position: Int): Int { + return itemViewType + } +} \ No newline at end of file diff --git a/app/src/main/java/ani/dantotsu/media/SearchActivity.kt b/app/src/main/java/ani/dantotsu/media/SearchActivity.kt index 358c9700..b4e16391 100644 --- a/app/src/main/java/ani/dantotsu/media/SearchActivity.kt +++ b/app/src/main/java/ani/dantotsu/media/SearchActivity.kt @@ -15,10 +15,16 @@ import androidx.recyclerview.widget.GridLayoutManager import androidx.recyclerview.widget.RecyclerView import ani.dantotsu.connections.anilist.Anilist import ani.dantotsu.connections.anilist.AnilistSearch -import ani.dantotsu.connections.anilist.SearchResults +import ani.dantotsu.connections.anilist.AnilistSearch.SearchType +import ani.dantotsu.connections.anilist.AniMangaSearchResults +import ani.dantotsu.connections.anilist.CharacterSearchResults +import ani.dantotsu.connections.anilist.StaffSearchResults +import ani.dantotsu.connections.anilist.StudioSearchResults +import ani.dantotsu.connections.anilist.UserSearchResults import ani.dantotsu.databinding.ActivitySearchBinding import ani.dantotsu.initActivity import ani.dantotsu.navBarHeight +import ani.dantotsu.profile.UsersAdapter import ani.dantotsu.px import ani.dantotsu.settings.saving.PrefManager import ani.dantotsu.settings.saving.PrefName @@ -35,14 +41,25 @@ class SearchActivity : AppCompatActivity() { val model: AnilistSearch by viewModels() var style: Int = 0 + lateinit var searchType: SearchType private var screenWidth: Float = 0f private lateinit var mediaAdaptor: MediaAdaptor + private lateinit var characterAdaptor: CharacterAdapter + private lateinit var studioAdaptor: StudioAdapter + private lateinit var staffAdaptor: AuthorAdapter + private lateinit var usersAdapter: UsersAdapter + private lateinit var progressAdapter: ProgressAdapter private lateinit var concatAdapter: ConcatAdapter - private lateinit var headerAdaptor: SearchAdapter + private lateinit var headerAdaptor: HeaderInterface + + lateinit var aniMangaResult: AniMangaSearchResults + lateinit var characterResult: CharacterSearchResults + lateinit var studioResult: StudioSearchResults + lateinit var staffResult: StaffSearchResults + lateinit var userResult: UserSearchResults - lateinit var result: SearchResults lateinit var updateChips: (() -> Unit) override fun onCreate(savedInstanceState: Bundle?) { @@ -59,39 +76,117 @@ class SearchActivity : AppCompatActivity() { bottom = navBarHeight + 80f.px ) - style = PrefManager.getVal(PrefName.SearchStyle) - var listOnly: Boolean? = intent.getBooleanExtra("listOnly", false) - if (!listOnly!!) listOnly = null - val notSet = model.notSet - if (model.notSet) { - model.notSet = false - model.searchResults = SearchResults( - intent.getStringExtra("type") ?: "ANIME", - isAdult = if (Anilist.adult) intent.getBooleanExtra("hentai", false) else false, - onList = listOnly, - search = intent.getStringExtra("query"), - genres = intent.getStringExtra("genre")?.let { mutableListOf(it) }, - tags = intent.getStringExtra("tag")?.let { mutableListOf(it) }, - sort = intent.getStringExtra("sortBy"), - status = intent.getStringExtra("status"), - source = intent.getStringExtra("source"), - countryOfOrigin = intent.getStringExtra("country"), - season = intent.getStringExtra("season"), - seasonYear = if (intent.getStringExtra("type") == "ANIME") intent.getStringExtra("seasonYear") - ?.toIntOrNull() else null, - startYear = if (intent.getStringExtra("type") == "MANGA") intent.getStringExtra("seasonYear") - ?.toIntOrNull() else null, - results = mutableListOf(), - hasNextPage = false - ) + searchType = SearchType.fromString(intent.getStringExtra("type") ?: "ANIME") + when (searchType) { + SearchType.ANIME, SearchType.MANGA -> { + style = PrefManager.getVal(PrefName.SearchStyle) + var listOnly: Boolean? = intent.getBooleanExtra("listOnly", false) + if (!listOnly!!) listOnly = null + + if (model.notSet) { + model.notSet = false + model.aniMangaSearchResults = AniMangaSearchResults( + intent.getStringExtra("type") ?: "ANIME", + isAdult = if (Anilist.adult) intent.getBooleanExtra( + "hentai", + false + ) else false, + onList = listOnly, + search = intent.getStringExtra("query"), + genres = intent.getStringExtra("genre")?.let { mutableListOf(it) }, + tags = intent.getStringExtra("tag")?.let { mutableListOf(it) }, + sort = intent.getStringExtra("sortBy"), + status = intent.getStringExtra("status"), + source = intent.getStringExtra("source"), + countryOfOrigin = intent.getStringExtra("country"), + season = intent.getStringExtra("season"), + seasonYear = if (intent.getStringExtra("type") == "ANIME") intent.getStringExtra( + "seasonYear" + ) + ?.toIntOrNull() else null, + startYear = if (intent.getStringExtra("type") == "MANGA") intent.getStringExtra( + "seasonYear" + ) + ?.toIntOrNull() else null, + results = mutableListOf(), + hasNextPage = false + ) + } + + aniMangaResult = model.aniMangaSearchResults + mediaAdaptor = + MediaAdaptor( + style, + model.aniMangaSearchResults.results, + this, + matchParent = true + ) + } + + SearchType.CHARACTER -> { + if (model.notSet) { + model.notSet = false + model.characterSearchResults = CharacterSearchResults( + search = intent.getStringExtra("query"), + results = mutableListOf(), + hasNextPage = false + ) + + characterResult = model.characterSearchResults + characterAdaptor = CharacterAdapter(model.characterSearchResults.results) + } + } + + SearchType.STUDIO -> { + if (model.notSet) { + model.notSet = false + model.studioSearchResults = StudioSearchResults( + search = intent.getStringExtra("query"), + results = mutableListOf(), + hasNextPage = false + ) + + studioResult = model.studioSearchResults + studioAdaptor = StudioAdapter(model.studioSearchResults.results) + } + } + + SearchType.STAFF -> { + if (model.notSet) { + model.notSet = false + model.staffSearchResults = StaffSearchResults( + search = intent.getStringExtra("query"), + results = mutableListOf(), + hasNextPage = false + ) + + staffResult = model.staffSearchResults + staffAdaptor = AuthorAdapter(model.staffSearchResults.results) + } + } + + SearchType.USER -> { + if (model.notSet) { + model.notSet = false + model.userSearchResults = UserSearchResults( + search = intent.getStringExtra("query"), + results = mutableListOf(), + hasNextPage = false + ) + + userResult = model.userSearchResults + usersAdapter = UsersAdapter(model.userSearchResults.results, grid = true) + } + } } - result = model.searchResults - progressAdapter = ProgressAdapter(searched = model.searched) - mediaAdaptor = MediaAdaptor(style, model.searchResults.results, this, matchParent = true) - headerAdaptor = SearchAdapter(this, model.searchResults.type) + headerAdaptor = if (searchType == SearchType.ANIME || searchType == SearchType.MANGA) { + SearchAdapter(this, searchType) + } else { + SupportingSearchAdapter(this, searchType) + } val gridSize = (screenWidth / 120f).toInt() val gridLayoutManager = GridLayoutManager(this, gridSize) @@ -108,7 +203,27 @@ class SearchActivity : AppCompatActivity() { } } - concatAdapter = ConcatAdapter(headerAdaptor, mediaAdaptor, progressAdapter) + concatAdapter = when (searchType) { + SearchType.ANIME, SearchType.MANGA -> { + ConcatAdapter(headerAdaptor, mediaAdaptor, progressAdapter) + } + + SearchType.CHARACTER -> { + ConcatAdapter(headerAdaptor, characterAdaptor, progressAdapter) + } + + SearchType.STUDIO -> { + ConcatAdapter(headerAdaptor, studioAdaptor, progressAdapter) + } + + SearchType.STAFF -> { + ConcatAdapter(headerAdaptor, staffAdaptor, progressAdapter) + } + + SearchType.USER -> { + ConcatAdapter(headerAdaptor, usersAdapter, progressAdapter) + } + } binding.searchRecyclerView.layoutManager = gridLayoutManager binding.searchRecyclerView.adapter = concatAdapter @@ -117,9 +232,9 @@ class SearchActivity : AppCompatActivity() { RecyclerView.OnScrollListener() { override fun onScrolled(v: RecyclerView, dx: Int, dy: Int) { if (!v.canScrollVertically(1)) { - if (model.searchResults.hasNextPage && model.searchResults.results.isNotEmpty() && !loading) { + if (model.hasNextPage(searchType) && model.resultsIsNotEmpty(searchType) && !loading) { scope.launch(Dispatchers.IO) { - model.loadNextPage(model.searchResults) + model.loadNextPage(searchType) } } } @@ -127,34 +242,110 @@ class SearchActivity : AppCompatActivity() { } }) - model.getSearch().observe(this) { - if (it != null) { - model.searchResults.apply { - onList = it.onList - isAdult = it.isAdult - perPage = it.perPage - search = it.search - sort = it.sort - genres = it.genres - excludedGenres = it.excludedGenres - excludedTags = it.excludedTags - tags = it.tags - season = it.season - startYear = it.startYear - seasonYear = it.seasonYear - status = it.status - source = it.source - format = it.format - countryOfOrigin = it.countryOfOrigin - page = it.page - hasNextPage = it.hasNextPage + when (searchType) { + SearchType.ANIME, SearchType.MANGA -> { + model.getSearch(searchType).observe(this) { + if (it != null) { + model.aniMangaSearchResults.apply { + onList = it.onList + isAdult = it.isAdult + perPage = it.perPage + search = it.search + sort = it.sort + genres = it.genres + excludedGenres = it.excludedGenres + excludedTags = it.excludedTags + tags = it.tags + season = it.season + startYear = it.startYear + seasonYear = it.seasonYear + status = it.status + source = it.source + format = it.format + countryOfOrigin = it.countryOfOrigin + page = it.page + hasNextPage = it.hasNextPage + } + + val prev = model.aniMangaSearchResults.results.size + model.aniMangaSearchResults.results.addAll(it.results) + mediaAdaptor.notifyItemRangeInserted(prev, it.results.size) + + progressAdapter.bar?.isVisible = it.hasNextPage + } } + } - val prev = model.searchResults.results.size - model.searchResults.results.addAll(it.results) - mediaAdaptor.notifyItemRangeInserted(prev, it.results.size) + SearchType.CHARACTER -> { + model.getSearch(searchType).observe(this) { + if (it != null) { + model.characterSearchResults.apply { + search = it.search + page = it.page + hasNextPage = it.hasNextPage + } - progressAdapter.bar?.isVisible = it.hasNextPage + val prev = model.characterSearchResults.results.size + model.characterSearchResults.results.addAll(it.results) + characterAdaptor.notifyItemRangeInserted(prev, it.results.size) + + progressAdapter.bar?.isVisible = it.hasNextPage + } + } + } + + SearchType.STUDIO -> { + model.getSearch(searchType).observe(this) { + if (it != null) { + model.studioSearchResults.apply { + search = it.search + page = it.page + hasNextPage = it.hasNextPage + } + + val prev = model.studioSearchResults.results.size + model.studioSearchResults.results.addAll(it.results) + studioAdaptor.notifyItemRangeInserted(prev, it.results.size) + + progressAdapter.bar?.isVisible = it.hasNextPage + } + } + } + + SearchType.STAFF -> { + model.getSearch(searchType).observe(this) { + if (it != null) { + model.staffSearchResults.apply { + search = it.search + page = it.page + hasNextPage = it.hasNextPage + } + + val prev = model.staffSearchResults.results.size + model.staffSearchResults.results.addAll(it.results) + staffAdaptor.notifyItemRangeInserted(prev, it.results.size) + + progressAdapter.bar?.isVisible = it.hasNextPage + } + } + } + + SearchType.USER -> { + model.getSearch(searchType).observe(this) { + if (it != null) { + model.userSearchResults.apply { + search = it.search + page = it.page + hasNextPage = it.hasNextPage + } + + val prev = model.userSearchResults.results.size + model.userSearchResults.results.addAll(it.results) + usersAdapter.notifyItemRangeInserted(prev, it.results.size) + + progressAdapter.bar?.isVisible = it.hasNextPage + } + } } } @@ -179,8 +370,32 @@ class SearchActivity : AppCompatActivity() { fun emptyMediaAdapter() { searchTimer.cancel() searchTimer.purge() - mediaAdaptor.notifyItemRangeRemoved(0, model.searchResults.results.size) - model.searchResults.results.clear() + when (searchType) { + SearchType.ANIME, SearchType.MANGA -> { + mediaAdaptor.notifyItemRangeRemoved(0, model.aniMangaSearchResults.results.size) + model.aniMangaSearchResults.results.clear() + } + + SearchType.CHARACTER -> { + characterAdaptor.notifyItemRangeRemoved(0, model.characterSearchResults.results.size) + model.characterSearchResults.results.clear() + } + + SearchType.STUDIO -> { + studioAdaptor.notifyItemRangeRemoved(0, model.studioSearchResults.results.size) + model.studioSearchResults.results.clear() + } + + SearchType.STAFF -> { + staffAdaptor.notifyItemRangeRemoved(0, model.staffSearchResults.results.size) + model.staffSearchResults.results.clear() + } + + SearchType.USER -> { + usersAdapter.notifyItemRangeRemoved(0, model.userSearchResults.results.size) + model.userSearchResults.results.clear() + } + } progressAdapter.bar?.visibility = View.GONE } @@ -188,10 +403,30 @@ class SearchActivity : AppCompatActivity() { private var loading = false fun search() { headerAdaptor.setHistoryVisibility(false) - val size = model.searchResults.results.size - model.searchResults.results.clear() + val size = model.size(searchType) + model.clearResults(searchType) binding.searchRecyclerView.post { - mediaAdaptor.notifyItemRangeRemoved(0, size) + when (searchType) { + SearchType.ANIME, SearchType.MANGA -> { + mediaAdaptor.notifyItemRangeRemoved(0, size) + } + + SearchType.CHARACTER -> { + characterAdaptor.notifyItemRangeRemoved(0, size) + } + + SearchType.STUDIO -> { + studioAdaptor.notifyItemRangeRemoved(0, size) + } + + SearchType.STAFF -> { + staffAdaptor.notifyItemRangeRemoved(0, size) + } + + SearchType.USER -> { + usersAdapter.notifyItemRangeRemoved(0, size) + } + } } progressAdapter.bar?.visibility = View.VISIBLE @@ -202,7 +437,7 @@ class SearchActivity : AppCompatActivity() { override fun run() { scope.launch(Dispatchers.IO) { loading = true - model.loadSearch(result) + model.loadSearch(searchType) loading = false } } @@ -213,8 +448,10 @@ class SearchActivity : AppCompatActivity() { @SuppressLint("NotifyDataSetChanged") fun recycler() { - mediaAdaptor.type = style - mediaAdaptor.notifyDataSetChanged() + if (searchType == SearchType.ANIME || searchType == SearchType.MANGA) { + mediaAdaptor.type = style + mediaAdaptor.notifyDataSetChanged() + } } var state: Parcelable? = null diff --git a/app/src/main/java/ani/dantotsu/media/SearchAdapter.kt b/app/src/main/java/ani/dantotsu/media/SearchAdapter.kt index 21f0b683..15081d35 100644 --- a/app/src/main/java/ani/dantotsu/media/SearchAdapter.kt +++ b/app/src/main/java/ani/dantotsu/media/SearchAdapter.kt @@ -9,8 +9,6 @@ import android.view.LayoutInflater import android.view.MotionEvent import android.view.View import android.view.ViewGroup -import android.view.animation.AlphaAnimation -import android.view.animation.Animation import android.view.inputmethod.EditorInfo import android.view.inputmethod.InputMethodManager import android.widget.PopupMenu @@ -22,8 +20,8 @@ import androidx.recyclerview.widget.RecyclerView.HORIZONTAL import ani.dantotsu.App.Companion.context import ani.dantotsu.R import ani.dantotsu.connections.anilist.Anilist +import ani.dantotsu.connections.anilist.AnilistSearch.SearchType import ani.dantotsu.databinding.ItemChipBinding -import ani.dantotsu.databinding.ItemSearchHeaderBinding import ani.dantotsu.openLinkInBrowser import ani.dantotsu.others.imagesearch.ImageSearchActivity import ani.dantotsu.settings.saving.PrefManager @@ -36,18 +34,11 @@ import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.delay import kotlinx.coroutines.launch - -class SearchAdapter(private val activity: SearchActivity, private val type: String) : - RecyclerView.Adapter() { - private val itemViewType = 6969 - var search: Runnable? = null - var requestFocus: Runnable? = null - private var textWatcher: TextWatcher? = null - private lateinit var searchHistoryAdapter: SearchHistoryAdapter - private lateinit var binding: ItemSearchHeaderBinding +class SearchAdapter(private val activity: SearchActivity, private val type: SearchType) : + HeaderInterface() { private fun updateFilterTextViewDrawable() { - val filterDrawable = when (activity.result.sort) { + val filterDrawable = when (activity.aniMangaResult.sort) { Anilist.sortBy[0] -> R.drawable.ic_round_area_chart_24 Anilist.sortBy[1] -> R.drawable.ic_round_filter_peak_24 Anilist.sortBy[2] -> R.drawable.ic_round_star_graph_24 @@ -60,12 +51,6 @@ class SearchAdapter(private val activity: SearchActivity, private val type: Stri binding.filterTextView.setCompoundDrawablesWithIntrinsicBounds(filterDrawable, 0, 0, 0) } - override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): SearchHeaderViewHolder { - val binding = - ItemSearchHeaderBinding.inflate(LayoutInflater.from(parent.context), parent, false) - return SearchHeaderViewHolder(binding) - } - @SuppressLint("ClickableViewAccessibility") override fun onBindViewHolder(holder: SearchHeaderViewHolder, position: Int) { binding = holder.binding @@ -79,6 +64,10 @@ class SearchAdapter(private val activity: SearchActivity, private val type: Stri val imm: InputMethodManager = activity.getSystemService(AppCompatActivity.INPUT_METHOD_SERVICE) as InputMethodManager + if (activity.searchType != SearchType.MANGA && activity.searchType != SearchType.ANIME) { + throw IllegalArgumentException("Invalid search type (wrong adapter)") + } + when (activity.style) { 0 -> { binding.searchResultGrid.alpha = 1f @@ -91,7 +80,7 @@ class SearchAdapter(private val activity: SearchActivity, private val type: Stri } } - binding.searchBar.hint = activity.result.type + binding.searchBar.hint = activity.aniMangaResult.type if (PrefManager.getVal(PrefName.Incognito)) { val startIconDrawableRes = R.drawable.ic_incognito_24 val startIconDrawable: Drawable? = @@ -99,11 +88,11 @@ class SearchAdapter(private val activity: SearchActivity, private val type: Stri binding.searchBar.startIconDrawable = startIconDrawable } - var adult = activity.result.isAdult - var listOnly = activity.result.onList + var adult = activity.aniMangaResult.isAdult + var listOnly = activity.aniMangaResult.onList binding.searchBarText.removeTextChangedListener(textWatcher) - binding.searchBarText.setText(activity.result.search) + binding.searchBarText.setText(activity.aniMangaResult.search) binding.searchAdultCheck.isChecked = adult binding.searchList.isChecked = listOnly == true @@ -124,49 +113,49 @@ class SearchAdapter(private val activity: SearchActivity, private val type: Stri popupMenu.setOnMenuItemClickListener { item -> when (item.itemId) { R.id.sort_by_score -> { - activity.result.sort = Anilist.sortBy[0] + activity.aniMangaResult.sort = Anilist.sortBy[0] activity.updateChips.invoke() activity.search() updateFilterTextViewDrawable() } R.id.sort_by_popular -> { - activity.result.sort = Anilist.sortBy[1] + activity.aniMangaResult.sort = Anilist.sortBy[1] activity.updateChips.invoke() activity.search() updateFilterTextViewDrawable() } R.id.sort_by_trending -> { - activity.result.sort = Anilist.sortBy[2] + activity.aniMangaResult.sort = Anilist.sortBy[2] activity.updateChips.invoke() activity.search() updateFilterTextViewDrawable() } R.id.sort_by_recent -> { - activity.result.sort = Anilist.sortBy[3] + activity.aniMangaResult.sort = Anilist.sortBy[3] activity.updateChips.invoke() activity.search() updateFilterTextViewDrawable() } R.id.sort_by_a_z -> { - activity.result.sort = Anilist.sortBy[4] + activity.aniMangaResult.sort = Anilist.sortBy[4] activity.updateChips.invoke() activity.search() updateFilterTextViewDrawable() } R.id.sort_by_z_a -> { - activity.result.sort = Anilist.sortBy[5] + activity.aniMangaResult.sort = Anilist.sortBy[5] activity.updateChips.invoke() activity.search() updateFilterTextViewDrawable() } R.id.sort_by_pure_pain -> { - activity.result.sort = Anilist.sortBy[6] + activity.aniMangaResult.sort = Anilist.sortBy[6] activity.updateChips.invoke() activity.search() updateFilterTextViewDrawable() @@ -177,7 +166,7 @@ class SearchAdapter(private val activity: SearchActivity, private val type: Stri popupMenu.show() true } - if (activity.result.type != "ANIME") { + if (activity.aniMangaResult.type != "ANIME") { binding.searchByImage.visibility = View.GONE } binding.searchByImage.setOnClickListener { @@ -190,7 +179,7 @@ class SearchAdapter(private val activity: SearchActivity, private val type: Stri } updateClearHistoryVisibility() fun searchTitle() { - activity.result.apply { + activity.aniMangaResult.apply { search = if (binding.searchBarText.text.toString() != "") binding.searchBarText.text.toString() else null onList = listOnly @@ -292,67 +281,12 @@ class SearchAdapter(private val activity: SearchActivity, private val type: Stri requestFocus = Runnable { binding.searchBarText.requestFocus() } } - fun setHistoryVisibility(visible: Boolean) { - if (visible) { - binding.searchResultLayout.startAnimation(fadeOutAnimation()) - binding.searchHistoryList.startAnimation(fadeInAnimation()) - binding.searchResultLayout.visibility = View.GONE - binding.searchHistoryList.visibility = View.VISIBLE - binding.searchByImage.visibility = View.VISIBLE - } else { - if (binding.searchResultLayout.visibility != View.VISIBLE) { - binding.searchResultLayout.startAnimation(fadeInAnimation()) - binding.searchHistoryList.startAnimation(fadeOutAnimation()) - } - - binding.searchResultLayout.visibility = View.VISIBLE - binding.clearHistory.visibility = View.GONE - binding.searchHistoryList.visibility = View.GONE - binding.searchByImage.visibility = View.GONE - } - } - - private fun updateClearHistoryVisibility() { - binding.clearHistory.visibility = - if (searchHistoryAdapter.itemCount > 0) View.VISIBLE else View.GONE - } - - private fun fadeInAnimation(): Animation { - return AlphaAnimation(0f, 1f).apply { - duration = 150 - } - } - - private fun fadeOutAnimation(): Animation { - return AlphaAnimation(1f, 0f).apply { - duration = 150 - } - } - - - fun addHistory() { - if (::searchHistoryAdapter.isInitialized && - binding.searchBarText.text.toString().isNotBlank() - ) - searchHistoryAdapter.add(binding.searchBarText.text.toString()) - } - - override fun getItemCount(): Int = 1 - - inner class SearchHeaderViewHolder(val binding: ItemSearchHeaderBinding) : - RecyclerView.ViewHolder(binding.root) - - override fun getItemViewType(position: Int): Int { - return itemViewType - } - - class SearchChipAdapter( val activity: SearchActivity, private val searchAdapter: SearchAdapter ) : RecyclerView.Adapter() { - private var chips = activity.result.toChipList() + private var chips = activity.aniMangaResult.toChipList() inner class SearchChipViewHolder(val binding: ItemChipBinding) : RecyclerView.ViewHolder(binding.root) @@ -369,7 +303,7 @@ class SearchAdapter(private val activity: SearchActivity, private val type: Stri holder.binding.root.apply { text = chip.text.replace("_", " ") setOnClickListener { - activity.result.removeChip(chip) + activity.aniMangaResult.removeChip(chip) update() activity.search() searchAdapter.updateFilterTextViewDrawable() @@ -379,7 +313,7 @@ class SearchAdapter(private val activity: SearchActivity, private val type: Stri @SuppressLint("NotifyDataSetChanged") fun update() { - chips = activity.result.toChipList() + chips = activity.aniMangaResult.toChipList() notifyDataSetChanged() searchAdapter.updateFilterTextViewDrawable() } diff --git a/app/src/main/java/ani/dantotsu/media/SearchFilterBottomDialog.kt b/app/src/main/java/ani/dantotsu/media/SearchFilterBottomDialog.kt index 6658532c..f9c08d5e 100644 --- a/app/src/main/java/ani/dantotsu/media/SearchFilterBottomDialog.kt +++ b/app/src/main/java/ani/dantotsu/media/SearchFilterBottomDialog.kt @@ -57,7 +57,7 @@ class SearchFilterBottomDialog : BottomSheetDialogFragment() { } private fun setSortByFilterImage() { - val filterDrawable = when (activity.result.sort) { + val filterDrawable = when (activity.aniMangaResult.sort) { Anilist.sortBy[0] -> R.drawable.ic_round_area_chart_24 Anilist.sortBy[1] -> R.drawable.ic_round_filter_peak_24 Anilist.sortBy[2] -> R.drawable.ic_round_star_graph_24 @@ -71,10 +71,10 @@ class SearchFilterBottomDialog : BottomSheetDialogFragment() { } private fun resetSearchFilter() { - activity.result.sort = null + activity.aniMangaResult.sort = null binding.sortByFilter.setImageResource(R.drawable.ic_round_filter_alt_24) startBounceZoomAnimation(binding.sortByFilter) - activity.result.countryOfOrigin = null + activity.aniMangaResult.countryOfOrigin = null binding.countryFilter.setImageResource(R.drawable.ic_round_globe_search_googlefonts) startBounceZoomAnimation(binding.countryFilter) @@ -98,10 +98,10 @@ class SearchFilterBottomDialog : BottomSheetDialogFragment() { activity = requireActivity() as SearchActivity - selectedGenres = activity.result.genres ?: mutableListOf() - exGenres = activity.result.excludedGenres ?: mutableListOf() - selectedTags = activity.result.tags ?: mutableListOf() - exTags = activity.result.excludedTags ?: mutableListOf() + selectedGenres = activity.aniMangaResult.genres ?: mutableListOf() + exGenres = activity.aniMangaResult.excludedGenres ?: mutableListOf() + selectedTags = activity.aniMangaResult.tags ?: mutableListOf() + exTags = activity.aniMangaResult.excludedTags ?: mutableListOf() setSortByFilterImage() binding.resetSearchFilter.setOnClickListener { @@ -126,7 +126,7 @@ class SearchFilterBottomDialog : BottomSheetDialogFragment() { resetSearchFilter() CoroutineScope(Dispatchers.Main).launch { - activity.result.apply { + activity.aniMangaResult.apply { status = binding.searchStatus.text.toString().replace(" ", "_").ifBlank { null } source = @@ -135,7 +135,7 @@ class SearchFilterBottomDialog : BottomSheetDialogFragment() { season = binding.searchSeason.text.toString().ifBlank { null } startYear = binding.searchYear.text.toString().toIntOrNull() seasonYear = binding.searchYear.text.toString().toIntOrNull() - sort = activity.result.sort + sort = activity.aniMangaResult.sort genres = selectedGenres tags = selectedTags excludedGenres = exGenres @@ -155,43 +155,43 @@ class SearchFilterBottomDialog : BottomSheetDialogFragment() { popupMenu.setOnMenuItemClickListener { menuItem -> when (menuItem.itemId) { R.id.sort_by_score -> { - activity.result.sort = Anilist.sortBy[0] + activity.aniMangaResult.sort = Anilist.sortBy[0] binding.sortByFilter.setImageResource(R.drawable.ic_round_area_chart_24) startBounceZoomAnimation() } R.id.sort_by_popular -> { - activity.result.sort = Anilist.sortBy[1] + activity.aniMangaResult.sort = Anilist.sortBy[1] binding.sortByFilter.setImageResource(R.drawable.ic_round_filter_peak_24) startBounceZoomAnimation() } R.id.sort_by_trending -> { - activity.result.sort = Anilist.sortBy[2] + activity.aniMangaResult.sort = Anilist.sortBy[2] binding.sortByFilter.setImageResource(R.drawable.ic_round_star_graph_24) startBounceZoomAnimation() } R.id.sort_by_recent -> { - activity.result.sort = Anilist.sortBy[3] + activity.aniMangaResult.sort = Anilist.sortBy[3] binding.sortByFilter.setImageResource(R.drawable.ic_round_new_releases_24) startBounceZoomAnimation() } R.id.sort_by_a_z -> { - activity.result.sort = Anilist.sortBy[4] + activity.aniMangaResult.sort = Anilist.sortBy[4] binding.sortByFilter.setImageResource(R.drawable.ic_round_filter_list_24) startBounceZoomAnimation() } R.id.sort_by_z_a -> { - activity.result.sort = Anilist.sortBy[5] + activity.aniMangaResult.sort = Anilist.sortBy[5] binding.sortByFilter.setImageResource(R.drawable.ic_round_filter_list_24_reverse) startBounceZoomAnimation() } R.id.sort_by_pure_pain -> { - activity.result.sort = Anilist.sortBy[6] + activity.aniMangaResult.sort = Anilist.sortBy[6] binding.sortByFilter.setImageResource(R.drawable.ic_round_assist_walker_24) startBounceZoomAnimation() } @@ -212,25 +212,25 @@ class SearchFilterBottomDialog : BottomSheetDialogFragment() { } R.id.country_china -> { - activity.result.countryOfOrigin = "CN" + activity.aniMangaResult.countryOfOrigin = "CN" binding.countryFilter.setImageResource(R.drawable.ic_round_globe_china_googlefonts) startBounceZoomAnimation(binding.countryFilter) } R.id.country_south_korea -> { - activity.result.countryOfOrigin = "KR" + activity.aniMangaResult.countryOfOrigin = "KR" binding.countryFilter.setImageResource(R.drawable.ic_round_globe_south_korea_googlefonts) startBounceZoomAnimation(binding.countryFilter) } R.id.country_japan -> { - activity.result.countryOfOrigin = "JP" + activity.aniMangaResult.countryOfOrigin = "JP" binding.countryFilter.setImageResource(R.drawable.ic_round_globe_japan_googlefonts) startBounceZoomAnimation(binding.countryFilter) } R.id.country_taiwan -> { - activity.result.countryOfOrigin = "TW" + activity.aniMangaResult.countryOfOrigin = "TW" binding.countryFilter.setImageResource(R.drawable.ic_round_globe_taiwan_googlefonts) startBounceZoomAnimation(binding.countryFilter) } @@ -241,18 +241,18 @@ class SearchFilterBottomDialog : BottomSheetDialogFragment() { } binding.searchFilterApply.setOnClickListener { - activity.result.apply { + activity.aniMangaResult.apply { status = binding.searchStatus.text.toString().replace(" ", "_").ifBlank { null } source = binding.searchSource.text.toString().replace(" ", "_").ifBlank { null } format = binding.searchFormat.text.toString().ifBlank { null } season = binding.searchSeason.text.toString().ifBlank { null } - if (activity.result.type == "ANIME") { + if (activity.aniMangaResult.type == "ANIME") { seasonYear = binding.searchYear.text.toString().toIntOrNull() } else { startYear = binding.searchYear.text.toString().toIntOrNull() } - sort = activity.result.sort - countryOfOrigin = activity.result.countryOfOrigin + sort = activity.aniMangaResult.sort + countryOfOrigin = activity.aniMangaResult.countryOfOrigin genres = selectedGenres tags = selectedTags excludedGenres = exGenres @@ -266,8 +266,8 @@ class SearchFilterBottomDialog : BottomSheetDialogFragment() { dismiss() } val format = - if (activity.result.type == "ANIME") Anilist.animeStatus else Anilist.mangaStatus - binding.searchStatus.setText(activity.result.status?.replace("_", " ")) + if (activity.aniMangaResult.type == "ANIME") Anilist.animeStatus else Anilist.mangaStatus + binding.searchStatus.setText(activity.aniMangaResult.status?.replace("_", " ")) binding.searchStatus.setAdapter( ArrayAdapter( binding.root.context, @@ -276,7 +276,7 @@ class SearchFilterBottomDialog : BottomSheetDialogFragment() { ) ) - binding.searchSource.setText(activity.result.source?.replace("_", " ")) + binding.searchSource.setText(activity.aniMangaResult.source?.replace("_", " ")) binding.searchSource.setAdapter( ArrayAdapter( binding.root.context, @@ -285,19 +285,19 @@ class SearchFilterBottomDialog : BottomSheetDialogFragment() { ) ) - binding.searchFormat.setText(activity.result.format) + binding.searchFormat.setText(activity.aniMangaResult.format) binding.searchFormat.setAdapter( ArrayAdapter( binding.root.context, R.layout.item_dropdown, - (if (activity.result.type == "ANIME") Anilist.animeFormats else Anilist.mangaFormats).toTypedArray() + (if (activity.aniMangaResult.type == "ANIME") Anilist.animeFormats else Anilist.mangaFormats).toTypedArray() ) ) - if (activity.result.type == "ANIME") { - binding.searchYear.setText(activity.result.seasonYear?.toString()) + if (activity.aniMangaResult.type == "ANIME") { + binding.searchYear.setText(activity.aniMangaResult.seasonYear?.toString()) } else { - binding.searchYear.setText(activity.result.startYear?.toString()) + binding.searchYear.setText(activity.aniMangaResult.startYear?.toString()) } binding.searchYear.setAdapter( ArrayAdapter( @@ -308,9 +308,9 @@ class SearchFilterBottomDialog : BottomSheetDialogFragment() { ) ) - if (activity.result.type == "MANGA") binding.searchSeasonCont.visibility = GONE + if (activity.aniMangaResult.type == "MANGA") binding.searchSeasonCont.visibility = GONE else { - binding.searchSeason.setText(activity.result.season) + binding.searchSeason.setText(activity.aniMangaResult.season) binding.searchSeason.setAdapter( ArrayAdapter( binding.root.context, @@ -346,7 +346,7 @@ class SearchFilterBottomDialog : BottomSheetDialogFragment() { binding.searchGenresGrid.isChecked = false binding.searchFilterTags.adapter = - FilterChipAdapter(Anilist.tags?.get(activity.result.isAdult) ?: listOf()) { chip -> + FilterChipAdapter(Anilist.tags?.get(activity.aniMangaResult.isAdult) ?: listOf()) { chip -> val tag = chip.text.toString() chip.isChecked = selectedTags.contains(tag) chip.isCloseIconVisible = exTags.contains(tag) diff --git a/app/src/main/java/ani/dantotsu/media/SearchHistoryAdapter.kt b/app/src/main/java/ani/dantotsu/media/SearchHistoryAdapter.kt index 4e2988e3..70619036 100644 --- a/app/src/main/java/ani/dantotsu/media/SearchHistoryAdapter.kt +++ b/app/src/main/java/ani/dantotsu/media/SearchHistoryAdapter.kt @@ -7,23 +7,26 @@ import androidx.recyclerview.widget.DiffUtil import androidx.recyclerview.widget.ListAdapter import androidx.recyclerview.widget.RecyclerView import ani.dantotsu.R +import ani.dantotsu.connections.anilist.AnilistSearch.SearchType import ani.dantotsu.databinding.ItemSearchHistoryBinding import ani.dantotsu.settings.saving.PrefManager import ani.dantotsu.settings.saving.PrefManager.asLiveStringSet import ani.dantotsu.settings.saving.PrefName import ani.dantotsu.settings.saving.SharedPreferenceStringSetLiveData -import java.util.Locale -class SearchHistoryAdapter(private val type: String, private val searchClicked: (String) -> Unit) : +class SearchHistoryAdapter(type: SearchType, private val searchClicked: (String) -> Unit) : ListAdapter( DIFF_CALLBACK_INSTALLED ) { private var searchHistoryLiveData: SharedPreferenceStringSetLiveData? = null private var searchHistory: MutableSet? = null - private var historyType: PrefName = when (type.lowercase(Locale.ROOT)) { - "anime" -> PrefName.AnimeSearchHistory - "manga" -> PrefName.MangaSearchHistory - else -> throw IllegalArgumentException("Invalid type") + private var historyType: PrefName = when (type) { + SearchType.ANIME -> PrefName.AnimeSearchHistory + SearchType.MANGA -> PrefName.MangaSearchHistory + SearchType.CHARACTER -> PrefName.CharacterSearchHistory + SearchType.STAFF -> PrefName.StaffSearchHistory + SearchType.STUDIO -> PrefName.StudioSearchHistory + SearchType.USER -> PrefName.UserSearchHistory } init { diff --git a/app/src/main/java/ani/dantotsu/media/Studio.kt b/app/src/main/java/ani/dantotsu/media/Studio.kt index 699213b3..cc862fe7 100644 --- a/app/src/main/java/ani/dantotsu/media/Studio.kt +++ b/app/src/main/java/ani/dantotsu/media/Studio.kt @@ -5,5 +5,8 @@ import java.io.Serializable data class Studio( val id: String, val name: String, + val isFavourite: Boolean?, + val favourites: Int?, + val imageUrl: String?, var yearMedia: MutableMap>? = null ) : Serializable diff --git a/app/src/main/java/ani/dantotsu/media/StudioAdapter.kt b/app/src/main/java/ani/dantotsu/media/StudioAdapter.kt new file mode 100644 index 00000000..5e4f60b7 --- /dev/null +++ b/app/src/main/java/ani/dantotsu/media/StudioAdapter.kt @@ -0,0 +1,61 @@ +package ani.dantotsu.media + +import android.app.Activity +import android.content.Intent +import android.view.LayoutInflater +import android.view.ViewGroup +import androidx.core.app.ActivityOptionsCompat +import androidx.core.content.ContextCompat +import androidx.core.util.Pair +import androidx.core.view.ViewCompat +import androidx.core.view.isVisible +import androidx.recyclerview.widget.RecyclerView +import ani.dantotsu.copyToClipboard +import ani.dantotsu.databinding.ItemCharacterBinding +import ani.dantotsu.loadImage +import ani.dantotsu.setAnimation +import java.io.Serializable + +class StudioAdapter( + private val studioList: MutableList +) : RecyclerView.Adapter() { + override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): StudioViewHolder { + val binding = + ItemCharacterBinding.inflate(LayoutInflater.from(parent.context), parent, false) + return StudioViewHolder(binding) + } + + override fun onBindViewHolder(holder: StudioViewHolder, position: Int) { + val binding = holder.binding + setAnimation(binding.root.context, holder.binding.root) + val studio = studioList.getOrNull(position) ?: return + binding.itemCompactRelation.isVisible = false + binding.itemCompactImage.loadImage(studio.imageUrl) + binding.itemCompactTitle.text = studio.name + } + + override fun getItemCount(): Int = studioList.size + inner class StudioViewHolder(val binding: ItemCharacterBinding) : + RecyclerView.ViewHolder(binding.root) { + init { + itemView.setOnClickListener { + val studio = studioList[bindingAdapterPosition] + ContextCompat.startActivity( + itemView.context, + Intent( + itemView.context, + StudioActivity::class.java + ).putExtra("studio", studio as Serializable), + ActivityOptionsCompat.makeSceneTransitionAnimation( + itemView.context as Activity, + Pair.create( + binding.itemCompactImage, + ViewCompat.getTransitionName(binding.itemCompactImage)!! + ), + ).toBundle() + ) + } + itemView.setOnLongClickListener { copyToClipboard(studioList[bindingAdapterPosition].name ?: ""); true } + } + } +} \ No newline at end of file diff --git a/app/src/main/java/ani/dantotsu/media/SupportingSearchAdapter.kt b/app/src/main/java/ani/dantotsu/media/SupportingSearchAdapter.kt new file mode 100644 index 00000000..fdc8a05d --- /dev/null +++ b/app/src/main/java/ani/dantotsu/media/SupportingSearchAdapter.kt @@ -0,0 +1,142 @@ +package ani.dantotsu.media + +import android.annotation.SuppressLint +import android.graphics.drawable.Drawable +import android.text.Editable +import android.text.TextWatcher +import android.view.View +import android.view.inputmethod.EditorInfo +import android.view.inputmethod.InputMethodManager +import androidx.appcompat.app.AppCompatActivity +import androidx.appcompat.content.res.AppCompatResources +import androidx.recyclerview.widget.LinearLayoutManager +import ani.dantotsu.App.Companion.context +import ani.dantotsu.R +import ani.dantotsu.connections.anilist.AnilistSearch.SearchType +import ani.dantotsu.connections.anilist.AnilistSearch.SearchType.Companion.toAnilistString +import ani.dantotsu.connections.anilist.SearchResults +import ani.dantotsu.settings.saving.PrefManager +import ani.dantotsu.settings.saving.PrefName +import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.delay +import kotlinx.coroutines.launch + +class SupportingSearchAdapter(private val activity: SearchActivity, private val type: SearchType) : + HeaderInterface() { + + @SuppressLint("ClickableViewAccessibility") + override fun onBindViewHolder(holder: SearchHeaderViewHolder, position: Int) { + binding = holder.binding + + searchHistoryAdapter = SearchHistoryAdapter(type) { + binding.searchBarText.setText(it) + } + binding.searchHistoryList.layoutManager = LinearLayoutManager(binding.root.context) + binding.searchHistoryList.adapter = searchHistoryAdapter + + val imm: InputMethodManager = + activity.getSystemService(AppCompatActivity.INPUT_METHOD_SERVICE) as InputMethodManager + + if (activity.searchType == SearchType.MANGA || activity.searchType == SearchType.ANIME) { + throw IllegalArgumentException("Invalid search type (wrong adapter)") + } + + binding.searchByImage.visibility = View.GONE + binding.searchResultGrid.visibility = View.GONE + binding.searchResultList.visibility = View.GONE + binding.searchFilter.visibility = View.GONE + binding.searchAdultCheck.visibility = View.GONE + binding.searchList.visibility = View.GONE + binding.searchChipRecycler.visibility = View.GONE + + binding.searchBar.hint = activity.searchType.toAnilistString() + if (PrefManager.getVal(PrefName.Incognito)) { + val startIconDrawableRes = R.drawable.ic_incognito_24 + val startIconDrawable: Drawable? = + context?.let { AppCompatResources.getDrawable(it, startIconDrawableRes) } + binding.searchBar.startIconDrawable = startIconDrawable + } + + binding.searchBarText.removeTextChangedListener(textWatcher) + when (type) { + SearchType.CHARACTER -> { + binding.searchBarText.setText(activity.characterResult.search) + } + + SearchType.STUDIO -> { + binding.searchBarText.setText(activity.studioResult.search) + } + + SearchType.STAFF -> { + binding.searchBarText.setText(activity.staffResult.search) + } + + SearchType.USER -> { + binding.searchBarText.setText(activity.userResult.search) + } + + else -> throw IllegalArgumentException("Invalid search type") + } + + binding.clearHistory.setOnClickListener { + it.startAnimation(fadeOutAnimation()) + it.visibility = View.GONE + searchHistoryAdapter.clearHistory() + } + updateClearHistoryVisibility() + fun searchTitle() { + val searchText = binding.searchBarText.text.toString().takeIf { it.isNotEmpty() } + + val result: SearchResults<*> = when (type) { + SearchType.CHARACTER -> activity.characterResult + SearchType.STUDIO -> activity.studioResult + SearchType.STAFF -> activity.staffResult + SearchType.USER -> activity.userResult + else -> throw IllegalArgumentException("Invalid search type") + } + + result.search = searchText + activity.search() + } + + textWatcher = object : TextWatcher { + override fun afterTextChanged(s: Editable) {} + + override fun beforeTextChanged(s: CharSequence, start: Int, count: Int, after: Int) {} + + override fun onTextChanged(s: CharSequence, start: Int, before: Int, count: Int) { + if (s.toString().isBlank()) { + activity.emptyMediaAdapter() + CoroutineScope(Dispatchers.IO).launch { + delay(200) + activity.runOnUiThread { + setHistoryVisibility(true) + } + } + } else { + setHistoryVisibility(false) + searchTitle() + } + } + } + binding.searchBarText.addTextChangedListener(textWatcher) + + binding.searchBarText.setOnEditorActionListener { _, actionId, _ -> + return@setOnEditorActionListener when (actionId) { + EditorInfo.IME_ACTION_SEARCH -> { + searchTitle() + binding.searchBarText.clearFocus() + imm.hideSoftInputFromWindow(binding.searchBarText.windowToken, 0) + true + } + + else -> false + } + } + binding.searchBar.setEndIconOnClickListener { searchTitle() } + + search = Runnable { searchTitle() } + requestFocus = Runnable { binding.searchBarText.requestFocus() } + } +} diff --git a/app/src/main/java/ani/dantotsu/profile/UsersAdapter.kt b/app/src/main/java/ani/dantotsu/profile/UsersAdapter.kt index 90363e76..7651d21f 100644 --- a/app/src/main/java/ani/dantotsu/profile/UsersAdapter.kt +++ b/app/src/main/java/ani/dantotsu/profile/UsersAdapter.kt @@ -4,16 +4,19 @@ import android.content.Intent import android.view.LayoutInflater import android.view.ViewGroup import androidx.core.content.ContextCompat +import androidx.core.view.isVisible import androidx.recyclerview.widget.RecyclerView +import androidx.viewbinding.ViewBinding import ani.dantotsu.databinding.ItemFollowerBinding +import ani.dantotsu.databinding.ItemFollowerGridBinding import ani.dantotsu.loadImage import ani.dantotsu.setAnimation -class UsersAdapter(private val user: ArrayList) : +class UsersAdapter(private val user: MutableList, private val grid: Boolean = false) : RecyclerView.Adapter() { - inner class UsersViewHolder(val binding: ItemFollowerBinding) : + inner class UsersViewHolder(val binding: ViewBinding) : RecyclerView.ViewHolder(binding.root) { init { itemView.setOnClickListener { @@ -27,6 +30,11 @@ class UsersAdapter(private val user: ArrayList) : override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): UsersViewHolder { return UsersViewHolder( + if (grid) ItemFollowerGridBinding.inflate( + LayoutInflater.from(parent.context), + parent, + false + ) else ItemFollowerBinding.inflate( LayoutInflater.from(parent.context), parent, @@ -36,12 +44,21 @@ class UsersAdapter(private val user: ArrayList) : } override fun onBindViewHolder(holder: UsersViewHolder, position: Int) { - val b = holder.binding - setAnimation(b.root.context, b.root) - val user = user[position] - b.profileUserAvatar.loadImage(user.pfp) - b.profileBannerImage.loadImage(user.banner ?: user.pfp) - b.profileUserName.text = user.name + setAnimation(holder.binding.root.context, holder.binding.root) + val user = user.getOrNull(position) ?: return + if (grid) { + val b = holder.binding as ItemFollowerGridBinding + b.profileUserAvatar.loadImage(user.pfp) + b.profileUserName.text = user.name + b.profileCompactScoreBG.isVisible = false + b.profileInfo.isVisible = false + b.profileCompactProgressContainer.isVisible = false + } else { + val b = holder.binding as ItemFollowerBinding + b.profileUserAvatar.loadImage(user.pfp) + b.profileBannerImage.loadImage(user.banner ?: user.pfp) + b.profileUserName.text = user.name + } } override fun getItemCount(): Int = user.size diff --git a/app/src/main/java/ani/dantotsu/settings/saving/Preferences.kt b/app/src/main/java/ani/dantotsu/settings/saving/Preferences.kt index 866871b3..03958657 100644 --- a/app/src/main/java/ani/dantotsu/settings/saving/Preferences.kt +++ b/app/src/main/java/ani/dantotsu/settings/saving/Preferences.kt @@ -37,6 +37,10 @@ enum class PrefName(val data: Pref) { //TODO: Split this into multiple files AnimeSearchHistory(Pref(Location.General, Set::class, setOf())), MangaSourcesOrder(Pref(Location.General, List::class, listOf())), MangaSearchHistory(Pref(Location.General, Set::class, setOf())), + CharacterSearchHistory(Pref(Location.General, Set::class, setOf())), + StaffSearchHistory(Pref(Location.General, Set::class, setOf())), + StudioSearchHistory(Pref(Location.General, Set::class, setOf())), + UserSearchHistory(Pref(Location.General, Set::class, setOf())), NovelSourcesOrder(Pref(Location.General, List::class, listOf())), CommentNotificationInterval(Pref(Location.General, Int::class, 0)), AnilistNotificationInterval(Pref(Location.General, Int::class, 3)), diff --git a/app/src/main/java/eu/kanade/tachiyomi/extension/api/ExtensionGithubApi.kt b/app/src/main/java/eu/kanade/tachiyomi/extension/api/ExtensionGithubApi.kt index c2771afd..aef59959 100644 --- a/app/src/main/java/eu/kanade/tachiyomi/extension/api/ExtensionGithubApi.kt +++ b/app/src/main/java/eu/kanade/tachiyomi/extension/api/ExtensionGithubApi.kt @@ -68,13 +68,18 @@ internal class ExtensionGithubApi { PrefManager.getVal>(PrefName.AnimeExtensionRepos).toMutableList() repos.forEach { + val repoUrl = if (it.contains("index.min.json")) { + it + } else { + "$it${if (it.endsWith('/')) "" else "/"}index.min.json" + } try { val githubResponse = try { networkService.client - .newCall(GET("${it}/index.min.json")) + .newCall(GET(repoUrl)) .awaitSuccess() } catch (e: Throwable) { - Logger.log("Failed to get repo: $it") + Logger.log("Failed to get repo: $repoUrl") Logger.log(e) null } diff --git a/app/src/main/res/layout/activity_author.xml b/app/src/main/res/layout/activity_author.xml deleted file mode 100644 index 61a5eb3b..00000000 --- a/app/src/main/res/layout/activity_author.xml +++ /dev/null @@ -1,111 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - diff --git a/app/src/main/res/layout/activity_character.xml b/app/src/main/res/layout/activity_character.xml index ccc2af4d..7535493a 100644 --- a/app/src/main/res/layout/activity_character.xml +++ b/app/src/main/res/layout/activity_character.xml @@ -104,20 +104,75 @@ android:indeterminate="true" app:layout_behavior="@string/appbar_scrolling_view_behavior" /> - + android:layout_height="match_parent" + app:layout_behavior="@string/appbar_scrolling_view_behavior"> + + + + + + + + + + + + + + + +