feat: searching

This commit is contained in:
rebel onion 2025-01-03 09:01:09 -06:00
parent 38d68a7976
commit 7b8af6ea8a
31 changed files with 2109 additions and 702 deletions

View file

@ -2,15 +2,25 @@ package ani.dantotsu.connections.anilist
import ani.dantotsu.R import ani.dantotsu.R
import ani.dantotsu.currContext import ani.dantotsu.currContext
import ani.dantotsu.media.Author
import ani.dantotsu.media.Character
import ani.dantotsu.media.Media import ani.dantotsu.media.Media
import ani.dantotsu.media.Studio
import ani.dantotsu.profile.User
import java.io.Serializable import java.io.Serializable
data class SearchResults( interface SearchResults<T> {
var search: String?
var page: Int
var results: MutableList<T>
var hasNextPage: Boolean
}
data class AniMangaSearchResults(
val type: String, val type: String,
var isAdult: Boolean, var isAdult: Boolean,
var onList: Boolean? = null, var onList: Boolean? = null,
var perPage: Int? = null, var perPage: Int? = null,
var search: String? = null,
var countryOfOrigin: String? = null, var countryOfOrigin: String? = null,
var sort: String? = null, var sort: String? = null,
var genres: MutableList<String>? = null, var genres: MutableList<String>? = null,
@ -23,10 +33,11 @@ data class SearchResults(
var seasonYear: Int? = null, var seasonYear: Int? = null,
var startYear: Int? = null, var startYear: Int? = null,
var season: String? = null, var season: String? = null,
var page: Int = 1, override var search: String? = null,
var results: MutableList<Media>, override var page: Int = 1,
var hasNextPage: Boolean, override var results: MutableList<Media>,
) : Serializable { override var hasNextPage: Boolean,
) : SearchResults<Media>, Serializable {
fun toChipList(): List<SearchChip> { fun toChipList(): List<SearchChip> {
val list = mutableListOf<SearchChip>() val list = mutableListOf<SearchChip>()
sort?.let { sort?.let {
@ -108,4 +119,33 @@ data class SearchResults(
val type: String, val type: String,
val text: String val text: String
) )
} }
data class CharacterSearchResults(
override var search: String?,
override var page: Int = 1,
override var results: MutableList<Character>,
override var hasNextPage: Boolean,
) : SearchResults<Character>, Serializable
data class StudioSearchResults(
override var search: String?,
override var page: Int = 1,
override var results: MutableList<Studio>,
override var hasNextPage: Boolean,
) : SearchResults<Studio>, Serializable
data class StaffSearchResults(
override var search: String?,
override var page: Int = 1,
override var results: MutableList<Author>,
override var hasNextPage: Boolean,
) : SearchResults<Author>, Serializable
data class UserSearchResults(
override var search: String?,
override var page: Int = 1,
override var results: MutableList<User>,
override var hasNextPage: Boolean,
) : SearchResults<User>, Serializable

View file

@ -311,7 +311,6 @@ object Anilist {
) )
val remaining = json.headers["X-RateLimit-Remaining"]?.toIntOrNull() ?: -1 val remaining = json.headers["X-RateLimit-Remaining"]?.toIntOrNull() ?: -1
Logger.log("Remaining requests: $remaining") Logger.log("Remaining requests: $remaining")
println("Remaining requests: $remaining")
if (json.code == 429) { if (json.code == 429) {
val retry = json.headers["Retry-After"]?.toIntOrNull() ?: -1 val retry = json.headers["Retry-After"]?.toIntOrNull() ?: -1
val passedLimitReset = json.headers["X-RateLimit-Reset"]?.toLongOrNull() ?: 0 val passedLimitReset = json.headers["X-RateLimit-Reset"]?.toLongOrNull() ?: 0

View file

@ -44,7 +44,8 @@ class AnilistQueries {
val response: Query.Viewer? val response: Query.Viewer?
measureTimeMillis { measureTimeMillis {
response = executeQuery( 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") } }.also { println("time : $it") }
val user = response?.data?.user ?: return false val user = response?.data?.user ?: return false
@ -96,12 +97,10 @@ class AnilistQueries {
fun mediaDetails(media: Media): Media { fun mediaDetails(media: Media): Media {
media.cameFromContinue = false 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 { runBlocking {
val anilist = async { val anilist = async {
var response = executeQuery<Query.Media>(query, force = true) var response =
executeQuery<Query.Media>(fullMediaInformation(media.id), force = true)
if (response != null) { if (response != null) {
fun parse() { fun parse() {
val fetchedMedia = response?.data?.media ?: return val fetchedMedia = response?.data?.media ?: return
@ -291,7 +290,10 @@ class AnilistQueries {
val firstStudio = get(0) val firstStudio = get(0)
media.anime.mainStudio = Studio( media.anime.mainStudio = Studio(
firstStudio.id.toString(), 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() if (response.data?.media != null) parse()
else { else {
snackString(currContext()?.getString(R.string.adult_stuff)) 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() if (response?.data?.media != null) parse()
else snackString(currContext()?.getString(R.string.what_did_you_open)) else snackString(currContext()?.getString(R.string.what_did_you_open))
} }
@ -400,8 +406,6 @@ class AnilistQueries {
return media return media
} }
private suspend fun favMedia(anime: Boolean, id: Int? = Anilist.userid): ArrayList<Media> { private suspend fun favMedia(anime: Boolean, id: Int? = Anilist.userid): ArrayList<Media> {
var hasNextPage = true var hasNextPage = true
var page = 0 var page = 0
@ -426,8 +430,6 @@ class AnilistQueries {
return responseArray return responseArray
} }
suspend fun getUserStatus(): ArrayList<User>? { suspend fun getUserStatus(): ArrayList<User>? {
val toShow: List<Boolean> = val toShow: List<Boolean> =
PrefManager.getVal(PrefName.HomeLayout) PrefManager.getVal(PrefName.HomeLayout)
@ -485,19 +487,23 @@ class AnilistQueries {
return list.toCollection(ArrayList()) return list.toCollection(ArrayList())
} else return null } else return null
} }
private fun favMediaQuery(anime: Boolean, page: Int, id: Int? = Anilist.userid): String { 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 { 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 { 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 } } } } }""" 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 { 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 } } } } } """ 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<String, ArrayList<Media>> { suspend fun initHomePage(): Map<String, ArrayList<Media>> {
val removeList = PrefManager.getCustomVal("removeList", setOf<Int>()) val removeList = PrefManager.getCustomVal("removeList", setOf<Int>())
val hidePrivate = PrefManager.getVal<Boolean>(PrefName.HidePrivate) val hidePrivate = PrefManager.getVal<Boolean>(PrefName.HidePrivate)
@ -888,7 +894,7 @@ class AnilistQueries {
return null return null
} }
suspend fun search( suspend fun searchAniManga(
type: String, type: String,
page: Int? = null, page: Int? = null,
perPage: Int? = null, perPage: Int? = null,
@ -910,52 +916,7 @@ class AnilistQueries {
id: Int? = null, id: Int? = null,
hd: Boolean = false, hd: Boolean = false,
adultOnly: Boolean = false adultOnly: Boolean = false
): SearchResults? { ): AniMangaSearchResults? {
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(""" """, "")
val variables = """{"type":"$type","isAdult":$isAdult val variables = """{"type":"$type","isAdult":$isAdult
${if (adultOnly) ""","isAdult":true""" else ""} ${if (adultOnly) ""","isAdult":true""" else ""}
${if (onList != null) ""","onList":$onList""" else ""} ${if (onList != null) ""","onList":$onList""" else ""}
@ -1000,8 +961,9 @@ query (${"$"}page: Int = 1, ${"$"}id: Int, ${"$"}type: MediaType, ${"$"}isAdult:
}]""" }]"""
else "" else ""
} }
}""".replace("\n", " ").replace(""" """, "") }""".prepare()
val response = executeQuery<Query.Page>(query, variables, true)?.data?.page val response =
executeQuery<Query.Page>(aniMangaSearch(perPage), variables, true)?.data?.page
if (response?.media != null) { if (response?.media != null) {
val responseArray = arrayListOf<Media>() val responseArray = arrayListOf<Media>()
response.media?.forEach { i -> response.media?.forEach { i ->
@ -1021,7 +983,7 @@ query (${"$"}page: Int = 1, ${"$"}id: Int, ${"$"}type: MediaType, ${"$"}isAdult:
val pageInfo = response.pageInfo ?: return null val pageInfo = response.pageInfo ?: return null
return SearchResults( return AniMangaSearchResults(
type = type, type = type,
perPage = perPage, perPage = perPage,
search = search, search = search,
@ -1047,6 +1009,169 @@ query (${"$"}page: Int = 1, ${"$"}id: Int, ${"$"}type: MediaType, ${"$"}isAdult:
return null 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.Page>(query, force = true)?.data?.page
if (response?.characters != null) {
val responseArray = arrayListOf<Character>()
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.Page>(query, force = true)?.data?.page
if (response?.studios != null) {
val responseArray = arrayListOf<Studio>()
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.Page>(query, force = true)?.data?.page
if (response?.staff != null) {
val responseArray = arrayListOf<Author>()
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.Page>(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<Media> { private fun mediaList(media1: Page?): ArrayList<Media> {
val combinedList = arrayListOf<Media>() val combinedList = arrayListOf<Media>()
media1?.media?.mapTo(combinedList) { Media(it) } 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, " } ?: "" val countryFilter = country?.let { "countryOfOrigin:$it, " } ?: ""
return buildString { 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 { private fun recentAnimeUpdates(page: Int): String {
val currentTime = System.currentTimeMillis() / 1000 val currentTime = System.currentTimeMillis() / 1000
return buildString { 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 { private fun queryAnimeList(): String {
return buildString { 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 { private fun queryMangaList(): String {
return buildString { 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 list
} }
suspend fun recentlyUpdated( suspend fun recentlyUpdated(
greater: Long = 0, greater: Long = 0,
lesser: Long = System.currentTimeMillis() / 1000 - 10000 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? { suspend fun execute(page: Int = 1): Page? {
val query = """{ val query = """{
Page(page:$page,perPage:50) { Page(page:$page,perPage:50) {
pageInfo { $standardPageInformation
hasNextPage
total
}
airingSchedules( airingSchedules(
airingAt_greater: $greater airingAt_greater: $greater
airingAt_lesser: $lesser airingAt_lesser: $lesser
@ -1165,35 +1325,11 @@ Page(page:$page,perPage:50) {
episode episode
airingAt airingAt
media { media {
id ${standardMediaInformation()}
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
}
} }
} }
} }
}""".replace("\n", " ").replace(""" """, "") }""".prepare()
return executeQuery<Query.Page>(query, force = true)?.data?.page return executeQuery<Query.Page>(query, force = true)?.data?.page
} }
@ -1221,68 +1357,37 @@ Page(page:$page,perPage:50) {
suspend fun getCharacterDetails(character: Character): Character { suspend fun getCharacterDetails(character: Character): Character {
val query = """ { val query = """ {
Character(id: ${character.id}) { Character(id: ${character.id}) {
id ${characterInformation(true)}
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
}
}
}
}
} }
}""".replace("\n", " ").replace(""" """, "") }""".prepare()
executeQuery<Query.Character>(query, force = true)?.data?.character?.apply { executeQuery<Query.Character>(query, force = true)?.data?.character?.let { i ->
character.age = age return Character(
character.gender = gender i.id,
character.description = description i.name?.full,
character.dateOfBirth = dateOfBirth i.image?.large ?: i.image?.medium,
character.roles = arrayListOf() null,
media?.edges?.forEach { i -> null.toString(),
val m = Media(i) i.isFavourite ?: false,
m.relation = i.characterRole.toString() i.description,
character.roles?.add(m) 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<Author>?
)
} }
return character return character
} }
@ -1290,45 +1395,9 @@ Page(page:$page,perPage:50) {
suspend fun getStudioDetails(studio: Studio): Studio { suspend fun getStudioDetails(studio: Studio): Studio {
fun query(page: Int = 0) = """ { fun query(page: Int = 0) = """ {
Studio(id: ${studio.id}) { Studio(id: ${studio.id}) {
id ${studioInformation(page, ITEMS_PER_PAGE)}
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
}
}
}
}
} }
}""".replace("\n", " ").replace(""" """, "") }""".prepare()
var hasNextPage = true var hasNextPage = true
val yearMedia = mutableMapOf<String, ArrayList<Media>>() val yearMedia = mutableMapOf<String, ArrayList<Media>>()
@ -1363,66 +1432,15 @@ Page(page:$page,perPage:50) {
suspend fun getAuthorDetails(author: Author): Author { suspend fun getAuthorDetails(author: Author): Author {
fun query(page: Int = 0) = """ { fun query(page: Int = 0) = """ {
Staff(id: ${author.id}) { Staff(id: ${author.id}) {
id ${staffInformation(page, ITEMS_PER_PAGE)}
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
}
}
}
}
characters(page: $page,sort:FAVOURITES_DESC) { characters(page: $page,sort:FAVOURITES_DESC) {
pageInfo{ $standardPageInformation
hasNextPage
}
nodes{ nodes{
id ${characterInformation(false)}
name {
first
middle
last
full
native
userPreferred
}
image {
large
medium
}
} }
} }
} }
}""".replace("\n", " ").replace(""" """, "") }""".prepare()
var hasNextPage = true var hasNextPage = true
val yearMedia = mutableMapOf<String, ArrayList<Media>>() val yearMedia = mutableMapOf<String, ArrayList<Media>>()
@ -1433,6 +1451,16 @@ Page(page:$page,perPage:50) {
val query = executeQuery<Query.Author>( val query = executeQuery<Query.Author>(
query(page), force = true query(page), force = true
)?.data?.author )?.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 { hasNextPage = query?.staffMedia?.let {
it.edges?.forEach { i -> it.edges?.forEach { i ->
i.node?.apply { i.node?.apply {
@ -1480,14 +1508,14 @@ Page(page:$page,perPage:50) {
sort: String = "SCORE_DESC" sort: String = "SCORE_DESC"
): Query.ReviewsResponse? { ): Query.ReviewsResponse? {
return executeQuery<Query.ReviewsResponse>( return executeQuery<Query.ReviewsResponse>(
"""{Page(page:$page,perPage:10){pageInfo{currentPage,hasNextPage,total}reviews(mediaId:$mediaId,sort:$sort){id,mediaId,mediaType,summary,body(asHtml:true)rating,ratingAmount,userRating,score,private,siteUrl,createdAt,updatedAt,user{id,name,bannerImage avatar{medium,large}}}}}""", """{Page(page:$page,perPage:10){$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 force = true
) )
} }
suspend fun getUserProfile(id: Int): Query.UserProfileResponse? { suspend fun getUserProfile(id: Int): Query.UserProfileResponse? {
return executeQuery<Query.UserProfileResponse>( return executeQuery<Query.UserProfileResponse>(
"""{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 force = true
) )
} }
@ -1513,7 +1541,7 @@ Page(page:$page,perPage:50) {
} }
private fun userFavMediaQuery(anime: Boolean, id: Int): String { 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? { suspend fun userFollowing(id: Int): Query.Following? {
@ -1535,7 +1563,7 @@ Page(page:$page,perPage:50) {
"""{ """{
favoriteAnime:${userFavMediaQuery(true, id)} favoriteAnime:${userFavMediaQuery(true, id)}
favoriteManga:${userFavMediaQuery(false, 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 typeIn = "type_in:[AIRING,MEDIA_MERGE,MEDIA_DELETION,MEDIA_DATA_CHANGE]"
val reset = if (resetNotification) "true" else "false" val reset = if (resetNotification) "true" else "false"
val res = executeQuery<NotificationResponse>( val res = executeQuery<NotificationResponse>(
"""{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 force = true
) )
if (res != null && resetNotification) { if (res != null && resetNotification) {
@ -1571,7 +1599,8 @@ Page(page:$page,perPage:50) {
else if (userId != null) "userId:$userId," else if (userId != null) "userId:$userId,"
else if (global) "isFollowing:false,hasRepliesOrTypeText:true," else if (global) "isFollowing:false,hasRepliesOrTypeText:true,"
else "isFollowing: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<FeedResponse>( return executeQuery<FeedResponse>(
"""{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}}}}}}""", """{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 force = true

View file

@ -128,7 +128,7 @@ class AnilistHomeViewModel : ViewModel() {
class AnilistAnimeViewModel : ViewModel() { class AnilistAnimeViewModel : ViewModel() {
var searched = false var searched = false
var notSet = true var notSet = true
lateinit var searchResults: SearchResults lateinit var aniMangaSearchResults: AniMangaSearchResults
private val type = "ANIME" private val type = "ANIME"
private val trending: MutableLiveData<MutableList<Media>> = private val trending: MutableLiveData<MutableList<Media>> =
MutableLiveData<MutableList<Media>>(null) MutableLiveData<MutableList<Media>>(null)
@ -137,7 +137,7 @@ class AnilistAnimeViewModel : ViewModel() {
suspend fun loadTrending(i: Int) { suspend fun loadTrending(i: Int) {
val (season, year) = Anilist.currentSeasons[i] val (season, year) = Anilist.currentSeasons[i]
trending.postValue( trending.postValue(
Anilist.query.search( Anilist.query.searchAniManga(
type, type,
perPage = 12, perPage = 12,
sort = Anilist.sortBy[2], sort = Anilist.sortBy[2],
@ -150,9 +150,9 @@ class AnilistAnimeViewModel : ViewModel() {
} }
private val animePopular = MutableLiveData<SearchResults?>(null) private val animePopular = MutableLiveData<AniMangaSearchResults?>(null)
fun getPopular(): LiveData<SearchResults?> = animePopular fun getPopular(): LiveData<AniMangaSearchResults?> = animePopular
suspend fun loadPopular( suspend fun loadPopular(
type: String, type: String,
searchVal: String? = null, searchVal: String? = null,
@ -161,7 +161,7 @@ class AnilistAnimeViewModel : ViewModel() {
onList: Boolean = true, onList: Boolean = true,
) { ) {
animePopular.postValue( animePopular.postValue(
Anilist.query.search( Anilist.query.searchAniManga(
type, type,
search = searchVal, search = searchVal,
onList = if (onList) null else false, onList = if (onList) null else false,
@ -173,8 +173,8 @@ class AnilistAnimeViewModel : ViewModel() {
} }
suspend fun loadNextPage(r: SearchResults) = animePopular.postValue( suspend fun loadNextPage(r: AniMangaSearchResults) = animePopular.postValue(
Anilist.query.search( Anilist.query.searchAniManga(
r.type, r.type,
r.page + 1, r.page + 1,
r.perPage, r.perPage,
@ -224,7 +224,7 @@ class AnilistAnimeViewModel : ViewModel() {
class AnilistMangaViewModel : ViewModel() { class AnilistMangaViewModel : ViewModel() {
var searched = false var searched = false
var notSet = true var notSet = true
lateinit var searchResults: SearchResults lateinit var aniMangaSearchResults: AniMangaSearchResults
private val type = "MANGA" private val type = "MANGA"
private val trending: MutableLiveData<MutableList<Media>> = private val trending: MutableLiveData<MutableList<Media>> =
MutableLiveData<MutableList<Media>>(null) MutableLiveData<MutableList<Media>>(null)
@ -232,7 +232,7 @@ class AnilistMangaViewModel : ViewModel() {
fun getTrending(): LiveData<MutableList<Media>> = trending fun getTrending(): LiveData<MutableList<Media>> = trending
suspend fun loadTrending() = suspend fun loadTrending() =
trending.postValue( trending.postValue(
Anilist.query.search( Anilist.query.searchAniManga(
type, type,
perPage = 10, perPage = 10,
sort = Anilist.sortBy[2], sort = Anilist.sortBy[2],
@ -242,8 +242,8 @@ class AnilistMangaViewModel : ViewModel() {
) )
private val mangaPopular = MutableLiveData<SearchResults?>(null) private val mangaPopular = MutableLiveData<AniMangaSearchResults?>(null)
fun getPopular(): LiveData<SearchResults?> = mangaPopular fun getPopular(): LiveData<AniMangaSearchResults?> = mangaPopular
suspend fun loadPopular( suspend fun loadPopular(
type: String, type: String,
searchVal: String? = null, searchVal: String? = null,
@ -252,7 +252,7 @@ class AnilistMangaViewModel : ViewModel() {
onList: Boolean = true, onList: Boolean = true,
) { ) {
mangaPopular.postValue( mangaPopular.postValue(
Anilist.query.search( Anilist.query.searchAniManga(
type, type,
search = searchVal, search = searchVal,
onList = if (onList) null else false, onList = if (onList) null else false,
@ -264,8 +264,8 @@ class AnilistMangaViewModel : ViewModel() {
} }
suspend fun loadNextPage(r: SearchResults) = mangaPopular.postValue( suspend fun loadNextPage(r: AniMangaSearchResults) = mangaPopular.postValue(
Anilist.query.search( Anilist.query.searchAniManga(
r.type, r.type,
r.page + 1, r.page + 1,
r.perPage, r.perPage,
@ -325,14 +325,126 @@ class AnilistMangaViewModel : ViewModel() {
} }
class AnilistSearch : 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 searched = false
var notSet = true var notSet = true
lateinit var searchResults: SearchResults lateinit var aniMangaSearchResults: AniMangaSearchResults
private val result: MutableLiveData<SearchResults?> = MutableLiveData<SearchResults?>(null) private val aniMangaResult: MutableLiveData<AniMangaSearchResults?> = MutableLiveData<AniMangaSearchResults?>(null)
fun getSearch(): LiveData<SearchResults?> = result lateinit var characterSearchResults: CharacterSearchResults
suspend fun loadSearch(r: SearchResults) = result.postValue( private val characterResult: MutableLiveData<CharacterSearchResults?> = MutableLiveData<CharacterSearchResults?>(null)
Anilist.query.search(
lateinit var studioSearchResults: StudioSearchResults
private val studioResult: MutableLiveData<StudioSearchResults?> = MutableLiveData<StudioSearchResults?>(null)
lateinit var staffSearchResults: StaffSearchResults
private val staffResult: MutableLiveData<StaffSearchResults?> = MutableLiveData<StaffSearchResults?>(null)
lateinit var userSearchResults: UserSearchResults
private val userResult: MutableLiveData<UserSearchResults?> = MutableLiveData<UserSearchResults?>(null)
fun <T> getSearch(type: SearchType): MutableLiveData<T?> {
return when (type) {
SearchType.ANIME, SearchType.MANGA -> aniMangaResult as MutableLiveData<T?>
SearchType.CHARACTER -> characterResult as MutableLiveData<T?>
SearchType.STUDIO -> studioResult as MutableLiveData<T?>
SearchType.STAFF -> staffResult as MutableLiveData<T?>
SearchType.USER -> userResult as MutableLiveData<T?>
}
}
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.type,
r.page, r.page,
r.perPage, r.perPage,
@ -354,8 +466,36 @@ class AnilistSearch : ViewModel() {
) )
) )
suspend fun loadNextPage(r: SearchResults) = result.postValue( private suspend fun loadCharacterSearch(r: CharacterSearchResults) = characterResult.postValue(
Anilist.query.search( 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.type,
r.page + 1, r.page + 1,
r.perPage, r.perPage,
@ -376,6 +516,34 @@ class AnilistSearch : ViewModel() {
r.season 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() { class GenresViewModel : ViewModel() {

View file

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

View file

@ -446,7 +446,7 @@ data class MediaEdge(
@SerialName("staffRole") var staffRole: String?, @SerialName("staffRole") var staffRole: String?,
// The voice actors of the character // The voice actors of the character
// @SerialName("voiceActors") var voiceActors: List<Staff>?, @SerialName("voiceActors") var voiceActors: List<Staff>?,
// The voice actors of the character with role date // The voice actors of the character with role date
// @SerialName("voiceActorRoles") var voiceActorRoles: List<StaffRoleType>?, // @SerialName("voiceActorRoles") var voiceActorRoles: List<StaffRoleType>?,

View file

@ -24,7 +24,7 @@ import ani.dantotsu.Refresh
import ani.dantotsu.bottomBar import ani.dantotsu.bottomBar
import ani.dantotsu.connections.anilist.Anilist import ani.dantotsu.connections.anilist.Anilist
import ani.dantotsu.connections.anilist.AnilistAnimeViewModel 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.connections.anilist.getUserId
import ani.dantotsu.databinding.FragmentAnimeBinding import ani.dantotsu.databinding.FragmentAnimeBinding
import ani.dantotsu.media.MediaAdaptor import ani.dantotsu.media.MediaAdaptor
@ -100,7 +100,7 @@ class AnimeFragment : Fragment() {
var loading = true var loading = true
if (model.notSet) { if (model.notSet) {
model.notSet = false model.notSet = false
model.searchResults = SearchResults( model.aniMangaSearchResults = AniMangaSearchResults(
"ANIME", "ANIME",
isAdult = false, isAdult = false,
onList = false, onList = false,
@ -109,7 +109,7 @@ class AnimeFragment : Fragment() {
sort = Anilist.sortBy[1] 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 progressAdaptor = ProgressAdapter(searched = model.searched)
val adapter = ConcatAdapter(animePageAdapter, popularAdaptor, progressAdaptor) val adapter = ConcatAdapter(animePageAdapter, popularAdaptor, progressAdaptor)
binding.animePageRecyclerView.adapter = adapter binding.animePageRecyclerView.adapter = adapter
@ -142,7 +142,7 @@ class AnimeFragment : Fragment() {
animePageAdapter.onIncludeListClick = { checked -> animePageAdapter.onIncludeListClick = { checked ->
oldIncludeList = !checked oldIncludeList = !checked
loading = true loading = true
model.searchResults.results.clear() model.aniMangaSearchResults.results.clear()
popularAdaptor.notifyDataSetChanged() popularAdaptor.notifyDataSetChanged()
scope.launch(Dispatchers.IO) { scope.launch(Dispatchers.IO) {
model.loadPopular("ANIME", sort = Anilist.sortBy[1], onList = checked) model.loadPopular("ANIME", sort = Anilist.sortBy[1], onList = checked)
@ -152,17 +152,17 @@ class AnimeFragment : Fragment() {
model.getPopular().observe(viewLifecycleOwner) { model.getPopular().observe(viewLifecycleOwner) {
if (it != null) { if (it != null) {
if (oldIncludeList == (it.onList != false)) { if (oldIncludeList == (it.onList != false)) {
val prev = model.searchResults.results.size val prev = model.aniMangaSearchResults.results.size
model.searchResults.results.addAll(it.results) model.aniMangaSearchResults.results.addAll(it.results)
popularAdaptor.notifyItemRangeInserted(prev, it.results.size) popularAdaptor.notifyItemRangeInserted(prev, it.results.size)
} else { } else {
model.searchResults.results.addAll(it.results) model.aniMangaSearchResults.results.addAll(it.results)
popularAdaptor.notifyDataSetChanged() popularAdaptor.notifyDataSetChanged()
oldIncludeList = it.onList ?: true oldIncludeList = it.onList ?: true
} }
model.searchResults.onList = it.onList model.aniMangaSearchResults.onList = it.onList
model.searchResults.hasNextPage = it.hasNextPage model.aniMangaSearchResults.hasNextPage = it.hasNextPage
model.searchResults.page = it.page model.aniMangaSearchResults.page = it.page
if (it.hasNextPage) if (it.hasNextPage)
progressAdaptor.bar?.visibility = View.VISIBLE progressAdaptor.bar?.visibility = View.VISIBLE
else { else {
@ -177,10 +177,10 @@ class AnimeFragment : Fragment() {
RecyclerView.OnScrollListener() { RecyclerView.OnScrollListener() {
override fun onScrolled(v: RecyclerView, dx: Int, dy: Int) { override fun onScrolled(v: RecyclerView, dx: Int, dy: Int) {
if (!v.canScrollVertically(1)) { 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) { scope.launch(Dispatchers.IO) {
loading = true loading = true
model.loadNextPage(model.searchResults) model.loadNextPage(model.aniMangaSearchResults)
} }
} }
} }

View file

@ -13,7 +13,6 @@ import androidx.core.content.ContextCompat
import androidx.core.view.isVisible import androidx.core.view.isVisible
import androidx.core.view.updateLayoutParams import androidx.core.view.updateLayoutParams
import androidx.core.view.updatePadding import androidx.core.view.updatePadding
import androidx.lifecycle.LiveData
import androidx.lifecycle.MutableLiveData import androidx.lifecycle.MutableLiveData
import androidx.recyclerview.widget.LinearLayoutManager import androidx.recyclerview.widget.LinearLayoutManager
import androidx.recyclerview.widget.RecyclerView import androidx.recyclerview.widget.RecyclerView
@ -21,7 +20,6 @@ import androidx.viewpager2.widget.ViewPager2
import ani.dantotsu.MediaPageTransformer import ani.dantotsu.MediaPageTransformer
import ani.dantotsu.R import ani.dantotsu.R
import ani.dantotsu.connections.anilist.Anilist import ani.dantotsu.connections.anilist.Anilist
import ani.dantotsu.currContext
import ani.dantotsu.databinding.ItemAnimePageBinding import ani.dantotsu.databinding.ItemAnimePageBinding
import ani.dantotsu.databinding.LayoutTrendingBinding import ani.dantotsu.databinding.LayoutTrendingBinding
import ani.dantotsu.getAppString import ani.dantotsu.getAppString
@ -32,7 +30,6 @@ import ani.dantotsu.media.GenreActivity
import ani.dantotsu.media.Media import ani.dantotsu.media.Media
import ani.dantotsu.media.MediaAdaptor import ani.dantotsu.media.MediaAdaptor
import ani.dantotsu.media.MediaListViewActivity import ani.dantotsu.media.MediaListViewActivity
import ani.dantotsu.media.SearchActivity
import ani.dantotsu.profile.ProfileActivity import ani.dantotsu.profile.ProfileActivity
import ani.dantotsu.px import ani.dantotsu.px
import ani.dantotsu.setSafeOnClickListener import ani.dantotsu.setSafeOnClickListener
@ -83,12 +80,11 @@ class AnimePageAdapter : RecyclerView.Adapter<AnimePageAdapter.AnimePageViewHold
updateAvatar() updateAvatar()
trendingBinding.searchBar.hint = "ANIME" trendingBinding.searchBar.hint = binding.root.context.getString(R.string.search)
trendingBinding.searchBarText.setOnClickListener { trendingBinding.searchBarText.setOnClickListener {
ContextCompat.startActivity( SearchBottomSheet.newInstance().show(
it.context, (binding.root.context as AppCompatActivity).supportFragmentManager,
Intent(it.context, SearchActivity::class.java).putExtra("type", "ANIME"), "search"
null
) )
} }

View file

@ -22,7 +22,7 @@ import ani.dantotsu.Refresh
import ani.dantotsu.bottomBar import ani.dantotsu.bottomBar
import ani.dantotsu.connections.anilist.Anilist import ani.dantotsu.connections.anilist.Anilist
import ani.dantotsu.connections.anilist.AnilistMangaViewModel import ani.dantotsu.connections.anilist.AnilistMangaViewModel
import ani.dantotsu.connections.anilist.SearchResults import ani.dantotsu.connections.anilist.AniMangaSearchResults
import ani.dantotsu.connections.anilist.getUserId import ani.dantotsu.connections.anilist.getUserId
import ani.dantotsu.databinding.FragmentMangaBinding import ani.dantotsu.databinding.FragmentMangaBinding
import ani.dantotsu.media.MediaAdaptor import ani.dantotsu.media.MediaAdaptor
@ -94,7 +94,7 @@ class MangaFragment : Fragment() {
var loading = true var loading = true
if (model.notSet) { if (model.notSet) {
model.notSet = false model.notSet = false
model.searchResults = SearchResults( model.aniMangaSearchResults = AniMangaSearchResults(
"MANGA", "MANGA",
isAdult = false, isAdult = false,
onList = false, onList = false,
@ -103,7 +103,7 @@ class MangaFragment : Fragment() {
sort = Anilist.sortBy[1] 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 progressAdaptor = ProgressAdapter(searched = model.searched)
binding.mangaPageRecyclerView.adapter = binding.mangaPageRecyclerView.adapter =
ConcatAdapter(mangaPageAdapter, popularAdaptor, progressAdaptor) ConcatAdapter(mangaPageAdapter, popularAdaptor, progressAdaptor)
@ -135,10 +135,10 @@ class MangaFragment : Fragment() {
RecyclerView.OnScrollListener() { RecyclerView.OnScrollListener() {
override fun onScrolled(v: RecyclerView, dx: Int, dy: Int) { override fun onScrolled(v: RecyclerView, dx: Int, dy: Int) {
if (!v.canScrollVertically(1)) { 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) { scope.launch(Dispatchers.IO) {
loading = true loading = true
model.loadNextPage(model.searchResults) model.loadNextPage(model.aniMangaSearchResults)
} }
} }
} }
@ -220,7 +220,7 @@ class MangaFragment : Fragment() {
mangaPageAdapter.onIncludeListClick = { checked -> mangaPageAdapter.onIncludeListClick = { checked ->
oldIncludeList = !checked oldIncludeList = !checked
loading = true loading = true
model.searchResults.results.clear() model.aniMangaSearchResults.results.clear()
popularAdaptor.notifyDataSetChanged() popularAdaptor.notifyDataSetChanged()
scope.launch(Dispatchers.IO) { scope.launch(Dispatchers.IO) {
model.loadPopular("MANGA", sort = Anilist.sortBy[1], onList = checked) model.loadPopular("MANGA", sort = Anilist.sortBy[1], onList = checked)
@ -230,17 +230,17 @@ class MangaFragment : Fragment() {
model.getPopular().observe(viewLifecycleOwner) { model.getPopular().observe(viewLifecycleOwner) {
if (it != null) { if (it != null) {
if (oldIncludeList == (it.onList != false)) { if (oldIncludeList == (it.onList != false)) {
val prev = model.searchResults.results.size val prev = model.aniMangaSearchResults.results.size
model.searchResults.results.addAll(it.results) model.aniMangaSearchResults.results.addAll(it.results)
popularAdaptor.notifyItemRangeInserted(prev, it.results.size) popularAdaptor.notifyItemRangeInserted(prev, it.results.size)
} else { } else {
model.searchResults.results.addAll(it.results) model.aniMangaSearchResults.results.addAll(it.results)
popularAdaptor.notifyDataSetChanged() popularAdaptor.notifyDataSetChanged()
oldIncludeList = it.onList ?: true oldIncludeList = it.onList ?: true
} }
model.searchResults.onList = it.onList model.aniMangaSearchResults.onList = it.onList
model.searchResults.hasNextPage = it.hasNextPage model.aniMangaSearchResults.hasNextPage = it.hasNextPage
model.searchResults.page = it.page model.aniMangaSearchResults.page = it.page
if (it.hasNextPage) if (it.hasNextPage)
progressAdaptor.bar?.visibility = View.VISIBLE progressAdaptor.bar?.visibility = View.VISIBLE
else { else {

View file

@ -82,12 +82,11 @@ class MangaPageAdapter : RecyclerView.Adapter<MangaPageAdapter.MangaPageViewHold
trendingBinding.notificationCount.isVisible = Anilist.unreadNotificationCount > 0 trendingBinding.notificationCount.isVisible = Anilist.unreadNotificationCount > 0
&& PrefManager.getVal<Boolean>(PrefName.ShowNotificationRedDot) == true && PrefManager.getVal<Boolean>(PrefName.ShowNotificationRedDot) == true
trendingBinding.notificationCount.text = Anilist.unreadNotificationCount.toString() trendingBinding.notificationCount.text = Anilist.unreadNotificationCount.toString()
trendingBinding.searchBar.hint = "MANGA" trendingBinding.searchBar.hint = binding.root.context.getString(R.string.search)
trendingBinding.searchBarText.setOnClickListener { trendingBinding.searchBarText.setOnClickListener {
ContextCompat.startActivity( SearchBottomSheet.newInstance().show(
it.context, (binding.root.context as AppCompatActivity).supportFragmentManager,
Intent(it.context, SearchActivity::class.java).putExtra("type", "MANGA"), "search"
null
) )
} }

View file

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

View file

@ -7,6 +7,12 @@ data class Author(
var name: String?, var name: String?,
var image: String?, var image: String?,
var role: String?, var role: String?,
var age: Int? = null,
var yearsActive: List<Int>? = null,
var dateOfBirth: String? = null,
var dateOfDeath: String? = null,
var homeTown: String? = null,
var yearMedia: MutableMap<String, ArrayList<Media>>? = null, var yearMedia: MutableMap<String, ArrayList<Media>>? = null,
var character: ArrayList<Character>? = null var character: ArrayList<Character>? = null,
var isFav: Boolean = false
) : Serializable ) : Serializable

View file

@ -1,11 +1,13 @@
package ani.dantotsu.media package ani.dantotsu.media
import android.content.Intent
import android.os.Bundle import android.os.Bundle
import android.view.View import android.view.View
import android.view.ViewGroup import android.view.ViewGroup
import androidx.activity.viewModels import androidx.activity.viewModels
import androidx.appcompat.app.AppCompatActivity import androidx.appcompat.app.AppCompatActivity
import androidx.core.content.ContextCompat import androidx.core.content.ContextCompat
import androidx.core.math.MathUtils.clamp
import androidx.core.view.updateLayoutParams import androidx.core.view.updateLayoutParams
import androidx.core.view.updatePadding import androidx.core.view.updatePadding
import androidx.lifecycle.MutableLiveData import androidx.lifecycle.MutableLiveData
@ -16,57 +18,127 @@ import androidx.recyclerview.widget.LinearLayoutManager
import ani.dantotsu.EmptyAdapter import ani.dantotsu.EmptyAdapter
import ani.dantotsu.R import ani.dantotsu.R
import ani.dantotsu.Refresh 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.initActivity
import ani.dantotsu.loadImage
import ani.dantotsu.navBarHeight 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.others.getSerialized
import ani.dantotsu.px 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.statusBarHeight
import ani.dantotsu.themes.ThemeManager 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.Dispatchers
import kotlinx.coroutines.launch import kotlinx.coroutines.launch
import kotlinx.coroutines.withContext import kotlinx.coroutines.withContext
import kotlin.math.abs
class AuthorActivity : AppCompatActivity() { class AuthorActivity : AppCompatActivity(), AppBarLayout.OnOffsetChangedListener {
private lateinit var binding: ActivityAuthorBinding private lateinit var binding: ActivityCharacterBinding
private val scope = lifecycleScope private val scope = lifecycleScope
private val model: OtherDetailsViewModel by viewModels() private val model: OtherDetailsViewModel by viewModels()
private var author: Author? = null private lateinit var author: Author
private var loaded = false 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?) { override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState) super.onCreate(savedInstanceState)
ThemeManager(this).applyTheme() ThemeManager(this).applyTheme()
binding = ActivityAuthorBinding.inflate(layoutInflater) binding = ActivityCharacterBinding.inflate(layoutInflater)
setContentView(binding.root) setContentView(binding.root)
initActivity(this) 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<ViewGroup.MarginLayoutParams> { topMargin += statusBarHeight } banner.updateLayoutParams { height += statusBarHeight }
binding.studioRecycler.updatePadding(bottom = 64f.px + navBarHeight) binding.characterClose.updateLayoutParams<ViewGroup.MarginLayoutParams> { topMargin += statusBarHeight }
binding.studioTitle.isSelected = true binding.characterCollapsing.minimumHeight = statusBarHeight
binding.characterCover.updateLayoutParams<ViewGroup.MarginLayoutParams> { topMargin += statusBarHeight }
binding.characterRecyclerView.updatePadding(bottom = 64f.px + navBarHeight)
binding.characterTitle.isSelected = true
binding.characterAppBar.addOnOffsetChangedListener(this)
author = intent.getSerialized("author") binding.characterClose.setOnClickListener {
binding.studioTitle.text = author?.name
binding.studioClose.setOnClickListener {
onBackPressedDispatcher.onBackPressed() 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) { model.getAuthor().observe(this) {
if (it != null) { if (it != null) {
author = it author = it
loaded = true loaded = true
binding.studioProgressBar.visibility = View.GONE binding.characterProgress.visibility = View.GONE
binding.studioRecycler.visibility = View.VISIBLE binding.characterRecyclerView.visibility = View.VISIBLE
if (author!!.yearMedia.isNullOrEmpty()) { if (author.yearMedia.isNullOrEmpty()) {
binding.studioRecycler.visibility = View.GONE binding.characterRecyclerView.visibility = View.GONE
} }
val titlePosition = arrayListOf<Int>() val titlePosition = arrayListOf<Int>()
val concatAdapter = ConcatAdapter() val concatAdapter = ConcatAdapter()
val map = author!!.yearMedia ?: return@observe val map = author.yearMedia ?: return@observe
val keys = map.keys.toTypedArray() val keys = map.keys.toTypedArray()
var pos = 0 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) { for (i in keys.indices) {
val medias = map[keys[i]]!! val medias = map[keys[i]]!!
val empty = if (medias.size >= 4) medias.size % 4 else 4 - medias.size 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(MediaAdaptor(0, medias, this, true))
concatAdapter.addAdapter(EmptyAdapter(empty)) concatAdapter.addAdapter(EmptyAdapter(empty))
} }
binding.studioRecycler.adapter = concatAdapter binding.characterRecyclerView.adapter = concatAdapter
binding.studioRecycler.layoutManager = gridLayoutManager binding.characterRecyclerView.layoutManager = gridLayoutManager
binding.charactersRecycler.visibility = View.VISIBLE binding.authorCharactersRecycler.visibility = View.VISIBLE
binding.charactersText.visibility = View.VISIBLE binding.AuthorCharactersText.visibility = View.VISIBLE
binding.charactersRecycler.adapter = binding.authorCharactersRecycler.adapter =
CharacterAdapter(author!!.character ?: arrayListOf()) CharacterAdapter(author.character ?: arrayListOf())
binding.charactersRecycler.layoutManager = binding.authorCharactersRecycler.layoutManager =
LinearLayoutManager(this, LinearLayoutManager.HORIZONTAL, false) LinearLayoutManager(this, LinearLayoutManager.HORIZONTAL, false)
if (author!!.character.isNullOrEmpty()) { if (author.character.isNullOrEmpty()) {
binding.charactersRecycler.visibility = View.GONE binding.authorCharactersRecycler.visibility = View.GONE
binding.charactersText.visibility = View.GONE binding.AuthorCharactersText.visibility = View.GONE
} }
} }
} }
@ -109,14 +185,28 @@ class AuthorActivity : AppCompatActivity() {
live.observe(this) { live.observe(this) {
if (it) { if (it) {
scope.launch { scope.launch {
if (author != null) withContext(Dispatchers.IO) { model.loadAuthor(author) }
withContext(Dispatchers.IO) { model.loadAuthor(author!!) }
live.postValue(false) 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() { override fun onDestroy() {
if (Refresh.activity.containsKey(this.hashCode())) { if (Refresh.activity.containsKey(this.hashCode())) {
Refresh.activity.remove(this.hashCode()) Refresh.activity.remove(this.hashCode())
@ -125,7 +215,31 @@ class AuthorActivity : AppCompatActivity() {
} }
override fun onResume() { 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() 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)
}
}
} }

View file

@ -15,7 +15,7 @@ import ani.dantotsu.setAnimation
import java.io.Serializable import java.io.Serializable
class AuthorAdapter( class AuthorAdapter(
private val authorList: ArrayList<Author>, private val authorList: MutableList<Author>,
) : RecyclerView.Adapter<AuthorAdapter.AuthorViewHolder>() { ) : RecyclerView.Adapter<AuthorAdapter.AuthorViewHolder>() {
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): AuthorViewHolder { override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): AuthorViewHolder {
val binding = val binding =
@ -26,7 +26,7 @@ class AuthorAdapter(
override fun onBindViewHolder(holder: AuthorViewHolder, position: Int) { override fun onBindViewHolder(holder: AuthorViewHolder, position: Int) {
val binding = holder.binding val binding = holder.binding
setAnimation(binding.root.context, holder.binding.root) setAnimation(binding.root.context, holder.binding.root)
val author = authorList[position] val author = authorList.getOrNull(position) ?: return
binding.itemCompactRelation.text = author.role binding.itemCompactRelation.text = author.role
binding.itemCompactImage.loadImage(author.image) binding.itemCompactImage.loadImage(author.image)
binding.itemCompactTitle.text = author.name binding.itemCompactTitle.text = author.name

View file

@ -16,7 +16,7 @@ import ani.dantotsu.setAnimation
import java.io.Serializable import java.io.Serializable
class CharacterAdapter( class CharacterAdapter(
private val characterList: ArrayList<Character> private val characterList: MutableList<Character>
) : RecyclerView.Adapter<CharacterAdapter.CharacterViewHolder>() { ) : RecyclerView.Adapter<CharacterAdapter.CharacterViewHolder>() {
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): CharacterViewHolder { override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): CharacterViewHolder {
val binding = val binding =
@ -27,9 +27,8 @@ class CharacterAdapter(
override fun onBindViewHolder(holder: CharacterViewHolder, position: Int) { override fun onBindViewHolder(holder: CharacterViewHolder, position: Int) {
val binding = holder.binding val binding = holder.binding
setAnimation(binding.root.context, holder.binding.root) setAnimation(binding.root.context, holder.binding.root)
val character = characterList[position] val character = characterList.getOrNull(position) ?: return
val whitespace = "${character.role} " val whitespace = "${if (character.role.lowercase() == "null") "" else character.role} "
character.voiceActor
binding.itemCompactRelation.text = whitespace binding.itemCompactRelation.text = whitespace
binding.itemCompactImage.loadImage(character.image) binding.itemCompactImage.loadImage(character.image)
binding.itemCompactTitle.text = character.name binding.itemCompactTitle.text = character.name

View file

@ -9,6 +9,7 @@ import androidx.appcompat.app.AppCompatActivity
import androidx.core.content.ContextCompat import androidx.core.content.ContextCompat
import androidx.core.math.MathUtils.clamp import androidx.core.math.MathUtils.clamp
import androidx.core.view.isGone import androidx.core.view.isGone
import androidx.core.view.isVisible
import androidx.core.view.updateLayoutParams import androidx.core.view.updateLayoutParams
import androidx.core.view.updatePadding import androidx.core.view.updatePadding
import androidx.lifecycle.MutableLiveData import androidx.lifecycle.MutableLiveData
@ -45,6 +46,11 @@ class CharacterDetailsActivity : AppCompatActivity(), AppBarLayout.OnOffsetChang
private lateinit var character: Character private lateinit var character: Character
private var loaded = false 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?) { override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState) super.onCreate(savedInstanceState)
@ -71,6 +77,11 @@ class CharacterDetailsActivity : AppCompatActivity(), AppBarLayout.OnOffsetChang
binding.characterClose.setOnClickListener { binding.characterClose.setOnClickListener {
onBackPressedDispatcher.onBackPressed() onBackPressedDispatcher.onBackPressed()
} }
binding.authorCharactersRecycler.isVisible = false
binding.AuthorCharactersText.isVisible = false
binding.authorCharacterDesc.isVisible = false
character = intent.getSerialized("character") ?: return character = intent.getSerialized("character") ?: return
binding.characterTitle.text = character.name binding.characterTitle.text = character.name
banner.loadImage(character.banner) banner.loadImage(character.banner)
@ -158,11 +169,6 @@ class CharacterDetailsActivity : AppCompatActivity(), AppBarLayout.OnOffsetChang
super.onResume() 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) { override fun onOffsetChanged(appBar: AppBarLayout, i: Int) {
if (mMaxScrollSize == 0) mMaxScrollSize = appBar.totalScrollRange if (mMaxScrollSize == 0) mMaxScrollSize = appBar.totalScrollRange
val percentage = abs(i) * 100 / mMaxScrollSize val percentage = abs(i) * 100 / mMaxScrollSize

View file

@ -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<HeaderInterface.SearchHeaderViewHolder>() {
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
}
}

View file

@ -15,10 +15,16 @@ import androidx.recyclerview.widget.GridLayoutManager
import androidx.recyclerview.widget.RecyclerView import androidx.recyclerview.widget.RecyclerView
import ani.dantotsu.connections.anilist.Anilist import ani.dantotsu.connections.anilist.Anilist
import ani.dantotsu.connections.anilist.AnilistSearch 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.databinding.ActivitySearchBinding
import ani.dantotsu.initActivity import ani.dantotsu.initActivity
import ani.dantotsu.navBarHeight import ani.dantotsu.navBarHeight
import ani.dantotsu.profile.UsersAdapter
import ani.dantotsu.px import ani.dantotsu.px
import ani.dantotsu.settings.saving.PrefManager import ani.dantotsu.settings.saving.PrefManager
import ani.dantotsu.settings.saving.PrefName import ani.dantotsu.settings.saving.PrefName
@ -35,14 +41,25 @@ class SearchActivity : AppCompatActivity() {
val model: AnilistSearch by viewModels() val model: AnilistSearch by viewModels()
var style: Int = 0 var style: Int = 0
lateinit var searchType: SearchType
private var screenWidth: Float = 0f private var screenWidth: Float = 0f
private lateinit var mediaAdaptor: MediaAdaptor 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 progressAdapter: ProgressAdapter
private lateinit var concatAdapter: ConcatAdapter 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) lateinit var updateChips: (() -> Unit)
override fun onCreate(savedInstanceState: Bundle?) { override fun onCreate(savedInstanceState: Bundle?) {
@ -59,39 +76,117 @@ class SearchActivity : AppCompatActivity() {
bottom = navBarHeight + 80f.px bottom = navBarHeight + 80f.px
) )
style = PrefManager.getVal(PrefName.SearchStyle)
var listOnly: Boolean? = intent.getBooleanExtra("listOnly", false)
if (!listOnly!!) listOnly = null
val notSet = model.notSet val notSet = model.notSet
if (model.notSet) { searchType = SearchType.fromString(intent.getStringExtra("type") ?: "ANIME")
model.notSet = false when (searchType) {
model.searchResults = SearchResults( SearchType.ANIME, SearchType.MANGA -> {
intent.getStringExtra("type") ?: "ANIME", style = PrefManager.getVal(PrefName.SearchStyle)
isAdult = if (Anilist.adult) intent.getBooleanExtra("hentai", false) else false, var listOnly: Boolean? = intent.getBooleanExtra("listOnly", false)
onList = listOnly, if (!listOnly!!) listOnly = null
search = intent.getStringExtra("query"),
genres = intent.getStringExtra("genre")?.let { mutableListOf(it) }, if (model.notSet) {
tags = intent.getStringExtra("tag")?.let { mutableListOf(it) }, model.notSet = false
sort = intent.getStringExtra("sortBy"), model.aniMangaSearchResults = AniMangaSearchResults(
status = intent.getStringExtra("status"), intent.getStringExtra("type") ?: "ANIME",
source = intent.getStringExtra("source"), isAdult = if (Anilist.adult) intent.getBooleanExtra(
countryOfOrigin = intent.getStringExtra("country"), "hentai",
season = intent.getStringExtra("season"), false
seasonYear = if (intent.getStringExtra("type") == "ANIME") intent.getStringExtra("seasonYear") ) else false,
?.toIntOrNull() else null, onList = listOnly,
startYear = if (intent.getStringExtra("type") == "MANGA") intent.getStringExtra("seasonYear") search = intent.getStringExtra("query"),
?.toIntOrNull() else null, genres = intent.getStringExtra("genre")?.let { mutableListOf(it) },
results = mutableListOf(), tags = intent.getStringExtra("tag")?.let { mutableListOf(it) },
hasNextPage = false 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) progressAdapter = ProgressAdapter(searched = model.searched)
mediaAdaptor = MediaAdaptor(style, model.searchResults.results, this, matchParent = true) headerAdaptor = if (searchType == SearchType.ANIME || searchType == SearchType.MANGA) {
headerAdaptor = SearchAdapter(this, model.searchResults.type) SearchAdapter(this, searchType)
} else {
SupportingSearchAdapter(this, searchType)
}
val gridSize = (screenWidth / 120f).toInt() val gridSize = (screenWidth / 120f).toInt()
val gridLayoutManager = GridLayoutManager(this, gridSize) 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.layoutManager = gridLayoutManager
binding.searchRecyclerView.adapter = concatAdapter binding.searchRecyclerView.adapter = concatAdapter
@ -117,9 +232,9 @@ class SearchActivity : AppCompatActivity() {
RecyclerView.OnScrollListener() { RecyclerView.OnScrollListener() {
override fun onScrolled(v: RecyclerView, dx: Int, dy: Int) { override fun onScrolled(v: RecyclerView, dx: Int, dy: Int) {
if (!v.canScrollVertically(1)) { 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) { scope.launch(Dispatchers.IO) {
model.loadNextPage(model.searchResults) model.loadNextPage(searchType)
} }
} }
} }
@ -127,34 +242,110 @@ class SearchActivity : AppCompatActivity() {
} }
}) })
model.getSearch().observe(this) { when (searchType) {
if (it != null) { SearchType.ANIME, SearchType.MANGA -> {
model.searchResults.apply { model.getSearch<AniMangaSearchResults>(searchType).observe(this) {
onList = it.onList if (it != null) {
isAdult = it.isAdult model.aniMangaSearchResults.apply {
perPage = it.perPage onList = it.onList
search = it.search isAdult = it.isAdult
sort = it.sort perPage = it.perPage
genres = it.genres search = it.search
excludedGenres = it.excludedGenres sort = it.sort
excludedTags = it.excludedTags genres = it.genres
tags = it.tags excludedGenres = it.excludedGenres
season = it.season excludedTags = it.excludedTags
startYear = it.startYear tags = it.tags
seasonYear = it.seasonYear season = it.season
status = it.status startYear = it.startYear
source = it.source seasonYear = it.seasonYear
format = it.format status = it.status
countryOfOrigin = it.countryOfOrigin source = it.source
page = it.page format = it.format
hasNextPage = it.hasNextPage 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 SearchType.CHARACTER -> {
model.searchResults.results.addAll(it.results) model.getSearch<CharacterSearchResults>(searchType).observe(this) {
mediaAdaptor.notifyItemRangeInserted(prev, it.results.size) 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<StudioSearchResults>(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<StaffSearchResults>(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<UserSearchResults>(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() { fun emptyMediaAdapter() {
searchTimer.cancel() searchTimer.cancel()
searchTimer.purge() searchTimer.purge()
mediaAdaptor.notifyItemRangeRemoved(0, model.searchResults.results.size) when (searchType) {
model.searchResults.results.clear() 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 progressAdapter.bar?.visibility = View.GONE
} }
@ -188,10 +403,30 @@ class SearchActivity : AppCompatActivity() {
private var loading = false private var loading = false
fun search() { fun search() {
headerAdaptor.setHistoryVisibility(false) headerAdaptor.setHistoryVisibility(false)
val size = model.searchResults.results.size val size = model.size(searchType)
model.searchResults.results.clear() model.clearResults(searchType)
binding.searchRecyclerView.post { 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 progressAdapter.bar?.visibility = View.VISIBLE
@ -202,7 +437,7 @@ class SearchActivity : AppCompatActivity() {
override fun run() { override fun run() {
scope.launch(Dispatchers.IO) { scope.launch(Dispatchers.IO) {
loading = true loading = true
model.loadSearch(result) model.loadSearch(searchType)
loading = false loading = false
} }
} }
@ -213,8 +448,10 @@ class SearchActivity : AppCompatActivity() {
@SuppressLint("NotifyDataSetChanged") @SuppressLint("NotifyDataSetChanged")
fun recycler() { fun recycler() {
mediaAdaptor.type = style if (searchType == SearchType.ANIME || searchType == SearchType.MANGA) {
mediaAdaptor.notifyDataSetChanged() mediaAdaptor.type = style
mediaAdaptor.notifyDataSetChanged()
}
} }
var state: Parcelable? = null var state: Parcelable? = null

View file

@ -9,8 +9,6 @@ import android.view.LayoutInflater
import android.view.MotionEvent import android.view.MotionEvent
import android.view.View import android.view.View
import android.view.ViewGroup import android.view.ViewGroup
import android.view.animation.AlphaAnimation
import android.view.animation.Animation
import android.view.inputmethod.EditorInfo import android.view.inputmethod.EditorInfo
import android.view.inputmethod.InputMethodManager import android.view.inputmethod.InputMethodManager
import android.widget.PopupMenu import android.widget.PopupMenu
@ -22,8 +20,8 @@ import androidx.recyclerview.widget.RecyclerView.HORIZONTAL
import ani.dantotsu.App.Companion.context import ani.dantotsu.App.Companion.context
import ani.dantotsu.R import ani.dantotsu.R
import ani.dantotsu.connections.anilist.Anilist import ani.dantotsu.connections.anilist.Anilist
import ani.dantotsu.connections.anilist.AnilistSearch.SearchType
import ani.dantotsu.databinding.ItemChipBinding import ani.dantotsu.databinding.ItemChipBinding
import ani.dantotsu.databinding.ItemSearchHeaderBinding
import ani.dantotsu.openLinkInBrowser import ani.dantotsu.openLinkInBrowser
import ani.dantotsu.others.imagesearch.ImageSearchActivity import ani.dantotsu.others.imagesearch.ImageSearchActivity
import ani.dantotsu.settings.saving.PrefManager import ani.dantotsu.settings.saving.PrefManager
@ -36,18 +34,11 @@ import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.delay import kotlinx.coroutines.delay
import kotlinx.coroutines.launch import kotlinx.coroutines.launch
class SearchAdapter(private val activity: SearchActivity, private val type: SearchType) :
class SearchAdapter(private val activity: SearchActivity, private val type: String) : HeaderInterface() {
RecyclerView.Adapter<SearchAdapter.SearchHeaderViewHolder>() {
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
private fun updateFilterTextViewDrawable() { 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[0] -> R.drawable.ic_round_area_chart_24
Anilist.sortBy[1] -> R.drawable.ic_round_filter_peak_24 Anilist.sortBy[1] -> R.drawable.ic_round_filter_peak_24
Anilist.sortBy[2] -> R.drawable.ic_round_star_graph_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) 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") @SuppressLint("ClickableViewAccessibility")
override fun onBindViewHolder(holder: SearchHeaderViewHolder, position: Int) { override fun onBindViewHolder(holder: SearchHeaderViewHolder, position: Int) {
binding = holder.binding binding = holder.binding
@ -79,6 +64,10 @@ class SearchAdapter(private val activity: SearchActivity, private val type: Stri
val imm: InputMethodManager = val imm: InputMethodManager =
activity.getSystemService(AppCompatActivity.INPUT_METHOD_SERVICE) as 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) { when (activity.style) {
0 -> { 0 -> {
binding.searchResultGrid.alpha = 1f 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)) { if (PrefManager.getVal(PrefName.Incognito)) {
val startIconDrawableRes = R.drawable.ic_incognito_24 val startIconDrawableRes = R.drawable.ic_incognito_24
val startIconDrawable: Drawable? = val startIconDrawable: Drawable? =
@ -99,11 +88,11 @@ class SearchAdapter(private val activity: SearchActivity, private val type: Stri
binding.searchBar.startIconDrawable = startIconDrawable binding.searchBar.startIconDrawable = startIconDrawable
} }
var adult = activity.result.isAdult var adult = activity.aniMangaResult.isAdult
var listOnly = activity.result.onList var listOnly = activity.aniMangaResult.onList
binding.searchBarText.removeTextChangedListener(textWatcher) binding.searchBarText.removeTextChangedListener(textWatcher)
binding.searchBarText.setText(activity.result.search) binding.searchBarText.setText(activity.aniMangaResult.search)
binding.searchAdultCheck.isChecked = adult binding.searchAdultCheck.isChecked = adult
binding.searchList.isChecked = listOnly == true binding.searchList.isChecked = listOnly == true
@ -124,49 +113,49 @@ class SearchAdapter(private val activity: SearchActivity, private val type: Stri
popupMenu.setOnMenuItemClickListener { item -> popupMenu.setOnMenuItemClickListener { item ->
when (item.itemId) { when (item.itemId) {
R.id.sort_by_score -> { R.id.sort_by_score -> {
activity.result.sort = Anilist.sortBy[0] activity.aniMangaResult.sort = Anilist.sortBy[0]
activity.updateChips.invoke() activity.updateChips.invoke()
activity.search() activity.search()
updateFilterTextViewDrawable() updateFilterTextViewDrawable()
} }
R.id.sort_by_popular -> { R.id.sort_by_popular -> {
activity.result.sort = Anilist.sortBy[1] activity.aniMangaResult.sort = Anilist.sortBy[1]
activity.updateChips.invoke() activity.updateChips.invoke()
activity.search() activity.search()
updateFilterTextViewDrawable() updateFilterTextViewDrawable()
} }
R.id.sort_by_trending -> { R.id.sort_by_trending -> {
activity.result.sort = Anilist.sortBy[2] activity.aniMangaResult.sort = Anilist.sortBy[2]
activity.updateChips.invoke() activity.updateChips.invoke()
activity.search() activity.search()
updateFilterTextViewDrawable() updateFilterTextViewDrawable()
} }
R.id.sort_by_recent -> { R.id.sort_by_recent -> {
activity.result.sort = Anilist.sortBy[3] activity.aniMangaResult.sort = Anilist.sortBy[3]
activity.updateChips.invoke() activity.updateChips.invoke()
activity.search() activity.search()
updateFilterTextViewDrawable() updateFilterTextViewDrawable()
} }
R.id.sort_by_a_z -> { R.id.sort_by_a_z -> {
activity.result.sort = Anilist.sortBy[4] activity.aniMangaResult.sort = Anilist.sortBy[4]
activity.updateChips.invoke() activity.updateChips.invoke()
activity.search() activity.search()
updateFilterTextViewDrawable() updateFilterTextViewDrawable()
} }
R.id.sort_by_z_a -> { R.id.sort_by_z_a -> {
activity.result.sort = Anilist.sortBy[5] activity.aniMangaResult.sort = Anilist.sortBy[5]
activity.updateChips.invoke() activity.updateChips.invoke()
activity.search() activity.search()
updateFilterTextViewDrawable() updateFilterTextViewDrawable()
} }
R.id.sort_by_pure_pain -> { R.id.sort_by_pure_pain -> {
activity.result.sort = Anilist.sortBy[6] activity.aniMangaResult.sort = Anilist.sortBy[6]
activity.updateChips.invoke() activity.updateChips.invoke()
activity.search() activity.search()
updateFilterTextViewDrawable() updateFilterTextViewDrawable()
@ -177,7 +166,7 @@ class SearchAdapter(private val activity: SearchActivity, private val type: Stri
popupMenu.show() popupMenu.show()
true true
} }
if (activity.result.type != "ANIME") { if (activity.aniMangaResult.type != "ANIME") {
binding.searchByImage.visibility = View.GONE binding.searchByImage.visibility = View.GONE
} }
binding.searchByImage.setOnClickListener { binding.searchByImage.setOnClickListener {
@ -190,7 +179,7 @@ class SearchAdapter(private val activity: SearchActivity, private val type: Stri
} }
updateClearHistoryVisibility() updateClearHistoryVisibility()
fun searchTitle() { fun searchTitle() {
activity.result.apply { activity.aniMangaResult.apply {
search = search =
if (binding.searchBarText.text.toString() != "") binding.searchBarText.text.toString() else null if (binding.searchBarText.text.toString() != "") binding.searchBarText.text.toString() else null
onList = listOnly onList = listOnly
@ -292,67 +281,12 @@ class SearchAdapter(private val activity: SearchActivity, private val type: Stri
requestFocus = Runnable { binding.searchBarText.requestFocus() } 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( class SearchChipAdapter(
val activity: SearchActivity, val activity: SearchActivity,
private val searchAdapter: SearchAdapter private val searchAdapter: SearchAdapter
) : ) :
RecyclerView.Adapter<SearchChipAdapter.SearchChipViewHolder>() { RecyclerView.Adapter<SearchChipAdapter.SearchChipViewHolder>() {
private var chips = activity.result.toChipList() private var chips = activity.aniMangaResult.toChipList()
inner class SearchChipViewHolder(val binding: ItemChipBinding) : inner class SearchChipViewHolder(val binding: ItemChipBinding) :
RecyclerView.ViewHolder(binding.root) RecyclerView.ViewHolder(binding.root)
@ -369,7 +303,7 @@ class SearchAdapter(private val activity: SearchActivity, private val type: Stri
holder.binding.root.apply { holder.binding.root.apply {
text = chip.text.replace("_", " ") text = chip.text.replace("_", " ")
setOnClickListener { setOnClickListener {
activity.result.removeChip(chip) activity.aniMangaResult.removeChip(chip)
update() update()
activity.search() activity.search()
searchAdapter.updateFilterTextViewDrawable() searchAdapter.updateFilterTextViewDrawable()
@ -379,7 +313,7 @@ class SearchAdapter(private val activity: SearchActivity, private val type: Stri
@SuppressLint("NotifyDataSetChanged") @SuppressLint("NotifyDataSetChanged")
fun update() { fun update() {
chips = activity.result.toChipList() chips = activity.aniMangaResult.toChipList()
notifyDataSetChanged() notifyDataSetChanged()
searchAdapter.updateFilterTextViewDrawable() searchAdapter.updateFilterTextViewDrawable()
} }

View file

@ -57,7 +57,7 @@ class SearchFilterBottomDialog : BottomSheetDialogFragment() {
} }
private fun setSortByFilterImage() { 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[0] -> R.drawable.ic_round_area_chart_24
Anilist.sortBy[1] -> R.drawable.ic_round_filter_peak_24 Anilist.sortBy[1] -> R.drawable.ic_round_filter_peak_24
Anilist.sortBy[2] -> R.drawable.ic_round_star_graph_24 Anilist.sortBy[2] -> R.drawable.ic_round_star_graph_24
@ -71,10 +71,10 @@ class SearchFilterBottomDialog : BottomSheetDialogFragment() {
} }
private fun resetSearchFilter() { private fun resetSearchFilter() {
activity.result.sort = null activity.aniMangaResult.sort = null
binding.sortByFilter.setImageResource(R.drawable.ic_round_filter_alt_24) binding.sortByFilter.setImageResource(R.drawable.ic_round_filter_alt_24)
startBounceZoomAnimation(binding.sortByFilter) startBounceZoomAnimation(binding.sortByFilter)
activity.result.countryOfOrigin = null activity.aniMangaResult.countryOfOrigin = null
binding.countryFilter.setImageResource(R.drawable.ic_round_globe_search_googlefonts) binding.countryFilter.setImageResource(R.drawable.ic_round_globe_search_googlefonts)
startBounceZoomAnimation(binding.countryFilter) startBounceZoomAnimation(binding.countryFilter)
@ -98,10 +98,10 @@ class SearchFilterBottomDialog : BottomSheetDialogFragment() {
activity = requireActivity() as SearchActivity activity = requireActivity() as SearchActivity
selectedGenres = activity.result.genres ?: mutableListOf() selectedGenres = activity.aniMangaResult.genres ?: mutableListOf()
exGenres = activity.result.excludedGenres ?: mutableListOf() exGenres = activity.aniMangaResult.excludedGenres ?: mutableListOf()
selectedTags = activity.result.tags ?: mutableListOf() selectedTags = activity.aniMangaResult.tags ?: mutableListOf()
exTags = activity.result.excludedTags ?: mutableListOf() exTags = activity.aniMangaResult.excludedTags ?: mutableListOf()
setSortByFilterImage() setSortByFilterImage()
binding.resetSearchFilter.setOnClickListener { binding.resetSearchFilter.setOnClickListener {
@ -126,7 +126,7 @@ class SearchFilterBottomDialog : BottomSheetDialogFragment() {
resetSearchFilter() resetSearchFilter()
CoroutineScope(Dispatchers.Main).launch { CoroutineScope(Dispatchers.Main).launch {
activity.result.apply { activity.aniMangaResult.apply {
status = status =
binding.searchStatus.text.toString().replace(" ", "_").ifBlank { null } binding.searchStatus.text.toString().replace(" ", "_").ifBlank { null }
source = source =
@ -135,7 +135,7 @@ class SearchFilterBottomDialog : BottomSheetDialogFragment() {
season = binding.searchSeason.text.toString().ifBlank { null } season = binding.searchSeason.text.toString().ifBlank { null }
startYear = binding.searchYear.text.toString().toIntOrNull() startYear = binding.searchYear.text.toString().toIntOrNull()
seasonYear = binding.searchYear.text.toString().toIntOrNull() seasonYear = binding.searchYear.text.toString().toIntOrNull()
sort = activity.result.sort sort = activity.aniMangaResult.sort
genres = selectedGenres genres = selectedGenres
tags = selectedTags tags = selectedTags
excludedGenres = exGenres excludedGenres = exGenres
@ -155,43 +155,43 @@ class SearchFilterBottomDialog : BottomSheetDialogFragment() {
popupMenu.setOnMenuItemClickListener { menuItem -> popupMenu.setOnMenuItemClickListener { menuItem ->
when (menuItem.itemId) { when (menuItem.itemId) {
R.id.sort_by_score -> { 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) binding.sortByFilter.setImageResource(R.drawable.ic_round_area_chart_24)
startBounceZoomAnimation() startBounceZoomAnimation()
} }
R.id.sort_by_popular -> { 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) binding.sortByFilter.setImageResource(R.drawable.ic_round_filter_peak_24)
startBounceZoomAnimation() startBounceZoomAnimation()
} }
R.id.sort_by_trending -> { 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) binding.sortByFilter.setImageResource(R.drawable.ic_round_star_graph_24)
startBounceZoomAnimation() startBounceZoomAnimation()
} }
R.id.sort_by_recent -> { 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) binding.sortByFilter.setImageResource(R.drawable.ic_round_new_releases_24)
startBounceZoomAnimation() startBounceZoomAnimation()
} }
R.id.sort_by_a_z -> { 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) binding.sortByFilter.setImageResource(R.drawable.ic_round_filter_list_24)
startBounceZoomAnimation() startBounceZoomAnimation()
} }
R.id.sort_by_z_a -> { 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) binding.sortByFilter.setImageResource(R.drawable.ic_round_filter_list_24_reverse)
startBounceZoomAnimation() startBounceZoomAnimation()
} }
R.id.sort_by_pure_pain -> { 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) binding.sortByFilter.setImageResource(R.drawable.ic_round_assist_walker_24)
startBounceZoomAnimation() startBounceZoomAnimation()
} }
@ -212,25 +212,25 @@ class SearchFilterBottomDialog : BottomSheetDialogFragment() {
} }
R.id.country_china -> { R.id.country_china -> {
activity.result.countryOfOrigin = "CN" activity.aniMangaResult.countryOfOrigin = "CN"
binding.countryFilter.setImageResource(R.drawable.ic_round_globe_china_googlefonts) binding.countryFilter.setImageResource(R.drawable.ic_round_globe_china_googlefonts)
startBounceZoomAnimation(binding.countryFilter) startBounceZoomAnimation(binding.countryFilter)
} }
R.id.country_south_korea -> { R.id.country_south_korea -> {
activity.result.countryOfOrigin = "KR" activity.aniMangaResult.countryOfOrigin = "KR"
binding.countryFilter.setImageResource(R.drawable.ic_round_globe_south_korea_googlefonts) binding.countryFilter.setImageResource(R.drawable.ic_round_globe_south_korea_googlefonts)
startBounceZoomAnimation(binding.countryFilter) startBounceZoomAnimation(binding.countryFilter)
} }
R.id.country_japan -> { R.id.country_japan -> {
activity.result.countryOfOrigin = "JP" activity.aniMangaResult.countryOfOrigin = "JP"
binding.countryFilter.setImageResource(R.drawable.ic_round_globe_japan_googlefonts) binding.countryFilter.setImageResource(R.drawable.ic_round_globe_japan_googlefonts)
startBounceZoomAnimation(binding.countryFilter) startBounceZoomAnimation(binding.countryFilter)
} }
R.id.country_taiwan -> { R.id.country_taiwan -> {
activity.result.countryOfOrigin = "TW" activity.aniMangaResult.countryOfOrigin = "TW"
binding.countryFilter.setImageResource(R.drawable.ic_round_globe_taiwan_googlefonts) binding.countryFilter.setImageResource(R.drawable.ic_round_globe_taiwan_googlefonts)
startBounceZoomAnimation(binding.countryFilter) startBounceZoomAnimation(binding.countryFilter)
} }
@ -241,18 +241,18 @@ class SearchFilterBottomDialog : BottomSheetDialogFragment() {
} }
binding.searchFilterApply.setOnClickListener { binding.searchFilterApply.setOnClickListener {
activity.result.apply { activity.aniMangaResult.apply {
status = binding.searchStatus.text.toString().replace(" ", "_").ifBlank { null } status = binding.searchStatus.text.toString().replace(" ", "_").ifBlank { null }
source = binding.searchSource.text.toString().replace(" ", "_").ifBlank { null } source = binding.searchSource.text.toString().replace(" ", "_").ifBlank { null }
format = binding.searchFormat.text.toString().ifBlank { null } format = binding.searchFormat.text.toString().ifBlank { null }
season = binding.searchSeason.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() seasonYear = binding.searchYear.text.toString().toIntOrNull()
} else { } else {
startYear = binding.searchYear.text.toString().toIntOrNull() startYear = binding.searchYear.text.toString().toIntOrNull()
} }
sort = activity.result.sort sort = activity.aniMangaResult.sort
countryOfOrigin = activity.result.countryOfOrigin countryOfOrigin = activity.aniMangaResult.countryOfOrigin
genres = selectedGenres genres = selectedGenres
tags = selectedTags tags = selectedTags
excludedGenres = exGenres excludedGenres = exGenres
@ -266,8 +266,8 @@ class SearchFilterBottomDialog : BottomSheetDialogFragment() {
dismiss() dismiss()
} }
val format = val format =
if (activity.result.type == "ANIME") Anilist.animeStatus else Anilist.mangaStatus if (activity.aniMangaResult.type == "ANIME") Anilist.animeStatus else Anilist.mangaStatus
binding.searchStatus.setText(activity.result.status?.replace("_", " ")) binding.searchStatus.setText(activity.aniMangaResult.status?.replace("_", " "))
binding.searchStatus.setAdapter( binding.searchStatus.setAdapter(
ArrayAdapter( ArrayAdapter(
binding.root.context, 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( binding.searchSource.setAdapter(
ArrayAdapter( ArrayAdapter(
binding.root.context, 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( binding.searchFormat.setAdapter(
ArrayAdapter( ArrayAdapter(
binding.root.context, binding.root.context,
R.layout.item_dropdown, 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") { if (activity.aniMangaResult.type == "ANIME") {
binding.searchYear.setText(activity.result.seasonYear?.toString()) binding.searchYear.setText(activity.aniMangaResult.seasonYear?.toString())
} else { } else {
binding.searchYear.setText(activity.result.startYear?.toString()) binding.searchYear.setText(activity.aniMangaResult.startYear?.toString())
} }
binding.searchYear.setAdapter( binding.searchYear.setAdapter(
ArrayAdapter( 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 { else {
binding.searchSeason.setText(activity.result.season) binding.searchSeason.setText(activity.aniMangaResult.season)
binding.searchSeason.setAdapter( binding.searchSeason.setAdapter(
ArrayAdapter( ArrayAdapter(
binding.root.context, binding.root.context,
@ -346,7 +346,7 @@ class SearchFilterBottomDialog : BottomSheetDialogFragment() {
binding.searchGenresGrid.isChecked = false binding.searchGenresGrid.isChecked = false
binding.searchFilterTags.adapter = 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() val tag = chip.text.toString()
chip.isChecked = selectedTags.contains(tag) chip.isChecked = selectedTags.contains(tag)
chip.isCloseIconVisible = exTags.contains(tag) chip.isCloseIconVisible = exTags.contains(tag)

View file

@ -7,23 +7,26 @@ import androidx.recyclerview.widget.DiffUtil
import androidx.recyclerview.widget.ListAdapter import androidx.recyclerview.widget.ListAdapter
import androidx.recyclerview.widget.RecyclerView import androidx.recyclerview.widget.RecyclerView
import ani.dantotsu.R import ani.dantotsu.R
import ani.dantotsu.connections.anilist.AnilistSearch.SearchType
import ani.dantotsu.databinding.ItemSearchHistoryBinding import ani.dantotsu.databinding.ItemSearchHistoryBinding
import ani.dantotsu.settings.saving.PrefManager import ani.dantotsu.settings.saving.PrefManager
import ani.dantotsu.settings.saving.PrefManager.asLiveStringSet import ani.dantotsu.settings.saving.PrefManager.asLiveStringSet
import ani.dantotsu.settings.saving.PrefName import ani.dantotsu.settings.saving.PrefName
import ani.dantotsu.settings.saving.SharedPreferenceStringSetLiveData 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<String, SearchHistoryAdapter.SearchHistoryViewHolder>( ListAdapter<String, SearchHistoryAdapter.SearchHistoryViewHolder>(
DIFF_CALLBACK_INSTALLED DIFF_CALLBACK_INSTALLED
) { ) {
private var searchHistoryLiveData: SharedPreferenceStringSetLiveData? = null private var searchHistoryLiveData: SharedPreferenceStringSetLiveData? = null
private var searchHistory: MutableSet<String>? = null private var searchHistory: MutableSet<String>? = null
private var historyType: PrefName = when (type.lowercase(Locale.ROOT)) { private var historyType: PrefName = when (type) {
"anime" -> PrefName.AnimeSearchHistory SearchType.ANIME -> PrefName.AnimeSearchHistory
"manga" -> PrefName.MangaSearchHistory SearchType.MANGA -> PrefName.MangaSearchHistory
else -> throw IllegalArgumentException("Invalid type") SearchType.CHARACTER -> PrefName.CharacterSearchHistory
SearchType.STAFF -> PrefName.StaffSearchHistory
SearchType.STUDIO -> PrefName.StudioSearchHistory
SearchType.USER -> PrefName.UserSearchHistory
} }
init { init {

View file

@ -5,5 +5,8 @@ import java.io.Serializable
data class Studio( data class Studio(
val id: String, val id: String,
val name: String, val name: String,
val isFavourite: Boolean?,
val favourites: Int?,
val imageUrl: String?,
var yearMedia: MutableMap<String, ArrayList<Media>>? = null var yearMedia: MutableMap<String, ArrayList<Media>>? = null
) : Serializable ) : Serializable

View file

@ -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<Studio>
) : RecyclerView.Adapter<StudioAdapter.StudioViewHolder>() {
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 }
}
}
}

View file

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

View file

@ -4,16 +4,19 @@ import android.content.Intent
import android.view.LayoutInflater import android.view.LayoutInflater
import android.view.ViewGroup import android.view.ViewGroup
import androidx.core.content.ContextCompat import androidx.core.content.ContextCompat
import androidx.core.view.isVisible
import androidx.recyclerview.widget.RecyclerView import androidx.recyclerview.widget.RecyclerView
import androidx.viewbinding.ViewBinding
import ani.dantotsu.databinding.ItemFollowerBinding import ani.dantotsu.databinding.ItemFollowerBinding
import ani.dantotsu.databinding.ItemFollowerGridBinding
import ani.dantotsu.loadImage import ani.dantotsu.loadImage
import ani.dantotsu.setAnimation import ani.dantotsu.setAnimation
class UsersAdapter(private val user: ArrayList<User>) : class UsersAdapter(private val user: MutableList<User>, private val grid: Boolean = false) :
RecyclerView.Adapter<UsersAdapter.UsersViewHolder>() { RecyclerView.Adapter<UsersAdapter.UsersViewHolder>() {
inner class UsersViewHolder(val binding: ItemFollowerBinding) : inner class UsersViewHolder(val binding: ViewBinding) :
RecyclerView.ViewHolder(binding.root) { RecyclerView.ViewHolder(binding.root) {
init { init {
itemView.setOnClickListener { itemView.setOnClickListener {
@ -27,6 +30,11 @@ class UsersAdapter(private val user: ArrayList<User>) :
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): UsersViewHolder { override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): UsersViewHolder {
return UsersViewHolder( return UsersViewHolder(
if (grid) ItemFollowerGridBinding.inflate(
LayoutInflater.from(parent.context),
parent,
false
) else
ItemFollowerBinding.inflate( ItemFollowerBinding.inflate(
LayoutInflater.from(parent.context), LayoutInflater.from(parent.context),
parent, parent,
@ -36,12 +44,21 @@ class UsersAdapter(private val user: ArrayList<User>) :
} }
override fun onBindViewHolder(holder: UsersViewHolder, position: Int) { override fun onBindViewHolder(holder: UsersViewHolder, position: Int) {
val b = holder.binding setAnimation(holder.binding.root.context, holder.binding.root)
setAnimation(b.root.context, b.root) val user = user.getOrNull(position) ?: return
val user = user[position] if (grid) {
b.profileUserAvatar.loadImage(user.pfp) val b = holder.binding as ItemFollowerGridBinding
b.profileBannerImage.loadImage(user.banner ?: user.pfp) b.profileUserAvatar.loadImage(user.pfp)
b.profileUserName.text = user.name 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 override fun getItemCount(): Int = user.size

View file

@ -37,6 +37,10 @@ enum class PrefName(val data: Pref) { //TODO: Split this into multiple files
AnimeSearchHistory(Pref(Location.General, Set::class, setOf<String>())), AnimeSearchHistory(Pref(Location.General, Set::class, setOf<String>())),
MangaSourcesOrder(Pref(Location.General, List::class, listOf<String>())), MangaSourcesOrder(Pref(Location.General, List::class, listOf<String>())),
MangaSearchHistory(Pref(Location.General, Set::class, setOf<String>())), MangaSearchHistory(Pref(Location.General, Set::class, setOf<String>())),
CharacterSearchHistory(Pref(Location.General, Set::class, setOf<String>())),
StaffSearchHistory(Pref(Location.General, Set::class, setOf<String>())),
StudioSearchHistory(Pref(Location.General, Set::class, setOf<String>())),
UserSearchHistory(Pref(Location.General, Set::class, setOf<String>())),
NovelSourcesOrder(Pref(Location.General, List::class, listOf<String>())), NovelSourcesOrder(Pref(Location.General, List::class, listOf<String>())),
CommentNotificationInterval(Pref(Location.General, Int::class, 0)), CommentNotificationInterval(Pref(Location.General, Int::class, 0)),
AnilistNotificationInterval(Pref(Location.General, Int::class, 3)), AnilistNotificationInterval(Pref(Location.General, Int::class, 3)),

View file

@ -68,13 +68,18 @@ internal class ExtensionGithubApi {
PrefManager.getVal<Set<String>>(PrefName.AnimeExtensionRepos).toMutableList() PrefManager.getVal<Set<String>>(PrefName.AnimeExtensionRepos).toMutableList()
repos.forEach { repos.forEach {
val repoUrl = if (it.contains("index.min.json")) {
it
} else {
"$it${if (it.endsWith('/')) "" else "/"}index.min.json"
}
try { try {
val githubResponse = try { val githubResponse = try {
networkService.client networkService.client
.newCall(GET("${it}/index.min.json")) .newCall(GET(repoUrl))
.awaitSuccess() .awaitSuccess()
} catch (e: Throwable) { } catch (e: Throwable) {
Logger.log("Failed to get repo: $it") Logger.log("Failed to get repo: $repoUrl")
Logger.log(e) Logger.log(e)
null null
} }

View file

@ -1,111 +0,0 @@
<?xml version="1.0" encoding="utf-8"?>
<androidx.core.widget.NestedScrollView xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent"
android:layout_height="match_parent">
<FrameLayout
android:layout_width="match_parent"
android:layout_height="match_parent">
<com.google.android.material.appbar.AppBarLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:background="?attr/colorSurface">
<TextView
android:id="@+id/studioTitle"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_margin="16dp"
android:ellipsize="marquee"
android:fontFamily="@font/poppins_bold"
android:singleLine="true"
android:textAlignment="center"
android:textColor="@color/bg_opp"
android:textSize="20sp"
tools:text="@string/name" />
</com.google.android.material.appbar.AppBarLayout>
<LinearLayout
android:layout_width="match_parent"
android:layout_height="match_parent"
android:layout_marginTop="64dp"
android:orientation="vertical">
<TextView
android:id="@+id/charactersText"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginStart="8dp"
android:layout_marginEnd="32dp"
android:fontFamily="@font/poppins_bold"
android:padding="8dp"
android:text="@string/characters"
android:textSize="18sp"
android:visibility="gone" />
<ani.dantotsu.FadingEdgeRecyclerView
android:id="@+id/charactersRecycler"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:clipToPadding="false"
android:nestedScrollingEnabled="true"
android:paddingStart="20dp"
android:paddingEnd="20dp"
android:requiresFadingEdge="horizontal"
android:visibility="gone"
tools:itemCount="4"
tools:layoutManager="androidx.recyclerview.widget.GridLayoutManager"
tools:listitem="@layout/item_media_compact"
tools:orientation="horizontal"
tools:visibility="visible" />
<androidx.recyclerview.widget.RecyclerView
android:id="@+id/studioRecycler"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:clipToPadding="false"
android:paddingTop="16dp"
android:visibility="gone"
app:layout_behavior="com.google.android.material.appbar.AppBarLayout$ScrollingViewBehavior"
tools:itemCount="2"
tools:layoutManager="androidx.recyclerview.widget.LinearLayoutManager"
tools:listitem="@layout/item_media_compact"
tools:orientation="vertical"
tools:visibility="visible" />
</LinearLayout>
<ProgressBar
android:id="@+id/studioProgressBar"
style="?android:attr/progressBarStyle"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_margin="32dp"
app:layout_behavior="@string/appbar_scrolling_view_behavior"
tools:visibility="gone" />
<androidx.cardview.widget.CardView
android:id="@+id/studioClose"
android:layout_width="32dp"
android:layout_height="32dp"
android:layout_gravity="end"
android:layout_margin="16dp"
android:translationZ="2dp"
app:cardBackgroundColor="@color/nav_bg"
app:cardCornerRadius="16dp">
<androidx.constraintlayout.utils.widget.ImageFilterView
android:layout_width="24dp"
android:layout_height="24dp"
android:layout_gravity="center"
android:src="@drawable/ic_round_close_24"
tools:ignore="ContentDescription" />
</androidx.cardview.widget.CardView>
</FrameLayout>
</androidx.core.widget.NestedScrollView>

View file

@ -104,20 +104,75 @@
android:indeterminate="true" android:indeterminate="true"
app:layout_behavior="@string/appbar_scrolling_view_behavior" /> app:layout_behavior="@string/appbar_scrolling_view_behavior" />
<ani.dantotsu.FadingEdgeRecyclerView <androidx.core.widget.NestedScrollView
android:id="@+id/characterRecyclerView"
android:layout_width="match_parent" android:layout_width="match_parent"
android:layout_height="wrap_content" android:layout_height="match_parent"
android:clipToPadding="false" app:layout_behavior="@string/appbar_scrolling_view_behavior">
android:padding="16dp"
android:requiresFadingEdge="vertical" <LinearLayout
android:visibility="gone" android:layout_width="match_parent"
app:layout_behavior="@string/appbar_scrolling_view_behavior" android:layout_height="wrap_content"
app:spanCount="2" android:orientation="vertical"
tools:itemCount="10" android:paddingStart="20dp"
tools:layoutManager="GridLayoutManager" android:paddingEnd="20dp"
tools:listitem="@layout/item_media_compact" app:layout_behavior="@string/appbar_scrolling_view_behavior">
tools:orientation="vertical" />
<TextView
android:id="@+id/authorCharacterDesc"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginStart="32dp"
android:layout_marginEnd="32dp"
android:layout_marginBottom="32dp"
android:alpha="0.58"
tools:ignore="TextContrastCheck"
tools:maxLines="10"
tools:text="@tools:sample/lorem/random" />
<TextView
android:id="@+id/AuthorCharactersText"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginStart="8dp"
android:layout_marginEnd="32dp"
android:fontFamily="@font/poppins_bold"
android:padding="8dp"
android:text="@string/characters"
android:textSize="18sp"
android:visibility="gone"
app:layout_behavior="@string/appbar_scrolling_view_behavior" />
<ani.dantotsu.FadingEdgeRecyclerView
android:id="@+id/authorCharactersRecycler"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:clipToPadding="false"
android:nestedScrollingEnabled="true"
android:requiresFadingEdge="horizontal"
android:visibility="gone"
tools:itemCount="4"
tools:layoutManager="androidx.recyclerview.widget.GridLayoutManager"
tools:listitem="@layout/item_media_compact"
tools:orientation="horizontal"
tools:visibility="visible" />
<ani.dantotsu.FadingEdgeRecyclerView
android:id="@+id/characterRecyclerView"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:clipToPadding="false"
android:padding="16dp"
android:requiresFadingEdge="vertical"
android:visibility="gone"
app:layout_behavior="@string/appbar_scrolling_view_behavior"
app:spanCount="2"
tools:itemCount="10"
tools:layoutManager="GridLayoutManager"
tools:listitem="@layout/item_media_compact"
tools:orientation="vertical" />
</LinearLayout>
</androidx.core.widget.NestedScrollView>
<androidx.cardview.widget.CardView <androidx.cardview.widget.CardView
android:id="@+id/characterClose" android:id="@+id/characterClose"

View file

@ -0,0 +1,117 @@
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="vertical"
android:padding="16dp">
<Button
android:id="@+id/animeSearch"
style="@style/Widget.Material3.Button.TextButton"
android:layout_width="match_parent"
android:layout_height="48dp"
android:layout_marginBottom="16dp"
android:fontFamily="@font/poppins_bold"
android:insetTop="0dp"
android:insetBottom="0dp"
android:paddingStart="32dp"
android:paddingEnd="32dp"
android:text="@string/anime"
android:textSize="17sp"
android:textAlignment="viewStart"
android:textAllCaps="false"
android:textColor="?attr/colorOnBackground"
app:cornerRadius="0dp" />
<Button
android:id="@+id/mangaSearch"
style="@style/Widget.Material3.Button.TextButton"
android:layout_width="match_parent"
android:layout_height="48dp"
android:layout_marginBottom="16dp"
android:fontFamily="@font/poppins_bold"
android:insetTop="0dp"
android:insetBottom="0dp"
android:paddingStart="32dp"
android:paddingEnd="32dp"
android:text="@string/manga"
android:textSize="17sp"
android:textAlignment="viewStart"
android:textAllCaps="false"
android:textColor="?attr/colorOnBackground"
app:cornerRadius="0dp" />
<Button
android:id="@+id/userSearch"
style="@style/Widget.Material3.Button.TextButton"
android:layout_width="match_parent"
android:layout_height="48dp"
android:layout_marginBottom="16dp"
android:fontFamily="@font/poppins_bold"
android:insetTop="0dp"
android:insetBottom="0dp"
android:paddingStart="32dp"
android:paddingEnd="32dp"
android:text="@string/users"
android:textSize="17sp"
android:textAlignment="viewStart"
android:textAllCaps="false"
android:textColor="?attr/colorOnBackground"
app:cornerRadius="0dp" />
<Button
android:id="@+id/characterSearch"
style="@style/Widget.Material3.Button.TextButton"
android:layout_width="match_parent"
android:layout_height="48dp"
android:layout_marginBottom="16dp"
android:fontFamily="@font/poppins_bold"
android:insetTop="0dp"
android:insetBottom="0dp"
android:paddingStart="32dp"
android:paddingEnd="32dp"
android:text="@string/characters"
android:textSize="17sp"
android:textAlignment="viewStart"
android:textAllCaps="false"
android:textColor="?attr/colorOnBackground"
app:cornerRadius="0dp" />
<Button
android:id="@+id/staffSearch"
style="@style/Widget.Material3.Button.TextButton"
android:layout_width="match_parent"
android:layout_height="48dp"
android:layout_marginBottom="16dp"
android:fontFamily="@font/poppins_bold"
android:insetTop="0dp"
android:insetBottom="0dp"
android:paddingStart="32dp"
android:paddingEnd="32dp"
android:text="@string/staff"
android:textSize="17sp"
android:textAlignment="viewStart"
android:textAllCaps="false"
android:textColor="?attr/colorOnBackground"
app:cornerRadius="0dp" />
<Button
android:id="@+id/studioSearch"
style="@style/Widget.Material3.Button.TextButton"
android:layout_width="match_parent"
android:layout_height="48dp"
android:layout_marginBottom="16dp"
android:fontFamily="@font/poppins_bold"
android:insetTop="0dp"
android:insetBottom="0dp"
android:paddingStart="32dp"
android:paddingEnd="32dp"
android:text="@string/studios"
android:textSize="17sp"
android:textAlignment="viewStart"
android:textAllCaps="false"
android:textColor="?attr/colorOnBackground"
app:cornerRadius="0dp" />
</LinearLayout>

View file

@ -624,6 +624,9 @@
<string name="age">"__Age:__ "</string> <string name="age">"__Age:__ "</string>
<string name="birthday">\n"__Birthday:__ "</string> <string name="birthday">\n"__Birthday:__ "</string>
<string name="gender">\n"__Gender:__ "</string>" <string name="gender">\n"__Gender:__ "</string>"
<string name="hometown">\n"__Home Town:__ "</string>
<string name="years_active">\n"__Years Active:__ "</string>
<string name="date_of_death">\n"__Date of Death:__ "</string>
<string name="male">Male</string> <string name="male">Male</string>
<string name="female">Female</string> <string name="female">Female</string>
@ -1097,5 +1100,6 @@ Non quae tempore quo provident laudantium qui illo dolor vel quia dolor et exerc
<string name="privacy_policy">Privacy Policy</string> <string name="privacy_policy">Privacy Policy</string>
<string name="privacy_policy_desc">Read our privacy policy</string> <string name="privacy_policy_desc">Read our privacy policy</string>
<string name="failed_to_load">Failed to load</string> <string name="failed_to_load">Failed to load</string>
<string name="studios">Studios</string>
</resources> </resources>