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.currContext
import ani.dantotsu.media.Author
import ani.dantotsu.media.Character
import ani.dantotsu.media.Media
import ani.dantotsu.media.Studio
import ani.dantotsu.profile.User
import java.io.Serializable
data class SearchResults(
interface SearchResults<T> {
var search: String?
var page: Int
var results: MutableList<T>
var hasNextPage: Boolean
}
data class AniMangaSearchResults(
val type: String,
var isAdult: Boolean,
var onList: Boolean? = null,
var perPage: Int? = null,
var search: String? = null,
var countryOfOrigin: String? = null,
var sort: String? = null,
var genres: MutableList<String>? = null,
@ -23,10 +33,11 @@ data class SearchResults(
var seasonYear: Int? = null,
var startYear: Int? = null,
var season: String? = null,
var page: Int = 1,
var results: MutableList<Media>,
var hasNextPage: Boolean,
) : Serializable {
override var search: String? = null,
override var page: Int = 1,
override var results: MutableList<Media>,
override var hasNextPage: Boolean,
) : SearchResults<Media>, Serializable {
fun toChipList(): List<SearchChip> {
val list = mutableListOf<SearchChip>()
sort?.let {
@ -108,4 +119,33 @@ data class SearchResults(
val type: 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
Logger.log("Remaining requests: $remaining")
println("Remaining requests: $remaining")
if (json.code == 429) {
val retry = json.headers["Retry-After"]?.toIntOrNull() ?: -1
val passedLimitReset = json.headers["X-RateLimit-Reset"]?.toLongOrNull() ?: 0

View file

@ -44,7 +44,8 @@ class AnilistQueries {
val response: Query.Viewer?
measureTimeMillis {
response = executeQuery(
"""{Viewer{name options{timezone titleLanguage staffNameLanguage activityMergeTime airingNotifications displayAdultContent restrictMessagesToFollowing} avatar{medium} bannerImage id mediaListOptions{scoreFormat rowOrder animeList{customLists} mangaList{customLists}} statistics{anime{episodesWatched} manga{chaptersRead}} unreadNotificationCount}}""")
"""{Viewer{name options{timezone titleLanguage staffNameLanguage activityMergeTime airingNotifications displayAdultContent restrictMessagesToFollowing} avatar{medium} bannerImage id mediaListOptions{scoreFormat rowOrder animeList{customLists} mangaList{customLists}} statistics{anime{episodesWatched} manga{chaptersRead}} unreadNotificationCount}}"""
)
}.also { println("time : $it") }
val user = response?.data?.user ?: return false
@ -96,12 +97,10 @@ class AnilistQueries {
fun mediaDetails(media: Media): Media {
media.cameFromContinue = false
val query =
"""{Media(id:${media.id}){id favourites popularity episodes chapters streamingEpisodes {title thumbnail url site} mediaListEntry{id status score(format:POINT_100)progress private notes repeat customLists updatedAt startedAt{year month day}completedAt{year month day}}reviews(perPage:3, sort:SCORE_DESC){nodes{id mediaId mediaType summary body(asHtml:true) rating ratingAmount userRating score private siteUrl createdAt updatedAt user{id name bannerImage avatar{medium large}}}}isFavourite siteUrl idMal nextAiringEpisode{episode airingAt}source countryOfOrigin format duration season seasonYear startDate{year month day}endDate{year month day}genres studios(isMain:true){nodes{id name siteUrl}}description trailer{site id}synonyms tags{name rank isMediaSpoiler}characters(sort:[ROLE,FAVOURITES_DESC],perPage:25,page:1){edges{role voiceActors { id name { first middle last full native userPreferred } image { large medium } languageV2 } node{id image{medium}name{userPreferred}isFavourite}}}relations{edges{relationType(version:2)node{id idMal mediaListEntry{progress private score(format:POINT_100)status}episodes chapters nextAiringEpisode{episode}popularity meanScore isAdult isFavourite format title{english romaji userPreferred}type status(version:2)bannerImage coverImage{large}}}}staffPreview:staff(perPage:8,sort:[RELEVANCE,ID]){edges{role node{id image{large medium}name{userPreferred}}}}recommendations(sort:RATING_DESC){nodes{mediaRecommendation{id idMal mediaListEntry{progress private score(format:POINT_100)status}episodes chapters nextAiringEpisode{episode}meanScore isAdult isFavourite format title{english romaji userPreferred}type status(version:2)bannerImage coverImage{large}}}}externalLinks{url site}}Page(page:1){pageInfo{total perPage currentPage lastPage hasNextPage}mediaList(isFollowing:true,sort:[STATUS],mediaId:${media.id}){id status score(format: POINT_100) progress progressVolumes user{id name avatar{large medium}}}}}"""
runBlocking {
val anilist = async {
var response = executeQuery<Query.Media>(query, force = true)
var response =
executeQuery<Query.Media>(fullMediaInformation(media.id), force = true)
if (response != null) {
fun parse() {
val fetchedMedia = response?.data?.media ?: return
@ -291,7 +290,10 @@ class AnilistQueries {
val firstStudio = get(0)
media.anime.mainStudio = Studio(
firstStudio.id.toString(),
firstStudio.name ?: "N/A"
firstStudio.name ?: "N/A",
firstStudio.isFavourite ?: false,
firstStudio.favourites ?: 0,
null
)
}
}
@ -333,7 +335,11 @@ class AnilistQueries {
if (response.data?.media != null) parse()
else {
snackString(currContext()?.getString(R.string.adult_stuff))
response = executeQuery(query, force = true, useToken = false)
response = executeQuery(
fullMediaInformation(media.id),
force = true,
useToken = false
)
if (response?.data?.media != null) parse()
else snackString(currContext()?.getString(R.string.what_did_you_open))
}
@ -400,8 +406,6 @@ class AnilistQueries {
return media
}
private suspend fun favMedia(anime: Boolean, id: Int? = Anilist.userid): ArrayList<Media> {
var hasNextPage = true
var page = 0
@ -426,8 +430,6 @@ class AnilistQueries {
return responseArray
}
suspend fun getUserStatus(): ArrayList<User>? {
val toShow: List<Boolean> =
PrefManager.getVal(PrefName.HomeLayout)
@ -485,19 +487,23 @@ class AnilistQueries {
return list.toCollection(ArrayList())
} else return null
}
private fun favMediaQuery(anime: Boolean, page: Int, id: Int? = Anilist.userid): String {
return """User(id:${id}){id favourites{${if (anime) "anime" else "manga"}(page:$page){pageInfo{hasNextPage}edges{favouriteOrder node{id idMal isAdult mediaListEntry{ progress private score(format:POINT_100) status } chapters isFavourite format episodes nextAiringEpisode{episode}meanScore isFavourite format startDate{year month day} title{english romaji userPreferred}type status(version:2)bannerImage coverImage{large}}}}}}"""
return """User(id:${id}){id favourites{${if (anime) "anime" else "manga"}(page:$page){$standardPageInformation edges{favouriteOrder node{id idMal isAdult mediaListEntry{ progress private score(format:POINT_100) status } chapters isFavourite format episodes nextAiringEpisode{episode}meanScore isFavourite format startDate{year month day} title{english romaji userPreferred}type status(version:2)bannerImage coverImage{large}}}}}}"""
}
private fun recommendationQuery(): String {
return """ Page(page: 1, perPage:30) { pageInfo { total currentPage hasNextPage } recommendations(sort: RATING_DESC, onList: true) { rating userRating mediaRecommendation { id idMal isAdult mediaListEntry { progress private score(format:POINT_100) status } chapters isFavourite format episodes nextAiringEpisode {episode} popularity meanScore isFavourite format title {english romaji userPreferred } type status(version: 2) bannerImage coverImage { large } } } } """
return """ Page(page: 1, perPage:30) { $standardPageInformation recommendations(sort: RATING_DESC, onList: true) { rating userRating mediaRecommendation { id idMal isAdult mediaListEntry { progress private score(format:POINT_100) status } chapters isFavourite format episodes nextAiringEpisode {episode} popularity meanScore isFavourite format title {english romaji userPreferred } type status(version: 2) bannerImage coverImage { large } } } } """
}
private fun recommendationPlannedQuery(type: String): String {
return """ MediaListCollection(userId: ${Anilist.userid}, type: $type, status: PLANNING${if (type == "ANIME") ", sort: MEDIA_POPULARITY_DESC" else ""} ) { lists { entries { media { id mediaListEntry { progress private score(format:POINT_100) status } idMal type isAdult popularity status(version: 2) chapters episodes nextAiringEpisode {episode} meanScore isFavourite format bannerImage coverImage{large} title { english romaji userPreferred } } } } }"""
}
private fun continueMediaQuery(type: String, status: String): String {
return """ MediaListCollection(userId: ${Anilist.userid}, type: $type, status: $status , sort: UPDATED_TIME ) { lists { entries { progress private score(format:POINT_100) status media { id idMal type isAdult status chapters episodes nextAiringEpisode {episode} meanScore isFavourite format bannerImage coverImage{large} title { english romaji userPreferred } } } } } """
}
suspend fun initHomePage(): Map<String, ArrayList<Media>> {
val removeList = PrefManager.getCustomVal("removeList", setOf<Int>())
val hidePrivate = PrefManager.getVal<Boolean>(PrefName.HidePrivate)
@ -888,7 +894,7 @@ class AnilistQueries {
return null
}
suspend fun search(
suspend fun searchAniManga(
type: String,
page: Int? = null,
perPage: Int? = null,
@ -910,52 +916,7 @@ class AnilistQueries {
id: Int? = null,
hd: Boolean = false,
adultOnly: Boolean = false
): SearchResults? {
val query = """
query (${"$"}page: Int = 1, ${"$"}id: Int, ${"$"}type: MediaType, ${"$"}isAdult: Boolean = false, ${"$"}search: String, ${"$"}format: [MediaFormat], ${"$"}status: MediaStatus, ${"$"}countryOfOrigin: CountryCode, ${"$"}source: MediaSource, ${"$"}season: MediaSeason, ${"$"}seasonYear: Int, ${"$"}year: String, ${"$"}onList: Boolean, ${"$"}yearLesser: FuzzyDateInt, ${"$"}yearGreater: FuzzyDateInt, ${"$"}episodeLesser: Int, ${"$"}episodeGreater: Int, ${"$"}durationLesser: Int, ${"$"}durationGreater: Int, ${"$"}chapterLesser: Int, ${"$"}chapterGreater: Int, ${"$"}volumeLesser: Int, ${"$"}volumeGreater: Int, ${"$"}licensedBy: [String], ${"$"}isLicensed: Boolean, ${"$"}genres: [String], ${"$"}excludedGenres: [String], ${"$"}tags: [String], ${"$"}excludedTags: [String], ${"$"}minimumTagRank: Int, ${"$"}sort: [MediaSort] = [POPULARITY_DESC, SCORE_DESC, START_DATE_DESC]) {
Page(page: ${"$"}page, perPage: ${perPage ?: 50}) {
pageInfo {
total
perPage
currentPage
lastPage
hasNextPage
}
media(id: ${"$"}id, type: ${"$"}type, season: ${"$"}season, format_in: ${"$"}format, status: ${"$"}status, countryOfOrigin: ${"$"}countryOfOrigin, source: ${"$"}source, search: ${"$"}search, onList: ${"$"}onList, seasonYear: ${"$"}seasonYear, startDate_like: ${"$"}year, startDate_lesser: ${"$"}yearLesser, startDate_greater: ${"$"}yearGreater, episodes_lesser: ${"$"}episodeLesser, episodes_greater: ${"$"}episodeGreater, duration_lesser: ${"$"}durationLesser, duration_greater: ${"$"}durationGreater, chapters_lesser: ${"$"}chapterLesser, chapters_greater: ${"$"}chapterGreater, volumes_lesser: ${"$"}volumeLesser, volumes_greater: ${"$"}volumeGreater, licensedBy_in: ${"$"}licensedBy, isLicensed: ${"$"}isLicensed, genre_in: ${"$"}genres, genre_not_in: ${"$"}excludedGenres, tag_in: ${"$"}tags, tag_not_in: ${"$"}excludedTags, minimumTagRank: ${"$"}minimumTagRank, sort: ${"$"}sort, isAdult: ${"$"}isAdult) {
id
idMal
isAdult
status
chapters
episodes
nextAiringEpisode {
episode
}
type
genres
meanScore
isFavourite
format
bannerImage
coverImage {
large
extraLarge
}
title {
english
romaji
userPreferred
}
mediaListEntry {
progress
private
score(format: POINT_100)
status
}
}
}
}
""".replace("\n", " ").replace(""" """, "")
): AniMangaSearchResults? {
val variables = """{"type":"$type","isAdult":$isAdult
${if (adultOnly) ""","isAdult":true""" else ""}
${if (onList != null) ""","onList":$onList""" else ""}
@ -1000,8 +961,9 @@ query (${"$"}page: Int = 1, ${"$"}id: Int, ${"$"}type: MediaType, ${"$"}isAdult:
}]"""
else ""
}
}""".replace("\n", " ").replace(""" """, "")
val response = executeQuery<Query.Page>(query, variables, true)?.data?.page
}""".prepare()
val response =
executeQuery<Query.Page>(aniMangaSearch(perPage), variables, true)?.data?.page
if (response?.media != null) {
val responseArray = arrayListOf<Media>()
response.media?.forEach { i ->
@ -1021,7 +983,7 @@ query (${"$"}page: Int = 1, ${"$"}id: Int, ${"$"}type: MediaType, ${"$"}isAdult:
val pageInfo = response.pageInfo ?: return null
return SearchResults(
return AniMangaSearchResults(
type = type,
perPage = perPage,
search = search,
@ -1047,6 +1009,169 @@ query (${"$"}page: Int = 1, ${"$"}id: Int, ${"$"}type: MediaType, ${"$"}isAdult:
return null
}
suspend fun searchCharacters(page: Int, search: String?): CharacterSearchResults? {
if (search.isNullOrBlank()) return null
val query = """
{
Page(page: $page, perPage: $ITEMS_PER_PAGE) {
$standardPageInformation
characters(search: "$search") {
${characterInformation(false)}
}
}
}
""".prepare()
val response = executeQuery<Query.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> {
val combinedList = arrayListOf<Media>()
media1?.media?.mapTo(combinedList) { Media(it) }
@ -1071,26 +1196,65 @@ query (${"$"}page: Int = 1, ${"$"}id: Int, ${"$"}type: MediaType, ${"$"}isAdult:
val countryFilter = country?.let { "countryOfOrigin:$it, " } ?: ""
return buildString {
append("""Page(page:1,perPage:50){pageInfo{hasNextPage total}media(sort:$sort, type:$type, $formatFilter $countryFilter $includeList $isAdult){id idMal status chapters episodes nextAiringEpisode{episode} isAdult type meanScore isFavourite format bannerImage countryOfOrigin coverImage{large} title{english romaji userPreferred} mediaListEntry{progress private score(format:POINT_100) status}}}""")
append("""Page(page:1,perPage:50){$standardPageInformation media(sort:$sort, type:$type, $formatFilter $countryFilter $includeList $isAdult){id idMal status chapters episodes nextAiringEpisode{episode} isAdult type meanScore isFavourite format bannerImage countryOfOrigin coverImage{large} title{english romaji userPreferred} mediaListEntry{progress private score(format:POINT_100) status}}}""")
}
}
private fun recentAnimeUpdates(page: Int): String {
val currentTime = System.currentTimeMillis() / 1000
return buildString {
append("""Page(page:$page,perPage:50){pageInfo{hasNextPage total}airingSchedules(airingAt_greater:0 airingAt_lesser:${currentTime - 10000} sort:TIME_DESC){episode airingAt media{id idMal status chapters episodes nextAiringEpisode{episode} isAdult type meanScore isFavourite format bannerImage countryOfOrigin coverImage{large} title{english romaji userPreferred} mediaListEntry{progress private score(format:POINT_100) status}}}}""")
append("""Page(page:$page,perPage:50){$standardPageInformation airingSchedules(airingAt_greater:0 airingAt_lesser:${currentTime - 10000} sort:TIME_DESC){episode airingAt media{id idMal status chapters episodes nextAiringEpisode{episode} isAdult type meanScore isFavourite format bannerImage countryOfOrigin coverImage{large} title{english romaji userPreferred} mediaListEntry{progress private score(format:POINT_100) status}}}}""")
}
}
private fun queryAnimeList(): String {
return buildString {
append("""{recentUpdates:${recentAnimeUpdates(1)} recentUpdates2:${recentAnimeUpdates(2)} trendingMovies:${buildQueryString("POPULARITY_DESC", "ANIME", "MOVIE")} topRated:${buildQueryString("SCORE_DESC", "ANIME")} mostFav:${buildQueryString("FAVOURITES_DESC", "ANIME")}}""")
append(
"""{recentUpdates:${recentAnimeUpdates(1)} recentUpdates2:${recentAnimeUpdates(2)} trendingMovies:${
buildQueryString(
"POPULARITY_DESC",
"ANIME",
"MOVIE"
)
} topRated:${
buildQueryString(
"SCORE_DESC",
"ANIME"
)
} mostFav:${buildQueryString("FAVOURITES_DESC", "ANIME")}}"""
)
}
}
private fun queryMangaList(): String {
return buildString {
append("""{trendingManga:${buildQueryString("POPULARITY_DESC", "MANGA", country = "JP")} trendingManhwa:${buildQueryString("POPULARITY_DESC", "MANGA", country = "KR")} trendingNovel:${buildQueryString("POPULARITY_DESC", "MANGA", format = "NOVEL", country = "JP")} topRated:${buildQueryString("SCORE_DESC", "MANGA")} mostFav:${buildQueryString("FAVOURITES_DESC", "MANGA")}}""")
append(
"""{trendingManga:${
buildQueryString(
"POPULARITY_DESC",
"MANGA",
country = "JP"
)
} trendingManhwa:${
buildQueryString(
"POPULARITY_DESC",
"MANGA",
country = "KR"
)
} trendingNovel:${
buildQueryString(
"POPULARITY_DESC",
"MANGA",
format = "NOVEL",
country = "JP"
)
} topRated:${
buildQueryString(
"SCORE_DESC",
"MANGA"
)
} mostFav:${buildQueryString("FAVOURITES_DESC", "MANGA")}}"""
)
}
}
@ -1145,7 +1309,6 @@ query (${"$"}page: Int = 1, ${"$"}id: Int, ${"$"}type: MediaType, ${"$"}isAdult:
list
}
suspend fun recentlyUpdated(
greater: Long = 0,
lesser: Long = System.currentTimeMillis() / 1000 - 10000
@ -1153,10 +1316,7 @@ query (${"$"}page: Int = 1, ${"$"}id: Int, ${"$"}type: MediaType, ${"$"}isAdult:
suspend fun execute(page: Int = 1): Page? {
val query = """{
Page(page:$page,perPage:50) {
pageInfo {
hasNextPage
total
}
$standardPageInformation
airingSchedules(
airingAt_greater: $greater
airingAt_lesser: $lesser
@ -1165,35 +1325,11 @@ Page(page:$page,perPage:50) {
episode
airingAt
media {
id
idMal
status
chapters
episodes
nextAiringEpisode { episode }
isAdult
type
meanScore
isFavourite
format
bannerImage
countryOfOrigin
coverImage { large }
title {
english
romaji
userPreferred
}
mediaListEntry {
progress
private
score(format: POINT_100)
status
}
${standardMediaInformation()}
}
}
}
}""".replace("\n", " ").replace(""" """, "")
}""".prepare()
return executeQuery<Query.Page>(query, force = true)?.data?.page
}
@ -1221,68 +1357,37 @@ Page(page:$page,perPage:50) {
suspend fun getCharacterDetails(character: Character): Character {
val query = """ {
Character(id: ${character.id}) {
id
age
gender
description
dateOfBirth {
year
month
day
}
media(page: 0,sort:[POPULARITY_DESC,SCORE_DESC]) {
pageInfo {
total
perPage
currentPage
lastPage
hasNextPage
}
edges {
id
characterRole
node {
id
idMal
isAdult
status
chapters
episodes
nextAiringEpisode { episode }
type
meanScore
isFavourite
format
bannerImage
countryOfOrigin
coverImage { large }
title {
english
romaji
userPreferred
}
mediaListEntry {
progress
private
score(format: POINT_100)
status
}
}
}
}
${characterInformation(true)}
}
}""".replace("\n", " ").replace(""" """, "")
executeQuery<Query.Character>(query, force = true)?.data?.character?.apply {
character.age = age
character.gender = gender
character.description = description
character.dateOfBirth = dateOfBirth
character.roles = arrayListOf()
media?.edges?.forEach { i ->
val m = Media(i)
m.relation = i.characterRole.toString()
character.roles?.add(m)
}
}""".prepare()
executeQuery<Query.Character>(query, force = true)?.data?.character?.let { i ->
return Character(
i.id,
i.name?.full,
i.image?.large ?: i.image?.medium,
null,
null.toString(),
i.isFavourite ?: false,
i.description,
i.age,
i.gender,
i.dateOfBirth,
i.media?.edges?.map {
val m = Media(it)
m.relation = it.characterRole.toString()
m
}?.let { ArrayList(it) },
i.media?.edges?.flatMap { edge ->
edge.voiceActors?.map { va ->
Author(
va.id,
va.name?.userPreferred,
va.image?.large ?: va.image?.medium,
va.languageV2
)
} ?: emptyList()
} as ArrayList<Author>?
)
}
return character
}
@ -1290,45 +1395,9 @@ Page(page:$page,perPage:50) {
suspend fun getStudioDetails(studio: Studio): Studio {
fun query(page: Int = 0) = """ {
Studio(id: ${studio.id}) {
id
media(page: $page,sort:START_DATE_DESC) {
pageInfo{
hasNextPage
}
edges {
id
node {
id
idMal
isAdult
status
chapters
episodes
nextAiringEpisode { episode }
type
meanScore
startDate{ year }
isFavourite
format
bannerImage
countryOfOrigin
coverImage { large }
title {
english
romaji
userPreferred
}
mediaListEntry {
progress
private
score(format: POINT_100)
status
}
}
}
}
${studioInformation(page, ITEMS_PER_PAGE)}
}
}""".replace("\n", " ").replace(""" """, "")
}""".prepare()
var hasNextPage = true
val yearMedia = mutableMapOf<String, ArrayList<Media>>()
@ -1363,66 +1432,15 @@ Page(page:$page,perPage:50) {
suspend fun getAuthorDetails(author: Author): Author {
fun query(page: Int = 0) = """ {
Staff(id: ${author.id}) {
id
staffMedia(page: $page,sort:START_DATE_DESC) {
pageInfo{
hasNextPage
}
edges {
staffRole
id
node {
id
idMal
isAdult
status
chapters
episodes
nextAiringEpisode { episode }
type
meanScore
startDate{ year }
isFavourite
format
bannerImage
countryOfOrigin
coverImage { large }
title {
english
romaji
userPreferred
}
mediaListEntry {
progress
private
score(format: POINT_100)
status
}
}
}
}
${staffInformation(page, ITEMS_PER_PAGE)}
characters(page: $page,sort:FAVOURITES_DESC) {
pageInfo{
hasNextPage
}
$standardPageInformation
nodes{
id
name {
first
middle
last
full
native
userPreferred
}
image {
large
medium
}
${characterInformation(false)}
}
}
}
}""".replace("\n", " ").replace(""" """, "")
}""".prepare()
var hasNextPage = true
val yearMedia = mutableMapOf<String, ArrayList<Media>>()
@ -1433,6 +1451,16 @@ Page(page:$page,perPage:50) {
val query = executeQuery<Query.Author>(
query(page), force = true
)?.data?.author
author.age = query?.age
author.yearsActive =
if (query?.yearsActive?.isEmpty() == true) null else query?.yearsActive
author.homeTown = if (query?.homeTown?.isBlank() == true) null else query?.homeTown
author.dateOfDeath = if (query?.dateOfDeath?.toStringOrEmpty()
?.isBlank() == true
) null else query?.dateOfDeath?.toStringOrEmpty()
author.dateOfBirth = if (query?.dateOfBirth?.toStringOrEmpty()
?.isBlank() == true
) null else query?.dateOfBirth?.toStringOrEmpty()
hasNextPage = query?.staffMedia?.let {
it.edges?.forEach { i ->
i.node?.apply {
@ -1480,14 +1508,14 @@ Page(page:$page,perPage:50) {
sort: String = "SCORE_DESC"
): Query.ReviewsResponse? {
return executeQuery<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
)
}
suspend fun getUserProfile(id: Int): 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
)
}
@ -1513,7 +1541,7 @@ Page(page:$page,perPage:50) {
}
private fun userFavMediaQuery(anime: Boolean, id: Int): String {
return """User(id:${id}){id favourites{${if (anime) "anime" else "manga"}(page:1){pageInfo{hasNextPage}edges{favouriteOrder node{id idMal isAdult mediaListEntry{ progress private score(format:POINT_100) status } chapters isFavourite format episodes nextAiringEpisode{episode}meanScore isFavourite format startDate{year month day} title{english romaji userPreferred}type status(version:2)bannerImage coverImage{large}}}}}}"""
return """User(id:${id}){id favourites{${if (anime) "anime" else "manga"}(page:1){$standardPageInformation edges{favouriteOrder node{id idMal isAdult mediaListEntry{ progress private score(format:POINT_100) status } chapters isFavourite format episodes nextAiringEpisode{episode}meanScore isFavourite format startDate{year month day} title{english romaji userPreferred}type status(version:2)bannerImage coverImage{large}}}}}}"""
}
suspend fun userFollowing(id: Int): Query.Following? {
@ -1535,7 +1563,7 @@ Page(page:$page,perPage:50) {
"""{
favoriteAnime:${userFavMediaQuery(true, id)}
favoriteManga:${userFavMediaQuery(false, id)}
}""".trimIndent(), force = true
}""".prepare(), force = true
)
}
@ -1549,7 +1577,7 @@ Page(page:$page,perPage:50) {
val typeIn = "type_in:[AIRING,MEDIA_MERGE,MEDIA_DELETION,MEDIA_DATA_CHANGE]"
val reset = if (resetNotification) "true" else "false"
val res = executeQuery<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
)
if (res != null && resetNotification) {
@ -1571,7 +1599,8 @@ Page(page:$page,perPage:50) {
else if (userId != null) "userId:$userId,"
else if (global) "isFollowing:false,hasRepliesOrTypeText:true,"
else "isFollowing:true,"
val typeIn = if (filter == "isFollowing:true,") "type_in:[TEXT,ANIME_LIST,MANGA_LIST,MEDIA_LIST]," else ""
val typeIn =
if (filter == "isFollowing:true,") "type_in:[TEXT,ANIME_LIST,MANGA_LIST,MEDIA_LIST]," else ""
return executeQuery<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}}}}}}""",
force = true

View file

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

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?,
// 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
// @SerialName("voiceActorRoles") var voiceActorRoles: List<StaffRoleType>?,

View file

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

View file

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

View file

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

View file

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

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 image: 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 character: ArrayList<Character>? = null
var character: ArrayList<Character>? = null,
var isFav: Boolean = false
) : Serializable

View file

@ -1,11 +1,13 @@
package ani.dantotsu.media
import android.content.Intent
import android.os.Bundle
import android.view.View
import android.view.ViewGroup
import androidx.activity.viewModels
import androidx.appcompat.app.AppCompatActivity
import androidx.core.content.ContextCompat
import androidx.core.math.MathUtils.clamp
import androidx.core.view.updateLayoutParams
import androidx.core.view.updatePadding
import androidx.lifecycle.MutableLiveData
@ -16,57 +18,127 @@ import androidx.recyclerview.widget.LinearLayoutManager
import ani.dantotsu.EmptyAdapter
import ani.dantotsu.R
import ani.dantotsu.Refresh
import ani.dantotsu.databinding.ActivityAuthorBinding
import ani.dantotsu.connections.anilist.Anilist
import ani.dantotsu.connections.anilist.AnilistMutations
import ani.dantotsu.databinding.ActivityCharacterBinding
import ani.dantotsu.initActivity
import ani.dantotsu.loadImage
import ani.dantotsu.navBarHeight
import ani.dantotsu.openLinkInBrowser
import ani.dantotsu.others.ImageViewDialog
import ani.dantotsu.others.SpoilerPlugin
import ani.dantotsu.others.getSerialized
import ani.dantotsu.px
import ani.dantotsu.settings.saving.PrefManager
import ani.dantotsu.settings.saving.PrefName
import ani.dantotsu.snackString
import ani.dantotsu.statusBarHeight
import ani.dantotsu.themes.ThemeManager
import com.google.android.material.appbar.AppBarLayout
import io.noties.markwon.Markwon
import io.noties.markwon.SoftBreakAddsNewLinePlugin
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.launch
import kotlinx.coroutines.withContext
import kotlin.math.abs
class AuthorActivity : AppCompatActivity() {
private lateinit var binding: ActivityAuthorBinding
class AuthorActivity : AppCompatActivity(), AppBarLayout.OnOffsetChangedListener {
private lateinit var binding: ActivityCharacterBinding
private val scope = lifecycleScope
private val model: OtherDetailsViewModel by viewModels()
private var author: Author? = null
private lateinit var author: Author
private var loaded = false
private var screenWidth: Float = 0f
private val percent = 30
private var mMaxScrollSize = 0
private var isCollapsed = false
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
ThemeManager(this).applyTheme()
binding = ActivityAuthorBinding.inflate(layoutInflater)
binding = ActivityCharacterBinding.inflate(layoutInflater)
setContentView(binding.root)
initActivity(this)
this.window.statusBarColor = ContextCompat.getColor(this, R.color.nav_bg)
screenWidth = resources.displayMetrics.run { widthPixels / density }
if (PrefManager.getVal(PrefName.ImmersiveMode)) this.window.statusBarColor =
ContextCompat.getColor(this, R.color.transparent)
val screenWidth = resources.displayMetrics.run { widthPixels / density }
val banner =
if (PrefManager.getVal(PrefName.BannerAnimations)) binding.characterBanner else binding.characterBannerNoKen
binding.root.updateLayoutParams<ViewGroup.MarginLayoutParams> { topMargin += statusBarHeight }
binding.studioRecycler.updatePadding(bottom = 64f.px + navBarHeight)
binding.studioTitle.isSelected = true
banner.updateLayoutParams { height += statusBarHeight }
binding.characterClose.updateLayoutParams<ViewGroup.MarginLayoutParams> { topMargin += statusBarHeight }
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.studioTitle.text = author?.name
binding.studioClose.setOnClickListener {
binding.characterClose.setOnClickListener {
onBackPressedDispatcher.onBackPressed()
}
author = intent.getSerialized("author") ?: return
binding.characterTitle.text = author.name
binding.characterCoverImage.loadImage(author.image)
binding.characterCoverImage.setOnLongClickListener {
ImageViewDialog.newInstance(
this,
author.name,
author.image
)
}
val link = "https://anilist.co/staff/${author.id}"
binding.characterShare.setOnClickListener {
val i = Intent(Intent.ACTION_SEND)
i.type = "text/plain"
i.putExtra(Intent.EXTRA_TEXT, link)
startActivity(Intent.createChooser(i, author.name))
}
binding.characterShare.setOnLongClickListener {
openLinkInBrowser(link)
true
}
lifecycleScope.launch {
withContext(Dispatchers.IO) {
author.isFav =
Anilist.query.isUserFav(AnilistMutations.FavType.STAFF, author.id)
}
withContext(Dispatchers.Main) {
binding.characterFav.setImageResource(
if (author.isFav) R.drawable.ic_round_favorite_24 else R.drawable.ic_round_favorite_border_24
)
}
}
binding.characterFav.setOnClickListener {
scope.launch {
lifecycleScope.launch {
if (Anilist.mutation.toggleFav(AnilistMutations.FavType.CHARACTER, author.id)) {
author.isFav = !author.isFav
binding.characterFav.setImageResource(
if (author.isFav) R.drawable.ic_round_favorite_24 else R.drawable.ic_round_favorite_border_24
)
} else {
snackString("Failed to toggle favorite")
}
}
}
}
model.getAuthor().observe(this) {
if (it != null) {
author = it
loaded = true
binding.studioProgressBar.visibility = View.GONE
binding.studioRecycler.visibility = View.VISIBLE
if (author!!.yearMedia.isNullOrEmpty()) {
binding.studioRecycler.visibility = View.GONE
binding.characterProgress.visibility = View.GONE
binding.characterRecyclerView.visibility = View.VISIBLE
if (author.yearMedia.isNullOrEmpty()) {
binding.characterRecyclerView.visibility = View.GONE
}
val titlePosition = arrayListOf<Int>()
val concatAdapter = ConcatAdapter()
val map = author!!.yearMedia ?: return@observe
val map = author.yearMedia ?: return@observe
val keys = map.keys.toTypedArray()
var pos = 0
@ -80,6 +152,10 @@ class AuthorActivity : AppCompatActivity() {
}
}
}
val desc = createDesc(author)
val markWon = Markwon.builder(this).usePlugin(SoftBreakAddsNewLinePlugin.create())
.usePlugin(SpoilerPlugin()).build()
markWon.setMarkdown(binding.authorCharacterDesc, desc)
for (i in keys.indices) {
val medias = map[keys[i]]!!
val empty = if (medias.size >= 4) medias.size % 4 else 4 - medias.size
@ -90,18 +166,18 @@ class AuthorActivity : AppCompatActivity() {
concatAdapter.addAdapter(MediaAdaptor(0, medias, this, true))
concatAdapter.addAdapter(EmptyAdapter(empty))
}
binding.studioRecycler.adapter = concatAdapter
binding.studioRecycler.layoutManager = gridLayoutManager
binding.characterRecyclerView.adapter = concatAdapter
binding.characterRecyclerView.layoutManager = gridLayoutManager
binding.charactersRecycler.visibility = View.VISIBLE
binding.charactersText.visibility = View.VISIBLE
binding.charactersRecycler.adapter =
CharacterAdapter(author!!.character ?: arrayListOf())
binding.charactersRecycler.layoutManager =
binding.authorCharactersRecycler.visibility = View.VISIBLE
binding.AuthorCharactersText.visibility = View.VISIBLE
binding.authorCharactersRecycler.adapter =
CharacterAdapter(author.character ?: arrayListOf())
binding.authorCharactersRecycler.layoutManager =
LinearLayoutManager(this, LinearLayoutManager.HORIZONTAL, false)
if (author!!.character.isNullOrEmpty()) {
binding.charactersRecycler.visibility = View.GONE
binding.charactersText.visibility = View.GONE
if (author.character.isNullOrEmpty()) {
binding.authorCharactersRecycler.visibility = View.GONE
binding.AuthorCharactersText.visibility = View.GONE
}
}
}
@ -109,14 +185,28 @@ class AuthorActivity : AppCompatActivity() {
live.observe(this) {
if (it) {
scope.launch {
if (author != null)
withContext(Dispatchers.IO) { model.loadAuthor(author!!) }
withContext(Dispatchers.IO) { model.loadAuthor(author) }
live.postValue(false)
}
}
}
}
private fun createDesc(author: Author): String {
val age = if (author.age != null) "${getString(R.string.age)} ${author.age}" else ""
val yearsActive =
if (author.yearsActive != null) "${getString(R.string.years_active)} ${author.yearsActive}" else ""
val dob =
if (author.dateOfBirth != null) "${getString(R.string.birthday)} ${author.dateOfBirth}" else ""
val homeTown =
if (author.homeTown != null) "${getString(R.string.hometown)} ${author.homeTown}" else ""
val dod =
if (author.dateOfDeath != null) "${getString(R.string.date_of_death)} ${author.dateOfDeath}" else ""
return "$age $yearsActive $dob $homeTown $dod"
}
override fun onDestroy() {
if (Refresh.activity.containsKey(this.hashCode())) {
Refresh.activity.remove(this.hashCode())
@ -125,7 +215,31 @@ class AuthorActivity : AppCompatActivity() {
}
override fun onResume() {
binding.studioProgressBar.visibility = if (!loaded) View.VISIBLE else View.GONE
binding.characterProgress.visibility = if (!loaded) View.VISIBLE else View.GONE
super.onResume()
}
override fun onOffsetChanged(appBar: AppBarLayout, i: Int) {
if (mMaxScrollSize == 0) mMaxScrollSize = appBar.totalScrollRange
val percentage = abs(i) * 100 / mMaxScrollSize
val cap = clamp((percent - percentage) / percent.toFloat(), 0f, 1f)
binding.characterCover.scaleX = 1f * cap
binding.characterCover.scaleY = 1f * cap
binding.characterCover.cardElevation = 32f * cap
binding.characterCover.visibility =
if (binding.characterCover.scaleX == 0f) View.GONE else View.VISIBLE
val immersiveMode: Boolean = PrefManager.getVal(PrefName.ImmersiveMode)
if (percentage >= percent && !isCollapsed) {
isCollapsed = true
if (immersiveMode) this.window.statusBarColor =
ContextCompat.getColor(this, R.color.nav_bg)
}
if (percentage <= percent && isCollapsed) {
isCollapsed = false
if (immersiveMode) this.window.statusBarColor =
ContextCompat.getColor(this, R.color.transparent)
}
}
}

View file

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

View file

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

View file

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

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

View file

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

View file

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

View file

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

View file

@ -5,5 +5,8 @@ import java.io.Serializable
data class Studio(
val id: String,
val name: String,
val isFavourite: Boolean?,
val favourites: Int?,
val imageUrl: String?,
var yearMedia: MutableMap<String, ArrayList<Media>>? = null
) : 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.ViewGroup
import androidx.core.content.ContextCompat
import androidx.core.view.isVisible
import androidx.recyclerview.widget.RecyclerView
import androidx.viewbinding.ViewBinding
import ani.dantotsu.databinding.ItemFollowerBinding
import ani.dantotsu.databinding.ItemFollowerGridBinding
import ani.dantotsu.loadImage
import ani.dantotsu.setAnimation
class UsersAdapter(private val user: ArrayList<User>) :
class UsersAdapter(private val user: MutableList<User>, private val grid: Boolean = false) :
RecyclerView.Adapter<UsersAdapter.UsersViewHolder>() {
inner class UsersViewHolder(val binding: ItemFollowerBinding) :
inner class UsersViewHolder(val binding: ViewBinding) :
RecyclerView.ViewHolder(binding.root) {
init {
itemView.setOnClickListener {
@ -27,6 +30,11 @@ class UsersAdapter(private val user: ArrayList<User>) :
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): UsersViewHolder {
return UsersViewHolder(
if (grid) ItemFollowerGridBinding.inflate(
LayoutInflater.from(parent.context),
parent,
false
) else
ItemFollowerBinding.inflate(
LayoutInflater.from(parent.context),
parent,
@ -36,12 +44,21 @@ class UsersAdapter(private val user: ArrayList<User>) :
}
override fun onBindViewHolder(holder: UsersViewHolder, position: Int) {
val b = holder.binding
setAnimation(b.root.context, b.root)
val user = user[position]
b.profileUserAvatar.loadImage(user.pfp)
b.profileBannerImage.loadImage(user.banner ?: user.pfp)
b.profileUserName.text = user.name
setAnimation(holder.binding.root.context, holder.binding.root)
val user = user.getOrNull(position) ?: return
if (grid) {
val b = holder.binding as ItemFollowerGridBinding
b.profileUserAvatar.loadImage(user.pfp)
b.profileUserName.text = user.name
b.profileCompactScoreBG.isVisible = false
b.profileInfo.isVisible = false
b.profileCompactProgressContainer.isVisible = false
} else {
val b = holder.binding as ItemFollowerBinding
b.profileUserAvatar.loadImage(user.pfp)
b.profileBannerImage.loadImage(user.banner ?: user.pfp)
b.profileUserName.text = user.name
}
}
override fun getItemCount(): Int = user.size

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>())),
MangaSourcesOrder(Pref(Location.General, List::class, listOf<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>())),
CommentNotificationInterval(Pref(Location.General, Int::class, 0)),
AnilistNotificationInterval(Pref(Location.General, Int::class, 3)),

View file

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

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"
app:layout_behavior="@string/appbar_scrolling_view_behavior" />
<ani.dantotsu.FadingEdgeRecyclerView
android:id="@+id/characterRecyclerView"
<androidx.core.widget.NestedScrollView
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" />
android:layout_height="match_parent"
app:layout_behavior="@string/appbar_scrolling_view_behavior">
<LinearLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="vertical"
android:paddingStart="20dp"
android:paddingEnd="20dp"
app:layout_behavior="@string/appbar_scrolling_view_behavior">
<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
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="birthday">\n"__Birthday:__ "</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="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_desc">Read our privacy policy</string>
<string name="failed_to_load">Failed to load</string>
<string name="studios">Studios</string>
</resources>